linux pic指的是什么

97次阅读
没有评论

共计 5058 个字符,预计需要花费 13 分钟才能阅读完成。

这篇“linux pic 指的是什么”文章的知识点大部分人都不太理解,所以丸趣 TV 小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“linux pic 指的是什么”文章吧。

在 linux 中,pic 的中文意思为“位置无关代码”,是指代码无论被加载到哪个地址上都可以正常执行。PIC 用于生成位置无关的共享库,所谓位置无关,指的是共享库的代码断是只读的,存放在代码段,多个进程可同时公用这份代码段而不需要拷贝副本。

本教程操作环境:linux7.3 系统、Dell G3 电脑。

在 linux 中,pic 全称“Position Independent Code”,中文意思为“位置无关代码”。

一、程序虚拟地址空间及位置有关代码概述

Linux 进程从磁盘加载到内存中运行的过程中,内核会为进程分配虚拟地址空间,虚拟地址空间被划分为一块块的区域 (Segment),其中最重要的几个区域如下:

图 1 – 应用程序虚拟地址空间说明

内核地址空间,对所有应用来说都是相同的,这部分地址空间应用无法直接访问。内核地址空间不是本文关注的重点,我们重点关注应用程序的重要的一些 SEGMENT。

表 1 – 应用程序重要 segment 描述

如果系统没有开启地址随机化 (ASLR – Address Space Layout Randomization,地址随机化,后文会介绍),则 Linux 会将上面表格中的各个 segment 的地址空间放到一个固定的地址上面。

我们写一个实际的程序来看看在一个 Linux X86_64 的机器上各个 segment 的地址是如何排布的,程序如下,覆盖了我们关心的 segment。

图 2 – 虚拟地址空间演示程序

编译

gcc -o addr_test addr_test.c -static

(此处使用静态链接,以便演示位置相关代码的特征)

我们运行这个程序 3 次,会发现所有的地址都是一个固定值。这是因为在没有开 ASLR 特性时,系统不会随机化分配程序的虚拟地址空间,程序所有的地址都是按照固定的规则来生成。

图 3 – 固定 segment 地址分布

通过 objdump 命令反汇编后可以看到,对于全局变量和函数调用的访问,汇编指令跟的地址都是固定的,这样的代码我们就称它为位置相关的。

图 4 – 位置相关代码汇编语句实例

这种代码,由于地址是写死的,只能加载到指定地址上运行,一旦加载地址有变化,由于代码里访问的变量、函数地址是固定的, 加载地址变化后程序无法正常执行。

固定地址的方式虽然简单,但是无法实现一些高级特性比如动态库支持。动态库的代码会通过 mmap() 系统调用来映射到进程的虚拟地址空间,不同的进程中,同一个动态库映射的虚拟地址是不确定的。如果动态库的实现上使用位置相关的代码,则无法达到其任意地址运行的目的,这种情况下我们就需要引入位置无关代码 PIC 的概念了。

另外,我们可以看到,在没有开启地址随机化特性的系统上,由于程序各个 segment 的地址是固定的,黑客在攻击时会更加简单 (感兴趣的同学可以搜索一下 Ret2shellcode 或 Ret2libc 攻击),此时需要引入 PIE 的概念搭配 ASLR 一起来防护。

二、位置无关代码 PIC 和动态库的实现

PIC 位置无关代码是指代码无论被加载到哪个地址上都可以正常执行。gcc 选项中添加 -fPIC 会产生相关代码。

PIC 用于生成位置无关的共享库,所谓位置无关,指的是共享库的代码断是只读的,存放在代码段,多个进程可同时公用这份代码段而不需要拷贝副本。库中的变量(全局变量和静态变量)通过 GOT 表访问,而库中的函数,通过 PLT- GOT- 函数位置进行访问。Linux 下编译共享库时,必须加上 -fPIC 参数, 否则在链接时会有错误提示 (有资料说 AMD64 的机器才会出现这种错误, 但我在 Inter 的机器上也出现了)。

关键点 #1 – 代码段和数据段的偏移

代码段和数据段之间的偏移,在链接的时候由链接器给出,对于 PIC 来说非常重要。当链接器将各个目标文件的所有 p 组合到一起的时候,链接器完全知道每个 p 的大小和它们之间的相对位置。

图 5 – 代码段和数据段偏移示例

如上图所示,示例中这里 TEXT 和 DATA 时紧紧挨着的,其实无论 DATA 和 TEXT 是否是相邻的,链接器都能知道这两个段的偏移。根据这个偏移,可以计算出在 TEXT 段内任意一条指令相对于 DATA 段起始地址的相对偏移量。如上图,无论 TEXT 段被放到了哪个虚拟地址上,假设一条 mov 指令在 TEXT 内部的 0xe0 偏移处,那么我们可以知道,DATA 段的相对偏移位置就是:TEXT 段的大小 – mov 指令在 TEXT 内部的偏移 = 0xXXXXE000 – 0xXXXX00E0 = 0xDF20

关键点 #2 – X86 上指令相对偏移的计算

如果使用相对位置进行处理,可以看到代码能够做到位置无关。但在 X86 平台上 mov 指令对于数据的引用需要一个绝对地址,那应该怎么办呢?

从“关键点 1”里的描述来看,我们如果知道了当前指令的地址,那么就可以计算出数据段的地址。X86 平台上没有获取当前指令指针寄存器 IP 的值的指令(X64 上可以直接访问 RIP),但可以通过一个小技巧来获取。来看一段伪代码:

图 6 – X86 平台获取指令地址汇编

这段代码在实际运行时,会有以下的事情发生:

当 cpu 执行 call STUB 的时候,会将下一条指令的地址保存到 stack 上,然后跳到标签 STUB 处执行。

STUB 处的指令是 pop ebx, 这样就将 pop ebx 这条指令所在的地址从 stack 弹出放到了 ebx 寄存器中,这样就得到了 IP 寄存器的值。

1. 全局偏移表 GOT

在理解了前面的几点后,来看看在 X86 上是如何实现位置无关的数据引用的,此特性是通过全局偏移表 global offset table(GOT)来实现的。

GOT 是一张在 data p 中保存的一张表,里面记录了很多地址字段 (entry)。假设一条指令想要引用一个变量,并不是直接去用绝对地址,而是去引用 GOT 里的一个 entry。GOT 表在 data p 中的地址是明确的,GOT 的 entry 包含了变量的绝对地址。

图 7 – 代码地址和 GOT 表 entry 关系

如上图,根据 关键点 1 和“关键点 2”,我们可以先获取到当前 IP 的值,然后计算得到 GOT 表的绝对地址,由于变量的地址 entry 在 GOT 表中的偏移也是已知的,因此可以实现位置无关的数据访问。

以一条绝对地址的 mov 指令的伪代码为例(X86 平台):

图 8 – 位置相关 mov 指令示例

如果要变成位置无关的代码,则要多几个步骤

图 9 – 结合 GOT 实现位置无关的 mov 指令示例

通过上面的步骤,就可以实现代码访问变量的地址无关化。但是还有一个问题,这个 GOT 表里存储的 VAR_ADDR 值又是怎么变成实际的绝对地址的呢?

假设有一个 libtest.so, 有一个全局变量 g_var, 我们通过 readelf -r libtest.so 后,会看到如下的输出

linux pic 指的是什么

图 10 – rel.dyn 段全局变量重定向描述字段

动态加载器会解析 rel.dyn 段,当它看到重定向类型为 R_386_GLOB_DAT 的时候,会做如下操作:将符号 g_var 实际的地址值替换到偏移 0x1fe4 处(也就是将 Sym.Value 的值替换为实际地址值)

2. 函数调用的位置无关化实现

从理论上讲,函数的 PIC 实现也可以通过和数据引用 GOT 表相同的方式实现位置无关。不直接使用函数的地址,而是通过查 GOT 来找到实际的函数绝对地址。但实际上函数的 PIC 特性并不是这么做的,实际情况会复杂一些。为什么不按照和数据引用一样的方式,先来看一个概念:延迟绑定。

对于动态库的函数来说,在没有加载到程序的地址空间前,函数的实际地址都是未知的,动态加载器会处理这些问题,解析出实际地址的过程, 这个过程称之为绑定。绑定的动作会消耗一些时间,因为加载器要通过特殊的查表、替换操作。

如果动态库有成百上千个函数接口,而实际的进程只用到了其中的几十个接口,如果全部都在加载的时候进行绑定操作,没有意义并且非常耗时。因此提出了延迟绑定的概念,程序只有在使用到对应接口时才实时地绑定接口地址。

因为有了延迟绑定的需求,所以函数的 PIC 实现和数据访问的 PIC 有所区别。为了实现延迟绑定,就额外增加了一个间接表 PLT(过程链接表)。

PLT 搭配 GOT 实现延迟绑定的过程如下:

第一次调用函数

linux pic 指的是什么

图 11 – 首次调用 PIC 函数时 PLT,GOT 关系

首先跳到 PLT 表对应函数地址 PLT[n], 然后取出 GOT 中对应的 entry。GOT[n] 里保存了实际要跳转的函数的地址,首次执行时此值为 PLT[n] 的 prepare resolver 的地址,这里准备了要解析的函数的相关参数,然后到 PLT[0] 处调用 resolver 进行解析。

resolver 函数会做几件事情:

(1)解析出代码想要调用的 func 函数的实际地址 A

(2)用实际地址 A 覆盖 GOT[n] 保存的 plt_resolve_addr 的值

(3)调用 func 函数

首次调用后,上图的链接关系会变成下图所示:

linux pic 指的是什么

图 12 – 首次调用 PIC 函数后 PLT,GOT 关系

随后的调用函数过程,就不需要再走 resolver 过程了

三、位置无关可执行程序 PIE

PIE,全称 Position Independent Executable。2000 年早期及以前,PIC 用于动态库。对于可执行程序来讲,仍然是使用绝对地址链接,它可以使用动态库,但程序本身的各个 segment 地址仍然是固定的。随着 ASLR 的出现,可执行程序运行时各个 segment 的虚拟地址能够随机分布,这样就让攻击者难以预测程序运行地址,让缓存溢出攻击变得更困难。OS 在使能 ASLR 的时候,会检查可执行程序是否是 PIE 的可执行程序。gcc 选项中添加 -fPIE 会产生相关代码。

四、Linux ASLR 机制和 PIE 的关系

ASLR 的全称为 Address Space Layout Randomization。在 Linux 2.6.12 中被引入到 Linux 系统,它将进程的某些虚拟地址进行随机化,增大了入侵者预测目的地址的难度,降低应用程序被攻击成功的风险。

在 Linux 系统上,ASLR 有三个级别

linux pic 指的是什么

表 2 – ASLR 级别描述

ASLR 的级别通过两种方式配置:

echo level   /proc/sys/kernel/randomize_va_space

sysctl -w kernel.randomize_va_space=level

例子:

echo 0   /proc/sys/kernel/randomize_va_space  关闭地址随机化 

sysctl -w kernel.randomize_va_space=2  最大级别的地址随机化 

我们还是以文章开头的那个程序来说明 ASLR 在不同级别下时如何表现的,首先在 ASLR 关闭的情况下,相关地址不变,输出如下:

linux pic 指的是什么

图 13 – ASLR= 0 时虚拟地址空间分配情况

我们把 ASLR 级别设置为 1,运行两次,看看结果:

linux pic 指的是什么

图 14 – ASLR= 1 时虚拟地址空间分配情况

可以看到 STACK 和 MMAP 的地址发生了变化。堆、数据段、代码段仍然是固定地址。

接下来我们把 ASLR 级别设置为 2,运行两次,看看结果:

linux pic 指的是什么

图 15 – ASLR=2,PIE 不启用时虚拟地址空间分配情况

可以看到此时堆的地址也发生了变化,但是我们发现 BSS,DATA,TEXT 段的地址仍然是固定的,不是说 ASLR= 2 的时候,是完全随机化吗?

这里就引出了 PIE 和 ASLR 的关系了。从上面的实验可以看出,如果不对可执行文件做一些特殊处理,ASLR 即使在设置为完全随机化的时候,也仅能对 STACK,HEAP,MMAP 等运行时才分配的地址空间进行随机化,而可执行文件本身的 BSS,DATA,TEXT 等没有办法随机化。结合文章前面讲到的 PIE 相关知识,我们也很容易理解这一点,因为编译和链接过程中,如果没有 PIE 的选项,生成的可执行文件里都是位置相关的代码。如果 OS 不管这一点,ASLR= 2 时也将 BSS,DATA,TEXT 等随意排布,可想而知程序根本不能正常运行起来。

明白了原因,我们在编译时加入 PIE 选项,然后在 ASLR= 2 时重新运行一下看看结果如何

linux pic 指的是什么

图 16 – ASLR=2,PIE 启用时虚拟地址空间分配情况

可以看到在 PIE 打开的情况下,搭配 ASLR=2,可以实现各个段的虚拟地址完全随机化分布。

以上就是关于“linux pic 指的是什么”这篇文章的内容,相信大家都有了一定的了解,希望丸趣 TV 小编分享的内容对大家有帮助,若想了解更多相关的知识内容,请关注丸趣 TV 行业资讯频道。

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-07-29发表,共计5058字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)