by Phil Martin
“What do these programming basics have to do with security?”, you might be asking. The reason it is important is that not only do we need to understand the limitations on what can be stored in a variable of a specific data type, we also need to understand the possible operations we can carry out on each data type. If we fail to grasp these concepts, then conversion errors will happen that will eventually lead to security issues.
When a numeric data type is converted to another numeric data type, it can undergo a widening conversion, or a narrowing conversion. Sometimes we use the terms expansion and truncation, respectively, to mean the same thing. Widening happens when we convert from a smaller data type to a larger one – we widen the number of bytes used to hold the value. For example, if we convert an Int16 to an Int64, we are placing the same value in a data type that can hold a much greater range of values. The unused bytes in the Int64 data type are set to zero so that the value remains the same. Figure 69 illustrates a widening conversion.
Figure 69: A Widening Conversion
A narrowing conversion, as shown in Figure 70, converts a larger data type to a smaller one, resulting in a loss of information. As an example, if we try and convert an Int64 to an Int16, there will not be enough space to hold the entire value. While the conversion will appear to succeed because an exception is not generated (usually), the resulting value will more than likely not resemble the original value as the bits will not align at all. Another type of narrowing conversion is trying to convert a value that holds fractions such as a float, to an integer. In this example we try and convert “29.32” to an integer, but because the integer value by definition does not understand fractional values, we wind up with “29”. To avoid these types of problems, we can use input length and range validations using regular expressions, impose maximum length protections, and implement proper exception management patterns.
Figure 70: A Narrowing Conversion
Figure 71 shows the valid widening conversions we can carry out.
Type
Can be converted without loss of data to…
Byte
UInt16, Int16, UInt32, Int32, UInt64, Int64, Single, Double, Decimal
SByte
Int16, Int32, Int64, Double, Decimal
Int16
Int32, Int64, Double, Decimal
UInt16
UInt32, Int32, UInt64, Int64, Single, Double, Decimal
Char
UInt16, UInt32, Int32, UInt64, Int64, Single, Double, Decimal
Int32 and UInt32
Int64, Double, Decimal
Int64 AND UInt64
Decimal
Single
Double
Figure 71: Conversion of data types without loss
Unmanaged vs. Managed Code
All mainstream programming languages can be classified as either managed or unmanaged. Unmanaged programming languages, such as C and C++, have the following characteristics:
There is no runtime execution environment in between the code and the OS.
Code compiles to native code which will execute only on the processor architecture for which it is targeted.
Memory allocation and deallocation must be explicitly carried out in code.
Developers are required to check array bounds, handle data type conversions and release memory when done.
While unmanaged code runs faster, it is more susceptible to attacks such as buffer overflows and string formatting vulnerabilities, and it requires a great deal more developer expertise than managed languages.
Managed languages such as Java and .Net, which include both C# and VB.Net, have the following characteristics:
There is an execution runtime environment sitting between the code and the operating system.
Code is not directly compiled into native code, but rather some type of intermediate object code.
Memory allocation and deallocation is automatically handled by the runtime environment.
The time required for development is faster since the developer does not have to worry about memory management, bounds checking, garbage collection and type safety checks.
While it executes slower than unmanaged code, managed code results in far fewer defects due to memory allocation and bounds checking, is less susceptible to attacks such as buffer overflows, and requires less developer expertise in the lower level bowels of the operating system. In general, managed code is far superior to unmanaged code unless performance and memory requirements are at a premium.
The Common Language Runtime (CLR)
The CLR is the managed runtime environment for .Net languages. .Net converts code into a Common Intermediate Language, or CLI, which allows most .Net languages to work together at run-time as if they were one language. For example, a single application could be written partially in both C# and VB.Net.
At run-time, the CLR’s just-in-time compiler, or JIT compiler, transforms CLI into machine instructions for the native processor. The CLR has its own security execution model that is quite a bit more powerful than the one most OSs come with. Operating system models are generally based on user identity, but .Net implements something called code access security that calculates permissions based on code attributes such as the URL, the site, the application directory and other values. The rest of how this security works is discussed a little later.
Java Virtual Machine (JVM)
The other common managed environment is Java, which runs on the Java Runtime Environment, or JRE. The primary components within the JRE are the Java Virtual Machine, or JVM, Java Package classes, and other runtime libraries. The JVM is the one who loads Java programs and executes them. Some of the most important aspects of the JRE are the following:
Java Class file format
Java Bytecode language and instruction set
Class Loader
Bytecode Verifier
Java Security Manager (JSM)
Garbage collector
Figure 72: Java Virtual Machine Activities
The Java Class File controls how the content is stored and accessed in a manner that is platform independent. Source code is compiled into Java Bytecode, which includes all instructions necessary to perform low-level machine operations such as push and pop, manipulating CPU registers and performing arithmetic and logical operations. The Class Loader is a Java runtime object that, obviously enough, loads Java classes into the JVM after performing some security checks. The JVM calls the Bytecode Verifier, which is probably the most important component of the JVM when ensuring type consistency. If the Bytecode type checks are successful, the Bytecode is compiled to run-time format using a JIT compiler. The resulting object is then executed within the JSM, which mediates any calls outside of the sandbox and approves or denies a given call based on the configured security policy. Figure 72 shows the progression.
Buffer Overflow Canaries
We’re going to dive deep into what a buffer overflow attack is in just a while, but since we’re about to discuss compiler switches we need to quickly talk about canaries. To aid in combating buffer overflow attacks, compilers can add additional code that will allocate space on the stack in which a security cookie, or a canary, is placed. If a buffer overflow occurs, this security cookie will be the first thing to be overwritten and mangled. Therefore, after every function is executed, if the security cookie does not match the value put on the stack, then the code knows something went wrong that resulted in a buffer overflow. This value is called a canary after the use of canaries in mines to detect deadly gas emissions – if the canary passes out, then we know something bad has or is about to happen. Figure 73 illustrates the use of an overflow canary.
Figure 73: How an Overflow Canary Works
Visual Studio supports the /GS command line option to enable and disable this feature. In the GCC open source compiler, the feature is called StackGuard. Both implement canaries, albeit in slightly different manners.
Encryption
We’re about to get into source code concepts, b
ut one last detour before we do. Let’s revisit encryption and add some details that would cause other roles such as Product and Project to pass out in a TMI (too much information) coma.
Hashing
An interesting thing to note with hashing algorithms is that the output will always be the same regardless of the length of the input plain text. A 128-bit hashing algorithm will produce a digest 128 characters in length for both a 5-character string as well as a 5,000-character string. MD5 was the most common hash algorithm originally, followed by SHA-1, both of which have been proven to have fatal weaknesses. SHA-2 is now the golden standard, and SHA-3 is waiting in the wings when the day comes that SHA-2 is found to have a weakness.
There are many different hashing algorithms out there, and not all of them provide the same level of security. For example, a robust hashing algorithm should not produce the exact same hash value for two different plain text inputs. If it does, then it is said to create a collision. Even if the chance of this happening is extremely low, an attacker could launch a birthday attack against a hashing algorithm. This attack gets its name from a type of brute-force attack in which two people might have entered different birthdays, and if a hashing algorithm produces the same hash for both birthday values, then it could be exploited. A secure algorithm will be collision free, meaning that it will never generate the same hash value for two different input values.
To make hashes even more secure, a good algorithm will allow a salt to be specified that makes the resulting hash even more unpredictable. A salt is a string of random bytes that is fed into a hashing algorithm but should be unique to each value in a set. For example, let’s say we have a list of users and passwords, with each password being stored as a hash instead of the actual value. If an attacker were to get hold of the list of hashed passwords, he could compare it to a huge list of pre-hashed values to see if there is a match. This list of pre-computed hash values is called a rainbow table and is often used in successful attacks. However, what if we assigned each user a unique salt that was used when generating the password hash? Now, it probably will be impossible for the attack to use a brute-force rainbow table attack – or a dictionary attack - since he would also need to have the salt associated with each user.
Let’s quickly go over the most common hashing algorithms. Each differs in three major aspects – the length of the resulting digest, how collision-free the digest is, and the number of rounds of computations. MD2, MD4 and MD5 were the original batch designed by Ronald Rivest and produce a 128-bit digest, but all have been proven to be vulnerable to collisions. In spite of this, you will often find MD5 still in-use. All software should be using one of the Secure Hash Algorithms family, commonly called ‘SHA-X’, where ‘X’ is either a sequential number or the length of the output. While both SHA-0 and SHA-1 produced digest values of 160-bits, SHA-2 includes SHA-224, SHA-256, SHA-384 and SHA-512, which produce 224, 256, 384 and 512 bits of digest values, respectively. Another secure possibility is to use the HAVAL algorithm, which allows us to specify both the output length and the number of rounds of computations. HAVAL can produce a digest value from 128 bits in length up to 256 bits. As a rule of thumb, the longer the resulting digest, the more secure it is since it takes more work for an attacker to break through. In security-speak, the work factor increases with the length of the digest value.
The One-Time Pad
A one-time pad is the perfect encryption scheme and is the only one considered to be unbreakable if properly implemented. It was invented in 1917 by Gilbert Vernam, so is sometimes called the Vernam cipher.
It does not use alphabets like other ciphers, but rather uses a pad (or key) made up of random values. The plaintext message is first converted into bits, and then run through a binary mathematic function known as exclusive-OR, or more commonly called XOR. XOR takes in two bits and returns a single bit according to the following rules:
If the bits are the same, return 0.
If the bits are different, return 1.
Let’s say you have a plaintext of ‘1101’, and the pad is ‘1001’. If you XOR these together you wind up with ‘0100’ – that is the cipher text. The receiver must also possess the pad of ‘1001’. By performing the XOR operation again using the ciphertext and the pad, you get ‘1101’, which is the original plaintext. So, if you XOR A and B to produce C, and then you XOR B and C, you will always get back A. If you XOR A and C, you will always get back B.
The one-time pad is unbreakable only if the following are true:
The pad must be used one time only.
The pad must be at least as long as the message.
The pad must be securely delivered to the recipient.
The pad must be protected at both ends.
The pad must be made up of truly random values.
You might think that the last requirement of random values would be easy with today’s computers. Unfortunately, it is deceptively difficult for computers to generate truly random values. In fact, a computer-based number generator is only capable of creating pseudorandom numbers. So, while the one-time pad sounds great in practice, it is impractical in most situations, primarily when computing power is not available.
Core Programming Concepts
Now it’s time to focus on how to write secure code. Just like there are some core security concepts across all roles, there are four security concepts that are specific to writing secure code – encapsulation, cohesiveness, single responsibility and coupling.
When a module has a high degree of encapsulation, it ‘hides’ its internal complexity from processes consuming the module. Programmers sometimes refer to a highly-encapsulated process as being a ‘black box’, referring to the entire module appearing as a simple non-descript ‘thing’ that hides whatever it is doing inside of the box. Well-encapsulated modules expose only the minimum interfaces required to get data into and out of the module.
A cohesive module is designed to perform one and only one logical operation. The degree of cohesiveness indicates the strength to which the various responsibilities of a module are related. For example, a module that can authenticate a user, calculate price changes, and charge a credit card has an extremely low cohesive factor. But if the module were simply restricted to accepting a user name and password and providing a Yes/No answer representing the success or failure of the authentication process, then cohesiveness is very high.
The goal is to make all modules with a high degree of cohesiveness. Of course, this can be taken too far if an abnormally large amount of code is required to tie several modules together – this would indicate that the level of complexity is too low, and the module should take on more responsibilities. As an example of over-rotating on cohesiveness, we might encounter three modules – one to validate that a username exists, one to hash a password, and one to validate that a hashed password for an existing username is correct. It would probably make more sense to combine all three into a single module since all functions are very closely related to the authentication process. Figure 74 illustrates the relationship between cohesiveness and coupling.
Cohesiveness is closely related to the single responsibility principle, which states that a module should have responsibility over a single part of the overall functionality provided by software, and that responsibility should be entirely encapsulated by the module. As a module follows this principal, cohesiveness should increase as well.
Coupling reflects the degree of dependency between modules, or how dependent on each other they are. A low degree of coupling is desired, as this increases the reusability of each module. For example, suppose we want to update a record in a database through a series of modules. A high degree of coupling might require us to use one module to locate a record identifier, which is then passed into a second module to construct a SQL statement, which must then be fed into a third module to execute the statement. This represents a high degree of coupling and complicates just about everything. Instead, we can embed SQL construction logic into the third m
odule, thereby reducing the level of coupling with the second module.
Another way to measure the degree of coupling is to assess how hardcoded or rigid the interaction between two modules are. The more knowledge each has of the other, the more coupled they are. The use of interfaces instead of class names, or the introduction of a message-based communication mechanism between modules both reduce coupling.
Figure 74: The Relationship Between Cohesiveness, Coupling and Quality
Unit Testing
We’re almost to the point of talking about actual, real-world vulnerabilities and how to code around them, but let’s pause for a moment and consider unit testing. While most subjects on testing belong to the Testing role, this type of testing is a wholly developer-centric activity.
Unit testing is the most basic type of test and is actually built and executed by the developer, not a software tester. Creating unit tests requires forethought and planning, and increases the up-front time required to generate code. However, it is actually a great time-saver in the long run for four reasons: