今天试试复盘这个决赛
题目 介绍 这里有一个在屏幕上画flag的小程序,可好像出了点问题,flag丢失了,需要把它找回来,并尝试截图留念。
找回flag样例:
要求
自行寻找办法加载驱动文件,再执行题目exe文件。
不得直接patch系统组件实现绘制(如:直接编写D3D代码绘制flag),只能对题目自身代码进行修改或调用。
找回的flag需要和预期图案(包括颜色)一致,如果绘制结果存在偏差会扣除一定分数。
修复后的flag截图操作必须在题目同一系统环境中进行(如:虚拟机运行题目则在虚拟机中截图,本机运行题目则在本机截图;不得拍照)。
赛后需要提交找回flag的截图 、解题代码或文档 和截图代码或文档 进行评分,方法越多得分越高。
建议使用系统版本:Win10 1809、Win10 1903、Win10 1909、Win10 2004、Win10 20h1、Win10 20h2、Win10 21h1、Win10 21h2,在虚拟机中可能无法正常显示图形。
提交结果打包为XXX_writeup_A.zip,XXX为名称,A为提交序号,从1开始。
分析 P.S.,在做复现的时候发现虚拟机无法正常绘制,且自己 Win11 的物理机运行会蓝屏,因此本次复现不含动态调试部分,一切只停留于静态分析和理论阶段,刷它指定的系统成本过高了接受不了。
驱动分析 IDA 打开,
sub_140001150
函数很像是注册的驱动卸载函数。
sub_140001188
函数应该就是获取了一下系统信息,没什么东西。
sub_140001414
函数往下跟到 sub_1400014A0
函数有大东西,不过这个函数不是直接调用的,像是注册了某种回调,三环程序应该是处罚这个回调的。
开头通过调用 sub_140001318
函数获得了 dwm.exe
的 EPROCESS
结构。
对于接下来调用的函数
sub_140001000
比较像是获取指定进程的某个 DLL
,具体也跟进来看看
对于这些API,网上找到了一些说法:
GetUserModuleBaseAddress(): 实现取进程中模块基址,该功能在《驱动开发:内核取应用层模块基地址》
中详细介绍过原理,这段代码核心原理如下所示,此处最需要注意的是如果是32位进程
则我们需要得到PPEB32 Peb32
结构体,该结构体通常可以直接使用PsGetProcessWow64Process()
这个内核函数获取到,而如果是64位进程
则需要将寻找PEB的函数替换为PsGetProcessPeb()
这个地方也不难判断,就是获取 PEB 结构体,只不过多了一个 32 位和 64 位的判断,以 32 位的为例,中间有类似遍历链表的写法,如果找到了那么把某个结果保存到第二个参数指向的位置然后返回。
这里且当 sub_140001264(v24, "D3DCompile");
函数是获取了某个函数的地址作为返回值出去的,随后是比较关键的点
调用了两次 ZwAllocateVirtualMemory
函数给进程申请内存,然后拷贝 shellcode 并进行了一定的异或混淆,最关键它把 D3DCompile
的地址和第二次申请内存的地址保存在第一次申请的内存后方,应该是方便 shellcode
找到虚拟代码,剩下的大概没有什么了,虽然没有运行成功大概也能猜测这个 shellcode 应该就是直接在屏幕绘制的代码了。
exe分析 三环程序比较大,先用火绒剑分析一下行为,主要是排除 exe 有跟内核做直接数据交互。
然而并没有,但是发现它也打开了 D3DCompiler_47.dll
,于是从这里开始交叉引用,通过DLL路径交叉是一个比较好的思路,不管动态加载或者是运行时直接导入,都是可以大概分析到主逻辑的。
里面就进行了一个 NtQuerySystemInformation
,外面是创建线程调用的这个函数,这里应该是触发回调的一个函数,为了验证也是准备去调试,但是它根本不触发这个回调,如图所见。
之前配置环境的时候一直以为是虚拟机没有 dwm.exe
这个进程,结果没想到是回调没有办法调用,于是我选择自己运行一个 dwm.exe 进程(我直接拿初赛的三环程序去改名然后运行,可以在第一个函数成功被获取),然后自己写一个驱动手动调用那个回调写shellcode。
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 #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 bianliqudongmokuai (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"2022GameSafeRace.sys" ); bianliqudongmokuai(&name,&Base,&Size); if (Base) { kprintf(("Base:%p Size:%x\n" ), Base, Size); func funcptr = (func)(Base + 0x1490 ); DbgBreakPoint(); funcptr(); } return STATUS_SUCCESS; }
运行结果如下,用初赛的exe改名为dwm成功被这个函数获取到EProcess。
随后使用静态分析去解一下 shellcode
,用下面的IDC脚本即可
1 2 3 4 5 6 7 8 9 10 #include <idc.idc> static main () { auto start_ea = 0x000000140005A00 ; auto end_ea = 0x000000140005A00 +0x16E6 ; auto len = end_ea - start_ea; auto ea=0 ; for (ea = start_ea; ea < end_ea; ea++) { PatchByte(ea, Byte(ea)^0xC3 ); } }
解密后的 shellcode
可以被直接反编译
看起来跟初赛是差不多的,相同的配方,相同的味道。
再往下看
就连这个ACE都是一样的,这里大概是一个全新的虚拟机了。
然后本来是打算搜字节码去看看shellcode有没有写成功的,但是发现还是搜不到,突然想到好像这个回调最后会 free 这片内存,所以决定直接改 sys 去把原来的 free 给 jmp
掉(还是失败,想复现太难了 qwq)。
还是老老实实分析虚拟机代码吧,看到 unk_140004030
,它被放到了 BaseAddress + 0x16E6
的位置上,这里的代码在我们看来是在 0x140005A00
,而直接分析可得,代码实际在 &qword_140009600[136]=0x140009600+136*8=0x140009a40
的位置上。
然而这里没找到对应的数据,确实也不太会分析了,按理来说如果能直接调试运行到这的话是肯定可以定位shellcode找到位置dump出来的。
这里还原一下虚拟机的流程吧
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 #include <stdio.h> #include <string.h> #include <stdlib.h> unsigned int code[] ={0 };int main () { int Stack[0x50 ]; unsigned __int64 RIP_S=0 ; unsigned __int64 v10; unsigned int opcode; unsigned __int64 v12; __int64 v13; __int32 v14; __int64 v15; __int64 v16; __int32 v17; unsigned __int64 v18; unsigned __int64 v19; __int32 v20; __int32 v21; __int32 v22; __int64 result; memset (Stack,0 ,sizeof (Stack)); Stack[8 ] = 50 ; Stack[9 ] = 50 ; do { v10 = RIP_S; opcode = code[RIP_S + 272 ]; if ( (int )opcode > (int )0x9A8ECD52 ) { switch ( opcode ) { case 0xEE2362FC : ++RIP_S; v21 = Stack[0 ]; v22 = Stack[0 ] * (Stack[1 ] + 1 ); Stack[0 ] = code[RIP_S + 272 ] ^ 0x414345 ; Stack[1 ] = (Stack[0 ] ^ (Stack[1 ] + v21)) % 256 + (((Stack[0 ] ^ (v21 * Stack[1 ])) % 256 + (((Stack[0 ] ^ (Stack[1 ] + v22)) % 256 ) << 8 )) << 8 ); break ; case 0xEE69524A : v19 = 0 ; v20 = code[v10 + 273 ]; code[RIP_S + 272 ] = -1 ; code[v10 + 273 ] = -1 ; if ( RIP_S != 1 ) { do { code[v19 + 272 ] ^= v20; ++v19; } while ( v19 < RIP_S - 1 ); } ++RIP_S; break ; case 0xFF4578AE : RIP_S += 2 ; v16 = code[v10 + 273 ]; v17 = code[RIP_S + 272 ]; if ( v16 ) { v18 = RIP_S; do { code[++v18 + 272 ] ^= v17; v17 = code[v18 + 271 ] + 305419896 * v17; --v16; } while ( v16 ); } code[v10 + 272 ] = -1 ; code[v10 + 273 ] = -1 ; code[RIP_S + 272 ] = -1 ; break ; case 0x1132EADF : RIP_S += 2 ; Stack[code[RIP_S + 272 ]] = code[v10 + 273 ]; break ; default : if ( opcode == 2018683631 && code[272 ] == -295083446 && code[273 ] == 1755241482 && code[274 ] == -1729111095 ) printf ("call Paint(%d, %d, %d, %d, NAN, a3, a4, a5, a6, a7)" ,Stack[4 ], Stack[5 ], Stack[6 ], Stack[7 ]); break ; } } else { switch ( opcode ) { case 0x9A8ECD52 : Stack[0 ] -= Stack[1 ]; break ; case 0x88659264 : RIP_S += 2 ; v12 = RIP_S; v13 = code[v10 + 273 ]; v14 = code[RIP_S + 272 ]; code[v10 + 272 ] = -1 ; code[v10 + 273 ] = -1 ; v15 = v13; code[RIP_S + 272 ] = -1 ; if ( v13 ) { do { code[++v12 + 272 ] ^= v14; --v15; } while ( v15 ); } break ; case 0x89657EAD : Stack[0 ] += Stack[1 ]; break ; case 0x8E7CADF2 : RIP_S += 2 ; Stack[code[RIP_S + 272 ]] = Stack[code[v10 + 273 ]]; break ; case 0x9645AAED : if ( code[272 ] == 0xEE69624A && code[273 ] == 0x689EDC0A && code[274 ] == 0x98EFDBC9 ) printf ("call Paint(%d, %d, %d, %d, NAN, a3, a4, a5, a6, a7)" ,Stack[4 ], Stack[5 ], Stack[6 ], Stack[7 ]); break ; case 0x9645AEDC : RIP_S = 0x671 ; break ; } } result = 0x671 ; ++RIP_S; } while ( RIP_S < 0x671 ); }
对比起来这个虚拟机的流程也是更大更难去分析了,但是根据已有的资料看来,似乎出的问题与初赛一致,最好的办法就是做 hook 然后替换坐标。据说决赛是卷方法数,当然其他的方法也可以有,这里可以说一些理论可行的方案:
自己生成正确的指令流,直接PATCH SYS 文件。
等代码注入完成之后,搜索指令的特征码找到三环程序中代码的位置,替换(感觉和上面算一种)。
hook 绘制的代码,写入正确坐标。
不用虚拟机,自己接管流程,然后自己计算正确的坐标和加密的参数调用绘制函数。
不知道它代码坐标计算出错的原因,如果是逻辑错误可以直接修虚拟机,也能算一种。
脑子有限,只能想那么多了,希望有时间那个旧电脑退役了刷个系统再去实现这些操作把。