ARM简介
ARM, Advanced RISC Machine, 是一个32位RISC处理器架构。ARM处理器广泛应用于嵌入式系统和物联网设备中,例如路由器,交换机,智能手机等。
ARM处理器支持7中运行模式,每种模式有自己的堆栈空间以及一组不同的寄存器子集。
- 用户模式(user):正常程序执行模式;
- 快速中断模式(FIQ):高优先级的中断产生会进入该种模式,用于高速通道传输;
- 外部中断模式(IRQ):低优先级中断产生会进入该模式,用于普通的中断处理;
- 特权模式(Supervisor):复位和软中断指令会进入该模式;
- 数据访问中止模式(Abort):当存储异常时会进入该模式;
- 未定义指令中止模式(Undefined):执行未定义指令会进入该模式;
- 系统模式(System):用于运行特权级操作系统任务;
不过在Cortex系列中稍有不用,Cortex-A 和 Cortex-R 处理器有以上7种模式,而Cortex-M则只有两种模式,Thread 模式和 Handler 模式,Thread 模式没有特权,用于应用程序代码, Handler 模式有特权,用于异常处理程序(以下情况不适用于Cortex-M处理器)。
- 由于大部分的嵌入式 IoT 设备的 ARM 指令集都是基于 32 位小端序的 armeabi 架构,因此在介绍时就会以此架构为例进行讲解,使用 file 命令查看到的信息如下:
1 | bin/busybox: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped |
寄存器介绍
ARM处理器有40个寄存器,32个通用寄存器,7个状态寄存器,一个PC寄存器,每一个模式对应有一种寄存器。
几个常用的寄存器:
- R0-R3 用户函数调用参数传递
- R13,堆栈指针寄存器,也称为 SP
- R14,又称为LR,链接寄存器,用于保存函数调用时的返回地址
- R15,又称为PC,程序计数器。在ARM状态下,位[1:0]为0,位[31:2]用于保存PC;在Thumb状态下,位[0]为0,位[31:1]用于保存PC。对于ARM指令集而言,PC总是指向当前指令的下两条指令的地址,即PC的值为当前指令的地址值加8个字节。
函数参数传递
R0-R3 为用户函数调用参数传递的寄存器,分别用于传递函数的第一、第二、第三个参数,如当调用 test 函数时,汇编代码中会将 $R0、$R1、$R2 寄存器分别赋值为立即数 1、2、3,接着再去执行相关汇编指令去调用 test 函数。
1 | MOV R0, #1 |
- 若函数调用所需要的参数数量大于 3 时,同样会借助栈进行剩下参数的传递。
指令集介绍
此处列举出在对 armeabi 指令集进行逆向过程中常使用的汇编指令类型。
Load/Store 类型指令
Load/Store 类型的指令的作用为从地址中读取数据和从地址中存入数据,Load 类型的指令有 LDR、LDRB 等,用法举例如下:
1 | LDR R0, [addr, #10] // 将 addr 地址偏移10字节处的四字节内容存入 R0 寄存器中 |
Store 类型的指令有 STR、STRB 等,用法举例如下:
1 | STR R0, [addr, #10] // 将 R0 寄存器的数据存储到 addr 地址偏移10字节内存地址处 |
算数运算类型指令
armeabi 指令集的算数运算类型指令名称与 x86 几乎完成相似,只是操作数的格式与 x86 指令架构不同。
- 加法类型指令:
1 | ADD R0, R1, #1 // 将 R1 寄存器的值加1之后存储到R0寄存器中 |
- 减法类型指令:
1 | SUB R3, R11, #10 // 将 R11 寄存器的值减去10之后存储到 R3 寄存器中 |
条件类型指令
ARM 指令集中的条件类型指令和x86完全一致,常见的条件比较类型的指令有:
1 | BNE 0xC680 // 上一条 cmp 指令比较结果不相等时,跳转到 0xC680 地址处 |
跳转类型指令
常见的跳转指令有:
1 | B 0xC680 // 直接跳转到 0xC680 地址处,即将 PC 寄存的值赋值为 0xC680 |
寻址特点
- pc 方式寻址
如果汇编代码中使用 pc 间接寻址的话,实际得到的是 pc+偏移量+8 的地址中的内容,如:
1 | # pc = 0x1000 |
那么 r0 寄存器的结果是:r0 = [0x1000+12+8] = [0x101a]
堆栈结构
ARM 指令集对栈的操作和x86也几乎一致,在初始化函数栈时,会使用 PUSH 指令,将调用栈前的环境保存起来,在函数退出时,使用 POP 指令还原调用前的环境。
但是在 IDA 中查看函数的汇编代码,在开头部分可以看到 STMFD 这个指令,找不到 push 指令。
1 | STMFD SP!, {R4,R11,LR} |
实际上 STMFD 指令可以看成是 push 指令的扩展,如针对于上述的指令可以扩展成如下指令:
1 | PUSH R4 |
所以该指令的作用也就是将第二个操作数的寄存器依次压入到栈中(SP 指针指向的位置开始)。
同样在函数的末尾进行栈还原时,可以发现 LDMFD 指令,该指令的作用和 STMFD 指令相反,将从 SP 栈顶的数据开始依次 POP 到 R4-R11 以及 PC 寄存器中。
1 | LDMFD SP!, {R4,R11,PC} |
因此也可以扩展为如下指令:
1 | POP R4 |
- 函数开头将 LR 寄存器压入到栈中,函数退出时将 LR 寄存器的值通过栈传递到 PC 寄存器中,可以说明 LR 寄存器存储的是函数返回地址。
逆向技巧
可能需要调整汇编模式:
在IDA中可以Edit->segments->change segment register value(快捷键ALT+G)中改变T的值来改变IDA对代码的解析方式。
- 0为ARM
- 1为thumb
ARMPwn
arm指令和X86有相似之处,相比mips指令更容易理解.
关于X86用到的ROP相关指令和ARM指令的对比:
arm架构下的函数返回时,使用LDMFD指令和BX指令完成调用前环境还原和跳转.LDMFD指令pop保存的寄存器的值以及LR的值,然后跳转到LR.在ROP的时候很多情况下可以利用LDMFD指令来减少ROP时用到的gadgets.
寻找gadgets
ROP的最终目的是将可控的命令传入system函数并进行调用,cmd一般来说放到栈上,这就需要找一个gadget来设置R0为一个栈地址,然后再寻找一个gadget跳转并调用system函数即可.
ARM架构下的ROP再函数返回时大多数情况pop还原一些保存的寄存器的值.例如R4-R5,R4-R7等,这种情况可以利用函数本身的gadget来减少ROP的复杂程度.而如果函数返回时没有pop出lr以外的寄存器的话,这种情况就需要另外调整寻找gadgets的思路了.
函数推出时pop多个寄存器
首先利用ROPGadget将libc动态链接库中可利用的gadget找出来