写一个PE的壳_Part 1:加载PE文件到内存

前面都是PE的零散的知识,可以通过给PE文件写一个壳将前面的PE相关知识进行汇总

网上看到了一个英文教程(详细看参考),里面包含了给PE加壳和调用LIEF库的操作(这个库很简单);按照教程实现一遍(错误都进行了修正,主要是Part 4),对理解PE文件格式和脱壳很有帮助

下面是结合自己的实践写的笔记,不是教程的原版翻译

教程主要实现的壳的整体功能:

  • 1.A PE File will be copied as-is (at first) in a custom section.
  • 2.The unpacker will load this PE in memory and jump on it.

源码:https://github.com/jeremybeaume/packer-tutorial


编译器选取

可以使用Visual Studio或者MinGW,本教程使用MinGW64

MinGW编译器安装教程:MinGW-w64安装教程——著名C/C++编译器GCC的Windows版本_无知人生 (ivu4e.com)

  • 小插曲:由于实验的机器使用的是win10,64位;导致开始使用MinGW64编译时总是达不到预期效果,后来发现MinGW64默认生成的是64位程序,尝试使用-m32参数显示缺库,还是不生效(生效是要32位的库是全的才可以,自己电脑不想安装那么多库,不处理了)

  • 解决:重新下载i686-8.1.0-release-win32-sjlj-rt_v6-rev0版本,使用i686-w64-mingw32-gcc.exe 就可以了


教程使用的测试程序:以系统自带的32位计算器进行测试,路径:C:\Windows\SysWOW64\calc.exe


系列汇总


Part 1:加载PE文件到内存

目的:写一个读取PE文件,分析它的头部信息、将它的节区信息映射到内存的程序(loader.exe

1.main函数框架

main函数实现的主要功能如下:

  • 1.读取一个文件,申请一块能放下文件大小的内存
  • 2.调用load_PE函数将PE文件读取到上面申请的内存中,然后返回PE文件的EP(暂时先不关心具体的实现)
  • 3.从PE文件EP处开始执行
#include <stdio.h>
#include <stdlib.h>
// loads a PE in memory, returns the entry point address
void* load_PE (char* PE_data);
int main(int argc, char** argv) {
    if(argc<2) {
        printf("missing path argument\n");
        return 1;
    }

    FILE* exe_file = fopen(argv[1], "rb");		//读取一个PE文件
    if(!exe_file) {
        printf("error opening file\n");
        return 1;
    }

    fseek(exe_file, 0L, SEEK_END);				// Get file size: put pointer at the end
    long int file_size = ftell(exe_file);		// and read its position
    fseek(exe_file, 0L, SEEK_SET);				// put the pointer back at the beginning
    char* exe_file_data = malloc(file_size+1);	// allocate memory and read the whole file

    // read whole file
    size_t n_read = fread(exe_file_data, 1, file_size, exe_file);
    if(n_read != file_size) {
        printf("reading error (%d)\n", n_read);
        return 1;
    }

    // load the PE in memory
    printf("[+] Loading PE file\n");
    void* start_address = load_PE(exe_file_data);
    if(start_address) {
        // call its entry point
        ((void (*)(void)) start_address)();		//注意:强制将void*转换成一个函数指针
    }
    return 0;
}

void* load_PE (char* PE_data) {
    //TODO
    return NULL;
}
  • 编译命令:i686-w64-mingw32-gcc.exe main.c -o loader.exe,生成一个名称为loader的PE文件

  • 运行:loader.exe C:\Windows\SysWOW64\calc.exe

  • 结果:有[+] Loading PE file输出说明框架暂时操作都是OK的
    在这里插入图片描述

上面框架中的load_PE函数没有实现,下面介绍load_PE需要做的工作:将PE头信息和section部分加入我们自己申请的内存

2.加载PE Header

PE文件里能看到3种类型地址,要熟悉他们的含义:

  • RAW:一个磁盘上PE文件的偏移地址
  • VA(Virtual Address):内存里的绝对虚拟地址,比如程序里打印一个指针就是这个地址
  • RVA(Relative Virtual Addresses):相对偏移地址,相对指的是相对于PE文件被加载到内存后的基址(ImageBase

CFF查看calc.exe,显示了头部信息,是否是32位等基本的汇总信息

在这里插入图片描述

这里只是简单过一遍PE文件的头部知识

Dos Header

  • CFF查看Dos Header

在这里插入图片描述

每个PE文件都是以Dos Header开始的,签名(e_magic)总是“MZ” (0x5A4D),e_lfanew是NT Headers的RAW地址(0xE8)

  • 疑惑e_lfanew(0x3C)NT header(0xE8)之间存放的是什么?

DOS stub,里面有一个固定的字符串(This program cannot run in DOS mode),这里基本不用关心(CFF里也没有专门呈现)

在这里插入图片描述

  • 头文件:PE文件的数据结构定义在winnt.h,为了使用标准的windows函数,也要包含头文件windows.h
#include <winnt.h>
#include <windows.h>
  • 编程处理Dos Header

在这里插入图片描述

NT Headers

从上到下,按照CFF里展示的顺序介绍NT Headers

CFF查看

[signature]

NT Headers里面只有一个signature (“PE”),另外2个其他头结构都在NT Headers的signature后面

在这里插入图片描述

[File Header]

signature后面是“File Header”,下面CFF查看“File Header”的信息

在这里插入图片描述

几个重要成员:

  • NumberOfSectons:区段的个数,后面会用到,要留意
  • SizeOfOptionalHeader:“Optional Header”的大小,这个信息在Part 4中错误导致程序没有被识别为可执行程序
  • Characteristics:里面的“File is a executable”“File is a DLL”是唯一区分文件是EXE还是DLL的地方
[Optional Header]

“File Header”后面是“Optional Header”“Optional Header”是PE文件中核心部分

在这里插入图片描述

重要成员如下:

  • Magic:另一个签名,区分PE文件是PE32还是PE64
  • AddressOfEntryPoint: 程序入口地址,即加载PE到内存后,运行时第一条执行语句的RVA地址,注意是相对地址,很重要
  • ImageBase:二进制文件优先加载进内存的地址,所有RVA计算的参考基准
  • SizeOfimage:模块被加载到内存后的虚拟大小,也可以理解成为了加载模块,自己的PE加载器最少要申请的空间大小
  • SizeofHeaders:所有头文件总大小
  • DLLCharacteristics:有很多标志,最有用的是“Dll can move”,标识模块的ASLR是否使能 (或者他能否被移动)

注意:ASLR( Address Space Layout Randomization,地址空间布局随机化)是一种通过增加攻击者预测目的地址难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目一种技术;由于篇幅的原因,将ASLR的简单介绍放在Part 2

下面是代码需要处理的部分

代码处理

winnt.h中“NT Headers” 结构定义如下

typedef struct _IMAGE_NT_HEADERS { 
  DWORD             		Signature;
  IMAGE_FILE_HEADER     	FileHeader;
  IMAGE_OPTIONAL_HEADER32   OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

可以看到结构里没有指针,代码处理如下

DWORD hdr_image_base  = p_NT_HDR->OptionalHeader.ImageBase;
DWORD size_of_image   = p_NT_HDR->OptionalHeader.SizeOfImage;
DWORD entry_point_RVA = p_NT_HDR->OptionalHeader.AddressOfEntryPoint;
DWORD size_of_headers = p_NT_HDR->OptionalHeader.SizeOfHeaders;

有了模块被加载到内存后的虚拟大小(SizeOfimage)后,需要申请一块内存;下满是按照被加载的二进制文件的ASLR是使能为前提的(参数为NULL的VirtualAlloc申请一块任意内存的地址来支持的

在这里插入图片描述

开始加载PE内容进入内存,大体上有2种方式:

  • 一种是直接一次拷贝进内存,然后在按照PE文件格式解析(下面采用的方式)
  • 另一种是直接按照PE格式,分部分的加入内存

首先将PE的头加入内存,即将PE header复制到ImageBase(上面我们在heap上申请的一块内存)处

memcpy(ImageBase, PE_data, size_of_headers);

现状:现在我们已经有一块内存,里面有PE header了,但是没有代码和数据,因此需要引出Section的话题

3.加载Sections部分

PE的二进制文件一旦被加载进入内存,相似属性的部分都被分割成不同部分,成为section;头信息中是有这些section的描述或者叫引导信息的,被成为section table或者Section Headers

CFF查看

在这里插入图片描述

其中,有5个区段(section),每个区段都有的重要数据成员:

  • Name:区段的名字,比如 .text 区段通常用来存代码, .data 通常用来存数据
  • VirtualSize内存中占用的大小(the size it occupies in memory)
  • VirtualAddress内存中的偏移,是RVA
  • RawSize磁盘中占用的大小(the size of the section data in the PE file)
  • RawAddress磁盘中,相对于PE文件开始处的偏移(the offset of the beginning of the data in the PE file)
  • Characteristics:权限,读/写、执行等权限

注意:一定要区分好磁盘和内存

  • 磁盘:是一个可执行文件放在我们电脑上的位置,比如:C:\Windows\SysWOW64\里面放的文件,就是一个静态的二进制文件
  • 内存:当执行可执行文件时,PE装载器会将磁盘上静态的二进制文件,按照固定格式加载进入内存

代码处理

上面介绍完,现在可以将PE文件的扇区部分也加载进入内存

  • 加载section进入内存
// Section headers starts right after the IMAGE_NT_HEADERS, so we do some pointer arithmetic-fu here.
IMAGE_SECTION_HEADER* sections = (IMAGE_SECTION_HEADER*) (p_NT_HDR + 1); 

// For each sections
for(int i=0; i<p_NT_HDR->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 = ImageBase + sections[i].VirtualAddress; //ImageBase是我们自己申请的一块heap上内存

    // check if there is Raw data to copy
    if(sections[i].SizeOfRawData > 0) {
        // We copy SizeOfRaw data bytes, from the offset PointertoRawData in the file
        memcpy(dest, PE_data + sections[i].PointerToRawData, sections[i].SizeOfRawData);
    } else {
        memset(dest, 0, sections[i].Misc.VirtualSize);
    }
}

目前为止,已经将所有区段都加载到我们自己申请的内存中了,区段是不是就处理完了呢?

没有,因为现在所有区段的属性都是read/write,还要将每个区段的权限按照section table的原始Characteristics进行一下设置

  • 设置section的属性
// Set permission for the PE hader to read only
DWORD oldProtect;
VirtualProtect(ImageBase, p_NT_HDR->OptionalHeader.SizeOfHeaders, PAGE_READONLY, &oldProtect);

// 设置每个section的权限
for(int i=0; i<p_NT_HDR->FileHeader.NumberOfSections; ++i) {
    char* dest = ImageBase + sections[i].VirtualAddress;
    DWORD s_perm = sections[i].Characteristics;
    DWORD v_perm = 0; //flags are not the same between virtal protect and the section header
    if(s_perm & IMAGE_SCN_MEM_EXECUTE) {
        v_perm = (s_perm & IMAGE_SCN_MEM_WRITE) ? PAGE_EXECUTE_READWRITE: PAGE_EXECUTE_READ;
    } else {
        v_perm = (s_perm & IMAGE_SCN_MEM_WRITE) ? PAGE_READWRITE: PAGE_READONLY;
    }
    VirtualProtect(dest, sections[i].Misc.VirtualSize, v_perm, &oldProtect);
}
  • 返回PE文件入口

加载一个PE文件进入内存暂时告一段落,现在将PE文件的入口地址(VA)返回

return (void*) (ImageBase + entry_point_RVA);

4.验证结果

上面都做完了,再编译(i686-w64-mingw32-gcc.exe main.c -o loader.exe)一下,运行:loader.exe C:\Windows\SysWOW64\calc.exe,计算器没有出现,没有达到预期效果

原因是2个非常重要的表我们还没有处理:添加输入表和管理重定位表,这将在Part 2部分介绍

5.参考

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值