Linux逆向---ELF格式分析之文件头和程序头

在Linux下,可以利用vim编辑器来对编译生成后的可执行程序进行编辑,比如说把75jne指令改成74je指令,这样可以在不重新编译的情况下去修改程序的控制流,这样玩感觉还是很有意思的,不过也仅限于此,所以我借了一本书想要学学逆向。。结果发现这本书真的难啃。。如果只是读它的内容的话很快就读过去了,但是会发现读完之后自己还是什么都不知道,于是我决定慢慢读,并且用例子去对照着看,感觉这样或许会有些效果。

这里我的系统是64位Ubuntu,32位和64位的可执行程序的十六进制表示还是有一些区别的,所以这里有必要说明一下,很显著的一个特征就是32位中用4个字节表示的东西,这里需要用8个字节来进行表示。

1.源代码:

这里为了简单期间,我实现了一个helloworld。。然后用它编译后的程序来进行之后的分析。

hello.c:

#include <stdio.h>
int main()
{
    printf("hello world");
    return 0;
}

编译生成hello.out

gcc hello.c -o hello.out

2.文件头

1.查看文件头信息:

readelf -h hello.out

输出:

ELF 头:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  类别:                              ELF64
  数据:                              2 补码,小端序 (little endian)
  版本:                              1 (current)
  OS/ABI:                            UNIX - System V
  ABI 版本:                          0
  类型:                              EXEC (可执行文件)
  系统架构:                          Advanced Micro Devices X86-64
  版本:                              0x1
  入口点地址:               0x400430
  程序头起点:          64 (bytes into file)
  Start of section headers:          6616 (bytes into file)
  标志:             0x0
  本头的大小:       64 (字节)
  程序头大小:       56 (字节)
  Number of program headers:         9
  节头大小:         64 (字节)
  节头数量:         31
  字符串表索引节头: 28

2.查看十六进制格式:

hexedit hello.out

输出

00000000   7F 45 4C 46  02 01 01 00  00 00 00 00  00 00 00 00  .ELF............
00000010   02 00 3E 00  01 00 00 00  30 04 40 00  00 00 00 00  ..>.....0.@.....
00000020   40 00 00 00  00 00 00 00  D8 19 00 00  00 00 00 00  @...............
00000030   00 00 00 00  40 00 38 00  09 00 40 00  1F 00 1C 00  [email protected]...@.....

通过man 5 ELF对ELF手册的查看,可以知道头部可以使用一个结构体表示:

#define EI_NIDENT 16
typedef struct {
	unsigned char e_ident[EI_NIDENT];
	uint16_t      e_type;
	uint16_t      e_machine;
    uint32_t      e_version;
    ElfN_Addr     e_entry;
    ElfN_Off      e_phoff;
    ElfN_Off      e_shoff;
    uint32_t      e_flags;
    uint16_t      e_ehsize;
    uint16_t      e_phentsize;
    uint16_t      e_phnum;
    uint16_t      e_shentsize;
    uint16_t      e_shnum;
    uint16_t      e_shstrndx;
} ElfN_Ehdr;

3.分析

e_ident:

第一行为ELF头,共16个字节 ,表示了e_ident

  • 0~4:MAGIC

  • 5: 02表示64位文件,若为1则为32位,否则都不是

  • 6: 01表明是小端编码,为2则为大端编码

  • 7: 01文件版本,1表明是当前版本

  • 8~16: 暂时未用到,用于以后扩展

e_type

02 00 实际上应该为00 02,后面的部分看的时候也需要转换一下,即数值2,表明为可执行文件,其他的数值及类型对应关系在man手册中也都能查到。

e_machine

003E 为体系架构,

e_version

0001 为当前版本,也就是数值1

e_entry

30 04 40 00 00 00 00 00 ->00 00 00 00 00 40 04 30 即0x400430,为程序入口地址

e_phoff

0000 0000 0000 0040程序头起点 0x40=64byte

e_shoff

0000 0000 0000 19D8节头起点 0x19d8=6616

e_flags

0000 0000 标志:0x0

e_ehsize

0040 ELF头长度 0x40=64

e_phentsize

0038 程序头长度 0x38=56

e_phnum

0009 程序头表的项目数量 9

e_shentsize

0040 节头表的项目大小 0x40=64字节

e_shnum

001F 节头数量0x1f=31

e_shstrndx

001C 字符串索引节头 0x1c=28

3.程序头实例:

程序头对段的描述,是程序装载必需的一部分。

1.查看程序头表:

使用如下命令:

readelf -l hello.out

输出:

Elf 文件类型为 EXEC (可执行文件)
入口点 0x400430
共有 9 个程序头,开始于偏移量 64

程序头:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001f8 0x00000000000001f8  R E    8
  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000006fc 0x00000000000006fc  R E    200000
  LOAD           0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                 0x0000000000000228 0x0000000000000230  RW     200000
  DYNAMIC        0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
                 0x00000000000001d0 0x00000000000001d0  RW     8
  NOTE           0x0000000000000254 0x0000000000400254 0x0000000000400254
                 0x0000000000000044 0x0000000000000044  R      4
  GNU_EH_FRAME   0x00000000000005d0 0x00000000004005d0 0x00000000004005d0
                 0x0000000000000034 0x0000000000000034  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     10
  GNU_RELRO      0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                 0x00000000000001f0 0x00000000000001f0  R      1

 Section to Segment mapping:
  段节...
   00     
   01     .interp 
   02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame 
   03     .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 
   04     .dynamic 
   05     .note.ABI-tag .note.gnu.build-id 
   06     .eh_frame_hdr 
   07     
   08     .init_array .fini_array .jcr .dynamic .got

这个表中的每一项都是对应着结构体Elf64_Phdr的:

           typedef struct {
               uint32_t   p_type;
               uint32_t   p_flags;
               Elf64_Off  p_offset;
               Elf64_Addr p_vaddr;
               Elf64_Addr p_paddr;
               uint64_t   p_filesz;
               uint64_t   p_memsz;
               uint64_t   p_align;
           } Elf64_Phdr;

2.对各段的观察

PHDR:

在实例中的地址为0x40~0x238,可以用hexedit去观察它的内容,不过基本都是不可见字符,所以写出来没什么意义。

PHDR段保存了程序头表本身的位置和大小,程序头表则保存了所有的程序头对文件中段的描述信息。

这里我们可以简单的进行一个计算,这个结构体所占空间为4+4+8+8+8+8+8+8=56,一共9个项目,所以

hex(56)+0x40=0x238,这也足以说明这一段的含义。

INTERP:

对程序解释器位置的描述 0x238-0x254,这一段可以用hexedit观察到内容:

从可见字符可知,这个程序的程序解释器为:/lib64/ld-linux-x86-64.so.2

									2F 6C 69 62  36 34 2F 6C  ......../lib64/l
00000240  64 2D 6C 69  6E 75 78 2D  78 38 36 2D  36 34 2E 73  d-linux-x86-64.s
00000250  6F 2E 32 00  										  o.2.				   

LOAD1:

程序代码段: 0x000-0x6fc,这里内容太多就不贴出来了,主要是一些机器指令。

LOAD2:

数据段:0xe10-0x1038,这里大多数是二进制数据,全部贴出来也没有什么意义

DYNAMIC

动态段:0xe28~0xff8

动态段包含了动态链接器所必需的一些信息,在动态段中包含了一些标记和指针。包括运行时所需要的共享库列表、全局偏移表的地址,以及重定位条目的相关信息等。

NOTE:

0x254-0x298,保存了与特定供应商或者系统相关的附加信息,实际的可执行文件运行时并不需要这个段,可以查看一下这一段的内容。

						04 00 00 00  10 00 00 00  01 00 00 00  o.2.............
00000260   47 4E 55 00  00 00 00 00  02 00 00 00  06 00 00 00  GNU.............
00000270   20 00 00 00  04 00 00 00  14 00 00 00  03 00 00 00   ...............
00000280   47 4E 55 00  3D F4 DD 79  8B A7 5D 1A  69 C7 CD C9  GNU.=..y..].i...
00000290   1E E5 C1 CD  69 A2 C8 F4

GNU_*:

这部分似乎并不怎么被关注,并且man手册中提的也很少,所以先忽略掉。

3.分析

通过对以上的地址观察,我们可以得到一些结论:

  • 执行所需的程序部分总体上可以看做是代码段和数据段组成的。
  • PHDR、INTERP、NOTE段被包含在了代码段中。
  • DYNAMIC段被包含在了数据段中。

4.程序的剩余部分

剩余地址的部分为对ELF节头的描述,如果去掉,程序仍然可以正常运行,但是无法利用节头来引用节,默认是有节头的,这里我也是试验了一下,因为我分析的文件就是一个可执行文件,数据段停止于0x1038,于是我将0x1038之后的所有字节全部清除掉,这里我做了一个副本hello1.out,然后用vim+:%!xxd做到的直接对二进制文件进行编辑。编辑之后再查看差不多是这个样子:

pic2

然后运行一下编辑之后的程序:

pic3

可以来查看一下编辑后的hello1.out的大小,也发生了变化:

pic4

可见程序仍可运行,而我们用指令去查询这个新程序的节头表时,会出现以下的现象:

pic5

可见,这个程序的确找不到节头表了,尽管没有节头可执行程序仍然可以运行,但是这样的程序会让gdb、objdump这样的工具没法排上用场,也会对逆向造成极大的障碍。

猜你喜欢

转载自blog.csdn.net/zekdot/article/details/84594838