跨平台系统调用驱动的植入程序与 C2 —— 直接系统调用(Windows)、原始系统调用(Linux)、HTTPS/DNS/ICMP 通道。无 winapi 层。
English | 简体中文 | 조선어 | Русский
零 WinAPI。所有 NT 原语在运行时从内存映像中的 ntdll 动态解析——通过 Plan9 汇编存根直接发出 SYSCALL 指令,无导入表(IAT 全零),不触碰 ntdll 用户态钩子。C2 走 HTTPS / DNS / ICMP 通道,每条消息 AES-256-GCM AEAD 封装并附带独立 nonce。原生 Windows 植入逻辑,同时保留 Linux / macOS 服务端构建。
这不是 syscall 封装库,而是完整植入框架:从代码注入、文件 I/O、注册表持久化到令牌冒名,所有操作一律走开机时从当前 ntdll 导出表解析出的原始 Nt* syscall。二进制内不存在任何 kernel32/advapi32/user32/win32u 导入,用户态钩子(inline hook、IAT hook、Callbacks)无处着力。每个函数名以 djb2 哈希常量存在,反汇编结果中检索不到明文字符串——只有 32 位哈希和 SYSCALL 助记符。
ReadGSBase ──GS:[0x60]──▶ TEB ──+0x60──▶ PEB ──+0x18──▶ PEB_LDR_DATA
│
+0x10 InMemoryOrderModuleList ◀── 链表头
│
遍历节点,取 module->DllBase 上读 PE 头,djb2 匹配 ntdll" 基址
│
IMAGE_DOS_HEADER.e_lfanew ──▶ IMAGE_NT_HEADERS
│
OptionalHeader.DataDirectory[0].VirtualAddress ──▶ 导出表
│
AddressOfNames[] 逐个 djb2 → 命中目标函数名 → 序号 → AddressOfFunctions[]
│
函数地址 ──▶ 前导序列扫描 ──▶ 提取 SSN
h = 5381; h = h*33 + c。名称哈希在编译期完成(hash.go 以字符串 + UTF-16 双变体同构),二进制只有 uint32 常量。resolve.go)逐字节在目标函数前 64 字节窗口内匹配以下模式并容忍 jmp 跳板(EB xx 短跳 / E9 xx xx xx xx 近跳 → 沿跳转链继续扫描):
B8 xx xx 00 00 0F 05(mov eax, SSN; syscall)B8 xx xx 00 00 C3(mov eax, SSN; ret)4C 8B D1 B8 xx xx 00 00 0F 05(mov r10, rdx; mov eax, SSN; syscall)B8 之后 4 字节小端,低 16 位即 SSN,存入 map[uint32]uint16 缓存(函数哈希 → SSN)。NtUser*/NtGdi*)走 win32u.dll,其 SSN 空间与 ntdll 分离,单独解析、单独缓存。asm_amd64.s 手写 ABI,关键语义:
| 阶段 | 寄存器动作 |
|---|---|
| 参数入位 | 第 1~3 参依次送 CX/R8/R9(syscallwin 约定),第 4 参送 R10(必须,SYSCALL 会 clobber RCX/R11) |
| SSN 注入 | movl $ssn, AX(AX=RAX,Plan9 为长度无前缀命名) |
| 触发 | SYSCALL → 内核从 MSR_LSTAR 进入 KiSystemCall64 |
| 返回 | NTSTATUS 在 RAX;NtDeviceIoControlFile 等会把输出缓冲指针放 RDX |
Syscall(≤7 参)与新加 Syscall9(9 参,栈帧 $0-88,SUB $0x50 为第 8/9 参预留)覆盖 NtGdiBitBlt 这类重参数量 syscall。零 Wi-Fi 调用:识别函数、系统调用、参数铺排全部在无库依赖的裸汇编里完成。
直接 syscall 的调用栈里始终只有植入自身的帧——密集 EDR 靠栈回溯即可抓现行。间接模式在 ntdll .text 中搜索 0F 05 C3(syscall; ret)gadget,经其落地:
KiUserExceptionDispatch/栈回溯呈现 ntdll 帧;syscall 指令所在页标记为非执行页(NtProtectVirtualMemory 至 PAGE_PRESERVE)——扫描时跳过首个字节偏移、改用相邻 0F 05 双字节序列落位,进一步绕开“页颗粒”防护。SSN 非稳定接口:10.0.19041(20H1)到 10.0.26100(24H2)间同一函数的编号漂移频繁。fingerprint.go 将当次 ntdll 构建的所有已解析 SSN 排序 + 哈希,生成 64 位指纹:
EDR 惯用手法:把 ntdll 的 .text 页做成 RWX 并注入 5~14 字节跳板(jmp r11; push r11; mov r11, <handler> 等)。Unhook 流程:
NtCreateFile(\SystemRoot\System32\ntdll.dll) → NtMapViewOfSection(SEC_IMAGE)
→ 新版 .text 与 in-memory .text 逐字节 diff
→ NtProtectVirtualMemory(.text → RWX) → memcpy(干净页)
→ 恢复保护(RX) → FlushInstructionCache → 清空 SSN 缓存
之后每个信标前半段无条件执行 CheckAndUnhook()——即便 EDR 二次挂钩,也会在下一个 beacon 前被再次抹平。
Agent 信标循环内轮换 4 种注入技术,两次注入的内存布局、落点、线程创建路径互不相同。
| 方法 | 原语链 | 隐蔽性分析 |
|---|---|---|
| Section 映射 | NtCreateSection → NtMapViewOfSection(本地写→远程读)→ NtCreateThreadEx(shellcode 地址) | Section 由磁盘文件背书,VAD 中无 PAGE_EXECUTE_READWRITE;远程私有页仅以 PAGE_EXECUTE_READ 呈现,内存扫描器按保护位白名单放行 |
| 进程镂空 | NtCreateUserProcess(挂起)→ NtSuspendProcess → 索引镜像基址 → 清零原 Base → 分配 + NtWriteVirtualMemory(shellcode)→ NtSetContextThread(RIP=载荷)→ NtResumeProcess | 任务管理器里是合法的 svchost.exe——路径、PID、PPID 全真,只有内存内容被替换 |
| APC 排队 | NtQuerySystemInformation(SystemHandleInformation) 枚举线程 → NtOpenThread → NtQueueApcThread | 零新线程、零新 TEB、零新栈分配;等待目标线程进入可警醒等(WaitForSingleObject/I/O 完成)时插入内核 APC |
| 模块覆盖 | 目标进程内分配 → 写入最小合法 PE 头 + shellcode → NtCreateThreadEx(入口点) | 模块列表出现看似合理的 DLL 名;PE 头几何结构(节表、SizeOfHeaders)足矣骗过裸模块枚举 |
轮换顺序由 agent 内部取模推进——同一植入的不同次注入在不同宿主上无稳定指纹。
全量检查跑完后输出评分威胁报告;达到 Critical(OU)阈值触发自动销毁(SelfDel + NtTerminateProcess)。
| 检测项 | 机制细节 |
|---|---|
| VM 检测 | CPUID(0x40000000) 读 12 字节超管理器签名(VMwareVMware、Microsoft Hv、KVMKVMKVM、XenVMMXenVMM、QEMU、VBoxVBoxVBox、prl hyperv…);叶节点 0x40000001 的厂商字符串作回退 |
| 沙箱检测 | 逻辑处理器数量;注册表工件 HKLM\SOFTWARE\VMware, Inc.\VMware Tools、VBoxGuestAdditions、服务键检查;30+ 进程名扫描(wireshark、procmon、x64dbg、idaq、dumpcap、ollydbg…) |
| 调试器检测 | PEB.BeingDebugged(0x02)、PEB.NtGlobalFlag(0x68, 对照 FLG_HEAP_ENABLE_TAIL_CHECK…) 、堆调试标志、ProcessDebugPort、ProcessDebugObjectHandle、ProcessDebugFlags、硬件断点 DR0~DR7(经 Context 读 Dr0..Dr7 且校验 DR7 启用位)、RDTSC 单步计时 |
| 计时异常 | 50 次 NtQuerySystemInformation 延迟采样,算均值 μ / 标准差 σ;>10% 样本超 μ±3σ 判为插桩开销(EDR 钩子加层的时延特征) |
| PEB 规避 | 全程 GS 段偏移直读 + NtWriteVirtualMemory 就地修补 BeingDebugged、NtGlobalFlag、ProcessHeap.Flags/ForceFlags、DebugPort 清 0;线程级再补 NtSetInformationThread(ThreadHideFromDebugger=0x11) |
| KUSER_SHARED_DATA 读取 | 只读 0xFFFFF78000000000 常量区的稳定洞(TickCountMultiplier、SystemTime/InterruptTime、NumberOfPhysicalPages 等),无非法解析风险,供 evasion 第 0x09 子命令上报 |
全 Nt* 实现:
EnablePrivilege(index) — 任意权限按 LUID/索引置位EnableAllTokenPrivileges() — 一次 20 项(SeDebug、SeImpersonate、SeTcb、SeBackup、SeRestore…)GetProcessTokenIntegrityLevel() — 强制完整性等级(Medium/High/System)StealProcessToken(pid) — NtOpenProcessToken → NtDuplicateToken,冒用 SYSTEM 等身份ImpersonateThread() / RevertToSelf()EnumVirtualMemory() — NtQueryVirtualMemory 全区域遍历FindWritableExecRegions() — 定位已提交 PAGE_EXECUTE_READWRITE 区(自 RX→RWX 切换考点)HideRegion() / UnhideRegion() — PAGE_NOACCESS 隐藏与恢复NtCreateFile → NtReadFile/NtWriteFile → NtClose;零 CreateFileA/ReadFile/WriteFile/DeleteFileW。ReadFileContents()、WriteFileContents()、DeleteFileNt()、FileExists()。NtCreateKey → NtSetValueKey;AddRunKeyPersistence()(CurrentImagePath() 经 NtQueryInformationProcess(ProcessImageFileName=27) 取自身路径后自引)与 RemoveRunKeyPersistence();零 Advapi32。EnumerateSystemHandles()、FindEDRHandles()(PID × 30+ 名单)、CloseEDRHandles()、IsProcessMonitored()。NtFreeVirtualMemory。