骑麦兜看落日

[Binary]通过%a的leak

字数统计: 1.8k阅读时长: 8 min
2018/12/20 Share

FORTIFY

FORTIFY用于检查缓冲区溢出错误,当采用与字符串或内存操作相关的函数,如memcpy,memset,stpcpy,strcpy,strncpy,strcat,strncat,sprintf,snprintf,vsprintf,vsnprintf,gets时会进行相关检查

gcc -D_FORTIFY_SOURCE=1 仅仅只会在编译时进行检查

gcc -D_FORTIFY_SOURCE=2 程序执行时也会有检查(如果检查到缓冲区溢出就终止程序)

__printf_chk

__prinf_chk函数是一个带有check的printf函数,其原型为

1
int __printf_chk (int flag , const char * format );

随着flag的值越高,check的等级越高

printf_chk函数可以有效的阻挡格式化字符串的攻击,无法直接使用%x$p,也无法使用%n

%a

最近的2018HCTF和2018BCTF中都遇到了通过%aleak出libc地址的操作,但我在调试的时候并没有发现leak出的值在哪里,虽然计算机是一门玄学,但是也不至于无中生有吧,后来慢慢调试发现了%a等一系列与浮点数有关的转义说明的内部运作方式

以这段代码为例

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>                                  
#include <stdlib.h>

int main()
{
printf("%d,%d,%d,%d,%d,%d\n",1,2,3,4,5,6);
printf("%f,%f,%f,%f,%f,%f\n",1.0,2.0,3.0,4.0,5.0,6.0);
printf("%d,%f,%d,%f,%d,%f\n",1,2.0,3,4.0,5,6.0);
printf("%d,%d,%d,%f,%f,%f\n",1.0,2.0,3.0,4,5,6);

return 0;
}

64位下的__printf_chk

编译

gcc -o test -g -O1 test.c

1
2
3
4
5
6
7
8
9
10
0x40054a <main+4>     push   6
0x40054c <main+6> push 5
0x40054e <main+8> mov r9d, 4
0x400554 <main+14> mov r8d, 3
0x40055a <main+20> mov ecx, 2
0x40055f <main+25> mov edx, 1
0x400564 <main+30> mov esi, 0x4006c4
0x400569 <main+35> mov edi, 1
0x40056e <main+40> mov eax, 0
0x400573 <main+45> call __printf_chk@plt <0x400430>

第一句printf("%d,%d,%d,%d,%d,%d\n",1,2,3,4,5,6);

遵守一般的调用约定,前6个参数通过寄存器rdirsirdxrcxrcxr8r9,剩余的参数通过栈传递

1
2
3
4
5
6
7
8
9
10
0x400578 <main+50>     movsd  xmm5, qword ptr [rip + 0x190]
0x400580 <main+58> movsd xmm4, qword ptr [rip + 0x190]
0x400588 <main+66> movsd xmm3, qword ptr [rip + 0x190]
0x400590 <main+74> movsd xmm2, qword ptr [rip + 0x190]
0x400598 <main+82> movsd xmm1, qword ptr [rip + 0x190]
0x4005a0 <main+90> movsd xmm0, qword ptr [rip + 0x190]
0x4005a8 <main+98> mov esi, 0x4006d7
0x4005ad <main+103> mov edi, 1
0x4005b2 <main+108> mov eax, 6
0x4005b7 <main+113> call __printf_chk@plt <0x400430>

第二句printf("%f,%f,%f,%f,%f,%f\n",1.0,2.0,3.0,4.0,5.0,6.0);

浮点数通过浮点数寄存器xmm0xmm7,剩余的参数通过栈传递

1
2
3
4
5
6
7
8
9
10
0x4005bc <main+118>    movsd  xmm2, qword ptr [rip + 0x14c]
0x4005c4 <main+126> mov r8d, 5
0x4005ca <main+132> movsd xmm1, qword ptr [rip + 0x14e]
0x4005d2 <main+140> mov ecx, 3
0x4005d7 <main+145> movsd xmm0, qword ptr [rip + 0x151]
0x4005df <main+153> mov edx, 1
0x4005e4 <main+158> mov esi, 0x4006ea
0x4005e9 <main+163> mov edi, 1
0x4005ee <main+168> mov eax, 3
0x4005f3 <main+173> call __printf_chk@plt <0x400430>

第三句printf("%d,%f,%d,%f,%d,%f\n",1,2.0,3,4.0,5,6.0);

和预想一样,整数存入整数寄存器,浮点数存入浮点数寄存器

1
2
3
4
5
6
7
8
9
10
0x4005f8 <main+178>    mov    r8d, 6
0x4005fe <main+184> mov ecx, 5
0x400603 <main+189> mov edx, 4
0x400608 <main+194> movsd xmm2, qword ptr [rip + 0x118]
0x400610 <main+202> movsd xmm1, qword ptr [rip + 0x118]
0x400618 <main+210> movsd xmm0, qword ptr [rip + 0x118]
0x400620 <main+218> mov esi, 0x4006fd
0x400625 <main+223> mov edi, 1
0x40062a <main+228> mov eax, 3
0x40062f <main+233> call __printf_chk@plt <0x400430>

第四句printf("%d,%d,%d,%f,%f,%f\n",1.0,2.0,3.0,4,5,6);

编译器在编译时会根据格式化字符串的值选择寄存器的类型而不是通过格式化字符串选择

到了这里,还是无法解释为何会leak出不存在的值,那就继续调试

第一句printf("%d,%d,%d,%d,%d,%d\n",1,2,3,4,5,6);

1
2
3
4
5
6
7
8
0x7ffff7b238f1 <__printf_chk+17>     test   al, al
0x7ffff7b238f3 <__printf_chk+19> mov qword ptr [rsp + 0x30], rdx
0x7ffff7b238f8 <__printf_chk+24> mov qword ptr [rsp + 0x38], rcx
0x7ffff7b238fd <__printf_chk+29> mov qword ptr [rsp + 0x40], r8
0x7ffff7b23902 <__printf_chk+34> mov qword ptr [rsp + 0x48], r9
0x7ffff7b23907 <__printf_chk+39> je __printf_chk+96 <0x7ffff7b23940>

0x7ffff7b23940 <__printf_chk+96> mov rbp, qword ptr [rip + 0x2ad609]

跟进__printf_chk函数,发现这里进行了堆栈操作,将之前寄存器的值压入栈中

之后一个条件跳转,然后就是正常的执行流程

第二句printf("%f,%f,%f,%f,%f,%f\n",1.0,2.0,3.0,4.0,5.0,6.0);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0x7ffff7b238f1 <__printf_chk+17>    test   al, al
0x7ffff7b238f3 <__printf_chk+19> mov qword ptr [rsp + 0x30], rdx
0x7ffff7b238f8 <__printf_chk+24> mov qword ptr [rsp + 0x38], rcx
0x7ffff7b238fd <__printf_chk+29> mov qword ptr [rsp + 0x40], r8
0x7ffff7b23902 <__printf_chk+34> mov qword ptr [rsp + 0x48], r9
0x7ffff7b23907 <__printf_chk+39> je __printf_chk+96 <0x7ffff7b23940>

0x7ffff7b23909 <__printf_chk+41> movaps xmmword ptr [rsp + 0x50], xmm0
0x7ffff7b2390e <__printf_chk+46> movaps xmmword ptr [rsp + 0x60], xmm1
0x7ffff7b23913 <__printf_chk+51> movaps xmmword ptr [rsp + 0x70], xmm2
0x7ffff7b23918 <__printf_chk+56> movaps xmmword ptr [rsp + 0x80], xmm3
0x7ffff7b23920 <__printf_chk+64> movaps xmmword ptr [rsp + 0x90], xmm4
0x7ffff7b23928 <__printf_chk+72> movaps xmmword ptr [rsp + 0xa0], xmm5
0x7ffff7b23930 <__printf_chk+80> movaps xmmword ptr [rsp + 0xb0], xmm6
0x7ffff7b23938 <__printf_chk+88> movaps xmmword ptr [rsp + 0xc0], xmm7

跟进__printf_chk函数,发现虽然没有使用整数寄存器,但是同样将整数寄存器存入了栈中

之后由于未符合条件未发生跳转,接下来将浮点数寄存器压入栈中

那么这个寄存器al是什么呢

在调用__printf_chk之前,首先会对eax赋值,其值为浮点数的个数,如果没有浮点数,其值为0

1
0x4005b2 <main+108>    mov    eax, 6

64位下可变参数的传递

我们可以推测栈的布局如下

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
+--------------------------+
| value1 |
+--------------------------+
| value2 |
+--------------------------+
| ... |
+--------------------------+
| integer arg1 | <= rdx
+--------------------------+
| integer arg2 | <= rcx
+--------------------------+
| integer arg3 | <= r8
+--------------------------+
| integer arg4 | <= r9
+--------------------------+
| xmm arg1 | <= xmm0
+--------------------------+
| ... |
+--------------------------+
| xmm arg7 | <= xmm7
+--------------------------+
| rbp |
+--------------------------+
| ret |
+--------------------------+
| arg1 |
+--------------------------+
| ... |
+--------------------------+
| argn |
+--------------------------+

对于格式化字符串,在栈中有一段shadow space,用于将寄存器中的值压栈,这样有利于va_start (ap, format);的实现

shadow space中有一段栈空间用于保存整数型变量,一段栈空间用于保存浮点数类型变量,两者互不影响

在开始的时候会将所有用于传递整数类型的寄存器rdxrcxr8r9压入栈用

之后会判断al的值是否为0,al为参数列表中使用xmm寄存器的个数,当al不为0时,会将所有的浮点数寄存器压入栈中

然后根据va_start (ap, format);读取相应栈上的值

那么回到问题的最开始,leak出的值是从哪里来的呢?

很简单,由于程序编译时参数列表并没有值,所以不会将浮点数寄存器压栈,但是在根据va_start (ap, format);读取的时候仍然会从shadow space中对应浮点数的部分读取,因此我们会leak出之前就存在在栈上的垃圾值

32位下的浮点数转义说明

对于32位的系统,由于调用约定本

来就是通过栈传递参数,所以不需要shadow space,就是按照顺序将参数列表的值压栈,然后调用

1
2
3
4
5
6
7
8
9
10
11
12
0x8048490 <main+101>    push   0x40180000
0x8048495 <main+106> push 0
0x8048497 <main+108> push 5
0x8048499 <main+110> push 0x40100000
0x804849e <main+115> push 0
0x80484a0 <main+117> push 3
0x80484a2 <main+119> push 0x40000000
0x80484a7 <main+124> push 0
0x80484a9 <main+126> push 1
0x80484ab <main+128> push 0x80485a6
0x80484b0 <main+133> push 1
0x80484b2 <main+135> call __printf_chk@plt <0x8048310>

使用%a唯一的好处可能就是能够读到更多的内容

CATALOG
  1. 1. FORTIFY
  2. 2. __printf_chk
  3. 3. %a
    1. 3.1. 64位下的__printf_chk
    2. 3.2. 64位下可变参数的传递
    3. 3.3. 32位下的浮点数转义说明