背景
有时候程序员们需要写一段独立于位置操作的代码,可当作一段数据写到其他进程或者网络中去。该类型代码在它诞生之初就被称为shellcode,在软件利用中黑客们以此获取到shell权限。方法就是通过这样或那样的恶意手法使得这段代码得以执行,完成它的使命。当然了,该代码仅能靠它自己,作者无法使用现代软件开发的实践来推进shellcode的编写。
汇编常用于编写shellcode,特别是对代码大小挑剔的时候,汇编就是不错的选择。对我个人而言,多数项目都需要一段类似可以注入到其他进程的代码。这时候我就不是特别在意代码大小了,反而是开发效率以及调试能力显得尤为重要。一开始,我用NASM编写独立的汇编程序,把获得的输出文件转换为C数组,然后整合到我的程序中。这正是你在milw0rm这样的网站上所看到的,大多数exploit payload的获取方式。最终我厌倦了这样的方式,虽然很怀念NASM完备的功能,我还是开始使用内联汇编来解决问题。随着经验的积累,我发现了一个完全可用的纯C开发shellcode的方法,仅需2条内联汇编指令。就开发速度和调试shellcode时的上下文而言,真的比单纯使用汇编的方法有很大的改进。运用机器级的比如ollydbg这样的调试器,我毫不含糊,但这相对于用Visual Studio调试器来调试C源码,就是小菜一碟。
准备工作
为了确保能生成可用作shellcode这样特定格式的代码,我们需要对Visual Studio做些特殊的配置。下面的各项配置,可能随编译器的变更而变更:
1、使用Release模式。近来编译器的Debug模式可能产生逆序的函数,并且会插入许多与位置相关的调用。
2、禁用优化。编译器会默认优化那些没有使用的函数,而那可能正是我们所需要的。
3、禁用栈缓冲区安全检查(/Gs)。在函数头尾所调用的栈检查函数,存在于二进制文件的某个特定位置,导致输出的函数不能重定位,这对shellcode是无意义的。
第一个shellcode
#include <stdio.h> void shell_code() { for (;;) ; } void __declspec(naked) END_SHELLCODE(void) {} int main(int argc, char *argv[]) { int sizeofshellcode = (int)END_SHELLCODE - (int)shell_code; // Show some info about our shellcode buffer printf("Shellcode starts at %p and is %d bytes long", shell_code. sizeofshellcode); // Now we can test out the shellcode by calling it from C! shell_code(); return 0; }
这里所示例的shellcode除了一个无限循环,啥事也没干。不过有一点是比较重要的————放在shell_code函数之后的END_SHELLCODE。有了这个,我们就能通过shell_code函数开头和END_SHELLCODE函数开头间的距离来确定shellcode的长度了。还有,C语言在这里所体现的好处就是我们能够把程序本身当作一段数据来访问,所以如果我们需要把shellcode写到另外一份文件中,仅需简单的调用fwrite(shell_code, sizeofshellcode, 1, filehandle)。
Visual Studio环境中,通过调用shell_code函数,借助IDE的调试技能,就可以很容易的调试shellcode了。
在上面所示的第一个小案例中,shellcode仅用了一个函数,其实我们可以使用许多函数。只是所有的函数需要连续地存放在shell_code函数和END_SHELLCODE函数之间,这是因为当在内部函数间调用时,call指令总是相对的。call指令的意思是“从距这里X字节的地方调用一个函数”。所以如果我们把执行call的代码和被调用的代码都拷贝到其他地方,同时又保证了它们间的相对距离,那么链接时就不会出岔子。
Shellcode中数据的使用
传统C源码中,如果要用一段诸如ASCII字符的数据,可以直接内嵌进去,无需担心数据的存放,比如: WinExec(“evil.exe”)。这里的“evil.exe”字符串被存储在C程序的静态区域(很可能是二进制的.rdata节中),如果我们把这段代码拷贝出来,试图将其注入到其他进程中,就会因那段字符不存在于其他进程的特定位置而失败。传统汇编编写的shellcode可以轻松的使用数据,这通过使用call指令获取到指向代码本身的指针,而这段代码可能就混杂着数据。下面是一个使用汇编实现的shellcode方式的WinExec调用:
call end_of_string db 'evil.exe',0 end_of_string: call WinExec
这里的第一个call指令跳过字符数据”evial.exe”,同时在栈顶存放了一个指向字符串的指针,稍后会被用作WinExec函数的参数。这种新颖的使用数据的方法有着很高的空间利用率,但是很可惜在C语言中没有与此等价的直接调用。在用C写shellcode时,我建议使用栈区来存放和使用字符串。为了使微软编译器在栈上动态的分配字符以便重定位,你需要如下处理:
char mystring[] = {'e','v','i','l','.','e','x','e',0}; winexec(mystring);
你会发现,我将字符串声明为字符数组的形式。如果我这样写char mystring[] = “evil.exe”; 在老式的微软编译器中,它会通过一系列的mov指令来构成字符串,而现在仅会简单的将字符串从内存中的固定位置拷贝到栈中,而如果需要重定位代码,这就无效了。把两种方法都试试,下载免费的IDA Pro版本看看它们的反汇编代码。上面的赋值语句的反汇编应该看起来如下所示:
mov [ebp+mystring], 65h mov [ebp+mystring+1], 76h mov [ebp+mystring+2], 69h mov [ebp+mystring+3], 6Ch mov [ebp+mystring+4], 2Eh mov [ebp+mystring+5], 65h mov [ebp+mystring+6], 78h mov [ebp+mystring+7], 65h mov [ebp+mystring+8], 0
处理数据时,字符串真的是很头疼的一件事。其他比如结构体、枚举、typedef声明、函数指针啊这些,都能如你预期的那