遵照着师傅的建议,来复盘一下 2020 年的比赛。

初赛

题目说明

初赛共分为ring3和ring0两道题,每题5分,累计积分,即满分为10分。

初赛ring3题目:(本题共5分)

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

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

2, 请提供文档,详细描述解题过程,如涉及编写程序,必须提供源代码。(1分)

初赛ring0题目:(本题共5分)

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

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

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

请以文档方式,详细描述解题过程,如涉及编写程序,必须提供源代码。

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

Ring3

提供了 dump 文件而且提供了正常的游戏文件,那么马上可以想到去提取内存中的指令去 diff,这里直接 windbg preview 打开然后使用命令 .writemem D:\winmine.exe 0x1000000 0x1020000-1

为了方便,可以先用 CyberChef 把提取的内存文件转为 hex 然后 split by \n

同样正常打开 winmine 再去附加 dump,同样的方法得到文件,然后使用一个脚本去 diff

1
2
3
4
5
6
7
8
f1=open("download1.dat","r").read().split("\n")
f2=open("download2.dat","r").read().split("\n")

l=len(f1)

for i in range(l):
if f1[i]!=f2[i]:
print(f"Memory Addr: {hex(i)} ErrorDump:{f1[i]},CorrectDump:{f2[i]}")

一般这种修改喜欢去动跳转,所以着重找 E8 E9 EB 这些无条件跳转指令。

在外挂的 dump 中发现一个 EB jmp 指令,于是直接用 x32dbg 去启动找到对应的位置。

按照它字节码的形式,该命令应该被改为 jmp 0x10035b0

而进行了修改之后,发现点雷不会爆

第一点就分析出来了

winmine.exe+0x3591,修改前:PUSH 0 修改后:jmp 0x10035B0,外挂功能是点雷不会爆炸。

后面本来没什么头绪了,意外看到有一连串的 90。

于是尝试修改,发现时间暂停了,这里应该是计数器。

winmine.exe+0x2FF5,修改前:inc dword ptr ds:[0x0100579C] 修改后:NOP*6,外挂功能是时间暂停。

Ring0

加载驱动

直接用一个 LoadDriver 三环进程加载:

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
#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("xia0ji233", ".\\DriverDemo.sys");
getchar();
UnloadDriver("xia0ji233");
}

运行之后发现出现错误

通过查找 GetLastError 能发现

1
2
3
ERROR_GEN_FAILURE
31 (0x1F)
A device attached to the system is not functioning.

驱动 VMP 了,遂放弃(bushi

但是通过参考了一些其他的文章[1],可以得知,既然无法 PATCH 内存和文件,必定是驱动内部有一个机制来检测电脑状态是否可以加载驱动,遂打开 IDA 分析,找到关键字符串 MmGetSystemRoutineAddress,通过这个看看它调用了什么内核函数。参考的文章中,使用了 unicorn_PE 去直接脱,笔者试了一下的确是不错的方法,那么我这里就复现一下文中没提到的另一种方法,也就是带壳直接调试分析。

开机之后,断点打在这个函数,加载之后成功被断下,然后跳出来,虽然不知道在哪,但是通过上面一系列赋值吸引到了我,直接跳过去看看发现赋值了一个字符串 KdDisableDebugger

通过查阅 MSDN [2]得知,该函数是用于反调试的。要把这个过掉,要么 PATCH 这个函数调用,要么hook这个函数,都可以,但是 PATCH 有一定的危险就是你不知道哪个时候的 RAX 的值是多少,因此选择 hook 掉这个函数然后直接返回 STATUS_DEBUGGER_INACTIVE

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
#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 xia0ji233\n"));
}

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

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

return STATUS_SUCCESS;
}

但是这个被 VMP 的在调试过程中看起来逻辑也挺清晰的,因此想到能不能把内存都dump一下看看,但是由于它无法成功被加载, windbg 中也看不到它的地址,调试的时候也比较难确定范围。根据 @Qfrost 的提示,选择使用 LoadImageNotify 去 dump 一下此刻的驱动(暂时放弃)。

但是接着往下跟的过程中,不久后发现了另一处很有意思的调用。

拉到最后,在内存视图中发现一串类似注册表的路径:

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

同时发生的变动中还有 key 这个数据,这里其实有点猜的成分在了。

在尝试了 {61B942F7-A946-4585-B624-B2C0228FFEBC} 作为数据的时候发现不行,但是它作为项且在里面有一个 key 的值为 1 时,驱动加载成功。

如果不猜的话可以选择hook一下跟注册表相关的 API (ZwOpenKeyZwQueryValueKey),看看它的调用情况,大概率也能凭自己写出来。

既然现在能加载成功了,那么直接 dump sys,虽然已经成功加载,但是 windbg 中的 lm 还是无法列出模块的地址,因此直接写个驱动查地址用 windbg 去 dump,当然可以不用写,用 ARK 工具也能直接找到块的基址,但是为了训练一下自己还是自己写一下吧[3]:

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
#include"ntddk.h"  
#define kprintf(format, ...) DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, format, ##__VA_ARGS__)
typedef void (* func)();
PDRIVER_OBJECT g_Object = NULL;
typedef struct _LDR_DATA_TABLE_ENTRY {
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;//驱动的满路径
UNICODE_STRING BaseDllName;//不带路径的驱动名字
ULONG Flags;
USHORT LoadCount;
USHORT TlsIndex;
union {
LIST_ENTRY HashLinks;
struct {
PVOID SectionPointer;
ULONG CheckSum;
};
};
union {
struct {
ULONG TimeDateStamp;
};
struct {
PVOID LoadedImports;
};
};
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

VOID Getaddr(PUNICODE_STRING name, UINT64* pBaseAddr,UINT64* pSize)
{
LDR_DATA_TABLE_ENTRY*TE, *Tmp;
TE = (LDR_DATA_TABLE_ENTRY*)g_Object->DriverSection;
PLIST_ENTRY LinkList;
;
int i = 0;
LinkList = TE->InLoadOrderLinks.Flink;
while (LinkList != &TE->InLoadOrderLinks)
{
Tmp = (LDR_DATA_TABLE_ENTRY*)LinkList;
if (RtlCompareUnicodeString(&Tmp->BaseDllName, name, FALSE))
{
}
else
{
kprintf(("Found Module!\n"));
*pBaseAddr = (UINT64)(Tmp->DllBase);
*pSize = (UINT64)(Tmp->SizeOfImage);

}
LinkList = LinkList->Flink;
i++;
}


}
VOID Unload(PDRIVER_OBJECT DriverObject)
{
kprintf(("BYE xia0ji233\n"));
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
kprintf(("Hello xia0ji233\n"));
g_Object = DriverObject;
DriverObject->DriverUnload = Unload;
UNICODE_STRING name;
UINT64 Base = 0;
UINT64 Size=0;
RtlInitUnicodeString(&name, L"DriverDemo.sys");
Getaddr(&name,&Base,&Size);
if (Base) {
kprintf(("Base:%p Size:%x\n"), Base, Size);
}

return STATUS_SUCCESS;
}

加载驱动之后,获得基址

1
.writemem D:\dump.sys 0xFFFFF8081C4E0000 0xFFFFF8081C4E0000+0x39f000-1

拖出来的 sys 还是有部分垃圾指令非常影响阅读,所以直接看字符串。

发现 hello world 字符串,直接跟过去。

分析不来,遂放弃,于是考虑从其它字符串入手。

其中有一个是设备,但是 \BaseNameObjects\tp2020 本来没有什么头绪,后面突然想到搜一下 BaseNameObjects 是个什么东西,在 MSDN 上发现一些东西,这个是跟事件有关的东西,然后就再没什么思路了。

后面看才知道是要对这个事件做一些操作,把这个事件置为信号态之后就可以输出 hello world 了,这个壳不太会脱还是太菜了,这里也 copy 一下正确解法吧:

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
#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;

}

最终也是成功输出,Mark 一下。

参考文章