在Linux下,可以利用vim编辑器来对编译生成后的可执行程序进行编辑,比如说把75jne指令改成74je指令,这样可以在不重新编译的情况下去修改程序的控制流,这样玩感觉还是很有意思的,不过也仅限于此,所以我借了一本书想要学学逆向。。结果发现这本书真的难啃。。如果只是读它的内容的话很快就读过去了,但是会发现读完之后自己还是什么都不知道,于是我决定慢慢读,并且用例子去对照着看,感觉这样或许会有些效果。
这里我的系统是64位Ubuntu,32位和64位的可执行程序的十六进制表示还是有一些区别的,所以这里有必要说明一下,很显著的一个特征就是32位中用4个字节表示的东西,这里需要用8个字节来进行表示。
1.源代码:
这里为了简单期间,我实现了一个helloworld。。然后用它编译后的程序来进行之后的分析。
hello.c:
#include
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 ....@.8...@.....
通过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做到的直接对二进制文件进行编辑。编辑之后再查看差不多是这个样子:
然后运行一下编辑之后的程序:
可以来查看一下编辑后的hello1.out的大小,也发生了变化:
可见程序仍可运行,而我们用指令去查询这个新程序的节头表时,会出现以下的现象:
可见,这个程序的确找不到节头表了,尽管没有节头可执行程序仍然可以运行,但是这样的程序会让gdb、objdump这样的工具没法排上用场,也会对逆向造成极大的障碍。