之前对某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中对应的位置应该是0x86,嗯?这是什么情况?回到IDA,发现在拷贝字符串之前调用过WideCharToMultiByte函数,将宽字节字符串转换成多字节字符串,是不是在经过这个函数的时候改变了0x86的值呢?
经过之前漏洞挖掘时的分析,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);
*本文作者:千里目安全实验室,转载请注明来自 FreeBuf.COM