Code Review

Code Fortification

Insecure Coding Practices

Most of the previous exploits are made possible due to poor coding practices during development. You should attempt to leverage these mistakes whenever you can. The following are examples of insecure coding practices. Note that these apply to most types of software, not just web apps:

  • Lack of input validation. This negligent practice alone is responsible for most of the injection and scripting attacks mentioned in this topic.
  • Hard-coded credentials. There are many ways these passwords could be exposed, including through SQL injection and XSS.
  • Storage and/or transmission of sensitive data in cleartext. You can sniff cleartext data transmitted over a network, or read exfiltrated cleartext data with minimal effort.
  • Unauthorized and/or insecure functions and unprotected APIs. Sometimes these functions and APIs are kept around for compatibility purposes, despite the security risk. You may be able to leverage the weaknesses in these functions/APIs.
  • Overly verbose errors. Whether intentional or not, some apps reveal a great deal about a code’s structure and execution through error messages returned to the user. A simple form injection might return an SQL error revealing a table’s column names, for example.
  • Lack of error handling. Although revealing too much in an error can be a problem, not handling errors at all is an even bigger problem. For example, an app may not respond gracefully to unexpected input, crashing the app or corrupting data.
  • Hidden elements. Just because an element is not immediately visible on the page doesn’t mean you can’t find it by exploring the page’s code. In particular, you may be able to find sensitive data that is exposed in the page’s DOM (i.e., the hierarchical tree-like structure of the HTML) but not displayed on the screen.
  • Verbose comments in source code. Comments are not meant to be truly hidden from the client, just suppressed in the marked-up page. Some developers forget this and include sensitive information in comments, such as server-side functionality, snippets of old code, and other information that can help you identify weak points or attack vectors.
  • Lack of code signing. Code that lacks a digital signature cannot be validated for its authenticity and integrity. It may be easier to inject malicious code into a running process when no mechanisms exist to compare that code against the authorized code.
  • Race conditions. These occur when the resulting outcome from execution processes is directly dependent on the order and timing of certain events. Issues arise if these events fail to execute in the order and timing intended by the developer. For example, an app can check that a file exists and then use it later. You may be able to replace the file after it is checked by the app but not yet used; this can trigger app instability or privilege escalation.

Reverse Engineering

Reverse engineering, as applied to software, is the process of breaking down a program into its base components in order to reveal more about how it functions. One example of reverse engineering is the attempt to analyze a program’s implementation of digital rights management (DRM) copy protection mechanisms. If enough is learned about how the copy protection works at a lower level, it can be broken.

Even if you don’t have access to an app’s source code during your pen test, you may be able to obtain the app’s binaries or capture information about the app during execution; this can enable you to reverse engineer the app to look for potential weaknesses in design, programming, or implementation.

When it comes to software, there are three primary methods of performing reverse engineering: decompilation, disassembly, and debugging.

Use an Obfuscator!


Decompilation is the reverse engineering process of translating an executable into high-level source code. This typically involves translating the machine language code of compiled binaries into the source code that the software was written in before being run through a compiler. However, decompilation can also involve translating intermediary bytecode that is normally executed by an interpreter into the original source code.

Being able to deconstruct an executable into its source code means that you don’t just need to rely on dynamic analysis to test a target app. You can use it to recover lost source code, as well as examine malware. You can also perform static code analysis to correct errors. Decompiling an app will help you determine if the app’s logic will produce unintended results, if the app uses insecure libraries and APIs, and if the app exhibits any of the other poor coding practices that developers can fall prey to.

Some apps are easier to deconstruct than others. For example, the nature of the class files in the Java programming language enables them to be easily decompiled into source code. You can therefore reverse engineer apps written in Java with freely available, easy-to-use tools. However, some languages and third-party tools are designed to obfuscate source code before it is compiled. Obfuscated code is difficult to dissect because it uses convoluted and non-straightforward expressions that are not friendly to human analysis. For example, the name of a string variable in the source code might be something simple and self-explanatory like count, but in the decompiled code, it may appear as a seemingly random combination of numbers, like 42893285936546456421324. This makes it more difficult for a human reviewer to understand and retain the variable’s purpose, as well as trace the variable throughout the code.

The following table compares some popular decompilers.

VB DecompilerUsed to restore source code for Visual Studio .NET compiled applications.
Delphi Decompiler (DeDe)Used to restore source code for executables compiled with Delphi Builder, Kylix, and Kol.
Hex-Rays IDAConverts native processor code into a human readable C-like pseudocode text. Supports compiler-generated code for x86, x64, ARM32, ARM64, and PowerPC processors.
dotPeekDecompiles .NET assemblies to C#. Supports multiple formats, including DLLs, *.exe executables, and Windows *.winmd metadata files.
CFF ExplorerDisplays the programming language and platform the software was developed in.

Note: For more information on decompilers, see

Disassembly and Debugging

Disassembly is the reverse engineering process of translating low-level machine code into higher level assembly language code. Assembly language is lower level than typical source code, but it is still human readable and can include familiar programming elements like variables, functions, and even comments. Like decompilation, the purpose of disassembly is to better understand how an app functions in ways that might not be visible during normal execution. A tool that performs disassembly is called a disassembler.

Disassembly certainly has its disadvantages when compared to decompilation. Assembly code is not as concise as high-level code; it’s more repetitive; the linear flow of the code is not as well structured; and, of course, it requires knowledge of assembly, which not many people possess. However, disassemblers tend to be more common than decompilers, as accurate decompilation is difficult. Likewise, disassembly is deterministic—in other words, a machine code instruction will always translate to the same assembly instruction. In decompilation, translating one machine code instruction can result in multiple different high-level expressions.

Note: Hex-Rays IDA is also a disassembler/debugger.

Debugging is the process of manipulating a program’s running state in order to analyze it for general bugs, vulnerabilities, and other issues. You manipulate its running state by stepping through, halting, or otherwise modifying portions of the program’s underlying code, directly affecting the program as it executes. Debuggers are common in integrated development environments (IDEs) for developers to debug code as they write or test it, but they can also be used on compiled software as a form of interactive reverse engineering. These debuggers can include a decompiler for modification of source code, but more commonly they include a disassembler for modification of assembly instructions during execution.

Debugging can aid a pen test because it not only translates machine code for static analysis, but also enables you to change that code and perform dynamic analysis on the program to see its effect. This can make it much easier to understand how an app functions and how it might be vulnerable.

The following table summarizes some popular disassembler/debugger tools.

OLLYDBGA debugger included with Kali Linux that analyzes binary code found in 32-bit Windows applications.
Immunity debuggerA debugger that includes both CLIs and GUIs and that can load and modify Python scripts during runtime.
GDB(GNU Project Debugger) An open source debugger that works on most Unix and Windows versions, along with macOS.
WinDBG(Windows Debugger) A free debugging tool created and distributed by Microsoft for Windows operating systems.

Static Code vs Dynamic Analysis

Static code analysis is the process of reviewing source code while it is in a static state, i.e., it is not executing. Static code analysis can be done manually by human reviewers or it can be done automatically by analysis tools that detect common mistakes in code. As a pen tester, if you have access to a target app’s source code, you can perform static analysis to discover how the app functions and potentially identify security issues and bugs that result from programming mistakes or poor coding practices. Another term commonly used in the industry is SAST, or static application security testing.

Static analysis reveals a low-level perspective of an app’s logic. This perspective can provide you with details that you might not get from testing an app during execution. For example, there may be limitless permutations of input that can be handled by the program’s logic; by understanding the input handler routine itself, you might be able to gain instant insight into any potential problems.

Because you’re reviewing code, static analysis requires familiarity with whatever language the app is written in. In many cases, you’ll need to be quite proficient in the target language in order to actually spot issues that the developer missed. Any tools you employ to automate the process will likely target specific languages and may not work with all source code.

Dynamic Analysis

As opposed to static code analysis, dynamic analysis is the process of reviewing an app while it is executing. This helps reveal issues that a static code analysis may miss, as some issues are more easily identifiable when a program is running and accepting unpredictable user input. For example, even if you know the code behind an input handling routine, you won’t necessarily understand how it could be problematic until you actually feed it into an input that causes a problem. Another term commonly used in the industry is DAST, or dynamic application security testing.

As a pen tester, you’re more likely to test applications dynamically than to test source code statically. Just as you’ve attempted to exploit web apps, you may be called on to exploit desktop apps, server apps, mobile apps, and more. Testing the apps’ inputs for weaknesses to DoS, privilege escalation, etc., is the most common form of dynamic analysis, but not the only one. You can also test an app’s behavior as it executes on specific platforms or custom environments. Testing the running app to see how it interacts with other running apps might also reveal security issues with interprocess communication.

Unlike static analysis, you don’t necessarily need to be familiar with the language the app was written in. However, in some cases, knowing the language can guide your efforts. Dynamic analysis can also be conducted manually or with tools that automate the testing process.

Guidelines for Testing Source Code and Compiled Apps

When testing source code and compiled apps:

  • Perform static code analysis of any source code you obtain in the pen test.
  • Use static code analysis to look for vulnerabilities in the code.
  • Perform dynamic analysis of compiled apps you target in the pen test.
  • Test an app’s inputs, behavior in specific environments, and interaction with other apps.
  • Use automated tools to optimize the static and dynamic analysis processes.
  • Use fuzzers to send an app’s input random or unusual values.
  • Reverse engineer software to learn more about how it works.
  • Use a decompiler to translate a binary executable into high-level source code.
  • Understand that decompiled code can be obfuscated and not completely true to the source.
  • Use a disassembler to translate a binary executable into assembly code.
  • Understand that disassembled code can be difficult to read.
  • Use a debugger to perform interactive reverse engineering on an app.

Follow NASA’s 10 Rules:

  1. Avoid complex flow constructs, such as goto and recursion.
  2. All loops must have fixed bounds. This prevents runaway code.
  3. Avoid heap memory allocation.
  4. Restrict functions to a single printed page.
  5. Use a minimum of two runtime assertions per function.
  6. Restrict the scope of data to the smallest possible.
  7. Check the return value of all non-void functions, or cast to void to indicate the return value is useless.
  8. Use the preprocessor sparingly.
  9. Limit pointer use to a single dereference, and do not use function pointers.
  10. Compile with all possible warnings active; all warnings should then be addressed before release of the software.

Leave a Reply

Your email address will not be published. Required fields are marked *