【原创】Themida的另类破解

标 题: 【原创】Themida的另类破解
作 者: wulje
时 间: 2008-09-06,08:43:33
链 接: http://bbs.pediy.com/showthread.php?t=72152

【文章标题】: Themida的另类破解
【下载地址】: 自己搜索下载
【作者声明】: 只是感兴趣,没有其他目的。失误之处敬请诸位大侠赐教!
——————————————————————————–
【详细过程】
大家都知道Themida的强大保护功能,几乎无人能完整的破解它的VM中的handler(最新进展如何?)。我也试图跟踪过它的几个 handler,它为一个简单的入栈操作竟有长达数千行的垃圾代码和无穷的转跳,如果要把它168(?)个handler都清理出来,他一定会疯掉!或累死掉?我曾经下决心不碰Themida了,不知为什么鬼使神差的我又跟踪了几个Themida加密软件,竟然还有新的发现!
附件提供的一个程序是用汇编写的PE信息小工具,用Themida1855加密,加密选择RISC-64 processor,全选加密可选项,并对部分API函数使用VM加密,入口虚拟系数选择15。应该说对该程序用了较强的加密措施。现在介绍用我的脚本,一步步对它破解(绝不暗中使用对原程序已知的信息),居然还原了所有的代码,请看下面详细过程。

一、介绍一个万能脚本
1.下面的脚本适用于任何用Themida 1.8.5.5版加密的程序,操作十分简单。
————————————————————–
//本脚本适用于Thmida 1.8.5.5版本加密的任何程序

bphwc
bc
jmp Second //第一次运行后,将本行注释掉后保存一次(本)脚本

data:
var mem
var mem1
var temIAT
var temESI
var temAPI
var APIstr
var Addr_0
var Addr_1
var Addr_2
var Addr_3
var Addr_4
var Addr_5
var Rva
Init:
mov Rva,65b970 //用对话框中的值修改该值,并将第一行的jmp Second注释掉
mov Addr_0,Rva-997
mov Addr_1,Rva-809
mov Addr_2,Rva-ea
mov Addr_3,Rva-9d
mov Addr_4,Rva
mov Addr_5,Rva+74c

start:
esto
esto
bphws Addr_0,"x"
esto
bphwc Addr_0
mov [Addr_0],0062e990 //废除监视断点的代码

bp Addr_1
bp Addr_2
bp Addr_3
bp Addr_4
bp Addr_5

First:
run
cmp eip,Addr_1
jz A1
cmp eip,Addr_2
jz A2
cmp eip,Addr_3
jz A3
cmp eip,Addr_4
jz A4
cmp eip,Addr_5
jz A5
jmp First

A1:
mov temAPI,eax //eax 是API函数地址
gn temAPI //显示函数名
log $RESULT
add APIstr," "
add APIstr,$RESULT_2 //恢复API函数名(INT表)
jmp First
A2:
mov temIAT,eax
cmp edx,10000
ja next
xor edx,80000000
mov [eax],edx //修复IAT表(写入序列号)
jmp First
next:
mov [eax],temAPI
jmp First

A3:
mov mem1,eax //eax是内存中呼叫API地址
mov temESI,[esi] //获取转跳标记
jmp First

A4:
cmp temESI,AAAAAAAA
jnz step3
mov [mem1],#FF25# //修复代码中转跳地址
mov [mem1+2],temIAT
jmp First

step3:
mov [mem1],#FF15# //修复代码中呼叫地址
mov [mem1+2],temIAT
jmp First

A5:
bc
pause

Second: //查找写IAT地址的一条指令
var memory
var tmp
var temp
var tmpbp
var Str

gmi eip,CODEBASE
mov memory,$RESULT
find memory,#0000000000000000000000000000000000000000000000#
mov tmp,$RESULT
bphws tmp,"w"
esto
esto
esto
bphwc tmp
sto
find memory,#909090909090909090909090909090909090#
mov temp,$RESULT+2
bphws temp,"w"
mov tmpbp,eip
run
cmp eip,tmpbp+2
jnz next2
run
next2:
itoa eip
mov Str,"请用下列数值:"
add Str,$RESULT
add Str," 修改脚本中的Rva."
bphwc
msg Str
pause //(脚本完)

本脚本的使用方法是:用OD打开程序后,如果第一次加载本脚本,则会弹出一个对话框,用对话框中的数据修改脚本中的Rva值(第20行,且只有一处),并一定将第3行的jmp Second注释掉,一定将脚本保存一次!这时脚本就是该程序的专用脚本了。重新用OD加载程序后,可以无须任何更改,直接调用本脚本了!
第2次运行脚本,屏幕快速闪动,一会就出现了神奇的效果了。如果想应用于新的程序,则只须将注释掉的jmp Second恢复即可。
—————————————————————————-

2.挖掉Themida的一只监视眼,蒙上它的另一只眼
本脚本的运行原理,请参考我原先的一篇文章《对Themida加密VC++程序的完美脱壳》,当时我初次接触Themida,那个脚本有很多不完善的地方,如不容易进行到底,移植困难等。Themida有很多监视断点的SEH,当时没有采用相应措施,所以被Themida频频发现。它的“监视”方法很多,用得最多的大约有(1)int 1中断,(2)内存访问异常中断,(3)代码扫描等。对于(1)(2)没有任何办法摘除,但脚本的一个循环可与它周旋;对于(3)在我跟踪的代码段中,用脚本将它摘除了。所以脚本运行得非常顺利(使用在其它被Themida加密的程序中也一样)。
(1)脚本中的 mov [Addr_0],0062e990 就是将该地址的“jb”改成了“jmp”,跳过了代码扫描监视。—相当于挖掉了它的一只眼睛—
(2)Thmida的另一(多)只眼睛实在难于发现,不管你是什么类型的断点,很快被发现并单步异常后弹出对话框,然后退出运行。请注意,Themida设置的是单步中断,如果让它自行处理这个中断,就是前面的结果。如果将它拦截下来,自已处理就可以绕过了它的SEH。“拦截”方法就是打开OD“选项–调试设置–异常页”只选“非法访问内存”一项,其余全部不能选取。这样除非法访问内存异常让它自行处理外,其余全部交由OD处理。这样当Themida发现断点后产生的单步异常就被OD拦截了。自行处理的方式就是脚本中“First:”标签部分。(用脚本处理OD拦截下来的中断是非常容易的)
脚本中的First部分(断点循环)就是判断中断地址是不是自己设置的,是则进入相应操作;若不是,则一定是Thmida发现断点后的 SEH中断地址(单步中断),处理方式就是用“run“。OD的这个run,就是让程序在eip中断地址上继续运行,绕过了Themida的SEH处理程序(陷阱)。—相当于蒙上了它的另一只眼睛—-
脚本中的其余部分无须多说,请参考我的前一篇文章。

3.运行脚本后的神奇效果
用OD打开程序后,加载这个脚本,程序运行得很快。当它暂停后Themida外壳已经被除掉。转到代码段401000,让OD分析代码一次。如果Themida在code段中没有虚拟API函数,则显示在你面前的是一个非常清晰的可读的反汇编程序,各个API函数调用和名称显露无遗。如果你仅仅是想了解原程序的基本结构和思路,这就够了。如果Themida虚拟了部分API,请往下看。如果你一定要dump出来,并去掉垃圾代码,也请往下看。

二、恢复程序的入口代码
如果想进入它的VM(虚拟机)部分,清理它的handler,还原出原代码,我是绝对无能为力了,下决心不碰它了。
那么,我是怎样恢复入口代码的呢?跟踪Themida发现,它对解压出来的代码段并不重视,居然没有设置断点监视?你可以任意设断和修改,我巧妙地利用了它这一弱点。

1.设断技巧(以提供的PeInfo_them.exe为例)
用脚本脱壳后,转到401000代码区,让OD对代码分析一次,来到程序的尾部,API函数都集中在这里(称它为API表)(VC++程序的函数表分散在代码中,相对集中)。

004015FC $- FF25 5820400>jmp dword ptr [402058] ; USER32.wsprintfA
00401602 .- FF25 5420400>jmp dword ptr [402054] ; USER32.DialogBoxParamA jmp dword ptr [402050] ; USER32.EndDialog
0040160E $- FF25 4C20400>jmp dword ptr [40204C] ; USER32.GetDlgItem
00401614 .- FF25 4820400>jmp dword ptr [402048] ; USER32.GetWindowTextLengthA jmp dword ptr [402044] ; USER32.LoadIconA
00401620 .- FF25 4020400>jmp dword ptr [402040] ; USER32.MessageBoxA jmp dword ptr [40203C] ; USER32.SendMessageA jmp dword ptr [402038] ; USER32.SetWindowTextA
00401632 $- FF25 2420400>jmp dword ptr [402024] ; kernel32.CloseHandle
00401638 $- FF25 2820400>jmp dword ptr [402028] ; kernel32.CreateFileA
0040163E $- FF25 2C20400>jmp dword ptr [40202C] ; kernel32.CreateFileMappingA
00401644 .- FF25 3020400>jmp dword ptr [402030] ; kernel32.ExitProcess jmp dword ptr [402020] ; kernel32.FreeLibrary jmp dword ptr [40201C] ; kernel32.GetFileSize
00401656 .- FF25 1820400>jmp dword ptr [402018] ; kernel32.GetModuleHandleA jmp dword ptr [402014] ; kernel32.LoadLibraryA jmp dword ptr [402010] ; kernel32.MapViewOfFile
00401668 $- FF25 0C20400>jmp dword ptr [40200C] ; kernel32.UnmapViewOfFile
0040166E $- FF25 0820400>jmp dword ptr [402008] ; kernel32.lstrcpyA
00401674 $- FF25 0020400>jmp dword ptr [402000] ; comdlg32.GetOpenFileNameA

(1)注意到API表中有“$”的地方(如果全都没有,则再分析一次),$表示程序中有call在呼叫,若没有则表示没有关联(VC++编写的程序有很多无关联的垃圾函数)。在这里没有关联的函数肯定是被Themida虚拟了call代码。在没有“$”的地址上设F2中断。
明眼一看,本例是一个对话框窗口,它只有DialogBoxParamA,没有CreateWindowEx、消息循环等函数。若是Window窗口,则不能在消息循环函数上设断,否则OD将进入假死状态。
(2)在解压后代码段中浏览,发现了一段连续的乱码,用OD分析也无用,它一般就是入口代码段了。本例是4015B6–4015FB这一段(被虚拟了)。
(3)从401000开始寻找跳进006xxxxx段的jmp,这些jmp都是跳到被VM虚拟后的API代码中(代码E9前面都有一个“-”号),本例中跳进VM中的8个jmp地址如下:

0040100D .- E9 E0532300 jmp 006363F2
00401029 .- E9 408F2300 jmp 00639F6E
0040103E .- E9 95CC2300 jmp 0063DCD8
00401371 .- E9 4A092400 jmp 00641CC0
00401385 .- E9 F0422400 jmp 0064567A
004013C9 .- E9 B2802400 jmp 00649480
004013DD .- E9 61B52400 jmp 0064C943
0040151B .- E9 39F32400 jmp 00650859
在以上的每个地址都用F2设断。(放心设断,Themida是不来过问的。)

2.操作技巧(以后的操作基本上都是F9或alt-F9,无须shift-F9)
(1)当设置完成后,按F9运行一次,程序中断在
0040165C .- FF25 1420400>jmp dword ptr [402014] ; kernel32.LoadLibraryA
且在堆栈中出现
0013FF90 00654697 /CALL 到 LoadLibraryA
0013FF94 00402060 \FileName = “RichEd20.dll"
0013FF98 004001C0 ASCII " " <–这是应用程序开始的栈顶,dump堆栈平衡时弹出至此
0013FF9C 0013FFE0 指向下一个 SEH 记录的指针
0013FFA0 005E0DB2 SE处理程序
0013FFA4 7C930738 ntdll.7C930738
堆栈中0013FF98是进入应用程序后的栈顶,13FF94的(00402060)是压入的参数,0013FF90的(00654697)是VM中的呼叫地址。
记录下堆栈中的数据,并将4015B6开始的乱码改写为:(它就是解密出来的入口第一组代码)
push 00402060
call 0040165C jmp dword ptr [402018] ; kernel32.GetModuleHandleA

堆栈中出现
0013FF90 006546A3 /CALL 到 GetModuleHandleA
0013FF94 00000000 \pModule = NULL
这里记下堆栈,接着前面如下修改乱码;
push 0
call 00401656 jmp dword ptr [402054] ; USER32.DialogBoxParamA
堆栈是
0013FF80 006546BB /CALL 到 DialogBoxParamA
0013FF84 00400000 |hInst = 00400000 <–GetModuleHandleA的返回值
0013FF88 000003E8 |pTemplate = 3E8 <–资源ID
0013FF8C 00000000 |hOwner = NULL
0013FF90 00401544 |DlgProc = PeInfo_t.00401544 <–窗口程序地址
0013FF94 00000000 \lParam = NULL

因为程序进入了DialogBoxParamA后,不会退出,但初始化窗口时还会调用其它函数,只有关闭窗口后才退出。
这里将前面3次中断后堆栈中的值,加上必须保存的句柄,将入口处的乱码改写如下:
push 402060
call 0040165C ;LoadLibraryA
mov [xxxxx],eax <—返回RichEdit20A句柄
push 0
call 00401656 ;GetModuleHandleA
mov [xxxxx],eax <—返回GetModuleHandleA句柄
push 0
push 401544
push 0
push 3E8
push [xxxxx] <—它一定是GetModuleHandleA返回的句柄
call 00401602 ;DialogBoxParamA

打开403000全局变量区,每中断一次就会写入一些数据。你会发现LoadLibraryA句柄存入了[403004],GetModuleHandleA句柄存入了[403000],这样前面的三个[xxxxx]都解决了(需要一些API函数知识):即

push 402060 <—查402060是字串RichEd20.dll,原来是装载RichEd20库
call 0040165C ;call LoadLibraryA
mov [403004],eax <—返回RichEdit20A句柄
push 0
call 00401656 ;call GetModuleHandleA
mov [403000],eax <—返回GetModuleHandleA句柄
push 0
push 401544 <–DialogBoxParamA函数的Proc地址
push 0
push 3E8 <–窗口资源ID
push [403000] jmp dword ptr [40203C] ; USER32.SendMessageA
堆栈中是
0013FC14 00641CD7 /CALL 到 SendMessageA
0013FC18 004600B2 |hWnd = 4600B2
0013FC1C 00000080 |Message = WM_SETICON
0013FC20 00000001 |wParam = 1
0013FC24 0C1A027D \lParam = C1A027D
原来401371加密前应该是call 401625(即call SendMessageA),查看地址401371,参数非常清晰,你不必费力去研究它的参数来源,它是装载图标。
注意,这个call SendMessageA不是接在开局的call DialogBoxParamA后面,因为程序还没有退出DialogBoxParamA。
记下00401371地址,将要修改为call 401625(参数修复前不要修改,可能程序会反复调用它)。
……………………………………
这样反复F9,反复记下中断地址和中断函数,(每中断一个jmp的地址,就关闭它的断点(API表中的F2不能关闭)。这样凡是与初始化有关的被虚拟的API都现身了!若发现再F9,老在API表中的SendMessageA(或其它函数)上中断,这是初始化中的调用,运行时总要进入系统领空,可按alt-F9让其返回(在VM中),再F9回到用户领空。窗口未出现前可以先关闭SendMessageA的F2断点,但窗口出现后一定要恢复其断点。

(5)如果F9后,OD没有反应,但状态栏提示读取[FFFFFFFF]异常,不管它,这时,窗口已经出现了(看桌面状态条),程序进入了DialogBoxParamA中的消息循环。
到此为止,代码段中还有5个断点没有被访问,它们是窗口运行后才要访问的地址。

(6)按普通程序一样运行窗口,让它随便打开一个.exe文件,它立即中断在某jmp虚拟函数上,按前面同样的方法操作,在API函数表中就发现了它调用的API是谁了,这样就一个个地把Themida虚拟的函数清理完了。当代码段中的F2中断地址关闭完后。被VM虚拟的全部函数都现身了。(如何恢复参数,后面再说)

(7)反复运行窗口发现在代码中设置的断点0040151B9(jmp 00650859)始终没有被调用,而API表中的 MessageBoxA也没有被调用,可以肯定0040151B9(jmp 00650859)是虚拟了call MessageBoxA。原来它是一个出错对话框,当你打开一个非exe文件时,对话框就出现了。在堆栈中也可以找到它全部参数。

(8)入口的后半部
当清理完代码段中的所有jmp断点后(被虚拟的API),可以关闭窗口了,按下关闭钮,立即中断在
0040164A .- FF25 2020400>jmp dword ptr [402020] ; kernel32.FreeLibrary
堆栈是
0013FF90 006546CA /CALL 到 FreeLibrary
0013FF94 74D90000 \hLibModule = 74D90000 0040100D .- E9 E0532300 jmp 006363F2 00401029 .- E9 408F2300 jmp 00639F6E 0040103E .- E9 95CC2300 jmp 0063DCD8 <–应该是call SendMessageA
00401043 . 46 inc esi
00401044 . 6F outs dx, dword ptr es:[edi]
00401045 . C2 0400 retn 4

(1)第一个call GetWindowTextLengthA,参数只有一个push [40300C],前面已知这是RichEdit20A的句柄,显然它是获取编辑软件RichEdit20A中的字符数,结果应该暂存,eax怎样操作?
(2)第2个是call SendMessageA,消息发往RichEdit20A,堆栈中的表示是:
0013F798 00639F81 /CALL 到 SendMessageA
0013F79C 00450292 |hWnd = 450292
0013F7A0 00000437 |Message = MSG(437)
0013F7A4 00000000 |wParam = 0
0013F7A8 0013F7CC \lParam = 13F7CC <–移动光标或选择范围的操作是该参数指向一个CHARRANGE结构(8字节)

这下难了,必须弄清RichEdit20A是干什么的?原来它是一个大型文本编辑软件,操作十分复杂,这里是大才小用,只用它来显示数据,并不对数据编辑。对RichEdit20A的操作主要是靠SendMessageA发送消息。它操作前,一定要确定光标位,确定光标位消息中的 lParam参数指向一个CHARRANGE结构(2个dword),这两个值相等,其值是光标位置;若不等则表示选择一个区间。
清楚了,GetWindowTextLengthA获取屏幕上字符长度,第1个SendMessageA应该将光标定于尾部,好让第2个 SendMessageA向它添加字符。那么第1个SendMessageA的参数13F7CC就一定是指向CHARRANGE结构,从ebp的值知 13F7CC是指向[ebp-8],[ebp-4]即一个CHARRANGE结构,两个dword值应该相等且就是当前的字串长度。
这样,显然应该有:mov [ebp-8],eax;mov [ebp-4],eax。所以,代码应该如下:
call GetWindowTextLengthA
mov [ebp-8],eax
mov [ebp-4],eax
lea eax,[ebp-8]
push eax <—取CHARRANGE结构地址
push 0
push 437
push [40300C]
call SendMessageA

第2个SendMessageA一定是将buffer中的字串添加上去。它的参数lParam应该指向一个buffer。堆栈中参数是
0013F798 0063DCE9 /CALL 到 SendMessageA
0013F79C 00E20202 |hWnd = 450292
0013F7A0 000000C2 |Message = EM_REPLACESEL <– EM_REPLACESEL=C2
0013F7A4 00000000 |wParam = 0
0013F7A8 0040222F \Text = "————————————–
MSG消息EM_REPLACESEL也表示向RichEdit20A添加字符,参数lParam指向40222F,是直接push 40222F还是由调用程序带来?查调用401000的呼叫,知道它有多个地方调用,它的参数一定得由调用程序带来。
这样,接着的代码是
push [ebp+8] <–唯一的带来参数
push 0
push 0C2 <– 0C2=EM_REPLACESEL
push [40300C]
call SendMessageA

(4)SendMessageA没有返回值,最后还有乱码46 6f的原码是什么?观察程序,前面有pushad,后面一定要有popad,又汇编程序是堆栈自动平衡方式,最后一般都有leave,最后这个C2 0400 (retn 4)是否是原码呢?应该说是,Themida一般不去惹 call ebx、jmp eax、retn等代码,因为它的转跳是不定的,themida操作起来很复杂。
至此,这段代码被还原了。有的地方说法可能很勉强(因为有原码参考,所以我说得很肯定),用在其它地方可能就不正确了。不管怎么说,分析起来虽然很困难,但肯定比从VM虚拟代码中还原要容易得多了。
本方法有个致命的弱点是,一些与被虚拟的函数无关的代码,如add eax,6,mov ecx,edi等是永远找不回来的。好在是已经编译过的.exe文件再用Themida加密,如果不提供原编译文件,则只能对API虚拟了,包含它附近的几个代码。有关API的参数代码相对简单一些,很少有前面说到的那些代码形式,退后一步说,即使有,且被漏掉了,但所有的API都被还原了,关系清楚了,虽然dump后的程序不能运行,但破解的欲望达到了!

四、可能出现的问题
1.如果你用OD打开文件后加载脚本,OD象没有反应一样直接就运行到了窗口出现。
结论:你可能是第1次将脚本用到其它程序上而又忘记了将脚本中的jmp Second恢复过来,或者你忘记了存盘。
2.用脚本第1次弹出对话框没问题,第2次按要求修改也存了盘,但就是不出现任何结果。
结论:Themida版本可能不是1.8.5.5版。或者“调试设置–异常页”的选项选得太多,特别是选择了“忽略特权指令或无效指令”。
3.脚本不能运行到底,Themida弹出警告框后退出。
结论:“调试设置–异常页”的选项选得太多,特别是选择了“忽略单步异常”。

我说话很啰索,谢谢你耐心读完。

————————————————————————–
【版权声明】: 本文原创于看雪技术论坛, 转载请注明作者并保持文章的完整, 谢谢!

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 變更 )

Twitter picture

You are commenting using your Twitter account. Log Out / 變更 )

Facebook照片

You are commenting using your Facebook account. Log Out / 變更 )

Google+ photo

You are commenting using your Google+ account. Log Out / 變更 )

連結到 %s


%d 位部落客按了讚: