zhaohj999 发表于 2015-5-17 10:31

一款商业软件的Themida-2.1.*脱壳

大家好,我是zhaohj999,一直来论坛混,感觉对不起大家。下面我就themida2.1.x的脱壳过程发表一下。
大家知道,themida的核心是esi表(解码表)和ebx表(dllbase),从这两个表分析配合脚本就ok了。
【软件地址】http://pan.baidu.com/share/home?uk=1931954324&view=share#category/type=0
【脱壳目的】只是感兴趣,相互交流
脱壳过程:
一:先找这两个表
我用脚本跑:
bpmc            //删除内存断点
BPHWCALL      //清所有硬件断点
BP 7c809b0a   //在VirtualAlloc返回处下F2断点(bp VirtualAlloc+5)
run                   //执行F9后中断在VirtualAlloc返回处
BC 7c809b0a   //删除F2断点
BPHWS eax+8ef1,"x"   // 在新的VirtualAlloc处下硬件执行断点,这个8ef1就是GetProcAddress的偏移
run             //F9

LOOP:         //不是就断续运行
cmp eax,7C80AE40   //eax是不是GetProcAddress
JE IsGetProcAddress
run
jmp LOOP   //中断在VirtualAlloc,并且eax=GetProcAddress,连续中断两次后,可得到esi和ebx的值
IsGetProcAddress:
log "IsGetProcAddress"
run//中断一次
run//中断第二次,此时刚好填充好esi表和ebx表
BPHWCALL
var tmp
mov tmp,
bp tmp//看堆栈返回地址
run
BC tmp
//上面是TMD2.1.x通用查找esi和ebx表脚本,本程序见下面语句
pause

得到:
00E4A018    8BB5 8D180015   mov esi,dword ptr ss: //ESI表
esi=00E35DEB~00E41EB6
00E35DEB15 59 38 6D 50 8F 60 54 5E 1D 00 60 AF 1D 00 20 //esi表开头
00E35DFB0F 9A 00 C0 FF FF FF FF DD DD DD DD 0F AD 27 DA
...
00E3603BFF FF FF FF DD DD DD DD 0C 34 90 1A BB BB BB BB
00E3604B48 F5 60 F4 FF FF FF FF DD DD DD DD FB 74 19 07
00E3605BBB BB BB BB 49 F5 60 14 81 88 05 C0 AA AA AA AA
00E3606BFF FF FF FF DD DD DD DD 0C 34 20 6D BB BB BB BB
00E3607B49 F5 60 34 82 88 05 80 AA AA AA AA FF FF FF FF
00E3608BDD DD DD DD FB 74 B1 07 BB BB BB BB 49 F5 60 54
00E3609B83 88 05 40 AA AA AA AA FF FF FF FF DD DD DD DD
...
00E41E9BAA AA AA AA FF FF FF FF DD DD DD DD EE EE EE EE      
00E41EABDD DD DD DD 00 00 00 00 00 00 00 00               //以8个0结束esi表

00E4A031    8B9D C52D0015   mov ebx,dword ptr ss: //EBX表
ebx=02F00000
02F0000010000000ADSEC.10000000
02F0000476060000SETUPAPI.76060000
02F0000868BE0000HID.68BE0000
02F0000C73D30000offset MFC42.Ordinal1482
02F0001077BE0000MSVCRT.77BE0000
02F000147C800000kernel32.7C800000
02F0001877D10000USER32.77D10000
02F0001C77EF0000GDI32.77EF0000
02F000207D590000offset SHELL32.Ordinal517
02F0002476990000ole32.76990000
02F0002802FB0000offset OG70AS.Ordinal3998
02F0002C71A20000offset WS2_32.Ordinal496
02F0003000000000
=============================

二:得到这两个表后,开始分析:
esi=00E35DEB~00E41EB6
ebx=02F00000~002F0030
第一类:带0xbbbbbb api的处理
第二类:不带0xbbbbbb api的处理
第三类:加密api 的处理

第一类                  第二类            第三类
带0xbbbbbbbb   不带0xbbbbbb   加密的api
GetApi1                  GetApi2      GetApi2
GetIat1                  GetIat1         GetIat2
FillCall1                     FillCall1          FillCall2

对本程序,esi表第一项是不带0xbbbbbbbb的,也就是第二类

1:对ebx表第一个dword下内存访问断点,断在
00E4A086    FF33            push dword ptr ds:                  ; ADSEC.10000000
00E4A088    812C24 F03C300F sub dword ptr ss:,0xF303CF0
00E4A08F    59            pop ecx
GetBase=00E4A086 寄存器
*******


2:找GetAPi2,对esi表第一个dword下内存访问断点,断在
00E4A9CF    AD            lods dword ptr ds:
F7,来到
00E4A9E0    BF 00000000   mov edi,0x0
00E4A9E5    897E FC         mov dword ptr ds:,edi//edi=0 读完esi表后清0
...
00E4AA15    3D EEEEEEEE   cmp eax,0xEEEEEEEE//比较一大项是否结束
...
00E4AECB    3B02            cmp eax,dword ptr ds:
00E4AECD    0F84 1D010000   je 老虎YJK?00E4AFF0
比较函数名的hash值了,edx是函数名hash表,eax是本次函数名hash值
可以在00E4AFF0按F4,说明找到了api的索引表
....
00E4B019    3B8D 89130015   cmp ecx,dword ptr ss:    ; USER32.77D10000
00E4B01F    74 10         je short 老虎YJK?00E4B031
00E4B021    3B8D 252D0015   cmp ecx,dword ptr ss:    ; ADVAPI32.77DA0000
00E4B027    74 08         je short 老虎YJK?00E4B031
00E4B029    3B8D C1190015   cmp ecx,dword ptr ss:    ; kernel32.7C800000
00E4B02F    75 31         jnz short 老虎YJK?00E4B062
比较是否是3个加密的api
...
00E4B0FE    8B9D C52D0015   mov ebx,dword ptr ss://取基地址dllbase
...
00E4B13C    8B85 050D0015   mov eax,dword ptr ss://eax=7,大概是dll的索引值
00E4B142    60            pushad
...
00E4B16C    0385 71170015   add eax,dword ptr ss:    ; ADSEC.10027088
00E4B172    E9 0E000000   jmp 老虎YJK?00E4B185
...
00E4B24A    8138 E8000000   cmp dword ptr ds:,0xE8
执行到00E4B24A,eax=10021ee0 ADSEC._HKSEC+RUN@4 ecx=10000000 ADSEC.10000000
eax=func-addr ecx=imagebase
GetApi2=00E4B24A
*******
...
00E4B4AB    2BD9            sub ebx,ecx;USER32.77D10000 //判断dllbase是否属于加密的api
00E4B4AD    0F84 CC000000   je 老虎YJK?00E4B57F //跳说明是加密的api
...
00E4E946    61            popad
00E4E947    0F85 12000000   jnz 老虎YJK?00E4E95F //
    执行到00E4E947 ,eax=func-addr ecx=imagebase
....
00E4B638    3B85 81250015   cmp eax,dword ptr ss:   ; kernel32.ExitProcess
00E4B63E    0F85 9E000000   jnz 老虎YJK?00E4B6E2 //判断api是否是kernel32.ExitProcess
    执行到00E4B63E ,eax=func-addr ecx=imagebase
...
00E4B6E2    3B85 81250015   cmp eax,dword ptr ss:   ; kernel32.ExitProcess
00E4B6E8^ 0F84 FFFDFFFF   je 老虎YJK?00E4B4ED//判断api
    执行到00E4B63E ,eax=func-addr ecx=imagebase
...
00E4CADC    3B85 2D100015   cmp eax,dword ptr ss:   ; USER32.wsprintfA
00E4CAE2    0F85 2D000000   jnz 老虎YJK?00E4CB15//判断api是否是USER32.wsprintfA
这里让它跳
    执行到00E4CAE2 ,eax=func-addr ecx=imagebase
...
00E4CB15    3B85 CD0C0015   cmp eax,dword ptr ss:   ; kernel32.RaiseException
00E4CB1B    0F85 07010000   jnz 老虎YJK?00E4CC28
...
00E4CC28    3B85 3D8C0E15   cmp eax,dword ptr ss:   ; ntdll.RtlEnterCriticalSection
00E4CC2E    0F84 27000000   je 老虎YJK?00E4CC5B
...
00E4CC35    3B85 418C0E15   cmp eax,dword ptr ss:   ; ntdll.RtlLeaveCriticalSection
00E4CC3B    0F85 20000000   jnz 老虎YJK?00E4CC61
...

3:找GetIat1和FillCall1
因为现在是第二类,先处理GetIat1和FillCall1
这时在代码段下“设置内存写入断点”,断在
00E4D9AF    8938            mov dword ptr ds:,edi      ; ADSEC._HKSEC_RUN@4
此时,=0,寄存器eax是IAT地址,即当eip=00E4D9AF而且=0时,是写入iat

GetIat1=00E4D9AF,eip=00E4D9AF =0,edi是原OrigFuncAddr
*******
...

f9几次
00E4DFE4    AB            stos dword ptr es:
此时,=90909090,寄存器edi是代码段地址,即当eip=00E4DFE4而且=90909090时,是填充code
ECX是OrigFuncAddr
stos dword ptr es:
执行后(sti),需要
mov ,eax
mov ,ecx

FillCall1=00E4DFE4,eip=00E4DFE4 =90909090
*********

4:对ebx表对应项下内存写入断点,找 BaseZero
00E4E15C    8919            mov dword ptr ds:,ebx
BaseZero=00E4E15C,寄存器ecx是ebx表地址
********

5:找GetIat2
取消所有断点,在kernel32.dll的esi表第一个dword下内存访问断点:地址00E3D957
F9运行
00E4A9CF    AD            lods dword ptr ds: //esi=00E3D957,esi表
再在代码段下内存写入断点,F9
00D9AE64    8F02            pop dword ptr ds:                   ; 03540000
此时,=0,是加密的func addr
GetIat2=00D9AE64 eip=00D9AE64 =0 ,这时把func addr写入替换即可
*******

再F9,来到
00D9AE64    8F02            pop dword ptr ds:
此时,=90909090,是加密的func addr
FillCall2=00D9AE64 eip=00D9AE64 =90909090 ,这时把func addr写入替换即可
*********

6:找GetApi1
取消所有断点
对esi表含0xbbbbbbbb的下内存访问断点,地址:00E3EDC3
F9,来到
00E4A9CF    AD            lods dword ptr ds://ESI表带bbbbbbbb的第一个dword
F7,来到
00E4A9E0    BF 00000000   mov edi,0x0
00E4A9E5    897E FC         mov dword ptr ds:,edi //esi表第一个dword读完清0
...
00E4AA15    3D EEEEEEEE   cmp eax,0xEEEEEEEE//比较是否一个大项结束
...
00E4ABA9    813E BBBBBBBB   cmp dword ptr ds:,0xBBBBBBBB //比较是否是0xbbbbbbbb
此时=0xbbbbbbbb
...
00E4ABCB    C700 00000000   mov dword ptr ds:,0x0//清bbbbbbbb对应的dword
...
00E4AC4E    FF33            push dword ptr ds:   ; OG70AS.Ordinal3998
取dllbase,==02FB0000offset OG70AS.Ordinal3998
...
00E4AC60    81E9 FB7ACA6E   sub ecx,0x6ECA7AFB
00E4AC66    F5            cmc//此时ecx是dllbase
...
00E4AD05    0385 951F0015   add eax,dword ptr ss:
00E4AD0B    F9            stc
...
00E4AD62    2D B75DCF30   sub eax,0x30CF5DB7
00E4AD67    60            pushad
此时,寄存器:
EAX 03020B40 OG70AS.#5462
ECX 02FB0000 offset OG70AS.Ordinal3998
esp 0012FF04 OG70AS.#5462
执行到00E4AD67,eax=func addr , ecx=dll base
...
00E4E946    61            popad
00E4E947    0F85 12000000   jnz 老虎YJK?00E4E95F
执行到00E4E947,eax=func addr , ecx=dll base
00E4B546    50            push eax
00E4B547    8B3C24          mov edi,dword ptr ss:               ; OG70AS.#5462
执行到00E4B547,eax=func addr , ecx=dll base ,是func addr

GetApi1=00E4B547
*******

7:
GetApi1=00E4B547       GetApi2=00E4B24A   BaseZero=00E4E15C    GetBase=00E4A086
GetIat1=00E4D9AF       GetIat2=00D9AE64
FillCall1=00E4DFE4   FillCall2=00D9AE64

===============================

三:修复oep

脚本运行到伪OEP了,也就是说OEP已经被偷了代码了。这时候看下参考程序的OEP头代码:

006C4F9A    55            push ebp
006C4F9B    8BEC            mov ebp,esp
006C4F9D    6A FF         push -0x1
006C4F9F    68 000C7000   push 老虎YJK?00700C00//第一个push
006C4FA4    68 144F6C00   push 老虎YJK?006c4f14//第二个push push <jmp.&MSVCRT._except_handler3>
006C4FA9    64:A1 00000000mov eax,dword ptr fs:
006C4FAF    50            push eax
006C4FB0    64:8925 0000000>mov dword ptr fs:,esp
006C4FB7    83EC 68         sub esp,0x68
006C4FBA    53            push ebx
006C4FBB    56            push esi
006C4FBC    57            push edi
006C4FBD    8965 E8         mov dword ptr ss:,esp
006C4FC0    33DB            xor ebx,ebx
006C4FC2    895D FC         mov dword ptr ss:,ebx
006C4FC5    6A 02         push 0x2   《==脚本停在此处

看EBP寄存器,这个地址在堆栈窗中跟随:

0012FF34   0012FFE0指向下一个 SEH 记录的指针
0012FF38   006C4F14SE处理程序      //第二个push
0012FF3C   00700C00老虎YJK?00700C00//第一个push
0012FF40   00000000
0012FF44   00000000

=====================
四:自校验
00E4A806    83BD 350D0015 6>cmp dword ptr ss:,0x64//超过0x64后加自校验
00E4A80D    0F82 5E010000   jb 老虎YJK?00E4A971//改成jmp

,E95F01000090//修改后代码,脚本用
-------------
五:E8、E9改成FF15、FF25的处理

00E4DE2B    0BDB            or ebx,ebx
00E4DE2D    0F84 15000000   je 老虎YJK?00E4DE48//ebx=0 跳,此时edi是代码段地址
...
00E4DE48    83F8 50         cmp eax,0x50   //eax<50则跳 ,估计eax记录的是api的index号
00E4DE4B    0F82 88000000   jb 老虎YJK?00E4DED9
...
00E4DE8E    AA            stos byte ptr es://eax>50时填充90的地址,就是说90在E8E9前面
...
00E4DEB7    AA            stos byte ptr es://eax>50时填充E8E9
...
00E4DF58    8803            mov byte ptr ds:,al //eax<50时填充非90的数据在E8(E9) XXXXXXXX YY中的YY
...
00E4DEFA    AA            stos byte ptr es: //当eax<50时的填充e8、e9
00E4DEFB    FC            cld
...
00E4DEFD    807F FF E9      cmp byte ptr ds:,0xE9//e8 e9不同情况处理
00E4DF01    0F85 8A000000   jnz 老虎YJK?00E4DF91//e8跳



====================
脚本:
start:
var GetApi1
var GetIat1
var FillCall1

var GetApi2
var GetIat2
var FillCall2

var BaseZero
var GetBase

var CodeStart
var CodeSize

var ModBase //模块基址
var FuncAddr //未加密api地址
var FillAddr //fill地址
var IatMin   
var IatMax
var IatCur//api所属对应iat地址
var OrigFuncAddr //壳填在iat中的地址
var NewCallValue

mov GetApi1,00E4B547
mov GetIat1,00E4D9AF
mov FillCall1,00E4DFE4

mov GetApi2,00E4B24A
mov GetIat2,00D9AE64
mov FillCall2,00D9AE64

mov BaseZero,00E4E15C
mov GetBase,00E4A086

mov CodeStart,00401000
mov CodeSize,0088b000

mov IatMin,ffffffff
mov IatMax,0
mov nFuncCount,0
mov nIatCount,0

BPHWCALL            //删除所有硬件断点
BPHWS GetBase,"x"   //设置硬件执行断点
BPHWS BaseZero,"x"//设置硬件执行断点

run   //F9 (或用ESTO=SHIFT-F9)
pause
mov ,#E95F01000090#    //校验
MOV ,#909090909090#    //填充E8、E9前让它统一填充90
jmp func

//暂停在从ebx表取值
PauseAt_GetBase:
        BPHWS GetApi2,"x"
        BPHWS GetApi1,"x"
        BPWMCodeStart,CodeSize //代码断设置内存写入断点

        run
        jmp func


//函数8 暂停在ebx表清0处
PauseAt_BaseZero:
//{
        log "BaseZero"
        cmp ,0
        JE FINDOEP
        run
        jmp func
//}


//函数1,获取0xbbbbbbbb api地址
PauseAt_GetApi1:
//{
//pause
        mov FuncAddr,eax//eax=func addr , ecx=dll base
        mov ModBase,ecx
        run
        jmp func


//}

//函数2,fill 非加密iat
PauseAt_GetIat1:
//{
//pause
        mov IatCur,eax//00E4D9AFmov dword ptr ds:,edi
        mov OrigFuncAddr,edi
        cmp OrigFuncAddr,FuncAddr
        JE FillIat1_Next
        mov edi,FuncAddr
FillIat1_Next:
        run
        jmp
//}

//函数3 fill非加密call or jmp
PauseAt_FillCall1:
//{
//pause
        sti
        mov FillAddr,edi//00E4DFE4stos dword ptr es:
        sub FillAddr,5
//当前指针                              ||                                 ||
        find FillAddr,#E8#,2   //90 E8 xx xx xx xx 或者 90 E9 xx xx xx
        cmp $RESULT,0
        sub FillAddr,1
        jeFillcall1FF25      //不是E8则跳转到E9
        mov ,#ff15#
      jmp FillCall1_next

Fillcall1FF25:
        mov ,#ff25#

FillCall1_next:
        add FillAddr,2
        mov ,IatCur
        run
        jmp func

//函数4,获取非0xbbbbbb api地址,00E4B172
PauseAt_GetApi2:
//{
//pause
        mov FuncAddr,eax//执行到此,eax=func-addr ecx=imagebase
        mov ModBase,ecx
        cmp FuncAddr,77D1A8AD
        jne PauseAt_GetApi2_next
        GO 00E4CAE2
        pause//修改USER32.wsprintfA,修改标志位让它跳转。本程序对此api特别处理的

PauseAt_GetApi2_next:
        run
        jmp func

//}

//函数5,填充加密iat 00D9AE64
PauseAt_GetIat2:
//{
//pause   
        mov IatCur,edx//00D9AE64pop dword ptr ds:
        mov OrigFuncAddr,
        mov ,FuncAddr

GetIat2_next:
        jmp GetMinMaxIat
//}


//函数6,填充加密call
PauseAt_FillCall2:
//{
        sti       
        mov FillAddr,edx //00D9AE64pop dword ptr ds:
        sub FillAddr,1
//当前指针                            ||                                 ||
        find FillAddr,#E8#,2   // E8 xx xx xx xx 90 或者 E9 xx xx xx xx 90
        cmp $RESULT,0
        je Fillcall2FF25       //不是E8则跳转到E9
        mov ,#ff15#
      jmp Fillcall2_next

Fillcall2FF25:
        mov ,#ff25#

Fillcall2_next:
        add FillAddr,2
        mov ,IatCur//改成FuncAddr对应的IAT地址

        run
        jmp func


//}


//函数7,获取最大,最小iat
GetMinMaxIat:
//{
        cmp IatCur,IatMin
        ja Next   //如果IatCur>IatMin 则跳
        mov IatMin,IatCur
Next:
        cmp IatCur,IatMax
        jb Finish//如果IatCur<IatMax 则跳
        mov IatMax,IatCur
Finish:
        run
        jmp func
//}


//函数9,开始找oep
FINDOEP:
//{
        BPHWCALL //清除所有硬件断点
        bpmc//清除所有内存断点
        BPRMCodeStart,CodeSize//代码断内存访问断点
        run
        log "FINDOEP"
        log IatMin
        log IatMax
        bpmc//清除所有内存断点
//下面对本程序修补oep
//006C4F9A是真正oep地址
        mov ,#558BEC6AFF68000C700068144F6C0064A100000000506489250000000083EC685356578965E833DB895DFC#
//本程序OEP修补完成
        ret
//结束脚本

//}

//函数10,eip switch case
func:
//{
        mov flag,0
        cmp eip,GetApi1   //分支1, 停在GetApi1
        JE PauseAt_GetApi1

        cmp eip,GetIat1   //分支2 停在GetIat1
        JNE func_next1
        cmp ,0
        JE PauseAt_GetIat1
        jmp NotAll

func_next1:
        cmp eip,FillCall1   //分支3 停在FillCall1
        JNE func_next2
        cmp ,90909090
        JE PauseAt_FillCall1
        jmp NotAll
   
func_next2:

        cmp eip,GetApi2   // 分支4 停在GetApi2
        je PauseAt_GetApi2


        cmp eip, GetIat2
        JNE func_next3

        cmp ,0
        JE PauseAt_GetIat2//分支5 停在GetIat2

        cmp ,90909090
        JE PauseAt_FillCall2//分支6 停在FillCall2
        jmp NotAll

func_next3:
        cmp eip,BaseZero    //分支7,停在BaseZero
        je PauseAt_BaseZero

        cmp eip,GetBase   //分支8,停在GetBase
        je PauseAt_GetBase
   
NotAll:
        run
        jmp func
//}







yuzhiboqianyuan 发表于 2015-5-17 11:04

本帖最后由 yuzhiboqianyuan 于 2015-5-17 11:32 编辑

论坛到现在,我第一次看到敢搞TMD,赞一个,好好研究。

yuzhiboqianyuan 发表于 2015-5-17 11:09

yuzhiboqianyuan 发表于 2015-5-17 11:04
论坛到现在,我第一看到敢搞TMD,赞一个,好好研究。

要想看懂真是不容易啊,

mjsz 发表于 2015-5-17 11:30

想说爱你真的很难呀...想说看懂也真的很难呀{:5_118:}

虚竹 发表于 2015-5-17 12:48

.....精华的节奏 ?如果是原创的话应该是精华了

zhaohj999 发表于 2015-5-17 12:52

TMD实际上不难的,关键是两个表即解码表和api基址表。
这两个表也可不用脚本,在代码段设置内存写入断点,断下后一直F7就可跟踪到的。
象这种 lods dword ptr ds:的语句,一看就是读esi表了。
TMD很有意思的是对E8E9的处理,5字节要还原成6字节,前后90部分的处理
00E4DE48    83F8 50         cmp eax,0x50
这里花了点时间,不统一的话脚本很难处理。
至于校验与oep修复就相对简单。

yuzhiboqianyuan 发表于 2015-5-17 18:33

楼主,再加把劲,出个视频教程,或者图文教程,
造福后来人!

ningzhonghui 发表于 2015-5-17 19:53

过来支持下大牛分享知识,表示看着头都痛.... 大牛有空录个视频上来更好

Shark恒 发表于 2015-5-17 19:54

期待下次教程可以带有图片{:5_116:}

fzx118 发表于 2015-5-17 22:10

我还看得不是很懂但是谢谢分享精神哟
页: [1] 2 3 4
查看完整版本: 一款商业软件的Themida-2.1.*脱壳