电脑疯子技术论坛|电脑极客社区

微信扫一扫 分享朋友圈

已有 707 人浏览分享

静态链接符号重定位 | GCC | PWN基础

[复制链接]
707 0
静态链接介绍

静态链接器以一组可重定位目标文件和命令行参数作为输入,生成一个完全链接的可以加载和运行的可执行目标文件作为输出

其中重定位目标文件可以是:

o目标文件

.a静态链接库文件

二者本质都是可重定位文件

关于静态链接库文件

若要将自己写的文件生成静态库.a文件,通过命令ar完成

首先将.c文件编译生成目标文件.o再通过ar生成静态库文件

例如:若我们要用libmymath.o创建静态库文件,则其对应的命令为:

ar -cr libmymath.a libmymath.o

-c创建

-r替换

表示当前插入的模块名已经在库中存在,则替换同名的模块。如果若干模块中有一个模块在库中不存在ar显示一
个错误信息,并不替换其他同名的模块。默认的情况下,新的成员增加在库的结尾处

之后若要使用该静态库编译链接出可执行文件则:

gcc -o main main.c -L. libmymath.a

-L.指定库的查找位置,后面跟着.就表示在当前目录下查找

QQ截图20221206160123.png

实验相关

为什么要做这个实验(实验目的)

通过观察静态链接的流程,着重关注其中重定位的过程以及静态链接后文件的布局以小见大理解当
静态链接标准静态库时的过程,并为接下来理解动态链接库打基础

实验代码

sub.c

int nSubData = 100;

int fnSub(int num)
{
        return num - 1;
}

main.c

extern int nSubData;
extern int fnSub(int num);

int main(void)
{
        int result = fnSub(nSubData);
        return 0;
}

编译链接

gcc -fno-pie -m32 -c sub.c main.c
# 关闭 pie
ld -m elf_i386 sub.o main.o -e main -o mainNone
# 其中 -e 用于指定 main 作为程序的入口,ld默认的为 _start

由于main.c与sub.c中并没有引入使用标准库的函数,若引入了标准库并使用了其中的函数不建议使用ld来
进行链接,因为需要找对应所依赖的静态库文件,直接使用gcc -static即可

静态链接过程

文本链接器再进行静态链接时一般采用 两步链接(Two-pass Linking)的方法,将链接的过程分为两步:

空间与地址分配

符号解析与重定位

先来简单介绍这两步骤各自的任务:

空间与地址分配

对于多个输入文件(可重定位文件),若将其按序叠加会产生许多零散的段,每个段又有地址和空间的对齐
要求使内存空间会产生大量的内部碎片,非常浪费空间

所以链接器采用 相似段合并的方案合并到输出文件,以我们刚刚的程序为例画一个简图(接下来会不断完善)就是:

QQ截图20221206160801.png

有关.bss段:之前在介绍 ELF 的文章中说到,.bss在目标文件和可执行文件中是不占用文件空间的但在链
接器合并各个 Section 的同时,也会将.bss进行合并,并分配虚拟空间

扫描所有的输入可重定位文件,获得每个Section的属性信息(长度、位置等),并将其合并,计算合并后
各个Section的长度与位置关系,建立映射关系。
收集所有输入可重定位文件中的符号表中所有的符号定义和符号引用,统一放到全局符号表中
链接器为目标文件分配地址和空间中地址和空间其实有两个含义:

在链接后输出的可执行文件中的空间
当可执行文件装载进内存后的虚拟地址的虚拟地址空间
对于有实际数据的段,它们在文件中和虚拟地址中都要分配空间,但恰好.bss是个特例 对于它来说仅
在装载进内存时分配虚拟地址空间

符号解析与重定位

通过第一步收集到的信息,以及全局符号表中的内容,读取输入文件中Section的数据、重定位信息
进行符号解析与重定位,调整代码中的地址

实验观察

main.o与sub.o的Section信息

main.o

QQ截图20221206161022.png

sub.o

1108.png

main.o的符号表与可重定位信息

readelf -s main.o查看main.o的符号表

1106.png

可以看到其中符号nSubData与fnSub的Ndx类型为UND即 该符号未定义,说明该符号在当前文件中只是被引用,实际定义
在其他文件中,那也就更表示这些符号是需要在接下来被重定位的,由于当静态链接为可执行文件时,必须有确定的地址虚
拟地址VMA ,所以链接器在链接过程中确定这两个符号在可执行文件中的地址,然后再将这两个地址回填入main的代码段
中对应使用他们的地方

接下来看一下main.o中<main>函数的代码段:

1103.png

图中两个红框中分别对应了代码中nSubData的入栈,与调用fnSub可以看出此处无论是要入栈的值还是
要调用函数的地址都不是真实虚拟地址

其中00 00 00 00代表的是nSubData数据的地址,由于在链接前并不知道这个位于其他文件中的符号会被安
排在什么地方,所以只好先用0来代替其位置,之后在进行替换

但为什么函数的地址不也用00 00 00 00来代替,链接时再替换呢?还是要在 call 的地址处写入十进制的-4这
就涉及到了 指令地址修正 的几种方式,先来说一下

再次之前再来看一下main.o的重定位表

1102.png

其中的Offset偏移地址代表的是在对应section中,比如说.text中,要进行修正地址的位置:

nSubData在.text中 要修正的地址就是那一串00 00 00 00的位置,也就是11 + 1 = 12偏移位置

fnSub在.text中要修正的位置就是 -4 (fc ff ff ff)对应的位置,也就是1a + 1 = 1b偏移位置

指令修正方式

对于32位x86平台下的ELF文件的重定位入口所修正的指令寻址方式只有两种:

绝对近址32位寻址R_386_32

相对近址32位寻址R_386_PC32

1101.png

A = 保存在被修正位置的值

P = 被修正的位置相对于段开始的偏移量或者虚拟地址(对于可执行文件)

S = 符号的实际地址(物理地址)

所以对于 :

nSubData采用绝对近址寻址:在链接合并后实际就是将nSubData这个位于.data的全局变量的虚拟
地址直接覆盖到00 00 00 00位置,而这个
fnSub采用相对近址寻址,既然是相对,那么一定是不是个地址,而是一个偏移:

观察mainNone修正后的地址

main.o与sub.o链接为mainNone

Section 信息

618.png

首先验证几个结论:

.text的大小为0x3d正好= 0x32 + 0xb( 参照上面main.o与sub.o的Section header table)
.data的大小为0x4正好= 0x0 + 0x4

符号表

链接器第一遍扫描文件时会把section进行合并安排到对应的地址

616.png

其中main()的虚拟地址为0x0804900大小为0x32,所以main()的结尾地址为0x0804900 + 0x32 = 0x0804
9032正是fnSub的地址,说明fnSub紧跟在main后面,其次要关注的是nSubData位于0x0804c000位置处
所以此时我们关注的几个符号的虚拟地址空间的布局为:

38.png

nSubDataR_386_32绝对近址32位寻址

当链接器第二次扫描目标文件时,会检查目标文件中需要重定位的符号

为了将.text的main()中使用nSubData地方的00 00 00 00替换为其虚拟地址,需要进行以下两步

计算出在可执行文件main中什么位置来填写这个绝对地址(虚拟地址)
填写的虚拟地址是多少

从前面我们知道要填写这个虚拟地址的位置是在.text段中,由可执行文件main的Section header table可知.text在
文件中的偏移为0x1000,又因为先存放的为main.o中的代码,所以直接从刚刚main.o的重定位表中可知,要替换
的位置在main.o中的偏移为0x1b,所以0x12 + 0x1000 = 0x1012(DEC 4114) 这个位置就是要要填入nSubD
ata虚拟地址的位置

通过od查看该偏移od -Ax -t x1 -j 4114 -N 4 mainNone

36.png


可以看到正是nSubData的虚拟地址0x0804c000

可以看到该地址中填写的正式,nSubData的虚拟地址,这也印证了 绝对近址32位寻址R_386_32的寻址方式

fnSubR_386_PC32相对近址32位寻址

对于相对近址寻址,要考虑这样两个问题:

计算出在可执行文件main中什么位置来填写这个相对地址(虚拟地址)

填写的相对地址是多少

由于 call 指令需要一个相对地址(偏移量),所以要计算出 当前要填入地址的位置距离fnSub虚拟地址之间的偏移

fnSub()虚拟地址:0x08049032

main()虚拟地址:0x0804900

call fnSub的语句在main()内的偏移量:1b虚拟地址0x080491b

所以:

0x08049032-0x080491b= 0x17

这样算不完全对,因为在执行call指令时,PC的值自动增加到下一条指令的开始处(本条指令末尾)
所以实际PC应该上移动

0x8049032 - (0x080491b + 0x4) = 0x13

在编译的时候,编译器已经替我们算出了这个-4所以之前在main.o中,call的地址处写的是-4

35.png

再用od查看一下od -Ax -t x1 -j 4123 -N 4 mainNone

5.png

您需要登录后才可以回帖 登录 | 注册

本版积分规则

1

关注

0

粉丝

9021

主题
精彩推荐
热门资讯
网友晒图
图文推荐

Powered by Pcgho! X3.4

© 2008-2022 Pcgho Inc.