首页
论坛
课程
招聘
[原创]Android elf hook的方式
2020-1-2 12:58 5979

[原创]Android elf hook的方式

2020-1-2 12:58
5979

got/plt hook

自己写了之后觉得,got和plt应该分开更好一些。got hook有爱奇艺的xhook,个人或者github上一些开源的got hook。爱奇艺说是plt hook,个人觉得很疑惑,看实现也是替换got表中的地址值,和plt没太大关系吧。

 

plt hook我知道的应该是Facebook:https://github.com/facebookincubator/profilo/tree/master/deps/plthooks
待测试,应该是替换/hook plt的代码,所以应该可以获取到的函数地址还是真实函数的地址,所以可以绕过hook检测。但同时缺陷也比较明显,如果先获取函数地址,在直接调用这个函数地址/指针,则不经过plt,那么就无法hook。

 

图片描述
图片描述

 

直接取got表中的值,不经过plt。

 

所以各有优缺点,plt hook有inline hook的原理,比got hook到的几率更小。

 

plt可以理解为就是一个跳板
图片描述

 

got表存储绝对地址
图片描述

 

plt的延迟绑定在Android上面并没有什么体现,所以简单理解为跳板即可。

只能hook显示导入的函数

对于dlsym获取函数再调用的无效,可以hook dlsym过滤。如果没有显示引用函数a,但是有dlsym调用,那么plt/got中没有函数a,只能过滤dlsym。

特征太明显

通过获取函数地址,和函数应该所在的so在maps中的地址做对比,不在so的代码中,绝大多数就是被hook了。

隐藏特征

可以hook读maps文件,过滤为合理的地址,但是实际应该自己分析有没有影响到一些正常功能的逻辑,比如你把libc.so的地址改变了,会不会有其他的正常代码去读取这个内存地址的内容或者解析elf等。

 

还有一个方式,是发现很多检测方式存在不合理的地方,通过maps确定libc.so的地址,只是线性的获取,从第一个到最后一个。那么其实可以再次加载libc.so,这样maps中有两个so了,而只要hook函数的地址在第二个libc.so和第一个libc.so之间即可。所以说写代码一定要逻辑严谨。

导出表hook

这个叫法可能会有些歧义。
got表其实可以看做既有导入也有导出,导入的就是其他so的一些函数,比如libc的open,导出就是供其他so调用的一些函数,有的在got表中也存在。这里说的导出表hook不是指这种,因为这和got表hook是一样的,不过确实存在一些情况,本so的导出函数被本so的其他函数调用但是不是直接采用偏移而是走plt、got,所以这种的got表hook可以实现hook本so的导出函数。
图片描述

 

在linker加载so的时候会把要导入的函数绝对地址写入got表,dlsym也会返回一个导出函数的绝对地址。分析这两个过程发现都是通过解析导出函数的so,通过基址+偏移值确定函数绝对地址的。所以把这个偏移值修改,指向hook函数即可完成hook。

 

导出表hook后,通过日志查看,open函数还是真实地址,dlsym返回的dlopen是hook函数,执行也触发hook了。高版本open和open64是同一个函数。
图片描述

 

再次加载so,可以不是同一个so,主要是触发init函数。got表中的open函数为上次的hook函数。因为再次导出表hook了,所以dlsym返回的open函数为新的hook函数。
图片描述

 

通过这两次测试,得出结论:
1、因为libc.so的open函数已经被导出表hook了,所以之后加载的so如果导入open函数,那么不用再进行got表hook了,linker会把hook函数地址写入got表。但是对已经加载过的so,因为got已经写入完成,无法hook。
2、dlsym获取函数也会返回hook函数地址,对所有so生效,已加载和未加载的。

 

可以发现导出表hook和got表hook是相反的且互补的。
使用got表hook只能对已加载的so的got表进行修改,未加载的无法完成,当然可以定时扫描新加载了哪些so或者hook dlopen(但是依赖的so的加载拦截不到),但都不是很完美。且对一些在init、init_array、JNI_OnLoad函数内的hook很难生效,因为时机不可控。对dlsym方式,got表hook无效,不过可以hook dlsym做一个变通。

 

使用导出表hook弥补了这些不足,不同于got表hook对需要导入函数的so进行修改,而是对导出函数的so进行修改。所以一次hook可以对之后加载的so都生效,但对已经加载的so,解析好got表的就无效了。对dlsym同样生效。

 

所以结合优缺点应该got表和导出表hook同时使用,能起到不错的效果。但是好像没什么人实现导出表hook(可能是我孤陋寡闻了),写了个简单的测试导出表hook,抽时间完善开源。

 

对于特征检测和隐藏和got表其实是一样的。

 

注:谢谢@Amun提醒,确实存在这种情况,如果是前向偏移是实现不了的,所以hook前需要判断hook函数和被hook函数/so的地址是向前还是向后偏移。

linux上较常用的LD_PRELOAD

这个在Android上好像没什么人用,我记得好像只见过梆梆的壳里对art的处理使用过。没啥难度,资料也多就不细说了。因为对于Android应用程序来说都是通过zygot/zygote64 fork出来的,这个LD_PRELOAD hook基本没什么用武之地,毕竟新的纯linux进程的情况在Android上太少用了,只在一些特殊情况使用,所以放在最后吧,有时间再说。

inline hook

有了got表hook和导出表hook为什么还要有inline hook,那是因为前面的hook都是对于导出函数而言的,多是用于系统api的hook,对于非导出函数就无能为力了。还有一个场景,在执行到某个函数的中间的某条指令想打印下/修改寄存器。以上的hook方式检测起来太容易。

 

已实现一个inline hook框架,关于inline hook参见如下链接。
https://bbs.pediy.com/thread-257020.htm

 

虽然inline hook能实现以上hook能做到也能实现不能做到的,但是其难度是较高的且并不像以上的hook方式那么保险,可能要建立在先逆向的基础上才能保证hook成功,比如函数指令长度至少要能放下一个跳板,不然覆盖了后面的指令造成无法预料的错误。

特征

因为是在原函数地址覆盖指令,所以没有改变函数地址,不存在以上的函数的特征。
检测常用的就是采集函数的指令,比如常见的hook框架的跳板或者正常函数的指令,甚至可以做个数据库保存每个厂家、版本系统的so的某个函数指令。而解析指令这种事情最好不要放在客户端,不然逻辑、想做什么都暴露了。

 

不太常用的还有,比如覆盖指令的时候需要修改一页内存至少为可写,当然一般都是读写、执行,所以在maps中这一页内存会被单独列出来,等于告诉别人这页内存我修改过了,那么这页内的函数多数也是被修改过了。

 

还有检测hook框架特征等等吧,比以上的hook来说检测起来会麻烦很多。当然inline hook的这些特征检测方式,以上几种hook也是可以用来检测的。

 

通过dlopen的一个问题也可以引出一种检测方式,检测lr寄存器。如果调用该函数a的上一个函数b被hook了,如果需要返回值,那么lr寄存器肯定不是函数b范围内的地址,那么通过检测这个地址就可以判断是否被hook。

异常hook

我是觉得这个是最接近完美的方法,可以解决inline hook的指令太短的问题。且只需备份一条指令,修复一条指令所需考虑的比inline hook修复多条指令要容易的多。

 

借助异常机制,捕获返回寄存器的数组,通过操作pc寄存器即可控制接下来的流程。
好像没见到这种hook框架(对我而言),打算近期完善开源。

 

特征的话和inline hook基本是一样的。

系统调用hook

以open函数为例,最终实现是通过软中断的方式和内核通信实现的,如果不通过api而是写汇编发送软中断来实现,那么基于系统api的hook是拦截不了的(这也可以算是一种检测/绕过hook的方式,因为可以不经过应用层的hook,所以是绕过hook,实现检测的话可以通过比较调用api和直接使用软中断结果是否不同,当然这个取决于hook是否做处理了)。如果逆向分析后hook也取决也是否封装成函数,如果不是函数而是插入调用函数中,就麻烦了。所以如果是对这种最终走到软中断的系统api做监控,那么在源头做监控就不会漏掉了,这也是其好处,但是同样坏处是这种方式基本上不能应用于app、通用hook框架上的,因为其实现要么在内核这写代码实现,要么通过内核模块。就算是有root权限等不需要前两种方式,那至少也要逆向分析内核吧。比较难做成一个通用hook框架,不过做沙箱、api监控还是要用的。

 

在内核中实现,那最简单的就是在接收到系统调用的代码中插入处理代码,不过每次修改都要重新编译,可能多数都是写demo验证下,使用内核模块的居多。

 

内核模块的一种思路是在内核中进行inline hook,把处理某个系统调用的函数inline hook了。一种思路是sys_call_table中存储了所有处理系统调用的函数,替换sys_call_table中的地址为hook函数即可。

 

如果采用替换sys_call_table的方式,也算比较简单吧。这个排在最后,因为对于我来说做监控的需求比较少,所以这个有时间在写吧。

特征

因为不是在应用层修改,所以对于应用来说基本没什么特征,检测的话可能只能检测系统、内核是否是修改过的,只能标记个风险吧。毕竟普通应用权限有限,无法直接接触到内核。

结尾

以上基本就是Android中对elf中函数、指令进行hook的方式(我了解的)。应该算是目前最全的Android elf hook方式了,这些方式的原理、优缺点、适用场景、特征、检测方式等都有说明,因为好像没发现有做过类似总结的,所以把这个做了,也是希望能把我以前的一些疑惑解答了同时帮了其他有类似疑惑的人。能做到不是只会用框架,而是了解原理、自己也能实现,不再有疑惑。如果能起到这么点作用也是很好了。剩余几种方式未放代码的,尽量近期实现开源(如果我确实有时间的话)。


[公告] 2021 KCTF 春季赛 防守方征题火热进行中!

最后于 2020-1-2 16:12 被卓桐编辑 ,原因:
收藏
点赞1
打赏
分享
最新回复 (6)
雪    币: 186
活跃值: 活跃值 (1063)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
葫芦娃 活跃值 1 2020-1-2 14:30
2
0
利用signal的hook之前写过一个,100行代码左右就可以实现基本功能。
后来github也有人开源了一个: https://github.com/dodola/TrapHook, 其实也是老技术了
雪    币: 6728
活跃值: 活跃值 (198)
能力值: ( LV13,RANK:393 )
在线值:
发帖
回帖
粉丝
卓桐 活跃值 4 2020-1-2 15:06
3
0
葫芦娃 利用signal的hook之前写过一个,100行代码左右就可以实现基本功能。 后来github也有人开源了一个: https://github.com/dodola/TrapHook, 其实也是老技 ...
嗯,这些hook方式都是老技术,不过很多都是零散的,很少有真正不需要自己修复、定制就可用的,或者太注重代码艺术、框架本身造成参与开发成本较高。还有就是那种hook方式最适合什么场景、优缺点等很少有人提及,写这个也主要是答疑,都不是新技术
雪    币: 1358
活跃值: 活跃值 (404)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
Amun 活跃值 2020-1-2 15:35
4
0
因为导出表的符号偏移是无符号类型,不能指哪打哪
雪    币: 6728
活跃值: 活跃值 (198)
能力值: ( LV13,RANK:393 )
在线值:
发帖
回帖
粉丝
卓桐 活跃值 4 2020-1-2 16:09
5
0
Amun 因为导出表的符号偏移是无符号类型,不能指哪打哪
多谢提醒,加上备注
雪    币: 34
活跃值: 活跃值 (88)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
winkar 活跃值 2020-4-30 15:52
7
0
关于got表的hook有个问题想请教大佬。
文章中,在got hook的检测里提到:“通过获取函数地址,和函数应该所在的so在maps中的地址做对比”
但我实际测试的时候发现一个问题:直接通过函数名取到的地址,跟调用时的地址是不同的。比如下面这行代码:
LOGD("HOOKTEST open %x %d", open, open("asd", O_RDONLY));

打印出的第一个open的地址仍然是hook前的地址,但调用的却是修改之后的open的地址。用IDA查看这个so发现,两个open分别是两个不同的got表项。所以导致了这个结果。

所以“获取函数地址”是否必须通过读取got表项完成?
顺带问一下,除了解析elf header之外,还有什么别的方法获取到got表项?
游客
登录 | 注册 方可回帖
返回