使Win32程序更安全的15个小技巧

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等间接使用SSPI的技术。如果应用程序是在Windows 2000上运行的,则要求使用Negotiate SSP而不是NTLM。例如,如果你使用了RPC并调用了RpcBindingSetAuthInfo[Ex]函数,则应该将AuthSvc参数设置为RPC_C_AUTHN_GSS_NEGOTIATE而不是 RPC_C_AUTHN_WINNT或RPC_C_AUTHN_DEFAULT。这也适用于在使用DCOM的情况下对CoSetProxyBlanket()或CoInitializeSecurity()的调用。其授权的级别被分别设置在dwAuthnSvc变量和SOLE_AUTHENTICATION_INFO结构中。
4、使用分组的隐私和完整性检查机制
使用DCOM和RPC都能够对客户机端和服务器之间的通道进行加密和完整性检查,除非我们传送的数据量非常大,它对性能的影响是非常小的。如果使用DCOM,我们可以在COM+管理器中或通过在程序中调用将dwAuthnLevel设置为RPC_C_AUTHN_LEVEL_PKT_PRIVACY的CoSetProxyBlanket执行相应的操作。如果使用RPC,可以调用将AuthLevel设置为RPC_C_AUTHN_LEVEL_PKT_PRIVACY的RpcBindingSetAuthInfo或RpcBindingSetAuthInfoEx执行相应的操作。
5、保持良好的ACL
注册表、命名管道、互斥的共享变量等对象糟糕的访问控制列表(ACL)是一个常见问题,它们能够使黑客浏览或改变系统中的资源。对象的ACL应该指明哪些用户能够操作指定的对象。通过长期的实践,我总结出了下面的规律:开发团队中的某个成员负责ACL中的每个ACE,开发团队中的所有人都遵守所有的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)有助于帮助测试人员决定如何测试界面。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值