系列汇总
- 写一个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 4,Part 5会轻松很多;Part 6主要是对Part 4 中的最终结果中加入简单的混淆
1.清理
step 1:移除不必要信息
- 当前现状
我们已经使用 -nostartfiles
和 -nostdlib
编译选项,在最终的二进制文件中移除了C的标准库和运行时库;但是结果PE中可能还有其他不必要的section和符号信息
给cal.exe打包的后的section分布如下
- 加入改变
我们将新增2个编译选项:-fno-ident
和 -fno-asynchronous-unwind-tables
进一步清除section
-fno-ident:忽略#ident命令
-fno-asynchronous-unwind-tables:用来不生成CFI指令,禁止生成.eh_frame和.eh_frame_hdr section
同时使用strip.exe
移除符号信息和调试信息,进一步减小文件体积
因此,compile_stub函数更改如下
def compile_stub(input_cfile, output_exe_file, more_parameters = []):
cmd = (["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
"-fno-ident", "-fno-asynchronous-unwind-tables", # Remove unnecessary sections
"-lkernel32" # Add necessary imports
])
print("[+] Compiling stub : "+" ".join(cmd))
subprocess.run(cmd)
subprocess.run(["strip.exe", output_exe_file])
step 2:重命名.packed
当前我们有一个非常明确的名字,.packed
是我们自己的section;为了通用性,我们可以将它定义为只读(read-only),且名字用通用名字.rodata
替换
packed_section = lief.PE.Section(".rodata")
packed_section.content = packed_data
packed_section.size = len(packed_data)
packed_section.characteristics = (lief.PE.SECTION_CHARACTERISTICS.MEM_READ
| lief.PE.SECTION_CHARACTERISTICS.CNT_INITIALIZED_DATA)
不知你是否还记得,我们在C程序里是要解析.packed
名称的section的,且整个section是二进制文件中的最后一个section,因此C代码修正如下:
char* packed_PE = unpacker_VA + sections[p_NT_HDR->FileHeader.NumberOfSections - 1].VirtualAddress;
现在一切操作正常的话,对于支持ASLR的二进制文件(本次使用calc.exe测试)打包后,我们应该有3个section:.text
、.idata
(upacker.exe的import table)和.rodata
(要被打包的PE)
可以看到,.rdata
消失了,运行一下,程序依然可以正常运行;可以发现,在.rodata
中有一个完整的PE信息,这对于解密者来说没有什么秘密而言
2.简单的混淆
MZ
和DOS stub这些在在.rodata
中都可以找到,我们试着隐藏一下这些信息
为了简单处理,我们对整个输入文件进行异或
处理;简单做法就是给.rdata
放入input数据时异或一次,真正执行input数据前再异或一次进行解密
step 1:改变packer(python)
使用硬编码,对输入文件进行异或操作;为了让它更复杂一点,使用每个结果字节作为下一个字节的密钥(CBC加密模式)
def encrypt_data(data) :
KEY = 0xAA
result = [0] * len(data)
for i in range(0, len(data)):
KEY = data[i] ^ KEY
result[i] = KEY
return result
input数据放入.rodata
前,调用encrypt_data
函数加密input文件
packed_data = encrypt_data(list(input_PE_data)) # encrypt the input file data
packed_data = pad_data(packed_data, file_alignment) # pad with 0 to align with file alignment (removes a lief warning)
packed_section = lief.PE.Section(".rodata")
packed_section.content = packed_data
step 2:改变unpacker(C)
执行input文件前,要执行解密的过程,即执行上面pyhton中encrypt_data
函数的逆运算
void decrypt_data(char* src, DWORD size) {
DWORD oldProtect;
//make sure we can write on the destination
VirtualProtect(src, size, PAGE_READWRITE, &oldProtect);
DWORD KEY = 0xAA;
DWORD new_key = 0;
for(DWORD i=0; i<size; ++i) {
new_key = src[i];
src[i] = src[i] ^ KEY;
KEY = new_key;
}
}
加载.rodata
数据前,要调用encrypt_data
进行解密
encrypt_data(packed_PE, sections[p_NT_HDR->FileHeader.NumberOfSections - 1].SizeOfRawData);
3.结果
被打包测试程序是calc.exe,最后得到的打包好的二进制文件依然可以弹出计算器
现在查看打包好的二进制文件,拥有的section更少,.rodata
里面的内容已经被混淆了
仅仅一个简单的混淆,再想逆向出PE的完整内容就会增加很多难度;静态分析时,杀毒原件查看文件时于不会看到calc.exe的签名,因为calc.exe的所有内容都被异或处理了;动态分析时,某个时刻还是可以找到calc.exe的所有信息的
4.其他可能
- 输入表包含了5个已知的函数(用来导入其他函数),他们是恶意代码常用的函数;我们可以使用
LIEF
添加许多其他看起来不那么可疑的内容,即使我们没有使用它们 - 异或非常容易被破解;知道PE文件的头2个bytes是
MZ
,很容易Crack出关键字KEY和算法;这里可以使用压缩算法或者自己写的复杂算法 - 避免添加一个完整的section,可以通过其他方式找到被打包的二进制数据