【逆向】栈溢出漏洞学习过程(一)通过字符串验证的简单程序分析

目录

前言:

1 准备程序

PS:翻车现场:

2 分析程序执行流程(栈溢出原理)

2.1 看代码中忘记的知识补充一下:

2.1.1 主函数中涉及的寄存器相关知识:

2.1.2 涉及到的指令

地址传送指令LEA

PTR

REP STOS 

int 3中断

段超越指令前缀

2.2 main函数中的初始化部分

2.3 scanf()函数部分

2.4 函数verify_password

2.5 回到主函数

2.6 回顾过程

3 思考利用方式

栈的存储结构     

字符数组的定义和使用

构造利用栈溢出

4 我的想法有:

4.1 修改字符串

4.2 接着向下溢出

4.2.1 首先我们要找到输出success的地址

4.2.2 准备十六进制编辑工具

4.2.3 检验结果

4.3 插入shellcode,也是覆盖EIP

4.3.1 编写shellcode

4.3.2 MessageBox函数 

MessageBox函数的调用方式

MessageBox函数的位置

完成整个调用汇编代码的编写

确定修改之后的程序的栈空间走向和EIP位置(即如何覆盖)

出现错误:地址不可读

问题排查

错误解决

总结:



前言:

学习帖子:https://blog.csdn.net/Breeze_CAT/article/details/89788864

大致流程是跟着这篇帖子走下去的,还是比较适合萌新(自己)的。

过程中有些地方看不懂需要延伸的和自己的理解,写在了下面。因为自己真的是个小白,好多不明白的东西,╮(╯▽╰)╭ 。

1.准备程序

首先打开VC6.0或者其他编辑器codeblocks等,将如下代码编译成exe文件

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#define PASSWORD "1234567"  //写入静态密码

int verify_password(char *password)//确认密码是否输入正确
{
	int authenticated;
	char buffer[8];
	authenticated=strcmp(password,PASSWORD);
	strcpy(buffer,password);  //存在栈溢出的函数
	return authenticated;
}

int main()
{
	int valid_flag=0;
	char password[1024];
	scanf("%s",password); //输入密码
	valid_flag=verify_password(password);
	if(valid_flag) //返回0代表正确,返回1代表错误
	{
		printf("incorrect password!\n");
	}
	else
	{
		printf("success\n");
	}
	//getchar();//暂停一下
	return 0;
}

一开始我选择使用的是codeblocks,运行程序结果如图:

通过观察代码可以知道,输入的数据是和定义的宏“1234567”对比的,显然当输入1234566时如下图,返回不正确的提示。

那么,当输入1234567时,可以看到程序返回了正确提示:

 

文章里讲到使用Debug编译器,大概指用debug模式下生成exe文件吧。

用OllyDbg打开刚刚编译好的exe程序如图:

接下来就是寻找主函数的入口了 ,也许是初次接触,我还是觉得这像是一门玄学。

F8(我的F8需要和Fn一起使用,比较麻烦,网上看到有教程,在开机时进入BIOS模式然后进行修改云云,我还是老老实实先点击界面的按钮吧)

==================================

PS:翻车现场:

在我使用了codeblocks编译生成的exe后,od打开程序,代码顺序流程和在VC6.0即博客中的完全不一样,找不到主函数了,

o(╥﹏╥)o,这是什么原因,

逼得我只按F7。

按了好几个轮回,都卡在直接输入了,还没看到main函数的影子然后就提示输入了,完了输入后 直接就返回结果结束了???什么情况,是我没有注意到main函数跳过去了吗?

==================================

2.分析程序执行流程(栈溢出原理)

所以现在我又老老实实选择了VC6.0编译运行出exe,跟着博客亦步亦趋的来吧!

果然换了VC编译器之后,就和博客中的一样了,

一直按F8(根据总结的经验看到GetCommandLineA后开始小心,然后看到有连续push的地方下面的call估计就是主函数了,然后F7跟进去看看是不是的。)

至于为什么codeblocks编译出来的exe程序用OD打开来和vc的不一样,现在自己还是个小白,先跟着博客正常的来一遍,然后再回过头讨论这个问题!

F8(也许碰到长的像的也要F7进去看一圈)点点点 ,也总算是找到了入口点了吧···

现在F7进入看看吧,这就是主函数的代码了,可以看到基本的输入输出函数在右边的注释中,在代码段中也可以看到call了几个函数:

老规矩,按照汇编代码画出栈图,自己分析下:

 顺便一提,改了个字体大小:

好了,现在开始看。

=============================================================

2.1.看代码中忘记的知识补充一下:

2.1.1.主函数中涉及的寄存器相关知识:

EDI 目的变址寄存器(Destination Index Register):用于指向字符串或数组的操作数。

ESI 源变址寄存器(Source Index Register):用于指向字符串或数组的源操作数。

EBX 基址寄存器(Base Address Register):用来存放存储器地址,以方便指向变量或数组中的元素。

2.1.2.涉及到的指令

地址传送指令LEA

存储器操作数具有地址属性,利用地址传送指令可以获取其地址。其中,最常用的是获取有效地址指令LEA(Load Effective Address),格式如下:

LEA r16/r32,mem   ;r16/r32=mem 的有效地址EA(不需要类型一致)

LEA将存储器操作数的有效地址(段内偏移地址)传送至16位或32位通用寄存器中,它的作用等同于汇编程序MASM的地址操作符OFFSET。但是,LEA指令是在指令执行时计算偏移地址,而OFFSET是在汇编阶段取得变量的偏移地址,后者执行速度更快。

(对于这句话的理解:比如编写汇编代码时,定数数据变量在.data部分声明了一个dvar dword 41424344h,那么在下面的代码部分两种方式都可以来获取该变量的地址,即在汇编阶段就取得的地址。而如果这个变量没有声明,在运行之前不知道这个地址,那么就只能选择LEA指令。

另外,LEA指令获取的是偏移地址,并没有读取变量内容。

不过,对于在汇编阶段无法确定的偏移地址,就只能用LEA指令获取了。

那么对于下面这条指令的意思即是

0040109C          |. 8DBD B8FBFFFF       LEA EDI,DWORD PTR SS:[EBP-448]

将EBP-448处的有效地址赋值给EDI  ---目的变址寄存器(用于指向字符串或数组的操作数)

至于其中的DWORD是双字类型,

PTR

PTR:类型转换操作符PTR是用来更改变量名的类型。

那么再来看这句话的意思就是:

将 [EBP-448]处的地址 以DWORD双字类型 存储在32位目的变址寄存器EDI中,让EDI指向[EBP-448]处(即栈中变量存储的区域)。

接着又看到一个陌生的指令:

REP STOS 

004010AC          |. F3:AB               REP STOS DWORD PTR ES:[EDI]

 网上查阅资料:参考博客:https://www.cnblogs.com/yuqiao-ray-vision/p/3754856.html

该指令是:

rep指令的目的是重复其上面的指令ECX的值是重复的次数.


STOS指令的作用是将eax中的值拷贝到ES:EDI指向的地址.

(如果设置了direction flag, 那么edi会在该指令执行后减小, 
如果没有设置direction flag, 那么edi的值会增加)

 那么分析一下这行命令的意思就是:

REP  :  重复0x112(十进制274)次,即274*4=1096个字节(一个EAX寄存器存储的是32位4个字节)

STOS:将EAX中的CCCCCCCC(8个C)赋值给EDI指向的地址,EDI的值增加,指向高地址,向下继续填充274次,将这些栈中的空间都赋值为CCCCCCCC。

int 3中断

值得一提的是0xCC是int 3中断,网上看了一些论坛 大多都是2009年的回答,可能那个时候就有了吧。如下:

网址:https://bbs.pediy.com/thread-103395.htm

就是汇编中的一个中断指令,原来的DOS就是一种单任务系统,如果执行一个任务而要想执行另外一个任务就需要中断当前执行的任务所以就有了中断指令!

 int 3是引起中断的指令
在内存中的显示就刚好是CC  你看到的那么多int 3就是某一连续填充CC的空间  理论上这些地方是不会访问到的  一旦访问到了就是程序逻辑错误 所以填充CC(int 3) 被访问则产生告警

int 3断点,程序运行到这里就会触发调试器 

大概理解了何为int 3。希望以后遇到具体事例的时候再加以分析。 

段超越指令前缀

只能跟随在具有存储器操作数的指令之前的指令。如:

SS:[EBP-448]

CS是代码段寄存器          CS:  读取指令                    偏移地址:EIP 指令指针寄存器
SS是堆栈段寄存器          SS:  堆栈操作                    偏移地址:ESP 堆栈指针寄存器
DS是数据段寄存器          ES:  一般的数据访问         偏移地址:有效地址EA

============================================================= 

好了,预备知识回顾完了,开始分析: 

2.2 main函数中的初始化部分

下面是对应OD中给scanf()函数push参数之前(用红色框圈住的地方),

代码的分析,自己画的图,(图一),加深下理解 (不是很好看,一会下面的函数尽量画的再整齐些。)

(在博客中看到其实可以从OD界面右下角观察栈的存储情况,不用像我这么麻烦,不过考虑到自己太小白,还是手工先理思路流程吧。)

                                                                                                (图一) 

  •    中间的一段初始化代码为初始化申请的空间为0xcc,可以看到从[EBP-448]处开始向下循环赋值1096个字节正好对应(图一)中粉红字体画出来的空白处1096个字节
  • 其中valid_flag(即SS:[EBP-4],第一个变量,对应源程序主函数中的valid_flag变量)初始化为0。  

 下面是初始化后的结果如图:

到了这里,该执行下面对scanf()函数的参数的压栈操作了。

2.3 scanf()函数部分

“从右至左的顺序将参数(password就是[local.257]和“%s”)入栈之后调用scanf。”

不他这里的[local.257],在我这里应该是SS:[EBP-0x404]

  • 从上图代码可以看到,先压进去了刚刚的EAX([EBP-0x404]指向的栈中变量位置),应该是用来作为变量存储输入的字符串,
  • 调用完scanf()函数后,[EBP-0x404]处以及下面1024(0x404是1028,还要减去4(valid_flag))个字节,是我们输入的字符串password(我们在源程序中定义的是password[1024])的内容。如下图。

调用scanf()函数时,在命令窗口随意输入了数值1234566如图:

代码到了这里:

看到调用完scanf函数使用ESP,8清理掉输入的参数后,又入栈了参数,调用了一个新的函数。

2.4函数verify_password

我们F7步入跟踪看一下:进入后看到这应该就是我们写的函数verify_password的了。

对应代码如下图:

下面是代码的画图分析:

  • 图中步骤1-3是verify_password()函数的初始化
  • 步骤4-6是 verify_password()函数体中调用的strcmp()比较函数的执行过程,返回值放在了verify_password()函数栈帧中的[新EBP-4]的位置
  • 步骤7-9是verify_password()函数体中调用的strcpy()拷贝函数的执行过程,返回值放在了verify_password()函数栈帧中的[新EBP-C]开始共8字节的位置。(蓝色闪光笔填充的地方)
  • 步骤10是verify_password()函数的清空撤退操作,其中注意的是:

  • 在末尾,MOV EAX,DWORD PTR SS:[EBP-4]
  • verify_password()函数将strcmp()字符串比较函数的返回值用EAX带了出去(带回主函数)

2.5 回到主函数

现在我们重新回到主函数,代码如下(F8一直向下走就好了):

我们可以看到在ADD ESP,4 清空verify_password()函数之前push进去的参数ecx的值后,栈帧回到了我们(图一)的状态。

下面的就是赋值操作了,在我画的图中也有表明,将从 verify_password()函数带出来的返回值存在了main函数的第一个变量即那个判断标志valid_flag中:

在这里闪光蓝色笔就是最新的操作了。当然这里的valid_flag可能不再是0了,毕竟上面我们输入的是1234566。

在这后面可以看到是一个if语句判断标志是否为0即是否相等,然后对应不同的输出。当然这里就不再是我们关注的重点了。

2.6 回顾过程

  到此,整个代码的大致流程已经梳理清楚了,关键的地方可以看到在verify_password()函数中strcpy函数的返回值那里 。

3.思考利用方式

那么,根据栈溢出的原理,我们可以知道,如果strcpy()中的赋值变量(password)超过被赋值变量(buffer)的空间,那么它就会向下溢出(向高地址处),这时,存储在他下面的正好是上面strcmp()函数的返回值,

经过上网查找可以知道

strcmp()函数的返回值有三种情况,

相等为0                                                                 在寄存器中的存储值是0x00000000

当不为0即不相等的时候:

  • 如果参数大于0是1,   是一个正数                        (下面程序返回的是1)
  • 如果参数小于零,返回值小于0。                          (下面程序返回的是0xffffffff)

但是注意百科中: 

栈的存储结构

中间我们来分析一下关于栈的存储结构,以及数据的存储的字节顺序 

     

              首先,栈是高地址向低地址生长,压栈操作为4 字节对齐。

             从下图栈的结构,这里输入的数据是1234566(字符串)(存储的是ASCII码0x31323334 35363600),

其中0x35363600是低四字节但是放在了高地址 (和代码压栈的顺序有关,应该是先压入了低四字节)。(放在了下面)

              “在寄存器中,一个字的表示是右边应该属于低位,左边属于高位,如果寄存器的高位和内存中的高地址相对应,低位和内存的低地址相对应,这就属于小端存储。反之则称为大端存储。大部分处理器都是小端存储的。

                 注意的是:字节序跟变量是保存在全局数据区还是在栈区是没有关系的   

                一般x86平台都是小端序,也就是高高低低,即:高位字节存放在内存高地址处,低位字节存放在低地址处。对于整型变量a=0x12345678,其内存布局为78 56 34 12
大端序一般在以太网通信中,所以又叫网络字节序,因此上面的变量a在大端序系统上就是12 34 56 78

此外,栈的生长方向在x86平台都是从高地址往低地址方向,在其他平台则不一定,例如,在ARM平台上,栈的增长方向是可以配置的  。”
     
  

               然后,再在这四个字节中看,这四个字节是char类型即字节类型,所以是从左到右是挨个存的。在内存中即从地低址开始存,(存在数据段,存放数据是由低地址向高地址的)。

       内存中四字节单字符(byte类型)0x35 36 36 00,是由低到高地址挨个存之后 :0x35 36 36 00,注意在栈中这一行显示是00 36 36 35,是由高地址向低地址显示的(这四字节)。应该是为了方便阅读。

      比如:存一个四字节数据0x35 36 36 00(DWORD类型),那么低字节低地址,在数据区显示由低到高是00 36 36 35.在栈中就是35 36 36 00(为了方便阅读,右边是低地址)

               可以看到采用的是小端存储:低字节放在了内存低地址处。

             (这里我后来看自己的博客有些问题没讲清楚,在上面我做了修改,在下面我也补充了关于strcmp返回值的存储问题)

有了上面的基础知识后,我们再来分析:

这里输入的是字符1234566,最后一个是结束符‘\0’,存在里面显示00。(从右向左存储)

可以看到,程序中定义的是buffer[8]但是字符串只能输入七个字符,第八个字符是'\0'。

字符数组的定义和使用

这和字符数组的定义和使用有关:

引用二级C++参考书中的话:

在C++中空字符('\0')用来作为一个字符串结束的标志。字符串以空字符结尾,即字符串的最后一个字符总是空字符。而字符数组可以不含空字符。如:char a[5]={'a','b','c','d','e'};。

字符串可以存储在一维数组内。如char[6]="Hello";字符串需要6个char存储空间。

  • 所以,如果输入十一个字符如输入12345678912(十六进制是),那么多出来的字符912和'\0'共4字节,以4个字节为单位压入栈,所以这个多出来的双字量存储的是0x00323139(从低地址到高地址)
  • 那么针对strcmp的返回结果,可以知道如果输入大于1234567的时候,返回结果是0x00000001,所以只要输入八个字符,这样字符串结尾的'\0'即0x00就会向下覆盖到0x01。

在这里注意到怎么快速查到数据窗口对应地址的值,即如何找到这个地址?

在下面信息栏中有提示,注意看就可以了。然后复制地址到下面的数据区域go to查找即可。

例如输入1234566时,比1234567小,看到返回0xFFFFFFFF ,下图是在verify_password()函数中刚调用完strcmp和strcpy函数后数据窗口中的情况

输入一个大于1234567的字符1244566,我们看到这一行是FFFFFFFF,(左面低地址右面高地址) 

返回值问题: 

那么如果输入一个大于1234567的字符1244566:

那么为什么在栈中显示的是0x000000001而在数据区显示的是0x01000000?strcmp函数的返回值是什么?

首先要明确一个概念:在数据区的确是从左到右从上到下都是低地址到高地址显示的。

如下图分析: 返回值的确是0x00000001(DWORD类型),但是在这个DWORD类型中低字节对应低地址,0x01如图在上一个数据0x00的上面(也是后面插进去的)

所以计算机中存储的的确是0x01000000。

但是栈区这样显示:

 每一行左边是高地址 右边是低地址,所以显示的是0x00000001。是OD为了方便阅读而这样显示的。但是在内存中存储都是一样的 ,只是显示不同。

造利用栈溢出

所以,到了构造利用栈溢出的环节了。

针对这种情况,如果想要绕过验证,则需要使strcmp()返回的eax的值变成0,即需要strcpy的返回值溢出覆盖住下面strcmp的返回值。让这个地方变成0.(这样说比较抽象,可以结合我手动画的图倒数第二张用蓝色闪光笔标注的地方查看)

至于为什么会向下溢出而不是向上溢出,想了下,应该是和他push进参数的方式有关。他是从低地址向高地址push进去的,回过去看上面的代码(或者我画的图)我们可以发现,他是让地址不断增加赋值的。所以如果超过的话,会继续向下赋值溢出。

4.想法有:

4.1.修改字符串

1.可以输入XXXXXXXX>=1234567,输入任意大于1234567的八个数,这样字符串的结尾有个‘\0’字符,会覆盖下面的数字。

2.输入负数需要覆盖0xffffffff ,则前七位数可以为小于1234567,接下来再输四数字0。

(注意strcmp的返回值即valid_flag的值是int型占4个字节!)

验证结果:

1.任意大于1234567的八位数字成功!

2.输入任意7位数字+四个0

这里返回结果不正确,和我想象的不一样,是什么原因?。。。

知道了,这里定义的password是char类型

所以,试试输入输入任意7位数字+四个'\0'(即四个空值)

'\0'的ASCLL码是0

但是'\0'没办法输入。

所以这种想法pass掉。

所以说对于仅仅想从利用输入一个字符串利用strcmp的返回值来通过的话,这个程序下,应该只能通过输入一个前7位大于1234567的八位数字,或者可以后面加几个0,但不要超过11位。

4.2接着向下溢出

那么是覆盖到哪里呢?博客中说道:

可否直接覆盖到返回地址到“输出success”的地方,也就是说我们要先找到输出success的位置。 

4.2.1首先我们要找到输出success的地址

ollydbg看一下:

   即直接跳到压入success的地方,然后向下执行。这里的地址是0x004010D0。所以接下来我们就是要让返回地址覆盖成这个地址。应该是指verify_password函数的EIP。

      在C/C++中在调用函数之前会保存当前函数的相关环境,在调用函数时首先进行参数压栈,然后call指令将当前eip的值压入堆栈中,然后调用函数,函数首先会将自身堆栈的栈底地址保存在ebp中,然后抬高esp并初始化本身的堆栈,通过多次调用最终在堆栈段形成这样的布局 

    call指令的实质是 push eip和jmp addr指令的组合,并不一定非要调用函数。call指令的大小为5个字节,所以call $ + 5表示先保存eip在跳转到它的下一跳指令处。这样就可以有效的避免检测到GetThreadContext中的相关函数调用。 

参考:https://blog.csdn.net/lanuage/article/details/52203447

在我自己画的图中更可以看到如下图:需要覆盖20字节到EIP,然后EIP中压入刚刚我们看到的success的地址。

博客中讲到:

由于命令行中只能输入ASCII码,有些16进制值无法用ASCII码表示出来,需要稍微修改一下函数代码,把手动输入改为从文件输入,但修改之后刚找到的地址也会改变,不过已经很明显了,接下来再找到也不会很难,代码修改如下:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#define PASSWORD "1234567"  //写入静态密码

int verify_password(char *password)//确认密码是否输入正确
{
	int authenticated;
	char buffer[8];
	authenticated=strcmp(password,PASSWORD);
	strcpy(buffer,password);  //存在栈溢出的函数
	return authenticated;
}

void main()
{
	int valid_flag=0;
	char password[1024];
	FILE * fp;
	if(!(fp=fopen("password.txt","rw+")))//文件的绝对路径或相对路径
	{
		exit(0);
	}
	fscanf(fp,"%s",password);
  //scanf("%s",password);
	valid_flag=verify_password(password);
	if(valid_flag)
	{
		printf("incorrect password!\n");
	}
	else
	{
		printf("success\n");
	}
	fclose(fp);
	getchar();
		getchar();
}

然后准备password.txt:

4.2.2准备十六进制编辑工具

下载安装UltraEdit的教程:

 https://blog.csdn.net/u012803478/article/details/80802594

结合https://blog.csdn.net/u013132035/article/details/54971503

成功安装。

先写一个正常的password.txt放在工程debug目录下,这样就不用绝对路径了:

ollydbg调试,再看一下新的输出success的位置:

现在的位置变成了这个0x0040FBAF。

之后我们将文件password.txt使用十六进制编辑器打开,构造20字节的内容,并且最后四字节是要覆盖的地址(反着写):

 

保存时总是出现这个错误:错误创建.bak的话

 在高级->配置中找到不备份确,定就好了

4.2.3检验结果

重新运行程序,检验结果的时候到了

OD调试基本上和上面的代码差不多有少许不同但是应该不用赘述,有了前面的铺垫,应该也很容易调试出来具体的流程,比如这个函数一开始多了几个输入函数和判断条件,在OD中注意区分,找到对应栈的地址查看就好了。如下图,箭头指的函数才是verify_password()函数。

调试到verify_password()函数的参数部分时,要开始小心注意观察栈中的变化:

在strcpy()之前:

覆盖后:

可以看到EIP部分已经被成功的覆盖。我们看看执行结果:

可以看到执行成功,直接RETN调过来输出success了! 

(在这里我对前面画图中的关于EBP理解错误的部分改了一下:每次函数压入的EBP是一个值换句话说是一个地址,这个地址是原来函数指向EBP的指针的地址,不知道我这样说对不对。大概就是因为调用完函数需要POP EBP,让EBP指向原来指向的位置。     --------如果这样理解还是有问题,以后我再来修改措辞吧╮(╯▽╰)╭)

之后继续运行会报一个错误,是因为我们覆盖过程中将原EBP也覆盖了,导致返回main之后找不到EBP,找不到EBP就会在返回的时候找不到之前的返回值,但这并不影响我们成功输出了success。

4.3 插入shellcode,也是覆盖EIP

 在这种方法中和上面往下溢出字符串感觉大同小异。只不过是自己写了一段可执行代码,可以选择在OD中编写汇编代码,会变成十六进制格式,然后将shellcode写在字符串里,后面跟着一堆NOP直到EIP位置写入buffer(verfiy_password()函数的strcpy()函数的存储变量)的地址。然后执行应该就可以了。这里我还有个想法,既然都已经在OD中编写了,应该是有地址的吧,那和上面字符串溢出一样,最后EIP写shellcode的地址不就行了?

下面来逐个验证实验下:

4.3.1 编写shellcode

首先修改代码如下:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<windows.h>
#define PASSWORD "1234567"

int verify_password(char *password)
{
	int authenticated;
	char buffer[60];
	authenticated=strcmp(password,PASSWORD);
	strcpy(buffer,password);
	return authenticated;
}


void main()
{
	int valid_flag=0;
	char password[1024];
	FILE * fp;
	LoadLibrary("user32.dll");
	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("success\n");
	}
	fclose(fp);
	getchar();
}

4.3.2 MessageBox函数 

博客中的原话:

主要修改的地方是verify_password函数中buffer的空间由8改为了60,以便我们在里面写入代码;

在主函数中增加了一句 LoadLibrary("user32.dll");加载user32.dll模块,之后我们写入的代码要调用这里面的MessageBox函数。

所以,我们要通过向栈中写入代码的方式,来让程序弹出一个消息窗(也就是说写入一个消息窗的代码并让它执行)。

确定下面几件事:

  • MessageBox函数的调用方式
  • MessageBox函数的位置
  • 完成整个调用汇编代码的编写
  • 确定修改之后的程序的栈空间走向和EIP位置(即如何覆盖)
  • MessageBox函数的调用方式

int MessageBox(
    HWND,//这个参数代表窗口所属,如果为NULL则代表不属于任何窗口
    LPCTSTR,//这个参数代表消息框中显示内容的字符串
    LPCTSTR,//这个参数代表消息标题显示内容的字符串
    UINT//这个参数代表框的风格,NULL为默认
)

  • MessageBox函数的位置

MessageBox函数是系统通过对中间两个字符创参数的类型(ASCII或UNICODE)来决定调用MessageBoxA或者是MessageBoxB,我们这里使用ASCII型字符创,那么我们可以直接去寻找MessageBoxA的地址,寻找方式如下:

1.在vc6.0的Common中的Tools文件夹下名字是DEPENDS.EXE 

2.之后随便将一个“有窗口的软件”拖进工具,然后我们查找user32.dll中的MessageBoxA,找到user32.dll的基地址和MessageBoxA的偏移地址: 

 

关于拖进去的窗口程序:这里因为正好之前逆向分析课程写过一个简单的hello.exe窗口程序,把代码分享在下面:

extern MessageBoxA

section .text
global main

main:
    push dword 0
	push dword title
	push dword text
	push dword 0
	call MessageBoxA
	ret
	
	section .date
	title: db 'MessageBox', 0
	text: db 'Hello World!', 0

 相关工具的安装使用还有编译方法:我之前有写过一个博客,这里就不赘述了:

https://blog.csdn.net/qq_43633973/article/details/100516084

计算可得MessageBoxA的地址为:0x69E00000+0x00077E60=0x69E77E60。之后我们进行汇编编码(汇编代码可以在od中选择一块区域nop掉然后编写,会自动生成二进制格式):  

  • 完成整个调用汇编代码的编写

(汇编代码可以在od中选择一块区域nop掉然后编写,会自动生成二进制格式):  

这个地方一开始踌躇了很久,应该就是随便在OD中找一块区域,(只是借助它编写命令获得机器码(16进制))

博客中的代码拿过来参考下:

      33DB          xor ebx,ebx   //这里是为了让ebx为0,因为有两个参数值是NULL并且字符创需要一个结束符,如果直接mov ebx,0会使二进制代码中出现0,在strcpy时会被认为字符创结束符而结束复制。所以使用这种方式。
      53            push ebx      //先入栈一个0作为窗口显示字符创的结束符
      68 6f50776e   push 0x6e77506f  //这两句是构造窗口显示的字符创,我们这里让窗口都显示“HelloPwn”
      68 48656c6c   push 0x6c6c6548
      8BC4          mov eax,esp    //字符串(字符串开始地址就是esp栈顶)给eax
      53            push ebx       //四个参数从右至左依次入栈
      50            push eax
      50            push eax
      53            push ebx
      B8 EA07d577   mov eax,0x77d507EA   //将MessageBoxA的地址给eax
      FFD0          call eax             //调用

在OD中打进去,注意地址。

在UD中可以制作字符并转换字符的16进制如图:博主写的是HelloPwn,我写了自己的称呼

可以获得push进去的字符串的16进制注意push顺序,可以画图分析,根据上面我们分析过的栈中的存储方式,我们可以知道他在栈中的形式如下:(四字节一组,按照顺序向下排,四个字节中逆向存储)但是对应的我们自己的push操作如果想要达到这种效果,则需要又下至上(高地址向低地址)push进去。

所以压入的顺序是

push 6C6C6C6F

push 6C6C6548

成功获得这部分指令的机器码

33 DB 53 68 6F 6C 6C 6C 68 48 65 6C 6C 8B C4 53 50 50 53 88 60 7E E7 69 FF D0

 如果在整个过程中出现了00(一般是由于数字出现00造成的),那么我们应该换一种表达方法,如(上述代码中使用了xor指令也是可以的):

      B8 5b000400   mov eax,0x0004005b
      改为
      B8 6b101410   mov eax,0x1014106b
      2D 10101010   sub eax,0x10101010

  • 确定修改之后的程序的栈空间走向和EIP位置(即如何覆盖)

 接下来我们确定栈空间,我们将6组“1234567890”写在文件中,然后在OD中查看,还是在执行完strcpy之后,查看栈空间,strcpy前:

 (这个函数的buffer[60]有60字节)

编辑文本:

打开OD调试运行:可以看到在strcpy执行前:

在strcpy执行后: 

 第61个字符串结束符00覆盖了authenticated的最后一个字节01(和上面讲的原理一样),下面就是EBP和返回地址

 我们要做的是:

所以我们需要做的就是在文件中构造如下的内容,

即先写代码,然后用0x90(nop)填充

在69~72字节处填写buffer的初始地址,用来覆盖返回值,直接返回到我们写的代码上执行,

就是上图的0x0019FA9C,

构造的文件:

采用16进制编写

 格式:刚刚上面获得的机器码(正序即可)+很多个NOP正好覆盖到EIP的上面,自己查+0x0019FA9C(逆序存放)

打开UD编写如下:(自己纯手敲上去的,一复制就变成10进制了,麻烦有人看到我的博客,知道快捷复制方式可以告诉我吗?不胜感激。)

下面调试看看吧!

依旧是strcpy前的情况:

 

 执行完strcpy后:不知道为什么没有正确显示弹框

去检查一下机器码是不是写错了。

找到一个错误把B写成了8

 出现错误:地址不可读

报错:地址不可读,at address is not readable pass exception to program

看来程序代码书写的都没问题了,是这个地址的问题了,那么再来检查一遍这个69E77E60是不是不对:

================?================================待解决==========

================回来了==============

问题排查

今天早上起来发现又有一个拼写错误。我在password里面写成了33D8应该是33DB!

修改后:

又检查了一遍地址,还是感觉没问题 啊 

 又去回过头看了是不是因为自己不是debug编译?

如果要改变当前工程的版本配置,可以在工具栏上点右键,选中“build”选项(不是“mini build”),然后在该工具栏上有一个选择编译版本的下拉框,可以选在“win32 Debug”或“win32 Release”。

 在VC中F10出现这个:网上查了查说好像并没有什么实际影响:

https://www.52pojie.cn/thread-956567-1-1.html

https://bbs.csdn.net/topics/390874574

好像是user32.dll的问题:

 我把程序拖进Dependency Walker中,发现没有找到user32.dll

查看源程序:

发现是user32.dll的问题?

查阅资料:

https://www.cnblogs.com/Braveliu/p/9342633.html

https://bbs.csdn.net/topics/391035554?list=3425870




 尝试了一圈好像没什么用,我直接在代码里写了个MessageBoxA()函数:

发现是可以调用成功的,我也看了,不存在64位程序引用user32.dll失败的问题,上面的连接中有一个办法是把user32.dll拷贝到如下图的文件夹下面,但是我已经找到了,而且引用的也是正确的。

加了这个函数之后生成的exe我再拖进dependency看看:

额,发现出现了,而且地址还是上面的地址,看来这个地址是固定的,应该没问题。而且加入了windows.h头文件应该就是相当于可以使用user32.dll的吧,因为可以使用这个函数。

 ==??

错误解决

又看到一个希望,试一下:

打开虚拟机的xp系统,里面放进去我们的VC++6.0,32位OD(很重要,不然无法调试,这里选择了吾爱破解版的OD),准备test程序代码和password。

查看messageboxA地址

 发现地址是0x77D10000+0x0004058A=0x77D5058A

另外注意到代码段的地址也改变了,需要相应的进行修改

起始地址变成了0x0012FADC 

修改后调试。

成功啦!

 总结:

换到XP系统中,使用吾爱破解版OD,VC++6.0软件,按照流程走就不会出现乱七八糟的问题了!

至于WIN10下VC6.0编译的exe为什么调用不了MessageBoxA说地址不可读,猜测和权限之类的有关系。

好啦,目前告一段落啦。画下一个可怜的句号~

======时间的分割线====2019-09-27======

我又回来了,写作业的时候突然发现,之前忽略的一个小点竟然浪费了我2,3个小时!不记下来难受。

也没啥,就是不知道有没有注意到一个问题:

比如用vc6.0或者其他编译器编译文件的时候,如果需要打开一个文件只用文件名,(相对地址),想在编译器通过的话就放在那个project的文件夹下,不用放在debug中如图:

但是如果就这样踏上OD去调试,你会发现怎么调试都很奇怪!程序莫名其妙的就结束了,分析来分析去,发现在判断文件是否为空那里出现了问题!

就是说在OD中他说找不到文件,这个时候你就要把password挪一下位置了,放到debug文件夹中,如图:

(在OD中是这样的,图是截的codeblocks创建的project,但应该都差不多,一个小细节,当然如果文件路径填写绝对地址就不用这么麻烦了。) 

》》=========结束=====我接着去写作业了===========================

发布了68 篇原创文章 · 获赞 20 · 访问量 6865

猜你喜欢

转载自blog.csdn.net/qq_43633973/article/details/100984653