之前对某 ActiveX 控件进行漏洞挖掘的时候发现了一个栈溢出漏洞, 最近一时兴起写下了这篇针对该漏洞的从挖掘到成功利用的分享文章. 如题, 本文将从漏洞挖掘和漏洞利用两个方面进行分享. 话不多说, 直接进入主题.
漏洞挖掘部分
拿到这个控件之后, 先对该控件的属性和包含的函数等信息进行大致了解, 这里推荐一个工具 --COMRaider, 它可以识别出控件很多信息. 这里就不介绍使用方法了, 下面给出一个识别该控件的截图.
该工具还提供 fuzz 功能, 在后续进行逆向分析的同时可以跑下 fuzz, 说不定有惊喜(本次 fuzz 并没有惊喜出现).
了解过控件之后, 就可以进行静态分析了, 过程中使用了 IDA 和 com.plw 插件.
使用 IDA 加载控件文件, 按 Ctrl+F11 调用插件识别控件中的函数:
这些识别出来的函数都是可以在浏览器中直接调用的, 所以这些函数的参数都是攻击者可以控制的, 需要对这些参数逐一进行追踪和分析. 当分析到 OpenDevice 这个函数时发现了问题. 以下将分享该函数详细分析过程.
使用 F5 查看该函数的伪 C 代码:
追踪参数 lFlags 的处理过程, 在第 41 行发现该参数进行了与运算后并将结果给到 v6:
继续追踪 v6, 发现 v6 都用在 if 语句的判断中, 可以认为 lFlags 参数是用作分支判断的. 其中有一个分支有些奇怪, 在第 42 行:
因为 v6 是由 lFlags 和 0xF00 进行与操作得到的, 所以 v6 不可能取值到 3. 不知道这是程序的 bug 还是啥. 所以之后可以跳过对该分支的分析.
追踪参数 szAppID 的处理过程, 对该参数的处理在 v6=3 的分支中有一部分, 但可以不进行分析, 因为根本执行不到. 剩下的只有如下图所示的部分:
该部分代码在 v6=256 或 768 是可以执行到, 在第 95 行发现调用了 WideCharToMultiByte 函数, 将 szAppID 参数地址中的宽字节字符串转换成多字节字符串后并放入 & v21 地址中, 函数最多可以向 & v21 地址空间写入 v14 个字符, 而 v14 是 szAppID 地址下字符串长度的两倍加二. 这里需要判断 & v21 地址空间实际大小. 查看伪 C 代码最上部分的参数说明:
发现 v21 这个变量后面的地址是 sp+0h, 即地址为栈顶. 而变量 String1 对应的地址为 sp+Ch, 并且从伪 C 代码中可以看出 String1 并不是 v21 的一部分, 是可以独立使用的变量, 如果这么分析 & v21 对应的地址空间大小只有 Ch, 是可以溢出的, 这时就要通过汇编代码来分析确认栈顶地址对应的空间大小是不是真的像之前分析的那样:
图中框 1 将实际接收地址 (存在寄存器 edi 中) 压入栈中, 由框 2 可知 edi 的数据来自 esp, 在这之前调用了框 3 中的函数, 跟进函数分析 esp 的处理:
分析该函数的作用是根据 eax 中的数据在栈顶之外再开辟对应大小的空间, 并将最终的栈顶地址返回到 esp, 再回到原函数, 分析 eax 中数据的大小:
从上图框中的汇编代码可知, eax 中的数据是大于等于 lpString 长度乘二加二, 查看 lpString 的偏移量:
可知 lpString 就是 szAppID, 所以使用 WideCharToMultiByte 函数不会造成溢出.
然后继续追踪转为多字节字符串之后的地址空间 & v21, 在第 93 行发现 v12=&v21:
继续追踪 v12, 发现在第 102 行将 v12 使用到了字符串拷贝函数中:
该函数会将 v12 地址空间中的字符串全部拷贝到 & String1 中, 通过上述分析发现 v12 是攻击者可控的, 而且没有进行长度限制, 这里需要判断 & String1 地址空间的大小. 直接查看该处对应的汇编代码:
发现拷贝的目的地址空间为 ebp+String1, 查看对应的栈空间大小:
由此可知 ebp+String1 对应的地址空间大小为 74h, 是有限的, 所以很有可能存在栈溢出漏洞. 为了验证以上的分析, 编写一个调用该控件函数的页面, 触发可能存在漏洞的代码部分需要满足对应分支的条件, 以上分析可知条件为 lFlags&0xF00 为 768 或 256, 为了溢出覆盖到返回地址, 所以 szAppID 大于等于 78h.
编写一个测试页面, 内容如下:
使用 ollydbg 附加到要加载该页面的 ie 浏览器上, 在可执行模块中没有找到该控件的文件, 为了向存在漏洞的部分下断点, 我在 ollydbg 中设置了暂停于新模块:
设置完成后, 让 ie 浏览器加载页面, 当暂停的时候在可执行模块中查找控件对应的文件:
在 ida 中获取对应代码段的相对地址, 根据相对地址在 ollydbg 相对位置设置断点, 此处我在调用拷贝函数前和函数 RETN 命令前分别设置了断点:
继续执行, 直到暂停于调用字符串拷贝函数前, 单步执行到调用完字符串拷贝函数后, 查看 EBP 下的内容:
发现和我之前分析的一样, 正好溢出到函数返回地址处. 让程序继续执行, 发现浏览器已经崩溃:
至此可以确定该处确实存在栈溢出.
漏洞利用部分
该部分将分享如何利用上述发现的栈溢出漏洞弹出计算器. 因为 Win7 下开启了 ASLR 和 DEP 两个保护, 而 ActiveX 控件又不具有交互性, 所以在 Win7 下利用不现实, 这里以 WinXPENSP3 为靶机进行漏洞利用.
该栈溢出漏洞利用的大体思路是覆盖函数返回地址作为跳板, 然后在目标地址处填入 shellcode 进行执行. 思路很简单, 但中间还是有个坑比较尴尬.
首先我先找一个跳板, 使用 ollydbg 附加到要加载网页的 ie 浏览器上, 然后在可执行模块中找一个存在 jmp esp 指令的系统 dll 文件, 这里凑巧找到的是 advapi32.dll:
可见 jmp esp 的地址为 \ x77DEF049, 由漏洞挖掘部分的分析可知, 字符串的第 121~124 个字符会溢出覆盖掉返回地址, 所以这里构造一个 payload, 前 120 个字符统一用 A 填充, 之后为 "\x49\xF0\xDE\x77", 最后再填充字符 B 至长度为 150, 代码如下图:
和漏洞挖掘时一样, 分别在函数调用字符串拷贝函数之前和函数返回之前添加断点:
使用 ie 浏览器加载页面, 单步执行到字符串拷贝函数之后, 查看栈底中的数据:
发现栈中的数据和预期一样, 继续执行到函数返回前, 然后单步执行到 retn 之后, 发现成功执行到 advapi32 中的 jmp esp 处:
再次单步执行, 查看 EIP 所指向的地址在溢出字符串中的位置:
可以看出, 在返回地址之后的第 13 个字符开始为 jmp esp 跳转后执行的代码段. 所以编写 payload 字符串为 120 个字符 A, 之后为 "\x49\xF0\xDE\x77", 然后为 12 个字符 B, 最后为 4 个字符 C:
使用 ie 浏览器加载页面, 在 ollydbg 中查看 jmp esp 后的指令是否为 4 个 C:
发现和预期的结果一样, 这样就可以将 4 个字符 C 的位置改为 shellcode 即可.
这里使用一个在 exploit-db 中找的弹计算器的 shellcode 进行尝试, 页面代码如下:
加载页面, 让程序暂停在调用字符串拷贝函数之后, 比较内存中的内容和 shellcode 是否一致:
对比发现, 上图红框中的 0x3F 在 shellcode 中对应的位置应该是 0×86, 嗯? 这是什么情况? 回到 IDA, 发现在拷贝字符串之前调用过 WideCharToMultiByte 函数, 将宽字节字符串转换成多字节字符串, 是不是在经过这个函数的时候改变了 0×86 的值呢?
经过之前漏洞挖掘时的分析, WideCharToMultiByte 函数将转换后的字符串存在了栈顶, 在 ollydbg 查看下栈顶转换后的字符串内容:
原因如之前推测的那样.
原因找到之后, 就要想如何解决, 我第一想法是通过 MultiByteToWideChar 函数将 shellcode 从多字节字符串转换成宽字节字符串之后放到 payload 中, 让程序运行 WideCharToMultiByte 解码就能变回原来的多字节字符串了, 直接上代码:
代码中为了方便对比, 将转为宽字节的字符串和转回多字节的字符串都写入文件中, 结果如下图:
很明显地可以发现转回后的多字节的字符串与转码前的不一样...
检查完代码发现没有什么问题之后, 只能猜测两次转换是无法完全得到起初的字符串的. 只能放弃这种思路了.
经过多次尝试后, 发现 \ x80 之前的字符宽字节字符串和多字节字符串相互转换是不会改变的. 这就想到是不是使用只由 \ x80 之前的字符组成的 shellcode 就可以避免这个问题了.
为了生成这样的 shellcode, 我使用了 kali 里面 msfvenom 命令来生成 shellcode:
将生成的 shellcode 添加到页面中:
因为 shellcode 有点长, 就不进行在内存中一一对比了, 直接在 ie 浏览器中加载页面, 看是否会弹出计算器:
成功弹出计算器.
到此该栈溢出漏洞的利用就结束了.
后记
针对上面出现的漏洞, 在开发中我们要如何避免出现这种漏洞呢?
答: 使用安全的字符串拷贝方法
1, 使用 char * strncpy ( char * destination, const char * source, size_t num );
将 destination 地址空间的大小减一 (为了存放'\0') 的值传给第三个参数 num, 这样即使 source 中的字符串长度超过了 destination 的容量, 也只会拷贝 num 大小的字符串.
例:
- char buf[MAX];
- strncpy(buf, src, MAX-1);
注: 为了避免 source 的长度大于等于 num 时导致 destination 最后一个字符不是'\0'的情况, 需要在拷贝之前对 destination 地址空间的内容初始话为'\0', 或者在拷贝之后给 destination 最后一个字符赋值为'\0'.
2, 使用 int snprintf( char *buffer, int buff_size, const char *format, ... );
使用第二个参数 buff_size 来标识 buffer 的容量, 这样打印到 buffer 里面的字符串不会超过 buff_size 的限制, 并且会在字符串的最后或 buffer 的最后一位赋值为'\0'作为字符串的结束.
例:
- char buf[MAX];
- snprintf(buf, sizeof(buf), "%s", src);
来源: http://www.tuicool.com/articles/bmYbM3m