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; SC_HANDLE hServiceDDK = NULL; 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; SC_HANDLE hServiceDDK = NULL; 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错误):
- 首先按下Win键+I打开设置。
- 点击“更新与安全”,然后点击“恢复”。
- 在“高级启动”部分,点击“立即重启”。
- 计算机将进入高级启动选项。在这里,选择“疑难解答”。
- 接着选择“高级选项”。
- 在高级选项中,选择“启动设置”。
- 在启动设置中,点击“重新启动”。
- 计算机将重新启动并显示启动设置。在这里,找到“禁用驱动程序签名强制执行”的选项,按其对应的数字键(一般是7)。
- 计算机将继续启动,此时驱动程序强制签名已被禁用。
然后记得要管理员启动(要不然创建不了句柄)
加载之后的结果:

关于加载驱动之后的报错: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
|
之后启动服务
然后就会断下来了

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

发现这个函数去了一个地址存了起来,然后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; 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的东西了(