改变程序流程的一些简单办法

zhyjc6于2019-10-30发布 约4846字·约10分钟 本文总阅读量

背景

这是一篇很基础的介绍如何更改函数流程的文章,本来只是一个简单的实验,我可以草草应付了事,但是为了更好地分析问题,写出更高质量的文章。我在调试过程中还是有了不少的收获。

目标

  1. 通过对程序输入的密码的长度、内容等修改用Ollydbg来验证缓冲区溢出的存在
  2. 完成淹没相邻变量改变程序流程实验
  3. 完成淹没返回地址改变程序流程实验

步骤与结果

程序源码

两个程序功能是完全一样的,只有输入方式不同,一个是读取文件输入,一个是直接在窗口获取输入。

stackvar实验源码

#include <stdio.h>
#include <string>
#define PASSWORD "1234567"
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);
}

overflowret实验源码

#include <stdio.h>
#include <string>
#define PASSWORD "1234567"
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);
}

验证缓冲区溢出

  1. 首先我们使用 OllyICE 打开程序,输入几个错误密码和正确密码,查看程序是否正常,当输入的密码正确时,程序返回了正确响应。

  2. 输入一段较长的密码,查看是否有缓冲区溢出。输入较长字符串时程序报错,显然是存在缓冲区溢出。因为地址31313131就是我们输入的1111,它能跳转到我们输入的地址,也就是说只要我们输入正确的可执行地址,我们就可以控制程序流程。

淹没相邻变量

我们先观察程序源码,从源码我们就可以直接看出:我们输入的字符串被指针 password 传入密码验证函数,密码验证程序先是定义了一个int型的变量,然后是一个8字节的缓冲区,之后使用传入的指针把我们的输入字符串复制到缓冲区,但是没有复制大小限制。所以我们输入的字符串够长,当缓冲区填满了之后,下一个地址就是int型变量的地址了,这个变量就是最终的返回值。我们只要把这个变量更改为零,整个函数的返回值就是零了,从而达到更改程序运行流程的效果。但是要把整个INT型变量赋值为零,就需要四个字节的0x00,但是字符串会被第一个0截断,所以我们最多只能赋值一个字节的0.那么这就比较棘手了,除非已经有三个字节为零了!

把程序拖入ollyICE,运行程序,输入aaaaaaa运行到该函数处,让程序运行到strcpy函数刚刚结束的位置:

查看堆栈:

同样的步骤,输入1111111,运行到同样的地点,查看堆栈:

我们惊喜地发现,当我们输入的字符串大于“1234567”时,INT 型变量值为00000001,当小于时其值为FFFFFFFF,所以我们就构造一个大于“1234567”的字符串,让其只有一个字节为1. 但是这到底是为什么呢?具体原因我们得分析其strcmp函数:

strcmp的汇编代码:

00401370 >/$  8B5424 04     mov     edx, dword ptr [esp+4]    //参数2地址入栈
00401374  |.  8B4C24 08     mov     ecx, dword ptr [esp+8]    //参数1地址入栈
00401378  |.  F7C2 03000000 test    edx, 3	//验证地址后2位是否为0,
0040137E  |.  75 3C         jnz     short 004013BC    //不为0说明字符串起始地址不是四字节的开头,具体是第几字节继续验证
00401380  |>  8B02          /mov     eax, dword ptr [edx]    //参数2的值赋值给EAX
00401382  |.  3A01          |cmp     al, byte ptr [ecx]    //比较第一个字节
00401384  |.  75 2E         |jnz     short 004013B4    //不等则跳转
00401386  |.  0AC0          |or      al, al    //如果为0说明字符串结尾,则跳转到函数返回
00401388  |.  74 26         |je      short 004013B0
0040138A  |.  3A61 01       |cmp     ah, byte ptr [ecx+1]    //比较第二字节
0040138D  |.  75 25         |jnz     short 004013B4
0040138F  |.  0AE4          |or      ah, ah    //如果为0说明字符串结尾,则跳转到函数返回
00401391  |.  74 1D         |je      short 004013B0
00401393  |.  C1E8 10       |shr     eax, 10    //右移16位,用于比较3、4字节
00401396  |.  3A41 02       |cmp     al, byte ptr [ecx+2]  //比较第三字节
00401399  |.  75 19         |jnz     short 004013B4
0040139B  |.  0AC0          |or      al, al    //如果为0说明字符串结尾,则跳转到函数返回
0040139D  |.  74 11         |je      short 004013B0
0040139F  |.  3A61 03       |cmp     ah, byte ptr [ecx+3]    //比较第四字节
004013A2  |.  75 10         |jnz     short 004013B4
004013A4  |.  83C1 04       |add     ecx, 4    //取接下来的四字节
004013A7  |.  83C2 04       |add     edx, 4    //取接下来的四字节
004013AA  |.  0AE4          |or      ah, ah    //如果为0说明字符串结尾,则跳转到函数返回
004013AC  |.^ 75 D2         \jnz     short 00401380    //第四字节不为/0则字符串还没结束,继续比较

004013AE  |.  8BFF          mov     edi, edi    //字符串相等退出
004013B0  |>  33C0          xor     eax, eax    //eax=0
004013B2  |.  C3            retn
004013B3  |   90            nop
004013B4  |>  1BC0          sbb     eax, eax    //字符串不等退出,eax = eax-eax-cf
004013B6  |.  D1E0          shl     eax, 1    //左移一位,最高位进入cf,最低位补0
004013B8  |.  40            inc     eax    //加1
004013B9  |.  C3            retn
004013BA  |   8BFF          mov     edi, edi
004013BC  |>  F7C2 01000000 test    edx, 1    //验证地址最后1位是否为0
004013C2  |.  74 14         je      short 004013D8    //如果为0那么最后两位就是10,模4余2,说明字符串起始地址位于某一个四字节中的第三字节(注意小端存储)
004013C4  |.  8A02          mov     al, byte ptr [edx]    //否则的话就是01,第二字节
004013C6  |.  42            inc     edx
004013C7  |.  3A01          cmp     al, byte ptr [ecx]
004013C9  |.^ 75 E9         jnz     short 004013B4
004013CB  |.  41            inc     ecx
004013CC  |.  0AC0          or      al, al
004013CE  |.^ 74 E0         je      short 004013B0    //字符串结尾
004013D0  |.  F7C2 02000000 test    edx, 2    //test 1得到最后一位为1,加1又为0,这里测试倒数第二位
004013D6  |.^ 74 A8         je      short 00401380    //为0,则最后两位为00,回归正常轨道 
004013D8  |>  66:8B02       mov     ax, word ptr [edx]    //否则就是10,第三个字节
004013DB  |.  83C2 02       add     edx, 2
004013DE  |.  3A01          cmp     al, byte ptr [ecx]
004013E0  |.^ 75 D2         jnz     short 004013B4    //不等,退出
004013E2  |.  0AC0          or      al, al
004013E4  |.^ 74 CA         je      short 004013B0    //字符串结尾
004013E6  |.  3A61 01       cmp     ah, byte ptr [ecx+1]
004013E9  |.^ 75 C9         jnz     short 004013B4    //不等,退出
004013EB  |.  0AE4          or      ah, ah
004013ED  |.^ 74 C1         je      short 004013B0    //字符串结尾
004013EF  |.  83C1 02       add     ecx, 2
004013F2  \.^ EB 8C         jmp     short 00401380    //10+10=100回归正常轨道
004013F4      CC            int3

以上分析表明,我们输入的字符中第一个报错的字符大于正确密码对应的字符,那么我们的返回值EAX就会为1,这是我们想要的结果。

那么如何覆盖这最后的一个字节呢?其实很简单,我们只需要把8个字节的缓冲区填满,保证第9个字节为零就OK了!

由于字符串末尾有一个不显示的 /0 ,所以我们只需要任意输入一个大于“1234567”(字典排序比较法)的8个字符组成的字符串就可以成功覆盖啦!

下面我以8个‘2’为例:

程序进入密码验证函数中的strcpy函数之后:

we did it!

淹没返回地址

我们使用文件输入的那个文件来利用。首先查看源码:

可以明显的看出该函数的栈底应该是以下布局的:

缓冲区 buffer
缓冲区 buffer
INT型变量 authenticated
EBP栈底 0x
返回地址 0x
函数参数 password

从上面的分析来看,我们的返回地址就在缓冲区填满(8字节)+ int 变量(4字节)+ EBP(4字节)的后面。

所以我们填充16个字节,而第17-20个字节就是我们在返回地址处覆盖的新地址,函数结束后就会返回到该新地址执行程序。如果有偏差我们再继续调整。

那么我们想要跳转到哪里呢?

我们的初心是绕过密码验证程序,那么我们就跳转到输出密码验证成功的printf函数的地址吧!

所以我们的shellcode就是: 16个任意字符+“0040112f”

当然地址是不能这样输入的。我们使用010 editor编辑shellcode:

注意此处的顺序,由于系统是小端存储,所以我们的低位就要放在前面。

测试:

strcpy函数之后,返回地址已被我们覆盖:

继续运行,Congratulation!