Skip to main content
Anti-tampering techniques are defensive mechanisms that detect when an application is running in a compromised environment or being analyzed, then take action to prevent further execution or analysis.

Overview

Anti-tampering protections typically fall into three categories:

Environment Detection

Detecting jailbroken devices and modified system components.

Debugger Detection

Identifying when the app is being debugged or instrumented.

Integrity Checks

Verifying that the application code hasn’t been modified.

Jailbreak Detection

Jailbreak detection attempts to identify if the device has been modified to allow root access and unsigned code execution.

Common Detection Methods

Looking for files that only exist on jailbroken devices:
func isJailbroken() -> Bool {
    let jailbreakPaths = [
        "/Applications/Cydia.app",
        "/Applications/blackra1n.app",
        "/Applications/FakeCarrier.app",
        "/Applications/Icy.app",
        "/Applications/IntelliScreen.app",
        "/Applications/MxTube.app",
        "/Applications/RockApp.app",
        "/Applications/SBSettings.app",
        "/Applications/WinterBoard.app",
        "/Library/MobileSubstrate/MobileSubstrate.dylib",
        "/Library/MobileSubstrate/DynamicLibraries/",
        "/usr/sbin/sshd",
        "/usr/bin/sshd",
        "/usr/libexec/sftp-server",
        "/usr/libexec/ssh-keysign",
        "/bin/bash",
        "/bin/sh",
        "/etc/apt",
        "/private/var/lib/apt",
        "/private/var/lib/cydia",
        "/private/var/stash"
    ]
    
    for path in jailbreakPaths {
        if FileManager.default.fileExists(atPath: path) {
            return true
        }
    }
    
    return false
}

Advanced Jailbreak Detection

System Call Inspection
Checking for modified system call tables:
#import <sys/sysctl.h>

BOOL isJailbrokenViaKernelCheck() {
    int mib[4];
    struct kinfo_proc info;
    size_t size = sizeof(info);
    
    info.kp_proc.p_flag = 0;
    mib[0] = CTL_KERN;
    mib[1] = KERN_PROC;
    mib[2] = KERN_PROC_PID;
    mib[3] = getpid();
    
    sysctl(mib, 4, &info, &size, NULL, 0);
    
    // P_TRACED flag indicates debugging/modification
    return (info.kp_proc.p_flag & P_TRACED) != 0;
}
Integrity Verification
Verifying system file checksums:
import CryptoKit

func verifySystemIntegrity() -> Bool {
    let criticalFiles: [String: String] = [
        "/System/Library/CoreServices/SpringBoard.app/SpringBoard":
            "expected_hash_here",
        "/usr/lib/libSystem.B.dylib":
            "expected_hash_here"
    ]
    
    for (path, expectedHash) in criticalFiles {
        guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
            return false
        }
        
        let hash = SHA256.hash(data: data)
        let hashString = hash.compactMap { 
            String(format: "%02x", $0) 
        }.joined()
        
        if hashString != expectedHash {
            return false  // File modified
        }
    }
    
    return true
}

Debugger Detection

Detecting when the application is being debugged allows it to exit or alter behavior.

Detection Techniques

#include <sys/sysctl.h>
#include <unistd.h>

BOOL isBeingDebugged() {
    int mib[4];
    struct kinfo_proc info;
    size_t size = sizeof(info);
    
    info.kp_proc.p_flag = 0;
    mib[0] = CTL_KERN;
    mib[1] = KERN_PROC;
    mib[2] = KERN_PROC_PID;
    mib[3] = getpid();
    
    if (sysctl(mib, 4, &info, &size, NULL, 0) == -1) {
        return NO;
    }
    
    return (info.kp_proc.p_flag & P_TRACED) != 0;
}

Continuous Monitoring

Anti-debugging checks should run continuously, not just at startup, to detect debuggers attached after launch.
class DebugDetector {
    private var timer: Timer?
    
    func startMonitoring() {
        timer = Timer.scheduledTimer(
            withTimeInterval: 1.0,
            repeats: true
        ) { [weak self] _ in
            self?.performChecks()
        }
    }
    
    private func performChecks() {
        if isBeingDebugged() || checkExceptionPorts() {
            handleTamperingDetected()
        }
    }
    
    private func handleTamperingDetected() {
        // Don't obviously crash - that's easy to bypass
        // Instead, corrupt data or alter behavior subtly
        corruptCriticalData()
        
        // Or delay and exit
        DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
            exit(0)
        }
    }
}

Integrity Checks

Verifying that the application binary hasn’t been modified.
#import <Security/Security.h>

BOOL verifyCodeSignature() {
    SecStaticCodeRef staticCode = NULL;
    OSStatus status;
    
    // Get reference to current binary
    status = SecStaticCodeCreateWithPath(
        (__bridge CFURLRef)[NSBundle mainBundle].bundleURL,
        kSecCSDefaultFlags,
        &staticCode
    );
    
    if (status != errSecSuccess) {
        return NO;
    }
    
    // Verify signature
    status = SecStaticCodeCheckValidity(
        staticCode,
        kSecCSDefaultFlags,
        NULL
    );
    
    CFRelease(staticCode);
    return status == errSecSuccess;
}

Example Analysis: NoTampering.ipa

The example file is located at: /home/daytona/workspace/source/ObfuscatedAppExamples/NoTampering.ipa

Extracting and Analyzing

1

Extract the IPA

unzip NoTampering.ipa -d NoTampering
cd NoTampering/Payload/*.app
2

Identify Protection Mechanisms

# Check for anti-debug symbols
nm NoTampering | grep -i "debug\|ptrace\|sysctl"

# Look for jailbreak detection strings
strings NoTampering | grep -i "cydia\|substrate\|jailbreak"

# Examine imports
otool -L NoTampering
3

Static Analysis in Hopper

  • Search for ptrace calls
  • Locate sysctl with P_TRACED checks
  • Find file existence checks for jailbreak paths
  • Identify integrity verification routines
4

Dynamic Analysis Strategy

Plan how to bypass detected protections using Frida or patches.

Bypassing with Frida

// Hook ptrace to prevent PT_DENY_ATTACH
var ptrace_ptr = Module.findExportByName(null, "ptrace");
if (ptrace_ptr) {
    Interceptor.attach(ptrace_ptr, {
        onEnter: function(args) {
            var request = args[0].toInt32();
            if (request === 31) {  // PT_DENY_ATTACH
                console.log("[*] Blocked PT_DENY_ATTACH");
                args[0] = ptr(0);  // Change to invalid request
            }
        }
    });
}

Comprehensive Bypass Script

// all-in-one-bypass.js

setTimeout(function() {
    console.log("[*] Starting anti-tampering bypass...");
    
    // 1. Bypass ptrace
    bypassPtrace();
    
    // 2. Bypass sysctl
    bypassSysctl();
    
    // 3. Bypass file checks
    bypassFileChecks();
    
    // 4. Bypass dyld checks
    bypassDyldChecks();
    
    // 5. Bypass integrity checks
    bypassIntegrityChecks();
    
    console.log("[*] Bypass complete!");
}, 0);

function bypassPtrace() {
    var ptrace_ptr = Module.findExportByName(null, "ptrace");
    if (ptrace_ptr) {
        Interceptor.attach(ptrace_ptr, {
            onEnter: function(args) {
                if (args[0].toInt32() === 31) {
                    args[0] = ptr(0);
                }
            }
        });
        console.log("[+] ptrace bypass installed");
    }
}

function bypassSysctl() {
    var sysctl_ptr = Module.findExportByName(null, "sysctl");
    if (sysctl_ptr) {
        Interceptor.attach(sysctl_ptr, {
            onLeave: function(retval) {
                var info = ptr(this.context.r1);
                var flags = info.add(0x20).readU32();
                if (flags & 0x800) {
                    info.add(0x20).writeU32(flags & ~0x800);
                }
            }
        });
        console.log("[+] sysctl bypass installed");
    }
}

function bypassFileChecks() {
    var fileExists = ObjC.classes.NSFileManager['- fileExistsAtPath:'];
    Interceptor.attach(fileExists.implementation, {
        onEnter: function(args) {
            this.path = ObjC.Object(args[2]).toString();
        },
        onLeave: function(retval) {
            var suspicious = ["Cydia", "substrate", "bash", "ssh", "apt"];
            for (var s of suspicious) {
                if (this.path.includes(s)) {
                    retval.replace(ptr(0));
                    return;
                }
            }
        }
    });
    console.log("[+] File check bypass installed");
}

function bypassDyldChecks() {
    // Override dyld image count to hide injected libraries
    var dyld_image_count = Module.findExportByName(null, "_dyld_image_count");
    if (dyld_image_count) {
        Interceptor.replace(dyld_image_count, new NativeCallback(
            function() { return 10; },  // Return fixed small number
            'uint32', []
        ));
        console.log("[+] dyld bypass installed");
    }
}

function bypassIntegrityChecks() {
    // Hook common crypto functions used for checksums
    var sha256 = Module.findExportByName(null, "CC_SHA256");
    if (sha256) {
        Interceptor.attach(sha256, {
            onLeave: function(retval) {
                // Could replace with known good hash if needed
            }
        });
        console.log("[+] Integrity check bypass installed");
    }
}

Running the Bypass

# Attach to running app
frida -U -f com.example.notampering -l all-in-one-bypass.js

# Or spawn and attach
frida -U -f com.example.notampering -l all-in-one-bypass.js --no-pause

Advanced Countermeasures

Obfuscated Checks

Hide anti-tampering logic within obfuscated code to make it harder to locate and bypass.

Distributed Checks

Spread checks throughout the codebase rather than centralizing them.

Time-Delayed Responses

Don’t crash immediately when tampering is detected - delay or corrupt data subtly.

Server-Side Validation

Verify device integrity on the server side using device attestation.

Best Practices for Analysis

1

Identify All Protection Mechanisms

Catalog every anti-tampering check before attempting to bypass them.
2

Prioritize Bypasses

Start with the most critical protections (like ptrace) that block analysis entirely.
3

Test Incrementally

Bypass one protection at a time and verify the app still functions correctly.
4

Document Patches

Keep detailed notes on what you bypassed and how, for reproducibility.
5

Consider Automation

Create reusable Frida scripts for common protection patterns.
Some apps use multiple layers of protection. Bypassing one check may trigger others. Monitor app behavior carefully after each bypass.

Further Reading

Detection Techniques

Learn to identify anti-tampering mechanisms in binaries.

Control Flow Flattening

Often combined with anti-tampering to hide protection logic.

Build docs developers (and LLMs) love