C: printf() 的堆栈漏洞?

C: printf()'s stack vulnerability?

我写了一个简单的C程序来看一下printf()的栈帧

#include <stdio.h>
int main(void){
    printf("%s");
}

我认为堆栈的工作方式是 main() 首先将“%s”压入堆栈,因此 printf 要么会出现段错误,要么会打印出垃圾。但是,在我的反汇编中,没有任何地方将“%s”压入堆栈。我打印出了 %fp 和 %sp 之间的所有值,但是其中 none 个包含“%s”。

main 的程序集转储:

0x00400950 <+0>:    lui gp,0x2
0x00400954 <+4>:    addiu   gp,gp,-32224
0x00400958 <+8>:    addu    gp,gp,t9
0x0040095c <+12>:   addiu   sp,sp,-32
0x00400960 <+16>:   sw  ra,28(sp)
0x00400964 <+20>:   sw  s8,24(sp)
0x00400968 <+24>:   move    s8,sp
0x0040096c <+28>:   sw  gp,16(sp)
0x00400970 <+32>:   lw  v0,-32744(gp)
0x00400974 <+36>:   nop
0x00400978 <+40>:   addiu   v0,v0,2864
0x0040097c <+44>:   move    a0,v0
0x00400980 <+48>:   lw  v0,-32688(gp)
0x00400984 <+52>:   nop
0x00400988 <+56>:   move    t9,v0
0x0040098c <+60>:   jalr    t9
0x00400990 <+64>:   nop
0x00400994 <+68>:   lw  gp,16(s8)
0x00400998 <+72>:   move    sp,s8
0x0040099c <+76>:   lw  ra,28(sp)
0x004009a0 <+80>:   lw  s8,24(sp)
0x004009a4 <+84>:   addiu   sp,sp,32
0x004009a8 <+88>:   jr  ra
0x004009ac <+92>:   nop

如果“%s”没有存储在栈上,它存储在哪里?另外,它从哪里得到相应的字符串打印出来?

        .file   "1.c"
        .section        .rodata
.LC0:
        .string "%s"
        .text
        .globl  main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        movl    $.LC0, %edi
        movl    [=10=], %eax
        call    printf
        movl    [=10=], %eax
        popq    %rbp
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:
        .size   main, .-main
        .ident  "GCC: (Ubuntu 5.2.1-22ubuntu2) 5.2.1 20151010"
        .section        .note.GNU-stack,"",@progbits

我用过gcc生成程序集。该字符串未按照您的想法存储。字符串是静态存储的。

据我所知,mips arch 使用 a0 ~ a3 作为函数调用的前四个参数。

通常,在实现级别将发生的是 "%s" 字符串文字在某种静态存储中。当调用 printf 时,指向该字符串的指针作为参数传递。这并不一定意味着这个指针被压入堆栈。如何传递参数取决于参数传递约定。它可以加载到寄存器中。

在您的特定情况下,这里是 "%s" 准备通过的地方:

0x00400970 <+32>:   lw  v0,-32744(gp)
0x00400974 <+36>:   nop
0x00400978 <+40>:   addiu   v0,v0,2864
0x0040097c <+44>:   move    a0,v0

首先从相对于全局指针寄存器的数据区加载基地址。然后将这个基地址偏移2864得到"%s"的地址。然后地址被移动到 a0,寄存器 v0 被重新用于计算 printf 的地址(由于它在共享库中,这很复杂)。

现在由于 "%s" 没有相应的 char * 参数,当然行为在形式上是未定义的。但实际行为是什么?

实际行为可能是 printf 会尝试以某种方式提取 char * 指针,也许是从堆栈中提取。 (可变参数函数的尾随参数通常只是放在堆栈上。)

现在由于调用者没有在那里放置参数,printf 提取一些 "garbage" 单词并将其视为 char *,打印该单词指向的内存作为一个以 null 结尾的字符串。也就是说,如果该词指向有效内存。

如果您的目标是转储一些字节的堆栈内存,这根本就不可靠。你不知道什么样的值被解释为 char * 指针,或者它指向什么,或者它是否指向任何东西,更不用说堆栈了。

伪造的 char * 本身可能会从堆栈中拉出,但您实际上并没有打印该指针本身。

以下 可能会 为您提供几个字节的堆栈:

printf("%p\n");

此外,类似地,任何不带参数的数字转换都可以。原因是 %p%s 不同,它实际上打印指针本身。如果 %p 的参数值是从堆栈中提取的,那么该值的打印表示会泄漏一些有关一小部分堆栈的信息。