首页
社区
课程
招聘
PEtite2.4脱壳
发表于: 2020-11-6 18:59 3261

PEtite2.4脱壳

2020-11-6 18:59
3261

​ 最近由于工作的需要,逆向了一款带PEtite壳的病毒,2.4版本的,话不多说开干。

样本信息:

md5: BCEAEEFF17874D3323237BE240D9C5E4

【详细过程】

查一下壳和入口点

 

 

 

入口点关注标红的内容( 真是句句指令有大用途 ):

 

第二条指令pusha, 压入的eax后面也有用途;

 

seg000:00401785处的 0A6d56304h 是PE Header校验CRC;

 

004017A7处的ESI指向(需要解密的)源数据;

 

383h是解密长度,

 

mov edi, eax; // 堆地址为解密目的地址

 

sub_4017bf是解密函数;上面紧邻的push eax,压入的是VirtualAlloc申请的堆地址,下面retn时会进去堆执行代码;

解密函数:

 

主要逻辑就是从esi取值 异或长度(传进来的ebx),然后ebx递减,写入edi(edi指向上面申请到的堆内存);

 

下面还有重复字节的展开;

 

 

具体的就不一一赘述了,直接上我逆成c语言的形式吧, 这样看着方便些;

 

这里提一下sub_401857函数

 

 

有没有看着很熟悉的。

 

幸亏我前面研究upx解压缩时看到了徐大神的帖子(https://bbs.pediy.com/thread-85348.htm),我c中直接搬过来了;

 

下面就是我整理的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
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
int getbit(unsigned int *pcom_dword, unsigned int **ppsrc)
{
    int temp;
    temp = ((*pcom_dword)>>31)&1; //得到 符号位
    (*pcom_dword) <<= 1;
    if(0 != (*pcom_dword))
    {
        *pcom_dword = **ppsrc;
        temp = ((*pcom_dword)>>31)&1;
        (*pcom_dword) <<= 1;
        *pcom_dword += ((unsigned int)*ppsrc >= 0xFFFFFFFC ? 0 : 1);
        (unsigned int)*ppsrc += 4;
    }
    return temp;
}
 
src    =    0x401448h
declen = 0x383h
 
sub_401857(psrc, pdst, declen)
{
    index = 0;
    lastlen    =    declen;
    edx     =    0;
    ecx        =    0;   
    do{       
        *(char*)pdst++    = (*(char*)psrc++ ^ lastlen);
        lastlen--;
__loop:
        if( lastlen <= 0 )
        {
            break;
        }
        if( getbit(&edx, &psrc) ==0 )
        {
            continue;
        }
        ecx++;
        do{
            temp = getbit(&edx, &psrc);
            ecx  <<= 1;
            ecx  += temp;
        }while( getbit(&edx, &psrc) )
        ecx -= 3;
        if( ecx < 0 )
        {           
            eax = var14;
            ecx++;
        }
        else{
            eax    = ecx;
            ecx = 5;
            do{
                temp = getbit(&edx, &psrc);
                eax <<= 1;
                eax += temp;
            }while( ecx-- )           
 
            eax = ~eax;
            ebp += 1 + ((eax > -3A0h) : 0 ? 1);
            ebp += 1 + ((eax > -3FA0h) : 0 ? 1);
            var14 = eax;
        }       
        temp = getbit(&edx, &psrc);
        ecx <<=1;
        ecx += temp;
        temp = getbit(&edx, &psrc);
        ecx <<=1;
        ecx += temp;
        if( ecx == 0 )
        {
            ecx++;
            do{
                temp = getbit(&edx, &psrc);
                ecx  <<= 1;
                ecx  += temp;
            }while( getbit(&edx, &psrc) < 0)
            ecx += 2;
        }
        ecx += ebp;
        lastlen    -= ecx;
        if( lastlen < 0 )
            break;
        memcpy(pst, pdst+eax, ecx);
        pdst    += ecx;
        goto __loop;
 
    }while( 1 );   
__end:
    return ;
}

好了,当解密完成后,就如开始提到的进入堆内存执行;

 

我直接动态调试了,注意此时进入堆时的寄存器以及堆栈, 堆是下面这个样子的:

1
2
3
4
5
6
7
8
9
10
         001EFBC8   A6D56304
$ ==>    001EFBCC   00000000 
$+4      001EFBD0   00000000 
$+8      001EFBD4   001EFBF4 
$+C      001EFBD8   001EFBEC 
$+10     001EFBDC   7FFDB000 
$+14     001EFBE0 < 013D1779  92de1c7.EntryPoint
$+18     001EFBE4   00520000 
$+1C     001EFBE8   013D4000  92de1c7.013D4000
$+20     001EFBEC   77973C45  返回到 kernel32.77973C45 自 ???

ebp是开头 lea ebp, [eax-4000h] 指定的模块基址;

 

其中520000是申请的堆基址

 

以下是解密后的堆内存中的代码:

 

 

其中框中的会把sub_17bf解密函数拷贝到本模块的520383处;

 

这段代码是以一个结构数组进行代码解密恢复到原始PE处, 有调用520383解密函数,参数ebx, esi, edi都是结构数组中的数据;

 

 

其中结构数组位置在520340处;怕排板乱了直接用图片了,如下:

 

 

这个结构体数据最后一个成员是全零的,未贴上;

 

当DstRVA和SrcRVA一样时有个交叉处理的;

 

这个结构体数组结合上面解密函数就是下面的三个步骤:

1
2
3
4
5
6
7
8
9
10
11
ebx : 2fh
esi : 1414h+ImageBase
edi : 3000h
 
解密逻辑:
[esi] xor len  -->[edi]
 
[13d1414h]xor  -->[13d3000h]   解密后剩余清零
[13d1250h]xor  -->[13d2000h]
[13d1000h]xor  -->[13d1000h]
注:其中13d0000是动态调试时样本主PE的加载基址;

 

对应区块大小解密后,剩余的长度用0填充, 然后加 15h字节,处理下一个数组成员;

 

 

结构体数组最后一个有效成员的第四个DstSize成员为2001h, 最后一个字节控制了需不需要修复 远跳指令 E8, E9一类的, 这里解密完第三个直接进行指令修复了;

 

 

修复导入表;其中2B0EFA5Ch是加密的入口点,后面会运算出来(我这样理解的,应该是对的)

 

 

获取的地址还不老老实实的放回去,还要处理一下;处理成下面样子

1
2
3
4
5
push D124963B           
 
rol dword ptr ss:[esp],59
 
ret

 

这里获取完导入表地址,会遍历地址表,sbb,【esp+20】就是上面保存的2B0EFA5Ch,应该是根据函数地址位置运算出来一个偏移;

 

每一个模块导入表处理完都运算一次;处理出来一个最终的偏移FFFFCFF7

 

 

这里有个对PE.NtHeader 的校验;

 

 

修复FF15call的;

 

 

这里还有个恢复内存属性的操作,是以结构体最后一个字节决定的 movzx eax, byte ptr[esp+14h];

 

下面就是计算原始OEP了

 

[esp+1c] 保存的是导入表地址;

 

[esp+14h] 这个位置保存的就是获取模块导入表函数校验出来的偏移FFFFCFF7,然后eax+edx+9 就指向了13d1000处;

 

这里的9正是导入的9个函数,肯定不是巧合,上面的sbb还体现在这里了;

 

最后的jmp前 push的就是返回地址了,调用玩VirtualFree释放完本内存,就跳过去了;至此结束;


[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

上传的附件:
收藏
免费 1
支持
分享
最新回复 (1)
雪    币: 1935
活跃值: (4180)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
不错不错
2020-11-8 14:48
0
游客
登录 | 注册 方可回帖
返回
//