-
-
[原创]独行孤客CrackMe-第五题的writeup(一路的艰辛)
-
2017-6-11 01:02 3176
-
程序的特点:MFC程序,反调试,隐蔽的驱动文件,驱动反调试等。
1 MFC程序的特点
MFC程序使用了大量的深度封装的类,这些类除了结构复杂的特点,还有虚函数。虚函数支持了C++的多态性。在C++内存模型中,如果想访问一个对象的虚函数,需要做两件事,首先判断该对象的类型(找到虚函数表,对象前一个DWORD指向该虚表),然后根据索引访问虚表中的函数。这意味着大量使用虚函数和继承的MFC程序有很多张虚表,每有一个实现了虚函数的类,就可能有一张虚表。在MFC中,对虚函数很少有直接的访问,通常是通过虚表地址加上偏移来计算实际调用地址,很不直观。
幸运的是该MFC程序没有在虚函数上绕弯路,尽管如此,深入分析仍然很困难。
2 隐蔽的驱动文件
MFC仍然是一个win32程序,离不开消息事件的驱动,离不开窗口过程的处理。
使用IDA打开程序,没有受到任何阻挠,来到 0x40C14E处的 call _WinMain@16,有兴趣的可以阅读CWinApp类的源码,CWinApp类封装了win32程序的几个关键函数。
连续跟随调用地址,便会看到几个头疼的虚函数调用。
.text:0041C6EC call dword ptr [eax+84h] .text:0041C6FA call dword ptr [eax+50h] .text:0041C711 call dword ptr [eax+58h] .text:0041C711 call dword ptr [eax+68h]
上面的eax便是虚函数表的地址,仅靠代码是无法知道这些对应什么函数调用。
当程序运行在非xp系统时,提示“请在xp平台中重新运行CrackMe",搜索关键字"xp",然后ctrl+x找到唯一引用,来到sub_4013E0。
快速浏览一下,调用了GetSystemMenu和AppendMenuA(创建系统菜单相关的调用),和SendMessageA(msg=80h,查找得知是让图标与窗口关联的消息),还有PostMessageA(msg=12h,让程序退出的消息)
在程序退出提示框之前有一个call sub_402210,判断是否是符合的xp系统。
.text:004014BE mov ecx, ebp .text:004014C0 call sub_402210 ;判断是否是xp系统 .text:004014C5 mov ebx, ds:PostMessageA .text:004014CB test eax, eax .text:004014CD jnz short loc_4014E9 .text:004014CF push 0 ; int .text:004014D1 push 0 ; uType .text:004014D3 push offset Text ; "请在xp平台中重新运行CrackMe" .text:004014D8 call sub_41DB51 .text:004014DD mov edx, [ebp+1Ch] .text:004014E0 push 0 ; lParam .text:004014E2 push 0 ; wParam .text:004014E4 push 12h ; Msg .text:004014E6 push edx ; hWnd .text:004014E7 call ebx ; PostMessageA
如果是xp系统,然后 jnz short loc_4014E9
.text:004014E9 loc_4014E9: ; CODE XREF: sub_4013E0+EDj .text:004014E9 lea eax, [esp+12Ch+Buffer] .text:004014ED push eax ; lpBuffer .text:004014EE push 104h ; nBufferLength .text:004014F3 call ds:GetCurrentDirectoryA ;获取当前工作目录 .text:004014F9 mov edi, offset aVmxdrv_sys ; "\\vmxdrv.sys"
经过各种字符串的操作后, jnz short loc_401587
.text:00401587 loc_401587: ; CODE XREF: sub_4013E0+18Dj .text:00401587 lea edx, [esp+12Ch+Buffer] .text:0040158B mov ecx, ebp .text:0040158D push edx ; lpFileName .text:0040158E push offset ServiceName ; "vmxdrv" .text:00401593 call sub_401AA0 ;这是一个重要的函数 .text:00401598 test eax, eax .text:0040159A mov [ebp+64h], eax .text:0040159D jnz short loc_4015C5
细细分析一下 sub_401AA0
.text:00401AC6 lea eax, [esp+10Ch+Buffer] .text:00401ACA push eax ; lpBuffer .text:00401ACB push 100h ; nBufferLength .text:00401AD0 push ecx ; lpFileName .text:00401AD1 call ds:GetFullPathNameA ;获取指定文件的全路径 .text:00401AD7 push 0F003Fh ; dwDesiredAccess .text:00401ADC push 0 ; lpDatabaseName .text:00401ADE push 0 ; lpMachineName .text:00401AE0 call ds:OpenSCManagerA ;连接到服务控制管理器的数据库,并获得可创建服务的权限 .text:00401AE6 mov ebp, eax .text:00401AE8 test ebp, ebp .text:00401AEA jnz short loc_401B0D .text:00401AEC call ds:GetLastError .text:00401AF2 push eax .text:00401AF3 push offset aOpenscmanagerF ; "OpenSCManager() Faild %d ! \n" .text:00401AF8 call sub_40B946 .text:00401B0D loc_401B0D: ; CODE XREF: sub_401AA0+4Aj .text:00401B0D push ebx .text:00401B0E push esi .text:00401B0F push offset aOpenscmanagerO ; "OpenSCManager() ok ! \n" .text:00401B14 call sub_40B946 .text:00401B19 add esp, 4 .text:00401B1C mov edi, [esp+110h+lpServiceName] .text:00401B23 lea edx, [esp+110h+Buffer] .text:00401B27 push 0 ; lpPassword .text:00401B29 push 0 ; lpServiceStartName .text:00401B2B push 0 ; lpDependencies .text:00401B2D push 0 ; lpdwTagId .text:00401B2F push 0 ; lpLoadOrderGroup .text:00401B31 push edx ; lpBinaryPathName .text:00401B32 push 0 ; dwErrorControl .text:00401B34 push 3 ; dwStartType .text:00401B36 push 1 ; dwServiceType .text:00401B38 push 0F01FFh ; dwDesiredAccess .text:00401B3D push edi ; lpDisplayName .text:00401B3E push edi ; lpServiceName .text:00401B3F push ebp ; hSCManager .text:00401B40 call ds:CreateServiceA ;创建服务,相当于执行sc create ServiceName binPath= BinaryPathName DisplayName=displayName type= kernel start= demand .text:00401B77 loc_401B77: ; CODE XREF: sub_401AA0+B9j .text:00401B77 ; sub_401AA0+C0j .text:00401B77 push offset aCrateservice_0 ; "CrateService() Faild Service is ERROR_I"... .text:00401B7C call sub_40B946 .text:00401B81 add esp, 4 .text:00401B84 push 0F01FFh ; dwDesiredAccess .text:00401B89 push edi ; lpServiceName .text:00401B8A push ebp ; hSCManager .text:00401B8B call ds:OpenServiceA ;如果创建服务失败,可能是已经存在该服务,然后尝试打开该服务 .text:00401B91 mov esi, eax .text:00401B93 test esi, esi .text:00401B95 jnz short loc_401BAB .text:00401B97 call ebx ; GetLastError .text:00401B99 push eax .text:00401B9A push offset aOpenserviceFai ; "OpenService() Faild %d ! \n" .text:00401B9F call sub_40B946 .text:00401BA4 add esp, 8 .text:00401BA7 xor edi, edi .text:00401BA9 jmp short loc_401C13
突然意识到创建服务传入的BinaryPathName=...\vmxdrv.sys在哪里,原来在 sub_401F20处创建了一个驱动文件在
.text:00401566 call sub_401F20
下一行指令下断点,在该程序当前目录下会找到一个vmxdrv.sys驱动文件。
第一次分析驱动文件,经过查找相关资料,得知访问驱动文件,需要一下几个关键函数 CreateFile,DeviceIoControl,WriteFile,ReadFile。
CreateFile是通过"\\.\vmxdrv"作为文件名访问服务的,DeviceIoControl传入控制码,而WriteFile和ReadFile是从驱动设备写入和读取数据。
找到DeviceIoControl的两个调用 sub_4022A0 和sub_401D50。
简单分析下sub_4022A0,发现该调用只是简单发送一个0x222004H的IoControlCode,调用该过程的父调用恰好位于上述的打开vmxdrv服务之后,而且调用了5次该过程。
接下来分析sub_401D50,经一下分析,得知该过程的作用就是往驱动设备发送输入的字符串,然后读取输出,并将输出的16个字节变成长度为32的HexString。
.text:00401E61 lea edx, [esp+33Ch+NumberOfBytesRead] .text:00401E65 push ebx ; lpOverlapped .text:00401E66 push edx ; lpNumberOfBytesRead .text:00401E67 lea eax, [esp+344h+encrypted] .text:00401E6E push 10h ; nNumberOfBytesToRead .text:00401E70 push eax ; lpBuffer ;读取10h个字节,并使用lpBuffer接收 .text:00401E71 push edi ; hFile .text:00401E72 call ds:ReadFile .text:00401E78 lea ecx, [esp+33Ch+encrypted] .text:00401E7F push 10h .text:00401E81 lea edx, [esp+340h+transformed] .text:00401E85 push ecx .text:00401E86 push edx .text:00401E87 call HexString ;这是一个Byte2HexString,在反调试讲述 .text:00401E8C add esp, 0Ch .text:00401E8F mov eax, [eax+4] .text:00401E92 mov [esp+33Ch+var_4], ebx
该过程的caller分析如下
.text:004017C2 mov eax, [eax+4] .text:004017C5 cmp dword ptr [ecx-8], 6 ;输入的字符串长度需要满足为6 .text:004017C9 jnz loc_4018C2 ;这是一个什么都不做的分支跳转 .text:004017CF mov ecx, eax .text:004017D1 call IsDebuggerPresent ;调试标志 .text:004017D6 test eax, eax .text:004017D8 jnz loc_4018C2 .text:004017DE mov eax, [esi+64h] .text:004017E1 test eax, eax .text:004017E3 jz short loc_401802 .text:004017E5 mov edx, [esp+2Ch+var_20] .text:004017E9 lea ecx, [esp+2Ch+var_20] .text:004017ED mov eax, [edx-8] .text:004017F0 push eax ; size_t .text:004017F1 push 0 .text:004017F3 call sub_418263 .text:004017F8 push eax ; char * .text:004017F9 mov ecx, esi .text:004017FB call Input2HexString .text:00401800 jmp short loc_401812 .text:00401812 loc_401812: ; CODE XREF: sub_401760+A0j .text:00401812 lea ecx, [esp+2Ch+var_1C] .text:00401816 lea edx, [esi+5Ch] .text:00401819 push ecx .text:0040181A push ecx .text:0040181B mov ecx, esp .text:0040181D mov [esp+34h+var_14], esp .text:00401821 push edx .text:00401822 call sub_417D43 .text:00401827 mov ecx, esi .text:00401829 call sub_401920 .text:0040182E push 0Ah ; size_t .text:00401830 lea eax, [esp+30h+var_18] .text:00401834 push 2 ; int .text:00401836 push eax ; int .text:00401837 lea ecx, [esp+38h+var_1C] .text:0040183B call sub_415A78 ;这是一个MD5处理过程,根据几个常量特征可以快速得知 .text:00401840 lea ecx, [esp+2Ch+var_18] .text:00401844 mov [esp+2Ch+var_8], 2 .text:00401849 call sub_4182FA .text:0040184E lea ebp, [esi+6Ch] .text:00401851 push offset byte_431398 ; lpString .text:00401856 mov ecx, ebp ; this .text:00401858 call ??4CString@@QAEABV0@PBD@Z ; CString::operator=(char const *) .text:0040185D push offset byte_431398 ; lpString .text:00401862 mov ecx, edi ; this .text:00401864 call ??4CString@@QAEABV0@PBD@Z ; CString::operator=(char const *) .text:00401869 push 0 .text:0040186B mov ecx, esi .text:0040186D call sub_41A4F7 .text:00401872 mov ecx, [esp+30h+var_1C] .text:00401876 push offset a888aeda4ab ; "888aeda4ab" ;将加密后的字符串与"888aeda4ab"比较 .text:0040187B push ecx ; unsigned __int8 * .text:0040187C call __mbsicmp .text:00401881 add esp, 8 .text:00401884 test eax, eax .text:00401886 jnz short loc_401891 .text:00401888 mov ecx, esi .text:0040188A call sub_402030 ;输出success
3分析驱动文件
驱动文件的入口是DriverEntry,尽量参考一些驱动资料。
通常在加载驱动结束前有重要的一个MajorFunction。
memset32(DriverObject->MajorFunction, (int)sub_106EC, 0x1Bu); DriverObject->MajorFunction[0] = (PDRIVER_DISPATCH)sub_106C8; ;IRP_MJ_CREATE, sub_106C8是默认的分发处理 IRP_DISPATCH DriverObject->MajorFunction[3] = (PDRIVER_DISPATCH)sub_105A8; ;IRP_MJ_READ DriverObject->MajorFunction[4] = (PDRIVER_DISPATCH)sub_1061C; ;IRP_MJ_WRITE DriverObject->MajorFunction[14] = (PDRIVER_DISPATCH)sub_1071A; ;IRP_MJ_DEVICE_CONTROL DriverObject->MajorFunction[18] = (PDRIVER_DISPATCH)sub_106C8; ;IRP_MJ_CLEANUP DriverObject->MajorFunction[2] = (PDRIVER_DISPATCH)sub_106C8; ;IRP_MJ_CLOSE (来自wdm.h) DriverObject->DriverUnload = (PDRIVER_UNLOAD)sub_10564;
再来分析 sub_1071A 设备控制处理
int __stdcall sub_1071A(int a1, PIRP Irp) { switch ( *(_DWORD *)(Irp->Tail.Overlay.PacketType + 12) ) { case 0x222004: ;前几个小节提到DeviceIoControl API的几个调用控制码都是0x222004 dword_114D8 = 1; ;这是一个流程控制标志,俗称暗桩 dword_114E0 = (int)IoGetCurrentProcess(); sub_10486(); ;反调试 break; case 0x222008: DbgPrint("%s\n", Irp->AssociatedIrp.IrpCount); break; case 0x22200C: DbgPrint("bye!\n"); break; default: DbgPrint("unkonw code!\n"); break; } Irp->IoStatus.Status = 0; Irp->IoStatus.Information = 0; IofCompleteRequest(Irp, 0); return 0; } PEPROCESS sub_10486() { PEPROCESS result; // eax@1 struct _EPROCESS *v1; // edx@1 result = IoGetCurrentProcess(); v1 = result; while ( result != (PEPROCESS)dword_114E0 ) { result = (PEPROCESS)(*((_DWORD *)result + 34) - 136); if ( result == v1 ) return result; } *((_DWORD *)result + 47) = 0; ;and dword ptr [eax+0BCh], 0, 0BCH DebugPort(参考PEPROCESS)置零 return result; }
然后分析输入处理 sub_1061C
int __stdcall sub_1061C(int a1, PIRP Irp) { PIRP v2; // esi@1 size_t v3; // edi@1 PVOID v4; // eax@1 void *v5; // ebx@1 int result; // eax@2 struct _IRP *Irpa; // [sp+18h] [bp+Ch]@1 v2 = Irp; Irpa = Irp->AssociatedIrp.MasterIrp; v3 = *(_DWORD *)(v2->Tail.Overlay.PacketType + 4); v4 = ExAllocatePoolWithTag(PagedPool, *(_DWORD *)(v2->Tail.Overlay.PacketType + 4), 0x4C4D544Eu); v5 = v4; if ( v4 ) { memset(v4, 0, v3); memcpy(v5, Irpa, v3); ;v5是输入的字符串 if ( dword_114D8 ) ;这个标志来自于Device Control, 经前文分析,程序中调用deviceiocontrol后,dword_114D8置1 { sub_104B6(v5, (int)byte_114C8); ;在该函数中,先经过简单的处理后,进行MD5加密 dword_114DC = 1; ;加密完成后,dword_114DC置1 } ExFreePoolWithTag(v5, 0); v2->IoStatus.Status = 0; v2->IoStatus.Information = v3; IofCompleteRequest(v2, 0); DbgPrint("DispatchWrite called\n"); result = 0; } else { v2->IoStatus.Information = 0; v2->IoStatus.Status = -1073741670; IofCompleteRequest(v2, 0); result = -1073741670; } return result; } sub_104B6: if ( result <= 16 ) { memcpy(&v6, a1, result); v4 = 0; if ( dword_114D8 ) ++v6; if ( v3 > 0 ) { do { *(&v6 + v4) += v4; ;byte[6]分别与byte[6]{1,1,2,3,4,5}相加 ++v4; } while ( v4 < v3 ); } sub_108B2(&v5); sub_11124((int)&v5, &v6, strlen(&v6)); ;MD5加密,该加密算法非常复杂,是通过几个常量特征在网上搜索得知 result = sub_111CE(&v5, a2); }
最后分析读取处理
int __stdcall sub_105A8(int a1, PIRP Irp) { struct _IRP *v2; // edi@1 signed int v3; // edx@2 int v4; // edi@5 v2 = Irp->AssociatedIrp.MasterIrp; if ( !dword_114DC ) ;如果没有加密,始终输出这些值 byte_114C8是4个DWORD,16个字节 { v3 = 3; do { byte_114C8[v3] = 3 * v3 - 100; ++v3; } while ( v3 < 16 ); byte_114C8[0] = -53; byte_114C9 = -86; byte_114CA = -34; byte_114CB = -80; } *(_DWORD *)&v2->Type = *(_DWORD *)byte_114C8; ;如果已经加密,使用4个DWORD复制 v4 = (int)&v2->MdlAddress; *(_DWORD *)v4 = *(_DWORD *)&byte_114C8[4]; v4 += 4; *(_DWORD *)v4 = *(_DWORD *)&byte_114C8[8]; *(_DWORD *)(v4 + 4) = *(_DWORD *)&byte_114C8[12]; Irp->IoStatus.Status = 0; Irp->IoStatus.Information = 16; IofCompleteRequest(Irp, 0); return 0; }
经过以上简单分析,驱动文件的算法如下:
1 接受IoControlCode=222004H后,设置dword_114D8=1,并使DebugPort清零
2 接受输入时,当dword_114D8=1时,先讲字符串与byte{1,1,2,3,4,5}对应相加,然后使用MD5加密,最后设置dword_114DC=1
3 处理读取时,判断dword_114DC=1,如果为1,则输出加密后的16个字节,否则固定输出某些字节
隐藏的dword_114E0,存放IoGetCurrentProcess()返回值,猜测和多线程有关。
而MFC程序的加密算法大致如下:
1 接受6位字符串输入
2 向驱动设备输入该6位字符串
3 读取加密处理后的16个字节,然后转成HexString
4 再次HexString进行MD5加密处理,再次转成HexString
5 讲HexString与"888aeda4ab"比较,似乎有些不对劲,32长度的HexString如何与10长度的比较,肯定还有一些不清楚的地方。
4反调试
1 IsDebuggerPresent 这是一个很常见的反调试。
如果eax=1,则跳到loc_4018C2。
由于这个函数不需要任何参数,所以无需调整堆栈,分别使用5个nop指令和一个xor eax, xor eax替换掉call 和 test指令
.text:004017D1 call IsDebuggerPresent .text:004017D6 test eax, eax .text:004017D8 jnz loc_4018C2
2 头疼的DeviceIoControl
当在调试该程序时,运行到222004H设备控制码时便会抛出内核访问异常,程序无法正常调试。
本来想对驱动文件patch,不让DebugPort置零,然后创建服务。但是这会导致驱动校验无法通过。
观察到除了222004H设备控制码外,还有一个222008H,在修改驱动文件失败后,只能转向修改MFC程序,缺陷就是发送222004H控制码,导致输出的结果只有一种,无法确认对驱动的分析是否正确。
接下来修改MFC程序的几处DeviceIoControl调用,ptach程序,将设备控制码改成222008H。
5使用OD调试MFC程序
果然没有让人头疼的内核异常了。
1.在 0x004017FB( call Input2HexString )处下断点
输入字符串ABCdef,
观察栈顶的指针指向的内容,发现并非是ABCdef,居然是fedcba,多试几次可以确认是未发现的转小写和翻转字符串的处理。
2.在 0x00401E78(call ds:ReadFile指令之后)处下断点
得到输出为16个字节, 0xCB, 0xAA, 0xDE, 0xB0,0xA8,0xAB,0xAE, 0xB1,0xB4,0xB7,0xBA,0xBD,0xC0,0xC3,0xC6,0xC9
3在0x00401E87(call sub_403200)处下断点
步进观察最佳,可以发现hexstring的踪迹 cbaadeb0a8abaeb1b4b7babdc0c3c6c9
4在0x0040183B(call sub_415A78)处下断点
仍然是步进观察最佳,可以发现hexstring的踪迹 c8ebbe345d3b2e7d0b60748aa182d69e
发现该过程除了再次MD5加密外,还将3-12位置的字符串取出来
5在0x0040187B(push ecx)处下断点
发现需要比较的内容果然是ebbe345d3b。
可以通过改变call ds:ReadFile返回的十六个字节的内容来对程序这端的加密过程进行确认。
6 最后写算法穷举
算法过程就是
1 将输入的6位字符串转小写
2 颠倒字符串
3 在驱动程序里,将6位字符串分别与{1,1,2,3,4,5}对应位置的数字相加
4 在驱动程序里MD5处理
5 HexString处理,再次MD5处理,还有再次HexString处理
6 取出3-12位的字符串并与888aeda4ab比较。
双核4线程,穷举时间小于15分钟。
成果不敢独享,默默感谢群里几位大牛的提示和帮助。
附上穷举代码,多线程,Java代码
public class CM5 { public static void main(String[] args) throws Exception { long cnt = 2176782336L; int threadNum = 4; for(int i = 0;i< threadNum;i++){ new RoutineThread(cnt*(i+1)/4,cnt*(i+1)/4).start(); } } static class RoutineThread extends Thread{ MessageDigest md5= MessageDigest.getInstance("MD5"); long start, end; RoutineThread(long start, long end) throws Exception { this.start = start; this.end = end; }; public void run(){ byte [] inc = new byte[]{1,1,2,3,4,5}; for(long i = start; i < end; i ++){ byte[] bytes = Long.toString(i,36).getBytes(); int displacement = bytes.length - 6; byte[] origin = new byte[]{0x30,0x30,0x30,0x30,0x30,0x30}; for(int m = 5; m >= 6 - bytes.length; m--){ origin[m] = (byte) (bytes[m + displacement] + inc[m]); } String dst = bytesToHexString(md5.digest(bytesToHexString(md5.digest((origin))).getBytes())).substring(2,12); if(dst.equals("888aeda4ab")){ System.out.println(new String(bytes)); //输出6891us } } } } private static String bytesToHexString(byte[] buf) { StringBuilder sb = new StringBuilder(buf.length * 2); String tmp = ""; for (int i = 0; i < buf.length; i ++) { tmp = Integer.toHexString(0xff & buf[i]); tmp = tmp.length() == 1 ? "0" + tmp : tmp; sb.append(tmp); } return sb.toString(); } }
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界