# 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)

### 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:

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:

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`:

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.

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)


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.

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`:

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.


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:

Có thể thấy key này không phù hợp để giải, check key lại một lần nữa:

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:

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
```


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:

Sau syscall, RAX = -1 -> Sửa RAX = 0 và tiếp tục chạy


```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)

### Tổng quan
Với việc đề cho file APK -> là challenge android. Mở file bằng JADX:

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

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:

Sử dụng Reference để lần ngược lại vị trí gọi tới Class này:


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.

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=...`

Ở 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:


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:

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:

Ở file `smali\com\example\fishi\flappybird\gameView$2$1.smali`:

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`:

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:

### Flag
`W1{N1c3_t0_s33_y0u_h3r3_ea527304fcfe2a59abaefbfebb54675d}`