4.2.1 Shellcode开发

4.2.1 Shellcode开发

1.Shellcode概述

为了更好地说明,本节以栈溢出漏洞利用为例。Aleph One论文《Smashing The Stack For Fun And Profit》,其中详细描述了Linux系统中栈的结构和如何利用基于栈的缓冲区溢出。在这篇具有划时代意义的论文中,Aleph One演示了如何向进程中植入一段用于获得Shell的代码,并在论文中称这段被植入进程的代码为“Shellcode”。

后来人们干脆统一用Shellcode这个专用术语来统称缓冲区溢出攻击中植入进程的代码。这段代码可以是出于恶作剧目的的弹出一个消息框,也可以是出于攻击目的的删改重要文件、窃取数据、上传木马病毒并运行,甚至是出于破坏目的的格式化硬盘等。请注意本节讨论的Shellcode是这种广义上的植入进程的代码,而不是狭义上的仅仅用来获得Shell的代码。

Shellcode往往要用汇编语言编写,并转换成二进制机器码,其内容和长度经常还会受到很多苛刻限制,故开发和调试的难度很高。

Shellcode与漏洞利用Exploit关系如下所述。

植入代码之前需要做大量的调试工作,例如,弄清楚程序有几个输入点,这些输入将最终会当作哪个函数的第几个参数读入到内存的哪一个区域,哪一个输入会造成栈溢出,在复制到栈区的时候对这些数据有没有额外的限制等。调试之后还要计算函数返回地址距离缓冲区的偏移并淹没之,选择指令的地址,最终制作出一个有攻击效果的“承载”着Shellcode的输入字符串。这个代码植入的过程就是漏洞利用,也就是Exploit。

2.定位Shellcode

当我们可以用越界的字符完全控制返回地址后,需要将返回地址改写成Shellcode在内存中的起始地址。在实际的漏洞利用过程中,由于动态链接库的装入和卸载等原因,Windows进程的函数栈帧很有可能会产生“移位”,即Shellcode在内存中的地址是会动态变化的,因此将返回地址简单地覆盖成一个定值的做法往往不能让Exploit奏效。

要想使Exploit不至于10次中只有两次能成功地运行Shellcode,我们必须想出一种方法能够在程序运行时动态定位栈中的Shellcode。

一般情况下,ESP寄存器中的地址总是指向系统中且不会被溢出的数据破坏。函数返回时,ESP所指的位置恰好是我们所淹没的返回地址的下一个位置。

由于ESP寄存器在函数返回后不被溢出数据干扰,且始终指向返回地址之后的位置,我们可以使用以下的这种定位Shellcode的方法来进行动态定位。

(1)用内存中任意一个jmp esp指令的地址覆盖函数返回地址,而不是原来用手工查出的Shellcode起始地址直接覆盖。

(2)函数返回后被重定向去执行内存中的这条jmp esp指令,而不是直接开始执行Shellcode。

(3)由于ESP在函数返回时仍指向栈区(函数返回地址之后),jmp esp指令被执行后,处理器会到栈区函数返回地址之后的地方取指令执行。

(4)重新布置Shellcode。在淹没函数返回地址后,继续淹没一片栈空间,将缓冲区前边一段地方用任意数据填充,把Shellcode恰好摆放在函数返回地址之后。这样,jmp esp指令执行过后会恰好跳进Shellcode。

这种移位Shellcode的方法使用进程空间里一条jmp esp指令作为“跳板”,不论栈帧怎么“移位”,都能精确地跳回栈区,从而适应程序运行中Shellcode内存地址的动态变化。

3.Shellcode编码

在很多漏洞利用场景中,Shellcode的内容将会受到限制,首先,所有的字符串函数都会对NULL字节进行限制。通常我们需要选择特殊的指令来避免在Shellcode中直接出现NULL字节或字。

其次,有些函数还会要求Shellcode必须为可见字符的ASCII值或Unicode值。在这种限制较多的情况下,如果仍然通过挑选指令的办法控制Shellcode的值的话,将会给开发带来很大困难。毕竟用汇编语言写程序就已经不那么容易了,如果在关心程序逻辑和流程的同时,还要分心去选择合适的指令将会让我这样不很聪明的程序员崩溃掉。

最后,除了以上提到的软件自身的限制之外,在进行网络攻击时,基于特征的IDS系统往往也会对常见的Shellcode进行拦截。

那么,怎样突破重重防护,把Shellcode从程序接口安全地送入堆栈呢?一个比较容易想到的办法就是给Shellcode“乔装打扮”,让其“蒙混过关”后再展开行动。

我们可以先专心完成Shellcode的逻辑,然后使用编码技术对Shellcode进行编码,使其内容达到限制的要求,最后再精心构造十几个字节的解码程序,放在Shellcode开始执行的地方。

当Exploit成功时,Shellcode顶端的解码程序首先运行,它会在内存中将真正的Shellcode还原成原来的样子,然后执行之。这种对Shellcode编码的方法和软件加壳的原理非常类似。Shellcode编码示意如图4.27所示。

图4.27 Shellcode编码示意图

下面将在上节所实现的通用Shellcode的基础上,演示一个最简单的Shellcode加壳过程,这包括:对原始Shellcode编码,开发解码器,将解码器和经过编码的Shellcode送入装载器运行调试。

最简单的编码过程莫过于异或运算了,因为对应的解码过程也同样最简单,我们可以编写程序对Shellcode的每个字节用特定的数据进行异或运算,使得整个Shellcode的内容达到要求。在编码时需要注意以下几点:

(1)用于异或的特定数据相当于加密算法的密钥,在选取时不可与Shellcode已有字节相同,否则编码后会产生NULL字节;

(2)可以选用多个密钥分别对Shellcode的不同区域进行编码,但会增加解码操作的复杂性;

(3)可以对Shellcode进行很多轮编码运算。