环境说明
-
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