初窥ARM&MIPS
雨下整夜,我的爱溢出就像雨水。 ——《七里香》周杰伦
之前做的pwn题都是基于x86架构的ELF文件题目,这一次参加TSCTF,有一大半的题目是非x86架构的题目,不得不现学相关知识,用这篇博客来总结一下在题目中学到的东西。
ps:题目的相关二进制文件和EXP放在了github上。
ARM
环境搭建
我们利用qemu来启动arm程序。qemu是一种虚拟化模拟器,在user mode其能模拟不同架构的CPU,并运行为这些不同架构CPU编译的Linux文件,比如我们这里将要启动的ARM文件。
安装qemu
|
|
安装qemu后,我们就可以启动静态链接的ARM程序了,如果是动态链接的ARM程序,那么我们还需要指出其动态链接库储存的位置,如果题目没有给出文件的动态链接库,需要自己下载。
下载动态链接库
|
|
接下来我们就可以启动动态链接的arm程序了
启动程序
|
|
ok,现在程序已经可以启动起来了,最后还需要做的就是用gdb来调试程序,这里需要用到的是gdb-multiarch,该gdb可以attach到不同架构的文件上。
安装gdb-multiarch
|
|
之后我们在qemu-arm中开启一个端口,然后用gdb-multiarch去链接即可调试arm程序
利用gdb-multiarch调试arm程序
|
|
寄存器,指令知识
寄存器
ARM处理器一共有7种工作模式
- USR模式:正常用户模式,程序正常执行
- FIQ模式(Fast Interrupt Request):处理快速中断,支持高速数据传送或通道处理
- IRQ模式:处理普通中断
- SVC模式(Supervisor):操作系统保护模式,处理软件中断swi reset
- ABT中止(Abort mode):处理存储器故障,实现虚拟储存器和存储器保护
- UND未定义(Undefined):处理未定义的指令陷阱,支持硬件协处理器的软件仿真
- SYS系统模式:运行特权操作系统任务,基本等于USR
每种工作模式下用到的寄存器都会有所不同,其总共拥有31个通用寄存器
- R0~R15
- R13_irq,R14_irq
- R13_svc,R14_svc
- R13_abt,R14_abt
- R13_und,R14_und
- R8_fiq~R14_fiq
和6个状态寄存器
- CPSR
- SPSR_fiq
- SPSR_irq
- SPSR_svc
- SPSR_abt
- SPSR_und
每个状态下用到的通用寄存器和状态寄存器汇总如下:
system&user | FIRQ | Supervisor | Abort | IRQ | Undefined |
---|---|---|---|---|---|
R0 | R0 | R0 | R0 | R0 | R0 |
R1 | R1 | R1 | R1 | R1 | R1 |
R2 | R2 | R2 | R2 | R2 | R2 |
R3 | R3 | R3 | R3 | R3 | R3 |
R4 | R4 | R4 | R4 | R4 | R4 |
R5 | R5 | R5 | R5 | R5 | R5 |
R6 | R6 | R6 | R6 | R6 | R6 |
R7 | R7 | R7 | R7 | R7 | R7 |
R8 | R8_firq | R8 | R8 | R8 | R8 |
R9 | R9_firq | R9 | R9 | R9 | R9 |
R10 | R10_firq | R10 | R10 | R10 | R10 |
R11 | R11_firq | R11 | R11 | R11 | R11 |
R12 | R12_firq | R12 | R12 | R12 | R12 |
R13 | R13_firq | R13_svc | R13_abt | R13_irq | R13_und |
R14 | R14_firq | R14_svc | R14_abt | R14_irq | R14_und |
R15 | R15 | R15 | R15 | R15 | R15 |
CPSR | CPSR | CPSR | CPSR | CPSR | CPSR |
SPSR_firq | SPSR_svc | SPSR_abt | SPSR_irq | SPSR_und |
现在我们已经知道了各个工作模式下,都会用到哪些寄存器,接下来需要解决的问题是这些寄存器都有什么作用?和x86寄存器有哪些相似的地方?这里需要先介绍一个标准叫APCS,全称ARM Procedure Call standard(ARM过程调用标准),其定义了各个寄存器在程序中的作用并给各个寄存器起了别名。
- R0-R3用于向子程序传递参数,APCS的别名为a1-a4,其类似于x86的rdi,rsi,rdx,rcx。R0寄存器也用于函数返回值的传递,类似于rax。
- R4-R9用于储存局部变量,APCS的别名为v1-v6
- R10是栈限制,APCS的别名为sl
- R11是帧限制,APCS的别名为fp
- R12是内部过程调用寄存器,APCS的别名为ip
- R13是栈指针,APCS的别名为sp
- R14是连接寄存器,APCS的别名为lr
- R15是程序计数器,APCS的别名为pc
其中我们需要重点关注的是R0-R3,R13,R14,R15。R13和R15根据其别名能想到其功能,而R14的作用是保存返回地址,其对于理解ARM下的栈溢出有着关键作用。
指令
这里只简单介绍一下ARM的指令结构和常用指令,更详细的信息请看参考中ARM指令集详解。
ARM的指令结构为:<opcode>{<cond>}{S} <Rd>,<Rn>{,<operand2>}
<>内的信息是必须的,{}内的项是可选的,如<opcode>是指令助记符,是必须有的,而{<cond>}为指令执行条件,是可选的,一般不写默认是AL即无条件执行。
- opcode:指令助记符
- cond:执行条件
- S:是否影响CPSR寄存器的值,有S时影响,没有时不影响
- Rd:目标寄存器
- Rn:第一个操作数的寄存器
- operand2:第二个操作数
OK,知道了ARM指令的结构后,我们来介绍几个ARM特有并且常见的指令
-
LDR
LDR指令用于从内存中读取数据放入寄存器中。
如:LDR R0,[R1] 即将R1寄存器储存的地址处的数据读入R0中
LDR R0,[R1,#0x100] 即将R1+0x100地址处的数据读入R0中,不修改R1的值
LDR R0,[R1],#0x100 即将R1+0x100地址处的数据读入R0中,并且修改R1为R1+100(这里出现了第二个操作数,加了一个#号即为立即数,并且结果会写回R1中,注意和上一条指令的区别)
与LDR相对于的还有一个LDP指令,其是从内存中读取连续的数据赋值给寄存器
如:LDP R0,R1,[SP,#0x100] 即将SP+0x100地址处的值,连续读入到R0和R1中,连续读入的意思是假设R0读取的是0xfff0地址处的值,R1读取0xfff8地址处的值(假设是64位)。
-
STR
STR指令用于将数据储存在内存中。
如:STR R0,[R1] 即将数据R0储存在R1寄存器储存的地址处。
与STR相似的还有个指令是STP,其功能类似与LDP,其将多个寄存器的值,连续储存在内存中。
如:STP R0,R1,[SP,#0x100] 即将数据R0和R1连续储存在SP+0x100地址处,连续意思与上面类似。
-
BL
BL是带链接的跳转指令,其会将下一条指令拷贝到R14中,然后跳转到指定地址处。
与之类似的还有B和BX,B是直接跳转,BX是带状态切换的跳转指令,跳转到Rm 指定的地址执行程序,若Rm 的位[0]为1,则跳转时自动将CPSR 中的标志T 置位,即把目标地址的代码解释为Thumb代码;若Rm 的位[0]为0,则跳转时自动将CPSR 中的标志T 复位,即把目标地址的代码解释为ARM代码。
栈
ARM的栈结构和X86类似,R0-R3用于储存前四个参数,多余的参数会储存在栈中,在用户模式下会根据R14寄存器储存的值来返回,因此在函数开头和结尾常见的指令是:
STP X29, X30, [SP,#-0x110]!
LDP X29, X30, [SP],#0x110
例题
这里就以TSCTF的helloARM作为例题来讲解。
IDA打开后很容易发现,有一个oooooo函数存在栈溢出漏洞,这里我们从其汇编代码来观察函数,顺便学习巩固一下ARM的指令。
第一条语句:STP X29,X30,[SP,#-0x110]!
X29相当于rbp,X30相当于返回地址,这句话的意思就是将rbp和返回地址,保存在sp-0x110处,叹号的意思是先修改寄存器再访问内存,相当于先计算sp=sp-0x110,再访问[sp],所以执行这条语句后,SP寄存器也被修改了。不得不说ARM指令比起x86来说,简单了很多。
第二条语句:MOV X29,SP
将SP的值赋给X29,从后面观察发现,访问局部变量就都是通过X29来访问的了。
第三-七条语句
设置memset函数的三个参数,并用BL来跳转到memset函数处,根据前面的知识可得,BL会将下一条语句的地址赋值给R14,即这里的X30。
read函数处
根据X0的值我们知道其读取的地址是在rsp+0x10处,并且可以读入0x500的值,而分配给这个函数的零时变量只有0x100大小,所以存在溢出。(从第一句我们可以知道该函数的栈空间大小总共是0x110,但是最低的0x10直接数据留给了rbp和返回地址,所以变量就只有0x100)。
倒数第二句:LDP X29,X30,[SP],#0x110
将第一句保存的rbp和返回地址的值赋值给X29和X30,相当于恢复环境
最后一句:RET
根据X30的值,来修改rip,相当于B X30,所以如果能修改X30的值就可以控制程序流。
根据上面的分析,我们发现我们可以溢出0x400的数据,但是X29是保存在低地址处的,也就是说没有办法修改oooooo函数保存的X29的值,那么我们就去修改main函数在栈地址上保存的值,观察main函数会发现,其X29也是保存在栈的低地址处的,所以只需要溢出0x108字节就可以控制main函数的X29了。
OK,现在我们可以控制控制流了,只要再能控制R0-R2三个寄存器的话,就可以get shell了。
观察了下IDA,发现程序没有能直接控制R0-R2的gadget,不像x86有push和pop,不过我们可以像x86利用ret2libc_csu一样来利用arm的ret2libc_csu,这里就不详细介绍ret2libc_csu了,基本原理和x86一致,直接看EXP。
|
|
MIPS
简介
MIPS是Microcomputer without interlocked pipeline stages的缩写,含义是无互锁流水级微处理器,其另一个含义是每秒百万条指令——Millions of instructions per second。其采用精简指令系统计算机构(RISC结构)设计,相比于x86/64的CISC结构,更为简单,设计周期更短。最关键的一点,MIPS架构的授权费用比较低。
环境搭建
MIPS的环境搭建基本和ARM差不多,唯一的区别是在动态链接库搜索和qemu启动程序上面。
|
|
寄存器,指令和栈结构知识
寄存器
MIPS一共有32个寄存器,每个寄存器的功能和别名用下面的表格来展现。
编号 | 助记符号 | 含义 |
---|---|---|
0 | zero | 永远为0 |
1 | at | 用于汇编器的暂时变量 |
2-3 | v0,v1 | 子函数调用返回结果 |
4-7 | a0-a3 | 子函数调用的参数 |
8-15(24-25) | t0-t7(t8-t9) | 暂时变量,子函数使用时不需要保存与恢复 |
s0-s7 | 子函数寄存器变量。在返回之前子函数必须保存和恢复使用过的变量,从而调用函数知道这些寄存器的值没有变化 | |
26,27 | k0,k1 | 通常被中断或异常处理处理程序使用作为保存一些系统参数 |
28 | gp | 全局指针,一些运行系统维护这个指针来更方便的存取static和extern变量。 |
29 | sp | 堆栈指针 |
30 | s8/fp | 第9个寄存器变量/子函数可作为帧指针用 |
31 | ra | 子函数返回地址 |
根据表格已经可以了解到大多数寄存器的作用,这里只取其中几个比较特别的寄存器来讲解一下。
$0(zero):该寄存器的值永远为0,不管你向其存放什么值,都会返回0。
$31(ra):存放函数的返回地址,一般在程序结尾,都会有一句jr $ra来实现程序返回,这与X86和ARM的返回方式有区别。
$28(gp):该寄存器会存放一个全局指针,它会指向运行时决定的静态数据区域,这样当需要对这些数据进行存取时,只需要一条指令就可以。
有一点需要注意的是,MIPS相比x86和ARM,是没有状态寄存器的。
指令
MIPS的指令分成三种格式,R,I,J。
R格式
R格式(Register format)指令为纯寄存器指令,所有的操作数均保存在寄存器中(移位指令除外),其各个字段的含义如下:
opcode | rs | rt | rd | shamt | funct |
---|---|---|---|---|---|
31-26 | 25-21 | 20-16 | 15-11 | 10-6 | 5-0 |
操作符 | 源操作数寄存器1 | 源操作数寄存器2 | 目的操作数寄存器 | 位移量 | 操作符附加段 |
对于R格式的指令,其opcode都为零,由funct字段来区别不同的指令。
例如add和sub指令:
指令 | opcode | rs | rt | rd | shamt | funct |
---|---|---|---|---|---|---|
add | 00000 | rs | rt | rd | 00000 | 100000 |
sub | 000000 | rs | rt | rd | 00000 | 100010 |
算数指令:add,addu,sub,subu,slt,sltu。其中u的意思是进行unsigned运算,slt指令的功能是rd=(rs<rt)?1:0,用来判断rs和rt寄存器值的大小。
逻辑指令:and,xor,or,nor
位移指令:sll,srl,sra,sllv,srlv,srav。有v的是由rs决定位移位数,没有的是由shamt决定。而有a的是保留符号位。
跳转指令:jr。该指令一般用在函数返回。
I格式
I格式(Immediate format)指令为带立即数的指令,最多能使用两个寄存器,其指令结构为:
opcode | rs | rd | im |
---|---|---|---|
31-26 | 25-21 | 20-16 | 15-0 |
操作符 | 源操作数寄存器 | 目的操作数寄存器 | 立即数 |
不同的指令有opcode来区分。
算术指令:addi,addiu,slti,sltiu。与R格式的指令基本一致,不过多了个i,其代表立即数。
逻辑指令:andi,ori,xori。
载入指令:li,lui,lw,sw。l代表load,从内存中加载数据到寄存器。s代表store,将寄存器的数值储存到内存中。
跳转指令:beq,bne。eq代表当两个寄存器的值相等时,才跳转。ne代表两个寄存器的值不相等时才跳转。
J格式
J格式(Jump format)为长跳转指令,仅有一个操作数,其指令结构为:
opcode | address |
---|---|
31-26 | 25-0 |
操作符 | 地址 |
注意这里的地址是伪直接地址,其最后的跳转地址是由pc的高四位和该地址右移两位得到,即PC=PC[29:31]+address«2。
其指令有:j,jal。jal会将下一条指令的地址赋值给$31即$ra,通常用于子程序的调用中。
栈结构
MIPS的栈结构和x86是一样的,这里需要关注的是其如何保存返回地址和如何返回。
在函数开头通常会有sw $ra,0x1c($sp);这样的语句,它的作用就是保存返回地址到栈上。
函数结尾会有**lw $ra, 0x1c($sp);jr $ra;**这样的语句,它会将返回地址读入到ra中,并跳转。
例题
这里以TSCTF HelloMIPS题目作为例题的讲解。
题目的漏洞出现在oooooo函数处,有一个栈溢出漏洞,我们通过阅读汇编代码,来巩固上面学到的指令。
第一,二条语句:
li指令给$gp赋值,addu再使其指向所需要的全局变量处。
第三-六条语句:
给程序分配栈空间,栈空间大小为0x120,把返回地址,和调用函数的帧指针储存再栈地址高字节处,并将栈顶指针赋值给$fp。
memset处:
将栈空间0x18-0x118内的值赋为0,这里出现了$zero这个常为零的寄存器。
这里还有个细节需要注意,在每个跳转指令后面都跟着一个nop指令,这是因为MIPS在执行跳转指令时,会先执行下一条指令。
read处:
向栈空间0x18处读入0x500的地址,而栈空间总共才分配0x120大小的值,所以存在栈溢出。
剩余部分
恢复现场,将返回地址,保存的帧指针等都重新赋值给寄存器,而返回地址是保存在0x11c处的,而栈溢出可以修改此处的值,所以可以控制程序流。
题目还泄漏了libc的地址,因此我们可以到libc中找控制$a0-$a3的gadget,再调用system来获得shell。这些就不再讲解了,都是老ROP了,直接看EXP。
|
|