吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 4471|回复: 11
收起左侧

[原创] 加壳原理笔记03 - 加载到固定基址

[复制链接]
weakptr 发表于 2021-10-20 19:55
本帖最后由 weakptr 于 2021-11-4 10:34 编辑

这是刚进论坛没多久的萌新一边学一边写的,内容可能还有错误,还望大佬指出,也请多包涵!

目录:

加壳原理笔记01:PE头格式、加载、导入表、重定位
加壳原理笔记02:加壳案例
加壳原理笔记03 - 加载到固定基址
加壳原理笔记04 - zlib压缩壳案例
加壳原理笔记05:利用图片隐藏
加壳原理笔记06:反调试技术入门
加壳原理笔记07:花指令入门
加壳原理笔记08:代码混淆技术入门




前言

距离上一篇加壳原理已经过去挺久了,这段时间稍微折腾了一下 nasm,尝试手工制作了 PE32 文件,积累了一些基本的知识吧。

所以现在继续学习加壳——如何对不支持 ASLR 的 PE32 程序进行加壳?

0x01 关于ASLR

ASLR是一项内存保护技术,用于防范内存损坏漏洞,比如缓冲区溢出。需要注意的是 ASLR 并不是 解决 了相关威胁,而是让利用相关的漏洞变得更加困难和具有挑战性。

ASLR 的全名是 Address Space Layout Randomization ,地址空间布局随机化技术。一个典型的 PE32 程序在没有 ASLR 支持的情况下, 地址空间布局是确定的:程序镜像总会加载到固定的地址。这个地址会在文件头里指定。攻击者可以利用这一特点来构造恶意数据,让存在内存损坏漏洞的程序按攻击者意图跳过或执行特定逻辑,造成安全威胁。

对应 ASLR 的地址空间布局随机化,程序需要再次编译来支持重定位 Relocation ,别无他法(大概)。

0x02 思路

对于加壳一个没有重定位,不支持 ASLR 的 PE32 程序,假设这个程序的基址是 0x04000000,原先的 VirtualAlloc 方式分配内存是行不通的。加壳后程序若开启 ASLR,则 0x04000000 可能已经存在其他模块,并不能保证这个基址可用。所以加壳后的程序必须也使用 0x04000000 这个基址,而且标记为不支持 ASLR,避免基址已经被其他模块使用造成加载器无法工作。

将加壳后程序的基址设置为固定的 0x04000000 又会产生新的问题:加载器的代码段不能放在 0x04000000 ,否则加载器运行时就会出现被被加载的代码覆盖的情况,导致程序跑飞。所以编译后的加载器所有 Section 都必须有一定的偏移,这个偏移值就是被加载程序的 Section 大小之和(对齐后)。而因此多出来的空间单独分成一个 Section ,正好用来放要加载的程序。

另外,还必须确认文件头大小是否一致,因为我们需要将被加载程序的文件头覆盖加载器的文件头。而最开始预留的空间必须分配为一个 Section,让 Windows 的加载器能顺利加载程序而不报“不是有效的Win32程序”错误。

内存布局示意图如下:

内存布局

所以加载器的加载步骤如下:

  1. 寻找被加载的 Section 。
  2. 复制文件头覆盖自己的文件头。
  3. 以自己的基址为被加载程序的基址,完成加载。

加壳机的加壳步骤如下:

  1. 解析被加壳程序,获取 Section 大小、文件头大小、对齐大小等信息。
  2. 生成加载器程序,根据上一步取得的数据计算出加载器 Section 的偏移和对齐。
  3. 合并被加壳程序和加载器,生成被加壳程序。

案例程序如下:

#include <Windows.h>

int main(void) {
  MessageBoxA(NULL, "Hello world!", "MSGBOX", MB_OK);
  return 0;
}

0x03 加载器修改

加载器需要把 VirtualAlloc 改成 GetModuleHandleA,并解除当前程序文件头的写保护,并在随后的复制 Section 阶段同样用 VirtualProtect 解除写保护,添加执行权限。

void *load_PE(char *PE_data) {
  IMAGE_DOS_HEADER *p_DOS_header = (IMAGE_DOS_HEADER *)PE_data;
  IMAGE_NT_HEADERS *p_NT_headers = (IMAGE_NT_HEADERS *)(PE_data + p_DOS_header->e_lfanew);

  // extract information from PE header
  DWORD size_of_image = p_NT_headers->OptionalHeader.SizeOfImage;
  DWORD entry_point_RVA = p_NT_headers->OptionalHeader.AddressOfEntryPoint;
  DWORD size_of_headers = p_NT_headers->OptionalHeader.SizeOfHeaders;

  // base address
  char *p_image_base = (char *)GetModuleHandleA(NULL);
  if (p_image_base == NULL) {
    return NULL;
  }

  // make sure we can write in allocated memory
  DWORD oldProtect;
  VirtualProtect(p_image_base, p_NT_headers->OptionalHeader.SizeOfHeaders, PAGE_READWRITE, &oldProtect);

  // copy PE headers in memory
  mymemcpy(p_image_base, PE_data, size_of_headers);

  // Section headers starts right after the IMAGE_NT_HEADERS struct, so we do some pointer arithmetic-fu here.
  IMAGE_SECTION_HEADER *sections = (IMAGE_SECTION_HEADER *)(p_NT_headers + 1);

  for (int i = 0; i < p_NT_headers->FileHeader.NumberOfSections; i++) {
    // calculate the VA we need to copy the content, from the RVA
    // section[i].VirtualAddress is a RVA, mind it
    char *dest = p_image_base + sections[i].VirtualAddress;

    // check if there is Raw data to copy
    if (sections[i].SizeOfRawData > 0) {
      // make sure we can write in allocated sections
      VirtualProtect(dest, sections[i].SizeOfRawData, PAGE_READWRITE, &old_protect);
      // We copy SizeOfRaw data bytes, from the offset PointerToRawData in the file
      mymemcpy(dest, PE_data + sections[i].PointerToRawData, sections[i].SizeOfRawData);
    } else {
      VirtualProtect(dest, sections[i].Misc.VirtualSize, PAGE_READWRITE, &old_protect);
      for (size_t i = 0; i < sections[i].Misc.VirtualSize; i++) {
        dest[i] = 0;
      }
    }
  }

    // ...
}

此外还有一个坑:不知道为啥,我用 lief python 生成的 DataDirectories 实际只有15个元素(包括最后一个 null 元素),但 winnt.h 里定义的 DataDirectories 是固定长度 16 个元素,所以直接算 p_NT_header + 1 得到的偏移值会比预期的大 8 个字节,导致报找不到 .packed

改成这样。

int _start(void) {
  char *unpacker_VA = (char *)GetModuleHandleA(NULL);

  IMAGE_DOS_HEADER *p_DOS_header = (PIMAGE_DOS_HEADER)unpacker_VA;
  IMAGE_NT_HEADERS *p_NT_headers = (PIMAGE_NT_HEADERS)(unpacker_VA + p_DOS_header->e_lfanew);
  IMAGE_SECTION_HEADER *sections = (PIMAGE_SECTION_HEADER)(p_NT_headers + 1);
  // 注意看这里再计算了一次偏移
  sections = (PIMAGE_SECTION_HEADER)((char *)sections - (IMAGE_NUMBEROF_DIRECTORY_ENTRIES -
                                                         p_NT_headers->OptionalHeader.NumberOfRvaAndSizes) *
                                                            sizeof(IMAGE_DATA_DIRECTORY));

0x04 加壳器

加壳器这次用 python 写,MinGW 下又要重新编译 LIEF 太折磨人了。

4.1 工具函数

先是导入和定义必要的工具。

import lief

def align(x, al):
    """ return <x> aligned to <al> """
    return ((x+(al-1))//al)*al

4.2 解析

先分析案例程序,获得必要的数据。

binary = lief.PE.parse('example.exe')

# calculate shift offset and reserved section size
image_base = binary.optional_header.imagebase
lowest_rva = min([s.virtual_address for s in binary.sections])
highest_rva = max([s.virtual_address + s.size for s in binary.sections])
sect_alignment = binary.optional_header.section_alignment
print('[+] analyze origin demo program binary success.')

取得基址、所有 section 中最低的起始 rva 和最高的结束 rva,得到整个 PE 镜像的 Sections 覆盖的内存范围。

4.3 构造加载器

使用 MinGW 来完成加载器构造——当然有其他更好的做法,加壳还要装一个 MinGW 未免太麻烦,但我也不知道该怎么做就是了(我猜的话,大概拿 nasm 应该就刑。)

编译命令在 Python 脚本里生成并执行。

# compile shifted loader program
compile_args = [
    'loader.c',
    '-m32',
    '-O2',
    '-Wall',
    '-Wl,--entry=__start',
    '-nodefaultlibs',
    '-nostartfiles',
    '-lkernel32',
    '-luser32',
    f'-Wl,--image-base={hex(image_base)}',
    f'-Wl,--section-start=.text={hex(align(image_base+highest_rva,sect_alignment))}',
    '-o',
    'shifted-loader.exe'
]

try:
    check_output(' '.join(['gcc', *compile_args]), shell=True, stderr=STDOUT)
    print('[+] compile shifted loader program success.')
except CalledProcessError as e:
    print(f'[!] loader compilation failed, {e.stdout.decode()}')
    raise

shifted_loader = lief.PE.parse('shifted-loader.exe')
sect_alignment = shifted_loader.optional_header.section_alignment
file_alignment = shifted_loader.optional_header.file_alignment

-luser32 是因为我添加了一个 MessageBoxA 的调用。

-Wl,--image-base=... 设置了加载器的基址,确保加载器和被加壳的程序落在同一个基址上。

-Wl,--section-start=... 因为知道第一个 section 一定是 .text 所以只设置了第一个 section 的地址,之后的 section 会自动往后挪。

其他参数不多解释了。

编译完成后,再解析出加载器的对齐信息,准备用于构造完整的被加壳程序。

4.4 构造加壳程序

加载器和被加载的程序都已经就绪,接下来就是把加载器和程序合并成加壳后的程序了。这一步还是先在创建 lief 的PE32 对象,然后填充基址、Section 对齐、文件对齐,并且把 DLL Characteristics 重置到 0,目的是声明不支持 ASLR。

# create new binary from scratch
output = lief.PE.Binary('packed', lief.PE.PE_TYPE.PE32)

# copy essential fields from shifted_loader
output.optional_header.imagebase = shifted_loader.optional_header.imagebase
output.optional_header.section_alignment = shifted_loader.optional_header.section_alignment
output.optional_header.file_alignment = shifted_loader.optional_header.file_alignment

# disable ASLR
output.optional_header.dll_characteristics = 0

先准备这些文件头字段,接下来开始填充 Section ,最先填充的就是准备用作被加载程序内存空间的 .alloc 节。

# add .alloc section
allocate_size = align(highest_rva-lowest_rva, sect_alignment)
allocate_section = lief.PE.Section(".alloc")
allocate_section.virtual_address = lowest_rva
allocate_section.virtual_size = allocate_size
allocate_section.characteristics = (lief.PE.SECTION_CHARACTERISTICS.MEM_READ
                                    | lief.PE.SECTION_CHARACTERISTICS.MEM_WRITE
                                    | lief.PE.SECTION_CHARACTERISTICS.CNT_UNINITIALIZED_DATA)
output.add_section(allocate_section)

.alloc 节起始点放置在低位,长度为被加载程序的节大小之和对齐。

之后开始复制加载器的节。

# copy sections
for s in shifted_loader.sections:
    # let lief recalculate section offset and sizeof raw data
    s.offset = 0
    s.sizeof_raw_data = 0
    output.add_section(s)

需要注意offsetsizeof_raw_data 置零,让 lief 去计算偏移和大小,后面添加的一应节都按这样操作。新创建的 Section 还好,对于从加载器里复制的 Section,保留 offsetsizeof_raw_data 会导致最终成品的 Section 数据不正确,造成 ntdll 里加载PE文件时,读取PE数据结构时出错。可以自行用 x32dbg 验证。

最后把被加载的文件打包进去。

# add packed section
with open('example.exe', 'rb') as f:
    packed_section = lief.PE.Section('.packed')
    packed_section.content = list(f.read())
    packed_section.characteristics = (lief.PE.SECTION_CHARACTERISTICS.MEM_READ |
                                      lief.PE.SECTION_CHARACTERISTICS.CNT_INITIALIZED_DATA)
    output.add_section(packed_section)

同样,让 lief 去计算偏移和大小。复制好节,继续复制 Data Directories,这又有一个坑。

# copy data directories
for i in range(0, 15):
    src = shifted_loader.data_directories[i]
    output.data_directories[i].rva = src.rva
    output.data_directories[i].size = src.size

# correct number of data directories
# warning: size of data directories may disagree with IMAGE_NT_HEADERS.DataDirectory in winnt.h
output.optional_header.numberof_rva_and_size = len(output.data_directories)

需要注意到,lief 的数据结构里,允许的 data_directories 只有 15 个winnt.h 里定义的 DATA_DIRECTORIES 数组,是固定16个元素

如果直接 range(16) 去遍历,会出现 IndexError ,如果忽视这个长度问题,直接在加载器里采用 Windows SDK 的头文件定义的结构,会导致取节表指针的时候比预期的多偏移 8 个字节,造成问题。调试起来简直太折磨人了。

之后再复制入口点和镜像大小。

# copy original address of entrypoint
output.optional_header.addressof_entrypoint = shifted_loader.optional_header.addressof_entrypoint
# let lief recalculate size of image
output.optional_header.sizeof_image = 0

注意,入口点和镜像大小的字段必须在复制完 Section 之后再复制,不然 lief 会犯傻,原因不明,有兴趣可以自己改一改顺序看看结果。

到这里,基本准备就绪,就可以把构造好的可执行文件写入硬盘了。

# build output binary
builder = lief.PE.Builder(output)
builder.build()
builder.write('packed.exe')

4.5 完整代码

# %%
import lief
from subprocess import STDOUT, CalledProcessError, check_output

def align(x, al):
    """ return <x> aligned to <al> """
    return ((x+(al-1))//al)*al

# %%
# compile origin demo program
try:
    check_output('gcc example.c -m32 -O2 -o example.exe', shell=True, stderr=STDOUT)
except CalledProcessError as e:
    print(f'[!] demo program compilation failed, {e.stdout.decode()}')
    raise

binary = lief.PE.parse('example.exe')
print('[+] compile origin demo program success.')

# %%
# calculate shift offset and reserved section size
image_base = binary.optional_header.imagebase
lowest_rva = min([s.virtual_address for s in binary.sections])
highest_rva = max([s.virtual_address + s.size for s in binary.sections])
sect_alignment = binary.optional_header.section_alignment
print('[+] analyze origin demo program binary success.')

# %%
# compile shifted loader program
compile_args = [
    'loader.c',
    '-m32',
    '-O2',
    '-Wall',
    '-Wl,--entry=__start',
    '-nodefaultlibs',
    '-nostartfiles',
    '-lkernel32',
    '-luser32',
    f'-Wl,--image-base={hex(image_base)}',
    f'-Wl,--section-start=.text={hex(align(image_base+highest_rva,sect_alignment))}',
    '-o',
    'shifted-loader.exe'
]

try:
    check_output(' '.join(['gcc', *compile_args]), shell=True, stderr=STDOUT)
    print('[+] compile shifted loader program success.')
except CalledProcessError as e:
    print(f'[!] loader compilation failed, {e.stdout.decode()}')
    raise

shifted_loader = lief.PE.parse('shifted-loader.exe')
sect_alignment = shifted_loader.optional_header.section_alignment
file_alignment = shifted_loader.optional_header.file_alignment

# %%
# create new binary from scratch
output = lief.PE.Binary('packed', lief.PE.PE_TYPE.PE32)

# copy essential fields from shifted_loader
output.optional_header.imagebase = shifted_loader.optional_header.imagebase
output.optional_header.section_alignment = shifted_loader.optional_header.section_alignment
output.optional_header.file_alignment = shifted_loader.optional_header.file_alignment

# disable ASLR
output.optional_header.dll_characteristics = 0

# add .alloc section
allocate_size = align(highest_rva-lowest_rva, sect_alignment)
allocate_section = lief.PE.Section(".alloc")
allocate_section.virtual_address = lowest_rva
allocate_section.virtual_size = allocate_size
allocate_section.characteristics = (lief.PE.SECTION_CHARACTERISTICS.MEM_READ
                                    | lief.PE.SECTION_CHARACTERISTICS.MEM_WRITE
                                    | lief.PE.SECTION_CHARACTERISTICS.CNT_UNINITIALIZED_DATA)
output.add_section(allocate_section)

# copy sections
for s in shifted_loader.sections:
    # let lief recalculate section offset and sizeof raw data
    s.offset = 0
    s.sizeof_raw_data = 0
    output.add_section(s)

# add packed section
with open('example.exe', 'rb') as f:
    packed_section = lief.PE.Section('.packed')
    packed_section.content = list(f.read())
    packed_section.characteristics = (lief.PE.SECTION_CHARACTERISTICS.MEM_READ |
                                      lief.PE.SECTION_CHARACTERISTICS.CNT_INITIALIZED_DATA)
    output.add_section(packed_section)

# copy data directories
for i in range(0, 15):
    src = shifted_loader.data_directories[i]
    output.data_directories[i].rva = src.rva
    output.data_directories[i].size = src.size

# correct number of data directories
# warning: size of data directories may disagree with IMAGE_NT_HEADERS.DataDirectory in winnt.h
output.optional_header.numberof_rva_and_size = len(output.data_directories)
# copy original address of entrypoint
output.optional_header.addressof_entrypoint = shifted_loader.optional_header.addressof_entrypoint
# let lief recalculate size of image
output.optional_header.sizeof_image = 0

# build output binary
builder = lief.PE.Builder(output)
builder.build()
builder.write('packed.exe')
print('[+] create packed binary success.')

只放一下加载器代码,一共三个代码文件托管在 Gist 上,需要安装 MinGW 和 LIEF,配置方式不赘述。还不会 C 和 Python 的话建议学一下先呢。

完整代码的 GIST

0x05 成果

加壳机运行效果。

加壳机

packed.exe 的节表信息如下。

image-20211020095008599

结论

整个过程里踩了不少坑,几乎都要靠 x32dbg 调试和 CFF Explorer 挨个文件头字段检查。有个比较实用的做法是拿 LIEF 解析好加壳后的文件,把输出结果和原始加载器对比。

import lief

packed = lief.PE.parse('packed.exe')
loader = lief.PE.parse('shifted-loader.exe')

with open('packed-analysis.txt', 'w+', encoding='utf-8') as out:
    print('-----'*20, file=out)
    print('packed.exe', file=out)
    print('-----'*20, file=out)
    print(packed.header, file=out)
    print(packed.optional_header, file=out)

    for entry in packed.data_directories:
        print(entry, file=out)

    for s in packed.sections:
        print(s, file=out)

with open('loader-analysis.txt', 'w+', encoding='utf-8') as out:
    print('-----'*20, file=out)
    print('shifted-loader.exe', file=out)
    print('-----'*20, file=out)
    print(loader.header, file=out)
    print(loader.optional_header, file=out)

    for entry in loader.data_directories:
        print(entry, file=out)

    for s in loader.sections:
        print(s, file=out)

分析好之后就可以拿 vscode 去比较了。

code -n -d packed-analysis.txt loader-analysis.txt

比起直接拿 CFF Explorer 硬看字段哪儿不对,和编译器产生的正常文件比较能排除掉一些无关的字段。但也不是万能,比如说之前没有写 section.offset=0,结果生成的 PE32 文件导入表内容坏了,一直没意识到。直到 x32dbg 调试中发现 ntdll 里加载导入表时碰到了一个无效地址(我怎么知道是加载导入表时呢,胆大心细加上99%的运气...),然后看 CFF Explorer 才发现导入表完全挂了,再回头看节表才发现 .idata 的偏移和大小都是坏的...

还有 data directories 的坑,也是靠 x32dbg,跳转到内存,才发现 (IMAGE_SECTION_HEADER*)(PIMAGE_NT_HEADERS+1) 算出来的偏移值多了8字节,冥思苦想这8字节怎么回事,胡乱分析,然后突然意识到 data directory 正好 8 字节,加壳机里又有个很迷惑的 range(0,15),反复确认了几次才发现真的是 LIEF 就给了 15 个 Data directory —— 但 Windows SDK 里 winnt.h 定义的是 固定 16 个元素 ,之后去翻 PE Format 文档才发现微软早就挖好了这个坑等你翻文档:

Note that the number of directories is not fixed. Before looking for a specific directory, check the NumberOfRvaAndSizes field in the optional header.

原先的文章预计是要做一个压缩壳,简单试验了一下没啥难度,代码都不用几行(VS+CMake+VCPKG 同时用 LIEF 和 ZLIB/LZO 什么的有点费劲,所以用 Windows Compression API),就这样水一篇文章有点不好意思。所以就先去看怎么对付不能重定位的PE32了,结果搞 LIEF 的各种环境编译、折腾VC++的Pragma、翻 Linker Script 手册看能不能改节表偏移、学NASM、从国庆坑到现在。

本篇的参考文章是:https://bidouillesecurity.com/tutorial-writing-a-pe-packer-part-4/

文中有些地方比较怪,比如说先编译了正常 loader 再编译 shifted_loader 就让人不是很理解,照抄抄出一堆bug。所以本文的脚本和参考的脚本已经有点对不上了。

受制于不知道怎么编译出没有重定位的程序,我拿一个有重定位的做了实验(理论上来说,应该是一样的吧?),所以到头也不确定是不是真的能把没有重定位的程序跑起来。

就这样吧,这个结论有点长。到这就差不多了。

免费评分

参与人数 5吾爱币 +4 热心值 +5 收起 理由
shen54188 + 1 + 1 谢谢@Thanks!
加嘞个油 + 1 + 1 我很赞同!
macolma + 1 热心回复!
笙若 + 1 + 1 谢谢@Thanks!
xiier + 1 + 1 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

nanaqilin 发表于 2021-10-20 19:58
你这个是用python加的吧,我要是想用C语方加的话,也是一样的原理吗?
 楼主| weakptr 发表于 2021-10-20 20:00
nanaqilin 发表于 2021-10-20 19:58
你这个是用python加的吧,我要是想用C语方加的话,也是一样的原理吗?

一样的,C语言加的话找相关的PE操作库,或者自己算PE头里的相关偏移和镜像大小,手动对齐节和文件
bigdawn 发表于 2021-10-20 20:17
weiwude 发表于 2021-10-21 10:27
可以多多学习,谢谢楼主
吾爱_小飞 发表于 2021-10-22 09:41
666666666牛逼
cjc3528 发表于 2021-10-22 12:19

膜拜大佬,菜鸟进来学习下,谢谢分享
头像被屏蔽
hua111 发表于 2021-11-16 11:23
提示: 作者被禁止或删除 内容自动屏蔽
三和老哥 发表于 2021-11-20 16:47
好厉害 男神
D0NE 发表于 2024-8-20 22:55
感谢分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-12-23 00:19

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表