【原创】手写PE文件,打造史上最小LZMA解压DLL

因程序需求,需要在VB中调用LZMA解压数据,经过N天研究出此成果~

什么是LZMA:LZMA应该是目前世界上数一数二的压缩算法——压缩时相同的时间得到压缩比最高,解压时速度极快且几乎不占内存。如果你对LZMA算法并无耳闻,那么7z总听说过吧。。没错,LZMA即7z作者发明的,7z使用的算法。什么?7z也没听说过?这样吧,你在网上下的软件,比如旺旺、暴风影音、人人桌面,它们的安装程序都是清一色的NSIS,打包的压缩算法是LZMA。。

用到的工具:

文件、内存编辑器:winhex
汇编器、调试器:ollydbg
upx shell

参考的资料:

看雪论坛《加密与解密》
LZMA SDK
NSIS Source
UPX Source

为什么要使用upx shell呢?在这里偷偷告诉大家,upx shell是支持用lzma算法压缩exe、dll文件的,所以呢,其生成的加壳文件中必定有最最精简最最完美的lzma解压代码,我们有足够的理由相信,不管你用哪个版本的lzma sdk来编译,出来的C语言版本都不会有upx做好的asm版本要好~

废话不再多说,下面来一步步操作:

1、自然是随便找个EXE文件用UPX加壳,注意UPX核心要选择2.92b(只有这个版本是支持LZMA的),高级选项中勾选LZMA。

2、将加壳的EXE用OD打开。从入口处往下看,可以看到有句mov [ebx],30002,然后后面是几个nop对齐,然后是熟悉的push ebp,这即是upx lzma核心解密函数的入口。把这个地址记为0,数到0x0A85,可以看到有对应的pop ebp,这2694字节就是upx lzma解密函数(没有写ret),是由4.43版的LZMA SDK修改得来的,函数声明即4.43版LZMA SDK中的LzmaDecode:

int LzmaDecode(CLzmaDecoderState *vs, const unsigned char *inStream, SizeT inSize, SizeT *inSizeProcessed, unsigned char *outStream, SizeT outSize, SizeT *outSizeProcessed);

与原版SDK不同的是,作为壳的loader,是不太允许去malloc的,所以这个函数被修改过,CLzmaDecoderState不再是原来的那个,而是包含了解压字典,这个结构的大小从加壳EXE中可以看出来有句lea ebx,[esp-3e80],所以我们如果要在C语言中内嵌这段代码,调用的时候也要开一个16K的空间才行。我这里说的都是结果,如果你对我怎么知道这些的感兴趣,可以下载upx的源代码看一下(注意是upx而不是upx shell哦)。

3、我所要编写的一个函数是这样的:

int LzmaDecode(byte *dest, long destLen, const byte *src, long srcLen);

于是我在OD中找到一段空余的空间,模仿upx调用解密函数前面一段的代码,自己编写以下代码:

push ebp ;堆栈框架
mov ebp,esp
sub esp,3e80 ;栈内开16K空间
push edi ;保存edi(Windows下C编译器与API规范规定被调用函数不得修改ebx ebp esi edi四个寄存器)
mov ecx,0fa0 ;准备给16K空间填0
xor eax,eax
lea edi,[esp+4]
rep stos
lea eax,[esp+4]
push eax ;outSizeProcessed
add eax,4
push [ebp+c] ;outSize
push [ebp+8] ;outStream
push eax ;inSizeProcessed
add eax,4
push [ebp+14] ;inSize
push [ebp+10] ;inStream
push eax ;lzmaDecodeState
mov dword [eax],20003 ;硬编码LZMA基本属性,lc=3 lp=0 pb=2
push eax ;dummy call,这里本来是写Call XXXX的,但是函数直接接下去了,要留出一格返回地址的空间
jmp XXXXXXXX ;这里插入LzmaDecode本身的代码
add esp,20 ;相当于ret,7个参数加上一个dummy的返回地址是0x20字节
pop edi ;恢复edi
leave ;出堆栈框架,返回
retn 10

4、用winhex打开调试程序的内存,复制以上汇编好的和2694字节的解压机器码按粘贴到新文件中。

5、到了这一步,我们就已经可以用masm或者vc写一大堆dd把刚才弄好的数据直接汇编成dll了。不过我们再来一点新的挑战——手写DLL。因为编译器总会给生成的文件弄一大堆不需要的东西,比如重定位段(这个程序是不需要重定位的),而手写的话就可以把文件做到最精简。

注意:下面的内容是最让人抓狂的PE文件结构。。。

6、首先,弄清楚我们需要什么。刚才写好的那段代码总长度是2757字节,加上简单的一句DllMain(mov eax,1 ret 0c),就是2765字节,也就是0xACD,所以我们的代码段长度要对齐到0xC00。

然后我们需要一个导出表,导出表中只有一个函数。为了节省空间,我把导出表放到了dos stub的地方。

最后文件的布局就是:

0x0000:DOS文件头(RVA:0x0000)
0x0040:导出表(原DOS代码)
0x00B0:PE文件头
0x01A8:区段表
0x0200:代码段- LzmaDecode(RVA:0x1000)
0x0CC5:DllMain
0x0CD0:结束(最后有4字节的0,为了美观还是对齐到十位吧~)

文件共3280字节。

7、需要注意的有一个地方,就是区段表中VirtualSize和SizeOfRawData的填写。很长的一段时间我的DLL无法正常加载(LoadLibrary失败,LastError是BAD_EXE_FORMAT什么的),折腾好久也不好,最后LordPE修复一下好了,把修复的和原来的比对了一下,发现SizeOfRawData尽管资料上说是要填对齐值,但实际上看来这个值是不得超过文件结尾的,也就是说我开始填了0x0C00,但加载的时候NtLoader看来是遇到了EOF,造成加载失败。

最终文件可以在这里下载

LzmaDecode

8 Replies to “【原创】手写PE文件,打造史上最小LZMA解压DLL”

    1. int LzmaDecode(byte *dest, long destLen, const byte *src, long srcLen);
      stdcall调用约定,在VB中使用比较方便,C的话用LoadLibrary和GetProcAddress

    2. int LzmaDecode(byte *dest, long destLen, const byte *src, long srcLen);
      stdcall调用约定,VB非常方便,C用LoadLibrary和GetProcAddress

Leave a Reply

Your email address will not be published. Required fields are marked *