ARM入门
一、ARM简介
CTF比赛中,大部分题的都是x86、x86_64的程序,这类程序是属于Intel处理器支持的。但其实,在生活中配置ARM处理器的设备要多得多,比如:Android、网络设备、智能家居等
Intel和ARM之间的区别主要是指令集:
- CISC 复杂指令集
- RISC 精简指令集
精简指令集通过减少每条指令的时钟周期来缩短执行时间,可以更快的执行指令,但因为指令较少,因此在实现功能时,会显得比Intel冗长。其次,在x86上,大多数指令都可以直接对内存中的数据进行操作,而在ARM上,必须先将内存中的数据从内存移到寄存器中,然后再进行操作
ARM和x86之间的更多区别是:
- 在ARM中,大多数指令可用于条件执行
- Intel x86和x86-64系列处理器使用little-endian格式
- 在v3之前,ARM体系结构为little-endian字节序,此后,ARM处理器成为BI-endian,并具允许可切换字节序
ARM汇编程序的编译:
- as program.s –o program.o
- ld program.o –o program
二、ARM数据类型和寄存器
与高级语言类似,ARM支持对不同数据类型的操作,通常是与ldr、str这类存储加载指令一起使用:
-
带符号的数据类型可以同时包含正值和负值,因此范围较小
-
无符号数据类型可以容纳较大的正值(包括“零”),但不能容纳负值,因此范围更广
2.1 字节序
有两种查看内存中字节的基本方法:小端(LE)或大端(BE)。这两者的区别在于对象的每个字节存储在内存中的字节顺序
在像Intel x86这样的低位字节机器上,最低有效字节存储在最低地址上,在big-endian计算机上,最高有效字节存储在最低地址。ARM体系结构在版本3之前是低端字节序的,因为那时它是双向字节序的,这意味着它具有允许可切换字节序的设置
2.2 寄存器
寄存器的数量取决于ARM版本,ARM32有30个通用寄存器(基于ARMv6-M和基于ARMv7-M的处理器除外),前16个寄存器可在用户级模式下访问,其他寄存器可在特权软件执行中使用
其中,r0-15寄存器可在任何特权模式下访问。这16个寄存器可以分为两组:通用寄存器(R0-R11)和专用寄存器(R12-R15)
2.2.1 32位寄存器
R0-R12:可在常规操作期间用于存储临时值,指针(到存储器的位置)等,例如:
- R0在算术操作期间可称为累加器,或用于存储先前调用的函数的结果
- R7在处理系统调用时非常有用,因为它存储系统调用号
- R11帮助我们跟踪用作帧指针的堆栈的边界
- ARM上的函数调用约定指定函数的前四个参数存储在寄存器r0-r3中
- R13:SP(堆栈指针)。堆栈指针指向堆栈的顶部。堆栈是用于函数特定存储的内存区域,函数返回时将对其进行回收。因此,通过从堆栈指针中减去我们要分配的值(以字节为单位),堆栈指针可用于在堆栈上分配空间。换句话说,如果我们要分配一个32位值,则从堆栈指针中减去4
- R14:LR(链接寄存器)。进行功能调用时,链接寄存器将使用一个内存地址进行更新,该内存地址引用了从其开始该功能的下一条指令。这样做可以使程序返回到“父”函数,该子函数在“子”函数完成后启动“子”函数调用
- R15:PC(程序计数器)。程序计数器自动增加执行指令的大小。在ARM状态下,此大小始终为4个字节,在THUMB模式下,此大小始终为2个字节。当执行转移指令时,PC保留目标地址。在执行期间,PC在ARM状态下存储当前指令的地址加8(两个ARM指令),在Thumb(v1)状态下存储当前指令的地址加4(两个Thumb指令)。这与x86不同,x86中PC始终指向要执行的下一条指令
TIPS:
- 当参数少于4个时,子程序间通过寄存器R0~R3来传递参数;当参数个数多于4个时,将多余的参数通过数据栈进行传递,入栈顺序与参数顺序正好相反,子程序返回前无需恢复R0~R3的值
- 在子程序中,使用R4~R11保存局部变量,若使用需要入栈保存,子程序返回前需要恢复这些寄存器;R12是临时寄存器,使用不需要保存
- R13用作数据帧指针,记作SP;R14用作链接寄存器,记作LR,用于保存子程序返回时的地址;R15是程序计数器,记作PC
- ATPCS规定堆栈是满递减堆栈FD
- 子程序返回32位的整数,使用R0返回;返回64位整数时,使用R0返回低位,R1返回高位
2.2.2 64位寄存器
ARM64位参数调用规则遵循AAPCS64,规定堆栈为满递减堆栈,寄存器调用规则如下:
- 子程序调用时必须要保存的寄存器:X19~X29和SP(X31)
- 不需要保存的寄存器:X0X7,X9X15
2.2.3 32位与64位寄存器的差异
- 栈arm32下,前4个参数是通过r0~r3传递,第4个参数需要通过sp访问,第5个参数需要通过sp + 4 访问,第n个参数需要通过sp + 4*(n-4)访问
- arm64下,前8个参数是通过x0~x7传递,第8个参数需要通过sp访问,第9个参数需要通过sp + 8 访问,第n个参数需要通过sp + 8*(n-8)访问
- ARM指令在32位下和在64位下并不是完全一致的,但大部分指令是通用的,特别的,” mov r2, r1, lsl #2”仅在ARM32下支持,它等同于ARM64的” lsl r2, r1, #2”
- 还有一些32位存在的指令在64位下是不存在的,比如vswp指令,条件执行指令subgt,addle等
三、ARM指令集
3.1 ARM指令集简介
ARM处理器具有两种可以运行的主要状态(此处不包括Jazelle):ARM和Thumb
这两种状态之间的主要区别是指令集,其中ARM状态下的指令始终为32位,Thumb状态下的指令始终为16位(但可以为32位)
现在,ARM引入了增强的Thumb指令集(Thumbv2),该指令集允许32位Thumb指令甚至条件执行,而在此之前的版本中是不可能的,为了在Thumb状态下使用条件执行,引入了“ it”指令。但是,这个指令在后来的版本中被删除并替换成了其他的
在编写ARM shellcode时,我们需要摆脱NULL字节,并使用16位Thumb指令而不是32位ARM指令来减少使用它们的机会
Thumb和ARM一样也有不同的版本:
- Thumb-1(16位指令):在ARMv6和更早的体系结构中使用
- Thumb-2(16位和32位指令):通过添加更多指令并使它们的宽度为16位或32位(ARMv6T2,ARMv7)来扩展Thumb-1
- ThumbEE:包括一些针对动态生成的代码的更改和添加
ARM和Thumb之间的区别:
- 条件执行:ARM状态下的所有指令均支持条件执行。某些ARM处理器版本允许使用“it”指令在Thumb中有条件执行
- 32位ARM和Thumb指令:32位Thumb指令带有.w后缀
- 桶式移位器(barrel shifter)是ARM模式的另一个独特功能。它可以用于将多个指令缩小为一个。例如,您可以使用左移,而不是使用两条指令,将寄存器乘以2并使用mov将结果存储到另一个寄存器中:mov r1, r0, lsl
要切换处理器执行的状态,必须满足以下两个条件之一:
- 我们可以使用分支指令BX(分支和交换)或BLX(分支,链接和交换)并将目标寄存器的最低有效位设置为1。这可以通过在偏移量上加上1来实现,例如0x5530 + 1。可能会认为这会导致对齐问题,因为指令是2字节或4字节对齐的。这不是问题,因为处理器将忽略最低有效位
- 我们知道如果当前程序状态寄存器中的T位置1,则我们处于Thumb模式
3.2 ARM指令简介
汇编语言由指令构成,而指令是主要的构建块。ARM指令通常后跟一个或两个操作数,并且通常使用以下模板:
MNEMONIC {S} {condition} {Rd},Operand1,Operand2
注意,由于ARM指令集的灵活性,并非所有指令都使用模板中提供的所有字段。其中,条件字段与CPSR寄存器的值紧密相关,或者确切地说,与寄存器内特定位的值紧密相关
Operand2被称为灵活操作数,因为我们可以以多种形式使用它,例如,我们可以将这些表达式用作Operand2:
下面以一些常见指令为例:
3.2.1 RAM32的指令:
指令 | 描述 | 指令 | 描述 |
---|---|---|---|
MOV | 移动数据 | EOR | 按位异或 |
MVN | 移动并取反 | LDR | 加载 |
ADD | 加 | STR | 存储 |
SUB | 减 | LDM | 加载多个 |
MUL | 乘 | STM | 存储多个 |
LSL | 逻辑左移 | PUSH | 入栈 |
LSR | 逻辑右移 | POP | 出栈 |
ASR | 算术右移 | B | 跳转 |
ROR | 右旋 | BL | Link跳转 |
CMP | 比较 | BX | 分支跳转 |
AND | 按位与 | BLX | 使用Link分支跳转 |
ORR | 按位或 | SWI/SVC | 系统调用 |
3.2.2 LDR 和 STR:
ARM使用加载存储模型进行内存访问,这意味着只有加载/存储(LDR和STR)指令才能访问内存
通常,LDR用于将某些内容从内存加载到寄存器中,而STR用于将某些内容从寄存器存储到内存地址中
- LDR操作:将R0中的地址的值加载到R2寄存器中
- STR操作:将R2中的值存储到R1中的内存地址处
3.2.3 LDM 和 STM:
在执行压栈和出栈的指令时,通常使用LDMIA/STMDB
但事实上在汇编的过程中,可以看到LDMIA和STMDB指令已转换为PUSH和POP,那是因为 PUSH和STMDB sp!, reglist,POP和LDMIA sp! Reglist是等价的
3.2.4 条件执行:
条件码应用举例:
比较两个值大小,并进行相应加1处理,C语言代码为:
if ( a > b )
a++;
else
b++;
对应的ARM指令如下(其中R0中保存a 的值,R1中保存b的值):
CMP R0, R1 ; R0与R1比较,做R0-R1的操作
ADDHI R0, R0, #1 ;若R0 > R1, 则R0 = R0 + 1
ADDLS R1, R1, #1 ; 若R0 <= R1, 则R1 = R1 + 1
3.2.5 分支:
分支指令分为三种:
- 支(B)
- 简单跳转到功能
- 分支链接(BL)
- 将(PC + 4)保存为LR并跳转至功能
- 分支交换(BX)和分支链接交换(BLX)
- 与B / BL +exchange指令集相同(ARM <-> Thumb)
- 需要一个寄存器作为第一个操作数:BX / BLX reg
- BX / BLX用于将指令集从ARM交换到Thumb
3.2.6 有条件分支:
BEQ的条件分支:将值移入寄存器并在寄存器等于指定值的情况下跳转到另一个函数
3.2.7 ARM32与ARM64常用指令对应关系
四、ARM 堆栈和函数
栈是一种先进后出的数据结构,栈底是第一个进栈的数据所处位置,栈顶是最后一个数据进栈所处的位置。在创建进程时会在栈中分配相应内存,我们使用堆栈来保存局部变量、参数传递、保存寄存器的值
ARM中主要使用PUSH和POP指令与堆栈进行交互
注意,这里的PUSH和POP是其他一些与内存相关的指令的别名,而不是真实的指令
4.1 四种堆栈:ARM采用的满降栈
- 满/空栈
- 根据SP指针指向的位置,栈可以分为满栈和空栈
- 满栈:当堆栈指针总是指向最后压入堆栈的数据
- 空栈:当堆栈指针总是指向下一个将要放入数据的空位置
- 升/降栈
- 根据SP指针移动的方向,栈可以分为升栈和降栈
- 升栈:随着数据的入栈,SP指针从低地址->高地址移动
- 降栈:随着数据的入栈,SP指针从高地址->低地址移动
这是不同的栈使用的压栈/出栈(存储多个/加载多个)指令:
这个例子是在满降的栈中,在执行push和pop操作时,sp的变化:
- 最开始,sp指向0x01的内存,然后向r0中写入立即数2,sp不变
- 执行push操作,r0中的立即数2入栈,sp指向0x02的内存
- 再次将立即数3写入r0,sp不变,整个栈是从低地址向高地址生长的
- 最后执行pop,将栈顶(低位)的立即数2弹出到r0,sp重新指向0x01,r0从3变为2
4.2 栈帧
栈帧(stack frame)就是一个函数所使用的那部分栈,所有函数的栈帧串起来就组成了一个完整的栈。栈帧的两个边界分别由fp(r11)和sp(r13)来限定
前面描述的是ARM的栈帧布局方式。main stack frame为调用函数的栈帧,func1 stack frame为当前函数(被调用者)的栈帧,栈底在高地址,栈向下增长
FP就是栈基址,它指向函数的栈帧起始地址;SP则是函数的栈指针,它指向栈顶的位置。ARM压栈的顺序依次为当前函数指针PC、返回指针LR、栈指针SP、栈基址FP、传入参数个数及指针、本地变量和临时变量
如果函数准备调用另一个函数,跳转之前临时变量区先要保存另一个函数的参数。从main函数进入到func1函数,main函数的上边界和下边界保存在被它调用的栈帧里面
ARM也可以用栈基址和栈指针明确标示栈帧的位置,栈指针SP一直移动
4.3 函数体
ARM中的函数主要由Prologue、Body、Epilogue组成
Prologue的目的是保存程序的先前状态,并为函数的局部变量设置堆栈
Body主要负责实现函数功能,简单的例子:
注意:当要传递的参数超过4个时,ARM会另外用堆栈来存储其余参数
Epilogue用于将程序的状态恢复到其初始状态
4.4 叶函数和非叶函数
叶函数是指本身不会调用其他函数。非叶函数是指除了它自己的逻辑外,还会调用到其他的函数。这两种函数的实现是相似的,但也存在一些不同
他们Prologue和Epilogue的实现方式不同:
- 非叶函数中Prologue会将更多的寄存器保存到堆栈中。因为在执行非叶函数时LR会被修改,因此需要保留该寄存器的值,便于以后恢复
- 在跳转到主函数之前,BL指令将函数main的下一条指令的地址保存到LR寄存器中。由于叶函数不会在执行过程中更改LR寄存器的值,因此该寄存器现在可用于返回父(主)函数
五、搭建ARM PWN环境
-
Ubuntu 18.04
-
安装 gdb、gdb-multiarch
$ sudo apt-get install gdb gdb-multiarch
-
安装 gdb plugin(peda、pwndbg、gef)
- peda-arm:https://github.com/alset0326/peda-arm
- pwndbg:https://github.com/pwndbg/pwndbg
- gef:https://github.com/hugsy/gef
-
安装 pwntools
$ sudo pip install pwntools
qemu是一款可执行硬件虚拟化的虚拟机,与他类似的还有Bochs、PearPC,但qemu具有高速(配合KVM)、跨平台的特性
qemu主要有两种运行模式:
-
qemu-user
-
qemu-system
安装 qemu-user:
$ sudo apt-get install qemu qemu-user qemu-user-static
此时可以运行静态链接的arm程序,而要运行动态链接的程序,需要安装对应架构的动态链接库:
$ apt search "libc6-" | grep "arm“
安装 qemu-system:
$ sudo apt-get install qemu qemu-user-static qemu-system uml-utilities bridge-utils
配置qemu-system网络:
qemu-system模式配置网络常见的方法是tap桥接,安装网络配置的依赖文件:
$ sudo apt install uml-utilities bridge-utils
修改Ubuntu主机网络接口配置文件:
$ sudo vim /etc/network/interfaces