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 压栈、出栈、多寄存器存储/加载

这里我特别想提一下 BLBX 的区别。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参与运算的指令,先确认当前是指令集状态,再手动算一遍偏移。

小技巧: 在IDA中,按 Alt+G 可以查看当前函数的指令集类型。如果看到 T 标志,说明是Thumb模式。这个功能我几乎每天都会用到。

三、栈帧结构

栈帧是函数调用时在栈上分配的一块区域,用于保存局部变量、传递参数、保存返回地址等。理解栈帧结构,是逆向分析函数调用的关键。

一个典型的ARM函数调用过程是这样的:

  1. 调用者将参数放入R0-R3(如果参数超过4个,多余的参数压入栈中)
  2. 调用者执行 BL 指令跳转到被调用函数,同时将返回地址存入LR
  3. 被调用函数将LR压栈(保存返回地址),然后将SP减去一个值(分配栈空间)
  4. 被调用函数执行自己的逻辑
  5. 被调用函数将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、密钥、错误信息等。找到这些字符串,然后查看它们的交叉引用,就能定位到使用这些字符串的代码。

注意: 有些so文件会对字符串进行加密或混淆,直接搜索字符串可能找不到。这时你需要动态调试,在运行时解密字符串。IDA的调试器(按 F9 启动)可以配合使用。

五、知识体系总览

为了让你更直观地理解本章的知识结构,我画了一张图:

Native层逆向(二):ARM汇编与IDA Pro知识体系 ARM指令集 寄存器体系 栈帧结构 ARM状态 vs Thumb状态 常用指令:MOV, LDR, STR, BL, BX 条件执行:EQ, NE, GT, LT R0-R3:参数与返回值 R13(SP):栈指针 R14(LR):返回地址 R15(PC):程序计数器 函数调用过程 PUSH/POP 操作 栈空间分配与释放 IDA Pro 基础使用

这张图把本章的核心知识点串起来了。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汇编逆向,说白了就是“读指令、看寄存器、理栈帧”。指令集是语言,寄存器是变量,栈帧是内存布局。把这三点搞明白,任何so文件在你面前都是透明的。

好了,这一章的内容就到这儿。ARM汇编和IDA Pro是Native层逆向的两大基石,多练、多分析,慢慢就会形成肌肉记忆。下一章,咱们会深入动态调试,用实际案例来巩固今天学的知识。


公众号:蓝海资料掘金营,微信deep3321