Flare-on9 2022 chính thức kết thúc vào cuối tuần qua, chúng mình cũng đã giải được một số challenges và dưới đây là writeup challenge 1 -> 7

01_flaredle

Đề bài cho 4 file, mở file index.html lên thì thấy file script.js được load:

Trong file script.js có một số biến toàn cục được khởi tạo và hàm checkGuess đáng chú ý. Từ đoạn mã trong file script.js, có thể thấy flag của bài là chuỗi rightGuessString + ‘@flare-on.com’. Trong đó, chuỗi rightGuessString được gán bằng phần từ thứ 57 trong mảng WORD, và mảng này nằm trong file word.js. Mở file word.js và tìm phần tử thứ 57, có được flag:

flareonisallaboutcats@flare-on.com

02_PixelPoker

Đề bài cho file PixelPoker.exe, chạy thử một hồi, phát hiện ra mỗi khi click vào một vị trí bất kì trong chương trình thì số lần click sẽ được lưu lại và hiện trên thanh title, cùng với đó là cặp số đáng nghi ngờ, khi click 10 lần sẽ hiện ra thông báo yêu cầu chơi lại.

Mở file với ida, tìm theo chuỗi “Womp womp… :(” và các reference đến chuỗi này thì gặp hàm sub_4012C0

Tại hàm sub_4012C0 có 1 đoạn vòng lặp 2 vòng do..while. Bên trong đó có hàm gọi tới hàm sub_4015D0. Kiểm tra thử hàm sub_4015D0, có thể thấy hàm này sử dụng hàm sub_4011A0 để xử lí dữ liệu pixel được truyền vào. Kết quả sau khi xử lí được hàm SetPixel sử dụng.

Giờ check lại kỹ hơn 2 lệnh điều kiện. Khi debug thu được cặp giá trị khi so sánh là (0x5f, 0x139) = (95, 313). Có thể đây là 2 số hiển thị ở trên tilte, tiến hành thử rê chuột vào vị trí của 2 số này này và click vào thì flag hiện ra lập tức.

Quay lại lúc chương trình bớt đầu chạy, có hàm LoadImageW để load resouce id = 129. Handle xử lí dữ liệu tại resouce này được lưu ở biến toàn cục tên h.

Dữ liệu này sau đó sẽ được load bằng hàm GetObject, dựa vào kích thước và kiểu dữ liệu của resouce, kết quả của hàm GetObject được lưu vào struct BITMAP tại VA 0x41327C

Đặt kiểu dữ liệu tại VA 0x41327C thành BITMAP

Sau đó, sẽ thấy 2 lệnh if điều kiện ở trên thấy được vị trí được xác định dựa trên width và heigth của bitmap, các giá trị tại 0x412004 và 0x412008 được gán bằng hằng số:

x = 0x52414C46 % bitmap.width   // 95

y = 0x6E4F2D45 % bitmap.heigth  // 313

03_magic8ball

Đề bài cho file Magic8Ball.exe, khi chạy lên có giao diện hướng dẫn dùng là nhập câu hỏi và bấm các phím mũi tên. Sau khi nhấn enter thì có thông báo hiện lên ở giữa quả bóng.

Dựa vào các string hiện lên, tìm được hàm khởi tạo các giá trị này tại địa chỉ 0x402090. Việc gán một loạt giá trị cho biến this có thể suy ra được đây là hàm khởi tạo cho biến này, ngoài các giá trị text chứa tên font chữ, đường dẫn ảnh hay các text đã hiện ra trên màn hình như “Press arrow keys…”, “start type…”, có một đoạn text “gimme flag pls?” được gán vào this + 92 có vẻ khả nghi cần được theo dõi sau.

Tiếp tục tìm các reference của hàm 0x402090, xác định được hàm này được gọi tại hàm 0x4027a0. Biến this được khởi tạo vừa rồi chính là g_object, biến này sau đó được một loạt các hàm khác sử dụng.

Bắt đầu phân tích hàm tại 0x4027a0, sau phần khởi tạo dữ liệu g_object tại hàm 0x402090, có một đoạn code sử dụng biến g_object ở trên trông khả nghi.

Trong số các hàm sub_401E50, sub_4024E0, sub_4022A0, phần code của hàm sub_4024E0 trông khả nghi nhất khi có một loạt lệnh kiểm tra một mảng với các giá trị ‘L’, ‘R’, ‘D’, ‘U’, đồng thời có một đoạn text được so sánh với chuỗi tại biến this + 92, this ở đây chính là biến g_object và giá trị this + 92 là chuỗi “gimme flag pls?” được khởi tạo ở trên. Các kí tự ‘L’, ‘R’,.. có thể là các kí tự ứng với các phím mũi tên Trái (Left), Phải (Rigth), Xuống (Down), Lên (Up). Từ đây có thể đoán được yêu cầu là các phím mũi tên cần được bấm theo thứ tự nhất định cùng với chuỗi nhập vào phải là “gimme flag pls?” thì chương trình sẽ xử lí dữ liệu qua các hàm sub_401220, sub_401A10 để in ra flag.

Lưu lại thứ tự các kí tự mũi tên được kiểm tra, ta được dãy: “LLURULDUL”, thử bấm các phím mũi tên theo dãy này và nhập chuỗi “gimme flag pls?” và nhấn enter thì được flag:

04_darn_mice

Đề bài cho file darn_mice.exe, thử chạy trên cmd thì không thấy thông tin gì, sau đó thử thêm với tham số ngẫu nhiên thì thấy có thông báo nhiều khả năng là flag sai.

Load file lên ida, thấy hàm chương trình cần có 2 tham số, main gọi hàm sub_401000 và truyền vào argv 1 là tham số thứ nhất của chương trình để xử lí.

Hàm sub_401000 sau khi khởi tạo giá trị cho mảng v5, sẽ kiểm tra độ dài của argv[1] nếu = 0 hoặc > 0x23 (35) thì sẽ in ra thông báo “No, nevermind.” và kết thúc. Khi thực thi với tham số ngẫu nhiên, chương trình chỉ chạy đến đoạn code in ra màn hình thông báo “You leave the room, and a mo…”, còn đoạn code cuối in ra chuỗi “when you return…” không được thực thi.

Nhận thấy có một đoạn code vòng lặp for, trong đó, sẽ thực thi shellcode tại biến v2. Ta thấy chỉ có 1 phần tử của v2 được thiết lập thông qua phép toán dựa theo biến v5 được thiết lập sẵn và biến Str là giá trị tham số được truyền vào.

*(_BYTE *)v2 = Str[i] + v5[i];  v2(v2);

Đến đây nghĩ đến việc để chương trình có thể tiếp tục chạy thì shellcode phải hợp lệ và đảm bảo rằng, sau khi thực thi shellcode, chương trình có thể tiếp tục thực thi. Với việc shellcode chỉ chứa 1 byte duy nhất và sau khi thực hiện lệnh đó, phải quay về được vị trị hiện tại để tiếp tục thực thi. Sau khi test thử một vài lệnh và ko thành công, để ý đoạn sau có chuỗi “when you return…”, nghĩ đến lệnh ret với opcode là 0xc3, nghĩa là Str[i] + v5[i] = 0xc3. Thử debug và fix opcode thành ret để kiểm tra. Ở hình dưới đây, khi vùng nhớ của shellcode được khởi tạo ở địa chỉ 0x00c00000, opcode được sửa thành 0xc3, khi thực thi lệnh call, shellcode push địa chỉ tiếp theo vào stack và sau đó thực thi lệnh ret -> quay trở lại được luồng thực thi lúc đầu.

Tiến thành thử code đoạn mã để lấy tất cả giá trị Str[i] = 0xc3 – v5[i].

Compile và thực thi, ta thu được chuỗi cần tìm là “see three, C3 C3 C3 C3 C3 C3 C3! XD”. Thử truyền chuỗi này vào chương trình gốc thì thu được flag

05_t8

Bài 5 gồm có 2 tập tin, t8.exe và traffic.pcapng. Dựa vào đây có thể đoán hướng làm là kiểm tra xem tập tin pcapng chứa các thông tin gì, có thể đây là các thông tin mà t8.exe tạo ra. Để có được flag thì cần phải tìm hiểu xem t8.exe tạo ra các thông tin đó như thế nào và tìm cách giải mã chúng. Kiểm tra tập tin pcapng trước, thấy có 2 stream HTTP được gửi đi đến máy chủ flare-on.com. 2 stream có format giống nhau, các thông tin gồm phương thức POST, UserAgent, data gửi đi ở dạng widechar base64, data nhận lại ở dạng ansi base64. Thử decode các giá trị base64 này thì nhận được dữ liệu khó hiểu nên có thể coi đây là dữ liệu đã được mã hóa trước khi chuyển sang base64.

Load t8 vào IDA, gặp ngay đoạn code đầu tiên kiểm tra điều kiện nếu không thỏa mãn sẽ ngủ trong 43200000 milisecond = 12h khá củ chuối. Đoạn code sử dụng dữ liệu từ biến xmmword_45088C.

Xem xref của các địa chỉ xmmword_45088C, tìm được hàm sub_401020 khởi tạo giá trị này. xmmword_45088C được gán bằng thời gian hiện tại của chương trình lúc thực thi.

Search thử các hằng số 356.25, 1524.5, 29.53,.. trong hàm sub_404570, sau một hồi mò thì tìm thấy tài liệu mô tả về cách tính chu kì mặt trăng. Do đó biết được hàm này nhận đầu vào là dữ liệu thời gian, tính ra ngày hiện tại trong một chu kì mặt trăng. Đoạn code vòng lặp for kiểm tra thời điểm thực thi có đúng vào ngày thứ 15 trong chu kì mới của mặt trăng hay không.

Trong lúc debug điều kiện này không thỏa mãn nên cách bỏ qua đoạn check là sửa cờ ZF hoặc patch lệnh jz. Đoạn code tiếp theo không có gì đặc biệt chương trình dùng phép xor để decrypt 1 đoạn dữ liệu, sau khi debug qua thì ta biết được dữ liệu này là chuỗi widechar “flare-on.com“.

Sau khi khởi tạo chuỗi “flare-on.com“, chương trình tiếp tục gọi hàm sub_4034C0. Theo dõi trong IDA thì thấy được hàm này giống với việc khởi tạo một object C++ với nhiều trường được thiết lập các giá trị ban đầu. Một số trường dựa vào đọc code ida và debug ta biết được:

  • this + 0: con trỏ tới vftable, chứa danh sách các hàm của object
  • this + 20: được gán bằng con trỏ trỏ tới chuỗi “flare-on.com“. Vì this đang ép kiểu là (char*) nên offset thực tế của trường này là 20xsizeof(char) = 20×1 = 20.
  • this + 17: trường này được gán bằng một con trỏ trỏ tới vùng nhớ có kích thước 2048 byte. Vì đang ép kiểu là (DWORD*) nên offset thực tế của trường này là 17xsizeof(DWORD) = 17×4 = 68.

Để dễ đọc hơn, ta tạo struct chứa các hàm vftable, kết quả là struct tên struc_44B918 được tạo.

Sau đó, tạo một struct mới tên là client mô phỏng lại object này, trường đầu tiên là con trỏ tới vftable, các trường chưa biết để trong các mảng, ta tính được các offset đến các trường đã biết và biết được kích thước của struct này là 0x4c=76 byte dựa theo phần memory được cấp ở trên. Tạo struct này trong cửa sổ LocalType của IDA, struct ban đầu có dạng:

struct client

{

struct struc_44B918 *vftable; // +0

char data_unknow[16];         // +4   

char *host_flare_on;          // +20

char data_unknow2[44];        // +24

char *buf;                    // +68

char data_unknow3[4];

};

Áp dụng struct này vào biến this, ta được kết quả như hình:

Để tiện theo dõi, ta sửa tiếp các trường của struct struc_44B918, tên các trường trùng với tên các hàm.

Tiếp tục phân tích đoạn code tiếp theo. Trong phần code này, chương trình tạo chuỗi POST dạng widechar và truyền vào hàm vftable->sub_403770 của client object. Hàm sub_403770 có tác dụng copy chuỗi POST vào this + 4. Ta biết POST là tên phương thức HTTP, do đó có thể đoán trường tại offset + 4 này sẽ chứa HTTP method, method dài nhất có thể là OPTIONS (7 kí tự), có kích thước 16 byte (bao gồm cả NULL) ở dạng widechar. Struct client được sửa lại dạng như dưới đây. Vừa phân tích code, debug xem các giá trị, dần dần sẽ hiểu được ý nghĩa các trường khác.

struct client

{

struct struc_44B918 *vftable; // +0

WCHAR method[8];              // +4   

char *host_flare_on;          // +20

char data_unknow2[44];        // +24

char *buf;                    // +68

char data_unknow3[4];

};

Ở phần code tiếp theo, sau khi debug qua, ta biết được chương trình dùng hàm sub_4025B0 để chuyển giá trị lưu tại dword_450870 sang dạng chuỗi số thập phân và nối chuỗi này với chuỗi được lưu tại dword_450874, sau đó chuỗi kết quả sẽ được copy vào biến mới v30. Chuỗi này có kiểu là widechar.

2 giá trị dword_450870 và dword_450874 được khởi tạo tại hàm sub_401020. Trong đó dword_450874 được khởi tạo là chuỗi “FO9”, còn dword_450870 được khởi tạo bằng một giá trị ngẫu nhiên dựa trên thời điểm thực thi của ứng dụng.

Sau đó chương trình gọi hàm sub_4037A0. Để ida decompile đúng thì cần Set call type cho lệnh call này theo prototype của hàm sub_4037A0. Nhận thấy chương trình truyền vào cho hàm sub_4037A0 chuỗi v30 (FO9xxx) để xử lí.

v6->vftable->sub_4037A0((char *)v6, (_DWORD *)v30);

Hàm sub_4037A0 tiếp tục truyền FO9xxx vào một hàm khác là sub_403910 xử lí, kết quả sau khi xử lí được lưu tại v4, sau đó, v4 này sẽ được copy vào data_unknow2[20] trong struct client.

Kiểm tra hàm sub_403910, phát hiện một số hằng số của thuật toán MD5 (1732584193, 4023233417, 2562383102,…), đồng thời ở cuối hàm có gọi tới hàm sub_402550 có cách sử dụng giống với hàm swprintf() với tham số kích thước của chuỗi là 0x21=33 kí tự có thể tương đương với 32 kí tự của md5 bao gồm cả null char. Kết hợp với debug thì xác nhận rằng sub_403910 tính md5 của chuỗi FO9xxx và trả về kết quả là md5 ở dạng widechar.

Quay trở lại luồng thực thi chính, sau khi tính md5 của chuỗi FO9xxx, chương trình tiếp tục khởi tạo chuỗi widechar “ahoy” và truyền chuỗi này vào cho hàm sub_403D70. Bên trong sub_403D70 chương trình gọi tới 2 hàm khác trong vftable của object client là hàm sub_403860 và sub_403860. Trong quá trình debug, thấy được hàm sub_403860 sẽ mã hóa chuỗi widechar “ahoy” bằng thuật toán rc4 với khóa là widechar md5_FO9. Có thể xác định được mã hóa dùng rc4 là vì rc4 sử dụng các vòng lặp với số lần là 256, đồng thời kích thước key cũng là 256 byte.

Còn hàm sub_403C20 sẽ chuyển dữ liệu sau khi mã hóa sang dạng chuỗi widechar base64 và lưu lại kết quả vào biến lpOptional. Base64 được xác định dựa trên việc sử dụng chuỗi kí tự “ABCDEFGHIJKL….0123456789+/” và các padding “=” được sử dụng trong hàm sub_4014C0. Một cách khác để xác định các hàm rc4, base64 hoặc chức năng của các hàm chưa biết là debug và kiểm tra input/output sau khi các hàm thực thi xong.

Tiếp theo trong hàm sub_403D70, chương trình gọi ObtainUserAgentString để tạo chuỗi useragent, sau đó, useragent sẽ được nối thêm chuỗi số thập phân ngẫu nhiên sinh ra từ dword_450870 hoặc chuỗi widechar “CLR” dựa vào tham số truyền vào. Trong trường hợp này khi debug, chuỗi được thêm vào là chuỗi số thập phân ngẫu nhiên.

Sau đó, chương trình sẽ tạo kết nối HTTP tới host flare-on.com với method là POST, dữ liệu gửi lên nằm ở biến lpOptional. Dữ liệu từ server trả về được lưu vào biến buf trong object client, kích thước của dữ liệu được lưu vào 4 byte cuối cùng trong struct client.

struct client

{

struct struc_44B918 *vftable;

WCHAR method[8];

char *host_flare_on;

char data_unknow2[20];

WCHAR *md5_FO9;

char data_unknow22[20];

char *buf;

int bufsize;

};

Khi nhận đủ dữ liệu, chương trình quay trở lại luồng thực thi chính và tiếp tục thực hiện hàm sub_404200. Hàm sub_404200 gọi tiếp hàm sub_4043F0. Dựa vào dữ liệu trong file pcap, ta biết được dữ liệu server trả về có dạng base64. Kết hợp với việc debug, thấy hàm sub_4043F0 khi thực thi sẽ gọi hàm sub_403CE0 giải mã dữ liệu base64 trong biến buf của client. Dữ liệu sau khi giải mã base64 sẽ được giải mã tiếp bằng rc4 với khóa là chuỗi widechar md5_FO9 bằng hàm sub_403860 trong vftable, tương tự lúc mã hóa dữ liệu trước khi gửi đi.

Phân tích đến đây, ta đã biết được cách dữ liệu của gói tin đầu tiên được mã hóa và giải mã ra sao:

Do đó, có thể tiến hành giải mã stream đầu tiên trong file pcapng bằng cách sử dụng fakenet để gửi dữ liệu từ file pcapng đến chương trình qua giao thức http. Dữ liệu response trả về mặc định cho giao thức http của fakenet nằm tại file defaultFiles/FakeNet.html:

Sau đó patch dữ liệu số ngẫu nhiên tại dword_450870 được khởi tạo tại hàm sub_401020 theo dữ liệu trong Useragent của file pcap (11950), để chương trình chạy và kiểm tra dữ liệu sau khi giải mã.

Khi chương trình thực thi qua hàm sub_4043F0, dữ liệu đầu ra trông có dạng không giống raw data lắm.

Tiếp tục debug thì thấy chương trình phân tách đoạn data sau khi giải mã bởi dấu “,” và dùng hàm sub_404570 để tính ngày trong chu kì mặt trăng mới từ dữ liệu trong từng đoạn data. Sau đó chương trình dùng ngày này làm index để lấy các kí tự trong các mảng word_44B842 và word_44B840 tại hàm sub_4041E0.

Sau khi xử lí hết các dữ liệu, ta có chuỗi “i_s33_you_m00n”. Tiếp tục debug, chương trình nối chuỗi trên với host flare-on.com để tạo thành flag hoàn chỉnh. Nếu tiếp tục phân tích, ta sẽ thấy chương trình thiết lập lại key của rc4 bằng widechar md5 của flag để thực hiện mã hóa và giải mã trong các lần tiếp theo.

Sửa dữ liệu trả về của fakenet và để chương trình tiếp tục thực thi. Dữ liệu sau khi giải mã là một đoạn shellcode và ta nhận được thông báo như hình.

06_alamode

Đề bài cho 1 tập tin HowDoesThisWork.dll và một file chat log. Kiểm tra file dll với CFF thì thấy đây là file dotnet 32bit, tiến hành load vào dnspy, tìm được một hàm GetFlag có nhiệm vụ nhận tham số là password, gửi tham số để NamedPipe có tên “FlareOn” sau đó trả về chuỗi output từ Pipe.

Sau nhiều lần thử dùng rundll32, regsvr32 để load thử file dll này và monitor thì đều bị báo lỗi, thử dùng strings để check xem có gì hay ho không thì phát hiện có nhiều tên hàm Native API (GetProcAddress, LoadLibraryExW,…). Search thử tài liệu về dotnet, tìm thấy bài nói về format của dotnet file có 1 đoạn nhắc đến Native code.

Check lại header của file dll thì thấy có native entrypoint thật.

Vậy là có thể dùng x32bdg để debug đoạn code native này. Tiến hành sửa entrypoint này thành EBFE và dùng lệnh “rundll32 HowDoesThisWork.dll,xxx” (xxx là một chuỗi bất kì, cho câu lệnh hợp lệ) để chương trình chạy và attach x32dbg vào. Sau khi attack debuger, sửa lại 2 lệnh của entrypoint về giá trị ban đầu (55 8b) để debug chương trình.

Tiếp tục load file dll này vào ida, chọn format là PE file thay vì Microsoft.Net assembly, nhảy tới RVA 0x181a để bắt đầu đọc code. Xem tiếp hàm sub_100016E4, có một đoạn code gọi tới hàm sub_10001163. Hàm sub_10001163 gọi tới hàm sub_100012F1 sau đó thực hiện lệnh call tới dword_10015a30 với tham số là hàm sub_10001094. Sau đó hàm sub_100012F1 thực hiện khởi tạo giá trị cho một loạt dword khác nhau. Khi debug qua, thấy các hàm này có tác dụng giải mã string, lấy địa chỉ các api và gán cho các biến toàn cục. Các string được giả mã bằng cách XOR từng kí tự với hằng số 0x17.

Dùng script idapython để tự động decrypt các chuỗi và đổi tên các biến. Ta biết chương trình sẽ tạo một thread mới chạy hàm sub_10001094.

Hàm sub_10001094 có nhiệm vụ tạo NamedPipe “\.\pipe\FlareOn”, kết nối tời pipe này, đọc nội dung từ pipe và truyền dữ liệu đọc được vào hàm sub_10001000 sau đó sẽ được ghi lại vào pipe.

Trong hàm sub_10001000, tiếp tục phát hiện thuật toán RC4 được sử dụng trong hàm sub_100011EF và sub_10001187 với khóa được thiết lập là 8 byte tại địa chỉ 0x10015000 (558BEC83EC20EBFE). Chương trình giải mã 9 byte tại 0x10015028 (kết quả là chuỗi “MyV0ic3!”) và so sánh kết quả với giá trị đọc được tại pipe. Nếu giống nhau, chương trình sử dụng trạng thái rc4 trước đó để giải mã tiếp 31 byte dữ liệu tại địa chỉ 0x10015008.

Thử giải mã tất cả 40 byte dữ liệu trên với key rc4 ở trên thì thu được flag.

Note

Trong lúc debug với rundll32 thì bị crash, đó là do chương trình không lấy được hàm CreateNamedPipeA trong module kernelbase.dll, đúng ra thì chương trình cần tìm các hàm trong module kernel32.dll.

Điều này là do chương trình thực hiện lấy địa chỉ module thứ 4 trong danh sách PEB.InMemoryOrderModuleList.

Nếu thông thường, trong cách tiến trình native (không phải dotnet), danh sách PEB.InMemoryOrderModuleList có thứ tự là [current module].exe -> ntdll.dll -> KERNEL32.DLL -> KERNELBASE.dll thì trong một tiến trình dotnet, thứ tự của danh sách sẽ là [current module].exe -> ntdll.dll -> mscoree.dll -> KERNEL32.DLL -> KERNELBASE.dll.

Do đó để chương trình thực thi được thì cần một loader 32 bit bằng .net framework. Khi đó, phần code native sẽ được thực thi trong một thread mới và tạo ra cổng “\.\pipe\FlareOn”, phần code c# được gọi qua loader sẽ gửi dữ liệu đến cổng này và nhận về flag nếu input đúng (“MyV0ic3!”).

07_anode

Đề bài cho 1 tập tin anode.exe 64bit với kích thước > 50mb, khi thực thi, chương trình yêu cầu nhập flag và đưa ra thông báo nếu flag bị sai. Check thử một số thông tin bằng cff và strings thì thấy nhiều khả năng đây là code của nodejs được build sang dạng file exe giống như kiểu pyinstaller convert .py sang exe.

Đặc biệt là cuối file có một đoạn script rất dài bắt đầu từ offset 0x35dfa00, ở cuối file có kèm theo chuỗi “<nexe~~sentinel>”. Dựa vào các thông tin này, có thể xác định được chương trình dùng nexe để convert từ file js của node sang file exe.

Dump đoạn script từ offset 0x35dfa00 ra, thử chạy bằng nodejs thông thường thì báo lỗi, sau đó tìm được gói nexe_unpacker. Script sau khi extract ra thì bắt đầu từ đoạn offset 0x35E3806, còn đoạn script ở trên từ 0x35dfa00 là base script dùng để khởi tạo môi trường thực thi. Đọc đoạn script thì thấy hàm check độ dài của flag là 44 kí tự, script sau khi dump ra có thể chạy được bằng nodejs nhưng output thì không giống như chương trình gốc khi điền đủ 44 kí tự.

Thông thường thì 1n sẽ luôn > 0, do đó script nhúng trong file anode.js chắc chắn được thực thi bằng cách nào đó để vượt qua được đoạn code này. Đọc đoạn code extract ra từ nexe_unpacker, thấy sau khi nhận flag từ input, chương trình lưu giá trị của từng kí tự vào mảng b. Mảng b sau đó được xử lí qua vòng lặp while(true) với từng case (tổng cộng 1024 case) được quyết định bởi biến state, biến state cuối cùng để thoát vòng lặp là 185078700. Sau khi thoát khỏi vòng lặp dữ liệu trong mảng b sẽ được so sánh với mảng target. Vì vậy có thể hình dùng là chuỗi flag nhập vào sau khi xử lí bởi vòng while true kia sẽ có kết quả như ở mảng target.

Trong suốt quá trình thực thi, giá trị của hàm Math.random() được sử dụng liên tục để tính toán. Và giống như việc 1n luôn > 0, script thực thi bằng nodejs thông thường sẽ luôn cho ra các giá trị ngẫu nhiên nên kết quả của mảng b sau khi xử lí sẽ khó có thể giống nhau trong mỗi lần thực thi được.

Vì thế nên ta tìm cách để ghi lại các giá trị Math.random() sinh ra khi file anode.exe gốc được thực thi. Thấy script được nhúng thẳng ở cuối file, nên thử tìm cách để chèn thêm lệnh console.log của js vào đoạn script ban đầu để lưu lại các giá trị Math.random(). Thử chèn thêm lệnh log vào để test thì báo lỗi, trông giống như script bị sai format, syntax gì đó có thể do kích thước script bị thay đổi.

Thử thêm một đoạn code vào thay cho chuỗi comment ở trên thì kết quả khả quan hơn, script thực thi được. Vậy là có thể chèn script vào để thực thi, tuy nhiên phải đảm bảo format của script chèn vào hợp lệ.

Sau khi search thử một hồi về cách hoạt động của nexe, gặp được link này nói một cách tổng quát nexe_output = node + js file, kèm theo đó là đoạn code chèn js file vào binary node. Đại khái là output sẽ bao gồm:

  • binary: file exe node được build
  • code: đoạn code js base, khởi tạo môi trường thực thi.
  • bundle: phần code js chính
  • signature: gồm chuỗi <nexe~~sentinel> + kích thước code (double) + kích thước script js chính (double)

Ngoài ra thì phần code js base có một đoạn mô tả các script được nhúng, bao gồm cả kích thước của chúng:

Đến đây có thể viết 1 đoạn python script để chèn script js bất kì vào file anode.exe gốc ban đầu, các bước thực hiện là:

  • đọc file pe anode gốc.
  • đọc script cần chèn, lưu lại kích thước của script sang 2 dạng:
    • chuỗi số thập phân để chèn vào đoạn script base
    • double 8byte để lưu vào signature
  • Sửa phần kích thước trong đoạn script base với giá trị vừa lưu ở trên
  • lưu lại kích thước của script base ở dạng double
  • tạo ra đoạn signature nexe gồm chuỗi ‘<nexe~~sentinel>’ + base_script_size_double + script_size_double
  • ghi dữ liệu ra file mới gồm: file pe gốc + đoạn script base + script cần chèn + đoạn signature của nexe

Sau khi có thể chèn script tùy ý vào để thực thi, ta đã có thể log lại được hết các giá trị của Math.random() cũng như các giá trị của biến state kiểm soát vòng lặp.

Hướng để tìm flag thì vẫn chưa có, một số cách đã thử mà không thành công là:

  • chuyển đoạn code xử lí input từ mảng b sang python, thay thế các giá trị math.random = giá trị log lại được rồi dùng z3 để tìm b: cách này được mọi người đi trước bảo z3 ngáo không ra được đâu nên bỏ qua luôn.
  • cho input = 1 mảng [0x00 hoặc 0xff] 44 kí tự: output cũng vô nghĩa.
  • cho input = target và chạy lại script: kết quả output khá vô nghĩa.
  • cho input = target và chạy script theo chiều ngược lại của thứ tự các state kiểm soát switch case (ví dụ ban đầu: case 306211 -> case 311489 -> case 755154 thì chạy từ case 755154 -> case 311489 -> case 306211): kết quả cũng ko ra gì.

Trong khi bế tắc thì được một người tiền bối cho biết là làm ngược lại là được. Vậy là cách chạy ngược lại trên bị sai ở đâu đó. Sau khi nhiều lần thử đi thử lại thì có vài lưu ý khi chạy ngược lại:

  • Chỉ cần đảo ngược lại cả các phép toán “+=”, “-=”. Đối với các phép toán +, -, &, ^ còn lại giữ nguyên.
  • Một số đoạn code có sử dụng giá trị của Math.random() trong lệnh điều kiện, khi đó, lúc chạy ngược lại cần đưa giá trị Math.random() trong hàm so sánh trước.
  • Giá trị của state cuối cùng để break khỏi vòng lặp while true.

Cách để chạy ngược lại có thể khái quát như sau:

  • Cho chạy vòng lặp while true lần 1, lưu lại thành các mảng giá trị của các biến state, giá trị random dùng trong phép so sánh, giá trị random bình thường.
  • Gán input = target
  • copy lại vòng lặp while true, sửa giá trị break của vòng lặp thành giá trị đầu tiên của state ban đầu
  • với từng giá trị của biến state, giá trị random dùng trong phép so sánh, giá trị random bình thường, sẽ lấy ra từ mảng đã lưu ở vòng lặp đầu tiên theo chiều ngược lại.

Sau khi tạo js script chạy ngược lại như vậy, chèn lại vào file anode.exe ban đầu và chạy script ta được flag.

 

By,

RE Team

1.231 lượt xem