---
# System prepended metadata

title: '[[MSEC CTF] Write-up: en-enc'

---

# [[MSEC CTF] Write-up: ez-enc

:::info
**Thông tin Challenge**
* **Tên bài:** ez-enc
* **Thể loại:** Reverse Engineering (Dịch ngược)
![image](https://hackmd.io/_uploads/BkXX-D8IWl.png)
:::

---

## 1. Thăm dò:

Khi giải nén file ez-enc.zip, ta nhận được một loạt các file thực thi (.exe) được đánh số thứ tự từ 0.exe đến 26.exe.

Quan sát nhanh:

* Kích thước file, mã hash và so sánh nội dung byte cho thấy các file này gần như giống hệt nhau.

* Sự khác biệt chỉ nằm ở một đoạn rất nhỏ (thường là 16 bytes).

**Nhận định:** Điều này gợi ý rằng mỗi file .exe đang chứa một "mảnh" dữ liệu (chunk) được nhúng (patch) vào trong binary. Việc ghép các mảnh này lại có thể dẫn đến lời giải.

## 2. Tìm dữ liệu khác biệt:

Để tìm vị trí các byte khác nhau, ta có thể so sánh file 0.exe với 1.exe (hoặc bất kỳ cặp nào khác). Sử dụng công cụ so sánh binary (hoặc command line như cmp), ta thấy các offset khác nhau tạo thành một dải liên tiếp dài 16 bytes.

Cấu trúc 16 bytes này chia làm 2 phần:
1. 8 bytes đầu: Đóng vai trò là key.
2. 8 bytes sau: Đóng vai trò là expected (kết quả mong đợi).

## 3. Re Reverse Logic & Extraction

Khi disassemble (dịch ngược) một file bất kỳ bằng IDA/Ghidra hoặc objdump, logic kiểm tra flag của chương trình hoạt động như sau:

1. Chương trình nhận input là 8 ký tự.
2. Thực hiện phép XOR input với key.
3. So sánh kết quả với expected.

Công thức:

$$Input \oplus Key = Expected$$

Từ đó, ta có thể suy ngược ra dữ liệu gốc (Input) mà không cần chạy chương trình:

$$Input = Key \oplus Expected$$

**Chiến thuật:** Mỗi file .exe chứa 1 chunk (8 bytes). Ta có 27 file (0 đến 26), ghép theo thứ tự sẽ được một chuỗi dữ liệu dài.

## 4. Xử lý Encoding nhiều lớp

Sau khi ghép tất cả các chunk lại, ta thu được một chuỗi trông giống định dạng Base64. Tuy nhiên, khi decode trực tiếp sẽ gặp lỗi.

Vấn đề gặp phải:

1. Bit Flip: Có một vài byte bị "lật bit" (thường là XOR với 0x80), làm cho ký tự không còn nằm trong bảng mã Base64 hợp lệ.
2. Corrupted Bytes: Một số layer giải mã có 1-2 ký tự bị hỏng hoàn toàn, cần phải brute-force nhỏ để sửa.

Quy trình giải mã (Recursive):

* Lặp lại quá trình decode nhiều lần (Peel layers).
* Trước mỗi lần decode:
  * Kiểm tra và sửa lỗi bit (XOR 0x80 nếu byte > 127 hoặc không thuộc charset).
  * Nếu vẫn còn lỗi, thử brute-force 1-2 vị trí sai.

Dừng lại khi tìm thấy pattern của Flag (MSEC{...}).

## 5. Script Solve

Dưới đây là đoạn code Python hoàn chỉnh để tự động hóa toàn bộ quá trình: đọc file, trích xuất dữ liệu, sửa lỗi và giải mã.

```
#!/usr/bin/env python3
import base64, glob, os, string, itertools

# Thiết lập bảng mã Base64 tiêu chuẩn
B64_ALPH = (string.ascii_letters + string.digits + "+/=").encode()
B64_SET = set(B64_ALPH)
B64_64 = (string.ascii_letters + string.digits + "+/").encode()

def fix_flip_msb(b: bytes) -> bytes:
    """Nếu gặp byte không thuộc base64, thử XOR 0x80 để sửa (lật MSB)."""
    bb = bytearray(b)
    for i, c in enumerate(bb):
        if c in B64_SET:
            continue
        c2 = c ^ 0x80
        if c2 in B64_SET:
            bb[i] = c2
    return bytes(bb)

def try_b64decode_strict(b: bytes):
    """Decode base64 với padding tự động, trả None nếu fail."""
    # Thêm padding để độ dài chia hết cho 4
    pad = (-len(b)) % 4
    bb = b + b"=" * pad
    try:
        return base64.b64decode(bb, validate=True)
    except Exception:
        return None

def brute_fix_small(b: bytes, max_bad=2):
    """
    Nếu còn <= max_bad ký tự không thuộc base64, 
    brute-force thử thay bằng bảng 64 ký tự chuẩn.
    """
    bad = [i for i, c in enumerate(b) if c not in B64_SET]
    if len(bad) == 0:
        return b
    if len(bad) > max_bad:
        return None

    # Brute-force các vị trí lỗi
    for repl in itertools.product(B64_64, repeat=len(bad)):
        tmp = bytearray(b)
        for pos, ch in zip(bad, repl):
            tmp[pos] = ch
        
        # Thử decode sau khi sửa
        out = try_b64decode_strict(bytes(tmp))
        if out is not None:
            return bytes(tmp) # Trả về chuỗi đã sửa (encoded) để decode sau
    return None

def peel_layers(data: bytes):
    """Lột nhiều lớp base64 cho đến khi ra flag hoặc không decode được nữa."""
    cur = data
    for _ in range(20): # Giả sử tối đa 20 lớp
        # 1. Fix kiểu flip MSB trước
        cur = fix_flip_msb(cur)
        
        # 2. Brute-force nếu còn ít ký tự hỏng
        fixed = brute_fix_small(cur, max_bad=2)
        if fixed is None:
            break
        cur = fixed

        # 3. Decode
        out = try_b64decode_strict(cur)
        if out is None:
            break
        
        cur = out
        
        # Kiểm tra Flag
        if b"MSEC{" in cur and b"}" in cur:
            return cur
    return cur

def main():
    # Đọc file theo thứ tự số (0.exe, 1.exe, ...)
    files = sorted(glob.glob("*.exe"), key=lambda p: int(os.path.splitext(os.path.basename(p))[0]))
    if not files:
        raise SystemExit("Chạy script trong thư mục chứa 0.exe .. 26.exe")

    blobs = [open(p, "rb").read() for p in files]
    base = blobs[0]

    # Tìm tất cả offset khác nhau so với file 0
    diff_offsets = set()
    for b in blobs[1:]:
        n = min(len(base), len(b))
        diff_offsets.update(i for i in range(n) if base[i] != b[i])

    if not diff_offsets:
        raise SystemExit("Không tìm thấy khác biệt giữa các file?")

    # Gom offset thành các đoạn liên tiếp
    diff_sorted = sorted(diff_offsets)
    runs = []
    if diff_sorted:
        start = prev = diff_sorted[0]
        for x in diff_sorted[1:]:
            if x == prev + 1:
                prev = x
            else:
                runs.append((start, prev))
                start = prev = x
        runs.append((start, prev))

    # Chọn run dài nhất (thường là block 16 bytes key+expected)
    runs.sort(key=lambda t: (t[1] - t[0] + 1), reverse=True)
    s, e = runs[0]
    length = e - s + 1
    
    if length < 16:
        raise SystemExit(f"Block khác nhau dài nhất chỉ {length} bytes (<16). Cần kiểm tra lại.")

    # Lấy đúng 16 bytes đầu của run làm (key || expected)
    # Lưu ý: logic gốc dùng s16:e16 nhưng thực tế diff 16 byte là đủ
    s16 = s
    e16 = s + 16

    chunks = []
    for b in blobs:
        block = b[s16:e16]
        key = block[:8]
        exp = block[8:]
        # XOR để lấy chunk gốc
        chunk = bytes(a ^ c for a, c in zip(key, exp))
        chunks.append(chunk)

    joined = b"".join(chunks)
    
    # Bắt đầu giải mã các lớp
    final = peel_layers(joined)

    # In ra flag
    if b"MSEC{" in final:
        st = final.find(b"MSEC{")
        ed = final.find(b"}", st)
        if ed != -1:
            print(f"\n[+] Flag found: {final[st:ed+1].decode(errors='ignore')}")
            return

    print(f"\n[!] Raw Output: {final}")

if __name__ == "__main__":
    main()
```

## 6. Kết quả

:::success
Flag: MSEC{AU7oMATION_is_thE_K3Y}
:::