【文章标题】: 一个奇怪的带壳crackme的简单分析 【文章作者】: hawking 【作者邮箱】: rich_hawking@hotmail.com 【软件名称】: Bustme4 【软件大小】: 21.5k 【下载地址】: bm4.zip 【加壳方式】: 不详 【保护方式】: 未知 【使用工具】: PEiD OLLYICE 【操作平台】: win2k 【作者声明】: 只是感兴趣,没有其他目的。失误之处敬请诸位大侠赐教! -------------------------------------------------------------------------------- 【详细过程】 首先声明,本人菜菜,分析水平有限,本文只是自己破解过程的一个记录,供和我一样的菜鸟参考,也备自己日后遇到类似问题时查找。侠之大者可以直接漂过。如果肯指点一二,感激不尽。 近来坛子里加壳与脱壳版里一个奇怪的带壳crackme讨论的人很多,不少大牛都信手给出了key,成了创意比拼。我等小菜,载入OD里还没开始跑就GameOver了。实在是心有不甘。 发扬一下cracker的风格,自己动手搞定它。感谢skylly,我一开始茫无头绪时,是你的提示给了我前进的方向。
(一)Anti (Validate部分参见11楼)
首先用PDiD查一下壳,显示Nothing found * 点击EP Section后面的按钮可以看出有6个段,段名都是$Content$nbsp;看来好像是加壳了。 OD载入
00409508 >- E9 00500000 jmp 0040E50D 0040950D 0000 add byte ptr [eax], al 0040950F 0000 add byte ptr [eax], al
F8,来到0040E50D,OD提示不知如何单步,晕,才第一条指令啊。
1 tlscallback+CC保护入口 我们再次在PEiD中打开,点一下Subsystem后面的按钮,看看PE Details 可以看到 Directory Information RVA SIZE TLSTable: 00009000 00000024 点一下后面的按钮打开TLS Viewer可以看到 CallbackTableVA: 00409060 在数据窗口里可以看到00409060处的数值是00409040。 具体TLS是什么我也不清楚,如果有大侠路过,请指点一下。只是大概了解这里的Callback Function会在系统加载程序的时候执行(程序第一条代码执行之前),我们可以看 到这里的Callback Function地址是00409040,我们在这里下断。然后将OD调试选项中事件选项卡设置第一次暂停于系统断点(默认情况下会暂停于WinMain)。 Ctrl+F2重新开始。 77F813B2 C3 retn 77F813B3 33C9 xor ecx, ecx 77F813B5 E9 A5BE0000 jmp 77F8D25F F9 00409040 803D 08954000 C>cmp byte ptr [<模块入口点>], 0CC ;看看模块入口点是否下断 是则很可能被调试 00409047 75 0A jnz short 00409053 ;如果没下断则直接去模块入口点 00409049 C705 09954000 0>mov dword ptr [409509], 5000 ;如果下断了则修改模块入口点代码,使jmp跳向一个没有内容的地址,引发异常 00409053 C2 0C00 retn 0C 原来OD会自动在模块入口点下Int3断点,如果我们Alt+B打开断点窗口就会看到 地址 模块 激活 反汇编 00409508 Bustme4 仅一次 jmp 00409180 我们修改jnz 为 jmp ,始终让它跳就可以避免这里的检测了。 ----------------------------------------------------------------------------------------- CC 反调试 注意后面通过的00750000段代码也存在Int3反调试,有些地址下F2断点将会导致程序异常。 0075001E 8B0424 mov eax, dword ptr [esp] 00750021 8038 CC cmp byte ptr [eax], 0CC 00750024 - 0F84 A5C5FFFF je 0074C5CF 0075002A C3 retn 另外00401000代码段也存在Int3反调试。 00401081 A1 F2324000 mov eax, dword ptr [<&USER32.GetDlgItemTextA>] 00401086 8038 CC cmp byte ptr [eax], 0CC 00401089 - 0F84 2AE1FFFF je 003FF1B9 0040108F A1 F6324000 mov eax, dword ptr [<&USER32.MessageBoxA>] 00401094 8038 CC cmp byte ptr [eax], 0CC 00401097 - 0F84 2AE1FFFF je 003FF1C7 如果想在上述地址下断点的话,就要修改一下je处的代码,直接nop掉就可以了。 ----------------------------------------------------------------------------------------- 由于程序老是出现意外,我们不得不频繁重新开始,这种重复性的工作太没有意思了。怎么办,交给脚本来处理好了,这里我们用OllyScript写脚本通过检测。 脚本其实很简单的,脚本命令总共也就那么几条,看一下OllyScript附带的说明文档就搞定了,也可以参考hnhuqiong大侠的ODbgScript 入门系列。 Script: asm 00409047,"jmp 00409053" esto 脚本很简单,由于水平不高,地址都是硬编码的,有些地址可能会因为机器的不同而存在差异,请自行修改。 2 进程入口参数校验 来到模块入口点 00409508 >^\E9 73FCFFFF jmp 00409180 00409180 803D E1944000 0>cmp byte ptr [4094E1], 1 ;比较4094E1处标记,是1的话则Call ExitProcess Game Over! 00409187 0F84 73030000 je 00409500 0040918D C605 E1944000 0>mov byte ptr [4094E1], 1 ;设4094E1处标记为1,如果程序被Dump的话,则执行Dump后的程序会不正常 00409194 FF15 1E314000 call dword ptr [<&KERNEL32.GetCommand>; KERNEL32.GetCommandLineA 取命令行参数 0040919A 83F8 00 cmp eax, 0 0040919D 74 16 je short 004091B5 0040919F 50 push eax 004091A0 68 52944000 push 00409452 ; ASCII "Teeth_In_The_Grass" 004091A5 FF15 C6304000 call dword ptr [<&KERNEL32.lstrcmp>] ; KERNEL32.lstrcmpA 比较参数与上面的字符串 004091AB 83F8 00 cmp eax, 0 004091AE 75 05 jnz short 004091B5 ;如果命令行参数不等于上面的字符串则跳 004091B0 - E9 F7FAFFFF jmp 00408CAC 004091B5 68 58020000 push 258 004091BA 68 F6914000 push 004091F6 004091BF 6A 00 push 0 004091C1 FF15 1A314000 call dword ptr [<&KERNEL32.GetModuleF>; KERNEL32.GetModuleFileNameA 004091C7 68 80944000 push 00409480 004091CC 68 A0944000 push 004094A0 004091D1 6A 00 push 0 004091D3 6A 00 push 0 004091D5 6A 00 push 0 004091D7 6A 00 push 0 004091D9 6A 00 push 0 004091DB 6A 00 push 0 004091DD 68 52944000 push 00409452 ; ASCII "Teeth_In_The_Grass" 004091E2 68 F6914000 push 004091F6 004091E7 FF15 16314000 call dword ptr [<&KERNEL32.CreateProc>; KERNEL32.CreateProcessA 将CommandLine设为上面的参数然后创建进程 004091ED 6A 00 push 0 004091EF FF15 C2304000 call dword ptr [<&KERNEL32.ExitProces>; KERNEL32.ExitProcess Game Over! 004091F5 C3 retn 这里我们一路F8,可以看到由于程序没有带命令行参数启动,导致上面的字符串比较没能通过,程序将"Teeth_In_The_Grass"作为CommandLine的值,重新创建了一个新的进程,然后就退出了。 要通过这里的检测,我们必须将"Teeth_In_The_Grass"作为参数创建进程并调试。 Script: asm 0040918D,"mov byte ptr[4094E1],0" go 40919a mov eax,00409452 go 4091B0 3 SetUnhandledExceptionFilter 然后就是一路jmp来jmp去,来到 004033C7 FF20 jmp dword ptr [eax] ;这里jmp到SetUnHandledExceptionFilter 看看堆栈 0006FFBC FFFFFFFF /CALL 到 SetUnhandledExceptionFilter 0006FFC0 00405325 \pTopLevelFilter = Bustme4.00405325 0006FFC4 77E71AF6 返回到 KERNEL32.77E71AF6 观察堆栈可以知道,如果程序出现了UnhandledException,正常运行时将会由00405325处的代码进行处理。由于我们正在调试程序,这时如果F9,OD会提示程序不知如何继续。 程序从004091B0处开始就一直跳来跳去,也没干什么正事。要想过这里的检测,我们改一下004091B0处的代码 Script: asm 004091B0,"jmp 00405325" 4 CreateThead 线程保护 ........ 004053AF FF15 DE304000 call dword ptr [<&KERNEL32.VirtualAll>; KERNEL32.VirtualAlloc ........ 00405441 F3:A4 rep movs byte ptr es:[edi], byte ptr [esi] ........ 0040589B FF15 F2304000 call dword ptr [<&KERNEL32.GetCurrentProcessId>] ; KERNEL32.GetCurrentProcessId ........ 0040590D FF15 D2304000 call dword ptr [<&KERNEL32.OpenProcess>] ; KERNEL32.OpenProcess ........ 00405953 6A 02 push 2 ;传Options参数 ........ 00405A03 FF15 EE304000 call dword ptr [<&KERNEL32.DuplicateHandle>] ; KERNEL32.DuplicateHandle 堆栈: 0006FF74 00000040 |hSourceProcess = 00000040 (window) 0006FF78 FFFFFFFE |hSource = FFFFFFFE 0006FF7C 00000040 |hTargetProcess = 00000040 (window) 0006FF80 004052F5 |phTarget = Bustme4.004052F5 0006FF84 00000000 |Access = 0 0006FF88 00000001 |Inheritable = TRUE 0006FF8C 00000002 \Options = DUPLICATE_SAME_ACCESS 这儿程序复制了一个句柄,源句柄为FFFFFFFE(-2),这里按F8将会出现引用的内存不能为"written"的错误。 SourceProcess应该是不存在FFFFFFFE的句柄的,这里不知道有没有什么好的方法处理一下。我们采用最笨的方法,直接跳过它,注意堆栈平衡就可以了。 Script: asm 00405953,"jmp 00405A09" ........ 00405A66 FF15 D6304000 call dword ptr [<&KERNEL32.CloseHandle>] ; KERNEL32.CloseHandle ........ 00405AF1 FF15 02314000 call dword ptr [<&KERNEL32.CreateThread>] ; KERNEL32.CreateThread 堆栈: 0006FF80 00000000 |pSecurity = NULL 0006FF84 00000000 |StackSize = 0 0006FF88 00406727 |ThreadFunction = Bustme4.00406727 0006FF8C 00000000 |pThreadParm = NULL 0006FF90 00000000 |CreationFlags = 0 0006FF94 0040501C \pThreadId = Bustme4.0040501C 这里程序创建了一个新线程,线程函数入口地址00406727,这个线程除了SuspendThread VirtualQuery ResumeThread外没做什么特别的事情。其实并没有像有些大侠说的那样是什么线程保护。 ........ 00405B9D FF15 0A314000 call dword ptr [<&KERNEL32.GetExitCodeThread>] ; KERNEL32.GetExitCodeThread ........ 00405BB8 813D F5524000 0>cmp dword ptr [4052F5], 103 ;比较线程退出代码 ........ 00405BD9 ^\0F84 5EFFFFFF je 00405B3D 00405BDF 66:87CB xchg bx, cx 直接在00405BDF处下断F9就可以通过以上代码了。 5 GetTickCount 时间校验 ........ 00405C46 FF15 12314000 call dword ptr [<&KERNEL32.GetTickCount>] ; KERNEL32.GetTickCount ........ 00405C63 89C6 mov esi, eax ........ 00405D6E FF15 12314000 call dword ptr [<&KERNEL32.GetTickCount>] ; KERNEL32.GetTickCount ........ 00405D8B 29F0 sub eax, esi ........ 00405DB6 3D D0070000 cmp eax, 7D0 ........ 00405DCD ^\0F8F 8BF7FFFF jg 0040555E ;如果单步调试的话需要将这一句nop掉,如果直接F4过来没有在两个GetTickCount中间的代码停顿过的话可不作处理 6 RaiseException ........ 00405E55 C705 F9524000 3>mov dword ptr [4052F9], 30 ;后面RaiseException会循环0x30次 ........ 00405FB4 FF15 EA304000 call dword ptr [<&KERNEL32.RaiseException>] ; KERNEL32.RaiseException 堆栈: 0006FF40 40010007 |ExceptionCode = 40010007 0006FF44 00000000 |ExceptionFlags = EXCEPTION_CONTINUABLE 0006FF48 00000002 |nArguments = 2 0006FF4C 00401000 \pArguments = Bustme4.00401000 0006FF50 0006FFE0 指向下一个 SEH 记录的指针 0006FF54 004060BC SE处理程序 很明显程序在这里人为地制造了一处错误。如果要想程序正常运行,必须要跳到SE处理程序入口处004060BC。 我们在004060BC处下断,不断地Shift+F9,可最终程序也没有停在我们想断下来的004060BC处,而是弹出一个窗口告诉我们某某地址内存不能为"read",然后就Game Over了。 这里不知道有没有什么好的办法处理通过,我用的是将ExceptionCode改为C0123333,然后通过。 Script: asm 00405E55,"mov dword ptr[4052F9],1" asm 00405F9B,"push 0C0123333" ........ 00406227 FF15 12314000 call dword ptr [<&KERNEL32.GetTickCount>] ; KERNEL32.GetTickCount ........ 00406286 C705 F9524000 3>mov dword ptr [4052F9], 30 ........ 0040640B FF15 EA304000 call dword ptr [<&KERNEL32.RaiseException>] ; KERNEL32.RaiseException 堆栈: 0006FF40 40010007 |ExceptionCode = 40010007 0006FF44 00000000 |ExceptionFlags = EXCEPTION_CONTINUABLE 0006FF48 00000002 |nArguments = 2 0006FF4C 00401000 \pArguments = Bustme4.00401000 0006FF50 0006FFE0 指向下一个 SEH 记录的指针 0006FF54 00406508 SE处理程序 按上面相同的方法处理通过 Script: asm 00406286,"mov dword ptr [4052F9],1" asm 004063E7,"push 0C0123333" 0040667D FF15 12314000 call dword ptr [<&KERNEL32.GetTickCount>] ; KERNEL32.GetTickCount 004066B2 3D 88130000 cmp eax, 1388 004066D5 /0F8F 84050000 jg 00406C5F ;同上处理 7 GetWindowThreadProcessId 检测调试器 ....... 00750000 58 pop eax 00750001 3D 5FAF2F9F cmp eax, 9F2FAF5F 00750006 - 75 FE jnz short 00750006 00750008 E8 11000000 call 0075001E ........ 007513D2 FF10 call dword ptr [eax] ; USER32.GetForegroundWindow ........ 0075145D FF10 call dword ptr [eax] ; USER32.GetWindowThreadProcessId 堆栈: 0006FFB4 0075145F /CALL 到 GetWindowThreadProcessId 来自 0075145D 0006FFB8 003C0690 |hWnd = 003C0690 ('OllyICE - Bustme4.exe',class='pediy06',wndproc=03503168) 0006FFBC 004052ED \pProcessID = Bustme4.004052ED 这里取得创建当前窗口的进程ID,并保存在004052ED处。由于我们正在用OD调试程序,所以这里取得是OD的进程ID。 0075151D FF10 call dword ptr [eax] ; KERNEL32.OpenProcess 0075162F FF10 call dword ptr [eax] ; KERNEL32.ReadProcessMemory 007516B5 FF10 call dword ptr [eax] ; KERNEL32.CloseHandle 00751728 FF15 C6304000 call dword ptr [<&KERNEL32.lstrcmp>] ; KERNEL32.lstrcmpA 00751760 83F8 00 cmp eax, 0 00751778 /75 2F jnz short 007517A9 ;这里必须跳,否则紧随其后的代码会将004052EC的值减1 这里程序设置了一个暗桩,如果检测到被调试,则最终004052EC值为1,否则004052EC值为2。检测后并不立即异常,而是在后面有对004052EC寄存的值检测的代码,如果检测到是1则会使程序运行不正常而退出。 要跳过这里程序的检测,可以按照通用的方法在调用GetWindowThreadProcessID后立即将004052ED处的值改为Explorer.exe的进程ID, 也可以修改上面的jnz代码为jmp。 Script: asm 00751778,"jmp 007517A9" 8 OutputDebugStringA 检测调试器 00751851 FF15 0E314000 call dword ptr [<&KERNEL32.OutputDebugStringA>] ; KERNEL32.OutputDebugStringA 这里还有一处检测,由于我用的是OllyICE,己经针对OutputDebugStringA作了修改了,此处顺利通过。 9 Anti_Dump 00751287 FF15 E2304000 call dword ptr [<&KERNEL32.VirtualPro>; KERNEL32.VirtualProtect 堆栈: 0006FF6C 00405000 |Address = Bustme4.00405000 0006FF70 00003A53 |Size = 3A53 (14931.) 0006FF74 00000001 |NewProtect = PAGE_NOACCESS 0006FF78 0006FF7C \pOldProtect = 0006FF7C 这里将00405000段代码保护方式设置成了PAGE_NOACCESS,这样在您DUMP程序的时候就会出错。如果您想Dump程序,就要想办法跳过这段代码或者将NewProtect的值设置成PAGE_READWRITE之类的。 之后程序就没有什么Anti了,程序又VirtualAlloc若干段内存,并对00401000段进行解码,解码完之后会来到00401014,然后进入0076000调用DialogBoxParamA载入程序界面,并设定DlgProc地址在00401040。 给个跑到00401014处的完整Script: ************************************************** asm 00409047,"jmp 00409053" esto asm 0040918D,"mov byte ptr[4094E1],0" go 40919a mov eax,00409452 asm 004091B0,"jmp 00405325" asm 00405953,"jmp 00405A09" go 00405A09 asm 00405E55,"mov dword ptr[4052F9],1" asm 00405F9B,"push 0C0123333" esto asm 00406286,"mov dword ptr [4052F9],1" asm 004063E7,"push 0C0123333" esto asm 00751778,"jmp 007517A9" go 00401014 ret ************************************************** 注意您有可能需要修改脚本中的相应地址,并且在运行脚本之前请清除所有的断点,调试选项中选择忽略所有异常。
|