Skip to main content
String encryption is a fundamental obfuscation technique used to hide sensitive information like API keys, URLs, encryption keys, and debug messages from static analysis.

Why Encrypt Strings?

Hide Sensitive Data

API keys, URLs, and credentials are invisible in disassembled code.

Prevent Patching

Critical strings can’t be easily modified to bypass security checks.

Obscure Logic

Error messages and debug strings that reveal program logic are hidden.

Compliance

Meet security requirements that prohibit hardcoded secrets.

String Obfuscation Techniques

Simple but effective byte-wise XOR with a key:
// Encrypted string (compile-time)
let encrypted: [UInt8] = [0x72, 0x65, 0x6C, 0x6C, 0x6F]
let key: UInt8 = 0x42

// Decrypt at runtime
func decrypt(_ data: [UInt8], key: UInt8) -> String {
    let decrypted = data.map { $0 ^ key }
    return String(bytes: decrypted, encoding: .utf8)!
}

let apiKey = decrypt(encrypted, key: key)
// Result: "Hello"
XOR is reversible: (data ^ key) ^ key = data

Runtime Decryption

Common Decryption Patterns

1

Lazy Decryption

Strings are decrypted only when first accessed:
class StringManager {
    private static var cache: [String: String] = [:]
    
    static func get(_ key: String) -> String {
        if let cached = cache[key] {
            return cached
        }
        
        let encrypted = encryptedStrings[key]!
        let decrypted = decrypt(encrypted)
        cache[key] = decrypted
        
        return decrypted
    }
}
2

Batch Decryption

All strings decrypted at app startup:
class AppDelegate: UIApplicationDelegate {
    func application(_ application: UIApplication,
                    didFinishLaunchingWithOptions...) -> Bool {
        StringManager.decryptAll()
        return true
    }
}
3

On-Demand Decryption

Decrypt, use, and immediately discard:
func makeAPICall() {
    let url = decrypt("api_url")
    // Use url
    // url is deallocated after function returns
}

Anti-Dumping Techniques

Overwrite decrypted strings after use:
func secureDecrypt(_ data: Data) -> String {
    var decrypted = decrypt(data)
    defer {
        // Clear from memory
        decrypted.withUnsafeMutableBytes { ptr in
            memset(ptr.baseAddress, 0, ptr.count)
        }
    }
    return decrypted
}
Store parts of strings in different locations:
let part1 = decrypt(encrypted1)  // "https://"
let part2 = decrypt(encrypted2)  // "api."
let part3 = decrypt(encrypted3)  // "example.com"
let fullURL = part1 + part2 + part3
Generate strings algorithmically:
func getAPIKey() -> String {
    let base = decrypt(baseKey)
    let timestamp = Date().timeIntervalSince1970
    let hash = SHA256.hash(data: "\(base)\(timestamp)".data(using: .utf8)!)
    return hash.compactMap { String(format: "%02x", $0) }.joined()
}

Finding Encrypted Strings

Static Analysis

# Extract all readable strings
strings -a binary_name

# Look for Base64 patterns
strings -a binary_name | grep -E '^[A-Za-z0-9+/]{20,}={0,2}$'

# Find hex-encoded data
strings -a binary_name | grep -E '^[0-9a-fA-F]{16,}$'

Dynamic Analysis with Frida

// Hook common decryption function patterns
var decrypt = Module.findExportByName(null, "_decrypt");
if (decrypt) {
    Interceptor.attach(decrypt, {
        onEnter: function(args) {
            console.log("[decrypt] Input:");
            console.log(hexdump(args[0], { length: 64 }));
        },
        onLeave: function(retval) {
            console.log("[decrypt] Output: " + retval.readUtf8String());
        }
    });
}

// Hook String initializers
var NSString = ObjC.classes.NSString;
Interceptor.attach(NSString['- initWithData:encoding:'].implementation, {
    onEnter: function(args) {
        this.data = args[2];
    },
    onLeave: function(retval) {
        var str = ObjC.Object(retval).toString();
        if (str.length > 10) {
            console.log("[String] " + str);
        }
    }
});

Automated String Extraction

Custom Decryption Script

import frida
import sys

def on_message(message, data):
    if message['type'] == 'send':
        print(f"[*] {message['payload']}")

# Frida script to extract all decrypted strings
script_code = """
var decrypted_strings = new Set();

// Hook all potential decryption functions
var decrypt_funcs = ['_decrypt', '_decryptString', '_decodeString'];

decrypt_funcs.forEach(function(name) {
    var addr = Module.findExportByName(null, name);
    if (addr) {
        Interceptor.attach(addr, {
            onLeave: function(retval) {
                try {
                    var str = retval.readUtf8String();
                    if (str && str.length > 0 && !decrypted_strings.has(str)) {
                        decrypted_strings.add(str);
                        send(str);
                    }
                } catch (e) {}
            }
        });
    }
});
"""

# Attach to app
device = frida.get_usb_device()
pid = device.spawn(["com.example.app"])
session = device.attach(pid)
script = session.create_script(script_code)
script.on('message', on_message)
script.load()
device.resume(pid)

# Keep script running
sys.stdin.read()
Run the app through common workflows to trigger decryption of different strings throughout the application.

Example Workflow

Best Practices

It’s often faster to dump decrypted strings from memory than to reverse the decryption algorithm. Use Frida or LLDB to intercept strings as they’re decrypted.
Check imports for CommonCrypto, OpenSSL, or custom crypto implementations. These are indicators of where decryption happens.
Encrypted data has high entropy (randomness). Use tools like binwalk or custom scripts to identify high-entropy byte sequences.
Some strings are only decrypted in specific scenarios (error conditions, premium features, etc.). Thoroughly exercise the app.
Maintain a database of extracted strings categorized by type (URLs, keys, messages) for reference during analysis.

Defeating Advanced Protection

White-Box Cryptography

When the key is embedded in the algorithm itself, making it inseparable from the decryption logic. Requires advanced mathematical analysis or dynamic extraction.

Code Virtualization

Decryption logic runs in a custom VM. Must analyze the VM instruction set or extract strings dynamically during execution.

Server-Side Decryption

Strings are retrieved from a server at runtime. Monitor network traffic or hook network APIs to capture decrypted values.

JIT Decryption

Strings are generated just-in-time using complex algorithms. Use DBI (Dynamic Binary Instrumentation) to trace execution and capture results.

Further Reading

Anti-Tampering

Learn about runtime protection mechanisms often used alongside string encryption.

Detection Techniques

Master systematic approaches to identifying obfuscation in binaries.

Build docs developers (and LLMs) love