谈谈VMP2.05的脱壳修复以及跨平台
本帖最后由 wgz001 于 2011-5-12 21:35 编辑VMP的外壳部分不能说难,只能说烦。特别是IAT的修复,由于函数调用的错位(并非是规则的call xxxx nop或者nop call xxxx系列)、寄存器使用的随机性、大量的乱序等,使得写脚本修复是个非常蛋疼的事情,而且往往会修复错,修复漏,而VM代码里的IAT更是无法得到修复。于是,目前公开的方法中,最简单的也是最治本的修复方式,就是先生成张调用关系的表,然后写个DLL去读,在程序运行前,修正一些值,来达到跨平台。其他的,还真想不到有更完美的方法。或许牛人间有更好的处理方法,我这种菜鸟实在想不到了。。
至于修复IAT,牛人的文章不少了,比如kissy牛的2个视频,以及aa1ss2牛的学习笔记,这里就不再多说了。
下面写下个人脱壳的记录,当算是个简单的笔记,跟步骤的总结,没什么技术含量,一切都是按牛人的脚步学习的。
试练品是个XP记事本,加上VMP外壳的全选项,顺便把OEP处的代码也给和谐掉。下面开始脱壳:
1.CTRL+G,VirtualProtect,在VirtualProtectEx的调用处下好断点:
7C801AE3 E8 75FFFFFF call kernel32.VirtualProtectEx
接着,在数据窗口定位到代码段,比如,这个样本的为01001000.主要是为了观察是否已经解码,找返回的时机。
2.一直F9,一直到代码段解码,取消断点。ALT+F9,返回到程序代码区。
3.HOOK VM_RETN,找程序的入口。
先来看下VM_RETN这条handler长成啥样吧:
01071753 Main pushad //VM_Retn
01071754 Main xchg ax,di ; EAX=0000F694, EDI=0006000E
01071756 Main jmp NOTEPAD_.0106FB88
0106FB88 Main sub ch,bl ; ECX=01072F53
0106FB8A Main mov esp,ebp
0106FB8C Main movzx cx,al ; ECX=01070094
0106FB90 Main xadd cl,ah ; EAX=00009494, ECX=0107008A
0106FB93 Main call NOTEPAD_.0107152C
0107152C Main mov ecx,dword ptr ss: ; ECX=00000246
01071530 Main xchg eax,esi ; EAX=01018E26, ESI=00009494
01071531 Main mov eax,dword ptr ss: ; EAX=0006FFB0
01071535 Main jmp NOTEPAD_.0107147E
0107147E Main bswap cx ; ECX=00000000
01071481 Main aaa ; EAX=0006FF00
01071482 Main mov eax,dword ptr ss: ; EAX=F035F4BE
01071486 Main pushfd
01071487 Main adc esi,A8EF9681 ; ESI=A8F02B15
0107148D Main jmp NOTEPAD_.0107163C
0107163C Main std
0107163D Main mov esi,dword ptr ss: ; ESI=0100CB51
01071641 Main xor di,dx ; EDI=00060248
01071644 Main neg di ; EDI=0006FDB8
01071647 Main shld si,sp,cl
0107164B Main btr bp,0E ; EBP=0006BF84
01071650 Main mov edi,dword ptr ss: ; EDI=524DB0B9
01071654 Main sub esi,734AB4D1 ; ESI=8DB61680
0107165A Main lea ebp,dword ptr ds: ; EBP=2DB839B8
01071660 Main mov esi,dword ptr ss: ; ESI=80A871EF
01071664 Main inc bp ; EBP=2DB839B9
01071667 Main rcr bp,cl
0107166A Main mov ebp,dword ptr ss: ; EBP=E137F11A
0107166E Main cmp bl,bh
01071670 Main movzx bx,dl ; EBX=C71A0046
01071674 Main mov cl,byte ptr ss: ; ECX=00000098
01071678 Main bt bx,0F
0107167D Main mov ebx,dword ptr ss: ; EBX=05B50161
01071681 Main rol dh,cl
01071683 Main mov ecx,dword ptr ss: ; ECX=213EBC0B
01071687 Main dec dx ; EDX=00000245
0107168A Main mov edx,dword ptr ss: ; EDX=DA74A7B8
0107168E Main clc
0107168F Main add esp,30
01071692 Main je NOTEPAD_.0106FD6C
01071698 Main popfd
01071699 Main pushad
0107169A Main mov dword ptr ss:,AD22F63E
010716A1 Main push dword ptr ss:
010716A4 Main pushfd
010716A5 Main push dword ptr ss:
010716A9 Main retn 2C直接在010716A9下好断点,F9运行,断下后取消断点,F7,来到下面的地方:
0102A32B 9C pushfd
0102A32C 9C pushfd
0102A32D 8F0424 pop dword ptr ss:
0102A330 F5 cmc
0102A331 81F5 EA0E31E1 xor ebp,E1310EEA
0102A337 ^ E9 29E3FEFF jmp NOTEPAD_.01018665
0102A33C AF scas dword ptr es:
4.继续F7几次,就可以来到下面的地方:
01007568 68 BA750001 push NOTEPAD_.010075BA
0100756D 64:A1 00000000 mov eax,dword ptr fs:
01007573 50 push eax
01007574 8B4424 10 mov eax,dword ptr ss:
01007578 896C24 10 mov dword ptr ss:,ebp
0100757C 8D6C24 10 lea ebp,dword ptr ss:
01007580 2BE0 sub esp,eax
01007582 53 push ebx
01007583 56 push esi
01007584 57 push edi
01007585 8B45 F8 mov eax,dword ptr ss:
01007588 8965 E8 mov dword ptr ss:,esp
0100758B 50 push eax
0100758C 8B45 FC mov eax,dword ptr ss:
0100758F C745 FC FFFFFFFF mov dword ptr ss:,-1
01007596 8945 F8 mov dword ptr ss:,eax
01007599 8D45 F0 lea eax,dword ptr ss:
0100759C 64:A3 00000000 mov dword ptr fs:,eax
010075A2 C3 retn
这是多么熟悉的代码啊。因为VM并不支持CALL的嵌套,这就给我们可趁之机。先dump一份。
5.下面就来补代码:
此时看堆栈窗口:
0006FFB8 0101654F 返回到 NOTEPAD_.0101654F 来自 NOTEPAD_.0101BFA7 //CALL的出口,注意
0006FFBC 01001898 NOTEPAD_.01001898
0006FFC0 00000070
于是,在代码段,随便找个地址,补入如下代码:
01008890 > 6A 70 push 70
01008892 68 98180001 push dumped1.01001898
01008897 68 4F650101 push dumped1.0101654F
0100889C ^ E9 C7ECFFFF jmp dumped1.01007568
010088A1 90 nop
补完后,保存一下,顺便把OEP设置成8890.
OK,程序能够正常。不过此时还不能跨平台。
6.下面开始修复跨平台,这部分我就不说了,自己看牛人的文章跟视频,跑份IAT调用的对应关系表,部分如下:
1013360,DE02099D,comdlg32,PageSetupDlgW
102B3D6,E53997CF,comdlg32,ChooseFontW
1014ABD,4443359,comdlg32,PrintDlgExW
103107A,F8A5F191,comdlg32,ReplaceTextW
1019B4F,8F70D755,comdlg32,GetFileTitleW
101403B,AB122473,comdlg32,FindTextW
101CF16,7495EB47,comdlg32,CommDlgExtendedError
1016371,D8A0FB6B,comdlg32,GetOpenFileNameW
1014D62,BED81FBF,shell32,DragAcceptFiles
10230B9,A0524363,shell32,DragFinish
1022832,F10CB8C9,shell32,DragQueryFileW
101D8F2,4AD7BE0D,comdlg32,GetSaveFileNameW
10314C3,F6D99DC5,shell32,ShellAboutW
102A015,5B0DDC5B,winspool,GetPrinterDriverW
1023366,181A1537,winspool,OpenPrinterW
7.下面就简单的写个DLL,来完成工作,原理很简单,比如某个函数调用中,关键代码如下:
01031019 BA A2390001 mov edx,dump.010039A2
010328AE 8B92 FD430000 mov edx,dword ptr ds: //注意这里
010328BE 8D92 DB73ECD6 lea edx,dword ptr ds: //这句出真实的地址
ds:=A594FBB8
edx=010039A2 (dump.010039A2)
找下对应的关系表:
1007D9F,D6EC73DB,kernel32,GetCommandLineW
可知,在地址01007D9F中存着解密的key,而解密的key为多少,即为真实的API地址-表中对用的key
其实也就是这句的逆过程:lea edx,dword ptr ds:
由于不同的平台,API的地址有差异,所有想要跨平台,就需要修正这个解密的key值,方法是在当前的用户平台上,先获取当前用户平台的API地址,再减去表中对应的那个KEY值即可。
知道了原理,就可以写代码了,代码写的很挫,仅供参考:
#include <windows.h>
#include <stdio.h>
extern "C" BOOL __declspec(dllexport) FkVMPIAT();
BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call,LPVOID lpReserved)
{
if (ul_reason_for_call==DLL_PROCESS_ATTACH)
{
FkVMPIAT();
}
return TRUE;
}
BOOL FkVMPIAT()
{
DWORD dwImageBase=(DWORD)GetModuleHandle(NULL);
PIMAGE_DOS_HEADER pDosHeader=(PIMAGE_DOS_HEADER)dwImageBase;
if (pDosHeader->e_magic!=IMAGE_DOS_SIGNATURE)
{
return FALSE;
}
PIMAGE_NT_HEADERS pNtHeader=(PIMAGE_NT_HEADERS)(dwImageBase+pDosHeader->e_lfanew);
if (pNtHeader->Signature!=IMAGE_NT_SIGNATURE)
{
return FALSE;
}
UINT NumOfSection=pNtHeader->FileHeader.NumberOfSections;
PIMAGE_SECTION_HEADER pSectionHeader=(PIMAGE_SECTION_HEADER)((UINT_PTR)pNtHeader+0x18+
(UINT_PTR)(pNtHeader->FileHeader.SizeOfOptionalHeader));
for(int i=0;i<NumOfSection;i++)
{
DWORD dwSecVA=pSectionHeader->VirtualAddress+dwImageBase;
DWORD dwSecSize=pSectionHeader->Misc.VirtualSize;
DWORD dwOldProtect;
VirtualProtect((LPVOID)dwSecVA,dwSecSize,PAGE_EXECUTE_READWRITE,&dwOldProtect);
pSectionHeader=(PIMAGE_SECTION_HEADER)((DWORD)pSectionHeader+sizeof(IMAGE_SECTION_HEADER));
}
FILE *fp;
fp=fopen("FkIAT.txt","r");
if (fp==NULL)
{
MessageBox(NULL,"请把fkiat.txt放当前目录下!","ximo",MB_OK|MB_ICONINFORMATION);
}
char buf={0};
while(!feof(fp))
{
fgets(buf,256,fp);
DWORD dwIATAddr,dwKey;
char szIATAddr={0};
char szKey={0};
char szDllName={0};
char szFunName={0};
char *p;
int i=0;
if ((p=strchr(buf,','))==NULL)
{
continue;
}
while(buf!=',')
{
szIATAddr=buf;
i++;
}
sscanf(szIATAddr,"%08x",&dwIATAddr);
p=strchr(buf,',');
i=0;
while(p!=',')
{
szKey=p;
i++;
}
sscanf(szKey,"%08x",&dwKey);
p=strchr(p+1,',');
i=0;
while(p!=',')
{
szDllName=p;
i++;
}
p=strchr(p+1,',');
i=0;
while(p!=0x0A)
{
szFunName=p;
i++;
}
//特殊处理下,不知道是否还有其他动态库也是如此的,想不出好的,只能硬编码了,汗
if (strcmp(szDllName,"winspool")==0)
{
wsprintf(szDllName,"%s","winspool.drv");
}
//特殊处理2
/*
if ((p=strchr(szDllName,0x20))!=0)
{
wsprintf(szDllName,"%s",p+1);
}*/
if ((p=strstr(szDllName,"offset"))!=0)
{
wsprintf(szDllName,"%s",p+7);
}
HMODULE hdll=GetModuleHandle(szDllName);
if (!hdll)
{
hdll=LoadLibrary(szDllName);
}
DWORD dwIAT=(DWORD)GetProcAddress(hdll,(LPCSTR)szFunName);
DWORD dwWrite=dwIAT-dwKey;
__asm
{
pushad
mov eax,dwIATAddr
mov ecx,dwWrite
mov dword ptr ,ecx
popad
}
}
fclose(fp);
return TRUE;
}
8,最后就是用PE工具把这个DLL加到脱壳后文件的导入表中了。这个用LORDPE,CFF等工具都能搞定了。
然后把3个文件:脱壳后文件,DLL,以及对应关系的txt这3个文件放在一起,就OK了。放到其他平台测试一下,OK了。
脱壳也就到此结束了。
呃..这个我怎么觉的我看过哈~好像是超哥的~支持ximo大大~ 很好很强大,给分吧,没了 如果是教程那该多好啊! 谢谢分享! 支持了 貌似超哥发过了呃··· 给的很详细 学习~~ 回复 stucky 的帖子
想知道 导入了dll后 他是怎么加载的就算导入dll也应该有导出函数吧脱壳后的程序 怎么加载 导出函数修复iat的呢? 好东西。。支持+收藏了 很好很强大,给分吧,没了