给出代码片段:
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位,这表明函数是连续分配的,引用相对但其他来源表明无法保证编译器必须连续分配,我担心该文件是一种简化。
答案 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