学习逆向的初衷是想系统学习Android下的hook技术和工具, 想系统学习Android的hook技术和工具是因为Android移动性能实战这本书. 这本书里用hook技术hook一些关键函数来计算关键函数的调用参数和调用时长, 从而确定性能问题发生的位置和原因. 但目前没有比较系统的讲解hook的书籍, 所以就系统的了解下逆向分析.
在读了姜维的Android应用安全防护和逆向分析和丰生强的Android软件安全与逆向分析后, 准备分享下这方面知识. 在写文章时发现, 这两本书缺少对最新的逆向工具和加固工具的描述. 在查阅相关文献后补充了这一部分.
本文从五个维度来讲解Android逆向, 每个维度尽量分'原理', '工具', '实例'三个方面.
- 反编译
- 静态分析
- 动态分析
- 重编译
- Docker
1.反编译
1.1 原理
Android App正向编译
说到反编译, 先来看下正向编译, 如上图, 正向编译是
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反编译)
如何反反反编译呢:
- 阅读反编译源码修复缺陷
1.2 工具
反编译工具-dex-class
上图的反编译工具走的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的一个前端, 同时集成了很多其他工具, 功能强大.
反编译工具-dex-smali
看下上图, 这些工具走的是java <- class/jar <- dex <- apk
路线. 将dex文件转化为smali汇编, 然后直接阅读smali汇编语言, 或者smali再转为java(这里没有强大的工具, 可能经常无法成功转化).
最常用反编译工具
从上图可以看到有很多反编译工具, 我们平时最常用的是dex2jar jd-gui
和ApkTool
.
jd-gui不仅有不错的界面, 最关键的是支持类之间的跳转, 在混淆后的代码中跳转可以大大方便我们查看.
ApkTool隐隐有无冕之王的声势, 可以反编译代码和资源, 修改后可以重编译成apk, 在Android Studio下使用smalidea插件还可以完成无源码调试, 十分强大.
1.3 实例
这里以一个实例说明下反反编译和反反反编译:
使用早期ApkTool反编译apk时,可能会遇到反编译失败, 出现如下问题:
代码语言:javascript复制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
names`, 其github提交如下:
create fake names to prevent abuse from duplicate key names
2.静态分析
2.1 原理
什么是静态分析?
不运行代码,采用反编译工具生成程序的反编译代码,然后阅读反编译代码来掌握程序功能.
Android静态分析步骤:
- 反编译apk程序
- 查看Application类(在Activity启动之前, 一般加固/授权放在这里)
- 查看MainActivity类
- 找关键代码
反静态分析:
- 代码混淆(ProGuard等)
- 使用NDK STL编写
- 手动注册native函数()
- 默认情况, 使用javah, com.example.k12 -> java_完整包名_类名_方法名.
但可以使用函数映射表 `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加壳,指令抽取等)
反反静态分析:
- 定位关键代码技巧
- 信息反馈法(点击界面, 出现`注册失败`, 那么检查代码里哪里使用到了`注册失败`)
代码语言:javascript复制- 特征函数法/关键系统调用(一般情况下, 最终都会调用到系统函数. 为了提升难度, 可以自制和系统函数功能相同的函数, 这样难以下断点)
代码语言:javascript复制- Log代码注入法/栈跟踪法(动静分析结合, 在合适位置注入log, 编译运行时可以打印当前上下文信息和堆栈信息)
- IDA分析汇编(asm->c, 虽然很多函数还没重定位, 但是c比汇编的表达力更强, 更便于分析)
- 脱壳
- IDA脱壳(dvm:dvmDexFileOpenPartial, art:openDexFileNative, 无论如何, 最终都是要调用系统API加载dex, 在这里加断点, 然后dump出内存中的dex文件[现在一些加固工具都是自己写加载dex的函数, 这样简单在上述方法上加断点是无法命中的])
代码语言:javascript复制- Xposed/VirtualXposed
Dex文件格式
这里不详细介绍,
Dalvik指令集
空指令 寄存器数据操作指令 返回指令 数据定义指令 锁指令 实例操作指令
数组/字段操作指令 异常指令 跳转指令 比较指令 方法调用指令 数据转换指令
数据运算指令
.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时, 一般只关注特殊的几点逻辑,
注意跳转语句跳转到了哪些特殊函数.
ELF文件格式和寻址方式
Arm汇编语法
跳转指令 存储器访问指令 数据处理指令(加减乘除)
空操作 软中断
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的方法是个空方法.
指令抽取-抽取完成
指令抽取-hook-findClass
该方法的流程如下:
指令抽取流程
四代加固VMP技术:
基于三代加固技术,把原本可执行文件中的机器指令代码转换成了它自己虚拟机的指令,而且还插入了大量的垃圾代码。
这种方法将核心代码转化为虚拟机自己的指令, 破解apk的难度和破解虚拟机指令的难度一致. PC上存在类似的VMProtect, 号称无人一定能破.
VMP加固原理
从难度方面来说, 二代加固一般还有破解思路, 但到了四代加固这里, 一般的逆向脱壳技术全部失效, 你面对的是如何破解这个虚拟机.
apk加壳实例:
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地址:
demo分为三个项目:
- DexReinforcingTools
- 给Apk加壳的工具, 可以用java或者cpp或者任何其他语言写成.
- MyApk
- 需要加固的Apk
- ShellingMyApk
- 脱壳Apk, 实际安装到用户手机上的是该Apk, 其在Application的attachBaseContext 时会解压得到实际的apk文件, 然后运行实际的Apk.
这里再说下, 这种二代加壳是现在最简单的加壳方式, 也是最基本的加壳方式.
参考文档:
3.动态分析
3.1 原理
动态分析主要基于下面两个工具:
JPDA(Java Platform Debugger Architecture)
JPDA原理图
JPDA分为三层, 分别是JVMTI,JDWP,JDI.
JVMTI(Java Virtual Machine Tool Interface)是一套由虚拟机直接提供的 native接口,通过这些接口,开发人员不仅调试在该虚拟机上运行的 Java程序,还能查看它们运行的状态,设置回调函数,控制某些环境变量,从而优化程序性能。
JDWP(Java Debug Wire Protocol)是一个为 Java调试而设计的一个通讯交互协议,它定义了调试器和被调试程序之间传递的信息的格式。
JDI(Java Debug Interface)提供 Java API 来远程控制被调试虚拟机
JPDA-JVM
Android调试模型是一种JPDA框架的具体实现
有两点主要区别:
- JVM TI适配了Android设备特有的Dalvik虚拟机/ART虚拟机
- JDWP的实现支持ADB和Socket两种通信方式
JPDA-Android
ptrace(process trace)
ptrace原理
ptrace()
提供了跟踪和调试的功能。它允许一个进程(跟踪进程tracer)去控制另外一个进程(被跟踪进程tracee)。
tracer可以观察和控制tracee的运行,可以查看和改变tracee的内存和寄存器。它主要用来实现断点调试和系统调用跟踪。
tracer流程一般如下:
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调试也是通过它实现的.
gdb流程图
检测ptrace
当我们使用ptrace方式跟踪一个进程时,目标进程会记录自己被谁跟踪,可以查看/proc/pid/status来确认. 所以apk里为了防止被逆向, 一般都会新开一个线程, 对status做检测, 如果TracerPid不为0, 立刻退出apk.
/proc/pid/status
正常情况
被ptrace时的status状态
被ptrace时
反动态分析:
- 检查是否有调试
- Debug.isDebuggerConnected();
代码语言:javascript复制- 针对ptrace, 检查TracerPid是否为0
- 检测是否在模拟器
- getprop不同(虚拟机和真机的环境变量不同,
比如虚拟机的ro.kernel.qemu=1而真机没有这个属性)
反反动态分析:
- 对抗反调试
- java层:smali代码注释掉
代码语言:javascript复制- native层 (nop掉so文件或内存中指令, 断点fopen/fget并修改内存)
Android程序是否可调试:
Android程序是否可调试
开启调试:
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以前未设置此属性,可以备份恢复数据
参考地址:
3.2 工具
动态分析工具
这里特别推荐下VirtualXposed, 其基于VirtualApp和epic, 将Xposed安装到VirtualApp中, 可以不用root权限就使用Xposed, 而且安装插件后重启极快.
Frida是一个DBI工具, 使用其进行动态分析时, 被分析进程的TracerPid仍为0. 下图是Frida原理, 其最初建立连接时通过ptrace向相关进程注入代码, 其后使用其特有的通道来通信, 如下图. Frida-Gadget支持Android下非root和iOS下非越狱的逆向.
Frida原理
IDA家喻户晓, 其支持dex和so的动态分析, 尤其是asm->c的转化, 可以大大方便分析.
radare是一个比IDA还要强大的工具, 其起源是调查取证, 不过目前支持数不胜数的功能. 但是其学习曲线比Vim还要陡峭
3.3 实例
无源码动态调试smali代码
可以将apk用ApkTool反编译后, 使用AndroidStudio smalidea插件来调试apk.
这里来张图感受下无源码调试的强大.
AndroidStudio smalidea无源码调试
分享一个小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
这里的示例不在展开, 只说明这种方法和其效果, 对其感兴趣可以看下述链接.
IDA动态调试
IDA动态调试可以获得内存中的信息, 比如在dvmDexFileOpenPartial函数上加断点, 然后执行IDA脚本直接把内存中的dex拷贝出来以脱壳. 详情见Android应用安全防护和逆向分析相关章节. 这里也不做详细介绍,
只用下图展示IDA的强大.
IDA动态调试
VirtualXposed hook java
VirtualXposed可以hook java, 相比Xposed安装插件需要重启手机, VirtualXposed只用重启下Xposed程序, 如果前者重启手机耗时1min, 后者重启Xposed程序只用1s不到. 对于一些简单的hook或者逆向, 或者验证Xposed插件逻辑, 这里强烈推荐VirtualXposed. 不过Xposed只支持hook java层, 如果需要hook native层, 可以使用下一个工具Frida.
VirtualXposed hook 构造函数
VirtualXposed hook 方法
Frida
Frida支持java/native层的hook. 而且Frida支持脚本, 这样可以更方便的复现结果.
比如Frida的这个Android示例. 将下面的代码放到一个py脚本中, 随时运行都可以获得结果. 不像IDA还需要恢复现场.
Frida-hook
4.重编译
4.1 原理
反重编译:
运行时检查签名(signatures比较长,hash后比较)
运行时校验保护(校验classes.dex的md5)
反反重编译:
查关键函数, 注释掉或nop掉
如果到这一步, 光靠本地的检测基本无效, 可以考虑在http请求时加入对apk签名的检查, 如果不合法就不返回数据. 但是这样无法阻止app被非法本地运行, 逆向者也可以通过抓包正常apk的请求来模拟正常请求. 不过这样可以进一步提高破解门槛.
5.Docker
5.1 原理
与逆向工具高内聚,与外界系统低耦合
在Linux下, Docker性能不错, 还可以使用VNC连接桌面.
代码语言:javascript复制# 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 root@127.0.0.1 #password: rootpass
vncviewer HOST::VNC_PORT