-
-
[原创]看雪未知壳
-
发表于: 2017-5-1 11:01 4201
-
准备工具:
OD
PEID
Import REC
OllySubScript
壳的套路知识:
①. PBpack(加壳程序)这个我们看不到的
1.1 初始化PE信息
1.2 代码段加密
1.3 Stub代码修复重定位
1.4 添加区段
②. Stub(解密程序)我们分析的就是这个
2.1 手动获取kernel32基地址 (因为GetProcAddress在kernel32.dll里面)
2.2 手动模拟GetProcAddress函数 (拥有这个函数就拥有所有API)
2.3 解密代码
2.4 跳转到OEP
1.准备工作
PE工具查看有没有切入点
通过链接器可以判断程序应该是VC6.0
并且发现程序导入表基本没有任何API,那就是通过kernel32.dll手动去导出表手动获取API
2. OD分析程序流程(我采用的是一步步跟,因为你必须了解作者模拟的GerProcAddress函数,否则后面混淆代码你看不懂)
2.1 F7继续跟踪
2.2 套路第一步获取Kenrel32.dll
这是一段通用获取kernel32.dll的代码,你们可以保存起来
0047A0B5 > 64:A1 30000000 mov eax,dword ptr fs:[0x30] //PEB
0047A0BB 8B40 0C mov eax,dword ptr ds:[eax+0xC] //PEB_LDR_DATA
0047A0BE 8B40 0C mov eax,dword ptr ds:[eax+0xC] //InInitializationOrderModuleList.Flink
0047A0C1 8B00 mov eax,dword ptr ds:[eax] //ntdll.dll
0047A0C3 8B00 mov eax,dword ptr ds:[eax] //kernel32.dll
0047A0C5 8B40 18 mov eax,dword ptr ds:[eax+0x18] //kernel32的基地址
不过在win7还是XP下kernel32基地址或许并不是在第二位而是在第三位。可以使用这段代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | .code start: assume fs:nothing push ecx push edi push esi xor ecx,ecx mov esi,[fs:30h] ;Ptr32 _PEB mov esi,[esi+0ch] ;Ptr32 _PEB_LDR_DATA mov esi,[esi+1ch] ;Get InInitializationOrderModuleList.Flink next_module: mov eax,[esi+8h] ;eax=kernel32.DLL地址 00111E68 76B50000 kernel32.76B50000 mov edi,[esi+20h] ;BaseDllName mov esi,[esi] ;下一个模块 cmp [edi+12*2],cx ;模块结尾是0 jne next_module ;继续循环 pop esi pop edi pop ecx ret end start |
2.3 套路第二步通过导出表获取到GetProcAddress或则手写GetProcAddress
F7跟进函数内部,发现代码里有很多没用的混淆代码(请眼熟这个函数的代码)
C语言版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | DWORD MyGetProcAddress( DWORD dwBase, char * szName) { //1 获取导出表结构 unsigned char * buf = (unsigned char *)dwBase; PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)buf; PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + buf); PIMAGE_DATA_DIRECTORY pExportDir = (pNt->OptionalHeader.DataDirectory + 0); PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY) (pExportDir->VirtualAddress + buf); //2 解析导出表,根据函数名获取函数地址。 DWORD FunNumber = pExport->NumberOfFunctions; DWORD NameNumber = pExport->NumberOfNames; PDWORD Eat = (PDWORD)(pExport->AddressOfFunctions + buf); PDWORD Ent = (PDWORD)(pExport->AddressOfNames + buf); PWORD OrderTable = ( PWORD )(pExport->AddressOfNameOrdinals + buf); for ( DWORD i = 0; i < NameNumber;i++) { char * FunName = ( char *)(Ent[i] + buf); if ( strcmp (FunName, szName) == 0) { DWORD FunOrder = OrderTable[i]; return Eat[FunOrder] + dwBase; } } return 0; } |
汇编版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | _getApi proc _hModule,_lpApi local @ret local @dwLen pushad mov @ret,0 ;计算API字符串的长度,含最后的零,经典求长度指令 mov edi,_lpApi ;edi等于GetProcAddress mov ecx,-1 ;FFFFFFFFFF方便计算 xor al,al ;每次用eax跟edi对比等于0表示找到字符串结束符号 cld ;复位标志寄存器的方向标志, 以让串地址由低到高 repnz scasb ;edi指向的内容字符串结束0就结束,否则edi每次+1(byte) ; mov ecx,edi ;ecx等于LoadLibraryA ; sub ecx,_lpApi ;字符串占的内存位置一减就得到GetProcAddress的字节数,因为它们两个字符串是连在一起的 not ecx ;算出来结果取反 mov @dwLen,ecx ;从pe文件头的数据目录获取导出表地址 mov esi,_hModule ;esi指向kernel32基地址 add esi,[esi+3ch] ;PE头 assume esi:ptr IMAGE_NT_HEADERS mov esi,[esi].OptionalHeader.DataDirectory.VirtualAddress ;导出表VA add esi,_hModule ;基地址+VA等于=内存中真正地址 assume esi:ptr IMAGE_EXPORT_DIRECTORY ;查找符合名称的导出函数名 mov ebx,[esi].AddressOfNames add ebx,_hModule ;基地址+VA xor edx,edx ;清零 .repeat push esi mov edi,[ebx] ;edi输出表中当前函数名字 add edi,_hModule mov esi,_lpApi ;esi函数名字首地址 mov ecx,@dwLen ;dwLen是长度 repz cmpsb ;对比API . if ZERO? pop esi ;恢复Esi jmp @F ;查找该函数的地址索引 .endif pop esi add ebx,4 ;下一个函数 inc edx .until edx>=[esi].NumberOfNames ;每次edx自增,跟函数名称数量做对比 jmp _ret @@: ;函数名称索引 -> 序号索引 -> 地址索引 ;公式: ;API’s Address = ( API’s Ordinal * 4 ) + AddressOfFunctions’ VA + Kernel32 imagebase sub ebx,[esi].AddressOfNames ; 上面的 repz cmpsb 那里,如果匹配的话, sub ebx,_hModule ; esi 就指向了下一个函数的首地址,所以要先减掉它。 shr ebx,1 ; 要除以 2 ,还是因为 repz cmpsb 那行 add ebx,[esi].AddressOfNameOrdinals ; AddressOfNameOrdinals add ebx,_hModule ; 别忘了基地址 movzx eax,word ptr [ebx] ; Now, eax = API’s Ordinal shl eax,2 ; 要乘以 4 才得到偏移 add eax,[esi].AddressOfFunctions ; + AddressOfFunctions’ VA add eax,_hModule ;从地址表得到导出函数的地址 mov eax,[eax] ; 得到函数的 RVA add eax,_hModule mov @ret,eax _ret: assume esi:nothing popad mov eax,@ret ret _getApi endp |
2.4 再开一个OD按照我们正常脱壳方式来
2.5 顺利达到OEP,发现程序入口点是标准的VC6.0
我们拿一个正常的VC6.0程序来对比来:
通过两张图对比,很明显导入表给加密了。
F7跟进call发现
跟踪重点:
我们只要找到原始IAT函数地址就可以阻止壳程序修改,把壳给脱掉
程序要修改IAT必然会用到两个API(当然这两个函数也可以自己手写):
LoadLibraryA/W,GetProcAddress
如果程序要手写这两个API前提是需要获取到Kernel32基地址
直接硬件断点
这段地址空间都是new出来的,无法直接保存
开启Run跟踪继续跟踪代码(程序每执行一行代码就jmp或则call,我们没必要每一句都详解看)
关键点就是操作导出表部分
Kernel32基地址
PE头
导出表大小
导出表RVA
我们发现这个代码貌似跟前面分析的一模一样
我们关注的是在哪里获取到的IAT,然后再哪里把IAT给修改了。
存入函数地址到memcpy的首字节
现在edx已经被修改成加密后的了
继续跟踪发现程序到回起点
关键两句代码:
第一句:
005814DC mov dword ptr ds:[ecx+eaw-0x4],edx
修改成:
005814DC mov ecx,edx
第二句:
00580EE2 mov eax,dword ptr ss:[ebp-0x58]
修改成
00580EE2 mov eax,ecx
2.7 由于这段地址空间都是new出来的,就算你修改了也无法直接保存,所以要找到new出来的基地址+偏移就可以
第一处修改
第二处修改
发现edx保存的就是正确的IAT地址
那样我们直接到OEP脱壳即可
3. 脱壳脚本编写
1. 0047148B原始OEP下硬件断点(因为代码段也是加密的,下软件断点会填充CC字节,会打乱原来恢复的数据)
2. 找到0047A37D VirtualAlloc获取代码基地址(因为那段空间都是new出来的,基地址是每次都在变)
3. 基地址+14DC处下硬件断点
4. 运行到基地址+14DC处再进行下一步并取消此处硬件断点
5. 基地址+14DC处修改代码,mov ecx,edx
6. 基地址+0EE2处修改代码, mov eax,ecx
7. OEP处dump文件
8. 修复文件
脚本代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | //第一步清除所有断点 //清除所有硬件断点 BPHWC //清除所有软件断点 BC //清除所有内存断点 BPMC //设置OEP断点 BPHWS 0047148B, "x" //设置获取基地址断点(VirtualAlloc) BPHWS 0047A37F, "x" //运行到基地址 loop1: RUN CMP 0047A37F,eip //判断是否到了47A37F JNE loop1 //不是继续循环 //保存基地址,保存两处要修改的地址 MOV vBase,eax //基地址 MOV vCode1,vBase ADD vCode1,14DC //基地址+14DC=第一处要修改的地址 MOV vCode2,vBase ADD vCode2,0EE2 //基地址+0EE2=第二处要修改的地址 //在第一处下硬件执行断点 BPHWS vCode1, "x" //运行到第一处修改代码处 loop2: RUN CMP vCode1,eip JNE loop2 //修改代码 //mov ecx,edx ASM vCode1, "MOV ECX,EDX" MOV vCode,vCode1 ADD vCode,2 FILL vCode,2,90 //在第二处下硬件执行断点 BPHWS vCode2, "x" //运行到第二处修改代码处 loop3: RUN CMP vCode2,eip JNZ loop3 //修改代码 //mov eax,ecx ASM vCode2, "MOV EAX,ECX" ADD vCode2,2 FILL vCode2,1,90 //清除断点 BPHWC vCode1 //BPHWC vCode2 //运行OEP loop4: RUN //运行到OEP cmp 0047148B,eip JNZ loop4 MSG "到底OEP" |
致谢
感谢15PB老师们的辛勤栽培!
赞赏
- [分享]VMP学习笔记之万用门(七) 27343
- [分享]VMP学习笔记之壳的重定位修复(五) 12335
- [分享]VMP学习笔记之壳基础流程(一) 17738
- [分享]VMP学习笔记之Handle块优化与壳模板初始化(四) 10277