avatar

目录
有趣的二进制(修改函数返回地址)

上次分析的突破密码验证中,通过改写领近变量实现密码突破验证。

这次碰到一个更好玩的,也是通过栈溢出,当字符达到一个上限时,EIP寄存器中的地址会被覆盖,文字解释不清,直接上操作吧。

正文

首先来看源码, 源码是上一篇文章中的源码, 只是我对他进行了改写。

c++
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
34
35
36
37
38
39
40
41
42
43
#include <stdio.h>
#include <string.h>
#include <iostream>
#define PASSWORD "1234567"
using namespace std;
int verify_password (char *password)
{
int authenticated;
char buffer[8];// add local buff
//strcmp比较两个字符串并根据比较结果返回整数
//strcmp(str1,str2),若str1=str2,则返回零;若str1<str2,则返回负数;若str1>str2,则返回正数
cout << ("authenticated:")<<&authenticated <<endl;
cout <<("buffer:")<<&buffer<<endl;
authenticated=strcmp(password,PASSWORD);
//strcpy把含有'\0'结束符的字符串复制到另一个地址空间,返回值的类型为char*。
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 passed the verification!\n");
break;
}
}
}

EIP地址冲刷

通过直接测试可以看出来,当输入27个字符时,程序就会崩溃。

所以这时候把程序拖入od加载,分析一下为什么会崩溃。

OD界面:

首先点击E,找到函数入口模块,然后往下划在比较函数和复制函数处下断

因为程序崩溃是在执行了复制函数后才会发生的,所以当输入27个字符后,就可以F8步过比较函数的call,一路执行到复制函数call的下一句mov语句处停下。

源码中我输出了两个变量的地址,当跟到mov语句时,因为已经执行了复制函数,所以这时在堆栈窗口观察buffer变量的地址鼠标点一下堆栈窗口,快捷键Ctrl+G,输入buffer的地址就可以跟踪

然后对栈顶进行锁定,这样子即使继续执行,也可以继续观察。

因为输入的是27个字符,且都是重复的4321,所以第七组4321中只输入了432

此时记录下第七组的地址00323334,接着F8继续执行下去

当执行了retn返回后,界面变为空白,这时观察右边寄存器窗口中eip的值,为00323334,这个值就和刚才记录的第七组4321的地址是相符的。

意思就是,此时程序的返回地址,被覆盖掉,指向了由于用户输入的多的字符而溢出的地址中。

崩溃原因

那为什么会崩溃了,此时程序处于暂停状态,可以点击上方的E

选择程序模块

此时就会再次回头程序入口处

这时,通过快捷键ctrl+G搜索EIP此时指向的地址

地址搜索不到,所以是个无效的指令地址,所以这时候如果继续运行,处理器在取指的时候就会发生错误使程序崩溃。


为什么会覆盖

一般栈溢出是由于局部变量超出你定义的范围导致的。如果溢出的不是很多,那可能会覆盖参数区域,如果溢出的再多一点,那有可能覆盖返回地址区域。再多就会覆盖上一层的栈中地址。

​ 图源:《0day安全 软件漏洞分析技术第二版》

漏洞利用

从上面的分析了解到,只要最后覆盖EIP的指令地址,是有效的,那么就可以达到自己的目的,例如弹计算器啊,记事本啊,对话框这些基本功能。

通过跟大佬一番有力沟通,了解到可以使用shellcode来实现这类功能,通过百度了解到,就是编写一个功能,然后获取十六进制转换为硬编码。

由于我这台电脑,上次手误,把系统的计算器删除后,就再也没能打开,恢复不了了。所以只能弹出记事本了。

c++
1
2
3
4
5
6
7
#include <stdio.h>
#include <iostream>
int main()
{
system("notepad");
return 0;
}

将生成的exe拖进od

红框处标记的就是system("notepad")的十六进制,将其复制出来,修改成硬编码。

修改一下之前的源码

c++
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
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>
#include <string.h>
#include <iostream>
#define PASSWORD "1234567"
using namespace std;
int verify_password (char *password)
{
int authenticated;
char buffer[8];// add local buff
//strcmp比较两个字符串并根据比较结果返回整数
//strcmp(str1,str2),若str1=str2,则返回零;若str1<str2,则返回负数;若str1>str2,则返回正数
cout << ("authenticated:")<<&authenticated <<endl;
cout <<("buffer:")<<&buffer<<endl;
authenticated=strcmp(password,PASSWORD);
//strcpy把含有'\0'结束符的字符串复制到另一个地址空间,返回值的类型为char*。
strcpy(buffer,password);//over flowed here!
return authenticated;
}


main()
{
int valid_flag=0;
char password[1024];
char notp[] = "\xC7\x04\x24\x45\x50\x40\x00\xE8\xAE\x26\x00\x00";//shellcode硬编码
cout<<("notp:")<<&notp<<endl;
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 passed the verification!\n");
break;
}
}
}

现在的思路就是,输出shellcode的地址,因为这个shellcode的作用是弹出记事本,所以当复制函数执行完后,将最后的地址修改为shellcode的地址,使其执行弹记事本的命令

这次直接在复制函数处下断。shellcode的地址已经输出出来了。

在最后一组位置,右键点击修改

修改为shellcode的指令地址。

当我以为有记事本出来的时候,啪,已终止

没想懂。重新运行程序在观察一遍

这次直接跳转到shellcode指令地址观看一下汇编代码。发现和前面的不一样

这是一开始获取十六进制时候的截图,不同的地方在于,原来的call的是system的函数地址,而这个地址在每个程序中都是不同的,是动态的,我直接复制粘贴调用过去,所以不正确,才会产生现在上面那图那样,CALL了一个不知道什么位置的指令地址。


这个方法行不通,换一种,既然shellcode的方式用不了,那就不获取他硬编码,直接写在程序中。比如像之前分析的,突破密码验证一样,写在那个位置直接调用。

c++
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
34
35
36
37
38
39
40
41
42
43
#include <stdio.h>
#include <string.h>
#include <iostream>
#define PASSWORD "1234567"
using namespace std;
int verify_password (char *password)
{
int authenticated;
char buffer[8];// add local buff
//strcmp比较两个字符串并根据比较结果返回整数
//strcmp(str1,str2),若str1=str2,则返回零;若str1<str2,则返回负数;若str1>str2,则返回正数
cout << ("authenticated:")<<&authenticated <<endl;
cout <<("buffer:")<<&buffer<<endl;
authenticated=strcmp(password,PASSWORD);
//strcpy把含有'\0'结束符的字符串复制到另一个地址空间,返回值的类型为char*。
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
{
system("notepad");
printf("Congratulation! You have passed the verification!\n");
break;
}
}
}

拖进od,首先找到system("notepad")的地址,首先进入入口处,往下拉,找到这里将其指令地址复制出来00401518

c++
1
2
00401518    C70424 9A504000 mov dword ptr ss:[esp],stack_ov.0040509A ; ASCII "notepad"
0040151F E8 E4260000 call <jmp.&msvcrt.system>

现在重复刚才的流程,在复制函数处下断,等执行到MOV时候,修改最后一组地址,使其指向这个地址

当执行到retn后,不会像之前那样空白或已终止,而是会走到system(notepad)的位置

当步过CALL的时候,期待已久的记事本就出来了。


总结

这个实验,从文字上看不懂,一定得上手测试,拖进od或者任何可以有此功能的软件(x32dbg,Windbg)中进行跟流程,多下断点,多观察堆栈区和寄存器区。shellcode那里属于自己的一个试错,当时想着能不能找到一个万能地址,让其可以弹出来,后面越来越琢磨发现自己有点想屁吃,要是想弹出来,感觉只能通过远程注入的方式才行。这次这个弹记事本的方式相当于hook,jmp过去.覆盖EIP的是数值。

文章作者: KeyboArd
文章链接: https://www.wrpzkb.cn/byte2/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 KeyboArd's Blog
打赏
  • 微信
    微信
  • 支付寶
    支付寶

评论