0Day安全软件漏洞分析第2版阅读随笔2

本文最后更新于:2024年9月27日 下午

0Day安全软件漏洞分析第2版阅读随笔2

程序中所使用的缓冲区可以是堆区、栈区和存放静态变量的数据区。本章主要介绍在系统栈中发生溢出的情形。

第二章-栈溢出原理与实践

系统栈的工作原理

操作系统的内存

进程使用内存可以按照功能可以非常简单地分为以下4个部分(实际上的内存远没有这么简单):

  • 代码区:存放二进制机器码,CPU在此处取指并执行。PE文件的代码段中包含的二进制机器码会被装入内存的代码区(.text)

  • 数据区:存放全局变量

  • 堆区:进程可以在堆区动态地请求一定大小的内存,用完后还给堆区。动态分配和回收是堆区的特点

  • 栈区:用于动态存储函数之间的调用关系,以保证被调函数在返回时恢复到母函数中继续执行。

系统栈、函数栈帧

​ 首先栈是一种数据结构,这个肯定都知道啦🤣两种操作,PUSH和POP,存在栈顶TOP和栈底BASE,一般来说,TOP是动态的,BASE是静态的。

​ 内存中的栈区实际上就是系统栈,是系统实现的一种管理函数调用的数据结构,由系统自动维护。

​ 不同的操作系统在函数调用的规定上是不同的,如果要明确使用某一种调用约定,需要在函数前加上调用约定的声明。默认情况下,采用_stdcall这种方式。

​ 而函数的调用就是通过系统栈的来实现的,当一个新的函数被调用时,会为其开辟一个新的栈帧,OS会进行以下操作:

  1. 按规定顺序压入函数参数
  2. 压入返回地址,一般为函数调用指令的地址
  3. 此时CPU开始读取新的代码区的指令
  4. 压入当前栈帧的栈底指针值(EBP)
  5. EBP<=ESP,此时EBP指向的内存位置存储的是源函数的栈底指针值
  6. ESP=ESP-所需空间

汇编伪代码:

1
2
3
4
5
6
7
PUSH arg3;
PUSH arg2;
PUSH arg1;
CALL func; //此时压入返回地址,代码区转换
PUSH EBP;
MOV EBP,ESP;
SUB ESP,EBX;//假设EBX为需要的内存大小,静态变量使用内存大小的确定在编译器编译时就已经确定了

当函数调用完成时,

  1. 返回值存放在EAX寄存器
  2. ESP=ESP+占用内存大小
  3. EBP=(EBP)
  4. 函数返回值addr弹回,EIP=addr

汇编伪代码:

1
2
3
4
ADD  ESP,EBX;
MOV EBP,(EBP);
POP EBP;
RETN;

关于局部变量的存储,这里书里没有讲,寄存器和内存都可以用来存放函数执行时所需的数据。寄存器的存取速度比内存快很多,所以通常会优先把数据存入寄存器中。但是由于寄存器数量有限,因此当寄存器不够用时会将数据存放在栈内存中。

修改临界变量

原理

在系统栈中存在某一种可能的情况,局部变量紧挨着排列,当局部变量中存在数组之类的缓冲区时,并且在赋值时数组可以发生越界,那么越界的数组元素可能破坏相邻变量的值,甚至当紧挨着EBP时,从而修改EBP、返回地址等等。

crack_me2

人为制造溢出:

注意buffer的声明位置

我们的目标是溢出到返回值,让其返回1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h> 
#define PASSWORD "3xsh0re"
#define FLAG "flag{stack_overflow_right?}"
int verify_password (char *password)
{
int authenticated;
char buffer[8];// add local buffto be overflowed
authenticated=strcmp(password,PASSWORD);
strcpy(buffer,password);//over flowed here!
return authenticated;
}
main()
{
int valid_flag=0;
char password[1024];
while(1)
{
printf("please input password: ");
scanf("%s",password);
valid_flag = verify_password(password);
if(valid_flag)
{
printf("incorrect password!\n\n");
}
else
{
printf("Congratulation! You have got the flag:%s\n",FLAG);
break;
}
}
}

此时笔者转去学习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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h> 
#define PASSWORD "3xsh0re"
int verify_password (char *password)
{
int authenticated;
char buffer[8];
authenticated=strcmp(password,PASSWORD);
strcpy(buffer,password);//over flowed here!
return authenticated;
}
main()
{
int valid_flag=0;
char password[1024];
FILE * fp;
if(!(fp=fopen("password.txt","rw+")))
{
exit(0);
}
fscanf(fp,"%s",password);
valid_flag = verify_password(password);
if(valid_flag)
{
printf("incorrect password!\n");
}
else
{
printf("Congratulation! You have passed the verification!\n");
}
fclose(fp);
}

将上面的代码编译为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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h> 
#include <windows.h> //包含user32.dll
#define PASSWORD "3xsh0re"
int verify_password (char *password)
{
int authenticated;
char buffer[44];
authenticated=strcmp(password,PASSWORD);
strcpy(buffer,password);//over flowed here!
return authenticated;
}
main()
{
int valid_flag=0;
char password[1024];
FILE * fp;
LoadLibrary("user32.dll");//prepare for messagebox
if(!(fp=fopen("password.txt","rw+")))
{
exit(0);
}
fscanf(fp,"%s",password);
valid_flag = verify_password(password);
if(valid_flag)
{
printf("incorrect password!\n");
}
else
{
printf("Congratulation! You have passed the verification!\n");
}
fclose(fp);
}

和之前一样需要获得函数调用在内存中的返回返回到缓冲区的地址,打开x64dbg,开调!

根据之前的调试方法,我们可以找到缓冲区的地址:0000 0000 0061 F9B0,接下来就需要我们编写password.txt里的内容了,肯定是要淹没到返回地址的位置的,看看上面的堆栈信息,可以发现我们需要设置7*12345678+缓冲区地址。在Winhex里编辑如下:

调试一下,发现成功淹没

接下来就是编写我们植入的代码,我们需要定位到内存中MessageBox的地址,这里不沿用书的方法,我们直接在x64dbg中查看符号文件,搜索即可得到地址为:0x00007FFB294DA000

然后编写shellcode:这里笔者花了大量的时间来写shellcode,

第一个问题是一开始没有调高栈顶,也就是没有对rsp进行修改,导致运行时新入栈的值淹没了我的shellcode,所以调高了栈顶

第二个问题是发现无法在堆栈中执行命令,尝试在系统中关闭了DEP,但是还是不行,然后在调试器中,右键转到内存,然后右键该区域,内存权限,修改为可执行权限即可

第三个问题是发现64位的地址中会有0x00,不可避免,选择使用逻辑移位的方式构造跳转地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SUB 	rsp,64;调高栈顶,防止栈帧破坏shellcode
XOR ebx,ebx;得0
PUSH ebx
MOV rax,0x3378736830726521;!er0hsx3
PUSH rax
MOV rax,rsp;将字符串指针给rax
PUSH ebx;压入函数参数
PUSH rax
PUSH rax
PUSH ebx
MOV rax,0x7FFB294DA0;防止0x00被截断,使用移位的方式构造函数地址
SHL rax,24
SHR rax,16
CALL rax;call MessageBoxA
ADD rsp,64;回收栈顶

·

右键修改可执行权限

全选然后设置权限

发现已经执行到MessageBoxA函数处😁:

最后一个问题没有解决!为什么不弹窗!为什么!😅

第二章就结束了,虽然最后有个小遗憾,但是笔者已经花了大量的时间动手去完成任务,并且效果还不错,所以就不纠结于弹窗了哈哈😀


0Day安全软件漏洞分析第2版阅读随笔2
https://3xsh0re.github.io/2023/12/02/0Day安全软件漏洞分析第2版阅读随笔2/
作者
3xsh0re
发布于
2023年12月2日
许可协议