GDGJ2020 CTF
前言
节前打了今年第一场线下赛,又被干了。作为一名web狗竟然只做出一题web,感到十分忏愧,于是利用国庆放假的时间,好好把pwn做了一下,发现全是奇奇怪怪的题,所以以下这篇不是writeup,这是一篇吐槽。
0x01 slide
1 | [*] '/home/kira/pwn/guanjv2020/slide' |
IDA打开一看,很简单嘛,32位栈溢出。1
2
3
4
5
6
7
8
9
10int __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 | [*] '/home/kira/pwn/guanjv2020/realloc' |
这题模拟栈操作,有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
16void *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;
}
漏洞点在top1
2
3
4
5
6
7
8
9int __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
14struct 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
,而且只能申请的空间只能不停变大,不可逆。
简单说说我的思路:
- 漏洞可以干什么?最主要是可以修改chunk的size,那么可以进行overlapping。
- 修改size还有什么常见套路,unlink!PIE没开,stack指针存放bss段,地址可知,条件满足,干就完了。
- 当然,需要一定的堆风水技巧。
- unlink完后相信不用多说了,改stack指针到got表,泄露修改一把梭。
exp:1
print('略略')
这题应该是这4题里面最难的,最少代码写起来是最长。这题比较有意思,可以玩玩,而且至少程序运行的逻辑是合理的,不是为出题而出题的奇葩题目。
heap3
1 | [*] '/home/kira/pwn/guanjv2020/heap3' |
好了,终于到这题奇葩的题目了。这是80分的题目,有多奇葩请继续看。
1 | void __fastcall __noreturn main(__int64 a1, char **a2, char **a3) |
先扫一样伪代码,emmmmm,经典菜单题嘛,有add,delete,show。有show,有delete估计难不到哪里去。
然后按照做题经验,一个一个功能点进去看,先看看add吧1
2
3
4
5
6
7
8
9
10
11
12
13unsigned __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 | unsigned __int64 __fastcall sub_16F2(_QWORD *a1) |
emmmm,看不懂干嘛的,暂时不管,继续看另一个函数sub_248A1
2
3
4
5
6
7
8
9
10
11
12unsigned __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
13unsigned __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 | $ ./heap3 |
如果说刚才我只是有点看不懂伪代码,现在是彻底崩溃了。怎么正常输入都没反应。。。。。。难道要输入\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
31unsigned __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 | [*] '/home/kira/pwn/guanjv2020/stack4' |
这题又是C++
,估计还是heap3的出题人。
1 | __int64 __fastcall main(__int64 a1, char **a2, char **a3) |
代码不多,有一个很明显的栈溢出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);
作者原意应该是调用一下函数,输出END1
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
800: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题理论的比赛,实在做不动了。还是日站有意思。
写得有点累了,凑数文,将就将就。