系列汇总
- 写一个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.参考
1.解决思路 step 1:当前现状Part 3中遗留了一个隐患,MinGW无法生成重定位表,因此无法产生可移动的二进制文件;Part 4中要处理MinGW生成的可执行文件(不能移动的,即不支持ASLR的二进制文件)的打包问题
寻找解决办法前,先回顾一个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也可以)的内存布局示意图
| VA | RVA | CONTENT |
|---|---|---|
| 0x00400000 | 0 | unpacker PE header |
| 0x00401000 | 0x1000 | unpacker .text section |
| 0x00402000 | 0x2000 | unpacker .rdata section |
| 0x00403000 | 0x3000 | unpacker .idata section |
| 0x00404000 | 0x4000 | unpacker .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没有重定位表,没法重定位,因此不能运行
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:上面修改就解决问题了?
step 3:内存布局当然,先将test.exe加载在packe.exe的Image Base算出(是否支持ASLR处理方式不同),然后不破坏packed.exe的PE header,解析.packed开始执行test.exe;但是已经巧妙的将test.exe的section放入packed.exe区段的起始位置,这样test.exe就不需要重定位了,巧妙的解决了没有重定位表的问题
如果我们想打包一个二进制文件,希望放在VA 0x00400000开始的内存中,下面是unpacker.exe修改后(文件名改成了packe.exe)的内存布局示意图
| VA | RVA | CONTENT |
|---|---|---|
| image base of the packed binary | 0 | unpacker PE header |
| 0x1000 | .alloc section, for the packed binary loading | |
| 0x1000 + size of the packed binary in memory | sections of the unpacker | |
| … | sections of the unpacker | |
| 0x4000 + size of the packed binary in memory | .packed section, with the packed PE file |
相对于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 -lkernel32step 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
-
1.Writing a PE packer – Part 4 : packing with no relocation
-
2.02 - Create a PE from scratch — LIEF Documentation (lief-project.github.io)



