跳到主要内容

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 -sWsystemgets 符号)得到 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,劫持面更宽。


审计清单

  1. find / -perm -4000 中是否存在自研或非常见的 SUID 程序。
  2. checksec 结果:NX、PIE、canary、RELRO 是否齐全。
  3. 程序是否存在无长度校验的输入点,是否引用 systemstrcpy 等危险符号。
  4. 服务监听地址是否暴露在本地之外。

防御建议

  1. 使用现代默认编译选项:PIE + canary + FULL RELRO。
  2. SUID 最小化,自研程序能不用 SUID 就不用。
  3. 自研工具避免用 system 拼接外部输入。
  4. 对所有输入做长度校验,杜绝栈溢出源头。