kir[A]'s 小黑屋

GDGJ2020 pwn writeup(大雾)

字数统计: 2.9k阅读时长: 13 min
2020/10/03 Share

GDGJ2020 CTF

前言

节前打了今年第一场线下赛,又被干了。作为一名web狗竟然只做出一题web,感到十分忏愧,于是利用国庆放假的时间,好好把pwn做了一下,发现全是奇奇怪怪的题,所以以下这篇不是writeup,这是一篇吐槽。

0x01 slide

1
2
3
4
5
6
[*] '/home/kira/pwn/guanjv2020/slide'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

IDA打开一看,很简单嘛,32位栈溢出。

1
2
3
4
5
6
7
8
9
10
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf; // [esp+0h] [ebp-108h]

setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
puts("wuhu~~~");
read(0, &buf, 0x110u);
return 0;
}

然后细心再看看,溢出长度只有8字节。emmmmmm,想必是栈迁移,熟悉的套路。

然而按下tab键,看看汇编码,发现事情并不是那么简单。

1
2
3
4
.text:08048525                 mov     ecx, [ebp+var_4]
.text:08048528 leave
.text:08048529 lea esp, [ecx-4]
.text:0804852C retn

高版本的gcc编译,并不是那么简单的栈溢出。正常情况需要知道stack地址,然后控制ebp-4为ropchain的地址+4,这样才能进行rop。之前出题的时候为了简单难度,一般都不让做题人直接在main函数里做栈溢出。这个出题人是故意搞我的吧。由于没遇过不泄露stack地址的情况,比赛当时想了10min就放弃了。比较misc只做出一题,web只做出一题,花时间做pwn翻车就完蛋了。

下面说一下思路:

  • 这题的输入长度给了0x110,长度不算短,前面说过一般这种情况是需要stack地址,现在没有是不是不能做呢?这里可以用到一个pwn常见的技巧,低位写,直接修改ebp-4的最低位,然后就有概率可以跳到我们输入的stack空间。
  • 然后,可以把ropchain写到stack上,然后看脸跳过去。为了提高非洲人成功的概率,可以用ret这个gadget做滑梯。

exp:

1
print('略')

这题真不好做,场上不知道哪位大佬做出来,佩服佩服。

realloc

1
2
3
4
5
6
[*] '/home/kira/pwn/guanjv2020/realloc'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

这题模拟栈操作,有push,pop的功能,还可以用top查看栈顶的数据

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
char **v4; // [rsp+0h] [rbp-60h]
__int64 value; // [rsp+20h] [rbp-40h]
__int64 *stack; // [rsp+28h] [rbp-38h]
char s1; // [rsp+30h] [rbp-30h]
char s; // [rsp+40h] [rbp-20h]
unsigned __int64 v9; // [rsp+58h] [rbp-8h]

v4 = a2;
v9 = __readfsqword(0x28u);
sub_400866();
puts("here are two stacks, you can input command push, pop, top and exit");
puts("example: \"push s1 1234\", \"pop s2\", \"top s1\", \"exit\"");
while ( 1 )
{
memset(&s, 0, 0x10uLL);
printf("> ", 0LL, v4);
if ( __isoc99_scanf("%8s", &s) == -1 || !strncmp(&s, "exit", 4uLL) )
break;
if ( !strncmp(&s, "push", 4uLL) || !strncmp(&s, "pop", 3uLL) || !strncmp(&s, "top", 3uLL) )
{
__isoc99_scanf("%4s", &s1);
if ( !strncmp(&s1, "s1", 2uLL) )
{
stack = &qword_6020C0;
LABEL_14:
if ( !strncmp(&s, "push", 4uLL) )
{
__isoc99_scanf("%lu", &value);
push(stack, value);
}
else if ( !strncmp(&s, "pop", 3uLL) )
{
pop(stack);
}
else
{
top(stack);
}
}
else
{
if ( !strncmp(&s1, "s2", 2uLL) )
{
stack = &qword_6020B0;
goto LABEL_14;
}
puts("unknown stack");
}
}
else
{
puts("unknown command~");
}
}
puts("bye~");
return 0LL;
}

题目初始化是创建了两个stack,申请0x50的heap空间。并且用了两个dword分别表示stack空间大小和栈顶位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void *sub_400866()
{
void *result; // rax

setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
qword_6020C0 = malloc(0x50uLL);
result = malloc(0x50uLL);
qword_6020B0 = result;
dword_6020C8 = 10;
dword_6020B8 = 10;
dword_6020CC = 0;
dword_6020BC = 0;
return result;
}

漏洞点在top

1
2
3
4
5
6
7
8
9
int __fastcall top(__int64 a1)
{
int result; // eax

result = *(a1 + 12);
if ( result >= 0 )
result = printf("top element: %ld\n", *(*a1 + 8LL * *(a1 + 12) - 8));
return result;
}

当栈顶为0时,仍然可以进行pop,导致栈顶可以变成-1。留意之前程序使用heap作为stack空间,此时进行push就可以修改申请到的chunk的size。

下面看看关键函数push。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct stack *__fastcall push(struct stack *stack, __int64 value)
{
struct stack *result; // rax

if ( stack->top == stack->stack_size )
{
stack->stack_size *= 2;
stack->memory = realloc(stack->memory, 8LL * stack->stack_size);
}
*&stack->memory[8 * stack->top] = value;
result = stack;
++stack->top;
return result;
}

当前栈顶若与stack空间大小相等,就调用realloc进行扩容。这里是程序里唯一可以进行free和malloc的地方(除了开头申请stack空间的地方)。程序逻辑不复杂,不过利用起来可没那么简单。申请chunk的大小固定,只能是0x50*N,而且只能申请的空间只能不停变大,不可逆。

简单说说我的思路:

  1. 漏洞可以干什么?最主要是可以修改chunk的size,那么可以进行overlapping。
  2. 修改size还有什么常见套路,unlink!PIE没开,stack指针存放bss段,地址可知,条件满足,干就完了。
  3. 当然,需要一定的堆风水技巧。
  4. unlink完后相信不用多说了,改stack指针到got表,泄露修改一把梭。

exp:

1
print('略略')

这题应该是这4题里面最难的,最少代码写起来是最长。这题比较有意思,可以玩玩,而且至少程序运行的逻辑是合理的,不是为出题而出题的奇葩题目。

heap3

1
2
3
4
5
6
[*] '/home/kira/pwn/guanjv2020/heap3'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

好了,终于到这题奇葩的题目了。这是80分的题目,有多奇葩请继续看。

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
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
_QWORD *v3; // rbx
_QWORD *v4; // rbx
_QWORD *v5; // rbx
__int64 v6; // rax
int v7; // [rsp+Ch] [rbp-34h]
_QWORD *v8; // [rsp+10h] [rbp-30h]
_QWORD *v9; // [rsp+18h] [rbp-28h]
_QWORD *v10; // [rsp+20h] [rbp-20h]
unsigned __int64 v11; // [rsp+28h] [rbp-18h]

v11 = __readfsqword(0x28u);
v7 = 0;
sub_127E();
sub_12E8();
while ( 1 )
{
while ( 1 )
{
menu();
std::istream::operator>>(&std::cin, &v7);
if ( v7 != 2 )
break;
v4 = (_QWORD *)operator new(8uLL);
delete(v4);
v9 = v4;
}
if ( v7 > 2 )
{
if ( v7 == 3 )
{
v5 = (_QWORD *)operator new(8uLL);
print(v5);
v10 = v5;
}
else
{
if ( v7 == 4 )
exit(0);
LABEL_13:
v6 = std::operator<<<std::char_traits<char>>(&std::cout, "Invalid choice");
std::ostream::operator<<(v6, &std::endl<char,std::char_traits<char>>);
}
}
else
{
if ( v7 != 1 )
goto LABEL_13;
v3 = (_QWORD *)operator new(8uLL);
add(v3);
v8 = v3;
}
}
}

先扫一样伪代码,emmmmm,经典菜单题嘛,有add,delete,show。有show,有delete估计难不到哪里去。

然后按照做题经验,一个一个功能点进去看,先看看add吧

1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned __int64 __fastcall add(_QWORD *a1)
{
_QWORD *v2; // [rsp+10h] [rbp-20h]
unsigned __int64 v3; // [rsp+18h] [rbp-18h]

v3 = __readfsqword(0x28u);
sub_16F2(a1);
*a1 = off_205CC0;
*a1 = off_205CC0;
v2 = a1;
sub_248A((__int64)&unk_2062A0, (__int64)&v2);
return __readfsqword(0x28u) ^ v3;
}

什么鬼,继续点开sub_16F2继续

1
2
3
4
5
6
7
8
unsigned __int64 __fastcall sub_16F2(_QWORD *a1)
{
unsigned __int64 v1; // ST18_8

v1 = __readfsqword(0x28u);
*a1 = &off_205CF8;
return __readfsqword(0x28u) ^ v1;
}

emmmm,看不懂干嘛的,暂时不管,继续看另一个函数sub_248A

1
2
3
4
5
6
7
8
9
10
11
12
unsigned __int64 __fastcall sub_248A(__int64 a1, __int64 a2)
{
unsigned __int64 v2; // ST18_8
__int64 v3; // rbx
__int64 v4; // rax

v2 = __readfsqword(0x28u);
v3 = equal(a2);
v4 = sub_2214(a1);
sub_2A5A(a1, v4, v3);
return __readfsqword(0x28u) ^ v2;
}

emmmmm,怎么还没看到熟悉的代码,什么input siez,input content之类的代码,继续点进去

1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned __int64 __fastcall sub_2A5A(__int64 a1, __int64 a2, __int64 a3)
{
unsigned __int64 v3; // ST28_8
__int64 v4; // rax
__int64 v5; // ST20_8

v3 = __readfsqword(0x28u);
v4 = sub_2A24(a3);
v5 = sub_2FCA(a1, v4);
std::__detail::_List_node_base::_M_hook(v5, a2);
sub_307E(a1, 1LL);
return __readfsqword(0x28u) ^ v3;
}

好吧,我直接运行看一下算了。

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
$ ./heap3
what's your name!
fuckyou
hello ,fuckyou
------------------
1. Add note
2. Delete note
3. Print note
4. Exit
------------------
Your choice :1
------------------
1. Add note
2. Delete note
3. Print note
4. Exit
------------------
Your choice :2
------------------
1. Add note
2. Delete note
3. Print note
4. Exit
------------------
Your choice :3
------------------
1. Add note
2. Delete note
3. Print note
4. Exit
------------------
Your choice :

如果说刚才我只是有点看不懂伪代码,现在是彻底崩溃了。怎么正常输入都没反应。。。。。。难道要输入\x01,\x02,\x03,写代码测试吧。然后就看到下面这个情况了,无尽的Invalid choice,根本停不下来。

一顿脸滚键盘的自暴自弃操作后发现了玄机

这是什么情况,久违的问候!Note size出现了。测试发现,直接在菜单输入1,2,3并不能正常进入对应的函数,只有输入4 exit后才开始依次调用之前输入的函数。黑人问号*3。第一次见这么扯淡的题目。然后不难发现真正功能函数的位置

漏洞不难发现,delete没有清空指针,该干嘛干嘛去吧。

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
unsigned __int64 true_delete()
{
__int64 v0; // rax
void *v1; // rax
void **v2; // rax
__int64 v3; // rax
int v5; // [rsp+14h] [rbp-Ch]
unsigned __int64 v6; // [rsp+18h] [rbp-8h]

v6 = __readfsqword(0x28u);
v5 = 0;
std::operator<<<std::char_traits<char>>(&std::cout, "Index :");
std::istream::operator>>(&std::cin, &v5);
if ( v5 < 0 || v5 >= count )
{
v0 = std::operator<<<std::char_traits<char>>(&std::cout, "Out of bound!");
std::ostream::operator<<(v0, &std::endl<char,std::char_traits<char>>);
exit(0);
}
if ( *(_QWORD *)sub_24F6(&list, v5) != 0LL )
{
v1 = *(void **)(*(_QWORD *)sub_24F6(&list, v5) + 8LL);
if ( v1 )
operator delete[](v1);
v2 = (void **)sub_24F6(&list, v5);
operator delete(*v2);
v3 = std::operator<<<std::char_traits<char>>(&std::cout, "Success");
std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
}
return __readfsqword(0x28u) ^ v6;
}

程序入口还有一个格式化字符串漏洞,简直白给

1
2
3
4
5
6
7
8
9
10
11
12
__int64 sub_12E8()
{
char buf; // [rsp+0h] [rbp-10h]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
puts("what's your name!");
read(0, &buf, 8uLL);
printf("hello ,", &buf);
printf(&buf);
return 0LL;
}

这题说实话没有什么难度,如果用C写,估计15min内怎么也做出来了。漏洞实在不难利用,想怎么玩都行。真正操作我用7步,不知道有没有更简单的。

exp:

1
print('略略'*10086)

这题估计跟某些C++的特性有关吧,反正就是挺恶心的一题,基本功能都有问题的,出题人到底有没有测试过的。

stack4

1
2
3
4
5
6
[*] '/home/kira/pwn/guanjv2020/stack4'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

这题又是C++,估计还是heap3的出题人。

1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
char v4; // [rsp+0h] [rbp-30h]
unsigned __int64 v5; // [rsp+18h] [rbp-18h]

v5 = __readfsqword(0x28u);
sub_400B96();
sub_400CC2((__int64)&v4);
read(0, &unk_6021E0, 0x200uLL);
move((__int64)&v4, &unk_6021E0);
sub_400CF0(&v4);
return 0LL;
}

代码不多,有一个很明显的栈溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__int64 __fastcall move(__int64 a1, const void *a2)
{
__int64 dest; // [rsp+10h] [rbp-30h]
__int64 v4; // [rsp+18h] [rbp-28h]
__int64 v5; // [rsp+20h] [rbp-20h]
__int64 v6; // [rsp+28h] [rbp-18h]
unsigned __int64 v7; // [rsp+38h] [rbp-8h]

v7 = __readfsqword(0x28u);
dest = 0LL;
v4 = 0LL;
v5 = 0LL;
v6 = 0LL;
memmove(&dest, a2, 0x200uLL);
(*(void (__fastcall **)(__int64))(*(_QWORD *)a1 + 24LL))(a1);
return 0LL;
}

结尾有一个奇怪的调用(*(void (__fastcall **)(__int64))(*(_QWORD *)a1 + 24LL))(a1);

作者原意应该是调用一下函数,输出END

1
2
3
4
5
6
7
8
__int64 end()
{
__int64 v0; // rax

v0 = std::operator<<<std::char_traits<char>>(&std::cout, "END!");
std::ostream::operator<<(v0, &std::endl<char,std::char_traits<char>>);
return 0LL;
}

但是由于使用了memmove,所以无论你输入什么,长度多少,都无可避免地让程序崩掉。所以,这也是一题基本功能都没做好的垃圾题目,为了溢出而溢出。

简单讲一下这题怎么玩,留意到memmove之后的调用

1
2
3
4
5
6
7
.text:0000000000400D9D                 mov     rax, [rbp+var_38]
.text:0000000000400DA1 mov rax, [rax]
.text:0000000000400DA4 add rax, 18h
.text:0000000000400DA8 mov rax, [rax]
.text:0000000000400DAB mov rdx, [rbp+var_38]
.text:0000000000400DAF mov rdi, rdx
.text:0000000000400DB2 call rax

使用gdb调试可以发现,未进行memmove前的栈空间如下:

1
2
3
4
5
6
7
8
00:0000│ rsp      0x7ffd712497f0 —▸ 0x6021e0 —▸ 0x400ce7 ◂— 0xc3c990fffffcb4e8
01:0008│ 0x7ffd712497f8 —▸ 0x7ffd71249840 —▸ 0x400ea8 —▸ 0x400cf0 ◂— 0x10ec8348e5894855
02:0010│ rax rdi 0x7ffd71249800 ◂— 0x0
... ↓
07:0038│ 0x7ffd71249828 ◂— 0x80c57ddd38b76200
08:0040│ rbp 0x7ffd71249830 —▸ 0x7ffd71249870 —▸ 0x400e00 ◂— 0x41ff894156415741
09:0048│ 0x7ffd71249838 —▸ 0x400c27 ◂— 0xe8c78948d0458d48
0a:0050│ 0x7ffd71249840 —▸ 0x400ea8 —▸ 0x400cf0 ◂— 0x10ec8348e5894855

可以看到只要覆盖rbp-0x10就可以控制call rax。至于call什么,我想到的是leave ret这个gadget。call完之后ret到rbp-0x8,可以继续进行rop,难度不大。

总结

C++练得太少了,类似的题目用C++的基本GG,这还是一场3.5小时20题CTF+80题理论的比赛,实在做不动了。还是日站有意思。

写得有点累了,凑数文,将就将就。

CATALOG
  1. 1. GDGJ2020 CTF
    1. 1.1. 前言
    2. 1.2. 0x01 slide
    3. 1.3. realloc
    4. 1.4. heap3
    5. 1.5. stack4
    6. 1.6. 总结