# HCMUSCTF 2022 - Secure Vault ## Tổng quan Nhìn chung đây có vẻ là một bài tìm password: ![](https://i.imgur.com/wrxuxzM.png) Khi nhìn vào thư mục của file thực thi thì chúng ta có thể xác định được chương trình sử dụng frameword `Qt` để lập trình ứng dụng. Cá nhân bản thân mình cũng đã từng một chút làm việc với framework này nên ngay từ lúc phát hiện điều này, mình liền nghĩ ngay đến việc check các class của chương trình. ## Tìm hàm check password IDA có một plugin tên là **Class Informer** cho những bạn muốn xài IDA, tuy nhiên trong lúc thi mình đã xài Ghidra vì Ghidra có hỗ trợ hiện các class của chương trình và mình cảm thấy giao diện GUI của Ghidra dễ nhìn hơn. Phía bên tay trái của Ghidra sau khi đã load file thực thi vào có phần `Classes`, chúng ta sẽ vô đó, vào mục `SecureVault`, sau đó mục này sẽ xuất hiện 2 node `vftable` khá khả nghi. ![](https://i.imgur.com/FW5kY60.png) Vào xem các hàm trong vftable, mình nhận thấy `FUN_140001490` chính là hàm chúng ta cần tìm. ## Reverse Engineer Tới lúc này mình quay lại IDA, nhảy vào hàm `sub_140001490` và bắt đầu quá trình dịch ngược. ```c= __int64 __fastcall sub_140001490(__int64 a1, __int64 a2, __int64 a3, _QWORD **a4) { int v5; // esi __int64 result; // rax unsigned int v8; // ebx __int64 v9[3]; // [rsp+20h] [rbp-18h] BYREF v5 = a2; result = QDialog::qt_metacall(a1, a2, a3); v8 = result; if ( (int)result >= 0 ) { if ( v5 ) { if ( v5 != 7 ) return v8; if ( (int)result < 3 ) { v9[0] = 0i64; **a4 = *(_QWORD *)QMetaType::QMetaType((QMetaType *)v9); } } else if ( (int)result < 3 ) { if ( (_DWORD)result ) { if ( (_DWORD)result == 1 ) sub_140002030(a1); else sub_140001CB0(a1); } else { sub_140002140(a1); } } v8 -= 3; return v8; } return result; } ``` Nhìn lướt qua một hồi, mình thấy rằng hàm `sub_140001CB0` chắc chắn chính là hàm check password: ```c= if ( *(_BYTE *)(a1 + 48) ) { QPlainTextEdit::toPlainText(*(_QWORD *)(*(_QWORD *)(a1 + 40) + 16i64), v20); v3 = &v30; v4 = 256i64; do ... ``` Đầu tiên chúng ta phải thoả mãn được điều kiện if trên bằng cách làm cho `*(_BYTE *)(a1 + 48) != 0`, như thế mới có thể vào được đoạn check. Quay lại hàm `sub_140002140`: ```c= __int64 __fastcall sub_140002140(__int64 a1) { __int64 v2; // rbx unsigned __int16 v3; // di char v4; // al char v6[16]; // [rsp+20h] [rbp-28h] BYREF __int64 v7; // [rsp+30h] [rbp-18h] QPlainTextEdit::toPlainText(*(_QWORD *)(*(_QWORD *)(a1 + 40) + 16i64), v6); v2 = 0i64; if ( v7 <= 0 ) { LABEL_4: QString::~QString(v6); v4 = 1; } else { while ( 1 ) { v3 = *(_WORD *)QString::operator[](v6, v2); if ( v3 < *(_WORD *)QString::operator[](v6, v2 - 1) ) break; if ( ++v2 >= v7 ) goto LABEL_4; } QString::~QString(v6); v4 = 0; } *(_BYTE *)(a1 + 48) = v4; return sub_140002AB0(a1); } ``` Có thể thấy được rằng để set được bit cho `(a1 + 48)`, password nhập vào phải có các kí tự sao cho `password[i + 1] >= password[i]` , ví dụ `password="abcccdef"` là một password thoả điều kiện trên. Đi vào đoạn quan trọng nhất là check password ở hàm `sub_1400036C0` ```c= char __fastcall sub_1400036C0(_QWORD *a1, unsigned __int8 *a2) { int v4; // ebx unsigned __int8 *v5; // r8 unsigned __int8 *v6; // rdx unsigned __int8 *v7; // rdx unsigned __int8 *i; // rcx __int64 v9; // rax char v10; // r14 _WORD *v11; // rcx __int64 v12; // rdx _QWORD *v13; // rsi __int64 v14; // rdx __int64 v15; // rbp _BYTE *j; // rcx v4 = 0; memset(a1, 0, 0x800ui64); v5 = a2; if ( *((_QWORD *)a2 + 3) < 0x10ui64 ) { v6 = a2; } else { v5 = *(unsigned __int8 **)a2; v6 = *(unsigned __int8 **)a2; } v7 = &v6[*((_QWORD *)a2 + 2)]; for ( i = v5; i != v7; ++a1[v9] ) v9 = *i++; v10 = anotherCondition(a1); v11 = a1 + 256; v12 = 256i64; do { *v11++ = 0; --v12; } while ( v12 ); v13 = (_QWORD *)a1[832]; if ( v13 ) { v14 = v13[1]; v15 = v13[2]; if ( v14 ) { visitTree((__int64)a1, v14, 0, 1); visitTree((__int64)a1, v15, 1, 1); } else { *((_WORD *)a1 + *((unsigned __int8 *)v13 + 32) + 1024) = 0; } j_j_free(v13); } for ( j = (char *)a1 + 2049; *(j - 1) == j[&unk_14000A200 - (_UNKNOWN *)a1 - 2049] && *j == j[byte_14000A201 - (_BYTE *)a1 - 2049]; j += 2 ) { if ( (unsigned int)++v4 >= 0x100 ) return v10; } return 0; } ``` Ở đây mình đã đổi tên hàm cùng với kiểu dữ liệu của một vài tham số để bạn đọc có thể dễ theo dõi hơn. Đầu tiên chương trình sẽ đếm tần số xuất hiện của từng kí tự trong password và lưu vào mảng `a1`, sau đó luồng thực thi sẽ đi vào hàm tên là `anotherCondition`. Hàm này tương đối dài và khó Reverse. Trong quá trình thi mình đã bỏ giữa chừng vì logic khá khó hiểu, cho tới khi giúp đồng đội xong một challenge khác thì mình nhận ra đây là một cấu trúc dữ liệu xử lí trên cây. Để có thể biểu diễn cây nhị phân, đa số chúng ta sẽ tạo ra một struct gồm một vài thành phần như `node* Left`, `node* Right`, ... ![](https://i.imgur.com/NgSpe3z.png) Tuy nhiên còn 1 cách khác để biểu diễn cây mà mình biết (cách này không thể áp dụng mọi nơi, tuy nhiên lại rất hiệu quả trong một số trường hợp): Cây nhị phân sẽ được biểu diễn trên mảng một chiều (mảng `tree` làm ví dụ) với ràng buộc: `tree[i]` sẽ là cha của `tree[i * 2]` và `tree[i * 2 + 1]` ![](https://i.imgur.com/HGzFxT5.png) Trong bài này, cả 2 cách lưu trữ và xử lí cây đều được sử dụng. Khi đã xác định được cấu trúc dữ liệu được sử dụng, từ lúc đó mình làm bài này khá mượt. Vì luồng thực thi khá dài dòng nên mình sẽ tóm tắt lại quá trình xử lí hàm này: - Đầu tiên hàm sẽ check điều kiện: tần số xuất hiện của các kí tự được xếp thành một dãy số tăng dần phải tương đồng với dãy số 2^0^, 2^0^, 2^1^, 2^2^, ..., 2^n^. Ví dụ `password=abbbbccd` sẽ thoả điều kiện này. Nếu không thỏa điều kiện, input vừa được nhập vào không phải là password. - Sau đó chương trình sẽ gán mỗi nút giá trị `n - id + 1`. Khi kết thúc giải, mình được biết đoạn check này xài thuật toán liên quan tới [Huffman Tree](https://www.geeksforgeeks.org/huffman-coding-greedy-algo-3/) Cuối cùng chương trình sẽ kiểm tra các node vừa tạo được có giống với trạng thái node mà chương trình mong muốn không. ## Tìm password Mình đã xác định được cách kiểm tra password, chuyện bây giờ cần làm chính là khôi phục lại password đúng. Tới đây mình tìm cách trích xuất trạng thái node mà chương trình mong muốn, nằm ở địa chỉ `0x14000A200`. Code bên dưới mình sử dụng để lấy được nội dung các bytes ở địa chỉ đó, cụ thể hơn mình sử dụng idapython: ```python= start = 0x14000A200 print(get_bytes(start, 512)) ``` Sau đó mình parse dãy 512 bytes này ra: ```python= a = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\t\x00\x01~\x07\x0e\x04\x1e\x05\x02\x02\x06\x03>\x06\xff\t\xfe\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' for i in range(256): if a[i*2] != 0 and a[i*2+1] != 0: print(chr(i), hex(a[i*2]), hex(a[i*2+1])) # results: # 0 0xfe 0x9 # 1 0x0 0x1 # 2 0x7e 0x7 # 3 0xe 0x4 # 4 0x1e 0x5 # 5 0x2 0x2 # 6 0x6 0x3 # 7 0x3e 0x6 # 8 0xff 0x9 # 9 0xfe 0x8 ``` Ở đây có 3 cột, tuy nhiên chúng ta chỉ quan tâm tới cột đầu và cột số 3. Hai cột 1 và cột 3 lần lượt là kí tự cần có trong password và giá trị `n - id + 1` của kí tự đó. Như thế password của chúng ta sẽ có kết quả như sau: ```python= a = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\t\x00\x01~\x07\x0e\x04\x1e\x05\x02\x02\x06\x03>\x06\xff\t\xfe\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00![](https://i.imgur.com/hVgvVPC.png) \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' ans = '' for i in range(256): if a[2*i] != 0 or a[2*i+1] != 0: ans += (chr(i) * int(pow(2, 9 - a[2*i+1]))) print(ans) # ans = 01111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111222233333333333333333333333333333333444444444444444455555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555666666666666666666666666666666666666666666666666666666666666666677777777899 ``` Nhập password vào: ![](https://i.imgur.com/7GVkph4.png) Nhưng: ![](https://i.imgur.com/kvUBpzE.png) ## Mảnh ghép cuối cùng Tới đây mình cứ nghĩ là lỗi của chương trình nên mình đã hỏi tác giả, và đây là câu trả lời: ![](https://i.imgur.com/g2YWd7l.png) Có một chi tiết cần được đề cập tới chính là khi nhập đúng password, chương trình sẽ decrypt flag dựa trên password ta nhập vào. Lúc này chỉ còn một trường hợp duy nhất chính là hàm decrypt đã bị gì đó, thế là mình quyết định tiếp tục teverse đoạn decrypt flag: ```c= ... v8 = v21; if ( QString::isNull((QString *)v20) ) v9 = 0i64; else v9 = QString::data((QString *)v20); v16 = v8; v17 = v9; sub_140002DB0(v23, &v16); QString::QString((QString *)v18, (const struct QString *)v23); QString::append((QString *)v18, (const struct QString *)(a1 + 56)); v10 = v19; ... ``` Minh để ý đến việc có vẻ key chính là `a1 + 56` được add vô trước khi chuẩn bị decrypt, tới đây tìm các hàm liên quan tới `a1 + 56` thì có hàm `sub_140002AB0` nhìn rất khả quan: ```c= v3 = 0i64; if ( QString::isNull((QString *)v18) ) v4 = 0i64; else v4 = QString::data((QString *)v18); v14 = v2; v15 = v4; v5 = *(_QWORD *)(a1 + 72); if ( !QString::isNull((QString *)(a1 + 56)) ) v3 = QString::data((QString *)(a1 + 56)); v16 = v5; v17 = v3; v6 = sha256((QString *)v20, (__int64)&v16, (__int64)&v14); QString::operator=(a1 + 56, v6); v7 = v20; ``` Nhìn qua thì có thể thấy: `a1 + 56` được tạo thành bằng cách sha256 đoạn password mình vừa nhập vào. Như thế để ra được flag chính xác, mình cần phải nhập từng kí tự của input chứ không phải `Ctrl + C` `Ctrl + V`. Mình viết một đoạn script để tính đoạn hash đó: ```python= import hashlib string = b'01111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111222233333333333333333333333333333333444444444444444455555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555666666666666666666666666666666666666666666666666666666666666666677777777899' result = b'' for i in range(len(string)): hash = hashlib.sha256(result + string[:i+1]) result = hash.hexdigest().encode() print(result) ``` Patch đoạn hash này vô `a + 56` lúc decrypt flag: ![](https://i.imgur.com/VnSQJ0b.png) `HCMUS-CTF{1s_my_p@ssw0rd_l0ng_3n0ugh_t0_typ3_:v}`