系列汇总
- 写一个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:简单的混淆
文章目录
Part 3:Setion里实现PE装载器
Part 3主要做了什么?
Part 1和Part 2中我们写了一个简易的PE装载器(
loader.exe
),可以加载一个32位程序进入我们申请的内存,然后找到被加载程序的EP,并开始运行被加载程序Part 3中我们将简易的PE装载器放在一个section里实现,用python将一个32位程序加载进入section(并且执行32位程序),最终生成一个可执行的二进制文件
1.unpack部分
这部分可以当成是解压缩部分,脱壳部分都可以,主要是为了还原原程序并执行它
step 1:通用想法
思路:修改Part 1和Part 2的C语言代码,功能转移到一个名为”.packed
“的区段中读取PE文件,需要做的事情:
- 1.列举当前运行程序的section
- 2.找到名为”
.packed
“的section,后续里面会加载一个PE文件内容(即要加载的文件,如calc.exe) - 3.加载PE文件进入内存,并执行它
实现:Part 1和Part 2中的loader.exe
的功能被修改后,构建成unpacker.exe
,里面有一个”.packed
“的区段,用来实现loader.exe
的功能
step 2:修改代码
只需要改变loader.exe
的主函数;为了简化PE的结果(Mingw会产生大量的区段),需要使用编译选项避免链接C的运行时库和C标准库,此时main
函数已经不需要了,将函数的入口改成__start
函数
#include <windows.h>
#include <winnt.h>
// loads a PE in memory, returns the entry point address
void* load_PE (char* PE_data); //这个函数不变
// 3个以my开头的自定义函数
// some basic functions intended to limits the external libraries needed
int mystrcmp(char* a, char* b);
void mymemcpy(char* dst, char* src, unsigned int size);
void mymemset(char* dst, char c, unsigned int size);
int _start(void) { //Entrypoint for the program
// Get the current module VA (ie PE header addr)
char* unpacker_VA = (char*) GetModuleHandleA(NULL);
// get to the section header
IMAGE_DOS_HEADER* p_DOS_HDR = (IMAGE_DOS_HEADER*) unpacker_VA;
IMAGE_NT_HEADERS* p_NT_HDR = (IMAGE_NT_HEADERS*) (((char*) p_DOS_HDR) + p_DOS_HDR->e_lfanew);
IMAGE_SECTION_HEADER* sections = (IMAGE_SECTION_HEADER*) (p_NT_HDR + 1);
char* packed_PE = NULL;
char packed_section_name[] = ".packed";
// search for the ".packed" section
for(int i=0; i<p_NT_HDR->FileHeader.NumberOfSections; ++i) {
if (mystrcmp(sections[i].Name, packed_section_name)) {
packed_PE = unpacker_VA + sections[i].VirtualAddress;
break;
}
}
// load the data located at the .packed section
// packed_PE当成一个PE文件处理,后续会用python中的lief放入一个PE文件进入.packed区段
if(packed_PE != NULL) {
void (*packed_entry_point)(void) = (void(*)()) load_PE(packed_PE);
packed_entry_point();
}
}
上面代码的解释:
-
程序流程:分析当前模块(
unpacker.exe
)的PE文件头,找到一个命令为“.packed
”的区段,将这个区段当成一个PE文件加载并运行 -
处理细节:因为不链接C的库,上面代码中移除了
stdio.h
和stdlib.h
头文件,但是我们还需要strcmp
,memcpy
和memset
函数,因此我们用my
开头的自定义函数实现相同的功能
step 3:编译选项
1.使用的完整编译命令
i686-w64-mingw32-gcc.exe unpack.c -o unpacker.exe "-Wl,--entry=__start" -nostartfiles -nostdlib -lkernel32
2.参数解释
nostartfiles
:移除C运行时库(会调用带有典型argc
和argv
参数的main函数);Windows中操作系统不会分析命令行,为了获取每一个参数,程序需要调用GetCommandLineh
函数且切片来获取结果;这是C运行时库为我们做的事情之一,现在我们不需要了nostdlib
:不链接C标准库 (libC, kernel32.dll, user32.dll, etc …);我们需要告诉连接器需要的特定库,因此使用-lkernel32
选项-Wl,--entry=__start
:设置程序的入口点,此时已经不是main函数了,而是我们在程序里写的_start
函数
3.编译结果
会得到一个名为unpacker.exe
的文件,如果查看它的导入表,将只会看到kernel32.dll
,里面含有5个函数
到目前位置,我们已经生成了一个简易的壳(unpacker.exe
),但是里面是没有名为”.packed
“的section的
因此,下面的任务是增加一个”.packed
“的section,且将一个二进制文件(32位)添加到section中
2.用python打包
既然使用python,就要先准备好工具,如果你是第一次接触python,只需要记住一点,python2和python3是不兼容的,出现问题百度就可以了
- 工具:编写python最好使用IDE工具,便于调试,我一直使用的是社区版的pycharm(免费)
- 库:python的库非常丰富,建议将Anaconda也一起安装一下,网上有教程
step 1:lief安装和使用
我们将用一个名为lief
的python库处理PE文件,它是一个查看和修改PE文件的常用库,比如添加导入表等
lief
的源码是用C++写的,有兴趣的可以看看,不是很复杂,但是对理解PE、ELF等还是很有帮助的
lief
官网:GitHub - lief-project/LIEF: LIEF - Library to Instrument Executable Formats
pycharm中安装lief:Terminal下直接输入 python -m pip install lief 进行安装
我们要做的事很简单:给我们前面编译的uppacked.exe
添加一个名为“.packed
”的区段,里面包含一个32位PE文件的拷贝(比如calc.exe)
step 2:避免告警
为了避免告警,需要下面2个python函数
def align(x, al):
""" return <x> aligned to <al> """
if x % al == 0:
return x
else:
return x - (x % al) + al
def pad_data(data, al):
""" return <data> padded with 0 to a size aligned with <al> """
return data + ([0] * (align(len(data), al) - len(data)))
其中:第一个函数是按照int进行对齐来使用的,第二个函数是将真实长度与对齐长度之间的数据清零
step 3:命令行参数
由于没有链接C库,我们要自己手动分析命令行参数,使用下面的代码:
parser = argparse.ArgumentParser(description='Pack PE binary')
parser.add_argument('input', metavar="FILE", help='input file')
parser.add_argument('-p', metavar="UNPACKER", help='unpacker .exe', required=True)
parser.add_argument('-o', metavar="FILE", help='output', default="packed.exe")
args = parser.parse_args()
step 4:准备文件
现在我们想2个问题,给哪个文件添加区段?区段里的内容是什么?
答案:给上面写的空壳程序unpacker.exe
添加区段,区段里的内容是测试程序calc.exe
;因此,python编程中要先打开这2个PE文件
# open the unpack.exe binary
unpack_PE = lief.PE.parse(args.p) #unpacker.exe
# we're going to keep the same alignment as the ones in unpack_PE,
# because this is the PE we are modifying
file_alignment = unpack_PE.optional_header.file_alignment
section_alignment = unpack_PE.optional_header.section_alignment
# read the whole file to be packed
with open(args.input, "rb") as f: #比如calc.exe
input_PE_data = f.read()
准备工作已经做完了,现在需要给unpacker.exe
添加一个名为”.packed
“的区段
step 5:给PE文件添加一个section
- 先创造一个区段
区段名字是".packed
",内容content
是测试程序calc.exe
# lief expects a list, not a "bytes" object.
packed_data = list(input_PE_data)
# pad with 0 to align with file alignment (removes a lief warning)
packed_data = pad_data(packed_data, file_alignment)
packed_section = lief.PE.Section(".packed")
packed_section.content = packed_data #添加内容,比如calc.exe
packed_section.size = len(packed_data)
packed_section.characteristics = (lief.PE.SECTION_CHARACTERISTICS.MEM_READ
| lief.PE.SECTION_CHARACTERISTICS.MEM_WRITE
| lief.PE.SECTION_CHARACTERISTICS.CNT_INITIALIZED_DATA)
# We don't need to specify a Relative Virtual Address here, lief will just put it at the end, that doesn't matter.
unpack_PE.add_section(packed_section) #unpacker.exe加了一个区段,里面内容是calc.exe
- 保存修改(或者叫打包)成二进制文件
lief
为我们做了大量的计算,比如会自动更新section个数等,因此只要将你感兴趣的区域设置成0即可,保存修改并命名为packed.exe
# remove the SizeOfImage, which should change, as we added a section. Lief will compute this for us.
unpack_PE.optional_header.sizeof_image = 0
# save the resulting PE
if(os.path.exists(args.o)):
# little trick here : lief emits no warning when it cannot write because the output
# file is already opened. Using this function ensure we fail in this case (avoid errors).
os.remove(args.o)
builder = lief.PE.Builder(unpack_PE) #构建unpacker.exe
builder.build()
builder.write(args.o)
3.结果展示
打包生成二进制文件,python命令:python.exe .\packer.py C:\Windows\SysWOW64\calc.exe -p .\unpacker.exe
注意:上面命令要根据自己的文件路径进行实时调整
如果使用pycharm打包,需要附带参数,pycharm设置参数路径方法:
#路径:run菜单 -> Edit Configurations中,Parameters
#内容:C:\Windows\SysWOW64\calc.exe -p C:\unpacker.exe -o C:\packed.exe
CFF查看打包前后的区段变化
相对于unpacker.exe
,仅仅增减了一个.packed
”的区段;直接执行packed.exe
,会出现计算器
Part 3我们做了什么有用的?感觉什么也没做
- 没有减少打包程序的大小:我们相当于在原程序(
calc.exe
)外面套了一个外壳(unpacker.exe
),使整体变大了 - 没有混淆代码,我们仅仅改变了导入表:
packed.exe
里只能看到kernel32.dll
,隐藏了calc.exe
的导入表,但是这对于脱壳来说没什么实际用处;要想恢复原始PE也很简单:只需提取.packed
部分内容即可 - 反调试没有支持:原始PE文件原封不动的包含在“
.packed
”里,没有任何反调试扩展支持
那Part 3有什么用?Part 3相当于一个框架,可以在里面添加一个很多特定需求
4.遗留问题
现在看起来一切还都是正常的,但是有一个隐患还没有解决;Mingw32无法生成重定位表,如果尝试打包一个用Mingw32编译的二进制文件,它不会正确运行
- 1.正常的情况
现在看一下打包calc.exe
,生成的packed.exe
的DLLCharacteristics
特性
此时显示packed.exe
不能移动,勾选上,保存,还是可以正常运行,似乎没有什么问题
- 2.异常的情况
用Mingw32编译的二进制文件(test.exe),正常运行效果如下:
打包命令:python.exe .\packer.py C:\OneDriver\test\test.exe -p .\unpacker.exe
运行效果:产生的二进制文件不能运行,即使强制勾选DLLCharacteristics
使能DLL can move
,但是由于没有重定位表,还是不能运行