栈溢出能直接改写返回地址,制漏这件事我从入行到现在都觉得挺魔幻的洞利。一个程序读入超过缓冲区长度的制漏数据,多出来的洞利字节顺着栈往下爬,恰好覆盖掉函数保存的制漏EIP,然后你控制它跳到shellcode——整个过程没有任何“漏洞”的洞利显式报错,程序安安静静地就跑偏了。制漏
最经典的洞利入门案例是
gets()。这个函数从来不管目标缓冲区多大,制漏你喂多少它写多少。洞利CTF里但凡看到它,制漏基本等于出题人在喊“这里可以溢出”。洞利编译时关掉栈保护(-fno-stack-protector)、制漏关掉PIE、洞利关掉NX,制漏就是最纯粹的ret2text或者ret2shellcode环境。我第一次打通这种题的时候,盯着cat flag的输出愣了好几秒,感觉像是找到了程序的暗门。但真实环境不会这么友好。ASLR一开,栈地址每次都在变;NX一开,栈上代码不可执行;Canary一加,返回地址前面多一个随机值,覆盖时先把它踩坏,程序在ret之前就
__stack_chk_fail了。绕过手段一层一层叠上来:泄露libc基址走ret2libc,用ROPgadget拼gadget链绕过NX,Canary可以用格式化字符串泄露或者暴力爆破(fork进程场景下)。每个保护机制都像是锁,而pwn的过程就是找钥匙或者撬锁。ROP是真正让我觉得“原来还能这样”的东西。你不能执行栈上的代码,但可以把libc里已有的指令片段串起来。一个
pop rdi; ret的gadget,加上/bin/sh字符串的地址,再加上system的地址,三段拼在一起,程序就替你调了system("/bin/sh")。没有写一行新代码,全是复用。我第一次手动构造ROP链的时候,gadget地址找错了一位,程序直接segfault,调了快一个小时才定位到。这种调试过程很折磨,但打通那一刻的反馈也最直接。堆利用是另一个世界。栈溢出至少还有个明确的覆盖方向,堆里面全是指针和chunk头,free之后的bin链表、tcache的
next指针、unsorted bin的fd/bk——稍微写错一个字节,后续malloc就崩。UAF(Use After Free)是最常见的入口:free掉一个chunk,但指针没置空,程序还能通过这个悬空指针读写。攻击者就可以改tcache的fd,让下一次malloc返回任意地址。glibc 2.31之后tcache加了key校验,2.34又把hook全删了,利用手法一直在变。搞堆题的时候我经常觉得自己在跟glibc的开发者下棋,他们堵一条路,我们就绕另一条。工具方面,pwntools基本是标配。
cyclic找偏移、ROP自动搜gadget、remote连远程,省了大量手工活。GDB配pwndbg或者gef,看栈、看heap、看寄存器,调试效率比裸GDB高太多。但工具再顺手,核心还是得理解程序在内存里到底怎么跑。偏移算错、字节序搞反、栈对齐没注意(system要求16字节对齐,不对齐就崩),这些坑一个都躲不掉。学pwn没有捷径,就是一道道题打过去。每道题卡住的地方不一样,可能是保护机制没见过,可能是利用链少了一环,可能是环境版本对不上。但打通之后回头看,那些卡住你的点往往就那么几个固定套路。二进制漏洞利用说到底,就是在程序员的疏忽和系统的防御之间找那条窄路。
顶: 29踩: 7






评论专区