RegQueryValueEx
gethostbyname/getaddrinfo
_localtime64
FindFirstFile/FindNextFile
VerQueryValue
CreateFileMapping相关
SetDllDirectory
Windows API就没有问题、没有BUG吗?答案是否定的!代码都是写出来,怎么可能完全没有问题呢?下面我们就来看看目前发现有哪些Windows API是有问题的,或者说使用上面有误区的。
1、RegQueryValueEx
首先看看这个API,获取注册表里面的信息,这个API本身没有问题,暂时还没见到崩溃在这个API里面的。不过这个API的使用上面有一些小技巧需要注意。使用不当会引发一些意想不到的问题甚至崩溃(API的具体使用请查阅MSDN,下面不再赘述)。
问题主要发生在我们获取注册表里面的字符串值的情况下,看看这样一段代码:
DWORD dwType = REG_SZ;
DWORD dwcbData = 0;
LONG lRet =::RegQueryValueExW(hKey, L"InstallTimeTest", NULL, &dwType, NULL, &dwcbData);
if (ERROR_SUCCESS == lRet
&& REG_SZ == dwType)
{
PBYTE pBuffer = new BYTE[dwcbData];
我们一般在枚举注册表时,使用这样的代码来探测,某个注册表项需要的存储空间是多大,然后分配空间,再进行读取,一般情况下是没有问题的。
但是再看看这样的一个设置注册表的操作:
DWORD dwType = REG_SZ;
wchar_t szInstalTime[5] = {L'1', L'2', L'3', L'4', L'5'};
DWORD dwcbData = sizeof(szInstalTime); // 字节数
LONG lRet =::RegSetValueExW(hKey, L"InstallTimeTest", NULL, dwType, LPBYTE(szInstalTime), dwcbData);
if (ERROR_SUCCESS == lRet)
{
bRet = true;
}
奇葩了吧,是的,它设置了一个字符串到注册表里面,字符串的总长度是5,字节数是10字节(宽字符,不包括结尾的0),传给RegSetValueEx的长度也是10,函数执行成功了,似乎成功写入了,但是看看注册表里面的情况:
竟然没有结尾的0,此时我们再用上面的方式去读取的时候,它首先会告诉你需要10字节的空间,你分配10个字节,然后再去读就会读到一个不以0结尾的字符串存放到你的缓冲区里面,之后你再对缓冲区进行字符串的各种操作,读写,计算长度等等,会发生什么我想就不用我多说了。
也许会有人说,微软不是说了使用RegSetValueEx时,如果是设置字符串的话,传入的长度要包括结尾的0吗?是不是RegSetValueEx用错了,Yes,你没说错,但是问题是这种使用方法也是可以的,不排除有人恶意为之来使得你的程序崩溃,如果一个安全软件轻易就这样崩溃了……有没有一种人生观被彻底摧毁的敢脚。
好吧,说回来,还是看看我们应该怎样应对这种异常情况吧,其实知道了原因我想方法也很简单了,就是每次分配的时候,多分配一些内存(宽字符要多分配2个字节,对于多行字符串就要多分配4个字节了,因为它是以两个0结尾的)。再有一种使用方法是这样的,如果你大概知道需要多长的空间,而且你对数据没读全不是很关心。那么你提前分配一定字节的空间(譬如N字节),然后调用RegQueryValueExW时传入空间大小时,传入N-2或者N-4字节,也可以解决这个问题。
2、gethostbyname/getaddrinfo
在hosts文件里面含有一些特殊构造的数据时,这两个API已经被证明必然会crash,其实原因就在于它里面有一处调用没有对指针判空就调用wcslen来计算长度,可以通过反汇编mswsock模块来研究这个问题。而其解决方法也很直接,那就是直接写一个此类API的代理函数,然后把这种crash捕获住,发生异常时直接返回失败即可,因为这就是一个简单的AV异常,因此捕获之后不会造成其它的问题,是安全的捕获。
代理函数可以这样写:
struct hostent FAR * PASCAL FAR gethostbyname_safe(IN const char FAR * name)
{
if (NULL == name)
return NULL;
__try
{
return ::gethostbyname(name);
}
__except (GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
return NULL;
}
}
至于getaddrinfo函数则可以如法炮制。
3、_localtime64
这并不是一个API咯,而是微软扩展的CRT函数,用来把一个time_t时间转换成本地时间并存放到tm结构中。使用中发现如果给它传入了一个不正确的值,会引发crash。最早是蓝光的兄弟报告了这个函数的问题(其实这个函数和_localtime32是类似的,另外一个函数是_localtime,这个函数则是在编译期根据是否宏定义了_USE_32BIT_TIME_T来决定是调用_localtime32还是调用_localtime64)。
你可以试试给这几个函数传入0之类的异常数据(负数,超大的数,64位版本超过After 23:59:59, December 31, 3000)试试……
其实微软也知道这个问题,在MSDN上面有这样一段话(后来想想微软也挺坑的):
These functions validate their parameters. If timer is a null pointer, or if the timer value is negative, these functions invoke an invalid parameter handler, as described in Parameter Validation. If execution is allowed to continue, the functions return NULL and set errno to EINVAL.
它建议使用_set_invalid_parameter_handler来设置错误参数异常处理函数,不过很遗憾,其实这种方法也有漏洞,它虽然能捕获这种错误,但是当系统滥用这种方法的时候,譬如多个模块调用这个函数来设置自己的错误处理函数,这种方法变得非常不可靠。譬如说,模块1调用了_set_invalid_parameter_handler,然后模块2又调用了_set_invalid_parameter_handler,过了一段时间模块2卸载了,这时错误处理函数还指向模块2里面,此时如果模块1再发生参数错误就会导致执行一个非法地址的错误处理函数,问题更严重了,模块2卸载前把错误处理函数恢复如何呢?但是如果还有模块3呢?怎么保证恢复的错误处理函数指针是完全可靠的呢?总的来说,此方法不太可靠,不建议使用。
更好的解决方案是按照自己的需求,对_localtime系列函数再包装一下,先帮他们检查一下参数,然后再传给真正的_localtime函数处理,以规避这种问题。可以参考下面的这个函数的检测方式(你完全可以根据自己的需求来实现这个函数)。
inline time_t FixupTime64Range(const time_t time)
{
time_t tmp_time = time;
if(tmp_time < 0 ||// underflow
tmp_time > (_MAX__TIME64_T - 14 * 60 * 60)) // overflow
{
tmp_time = 0; // reset time to 0
}
return tmp_time;
}
/* number of seconds from00:00:00, 01/01/1970 UTC to 23:59:59. 12/31/3000 UTC */
#define _MAX__TIME64_T 0x793406fffi64
14 * 60 * 60 用于控制范围,目前最早的时区是UTC+14(http://zh.wikipedia.org/zh-hant/UTC%2B14)
4、FindFirstFile/FindNextFile
这两个组合API大部分人都用过吧,用来枚举文件和文件夹的,一般使用很难发现问题,但是在安全领域,用来进行文件扫描时,因为时间一般比较长,很容易发生扫描的目标文件夹或者磁盘里面发生文件被删除的情况,此时这两个API很容易就crash了,从实践来看,发生在FindNextFile里面更多一些,可以使用如下方法进行规避(封装一个SafeFindNextFile函数,FindFirstFile也可以类似为之)。如果是比较重要的逻辑,这两个API发生了crash之后,建议重置逻辑(譬如如果你正在进行病毒扫描,可以保存初始状态,然后重新启动扫描)。
BOOL bRet = FALSE;
__try
{
bRet = FindNextFile(hFindFile, lpFindFileData);
}
__except( EXCEPTION_EXECUTE_HANDLER )
{
}
return bRet;
5、VerQueryValue
这个API的crash,相信肯定有不少人遇到过,这个函数通常用于从文件的版本资源中获取一些类似于文件版本之类的信息,不过这个API一般要和GetFileVersionInfo和GetFileVersionInfoSize配合调用,而且它的使用会有一些小小的麻烦(主要在于对第二个参数传递的字符串的理解),这里因为不是讲解这个API的使用,因此就不再啰嗦了,有兴趣的可以查阅MSDN上面对这个API的解说。
至于这API会发生crash的原因主要是因为它内部的一些实现上面不够健壮,在获取一些特殊的样本的版本信息时,它必然crash,说到特殊样本,这也是为什么一般使用的情况,基本不会发生问题,主要是安全软件在进行样本扫描时遇到的机率比较大。
解决方法和上面的FindNextFile类似,把这种异常catch住自己实现一个安全的函数,因为一般取文件的版本,描述等等信息都是辅助显示的,并不是关键逻辑,因此使用这种方式是比较简单和安全的。
inline BOOL APIENTRY VerQueryValueWSafe(const LPVOID pBlock, LPTSTR lpSubBlock, LPVOID *lplpBuffer, PUINT puLen)
{
BOOL bRet = FALSE;
__try
{
bRet = VerQueryValueW(pBlock, lpSubBlock, lplpBuffer, puLen);
}
__except( EXCEPTION_EXECUTE_HANDLER )
{
bRet = FALSE;
}
return bRet;
}
6、CreateFileMapping相关
其实CreateFileMapping这个API本身内部并没有见过crash,其出现问题一般都是一些使用上的问题以及一些无法避免的问题导致在后面的使用中出现crash。
使用内存映射文件的一个常见场景是随机读取文件内容时,此时会将一个文件通过CreateFileMapping和MapViewOfFile映射到内存之后进行随机读写。在映射完成之后,即将读写之前,此时如果文件所在的硬盘卷被卸载,或者映射的是一个移动设备(包括U盘,移动硬盘等等)文件,而移动设备被拔掉,或者一个网络文件,网络异常断开等等情况,会立即引发一个crash,错误ID类似:0xC0000006: In page error.
究其原因,在于内存映射文件的实现机制有点类似虚拟内存,他也保留了一个地址空间区域,在需要的时候才会把相关内容提交到物理存储器,也就是说并不是真的一开始就把整个文件放到内存里面去了,而是在需要的时候产生一个类似“缺页中断”响应,去外部存储器上面把文件的相关内容真正映射到物理存储器,如果那个文件没了,或者U盘被拔掉了,那么系统也处理不了,只好抛出异常了。
其实这个问题MSDN上面已经有个说明了:http://msdn.microsoft.com/en-us/library/windows/desktop/aa366801(v=vs.85).aspx,也给出了一种解决方案。
DWORD dwLength;
__try
{
dwLength = *((LPDWORD) lpMapAddress);
}
__except(GetExceptionCode()==EXCEPTION_IN_PAGE_ERROR ?
EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
// Failed to read from the view.
}
既然避免不了的问题,让它优雅的结束也许也是一种不错的方式。处理这个问题的另外一种方式是不使用内存映射,而使用ReadFile之类的API去读取,可以通过判断ReadFile的返回值来识别这种情况。
7、SetDllDirectory
这个API的问题是因为微软的某个版本的kernel32.dll里面对SetDllDirectory的实现有缺陷,它内部调用的线程同步的API调错了,造成使用这个API就会出现访问违例的crash。后来微软修正了kernel32.dll里面的这个问题。但是不排除外面还存在类似的问题。
解决方法最好的一种是就是不使用这个API,从使用这个API的目的来,仅仅是为了指定动态链接库的搜索路径,因此可以使用LoadLibraryEx来替代,这个API可指定LOAD_WITH_ALTERED_SEARCH_PATH临时改变搜索路径。其实出于安全考虑,我的建议是任何时候都应该使用绝对路径来加载DLL。