标题
一些坑
- 有的时候直接把自己签名好的apk在命令行进行安装的时候,出现
[INSTALL_FAILED_TEST_ONLY]
的安装 错误的提示
-
原因:
Android Studio 3.0
会在debug apk的manifest
文件<application>
标签里自动添加<android:testOnly="true">
属性 -
解决方案:
adb install -t app-debug.apk
看书笔记
Dalvik可执行格式与字节范围
Dalvik虚拟机
art虚拟机的可以看看这边
https://www.jianshu.com/p/20dcfcf27004
https://www.kancloud.cn/alex_wsc/androids/401771
-
运行时为每个线程维护了一个PC计数器和一个调用栈(这个栈用来存放寄存器)
-
执行流程
-
init
进程先完成设备的初始化工作,再读取init.rc
文件并启动系统中的Zygote
进程 -
Zygote
进程启动后先初始化Dalvik虚拟机,再启动system_server
进程进入Zygote
模式,通过socket
等候命令下达。(有看到资料说art就不用socket通信了?) -
在执行一个应用程序的是偶,
system_server
通过Binder IPC
方式将命令发给Zygote
。 -
Zygote
收到命令后,通过fork()
自身来创造一个Dalvik虚拟机的实例来执行应用程序的入口函数 -
Dalvik虚拟机先通过
loadClassFromDex()
函数来装载类。每个类解析后,都会获得运行环境中的一个ClassObject
类型的数据结构存储。(虚拟机使用gDvm.loadedClasses
全局散列表来存储和查询所有装载进来的类) -
字节码验证器使用
dvmVerifyCodeFlow()
函数对装入的代码进行校验,虚拟机调用FindClass()
函数查找并装载main()
方法类。 -
虚拟机调用
dvmInterpret()
函数来初始化解释器并解释字节码流。
-
smali语法相关
- 类型
语法 | 含义 | 备注 |
---|---|---|
v | void, 只用于返回值类型 | |
Z | boolean | |
B | byte | |
S | short | |
C | char | |
I | int | |
J | long | |
F | float | 64位类型,需要用两个相邻存储器 |
D | double | 64位类型,需要用两个相邻存储器 |
L | java类类型 | 如 Ljava/lang/String; 表示java/lang/String |
[ | 数组类型 | 如 [[I 表示 int[][] |
- 方法
Lpackage/name/ObjectName;->MethodName(III)Z
Lpackage/name/ObjectName;
为一个类型,Method
是方法名,III
是三个整型类型参数,Z
是返回类型
boolean Method(int a,int b,int c){ }
boolean res = ObjectName.Method(b,c,d);
method(I[[IILjava/lang/String;[Ljava/lang/Object;])Ljava/lang/String
String method(int ,int[][], int, String, Object[])
-
# virtual methods
表示是虚方法# direct methods
表示是一个实方法
-
指令
具体看书吧,wps第79页
指令 | 意义 |
---|---|
if-eq | if ( vA == vB ) |
if-ne | if ( vA != vB ) |
if-lt | if ( vA < vB ) |
if-ge | if ( vA >= vB ) |
if-gt | if ( vA > vB ) |
if-le | if ( vA <= vB ) |
if-eqz | if ( !vAA ) |
if-nez | if ( vAA ) |
if-ltz | if ( vAA < 0 ) |
if-lez | if ( vAA <= 0) |
if-gtz | if ( vAA > 0 ) |
if-gez | if ( vAA >= 0) |
cmpl-float | 如果vBB大于vCC;结果为-1,如果等于,结果为0;如果小于结果为1 |
cmpg-float | 如果vBB大于vCC;结果为1,如果等于,结果为0;如果小于结果为-1 |
cmp-long | 如果vBB大于vCC,结果为1,如果等于,结果为0;如果小于,结果为-1 |
android常见文件格式
- apk
- 生成流程
- dex文件结构
https://bbs.pediy.com/thread-255344.htm
dex文件结构体 | DexHeader | 意义 | |
---|---|---|---|
dex header | magic | 固定值 64 65 78 0a 30 33 35 00 (dex.035.)(8个字) | |
string_ids | checksum | 校验和 (除掉自己和magic)(4个字) | |
type_ids | signature | 签名(除掉自己和checksum magic)(20个字) | |
proto_ids | filesize | 整个Dex大小(下面都是4个字大小) | |
field_ids | headerSize | Dexheader头大小 | |
method_ids | endianTag | 字节序标记,指定是大端还是小端。一般0x12345678,是小端 | |
class_def | linkSize | 链接段大小,一般是0 | |
data | linkOff | 链接段偏移,一般是0 | |
link_data | mapOff | DexMapList的文件偏移 | |
stringIdsSize | |||
stringIdsOff | 放字符串的 | ||
typeIdsSize | |||
typeIdsOff | 放类型的 | ||
protoIdsSize | |||
protoIdsOff | 放方法声明的(返回类型+参数类型) | ||
filedIdsSize | |||
filedIdsOff | 放成员变量的 | ||
methodIdsSize | |||
methodIdsOff | |||
classDefsSize | |||
classDefsOff | |||
dataSize | |||
dataOff |
- 验证与优化
问题
:如果直接在虚拟机加载dex文件,而dex文件验证失败,则会导致部分资源(比如加载的native动态库)难以从内存释放。
专门验证与优化的工具dexopt
,使Dalvik虚拟机在加载DEX文件时,通过制定的验证与优化选项来调用dexopt
进行相应的验证和优化操作
- oat的文件格式
- ART虚拟机:
dex2oat把DEX的Dalvik字节编码编译成Native机器码后生成oat文件。Anroid 7.0后还引进了JIT(即使编译)
OAT也是个Android上的ELF文件,一个OAT文件必定包含oatdata
、oatexec
和oatlastword
-
oatdata
符号指向的地址是OAT所在的.rodata
段。这里存放着OAT文件头oatHeader
、OAT的dex文件头OATDexFile
、原始的DEX文件DexFile、OAT的DEX类OatClass
等信息。 -
oatexec
符号指向的地址是OAT所在的.text
段,这里存放着编译生成的Native指令代码。 -
oatlastword
符号指向的地址是OAT文件结束处在ELF中的文件偏移,通过它可以确定OAT文件内容在哪里结束。
oatdump
命令可以把OAT文件中的DEX取出来
oatdump --oat-file=/data/dalvik-cache/arm/system@framework@boot.oat --export-dex-to=/data/local/tmp
- odex文件格式
ODEX与dex相比多了一下几个:
DexOptHeader
: ODEX文件头,描述了ODEX文件的基本信息。Dependences
: 依赖库列表,描述ODEX文件加载时可能使用的依赖库。ClassLookips
:优化数据块的类索引列表信息,用于提高类搜索速度RegisterMaps
:优化数据块的寄存图信息,主要用于帮助Dalvik虚拟机进行精确的垃圾回收
将ODEX转换成DEX,则需要先将ODEX文件反编译为smali文件,再将smali文件编译成DEX文件。
静态分析
(主要使用工具: IDA, JEB,Androguard)
- 代码定位技巧
- 入口分析法
根据AndroidManifest.xml
中定义的主Activity,可以查看其所在类的OnCreate()
方法。
!
如果要在程序组建之间传递全局变量,或者在Activity启动前做一些初始化工作,可以使用Application
类。(在使用Application
类时需要在程序中添加一个继承自android.app.Application
类,然后重写他的onCreate()
方法,最后在AndroidManifest.xml
的<application>
中添加android:name
属性,其值为android.app.Application
。在该方法初始化的全局变量可以在Android的其他组件中访问。)
! Application
类比程序其他所有类启动得都早,一些商业授权验证代码就会转移到该类中。因此在分析Android程序的过程中,需要首先查看程序是否有Application类,如果有,就要查看他的onCreate()
是否做了一些影响逆向分析的工作
- 信息反馈法
先运行目标程序,然后将程序运行时给出的反馈信息作为突破口在寻找关键代码。比如如果程序有弹出消息,则可以在反汇编代码中搜索Toast
- 特征函数法
针对不同类型的APK程序,配合查阅SDK文档,找出其可能用得到的特征函数,然后定位程序的关键点。
动态分析
(常用工具: MobSF)(也可以使用JDB、JEB、IDA来进行动态调试)
- 动态分析技巧
- 代码注入法
先反编译Android程序,然后在smali文件中手动添加Log
调用代码,然后重新打包程序并运行,查看输出结果。
const-string v3,"SN"
invoke-static {v3,v0}, Landroid/util/Log;-> v(Ljava/lang/String;Ljava/lang/String;)I #log.v()
- 栈跟踪法
也属于代码注入的范围,就是追踪调用的函数。在代码里插入
new Exception("print trace").printStackTrace();
对应的smali反汇编代码为
new-instance v0, Ljava/lang/Exception;
const-string v1, "print trace"
invoke-direct {v0,v1}, Ljava/lang/Expection;-><init>(Ljava/lang/String;)V
invoke-virtual {v0}, Ljava/lang/Execption;->printStackTrce()v
然后在Logcat
窗口就可以看到输出的栈跟踪信息,或者在命令行输入adb logcat -s System.err:V *:W
- Method Profiling
由于现在AS的Device Monitor基本上没啥用个了,所以就跳过吧
- UI检查
Anrdoid系统提供给了一个dumpsys
命令,可以使用该命令打印当前系统中运行的所有程序的组件信息
$ adb shell dumpsys activity top (top表示只打印当前栈顶部的Activity信息)
ARM汇编
1.主要组成
- 处理器类型声明:
可以使用.arch
指定处理器构架,.cpu
指定处理器的型号
- 代码与数据声明:
一个完整的程序在编译后会有存放代码的代码段.text
和用于存放数据的数据段.rodata
。有些程序可能没有数据段,只有代码段。
- 符号:
在汇编代码中可以直接引用外部符号。比如bl printf
,直接引用了外部printf()
函数。
在汇编代码中还可以使用.global
声明全局变量。全局符号可以被程序外部引用
- 子程序:
高级语言中的函数在汇编语言中称为子程序。
子程序由.type
伪指令声明为%function
的符号,如.type add,%function
。
子程序一般由.fnstart
伪指令开始,由.fnend
伪指令结束。
.global 子程序
.type 子程序名, %function
子程序名: @@子程序名
.fnstart
<----汇编指令---->
.fnend
- 寄存器
不分组寄存器 R0-R7
+ 分组寄存器 R8-R14
+ 程序计数器 R15 (PC)
+ 当前程序状态寄存器 CPSR
-
ARM寄存器有两种工作状态,即AEM状态与Thumb状态
当处理器处于ARM状态时,会执行32位对齐的ARM指令
当处理器处于Thumb状态时,执行的是16位对齐的Thumb指令。
不同状态的存储器命名也有一定的差异
ARM | Thumb |
---|---|
R0~R7 | R0~R7 |
CPSR | CPSR |
R11 | FP |
R12 | IP |
R13 | SP |
R14 | LR |
R15 | PC |
- 寻址方式
- 立即寻址
MOV R0, #2
- 寄存器寻址
MOV R0, R1
- 寄存器移位寻址
MOV R0, R1, LSL #2 @ R0 = R1<<2 即 R0 =R1 * 4
指令 | 操作 |
---|---|
LSL | 逻辑左移,移位后对寄存器空出的低位补0 |
LSR | 逻辑右移,移位后对寄存器空出的高位补0 |
ASR | 算数右移,移位过程中算数符号位不变。如果源操作数为正数,则移位后对空出的高位补0,否则补1 |
ROR | 循环右移,移位后在移出的低位中填写移位空出的高位 |
RRX | 带扩展的循环右移,操作数右移1位,移位空出的高位用带有C标志的值填充 |
- 寄存器间接寻址
MOV R0, [ R1 ]
- 基址寻址
MOV R0, [R1, #-4] @ 把R1指向的地址减4后获得的地址中的值,赋值给R0
- 多寄存器寻址
LDMIA R0, { R1, R2, R3, R4} @ R1 = [R0], R2 = [ R0 + #4], R3 = [ R0 + #8], R4 = [ R0 + #12]
- 堆栈寻址
寻址指令: LDMFA/STMFA
、 LDMEA/STMEA
、 LDMFD/STMFD
、 LDMED/STMED
STM/LDM
是指令前缀、FD/ED/FA/EA
是指令后缀
STMFD SP!, { R1-R7, LR } @将R1-R7和LR寄存器入栈,多用于保存子程序现场
LDMFD SP!, { R1-R7, LR } @将数据出栈,放入R1-R7寄存器,多用于子程序恢复现场
- 快拷贝寻址
将连续地址数据从存储器的某个位置赋值到另一个位置。
寻址指令:LDMIA/STMIA
、LDMDA/STMDA
、LDMIB/STMIB
、LDMDB/STMDB
STM/LDM
是指令前缀、IA/DA/IB/DB
是指令后缀
LDMIA R0!, { R1-R3 } @从R0寄存器指向的存储单元中读取3个字,将其分别放到R1~R3寄存器中
STMIA R0!, { R1-R3 } @将R1~R3寄存器内容存储到R0寄存器指向的存储单元中
- 相对寻址
BL NEXT
...
NEXT:
...
软件壳
1. 第一代壳(动态加载型壳)
这个时期的夹克技术主要对本地的DEX文件、so库、资源文件进行加密,再运行时进行动态还原。
这样的壳通常会采取一些常见的反调试方法来阻止针对软件进行的动态调试
- 缓存脱壳法
只要将/data/dalvik-cache
目录下的ODEX文件去除,进行一次ODEX操作就可以完成脱壳
但是这种很少能起作用了
- 内存Dump脱壳法
脱壳工具android-unpacker
:通过find_magic_memory()
方法读取/proc/pid/maps
内存映射表,找到DEX所在的内存起始位置,然后用dump_memory()
方法将内存Dump下来(peek_memory()
?)
- 动态调试脱壳法
本质还是内存Dump脱壳法,重要的是寻找合适的Dump时机,即DEX文件已经在内存中完全解密,且其中的代码还没有开始执行。
通过调试器给dvmDexFileOpenPartial()
或者dexFileParse()
方法设置断点,当执行到断点时,使用内存Dump脚本将其Dump下来,就可以完成脱壳。
甚至由于dexFileParse()
中用了memcmp(data, DEX_OPT_MAGIC , 4)
进行DEX文件头信息的判断,所以可以通过hook这个函数,获得data也就是dex文件的起始地址。
- Hook脱壳法
和动态调试脱壳一样,不同在于Hook则是使用Hook框架,配合Hook代码实现工具自动化脱壳。
书中的例子以Cydia Substrate
框架为例,使用了MSHookFunction()
来进行hook
- 系统定制脱壳法
与Hook脱壳法类似。针对第一代壳在dvmDexFileOpenPartial()
或者dexFileParse()
设断点,修改在源码中的实现,然后编译修改后的代码,以刷机方式实现脱壳
2. 第二代壳(代码抽取型壳)
除了第一代的技术特点,还提取了DEX文件中所有的方法实现并在本地进行加密保存。APK运行时,会调用软件壳的Native的解密方法对DEX文件中的方法进行还原。 ( 比如将DEX文件中的DexCode提取后填零,当APK运行时会在内存中进行动态解密,所有的解密方法内容指针都位于DEX文件结构体外部的内存中 ) 。
这种壳会通过在DEX方法执行前解密、执行后加密的技术手段,防止以内存Dump的方式对DEX文件进行脱壳。即便DEX已经加载在内存中,也仍然处于加密状态,但是所有DEX方法在运行时是解密的
这样的壳通常会采用多进程保护、API Hook等反调试与反内存Dump技术
- 内存重组脱壳法
通过解析内存中的DEX文件的格式,将其重新组合成DEX文件。在APK运行后的任意时刻使用kill
命令让程序暂停,然后从内存中将其重组并Dump出来。
libdvm.so → gDvm → userDexFiles → pEntries.isDex(为true
,pRawDexFile
有效) → pRawDexFile → pDvmDex → pDexFile
https://github.com/CvvT/dumpDex
- Hook脱壳法
不需要暂停程序的运行,重点是查找合适的Hook点
一个合适的Hook点是libdvm.so
中的dvmCallMethodV()
方法,这个方法是用来启动java方法的。当确定是Application
类的onCreate()
方法后,获取ClassObject类型的类对象指针,可以通过pDvmDex
字段选取DvmDex信息,然后dump出dex文件。
- 系统定制脱壳法
脱壳工具DexHunter
:https://github.com/zyq8709/DexHunter
3. 第三代壳(代码混淆壳,就是vmp壳)
在基于LLVM Pass实现的代码混淆壳、虚拟执行壳中使用较多的技术,有指令变换、花指令混淆、指令膨胀、代码流程混淆等。
这里的第三代壳指的是对原生程序代码进行混淆的第三代壳。
注入
ptrace
- 看代码心得
- 将当前的hook进程attach到目标进程,然后保存当前远程进程的上下文寄存器环境
- 获取远程进程中的mmap函数,申请一块内存空间,获得远程进程中申请的内存空间的首地址(在调用远程函数的时候arm_lr会设为0,这样远程进程在实行要调用的远程函数时就会因为返回地址有问题而暂停,这时候就可以保存他的寄存器上下文地址;返回值可以由arm_r0中获得)
- 获取远程进程中的dlopen、dlsym和dlclose,为了链接我们注入模块so文件(其中,传递参数的时候就需要将加载so库的路径和目标函数名写进远程进程内存中,方便远程函数调用)
- dlopen返回so文件在链接后的内存地址
- dlsym返回目标函数在内存里的地址
- 根据dlsym的返回值,远程call
ptrace_call
远程进程中的目标函数 - 恢复最初的上下文寄存器环境
- detach远程进程
我看的代码里没用到dlcose
函数,应该可用可不用吧,一般为了安全会用它吧
-
总结
- 还是要去看Linux编程,好多地方都是连蒙带猜。
- 就是开始call远程函数后,ARM_lr(即函数返回地址)一直都是0,在第五步之前一直没改过;让目标进程继续运行用的也
ptrace_cont
- 用ptrace进行hook的时候是将对hook函数的一些操作写在so的入口函数里的,dlsym后的函数调用也是调用的这个入口函数。
- 我看的2048的那个InlineHook例子是在入口函数中,dlopen目标函数在的so文件,然后再通过dlsym获得目标hook函数的地址。最终的Hook操作是通过
libsubstrate.so
中的MSHookFunction
函数来进行操作的,相当于是替换了目标函数。
Hook
基于异常的Hook
- 基本原理:利用SIGILL异常机制,对想要监控的地址设置一条非法指令。当程序执行到非法指令的位置就会回调预先设置好的异常处理函数。我们的Hook操作就是在异常处理函数中进行的。
1.初期设置
- 静态分析,找到我们要改写的目标指令的地址
- 利用
sigaction
结构体,对信号修改处理进行一定的设置
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);这边的sa_sigaction就是自己定义的处理异常的hook函数
sigset_t sa_mask;//用来设置在处理该信号时暂时将sa_mask指定的信号集搁置,这边需要使用sigemptyset进行置空初始化
int sa_flags;//设置为SA_SIGINFO ,指定处理函数为sigaction的sa_sigaction函数
void (*sa_restorer)(void);
}
//我们这边信号需要接收附加信息,就必须给sa_sigaction赋信号处理函数指针,同时还要给sa_flags赋SA_SIGINFO
- 保存目标地址的opcode,并改写为非法指令
指令集 | 对应非法指令 |
---|---|
ARM | 0xe7f***f* |
Thumb | 0xde** |
Thumb2 | 0xf7f*a*** |
但是我在他书上的代码写得是0xf7fXaXXX
才是arm的非法指令。。。而且他代码在进行辨别的时候只区分了Thumb和arm,没看到Thumb2;而且他那边给uiArmillegalValue
赋的值也是0x7f000f0
,不懂到底哪个才是对的
2. 异常回调的HookHandler
-
ExceptionHookHandler
有三个参数,signum为产生异常的信号量,Ssiginfo为信号信息,context为上下文句柄。其中寄存器状态在ucontext->uc_mcontext
中存着。 -
进行Hook操作
-
一开始肯定是运行到目标地址的,恢复目标地址。
为了之后能够再一次进行这个异常Hook,我们需要在执行下一条指令的时候,再将目标地址改写为非法指令的。所以我们需要再将下一条指令改写为非法指令进行这个操作。当实行到下一条指令的时候,将目标地址改写为非法指令。
指令地址也是有区别的,arm是+2,Thumb有些是+2有些是+4
指令集 | 长度 | 位[15:13] | 位[12:11] |
---|---|---|---|
16bit Thumb指令(+2) | 16位 | 非111 | 任意 |
16bit Thumb无条件跳转令(+2) | 16位 | 111 | 00 |
32bit Thumb指令(+4) | 32位 | 111 | 非00 |
记得在设置非法指令(即往里面写东西的时候)的时候要增加代码段的可写权限
sysconf(_SC_PAGESIZE); //获取系统页面大小,即代码段大小(?)
mprotect((void *) addr, (pagesize), PROT_READ | PROT_WRITE | PROT_EXEC);
导入表HOOK
- 基本原理:以so文件的导入作为目标进行函数指针替换
-
通过静态分析找到要hook的目标函数地址
-
打开要寻找got表的so文件(就普通的文件读取
'rb'
) -
找到.got表的起始位置和大小(起始位置是相对于模块基址的偏移量)
① 在elf header中找到存放所有节的节名的
.shstrtab
区(我们是在节头表里找的)iShstrtabOffset = Elf32_ElfHeader.e_shoff + //节区偏移 Elf32_ElfHeader.e_shstrndx * // 节区名字符串表的节区内的偏移(第几个) Elf32_ElfHeader.e_shentsize;//节区每个项目的大小
② 在
.shstrtab
节里找到.got
节Elf32_SectionHeader.sh_name
中指明了在.shstrtab
区里本节名的存储偏移量(是指在.shstrtab
中的)Elf32_SectionHeader.sh_type == SHT_PROGBITS
- 节中的
sh_addr
和sh_size
分别存储了节的地址和大小
-
获得目标模块基址后,在
.got
节里根据目标函数的地址,找到存放目标函数的地址(函数地址与函数地址之间+4)。修改地址权限,将新地址写进去。
Inline Hook
- 基本原理:直接修改需要Hook的位置的指令代码,并让其跳转到桩函数中。在桩函数里会处理寄存器信息,并调用自定义的桩函数。处理完桩函数后,跳转到存有原指令的地方并执行,然后再跳转回原来的函数。
//hook点信息
typedef struct tagINLINEHOOKINFO{
void *pHookAddr; //hook的地址
void *pStubShellCodeAddr; //shellcode stub的地址
void (*onCallBack)(struct pt_regs *); //回调函数,跳转过去的函数地址(这边参数不一定只是寄存器的值,见机行事)
void ** ppOldFuncAddr; //shellcode 中存放old function的地址
BYTE szbyBackupOpcodes[OPCODEMAXLEN]; //原来的opcodes
} INLINE_HOOK_INFO;
-
先通过静态分析,确定要hook的地址,对
INLINE_HOOK_INFO
结构体中的pHookAddr
和onCallBack
进行初始化,然后对目标地址进行hook(hook的时候要注意区分arm和thumb) -
InitArmHookInfo()
备份原opcode到szbyBackupOpcodes
-
通过
BuildStub()
构造stub函数,以及回调老函数(这边老函数指的是被hook替换的原opcode)在代码里,是借助shellcode来完成这一环的。
① 先
malloc()
分配空间并使用memcpy()
把shellcode写进去,还要更改他为可读可写可执行属性PROT_READ | PROT_WRITE | PROT_EXEC
(mprotect()
)② 将shellcode中的
_hookstub_function_addr_s
这个跳到用户自定义函数的地址进行修改和填充。并且在INLINE_HOOK_INFO
结构体中的pStubShellCodeAddr
进行内存shellcode的代码地址备份。 -
通过
BuildOldFunction()
构造16比特的原opcode的指令块,并将其改为可读可写可执行属性PROT_READ | PROT_WRITE | PROT_EXEC
(mprotect()
) 。先将
pstInlineHook->szbyBackupOpcodes
中的备份原opcode写进指令块(占8个比特),然后再填充跳转指令,要跳转的地址为pstInlineHook->pHookAddr + 8
。
最后填充pstInlineHook->ppOldFuncAddr
回调地址。填充完跳转地址后要记得用
cacheflush
进行刷新,防止崩溃
LDR PC, [PC, #-4] @ LDR PC, [PC, #-4]对应的机器码为:0xE51FF004
addr
http://ele7enxxh.com/Android-Arm-Inline-Hook.html
由于tx给的示例没有进行指令修复这个东西,因为如果涉及到跳转指令,pc可能会有变化。指令修复等看了ele7enxxh的分析再填充。
- 通过
RebuildHookTarget()
修改目标hook地址的操作权限,然后将pstInlineHook->pStubShellCodeAddr
中的地址作为跳转指令的目标地址进行填充
记得要cacheflush,防止崩溃
注意事项
-
Arm模式与Thumb模式的区别
在对一个函数进行Inline Hook时,首先需要判断当前函数指令是Arm指令还是Thumb指令,指令使用目标地址值的bit[0]来确定目标地址的指令类型。
bit[0]的值为1时,目标程序为Thumb指令;bit[0]值为0时,目标程序为ARM指令。
-
hook目标指令的覆盖要放到最后,这样可以防止在Hook一些频繁执行的函数时出现崩溃。
崩溃的原因: 在设置Inline Hook的过程中,程序正常运行了Hook逻辑,而桩函数或者原函数(就是原opcocde的那个指令块)未构造成功而倒是崩溃。
-
在tx给的另一个例子中,他是通过调用’/data/local/tmp/main/libsubstrate.so’中的’MSHookFunction’的函数来进行的。
使用IDA进行最简单的动态调试脱壳
-
基本原理:根据APP应用的启动流程可以知道,dex文件最终会被加载进内存,所以思路就是,只要在载入内存前,找到并dump出来即可
-
准备:
1、root好的测试机和模拟器(x86应该也可以)
2、IDA在6.6以上版本 或者手机为5.0以下 否则会出现 pie异常(抄来的)
1. 启动ida 端口监听
-
将IDA目录下的android_server文件push进/data/local下(只要让文件可执行就行)
-
赋予权限
chmod 777 /data/local/android_server
-
执行
./data/local/android_server
(如果是x86环境,IDA7.0有对应的android_x86_server) -
输入
adb forward tcp:23946 tcp:23946
,让手机中的23946端口映射到PC本地23946端口,用来IDA和手机通信连接 -
执行
adb shell am start –D –n com.example.protectapp/org.isclab.shh.protectapp.MainActivity
(包名/入口类 一定要写完整),让需要脱壳的程序在开启入口活动前进入等待调试连接状态。
2. IDA下断点
-
ARM环境:Debugger->Remote ARMLinux/Android debugger
x86环境:Debugger->Remote Linux debugger
-
hostname:127.0.0.1 port:23946
Debugger setup Event选项卡中选第3、4、5个
再选择目标进程进行attach(进程号可以用
ps
命令查) -
对加载dex文件的函数下断点
-
dvm环境
选择libdvm.so文件(modules模块下,也可以经过Debugger->Debugger windows->Modules List),双击进入
选择
dvmDexFileOpenPartialPKviPP6DvmDex
函数并按F2下断点(这个函数第一个参数是dex的内存首地址,第二个是dex的长度) -
art环境
选择libart.so文件,双击进入
搜索
Openmemory
关键字的函数,并按下F2下断点(这个函数第一个参数好像也是dex的内存首地址,但是好像8.0后的so文件中没有这个函数了)
-
3. 运行程序到下断处
-
输入
adb forward tcp:8700 jdwp:12876
(12876为attach目标进程的pid) -
输入
jdb –connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
jdb转发运行程序 -
按F9让程序继续运行(看到有的说重点再是让进程运行起来之后,再用jdb连接手机虚拟机,否则连不起来,会阻塞,会无法添加到vm,顺序很重要这就是我无法dump dex的原因吗?)
- 有些教程说要使用到Android Device Monitor,但是这个东西感觉已经名存实亡了,点开啥都没有,更别说选中要调试的目标进程了
如果有反调试是不能运行到断点的,下别的断点 1
- 打开寄存器窗口,可以看到有寄存器存着dex的地址(在我看的arm环境的教程里写的直接就是.dex文件,我自己在x86运行的时候看到EAX存着的是.odex文件,但是具体看他的文件开头的魔数又是dex文件的)
4. dump dex
dump dex可以有两种方法,我觉得第二种更简单粗暴一点,但是我两种都没有成功把dex dump下来。。。
- 直接调试时候断点到libdvm.so加载dex时候,找到在内存中的起始点r0和大小r1,然后通过ida
Shift +F2
运行脚本
这种只能dump安装运行时apk中classes.dex,不方便dump多次dex加载或动态加载时定位到起始地址
//这种是在dvm环境下的,因为dex的首地址和长度都在寄存器里
static main(void)
{
auto fp, begin, end, dexbyte;
fp = fopen("C:\\dump.dex", "wb");
begin = r0;
end = r0 + r1;
for ( dexbyte = begin; dexbyte < end; dexbyte ++ )
fputc(Byte(dexbyte), fp);
}
- 直接运行apk之后通过首先通过
cat /proc/[pid]/maps
查看目标dex文件所在的内存地址,这样就可以查找所有加载过的dex内存映射地址,不论是在什么时候加载的,只要运行了,就必会在内存中找到相应的映射地址
static main(void)
{
auto fp, begin, end, dexbyte;
fp = fopen("C:\\dump.dex", "wb");
begin = 0x4b92f000;
end = 0x4bce7000;
for ( dexbyte = begin; dexbyte < end; dexbyte ++ )
fputc(Byte(dexbyte), fp);
}
- 总结
-
真的好坑啊,x86都还给老师了。而且IDA那个并不是dump错了,而是根本就没办法dump下来,脚本执行后嘛也没有,不知道是什么原因。
-
现在基本没看到基于8.0以上的脱壳,所以可能还需要去看系统源码才能知道从哪里拦截dex文件
-
odex和dex结构啥的要重新看看,我已经忘得七七八八了;arm指令集也是
- 附
-
对于dump出来的文件是
odex
直接打开看不够清晰(我就说嘛),如用GDA工具打开里面都是类似汇编语言。这时就需要用到baksmali.jar
和smali.jar
来转换成dex文件,可以直接方便查阅。 -
这只是最简单脱壳方法,很多高级壳会动态修改dex的结构体,比如将codeoffset指向内存中的其他地址,这样的话你dump出来的dex文件其实是不完整的,因为代码段保存在了内存中的其他位置。那么针对这种反调试情况下以及其他阻碍动态调试下,dump内容和ida调试都很难进行下去
-
网上有这样的文章:利用开源脱壳工具DexExtractor来脱壳。它的原理就是修改系统的DexFile.cpp源码,在解析dex的函数开头处加上自己的dumpdex逻辑。 这样不论怎么绕,只要加载了dex就可以dump出来,可以研究下,传送门:http://blog.csdn.net/jiangwei0910410003/article/details/54409957
- IDA 调试快捷键
按键 | 作用 |
---|---|
F2 | 下断点 |
F4 | 运行到光标处 |
F7 | 单步步入调试 |
F8 | 单步步过调试 |
Ctrl + F7 | 运行到返回 |
NOP指令 00 00 A0 E1 (MOV r0, r0
)