0、背景
近年来随着开源社群和安全商业化的发展,以Elastic为代表的防守方逐步将原先讳莫如深的检测规则公开,而CobalStrike、Nighthawk等商业化远控产品也必须做出回应以重新获取信任,这场在端上展开的猫鼠游戏逐步走向了白热化,本文仅以第三方视角回顾下这场战争
1、文件查杀篇
远控和后渗透工具想要获得落脚点首先要逃避杀软的拦截,早年间主要以加密、混淆的对抗方式为主,近年来演变出了三种主要的流派
1.1 、内存加载流
(1)ReflectiveLoad
dll注入以前是一种很流行的技术,核心是调用CreateRemoteThread远程执行LoadLibrary函数,参数是想要注入的dll路径,使得白进程加载执行恶意功能 以绕过检测。但缺点是待加载的dll文件需要落盘,在这个位置就提供了检测的空间。2013年stephenfewer提出的改进方案就是大名鼎鼎的ReflectiveDLLInjection:
dll声明一个导出函数ReflectiveLoader,注入的时候分配一段RWX的空间将dll写入目标进程,CreateRemoteThread远程执行ReflectiveLoader,函数的主要作用就是手动完成了LoadLibrary的动作,实现了纯内存加载的方案
而后来的CobalStrike则以一种更优雅的方式优化了这个过程
上图是原始的反射Dll和CS优化后的区别,可以看到在DOS Header的填充是有明显区别的,他是将PE直接变成了shellcode!这样就可以直接将rip指向这段内存而不必再去计算导出函数的偏移地址了,具体实现原理如下:
DOS Header中现在还有意义的只有MZ的标志头和e_lfanew,剩余的位置可以随意填充
MZ的标志头当成字节码对应的汇编是:
dec ebp;
pop edx;
逆反操作就是
inc ebp;
push edx;
接着
call 0 ; 获取下一条指令的内存地址
pop edx ; 把下一条指令给edx
add edx,functionoffset-0x09 ; 计算在内存的位置
push ebp ;
mov ebp,esp ;
call edx ; 调用ReflectiveLoader
CobalStrike就是通过这种方式实现了分离免杀,隔离了loader和beacon
但ReflectiveLoader的关键词也成了非常明显的检测特征
2017年NSA的DOUBLEPULSAR武器库被泄露,另一种的反射dll加载技术被公开,后来受此启发有了sRDI项目,主要解决了原始反射注入的两个问题:
1、因为要重新编译所以需要源码
2、通过LoadLibrary执行,所以载荷要在DllMain中,无法执行导出函数中的内容
具体的实现是:多加载了LdrLoadDll、LdrGetProcAddress两个函数以执行导出函数的内容
然后在dll的前面引入两个shellcode模块
Bootstrap负责获取内存中的当前位置、计算和设置寄存器、带着需要执行的函数hash等数据传递给RDI
RDI则负责内存展开dll、调用DllMain和根据hash调用导出函数
这种方式的检测特征变的更少了,因为去除了DOS Header中的大部分信息,并且在执行后清理掉了相关内存,但是堆栈信息还是比较明显的
能看到Sleep调用的上层来自未知内存区域,特征还是比较明显的
继续进化,为了响应CobalStrike的User-Defined Reflective Loader,出现了BokuLoader项目
缝合了几种反检测能力,比如DLL模块踩踏,间接系统调用等,后面的章节会进行详细分析
(2)execute-assembly
对于后渗透工具来说也有类似的免杀需求,那有没有能直接内存加载还不用改造二进制的方案呢?
CobalStrike 3.11版本中引入了execute-assembly功能,可以直接在内存加载C#的二进制程序,原理如下:
C#提供了两种官方的函数可以直接实现反射加载的能力
1、Assembly.Load
2、Load_3
但前提是agent需要有CLR的环境才能调用,这里面就涉及到先加载CLR环境的问题,一种方式如下:
CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (LPVOID)&metaHost); //检索接口ICLRMetaHost
metaHost->GetRuntime(L”v4.0.30319”, IID_ICLRRuntimeInfo, (LPVOID)&runtimeInfo); //用于指定版本的接口
runtimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_ICLRRuntimeHost, (LPVOID*)&runtimeHost); //用于将对象加载到当前进程并检索接口ICLRRuntimeHost
runtimeHost->Start(); //在当前进程初始化CLR
通过这种方式执行端上完全没有新进程的创建记录,但是检测方案也很明显
通过ETW消费Microsoft-Windows-DotNETRuntime日志即可发现
而后xpnsec再一次优化了方案,hook了EtwEventWrite函数直接返回,就可以绕过ETW的日志上报
HMODULE hNtdll = GetModuleHandleA(“ntdll.dll”);
LPVOID pEtwEventWrite = GetProcAddress(hNtdll, “EtwEventWrite”);
VirtualProtectEx(pi.hProcess, (LPVOID)pEtwEventWrite, 1, PAGE_EXECUTE_READWRITE, &oldProtect);
WriteProcessMemory(pi.hProcess, (LPVOID)pEtwEventWrite, &patch, sizeof(char), NULL);
但这种方案又带来了新的检测点,在moneta等内存扫描工具中可以明显标识出ntdll的内存被修改,原理是:通过查询Working Set是否变为了Private,正常来说这段dll所在的内存应该是Shared状态,如果被修改后,就会转为Private
继续优化,现在需要不修改内存就能劫持函数执行流的方案,很明显就是硬件断点,运行到EtwEventWrite时,流程转到RtlAddVectoredExceptionHandler注册的异常处理函数,直接返回即可
(2)BOF
上面的方式只能运行CSharp的后渗透工具,但其他格式也有执行的需求,CobalStrike在4.1版本引入了BOF(Beacon Object File)格式
是已编译但未链接产生的Obj文件,在beacon.h中定义了一系列基础能力比如BeaconFormatPrintf、BeaconInjectProcess
然后在加载时:加载读取bof文件、解析IMAGE_FILE_HEADER、IMAGE_SECTION_HEADER、IMAGE_SYMBOL等位置,处理数据重定位、填充函数指针,最后根据符号表找到go函数,call进去
1.2 、载荷转移流
随着PatchGuard之类安全机制的上线,安全软件的权限也被收敛,监控也被赶到了使用系统的回调API,现在做进程的安全检查的一般逻辑:注册PsSetCreateProcessNotifyRoutineEx回调,其接收的第一个参数为NotifyRoutine 是一个指向PCREATE_PROCESS_NOTIFY_ROUTINE_EX的指针 他的第三个参数为CreateInfo 指向PS_CREATE_NOTIFY_INFO
EDR、杀软等通过FileObject找到磁盘上的文件进行扫描,如果能实现FileObject跟实际执行内容不一致,就可以绕过检测
启动进程的一般流程:
打开要启动的可执行文件的句柄:hFile = CreateFile(“svchost.exe”)
创建一个image节,节将文件映射到内存:hSection = NtCreateSection(hFile, SEC_IMAGE),这就是上面的FileObject
使用image节创建进程:hProcess =NtCreateProcessEx(hSection)
分配进程参数和环境变量:CreateEnvironmentBlock & NtWriteVirtualMemory
创建在进程中执行的线程:NtThreadEx,触发回调
(1)Process Hollowing
以暂停模式启动傀儡进程,Unmap掉原始内容再重写为新的内容
检测:如果做了API hook,Unmap目标进程的exe模块的行为就非常可疑
(2)process_overwriting
overwriting应该算hollowing的一种不优雅变体,需假设新pe的image size小于原始的image size,就可以不进行unmap直接替换
检测:
(3)Process Doppelgänging
不进行unmap的另一种方案:Doppelgänging 直接替换Image
利用事务回滚在开始执行前替换内容
1、NtCreateTransaction 创建一个事物
2、CreateFileTransacted 打开原进程句柄
3、WriteFile & NtCreateSection 在原句柄中写入恶意内容,创建section
4、NtRollbackTransaction 回滚
5、NtCreateThreadEx 开始执行
这种方式仅在当时有效,后续就直接被官方下场ban掉了
可参考git issues中的讨论
(4)Process Herpaderping
同样是做Image的替换,Herpaderping采用的方式是利用时间差修改磁盘上的文件
检测:sysmon为此直接增加了一种检测类型,判断磁盘文件与执行内容是否一致
(5)Process Ghosting
正常来说可执行文件被映射后是不能被删除的,但可以”拆分”删除的过程,将映射的步骤插入其中,即可实现类似linux的那种进程可执行文件被删除的效果
创建一个文件,标记FileDispositionInformation.DeleteFile=True
检测:会被记录Image is locked for access
(6)Process Reimaging
Process Reimaging跟前面的方案有些区别, 欺骗的是K32GetProcessImageFileName等API的返回结果,当进程创建后修改FILE_OBLOG文件路径,此时VAD已经记录且不会更新
具体的实现方式还是挺简单的,执行进程后修改文件夹名称即可触发
微软也发布更新补丁修正了结果,整体实战价值还是比较有限
1.3 、lolbins
杀软等终端防护工具因为系统稳定性 && 对合法签名的信任 等原因一般不会删除隔离这类进程(火绒是意外),如果能利用其本身的功能在内存中加载就可以保持存活并且迷惑对手,这就是前几年比较火的不落地执行概念
(1)lolbins
https://lolbas-project.github.io/ 之类的项目总结出了一系列合法系统进程、脚本、第三方进程等代理执行恶意载荷的方式
检测逻辑也很简单,针对命令行参数挨个加规则即可,属于体力活
(2)DotNetToJScript
17年James Forshaw开发出了DotNetToJScript技术,将.Net的二进制程序序列化后,再通过Assembly.Load加载,支持js/vbs/vba等多种格式,利用时可以直接作为脚本通过cscript/wscript执行,也可以转换成wmic等lolbins依赖的xsl格式远程加载,开创了一种新的思路
具体实现:
1、从文件中读取.Net二进制文件
2、创建一个绑定委托,未来调用Assembly.Load去在内存中加载托管程序集
3、通过BinaryFormatter序列化对象,再base64后填充到对应的模板
4、执行时通过ActiveXObject调用COM中的反序列化器,反序列化后动态加载
检测:
win10 中引入了AMSI机制,并将DotNetToJScript的特征加入检测列表
(3)GadgetToJScript
对DotNetToJScript的一种改进,直接通过类似.Net反序列化漏洞利用的方式执行,并绕过了AMSI和.Net 4.8+对Assembly.Load的阻止
1、.Net 4.8+ 版本中对开启了WLDP的设备,ActivitySurrogateSelector链在触发时强制进行白名单检查,GadgetToJscript在启用了-b参数时,会先发序列化一次,通过AppSettings配置禁用检查
2、调用Assembly.LoadFrom / CompileAssemblyFromFile 进行加载或编译
2、通过ysoserial.net的ActivitySurrogateSelector链进行反序列化,执行Assembly.Load操作
3、base64之后填入模板
从上面的动作中没看到任何劫持amsi.dll之类的操作,那为什么可以宣称可以绕过AMSI呢?答案很可能来自于这个项目
AMSI是基于关键词/关键函数的,DotNetToJScript手动调用了DynaticInvoke导致触发了检测,但GadgetToJScript通过gadget的链触发,不会触发检测
检测:主要依赖wscript加载clr.dll等.net环境信息
(未完待续)
发表评论
您还未登录,请先登录。
登录