首页
社区
课程
招聘
[原创]看雪未知壳
发表于: 2017-5-1 11:01 4201

[原创]看雪未知壳

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老师们的辛勤栽培!


[注意]看雪招聘,专注安全领域的专业人才平台!

上传的附件:
收藏
免费
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册