3.1 shellcode概述
3.1.1 shellcode与exploit
1996年,Aleph One在Underground发表了著名论文Smashing the Stack for Fun and Profit,其中详细描述了Linux系统中栈的结构和如何利用基于栈的缓冲区溢出。在这篇具有划时代意义的论文中,Aleph One演示了如何向进程中植入一段用于获得shell的代码,并在论文中称这段被植入进程的代码为“shellcode”。
后来人们干脆统一用shellcode这个专用术语来通称缓冲区溢出攻击中植入进程的代码。这段代码可以是出于恶作剧目的的弹出一个消息框,也可以是出于攻击目的的删改重要文件、窃取数据、上传木马病毒并运行,甚至是出于破坏目的的格式化硬盘等。请注意本章讨论的shellcode是这种广义上的植入进程的代码,而不是狭义上的仅仅用来获得shell的代码。
shellcode往往需要用汇编语言编写,并转换成二进制机器码,其内容和长度经常还会受到很多苛刻限制,故开发和调试的难度很高。
在技术文献中,我们还会经常看到另一个术语——exploit。
植入代码之前需要做大量的调试工作,例如,弄清楚程序有几个输入点,这些输入将最终会当作哪个函数的第几个参数读入到内存的哪一个区域,哪一个输入会造成栈溢出,在复制到栈区的时候对这些数据有没有额外的限制等。调试之后还要计算函数返回地址距离缓冲区的偏移并淹没之,选择指令的地址,最终制作出一个有攻击效果的“承载”着shellcode的输入字符串。这个代码植入的过程就是漏洞利用,也就是exploit。
exploit一般以一段代码的形式出现,用于生成攻击性的网络数据包或者其他形式的攻击性输入。expliot的核心是淹没返回地址,劫持进程的控制权,之后跳转去执行shellcode。与shellcode具有一定的通用性不同,exploit往往是针对特定漏洞而言的。
其实,漏洞利用的过程就好像一枚导弹飞向目标的过程。导弹的设计者关注的是怎样计算飞行路线,锁定目标,最终把弹头精确地运载到目的地并引爆,而并不关心所承载的弹头到底是用来在地上砸一个坑的铅球,还是用来毁灭一个国家的核弹头;这就如同exploit关心的是怎样淹没返回地址,获得进程控制权,把EIP传递给shellcode让其得到执行并发挥作用,而不关心shellcode到底是弹出一个消息框的恶作剧,还是用于格式化对方硬盘的穷凶极恶的代码,如图3.1.1所示。
图3.1.1 缓冲区溢出过程中的功能模块划分
随着现代化软件开发技术的发展,模块化、封装、代码重用等思想在漏洞利用技术中也得以体现。试想如果仿照武器的设计思想,分开设计导弹和弹头,将各自的技术细节封装起来,使用标准化的接口,漏洞利用的过程是不是会更容易些呢?其实在第4章中将介绍到的通用漏洞测试平台Metasploit就是利用了这种观点。Metasploit通过规范化exploit和shellcode之间的接口把漏洞利用的过程封装成易用的模块,大大减少了expliot开发过程中的重复工作,深刻体现了代码重用和模块化、结构化的思想。在这个平台中:
(1)所有的exploit都使用漏洞名称来命名,里边包含有这个漏洞的函数返回地址,所使用的跳转指令地址等关键信息。
(2)将常用的shellcode(例如,用于绑定端口反向连接、执行任意命令等)封装成一个个通用的模块,可以轻易地与任意漏洞的exploit进行组合。
题外话:与导弹的比喻不谋而合,在Metasploit中存在漏洞的受害主机会被当作一个叫“target”的选项进行配置,而shellcode同样也有一个更加形象的名字:payload。不知道在Metasploit以后的版本中会不会把exploit配置改成missile。
3.1.2 shellcode需要解决的问题
2.4节中的代码植入过程是一个简化到了极点的实验。其实,这个实验中还有一些问题需要进一步完善。
在2.4节的代码植入实验中,我们直接用OllyDbg查出了栈中shellcode的起始地址。而在实际调试漏洞时,尤其是在调试IE中的漏洞时,我们经常会发现有缺陷的函数位于某个动态链接库中,且在程序运行过程中被动态装载。这时的栈中情况将会是动态变化着的,也就是说,这次从调试器中直接抄出来的shellcode起始地址下次就变了。所以,要编写出比较通用的shellcode就必须找到一种途径让程序能够自动定位到shellcode的起始地址。有关利用跳转指令定位shellcode的讨论将在3.2节中进行。
缓冲区中包括shellcode、函数返回地址,还有一些用于填充的数据。3.3节中将介绍怎样组织缓冲区内的这些内容。
不同的机器、不同的操作系统中同一个API函数的入口地址往往会有差异。还记得2.4节中我们是怎样通过Depends获得MessageBoxA函数入口地址的吗?直接使用手工查出的API地址的shellcode很可能在调试通过后换一台计算机就会因为函数地址不同而出错。为此,我们必须让shellcode自己在运行时动态地获得当前系统的API地址。3.4节会带领您综合跳转地址、shellcode的分布、自动获得API等技术,把2.4节中那段简陋的shellcode改造成比较通用的版本。在这节的实验中还将穿插介绍shellcode的调试方法,怎样从汇编代码中提取机器代码等实际操作中将遇到的问题。
3.5节中将着重介绍如何通过使用对shellcode编码解码的方法,绕过软件对缓冲区的限制及IDS等的检查。
3.6节重点介绍了在整个缓冲区空间有限的情况下,怎样使代码更加精简干练,从而尽量缩短shellcode的尺寸,开发出短小精悍的shellcode。这节中的实验部分最终只用了191个字节的机器码就实现了一个把命令行窗口绑定到特定端口的bindshell。
本章前5节的知识是在Windows平台下开发shellcode的核心知识,也是后续学习的基础。当然,如果您对shellcode开发技术本身很感兴趣,并且有丰富的汇编语言编程经验,相信3.6节中讨论的编程技术对您开发更高级的shellcode一定会有所帮助。