绕过句柄权限过滤的内存保护

句柄权限过滤

在之前学的常规进程保护种,在驱动层是通过注册打开进程句柄的回调,通过过滤一些权限的方式防止其它进程对指定进程进行跨进程的一些操作,包括进程读写等等。

绕过思路

它可以相当于是对用户层的 OpenProcess 这个 API 作了拦截,那么我们绕过的思路自然也很清楚,那肯定不能通过这个 API 再去获取句柄了,而是可以直接使用驱动附加进程,然后通过 R0 和 R3 通信的方式去读取内存,这里我们做一个简单的小测试,目前代码已经上传至 Github,供大家学习使用。这个里一共包含四个项目,分别对应了游戏程序,外挂程序,游戏驱动和外挂驱动。

这里有一个测试的小技巧,如果觉得每次测试驱动都要安装卸载麻烦,可以把加载驱动写在窗体初始化的时候,把卸载卸载窗体对象析构的时候。

在保护指定进程的时候,通过注册回调的方式过滤句柄权限,用的还是之前的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
OB_PREOP_CALLBACK_STATUS MyProtect(
PVOID RegistrationContext,
POB_PRE_OPERATION_INFORMATION OperationInformation) {
if (OperationInformation->KernelHandle) {

}
else {
ACCESS_MASK AccessBitsToClear = PROCESS_TERMINATE | PROCESS_VM_READ | PROCESS_VM_WRITE;
PEPROCESS process = (PEPROCESS)OperationInformation->Object;
PUCHAR processName = PsGetProcessImageFileName(process);
if (_stricmp((char *)processName, "XGame.exe") != 0) {
return OB_PREOP_SUCCESS;
}
ACCESS_MASK Origin = OperationInformation->Parameters->CreateHandleInformation.OriginalDesiredAccess;
if (Origin&AccessBitsToClear) {
if (OperationInformation->Operation == OB_OPERATION_HANDLE_CREATE) {
OperationInformation->Parameters->CreateHandleInformation.DesiredAccess &= ~AccessBitsToClear;
}
UINT64 NEW = (ACCESS_MASK)(OperationInformation->Parameters->CreateHandleInformation.DesiredAccess);
kprintf(("xia0ji233: old=%p new=%p\n"), Origin, NEW);
}
}
return OB_PREOP_SUCCESS;
}

这个保护方式我们可以使用另一个驱动调用 KeStackAttach 附加进程,直接读取内存的方式实现。

具体操作步骤

这里给出一个内核层读取进程内存的函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
NTSTATUS ReadProcessMemory(UINT32 PID,PVOID Addr,UINT32 len,PVOID Buffer) {
KAPC_STATE apc_state;
PEPROCESS Process;
RtlZeroMemory(&apc_state, sizeof(KAPC_STATE));
PVOID tmpBuf_Kernel = ExAllocatePool(NonPagedPool, len);
PsLookupProcessByProcessId(PID, &Process);
KeStackAttachProcess((PVOID)Process, &apc_state);
int dwRet = MmIsAddressValid(Addr);
if (dwRet) {
kprintf(("xia0ji233:Attach Success\n"));
RtlCopyMemory(tmpBuf_Kernel, Addr, len);
kprintf(("xia0ji233:readmemory:%p\n"), *(UINT64*)Addr);
}
KeUnstackDetachProcess(&apc_state);
RtlCopyMemory(Buffer, tmpBuf_Kernel, len);

ExFreePool(tmpBuf_Kernel);
return dwRet;
}

参数分别是进程PID进程虚拟地址读取内存的长度接收缓冲区

这里我们需要分配一个内核空间作为中转,使用 KeStackAttachPorcess 函数去附加进程,这样我们的驱动就可以直接访问内核的虚拟地址了。

R3 层我们对进程操作是使用句柄,R0 层有专门的进程结构,也就是这个 EPProcess。这个使用 PsLookupProcessByProcessId 这个 API 去获取进程结构体,这里的第一个参数虽然叫 HANDLE 但是其实就是 PID 了。

之后我们把内存拷贝到内核空间,然后通过设备返回,就可以达到绕过保护去读取被保护进程的游戏的目的了。

实验情况

代码上传到 Github 了,所用的就是这些代码编译的程序和驱动。

游戏设计

游戏程序是随机生成一个 short 数字并把内存地址打印到其中一个文本框。通过使用第二个文本框输入的数值校验是否正确。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int *ptr=NULL;
void CXGameDlg::OnBnClickedButton1()
{
// TODO: 在此添加控件通知处理程序代码
if (ptr) {
free(ptr);
}
ptr = (int *)malloc(sizeof(int));
*ptr = rand();
WCHAR s[32];
wsprintf(s, L"%p", ptr);
SetDlgItemText(IDC_EDIT1, s);
}


void CXGameDlg::OnBnClickedButton2()
{
// TODO: 在此添加控件通知处理程序代码
CString s;
GetDlgItemText(IDC_EDIT2, s);
int ans = _ttoi(s);
if (ans == *ptr) {
MessageBox(L"你的猜测正确", L"Warning");
}
else {
MessageBox(L"你的猜测错误", L"Warning");
}
}

这个逻辑实现也不难,主要还有窗体加载的时候加载驱动,析构的时候卸载驱动,这里也给一下我所用的卸载驱动和加载驱动的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
void DebugPrint(const char* format, ...)
{
char buffer[256];
va_list args;
va_start(args, format);
vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);

OutputDebugStringA(buffer);
}
void CXGameDlg::UnloadDriver(const char *ServeName)
{
SC_HANDLE hServiceMgr = NULL;//SCM管理器的句柄
SC_HANDLE hServiceDDK = NULL;//NT驱动程序的服务句柄
hServiceMgr = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
DebugPrint("Open SCM handle=%p,GetLastError=%p\n", hServiceMgr, GetLastError());
hServiceDDK = OpenServiceA(hServiceMgr, ServeName, SERVICE_ALL_ACCESS);
if (hServiceDDK) {
int bRet = 0;
SERVICE_STATUS status;
bRet = ControlService(hServiceDDK, SERVICE_CONTROL_STOP, &status);
if (bRet) {
DebugPrint("Stop Service Success\n");
}
else {
DebugPrint("Can't Stop Service\n");
goto GETLASTERROR;
}
bRet = DeleteService(hServiceDDK);
if (bRet) {
DebugPrint("Unload Success\n");
}
else {
DebugPrint("Unload Fail\n");
}
GETLASTERROR:
DebugPrint("GetLastError=%p\n", GetLastError());
}
else {
DebugPrint("OpenServe Failed\n");

}

}

void CXGameDlg::LoadDriver(const char * ServeName, const char * DriverPath) {
char FullPath[256] = { 0 };
GetFullPathNameA(DriverPath, 256, FullPath, NULL);
SC_HANDLE hServiceMgr = NULL;//SCM管理器的句柄
SC_HANDLE hServiceDDK = NULL;//NT驱动程序的服务句柄
hServiceMgr = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
printf("Open SCM handle=%p,GetLastError=%p\n", hServiceMgr, GetLastError());
hServiceDDK = CreateServiceA(
hServiceMgr,
ServeName,
ServeName,
SERVICE_START,
SERVICE_KERNEL_DRIVER,
SERVICE_DEMAND_START,
SERVICE_ERROR_NORMAL,
FullPath,
NULL,
NULL,
NULL,
NULL,
NULL
);

if (GetLastError() == ERROR_SERVICE_EXISTS) {
DebugPrint("Service Already Exists\n");
hServiceDDK = OpenServiceA(hServiceMgr, ServeName, SERVICE_START);
}
else if (GetLastError() != 0) {
DebugPrint("GetLastError=%p\n", GetLastError());

return;
}
DebugPrint("hServiceDDK=%p\n", hServiceDDK);

int bRet = StartService(hServiceDDK, NULL, NULL);
if (GetLastError() == ERROR_SERVICE_ALREADY_RUNNING) {
DebugPrint("Service Already Running\n");
}
else {
if (bRet == 0) {
DebugPrint("Service Running Failed\n");
DebugPrint("GetLastError=%p\n", GetLastError());
}
else {
DebugPrint("Service Start Success\n");
}
}
}

这里对应加载的驱动是 Xprotect.sys,直接根据进程名保护就可以了(驱动完整代码在最后贴出)。

可以看到,一般的 CE 是无法直接读取到内存的。

外挂设计

之后我们编写外挂程序,这样子设计:

对应的控件事件也不难:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#define SYMBOL L"\\??\\xia0ji2333"
#define ReadWriteCode CTL_CODE (FILE_DEVICE_UNKNOWN,0x805,METHOD_BUFFERED, FILE_ANY_ACCESS)

HANDLE hFile=NULL;

void CXGuaDlg::OnBnClickedButton1()
{

// TODO: 在此添加控件通知处理程序代码
if (!hFile) {
hFile = CreateFile(SYMBOL, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
SetDlgItemText(IDC_EDIT4, L"打开设备成功");
}
}




void CXGuaDlg::OnBnClickedButton2()
{
// TODO: 在此添加控件通知处理程序代码
if (hFile) {
CloseHandle(hFile);
hFile = NULL;
SetDlgItemText(IDC_EDIT4, L"关闭设备成功");
}
}


void CXGuaDlg::OnBnClickedButton3()
{
// TODO: 在此添加控件通知处理程序代码
typedef struct Mm {
UINT32 PID;
UINT32 len;
PVOID Addr;
}Mm;
CString s;
char buffer[32];
WCHAR LOG[512],Mem[256];
DWORD dwRetSize;
UINT32 PID,len;
PVOID Addr;
Mm data;

GetDlgItemText(IDC_EDIT1, s);
PID = _ttoi(s);
GetDlgItemText(IDC_EDIT2, s);
Addr = (PVOID)_tcstoui64(s,nullptr,16);
GetDlgItemText(IDC_EDIT3, s);
len = _ttoi(s);

data.PID = PID;
data.len = len;
data.Addr = Addr;
memset(buffer, 0, sizeof(buffer));
memset(LOG, 0, sizeof(LOG));
memset(Mem, 0, sizeof(Mem));
DeviceIoControl(
hFile,//CreateFile打开驱动设备 返回的句柄
ReadWriteCode,//控制码 CTL_CODE
&data,//输入缓冲区指针
sizeof(data),//输入缓冲区大小
Mem,//返回缓冲区
len,//返回缓冲区大小
&dwRetSize,//返回字节数
NULL
);
wsprintf(LOG, L"读取到的内存为:%d", *(UINT64*)Mem);
SetDlgItemText(IDC_EDIT4, LOG);
}

这里 mark 一下,从字符串里面进行十六进制的数值转换可以使用函数 _tcstoui64

驱动我们就根据上面写的主要函数处理即可。

测试截图

可以看到,能成功的读取指定地址的内存。

总结

通过使用驱动绕过读保护,使用 KeStackAttachProcess 附加进程来读虚拟内存,绕过一般的读保护。