ELF文件头读取修改

ELF = Executable and Linkable Format,可执行连接格式,是UNIX系统实验室(USL)作为应用程序二进制接口(Application Binary Interface,ABI)而开发和发布的。扩展名为elf。

ELF 文件有三种类型:可重定位文件:也就是通常称的目标文件,后缀为.o。共享文件:也就是通常称的库文件,后缀为.so。可执行文件:本文主要讨论的文件格式,总的来说,可执行文件的格式与上述两种文件的格式之间的区别主要在于观察的角度不同:一种称为连接视图(Linking View),一种称为执行视图(Execution View)。
一个典型的ELF文件有两种描述视图:program header和section header.
program header:是对程序运行时所使用的段的描述.
section header: 是对所有二进制段的描述.
每一个ELF文件是由一个ELF 文件头(ELF header)和其余的文件数据构成.这些文件数据包括一下一些内容:
·Program header table 描述0个或是多个段(segments)
·Section header table, 描述0个或是多个节(sections)
·要写到上面两个表中的数据.
段(segments)包含的是程序运行是必要的信息
节(sections)包含的是链接和重定向时所需要的重要数据
同一时间整个文件中的每个beyt不会属于一个以上的段,但是也可以存在不属于任何段的字节.

linux下ELF文件分析工具:
readelfis: 是一个unix下的二进制工具,用来显示一个或多个ELF文件的信息. 
elfdump: 是一个Solaris命令,用来查看单个ELF文件的信息.
objdump:     可以查看ELF文件或是其它对象格式的更多信息.


关键词:Dynamic binding/ld.so/mdb/link map/Solaris

1. 基本概念

Link-Editor - 链接器:即ld(1),输入一个或多个输入文件(*.o/*.so/*.a),经过连接和解释数据,输出一个目标文件(*.o/*.so/*.a/可执行 文件)。ld通常作为编译环境的一部分来执行。

Runtime Linker - 动态链接器: 即ld.so.1(1), 在运行时刻处理动态的可执行程序和共享库,把可执行程序和共享库绑定在一起创建一个可执行的进程。

Shared objects - 共享对象: 也叫共享库,是动态链接系统的基础。共享对象类似与动态可执行文件,但共享对象没有被指定虚拟内存地址。 共享对象可以在系统中多个应用程序共同使用和共享。

Dynamic executables - 动态可执行文件:通常依赖于一个或者多个共享对象。 为了产生一个可以执行的进程,一个或者多个共享对象必须绑定在动态可执行文件上。

runtime linker主要负责以下几方面工作:


1.分析可执行文件中包含的动态信息部分(对ELF文件来说就是.dynamic section)来决定该文件运行所需的依赖库;2.定位和装载这些依赖库,分析这些依赖库所包含的动态信息部分,来决定是否需装载要任何附加的依赖库;3.对动态库进行必要的重定位,在进程的执行期间绑定这些对象;4.调用这些依赖库提供的初始化函数(ELF文件来说就是.init section,而且顺序是先执行依赖库的,再执行可执行文件的);5.把控制权转交给应用程序;6.在应用程序执行期间,能被再调用,来执行延后的函数绑定(即动态解析);7.在应用程序调用dlopen(3C)打开动态库和用dlsym(3C)绑定这些库的符号时,也要被调用;

2. 测试与验证


写一个最简的测试程序test.c:

#include int main(int agrc, char *argv[]){     printf (\"hello world\\n\");     return 0;}

编译和链接后产生ELF文件:

# cc test.c -o test# file testtest:        ELF 32-bit LSB executable 80386 Version 1, dynamically linked, not stripped

用mdb反汇编main函数:

# mdb test> main::dismain:                  pushl  %ebpmain+1:                 movl  %esp,%ebpmain+3:                 subl  $0x10,%espmain+6:                 movl  %ebx,-0x8(%ebp)main+9:                 movl  %esi,-0xc(%ebp)main+0xc:                movl  %edi,-0x10(%ebp)main+0xf:                pushl  $0x80506ecmain+0x14:               call  -0x148  <LT:printf>main+0x19:               addl  $0x4,%espmain+0x1c:               movl  $0x0,-0x4(%ebp)main+0x23:               jmp   +0x5    main+0x28:               movl  -0x4(%ebp),%eaxmain+0x2b:               movl  -0x8(%ebp),%ebxmain+0x2e:               movl  -0xc(%ebp),%esimain+0x31:               movl  -0x10(%ebp),%edimain+0x34:               leavemain+0x35:               ret

可以看到,main+0x14处调用了函数printf,调用前把传递的字符串参数压入栈:

> 0x80506ec/s0x80506ec:    hello world

“hello world”在ELF文件的.rodata1 section,处于test的代码段:

# /usr/ccs/bin/elfdump -c -N .rodata1 testSection Header[13]:  sh_name: .rodata1  sh_addr:    0x80506ec     sh_flags:  [ SHF_ALLOC ]  sh_size:    0xd         sh_type:   [ SHT_PROGBITS ]  sh_offset:   0x6ec        sh_entsize: 0  sh_link:    0          sh_info:   0  sh_addralign: 0x4

用mdb在main+0x14处设置断点,然后运行程序:

> main+0x14:b> :rmdb: stop at main+0x14mdb: target stopped at:main+0x14:    call  -0x148  <LT:printf>
程序在调用printf之前停止,我们计算一下printf的地址:

> main+0x14-0x148=X          8050544

验证一下,地址0x8050544是否正确:

# /usr/ccs/bin/elfdump -s -N .symtab test | grep printf    [38]  0x08050544 0x00000000  FUNC GLOB  D   0 UNDEF     printf# /usr/ccs/bin/elfdump -s -N .dynsym test | grep printf    [1]  0x08050544 0x00000000  FUNC GLOB  D   0 UNDEF     printf
在test文件的.symtab和.dynsym section都可以找到符号表中包含printf,符号表实际上是一个数组,数组元素定义如下:

typedef struct {  Elf32_Word st_name;  Elf32_Addr st_value;  Elf32_Word st_size;  unsigned char st_info;  unsigned char st_other;  Elf32_Half st_shndx;} Elf32_Sym;

printf的st_value就是0x08050544,在ELF的可执行文件中,这就是printf的虚存地址,而这恰好就是我们mdb中计算的地 址。

我们同样可以用nm(1)命令确认这一点:

# /usr/ccs/bin/nm -x test | grep printf[Index]  Value    Size   Type  Bind  Other Shndx  Name......[38]   |0x08050544|0x00000000|FUNC |GLOB |0   |UNDEF  |printf
printf的st_shndx的值是UNDEF,说明printf未在test中定义。既然程序可以链接通过,那么printf肯定存在于它依赖的共享 库中。

test依赖的共享库如下:

# ldd test     libc.so.1 =>    /lib/libc.so.1     libm.so.2 =>    /lib/libm.so.2
当一个程序有多个共享库依赖时,runtime linker是按照一定的顺序运行各个库的.init函数的,即前面提到的步骤4,查看顺序用ldd -i:

# ldd -i /usr/bin/cp     libcmdutils.so.1 =>    /lib/libcmdutils.so.1     libavl.so.1 =>  /lib/libavl.so.1     libsec.so.1 =>  /lib/libsec.so.1     libc.so.1 =>    /lib/libc.so.1     libm.so.2 =>    /lib/libm.so.2  init object=/lib/libc.so.1  init object=/lib/libavl.so.1  init object=/lib/libcmdutils.so.1  init object=/lib/libsec.so.1
test依赖的库只有libc(3LIB)和libm(3LIB),libm是数学库,因此printf一定在libc(3LIB)中。我们知道,在 libc(3LIB)库中,包含了System V, ANSI C, POSIX等多种标准的函数实现。

查看libc.so的符号表中的printf:

# /usr/ccs/bin/nm -x /usr/lib/libc.so | grep  \"|printf___FCKpd___13quot;[Index]  Value    Size   Type  Bind  Other Shndx  Name......[7653]  |0x00061f39|0x00000105|FUNC |GLOB |0   |11    |printf
libc.so中printf的st_value是0x00061f39,由于libc.so是一个共享库,因此这个地址只是printf在 libc.so中的偏移量,需要和libc.so的加载地址相加才可以得出真正的虚存地址,而这个地址才是真正的printf函数的代码入口。

libc.so中printf的st_shndx的值为11,当st_shndx是数值是,代表改函数所在的section header的索引号:

# /usr/ccs/bin/elfdump -c /usr/lib/libc.so | grep 11Section Header[11]:  sh_name: .text  sh_size:    0x110        sh_type:   [ SHT_SUNW_SIGNATURE ]
ELF文件test中的.symtab和.dynsym都包含了printf,而且st_value都相同,但是我们看到如果strip以后,nm命令没 有输出,这是因为test文件中的.symtab section被去除的原因:

# /usr/ccs/bin/strip test# /usr/ccs/bin/elfdump -s -N .symtab test | grep printf# /usr/ccs/bin/nm -x test1 | grep printf# /usr/ccs/bin/elfdump -s -N .dynsym test | grep printf    [1]  0x08050544 0x00000000  FUNC GLOB  D   0 UNDEF     printf

实际上只有.dynsym才被影射入内存,.dynsym是实现动态链接必须的信息,.symtab根本不会影射入内存。

在test创建的进程中,printf位于地址8050544,用mdb反汇编printf的代码:

> 8050544::disPLT:printf:              jmp   *0x8060714PLT:printf:              pushl  $0x18PLT:printf:              jmp   -0x4b   <0x8050504>LT:_get_exit_frame_monitor:   jmp   *0x8060718PLT:_get_exit_frame_monitor:   pushl  $0x20PLT:_get_exit_frame_monitor:   jmp   -0x5b   <0x8050504>................

可以看到,实际上,printf的代码只有3条指令,显然,这并不是真正printf的实现,而是叫做PLT的其中部分代码。


Global Offset Table - 全局偏移量表:GOT存在于可执行文件的数据段中,用于存放位置无关函数的绝对地址。GOT表中的绝对地址实际上是在运行阶段时,在位置无关函数首次被 runtime linker解析后才确定。在此之前,GOT中的初值主要是为了帮助PLT跳转到runtime linker,把控制权转交给它的动态绑定函数。

其实,.got的初值在test文件中已经定义:


# /usr/ccs/bin/elfdump -c -N .got testSection Header[14]:  sh_name: .got  sh_addr:    0x80606fc     sh_flags:  [ SHF_WRITE  SHF_ALLOC ]  sh_size:    0x20        sh_type:   [ SHT_PROGBITS ]  sh_offset:   0x6fc        sh_entsize: 0x4  sh_link:    0          sh_info:   0  sh_addralign: 0x4# /usr/ccs/bin/elfdump -G testGlobal Offset Table Section:  .got  (8 entries)ndx    addr    value   reloc          addend  symbol[00000]  080606fc  0806071c R_386_NONE      00000000[00001]  08060700  00000000 R_386_NONE      00000000[00002]  08060704  00000000 R_386_NONE      00000000[00003]  08060708  0805051a R_386_JMP_SLOT    00000000 atexit[00004]  0806070c  0805052a R_386_JMP_SLOT    00000000 __fpstart[00005]  08060710  0805053a R_386_JMP_SLOT    00000000 exit[00006]  08060714  0805054a R_386_JMP_SLOT    00000000 printf[00007]  08060718  0805055a R_386_JMP_SLOT    00000000 _get_exit_frame_monitor

可以看到,在ELF文件中的GOT共有8个表项:

   GOT[0]是保留项,被初始化为.dynamic section的起始地址。
   GOT[1]和GOT[2]初值为0,在装入内存后初始化。
   GOT[3]-GOT[7],被初始化成了对应符号的在PLT中第2条指令的地址。

GOT的结束地址也可以根据section header中的sh_size计算出来:

> 0x80606fc+20=X          806071c
而test运行到main+0x14断点处,查看GOT:


> 0x80606fc,9/naX0x80606fc:0x80606fc:    806071c0x8060700:    d17fd9000x8060704:    d17cb2600x8060708:    d17108140x806070c:    d1701e510x8060710:    805053a0x8060714:    805054a0x8060718:    805055a0x806071c:    1

可以看到,GOT的内容和ELF文件定义的初始值相比,有了一些变化:

> 0x80606fc,9/nap0x80606fc:0x80606fc:    0x806071c              --->未改变,.dynamic section的起始地址0x8060700:    0xd17fd900             --->改变,Rt_map首地址,也是link_map首地址0x8060704:    ld.so.1`elf_rtbndr        --->改变,Runtime linker的入口0x8060708:    libc.so.1`atexit         --->改变,已经被ld.so解析成绝对地址0x806070c:    libc.so.1`_fpstart        --->改变,已经被ld.so解析成绝对地址0x8060710:    PLT:exit              --->未改变,还未解析,指向PLT:exit的第2条指令0x8060714:    PLT:printf             --->未改变,还未解析,指向PLT:printf的第2条指令0x8060718:    PLT:_get_exit_frame_monitor  --->未改变,还未解析,指向PLT:_get_exit_frame_monitor的第2条指令0x806071c:    1


在此时,runtim linker把link map和自己的入口函数地址填入了GOT[1]和GOT[2]中,并且atexit和_fpstart已经被解析成绝对地址。这是因为每个可执行文件的实 际入口是_start例程,这个例程执行中会调用atexit和_fpstart,然后才调用main函数:


> _start::dis_start:                 pushl  $0x0_start+2:                pushl  $0x0_start+4:                movl  %esp,%ebp_start+6:                pushl  %edx_start+7:                movl  $0x806071c,%eax_start+0xc:              testl  %eax,%eax_start+0xe:              je    +0x7    <_start+0x15>_start+0x10:              call  -0x64   <LT:atexit>_start+0x15:              pushl  $0x80506cc_start+0x1a:              call  -0x6e   <LT:atexit>_start+0x1f:              leal  0x80607f4,%eax_start+0x25:              movl  (%eax),%eax_start+0x27:              testl  %eax,%eax_start+0x29:              je    +0x17   <_start+0x40>_start+0x2b:              leal  0x80607f8,%eax_start+0x31:              movl  (%eax),%eax_start+0x33:              testl  %eax,%eax_start+0x35:              je    +0xb    <_start+0x40>_start+0x37:              pushl  %eax_start+0x38:              call  -0x8c   <LT:atexit>_start+0x3d:              addl  $0x4,%esp_start+0x40:              movl  0x8(%ebp),%eax_start+0x43:              movl  0x80607d4,%edx_start+0x49:              testl  %edx,%edx_start+0x4b:              jne   +0xc    <_start+0x57>_start+0x4d:              leal  0x10(%ebp,%eax,4),%edx_start+0x51:              movl  %edx,0x80607d4_start+0x57:              andl  $0xfffffff0,%esp_start+0x5a:              pushl  %edx_start+0x5b:              leal  0xc(%ebp),%edx_start+0x5e:              movl  %edx,0x80607f0_start+0x64:              pushl  %edx_start+0x65:              pushl  %eax_start+0x66:              call  -0xaa   <LT:__fpstart>_start+0x6b:              call  +0x29   <__fsr>_start+0x70:              call  +0xd8   <_init>_start+0x75:              call  +0x9b   

_start+0x7a:              addl  $0xc,%esp_start+0x7d:              pushl  %eax_start+0x7e:              call  -0xb2   <LT:exit>_start+0x83:              pushl  $0x0_start+0x85:              movl  $0x1,%eax_start+0x8a:              lcall  $0x7,$0x0_start+0x91:              hlt

Procedure Linkage Table - 过程链接表:PLT存在于每个ELF可执行文件的代码段,它和可执行文件的数据段中的GOT来一起决定位置无关函数的绝对地址。首先,第一次调用位置无关 函数时,会进入相应函数的PLT入口,PLT的指令会从GOT中读出默认地址,该地址正好是PLT0的入口地址,PLT0会把控制权交给runtime linker,由runtime linker解析出该函数的绝对地址,然后将这个绝对地址存入GOT,然后,该函数将被调用。然后,当再次调用该函数时,由于GOT中已经存放了该函数入 口的绝对地址,因此PLT对应的指令会直接跳转到函数绝对地址,而不会再由runtime linker解析。

PLT的一般格式如下:

.PLT0:pushl got_plus_4
    jmp *got_plus_8
    nop; nop
    nop; nop
.PLT1:jmp *name1_in_GOT
    pushl $offset@PC
    jmp .PLT0@PC ...
.PLT2:jmp *name2_in_GOT
    push $offset
    jmp .PLT0@PC
.PLT2:jmp *name3_in_GOT
    push $offset
    jmp .PLT0@PC


可以通过elfdump来实际查看test文件验证一下:

# /usr/ccs/bin/elfdump -c -N .plt testSection Header[8]:  sh_name: .plt  sh_addr:    0x8050504     sh_flags:  [ SHF_ALLOC  SHF_EXECINSTR ]  sh_size:    0x60        sh_type:   [ SHT_PROGBITS ]  sh_offset:   0x504        sh_entsize: 0x10  sh_link:    0          sh_info:   0  sh_addralign: 0x4

这样,PLT的结束地址也可以计算出来:

> 0x8050504+0x60=X          8050564
根据.plt的起始和结束地址可以反汇编:


> 0x8050504::dis -a -n 138050504                 pushl  0x8060700         ---->pushl got_plus_4,指向Rt_map地址805050a                 jmp   *0x8060704        ---->jmp *got_plus_8,跳转到Runtime linker的入口8050510                 addb  %al,(%eax)8050512                 addb  %al,(%eax)8050514                 jmp   *0x8060708805051a                 pushl  $0x0805051f                 jmp   -0x1b   <0x8050504>8050524                 jmp   *0x806070c805052a                 pushl  $0x8805052f                 jmp   -0x2b   <0x8050504>8050534                 jmp   *0x8060710805053a                 pushl  $0x10805053f                 jmp   -0x3b   <0x8050504>8050544                 jmp   *0x8060714        ---->跳转到0x805054a,即下一条指令805054a                 pushl  $0x18805054f                 jmp   -0x4b   <0x8050504>8050554                 jmp   *0x8060718805055a                 pushl  $0x20805055f                 jmp   -0x5b   <0x8050504>8050564                 addb  %al,(%eax)

或者包含符号信息:


> 0x8050504::dis -n 130x8050504:               pushl  0x80607000x805050a:               jmp   *0x80607040x8050510:               addb  %al,(%eax)0x8050512:               addb  %al,(%eax)PLT=libc.so.1`atexit:        jmp   *0x8060708PLT=libc.so.1`atexit:        pushl  $0x0PLT=libc.so.1`atexit:        jmp   -0x1b   <0x8050504>LT=libc.so.1`_fpstart:      jmp   *0x806070cPLT=libc.so.1`_fpstart:      pushl  $0x8PLT=libc.so.1`_fpstart:      jmp   -0x2b   <0x8050504>LT:exit:                jmp   *0x8060710PLT:exit:                pushl  $0x10PLT:exit:                jmp   -0x3b   <0x8050504>PLT:printf:              jmp   *0x8060714PLT:printf:              pushl  $0x18PLT:printf:              jmp   -0x4b   <0x8050504>PLT:_get_exit_frame_monitor:   jmp   *0x8060718PLT:_get_exit_frame_monitor:   pushl  $0x20PLT:_get_exit_frame_monitor:   jmp   -0x5b   <0x8050504>0x8050564:               addb  %al,(%eax)


在main+0x14处,继续单步运行:

> :smdb: target stopped atLT:printf:    jmp   *0x8060714
查看0x8060714即printf在GOT中的内容,其实就是PLT:printf中下一条push指令:

> *0x8060714=X          805054a> *0x8060714::dis -n 1PLT:printf:              pushl  $0x18PLT:printf:              jmp   -0x4b   <0x8050504>

继续单部执行,马上就要把0x18压入栈,这个0x18就是printf在重定位表中的偏移量:


# /usr/ccs/bin/elfdump -c -N .rel.plt testSection Header[7]:  sh_name: .rel.plt  sh_addr:    0x80504dc     sh_flags:  [ SHF_ALLOC  SHF_INFO_LINK ]  sh_size:    0x28        sh_type:   [ SHT_REL ]  sh_offset:   0x4dc        sh_entsize: 0x8  sh_link:    3          sh_info:   8  sh_addralign: 0x4# /usr/ccs/bin/elfdump -d testDynamic Section:  .dynamic   index  tag          value    [0]  NEEDED        0x111         libc.so.1    [1]  INIT         0x80506b0    [2]  FINI         0x80506cc    [3]  HASH         0x80500e8    [4]  STRTAB        0x805036c    [5]  STRSZ        0x137    [6]  SYMTAB        0x80501cc    [7]  SYMENT        0x10    [8]  CHECKSUM      0x5a2b    [9]  VERNEED       0x80504a4    [10]  VERNEEDNUM     0x1    [11]  PLTRELSZ      0x28    [12]  PLTREL        0x11    [13]  JMPREL        0x80504dc  ---> 重定位表.rel.plt的基地址    [14]  REL          0x80504d4    [15]  RELSZ        0x30    [16]  RELENT        0x8    [17]  DEBUG        0    [18]  FEATURE_1      0x1          [ PARINIT ]    [19]  FLAGS        0            0    [20]  FLAGS_1       0            0    [21]  PLTGOT        0x80606fc


直接查看重定位表内容:

# /usr/ccs/bin/elfdump -r  testRelocation Section:  .rel.data     type                  offset         section      with respect to     R_386_32             0x80607f8         .rel.data    __1cG__CrunMdo_exit_code6F_v_Relocation Section:  .rel.plt     type                  offset         section      with respect to     R_386_JMP_SLOT         0x8060708         .rel.plt     atexit     R_386_JMP_SLOT         0x806070c         .rel.plt     __fpstart     R_386_JMP_SLOT         0x8060710         .rel.plt     exit     R_386_JMP_SLOT         0x8060714         .rel.plt     printf     R_386_JMP_SLOT         0x8060718         .rel.plt     _get_exit_frame_monitor

其中,printf是4项,而在32位x86平台上,重定位表的每项的长度为8字节,定义如下:

typedef struct {  Elf32_Addr r_offset;  Elf32_Word r_info;} Elf32_Rel;
因此,printf在重定位表中偏移量=(4-1)*8=24,即16进制的0x18。

用mdb查看实际内存中的重定位表:

> 0x80504dc,a/nap0x80504dc:0x80504dc:    0x80607080x80504e0:    0xf070x80504e4:    0x806070c0x80504e8:    0x10070x80504ec:    0x80607100x80504f0:    0x12070x80504f4:    0x80607140x80504f8:    0x1070x80504fc:    0x80607180x8050500:    0x1307

可以看到,printf的r_offset是0x8060714,r_info是0x107。对照前面的GOT各项的地址,可以发现,0x8060714 就是GOT[7]的地址。

> :smdb: target stopped atLT:printf:    pushl  $0x18
继续单步执行:

> :smdb: target stopped atLT:printf:    jmp   -0x4b   <0x8050504>
地址0x8050504就是PLT0的地址:

> :smdb: target stopped at:0x8050504:    pushl  0x8060700
0x8060700就是GOT[1],存储的就是Rt_map的首地址,相当于把Rt_map的首地址压栈:

> :smdb: target stopped at:0x805050a:    jmp   *0x8060704
0x8060704就是GOT[2],存储着runtime linker - ld.so的入口地址:

> :smdb: target stopped at:ld.so.1`elf_rtbndr:    pushl  %ebp
可以看到,这样控制权就由PLT这样转换到runtime linker了,显然,下面将进入runtime link editor来动态绑定了,我们查看目前栈的状态:

> Rt_map的首地址0x8047350:    0x18     ----> printf对应项重定位表中的偏移量0x8047354:    main+0x19  ----> printf返回后应跳转的地址0x8047358:    0x80506ec0x804735c:    0x80474600x8047360:    0x80473540x8047364:    0xd17fb8400x8047368:    0x80474600x804736c:    0x804738c0x8047370:    _start+0x7a0x8047374:    10x8047378:    0x80473980x804737c:    0x80473a00x8047380:    _start+0x1f0x8047384:    _fini0x8047388:    ld.so.1`atexit_fini

查看ld.so.1`elf_rtbndr函数的定义,这部分是平台相关的,我们只关心32bit x86部分的实现:

link:http://cvs.opensolaris.org/sourc ... tld/i386/boot_elf.s


  288 #if defined(lint)  289  290 extern unsigned long   elf_bndr(Rt_map *, unsigned long, caddr_t);  291  292 void  293 elf_rtbndr(Rt_map * lmp, unsigned long reloc, caddr_t pc)  294 {  295    (void) elf_bndr(lmp, reloc, pc);  296 }  297  298 #else  299    .globl   elf_bndr  300    .globl   elf_rtbndr  301    .weak   _elf_rtbndr  302    _elf_rtbndr = elf_rtbndr   / Make dbx happy  303    .type  elf_rtbndr,@function  304    .align   4  305  306 elf_rtbndr:  307    pushl   %ebp  308    movl   %esp, %ebp  309    pushl   %eax  310    pushl   %ecx  311    pushl   %edx  312    pushl   12(%ebp)      / push pc  313    pushl   8(%ebp)        / push reloc  314    pushl   4(%ebp)        / push *lmp  315    call   elf_bndr@PLT      / call the C binder code  316    addl   $12, %esp      / pop args  317    movl   %eax, 8(%ebp)      / store final destination  318    popl   %edx  319    popl   %ecx  320    popl   %eax  321    movl   %ebp, %esp  322    popl   %ebp  323    addl   $4,%esp        / pop args  324    ret           / invoke resolved function  325    .size    elf_rtbndr, .-elf_rtbndr  326 #endif

315行调用的elf_bndr是平台相关代码,函数原型如下:

  290 extern unsigned long   elf_bndr(Rt_map *, unsigned long, caddr_t);
因此在elf_rtbndr的312-314这几行,实际上是为调用elf_bndr做传递参数的准备:

  312    pushl   12(%ebp)      / push返回地址 main+0x19  313    pushl   8(%ebp)      / push重定位表的对应printf项的偏移量 0x18  314    pushl   4(%ebp)      / push Rt_map的首地址,0xd17fd900
根据32位x86的ABI,压栈顺序是从右到左,正好吻合elf_bndr的参数顺序和类型定义。

通过在elf_bndr函数调用前设置断点来验证一下:

> ld.so.1`elf_rtbndr::disld.so.1`elf_rtbndr:         pushl  %ebpld.so.1`elf_rtbndr+1:        movl  %esp,%ebpld.so.1`elf_rtbndr+3:        pushl  %eaxld.so.1`elf_rtbndr+4:        pushl  %ecxld.so.1`elf_rtbndr+5:        pushl  %edxld.so.1`elf_rtbndr+6:        pushl  0xc(%ebp)ld.so.1`elf_rtbndr+9:        pushl  0x8(%ebp)ld.so.1`elf_rtbndr+0xc:      pushl  0x4(%ebp)ld.so.1`elf_rtbndr+0xf:      call  +0x14c5d ld.so.1`elf_rtbndr+0x14:      addl  $0xc,%espld.so.1`elf_rtbndr+0x17:      movl  %eax,0x8(%ebp)ld.so.1`elf_rtbndr+0x1a:      popl  %edxld.so.1`elf_rtbndr+0x1b:      popl  %ecxld.so.1`elf_rtbndr+0x1c:      popl  %eaxld.so.1`elf_rtbndr+0x1d:      movl  %ebp,%espld.so.1`elf_rtbndr+0x1f:      popl  %ebpld.so.1`elf_rtbndr+0x20:      addl  $0x4,%espld.so.1`elf_rtbndr+0x23:      ret> ld.so.1`elf_rtbndr+0xf:b> :cmdb: stop at ld.so.1`elf_rtbndr+0xfmdb: target stopped at:ld.so.1`elf_rtbndr+0xf: call  +0x14c5d
下面检查ld.so.1`elf_bndr调用前栈的状况,可以看到,3个参数已经按顺序压入栈中:

>
elf_rtbndr会返回我们需要的printf在libc.so中的绝对地址吗?

用mdb在ld.so.1`elf_rtbndr返回处设置断点,继续执行:


> ld.so.1`elf_rtbndr+0x14:b> :cmdb: stop at ld.so.1`elf_rtbndr+0x14mdb: target stopped at:ld.so.1`elf_rtbndr+0x14:addl  $0xc,%esp
检查一下函数返回值,它应该存在rax的寄存器中:

>
显然,d1741f39就是printf的绝对地址,它处于libc.so中:

> d1741f39::dis -wlibc.so.1`printf:          pushl  %ebplibc.so.1`printf+1:         movl  %esp,%ebplibc.so.1`printf+3:         subl  $0x10,%esplibc.so.1`printf+6:         andl  $0xfffffff0,%esplibc.so.1`printf+9:         pushl  %ebxlibc.so.1`printf+0xa:        pushl  %esilibc.so.1`printf+0xb:        pushl  %edilibc.so.1`printf+0xc:        call  +0x5    libc.so.1`printf+0x11:       popl  %ebxlibc.so.1`printf+0x12:       addl  $0x6d0b6,%ebxlibc.so.1`printf+0x18:       movl  0x244(%ebx),%esi

此时此刻,GOT中的printf的对应项GOT[7],即0x8060714地址处,已经被ld.so修改成printf的绝对地址:

> 0x80606fc,9/nap0x80606fc:0x80606fc:    0x806071c0x8060700:    0xd17fd9000x8060704:    ld.so.1`elf_rtbndr0x8060708:    libc.so.1`atexit0x806070c:    libc.so.1`_fpstart0x8060710:    PLT:exit0x8060714:    libc.so.1`printf0x8060718:    PLT:_get_exit_frame_monitor0x806071c:    1>
printf被成功解析后,ld.so修改了GOT[7],接着就应该把控制权转到libc的printf函数了。显然,在 ld.so.1`elf_rtbndr+0x17处的指令将会把eax寄存器中的printf的绝对函数地址存入栈中:

> ld.so.1`elf_rtbndr+0x17:b> :cmdb: stop at ld.so.1`elf_rtbndr+0x17mdb: target stopped at:ld.so.1`elf_rtbndr+0x17:movl  %eax,0x8(%ebp)
此时栈中还没有printf的地址:

>
单步执行后,再观察栈,会发现,printf已经存入栈:

> :smdb: target stopped at:ld.so.1`elf_rtbndr+0x1a:popl  %edx>

在ld.so.1`elf_rtbndr返回的前一刻,printf恰好成为ld.so.1`elf_rtbndr的返回地址:

> :smdb: target stopped at:ld.so.1`elf_rtbndr+0x23:ret>
这样,控制权就由ld.so到了我们要调用的函数 - printf:

> :smdb: target stopped at:libc.so.1`printf:     pushl  %ebp
至此,一个完整的动态绑定过程结束,此时可以再次反汇编我们的main函数:

> main::dismain:                  pushl  %ebpmain+1:                 movl  %esp,%ebpmain+3:                 subl  $0x10,%espmain+6:                 movl  %ebx,-0x8(%ebp)main+9:                 movl  %esi,-0xc(%ebp)main+0xc:                movl  %edi,-0x10(%ebp)main+0xf:                pushl  $0x80506ecmain+0x14:               call  -0x148  main+0x19:               addl  $0x4,%espmain+0x1c:               movl  $0x0,-0x4(%ebp)main+0x23:               jmp   +0x5    main+0x28:               movl  -0x4(%ebp),%eaxmain+0x2b:               movl  -0x8(%ebp),%ebxmain+0x2e:               movl  -0xc(%ebp),%esimain+0x31:               movl  -0x10(%ebp),%edimain+0x34:               leavemain+0x35:               ret>
可以看到,由于GOT[7]已经存储了printf的绝对地址,因此,反汇编结果发生了变化。

进程第一次调用printf的动态解析的过程如下:

  main   |   VPLT:printf的第1条指令<---GOT[7]指向的地址   |                 |   V                 |PLT:printf的第2条指令<---------+   |   VPLT:printf的第3条指令   |   VPLT0ld.so.1`elf_rtbndr   |   Vlibc.so.1`printf

如果该进程再次调用printf:

main  |  VPLT:printf的第1条指令<---GOT[7]指向的地址  |               |  V               |libc.so.1`printf<---------+


3. elf_bndr函数

elf_rtbndr在32bit x86平台的源代码的位置在:
link:http://cvs.opensolaris.org/sourc ... tld/i386/i386_elf.c

要实现动态绑定,elf_bndr应至少完成如下工作:

3.1 确定要绑定的符号

下面部分elf_bndr的代码就是根据重定位表来确定要绑定的符号:


  231    /*  232    * Use relocation entry to get symbol table entry and symbol name.  233    */  234    addr = (ulong_t)JMPREL(lmp);  235    rptr = (Rel *)(addr + reloff);  236    rsymndx = ELF_R_SYM(rptr->r_info);  237    sym = (Sym *)((ulong_t)SYMTAB(lmp) + (rsymndx * SYMENT(lmp)));  238    name = (char *)(STRTAB(lmp) + sym->st_name);  239

你可能感兴趣的:(ELF文件头读取修改)