版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。
--------------------------------------------------------------------------------
【详细过程】
我经过探索实现了对Themida(1.8.5.5版)加密VC++程序的完美脱壳.如果文中的方法只适用于这个特例,则本文就毫无价值....,我把我的方法提供给大家研究,对象是想升级的菜鸟们,请高手不要嘲笑我对脚本或程序的详细注释.
一.脱壳思路
Themida是一款强大的加密加壳工具,特别是1.8.5.5版,它破坏了程序的入口代码并用一段壳代码取代了它,根本模糊了OEP界线.要么脱壳不完全,要么进不了程序代码(菜鸟的肤浅认识).但Themida也暴露出了它自身的弱点:
1.它对入口代码的破坏一般只有几十个字节,对一些固定模式的入口程序,如VC++,VB,Delphi等入口的发现和修复还是比较容易的.Themida对它不熟习的入口代码则脱壳后原样不变!这时,OEP就清晰了.
2.和所有的加壳软件一样,Themida也只能在现场获取API地址,经加密后再写入程序代码中.正确的API地址一定会在某个寄存器中瞬间出现,这就暴露了它加密的蛛丝马迹,捕获在themida代码中出现API地址的代码段就成为了脱壳的突破口.
3.任何程序运行完成后都会"返回到kernel32.7C816FD"(菜鸟的肤浅认识).因此当堆栈顶是"返回到kernel32.7C816FD"时,是从壳代码进入程序代码的重要标志.
我就是抓住以上三点作为突破口实现了对Themida(1.8.5.5)版的完美脱壳.
二.脱壳脚本的说明(测试程序和脚本见附件)
完整的脱壳脚本:(ODbgScript1.65.1.0版OllyDBG1.0.10.0版)
data:
varmem
varmem1
vartemIAT
vartemESI
vartemAPI
varAPIstr
bphwc
start:
esto
esto
bphws65d270,"w"
reset:
esto
cmp[65d270],5ec103ad//判断加密段代码是否出现
jnzreset
bphwc65d270
bphws65d992,"x"
step:
esto//断点65d992
bphwc
cmpeip,65d273//以API函数名装载方式则跳
jzstep6
cmpeip,65e1c8//准备退出
jzstep5
//----以序列号方式装载则进入-------
oredx,80000000
mov[eax],edx//修复IAT表(写入序列号)
movtemIAT,eax//保存IAT地址
bphws65d9a0,"x"
step2:
esto//断点65d9a0
bphwc
cmpeax,FFFFFFFF//判断当前函数是否加密新的地址
jnznext
bphws65d992,"x"
bphws65d273,"x"
bphws65e1c8,"x"
jmpstep
next:
bphws65d9d2,"x"
esto//断点65d9d2
bphwc
movmem1,eax//eax是内存中呼叫API的地址
movtemESI,[esi]//获取转跳标记
bphws65da7c,"x"
esto//断点65da7c
bphwc65da7c
cmptemESI,AAAAAAAA
jnzstep3
mov[mem1],#FF25#//修复代码中转跳地址
mov[mem1+2],temIAT
bphws65d9a0,"x"
jmpstep2
step3:
mov[mem1],#FF15#//修复代码中呼叫地址
mov[mem1+2],temIAT
bphws65d9a0,"x"
jmpstep2
step5:
bphwc
//---完善INT表,插入库函数名-----
addAPIstr,"MFC42D.dllMFCO42D.dllMSVCRTD.dllKERNEL32.dll"//MFC42D前加4个空格
mov[410000],APIstr
//--写入平衡堆栈,入口地址和跳出壳的代码
mov[65e1cf],#83C44CB8902E400050C3#
//--恢复应用程序入口代码
mov[402e90],#558BEC6AFF687068410068F830400064A100000000506489250000000083C4945356578965E8C745FC000000006A02#
sti
sti
sti
sti
sti
pause//成功脱壳,然后Dump,稍加修改PE头后,程序正常运行.
//-----以函数名方式装载的进入---------
step6:
movtemAPI,eax//eax是API函数地址
gntemAPI//显示函数名
log$RESULT_2
addAPIstr,""//两个空格
addAPIstr,$RESULT_2//恢复API函数名表(INT表)
bphws65d992,"x"
esto
bphwc//断点65d992
movtemIAT,eax//保存IAT地址
bphws65d9a0,"x"
jmpstep2
------------------------------------------------------------------
硬件断点的选择
本脚本涉及了65d270,65d992,65d273,65d9a0,65d9d2,65da7c,65e1c8等七个断点,正确选择这些断点是脱壳的关键.如果这些断点只适用你这个特例,则本文就毫无价值.恰恰Themida对不同程序的加密过程(代码)基本相同,只是地址不同,熟习这些断点处的汇编代码对搜索其它程序中类似的断点提供了参考.
Themida的加密代码段只是在加密时出现,加密前后都是其它用途的代码.下面先说如何捕获加密代码段:
1.加密API函数,IAT地址等的代码段的捕获:
多数软件解压后都在401000开始后的段中运行(菜鸟的肤浅认识),首先了解软件解压后执行代码段的长度,只须先用OD运行一次,在内存段中从401000向下观察,本程序长度到4031E7为止,以下都是无意义的全CC字节.在"4031E7"设置写入中断,再次从头运行到中断后,观察401000---4031E7中,出现了很多连续的"9090"字节.这些就是需要加密的代码处.
然后在连续的9090中任选一个地址,比如4030F0,设置为写入中断,F9运行,中断后就进入了themida的"加密代码段"了.试用F8跟踪运行,你逐渐就会发现themida的加密的过程了.现将加密段部分代码注释如下.
2.硬件断点的选择:(在代码中作了原因的批注,若用F8跟踪几次,就会理解了)
加密过程从这里开始....
0065D0AFC785B10D740900000000movdwordptr[ebp+9740DB1],0
0065D0B9C785112C740900000000movdwordptr[ebp+9742C11],0
0065D0C383BDBDC2850900cmpdwordptr[ebp+985C2BD],0
0065D0CA0F8408000000je0065D0D8
0065D0D08D9D2F628409leaebx,dwordptr[ebp+984622F]
0065D0D6FFD3callebx
0065D0D8FF8545097409incdwordptr[ebp+9740945]
0065D0DE83BD4509740964cmpdwordptr[ebp+9740945],64
0065D0E50F8262000000jb0065D14D
如果API加载是用函数名方式(函数名是不显示的),则进入这里,若是序列号方式则直接转跳到65D274,
而65D273则是获取API原装地址的重要断点.[65D270]处的代码比较简单,若它写入了"AD03C15E",则该段加密代码全部现身了.这就是脚本中写入bphws65d270,"w"...cmp[65d270],5ec103ad的原因.
0065D2538B8505037409moveax,dwordptr[ebp+9740305]
0065D259D1E0shleax,1
0065D25B038519037409addeax,dwordptr[ebp+9740319]
0065D26133F6xoresi,esi
0065D26396xchgeax,esi
0065D26466:ADlodswordptr[esi]
0065D266C1E002shleax,2
0065D26903855D217409addeax,dwordptr[ebp+974215D]
0065D26F96xchgeax,esi
0065D270ADlodsdwordptr[esi];当[65D270]写入了5EC103AD时,加密段代码准备完成----这是我们捕获该段代码的重要方法
0065D27103C1addeax,ecx
0065D2735Epopesi;eax中显示原装API地址-------解密的关键地址之一
0065D27483BDC529740901cmpdwordptr[ebp+97429C5],1;序列号加载时,直接跳到这里
这是一段很长的代码,判断当前的API函数在Themida中是否有现存的加密代码,Themida事先加密了一批很常用的kernel32中的函数.
它对于我们解密意义不大,但用于识别这段代码确很显眼.
0065D364/7507jnzshort0065D36D
0065D366|8B8531187409moveax,dwordptr[ebp+9741831]
0065D36C|47incedi
0065D36D/3B85C5C28509cmpeax,dwordptr[ebp+985C2C5]
0065D3737507jnzshort0065D37C
0065D3758B85AD117409moveax,dwordptr[ebp+97411AD]
0065D37B47incedi
0065D37C3B85CDC28509cmpeax,dwordptr[ebp+985C2CD]
0065D3827507jnzshort0065D38B
0065D3848B852D337409moveax,dwordptr[ebp+974332D]
0065D38A47incedi
0065D38B3B85D1C28509cmpeax,dwordptr[ebp+985C2D1]
0065D3917507jnzshort0065D39A
0065D3938B85992B7409moveax,dwordptr[ebp+9742B99]
0065D39947incedi
0065D39A3B85D5C28509cmpeax,dwordptr[ebp+985C2D5]
0065D3A07507jnzshort0065D3A9
0065D3A28B8501057409moveax,dwordptr[ebp+9740501]
0065D3A847incedi
0065D3A93B85D9C28509cmpeax,dwordptr[ebp+985C2D9]
0065D3AF7507jnzshort0065D3B8
0065D3B18B85C10D7409moveax,dwordptr[ebp+9740DC1]
0065D3B747incedi
0065D3B83B85DDC28509cmpeax,dwordptr[ebp+985C2DD]
0065D3BE7507jnzshort0065D3C7
0065D3C08B8599267409moveax,dwordptr[ebp+9742699]
0065D3C647incedi
0065D3C73B85E1C28509cmpeax,dwordptr[ebp+985C2E1]
这段代码中,65D990处:eax是IAT表中的地址,ecx是API地址(序列方式未加密,函数名方式则已加密)
但Themida犯了一个大错,它把序列号一直保存在edx中,被我拣了个大便宜.所以把它设为断点.
0065D974ADlodsdwordptr[esi]
0065D975C746FC00000000movdwordptr[esi-4],0
0065D97CC1C005roleax,5
0065D97F050FDD1931addeax,3119DD0F
0065D9840385F9067409addeax,dwordptr[ebp+97406F9]
0065D98A8B8DC92E7409movecx,dwordptr[ebp+9742EC9]
0065D9908908movdwordptr[eax],ecx;IAT表地址和序列号的重要断点
0065D992ADlodsdwordptr[esi]
0065D993C746FC00000000movdwordptr[esi-4],0
0065D99A89B501097409movdwordptr[ebp+9740901],esi
0065D9A083F8FFcmpeax,-1;对同一函数是否加密其它地址的标记
执行代码中有很多呼叫API的转跳,Themida绝不放过,65D9D2的eax中出现呼叫处地址.呼叫方式有FF25..(jmp[...])方式和FF15..(call[...])方式,[esi]存放的是区分它的标记,所以它也就设为断点.
0065D9C9C1C003roleax,3
0065D9CC0385F9067409addeax,dwordptr[ebp+97406F9]
0065D9D283BD3D20740901cmpdwordptr[ebp+974203D],1;获取代码是的呼叫地址和方式
0065D9D90F849D000000je0065DA7C
其实在下面这段代码中,什么也不做,但必须在65DA7C中断,等到它加密了呼叫地址后,才把正确的地址写回去.
65DA89是加密完成后的出口,设置断点在65E1C8的原因是运行到该点后,释放了申请的内存.
0065DA708B85C92E7409moveax,dwordptr[ebp+9742EC9]
0065DA762BC7subeax,edi
0065DA7883E804subeax,4
0065DA7BABstosdwordptres:[edi];加密呼叫转移
0065DA7CADlodsdwordptr[esi]
0065DA7DC746FC00000000movdwordptr[esi-4],0
0065DA84^E911FFFFFFjmp0065D99A
0065DA8989B501097409movdwordptr[ebp+9740901],esi;加密工作完成的出口
3.断点设置的技巧
Themida非等闲之辈,它几乎每时每刻都在监视代码中有无断点,一经发现程序就终止了,硬件断点也不例外.因此脚本中断点的设置几乎是用后立即删除,随用随设.我无法解释好好的脚本,有时也过不了(themida警告后退出)!如果你总是过不了脚本,请删除OD的UDD中相关Ctest_themida.exe调试的所有.udd文件后重试,并关闭其它任何打开的文件或程序.
4.关于OEP
Themida1.8.5.5版对OEP(入口地址)的加密已经不是按原样复员入口处的代码了.它破坏了入口代码长达几十行,壳也不从这里进入应用程序.发现它什么时候,从什么地址进入应用程序是个很头晕的事情.就算你通过在堆栈"返回到kernel32.7C816FD"前设置了写入断点,程序到达该点时,依然是在壳的内部.想跟踪到达应用程序的代码中,几乎是不可能的.有几个特例是成功的,那就是用Themida加密前,先将入口代码进行转跳并加入垃圾代码,让Themida不知道是用什么方式编写的软件,它只好原样保留了入口代码.这时用堆栈"返回到kernel32.7C816FD"前设置了写入断点的方法,很容易跟踪到"popadpopfdretn".找到OEP了.
但是用VC++,VB,Delphi等编写的软件,几乎入口都是相类似的代码,分析一个VC++的入口:
0040xxxx>/$55pushebp
0040xxxx|.8BECmovebp,esp
0040xxxx|.6AFFpush-1
0040xxxx|.6870684100push00416870
0040xxxx|.68F8304000push
0040xxxx|.64:A100000000moveax,dwordptrfs:[0]
0040xxxx|.50pusheax
0040xxxx|.64:892500000000movdwordptrfs:[0],esp
0040xxxx|.83C494addesp,-6C
0040xxxx|.53pushebx
0040xxxx|.56pushesi
0040xxxx|.57pushedi
0040xxxx|.8965E8movdwordptr[ebp-18],esp
0040xxxx|.C745FC00000000movdwordptr[ebp-4],0
0040xxxx|.6A02push2
以上部分被破坏,下面随不同程序稍有不同,脚本能正确将它还原.
0040xxxx|.FF15708A4100calldwordptr[]
0040xxxx|.83C404addesp,4
0040xxxx|.C70520794100FFFFFFFFmovdwordptr[417920],-1
0040xxxx|.A120794100moveax,dwordptr[417920]
0040xxxx|.A330794100movdwordptr[417930],eax
0040xxxx|.FF158C8A4100calldwordptr[];相对不变可供搜索
0040xxxx|.8B0D0C794100movecx,dwordptr[41790C];相对不变可供搜索
0040xxxx|.8908movdwordptr[eax],ecx;相对不变可供搜索
0040xxxx|.FF15688A4100calldwordptr[];相对不变可供搜索
0040xxxx|.8B1508794100movedx,dwordptr[417908]
0040xxxx|.8910movdwordptr[eax],edx
0040xxxx|.A1648A4100moveax,dwordptr[]
0040xxxx|.8B08movecx,dwordptr[eax]
0040xxxx|.890D14794100movdwordptr[417914],ecx
0040xxxx|.E8D6010000call004030E0
0040xxxx|.833DD076410000cmpdwordptr[4176D0],0
0040xxxx|.750Ejnzshort00402F21
0040xxxx|.68D0304000push004030D0
0040xxxx|.FF15608A4100calldwordptr[]
0040xxxx|.83C404addesp,4
0040xxxx|>E88A010000call004030B0
0040xxxx|.6814754100push00417514
0040xxxx|.6810744100push00417410
0040xxxx|.E873010000call