---
# System prepended metadata

title: Wannagame Champion 2025
tags: [RC4, _inactive, Write-up, Algorithm, AES, Antidebug, XTEA, Runtime, Linux, AutoRev]

---

Vũ Hoàng Long
24520019
Team: 3HLD

# Reverse

## Buzzing
linux root, eBPF Assembly

Thực sự dạng này mình khá ít gặp nên khá lúng túng không biết làm sao. Research một hồi lâu thì mới ra được dùng lệnh ` llvm-objdump -d locker.bpf.o` để dump ra code eBPF Assembly chứa logic check.

Nó chặn cụ thể tên file là "readflag". Nếu tên chương trình KHÔNG phải là "readflag", nó sẽ nhảy qua đoạn kiểm tra (goto ...) và cho phép chạy bình thường. Tuy nhiên, mình không thể dùng lệnh cp (copy), và phải thử Symlink (ln -s). Symlink hoạt động như một "shortcut". Khi chạy symlink, hệ thống eBPF sẽ nhìn thấy tên của symlink thay vì readflag.

![image](https://hackmd.io/_uploads/SJc3NN4f-g.png)



## checker
crypto - antidebug - runtime overwrite data

Đầu tiên là file checker.exe yêu cầu nhập 1 hoặc 2 để flag checker cho part1 và part2+3, mỗi lựa chọn sẽ sinh ra một file flag_checker.exe khác nhau và chương trình con này sẽ bị xóa ngay sau khi thực hiện xong
![image](https://hackmd.io/_uploads/rJtDF-4MZl.png)
![image](https://hackmd.io/_uploads/rJKcKWVfZg.png)
Vậy là 2 file này có sẵn trong resource và được load ra bằng các hàm này. mình có thể dùng Resource Hacker để lấy file hoặc đơn giản là chờ khi nhập lựa chọn rồi copy file flag_checker.exe của từng part ra một cái tên khác để phân tích.

### Part 1
Sau khi đã đọc qua các hàm và đổi tên theo ý nghĩa:
```c
int __fastcall main(int argc, const char **argv, const char **envp)
{
  __int64 v3; // r8
  _QWORD *v4; // rax
  __int64 v5; // r8
  char v7; // [rsp+30h] [rbp-128h] BYREF
  _BYTE v8[3]; // [rsp+31h] [rbp-127h] BYREF
  unsigned int v9; // [rsp+34h] [rbp-124h]
  int v10; // [rsp+38h] [rbp-120h]
  __int64 v11; // [rsp+40h] [rbp-118h]
  _QWORD *v12; // [rsp+48h] [rbp-110h]
  __int64 v13; // [rsp+50h] [rbp-108h]
  _BYTE v14[8]; // [rsp+58h] [rbp-100h] BYREF
  _BYTE v15[16]; // [rsp+60h] [rbp-F8h] BYREF
  _BYTE v16[16]; // [rsp+70h] [rbp-E8h] BYREF
  _BYTE input_encrypted[24]; // [rsp+80h] [rbp-D8h] BYREF
  _BYTE target[24]; // [rsp+98h] [rbp-C0h] BYREF
  _BYTE v19[24]; // [rsp+B0h] [rbp-A8h] BYREF
  _BYTE v20[24]; // [rsp+C8h] [rbp-90h] BYREF
  _BYTE v21[24]; // [rsp+E0h] [rbp-78h] BYREF
  _BYTE v22[24]; // [rsp+F8h] [rbp-60h] BYREF
  _BYTE v23[16]; // [rsp+110h] [rbp-48h] BYREF
  _BYTE v24[15]; // [rsp+120h] [rbp-38h] BYREF
  char v25; // [rsp+12Fh] [rbp-29h] BYREF

  if ( (word_140042018[2] & 1) != 0 )
    print(&io, "Flag checker 1 !!!!!!!!!!!\n", envp);
  else
    print(&io, "Flag checker 1 !!!!!!!!!!\n", envp);
  print(&io, "Your flag: ", v3);
  sub_1400040C0(&qword_140043580, &unk_1400432E8);
  v11 = unknown_libname_59(&v7);
  v12 = (_QWORD *)sub_1400099D0(&unk_1400432E8, v14);
  v4 = (_QWORD *)sub_140009480(&unk_1400432E8, v15);
  sub_140003F40(v22, *v4, *v12, v11);
  Xor(v21, v22, &unk_1400432A0);
  RC4(v20, v21, &unk_1400432B8);
  v9 = seed_gen(1337LL);
  LCG(v19, v20, v9);
  Chacha((unsigned int)target, (unsigned int)v19, (unsigned int)&unk_1400432D0, (unsigned int)&unk_140043288, 0);
  v24[0] = -4;
  v24[1] = 118;
  v24[2] = -44;
  v24[3] = 9;
  v24[4] = -93;
  v24[5] = -40;
  v24[6] = 80;
  v24[7] = 47;
  v24[8] = -71;
  v24[9] = -41;
  v24[10] = -70;
  v24[11] = -32;
  v24[12] = -80;
  v24[13] = 52;
  v24[14] = -78;
  v13 = unknown_libname_59(v8);
  qmemcpy(v16, (const void *)unknown_libname_63(v23, v24, &v25), sizeof(v16));
  sub_140006820(input_encrypted, v16, v13);
  if ( (unsigned __int8)cmp(target, input_encrypted) )
    print(&io, "Correct!\n", v5);
  else
    print(&io, "Incorrect!\n", v5);
  v10 = 0;
  sub_1400074D0(input_encrypted);
  sub_1400074D0(target);
  sub_1400074D0(v19);
  sub_1400074D0(v20);
  sub_1400074D0(v21);
  sub_1400074D0(v22);
  return v10;
}
```
Vậy flow là input -> xor -> RC4 -> LCG -> Chacha.

Có thể thấy, cho dù qua nhiều bước khác nhau, nhưng tất cả các loại này để mã hóa kiểu xor... mình đoán nếu như các key là cố định, mình hoàn toàn có thể tìm flag mà không cần reverse từng hàm. Bằng cách debug với một input bất kì, ghi lại output cuối cùng ở `input_encrypted` trước khi nó gọi hàm cmp, ta sẽ có flow:
`input ^ x1 ^ x2 ^ x3 ... = input_encrypted`
Vậy với input bất kì và ghi lại input_encrypted, ta sẽ tính được tổng quát quá trình encrypt thực ra chỉ là xor
`x1 ^ x2 ^ x3 ... = input_encrypted ^ input`
Vậy chỉ cần lấy `target` xor với `x1 ^ x2 ^ x3 ...`, ra sẽ có được input cần nhập. 
```py 
v18 =   "FC 76 D4 09 A3 D8 50 2F B9 D7 BA E0 B0 34 B2"
inp = 'a' * len(v18)
encrypted = "CA 26 CE 2B AA 8D 5F 29 E9 D8 BC DE B5 61 A7"

v18 = bytes.fromhex(v18)
encrypted = bytes.fromhex(encrypted)

for i in range(len(v18)):
    x = encrypted[i] ^ ord('a')
    print(chr(v18[i] ^ x), end="")
```
Flag part 1: `W1{Ch4ng1ng_d4t`


### Part 2:
Sau khi xem hàm main ở flag_checker.exe của part 2, mình thấy nó cũng gần y chang như part 1, mình cũng dùng cách tương tự để giải nhưng lần này lại ra input là một chuỗi không đọc được???

Flow ở main rõ ràng là giống với part 1, nên chỉ có một khả năng đó là các mảng key/target lúc debug khác với lúc chạy thường -> `x1 ^ x2 ^ x3 ...` này cũng sẽ khác.

Mọi thứ mảng key cần thiết cho các bước mã hóa đều không có sẵn mà được tạo ra ở trước khi gọi vào hàm main. Bằng việc XREF các mảng đó, mình đến được hàm `__scrt_common_main_seh()` tìm đoạn khởi tạo:

```c 
// ...
  v3 = (unsigned int)dword_7FF65DC64CF0;
  if ( dword_7FF65DC64CF0 == 1 )
  {
LABEL_19:
    _scrt_fastfail(7LL);
    goto LABEL_20;
  }
  if ( dword_7FF65DC64CF0 )
  {
    v2 = 1;
  }
  else
  {
    dword_7FF65DC64CF0 = 1;
    if ( initterm_e((_PIFV *)&qword_7FF65DC503C8, (_PIFV *)&qword_7FF65DC50408) )
      return 255LL;
    initterm((_PVFV *)&First, (_PVFV *)&Last);
    dword_7FF65DC64CF0 = 2;
  }
// ...
```

Sau khi đọc và đổi tên các hàm:
![image](https://hackmd.io/_uploads/rys0GzNGWg.png)

Và đúng thật là ở hàm cuối cùng được gọi nó có antidebug:

```c
__int64 __fastcall overwrite_data_1(__int64 a1)
{
  _QWORD *v1; // rax
  _QWORD *v3; // [rsp+30h] [rbp-28h]
  _BYTE v4[8]; // [rsp+40h] [rbp-18h] BYREF
  _BYTE v5[16]; // [rsp+48h] [rbp-10h] BYREF

  if ( !*(&NtCurrentPeb()->InheritedAddressSpace + dword_7FF65DC63000) )
  {
    v3 = (_QWORD *)sub_7FF65DC2A680(&unk_7FF65DC645D0, v4);
    v1 = (_QWORD *)sub_7FF65DC2A0B0(&unk_7FF65DC645D0, v5);
    sub_7FF65DC26880((char *)&off_7FF65DC64650 - 152, *v1, *v3);
  }
  return a1;
}
```
InheritedAddressSpace lưu trạng thái của debug process clone, nếu đang debug thì = 1. Vậy trên thực tế thì các hàm bên trong này được chạy còn lúc mình debug thì nó không chạy nên không overwrite.

Vậy mình sẽ đặt break point chỗ này và sửa lại giá trị trả về của cái antidebug kia.
![image](https://hackmd.io/_uploads/BJpoEGVGbl.png)
Phần còn lại làm như part 1 (đặt breakpoint ở trước cmp, continue, nhập 'a'*17, ghi lại giá trị encrypted mới):

```py 
v18 =   "4E DD 81 CD 84 8F DD 2B C2 5A A6 B7 A9 1A F7 ED F3"
inp = 'a' * len(v18)
encrypted = "1B E3 D1 C2 BA 9C C9 24 94 0A AA B3 97 32 E5 D3 E3"

v18 = bytes.fromhex(v18)
encrypted = bytes.fromhex(encrypted)

for i in range(len(v18)):
    x = encrypted[i] ^ ord('a')
    print(chr(v18[i] ^ x), end="")
```
Flag part2: `4_1n_run71me_Is_q`

### Part 3

Mình đã thử tìm xem trong resource xem còn gì không nhưng thực sự chỉ có 2 file đó thôi. Vậy là đúng file của part 2 chứa cả việc checker cho part 3 (đoán vậy vì nó đã in thông báo Flag 2+3). Nhưng mà nó ở đâu??

Để tìm ra phần quan trọng cần cho part 3, mình đã thử debug ở nhiều chỗ có vẻ khả nghi, tìm Tls callback, tìm hàm mới nào được sinh ra, ... thì mình thấy các hàm được gọi ở initterm có những hàm ghi bộ nhớ ra đó nhưng mà chưa hề được sử dụng, đó là build_CHACHA_NONCE_1 và build_res_diff (do mình tự đổi tên sau khi solve được). Vậy có khả năng là còn một cơ chế gì đó nữa để 2 mảng này được ghi đè lên dữ liệu mà chương trình đã chạy. XREF vào 2 mảng này thì mình đã phát hiện ra nó:
![image](https://hackmd.io/_uploads/H1u_qfNMZl.png)
```c
__int64 OVERWRITE_DATA_2()
{
  _QWORD *v0; // rax
  _QWORD *v1; // rax
  _QWORD *v3; // [rsp+30h] [rbp-48h]
  _QWORD *v4; // [rsp+40h] [rbp-38h]
  _BYTE v5[8]; // [rsp+50h] [rbp-28h] BYREF
  _BYTE v6[8]; // [rsp+58h] [rbp-20h] BYREF
  _BYTE v7[8]; // [rsp+60h] [rbp-18h] BYREF
  _BYTE v8[16]; // [rsp+68h] [rbp-10h] BYREF

  v3 = (_QWORD *)sub_7FF65DC2A680(&unk_7FF65DC645E8, v5);
  v0 = (_QWORD *)sub_7FF65DC2A0B0(&unk_7FF65DC645E8, v6);
  sub_7FF65DC26880((char *)&unk_7FF65DC64638 - 176, *v0, *v3);
  v4 = (_QWORD *)sub_7FF65DC2A680(&off_7FF65DC64668, v7);
  v1 = (_QWORD *)sub_7FF65DC2A0B0(&off_7FF65DC64668, v8);
  sub_7FF65DC26880((char *)&off_7FF65DC64618 + 136, *v1, *v4);
  return *((unsigned __int16 *)&unk_7FF65DC63010 + 6) + (*((unsigned __int16 *)&unk_7FF65DC63010 + 2) << 16);
}
```
Đây là hàm sẽ ghi đè chachanonce và target (đổi target thì khả năng đây chính là part 3 rất cao). XREF vào thì thấy hàm này được gọi ở seed_gen nhưng không tự nhiên được chạy:
![image](https://hackmd.io/_uploads/BkJHoMNzZl.png)
Vậy để cái này chạy except phải sảy ra, coi đoạn trên thì thấy có lệnh div có thể gây lỗi nếu chia 0. Nhưng mình chưa biết cách nào để chỗ đó = 0 nên tạm thời cứ setip vào mà chạy trước, sau đó làm tương tự part 1 và 2. Nhưng lần này khi step kĩ thì mình thấy, hàm seed_gen sau khi gọi vào OVERWRITE_DATA_2 thì sẽ return luôn giá trị còn lại trong rax sau hàm này, đó là `*((unsigned __int16 *)&unk_7FF65DC63010 + 6) + (*((unsigned __int16 *)&unk_7FF65DC63010 + 2) << 16);`
![image](https://hackmd.io/_uploads/BJigRf4MZl.png)
Nhưng đây lại là giá trị ngẫu nhiên, sẽ khác nhau qua các lần tạo file flag_checker khác nhau (có vẻ như là địa chỉ ảo mỗi lần khởi chạy, mình sẽ tìm hiểu thêm sau giải). Chưa biết tính thế nào thì thôi mình code lại logic của chương trình và bruteforces luôn cái seed.

```py 
import binascii
from Crypto.Cipher import ChaCha20, ARC4
from multiprocessing import Pool, cpu_count

# ============================================================
# CONFIG
# ============================================================
target_hex = "3A 8A 52 67 64 47 E6 AF 6C 10 A9 D5 6C 25"
target_bytes = binascii.unhexlify(target_hex.replace(" ", ""))

CHACHA_KEY   = b'\xAA' * 32
CHACHA_NONCE = b'\x01' * 12

rc4_key_hex = '02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11'
RC4_KEY = binascii.unhexlify(rc4_key_hex.replace(" ", ""))

XOR_KEY = b"skibidi"
    
LCG_A = 0x5851F42D4C957F2D
LCG_C = 0x14057B7EF767814F

cipher_cc20 = ChaCha20.new(key=CHACHA_KEY, nonce=CHACHA_NONCE)
after_chacha = cipher_cc20.decrypt(target_bytes)
print("[+] Precomputed ChaCha20 OK:", after_chacha.hex())

# ============================================================
def decrypt_lcg_block(data, seed):
    state = seed & 0xFFFFFFFFFFFFFFFF
    out = bytearray(len(data))

    for i, b in enumerate(data):
        t = state
        key = 0
        for _ in range(8):
            key ^= (t & 0xFF)
            t >>= 8
        out[i] = b ^ key
        state = (state * LCG_A + LCG_C) & 0xFFFFFFFFFFFFFFFF

    return out

def check_flag(buf):
    if len(buf) != 14: 
        return False
    if buf[-1] != ord('}'): 
        return False
    for c in buf:
        if c < 32 or c > 126:
            return False
    return True

# ============================================================
def worker(args):
    start, end = args
    rc4 = ARC4.new(RC4_KEY)

    for seed in range(start, end):
        # LCG
        tmp = decrypt_lcg_block(after_chacha, seed)

        # RC4
        mid = rc4.decrypt(tmp)

        # XOR
        dec = bytes(mid[i] ^ XOR_KEY[i % len(XOR_KEY)] for i in range(len(mid)))

        if check_flag(dec):
            print("\n[!!!] FOUND FLAG:", dec, " seed =", hex(seed))

        # Reset RC4 state for next seed
        rc4 = ARC4.new(RC4_KEY)

    return False

# ============================================================
if __name__ == "__main__":
    N = cpu_count()
    print("[+] Using", N, "CPU cores")

    total = 0x100000000   # 2^32 seeds
    chunk = total // N

    ranges = []
    for i in range(N):
        s = i * chunk
        e = s + chunk if i < N-1 else total
        ranges.append((s, e))
        print(f"Worker {i}: {hex(s)} → {hex(e)}")

    with Pool(N) as p:
        r = p.map(worker, ranges)

    print("[DONE]")
```
Kết quả ra tầm 20 cái thỏa mãn nhưng đây là output đẹp nhất:
seed: `0xe1155fef`
Flag part 3: `u173_fun_h4h4}`

Flag full: `W1{Ch4ng1ng_d4t4_1n_run71me_Is_qu173_fun_h4h4}`

## noRush
crypto - autorev

Đây là dạng bài viết script để tự động giải hàng loạt các file binary. Để làm được điều đó mình cần trước tiên giải được 1 file rồi xem logic của nó có tương tự với các file còn lại không. Nếu giống y nhau chỉ khác các key/target để có input khác thì ta có thế đơn giản viết script đọc hết tất cả các mảng đó và giải cùng 1 logic để ra hàng loạt các đáp án và ghép lại với nhau. Mình đã gặp 1 bài tương tự ở đợt tuyển mem teamT năm ngoái và bài đó trong giải mình đã không làm kịp và upsolve sau giải. Lần này cũng vậy ;-; có lẽ mình cần luyện thêm nhiều ở dạng này để làm kịp thời gian.

Phân tích reze_000, ta được flow như sau (code khá dài và rối):
```
input

permutation     
bitmask
swap(ROL2)
xor
XTEA
ROL1
TEA
RC4 
AES    

checker compare
```
Về cơ bản thì tất cả đều reverse được, nhưng quá dài nên chỉ cần sai một bước thì sẽ không biết sai ở đâu, cách làm của mình để chắc ăn là mình sẽ reverse từ dưới lên từng hàm một. Ví dụ khi giải mã AES, để kiểm tra xem code python đã decrypt đúng chưa, mình debug đặt breakpoint ngày trước khi vào AES và sửa mảng hiện tại thành mảng mà mình decrypt được từ target sau đó mới cho chạy tiếp, chạy xong đúng khớp target và in ra correct thì oke bước đó. Cuối cùng ra được một script chuẩn để giải reze_000.

Đọc tiếp cái file tiếp theo, mình thấy không chỉ các mảng thay đổi giá trị, mà ở mỗi file là một hoán vị khác nhau của bước trong flow ở trên. Vậy nên ngoài việc dump mảng của mỗi file, ta cần dump được flow chính và phải sửa code solve sao cho phù hợp chạy được với tất cả các file:

:::spoiler Script giải reze_000
Script này có thể giải được nhiều file, chỉ cần sửa giá trị các mảng
```python
from Crypto.Cipher import AES
import struct
from Crypto.Cipher import ARC4

def aes_ctr_rust(key: bytes, nonce: bytes, data: bytes, counter_endian: str = "big") -> bytes:
    aes = AES.new(key, AES.MODE_ECB)
    counter = bytearray(nonce)  # copy
    out = bytearray()
    pos = 0
    n = len(data)
    while pos < n:
        keystream = aes.encrypt(bytes(counter))
        chunk = data[pos:pos+16]
        # xor (pad keystream if chunk < 16)
        for i, b in enumerate(chunk):
            out.append(b ^ keystream[i])
        pos += 16

        # increment counter
        if counter_endian == "little":
            # tăng little-endian (index 0 là LSB)
            for i in range(16):
                counter[i] = (counter[i] + 1) & 0xFF
                if counter[i] != 0:
                    break
        else:
            # tăng big-endian (index 15 là LSB) -- phù hợp với ASM bạn dán
            for i in range(15, -1, -1):
                counter[i] = (counter[i] + 1) & 0xFF
                if counter[i] != 0:
                    break

    return bytes(out)

def decrypt_Aes(iv, key, ciphertext):
    p = aes_ctr_rust(key, iv, ciphertext, counter_endian="big")
    return p

# iv = bytes.fromhex("9269D20F44D9232C2BEEBBA87AC47BB7")[::-1]
# key = bytes.fromhex("EC36452F5D9319DC653374E963334B76")[::-1]
# ciphertext = bytes.fromhex("15568527A1FF60928701696C407A402B")[::-1] + bytes.fromhex("344DF3AD05929D449815B52910A125EF")[::-1]

# print(" ".join(f"{x:02X}" for x in ciphertext))

# print(" ".join(f"{x:02X}" for x in Aes(iv, key, ciphertext)))


# C1 69 D8 C3 CF 9B 6A 47 03 3C A7 A6 CB 0E ED E5 9B 1C A5 CD 00 95 FB 67 22 36 9B 84 98 D1 9A F8



def decrypt_rc4(cipher: bytearray, key: bytes):
    c = ARC4.new(key)
    p = c.decrypt(cipher)
    return p

# key = bytes.fromhex("E7 E5 99 70 F0 14 2E 4D A0 A9 18 3F EF CE 37 95")
# c = bytes.fromhex("6C 8C 5A EF 7A AC 59 BA 49 A6 F4 81 D6 67 0B E7 C6 51 B4 E8 5E 4B A9 B7 8C DB 43 0C 62 F7 38 64")
# p = rc4_custom_decrypt(c, key)
# print(" ".join(f"{x:02X}" for x in p))











MASK32 = 0xFFFFFFFF

def _u32(x: int) -> int:
    return x & MASK32

def decrypt_block(ct8: bytes, C) -> bytes:
    """Giải 1 block 8-byte (ct8 length must be 8) theo thuật toán đảo ngược."""
    assert len(ct8) == 8
    # lấy hai từ 32-bit little-endian (asm dùng đọc 32-bit từ buffer)
    v30 = int.from_bytes(ct8[0:4], 'little')
    v31 = int.from_bytes(ct8[4:8], 'little')

    # hằng số theo ASM bạn đưa
    
    START = 0x9E3779B9           # v32 khởi
    DELTA = 0x61C88647           # giá trị bị trừ mỗi vòng trong ASM

    # tính v32 sau khi chạy encryption 32 vòng:
    # v32_final = START - DELTA*32  (lưu ý làm modulo 2^32)
    v32 = _u32(START - (_u32(DELTA * 32)))

    # chạy 32 vòng đảo ngược (r = 31 downto 0 equivalently: add DELTA then undo)
    for _ in range(32):
        # reverse step: trước hết +DELTA để lấy v32 sử dụng trong vòng đó
        v32 = _u32(v32 + DELTA)

        # đảo ngược thứ tự: encryption làm v30 += f(...); v31 += g(...),
        # nên để đảo: ta phải trừ g (dùng v30 hiện hành), rồi trừ f (dùng v31 đã được phục hồi).
        # Cẩn thận với mask 32-bit ở mọi bước.
        # g(v30, v32)  <-> biểu thức tương ứng trong ASM cho v31
        g = _u32(( (16 * v30 + C[1]) ^ _u32(v32 + v30) ^ _u32((v30 >> 5) - C[3]) ))
        v31 = _u32(v31 - g)

        # f(v31, v32)  <-> biểu thức tương ứng trong ASM cho v30
        f = _u32(( (16 * v31 + C[0]) ^ _u32(v31 + v32) ^ _u32((v31 >> 5) - C[2]) ))
        v30 = _u32(v30 - f)

    # trả về little-endian 8 byte
    return v30.to_bytes(4, 'little') + v31.to_bytes(4, 'little')

def decrypt_TEA(ciphertext: bytes, C) -> bytes:
    """Giải 32-byte ciphertext (4 block x 8 bytes) và trả về 32-byte plaintext."""
    assert len(ciphertext) == 32
    out = bytearray()
    # offsets: 0,8,16,24  (the loop in asm xử lý như vậy)
    for off in (0, 8, 16, 24):
        block = ciphertext[off:off+8]
        out.extend(decrypt_block(block, C))
    return bytes(out)

# ct_hex = "06 D0 DB B1 D8 14 F6 23 A8 EE C2 B7 25 E4 2D C6 5C 36 4B 4B 9F 39 C1 8C 61 1D E0 CE 26 C9 91 EE"
# ct = bytes.fromhex(ct_hex)
# pt = decrypt_all(ct)
# in hex cặp phân tách bằng space
# print(" ".join(f"{b:02X}" for b in pt))








def ror1(x, r):
    return ((x >> r) | ((x << (8 - r)) & 0xFF)) & 0xFF

def decrypt_ROL1(buf, step):
    res = bytearray()
    for k in range(32):
        res.append(ror1(buf[k], step))
    return res

# c = bytes.fromhex('49 C9 5A B3 79 16 03 60 39 98 D1 C2 77 E4 62 02 B0 12 6D CF 0C 0D FB A1 4F 9D 4B 08 49 93 0D E6')
# p = reverseROL1(c)
# print(" ".join(f"{b:02X}" for b in p))








MASK32 = 0xFFFFFFFF
DELTA = 0x61C88647  # 1640531527

# Bảng dword_55555555CDF0 (đã hiệu chỉnh thành 4 giá trị 32-bit)

def rol32(x, n):
    return ((x << n) | (x >> (32 - n))) & MASK32

def u32(x):
    return x & MASK32

def decrypt_block_pair(v0: int, v1: int, T):
    """
    Reverse one 32-round encryption that does (per-round):
      v0 += (v1 + ((16*v1) ^ (v1 >> 5))) ^ (T[((byte)v24 + 71) & 3] + v24 + 1640531527)
      v1 += (v0 + ((16*v0) ^ (v0 >> 5))) ^ (v24 + T_index2)
      v24 -= DELTA
    where v24 starts at -DELTA and the used v24 values are -delta, -2delta, ..., -32delta.
    For decryption we iterate v24 = -32*DELTA .. -DELTA, adding DELTA each reverse round.
    """
    # start v24 at -32*DELTA (value used in last encryption round)
    v24 = u32((-32 * DELTA) & MASK32)  # keep as 32-bit unsigned representation
    # But we need signed-like behaviors for indexing and byte extraction; using u32 and python ops works if we mask properly.
    v0 = u32(v0)
    v1 = u32(v1)

    # perform 32 reverse rounds
    for _ in range(32):
        # compute table2 index used in encryption: offset = (v24 >> 9) & 0xC ; index = offset // 4
        offset = (v24 >> 9) & 0xC
        idx2 = offset // 4  # yields 0..3
        t2 = T[idx2]

        # undo v1 addition: v1 -= (v0 + ((16*v0) ^ (v0 >> 5))) ^ (v24 + t2)
        part_v0 = ( (16 * v0) ^ (v0 >> 5) ) & MASK32
        expr1 = u32((v0 + part_v0) & MASK32)
        expr2 = u32(( (v24 + t2) & MASK32 ))
        v1 = u32((v1 - (expr1 ^ expr2)) & MASK32)

        # compute index for table1: ((byte)v24 + 71) & 3
        byte_v24 = v24 & 0xFF
        idx1 = ( (byte_v24 + 71) & 3 )
        t1 = T[idx1]

        # undo v0 addition: v0 -= (v1 + ((16*v1) ^ (v1 >> 5))) ^ (t1 + v24 + 1640531527)
        part_v1 = ( (16 * v1) ^ (v1 >> 5) ) & MASK32
        expr3 = u32((v1 + part_v1) & MASK32)
        expr4 = u32((t1 + v24 + DELTA) & MASK32)
        v0 = u32((v0 - (expr3 ^ expr4)) & MASK32)

        # increase v24 by DELTA to move to previous round's v24 (reverse order)
        v24 = u32((v24 + DELTA) & MASK32)

    return v0, v1

def decrypt_XTEA(cipher32: bytes, T) -> bytes:
    assert len(cipher32) == 32
    out = bytearray(32)
    # process 4 blocks of 8 bytes: offsets 0,8,16,24
    for i in range(4):
        off = i * 8
        # little-endian u32 reads (as in C): *(u32*)(buf + off), *(u32*)(buf + off + 4)
        v0 = int.from_bytes(cipher32[off:off+4], 'little')
        v1 = int.from_bytes(cipher32[off+4:off+8], 'little')
        p0, p1 = decrypt_block_pair(v0, v1, T)
        out[off:off+4] = p0.to_bytes(4, 'little')
        out[off+4:off+8] = p1.to_bytes(4, 'little')
    return bytes(out)

# c = bytes.fromhex('29 39 4B 76 2F C2 60 0C 27 13 3A 58 EE 9C 4C 40 16 42 AD F9 81 A1 7F 34 E9 B3 69 01 29 72 A1 DC')
# p = decrypt_XTEA(c)
# print(" ".join(f"{b:02X}" for b in p))





def decrypt_xor(x, val):
    res = bytearray()
    for k in range(32):
        res.append(x[k] ^ val)
    return res
# c = bytes.fromhex('D5 D0 82 82 CB 88 C6 87 10 D0 8A C4 04 2F CE 62 55 F8 20 A5 1A 88 F2 14 3E F2 E2 53 5B 54 D0 2B')
# p = decrypt_xor(c, 0xDD)
# print(" ".join(f"{b:02X}" for b in p))





def decrypt_swapROL2(x):
    res = bytearray()
    for k in range(0, 32, 2):
        res.append(x[k + 1])
        res.append(x[k])
    return res
# c = bytes.fromhex('08 0D 5F 5F 16 55 1B 5A CD 0D 57 19 D9 F2 13 BF 88 25 FD 78 C7 55 2F C9 E3 2F 3F 8E 86 89 0D F6')
# p = decrypt_swapROL2(c)
# print(" ".join(f"{b:02X}" for b in p))
    
    







# import struct
# arr1_hex = '01 00 00 00 02 00 00 00 05 00 00 00 0D 00 00 00 13 00 00 00 24 00 00 00 4E 00 00 00 AD 00 00 00 08 01 00 00 A8 02 00 00 2D 04 00 00 B3 0D 00 00 A4 1D 00 00 31 2A 00 00 0B 5D 00 00 CF A9 00 00 C6 DB 01 00 CE 4E 03 00 2A F7 05 00 C2 75 0F 00 59 0D 1A 00 37 0A 3E 00 06 E2 61 00 D7 34 BB 00 EB B9 A0 01 90 FD 66 03 32 A0 FB 04 58 B4 BB 08 A4 DE E3 14 B4 E0 EF 25 63 4A 4C 57 21 03 EE 81'
# arr1_raw = bytes.fromhex(arr1_hex)
# ARR1 = [struct.unpack('<I', arr1_raw[i:i+4])[0] for i in range(0, len(arr1_raw), 4)]
def decrypt_bitmask(ARR1, output):
    M = [[(ARR1[row] >> col) & 1 for col in range(32)] for row in range(32)]

    # Copy output thành vector cột
    b = [o for o in output]

    # Gauss-Jordan elimination trên GF(2)
    # biến đổi M thành ma trận đơn vị và áp dụng y hệt lên b
    for col in range(32):
        # tìm pivot
        pivot = None
        for row in range(col, 32):
            if M[row][col]:
                pivot = row
                break
        if pivot is None:
            raise Exception("Ma trận không khả nghịch — arr1 không full-rank!")

        # đổi hàng pivot lên đầu khối
        M[col], M[pivot] = M[pivot], M[col]
        b[col], b[pivot] = b[pivot], b[col]

        # khử các hàng khác
        for row in range(32):
            if row != col and M[row][col]:
                for k in range(col, 32):
                    M[row][k] ^= M[col][k]
                b[row] ^= b[col]

    # Lúc này M = I, b = input
    return bytes(b)
                
# c = bytes.fromhex('0D 08 5F 5F 55 16 5A 1B 0D CD 19 57 F2 D9 BF 13 25 88 78 FD 55 C7 C9 2F 2F E3 8E 3F 89 86 F6 0D')
# p = decrypt_bitmask(c)
# print(" ".join(f"{b:02X}" for b in p))
                
                
                
# permu_hex = '0B 00 00 00 00 00 00 00 18 00 00 00 00 00 00 00 0F 00 00 00 00 00 00 00 0A 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 0E 00 00 00 00 00 00 00 11 00 00 00 00 00 00 00 1B 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 12 00 00 00 00 00 00 00 0C 00 00 00 00 00 00 00 1E 00 00 00 00 00 00 00 15 00 00 00 00 00 00 00 17 00 00 00 00 00 00 00 08 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 1A 00 00 00 00 00 00 00 14 00 00 00 00 00 00 00 0D 00 00 00 00 00 00 00 19 00 00 00 00 00 00 00 05 00 00 00 00 00 00 00 10 00 00 00 00 00 00 00 1D 00 00 00 00 00 00 00 09 00 00 00 00 00 00 00 16 00 00 00 00 00 00 00 06 00 00 00 00 00 00 00 03 00 00 00 00 00 00 00 1F 00 00 00 00 00 00 00 07 00 00 00 00 00 00 00 13 00 00 00 00 00 00 00 1C 00 00 00 00 00 00 00'
# permu_raw = bytes.fromhex(permu_hex)
# PERMUTATION = [struct.unpack('<Q', permu_raw[i:i+8])[0] for i in range(0, len(permu_raw), 8)]     
def decrypt_permutation(permutation, encrypted):
    """
    permutation: list hoặc bytes 32 phần tử
    encrypted: 32-byte array
    return raw_input (32 byte)
    """
    assert len(permutation) == 32
    assert len(encrypted) == 32

    # Tạo perm_inv: vị trí index trong raw_input
    perm_inv = [0] * 32
    for i in range(32):
        p = permutation[i]
        if p >= 32:
            raise Exception("Invalid permutation element")
        perm_inv[p] = i

    # Giải ngược
    raw = [0] * 32
    for raw_idx in range(32):
        enc_idx = perm_inv[raw_idx]
        raw[raw_idx] = encrypted[enc_idx]

    return bytes(raw)

# encrypted = bytes.fromhex('0D 08 52 00 50 44 00 00 0D 89 02 49 A2 00 5E 00 4E 00 00 48 06 0A 00 82 00 03 1A 47 7D 0A 05 00')
# inp = decrypt_permutation(PERMUTATION, encrypted)

# print(inp)



def main():
    ciphertext = bytes.fromhex("15568527A1FF60928701696C407A402B")[::-1] + bytes.fromhex("344DF3AD05929D449815B52910A125EF")[::-1]
    iv = bytes.fromhex("9269D20F44D9232C2BEEBBA87AC47BB7")[::-1]
    key = bytes.fromhex("EC36452F5D9319DC653374E963334B76")[::-1]
    keyrc4 = bytes.fromhex("E7 E5 99 70 F0 14 2E 4D A0 A9 18 3F EF CE 37 95")
    arr1_hex = '01 00 00 00 02 00 00 00 05 00 00 00 0D 00 00 00 13 00 00 00 24 00 00 00 4E 00 00 00 AD 00 00 00 08 01 00 00 A8 02 00 00 2D 04 00 00 B3 0D 00 00 A4 1D 00 00 31 2A 00 00 0B 5D 00 00 CF A9 00 00 C6 DB 01 00 CE 4E 03 00 2A F7 05 00 C2 75 0F 00 59 0D 1A 00 37 0A 3E 00 06 E2 61 00 D7 34 BB 00 EB B9 A0 01 90 FD 66 03 32 A0 FB 04 58 B4 BB 08 A4 DE E3 14 B4 E0 EF 25 63 4A 4C 57 21 03 EE 81'
    permu_hex = '0B 00 00 00 00 00 00 00 18 00 00 00 00 00 00 00 0F 00 00 00 00 00 00 00 0A 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 0E 00 00 00 00 00 00 00 11 00 00 00 00 00 00 00 1B 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 12 00 00 00 00 00 00 00 0C 00 00 00 00 00 00 00 1E 00 00 00 00 00 00 00 15 00 00 00 00 00 00 00 17 00 00 00 00 00 00 00 08 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 1A 00 00 00 00 00 00 00 14 00 00 00 00 00 00 00 0D 00 00 00 00 00 00 00 19 00 00 00 00 00 00 00 05 00 00 00 00 00 00 00 10 00 00 00 00 00 00 00 1D 00 00 00 00 00 00 00 09 00 00 00 00 00 00 00 16 00 00 00 00 00 00 00 06 00 00 00 00 00 00 00 03 00 00 00 00 00 00 00 1F 00 00 00 00 00 00 00 07 00 00 00 00 00 00 00 13 00 00 00 00 00 00 00 1C 00 00 00 00 00 00 00'
    T = [0x89068318, 0x50E0E2FA, 0xBAE51343, 0xA82B0892]
    C = [0x79FC5FF9, 0x3703E082, 0x297DDF89, 0x284CA86A]
    XOR_VAL = 0xDD
    ROL1_VAL = 3
    flow = [1, 2, 3, 4, 5, 6, 7, 8, 9]
    
    arr1_raw = bytes.fromhex(arr1_hex)
    ARR1 = [struct.unpack('<I', arr1_raw[i:i+4])[0] for i in range(0, len(arr1_raw), 4)]
    permu_raw = bytes.fromhex(permu_hex)
    PERMUTATION = [struct.unpack('<Q', permu_raw[i:i+8])[0] for i in range(0, len(permu_raw), 8)]   
    
    
    x = ciphertext
    for i in range(9):
        if flow[i] == 1:
            x = decrypt_Aes(iv, key, x)
        elif flow[i] == 2:
            x = decrypt_rc4(x, keyrc4)
        elif flow[i] == 3:    
            x = decrypt_TEA(x, C)
        elif flow[i] == 4:    
            x = decrypt_ROL1(x, ROL1_VAL)
        elif flow[i] == 5:    
            x = decrypt_XTEA(x, T)
        elif flow[i] == 6:    
            x = decrypt_xor(x, XOR_VAL)
        elif flow[i] == 7:    
            x = decrypt_swapROL2(x)
        elif flow[i] == 8:    
            x = decrypt_bitmask(ARR1, x)
        elif flow[i] == 9:    
            x = decrypt_permutation(PERMUTATION, x)
    
    print(x)
    
main()
```
:::
reze_000:
`b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x02\x05\x00\x00\x03^\x08\x06\x00\x00\x00\x82\xa2}'`
Mình thấy header của PNG, vậy khi giải được tất cả các file ghép lại thì sẽ có một ảnh chứa flag.

Vậy nhiệm vụ tiếp theo là dump hết ra cái đống mảng của từng file để giải. Vì có quá nhiều file cũng như quá nhiều thông tin nên mình sẽ chọn làm riêng bước dump cho từng mảng, ghi giá trị vào các file riêng để thuận tiện cho việc kiểm tra, và thay vì gán mảng trong script solve sẽ thay bằng việc đọc từ các file đã dump.

Dump như thế nào? Đây là một vấn đề khá lớn với mình, để auto lấy được các mảng thì mình cần phải biết địa chỉ, offset của từng mảng, nhưng cái này ở mỗi file lại khác nhau mà mình không tìm ra quy luật nào nhanh gọn để tính hoặc lấy offset cả. Chỉ còn nước cuối cùng là khổ dâm với objdump, tìm thủ công những đoạn nó lấy mảng vì ở đó có địa chỉ gốc.

Hiện tại mình đã dump được hết các file cũng như tính được hết input, nhưng ghép lại ra file PNG không được được. Có thể trong quá trình dump đã sai sót mảng nào đó, mình đã viết subprocess để thửu từng input vào từng file xem file nào sai thì sửa lại bằng tay... sẽ xong trong tối nay.


![image](https://hackmd.io/_uploads/HJ9GxCNG-g.png)
Đây là hình ảnh cuối cùng mình nhận được trong giải, không biết sao nó lại thiếu mất một vài byte mà vẫn in được một phần ;-;
Full code:
https://github.com/UITlogn/auto_reverse_champion_2025
Không hiểu sao mình đã check đúng hết 6519 file nhưng vẫn không đọc được ảnh đầy đủ

Mình đã xem lại code có một chút vấn đề là mình có ghi thừa những kết quả sai vào file flag.png nên nó bị vậy, sau khi sửa lại script thì mình được full:

![image](https://hackmd.io/_uploads/ByGBBAIfWl.png)

Update:
Sau buổi phỏng vấn thì mình được các anh khuyên đổi sang các tool khác để dump như là capstone hoặc iced x86. Mình sẽ thử tìm hiểu về capstone trước.

Update code:
https://github.com/UITlogn/auto_reverse_champion_2025_update

## Who's luckier
seed random by time, ...

Mình đọc sơ qua thì thấy bài này lấy time(0) làm seed để qua nhiều lần gọi rand(), nhảy qua đúng flow của các case để đến được nơi decrypt flag. Vậy nên mình đã thử viết script để fake time khi gọi chạy rồi brute forces seed gần với ngày tạo file nhưng không ra. Có lẽ bên trong đoạn switch kia còn ẩn chứa cái gì đó nữa...

Phân tích từng case trong switch(dword_28E20), dword_28E20 đóng vai trò một state machine, thực hiện nhiều lần gọi biến đổi liên quan đến rand() để quyết định nhảy đến case nào tiếp theo. Mục tiêu là nhảy được đến case 7 và dword_28E64 == 39577:
* Hash SHA-256 mảng unk_28E00.
* Dùng hash làm key AES-256-CBC
* IV sinh từ hex string ivhex, XREF thì thấy nó lấy từ hàm sub_1535E mỗi chuỗi cố định.
* Decrypt AES-256-CBC file flag_enc.dat → xuất ra flag_output.png.

Hàm gán IV:
```c
unsigned __int64 sub_1535E()
{
  char v1; // [rsp+7h] [rbp-29h] BYREF
  char *v2; // [rsp+8h] [rbp-28h]
  char *v3; // [rsp+10h] [rbp-20h]
  unsigned __int64 v4; // [rsp+18h] [rbp-18h]

  v4 = __readfsqword(0x28u);
  v2 = &v1;
  sub_15B2E(&ivhex, "deadbeefdeadbeefdeadbeefdeadbeef", &v1);
  sub_15DD2(&v1);
  __cxa_atexit((void (*)(void *))&std::string::~string, &ivhex, &dso_handle);
  v3 = &v1;
  sub_15B2E(&unk_28E40, &unk_17852, &v1);
  sub_15DD2(&v1);
  __cxa_atexit((void (*)(void *))&std::string::~string, &unk_28E40, &dso_handle);
  return v4 - __readfsqword(0x28u);
}
```

Case 7:
```c
      case 7:
        if ( dword_28E64 != 39577 )
        {
          dword_28E20 = 6;
          continue;
        }
LABEL_29:
        v4 = std::string::length(&unk_28E40);
        v5 = std::string::c_str(&unk_28E40);
        SHA256(v5, v4, &key);
        for ( i = 0; i <= 15; ++i )
        {
          std::string::substr(v30, &ivhex, 2 * i, 2LL);
          v6 = std::string::c_str(v30);
          __isoc23_sscanf(v6, "%02hhx", (char *)&iv + i);
          std::string::~string(v30);
        }
        std::ifstream::basic_ifstream(v30, "flag_enc.dat", 4LL);
        v24 = &v17;
        sub_15C4C(v26);
        sub_15C02(v25, v30);
        sub_15C76(v27, v25[0], v25[1], v26[0], v26[1], &v17);
        sub_15F48(&v17);
        std::ifstream::close(v30);
        v21 = EVP_CIPHER_CTX_new();
        v7 = EVP_aes_256_cbc();
        EVP_DecryptInit_ex(v21, v7, 0LL, &key, &iv);
        v8 = sub_15D60(v27) + 16;
        v22 = v8 - 1;
        v9 = 16 * ((v8 + 15) / 0x10uLL);
        while ( v16 != &v16[-(v9 & 0xFFFFFFFFFFFFF000LL)] )
          ;
        v10 = alloca(v9 & 0xFFF);
        if ( (v9 & 0xFFF) != 0 )
          *(_QWORD *)&v16[(v9 & 0xFFF) - 8] = *(_QWORD *)&v16[(v9 & 0xFFF) - 8];
        v23 = v16;
        LODWORD(v26[0]) = 0;
        v20 = 0;
        v11 = sub_15D60(v27);
        v12 = sub_15D84(v27);
        EVP_DecryptUpdate(v21, v23, v26, v12, v11);
        v20 = v26[0];
        EVP_DecryptFinal_ex(v21, &v23[SLODWORD(v26[0])], v26);
        v20 += LODWORD(v26[0]);
        EVP_CIPHER_CTX_free(v21);
        std::ofstream::basic_ofstream(v28, "flag_output.png", 4LL);
        if ( (unsigned __int8)std::ios::operator!(&v29) )
        {
          v13 = std::operator<<<std::char_traits<char>>(&std::cerr, "Error: Cannot open output file flag_output.png");
          std::ostream::operator<<(v13, &std::endl<char,std::char_traits<char>>);
        }
        std::ostream::write((std::ostream *)v28, v23, v20);
        std::ofstream::close(v28);
        v14 = std::operator<<<std::char_traits<char>>(
                &std::cout,
                "Decrypted data successfully written to flag_output.png");
        std::ostream::operator<<(v14, &std::endl<char,std::char_traits<char>>);
        std::ofstream::~ofstream(v28);
        sub_15D06(v27);
        std::ifstream::~ifstream(v30);
        return 0LL;
```

Vậy giờ mình cần tìm mảng unk_28E40 để hash ra key.
XREF vào unk_28E40 thì ta tới được đoạn này:
```c 
      case 12:
        if ( dword_28E60 <= dword_28E64 )
        {
          dword_28E20 = 6;
        }
        else
        {
          std::string::operator=(&unk_28E40, &unk_17852);
          for ( j = dword_28E24 - 1; j >= 0; --j )
          {
            sub_15561(v30, *((unsigned int *)off_23C68 + j));
            std::string::operator+=(&unk_28E40, v30);
            std::string::~string(v30);
          }
          dword_28E20 = 7;
          dword_28E64 = dword_28E60;
          std::operator<<<std::char_traits<char>>(&std::cout, "I dare you are so lucky to be here!\n");
        }
        continue;
```
Lại XREF vào off_23C68, ta được:
![image](https://hackmd.io/_uploads/HJAHXywMWl.png)
Trong main:
```c 
      case 9:
        *((_DWORD *)off_23C68 + dword_28E24++) = dword_28E28;
        dword_28E20 = 2;
        continue;
///...
      case 4:
        if ( *((_DWORD *)off_23C68 + dword_28E24) <= *((_DWORD *)off_23C68 + dword_28E24 - 1)
          || *((_DWORD *)off_23C78 + *((int *)off_23C68 + dword_28E24)) <= *((_DWORD *)off_23C78
                                                                           + *((int *)off_23C68 + dword_28E24 - 1)) )
        {
          dword_28E20 = 6;
        }
        else
        {
          off_23C60 = (_UNKNOWN *)((char *)off_23C60 - 559038737 - 0xCAFEBABE % dword_28E20);
          dword_28E20 = 5;
        }
        continue;
```

Hàm sub_2816 dẫn đếm một hàm quá lớn mà IDA không thể decompile được, xem graph view thì mình thấy có khi nó lại là một switch case khác:
![image](https://hackmd.io/_uploads/BkpzV1vGZe.png)
![image](https://hackmd.io/_uploads/rytL4JwM-e.png)
Có tận 500 case...

Mình đã có tìm hiểu xem nó tác động gì tới off_23C68 không nhưng vẫn khá khó khăn, nên thôi quay lại phân tích nhưng gì trong main trước:

Case 9: 
```
off_23C68[dword_28E24++] = dword_28E28;
// = rand % 20 + 1 ở case 8 và = rand() % 5000 ở case 0;
```
Case 4: Kiểu tra nếu:
```
if (
    off_23C68[dword_28E24]] <= off_23C68[dword_28E24 - 1] &&
    off_23C78[off_23C68[dword_28E24]] <= off_23C78[off_23C68[dword_28E24 - 1]])
    jumpcase6();
else
    jumpcase5()
```
mà ở case 6 lại reset dword_28E24 = 0; dword_28E20 = 0;
Còn ở case 5:
```c 
      case 5:
        v27[0] = 20;
        dword_28E24 = *Min(&dword_28E24, v27);
        dword_28E20 = 4;
        if ( !--dword_28E24 )
          dword_28E20 = 10;
        continue;
```
Nếu để đoan này sảy ra `if ( !--dword_28E24 ) dword_28E20 = 10;` thì nó sẽ đến case 10 và in `I think not many people have accumulated enough virtue to be blessed with the luck to be here :)` rồi lại gọi sub_2816(dword_28E24), còn khi quay lại case 4, nó sẽ tiếp tục kiểm tra điều kiện kia (4 5 4 5 ... 5 4 10), vậy đây rõ ràng là một vòng lặp, tóm tắt như sau:
```
i = dword_28E24
b = off_23C68
a = off_23C78
// từ bây giờ mình sẽ của a, b, i để kí hiệu
for (i = 20 -> 1) {
    if (b[i] <= b[i - 1] || a[b[i]] <= a[b[i - 1]])
        reset;
}
```
Vậy ta có thể xem như b là một mảng lưu một tập hợp của a bằng các chỉ số phải tăng dần, và giá trị tưởng ứng của các vị trí đó trong a cũng phải tăng dần.

Mà dword_28E64 = dword_28E60 (case 12) = sub_2816(dword_28E24) =  (case 10).
Case 7 lại kiểm tra dword_28E64 = 39577. Vậy ta cần làm cho sub_2816(dword_28E24) = 39577.

Vậy tóm lại là rand() với seed nào đó nó phải đủ may mắn để tạo ra mảng index 20 số là các vị trí của một dãy con tăng trong mảng ban đầu, mà sao cho sub_2816() trả về 39577.

Đã đến lúc thực sự phải phân tích sub_2816()...
...
...

...
Và mình thấy được phân quan trọng là nó tính tổng của các `off_23C70[b[i]]`.

Tổng kết: Cho mảng a là off_23C68, mảng c là off_23C70 (2 mảng này có sẵn trong IDA). Cần tìm mảng b có 20 phần tử sao cho a[b[i]] là dãy tăng dần và tổng c[b[i]] bằng 39577.

![image](https://hackmd.io/_uploads/HJYdtgDMZl.png)
Format phần tử đầu 4 byte rồi tạo array 5000 phần tử:
![image](https://hackmd.io/_uploads/SyDTFevzZx.png)
Tương tự với mảng kia.

Hmm mà liệu có thể có nhiều dãy thỏa mãn không nhỉ? Vậy phải có ràng buộc gì đó để tìm chứ? Mình chợt nghĩ đến bài toán LIS tìm dãy con tăng dài nhất, nếu có nhiều dãy thì chọn dãy có tổng trọng số (mảng c) lớn nhất, mà nếu có nhiều dãy con tăng có trọng số lớn nhất thì tìm dãy có thứ tự từ điển lớn...(xàm rồi). Đây chỉ là phỏng đoán, khả năng mảng 5000 phần tử mà lis 20 thì hơi khó, tổng 39577 thì cũng khá nhỏ (đã test và thấy maxsum 167288, lis thì 140). Để tìm một dãy con có độ dài cố định và tổng cố định (mà chưa biết liệu đó có phải là lớn nhất hay chưa), thì ta phải gọi dp[i][j][k] = 0/1 nếu tồn tại dãy con độ dài i, hiện đang kết thúc tại vị trí j trên mảng a và có tổng trên mảng c là k. Độ phức tạp bộ nhớ là 20 * 5000 * 39567 khoảng 4e9 là quá lớn. Vì tổng 39567 gây ra độ phức tạp lớn nhiều nhất, nên mình sẽ thử giả thuyết tìm dãy con tăng có độ dài 20 và tổng lớn nhất trước (20 * 5000^2).

```py
lis = 20
dp = [[0 for j in range(len(a))] for i in range(lis)]
ans = 0
for i in range(len(a)):
    for j in range(i):
        if a[i] > a[j]:
            dp[0][i] = c[i]
            for l in range(1, lis):
                dp[l][i] = max(dp[l][i], dp[l - 1][j] + c[i])
    ans = max(ans, dp[lis - 1][i])
print(ans)
```
Kết quả in ra 39567 cho ta thấy ta đã xác định đúng.
Để truy vết lại các vị trí đã chọn để tạo dãy con, mình sẽ dùng mảng trace với các trạng thái tương tự dp, lưu vị trí j trước đó đã chọn khi nối i vào để có kết quả dp ở hiện tại. Sau đó chỉ cần trace ngược từ vị trí đạt ans lớn nhất dần về trước.
```py
import struct
a = open("a.txt", 'r').read()
a = bytes.fromhex(a)
a = [struct.unpack('<I', a[i:i+4])[0] for i in range(0, len(a), 4)]

c = open("c.txt", 'r').read()
c = bytes.fromhex(c)
c = [struct.unpack('<I', c[i:i+4])[0] for i in range(0, len(c), 4)]

lis = 20
dp = [[0 for j in range(len(a))] for i in range(lis)]
trace = [[-1 for j in range(len(a))] for i in range(lis)]
ans = 0
for i in range(len(a)):
    for j in range(i):
        if a[i] > a[j]:
            dp[0][i] = c[i]
            for l in range(1, lis):
                if dp[l][i] < dp[l - 1][j] + c[i]:
                    dp[l][i] = dp[l - 1][j] + c[i]
                    trace[l][i] = j
    if ans < dp[lis - 1][i]:
        ans = dp[lis - 1][i]
        pos = i
print(ans)

res = []
while pos > -1:
    res.append(pos)
    pos = trace[lis - len(res)][pos]
print(len(res))
print(res)

import hashlib
from Crypto.Cipher import AES

iv = bytes.fromhex("deadbeefdeadbeefdeadbeefdeadbeef")
key = "".join(str(i) for i in res)
key = hashlib.sha256(key.encode()).digest()
        
cipher = open("flag_enc.dat", "rb").read()
aes = AES.new(key, AES.MODE_CBC, iv)
plain = aes.decrypt(cipher)
print(plain[:10])
with open("flag.png", "wb") as f:
    f.write(plain)
```
![image](https://hackmd.io/_uploads/H1ckkAZQ-g.png)







## Protocols

Trong `run.sh`:
```
xxd -p -c 16 | while read line; do dotnet saw.dll $line; done
```
read input theo line, chia thành block 16 bytes, hex encode (32 hex chars), mỗi dòng hex chạy: `dotnet saw.dll <hex_block>`
flag.enc có đúng 3 dòng × 16 bytes => flag dài 48 bytes.

![image](https://hackmd.io/_uploads/HJ2SWRbQWx.png)
`saw.dll` vừa là file .NET, vừa là file ZIP, bên trong DLL có JAR

Thử unzip:
![image](https://hackmd.io/_uploads/H1aTV0-QZl.png)
Vì toàn bộ code rất dài nên mình sẽ để nhưng gì mình đọc được ở đây: [file](https://drive.google.com/file/d/1sDwrMoY2xCuKjieDlGc6bFix8VsEG2Kj/view?usp=sharing)

Mình thấy được 3 thành phần chính:
1. Master Controller (C# - saw.dll): luồng chính, quản lí trạng thái trong mảng s, giao tiếp gửi nhận lệnh.
2. Crypto Oracle (Python - saw.py): giải mã instructions mà C# gửi đến. Các mảng byte trong C# (ví dụ: new byte[] {34, 38...}) là opcode đã bị mã hóa. Python dùng Curse.dll làm khóa để giải mã chúng và trả về opcode sạch (plaintext) cho C#.
3. Arithmetic VM (Java - Saw.class, Hack.class): Đây là nơi thực hiện các phép toán logic (cộng, trừ, XOR, shift...). C# sử dụng giao thức JDWP (Java Debug Wire Protocol) để điều khiển máy ảo Java, gọi các hàm trong Hack.class để biến đổi dữ liệu.

Flow:
File saw.dll thực hiện thuật toán mã hóa gồm 22 vòng lặp (rr từ 0 đến 22):
- Input: 3 dòng trong flag.enc chính là output của thuật toán này. Bạn cần tìm Input (chính là Flag) bằng cách đảo ngược thuật toán.
- State: Mảng long[] s trong C# lưu trữ trạng thái. Input được nạp vào s[32], s[33], s[34], s[35].
- Vòng lặp (Loop):
    + C# gửi một mảng byte mã hóa (encrypted blob) tới Python.
    + Python giải mã và trả về opcode.
    + C# dùng hàm Stop() để gửi opcode này tới Java (qua JDWP).
    + Java (Hack.class) thực thi phép toán trên mảng Hack.m hoặc trả về kết quả.
    + C# cập nhật mảng s dựa trên kết quả trả về.

## Dutchman_app
apk, ...

Mình đọc thấy có đoạn check devide ID, bypass được rồi nhưng khi build gặp lỗi nên chưa biết làm gì tiếp


## Quit buzzing around
linux root, eBPF Assembly


# The end
Giải này mình bị stuck ở bài n0Rush khá lâu vì đọc objdump gây sai lệnh khá nhiều. Vì vậy nên mình chưa kịp đọc các bài sau. Sau giải thì team lại bị ban do có thành viên k20 âm thầm submit flag có instance ở team khác... giải này mình làm gần như full time nên nói chung là quá khổ ;-;

Qua giải mình rút ra là phải tìm cách tăng tốc độ làm những bài auto reverse, tìm những cách làm thông minh hơn sau giải, đồng thời xem lại kiến thức về APK và LINUX. Sẽ còn update...