%title缩略图

ELF程序头

EFL的程序头是对二进制文件段的描述,是程序装载所必须的一部分。它就像一张表,里头有多个条目,每个条目就是一条段(segment)描述信息,所以很多文章都称它为程序头表,你可以称它为程序头表也可以称它为程序头,它描述了段的基本信息,例如段的类型、段数据的偏移地址等等,它只对可执行文件和共享对象文件有意义。

注意:段(segment)是在内核装载时被解析的。段描述了可执行文件的内存布局以及如何映射到内存中。

下面我们打开elf手册看看ELF程序头的构成

%title插图%num
%title插图%num

可以看到程序头表的由8个字段构成,严格意义来讲应该说是程序头表中的每个段的构成是由8个字段构成,分别是:

p_type 长度:4个字节
p_flags 长度:4个字节
p_offset 长度:8个字节
p_vaddr 长度:8个字节
p_paddr 长度:8个字节
p_filesz 长度:8个字节
p_memsz 长度:8个字节
p_align 长度:8个字节

由于程序头表跟文件头想关联,通过前一篇文章我们知道程序头的文件偏移地址是0x40,即文件的64字节后面开始是程序头表,那么程序头表多大呢?通过前一篇文章已经知道了e_phentsize*e_phnum的结果就是程序头表的大小,e_phentsize表示每个条目(即段)的大小(大小固定:56个字节),它的文件偏移地址是0x36,十进制是54,e_phnum表示程序头表有多少个条目,它的文件偏移地址是0x38,十进制是56,那么我们用xxd指令看看e_phentsize的大小是多少,e_phnum的数量是多少

%title插图%num

可以看到e_phentsize的大小是0x38,十进制是56,e_phnum的数量是0xb,十进制是11,56*11=616。说明hello,world实例文件的程序头表里头有11个条目,即11个段,每个段的大小是56个字节,整个程序头表的大小是616个字节。

既然有11个段,那么我们来逐个查看

第1个段(segment)

使用xxd指令从文件偏移地址为64的地方开始取56个字节。

%title插图%num

如图取出了第一个段(segment),内容是什么呢,直接看根本看不懂,继续结合elf手册,上文提到了每个段分别由8个字段构成,那么我们挨个查看elf手册

p_type

%title插图%num

p_type。翻译过来简述就是该字段表示段(segment)的类型。如图是10种类型,由于该手册比较老旧,我这边对比4.4.166内核,发现一共由14种类型,在该基础上增加了PT_TLS、PT_LOOS、PT_HIOS和PT_GNU_EH_FRAME

%title插图%num

PT_TLS。看注释的意思是局部线程存储段,具体用意暂未知

PT_LOOS,PT_HIOS看注释是与特定操作系统有关的

PT_GNU_EH_FRAME。暂未知,看到其它文章解释说是该字段保存的数据是在.eh_frame_hdr节中定义的异常处理信息的位置与大小,即与.eh_fream_hdr节相结合才有意义。

我的示例文件p_type的内容是06000000,由于字节序是小端序,所以是0x6,即对应的是PT_PHDR,说明第一个段的类型是PT_PHDR,elf手册对PT_PHDR的解释是该类型如果存在则表示程序头表本身在文件和程序的内存映像中的位置和大小。 在文件中,此段类型可能不会出现多次。 仅当程序头表是程序的存储映像的一部分时,它才可能发生。 如果存在,它必须在任何可装入段条目之前。

%title插图%num

p_flags

%title插图%num

翻译过来就是段的权限标记。即段的读、写和执行权限标记。我这里是04000000,即0x4

%title插图%num

对应的是PF_R标记,表示该段权限是可读,如下图是Linux源码中的标记值。

%title插图%num

p_offset

%title插图%num

简述就是该字段表示该段(segment)在文件中的偏移量。我这里该字段的内容是4000000000000000 即0x40,偏移量是64,说明PT_PHDR这个段的段数据在文件中的偏移量是64

%title插图%num

p_vaddr

%title插图%num

翻译过来就是该字段保存段(segment)的第一个字节在内存中的虚拟地址。我这里该字段的内容是4000000000000000,即0x40。说明PT_PHDR在内存中的虚拟地址是0x40

%title插图%num

p_paddr

%title插图%num

翻译过来就是该字段表示段(segment)的物理地址,然而在BSD系统里该字段没有使用,值是0。我这里是4000000000000000,即0x40。说明PT_PHDR的物理地址是0x40

%title插图%num

p_filesz

%title插图%num

翻译过来就是该字段保存该段(segment)的文件映像中的字节数。 可能为零。我这里是6802000000000000,即0x268,十进制是616,值得注意的是我发现大小正好是示例文件的程序头表的大小。

%title插图%num

p_memsz

%title插图%num

翻译过来就是该字段保存段的内存映像中的字节数。可能是零。实际上经过验证发现该字段同样保存的是程序头表的大小。我这里是6802000000000000,即0x268,十进制是616

%title插图%num

p_align

%title插图%num

翻译过来就是该字段保存的是该段(segment)在内存和文件中的对齐字节数,p_vaddr和p_offset必须具有一致的值,以页大小为模。 值如果是0和1则表示不需要对齐。 否则,p_align应该是2的正整数幂。我这里的值是0800000000000000,即0x8,说明该段是8个字节一对齐。8正好是2的正整数幂。

%title插图%num

第2个段(segment)

已知第1个段是PT_PHDR段,在程序头表里的描述信息大小是56个字节,那么64+56字节后就是第2个段,我们继续使用xxd指令取第2个段的内容,因为每个段的大小是56个字节,所以继续取56个字节

%title插图%num

p_type是03000000,即0x3

%title插图%num

看看0x3在linux源码中对应的是什么类型

%title插图%num

看到是PT_INTERP类型,即该段是PT_INTERP段。PT_INTERP段是用于存放程序动态链接器的信息,该段以null为终止符。

接下来看看p_flags,p_flags是04000000,即0x4

%title插图%num

linux内核源码中4表示只读的权限标志,说明PT_INTERP段是具有可读权限。

p_offset。再接着看看p_offset,即PT_INTERP段的文件偏移量,我这里是a802000000000000,即0x2a8,十进制是680

%title插图%num

再接着看看p_vaddr,即PT_INTERP段在内存中的虚拟地址,我这里是a802000000000000,即0x2a8

%title插图%num

再接着看看p_paddr,即PT_INTERP段的物理地址,我这里是a802000000000000,即0x2a8

%title插图%num

再接着看看p_filesz,PT_INTERP段的文件映像中的字节数,我这里是1c00000000000000,即0x1c,十进制是28

%title插图%num

再来看看p_memsz,即PT_INTERP段的内存映像中的字节数,我这里是1c00000000000000,即0x1c,十进制是28

%title插图%num

最后看看p_align,即PT_INTERP段的对齐字节数,如果为0或者为1则表示不需要对齐,我这里是0100000000000000,即0x1,说明PT_INTERP段不需要对齐

%title插图%num

看完了PT_INTERP段的描述信息,那么我们接下来可以尝试看看PT_INTERP段的具体数据是什么,一致PT_INTERP段的数据大小是28个字节,文件偏移地址是680字节,我们使用xxd指令看看

%title插图%num

已知PT_INTERP段是存放程序动态链接器的,如图已经得到了证明,动态链接器的位置是:/lib64/ld-linux-x86-64.so.2

第3个段(segment)

已知第2个段是PT_INTERP段,在程序头表里的描述信息大小是56个字节,那么120+56字节后就是第3个段,我们继续使用xxd指令取第3个段的内容,因为每个段的大小是56个字节,所以继续取56个字节

%title插图%num

p_type。p_type是01000000,即0x1

%title插图%num

查看Linux源码看看1表示什么段

%title插图%num

是PT_LOAD段,一个可执行文件至少有1个PT_LOAD段,该段存放的数据将被装载或者映射到内存中,文件中的字节被映射到内存段的开头。

p_flags。接下来继续查看p_flags字段,p_flags的内容是04000000,即0x4

%title插图%num

内核源码中对应该值的权限标志是只读标志,说明PT_LOAD段的权限是只读

p_offset。接下来继续查看p_offset字段,我这边p_offset字段是0000000000000000,即0

%title插图%num

说明该段偏移地址是0

p_vaddr。再看看p_vaddr字段,我这边p_vaddr字段也是0

%title插图%num

说明该段在内存中的虚拟地址也是0

p_paddr。再看看p_paddr字段,同样也是0

%title插图%num

说明该段在内存中的物理地址也是0

p_filesz。看看该段数据在文件映像中的字节数。我这里是6005000000000000,即0x560,十进制是1376

%title插图%num

说明PT_LOAD字段在文件映像中的字节数是1376个字节。

p_memsz。看看PT_LOAD段数据在内存映像中的字节数。我这里是6005000000000000,即0x560,十进制是1376

%title插图%num

说明PT_LOAD段在内存映像中的字节数也是1376个字节。

p_algin。看看该段在文件和内存中是多少字节一对齐,我这里是0010000000000000,即0x1000,十进制是4096

说明PT_LOAD段的数据在文件和内存中是4096个字节一对齐。

看完了PT_LOAD段的描述信息,那么我们接下来可以尝试看看PT_LOAD段的具体数据是什么,已知PT_LOAD段的数据大小是1376个字节,文件偏移地址是0字节,我们使用xxd指令看看

%title插图%num

可以看到包含了文件头,程序头表等等信息,这些信息最终会被加载到内存中。

注意:PT_LOAD段的对齐字节数不管是在文件中还是在内存中都是4096个字节,我这里只查看了实际的字节数是1376个字节(p_filesz字段的值),4096-1376=2720,这2720个字节其实都是填充的字节,如果非要查看一下也可以,把1376换成4096,即可查看整个PT_LOAD段的段数据,但是你会看到后面大部分都是0,因为实际字节数是1376个字节(p_filesz字段的值),其余是填充的,为了内存对齐用的。

第4个段(segment)

已知第3个段是PT_LOAD段,在程序头表里的描述信息大小是56个字节,那么176+56字节后就是第4个段,我们继续使用xxd指令取第4个段的内容,因为每个段的大小是56个字节,所以继续取56个字节

%title插图%num

p_type。先看看是什么段,我这里是01000000,即0x1

%title插图%num

已知linux源码中p_type类型值为1,对应的段是PT_LOAD,与上一个相同,又是一个PT_LOAD段,这是第2个PT_LOAD段了,前面说过了PT_LOAD段至少有一个,所以出现多个不要惊讶。

p_flags。再看看权限标志,我这里是05000000,即0x5

%title插图%num

权限值是5,对应的是什么权限呢?让我们看看Linux源码

源码中,只有4,2和1,分别对应读权限,写权限和执行权限,熟悉Linux权限的都知道以上3个权限是可以并存的,数值相加。4,2和1能组成5的就只有4和1了,4表示读权限,1表示执行权限,说明这第2个PT_LOAD段是可读可执行的权限。

p_offset。再看看这第2个PT_LOAD段在文件中的偏移量或者说偏移地址。我这里是0010000000000000,即0x1000,十进制4096

%title插图%num

说明第2个PT_LOAD段在文件中的偏移地址是4096,或者说偏移量是4096。

p_vaddr和p_paddr也是0010000000000000,即0x1000

%title插图%num

说明第2个PT_LOAD段在内存中的虚拟地址是0x1000,物理地址也是0x1000

p_filesz和p_memsz的内容是一样的,都是bd01000000000000,即0x1bd,十进制是445

%title插图%num

说明第2个PT_LOAD段的数据在文件映像中的大小是0x1bd,即445个字节,在内存映像中的大小也是0x1bd,即445个字节

p_algin。看看第2个PT_LOAD段的数据在内存中的对齐字节数,我这里是0010000000000000,即0x1000,十进制4096

%title插图%num

第2个PT_LOAD段的数据在文件和内存中的对齐字节数也是4096个字节。

看完了第2个PT_LOAD段的描述信息,那么我们接下来可以尝试看看第2个PT_LOAD段的具体数据是什么,已知第2个PT_LOAD段的数据大小是445个字节,文件偏移地址是4096字节,我们使用xxd指令看看

%title插图%num

一堆看不懂的数据。里面存放的是程序的代码和数据。

第5个段(segment)

已知第4个段是第二个PT_LOAD段,在程序头表里的描述信息大小是56个字节,那么232+56=288,288个字节后就是第5个段,我们继续使用xxd指令取第5个段的内容,因为每个段的大小是56个字节,所以继续取56个字节

%title插图%num

几个前面几轮拆分,这里就不去挨个拆分了。

p_type。从图中直接看出p_type是01000000,即0x1,表示PT_LOAD段,说明这是第3个PT_LOAD段

p_flags。这里是04000000,即0x4,表示第3个PT_LOAD段是可读权限。

p_offset。这里是0020000000000000,即0x2000,十进制是8192,说明第3个PT_LOAD段的段数据在文件中的偏移量是8192个字节。

p_vaddr。这里也是0020000000000000,即0x2000,十进制是8192,说明第3个PT_LOAD段的段数据在内存中的虚拟地址是8192个字节。

p_paddr。这里也是0020000000000000,即0x2000,十进制是8192,说明第3个PT_LOAD段的段数据在内存中的物理地址也是8192个字节。

p_filesz。这里是5801000000000000,即0x158,十进制是344,说明第3个PT_LOAD段的段数据在文件映像中的大小344个字节。

p_memsz。这里也是5801000000000000,即0x158,十进制是344,说明第3个PT_LOAD段的段数据在内存映像中的大小也是344个字节。

p_align。这里是0010000000000000,即0x1000,十进制是4096,说明第3个PT_LOAD段的段数据对齐字节数是4096个字节。

看完了第3个PT_LOAD段的描述信息,那么我们接下来也看看第3个PT_LOAD段的具体数据是什么,已知第3个PT_LOAD段的数据大小是344个字节,文件偏移地址是8192字节,我们使用xxd指令看看

%title插图%num

我们这回在第3个PT_LOAD段看到了程序会打印的字符串Hello,world!,那说明PT_LOAD段的数据跟程序逻辑代码有关了。

第6个段(segment)

已知第5个段是第3个PT_LOAD段,在程序头表里的描述信息大小是56个字节,那么288+56=344,344个字节后就是第6个段,我们继续使用xxd指令取第6个段的内容,因为每个段的大小是56个字节,所以继续取56个字节

%title插图%num

p_type。这里是01000000,又是一个PT_LOAD段,这是第4个PT_LOAD段。

p_flags。这里是06000000,即0x6,4+2=6,4是可读权限,2是可写权限,说明第4个PT_LOAD段的权限是可读可写。

p_offset。这里是e82d000000000000,即0x2de8,十进制是11752,说明第4个PT_LOAD段的段数据在文件中的偏移量是11752个字节。

p_vaddr和p_paddr是一样的,e83d000000000000,即0x3de8,十进制是15848,说明第4个PT_LOAD段的段数据在内存中的虚拟地址和物理地址都是0x3de8。

p_filesz。这里是4802000000000000,即0x248,十进制是584,说明第4个PT_LOAD段的段数据在文件映像中的大小是584个字节。

p_memsz。这里是5002000000000000,即0x250,十进制是592,说明第4个PT_LOAD段的段数据在内存映像中的大小是592个字节。

p_align。这里是0010000000000000,即0x1000,十进制是4096,说明第4个PT_LOAD段的段数据对齐字节数是4096个字节。

看完了第4个PT_LOAD段的描述信息,那么我们看看这第4个PT_LOAD段的具体数据是什么,已知第4个PT_LOAD段的数据大小是584个字节,文件偏移地址是11752字节,我们使用xxd指令看看

%title插图%num

同样是看不懂的数据。目前仅仅知道里面的数据是一些代码和数据

第7个段(segment)

已知第6个段是第4个PT_LOAD段,在程序头表里的描述信息大小是56个字节,那么344+56=400,400个字节后就是第7个段,我们继续使用xxd指令取第7个段的内容,因为每个段的大小是56个字节,所以继续取56个字节

%title插图%num

p_type。这里是02000000,这是一个PT_DYNAMIC段,如下图:

%title插图%num

看看elf手册里对该段(segment)的解释

%title插图%num

手册里解释说PT_DYNAMIC段是代表动态链接信息的段。经过查阅相关资料进一步解释说PT_DYNAMIC段,也称动态段,该段是动态链接可执行文件所特有的段,包含了动态链接器所必须的一些信息。在动态段数据中包含了一些标记值和指针,包括但不限于以下几种:

运行时需要链接的共享库列表
全局偏移表(GOT表)的地址
重定位条目的相关信息

上面说该段的数据中包含了一些标记值和指针,那么有哪些标记值呢?如下:

DT_HASH。表示符号散列表的地址
DT_STRTAB。字符串表的地址
DT_SYMTAB。符号表地址
DT_RELA。相对地址重定位表的地址
DT_RELASZ。Rela表的字节大小
DT_RELAENT。Rela表每个条目的字节大小
DT_STRSZ。字符串表的字节大小
DT_SYMENT。符号表每个条目的字节大小
DT_INIT。初始化函数的地址
DT_FINI。终止函数的地址
DT_SONAME。共享目标文件名的字符串表偏移量
DT_RPATH。库搜索路径的字符串表偏移量(已弃用)
DT_SYMBOLIC。修改链接器,在可执行文件之前的共享目标文件中搜索符号
DT_REL。Rel relocs表的地址
DT_RELSZ。Rel表的大小
DT_RELENT。Rel表每个条目的字节大小
DT_PLTREL。PLT引用的reloc类型(Rela或Rel)
DT_DEBUG。还未进行定义,为调试保留
DT_TEXTREL。缺少此项表明重定位只能应用于可写段
DT_JMPREL。仅用于PLT的重定位条目地址
DT_BIND_NOW。指示动态链接器在控制权交给可执行文件之前处理所有的重定位
DT_RUNPATH。库搜索路径的字符表偏移量

p_flags。这里是06000000,即0x6,4+2=6,4是可读权限,2是可写权限,说明PT_DYNAMIC段的权限是可读可写。

%title插图%num

p_offset。这里是f82d000000000000,即0x2df8,十进制是11768,说明PT_DYNAMIC段的段数据在文件中的偏移量是11768个字节。

p_vaddr和p_paddr是一样的,f83d000000000000,即0x3df8,十进制是15864,说明PT_DYNAMIC段的段数据在内存中的虚拟地址和物理地址都是0x3df8。

p_filesz。这里是e001000000000000,即0x1e0,十进制是480,说明PT_DYNAMIC段的段数据在文件映像中的大小是480个字节。

p_memsz。这里是e001000000000000,即0x1e0,十进制是480,说明PT_DYNAMIC段的段数据在内存映像中的大小是480个字节。

p_align。这里是0800000000000000,即0x8,十进制是8,说明PT_DYNAMIC段的段数据对齐字节数是8个字节。

看完了PT_DYNAMIC段的描述信息,那么我们看看这PT_DYNAMIC段的具体数据是什么,已知PT_DYNAMIC段的数据大小是480个字节,文件偏移地址是11768字节,我们使用xxd指令看看

%title插图%num

这些数据是动态链接文件所必须的一些信息。暂不做讨论,后面会利用本示例文件专门进行拆解。

第8个段(segment)

已知第7个段是第PT_DYNAMIC段,在程序头表里的描述信息大小是56个字节,那么400+56=456,456个字节后就是第8个段,我们继续使用xxd指令取第8个段的内容,因为每个段的大小是56个字节,所以继续取56个字节

%title插图%num

p_type。如图p_type是04000000,即0x4,对应的是PT_NOTE,说明是PT_NOE段。

%title插图%num

PT_NOE段保存的数据是附加信息,例如与特定供应商或者特定系统相关的信息。有时供应商或者系统构建者需要在目标文件上标记特定的信息,以便于其它程序对一致性、兼容性等等进行检查。SHT_NOTE类型的节(section)和PT_NOTE段就是用于这一目的。该节(section)或者该段(segment)中的数据条目可以有任意数量,每个条目的大小都是4字节。事实上,PT_NOTE只保存了操作系统的规范信息,在可执行文件运行是不需要这个段的(因为系统会假设可执行文件是本地的),这个段成了很容易被病毒感染的一个地方。

p_flags。我的示例文件显示的是04000000,即0x4,说明是可读的权限。

%title插图%num

p_offset。我这儿是c402000000000000,即0x2c4,十进制是708,说明PT_NOTE段的段数据是在文件的708个字节处开始的。

p_vaddr。跟p_offset是一样的,也是0x2c4,十进制是708,说明PT_NOTE段在内存中的虚拟地址是0x2c4。

p_paddr。跟p_vaddr是一样,也是0x2c4,十进制是708,说明PT_NOTE段在内存中的物理地址是0x2c4。

p_filesz和p_memsz的内容是一样的,都是4400000000000000,即0x44,十进制是68,说明PT_NOTE段的段数据在文件映像中和内存映像中的大小是68个字节。

p_align。对齐字节数,我这里是0400000000000000,即0x4,说明PT_NOTE段的对齐字节数是4字节一对齐。

看完了PT_NOTE段的描述信息,那么我们看看这PT_NOTE段的具体数据是什么,已知PT_NOTE段的数据大小是68个字节,文件偏移地址是708字节,我们使用xxd指令看看

%title插图%num

这些内容保存的是操作系统的规范信息。

第9个段(segment)

已知第8个段是第PT_NOTE段,在程序头表里的描述信息大小是56个字节,那么456+56=512,512个字节后就是第9个段,我们继续使用xxd指令取第9个段的内容,因为每个段的大小是56个字节,所以继续取56个字节

%title插图%num

p_type。p_type是50e57464,即0x6474e550,查看Linux源码

%title插图%num

原来该段是PT_GNU_EH_FRAME段,该段保存的数据是在.eh_frame_hdr节中定义的异常处理信息的位置与大小。

p_flags。p_flags是04000000,即0x4,说明PT_GNU_EH_FRAME段段是可读权限。

p_offset。p_offset是0x1420000000000000,即0x2014。说明PT_GNU_EH_FRAME段段在文件中的偏移量是0x2014

p_vaddr和p_paddr也是0x1420000000000000,说明PT_GNU_EH_FRAME段段在内存中的虚拟地址和物理地址也是0x2014

p_filesz和p_memsz都是3c00000000000000,即0x3c,说明PT_GNU_EH_FRAME段段的段数据在文件映像中和内存映像中的大小都是0x3c

p_align。p_align是0400000000000000,即0x4,说明PT_GNU_EH_FRAME段段的段数据的对齐是4字节。

第10个段(segment)

已知第9个段是第PT_GNU_EH_FRAME段,在程序头表里的描述信息大小是56个字节,那么512+56=568,568个字节后就是第10个段,我们继续使用xxd指令取第10个段的内容,因为每个段的大小是56个字节,所以继续取56个字节

%title插图%num

p_type。p_type的内容是51e57464,即0x6474e551。

%title插图%num

说明该段是PT_GNU_STACK段。该段是GNU扩展,Linux内核使用该扩展通过p_flags成员中设置的权限标志来控制堆栈的状态。

p_flags。p_flags是06000000,即0x6,说明PT_GNU_STACK段的权限是可读和可写的权限。

p_offset、p_vaddr、p_paddr、p_filesz和p_memsz都是0

p_align。p_align是1000000000000000,即0x10,说明PT_GNU_STACK的对齐字节数是4字节一对齐。

第11个段(segment)

这是示例文件的最后一个段。

已知第10个段是第PT_GNU_STACK段,在程序头表里的描述信息大小是56个字节,那么568+56=624,624个字节后就是第11个段,我们继续使用xxd指令取第11个段的内容,因为每个段的大小是56个字节,所以继续取56个字节

%title插图%num

p_type。p_type是52e57464,即0x6474e552。段是什么类型呢

查阅到相关资料,该段的名称是PT_GNU_RELRO,PT_GNU_RELRO段可用来为数据段开启RELRO安全保护机制。RELRO是一种保护技术,可以用来防止恶意共享库的注入、dtors注入和got.plt注入

p_flags。p_flags是04000000,即0x4,权限是只读

p_offset。p_offset是e82d000000000000,即0x2de8,PT_GNU_RELRO段的文件偏移量是0x2de8,这个偏移量正好是第4个PT_LOAD段,这第4个PT_LOAD段前面已经看过了,权限是可读可写,说明里面存放的是数据,载入内存后就是数据段,而这里的PT_GNU_RELRO的保护对象就是数据段,所以偏移量相等

p_vaddr和p_paddr是一样的,都是e83d000000000000,即0x3de8,说明PT_GNU_RELRO段的数据在内存中的虚拟地址和物理地址都是0x3de8

p_filesz和p_memsz也是一样的,都是1802000000000000,即0x218,十进制是536,说明PT_GNU_RELRO段的数据在文件映像中和内存映像中的大小是536个字节。

p_align。p_align是0100000000000000,即0x1,已知该字段为0或1时说明不要求对齐。

程序头表的拆解结束。

发表回复