一 问题描述:
一共用了4个程序作实验.
第一个程序是我自己写的,直接用tmd2.1.8默认方式加壳,成功脱掉.
第二个程序是别人的程序好像用vs2005写的,peid显示链接器版本8.0,没加壳,然后我用tmd2.1.8默认加密方式加壳,也成功脱掉.(dtws2.exe)
第三个程序是别人的程序,已加好壳,链接器版本8.0,用peid查壳2.1.*,个人感觉是成功脱掉了,可脱掉后,运行时,运行到vm中,就莫名奇妙的缓冲区溢出了.希望大牛能给点指导意见,谢谢!(GacRunner.exe)
/*78CAE9*/ xor ecx, eax
/*78CAEB*/ call dump_2.006BDB43
/*6BDB43*/ cmp ecx, dword ptr ds:[0x9D1724] 这两个值不相等,然后程序崩溃
/*6BDB49*/ jnz short dump_2.006BDB4D
/*6BDB4B*/ prefix rep:
/*6BDB4C*/ ret
/*6BDB4D*/ jmp dump_2.006CA7EC
第四个程序是另一家公司的,peid查壳也是2.1.*,链接器版本8.0,但跟第三个程序入口点不一样,成功脱掉.(xiumoPKGame.exe)
注:虽然全是游戏进程,但本人不是写外挂的,一直都觉得游戏所用的技术才是最先进的,所以喜欢研究一些游戏,这也是本人第一次脱壳.研究了快两个星期了,把这些经验写出来,希望对大家有帮助!一直是坚持 "取之论坛,回馈论坛"的原则,也就是炒现饭了.后面附件会上一些脚本,及研究样本,脚本不具有通用性,但只要明白了themida壳的基本原理,脚本就很好写了.
二.themida基本原理,个人简单理解 ,欢迎拍砖
这个壳只会对导入表中来自kernel32.dll ,user32.dll,advapi32.dll 三个dll中的函数作加密变形.外壳程序会把要执行的代码变在VirtualAlloc 分配的地址空间去执行,执行完了再VirtualFree.
该壳会把 代码段,只读数据段,数据段合并成一段
外壳程序在运行时,会把 代码段 先部分填充,填充成下面这种形式.只填充除导入函数调用相关的代码.还有 mov eax,ds:[??] 这种类型.如果是来自上面3个dll中的函数,就把原函数的代码虚拟化放在
壳申请的内存中比如 call ds:[MessageBoxW] ,变成 call 3d000000 (存虚拟化的MessageBoxW)
/*401000*/ push esi
/*401001*/ push edi
/*401002*/ nop call ds:[??] 导入函数 (6字节)
/*401003*/ nop
/*401004*/ nop
/*401005*/ nop
/*401006*/ nop
/*401007*/ nop
/*401008*/ push 0x1
/*40100A*/ push 0x0
/*40100C*/ push 0x0
/*40100E*/ mov edi, eax
......
填充成上面这种形式之后,就开始处理导入函数了.不是上面三个dll中的函数,就不虚拟化,不过把
call ds:[??] 6字节 变成了 call yy 5字节.每处理一个dll时,先VirtualAlloc,再FreeVirtual.(规律的来源)
call填充完之后,就开始执行 原程序代码了,从原程序oep处执行
三.esi表和ebx表,个人定义的(themida是怎样填充call的)
tmd就是根据esi表来填call的,ebx表里全部装的是dll的imagebase.发一段esi表和ebx表的具体数据
esi 表
1.ijl15.dll
0x4b7fdd62, 0xbbbbbbbb, 0x0d84aa6e, 0xc000dd88, 0xaaaaaaaa, 0xffffffff, 0xdddddddd,
0x00100000, 0xbbbbbbbb, 0x2d84aa6e, 0x0000dd88, 0xaaaaaaaa, 0xffffffff, 0xdddddddd,
0x4b67dd62, 0xbbbbbbbb,0x4d84aa6e, 0x4000dd87, 0xaaaaaaaa, 0xffffffff, 0xdddddddd,
0xeeeeeeee, 0xdddddddd,
2.psapi.dll
0x615b7db1, 0xad84aa54, 0xc0065651, 0xaaaaaaaa, 0xffffffff, 0xdddddddd,
0x913b7694, 0xcd84aa54, 0x8000dd89, 0xaaaaaaaa, 0xffffffff, 0xdddddddd,
0x4ae33057, 0xed84aa54, 0x00065651, 0xaaaaaaaa, 0xffffffff, 0xdddddddd,
0xeeeeeeee, 0xdddddddd,
3.kernel32.dll
0xc99a240b, 0x082401bf, 0x55656ea1, 0xd5640053, 0xffffffff,0xdddddddd,
0xb313e96f, 0xe82401bf, 0xc5656e92, 0xcd653e7b, 0xb5652bf8, 0x7d63fb6d, 0x2d63fb6b, 0xfd63d972, 0xf563d987, 0xdd63d986, 0xffffffff, 0xdddddddd,
0xe077678d, 0xc82401bf, 0x55653e7b, 0x3d652d7c, 0x8563d99d, 0xffffffff, 0xdddddddd, 0xe1568bb2, 0xa82401bf, 0xfd64fb7a, 0x6d6401f9, 0xffffffff, 0xdddddddd,
0xc1cae679, 0x882401c0, 0x25656e94, 0x0d653e79, 0x95652d31, 0x3d63fb56, 0xfd63d963, 0x5d63d97f, 0xffffffff, 0xdddddddd,
0xaad005e7, 0x682401c0, 0x3d6540d4, 0xbd63ffad, 0x2d63fc2e, 0x6563fc32, 0xb563fd2e, 0x6d640729, 0x9d63ead5, 0xffffffff, 0xdddddddd,
0x2223cd36, 0x482401c0, 0xbd6540d5, 0x7565460d, 0x3d63ffad, 0xed63fc3b, 0xf563fc2f, 0x4d63fc33, 0xbd63fc4c, 0x4563fc55, 0x3563fd2e, 0xed640712, 0xffffffff, 0xdddddddd,
0x42ee5b73, 0x282401c0, 0x05654e97, 0xaaaaaaaa, 0xffffffff, 0xdddddddd,
.....
0x000000,0x00000
ebx表
0x????(ijl15.dll的imagebase),
0x????(第二个dll的imagebase),
0x7c800000(kernel32.dll的imagebae),.....
esi表,相信大家可以猜出来是什么东西了.论坛上也有介绍.每一小项以0xffffffff,0xdddddddd结束,
每一大项以0xeeeeeeee,0xdddddd结束.
每一小项其实就是代表一个导入api的相关信息
总表以0x00000000,0x00000000结束
每一大项就是指一个dll.
每一小项第一个dword 可以解析出 导入函数的"函数地址"
每一小项第二个dword,如果是0xbbbbbbbb,说明是 mov eax,ds:[xx]这种方式
每一小项如果第二个dword不是0xbbbbbbb,就根据这一项解析出iat的地址
每一小项第二个dword之后的每个dword解析出 call 的地址 如果有0xaaaaaaa 说明是 jmp ds:[xx] 这种调用 方式,否则说明是 call ds:[xx] 这种方式
举例
00401000 call ds:[00402000]
00401100 call ds:[00402000]
00402000地址处存的是 7c809af1 //api VirtualAlloc的地址
那么通过解析每一小项的第一外dword 可以得到 7c809af1,解析第二个(如果不是0xbbbbbbb)可以得到 00402000,如果第二个是0xbbbbbbbb,则通过第三个得到00402000,通过之后的可以得到 00401000(call指令的地址),或其它调用处的地址 比如 00401100 也调用了.
我把上面三个数字分别定义成 api地址(7c809af1),iat地址(00402000),FillCall地址(00401100+1),有些是没有FillCall地址的 比如程序中是这样的调用的 mov eax,ds:[xx] call eax
只要找到 上面3个地址 就可以脱壳了
四.怎样找到关键的3个地址
esi表就派上用场了.对esi表中的第一项下内存访问断点,f7跟一段时间就可以得到api地址和相应的模块地址.再对代码段下内存写断点,就可以找到 iat地址和fillcall地址.这时规律就出来了.我把这个规律分3类
第一类:带0xbbbbbb api的处理
第二类:不带0xbbbbbb api的处理
第三类:加密api 的处理
GetApi1 GetApi2 GetApi2
GetIat1 GetIat1 GetIat2
FillCall 1 FillCall1 FillCall2
从左往右依次为第一类,第二类,第三类
GetAPi1 设个硬件执行断点
GetApi2 设个硬件执行断点
再对代码段设个内存写断点
在这三个地方作点手脚,把加密的call给还原回去,.... 脱壳
还有一个关键性地址,定义为 BaseZero,也就是ebx表中 imagebase清0时的指令地址,
写脚本时有用.
我们还可以再定义一个地址叫GetBase,此处的指令刚好第一次访问 ebx表,这样在写脚本时作下判断,可以加快脚本执行速度
五.soeasy,找oep及还原变形的oep
这时ebx表就派上用场了,在把ebx表的最后一个dword变成0后,对代码段下个内存访问断点,直接就到oep.不过大多数情况这个oep是被tmd变形了.怎样恢复其实有个小技巧.比如你要搞一款游戏,用peid查到这个加壳的程序是链接器版本是8.0,你这时可以找游戏目录下其它的exe或dll看是链接器版本是不是8.0,如果是8.0恭喜你,中奖了,od打开没加壳的,对比下oep.
我脱的这几个程序 oep 全是
oep: call 123456,
oep+5: jmp 654321
123456: ....
你用上面的找oep的方法,其实会停在 123456 这个地址处.对比下oep其实 这个123456地址跟真实的oep没隔多远,找到oep后,把oep变成 call ??,jmp ?? 这样就修复了oep,然后 就是 用uif fix iat,dump,import rec 修复iat.done
六.忘了说怎么去找到esi表和ebx表
这里其实有个小技巧,我是从unpack网站上看到的.od加载程序,在VirtualAlloc的返回处,下断点,
在整个调试过程中,尽量少用 bp断点,这样跟快了,很容易就跟飞了,也不要在api头下bp,所以就下在VirtualAlloc的返回处了,第一次断下后,下硬件执行断点 he eax+8ef1 ,eax就是VirtualAlloc的返回值,这一块virtual memory 其实是为了加载 kernel32.dll (not as image),8ef1 就是 VirtualAlloc
的 FileOffset,not Rva.he eax+8ef1 之后,取消bp的 VirtualAlloc,f9一直跑,每次断下后,注意观察eax的值,当eax的值是 GetProcAddress 后,这时esi 和ebx就快出来了,这时 f9 一直跑,在eax=GetProcAddress 之后,中断两次在 上面的硬件断点时,注意看堆栈中的返回值,跑到堆栈的返回值处,鼠标往下拉下,简单的模拟跟几步,会看到
/*4DF5F5*/ mov esi, dword ptr ss:[ebp+0x9DA368B]
/*4DF5FB*/ jmp TestApi2.004DF605
/*4DF605*/ mov ebx, dword ptr ss:[ebp+0x9DA2395]
testapi.exe
各个版本好像有点不一样,这是我自己的程序用tmd2.1.8加壳后,找到的esi 和 ebx
七. 注意事项
1.在整个下断过程中,尽量用硬件断点,我发现bp断点,跑快了很容易就跑飞了
2.忘了说,不能调试的开od kernle mdoe,如果被检测到调试器,有时是需要重启电脑的
八.找oep的灵感来源
1.在google上搜到youtube上 老外超easy脱壳方式,虽然我看那种脱壳方式失败了,但我有了另外的发现.老外的思想基本上就是说 在ZwFreeVirtualMemory的返回处下断点,不断的跑,直到edi的值
稳定下来(不是一直都是红色),然后在代码段下内存访问段点,这样再次断下直接就到oep了.然后我就试了下,确实是可以的,直不过分界线不是很明显.这让我想到tmd的执行方式可能就 是 VirtualAlloc,do sth 再VirtualFree.后来又看到unpack上 找esi表的方法,我往下看了下,发现了ebx表,发现里面全是dll imagebase,我就把这个表给用上了.试了好几个程序,找oep都很准 .只要ebx表清完0之后,代码段下内存访问断点,直接就到oep了.
九.实例讲解testapi.exe
1.先用我自己写的一个程序 testapi2.1.8.exe ,这个程序是直接用tmd2.1.8 (csdn上下载的)默认加密方式,没改oep.第一次写脚本,也不会用什么复杂的命令,只好用这个很简单的脚本了,下面这个脚本是通用的找esi,ebx表的,如果找不到说明 可能不适用tmd版本.那就需要根本上面的原理去找了.
尽量不要有bp 断点,也就是alt+b中的断点 全删了,否则很容易跟飞
2. 找GetBase,我们的主要目的是找到上面那三类的规律,方便下断点
//清所有硬件断点,并在在VirtualAlloc返回处下断点
BPHWCALL
BP 7c809b0a
run
//此时中断在VirtualAlloc返回处,清VirtualAlloc断点,并在新的VirtualAlloc处下硬件执行断点
BC 7c809b0a
BPHWS eax+8ef1,"x"
run
//不断的中断在新的VirtualAlloc处,比较eax是不是GetProcAddress
//不是就断续运行
LOOP:
cmp eax,7C80AE40
JE IsGetProcAddress
run
jmp LOOP
//中断在VirtualAlloc,并且eax=GetProcAddress,连续中断两次后,可得到esi和ebx的值
IsGetProcAddress:
log "IsGetProcAddress"
run
//中断一次
run
//中断第二次,此时刚好填充好esi表和ebx表
log "IsGetProcAddress2"
运行上面的脚本停下来后
堆栈是0012FF5C 004E1706 RETURN to TestApi2.004E1706
看下 TestApi2.004DF5ED 这个地址处的代码
/*4E1706*/ jpo TestApi2.004E1721
/*4E170C*/ jmp TestApi2.004E1721
/*4E1711*/ dec dword ptr ds:[ebx-0x3B8B59A9]
/*4E1717*/ lds esp, edx
/*4E1719*/ mov al, byte ptr ds:[0x39E57AB3]
/*4E171E*/ xor byte ptr ds:[edx+0x1C], cl
/*4E1721*/ mov dword ptr ss:[ebp+0x9E00D14], eax
/*4E1727*/ cmc
/*4E1728*/ mov esi, dword ptr ss:[ebp+0x9DB3578] //esi表
/*4E172E*/ cmc
/*4E172F*/ mov ebx, dword ptr ss:[ebp+0x9DB1C53] //ebx表
/*4E1735*/ cmc
/*4E1736*/ mov dword ptr ss:[ebp+0x9DF18D9], esi
/*4E173C*/ jge TestApi2.004E175A
/*4E1742*/ pushad
/*4E1743*/ mov cl, 0xE5
执行完两个mov 之后 要记录下 ebx和 esi的值
ebx={ 0x7c800000, 0x77d10000, 0x77da0000, 0x10200000};
依次是是krnl32,user32,advapi32,msvcr71d
esi={
//kernel32.dll
0x35451750, 0x2ac8cd89, 0xeed97e99, 0xffffffff, 0xdddddddd,
0x27c5585a, 0xcac8cd89, 0xded97e98, 0xffffffff, 0xdddddddd,
0x80d11ceb, 0xeac8cd89, 0x0ed97e83, 0xffffffff, 0xdddddddd,
0xeeeeeeee, 0xdddddddd,
//user32.dll
0x18a32f00, 0x0ac8cd8a, 0xbed97e99, 0xffffffff, 0xdddddddd,
0xeeeeeeee, 0xdddddddd,
//advapi32.dll
0x21d91677, 0x4ac8cd89, 0x56d97e99, 0xffffffff, 0xdddddddd,
0xdb1718fb, 0x6ac8cd89, 0x4ed97e98, 0xffffffff, 0xdddddddd,
0xeeeeeeee, 0xdddddddd,
//msvcr71d.dll
0x770af0e5, 0x4c081957, 0xc0000225, 0xaaaaaaaa, 0xffffffff, 0xdddddddd,
0xc0f9fd27, 0x6c081957, 0x0000029d, 0xaaaaaaaa, 0xffffffff, 0xdddddddd,
0x573a4c0a, 0x8c081957, 0x00000297, 0xaaaaaaaa, 0xffffffff, 0xdddddddd,
0xfdce5b59, 0xac081957, 0x40000244, 0xffffffff, 0xdddddddd,
0x90d2808b, 0xcc081957, 0xc0000275, 0xaaaaaaaa, 0xffffffff, 0xdddddddd,
0x103e7d4e, 0xec081957, 0xe000022e, 0xffffffff, 0xdddddddd,
0xa71f7fca, 0x0c081958, 0x80000232, 0xffffffff, 0xdddddddd,
0xacd5dd6a, 0x2c081958, 0x40000234, 0xffffffff, 0xdddddddd,
0x6b2067ac, 0x4c081958, 0xffffffff, 0xdddddddd,
0x031a0a45, 0x6c081958, 0x0000024a, 0xffffffff, 0xdddddddd,
0xce8624bd, 0x8c081958, 0x40000274, 0xaaaaaaaa, 0xffffffff, 0xdddddddd,
0x917be5ba, 0xac081958, 0x20000214, 0xffffffff, 0xdddddddd,
0x82e69949, 0xcc081958, 0xa0000258, 0xffffffff, 0xdddddddd,
0x4e04e8f9, 0xec081958, 0xe0000257, 0xffffffff, 0xdddddddd,
0x35044edf, 0x0c081959, 0x00000275, 0xaaaaaaaa, 0xffffffff, 0xdddddddd,
0xfa72f3b6, 0x2c081959, 0x40000250, 0xffffffff, 0xdddddddd,
0xd3cc3342, 0x4c081959, 0x8000024f, 0xffffffff, 0xdddddddd,
0x8932339b, 0x6c081959, 0xa000023a, 0xffffffff, 0xdddddddd,
0xeeeeeeee, 0xdddddddd,
0x00000000, 0x00000000};
这个exe 只依赖这4个dll.由于ebx表初次出现,我们让时先来找GetBase,这个值其实不是必须的,不过为了让脚本执行得快点,我们还是找一下.对ebx表第一个dword下内存访问断点,正好断在
/*4E179D*/ mov ecx, dword ptr ds:[edx] //这句就是取dllbase
/*4E179F*/ pop edx
/*4E17A0*/ stc
GetBase=4E179D 3.由于esi表前面是加密的,就先来找GetAPi2
这个程序很简单,我脱的几个程序,好像加密api都没有0xbbb这样的.对esi表第一个dword下内存访问断点 跑起来,还是那句话,尽量不要有bp断点,否则很容易更飞,断下后来到
/*4E2137*/ lods dword ptr ds:[esi] //到esi表中的值
/*4E2138*/ jmp TestApi2.004E2147
/*4E213D*/ push ebx
/*4E213E*/ lahf
/*4E213F*/ mov cl, 0x49
一些关键指令,尽量在大脑里留点印象 ,这时要有耐心的f7,遇到call 也要f7,否则可能更飞,这里代码不是很多.f7 一路上会遇到
cmp eax,0xeeeeeee
rol eax,0x3
rol eax,0x10
cmp eax,0x1000
...
/*4E255E*/ mov dl, 0xAC
/*4E2560*/ popad
/*4E2561*/ cmp eax, dword ptr ds:[edx] //这句就是在比较函数名的hash值了,edx是函数名hash表,eax是本次函数名hash值
/*4E2563*/ je TestApi2.004E2612 // 跳转
/*4E2569*/ pushad
/*4E256A*/ jmp TestApi2.004E2575
接上面那个 cmp,其实我也不知道 hash是什么东东,反正是根据函数得到的一个dword,je 时就说明找到了,这时有个index值,得到一个api的方法,大家都知道的,根据func name index 得到 ord ,再根据ord 得到 func rva,再加上imagebase 得到func addr ,在je 这个地址 下he 004E2612 ,这之后的就是根据上面的 方法得到api地址. 这中间我们要对一些指令有点印象
....
//下面这段代码跟以往一些版本有些不同的,以前好像是sub ??? ,就是判断是不是那3个要加密的imagebase,网上有种说法是直接改成不比较,也就是如果当前处理的dll是加密dll,也按未加密的方式填iat,填call,我试了下改这个地方的代码,已经不行了
/*4E262B*/ mov byte ptr ss:[ebp+0x9E88B26], 0x0 //是否是加密dll
/*4E2632*/ mov dword ptr ss:[ebp+0x9E88B2F], 0x0
/*4E263C*/ mov ecx, 0x1
/*4E2641*/ test ecx, ecx
/*4E2643*/ je short TestApi2.004E2696
/*4E2645*/ mov ecx, dword ptr ss:[ebp+0x9DB1C53]
/*4E264B*/ mov ecx, dword ptr ds:[ecx]
/*4E264D*/ cmp ecx, dword ptr ss:[ebp+0x9DB1706] //user32.imagebase
/*4E2653*/ je short TestApi2.004E2665
/*4E2655*/ cmp ecx, dword ptr ss:[ebp+0x9DB2917] //advapi32.imagebase
/*4E265B*/ je short TestApi2.004E2665
/*4E265D*/ cmp ecx, dword ptr ss:[ebp+0x9DB0AB9] //kernel32.imagebase
/*4E2663*/ jnz short TestApi2.004E2696 //不是上面3个dll,跳转
/*4E2665*/ mov byte ptr ss:[ebp+0x9E88B26], 0x1 //是否是加密dll
/*4E266C*/ lea edx, dword ptr ss:[ebp+0x9E88B33]
/*4E2672*/ lea ecx, dword ptr ss:[ebp+0x9E88C0B]
/*4E2678*/ cmp edx, ecx
/*4E267A*/ jnb short TestApi2.004E2696
/*4E267C*/ cmp dword ptr ds:[edx], 0x0
/*4E267F*/ je short TestApi2.004E2696
/*4E2681*/ cmp dword ptr ds:[edx], eax
/*4E2683*/ jnz short TestApi2.004E2691
/*4E2685*/ mov dword ptr ss:[ebp+0x9E88B2F], 0x1
/*4E268F*/ jmp short TestApi2.004E2696
/*4E2691*/ add edx, 0x4
/*4E2694*/ jmp short TestApi2.004E2678
有耐心的f7 ,中途关注下寄存器中有没有自己认得的数字,dllbase sth
/*4E27A6*/ lods word ptr ds:[esi] //像这种会让你联想到???,ord
/*4E27A8*/ jmp TestApi2.004E27BD
/*4E27AD*/ xlat byte ptr ds:[ebx+al]
//之后又出现这种,你会想到??
....
/*4E27D2*/ lods dword ptr ds:[esi] //这个得到func rva,看来离函数地址近了
/*4E27D3*/ jns TestApi2.004E27EB
/*4E27D9*/ pushad
/*4E27DA*/ jmp TestApi2.004E27E4
/*4E27DF*/ nop
/*4E27E0*/ pop edi
/*4E27E1*/ mov esi, 0xE6813424
/*4E27E6*/ das
/*4E27E7*/ xor bl, byte ptr ds:[ebx]
/*4E27E9*/ push esi
/*4E27EA*/ popad
/*4E27EB*/ pushad
/*4E27EC*/ mov bl, 0xA9
/*4E27EE*/ mov al, 0x98
/*4E27F0*/ popad
/*4E27F1*/ add eax, 0x1FDF2115
/*4E27F6*/ sub eax, 0x1BB749DE
/*4E27FB*/ add eax, ecx
/*4E27FD*/ add eax, 0x1BB749DE
/*4E2802*/ sub eax, 0x1FDF2115 //这个执行完,eax=func addr ecx=imagebase
/*4E2807*/ pushad
/*4E2808*/ push edx
/*4E2809*/ jmp TestApi2.004E281E
到此 第三类 也就是 GetApi2的值已经搞定
GetApi2=4E2807 ,
记录下,写脚本用
4.找GetIat2 和 FillCall2
因为我们现在正在处理加密api 所以... 代码段内存写断点,一次搞定两个地址 F9 断在
/*44148B*/ pop dword ptr ds:[edx] // 此时 edx=0040200c [esp]=0276000(你们那儿可能不一样) [edx]=0
/*44148D*/ jmp TestApi2.0043B1AE
这时可以用ida 看下 没加壳的 testapi.exe 0040200c处是什么东东 .规律来了
当 eip=44148B and [edx]=0 时 我们可以 认为 edx=iat, 44148B这个地址收下了
GetIat2=44148B ,
再f9,停在
/*444194*/ mov byte ptr ds:[ebx], al //ebx=0040103c,al=e8,这又是在搞么,e8你认识不?,去ebx那儿看哈,或用ida看下没加壳的程序
再f9
/*44148B */ pop dword ptr ds:[edx] edx=0040103d,[edx]=90909090,这里又是在搞么?? 44148B 这个地址咋又出现了
/*44148D*/ jmp TestApi2.0043B1AE
44148B 这个地址我又收下了
FillCall2=44148B
当 eip=44148B and [edx]=90909090 时 我们可以定义 认为 call addr=edx
5.来得到BaseZero
通过加密dll,我们得到了GetApi2,GetIat2,FillCall2,3 个值,这时对ebx表中相应的dword下内存写断点,f9,断在
/*4E5A84*/ mov dword ptr ds:[ecx], ebx //ebx=0,这句就是BaseZero
/*4E5A86*/ pop ebx
/*4E5A87*/ jmp TestApi2.004E5A9D
BaseZero=4E5A84 6.搞定GetApi
由于这个程序esi表中,没0xbbbbbbbb这样的项,所以这个值就不要了
7.搞定GetIat1和FillCall1
在BaseZero这个地址处下硬件执行断点,当清完一个加密的dll,准备处理下一个未加密的dll时,开代码段内存写断点
...
/*4E532F*/ mov dword ptr ds:[edx], ecx //edx=0040201c ecx=1020C034 (msvcr71d._except_handler3),edx=iat地址
ds:[0040201C]=00000000
/*4E5331*/ mov edx, dword ptr ss:[esp]
...
GetIat1=4E532F
再F9
...
/*4E5825*/ stos byte ptr es:[edi] //填e8 or e9
/*4E5826*/ pushad
/*4E5827*/ clc
...
再f9
/*4E58D0*/ stos dword ptr es:[edi] //edi=0040112f [edi]=90909090,找到了
/*4E58D1*/ jpo TestApi2.004E58E8
/*4E58D7*/ jmp TestApi2.004E58E8
FillCall1=4E58D0 8.到此GetApi1,GetIat1,FillCall1,GetApi2,GetIat2,FillCall2,GetBase,BaseZero 这8个值中,除了GetApi1,由于没有0xbbbbbb项,都获取到了,现在可以写脚本了
GetApi1= none GetApi2=4e2807 BaseZero=4e5a84 GetBase=4e179d
GetIat1=4E532F GetIat2=44148b
FillCall1=4E58D0 FillCall2=44148b
下面这个脚本是最后的优化版本,针对这个testapi.exe 有些是没用到的
//清所有硬件断点,并在在VirtualAlloc返回处下断点
BPHWCALL
BP 7c809b0a
run
//此时中断在VirtualAlloc返回处,清VirtualAlloc断点,并在新的VirtualAlloc处下硬件执行断点
BC 7c809b0a
BPHWS eax+8ef1,"x"
run
//不断的中断在新的VirtualAlloc处,比较eax是不是GetProcAddress
//不是就断续运行
LOOP:
cmp eax,7C80AE40
JE IsGetProcAddress
run
jmp LOOP
//中断在VirtualAlloc,并且eax=GetProcAddress,连续中断两次后,可得到esi和ebx的值
IsGetProcAddress:
log "IsGetProcAddress"
run
//中断一次
run
//中断第二次,此时刚好填充好esi表和ebx表
//上面是通用 找esi和ebx 表 脚本,下面的脚本开始 反加密
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 NewCallValue //new call ??
var IatMin
var IatMax
var IatCur //api所属 iat地址
var OrigFuncAddr //壳填在iat中的地址
mov GetApi1,0
mov GetIat1,4e532f
mov FillCall1,4e58d0
mov GetApi2 ,4e2807
mov GetIat2,44148b
mov FillCall2,44148b
mov BaseZero,4e5a84
mov GetBase,4e179d
mov CodeStart,00401000
mov CodeSize,00003000
mov IatMin,90909090
mov IatMax,0
mov nFuncCount,0
mov nIatCount,0
BPHWCALL
BPHWS GetBase,"x"
BPHWS BaseZero,"x"
run
jmp func
//暂停在从ebx表取值
PauseAt_GetBase:
//{
log "GetBase"
cmp [edx],7c800000
JE PauseAt_GetBase_Next
cmp [edx],77d10000
JE PauseAt_GetBase_Next
cmp [edx],77da0000
JE PauseAt_GetBase_Next
//不是上面3个就不要内存写断点
bpmc
bphwc GetApi2 //清getapi2
//bphwc GetApi1 //getapi1
run
jmp func
PauseAt_GetBase_Next:
BPHWS GetApi2,"x"
//BPHWS GetApi1,"x"
BPWM CodeStart,CodeSize //代码断内存写断点
run
jmp func
//}
//函数8 暂停在ebx表清0处
PauseAt_BaseZero:
//{
log "BaseZero"
cmp [ecx+4],0
JE FINDOEP
run
jmp func
//}
//函数1,获取0xbbbbbb api地址
PauseAt_GetApi1:
//{
mov FuncAddr,eax
mov ModBase,ecx
log "GetApi1"
log ModBase
log FuncAddr
jmp FixRelocApi
//}
//函数2,fill 非加密iat 4E3208
PauseAt_GetIat1:
//{
mov IatCur,edx
mov OrigFuncAddr,ecx
log "GetIat1"
log ModBase
log FuncAddr
log IatCur
log OrigFuncAddr
cmp OrigFuncAddr,FuncAddr
JE FillIat1_Next
mov ecx,FuncAddr
FillIat1_Next:
jmp GetMinMaxIat
//}
//函数3 fill 非加密 call
PauseAt_FillCall1://填充非加密call or jmp
//{
mov FillAddr,edi
log "FillCall1"
log ModBase
log FuncAddr
log IatCur
log OrigFuncAddr
log FillAddr
cmp OrigFuncAddr,FuncAddr
JE FillCall1_next
log "not equel"
mov NewCallValue,FuncAddr
sub NewCallValue,FillAddr
sub NewCallValue,4
mov eax,NewCallValue
FillCall1_next:
run
jmp func
//}
//函数4,获取非0xbbbbbb api地址,4E0797
PauseAt_GetApi2:
//{
mov FuncAddr,eax
mov ModBase,ecx
log "GetApi2"
log ModBase
log FuncAddr
jmp FixRelocApi
//}
//函数5,填充加密iat 4427DD
PauseAt_GetIat2:
//{
mov IatCur,edx
mov OrigFuncAddr,[esp]
mov [esp],FuncAddr
log "FillIat2"
log ModBase
log FuncAddr
log IatCur
log OrigFuncAddr
jmp GetMinMaxIat
//}
//函数6,填充加密call 4427DD
PauseAt_FillCall2:
//{
mov FillAddr,edx
mov NewCallValue,FuncAddr
sub NewCallValue,FillAddr
sub NewCallValue,4
mov [esp],NewCallValue
log "FillCall2"
log ModBase
log FuncAddr
log IatCur
log OrigFuncAddr
log FillAddr
log NewCallValue
run
jmp func
//}
//函数7,获取最大,最小iat
GetMinMaxIat:
//{
cmp IatCur,IatMin
ja Next
mov IatMin,IatCur
Next:
cmp IatCur,IatMax
jb Finish
mov IatMax,IatCur
Finish:
run
jmp func
//}
//函数9,开始找oep
FINDOEP:
//{
BPHWCALL
bpmc
BPRM 401000,3000 //代码断内存访问断点
run
log "FINDOEP"
log IatMin
log IatMax
bpmc
ret
//}
//函数10,eip switch case
func:
//{
// cmp eip,GetApi1 //分支1, 停在GetApi1
// JE PauseAt_GetApi1
cmp eip,GetIat1 //分支2 停在GetIat1
JNE func_next1
cmp [edx],0
JE PauseAt_GetIat1
jmp NotAll
func_next1:
cmp eip,FillCall1 //分支3 停在FillCall1
JNE func_next2
cmp [edi],90909090
JE PauseAt_FillCall1
jmp NotAll
func_next2:
cmp eip,GetApi2 // 分支4 停在GetApi2
je PauseAt_GetApi2
cmp eip, GetIat2
JNE func_next3
cmp [edx],0
JE PauseAt_GetIat2 //分支5 停在GetIat2
cmp [edx],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
//}
//函数11,修正部分函数地址
FixRelocApi:
//{
cmp ModBase,7c800000
jne CON
cmp FuncAddr,7c8092ef
JE SetLastError
cmp FuncAddr,7c809136
JE HeapSize
cmp FuncAddr,7c80911e
JE RtlReAllocHeap
cmp FuncAddr,7c8090db
JE RtlGetLastWind32Error
cmp FuncAddr,7c8090f6
JE RtlAllocHeap
cmp FuncAddr,7c80910c
JE RtlFreeHeap
cmp FuncAddr,7c8091c9
JE RtlLeaveCritical
cmp FuncAddr,7c8090bd
JE RtlEnterCritical
cmp FuncAddr,7c80906a
JE RtlDeleteCritical
jmp CON
RtlDeleteCritical:
mov FuncAddr,7C93137A
jmp CON
RtlEnterCritical:
mov FuncAddr,7C921000
jmp CON
RtlLeaveCritical:
mov FuncAddr,7C9210E0
jmp CON
RtlFreeHeap:
mov FuncAddr,7C92FF2D
jmp CON
RtlAllocHeap:
mov FuncAddr,7C9300C4
jmp CON
RtlGetLastWind32Error:
mov FuncAddr,7C92FE21
jmp CON
RtlReAllocHeap:
mov FuncAddr,7C938477
jmp CON
HeapSize:
mov FuncAddr,7c9304dd
jmp CON
SetLastError:
mov FuncAddr,7c92fe30
jmp CON
CON:
run
jmp func
//}
脚本运行完正好停在oep(00401140)
这是用UIF(universal import fixer),修复后,用od dump,就脱壳成功了.
再用LordPe 删除不需要的区段,再重建pe 就ok了
1.uif
2.od dump
3.脱壳后,还没减肥
实例二 dtws2.exe
1.这个exe 是游戏目录下没加壳的,我用tmd2.1.8加了壳,然后再用上面的方法脱.先用通用脚本找到esi表和ebx表,记录下
2.找GetBase
对ebx表下内存访问断点,断在
/*6200E4*/ mov ecx, dword ptr ds:[edx] //ds:[026E0000]=76D30000 (iphlpapi.76D30000)
ecx=00C78F59
/*6200E6*/ pop edx
GetBase=6200E4
ebx={
0x76d30000, 0x10000000, 0x76f30000, 0x7c800000, 0x77d10000, 0x77ef0000, 0x76320000, 0x72f70000, 0x77da0000, 0x77f40000, 0x770f0000, 0x71a20000, 0x02800000, 0x3e410000, 0x76b10000};
依次是iphlpapi.dll,libeay32.dll,wldap32.dll,kernel32.dll ....
esi={
0xa135d069, 0xd426dfae, 0x00003e2a, 0xaaaaaaaa, 0xffffffff, 0xdddddddd, 0xeeeeeeee, 0xdddddddd, 0xa0566741, 0xbbbbbbbb, 0x9426df77, 0x80009ba6, 0xaaaaaaaa, 0xffffffff, 0xdddddddd,....};
只抄了最前面一部分
3.找GetApi2
由于ebx表第一项是非加密dll 并且esi表 第一项不带0xbbbbbb,esi第一项下内存访问,然后往下跟找到
/*6210C8*/ sub eax, 0x33917DD2
/*6210CD*/ add eax, ecx
/*6210CF*/ push ebx
/*6210D0*/ mov ebx, 0x2D4A6024
/*6210D5*/ shr ebx, 0x6
/*6210D8*/ shl ebx, 0x5
/*6210DB*/ dec ebx
/*6210DC*/ add ebx, 0x55925CE5
/*6210E2*/ shr ebx, 1
/*6210E4*/ sub ebx, 0x28A48A0
/*6210EA*/ add eax, ebx //执行完之后 eax=func addr ,ecx=dllbase
/*6210EC*/ pop ebx
GetApi2=6210EC
4.找GetIat1和FillCall1
这时代码段下内存写断点,断在
/*623CCB*/ pop dword ptr ds:[eax] //这句
/*623CCD*/ jmp dtws2.00623CE0
GetIat1=623CCB
再F9几次,填充call
/*624315*/ stos dword ptr es:[edi]
/*624316*/ jnz dtws2.00624326
FillCall1=624315
5.对ebx表 对应项下内存写找 BaseZero
/*6244F2*/ mov dword ptr ds:[ecx], ebx
/*6244F4*/ pop ebx
BaseZero=6244F2
6.GetApi2,GetIat1,FillCall1 搞定 这时搞 GetIat2
在准备访问ebx表中kernel32.dll时,下内存写断点,he GetApi2 看这个地址对不对,验证发现GetApi2是对 可以根据内存断点找到 GetIat2 和FillCall2
/*57BB64*/ pop dword ptr ds:[edx]
/*57BB66*/ jmp dtws2.005715CF
GetIat2 =FillCall2=57BB64
7.搜索发现有0xbb项,对esi表 含0xbb的 下内存访问断点,中途可以删除一些不必要的项
/*620D8D*/ popad
/*620D8E*/ sub eax, 0x22202137
/*620D93*/ add eax, 0x736C1130
/*620D98*/ add eax, ecx
/*620D9A*/ sub eax, 0x736C1130
/*620D9F*/ add eax, 0x22202137 //执行完之后 eax=func addr ,ecx=dll base
/*620DA4*/ stc
GetApi1=620DA4
8 8个值都搞定,开始写脚本
GetApi1= 620DA4 GetApi2= 6210EC BaseZero=6244F2 GetBase=6200E4
GetIat1= 623CCB GetIat2=57BB64
FillCall1= 624315 FillCall2=57BB64
脚本跟上面的模板基本一样,见附件,运行完脚本,uif fix,这次用od dump,不重建输入表,因为有时数据量太大有过死机经验,所以dump之后用 ImportREC fix.
1.加壳后
2.uif fix图
3.oddump图
4.重建iat
5脱壳后报下图错误,run time error
6.脱壳后(带nop)
text:0047230B call __IsNonwritableInCurrentImage
.text:00472310 test eax, eax
.text:00472312 pop ecx
.text:00472313 jz short loc_472320 // nop
.text:00472315 push [esp+arg_0]
.text:00472319 call ds:off_4BA658
.text:0047231F pop ecx
把对IsNonwritableInCurrentImage 这个函数的所有调用后的 je/jz nop
具体原因可上网查 什么vs2005新增特性,加壳后把 只读段跟代码段等等全合并了,导致只读段变成了可写段
附件是 加壳前,加壳后,脱壳后 的 测试程序 和 脱壳脚本
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
上传的附件: