# LLM Đọc tên đề thì hiểu luôn bài là dạng Linked List gì đó. Chạy thử thì bài bắt ta nhập 3 Key (0 1 2). Nạp vào IDA xem thử: ```cpp! int __cdecl main(int argc, const char **argv, const char **envp) { unsigned int count; // [rsp+20h] [rbp-78h] char *v5; // [rsp+28h] [rbp-70h] _BYTE *Block; // [rsp+30h] [rbp-68h] __int64 v7; // [rsp+48h] [rbp-50h] __int64 v8; // [rsp+50h] [rbp-48h] char *v9; // [rsp+58h] [rbp-40h] BYREF char *v10; // [rsp+60h] [rbp-38h] BYREF char *v11; // [rsp+68h] [rbp-30h] BYREF char *v12; // [rsp+70h] [rbp-28h] BYREF char v13[8]; // [rsp+78h] [rbp-20h] BYREF v10 = 0i64; // null v11 = 0i64; // null v9 = 0i64; // null v12 = 0i64; // null printf("Enter key to get flag!\n"); v5 = data; count = 0; while ( v5 ) // đoạn này ta nhập key 3 lần (key 0 1 2) { printf("Key%d : ", count); scanf("%d", v13); add_node(&v10, &v11, v13[0], v5[9]); v5 = *(v5 + 10); ++count; } Congdathuc(data, v10, &v9, &v12); v7 = Calculate(v9, 1337i64); v8 = Calculate(v9, 9999i64); if ( v7 == 0xD427202CB4B2i64 ) { printf("Correct!\n"); Block = malloc(0x31ui64); sub_7FF637791490(&unk_7FF637794078, 0x30ui64, v8, Block); Block[48] = 0; printf("Flag: %s\n", Block); free(Block); } else { printf("Wrong!\n"); } return 0; } ``` Cùng phân tích nào: - Ta được cho sẵn v5, mở IDA ra và thấy là dường như nó là 1 cái LinkedList đôi. Đề bài này coi LinkedList là 1 cách biểu diễn đa thức, đa thức `v5` sau khi dump data thì ta biết được nó là đa thức có dạng sau: $$ 36⋅x^4+18⋅x^2+7 $$ - Đồng thời có luôn cả format Node: ``` struct Node { struct Node *prev; uint8_t coef; uint8_t deg; struct Node *next; }; ``` - Tiếp theo, chương trình lấy giá trị key của mình nhập làm tham số cho add_node. Tham số deg là lấy của v5 100% (tức là các số mũ như nhau) - Thực hiện các phép toán dựa trên LinkedList rồi check với Kết Qủa có sẵn. Cùng nhau mổ xẻ các hàm quan trọng nhé: ```cpp! char **__fastcall add_node(char **tail, char **top, char a1, char a2) { char **result; // rax char *v5; // [rsp+20h] [rbp-18h] v5 = makenode(a1, a2); if ( *tail ) { // Đã có tail *(*top + 10) = v5; *v5 = *top; result = top; *top = v5; } else { // Null List *top = v5; result = tail; *tail = v5; } return result; } ``` Theo Logic thẻ bản chất hàm add node trông đơn giản như này thôi: ``` 0 node tail ──┐ node top ──┘ 1 node tail -> [A] <-> [B] <-> [C] <- top ``` `make_node`: Đây làm sinh 1 node (1 phần tử của đa thức): ```cpp! char *__fastcall makenode(char a1, char a2) { char *v3; // [rsp+20h] [rbp-18h] v3 = malloc(0x12ui64); if ( !v3 ) return 0i64; v3[8] = a1; // coef v3[9] = a2; // deg *v3 = 0i64; // prev *(v3 + 10) = 0i64; // next return v3; } ``` Vậy với vòng lặp chạy 3 lần (do `v5` có 3 Node), chương trình nhảy xuống hàm Congdathuc: ```cpp! __int64 __fastcall Congdathuc(__int64 a1, __int64 a2, char **a3, char **a4) { __int64 result; // rax List mới cầm kqua while ( a1 || a2 ) { if ( a2 && (!a1 || *(a1 + 9) >= *(a2 + 9)) ) { if ( a1 && *(a2 + 9) >= *(a1 + 9) ) // đề cho như kia thì chỉ nhảy vào trường hợp degree bằng nhau này thôi { add_node(a3, a4, *(a2 + 8) + *(a1 + 8), *(a1 + 9));// Đưa các kết quả (Tổng hệ số) các phần tử a1 = *(a1 + 10); result = *(a2 + 10); a2 = result; } else { add_node(a3, a4, *(a2 + 8), *(a2 + 9)); result = *(a2 + 10); a2 = result; } } else { add_node(a3, a4, *(a1 + 8), *(a1 + 9)); result = *(a1 + 10); a1 = result; } } return result; } ``` Sau khi cộng xong thì nhảy vào Calculator để tính giá trị của đa thức mới ta cóa :D ```cpp! __int64 __fastcall sub_7FF6377913F0(__int64 a1, __int64 a2) { int i; // [rsp+0h] [rbp-28h] __int64 v4; // [rsp+8h] [rbp-20h] __int64 v5; // [rsp+10h] [rbp-18h] v5 = 0i64; while ( a1 ) { v4 = 1i64; for ( i = 0; i < *(a1 + 9); ++i ) // coef*x cộng với nhay cho đến khi hết deg v4 *= a2; //a2 = x v5 += v4 * *(a1 + 8); a1 = *(a1 + 10); } return v5; } ``` Vậy nếu như Đa thức: $$ (36 + k0)⋅x^4+(18+k1)⋅x^2+7+k0 $$ Ta nhập cộng với Đa thức `v5` mà khi thay `x = 1337` mà nhận được Kết quả: `0xD427202CB4B2i64` thì nó sẽ sinh khóa bằng `Calculate` với x = 9999 để giải mã flag: Ta brute luôn cho nhanh: ```python! TARGET = 0xD427202CB4B2 x = 1337 x2 = pow(x, 2) x4 = pow(x, 4) for k0 in range(256): part0 = (36 + k0) * pow(x, 4) rem1 = TARGET - part0 if rem1 < 0: continue for k1 in range(256): part1 = (18 + k1) * pow(x, 2) rem2 = rem1 - part1 if rem2 < 18: continue k2 = rem2 - 7 if 0 <= k2 <= 255: print(k0, k1, k2) ``` Flag: **KCSC{You_have_passed_the_DSA_final_exam!!!}** # PuRE (easy): Ném file vào IDA, ta được đưa đến main là 1 Puzzle Game (mình đổi tên vài hàm cho dễ quan sát): ```cpp! __int64 __fastcall main(int a1, char **a2, char **a3) { char v4[256]; // [rsp+0h] [rbp-710h] BYREF char dest[512]; // [rsp+100h] [rbp-610h] BYREF char s[1008]; // [rsp+300h] [rbp-410h] BYREF __int64 v7; // [rsp+6F0h] [rbp-20h] unsigned int seed; // [rsp+6F8h] [rbp-18h] int v9; // [rsp+6FCh] [rbp-14h] char *v10; // [rsp+700h] [rbp-10h] __int64 i; // [rsp+708h] [rbp-8h] seed = time(0LL); srand(seed); puts("======================================================="); puts("= PUZZLE CTF CHALLENGE ="); puts("======================================================="); reset(); while ( 1 ) { printf("\n>> "); if ( !fgets(s, 1000, stdin) ) break; s[strcspn(s, "\n")] = 0; if ( s[0] ) { strcpy(dest, s); for ( i = 0LL; dest[i]; ++i ) dest[i] = tolower((unsigned __int8)dest[i]); if ( !strcmp(dest, "quit") || !strcmp(dest, "exit") ) { puts("Bye!"); return 0LL; } if ( !strcmp(dest, "solution") ) { if ( dword_55555555EFA0 ) moves_reversed(); else puts("[!] Doc code chua ma go command linh tinh?"); } else if ( !strcmp(dest, "test") ) { reset(); Flow_check = 0; Scramble_and_Store(36LL); show_Cube(); } else if ( !strcmp(dest, "help") ) { printf("[!] Ga the, cho cai nay thi co du trinh solve khong: 0x%x", seed); } else if ( !strcmp(dest, "challenge") ) { if ( dword_55555555A16C ) { puts("\n[!] Starting REAL CHALLENGE..."); puts("[!] Scrambling 1836 times. Du trinh ko???\n"); reset(); puts("[!] Reset challenge...Done\n"); Flow_check = 1; Scramble_and_Store(1836LL); } else { puts("\n[!] You must complete TEST mode first!"); } } else if ( dword_55555555EFA0 ) { v7 = 0LL; v10 = strtok(s, " "); v9 = 0; while ( v10 ) { if ( (unsigned int)sub_555555556169(v10) ) ++v9; else printf("[!] Unknown: %s\n", v10); v10 = strtok(0LL, " "); } if ( v9 > 0 ) { if ( (unsigned int)sub_555555556894() ) { putchar(10); puts("================================================"); puts("= CONGRATULATIONS! CHALLENGE SOLVED! ="); puts("================================================"); if ( Flow_check ) { sub_555555555219(v4, 256LL); printf(" FLAG: %s\n", v4); printf(" Total moves: %d\n", (unsigned int)dword_55555555A164); putchar(10); puts(" You are a TRUE Rot's Master!"); } else { putchar(10); puts(" Nice! You solved the practice cube!"); printf(" Total moves: %d\n", (unsigned int)dword_55555555A164); putchar(10); puts(" >> You can now access CHALLENGE mode!"); puts(" >> Type 'challenge' for the REAL challenge!"); dword_55555555A16C = 1; } puts("================================================"); if ( Flow_check ) return 0LL; reset(); Flow_check = 0; dword_55555555EFA0 = 0; } else { if ( dword_55555555A164 > 998 ) { puts("\n[!] Too many moves! This puzzle is not that kind of ez=)))"); return 0xFFFFFFFFLL; } printf("Moves: %d\n", (unsigned int)dword_55555555A164); } } } else { puts("[!] Doc code chua ma go tinh tinh zay"); } } } return 0LL; } ``` `Scramble and Store`: ```cpp! __int64 __fastcall sub_555555556391(int a1) { __int64 result; // rax char *src[12]; // [rsp+10h] [rbp-70h] int v3; // [rsp+70h] [rbp-10h] int v4; // [rsp+74h] [rbp-Ch] int i; // [rsp+78h] [rbp-8h] int j; // [rsp+7Ch] [rbp-4h] src[0] = "U"; src[1] = "U'"; src[2] = "D"; src[3] = "D'"; src[4] = "L"; src[5] = "L'"; src[6] = "R"; src[7] = "R'"; src[8] = "F"; src[9] = "F'"; src[10] = "B"; src[11] = "B'"; if ( a1 > 50 ) { printf("[!] Scrambling with % d moves", (unsigned int)a1); for ( i = 0; i < a1; ++i ) { v4 = rand() % 12; strcpy((char *)&res + 10 * i, src[v4]); Scramble(src[v4]); if ( !(i % 100) ) putchar(46); } puts(" Done!"); } else { printf("[!] Scramble (%d moves): ", (unsigned int)a1); for ( j = 0; j < a1; ++j ) { v3 = rand() % 12; strcpy((char *)&res + 10 * j, src[v3]); Scramble(src[v3]); printf("%s ", src[v3]); } putchar(10); } result = (unsigned int)a1; dword_55555555EFA0 = a1; dword_55555555A164 = 0; return result; } ``` Đây là bên trong `Scamble and Store`, có vẻ như đề bài lấy seed theo thời gian, tạo `srand()` theo seed đó và từ việc gọi `rand()` sẽ chọn 1 cách tráo rubik ngẫu nhiên. Khi gọi help, ta được ném cho seed và khi gọi test ta được ném cho 1 challenge warm-up nhỏ là giải khối rubik bị tráo 36 lần và cuối cùng khi gọi `challenge` ta sẽ phải giải khối được tráo là `1836` lần tráo: Flow bắt buộc của đề là: `help -> test -> challenge -> Flag`. Dường như ta chỉ cần chạy qua `test` bằng cách ném Scramble cho 1 con bot giải Rubik nào đó trên mạng: ![Screenshot 2025-12-15 103431](https://hackmd.io/_uploads/ByfjfW6GWe.png) ![image](https://hackmd.io/_uploads/BJ42GWaz-x.png) Rồi giải `challenge` bằng cách sử dụng gọi `solution` là xong: ![image](https://hackmd.io/_uploads/BkJyQZpG-l.png) nhưng không :v ```cpp! if ( dword_55555555A164 > 998 ) { puts("\n[!] Too many moves! This puzzle is not that kind of ez=)))"); // Move phải dưới 999, Trong khi `moves_reversed` sẽ đưa ta hơn 1 nghìn moves :((( ``` Trong Python có 1 cái thư viện là `kociemba` giúp hỗ trợ giải nhanh rubik với số moves ít nhất có thể. > Chúng ta đã từng chơi Rubik cũng hiểu rằng bản chất có thể rút ngắn số moves được như vậy là do có những lệnh tráo vô nghĩa (trái rồi xoay phải ngược lại = ban đầu). 1836 lần Scramble random này hoàn toàn có thể rút gọn để ra được lời giải tối ưu!. Solution using `kociemba`: ```python! from pwn import * import kociemba import re HOST = '67.223.119.69' PORT = 5025 MOVES_MAP = ["U", "U'", "D", "D'", "L", "L'", "R", "R'", "F", "F'", "B", "B'"] inverse_map = { "U": "U'", "U'": "U", "D": "D'", "D'": "D", "L": "L'", "L'": "L", "R": "R'", "R'": "R", "F": "F'", "F'": "F", "B": "B'", "B'": "B" } # Cặp đối Moves # Đoạn test mình dùng Python lấy srand() của ctypes nhưng lạ là rand() bị lệch nên mình nhờ Gemini gen 1 cái rand() dường như chuẩn xác hơn class GlibcRand: def __init__(self): self.r = [0] * 344 self.idx = 0 def srand(self, seed): self.r[0] = seed for i in range(1, 31): self.r[i] = (16807 * self.r[i-1]) % 2147483647 if self.r[0] < 0: self.r[0] += 2147483647 for i in range(31, 34): self.r[i] = self.r[i-31] for i in range(34, 344): self.r[i] = (self.r[i-31] + self.r[i-3]) & 0xFFFFFFFF self.idx = 0 def rand(self): ptr_3 = (self.idx + 344 - 3) % 344 ptr_31 = (self.idx + 344 - 31) % 344 val = (self.r[ptr_3] + self.r[ptr_31]) & 0xFFFFFFFF self.r[self.idx] = val self.idx = (self.idx + 1) % 344 return val >> 1 # Mô phỏng rubik genbygipity :<< class ServerCube: def __init__(self): # 6 faces * 9 stickers. Order: U(0), D(1), L(2), R(3), F(4), B(5) # Colors: W, Y, O, R, G, B self.state = ['W']*9 + ['Y']*9 + ['O']*9 + ['R']*9 + ['G']*9 + ['B']*9 # Faces mapping self.U, self.D, self.L, self.R, self.F, self.B = 0, 1, 2, 3, 4, 5 def rotate_face_cw(self, face_idx): # Rotate 3x3 face clockwise b = self.state[face_idx*9 : (face_idx+1)*9] # 0 1 2 6 3 0 # 3 4 5 -> 7 4 1 # 6 7 8 8 5 2 new_b = [b[6], b[3], b[0], b[7], b[4], b[1], b[8], b[5], b[2]] self.state[face_idx*9 : (face_idx+1)*9] = new_b def rotate_face_ccw(self, face_idx): # Counter-clockwise (3 CW rotations) for _ in range(3): self.rotate_face_cw(face_idx) def apply_move(self, move): # Mapping logic verified against server ASM if move == 'U': self.rotate_face_cw(0) # Face U # Cycle rows: F(4) <- R(3) <- B(5) <- L(2) <- F(4) # Indices: 0,1,2 of each face self._cycle_rows(4, 3, 5, 2, [0,1,2]) elif move == "U'": self.rotate_face_ccw(0) self._cycle_rows(2, 5, 3, 4, [0,1,2]) # Reverse U elif move == 'D': self.rotate_face_cw(1) # Face D # Cycle rows: F(4) <- L(2) <- B(5) <- R(3) <- F(4) # Indices: 6,7,8 (Bottom row) self._cycle_rows(4, 2, 5, 3, [6,7,8]) elif move == "D'": self.rotate_face_ccw(1) self._cycle_rows(3, 5, 2, 4, [6,7,8]) elif move == 'L': self.rotate_face_cw(2) # Face L # Cycle cols: U(0) <- B(5) <- D(1) <- F(4) <- U(0) # Note: B orientation is tricky. Server logic: # U left col (0,3,6) gets B right col (8,5,2)? Or B left col? # Standard L: U(Left) <- B(RightInv) <- D(Left) <- F(Left) # Let's use generic simulation of standard move logic self._cycle_col_special_L(True) elif move == "L'": self.rotate_face_ccw(2) self._cycle_col_special_L(False) elif move == 'R': self.rotate_face_cw(3) # Face R self._cycle_col_special_R(True) elif move == "R'": self.rotate_face_ccw(3) self._cycle_col_special_R(False) elif move == 'F': self.rotate_face_cw(4) # Face F # Cycle: U(Bot) <- L(Right) <- D(Top) <- R(Left) self._cycle_ring_F(True) elif move == "F'": self.rotate_face_ccw(4) self._cycle_ring_F(False) elif move == 'B': self.rotate_face_cw(5) # Face B self._cycle_ring_B(True) elif move == "B'": self.rotate_face_ccw(5) self._cycle_ring_B(False) def _cycle_rows(self, f1, f2, f3, f4, idxs): # f1 gets f2, f2 gets f3, f3 gets f4, f4 gets f1 for i in idxs: temp = self.state[f1*9 + i] self.state[f1*9 + i] = self.state[f2*9 + i] self.state[f2*9 + i] = self.state[f3*9 + i] self.state[f3*9 + i] = self.state[f4*9 + i] self.state[f4*9 + i] = temp def _cycle_col_special_L(self, cw): # U(0,3,6), F(0,3,6), D(0,3,6), B(8,5,2) u = [0,3,6]; f = [0,3,6]; d = [0,3,6]; b = [8,5,2] if cw: # U <- B <- D <- F <- U self._cycle_4_arrays(0, 5, 1, 4, u, b, d, f) else: # U <- F <- D <- B <- U self._cycle_4_arrays(0, 4, 1, 5, u, f, d, b) def _cycle_col_special_R(self, cw): # U(2,5,8), B(6,3,0), D(2,5,8), F(2,5,8) u = [2,5,8]; b = [6,3,0]; d = [2,5,8]; f = [2,5,8] if cw: # U <- F <- D <- B <- U self._cycle_4_arrays(0, 4, 1, 5, u, f, d, b) else: self._cycle_4_arrays(0, 5, 1, 4, u, b, d, f) def _cycle_ring_F(self, cw): # U(6,7,8), R(0,3,6), D(2,1,0), L(8,5,2) u = [6,7,8]; r = [0,3,6]; d = [2,1,0]; l = [8,5,2] if cw: # U <- L <- D <- R <- U self._cycle_4_arrays(0, 2, 1, 3, u, l, d, r) else: self._cycle_4_arrays(0, 3, 1, 2, u, r, d, l) def _cycle_ring_B(self, cw): # U(2,1,0), L(0,3,6), D(6,7,8), R(8,5,2) u = [2,1,0]; l = [0,3,6]; d = [6,7,8]; r = [8,5,2] if cw: # U <- R <- D <- L <- U self._cycle_4_arrays(0, 3, 1, 2, u, r, d, l) else: self._cycle_4_arrays(0, 2, 1, 3, u, l, d, r) def _cycle_4_arrays(self, f1, f2, f3, f4, idx1, idx2, idx3, idx4): # f1[idx1] gets f2[idx2], etc... for k in range(3): t = self.state[f1*9 + idx1[k]] self.state[f1*9 + idx1[k]] = self.state[f2*9 + idx2[k]] self.state[f2*9 + idx2[k]] = self.state[f3*9 + idx3[k]] self.state[f3*9 + idx3[k]] = self.state[f4*9 + idx4[k]] self.state[f4*9 + idx4[k]] = t def to_kociemba(self): cmap = {'W':'U', 'R':'R', 'G':'F', 'Y':'D', 'O':'L', 'B':'B'} k_str = "" # Face U for i in range(9): k_str += cmap[self.state[0*9 + i]] # Face R for i in range(9): k_str += cmap[self.state[3*9 + i]] # Face F for i in range(9): k_str += cmap[self.state[4*9 + i]] # Face D for i in range(9): k_str += cmap[self.state[1*9 + i]] # Face L for i in range(9): k_str += cmap[self.state[2*9 + i]] # Face B for i in range(9): k_str += cmap[self.state[5*9 + i]] return k_str def main(): r = remote(HOST, PORT) print("Testing") r.recvuntil(b'>> ') r.sendline(b'test') r.recvuntil(b'Scramble (36 moves): ') scramble_line = r.recvline().strip().decode() r.recvuntil(b'>> ') moves = scramble_line.split() sol_moves = [inverse_map[m] for m in moves[::-1]] # ánh xạ đảo ngược các Moves nhận được r.sendline(" ".join(sol_moves).encode()) if "Nice!" in r.recvuntil(b'Nice!').decode(): print("Ngon! Test ok") r.recvuntil(b'REAL challenge!') r.recvuntil(b'>> ') r.sendline(b'help') res_help = r.recvuntil(b'>> ').decode() match = re.search(r'(0x[0-9a-fA-F]+)', res_help) if not match: return SEED = int(match.group(1), 16) print(hex(SEED)) print('Final Solve') r.sendline(b'challenge') # 1. GENERATE MOVES (GlibcRand) rng = GlibcRand() rng.srand(SEED) print("[*] Burning 36 moves...") for _ in range(36): rng.rand() print("[*] Generating 1836 moves...") challenge_moves = [] for _ in range(1836): val = rng.rand() challenge_moves.append(MOVES_MAP[val % 12]) # 2. APPLY TO CUSTOM CUBE (No Pycuber) print("[*] Applying moves to Simulator...") cube = ServerCube() for m in challenge_moves: cube.apply_move(m) # 3. SOLVE state_str = cube.to_kociemba() print(f"[*] Kociemba State: {state_str}") # Sanity check: Center of U face (index 4) MUST be U if state_str[4] != 'U': print("[!] WARNING: Center of U is NOT U. Logic error likely.") raw_sol = kociemba.solve(state_str) final_sol = [] for m in raw_sol.split(): if '2' in m: base = m.replace('2', '') final_sol.append(base) final_sol.append(base) else: final_sol.append(m) solution = " ".join(final_sol) print(f"Found: ({len(solution.split())} moves)") r.recvuntil(b'>> ') r.sendline(solution.encode()) print("Flag:") r.interactive() if __name__ == "__main__": main() ``` Flag: **KCSC{Does the "p" in puRE stand for puzzle or pwn? Congrats, no matter how u solve it. puREvenge in KCSC CTF won't be this easy}** # Mi đi um An ti để BUG: Nghe đề là thấy bài sẽ sử dụng nhiều yếu tố liên quan đến antidebugging, thử load vào IDA coi sao: ![image](https://hackmd.io/_uploads/r1PSFl3G-x.png) - Trước hết, ta cần biết rằng chương trình sẽ đăng ký 1 `Handler` handling exception nếu ở 1 đoạn nào đó được ném ra. `TLScallback` chính là nơi đăng ký Hander: ![image](https://hackmd.io/_uploads/ryE-LnvQ-x.png) - Ta thấy rằng bài sử dụng rất nhiều Window API, đồng thời bên trong hàm main ta gặp rải rác vô cùng nhiều các lệnh `CC - INT3` và `IsDebuggerPresent` với mục đích là ngắt luồng, hook vào `IsDebuggerPresent` ghi các byte của nó để thực hiện luồng ngầm mà các Debugger như IDA không thể trace được. Nói cách khác bài làm rối hoàn toàn hàm main và ngăn cản ta debug nó. `INT 3` sẽ gây nên Exception, mỗi khi chạy đến OPCODE này chương trình sẽ dừng và ném Exception cho `Handler` xử lý, cụ thể ta tìm được Handler trông như sau: ![image](https://hackmd.io/_uploads/rkh-UWhGZe.png) ![image](https://hackmd.io/_uploads/HJCM8WnzWl.png) Trong đó có: ```cpp! void sub_4812F0() { HANDLE CurrentProcess; // eax DWORD flOldProtect; // [esp+0h] [ebp-8h] BYREF if ( !dword_484058 ) { VirtualProtect(IsDebuggerPresent, 5u, 0x40u, &flOldProtect); IsDebuggerPresent = (BOOL (__stdcall *)())dword_484480; *((_BYTE *)&IsDebuggerPresent + 4) = byte_484484; VirtualProtect(IsDebuggerPresent, 5u, flOldProtect, &flOldProtect);// Thay 5 byte đầu tiên của IsDebuggerPresent thành 0x40 - (PAGE_EXECUTE_READWRITE CurrentProcess = GetCurrentProcess(); FlushInstructionCache(CurrentProcess, IsDebuggerPresent, 5u);// Xóa Cache CPU } } ``` ```cpp! BOOL __thiscall De_tem(_BYTE *dest) { HANDLE CurrentProcess; // eax DWORD flOldProtect; // [esp+8h] [ebp-10h] BYREF _BYTE v5[5]; // [esp+Ch] [ebp-Ch] v5[0] = 0xE9; // JMP rel32 OPCODE if ( dword_484058 ) { dword_484480 = (int)IsDebuggerPresent; byte_484484 = *((_BYTE *)&IsDebuggerPresent + 4);// Lưu 5 byte của IsDebuggerPresent để sau khôi phục dword_484058 = 0; } VirtualProtect(IsDebuggerPresent, 5u, 0x40u, &flOldProtect);// Writable cho 5 byte đầu này *(_DWORD *)&v5[1] = dest - (_BYTE *)IsDebuggerPresent - 5; IsDebuggerPresent = *(BOOL (__stdcall **)())v5;// Đè tem IsDebuggerPresent, lúc này call esi trong main ko gọi kiểm tra debug nữa // Thay vào đó, sẽ bị lùa thực hiện 1 luồng nào đó khác lấy từ Proc_chart *((_BYTE *)&IsDebuggerPresent + 4) = v5[4]; VirtualProtect(IsDebuggerPresent, 5u, flOldProtect, &flOldProtect);// Khôi phục quyền, xóa Cache... CurrentProcess = GetCurrentProcess(); return FlushInstructionCache(CurrentProcess, IsDebuggerPresent, 5u); } ``` ## Hook Win API: Đoạn này mìn nói một chút về hàm `De_tem`: - Đầu tiên ta cần hiểu về `E9 - JMP rel32` hay "Jump tương đối 32bit", nó sẽ nhảy đến địa chỉ gần nó theo công thức: $$ DEST(EIPsauJUMP) + Offset $$ $$ Offset = Dest - (NextIntruction) $$ - Trong bài, ta thấy `v5` sẽ chứa 5 byte đè lên API `IsDebuggerPresent`, với byte đầu tiên là lệnh `JMP` này. - 4 byte còn lại được tính toán là: `*(_DWORD *)&v5[1] = dest - (_BYTE *)IsDebuggerPresent - 5;` hay: $$ Offset = Dest - (IsDebuggerPresent + 5) $$ > NextInstruction phải là DebuggerAPI + 5 do khi EIP mặc định đọc JMP rel32 0xE9 sẽ đọc thêm 4 byte nữa. - Vậy De_tem sẽ thay thế hoàn toàn việc check Debugger thành các HÀM/API được để ở Proc_chart. ## Tiếp: Okay, hiệu được các chương trình dấu luồng rồi thì ta thử mở Proc_chart ra xem nó đang lưu cái gì nhé: ``` .rdata:004831B4 Proc_chart dd offset GetStdHandle ; DATA XREF: Handler+24↑r .rdata:004831B4 ; sub_4813F0+24↑r ... .rdata:004831B8 dd offset GetStdHandle .rdata:004831BC dd offset WriteConsoleA .rdata:004831C0 dd offset WriteConsoleA .rdata:004831C4 dd offset ReadConsoleA .rdata:004831C8 dd offset WriteConsoleA .rdata:004831CC dd offset InitRC4 .rdata:004831D0 dd offset RC4_ENC .rdata:004831D4 dd offset XTEA_ENC .rdata:004831D8 dd offset InitRC4 .rdata:004831DC dd offset RC4_ENC .rdata:004831E0 dd offset WriteConsoleA .rdata:004831E4 dd offset ExitProcess .rdata:004831E8 dd offset WriteConsol // Mình đã đổi tên API/Hàm cho dễ nhìn rùi. ``` Đã hiểu, vậy đề bài sẽ lần lượt thực hiện toàn bộ Proc_chart trên ngầm trong hàm main bằng cách: ``` -> Đi đến INT 3 -> Exception -> Handler handling Exception: Sửa 5bytes đầu IsDebuggerPresent -> Gọi check Debugger -> Chạy đến luồng khác (do API này bị biến đổi) -> Khôi phục IsDebuggerPresent (trả 5 byte đầu bị sửa thành JUMP -> Successfully Hiding Flow...! ``` và làm như sau: ![image](https://hackmd.io/_uploads/ByLVuW3Gbl.png) ![image](https://hackmd.io/_uploads/SJdI_Z2MZg.png) ![image](https://hackmd.io/_uploads/H1swOb3Gbg.png) Vậy, Bản chất của `main` là đẩy các tham số lên stack cho API\Hàm trong Proc_chart chạy, và nõ chạy lần lượt từ trên xuống. Luồng gốc của đề bài thật ra chỉ như này: ``` Nhập Input -> Chương trình lấy Input -> Mã hóa RC4 liked -> Mã hóa XTEA liked -> Mã hóa RC4 liked thêm lần nữa -> So sánh với Target ``` Và chỉ có thể thôi, ta dump các giá trị như `Keys, Target` rồi tuân theo đúng logic mã hóa truy ngược Flag từ `Target là xong bài`: ```python! import struct from binascii import unhexlify, hexlify TARGET_CIPHER = unhexlify("7B6FB4F7813DEFEEB21E7E8D0C9D6741CB2053FD4771ADC6BC0EF2A26904A3B25ED7D06E636F729E") RC4_KEY_OLD = unhexlify("1A891492225D4F") # Key1 RC4_KEY_NEW = unhexlify("36BDD13B9AA8EF29FF") # Key2 XTEA_KEY_WORDS = [0xDEADBEEF, 0xCAFEBABE, 0x13371337, 0xBAADF00D] MASK = 0xFFFFFFFF def rc4_init(key: bytes) -> bytearray: state = bytearray(264) # S-box (256) + i (4) + j (4) key_len = len(key) j_val = 0 # 1. Khởi tạo S-box: S[i] = i for i in range(256): state[i] = i # 2. Permutation for i in range(256): v6 = state[i] # j = (j + Key[i % key_len] + S[i]) & 0xFF j_val = (j_val + key[i % key_len] + v6) & 0xFF # swap S[i], S[j] tmp = state[j_val] state[i] = tmp state[j_val] = v6 state[256:264] = b'\x00' * 8 return state def xtea_decrypt_block(v0, v1, key): delta = 0x61C88647 mask = 0xFFFFFFFF sum_ = (-delta * 32) & mask for _ in range(32): v1 = (v1 - ((sum_ + v0) ^ (key[2] + ((v0 << 4) & mask)) ^ (key[3] + (v0 >> 5)))) & mask v0 = (v0 - ((sum_ + v1) ^ (key[0] + ((v1 << 4) & mask)) ^ (key[1] + (v1 >> 5)))) & mask sum_ = (sum_ + delta) & mask return v0, v1 def rc4_decrypt(state: bytearray, buf: bytearray, offset: int, length: int): if length <= 0: return i = int.from_bytes(state[256:260], "little") j = int.from_bytes(state[260:264], "little") for n in range(length): i = (i + 1) & 0xFF j = (j + state[i]) & 0xFF tmp = state[i] state[i] = state[j] state[j] = tmp k = state[(state[i] + state[j]) & 0xFF] buf[offset + n] ^= k state[256:260] = i.to_bytes(4, "little") state[260:264] = j.to_bytes(4, "little") def xtea_decrypt(data: bytes, key): out = bytearray() for i in range(0, len(data), 8): v0 = int.from_bytes(data[i:i+4], "little") v1 = int.from_bytes(data[i+4:i+8], "little") v0, v1 = xtea_decrypt_block(v0, v1, key) out += v0.to_bytes(4, "little") out += v1.to_bytes(4, "little") pad = out[-1] return bytes(out[:-pad]) def main(): rc4_state_new = rc4_init(RC4_KEY_NEW) ciphertext_buffer = bytearray(TARGET_CIPHER) # REV RC4-2 rc4_decrypt(rc4_state_new, ciphertext_buffer, 0, len(ciphertext_buffer)) XTEA_INPUT_CIPHER = bytes(ciphertext_buffer) # XTEA RC4_INPUT_TEMP = xtea_decrypt(XTEA_INPUT_CIPHER, XTEA_KEY_WORDS) # REV RC4-1 rc4_state_old = rc4_init(RC4_KEY_OLD) flag_buffer = bytearray(RC4_INPUT_TEMP) rc4_decrypt(rc4_state_old, flag_buffer, 0, len(flag_buffer)) FINAL_FLAG = bytes(flag_buffer) try: print(f"FLAG: {FINAL_FLAG.decode('latin-1')}") except Exception: print(f"FLAG HEX{hexlify(FINAL_FLAG).decode()}") if __name__ == "__main__": main() ``` Flag: **KCSC{now_you_know_how_to_hook_WINAPI}** ## Cách giải thứ 2: Tracing trực tiếp để luận logic: Cách trên mình nhìn vào `Proc_chart` và code tại hàm `main` để đoán luồng chương trình. Khi đã hiểu rõ cách Handler bài này hoạt động, ta có thể tracing trực tiếp để luận luồng. Nói sơ qua lại 1 chút hàm `Handler` custom được sử dụng, nó ghi đè byte của WindowAPI `IsDebuggerPresent` để khi API này được gọi bản chất luồng thật sẽ được chạy: ```cpp LONG __stdcall Handler(struct _EXCEPTION_POINTERS *ExceptionInfo) { PCONTEXT ContextRecord; // esi PVOID v2; // eax if ( ExceptionInfo->ExceptionRecord->ExceptionCode != 0x80000003 ) return 0; ContextRecord = ExceptionInfo->ContextRecord; Remove_old_Isdebuger_hook(); if ( dword_7F43E4 < 15 ) { De_tem(Proc_chart[dword_7F43E4]); ++dword_7F43E4; } if ( Handle ) RemoveVectoredExceptionHandler(Handle); v2 = AddVectoredExceptionHandler(1u, sub_7F13F0);// Đăng ký Handler mới, đảm bảo nó xuất hiện đầu tiên để xử lý Exception mới được ném ra, các địa chi Handler mới sẽ khác với mỗi lần đăng ký ++ContextRecord->Eip; // ContextRecord chính là trạng thái của CPU khi dừng lại tại lệnh INT3, tăng nó lên 1 sau khi thao túng API để tiếp tục tới lệnh tiếp theo Handle = v2; return -1; } ``` - Ta thấy rằng, chương trình đăng ký 1 Handler mới sau khi Handler hiện tại xử lý xong Exception ném bởi INT3 và địa chỉ của Handler mới này sẽ khác nhau với mỗi lần đăng ký. Nếu đã đọc hiểu code của `TLScallback` thì đoạn đăng ký Handler mới gần như giống hệt: >Đây là code bên TLScallback: ![image](https://hackmd.io/_uploads/B10ULhvQbg.png) >Đây là code bên Handler: ![image](https://hackmd.io/_uploads/ryyu8hvX-g.png) - Vậy để trace thì đơn giản ta sẽ đặt breakpoint tại mỗi vị trí bắt đầu của các Handler rồi soi giá trị luồng được lấy từ Proc_chart là gì hoặc Stepin vào `call IsDebuggerPresent`(cụ thể là dòng này): ![image](https://hackmd.io/_uploads/HkZBMTLm-x.png) ![image](https://hackmd.io/_uploads/ryd9z6L7be.png) >Ví dụ: Ở ảnh dưới là mình cho StepIn call IsDebuggerPresent hiện tại là GetStdHanle hay chính là lần call API đầu tiên, ta có thể từ đó đoán được luồng tiếp theo... - Cứ làm tương tự như vậy - F7 soi API rồi dần dần ta sẽ nhìn thấy flow của chương trình giống hệt cách mình đã phân tích tĩnh ở trên. ### Phân tích: Mình sẽ đặt breakpoint ở mỗi lần gọi `IBP - IsDebuggerPresent` rồi Stepin cho trực quan. Lần gọi `IBP` đầu tiên, ta thấy rằng IBP đã bị patch thành: ![image](https://hackmd.io/_uploads/H1KY8l3QWx.png) - Tham số cho hàm này chỉ có 1 và được đẩy lên stack là `-11 - STD_OUTPUT_HANDLE`: ![image](https://hackmd.io/_uploads/SyatwgnXbe.png) `IBP` sau đó tiếp tục được patch thành `GetStvHandle`: ![image](https://hackmd.io/_uploads/H1FQ_e3Qbl.png) - Tham số được truyền là `-10 - STD_INPUT_HANDLE`: ![image](https://hackmd.io/_uploads/S1BD_ln7-g.png) Tiếp tục, `IBP` lần này được patch thành `WriteConsoleA`, 1 Windows API đẩy text ra Output (Hiện đăng ký là Màn hình): ![image](https://hackmd.io/_uploads/Hysa_lhmZl.png) - Tham số được push lên stack lần lượt là: ![image](https://hackmd.io/_uploads/H1AeKl2X-e.png) - Ta có thể đọc nhanh cách sử dụng WriteConsoleA [tại đây](https://learn.microsoft.com/en-us/windows/console/writeconsole) `IBP` sau đó tiếp tục bị patch thành `WriteConsoleA`, đoạn này là để in `Enter:`: ![image](https://hackmd.io/_uploads/SJOZqx2mWe.png) Lần tiếp theo thì `IBP` sẽ bị ghi đè bởi `ReadConsoleA`: ![image](https://hackmd.io/_uploads/r1VhNznQ-e.png) - Đây là tham số được đưa vào API này, ta có thể đọc về API này [tại đây](https://learn.microsoft.com/en-us/windows/console/readconsole): ![Screenshot 2025-12-26 210050](https://hackmd.io/_uploads/Hy7vrGhXWx.png) - Sau khi chạy xong thì Flag được lưu ở `FlagBuf` và Lenght được lưu tại `ptrFlaglen` chuẩn như đã dự đoán: ![image](https://hackmd.io/_uploads/HJv1_fhmbe.png) ![image](https://hackmd.io/_uploads/HyhCvz37Zx.png) Sau khi đọc xong thì `FlagLenght` được lưu vào eax và đề bài bắt buộc nó phải dài khoảng `64byte`, mình đặt RIP sang luồng bên trái do đang trace để hiểu code, đến đoạn sau ta thấy nó có cấu trúc tương tự như khi đẩy `KCSC...` HAY `Enter...` với input là `STD_OUTPUT_HANDLE` nên ta đoán ngay lần tới cũng chỉ in text lên màn hình ![image](https://hackmd.io/_uploads/HJF3HMnQbx.png) Trace tiếp thì các đoạn call API trá hình sau nằm đúng như những gì mình đoán khi nó sẽ mã hóa Input của ta theo những gì đã phân tích khi lần lượt gọi các hàm như `RC4 -> XTEA -> RC4` để mã hóa Input của ta. Đây chính là cách để xác minh những gì mình đoán ở cách 1. Đây là cũng là một cách luận luồng chương trình thay vì đoán thứ tự dựa vào `Proc_chart` để có thể hiểu chính xác hơn luồng của code giả sử nếu bài khó hơn khi mà thứ tự `Proc_chart` bị shuffle... # Just_EzReversing (easy): IDA GO GO... ```cpp! __int64 __fastcall sub_140009BD0(char *a1, const char *a2) { int v2; // r8d int v3; // r9d size_t v4; // rax int v5; // edx int v6; // r8d int v7; // r9d size_t v8; // rbx const char *b32_input; // rsi size_t v10; // rdx int v11; // eax int v12; // edx int v13; // r8d int v14; // r9d sub_1400026C7(); sub_140009B80((_DWORD)a1, (_DWORD)a2, (unsigned int)aEnterTheFlag, (unsigned int)"%s", v2, v3);// scanning __acrt_iob_func((unsigned int)a1); fgets(a1, (int)a2, (FILE *)0x64); input[strcspn(a1, a2)] = 0; v4 = strlen(a1); if ( (double)(int)v4 == 36.0 ) // lenght = 36 { v8 = 0LL; b32_input = (const char *)base32_enc(a1, a2, v4, input, 1LL, (double)(int)v4); while ( v8 < strlen(Buf1) ) { v10 = (size_t)&b32_input[v8]; // v10 lần lượt sẽ là: b32_input[v8:v8+2]... v8 += 2LL; strncpy(Buf1, b32_input, v10); MD5(Buf1, b32_input, Buf1, &unk_140010324); v11 = dword_1400100A0; *(__m128i *)((char *)&store + dword_1400100A0) = _mm_load_si128((const __m128i *)Buf1); dword_1400100A0 = v11 + 16; } //Lần lượt MD các Block b32_inut if ( !memcmp(Buf1, b32_input, (size_t)&Size) ) { sub_140009B80((unsigned int)Buf1, (_DWORD)b32_input, v12, (unsigned int)"Correct!\n", v13, v14); return 1LL; } else { sub_140009B80((unsigned int)Buf1, (_DWORD)b32_input, v12, (unsigned int)"Incorrect!\n", v13, v14); return 0LL; } } ``` Tại sao mình biết được đề dùng 2 hàm (Base32_enc và MD5), thử debug động ta thấy sau khi chạy b32_enc, b32_input có dạng: ![image](https://hackmd.io/_uploads/B1tZ8W6fWx.png) - Sau khi chạy `MD5 thì kết quả được lưu ở Buf1 là MD5 2 ký tự đầu` rồi đẩy vào `store`: ![image](https://hackmd.io/_uploads/SkjCL-TzWg.png) ![image](https://hackmd.io/_uploads/HyGSw-TG-g.png) - Lần lặp thứ 2, ta thu được lần lượt là b32 của 2 ký tự sau đó và MD5: ![image](https://hackmd.io/_uploads/H18PDbTGZx.png) Bài này khá đơn giản, ta chỉ cần tìm Input sao cho: `MD5( b32_input[2*i : 2*i+2] ) == Size[16*i : 16*i+16]` là ok!! Ta chỉ cần dump cái mảng `Size` ra là được: ```python! import hashlib import itertools import base64 BASE32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" SIZE = bytes.fromhex( "D9 B2 52 86 6A 25 98 6F D8 EC 8A 6C 9B DE D2 75 " "C7 9B DA 0E 66 F0 7B EE 66 2A 68 40 B9 07 BF 39 " "8D 36 B3 61 A4 79 44 22 C7 06 81 58 6A 95 73 50 " "18 2B E0 C5 CD CD 50 72 BB 18 64 CD EE 4D 3D 6E " "AC CB 66 F0 EC D8 26 AA C8 90 65 99 0E 1D A9 7F " "56 9B 3C ED 35 55 BF AE 63 CA DB 65 58 A6 FC BE " "33 D2 A0 8E EE 4C 17 81 19 40 FE FB C4 E0 97 78 " "6B A8 9A B4 18 23 AF 9B 65 EA 5D 23 30 31 B9 F8 " "E4 A5 9D F8 B9 72 06 10 9E B4 B7 F2 FE 52 8A 4D " "AD 44 AC 8C 02 28 5F 6B 91 64 A0 84 92 A7 8F 92 " "0A 5B 65 72 5F 6A 3B A9 58 21 CE C6 1D BB 46 D3 " "2F F2 D4 5E 98 3C 58 E9 6C 9B A3 3D 69 72 1C E8 " "51 F5 81 93 77 65 89 0F 2A 70 6C 77 EA 8A F3 CC " "A3 6C 71 EB 23 9A 02 31 BA 0C 6E 7A 45 90 F0 62 " "42 AD C7 77 F3 C2 C9 70 05 15 0C B7 B8 61 65 B6 " "C2 3F A9 99 69 25 B6 10 71 0D 93 E2 8C 59 A3 E2 " "D5 C4 42 58 D5 16 59 F9 62 79 C4 70 CE 81 85 DC " "6E 02 9F BF A8 17 DF C4 58 59 8A 5D 38 A6 75 5F " "7C 78 EB B4 C2 23 C9 6F 8E 6C CC 29 F7 3C C2 8E " "76 AA 96 36 9A BB BA 52 E6 21 BF A8 3D A8 E6 4F " "BA 7D 08 34 D2 3B 91 50 4B ED 68 59 01 DC 3A C5 " "38 F9 9A BB C1 D3 39 C2 77 C0 66 9E 7B C3 73 C0 " "0A 5B 65 72 5F 6A 3B A9 58 21 CE C6 1D BB 46 D3 " "85 3B C5 5C 10 41 F2 4B 93 92 06 93 31 E6 53 36 " "F2 14 A7 D4 2E 0D E5 87 5D 55 18 9E 01 E2 E1 87 " "7B 7C D2 4E A6 F0 8B 71 1C F4 05 3B EA C4 3C C5 " "64 F3 BD 17 41 AB 8D 6B A5 45 A1 AE 09 BB 87 28 " "8A 6B 17 8D 3A F0 A5 A9 B2 74 4C A3 19 21 D5 E2 " "BF D2 34 39 10 37 8A 1B 53 7C 3C 80 36 A5 C0 7E " "20 63 84 97 17 D3 2D D1 9E 53 4B 77 CA BA C5 17 " "20 63 84 97 17 D3 2D D1 9E 53 4B 77 CA BA C5 17" ) def solve(): blocks = [SIZE[i:i+16] for i in range(0, len(SIZE), 16)] result = "" for idx, target in enumerate(blocks): found = False for a, b in itertools.product(BASE32, repeat=2): h = hashlib.md5((a+b).encode()).digest() if h == target: result += a + b print(f"Block {idx}: {a+b}") found = True break if not found: raise RuntimeError("het") return result b32 = solve() raw = base64.b32decode(b32) print(raw) ``` Ta được 1 chuỗi Base32 như này: `JNBVGQ33GN5F6YTSOU3TGX3NMQ2V6NDOMRPWIM3DN5SDGX3CGRZTGMZSPU`. Mang đi decode để được Flag: Flag: **KCSC{3z_bru73_md5_4nd_d3cod3_b4s332}** # The reward lies hidden (easy): Nếu để nhận xét thì chắc câu này khoai nhất trong các câu EZ :v Nạp file vào IDA, tìm nhanh main: ```cpp! int __cdecl main(int argc, const char **argv, const char **envp) { __int64 v3; // rbx unsigned __int64 v4; // r14 __int128 v5; // rdi size_t v6; // rax void *v7; // rax _QWORD *v8; // r14 size_t v9; // rcx void *v10; // rcx void **v11; // rdx __int64 v12; // rax __int64 v13; // rdx unsigned __int64 v14; // r8 void **v15; // r9 void **v16; // rax __int64 v17; // rcx void **v18; // rax __int64 v19; // rcx void **v20; // rax __int64 v21; // rcx void **v22; // rax __int64 v23; // rcx void **v24; // rax __int64 v25; // rcx void **v26; // rax __int64 v27; // rcx void **v28; // rax __int64 v29; // rcx void **v30; // rax __int64 i; // rsi __int64 v32; // rax __int64 v33; // rax __int64 j; // rdx __int64 still_the_result; // rax unsigned __int64 v36; // rcx char *v37; // rdi void *v38; // rcx __int128 v40; // [rsp+20h] [rbp-E0h] BYREF __m128i v41; // [rsp+30h] [rbp-D0h] __int64 result[24]; // [rsp+50h] [rbp-B0h] void *Block[2]; // [rsp+110h] [rbp+10h] BYREF __m128i v44; // [rsp+120h] [rbp+20h] v3 = 0i64; Src[0] ^= 0x12u; byte_7FF6D291D0B5 ^= 0x3Du; byte_7FF6D291D0B6 ^= 0x46u; byte_7FF6D291D0B7 ^= 0xA1u; byte_7FF6D291D0B8 ^= 0x42u; byte_7FF6D291D0B9 ^= 0xDFu; // sau khi XOR được lần lượt Flag: *(_OWORD *)Block = 0i64; v44.m128i_i64[0] = 0i64; v4 = 15i64; v44.m128i_i64[1] = 15i64; LOBYTE(Block[0]) = 0; v40 = 0i64; *(_QWORD *)&v5 = -1i64; do *(_QWORD *)&v5 = v5 + 1; while ( Src[v5] ); *((_QWORD *)&v5 + 1) = 0x7FFFFFFFFFFFFFFFi64; if ( (unsigned __int64)v5 > 0x7FFFFFFFFFFFFFFFi64 ) ((void (__fastcall __noreturn *)(int, const char **, const char **))sub_7FF6D2911220)(argc, argv, envp); if ( (unsigned __int64)v5 <= 0xF ) { v41.m128i_i64[0] = v5; v41.m128i_i64[1] = 15i64; memcpy(&v40, Src, v5); *((_BYTE *)&v40 + v5) = 0; goto LABEL_19; } if ( ((unsigned __int64)v5 | 0xF) > 0x7FFFFFFFFFFFFFFFi64 ) { v6 = 0x8000000000000027ui64; LABEL_8: v7 = operator new(v6); if ( !v7 ) goto LABEL_22; v8 = (_QWORD *)(((unsigned __int64)v7 + 39) & 0xFFFFFFFFFFFFFFE0ui64); *(v8 - 1) = v7; goto LABEL_18; } *((_QWORD *)&v5 + 1) = v5 | 0xF; if ( ((unsigned __int64)v5 | 0xF) < 0x16 ) *((_QWORD *)&v5 + 1) = 22i64; v9 = *((_QWORD *)&v5 + 1) + 1i64; if ( *((_QWORD *)&v5 + 1) == -1i64 ) { v8 = 0i64; } else { if ( v9 >= 0x1000 ) { v6 = *((_QWORD *)&v5 + 1) + 40i64; if ( *((_QWORD *)&v5 + 1) + 40i64 <= (unsigned __int64)(*((_QWORD *)&v5 + 1) + 1i64) ) sub_7FF6D2911180(v9, argv, envp); goto LABEL_8; } v8 = operator new(v9); } LABEL_18: *(_QWORD *)&v40 = v8; v41 = (__m128i)v5; memcpy(v8, Src, v5); *((_BYTE *)v8 + v5) = 0; v4 = v44.m128i_u64[1]; LABEL_19: if ( v4 > 0xF ) { v10 = Block[0]; if ( v4 + 1 >= 0x1000 ) { v10 = (void *)*((_QWORD *)Block[0] - 1); if ( (unsigned __int64)(Block[0] - v10 - 8) > 0x1F ) LABEL_22: invalid_parameter_noinfo_noreturn(); } j_j_free(v10); } *(_OWORD *)Block = v40; v44 = v41; v11 = Block; if ( _mm_srli_si128(v41, 8).m128i_u64[0] > 0xF ) v11 = (void **)v40; v12 = sub_7FF6D2912880(std::cout, (__int64)v11, v41.m128i_i64[0]); std::ostream::operator<<(v12, sub_7FF6D2912260); getLine(std::cin, Block); // Block chứa String input if ( (v44.m128i_i64[0] ^ 0x7A) == 194 ) // inputlenght check { // Đóng gói theo dạng: // [C6,C3,C1,C0,C4,C2,C7] v13 = 0i64; v14 = v44.m128i_u64[1]; v15 = (void **)Block[0]; do { v16 = Block; if ( v14 > 0xF ) v16 = v15; v17 = SBYTE6(v16[v13]); v18 = Block; if ( v14 > 0xF ) v18 = v15; v19 = ((__int64)SBYTE3(v18[v13]) << 8) | v17; v20 = Block; if ( v14 > 0xF ) v20 = v15; v21 = ((__int64)SBYTE1(v20[v13]) << 16) | v19; v22 = Block; if ( v14 > 0xF ) v22 = v15; v23 = ((__int64)SLOBYTE(v22[v13]) << 24) | v21; v24 = Block; if ( v14 > 0xF ) v24 = v15; v25 = ((__int64)SBYTE4(v24[v13]) << 32) | v23; v26 = Block; if ( v14 > 0xF ) v26 = v15; v27 = ((__int64)SBYTE5(v26[v13]) << 40) | v25; v28 = Block; if ( v14 > 0xF ) v28 = v15; v29 = ((__int64)SBYTE2(v28[v13]) << 48) | v27; v30 = Block; if ( v14 > 0xF ) v30 = v15; result[v13] = v29 | ((__int64)SHIBYTE(v30[v13]) << 56); ++v13; } while ( v13 < 23 ); for ( i = 0i64; i < 23; ++i ) { v32 = int64_mul(*(_QWORD *)&A[i * 8 + 64], *(_QWORD *)((char *)&B + i * 8)); v33 = int64_mul(v32, *(_QWORD *)&A[i * 8 + 512]); result[i] = int64_mul(v33, result[i]); } for ( j = 0i64; j < 184; j += 8i64 ) { still_the_result = result[(unsigned __int64)j / 8]; v36 = (((((((still_the_result - *(_QWORD *)&A[j + 256]) | ((unsigned __int64)(still_the_result - *(_QWORD *)&A[j + 256]) >> 32)) >> 16) | (still_the_result - *(_QWORD *)&A[j + 256]) | ((unsigned __int64)(still_the_result - *(_QWORD *)&A[j + 256]) >> 32)) >> 8) | (((still_the_result - *(_QWORD *)&A[j + 256]) | ((unsigned __int64)(still_the_result - *(_QWORD *)&A[j + 256]) >> 32)) >> 16) | (still_the_result - *(_QWORD *)&A[j + 256]) | ((unsigned __int64)(still_the_result - *(_QWORD *)&A[j + 256]) >> 32)) >> 4) | (((((still_the_result - *(_QWORD *)&A[j + 256]) | ((unsigned __int64)(still_the_result - *(_QWORD *)&A[j + 256]) >> 32)) >> 16) | (still_the_result - *(_QWORD *)&A[j + 256]) | ((unsigned __int64)(still_the_result - *(_QWORD *)&A[j + 256]) >> 32)) >> 8) | (((still_the_result - *(_QWORD *)&A[j + 256]) | ((unsigned __int64)(still_the_result - *(_QWORD *)&A[j + 256]) >> 32)) >> 16) | (still_the_result - *(_QWORD *)&A[j + 256]) | ((unsigned __int64)(still_the_result - *(_QWORD *)&A[j + 256]) >> 32); if ( (((unsigned int)(v36 >> 2) | (unsigned int)v36 | (unsigned int)(((v36 >> 2) | v36) >> 1)) & 1) != 0 )// LSB V36 == 0 OR 1 (kết quả đoạn so sánh trên) { sub_7FF6D2911020("\n"); do { sub_7FF6D29126B0((__int64 *)std::cout, A[v3 + 488] ^ A[v3 + 40]); ++v3; } while ( v3 < 14 ); goto LABEL_59; } } sub_7FF6D2911020("\n"); v37 = (char *)&unk_7FF6D29146F0; do { sub_7FF6D2911020("%c"); if ( (unsigned int)v3 % 0x61 == 96 ) sub_7FF6D2911020("\n"); LODWORD(v3) = v3 + 1; v37 += 4; } while ( (int)v3 < 6402 ); } else { do { sub_7FF6D29126B0((__int64 *)std::cout, A[v3 + 440] ^ A[v3]); ++v3; } while ( v3 < 34 ); } LABEL_59: if ( v44.m128i_i64[1] > 0xFui64 ) { v38 = Block[0]; if ( (unsigned __int64)(v44.m128i_i64[1] + 1) >= 0x1000 ) { v38 = (void *)*((_QWORD *)Block[0] - 1); if ( (unsigned __int64)(Block[0] - v38 - 8) > 0x1F ) invalid_parameter_noinfo_noreturn(); } j_j_free(v38); } return 0; } ``` Trông khá rối nhỉ, nhưng bản chất, main thực hiện đúng 3 thứ duy nhất: - Đầu tiên, nó nhận Input bắt buộc phải dài 184 byte. ```cpp! getLine(std::cin, Block); // Block chứa String input if ( (v44.m128i_i64[0] ^ 0x7A) == 194 ) // inputlenght check { // Đóng gói theo dạng: // [C6,C3,C1,C0,C4,C2,C7] v13 = 0i64; v14 = v44.m128i_u64[1]; // v14 cầm độ lớn Block (>15 thì lưu trên Heap) v15 = (void **)Block[0]; // Block ptr do { v16 = Block; if ( v14 > 0xF ) v16 = v15; v17 = SBYTE6(v16[v13]); // Lấy C6 v18 = Block; if ( v14 > 0xF ) v18 = v15; v19 = ((__int64)SBYTE3(v18[v13]) << 8) | v17;// Lấy C3, ghép sau C6 v20 = Block; if ( v14 > 0xF ) v20 = v15; v21 = ((__int64)SBYTE1(v20[v13]) << 16) | v19;// ... Logic tương tự v22 = Block; if ( v14 > 0xF ) v22 = v15; v23 = ((__int64)SLOBYTE(v22[v13]) << 24) | v21; v24 = Block; if ( v14 > 0xF ) v24 = v15; v25 = ((__int64)SBYTE4(v24[v13]) << 32) | v23; v26 = Block; if ( v14 > 0xF ) v26 = v15; v27 = ((__int64)SBYTE5(v26[v13]) << 40) | v25; v28 = Block; if ( v14 > 0xF ) v28 = v15; v29 = ((__int64)SBYTE2(v28[v13]) << 48) | v27; v30 = Block; if ( v14 > 0xF ) v30 = v15; result[v13] = v29 | ((__int64)SHIBYTE(v30[v13]) << 56);// result[i] = _int64()[C6,C3,...C7] <- C7 đưa vào cuối ++v13; } ``` - Tiếp theo, với mỗi 8 byte của Input, nó thực hiện `Shuffle Packin`, chuyển thành 1 cục QWORD như này: `[C6,C3,C1,C0,C4,C2,C7] = result[]` rồi lưu nó thành 1 array kiểu: `Output[23Qword] = [result[0],result[1]] . - Debugging động, sau lần lặp đầu tiên, result trông có dạng: ![image](https://hackmd.io/_uploads/ryDRtZ6MZe.png) - Input mình nhập là `KCSC{aaaaaa...a}`, có thể thấy việc đóng gói mà mình phát hiện ra là đúng. - Cuối cùng, nó cầm cái `Output` này đi nhân với `A*B*C` theo `imul_int64`, rồi đem so sánh nó với `Target`. Oh wow, vậy chẳng phải ta chỉ cần thực hiện chia ngược lại `Target` cho `A*B*C` là ra Shuffle Input rồi và chỉ cân đảo byte là ra được Flag hay sao :v? Hiểu được điều này, giờ mình chỉ cần: - Dump A và B từ IDA (2 Blob này chứa các Số hạng lẫn `Target` lun, tùy cách dùng mà lấy các Block khác nhau): ```python ConstA = [ 0x21F63DF6EA176765, 0x0AB1FD5C1E0A9765, 0x40382E78D2BF618F, 0x1883A77DECE9FAF7, 0x7EF4324F07E21EB5, 0x8C27B1CABDC5778F, 0x4AB7BC73427DF467, 0x0FEE7AA89FBDCF35F, 0x0FBFB328BB71B46B1, 0x3A54E11FDDC74D27, 0x0AABADEE9278DCE1D, 0x64A10164A8BDBE83, 0x0D64ADFD1EF8882C1, 0x0A3CDC5274D7B6CD5, 0x05388BCE06632DA1, 0x961201B184768A51, 0x0F318EB353D285DF7, 0x0F62D256FF95FAD05, 0x0C8D42BE55BD754DB, 0x0D1C86DAB8378475F, 0x8537555ABD8B7219, 0x0EC927CC094A2DAFD, 0x0D278D636CCBE1A01 ] ConstB = [ 0xB695C2BD10A19523, 0x3B858B3E8671B94D, 0x9D99CDB818E8BF0F, 0xEC2AFD8FC9140303, 0xC3E16C0BA35B97B9, 0x5085BA414E33D2DD, 0xF001B18435C1CC93, 0xBD9A607B178C4267, 0xF848AEC0E54EA671, 0xEC40D1CE64B42A89, 0x7A69DCA7959C9975, 0xE7015D92ECA6C995, 0x9C70C9956AD4CC6D, 0x4B95EE2F5CB11853, 0x2A1FD7BE9190BD25, 0xF425838CD21568FB, 0x6319F7F42D581163, 0xC9DA1F9E8F790A0F, 0x1076477A4D86F435, 0xFD7C305A832AD541, 0x2D2ED34F5A8ED569, 0x820C45DC6BD2EB43, 0x0A91349CBCB508CC5 ] ConstC = [ 0x0BC7C3C0B0BE5E531, 0x111EC8CE746A6243, 0x499851BCDA2073BD, 0x36E0DD8C4C897C55, 0x1DEA534DA4FCFEB7, 0x7B57E4E5165E26B9, 0x4A1C2B6AC91891A7, 0x44EEC6699738FA8D, 0x0AD4DFE6B00A02157, 0x2EB554CA9635FAFB, 0x623F7B0197F79C13, 0x0A3415911EFF3BED9, 0x4B4FD9B5E21BCFF3, 0x6F78C98DC732D231, 0x2AEC3662719E09CB, 0x915462C402BE8401, 0x0FA2BC3A4BBFA039, 0x669B5347E3C2CAE5, 0x13ED2473AC48603D, 0x0F5DDA0418D09D53B, 0x2548ACAB280A4AE3, 0x858E612083B3E24B, 0x1FDF3D9116AC2313 ] Target = [ 0x47EE0B7C8B7D3CBB, 0x0E6287DF7349CED, 0x777BEDA8C9D49B43, 0x56F80210D3708C41, 0x0CA97B6C052AEF171, 0x78B8C3301E2A2DF8, 0x0B3B52628FA5B7DFD, 0x0A8CBBC861ECE9708, 0x8674B6F2DE5669EC, 0x20B5D04D1270C20E, 0x201451D60FC4D54D, 0x0CAFCB8DF62EB5239, 0x8266259ED31390A1, 0x7A6B60936C3EE9B1, 0x82F95AE8EBFDF80F, 0x0E992454B2A028365, 0x0CCA3EB3AEBAAAAF1, 0x4F7B7482BA6B9B50, 0x32A448B338174DBC, 0x9BF0D87A84A81B7A, 0x90B0BCC994E1A8C3, 0x907F8298C8E385FC, 0x9C6EC09F1FA0F07F ] ``` - Tính theo Logic ngược lại: $$\text{Input}_\text{gốc} \xrightarrow{\text{Shuffle/Pack}} \text{Input}_\text{Khối} \xrightarrow{\times K \pmod{2^{64}}} \text{Target}$$ - Giờ chuyển thành: $$\text{Input}_\text{Khối} = \text{Target} \times K^{-1} \pmod{2^{64}} \xrightarrow{\text{Unshuffle}} \text{Input}_\text{gốc}$$ ```python! from Crypto.Util.number import inverse ConstA = [ 0x21F63DF6EA176765, 0x0AB1FD5C1E0A9765, 0x40382E78D2BF618F, 0x1883A77DECE9FAF7, 0x7EF4324F07E21EB5, 0x8C27B1CABDC5778F, 0x4AB7BC73427DF467, 0x0FEE7AA89FBDCF35F, 0x0FBFB328BB71B46B1, 0x3A54E11FDDC74D27, 0x0AABADEE9278DCE1D, 0x64A10164A8BDBE83, 0x0D64ADFD1EF8882C1, 0x0A3CDC5274D7B6CD5, 0x05388BCE06632DA1, 0x961201B184768A51, 0x0F318EB353D285DF7, 0x0F62D256FF95FAD05, 0x0C8D42BE55BD754DB, 0x0D1C86DAB8378475F, 0x8537555ABD8B7219, 0x0EC927CC094A2DAFD, 0x0D278D636CCBE1A01 ] ConstB = [ 0xB695C2BD10A19523, 0x3B858B3E8671B94D, 0x9D99CDB818E8BF0F, 0xEC2AFD8FC9140303, 0xC3E16C0BA35B97B9, 0x5085BA414E33D2DD, 0xF001B18435C1CC93, 0xBD9A607B178C4267, 0xF848AEC0E54EA671, 0xEC40D1CE64B42A89, 0x7A69DCA7959C9975, 0xE7015D92ECA6C995, 0x9C70C9956AD4CC6D, 0x4B95EE2F5CB11853, 0x2A1FD7BE9190BD25, 0xF425838CD21568FB, 0x6319F7F42D581163, 0xC9DA1F9E8F790A0F, 0x1076477A4D86F435, 0xFD7C305A832AD541, 0x2D2ED34F5A8ED569, 0x820C45DC6BD2EB43, 0x0A91349CBCB508CC5 ] ConstC = [ 0x0BC7C3C0B0BE5E531, 0x111EC8CE746A6243, 0x499851BCDA2073BD, 0x36E0DD8C4C897C55, 0x1DEA534DA4FCFEB7, 0x7B57E4E5165E26B9, 0x4A1C2B6AC91891A7, 0x44EEC6699738FA8D, 0x0AD4DFE6B00A02157, 0x2EB554CA9635FAFB, 0x623F7B0197F79C13, 0x0A3415911EFF3BED9, 0x4B4FD9B5E21BCFF3, 0x6F78C98DC732D231, 0x2AEC3662719E09CB, 0x915462C402BE8401, 0x0FA2BC3A4BBFA039, 0x669B5347E3C2CAE5, 0x13ED2473AC48603D, 0x0F5DDA0418D09D53B, 0x2548ACAB280A4AE3, 0x858E612083B3E24B, 0x1FDF3D9116AC2313 ] Target = [ 0x47EE0B7C8B7D3CBB, 0x0E6287DF7349CED, 0x777BEDA8C9D49B43, 0x56F80210D3708C41, 0x0CA97B6C052AEF171, 0x78B8C3301E2A2DF8, 0x0B3B52628FA5B7DFD, 0x0A8CBBC861ECE9708, 0x8674B6F2DE5669EC, 0x20B5D04D1270C20E, 0x201451D60FC4D54D, 0x0CAFCB8DF62EB5239, 0x8266259ED31390A1, 0x7A6B60936C3EE9B1, 0x82F95AE8EBFDF80F, 0x0E992454B2A028365, 0x0CCA3EB3AEBAAAAF1, 0x4F7B7482BA6B9B50, 0x32A448B338174DBC, 0x9BF0D87A84A81B7A, 0x90B0BCC994E1A8C3, 0x907F8298C8E385FC, 0x9C6EC09F1FA0F07F ] def solve(): flag_bytes = b"" mask = 0xFFFFFFFFFFFFFFFF print("[*] Decrypting and Unshuffling...") for i in range(23): K = (ConstA[i] * ConstB[i] * ConstC[i]) & mask K_inv = inverse(K, 2**64) ori_val = (Target[i] * K_inv) & mask jumbled = ori_val.to_bytes(8, 'little') ordered = bytearray(8) #Unshuffling... ordered[0] = jumbled[3] ordered[1] = jumbled[2] ordered[2] = jumbled[6] ordered[3] = jumbled[1] ordered[4] = jumbled[4] ordered[5] = jumbled[5] ordered[6] = jumbled[0] ordered[7] = jumbled[7] flag_bytes += ordered final_flag = flag_bytes print(f"FLAG: {final_flag}") if __name__ == "__main__": solve() ``` Flag: **KCSC{Welcome_to_the_RE_category_in_this_CTF!_Thank_you_for_fighting_until_the_very_end_of_this_challenge._If_you_are_First_Blood,_you_earn_a_100k_bounty._Congrats!_Stay_sharp,_hacker!}** # keygenme (medium): Oh wow ta có bài GUI App C++ nè :D - Mở File lên ta được như này: ![image](https://hackmd.io/_uploads/ryMgFXnGZg.png) - Nhấn Active thì nó sẽ ra: ![image](https://hackmd.io/_uploads/SJMMKXhfZg.png) - Hoặc: ![image](https://hackmd.io/_uploads/BJzXYm3G-l.png) - Đem chúng đi giải mã thì biết được chúng là FakeFlag. - Okay vậy là ta cần tìm username và Key đúng là xong, nạp file zô IDA bắt đầu ngắm nghía: Trước hết, ta tìm được `WinMain`: ```cpp! int __stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) { _QWORD *v6; // rax ATOM v7; // dx int result; // eax HWND Window; // rbx int v10; // edi int v11; // ebp int SystemMetrics; // esi int v13; // eax struct tagMSG Msg; // [rsp+60h] [rbp-E8h] BYREF struct tagRECT Rect; // [rsp+90h] [rbp-B8h] BYREF WNDCLASSW WndClass; // [rsp+A0h] [rbp-A8h] BYREF WCHAR ClassName[4]; // [rsp+F0h] [rbp-58h] BYREF __int64 v18; // [rsp+F8h] [rbp-50h] __int64 v19; // [rsp+100h] [rbp-48h] __int64 v20; // [rsp+108h] [rbp-40h] INITCOMMONCONTROLSEX picce; // [rsp+118h] [rbp-30h] BYREF picce.dwSize = 8; picce.dwICC = 0x4000; InitCommonControlsEx(&picce); v6 = VirtualAlloc(0i64, 0x110ui64, 0x3000u, 0x40u); Dynamic_FlagcheckFunc__ = v6; if ( v6 ) { *v6 = bigdata[0]; // 4885D2550F94C04D85C05756534889D30F94C208D00F85ED00000031C04885C90F84E2000000803C0100740B48FFC0483D0001000075EF31D2803C1300741448FFC24881FA0001000075EE41BB00010000EB034189D34585DB0F84A900000083F80C0F85A000000031C080396B0F8597000000807902610F858D000000807904330F858300000080790633757D80790835757780790A6E75718079016D756B80790372756580790576755F80790772755980790921755380790B39754D4531D241B937130000BE0C0000004489D0420FBE2C1399F7FE4489C84431CD4863D20FB63C1131D2F7F631EF89D2440FB60C114101F947390C90750F49FFC24539D37FCAB801000000EB02 v6[33] = Tail_of_bigdata; // 31C05B5E5F5DC390 qmemcpy( (void *)((unsigned __int64)(v6 + 1) & 0xFFFFFFFFFFFFFFF8ui64), (const void *)((char *)bigdata - ((char *)v6 - ((unsigned __int64)(v6 + 1) & 0xFFFFFFFFFFFFFFF8ui64))), 8i64 * (((unsigned int)v6 - (((_DWORD)v6 + 8) & 0xFFFFFFF8) + 272) >> 3)); *(_QWORD *)ClassName = 'C\0S\0C\0K'; // kcsc :D v18 = 'G\0y\0e\0K'; v19 = 'l\0C\0n\0e'; v20 = 's\0s\0a'; *(_QWORD *)&WndClass.style = 0i64; *(_QWORD *)&WndClass.cbClsExtra = 0i64; *(_OWORD *)&WndClass.hbrBackground = 0i64; WndClass.lpfnWndProc = (WNDPROC)WinProc; WndClass.hInstance = hInstance; WndClass.lpszClassName = ClassName; WndClass.hCursor = LoadCursorW(0i64, (LPCWSTR)0x7F00); WndClass.hbrBackground = 0i64; WndClass.hIcon = LoadIconW(0i64, (LPCWSTR)0x7F00); v7 = RegisterClassW(&WndClass); result = 0; if ( v7 ) { Window = CreateWindowExW( 1u, ClassName, L"KCSC Software Activation", // /omg 0xCA0000u, 0x80000000, 0x80000000, 520, 310, 0i64, 0i64, hInstance, 0i64); result = 0; if ( Window ) { ShowWindow(Window, nShowCmd); UpdateWindow(Window); GetWindowRect(Window, &Rect); v10 = Rect.right - Rect.left; v11 = Rect.bottom - Rect.top; SystemMetrics = GetSystemMetrics(0); v13 = GetSystemMetrics(1); SetWindowPos(Window, 0i64, (SystemMetrics - v10) / 2, (v13 - v11) / 2, 0, 0, 5u); while ( GetMessageW(&Msg, 0i64, 0, 0) > 0 ) { TranslateMessage(&Msg); DispatchMessageW(&Msg); } return Msg.wParam; } } } else { MessageBoxW(0i64, L"Failed to initialize!", L"Error", 0x10u); return 0; } return result; } ``` - Ta thấy chương trình đang xử lý 1 vùng nhớ Bigdata nào đó, thật ra nó đang cố decode vùng nhớ này thành các Opcodes, sau đó thực hiện chúng trong thời gian thực, hàm `WinProc` và việc Debugging động cũng sẽ cũng có quan điểm này của mình. - Tiếp theo, ngắm `WinProc` ta chỉ nên quan tâm bắt đầu từ phần này vì những phần khác chỉ ra râu ria tạo Giao diện linh tinh: ```cpp! if ( (_WORD)a3 == 3 && (a3 & 0xFFFF0000) == 0 ) { GetWindowTextA(qword_7FF7912B0F50, String, 256);// Lấy User + Seri GetWindowTextA(qword_7FF7912B0F48, Str.m128i_i8, 256); if ( String[0] && Str.m128i_i8[0] ) { if ( Dynamic_FlagcheckFunc__ && (unsigned int)((__int64 (__fastcall *)(CHAR *, __m128i *, __int16 *))Dynamic_FlagcheckFunc__)( String, &Str, SMT_SUS) == 1 ) // Dynamic flagcheckfunc() { qmemcpy(chText, byte_7FF7912A6020, sizeof(chText)); v22 = strlen(Str.m128i_i8); sub_7FF7912921BD((__int64)&Str, v22, chText, 280); v26 = 0; sub_7FF7912A53B0(Buffer, 0x400ui64, (wchar_t *)Format, chText); put_result_fail_or_success_(hWndParent, Buffer, 1); } else { v21 = sub_7FF791292242((__int64)&unk_7FF7912A61A0, dword_7FF7912A6140); sub_7FF7912A53B0(Buffer, 0x400ui64, (wchar_t *)Format, v21); put_result_fail_or_success_(hWndParent, Buffer, 0); } return 0i64; } else { MessageBoxW(hWndParent, L"Please enter both Username and Serial Key!", "I", 0x30u); return 0i64; } } } ``` - Có 1 con trỏ hàm, mình sẽ đặt tên nó là `Dynamic_FlagcheckFunc`. Khi debug động thì hàm này sẽ trỏ vào 1 vùng code được Gen Runtime và mình tin là sau khi được chế biến từ `Bigdata`. - Dường như `Dynamic` sẽ xử lý gì đó với Input là `username và seri`, nếu đúng thì sẽ chạy vào cái hàm `put_result` cho ta. Bắt đầu Debug động, mình sẽ đặt Breakpoint tại đoạn gọi `Dynamic_FlagcheckFunc`, Stepin để coi nó làm gì: ```asm! debug042:0000021815EB0000 test rdx, rdx ; rdx cầm Seri, sau sẽ đưa cho rbx debug042:0000021815EB0003 push rbp debug042:0000021815EB0004 setz al debug042:0000021815EB0007 test r8, r8 ; r8 = SMT_SUS debug042:0000021815EB000A push rdi debug042:0000021815EB000B push rsi debug042:0000021815EB000C push rbx ; đoạn này kiểm tra coi username và seri có rỗng ko debug042:0000021815EB000D mov rbx, rdx debug042:0000021815EB0010 setz dl debug042:0000021815EB0013 or al, dl debug042:0000021815EB0015 jnz loc_21815EB0108 debug042:0000021815EB001B xor eax, eax debug042:0000021815EB001D test rcx, rcx ; rcx cầm username debug042:0000021815EB0020 jz loc_21815EB0108 debug042:0000021815EB0026 debug042:0000021815EB0026 loc_21815EB0026: ; CODE XREF: debug042:0000021815EB0035↓j debug042:0000021815EB0026 cmp byte ptr [rcx+rax], 0 ; check lenght username, ko cần qtam vì nó hardcoded debug042:0000021815EB002A jz short loc_21815EB0037 debug042:0000021815EB002C inc rax debug042:0000021815EB002F cmp rax, 256 debug042:0000021815EB0035 jnz short loc_21815EB0026 ; check lenght username, ko cần qtam vì nó hardcoded debug042:0000021815EB0037 debug042:0000021815EB0037 loc_21815EB0037: ; CODE XREF: debug042:0000021815EB002A↑j debug042:0000021815EB0037 xor edx, edx debug042:0000021815EB0039 debug042:0000021815EB0039 loc_21815EB0039: ; CODE XREF: debug042:0000021815EB0049↓j debug042:0000021815EB0039 cmp byte ptr [rbx+rdx], 0 debug042:0000021815EB003D jz short loc_21815EB0053 ; kiểm tra Seri lenght (max 256b) debug042:0000021815EB003F inc rdx debug042:0000021815EB0042 cmp rdx, 100h debug042:0000021815EB0049 jnz short loc_21815EB0039 debug042:0000021815EB004B mov r11d, 100h debug042:0000021815EB0051 jmp short loc_21815EB0056 debug042:0000021815EB0053 ; --------------------------------------------------------------------------- debug042:0000021815EB0053 debug042:0000021815EB0053 loc_21815EB0053: ; CODE XREF: debug042:0000021815EB003D↑j debug042:0000021815EB0053 mov r11d, edx debug042:0000021815EB0056 debug042:0000021815EB0056 loc_21815EB0056: ; CODE XREF: debug042:0000021815EB0051↑j debug042:0000021815EB0056 test r11d, r11d debug042:0000021815EB0059 jz loc_21815EB0108 debug042:0000021815EB005F cmp eax, 0Ch debug042:0000021815EB0062 jnz loc_21815EB0108 debug042:0000021815EB0068 xor eax, eax debug042:0000021815EB006A cmp byte ptr [rcx], 6Bh ; 'k' debug042:0000021815EB006D jnz loc_21815EB010A debug042:0000021815EB0073 cmp byte ptr [rcx+2], 61h ; 'a' debug042:0000021815EB0077 jnz loc_21815EB010A debug042:0000021815EB007D cmp byte ptr [rcx+4], 33h ; '3' debug042:0000021815EB0081 jnz loc_21815EB010A debug042:0000021815EB0087 cmp byte ptr [rcx+6], 33h ; '3' debug042:0000021815EB008B jnz short loc_21815EB010A debug042:0000021815EB008D cmp byte ptr [rcx+8], 35h ; '5' debug042:0000021815EB0091 jnz short loc_21815EB010A debug042:0000021815EB0093 cmp byte ptr [rcx+0Ah], 6Eh ; 'n' debug042:0000021815EB0097 jnz short loc_21815EB010A debug042:0000021815EB0099 cmp byte ptr [rcx+1], 6Dh ; 'm' debug042:0000021815EB009D jnz short loc_21815EB010A debug042:0000021815EB009F cmp byte ptr [rcx+3], 72h ; 'r' debug042:0000021815EB00A3 jnz short loc_21815EB010A debug042:0000021815EB00A5 cmp byte ptr [rcx+5], 76h ; 'v' debug042:0000021815EB00A9 jnz short loc_21815EB010A debug042:0000021815EB00AB cmp byte ptr [rcx+7], 72h ; 'r' debug042:0000021815EB00AF jnz short loc_21815EB010A debug042:0000021815EB00B1 cmp byte ptr [rcx+9], 21h ; '!' debug042:0000021815EB00B5 jnz short loc_21815EB010A debug042:0000021815EB00B7 cmp byte ptr [rcx+0Bh], 39h ; '9' ; username = hardcoded (kmar3v3r5!n9) debug042:0000021815EB00BB jnz short loc_21815EB010A debug042:0000021815EB00BD xor r10d, r10d ; r10d = biến lặp i debug042:0000021815EB00C0 mov r9d, 1337h ; r9d_prev = 0x1337, esi = 12 = usernamelenght debug042:0000021815EB00C6 mov esi, 12 debug042:0000021815EB00CB debug042:0000021815EB00CB loc_21815EB00CB: ; CODE XREF: debug042:0000021815EB00FF↓j debug042:0000021815EB00CB mov eax, r10d debug042:0000021815EB00CE movsx ebp, byte ptr [rbx+r10] ; seri[i] debug042:0000021815EB00D3 cdq debug042:0000021815EB00D4 idiv esi ; edx = i % 12 debug042:0000021815EB00D6 mov eax, r9d ; chuyển r9d -> eax debug042:0000021815EB00D9 xor ebp, r9d ; seri[i] ^ r9_prev debug042:0000021815EB00DC movsxd rdx, edx ; rdx = i %12 debug042:0000021815EB00DF movzx edi, byte ptr [rcx+rdx] ; edi = username[i % 12] debug042:0000021815EB00E3 xor edx, edx debug042:0000021815EB00E5 div esi ; edx = r9d_prev % 12 debug042:0000021815EB00E7 xor edi, ebp ; edi = username[i%12] ^ (seri[i] ^ r9_prev) debug042:0000021815EB00E9 mov edx, edx debug042:0000021815EB00EB movzx r9d, byte ptr [rcx+rdx] debug042:0000021815EB00F0 add r9d, edi ; r9d_now = username[i%12] ^ (seri[i] ^ r9_prev) + username[r9d_prev%12] debug042:0000021815EB00F3 cmp [r8+r10*4], r9d ; SMT_SUS[i] phải bằng r9d_now debug042:0000021815EB00F7 jnz short loc_21815EB0108 debug042:0000021815EB00F9 inc r10 debug042:0000021815EB00FC cmp r11d, r10d debug042:0000021815EB00FF jg short loc_21815EB00CB debug042:0000021815EB0101 mov eax, 1 debug042:0000021815EB0106 jmp short loc_21815EB010A ``` - Có vẻ như đoạn code đang check `username` (mặc định phải là `kmar3v3r5!n9` không thì cúc) và đồng thời check `seri` bằng cách: ``` Loop: SMT_SUS[i] = username[i%12] ^ (seri[i] ^ r9_prev) + username[r9d_prev%12] r9_prev = SMT_SUS[i - 1] ``` - `i` bắt đầu chạy từ 0 - `r9` bắt đầu khởi tạo = 1337h - `SMT_SUS` là bảng Target Table được tạo sẵn, ta chỉ lấy Blob này ra. Trong quá trình debug mình còn phát hiện ra bài sẽ lấy Seri làm key (+ Padding = clone) để giải mã Flag theo dạng RC4: ![image](https://hackmd.io/_uploads/rkf1pZ6GWg.png) ![image](https://hackmd.io/_uploads/HyIWT-pz-g.png) > Nhưng bản chất ta giải được Seri là đủ ra flag rồi... Hiểu được điều này, dường như việc gen username và seri key vô cùng đơn giản: ```python! SMT = [ 0x1350,0x13E9,0x13FC,0x1400,0x1453,0x1489,0x14F7,0x1529, 0x15A0,0x15DF,0x166D,0x1677,0x169B,0x1733,0x17B8,0x1803, 0x1875,0x1858,0x188D,0x18F3,0x1905,0x199D,0x1A39,0x1AC8, 0x1AC1,0x1AB9,0x1B23,0x1B6E,0x1B9A,0x1C2F,0x1CBB,0x1D36, 0x1DA8,0x1E02,0x1EA2,0x1EFF,0x1F27,0x1F90,0x1FEC,0x201E, 0x20D9,0x20BB,0x212D,0x212E,0x21A4,0x2200,0x228D,0x2366, 0x239F,0x0110 ] USERNAME = b'kmar3v3r5!n9' def gen_serial(SMT_SUS, username): serial = [] r9_prev = 0x1337 uname_len = len(username) for i, r9 in enumerate(SMT_SUS): u1 = username[i % uname_len] u2 = username[r9_prev % uname_len] temp = (r9 - u2) & 0xFFFFFFFF temp = temp ^ u1 s_byte = (temp ^ r9_prev) & 0xFF serial.append(s_byte) r9_prev = r9 & 0xFFFFFFFF return bytes(serial) def main(): serial = gen_serial(SMT, USERNAME) print(serial) if __name__ == "__main__": main() #KCSC-2026-JU57-R341-UX0R-4ndd-U4DD-W!TH-U53R-N4M3% ``` Đoạn này khá bựa, kết quả ra được dính quả % ở cuối, khi nhập cả cái seri này thì chương trình gen FakeFlag và chỉ gen FlagRiel khi bỏ `%` đi (Khả năng mình dump sai Blob SMT_SUS) :vvv. Màn hình hiện Gen Flag thành công và cho ta: `VXpCT1ZGRXpkRVJOUnpRMVkycFNNR1JVUlRCa1EwVjNZbXBXWmxkVVFqRllNR2N3WkdwT1psVXpWbXBaZWsweFRsZGFNVTFVUmpWWWVsSnFaRU5HTWs1SVVUQmtSRTVyV0hwa2IwMHhPVTFKVjAxNlltcFZlbGd4VFhkYWJsSXpUa2hKZWxneFkyaGtSMmhtVld0Tk1GaDZVblZhUmpsRlRUSk5kMXBFVG1aUmJVWjZXbFJaTUZoNlRtWldRMFowVFROT1psaDZNSEJMVTJ3NQ==` Vứt lên `decoder` rồi b64decode 2 lần là ra Flag: >Thế mà 1 lúc mình mới biết đấy :vvv, bài này nói chung rút thời gian mình đi hơi nhiều do tự nhiên gặp mấy lỗi trông đần như này... ![image](https://hackmd.io/_uploads/r1Keyv3MWl.png) # 33,550,337 (medium): Câu này cho ta 1 file `.apk` và ghi nhét nó vào các trình phân tích thì mình thấy rằng nó bị làm rối khá nặng, mình làm khá ít bài về `.apk` nên cũng hơi rối vì những bài mình làm chủ yếu Logic nó ở `MainActivities` hết rồi :v Ngồi ngẩn ngơ 1 lúc thì mình nghĩ ra 1 cách đó là `Hack` trực tiếp apk... Công cụ mình dùng không xa lạ với anh em có tuổi thơ mod mấy game offline để bug vô hạn tiền mạng hay máu các thứ... Đó là `Game Guardian`! Đầu tiên cài Tool và file `.apk` vào máy ảo, mình chọn NOX Player: - Mở App và Tool, ở màn hình chính có vẻ như App cần ta nhấp đủ 33 triệu lần mới nhả flag :v ![image](https://hackmd.io/_uploads/rkcpbvhGZe.png) - Nhấp 3 lần, rồi mở Tool: ![Screenshot 2025-12-14 231237](https://hackmd.io/_uploads/rksZMv2GZx.png) ![Screenshot 2025-12-14 231250](https://hackmd.io/_uploads/Byi-GP2Mbe.png) - Có vẻ như nó tìm được khá nhiều giá trị 3 trong process hiện tại, nhấp thêm vài lần rồi tìm Cycle progress bằng Tool 1 lần nữa... ![image](https://hackmd.io/_uploads/H1BSfw2G-e.png) - Ô có vẻ như chỉ còn 1 giá trị tương ứng với Cycle hiện tại, sửa nó thành 999999999 là ngon! - ![image](https://hackmd.io/_uploads/Sk2OfDnzWg.png) Flag kìa!!! # Flag in Wonderland (tobecontinue) Đề bài là 1 challenge Flagchecker được viết bằng ngôn ngữ Gameboy cổ, để có thể reverse bài này mình sử dụng 2 tool đó là `bgb` và GhidraBoy. ![image](https://hackmd.io/_uploads/r1aMeFBQbg.png) - Cụ thể bài cần ta nhập đúng flag thôi :v Sau khi nạp file lên GhidraBoy thì mình nhanh chóng đi tìm cái dòng `Enterflag` nằm ở đâu để tìm manh mối xử lý Input, khi mình tìm được thì mình nhảy vào được 1 hàm trông như này: > Mình đã đổi tên biến và hàm theo ý hiểu của mình. ![image](https://hackmd.io/_uploads/Skt8lYHQZg.png) ![image](https://hackmd.io/_uploads/r19wxtrmZl.png) Cụ thể tại sao mình viết biến key chính là mảng được XOR với OxCC đó chính là khi debug bằng `bgb`, mình có để ý ở phần đầu WRAM có 1 dòng như này: ![image](https://hackmd.io/_uploads/S16CgKrX-x.png) - Key bắt đầu từ C0F6 cũng chính là giá trị được show trên GhidraBoy. - Đồng thời mình cũng bắt được Ciphertext nằm ở ngay đầu WRAM do hàm `MEMCMP` mình đoán ở dưới có truyền vào địa chỉ C0D6. - Sau khi chạy dần vòng lặp XOR ở đầu hàm thì mình thấy cái vùng mình nói là Key biến đổi dần dần rồi thành như này: ![image](https://hackmd.io/_uploads/BJ9TZtH7bg.png) Được rồi, vì là bài flag checker và String được compare với Input là 1 chuỗi byte dị dị như này thì mình đoán đề bài sẽ thử mã hóa Input rồi so sánh với Ciphertext xem có giống nhau hay không, nếu không giống thì trả về 0, từ đó thoát chương trình: ![image](https://hackmd.io/_uploads/H1FdGKHQbx.png) ## Maybe_Sbox: ```CPP! void Maybe_Sbox(int a1,int a2) { char *pcVar1; byte bVar2; uint in_AF; char cVar3; int i; code *UNRECOVERED_JUMPTABLE; undefined2 local_8; undefined2 local_2; local_8 = 0; i = 0; do { /* S[i] = i */ *(char *)(a2 + i) = (char)i; i = i + 1; /* chia dư 256 */ } while ((char)((uint)i >> 8) == '\0'); local_2 = 0; do { /* j % lenKey (inAF chính là trạng thái của thanh ghi khi mới bắt đầu hàm) */ i = FUN_059c(in_AF >> 8,local_2); bVar2 = (char)local_8 + *(char *)(a2 + local_2) + *(char *)(a1 + i); local_8 = (uint)bVar2; cVar3 = *(char *)(local_8 + a2); pcVar1 = (char *)(a2 + (uint)(byte)(*(char *)(a1 + i) + cVar3 + bVar2)); *(char *)(local_8 + a2) = *pcVar1; *pcVar1 = cVar3; if ((char)((char)local_2 + '\x01') == '\0') { local_2._1_1_ = local_2._1_1_ + '\x01'; } local_2 = CONCAT11(local_2._1_1_,(char)local_2 + '\x01'); } while (local_2._1_1_ == '\0'); /* WARNING: Could not recover jumptable at 0x02bb. Too many branches */ /* WARNING: Treating indirect jump as call */ (*UNRECOVERED_JUMPTABLE)((char)((uint)a2 >> 8),UNRECOVERED_JUMPTABLE,local_2); return; } ``` Mình thử dựa vào đoạn code rồi đoán rằng đây là 1 hàm tạo Sbox trông như này: ```python! maybe_Sbox: S[] = [i for in range(256)] for j in range(256): i = j % keyLen local8 = local8 + S[j] + key[i] idx = key[i] + local8 + S[local8] S[local8], S[idx] = S[idx], S[local8] ``` ## Supa_Crypto Đây mới chính là cái khó của bài, tại phần code có phần tham số khá là Lmao khi cụ thể ngoài hàm có thể chỉ truyền vào 3 tham số nhưng khi vào trong đọc logic hàm thì lại thấy nó nhận đến 6 tham số... Được hint của các anh là đọc phần source code asm và debug động thì đây là những gì mình rút ra: ```cpp! undefined2 Supa_Crypto(int param_1,byte *Sbox,int param_3,byte param_4,char param_5,byte param_6 ) { char cVar1; byte *pbVar2; byte bVar3; byte m; undefined2 unaff_retaddr; byte i; byte local_c; byte j; undefined2 n; i = 0; j = 0; local_c = *Sbox; bVar3 = 0; n = 0; while (n._1_1_ < (byte)(param_5 + ((byte)n < param_4))) { for (m = 0; m < param_6; m = m + 1) { i = i + 1; pbVar2 = Sbox + i; bVar3 = *pbVar2; j = j + bVar3; *pbVar2 = Sbox[j]; Sbox[j] = bVar3; bVar3 = Sbox[(byte)(*pbVar2 + bVar3)]; } local_c = local_c ^ bVar3 ^ *(byte *)(n + param_1); *(byte *)(n + param_3) = local_c; cVar1 = (byte)n + 1; n = CONCAT11(n._1_1_,cVar1); if (cVar1 == '\0') { n = (uint)(byte)(n._1_1_ + 1) << 8; } } /* WARNING: Treating indirect jump as return */ return unaff_retaddr; } ``` Cụ thể đầu tiên tại sao lai có tận 6 tham số, minh xin được giải thích như sau: - `param1, param3`: Tham số này mình đoán dựa trên cách đề bài làm việc với chúng và hint rằng đây là mã hóa RC4 Custom, `param1` mình đoán nó là source và `param3` sẽ là dest dựa trên việc tính toán được thực hiện trên `param1` rồi lưu vào vùng nhớ của `param3`. - `param2`: Đây chính là địa chỉ Sbox được tạo ra trước khi chạy hàm này. Mình có 2 manh mối để đưa ra kết luận này: - Nếu tin tưởng vào pseudo Ghidra, có thể Maybe_Sbox truyền Sbox lên stack tại vùng nhớ auStack165, ta có thể thấy pattern này lặp lại ở lần mã hóa tiếp theo:![image](https://hackmd.io/_uploads/B1EsSFrQ-e.png) - Để ý nếu như param2 là Sbox thì đoạn code mã hóa trông khá giống PRGA: ![image](https://hackmd.io/_uploads/B1pVIYH7Zl.png) - `param 4 và 5` mình đoán chúng lần lượt có thể là 0 và 0x20, tại sao? - ![image](https://hackmd.io/_uploads/H1xl_KHQbx.png) ![image](https://hackmd.io/_uploads/H1IM_YHQWx.png) - Ta thấy rằng tham số được truyền vào hàm qua cơ chế push stack, stack được push BC trong đó B là byte cao còn C là byte thấp lưu giá trị là 0x20, trong code có `(n._1_1_ < (byte)(param_5 + ((byte)n < param_4)))` bản chất chỉ đang so sánh n với 32 nằm trong BC: - `param6`: Đây mình đoán là tham số đầu tiên được đẩy vào hàm, với lần mã hóa đầu tiên `param6` có giá trị là 0x17: ![image](https://hackmd.io/_uploads/Hkq4YKSXbe.png) ![image](https://hackmd.io/_uploads/H13rKtHm-g.png) Vậy trông đoạn code sẽ được mình mô phỏng kiểu như này: ```python! PRGA: i = 0 j = 0 n = 0 temp = 0 while(maybe: n <= 32): for m in range(param_6): i = i + 1 & 0xFF j = j + S[i] & 0xFF S[i], S[j] = S[j], S[i] & 0xFF temp = S[S[j] + S[i]] & 0xFF c = c ^ temp ^ src[n] & 0xFF dest[n] = c ``` ## Kết luận: Vậy dựa trên pseudo và dự đoán của mình, thì giờ mình chỉ cần mò `param6` ứng với từng lần gọi `Supa_Crypto` thì ta dường như có thể giải ra flag, cụ thể ta thấy rằng Input sẽ bị đem đi mã hóa `Maybe_Sbox + Supa_Crypto` 3 lần, nên ta sẽ giải mã Cipher 3 lần ứng với 3 cái `param6`. - Param6-1 = 0x17, - Param6-2 = 0x20 ![image](https://hackmd.io/_uploads/H1RucKrX-x.png) - Param6-3 = 0x37 - Mình mò không được nên thử lấy tham số cuối được truyền vào hàm `Supa_Crypto` cuối thì vẫn đúng là Param6-3 riel: ```python! def decrypt(ciphertext, key, param6): S = list(range(256)) key_len = len(key) local8 = 0 for j in range(256): i = j % key_len local8 = (local8 + S[j] + key[i]) & 0xFF idx = (key[i] + local8 + S[local8]) & 0xFF S[local8], S[idx] = S[idx], S[local8] i = 0 j = 0 prev_cipher_byte = S[0] plaintext = [0] * len(ciphertext) for n in range(len(ciphertext)): temp = 0 for m in range(param6): i = (i + 1) & 0xFF j = (j + S[i]) & 0xFF S[i], S[j] = S[j], S[i] temp= S[(S[i] + S[j]) & 0xFF] current_cipher_byte = ciphertext[n] plaintext[n] = (prev_cipher_byte ^ temp ^ current_cipher_byte) & 0xFF prev_cipher_byte = current_cipher_byte return bytes(plaintext) cipher = bytes.fromhex('ED4E69BE8449C676D7B70CA3061B4BE2115B4151D737D8B82B3F047D9664A1') key = b't0j1t4_h34rt_n1_fur3t41' l1 = decrypt(cipher, key, 0x37) l2 = decrypt(l1, key, 0x20) l3 = decrypt(l2, key, 0x17) print(f"flag: {l3}") ``` Flag: **KCSC{t0r4w4r3_n0_f41ry_t4l3!!!!}** # easy_obf (hard): > Hơi tiếc câu này mình không giải được trong quá trình tham gia giải, cách của mình nếu đủ trâu bò thì sẽ giải ra thui nhưng mà lúc đó mình đã oải + cố gắng tìm cách khác mà không được :< > ![image](https://hackmd.io/_uploads/ryMQplb7Zx.png) ## Phân tích: Nạp file đề bài vào ida thì việc nhìn pseudo sẽ là không khả quan do mã nguồn đã bị làm rối (đúng như des của đề: `Don't trust your disassembler !!!`): ![image](https://hackmd.io/_uploads/Syp_FMbm-x.png) Để nhìn được Logic gốc ta phải chuyển góc nhìn sang phần assembly: ![image](https://hackmd.io/_uploads/SygJ5z-mWe.png) Có thể thấy rằng mã bao gồm rất nhiều các block: ```asm push Address mov [rsp + 4], 1 retn ``` Ta đều biết rằng, trong asm mỗi khi caller gọi tới 1 hàm callee, nó sẽ đẩy địa chỉ instruction tiếp theo lên stack để sau khi calee chạy xong thì lệnh retn chỉ cần pop địa chỉ ở đỉnh stack và gán nó cho RIP thôi... Nói cách khác, bài sử dụng chính cơ chế này của `retn - C3` để làm rối không cho IDA gen Pseudo cũng như là cách để điều khiển luồng cho chương trình -> `PUSH Address -> RETN == JUMP Address`. Cụ thể, 1 block điều luồng có dạng như trên sẽ đẩy value là `địa chỉ`, nhưng trong x64 lệnh push chỉ đẩy lên stack một giá trị 32bit, vì vậy sau khi đẩy address lên stack block sẽ còn có 1 đoạn `mov [rsp + 4], 1` giúp điền nốt giá trị của địa chỉ cần nhảy tới. Hiểu được cách làm rối của đề, cách mình làm sẽ là trace liên tục các `jump` trá hình này, nhặt các instruction rồi sau đó thử ghép chúng lại để tìm manh mối logic của đề bài. ### Diving Deeper... Chạy đề trên cmd: ``` PS C:\Users\duong\Downloads> .\easy_of.exe Usage: <chall.exe> <flag> ``` Test: ``` PS C:\Users\duong\Downloads> .\easy_of.exe "KCSC{hihihi}" Wrong! ``` Dựa vào kết quả ta biết rằng đây là 1 bài theo dạng Flagchecker, việc cần làm bây giờ là mò xem đoạn checkflag hay ít nhất là đoạn nào đang sờ mó đến flag hoặc input... Dưới đây là toàn bộ các đoạn instruction mà mình trâu bò trace và ghi lại được trong quá trình giải bài: https://docs.google.com/document/d/1C987Txjl8AluU_A7PaMc_LgglrqBEmSPovfmskq9swk/edit?usp=sharing (Dài quá nên phải cho lên docs :(((. Flow sẽ là từ trên xuống dưới không bị ngắt quảng quay ngược về trước (mình trải phẳng Loop rùi...) Có code rồi thì ta thử phân tích coi chương trình thực sự đang làm cái quái gì nhé: ### Analyzing codes: Instruction đầu tiên mà ta gặp đó chính là: ```asm .upx0:0000000140006598 sub rsp, 3D8h ``` - Đây theo mình là những dòng code đầu tiên của Logic chính, nó lấy 1 vùng nhớ khá lớn để bắt đầu xây dựng logic, vùng nhớ cần sử dụng, ... Tiếp theo, tại Blob này dường như chương trình đang điền gì đó vào stack (Thẻ 2): ![image](https://hackmd.io/_uploads/rkDZnXbQZe.png) Cụ thể logic gán của đoạn này như sau: ```asm! mov eax, 0Ch imul rax, i (bắt đầu từ 0, mỗi khi gán value 3 lần cho vùng [rax + rcx] thì tăng i lên 1 đơn vị. Có thể đoạn này khởi tạo mảng hay ma trận gì đó). lea rax, [rsp+rax+Offset] mov ecx, 4 imul ecx, 0 (tăng dần từ 0 -> 2) mov dword ptr [rax+rcx], value ``` - Dựa vào logic thì ta có thể tách được 2 mảng được tạo bằng Logic này, 1 cái bắt đầu từ: `[rsp + rax + 178h]` chứa khoảng 9 ký tự (3 * 3) và 1 cái bắt đầu từ `[rsp + rax + F8h]`: - Ta sẽ được 2 mảng nếu được dựng thành ma trận trông chúng sẽ như này: ```python # Ma trận 3x3 matrix = [ [0x06, 0x18, 0x01], [0x0D, 0x10, 0x0A], [0x14, 0x11, 0x0F] ] # Ciphertext dump được cipher = [ 0xE9, 0xAF, 0x12, 0x2D, 0x6B, 0x44, 0x51, 0xEC, 0x49, 0x5B, 0xD1, 0x3E, 0x8F, 0xEA, 0x56, 0x1A, 0x94, 0x11, 0x12, 0xEC, 0xC2, 0xA4, 0xDA, 0x9D ] ``` Tiếp theo, chương trình dường như đang load các strings lên stack (Thẻ 3: Load Strings) theo mình tìm hiểu được những strings API hay strings được đề bài in ra sẽ được đẩy lên stack để tránh bị các công cụ phân tích tĩnh như IDA dò được và cho vào mục `.data` hay bị phát hiệ bởi `strings` hay các công cụ phân tích tĩnh đơn giản. - Chương trình đẩy các strings là các WindowAPI như: `LoadLibraryA (Để nạp các file .all), GetProcAddress (Tìm địa chỉ các API khác dựa vào tên đã nạp trong Stack), GetStdHandle (Quản lý I/O bàn phím màn hình), WriteConsoleA, GetCommandLineW và CommandLineToArgvW (Cái này dùng để tách Tham số truyền vào chương trình hay tách Input để kiểm tra`. - Kèm theo đó là các Strings tương tác mà author viết như `Usage, Wrong, Correct, KCSC{...}`. Nhìn vào strings ta nhận ra có thể đề bài sẽ chỉ nhận thân flag thôi chứ phần `KCSC{}` không được xét mà sẽ được gói lại bằng chương trình về sau. Sau khi load các strings, chương trình sẽ tiến hành xây dựng Buffer1[256] và Buffer2[256] (chúng được khởi tạo rỗng bằng các giá trị 0). `sub_140006000` chính là hàm gì đó có chức năng tạo Buffer. Code sẽ gọi hàm này 2 lần, tạo 2 Buffer1 và 2 ở 2 vùng nhớ: [rsp + 1D0h] và [rsp + 2D0h]. (Thẻ 4) Sau khi setup Buffer, chương trình gọi đến hàm xử lý Windows API như `loc_14000C08E+2` sẽ tìm địa chỉ `kernel32.32`, cụ thể: ```asm! mov rax, gs:60h ; Trong x64 trên thanh ghi đoạn GS tại Offset 60h trỏ tới PEB (Process Environment Block) mov rax, [rax+18h] ; Tại Offset 18h của PEB trỏ tới cấu trúc PEB_LDR_DATA, nó lưu trữ danh sách các .dll đã được nạp vào chương trình .upx0:00000001400060DE 48 8B 40 20 mov rax, [rax+20h] ; Tại offset 20h của PEB_LDR_DATA là LinkedList chứa các module theo thứ tự được lưu vòa bộ nhớ. mov rax, [rax] ; Đi tới node tiếp theo (ntdll.dll) mov rax, [rax] ; Đi vào node tiếp (kernel32.dll) mov rax, [rax+20h] ;Trong cấu trúc LDR_DATA_TABLE_ENTRY, khi duyệt bằng InMemoryOrderLinks (nằm ở offset 0x10 của struct), thì trường DllBase (địa chỉ cơ sở của DLL) nằm ở offset 0x30.Khoảng cách từ 0x10 đến 0x30 chính là 0x20 -> Lệnh này lấy địa chỉ gốc của kernel32.dll đưa vào rax ``` Sau khi tìm địa chỉ của kernel32.dll thì nhảy đến hàm trông khá là rối, dường như nó đang cố tìm địa chỉ API trong `Export Table (GetProcAddress manual implementation)`. Cụ thể qua hàm `word_14000C11A+2`, tham số được truyền vào chính là địa chỉ của `kernel32.all` được tìm thấy ban nãy và string `LoadLibraryA`![image](https://hackmd.io/_uploads/BkNUjDZQZe.png) Tại sao mình biết được thì trong lúc trace thì mình bị dính vào 1 cái loop chạy rất lâu và khi quan sát bộ nhớ thì tên các API được cập nhật liên tục, còn phần code thì quá khó hiểu nên mình chỉ hiểu sơ sơ: ![image](https://hackmd.io/_uploads/BkypjvW7bx.png) Tiếp, chương trình bắt đầu gọi dần các API khi đã setup xong: ```asm! call qword ptr [rsp+140h] // Gọi tới LoadLibraryA call qword ptr [rsp+1A8h] // Gọi tới GetStdHandle call qword ptr [rsp+1B0h] // Gọi tới: kernelbase_GetCommandLineW call qword ptr [rsp+1B8h] // CommandLineToArgvW ; Đây chính là API sẽ cắt lấy Input của ta. ``` Đằng sau còn có: ```asm! .upx0:0000000140007077 48 89 84 24 70 01 00 00 mov [rsp+170h], rax .upx1:0000000140009F8D 83 BC 24 58 01 00 00 02 cmp dword ptr [rsp+158h], 2 .upx2:000000014000CFBD 0F 84 10 00 00 00 jz loc_14000CFD3 (có nhảy vào đây) .upx2:000000014000D010 B8 08 00 00 00 mov eax, 8 .upx0:00000001400070D5 48 6B C0 01 imul rax, 1 .upx1:0000000140009FE9 48 8B 8C 24 70 01 00 00 mov rcx, [rsp+170h] .upx2:000000014000D025 48 8B 0C 01 mov rcx, [rcx+rax] .upx0:00000001400070E9 FF 94 24 C0 01 00 00 call qword ptr [rsp+1C0h] //lstrlenW .upx1:000000014000A001 89 84 24 34 01 00 00 mov [rsp+134h], eax .upx2:000000014000D039 83 BC 24 34 01 00 00 17 cmp dword ptr [rsp+134h], 23 ``` - Đoạn này sẽ kiểm tra xem ta có nhập Input hay không và Input phải có đọ dài là 23 (ta nắm được độ dài flag). Ta cũng biết luôn Flag nằm ở đoạn `mov rcx, [rsp+170h]` (để ý phần này trong asm một chút ta sẽ thấy địa chỉ này lưu string từ byte thứ 8 or argv[1]). Tiếp theo, chương trình xử lý Input ta nhập thông qua vòng lặp, chuyển nén dữ liệu từ 2 byte còn 1 byte: ```asm! .upx0:0000000140007153 C7 44 24 34 00 00 00 00 mov dword ptr [rsp+34h], 0 .upx2:000000014000D0A9 8B 84 24 34 01 00 00 mov eax, [rsp+134h] .upx0:000000014000717D 39 44 24 34 cmp [rsp+34h], eax .upx1:000000014000A07F 0F 8D 10 00 00 00 jge loc_14000A095 (không nhảy vào cái này) .upx2:000000014000D0C0 B8 08 00 00 00 mov eax, 8 .upx0:0000000140007191 48 6B C0 01 imul rax, 1 .upx1:000000014000A0A5 48 63 4C 24 34 movsxd rcx, dword ptr [rsp+34h] .upx2:000000014000D0D5 48 8B 94 24 70 01 00 00 mov rdx, [rsp+170h] .upx0:00000001400071A5 48 8B 04 02 mov rax, [rdx+rax] .upx1:000000014000A0BA 0F B7 04 48 movzx eax, word ptr [rax+rcx*2] .upx2:000000014000D0ED 25 FF 00 00 00 and eax, 0FFh .upx0:00000001400071B9 48 63 4C 24 34 movsxd rcx, dword ptr [rsp+34h] .upx1:000000014000A0CE 88 84 0C D0 01 00 00 mov [rsp+rcx+1D0h], al .upx2:000000014000D095 8B 44 24 34 mov eax, [rsp+34h] .upx0:000000014000716B FF C0 inc eax .upx1:000000014000A06B 89 44 24 34 mov [rsp+34h], eax .upx2:000000014000D0A9 8B 84 24 34 01 00 00 mov eax, [rsp+134h] .upx0:000000014000717D 39 44 24 34 cmp [rsp+34h], eax .upx1:000000014000A07F 0F 8D 10 00 00 00 jge loc_14000A095 ... Lặp lại Logic này: Dường như nó chuyển flagừ 2 byte còn 1 byte rồi lưu lần lượt vào Buffer trên Stack ``` - Cụ thể `[rsp+34h] là biến đếm i, mỗi lần chạy nó sẽ được so sánh với len(Input) được lưu ở [rsp + 134h] mà ta biết được ở hàm trước`. Ta để ý mỗi khi logic thực hiện xong i sẽ được tăng bằng cách `mov eax, [rsp+34h] -> inc eax -> mov [rsp+34h], eax`. - Logic bên trong Loop này là nén các ký tự từ 2 bit xuống 1 bit (Trên Windows sử dụng chuẩn UTF-16 cho các hàm có hậu tố `CommandLineToArgvW - được nạp và sử dụng trong bài`): ```asm! movsxd rcx, dword ptr [rsp+34h] .upx2:000000014000D0D5 48 8B 94 24 70 01 00 00 mov rdx, [rsp+170h] .upx0:00000001400071A5 48 8B 04 02 mov rax, [rdx+rax] .upx1:000000014000A0BA 0F B7 04 48 movzx eax, word ptr [rax+rcx*2] .upx2:000000014000D0ED 25 FF 00 00 00 and eax, 0FFh .upx0:00000001400071B9 48 63 4C 24 34 movsxd rcx, dword ptr [rsp+34h] .upx1:000000014000A0CE 88 84 0C D0 01 00 00 mov [rsp+rcx+1D0h], al ``` - rdx cầm chuỗi, rcx cầm biến đếm lặp `i`, duyệt 1 ký tự nó được tăng gấp đôi giá trị (do đọc dword), các giá trị đọc được lấy byte thấp thông qua `and 0FFh` rồi lưu vào buffer `[rsp + 1D0h]` mà ta đã tạo từ trước. Cuối cùng, sau khi đã duyệt nén các ký tự, ta đến với logic check Flag chính của bài: ```asm! .upx0:00000001400071E3 4C 8D 8C 24 78 01 00 00 lea r9, [rsp+178h] ;Nếu còn nhớ thì đây chính là địa chỉ của cái ma trận 3x3 đề xây dựng lúc đầu. .upx1:000000014000A0F9 41 B8 18 00 00 00 mov r8d, 18h ; len(Input tính cả '\0') .upx2:000000014000D12A 48 8D 94 24 D0 02 00 00 lea rdx, [rsp+2D0h] ; Buffer đã được tọa từ trước nhưng đang trống, có thể kết quả của hàm xử lý input cuối cùng sẽ được ném vào đây .upx0:00000001400071FB 48 8D 8C 24 D0 01 00 00 lea rcx, [rsp+1D0h] ; Đoạn Input đã được xử lý ``` Nhìn vào Thẻ 6: End Game trong link docs: ```asm! .upx1:00000001400093A4 C7 44 24 08 00 00 00 00 mov dword ptr [rsp+8], 0 ; Biến đếm i .upx0:000000014000643A 8B 44 24 30 mov eax, [rsp+30h] .upx1:00000001400093CF 39 44 24 08 cmp [rsp+8], eax ... .upx0:0000000140006426 8B 44 24 08 mov eax, [rsp+8] .upx1:00000001400093BC 83 C0 03 add eax, 3 .upx2:000000014000C40C 89 44 24 08 mov [rsp+8], eax .upx0:000000014000643A 8B 44 24 30 mov eax, [rsp+30h] .upx1:00000001400093CF 39 44 24 08 cmp [rsp+8], eax ; Đoạn này là vòng lặp ngoài cùng, tăng i lên 3 và kiểm tra xem nó có < 24 hay không ``` - Dường như đây là 3 Loop lồng nhau, đi sâu nữa coi ... ```asm! .upx2:000000014000C420 0F 8D 10 00 00 00 jge loc_14000C436 .upx0:000000014000644E C7 44 24 04 00 00 00 00 mov dword ptr [rsp+4], 0 .upx2:000000014000C45A 83 7C 24 04 03 cmp dword ptr [rsp+4], 3 .upx0:0000000140006478 0F 8D 10 00 00 00 jge loc_14000648E .upx1:0000000140009407 C7 44 24 0C 00 00 00 00 mov dword ptr [rsp+0Ch], 0 ; sum .upx2:000000014000C46F C7 04 24 00 00 00 00 mov dword ptr [rsp], 0 .upx1:0000000140009432 83 3C 24 03 cmp dword ptr [rsp], 3 .upx2:000000014000C498 0F 8D 10 00 00 00 jge loc_14000C4AE Đoạn này sẽ tạo các biến lặp j (0 -> 2) và k (0 -> 2) ``` ```asm! .upx0:00000001400064C1 48 63 44 24 04 movsxd rax, dword ptr [rsp+4] .upx1:0000000140009446 48 6B C0 0C imul rax, 0Ch .upx2:000000014000C4BE 48 8B 4C 24 38 mov rcx, [rsp+38h] .upx0:00000001400064D6 48 03 C8 add rcx, rax .upx1:000000014000945A 48 8B C1 mov rax, rcx Đoạn này lấy j*12 nghĩa là nó trỏ thẳng vào đầu hàng của ma trận 3x3 lưu ở [rsp+38h] (38h - 18h == 20h -> Nơi lưu 3X3 đã được khai báo đầu hàm này) ``` ```asm! .upx0:00000001400064E9 8B 14 24 mov edx, [rsp] .upx1:000000014000946D 44 8B 44 24 08 mov r8d, [rsp+8] .upx2:000000014000C4E7 44 03 C2 add r8d, edx .upx0:00000001400064FC 41 8B D0 mov edx, r8d .upx1:0000000140009482 48 63 D2 movsxd rdx, edx .upx2:000000014000C4FA 4C 8B 44 24 20 mov r8, [rsp+20h] .upx0:000000014000650F 41 0F B6 14 10 movzx edx, byte ptr [r8+rdx] Đoạn này lấy i + k (Hay vị trí ký tự Input cần lấy). Rồi sau đó load Input vào r8 rồi lấy giá trị ở vị trí Input[i+k] ... .upx1:0000000140009495 8B 04 88 mov eax, [rax+rcx*4] ; Matrix[j][k] .upx2:000000014000C50F 0F AF C2 imul eax, edx .upx0:0000000140006524 8B 4C 24 0C mov ecx, [rsp+0Ch] .upx1:00000001400094A8 03 C8 add ecx, eax ; add sum .upx2:000000014000C522 8B C1 mov eax, ecx .upx0:0000000140006538 89 44 24 0C mov [rsp+0Ch], eax Đoạn này tiếp tục lấy rax = Matrix[k][j] đem nhân với edx là Input rối cộng dồn vào sum... Có thể thấy ngay đây chính là nhân ma trận chính hiệu! ``` Vậy Logic của bài toán có thể là như sau: Chương trình sẽ chặt Input của ta ra thành 3 mẩu mỗi mẩu khoảng 8 byte rồi xếp nó như 1 ma trận với 3 dòng tương đương với 3 mẩu. Ta được cho 1 Ma trận 3x3, ma trận còn lại mình coi là Ciphertext từ flag và vẻ đúng như mình dự đoán, ta cần nhập Input sao cho: ![image](https://hackmd.io/_uploads/ryoOiK-mZg.png) Do ta đã dump được Matrix và Ciphertext, mình có thể bruteforce luôn flag: ## Solution ```python! # Ma trận 3x3 matrix = [ [0x06, 0x18, 0x01], [0x0D, 0x10, 0x0A], [0x14, 0x11, 0x0F] ] # Ciphertext dump được cipher = [ 0xE9, 0xAF, 0x12, 0x2D, 0x6B, 0x44, 0x51, 0xEC, 0x49, 0x5B, 0xD1, 0x3E, 0x8F, 0xEA, 0x56, 0x1A, 0x94, 0x11, 0x12, 0xEC, 0xC2, 0xA4, 0xDA, 0x9D ] def solve_block(m, c_block): for i in range(256): for j in range(256): for k in range(256): res0 = (m[0][0]*i + m[0][1]*j + m[0][2]*k) % 256 res1 = (m[1][0]*i + m[1][1]*j + m[1][2]*k) % 256 res2 = (m[2][0]*i + m[2][1]*j + m[2][2]*k) % 256 if [res0, res1, res2] == c_block: return [i, j, k] return [0, 0, 0] flag = "" for b in range(0, len(cipher), 3): block = cipher[b:b+3] decoded = solve_block(matrix, block) flag += "".join(chr(x) for x in decoded) print(f"flag: {flag}") ``` Flag: **KCSC{easy_obfuscate_0328db2e}** > Bài này còn có cách khác nhanh hơn, mình có thể sẽ cập nhật trong tương lai ...