freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

静态链接符号重定位 | GCC | PWN基础
2022-11-03 10:37:23
所属地 江西省

静态链接介绍

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

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

  • .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.指定库的查找位置,后面跟着.就表示在当前目录下查找

1666760926_6358c0dee2b886d432ac6.png!small?1666760920433

实验相关

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

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

实验代码

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.csub.c中并没有引入使用标准库的函数,若引入了标准库并使用了其中的函数,不建议使用ld来进行链接,因为需要找对应所依赖的静态库文件,直接使用gcc -static即可

静态链接过程

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

  1. 空间与地址分配
  2. 符号解析与重定位

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

空间与地址分配

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

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

1666760942_6358c0ee12034002fc368.png!small?1666760935727

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


扫描所有的输入可重定位文件,获得每个Section的属性信息(长度、位置等),并将其合并,计算合并后,各个Section的长度与位置关系,建立映射关系。

收集所有输入可重定位文件中的符号表中所有的符号定义和符号引用,统一放到全局符号表中

链接器为目标文件分配地址和空间中地址和空间其实有两个含义:

  • 在链接后输出的可执行文件中的空间
  • 当可执行文件装载进内存后的虚拟地址的虚拟地址空间

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

符号解析与重定位

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

实验观察

main.osub.oSection信息

main.o

1666760953_6358c0f935d8f05950bb0.png!small?1666760946793

sub.o

1666760957_6358c0fd23024703d9a43.png!small?1666760950656

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

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

1666760960_6358c100b1ab97fa458e4.png!small?1666760954302

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

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

1666760986_6358c11a75e97ef6cd9d4.png!small?1666760979949


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

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

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

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

1666760995_6358c123892ae61eee0b9.png!small?1666760989061

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

  • nSubData.text中 要修正的地址就是那一串00 00 00 00的位置,也就是11 + 1 = 12偏移位置
  • fnSub.text中要修正的位置就是 -4 (fc ff ff ff)对应的位置,也就是1a + 1 = 1b偏移位置

指令修正方式

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

  • 绝对近址32位寻址R_386_32
  • 相对近址32位寻址R_386_PC32
宏定义重定位修正方法
R_386_321绝对寻址修正 S + A
R_386_PC322相对寻址修正 S + A -P

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

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

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

所以对于 :

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

观察mainNone修正后的地址

main.osub.o链接为mainNone

Section 信息

1666761004_6358c12c31c35b30622b3.png!small?1666760997683

首先验证几个结论:

  • .text的大小为0x3d正好= 0x32 + 0xb( 参照上面main.osub.oSection header table
  • .data的大小为0x4正好= 0x0 + 0x4

符号表

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

1666761012_6358c1344568707229fea.png!small?1666761005780

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

1666761021_6358c13d998da6021ff4e.png!small?1666761015069

nSubDataR_386_32绝对近址32位寻址

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

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

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

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

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

1666761028_6358c14422e1ea385d086.png!small?1666761021497

可以看到正是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

1666761034_6358c14a8bd08ffe5cc24.png!small?1666761028490

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

1666761037_6358c14d875a1759b7bd7.png!small?1666761031002

# 系统安全
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录