背景
这是一篇很基础的介绍如何更改函数流程的文章,本来只是一个简单的实验,我可以草草应付了事,但是为了更好地分析问题,写出更高质量的文章。我在调试过程中还是有了不少的收获。
目标
- 通过对程序输入的密码的长度、内容等修改用Ollydbg来验证缓冲区溢出的存在
- 完成淹没相邻变量改变程序流程实验
- 完成淹没返回地址改变程序流程实验
步骤与结果
程序源码
两个程序功能是完全一样的,只有输入方式不同,一个是读取文件输入,一个是直接在窗口获取输入。
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);
}
验证缓冲区溢出
-
首先我们使用 OllyICE 打开程序,输入几个错误密码和正确密码,查看程序是否正常,当输入的密码正确时,程序返回了正确响应。
-
输入一段较长的密码,查看是否有缓冲区溢出。输入较长字符串时程序报错,显然是存在缓冲区溢出。因为地址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!