Programming 版 (精华区)

发信人: Roe (Roe), 信区: Programming
标  题: 使Win32程序更安全的15个小技巧
发信站: 哈工大紫丁香 (2001年12月10日09:37:29 星期一), 站内信件


使Win32程序更安全的15个小技巧
刘彦青 编译  
01-12-3 下午 01:25:43
------------------------------------------------------------------------------
--
 
1、留意危险的函数 
一些函数是非常危险的,不恰当地使用这些函数可能导致缓冲区溢出,如果你幸运的话,
可能这只会导致你自己的应用程序崩溃。如果不走运的话,可能会导致恶意代码入侵你的
系统。下面是一些常见的需要留心的函数: 
strcpy (and variants such as lstrcpy, wcscpy, etc.) 
strcat (and variants such as lstrcat, wcscat, etc.) 
memcpy (and variants such as CopyMemory, _memccpy, bcopy, etc.) 
gets (and to a lesser extent, fgets) 
sprintf (and variants such as swprintf, vsprintf, etc.) 
scanf (and variants such as sscanf, swscanf, fscanf, etc.) 
仔细地分析这些函数,要确保使它们正确地检查边界情况。有些编程人员则走得更远,它
们绝对不使用这些函数,将它们认为是代码缺陷或bug。 
函数1、2的问题是,它们在拷贝数据时直到遇到空(null)字符才停止,使它们很容易受
到攻击。黑客可以不使用空(null)字符,或将它放在缓冲区外的一个位置上。至少,应
该使用带“n”字符的函数代替相应的函数,例如,应该使用strncpy()而不是strcpy(),
使用strncat()而不是strcat()。例如,应该将下面的代码段: 
#define MAX_BUFF 80 
char szBuff[MAX_BUFF]; 
strcpy(szBuff,szArg); 
将变为: 
#define MAX_BUFF 80 
char szBuff[MAX_BUFF]; 
strncpy(szBuff,szArg,MAX_BUFF); 
上面的第四个函数gets()非常危险,因此应该立即从代码中删除它,一旦它到达一行的末
尾,就会直接将用户的输入拷贝到缓冲区中。我们无法让它知道它应该拷贝多少个字符。
最近,在一个软件项目中我就遇到了类似的问题,经过测试后,我用ReadConsole()代替了
gets()。 
注意基于栈的缓冲区 
当调用上面危险的函数向基于栈的缓冲区中拷贝数据时应当特别小心。一般来说,在栈中
分配缓冲区的系统上执行恶意代码比在堆中分配缓冲区的系统要容易得多。例如:下面的
代码在栈中分配64字节的缓冲区: 
void foo() { 
char buff[64]; 

而下面的代码则在堆中分配缓冲区: 
void foo() { 
char *buff = malloc(64); 

此外,应该注意_alloca()的使用。从形式和作用二方面看,它都与malloc()类似,但它在
栈中分配内存。而MFC中的CString类和STL字符串类则相对比较安全,因为它们从堆中分配
缓冲区。 
2、使用/robust开关 
如果是在Windows 2000操作系统平台上创建使用远程过程调用(RPC)的应用程序,一定要
使用/robust MIDL编译器选项,它将在代码中添加更多的比较严格的完整性检查,减少许
多DOS攻击的危险。DOS攻击一般都使用了RPC。 
3、使用Negotiate而不是NTLM 
如果应用程序使用的是安全支持提供者界面(SSPI),或者使用了RPC、DCOM等间接使用S
SPI的技术。如果应用程序是在Windows 2000上运行的,则要求使用Negotiate SSP而不是
NTLM。例如,如果你使用了RPC并调用了RpcBindingSetAuthInfo[Ex]函数,则应该将Auth
Svc参数设置为RPC_C_AUTHN_GSS_NEGOTIATE而不是 RPC_C_AUTHN_WINNT或RPC_C_AUTHN_DE
FAULT。这也适用于在使用DCOM的情况下对CoSetProxyBlanket()或CoInitializeSecurity
()的调用。其授权的级别被分别设置在dwAuthnSvc变量和SOLE_AUTHENTICATION_INFO结构
中。 
4、使用分组的隐私和完整性检查机制 
使用DCOM和RPC都能够对客户机端和服务器之间的通道进行加密和完整性检查,除非我们传
送的数据量非常大,它对性能的影响是非常小的。如果使用DCOM,我们可以在COM+管理器
中或通过在程序中调用将dwAuthnLevel设置为RPC_C_AUTHN_LEVEL_PKT_PRIVACY的CoSetPr
oxyBlanket执行相应的操作。如果使用RPC,可以调用将AuthLevel设置为RPC_C_AUTHN_LE
VEL_PKT_PRIVACY的RpcBindingSetAuthInfo或RpcBindingSetAuthInfoEx执行相应的操作。
 
5、保持良好的ACL 
注册表、命名管道、互斥的共享变量等对象糟糕的访问控制列表(ACL)是一个常见问题,
它们能够使黑客浏览或改变系统中的资源。对象的ACL应该指明哪些用户能够操作指定的对
象。通过长期的实践,我总结出了下面的规律:开发团队中的某个成员负责ACL中的每个A
CE,开发团队中的所有人都遵守所有的ACE,并将这个ACL当作缺省的设置,不要指望用户
能够正确地设置ACL,它们一般不具备这种能力。 
在创建命名管道或信号等安全系统对象时,使用NULL作为缺省的ACL一般来说没有什么问题
,这意味着它继承了进程的安全描述符。但是,明确地建立一个空的安全描述符不是一个
好主意,这意味着该对象没有访问方面的控制,任何人都可以对它进行操作。ACL是我们的
好朋友,我们应该正确地使用它们,并学会如何使用SetSecurityInfo()函数。 
6、用最低的权限运行程序 
在Windows NT或Windows 2000上运行的所有代码都是运行在用户帐户中的,因此,代码的
权限与用户帐户的权限是相同的。如果程序是在一个拥有LocalSystem或管理员权限等权限
较高的帐户中运行,程序中的恶意代码也将具有同等的权限,其危害就非常高了。 
因此,只赋予程序能够完成工作所必需的权限是一个应该遵守的规则。无需要求程序具有
system、administrative或power-user等权限,因为具有这些权限的帐户都有特定的权力
。如果需要具备这些权限,程序要支持使用CreateProcessAsUser()函数的二次登录。另一
个可行的比较安全的方法是使用CreateRestrictedToken()函数启动一个具有较低权限的线
程。 
7、安全地存储秘密数据是一个永恒的难题,在软件是很难保证秘密数据的安全性的。如果
必须保证数据的安全性,请参阅相关的资料。 
8、使用CryptoAPI 
不要自己编写加密系统代码,而应该使用内置在操作系统中的相应代码。一般来说,自己
编写的密码系统的安全性并不高。使用RC2、RC4、DES和3DES等对称性密码系统MD5和SHA-
1等哈希函数或RSA等非对称性密码。MSDN中包含许多样例和代码,可以帮助我们很容易地
编写加密系统。 
9、在代码中添加安全性注释 
从长远的眼光来看,安全注释可以节约大量的时间。如果你的代码有安全要求或执行了与
安全有关的操作,则可以象下面的代码那样添加相应的安全注释: 
// SECURITY. 使用数据保护API存储口令 
// SECURITY. 口信和附加的信息从GatherDetails()传递给我们 
// SECURITY. pOut是一个从GatherDetails()传递给我们的指针 
assert(pOut != NULL); 
if (pOut != NULL) { 
DATA_BLOB blobPwd={cbPwd,szPwd}; 
DATA_BLOB blobEntropy={cbEntropy,bEntropy}; 
BOOL fRet = CryptProtectData(blobPwd, 
L"password", 
blobEntrpoy, 
NULL,NULL, 
CRYPTPROTECT_UI_FORBIDDEN, 
pOut); 

10、检查文件名 
过去,许多平台都有许多标准的文件错误。例如,假设有一段代码接受用户输入的文件名
,并打开文件。但你却不允许任何人访问名字为ServerConfiguration.xml的文件,如果有
人要求打开该文件,应该简单地返回“文件没找到”而不应该返回“拒绝访问”,否则黑
客就知道该文件是存在的,他只是没有能够访问它而已。黑客就可能使用该文件的FAT 8.
3文件名━━Server~1.xml试图访问该文件,由于程序不会检查这个文件名,因此黑客能够
轻易地获得你的服务器的配置。因此,应该确保检查所有可能的文件请求,更好的方法是
,不要根据文件名作出安全决策。 
11、允许使用长口令 
Windows 2000之前的Windows版本只支持使用14位字符长度的口令,Windows 2000支持长达
127个字符的口令。不要在应用软件中硬性规定只能接受14个字符长度的口令。 
12、清除没用的秘密 
一旦没有了任何用处,一些机密数据就应该被及时地清除。无论数据是被存储在内存或磁
盘中,一旦不需要再使用它们时,就覆盖它们。机密数据也可能被写到页交换文件中了,
但至少要使用ZeroMemory()来清除这些数据。如果感觉到这些措施还不足以保证机密数据
的安全,可以使用下面的代码来清除这些数据: 
void Scrub(LPVOID pBlob, DWORD cbBlob) { 
const int iParanoiaLevel = 7; 
for (int i=0; i < iParanoiaLevel; i++) { 
memset(pBlob,0xFF,cbBlob); // all 1's 
memset(pBlob,0x00,cbBlob); // all 0's 
memset(pBlob,0xAA,cbBlob); // 10101010 
memset(pBlob,0x55,cbBlob); // 01010101 

 
ZeroMemory(pBlob,cbBlob); 

13、检查所有与安全相关的函数的返回值 
检查返回值是在编程中的一项标准活动,但如果一个安全函数失败时,它就显得尤其重要
。例如,我曾见过调用RpcImpersonateClient()的代码,但它却没有检查该函数调用是否
成功了,或者其返回值是否为RPC_S_OK,而且下面的几行代码访问了一些机密的数据并将
这些数据返回给用户。更需要注意的是,一旦用户试图伪装成合法用户登录失败,就会在
调用线程时调用其他方法,在本例中是LocalSystem。包括那些只有管理员和系统才能访问
的系统中的ACL就会被用户访问,这些数据本来是不能够被普通用户访问的。登录失败后,
用户就能够访问无权访问的资源,这太可怕了。 
14、记录事件日志 
我们应该记录所有与Windows NT/2000安全性有关的事件,例如,不能访问自己的对象。但
不要记载操作系统可能已经记载的事件,例如logon、logoff,记录这些事件只能给管理员
增加无谓的工作量。使用RegisterEventSource()和ReportEvent()函数。如果是在编写脚
本,则使用Windows Script Host WScript.Shell对象,它有一个LogEvent方法。 
15、在文档中记录界面 
在文档中记录在编程中使用的命名管道、网络界面、RPC、DCOM协议和端口,这样作有二点
好处:1)它有助于防火墙软件管理人员判断哪些端口需要开放;2)有助于帮助测试人员
决定如何测试界面。 
 

--
※ 来源:·哈工大紫丁香 bbs.hit.edu.cn·[FROM: dianqi.hit.edu.cn]
[百宝箱] [返回首页] [上级目录] [根目录] [返回顶部] [刷新] [返回]
Powered by KBS BBS 2.0 (http://dev.kcn.cn)
页面执行时间:3.305毫秒