LOADING

加载过慢请开启缓存 浏览器默认开启

dl学习总结

最近学习的dl攻击相关知识,以及ELF文件结构、编译过程、动态链接机制等基础知识,有误还请多多指正。

1.程序编译流程(从代码到可执行文件)


在Unix系统上,从源文件到目标文件的转化是由编译器驱动程序完成的。该转化过程通常包括四个阶段:预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)、链接(Linking)。执行这四个阶段的程序(预处理器、编译器、汇编器、和链接器)一起构成了编译系统。一图流总结为

bianyi

1.1预处理阶段

处理c语言中的#define、#ifdef、#include系列的指令,并删除所有注释,可使用以下命令得到预处理后的程序

gcc -E tes.c -o tes.i

为了便于理解,我编写了一个测试程序,观察一下两个文件的内容

tes.c:
tesc
tes.i:
tesi

可以发现预处理后的文件多了上千行的代码,新增的部分包含了头文件中的内容.

1.2 编译阶段

此阶段将c语言程序处理为汇编语言程序,其具体过程还请参考《编译原理》(人生苦短,我学编原),可使用以下命令得到编译后的程序(注意一定得是大写):

gcc -S tes.i -o tes.s

tes.s:

	.file	"tes.c"
	.text
	.globl	glo
	.bss
	.align 4
	.type	glo, @object
	.size	glo, 4
glo:
	.zero	4
	.text
	.globl	func
	.type	func, @function
func:
.LFB6:
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	%edi, -20(%rbp)
	movl	$5, -4(%rbp)
	movl	-20(%rbp), %eax
	addl	%eax, -4(%rbp)
	movl	-4(%rbp), %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE6:
	.size	func, .-func
	.section	.rodata
.LC0:
	.string	"%d"
.LC1:
	.string	"Hello!"
	.text
	.globl	main
	.type	main, @function
main:
.LFB7:
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$16, %rsp
	movl	$10, -8(%rbp)
	movl	$0, -4(%rbp)
	movl	-8(%rbp), %eax
	movl	%eax, %edi
	call	func
	movl	%eax, -4(%rbp)
	movl	-4(%rbp), %eax
	movl	%eax, %esi
	leaq	.LC0(%rip), %rax
	movq	%rax, %rdi
	movl	$0, %eax
	call	printf@PLT
	leaq	.LC1(%rip), %rax
	movq	%rax, %rdi
	movl	$0, %eax
	call	printf@PLT
	movl	$0, %eax
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE7:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 11.2.0-19ubuntu1) 11.2.0"
	.section	.note.GNU-stack,"",@progbits
	.section	.note.gnu.property,"a"
	.align 8
	.long	1f - 0f
	.long	4f - 1f
	.long	5
0:
	.string	"GNU"
1:
	.align 8
	.long	0xc0000002
	.long	3f - 2f
2:
	.long	0x3
3:
	.align 8
4:

1.3汇编阶段

该过程就是将汇编代码转为机器码,把这些指令打包成一种叫做可重定位目标程序的格式,也叫做目标文件(即.o文件),可使用以下命令得到目标文件:

gcc -c tes.s -o tes.o

1.4 链接阶段

该阶段为本文的重点讨论部分,也是最复杂的一部分(自我认为),接下来先简单介绍一下其为何物。
链接是将各种目标文件组合成一个单一可执行文件的过程。例如上述程序调用了printf函数,那么就需要将printf.o和tes.o文件进行链接。链接的时机很自由,可以是编译时、加载时、运行时。而链接又主要分为静态链接和动态链接。

1.4.1 静态链接

静态链接通俗来讲就是将多个目标文件集合到一个可执行文件中,如下图所示,其中libc.a为libc静态库。
static_linking
这种方式很显然会严重造成空间的浪费,因为在目标文件中可能包含有很多未被用到的函数,并且如果在多个源文件中使用了同一个函数,采用静态链接会使得内存中存在多个副本;另一方面,如果库函数的代码更新了,那么就需要重新进行编译,较为繁琐。
由此,自然便出现了动态链接的方式。

1.4.2 动态链接

正如静态库(*.a)之于静态链接,动态链接中使用的是共享库。共享库是一个目标模块,在运行或加载时,可以加载到任意的内存地址,并和一个在内存中的程序链接起来。这个过程就是动态链接。每个共享库都只包含一个共享对象文件(Shared Object file),即*.so文件。

动态链接的过程

经翻译器(即前三阶段)处理后得到的可重定位目标文件(*.o),首先通过链接器将其与相应的SO文件(如libc.so)静态执行一些链接,得到部分链接的可执行目标文件(也就是pwn题中常规的附件),然后当加载程序时通过动态链接器(如ld-linux.so)进行动态链接,在内存中形成完全链接的可执行文件。一图流如下:

dyn_linking

接下来我们讨论ELF(Executable and Linking Format可执行和可链接的格式)文件,即目标文件与可执行文件的大致布局。


总的来说,链接过程可以由此图概括(不得不说这图做的是真好)

linking

2.ELF文件简介


首先要明白两个概念,section和segment,也就是节和段(面试被问的问题,当时一脸懵,现在看来还是得多了解基础)。先来个一图流:

ELF_format

ELF头记录着整个ELF文件的基本信息,其中包含32/64位、大/小端、程序头部表偏移、节区头部表偏移、入口地址等信息。

2.1 Section && Segment

Section(节)

首先,节是可链接目标文件(*.o)和可执行文件的组成结构。如果学过汇编语言的话应该很清楚节是什么(反正我没学过),如汇编中的.text,.bss,.data就是所谓的节,它们告诉汇编器接下来的代码应该放入.o文件的哪个节中。此时的节只有常规的几个节区,即与动态链接无关的节,如图所示:

o_section

这里显示的节头就是上边的Section header table,该部分保存了若干个描述section信息的结构体,程序定位节区的地址即可通过ELF头找到节区头部表,进而找到对应的节区。

当被链接为动态链接可执行文件时,由于需要在加载时进行动态链接,所以新建了一些动态链接相关节区,如下图所示(不完全展示):

e_section

Segment(段)

当该文件被加载到内存中时,文件就会以Segment组织。简单来讲,段是由连续的节组成的。下图为一个示例:

segment

可以发现,此时文件拥有了程序头,程序头与节区头部表作用类似,其中保存的结构体表示该程序的分段信息,指定了每个段的起始地址、偏移、对齐方式等信息。例如.text section的内容会组装到代码段中,.data, .bss等节的内容会包含在数据段中。

2.2 动态链接相关Section简介

.interp

保存一个字符串,即系统中动态链接器的路径,如/lib64/ld-linux-x86-64.so.2

.dynamic

类似于上文的头部表,保存了动态链接器用到的信息。其由许多结构体组成,这里我用IDA处理过后的内容作为示例(不完全),结构体包含d_tag、d_un两项,d_tag标识了节表名或表示其他含义,d_un则表示一个值或地址偏移。

.dynamic

对比2.1的程序头部表可以发现,该Section出现在了两个Segment中,这表明Segment的划分是可以交叉的。

接下来是介绍一些.dynamic中标识过的几个重要的节表。

.dynsym && .dynstr

即符号表和字符串表,记录了定位和重定位程序的符号定义和符号应用的信息。符号表.dynsym也由若干个结构体组成,st_name为在字符串表中的索引,st_info低四位表示符号的类型,高四位为绑定属性,st_other表示符号的可见性,st_shndx表示该表项相关的节在节头表中的索引,st_value为关联符号的值,可以是绝对地址值或者偏移值,st_size表示该符号的关联大小。

字符串表.dynstr则是符号表中的符号字符串,以空字符结尾,第一个字节为空字符。

dynsymstr

.rela.dyn && .rela.plt

即重定位节,记录所有变量或函数的动态链接重定位信息,通俗来讲就是需要修正的函数地址。其也也是由若干个结构体构成,r_offset指定了应用重定位操作的位置,r_info的高四位指定了其在符号表的索引(r_info<<32),剩余位数代表重定位类型((Elf64_Word)(r_info)),r_addend为常量加数。IDA中每个条目的后方的蓝色字体即说明了重定位类型和符号信息/计算方法。

rela

.rela.dyn的信息由.dynamic中的DT_RELA指定,.rela.plt由DT_JMPREL指定。区别在于前者在程序运行时进行重定位;后者在函数首次调用时重定位。前者包含的常见的条目有.init、.fini、.bss、.data、.got节,而后者则是单一的.got节(若开启延迟绑定则为.got.plt)。

.plt && .got

首先,以防你不知道什么叫延迟绑定,延迟绑定是一款用于减少动态链接器在加载程序时进行重定位的时间的技术,在这款技术中,你将会在函数第一次被调用时进行重定位,从而获取函数的绝对地址,同时,在启动延迟绑定的时候,程序的RELRO会变为黄色的Partial RELRO,你可以狠狠地糟蹋got表。所以,延迟绑定,启动!(

.got叫做全局偏移表(Global Offset Table),在开启延迟绑定时,got表被分为.got和.got.plt,其中.got节在程序执行开始后不可写,.got.plt由于延迟绑定,所以是可读可写的。

got

值得注意的是,.got.plt中的前三个表项存放了三个地址,分别表示.dynamic节、linkmap、_dl_runtime_resolve函数的地址

.plt叫做过程链接表(Procedure Linkage Table),其中保存的是一些代码,看完其功能自然就会明白。其通常包含.plt和.plt.sec两部分。

plt

plt.sec

为了便于理解,我更改了其函数名。其执行流如下,若为延迟绑定的第一次调用,代码中call printf ==> _printf(.plt.sec) ==> printf@plt (.plt)[printf_ptr(.got.plt)中存放的地址] ==> push 0(为该函数的索引值) ==> plt0 ==> push linkmap ==> _dl_runtime_resolve ==> _dl_fixup(更改.got.plt中的值,并执行printf)。第二次及以后调用printf时,printf_ptr(.got.plt)中保存的已经是printf的真实地址了,便可直接执行。

所以plt节的作用简单来说就是引导执行动态链接的代码。

3.动态链接相关攻击


上边提到了动态链接时的一个重要函数_dl_runtime_resolve和结构体link_map,接下来将围绕这些进行展开。

link_map

link_map是一个链式结构的结构体,其内容很多,这里介绍几个主要字段。link_map的每一个结构体保存着该程序所用到的一个共享库的信息(第一个为源文件),l_next指向下一个结构体的首地址,l_name表示该共享库的名称,l_addr为该库的首地址,l_info[x]表示该文件下的.dynamic节,第x项表示d_tag=x的表项(个别d_tag值特殊的表项不满足该条件,但仍然包含在l_info内)。下面贴出了该示例程序的link_map主要字段的内容。

link_map0

link_map1

link_map2

link_map3

_dl_runtime_resolve

该函数有两个输入参数,即dl_runtime_resolve(link_map_obj, reloc_arg),link_map_obj即为link_map结构体,reloc_arg为在重定位目标函数的索引值,即在.plt中push进栈顶的两个值。该函数调用_dl_fix_up函数,使用两个输入参数寻找目标函数的地址,并将其写入到对应的.got中。

寻找地址的过程在了解了link_map结构和几个动态链接相关节的结构时就猜也能猜出个大概了。

首先link_map_obj标识了源文件的.dynamic所在位置,于是便可以定位到.rela.plt等节的位置,reloc_arg标识了目标函数(以下成为bar)对应的结构体在.rela.plt中的索引位置,r_offset告知了bar_ptr(.got.plt)的位置,即要将bar真实地址写入的地方,而r_info可以获取bar在.dyn.sym中的索引,进而获取其在.dyn.str中的函数名;通过link_map_obj.l_next字段可以转移到bar所在的库对应的结构中,再利用一些手段通过函数名(hash的方法)获得其在外部库中的.dyn.sym的结构,于是l_addr+st_value便可获得bar的真实地址,最后将其写入bar_ptr并进行第一次调用。

于是攻击的思路便有了,在无法获取基地址改写got表的情况下,注意到所有的节表都是通过.dynamic寻址的,而.dynamic只有在NO RELRO时可写,因此伪造节表首地址是几乎不可行的,但是reloc_arg采用的是偏移寻址,如果可以控制reloc_arg的值,便可以将其指向一个可控地址,这里有我们伪造的r_offset和r_info,r_info同理也是偏移寻址,因此可以控制r_info指向伪造的函数名,如system,接下来dl就会去解析system的地址,最后执行system。

参考资料