首页
社区
课程
招聘
[分享]一个Shellcode加载器的设计思路:从加密到内存对抗
发表于: 2026-8-21 17:27 149

[分享]一个Shellcode加载器的设计思路:从加密到内存对抗

2026-8-21 17:27
149

引言

Shellcode加载器的核心架构是什么?


将Shellcode放在一个地方 → 使用函数申请空间或修改权限 → 执行Shellcode


哪怕加载器再怎么改造、做了多少免杀处理,核心架构始终不变。


我们要面对的是静态查杀、动态查杀,甚至多种多样的行为封锁。Shellcode的落地方式和加载时机,决定了加载器能否在免杀对抗中站住脚。


本文不提供完整源码,而是记录我在构建加载器时的设计思路和决策逻辑。


一、核心架构与关键决策

加载器本质上要解决三个问题:怎么藏住Shellcode、怎么避开检测、怎么让系统帮我执行它。每个问题都有多种解法,但组合在一起时,必须有一条连贯的逻辑线。


我的核心执行流程是:


反沙箱/反调试检测 → 加载加密资源 → 分配RW内存 → 复制密文 → 

原地解密 → 修改为可执行权限 → 安装Sleep Hook → 触发执行

这个流程不是技术堆砌,而是基于一个核心判断:每一步都应该让杀软的"异常信号"尽可能低。


下面是我在构建过程中遇到的五个关键决策点,每个决策都直接影响了下游技术的选择。


1. 为什么加密用AES而不是XOR?


XOR加密不改变明文的分布特征。如果Shellcode里有连续的0x90(NOP),XOR后依然呈现规律性重复,YARA规则很容易提取出密文特征。


AES-CBC是块加密,输出接近随机分布。资源区里的密文不会触发签名匹配,配合Sleep Hook使用时,解密后的明文也不会在内存中长期暴露。


2. 为什么把Shellcode放在资源区(.rsrc)而不是数组里?


unsigned char buf[]会落在.data或.rdata节,杀软对整个节的扫描策略通常更严。


资源区的原始用途是存放图标、版本信息,杀软对它的扫描强度相对较弱——不是技术上不能扫,而是误报风险太高。把密文伪装成正常资源数据,是一种"不增加特征暴露面"的做法。


3. 为什么用原地解密?


如果先解密到临时内存再复制到可执行内存,解密后的Shellcode会先后出现在两个内存区域,内存暴露窗口被放大。


原地解密的好处是:解密前后在同一块内存里,没有额外副本。从杀软视角看,这只是"一段资源数据被修改了",而不是"敏感数据被复制到了新位置"。


4. 为什么在Sleep Hook里加内存加密?


杀软的动态内存扫描大多是间歇性触发(比如每2秒扫描一次)。如果Shellcode在两次扫描之间一直处于明文状态,就有可能被命中。


Sleep Hook的逻辑是:执行期间保持明文,进入Sleep后立即加密成随机数据,Sleep结束后再解密回来。


这样,Shellcode在内存中只有"正在执行"时才是明文,而执行是一个极短的瞬时状态。杀软的间歇扫描窗口,撞上明文状态的概率被降到了最低。


5. 为什么反沙箱/反调试要放在最前面?


放在解密之后,调试器或沙箱就有机会在解密后的明文内存上设置断点、Dump完整Shellcode、记录中间数据。


前置检测意味着:任何异常环境在接触到Shellcode之前就被拒绝了。即使对方猜到了加载方式,也无法在受控环境中获得明文的Shellcode。


二、核心技术定位

这一部分介绍每项技术在这个流程中的具体定位——它们不是孤立存在的,而是被放置在流程中某个特定位置,承担特定职责。


1. 反调试与反沙箱技术

定位说明:

云沙箱和自动上传已经是各大杀软的标配。没有这道防线,第一次上传即使没被标记,云沙箱提取的特征也可能在几小时后让你被全网封杀。它解决的是"载荷不该被公开分析"的问题——在流程中负责环境隔离,确保后续操作只发生在受控的真实用户环境中。


常见检测思路:


反调试检测:


IsDebuggerPresent():查询PEB中的BeingDebugged标志


CheckRemoteDebuggerPresent():检查是否有远程调试器


PEB结构字段检测:直接读取NtGlobalFlag等标志位


NtQueryInformationProcess:查询调试端口,比PEB标志更底层


时间检测(rdtsc):调试器单步会拖慢速度,利用时间差判断


异常处理机制:利用调试器优先捕获异常的差异设置陷阱


cpp


// API检测示例

bool CheckDebuggerAPIs() {

    if (IsDebuggerPresent()) return true;

    BOOL isRemoteDebuggerPresent = FALSE;

    CheckRemoteDebuggerPresent(GetCurrentProcess(), &isRemoteDebuggerPresent);

    return isRemoteDebuggerPresent;

}


// PEB检测示例

bool CheckPEBFlags() {

    PPEB peb = NtCurrentTeb()->ProcessEnvironmentBlock;

    if (peb->BeingDebugged) return true;

    if (peb->NtGlobalFlag & 0x70) return true;

    return false;

}

反沙箱检测:


CPUID指令检测:虚拟机中会置位Hypervisor Present Bit或返回特定厂商标识


硬件信息检测:沙箱通常分配更少资源(内存<4GB、CPU<2核)


延时执行检测:沙箱可能加速Sleep,通过实际耗时与预期的偏差判断


用户交互检测:鼠标是否长期静止、桌面窗口是否存在


cpp


// 沙箱资源检测示例

bool CheckSystemResources() {

    MEMORYSTATUSEX memStatus;

    memStatus.dwLength = sizeof(memStatus);

    GlobalMemoryStatusEx(&memStatus);

    DWORDLONG totalPhysMem = memStatus.ullTotalPhys / (1024 * 1024);

    SYSTEM_INFO sysInfo;

    GetSystemInfo(&sysInfo);

    if (totalPhysMem < 4096 || sysInfo.dwNumberOfProcessors < 2) return true;

    return false;

}

2. 加密算法与加密混淆

定位说明:

加密的目的是消除载荷的静态特征,让资源区里存放的是一段随机数据,而不是可识别的Shellcode。之所以选择AES-CBC,是因为它的输出分布接近随机,能最大化降低静态匹配的命中率。


核心所在:加密 → 写入资源 → 载入程序


整个加密载荷的"落地"流程分为三步:


第一步:加密Shellcode,生成密文文件


这一步在加载器之外完成。我写了一个独立的加密工具:读取原始Shellcode,用AES-256-CBC加密,输出一段密文。


关键点:加密工具与加载器的唯一耦合是使用相同的AES密钥和IV。密钥变更时,只需要重新加密并替换资源文件,不需要重新编译加载器本身。


第二步:将密文作为资源写入加载器


在加载器工程中,将密文文件以RT_RCDATA类型嵌入到资源区(.rsrc节):


IDR_SHELLCODE RCDATA "shellcode_encrypted.bin"

RT_RCDATA不要求数据符合任何特定格式,任意二进制数据都可以通过这种方式嵌入,且不会被编译器做额外处理。


第三步:加载器运行时通过资源API读取密文


FindResourceExW:定位资源在PE文件中的位置


LoadResource:将资源数据从文件映射到内存


LockResource:获取指向密文数据的指针和数据长度


密文在内存中是以"只读资源页"形式存在的,直到后续的VirtualAlloc+解密操作,才会被复制到可操作的内存区域。


三步关系:


加密工具(外部)→ 生成密文文件 → 资源编译器嵌入(编译时)→ 

资源API读取(运行时)→ 得到密文指针 → 后续解密

为什么这样设计?把加密过程前置到编译阶段,运行时没有加密开销,解密是纯内存操作,减少了API调用链长度;同时密钥与加载器分离,密钥变更不需要重写加载器逻辑。


3. Hook钩子技术

定位说明:

Hook在这里的用途是在特定事件(Sleep)发生时,触发内存加密动作。它解决的是"载荷在内存中不该长时间明文暴露"的问题。放在解密之后,是因为它依赖解密完成后的Shellcode地址和长度信息。


核心所在:挂载Sleep Hook → 拦截休眠事件 → 执行加密/解密动作


整个Hook机制的触发链:


正常执行流:加载器执行Shellcode → Shellcode内部调用Sleep() → 

系统进入休眠 → 休眠结束 → 继续执行


挂载Hook后:加载器执行Shellcode → Shellcode内部调用Sleep() → 

被MySleep()拦截 → 先加密内存 → 调用原Sleep() → 

休眠结束 → 解密内存 → 返回Shellcode继续执行

Shellcode在休眠期间处于空闲状态,不执行任何指令,但明文代码依然躺在可执行内存里。如果杀软在这时发起内存扫描,明文Shellcode就暴露了。Hook要在系统进入空闲期之前,先把内存"打扫干净"。


挂载操作:


使用Detours库,核心代码三行:


DetourTransactionBegin();

DetourUpdateThread(GetCurrentThread());

DetourAttach(&(PVOID&)OldSleep, MySleep);

DetourTransactionCommit();

为什么选择Sleep作为Hook点?


因为它是"可预测的空闲信号"。很多Shellcode在完成主任务后会进入循环,循环中调用Sleep降低CPU占用。Sleep被调用的时机,恰好是Shellcode"无事可做"的时段,且时长足够完成加密+权限修改操作。如果Hook点选在执行密集的API上,加密开销可能干扰Shellcode的正常逻辑。


挂载时机:


Hook必须在Shellcode解密并进入可执行状态后才能安装。只有解密完成后,全局变量g_pShellcode和g_dwShellcodeSize才有有效值。


解密 → 修改权限 → 保存全局变量 → 安装Hook → 触发执行

4. 内存加解密技术

定位说明:

内存加密是配合Hook使用的执行层保护手段,把Shellcode的明文时间窗口压缩到执行瞬间。在大多数时间里,Shellcode在内存中以密文形式存在。


与Hook的协作关系:


Hook是"触发器",内存加密是"执行器"。


Hook捕获Sleep调用 → 进入MySleep → 调用内存加密 → 权限降级 →

执行原Sleep(此时内存已加密) → 休眠结束 → 解除加密 → 恢复权限

核心所在:加密时机与权限变更


cpp


// 进入休眠前

VirtualProtect(g_pShellcode, g_dwShellcodeSize, PAGE_READWRITE, &old);

memory_crypt(g_pShellcode, g_dwShellcodeSize, AES_ENCRYPT);

VirtualProtect(g_pShellcode, g_dwShellcodeSize, PAGE_READWRITE, &old);

OldSleep(dwMilliseconds);


// 休眠结束后

VirtualProtect(g_pShellcode, g_dwShellcodeSize, PAGE_READWRITE, &old);

memory_crypt(g_pShellcode, g_dwShellcodeSize, AES_DECRYPT);

VirtualProtect(g_pShellcode, g_dwShellcodeSize, PAGE_EXECUTE_READWRITE, &old);

三个关键点:


一、休眠期间加密,休眠结束还原


明文Shellcode只在执行前瞬间和执行中存在,其余时间(包括整个休眠周期)都是密文。杀软的间歇性扫描撞上明文窗口的概率被降至最低。


二、权限同步变更


加密的同时配合内存权限降级,从可执行降为普通读写。即使杀软扫描到这段内存,也不会因为"可执行内存中存在代码"而触发告警。


三、加密范围的选择


直接对整块Shellcode内存做全量加密,不需要区分代码段和数据段。代价是加密/解密时间与Shellcode大小成正比。但Shellcode通常在几百KB以内,Sleep执行间隔是秒级,开销可接受。


三、总结:单点突破与系统设计的区别

从反沙箱检测到内存加密,这个加载器的每一项技术都在回应同一个核心问题:如何让杀软的检测逻辑无法识别我的载荷。


免杀对抗中,单点技术很难长期有效。XOR加密、API混淆、加壳工具——任何一个"单一手段"被研究透之后,杀软都能针对性地提取特征。真正让载荷保持有效周期的,不是某一个技巧有多强,而是整套流程是否自洽。


如果把每项技术单独拆开看,它们都很常见,甚至可以说是免杀领域的"标配":


反调试检测


AES加密


Sleep Hook


内存权限修改


但这些技术组合在一起,就形成了一条有依赖关系的链路:前置检测保护解密环境 → AES消除静态特征 → Sleep Hook缩小明文窗口 → 内存加密降低扫描命中率。 前一步的不足,由后一步来弥补;后一步的有效性,依赖前一步的完成质量。


这个设计本身没有使用任何"黑科技"或未公开技术。它的价值不在于某段代码有多巧妙,而在于把已知技术组合成一套可验证的防御体系——这是我理解的"免杀对抗"的真正含义。

————————————————

版权声明:本文为CSDN博主「万奈&星尘」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:5cdK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1L8r3!0Y4i4K6u0W2j5%4y4V1L8W2)9J5k6h3&6W2N6q4)9J5c8U0t1#2x3o6u0Q4y4h3j5&6x3U0M7H3x3U0b7@1y4g2)9J5c8X3q4J5N6r3W2U0L8r3g2Q4x3V1k6V1k6i4c8S2K9h3I4K6i4K6u0r3x3e0j5K6y4U0l9@1y4K6j5J5



传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!

收藏
免费 0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回