使用C编写shellcode

背景

有时候程序员们需要写一段独立于位置操作的代码,可当作一段数据写到其他进程或者网络中去。该类型代码在它诞生之初就被称为 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

这里所示例的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指令跳过字符数据”evial.exe”,同时在栈顶存放了一个指向字符串的指针,稍后会被用作WinExec函数的参 数。这种新颖的 使用数据的方法有着很高的空间利用率,但是很可惜在C语言中没有与此等价的直接调用。在用C写shellcode时,我建议使用栈区来存放和使用字符串。 为了使微软编译器在栈上动态的分配字符以便重定位,你需要如下处理:

你会发现,我将字符串声明为字符数组的形式。如果我这样写char mystring[] = “evil.exe”; 在老式的微软编译器 中,它会通过一 系列的mov指令来构成字符串,而现在仅会简单的将字符串从内存中的固定位置拷贝到栈中,而如果需要重定位代码,这就无效了。把两种方法都试试,下载免费 的IDA Pro版本看看它们的反汇编代码。上面的赋值语句的反汇编应该看起来如下所示:

处理数据时,字符串真的是很头疼的一件事。其他比如结构体、枚举、typedef声明、函数指针啊这些,都能如你预期的那样正常工作,你可以利用C提供的全套功能。确保数据为局部变量,一切都OK了。

使用库函数

这篇文章专注于Windows环境的shellcode。上面所提及的一些规则也适用于Unix系统。Windows环境下的 shellcode会更复 杂一点,因为我们没有一致公开的方法进行系统调用,就像在Unix中仅需几条汇编代码就可以的那样(对int 80h的调用)。我们需要利用DLL中提供 的API函数,来进行系统调用做些像读写文件、网络通信这样的事。这些DLL最终会进行必要的系统调用,而它的实现细节几乎随着每次Windows的发布 而变化。像《The Shellcoder’s Handbook》这样的标榜性著作描绘了搜寻内存中DLL和函数的方法。如果想将shellcode做 到在不同Windows版本间的可移植性,有两个函数是必须的:1、查找kernel32.dll的函数;2、实现GetProcAddress()函数 或者查找GetProcAddress()地址的函数。我所提供的实现是基于hash的而非字符串的比较,下面我将提供用于shellcode的hash 实现,并做个简短的说明。

Hash函数

在 shellcode中,使用hash进行函数的查询是比较普遍的。较流行的ROR13 hash方法是最常用的,它的实现也用在了 《The Shellcoder’s Handbook》中。它的基本思想是当我们要查询一个名为“MyFunction”的函数时,不是将字符串存放在 内存中,对每个函数名进行字符串的比对,而是生成一个32位的hash值,将每个函数名进行hash比对。这并不能减小运行时间,但是可以节省 shellcode的空间,也具有一定的反逆向功效。下面我提供了ASCII和Unicode版本的ROR13 hash实现

查找DLL

有3个链表可以用来描述内存中加载的DLL:

InMemoryOrderModuleList、 InInitializationOrderModuleList和InLoadOrderModuleList。它们都在PEB(进程环境块)中。在你 的shellcode中,用哪个都可以,我所用的是InMemoryOrderModuleList。需要如下的两条内联汇编来访问PEB:

现在我们已经获取了PEB,可以查询内存中的DLL了。唯一的一直存在于Windows进程内存中的DLL是ntdll.dll,但 kernel32.dll会更方便一点,并且在99.99%的Windows进程中(Win32子系统)都可用。下面我提供的代码实现会查询module 列表,利用unicode的ROR13 hash值查到kernel32.dll。

[via@ idf实验室 / Nick Harbour ]