0Day安全软件漏洞分析第2版阅读随笔2
本文最后更新于:2024年9月27日 下午
0Day安全软件漏洞分析第2版阅读随笔2
程序中所使用的缓冲区可以是堆区、栈区和存放静态变量的数据区。本章主要介绍在系统栈中发生溢出的情形。
第二章-栈溢出原理与实践
系统栈的工作原理
操作系统的内存
进程使用内存可以按照功能可以非常简单地分为以下4个部分(实际上的内存远没有这么简单):
代码区:存放二进制机器码,CPU在此处取指并执行。PE文件的代码段中包含的二进制机器码会被装入内存的代码区(.text)
数据区:存放全局变量
堆区:进程可以在堆区动态地请求一定大小的内存,用完后还给堆区。动态分配和回收是堆区的特点。
栈区:用于动态存储函数之间的调用关系,以保证被调函数在返回时恢复到母函数中继续执行。
系统栈、函数栈帧
首先栈是一种数据结构,这个肯定都知道啦🤣两种操作,PUSH和POP,存在栈顶TOP和栈底BASE,一般来说,TOP是动态的,BASE是静态的。
内存中的栈区实际上就是系统栈,是系统实现的一种管理函数调用的数据结构,由系统自动维护。
不同的操作系统在函数调用的规定上是不同的,如果要明确使用某一种调用约定,需要在函数前加上调用约定的声明。默认情况下,采用_stdcall
这种方式。
而函数的调用就是通过系统栈的来实现的,当一个新的函数被调用时,会为其开辟一个新的栈帧,OS会进行以下操作:
- 按规定顺序压入函数参数
- 压入返回地址,一般为函数调用指令的地址
- 此时CPU开始读取新的代码区的指令
- 压入当前栈帧的栈底指针值(EBP)
- EBP<=ESP,此时EBP指向的内存位置存储的是源函数的栈底指针值
- ESP=ESP-所需空间
汇编伪代码:
1 |
|
当函数调用完成时,
- 返回值存放在EAX寄存器
- ESP=ESP+占用内存大小
- EBP=(EBP)
- 函数返回值addr弹回,EIP=addr
汇编伪代码:
1 |
|
关于局部变量的存储,这里书里没有讲,寄存器和内存都可以用来存放函数执行时
所需的数据
。寄存器的存取速度比内存快很多,所以通常会优先把数据存入寄存器中。但是由于寄存器数量有限,因此当寄存器不够用时会将数据存放在栈内存中。
修改临界变量
原理
在系统栈中存在某一种可能的情况,局部变量紧挨着排列,当局部变量中存在数组之类的缓冲区时,并且在赋值时数组可以发生越界,那么越界的数组元素可能破坏相邻变量的值,甚至当紧挨着EBP
时,从而修改EBP
、返回地址等等。
crack_me2
人为制造溢出:
注意buffer
的声明位置
我们的目标是溢出到返回值,让其返回1
1 |
|
此时笔者转去学习x64dbg
了,不然用起来很不顺手🤣,如果需要了解简单的用法,见上一篇博客
浅浅学习归来,
首先将程序拖进x64dbg
,然后寻找验证函数的位置,在此模块搜索字符串,然后定位到大致位置
分析一下:大概在读取输入之后,调用了地址0000000000401550
处的函数,找到该处
挺近的,可以发现push rbp
压入栈底,然后在call完strcpy
函数后打下断点
那么我们先运行程序到输入密码处,然后输入密码,开始调试程序
进入断点,此时我们知道存在一个strcpy
函数,那么之后会有一个返回值,在内存中找到返回值的地方,选中上断点的反汇编代码,然后在详细信息表格中双击将要写入eax寄存器的那个内存的地址,然后会在内存试图里显示,由于我这里输入的是xxxxxxx
,所以值是1,当然不能通过验证
如果我们输入8个x
呢,可以发现,返回标志被溢出的换行符替换为0了,即可通过验证!
至于为什么是8个x
,其实很好理解,因为定义的是一个8个字符的数组哒!刚好最后一个截断字符溢出哒!
刚好我们人为制造的字符数组和Auth相邻哒!
但是你会意外的发现,如果输入字符串小于3xsh0re
,即使输入8个字符也不能冲破验证,这是因为小于则strcmp
函数的返回值为-1,那么就会导致Auth的值为0xFFFFFFFF,就算淹没了两个字节变为0xFFFFFF00也无济于事的🤣
当然笔者这里还有一个办法,想办法在输入中跟上7个截断字符,刚好淹没所有的Auth,当然这里在命令行里输入不太方便,然后笔者发现在之后的crack实验,我们通过文件读取的方式可以实现!😎
控制程序执行的流程
原理
我们通过上面的学习,已经知道利用栈溢出可以修改邻接变量,更通用、更强大的攻击通过缓冲区溢出改写的目标往往不是某一个变量,而是瞄准栈帧最下方的 EBP 和函数返回地址等栈帧状态值。
crack_me3
用键盘输入字符的 ASCII 表示范围有限,很多值(如 0x11、0x12 等符号)无法直接用键盘输入,所以我们把用于实验的代码稍作改动,将程序的输入由键盘改为从文件中读取字符串。
1 |
|
将上面的代码编译为exe,然后在同级目录下创建password.txt文件。
在动手之前,理清思路:
1.我们需要摸清楚栈中的状况
2.破解目标是通过密钥验证,所以应该知道密钥验证通过的指令地址
3.通过文件缓冲区溢出在返回地址处填上地址
通过x64dbg打开之前的编译好的文件,找到验证通过的指令地址。可以通过右键查找用户模块字符串快速找到。
可以得到地址为0x0000 0000 0040 161B,这里因为是在x86-64下,地址为64位,不同于书上是32位的。那么我们之后覆盖的返回值地址就是这里了😋
然后我们找到缓冲区的位置,首先在password.txt里输入“1234”,然后在通过调试在验证函数的下面这个地方打上断点:
然后在堆栈区找到我们的缓冲区地址:我们这里是0000 0000 0061 F9D0,可以发现其中填充的是“34333231”,正是我们的输入。
接下来就是制作我们的payload了,从上面的图可以发现,要淹没返回值需要5*4个无效字节加上构造的返回值地址,这里为什么是5个不是6个,因为对于双字数据,在内存中的存放按照从地址低到高这里也就是1234,而在作为数值时是从高到低的,这是由于调试器的原因会在堆栈区按数值显示,也就是对于00..061F9D0的偏移在栈中是7->0,所以是填充6个。
使用winhex编辑txt文件的16进制,内容如下:
然后运行,成功!
在缓冲区植入代码
原理
我们已经完成修改返回地址的壮举!😋那么我们为何不将返回地址改到我们自己的输入上呢,这样不就可以执行我们自己的代码了,那么我们的代码放哪,放缓冲区噻,之前填充的是无用的数值,填充指令不就可以了。
crack_me4
和实验3一样通过文件读取,不过改了缓冲区大小,加入了包含窗口调用dll。目的是在缓冲区填充弹窗的代码!
1 |
|
和之前一样需要获得函数调用在内存中的返回返回到缓冲区的地址,打开x64dbg,开调!
根据之前的调试方法,我们可以找到缓冲区的地址:0000 0000 0061 F9B0,接下来就需要我们编写password.txt里的内容了,肯定是要淹没到返回地址的位置的,看看上面的堆栈信息,可以发现我们需要设置7*12345678+缓冲区地址。在Winhex里编辑如下:
调试一下,发现成功淹没
接下来就是编写我们植入的代码,我们需要定位到内存中MessageBox的地址,这里不沿用书的方法,我们直接在x64dbg中查看符号文件,搜索即可得到地址为:0x00007FFB294DA000
然后编写shellcode:这里笔者花了大量的时间来写shellcode,
第一个问题是一开始没有调高栈顶,也就是没有对rsp进行修改,导致运行时新入栈的值淹没了我的shellcode,所以调高了栈顶
第二个问题是发现无法在堆栈中执行命令,尝试在系统中关闭了DEP,但是还是不行,然后在调试器中,右键转到内存,然后右键该区域,内存权限,修改为可执行权限即可
第三个问题是发现64位的地址中会有0x00,不可避免,选择使用逻辑移位的方式构造跳转地址。
1 |
|
·
右键修改可执行权限
全选然后设置权限
发现已经执行到MessageBoxA函数处😁:
最后一个问题没有解决!为什么不弹窗!为什么!😅
第二章就结束了,虽然最后有个小遗憾,但是笔者已经花了大量的时间动手去完成任务,并且效果还不错,所以就不纠结于弹窗了哈哈😀