【文章标题】: 一个奇怪的带壳crackme的简单分析
【文章作者】: hawking
【作者邮箱】: rich_hawking@hotmail.com
【软件名称】: Bustme4
【软件大小】: 21.5k
【下载地址】: http://bbs1.pediy.com:8081/attachment.php?s=&attachmentid=3623
【加壳方式】: 不详
【保护方式】: 未知
【使用工具】: PEiD OLLYICE
【操作平台】: win2k
【作者声明】: 只是感兴趣,没有其他目的。失误之处敬请诸位大侠赐教!
--------------------------------------------------------------------------------
【详细过程】
首先声明,本人菜菜,分析水平有限,本文只是自己破解过程的一个记录,供和我一样的菜鸟参考,也备自己日后遇到类似问题时查找。侠之大者可以直接漂过。如果肯指点一二,感激不尽。
近来坛子里加壳与脱壳版里一个奇怪的带壳crackme讨论的人很多,不少大牛都信手给出了key,成了创意比拼。我等小菜,载入OD里还没开始跑就GameOver了。实在是心有不甘。
发扬一下cracker的风格,自己动手搞定它。感谢skylly,我一开始茫无头绪时,是你的提示给了我前进的方向。
(一)Anti (Validate部分参见11楼)
首先用PDiD查一下壳,显示Nothing found * 点击EP Section后面的按钮可以看出有6个段,段名都是$ 看来好像是加壳了。
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
**************************************************
注意您有可能需要修改脚本中的相应地址,并且在运行脚本之前请清除所有的断点,调试选项中选择忽略所有异常。
--------------------------------------------------------------------------------
【经验总结】
这个壳用
1 tlscallback+CC保护入口
2 进程入口参数校验
3
SetUnhandledExceptionFilter
4 CreateThead 线程保护 (个人感觉好像这里CreateThread 没什么Anti的作用,不知道分析的对不对,请高手指正 )
5
GetTickCount 时间校验
6
RaiseException (程序正常运行的时候不知道它是怎么通过这里的,请高手说说)
7
GetWindowThreadProcessId 检测调试器
8 OutputDebugStringA 检测调试器
9 Anti_Dump 设置某些代码段为PAGE_NOACCESS防止Dump
--------------------------------------------------------------------------------
【版权声明】: 本文原创于看雪技术论坛, 转载请注明作者并保持文章的完整, 谢谢!
2006年12月24日 11:28:51
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课