前置知识 : 汇编关键词 : 漏洞 Shellcode 堆栈 菜鸟版 Exploit 编写指南之五十六 : 2009 年第 10 期 突破 Windows 2003 基于硬件的 DEP 文 / david kennedy 译 / riusksk 创建数据执行保护 DEP(Data Exectution Protection) 的初衷是为了防止不可运行 的内存区域运行代码 在写本文之前, 我仔细阅读了 Uninformed 上 skape 与 Skywing 合写的文章 Bypassing Windows Hardware-Enforced DEP, 这是篇不错的文章, 值得一看 skape 与 Skywing 均是具备精明头脑的人, 是公认的 汇编超人 背景 开始之前, 我们先简单讲解堆栈溢出的基础知识 在当今现实世界中, 这类溢出已经很 普遍了, 是很容易发生的一类安全漏洞 只要程序员在编写程序时, 未对分配一定大小的特 定区域进行合适的边界检测即可发生此类情况 下面就以 SLMAIL 栈溢出为例说明 Offensive Security 安全组织的 Mati Aharoni 在 2004 年发现了 SLMAIL 这个漏洞, 该漏 洞的 exploit 正是利用了 SlMAIL POP3 SERVER(port 110) 中 PASS 域未进行有效的边 界检测而进行攻击的 关于该漏洞的分析可参 见 http://www.milw0rm.com/exploits/638 如果定位一下实际攻击发生的位置, 就会发现它是发生在 PASS+buffer 处,buffer 中包含 4654 个 A (\x41 触发溢出 ) shellcode 地址, 一些 nop 以及我们的 shellcode 为了实现溢出攻击, 只有通过覆盖 EIP 中指向的特定内存地址才可触发, 而 EIP 正是指向 系统即将运行的下一条指令的地址 如果我们能够控制 EIP, 那么就可以让系统执行到我们的 shellcode 地址 这类典型的 跳转地址通常为 call esp 或者 jmp esp, 并让 ESP 指向堆栈中特定的地址 ( 例如我们的 shellcode 地址 ) 通过分析这份 exploit 代码, 可以发现 4654 个 A 被发送到服务器 后, 接下来的 0x78396ddf 正是用来覆盖 EIP 并正确跳转到 shellcode 的内存地址 NOP 在汇编中用 \x90 来表示, 相当于 无操作 它并没有执行任何操作, 只是 继续执行下一条代码, 直到有效指令 当你无法 100% 确定它将运行到我们预想的地址时,
可以使其 滑行 一段距离, 直至 shellcode 与此同时, 它还可以删除一些被合法函数遗弃的垃圾字符 nop 被执行后, 将会运行包含恶意代码的 shellcode, 例如 reverse shell bind shell useradd 等 栈溢出的整个思路是 : 覆盖 EIP, 跳转到 shellcode(jmp esp), 执行 shellcode 如果你看一下这份 exploit 的日期以及测试平台, 就会发现 exploit 是在 Windows 2000 SP4 平台上测试的 如果 exploit 是运行在 Windows XP SP2 Windows 2003 SP1 或者 Windows 2003 SP2 上又会如何呢? 本文这里只谈论 Windows 2003 SP2, 因为每一操作系统在存在不同点的同时, 亦会 有相对的共同之处 在 Windows XP SP2 与 Windows 2003 SP1 上绕过 DEP 比 Windows 2003 SP2 更为容易, 因为 Windows 2003 SP2 内存中存在两项检测机制, 而其它系统 只有一项 ( 比较 AL 与 EBP 和 EBP 与 ESI) 实际测试 打开调试器 Immunity Debugger, 先从 milw0rm 上下载 exploit, 并通过调试器来运行它, 图 1 是在 *unix box 上运行 exploit 的截图 图 1 在调试器中, 可以看到被控制的堆栈中第一条指令发生了访问违例异常, 如图 2 所示 图 2
进一步分析, 右击 我的电脑 -> 属性 -> 高级, 打开 高级 后 ( 译注 : 在中文 Windows XP SP2 上, 是选择性能中的 设置 后才能看到 数据执行保护 ), 选择 数据执行保护, 接下来我们可以看到 为除下列选定程序之外的所有程序和服务启用 DEP(U): 的选项 为了 exploit 系统, 以此来提升访问权限, 就要克服上面的问题 既然已经知道了开启 DEP, 我们就需要将其禁止掉, 以便在被控制的栈中正确地执行 我们的 shellcode 幸运地是, 这里就有一种方法可以做到 在这份特殊的 exploit 当中, 我所使用的溢出方式尤为简单, 但在实际当中会遇到很多难以想像的困难 这里重述一下 Skape 与 Skywing 的一些观点 : 为了绕过 DEP 就必须调用函数 ZwSetInformationProcess( 在 LdrpCheckNXCompatibility 实例中 ) 当 ZwSetInformationProcess 函数被调用时, 通过相关设置来禁止 DEP, 以使程序 跳转到我们所控制的堆栈中 在深入分析之前, 我们先来看看这个函数的实际情况 我们将 会看到 NTDLL 中的地址 0x7C83F517, 正是 ZwSetInformationProcess 的起始点, 也 是禁止 DEP 的入口, 如图 3 所示. 图 3 通过上面的调用指令可知, 它先执行 MOV DWORT PTR SS:[EBP-4],2, 欲向指定的内存地址中写入立即数 2 如果寄存器被写入非法数据, 就会导致写入失败, 甚至产生之前我们所看到的异常事件 接下来, 将立即数 4 压入栈中, 将 EAX 入栈, 将 22 压栈, 将 -1 入栈, 最后调用函数 ZwSetInformationProcess 跟进函数之后, 来到如图 4 所示 的地方 图 4 上面的指令对 ESI 进行了一些操作, 因此 ESI 必须为一可写地址, 否则将会触发异常 现在我们需要让 EBP 与 ESI 指向可写的内存地址, 这样后续的指令才能得以运行 从 vanilla
SLMail exploit 代码来看, 它并未绕过 DEP, 执行之后将无法完全地绕过 NX 这里需要注意一下 LEAVE 这条指令, 它至少改变了 EBP 和 ESP 的值 现在的问题就是 EBP 是否指向了我们的 HEAP, 因此若想执行恶意代码, 就需要使其指向可控制栈的附近内存区域 图 5 是溢出时寄存器的情况 图中寄存器 ECX 指向了有利于我们的 HEAP, 因为它是可写的 如果想摧毁它, 我们仅需使用 heap spray 技术即可实现 不过我们需要来点更具创造性 的操作 图 5 其实更为理想的寄存器可以使用 ESP, 也可能是 ECX ESP 指针更为完美地接近我们 的 shellcode, 以及 ECX 指向的内存地址 注意, 为了禁止 NX,EBP 与 ESI 必须指向可 写的内存地址 我们先处理 EBP, 在 shell32 中找到指令 push esp pop ebp retn0x4, 内存地址为 0x7C93C899, 如图 6 所示 先将 ESP 的值压入栈中, 如图 7 所示 ESP 的 值为 01F3A154, 查看该地址的内容, 如图 8 所示 栈中显示为 01F3A158, 现在我们需 要将该值 POP 出栈给 EBP, 如图 9 所示 图 6 图 7 图 9
图 9 现在 EBP 指向了原 ESP 地址, 即 shellcode 邻近地址 接一来, 我们需要使 ESI 指向可执行区域 我们可以简单地使用 push esp push esp pop ebp pop esi,retn 或 者其它类似功能的指令来实现, 只不过需要在内存中搜索 这里我并没有找到合适的地址, 打算来点有创意的 我们需要使 ESI 指向可写的内存区域, 而 ESP 或者 ECX 在地址上亦须为有效 接着 看一下接下来的指令情况 注意, 这里很容易混淆 在地址 0x7C806B03 的指令为 pop ebx,retn, 这将使已经存在栈中的内存地址 pop 给 EBX 寄存器, 这样我们就可以注入 任意欲被执行的地址了 请看下面的代码 : # POP EBX,RETN 0x7C806B03 @NTDLL? disablenx+='\x03\x6b\x80\x7c' # 0x7C8043A3 will be EBX when POP # 为了绕过 NX 需要 ESI 为可写 #POP EDI,POP ESI, RETN 0x7c8043a3 @NTDLL disablenx+='\xa3\x43\x80\x7c' 当调用 NTDLL 中地址 0x7C806B03 处的指令时, 这将会 pop 0x7c8043a3 给 EBX, 如图 10 所示 但这仍然未起到什么效果, 因为 ESI 一直为错误地址 00000000, 接下来 被执行的指令为 :
图 10 #PUSH ECX,CALL EBX 0x7c934f57 @SHELL32 disablenx+='\x57\x4f\x93\x7c' #This will go to EBX(0x7c8043a3) 以上指令将 ECX 压栈, 并调用 EBX 记住, 我们可以在上一步之前将 ECX 指向内存中的其它地址 当 ECX 被压入栈中后, 将会调用 EBX, 即 pop edi,pop esi,retn, 这之所以重要是因为它将从栈中弹出一个数值给 EDI 我们无需关注 EDI, 但需要移除栈中 的一个地址, 以便更改弹给 ESI 的值, 第二条指令 POP ESI 将会把 EBX 的值 POP 给 ESI 执行后,EBP 和 ESI 就指向了可写的内存地址, 如图 11 所示 图 11 由上可知 EBP 正是原始 ESP 的值 ( 入口 ), 其中 ESI 指向的正是 ECX 的内存地址 接下来我们调用 ZwSetInformationProcess 去禁止 DEP, 这个地址位于 0x7c83f517, 如图 12 所示 图 12 我们先查看 EBP 地址是否为可写, 若是, 则设置合适的参数供 ZwSetInformationProcess 调用 一旦执行到这里, 将会起到一定作用, 接着检查一下 ESI, 如图 13 所示
图 13 ESI 为可写地址,pop esi 之后,EBP 的值被赋予 ESP 并返回 我们只需找出 shellcode 被加载的地址, 然后设置一个地址去执行 JMP ESP 几分钟后, 结果如图 14 所示 注意, EIP 指向 FFFFFFFF 这是一个无效地址, 查看堆栈情况, 如图 15 所示 图 14 图 15 很明显, 第五个地址正指向了我们所控制的堆栈 由于 ZwSetInformaationProcess 执行一些 push pop 及其它操作, 而遗留下一些垃圾在栈中, 以致无法进入 shellcode 既然无法控制这些地址, 那么我们可以控制它前面的地址 如果我们返回到一个被 忽视 的前方地址, 那么它就可以带领我们到达正确的内存地址, 就可以进入栈中执行 shellcode 回头查看之前的代码 : #0x7c93c899 @SHELL32 PUSH ESP,POP EBP,RETN0x4 disablenx='\x99\xc8\x93\xx7c' #Get EBP close to our controlled stack #POP EBX,RETN 0x7c806b03 @NTDLL disablenx+='\x03\x6b\x80\x7c' #0x7c8043a3 will be EBX when POP
第一条指令中的 RETN0x4 执行后, 将会返回到下一条指令中的 POP EBX,RETN, 但省略了后面的 4 个字符, 我们可以用 \xff\xff\xff\xff 来填充, 以替换即将修改寄存器值的地址 将指令合在一起即为 : disablenx='\x99\xc8\x93\xx7c' #Get EBP close to our controlled stack disablenx+='\x03\x6b\x80\x7c' # 0x7c8043a3 will be EBX when POP disablen+='\xff\xff\xff\xff' #JUNK 系统将先进入地址 0x7c93c899, 然后 0x7c8043a3, 接着忽略 FFFFFFFF, 并继续 执行下去 这里是否可能在 DEP 被禁止后, 我们就返回到 FFFFFFFF 去修改 ESP 并从栈中 POP 一些数据, 从而进入我们的 shellcode 呢? 请看下面的操作 当执行到下面的指令 : #PUSH ECX,CALL EBX 0x7c934f57 @SHELL32 disablenx+='\x57\x4f\x93\x7c' #This will go to EBX(0x7c8043a3) 这条指令将 ECX 压入栈中, 调用 EBX, 接着 POP 给 ESI 一个可写的内存地址 之后, 将会直接调用 ZwSetInformationProcess 函数去禁止 DEP 但我们并未跳转到 ZwSetInformationProcess, 而是执行 RETN0x10, 接着再进入 ZwSetInformationProcess 看下面的指令 : #RETN0x10 @SHELL32 #disablenx+='\x95\x74\x8f\x7c' #Stack Alignment 这将先执行 RETN10 指令 当我们一调用 ZwSetInformationProcess, 它就开始变 戏法了, 先去检测 EBP, 再检测 ESI, 然后 LEAVE, 最后 RETN 0x4 现在在我们执行过
的原始指令之后还有其它一些指令, 就是我们的 \xff\xff\xff\xff 我们可以用 NTDLL 中的内存地址 0x7c85e6f7 替换 \xff\xff\xff\xff, 地址 0x7c85e6f7 的内容如图 16 所示 图 16 给 ESP 加上 20,POP 两个寄存器, 然后 RETN4, 这将直接跳转到我们控制的堆栈中, 即 shellcode 所在的地址 最后一个问题, 相对简单, 也就是精确地查找出可实现 JMP/CALL ESP 的内存地址 这个可以使用 Metasploit, 进入 tools 部分, 然后利用 pattern_create 和 pattern_offset 找出正确的地址 ( 译注 : 除了运用以上工具之外, 还可以利用 OD 来查 找指令 JMP/CALL ESP 的地址, 之前亦曾有人开发过一个插件 OllyUni, 用它可以轻易的 获得整个进程空间中的各类跳转地址, 当然读者也可自己写个 OD 脚本来实现以上功能 ) 使用它即可找到 JMP ESP 的地址, 如图 17 所示 一旦跳到这里, 就可以看到如图 18 所 示的情况 图 17
图 18 现在就进入了我们想要到达的地址, 来到 NOP slide, 最后到达 shellcode 我修改了 SLMAIL 中的 shellcode, 使其可以添加一个用户名 rel1k 另外发现 0xff 0x00 和 0x0a 均为受限字符 让我们来看看它执行前后的情况, 如图 19 所示 注意用户账号, 接着发送 payload, 如图 20 所示 payload 发送后, 重新查看用户账号情况, 如图 21 所示一本地管理员账号 rel1k 被成功添加了
结论 图 19 图 20 图 21 本文讲述的是一个绕过 DEP 的 exploit 入门例子 我想指出的是, 无论如何这都不能 说是微软的问题, 因为他们选择了允许向后兼容 ( 正如 Skape 和 skywing 的文章所提到的一样 ) 足够有趣的是, 我并没有真正地遇到过上面的情况, 大部分绕过 NX 的 exploit 都已经对 ESI 和 EBP 稍作修改了, 而各寄存器之间有所不同的是它们并未指向有用的地址 ECX 和 ESP 是否为可写的内存地址应该是很普遍的问题, 应该稍作修改以便配合其它 exploits 的工作