随着移动安全越来越火,各种调试工具也都层出不穷,但因为环境和需求的不同,并没有工具是万能的。另外工具是死的,人是活的,如果能搞懂工具的原理再结合上自身的经验,你也可以创造出属于自己的调试武器。因此,笔者将会在这一系列文章中分享一些自己经常用或原创的调试工具以及手段,希望能对国内移动安全的研究起到一些催化剂的作用。
目录如下:
安卓动态调试七种武器之长生剑 - Smali Instrumentation
安卓动态调试七种武器之孔雀翎 – Ida Pro
安卓动态调试七种武器之离别钩 – Hooking (上)
安卓动态调试七种武器之离别钩 – Hooking (下)
安卓动态调试七种武器之碧玉刀- Customized DVM
安卓动态调试七种武器之多情环- Customized Kernel
安卓动态调试七种武器之霸王枪 - Anti Anti-debugging
安卓动态调试七种武器之拳头 - Tricks & Summary
文章中所有提到的代码和工具都可以在我的github下载到,地址是: https://github.com/zhengmin1989/TheSevenWeapons
我们在离别钩(上)中已经可以做到动态的加载自定义so文件并且运行so文件中的函数了,但还不能做到hook目标函数,这里我们需要用到函数挂钩的技术来做到这一点。函数挂钩的基本原理是先用mprotect()将原代码段改成可读可写可执行,然后修改原函数入口处的代码,让pc指针跳转到动态加载的so文件中的hook函数中,执行完hook函数以后再让pc指针跳转回原本的函数中。
用来注入的程序hook5逻辑与之前的hook4相比并没有太大变化,仅仅少了“调用 dlclose 卸载so文件”这一个步骤,因为我们想要执行的hook后的函数在so中,所以并不需要调用dlclose进行卸载。基本步骤如下:
保存当前寄存器的状态 -> 获取目标程序的mmap, dlopen, dlsym, dlclose 地址 -> 调用mmap分配一段内存空间用来保存参数信息 –> 调用dlopen加载so文件 -> 调用dlsym找到目标函数地址 -> 使用ptrace_call执行目标函数 -> 恢复寄存器的状态
hook5的主要代码逻辑如下:
我们知道arm处理器支持两种指令集,一种是arm指令集,另一种是thumb指令集。所以要hook的函数可能是被编译成arm指令集的,也有可能是被编译成thumb指令集的。Thumb指令集可以看作是arm指令压缩形式的子集,它是为减小代码量而提出,具有16bit的代码密度。Thumb指令体系并不完整,只支持通用功能,必要时仍需要使用ARM指令,如进入异常时。需要注意的一点是thumb指令的长度是不固定的,但arm指令是固定的32位长度。
为了让大家更容易的理解hook的原理,我们先只考虑arm指令集,因为arm相比thumb要简单一点,不需要考虑指令长度的问题。所以我们需要将target和hook的so编译成arm指令集的形式。怎么做呢?很简单,只要在Android.mk中的文件名后面加上”.arm”即可 (真正的文件不用加)。
确定了指令集以后,我们来看实现挂钩最重要的逻辑,这个逻辑是在注入后的so里实现的。首先我们需要一个结构体保存汇编代码和hook地址:
我们接着来看注入的逻辑,最重要的函数为hook_direct(),他有三个参数,一个参数是我们最开始定义的用来保存汇编代码和hook地址的结构体,第二个是我们要hook的原函数的地址,第三个是我们用来执行hook的函数。函数的源码如下:
虽然android有ASLR,但并没有PIE,所以program image是固定在0x8000这个地址的,因此我们用mprotect()函数将整个target代码段变成RWX,这样我们就能修改函数入口处的代码了。是否修改成功可以通过cat /proc/[pid]/maps查看:
随后我们需要确定目标函数的地址,这个有两种方法。如果目标程序本身没有被strip的话,那些symbol都是存在的,因此可以使用dlopen()和dlsym()等方法来获取目标函数地址。但很多情况,目标程序都会被strip,特别是可以直接运行的二进制文件默认都会被直接strip。比如target中的sevenWeapons()这个函数名会在编译的时候去掉,所以我们使用dlsym()的话是无法找到这个函数的。这时候我们就需要使用ida或者objdump来定位一下目标函数的地址。比如我们用objdump找一下target程序里面sevenWeapons(int number)这个函数的地址:
虽然target这个binary被strip了,但还是可以找到sevenWeapons()这个函数的起始地址是在0x84f4。因为”mov r2, r0”就是加载number这个参数的指令,随后调用了<printf@plt>用来输出结果。 最后一个参数也就是我们要执行的hook函数的地址。得到这个地址非常简单,因为是so中的函数,调用hook_direct()的时候直接写上函数名即可。
hook_direct(&eph,hookaddr,my_sevenWeapons);
接下来我们看如何修改函数入口(modify function entry),首先我们保存一下原函数的地址和那个函数的前三条指令。随后我们把目标函数的第一条指令修改为 LDR pc, [pc, #0],这条指令的意思是跳转到PC指针所指的地址,由于pc寄存器读出的值实际上是当前指令地址加8,所以我们把后面两处指令都保存为hook函数的地址,这样的话,我们就能控制PC跳转到hook函数的地址了。
最后我们再调用hook_cacheflush()这个函数来刷新一下指令的缓存。因为虽然前面的操作修改了内存中的指令,但有可能被修改的指令已经被缓存起来了,再执行的话,CPU可能会优先执行缓存中的指令,使得修改的指令得不到执行。所以我们需要使用一个隐藏的系统调用来刷新一下缓存。hook_cacheflush()代码如下:
刷新完缓存后,再执行到原函数的时候,pc指针就会跳转到我们自定义的hook函数中了,hook函数里的代码如下:
首先在hook函数中,我们可以获得原函数的参数,并且我们可以对原函数的参数进行修改,比如说将数字乘2。随后我们使用hook_precall(&eph);将原本函数的内容进行还原。hook_precall()内容如下:
在hook_precall()中,我们先对原本的三条指令进行还原,然后使用hook_cacheflush()对内存进行刷新。经过处理之后,我们就可以执行原来的函数orig_sevenWeapons(number)了。执行完后,如果我们还想再次hook这个函数,就需要调用hook_postcall(&eph)将原本的三条指令再进行一次修改。
下面我们来使用hook5和libinject2.so来注入一下target这个程序:
可以看到经过注入后,我们成功的获取了参数number的值,并且将”Hello,LiBieGou!”后面的数字变成了原来的两倍。
我们在上一节中介绍了如何hook native层的函数。下面我们再来讲讲如何利用adbi来hook JNI层的函数。Adbi是一个android平台上的一个注入框架,本身是开源的。Hook的原理和我们之前介绍的技术是一样的,这个框架支持arm和thumb指令集,也支持通过字符串定位symbol函数的地址。
首先我们需要一个例子用来讲解,所以我写了程序叫test2。
点击程序中的button后,程序会调用so中的Java_com_mzheng_libiegou_test2_MainActivity_stringFromJNI(JNIEnv* env,jobject thiz,jint a,jint b)函数用来计算a+b的结果。我们默认传的参数是a=1, b=1。接下来我就来教你如何利用adbi来hook这个JNI函数。
因为adbi是一个注入框架,我们下载好源码后,只要对应着源码中给的example照猫画虎即可。Hijack那个文件夹是保存的用来注入的程序,和我们之前讲的hook5.c的原理是一样的,所以不用做任何修改。我们只需要修改example中的代码,也就是将要注入的so文件的源码。
首先,我们在/adbi-master/instruments/example这个文件夹下新建两个文件”hookjni.c”和” hookjni_arm.c”。“hookjni_arm.c”其实只是一个壳,用来将hook函数的入口编译成arm指令集的,内容如下:
这个文件的目的仅仅是为了用arm指令集进行编译,可以看到Android.mk中在”hookjni_arm.c”后面多了个”.arm”:
下面我们来看”hookjni.c”:
这段代码和我上一节讲的代码非常像,my_init()用来进行hook操作,我们需要提供想要hook的so文件名和函数名,然后再提供thumb指令集和arm指令集的hook函数地址。
my_Java_com_mzheng_libiegou_test2_MainActivity_stringFromJNI()就是我们提供的hook函数了。我们在这个hook函数中把a和b都改成了10。除此之外,我们还使用counter这个全局变量来控制hook的次数,这里我们把counter设置为3,当hook了3次以后,就不再进行hook操作了。
编辑好代码后,我们只需要在adbi的根目录下执行” build.sh”进行编译:
编译好后,我们把hijack和libexample.so拷贝到/data/local/tmp目录下。然后使用hijack进行注入:
然后我们再点击button就可以看到我们已经成功的修改了a和b的值为10了,最后显示1+1=20。
通过cat adbi_example.log我们可以看到hook过程中打印的log:
可以看到adbi是通过thumb指令集进行hook的,原因是test2程序是用thumb指令集进行编译的。其实hook thumb指令集和arm指令集差不多,在”/adbi-master/instruments/base”中可以找到hook thumb指令集的逻辑:
其实h->jumpt[20]这个字符数组保存的就是thumb指令集下控制pc指针跳转到hook函数地址的代码。Hook完之后的流程就和arm指令集的hook一样了。
关于Cydia和Xposed的文章和例子已经很多了,这里就不再重复的进行介绍了。这里推荐一下瘦蛟舞和我写的文章,基本上就知道怎么使用这两个框架了:
Android.Hook框架xposed篇(Http流量监控) http://drops.wooyun.org/papers/7488
Android.Hook框架Cydia篇(脱壳机制作) http://drops.wooyun.org/tips/8084
手把手教你当微信运动第一名 – 利用Android Hook进行微信运动作弊 http://drops.wooyun.org/tips/8416
个人感觉Xposed框架要做的更好一些,主要原因是Cydia的作者已经很久没有更新过Cydia框架了,不光有很多bug还不支持ART。但是有很多不错的调试软件/插件是基于两个框架制作的,所以有时候还是需要用到Cyida的。
接下来就推荐几个很实用的基于Cydia和Xposed的插件:
1)ZjDroid: ZjDroid是基于Xposed Framewrok的动态逆向分析模块,逆向分析者可以通过ZjDroid完成以下工作: 1、DEX文件的内存dump 2、基于Dalvik关键指针的内存BackSmali,有效破解主流加固方案 3、敏感API的动态监控 4、指定内存区域数据dump 5、获取应用加载DEX信息。 6、获取指定DEX文件加载类信息。 7、dump Dalvik java堆信息。 8、在目标进程动态运行lua脚本。 https://github.com/halfkiss/ZjDroid
2)XPrivacy: XPrivacy是一款基于Xposed框架的模块应用,可以对所有应用可能泄露隐私的权限进行管理,对禁止可能会导致崩溃的应用采取欺骗策略,提供伪造信息,比如说可以伪造手机的IMEI号码等。 https://github.com/M66B/XPrivacy
3)Introspy: Introspy是一款可以追踪分析移动应用的黑盒测试工具并且可以发现安全问题。这个工具支持很多密码库的hook,还支持自定义hook。 https://github.com/iSECPartners/Introspy-Android
我们使用alictf上的evilapk400作为例子讲解如何利用introspy来调试程序。Evilapk400使用了比较复杂的dex加壳技术,如果不利用基于自定义dalvik的脱壳工具来进行脱壳的话做起来会非常麻烦。但我们如果换一种思路,直接通过动态调试的方法来获取加密算法的字符串,key和IV等信息就可以直接获取答案了。
首先我们安装cyida.apk,Introspy-Android Config.apk到手机上,然后用eclipse打开“Introspy-Android Core”的源码增加一个自定义的hook函数。虽然Introspy默认对密码库进行了hook,但却没有对一些strings的函数进行hook。所以我们手动添加一个对String.equals()的hook:
然后我们编译,生成并安装Introspy-Android Core.apk到手机上。然后我们安装上EvilApk400。然后打开Introspy-Android Config勾选com.ali.encryption。
接着打开evilapk400,然后随便输入点内容并点击登陆。
然后我们使用adb logcat就能看到Introspy输出的信息了:
通过log,很容易就能看出来evilapk400使用了DES加密。通过log我们获取了密文,Key以及IV,所以我们可以写一个python程序来计算出最后的答案:
本篇介绍了native层,JNI层以及JAVA层的hook,基本上可以满足我们平时对于android上hook的需求了。 另外文章中所有提到的代码和工具都可以在我的github下载到,地址是: https://github.com/zhengmin1989/TheSevenWeapons
作者:蒸米@阿里聚安全,更多技术文章,请访问阿里聚安全博客