学习逆向的初衷是想系统学习Android下的hook技术和工具, 想系统学习Android的hook技术和工具是因为Android移动性能实战这本书. 这本书里用hook技术hook一些关键函数来计算关键函数的调用参数和调用时长, 从而确定性能问题发生的位置和原因. 但目前没有比较系统的讲解hook的书籍, 所以就系统的了解下逆向分析.
在读了姜维的Android应用安全防护和逆向分析和丰生强的Android软件安全与逆向分析后, 准备分享下这方面知识. 在写文章时发现, 这两本书缺少对最新的逆向工具和加固工具的描述. 在查阅相关文献后补充了这一部分.
本文从五个维度来讲解Android逆向, 每个维度尽量分’原理’, ‘工具’, ‘实例’三个方面.
反编译
静态分析
动态分析
重编译
Docker
说到反编译, 先来看下正向编译, 如上图, 正向编译是
java -> class -> dex -> apk
反编译和正向编译稍有不同, 反编译可以分成两类:
java <- smali <- dex <- apk
这种方法是将dex文件转为smali, smali是Dalvik虚拟机的汇编语言, 可以用来动态调试程序.
java <- class/jar <- dex <- apk
这种方法中是将Dalvik字节码转化为等价的Java字节码, 然后用丰富的java分析工具分析源码.
阅读反编译工具源码查找缺陷
压力测试找反编译工具bug(下载很多apk, 写个脚本调用ApkTool反编译这些apk, ApkTool因为某些bug无法反编译某个apk, 这时我们就通过压力测试找到了ApkTool的bug, 将发现的这个应用到我们的apk中, 即可保护我们的apk免受ApkTool反编译)
上图的反编译工具走的java <- class/jar <- dex <- apk
路线, 即先把apk里的dex找到, 然后使用Enjarify/dex2jar/classyshark/jadx反编译得到jar包, 然后使用jd-gui/CFR/Procyon阅读jar包里的java源码. 这些工具各有优缺点, 我们一般选择dex2jar+jd-gui, 相比其他工具, jd-gui虽然很久不更新了, 但是支持跳转, 方便查看代码. 特别说明下Bytecode-Viewer, 其是Procyon的一个前端, 同时集成了很多其他工具, 功能强大.
看下上图, 这些工具走的是java <- class/jar <- dex <- apk
路线. 将dex文件转化为smali汇编, 然后直接阅读smali汇编语言, 或者smali再转为java(这里没有强大的工具, 可能经常无法成功转化).
从上图可以看到有很多反编译工具, 我们平时最常用的是dex2jar+jd-gui
和ApkTool
.
jd-gui不仅有不错的界面, 最关键的是支持类之间的跳转, 在混淆后的代码中跳转可以大大方便我们查看.
ApkTool隐隐有无冕之王的声势, 可以反编译代码和资源, 修改后可以重编译成apk, 在Android Studio下使用smalidea插件还可以完成无源码调试, 十分强大.
https://github.com/Storyyeller/enjarify
https://github.com/pxb1988/dex2jar
https://github.com/google/android-classyshark
https://github.com/skylot/jadx
https://github.com/java-decompiler/jd-gui
http://www.benf.org/other/cfr/
https://bitbucket.org/mstrobel/procyon/wiki/Java%20Decompiler
https://github.com/Konloch/bytecode-viewer
https://github.com/deathmarine/Luyten
http://www.secureteam.net/d4j
https://github.com/iBotPeaches/Apktool
https://github.com/demitsuri/smali2java
https://www.pnfsoftware.com/
这里以一个实例说明下反反编译和反反反编译:
使用早期ApkTool反编译apk时,可能会遇到反编译失败, 出现如下问题:
Exception in thread "main" brut.androlib.AndrolibException: Multiple res specs: attr/name
at brut.androlib.res.data.ResTypeSpec.addResSpec(ResTypeSpec.java:78)
at brut.androlib.res.decoder.ARSCDecoder.readEntry(ARSCDecoder.java:248)
at brut.androlib.res.decoder.ARSCDecoder.readTableType(ARSCDecoder.java:212)
at brut.androlib.res.decoder.ARSCDecoder.readTableTypeSpec(ARSCDecoder.java:154)
at brut.androlib.res.decoder.ARSCDecoder.readTablePackage(ARSCDecoder.java:116)
at brut.androlib.res.decoder.ARSCDecoder.readTableHeader(ARSCDecoder.java:78)
at brut.androlib.res.decoder.ARSCDecoder.decode(ARSCDecoder.java:47)
at brut.androlib.res.AndrolibResources.getResPackagesFromApk(AndrolibResources.java:544)
at brut.androlib.res.AndrolibResources.loadMainPkg(AndrolibResources.java:63)
at brut.androlib.res.AndrolibResources.getResTable(AndrolibResources.java:55)
at brut.androlib.Androlib.getResTable(Androlib.java:66)
at brut.androlib.ApkDecoder.setTargetSdkVersion(ApkDecoder.java:198)
at brut.androlib.ApkDecoder.decode(ApkDecoder.java:96)
at brut.apktool.Main.cmdDecode(Main.java:165)
at brut.apktool.Main.main(Main.java:81)
查看ApkTool代码发现, 是Apk利用了ApkTool的一个bug, Apk做了混淆,在编译时存入了重复id值,导致ApkTool crash.
针对这个问题, 解决办法是create fake names to prevent abuse from duplicate key
, 其github提交如下:
names
https://github.com/iBotPeaches/Apktool/commit/567907b187ad2f78b3564d0a0405e3b207832e17
不运行代码,采用反编译工具生成程序的反编译代码,然后阅读反编译代码来掌握程序功能.
反编译apk程序
查看Application类(在Activity启动之前, 一般加固/授权放在这里)
查看MainActivity类
找关键代码
代码混淆(ProGuard等)
使用NDK+STL编写
手动注册native函数()
static JNINativeMethod methods[] = {
{"dynamicGenerateKey", "(Ljava/lang/String;)Ljava/lang/String;", (void
*) native_dynamic_key}}; RegisterNatives(jclass clazz, const
JNINativeMethod* methods,jint nMethods)
来注册native函数名, 加固(dex/so加壳,指令抽取等)
定位关键代码技巧
信息反馈法(点击界面, 出现注册失败
, 那么检查代码里哪里使用到了注册失败
)
特征函数法/关键系统调用(一般情况下, 最终都会调用到系统函数. 为了提升难度, 可以自制和系统函数功能相同的函数, 这样难以下断点)
Log代码注入法/栈跟踪法(动静分析结合, 在合适位置注入log, 编译运行时可以打印当前上下文信息和堆栈信息)
IDA分析汇编(asm->c, 虽然很多函数还没重定位, 但是c比汇编的表达力更强, 更便于分析)
脱壳
IDA脱壳(dvm:dvmDexFileOpenPartial, art:openDexFileNative, 无论如何, 最终都是要调用系统API加载dex, 在这里加断点, 然后dump出内存中的dex文件[现在一些加固工具都是自己写加载dex的函数, 这样简单在上述方法上加断点是无法命中的])
Xposed/VirtualXposed
这里不详细介绍,
感兴趣参考”https://blog.csdn.net/jiangwei0910410003/article/details/50668549”
空指令 寄存器数据操作指令 返回指令 数据定义指令 锁指令 实例操作指令
数组/字段操作指令 异常指令 跳转指令 比较指令 方法调用指令 数据转换指令
数据运算指令
.field private isFlag:z 定义变量
.method 方法
.parameter 方法参数
.prologue 方法开始
.line 12 此方法位于第12行
return-void 函数返回void
.end method 函数结束
new-instance 创建实例
iput-object 对象赋值
iget-object 调用对象
invoke-static 调用静态函数条件跳转分支:
invoke-super 调用父函数
invoke-direct 调用函数
“if-eq vA, vB, :cond_**” 如果vA等于vB则跳转到:cond_**
“if-ne vA, vB, :cond_**” 如果vA不等于vB则跳转到:cond_**
“if-lt vA, vB, :cond_**” 如果vA小于vB则跳转到:cond_**
“if-ge vA, vB, :cond_**” 如果vA大于等于vB则跳转到:cond_**
“if-gt vA, vB, :cond_**” 如果vA大于vB则跳转到:cond_**
“if-le vA, vB, :cond_**” 如果vA小于等于vB则跳转到:cond_**
“if-eqz vA, :cond_**” 如果vA等于0则跳转到:cond_**
“if-nez vA, :cond_**” 如果vA不等于0则跳转到:cond_**
“if-ltz vA, :cond_**” 如果vA小于0则跳转到:cond_**
“if-gez vA, :cond_**” 如果vA大于等于0则跳转到:cond_**
“if-gtz vA, :cond_**” 如果vA大于0则跳转到:cond_**
“if-lez vA, :cond_**” 如果vA小于等于0则跳转到:cond_**
这里主要关注跳转指令, 因为我们逆向Apk时, 一般只关注特殊的几点逻辑,
注意跳转语句跳转到了哪些特殊函数.
这里不详细介绍,
感兴趣的同学可以参考”https://blog.csdn.net/jiangwei0910410003/article/details/49336613”
跳转指令 存储器访问指令 数据处理指令(加减乘除)
空操作 软中断
arm汇编里我们主要关注如下函数调用语句:
BL 执行函数调用
BLX执行函数调用, 可以在ARM和Thumb指令集间切换
这里解释下ARM和Thumb指令集的区别:
Thumb是ARM体系结构中一种指令集。
Thumb指令只有16bit,可以减小代码量。
Thumb指令功能并不完整,必要时仍需要使用ARM指令集。
扩展下NEON/VFP知识点:
VFP是一种浮点硬件加速器。
NEON是一个SIMD(单指令多数据)协处理器。
以加法指令为例,单指令单数据(SISD)的CPU对加法指令译码后,执行部件先访问内存,取得第一个操作数;之后再一次访问内存,取得第二个操作数;随后才能进行求和运算。而在SIMD型的CPU中,指令译码后几个执行部件同时访问内存,一次性获得所有操作数进行运算。这个特点使SIMD特别适合于多媒体应用等数据密集型运算。
第一代加固技术——混淆技术;
第二代加固技术——加壳技术(落地与不落地脱壳);
第三代加固技术——指令抽离;
第四代加固技术——指令转换,即VMP(虚拟软件保护)加固技术。
加壳是指给可执行文件加个外衣, 这个外衣就是壳程序. 壳程序先取得程序的控制权, 之后把加密的可执行程序在内存中解开为真正的程序并运行.
抽取dex文件中DexCode的部分结构,即虚拟机操作码。在虚拟机加载到此类的时候对DexCode结构进行还原。
比如此图中的getPwd方法很重要,需要抽取. 那么生成Dex文件后, 找到Dex文件中的getPwd的方法体, 将对应的方法体抽取出来放到so文件或者特定位置. 然后Hook住系统的FindClass方法, 当系统查找CoreUtils类时, 找到getPwd在内存中的位置, 然后将抽取出来的方法重新写入. 这样即使被破解拿到Dex, 这个Dex也是残缺的, 没有关键的函数.这时候如果我们查看Dex, 会发现getPwd的方法是个空方法.
该方法的流程如下:
基于三代加固技术,把原本可执行文件中的机器指令代码转换成了它自己虚拟机的指令,而且还插入了大量的垃圾代码。
这种方法将核心代码转化为虚拟机自己的指令, 破解apk的难度和破解虚拟机指令的难度一致. PC上存在类似的VMProtect, 号称无人一定能破.
从难度方面来说, 二代加固一般还有破解思路, 但到了四代加固这里, 一般的逆向脱壳技术全部失效, 你面对的是如何破解这个虚拟机.
https://blog.csdn.net/jiangwei0910410003/article/details/78070610
https://www.leiphone.com/news/201712/TABfBNU8x0lZIPoT.html
https://bbs.pediy.com/thread-224921.htm
apk加壳实例:
apk加壳实例可以用上图来说明, 我们把要加固的myapk.apk放到一个dex尾部. 这个dex有脱壳逻辑, 程序运行时, 首先运行这个脱壳dex, 脱壳dex从dex尾部获取到要加密的apk的大小, 然后从自己的dex中拷贝出这个myapk.apk, 最后调用Android系统API运行myapk.apk. 这样就算用ApkTool等逆向工具, 也无法直接获得我们加固的myapk.apk. 为了增大逆向难度, 我们可以把脱壳逻辑用c实现放到so文件中, 同时把加密的myapk.apk分段放到so文件中. 为了防止特征破解, 我们可以改写apk魔数. 这样下来, 一个简单的加固工具就完成了.
这里提供一个demo, 只有最简单的把myapk.apk放到脱壳dex尾部的功能, git地址:
https://github.com/oncealong/apk_dex_shell
demo分为三个项目:
DexReinforcingTools
MyApk
ShellingMyApk
这里再说下, 这种二代加壳是现在最简单的加壳方式, 也是最基本的加壳方式.
参考文档:
https://blog.csdn.net/jiangwei0910410003/article/details/48415225
动态分析主要基于下面两个工具:
JPDA分为三层, 分别是JVMTI,JDWP,JDI.
JVMTI(Java Virtual Machine Tool Interface)是一套由虚拟机直接提供的 native接口,通过这些接口,开发人员不仅调试在该虚拟机上运行的 Java程序,还能查看它们运行的状态,设置回调函数,控制某些环境变量,从而优化程序性能。
JDWP(Java Debug Wire Protocol)是一个为 Java调试而设计的一个通讯交互协议,它定义了调试器和被调试程序之间传递的信息的格式。
JDI(Java Debug Interface)提供 Java API 来远程控制被调试虚拟机
有两点主要区别:
JVM TI适配了Android设备特有的Dalvik虚拟机/ART虚拟机
JDWP的实现支持ADB和Socket两种通信方式
ptrace()
提供了跟踪和调试的功能。它允许一个进程(跟踪进程tracer)去控制另外一个进程(被跟踪进程tracee)。
tracer可以观察和控制tracee的运行,可以查看和改变tracee的内存和寄存器。它主要用来实现断点调试和系统调用跟踪。
tracer流程一般如下:
其中PTRACE_ATTACH/PTRACE_GETREGS/PTRACE_POKETEXT/PTRACE_SETREGS/PTRACE_DETACH定义如下:
PTRACE_ATTACH,表示附加到指定远程进程;
PTRACE_DETACH,表示从指定远程进程分离
PTRACE_GETREGS,表示读取远程进程当前寄存器环境
PTRACE_SETREGS,表示设置远程进程的寄存器环境
PTRACE_CONT,表示使远程进程继续运行
PTRACE_PEEKTEXT,从远程进程指定内存地址读取一个word大小的数据
PTRACE_POKETEXT,往远程进程指定内存地址写入一个word大小的数据
ptrace是*nix系统上最常用的系统调用之一, 常见的gdb调试也是通过它实现的.
当我们使用ptrace方式跟踪一个进程时,目标进程会记录自己被谁跟踪,可以查看/proc/pid/status来确认. 所以apk里为了防止被逆向, 一般都会新开一个线程, 对status做检测, 如果TracerPid不为0, 立刻退出apk.
正常情况
被ptrace时
检查是否有调试
Debug.isDebuggerConnected();
针对ptrace, 检查TracerPid是否为0
检测是否在模拟器
对抗反调试
java层:smali代码注释掉
native层 (nop掉so文件或内存中指令, 断点fopen/fget并修改内存)
1.下载mprop
, 注入init进程, 修改内存中属性值
./mprop ro.debuggable 1
2.重启adbd
stop;start
tip:
说到android:debuggable这个属性, 想到另一个属性android:allowBackup.
android:allowBackup默认为true, 一定要显式设置android:allowBackup=false.
否则adb backup/adb restore备份恢复数据
微信6.0以前未设置此属性,可以备份恢复数据
参考地址:
https://tech.meituan.com/android-remote-debug.html
http://burningcodes.net/%E7%90%86%E8%A7%A3ptrace%E8%B0%83%E8%AF%95%E5%8F%8A%E5%8F%8D%E8%B0%83%E8%AF%95/
https://ops.tips/gists/using-c-to-inspect-linux-syscalls/
https://www.nevermoe.com/?p=854
https://github.com/wpvsyou/mprop
这里特别推荐下VirtualXposed, 其基于VirtualApp和epic, 将Xposed安装到VirtualApp中, 可以不用root权限就使用Xposed, 而且安装插件后重启极快.
Frida是一个DBI工具, 使用其进行动态分析时, 被分析进程的TracerPid仍为0. 下图是Frida原理, 其最初建立连接时通过ptrace向相关进程注入代码, 其后使用其特有的通道来通信, 如下图. Frida-Gadget支持Android下非root和iOS下非越狱的逆向.
IDA家喻户晓, 其支持dex和so的动态分析, 尤其是asm->c的转化, 可以大大方便分析.
radare是一个比IDA还要强大的工具, 其起源是调查取证, 不过目前支持数不胜数的功能. 但是其学习曲线比Vim还要陡峭
https://forum.xda-developers.com/showthread.php?t=3034811
https://github.com/android-hacker/VirtualXposed
https://github.com/frida/frida
https://www.hex-rays.com/products/ida/
https://github.com/radare/radare2
http://rada.re/r/cmp.html
https://www.megabeets.net/a-journey-into-radare-2-part-1/
可以将apk用ApkTool反编译后, 使用AndroidStudio+smalidea插件来调试apk.
这里来张图感受下无源码调试的强大.
分享一个小tip, 如何让程序暂停在启动界面.
因为反逆向代码一般在Application的onCreate或更早就执行, 如果等到程序运行到MainActivity再attach进程, 时机就太晚了.
可以用如下命令让app停在等待debug界面:
等待debug一次: adb shell am set-debug-app -w com.oncealong.sample
一次debug不一定能解决问题,多次调试则在所难免,如果每次调试都执行上述语句, 稍显啰嗦, 那么此时可以执行下述语句:
一直等待debug: adb shell am set-debug-app -w --persistent com.oncealong.sample
待debug完毕, 使用下述语句取消打开app时的等待.
取消等待debug: adb shell am clear-debug-app
这里的示例不在展开, 只说明这种方法和其效果, 对其感兴趣可以看下述链接.
参考地址:
http://www.cnblogs.com/goodhacker/p/5592313.html
https://droidyue.com/blog/2017/05/14/a-little-but-useful-debug-skill_for_android/
IDA动态调试可以获得内存中的信息, 比如在dvmDexFileOpenPartial函数上加断点, 然后执行IDA脚本直接把内存中的dex拷贝出来以脱壳. 详情见Android应用安全防护和逆向分析相关章节. 这里也不做详细介绍,
只用下图展示IDA的强大.
参考地址:
https://blog.csdn.net/jltxgcy/article/details/50600241
https://blog.csdn.net/qq1084283172/article/details/46872937
VirtualXposed可以hook java, 相比Xposed安装插件需要重启手机, VirtualXposed只用重启下Xposed程序, 如果前者重启手机耗时1min, 后者重启Xposed程序只用1s不到. 对于一些简单的hook或者逆向, 或者验证Xposed插件逻辑, 这里强烈推荐VirtualXposed. 不过Xposed只支持hook java层, 如果需要hook native层, 可以使用下一个工具Frida.
参考地址:
https://github.com/android-hacker/VirtualXposed
https://github.com/ac-pm/Inspeckage
http://www.cnblogs.com/lkislam/p/4859959.html
Frida支持java/native层的hook. 而且Frida支持脚本, 这样可以更方便的复现结果.
比如Frida的这个Android示例. 将下面的代码放到一个py脚本中, 随时运行都可以获得结果. 不像IDA还需要恢复现场.
参考地址:
https://github.com/frida/frida/releases
https://github.com/dweinstein/awesome-Frida
https://www.anquanke.com/post/id/85758
https://www.anquanke.com/post/id/85759
https://koz.io/using-frida-on-android-without-root/
https://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool
http://blog.mengy.org/how-valgrind-work/
http://www.ninoishere.com/frida-learn-by-example/
https://www.frida.re/docs/presentations/osdc-2015-the-engineering-behind-the-reverse-engineering.pdf
http://dogewatch.github.io/2017/05/15/Hook-Native-Function-Use-Frida/
运行时检查签名(signatures比较长,hash后比较)
运行时校验保护(校验classes.dex的md5)
查关键函数, 注释掉或nop掉
如果到这一步, 光靠本地的检测基本无效, 可以考虑在http请求时加入对apk签名的检查, 如果不合法就不返回数据. 但是这样无法阻止app被非法本地运行, 逆向者也可以通过抓包正常apk的请求来模拟正常请求. 不过这样可以进一步提高破解门槛.
与逆向工具高内聚,与外界系统低耦合
在Linux下, Docker性能不错, 还可以使用VNC连接桌面.
# pull image
docker pull cryptax/android-re:latest
# run locally interactive
docker run -it --rm -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix
cryptax/android-re:latest /bin/bash
# run through ssh or VNC
docker run -d -p SSH_PORT:22 -p VNC_PORT:5900 cryptax/android-re
## sample: docker run -d --privileged -p 5900:5900 -p 5022:22
cryptax/android-re
ssh -X -p SSH_PORT root@127.0.0.1
## sample: ssh -p 5022 -X [email protected] #password: rootpass
vncviewer HOST::VNC_PORT
##vncviewer 127.0.0.1::5900
工具地址:
https://github.com/cryptax/androidre/