文章

linux上的延迟重定位

熟练地掌握linux的 链接绑定不仅仅是研究return to plt的必要,更是掌握linkmap、got表覆写、dlresolve等技术的基础

主题

  • plt、got表
  • 延迟绑定
  • 软硬链接

文件外函数的调用原理

函数调用的汇编实现

假如有这样一个代码system("cat flag")

在程序流流向这里的时候会发生什么样的事情呢?

我事先写好了一个这样的程序。让我们拖到ida里面好好看一看。

    push    offset command ; "cat flag"
    call    _system
    add     esp, 10h

可以看见他调用了_system,这个_system和libc里面的system不是一个东西。

我们点开_system可以看见

    .plt:08048410 ; =============== S U B R O U T I N E =========================
    .plt:08048410
    .plt:08048410 ; Attributes: thunk
    .plt:08048410
    .plt:08048410 ; int system(const char *command)
    .plt:08048410 _system         proc near               ; CODE XREF: main+163↓p
    .plt:08048410
    .plt:08048410 command         = dword ptr  4
    .plt:08048410
    .plt:08048410                 jmp     ds:off_804A014
    .plt:08048410 _system         endp
    .plt:08048410
    .plt:08048416 ; --------------------------------------------------------------

我们发现了这里面有着plt三个字,这就是所谓的plt表中的一部分。

再点开

    ds:off_804A014

我们就会发现

    .got.plt:0804A014 off_804A014     dd offset system        ; DATA XREF: _system↑r

我们发现了这里面有着got三个字,这就是所谓的got表中的一部分。

生成汇编

为什么需要有plt与got这些东西,而不在程序中直接call system这样的方法来访问system,的原因在于system是在glibc库中,而glibc属于动态库。

如果读者了解过windows的动态链接库的话,事实上这两者有所区别,但是大致原理基本相似

之所以无法直接直接call system,这正是用为程序中调用system的代码是在编译和链接中(事实上主要在编译中)确立的,而在此过程中,system的地址,编辑器无从而知。而在现代操作系统中,代码段也无法更改,所以试图在加载过程call aaaaa改为call xxxxx(&system)也是无用功。

同时因为编辑器同时无法确定system函数是在libc这样的动态库中还是在其他的中间文件(.o)中所以编辑会生成一样格式的代码,这使得无论你的某函数是在动态链接库中还是在其它文件中给出定义,这样的汇编代码都能实现调用system。

重定向

但是函数的调用总归是需要函数的具体地址的。那么就可以把情况分为两种。

  1. 静态链接库
  2. 动态链接库(如glibc)

在这两种情况中:

前一种函数的位置在编译过程中是已知的,所以在编译过程中可以计算出在编译过程中填进去占位的地址距离程序在被载入后此函数位置的偏移量,并将其填入某个位置。这就是 链接时重定位

后一种函数的位置在运行时是已知的,算出偏移量中也填入此表中。这就是 运行时重定位

函数的调用

在完成了重定向之后,函数的调用还面临着一个问题。那就是,在无法改变代码段的情况下,应当如何利用数据段中的偏移量呢。

答案是程序在链接中就生成一小段专门利用偏移量的代码。

链接器生成额外的伪代码如下:

    .text
    ...

    // 调用system的call指令
    call system_stub
    ...

    system_stub:
    mov rax, [system函数的储存地址] ; 获取system重定位之后的地址
    jmp rax ; 跳过去执行system函数

    .data
    ...
    system函数的储存地址 ;这里储存system函数重定位后的地址

在链接的过程中,如果发现函数实际上是动态链接库的代码,就会添加以上代码,并将system改为system_sub。

所以实际上在函数的调用中我们需要两样东西

  • 需要存放外部函数的数据段
  • 获取数据段存放函数地址,并且正确的访问他们的一小段额外代码

又因为动态库的函数往往不止一个所以许多这样的数据段与许多这样的代码形成了两张表。存储函数相对地址的表叫做 全局偏移表,即 got表,而存储那段额外的代码的表被叫做 程序链接表,即 plt表

延迟重定位

前文提到了函数的地址放在got表中,所以把got中的数据段填充满必须优先于调用之前,

延迟重定位,故名思意,一大特点是 延迟

延迟即尽可能的延迟,直到不得不执行为止。之所以linux会有这样的特性,是因为如果动态库函数非常之多,那么放在前面一起执行会大大地减弱程序的启动时间。所以linux使用延迟重定位机制来使载入动态库地时间被均匀分散到了执行时间中。

这就产生了一个问题:程序如何知道GOT表中某一数据项是否已经被载入(因为程序中的函数可能会被执行多次,这样子就不必每一次都去重定位了)。

在程序中地具体实现大致为

    void system@plt()
    {
    address_good:
    jmp *system@got            // 链接器将system@got填成下一语句lookup_system的地址
    return;
    lookup_system:
            调用重定位函数查找system地址,并写到system@got

            goto address_good;
    }

当第一次运行system函数时,程序流会跳到system@plt,然后会顺势执行jmp *system@got,但是很显然,got表中的数据项并没有填充正确的值。其实程序流并没有前往所谓的system@got,实际上它前往了lookup_system中去,这个函数的功能是调用重定位的函数来寻找system的函数地址,并将其写入system@got中去,然后再跳到addr_good,执行system然后返回到调用者。这样子下面的lookup_system就相当于被废除了。

然后在第二次调用system时,就直接走向system的真正的地址中。

事实上重定位中的最后实现并没有每一个plt中都含有符号解析与重定位的那一部分。所以程序中实现具体实现为。

    jmp     *system@got
    push    id(当前函数在plt中的偏移量/2)
    jmp     common@plt

公共plt的具体实现

在上文中我们基本缕清了函数重定向的前世今生与大致实现方式。接下来我要重点讲一下这个公共的函数的实现方式。

我们先在ida里看看这个函数的具体实现,在这里ida的F5已经无能为力了,我们硬刚下源码试试

    .plt:08048300 common          proc near               ; CODE XREF: .plt:0804831B↓j
    .plt:08048300                                         ; .plt:0804832B↓j ...
    .plt:08048300 ; __unwind {
    .plt:08048300                 push    ds:dword_804A004
    .plt:08048306                 jmp     ds:dword_804A008
    .plt:08048306 common          endp

很显然这个所谓的804A008就是所谓的重定向函数,而上面的804A004,就是参数。

而在这些地方放的就是

    .got.plt:0804A004 dword_804A004   dd 0                    ; DATA XREF: common↑r
    .got.plt:0804A008 ; int (*dword_804A008)(void)
    .got.plt:0804A008 dword_804A008   dd 0                    ; DATA XREF: common+6↑r

在下面我们发现了

    .got.plt:0804A00C off_804A00C     dd offset read          ; DATA XREF: _read↑r
    .got.plt:0804A010 off_804A010     dd offset __gmon_start__
    .got.plt:0804A010                                         ; DATA XREF: ___gmon_start__↑r
    .got.plt:0804A014 off_804A014     dd offset __libc_start_main
    .got.plt:0804A014                                         ; DATA XREF: ___libc_start_main↑r
    .got.plt:0804A018 off_804A018     dd offset write         ; DATA XREF: _write↑r
    .got.plt:0804A018 _got_plt        ends
    .got.plt:0804A018

发现接着跑下去就已经看不懂了

随手点一个进去

    extern:0804A034                 extrn write:near        ; CODE XREF: _write↑j
    extern:0804A034                                         ; DATA XREF: .got.plt:off_804A018↑o

好像是一个声明一样的东西。

经过了观看大神写的微博以后,我发现如果使用gdb进行动态调试的话,这个common,在没有进行运行时,其的值为0x0,在运行后会变成它的值会变成另一个值,而这个值最终指向的是 _dl_runtime_resolve,而这一个处于动态链接器中的函数。

_dl_runtime_resolve

因为每一个plt表里的函数都需要调用这个公共的dlresolve项,所以dlresolve得知道需要查找的是哪一个函数,并将GOT表的值写在哪个函数的plt段。因为在plt表里push了一个参数,我上面也写了你如果去算算的话,它与函数在plt表中的偏移量相同

在程序内部,维持有一个重定位信息表。利用readelf -r test,可以看见.rel.plt段中的消息

    Offset     Info     Type             Sym.Value  Sym. Name
    080496f8   00000107 R_386_JUMP_SLOT  00000000   puts
    080496fc   00000207 R_386_JUMP_SLOT  00000000   __gmon_start__
    08049700   00000407 R_386_JUMP_SLOT 000000000   __libc_start_main

程序可以通过这个公共的offset表,找到应当被填写GOT表函数的地址的位置。

其实想要彻底搞清楚这个过程还需要对于elf文件在linux系统的装载有着更深的理解才行,所以我这边的讲解只是浅尝辄止不深究了。

日后讲解这个dlresolve的时候还会像详细讲解这个过程与可能的利用。


License:  CC BY 4.0