程序生成过程中产生的几个警告信息都可以忽略掉,例如,"warning C4996: 'gets' was declared deprecated"和"warning C4996: 'strcpy' was declared deprecated",编译器推荐使用gets_s()来代替gets(),用strcpy_s()来代替strcpy()。如果完全使用这些替代函数,那么就可消除缓冲区溢出潜在的可能性。然而,这些只是警告信息,可以忽略甚至关闭,忽略这些警告信息是符合用最小的代价移植现有老系统这个前提的。
当使用托管扩展时,编译器会为main()及checkpassword()函数生成Microsoft媒介语言(MSIL或称为通用媒介语言CIL),CIL字节码会被打包进一个可执行文件,在调用即时编译器(JIT)将其翻译为本地程序集指令后,接着把控制权交给main()。
程序运行时,提示用户输入用户名:
接着程序要求用户输入密码,其被读入到声明在11行上的10个字符数组这个变量中,在插1中,如果在密码从标准输入读取之前,查看堆栈上的数组地址起始处的数据(本例中为0x002DF3D4),将会看到分配给密码的存储空间(以黑体字标出)及堆栈上的返回地址(以红色字标出)。返回地址在此为小尾字节序(Little Endian)。
代码段1:堆栈上数组地址起始处的数据
002DF3D4 00 00 00 00 04 f4 2d 00 a0 1b e7 79 80 63 54 00 ......-....y.cT. 002DF3E4 04 f4 2d 00 f9 0f 0a 02 01 00 00 00 79 3a 4e 00 ..-.........y:N. 002DF3F4 a8 2b 2f 00 38 f4 2d 00 da c4 fc 79 78 f4 2d 00 .+/.8.-....yx.-. 002DF404 48 f4 2d 00 60 13 40 00 01 00 00 00 50 53 54 00 H.-.`.@.....PST. |
倘若输入了更多的字符,以致密码字符数组存储空间无法容纳,一个攻击者就可以溢出此缓冲区,并以shellcode(可为任意的代码)地址覆盖掉返回地址。出于演示的目的,在此假定shellcode已被注入,且定位于0x00408130,为执行此代码,攻击者只需把下列字符串作为密码输入:
Enter 8 character password: 123456789012345678900|@ |
这个输入的字符串被复制到密码字符数组,溢出了此缓冲区并覆盖相应的
内存包括返回地址。字符串中的三个字符0|@覆盖了返回地址的前三个字节,而返回地址的最后一个字节被一个由gets()函数产生的null结尾字符所覆盖。注意,如果这个null不在最后一个字节上,那么不可能复制整个字符串,因为gets()函数会把这个null字符解释为字符串的结尾。那为什么要以上这三个字符呢?因为,这些字符的十六进制形式提供了内存中表示地址所需的值,"0"的ASCII十六进制码为0
x30,"|"为0x81,而"@"为0x40。如果把这三个字符以顺序{ '0', '|', '@' }连接起来,就可将shellcode(0x00408130)地址的小尾字节序表示形式写入到内存中。最后一个null字节 由字符串的null字符提供。(见代码段2。)
代码段2:
002DF3D4 31 32 33 34 35 36 37 38 39 30 31 32 33 34 35 36 1234567890123456 002DF3E4 37 38 39 30 30 81 40 00 01 00 00 00 79 3a 4e 00 78900.@.....y:N. 002DF3F4 a8 2b 2f 00 38 f4 2d 00 da c4 fc 79 78 f4 2d 00 .+/.8.-....yx.-. 002DF404 48 f4 2d 00 60 13 40 00 01 00 00 00 50 53 54 00 H.-.`.@.....PST. |
当checkpassword()函数返回时,控制权就传到shellcode而不是main()函数中的原始返回地址上。
为了简化这个攻击过程,在此,关闭了缓冲区安全检查选项/GS。如果这个选项没有关闭,编译器将会在声明在堆栈上的任何数组(缓冲区)之后插入一个"密探"--实际上为一个Cookie,见图1。
图1:基于"密探"的缓冲区溢出保护 |