unsigned到signed的隐式强制转换常常会导致程序错误与漏洞,比较著名的一个案例就是:函数getpeername的安全漏洞。
2002年, 从事FreeBSD开源操作系统项目的程序员意识到,他们对getpeername函数的实现存在安全漏洞.代码的简化版本如下:
//void *memcpy(void *dest, void *src, size_t n);
#define KSIZE 1024
char kbuf[KSIZE];
int copy_from_kernel(void *user_dest, int maxlen)
{
int len = KSIZE < maxlen ? KSIZE : maxlen;
memcpy(user_dest, kbuf, len);
retn len;
}
在这段代码里,最顶上屏蔽的是库函数memcpy的原型,这个函数是要将一段指定长度为n的字节从存储器的一个区域复制到另一个区域。
函数copy_from_kernel是要将一些操作系统内核维护的数据复制到指定的用户可以访问的存储器区域。对用户来说,大多数内核维护的数据结构应该是不可读的,因为这些数据结构可能包含其他用户和系统上运行的其他作业的敏感信息,但是显示为kbuf的区域是用户可以读的。参数maxlen给出的是分配给用户的缓冲区的长度,这个缓冲区是用参数user_dest指示的。然后,int len = KSIZE < maxlen ? KSIZE : maxlen; 行的计算确保复制的字节数据不会超出源或者目标缓冲区可用的范围。
不过,假设有些怀有恶意的程序员在调用copy_from_kernel的代码中对maxlen使用了负数值,那么,最小值计算会把这个值赋给len,然后len会作为参数n被传递给memcpy。不过,请注意参数n是被声明为数据类型size_t的。这个数据类型是在库文件stdio.h中(通过typedef)被声明的。典型地,在32位机器上被定义为unsigned int。既然参数n是无符号的,那么memcpy会把它当作一个非常大的正整数,并且试图将这样多字节的数据从内核区域复制到用户的缓冲区。虽然复制这么多字节(至少231个)实际上不会完成,因为程序会遇到进程中非法地址的错误,但是程序还是能读到没有被授权的内核存储器区域。
我们可以看到,这个问题是由于数据类型的不匹配造成的:在一个地方,长度参数是有符号数;而另一个地方,它又是无符号数。正如这个例子表明的那样,这样的不匹配会成为缺陷的原因,甚至会导致安全漏洞。幸运的是,还没有案例报告有程序员在FreeBSD上利用了这个漏洞。他们发布了一个安全建议,“FreeBSD-SA-02:38.signed-error”,建议系统管理员如何应用补丁消除这个漏洞。要修正这个缺陷,只要将copy_from_kernel的参数maxlen声明为类型size_t,也就是与memcpy的参数n一致。同时,我们也应该将本地变量len和返回值声明为size_t。