最近由于工作的需要,逆向了一款带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释放完本内存,就跳过去了;至此结束;
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)