# BlitzCTF 2025 Writeup
## Web
### Middle of nowhere
@daq

Khi ấn vào URL, dưới đây là giao diện trang web

Ấn follow my lead, sẽ dẫn đến trang login

Sau một lúc mò và một vài lần nhập thử credentials sai thì thấy trên BurpSuite có API `/_next/static/chunks/pages/login-7593437a57f70c0b.js`
Ở phần response thấy đoạn sau

Có thể thấy username là `adm1n` và pass là `BlitZ123Hackzzzzzzzz`
Thử đăng nhập với thông tin đó
Nó kẹt ở trạng thái `Decrypting...` mãi nhưng thử reload lại page sẽ xuất hiện flag

```
FLAG: Blitz{N3x7js_M1ddl3w4r3_Byyyyp4sssssss}
```
### Fleg Store
@Haizzzzzzzzz

Truy cập vào link trong ảnh thì ta được giao diện như sau

Bỏ qua phần login vì nó không có gì đặc biệt cả

Sau khi đăng nhập xong thì đạp vào mặt chúng ta là các chức năng cơ bản của trang web bao gồm:
- DashBoard(nơi để tạo coupon và hiện các coupon đã tạo)

- Shop(nơi trưng bày các vật phẩm)

- Cart(nơi hiện các vật phẩm đã add và tổng giá trị của chúng đồng thời cũng là nơi thanh toán)

- Redeem(chỗ để add coupon)

Sau khi dạo vòng quanh các chức năng cơ bản của trang web thì ta có thể cơ bản xác định rằng để giải được bài này ta tạo coupon và add coupon để mua được vật phẩm flag.txt
Nghe có vẻ dễ đúng không!!!
Nhưng khi ta Generate Coupon liên tục thì ta nhận thấy rằng dường như trang web chỉ cho chúng ta tạo 5 coupon

Vậy chúng ta lấy đâu ra thêm coupon???
Sau khi thực hiện vài thao tác nghiệp vụ chuyên nghiệp(cụ thể là F12)thì mình tìm được hàm này

Sau khi tra vội bằng Gemini thì mình biết được hàm này hoạt động cơ bản như sau
- Khi ta click vào Generate Coupon thì client sẽ gửi một POST request đến server sau sau đó server sẽ trả lại cho chúng ta một response
- Sau đó hàm tiếp tục check nếu trong response có thuộc tính error thì không hiện coupon và ngược lại
Sau khi hiểu được hàm này thì mình nhận ra được hai điểm bất thường
- Thứ nhất là tại sao trong hàm ghi check thuộc tính error mà mình tìm trong response lại ko có TẠI SAO??? TẠI SAO??? TẠI TẠI SAO???(vị tiền bối nào đọc được wu này thì nhớ giải đáp cho vãn bối)
- Thứ hai là như mình đã nói ở trên thì dường như web chỉ cho tạo ra 5 coupon nhưng trong hàm lại không có phần nào quy định như vậy cả
--> Ta có thể thử spam request tạo coupon liên tục thông qua Burp để khi mà response chưa kịp trả về thì ta đã gửi tiếp request
***(hành vi này chống chỉ định dành cho những ai dùng BurpSuite Community)***


Việc còn lại là mua cờ thôi!!!
```
Blitz{FLEG_L00T3R_SH0P}
```
### Fleg Store 2.0
@daq

Khi truy cập trang web thấy giao diện

Có thể thấy trang web sẽ lưu số lần mình click chuột, thử vào mục Buy Flag

Như vậy phải click chuột 9,999 lần mới có thể mua được flag
Thấy có mục create backup, thử ấn và nó tải về 1 file json

Sau khi mở file thấy đoạn sau, là số click của mình

Sửa thành 9,999 và chọn restore backup

Như vậy là số click của mình đã là 9999, bây giờ có thể vào mua flag

```
FLAG: Blitz{FlEg_l00t3R_sh0p_Butt_w1th_cl1qu35}
```
### Unstoppable force meets immovable object
@daq

Truy cập trang web thấy login page không có gì đặc biệt

Quay sang file zip đã cho để phân tích, mở file `main.py` thấy đoạn sau
```python
NOT_PASSWORD = "P@ssword@123"
def immovable_object(data, block_size=32):
if len(data) % block_size != 0:
data += b"\0" * (block_size - (len(data) % block_size))
h = 0
for i in range(0, len(data), block_size):
block = int.from_bytes(data[i : i + block_size], "big")
h ^= block
return h
```
Có thể thấy hàm `immovable_object` không phải là một hàm băm an toàn, đơn giản nó chỉ XOR các chunk 32 byte
Và những data nhỏ hơn 32 byte được pad với NULL đến đúng 1 block
Ví dụ có `B0 ⊕ B1 ⊕ B1 = B0`
Vì những block giống nhau sẽ tự XOR chính nó đi, nên khi XOR `B0` với `B1` và `B1` vẫn chỉ tương tự như XOR mỗi `B0`
Sau đó thấy đoạn code này
```python
@app.route("/", methods=["GET", "POST"])
def home():
if request.method == "POST":
password = request.form["password"]
unstopabble_force = immovable_object(password.encode("utf-8"))
if password != NOT_PASSWORD and unstopabble_force == immovable_object(
NOT_PASSWORD.encode()
):
return FLAG
return redirect(url_for("home"))
url_for("static", filename="style.css")
return render_template("index.html")
```
Như vậy để bypass logic cần phải nhập khác với `NOT_PASSWORD` và 2 kết quả XOR giống nhau hay còn gọi là một collision
Từ đó viết ra code python
```python
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
url = "https://ufmio-n1sj9nsb.blitzhack.xyz/"
NOT_PASSWORD = "P@ssword@123"
B0 = NOT_PASSWORD.encode() + b"\x00" * (32 - len(NOT_PASSWORD))
B1 = b"A" * 32
payload = (B0 + B1 + B1).decode("latin-1") # keep raw bytes intact
res = requests.post(url, data={"password": payload}, verify=False)
print(res.text)
```
Sau khi chạy code đã tìm ra được flag

```
FLAG: blitz{60nn4_b3_4_b16_c0ll1510n_wh3n_un570pp4bl3_f0rc3_m3375_1mm0v4bl3_0bj3c7}
```
### Unstoppable force meets immovable object 2
@daq

Tương tự như bài trước, khi truy cập URL vẫn chỉ là trang login

Ở trong file zip đã cho, ở file `main.py` thấy đoạn sau
```python
@app.route("/", methods=["GET", "POST"])
def home():
if request.method == "POST":
username = request.form["username"]
password = request.form["password"]
if username != password and complex_custom_hash(
password
) == complex_custom_hash(username):
return FLAG
return redirect(url_for("home"))
```
Có thể thấy endpoint sẽ trả về flag khi 2 string khác nhau đều hash cùng một giá trị dưới hàm `complex_custom_hash` sau đây
```python
def complex_custom_hash(data_string):
if not isinstance(data_string, str):
raise TypeError("Input must be a string.")
data_bytes = data_string.encode("utf-8")
P = 2**61 - 1
B = 101
hash_val = 0
for byte_val in data_bytes:
hash_val = (hash_val * B + byte_val) % P
length_mix = (len(data_bytes) * 123456789) % P
hash_val = (hash_val + length_mix) % P
chunk_size = 40
num_chunks = 64 // chunk_size
folded_hash = 0
temp_hash = hash_val
for _ in range(num_chunks):
chunk = temp_hash & ((1 << chunk_size) - 1)
folded_hash = (folded_hash + chunk) % (1 << chunk_size)
temp_hash >>= chunk_size
final_small_hash = folded_hash
scrambled_hash = 0
for _ in range(3):
scrambled_hash = (
final_small_hash ^ (final_small_hash >> 7) ^ (final_small_hash << 3)
) & ((1 << chunk_size) - 1)
final_small_hash = scrambled_hash
return f"{scrambled_hash:04x}"
```
Sau lần thực hiện đầu tiên chỉ còn 40 bit trạng thái nên có nhiều nhất $2^{40}$ output có thể
Sử dụng birthday attack (hint dựa vào phần mô tả) cần khoảng $2^{20}$ lượt để tìm một collision
Từ đó viết được code sau
```python
import random, string
import requests
#PoC
MASK = (1 << 40) - 1
P = 2**61 - 1
B = 101
SHIFT = lambda x: (x ^ (x>>7) ^ (x<<3)) & MASK
def h(s: str) -> int:
v = 0
for b in s.encode(): v = (v*B + b) % P
v = (v + len(s)*123456789) % P
v &= MASK
for _ in range(3): v = SHIFT(v)
return v
def collision(max_trials=2_000_000):
seen = {}
alpha = string.ascii_letters + string.digits
for _ in range(max_trials):
s = ''.join(random.choice(alpha) for _ in range(random.randint(4,12)))
k = h(s)
if k in seen and seen[k] != s:
return seen[k], s, k, seen
seen[k] = s
return None, None, None, seen
u, p, k, seen = collision()
if u and p:
print(f"Collision after {len(set(seen))} trials:")
print(f"Username = {u}")
print(f"Password = {p}")
print(f"Hash = {k:010x}")
else:
print("No collision found.")
#Exploit
url = "https://ufmiotwo-asdhwsad.blitzhack.xyz/"
data = {
"username": u,
"password": p
}
r = requests.post(url, data=data, allow_redirects=False)
if r.status_code == 200 and r.text:
print("Flag:", r.text.strip())
else:
print("Login failed or wrong endpoint")
```

Như vậy sau 538655 lần đã tìm được username, pass, hash cùng với đó là flag
```
FLAG: Blitz{b1r7hd4y_p4r4d0x_3475_5h177y_h45h35_l1k3_7h15}
```
## Hardware
### Redstone Logic 1
@daq

Vì không tìm hiểu nhiều về hardware nên bài này mình chỉ còn cách đoán mò, để ý chỉ có 2 submit attempts
Có thể thấy đáp án chỉ có thể là `11` hoặc `10` hoặc `01` hoặc `00`
Và cực kì may mắn mình đã đoán bừa trúng là `00` trong ngay attempt đầu
```
FLAG: Blitz{00}
```
### Redstone Logic 2
@daq

So với bài trước thì bài này không giới hạn lượt submit
Có thể thấy đáp án chỉ có thể là một trong những số có 5 chữ số tạo thành bởi số 1 và số 0, mình quyết định brute force từng số một và ra kết quả
```
FLAG: Blitz{10010}
```
### IOT Sucks
@ansobad

Tôi thấy đèn LED nhấp nháy (hoặc dùng code/script để phân tích mức sáng từng frame)
Mã Morse là điều tôi liên tưởng đến nó mỗi chu kỳ sáng → chấm hoặc gạch, phân theo thời gian
- HIGH trong 500ms → . (chấm)
- HIGH trong 700ms → - (gạch)
- LOW 400ms → khoảng cách trong 1 ký tự
- LOW 900ms → giữa các ký tự
- LOW 500ms → sau 1 chữ cái
Tôi đo độ ms bằng `VLC Media Player`

Tải file `frame` đã trích xuất
Tôi đã tách toàn bộ video `iot.mkv` thành các ảnh mỗi 10 frame (tức mỗi ảnh cách nhau ~167ms), tổng cộng 540 ảnh.

Mỗi file là một ảnh chụp 1 thời điểm trong video
Tôi không biết làm bài này theo cách khác nên mình sẽ lamf thủ công
- Di chuyển từng ảnh bằng mũi tên → kiểm tra đèn sáng hay tắt
- Ghi lại liên tiếp những ảnh nào có đèn sáng → đếm số ảnh → suy ra thời gian đèn sáng

`-- --- .-. ... . .. --- - .---- ..... -... .- -..
`
Ta sẽ được mã như này => `MORSE IOT 15 BAD`
Giờ sẽ là ghép chữ thôi
```
Flag: Blitz{m0rs3i0t_15_b4d}
```
## Forensics
### Essay
@daq

Có thể thấy đó là file `.docm`, ngay lập tức nghĩ đến macro cùng với đó là olevba để phân tích
Đầu tiên, dùng lệnh `olevba Essay.docm` để phân tích

Decode đoạn đó ra được phrase `Sup3rS3cretPassW0RD` (tưởng sẽ là password để extract nhưng cuối cùng lại không cần)
Tiếp tục dùng binwalk để extract

Có file `0.zip` ở đó rất đáng ngờ
Dùng lệnh `unzip 0.zip -d second_doc` để giải nén sang một folder mới
Vào folder word thấy có `vbaProject.bin` là file chứa những thứ liên quan đến macro

Strings file đó và thấy dòng mã base64 (thực ra không cần giải nén `0.zip` mà file này ở file extracted gốc cũng sẽ thấy đoạn base64)

Cho lên CyberChef để giải mã

```
FLAG: Blitz{0l3_D3Mp_M3l10s}
```
### The Warrior's Legacy #1
@hphuc204

Trước hết ta tải file về có định dạng là warriors_legacy.zip
Sau đó giải nén file : ```warriors_legacy.zip -d gear/```
Chúng ta sẽ kiểm tra file vừa giải nén có tên là gì:

Tiếp theo chuyển đến thư mục đó rồi xem bên trong nó có những gì.

Ta có thể thấy đây là file save của Minecraft, một world save folder chứa:
```
advancements/
data/
datapacks/
DIM-1/ ← Nether
DIM1/ ← The End
entities/
icon.png ← Biểu tượng world
level.dat ← File chính lưu metadata của thế giới
level.dat_old ← Bản backup
playerdata/
region/ ← Chunk world
stats/
```
Xem lại trong description ta có thể kiểm tra các file trong Minecraft world để tìm flag bị ẩn trong:
+ Tên nhân vật / advancement / statistic
+ Nội dung command block / lectern / sign / item lore / item name
+ File level.dat hoặc các file trong playerdata, entities, region
Ta sẽ dùng tool đọc .dat và .nbt (định dạng của Minecraft). Cách nhanh nhất là dùng nbttools trong python để giải mã định dạng NBT (Named Binary Tag)
Tiếp đến ta sẽ tạo một môi trường Python ảo và kích hoạt nó để cài nbtlib vào đó:

Sau đó chúng ta sẽ sử dụng lệnh để đọc nội dung của level.dat
```python -m nbtlib -r level.dat```

Nhìn kĩ ta sẽ thấy flag bị chia thành nhiều phần trong các item theo dạng base64.
Ta sẽ giải mã lần lượt các phần
```
echo QmxpdHp7TkJUXw== | base64 -d
echo Rm9yX0Jsb2Nrc30= | base64 -d
echo SXNfTm90X0p1c3Rf | base64 -d
```

Sau đó ta ghép chúng lại thành một flag đầy đủ là
```
Blitz{NBT_Is_Not_Just_For_Blocks}
```
## OSINT
### The Lost Dog
@daq

Dựa vào description, đoạn `On walks, he’d pull one unforgettable stunt involving a knothole, a fence, and a rival dog`

Đoạn `The dog belonged to someone else. While its owner was away for a day, a friend looked after it.`

Đoạn `That friend — minimalist by design — built a fast, statically typed language with systems-level access and C-like syntax` nói về Walter Bright, cha đẻ ngôn ngữ lập trình D, mà syntax tương tự với ngôn ngữ C và cũng là bạn của chủ của chú chó


Cả 2 dẫn chứng cuối đều support cho cái tên ở dẫn chững đầu tiên, đó là Rover
```
FLAG: Blitz{Rover}
```
Tất cả các thông tin trên được tìm thấy ở https://news.ycombinator.com/item?id=42834739
### Hacked by Kids Part 1
@ansobad

Bài này mới nhìn vào tôi gần như không biết tìm đó ở đâu nên ta sẽ phải đi từ từ
- Xác định các từ khóa quan trọng như "hacked an officer’s security", "leaking private messages and files", "young group", và "embarrass powerful people".
- Tìm hiểu về các nhóm hacker trẻ tuổi đã từng thực hiện các cuộc tấn công nhằm vào các quan chức hoặc tổ chức chính phủ.
Tôi tìm được 1 trang uy tín để tìm vụ án này
https://www.justice.gov/
Tại vì mấy vụ hack này thì Mĩ là ưu tiên hàng đầu để tìm
https://www.justice.gov/usao-edva/pr/two-men-arrested-allegedly-hacking-senior-us-government-officials
Đây là link của vụ án đó tôi đã tìm ra

```
Blitz{1:16-mj-406}
```
## Crypto
@ansobad
### Broken encoding or what?

String cần giải mã:
`voubz{nabay_ir_ut_jpf_mak_gdrwbj_euhs}`
Sau khi thử nhiều cách (Dvorak, Azerty, đảo ngược…), chuỗi đã được giải mã bằng layout workman ↦ qwerty
https://www.dcode.fr/keyboard-change-cipher

`Blitz{catch_me_if_you_can_qwerty_kids}`
### Custom RSA? - Revenge

**Phân tích mã nguồn (crypto1.py)**
`m = b"Blitz{REDACTED}"`
Đây là flag plaintext, được lưu ở dạng bytes.
Sẽ được mã hóa RSA.
Đưa về dạng RSA thì tôi sẽ thay mã RSA trong challenge được sửa đổi một chút:
$ϕ(n)=(p−1)(q−1)$
Chall này: `mod_phi=(p−1)(q−1)(e−1)`
Tức là hàm được nhân thêm `(e-1) `
Đây là Custom RSA
```python
mod_phi = 381679521901481226602014060495892168161810654344421566396411258375972593287031851626446898065545609421743932153327689119440405912
n = 1236102848705753437579242450812782858653671889829265508760569425093229541662967763302228061
c = 337624956533508120294617117960499986227311117648449203609049153277315646351029821010820258
```
Tính khóa được giấu
`d = pow(e, -1, mod_phi)`
Ở đây vì có (e-1) nên tôi phải tìm đúng e mới tính được d
Tìm e
```py
r = mod_phi // n
while True:
if mod_phi % r == 0:
e = r + 1
break
r += 1
```
Ta ra được e
`e = 308776508606152118670230312260475727067`
`r = mod_phi // n` Vì (p-1)(q-1) ≈ n, nên phép chia này cho ta gần đúng (e-1)
Tìm d
```python
from Crypto.Util.number import long_to_bytes
d = pow(e, -1, mod_phi)
```
Tìm m để ra flag
```python
m = pow(c, d, n)
flag = long_to_bytes(m)
print(flag.decode())
```
Code
```python
from Crypto.Util.number import long_to_bytes
# Các giá trị đã cho
mod_phi = 381679521901481226602014060495892168161810654344421566396411258375972593287031851626446898065545609421743932153327689119440405912
n = 1236102848705753437579242450812782858653671889829265508760569425093229541662967763302228061
c = 337624956533508120294617117960499986227311117648449203609049153277315646351029821010820258
# Bước 1: Tìm e
r = mod_phi // n # Ước lượng e-1
print(f"Ước lượng ban đầu e-1 ≈ {r}")
for delta in range(0, 10000):
candidate = r + delta
if mod_phi % candidate == 0:
e = candidate + 1
print(f" Tìm được e = {e}")
break
else:
print("Không tìm thấy e!")
exit()
# Bước 2: Tính d (khóa bí mật)
d = pow(e, -1, mod_phi)
print(f" Khóa bí mật d = {d}")
# Bước 3: Giải mã
m = pow(c, d, n)
flag = long_to_bytes(m).decode()
print(f" Flag giải mã được: {flag}")
```
```
Flag: Blitz{Cust0m_RSA_OMGGG}
```
### Randomized Chaos

Phân tích Mã hóa trong `enc_3.py`
```python
def encrypt(flag):
res = []
for i, f in enumerate(flag):
k = random.getrandbits(32)
k = k & 0xFF # Lấy 8 bit cuối
r = rol(((f ^ k) & (~k | f)), k % 8)
res.append(r)
return bytes(res)
```
Dễ thấy:
- Mỗi byte f trong flag được mã hóa với một giá trị ngẫu nhiên k ∈ [0..255].
+ Công thức mã hóa (chatGPT): r = rol( ((f ⊕ k) ∧ (¬k ∨ f)), k % 8 )
Với mỗi byte f, khi dùng k ngẫu nhiên thì byte mã hóa r sẽ phân bố theo một xác suất nhất định.
Đây là dạng bài mà tôi thấy khá lạ ý tưởng bài này sẽ là: Khai thác phân bố xác suất:
- Vì mỗi dòng trong output_4.txt là một bản mã khác nhau của flag với key khác nhau (random), nên:
- Với một vị trí i, ta có 6.240 mẫu giá trị mã hóa khác nhau (vì flag mã hóa 6.240 lần).
- Từ đó, ta tạo bảng tần suất các giá trị mã hóa r xuất hiện tại mỗi vị trí.
→ Với mỗi vị trí i, nếu thử toàn bộ f ∈ [0..255], ta tính phân bố lý thuyết của r khi mã hóa f.
→ So sánh phân bố thực tế (từ file) và phân bố lý thuyết (từ brute-force), ta tìm ra giá trị f phù hợp
Trong enc_3.py, hàm encrypt(flag) sẽ mã hóa từng byte của flag như sau:
```python
for i, f in enumerate(flag):
k = random.getrandbits(32) # tạo số 32-bit ngẫu nhiên
k = k & 0xFF # chỉ lấy 8 bit cuối → k ∈ [0, 255]
r = rol(((f ^ k) & (~k | f)), k % 8) # mã hóa từng byte
```
File output_4.txt chứa 6.240 dòng, mỗi dòng là flag đã được mã hóa với random key khác nhau.
- Với mỗi byte trong flag (giả sử flag dài 37 byte), ta sẽ có: 6.240 giá trị r ứng với 6.240 lần mã hóa.
- Thu được bảng tần suất Counter[r] tại từng vị trí.
Đến đây tôi cần gợi ý đến từ chat
So sánh với phân phối thật C[r] trong dữ liệu
Likelihood + brute-force: Duyệt toàn bộ f, chọn byte có phân bố phù hợp nhất
```python
from collections import Counter
import math
def rol(x, n):
return ((x << n) | (x >> (8 - n))) & 0xFF
def encrypt_byte(f, k):
return rol(((f ^ k) & ((~k & 0xFF) | f)), k % 8)
# Tính phân bố lý thuyết cho mỗi f
def build_theoretical_dist(f):
dist = Counter()
for k in range(256):
r = encrypt_byte(f, k)
dist[r] += 1
total = sum(dist.values())
for r in dist:
dist[r] /= total
return dist
# Tính log-likelihood giữa phân bố thực và lý thuyết
def score(candidate_dist, real_dist):
s = 0
for r in real_dist:
p = candidate_dist.get(r, 1e-6)
s -= real_dist[r] * math.log(p)
return s
```
Kết hợp với` output_4.txt`
Code:
```python
flag_len = len(lines[0])
recovered_flag = []
for j in range(flag_len):
real_dist = Counter(line[j] for line in lines)
best_score = float('inf')
best_f = None
for f in range(256):
candidate_dist = build_theoretical_dist(f)
sc = score(candidate_dist, real_dist)
if sc < best_score:
best_score = sc
best_f = f
recovered_flag.append(best_f)
flag = bytes(recovered_flag)
print("Recovered flag:", flag.decode())
```
```
FLAG: Blitz{RaND0m_KEY_GENeRateD_By_Zwique}
```
### Fiboz-cryption

Từ file `Fiboz.py`, tôi biết:
- Tạo dãy Fibonacci-like: bắt đầu từ hai seed a, b.
```python
seq = [a, b]
for _ in range(length - 2):
seq.append(seq[-1] + seq[-2])
```
- Tạo key: Mỗi số trong dãy sẽ được biến đổi bằng số bước Collatz(tham khảo chat), sau đó lấy `mod 256`.
- Mã hóa: Dữ liệu được XOR từng byte với key
```python
# Tính số bước của chuỗi Collatz từ n đến 1
def collatz_len(n):
steps = 0
while n != 1:
n = n // 2 if n % 2 == 0 else 3 * n + 1
steps += 1
return steps
# Tạo dãy Fibonacci-like dài l phần tử, bắt đầu từ a, b
def fib_like(l, a, b):
seq = [a, b]
for _ in range(l - 2):
seq.append(seq[-1] + seq[-2])
return seq
# Tạo key bằng cách tính collatz_len cho mỗi phần tử, sau đó lấy mod 256
def make_key(l, a, b):
return [collatz_len(n) % 256 for n in fib_like(l, a, b)]
```
Ta thử đọc file mã hóa xem thế nào nhé:
```python
from pathlib import Path
cipher = Path("output.enc").read_bytes()
```
Tôi nhận ra được:
- Flag bắt đầu bằng Blitz{ → dùng để xác định đúng key.
- Key dài bằng độ dài ciphertext (64 bytes).
- Ta sẽ thử nhiều giá trị a, b và kiểm tra.-> Brute tìm a, b, l
Code:
```python
target_prefix = b"Blitz{"
valid_ascii = set(range(32, 127)) # Kiểm tra ký tự ASCII in được
# Brute-force seed a, b
for a in range(100_000, 200_000):
if collatz_len(a) % 256 != 180:
continue # tương ứng với key[0]
for b in range(150_000, 250_000):
if collatz_len(b) % 256 != 129:
continue # tương ứng với key[1]
if collatz_len(a + b) % 256 != 246:
continue # tương ứng với key[2]
key = make_key(len(cipher), a, b)
plaintext = bytes(cipher[i] ^ key[i % len(key)] for i in range(len(cipher)))
# Kiểm tra flag hợp lệ
if plaintext.startswith(target_prefix) and plaintext.endswith(b"}") and all(c in valid_ascii for c in plaintext):
print(f"a,b: a={a}, b={b}")
print("Flag:", plaintext.decode())
exit()
print("Không tìm thấy flag.")
```
Kết quả:
```
a,b: a=121393, b=196418
Flag: Blitz{So_You_Have_Studied_Fibonacci_And_Collatz_Conjecture_Now?}
```
### Lightning Strike

Khi mở file, tôi thấy các chữ cái như B, l, i, t, z được in theo kiểu dọc xiên ziczac như hình sét đánh. Một số chữ viết hoa, một số viết thường.
Dữ liệu không hề chứa số hay ký hiệu — chỉ có chữ cái Blitz.
→ Điều bất thường duy nhất là chữ hoa vs chữ thường.
Thế thì chỉ còn 1 kỹ thuật giấu tin phổ biến chính là: `Case-based Steganography`
Ta sẽ thử quy ước:
- CHữ hoa : bit 1
- Chữ thường: bit 0
Tiên hành mã hóa nó:
```python
# Đọc file và lấy các ký tự chữ cái
with open('ZZZ.txt', 'r', encoding='utf-8') as f:
letters = [c for c in f.read() if c.isalpha()]
# Chuyển đổi HOA/thường thành chuỗi bit
bits = ''.join('1' if c.isupper() else '0' for c in letters)
# Chia nhóm 7-bit và chuyển sang ký tự ASCII
flag = ''.join(chr(int(bits[i:i+7], 2)) for i in range(0, len(bits), 7))
print(flag)
```
Toàn bộ file chứa 203 chữ cái → chia được thành 29 nhóm 7-bit thế nên mỗi nhóm tương ứng với 1 ký tự mã ASCII 7-bit
```
Flag: Blitz{17_h4pp3n5_50_f4s7_ZZZ}
```
## Misc
### Stratogreet
@hphuc204

Dạng bài này sẽ là: Trích xuất hình ảnh ẩn bên trong tín hiệu âm thanh từ file` stratogreet.wav` sử dụng kỹ thuật SSTV
Công cụ cần thiết để giải mã: Windows-RX-SSTV
Ta bắt đầu:
- Mở file stratogreet.wav bằng trình phát nhạc (VLC, Windows Media Player,…).
- Chờ → ảnh sẽ xuất hiện trong cửa sổ RX-SSTV tự động.
- Lưu ảnh bằng nút “Save Image” hoặc chụp lại màn hình.

Ảnh toàn bộ:

Trong hình ảnh challenge tôi liên tưởng đến: `"Apollo Soyuz"`
Tôi tra gg xem có thông tin nào về hình ảnh này và liên quan tới ngày:

Có lẽ đây chính là flag của bài này:
```
Blitz{1975_07_15}
```
### Diff n' Rae
@daq

Extract file zip thì được 2 file .jpg đều không mở được

Trong mô tả có từ khóa `compare`, ngay lập tức nghĩ đến lệnh `diff`
Đầu tiên strings cả 2 file và viết ra 2 file txt

`-n 1`: in ra cả những string chỉ dài 1 ký tự vì mặc định strings chỉ in những chuỗi có 4 kí tự trở lên, có thể sẽ bị sót thông tin quan trọng
Với việc in cả những string chỉ dài 1 ký tự, có thể lấy được từng byte là những kí tự ASCII ẩn
Tiếp theo dùng lệnh `diff` phân biệt 2 file txt

Ghép các dòng đó lại được `QmxpdHp7ZDFmRl8xU191NTNmdUx9`
Đó là đoạn mã base64, cho lên CyberChef để giải

```
FLAG: Blitz{d1fF_1S_u53fuL}
```
### Lazy Flag
@hphuc204

Trước hết ta sẽ thử nhấp vào link xem có gì không

Có thể thấy nó không có gì cả nhưng trong description có phần ```Guess the "Secret" I hide here and put the flag as Blitz{SECRET}```. Nó gợi ý có thể là ẩn trong file đính kèm hoặc chính nội dung đề
Ta sẽ thử tải nó dưới dạng Microsoft Worf(.docx)
Sau khi tải về nó sẽ có tên là
```
Lazy Flag.docx
```

Ta sẽ đồi file ```.docx``` thành ```.zip``` rồi mở ra xem


Ta có đoán ```word/document.xml``` là nơi gần như chắc chắn chứa flag nếu nó xuất hiện dưới dạng text.
Sử dụng ``` less word/document.xml``` để xem nội dung bên trong.

Thấy được kí tự đầu tiên là ```<w:t>L</w:t>```, đoạn sau là ```<w:t>l</w:t> ,<w:t>a</w:t>```.
Sau đó ta có thể thấy được các ký tự đáng chú ý (mỗi <w:t> chứa đúng 1 ký tự) lần lượt là ```L a g u h```.
Ghép lại ta được flag
```
FLAG: Blitz{LAUGH}
```
### Hidden Signal in Noise
@hphuc204

Dựa vào description ta có thể thấy 4 byte đầu là header (magic), mỗi ký tự của flag được tách thành 2 nửa byte (nibble) và mỗi nibble được lưu vào nửa cao (high nibble) của một byte, các byte này lặp lại sau mỗi X byte.
Trước hết hãy xem trong file này có gì ```xxd -g1 -l 64 magic.mrf```

Sau khi xem qua có thể có y tưởng mỗi byte cờ có thể thấy ```4 byte đầu: 5A A5 5A A5 → chính là header```và ```từ byte thứ 4 trở đi là phần dữ liệu ẩn, rất có thể chứa flag.```
Ta sẽ thử Brute-force bước nhảy X với các bước:
+ Mỗi ký tự cần 2 nibble ⇒ cần 2 bytes mang thông tin.
+ Các byte chứa nibble cách nhau mỗi X byte.
+ Tiến hành brute-force X trong khoảng 2 đến 64.
Code:
```python
def brute_force_flag(filename):
with open(filename, 'rb') as f:
data = f.read()
payload = data[4:] # Bỏ 4 byte header đầu tiên
print(f"[+] Tổng số byte sau header: {len(payload)}")
for X in range(2, 64): # Thử các bước nhảy từ 2 đến 63
nibbles = [b >> 4 for b in payload[::X]] # lấy high nibble
chars = []
for i in range(0, len(nibbles) - 1, 2):
byte = (nibbles[i] << 4) | nibbles[i + 1]
chars.append(byte)
try:
text = bytes(chars).decode('ascii', errors='ignore')
except Exception as e:
print(f"[!] Decode lỗi tại X={X}: {e}")
continue
if "Blitz{" in text:
print(f"[✅] Tìm thấy stride X = {X}")
print(f"[🧩] Flag đầy đủ: {text}")
final = text.split("}")[0] + "}"
print(f"[🏁] Final flag: {final}")
with open("flag.txt", "w") as out:
out.write(final)
print("[💾] Flag đã được lưu vào flag.txt")
return
else:
print(f"[ ] X = {X:02d} → Không có 'Blitz{{', thử: {text[:30]!r}")
print("[❌] Không tìm thấy flag trong các bước nhảy từ 2 đến 63.")
if __name__ == "__main__":
brute_force_flag("magic.mrf")
```

Ta đã tìm thấy flag:
```
FLAG: Blitz{H1dd3n_4n4lyt1c_Ch4ll3ng3}
```
### BlitzBot
@hphuc204

Dựa vào description ta có thể thấy có 3 loại robot: Blue, Yellow, và Red. Mỗi ngày, mỗi robot sẽ sinh ra các robot khác theo quy tắc sau:
+ Blue tạo: 2 Yellow + 3 Red
+ Yellow tạo: 2 Red + 3 Blue
+ Red tạo: 2 Blue + 3 Yellow
```
Đây là hệ phương trình tuyến tính => dùng ma trận và mũ ma trận để giải nhanh cho số ngày lớn.
```
Trước hết ta cần tạo môi trường thì cần có Python và pwntools để giao tiếp với server.

Sau đó sẽ chạy lại script: ```python blitzbot_solver.py```

Theo để bài vì ```Time Limit: 3 Seconds for all test cases``` nên rất lâu để tính toán và chắc chắn test case cuối cùng sẽ luôn bị chậm nên ta sẽ dùng script ```pwntools``` để tự động nhận + gửi kết quả tức thì
```python
from pwn import *
MOD = 10**9 + 7
def mat_mult(a, b):
res = [[0]*3 for _ in range(3)]
for i in range(3):
for j in range(3):
for k in range(3):
res[i][j] = (res[i][j] + a[i][k]*b[k][j]) % MOD
return res
def mat_pow(mat, exp):
res = [[int(i == j) for j in range(3)] for i in range(3)]
while exp:
if exp % 2 == 1:
res = mat_mult(res, mat)
mat = mat_mult(mat, mat)
exp //= 2
return res
def compute(B0, Y0, R0, N):
T = [
[1, 3, 2],
[2, 1, 3],
[3, 2, 1],
]
T_N = mat_pow(T, N)
init = [B0, Y0, R0]
result = [0, 0, 0]
for i in range(3):
for j in range(3):
result[i] = (result[i] + T_N[i][j]*init[j]) % MOD
return result
def main():
r = remote("pwn.blitzhack.xyz", 1234)
while True:
try:
line = r.readline().decode().strip()
if not line:
continue
print(f"[INPUT] {line}")
B0, Y0, R0, N = map(int, line.split())
BN, YN, RN = compute(B0, Y0, R0, N)
answer = f"{BN} {YN} {RN}"
print(f"[ANSWER] {answer}")
r.sendline(answer.encode())
except:
break
r.interactive()
if __name__ == "__main__":
main()
```

Sau khi chạy xong ta có được flag
```
FLAG: Blitz{SUD0_M4K3_B07***}
```
### Chamber of Secrets
@ansobad

→ Đây là ảnh JPEG bình thường
Mở ảnh bằng trình xem ảnh thấy đó là một trang sách – chương 1 của Harry Potter – “The Worst Birthday”.
JPEG luôn kết thúc bằng FFD9. Nếu có dữ liệu sau đó ⇒ thường là dữ liệu ẩn.
`xxd -p chamber.jpg | tail`
Kết quả có dòng bắt đầu bằng `ffd9 504b0304...` ⇒ thấy chuỗi `504b0304` nghĩa là `"PK\x03\x04"` → mở đầu của file ZIP.
Bài này tôi tra chatGPT xem những sự bất thường thì thấy có gắn thêm 1 file ZIP ở cuối.
Cắt phần zip ẩn:
`grep -aboUa -m1 -e $'\xff\xd9' chamber.jpg`
`dd if=chamber.jpg bs=1 skip=57867 of=hidden.zip`
Kiểm tra: `unzip -l hidden.zip`

Có 1 file flag ở đây nhưng đang bị mã hóa bằng pass
Tìm mật khẩu: `unzip -P exchanged hidden.zip`
Ra được đoạn: `https://lastchamberofsecrect.com/url-decode/base?galf=QmxpdHp7aDFkZDNuXzFuXzdoM19kMzNwX3hEfQ%3D%3D`
Thử decode bằng `base64`
```
Flag: Blitz{h1dd3n_1n_7h3_d33p_xD}
```
### Language of Madness
@daq

Mở file lên thấy có nhiều kí tự


Thấy mọi thứ đều in được dưới dạng ASCII (33–126)
Có đúng 94 ký tự khác nhau xuất hiện và chúng lặp lại trong một chu kỳ cố định bắt đầu với `'&BA@?>~...`
Hoán vị 94 ký tự đó là bảng mã `xlat1` của Malbolge.
Dùng lệnh `git clone https://github.com/bipinu/malbolge.git` để lấy code
Sau đó chạy code với text.txt sẽ ra flag
```
FLAG:Blitz{H4rd3st_Pr0gr4m1ng_L4ngu3s?}
```
### Duck’s Revenge
@daq

Đầu tiên kiểm tra xem là loại file gì và một số thông tin


Về byte pattern, có thể thấy cứ mỗi 2 byte sẽ lặp lại là `00` hay `08` hay `02`, có thể là USB HID keystroke report (1 byte + 1 bộ sửa đổi byte)
Thường thì một USB Rubber Ducky sẽ lưu trữ payload keystroke ở 1 file nhị phân tên là `inject.bin`
Mỗi HID 8 byte report sẽ trông như sau

Tuy nhiên, DuckEncoder tối ưu hóa không gian, trong các script đơn giản, mỗi keypress được lưu dưới dạng `(usage ID, modifier)`, y hệt như những gì ta thấy ở đây
Ví dụ như sau

Sử dụng code python sau
```python
MOD_SHIFT = 0x02
hid = { # letters
**{0x04+i: chr(ord('a')+i) for i in range(26)},
# numbers
0x1e:'1',0x1f:'2',0x20:'3',0x21:'4',0x22:'5',
0x23:'6',0x24:'7',0x25:'8',0x26:'9',0x27:'0',
0x28:'\n', 0x2d:'-', 0x2e:'=', 0x2f:'[', 0x30:']', 0x31:'\\',
0x33:';', 0x34:"'", 0x36:',', 0x37:'.', 0x38:'/',
}
shift_map = {'1':'!', '2':'@', '3':'#', '4':'$', '5':'%', '6':'^',
'7':'&', '8':'*', '9':'(', '0':')',
'-':'_', '=':'+', '[':'{', ']':'}', '\\':'|',
';':':', "'":'"', ',':'<', '.':'>', '/':'?'}
with open('naknak','rb') as f:
blob = f.read()
out = []
for usage, mod in zip(blob[0::2], blob[1::2]):
if usage == 0: # delay/no-key
continue
ch = hid.get(usage, '?')
if mod & MOD_SHIFT:
ch = (shift_map.get(ch, ch.upper()))
out.append(ch)
print(''.join(out))
```
Output giống như 1 đường link

Thử truy cập `https://justpaste.it/grp32`

```
FLAG: Blitz{1'm_4_nak}
```