[TOC]
一、前言
最近一段时间在研究Android加壳和脱壳技术,其中涉及到了一些hook技术,于是将自己学习的一些hook技术进行了一下梳理,以便后面回顾和大家学习。
本文第二节主要讲述编译原理,了解编译原理可以帮助进一步理解hook技术
本文第三节主要讲述NDK开发的一些基础知识
本文第四节主要讲述各类hook技术的实现原理
本文第五节主要讲述各hook技术的实现步骤和案例演示
二、编译原理
1.编译过程
我们可以借助gcc来实现上面的过程:
1 |
预处理阶段:预处理器(cpp)根据以字符#开头的命令修给原始的C程序,结果得到另一个C程序,通常以.i作为文件扩展名。主要是进行文本替换、宏展开、删除注释这类简单工作。 |
这里我们对编译过程做了一个初步的讲解,详细大家可以去看《程序员的自我修养——链接、装载与库》一书,下面我们主要介绍链接方式、链接库、可执行目标文件几个基本概念。
(1)链接方式
静态链接:
1 |
对于静态库,程序在编译链接时,将库的代码链接到可执行文件中,程序运行时不再需要静态库。在使用过程中只需要将库和我们的程序编译后的文件链接在一起就可形成一个可执行文件。 |
缺点:
1 |
1、内存和磁盘空间浪费:静态链接方式对于计算机内存和磁盘的空间浪费十分严重。假如一个c语言的静态库大小为1MB,系统中有100个需要使用到该库文件,采用静态链接的话,就要浪费进100M的内存,若数量再大,那浪费的也就更多。 |
动态链接:
1 |
由于静态链接具有浪费内存和模块更新困难等问题,提出了动态链接。基本实现思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将他们链接在一起形成一个完整的程序,而不是像静态链接那样把所有的程序模块都链接成一个单独的可执行文件。所以动态链接是将链接过程推迟到了运行时才进行。 |
例子:
1 |
同样,假如有程序1,程序2,和Lib.o三个文件,程序1和程序2在执行时都需要用到Lib.o文件,当运行程序1时,系统首先加载程序1,当发现需要Lib.o文件时,也同样加载到内存,再去加载程序2当发现也同样需要用到Lib.o文件时,则不需要重新加载Lib.o,只需要将程序2和Lib.o文件链接起来即可,内存中始终只存在一份Lib.o文件。 |
优点:
1 |
(1)毋庸置疑的就是节省内存; |
(2)链接库
我们在链接的过程中,一般会链接一些库文件,主要分为静态链接库和动态链接库。静态链接库一般为Windows下的.lib和Linux下的.a
,动态链接库一般为Windows下的.dll和Linux下的.so
,这里考虑到我们主要是对so文件hook讲解,下面我们主要介绍linux系统下的情况。
静态库:
1 |
命名规范为libXXX.a |
动态库:
1 |
命名规范为libXXX.so |
2.可执行文件(ELF)
目前PC平台比较流行的可执行文件格式主要是Windows下的PE和Linux下的ELF,它们都是COFF格式的变种。在Windows平台下就是我们比较熟悉的.exe文件,而Linux平台下现在便是统称的ELF文件。这里我们主要介绍一下Linux下的ELF文件。
ELF文件的类型:
1 |
可重定位目标文件:包含二进制代码和数据,其形式可以和其他目标文件进行合并,创建一个可执行目标文件。比如linux下的.o文件 |
ELF文件的结构:
elf文件在不同的平台上有不同的格式,在Unix和x86-64 Linux上称ELF:
(1)ELF文件结构
目标文件既要参与程序链接,又要参与程序执行:
1 |
(1)文件开始处:是一个ELF头部(ELF Header),用来描述整个文件的组织。节区部分包含链接视图的大量信息:指令、数据、符号表、重定位信息等。 |
下面我们来从分别从连接视角和程序执行的视角来看ELF文件:
1 |
ELF Header:描述了描述了体系结构和操作系统等基本信息并指出Section Header Table和Program Header Table在文件中的什么位置 |
下面我们来看一张更加详细的ELF结构图
从中我们可以详细的知道ELF文件各个字段的含义,其他字段的含义如下图
(2)GOT和PLT
上面我们简单的分析了ELF的文件结构,而这里我们介绍一下其中两个重要的节表GOT(全局偏移表)
和PLT(程序链接表)
首先,我们需要理解为什么需要GOT表和PLT表
经过上面的分析,我们知道程序在经历了编译流程后,就来到了链接过程,链接过程就是将一个或者多个中间文件(.o文件)通过链接器将它们链接成一个可执行文件,主要要完成以下事情:
1 |
1.各个中间文之间的同名section合并 |
但是当我们程序运行起来,glibc
动态库也装载了,函数地址也确定了,那我们程序如何去调用动态库中的函数呢,这个时候就需要理解一下重定位的概念:
重定位:
1 |
1.链接重定位:将一个或多个中间文件(.o文件)通过链接器将它们链接成一个可执行文件,一般分为两种情况: |
这里我们就可以明白流程,程序在加载动态库中函数时,需要两部分:
1 |
需要存放外部函数的代码段表(PLT表) |
这里我用一个实例加深大家的理解,例如程序在链接时发现scanf定义在动态库时,链接器生成一小段代码scanf_stub,这就是我们的PLT表,然后scanf_stub地址取代原来的scanf,因此程序此时就转换为链接scanf_stub,这个过程叫链接重定位,然后在运行时动态库glibc中的scanf_libc地址填入GOT表,然后程序通过scanf_stub访问到scanf_libc,这个过程叫运行时重定位。
讲到这里,其实我们对PLT和GOT表的作用已经了解了,PLT(程序链接表)
就是链接时需要存放外部函数的数据段,GOT(全局偏移表)
是存放函数地址的代码
PLT和GOT的结构:
1 |
PLT表中的第一项为公共表项,剩下的是每个动态库函数为一项,每项PLT都从对应的GOT表项中读取目标函数地址 |
根据操作系统规定不允许修改代码段,只能修改数据段,所以PLT表是不变的,GOT表是可以改变的
.plt | 代码段 | RE(可读,可执行) | .plt section 实际就是通常所说的过程链接表(Procedure Linkage Table, PLT) |
---|---|---|---|
.plt.got | 代码段 | RE | .plt.got section 用于存放 __cxa_finalize 函数对应的 PLT 条目 |
.got | 数据段 | RW(可读,可写) | .got section 中可以用于存放全局变量的地址;.got section 中也可以用于存放不需要延迟绑定的函数的地址。 |
.got.plt | 数据段 | RW | .got.plt section 用于存放需要延迟绑定的函数的地址 |
因此我们可以看一下程序调用PLT表和GOT表的逻辑
最后我们来详细看一下程序调用函数的变化流程:
程序第一次调用函数时:
此时第一步由函数调用跳入到PLT表中,然后第二步PLT表跳到GOT表中,可以看到第三步由GOT表回跳到PLT表中,这时候进行压栈,把代表函数的ID压栈,接着第四步跳转到公共的PLT表项中,第5步进入到GOT表中,然后_dl_runtime_resolve对动态函数进行地址解析和重定位,第七步把动态函数真实的地址写入到GOT表项中,然后执行函数并返回,此时GOT表中就存放了函数的真实地址
之后函数被调用时:
第一步还是由函数调用跳入到PLT表,但是第二步跳入到GOT表中时,由于这个时候该表项已经是动态函数的真实地址了,所以可以直接执行然后返回
三、NDK基础知识
这里我们主要介绍Android中的so文件加载的原理,为后面hook技术讲解做铺垫:
1.Android so文件的类型
NDK开发的so不再具备跨平台特性,需要编译提供不同平台支持
我们从官网可以得知so文件在不同架构下也不同,这里依次对应arm32位和64位,x86_32位和64位
我们可以使用指令查看我们手机的架构:
1 |
adb shell |
2.so文件加载
Android中我们通常使用系统提供的两种API:System.loadLibrary或者System.load来加载so文件:
1 |
//加载的是libnative-lib.so,注意的是这边只需要传入"native-lib" |
System.loadLibrary()和System.load()的区别:
1 |
(1)loadLibray传入的是编译脚本指定生成的so文件名称,一般不需要包含开头的lib和结尾的.so,而load传入的是so文件所在的绝对路径 |
源码分析:
Android 6.0:
[System.java] java.lang.System:
1 |
public static void load(String pathName) { |
[Runtime.java] java.lang.Runtime:
1 |
void load(String absolutePath, ClassLoader loader) { |
我们对比了Android6.0下的System.load和System.loadLibrary:
1 |
我们可以发现System.loadLibrary()中会修改类加载器,这个在我们后面hook过程可能会报错,而Runtime.loadLibray()中有重写的方法,则可以正确实现 |
Android 7.0:
[System.java] java.lang.System:
1 |
public static void load(String filename) { |
[Runtime.java] java.lang.Runtime:
1 |
synchronized void load0(Class fromClass, String filename) { |
我们可以发现不同版本的区别:
1 |
Android 6.0采用的是loadLibrary,6.0之后都采用的是loadLibrary0; 同理 load函数也一样,6.0之后采用的是load0 |
同时我们分析了loadLibrary0:
1 |
1. classLoader存在时,通过classLoader.findLibrary(libraryName)来获取存放指定so文件的路径; |
四、各类hook技术原理分析
hook技术就是指截获进程对某个API函数的调用,使得API的执行流程转向我们实现的代码片段,从而实现我们要的功能,在Android中使用hook的方法有很多,常用的Xposed和frida hook技术、inlinehook技术、基于inlinehook的开源框架Sandhook、PLT/Got hook技术、以及当下模拟cpu的Unicorn的hook技术,下面我们将逐一介绍其原理。
1.Xposed hook技术
Xposed的基本原理,我在源码编译(3)——Xposed框架定制中已经给大家做了详细的讲解,其主要就是Android应用进程都是由 zygote 进程孵化而来,zygote对应的可执行程序就是app_process,posed 框架通过替换系统的 app_process 可执行文件以及虚拟机动态链接库,让 zygote 在启动应用程序进程时注入框架代码,进而实现对应用程序进程的劫持。
具体怎么实现hook技术,Xposed就是通过修改了Art虚拟机,将需要hook的函数注册为Native函数,当执行这一函数时,虚拟机会优先执行Native函数,然后执行java函数,这样就成功完成了函数的hook。
具体实现流程:
在 Android 系统启动的时候, zygote 进程加载 XposedBridge 将所有需要替换的 Method 通过 JNI 方法 hookMethodNative 指向 Native 方法 xposedCallHandler , xposedCallHandler 在转入 handleHookedMethod 这个 Java 方法执行用户规定的 Hook Func
dvmCallMethodV会根据accessFlags决定调用native还是java函数,因此修改accessFlags后,Dalvik会认为这个函数是一个native函数,便走向了native分支也就是说Xposed在对java方法进行hook时,先将虚拟机里面这个方法的Method的accessFlag改为native对应的值,然后将该方法的nativeFunc指向自己实现的一个native方法,这样方法在调用时,就会调用到这个native方法,接管了控制权
其他的就详细参考上篇文章了
2.Frida hook技术
frida 也是一种动态插桩工具,原理和Xposed hook一样,也是把java method转为native method,但是Art下的实现与Dalivk有所不同,这里就需要了解ART的运行机制,这里主要参考博客:Frida源码分析
ART 是一种代替 Dalivk 的新的运行时,它具有更高的执行效率。ART虚拟机执行 Java 方法主要有两种模式:quick code 模式和 Interpreter 模式
1 |
quick code 模式:执行 arm 汇编指令 |
即使是在quick code模式中,也有类方法可能需要以Interpreter模式执行。反之亦然。解释执行的类方法通过函数artInterpreterToCompiledCodeBridge的返回值调用本地机器指令执行的类方法;本地机器指令执行的类方法通过函数GetQuickToInterpreterBridge的返回值调用解释执行的类方法
这里引用博客中的一张图
如图,对于一个native方法,ART虚拟机会先尝试使用quickcode的模式去执行,并检查ARTMethod结构中的entry_point_from_quick_compiledcode成员,这里分3种情况:
1 |
1.如果函数已经存在quick code, 则指向这个函数对应的 quick code的起始地址,而当quick code不存在时,它的值则会代表其他的意义; |
因此,frida将一个java method修改jni mthod 显然是不存在quick code,这时需要将entry_point_from_quick_compiledcode值修改为art_quick_generic_jni_trampoline 的地址
总结,frida把java method改为jni method,需要修改ARTMethod结构体中的这几个值:
1 |
accessflags = native |
3.inlinehook 技术
(1)基本原理
首先,我们先介绍一下什么是inline Hook:
1 |
inline Hook是一种拦截目标函数调用的方法,主要用于杀毒软件、沙箱和恶意软件。一般的想法是将一个函数重定向到我们自己的函数,以便我们可以在函数执行它之前和/或之后执行处理;这可能包括:检查参数、填充、记录、欺骗返回的数据和过滤调用。 |
(2)inlineHook组成
1 |
hook:一个5字节的相对跳转,在被写入目标函数以钩住它,跳转将从被钩住的函数跳转到我们的代码 |
(3)inlineHook实现
从示意图上,我们可以这样理解:
1 |
我们将目标函数MessgeBoxA()中的地址拿出来,然后我们用重写的hook函数替换,然后我们执行完成之后,再回调到函数的执行地址出,保证程序的正常运行 |
我们也可以通过上述示意图去理解inlinehook的基本原理
(4)Android-Inline-Hook和SandHook 技术
Android-lnline-Hook和SandHook都是基于inlinehook的两种开源框架,在Android中对native层hook,使用的比较常见,前者主要针对32位进行hook,后者即可以用于32位也可以用于64位,但是官方表示32位并未进行测试,所以应用在64位上仍然更多
4.PLT/GOT hook技术
前面我们已经很详细的讲述了全局偏移表(GOT)和动态链接表(PLT),Inline Hook能Hook几乎所有函数,但是兼容性较差,不能达到上线标准,相比于inlineHook,GOT Hook兼容性比较好,可以达到上线标准,但是只能Hook基于GOT表的一些函数
GOT/PLT Hook 主要是通过解析SO文件,将待hook函数在got表的地址替换为自己函数的入口地址,这样目标进程每次调用待hook函数时,实际上是执行了我们自己的函数
这里我们还要理解GOT表中含包含了导入表和导出表
1 |
导出表指将当前动态库的一些函数符号保留,供外部调用 |
例如导入表存放的是一些其他so的函数,例如libc的open,而导出表存放的是一些共其他so调用的函数,比如自己so中编写的函数,而无论导入表还是导出表基本都是针对导出函数,针对非导出函用inlinehook更常用一些
5.Unicorn hook技术
Unicore是一款非常优秀的跨平台模拟执行框架,该框架可以跨平台执行Arm, Arm64 (Armv8), M68K, Mips, Sparc, & X86 (include X86_64)等指令集的原生程序,通过模拟CPU,可以实现很多强大的功能,也可以实现函数级别的Hook
参考资料:无名大佬文章Unicorn 在 Android 的应用
nicorn 内部并没有函数的概念,它只是一个单纯的CPU, 没有HOOK_FUNCTION的callback,AndroidNativeEmu 中的函数级Hook 并不是真正意义上的Hook,它不仅能Hook存在的函数,还能Hook不存在的函数。AndroidNativeEmu 使用这种技术实现了JNI函数Hook、库函数Hook。 Jni函数是不存的,Hook它只是为了能够用Python 实现 Jni Functions。有一些库函数是存在的,Hook只是为了重新实现它
五、各类hook技术实操
1.Xposed hook实操
(1)环境安装
Xposed环境安装详细可以参考我写的Xposed系列文章,这里只是简单的总结一下:
1 |
(1) 4.4以下Android版本安装比较简单,只需要两步即可 |
这里我们用的是nexus5进行操作,简单演示一下android6.0的Xposed安装
资源准备:
1 |
asop镜像:https://developers.google.com/android/ota#hammerhead |
首先我们先下载n5镜像,然后刷机,这里我们已经安装就不再安装了
然后我们刷入 twrp-3.4.0-0-hammerhead.img
1 |
fastboot flash recovery twrp-3.4.0-0-hammerhead.img |
然后我们就可以进入recovery模式了
然后我们将Supersu拷贝进去,然后将Xposed-v89-sdk.zip拷贝进去
然后我们进入recovery模式,将两个文件依次刷入即可
接下来我们安装XposedInstall.apk,来管理Xposed
如果我们开机后发现xposed框架没有激活,尝试再重启一下,我们可以看见
这样我们的Xposed框架就成功安装了
(2)Xposed插件编写
Xposed插件编写的流程网上已经有很多了,这里我就简单的讲解一下
1 |
基本流程: |
首先,我们查找XposedBridgeApi.jar到新建工程的libs目录:
然后,修改AndroidManifest.xml文件,在Application标签下增加内容如下:
1 |
<meta-data |
修改app目录下的build.gradle文件:
1 |
进入app目录下的build.gradle文件, |
编写hook类:
我们新建一个hook类xposed01,并实现接口IXposedHookLoadPackage,并实现里面关键方法handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam),该方法会在每个软件被启动的时候回调,所以一般需要通过目标包名过滤
1 |
public class Xposed01 implements IXposedHookLoadPackage { |
新建assets文件夹,然后在assets目录下新建文件xposed_init,在里面写上hook类的完整路径
这里面可以写多个hook类,每个类写一个,我们就完成了基本的Xposed框架的编写
最后勾选模块,并重启即可生效
我们可以发现我们的xposed插件生效了,将我们系统中进程名打印出来了,说明hook成功了
2.frida hook实操
(1)环境安装
frida安装,使用frida过程中我们可以安装objection来进一步助力我们的hook工作,这个参考肉丝大佬的知识星球
工具安装(也可以选用其他版本):
1 |
pip install frida==12.8.0 |
安装成功后,查看frida和objection,确定版本正确
1 |
frida --version |
然后将frida_server推送到/data/local/tmp
下,并启动:(下载地址:https://github.com/frida/frida/releases)
(2)frida使用
然后我们就可以使用自动化工具objection和编写js脚本进行hook了
objection使用(详细参考肉丝大佬github的教程):
1 |
常见的hook命令: |
编写脚本:
启动方式:
1 |
attach方式 frida -U com.example.test -l hook.js |
这样我们就可以成功注入了,更加复杂的脚本编写可以参考frida博客
详细案例实操,这里可以参考之前我的文章:Android恶意样本分析——frida破解三层锁机样本
3.inlinehook实操
这里我们分别实现基于inlinehook的两个开源框架的具体使用方法
(1)Android-lnine-Hook
开源地址:https://github.com/ele7enxxh/Android-Inline-Hook
该框架只能针对32位的so文件进行hook
我们对so文件进行hook时,可以按照如下步骤进行:
1 |
(1)查看so文件中的目标函数 |
<1>编写目标函数so文件
我们编写案例,很明显这里会打印失败,然后我们使用inline-hook框架进行hook
<2>导入文件
我们将该框架中如下文件导入我们的项目中
我们需要使用inlineHook文件夹,并把这些文件直接拷贝到我们的工作目录:
<3>修改配置文件
<4>编写hook代码
我们导入inlinehook头文件就可以开始编写hook代码了
编译,报错:
这是因为框架仅仅针对32位,所以我们需要在配置文件里面指定一下
然后编译,发现能正常通过
首先声明hook的就函数,然后编写对应的新函数,这里我们hook的是strstr函数
然后调用inlinehook进行hook
最后我们发现就可以成功的hook
代码分析:
1 |
源码解析: |
inlinehook框架使用正确姿势:
1 |
我们对一个目标so文件hook步骤如下: |
(2)SandHook实操
因为上面使用inline框架只支持32位,所以这里我们用SandHook实现对64位native函数的hook,sandHook既支持32位、又支持64位
开源地址:https://github.com/asLody/SandHook
同样是上面的案例,这里我们使用SandHook进行实操
<1>导入文件
我们此路径下SandHook/nativehook/src/main/cpp/
文件全部导入
<2>配置环境
首先我们在CMakeList中加入c文件
然后在java代码中修改导入的so库
直接编译,报错:
然后我们同理将配置信息加入:
1 |
cmake { |
再次编译成功
<3>编写hook代码
SandHook使用和上面inlinehook框架基本一样
首先声明旧的函数,编写新的函数(目标函数strstr)
然后进行hook
最后发现可以成功hook
SandHook使用姿势:
1 |
(1)导包,将SandHook中cpp文件夹下的包全部导入到项目中,并修改CMakeLists.txt中添加native.cpp, 修改java层导入so库为sandHook-native |
4.PLT/GOT hook实操
前面我们已经介绍了Got表hook的原理,下面我们实例操作一下导入表函数的hook
参考博客:https://www.cnblogs.com/goodhacker/p/9306997.html
原理:
1 |
通过解析elf格式,分析Section header table找出静态的.got表的位置,并在内存中找到相应的.got表位置,这个时候内存中.got表保存着导入函数的地址,读取目标函数地址,与.got表每一项函数入口地址进行匹配,找到的话就直接替换新的函数地址,这样就完成了一次导入表的Hook操作了 |
首先,我们编写demo
我们编译后使用010Editor打开libnative-lib.so
然后我们用ida打开,并直接跳转到该地址
在got表中我们找到对应的mywin0函数
<1>获得so模块的加载地址
我们可以使用/proc/self/maps
去获得so模块的加载地址
1 |
char line[1024]; |
<2>找到got表的位置
我们首先根据段头找到section_header的首地址
然后我们遍历这个表就可以找到.got,然后根据got表地址再轮训找到函数地址
因为这种方法不能在内存中直接找到段头,内存中会抹去段头,所以我们可以通过加载so文件来定位
<3>定位到节表的地址
然后我们来获得节表的地址:
1 |
//读取elf文件 |
我们打印一下此事shof的值,验证一下节表的地址
这里可以发现成功读取
<4>定位到got表的位置和函数位置
然后我们拿到字符串的偏移值进行定位到got表,再进一步定位到函数
1 |
//2.拿到字符串表 |
这里我们就可以发现成功的hook
got hook使用姿势:
1 |
(1)使用/proc/self/maps去获得so模块的加载地址 |
5.Unicorn hook使用
这里我们简单了解一下基于unicorn的框架Unidbg的hook使用
开源地址:https://github.com/zhkl0228/unidbg
这里我们直接idea将项目拉取下来,然后等下项目环境配置完成
配置完成后,我们直接启动里面的示例代码查看hook效果
这里unidbg使用了xHook,xHook是一种PLT hook的方式,当然这只是unidbg强大功能其中的一种,也是hook技术中一种,这里就简单介绍到这,后续再详细讲如何使用
unidbg使用参考博客:https://www.qinless.com/670
六、实验总结
本文从程序加载的原理出发,讲解了当下常用的一些基本的hook方式和手段,后续对其中一些hook方式再次深入讲解,实验的一些样本和代码会上传到知识星球和github,文章参考学了了很多大佬的文章和大佬星球的内容,参考文献放在末尾,有什么问题,就请各位大佬一一指出了。
github的地址:github
参考文献
参考书籍:
1 |
参考书目:《程序员的自我修养——链接、装载与库》 |
GOT和PLT:
1 |
https://www.geek-share.com/detail/2774116640.html |
hook技术
1 |
https://zhuanlan.zhihu.com/p/389889716 |
got/plt hook:
1 |
https://www.likecs.com/show-203321775.html |
- source:security-kitchen.com
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论