很早之前就看到过一篇MDSec的文章 https://www.mdsec.co.uk/2021/06/bypassing-image-load-kernel-callbacks/ 提到了一个很有趣的点,图像加载回调,也就是对应内核的 PsSetLoadImageNotifyRoutine
函数。
什么是图像加载回调 PsSetLoadImageNotifyRoutine
是 Windows 内核提供的一个函数,用于注册驱动程序提供的回调函数。当系统加载或映射 一个图像(例如 DLL 或 EXE)到内存时,内核会调用该回调函数,通知驱动程序此事件。
该函数的原型如下:
1 2 3 NTSTATUS PsSetLoadImageNotifyRoutine ( PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine );
其中,NotifyRoutine
是指向驱动程序实现的回调函数的指针。
Demo:
1 2 3 4 5 6 7 8 9 10 11 12 13 void LoadImageNotifyRoutine (PUNICODE_STRING imageName, HANDLE pid, PIMAGE_INFO imageInfo) { UNREFERENCED_PARAMETER (imageInfo); PEPROCESS process = NULL ; PUNICODE_STRING processName = NULL ; PsLookupProcessByProcessId (pid, &process); SeLocateProcessImageName (process, &processName); DbgPrint ("%wZ (%d) loaded %wZ" , processName, pid, imageName); }PsSetLoadImageNotifyRoutine (LoadImageNotifyRoutine);
需要注意的是,注册的回调函数数量是有限制的。在 Windows 8.1 之前,最多允许 8 个驱动程序同时注册回调函数。在 Windows 8.1 及之后的版本中,这一限制增加到 64 个。
在卸载驱动程序之前,必须调用 PsRemoveLoadImageNotifyRoutine
函数来注销先前注册的回调函数,以避免潜在的系统不稳定或资源泄漏。
通过使用 PsSetLoadImageNotifyRoutine
,驱动程序可以监控系统中加载的图像,从而实现特定的功能,如安全监控、日志记录或其他需要跟踪图像加载事件的操作。
那我们需要去理解推断一下,AV/EDR这些会如何利用这一回调。
监控进程启动和模块加载
通过 PsSetLoadImageNotifyRoutine
注册的回调函数,EDR 可以在每次进程加载可执行文件或动态链接库(DLL)时接收到通知。这让 EDR 能够在每个模块加载到内存时进行检查,并根据预设规则来判断文件是否可信或存在可疑行为。
识别恶意模块注入
恶意软件常通过 DLL 注入等技术将自身注入到合法进程中,以隐藏其活动。EDR 利用 PsSetLoadImageNotifyRoutine
能够识别这种模块注入行为,通过分析加载的模块源路径、名称等信息来判断是否属于常见的恶意模块注入特征。
阻止特定模块的加载
一些 EDR 软件利用 PsSetLoadImageNotifyRoutine
检测到特定模块(如未签名的或来源可疑的模块)加载时,可以采取阻止措施。例如,若 EDR 检测到某些已知恶意模块或不受信任的 DLL 试图加载时,可以直接阻止其加载过程,以避免潜在的攻击。
行为分析和威胁检测
许多 EDR 具备行为分析能力,会将进程加载的模块信息与恶意软件行为模型对比。如果进程加载的模块符合恶意行为模式,EDR 会记录并标记该进程为潜在威胁,甚至可以进一步追踪该进程的后续行为。
记录审计日志
EDR 还可以使用 PsSetLoadImageNotifyRoutine
来记录系统中所有进程的模块加载情况,以便后续审计和事件响应分析。这些日志信息对于调查入侵活动具有重要价值,帮助分析人员了解攻击者的持久化方式和活动范围。
检测进程篡改
一些恶意软件会尝试通过加载恶意模块来修改合法进程的行为,造成持久化攻击。通过监控模块加载活动,EDR 可以检测到这些篡改行为并发出警报,从而增强系统的完整性保护。
那么我们想要规避这个监控可以找到这个数据结构,修改 PsSetLoadImageNotifyRoutine
注册的回调函数指针。或者让PsSetLoadImageNotifyRoutine
返回无效名称,然后伪造内核中加载的模块名称并将其伪装成合法模块。亦或者修改内核变量 PspNotifyEnableMask
来完成等等,有很多方法这里不一一列举。
小记:
PsSetLoadImageNotifyRoutine
在内核内部维护一个列表(数组),存储着所有注册的图像加载回调函数指针。在正常情况下,当系统加载一个新的映像(如 DLL 或 EXE)时,会遍历这个列表,调用所有注册的回调函数。
通过定位并直接修改这个数据结构,我们可以:
修改回调函数指针 :将原有回调函数替换为自定义的函数,从而在图像加载时执行自定义逻辑。(筛选不希望报告的图像加载)
清空回调函数数组 :将数组内容清空,系统在遍历时找不到任何回调,从而达到关闭所有图像加载回调的效果。
但是我们在初始访问阶段很难做到这一点,所以我尝试跟随MDSec的脚步去探寻。
什么玩意儿触发了回调 我们浅浅的搓两个Demo,一个Dll和一个加载他的EXE。
我们分解LoadLibraryA
的调用过程:
调用 LoadLibraryA
LoadLibraryA
接收一个 DLL 文件的名称作为参数,并开始查找并加载该 DLL。
如果文件名为空,或无法找到指定的 DLL,LoadLibraryA
会返回错误。
转换为 LoadLibraryExA
调用
LoadLibraryA
实际上是调用 LoadLibraryExA
的简化版本。LoadLibraryExA
函数允许通过传递标志来设置加载选项(如加载方式、位置等)。
LoadLibraryExA
接收 DLL 名称和加载标志,将其转换为宽字符形式,然后调用对应的宽字符版本 LoadLibraryExW
。
调用 LoadLibraryExW
进行宽字符处理
LoadLibraryExW
是 LoadLibraryExA
的宽字符版本,它最终会调用 LdrLoadDll
,这是 ntdll.dll 中负责加载 DLL 的底层函数。
该函数通过 LdrLoadDll
处理具体的 DLL 文件路径,并决定加载的具体位置。
进入 ntdll.dll 的 LdrLoadDll
函数
LdrLoadDll
是加载 DLL 的核心,它首先检查指定的 DLL 是否已在内存中被加载过,以避免重复加载。
LdrLoadDll
会调用 LdrpLoadDllInternal
来继续加载过程。
使用 LdrpLoadDllInternal
进行内部加载
LdrpLoadDllInternal
在确保 DLL 需要加载后,会调用 LdrpProcessWork
,该函数处理具体的加载任务。
其中,LdrpProcessWork
会根据传入的 DLL 名称查找文件,处理路径和加载标志等。
调用 LdrpMapDllFullPath
进行 DLL 映射
LdrpProcessWork
使用 LdrpMapDllFullPath
函数查找 DLL 并进行文件映射。该函数会尝试将 DLL 的路径转换为 NT 路径格式,供后续调用使用。
如果查找到 DLL 文件,该函数会返回一个 DLL 句柄,准备映射文件。
调用 LdrpMapDllNtFileName
映射 DLL
LdrpMapDllNtFileName
将 DLL 文件映射到进程地址空间。它会调用 LdrpMapDllWithSectionHandle
将 DLL 的文件内容映射到内存中,以便后续执行。
在实际映射过程中会调用到 LdrpMinimalMapModule
。
调用 LdrpMinimalMapModule
处理映像文件
LdrpMinimalMapModule
将 DLL 文件的内容映射到内存中,并处理相关的页面映射等。
该函数会调用 LdrpMapViewOfSection
,然后进一步调用底层的 NtMapViewOfSection
系统调用,以将 DLL 文件的内容加载到目标地址空间。
调用 NtMapViewOfSection
进行最终内存映射
NtMapViewOfSection
是内核态的系统调用函数,通过内存管理服务将 DLL 文件的各个部分映射到合适的内存页。
这一步完成后,DLL 文件的内容将以只读或读写的方式加载到内存中。
返回 DLL 句柄
映射成功后,控制返回到 LdrLoadDll
并最终返回到用户态的 LoadLibraryA
。此时 LoadLibraryA
返回一个指向已加载 DLL 的句柄。
应用程序可以通过此句柄调用 DLL 中的函数。
也就是说LoadLibraryA
的调用实际上是通过多层封装与系统调用逐层转换和映射实现的。最终通过 NtMapViewOfSection
完成 DLL 文件的加载和内存映射,从而将 DLL 加载到进程的地址空间中,供应用程序调用。
我们着重分析 LdrpMapDllNtFileName
是因为该函数在模块加载过程中起到关键作用,它负责打开并映射 DLL 文件,将其加载到内存中,而这个过程中会触发 PsSetLoadImageNotifyRoutine
注册的回调函数。
直接丢进IDA
LdrpMapDllNtFileName(__int64 dllBase, UNICODE_STRING *dllName)
的第2个参数dllName
顾名思义傻子都知道是啥意思。然后尝试打开,使用NtOpen
,如果成功打开后则检查一下,然后丢给NtCreateSection
创建内存映射区段。但是这里我的和MDsec的逆向有些出入,但是并不影响大体。
我们可以轻松的追踪到最后我们进入了LdrpMapDllWithSectionHandle
方法,通过名字也可以才到他的用途吧。不说废话直接跟进去。
我们顺着LdrpMapDllWithSectionHandle->LdrpMinimalMapModule->LdrpMapViewOfSection->NtMapViewOfSection摸查,截图太累了
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 __int64 __fastcall LdrpMinimalMapModule (__int64 moduleInfo, __int64 sectionHandle) { _QWORD *moduleBaseAddress; __int64 unicodeComparisonResult; __int64 sectionProtectionFlags; char isKernel32Dll; int allocationType; struct _TEB *currentThreadEnvironmentBlock; int allocationTypeFlags; __int64 viewSize; int resultCode; __int64 tempValue; int finalResult; __int64 imageBaseAddress; __int64 tempResult; __int64 imageBaseParameters[2 ]; __int64 userModeAddressRange[4 ]; void *originalUserPointer; __int64 viewBaseAddress; __int64 prevPrivilegeState; viewBaseAddress = sectionHandle; moduleBaseAddress = *(moduleInfo + 56 ); LdrpLogInternal ("minkernel\\ntdll\\ldrmap.c" , 700 , "LdrpMinimalMapModule" , 3 , "DLL name: %wZ\n" , moduleBaseAddress + 9 ); LOBYTE (unicodeComparisonResult) = 1 ; if (!RtlEqualUnicodeString (moduleBaseAddress + 11 , &LdrpKernel32DllName, unicodeComparisonResult) || (isKernel32Dll = 1 , (*(LdrpAppHeaders + 22 ) & 0x20 ) == 0 )) { isKernel32Dll = 0 ; } prevPrivilegeState = 0 i64; allocationType = 0x800000 ; if (!isKernel32Dll) { if (LdrpLargePageDllKeyHandle) { imageBaseAddress = moduleBaseAddress[12 ]; LODWORD (originalUserPointer) = 0 ; RtlQueryImageFileKeyOption (LdrpLargePageDllKeyHandle, imageBaseAddress, 4 i64, &originalUserPointer, 4 , 0 i64); if (originalUserPointer) { if (RtlAcquirePrivilege (&LdrpLockMemoryPrivilege, 1 i64, 0 i64, &prevPrivilegeState) >= 0 ) allocationType = 0x20000000 ; } } } currentThreadEnvironmentBlock = NtCurrentTeb (); *(moduleInfo + 168 ) = 0 i64; originalUserPointer = currentThreadEnvironmentBlock->NtTib.ArbitraryUserPointer; currentThreadEnvironmentBlock->NtTib.ArbitraryUserPointer = moduleBaseAddress[10 ]; allocationTypeFlags = allocationType | 0x40000 ; viewSize = (*(moduleInfo + 32 ) & 0x800000 ) != 0 ? 2 : 128 ; if ((*(moduleInfo + 32 ) & 0x800000 ) == 0 ) allocationTypeFlags = allocationType; if ((*(moduleInfo + 32 ) & 0x800 ) != 0 ) { userModeAddressRange[1 ] = LdrpMaximumUserModeAddress; userModeAddressRange[0 ] = 0 i64; imageBaseParameters[1 ] = userModeAddressRange; userModeAddressRange[2 ] = 0 i64; imageBaseParameters[0 ] = 1 i64; resultCode = ZwMapViewOfSectionEx ( viewBaseAddress, -1 i64, moduleBaseAddress + 6 , 0 i64, moduleInfo + 168 , allocationTypeFlags, viewSize, imageBaseParameters, 1 ); } else { resultCode = LdrpMapViewOfSection ( viewBaseAddress, viewSize, (moduleBaseAddress + 6 ), sectionProtectionFlags, moduleInfo + 168 , allocationTypeFlags, viewSize, (moduleBaseAddress + 9 )); } finalResult = resultCode; currentThreadEnvironmentBlock->NtTib.ArbitraryUserPointer = originalUserPointer; if (allocationTypeFlags == 0x20000000 ) RtlReleasePrivilege (prevPrivilegeState); switch (finalResult) { case 1073741827 : goto LABEL_21; case 1073741838 : finalResult = LdrpProcessMachineMismatch (moduleInfo); break ; case 1073741878 : LABEL_21: if (!*(moduleInfo + 176 ) && LdrpMapAndSnapWork) { LOBYTE (tempValue) = 1 ; if (LdrpCheckForRetryLoading (moduleInfo, tempValue)) { finalResult = -1073741267 ; } else if (isKernel32Dll) { finalResult = -1073741800 ; } } break ; } if (moduleBaseAddress[6 ] && (finalResult < 0 || finalResult == 1073741838 )) { NtUnmapViewOfSection (-1 i64); moduleBaseAddress[6 ] = 0 i64; } LODWORD (tempResult) = finalResult; LdrpLogInternal ("minkernel\\ntdll\\ldrmap.c" , 909 , "LdrpMinimalMapModule" , 4 , "Status: 0x%08lx\n" , tempResult); return finalResult; }
我们直接摸进 LdrpMapViewOfSection
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 __int64 __fastcall LdrpMapViewOfSection( __int64 lpBaseAddress, __int64 hFile, __int64 lpFileOffset, __int64 dwNumberOfBytesToMap, __int64 flMapType, int dwMapFlags, int ImageSectionName, __int64 AllocationAttributes) { __int64 v11[3 ]; if (!LdrpHpatAllocationOptOut(AllocationAttributes)) return ZwMapViewOfSection(lpBaseAddress, -1 i64, lpFileOffset); v11[0 ] = 5 i64; v11[1 ] = 0 x80i64; return ZwMapViewOfSectionEx(lpBaseAddress, -1 i64, lpFileOffset, 0 i64, flMapType, dwMapFlags, ImageSectionName, v11, 1 ); }
到这里已经是我们所能触达的尽头。
根据逆向整个加载过程,我们可以发现两个事情发生了一个是回调的触发一个是ETW的日志记录,
当 LdrpMapViewOfSection
或其他加载函数调用 ZwMapViewOfSection
或 ZwMapViewOfSectionEx
来将文件映射到内存时,Windows 内核会触发 PsSetLoadImageNotifyRoutine
注册的图像加载回调。
怎么绕过? 回到正题,MDSec的研究员产生了灵魂发问,这玩意儿怎么绕过呢?
首先肯定是自己去实现一个完整加载方法去加载,但是这条路比较坎坷。整体实现经历了7个过程:
确保要加载的数据是有效的 PE
将标题和部分复制到内存中,设置正确的内存权限
对图像库进行重新定位
解析两个导入表
执行 TLS 回调
注册异常处理程序
调用 DLL 入口点 ( DllMain
)
有一个很知名的开源项目 DarkLoadlibrary:https://github.com/bats3c/DarkLoadLibrary
这里不重复造轮子,Havoc的作者Cracked5pider也实现过一个类似的项目https://github.com/Cracked5pider/LdrLibraryEx
需要注意的一点是为了避免Nt/ZwCreateSection,我们映射的时候使用NtAllocateVirtualMemory和VirtualAlloc
但是这个功能移植到我们的不管是Loader还是Implant中都过于复杂和臃肿,我们不妨先看一下目前有哪些常见的规则是针对于此的去寻找更好的解决方法。
PS.
关于DarkLoadLibrary我个人是不推荐的,DarkLoadLibrary本身是为了取代sRDI,但是他们都使用了近乎相同的办法来解析PEB,然后将DLL映射到内存并调用导出函数,相比较于DarkLoadLibrary我觉得sRDI更加成熟可靠,并且DarkLoadLibrary会产生大量的RX属性的私有内存这是非常可疑的。虽然他在对抗图像加载上的想法很棒~,但是我并不建议使用它去替代LoadLibrary。并且一个正常的程序如果加载的合法dll过少(在安全产品的眼中),这会显得非常突兀不是么。
破壳 如何评判恶意的图像加载事件 来自Elastic的一个检测规则
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 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 [rule] description = """ Identifies the load of a Windows network module by a process where the creating thread's stack contains frames pointing outside any known executable image. This may indicate evasion via process injection. """ id = "aa265fbd-4c57-46ff-9e89-0635101cc50d" license = "Elastic License v2" name = "Network Module Loaded from Suspicious Unbacked Memory" os_list = ["windows" ] reference = [ "https://www.elastic.co/security-labs/pikabot-i-choose-you" , "https://www.elastic.co/security-labs/spring-cleaning-with-latrodectus" , "https://www.elastic.co/security-labs/upping-the-ante-detecting-in-memory-threats-with-kernel-call-stacks" , ] version = "1.0.41" query = ''' sequence by process.entity_id [process where event.action == "start" and process.parent.executable != null and not process.Ext .token.integrity_level_name == "low" and not user.id : ("S-1-5-19" , "S-1-5-20" ) and not (process.executable : ("?:\\ Program Files (x86)\\ *" , "?:\\ Program Files\\ *" , "?:\\ ProgramData\\ *" , "?:\\ Users\\ *\\ AppData\\ *" ) and process.code_signature.trusted == true ) and not (process.executable : ("?:\\ Program Files (x86)\\ *" , "?:\\ Program Files\\ *" ) and process.Ext .relative_file_creation_time >= 80000 ) and not process.executable : ("?:\\ Windows\\ Microsoft.NET\\ Framework*\\ NGenTask.exe" , "?:\\ Windows\\ Microsoft.NET\\ Framework*\\ ngen.exe" ) and not (process.executable : "?:\\ Windows\\ Microsoft.NET\\ Framework*\\ mscorsvw.exe" and process.parent.executable : "?:\\ Windows\\ Microsoft.NET\\ Framework*\\ ngen.exe" ) and not (process.executable : "?:\\ WINDOWS\\ SysWOW64\\ DWRCS.EXE" and process.parent.executable : "?:\\ WINDOWS\\ SysWOW64\\ DWRCS.EXE" and process.parent.args : "-service" ) and not (process.executable : "?:\\ Windows\\ System32\\ LogonUI.exe" and process.parent.executable : "?:\\ Windows\\ System32\\ winlogon.exe" ) and not (process.executable : "?:\\ Windows\\ SysWOW64\\ icacls.exe" and process.args : "?:\\ Program Files\\ Tenable\\ Nessus Agent\\ *" and process.parent.executable : "?:\\ Windows\\ SysWOW64\\ msiexec.exe" ) and not (process.name : "rundll32.exe" and process.command_line : "*zzzzInvokeManagedCustomActionOutOfProc*" and process.parent.executable : "?:\\ Windows\\ sys*\\ msiexec.exe" ) and not (process.code_signature.subject_name : ("Mozilla Corporation" , "Commvault Systems, Inc." , "Google LLC" , "YANDEX LLC" , "ConnectWise, Inc." , "Brave Software, Inc." , "Opera Norway AS" , "GitHub, Inc." , "Stefan Ries" , "JetBrains s.r.o." , "Intel(R) Rapid Storage Technology" , "Waves Inc" , "Dell Inc" , "Lenovo" , "DameWare Development, LLC." , "Essential Objects, Inc*" , "HP Inc." , "Aina Maximit Oy" , "Logitech Inc" , "N-ABLE TECHNOLOGIES LTD" , "Cognizant TriZetto Software Group, Inc." , "win.acme.simple@gmail.com" , "Crownalytics, LLC" , "Kodak Alaris Inc." , "JAM Software GmbH" , "UBISOFT ENTERTAINMENT INC." , "DASSAULT SYSTEMES SE" , "Link Data Security A/S" , "Western Digital Technologies, Inc." , "Rockstar Games, Inc." , "SEMPERIS INC." , "Micro-Star International CO., LTD." , "Kaseya Holdings Inc" , "KASEYA US LLC" , "Intel(R) Software Development Products" , "Commvault Systems, Inc." , "AAC Infotray AG" , "CORE.AI SCIENTIFIC TECHNOLOGIES PRIVATE LIMITED" , "ClaimMaster Software LLC" , "Cellebrite DI LTD" ) and process.code_signature.trusted == true ) and not (process.pe.original_file_name : ("msedge.exe" , "msedgewebview2.exe" ) and process.code_signature.subject_name : "Microsoft *" and process.code_signature.trusted == true ) and not (process.executable : "?:\\ Windows\\ System32\\ Essentials\\ SharedServiceHost.exe" and process.parent.executable : "?:\\ Windows\\ System32\\ services.exe" ) and not (process.name : "powershell.exe" and process.parent.executable : "?:\\ Program Files (x86)\\ Lenovo\\ VantageService\\ *\\ LenovoVantage-(LenovoSystemUpdateAddin).exe" ) and not process.hash.sha256 : ("35542bc04fbfa2e3ef68837640e0459c6f99729c0c73578c08ab351cdf030696" , "1005dcfddfbde91cc967ecc6d778c81cb4f7bede03121a3e6e0d9bae12a033e0" , "53cec44e4fc9a3477d264c5edc9e376af8fcca20853faa289387f5bd7eaae05f" , "49113f4cd7bbf3343a43b13edd745008c4f41da1d80c9f89dc90a4b0bb39b8f8" , "0ab8a14e7fd42818608cc0916fc26a12b2ae6b976c97310dc86011713e455d2d" , "afa3dc1ecd4e15a869dc57a280aee930fc0bab1cd49e17afd3944ae4ad1fc91f" , "18b177280b0e0e05aa0e26807f34634c3384ae4f5901f41ff5bb0a720ea5c106" , "4f1528318085f6117734a27f34a317a83b096d55532000f81ed67b1bb10632fa" , "35f210c1f941f917b81c2516bee9de8f0495baca42095685e9b0b76f67250dc9" , "edef301528767963f460bf7fe497ca3b5c648627a2d13abe74d5544578b56bd4" , "f856a3c582ca689bdea3e784ef6a0db37011bd5ebb31d7c79d0328ebfcf6d8a4" , "fe62ba0a61191a9b54aab7ba3221479c451b042fa30a08957cacff84ddfe094b" , "183cd12fbdd93aa785d3793a8872c4a9730dd154f6482c39f94552e556a7b4e9" , "ed6c844c72fccd7e7799d448f5e74df368cafd2631858573e6e29110c213ac79" , "43a84e01b5ddecf0b6297277d53c72025a73b00b9f0073933a700695a240b5e8" , "f4cb2a001dcee0577046c56df5adfe989bc875c29ecfe27c9569849a9a6f0671" , "ed3730a3436454022e8cf1a27569babef8c9c348ea875f1df80cba9b743365c9" , "51346e95656164783ae4de9d6b202f28be87358eb0e056d2d7cdd12b502d30e7" , "889cb8827a400984bea2e0561a1efbf9d18044e879974da8baa750a0c63748c6" , "96dcdb449ab48b21b6efd33afc59ae163dea5fc597ffaf5dd7030b20d2624467" , "43714f0fc5cea25a6ee936fd36a83f86a45f61447c16c8e9255ef317089ce39c" , "52a5036d1578a6b899fb5d14fd3ab12af463e94ac791d1dddb22b7e8cc1f4bd7" , "f5f15ab19171dbcb58e757cd6446be41e04adf00797d712b07e544066ecd3c67" , "49b95a804337ee7e12092cc7a13c2eb6c3acb33f9bbaac8e820b9184063642f2" , "4c974212f5fc3720d58a8df6c2b9587b85949edc676f9f82921c04f067c261f1" , "ed3730a3436454022e8cf1a27569babef8c9c348ea875f1df80cba9b743365c9" , "121a4e78abe13c92a7ff91d2e91bc98173724072cc891ecbbc10765e8d5bd024" , "54e55d6da825d709cf495eb18be10d8cbb92ac1904c1359999d53d3fa42161e2" , "07e79814fde31ff5968d5c0448014d931cd3a2e59b2ac841bc53a155c333a1b9" , "afcee50eace500b9b2c3dc4faab371fd040d769ba3a5197ef3a8762fe5457337" , "e7883c7d57f5cfe7d1649ab138f62f5042f7acda8ed3c8664c9335c3ddba85c4" , "8982729121fa3b3e6c283437f5832916a5a3611374ac848368c6edaae5086257" , "63f2a4be0dfbaf2b9740aa5c2320d0290451d6d5581cc6f8e183ced9ea796d95" ) and not (process.executable : "?:\\ Windows\\ SysWOW64\\ dllhost.exe" and process.parent.name : "svchost.exe" and process.Ext .effective_parent.executable : "?:\\ Program Files (x86)\\ Skillbrains\\ lightshot\\ *\\ Lightshot.exe" ) and not (process.executable : ("?:\\ Windows\\ SysWOW64\\ DWRCST.EXE" , "?:\\ Windows\\ SysWOW64\\ DWRCS.EXE" ) and process.parent.executable : ("?:\\ WINDOWS\\ SysWOW64\\ runonce.exe" , "?:\\ Windows\\ SysWOW64\\ DWRCST.EXE" , "?:\\ Windows\\ SysWOW64\\ DWRCS.EXE" )) and not (process.executable : "?:\\ Windows\\ system32\\ winsat.exe" and process.parent.executable : "?:\\ Windows\\ system32\\ rundll32.exe" and process.parent.args : "sysmain.dll,PfSvWsSwapAssessmentTask" ) and not (process.executable : "?:\\ Windows\\ System32\\ inetsrv\\ w3wp.exe" and process.parent.executable : ("?:\\ Windows\\ System32\\ inetsrv\\ w3wp.exe" , "?:\\ Windows\\ System32\\ svchost.exe" )) and not (process.name : "rundll32.exe" and process.args : "--no-sandbox" and _arraysearch(process.parent.thread.Ext .call_stack, $entry , $entry .symbol_info : "?:\\ Windows\\ assembly\\ NativeImages_*\\ EO.Base\\ *\\ EO.Base.ni.dll*" )) and not (user.id : "S-1-5-18" and process.parent.executable : ("?:\\ Program Files (x86)\\ Advanced Monitoring Agent\\ featureres\\ PMESetup.exe" , "?:\\ PROGRA~2\\ ADVANC~1\\ featureres\\ PMESetup.exe" , "?:\\ PROGRA~2\\ ADVANC~1\\ downloads\\ NetworkManagementInstall.exe" , "?:\\ PROGRA~2\\ ADVANC~1\\ staging\\ _new_setup.exe" , "?:\\ PROGRA~2\\ ADVANC~1\\ downloads\\ MAV-Installer.exe" )) ] [library where dll.name : ("ws2_32.dll" , "wininet.dll" , "winhttp.dll" ) and process.thread.Ext .call_stack_contains_unbacked == true and ( process.thread.Ext .call_stack_summary : ("ntdll.dll|wow64.dll|wow64cpu.dll|wow64.dll|ntdll.dll|kernelbase.dll|Unbacked" , "ntdll.dll|wow64.dll|wow64cpu.dll|wow64.dll|ntdll.dll|kernelbase.dll|Unbacked|kernel32.dll|ntdll.dll" , "ntdll.dll|kernelbase.dll|Unbacked" , "ntdll.dll|iphlpapi.dll|Unbacked" , "ntdll.dll|winhttp.dll|Unbacked" , "ntdll.dll|kernelbase.dll|wininet.dll|Unbacked" , "ntdll.dll|kernelbase.dll|Unbacked|kernel32.dll|ntdll.dll" , "ntdll.dll|wow64.dll|wow64cpu.dll|wow64.dll|ntdll.dll|Unbacked" , "ntdll.dll|wow64.dll|wow64cpu.dll|wow64.dll|ntdll.dll|wininet.dll|Unbacked|ntdll.dll" , "ntdll.dll|wow64.dll|wow64cpu.dll|wow64.dll|ntdll.dll|Unbacked|kernel32.dll|ntdll.dll" , "ntdll.dll|kernelbase.dll|Unbacked|kernelbase.dll|ntdll.dll|kernel32.dll|ntdll.dll" ) or startswith~ (process.thread.Ext .call_stack_summary, concat(concat("ntdll.dll|wow64.dll|wow64cpu.dll|wow64.dll|ntdll.dll|kernelbase.dll|Unbacked|" , process.name), "|kernel32.dll|ntdll.dll" )) ) and not _arraysearch(process.thread.Ext .call_stack, $entry , $entry .callsite_leading_bytes : ("*6764a118008b40243b835b080000753033c089835b0800008d835f080000508d8353000000506a006a00ff936b080000*" , "45fc33c08945f88bf28dbd82fcffffb9dc000000f3a566a568008000006a0052ff55b86803800000ff55bc8bf080bd82fcffff0074188d8592fdffff50ff55c4" , "*d74533db4c8b4d504c894d284c8d0d0f0000004c894d404c8b4d6041c6410c00ffd0*" , "83ec28488b4c2430ff15b40f00004883c428c3cccccccccccccc4c894424188954241048894c24084883ec38837c244801755f488d0d701f0000ff15720f0000" , "55c0488d55c0488b4d10e8886dadff488bc8488975b8488b55b0488b5220488b02488bd64533db4c8b45b04c8945804c8d050a0000004c894598c6470c00ffd0" , "488bcd48894da8488d8d78ffffff48894b10488975c0488b4db8488b4920488b01488bcf488bd64c8b45b84c8945884c8d050a0000004c8945a0c6430c00ffd0" , "80000000c7858c00000001000000488b4d50488b4920488b01488b8d800000004533db488b555048895528488d150e00000048895540488b5560c6420c00ffd0" )) and not _arraysearch(process.thread.Ext .call_stack, $entry , $entry .callsite_trailing_bytes : ("*8945b4488bcce82c000000908b45b4488b55a8c6420c01488b55a8488b*" , "c6430c01833d*5f007406ff1*" , "48898424e80300004883bc24e803000000750eff94247e030000898424f0030000eb30488d8c2430010000ff94248e03000048898424e80300004883bc24e803" , "8bd885db751eff55c88945f8eb168d8592fdffff50ff55d08bd885db7506ff55c88945f856ff55bc85db0f847601000080bd82fcffff0074508b45fc05a00000" , "488b55a8c6420c01833d8bbeb25f007406ff1593c7b25f8945b4488bcce82c000000908b45b4488b55a8c6420c01488b55a8488b8d70ffffff48894a10488d65" , "**45a848894590eb00488b4590488b5588c6420c01488b5588" , "*488b5560c6420c01488b5560488b4d2048894a" , "c6470c01833d6cf1b45f007406ff15f40cb45f8bf0e8b5e76f5f85f6400f95c6400fb6f64883bdc0000000007423488b8dc0000000e825ebe25e448bc0488b95" , "488b5560c6420c01833db339f55f007406ff153b43f55f898584000000488b4d00e845000000908b8584000000488b5560c6420c01488b5560488b4d2048894a" , "898383080000c7837f080000010000006a0056ff9397080000ff938b*" , "41c64?0c01833d*" , "c6430c0148ba*f87f0000833a00740c*" , "c6470c01833d*" , "a3b8eb*" , "a314a2e40585c*" , "488b9570ffffffc6420*" , "49bb60c0*" , "488b5590c6420c01833d*" , "*ba0c0000004c8d4c2450448d423441ffd3*" , "*ca85c0b8000000000fb6d60f95c0898424f40200000ac22b742458d0d80fb6c0*" , "*898424300300008d14922b9c24d0010000f7d28bc3034424600fbbc20facc2183b44240892" , "41c6460c0148ba*7f0000833a00740c*" , "8be55d558bec83ec08c745f800000000*" , "48898424b80000004883bc24b8000000000f848d000000488b442440480510130000488bd0488b8c24b8000000ff94249000000048898424d80000004883bc24" , "898383080000c7837f080000010000006a0056ff9397080000ff938b08000050ff9393080000cc8b1c24c3ce0000087c00960046003a005c00500072006f0067" , "488944242048837c24200075*" , "c6430c01833d*" , "*0000ba0c0000004c8d4c2450448d423441ffd3*" , "41c6470c0148ba*7f0000833a00740c48b9*" , "41c6470c0148ba*4c8b6d8849897508498bc5488b9548ffffff498957104881" , "c6460c01833db6e5e75f007406ff15eae8e65f488bd8e8aa38b25f488bc3488b4d8048894e104883c4785b5e5f415c415d415e415f5dc367611910090010e20c" , "49bb60c05eb7ff7f000048b9*" , "488945984885c07507bac4020000eb2d6685db750*" , "*488b5588498956104883c4785b5e5f415c415d415e" , "c6460c01833d*" , "898383080000c7837f080000010000006a0056ff9397080000*" , "8be55d558bec83ec08c745f800000000c745fc02000000*" , "898383080000c7837f080000010000006a0056ff*" , "49bbb014*f0000ba0c0000004c8d4c2450448d423441ffd349bb403*" , "*00ba0c0000004c8d4c2450448d423441ffd349bb*" , "*48bf7063107ef67f0000488b064889074883c6084883c708488b06488907415f415e415d415c41*" , "84c07507e8b8836effeb498b45f88b55c089108b45e48945f4c70518c*" , "488b8d70ffffffc6410c01833df07bca5e007406ff15a47fc95e488b8d70ffffff488b9528ffffff48895110488945b0ff15804ce6fe8945c4488bcce8780000" , "498bd7488bcf4c8be8e819bbffff4533ff4d85ed0f84520200004c8b9760020000488d95e0000000440fb74424744533c9498bcd41ffd24c8be04885c00f8420" , "488943384885c07512ff15035608004c8d054ce80800e93fffffff4c3973407438663b6b1c7511488b5330488bcfff15d65a080085c07421488b4b40ff157857" , "89434885c07507f0ff054b360c000f57c04489bb2c0200004c89bb400200004c89bb480200004489bb500200000f1183580200000f1183680200000f11837802" , "488944242048837c242000750e488d0d7e0f0000e8a1ffffffeb37488d15980f0000488b4c2420ff153d0f0000488944242848837c242800750e488d0d910f00" , "c6430c0148ba446a76e3fd7f0000833a00740c48b9988375e3fd7f0000ff11488b5580488953104883c4785b5e5f415c415d415e415f5dc30000001910090010" , "f7d20fca85c08af3a37002f103b8000000000fca0f95c01af08984243803000033b424e001000003c60fc8e9a50100004a8bc6c1ea0b8d94248c01000003023b" , "488945984885c07507bac4020000eb2d6685db7505bbffff00000fb7d3488bc8448bca89542420448bc2488d058dc00000ff140785c0752cbad0020000488b8d" , "807c240c00894604740d85c0740583f8ff7504c64608008bc65ec20800568bf18b4e0433c085c9741083f9ff740bff74240851ff1598412f03807c240c007407" , "488906488bcb41ff542420488b064903e6eb17488bcb41ff5424384883f8087e06488b46f8eb034833c04883c420415e415c5f5e5bc300000000000000000000" , "48898424f00300004883bc24f003000000750eff94248e030000898424f80300008b8c24fc030000ff942476030000eb30488d8c2440010000ff94249e030000" , "8945f8837df80074658b45fc05601a0000508b4df8518b55fc8b8250170000ffd08945f4837df4007426b9010000006bd1008b45fc0fbe8c10641b000085c974" , "c6430c0148ba446a8e00f87f0000833a00740c48b998838d00f87f0000ff11488b5580488953104883c4785b5e5f415c415d415e415f5dc30000001910090010" , "48890550cf4700483bc30f849e010000488d15f0ab3800488bc8ff15a7173600488b0d30cf4700488d15c1ab380048890532cf4700ff158c173600488b0d15cf" , "4889842470020000488d0d1e5e1100ff15c0fc10004889442458488d0dfc5d1100ff15aefc100048898424a0020000488d15cf5d1100488b4c2458ff159cfc10" , "488944242048837c242000750e488d0d7e0f0000e8a1ffffffeb37488d15980f0000488b4c2420ff153d0f0000488944242848837c242800750e488d0d910f00" , "4885c0750bb8030000004883c4205bc3488d9318020000488bc8ff53084885c0750bb8040000004883c4205bc3488b4b10ffd033c04883c4205bc3cccccccccc" , "488944242048837c242000750e488d0d7e0f0000e8a1ffffffeb37488d15980f0000488b4c2420ff153d0f0000488944242848837c242800750e488d0d910f00" , "488b8d70ffffffc6410c01833d1479e65d007406ff1524cde55d488b8d70ffffff488b9528ffffff48895110488945b0ff1558ca1cfe8945c4488bcce8780000" , "4c8bd04889442458488d05345d00004803c6488d4c2458ffd084c07407bac3020000eb2d6685db7505bb102700000fb7d3488d053fbd0000448bca8954242044" , "86f285c00f94c28b9424c0020000a3302858048bc2b8000000000f90c00fa3d00f95c03ae084f48984245c0300000f94c4c0dc022b9c24bc0100008d049b0fa4" , "86f285c00f94c28b9424c0020000a33028*" , "488944242848837c242800750bff9424be03000089442430eb27488d8c2470010000ff9424ce030000488944242848837c242800750bff9424be030000894424" , "4c8bf04885c00f84cc00000083fb027c22488d4c2458c74424585b57485dc744245c2047504166c74424600a00ff9570010000488d542468498bceff55384c8b" , "488bc84885c075098d41034883c4205bc3488b4308488d9318020000ffd04885c0750bb8040000004883c4205bc3488b4b10ffd033c04883c4205bc3cccc488b" )) ] until [process where event.action:"end" ] ''' min_endpoint_version = "8.8.0" [[actions]] action = "kill_process" field = "process.entity_id" state = 1 [[optional_actions]] action = "rollback" field = "process.entity_id" state = 0 [[threat]] framework = "MITRE ATT&CK" [[threat.technique]] id = "T1055" name = "Process Injection" reference = "https://attack.mitre.org/techniques/T1055/" [threat.tactic] id = "TA0005" name = "Defense Evasion" reference = "https://attack.mitre.org/tactics/TA0005/" [internal ] min_endpoint_version = "8.8.0"
我们可以观察到Elastic密切监控了”ws2_32.dll”, “wininet.dll”, “winhttp.dll”这三个dll,这三个dll和我们的C2通讯密切相关,确切的说想要通讯我们就离不开这三个dll。
有人会说我可以使用其他库比如curl但是curl的底层通讯还是依赖ws2_32
。因为 ws2_32.dll
提供了 cURL
进行网络连接的基本 API,比如创建套接字、连接服务器、发送和接收数据等功能。
Elastic列出了很多个可疑的堆栈状态,我们只需要关注一点,那就是Dll的加载是从非备份内存发生的。这在大多数的C2和RAT上都会产生(当然只要他们使用Shellcode这种内存加载的方式)。
我们使用经典的C2 CobaltStrike来进行演示,
可以看到我们加载wininet.dll
的堆栈过程中,LoadLibraryA
是从非备份内存发起的。
那么安全产品就可以追踪到这块内存进行告警和内存扫描。
如何避免被评判 我们把思路转换一下,从如何避免产生转换为如何避免被评判为非法的,这样会让我们变的更加轻松愉快。
我们可以注意到,再逆向ntdll的过程中我们发现了两个可以使EDR知道我们加载了这个DLL的方案,一个是回调通知一个是ETW。但是归根结底,目前通用的检测方案是对于加载过程的堆栈进行评判,所以我们需要一个干净的堆栈。
什么是堆栈 灵魂发问:什么是堆栈?
确切的说我们这里是要讨论栈(stack),堆是(Heap)我们今天只讨论Stack。为了口语上的舒适我习惯称为堆栈。这里我白嫖一下BRC4作者的一些书面。
堆栈(Stack)是计算机科学中用于临时存储数据的一种数据结构,通常用于存放局部变量和函数参数。可以将堆栈想象成一叠书,最后放上去的书会最先被拿走,这就是“后进先出”(Last In, First Out, LIFO)的原则。
在程序执行时,每当一个新线程被创建时,系统会为这个线程分配一个新的堆栈空间。这个堆栈的初始大小通常为1MB,除非开发者在创建线程时明确指定其他大小。堆栈从底部向顶部增长,像一个叠书一样。在这个过程中,当前的堆栈指针由寄存器(在x64中是RSP,在x86中是ESP)来存储,指示着当前堆栈的顶部位置。
堆栈的特点是,它的管理是自动的。编译器会在编译过程中自动计算出每个函数所需的堆栈空间并生成相应的汇编指令。当函数被调用时,编译器会为该函数分配所需的堆栈空间,例如:
1 2 3 void samplefunction () { char test[8192 ]; }
在这个例子中,编译器会将这个函数转换为类似下面的汇编代码:
1 sub rsp, 0x2000 // 从堆栈中减去8192字节
这表示函数在运行时会从堆栈中保留8192字节用于变量存储。当函数执行完毕后,它会将之前分配的空间释放掉:
1 2 add rsp, 0x2000 // 将8192字节加回堆栈 ret // 返回到调用该函数的位置
每个函数的堆栈帧(stack frame)包含了函数的返回地址、局部变量以及用于返回的必要信息。当一个函数调用另一个函数时,当前函数的地址会被压入堆栈,以便在被调用的函数执行完毕后能正确返回到原来的位置。
当我们调用多个函数时,堆栈会逐层形成堆栈帧。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void func3 () { char test[2048 ]; return ; }void func2 () { char test[4096 ]; func3(); }void func1 () { char test[8192 ]; func2(); }
这段代码在汇编层面上大致会变成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func3: sub rsp, 0x800 // 分配2048字节 ; 执行某些操作 add rsp, 0x800 // 释放2048字节 ret // 返回 func2: sub rsp, 0x1000 // 分配4096字节 call func3 // 调用 func3 add rsp, 0x1000 // 释放4096字节 ret // 返回 func1: sub rsp, 0x2000 // 分配8192字节 call func2 // 调用 func2 add rsp, 0x2000 // 释放8192字节 ret // 返回
每个函数的堆栈帧记录了局部变量的大小、返回地址和其他必要的信息。这样,程序在执行时能清楚地知道当前的执行上下文是什么,能有效管理内存。
为了让五岁的孩子也可以理解,我尝试解释的更清楚一些(lol)
想象你有一个叠好的盘子,每次你想放一个新盘子时,你会把它放到最上面;每次你需要一个盘子时,你也会先拿走最上面的那个。这就是堆栈的工作原理:后放进去的东西先拿出来。
我们记住两个重要的知识点:
在程序中,堆栈主要用于存放:
局部变量 :函数内部定义的变量。
函数调用的信息 :比如当你在一个函数中调用另一个函数时,程序需要记住返回到哪个地方去执行接下来的代码。
比如这个很基础的Demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void funcA () { int x = 10 ; }void funcB () { int y = 20 ; funcA(); }void funcC () { int z = 30 ; funcB(); }
在这段代码中,函数 funcC
调用了 funcB
,而 funcB
又调用了 funcA
。当 funcC
被调用时,会发生以下事情:
分配空间:
调用 funcB
:
在 funcC
中调用 funcB
,这时 funcC
的返回地址(即执行完 funcC
后要返回的地方)会被保存到堆栈中。
为 funcB
分配堆栈空间,存储局部变量 y
。
调用 funcA
:
在 funcB
中调用 funcA
,funcB
的返回地址也会被保存到堆栈中。
为 funcA
分配堆栈空间,存储局部变量 x
。
返回的过程
当 funcA
执行完毕时,它会返回到 funcB
,并释放 funcA
的堆栈空间。接着,funcB
也执行完毕,返回到 funcC
,并释放 funcB
的堆栈空间。最后,funcC
完成后,释放它的堆栈空间。
我来给各位描绘一下:
当程序开始时,堆栈是空的。
当调用 funcC
时,堆栈中会添加 funcC
的信息,包括返回地址和局部变量 z
。
1 2 3 4 5 6 堆栈: +--------------------+ | 返回地址 (funcC后) | +--------------------+ | z = 30 | +--------------------+
在 funcC
中调用 funcB
,堆栈会添加 funcB
的信息,包括返回地址和局部变量 y
。
1 2 3 4 5 6 7 8 9 10 堆栈: +--------------------+ | 返回地址 (funcB后) | +--------------------+ | y = 20 | +--------------------+ | 返回地址 (funcC后) | +--------------------+ | z = 30 | +--------------------+
在 funcB
中调用 funcA
,堆栈会添加 funcA
的信息,包括返回地址和局部变量 x
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 堆栈: +--------------------+ | 返回地址 (funcA后) | +--------------------+ | x = 10 | +--------------------+ | 返回地址 (funcB后) | +--------------------+ | y = 20 | +--------------------+ | 返回地址 (funcC后) | +--------------------+ | z = 30 | +--------------------+
当 funcA
执行完毕时,它会返回到 funcB
,堆栈将释放 funcA
的空间。
1 2 3 4 5 6 7 8 9 10 堆栈: +--------------------+ | 返回地址 (funcB后) | +--------------------+ | y = 20 | +--------------------+ | 返回地址 (funcC后) | +--------------------+ | z = 30 | +--------------------+
当 funcB
执行完毕时,它会返回到 funcC
,堆栈将释放 funcB
的空间。
1 2 3 4 5 6 堆栈: +--------------------+ | 返回地址 (funcC后) | +--------------------+ | z = 30 | +--------------------+
当 funcC
执行完毕时,程序将返回到主程序,堆栈会清空。
这要是还不能理解,请不要看下去了,我真的会谢~🥲
思考破局之法 不管杀软是通过Userland下的Hook还是ETWTi亦或者内核上的一些处理,都可以捕获到我们的堆栈遥测。那么有人会说我们不用shellcode不就行了,我们在合法的模块不就行了…
先把那个说不用shellcode的同学叉出去。
我们先来思考怎么才能在合法的模块中呢?
1、欺骗过程中的返回地址
2、模块践踏
3、使用虚假的堆栈
PS.SentinelOne检测特定模块的堆栈的返回地址
在我的小玩具XSafe中正是实现了Userland的Hook,对调用地址进行了(可能较为充分)检查。
在过去,我关注到的一下Proxy dll load技术,虽然可能稍微有些过时,但是还可以拿出来讲一讲。他们来自于Nighthawk 以及 bruteratelc4。
代理模块加载 在我们红队操作过程中,许多时候都会直接或者间接的利用到LoadLibrary
函数,用来解析并且继续执行我们所需的 API 函数。最常见的场景就是动态加载动态调用(简直太常见了)。很多的安全产品已经根据这一点进行了针对性检查。安全产品可以通过堆栈遍历LoadLibrary
来检查其调用的来源,并验证合法性。
堆栈遍历是安全供应商用来分析程序调用堆栈的一种技术,它追溯函数调用的顺序,以确定 WinAPI 调用的来源和上下文。通过了解执行了哪些函数、它们的顺序以及调用它们的模块,来抓住隐藏在内存中的威胁。
在堆栈分析期间,安全工具会寻找常见异常,例如调用是否源自私有可执行内存(当payload驻留在VirtualAlloc
/分配的内存区域中时,这是一种常见指标VirtualAllocEx
)或者是否尝试加载常被滥用的库(如winhttp.dll
或wininet.dll
用于 HTTP/HTTPS 的通信)。
当 shellcode 在私有虚拟内存(即由VirtualAlloc
/分配的内存VirtualAllocEx
)中执行时,从该区域进行的每个函数调用都将具有指向私有虚拟内存的返回地址。
举个例子:
我们这里借用一个经典的方案RtlQueueWorkItem
,通过这个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 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 #include <Windows.h> #include <winternl.h> #include <stdio.h> #define NtCurrentProcess() ( ( HANDLE ) ( LONG_PTR ) -1 ) #define IMPORTAPI( DLLFILE, FUNCNAME, RETTYPE, ...) \ typedef RETTYPE( WINAPI* type##FUNCNAME )( __VA_ARGS__ ); \ type##FUNCNAME FUNCNAME = (type##FUNCNAME)GetProcAddress((LoadLibraryW(DLLFILE), GetModuleHandleW(DLLFILE)), #FUNCNAME); HMODULE getModuleHandle(LPCWSTR libraryName) { const LIST_ENTRY* head = &NtCurrentTeb()->ProcessEnvironmentBlock->Ldr->InMemoryOrderModuleList; LIST_ENTRY* next = head->Flink; while (next != head) { LDR_DATA_TABLE_ENTRY* entry = CONTAINING_RECORD(next, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks); const UNICODE_STRING* basename = (UNICODE_STRING*)((BYTE*)&entry->FullDllName + sizeof (UNICODE_STRING)); if (_wcsicmp(libraryName, basename->Buffer) == 0 ) { return entry->DllBase; } next = next->Flink; } return NULL ; } HMODULE queueLoadLibrary(WCHAR* libraryName, BOOL swtch) { IMPORTAPI(L"NTDLL.dll" , NtWaitForSingleObject, NTSTATUS, HANDLE, BOOLEAN, PLARGE_INTEGER); if (swtch) { IMPORTAPI(L"NTDLL.dll" , RtlQueueWorkItem, NTSTATUS, PVOID, PVOID, ULONG); if (NT_SUCCESS(RtlQueueWorkItem(&LoadLibraryW, (PVOID)libraryName, WT_EXECUTEDEFAULT))) { LARGE_INTEGER timeout; timeout.QuadPart = -500000 ; NtWaitForSingleObject(NtCurrentProcess(), FALSE , &timeout); } } else { IMPORTAPI(L"NTDLL.dll" , RtlRegisterWait, NTSTATUS, PHANDLE, HANDLE, WAITORTIMERCALLBACKFUNC, PVOID, ULONG, ULONG); HANDLE newWaitObject; HANDLE eventObject = CreateEventW(NULL , FALSE , FALSE , NULL ); if (NT_SUCCESS(RtlRegisterWait(&newWaitObject, eventObject, LoadLibraryW, (PVOID)libraryName, 0 , WT_EXECUTEDEFAULT))) { WaitForSingleObject(eventObject, 500 ); } } return getModuleHandle(libraryName); }int main() { WCHAR libraryName[] = L"wininet.dll" ; HMODULE moduleHandle = queueLoadLibrary(libraryName, TRUE ); printf("0x%p" , moduleHandle); }
我们把他丢进Shellcode框架中(后面再谈),或者偷懒的可以直接PE2shellcode。
可以看到LoadLibrary的发起者已经变成的RtlQueueWorkItem。但是这样会有三个缺陷我们先说一个另一个后面再说,最后一个我就不说(哎,气死你)
安全产品只需要对RtlQueueWorkItem
进行密切监控就可以解决问题,同样的类似的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 主函数 (main) │ ├── 定义 DLL 名称: "Wininet.dll" │ ├── 调用 queueLoadLibrary 函数 │ ├── 传入参数: DLL 名称, swtch (TRUE ) │ │ │ ├── 进入 queueLoadLibrary │ │ ├── 导入 API │ │ │ ├── NtWaitForSingleObject │ │ │ └── RtlQueueWorkItem │ │ │ │ │ └── 判断 swtch │ │ ├── TRUE │ │ │ ├── 调用 RtlQueueWorkItem │ │ │ │ ├── 封装 LoadLibraryW 调用 │ │ │ │ └── 传入 DLL 名称 │ │ │ │ │ │ │ └── 调用 NtWaitForSingleObject │ │ │ └── 等待一定时间 │ │ │ │ │ └── FALSE (未执行) │ │ │ ├── 调用 getModuleHandle 函数 │ │ ├── 遍历当前进程模块列表 │ │ │ └── 查找 DLL 基地址 │ │ │ │ │ └── 返回模块句柄或 NULL │ │ │ └── 返回到主函数 │ └── 打印模块句柄
RtlQueueWorkItem
之所以能帮助规避检测,主要是因为它允许在一个干净的线程上下文中异步执行代码。这意味着,当我们通过 RtlQueueWorkItem
请求加载一个 DLL 时,它实际上是在一个新的线程中运行,而不是在原始 shellcode 的线程中直接调用 LoadLibrary
,当然我们先忽略别的监测点。
我们用文字来抽象的描绘一下:
当然,可以通过文字描述来解释代理加载的流程、线程及堆栈的变化。下面是一个简化的描述,模拟这个过程:
初始状态 :
当进程启动时,系统为该进程分配一个默认的堆栈,此时的堆栈指针为 RSP
。
线程创建时,堆栈中可以保存函数参数和局部变量。
调用 shellcode :
当 shellcode 开始执行时,RSP
指向当前堆栈的顶部,堆栈中可能保存了一些初始的局部变量。
在 shellcode 中有一个调用,触发 LoadLibrary
以加载一个 DLL。
加载 DLL 的直接调用 :
如果直接调用 LoadLibrary
,那么当前线程的堆栈会被修改。堆栈框架中将存储调用 LoadLibrary
的返回地址、参数等信息。
检测系统可以通过堆栈跟踪发现这个调用链。
使用 RtlQueueWorkItem 进行异步加载 :
通过 RtlQueueWorkItem
提交一个异步任务,这个任务将在一个新的线程中执行。
在新的线程中,系统为其分配一个新的堆栈,这个堆栈与原始线程的堆栈是独立的。
新线程的堆栈 :
新线程的堆栈是干净的,初始状态下没有任何来自原始 shellcode 的调用信息。
在这个新堆栈中执行 LoadLibrary
,参数等信息在新堆栈中被处理。
返回到主线程 :
当 DLL 加载完成后,新线程的执行结束,返回地址将被处理,原始线程的堆栈保持不变。
原始线程继续执行后续的代码,但没有直接调用 LoadLibrary
的痕迹。
原始线程堆栈(未使用代理加载) 1 2 3 4 5 6 7 |-----------Top Of The Stack----------- | | 局部变量和参数 | |-------------------------------------- | |------Stack Frame of LoadLibrary------ | <-- 调用 LoadLibrary | 返回地址:shellcode 的地址 | | | |-----------Bottom Of The Stack-------- |
新线程堆栈(使用 RtlQueueWorkItem) 1 2 3 4 5 6 7 |-----------Top Of The Stack----------- | | 局部变量和参数 (LoadLibrary) | |-------------------------------------- | |------Stack Frame of LoadLibrary------ | <-- 调用 LoadLibrary | 返回地址:新线程的返回地址 | | | |-----------Bottom Of The Stack-------- |
通过使用 RtlQueueWorkItem
,加载 DLL 的过程被分离到一个新线程中,堆栈中的调用信息和返回地址与原始 shellcode 无关。
返回地址欺骗 我们现在来看另一种方案,返回地址欺骗。说实话这是一个古老的技术,最早用于外挂技术。
注意:我们需要一个找到一个基于非易失性寄存器跳转(jmp rbx)
Demo代码如下:
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 .code SpoofStub PROC pop r11 add rsp , 8 mov rax , [rsp + 24 ] mov r10 , [rax ] mov [rsp ], r10 mov r10 , [rax + 8 ] mov [rax + 8 ], r11 mov [rax + 16 ], rbx lea rbx , fixup mov [rax ], rbx mov rbx , rax jmp r10 fixup: sub rsp , 16 mov rcx , rbx mov rbx , [rcx + 16 ] jmp QWORD PTR [rcx + 8 ] SpoofStub ENDP end
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 #pragma once #include <Windows.h> #include "Spoofer.h" typedef struct _PRM { const void * trampoline; void * function; void * rbx; } PRM, * PPRM;typedef BOOL (WINAPI* fnCheckGadget) (PBYTE) ;extern "C" PVOID SpoofStub (PVOID, PVOID, PVOID, PVOID, PPRM, PVOID, PVOID, PVOID, PVOID, PVOID) ;PVOID FindGadget (PVOID pModule, fnCheckGadget CallbackCheck) { for (int i = 0 ;; i++) { if (CallbackCheck ((PBYTE)pModule + i)) return (PBYTE)pModule + i; } }BOOL fnGadgetJmpRbx (PBYTE pAddr) { if ( ((PBYTE)pAddr)[0 ] == 0xFF && ((PBYTE)pAddr)[1 ] == 0x23 ) return TRUE; else return FALSE; }PVOID Spoofer (LPCSTR ModuleName, PVOID pFunction, PVOID pArg1, PVOID pArg2, PVOID pArg3, PVOID pArg4, PVOID pArg5, PVOID pArg6, PVOID pArg7, PVOID pArg8) { PVOID pGadgetAddr = NULL ; PVOID pK32 = GetModuleHandleA (ModuleName); pGadgetAddr = FindGadget (pK32, fnGadgetJmpRbx); PRM param = { pGadgetAddr, pFunction }; PVOID pRet = SpoofStub (pArg1, pArg2, pArg3, pArg4, ¶m, NULL , pArg5, pArg6, pArg7, pArg8); return pRet; }
我们的工作流程如下:
保存返回地址 :
pop r11
从栈中弹出返回地址(即调用 SpoofStub
的返回地址)并保存到 r11
中。
设置栈指针 :
add rsp, 8
跳过调用约定中的参数区域,为后续操作准备栈空间。
获取参数 :
mov rax, [rsp + 24]
将 param
的地址存储在 rax
中。
设置返回地址 :
mov r10, [rax]
获取 param.trampoline
(即查找到的 jmp rbx
指令地址)。
mov [rsp], r10
将这个地址存储为新的返回地址,确保在执行完被钩取函数后能跳转到正确的位置。
修改钩取函数信息 :
mov r10, [rax + 8]
获取被钩取函数的地址,并将原返回地址(r11
)存储到 param.function
中,以便后续恢复。
mov [rax + 16], rbx
保存当前 rbx
寄存器的值到 param.rbx
。
设置钩取的 trampoline 地址 :
使用 lea rbx, fixup
将控制流转向 fixup
标签,随后将其地址存储到 param.trampoline
。
跳转到被钩取的函数 :
jmp r10
跳转到被钩取的函数地址,执行实际的功能。
恢复状态 :
在 fixup
标签处,首先恢复栈指针,并还原 rbx
的值。
最后,通过 jmp QWORD PTR [rcx + 8]
跳转回原始的返回地址,确保控制流回到 SpoofStub
的调用处。
通过操作栈和寄存器,创建了一个伪装的返回地址。它将原本的返回地址替换为一个新的地址,控制函数执行的返回流。
我们用这种方法来加载Wininet看一下有什么区别
1 Spoofer (pKernel32, pLoadLibraryA, PVOID("wininet.dll" ), 0 , 0 , 0 , 0 , 0 , 0 , 0 );
可以观察到我们和LoadLibrary之间隔了一个ReleasePackageVirtualizationContext
,这是啥呢,这就是我们前面找到的jmp。也就是说我们的LoadLibrary的返回地址来自ReleasePackageVirtualizationContext
偏移0x1fa的地方。这样安全产品再分析的时候可能到这里就结束了。
这是一个比较敷衍的办法,但是我觉得这是一个很好的学习教材,正如我所提倡的思维不定式,我希望更多人可以从中的到启发,而不是一味的迷信固定式解决,返回地址欺骗同样有他的价值,以及它可以衍生出其他的对抗方式,这里不做详细解释~
代理加载(New) BRC4作者提出他在ntdll中逆向出了至少27个可用的回调来代理执行Windows Api,这听起来很酷,可惜我是个懒B~
介绍一下 Windows 回调函数是指由应用程序或操作系统在特定事件发生时调用的函数。回调机制通常用于事件驱动编程,它允许程序在发生特定条件时执行自定义代码。这些函数可以在多种场景中使用,例如消息处理、定时器、异步操作等。
回调函数的特点
异步执行 :回调函数通常在事件发生后被调用,而不是在主程序的控制流中被直接调用(Oops),这使得程序能够处理多个任务而不阻塞。
灵活性 :通过使用回调函数,程序可以在运行时动态决定要执行的操作,从而提高了灵活性和可重用性。
参数传递 :回调函数可以接受参数,这使得它们能够处理特定的数据或上下文信息。
常见用途
消息循环 :在Windows程序中,消息循环使用回调函数来处理来自系统的消息。
定时器 :可以设置定时器,当定时器到期时调用指定的回调函数。
异步I/O :在异步操作完成时,操作系统会调用相应的回调函数以处理结果。
说个题外的基础:如何在Windows API中注册回调
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <windows.h> void CALLBACK TimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime) { MessageBox(NULL , "Timer triggered!" , "Info" , MB_OK); }int main() { SetTimer(NULL , 0 , 1000 , (TIMERPROC)TimerProc); MSG msg; while (GetMessage(&msg, NULL , 0 , 0 )) { TranslateMessage(&msg); DispatchMessage(&msg); } return 0 ; }
当定时器到期时,TimerProc
回调函数会被调用,显示一个消息框。
Microsoft 贴心的为软件开发人员提供了大量回调,以便通过其他函数执行代码。有一位热心的Hxd已经替我们整理好了很多可以用来恶意利用的 https://github.com/aahmad097/AlternativeShellcodeExec 相信很多人都熟悉这个。
正如Paranoid Ninja所言所有这些回调都存在一个主要问题。执行回调时,回调与调用者线程位于同一线程中。
也就是我们上面返回地址欺骗中所提到的尴尬局面:
LoadLibrary returns to -> Callback Function returns to -> RX region
为了获得干净的堆栈,我们需要确保我们的 LoadLibrary 在独立于我们的 RX 区域的单独线程中执行,如果我们使用回调,我们需要回调能够将适当的参数传递给LoadLibraryA
。Windows 中的大多数回调要么没有参数,要么不将参数“按原样”转发给我们的目标函数“LoadLibrary”。
Paranoid Ninja说的很好(不插嘴了)
我们可以据此有一个大概的判断,线程池 API 可能是不错的选择。在目标线程中排队一个异步过程调用(APC)或许也可以。总之我们要找到合适的Windows Api。
开始摸索 Paranoid Ninja给出的Demo中选择了使用TpAllocWork
TpPostWork
TpReleaseWork
这三个Windows Api去完成这一操作。
TpAllocWork
、TpPostWork
和 TpReleaseWork
是 Windows 线程池 API 中的一组函数,用于创建和管理线程池中的工作项。它们的功能如下:
TpAllocWork :
用于分配一个工作项。
可以指定一个回调函数,这个函数将在后台线程中执行。
允许传递可选参数(虽然在某些情况下参数可能无法按预期传递)。
TpPostWork :
将之前通过 TpAllocWork
分配的工作项排入线程池,以便在可用线程上异步执行。
一旦工作项被排入,它将在线程池中执行,而不会阻塞调用线程。
TpReleaseWork :
用于释放与工作项相关的资源。
在工作项完成执行后调用,以确保没有内存泄漏或资源浪费。
虽然我对挖掘更多方式感兴趣,但是我太懒了一点也不想动,所以我仅仅给我我的推测和观点:
寻找特定的 API, “callback”、”thread”、”work” 等关键字。然后分析函数的参数,看是否可以
通过指针、结构体等传递参数。然后检查 API 是否支持异步或后台线程执行,并且观察一下调用的堆栈状态。
(https://processhacker.sourceforge.io/doc/nttp_8h.html)
TpAllocWork
的原型如下:
1 2 3 4 5 6 NTSTATUS NTAPI TpAllocWork ( PTP_WORK* ptpWrk, PTP_WORK_CALLBACK pfnwkCallback, PVOID OptionalArg, PTP_CALLBACK_ENVIRON CallbackEnvironment );
其中 pfnwkCallback
是回调函数指针,OptionalArg
是一个可选参数,可以传递给回调函数。
LoadLibraryA
是一个直接调用 Windows API 加载 DLL 的函数。为了在 TpAllocWork
中使用它,需要一个符合回调函数签名的函数。
TpAllocWork
期望的回调函数类型是 PTP_WORK_CALLBACK
,但 LoadLibraryA
不符合这个签名,因此必须间接使用。
由于 LoadLibraryA
直接调用不符合 TP_WORK_CALLBACK
的签名,因此需要用一个包装函数(如前面提到的 WorkCallback
)来接收 libName
参数,并调用 LoadLibraryA
。在这种情况下,libName
作为 OptionalArg
被传递:
WorkCallback
可以接收 Context
参数,并将其传递给 LoadLibraryA
。
回调函数签名什么意思呢?
回调函数签名是指函数的定义,包括返回类型、函数名称和参数类型。它描述了如何正确调用该函数,以及可以接受哪些类型的参数。签名通常用于指明函数的接口,使得其他代码能够正确地调用该函数。
以 Windows API 中的 TP_WORK_CALLBACK
类型为例,其签名如下:
1 2 3 4 5 VOID CALLBACK WorkCallback( PTP_CALLBACK_INSTANCE Instance , PVOID Context , PTP_WORK Work );
这个签名的组成部分是:
返回类型 :VOID
表示该函数不返回任何值。
调用约定 :CALLBACK
指明了调用约定,表示该函数的参数如何在调用时通过栈传递(例如谁负责清理栈)。
函数名称 :WorkCallback
是函数的名称,调用时将使用这个名称。
参数列表 :
PTP_CALLBACK_INSTANCE Instance
:第一个参数是指向回调实例的指针,提供上下文信息。
PVOID Context
:第二个参数是一个指向用户定义数据的指针,可以传递给回调的额外信息。
PTP_WORK Work
:第三个参数是指向工作对象的指针,代表与当前工作相关的上下文。
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 <windows.h> #include <stdio.h> #pragma comment(lib, "ntdll.lib" ) VOID CALLBACK WorkCallback ( _Inout_ PTP_CALLBACK_INSTANCE Instance, _Inout_opt_ PVOID Context, _Inout_ PTP_WORK Work ) { LoadLibraryA (Context); }int main () { CHAR* libName = "wininet.dll" ; PTP_WORK WorkReturn = NULL ; TpAllocWork (&WorkReturn, WorkCallback, libName, NULL ); TpPostWork (WorkReturn); TpReleaseWork (WorkReturn); WaitForSingleObject ((HANDLE)-1 , 1000 ); printf ("hWininet: %p\n" , GetModuleHandleA (libName)); return 0 ; }
我们的堆栈现在好像变得清澈无比(陷入沉思),但是!Paranoid Ninja想要追求更完美的堆栈,那么哪里不完美呢。
具体来说,调用 LoadLibraryA
后,返回地址的顺序变成了:
LoadLibraryA
的返回地址(指向回调函数)。
回调函数的返回地址(指向 RtlUserThreadStart
)。
最后是 TpPostWork
的返回地址。
根据我的观察:
当调用 LoadLibraryA
时,栈的状态可以描述如下:
LoadLibraryA
的返回地址 :这个地址指向 WorkCallback
函数结束后程序应该跳转的位置,即 LoadLibraryA
完成后会返回到这个地址。
WorkCallback
的返回地址 :在 LoadLibraryA
被调用时,程序的控制权会传递给 WorkCallback
,这个地址是 WorkCallback
函数结束后应该返回的位置,通常是指向 RtlUserThreadStart
的地址。
RtlUserThreadStart
的返回地址 :这是系统级的调用,指向线程的起始点,通常是由 TpPostWork
调用的返回地址。
因此,这个栈的结构可以被表示为:
1 2 3 4 5 6 7 |------------------------ | <- 栈顶 |RtlUserThreadStart返回地址 | |------------------------ | | WorkCallback返回地址 | <- 返回到 RtlUserThreadStart |------------------------ | | LoadLibraryA返回地址 | <- 返回到 WorkCallback |------------------------ | <- 栈底
1 2 3 4 5 6 > KernelBase.dll !LoadLibraryA () 未知 ntdll.dll !TppWorkpExecuteCallback () 未知 ntdll.dll !TppWorkerThread () 未知 kernel32.dll !BaseThreadInitThunk () 未知 ntdll.dll !RtlUserThreadStart () 未知
我可怜的ProcessHacker卡住了用文字描述吧。
不能理解的多看几遍自行理解,当 LoadLibraryA
被调用时,返回地址(Callback in RX Region
)也会被压入栈。这意味着返回过程会变得更加复杂,最终的栈帧包含了许多额外的层级。我们现在试图直接从调用TpPostWork
到LoadLibrary
省略中间的一层弯弯绕绕导致不必要的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 #include <windows.h> #include <stdio.h> typedef NTSTATUS (NTAPI* TPALLOCWORK) (PTP_WORK* ptpWrk, PTP_WORK_CALLBACK pfnwkCallback, PVOID OptionalArg, PTP_CALLBACK_ENVIRON CallbackEnvironment) ;typedef VOID (NTAPI* TPPOSTWORK) (PTP_WORK) ;typedef VOID (NTAPI* TPRELEASEWORK) (PTP_WORK) ; FARPROC pLoadLibraryA;UINT_PTR getLoadLibraryA () { return (UINT_PTR)pLoadLibraryA; }extern VOID CALLBACK WorkCallback (PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WORK Work) ;int main () { pLoadLibraryA = GetProcAddress (GetModuleHandleA ("kernel32" ), "LoadLibraryA" ); FARPROC pTpAllocWork = GetProcAddress (GetModuleHandleA ("ntdll" ), "TpAllocWork" ); FARPROC pTpPostWork = GetProcAddress (GetModuleHandleA ("ntdll" ), "TpPostWork" ); FARPROC pTpReleaseWork = GetProcAddress (GetModuleHandleA ("ntdll" ), "TpReleaseWork" ); CHAR* libName = "wininet.dll" ; PTP_WORK WorkReturn = NULL ; ((TPALLOCWORK)pTpAllocWork)(&WorkReturn, (PTP_WORK_CALLBACK)WorkCallback, libName, NULL ); ((TPPOSTWORK)pTpPostWork)(WorkReturn); ((TPRELEASEWORK)pTpReleaseWork)(WorkReturn); WaitForSingleObject ((HANDLE)-1 , 0x1000 ); printf ("hWininet: %p\n" , GetModuleHandleA (libName)); return 0 ; }
MASM
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 extern getLoadLibraryA:near .code WorkCallback PROC mov rcx , rdx xor rdx , rdx call getLoadLibraryA jmp rax WorkCallback ENDP END
为什么要这么做呢,我们直接看重点WorkCallback
根据前面Stack的介绍我们知道,函数调用时需要保存调用者的上下文,所以我们直接调用 LoadLibraryA
,而不通过 WorkCallback
的返回地址,用一些汇编技巧,这样看起来就像是我们实现了一个LoadLibraryA
的Stub。:通过这种方法,调用 LoadLibraryA
时不会将 WorkCallback
的返回地址推入栈中,堆栈结构保持简单,没有多余的层级。
mov rcx, rdx
将传入的库名(存储在 RDX 中)移动到 RCX 中,这是 LoadLibraryA
的第一个参数。
xor rdx, rdx
将 RDX 清零,作为 LoadLibraryA
的第二个参数。
call getLoadLibraryA
调用一个函数以获取 LoadLibraryA
的地址。
jmp rax
跳转到 LoadLibraryA
的地址,直接执行而不推送返回地址到栈中。
现在再回头看我们的堆栈,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 猜测: +-----------------------------+ | 返回地址 (TpPostWork) | <- 栈顶 +-----------------------------+ | WorkCallback 上下文 | +-----------------------------+ | libName (参数) | +-----------------------------+ | ... | +-----------------------------+ | (空或其他局部变量) | +-----------------------------+ | LoadLibraryA 返回地址 | <- 被清空,不在栈中 +-----------------------------+
1 2 3 4 5 6 > kernel32.dll !LoadLibraryAStub () 未知 ntdll.dll !TppWorkpExecuteCallback () 未知 ntdll.dll !TppWorkerThread () 未知 kernel32.dll !BaseThreadInitThunk () 未知 ntdll.dll !RtlUserThreadStart () 未知
绕过重获新生 Emmm真不错我们成功的学会了这个对抗技术,但是很快就被推出了新的检测规则,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 sequence by process.entity_id [process where event.action == "start"] [library where ( ((dll.Ext.relative_file_creation_time <= 300 or (dll.Ext.device.product_id : ("Virtual DVD-ROM", "Virtual Disk") and not dll.path : "C:\\*")) and (dll.code_signature.trusted == false or dll.code_signature.exists == false )) or dll.name : ("vaultcli.dll", "wmiutils.dll", "taskschd.dll", "dnsapi.dll", "dsquery.dll", "mstask.dll", "mstscax.dll", "sqlite3.dll", "clr.dll", "coreclr.dll", "ws2_32.dll", "wininet.dll", "dnsapi.dll", "winhttp.dll", "psapi.dll", "bitsproxy.dll", "softokn3.dll", "System.Management.Automation.dll", "Wldap32.dll") ) and process.thread.Ext.call_stack_summary : ("ntdll.dll|kernelbase.dll|ntdll.dll|kernel32.dll|ntdll.dll", "ntdll.dll|wow64.dll|wow64cpu.dll|wow64.dll|ntdll.dll|kernelbase.dll|ntdll.dll|kernel32.dll|ntdll.dll")] until [process where event.action == "end"]
简单理解一下
首先按进程 ID 进行排序**:
库文件条件 :
要求以下情况之一成立:
DLL 文件的相对创建时间小于等于 300 秒,或者设备产品 ID 为虚拟 DVD-ROM 或虚拟磁盘且不在 “C:*” 路径下,同时文件的代码签名不受信任或不存在;
或者 DLL 的名称为特定列表中的某一个,例如 “vaultcli.dll”、”wmiutils.dll” 等等。
线程调用栈条件 :
仅选择具有特定调用栈摘要的进程。这些摘要包括 “ntdll.dll|kernelbase.dll|ntdll.dll|kernel32.dll|ntdll.dll” 或类似结构的组合。
结束条件 :
也就是说我们可以观察到的组合有两种
调用栈组合 1:
ntdll.dll -> kernelbase.dll -> ntdll.dll -> kernel32.dll -> ntdll.dll
调用栈组合 2:
ntdll.dll -> wow64.dll -> wow64cpu.dll -> wow64.dll -> ntdll.dll -> kernelbase.dll -> ntdll.dll -> kernel32.dll -> 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 C:\Windows\System32 \ntdll.dll!ZwMapViewOfSection +0 x 14 C:\Windows\System32 \ntdll.dll!LdrControlFlowGuardEnforced +0 x 69 c C:\Windows\System32 \ntdll.dll!LdrControlFlowGuardEnforced +0 x 29 a C:\Windows\System32 \ntdll.dll!LdrControlFlowGuardEnforced +0 x 424 C:\Windows\System32 \ntdll.dll!RtlQueryPerformanceCounter +0 x 24 f C:\Windows\System32 \ntdll.dll!RtlQueryPerformanceCounter +0 x 708 C:\Windows\System32 \ntdll.dll!RtlQueryPerformanceCounter +0 xad0 C:\Windows\System32 \ntdll.dll!RtlImageRvaToSection +0 x 1e4 C:\Windows\System32 \ntdll.dll!RtlUnicodeToCustomCPN +0 x 3 fc C:\Windows\System32 \ntdll.dll!LdrLoadDll +0 xfa C:\Windows\System32 \KernelBase.dll!LoadLibraryExW +0 x 172 C:\Windows\System32 \KernelBase.dll!LoadLibraryExA +0 x 31 C:\Windows\System32 \KernelBase.dll!LoadLibraryA +0 x 3 f C:\Windows\System32 \ntdll.dll!TpReleaseWork +0 x 262 C:\Windows\System32 \ntdll.dll!RtlClearThreadWorkOnBehalfTicket +0 x 79 c C:\Windows\System32 \kernel32 .dll!BaseThreadInitThunk +0 x 1 d C:\Windows\System32 \ntdll.dll!RtlUserThreadStart +0 x 28
很显然命中了:ntdll.dll|kernelbase.dll|ntdll.dll|kernel32.dll|ntdll.dll
那么我们只要把这个过程改变为使用LdrLoadDll
直接越过了中间的kernelbase.dll
堆栈的调用过程变成了 ntdll.dll|kernel32.dll|ntdll.dll
最合理的调用过程应该是 ntdll.dll|kernelbase.dll|kernel32.dll|ntdll.dll
写的有些疲惫了,如有写错的地方请联系我及时更正,感激不尽!