第一次分析程序保护技术,花了不少的时间,分析的也不是很透彻,拿出来请大家发现分析过程中的问题。整理的时间仓促,可能有疏漏,大家还是挑重点的看。
谢谢和我一起进行分析工作的vcpestudy,在分析困顿之时,他提出一些很好的想法。
破除软件保护机制基本可分为三个步骤:定位保护代码、分析保护机制和破除保护机制。三个过程同等重要,但是对于不同的软件的保护机制不同,三个步骤分析难度也各不相同。
在除去程序外壳后,经常会出现文件自校验的错误。下面结合一个外挂程序,分析软件是如何进行自校验保护,也为以后的分析工作提供思路。
如何去除文件的自校验是这篇文章的目的。
外挂程序采用了UPX加壳,脱壳不难,无论是使用现有的脱壳工具或者手动都可以轻易的去除壳。运行后出现自校验错误提示,表明程序存在自校验。
一、 定位保护代码
1. 逆着MessageBoxA提示而上
一般的思路是通过追溯MessageBoxA弹出的堆栈,一直回溯到导致错误发生的代码位置,当然这样的工作量大,而且有些软件并没有提示的校验出错。
查看堆栈的信息
0012FD44 0045D473 /CALL 到 MessageBoxA 来自 V2.0045D46E
0012FD48 001D0146 |hOwner = 001D0146
0012FD4C 00FD542C |Text = "重新下载"
0012FD50 00FD50AC |Title = "提示"
0012FD54 00000040 \Style = MB_OK|MB_ICONASTERISK|MB_APPLMODAL
0012FD58 0012FDD0 指向下一个 SEH 记录的指针
0012FD5C 0045D4DD SE处理程序
……
0012FD78 00000000
0012FD7C 0012FDC0
0012FD80 004027DF 返回到 004027DF 来自0040237C
0012FD84 0012FDB0
0012FD88 00404576 返回到.00404576 来自004027D4
0012FD8C 00000038
0012FD90 00000000
0012FD94 00488509 返回到.00488509 来自00404550
0012FD98 00488511
一般堆栈都会很深,写这些保护代码的人并非“善良”之辈,不要希望在上层函数就可以发现有用的代码。一般可以结合IDA分析出调用函数表,在几个可能的地方预先设下断点,减少工作量。
2. 查找代码中的校验函数
程序保护所使用的校验算法一般会使用成熟的算法,如MD5、SHA、RCR32等,当然除了一些通过比较软件大小是否改变、修改时间的校验方法。
可喜的是这些代码的特征可以被PEID插件KryptoAnalyze的分析得到,在锁定特征代码出现的位置加以分析,就可以基本确定校验代码。
得到如下的结果,
DES算法是用来加密的,这里是用作校验,是无需考虑的。查看出现MD5的代码的位置
UPX1:00483F2B push eax
UPX1:00483F2C push 7
UPX1:00483F2E push 0D76AA478h
UPX1:00483F33 mov eax, ebx
UPX1:00483F35 mov ecx, [edi]
UPX1:00483F37 mov edx, [esi]
UPX1:00483F39 call sub_0_483D8C
UPX1:00483F3E mov eax, [edi]
UPX1:00483F40 push eax
UPX1:00483F41 mov eax, [esp+5Ch+var_3C]
UPX1:00483F45 push eax
UPX1:00483F46 push 0Ch
UPX1:00483F48 push 0E8C7B756h
UPX1:00483F4D mov eax, ebp
UPX1:00483F4F mov ecx, [esi]
UPX1:00483F51 mov edx, [ebx]
UPX1:00483F53 call sub_0_483D8C
看到MD5的轮运算常量,那么这个函数体就可能是MD5的Md5TransForm(),为了证实在函数外层不远处出现MD5Init()
UPX1:00484600 MD5Init proc near ; CODE XREF: MD5+25 p
UPX1:00484600 ; GetFileMD5+25 p
UPX1:00484600 mov dword ptr [eax], 67452301h
UPX1:00484606 mov dword ptr [eax+4], 0EFCDAB89h
UPX1:0048460D mov dword ptr [eax+8], 98BADCFEh
UPX1:00484614 mov dword ptr [eax+0Ch], 10325476h
UPX1:0048461B xor edx, edx
UPX1:0048461D mov [eax+10h], edx
UPX1:00484620 xor edx, edx
UPX1:00484622 mov [eax+14h], edx
UPX1:00484625 add eax, 18h
UPX1:00484628 mov edx, 40h
UPX1:0048462D call sub_0_4074A8
UPX1:00484632 retn
UPX1:00484632 MD5Init endp
那么究竟是拿什么内容来做校验呢,在MD5开始的位置即可寻找到答案,参见如下的代码:
UPX1:004847C1 call MD5Init
UPX1:004847C6 push 0 ; hTemplateFile
UPX1:004847C8 push 8000080h ; dwFlagsAndAttributes
UPX1:004847CD push 3 ; dwCreationDisposition
UPX1:004847CF push 0 ; lpSecurityAttributes
UPX1:004847D1 push 3 ; dwShareMode
UPX1:004847D3 push 80000000h ; dwDesiredAccess
UPX1:004847D8 mov eax, [ebp+var_4]
UPX1:004847DB call sub_0_4049DC
UPX1:004847E0 push eax ; lpFileName
UPX1:004847E1 call CreateFileA_0
……
UPX1:0048485D call GetFileSize_0
UPX1:00484862 mov ecx, eax
UPX1:00484864 lea eax, [ebp+var_6C]
UPX1:00484867 mov edx, [ebp+lpBaseAddress]
UPX1:0048486A call Md5Updata
这是代码的一部分,当然程序可能会对几个文件进行校验,也可能分几次对程序校验,但是按照这个思路,寻找整个校验代码应该是可以的。
二、 分析保护机制
软件保护往往会采用几种保护措施,比如定时退出、出错提示等。在确定了保护代码位置后,分析软件是如何进行自我保护。
第一部分:
UPX1:0048D702 mov eax, [ebp+var_20]
UPX1:0048D705 lea edx, [ebp+var_1C]
UPX1:0048D708 call GetFileMD5Value
UPX1:0048D70D mov eax, [ebp+var_1C]
UPX1:0048D710 mov edx, ds:off_0_492F90
UPX1:0048D716 call loc_0_404928
;校验文件的MD5值
UPX1:0048D71B jz short loc_0_48D768
UPX1:0048D71D push 40h
UPX1:0048D71F lea edx, [ebp+var_24]
UPX1:0048D722 mov eax, offset aB0b2c8abcce1cabe_0"
UPX1:0048D727 call sub_0_48843C
UPX1:0048D72C mov eax, [ebp+var_24]
UPX1:0048D72F call sub_0_4049DC
UPX1:0048D734 push eax
UPX1:0048D735 lea edx, [ebp+var_28]
UPX1:0048D738 mov eax, ds:off_0_492F94
UPX1:0048D73D call sub_0_48843C
UPX1:0048D742 mov eax, [ebp+var_28]
UPX1:0048D745 call sub_0_4049DC
UPX1:0048D74A mov edx, eax
UPX1:0048D74C mov eax, ds:off_0_4932B8
UPX1:0048D751 mov eax, [eax]
UPX1:0048D753 pop ecx
UPX1:0048D754 call Error_MessageBox
UPX1:0048D6BE lea eax, [ebp+var_18]
UPX1:0048D6C1 mov ecx, offset dword_0_48D88C
UPX1:0048D6C6 mov edx, ds:dword_0_494DE8
经过跟踪,第一处保护机制如下:
将第一次md5值->ByteToHexChars()->md5->ByteToHexChars()->分组转换->结果。进行结果校验,是否与原来的值相同。
第二部分:
UPX1:0048D7CA lea edx, [ebp+var_2C]
UPX1:0048D7CD mov eax, 17D92h
UPX1:0048D7D2 call sub_0_489DC4
UPX1:0048D7D7 mov eax, [ebp+var_2C]
UPX1:0048D7DA mov edx, offset dword_0_48D930
UPX1:0048D7DF call loc_0_404928 ;strcmp
;对文件的壳代码进行校验
UPX1:0048D7E4 jz short loc_0_48D7F3
UPX1:0048D7E6 mov dl, 1
UPX1:0048D7E8 mov eax, [ebx+34Ch]
UPX1:0048D7EE call sub_0_436104
第二处的保护机制:
校验文件壳代码,如果有修改,则通过设置一个时间信号产生一个WM_QUIT信号结束程序。校验壳的手段比较有意思,不敢恭维。
下面具体分析程序是如何产生的WM_QUIT信号,以及程序是如何处理的
loc_0_404928函数是内联的strcmp函数。函数结束后,通过ZF标志位来判断比较结果。
使用SetTimer设置程序退出信号。见如下的代码
sub_0_436104函数很有意思,
UPX0:00436104 cmp dl, [eax+40h]
UPX0:00436107 jz short locret_0_436111
UPX0:00436109 mov [eax+40h], dl
UPX0:0043610C call sub_0_436078
sub_0_436078函数如下所示:
……
UPX0:004360A1 cmp byte ptr [ebx+40h], 0
UPX0:004360A5 jz short loc_0_4360E1
UPX0:004360A7 cmp word ptr [ebx+3Ah], 0
UPX0:004360AC jz short loc_0_4360E1
UPX0:004360AE push 0 ; lpTimerFunc
UPX0:004360B0 push esi ; uElapse
UPX0:004360B1 push 1 ; nIDEvent
UPX0:004360B3 mov eax, [ebx+34h]
UPX0:004360B6 push eax ; hWnd
UPX0:004360B7 call SetTimer
……
看到SetTimer函数就知道应该知道程序用它来做什么,最容易想到的是函数通过设置一个定时器,每隔一段时间处理某种功能。
本程序中使用利用这个函数,做了两种不同的校验方法:
一种是通过设置WM_TIMER,定时的对配置文件中信息进行校验,如果校验失败,通过PostQuitMessage(),退出程序。
另一种就是上面代码分析的,如果校验结果不成功,通过SetTimer发送一个WM_TIMER消息,程序将在定时器消失后退出。
根据上面的思路,确定消息响应函数
0045CFDF E8 20A3FAFF call <jmp.&USER32.PeekMessageA>
0045CFE4 85C0 test eax, eax
0045CFE6 74 75 je short 0045D05D
0045CFE8 B3 01 mov bl, 1
0045CFEA > 837F 04 12 cmp dword ptr [edi+4], 12
0045CFEE 74 66 je short 0045D056
……
0045D049 E8 06A4FAFF call <jmp.&USER32.TranslateMessage>
0045D04E 57 push edi
0045D04F E8 F89FFAFF call <jmp.&USER32.DispatchMessageA>
0045CFEA 处EDI指向的结构是MSG结构体
typedef struct tagMSG {
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt; }
MSG;
[edi+4]表示的是message,那么0x12消息对应的是WM_QUIT,通过分析,代码中并没有对WM_TIMER(0x113)做处理,统一交由系统DispatchMessageA处理。
这里需要做的工作就是定位不同消息对应的响应函数。
77D1872A 8088 B40F0000 0>or byte ptr [eax+FB4], 1
77D18731 FF55 08 call dword ptr [ebp+8]
在内存区域发现
01110D24 E8 DB F2 FF FF 04 60 43 00 F4 44 FD 00 E8 CE F2 枸? `C.鬌?栉
01110D34 FF FF 04 60 43 00 80 EA FC 00 E8 C1 F2 FF FF 04 `C.€挈.枇?
对应的是处理函数
01110D24 E8 DBF2FFFF call 01110004
01110D29 04 60 add al, 60
01110D2B 43 inc ebx
01110D2C 00F4 add ah, dh
01110D2E 44 inc esp
定位到需要的WM_TIMER消息响应函数在
01110D31 E8 CEF2FFFF call 01110004
01110D36 04 60 add al, 60
01110D38 43 inc ebx
01110D39 0080 EAFC00E8 add byte ptr [eax+E800FCEA], al
01110004 59 pop ecx ; 01110D36
01110005 - E9 123D31FF jmp dump4-9-.00423D1C
00423D1C 55 push ebp
00423D1D 8BEC mov ebp, esp
00423D1F 31C0 xor eax, eax
00423D21 50 push eax
00423D22 FF75 14 push dword ptr [ebp+14]
00423D25 FF75 10 push dword ptr [ebp+10]
00423D28 FF75 0C push dword ptr [ebp+C]
00423D2B 89E2 mov edx, esp
00423D2D 8B41 04 mov eax, dword ptr [ecx+4]
00423D30 FF11 call dword ptr [ecx] ; dump4-9-.00436004
此时ecx指向的相对跳转代码后的是个字节
01110D21 00 00 00 E8 DB F2 FF FF 04 60 43 00 F4 44 FD 00 ...枸? `C.鬌?
01110D31 E8 CE F2 FF FF 04 60 43 00 80 EA FC 00 E8 C1 F2 栉? `C.€挈.枇
01110D41 FF FF 04 60 43 00 38 EA FC 00 E8 B4 F2 FF FF 04 `C.8挈.璐?
01110D51 60 43 00 90 58 FD 00 E8 A7 F2 FF FF 04 60 43 00 `C.怷?瑙? `C.
01110D61 70 6F FD 00 po?铓
0012FE38 /0012FE64
0012FE3C |77D18734 返回到 USER32.77D18734
0012FE40 |000C01AE
0012FE44 |0000C0D4
0012FE48 |00000000
0012FE4C |00000000
0012FE50 |01110D31
0012FE54 |DCBAABCD
0012FE58 |00000000
0012FE5C |0012FEA0
0012FE60 |01110D31
这样,对于每一个TPUtilWindow发出的每一个消息都可以对于到一个函数中,这样建立一个已知的消息响应函数表。每一个消息都可以准确的定位到自己的响应函数。
继续分析对WM_TIMER处理,下面的代码现实了对TPUtilWindow发出的每一个消息进行分发处理。
00436004 55 push ebp
00436005 8BEC mov ebp, esp
00436007 51 push ecx
00436008 53 push ebx
00436009 56 push esi
0043600A 57 push edi
0043600B 8BDA mov ebx, edx
0043600D 8945 FC mov dword ptr [ebp-4], eax
00436010 8B33 mov esi, dword ptr [ebx]
00436012 > 81FE 13010000 cmp esi, 113
00436018 75 3F jnz short 00436059
;如果发送的是WM_TIMER消息,就去执行004039BC,否则跳转
……
0043602F E8 88D9FCFF call 004039BC
……
0043603C EB 33 jmp short 00436071
0043603E ^ E9 5DDCFCFF jmp 00403CA0
……
00436057 EB 18 jmp short 00436071
00436059 8B43 08 mov eax, dword ptr [ebx+8]
……
00436069 E8 AE0FFDFF call <jmp.&USER32.DefWindowProcA>
0043606E 8943 0C mov dword ptr [ebx+C], eax
00436071 5F pop edi
00436072 5E pop esi
00436073 5B pop ebx
00436074 59 pop ecx
00436075 5D pop ebp
00436076 C3 retn
通过上面的分析,可以知道程序通过WM_TIMER消息设置循环校验,也使用WM_TIMER消息发送WM_QUIT消息。并通过上面的代码分发给不同的处理函数。
总结软件的保护:
MD5对目录下文件校验
校验配置文件
未知?
三、 破除保护机制
第一、 破除文件MD5校验
因为校验的值在文件中可以找到,通过重新计算得数值,重写到文件中,到比如下面所示
UPX1:00492F8C off_0_492F8C dd offset MD5_File_A ;
"......."
UPX1:00492F90 off_0_492F90 dd offset MD5_File_B
"......"
修改关键跳转,即是在校验不对的时候也跳往正常的程序流程,不过这样做可能导致意想不到的错误。在自己修正这个程序的自校验时,由于爆破的位置不对,带来了许多问题。
第二、 破除SetTimer消息
1)破除壳代码的校验,不会产生退出的信号
2)设置退出的时间,可能程序运行几天才可以退出,呵呵。。。
3)其它
参考文章
《加密解密 二版》 第五章 软件保护技术及其弱点 时间限制
标准SHA1算法识别技巧cocoruder https://www.xfocus.net/bbs/index.php?act=SE&f=2&t=58652&p=274110
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)