0day安全:软件漏洞分析技术(第二版)读书笔记(1-2)
漏洞利用技术才是实施有效攻击的最核心技术,才是突破安全边界、实施深度入侵的关键所在。ʕ •ᴥ•ʔ (๑˃̵ᴗ˂̵) ପ( ˘ᵕ˘ ) ੭
PUSHPOPCALLRET内存数据数值数据小端序
第一章 基础知识
我们通常把能够引起软件做一些“超出设计范围的事情”的bug 称为漏洞(vulnerability)。
- 功能性逻辑缺陷(bug):影响软件的正常功能,例如,执行结果错误、图标显示错误等。
- 安全性逻辑缺陷(漏洞):通常情况下不影响软件的正常功能,但被攻击者成功利用后,有可能引起软件去执行额外的恶意代码。常见的漏洞包括软件中的缓冲区溢出漏洞、网站中的跨站脚本漏洞(XSS)、 SQL注入漏洞等。
1.1 漏洞挖掘、漏洞分析、漏洞利用
利用漏洞进行攻击可以大致分为漏洞挖掘、漏洞分析、漏洞利用三个步骤。这三部分所用的技术有相同之处,比如都需要精通系统底层知识逆向工程等:同时也有一定的差异。
1.1.1 漏洞挖掘
由于安全性漏洞往往有极高的利用价值,例如,导致计算机被非法远程控制,数据库数据泄漏等,所以总是有无数技术精湛、精力旺盛的家伙在夜以继日地寻找软件中的这类逻辑瑕疵。他们精通二进制、汇编语言、操作系统底层的知识;他们往往也是杰出的程序员,因此能够敏锐地捕捉到程序员所犯的细小错误。
寻找漏洞的人并非全是攻击者。大型的软件企业也会雇用一些安全专家来测试自己产品中的漏洞,这种测试工作被称做 Penetrationtest(攻击测试),这些测试团队则被称做 Tigerteam 或者 Ethic hacker。
从技术角度讲,漏洞挖掘实际上是一种高级的测试 (QA)。 学术界一直热衷于使用静态分析的方法寻找源代码中的漏洞;而在工程界,不管是安全专家还是攻击者,普遍采用的漏洞挖掘方法是Fuzz
,这实际是一种“灰”盒测试。
1.1.2 漏洞分析
①在分析漏洞时,如果能够搜索到POC (proof of concept)代码,就能重现漏洞被触发的现场。这时可以使用调试器观察漏洞的细节,或者利用一此工具(如Paimei)更方便地找到漏洞的触发点。
当无法获得 POC 时,就只有厂商提供的对漏洞的简单描述了。
②一个比较通用的办法是使用补丁比较器,首先比较patch
前后可执行文件都有哪些地方被修改,之后可以利用反汇编T 具(如IDA Pro)重点逆向分析这些地方。
漏洞分析需要扎实的逆向基础和调试技术,除此以外还要精通各种场景下的漏洞利用方法。这种技术更多依靠的是经验,很难总结出通用的条款。
1.1.3 漏洞利用
手机安全及 Web 应用中的脚本注入攻击所使用的技术与 Windows 平台下缓冲区溢出相差较大,且自成体系。
1.2 二进制文件概述
1.2.1 PE文件格式
PE (Portable Exec utable)是 Win32 平台下可执行文件遵守的数据格式。常见的可执行文 件(如*.exe
文件和*.II
文件)都是典型的 PE 文件。
一个可执行文件不光包含了二进制的机器代码,还会自带许多其他信息,如字符串、菜单、图标、位图、字体等。PE 文件格式规定了所有的这些信息在可执行文件中如何组织。PE 在程序被执行时,操作系统会按照 PE 文件格式的约定去相应的地方准确地定位各种类型的资源,并分别装入内存的不同区域。
1.2.2 虚拟内存
进程所拥有的 4GB 虚拟内存中包含了程序运行时所必需的资源,比如代码、栈空间、堆空间、资源区、动态链接库等。在后面的章节中,我们将不停地辗转于虚拟内存中的这些区域。
注意:操作系统原理中也有“虚拟内存”的概念,那是指当实际的物理内存不够时,有时操作系统会把“部分硬盘空间”当做内存使用从而使程序得到装载运行的现象。请不要将用硬盘充当内存的“虚拟内存”与这里介绍的“虚拟内存”相混淆。此外,本书除第4篇内核安全外,其余所述之“内存”均指 Windows 用户态内存映射机制下的虚拟内存。
关于用户态模式下的虚拟内存
由下图可以看出,进程的虚拟地址空间,由多个虚拟内存区域构成。虚拟内存区域是进程的虚拟地址空间中的一个同质区间,即具有同样特性的连续地址范围。上图中所示的text数据段(代码段)、初始数据段、BSS数据段、堆、栈和内存映射,都是一个独立的虚拟内存区域。而为内存映射服务的地址空间处在堆栈之间的空余部分。
用户态进程的虚拟地址如何转换成物理地址、Linux内存管理(四)用户态内存映射
1.2.3 PE文件与虚拟内存之间的映射
在调试漏洞时,可能经常需要做这样两种操作。
- 静态反汇编工具看到的 PE 文件中某条指令的位置是相对于磁盘文件而言的,即所谓的文件偏移。我们可能还需要知道这条指令在内存中所处的位置,即虚拟内存地址(VA)。
- 反之,在动态调试时看到的某条指令的地址是虚拟内存地址,我们也经常需要回到 PE 文件中找到这条指令对应的机器码。
文件偏移地址(File Offset)
数据在 PE 文件中的地址叫文件偏移地址(文件地址)。这是文件在磁盘上存放时相对于文件开头的偏移。
装载基址(Image Base )
PE 装入内存时的基地址。默认情况下,EXE 文件在内存中的基地址是0x00400000,DL L文件是0x10000000,这些位置可以通过修改编译选项更改。
虚拟内存地址(Virtual Address, VA)
PE 文件中的指令被装入内存后的地址。
相对虚拟地址(Relative Virtual Address, RVA)
相对虚拟地址是内存地址相对于映射基址的偏移量。
虚拟内存地址、映射基址、相对虚拟内存地址三者之间有如下关系。
$$VA=Image Base + RVA$$
文件偏移是相对于文件开始处0字节的偏移,RVA(想对虚拟地址)则是相对于装载基址0x00400000
处的偏移。
由于操作系统在进行装载时“基本”上保持 PE 中的各种数据结构,所以文件偏移地址和RVA有很大的一致性。之所以说“基本”上一致是因为还有一些细微的差异。这些差异是由于文件数据的存放单位与内存数据存放单位不同而造成的。
- PE 文件中的数据按照磁盘数据标准存放,以
0x200
字节为基本单位进行组织。当一个数据节(section) 不足0x200
字节时,不足的地方将被 0x00 填充;当一个数据节超过0x200
字节时,下一个0x200
块将分配给这个节使用。因此 PE 数据节的大小永远是0x200
的整数倍。 - 当代码装入内存后,将按照内存数据标准存放,并以
0x1000
字节为基本单位进行组织。类似的,不足将被补全,若超出将分配下一个0x1000
为其所用。因此,内存中的节总是0x1000
的整数倍。
这种由存储单位差异引起的节基址差称为不足的地方将被 0x00 填充节偏移。
$.text节偏移=0x 1000-0x400=0xC00$
$.rdata节偏移=0x7000-0x6200=0xE00$
$.data节偏移=0x9000-0x7400=0x1C00$
$.rsrc节偏移=0x2D000-0x7800=0x25800$
$$文件偏移地址 = 虚拟内存地址(VA) - 装载基址(Image Base) -节偏移 = RVA - 节偏移$$
第二章 栈溢出原理与实践
2.1 系统栈的工作原理
简单说来,缓冲区溢出就是在大缓冲区中的数据向小缓冲区复制的过程中,由于没有注意小缓冲区的边界,“撑爆”了较小的缓冲区,从而冲掉了和小缓冲区相邻内存区域的其他数据而引起的内存问题。缓冲溢出是最常见的内存错误之一,也是攻击者入侵系统时所用到的最强大、最经典的一类漏洞利用方式。
成功地利用缓冲区溢出漏洞可以修改内存中变量的值,甚至可以劫持进程,执行恶意代码, 最终获得主机的控制权。
要透彻地理解这种攻击方式,需要回顾一些计算机体系架构方面的基础知识,搞清楚CPU、寄存器、内存是怎样协同工作而让程序流畅执行的。
根据不同的操作系统,一个进程可能被分配到不同的内存区域去执行。但是不管什么样的操作系统、什么样的计算机架构,进程使用的内存都可以按照功能大致分成以下 4 个部分。
- 代码区(text):这个区域存储着被装入执行的进制机器代码,处理器会到这个区域取指并执行。
- 数据区(data):用于存储全局变量等。
- 堆区(heap):进程可以在堆区动态地请求一定大小的内存,并在用完之后归还给堆区。动态分配和回收是堆区的特点。
- 栈区(stack):用于动态地存储函数之间的调用关系,以保证被调用函数在返回时恢复到母函数中继续执行。
具体内容:C/C++ 语言内存管理
在 Windows 平台下,高级语言写出的程序经过编译链接,最终会变成 PE 文件。当PE文件被装载运行后,就成了所谓的进程。PUSHPOPCALLRET
- PUSH XXX:
XXX
可以是立即数也可以是寄存器,意思是将XXX
入栈到栈顶。push XXX
等价于sub esp,0x04
,mov [esp],XXX
。- 每进行一次 PUSH,TOP(栈顶)自动加 1,即 esp 自动减 4。
- POP EAX:将栈顶元素弹出到寄存器 eax 中。
- 等价于
move eax,[esp]
,add esp 0x4
。 - 每进行一次 PUSH,TOP(栈顶)自动减 1,即 esp 自动加 4。
- 等价于
- CALL XXX:
XXX
可以是立即数(地址)也可以是寄存器,意思是将跳转到XXX
处继续执行。- 等价于
push 返回地址
,move eip,XXX
,jmp eip
。 CALL
指令会将返回地址压入栈中,然后修改 EIP 的值为XXX
,最后跳转到XXX
。
- 等价于
- RET:函数返回。
- 等价于
pop eip
,jmp eip
。 ret
会将栈顶元素(返回地址)弹出到寄存器 EIP 中,然后去执行 EIP 中的指令。
- 等价于
根据操作系统的不同、编译器和编译选项的不同,同一文件不同函数的代码在内存代码区中的分布可能相邻,也可能相离甚远,可能先后有序,也可能无序;但它们都在同一个PE文件的代码所映射的一个“节”里。我们可以简单地把它们在内存代码区中的分布位置理解成是散乱无关的。
2.2 函数调用约定与函数返回
2.2.1 函数调用约定
每一个 C++ 类成员函数都有一个this指针,在 Windows 平台中,这个指针一般是用ECX寄存器来传递的,但使用 GCC 编译器编译,这个指针会作为最后一个参数压入栈中。
2.2.2 函数返回
函数返回时步骤如下:
- 保存返回值:通常将函数的返回值保存在寄存器EAX中。
- 恢复栈帧:弹出当前栈帧,恢复上一个栈帧。具体包括:
- 在堆栈平衡的基础上,给 ESP 加上栈帧的大小,抬高栈顶(地址变大),回收当前栈帧的空间。将当前栈帧底部保存的前栈帧 EBP 值弹入EBP寄存器,恢复出上一个栈帧。
- 将函数返回地址弹给EIP寄存器。
- 跳转:按照函数返回地址跳回母函数(调用函数)中继续执行。
以 C 语言和 Win32 平台为例,函数返回时的相关的指令序列如下。
1 | add esp,XXX ;降低栈顶,回收当前的栈帧。此时栈顶元素是被保存的旧ebp的值,esp指向栈顶的旧ebp的值 |
函数调用与函数返回的的过程图如下。
2.3 爆破(修改跳转逻辑)
程序源代码如下:
1 |
|
源代码分析:分析main
函数,有两个判断分支,只有当输入的password
等于1234567
时密码正确才能通过验证,跳出循环。
破解思路:修改跳转逻辑,即使输入错误的密码,也将通过验证。
使用工具:IDA Pro、OllyDbg、LordPE、UltraEdit。
- 将编译过后的
.exe
文件拖入到 IDA 中,先根据函数流程图判断分支,然后定位到发生跳转的点(即爆破点); - 可以看到这条指令定位在
.text
节,并且 IDA 已经自动将该条指令的地址换算成了运行时的内存地址 VA:0x0040106E
。接着使用 OllyDbg 打开该 PE 文件,并搜素(Ctrl+G)地址0x0040106E
定位到该指令。
OllyDbg 在默认情况下将程序中断在 PE 装载器开始处,而不是main
函数的开始。可以按 F8 键单步跟踪,看看在main
函数被运行之前,装载器都做了哪些准备工作。一般情况下, main
函数位于GetCommandL ineA
函数调用后不远处,并且有明显的特征:mian函数在调用之前有3次连续的压栈操作,因为系统要给main传入默认的argc、argv等参数。找到main函数调用后,按F7键单步跟入就可以看到真正的代码了。
- 选中这条指令,按
F2
键下断点,成功后,指令的地址会被标记成不同颜色。 - 按
F9
键让程序运行起来,这时候控制权会回到程序,OllyDbg 暂时挂起。到程序提示输入密码的 Console界面随便输入一个错误的密码,回车确认后,OllyDbg会重新中断程序,收回控制权。 - 分析
0x0040106C
和0x0040106E
处的代码:TEST EAX,EAX
,JE XXXXXXXX
。也就是说,EAX 中的值为 0 时,跳转将被执行,程序进入密码确认流程;否则跳转不执行,程序进入密码重输的流程。 - 两种破解修改方法:
- 把 JE 这条指令的机器代码修改成 JNE(非 0 则跳转),那么整个程序的逻辑就会反过来:输入错误的密码会被确认,输入正确的密码反而要求重新输入!(双击JE这条指令,将其修改成 JNE,单击“Assemble"按钮将其写入内存如图 1.4.9 所示)OllyDbg 将汇编指令翻译成机器代码后写入内存。原来内存中的机器代码74 (JE)现在变 成了75 (JNE)。
- 把 TEST EAX ,EAX 指令修改成 XOR EAX,EAX 也能达到改变程序流程的目的,这时不论正确与否,密码都将被接受。
将 PE 文件硬编码。上面动态调试只是在内存中修改程序数据,当程序再一次打开的时候内存丢失则不会继续执行上述结果,所以还需要在二进制文件中也修改相应的字节。
上面得到的是内存虚拟地址(VA)为
0x0040106E
,使用LordPE 打开.exe
文件,如图 1.4.10。按照 VA 与文件地址的换算公式:$文件偏移地址 = 虚拟内存地址(VA) - 装载基址(Image Base) - 节偏移$
$ = 0x0040106E - 0x00400000 - (0x00001000 - 0x00001000) = 0x106E$
也就是说,这条指令在 PE 文件中位于距离文件开始处
106E
字节的地方。用UltraEdit 按照二进制方式打开该.exe
文件,如图1.4.11 所示。按快捷键 Ctrl+G,输入0x106E
直接跳到 JE 指令的机器代码处,如图1.4.12所示。将这一个字节的 74 (JE) 修改成 75 (JNE),保存后重新运行可执行文件,原本正确的密码1234567
现在反而提示错误了。
2.4 修改领接变量(返回值)
函数的局部变量在栈中一个挨着一个排列,如果这些局部变量中有数组之类的缓冲区,并且程序中存在数组越界的缺陷,那么越界的数组元素就有可能破坏栈中相邻变量的值,甚至破坏栈帧中所保存的 EBP 值、返回地址等重要数据。
大多数情况下,局部变量在栈中的分布是相邻的,但也有可能出于编译优化等需要而有所例外。
程序源代码如下:
1 |
|
源码分析:verify_password
函数中的strcpy(buffer,password)
直接将password
复制给了buffer
,没有经过过滤处理,且buffer
在内存的栈中,故可以操作内存修改栈中数据。
破解思路:修改栈数据,使得返回值authenticated
等于 0,绕过验证。
相对 Crack 实验的修改
- verify_ password 函数中的局部变量 char buffer[8] 的声明位置。
- 字符串比较之后的 strcpy(buffer,password)。
这两处修改实际上对程序的密码验证功能并没有额外作用,这里加上它们只是为了人为制造一个栈溢出漏洞。
如果我们输入的密码超过了7个字符(注意:字符串截断符NULL将占用一个字节),则越界字符的ASCII码会修改掉authenticated的值。如果这段溢出数据恰好把authenticated改为 0,则程序流程将被改变。
图 2.2.1 是verify_ password
函数运行时的函数栈。
(1)可以看到,在verify_ password
函数的栈帧中,局部变量int authenticated
恰好位于缓冲区char buffer[8]
的“下方”。
(2)authenticated
为int
类型,在内存中是一个 DWORD,占 4 个字节。所以,如果能够让buffer
数组越界,buffer[8]
、 buffer[9]
、buffer[10]
、buffer[11]
将写入相邻的变量authenticated
中。
(3)观察一下源代码不难发现,authenticated
变量的值来源于strcmp
函数的返回值,之后会返回给main
函数作为密码验证成功与否的标志变量:当authenticated
为 0 时,表示验证成功;反之,验证不成功。
- 假如输入的密码为7个英文字母
q
,按照字符串的序关系qqqqqq>1234567
, strcmp 应该返回 1,即authenticated 为 1。OllyDbg 动态调试的实际内存情况如图 2.2.3 所示,栈帧分布情况如表2-2-2。
在观察内存的时候应当注意内存数据 与 数值数据 的区别。在我们的调试环境中,内存由低到高分布,Win32 系统在内存中由低位向高位存储一个 4 字节的双字(DWORD),但在作为“数值”应用的时候,却是按照由高位字节向低位字节进行解释。这样一来,在我们的调试环境中,“内存数据”中的 DWORD 和我们逻辑上使用的“数值数据”是按每个字节(8位)序逆序过的。
例如,变量 authenticated 在内存中存储为0x01 00 10 00
,这个“内存数据”的双字会被计算机由高位向低位按字节解释成“数值数据”0x00 10 00 01
。出于便于阅读的目的,OllyDbg 在栈区显示的时候已经将内存中双字的字节序反转了,也就是说,栈区栏显示的是“数值数据”,而不是原始的“内存数据”( 栈区栏按照数值数据显示 ),所以,在栈内看数据时,从左向右读数据时对于 32 位系统地址的偏移量依次为0、 1、2、3。
小端序:数值数据的高字节存放于内存数据的低字节,如00rst
--> (对应ASCII码)00 72 73 74
-->(存于内存中时)74 73 72 00
。其实就是一个入栈原理,如数值数据00 72 73 74
,74
数值低位先入栈,相应的也就存于栈的高地址,73
位于栈的次高地址,所以在内存中也就变成了74 73 72 00
。
【大端序小端序】
下面试试输入超过 7 个字符,看看超过
buffer[8]
边界的数据能不能写进authenticated
变量的数据区。为了便于区分溢出的数据,这次我们输入的密码为qqqqqqqqrst
(8个q,‘q’、’r’、 ‘s’、‘t’ 的ASCII码相差1),结果如图 2.2.4 所示。栈中的情况和我们分析的一样,从输入的第 9 个字符开始,将依次写入
authenticated
变量。 按照我们的输入qqqqqqqqrst
,最终authenticated
的值应该是字符r
、s
、t
和用于截断字符串的null
所对应的 ASCII 码0x00747372
。这时的栈帧数据如表 2-2-3 所示。
- 如下图
严格说来,并不是任何 8 个字符的字符串都能冲破上述验证程序。
由代码中的authenticated=strcmp(password,PASSWORD)
, 我们知道 authenticated 的值来源于字符串比较函数 strcmp 的返回值。按照字符串的序关系,当输入的字符串大于1234567
时返回 1,这时 authenticated 在内存中的值为0x00000001
,可以用字串的截断符 NULL 淹没 authenticated 的低位字节而突破验证;当输入字符串小于1234567
时(例如,“0123” 等字符串),函数返回-1
,这时 authenticated 在内存中的值按照双字-1
的补码存放,为0xFFFFFFF
,如果这时也输入 8 个字符的字符串,截断符淹没 authenticated 低字节后,其值变为0xFFFFFF00
,所以这时是不能冲破验证程序的。
2.5 修改函数返回地址
上节实验改写邻接变量的方法是很有用的,但这种漏洞利用对代码环境的要求相对比较苛刻。更通用、更强大的攻击通过缓冲区溢出改写的目标往往不是某一个变量,而是瞄准栈帧最下方的 EBP 和 函数返回地址等栈帧状态值。
回顾上节实验中输入7个q
程序正常运行时的栈状态,如表2-3-1 所示。
如果继续增加输入的字符,那么超出buffer[8]
边界的字符将依次淹没authenticated
、 前栈帧EBP、返回地址。也就是说,控制好字符串的长度就可以让字符串中相应位置字符的 ASCII 码覆盖掉这些栈帧状态值。
输入19个
q
,第 9~12 个字符将 authenticated 冲刷为0x71717171
;第 13~16 个字将前栈帧 EBP 冲刷为0x71717171
;第 17~19 个字符连同 NULL 结束符将返回地址冲刷为0x00717171
。这里用 19 个字符作为输入,看看淹没返回地址会对程序产生什么影响。出于双字对齐的目的,我们输入的字符串按照
4321
为一个单元进行组织,最后输入的字符串为4321432143214321432
,Ollydbg 运行状态如图2.3.2,栈的情况如表2-3-1所示。
返回地址被字符 ASCII 码覆盖成了
0x00323334
,函数返回时的状态如图2.3.4所示。我们可以从调试器中的显示看出计算机中发生的事件:
- 函数返回时将返回地址装入EIP寄存器。
- 处理器按照EIP寄存器的地址
0x00323334
取指。 - 内存
0x00323334
处并没有合法的指令,处理器不知道该如何处理,报错。
由于0x00323334
是一个无效的指令地址,所以处理器在取指的时候发生了错误使程序崩溃。但如果这里我们给出一个有效的指令地址,就可以让处理器跳转到任意指令区去执行(比如直接跳转到程序验证通过的部分),也就是说,我们可以 通过淹没返回地址而控制程序的执行流程 。
2.6 控制程序执行流程(修改返回地址)
用键盘输入字符的 ASCII 表示范围有限,很多值(如0x11、0x12等符号)无法直接用键盘输入,所以把实验的代码做了下改动,将程序的输入改由PE文件同目录下从文件读取出字符串。
1 |
|
准备工作:
(1)要摸清楚栈中的状况,如函数地址距离缓冲区的偏移量等。
(2)要得到程序中密码验证通过的指令地址,以便程序直接跳去这个分支执行。
(3)要在password.txt
文件的相应偏移处填上这个地址。
(4)verify_password
函数返回后就会直接跳转到验证通过的正确分支去执行了。
首先用 OllyDbg 加载得到可执行 PE 文件,如图2.3.5所示。
阅读图 2.3.5 中显示的反汇编代码,返回地址为
0x00401107
,在0x0040110A
处将EAX中的函数返回值取出,在0x0040110D
处与 0 比较,然后决定跳转到提示验证错误的分支或提示验证通过的分支。可以知道通过验证的程序分支的指令地址为0x00401122
,如果把返回地址修改为0x00401122
,则可以绕过程序验证,如图2.3.6。仍然出于字节对齐、容易辨认的目的,仍然将“4321”作为一个输入单元。
buffer[8]
共需要两个这样的单元。- 第3个输入单元将authenticated覆盖;
- 第4个输入单元将前栈帧EBP值覆盖;
- 第5个输入单元将返回地址覆盖。
为了把第5个输入单元的 ASCII 码值
0x34333231
修改成验证通过分支的指令地址0x00401122
,我们将借助十六进制编辑工具 UItraEdit 来完成(0x40、 0x11 等ASCII码对应的符号很难用键盘输入)。创建一个名为
password.txt
的文件,并用记事本打开,在其中写入5个“4321”后保存到与实验程序同名的目录下。保存后用 UItraEdit 打开,将 UItraEdit 切换到十六进制。
注意这里的数值数据和内存数据,使用小端序。
切换回文本模式,保存为
password.txt
,用 OllyDbg 调试后,最终栈的状态如表2-3-4。
2.7 执行Shellcode
本节将介绍通过栈溢出让进程执行输入数据中植入的代码。在上节实验中,我们让函数返回到main
函数的验证通过分支的指令。试想一下,如果我们在buffer
里包含我们自己想要执行的代码,然后通过返回地址让程序跳转到系统栈里执行,我们岂不是可以让进程去执行本来没有的代码,直接去做其他事情了!
如图2.4.1所示,在本节实验中,我们准备向password.txt
文件里植入二进制的机器码,并用这段机器码来调用 Windows 的一个 API 函数MessageBoxA
, 最终在桌面上弹出一个消息框并显示failwest
字样。(即在输入的数据中包含shellcode,然后修改返回地址,使返回地址被修改为shellcode的起地址,数组入栈是将全部数据压入,同时栈顶是数组的第一个元素。)
这里还应该注意的是正常情况下返回地址是指令ADD ESP 4
对应的地址,当函数返回时去从恢复栈的这条指令继续执行,然后是参数、局部变量、被压栈的寄存器、ebp,然后是上一个调用函数的返回地址。
1 |
|
实验目的:在password.txt
文件中植入二进制的机器码,在password.txt
攻击成功时,密码验证程序应该执行植入的代码,并在桌面上弹出一个消息框显示failwest
字样。
在动手之前需要完成的几项工作:
(1)分析并调试漏洞程序,获得淹没返回地址的偏移。
(2)获得buffer
的起始地址,并将其写入password.txt
的相应偏移处,用来冲刷返回地址。
(3)向password.txt
中写入可执行的机器代码,用来调用 API 弹出一个消息框。
本节验证程序里
verify_password
中的缓冲区为44个字节,按照前边实验中对栈结构的分析,我们不难得出栈帧中的状态。如果在
password.txt
中写入恰好44个字符,那么第45个隐藏的截断符null
将冲掉authenticaed
低字节中的1,从而突破密码验证的限制。我们不妨就用44个字节作为输入来进行动态调试。出于字节对齐、容易辨认的目的,我们把
4321
作为个输入单元。buffer[44]
共需要11个这样的单元,第12个输入单元将athenticated
覆盖;第13个输入单元将前栈帧EBP值覆盖;第14个输入单元将返回地址覆盖。则共需要11组4321
,共44个字符,应该就可以绕过验证返回正确消息。此时栈的状态如图2.4.4和表2-4-2。
动态调试的结果证明了前边分析的正确性。我们可以得到以下信息。
buffer
数组的起始地址为0x0012FAF0
。该该地址靠近栈顶,存放buffer[0]
, 此时ESP指针为0x0012FA9C
;password.txt
文件中第53~ 56个字符的 ASCII 码值将写入栈帧中的返回地址,成为函数返回后执行的指令地址。(0-44 -> buffer,45-48-> athenticated,49-52 -> ebp,53-56 -> 返回地址)也就是说,将
buffer
的起始地址0x0012FAF0
写入password.txt
文件中的第53~56
个字节,在verify password
函数返回时会跳到我们输入的字串开始取指执行。
下面还需要给
password.txt
中植入机器代码。让程序弹出一个消息框只需要调用Windows
的 API 函数MessageBox
。MSDN 对这个函数的解释如下。1
2
3
4
5
6int MessageBoxA(
HwND hWnd,
LPCSTR IpText,
LPCSTR lpCaption,
UINT uType
);hWnd [in]
消息框所属窗口的句柄,如果为NULL,消息框则不属于任何窗口。lpTex[in]
字符串指针,所指字符串会在消息框中显示。lpCaption [in]
字符串指针,所指字符串将成为消息框的标题。uType [in]
消息框的风格(单按钮、多按钮等),NULL代表默认风格。
我们将给出调用这个API的汇编代码,然后翻译成机器代码,用十六进制编辑工具填入
password.txt
文件。用汇编语言调用
MessageboxA
需要3个步骤:(1)装载动态链接库
user32.dll
。 MessageBoxA 是动态链接库user32.dll的导出函数。虽然大多数有图形化操作界面的程序都已经装载了这个库,本实验已预先手动加载了它。(2)在汇编语言中调用这个函数需要获得这个函数的入口地址。
(3)在调用前需要向栈中按从右向左的顺序压入MessageBoxA的4个参数。
MessageBoxA的入口参数可以通过user32.dll在系统中加载的基址和MessageBoxA在库中的偏移相加得到。
题外话:熟悉MFC的程序员一定知道,其实系统中并不存在真正的MessagBox函数,对MessageBox这类API的调用最终都将由系统按照参数中字符串的类型选择“A”类函数( ASCII)或者“W”类函数( UNICODE)调用。因此,我们在汇编语言中调用的函数应该是MessageBoxA。 多说一句,其实MessageBoxA的实现只是在设置了 几个不常用参数后直接调用MessageBoxExA。
将上述汇编指令对应的机器代码以十六进制形式逐字写入
password.txt
,第53~ 56字节填入buffer
的起址0x0012FAF0
,其余的字节用0x90
(nop指令)填充。因为MessageBoxA 调用的代码执行完成后,没有写用于安全退出的代码的缘故。在植入代码中没有安全地退出,程序会崩溃。所以应该在植入代码中安全地退出程序,甚至在植入代码结束后修复堆栈和寄存器,让程序重新回到正常的执行流程。