本文简要介绍shellcode开发技术及其特点。理解这些概念可以有助于我们编写自己的shellcode。更进一步讲,你可以修改现有的漏洞利用代码来执行自己所需要的定制功能。
一、介绍
比方说你手头上有一个IE或Flash Playerd现成的漏洞利用代码,但它只能够打开计算器calc.exe。但是这实际上并没有什么卵用,不是吗?你真正想要的是可以执行一些远程命令或实现其他有用的功能。
在这种情况下,你可能想要利用已有的标准shellcode,比如来自Shell Storm数据库或由Metasploit的msfvenom工具生成。不过,你必须先理解编写shellcode的基本原则,才可以在自己的漏洞利用代码中有效地使用它们。对于不熟悉这个术语的同学们,可以参考一下维基百科:
在计算机安全中,shellcode是一小段代码,可以用于软件漏洞利用的载荷。被称为“shellcode”是因为它通常启动一个命令终端,攻击者可以通过这个终端控制受害的计算机,但是所有执行类似任务的代码片段都可以称作shellcode。......Shellcode通常是以机器码形式编写的。
shellcode是一段可用于漏洞利用载荷的机器码。“机器码”又是什么?让我们以下面的C代码为例:
#include <stdio.h> int main() { printf("Hello, World!/n"); return 0; }
这段C代码会编译成如下汇编代码:
_main PROC push ebp mov ebp, esp push OFFSET HelloWorld ; "Hello, World!/n" call _printf add esp, 4 xor eax, eax pop ebp ret 0 _main ENDP
此处,我们需要注意下main程序以及对printf函数的调用。正如调试器中突出显示的,这些代码已经编译成机器码:
所以,“55 8B EC 68 00 B0 33 01 … ”便是上述C代码的机器码。
二、shellcode如何应用到漏洞利用
举一个简单漏洞利用的示例,一个基于栈的缓冲区溢出漏洞。
void exploit(char *data) { char buffer[20]; // 缓冲区位于栈上 strcpy(buffer, data); // 使用strcpy复制数据 }
利用此漏洞的主要思路如下:(请注意本文目的不是详述缓冲区溢出的漏洞利用原理)
1)向应用程序发送长度超过20字节的字符串,其中包含shellcode。 2)由于写入数据越过静态分配缓冲区的边界,栈结构遭到破坏。同时,shellcode也会被放置在栈上。 3)字符串通过自定义的内存地址重写栈上某块重要数据(如保存的EIP或函数指针) 4)程序会从栈上跳转到你的shellcode,开始执行其中的机器码指令。
如果可以成功的利用此漏洞,你也能够运行自己的shellcode,并实际利用该漏洞做点有用的事情,而不仅仅是让程序崩溃。比如shellcode可以打开一个命令终端,下载并执行文件,重启计算机、启用远程桌面、或其他操作。
三、Shellcode特点
但是shellcode不能是任意的机器码。在编写自己的shellcode时,我们必须需要注意shellcode的一些限制:
1)不能使用字符串的直接偏移。 2)不能确定函数的地址(如printf) 3)必须避免一些特定字符(如NULL字节) 关于上述的每个问题,让我们进行一个简短的讨论。
1.字符串的直接偏移
即使你在C/C++代码中定义一个全局变量,一个取值为“Hello world”的字符串,或直接把该字符串作为参数传递给某个函数。但是,编译器会把字符串放置在一个特定的Section中(如.rdata或.data)。
由于需要与位置无关的代码,我们想把字符串作为代码的一部分,因此必须把字符串存储在栈上,正如你将在本文后续内容中所看到的。
2.函数地址
在C/C++中,调用某个函数非常简单。我们利用“#include<>”指定某个头文件,然后通过名称来调用某个函数。编译器和链接器会在背后帮你解决这个问题:解析函数的地址(例如,来自user32.dll的MessageBox函数),然后我们就可以轻而易举地通过名称调用函数。
在shellcode中,我们却不能以逸待劳了。因为我们无法确定包含所需函数的DLL文件是否已经加载到内存。受ASLR(地址空间布局随机化)机制的影响,系统不会每次都把DLL文件加载到相同地址上。而且,DLL文件可能随着Windows每次新发布的更新而发生变化,所以我们不能依赖DLL文件中某个特定的偏移。
我们需要把DLL文件加载到内存,然后直接通过shellcode查找所需要的函数。幸运的是,Windows API为我们提供了两个函数:LoadLibrary和GetProcAddress。我们可以使用这两个函数来查找函数的地址。
3.避免空字节
空字节(NULL)的取值为:0×00。在C/C++代码中,空字节被认为是字符串的结束符。正因如此,shellcode存在空字节可能会扰乱目标应用程序的功能,而我们的shellcode也可能无法正确地复制到内存中。
虽然不是强制的,但类似利用strcpy()函数触发缓冲区溢出的漏洞是非常常见的情况。该函数会逐字节拷贝字符串,直至遇到空字节。因此,如果shellcode包含空字节,strcpy函数便会在空字节处终止拷贝操作,引发栈上的shellcode不完整。正如你所料,shellcode当然也不会正常的运行。
上图中的两条指令从功能上来说是等价的,但你可以清楚地看到第一条指令包含空字节,而第二条指令却包含空字节。虽然空字节在编译后的代码中非常常见,但是我们可以很容易地避免。
还有,在一些特殊情况下,shellcode必须避免出现类似/r或/n的字符,甚至只能使用字母数字。
四、Linux平台与Windows平台的shellcode对比
相对于Windows平台,编写针对Linux平台的Shellcode可能更为简单。这是因为在linux平台上,我们可以轻松地通过0×80中断执行类似write、execve或send的系统调用。
例如,在linux平台上执行“Hello world”shellcode只需要以下几个步骤:
1)指定系统调用syscall序号(如“write”)。 2)指定系统调用syscall的参数(如,stdout,“Hellow, world”,字符串长度) 3)调用0x80中断来执行系统调用syscall。
这将会发起调用:write(stdout, “Hello, world”, length).
在Windows平台上,情况会更加复杂。为了生成更为可靠的shellcode,我们需要花费更多的步骤:
1)获取kernel32.dll 基地址; 2)定位 GetProcAddress函数的地址; 3)使用GetProcAddress确定 LoadLibrary函数的地址; 4)然后使用 LoadLibrary加载DLL文件(例如user32.dll); 5)使用 GetProcAddress查找某个函数的地址(例如MessageBox); 6)指定函数参数; 7)调用函数。
五、结论
本文是Windows平台shellcode编写入门系列的第1部分。本文介绍了什么是shellcode,shellcode存在哪些限制,以及Windows和Linux平台的Shellcode之间的区别。第2部分会包括简单介绍汇编语言、PE()文件格式、PEB(进程环境块)。接下来,你会了解这些内容如何帮助你编写自定义的shellcode。
*参考来源: securitycafe ,FB资深作者Rabbit_Run翻译,转载请注明来自FreeBuf黑客与极客(FreeBuf.COM)