15 Tips for Secure Win32 Programming

来源:互联网 发布:湖南卫视 知乎 编辑:程序博客网 时间:2024/05/18 13:31

source from:http://archive.devx.com/upload/free/features/zones/security/articles/2000/12dec00/mh1200/mh1200-1.asp by  Michael Howard

Why with so many Win32 programmers out there do we have a lack of Win32 secure programming checklists? Beats me, but it's time to fill the void. Here are 15 do's and don'ts for keeping your Win32 programming secure.

1. Beware of Dangerous Functions
Certain function calls are simply dangerous. Incorrect use of these functions may lead to buffer-overruns (see my previous "Best Defense" article, Testing for Buffer Overruns), which if you're lucky will crash only your application and if you're unlucky may cause code to be injected into your process and executed. Here are some of the most common functions to look out for:

  1. strcpy (and variants such as lstrcpy, wcscpy, etc.)
  2. strcat (and variants such as lstrcat, wcscat, etc.)
  3. memcpy (and variants such as CopyMemory, _memccpy, bcopy, etc.)
  4. gets (and to a lesser extent, fgets)
  5. sprintf (and variants such as swprintf, vsprintf, etc.)
  6. scanf (and variants such as sscanf, swscanf, fscanf, etc.)

Analyze all calls to these functions carefully to make sure they are checking the boundary conditions correctly. A number of programs have gone as far as banning the use of these functions altogether, classifying their use as code defects or bugs.

The problem with functions 1 and 2 is that they copy data until the functions hit a null character, leaving them vulnerable to attack. An attacker could leave off the null or place it in a strategic position beyond the end of the buffer. At the very least, you should replace calls to these functions with the 'n' versions. So, rather than strcpy() use strncpy(), and rather than strcat() use strncat(). For example:

#define MAX_BUFF 80
char szBuff[MAX_BUFF];
strcpy(szBuff,szArg);
becomes
#define MAX_BUFF 80
char szBuff[MAX_BUFF];
strncpy(szBuff,szArg,MAX_BUFF);

Worried about what this might do to performance? See my Note from the Trenches.

Function number 4, the gets() function, is so dangerous that you should remove it from your code immediately. It copies user input directly into a buffer once it reaches the end of a line. There is no way to tell the function how much it should copy. Recently, I did a design and code audit of a security product and found instances of gets() in test code (luckily, not the core product!). We fixed the code by replacing gets() with ReadConsole().

Watch Out for Those Stack-Based Buffers
Be especially careful when you call any of the dangerous functions to copy data into stack-based buffers. Generally speaking, executing malicious code is much easier on a system where the buffer is allocated on the stack than where memory is allocated on the heap. For example, the following C/C++ code allocates a 64-byte buffer on the stack:

void foo() {
char buff[64];
}
While this code allocates code on the heap:
void foo() {
char *buff = malloc(64);
}
Also, be careful with _alloca(). It looks and feels like malloc(), but it allocates memory on the stack. Two string classes in particular, the Microsoft Foundation Classes (MFC) CString class and the Standard Template Library (STL) string class, are a little safer because they allocate data on the heap.

 

 

2. Use the /robust Switch
If you create services that use remote procedure calls (RPC) on Windows 2000, make sure you use the /robust MIDL compiler switch. This will add more stringent integrity checking and mitigate many denial of service attacks, which rely on malformed RPC packets.

3. Use Negotiate Rather than NTLM—It's Free
If your application uses the Security Support Provider Interface (SSPI) or you use technologies that use SSPI indirectly, such as RPC and DCOM, request the Negotiate SSP rather than NTLM if your code runs on Windows 2000. Negotiate will use Kerberos authentication automatically if it is available, otherwise it will use NTLM. For example, if you use RPC and you call the RpcBindingSetAuthInfo[Ex] function, then set the AuthSvc argument to RPC_C_AUTHN_GSS_NEGOTIATE rather than RPC_C_AUTHN_WINNT or RPC_C_AUTHN_DEFAULT. The same applies to DCOM's use of CoSetProxyBlanket() or CoInitializeSecurity(), however the authentication level is set in the dwAuthnSvc variable and SOLE_AUTHENTICATION_INFO structure, respectively.

4. Use Packet Privacy and Integrity—It's Free
Both DCOM and RPC allow the channel between the client and server to be both encrypted and integrity checked. The performance impact is minimal unless you are transferring masses of data. In the case of DCOM, this feature may be enabled in the COM+ Explorer or programmatically by calling CoSetProxyBlanket with dwAuthnLevel set to RPC_C_AUTHN_LEVEL_PKT_PRIVACY. When using DCOM, call RpcBindingSetAuthInfo or RpcBindingSetAuthInfoEx with AuthLevel set to RPC_C_AUTHN_LEVEL_PKT_PRIVACY.

5. Keep Good ACLs—They're Your Friends
Bad access control lists (ACLs) on objects such as registry keys, files, named pipes, mutexes, and so on are a very common problem (see my second Note From the Trenches). They allow malicious users to view and/or change the resource. ACLs on objects should specify who is allowed to do what to a particular object. In fact, I have a little rule: someone on the development team has to vouch for every ACE in the ACL and everyone has to agree. Also, make these the default settings, don't expect your users to set ACLs correctly—because they won't.

When creating securable system objects such as named pipes and semaphores, using NULL as the default ACL is generally okay, this means inheriting the security descriptor of the process. However, explicitly setting an empty security descriptor is very bad, this means the object has no access control and anyone can manipulate it. ACLs are your friends, use them, and learn how to use the SetSecurityInfo() function.

 

 

 

 

6. Run with Least Privileges
All code that runs on Windows NT or Windows 2000 runs in the context of a user account, therefore code runs with whichever privileges are allowed for that user account. If the process runs as a highly privileged account such as LocalSystem or an administrator then any malicious code in that process also will run with the elevated capabilities. The potential for damage is very high.

The lesson here is require only the privileges needed to get the job done. Do not require your application to run with system, administrative, or power-user accounts, because these accounts have special privileges. If you require such capabilities, support secondary logon using the CreateProcessAsUser() function. Another useful security measure is to start new threads with reduced privileges using the CreateRestrictedToken() function.

7. Don't Store Secret Information in Software
Securely storing secrets is always a difficult proposition. Simply put, it's next to impossible to secure private data in software. If you must store secret data, read my "Best Defense" article Storing Your Secret Data in Windows for further details about how.

8. Use CryptoAPI
Don't write your own cryptography code. Use the code built into the operating system. Generally speaking, the roll-your-own-crypto approach is not secure. Use the standard symmetric ciphers such as RC2, RC4, DES, and 3DES, hash functions such as MD5 and SHA-1, and asymmetric ciphers such as RSA. MSDN includes many samples and code snippets to help get you started, and I've posted some sample code for COM+ (written in C++) that you can use to do basic crypto stuff.

9. Add Security Comments to Your Code
Security comments are a guaranteed long-term time saver. If you have code that assumes some security requirements or does something security related, add a comment before the code like in this (incomplete) code snippet:

// SECURITY. Store the password using the Data protection API
// SECURITY. Password and extra entropy are passed to us from GatherDetails()
// SECURITY. pOut is a pointer passed to us from 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. Check Those Filenames
Many platforms have faced a number of file canonicalization errors in the past. For example, imagine you have a program that accepts filenames and returns the file to the user. However, you don't allow anyone to access a special file named ServerConfiguration.xml. If you see a request for this file, simply return "File not Found." Don't send "Access Denied" because an attacker will know that the file exists, he just won't have access—yet! However, the attacker could access the file by using the FAT 8.3 filename (Server~1.xml) instead, and because you don't check for this filename the attacker will know your server configuration. So make sure you check that all file requests are valid, or better yet, don't make security decisions based on the name of a file.

 

 

11. Allow Long Passwords
Versions of Windows prior to Windows 2000 allowed 14-character passwords, Windows 2000 supports passwords up to 127 characters. Don't hard-code 14-character password limits in your application.

12. Clear Out Unused Secrets
Zero out secret data once it no longer has any use. Whether the data is stored temporarily in memory or written to disk, overwrite the data once you have finished using it. It is possible that secret data may be written to the pagefile. At the very least, use ZeroMemory() to scrub the data. If you are truly paranoid, which isn't a bad thing, use code like this:

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. Check the Return Conditions from All Security-Related Functions
Checking return values is standard programming practice, but it is doubly important when a security function fails. For example, I once saw some code that called RpcImpersonateClient() but failed to check whether the function succeeded or that its return status was RPC_S_OK. I missed that the next line of code accessed some sensitive data and returned it to the user. What's tricky is a call may be made in the context of the calling thread if the impersonation fails, which in this case was LocalSystem. The ACL on the resource being accessed included only those with Administrators (full control) and SYSTEM (full control) status; all other users should have been denied access. However, upon impersonation failure, the user gains access to the resource...very bad!

14. Write to the Event Log
Log anything that relates to security to the Windows NT/2000 event log, such as failed access to your private objects for example. Don't log anything that the OS may already log such as logon and logoff, it just creates more work for admins. Use the RegisterEventSource() and ReportEvent() functions. If you are writing scripts, then use the Windows Script Host WScript.Shell object, it has a LogEvent method.

15. Document Your Interfaces
Document the named pipes, sockets, RPC and DCOM protocols, and ports you use. This has two benefits: (1) it helps firewall admins determine which ports to open on the perimeter and (2) helps the testers determine how to test the interface.

The Checklist You've Been Waiting For
Keep these tips closeby and refer to them whenever you get into your Win32 programming. Now you have the security checklist that's been missing from your Win32 documentation. Don't say I never gave you anything.

原创粉丝点击