0x00 题

初赛共分为ring3和ring0两道题

初赛ring3题目:

winmine.exe是一个扫雷游戏程序,winmine.dmp是该程序的一份进程dump, 在这份dump中,winmine.exe的内存映像有指令被篡改,篡改实现了外挂功能。

请找出dump中,winmine.exe的内存映像中2处被篡改实现外挂功能的指令(被篡改指令的偏移、篡改前后的指令分别是什么),并分析这些指令篡改所实现的外挂功能是什么。

初赛ring0题目:

DriverDemo.sys是一个驱动程序,它内置了一些限制。

1, 不能篡改该文件,尝试使驱动成功加载。

2, 该驱动程序成功加载后,突破它的限制,但不允许patch文件或内存,使它成功打印出(用dbgview可接受)调试信息”hello world!”。

驱动未签名,需要设置Windows 10高级启动选项,禁用驱动程序强制签名后方可答题,支持使用虚拟机。

0x01 解

ring3

给了一个dmp文件,可以考虑先上Windbg分析一下

题目中提到了内存映像中有指令被篡改,因此先把winmine.dmp的内容转写为文件

用windbg打开.dmp,然后再view中找到Modules窗口,然后可以看到这个(需要加载符号表)

知道了基址和大小,就可以将内存写出到文件了、

参考:https://learn.microsoft.com/zh-cn/windows-hardware/drivers/debuggercmds/-writemem--write-memory-to-file-

1
.writemem winmine_dmp.exe 0x1000000 0x1020000

不能有中文路径(不是,哥们,都2024年了怎么还有中文路径问题啊)

由于这个写出的文件是从内存dump出来的,所以他的代码节位置和winmine.exe的代码节位置不同

我们并不需要这个dump出来的文件可运行,但是由于对比需要,我们还是需要找出它的代码节位置

这里可以从源文件中找几个有代表性的连续字节(特征码),然后去dump出来的文件那里找

使用CFF Explorer VIII查看winmine.exe的代码节位置,并挑选适合的特征码

然后在010editor中找到winmine_dmp.exe里对应的位置

再根据winmine.exe的PE结构知道代码节的大小

这里选用Raw Size即00003C00作为大小

用python整个diff脚本分析一下,注意两个文件的代码节起始位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import struct
f1 = open("winmine_dmp.exe", "rb")
f2 = open("winmine.exe")
data1 = []
data2 = []
with open('winmine.exe', "rb") as f:
data1 = list(f.read())
with open('winmine_dmp.exe', "rb") as f:
data2 = list(f.read())

s1 = 0x0400
s2 = 0x1000
r = 0x3c00

for i in range(r):
if data1[s1+i] != data2[s2+i]:
print(f"Memory Addr: {hex(s2+i)} ErrorDump:{hex(data2[s2+i])},CorrectDump:{hex(data1[s1+i])}")

由于代码数据在导入内存后会发生地址的改变(对齐粒度),这种改变是极其正常的,所以前面字节的变化可以忽略

重点在地址比较大的地方发生的改变

1
2
3
4
5
6
7
8
Memory Addr: 0x2ff5 ErrorDump:0x90,CorrectDump:0xff
Memory Addr: 0x2ff6 ErrorDump:0x90,CorrectDump:0x5
Memory Addr: 0x2ff7 ErrorDump:0x90,CorrectDump:0x9c
Memory Addr: 0x2ff8 ErrorDump:0x90,CorrectDump:0x57
Memory Addr: 0x2ff9 ErrorDump:0x90,CorrectDump:0x0
Memory Addr: 0x2ffa ErrorDump:0x90,CorrectDump:0x1
Memory Addr: 0x3591 ErrorDump:0xeb,CorrectDump:0x6a
Memory Addr: 0x3592 ErrorDump:0x1d,CorrectDump:0x0

代码这里用的是装到内存中的基地址作为输出

从这里可以看出有两个地址范围发生了比较大的改变,0x2ff5-0x2ffa和0x3591-0x3592

丢x32dbg里面看看

先看看0x2ff5的

跳转到这个地址然后nop掉

对比发现计时器不会动了,所以可以判断这个地址篡改后的功能是暂停计时

然后再看0x3591

运行看看

懂了,点雷不炸,相当于锁血了

至此ring3结束,到ring0了

ring0

只给了一个驱动文件(DriverDemo.sys),试着加载一下

加载驱动:https://blog.csdn.net/weixin_41725706/article/details/127781355

入门了解一下驱动:https://zhuanlan.zhihu.com/p/690402217

运行驱动的代码参考:https://xia0ji233.pro/2024/03/30/tencent-race-2020-pre/

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
#include <iostream>
#include <Windows.h>
WCHAR lpMsgBuf[0x50];
void 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) {
printf("Service Already Exists\n");
hServiceDDK = OpenServiceA(hServiceMgr, ServeName, SERVICE_START);
}
else if (GetLastError() != 0) {
printf("GetLastError=%p\n", GetLastError());

return;
}
printf("hServiceDDK=%p\n", hServiceDDK);
int bRet = StartService(hServiceDDK, NULL, NULL);
if (GetLastError() == ERROR_SERVICE_ALREADY_RUNNING) {
printf("Service Already Running\n");
}
else {
if (bRet == 0) {
printf("Service Load Fail(%d)\n", GetLastError());
}
else {
printf("Service Start Success\n");
}
}
}
void UnloadDriver(const char* ServeName) {
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 = OpenServiceA(hServiceMgr, ServeName, SERVICE_ALL_ACCESS);
if (hServiceDDK) {
int bRet = 0;
SERVICE_STATUS status;
bRet = ControlService(hServiceDDK, SERVICE_CONTROL_STOP, &status);
if (bRet) {
puts("Stop Service Success");
}
else {
puts("Can't Stop Service");
goto GETLASTERROR;
}
bRet = DeleteService(hServiceDDK);
if (bRet) {
puts("Unload Success");
}
else {
puts("Unload Fail");
}
GETLASTERROR:
printf("GetLastError=%p\n", GetLastError());
}
else {
printf("OpenServe Failed\n");
}
}
int main()
{
LoadDriver("WoaW04", "C:\\Users\\Admin\\Desktop\\DriverDemo.sys");
getchar();
UnloadDriver("WoaW04");
return 0;
}

记得禁用驱动程序签名(要不然会报577错误):

  1. 首先按下Win键+I打开设置。
  2. 点击“更新与安全”,然后点击“恢复”。
  3. 在“高级启动”部分,点击“立即重启”。
  4. 计算机将进入高级启动选项。在这里,选择“疑难解答”。
  5. 接着选择“高级选项”。
  6. 在高级选项中,选择“启动设置”。
  7. 在启动设置中,点击“重新启动”。
  8. 计算机将重新启动并显示启动设置。在这里,找到“禁用驱动程序签名强制执行”的选项,按其对应的数字键(一般是7)。
  9. 计算机将继续启动,此时驱动程序强制签名已被禁用。

然后记得要管理员启动(要不然创建不了句柄)

加载之后的结果:

关于加载驱动之后的报错:https://www.cnblogs.com/qingtian224/p/5580393.html

〖31〗-连到系统上的设备没有发挥作用。

用IDA打开,可以看到VMP

好,如果是vmp的话,那么就不做了

这里有两个方法继续做:

1.用Unicorn_PE

2.动调然后把驱动dump下来

先看看1

1.Unicorn_PE

参考:http://www.qfrost.com/posts/ctf/%E8%85%BE%E8%AE%AF%E6%B8%B8%E6%88%8F%E5%AE%89%E5%85%A8%E7%AB%9E%E8%B5%9B_2020/

关于Unicorn_PE:https://github.com/hzqst/unicorn_pe

基于Unicorn+capstone+Blackbone用于对PE文件做模拟执行后分析的一个超级好用的项目。

从Git上拖下源码编译后直接对驱动模拟执行并Dump

1
./unicorn_pe.exe DriverDemo.sys -dump -k -packed

这玩意坑挺多的,评价是dump的时候确实方便,等个十几分钟可以直接脱壳,但是配置环境的时候就不方便了(

还有参考的WP里面的是修过的,与实物不符(

dump出来的结果和2一样,后续分析后面了

来看看2

2.动调然后把驱动dump下来

这里需要用到双机调试

VS2019:https://blog.csdn.net/u011117126/article/details/124191548

Windbg:https://blog.csdn.net/qq_33504040/article/details/78349885

在进行调试之前先要知道断在哪个函数

本题加了VMP,但是导入表还是比较清楚的,可以找找相关函数

这里着重关注这个MmGetSystemRoutineAddress

参考:https://learn.microsoft.com/zh-cn/windows-hardware/drivers/ddi/wdm/nf-wdm-mmgetsystemroutineaddress

这个函数可以通过函数名获得函数地址,也就是所谓的暗桩调用。

这个一般会拿来调用一些奇奇怪怪的函数,从而实现反调试(

先装载一下驱动

1
sc create DriverDemo binpath="DriverDemo.sys" type=kernel

然后在windbg里面下API断点(注意这个函数是在nt里面的调用的,所以不需要指定驱动)

1
2
bu MmGetSystemRoutineAddress
bp MmGetSystemRoutineAddress

之后启动服务

1
sc start DriverDemo

然后就会断下来了

跳出去到调用这个函数的地方

发现这个函数去了一个地址存了起来,然后call了这个地址

跟进这个call看看

反调试

可以看看这个:https://blog.csdn.net/sanqiuai/article/details/120025003

处理这个反调试有两种方法:patch或者hook

这里参考:https://xia0ji233.pro/2024/03/30/tencent-race-2020-pre/

通过查阅 MSDN(https://learn.microsoft.com/zh-cn/windows-hardware/drivers/ddi/wdm/nf-wdm-kddisabledebugger)得知,该函数是用于反调试的。要把这个过掉,要么 PATCH 这个函数调用,要么hook这个函数,都可以,但是 PATCH 有一定的危险就是你不知道哪个时候的 RAX 的值是多少,因此选择 hook 掉这个函数然后直接返回STATUS_DEBUGGER_INACTIVE

用hook(此代码需要驱动开发相关知识)

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
#include "ntddk.h"  
#define kprintf(format, ...) DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, format, ##__VA_ARGS__)
PVOID addr;
PDRIVER_OBJECT g_Object = NULL;

unsigned char hookBYTE[] = {
0xB8,0x54,0x03,0x00,0xC0,
0xC3
};
int hookLen = sizeof(hookBYTE);
unsigned char originBYTE[0x50];

NTSTATUS MDLWriteMemory(PVOID pBaseAddress, PVOID pWriteData, SIZE_T writeDataSize)
{
PMDL pMdl = NULL;
PVOID pNewAddress = NULL;
pMdl = MmCreateMdl(NULL, pBaseAddress, writeDataSize);
if (NULL == pMdl)
{
return FALSE;
}
MmBuildMdlForNonPagedPool(pMdl);
pNewAddress = MmMapLockedPages(pMdl, KernelMode);
if (NULL == pNewAddress)
{
IoFreeMdl(pMdl);
}
RtlCopyMemory(pNewAddress, pWriteData, writeDataSize);
MmUnmapLockedPages(pNewAddress, pMdl);
IoFreeMdl(pMdl);
return TRUE;
}
void HookHandler() {
memcpy(originBYTE, addr, hookLen);
MDLWriteMemory(addr, hookBYTE, hookLen);
}
void unHookHandler() {
MDLWriteMemory(addr, originBYTE, hookLen);
}
VOID Unload(PDRIVER_OBJECT DriverObject)
{
unHookHandler();
kprintf(("BYE WoaW04\n"));
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
kprintf(("Hello WoaW04\n"));
g_Object = DriverObject;
DriverObject->DriverUnload = Unload;
//DbgBreakPoint();
UNICODE_STRING name;
RtlInitUnicodeString(&name, L"KdDisableDebugger");
addr = MmGetSystemRoutineAddress(&name);

kprintf("Found Function KdDisableDebugger in address: %p\n", addr);
HookHandler();
kprintf(("HookSuccess!\n"));
STATUS_DEBUGGER_INACTIVE;

return STATUS_SUCCESS;
}

先运行这个服务(和上面加载题目驱动一样),然后再运行题目,就可以调试了

不过似乎本题反调试的作用就是执行KdDisableDebugger后就无法手动break了,但是断点还是在的,一旦碰到就会断下来,可能没有进行什么更强的操作吧(

这题动调不太好看,还是直接dump驱动下来逆会比较好,断在MmGetSystemRoutineAddress的时候其实就相当于脱了VMP壳了,这个时候就可以得到DriverEntry了

本题有个问题就是DriverDemo.sys加载后不会出现在Windbg中,即无论是在Module窗口还是使用lm命令都无法查看到DriverDemo,从而也就找不到基址

但是有个邪门的招数,当加载这个驱动两次之后再加载,会发现出现了<Unloaded_DriverDemo.sys>+偏移:的前缀,如下图

本来应该是用别的什么软件获取基址然后再dump的,但是这里我走了个邪道

不过要注意在执行邪道的时候要注意:

1.让windbg的pdb文件全部加载完成,模块窗口能显示模块且没有Unable to enumerate kernel-mode unloaded modules, Win32 error 0n30报错

2.在两次之后要使用lm查看一下模块,并且要不在驱动模块的地方进行查看,如在nt!MmGetSystemRoutineAddress中进行查看,如下图:

总之这个时候就能dump了

1
.writemem D:\DriverDemo_dump.sys 基址 L大小

这个大小可以根据lm命令中的Unloaded Module中的DriverDemo大小计算出来

然后就和Unicorn_pe那边dump出来的东西一样了

接下来就开始分析吧

3.逆向工程

参考:https://bbs.kanxue.com/thread-268431.htm

这里采用unicorn_pe那边dump下来的进行分析,两个其实是一样的,就是地址不同罢了

重点是这个函数

需要去到反汇编窗口看

会发现这里有很多没有解析出来的数据,用c转为代码

然后去到函数开头设置一下函数尾

但是设置完之后会发现反编译还是不变,结果还是很奇怪

这个时候就要去找那两个sub了:sub_140001000和sub_140001160

应该是这两个函数里面识别出错导致IDA分析出问题了

以sub_140001160为例,可以在反汇编中看到这个:一个call后面接个int3

这个int3就是造成解析出错的罪魁祸首,它造成了函数的隔断,需要把它改为nop

其他地方也有很多这种情况,一一进行更改即可

然后是函数问题,IDA并没有想象中智能,经常会莫名其妙加个函数头,实际上是一个的函数会被他变成好几个,这个时候就需要去掉多出来的函数头,然后将真正的函数尾进行延长,从而让IDA能正确反编译

有些时候还会碰到这种中间有个0的,将0换成nop,其余变成代码

然后要把sub_140001160的返回地址位置改一改(最重要)

之后把1160给undefined,再重新识别,让IDA能识别出函数1160的return

最后就可以在sub_140143E84的位置看到完整的代码了

改一改函数名就可以去分析了

4.分析

参考:http://www.qfrost.com/posts/ctf/%E8%85%BE%E8%AE%AF%E6%B8%B8%E6%88%8F%E5%AE%89%E5%85%A8%E7%AB%9E%E8%B5%9B_2020/#ring0

改了一下函数名

第一问

让驱动能成功加载,那就看一下sub_140001160

v12被赋值,转为字符之后可以看到是这么一个东西

1
\\REGISTRY\\MACHINE\\SOFTWARE\\AppDataLow\\Tencent\\{61B942F7-A946-4585-B624-B2C0228FFEBC}

一眼注册表值,结合一下下面的key,不难判断这是对注册表某项的键值的验证

所以在注册表中指定位置加上上面那段即可

可以看到驱动加载了,而且输出了调试信息(这个调试信息要开Enable Verbose Kernel Capture才能看到)

第二问

搜字符串可以找到helloword的字样

跟过去找交叉引用

来到这个函数(这里也是参考进行修改)

往上跟会回到这里,是这个sub_140001D40里面的东西

看不太懂,参考一下大哥

驱动入口函数通过注册表check函数后创建了一个名为 “\BaseNamedObjects\tp2020” 的通知事件,并对这个事件调用了KeClearEvent函数对其清空,然后调用了 CreateThreadWaitEvent 函数,这个函数内创建了一个线程,跟进线程函数看一下

里面就是一个While循环并调用KeWaitForMutexObject等待前面创建的事件进入信号态,若事件进入信号态则会输出HelloWorld。并且这个驱动内再没有其他对于该事件对象的交叉引用了。 至此,题意非常明确了,要求我们写一个驱动将该事件设置为信号态,使得该驱动输出HelloWorld

这里本来应该有个while循环的,但我这边没有,不过不影响解题

因此就可以写驱动了

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
#include "ntifs.h"   

VOID PrintHelloWorld() {

UNICODE_STRING usEventName = { 0 };
HANDLE EventHandle = NULL;

RtlInitUnicodeString(&usEventName, L"\\BaseNamedObjects\\tp2020");
PRKEVENT pEvent = IoCreateNotificationEvent(&usEventName, &EventHandle);
if (!pEvent) {
DbgPrint("IoCreateNotificationEvent Error\n");
return;
}
KeSetEvent(pEvent, 0, FALSE);

}

VOID DriverUnload(PDRIVER_OBJECT DriverObject) {
if (NULL != DriverObject)
DbgPrint("[%ws]Driver Upload, Driver Object Address:%p", __FUNCTIONW__, DriverObject);
return;
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) {

KdBreakPoint();
UNREFERENCED_PARAMETER(RegistryPath);


PrintHelloWorld();

DriverObject->DriverUnload = DriverUnload;

return STATUS_SUCCESS;

}

最后也是成功输出了

总结:不想再碰ring0的东西了(