前言

起因是因为某手游用CE附加会发现无法附加上,那包括很多目前流行的游戏,steam也好,wegame也好基本上都是这个样子的;

在没有驱动编程知识的条件下盲人摸黑阶段,只有用r3的思路去揣测,是否是对进程,服务,等等内容进行遍历找CE的关键字段被检测到了,又或者调了一个Windows回调不停的遍历text段和关键部分看是否被篡改等等思考,当然用ida传统CTF逆向的方法去做很难,代码量太大,把文件扔进去ida自动扫描就要扫半天;

根据这些r3的思考也做过几次尝试,比如用魔改版的CE,比如写dll注入读数据,都没有结果,而且也不知道原因啊,卡了一段时间;

之后在b站刷视频,说是这个手游有一个驱动保护,权限高于用户层,那么去网上找了个system权限的ce工具,发现可以读写内存了,但是会被检测出来,游戏中途直接G掉,这个检测一时半会儿也找不出来,同时不能直接扣走这个驱动,它和游戏是一体的,扣走无法正常游戏;

后来看了一篇博客:https://blog.csdn.net/u011442768/article/details/109207144

里面分析了三种可能,同时这个文章用到了ark工具,我也下了个pc hunter来用,然后?然后整件事情就有了进展;

PcHunter分析

就一个一个的找过去,能发现这几个地方:

image.png

有一说一pchunter真好用吧,这个比拿着ida在那瞎jb逆好多了;

所有回调都是这个保护驱动进行的,那思路首先应该是逆一下这个驱动,找关键的回调函数,我其实都想照着上面说的博客文章进行一步一步的试了,dump驱动的镜像内存,直接跳偏移去看这些函数,但是我发现了一个问题如下:

image.png

可能是我不太会dump吧,搞出来的镜像貌似是静态的,关键代码段都被内嵌的upx加密了,线索被断掉了(菜);

花时间的话应该也能搞定dump这块,但之后的内容让我省了这一部分技术性的操作;

去必应搜索obcallback可以得到很多有用的东西,比如obcallback实际上会去操作OB_PRE_OPERATION_INFORMATION这个结构体,这个结构体是每个进程单独有的,里面有一个叫做 DesiredAccess 的字段,是影响进程读写内存等等权限的;

那之前是用SYSTEM级别权限的CE可以直接进行读写,是否可以猜测保护驱动展开的obcallback在一直给每个新开的进程读写内存降权,因为它的obcallback是在进程线程创建时会去调用;

根据这个猜测进行尝试,如果我在它的回调调用之后进行提权操作,那么就可以恢复读写权限;

用魔法打败魔法,它是驱动执行的ob回调,那么我们也写一个驱动来执行我的ob回调;

编写驱动尝试

去b站搜怎么写驱动啊,怎么让驱动调用obcallback,那也是给找着了;

首先还是用VS2022进行,但是需要安装SDK和WDK,同时这两个需要同样的版本号;

同时WDK需要和windows版本对应,为了使用支持VS2022版本的WDK还特别升级了Windows11 22H2进行和WDK进行匹配;

然后神奇的发现了电脑开机速度还变快了( 别用垃圾win11 21h2

之后环境配好之后开始写驱动,实现的代码如下:

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
//包含驱动开发中基本的数据结构,数据类型
//类似于iostream
#include <ntifs.h>
#include <ntddk.h>

//数据结构 LDR
typedef struct _LDR_DATA {
struct _LIST_ENTRY InLoadOrderLinks;
struct _LIST_ENTRY InMemoryOrderLinks;
struct _LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
ULONG32 SizeOfImage;
UINT8 _PADDINGO_[0x4];
struct _UNICODE_STRING FullDllName;
struct _UNICODE_STRING BaseDllName;
ULONG32 Flags;
} LDR_DATA, * PLDR_DATA;

//定义一个系统函数
NTKERNELAPI UCHAR* PsGetProcessImageFileName(PEPROCESS Process);

//声明回调函数
OB_PREOP_CALLBACK_STATUS MyCallBack(PVOID RegistrationContext, POB_PRE_OPERATION_INFORMATION OpertaionInformation);
//给ce改名,长ce名get名称函数有问题
const char* g_MyProcessName = "111.exe";
//实现
OB_PREOP_CALLBACK_STATUS MyCallBack(PVOID RegistrationContext, POB_PRE_OPERATION_INFORMATION OpertaionInformation)
{
//只要有进程创建就会调用,所以先if判断
//KdPrint(("this is in call back!!!\n"));
PEPROCESS Process = PsGetCurrentProcess();
//KdPrint((PsGetProcessImageFileName(Process)));
if (_strnicmp(g_MyProcessName, PsGetProcessImageFileName(Process), strlen(g_MyProcessName)) == 0)
{
//是我们要找的进程
//恢复权限 openprocess
OpertaionInformation->Parameters->CreateHandleInformation.DesiredAccess = 0x1fffff;
OpertaionInformation->Parameters->DuplicateHandleInformation.DesiredAccess = 0x1fffff;
KdPrint(("进入callback!\n"));
return 0;
}
return OB_PREOP_SUCCESS;
}

//结构体数组 定义了callback
OB_OPERATION_REGISTRATION ObUpperOperationRegistration[] =
{
//第二个参数是回调执行时期,第三个参数是回调地址
//这里让回调在进程线程创建和复制的时候执行
{NULL, OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE ,MyCallBack,NULL},
{NULL, OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE ,MyCallBack,NULL}
};
OB_OPERATION_REGISTRATION ObLowerOperationRegistration[] =
{
{NULL, OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE ,MyCallBack,NULL},
{NULL, OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE ,MyCallBack,NULL}
};

//注册回调需要的结构体变量
OB_CALLBACK_REGISTRATION UpperCallbackRegistration =
{
OB_FLT_REGISTRATION_VERSION, //版本号
2, //注册数量 (线程,进程创建时)
RTL_CONSTANT_STRING(L"880000"), //影响回调的执行顺序 回调号 越小后执行
NULL,
ObUpperOperationRegistration //另一个数据结构,包含回调函数
};
//执行两次,包含原神的回调函数在数字之间,必定在它的回调函数之后执行我们的
OB_CALLBACK_REGISTRATION LowerCallbackRegistration =
{
OB_FLT_REGISTRATION_VERSION,
2,
RTL_CONSTANT_STRING(L"1000"),
NULL,
ObLowerOperationRegistration
};

HANDLE g_UpperHandle, g_LowerHandle;

//声明回调函数的注册和卸载函数
BOOLEAN ObRegisterCallback(PDRIVER_OBJECT DriverObject);
void ObRegisterUnload();
//实现
BOOLEAN ObRegisterCallback(PDRIVER_OBJECT DriverObject)
{
NTSTATUS sta;
//注册回调函数需要签名
//或上一个0x20可以不需要签名 这个flag在win api里是timestamp的作用
PLDR_DATA ldr;
ldr = (PLDR_DATA)DriverObject->DriverSection;
ldr->Flags |= 0x20;

//指定回调类型 这个结构体的第一个参数
ObUpperOperationRegistration[0].ObjectType = PsProcessType;
ObUpperOperationRegistration[1].ObjectType = PsThreadType;

ObLowerOperationRegistration[0].ObjectType = PsProcessType;
ObLowerOperationRegistration[1].ObjectType = PsThreadType;

//进行注册 第二个参为返回句柄
sta = ObRegisterCallbacks(&UpperCallbackRegistration, &g_UpperHandle);
if (!NT_SUCCESS(sta))
{
//失败就进卸载
ObRegisterUnload();
g_UpperHandle = NULL;
}
sta = ObRegisterCallbacks(&LowerCallbackRegistration, &g_LowerHandle);
if (!NT_SUCCESS(sta))
{
ObRegisterUnload();
g_LowerHandle = NULL;
}
}
void ObRegisterUnload()
{
if (g_UpperHandle != NULL)
ObUnRegisterCallbacks(g_UpperHandle);
if (g_LowerHandle != NULL)
ObUnRegisterCallbacks(g_LowerHandle);
}


//下面的为驱动主体 ----------------------------------------

//每个驱动都需要卸载,卸载函数
void DriverUnload(PDRIVER_OBJECT DriverObject)
{
//卸载注册的回调
ObRegisterUnload();
KdPrint(("Driver unload!\n"));
}

// NTSTATUS 32位数据 -> DWORD
NTSTATUS DriverEntry(
PDRIVER_OBJECT DriverObject, //驱动对象 -> 句柄
PUNICODE_STRING RegistryPath //注册表路径
)
{
//指定卸载函数
DriverObject->DriverUnload = DriverUnload;

//注册回调函数
ObRegisterCallback(DriverObject);

//debug版本中是printf的作用,release版本无作用,注释掉了
KdPrint(("hello second bc.\n"));

return 0;
}

里面的注释都很到位,可以慢慢看;

解释下的话,驱动编写需要注意几点,驱动的main函数叫DriverEntry,同时需要给到卸载函数(析构的作用);

同时obcallback的调用需要进行注册,之后需要卸载;

KdPrint其实是DbgPrint的一个宏定义,在debug版本可以显示打印内容;

之后的话,搜索怎么调试驱动搞了半天,要什么双机调试,不然蓝屏之类的,所以驱动开发还是得要虚拟机,别用真机吧;

我懒得搞就下了个 driver monitor进行手动加载驱动,包括下载一个 debugview 进行对KdPrint内容的捕捉(既然不好调试就用最经典的print调试法);

然后发现driver monitor装载不上,是因为驱动要签名,又去找签名方法,找到了个好用的方法,b站视频如下:

驱动数字签名教程_哔哩哔哩_bilibili

之后就可以装载驱动进行测试了,发现我日还真能直接用原版ce进行读写内存了,而且不会显示被检测到:

image.png

但是还是依然无法进行调试器附加,原因是和pchunter找到的应用层钩子有关系啊,它钩住了一个ntdll的dbgbreakpoint,把这个恢复之后就能成功附加上了;

至此CE反附加就攻克了;

稳定性调试

当然ce能附加了,但游戏依然不稳定,时不时就会掉线,ce附加时不时也会自动脱落,有时候还会在搜数据的时候游戏弹异常直接G;

猜测它的驱动保护里还有一些对于调试器和读写内存的检测;

试了很多办法之后,不管是改调试器,还是开dbvm,等等都无法稳定调试;

那最稳妥的方法应该是折返回去好好逆一下保护驱动的dump出的内存;

想着我们写的驱动和它的保护驱动性质差不多,都要去注册ob回调,干脆一不做二不休把我们的驱动改个名扔它启动目录下去伪装成它的保护驱动,结果还相当有效,简直瞎猫碰死耗子了;

猜测原因是它的主程序执行驱动的时候,只是单纯做了简单判断有没有这个文件,跑没跑上,没有去检测文件签名属性,以及内容;

只能说运气特别好啊,省了超级多麻烦,这样一来游戏一执行还会启动我们的驱动程序,都省的用driver monitor了;

之后就是一马平川了,该怎么玩CE就怎么玩,游戏数据也能正常被保存到服务器上;

总结

总的说来,这个保护驱动并不强,最狠的最主要的就是通过obcallback来进行降权,没有内核钩子;

这一套下来也花了两天时间去做,反思了一下如果想要攻克其他类型的驱动保护,就需要进一步的学习驱动编程以及内核R0部分的内容,同时需要更好的逆向手法去获取内存中真实的镜像拿到代码去分析,才能对症下药去解决检测问题;

这其实还能算是我驱动编程的入门学习了;

没有使用r3那套进行辅佐保护我猜测的原因是模块化导致的,游戏一开始做的时候,保护和内容是分开的,同时以我的那套r3保护构想来说的话,太吃资源性能也是一方面的问题;