Windows内核安全

13、系统调用

1、课程目标

  1. 理解Windows系统调用机制
  2. 掌握SSDT和SSDT Shadow的结构
  3. 学会分析系统调用流程
  4. 理解系统调用在安全中的应用

2、名词解释

术语 解释
SSDT System Service Descriptor Table系统服务描述符表
SSDT Shadow GUI线程的系统服务表
syscall x64系统调用指令
MSR Model Specific Register特殊寄存器
LSTAR 系统调用入口点MSR寄存器

3、使用工具

工具 用途
WinDbg 分析SSDT
IDA Pro 逆向分析
PCHunter 查看SSDT

4、技术原理

4.1、系统调用流程

┌─────────────────────────────────────────────────────────────┐
│                    系统调用流程 (x64)                        │
│                                                             │
│  用户模式 (Ring3)                                          │
│  ┌────────────────────────────────────────────────────┐    │
│  │  NtCreateFile()                                    │    │
│  │      ↓                                             │    │
│  │  mov r10, rcx                                      │    │
│  │  mov eax, <syscall_number>  ; 例如 0x55           │    │
│  │  syscall                    ; 进入内核             │    │
│  └────────────────────────────────────────────────────┘    │
│                         │                                   │
│                         ↓                                   │
│  ───────────── Ring0/Ring3 边界 ────────────────            │
│                         │                                   │
│                         ↓                                   │
│  内核模式 (Ring0)                                          │
│  ┌────────────────────────────────────────────────────┐    │
│  │  KiSystemCall64 (MSR LSTAR)                        │    │
│  │      ↓                                             │    │
│  │  KiSystemServiceRepeat                             │    │
│  │      ↓                                             │    │
│  │  查找 SSDT[syscall_number]                         │    │
│  │      ↓                                             │    │
│  │  调用 NtCreateFile (内核实现)                       │    │
│  └────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘

4.2、SSDT结构

// SSDT结构定义
typedef struct _KSERVICE_TABLE_DESCRIPTOR {
    PULONG_PTR Base;          // 服务函数表基址
    PULONG     Count;         // 计数(未使用)
    ULONG      Limit;         // 服务数量
    PUCHAR     Number;        // 参数字节数表
} KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;

// x64 SSDT表项编码
// 低4位:参数栈空间/4
// 高28位:相对偏移
// 实际地址 = Base + (TableEntry >> 4)

5、代码实现

5.1、示例1:获取SSDT基址

// SSDTUtils.c - SSDT工具函数
#include <ntddk.h>

typedef struct _KSERVICE_TABLE_DESCRIPTOR {
    PULONG_PTR  Base;
    PULONG      Count;
    ULONG       Limit;
    PUCHAR      Number;
} KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;

// 全局变量
PKSERVICE_TABLE_DESCRIPTOR g_KeServiceDescriptorTable = NULL;

// 通过特征码搜索SSDT
PVOID FindSSDT() {
    // 方法1:通过导出符号(只有32位有效)
    // extern PKSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable;
    
    // 方法2:通过KiSystemServiceRepeat特征码搜索
    UNICODE_STRING funcName;
    RtlInitUnicodeString(&funcName, L"KeAddSystemServiceTable");
    
    PUCHAR funcAddr = (PUCHAR)MmGetSystemRoutineAddress(&funcName);
    if (!funcAddr) return NULL;
    
    // 在函数附近搜索SSDT引用
    // 这是一个简化的示例,实际需要更复杂的特征匹配
    for (ULONG i = 0; i < 0x100; i++) {
        // 搜索lea指令模式
        if (funcAddr[i] == 0x4C && funcAddr[i+1] == 0x8D) {
            // 找到可能的SSDT引用
            LONG offset = *(PLONG)(funcAddr + i + 3);
            PVOID ssdt = funcAddr + i + 7 + offset;
            return ssdt;
        }
    }
    
    return NULL;
}

// 获取系统调用地址
PVOID GetSyscallAddress(ULONG SyscallNumber) {
    if (!g_KeServiceDescriptorTable) {
        g_KeServiceDescriptorTable = (PKSERVICE_TABLE_DESCRIPTOR)FindSSDT();
        if (!g_KeServiceDescriptorTable) {
            return NULL;
        }
    }
    
    if (SyscallNumber >= g_KeServiceDescriptorTable->Limit) {
        return NULL;
    }
    
#ifdef _WIN64
    // x64: 表项是相对偏移
    LONG offset = g_KeServiceDescriptorTable->Base[SyscallNumber];
    offset = offset >> 4;  // 去掉参数信息
    return (PVOID)((PUCHAR)g_KeServiceDescriptorTable->Base + offset);
#else
    // x86: 表项是绝对地址
    return (PVOID)g_KeServiceDescriptorTable->Base[SyscallNumber];
#endif
}

// 获取系统调用号
ULONG GetSyscallNumber(PVOID FunctionAddress) {
    // 从ntdll中的存根函数获取系统调用号
    // mov eax, <syscall_number>
    // syscall / sysenter / int 2e
    
    PUCHAR code = (PUCHAR)FunctionAddress;
    
    // 检查是否是mov eax, imm32指令
    if (code[0] == 0xB8) {
        return *(PULONG)(code + 1);
    }
    
    // x64 ntdll pattern: mov r10, rcx; mov eax, imm32
    if (code[0] == 0x4C && code[1] == 0x8B && code[2] == 0xD1 &&
        code[3] == 0xB8) {
        return *(PULONG)(code + 4);
    }
    
    return (ULONG)-1;
}

5.2、示例2:枚举SSDT

// SSDTEnum.c - 枚举SSDT
#include <ntddk.h>

typedef struct _SSDT_ENTRY {
    ULONG   Index;
    PVOID   Address;
    CHAR    Name[64];
} SSDT_ENTRY, *PSSDT_ENTRY;

// 已知的系统调用名称(部分)
const char* g_SyscallNames[] = {
    "NtAccessCheck",
    "NtWorkerFactoryWorkerReady",
    "NtAcceptConnectPort",
    "NtMapUserPhysicalPagesScatter",
    "NtWaitForSingleObject",
    "NtCallbackReturn",
    "NtReadFile",
    "NtDeviceIoControlFile",
    "NtWriteFile",
    "NtRemoveIoCompletion",
    "NtReleaseSemaphore",
    "NtReplyWaitReceivePort",
    "NtReplyPort",
    "NtSetInformationThread",
    "NtSetEvent",
    "NtClose",
    // ... 更多
};

// 枚举SSDT
VOID EnumerateSSDT() {
    PKSERVICE_TABLE_DESCRIPTOR ssdt = (PKSERVICE_TABLE_DESCRIPTOR)FindSSDT();
    
    if (!ssdt) {
        DbgPrint("[SSDT] Failed to find SSDT\n");
        return;
    }
    
    DbgPrint("[SSDT] Base: 0x%p\n", ssdt->Base);
    DbgPrint("[SSDT] Limit: %d\n", ssdt->Limit);
    
    for (ULONG i = 0; i < min(ssdt->Limit, 50); i++) {
#ifdef _WIN64
        LONG offset = ((PLONG)ssdt->Base)[i] >> 4;
        PVOID funcAddr = (PVOID)((PUCHAR)ssdt->Base + offset);
#else
        PVOID funcAddr = (PVOID)ssdt->Base[i];
#endif
        
        DbgPrint("[SSDT] [%3d] 0x%p\n", i, funcAddr);
    }
}

// 检查SSDT是否被HOOK
BOOLEAN CheckSSDTHook(ULONG SyscallNumber, PVOID ExpectedAddress) {
    PVOID actualAddress = GetSyscallAddress(SyscallNumber);
    
    if (actualAddress != ExpectedAddress) {
        DbgPrint("[SSDT] HOOK detected! Syscall %d\n", SyscallNumber);
        DbgPrint("[SSDT]   Expected: 0x%p\n", ExpectedAddress);
        DbgPrint("[SSDT]   Actual:   0x%p\n", actualAddress);
        return TRUE;
    }
    
    return FALSE;
}

// 验证SSDT地址是否在ntoskrnl范围内
BOOLEAN ValidateSSDTEntry(PVOID Address) {
    // 获取ntoskrnl的地址范围
    PVOID ntosBase = NULL;
    ULONG ntosSize = 0;
    
    // 通过ZwQuerySystemInformation获取模块列表
    // 检查地址是否在ntoskrnl范围内
    
    UNICODE_STRING funcName;
    RtlInitUnicodeString(&funcName, L"NtClose");
    PVOID ntosAddr = MmGetSystemRoutineAddress(&funcName);
    
    if (!ntosAddr) return FALSE;
    
    // 简化检查:地址应该在内核空间
    if ((ULONG_PTR)Address < (ULONG_PTR)0xFFFF800000000000ULL) {
        return FALSE;
    }
    
    return TRUE;
}

5.3、示例3:系统调用监控

// SyscallMonitor.c - 系统调用监控
#include <ntddk.h>

// 使用Infinity Hook或类似技术监控系统调用
// 这里展示基本原理

typedef NTSTATUS (*PNTOPENPROCESS)(
    PHANDLE ProcessHandle,
    ACCESS_MASK DesiredAccess,
    POBJECT_ATTRIBUTES ObjectAttributes,
    PCLIENT_ID ClientId
);

PNTOPENPROCESS g_OriginalNtOpenProcess = NULL;

// Hook函数
NTSTATUS HookedNtOpenProcess(
    PHANDLE ProcessHandle,
    ACCESS_MASK DesiredAccess,
    POBJECT_ATTRIBUTES ObjectAttributes,
    PCLIENT_ID ClientId
) {
    // 记录调用信息
    if (ClientId && ClientId->UniqueProcess) {
        HANDLE targetPid = ClientId->UniqueProcess;
        HANDLE currentPid = PsGetCurrentProcessId();
        
        DbgPrint("[Syscall] NtOpenProcess: PID %d -> %d, Access: 0x%X\n",
                 (ULONG)(ULONG_PTR)currentPid,
                 (ULONG)(ULONG_PTR)targetPid,
                 DesiredAccess);
        
        // 可以在这里添加拦截逻辑
        // if (IsProtectedProcess(targetPid)) {
        //     return STATUS_ACCESS_DENIED;
        // }
    }
    
    // 调用原始函数
    return g_OriginalNtOpenProcess(ProcessHandle, DesiredAccess, 
                                    ObjectAttributes, ClientId);
}

// 注意:直接修改SSDT在x64上会触发PatchGuard
// 需要使用其他技术如Infinity Hook

5.4、示例4:通过MSR获取系统调用入口

// MSRSyscall.c - MSR系统调用入口
#include <ntddk.h>

#define MSR_LSTAR 0xC0000082  // x64 syscall入口
#define MSR_CSTAR 0xC0000083  // x64 compat syscall入口
#define MSR_STAR  0xC0000081  // syscall segments

// 读取MSR
ULONG64 ReadMSR(ULONG msr) {
    return __readmsr(msr);
}

// 获取系统调用入口点
PVOID GetSyscallEntryPoint() {
    return (PVOID)ReadMSR(MSR_LSTAR);
}

// 分析系统调用入口
VOID AnalyzeSyscallEntry() {
    PVOID entryPoint = GetSyscallEntryPoint();
    
    DbgPrint("[MSR] LSTAR: 0x%p (KiSystemCall64)\n", entryPoint);
    DbgPrint("[MSR] STAR:  0x%llX\n", ReadMSR(MSR_STAR));
    
    // 检查入口点是否被修改
    // 正常应该指向KiSystemCall64
    UNICODE_STRING funcName;
    RtlInitUnicodeString(&funcName, L"KeQueryPerformanceCounter");
    PVOID ntosAddr = MmGetSystemRoutineAddress(&funcName);
    
    // 简单验证:检查是否在ntoskrnl范围
    // 完整验证需要检查模块边界
}

// 检测MSR Hook
BOOLEAN DetectMSRHook() {
    PVOID currentEntry = GetSyscallEntryPoint();
    
    // 可以保存原始值在驱动加载时
    // 然后定期检查是否被修改
    
    // 简化检查
    PUCHAR code = (PUCHAR)currentEntry;
    
    // KiSystemCall64正常开始:swapgs
    if (code[0] != 0x0F || code[1] != 0x01 || code[2] != 0xF8) {
        DbgPrint("[MSR] Syscall entry may be hooked!\n");
        return TRUE;
    }
    
    return FALSE;
}

5.5、示例5:用户模式系统调用号提取

// SyscallExtract.c - 从ntdll提取系统调用号
#include <windows.h>
#include <stdio.h>

typedef struct _SYSCALL_INFO {
    char    Name[64];
    DWORD   Number;
    PVOID   Address;
} SYSCALL_INFO;

// 从ntdll提取系统调用号
DWORD GetSyscallNumber(PVOID FuncAddress) {
    PBYTE code = (PBYTE)FuncAddress;
    
    // x64 ntdll stub pattern:
    // 4C 8B D1          mov r10, rcx
    // B8 XX XX XX XX    mov eax, syscall_number
    // 0F 05             syscall
    // C3                ret
    
    if (code[0] == 0x4C && code[1] == 0x8B && code[2] == 0xD1 &&
        code[3] == 0xB8) {
        return *(DWORD*)(code + 4);
    }
    
    // Win7 x64 pattern (different)
    if (code[0] == 0xB8) {
        return *(DWORD*)(code + 1);
    }
    
    return -1;
}

void ExtractNtdllSyscalls() {
    HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
    if (!hNtdll) return;
    
    // 常见系统调用列表
    const char* syscalls[] = {
        "NtOpenProcess",
        "NtReadVirtualMemory",
        "NtWriteVirtualMemory",
        "NtAllocateVirtualMemory",
        "NtProtectVirtualMemory",
        "NtCreateThreadEx",
        "NtQuerySystemInformation",
        "NtQueryInformationProcess",
        "NtSetInformationProcess",
        "NtCreateFile",
        "NtClose"
    };
    
    printf("Syscall Numbers:\n");
    printf("%-30s %-10s %s\n", "Name", "Number", "Address");
    printf("----------------------------------------------\n");
    
    for (int i = 0; i < sizeof(syscalls)/sizeof(syscalls[0]); i++) {
        PVOID addr = GetProcAddress(hNtdll, syscalls[i]);
        if (addr) {
            DWORD num = GetSyscallNumber(addr);
            printf("%-30s 0x%-8X 0x%p\n", syscalls[i], num, addr);
        }
    }
}

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

6、课后作业

  1. 编写程序枚举完整的SSDT
  2. 实现SSDT Hook检测功能
  3. 分析不同Windows版本的系统调用号变化
  4. 研究Infinity Hook原理

7、扩展阅读

  • Windows系统调用详解
  • SSDT Hook防护与检测
  • PatchGuard机制分析