ELF相关知识
最近一个月学了一些零散的知识,本来在学习操作系统相关,想对着清华的ucore自己操作一下,结果bootloader没有达到理想的效果。 然后zxy同学拉我去搞个比赛,要求实现一个对ELF签名的工具,最后要写一个内核模块去hook系统调用,如果签名验证通过就可以执行,否则 不能执行。我的思路大致是这样的:
- 熟悉openssl,实现基本RSA签名
- 完成ELF文件读写模块,能够新建一个section,并将sign插入其中
- 能读.text section,并对其签名
- 完成证书相关的签名
- 内核模块的开发
目前我已经基本完成了第三步,遇到的主要困难有:
- openssl的文档过于垃圾,不知道怎么用
- ELF文件的解析,需要十分熟悉其结构
当然可以预知内核模块肯定也不好办,我之前都没有接触过。下面主要整理一下ELF相关的知识,以及给ELF文件添加一个section的方法。
ELF相关
首先给一个ELF文件的结构图:
ELF文件有两种视图,一种是链接版,一种是执行版。链接版主要由section构成,运行版则主要有segment构成。那么section和segment的区别是什么? 实际上segment由section构成,在映射到虚拟内存中后,就是我们常说的data segment,code segment之类的。所以我们主要关心section相关的结构。
首先,我们可以看到,ELF文件由ELF Header,program Header,section,section Header构成。当然链接视图中,program header是可选的,因为他主要用于告诉系统如何创建进程。用于生成进程的目标文件必须具有程序头部表,但是重定位文件不需要这个表。下面给一个比较形象的图:
可以看到program header主要和segment有关,section header则存储了每个section相关的表项。而ELF header则存储了ELF的相关信息,比如代码段入口,section的数目,section header的offset之类的。
#define EI_NIDENT 16
typedef struct {
unsigned char e_ident[EI_NIDENT]; // 标志符
ELF32_Half e_type;
ELF32_Half e_machine;
ELF32_Word e_version;
ELF32_Addr e_entry;
ELF32_Off e_phoff;
ELF32_Off e_shoff;
ELF32_Word e_flags;
ELF32_Half e_ehsize;
ELF32_Half e_phentsize;
ELF32_Half e_phnum;
ELF32_Half e_shentsize;
ELF32_Half e_shnum;
ELF32_Half e_shstrndx;
} Elf32_Ehdr;
这个是ELF Header的结构体,这个结构体一般存储了ELF文件的关键信息,通常情况下,我们可以通过readelf -h a
来查看一个ELF文件的头,其实熟悉之后直接xxd a
手撕16进制就行了(逃…那么我们接下来随便举个栗子:
root@Aurora:/home/code/solo/rubbish/hardwork/casual(master⚡) # readelf -h a
ELF 头:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
类别: ELF32
数据: 2 补码,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: DYN (共享目标文件)
系统架构: Intel 80386
版本: 0x1
入口点地址: 0x1060
程序头起点: 52 (bytes into file)
Start of section headers: 14328 (bytes into file)
标志: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 11
Size of section headers: 40 (bytes)
Number of section headers: 30
Section header string table index: 29
这是一个十分简单的hello world程序,再接下来给出前0x80个字节的hexdump:
00000000: 7f45 4c46 0101 0100 0000 0000 0000 0000 .ELF............
00000010: 0300 0300 0100 0000 6010 0000 3400 0000 ........`...4...
00000020: f837 0000 0000 0000 3400 2000 0b00 2800 .7......4. ...(.
00000030: 1e00 1d00 0600 0000 3400 0000 3400 0000 ........4...4... // 其实ELF Header到0x33即(1d00)的时候就结束了
00000040: 3400 0000 6001 0000 6001 0000 0400 0000 4...`...`.......
00000050: 0400 0000 0300 0000 9401 0000 9401 0000 ................
00000060: 9401 0000 1300 0000 1300 0000 0400 0000 ................
00000070: 0100 0000 0100 0000 0000 0000 0000 0000 ................
00000080: 0000 0000 bc03 0000 bc03 0000 0400 0000 ................
这里是readelf -S -W a
的末尾结果:
[24] .data PROGBITS 00004014 003014 000008 00 WA 0 0 4
[25] .bss NOBITS 0000401c 00301c 000004 00 WA 0 0 1
[26] .comment PROGBITS 00000000 00301c 000026 01 MS 0 0 1
[27] .symtab SYMTAB 00000000 003044 000450 10 28 45 4
[28] .strtab STRTAB 00000000 003494 00025c 00 0 0 1
[29] .shstrtab STRTAB 00000000 0036f0 000105 00 0 0 1
在这里不想太深入的介绍每个字段代表的详细意义,具体请参考ctf-wiki或者阅读《程序员的自我修养》这本书,内容十分详细,我们这里只带大家复习一些比较重要的字段。
- e_shoff
这一项给出节头表在文件中的字节偏移( Section Header table OFFset )如果文件中没有节头表,则为 0 - e_shentsize
这一项给出节头的字节长度(Section Header ENTry SIZE)。一个节头是节头表中的一项;节头表中所有项占据的空间大小相同。 - e_shnum
这一项给出节头表中的项数(Section Header NUMber)。因此, e_shnum 与 e_shentsize 的乘积即为节头表的字节长度。如果文件中没有节头表,则该项值为 0。 - e_shstrndx
这一项给出节头表中与节名字符串表相关的表项的索引值(Section Header table InDeX related with section name STRing table)。如果文件中没有节名字符串表,则该项值为SHN_UNDEF。
我们的目标是向ELF中插入一个section,那么首先要清楚ELF是怎么存储并识别section的,按照开发者的思路,我们很容易想到我们可以把每个section的信息和特征抽象成一个struct,然后把所有的struct存入一个数组,最后把数组的地址放入ELF Header就可以了。这样我们就可以直接通过ELF Header来获得section struct数组,进而间接获得section的信息。其实上面这几个字段的作用便是如此。存储struct的数组就是上面提到的Section Header Table,在ELF文件的末尾。
.shstrtab
是一个字符串section,他存储了所有section的名字,ELF Header中的e_shstrndx
便是其在Section Header Table中的索引,因此想要获得.shstrtab
的真是偏移我们只需要按如下公式计算:
shstrtabOff = e_shoff + e_shstrndx * e_shentsize // 基址 + 索引 × 大小
那怎么获得section name在.shstrtab
中的具体偏移呀?其实section struct的真是名字叫Elf32_Shdr(32位)
typedef struct {
ELF32_Word sh_name;
ELF32_Word sh_type;
ELF32_Word sh_flags;
ELF32_Addr sh_addr;
ELF32_Off sh_offset;
ELF32_Word sh_size;
ELF32_Word sh_link;
ELF32_Word sh_info;
ELF32_Word sh_addralign;
ELF32_Word sh_entsize;
} Elf32_Shdr;
这个结构提存储了每个section的详细信息,第一个字段sh_name
是section name在.shstrtab
中的偏移。
那么我们究竟要怎样才能插入一个section呢?步骤大致如下:
- 想Section Header Table中插入new section Header
- 向
.shstrtab
section中插入new section的section name - 修改
.shstrtab
的sh_size
- 修改ELF Header中的
e_shnum
字段 - 向目标位置写入section内容
那么我们似乎遇到了一些困惑,.shstrtab
section在ELF文件的中间,万一空间不够添加section name了咋整。不如我们直接添加到文件的末尾吧,然后修改一下sh_name不就可以了吗。
似乎想法不错,但是这里需要注意的是,我们要记得修改.shstrtab
的sh_size
,否则还没查到文件末尾就终止了。
修改后的ELF尾应该长这样:
|------------------------|
| section Header Table |----->这里已经插入了new section header
|------------------------|
| new section name |
|------------------------|
| section contain |
|------------------------|----> end
插入new section Header难点在计算shstrtab的偏移,首先你需要用filesize - shstrtab.sh_offset
来获得字符串在.shstrtab
中的偏移,至于怎么获得shstrtab.sh_offset
…你需要先根据ELF Header中的相关字段找到.shstrtab
的section Header,获取对应字段就ok了。这里需要注意要将section header的其他字段设置正确就可以了。
接下来,我们需要修改.shstrtab
的sh_size
,用filesize - shstrtab.sh_offset
就ok了,然后去修改ELF Header里的e_shnum
字段,加1即可。最后向文件尾插入对应size的内容就ok了,最后我们看一下程序运行的demo:
[25] .bss NOBITS 0000000000004078 003078 000008 00 WA 0 0 1
[26] .comment PROGBITS 0000000000000000 003078 000026 01 MS 0 0 1
[27] .symtab SYMTAB 0000000000000000 0030a0 0006d8 18 28 45 8
[28] .strtab STRTAB 0000000000000000 003778 0002af 00 0 0 1
[29] .shstrtab STRTAB 0000000000000000 003a27 0008cf 00 0 0 1
[30] .sign NOTE 00000000000042f8 0042f8 000100 00 A 0 0 1
可以看到我们成功多加了一个.sign
section,再来看看文件尾:
000042f0: 2e73 6967 6e00 0000 24e4 639b 5a57 60ec .sign...$.c.ZW`.
00004300: 1aa4 e313 cb4d 3fb9 9177 0539 2551 a21d .....M?..w.9%Q..
...
000043f0: be18 7eb1 25af f246 ..~.%..F
确实符合section name + section contain的结构,看来原理是没问题的,经测试可以通过readelf和objdump的检测。最后附上源码地址ELFSign,大家可以自己试一下。
阅读量