红队技术:绕过用户模式Hook,直接进行系统调用(二)
在本文中,我们将为读者全面介绍可以用来绕过用户模式hook的最流行、最有效的各种技术,并指出每种技术对红队的优缺点。最后,我们将总结一些防御者可以用来保护或检测这些技术的方法。
(接上文)
4. 提取SSN代码存根 (Disk)
打开C:\Windows\System32/NTDLL.dll的文件句柄。创建并映射一个带有SEC_COMMIT和PAGE_READONLY页保护的区段对象,以尝试绕过所有hook和通知。然后,通过解析PE头部并将调用存根复制到可执行内存中的方式来解析攻击者需要的系统调用。我们也可以用它来覆盖NTDLL现有副本中的任何潜在hook,但这需要使用NtProtectVirtualMemory,不过它可能早就被钩住了。大多数系统调用通常不超过32字节,但如果需要确定存根的长度的时候,其实64位PE文件支持一个异常目录,可以用来计算它。此外,NtOpenFile、NtCreateFile、NtReadFile也可能会被钩住,因为从磁盘读取NTDLL.dll会显得很可疑。
static
DWORD
WINAPI
RvaToOffset(
PIMAGE_NT_HEADERS NtHeaders,
DWORD Rva)
{
PIMAGE_SECTION_HEADER SectionHeader;
DWORD i, Size;
if(Rva == 0)return 0;
SectionHeader = IMAGE_FIRST_SECTION(NtHeaders);
for(i = 0;i<NUMBER_OF_SECTIONS(NtHeaders); i++) {
Size =SectionHeader[i].Misc.VirtualSize ?
SectionHeader[i].Misc.VirtualSize : SectionHeader[i].SizeOfRawData;
if(SectionHeader[i].VirtualAddress <= Rva &&
Rva<= (DWORD)SectionHeader[i].VirtualAddress + SectionHeader[i].SizeOfRawData)
{
if(Rva>= SectionHeader[i].VirtualAddress &&
Rva< SectionHeader[i].VirtualAddress +Size) {
returnSectionHeader[i].PointerToRawData + (Rva - SectionHeader[i].VirtualAddress);
}
}
}
return 0;
}
static
PVOID
WINAPI
GetProcAddressFromMappedDLL(
PVOIDDllbase,
const char*FunctionName)
{
PIMAGE_DOS_HEADER DosHeader;
PIMAGE_NT_HEADERS NtHeaders;
PIMAGE_SECTION_HEADER SectionHeader;
PIMAGE_DATA_DIRECTORY DataDirectory;
PIMAGE_EXPORT_DIRECTORY ExportDirectory;
DWORD Rva, Offset, NumberOfNames;
PCHAR Name;
PDWORD Functions, Names;
PWORD Ordinals;
DosHeader =(PIMAGE_DOS_HEADER)Dllbase;
NtHeaders =(PIMAGE_NT_HEADERS)((PBYTE)Dllbase + DosHeader->e_lfanew);
DataDirectory =(PIMAGE_DATA_DIRECTORY)NtHeaders->OptionalHeader.DataDirectory;
Rva =DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
Offset =RvaToOffset(NtHeaders, Rva);
ExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((PBYTE)Dllbase + Offset);
NumberOfNames = ExportDirectory->NumberOfNames;
Offset =RvaToOffset(NtHeaders, ExportDirectory->AddressOfNames);
Names =(PDWORD)((PBYTE)Dllbase + Offset);
Offset =RvaToOffset(NtHeaders, ExportDirectory->AddressOfFunctions);
Functions =(PDWORD)((PBYTE)Dllbase + Offset);
Offset = RvaToOffset(NtHeaders,ExportDirectory->AddressOfNameOrdinals);
Ordinals =(PWORD)((PBYTE)Dllbase + Offset);
do {
Name =(PCHAR)(RvaToOffset(NtHeaders, Names[NumberOfNames - 1]) + (PBYTE)Dllbase);
if(lstrcmpA(Name, FunctionName) == 0) {
return(PVOID)((PBYTE)Dllbase + RvaToOffset(NtHeaders,Functions[Ordinals[NumberOfNames - 1]]));
}
} while(--NumberOfNames);
return NULL;
}
5. 提取SSN (Disk)
它与前面描述的方法基本完全相同,唯一区别就是这里仅仅提取系统服务号(SSN)并用自己的代码存根手动执行。SyscallTables展示了如何转储这些数字,而hells Gate则展示了如何使用这些数字。
6.FireWalker
“FireWalker: A NewApproach to Generically Bypass User-Space EDR Hooking”一文中采用的方法是,安装一个向量异常处理程序,并将CPU陷阱标志设置为单步执行Win32 API或系统调用。然后,异常处理程序尝试定位原始系统调用存根。另一种方法是使用反汇编器和单独的例程来构建系统调用的调用图。Windows有一个内置的反汇编程序,可以用来计算指令的长度。不过,它的缺点是没有提供操作码的二进制视图,所以,Zydis反汇编程序库可能是一个更好的选择。在内部,windows的调试器引擎支持构建函数的调用图(以支持WinDbg中的uf命令),但不幸的是,目前还没有向开发人员公开API。
7.SysWhispers
SysWhispers包含一个Python脚本,该脚本可以为在AMD64/x64系统上运行的系统调用构造代码存根。并且,这些存根在WindowsXP/2003到Windows 10/2019的各个版本保持兼容。该生成器会用到从J00RU维护的列表中获取的SSN。并且,在运行过程中,它会根据通过PEB检测到的操作系统版本选择相应的SSN。在Windows的最新版本中,还可以使用KUSER_SHARED_DATA来选择要读取的主要版本、次要版本和内部版本。SysWhispers目前在红队中非常流行,常常用于绕过AV和EDR。下面是为NtopenProcess生成的示例代码存根:
NtOpenProcess:
mov rax,[gs:60h] ; Load PEBinto RAX.
NtOpenProcess_Check_X_X_XXXX: ; Check major version.
cmp dword[rax+118h], 5
je NtOpenProcess_SystemCall_5_X_XXXX
cmp dword[rax+118h], 6
je NtOpenProcess_Check_6_X_XXXX
cmp dword[rax+118h], 10
je NtOpenProcess_Check_10_0_XXXX
jmpNtOpenProcess_SystemCall_Unknown
NtOpenProcess_Check_6_X_XXXX: ; Check minor version forWindows Vista/7/8.
cmp dword[rax+11ch], 0
je NtOpenProcess_Check_6_0_XXXX
cmp dword[rax+11ch], 1
je NtOpenProcess_Check_6_1_XXXX
cmp dword[rax+11ch], 2
je NtOpenProcess_SystemCall_6_2_XXXX
cmp dword[rax+11ch], 3
je NtOpenProcess_SystemCall_6_3_XXXX
jmpNtOpenProcess_SystemCall_Unknown
NtOpenProcess_Check_6_0_XXXX: ; Check build number for WindowsVista.
cmp word[rax+120h], 6000
je NtOpenProcess_SystemCall_6_0_6000
cmp word[rax+120h], 6001
je NtOpenProcess_SystemCall_6_0_6001
cmp word[rax+120h], 6002
je NtOpenProcess_SystemCall_6_0_6002
jmpNtOpenProcess_SystemCall_Unknown
NtOpenProcess_Check_6_1_XXXX: ; Check build number for Windows7.
cmp word[rax+120h], 7600
je NtOpenProcess_SystemCall_6_1_7600
cmp word[rax+120h], 7601
je NtOpenProcess_SystemCall_6_1_7601
jmp NtOpenProcess_SystemCall_Unknown
NtOpenProcess_Check_10_0_XXXX: ; Check build number for Windows10.
cmp word[rax+120h], 10240
je NtOpenProcess_SystemCall_10_0_10240
cmp word[rax+120h], 10586
je NtOpenProcess_SystemCall_10_0_10586
cmp word[rax+120h], 14393
je NtOpenProcess_SystemCall_10_0_14393
cmp word[rax+120h], 15063
je NtOpenProcess_SystemCall_10_0_15063
cmp word[rax+120h], 16299
je NtOpenProcess_SystemCall_10_0_16299
cmp word[rax+120h], 17134
je NtOpenProcess_SystemCall_10_0_17134
cmp word[rax+120h], 17763
je NtOpenProcess_SystemCall_10_0_17763
cmp word[rax+120h], 18362
je NtOpenProcess_SystemCall_10_0_18362
cmp word[rax+120h], 18363
je NtOpenProcess_SystemCall_10_0_18363
cmp word[rax+120h], 19041
je NtOpenProcess_SystemCall_10_0_19041
jmpNtOpenProcess_SystemCall_Unknown
NtOpenProcess_SystemCall_5_X_XXXX: ; Windows XP and Server 2003
mov eax,0023h
jmpNtOpenProcess_Epilogue
NtOpenProcess_SystemCall_6_0_6000: ; Windows Vista SP0
mov eax,0023h
jmpNtOpenProcess_Epilogue
NtOpenProcess_SystemCall_6_0_6001: ; Windows Vista SP1 and Server 2008SP0
mov eax,0023h
jmpNtOpenProcess_Epilogue
NtOpenProcess_SystemCall_6_0_6002: ; Windows Vista SP2 and Server 2008SP2
mov eax,0023h
jmpNtOpenProcess_Epilogue
NtOpenProcess_SystemCall_6_1_7600: ; Windows 7 SP0
mov eax,0023h
jmpNtOpenProcess_Epilogue
NtOpenProcess_SystemCall_6_1_7601: ; Windows 7 SP1 and Server 2008 R2SP0
mov eax,0023h
jmpNtOpenProcess_Epilogue
NtOpenProcess_SystemCall_6_2_XXXX: ; Windows 8 and Server 2012
mov eax,0024h
jmpNtOpenProcess_Epilogue
NtOpenProcess_SystemCall_6_3_XXXX: ; Windows 8.1 and Server 2012 R2
mov eax,0025h
jmpNtOpenProcess_Epilogue
NtOpenProcess_SystemCall_10_0_10240: ; Windows 10.0.10240 (1507)
mov eax,0026h
jmpNtOpenProcess_Epilogue
NtOpenProcess_SystemCall_10_0_10586: ; Windows 10.0.10586 (1511)
mov eax,0026h
jmpNtOpenProcess_Epilogue
NtOpenProcess_SystemCall_10_0_14393: ; Windows 10.0.14393 (1607)
mov eax,0026h
jmpNtOpenProcess_Epilogue
NtOpenProcess_SystemCall_10_0_15063: ; Windows 10.0.15063 (1703)
mov eax,0026h
jmpNtOpenProcess_Epilogue
NtOpenProcess_SystemCall_10_0_16299: ; Windows 10.0.16299 (1709)
mov eax,0026h
jmpNtOpenProcess_Epilogue
NtOpenProcess_SystemCall_10_0_17134: ; Windows 10.0.17134 (1803)
mov eax,0026h
jmpNtOpenProcess_Epilogue
NtOpenProcess_SystemCall_10_0_17763: ; Windows 10.0.17763 (1809)
mov eax,0026h
jmpNtOpenProcess_Epilogue
NtOpenProcess_SystemCall_10_0_18362: ; Windows 10.0.18362 (1903)
mov eax,0026h
jmpNtOpenProcess_Epilogue
NtOpenProcess_SystemCall_10_0_18363: ; Windows 10.0.18363 (1909)
mov eax,0026h
jmp NtOpenProcess_Epilogue
NtOpenProcess_SystemCall_10_0_19041: ; Windows 10.0.19041 (2004)
mov eax,0026h
jmpNtOpenProcess_Epilogue
NtOpenProcess_SystemCall_Unknown: ; Unknown/unsupported version.
ret
NtOpenProcess_Epilogue:
mov r10, rcx
syscall
ret
8.按系统调用地址排序
有一种发现SSN的方法,既不需要加载NTDLL的新副本,也不需要进行unhooking,还不需要查询PEB或KUSER_SHARED_DATA以获取版本信息,更不需要手动从代码存根中读取它们。此外,该方法实施起来还很简单,并且适用于所有Windows版本。诚然,该方法基于某种勒索软件中使用的一种unhooking技术,但是该技术最初是由userman01在Discorde上提出的,按照他的话说:
“获取syscall索引的一种简便方法是——即使AV覆盖了它们——……简单地枚举所有Zw* 存根,然后按地址对其进行排序。”
听起来很完美!GetSyscallList()将解析NTDLL.dll的EAT,查找以“Zw”开头的所有函数名。在生成函数名称的哈希值之前,它将“Zw”替换为“Nt”。然后,将代码存根的哈希值和地址保存到SYSCALL_ENTRY结构表中。在收集好所有名称后,通过简单的冒泡算法,对代码地址进行升序排序。这样的话,SSN其实就是存储在表中的系统调用的索引。
#define RVA2VA(Type, Dllbase, Rva) (Type)((ULONG_PTR)Dllbase + Rva)
static
void
GetSyscallList(PSYSCALL_LIST List) {
PPEB_LDR_DATA Ldr;
PLDR_DATA_TABLE_ENTRY LdrEntry;
PIMAGE_DOS_HEADER DosHeader;
PIMAGE_NT_HEADERS NtHeaders;
DWORD i, j, NumberOfNames,VirtualAddress, Entries=0;
PIMAGE_DATA_DIRECTORY DataDirectory;
PIMAGE_EXPORT_DIRECTORY ExportDirectory;
PDWORD Functions;
PDWORD Names;
PWORD Ordinals;
PCHAR DllName, FunctionName;
PVOID Dllbase;
PSYSCALL_ENTRY Table;
SYSCALL_ENTRY Entry;
//
// Get theDllbase address of NTDLL.dll
// NTDLL isnot guaranteed to be the second in the list.
// so it's safer to loop through the fulllist and find it.
Ldr =(PPEB_LDR_DATA)NtCurrentTeb()->ProcessEnvironmentBlock->Ldr;
// For eachDLL loaded
for(LdrEntry=(PLDR_DATA_TABLE_ENTRY)Ldr->Reserved2[1];
LdrEntry->Dllbase != NULL;
LdrEntry=(PLDR_DATA_TABLE_ENTRY)LdrEntry->Reserved1[0])
{
Dllbase =LdrEntry->Dllbase;
DosHeader= (PIMAGE_DOS_HEADER)Dllbase;
NtHeaders= RVA2VA(PIMAGE_NT_HEADERS, Dllbase, DosHeader->e_lfanew);
DataDirectory =(PIMAGE_DATA_DIRECTORY)NtHeaders->OptionalHeader.DataDirectory;
VirtualAddress =DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
if(VirtualAddress == 0) continue;
ExportDirectory = (PIMAGE_EXPORT_DIRECTORY) RVA2VA(ULONG_PTR, Dllbase,VirtualAddress);
//
// If thisis NTDLL.dll, exit loop
//
DllName =RVA2VA(PCHAR,Dllbase, ExportDirectory->Name);
if((*(ULONG*)DllName | 0x20202020) != 'ldtn') continue;
if((*(ULONG*)(DllName + 4) | 0x20202020) == 'ld.l') break;
}
NumberOfNames = ExportDirectory->NumberOfNames;
Functions =RVA2VA(PDWORD,Dllbase, ExportDirectory->AddressOfFunctions);
Names = RVA2VA(PDWORD,Dllbase,ExportDirectory->AddressOfNames);
Ordinals = RVA2VA(PWORD, Dllbase,ExportDirectory->AddressOfNameOrdinals);
Table = List->Table;
do {
FunctionName = RVA2VA(PCHAR, Dllbase, Names[NumberOfNames-1]);
//
// Is thisa system call?
//
if(*(USHORT*)FunctionName== 'wZ') {
//
// SaveHash of system call and the address.
//
Table[Entries].Hash = HashSyscall(0x4e000074, &FunctionName[2]);
Table[Entries].Address = Functions[Ordinals[NumberOfNames-1]];
Entries++;
if(Entries == MAX_SYSCALLS) break;
}
} while(--NumberOfNames);
//
// Savetotal number of system calls found.
//
List->Entries = Entries;
//
// Sort thelist by address in ascending order.
//
for(i=0;i<Entries - 1; i++) {
for(j=0;j<Entries - i - 1; j++) {
if(Table[j].Address > Table[j+1].Address) {
//
//Swap entries.
//
Entry.Hash = Table[j].Hash;
Entry.Address = Table[j].Address;
Table[j].Hash = Table[j+1].Hash;
Table[j].Address = Table[j+1].Address;
Table[j+1].Hash = Entry.Hash;
Table[j+1].Address = Entry.Address;
}
}
}
}
下面是上述代码对应的AMD64/x64汇编代码:
;*************************************************
; Gather alist of system calls by parsing the
; exportaddress table of NTDLL.dll
;
; Generatea hash of the syscall name and save
; therelative virtual address to a table.
;
; Sorttable entries by virtual address in ascending order.
;
;**************************************************
%ifndefBIN
globalGetSyscallList_amd64
%endif
GetSyscallList_amd64:
; savenon-volatile registers
; rcxpoints to SYSCALL_LIST.
; it'ssaved last.
pushx rsi, rbx, rdi, rbp, rcx
push TEB.ProcessEnvironmentBlock
pop r11
mov rax, [gs:r11]
mov rax,[rax+PEB.Ldr]
mov rdi,[rax+PEB_LDR_DATA.InLoadOrderModuleList + LIST_ENTRY.Flink]
jmp scan_dll
;
; BecauseNTDLL.dll is not guaranteed to be second in the list of DLLs,
; wesearch until a match is found.
;
next_dll:
mov rdi,[rdi+LDR_DATA_TABLE_ENTRY.InLoadOrderLinks + LIST_ENTRY.Flink]
scan_dll:
mov rbx, [rdi+LDR_DATA_TABLE_ENTRY.Dllbase]
;
mov esi, [rbx+IMAGE_DOS_HEADER.e_lfanew]
add esi, r11d ; add 60h orTEB.ProcessEnvironmentBlock
; ecx =IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress
mov ecx,[rbx+rsi+IMAGE_NT_HEADERS.OptionalHeader + \
IMAGE_OPTIONAL_HEADER.DataDirectory + \
IMAGE_DIRECTORY_ENTRY_EXPORT * IMAGE_DATA_DIRECTORY_size + \
IMAGE_DATA_DIRECTORY.VirtualAddress - \
TEB.ProcessEnvironmentBlock]
jecxz next_dll ; if no exports, try next module inthe list
; rsi =offset IMAGE_EXPORT_DIRECTORY.Name
lea rsi, [rbx+rcx+IMAGE_EXPORT_DIRECTORY.Name]
; NTDLL?
lodsd
xchg eax, esi
add rsi, rbx
;
; Convertto lowercase by setting bit 5 of each byte.
;
lodsd
or eax, 0x20202020
cmp eax, 'ntdl'
jnz next_dll
lodsd
or eax, 0x20202020
cmp eax, 'l.dl'
jnz next_dll
;
; Load addressof SYSCALL_LIST.Table
;
pop rdi
push rdi
scasd ; skip Entries
push 0 ; Entries = 0
; rsi =offset IMAGE_EXPORT_DIRECTORY.Name
lea rsi, [rbx+rcx+IMAGE_EXPORT_DIRECTORY.NumberOfNames]
lodsd ; eax = NumberOfNames
xchg eax, ecx
; r8 =IMAGE_EXPORT_DIRECTORY.AddressOfFunctions
lodsd
xchg eax, r8d
add r8, rbx ; r8 = RVA2VA(r8, rbx)
; rbp =IMAGE_EXPORT_DIRECTORY.AddressOfNames
lodsd
xchg eax, ebp
add rbp, rbx ; rbp = RVA2VA(rbp, rbx)
; r9 =IMAGE_EXPORT_DIRECTORY.AddressOfNameOrdinals
lodsd
xchg eax, r9d
add r9, rbx ; r9 = RVA2VA(r9, rbx)
find_syscall:
mov esi, [rbp+rcx*4-4] ; rsi = AddressOfNames[rcx-1]
add rsi, rbx
lodsw
cmp ax, 'Zw' ; system call?
loopne find_syscall
jne sort_syscall
; hash thesystem call name
xor eax, eax
mov edx, 0x4e000074 ; "Nt"
hash_syscall:
lodsb
test al, al
jz get_address
ror edx, 8
add edx, eax
jmp hash_syscall
get_address:
movzx eax, word[r9+rcx*2] ; eax = AddressOfNameOrdinals[rcx]
mov eax, [r8+rax*4] ; eax = AddressOfFunctions[eax]
stosd ; save Address
xchg eax, edx
stosd ; save Hash
inc dword[rsp] ; Entries++
; exportsremaining?
test ecx, ecx
jnz find_syscall
;
; Bubblesort.
; ArrangesTable entries by Address in ascending order.
;
; based onthe 16-byte sort code by Jibz
;
;https://gist.github.com/jibsen/8afc36995aadb896b649
;
sort_syscall:
pop rax ; Entries
pop rdi ; List
stosd ; List->Entries = Entries
lea ecx, [eax - 1] ; ecx = Entries - 1
outerloop:
push rcx ; save rcx for outer loop
push rdi ; rdi = Table
push rdi ; rsi = Table
pop rsi
innerloop:
lodsq ; load Address + Hash
cmp eax, [rsi] ; do we need to swap?
jbe order_ok
xchg rax, [rsi] ; if so, this is first step
order_ok:
stosq ; second step, or justwrite back rax
loop innerloop
pop rdi
pop rcx ; restore number of elements
loop outerloop ; rcx is used for both loops
exit_get_list:
; restorenon-volatile registers
popx rsi, rbx, rdi, rbp
ret
要将系统调用名解析为SSN,我们可以使用以下函数。给定我们希望使用的系统调用名的哈希值,这个函数将在表中搜索匹配的系统调用名并返回SSN。如果操作系统不支持该系统调用,这个函数将直接返回FALSE:
//
// Get the System Service Number from list.
//
static
BOOL
GetSSN(PSYSCALL_LIST List, DWORD Hash, PDWORD Ssn) {
DWORD i;
for(i=0;i<List->Entries; i++) {
if(Hash ==List->Table[i].Hash) {
*Ssn =i;
returnTRUE;
}
}
returnFALSE;
}
下面是相应的汇编代码:
我们可以将用于执行SSN的代码存根可以嵌入PoC的.text区段中,但移动到不会被检测为手动调用的内存区域可能更有意义:
InvokeSsn_amd64:
pop rax ; return address
pop r10
push rax ; save in shadow space as _rcx
push rcx ; rax = ssn
pop rax
push rdx ; rcx = arg1
pop r10
push r8 ; rdx = arg2
pop rdx
push r9 ; r8 = arg3
pop r8
; r9 = arg4
mov r9, [rsp + SHADOW_SPACE_size]
syscall
jmp qword[rsp+SHADOW_SPACE._rcx]
下面的代码演示了如何使用上述函数调用ntdll!NtAllocateVirtualMemory:
SYSCALL_LIST List;
DWORD SsnId, SsnHash;
InvokeSsn_t InvokeSsn;
//
// Gather alist of system calls from the Export Address Table.
//
GetSyscallList(&List);
{
//
// Testallocating virtual memory
//
SsnHash =ct_HashSyscall("NtAllocateVirtualMemory");
if(!GetSSN(&List, SsnHash, &SsnId)) {
printf("Unable to find SSN for NtAllocateVirtualMemory :%08lX.\n", SsnHash);
return 0;
}
PVOIDbaseAddress = NULL;
SIZE_TRegionSize = 4096;
ULONGflAllocationType = MEM_COMMIT | MEM_RESERVE;
ULONGflProtect = PAGE_READWRITE;
NTSTATUSStatus;
InvokeSsn= (InvokeSsn_t)&InvokeSsn_stub;
printf("Invoking SSN : %ld\n", SsnId);
Status =InvokeSsn(
SsnId,
NtCurrentProcess(),
&baseAddress,
0,
&RegionSize,
flAllocationType,
flProtect
);
printf("Status : %s (%08lX)\n",
Status== STATUS_SUCCESS ? "Success" : "Failed", Status);
if(baseAddress != NULL) {
printf("Releasing memory allocated at %p\n", baseAddress);
VirtualFree(baseAddress, 0, MEM_RELEASE | MEM_DECOMMIT);
}
}
在根据userman01提出的想法写完代码后不久,我们又在这里发现了另一个实现同样想法的项目。
检测手动调用
防御方该如何保护自己?
字节签名和仿真
除非经过混淆/加密,否则映像中执行一个或多个系统调用的代码存根将清楚地表明恶意意图,因为没有任何合法理由让非微软应用程序直接执行它们。唯一的例外是恶意应用程序安装的UM hook发生了故障。为“syscall”指令提供YARA签名,或者为Fireeye的CAPA提供一个规则来自动发现它们,是一个好的习惯。一般来说,任何非微软的应用程序读取PEB或KUSER_SHARED_DATA都是干坏事的简单指标。通过Unicorn Engine模拟代码来检测混淆/加密代码内的存根,也是一个需要更多时间和精力来实现的想法,这一点并不难理解。
緩解政策
微软提供了一系列的缓解策略,可以在进程上强制执行,以阻止恶意代码的执行。其中,Import和Export Address Filtering是两种潜在的方法,可以防止系统调用名被枚举。此外,还有ProcessSystemCallDisablePolicy可以禁止Win32k系统调用user32.dll或win32u.dll库中的syscalls。另一个仍未被微软记录的策略是ProcessSystemCallFilterPolicy。
安插回调函数
在“Windows x64 systemservice hooks and advanced debugging ”一文中,对ProcessInstrumentationCallback信息类进行了详细的介绍;另外,Alex Ionescu在2015年Recon大会上题目为“ Hooking Nirvana presentation”的演讲中,也讨论过这个类。这个类不仅可以用于对系统调用进行post-processing,而且还可用于检测手动调用。防御者可以安装回调函数,并在每次调用后检查返回地址,以确定它是否源于NTDLL.dll、user32.dll、Win32u.dll或其他一些系统调用不该出现的内存区域。
ScyllaHide是一个使用这种检测方法的反调试库。但是,在写这篇文章的时候,它只能检查调用是否源于主机映像内部。一个简单的绕过方法,就是把返回地址改到主机映像外面的位置。如你所见,我们也可以操纵系统调用的NTSTATUS值。
ULONG_PTR
NTAPI
InstrumentationCallback(
_In_ULONG_PTR ReturnAddress,
_Inout_ULONG_PTR ReturnVal
)
{
PVOIDImagebase = NtCurrentPeb()->ImagebaseAddress;
PIMAGE_NT_HEADERS NtHeaders = RtlImageNtHeader(Imagebase);
// is thereturn address within the host image?
if(ReturnAddress >= (ULONG_PTR)Imagebase &&
ReturnAddress < (ULONG_PTR)Imagebase + NtHeaders->OptionalHeader.SizeOfImage)
{
// manualsystem call detected.
}
}
以下代码用于安装回调函数:
// Windows7-8.1 require SE_DEBUG for this to work, even on the current process
BOOLEANSeDebugWasEnabled;
Status =RtlAdjustPrivilege(SE_DEBUG_PRIVILEGE, TRUE, FALSE, &SeDebugWasEnabled);
PROCESS_INSTRUMENTATION_CALLBACK_INFORMATIONInstrumentationCallbackInfo;
InstrumentationCallbackInfo.Version = 0;
InstrumentationCallbackInfo.Reserved = 0;
InstrumentationCallbackInfo.Callback = InstrumentationCallback;
Status =NtSetInformationProcess(
ProcessHandle,
ProcessInstrumentationCallback,
&InstrumentationCallbackInfo,
sizeof(InstrumentationCallbackInfo)
);
幸运的是,对于红队来说,可以通过将callback设置为NULL来删除NtSetInformationProcess的所有回调函数。
Intel Processor Trace (IPT)
英特尔的二进制插桩工具,便于在指令级进行跟踪,具有触发和过滤功能,可用于拦截执行前后的系统调用。Intel Skylake及后续CPU型号也支持IPT,自1803版起,Windows10系统也提供了类似的功能。
- WinIPT for Windows RS5
- Windows Intel PT Support Driver
本文由secM整理并翻译,不代表白帽汇任何观点和立场
来源:
https://www.mdsec.co.uk/2020/12/bypassing-user-mode-hooks-and-direct-invocation-of-system-calls-for-red-teams/
最新评论