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

How does printf() var-arg referencing interact with stack memory layout?

给定代码片段:

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 位,这表明函数连续分配并引用相对地,但其他来源表明不能保证编译器必须连续分配,我担心这篇论文是一种简化。

is there any guarantee that the compiler would store "Val: %d" and '5' contiguously

几乎可以保证他们不会。 5 足够小,可以直接嵌入指令流中,而不是通过内存地址(指针)加载——类似于 movl #5, %eax and/or 然后压入堆栈——而字符串对象将被放置在可执行映像的只读数据区,并通过指针被引用。我们谈论的是编译时 可执行映像的布局。

除非你指的是堆栈运行时布局,其中是的,单词大小的指针 到该字符串,字长常量 5 将彼此相邻。但顺序可能与您的预期相反——研究 'C function calling convention'.

[稍后编辑:运行 现在一些带有 -S(输出程序集)的代码示例;我被提醒,在调用者中使用少量寄存器(即 CPU 寄存器可以被无害地覆盖),并且被调用函数的参数很少,参数可以完全通过寄存器传递以节省指令和内存。所以堆栈的布局实际上很难预测,即使攻击者可以访问源代码。尤其是 gcc -O2,它将我的 main -> my_function -> printf 函数序列折叠成 main -> printf]

大多数攻击利用堆栈溢出,因为恶意代码试图修改上述只读数据区域中的内存时遇到障碍 -- OS 中止进程。

printf 的行为是特殊的,因为格式字符串就像一个微型计算机程序,它告诉 printf 为它找到的每个 '%' 格式说明符查看堆栈上的参数。如果这些参数实际上从未被压入,and/or 的大小不同,printf 将盲目地遍历它不应该遍历的堆栈部分,并且可能会在堆栈的更上层(调用链下方)揭示私有数据可能所在的数据.如果 printf 的第一个参数至少是一个 constant,编译器至少可以在后续参数与 '%' 说明符不匹配时警告您,但是当它是一个变量时,所有赌注都将关闭。

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

有趣的问题。这是两个测试程序的汇编输出:一个 32-bit/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-bit 程序集 (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-bit 程序集 (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    , %edx
        movl    , %esi
        movq    %rax, %rdi
        movl    , %eax
        call    printf