前言:大一快结束了,一直听说过 phppwn 但是没有学,最近其实挺累的,打了许多比赛结果被 AI 踹成路边一条,比赛题目也是有很多自己没有见过的,还是沉下心一点点学习吧。
一开始学 phppwn 有点迷,还好之前有一定的 web 基础,做过 web 的 php 代码审计的一类题目。phppwn 的 gdb 调试和普通 pwn 不太一样,需要利用 dockerfile 起一个 docker 环境,调试的时候也需要有对应的函数。
好吧还是想吐槽一下,最开始的时候利用 off-by-null 一直有点懵,再想为什么要重复申请这个堆块,后来看了很久才发现 off-by-null 修改了指针地址。之前打堆的时候应该是遇见过一次这样的,其他的时候大部分都是在修改 size 位进行堆重叠的利用。这种方式的 off-by-null 也是很久没有遇见过了,可以算是对于堆基础的一个复习吧。
PHPPWN GDB 流程
PHPPWN 一般来说都是给一个拓展 .so 文件,我们在写题过程中启用这个拓展就可以直接进行内部函数的调用。
当然也会给 dockerfile 方便我们起一个 docker 环境复现相同的 php 版本环境:

构建镜像
保存文件后,在终端里重新跑一遍 build 命令(换个名字以示区分):
1 | docker build -t pwnshell_debug . |
带特权启动容器
Docker 默认是禁止容器内的进程使用 ptrace 系统调用的,这意味着就算你装了 GDB,你也无法 attach(附加)到任何进程上,会直接报 Operation not permitted。
你必须在启动时加上 --cap-add=SYS_PTRACE 参数:
1 | docker ps -a # 查看当前容器 |

监听端口
1 | docker exec -it 53871d62457c /bin/bash |
1 | apt-get update |

gdb 连接远程
1 | gdb -q \ |

看一眼 vmmap,觉得没什么问题,应该可以进行调试了:

gdb 调试的注意事项
当我们 gdb 去调试的时候必须得有一段 PHP 代码去触发它。
.so 是一个动态链接库,它自己是不能单独运行的。它的作用是给 PHP 增加新的函数(比如出题人加了一个叫 pwn_hacker() 的函数)。你要想让 GDB 抓住这个漏洞,就必须得让 PHP 引擎去执行这个恶意函数。
例如我们题目当中假如有 zif_addHacker 这个函数,我们想要下断点就比如要先引用这个函数:
1 |
|
先写一个 exp.php,然后重复执行上述 gdb 调试操作,然后直接下断点
1 | pwndbg> set breakpoint pending on |


这样就可以直接跳转到函数当中。
在 exp 当中下断点:
1 |
|
然后下断点即可:
1 | pwndbg> set breakpoint pending on |
那么在这里还有一个问题:出题人经常会在 php.ini 当中限制 var_dump 函数,我们删掉即可
直接 Ctrl + f 搜索 var_dump,,然后 Ctrl + k 直接给这一行删了就行

再次进行上述操作调试:删除旧容器,起一个新容器
还有一种方法
emmm 就下断点在对应函数,执行 c,如果有重复的,就多次 c 即可。
IDA 分析函数:

在 phppwn 当中,我们主要看得是 zif_ 类函数:
zif_addHacker
1 | unsigned __int64 __fastcall zif_addHacker(__int64 a1, __int64 a2) |
zend_parse_parameters:传参函数,v15 和 v14 是变量,我们可以利用addHacker($arg1, $arg2);去触发他,相当于接收两个字符串参数。- 先看一下 _zend_string 的结构体,当我们传入一个字符串参数的时候,参数会对应这个样一个结构体
1 | // PHP 底层的 zend_string 结构体 |
那么此时回看这两行代码:
1 | v8 = (_QWORD *)_emalloc(*(_QWORD *)(*(_QWORD *)v14 + 16LL) + 16LL); |
v14 + 16LL 实际上指的是 len,也就是相当于常规堆题目的 malloc(strlen(string) + 16);
那么多余的 16 是干嘛的?其实这里是把 v8 当做了一个数组结构体:
1 | struct{ |
自己往下看一看伪 C 代码大概就理解了,使用的都是 memcpy 复制过来的。
v13 是 v14 的长度,这里*((_BYTE *)v8 + v13 + 16) = 0;在 v8 堆的最后多添加了一位 \x00
- 很明显的 off-by-null
总结一下:
- 分配一个 Hacker 块,大小是 strlen(name)+0x10
- 再单独分配一块 content
- 把 content 拷到 content 缓冲区
- 把 name 拷到 Hacker+0x10
zif_removeHacker
1 | unsigned __int64 __fastcall zif_removeHacker(__int64 a1, __int64 a2, __int64 a3, __int64 a4) |
这里把创建过的 chunk 都 free 掉
1 | _efree(**(_QWORD **)v6); |
虽然把 chunklist 标为空闲,但是指针并没有置 0,存在 UAF 漏洞
zif_editHacker
只放出来一些重要的逻辑吧:
1 | v5 = *(void ***)v4; // 拿到 Hacker 的主体结构块 (之前分析的头部16字节+arg2) |
memcpy(dest, (const void *)(*(_QWORD *)v10 + 24LL), n);
- 取 chunklist->ptr
- 如果 new_content 长度小于等于 hacker->content_len,直接 memcpy 到 hacker->content
- 否则先 free 原 content,再 malloc 新 content,再拷贝
zif_displayHacker
1 | unsigned __int64 __fastcall zif_displayHacker(__int64 a1, __int64 a2) |
取 hacker->content 和 hacker->name,先对 name 调 strlen,再构造返回字符串。
值得一提的是传统 Linux Pwn 中包含 printf、puts 或者 write 这样的标准输出函数。
但是在 PHP 的架构中,底层的 C 函数只负责处理数据,然后把结果返回给上层的 PHP 引擎。真正的打印动作,是由你在 PHP 脚本里写的 echo 或 var_dump 来完成的。
*(_DWORD *)(a2 + 8) = 262; 会告诉 PHP 这个值是什么类型
- 引擎拿到返回值后,怎么知道里面装的是整数、布尔值,还是字符串呢?
- 在 PHP 底层的
zval(变量容器)结构中,偏移+8的位置专门用来存放类型标签。 262(十六进制 0x106) 是 Zend 引擎内部的一个魔法数字,代表IS_STRING。
因此一般来说这种就是要返回的值,即我们可以泄露的数据。
EXP 思路:
1 | struct Hacker { |
全局还有一个 16 项数组 chunkList,每项 0x10 字节,形如:
1 | struct chunklist { |
removeHacker 之后,chunklist 里还留着悬空指针;而 editHacker/displayHacker 只看 slot 是否“占用”,如果这个 chunklist 后来又被别的分配复用了,就能把旧对象当成新对象来改,形成 UAF。
信息泄露(绕过 ASLR)
因为系统开启了 ASLR(地址空间布局随机化),每次运行程序时 libc 和 vuln.so 的加载基地址都会变,所以 exp 首先需要找到它们的真实地址。
1 | function maps() { |
- 在 Linux 中,
/proc/self/maps记录了当前进程所有加载的模块和内存布局。 leakaddr()函数利用正则表达式,从maps的输出中匹配并提取出了:libc.so.6的基地址 ($libc_base)和PHP 漏洞扩展vuln.so的基地址 ($vuln_base)
有了基地址后,再通过固定的相对偏移计算出绝对地址:
$strlen_got = $vuln_base + 0x4020;目标:vuln.so中的strlen函数的 GOT 表地址。$system = $libc_base + 0x53110;目标:libc中system函数的地址。
这段代码是典型的 CTF Pwn(二进制漏洞利用)题目中,用 PHP 编写的 Exploit(攻击脚本)的核心准备阶段。
它的核心目的是:绕过操作系统的 ASLR(地址空间布局随机化)保护,计算出内存中关键函数的绝对地址。
我们可以把这段代码拆成两部分来理解:
leakaddr($buffer) 基地址泄露
1 | function leakaddr($buffer) { |
在现代操作系统中,为了安全,程序每次运行其加载到内存中的地址都是随机的(ASLR)。
当我们要想调用特定的系统函数(比如 system()),就必须先找到它们在内存中的准确位置。
这里的 $buffer 传入的通常是 /proc/self/maps 文件的内容。这个文件记录了当前 PHP 进程加载了哪些
内存模块。
正则表达式的作用:
- 第一个正则在 $buffer 中寻找加载的 libc.so.6(标准C库,里面包含 system 等危险函数)。
- 第二个正则在 $buffer 中寻找加载的 vuln.so(题目自定义的有漏洞的 PHP 扩展)。
它通过正则抓取内存范围开头的十六进制字符串,从而拿到这两个文件的内存基地址(Base Address)。有了基地址,再加上固定的偏移量,就能算出来任何函数的真实内存地址。
p64($addr) 数据的打包
1 | function p64($addr) { |
把一个我们在 PHP 里计算出来的普通十进制/十六进制地址,转换成内存能够识别的 8 字节二进制流。
$hex = dechex($addr);将地址转换为十六进制字符串(例如:`140737351933952` -> `"7ffff7dd4000"`)。if (strlen($hex) % 2) { $hex = "0" . $hex; }如果十六进制字符串长度是奇数,在前面补个 `0`(因为 2 个十六进制字符代表 1 个字节)。hex2bin($hex)把十六进制字符串转换成原始的二进制数据。strrev(...)反转字节序。将大端序转为机器识别的小端序(例如:`\x7f\xff\xf7` 变成 `\xf7\xff\x7f`)。str_pad(..., 8, "\0")如果这个地址转换后不满 8 个字节(64位),在后面用空字节 `\x00` 补齐到 8 字节。
泄露关键地址
1 | $vuln_base = hexdec($mbase[1][0] ?? "0"); |
1 | readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep ' system@@' |

1 | readelf -r vuln.so | grep strlen |

构造堆风水
1 | addHacker('aaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbb'); # 0 |
name 长度 24 的对象:Hacker 块大小 = 0x10 + 0x18 = 0x28
name 长度 8 的对象:Hacker 块大小 = 0x10 + 0x08 = 0x18
因此可以得出:
- #0 #1 #2 都是 0x28 大小的 Hacker 块
- #3 是特殊触发对象,名字是 “/readflag;#”
- #4 ~ #14 都是 0x18 大小的 Hacker 块
复用 chunk12 和 13 进行 off-by-null
1 | removeHacker(13); |
在申请完addHacker('aaaaaaaaaaaaaaaaaaaaaaaa', 'hhhhhhh');的时候我们看一下内存:

这里我圈出来了两个指针,前者是 name 指针,后者是 content 指针
1 | pwndbg> tele 0x7ffff62041f8 |
当前可以看到 content 的 ptr 是 0x7ffff6204228
由于之前分析过的 off-by-null,他会在我们的堆内存的 content 的最后加上 \x00,也就是说他会越界写 \x00
当我们复用堆块 12 的时候,content 就会越界写到 chunk13 的 name 指针,让最低的一位指针为 \x00
0x00007ffff6204228 -> 0x00007ffff6204200可以看一下下面的图:


在此之后,如果我们可以修改 chunk13 的 name 部分,由于 chunk13 的 name 和 chunk14 的 content 指针在地址上已经篡改为相邻的,我们修改 chunk13 的 name 时就可以修改到 chunk14 的 content 指针。
篡改 strlen 为 system,调用 binsh
1 | editHacker(13, p64(0x18) . 'hhhhhhhh' . p64($strlen_got)); |
editHacker(13, p64(0x18) . 'hhhhhhhh' . p64($strlen_got));执行前的堆情况:


我们根据上面的思路用 chunk13 篡改 content 指针为 strlen
为了不影响其他内容,我们前面选择正常覆盖不改变p64(0x18) . 'hhhhhhhh'即可,执行之后:


可以发现此时 chunk14 的指针位已经被修改为 strlen 了,之后修改 chunk14 篡改为 system 即可。

总 EXP:
1 |
|
参考资料:https://ixout.github.io/posts/5022/index.html#WACON2023-heaphp
说些什么吧!