printf()var-arg引用如何与堆栈内存布局交互?

时间:2015-08-02 02:25:15

标签: c memory stack string-formatting exploit

给出代码片段:

int main()
{
    printf("Val: %d", 5);
    return 0;
}

是否可以保证编译器会连续存储"Val: %d"'5'?例如:

+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| ... |  %d | ' ' | ':' | 'l' | 'a' | 'V' | '5' | ... |
+-----+-----+-----+-----+-----+-----+-----+-----+-----+
      ^                                   ^     ^
      |           Format String           | int |

这些参数究竟是如何在内存中分配的?

此外,printf函数是否相对于格式字符串或绝对值访问int?例如,在数据中

+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| ... |  %d | ' ' | ':' | 'l' | 'a' | 'V' | '5' | ... |
+-----+-----+-----+-----+-----+-----+-----+-----+-----+
      ^                                   ^     ^
      |           Format String           | int |

当函数遇到%d时,是否已存在将被引用的函数的第一个参数的存储内存地址,或者是否相对于格式字符串的第一个元素计算该值?

很抱歉,如果我感到困惑,我的主要目标是了解字符串格式化漏洞,允许用户提供格式字符串,如本文档所述

http://www.cis.syr.edu/~wedu/Teaching/cis643/LectureNotes_New/Format_String.pdf

我对第3页和第4页描述的攻击产生了担忧。我认为%x要跳过字符串占用的16位,这表明函数是连续分配的,引用相对但其他来源表明无法保证编译器必须连续分配,我担心该文件是一种简化。

2 个答案:

答案 0 :(得分:16)

  

是否保证编译器会存储" Val:%d"和' 5'连续

它几乎可以保证他们赢得。 5足够小,它可以直接嵌入到指令流中,而不是通过内存地址(指针)加载 - 类似于movl #5, %eax和/或后跟推入堆栈 - 而字符串对象将被布置在可执行映像的只读数据区域中,并将通过指针引用。我们正在讨论可执行映像的编译时间布局

除非您指的是堆栈运行时布局,其中是,该字符串的字大小指针,以及 - 大小常数5,将彼此相邻。但顺序可能与您的期望相反 - 研究C函数调用约定'。

[稍后编辑:现在用-S(输出程序集)运行一些代码示例;我提醒说,在调用者中使用轻度寄存器(即CPU寄存器可以被无错地覆盖),并且调用函数的参数很少,参数可以完全通过寄存器传递以保存指令和存储器。因此,即使攻击者可以访问源代码,堆栈的布局实际上也很难预测。特别是gcc -O2,它折叠了我的主要 - > my_function - > printf函数序列进入main - >的printf]

大多数漏洞使用堆栈溢出,因为恶意代码在试图修改上述只读数据区域中的内存时遇到了问题 - 操作系统中止了该过程。

printf的行为是特殊的,因为格式字符串就像一个微型计算机程序,告诉printf查看堆栈中每个'%'它找到的格式说明符。如果这些参数实际上从未被推送过,和/或具有不同的大小,那么printf将盲目地遍历堆栈的一部分,它不应该在堆栈中向下显示数据(在调用链下方)私有数据可能存在。如果printf的第一个参数至少是一个常量,编译器至少可以在后续参数不匹配'%'说明者,但当它是一个变量时,所有的赌注都会被取消。

从安全角度来看,printf很糟糕,而且计算密集,但非常强大且富有表现力。欢迎来到C.: - )

第二次编辑 现在你在评论中的第一个问题......因为你可以看到你的术语,也许思想有点乱码。研究以下内容,了解正在发生的事情。不要担心指向字符串的指针。这是在Linux 3.13 64位上用gcc 4.8.2编译的,没有标志。请注意格式说明符的过度使用本质上是如何向后遍历堆栈,揭示在前一个函数调用中传递的参数。

/* Do not compile this at home. */
#include <stdio.h>

int second() {
  printf("%08X %08X %08X %08X %08X %08X %08X %08X\n");
}

int first(int a, int b, int c, int d, int e, int f, int g, int h) {
  second();
}

int main(int argc, char **argv) {
  first(0xDEEDC0DE, 0x1EADBEEF, 0x11BEDEAD, 0xCAFAF000, 0xDAFEBABE, 0xAACEBACE, 0xE1ED1EAA, 0x10F00FAA);
  return 0;
}

两次背对背跑,stdio输出:

1EADBEEF 11BEDEAD CAFAF000 DAFEBABE AACEBACE 75F83520 00400568 88B151C8

1EADBEEF 11BEDEAD CAFAF000 DAFEBABE AACEBACE 8B4CBDC0 00400568 7BB841C8

答案 1 :(得分:3)

有趣的问题。以下是两个测试程序的汇编输出:一个32位/ MSVC,另一个64位GCC:

测试程序:

/*
 * Sample output:
 * A
 * B: 49, 2, 5.000000
 */
#include <stdio.h>

int main(int argc, char *argv[]) {
  printf ("A\n");
  printf ("B: %d, %c, %f\n", 0x31, 0x32, 5.0);
  return 0;
}

MSVS / 32位程序集(cl /Fa):

_DATA   SEGMENT
$SG2938 DB  'A', 0aH, 00H
    ORG $+1
$SG2939 DB  'B: %d, %c, %f', 0aH, 00H
...
CONST   SEGMENT
__real@4014000000000000 DQ 04014000000000000r   ; 5
...
    push    OFFSET $SG2938
    call    _printf
...
    movsd   xmm0, QWORD PTR __real@4014000000000000
    movsd   QWORD PTR [esp], xmm0
    push    50                  ; 00000032H
    push    49                  ; 00000031H
    push    OFFSET $SG2939
    call    _printf

GCC / 64位程序集(gcc -S):

.LC0:
        .string "A"
.LC1:
        .string "B: %d, %c, %f\n"
...
        movl    %edi, -4(%rbp)   // You'll notice that GCC substitutes "puts()" for "printf()" here
        movq    %rsi, -16(%rbp)
        movl    $.LC0, %edi
        call    puts
...
        movl    $.LC1, %eax     // Also notice the absence of "push": we're passing arguments in registers, instead of on the stack
        movsd   .LC2(%rip), %xmm0
        movl    $50, %edx
        movl    $49, %esi
        movq    %rax, %rdi
        movl    $1, %eax
        call    printf