Scanf函数是常用的函数,它的作用一般认为是让用户给变量赋值。使用方法一般是scanf(“%d”, &num) 第二个参数是变量的地址。如果第二个参数不是变量的地址,而是变量本身,那么程序就会报错了。实际上,如果第二个参数是变量本身,程序也不一定报错,报错与否取决于这个变量的大小。
透过现象看本质,scanf函数的作用其实是:把用户输入的字符格式化到指定格式,并输出到指定内存。事实上第二个参数可以是任意一个地址,而且我们可以直接指定它。
如果第二个参数是一个变量本身,那么程序的报错与否,取决于变量的值所代表的那段内存地址空间是否属于Ring3,换句话说,也就是取决于这块内存是否属于用户可以编辑的内存区域。
先来看一个会报错的情况。
1 #include <stdio.h> 2 3 int main() 4 { 5 int num = 6; 6 7 printf("input num.\r\n"); 8 scanf("%d", num); 9 10 printf("input finished.\r\n"); 11 printf("num\'s value is %d.\r\n", num); 12 13 system("pause"); 14 15 return 0; 16 }
程序第8行,scanf(“%d”, num)的第二个参数是整形变量num的自身,按常理来说是不对的,如果想把用户输入赋值给num,第二个参数应该是&num才对。编译运行后,果然报错。
现在我们改变示例程序,尝试突破第二个参数不能为变量自身的这个认识。我们把num的初始值修改为了74565(十六进制的12345)。再次编译运行。
这次竟然没有报错。用户成功输入了8。当然这个8并不是赋值给num了。可以看到num的值依然是74565。利用WinHex查看进程的内存,我们应该会在0x00012345开始的4字节内存空间内,发现以小尾方式存储的8,也就是08 00 00 00。
这证明了scanf函数的本质:把用户输入的字符格式化到指定格式,并输出到指定内存。第一个示例程序之所以报错,是因为num的初始值6,转化为十六进制地址0x00000006,是属于系统占用的内存区域,尝试修改这一区域的内存所以报错。0x00000000到0x0000FFFF这片区域是系统的领空,一旦num的值大于这个范围,程序便不会报错了(当然也不能太大,大于用户自身可操作的内存范围0x0001000-0x7FFF0000还是会报错)。虽然不会报错,但是程序运行的结果却并不是把用户的输入赋值给num,而是修改了内存中某个位置的内容,所以用起来要小心。
当然也可以直接制定要修改的内存位置。示例程序如下所示,我们想要改变虚拟内存中起始地址为0x00012345,占4个字节空间(int型数值)的地址:
#include <stdio.h> int main() { printf("Check target RAM (00012345) status.\r\n"); scanf("%d", 0x00012345);
printf("Check again.\r\n"); system("pause"); return 0; }
编译运行后,首先用WinHex查看当前0x00012345处的内存内容。
此时0x00012345处全是00。
输入任意整形数据,尝试把数据写入0x00012345起始的这片4字节区域。
输入的1234567890转换成十六进制是499602D2。小尾形式是D2 02 96 49。查看内存,这段地址果然发生了改变。
这种对scanf函数的利用方法,可以修改虚拟内存中任意位置的数据。对内存结构熟悉的话,可以用scanf函数偷偷修改一些东西。