第 5 章 漏洞利用 学习要求:掌握漏洞利用的核心思想、shellcode的概念;理解shellcode的 编写过程,掌握shellcode编码技术;掌握Windows安全防护技术相关的 ASLR 、GSStackProtection、DEP 、SafeSEH 、SEHOP等概念,了解其局限性;掌 握跳板攻击、堆喷洒、返回导向编程等漏洞利用技术,理解API函数自搜索技术 的原理。 课时:4课时。 分布:[漏洞利用概念—代码植入示例][shelcode编写—shelcode编码] [Windows安全防护—地址定位技术][API函数自搜索技术—绕过其他安全 防护]。 ..5.1 概念及示例 1.漏洞利用 5.1 1.概念 漏洞利用是指针对已有的漏洞,根据漏洞的类型和特点而采取相应的技术 方案,进行尝试性或实质性的攻击。exploit的意思是利用,它在黑客眼里就是 漏洞利用。有漏洞不一定就有exploit,但是有exploit就肯定有漏洞。 假设,刚刚发现了一个MiniShare最新版的0day漏洞。MiniShare是一款 文件共享软件,该0day漏洞是一个缓冲区溢出漏洞,这个漏洞影响之前的所有 版本。当用户向服务器发送的报文长度过大(超过堆栈边界)时就会触发该漏 洞。得到该漏洞后,可以做点什么呢? 善意点的,可以对同学或者朋友的计算 机搞恶作剧,让他的计算机弹出个对话框之类的。恶意的话,可以利用这个漏 洞向目标机器植入木马,窃取用户个人隐私等。那么,到底如何能达成这些目 的呢? 2.漏洞利用的手段 1996年,AlephOne在Underground 发表了著名论文SmashingtheStack 1 04 软件安全:漏洞利用及渗透测试 forFunandProfit,其中详细描述了Linux系统中栈的结构和如何利用基于栈的缓冲区 溢出。在这篇具有划时代意义的论文中,AlephOne演示了如何向进程中植入一段用于 获得shell(实际上,shell是一个命令解释器,它解释由用户输入的命令并且把它们送到内 核)的代码,并在论文中称这段被植入进程的获得shell的代码为shellcode。现在, shellcode已经表达的是广义上的植入进程的代码,而不是狭义上的仅仅用来获得shell 的代码。 漏洞利用的核心就是利用程序漏洞劫持进程的控制权,实现控制流劫持,以便执行植 入的shellcode或者达到其他的攻击目的。控制流劫持是一种危害性极大的攻击方式,攻 击者能够通过它来获取目标机器的控制权,甚至进行提权操作,对目标机器进行全面控 制。当攻击者掌握了被攻击程序的内存错误漏洞后,一般会考虑发起控制流劫持攻击。 早期的攻击通常采用代码植入的方式,通过上载一段代码,将控制转向这段代码执行。在 栈溢出漏洞利用过程中,攻击的目的是覆盖返回地址,以便劫持进程的控制权,让程序跳 转去执行shellcode。 3.漏洞利用的结构 要完成控制流劫持和达到不同攻击的目的,exploit最终是需要执行shellcode的,但 exploit中并不仅仅是shellcode。exploit要想达到攻击目标,需要做的工作更多,如对应 的触发漏洞、将控制权转移到shellcode的指令一般均不相同,而且这些语句通常独立于 shellcode的代码。这些能实现特定目标的exploit的有效载荷,称为payload。 一个经典的比喻,将漏洞利用的过程可以比作导弹发射的过程:exploit、payload和 shellcode分别是导弹发射装置、导弹和弹头。exploit是导弹发射装置,针对目标发射导 弹(payload);导弹到达目标之后,释放实际危害的弹头(类似shellcode)爆炸;导弹除了弹 头之外的其余部分用来实现对目标进行定位追踪、对弹头引爆等功能,在漏洞利用中,对 应payload的非shellcode部分。 总的来说,exploit是指利用漏洞进行攻击的动作;shellcode用来实现具体的功能; payload除了包含shellcode之外,还需要考虑如何触发漏洞并让系统或者程序执行 shellcode。 5.1.2 覆盖邻接变量示例 在4.1.2节,已经演示过如何利用栈溢出漏洞覆盖邻接变量、控制程序执行流程、实 现漏洞利用,完成软件破解。本节的实验,通过一个外部输入文件,再简单回顾一下利用 的过程。 假设已知一个系统的注册机验证过程的漏洞,程序举例如示例5-1。 【示例5-1】 #include #include #define REGCODE "12345678" 第5章 漏洞利用1 05 int verify (char * code) { int flag; char buffer[44]; flag=strcmp(REGCODE, code); strcpy(buffer, code); return flag; } 假设其主程序启动时要校验注册码: void main() { int vFlag=0; char regcode[1024]; FILE *fp; LoadLibrary("user32.dll"); if (!(fp=fopen("reg.txt","rw+"))) exit(0); fscanf(fp,"%s", regcode); vFlag=verify(regcode); if (vFlag) printf("wrong regcode!"); else printf("passed!"); fclose(fp); } verify函数的缓冲区为44字节,对应的栈帧状态如图5-1所示。 图5-1 程序对应的堆栈结构图 106软件安全:漏洞利用及渗透测试 利用这个漏洞可以破解该软件,让注册码无效。 注意:能成功破解有两个要素。第一是注册码字符串(前8字节)要小于REGCODE, 确保flag值为1;第二是通过结束符覆盖flag的高位1,得到使其值变为0的效果。 这是一种控制流劫持的漏洞利用手段。 只需要覆盖flag状态位使其变为0。设计要求:buffer(44字节)+字节(整数0)。 对应的实现:①在reg.txt中写入45字节(前8字节小于REGCODE),最后1字节为0; ②在reg.txt中写入44字节(前8字节小于REGCODE),fscanf读的时候自动添加结束 符0。 我们采用第一个方式,为了对reg.txt写入二进制数据,利用UltraEdit打开reg.txt, 并在该文件中写入123412341234123412341234123412341234123412341。需要将最后1 字节由ASCII1改为0x00。 单击工具栏的“切换至十六进制模式”,如图5-2所示,更改最后1字节为0即可。 图5-2切换至十六进制模式更改最后1字节 此时,运行所生成的exe程序执行成功。 5.3 代码植入示例 1. 通过覆盖返回地址让进程执行植入的shelcode是最传统的漏洞利用方式。 shelcode往往需要用汇编语言编写,并转换成二进制机器码,其内容和长度经常还 会受到很多苛刻限制,故开发和调试的难度很高。 植入代码之前需要做大量的调试工作,例如,弄清楚程序有几个输入点,这些输入将 最终会当作哪个函数的第几个参数读入内存的那一个区域,哪个输入会造成栈溢出,在复 制到栈区时对这些数据有没有额外的限制等。调试之后还要计算函数返回地址距离缓冲 区的偏移并覆盖,选择指令的地址,最终制作出一个有攻击效果的“承载”着shelcode的 输入字符串。 我们将以前面的程序为例,向其植入一段代码,使其达到可以覆盖返回地址,该返回 地址将执行一个MesageBox函数,弹出窗体。这个代码植入完成攻击的过程就是漏洞 利用,也就是exploit;含有shelcode的输入字符串就是payload;弹出对话框的机器码就 是shelcode。 【实验5-1】基于示例5-1,向其植入一段代码,弹出MesageBox窗体。在Windows XP环境下,基于VC6. 0进行实验。 为了能覆盖返回地址,需要在reg.xt中至少写入:bfer(44字节)+flag(4字节)+ tu 前EBP值(4字节),也就是53~56字节才是要覆盖的地址。 第5章漏洞利用107 让程序弹出一个消息框只需要调用Windows的API函数MessageBox。 我们将写出调用这个API的汇编代码,然后翻译成机器码,用十六进制编辑工具填 入reg.txt文件。 用汇编语言调用MessageBoxA需要3个步骤。 (1)装载动态链接库user32.dll。MessageBoxA是动态链接库user32.dll的导出函 数。虽然大多数有图形化操作界面的程序都已经装载了这个库,但是我们用来实验的 Console版并没有默认加载它。 (2)在汇编语言中调用这个函数需要获得函数的入口地址。 (3)在调用前需要向栈中按从右向左的顺序压入MessageBoxA的4个参数。 为了让植入的机器码更加简洁明了,在实验准备中构造漏洞程序时已经人工加载了 user32.dll库,所以第(1)步操作不用在汇编语言中考虑。 第一步:获得函数入口地址。 有多种方式可以获得函数入口地址,下面介绍两种。 基于工具来获得函数入口地址。MessageBoxA的入口地址可以通过user32.dll在系 统中加载的基址和MessageBoxA在库中的偏移地址相加得到。具体可以使用VC6.0自 带的小工具DependencyWalker获得这些信息。可以在VC6.0安装目录下的Tools下 找到它。 运行DependencyWalker后,随便拖曳一个有图形界面的PE文件进去,就可以看到 它所使用的库文件。在左栏中找到并选中use32. l 后,右栏中会列出这个库文件的所 rd 有导出函数及偏移地址,下栏中则列出了PE文件用到的所有的库的基址。 如图5-3所示,user32.dl 的基址为0x77D10000,MesageBoxA的偏移地址为 0x000407EA 。基址加上偏移地址就得到了MesageBoxA函数在内存中的入口地址: 0x77D507EA 。 图5- 3 运行DependencyWalker后,打开一个PE文件 1 08 软件安全:漏洞利用及渗透测试 注意:user32.dll的基址和其中导出函数的偏移地址与操作系统版本号、补丁版本号 等诸多因素相关,故用于实验的计算机上的函数入口地址很可能与这里不一致。一定注 意要在当前实验的计算机上重新计算函数入口地址,否则后面的函数调用会出错。 使用代码来获取相关函数地址。在C/C++ 语言中,GetProcAddress函数检索指定 的动态连接库(DynamicLinkedLibrary,DLL)中的输出库函数地址。如果函数调用成 功,返回值是DLL中的输出函数地址。函数原型如下: FARPROC GetProcAddress( HMODULE hModule, //DLL 模块句柄 LPCSTR lpProcName //函数名 ); 参数hModule包含此函数的DLL模块的句柄。LoadLibrary、AfxLoadLibrary或者 GetModuleHandle函数可以返回此句柄。参数lpProcName是包含函数名的以NULL 结尾的字符串,或者指定函数的序数值。如果此参数是一个序数值,它必须在一个字的低 字节,高字节必须为0。FARPROC 是一个4 字节指针,指向一个函数的内存地址, GetProcAddress的返回类型就是FARPROC。如果要存放这个地址,可以声明以一个 FARPROC变量来存放。 #include #include int main() { HINSTANCE LibHandle; FARPROC ProcAdd; LibHandle = LoadLibrary("user32"); //获取user32.dll 的地址 printf("user32 = 0x%x \n", LibHandle); //获取MessageBoxA 的地址 ProcAdd=(FARPROC)GetProcAddress(LibHandle,"MessageBoxA"); printf("MessageBoxA = 0x%x \n", ProcAdd); getchar(); return 0; } 运行上述代码后,同样可以得到MessageBoxA 函数在内存中的入口地 址:0x77D507EA。 第二步:编写函数调用汇编代码。 有了这个入口地址,就可以编写进行函数调用的汇编代码了。首先把字符串 westwest压入栈区,消息框的文本和标题都显示为westwest,只要重复压入指向这个字 符串的指针即可;第1个和第4个参数这里都将设置为NULL。 第5章漏洞利用109 写出的汇编指令所对应的机器码如表5-1所示。 表5- 1 汇编指令所对应的机器码 机器码(十六进制) 汇编指令注释 33DB xor ebx,ebx 将ebx的值设置为0 53 push ebx 将ebx的值入栈 6877657374 push 74736577 将字符串west入栈 6877657374 push 74736577 将字符串west入栈 8BC4 mov eax,esp 将栈顶指针存入eax(栈顶指针的值就是字符串的首 地址) 53 push ebx 入栈MesageBox的参数———类型 50 push eax 入栈MesageBox的参数———标题 50 push eax 入栈MesageBox的参数———消息 53 push ebx 入栈MesageBox的参数———句柄 B8EA07D577 moveax,0x77D507EA 调用MesageBoxA函数,注意,每个机器的该函数的 入口地址不同,按实际值写入FFD0 caleax 得到的shelcode:33DB53687765737468776573748BC453505053B8EA 07D577FFD0 。 第三步:注入shelcode代码。 将这段sheoe写入rett文件,且在返回地址处写bur的地址,如图5 所示。 lcdg.xfe4 图5- 4 seoe写入regtt hlcd.x Bufer的地址可以通过OlyDbg查看得到,也可以通过VC6. 到 , 即0012faf0(该地址跟随环境不同可能会发生变化)。 0转到反汇编方式得 攻击成功效果如图5-5所示。 注意:WindowsXP环境下静态API的地址是准确的,但是WindowsXP之后的操 作系统版本增加了ASLR(AddresSpaceLayoutRandomization)保护机制,地址就不准 确了,需要动态获取,利用地址定位技术或者通用型shelcode编写可以解决这个问题。 1 10 软件安全:漏洞利用及渗透测试 图5-5 攻击成功 .. 5.2 shellcode编写 漏洞利用中最关键的是shellcode的编写。 上面演示了一个通过汇编语言编写shellcode的例子,但是,直接用汇编语言编写很 麻烦,而且还需要查表来获得其机器码,很容易出错。此外,即使我们可以熟练地用汇编 语言编写shellcode代码,但还需要对一些特定字符进行转码。例如,对于strcpy等函数 造成的缓冲区溢出,会认为NULL是字符串的终结,所以shellcode中不能有NULL,如 果有需要则要进行变通或编码。 shellcode获取的工具。除了手动编写shellcode,可以利用Metasploit框架下的 msfvenom 生成shellcode,还有一些工具有助于获取shellcode,如cobaltstrike等。 本节重点介绍shellcode编写和代码提取的方法和思路。 5.2.1 提取shellcode代码 由于shellcode必须以机器码的形式存在,因此,如何得到机器码是一个关键技术。 一种简单编写并提取shellcode的方法如下。 1.用C语言书写要执行的shellcode 使用VC6.0编写程序,如示例5-2所示。 【示例5-2】 #include #include 第5章 漏洞利用1 11 void main() { MessageBox(NULL,NULL,NULL,0); return; } 2.换成对应的汇编代码 利用调试功能,找到其对应的汇编代码,如图5-6所示。 图5-6 得到汇编代码 直接得到的汇编语言通常需要进行再加工。对于push0而言,可以通过“xorebx,ebx” 之后执行pushebx来实现(push0的机器码会出现1字节的0,对于直接利用需要解决字节 为0的问题,因此转换为pushebx)。具体地,在工程中编写汇编语言如示例5-3所示。 【示例5-3】 #include #include void main(){ LoadLibrary("user32.dll"); //加载user32.dll _asm { xor ebx,ebx push ebx //push 0 push ebx push ebx push ebx mov eax, 77d507eah //77d507eah 是MessageBox 函数在系统中的地址 call eax 1 12 软件安全:漏洞利用及渗透测试 }r eturn; } push0不建议直接使用,因此采用了“xorebx,ebx”之后执行pushebx来代替。 3.根据汇编代码,找到对应地址中的机器码 同样,在第一行汇编代码处打断点,利用调试定位具体内存中的地址,如图5-7所示。 图5-7 定位具体内存中的地址 注意:实际调试时,MessageBox函数的入口地址需要根据自己的计算机重新计算。 这样,在Memory窗口就可以找到对应的机器码:33DB53535353B8EA04D5 77FFD0。 接下来就可以利用这个shellcode来实现漏洞利用了,一个VC6.0测试程序如示 例5-4所示。 【示例5-4】 #include #include char ourshellcode[]="\x33\xDB\x53\x53\x53\x53\xB8\xEA\x07\xD5\x77\xFF\xD0"; void main() { LoadLibrary("user32.dll"); int *ret;