• [pwn基础]动态链接原理


    [pwn基础]动态链接原理

    动态链接概念

    为了解决空间浪费和更新困难问题最简单的办法就是把程序的模块相互分割开来,形成独立的文件,而不是将它们静态链接在一起。

    简单的说:不对那些组成程序的目标文件进行链接,等到程序要运行时候才进行链接。

    把链接这个过程推迟到了运行时再进行,这就是动态链接(Dynamic Linking)的基本思想。

    动态链接调用so例子

    LibTest.h LibTest.c

    #ifndef LIBTEST_H
    #define LIBTEST_H
    
    void foobar(int i);
    
    #endif
    
    #include "LibTest.h"
    #include <stdio.h>
    
    void foobar(int i)
    {
        printf("Printing from Lib.so %d\n",i);
    }
    

    编译成.so(动态链接库)

    gcc -fPIC -shared LibTest.c -o libtest.so
    #-fPIC是与地址无关选项
    #-shared 是编译成so 动态联机
    #-o 输出,so文件名必须以lib开头
    

    Program1.c

    #include "LibTest.h"
    
    int main(int argc,char *argv[])
    {
        foobar(1);
        return 0;
    }
    

    Program2.c

    #include "Lib.h"
    
    int main(int argc,char *argv[])
    {
        foobar(2);
        return 0;
    }
    

    分别编译Program1 和Program2动态调用libtest.so

    gcc Program1.c -L. -ltest -o Program1
    gcc Program2.c -L. -ltest -o Program2
    export LD_LIBRARY_PATH=/home/pwn/testdemo:$LD_LIBRARY_PATH
    
    #上面命令的意思分别是
    #-L. 代表的是so在本地当前目录查找
    #-ltest 动态调用so有一套自己的命名规则,一般必须是lib带头,然后才是so名字.所以-l后面跟的是lib之后的so名,忽略后缀。
    #export LD_LIBRARY_PATH代表的是把动态链接目录加入环境变量,默认是/usr/lib下
    
    ~/testdemo » ldd Program1
            linux-vdso.so.1 (0x00007ffd25b6e000)
            libtest.so => /home/pwn/testdemo/libtest.so (0x00007f3ae466f000)
            libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3ae446a000)
            /lib64/ld-linux-x86-64.so.2 (0x00007f3ae467b000)
            
    #ldd命令可以用来查看当前程序所调用的动态so。
    

    运行结果:

    ~/testdemo » ./Program1                                                                             
    Printing from Lib.so 1
    
    ~/testdemo » ./Program2                                                                             
    Printing from Lib.so 2
    

    GOT(全局偏移表)

    GOT表的全称是Global Offset Table(全局偏移表)

    可以把它理解成为了动态链接,把所有的符号偏移量或(绝对地址)都放入到了一个表里这就是GOT表

    • .got表(一般放的是全局变量和static变量)
    • .got.plt表(一般放的就是引用so的函数,即导入函数)

    下面我们来做个实验加深下理解。

    /*a.c源码*/
    extern int shared; //外部符号,跨模块
    
    int main()
    {
        int a = 100;
        swap(&a,&shared);//外部符号,调用外部模块的swap函数
    }
    
    /*b.c源码*/
    extern int shared = 1;
    
    void swap(int *a,int *b)
    {
        *a ^= *b ^= *a ^= *b;
    }
    
    #编译成.so
    gcc -fPIC -shared b.c -o libb.so
    export LD_LIBRARY_PATH=/home/pwn/got:$LD_LIBRARY_PATH
    #编译a可执行程序
    gcc a.c -L. -lb -o a
    

    从下图中可以看到shared变量的访问和之前我没静态链接篇的访问方式是一模一样的,用的是当rip+偏移这种间接寻址的方式来访问三方模块的全局变量,而函数swap在这里则变成了swap@plt

    利用断点跟入swap@plt函数,然后跟到了plt表,后面会将plt表的用途,可以看到有个jmp是间接跳转,加上偏移后刚好就是got表的位置,对应的是存放swap函数的绝对地址。

    got表劫持小实验

    #include <stdio.h>
    void fun()
    {
            system("id");
    }
    int main()
    {
            //下面演示:Printf("id") 变成shell命令
            printf("id");
            return 0;
    }
    

    最后成功劫持,将printf劫持成了system函数,输出了当前id

    PLT(延迟绑定)

    PLT概念

    首先, 我们要知道, GOT和PLT只是一种重定向的实现方式. 所以为了理解他们的作用, 就要先知道什么是重定向, 以及我们为什么需要重定向.

    重定向我在静态链接文章中已经介绍过,就是编译成.o文件时候,那些外部符号变量和函数无法确定时候,预留的填充值,比如用0填充,然后等待链接时候才真实的被写入。

    之前介绍的是静态链接的情况,那么动态链接时候会怎么样呢?一遍实战一遍学习。

    #include <stdio.h>
    
    void print_banner()
    {
        printf("Welcome to World of PLT and GOT\n");
    }
    
    int main(int argc,char *argv[])
    {
        print_banner();
        return 0;
    }
    
    
     #编译分别生成.o 和可执行程序
     gcc -c plt.c -o plt.o -m32
     gcc -o plt plt.c -m32
    

    编译后产生了.o和plt可执行程序,我们先用objdump来看看plt.o的汇编源码,命令是objdump -M intel -dw plt.o

    可以看到call printf的这个地址是填0的,因为这时候编译器并不知道printf的函数真实地址,printf函数是需要程序被装载后才能确定地址,那么动态链接器为什么不在程序运行起来后,装载起来后

    再把真实的printf地址填进去呢?因为这个call printf的语句是在.text代码段的,运行起来后代码段是无法被修改的,只能修改.data数据段。

    ????????那怎么搞啊,都不能修改代码段,那搞什么。

    只能羡慕大佬么的技巧,大佬么总是那么骚,还是有办法搞的,动态链接器生成了一段额外的小代码判断,通过这段代码获取printf函数地址,并完成对它的调用。

    延迟绑定(PLT表)

    用来存放这小片段代码的地方就是PLT表,下面是伪代码片段。

    .text
        ....
    //调用printf的call指令
    call printf_plt
    ....
    
    printf_plt:
    	mov rax,[printf函数的存储地址] //GOT表中
    	jmp rax //跳过去执行printf函数
    	
    .got.plt
    .....
    printf下标
    	这里存储了printf函数重定位后的真实地址
    

    链接阶段发现printf定义在动态库 glibc时,链接器生成一段小代码 print@plt,然后printf@plt地址取代原来的printf。因此转化为链接阶段对printf@plt做链接重定位,而运行时才对printf做运行时重定位,具体调用流程图,可以参考如下:

    实战学习

    好的,接下来我们继续用上面的例子,详细对的PLT表进行分析,首先我们用命令objdump -M intel -dw plt查看每个段的数据,有汇编则反汇编。

    000003a0 <.plt>:
     3a0:   ff b3 04 00 00 00       push   DWORD PTR [ebx+0x4]
     3a6:   ff a3 08 00 00 00       jmp    DWORD PTR [ebx+0x8]
     3ac:   00 00                   add    BYTE PTR [eax],al
            ...
    000003b0 <puts@plt>:
     3b0:   ff a3 0c 00 00 00       jmp    DWORD PTR [ebx+0xc]
     3b6:   68 00 00 00 00          push   0x0
     3bb:   e9 e0 ff ff ff          jmp    3a0 <.plt>
            ...
    0000051d <print_banner>:
     51d:   55                      push   ebp
     51e:   89 e5                   mov    ebp,esp
     		...
     53a:   e8 71 fe ff ff          call   3b0 <puts@plt>
     		...
     547:   c3                      ret   
     
    +0000 0x56556fd8  e0 1e 00 00  00 00 00 00  00 00 00 00  90 cd e4 f7  │....│....│....│....│
    +0010 0x56556fe8  b0 de df f7  00 00 00 00  80 58 e1 f7  00 00 00 00 
    

    OK,我们对上面的代码进行分析,收我们关注printf_banner函数调用的printf,这里因为编译优化的缘故printf变成了puts,在53a这里可以看到调用了

    puts@plt,puts@plt这里有3句汇编代码,分别是jmp到ebx+0xC值的地址,然后又push0,又jmp到0x3a0,因为这里我们不知道ebx是什么值,所以需要动态调试来一步步详细的观察下,用命令pwndbg,gdb pltstart,b printf_banner c,然后单步到call puts@plt

    从上图中可以发现,ebx的值是0x56556fd8,这其实是got表装载到内存后的地址,我们可以用readelf -SW plt查看文件中got表的偏移。

    可以发现偏移正好是fd8,虚拟内存的起始地址加上fd8就是0x56556fd8

    那么如何查看程序加载的起始地址呢?可以借助强大的pwndbg中的vmmap命令来查看内存分布。

    正好是(起始地址)0x56555000+偏移(0xfd5)=0x56556fd8

    OK现在回归正题,我们已经知道puts@plt中的jmp是要跳转到GOT表中偏移0xC的位置,那么这个位置存放的是什么值呢?

    聪明的你已经猜到了,他其实就是puts函数的真实地址,但是!为了不影响程序运行的速度,因为我们程序一运行就把所有符号地址都确定,然后都填入got表,那一但我们调用到非常的动态库时候,性能肯定会受影响的。所以,采用了延迟绑定机制。

    000003b0 <puts@plt>:
     3b0:   ff a3 0c 00 00 00       jmp    DWORD PTR [ebx+0xc]
     3b6:   68 00 00 00 00          push   0x0
     3bb:   e9 e0 ff ff ff          jmp    3a0 <.plt>
    

    延迟绑定机制原理

    我们先来看看,这个got表偏移+0xC位置,在文件位置中的值是多少,可以看到他的值是0x3B6,你可以仔细看看puts@plt函数,jmp后下一句汇编地址是多少?

    00001ee0
    00000000
    00000000
    000003B6    [ebx+0xC]
    

    刚好是0x3b6,对应的汇编语句是push 0,接着又跳到了jmp 0x3a0 <.plt>,跳到了plt表。

     3b6:   68 00 00 00 00          push   0x0
    

    plt表中的汇编如下:

    这几句汇编代码会调用内核的_dl_runtime_resolve()函数,把puts函数在动态库中的真实地址放入到got表中。

    000003a0 <.plt>:
     3a0:   ff b3 04 00 00 00       push   DWORD PTR [ebx+0x4]
     3a6:   ff a3 08 00 00 00       jmp    DWORD PTR [ebx+0x8]
     3ac:   00 00                   add    BYTE PTR [eax],al
    

    所以延迟绑定机制的原理:就是第一次在调用函数时候,才把真实的地址放入got表(进行绑定),之后再调用这函数则直接jmp到真实地址。

    最后,在其他大佬博客上偷了张详细的函数调用plt表延迟绑定的流程图。

    参考文献:

    https://yjy123123.github.io/2021/12/06/延迟绑定过程分析/

    https://evilpan.com/2018/04/09/about-got-plt/ 非常完整详细的讲解博客

    《程序员的自我修养 链接、装载与库》 这本书,真的是神书,全部仔细看完肯定有帮助。


    __EOF__

  • 本文作者: VxerLee
  • 本文链接: https://www.cnblogs.com/VxerLee/p/16378703.html
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    【服务器数据恢复】MDisk重建,vdisk丢失导致数据库不可用的数据恢复案例
    基于Android的课程成绩管理系统的设计与实现
    机智云工业级4G Cat.1 DTU全面升级数据采集485/232 GC521
    视频汇聚平台EasyCVR从一分屏切换到四分屏后加载记录显示黑屏该如何解决?
    JS 添加数组元素( 4种方法 )
    3年测试经验,面试27k自动化测试岗被diss,想给进阶自动化的人提个醒...
    家政服务行业做开发微信小程序可以实现什么功能
    RabbitMQ如何确保消息发送和消息接收
    【Ubuntu20.04】安装XRDP远程桌面服务
    2023-09-21 LeetCode每日一题(收集树中金币)
  • 原文地址:https://www.cnblogs.com/VxerLee/p/16378703.html