Đề bài

Giải
Bài này cho mình dịch vụ TCP (qua TLS) ở augury.challs.pwnoh.io:1337 cho ta 3 lựa chọn:
- View Files: liệt kê và cho tải “ciphertext” của secret_pic.png (trả về một chuỗi hex rất dài)
- Upload File: nhập tên file, password, và nội dung file ở dạng hex; server thông báo “Your file has been uploaded and encrypted”
- Exit
Mục tiêu: giải mã secret_pic.png để lấy flag
Điểm mấu chốt: Challenge cố tình “quảng cáo” cực kỳ an toàn ⇒ gợi ý đến các lỗi keystream reuse trong OTP/CTR/stream cipher — một bẫy crypto kinh điển của CTF
Quan sát A - Định dạng trả về
- Khi “View Files”, server trả một dòng hex (không phải file PNG thật). Vậy dòng hex này là ciphertext của secret_pic.png
Quan sát B - Hành vi “Upload”
- Màn hình upload yêu cầu “nội dung file ở dạng hex” và báo “has been encrypted”
- Nếu hệ thống dùng stream cipher / OTP-style: C = P ⊕ K (K là dòng keystream). Nếu re-use keystream (cùng K) cho mọi file, ta sẽ phá được
Giả thuyết 1 - Keystream bị tái sử dụng giữa các file
- Nhiều đề beginner làm đúng lỗi này: dùng một PRNG tạo K và không thay nonce/IV, hoặc thậm chí “keo kiệt” dùng cùng một K cho tất cả.
- Nếu đúng, ta có thể tự tạo chosen-plaintext, để suy ra K rồi giải file bí mật.
Đây không phải đoán mò:
(i) Đề cho phép xem ciphertext bất kỳ lúc nào.
(ii) Đề cho phép upload dữ liệu hex tuỳ chọn → môi trường hoàn hảo cho chosen-plaintext.
(iii) Thông điệp “the most secure encryption techniques” ở CTF thường là irony (mồi).
Cách kiểm chứng giả thuyết 1
Ta thử upload một file có plaintext toàn 0 với độ dài đúng bằng ciphertext secret_pic.png. Khi đó:
C_zero = 0x00...00 ⊕ K = K
Tức ciphertext của file toàn 0 chính là keystream
Rồi ta lấy:
P_secret = C_secret ⊕ K = C_secret ⊕ C_zero
Nếu giả thuyết đúng, P_secret sẽ là PNG hợp lệ (bắt đầu bằng 8 byte PNG magic 89 50 4E 47 0D 0A 1A 0A) và file mở được
Khai thác
Bước 1 - Lấy ciphertext bí mật
Vào menu → View Files → chọn secret_pic.png → lấy chuỗi hex (gọi là CT_secret_hex)
Kiểm tra chiều dài (số ký tự hex phải chẵn; số byte = số ký tự / 2)
Bước 2 - Chuẩn bị upload file toàn 0
Ta cần upload một chuỗi hex gồm 00 lặp lại chính xác len(CT_secret_hex) ký tự (tức số byte bằng số byte của C_secret)
Tại sao phải đúng độ dài?
Vì ta muốn keystream K đủ dài để XOR toàn bộ C_secret. Nếu ngắn hơn, ta chỉ giải được prefix. Nếu dài hơn, server có thể cắt hoặc từ chối (tuỳ cài đặt)
Bước 3 - Upload file “keystream”
- Name: keystream (tuỳ ý, miễn trùng để lát nữa “View Files” đọc được)
- Password: tuỳ ý
- Contents (hex): 00 * (số byte C_secret)
Server báo “Your file has been uploaded and encrypted”
Bước 4 - Lấy keystream
- Vào View Files → chọn keystream → lấy CT_zero_hex
- Giải thích: vì plaintext = all-zero nên CT_zero = K
Bước 5 - Giải mã
- P_secret = hex_to_bytes(CT_secret_hex) XOR hex_to_bytes(CT_zero_hex)
- Ghi ra secret.png và kiểm tra magic:
-
$ file secret.png secret.png: PNG image data, ...
- Nếu là PNG hợp lệ, mở ảnh, flag thường nằm chữ vẽ trên ảnh hoặc PNG text chunk
Bước 6 - Tự động hoá (pwntools)
Script (đã chạy thành công ở bạn) làm đúng các bước trên:
- Lấy secret_pic.png (ciphertext)
- Upload zero file cùng độ dài (thu được keystream)
- XOR ra secret.png
- Thử trích flag từ tEXt/iTXt/zTXt (nếu có)
Nếu file secret.png báo data: thường do một trong các lỗi thao tác (phần dưới).
Script
#!/usr/bin/env python3
from pwn import *
import struct
HOST, PORT = "augury.challs.pwnoh.io", 1337
PNG_SIG = b"\x89PNG\r\n\x1a\n"
A = 3404970675
B = 3553295105
MOD = 2**32
def u32(b): return struct.unpack(">I", b)[0]
def p32(x): return struct.pack(">I", x & 0xffffffff)
def recv_menu(r):
r.recvuntil(b"Please select an option:")
r.recvuntil(b"> ")
def get_ciphertext(r, name):
r.sendline(b"2")
r.recvuntil(b"Choose a file to get")
r.recvuntil(b"> ")
r.sendline(name.encode())
ct_hex = r.recvline().strip().decode()
recv_menu(r)
return bytes.fromhex(ct_hex)
def next_state(s):
return (s * A + B) % MOD
def gen_stream(s0, nbytes):
out = bytearray()
s = s0
while len(out) < nbytes:
out += p32(s)
s = next_state(s)
return bytes(out[:nbytes])
def main():
r = remote(HOST, PORT, ssl=True)
recv_menu(r)
ct = get_ciphertext(r, "secret_pic.png")
r.close()
s0 = u32(ct[0:4]) ^ u32(PNG_SIG[0:4])
s1 = u32(ct[4:8]) ^ u32(PNG_SIG[4:8])
if next_state(s0) != (s1 & 0xffffffff):
print("[!] LCG check failed — cipher might differ. Abort to avoid garbage.")
print(f" s0={s0:#010x}, predicted s1={next_state(s0):#010x}, observed s1={s1:#010x}")
return
print(f"[*] Recovered LCG seed/state s0 = {s0:#010x}")
ks = gen_stream(s0, len(ct))
pt = bytes(c ^ k for c, k in zip(ct, ks))
with open("secret.png", "wb") as f:
f.write(pt)
print("[+] Wrote secret.png")
print("[*] First 16 bytes:", pt[:16].hex())
if pt.startswith(PNG_SIG):
print("[+] Looks like a valid PNG. Open secret.png to read the flag!")
else:
print("[!] Doesn’t start with PNG signature; share the first 32 bytes above if still off.")
if __name__ == "__main__":
main()
Flag
Flag: bctf{pr3d1c7_7h47_k3y57r34m}
'WriteUp > Crypto' 카테고리의 다른 글
| [Crypto] Guess Me! - m0leCon Teaser 2026 (0) | 2025.10.25 |
|---|---|
| [Crypto] EZ-Des - Dreamhack (0) | 2025.10.21 |
