一点声明
原文链接: http://linuxgazette.net/issue83/sandeep.html
原文作者: Sandeep S
这篇文章其实没有必要翻译,因为主要是讲ELF的,限于篇幅作者没有讲得很深入.实际上,已经有一本国产好书<程序员的自我修养>对ELF做了很详尽的阐述.如果有时间的话,推荐看看.但是,因为本系列还有个Part 3,为了使得系列连贯,很简单的翻译了.

上一篇文章中,我已经介绍过ptrace的基本特性,并给大家演示了一个小例子.正如上篇文章所描述的那样,ptrace的主要功能是跟踪一个正在运行的进程的内存或者寄存器(不管是为了调试还是其他邪恶的念头).正因如此,我们首先续约对二进制可执行文件有一些基本的了解,然后才能更加深入的认识到怎样查看甚至修改它们.在这篇文章中,我会简单的介绍ELF,它是Linux下常见的二进制文件格式.最后,我们会学习一个程序,它修改另一进程的进程空间,以此改变这个进程的输出.

1. ELF是什么?

ELF是Executable and Linking Format的缩写.它定义了Linux下二进制可执行(executable binaries)文件的组织结构,可重定位(relocatable)文件,共享文件(shared object)和进程崩溃产生的core文件也遵循ELF格式.链接器和装载器也使用ELF,ELF提供了一个有效的方法,使得链接器和装载器可以从各自的视角查看文件的内容.

2. ELF头部

每个ELF文件都有一个ELF头部.它总是从文件偏移量为0的地方开始,包含了二进制文件的组织细节--文件能否被解释(interpreted),使用了什么数据结构,等等.

ELF头部的额结构如下(参考/usr/src/include/linux/elf.h)

#define EI_NIDENT       16

typedef struct elf32_hdr{
  unsigned char e_ident[EI_NIDENT];
  Elf32_Half    e_type;
  Elf32_Half    e_machine;
  Elf32_Word    e_version;
  Elf32_Addr    e_entry;  /* Entry point */
  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;

简单介绍一下:

* e_ident

这个最初的字段标示了该文件为一个object文件,提供了一个机器无关的数据,解释文件的内容.

* e_type

该成员确定该object的类型: 可重定位文件,可执行文件,共享文件,core文件.

* e_machine

该成员变量指出了运行该程序需要的体系结构: Intel 386,Alpha,Sparc等等.

* e_version

这个成员确定object文件的版本.

* e_phoff

该成员保持着程序头表(program header table)在文件中的偏移量(以字节计数),假如该文件没有程序头表的的话,该成员就保持为0.

* e_shoff

该成员保持着section头表(section header table)在文件中的偏移量(以字节计数),假如该文件没有section头表的的话,该成员就保持为0.

* e_flags

该成员保存着相关文件的特定处理器标志,在i386中被弃用.

* e_ehsize

该成员保存着ELF头大小(以字节计数).

* e_phentsize & e_shentsize

这两个成员分别保存着在文件的程序头表(program header table)中一个入口的大小(以字节计数,所有的入口都是同样的大小)和section头表(section header table)中一个入口的大小(以字节计数,所有的入口都是同样的大小).

* e_phnum & e_shnum

这两个成员分别保存着在程序头表(program header table)和section头表(section header table)中的入口数目.程序头表是由e_phnum个program header组成的一维数据,section头表与此类似.

* e_shstrndx

该成员保存着跟section名字字符表相关入口的section头表(section header table)索引.

3. Sections 和 Segments

正如上面所说的,链接器认为一个ELF文件是由一组section组成的(组织信息在section header table),而装载器认为一个ELF文件是由一组segment组成的(组织信息在program header table).下面是对section header和segment/program header的详细介绍.

3.1 ELF Sections and Section Headers

在每个ELF文件中,总有一个由一组section头(section header)组成的section头表(section header table).第0个section头总为空,没有任何作用.每个section头都遵循着下面的结构参考(/usr/src/include/linux/elf.h):

typedef struct elf32_shdr {
  Elf32_Word sh_name;           /* Section name, index in string tbl (yes Elf32) */
  Elf32_Word sh_type;           /* Type of section (yes Elf32) */
  Elf32_Word sh_flags;          /* Miscellaneous section attributes */
  Elf32_Addr sh_addr;           /* Section virtual addr at execution */
  Elf32_Off sh_offset;          /* Section file offset */
  Elf32_Word sh_size;           /* Size of section in bytes */
  Elf32_Word sh_link;           /* Index of another section (yes Elf32) */
  Elf32_Word sh_info;           /* Additional section information (yes Elf32) */
  Elf32_Word sh_addralign;      /* Section alignment */
  Elf32_Word sh_entsize;        /* Entry size if section holds table */
} Elf32_Shdr;

简单介绍一下各数据成员的细节:

* sh_name

该成员指定了这个section的名字,它的值是section节头字符表section的索引,以NULL空字符结束.setcion的名字有很多种,这里列出一些:

.text 程序包含的机器指令.
.data 被初始化的数据.
.init 保存可执行指令,它构成了进程的初始化代码.(译注: 因此,当一个程序开始运行时,在main函数被调用之前,系统执行.init的中的代码.)

* sh_type

该成员把sections按内容和意义分类: 程序数据(program data),符号表(symbol table), 字符串表(string table)等等.

* sh_flags

section支持位的标记,用来描述多个属性.

* sh_addralign
一些sections有地址对齐的约束,例如,假如一个section保存着双字,系统就必须确定整个section是否双字对齐.值0和1意味着该section没有对齐要求.

剩下的都是自解释的数据成员,这里不加说明.

3.2 ELF Segments And Program Headers

ELF segment将在程序被装载时(进程内存映像被生成时)使用.每一个segment的信息都由一个segment头描述. segment头的的描述如下:

typedef struct
{
  Elf32_Word    p_type;                 /* Segment type */
  Elf32_Off     p_offset;               /* Segment file offset */
  Elf32_Addr    p_vaddr;                /* Segment virtual address */
  Elf32_Addr    p_paddr;                /* Segment physical address */
  Elf32_Word    p_filesz;               /* Segment size in file */
  Elf32_Word    p_memsz;                /* Segment size in memory */
  Elf32_Word    p_flags;                /* Segment flags */
  Elf32_Word    p_align;                /* Segment alignment */
} Elf32_Phdr;

解释一下:

* p_type

该成员指出怎样解释该数组元素的信息.类型值包括:unused,loadable,Dynamic linking information,reserved等等

* p_vaddr

该成员表示该段在内存中的首字节地址.

* p_paddr

该成员给出该段的物理地址(将被加载到内存中).

* p_flags

该成员给出了和该段相关的标志.

* p_align
该成员给出了该段在内存和文件中对齐值(alignment),如果这个段是可装载的,那么对齐值就是内存页(page)的大小.

剩下的都是自解释的数据成员,这里不加说明.

4. 装载ELF文件

对ELF我们已经有了一定的了解,接下来我们需要知道ELF是在何时被怎样加载的.通常我们在终端中输入想要运行的文件,这看起来实在简单,但是,在敲入回车之后,实际上发生了很多有趣的事.

首先shell调用libc函数,然后由libc函数调用内核例程.球就这么被踢给了内核,内核将打开这个文件,查看它的类型和格式,然后装载该程序和相关的库,接着初始化进程的栈空间,最后把控制权交给该进程.

程序被加载到0x08048000(查一下/proc/pid/maps)栈从0xBFFFFFFF开始(向下增长).

5. 注入代码

我们已经了解了程序是如何被装载到内存的细节了,很容易知道,当给出了一个进程和它的内存地址是,我们就能够跟踪它(假定我们拥有权限),并访问它的私有数据.Hmm,说起来确实简单,但实际上,实现起来还是有难度的.不管怎样,我们试试吧.

首先,我们写一个程序,读取另一进程的寄存器并修改之.由上一篇文章可知,request的可选值为:

PTRACE_ATTACH : Attach一个进程.
PTRACE_DETACH : Detach一个进程.

注意: 别忘了使用这些可选值,否则的话,哼哼,进程被暂停了就很难被恢复啦.

PTRACE_GETREGS : 读取进程的寄存器值到参数data中,参数addr被无视.data的数据结构如下:

struct user_regs_struct {
                long ebx, ecx, edx, esi, edi, ebp, eax;
                unsigned short ds, __ds, es, __es;
                unsigned short fs, __fs, gs, __gs;
                long orig_eax, eip;
                unsigned short cs, __cs;
                long eflags, esp;
                unsigned short ss, __ss;
        };

PTRACE_SETREGS : 与GETREGS相反.
PTRACE_POKETEXT : 从data读4个字节,写入被跟踪进程,写入地址为addr.

步骤是这样子的,我们将想要被执行的代码注入被跟踪进程内存映像,然后通过修改IP(instruction pointer)让被跟踪进程运行注入的代码.

下面有两个源文件,一个是要被注入的汇编代码,另外一个是跟踪程序.最后还有一个将被跟踪的程序.

Tracer.c
Code.S
Sample.c

编译代码:

#cc Sample.c -o loop
#cc Tracer.c Code.S -o catch

打开另外一个终端,运行loop程序.

#./loop

回到原先的终端,运行跟踪程序来跟踪loop进程,并改变它的输出.

//译注: 原文是#./catch `ps ax | grep "loop" | cut -f 3 -d ' '`,这在我的电脑上是cut不到loop的pid的.因为ps输出不一样.

#./catch `ps ax | grep "loop" | grep -v "grep" | cut -f 1 -d ' '`

打开运行loop进程的终端,看看输出吧!!

6. 小结

在第一篇文章中,我们展示了一个跟踪进程,对被跟踪进程运行的机器指令计数.在这篇文章中,我们研究了ELF,然后做了一个demo,向一个进程注入一段代码.在接下来的一篇文章中,我将向大家介绍怎样读取进程的内存.再见.

作者: Sandeep S

I am a final year student of Government Engineering College in Thrissur, Kerala, India. My areas of interests include FreeBSD, Networking and also Theoretical Computer Science.


译注: 文章的例子运行总是会segfault(没办法,毕竟是02年的老文章了).在下一篇文章我会解释为什么,可能的话,会说明解决的办法.