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

背景

直接系统调用(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类函数实现汇编指令如下

image.png

例如通过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'\0' };  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,rcx
0xb8,0xb9,0x00,0x00,0x00, //mov eax,0B9h
0x0f,0x05,                //syscall0xc3                      //ret
*/
unsigned char SYS_CALL_START_MAGIC[] = { 
 0x4c, 0x8b, 0xd1, 0xb8
};
#define SYS_CALL_START_MAGIC_LENGTH 4
#define MAX_SEARCH_LENGTH  24
DWORD 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算法。


本文作者:穿云箭安全实验室

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/151343.html

Tags:
评论  (1)
快来写下你的想法吧!

穿云箭安全实验室

文章数:1 积分: 0

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号