背景
什么是Direct System Call
怎么使用
NtFuncXXX PROC
mov r10, rcx
mov eax, {syscallId}
syscall
ret
NtFuncXXX ENDP
方法一
https://github.com/jthuraisamy/SysWhispers,该工具在github上有接近600的Star,最后生成的汇编示例如下:
通过收集各个系统的调用号,结合系统版本,进行条件判断,最后执行syscall
该方式优点:
-
能够绕过核心函数监控,应该是最早的一代Direct System Call
该方式缺点
-
生成代码较为臃肿;
-
由于系统号全部写死,不能适用于后面发布的最新Windows系统
方法二
整体原理是加载ntdll.dll镜像,通过PE文件读取到导出函数的代码段,然后利用VirtualAlloc将代码段放置在内存中,最后进行函数指针调用。
该方式优点:
-
通过读取ntdll.dll的实际代码方式,将ntdll的真实代码进行调用,避免ntdll.dll已被HOOK;
-
其它的函数也可以参考,很好的一种绕过HOOK的方法。
该方式缺点:
整合优化
获取函数存根
方法一:通过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方式,避免字符串
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文件格式转换,读取到响应的函数地址。
获取系统调用号
/*
0x4c,0x8b,0xd1, //mov r10,rcx
0xb8,0xb9,0x00,0x00,0x00, //mov eax,0B9h
0x0f,0x05, //syscall
0xc3 //ret
*/
unsigned char SYS_CALL_START_MAGIC[] = {
0x4c, 0x8b, 0xd1, 0xb8
};
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;
}
汇编指令
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
变量定义
extern "C"
{
VOID SetSyscallId(DWORD syscallId);
NTSTATUS WINAPI DynamicSyscall();
}
系统调用号获取
/*
0x4c,0x8b,0xd1, //mov r10,rcx
0xb8,0xb9,0x00,0x00,0x00, //mov eax,0B9h
0x0f,0x05, //syscall
0xc3 //ret
*/
unsigned char SYS_CALL_START_MAGIC[] = {
0x4c, 0x8b, 0xd1, 0xb8
};
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
动态调用
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);
总结
有如下优点:
-
兼容性强,不需要将系统调用号写死,兼容性可以有保障;
-
通过asm实现代码段,通过改变值进行函数调用;
-
从已经加载的DLL中获取系统号,并且考虑了已经被简单HOOK的场景。
待优化点:
-
由于汇编指令中全局变量,目前线程不安全,后期可以通过引入外部函数的方式进行加锁或者用户态自行实现;
-
函数名称的方式可使用函数Hash的方式,如果自己搞工具,可以自定义一套FastHash算法。
转自:穿云箭安全实验室