Linux-ELF文件结构
Linux-ELF文件结构
ELF文件结构
ELF Header
1 |
|
e_entry
该成员给出了系统首次将控制权转移到的虚拟地址,从而启动进程。如果文件没有相关的入口点,该成员的值为 0。
e_phoff⭐
该成员用于保存程序头表的文件偏移量(以字节为单位)。如果文件没有程序头表,该成员的值为 0。
e_shoff⭐
该成员保存段头表的文件偏移量(以字节为单位)。如果文件没有段头表,该成员的值为 0。
e_flags
该成员保存与文件相关的特定处理器标志。标志名称的格式为 EF_machine_flag。
e_ehsize
该成员保存以字节为单位的 ELF 头文件大小。
e_phentsize
该成员保存文件的程序头表中一个条目的大小(以字节为单位);所有条目大小相同。
e_phnum
该成员保存程序头表中条目的数量。因此,e_phentsize 和 e_phnum 的乘积就是表的字节大小。如果文件没有程序头表,则 e_phnum 的值为零。
e_shentsize
该成员表示段头的大小(以字节为单位)。段头是段头表中的一个条目;所有条目大小相同。
e_shnum
该成员用于保存段标头表中的条目数。因此,e_shentsize 和 e_shnum 的乘积就是段头表的大小(以字节为单位)。如果文件没有段头表,e_shnum 的值为 0。
如果段的数量大于或等于 SHN_LORESERVE(0xff00),则该成员的值为 0,段头表条目的实际数量包含在索引为 0 的段头的 sh_size 字段中(否则,初始条目的 sh_size 成员的值为 0)。
e_shstrndx
该成员保存与段名称字符串表相关的章节页眉表索引。如果文件没有章节名称字符串表,则该成员的值为 SHN_UNDEF。更多信息请参阅下文的 “章节 “和 “字符串表”。
如果段名字符串表段索引大于或等于 SHN_LORESERVE (0xff00),则该成员的值为 SHN_XINDEX (0xffff),段名字符串表段的实际索引包含在段头的 sh_link 字段中,索引为 0(否则,初始条目的 sh_link 成员包含 0)。
Program Header
Program Header (linuxbase.org)
可执行文件或共享对象文件的Program Header是一个结构数组,每个结构描述一个程序段或系统准备执行程序所需的其他信息。一个对象文件段包含一个或多个部分,如下文 “段内容 “所述。程序头只对可执行文件和共享对象文件有意义。文件通过 ELF 头的 e_phentsize 和 e_phnum 成员指定自己的程序头大小。
Program Header描述了ELF加载到内存进行执行时的信息。在从内存中dump下ELF时,Program Header会保留,而用于描述在静态文件时信息的Section Header则会被丢弃。
1 | typedef struct { |
p_type
该成员说明该数组元素描述的段的类型,或如何解释数组元素的信息。类型值及其含义如下。
p_offset⭐
该成员给出段的第一个字节所在文件开头的偏移量。
p_vaddr⭐
该成员给出段的第一个字节在内存中的虚拟地址。
p_paddr
在需要物理寻址的系统中,该成员为段的物理地址保留。由于 System V 忽略了应用程序的物理寻址,因此对于可执行文件和共享对象,该成员的内容未作指定。
p_filesz⭐
该成员给出段的文件映像字节数;可能为零。
p_memsz⭐
该成员给出段的内存映像的字节数;可能为零。
p_flags
该成员给出与段相关的标志。定义的标志值如下所示。
p_align
正如处理器补编本章 “程序加载 “所述,可加载进程段的 p_vaddr 和 p_offset 值必须一致,并与页面大小相乘。该成员给出了段在内存和文件中的对齐值。值 0 和 1 表示不需要对齐。否则,p_align 应为 2 的正整幂,p_vaddr 应等于 p_offset,并与 p_align 相乘。
关于更细节的成员组枚举数,详见
Program Header (linuxbase.org)
Section Header
1 | typedef struct { |
sh_name
该成员指定段的名称。其值是段头字符串表段的索引,给出了空尾字符串的位置。
sh_type
该成员对段的内容和语义进行分类。
sh_flags
部分支持描述其他属性的 1 比特标志。
sh_addr⭐
如果分段将出现在进程的内存映像中,该成员将给出分段第一个字节所在的地址。否则,该成员包含 0。
sh_offset⭐
该成员的值给出了从文件开头到该部分第一个字节的字节偏移量。下面描述的 SHT_NOBITS 部分类型在文件中不占空间,其 sh_offset 成员确定了文件中的概念位置。
sh_size
该成员以字节为单位给出段的大小。
除非段类型为 SHT_NOBITS,否则该段在文件中占用 sh_size 字节。SHT_NOBITS 类型的段的大小可能不为零,但它不占用文件中的任何空间。
sh_link
该成员包含一个段表的索引链接,其解释取决于分节类型。
sh_info
该成员包含额外信息,其解释取决于段类型。
如果该节标头的 sh_flags 字段包含 SHF_INFO_LINK 属性,则该成员表示节标头表索引。
sh_addralign
某些段具有地址对齐限制。例如,如果一个部分包含一个双字,系统必须确保整个部分的双字对齐。sh_addr 的值必须与 0 一致,并与 sh_addralign 的值相乘。目前,只允许使用 0 和 2 的正积分幂。0 和 1 表示该部分没有对齐限制。
sh_entsize
某些部分包含固定大小的条目表,如符号表。对于这样的部分,该成员以字节为单位给出每个条目的大小。如果分区没有固定大小的条目表,则该成员的值为 0。
一般SectionTable的最后一个元素,是.shstrtab
,这里存储了个各个节区的名字。
内存dump Patch⭐
将一个ELF从内存中加载回来后,需要进行一定的patch。
p_offset
与p_vaddr
加载进内存后,ELF的文件排布就会根据内存加载后的进行分布。
但是,Program Header本身是不会被修改的,因此需要手动修改。
p_offset
原来指向的是原来的文件存储下的偏移位置,但是由于加载到内存了,所以真实偏移应该是跟着p_vaddr
的,所以应该将p_offset
设置为p_vaddr
。
p_filesz
与p_memsz
原理同上,反正就设置p_filesz=p_memsz
补全Section Header
首先观察静态ELF的Section Header的特征。
- 在Program Header最后一个元素后紧跟着,即最后一个元素的
p_offset+p_filesz
得到的结果就是section_header_table
的偏移 - ELF Header的
e_shoff
正好对上了Section Header的偏移
然后观察内存dump后得到的Section Header特征:
- 原来
p_offset+p_filesz
指向的地方是错误的;而p_vaddr+p_memsz
指向的位置现在应该是存放Section Header的地方,但是还是被空字节填充了,需要修补 - ELF Header没有被修改过,因此
e_shoff
现在指向的还是原来的偏移,但现在应该修改到p_vaddr+p_memsz
所指向的地方
因此,应该:
- 修改ELF Header,将其指向ProgramHeader最后一个元素的
p_vaddr+p_memsz
- 将
p_vaddr+p_memsz
所指向的地方用SectionHeader进行填充。
修复Section Header
要把SectionTable中每个元素的s_offset
修改成s_addr
。原理和上面的p_offset=p_vaddr
一样
,但是最后3个元素保留下来,进行详细分析。
下面提供我现在分析的例子。此ELF程序一共有27个元素(0-26):
在patch前:
第24个元素是:
- .bss
- s_addr: 0x119A370
- s_offset: 0x1199370
第25个:
- .comment
- s_addr: 0x0
- s_offset: 0x1199370
第26个:
- .shstrtab
- s_addr: 0x0
- s_offset: 0x1199370
可以发现,第24个元素是.bss段,且s_addr
是非零值,意味着此段会被加载进内存中;而25和26元素则是0,意味着这两个分段按理只会留存在文件中,不会加载进内存;但是至少.shstrtab
是很重要的一个段,这里面存储了SectionTable每一个元素的s_name
。
但是!根据s_offset
可以发现,他们指向的实际上都是同一块文件区域!
因此,这里提出一种修改方式:
- 将这三个元素的
s_offset
都先修改成.bss
段的s_addr
- 检查
s_addr
,发现为空,没有想要的.shstrtab
段名;因此从静态ELF里面把.shstrtab
复制到s_addr
处。
修复Dynamic Symbol Table
似乎在修补好Section Table后,这里会自动找到。如果没有那就是没有,这玩意有可能会被strip掉。
Rebase
加载到IDA后,以防万一可以再根据起始地址Rebase一下。将Image Base设置成dump下来的起始基址。
破坏ELF文件头实现反静态分析
[原创]简单粗暴的so加解密实现-Android安全-看雪-安全社区|安全招聘|kanxue.com
本作者通过对linker的分析,提出了以下通过破坏so文件头结构来反静态分析的方法:
- e_ident[EI_NIDENT] 字段包含魔数、字节序、字长和版本,后面填充0。对于安卓的linker,通过verify_elf_object函数检验魔数,判定是否为.so文件。那么,我们可以向位置写入数据,至少可以向后面的0填充位置写入数据。遗憾的是,我在fedora 14下测试,是不能向0填充位置写数据,链接器报非0填充错误。
- 对于安卓的linker,对e_type、e_machine、e_version和e_flags字段并不关心,是可以修改成其他数据的(仅分析,没有实测)
- 对于动态链接库,e_entry 入口地址是无意义的,因为程序被加载时,设定的跳转地址是动态连接器的地址,这个字段是可以被作为数据填充的。
- so装载时,与链接视图没有关系,即e_shoff、e_shentsize、e_shnum和e_shstrndx这些字段是可以任意修改的。被修改之后,使用readelf和ida等工具打开,会报各种错误,相信读者已经见识过了。
- 既然so装载与装载视图紧密相关,自然e_phoff、e_phentsize和e_phnum这些字段是不能动的。
但是由于这是14年的文章,可能有所变化,还是要具体分析。
根据博主提供的解密挑战APP在高版本安卓已经无法运行为推测,此魔改手段应该已经失效了。
基于特定section的加解密实现(deprecated)
[原创]简单粗暴的so加解密实现-Android安全-看雪-安全社区|安全招聘|kanxue.com
- 将指定Native方法单独放在一个
.mytext
段里面 - 然后对编译后的so进行魔改
- 读取ELF Header的
e_shoff
(Section Table在文件中的偏移),e_shnum
(Section Table中元素数量),e_shstrtab
- 然后遍历Section Table,最后定位
.mytext
段的位置 - 然后将此段进行加密,写回ELF文件
- 然后基于前面ELF头部分可改的推测:将ELF头的
e_shoff
用.mytext
段地址覆盖;将e_entry
用.mytext
段的大小与地址覆盖。
- 读取ELF Header的
- 然后再在so里面提前实现解密逻辑
- 在
.init_array
里面实现解密逻辑(用__attribute__((constructor))
) - 读取so基址,读取ELF头,读取
e_entry
和e_shoff
获取.mytext
段的地址与大小,然后mprotect
修改内存权限,解密内存数据,写回去,恢复执行
- 在
基于特定函数的加解密实现(deprecated)
[原创]简单粗暴的so加解密实现-Android安全-看雪-安全社区|安全招聘|kanxue.com
这个更复杂一点,不过核心思路是读取.dynamic
符号表,来获取导出函数所在地址,并进行加解密。(针对Android so中的导出函数,因为so总是有函数表的)
Android Linker
关于linker的分析放在了.init
的相关blog里面。
Helpful Tools
010Editor的ELF.bt模板很好用!
F5刷新脚本
参考
Program Header (linuxbase.org)
- 本文作者: Taardis
- 本文链接: https://taardisaa.github.io/2023/10/12/Linux-ELF文件结构/
- 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!