重写 OpenProcess:绕过 Win32 API,直接进入 NT 内核(Win7 x86)

环境说明

  • Windows 7 x86

  • 不使用 OpenProcess / NtOpenProcess 等任何 3 环 API

  • 直接通过 sysenter 进入内核

一、背景:OpenProcess 的真实调用链

之前的文章说过,在 Win7 x86 上,一个普通的 OpenProcess 实际会经历如下路径:

cpp 复制代码
OpenProcess (Win32)
→ kernel32.dll
→ API-MS 转发层
→ kernelbase.dll
→ ntdll!NtOpenProcess
→ call [KUSER_SHARED_DATA.SystemCall]
→ ntdll!KiFastSystemCall
→ sysenter   ← Ring3 → Ring0

可以看到:

  • Win32 API 只是参数整理层

  • 真正进入内核的是 sysenter

内核只关心:

  • syscall 号(SSDT index)

  • 用户态栈布局

  • 参数顺序

本文的目标是:

完全绕过 Win32 / ntdll,在用户态手动构造 syscall 所需的一切条件,直接进入内核调用 NtOpenProcess。

二、仿写kernelbase.dll中的OpenProcess函数

kernelbase.dll 是 Win32 API 的主要实现层之一,它负责完成用户态参数整理,并把请求转换成 NT 层可接受的调用形式。

下面是kernelbase.dll中的OpenProcess函数的反汇编:

cpp 复制代码
.text:0DCEB359 ; Exported entry 466. OpenProcess
.text:0DCEB359
.text:0DCEB359 ; =============== S U B R O U T I N E =======================================
.text:0DCEB359
.text:0DCEB359 ; Attributes: bp-based frame
.text:0DCEB359
.text:0DCEB359 ; HANDLE __stdcall OpenProcess(DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwProcessId)
.text:0DCEB359                 public _OpenProcess@12
.text:0DCEB359 _OpenProcess@12 proc near               ; CODE XREF: GetProcessVersion(x)+20F12↓p
.text:0DCEB359                                         ; GetProcessVersion(x)+20F28↓p
.text:0DCEB359                                         ; DATA XREF: ...
.text:0DCEB359
.text:0DCEB359 ObjectAttributes= _OBJECT_ATTRIBUTES ptr -20h
.text:0DCEB359 ClientId        = _CLIENT_ID ptr -8
.text:0DCEB359 dwDesiredAccess = dword ptr  8
.text:0DCEB359 bInheritHandle  = dword ptr  0Ch
.text:0DCEB359 dwProcessId     = dword ptr  10h
.text:0DCEB359
.text:0DCEB359 ; FUNCTION CHUNK AT .text:0DCE74A4 SIZE 0000000D BYTES
.text:0DCEB359
.text:0DCEB359                 mov     edi, edi
.text:0DCEB35B                 push    ebp
.text:0DCEB35C                 mov     ebp, esp
.text:0DCEB35E                 sub     esp, 20h
.text:0DCEB361                 mov     eax, [ebp+dwProcessId]
.text:0DCEB364                 mov     [ebp+ClientId.UniqueProcess], eax
.text:0DCEB367                 mov     eax, [ebp+bInheritHandle]
.text:0DCEB36A                 push    esi
.text:0DCEB36B                 xor     esi, esi
.text:0DCEB36D                 neg     eax
.text:0DCEB36F                 sbb     eax, eax
.text:0DCEB371                 and     eax, 2
.text:0DCEB374                 mov     [ebp+ObjectAttributes.Attributes], eax
.text:0DCEB377                 lea     eax, [ebp+ClientId]
.text:0DCEB37A                 push    eax             ; ClientId
.text:0DCEB37B                 lea     eax, [ebp+ObjectAttributes]
.text:0DCEB37E                 push    eax             ; ObjectAttributes
.text:0DCEB37F                 push    [ebp+dwDesiredAccess] ; DesiredAccess
.text:0DCEB382                 lea     eax, [ebp+dwProcessId]
.text:0DCEB385                 push    eax             ; ProcessHandle
.text:0DCEB386                 mov     [ebp+ClientId.UniqueThread], esi
.text:0DCEB389                 mov     [ebp+ObjectAttributes.Length], 18h
.text:0DCEB390                 mov     [ebp+ObjectAttributes.RootDirectory], esi
.text:0DCEB393                 mov     [ebp+ObjectAttributes.ObjectName], esi
.text:0DCEB396                 mov     [ebp+ObjectAttributes.SecurityDescriptor], esi
.text:0DCEB399                 mov     [ebp+ObjectAttributes.SecurityQualityOfService], esi
.text:0DCEB39C                 call    ds:__imp__NtOpenProcess@16 ; NtOpenProcess(x,x,x,x)
.text:0DCEB3A2                 cmp     eax, esi
.text:0DCEB3A4                 pop     esi
.text:0DCEB3A5                 jl      loc_DCE74A4
.text:0DCEB3AB                 mov     eax, [ebp+dwProcessId]
.text:0DCEB3AE
.text:0DCEB3AE locret_DCEB3AE:                         ; CODE XREF: OpenProcess(x,x,x)-3EAD↑j
.text:0DCEB3AE                 leave
.text:0DCEB3AF                 retn    0Ch
.text:0DCEB3AF _OpenProcess@12 endp
.text:0DCEB3AF
.text:0DCEB3AF ; ---------------------------------------------------------------------------

它的作用可以一句话概括:

把 Win32 参数转换成 OBJECT_ATTRIBUTES + CLIENT_ID,然后调用 NtOpenProcess,成功就返回进程句柄,失败走异常路径。

三、重写代码

cpp 复制代码
#include <iostream>
#include <windows.h>
#include <winternl.h>
using namespace std;

#define OBJ_INHERIT 0x00000002L

typedef struct _CLIENT_ID
{
    HANDLE UniqueProcess;
    HANDLE UniqueThread;
} CLIENT_ID, * PCLIENT_ID;

HANDLE MyOpenProcess(
    DWORD dwDesiredAccess,
    BOOL  bInheritHandle,
    DWORD dwProcessId
)
{
    NTSTATUS status;
    HANDLE   hProcess = NULL;

    CLIENT_ID cid = { 0 };
    OBJECT_ATTRIBUTES oa;

    // ---------- 构造 CLIENT_ID ----------
    cid.UniqueProcess = (HANDLE)dwProcessId;
    cid.UniqueThread = NULL;

    // ---------- 构造 OBJECT_ATTRIBUTES ----------
    oa.Length = sizeof(OBJECT_ATTRIBUTES);
    oa.RootDirectory = NULL;
    oa.ObjectName = NULL;
    oa.SecurityDescriptor = NULL;
    oa.SecurityQualityOfService = NULL;
    oa.Attributes = bInheritHandle ? OBJ_INHERIT : 0;

    __asm
    {
        // NtOpenProcess 参数(右到左压栈)
        // NtOpenProcess(&hProcess, dwDesiredAccess, &oa, &cid)

        lea eax, dword ptr ds : [cid] ;
        push eax;// 参数4:PCLIENT_ID

        lea eax, dword ptr ds : [oa] ;
        push eax;// 参数3:POBJECT_ATTRIBUTES

        mov eax, dword ptr ds : [dwDesiredAccess] ;
        push eax;// 参数2:ACCESS_MASK

        lea eax, dword ptr ds : [hProcess] ;
        push eax; // 参数1:PHANDLE

        // ---------- 关键补齐 ----------
        // 用于模拟缺失的"第二层返回地址"
        // 以匹配 KiFastCallEntry 中的 add edx, 8 行为
        push 0;

        // ---------- 构造 sysenter 返回地址 ----------
        push label;

        // ---------- 设置 syscall 号 ----------
        // 0x0BE 为当前 Win7 x86 环境下 NtOpenProcess 的 SSDT index
        mov eax, 0x0BE;

        // ---------- 进入内核 ----------
        // sysenter 要求:
        // EAX = syscall number
        // EDX = 用户态 ESP
        mov edx, esp;
        _emit 0x0F;        // 由于vs不支持sysenter命令
        _emit 0x34;        //0F34是sysenter的硬编码

            label:
        // ---------- 恢复栈 ----------
        // 共 push 了 6 次(24 字节):
        // 4 个参数 + 0 占位 + label
        add esp, 20       // 参数 + 占位(返回地址由 sysexit 处理)

        // ---------- 保存返回状态 ----------
            mov status, eax
    }

    if (status < 0)
        return NULL;

    return hProcess;
}

int main()
{
    HANDLE h = MyOpenProcess(PROCESS_ALL_ACCESS, FALSE, 3184);
    if (h)
    {
        cout << h << endl;
    }

    system("pause");
    return 0;
}

标准情况下,在执行 sysenter 的那一刻,用户栈长这样(从低地址→高地址):

cpp 复制代码
ESP ->  ; ntdll stub 里 call [KUSER] 压入
      NtOpenProcess      ; 调用 NtOpenProcess 的返回地址
      arg1 = &hProcess
      arg2 = DesiredAccess
      arg3 = &oa
      arg4 = &cid

所以我们自己的代码堆栈也要按照标准的方式补齐,否则0环得到的参数将错位:

cpp 复制代码
ESP ->  MyOpenProcess    ; call [KUSER] 压入
      0                  ;  占位
      arg1 = &hProcess
      arg2 = DesiredAccess
      arg3 = &oa
      arg4 = &cid
相关推荐
MyBFuture3 小时前
C# 哈希表与堆栈队列实战指南
开发语言·windows·c#·visual studio
管理大亨3 小时前
Linux vs Windows:五大维度深度对决
linux·arm开发·windows
云技纵横5 小时前
Stream API 从入门到实践:常用操作、易错点与性能建议
开发语言·windows·python
独自归家的兔6 小时前
开发核心问题深度解析:通配符接口 + 第三方数据同步 + 九宫格算法
服务器·windows·microsoft
秋月的私语6 小时前
Windows Forms 多语言开发指南
windows·c#·form
YJlio6 小时前
Windows Sysinternals 文件工具学习笔记(12.10):PendMoves + MoveFile 实战——重启后文件替换的安全姿势
windows·笔记·学习
Jeremy_121387 小时前
如何让Win10/11 永久暂定更新
windows
小小的木头人7 小时前
Windows Server 2019 离线安装 OpenSSH 并仅启用 SFTP
windows·ssh
悟能不能悟7 小时前
java list.addAll介绍
java·windows·list