WINDOWS平台下的栈溢出攻击从0到1(篇幅略长但非常值得一看)
Posted i春秋
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了WINDOWS平台下的栈溢出攻击从0到1(篇幅略长但非常值得一看)相关的知识,希望对你有一定的参考价值。
Stack Buffer Overflow On Windows Part 1
1.介绍
本篇文章旨在带领学习二进制安全的新手朋友在Stack Buffer Overflow在Windows上的技术实现从0到1的这个过程。
笔者也希望能够通过这些技术分享帮助更多的朋友走入到二进制安全的领域中。
2.文章拓扑
由于本篇文章的篇幅略长,所以笔者在这里放一个文章的拓扑,让大家能够在开始阅读文章之前对整个文章的体系架构有一个宏观的了解。
.\\01.介绍
.\\02.文章拓扑
.\\03.从栈开始
.\\04.ESP、EBP寄存器与栈
.\\05.函数调用与返回
.\\06.开始溢出
.\\07.深入分析
.\\08.如何利用
.\\09.获取API地址
.\\10.编写shellcode
.\\11.利用失败
.\\12.获取JMP ESP地址
.\\13.Exploit It!
.\\14.结语
3.从栈开始
我们在文章的最开头先来引入一些基本的概念,也是后面成功理解这种漏洞的一些必要概念。
首先,就是这个“栈”。栈其实是一种数据结构,它遵从先进后出的原则。这个先进后出的意思也很简单,就是说先存储进去的数据,会被放在最里边,而后面存入的,则依次向外,所以最先进去的,最后才能出来。
我们还是给一张图,帮助大家理解。但是呢,这个图是笔者自己用微软的画图软件画的,希望大家不要吐槽,毕竟能吐槽的地方太多了。
通过这张图,相信大家也能够更加容易的理解栈这个东西了。
4.ESP、EBP寄存器与栈
接下来,我们来聊一聊ESP、EBP寄存器和栈之间的一些关系。我们这次通过实验来学习这个知识,同时通过这个实验也可以加强大家对栈的理解。
我们使用这样一个程序来观察栈与ESP和EBP寄存器的关系:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
#include “stdAfx.h” int main() { _asm{ mov eax,0x41414141 mov ebx,0x61616161 push eax push ebx pop eax pop ebx } return 0; } |
zusheng:解释一下上面这段代码的意思,_asm很简单就是使用汇编代码。mov eax,0x41414141 这段代码是将十六进制数据0x41414141赋值给寄存器eax。mov ebx,0x61616161 这段代码是将十六进制数据0x61616161赋值给寄存器ebx。push eax 把eax寄存器中的内容入栈。push ebx 把ebx寄存器中的内容入栈。pop eax 出栈操作,从栈中取出一个值赋给寄存器eax。PS:上面文章介绍到了出栈只有一个出口,只能从下往上一个个有序的出去,而入栈操作也只能从上而下一个个进来。所以当前出站的数据就是在栈顶的数据,也就是前面十六进制数据0x61616161并且赋值给了寄存器eax。pop ebx 出栈操作,从栈中取出一个值赋给寄存器ebx。
将这个程序编译成EXE文件后(该EXE文件在文末提供下载),我们使用IDA来载入这个程序,找到main函数的位置,如图:
可以看到地址是00401010,我们使用Immunity debugger来进行动态分析,载入程序,跳到main函数:
找到我们的内联汇编,如图:
我们在第一个指令处下一个断点,也就是00401028处下断,F9过去。我们步过两条MOV指令,来到00401032,也就是第一个push的地方。我们注意观察ESP寄存器的值,此时是0012FF34,如图:
接着,我们步过,入栈一个数据,如图:
注意ESP:
从0012FF34变成了0012FF30,也就是每次PUSH,我们的ESP寄存器都会做如下操作:
ESP = ESP - 4
而ESP其实是指向栈顶的,栈顶也就是我们栈的唯一出口,任何是后执行POP指令都是把在栈顶的数据出栈。那我们提到的另一个寄存器,可想而知就是指向栈底的了,也就是EBP寄存器。
我们来看看当前EBP寄存器指向什么地方:
可以看到是0012F80,也就是说,当前的栈的区域就是从0012FF30到0012FF80的这段内存所构成的。
出栈和入栈都是从栈顶出入,当数据进入栈的话指向栈顶的ESP就得减4,而数据入栈则是ESP加4。
5.函数调用与返回
我们还是拿个Demo来说事(同样会在文末提供下载),代码如下:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
#include "stdafx.h" #include "stdio.h" void test( int a, int b, int c) { printf ( "%d,%d,%d" ,a,b,c); } int main() { test(1,2,3); _asm{ mov eax,0x41414141 } return 0; } |
我们还是用IDA来确定main函数的位置:
通过两次交叉引用找到了main函数,我们记下地址00401080,我们用Immunity Debugger来加载程序,跳到这个地址:
可以看到我们用内联汇编留的标志,那么上面这个在0040109E处的CALL就是CALL的test()函数了。
我们可以看到调用test()函数的过程如下:
首先是PUSH参数进栈,显示3,然后是2,然后是1。我们回过头来看C代码:
1
|
test(1,2,3); |
我们不是先传的1,最后传的3吗?这是因为这个函数的调用约定是stdcall,stdcall调用约定的函数的参数都是从右到左依次入栈的。这就是关于调用,我们需要了解的。
接着,我们先来聊一个简单的问题。
我们在C语言的所谓的函数,其实就是子程序而已。也就说,我们调用函数,其实就是让程序从主程序,也就是main调到子程序中去执行子程序的代码。
但是我们来看下图这个程序:
我们在main中调用了doSomething()子程序,当doSomething子程序执行完后就会回到main函数,继续执行printf()对吧?
那我们接下来的注意力就要放到这个函数返回上了,也就是我们的函数具体是如何返回的。
我们继续使用Demo2.exe来调,刚才我们已经来到了main函数,接着,我们在CALL test之前断下,F9过去,我们来看当前的栈顶:
就是我们的三个参数,最底下是第一个PUSH进去的,我们接下来进入这个CALL注意栈窗口。按下F7,如图:
可以看到当前的栈顶存储这004010A3,这个地址是什么?我们跳过去看看,但是在跳过去之前最好记下当前地址,也就是00401005,如图:
我们有了记录就能放心大胆的跳到当前栈顶的那个地址了,如图:
看到这里,仿佛已经对于函数返回的细节有了一个了解。
我们回到test函数内部,也就是我们记录的地址:
可以看到就是JMP到00401020,我们F8过去,如图:
如此,我们就跳入了test函数中了,我们不用关心其他细节,往下翻,翻到test函数的retn指令,如图:
我们在这个retn上面下断,F9过来,注意观察栈窗口,如图:
我就问你,此时的栈顶是什么?是不是很熟悉的感觉,不确定的朋友跳过去看看。
到这里,我们几乎感觉真相已经浮出水面了,接下来就是最后一点了。
我们截图保存当前的寄存器情况,如图:
然后,步过retn,对比前后的寄存器情况,如图:
可以看到,其实retn指令的操作就是将retn时的栈顶的数据出栈到EIP寄存器,用伪代码表示就是:
pop eip
而这个在retn时处于栈顶的数据,我们一般称它为“返回地址”。
这个概念呢,希望大家好好揣摩,这对于我们的栈溢出的利用是相当重要的。
6.开始溢出
接下来,我们就开始来学习Stack Buufer Overflow。首先,我们依然准备了一个Demo(依然会在文后提供下载),代码如下:
01
02
03
04
05
06
07
08
09
10
11
|
#include "stdAfx.h" #include "string.h" char exp [] = "ABCDEFGH" ; int main() { char buffer[8]; strcpy (buffer, exp ); return 0; } |
我们后面的实验也均在这个源文件的基础上进行修改,我们编译通过之后运行,无任何问题,为什么呢?
因为这个程序本就没有问题,我们可以看到exp字符数组的长度是8,而buffer也是,自然能够存下。
接下来,我们修改源文件的exp数组,改成:
1
|
char exp [] = "ABCDEFGHAAAAAAAAAAAA" ; |
在后面加十个A,这样的话,buffer数组自然就没法存下了,我们编译运行,如图:
我就想问一下,这个0x41414141是什么啊?别告诉我你不知道?其实0x41是字符A的ASCII码,也就是说,我们的exp数组里的十个A对我们的程序做了什么不为人知的事情,那怎么办呢?
很简单,调试嘛,我们还是用IDA和Immunity Debugger配合,来分析发生了什么神奇的事情。
我们首先使用IDA找出出该程序的main函数的地址,如图:
就是00401010,我们用Immunity Debugger载入,跳到这个地址,如图:
然后,我们在main函数开头下个断点,F9过来,然后一直单步步过,直到retn我们都未发现异常,如图:
但是,此时,我们来看我们的retn时的栈顶:
bingo!返回地址被覆盖了,被覆盖成了41414141,也就是说此时返回的话EIP会指向41414141处,调到这个地方去执行代码,我们跳一下看看:
可以看到EIP果然不计后果的指向了41414141,但是该片内存什么都没有。所以导致了如下错误:
这下知道发生了什么了吧。这也是Stack Overflow的利用点——覆盖返回地址。
7.深入分析
经过上面的分析,我们只是从表面上了解了是我们的exp数组的长度超过了buffer的长度,导致多出来的A覆盖到了关键的返回地址,导致程序在返回的时候跳到了41414141这个地址。
但是,我们还是不清楚这其中发生了什么。于是,我们还应该对这个过程进行更加深入的分析。我们可以点击Immunity Debugger的debug-restart来重新分析。Restart之后我们再次跳到00401010,如图:
我们把关注点放到strcpy函数上,因为正是strcpy将exp的值塞进buffer里的,所以,我们应该关注strcpy函数,如图:
我们在这里下一个断点,F9过来,观察函数的参数:
根据我们之前学习的stdcall的函数调用约定来看,当前栈顶的数据就是调用strcpy函数的第一个参数,下面则是第二个参数。
也就是说,是要将00422310这个地址的数据放入0012FF78这个空间中,那么,我们就应该将注意放到0012FF78这个地址上了,我们将这个地址放在数据窗口中跟随,如图:
接着,我们单步步过strcpy的CALL,看0012FF78的内存:
可以看到,这段内存已经被我们的41淹没了,此时,我们再次restart,再次来到main函数的第一条指令,观察栈窗口,如图:
可以看到0012FF84的位置就存储着我们的main函数的返回地址,然而,strcpy过后这个位置存储的数据已经成了如下图这样:
这就是这个程序的情况了,也就是说我们得从0012FF78一直覆盖到0012FF80就到了返回地址,后面的四个字节就是返回地址了。
所以,我们的exp数组的前8个字节填充buffer,跟着的4个字节覆盖到返回回地址之前,最后四个字节自然就是返回地址。
如此,我们就能随意的控制main函数返回的地址了。例如,我们让函数返回到66666666这个地址,就只需要将exp改为:
我们可以试试看,如图:
嘿嘿,和我们预想的一样,成功将返回地址覆盖为了66666666。
8.如何利用
接下来到了学习漏洞最有趣的环节,HOW TO EXPLOIT?嘿嘿,只要解决这个问题,我们就能通过漏洞来做些EVIL的事情了。
我们首先思考一下,目前,我们能够控制EIP指向我们想要的任意地址,可以没有地址上有我们想要的代码啊。
怎么办呢?其实办法还是有的,这些办法都是来自于前人的总结(向开拓者致敬!)
先说下最简单的办法,我们先回想一下,我们可以控制的可不止是EIP,我们连控制EIP都是间接的基于栈来控制的,其实我们能够直接控制的是栈。
我们可以进行如下布局,如图:
直接在返回地址下面放置我们的shellcode覆盖返回地址为我们shellcode的起始地址。
这样的话,在main函数返回的时候,就能跳到我们的shellcode中执行我们的shellcode了。
9.获取API地址
目前,我们已经解决了如何Exploit的问题,那么现在的当务之急就是获取API在DLL中的地址,因为这个东西是我们编写shellcode所必备的东西。
那么我们就可以使用这样一个程序来获取,代码如下:
/*对于这个程序的实现死宅在这里特别感谢k0shl和IEEE.两位大牛。由于死宅的C语言很菜,
而且国内网上的资料坑人。很多地方都不是很明白,在这样的时刻是k0shl和IEEE这两位
乐于助人的大牛为死宅讲了函数指针和两个API的用法。感谢。*/
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
#include "stdafx.h" #include "windows.h" #include "stdio.h" void Usage() { char *syb = "=" ; char tag[60]; for ( int i=0;i<60;i++) { tag = *syb; } printf ( "%s" ,tag); printf ( "\\nAPIGeter\\n[Author]bbs.ichunqiu.com\\n[Usage]APIGeter [DLLName] [APIName]\\n" ); printf ( "%s" ,tag); } int main( int argc, char * argv[]) { if (argc!=3) { Usage(); return 1; } HINSTANCE DLLAddr = LoadLibrary(argv[1]); DWORD APIAddr = ( DWORD )GetProcAddress(DLLAddr,argv[2]); printf ( "[APIGeter]Welcome to use the APIGeter\\n[Author]bbs.ichunqiu.com\\n" ); printf ( "DLL-Name:%s\\nAddress:0x%x\\n" ,argv[1],DLLAddr); printf ( "API-Name:%s\\nAddress:0x%x\\n" ,argv[2],APIAddr); return 0; } |
很简单的东西,但是却坑了死宅半天。
有了这个小工具——APIGeter(文末放出下载),大家再也不用怕C语言不好了。
直接如下图:
就能轻松的获取API的地址了。
关于这个程序,相信会C语言的朋友都能看懂了(网上的代码用函数指针,不知道作者怎么想的)
10.编写shellcode
接下来,我们要做的就是编写shellcode了。笔者给大家选取了一种较为简单的方式进行编写。
就是使用内联汇编,我们只需要在vs中用_asm{}把汇编代码写进去就可以了,首先,我们使用APIGeter获取MessageBoxA在user32.dll中的地址,是0x77d507ea,如图:
然后我们写这样一个程序,代码如下:
01
02
03
04
05
06
07
08
09
10
11
|
#include "stdafx.h" #include "windows.h" int main() { LoadLibrary( "user32" ); _asm{ //assembly code } return 0; } |
以上是关于WINDOWS平台下的栈溢出攻击从0到1(篇幅略长但非常值得一看)的主要内容,如果未能解决你的问题,请参考以下文章