本文转自http://huaidan.org/pstzine/0x02/html/PSTZine_0x02_0x0A.html
==Ph4nt0m Security Team==
Issue 0x02, Phile #0x0A of 0x0A
|=---------------------------------------------------------------------------=|
|=----------------------=[ pe/elf 文件加壳时的处理 ]=----------------------=|
|=---------------------------------------------------------------------------=|
|=---------------------------------------------------------------------------=|
|=--------------------------=[ By dummy ]=--------------------------=|
|=-----------------------=[ <dummy_at_ph4nt0m.org> ]=----------------------=|
|=---------------------------------------------------------------------------=|
前言:
最初的壳是在感染型的病毒技术上发展出来的,加壳目的一般是压缩或加密。本文主要
就x86平台下win32 pe和linux elf 加壳程序的实现做简单介绍和总结,以自己以前写相关
程序做线索叙述,其中程序源码是开源的,有兴趣的朋友可以继续进行改进。
ps: 其中有些地方很久没碰,可能有地方描述有误,还请见谅:)
正文:
-------------------------------------------------------
slm x86 win32 r3 pe packer
mimisys x86 win32 r0 pe packer
elfp x86 linux r3 elf packer
-------------------------------------------------------
一、一个壳的组成
一个完整的壳程序主要由 2 个部分组成 packer 和 loader。它们具体的作用分别是:
(1) packer
负责将待加壳程序压缩和加密处理、把loader写到待加壳程序上。以slm的pakcer
为例具体操作包括,pe有效性判断、优化可压缩数据、压缩和加密、添加loader、存放
加壳参数和待加壳程序原数据(oep等等)、改写入口点等等。
(2) loader
主要工作是解压或解密被加壳的程序,以slm的loader为例具体的操作包括:获取自
身位置、获取加壳参数、进行解压或解密、填充导入表、重定位、tls 初始化等等。
二、slm (x86 win32 r3 pe packer)
资料:
http://www.microsoft.com/whdc/system/platform/firmware/PECOFF.mspx
工具:
lordpe pe 文件格式查看编辑工具
dumpbin vc 自带coff文件格式查看工具
ollydbg r3 调试工具
源码结构:
./slm/cm 公共头文件和模块
./slm/pk packer 实现
./slm/sc loader 实现
在做这个时候对 pe 也是刚刚了解,所以 slm 很多地方现在看来有些问题:)。在第一
节已经简单描述 slm 的工作流程,下面主要就我当初做的时候遇到的问题做一些描述:
(1) 资源的处理
slm 的资源处理做的比较烦琐,当初目的是为了把可压缩资源数据归并到一起,进
行一次压缩,不可压缩单独存放。下面简单介绍一下资源的目录数据格式,详细还是看
看微软的文档和相关源码:)
从IMAGE_NT_HEADERS.IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_RESOURCE]
取出资源数据的地址res_rva,经过转换后第一个结构体是IMAGE_RESOURCE_DIRECTORY
IMAGE_RESOURCE_DIRECTORY:
NumberOfIdEntries 目录下 id 名称入口项个数
NumberOfNamedEntries 目录下 name 名称入口项个数
紧跟着IMAGE_RESOURCE_DIRECTORY后面是IMAGE_RESOURCE_DIRECTORY_ENTRY结构
数组,这个数组的元素个数是 NumberOfIdEntries + NumberOfNamedEntries。
IMAGE_RESOURCE_DIRECTORY_ENTRY:
Id 目录id,只有NameIsString非真才有效
NameIsString 目录名称是否是字符串,如果为真NameOffset有效
NameOffset 目录名称串的偏移, 偏移是相对与res_rva*的。
DataIsDirectory 如果为真 OffsetToData 有效,否则OffsetToDirectory
有效
OffsetToData 指向资源数据,偏移类型rva
OffsetToDirectory 指向子目录,偏移类型rva
如果目录入口名称是字符串,通过NameOffset获取PIMAGE_RESOURCE_DIR_STRING_U
的结构指针,目录名是unicode格式,并且不是以零结尾的字符串。如果目录名不是字符
串而是id, 那么其值在winnt.h 定义。常见id有RT_ICON、RT_VERSION等等。
结构大致简单描述完了,有点要注意OffsetToDirectory、OffsetToData修改时要
进行 DWORD 对齐,否则会出现奇怪的现象。
(2) 导入表处理
从IMAGE_NT_HEADERS.IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_IMPORT]
取出导入表的地址imp_rva,经过转换后第一个结构体是IMAGE_IMPORT_DESCRIPTOR。
IMAGE_IMPORT_DESCRIPTOR:
Name 指向导入 dll 的名称,偏移类型 rva
FirstThunk 指向 IMAGE_THUNK_DATA 结构体,偏移类型 rva
OriginalFirstThunk 指向FirstThunk 的副本, 可以为空。偏移类型 rva
导入由IMAGE_IMPORT_DESCRIPTOR结构数组组成,数组长度由一个Name域为空的结
构表明。
FirstThunk和OriginalFirstThunk都是指向以IMAGE_THUNK_DATA数组组成的数据
结构,系统的加载器在进行导入表填充时,会把FirstThunk指向的结构修改掉。
(3) TLS 处理
这里说的tls是所谓的静态tls(在pe文件结构上进行实现),关于什么是tls可以看看
《windows 核心编程》线程那章。
1、tls 是怎样的
比如要在vc中声明一个tls变量需要这样__declspec(thread) int x = 0;在链接
时这个变量会被链接器放入.tls的节中。这个节从外边看和其他的节没有什么不同,唯
一的区别在IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_TLS]指向的一个结构会对
此节进行描述,这个结构是IMAGE_TLS_DIRECTORY。
IMAGE_TLS_DIRECTORY:
StartAddressOfRawData tls数据开始地址类型va
EndAddressOfRawData tls数据结束地址类型va
AddressOfIndex; tls slot的地址,默认tls slot为0
AddressOfCallBacks 指向一个PIMAGE_TLS_CALLBACK的数组,这个数组
以0结尾,每个PIMAGE_TLS_CALLBACK都是va类型
SizeOfZeroFill 数据区需要进行清 0 数据的大小
Characteristics
2、系统加载器怎样处理exe的tls
系统加载器在完成重定位和输入表填充后,就开始处理tls。如果存在tls_dir,
求出tls数据的大小EndAddressOfRawData - StartAddressOfRawData +
SizeOfZeroFill, 按照大小分配一块内存,地址存入(PDWORD)fs:[0x2c] +
tls_slot, 接着拷贝StartAddressOfRawData -> EndAddressOfRawData之间的数
据到新分配的内存中,然后使用SizeOfZeroFill 清零剩下的数据,然后进行循环回
调AddressOfCallBacks中的函数,PIMAGE_TLS_CALLBACK函数和DllMain原型很像,
只是没有返回值。
3、系统加载器怎样处理dll的tls
首先明确dll是可以使用tls的,唯一的不同是AddressOfCallBacks调用方式会
有些区别。如果目标dll是被静听链接到其他文件上,在进程创建完成时即被加载,
那么他的tls callback会触发,而LoadLibrary方式加载不会触发。
(4) rva & raw 转换
pe 文件中许多结构域的指针类型是rva, rva是pe文件由系统加载后,方法相关数
据的相对偏移量。而我们进行加壳处理时,是直接map的文件数据访问都是使用文件指
针,这就需要rva进行转换。(每次做pe相关工具时,我都会习惯写一个这样的函数,现在
不下10中版本,竟然没有一个可以保证是正确的 - -)
下面这个是最新的rva2raw版本,不保证正确性。
三、mimisys (x86 win32 r0 pe packer)
资料:
Windows Research Kernel
wrk/base/ntos/mm/sysload.c:MmLoadSystemImage
工具:
syser 内核调试器,你也可以选择其他的r0调试器
vmware 如果不想频繁重启,需要一个虚拟机
文件格式的一些处理参考slm, 这里主要就r0 pe和r3 pe区别做介绍:
(1) 节和页
r0空间的内存常常很紧张,就导致sys section属性有几个特殊地方
1、可换出和禁止换出
在内存不足时,系统内存管理器,会枚举已加载的section object, 如果存在
pageout属性,那么系统内存管理器就会换出这个节对应的页(这个节经过系统页对
齐后换出内存)节对齐原则VirtualAddress向上、VirtualSize向下。禁止换出如
名字所名这个节将永驻内存。
2、节对齐小于一页
大多数sys的节对齐指数都是小于一页,系统加载器在处理这类文件时相当很
简单。加载后文件和磁盘上的文件布局基本一致。当节对齐小于一页时,
SizeOfRawData必须大于等于VirtualSize,即不支持未初始化节。mimisys通过增
加SizeOfImage在文件加载后分配一个未初始化的缓冲区,保证解压过程。
(2) checksum校验
一句话: 只有正确的checksum sys文件才允许被加载。
(3) win2k相关问题
win2k的系统加载器和其他nt系统有几处不同,r3和r0都会有一些区别,比如r3 pe
的必须要有导入表,否则拒绝加载,r0 pe必须要有重定位信息,否则也会拒绝加载。这
种情况需要构造一个空的重定位目录即可。
mimisys采取的是合并节,导致加壳后只剩下两个节,第一个节是loader, 存放
loader和各种加壳参数,第二个节是原程序优化压缩后的数据(移动重定位,移动资源等
等)。两个节的属性都是不允许换出的。
四、elfp (x86 linux r3 elf packer)
资料:
Tool Interface Standard (TIS) Executable and Linking Format
http://www.x86.org/ftp/manuals/tools/elf.pdf
毛德操 《漫谈内核兼容》8,9 ELF映像的装入
http://linux.insigma.com.cn/jszl.asp?docid=132762762
http://linux.insigma.com.cn/jszl.asp?docid=133617926
linux 内核源码
linux/fs/binfmt_elf.c:load_elf_binary
工具:
objdump 进行elf文件格式的结构查看
http://www.gnu.org/software/binutils/binutils.html
ald 汇编级调试器,gdb无法调试没有调试信息文件的。
http://ald.sourceforge.net/
elfp是在magiclinux完成的linux elf文件压缩壳。
elf的格式是linux下主要的可执行文件格式,它也是在coff上基础上设计的,所以它和
pe文件的格式很相似,下面的叙述过程中会和 pe 文件以对比形式进行描述。
elf文件的第一个数据结构是以Elf32_Ehdr开始
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf32_Half e_type; /* Object file type */
Elf32_Half e_machine; /* Architecture */
Elf32_Word e_version; /* Object file version */
Elf32_Addr e_entry; /* Entry point virtual address */
Elf32_Off e_phoff; /* Program header table file offset */
Elf32_Off e_shoff; /* Section header table file offset */
Elf32_Word e_flags; /* Processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size in bytes */
Elf32_Half e_phentsize; /* Program header table entry size */
Elf32_Half e_phnum; /* Program header table entry count */
Elf32_Half e_shentsize; /* Section header table entry size */
Elf32_Half e_shnum; /* Section header table entry count */
Elf32_Half e_shstrndx; /* Section header string table index */
} Elf32_Ehdr;
e_ident 在 elf.h 中对应的 ELFMAG 宏,长度是四个字节
e_entry 入口点映像偏移(映像偏移即 pe 中所说的 rva)
e_phoff Elf32_Phdr 数组的文件偏移
e_shoff Elf32_Shdr 数组的文件偏移
e_ehsize Elf32_Ehdr 结构的大小
e_phentsize Elf32_Phdr 结构大小
e_phnum Elf32_Phdr 数组成员个数
e_shentsize Elf32_Shdr 结构大小
e_shnum Elf32_Shdr 数组成员个数
在Elf32_Ehdr之后即Elf32_Phdr数组,Elf32_Phdr的地址通过Elf32_Ehdr.e_ehsize来
确定。Elf32_Ehdr数组(或叫段表),你可以把phdr看做pe的节表。
typedef struct
{
Elf32_Word p_type; /* Segment type */
Elf32_Off p_offset; /* Segment file offset */
Elf32_Addr p_vaddr; /* Segment virtual address */
Elf32_Addr p_paddr; /* Segment physical address */
Elf32_Word p_filesz; /* Segment size in file */
Elf32_Word p_memsz; /* Segment size in memory */
Elf32_Word p_flags; /* Segment flags */
Elf32_Word p_align; /* Segment alignment */
} Elf32_Phdr;
p_type 描述这个段的加载行为属性
p_offset 段数据在文件中偏移,类似pe节中的PointerToRawData
p_vaddr 段数据加载后在映像中偏移, 类似pe节中的VirtualAddress
p_filesz 段数据在文件中大小,类型pe节中的SizeOfRawData
p_memsz 段数据加载后在映像中大小,类型pe节中的VirtualSize
p_flags 描述这个段的内存属性,类似pe节中的节属性
p_align 段对齐粒度
p_type主要的类型有
PT_LOAD 这个段需要装载到内存中
PT_PHDR 这个段存放的是Elf32_Phdr数组
PT_INTERP 这个段存放一个解释器名,请求系统加载器把映像装载需求转给这
个解释器,关于elf的解释器问题,可以理解为windows下ntdll装载
pe文件,elf文件的解释器主要负责重定位、导入表填充等操作
p_flags 主要类型有
PF_X 这个段可执行
PF_W 这个段可写
PF_R 这个段可读
在Elf32_Ehdr(段表)之后便是Elf32_Shdr数组(节表),你可能到这里很奇怪了,怎么这
个叫节表?如果你熟悉pe应该知道节表对pe文件的重要性,但这个可不是pe中的那个节表,你
应该把它看做nt header中data_dir[]结构,加载器或调试器等工具会通过节名确定节具体
用途,比如存储调试信息、版本信息、字符串表等等、elfp在加壳过程会丢弃节表。
下面简单讲讲elfp的loader处理过程(加壳过程很简单),在一个elf文件被加载后,它的
入口点在执行之前,堆栈中会由系统加载器push的一些参数。
// 堆栈结构:
// +-------------------+
// | return address | 返回地址
// +-------------------+
// | argc | 参数个数
// +-------------------+
// | argv[?], NULL | 参数表,以 NULL 结尾
// +-------------------+
// | envp[?], NULL | 环境表,以 NULL 结尾
// +-------------------+
// | auxv[?] | 中文不知道叫什么它,这个主要是给解释器使用,
// +-------------------+ 存放这个elf的相关信息如果被加壳程序需要解
释器,你需要重写正确填写这个参数,让解释器可
以正确的找到相关数据的地址。
elfp壳loader的执行流程大致如下:
申请内存-->把每个段解压到指定的地址上-->获取被加壳程序原始信息-->检查原
始段表、重写 auxv-->加载解释器-->调用解释器
关于 elf 的解释器,可以参考资料链接上的文字,那里比我描述的完整。
五、附录
[1] 本文代码
./pstzine_0A_01.zip
-EOF-