ELF相关知识

最近一个月学了一些零散的知识,本来在学习操作系统相关,想对着清华的ucore自己操作一下,结果bootloader没有达到理想的效果。 然后zxy同学拉我去搞个比赛,要求实现一个对ELF签名的工具,最后要写一个内核模块去hook系统调用,如果签名验证通过就可以执行,否则 不能执行。我的思路大致是这样的:

  1. 熟悉openssl,实现基本RSA签名
  2. 完成ELF文件读写模块,能够新建一个section,并将sign插入其中
  3. 能读.text section,并对其签名
  4. 完成证书相关的签名
  5. 内核模块的开发

目前我已经基本完成了第三步,遇到的主要困难有:

  1. openssl的文档过于垃圾,不知道怎么用
  2. ELF文件的解析,需要十分熟悉其结构

当然可以预知内核模块肯定也不好办,我之前都没有接触过。下面主要整理一下ELF相关的知识,以及给ELF文件添加一个section的方法。

ELF相关

首先给一个ELF文件的结构图:

object_file_format.png

ELF文件有两种视图,一种是链接版,一种是执行版。链接版主要由section构成,运行版则主要有segment构成。那么section和segment的区别是什么? 实际上segment由section构成,在映射到虚拟内存中后,就是我们常说的data segment,code segment之类的。所以我们主要关心section相关的结构。

首先,我们可以看到,ELF文件由ELF Header,program Header,section,section Header构成。当然链接视图中,program header是可选的,因为他主要用于告诉系统如何创建进程。用于生成进程的目标文件必须具有程序头部表,但是重定位文件不需要这个表。下面给一个比较形象的图:

elf-layout.png

可以看到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或者阅读《程序员的自我修养》这本书,内容十分详细,我们这里只带大家复习一些比较重要的字段。

我们的目标是向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呢?步骤大致如下:

  1. 想Section Header Table中插入new section Header
  2. .shstrtabsection中插入new section的section name
  3. 修改.shstrtabsh_size
  4. 修改ELF Header中的e_shnum字段
  5. 向目标位置写入section内容

那么我们似乎遇到了一些困惑,.shstrtabsection在ELF文件的中间,万一空间不够添加section name了咋整。不如我们直接添加到文件的末尾吧,然后修改一下sh_name不就可以了吗。 似乎想法不错,但是这里需要注意的是,我们要记得修改.shstrtabsh_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的其他字段设置正确就可以了。

接下来,我们需要修改.shstrtabsh_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

可以看到我们成功多加了一个.signsection,再来看看文件尾:

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,大家可以自己试一下。