Android逆向分析从入门到深入

2019-11-07 13:23:51 浏览数 (1)

学习逆向的初衷是想系统学习Android下的hook技术和工具, 想系统学习Android的hook技术和工具是因为Android移动性能实战这本书. 这本书里用hook技术hook一些关键函数来计算关键函数的调用参数和调用时长, 从而确定性能问题发生的位置和原因. 但目前没有比较系统的讲解hook的书籍, 所以就系统的了解下逆向分析.

在读了姜维的Android应用安全防护和逆向分析和丰生强的Android软件安全与逆向分析后, 准备分享下这方面知识. 在写文章时发现, 这两本书缺少对最新的逆向工具和加固工具的描述. 在查阅相关文献后补充了这一部分.

本文从五个维度来讲解Android逆向, 每个维度尽量分'原理', '工具', '实例'三个方面.

  • 反编译
  • 静态分析
  • 动态分析
  • 重编译
  • Docker

1.反编译

1.1 原理

Android App正向编译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反编译工具-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反编译工具-dex-smali

看下上图, 这些工具走的是java <- class/jar <- dex <- apk路线. 将dex文件转化为smali汇编, 然后直接阅读smali汇编语言, 或者smali再转为java(这里没有强大的工具, 可能经常无法成功转化).

最常用反编译工具

从上图可以看到有很多反编译工具, 我们平时最常用的是dex2jar jd-guiApkTool.

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 Decompiler

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/

1.3 实例

这里以一个实例说明下反反编译和反反反编译:

使用早期ApkTool反编译apk时,可能会遇到反编译失败, 出现如下问题:

代码语言:txt复制
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 namescreate fake names to prevent abuse from duplicate key names

实例地址:

https://github.com/iBotPeaches/Apktool/commit/567907b187ad2f78b3564d0a0405e3b207832e17

2.静态分析

2.1 原理

什么是静态分析?

不运行代码,采用反编译工具生成程序的反编译代码,然后阅读反编译代码来掌握程序功能.

Android静态分析步骤:

  • 反编译apk程序
  • 查看Application类(在Activity启动之前, 一般加固/授权放在这里)
  • 查看MainActivity类
  • 找关键代码

反静态分析:

  • 代码混淆(ProGuard等)
  • 使用NDK STL编写
  • 手动注册native函数()
代码语言:txt复制
-   默认情况, 使用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加壳,指令抽取等)

反反静态分析:

  • 定位关键代码技巧
代码语言:txt复制
-  信息反馈法(点击界面, 出现`注册失败`, 那么检查代码里哪里使用到了`注册失败`)
代码语言:txt复制
-  特征函数法/关键系统调用(一般情况下, 最终都会调用到系统函数. 为了提升难度, 可以自制和系统函数功能相同的函数, 这样难以下断点)
代码语言:txt复制
-   Log代码注入法/栈跟踪法(动静分析结合, 在合适位置注入log, 编译运行时可以打印当前上下文信息和堆栈信息)
  • IDA分析汇编(asm->c, 虽然很多函数还没重定位, 但是c比汇编的表达力更强, 更便于分析)
  • 脱壳
代码语言:txt复制
-   IDA脱壳(dvm:dvmDexFileOpenPartial, art:openDexFileNative, 无论如何, 最终都是要调用系统API加载dex, 在这里加断点, 然后dump出内存中的dex文件[现在一些加固工具都是自己写加载dex的函数, 这样简单在上述方法上加断点是无法命中的])
代码语言:txt复制
-   Xposed/VirtualXposed

Dex文件格式

这里不详细介绍,

感兴趣参考"https://blog.csdn.net/jiangwei0910410003/article/details/50668549"

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文件格式和寻址方式

这里不详细介绍,

感兴趣的同学可以参考"https://blog.csdn.net/jiangwei0910410003/article/details/49336613"

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指令抽取-hook-findClass

该方法的流程如下:

指令抽取流程指令抽取流程
四代加固VMP技术:

基于三代加固技术,把原本可执行文件中的机器指令代码转换成了它自己虚拟机的指令,而且还插入了大量的垃圾代码。

这种方法将核心代码转化为虚拟机自己的指令, 破解apk的难度和破解虚拟机指令的难度一致. PC上存在类似的VMProtect, 号称无人一定能破.

VMP加固原理VMP加固原理

从难度方面来说, 二代加固一般还有破解思路, 但到了四代加固这里, 一般的逆向脱壳技术全部失效, 你面对的是如何破解这个虚拟机.


https://blog.csdn.net/jiangwei0910410003/article/details/78070610

https://www.leiphone.com/news/201712/TABfBNU8x0lZIPoT.html

https://bbs.pediy.com/thread-224921.htm

2.2 实例

apk加壳实例:

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地址:

https://github.com/oncealong/apk_dex_shell

demo分为三个项目:

  • DexReinforcingTools
代码语言:txt复制
-   给Apk加壳的工具, 可以用java或者cpp或者任何其他语言写成.
  • MyApk
代码语言:txt复制
-   需要加固的Apk
  • ShellingMyApk
代码语言:txt复制
-   脱壳Apk, 实际安装到用户手机上的是该Apk, 其在Application的attachBaseContext 时会解压得到实际的apk文件, 然后运行实际的Apk.

这里再说下, 这种二代加壳是现在最简单的加壳方式, 也是最基本的加壳方式.

参考文档:

https://blog.csdn.net/jiangwei0910410003/article/details/48415225

3.动态分析

3.1 原理

动态分析主要基于下面两个工具:

JPDA(Java Platform Debugger Architecture)

JPDA原理图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-JVMJPDA-JVM
Android调试模型是一种JPDA框架的具体实现

有两点主要区别:

  • JVM TI适配了Android设备特有的Dalvik虚拟机/ART虚拟机
  • JDWP的实现支持ADB和Socket两种通信方式
JPDA-AndroidJPDA-Android

ptrace(process trace)

ptrace原理ptrace原理

ptrace()

提供了跟踪和调试的功能。它允许一个进程(跟踪进程tracer)去控制另外一个进程(被跟踪进程tracee)。

tracer可以观察和控制tracee的运行,可以查看和改变tracee的内存和寄存器。它主要用来实现断点调试和系统调用跟踪。

tracer流程一般如下:

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流程图gdb流程图

检测ptrace

当我们使用ptrace方式跟踪一个进程时,目标进程会记录自己被谁跟踪,可以查看/proc/pid/status来确认. 所以apk里为了防止被逆向, 一般都会新开一个线程, 对status做检测, 如果TracerPid不为0, 立刻退出apk.

/proc/pid/status/proc/pid/status

正常情况

被ptrace时的status状态被ptrace时的status状态

被ptrace时

反动态分析:

  • 检查是否有调试
代码语言:txt复制
-  Debug.isDebuggerConnected();
代码语言:txt复制
-  针对ptrace, 检查TracerPid是否为0
  • 检测是否在模拟器
代码语言:txt复制
-   getprop不同(虚拟机和真机的环境变量不同,
    比如虚拟机的ro.kernel.qemu=1而真机没有这个属性)

反反动态分析:

  • 对抗反调试
代码语言:txt复制
-   java层:smali代码注释掉
代码语言:txt复制
-   native层 (nop掉so文件或内存中指令, 断点fopen/fget并修改内存)

Android程序是否可调试:

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以前未设置此属性,可以备份恢复数据

参考地址:

https://tech.meituan.com/android-remote-debug.html

http://burningcodes.net/理解ptrace调试及反调试/

https://ops.tips/gists/using-c-to-inspect-linux-syscalls/

https://www.nevermoe.com/?p=854

https://github.com/wpvsyou/mprop

3.2 工具

动态分析工具动态分析工具

这里特别推荐下VirtualXposed, 其基于VirtualApp和epic, 将Xposed安装到VirtualApp中, 可以不用root权限就使用Xposed, 而且安装插件后重启极快.

Frida是一个DBI工具, 使用其进行动态分析时, 被分析进程的TracerPid仍为0. 下图是Frida原理, 其最初建立连接时通过ptrace向相关进程注入代码, 其后使用其特有的通道来通信, 如下图. Frida-Gadget支持Android下非root和iOS下非越狱的逆向.

Frida原理Frida原理

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/

3.3 实例

无源码动态调试smali代码

可以将apk用ApkTool反编译后, 使用AndroidStudio smalidea插件来调试apk.

这里来张图感受下无源码调试的强大.

AndroidStudio smalidea无源码调试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

这里的示例不在展开, 只说明这种方法和其效果, 对其感兴趣可以看下述链接.

参考地址:

http://www.cnblogs.com/goodhacker/p/5592313.html

https://droidyue.com/blog/2017/05/14/a-little-but-useful-debug-skill_for_android/

IDA动态调试

IDA动态调试可以获得内存中的信息, 比如在dvmDexFileOpenPartial函数上加断点, 然后执行IDA脚本直接把内存中的dex拷贝出来以脱壳. 详情见Android应用安全防护和逆向分析相关章节. 这里也不做详细介绍,

只用下图展示IDA的强大.

IDA动态调试IDA动态调试

参考地址:

https://blog.csdn.net/jltxgcy/article/details/50600241

https://blog.csdn.net/qq1084283172/article/details/46872937

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 构造函数
VirtualXposed hook 方法VirtualXposed hook 方法

参考地址:

https://github.com/android-hacker/VirtualXposed

https://github.com/ac-pm/Inspeckage

http://www.cnblogs.com/lkislam/p/4859959.html

Frida

Frida支持java/native层的hook. 而且Frida支持脚本, 这样可以更方便的复现结果.

比如Frida的这个Android示例. 将下面的代码放到一个py脚本中, 随时运行都可以获得结果. 不像IDA还需要恢复现场.

Frida-hookFrida-hook

参考地址:

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/

4.重编译

4.1 原理

反重编译:

运行时检查签名(signatures比较长,hash后比较)

运行时校验保护(校验classes.dex的md5)

反反重编译:

查关键函数, 注释掉或nop掉

如果到这一步, 光靠本地的检测基本无效, 可以考虑在http请求时加入对apk签名的检查, 如果不合法就不返回数据. 但是这样无法阻止app被非法本地运行, 逆向者也可以通过抓包正常apk的请求来模拟正常请求. 不过这样可以进一步提高破解门槛.

5.Docker


5.1 原理

与逆向工具高内聚,与外界系统低耦合

在Linux下, Docker性能不错, 还可以使用VNC连接桌面.

代码语言:txt复制
# 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

##vncviewer 127.0.0.1::5900

工具地址:

https://github.com/cryptax/androidre/

0 人点赞