目录结构:
一、链接总结
(1)符号解析:1、符号定义分类 2、静态链接解析过程 3、符号表条目
(2)重定位
(3)动态链接 1、静态库的缺点 2、位置无关代码 3、数据和代码调用
二、HIT linkbomb实验记录
链接的作用是合并多个可重定位目标文件与静态/动态库,分为两个步骤,符号解析and重定位。
符号解析后获得(1)要合并的.o文件的集合(2)需要确定地址的符号集合
重定位分三部分(1)同节合并(2)确定地址(3)修改引用
一、符号解析
目的:将每个模块引用的符号与目标模块定义的符号相关联
1、符号定义分类:
符号定义分为三种:
(1)全局符号:指模块内部定义,并能被外部引用的符号;
全局符号有强弱之分,函数名和已初始化的全局变量名是强符号,未初始化的全局变量名是弱符号。
(2)外部符号:其他模块定义,在本模块引用的符号,对应符号定义的弱符号,
(3)局部符号:本模块带static说明的符号
void func(){} //强符号func
void swap() ; //弱符号swap
int main(){}
多重符号处理规则:
(1)强符号不能重复定义
(2)一个符号被多次定义弱符号和一次强符号,则以强符号为准
(3)多个弱符号定义,任选其中一个;可以gcc -fno-common,来对这种情况输出警告信息
2、静态链接符号解析过程:
E: 所有合并.o文件的集合
U: 未解析符合集合
D: 已解析符号集合
1、按照命令行顺序扫描.o和.a文件
2、扫描过程将未解析的引用放入U
3、每遇到一个.o或.a,都试图去解析U中的符号
4、如果扫描到最后,U集合还未变空,则链接失败
注意必须将可重定位目标文件写在静态库前边,原因是如果颠倒次序,首先扫描静态库,而此时U为空,因此静态库中的.o文件不会加入E。而之后的.o文件中的引用符号无法找到其在静态库文件中的定义,U集合无法变空,因此导致链接错误。
如何创建静态库?———ar rs x.a \ 1.o 2.o …
符号解析的结果是
(1)E集合:.o文件集合,.o文件中含有重定位条目(记录符号位置、符号名、重定位类型),可用readelf -r x.o查看
(2)D集合:需要重定位符号集合
3、符号表条目
使用readelf -s命令可查看ELF可执行或可重定位文件的符号表
符号表中会显示:
(1)Num: 序号
(2)Value: 在对应节中的偏移量
(3)Size: 大小
(4)Type: 三种情况
1、FUNC:函数
2、OBJECT:全局变量
3、FILE:源文件的名字
4、SECTION:
(5) Bind: 两种情况:
1、bind=LOCAL:符号只能在本地使用,如static修饰的函数或变量
2、bind=CLOBAL(全局符号) :表示符号在本模块定义,但是可以被其它模块引用,如extern修饰的全局变量和函数
(6)Vis:
(7)Ndx:三种情况:
1、UND:本模块引用,但非本模块定义的符号
2、ABS:表示不需要被链接器处理,一般是文件名
3、COM:未分配空间的对象,如未被初始化的全局变量
4、数字:表示节索引号
(8)Name:符号名
二、重定位
在符号解析后获得集合E(.o文件集合)和符号集合D,接下来进入重定位阶段
重定位阶段会做三件事:
(1)同节合并 ----> 针对集合E,将多个.o中相同的节合并
(2)确定地址 ----> 针对集合D,确定D中符号的地址
(3)修改引用 ----> 针对引用符号,将确定好的地址添加到符号引用处,需要借助存放在.rel_data和.rel_text的重定位信息(汇编器会为每个引用生成)
基本重定位类型:
(1)绝对地址重定位,填入目标的绝对地址
(2)PC相对地址重定位,填入目标的相对偏移
三、动态链接
1、动态链接的优点是什么?
静态库的缺点:
(1)因为很多可执行文件都会合并相同的静态库.o文件,因此造成磁盘存储空间浪费,运行时内存资源浪费
(2)如果静态库函数有更新,则所有可执行文件都需要重新编译链接,十分麻烦。
针对静态库的缺点,当前OS很少使用静态库,转而使用共享库(Linux .so文件,Windows .dll文件),共享库在磁盘和内存中只有一个备份,可以在装入并运行时动态加载并链接。
共享库的创建方式:gcc -shared -fPIC
PIC: 位置无关代码,表示共享库代码的位置可以是不确定的
2、加载时动态链接过程:
gcc -o myproc main.o ./mylib.so(标准C库libc.so不用显式指出)
(1)先会调用静态链接器,生成部分链接的可执行目标文件(因为静态链接器链接的是.o和.so文件,所以并不会把共享库中的内容直接合并)
(2)文件调用加载器(execve),加载器会根据.interp节中的动态链接器地址,启动动态链接器
(3)动态链接器将需要链接的内容从共享库拷贝到存储空间,不占用磁盘空间
3、位置无关代码
什么是位置无关代码?— 简单说就是共享库中的代码,因为链接对象不同,合并过去的代码和数据地址是不能确定的,因此在创建动态库的时候,必需加 -fPIC选项生成位置无关代码。
4、动态链接对代码和数据的引用
(1)模块内函数调用 :直接偏移量寻址
(2)模块内数据调用:比静态链接复杂,因为无法确定链接后的地址,数据也不能通过偏移访问。因此必须动态获取当前地址,方式是通过call获取下一条指令的地址,加上已知的偏移。
(3)模块外数据调用:方法是在data节起始处构建GOT(GLOBAL_OFFSET_TABLE),然后重定位由动态链接器填入对应的值,通过固定的偏移访问(方法同模块内数据调用类似)。
(4)模块外函数调用:应用PLT(过程链接表),PLT位于.text节开始处,每项代表一个共享库函数,函数地址有动态链接器重定位填写,调用处直接call PLT的地址,PLT会跳到对应的GOT…再经过一系列跳转重定位…转到目标函数。
PPT及实验资料地址:https://github.com/Tory123/CSAPP/tree/master/linklab
实验分成五个phase,每个phase需要将main和对应的phase文件链接,修改可执行文件或可重定位目标文件。
定位字符串在.data中的偏移,然后在HexEdit中修改就可以了。
readelf -s phase1.o 查看phase1.o符号表:
这个奇怪的符号名应该就是字符串名。。。TYPE为OBJECT说明是全局变量,Size为200,Value=0,表示节内偏移为0。
接下来只需要查看data节在.o文件中的偏移是多少应该就可了,readelf -S phase1.o
Off = 80,打开HexEdit,定位 ALJsLxmF
不修改直接链接输出的话是这样的:
可见并不是从ALJsLxmF变量开始打印的,应该有一个偏移,其实现在直接在HexEdit中修改对应位置就可以了,但不妨反汇编看下:
11: 8d 90 11 00 00 00 lea 0x11(%eax),%edx
17: 83 ec 0c sub $0xc,%esp
1a: 52 push %edx
1b: 89 c3 mov %eax,%ebx
1d: e8 fc ff ff ff call 1e <do_phase+0x1e>
lea 0x11(%eax),%edx,可见加了偏移0x11,数数个数,第18个字符正好是开始输出的位置,接下来直接修改为自己的学号,链接输出:
readelf -s phase2.o 找到自己的输出函数,实验的求解可以分为两部分:
(1)如何寻访MYID,是phase2求解的关键
(2)使用HexEdit修改phase2.o
1、如何寻访MYID?
链接之前MYID存放在phase2.o的rodata节中,链接之后在MYID地址不确定,因此无法直接压栈传参。这里需要了解动态链接模块内数据的访问方式,因为是动态链接,所以模块内数据访问是通过GOT来实现的,反汇编phase2.o:
00000041 <do_phase>:
41: 55 push %ebp
42: 89 e5 mov %esp,%ebp
44: e8 fc ff ff ff call 45 <do_phase+0x4>
49: 05 01 00 00 00 add $0x1,%eax
实际上最后两句的功能就是将%eax指向GOT,那么接下来就是确定MYID在GOT中的偏移,可以在LLnDkLJR函数中查看,因为LLnDkLJR也需要访问GOT获取MYID的地址。
将未修改的phase2.o和main.o链接为可执行文件,反汇编查看:
000005b5 <yeDfwUkv>:
5b5: 55 push %ebp
5b6: 89 e5 mov %esp,%ebp
5b8: 53 push %ebx
5b9: 83 ec 04 sub $0x4,%esp
5bc: e8 9f fe ff ff call 460 <__x86.get_pc_thunk.bx>
5c1: 81 c3 13 1a 00 00 add $0x1a13,%ebx
5c7: 83 ec 08 sub $0x8,%esp
5ca: 8d 83 50 e7 ff ff lea -0x18b0(%ebx),%eax
5d0: 50 push %eax
5d1: ff 75 08 pushl 0x8(%ebp)
5d4: e8 07 fe ff ff call 3e0 <strcmp@plt>
lea -0x18b0(%ebx),%eax,说明MYID在可执行文件中的偏移为-0x18b0
2、使用HexEdit修改phase2.o实现学号输出
lea -0x18b0(%eax), %eax
push %eax
call -86
pop %eax
gcc -c 生成.o文件反汇编查看机器码:
00000000 <.text>:
0: 8d 80 50 e7 ff ff lea -0x18b0(%eax),%eax
6: 50 push %eax
7: e8 a6 ff ff ff call 0xffffffb2
c: 58 pop %eax
cookie字符串定义在栈中-0x17(%ebp)处,%eax记录循环次数,学号为9位数,循环9次每次打印一个字符。
objdump定位循环结构:
643: 8d 55 e9 lea -0x17(%ebp),%edx
646: 8b 45 e4 mov -0x1c(%ebp),%eax
649: 01 d0 add %edx,%eax
64b: 0f b6 00 movzbl (%eax),%eax
64e: 0f b6 c0 movzbl %al,%eax
651: 8d 93 70 00 00 00 lea 0x70(%ebx),%edx
657: 0f b6 04 02 movzbl (%edx,%eax,1),%eax
65b: 0f be c0 movsbl %al,%eax
65e: 83 ec 0c sub $0xc,%esp
661: 50 push %eax
662: e8 e9 fd ff ff call 450 <putchar@plt>
667: 83 c4 10 add $0x10,%esp
66a: 83 45 e4 01 addl $0x1,-0x1c(%ebp)
66e: 8b 45 e4 mov -0x1c(%ebp),%eax
671: 83 f8 09 cmp $0x9,%eax
674: 76 cd jbe 643 <do_phase+0x3e>
第十一项为即为数组名称,cDBDohBAOo,Size=256,COM表示未初始化
一开始题目没看懂,以为在phase3_patch.c中直接重新把学号赋值给cDBDohBAOo就可以了,没看清是映射数组…
因此phase3的关键就在于了解打印的是cDBDohBAOo数组中哪几个字符,需要通过查看内存获取cookie存储的值:
(gdb) x/s 0xffffd081
0xffffd081: "astpqrwhbf"
cookie存储的是字符,需要查看字符的ascii值,才能获取在cDBDohBAOo映射的位置。
ascii码对应关系如下:
’a‘:97 ->1
’s‘:115 ->1
’t‘:116 ->7
’p‘: 112 ->3
’q‘: 113 ->0
‘r’:114 ->0
‘w’:119 ->0
‘h’:104 ->2
‘b’:98 ->0
‘f’:102 ->9
因此可构造强符号cDBDohBAOo,覆盖原来的弱符号:
共120个字符,98-120是访问的范围
phase3_patch.c:
char cDBDohBAOo[256] = "111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110111912111111130017110";
phase4和phase3类似,通过一个十位的数组获取跳转表映射,输出内容.
分析反汇编,cookie定义在栈中-0x17(%ebp)处,查看内存获取数组中的值:
(gdb) x/s 0xffffd061
0xffffd061: "BGOMEIUFQJ"
对应26个英文字母,跳转表共有26个表项,由“BGOMEIUFQJ”序列可知访问跳转表表项的顺序为:2 7 15 13 5 9 21 6 17 10,接下来要做的就是修改跳转表对应表项的值。
通过反汇编可以获取跳转表表项在ELF文件中的偏移,在HexEdit中直接修改即可。
由于时间原因只改了前三个,原理都是一样的
尝试直接修改phase4.o,跳转表的内容在phase4.o的.rel.rodata和.symtab节中,.rel.rodata存储需重定位的只读数据,.symtab和动态链接有关。