
上次分析的突破密码验证中,通过改写领近变量实现密码突破验证。
这次碰到一个更好玩的,也是通过栈溢出,当字符达到一个上限时,EIP寄存器中的地址会被覆盖,文字解释不清,直接上操作吧。
正文
首先来看源码, 源码是上一篇文章中的源码, 只是我对他进行了改写。
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
| #include <stdio.h> #include <string.h> #include <iostream> #define PASSWORD "1234567" using namespace std; int verify_password (char *password) { int authenticated; char buffer[8]; cout << ("authenticated:")<<&authenticated <<endl; cout <<("buffer:")<<&buffer<<endl; authenticated=strcmp(password,PASSWORD); strcpy(buffer,password); 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来实现这类功能,通过百度了解到,就是编写一个功能,然后获取十六进制转换为硬编码。

由于我这台电脑,上次手误,把系统的计算器删除后,就再也没能打开,恢复不了了。所以只能弹出记事本了。
1 2 3 4 5 6 7
| #include <stdio.h> #include <iostream> int main() { system("notepad"); return 0; }
|
将生成的exe拖进od
红框处标记的就是system("notepad")
的十六进制,将其复制出来,修改成硬编码。

修改一下之前的源码
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 46
| #include <stdio.h> #include <string.h> #include <iostream> #define PASSWORD "1234567" using namespace std; int verify_password (char *password) { int authenticated; char buffer[8]; cout << ("authenticated:")<<&authenticated <<endl; cout <<("buffer:")<<&buffer<<endl; authenticated=strcmp(password,PASSWORD); strcpy(buffer,password); return authenticated; }
main() { int valid_flag=0; char password[1024]; char notp[] = "\xC7\x04\x24\x45\x50\x40\x00\xE8\xAE\x26\x00\x00"; cout<<("notp:")<<¬p<<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
的方式用不了,那就不获取他硬编码,直接写在程序中。比如像之前分析的,突破密码验证一样,写在那个位置直接调用。
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
| #include <stdio.h> #include <string.h> #include <iostream> #define PASSWORD "1234567" using namespace std; int verify_password (char *password) { int authenticated; char buffer[8]; cout << ("authenticated:")<<&authenticated <<endl; cout <<("buffer:")<<&buffer<<endl; authenticated=strcmp(password,PASSWORD); strcpy(buffer,password); 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

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
的是数值。
