baby serial

let-the-penguin-live

Kiểm tra thông tin file mkv đề cho
ffprobe -hide_banner challenge.mkv
Input #0, matroska,webm, from 'challenge.mkv':
Metadata:
title : Penguin
COMMENT : EH4X{k33p_try1ng}
MAJOR_BRAND : isom
MINOR_VERSION : 512
COMPATIBLE_BRANDS: isomiso2avc1mp41
ENCODER : Lavf62.3.100
Duration: 00:01:03.02, start: 0.000000, bitrate: 1021 kb/s
Stream #0:0: Video: h264 (High), yuv420p(tv, bt709, progressive), 576x320 [SAR 1:1 DAR 9:5], 23.98 fps, 23.98 tbr, 1k tbn (default)
Metadata:
HANDLER_NAME : ISO Media file produced by Google Inc.
VENDOR_ID : [0][0][0][0]
ENCODER : Lavc62.11.100 libx264
DURATION : 00:01:03.022000000
Stream #0:1: Audio: flac, 44100 Hz, stereo, s16 (default)
Metadata:
title : English (Stereo)
ENCODER : Lavc62.11.100 flac
DURATION : 00:01:03.019000000
Stream #0:2: Audio: flac, 44100 Hz, stereo, s16
Metadata:
title : English (5.1 Surround)
ENCODER : Lavc62.11.100 flac
DURATION : 00:01:03.019000000
Thấy nó chứa 2 file audio, trích xuất ra
ffmpeg -i challenge.mkv -map 0:1 -c copy audio_stereo.flac
ffmpeg -i challenge.mkv -map 0:2 -c copy audio_surround.flac
Ở đây thì mình đã nghe qua 2 file audio và thấy chúng không có điểm gì khác biệt lắm về mặt nội dung (đều nói về cùng nội dung trong vieo), có một file dài hơn file còn lại khoảng 1 giây nên mình đã nghi điểm khác biệt về mặt âm thanh sẽ là flag
Lấy ra điểm khác biệt giữa 2 file
ffmpeg -y -i audio_stereo.flac -i audio_surround.flac -filter_complex "amix=inputs=2:weights='1 -1'" -ss 00:00:00.0 -to 00:01:03.0 diff.flac
Sau đó mình sẽ xem quang phổ của nó

Flag: EH4X{0n3_tr4ck_m1nd_tw0_tr4ck_f1les}
painter

Bài này sẽ cho mình một file pcap USB

File pcap này sẽ bắt chuyển động của chuột
Chuột sẽ gửi:
- dx (int16)
- dy (int16)
Khôi phục thao tác chuột
import struct, math, os
import numpy as np
import cv2
pcap_path="pref.pcap"
data=open(pcap_path,"rb").read()
def u32le(b,o): return struct.unpack_from("<I",b,o)[0]
def u16le(b,o): return struct.unpack_from("<H",b,o)[0]
def u64le(b,o): return struct.unpack_from("<Q",b,o)[0]
# Minimal pcapng reader: pull Enhanced Packet Blocks
packets=[]
off=0
while off+8<=len(data):
btype=u32le(data,off)
blen=u32le(data,off+4)
if blen<12 or off+blen>len(data):
break
body=data[off+8:off+blen-4]
if btype==0x00000006 and len(body)>=20: # EPB
caplen=u32le(body,12)
pkt=body[20:20+caplen]
packets.append(pkt)
off+=blen
# DLT_USB_LINUX_MMAPPED (220): 64-byte header, then data_len bytes
coords=[]
x=y=0
deltas=[]
for pkt in packets:
if len(pkt)<64:
continue
dlen=u32le(pkt,36)
if 64+dlen>len(pkt):
continue
pl=pkt[64:64+dlen]
if len(pl)<6:
continue
dx=struct.unpack_from("<h",pl,2)[0]
dy=struct.unpack_from("<h",pl,4)[0]
x+=dx; y+=dy
coords.append((x,y))
deltas.append((dx,dy))
xy=np.array(coords, dtype=np.float64)
# PCA deskew angle
pts_center=xy-xy.mean(axis=0)
cov=np.cov(pts_center.T)
eigvals,eigvecs=np.linalg.eig(cov)
v=eigvecs[:, np.argsort(eigvals)[::-1][0]]
angle=math.atan2(v[1], v[0]) # radians
theta=-angle
R=np.array([[math.cos(theta), -math.sin(theta)],
[math.sin(theta), math.cos(theta)]])
rot=(pts_center @ R.T)
# Split stroke on fast cursor moves to reduce connecting lines
step=np.hypot(np.diff(rot[:,0]), np.diff(rot[:,1]))
thresh=np.percentile(step, 99) # cut only the fastest 1%
segs=[]
cur=[rot[0]]
for i in range(1,len(rot)):
if math.hypot(rot[i,0]-rot[i-1,0], rot[i,1]-rot[i-1,1])>thresh:
if len(cur)>1: segs.append(np.array(cur))
cur=[rot[i]]
else:
cur.append(rot[i])
if len(cur)>1: segs.append(np.array(cur))
allp=np.vstack(segs)
minx,miny=allp.min(axis=0); maxx,maxy=allp.max(axis=0)
pad=60
W=int((maxx-minx)+2*pad); H=int((maxy-miny)+2*pad)
canvas=np.ones((H,W), dtype=np.uint8)*255
for s in segs:
xs=(s[:,0]-minx+pad).astype(np.int32)
ys=(maxy-s[:,1]+pad).astype(np.int32) # invert Y for display
pts=np.stack([xs,ys],axis=1).reshape((-1,1,2))
cv2.polylines(canvas,[pts],False,0,2,lineType=cv2.LINE_AA)
out_path="recovered_drawing.png"
cv2.imwrite(out_path, canvas)
out_path, len(packets), len(coords), thresh
Mình sẽ thu được ảnh sau

Mirror ảnh lại thì sẽ thu được

Flag: EH4X{Wh4t_c0l0ur_15_th3_fl4g}
power leak

Thiết bị đang kiểm tra secret theo từng vị trí (position). Với mỗi position, hệ thống thử một guess (0-9). Khi guess đúng hoặc sai, chương trình đi qua đường code khác nhau (hoặc thực hiện phép tính khác), nên công suất tiêu thụ khác nhau tại một vài thời điểm
Dataset cho nhiều lần đo:
- 20 traces cho cùng một guess → để lấy trung bình giảm nhiễu (noise)
- 50 samples trong mỗi trace → power theo thời gian
Trong file này, phần lớn samples chỉ là baseline ~ như nhau. Nhưng có một khoảng ngắn (ở đây là sample ~20–21) mà:
- các guess tạo ra khác biệt rõ rệt
- và guess đúng thường tạo “đỉnh” (hoặc hình dạng) khác
Cho từng position:
- Group theo guess (0..9)
- Với mỗi guess, average 20 traces → ra “mean trace” ổn định
- Tìm “leak point” t:
- đo variance giữa 10 guess tại từng sample
- sample nào variance lớn → sample đó phân biệt guess mạnh nhất
- Ở sample leak đó, chọn guess có mean power lớn nhất (hoặc nhỏ nhất tùy bài) → ra digit của secret
Script
import pandas as pd
import numpy as np
import hashlib
df = pd.read_csv("power_traces.csv").sort_values(
["position", "guess", "trace_num", "sample"]
)
# (pos=6, guess=10, trace=20, sample=50)
arr = df["power_mW"].to_numpy().reshape((6, 10, 20, 50))
def between_guess_var_at(pos, t):
# mean over traces for each guess at sample t
mu = arr[pos, :, :, t].mean(axis=1) # (10,)
return float(mu.var(ddof=1)), mu
digits = []
for pos in range(6):
v20, mu20 = between_guess_var_at(pos, 20)
v21, mu21 = between_guess_var_at(pos, 21)
if v20 >= v21:
digit = int(np.argmax(mu20))
else:
digit = int(np.argmax(mu21))
digits.append(str(digit))
secret = "".join(digits)
h = hashlib.sha256(secret.encode()).hexdigest()
print("secret =", secret)
print("flag =", f"EHAX{{{h}}}")
Flag: EHAX{5bec84ad039e23fcd51d331e662e27be15542ca83fd8ef4d6c5e5a8ad614a54d}
Quantum Message

Bài này cho mình một file âm thanh, đầu tiên vẫn sẽ là kiểm tra quang phổ trước

Sau một hồi tìm kiếm thì mình thấy đây là dạng DTMF
Script giải mã
import numpy as np
import scipy.io.wavfile as wav
from itertools import groupby
import math
path = "challenge.wav"
sr, x = wav.read(path)
x = x.astype(np.float32)
if x.ndim > 1:
x = x[:, 0]
# Custom DTMF-like frequency bins (đúng với file này)
low_cent = np.array([301.5, 904.3, 1501.9, 2104.9], dtype=np.float32)
high_cent = np.array([2702.4, 3305.3, 3908.2], dtype=np.float32)
# STFT params
win = 8192
hop = 1024
window = np.hanning(win).astype(np.float32)
freqs = np.fft.rfftfreq(win, 1/sr)
mask = (freqs > 50) & (freqs < 5000)
fi = freqs[mask]
def nearest(val, arr):
idx = int(np.argmin(np.abs(arr - val)))
return idx
symbols = []
times = []
for start in range(0, len(x) - win, hop):
frame = x[start:start+win] * window
spec = np.abs(np.fft.rfft(frame))[mask]
top = np.argpartition(spec, -10)[-10:]
top = top[np.argsort(spec[top])[::-1]]
selected = []
for idx in top:
f = float(fi[idx])
if all(abs(f - s) > 40 for s in selected):
selected.append(f)
if len(selected) == 2:
break
if len(selected) != 2:
continue
a, b = sorted(selected)
if b < 2400:
continue
r = nearest(a, low_cent)
c = nearest(b, high_cent)
symbols.append((r, c))
times.append(start / sr)
# RLE theo symbol change
rle = []
for sym, group in groupby(zip(symbols, times), key=lambda st: st[0]):
g = list(group)
t0 = g[0][1]
t1 = g[-1][1] + hop/sr
rle.append((sym, t0, t1, t1 - t0))
# Ước lượng độ dài 1 "phím"
unit = np.median([d for _,_,_,d in rle])
# Expand các block dài (do lặp số) thành nhiều phím
expanded = []
for sym, t0, t1, d in rle:
n = max(1, int(round(d / unit)))
expanded.extend([sym] * n)
# Keypad mapping (giống DTMF chuẩn, nhưng tần số custom)
digit_map = {}
d = 1
for r in range(3):
for c in range(3):
digit_map[(r, c)] = str(d); d += 1
digit_map[(3, 1)] = "0"
digits = "".join(digit_map[s] for s in expanded)
# Parse ASCII: chỉ cho phép 2 hoặc 3 chữ số / ký tự printable
from functools import lru_cache
@lru_cache(None)
def rec(i):
if i == len(digits):
return [""]
out = []
for L in (2, 3):
if i + L <= len(digits):
v = int(digits[i:i+L])
if 32 <= v <= 126:
for tail in rec(i + L):
out.append(chr(v) + tail)
return out
sol = rec(0)[0]
print(sol)
Flag: EH4X{qu4ntum_phys1c5_15_50_5c4ry}
Jpeg Soul

Bài này stego theo dạng trích tất cả giá trị trong các bảng DQT, lấy bit cuối (value & 1), ghép thành bytes thì ra flag
Script
#!/usr/bin/env python3
import struct, re
ZIGZAG = [
0, 1, 8,16, 9, 2, 3,10,
17,24,32,25,18,11, 4, 5,
12,19,26,33,40,48,41,34,
27,20,13, 6, 7,14,21,28,
35,42,49,56,57,50,43,36,
29,22,15,23,30,37,44,51,
58,59,52,45,38,31,39,46,
53,60,61,54,47,55,62,63
]
def unzigzag(q64):
out = [0]*64
for pos, natural_idx in enumerate(ZIGZAG):
out[natural_idx] = q64[pos]
return out
def extract_all_dqt_tables(jpeg: bytes):
assert jpeg[:2] == b"\xff\xd8"
i = 2
tables = []
while i < len(jpeg):
if jpeg[i] != 0xFF:
j = jpeg.find(b"\xff", i)
if j < 0: break
i = j
while i < len(jpeg) and jpeg[i] == 0xFF:
i += 1
if i >= len(jpeg): break
marker = jpeg[i]; i += 1
if marker == 0xDA: # SOS
break
if marker in (0xD8, 0xD9) or (0xD0 <= marker <= 0xD7):
continue
seglen = struct.unpack(">H", jpeg[i:i+2])[0]; i += 2
seg = jpeg[i:i+seglen-2]; i += seglen-2
if marker == 0xDB: # DQT
j = 0
while j < len(seg):
pq_tq = seg[j]; j += 1
pq = pq_tq >> 4
if pq != 0:
raise ValueError("Only 8-bit DQT supported here (Pq=0).")
q = list(seg[j:j+64]); j += 64
tables.append(q)
return tables
def bits_to_bytes(bits):
out = bytearray()
for k in range(0, len(bits), 8):
chunk = bits[k:k+8]
if len(chunk) < 8: break
b = 0
for bit in chunk: # MSB-first
b = (b << 1) | bit
out.append(b)
return bytes(out)
jpeg = open("soul.jpg", "rb").read()
tables = extract_all_dqt_tables(jpeg)
vals = []
for t in tables:
vals.extend(unzigzag(t)) # <<< điểm khác biệt: un-zigzag rồi mới đọc
bits = [v & 1 for v in vals]
blob = bits_to_bytes(bits)
print(blob.decode(errors="replace"))
Flag: EHAX{jp3g_s3crt}
'WriteUp > Forensics' 카테고리의 다른 글
| Forensics - ESCHATON CTF Quals 2026 (0) | 2026.03.01 |
|---|---|
| Forensics - VSL CTF 2026 (0) | 2026.01.26 |
| Báo cáo dang dở - Cookie Arena (0) | 2025.11.22 |
| Under Control - Cookie Arena (0) | 2025.11.22 |
| Masks Off - HackTheBox (0) | 2025.11.21 |
