2.2.2 突破密码验证程序
2.2.2 突破密码验证程序
实验环境
实验流程
-
在vc6.0中编写test.c文件编译运行
#include <stdio.h> #define PASSWORD "1234567" int verify_password (char * password) { int authenticated; char buffer[8]; // add local buff to be overflowed authenticated = strcmp(password, PASSWORD); 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; } } }
运行结果:
-
用OllyDbg打开test.exe文件
-
假如我们输入的密码为 7 个英文字母“q”,按照字符串的序关系“qqqqqqq”>“1234567”,strcmp 应该返回 1,即 authenticated 为 1。在地址00401059处下断点,OllyDbg 动态调试的实际内存情况如下图所示。
鼠标落选到右下角窗口,快捷键control+g,在窗口中输入0012fb18
回车,显示如下:
另外。若把鼠标落选到左下角窗口,快捷键control+g,在窗口中输入0012fb18,回车。则显示如下:
也就是说,栈帧数据分布情况如下表所示:
局部变量名 内存地址 偏移3处的值 偏移2处的值 偏移1处的值 偏移0处的值 buffer[0~3] 0x0012fb18 0x71('q') 0x71('q') 0x71('q') 0x71('q') buffer[4~7] 0x0012fb1c NULL 0x71('q') 0x71('q') 0x71('q') authenticated 0x0012fb20 0x00 0x00 0x00 0x01 在观察内存的时候应当注意“内存数据”与“数值数据”的区别。在我们的调试环境中, 内存由低到高分布,您可以简单地把这种情形理解成Win32系统在内存中由低位向高位存储一 个 4 字节的双字(DWORD),但在作为“数值”应用的时候,却是按照由高位字节向低位字节进行解释。这样一来,在我们的调试环境中,“内存数据”中的 DWORD 和我们逻辑上使用的“数值数据”是按字节序逆序过的。
例如,变量 authenticated 在内存中存储为 0x 01 00 00 00,这个“内存数据”的双字会被计算机由高位向低位按字节解释成“数值数据” 0x 00 00 00 01。出于便于阅读的目的,OllyDbg 在栈区显示的时候已经将内存中双字的字节序反转了,也就是说,栈区栏显示的是“数值数据”,而不是原始的“内存数据”。所以,在栈内看数据时,从左向右对于左边地址的偏移依次为 3、 2、1、0。请您在实验中注意这一细节。
-
下面我们试试输入超过 7 个字符,看看超过 buffer[8]边界的数据能不能写进 authenticated 变量的数据区。为了便于区分溢出的数据,这次我们输入的密码为“qqqqqqqqrst”(‘q’、‘r’、‘s’、‘t’的 ASCII 码相差 1),结果如下图所示。
鼠标分别落选到下半区左下角和右下角窗口,快捷键control+g,在窗口中输入0012fb18
栈中的情况和我们分析的一样,从输入的第 9 个字符开始,将依次写入 authenticated 变量。按照我们的输入“qqqqqqqqrst”,最终 authenticated 的值应该是字符‘r’、‘s’、‘t’和用于截断字符串的 null 所对应的 ASCII 码 0x00747372。
这时的栈帧数据如下所示:
局部变量名 内存地址 偏移3处的值 偏移2处的值 偏移1处的值 偏移0处的值 buffer 0x0012fb18 0x71('q') 0x71('q') 0x71('q') 0x71('q') 0x0012fb1c 0x71('q') 0x71('q') 0x71('q') 0x71('q') authenticated被覆盖前 0x0012fb20 0x00 0x00 0x00 0x01 authenticated被覆盖后 0x0012fb20 NULL 0x74('t') 0x73('s') 0x72('r') authenticated 变量的值来源于 strcmp 函数的返回值,之后会返回给 main 函数作为密码验证成功与否的标志变量。当 authenticated 为 0 时,表示验证成功;反之,验证不成功。
我们已经知道越过数组 buffer[8]的边界的后续数据可以改写变量 authenticated,那么如果 我们用这段溢出数据恰好把 authenticated 改为 0,是不是就可以直接通过验证了呢?
字符串数据最后都有作为结束标志的 NULL(0),当我们输入 8 个‘q’的时候,按照前边的分析,buffer 所拥有的 8 个字节将全部被‘q’的 ASCII 码 0x71 填满,而字符串的第 9 个字符——作为结尾的 NULL 将刚好写入内存 0x0012FB20 处,即下一个双字的低位字节,恰好将 authenticated 从 0x 00 00 00 01 改成 0x 00 00 00 00,如下图所示。
为了方便比对,我们把前面输入7个字符‘q’的图拿来比对:
这时系统栈内的变化过程如表所示:
局部变量名 内存地址 偏移3处的值 偏移2处的值 偏移1处的值 偏移0处的值 buffer 0x0012fb18 0x71('q') 0x71('q') 0x71('q') 0x71('q') 0x0012fb1c 0x71('q') 0x71('q') 0x71('q') 0x71('q') authenticated被覆盖前 0x0012fb20 0x00 0x00 0x00 0x01 authenticated被覆盖后 0x0012fb20 0x00 0x00 0x00 0x00(NULL) -
经过上述分析和动态调试,我们知道即使不知道正确的密码“1234567”,只要输入一个为 8 个字符的字符串,那么字符串中隐藏的第 9 个截断符 NULL 就应该能够将 authenticated 低字节中的 1 覆盖成 0,从而绕过验证程序!修改邻接变量成功的界面如图所示:
题外话:严格说来,并不是任何 8 个字符的字符串都能冲破上述验证程序。由代码中的 authenticated=strcmp(password,PASSWORD),我们知道 authenticated 的值来源于字符串比较函数 strcmp 的返回值。按照字符串的序关系,当输入的字符串大于“1234567” 时,返回 1,这时 authenticated 在内存中的值为 0x00000001,可以用字串的截断符 NULL 淹没 authenticated 的低位字节而突破验证;当输入字符串小于“1234567”时(例如,“0123”等字符串),函数返回-1,这时 authenticated 在内存中的值按照双字 -1 的补码存放,为 0xFFFFFFFF,如果这时也输入 8 个字符的字符串,截断符淹没 authenticated 低字节后,其值变为 0xFFFFFF00,所以这时是不能冲破验证程序的。上图的“01234567”输入就属于这种情形。
如果您感兴趣,可以尝试进一步调试研究这种情况。