遵照着师傅的建议,来复盘一下 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; 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("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 (ZwOpenKey
,ZwQueryValueKey
),看看它的调用情况,大概率也能凭自己写出来。
print hello world
既然现在能加载成功了,那么直接 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 一下。
参考文章