13、Native层逆向(二):ARM汇编基础(指令集、寄存器、栈帧)、IDA Pro基础使用
好,咱们接着聊Native层逆向。上一章我们把ARM和Thumb的切换、so文件的加载流程捋了一遍。这一章,我带你深入到底层——ARM汇编。说实话,很多做逆向的朋友一看到汇编就头大,觉得那是底层开发才该碰的东西。但你想啊,你逆向一个so文件,反编译出来全是汇编指令,你不认识它,怎么分析逻辑?
所以这一章,咱们就啃这块硬骨头。我会用最直白的方式,把ARM汇编的指令集、寄存器、栈帧这些概念讲清楚。最后再带你上手IDA Pro,看看怎么用它来辅助分析。嗯,开始吧。
一、ARM指令集概览
ARM处理器有两种主要的指令集状态:ARM状态和Thumb状态。ARM指令是32位定长的,每条指令占4个字节。Thumb指令是16位或32位变长的,密度更高,代码更紧凑。咱们逆向时,大部分so文件里的函数都是Thumb指令,但关键的系统库函数往往是ARM指令。
我个人习惯,在IDA里先看函数的指令集类型。怎么判断?看函数地址的最低位:如果最低位是1,说明是Thumb指令;如果是0,就是ARM指令。这个细节,我在分析一个混淆过的so时吃过亏——当时没注意指令集切换,反汇编出来的代码全是乱的。
1. 常用ARM指令
ARM指令虽然多,但逆向中常用的就那么几十条。我按功能分类给你列一下:
| 类别 | 指令 | 说明 |
|---|---|---|
| 数据传输 | MOV, LDR, STR | 寄存器间传值、从内存加载、存入内存 |
| 算术运算 | ADD, SUB, MUL, SDIV | 加减乘除,注意ARM没有除法指令(用SDIV) |
| 逻辑运算 | AND, ORR, EOR, BIC | 与、或、异或、位清除 |
| 比较与跳转 | CMP, B, BL, BX, BLX | 比较、无条件跳转、带链接跳转、跳转并切换指令集 |
| 栈操作 | PUSH, POP, STM, LDM | 压栈、出栈、多寄存器存储/加载 |
这里我特别想提一下 BL 和 BX 的区别。BL 是带链接的跳转,它会将返回地址保存到LR寄存器(R14)中,用于函数调用。BX 是跳转并切换指令集,它根据目标地址的最低位决定切换到ARM还是Thumb。我在逆向一个加壳so时,发现壳代码就是用 BX 在ARM和Thumb之间来回切换,搞得我头都大了。
2. 条件执行
ARM指令的一个特色是条件执行。每条指令都可以带一个条件码后缀,比如 MOVEQ 表示“如果相等则执行MOV”,ADDNE 表示“如果不相等则执行ADD”。条件码有16种,常用的有:
EQ:相等(Z标志置位)NE:不相等(Z标志清零)GT:有符号大于LT:有符号小于HI:无符号大于LO:无符号小于
为什么会这样设计?说白了,是为了减少分支指令的数量,提高流水线效率。但在逆向时,条件执行会让代码逻辑变得复杂。我建议你遇到条件指令时,先在脑子里把它拆成“if-else”结构,这样更容易理解。
二、ARM寄存器体系
ARM处理器有37个寄存器,但咱们逆向时只需要关注其中16个通用寄存器(R0-R15)和当前程序状态寄存器(CPSR)。
这16个寄存器各有分工:
| 寄存器 | 别名 | 用途 |
|---|---|---|
| R0-R3 | 参数寄存器 | 传递函数参数,R0也用于返回值 |
| R4-R11 | 通用寄存器 | 保存局部变量,函数调用时需保存 |
| R12 | IP(内部过程调用) | 用于临时数据,链接时使用 |
| R13 | SP(栈指针) | 指向当前栈顶 |
| R14 | LR(链接寄存器) | 保存函数返回地址 |
| R15 | PC(程序计数器) | 指向当前正在执行的指令地址 |
嗯,这里有个坑。在ARM状态下,PC指向当前指令地址加8(因为ARM是三级流水线)。但在Thumb状态下,PC指向当前指令地址加4。我在IDA里看反汇编时,经常看到PC相关的计算,一开始总算不对。后来我养成了一个习惯:遇到PC参与运算的指令,先确认当前是指令集状态,再手动算一遍偏移。
Alt+G 可以查看当前函数的指令集类型。如果看到 T 标志,说明是Thumb模式。这个功能我几乎每天都会用到。
三、栈帧结构
栈帧是函数调用时在栈上分配的一块区域,用于保存局部变量、传递参数、保存返回地址等。理解栈帧结构,是逆向分析函数调用的关键。
一个典型的ARM函数调用过程是这样的:
- 调用者将参数放入R0-R3(如果参数超过4个,多余的参数压入栈中)
- 调用者执行
BL指令跳转到被调用函数,同时将返回地址存入LR - 被调用函数将LR压栈(保存返回地址),然后将SP减去一个值(分配栈空间)
- 被调用函数执行自己的逻辑
- 被调用函数将SP加上之前减去的值(释放栈空间),然后弹出LR到PC(返回)
用汇编代码表示就是:
; 被调用函数入口
function_name:
PUSH {LR} ; 保存返回地址
SUB SP, SP, #0x10 ; 分配16字节栈空间
; ... 函数体 ...
ADD SP, SP, #0x10 ; 释放栈空间
POP {PC} ; 返回(将LR弹出到PC)
我曾经逆向过一个混淆过的so,它的函数入口不是标准的 PUSH {LR},而是用 STMDB SP!, {R4-R11, LR} 这种多寄存器压栈指令。当时我差点没认出来,后来才意识到这是编译器优化后的结果。所以,遇到非标准的栈帧操作,别慌,先看看是不是编译器搞的鬼。
四、IDA Pro基础使用
IDA Pro是逆向分析的神器,没有之一。我刚开始用IDA时,面对密密麻麻的反汇编窗口也是一脸懵。但用多了,你会发现它其实很顺手。
1. 加载so文件
把so文件拖进IDA,它会自动识别文件格式(ELF),然后让你选择加载选项。这里我建议你勾选“Load resources”和“Make imports section”,这样IDA会自动解析导入表和导出表。
加载完成后,IDA会显示一个反汇编窗口。默认是文本视图,你可以按 空格键 切换到图形视图。图形视图会把函数调用关系用流程图的形式展示出来,非常直观。
2. 常用快捷键
IDA的快捷键很多,但咱们逆向时常用的就那么几个:
| 快捷键 | 功能 |
|---|---|
G |
跳转到指定地址 |
N |
重命名变量或函数 |
X |
查看交叉引用(谁调用了这个函数) |
F5 |
反编译为伪代码(需要Hex-Rays插件) |
Ctrl+Enter |
跳转到选中的地址或函数 |
Alt+M |
添加书签 |
我个人最常用的是 X 查看交叉引用。分析一个函数时,先看看谁调用了它,能快速理清调用链。有一次我分析一个恶意so,就是通过交叉引用找到了它的入口函数,然后顺藤摸瓜挖出了整个恶意逻辑。
3. 识别关键函数
在IDA里,你可以通过导入表快速找到so文件调用了哪些系统函数。比如 JNI_OnLoad 是so的初始化函数,Java_xxx 是JNI函数。这些函数往往是分析的突破口。
另外,IDA的“Strings”窗口(按 Shift+F12)也很重要。很多so文件会在代码中嵌入字符串,比如URL、密钥、错误信息等。找到这些字符串,然后查看它们的交叉引用,就能定位到使用这些字符串的代码。
F9 启动)可以配合使用。
五、知识体系总览
为了让你更直观地理解本章的知识结构,我画了一张图:
这张图把本章的核心知识点串起来了。ARM指令集、寄存器体系、栈帧结构是基础,IDA Pro是工具。基础打牢了,工具用熟了,逆向Native层就不再是难事。
六、实战小技巧
最后,分享几个我在实战中总结的小技巧:
- 看函数开头:函数开头通常是
PUSH {LR}或PUSH {R4-R11, LR},如果看到SUB SP, SP, #xxx,说明函数有局部变量。这个模式我百试不爽。 - 看函数结尾:函数结尾通常是
POP {PC}或BX LR。如果看到POP {R4-R11, PC},说明函数保存了多个寄存器。 - 看参数传递:函数调用前,参数会放到R0-R3中。如果看到
MOV R0, #0然后BL some_function,说明传了一个整数0给函数。 - 看字符串引用:在IDA中,字符串通常以
.rodata段的形式存在。找到字符串后,按X查看引用,就能定位到使用它的代码。
好了,这一章的内容就到这儿。ARM汇编和IDA Pro是Native层逆向的两大基石,多练、多分析,慢慢就会形成肌肉记忆。下一章,咱们会深入动态调试,用实际案例来巩固今天学的知识。
公众号:蓝海资料掘金营,微信deep3321