Security basics

This page deep dives into the basics of app hacking and reverse engineering, and helps to understand how easy it can be to exploit unaddressed vulnerabilities.

To protect an app, you first need to understand to hack it.

Understanding why and how apps get hacked, knowing where to look, and which tools to use provides a solid foundation for implementing an effective protection mechanism.

Reverse engineering

In the context of app hacking, reverse engineering is the process of analyzing an app to understand its internal workings, usually with the goal of finding vulnerabilities, copying features, breaking purchase validation, or inserting malicious code. This is usually done without access to the original source code, instead working from the compiled binary.

Reverse engineering can involve different techniques that vary in their purpose and complexity, but all pursue one objective – to discover and exploit vulnerabilities. Broadly, it can be broken down into four stages: static analysis, dynamic analysis, network analysis, and documentation.

Static analysis

Static analysis is a method of analyzing code without executing it. It is usually the first step and the easiest way to find vulnerabilities. This often involves using disassemblers and decompilers to convert machine code into assembly and high-level language representation, such as Swift or Objective-C.

Disassembling

Disassembling is the process of converting machine code into assembly language, which is more human-readable than raw binary. However, it can still be very difficult to interpret if you are not familiar with the specific assembly language.

Decompiling

Decompiling is the process of transforming machine code into a high-level language with the goal of recreating the source code in a more accessible form. Decompilers use various techniques to analyze the code and infer high-level abstractions such as loops, conditionals, and data structures. It is often done after disassembling as a further step.

Digest:

Static analysis can provide important metadata about the app, such as the libraries it links against, the architecture it's built for, and potentially even which compiler was used.

Disassembling can reveal hard-coded URLs, encryption keys, and access tokens, especially when scanned against common names, such as url, license, certificate, etc.

Compared to assembly, decompiled code is much easier to understand and work with, allowing for faster exploration of the code and a better understanding of what it does.

Looking at the function calls made by an app, especially to operating system APIs, can provide a sense of what the code does and how it interacts with the system.

Using a loosely configured compiler may allow to restore the original code with very high accuracy, including meaningful declaration names, class structures, and functions logic.

Reverse engineering tools can help automate parts of the static analysis process, using techniques such as pattern matching to identify common code structures or routines.

Dynamic analysis

Dynamic analysis focuses on analyzing the behavior of code while it’s running. Typically, this involves using debugging, binary instrumentation, and profiling techniques.

Debugging

Debugging analyzes step-by-step how an app works, allowing inspection of the current state at any given point. A debugger can step through code execution, track variable values, watch the call stack, set breakpoints and watchpoints, and control the execution flow. Additionally, log analysis tools can help tracking, searching, filtering, and visualizing logs generated by an app during execution, allowing to uncover patterns, spot anomalies, and track events.

Instrumentation

Dynamic binary instrumentation (DBI) allows runtime analysis and modification of an app behavior by injecting code to manipulate its functionality, analyze performance, detect vulnerabilities, and bypass security measures. It provides a detailed look at the program's operation, enabling an understanding of intricate details that might be not otherwise visible.

Profiling

Profilings looks at an app’s runtime behavior and measures the complexity of an app in terms of time and space, identifying parts that are time-consuming or resource-intensive and showing performance spikes, memory allocation patterns, and interactions with the operating system. Unlike instrumentation and debugging, profiling doesn't usually involve altering the program's state or behavior and operates at a higher level, focusing more on metrics than on individual instructions.

Digest:

Dynamic analysis provides insights into an app's real-time behavior, helping to understand the logic flow and state changes that occur during its execution.

Dynamic analysis allows for real-world scenario testing, providing insights into how the app responds to various inputs and conditions.

Compared to static analysis, it offers an alternative perspective for discovering vulnerabilities that are too complex to be found through static analysis alone.

While debugging and profiling are commonly used during development, they can also be used for reverse engineering other apps.

Dynamic binary instrumentation can analyze every instruction and is typically used when a highly granular level of information is required.

Profiling can reveal sensitive information about an app’s resource utilization patterns and data management to detect expensive operations, like encryption and obfuscation.

Neglecting to incorporate safeguards, such as debugger detection, during the compilation process can leave an app exposed and vulnerable to dynamic analysis.

In addition to identifying security flaws, dynamic analysis also plays a role in exposing poor coding practices, inefficiencies, and architectural weaknesses within an app.

Network analysis

Network analysis observes and dissects the network traffic between an app and servers, helping to understand how an app communicates and what data it transmits. This typically includes examining network protocols, API endpoints, data encryption, authentication mechanisms, and any third-party dependencies.

Man-in-the-Middle

A man-in-the-middle (MITM) attack involves intercepting network traffic between an app and the servers it interacts with. There are various techniques for intercepting traffic, including using local software or installing specialized hardware within the network. In the context of app reverse engineering, local network analyzers are commonly used for this purpose. These analyzers enable logging, viewing, and overriding of requests and responses, which can be helpful for further studying and reverse engineering an app.

Fuzzing

Fuzzing, or fuzz testing, focuses on inputting large volumes of random data, called fuzz, to the system in an attempt to make it crash. This is less applicable to app reverse engineering and mostly targets server apps and APIs. While it can be used to find vulnerabilities, it is primarily a tool used by developers and security researchers to discover and fix issues during development.

Digest:

Investigating network interactions can provide a different perspective for understanding an app's behavior, the data it handles, and the underlying infrastructure.

Network analysis can reveal how an app handles sensitive data and identify loopholes, such as blocking system access to licensing servers and disabling purchase validation.

While network analysis requires a distinct approach, static and dynamic analysis can also revel sensitive networking information used in an app, such as API endpoints, tokens, etc.

Documentation focuses on capturing the insights and understanding gained from the research and analysis of a an app or larger system. It can include descriptions of how components interact, the purpose and functionality of specific code segments, and high-level system architecture. This systematic organization of information facilitates easier navigation of complex systems, which helps to develop hypotheses, identify vulnerabilities, or uncover the logic and flow of the app during reverse engineering.

Documentation can be an essential resource for reverse engineers who work in teams, enabling collaborative efforts and knowledge transfer. This ensures that findings are not lost or forgotten over time, especially for larger and more complex projects.

App patching

App patching, as described in this guide, aims to permanently alter an app's behavior, for example, by disabling license checks or enabling otherwise restricted features. Typically, this is the final step in the hacking process, accomplished after successfully reverse engineering the app and identifying its vulnerabilities. The two commonly used techniques for app patching are binary patching and dynamic loading.

Binary patching

Binary patching is the process of modifying a compiled app's binary to change its behavior. There are different methods of binary patching:

  1. Instruction replacement: Changing specific instructions within the binary to modify code behavior. This can include replacing a conditional jump to bypass a license check, for example.

  2. Section insertion: Adding new sections to a binary can be used to introduce entirely new code or data. This might involve manipulating headers and various structures within the binary to accommodate the new content.

  3. Address redirection: Changing the pointers or references within the code to redirect the execution flow or data access to different locations. This can be used to deceive analysis tools or to reroute execution to malicious code.

  4. NOP insertion: No Operation (NOP), is an instruction that does nothing and moves to the next instruction. By replacing code with NOP instructions, functionality can be effectively “removed” from the binary, such as disabling a specific check or feature.

  5. Function hooking: Altering the entry points to specific functions and redirecting the execution flow to other code either within the binary or in an external library. This allows to insert custom code that gets executed when specific functions are called.

  6. Symbol table manipulation: By altering the symbol table, the references to functions or variables can be changed to redirect calls and accesses to different parts of the code. This is often used for function hooking.

Dynamic loading

Dynamic loading refers to the process of loading external code modules into an app's runtime on demand. In app development this is commonly used as part of dynamic linking, where dynamic libraries help break down the code into separate modules. Unlike static libraries, which are incorporated directly into an app binary during linking and loaded all at once upon app launch, dynamic libraries enhance design flexibility and reduce startup time. By loading only the necessary code at runtime, an app can reduce its initialization footprint and adapt more easily to different conditions or user needs.

In the context of app patching, dynamic loading can be exploited to inject unauthorized code and modify behavior by loading a malicious dynamic library into an app's runtime. Often, a dynamic library is embedded into the app bundle, and the app's binary is patched to include load instructions. However, alternative approaches for dynamic loading and code injection may also be utilized, such as dylib hijacking, substituting system frameworks, or deploying system kernel extensions.

Digest:

Patching an app requires a comprehensive understanding of binary formats, assembly language, and the specific system architecture involved.

Altering an app's binary or any of its contents would invalidate the code signature, making the app invalid without re-signing and triggering system warning.

Modifying a dependent dynamic library could facilitate the dynamic loading of an unauthorized code module, bypassing the need to alter the app binary.

In macOS, security features such as System Integrity Protection (SIP) and Gatekeeper act as safeguards against unauthorized code execution.

Installing pirated apps often requires disabling specific security components, thereby potentially compromising the entire system's security.

Code signing

Code signing is an essential step in app development that helps ensure the app's authenticity and integrity. The most complex parts are often done behind the scenes, without developers needing to worry about the technicalities. However, this is one of the most important steps in protecting an app.

The process of signing a macOS app involves creating a digital signature using a developer's private key, obtained from Apple’s Developer Program. This signature is embedded into the app's binary. When the app is downloaded or executed, the macOS operating system checks the signature, verifying it with the corresponding public key. If the signature is valid, the system confirms that the app has not been altered since signing, ensuring its integrity and authenticity.

Digest:

Code signing establishes trust in the source and integrity of the app, assuring that it hasn't been altered since it was signed.

A stolen private key used for code signing could be potentially used to sign a modified version of the app, making the protection of private keys crucial.

A altered app may be re-signed with another developer’s private key and appear as a legitimate copy, which underscores the importance of verifying the signature itself.

In macOS, notarization adds an extra layer of security by checking for malicious content and authenticating the developer's identity.

During app signing, not only the executable binary gets signed, but also every resource within the app bundle, including embedded libraries and frameworks.

When verifying the signature manually, it's important to validate each critical resource, including embedded dynamic libraries, not just the executable binary.

App protection

Now that we’ve covered the terminology and basics of app security, we can look at how apps can be protected against reverse engineering and hacking: