Đây là lần thứ hai mình tham gia, và là lần đầu tiên mình giải được đủ các challenge của Flare-on CTF, nên cảm thấy rất vui. Vui vì cuối cùng mình cũng có được cái huy hiệu của Flare-on, nhưng quan trọng hơn cả là mình cảm thấy trong một năm qua, mình đã tiến bộ hơn nhiều.
Sau hơn một tháng ngồi làm các challenge này, mình có một số cảm nhận, cũng như một số mẹo nhỏ cho bạn đọc:
Cấu trúc file PE là kiến thức mà mọi người chơi Reverse Engineer và Malware Analyst nên học. Ví dụ như ở bài 2 và bài 11, mình đã vận dụng kiến thức về file PE để có thể sửa lại file bị corrupt.
Reflective DLL Injection, cũng giống như trên, là một kỹ thuật được dùng nhiều trong malware, vì nó khó bị detect hơn so với các phương thức inject khác, kỹ thuật này được sử dụng ở bài 9, và bài 11.
windbg là một debugger rất mạnh, có thể script nữa. Thật sự mà nói, trong khoảng thời gian chơi RE gần 2 năm, mình chưa bao giờ dùng windbg, mình toàn coi tutorial RE của anh kienmanowar rồi dùng x64dbg vì nó có giao diện đẹp, bấm chuột click click, dễ xài hơn. Ở bài 9, mình phải tập sử dụng windbg để debug window kernel và lấy password (đây cũng là lần đầu tiên mình debug windows kernel).
Khi làm các công việc lặp đi lặp lại nhiều lần, hãy viết script nếu có thể. Mình đã viết script cho windbg, gdb ở bài 11, bài 9 và bài 10, giúp tiết kiệm rất nhiều thời gian.
"Khi bạn đã nhìn vào một thứ quá lâu mà vẫn không thấy gì, hãy lùi lại một bước để nhìn nó ở một góc độ khác" - anh Mạnh Luật (l4w) said. Điều này mình thấy rất đúng. Mình đã dùng tới tận 12 ngày để giải bài cuối cùng, nhưng trong đó, 11 ngày đầu, mình chỉ ngồi RE một đống DLL. Nhận thấy cách làm này không ổn, mình đã chuyển sang góc nhìn khác :arrow_right: dùng procmon để quan sát mọi thứ. Và, … mình chỉ tốn chưa tới 12 tiếng để tìm ra flag cho bài cuối ! (Nếu dùng wireshark bạn đọc cũng sẽ thấy được program làm gì đó, từ đó trace ngược lại và tìm ra flag).
Khi gặp source code bị obfuscate, hãy cố gắng tìm các pattern trong code, và dùng regex để rename các biến, các hàm … thay vì dùng find and replace (bài 6).
Monitor các hàm API để hiểu được flow program cũng là một cách hay, mình đã dùng cách này ở bài 7, và bài 10.
Khi RE các hàm lớn, có thể nhìn input và output để đoán xem hàm đó là gì.
Khi RE các hàm có rất nhiều phép xor, rotate, shift, khả năng cao đó là hàm của một thuật toán crypto hoặc hàm hash nào đó. Dùng plugin findcrypt trong IDA hoặc google các hằng số tìm thấy trong program để biết được hàm đó là gì.
Cuối cùng, xin cảm ơn Fireeye đã tổ chức một kỳ CTF rất chất lượng cho những người chơi Reverse Engineer ! Đồng thời mình cũng cảm ơn tất cả các bạn đọc đã bỏ ra thời gian quý giá để đọc tới đây, hi vọng mọi người sẽ thích bài này và ủng hộ các bài blog mà mình sẽ viết trong thời gian tiếp theo ^^!
–Trung–
Cùng đến với phần cuối cùng của hành trình bằng 3 thử thách:
What kind of crackme doesn't even ask for the password? We need to work on our COMmunication skills.
Ở bài này, chúng ta lại có 1 file .exe. Ta mở file lên trong IDA, nhảy thẳng tới hàm main, hàm này khá đơn giản, chỉ làm nhiệm vụ drop 1 file dll ra “C:\Users<name>\AppData\Local\Microsoft\Credentials\credHelper.dll”, sau đó load dll này và gọi hàm DllRegisterServer.
Vậy là file này khá đơn giản, giờ ta chuyển sang phân tích hàm DllRegisterServer của credHelper.dll
Hàm ở trên sẽ lấy data trong registry key “Password”, biến đổi nó trước khi dùng để làm RC4 key. Ở trên ta có thể thấy pattern "do *v6++ = v9++; while (v9 < 256);".
Vậy là ta đã biết cách để lấy được Flag, nhưng … không có Password.
Mình đã ngồi phân tích file credHelper.dll rất lâu nhưng không thấy chỗ nào tạo Password, đến đây mình quay lại xem file crackinstaller.exe để xem lại thì phát hiện ra mình đã bỏ sót các hàm Constructor.
Constructor là các hàm được gọi trước cả hàm main. Ví dụ, cho đoạn code sau:
1 2 3 4 5 6 7 8 9 10 11
#include<stdio.h> classTest { public: Test() { printf("Hello world from test\n"); } }; Test test; intmain(){ printf("Hello world\n"); }
Và output của chương trình trên là:
1 2
Hello world from test Hello world
Đó là một trong những trường hợp mà tồn tại hàm được gọi trước hàm main.
Như trên thì crackinstaller được load tại 0x00000013fd30000, ta vào IDA lấy offset:
1
.text:0000000140002E1A call cs:DeviceIoControl
Trong lúc viết bài này, mình có tắt windbg và bật lại nhiều lần nên Imagebase có thể thay đổi, tuy nhiên offset thì không !
Imagebase là 0x140000000 nên offset sẽ là 0x2E1A. Ta gõ “bp 000000013fd30000+0x2E1A” để đặt breakpoint ngay tại đây, sau đó dùng lệnh g để tiếp tục chương trình, lúc này @rip đang ở ngay tại 0x00000013fd32e1a. Ta gõ “u poi(@r8)” để disassemble đoạn shellcode này.
Ta gõ “.breakin” để switch từ user-mode debugging vào kernel-mode debugging, rồi gõ “bp cfs+573”, sau đó ở crackinstaller ta gõ lệnh “g” thì ta sẽ dừng laị tại cfs+573.
1 2 3 4 5 6 7 8 9 10 11
kd> g 0:000> g Breakpoint 2 hit cfs+0x573: fffff880`06530573 ff542428 call qword ptr [rsp+28h] kd> u poi(@rsp+28) 00000000`00070008 fb sti 00000000`00070009 48ba2092300000000000 mov rdx,309220h 00000000`00070013 41b800580000 mov r8d,5800h 00000000`00070019 41b970310000 mov r9d,3170h 00000000`0007001f ff2500000000 jmp qword ptr [00000000`00070025]
Hàm này thực hiện 1 việc rất giống Reflective DLL load, và đúng là như vậy, đoạn code trên được copy từ đây, reflective driver loader (trong repo này cũng sử dụng lại capcom.sys để làm ví dụ).
Ta thấy sau khi thực hiện load và fix bảng IAT, reloc table, thì nó gọi IOCreateDriver để gọi DriverEntry của file PE thứ 2.
Driver này sẽ theo dõi những thay đổi trong Registry, nếu trong path của key có chứa “{CEEACC6E-CCB2-4C4F-BCF6-D2176037A9A7}\Config” thì đoạn code trên sẽ được thực hiện.
Ta sẽ tìm cách đặt breakpoint tại hàm này. (Hiện tại ta vẫn đang ở đầu của hàm DriverBootstrap).
Ta dùng lệnh “bp nt!CmRegisterCallbackEx” , sau đó bấm “g”, ta sẽ dừng ngay đầu hàm này.
Tiếp theo ta gõ “bp @rcx”, đây chính là địa chỉ hàm callback.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
kd> bp nt!CmRegisterCallbackEx kd> g Breakpoint 5 hit nt!CmRegisterCallbackEx: fffff800`02ad0d30 4883ec38 sub rsp,38h kd> u @rcx fffffa80`045e5570 4c89442418 mov qword ptr [rsp+18h],r8 fffffa80`045e5575 4889542410 mov qword ptr [rsp+10h],rdx fffffa80`045e557a 48894c2408 mov qword ptr [rsp+8],rcx fffffa80`045e557f 56 push rsi fffffa80`045e5580 57 push rdi fffffa80`045e5581 4881ecf8030000 sub rsp,3F8h fffffa80`045e5588 488b842420040000 mov rax,qword ptr [rsp+420h] fffffa80`045e5590 4889442458 mov qword ptr [rsp+58h],rax kd> bp @rcx
Ta bấm “g” để chạy và dừng ngay tại hàm callback.
Giờ ta chỉ quan tâm tới lời gọi “ZwCreateKey” ở trong đoạn if nên: “bp @rip+0x56d, sau đó bấm “g”.
1 2 3 4
0:000> g ModLoad: 000007fe`fb640000 000007fe`fb660000 C:\Users\admin\AppData\Local\Microsoft\Credentials\credHelper.dll Breakpoint 2 hit fffffa80`045acadd ff15c5050000 call qword ptr [fffffa80`045ad0a8]
Tham số thứ 5 của hàm ZwCreate là những gì mà ta quan tâm, nên:
Yay, đây cũng chính là Password, giờ ta chỉ việc viết 1 chương trình nhỏ để gọi hàm decrypt flag trong credHelper.dll là xong. Ở đây mình không xài các hàm COM mà dùng thẳng vtable như trong C++ luôn.
puts("welcome to the land of sunshine and rainbows!"); puts("as a reward for getting this far in FLARE-ON, we've decided to make this one soooper easy"); putchar(10); printf("please enter a password friend :) "); buf[read(0, buf, 0xFFu) - 1] = 0; if ( sub_8048CDB(buf) ) printf("hooray! the flag is: %s\n", buf); else printf("sorry, but '%s' is not correct\n", buf); exit(0); }
Nhưng vẫn không đúng, ta vẫn bị “steal input”. Ta dùng tool strings và grep để tìm chuỗi “sorry i stole your input :)”:
Không tìm được chuỗi nào như vậy, có thể chuỗi này được build trên stack, nên không thể tìm thấy bằng tool strings.
Ở bài này mình lại quên mất 1 điều là: trước khi chạy hàm main thì program sẽ chạy các hàm Constructor. Để vào được tới hàm main, ở bài này có giải thích khá rõ. Để tóm tắt lại thì, mình để cái hình ở đây, cũng khá là dễ hiểu:
Ta xem code của hàm _init:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
void __cdecl init() { int v0; // esi int v1; // edi
init_proc(); v0 = &off_81A4F04 - funcs_8056364; if ( v0 ) { v1 = 0; do funcs_8056364[v1++](); while ( v1 != v0 ); } }
int __cdecl sub_80490C4(__pid_t parrent_pid) { // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
v50 = 0; if ( call_ptrace_dynamic(PTRACE_ATTACH, parrent_pid, 0, 0) == -1 ) { v49 = sub_804BD69(parrent_pid); // return "TracerPid" in "/proc/pid/status" if found if ( v49 ) { if ( call_ptrace_dynamic(PTRACE_ATTACH, v49, 0, 0) == -1 ) { kill(v49, SIGKILL); result = kill(parrent_pid, 9); } else { while ( 1 ) { result = waitpid(v49, &v16, 0); if ( result == -1 ) break; v35 = v16; if ( (unsigned __int8)v16 == 127 ) { v36 = v16; v48 = (v16 & 0xFF00) >> 8; if ( v48 != 19 && v48 != 17 ) call_ptrace_dynamic(7, v49, 0, v48); else call_ptrace_dynamic(7, v49, 0, 0); } } } } // truncated .... }
Hàm này nhận 1 tham số là pid của process cha. Đoạn code trên kiểm tra xem process cha có đang bị process khác trace (debug) không. Có thể đây là 1 kỹ thuật anti-debug.
Ở đoạn này, process con write giá trị (32 bit) 0xB0F vào địa chỉ 0x8048CDB của process cha, sau đó nó lại tạo ra thêm 1 process con nữa bằng hàm 0x804A0B4 (hàm này gọi tiếp hàm 0x8049C9C):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
__pid_t __cdecl sub_8049C9C(__pid_t pid)// <-- this func receives 1st child's PID { // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
prctl(4, 0, 0, 0, 0); signal(SIGINT, (__sighandler_t)1); signal(SIGQUIT, (__sighandler_t)1); signal(SIGTERM, (__sighandler_t)1); if ( call_ptrace_dynamic(PTRACE_ATTACH, pid, 0, 0) != -1 ) { while (1) { // a lot of "if" statements here ... } } puts("OOPSIE WOOPSIE!! Uwu We made a mistaky wakey!!!!"); return kill(pid, 9); }
Sau khi process con tạo ra process con thứ 2, thì nó tiếp tục nhảy vào vòng lặp với rất nhiều câu lệnh if, switch:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
while ( 1 ) { result = waitpid(parrent_pid, &stat_loc, 0); if ( result == -1 ) break; v27 = stat_loc; if ( (unsigned __int8)stat_loc == 127 ) { v28 = stat_loc; if ( (stat_loc & 0xFF00) >> 8 == 19 ) call_ptrace_dynamic(31, parrent_pid, 0, 0); v29 = stat_loc; if ( (stat_loc & 0xFF00) >> 8 == 5 ) // .. truncated } // .. truncated .... }
Đến giờ ta có 1 số thông tin như sau:
Process con 1 gọi ptrace(PTRACE_ATTACH, ...) lên process cha.
Process con 2 gọi ptrace(PTRACE_ATTACH, ...) lên process con 1.
Ca 2 process con đều thực thi 1 vòng lặp while (1) với rất nhiều if , switch, và gọi rất nhiều ptrace ở trong vòng lặp đó.
PTRACE_ATTACH thường được gọi khi 1 debugger muốn debug process khác.
Mình tìm thấy trên mạng có một số bài viết rất hay về ptrace (nếu bạn chưa biết nhiều về hàm ptrace thì có thể đọc các bài dưới đây để hiểu thêm về nó):
Mình sẽ tóm tắt lại tác dụng của 1 số câu lệnh ptrace được gọi trong chương trình này ở dưới:
ptrace(PTRACE_ATTACH, pid, 0, 0): attach vào 1 process có pid là pid.
ptrace(PTRACE_POKEDATA, pid, where, val): giá trị val kiểu dữ liệu là long vào địa chỉ where của process có PID là pid.
ptrace(PTRACE_PEEKDATA), pid, where, 0): đọc giá trị long tại địa chỉ where của process có PID là pid.
ptrace(PTRACE_SETREGS, pid, 0, ®s): ghi đè thanh ghi của process có PID là pid bằng các giá trị lưu trong regs.
ptrace(PTRACE_CONT, pid, 0, 0): tương tự lệnh continue trong gdb.
ptrace(PTRACE_GETREGS, parrent_pid, 0, ®s): lấy giá trị các thanh ghi của process có PID là pid và lưu vào regs.
ptrace(31, ...): (ptrace(PTRACE_SYSEMU, ...)): khi gặp 1 syscall, đừng execute syscall đó mà để debugger xử lý. Tham khảo.
Đến đây ta có thể nhận ra:
Process 1 giống như debugger và nó điều khiển process cha.
Process 2 giống như debugger và nó điều khiển process con 1.
Bốn bài viết ở trên hướng dẫn cách viêt 1 debugger đơn giản, pattern của debugger này là:
1 2 3 4 5
while (1) { reason = recv_reason(pid); // Lấy lý do process bị stop process(pid); // xử lý (tiếp tục chạy, hoặc step 1 lệnh, đổi thanh ghi, thoát, ...) }
Để lấy “lí do”, ta có thể sử dụng 1 trong các hàm wait.
Ta cùng xem lại vòng lặp while (1) của process con 1:
Ta thấy sau khi lấy được stat_loc bằng hàm waitpid, chương trình sử dụng giá trị sau:
1
(stat_loc & 0xFF00) >> 8
Đoạn trên được tạo ra bởi macro WSTOPSIG, dùng để lấy “lí do” mà process dừng lại:
1 2 3 4 5 6
/* source: https://unix.superglobalmegacorp.com/Net2/newsrc/sys/wait.h.html file: sys/wait.h */ #define _W_INT(w) (*(int *)&(w)) /* convert union wait to int */ #define WSTOPSIG(x) (_W_INT(x) >> 8)
Vậy là tùy vào “lí do” mà process con 1 sẽ điều khiển process cha như thế nào.
Trước khi đi tiếp, ta cần hiểu ý nghĩa của một số kết quả trả về bởi WSTOPSIG:
SIGTRAP: process dừng lại vì nó vừa gặp phải lệnh syscall.
SIGILL: process dừng lại vì CPU phải thực thi 1 lệnh asm không hợp lệ.
SIGSEGV: process dừng lại vì nó vừa truy cập vùng nhớ mà nó không được phép (vd: truy cập vùng nhớ không được phép).
Vì process con 1 điều khiển process cha thông qua việc làm “debugger”, nên mình không thể debug program này. Tuy nhiên mình có 1 cách khác để log lại các hàm được gọi, sẽ được viết ở dưới.
Việc phân tích tĩnh khá cực, vì nó rất dễ nhầm lẫn, process con 1 lại còn bị “debug” bởi process con 2 nữa nên mình hoàn toàn không biết cách nào để debug chương trình này.
Tuy nhiên có 1 kỹ thuật LD_PRELOAD dùng để hook library function trên linux, bạn đọc có thể tham khảo tại đây.
Vì program này gọi ptrace rất nhiều nên việc đầu tiên mình làm là hook hàm ptrace. Tuy nhiên, program này không gọi trực tiếp ptrace, mà nó gọi thông qua con trỏ hàm:
1 2 3 4 5 6 7
int __cdecl call_ptrace_dynamic(int a1, int a2, int a3, int a4) { // ... handle = dlopen("libc.so.6", 1); v5 = (int (__cdecl *)(int, int, int, int))dlsym(handle, "ptrace"); return v5(a1, a2, a3, a4); }
Như vậy, mình không hook ptrace nữa mà mình hook dlsym:
Hook đã hoạt động đúng như ta muốn, ở trên ta có thể thấy chuỗi “sorry i stole …” được build bằng cách dùng ptrace(PTRACE_PEEKDATA, ...) để đọc từ process khác, đó là lí do ta không thấy chuỗi đó khi dùng strings.
Bây giờ ta bắt đầu quay lại phân tích flow của process con 1. Bắt đầu từ việc process con này viết giá trị 0xB0F vào process cha như đã nói ở trên.
Vậy là khi process cha gặp SIGILL, process con 1 sẽ set eip = 0x8048DCB, ngoài ra còn set tham số thứ nhất (tại esp + 4) thành input mà ta nhập vào. Tóm lại nó chuyển đã chuyển sub_8048CDB(input) thành sub_8048DCB(input). Ta thử xem hàm sub_8048DCB:
Ở hàm này ta thấy có execve và nice. Đặc biệt là v8 = -nice(165), sau đó v8 được dùng làm buffer (???). Bởi vì hàm nice return 1 số nguyên rất nhỏ, không thể nào dùng làm địa chỉ buffer được, chắc chắn là có gì đó không ổn với hàm nice này.
Như đã giải thích ở trên thì mỗi khi gặp 1 syscall, process cha sẽ raise SIGTRAP, syscall number nằm ở eax. Ở process con, nó xử lý SIGTRAP như sau:
1 2 3 4 5
v3 = 322423550 * (regs.orig_eax ^ 0xDEADBEEF); // ... truncated if ( v3 == 0xE8135594 ) { /* ... */ } if ( v3 == 0x2499954E ) { /* ... */ } // ... a lot of cases ...
nice có syscall number là 0x22, khi đó v3 = 0x3DFC1166:
_BOOL4 __cdecl sub_8048DCB(char *inp) { // ... truncated len = strlen(inp); // truncated ... execve(inp, &argv, 0); // remove '\n' at the end --len; v8 = -nice(0xA5); // return a string j_customAES_expand_key_804B495((int)aes_ctx, v8);// custom_AES_enc_804BABC((BYTE *)aes_ctx, (BYTE *)&unk_81A50EC); custom_AES_enc_804BABC((BYTE *)aes_ctx, (BYTE *)&unk_81A50F0); custom_AES_enc_804BABC((BYTE *)aes_ctx, (BYTE *)&unk_81A50F4); custom_AES_enc_804BABC((BYTE *)aes_ctx, (BYTE *)&unk_81A50F8); if ( !memcmp(inp, &unk_81A50EC, 0x10u) ) { memset(&unk_81A50EC, 0, 0x10u); result = sub_8048F05(inp + 16); } else { memset(&unk_81A50EC, 0, 0x10u); result = 0; } return result; }
Hàm trên nhận vào input của chúng ta, loại bỏ dấu “\n” ở cuối cùng. Sau đó nó lấy 1 đoạn string bằng nice để làm key. Key này được dùng để AES decrypt 1 string khác.
Bạn đọc có thể RE thử hàm AES ở trên, nó đã bị sửa lại, block size chỉ còn 4 thay vì 16.
Sau đó, nó so sánh đoạn string vừa decrypt được với input của chúng ta. Đến đây ta hook hàm memcmp để xem nội dung của 2 tham số:
Vậy 16 ký tự đầu là “w3lc0mE_t0_Th3_l”. Bây giờ ta chạy lại chương trình với input “w3lc0mE_t0_Th3_lAAAAAA”:
Lần này vẫn là dòng chữ “sorry …” đó, nhưng khác với lần đầu.
Ở lần đầu, khi ta nhập input “AAAAA” thì chương trình hiện ngay dòng đó luôn, còn nếu ta nhập “w3lc0…” thì chương trình mất khoảng 1 vài phút mới hiện lên dòng chữ đó, chứng tỏ là input này có gì đó làm cho chương trình đi theo flow khác.
1 2 3 4 5
if ( !memcmp(inp, &unk_81A50EC, 0x10u) ) { memset(&unk_81A50EC, 0, 0x10u); result = sub_8048F05(inp + 16); // <---- go }
Bây giờ ta phân tích hàm sub_8048F05, hàm này nhận tham số là &input[16]:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
_BOOL4 __cdecl sub_8048F05(void *src) { // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
v1 = nice(0xA4); s = (char *)-v1; v2 = strlen((constchar *)-v1); v6 = calc_hash(0LL, (int)s, v2); // v6 = 0x674a1dea4b695809 v5 = 40000; memcpy(&file, src, 0x20u); for ( i = 0; i < v5; i += 8 ) sub_804C369(&file + i, v6, HIDWORD(v6), &v4); // decrypt ? return truncate(&file, 32) == 32; }
Ta phân tích tiếp hàm sub_804C369:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
unsignedint __cdecl sub_804C369(__mode_t *a1, int a2, int a3, constchar *a4) { // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
Với đoạn code trên, giả sử ta có 1 hàm f(x,y) với f == NULL (để tạo SIGSEGV), thì:
Nếu (*y)++ >= 16 thì return.
Ngược lại: set eip = x.
Tóm lại cái hàm f này giúp chương trình lặp 16 lần mà không cần dùng for, while, ....
Quay lại hàm sub_804C369, hàm này dùng để encrypt 1 block data gồm 8 bytes. Hàm này sử dụng vòng loop bằng SIGSEGV như giải thích ở trên, bao gồm 2 bước:
Expand key:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
voidexpand_key(uint64_t key, uint32_t *ctx) { uint32_t *_ctx = ctx; uint64_t k = key; for (int i = 0; i < 16; ++i) { _ctx[7] = k & 0xFFFFFFFFull; _ctx[19] = (k >> 32ull) & 0xFFFFFFFFull; _ctx[41] = pop_cnt(k) >> 1; int _v6 = k & 1; k = k >> 1; if (_v6) { k = k ^ 0x9E3779B9C6EF3720uLL; } _ctx = _ctx + 62; } }
Nó sẽ copy data vừa được encrypt lên stack của chính nó, đồng thời so sánh data đó với 1 mảng hardcode ở 0x81A5100, ở đây có 32 byte [100, 160, 96, 2, 234, 138, 135, 125, 108, 233, 124, 228, 130, 63, 45, 12, 140, 183, 181, 235, 207, 53, 79, 66, 79, 173, 43, 73, 32, 40, 124, 224].
Trên stack, v18 là một mảng char có độ dài 16000, nhưng đoạn trên có thể copy tới tận 40000 byte -> BufferOverflow. Ta viết 1 đoạn code nhỏ để lấy đoạn data sau khi được encrypt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
intmain() { uint32_t ctx[62*17]; uint64_t key = 0x674a1dea4b695809ULL; uint8_t* data = newuint8_t[40000]; FILE* f = fopen("dump.bin", "rb"); fread(data, 1, 40000, f); fclose(f); for (int i = 0; i < 40000; i = i + 8) { encipher_8bytes((uint32_t*)(data + i), key, ctx); } f = fopen("dec.bin", "wb"); fwrite(data, 1, 40000, f); fclose(f); delete[] data; return0; }
Ở trong IDA, ta có thể tính được khoảng cách từ v44 tới đầu mảng v18 là 16164. Ta mở HxD để tới offset này:
Vậy là v44 đã bị ghi đè bởi 0x8053B70, địa chỉ này nằm trong đoạn data được encrypt, nên ta sẽ: lấy đoạn data đã được encrypt để patch lên file break gốc, sau đó bỏ vào IDA để phân tích lại hàm 0x8053B70.
1 2 3 4 5 6 7 8 9 10 11
# python3
if __name__ == '__main__': withopen('break', 'rb') as f: data = f.read() withopen('dec.bin', 'rb') as f: patch_data = f.read() data = data[:0x4640] + patch_data + data[0x4640+40000:] withopen('patch', 'wb') as f: f.write(data) print ('[+] Done')
Sau khi có file mới, ta dùng IDA phân tích tiếp hàm sub_8053B70, hàm này gọi lại hàm sub_805492E(mình đã RE hàm này và rename lại hết, bạn đọc có thể tự RE lại các hàm liên quan tới bigint và xác nhận lại):
defegcd(a, b): if a == 0: return (b, 0, 1) else: g, y, x = egcd(b % a, a) return (g, x - (b // a) * y, y)
defmodinv(a, m): g, x, y = egcd(a, m) if g != 1: raise Exception('modular inverse does not exist') else: return x % m
if __name__ == '__main__': z = 0xd1cc3447d5a9e1e6adae92faaea8770db1fab16b1568ea13c3715f2aeba9d84f y = 0xc10357c7a53fa2f1ef4a5bf03a2d156039e7a57143000c8d8f45985aea41dd31 t = 0xd036c5d4e7eda23afceffbad4e087a48762840ebb18e3d51e4146f48c04697eb r = (((modinv(y, z) * t))) #print (hex(r)) g = r % z #print (hex(g)) for i inrange(24): print (chr((g >> (8*i)) & 0xFF), end = '') print ('')
One of our endpoints was infected with a very dangerous, yet unknown malware strain that operates in a fileless manner. The malware is - without doubt - an APT that is the ingenious work of the Cyber Army of the Republic of Kazohinia.
One of our experts said that it looks like they took an existing banking malware family, and modified it in a way that it can be used to collect and exfiltrate files from the hard drive.
The malware started destroying the disk, but our forensic investigators were able to salvage ones of the files. Your task is to find out as much as you can about the behavior of this malware, and try to find out what was the data that it tried to steal before it started wiping all evidence from the computer.
Good luck!
Ở challenge này chúng ta có file NTUSER.DAT, file này chứa thông tin về Registry của một user.
Đoạn registry key trên dùng pattern p*ll.*e để match (regex ?) “powershell.exe”, dùng để tránh antivirus quét phải cụm từ “powershell”. Paramerter “-ec” dùng để chạy đoạn code đã được mã hóa bằng base64, ta decode đoạn mã này:
1
iex (gp 'HKCU:\SOFTWARE\Timerpro').D
Trong powershell, iex dùng để gọi 1 script khác tham khảo.
Ta đến “HKCU\SOFTWARE\Timerpro” để xem key này chứa những gì:
Entry D chứa 1 đoạn code powershell khác. Ta copy đoạn này ra file mới cho dễ đọc:
1 2 3 4 5
$jjw="kcsukccudy"; functionhjmk{[System.Convert]::FromBase64String($args[0]);}; [byte[]]$rpl=hjmk("a very long base64 string"); functiongeapmkxsiw{$kjurpkot=hjmk($args[0]);[System.Text.Encoding]::ASCII.GetString($kjurpkot);};iex(geapmkxsiw("another base64 string"));iex(geapmkxsiw("and another one"));
Sau khi decode các đoạn string bị mã hoá trên, ta được:
Copy data ở biến $rpl vào 1 buffer được tạo bởi VirtualAlloc.
Tạo 1 thread với QueueUserAPC với tham số pfnAPC là buffer vừa tạo ở trên, chứng tỏ đoạn data này là 1 đoạn shellcode.
Sau đó nó đợi thread thực hiện xong bằng SleepEx.
Ta cùng xem đoạn shellcode bằng HxD trước khi phân tích nó.
Ta có thể thấy 2 bytes PE ở offset 0x10, nếu ta tiếp tục kéo xuống thì sẽ thấy rất nhiều byte 0x00, cho tới offset 0x1000 thì ta lại thấy rất nhiều bytes random. Khả năng cao đây là file PE nhưng đã bị phá huỷ mất DOS HEADER.
5 bytes đầu của file này là e9 f7 99 00 00, disassemble ta được “jmp 0x99fc”, ta bỏ file này vào IDA rồi tới offset 0x99FC để phân tích hàm này:
Đoạn code trên sử dụng kỹ thuật Reflective load dll (ngoài ra sau khi load được dll lên bộ nhớ, nó cũng zeroing một số phần data để gây khó khăn cho việc dump và phân tích).
Để dễ dàng debug chương trình trên, mình thêm 1 dòng trước khi đoạn script thực hiện VirtualAlloc:
1
Write-Host-Object ('The key that was pressed was: {0}'-f [System.Console]::ReadKey().Key.ToString());
Đoạn code trên sẽ đợi người dùng bấm 1 phím bất kỳ, như vậy ta sẽ có thời gian để attach debugger vào “powershell.exe”.
Ta tiến hành debug đoạn script powershell như sau:
Chạy đoạn script trên, chương trình sẽ đợi ta bấm gì đó từ bàn phím.
Dùng windbg, attach vào powershell.exe.
Viết 1 đoạn windbg script để thực hiện việc nhảy tới chỗ shellcode cho nhanh:
1 2 3 4 5 6 7 8
.block { bp kernel32!QueueUserApcStub g bp @rcx r $t0 = @rcx g }
Thực thi đoạn script trên bằng lệnh: $$>a<C:\Users\admin\Desktop\11_-_rabbithole\script.txt (bạn đọc tự thay đổi đường dẫn).
Sau đó bấm phím bất kỳ trong powershell, khi đó ta sẽ dừng lại ở đây:
Mẹo: dùng script khi phải làm những việc lặp đi lặp lại giúp tiết kiệm rất nhiều thời gian.
Để dump được file dll ra, ta sẽ:
Đặt breakpoint ngay trước khi nó kịp gọi DllMain.
Trong hàm sub_99FC có 1 số chỗ zeroing IAT, v.v …, ta sẽ patch những đoạn đó bằng 0x90 nop.
Dùng PE-Bear để xem thông tin về file mới nhận được:
Lý do lỗi là vì, file ta vừa nhận được cũng không hề có DOS HEADER. Ở đây mình sửa file như sau:
Copy phần DOS HEADER từ file khác insert vào đầu file này, pad bằng \x00 cho đủ 0x1000 bytes.
Chỗ nào trong PE-header liên quan tới RVA, cộng thêm 0x1000 (Data Directory, EntryPoint, …).
File mới đã có thể được nhận dạng bởi PE-Bear:
Nếu bạn đọc làm lại bước này thì imagebase sẽ khác mình, tuy nhiên các offset sẽ vẫn giống.
Ta phân tích file này bằng IDA:
Tuy nhiên có nhiều chỗ hiện màu đỏ như trên, đó là vì bảng IAT bị sai. Bây giờ ta sửa tiếp như sau, đầu tiên lấy offset con trỏ hàm 0x7FFAEF4D1B80:
Vậy offset là 0x200C7380090 - imagebase - 0x1000 = 0xF090 , lý do trừ 0x1000 là vì ta đã thêm 0x1000 bytes vào làm DOS HEADER. Đến đây, ta dùng windbg để xem các symbol tại địa chỉ này:
Kết hợp với source code trên, ta có thể bỏ bớt một số hàm cần phải RE. Bây giờ ta phân tích tiếp hàm sub_200C7373E21C:
Hàm sub_200C737E21C sẽ trả về một chuỗi ngẫu nhiên, sau đó tạo một registry key với tên này. Nó sử dụng xorshift64 để làm hàm random, với seed được tạo ra ở hàm sub_200C737D928 như sau:
Seed sẽ dựa vào giá trị SID của user. Ở đây ta phải patch giá trị seed trên cho bằng đúng với giá trị seed trên máy nạn nhân. Các công việc cần làm là:
Tìm SID của nạn nhân (tìm chuỗi “S-1-5-21” trong Registry Explorer, -> “S-1-5-21-3823548243-3100178540-2044283163”)
Patch memory SID bằng windbg khi chạy trên máy chúng ta bằng chuỗi SID vừa tìm ở trên.
Quan sát giá trị seed -> 0x55707b4efb307bfa.
Các lần chạy sau đó không patch SID nữa mà patch seed luôn.
Sau khi patch xong, debug (mình đã ngồi bấm step in, step out rất nhiều) và quan sát trong IDA thì ta thấy:
Program này đọc registry key “HKEY_CURRENT_USER\Software\Timerpro\Columncurrent\WebsoftwareProcesstemplate” và “HKEY_CURRENT_USER\Software\Timerpro\Languagetheme\WebsoftwareProcesstemplate”.
Decrypt 2 giá trị trên bằng RSA (và Serpent).
Giải nén giá trị trên bằng ApLib.
Thực hiện inject 1 trong 2 giá trị trên vào explorer.exe. (Columncurrent cho x64, LanguageTheme cho x86).
Đến đây ta viết đoạn code để dump và decrypt các giá trị registry trên:
defunsign_data(data: bytes, key: PublicRSAKey) -> tuple: # return (True, data) if verify successfully # else return (False, ) iflen(data) <= 128: return (False, ) ds = data[-128:] ds = int.from_bytes(ds, 'big') p = RSAPublicDecrypt(ds, key.e, key.n) p = p.to_bytes(128, 'big') ifnot p.startswith(b'\x00\x01'): return (False, ) i = 2 while i < len(p): if p[i] == 0: break i = i + 1 if i == len(p): return (False, ) p = p[i + 1:] signed_md5_hash = p[0:16] serpent_key = p[16:32] decrypted_size = int.from_bytes(p[32:36], 'little') decrypted_data = serpent.serpent_cbc_decrypt(serpent_key, data[:-128], b'\x00'*16)[:decrypted_size] md5_hash = md5(decrypted_data) if signed_md5_hash == md5_hash: return (True, decrypted_data) return (False, )
if __name__ == '__main__': iflen(argv) != 2: print ('[+] python dec.py <filename>') exit(1) n = \ "c3da263df172293373b0431e"+\ "e00bac4c3db723bee2d9ccc0a7ef8d03"+\ "68c33c577df7e64f09503437e9178533"+\ "c9f3b4d4eebd7fe1075e2e553939d43c"+\ "25eb8a89a5fd7ad5f8a52c20713ae878"+\ "cf2b1f322acfe8b7c55dad60b3520614"+\ "19fa713c903d9efc36baf95185880d03"+\ "ec165a51186cf1c323bc58c40b85fcbc"+\ "7fa162ad" n = int.from_bytes(a2b_hex(n), 'big') e = 0x10001 rsa_key = PublicRSAKey(e, n) withopen(argv[1], 'rb') as f: data = f.read() res = unsign_data(data, rsa_key) if res[0] == True: withopen(argv[1] + '.decrypt', 'wb') as f: f.write(res[1]) print ('[+] Unsigned %s (%d bytes -> %d bytes) !' % (argv[1], len(data), len(res[1]))) else: print ('[+] Wrong signature !')
1 2 3 4 5 6 7 8 9 10 11
# python3 from sys import argv import aplib
if __name__ == '__main__': withopen(argv[1], 'rb') as f: data = f.read() d = aplib.decompress(data[20:], True) withopen(argv[1] + '.decompressed', 'wb') as f: f.write(d) print ('[+] %s: %d -> %d bytes' % (argv[1], len(data), len(d)))
Với 3 đoạn code trên, ta decrypt được rất nhiều file, trong đó có vài file bắt đầu bằng “PX” (và cũng có file không decrypt được, có lẽ là vì những file đó không được encrypt theo cách này):
File này sẽ được malware decode trước khi inject vào process. Ta dùng code ở bài này để decode file PX về dạng PE file rồi dùng IDA phân tích:
Sau khi decode hết các file PX, ta có được rất nhiều file dll nhưng không biết chúng được load theo thứ tự nào. Có quá nhiều file nên việc phân tích tĩnh rất khó khăn.
Mình đã dành ra khoảng 10 ngày để phân tích tĩnh các file dll đó, nhưng vẫn không thấy được gì có ích cho việc tìm flag. Vì sắp hết thời gian nên mình đã dừng việc phân tích tĩnh lại để tìm một hướng đi khác.
Đoán rằng các file .dll trên sẽ được inject vào explorer.exe, ta dùng procmon để theo dõi explorer.exe (đừng quên patch seed):
Chạy file powershell và quan sát:
Ta thấy explorer đang cố gắng đọc hoặc ghi file gì đó ở “%appdata%\Microsoft\Oldsolution” và “%tmp%”. Tiếp theo, ta thử ném vào trong thư mục “Oldsolution” 1 file bất kỳ và làm lại như trên:
Lần này thì file chúng ta vừa tạo ra đã được program access. (Và nếu ta quay lại Oldsolution để xem thì file này cũng bị xoá luôn). Ta đến thư mục %tmp% để xem file B1FC.bin:
File của chúng ta đã bị nén lại (zip) và ghi ra %tmp%\B1FC.bin. Ngoài ra trong procmon còn 1 chỗ nữa thú vị:
Nó ghi gì đó vào registry, length = 1607200, nếu ta coi lại size của file B1FC.bin:
Ta có thể thấy gần bằng nhau, có thể đoán là file B1FC.bin đã được thêm padding, sau đó ghi lên registry. Ta thử xem “HKCU\Software\Timerpro\DiMap” chứa gì:
Vậy là file này còn bị mã hoá trước khi ghi vào registry. Biết được rằng file sẽ bị mã hoá rồi sau đó được ghi lên registry, ta có thể làm như sau:
Đặt breakpoint tại ZwSetValueKey (nếu bạn debug program này thì sẽ thấy nó xài rất nhiều hàm Zw, đó là lý do mình không đặt breakpoint tại RegSetValueExA và các hàm tương tự …)
Khi dừng lại tại breakpoint này ta kiểm tra xem key name có phải là “DiMap” không, nếu không thì continue.
Nếu là “DiMap”, ta bắt đầu quan sát stack trace, sau 1 vài stack call thì thấy hàm sub_180001000 của “WebmodeThemearchive.dll” dùng để mã hoá file zip.
Nó sẽ gọi hàm export có ordinal 27 của 8576b0d0.dll (file này thực ra là WebsoftwareProcesstemplate.dll), mà hàm 27 lại gọi hàm sau:
defdecrypt_base(data: bytes, key: int): assert0 <= key <= 0xFFFFFFFF# 4 bytes key ! assertlen(data) % 4 == 0# size must be aligned n = len(data) >> 2 data = [int.from_bytes(data[4*i:4*i+4], 'little') for i inrange(n)] data2 = [i for i in data] for i inrange(n): if i == 0: data2[i] = (key ^ data[i]) else: data2[i] = (key ^ data[i]) ^ data[i - 1] data2[i] = rol32(data2[i], 4*(i % 2)) r = [] for i inrange(n): for j in [0, 1, 2, 3]: r.append((data2[i] >> (8*j)) & 0xFF) returnbytearray(r)
if __name__ == '__main__': withopen('DiMap', 'rb') as f: data = f.read() data = decrypt_base(data, 0xFB307BFA) serpent_key = b'GSPyrv3C79ZbR0k1' data = serpent.serpent_cbc_decrypt(serpent_key, data) withopen('out.zip', 'wb') as f: f.write(data) print ('[+] Done!')
Sau khi chạy đoạn code trên ta được file zip mới, chứa flag: