SUID 二进制栈溢出与 ret2libc
自研 SUID 程序存在栈溢出时,ret2libc 是最直接的利用路径:构造 ROP 链以 root 身份调用 system("/bin/sh")。SUID 场景的特殊点在于,real uid 非 0 的进程直接起 shell 会被内核降权,因此链中必须先执行 setuid(0)。
静态分析流程见 SUID 自定义二进制静态分析,整体枚举思路见 Linux 提权方法与检查清单。
定位与回传
find / -perm -4000 -type f 2>/dev/null
自研二进制常见于 /usr/local/bin、/opt。二进制不便外带时用 base64 编码回传:
base64 -w0 /usr/local/bin/app > /tmp/app.b64
本地解码时保留原文件名,便于 checksec 等工具正确识别。
前置分析
checksec ./app
readelf -sW libc.so.6 | grep -E " system$| gets$"
| 防护 | 影响 |
|---|---|
| NX | 栈上 shellcode 不可执行,改走 ROP |
| PIE | 地址随机化,需要先泄露运行时地址 |
| canary | 需要绕过或避免覆盖返回地址前的 canary |
| RELRO | 影响 GOT 写,ret2libc 本身不受影响 |
libc 版本要与二进制一起拉回本地,保证静态偏移一致。
计算偏移
用 pattern/cyclic 计算返回地址偏移:先生成长模式串作为输入,崩溃后取返回地址处的值反查:
cyclic 200 → 输入 → 崩溃地址 → cyclic -l <value> → offset
泄露运行时地址
程序菜单提供 dlsym 打印这类复用功能时,直接泄露 system 的运行时地址,减去 libc 中的静态偏移(readelf -sW 查 system、gets 符号)得到 libc base:
libc_base = leaked_system - static_offset_system
构造 ROP 链(SUID 必须 setuid)
SUID 进程里 system("/bin/sh") 生成的 shell 会因 real uid 非 0 被内核降权,链中必须先调用 setuid(0):
ret # 栈对齐
pop rdi; ret
0 # setuid(0)
setuid
pop rdi; ret
binsh 地址
system
exit
gadget 用 ROPgadget 提取:
ROPgadget --binary ./app --only "pop|ret"
其他变体与操作要点
| 要点 | 说明 |
|---|---|
| 无 PIE 且栈可执行 | 可走 jmp rsp + shellcode |
存在 strcpy@plt | 先把 payload 写进全局变量,再跳转对应功能函数 |
| 交互式菜单程序 | 用 PTY 驱动(pty.spawn / pwntools process)比管道稳定 |
| 服务绑定 127.0.0.1 | 需在目标机本地运行利用脚本,或做端口转发 |
| 覆盖函数指针/标记变量 | 不覆盖返回地址,让程序调用 system("cat ..."),再配合 PATH 劫持伪造 cat |
PATH 劫持伪造 cat 时,伪造脚本内部要用绝对路径 /bin/cat 防止无限递归;musl 环境的 SUID 进程不清理 PATH,劫持面更宽。
审计清单
find / -perm -4000中是否存在自研或非常见的 SUID 程序。- checksec 结果:NX、PIE、canary、RELRO 是否齐全。
- 程序是否存在无长度校验的输入点,是否引用
system、strcpy等危险符号。 - 服务监听地址是否暴露在本地之外。
防御建议
- 使用现代默认编译选项:PIE + canary + FULL RELRO。
- SUID 最小化,自研程序能不用 SUID 就不用。
- 自研工具避免用
system拼接外部输入。 - 对所有输入做长度校验,杜绝栈溢出源头。