加壳原理04 - zlib压缩壳案例

作于: 2021 年 10 月 20 日,预计阅读时间 6 分钟

前言

本文在前一篇基础上,写一个使用 zlib 的压缩壳案例。

0x01 zlib 解压

1.1 概述

关于 zlib 的用法找了这些参考资料:

尝试了 zlib、lzo、Windows Compression API,对压缩和解压 API 的基本模式的基本认识大概是这样:

1.2 内存布局

压缩后的 .packed 节在头部留出 8 个字节,分别保存压缩后大小和压缩前大小,以便一次分配好内存完成解压。

偏移大小内容
0DWORD小端序,压缩后大小
4DWORD小端序,压缩前大小
8可变压缩后的数据

1.3 解压代码

解压过程在加载 PE 之前,找到 .packed 节后,开始读取头部大小,并调用解压代码。

if (packed != NULL) {
    DWORD compressed_size = *((DWORD *)packed);         // compressed size little-endian
    DWORD decompressed_size = *((DWORD *)(packed + 4)); // decompressed size little-endian
    void *compressed = (void *)(packed + 8);            // compressed buffer
    void *decompressed = malloc(decompressed_size);     // decompressed buffer
    if (decompressed == NULL) {
        MessageBoxA(NULL, "memory allocate failed", "malloc", MB_OK);
        return 0;
    }

    decompress(compressed, compressed_size, decompressed, decompressed_size);

    void (*entrypoint)(void) = (void (*)(void))load_PE(decompressed);
    entrypoint();

    return 0;
}

应该没有太多疑问。接下来的是解压代码。

void decompress(void *compressed, size_t length, void *decompressed, size_t decompressed_length) {
  z_stream inflate_stream;
  inflate_stream.zalloc = Z_NULL;
  inflate_stream.zfree = Z_NULL;
  inflate_stream.opaque = Z_NULL;
  inflate_stream.avail_in = (uInt)length;
  inflate_stream.next_in = (Bytef *)compressed;
  inflate_stream.avail_out = (uInt)decompressed_length;
  inflate_stream.next_out = (Bytef *)decompressed;
  inflateInit(&inflate_stream);

  int err = inflate(&inflate_stream, Z_NO_FLUSH);
  if (err != Z_STREAM_END) {
    inflateEnd(&inflate_stream);
    MessageBoxA(NULL, "zlib decompression failed", "zlib", MB_OK);
    return;
  }
  inflateEnd(&inflate_stream);
  return;
}

定义 inflate 流:

使用 inflateInit() 初始化流,然后调用 inflate() 解压。inflate() 会返回错误码,如果长度正好,会返回 Z_STREAM_END。如果输出 buffer 长度不足,但解压成功,会返回 Z_OK。其他情况会返回错误码。因为这里很清楚给定的压缩前长度,解压必定返回 Z_STREAM_END,其他情况都有问题,所以只做了一个判断。

对于其他情况,错误码可以用 zError 获取错误描述。

解压结束后要使用 inflateEnd() 关闭流。

0x02 zlib压缩

因为使用 python 写加壳机,就不用这么麻烦了。

在处理 .packed 节的时候,使用 structzlib 两个 python 自带的库就能完成压缩和填充头。

在脚本头部添加两句 import

import struct
import zlib

然后修改加壳代码中,添加 .packed 节的代码。

# add packed section
with open('example.exe', 'rb') as f:
    file_content = f.read()
    origin_length = len(file_content)
    compressed = zlib.compress(file_content, 9)
    compressed_length = len(compressed)
    section_content = struct.pack('<II', compressed_length, origin_length)
    section_content += compressed

    packed_section = lief.PE.Section('.packed')
    packed_section.content = list(section_content)
    packed_section.characteristics = (lief.PE.SECTION_CHARACTERISTICS.MEM_READ |
                                      lief.PE.SECTION_CHARACTERISTICS.CNT_INITIALIZED_DATA)
    output.add_section(packed_section)

可以看到使用 zlib.compress 就完成了压缩,不用原始 zlib 流那么麻烦。

struct.pack 指定了小端序,两个4字节int,分别填写压缩后大小和原始大小,连接压缩后的数据,填充进.packed 节。

就这样,压缩功能成功完成。

0x03 成果展示

compression-packer

image-20211020154513713

image-20211020154539546

总结

偷懒了,用了一些 msvcrt 的函数,比如 malloc,要加个 -lmsvcrt 链接选项。最终成品压缩率还可以,从107KB 压缩到了 49KB,zlib 不负期望。

写好壳程序之后,不管是加密还是压缩都是很容易的事情(指单纯做个简单实现),但问题依然存在:

下一篇还没想好做什么,得先继续学习充实下自己,找个方向。

/逆向/ /汇编/ /c++/ /python/ /windows/ /security/ /binary-analysis/