We have all seen articles in the news, seemingly every day, about security holes and vulnerabilities exposed in Embedded and Mobile Platforms and the software which power them. These security holes impact the quality of the product, the company that produced the product and the end user. Sometimes these security holes are introduced when product development efforts gets into a ‘rushed’ state and as a result fundamental security measures are skipped. While in this ‘rushed state” some security measures are overlooked and sometimes they are thrown out as they are perceived as being unnecessary and an additional expense.
Security is a huge topic, too big to cover completely in a single blog, but I want developers to think more about the basic building blocks that are the foundation for a stable and secure platform. In this blog, I will keep the subject mater generic enough to cover more than “just platform” or “just application” development. I will also avoid discussing product security processes and policy management. For process and policy management, I recommend reading the book by Microsoft, SDL – Security Development Life cycle. The book and supporting web pages contain a great deal of information about how to handle processes and procedures with respect to security issues during the product life cycle.
In the section that follows, I will go over what I consider the basics to obtain a secure implementation and set the stage for future blog articles where I can provide more details on individual aspects of security. Below is what I consider the top 15 areas a developer/architect should always consider in a solution design and implementation. (The tips below are mainly covering C/C++ development and the Windows CE/Windows Embedded platforms however, several of these rules apply to other platforms.)
Top 15 Security Tips:
- Safeguard your external interfaces: Make no assumptions about values passed to publicly exposed (exported) interfaces you may create. Validate and limit arguments that are passed. If a “char pointer” is passed, make sure the next argument is a “size” argument and validate the values being passed in against allowable ranges. If an interface accepts an enum, then make sure the values received are within the range of the defined enum. If during your argument validation the values are not acceptable, then exit gracefully and log/report an issue.
- Fortify your internal interfaces: Validate your arguments which are passed to internal functions. If the platform already provides a #define to identify a ‘debug’ build, then place conditionals around the argument validation (example: #ifdef/#endif blocks in C/C++) where execution speed is critical. The fortified interfaces can help prevent a bad passed argument (garbage) coming into a function or method and causing bigger issues (remember the old rule “GIGO” (Garbage in, Garbage out). Trap the garbage early on to reduce debug and trace time.
- Test Pointers Vigorously: Validate passed pointers vigorously (this applies to exposed and internal functions). Pointers are one of the biggest areas of compromise and issue in an implementation Too many times engineers will accept a “void pointer” (ex: myfunc(void *myval)). Make sure pointers that are passed on an interface are ‘typed’ pointers and an additional argument includes the size of the memory the pointer references (ex: myfunc(mystruct *myval, int mystructsize)). Make sure the function/method implementation verifies the passed pointers are of acceptable values. For example; if null is not an allowed value, then the implementation should verify that the passed pointer value is not null. If a pointer to a structure/struct is used then the implementation should validate that the passed size is the size expected for the structure (this is especially true for exposed interfaces). Additional testing can be done If the pointer is to a destination memory range by clearing the entire destination memory range. The validation would first check to see if the pointer is valid, and then validate if the passed size of the memory block is also valid and then test accessing the memory range by a simple memset() (or other approve secure API) on the entire range of memory to initialize the memory to all zeros. This sounds like a basic initialization technique, however it also tests access rights to the memory by this task/thread context. Some operating systems (like Windows Embedded and Windows CE) will throw an assert (or general access violation) if the memset() is being executed on an invalid range of memory. The cleared memory range will also aid in debugging and help prevent rouge data from finding it’s way into your implementation.
- Test your exposed interfaces rigorously: Utilize the best tools to help unit test any exposed interfaces you may have. These tools can be wonderful when version 2.0 comes out of your product and helps automate the testing process. When tools are not available, consider developer written unit tests which test with acceptable data and invalid data to verify error handling is working correctly.
- Leverage OS Features: If your platform allows for signed certificate validation, then utilize the functionality for all exposed interfaces to ensure your exposed functions are being used by an allowable/signed application. Windows CE platforms have a good architecture for validating what processes can attach to your DLL. Evaluate and consider making the validation ‘strict’ to require that the applications or other DLLs attaching to your DLL is not just using any certificate on the platform but a certificate of your own that only your product line uses.
- Deprecate insecure functions: Quickly deprecate unsafe functions that are used in your platform. In the Windows/WindowsCE product lines, replacements for standard/traditional “string” functions like strlen(), strcpy(), strcmp() are available and will help prevent buffer overrun issues/attacks. Spend the time to investigate what you should deprecate and enforce rules in your product development to assure these deprecated insecure functions are not used.
- Use logging to your advantage: This can be a sensitive topic as logging can also expose a weakness in a product. Evaluate where (or when in the development-test-cycle) logging is appropriate. Then Include logging throughout your application to log errors and warnings when issues arise.
- Never assume encryption alone keeps data secure: This is a big topic but I want engineers to understand that just ‘encrypting’ data does not make data completely secure. If the keys for the encrypted data are easily compromised then the data encrypted with those keys is just as safe as if the data was not encrypted at all. Investigate what level of encryption and possible key management techniques that are needed. I strongly recommend future research on this topic. I will follow up on this specific topic in more detail in the future.
- Validate all loaded configuration data: Validate the values read from any configuration files you may be using within your solution. Evaluate if keeping the configuration data encrypted with a technique where you can detect if the data was tampered.
- Be aware of what data you log to your log file: Never log security credentials to a log file! This sounds like a basic rule, but more times than you would think I have audited software at other companies to find that a developer had logged sensitive or confidential data.
- Lock down external references on DLLs: Reference count the number of times your shared DLLs are loaded and make sure it does not exceed your architectural design. One approach to counting the number of times your shared DLL is loaded is by using the DLL primary function DllMain(HANDLE hinstDLL, DWORD dwReason, LPVOID lpvReserved) and evaluating the the fdwReason values DLL_PROCESS_ATTACH, DLL_THREAD_ATTACH, DLL_PROCESS_DETACH and DLL_THREAD_DETACH. This is traditionally the same place you will add code to look up and validate the certificates that were used on the processes that is loading your DLL. Use the ATTACH and DETACH points to count the number of references and what application are referencing the DLL. Determine in your architecture what is the maximum number of times your DLL should ever be loaded and return false when your count is exceeded. The most powerful use of the DLL_PROCESS_DETACH/DLL_THREAD_DETACH notifications is by using these notifications to report if one of your critical .EXEs (or threads) is ever terminated. Upon this critical termination notification you have the ability to scrub memory, dump keys, and then reboot the platform.
- Implement well placed “firewalls” within your code: Have these small ‘firewalls’ serve as ‘sanity checks’ for your values during execution within your functions/methods and exit gracefully when failure/error conditions arise. One form of these firewalls is validating your pointers (as discussed previously). Another form would be validating the returned values from functions/methods you invoke.
- Sanitize your allocated memory after use: “Scrub” memory with all zeros before memory is freed/released. Do this for any type of memory that was allocated and then freed.
- Never assume obfuscation is good enough for keeping sensitive data secure: Choose an encryption level that is suitable for your needs and keep the encryption keys safe and changed frequently.
- Educate yourself: Nothing pays off more than learning about other techniques and tools available to make your solutions more secure. It is also critical to stay up to date on which approaches are not recommended or have been deprecated from best practices (ex: SHA1 vs SHA256).
In closing, I hope this blog has provided you with some ideas and gets you thinking more about security and how to make your products/solutions more stable and secure. In the near future, I will follow up more detailed information on some of these topics and possibly expand this list in the future. Happy and secure coding!