吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 412|回复: 4
上一主题 下一主题
收起左侧

[系统底层] 动态获取系统调用号

  [复制链接]
跳转到指定楼层
楼主
ClumsyBear 发表于 2026-6-11 14:49 回帖奖励
本帖最后由 ClumsyBear 于 2026-6-11 23:34 编辑

【新手向】动态获取系统调用号 —— 绕过用户态 Hook 的利器



很多刚接触 Windows 底层的同学都会遇到一个问题:杀软/EDR 把 ntdll.dll 里的函数 Hook 了,我们的调用被拦截怎么办?
一个经典的解法是 直接系统调用(Direct System Call)——不经过 ntdll.dll,自己触发 syscall 指令。但这里有个坑:系统调用号(System Service Number, SSN)在不同 Windows 版本间是会变化的。硬编码 SSN 意味着你的代码只能在特定版本上跑,于是便有了这篇文章。
本文将介绍一种动态获取调用号的方法:从磁盘上的 ntdll.dll 文件中读取原始字节,提取 SSN,然后填入自己的调用模板。这样无论 Win10 还是 Win11,无论什么稀奇古怪版本,应该都能自动适配。


我们知道一个API进入内核执行的流程
应用程序【kernel32!OpenProcess】 → 用户态(kernelbase!OpenProcess) → (ntdll!NtOpenProcess) → 系统调用(syscall 指令)
→ 内核态(ntoskrnl!KiSystemService / KiSystemService64)
→ 查表(根据 SSN 在 SSDT 中查找对应内核函数)
→ 检查(参数校验、权限检查、ETW 日志、回调等)
→ 执行(调用真正的内核函数,如 NtOpenProcess → ObOpenObjectByPointer)
→ 返回(原路返回:内核→syscall 返回→ntdll→kernelbase→kernel32→应用程序)


什么是系统调用号(SSN)

Windows 用户态程序想要执行特权操作(如打开文件、创建进程),必须通过系统调用进入内核。每个内核服务都有一个唯一的编号,这个编号就是 SSN
例如 NtOpenProcess 的 SSN 在不同版本中可能是 0x26。

ntdll.dll 的调用桩(Stub)
在ida中打开‘ntdll.dll`,查看函数NtOpenProcess(其他导出函数也一样),你会看到这样的 x64 汇编:
[C] 纯文本查看 复制代码
NtOpenProcess:
    mov     r10, rcx            ; 4C 8B D1
    mov     eax, 26h            ; B8 26 00 00 00    //这里就是 SSN
    syscall                     ; 0F 05
    ret                         ; C3



关键点:
  • 偏移 0x00:mov r10, rcx(3 字节:4C 8B D1)
  • 偏移 0x03:mov eax, XXXXXXXX — 这个 XXXXXXXX 就是我们要提取的 SSN(1 字节 opcode + 4 字节立即数)
  • 偏移 0x04:SSN 的第一个字节

所以直接从函数地址 + 0x04 处读取 4 个字节(DWORD),就是当前系统版本对应的调用号。

file:///C:/Users/youxing/AppData/Roaming/marktext/images/2026-06-11-02-49-30-image.png?msec=1781153787677
思路

加载 ntdll.dll

GetProcAddress 获取目标函数地址

读取函数起始处 +0x04 偏移的 DWORD → 得到 SSN

将 SSN 填入我们的 "调用模板"

调用模板 → 直接 syscall 进入内核

为什么要从磁盘加载? 因为内存中已映射的 ntdll.dll 可能已经被安全软件 Hook,指令开头的 SSN 可能被修改。从磁盘重新加载可以拿到原始的、未被篡改的调用桩。

完整代码
模板
我们需要一段汇编代码来实现"传入 SSN 并执行 syscall"。x64 下的 syscall 指令会:
——通过 rcx 传递第一个参数给内核
——通过 eax 传递系统调用号


代码
[C] 纯文本查看 复制代码
#include <windows.h>
#include <winternl.h>
#include <stdio.h>

//通用 syscall 模板
typedef NTSTATUS(NTAPI* pSyscallFn)();

//生成一个可执行的 syscall 桩,调用号由 ssn 参数填入
pSyscallFn CreateSyscallStub(DWORD ssn)
{
    //syscall 桩:mov r10, rcx; mov eax, SSN; syscall; ret
    unsigned char stub_template[] =
    {
        0x4C, 0x8B, 0xD1,             // mov r10, rcx
        0xB8, 0x00, 0x00, 0x00, 0x00, // mov eax, SSN(占位)
        0x0F, 0x05,                   // syscall
        0xC3                          // ret
    };

    //把调用号填入偏移 4 的位置
    *(DWORD*)&stub_template[4] = ssn;

    //分配内存
    LPVOID execMem = VirtualAlloc(
        NULL,
        sizeof(stub_template),
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE
    );

    if (!execMem)
    {
        return NULL;
    }
    memcpy(execMem, stub_template, sizeof(stub_template));
    return (pSyscallFn)execMem;
}

//动态获取NtOpenProcess函数的 SSN
DWORD GetSyscallNumber(const char* funcName)
 {
    //从磁盘加载 ntdll.dll,避免被 Hook 影响
    HMODULE hNtdll = LoadLibraryExW(
        L"C:\\Windows\\System32\\ntdll.dll",//防止DLL劫持
        NULL,
        DONT_RESOLVE_DLL_REFERENCES
    );

    if (!hNtdll)
    {
        printf("LoadLibraryEx 失败!错误码: %lu\n", GetLastError());
        return 0;
    }

    // 获取函数地址
    FARPROC pFunc = GetProcAddress(hNtdll, funcName);
    if (!pFunc)
    {
        printf("找不到函数: %s\n", funcName);
        FreeLibrary(hNtdll);
        return 0;
    }

    //x64 下,ntdll 的 syscall 桩结构固定:
    //   +0x00: mov r10, rcx   (3 字节)
    //   +0x03: mov eax, SSN   (opcode 0xB8 + 4 字节立即数)
    //   +0x04: SSN 的起始字节
    DWORD ssn = *(DWORD*)((BYTE*)pFunc + 4);

    printf("\t%s -> SSN: 0x%04X (%u)\n", funcName, ssn, ssn);

    FreeLibrary(hNtdll);
    return ssn;
}

//使用示例
int main()
{
    //获取 NtOpenProcess 的调用号
    DWORD ssn = GetSyscallNumber("NtOpenProcess");
    if (!ssn)
    {
        printf("获取 SSN 失败\n");
        return 1;
    }

    //创建专用的 syscall 桩
    pSyscallFn NtOpenProcess = CreateSyscallStub(ssn);
    if (!NtOpenProcess)
    {
        printf("创建 syscall 桩失败!\n");
        return 1;
    }

    //使用直接系统调用打开进程
    HANDLE hProcess;
    OBJECT_ATTRIBUTES oa = { sizeof(oa) };
    CLIENT_ID cid = { (HANDLE)1234, NULL };  //目标进程 PID

    NTSTATUS status = NtOpenProcess(
        &hProcess,                   
        PROCESS_ALL_ACCESS,  
        &oa,
        &cid
    );

    printf("\tNtOpenProcess 返回: 0x%08X\n", status);
    printf("\t打开的句柄: 0x%p\n", hProcess);

    return 0;
}



我们直接看内存

file://C:%5CUsers%5Cyouxing%5CAppData%5CRoaming%5Cmarktext%5Cimages%5C2026-06-11-01-19-05-image.png?msec=1781153787677
批量获取调用表
如果你需要完整的调用表(比如写一个通用的 syscall 框架),可以遍历 ntdll 的所有导出函数并自动提取:
该代码通过手动解析 ntdll.dll的 PE Export Table,在不依赖 Windows API 的情况下,批量枚举所有 NtNative API,并直接从函数序言中提取系统调用号(SSN),是构建 Direct Syscall / Hell’s Gate / Halo’s Gate 的基础技术


[C] 纯文本查看 复制代码
//批量提取所有 Nt* 函数的 SSN
#include <windows.h>
#include <winternl.h>
#include <stdio.h>

void DumpSyscallTable()
 {
    HMODULE hNtdll = LoadLibraryExW(
        L"C:\\Windows\\System32\\ntdll.dll",
        NULL,
        DONT_RESOLVE_DLL_REFERENCES
    );

    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)hNtdll;
    PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((BYTE*)hNtdll + pDos->e_lfanew);
    IMAGE_DATA_DIRECTORY exportDir = 
        pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];

    PIMAGE_EXPORT_DIRECTORY pExport = 
        (PIMAGE_EXPORT_DIRECTORY)((BYTE*)hNtdll + exportDir.VirtualAddress);

    DWORD* names = (DWORD*)((BYTE*)hNtdll + pExport->AddressOfNames);
    WORD* ordinals = (WORD*)((BYTE*)hNtdll + pExport->AddressOfNameOrdinals);
    DWORD* funcs = (DWORD*)((BYTE*)hNtdll + pExport->AddressOfFunctions);

    printf("========== ntdll Syscall Table ==========\n");
    DWORD i = 0;
    for (i = 0; i < pExport->NumberOfNames; i++)
     {
        char* funcName = (char*)((BYTE*)hNtdll + names[i]);

        //只看 Nt* 且不是 Ntdll* 的函数,过滤内部函数
        if (strncmp(funcName, "Nt", 2) == 0)//只保留真正的 Native API
         {
            BYTE* funcAddr = (BYTE*)hNtdll + funcs[ordinals[i]];
            DWORD ssn = *(DWORD*)(funcAddr + 4);
            printf("  %-40s -> 0x%04X\n", funcName, ssn);
          }
      }

    FreeLibrary(hNtdll);
}

int main()
{
    DumpSyscallTable();
    return 0;
}


结果

file://C:%5CUsers%5Cyouxing%5CAppData%5CRoaming%5Cmarktext%5Cimages%5C2026-06-11-02-07-20-image.png?msec=1781153787678---
关键注意事项
为什么要 LoadLibraryEx 而不是 GetModuleHandle?
GetModuleHandle 返回的是当前进程中已加载的 ntdll。如果安全软件做了 IAT Hook 或 Inline Hook,你读到的可能已经是修改后的指令。
LoadLibraryEx + DONT_RESOLVE_DLL_REFERENCES 会从磁盘重新映射一个干净的副本,不执行 DllMain,不解析导入表,拿到的是原始字节

注意:微软没有以文档形式保证过 ntdll 内部实现永不改变,因此理论上未来系统更新后这个模式可能发生变化
调用约定
直接 syscall 时,你必须完全正确地设置参数。内核期望的参数顺序和数量与 ntdll 导出的函数签名一致。参数错误可能导致 STATUS_ACCESS_VIOLATION (0xC0000005) 甚至蓝屏。x86 的情况
x86 下的调用桩不同:

[C] 纯文本查看 复制代码
mov     eax, SSN         ; B8 XX XX XX XX
mov     edx, 0x7FFE0300  ; BA 00 03 FE 7F
call    dword ptr [edx]  ; FF 12
ret     n                 ; C2 XX XX

SSN 偏移也是 +0x01(在 mov eax 后紧跟着),但整体逻辑类似。不过现在 Windows 10+ 的 32 位系统越来越少,建议优先关注 x64

规避检测

一些 EDR 会通过 ETW(Event Tracing for Windows)或内核回调来检测 syscall 指令的来源地址。如果你的 syscall 来自非 ntdll 的内存区域(比如你 VirtualAlloc 出来的那块),就可能触发告警。更进阶的做法包括:
-->间接 syscall:只跳转到 ntdll 中的 syscall; ret 指令
-->硬件断点(HWBP)syscall:利用调试寄存器
-->天堂之门(Heaven's Gate):在 Wow64 环境下切换到 64 位模式
感兴趣可以自行搜索。


小结[td]
步骤操作
用 LoadLibraryEx(DONT_RESOLVE_DLL_REFERENCES) 从磁盘加载干净的 ntdll
GetProcAddress 找到目标函数
读取 函数地址 + 0x04 处的 4 字节 DWORD → 得到 SSN
将 SSN 填入 mov eax, XXXXXXXX; syscall; ret 模板
通过函数指针调用生成的桩,直达内核

这种方法让你的代码能自适应不同 Windows 版本,不需要为每个版本硬编码调用号,也绕过了用户态的 Inline Hook,是 Windows 系统编程中非常实用的技巧。
参考链接

-->https://j00ru.vexillium.org/syscalls/nt/64/
-->https://idiotc4t.com/defense-evasion/dynamic-get-syscallid
-->https://xz.aliyun.com/news/17898


希望这篇文章对你有所帮助,祝你在逆向对抗/安全研究的道路上越走越远!如果文章中有任何理解不当或技术错误的地方,欢迎大神们指出,我会虚心学习。

免责声明:本文仅供技术研究和学习交流,请勿用于任何非法用途。读者因使用本文代码而产生的任何后果由本人自行承担,技术无罪,人心有罪。


免费评分

参与人数 2吾爱币 +1 热心值 +2 收起 理由
AG6 + 1 我很赞同!
yp17792351859 + 1 + 1 用心讨论,共获提升!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
Panel 发表于 2026-6-11 19:17
写得很好,基础但是又有用
沙发
dork 发表于 2026-6-11 17:55
感觉 GetProcAddress(hNtdll, funcName);这一步或者最多到pSyscallFn NtOpenProcess = CreateSyscallStub(ssn);就会触发主动防御了。
4#
 楼主| ClumsyBear 发表于 2026-6-11 20:05 |楼主
dork 发表于 2026-6-11 17:55
感觉 GetProcAddress(hNtdll, funcName);这一步或者最多到pSyscallFn NtOpenProcess = CreateSyscallStub(s ...

按道理GetProcAddress 不会触发主动防御,如果按顺序调用LoadLibraryEx, GetProcAddress, VirtualAlloc也确实可能会触发报警,行为较为明显,加载纯净ntdll ——拿调用号——复制。那确实可能报警
5#
周易 发表于 2026-6-12 21:17

在高版本Windows中,对于ntdll.dll的系统调用号,不需要GetProcAddress或读取目标函数。这是由于Zw函数导出顺序是经过排序的。

std::map<std::string, int> ntdll_name2num;
std::map<int, std::string> ntdll_num2name;

std::vector<std::pair<DWORD, std::string>> v;

auto dllBase = (PBYTE)hMod_ntdll;
auto imageDosHeader = (PIMAGE_DOS_HEADER)dllBase;
auto imageNtHeaders = (PIMAGE_NT_HEADERS)&dllBase[imageDosHeader->e_lfanew];
auto optionalHeader = &imageNtHeaders->OptionalHeader;
auto imageDirectoryEntryExport = &optionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
auto imageExportDirectory = (PIMAGE_EXPORT_DIRECTORY)&dllBase[imageDirectoryEntryExport->VirtualAddress];
auto functions = (PDWORD)&dllBase[imageExportDirectory->AddressOfFunctions];
auto names = (PDWORD)&dllBase[imageExportDirectory->AddressOfNames];
auto nameOrdinals = (PWORD)&dllBase[imageExportDirectory->AddressOfNameOrdinals];
for (DWORD i = 0; i != imageExportDirectory->NumberOfNames; i++)
{
    if (!memcmp(&dllBase[names[i]], "Zw", 2))
    {
        auto ordinal = nameOrdinals[i];
        v.push_back(std::make_pair(functions[ordinal], (PCHAR)&dllBase[names[i]]));
    }
}

std::sort(v.begin(), v.end());

for (int i = 0; i != v.size(); i++)
{
    ntdll_name2num[v[i].second] = i;
    ntdll_num2name[i] = v[i].second;
}
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-6-13 01:12

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表