一点声明
原文链接: 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)让被跟踪进程运行注入的代码.
下面有两个源文件,一个是要被注入的汇编代码,另外一个是跟踪程序.最后还有一个将被跟踪的程序.
编译代码:
#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年的老文章了).在下一篇文章我会解释为什么,可能的话,会说明解决的办法.
你好,那段shellcode有源程序吗,我的机器上执行显示段错误