# Tổng quan Trong phần writeup này sẽ đề cập đến solution cho 2 bài rev mình đã giải được trong cuộc thi. Đánh giá sơ bộ thì đề thi có mức độ phân bố chủ đề cho reverse engineer khá rộng (rust, Android/JNI, eBPF, C#), và 2 bài cuối có độ đánh đố khá cao (eBPF yêu cầu phải hiểu về kernel, C# thì phải hiểu về NetCore native). Trong quá trình làm thì mình chỉ riêng bài C# thì mình đánh giá là "gần xong", còn eBPF thì không có đủ thời gian để có thể nghiên cứu. ## 1. Warmup (rev - cRust binary) ![Pasted image 20231203155120](https://hackmd.io/_uploads/rk-H-B4Up.png) ### Tổng quan Analyze một chút về binary, dễ thấy đây là một file executable cho linux 64bit, và đã bị stripped (các thông tin về hàm về biến đã bị xóa): ``` /m/d/c/uit ❱ file chal chal: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=daa06d739e20d6f7674614bd3716bf8e84188d31, for GNU/Linux 3.2.0, stripped ``` Tiến hành mở file bằng decompiler IDA, có graph sơ bộ như sau: ![Pasted image 20231203155522](https://hackmd.io/_uploads/HJ5BbrN8T.png) Với hàm main được disassemble như trên, có thể xác định được đây là chương trình compile từ Rust. Dấu hiệu dễ thấy nhất là các hàm sẽ không bao giờ được gọi trực tiếp, mà hàm "main" thật sự sẽ được wrap bên trong hàm main giả này (theo ví dụ trên thì main thật sẽ là `sub_55845AA06100`) ### Tìm mục tiêu - Recon Chạy thử chương trình với output giả, ta có kết quả như sau: ![Pasted image 20231203204615](https://hackmd.io/_uploads/SJQIWrVLp.png) Dễ thấy đây sẽ là dạng crackme (Đi tìm password thỏa mãn chương trình), sau khi có được thông tin ban đầu, dựa vào từ khóa `Nope` được in ra màn hình, xét hàm `sub_55845AA06100`: ![Pasted image 20231203204813](https://hackmd.io/_uploads/rkjIZr4Ip.png) Với rust, các string sẽ được dùng theo dạng offset, và pseudo-code có khả năng cao sẽ sai, do đó có thể phỏng đoán rằng trong đoạn này sẽ thực hiện đọc input từ người dùng. ![Pasted image 20231203205016](https://hackmd.io/_uploads/BkVwWBEUT.png) Kéo xuống một chút, sẽ thấy được string offset được dùng thêm 2 lần nữa, và có một quá trình check gì đó đang diễn ra (biến input đã được đổi tên để tiện cho việc reverse) -> Có thể `if (!v11)` là hàm check nếu không thỏa mãn thì in Nope và kết thúc, và ngược lại toàn bộ mảng thỏa mãn thì sẽ in `Seems good`. ### Phân tích trọng tâm Boot debug server ở phía wsl hoặc máy ảo và tiến hành remote debug. Trước hết cần xác định xem input sau khi nhập sẽ được lưu ở đâu. Đặt debug ở dòng 87 (nguyên nhân ở dòng này là do nó gán tới biến sẽ được check trong vòng for -> ưu tiên biết nguồn gốc của biến này) ![Pasted image 20231203205924](https://hackmd.io/_uploads/HJEuWBNLp.png) ![Pasted image 20231203210029](https://hackmd.io/_uploads/SJdd-r48T.png) Sau khi xác định được biến chứa input đầu vào, tiến hành lục tìm các thông tin liên quan tới phần check. ![Pasted image 20231203210134](https://hackmd.io/_uploads/H1QYbH4Up.png) Dễ thấy, đây là một vòng for lặp 49 lần, mỗi lần lặp sẽ kiểm tra như sau: `inp[index] ^ v24[v9 = index = v25] == v22[index]` Debug để tìm hiểu về `v24`: ![Pasted image 20231211114937](https://hackmd.io/_uploads/SkwtWBEU6.png) Ban đầu, `v9 = v25 = 0x40` nên sẽ nhảy vào if -> Khởi tạo mảng `v24` và các giá trị `v9, v25` sẽ được reset về 0 cho việc lặp. Hơn nữa, `v7` cũng chứa giá trị là độ dài chuỗi nhập vào, nên `if (v7 == index)` có thể là check xem đã lặp hết input chưa, nếu đã lặp hết rồi thì exit (kiểm tra cùng size giữa key và input, output) `v10` được thể hiện ở dạng `v24[0].m128i_i32[v9]` -> Coi đây như là một mảng với các giá trị `int` (32bit), độ dài mảng này sẽ là 49. ![Pasted image 20231211115937](https://hackmd.io/_uploads/BJst-BVLp.png) ![Pasted image 20231211120338](https://hackmd.io/_uploads/B1AKWrN8p.png) Viết thử script để solve: ```py key = [0x832322A1, 0xC3722E76, 0x47698390, 0x9E156FB4, 0x689CD913, 0xE6BC6964, 0x842DA38C, 0x24E46B44, 0x571C2908, 0x458617EF, 0xC4808B64, 0xA82F1076, 0x5F260BCE, 0x75EDD17A, 0x5777387D, 0x5F12E030, 0x11F93675, 0xC0B4EC8C, 0xB7137690, 0x1DF0AEBD, 0x3CCEB952, 0x023A5134, 0x29562AC6, 0x456FD80C, 0xA962BE48, 0x86506F7E, 0x7DD0D11A, 0x13CCEC59, 0x05B4FDE4, 0x33EAC5B3, 0x2B38F371, 0xD49F9017, 0x4D1FCB25, 0x42C54BDF, 0x08479D47, 0xC79F3427, 0xC63A376B, 0x90BD92A9, 0x5972AEE7, 0xA908739D, 0x60B8B266, 0xD1B1773A, 0x7B679F24, 0x0DADA483, 0xECB9EAEA, 0x0299778E, 0x535CA68F, 0x0D545BE6, 0xB136BDC0] output = [0xBF, 0x7F, 0x60, 0x6B, 0x6E, 0xA1, 0xB4, 0x8B, 0x12, 0x01, 0x0A, 0x26, 0x4B, 0x53, 0x0A, 0x46, 0xB5, 0x03, 0x22, 0x02, 0xA9, 0x10, 0xAF, 0x6A, 0x16, 0x78, 0x2C, 0xD3, 0x1D, 0x09, 0xAF, 0x48, 0x32, 0x46, 0xC8, 0x5B, 0x93, 0x49, 0xA9, 0x96, 0x7B, 0xE3, 0xF2, 0xF8, 0x0C, 0x74, 0xAB, 0x6C, 0xD0] print(''.join([chr(i^(j&0xff)) for i,j in zip(output, key)])) ``` Output: ![Pasted image 20231211120424](https://hackmd.io/_uploads/S1r9brEIT.png) Có thể thấy key này không phù hợp để giải, check key lại một lần nữa: ![Pasted image 20231211120609](https://hackmd.io/_uploads/HJq5WrNLa.png) Cùng một input, nhưng mỗi lần chạy lại dùng một key khác nhau -> Có thể có cơ chế anti debug được sử dụng, quay trở lại phần đầu của hàm main: ![Pasted image 20231211122432](https://hackmd.io/_uploads/rkCqbB4IT.png) Bên trong hàm `sub_55E6534D6610` sẽ sử dụng các hàm để lấy unix epoch time -> Đây có thể là nguyên nhân khiến key phụ thuộc thời gian, vậy mục tiêu là sẽ cố gắng né if này. Nhưng nếu chương trình chạy bình thường thì nó vẫn sẽ đúng -> Nguyên nhân có thể do syscall đã check debugger ? Để tiện thì sẽ debug bằng GDB ở chỗ syscall: ``` gdb -q ./chal start brva 0x9145 ``` ![Pasted image 20231211123210](https://hackmd.io/_uploads/SJViWB48a.png) ![Pasted image 20231211123328](https://hackmd.io/_uploads/Byui-B4Ip.png) Như vậy thực chất hàm sẽ đang cố gắng gọi `ptrace(PTRACE_TRACEME);`. Tác dụng chính của hàm này sẽ debug chính nó, nhưng nếu vậy thì cần phải kết hợp với `fork()` (Một thread debug cho thread còn lại). Nhưng trong trường hợp này không có fork, nên đây chỉ là một dạng check debugger cơ bản (nếu một process đang bị debug thì không thể bị debug thêm nữa), nên khi chạy bình thường, syscall sẽ trả về 0, nhưng khi debug thì nó sẽ trả về -1 -> nhảy vào if. Để fix có 2 phương án: patch binary / patch memory,register trong lúc chạy. Ở đây đơn giản thì sẽ patch trực tiếp memory,register trong lúc chạy như sau: ![Pasted image 20231211123913](https://hackmd.io/_uploads/B16oZHNLT.png) Sau syscall, RAX = -1 -> Sửa RAX = 0 và tiếp tục chạy ![Pasted image 20231211123947](https://hackmd.io/_uploads/HJBhWrEUa.png) ![Pasted image 20231211124043](https://hackmd.io/_uploads/rJi3bH48a.png) ```py key = [0x6E1249E8, 0x7AD8474E, 0x8AC52E1B, 0x3ED51519, 0xC591080B, 0xB83E79C0, 0x4D9564D8, 0x005422D4, 0x5983BD65, 0xC74CB460, 0xE8BFC578, 0xDA357C4B, 0xE481BB14, 0x40B77026, 0xD15D317A, 0x1DF08419, 0xBFC1538C, 0x608A4C61, 0x51730A16, 0x312A9F37, 0x9667D8CC, 0x36304C22, 0xF8200C9C, 0x9F4E4E08, 0xF0F3A92F, 0xE251854F, 0xDA577918, 0xBC3116B6, 0x7709A72A, 0xB8C5FB6F, 0x12FB0DCB, 0x048A4271, 0xF4D86754, 0xF40CD922, 0x48C1DBAA, 0x190E9669, 0xA3C080F6, 0xC68B007E, 0x16DDB6CF, 0x5B57CCF2, 0xB2C8864F, 0x6C5700D3, 0x44BF50C7, 0x96D00BCC, 0x96E61869, 0x66F7224D, 0x1C3DAE9D, 0x0E6FE80F, 0xD9C58EAD] output = [0xBF, 0x7F, 0x60, 0x6B, 0x6E, 0xA1, 0xB4, 0x8B, 0x12, 0x01, 0x0A, 0x26, 0x4B, 0x53, 0x0A, 0x46, 0xB5, 0x03, 0x22, 0x02, 0xA9, 0x10, 0xAF, 0x6A, 0x16, 0x78, 0x2C, 0xD3, 0x1D, 0x09, 0xAF, 0x48, 0x32, 0x46, 0xC8, 0x5B, 0x93, 0x49, 0xA9, 0x96, 0x7B, 0xE3, 0xF2, 0xF8, 0x0C, 0x74, 0xAB, 0x6C, 0xD0] print(''.join([chr(i^(j&0xff)) for i,j in zip(output, key)])) ``` Kết quả: `W1{real_warm_up_9b45e23b974e7fd9fdb2e7fd4054e96c}` ### Flag `W1{real_warm_up_9b45e23b974e7fd9fdb2e7fd4054e96c}` ## 2. Happy Flappy (rev - Android) ![Pasted image 20231211134851](https://hackmd.io/_uploads/rkZTZS4Ia.png) ### Tổng quan Với việc đề cho file APK -> là challenge android. Mở file bằng JADX: ![Pasted image 20231211135051](https://hackmd.io/_uploads/HkVpWS4LT.png) APK khá cơ bản với 2 Activity và 1 Provider. ### Tìm mục tiêu - Recon Trước tiên, ta cần tìm vị trí mà chương trình sẽ đưa ra flag ![Pasted image 20231211135240](https://hackmd.io/_uploads/SJwTbSVIp.png) Sau khi xem qua các flag thì có vẻ như Class Champion sẽ đóng vai trò in flag được tạo từ native lib: ![Pasted image 20231211135327](https://hackmd.io/_uploads/S126ZB48T.png) Sử dụng Reference để lần ngược lại vị trí gọi tới Class này: ![Pasted image 20231211135629](https://hackmd.io/_uploads/ryxAWHVUp.png) ![Pasted image 20231211135646](https://hackmd.io/_uploads/SkV0-HEIT.png) Cả 2 đều nằm ở class gameView, được sử dụng bởi gameFragment. Có thể thấy game đang cố kiểm tra nếu như điểm số đạt được là `999999999` thì mới in flag -> Khó có thể đạt được nên cần tìm phương án khác. ### Phân tích trọng tâm Trước hết, do `getFlag()` không sử dụng bất cứ tham số nào -> Dùng IDA mở native lib để reverse quá trình tạo ra flag để clone. ![Pasted image 20231211140203](https://hackmd.io/_uploads/BJF0WB48T.png) Khi vào hàm `Java_com_example_fishi_flappybird_Champion_getFlag`, có thể thấy các string trong chương trình đều đã được mã hóa theo kiểu xor với một chuỗi cố định. Sau khi dump toàn bộ thì một số string được giải mã như sau: ```py from pwn import * key = b"\x2A\x62\x69\x23\x24\x5E\x26\x31\x32\x7A" sus_1 = [0x46, 0x0B, 0x0B, 0x45, 0x56, 0x37, 0x42, 0x50, 0x1F, 0x1D, 0x4B, 0x06, 0x0E, 0x46, 0x50] dat = sus_1 print(chr(dat[0] ^ 0x2a), end="") print(''.join([chr(i^j) for i,j in zip(dat[1:],key[1:] + key*9)])) sus_2 = [0x46, 0x0B, 0x0B, 0x5B, 0x54, 0x31, 0x55, 0x54, 0x56] dat = sus_2 print(chr(dat[0] ^ 0x2a), end="") print(''.join([chr(i^j) for i,j in zip(dat[1:],key[1:] + key*9)])) sus_3 = [0x46, 0x0B, 0x0B, 0x50, 0x51, 0x3C, 0x55, 0x45, 0x40, 0x1B, 0x5E, 0x07, 0x00, 0x00] dat = sus_3 print(chr(dat[0] ^ 0x2a), end="") print(''.join([chr(i^j) for i,j in zip(dat[1:],key[1:] + key*9)])) sus_4 = [0x0F, 0x0E, 0x11, 0x0E, 0x01, 0x32, 0x5E, 0x11, 0x17, 0x09] dat = sus_4 print(chr(dat[0] ^ 0x2a), end="") print(''.join([chr(i^j) for i,j in zip(dat[1:],key[1:] + key*9)])) sus_5 = [0x05, 0x12, 0x1B, 0x4C, 0x47, 0x71, 0x55, 0x54, 0x5E, 0x1C, 0x05, 0x0F, 0x08, 0x53, 0x57] dat = sus_5 print(chr(dat[0] ^ 0x2a), end="") print(''.join([chr(i^j) for i,j in zip(dat[1:],key[1:] + key*9)])) dat = p64(0x3086B120D50571B) + p32( 1984540) print(chr(dat[0] ^ 0x2a)) print(''.join([chr(i^j) for i,j in zip(dat[1:],key[1:] + key*9)])) dat = p64(307843608) print(chr(dat[0] ^ 0x2a)) print(''.join([chr(i^j) for i,j in zip(dat[1:],key[1:] + key * 9)])) dat = p64(0x5F17367010045259) + b'\vG' print(chr(115), end = "") print(''.join([chr(i^j) for i,j in zip(dat[1:],key[1:] + key * 9)])) sus_6 = [0x46, 0x0B, 0x0B, 0x45, 0x56, 0x37, 0x42, 0x50, 0x1F, 0x1D, 0x4B, 0x06, 0x0E, 0x46, 0x50, 0x70, 0x55, 0x5E] dat = sus_6 print(chr(108), end="") print(''.join([chr(i^j) for i,j in zip(dat[1:],key[1:] + key*9)])) sus_7 = [0x05, 0x06, 0x08, 0x57, 0x45, 0x71, 0x42, 0x50, 0x46, 0x1B, 0x05, 0x01, 0x06, 0x4E, 0x0A, 0x3B, 0x5E, 0x50, 0x5F, 0x0A, 0x46, 0x07, 0x47, 0x45, 0x4D, 0x2D, 0x4E, 0x58, 0x1C, 0x1C, 0x46, 0x03, 0x19, 0x53, 0x5D, 0x3C, 0x4F, 0x43, 0x56, 0x55, 0x4C, 0x0B, 0x05, 0x46, 0x57, 0x71, 0x4A, 0x5E, 0x55, 0x09, 0x05, 0x03, 0x19, 0x53, 0x0A, 0x32, 0x49, 0x56, 0x00] dat = sus_7 print(chr(47), end="") print(''.join([chr(i^j) for i,j in zip(dat[1:],key[1:] + key*9)])) dat = p32(1460143621) + b'\x45' print(chr(dat[0] ^ 0x2a)) print(''.join([chr(i^j) for i,j in zip(dat[1:],key[1:] + key * 9)])) sus_8 = [0x7A, 0x2D, 0x3A, 0x77, 0x04, 0x7B, 0x55, 0x11, 0x7A, 0x2E, 0x7E, 0x32, 0x46, 0x12, 0x0A, 0x6F, 0x2B, 0x3B, 0x7A, 0x15, 0x59, 0x16, 0x53, 0x03, 0x01, 0x2D, 0x2B, 0x3B, 0x71, 0x15, 0x44, 0x16, 0x0C, 0x4D, 0x50, 0x73, 0x72, 0x48, 0x42, 0x1F, 0x10, 0x42, 0x08, 0x53, 0x54, 0x32, 0x4F, 0x52, 0x53, 0x0E, 0x43, 0x0D, 0x07, 0x0C, 0x5C, 0x73, 0x51, 0x46, 0x45, 0x57, 0x4C, 0x0D, 0x1B, 0x4E, 0x09, 0x2B, 0x54, 0x5D, 0x57, 0x14, 0x49, 0x0D, 0x0D, 0x46, 0x40, 0x53, 0x2C, 0x72, 0x5D, 0x14, 0x5E, 0x07, 0x07, 0x57, 0x09, 0x12, 0x43, 0x5F, 0x55, 0x0E, 0x42, 0x58, 0x49, 0x06, 0x5E, 0x2B, 0x2B, 0x3B, 0x71, 0x15, 0x44, 0x0C, 0x0C, 0x40, 0x50, 0x37, 0x49, 0x5F, 0x08, 0x5A, 0x49, 0x0E, 0x06, 0x50, 0x41, 0x53, 0x2C, 0x3C, 0x38, 0x5F, 0x59] dat = sus_8 print(chr(80), end="") print(b''.join([chr(i^j).encode() for i,j in zip(dat[1:],key[1:] + key*9)])) sus_9 = [0x61, 0x58, 0x68, 0x36, 0x38, 0x35, 0x67, 0x48, 0x69, 0x54, 0x31, 0x42] dat = sus_9 # print(chr(80), end="") print(chr(dat[0] ^ 0x2a)) print(b''.join([chr(i^j).encode() for i,j in zip(dat[1:],key[1:] + key*9)])) ``` ``` libfrida-gadget libxposed libsubstratei# %lx-%lx %s /proc/self/maps 159.65.2.24b 2001$^&1 s0m3Th1n9= libfrida-gadget.so /data/data/com.example.fishi.flappybird/files/logs/app.log2 /data b'POST %s HTTP/1.1\r\nHost: %s\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: %zu\r\nCo' ``` Thông qua các string, có thể thấy được ở phần đầu chương trình đang cố gắng kiểm tra chương trình có đang sử dụng các tool để "vượt rào" hay không, một trong số đó là Frida, Xposed, ... Sau đó, sẽ thực hiện kết nối đến địa chỉ `159.65.2.24` cổng `2001` và thực hiện POST request, nội dung là `s0m3Th1n9=...` ![Pasted image 20231211140822](https://hackmd.io/_uploads/S1P1zS4IT.png) Ở hàm `sub_1AE0`, hay đã được đổi tên thành `get_last_in_log`, chương trình sẽ đọc từng dòng trong `/data/data/com.example.fishi.flappybird/files/logs/app.log`, lấy dòng cuối cùng, sau đó tách các phần bởi " - " và cũng lấy phần cuối cùng. sau đó thì cả 2 được tính độ dài để khởi tạo `data` (dữ liệu sẽ gửi trong POST request ở trên) Mọi thứ vẫn sẽ rất bình thường cho đến đoạn sau: ![Pasted image 20231211141737](https://hackmd.io/_uploads/Sk21GS4UT.png) ![Pasted image 20231211141759](https://hackmd.io/_uploads/rkgeGrEI6.png) Cả 2 string đều vượt quá 9 byte -> Có thể có cơ chế khác để ngắt string thông qua các biến phụ. Tới đây thì mình nảy ra ý tưởng khác: patch lại chương trình (giảm điều kiện để win xuống) để đảm bảo request được gửi là đúng. Quay lại code Java và tìm đoạn Log được tạo: ![Pasted image 20231211142108](https://hackmd.io/_uploads/SyNeGSV8T.png) Chương trình sử dụng signature của package để thực hiện ghi log, nếu thực hiện patch apk thì signature sẽ khác đi -> fail. Hướng đi khác: lấy signature từ log khi chương trình chạy bình thường, và patch luôn cả signature này vào log. Cách làm: Mở máy ảo AVD, cài APK và chơi thử game, sau đó dùng `adb root && adb shell` , `cd /data/data/com.example.fishi.flappybird/files/logs/` và `cat app.log` Sau khi có signature, thực hiện decompile app với APKTool: ![Pasted image 20231211142744](https://hackmd.io/_uploads/r1dgfrN8T.png) Ở file `smali\com\example\fishi\flappybird\gameView$2$1.smali`: ![Pasted image 20231211142944](https://hackmd.io/_uploads/SJhMfrVUT.png) Patch dòng 355 từ 9999999 thành một số nhỏ hơn, ví dụ như 4 Ở file `smali\com\example\fishi\flappybird\LogHelper.smali`: ![Pasted image 20231211143102](https://hackmd.io/_uploads/BJJQMHVIT.png) Patch trực tiếp `v1` thành const-string với giá trị là signature cho trước, như vậy khi log ra file thì nó sẽ luôn luôn là signature này -> bypass signature check. Dùng APKTool để build app và uber-apk-signer để sign app, cài vào máy thật (hoặc máy ảo chưa được root để đảm bảo hàm getFlag sẽ được chạy bình thường). Kết quả như sau: ![Pasted image 20231211143557](https://hackmd.io/_uploads/rJ8EfrEI6.png) ### Flag `W1{N1c3_t0_s33_y0u_h3r3_ea527304fcfe2a59abaefbfebb54675d}`