栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

写一个PE的壳

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

写一个PE的壳


系列汇总

  • 写一个PE的壳_Part 1:加载PE文件到内存
  • 写一个PE的壳_Part 2:ASLR+修复输入表(IAT)+重定位表支持(.reloc)
  • 写一个PE的壳_Part 3:Section里实现PE装载器
  • 写一个PE的壳_Part 4:修复对ASLR支持+lief构建新PE
  • 写一个PE的壳_Part 5:PE格式修复+lief源码修改
  • 写一个PE的壳_Part 6:简单的混淆

文章目录
  • 1.解决思路
    • step 1:当前现状
    • step 2:解决思路
    • step 3:内存布局
  • 2.修改unpack部分
    • Image Base
    • PE Header and Section
  • 3.修改python的打包程序
    • step 1:构建unpack部分
    • step 2:ASLR特殊处理
  • 4.结果验证
  • 5.参考

Part 3中遗留了一个隐患,MinGW无法生成重定位表,因此无法产生可移动的二进制文件;Part 4中要处理MinGW生成的可执行文件(不能移动的,即不支持ASLR的二进制文件)的打包问题

1.解决思路 step 1:当前现状

寻找解决办法前,先回顾一个Part 3中packed.exe是怎么处理支持ASLR的二进制文件(用calc.exe举例)的?

  • 1.packed.exe里面新增一个名称为.packed的区段,区段的内容被加载为calc.exe
  • 2.packed.exe负责解析.packed,放到一块我们申请的内存中,用参数是NULL的VirtualAlloc来申请一块随机内存来达到对ASLR的支持
  • 3.修复完calc.exe的输入表和重定位表,找到calc.exe的入口EP,执行calc.exe

下面是Part 3中unpacker.exe(或者叫packed.exe也可以)的内存布局示意图

VARVACONTENT
0x004000000unpacker PE header
0x004010000x1000unpacker .text section
0x004020000x2000unpacker .rdata section
0x004030000x3000unpacker .idata section
0x004040000x4000unpacker .packed section

下面给出了Part 3中问题的原因

  • 问题:为什么Part 3中对不支持ASLR的二进制不支持呢?

  • 假设:part 3中最后生成的壳(或者叫打包)文件名为:packed.exe;不支持ASLR的测试程序名为:test.exe,源码如下:

    //test.exe的源码
    #include 
    #include 
    int main() {
    	printf("this is a test for compil with MinGW, no relocn");
        return 0;
    }
    
  • 冲突:packed.exe默认是加载在0x400000的,test.exe因为不能移动(即不支持ASLR),导致也需要加载在0x400000上,产生冲突

  • 原因:packed.exe先运行,把坑占用了,test.exe没有重定位表,没法重定位,因此不能运行

step 2:解决思路

test.exe的重定位表没有,在packed.exe内存里拷贝一份不就解决了

问题1:那拷贝在哪里才能生效?

当然是不支持ASLR的程序(如test.exe)加载在0x00400000位置时,test.exe的重定位表应该放的位置(感觉像废话)

也从侧面看出来,测试整个进程packed.exe的加载基址应该是0x00400000,而不能是NULL的VirtualAlloc来申请一块随机内存,这部分后面后看到区别

问题2:packed.exe的重定位表已经把坑占了怎么办?

那就将packed.exe的重定位表整理后移呗,反正它也不用重定位表,下面是思路:

  • 1.packed.exe的重定位表都是在section里,先算好test.exe的重定位表的大小sizeof_alloc
  • 2.在packe.exe的第一个section的位置预留出sizeof_alloc大小的空间,后续load_PE函数会处理这里
  • 3.packed.exe的所有section整体向后平移sizeof_alloc就可以了

问题3:上面修改就解决问题了?

当然,先将test.exe加载在packe.exe的Image Base算出(是否支持ASLR处理方式不同),然后不破坏packed.exe的PE header,解析.packed开始执行test.exe;但是已经巧妙的将test.exe的section放入packed.exe区段的起始位置,这样test.exe就不需要重定位了,巧妙的解决了没有重定位表的问题

step 3:内存布局

如果我们想打包一个二进制文件,希望放在VA 0x00400000开始的内存中,下面是unpacker.exe修改后(文件名改成了packe.exe)的内存布局示意图

VARVACONTENT
image base of the packed binary0unpacker PE header
0x1000.alloc section, for the packed binary loading
0x1000 + size of the packed binary in memorysections of the unpacker
…sections of the unpacker
0x4000 + size of the packed binary in memory.packed section, with the packed PE file
2.修改unpack部分

相对于Part 3中的unpack部分,改的很少,主要是改动load_PE函数,c文件名是unpack_part4.c;整体修改前后对比,如果看不清楚也没有关系,后面会有详细的介绍

Part 3中支持ASLR是通过VirtualAlloc来申请一块内存来实现的,Part 4中要解决不支持ASLR的情况;几处改动说明:

Image Base

首先要检查PE文件的头中的DLLCharacteristics,看一下是否支持ASLR?

  • 不支持:也就没有重定位表什么事了,PE文件的加载位置就应该是默认的基址,使用当前模块地址作为基址
  • 支持:VirtualAlloc新申请一块内存,用于放被加载的程序(如test.exe)
char* ImageBase = NULL;
if(p_NT_HDR->OptionalHeader.DllCharacteristics & IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE) {
    ImageBase = (char*) VirtualAlloc(NULL, p_NT_HDR->OptionalHeader.SizeOfImage, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
    if(ImageBase == NULL) {
        // Allocation failed
        return NULL;
    }
} else {
    //if no ASLR : the packer would have placed us at the expected image base already
    ImageBase = (char*) GetModuleHandleA(NULL);
}
PE Header and Section

.alloc区块属性通常不是可读写的,因此在复制test.exe的PE Header和Section到.alloc时,都要考虑是否使用VirtualProtect来修改属性

DWORD oldProtect;
// The PE header is readonly, we have to make it writable to be able to change it
VirtualProtect(ImageBase, p_NT_HDR->OptionalHeader.SizeOfHeaders, PAGE_READWRITE, &oldProtect);
mymemcpy(ImageBase, PE_data, p_NT_HDR->OptionalHeader.SizeOfHeaders);

// 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_HDR + 1); 

// For each sections
for(int i=0; iFileHeader.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; 

    // check if there is Raw data to copy
    if(sections[i].SizeOfRawData > 0) {
        // A VirtualProtect to be sure we can write in the allocated section
        VirtualProtect(dest, sections[i].SizeOfRawData, PAGE_READWRITE, &oldProtect);
        // We copy SizeOfRaw data bytes, from the offset PointertoRawData in the file
        mymemcpy(dest, PE_data + sections[i].PointerToRawData, sections[i].SizeOfRawData);
    } else {
        // if no raw data to copy, we just put zeroes, based on the VirtualSize
        VirtualProtect(dest, sections[i].Misc.VirtualSize, PAGE_READWRITE, &oldProtect);
        mymemset(dest, 0, sections[i].Misc.VirtualSize);
    }
}

unpack部分已经修改完,其实最大的改变在python打包部分

3.修改python的打包程序

这部分原始程序有很多坑,本来想直接贴出来正确的程序了;但是LIEF源码没法在我的机器上被编译(官网说VS2015就可以,但是还支持了c++20的span,懒得更换编译器和配置了…),所以只能修改python代码,因此将解决思路简单做了一下笔记

注意:如果你的机器上可以编译LIEF源码,修改会更简单,下面也给出了建议

下面是原教程中,修改后python打包程序的主要框架:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nKQWG6eL-1653612536248)(…/…/图片/image-2022051232375.png)]

修改部分简单介绍一下:

step 1:构建unpack部分

原本使用MinGW构建unpack部分需要敲如下命令:

i686-w64-mingw32-gcc.exe unpack.c -o unpacker.exe "-Wl,--entry=__start" -nostartfiles -nostdlib -lkernel32

为了代码复用,现在使用python代码实现这一功能,下面是一个自动打包的函数

def compile_stub(input_cfile, output_exe_file, more_parameters = []):
    cmd = (["i686-w64-mingw32-gcc.exe", input_cfile, "-o", output_exe_file] # Force the ImageBase of the destination PE
        + more_parameters +
        ["-Wl,--entry=__start", # define the entry point
        "-nostartfiles", "-nostdlib", # no standard lib
        "-lkernel32" # Add necessary imports
        ])
    print("[+] Compiling stub : "+" ".join(cmd))
    subprocess.run(cmd)

compile_stub函数会使用2次,2次构建的区别主要是对section的处理,第二次会对section进行整体移动

  • 第一次是将unpack_part4.c,直接构建成unpack.exe
  • 第二次是将unpack_part4.c,构建成shifted_unpack.exe

2次完整编译命令如下:

#第1次完整编译命令
[+] Compiling stub : i686-w64-mingw32-gcc.exe unpack_part4.c -o unpack.exe -Wl,--entry=__start -nostartfiles -nostdlib -lkernel32
#第2次完整编译命令
[+] Compiling stub : i686-w64-mingw32-gcc.exe unpack_part4.c -o shifted_unpack.exe -Wl,--image-base=0x400000 -Wl,--section-start=.text=0x410000 -Wl,--section-start=.rdata=0x411000 -Wl,--section-start=.idata=0x412000 -Wl,--entry=__start -nostartfiles -nostdlib -lkernel32
step 2:ASLR特殊处理

这部分是对不支持ASLR的特殊处理,在上面else部分实现;主要做的就是添加名为.alloc的section,将unpacker.exe的所有section进行平移;处理步骤如下:

  • [step1].计算输入文件test.exe的区段的边界[min_RVA, max_RVA]

    是否是ASLR要区别对待,计算输入文件test.exe的区段的边界,为.alloc大小做准备

    if ASLR:
        output_PE = unpack_PE  # we can use the current state of unpack_PE as our output
    else:
        # We need to add an empty section, ".alloc" just after the header
        # It's size will at least the size offcupied by the sections on the input PE(比如calc.exe)

        # [step1].计算输入文件test.exe的区段的边界[min_RVA, max_RVA]
        # The RVA of the lowset section of input PE
        min_RVA = min([x.virtual_address for x in input_PE.sections])  # should be = 0x1000, the section alignment
        # The RVA of the end of the highest section
        max_RVA = max([x.virtual_address + x.size for x in input_PE.sections])
  • [step2].Now we create the section,name is “.alloc”

    .alloc只指定了内存信息,没有指定raw相关信息,后续会处理这里

        # [step2].Now we create the section
        alloc_section = lief.PE.Section(".alloc")
        alloc_section.virtual_address = min_RVA 	# 新区段的起始是输入文件区段起始
        alloc_section.virtual_size = align(max_RVA - min_RVA, section_alignment)  # 大小是输入文件的区段总大小
        alloc_section.characteristics = (lief.PE.SECTION_CHARACTERISTICS.MEM_READ
                                       | lief.PE.SECTION_CHARACTERISTICS.MEM_WRITE
                                       | lief.PE.SECTION_CHARACTERISTICS.CNT_UNINITIALIZED_DATA)
  • [step3].计算一下unpacked.exe向下偏移的大小

    为了将input文件的区段打包成.alloc后放在unpack.exe的区段起始位置,要计算一下unpacked.exe向下偏移的大小

        # [step3].计算一下unpacked.exe向下偏移的大小
        # to put the section just after ours, find the lowest section RVA in the stub
        min_unpack_RVA = min([x.virtual_address for x in unpack_PE.sections])
        # and compute how much we need to move to be exactly after the .alloc section
        shift_RVA = (min_RVA + alloc_section.virtual_size) - min_unpack_RVA
  • [step4].重新编译unpack_part4.c

    目的是将它的区段向下偏移,给我们的新区段.alloc预留出足够的空间,产生新的二进制文件shifted_unpack.exe

    注意:--section-start是VA,不是RVA

        # [step4].重新编译unpack_part4.c,目的是将它的区段向下偏移,给我们的新区段.alloc预留出足够的空间,产生新的二进制文件shifted_unpack.exe
        # We need to recompile the stub to make room for the .alloc section, by shifting all its sections
        compile_parameters = [f"-Wl,--image-base={hex(input_PE.optional_header.imagebase)}"]  # 很重要新的加载基址,test.exe的
        for s in unpack_PE.sections:
            compile_parameters += [f"-Wl,--section-start={s.name}={hex(input_PE.optional_header.imagebase + s.virtual_address + shift_RVA)}"]
        # recompile the stub with the shifted sections
        compile_stub("unpack_part4.c", "shifted_unpack.exe", compile_parameters)
        unpack_shifted_PE = lief.PE.parse("shifted_unpack.exe")
  • [step5].重新排列section

    弄一个新的PE文件,将必要的头信息,.alloc区段和重新编译的unpack_part4.c的区段依次按顺序存放

    弄一个新的PE文件的原因:windows希望section是按照Virtual Address大小进行排序,但是LIEF的add_section函数只能在已有的section后面插入新section

    注意:新增.packed区段不在这里实现;为了确保packecd.exe不支持ASLR,将dll_characteristics赋值为0

        # [step5].弄一个新的PE文件,将必要的头信息,.alloc区段,重新编译的unpack_part4.c的区段依次按顺序存放
        # This would insert .alloc section at the end of the table, so the RVA would not be in order.
        # but Windows doesn' t seem to like it : the binary doesn' t load.
        # output_PE = unpack_shifted_PE
        # output_PE.add_section(alloc_section)

        # Here is how we make a completely new PE, copying the important properties
        # And adding the sections in order
        output_PE = lief.PE.Binary("pe_from_scratch", lief.PE.PE_TYPE.PE32)		#空文件
        # Copy optional headers important fields
        output_PE.optional_header.imagebase = unpack_shifted_PE.optional_header.imagebase
        output_PE.optional_header.addressof_entrypoint = unpack_shifted_PE.optional_header.addressof_entrypoint
        output_PE.optional_header.section_alignment = unpack_shifted_PE.optional_header.section_alignment
        output_PE.optional_header.file_alignment = unpack_shifted_PE.optional_header.file_alignment
        output_PE.optional_header.sizeof_image = unpack_shifted_PE.optional_header.sizeof_image
        # make sure output_PE cannot move
        output_PE.optional_header.dll_characteristics = 0
        # copy the data directories (imports most notably)
        for i in range(0, 15):
            output_PE.data_directories[i].rva = unpack_shifted_PE.data_directories[i].rva
            output_PE.data_directories[i].size = unpack_shifted_PE.data_directories[i].size
        # add the sections in order
        output_PE.add_section(alloc_section)  # 最先添加的是.alloc区段
        for s in unpack_shifted_PE.sections:
            output_PE.add_section(s)
        # We now are ok to continue with the .packed section, as before
4.结果验证

用原教程中Part 4的py文件处理test.exe,生成名为packed_bug1.exe程序;运行时直接报错


这可能是由于PE文件格式异常,导致Windows装载器不能识别,详细的处理可以看下一篇笔记Part 5

5.参考
  • 1.Writing a PE packer – Part 4 : packing with no relocation

  • 2.02 - Create a PE from scratch — LIEF Documentation (lief-project.github.io)

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/935709.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号