4.2.2 突破密码验证程序
实验环境要求如表4-2-1所示。
表4-2-1 实验环境
|
|
推荐使用的环境 |
备 注 |
|
操作系统 |
Windows XP sp2 |
其他Win32操作系统也可进行本实验 |
|
编译器 |
Visual C++ 6.0 |
如使用其他编译器,需重新调试 |
|
编译选项 |
默认编译选项 |
VS2003和VS2005中的GS编译选项会使栈溢出实验失败 |
|
build版本 |
debug版本 |
如使用release版本,则需要重新调试 |
说明:如果完全采用实验指导所推荐的实验环境,将精确地重现指导中所有的细节;否则需要根据具体情况重新调试。
请您在开始实验前务必先确定实验环境是否符合要求。
按照程序的设计思路,只有输入了正确的密码“1234567”之后才能通过验证。程序运行情况如图4.2.2所示。
|
| 图4.2.2 程序正常运行时的情况 |
假如我们输入的密码为7个英文字母‘q’,按照字符串的序关系 “qqqqqqq”> “1234567”, strcmp应该返回1,即authenticated为1。OllyDbg动态调试的实际内存情况如图4.2.3所示。
|
| 图4.2.3 栈帧布局 |
也就是说,栈帧数据分布情况如表4-2-2所示。
表4-2-2 栈帧数据分布情况
|
局部变量名 |
内 存 地 址 |
偏移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 |
在观察内存的时候应当注意“内存数据”与“数值数据”的区别。在我们的调试环境中,内存由低到高分布,且计算机体系架构属于传统的“大顶机”( big endian,也有文献称其为“大端机”)。您可以简单地把这种情形理解成Win32系统在内存中由低位向高位存储一个4字节的双字(DWORD),但在作为“数值”应用的时候,却是按照由高位字节向低位字节进行解释。这样一来,在我们的调试环境中,“内存数据”中的DWORD和我们逻辑上使用的“数值数据”是按字节序逆序过的。
例如,变量authenticated在内存中存储为0x 01 00 00 00,这个“内存数据”的双字会被计算机由高位向低位按字节解释成“数值数据” 0x 00 00 00 01。出于便于阅读的目的,OllyDbg在栈区显示的时候已经将内存中双字的字节序反转了,也就是说,栈区栏显示的是“数值数据”,而不是原始的“内存数据”,所以,在栈内看数据时,从左向右对于左边地址的偏移依次为3、2、1、0。请您在实验中注意这一细节。
题外话:与CISIC(复杂指令集)和RISIC(经典指令集)的争论一样,大顶机模式(big endian)和小顶机模式(little endian)的位序问题属于计算机体系结构中不同的实现标准。在Intel x86几乎一统天下的今天,似乎大顶机模式已经成为通用的标准。本着兼容性的原则,在另一些架构中,如ARM体系,用户可以自己选择使用大顶机模式还是小顶机模式。
下面我们试试输入超过7个字符,看看超过buffer[8]边界的数据能不能写进authenticated变量的数据区。为了便于区分溢出的数据,这次我们输入的密码为“qqqqqqqqrst”(‘q’、‘r’、‘s’、‘t’的ASCII码相差1),结果如图4.2.4所示。
|
| 图4.2.4 覆盖邻接变量 |
图4.2.4 覆盖邻接变量
栈中的情况和我们分析的一样,从输入的第9个字符开始,将依次写入authenticated变量。按照我们的输入“qqqqqqqqrst”,最终authenticated的值应该是字符‘r’、‘s’、‘t’和用于截断字符串的null所对应的ASCII码0x00747372。
这时的栈帧数据如表4-2-3所示。
表4-2-3 栈帧数据
|
局部变量名 |
内存地址 |
偏移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,如图4.2.5所示。
|
| 图4.2.5 修改邻接变量 |
这时系统栈内的变化过程如表4-2-4所示。
表4-2-4 栈帧数据
|
局部变量名 |
内存地址 |
偏移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,从而绕过验证程序!修改邻接变量成功的界面如图4.2.6所示。
|
| 图4.2.6 修改邻接变量成功 |
题外话:严格说来,并不是任何8个字符的字符串都能冲破上述验证程序。由代码中的authenticated=strcmp(password,PASSWORD),我们知道authenticated的值来源于字符串比较函数strcmp的返回值。按照字符串的序关系,当输入的字符串大于“1234567”时,返回1,这时authenticated在内存中的值为0x00000001,可以用字串的截断符NULL淹没authenticated的低位字节而突破验证;当输入字符串小于“1234567”时(例如,“0123”等字符串),函数返回-1,这时authenticated在内存中的值按照双字-1的补码存放,为0xFFFFFFFF,如果这时也输入8个字符的字符串,截断符淹没authenticated低字节后,其值变为0xFFFFFF00,所以这时是不能冲破验证程序的。图4.2.6所示的“01234567”输入就属于这种情形。如果您感兴趣,可以尝试进一步调试研究这种情况。