Modern password security and authentication system
Mobile & App Security

Mobile App Security Testing: iOS and Android Penetration ...

Learn comprehensive mobile app security testing methodologies for iOS and Android. Master static analysis, dynamic analysis, and penetration testing techniqu...

mobile security penetration testing ios security android security app security testing mobile penetration testing

Mobile applications process 85% of sensitive user data, yet 76% of mobile apps contain at least one critical security vulnerability. According to the 2024 Mobile Security Report, organizations without comprehensive mobile app security testing experience 3x more data breaches and face average costs of $4.2M per incident. Mobile app security testing combines static analysis, dynamic analysis, and runtime manipulation to identify vulnerabilities before attackers exploit them. This comprehensive guide covers production-ready mobile app security testing methodologies for iOS and Android with complete code examples, automated testing workflows, and real-world attack simulations.

Table of Contents

  1. Understanding Mobile App Security Testing
  2. Setting Up Testing Environment
  3. Static Analysis
  4. Dynamic Analysis
  5. Runtime Manipulation
  6. iOS-Specific Testing
  7. Android-Specific Testing
  8. API Security Testing
  9. Automated Security Testing
  10. Reporting and Remediation
  11. Real-World Case Study
  12. FAQ
  13. Conclusion

Key Takeaways

  • Mobile apps need comprehensive security testing
  • Static analysis finds code-level vulnerabilities
  • Dynamic analysis identifies runtime issues
  • Runtime manipulation tests real-world attacks
  • Automated testing scales security efforts
  • Both iOS and Android require specific approaches

TL;DR

Mobile app security testing identifies vulnerabilities through static analysis, dynamic analysis, and runtime manipulation. This guide provides production-ready methodologies for iOS and Android security testing with complete automation workflows.

Understanding Mobile App Security Testing

What is Mobile App Security Testing?

Core Components:

  • Static Application Security Testing (SAST)
  • Dynamic Application Security Testing (DAST)
  • Interactive Application Security Testing (IAST)
  • Runtime Application Self-Protection (RASP)
  • Penetration testing

Why It Matters:

  • Mobile apps handle sensitive data
  • Multiple attack surfaces (client, server, network)
  • Platform-specific vulnerabilities
  • Compliance requirements
  • User trust and brand reputation

Common Mobile App Vulnerabilities

Top 10 Mobile App Security Risks (OWASP Mobile Top 10):

  1. Improper Platform Usage
  2. Insecure Data Storage
  3. Insecure Communication
  4. Insecure Authentication
  5. Insufficient Cryptography
  6. Insecure Authorization
  7. Client Code Quality
  8. Code Tampering
  9. Reverse Engineering
  10. Extraneous Functionality

Prerequisites

Required Knowledge:

  • Mobile app development (iOS/Android)
  • Security fundamentals
  • Network protocols
  • Cryptography basics
  • Only test apps you own or have authorization

Required Tools:

  • iOS: Xcode, Frida, class-dump, Hopper
  • Android: Android Studio, ADB, APKTool, jadx, Frida
  • Both: Burp Suite, mitmproxy, MobSF
  • Testing devices or emulators
  • Only test applications you own or have explicit written authorization
  • Follow responsible disclosure practices
  • Test in isolated environments (not production)
  • Respect user privacy and data protection laws
  • Never test third-party apps without permission
  • Follow OWASP Mobile Security Testing Guide

Setting Up Testing Environment

Step 1) Set Up iOS Testing Environment

Click to view setup code
#!/bin/bash
set -euo pipefail

# iOS Testing Environment Setup Script
# Comprehensive error handling and validation

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_FILE="${SCRIPT_DIR}/setup_ios_testing.log"

log() {
    echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}

error_exit() {
    log "ERROR: $*"
    exit 1
}

# Check prerequisites
check_requirements() {
    log "Checking prerequisites..."
    
    if ! command -v xcodebuild &> /dev/null; then
        error_exit "Xcode not found. Install Xcode from App Store."
    fi
    
    if ! command -v brew &> /dev/null; then
        error_exit "Homebrew not found. Install from https://brew.sh"
    fi
    
    log "Prerequisites check passed"
}

# Install iOS testing tools
install_ios_tools() {
    log "Installing iOS testing tools..."
    
    # Install Frida
    if ! brew list frida &> /dev/null; then
        log "Installing Frida..."
        brew install frida || error_exit "Failed to install Frida"
    else
        log "Frida already installed"
    fi
    
    # Install class-dump
    if ! command -v class-dump &> /dev/null; then
        log "Installing class-dump..."
        brew install class-dump || error_exit "Failed to install class-dump"
    else
        log "class-dump already installed"
    fi
    
    # Install Hopper Disassembler (if available)
    log "Hopper Disassembler: Download from https://www.hopperapp.com"
    
    log "iOS tools installation complete"
}

# Configure iOS device for testing
configure_device() {
    log "Configuring iOS device..."
    log "1. Connect iOS device via USB"
    log "2. Enable Developer Mode in Settings"
    log "3. Trust computer on device"
    log "4. Install Frida server on device:"
    log "   frida-ps -U"
    
    if frida-ps -U &> /dev/null; then
        log "Device connection verified"
    else
        log "WARNING: Could not verify device connection"
    fi
}

# Main setup
main() {
    log "Starting iOS testing environment setup..."
    check_requirements
    install_ios_tools
    configure_device
    log "iOS testing environment setup complete"
}

main "$@"

Step 2) Set Up Android Testing Environment

Click to view setup code
#!/bin/bash
set -euo pipefail

# Android Testing Environment Setup Script
# Comprehensive error handling and validation

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_FILE="${SCRIPT_DIR}/setup_android_testing.log"

log() {
    echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}

error_exit() {
    log "ERROR: $*"
    exit 1
}

# Check prerequisites
check_requirements() {
    log "Checking prerequisites..."
    
    if ! command -v java &> /dev/null; then
        error_exit "Java not found. Install JDK 11 or higher."
    fi
    
    if [ -z "${ANDROID_HOME:-}" ]; then
        error_exit "ANDROID_HOME not set. Install Android SDK."
    fi
    
    log "Prerequisites check passed"
}

# Install Android testing tools
install_android_tools() {
    log "Installing Android testing tools..."
    
    # Install APKTool
    if ! command -v apktool &> /dev/null; then
        log "Installing APKTool..."
        wget https://raw.githubusercontent.com/iBotPeaches/Apktool/master/scripts/linux/apktool -O /usr/local/bin/apktool || error_exit "Failed to download APKTool"
        chmod +x /usr/local/bin/apktool
    else
        log "APKTool already installed"
    fi
    
    # Install jadx
    if ! command -v jadx &> /dev/null; then
        log "Installing jadx..."
        wget https://github.com/skylot/jadx/releases/latest/download/jadx-1.4.7.zip -O /tmp/jadx.zip || error_exit "Failed to download jadx"
        unzip -q /tmp/jadx.zip -d /opt/
        ln -sf /opt/jadx/bin/jadx /usr/local/bin/jadx || true
        ln -sf /opt/jadx/bin/jadx-gui /usr/local/bin/jadx-gui || true
    else
        log "jadx already installed"
    fi
    
    # Install Frida
    if ! command -v frida &> /dev/null; then
        log "Installing Frida..."
        pip3 install frida-tools || error_exit "Failed to install Frida"
    else
        log "Frida already installed"
    fi
    
    log "Android tools installation complete"
}

# Configure Android device/emulator
configure_device() {
    log "Configuring Android device/emulator..."
    
    # Check ADB connection
    if ! adb devices | grep -q "device$"; then
        log "WARNING: No Android device connected"
        log "Connect device via USB and enable USB debugging"
    else
        log "Device connection verified"
        
        # Install Frida server
        log "Installing Frida server on device..."
        DEVICE_ARCH=$(adb shell getprop ro.product.cpu.abi)
        FRIDA_VERSION=$(frida --version)
        
        wget "https://github.com/frida/frida/releases/download/${FRIDA_VERSION}/frida-server-${FRIDA_VERSION}-android-${DEVICE_ARCH}.xz" -O /tmp/frida-server.xz
        xz -d /tmp/frida-server.xz
        adb push /tmp/frida-server /data/local/tmp/frida-server
        adb shell "chmod 755 /data/local/tmp/frida-server"
        adb shell "/data/local/tmp/frida-server &"
        
        log "Frida server installed and started"
    fi
}

# Main setup
main() {
    log "Starting Android testing environment setup..."
    check_requirements
    install_android_tools
    configure_device
    log "Android testing environment setup complete"
}

main "$@"

Static Analysis

Step 3) iOS Static Analysis

Click to view code
#!/usr/bin/env python3
"""
iOS Static Analysis Tool
Comprehensive static analysis for iOS applications with error handling and reporting.
"""

import os
import subprocess
import json
import re
from typing import List, Dict, Optional, Tuple
from dataclasses import dataclass, asdict
from enum import Enum
from pathlib import Path
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class Severity(Enum):
    """Vulnerability severity levels."""
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    CRITICAL = "critical"

@dataclass
class SecurityFinding:
    """Represents a security finding from static analysis."""
    file_path: str
    line_number: int
    vulnerability_type: str
    severity: Severity
    description: str
    recommendation: str
    code_snippet: Optional[str] = None
    
    def to_dict(self) -> Dict:
        """Convert to dictionary for JSON serialization."""
        return {
            **asdict(self),
            'severity': self.severity.value
        }

class iOSStaticAnalyzer:
    """Comprehensive iOS static analysis tool."""
    
    # Security patterns to detect
    SECURITY_PATTERNS = {
        'hardcoded_secrets': [
            (r'password\s*=\s*["\']([^"\']+)["\']', Severity.CRITICAL),
            (r'api[_-]?key\s*=\s*["\']([^"\']+)["\']', Severity.CRITICAL),
            (r'secret\s*=\s*["\']([^"\']+)["\']', Severity.CRITICAL),
            (r'private[_-]?key\s*=\s*["\']([^"\']+)["\']', Severity.CRITICAL),
        ],
        'insecure_storage': [
            (r'NSUserDefaults\s*\.\s*standardUserDefaults\s*\.\s*set', Severity.HIGH),
            (r'UserDefaults\s*\.\s*standard\s*\.\s*set', Severity.HIGH),
            (r'NSKeyedArchiver\s*\.\s*archiveRootObject', Severity.MEDIUM),
        ],
        'weak_crypto': [
            (r'MD5|md5', Severity.HIGH),
            (r'SHA1|sha1', Severity.HIGH),
            (r'DES|des', Severity.HIGH),
            (r'RC4|rc4', Severity.CRITICAL),
        ],
        'insecure_network': [
            (r'http://', Severity.MEDIUM),
            (r'NSURLSessionConfiguration\s*\.\s*default', Severity.LOW),
            (r'allowsArbitraryLoads\s*=\s*true', Severity.HIGH),
        ],
        'sql_injection': [
            (r'executeQuery.*\+.*NSString', Severity.HIGH),
            (r'stringByAppendingString.*SELECT', Severity.HIGH),
        ],
    }
    
    def __init__(self, app_path: str, output_dir: str = "./analysis_results"):
        """Initialize analyzer.
        
        Args:
            app_path: Path to iOS app (.app or .ipa file)
            output_dir: Directory for analysis results
        """
        self.app_path = Path(app_path)
        if not self.app_path.exists():
            raise FileNotFoundError(f"App path not found: {app_path}")
        
        self.output_dir = Path(output_dir)
        self.output_dir.mkdir(parents=True, exist_ok=True)
        
        self.findings: List[SecurityFinding] = []
        
    def extract_app_binary(self) -> Optional[Path]:
        """Extract and return path to app binary.
        
        Returns:
            Path to extracted binary, or None if extraction failed
        """
        try:
            if self.app_path.suffix == '.ipa':
                # Extract IPA
                logger.info("Extracting IPA file...")
                extract_dir = self.output_dir / "extracted_ipa"
                extract_dir.mkdir(exist_ok=True)
                
                subprocess.run(
                    ['unzip', '-q', str(self.app_path), '-d', str(extract_dir)],
                    check=True,
                    capture_output=True
                )
                
                # Find .app bundle
                app_bundle = next(extract_dir.rglob("*.app"), None)
                if not app_bundle:
                    logger.error("Could not find .app bundle in IPA")
                    return None
                
                self.app_path = app_bundle
            
            # Find binary in .app bundle
            binary_path = next(self.app_path.rglob("*"), None)
            if binary_path and binary_path.is_file() and os.access(binary_path, os.X_OK):
                logger.info(f"Found binary: {binary_path}")
                return binary_path
            
            logger.error("Could not find executable binary")
            return None
            
        except subprocess.CalledProcessError as e:
            logger.error(f"Failed to extract app: {e}")
            return None
        except Exception as e:
            logger.error(f"Unexpected error during extraction: {e}")
            return None
    
    def class_dump_binary(self, binary_path: Path) -> Optional[Path]:
        """Extract class information from binary using class-dump.
        
        Args:
            binary_path: Path to binary file
            
        Returns:
            Path to class dump output, or None if failed
        """
        try:
            output_file = self.output_dir / "class_dump.h"
            logger.info("Running class-dump...")
            
            result = subprocess.run(
                ['class-dump', str(binary_path)],
                capture_output=True,
                text=True,
                check=True
            )
            
            output_file.write_text(result.stdout)
            logger.info(f"Class dump saved to {output_file}")
            return output_file
            
        except subprocess.CalledProcessError as e:
            logger.warning(f"class-dump failed (may not be available): {e}")
            return None
        except FileNotFoundError:
            logger.warning("class-dump not found. Install with: brew install class-dump")
            return None
        except Exception as e:
            logger.error(f"Unexpected error during class dump: {e}")
            return None
    
    def analyze_source_files(self, source_dir: Path) -> List[SecurityFinding]:
        """Analyze source code files for security issues.
        
        Args:
            source_dir: Directory containing source files
            
        Returns:
            List of security findings
        """
        findings = []
        
        # Find all Swift and Objective-C files
        source_files = list(source_dir.rglob("*.swift")) + list(source_dir.rglob("*.m")) + list(source_dir.rglob("*.mm"))
        
        for file_path in source_files:
            try:
                content = file_path.read_text(encoding='utf-8', errors='ignore')
                lines = content.split('\n')
                
                for vuln_type, patterns in self.SECURITY_PATTERNS.items():
                    for pattern, severity in patterns:
                        for line_num, line in enumerate(lines, 1):
                            matches = re.finditer(pattern, line, re.IGNORECASE)
                            for match in matches:
                                finding = SecurityFinding(
                                    file_path=str(file_path.relative_to(source_dir)),
                                    line_number=line_num,
                                    vulnerability_type=vuln_type,
                                    severity=severity,
                                    description=f"Found potential {vuln_type} in {file_path.name}",
                                    recommendation=self._get_recommendation(vuln_type),
                                    code_snippet=line.strip()
                                )
                                findings.append(finding)
                                logger.debug(f"Found issue: {vuln_type} at {file_path}:{line_num}")
                                
            except Exception as e:
                logger.warning(f"Error analyzing {file_path}: {e}")
                continue
        
        return findings
    
    def _get_recommendation(self, vuln_type: str) -> str:
        """Get remediation recommendation for vulnerability type.
        
        Args:
            vuln_type: Type of vulnerability
            
        Returns:
            Recommendation text
        """
        recommendations = {
            'hardcoded_secrets': "Move secrets to Keychain or secure configuration. Never hardcode credentials.",
            'insecure_storage': "Use Keychain Services for sensitive data. Avoid NSUserDefaults for secrets.",
            'weak_crypto': "Use strong cryptographic algorithms (AES-256, SHA-256+). Avoid MD5, SHA1, DES, RC4.",
            'insecure_network': "Use HTTPS only. Implement certificate pinning. Avoid arbitrary loads.",
            'sql_injection': "Use parameterized queries. Validate and sanitize all input.",
        }
        return recommendations.get(vuln_type, "Review and fix the security issue.")
    
    def analyze_plist_files(self, app_path: Path) -> List[SecurityFinding]:
        """Analyze Info.plist and other plist files for misconfigurations.
        
        Args:
            app_path: Path to app bundle
            
        Returns:
            List of security findings
        """
        findings = []
        
        plist_files = list(app_path.rglob("*.plist"))
        
        for plist_file in plist_files:
            try:
                result = subprocess.run(
                    ['plutil', '-convert', 'json', '-o', '-', str(plist_file)],
                    capture_output=True,
                    text=True,
                    check=True
                )
                
                plist_data = json.loads(result.stdout)
                
                # Check for insecure configurations
                if 'NSAppTransportSecurity' in plist_data:
                    ats = plist_data['NSAppTransportSecurity']
                    if ats.get('NSAllowsArbitraryLoads') == True:
                        finding = SecurityFinding(
                            file_path=str(plist_file.relative_to(app_path)),
                            line_number=0,
                            vulnerability_type='insecure_transport',
                            severity=Severity.HIGH,
                            description="App allows arbitrary HTTP loads (bypasses ATS)",
                            recommendation="Disable NSAllowsArbitraryLoads. Use HTTPS for all connections."
                        )
                        findings.append(finding)
                
                # Check for weak keychain access groups
                if 'keychain-access-groups' in plist_data:
                    groups = plist_data['keychain-access-groups']
                    if not groups or len(groups) == 0:
                        finding = SecurityFinding(
                            file_path=str(plist_file.relative_to(app_path)),
                            line_number=0,
                            vulnerability_type='keychain_misconfiguration',
                            severity=Severity.MEDIUM,
                            description="Keychain access groups not properly configured",
                            recommendation="Configure proper keychain access groups for secure data sharing."
                        )
                        findings.append(finding)
                        
            except subprocess.CalledProcessError:
                logger.warning(f"Could not parse {plist_file} as JSON")
                continue
            except json.JSONDecodeError:
                logger.warning(f"Invalid JSON from plutil for {plist_file}")
                continue
            except Exception as e:
                logger.warning(f"Error analyzing {plist_file}: {e}")
                continue
        
        return findings
    
    def run_analysis(self) -> List[SecurityFinding]:
        """Run complete static analysis.
        
        Returns:
            List of all security findings
        """
        logger.info("Starting iOS static analysis...")
        
        # Extract binary
        binary_path = self.extract_app_binary()
        if binary_path:
            # Class dump
            self.class_dump_binary(binary_path)
        
        # Analyze source files if available
        if self.app_path.is_dir():
            source_findings = self.analyze_source_files(self.app_path)
            self.findings.extend(source_findings)
            
            # Analyze plist files
            plist_findings = self.analyze_plist_files(self.app_path)
            self.findings.extend(plist_findings)
        
        logger.info(f"Analysis complete. Found {len(self.findings)} issues.")
        return self.findings
    
    def generate_report(self) -> Path:
        """Generate comprehensive security report.
        
        Returns:
            Path to generated report file
        """
        report_path = self.output_dir / "security_report.json"
        
        report = {
            'app_path': str(self.app_path),
            'total_findings': len(self.findings),
            'findings_by_severity': {
                severity.value: len([f for f in self.findings if f.severity == severity])
                for severity in Severity
            },
            'findings': [f.to_dict() for f in self.findings]
        }
        
        report_path.write_text(json.dumps(report, indent=2))
        logger.info(f"Report generated: {report_path}")
        
        return report_path


# Main execution
if __name__ == "__main__":
    import sys
    
    if len(sys.argv) < 2:
        print("Usage: python ios_static_analyzer.py <app_path> [output_dir]")
        sys.exit(1)
    
    app_path = sys.argv[1]
    output_dir = sys.argv[2] if len(sys.argv) > 2 else "./analysis_results"
    
    try:
        analyzer = iOSStaticAnalyzer(app_path, output_dir)
        findings = analyzer.run_analysis()
        report_path = analyzer.generate_report()
        
        print(f"\nAnalysis complete!")
        print(f"Total findings: {len(findings)}")
        print(f"Report: {report_path}")
        
    except Exception as e:
        logger.error(f"Analysis failed: {e}")
        sys.exit(1)

Validation:

# Test the analyzer
python3 ios_static_analyzer.py /path/to/app.ipa ./test_results

# Verify findings
cat test_results/security_report.json | jq '.total_findings'

Common Errors:

  • class-dump not found: Install with brew install class-dump
  • Permission denied: Ensure app binary is executable
  • Invalid IPA: Verify IPA file is not corrupted

Step 4) Unit Tests for Static Analyzer

Click to view test code
#!/usr/bin/env python3
"""
Unit tests for iOS Static Analyzer
Comprehensive test coverage with pytest.
"""

import pytest
import tempfile
import json
from pathlib import Path
from unittest.mock import Mock, patch, MagicMock
from ios_static_analyzer import iOSStaticAnalyzer, SecurityFinding, Severity

class TestiOSStaticAnalyzer:
    """Unit tests for iOSStaticAnalyzer."""
    
    @pytest.fixture
    def temp_dir(self):
        """Create temporary directory for tests."""
        with tempfile.TemporaryDirectory() as tmpdir:
            yield Path(tmpdir)
    
    @pytest.fixture
    def sample_app(self, temp_dir):
        """Create sample app structure for testing."""
        app_dir = temp_dir / "TestApp.app"
        app_dir.mkdir()
        
        # Create Info.plist
        info_plist = app_dir / "Info.plist"
        info_plist.write_text("""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleExecutable</key>
    <string>TestApp</string>
</dict>
</plist>""")
        
        # Create binary
        binary = app_dir / "TestApp"
        binary.write_bytes(b'\x00' * 100)
        binary.chmod(0o755)
        
        return app_dir
    
    @pytest.fixture
    def sample_source(self, temp_dir):
        """Create sample source code with vulnerabilities."""
        source_dir = temp_dir / "sources"
        source_dir.mkdir()
        
        # Create vulnerable Swift file
        vulnerable_file = source_dir / "VulnerableCode.swift"
        vulnerable_file.write_text("""
        let password = "hardcoded123"
        let apiKey = "sk_live_1234567890"
        UserDefaults.standard.set("secret", forKey: "token")
        """)
        
        return source_dir
    
    def test_analyzer_initialization(self, temp_dir, sample_app):
        """Test analyzer initialization."""
        analyzer = iOSStaticAnalyzer(str(sample_app), str(temp_dir / "output"))
        assert analyzer.app_path.exists()
        assert analyzer.output_dir.exists()
        assert len(analyzer.findings) == 0
    
    def test_analyzer_raises_on_invalid_path(self, temp_dir):
        """Test analyzer raises error for invalid path."""
        with pytest.raises(FileNotFoundError):
            iOSStaticAnalyzer("/nonexistent/path", str(temp_dir / "output"))
    
    def test_find_binary_success(self, temp_dir, sample_app):
        """Test finding binary in app bundle."""
        analyzer = iOSStaticAnalyzer(str(sample_app), str(temp_dir / "output"))
        binary = analyzer.find_binary(Path(sample_app))
        assert binary is not None
        assert binary.exists()
    
    def test_analyze_source_files_finds_hardcoded_secrets(self, temp_dir, sample_source):
        """Test source analysis finds hardcoded secrets."""
        analyzer = iOSStaticAnalyzer(str(sample_source), str(temp_dir / "output"))
        findings = analyzer.analyze_source_files(sample_source)
        
        # Should find hardcoded password and API key
        secret_findings = [f for f in findings if f.vulnerability_type == 'hardcoded_secrets']
        assert len(secret_findings) >= 2
        
        # Check severity
        critical_findings = [f for f in secret_findings if f.severity == Severity.CRITICAL]
        assert len(critical_findings) >= 2
    
    def test_analyze_source_files_finds_insecure_storage(self, temp_dir, sample_source):
        """Test source analysis finds insecure storage."""
        analyzer = iOSStaticAnalyzer(str(sample_source), str(temp_dir / "output"))
        findings = analyzer.analyze_source_files(sample_source)
        
        insecure_storage = [f for f in findings if f.vulnerability_type == 'insecure_storage']
        assert len(insecure_storage) >= 1
        assert insecure_storage[0].severity == Severity.HIGH
    
    @patch('subprocess.run')
    def test_class_dump_success(self, mock_subprocess, temp_dir, sample_app):
        """Test successful class dump."""
        mock_subprocess.return_value = MagicMock(
            stdout="// Class dump output",
            returncode=0
        )
        
        analyzer = iOSStaticAnalyzer(str(sample_app), str(temp_dir / "output"))
        binary = analyzer.find_binary(Path(sample_app))
        
        result = analyzer.class_dump_binary(binary)
        assert result is not None
        assert result.exists()
        mock_subprocess.assert_called_once()
    
    @patch('subprocess.run')
    def test_class_dump_not_found(self, mock_subprocess, temp_dir, sample_app):
        """Test class dump handles missing tool gracefully."""
        mock_subprocess.side_effect = FileNotFoundError()
        
        analyzer = iOSStaticAnalyzer(str(sample_app), str(temp_dir / "output"))
        binary = analyzer.find_binary(Path(sample_app))
        
        result = analyzer.class_dump_binary(binary)
        assert result is None
    
    def test_generate_report(self, temp_dir, sample_source):
        """Test report generation."""
        analyzer = iOSStaticAnalyzer(str(sample_source), str(temp_dir / "output"))
        findings = analyzer.analyze_source_files(sample_source)
        analyzer.findings = findings
        
        report_path = analyzer.generate_report()
        assert report_path.exists()
        
        # Verify report content
        report_data = json.loads(report_path.read_text())
        assert 'total_findings' in report_data
        assert 'findings_by_severity' in report_data
        assert 'findings' in report_data
        assert report_data['total_findings'] == len(findings)
    
    def test_severity_enum(self):
        """Test severity enum values."""
        assert Severity.LOW.value == "low"
        assert Severity.MEDIUM.value == "medium"
        assert Severity.HIGH.value == "high"
        assert Severity.CRITICAL.value == "critical"
    
    def test_security_finding_to_dict(self):
        """Test SecurityFinding serialization."""
        finding = SecurityFinding(
            file_path="test.swift",
            line_number=10,
            vulnerability_type="hardcoded_secrets",
            severity=Severity.CRITICAL,
            description="Test finding",
            recommendation="Fix it"
        )
        
        finding_dict = finding.to_dict()
        assert finding_dict['severity'] == "critical"
        assert finding_dict['file_path'] == "test.swift"
        assert finding_dict['line_number'] == 10


# Integration tests
class TestiOSStaticAnalyzerIntegration:
    """Integration tests for complete analysis workflow."""
    
    @pytest.fixture
    def temp_dir(self):
        """Create temporary directory for tests."""
        with tempfile.TemporaryDirectory() as tmpdir:
            yield Path(tmpdir)
    
    def test_complete_analysis_workflow(self, temp_dir):
        """Test complete analysis workflow end-to-end."""
        # Create sample app structure
        app_dir = temp_dir / "TestApp.app"
        app_dir.mkdir()
        
        source_dir = app_dir / "Sources"
        source_dir.mkdir()
        
        # Create vulnerable source file
        vulnerable_file = source_dir / "App.swift"
        vulnerable_file.write_text('let apiKey = "sk_live_12345"')
        
        # Run analysis
        analyzer = iOSStaticAnalyzer(str(app_dir), str(temp_dir / "output"))
        findings = analyzer.run_analysis()
        
        # Verify findings
        assert len(findings) > 0
        assert any(f.vulnerability_type == 'hardcoded_secrets' for f in findings)
        
        # Generate report
        report_path = analyzer.generate_report()
        assert report_path.exists()


if __name__ == "__main__":
    pytest.main([__file__, "-v"])

Validation:

# Install pytest if not already installed
pip install pytest pytest-cov

# Run tests
pytest test_ios_static_analyzer.py -v

# Run with coverage
pytest test_ios_static_analyzer.py --cov=ios_static_analyzer --cov-report=html

Advanced Scenarios

Scenario 1: Basic Static Analysis

Objective: Perform basic static analysis on a simple iOS app.

Steps:

  1. Extract IPA file
  2. Run basic string extraction
  3. Analyze Info.plist
  4. Generate basic report

Expected Results:

  • Basic vulnerability findings
  • Simple report with low-level issues
  • Quick analysis (5-10 minutes)

Scenario 2: Intermediate Dynamic Analysis

Objective: Perform dynamic analysis with runtime manipulation.

Steps:

  1. Set up Frida hooks
  2. Intercept network calls
  3. Monitor file system access
  4. Analyze runtime behavior
  5. Generate comprehensive report

Expected Results:

  • Runtime vulnerability detection
  • Network traffic analysis
  • Behavioral findings
  • Medium complexity (30-60 minutes)

Scenario 3: Advanced Penetration Testing

Objective: Complete penetration test with all techniques.

Steps:

  1. Static analysis (SAST)
  2. Dynamic analysis (DAST)
  3. Runtime manipulation (Frida)
  4. API security testing
  5. Certificate pinning bypass
  6. Root/jailbreak detection bypass
  7. Comprehensive reporting

Expected Results:

  • Complete vulnerability assessment
  • All attack vectors tested
  • Production-ready security report
  • High complexity (2-4 hours)

Theory and “Why” Mobile App Security Testing Works

Why Static Analysis is Effective

Code-Level Visibility:

  • Static analysis examines source code without execution
  • Identifies vulnerabilities before deployment
  • Catches issues that dynamic analysis might miss
  • Provides comprehensive code coverage

Pattern Recognition:

  • Security patterns are identifiable in code
  • Hardcoded secrets follow predictable patterns
  • Insecure APIs have recognizable signatures
  • Automated tools excel at pattern matching

Early Detection:

  • Finds vulnerabilities in development phase
  • Reduces remediation costs (10x cheaper than post-deployment)
  • Prevents security debt accumulation
  • Enables shift-left security practices

Why Dynamic Analysis is Essential

Runtime Behavior:

  • Apps behave differently at runtime than in source
  • Runtime manipulation reveals actual vulnerabilities
  • Dynamic analysis catches logic flaws
  • Observes real-world attack scenarios

Network Security:

  • Tests actual network communications
  • Verifies certificate pinning implementation
  • Detects insecure data transmission
  • Validates API security controls

Real-World Testing:

  • Simulates actual attack conditions
  • Tests against real infrastructure
  • Validates defense mechanisms
  • Provides realistic risk assessment

Why Both SAST and DAST are Needed

Complementary Coverage:

  • SAST finds code-level issues
  • DAST finds runtime issues
  • Together provide comprehensive coverage
  • Neither alone is sufficient

Different Vulnerability Types:

  • SAST: Hardcoded secrets, weak crypto, SQL injection
  • DAST: Runtime manipulation, network issues, API flaws
  • Combined: Complete security picture

Comprehensive Troubleshooting

Issue: Static Analyzer Fails to Extract Binary

Symptoms:

  • Error: “Could not find executable binary”
  • Analysis returns no findings
  • Binary path is None

Diagnosis:

# Check IPA structure
unzip -l app.ipa | grep -E "\.app|Info.plist"

# Verify app bundle
ls -la /path/to/app.app/

# Check binary permissions
ls -la /path/to/app.app/AppName

Solutions:

  1. Verify IPA structure: Ensure IPA contains .app bundle
  2. Check Info.plist: Verify CFBundleExecutable is set
  3. Fix permissions: chmod +x /path/to/binary
  4. Manual extraction: Extract IPA manually and point to .app bundle

Prevention:

  • Validate IPA before analysis
  • Check app bundle structure
  • Verify binary exists and is executable

Issue: class-dump Fails or Not Found

Symptoms:

  • Error: “class-dump not found”
  • Warning: “class-dump failed”
  • No class dump output generated

Diagnosis:

# Check if class-dump is installed
which class-dump

# Test class-dump
class-dump --version

# Check Homebrew
brew list class-dump

Solutions:

  1. Install class-dump: brew install class-dump
  2. Verify installation: class-dump --version
  3. Alternative tools: Use Hopper, IDA Pro, or Ghidra
  4. Skip class-dump: Analysis can continue without it

Prevention:

  • Include class-dump in setup script
  • Verify tool availability before analysis
  • Provide fallback analysis methods

Issue: Analysis Takes Too Long

Symptoms:

  • Analysis runs for hours
  • High CPU/memory usage
  • Timeout errors

Diagnosis:

# Check file sizes
du -sh /path/to/app.app/

# Count source files
find /path/to/app.app/ -name "*.swift" | wc -l

# Monitor resource usage
top -p $(pgrep -f ios_static_analyzer)

Solutions:

  1. Optimize patterns: Reduce regex complexity
  2. Parallel processing: Process files in parallel
  3. Incremental analysis: Analyze only changed files
  4. Resource limits: Set memory/CPU limits
  5. Filter files: Skip non-source files

Prevention:

  • Set reasonable timeouts
  • Monitor resource usage
  • Optimize analysis patterns
  • Use incremental analysis

Issue: False Positives in Findings

Symptoms:

  • Many findings that aren’t vulnerabilities
  • Legitimate code flagged as insecure
  • High false positive rate

Diagnosis:

# Review findings
cat security_report.json | jq '.findings[] | select(.severity == "critical")'

# Check code context
grep -B 5 -A 5 "flagged_pattern" source_file.swift

Solutions:

  1. Tune patterns: Refine regex patterns
  2. Add context: Check surrounding code
  3. Whitelist patterns: Exclude known false positives
  4. Manual review: Review critical findings manually
  5. Machine learning: Use ML to reduce false positives

Prevention:

  • Test patterns on known good code
  • Continuously refine patterns
  • Use multiple detection methods
  • Combine static and dynamic analysis

Comparison: Mobile App Security Testing Tools

ToolTypeiOS SupportAndroid SupportCostStrengthsLimitations
MobSFSAST/DASTFreeComprehensive, automatedResource intensive
FridaDynamicFreePowerful runtime manipulationRequires device/emulator
Burp SuiteDASTPaidIndustry standard, comprehensiveExpensive, complex
APKToolStaticFreeAPK analysis, decompilationAndroid only
class-dumpStaticFreeObjective-C class extractioniOS only, limited
HopperStaticPaidAdvanced disassemblyExpensive, learning curve
Custom ScriptsBothFreeFull control, customizableRequires development

Why Custom Solutions Win:

  • Full control: Customize to specific needs
  • Cost-effective: No licensing fees
  • Integration: Easy CI/CD integration
  • Flexibility: Adapt to new threats
  • Learning: Deep understanding of tools

Architecture: Mobile App Security Testing Workflow

┌─────────────────────────────────────────────────────────────┐
│                    Mobile App Security Testing              │
└─────────────────────────────────────────────────────────────┘

        ┌─────────────────────┼─────────────────────┐
        │                     │                     │
        ▼                     ▼                     ▼
┌───────────────┐    ┌───────────────┐    ┌───────────────┐
│ Static Analysis│    │Dynamic Analysis│    │Runtime Analysis│
│   (SAST)       │    │   (DAST)       │    │   (Frida)      │
└───────────────┘    └───────────────┘    └───────────────┘
        │                     │                     │
        └─────────────────────┼─────────────────────┘


                    ┌─────────────────┐
                    │  Vulnerability  │
                    │   Aggregation   │
                    └─────────────────┘


                    ┌─────────────────┐
                    │  Risk Scoring   │
                    │  & Prioritization│
                    └─────────────────┘


                    ┌─────────────────┐
                    │ Security Report │
                    │  & Remediation  │
                    └─────────────────┘

Workflow Steps:

  1. Static Analysis: Analyze source code and binaries
  2. Dynamic Analysis: Test running application
  3. Runtime Analysis: Manipulate app at runtime
  4. Aggregation: Combine findings from all methods
  5. Scoring: Prioritize vulnerabilities by risk
  6. Reporting: Generate comprehensive security report
  7. Remediation: Fix vulnerabilities and retest

Limitations and Trade-offs

Static Analysis Limitations

Coverage:

  • Cannot detect runtime-only vulnerabilities
  • May miss obfuscated code
  • Limited by code availability
  • Cannot test network interactions
  • May produce false positives

Accuracy:

  • Pattern matching may flag false positives
  • Context analysis is limited
  • Cannot understand business logic
  • May miss complex vulnerabilities

Trade-offs:

  • Speed vs. Depth: Faster analysis = less thorough
  • False Positives vs. Coverage: More patterns = more false positives
  • Automation vs. Manual: Automated = faster but less nuanced

Dynamic Analysis Limitations

Environment:

  • Requires running application
  • Needs device/emulator setup
  • May miss code paths not executed
  • Limited by test coverage

Evasion:

  • Apps may detect analysis tools
  • Anti-debugging techniques may block analysis
  • Certificate pinning may prevent interception
  • Obfuscation may hide behavior

Trade-offs:

  • Coverage vs. Time: More tests = longer analysis
  • Automation vs. Manual: Automated = faster but less thorough
  • Safety vs. Depth: Safer tests = less realistic

Best Practices for Balanced Testing

Recommendation:

  • Use static analysis for code-level issues
  • Use dynamic analysis for runtime issues
  • Combine both for comprehensive coverage
  • Manual testing for complex scenarios
  • Continuous testing in CI/CD pipeline

Code Review Practices

Security Code Review Checklist

Authentication & Authorization:

  • No hardcoded credentials
  • Proper session management
  • Multi-factor authentication where needed
  • Role-based access control implemented

Data Protection:

  • Sensitive data encrypted at rest
  • Secure storage (Keychain/Keystore)
  • No sensitive data in logs
  • Proper data deletion

Network Security:

  • HTTPS for all connections
  • Certificate pinning implemented
  • No insecure protocols
  • Proper error handling

Input Validation:

  • All inputs validated
  • SQL injection prevention
  • XSS prevention
  • Path traversal prevention

Common Vulnerability Patterns to Avoid

Hardcoded Secrets:

// ❌ BAD
let apiKey = "sk_live_1234567890"

// ✅ GOOD
let apiKey = KeychainHelper.getAPIKey()

Insecure Storage:

// ❌ BAD
UserDefaults.standard.set(password, forKey: "password")

// ✅ GOOD
KeychainHelper.store(password, forKey: "password")

Weak Cryptography:

// ❌ BAD
let hash = MD5.hash(data)

// ✅ GOOD
let hash = SHA256.hash(data)

Performance Considerations

Analysis Performance

Optimization Strategies:

  • Parallel Processing: Analyze multiple files simultaneously
  • Incremental Analysis: Only analyze changed files
  • Caching: Cache analysis results for unchanged files
  • Pattern Optimization: Use efficient regex patterns
  • Resource Limits: Set memory/CPU limits

Performance Metrics:

  • Analysis Time: Target < 5 minutes for typical app
  • Memory Usage: Keep under 2GB for analysis
  • CPU Usage: Utilize multi-core processing
  • I/O Operations: Minimize file system access

Runtime Analysis Performance

Frida Performance:

  • Hook Overhead: Minimal (< 1ms per hook)
  • Memory Usage: ~50MB for Frida server
  • Network Impact: Negligible for monitoring
  • Battery Impact: Minimal for short sessions

Optimization:

  • Selective Hooking: Only hook relevant functions
  • Batch Operations: Group related hooks
  • Conditional Hooking: Hook only when needed
  • Resource Cleanup: Clean up hooks after use

Cleanup

Step 5) Clean Up Testing Environment

Click to view cleanup code
#!/bin/bash
set -euo pipefail

# Cleanup Mobile App Security Testing Environment
# Removes test artifacts and resets environment

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_FILE="${SCRIPT_DIR}/cleanup_testing.log"

log() {
    echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}

# Remove analysis results
cleanup_results() {
    log "Cleaning up analysis results..."
    
    if [ -d "./analysis_results" ]; then
        rm -rf ./analysis_results
        log "Removed analysis_results directory"
    fi
    
    if [ -d "./test_results" ]; then
        rm -rf ./test_results
        log "Removed test_results directory"
    fi
    
    if [ -d "./re_results" ]; then
        rm -rf ./re_results
        log "Removed re_results directory"
    fi
}

# Stop Frida servers
stop_frida() {
    log "Stopping Frida servers..."
    
    # Kill Frida processes
    pkill -f frida-server || true
    pkill -f frida-ps || true
    
    log "Frida servers stopped"
}

# Clean up extracted apps
cleanup_extracted() {
    log "Cleaning up extracted apps..."
    
    find . -type d -name "extracted_ipa" -exec rm -rf {} + 2>/dev/null || true
    find . -type d -name "*.app" -path "*/extracted_ipa/*" -exec rm -rf {} + 2>/dev/null || true
    
    log "Extracted apps cleaned"
}

# Remove temporary files
cleanup_temp() {
    log "Cleaning up temporary files..."
    
    find . -name "*.log" -type f -delete 2>/dev/null || true
    find . -name "*.tmp" -type f -delete 2>/dev/null || true
    find . -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
    find . -name "*.pyc" -type f -delete 2>/dev/null || true
    
    log "Temporary files cleaned"
}

# Verify cleanup
verify_cleanup() {
    log "Verifying cleanup..."
    
    if [ ! -d "./analysis_results" ] && [ ! -d "./test_results" ]; then
        log "✅ Cleanup verified: All test artifacts removed"
    else
        log "⚠️  Warning: Some artifacts may remain"
    fi
}

# Main cleanup
main() {
    log "Starting cleanup..."
    cleanup_results
    stop_frida
    cleanup_extracted
    cleanup_temp
    verify_cleanup
    log "Cleanup complete"
}

main "$@"

Validation:

# Run cleanup
bash cleanup_testing.sh

# Verify cleanup
ls -la | grep -E "analysis_results|test_results|re_results"

# Should show no results directories

Real-World Case Study

Challenge: A fintech mobile app with 500K+ users failed a security audit, discovering:

  • 12 hardcoded API keys in source code
  • Weak encryption for stored user data
  • Insecure network communication
  • Missing certificate pinning
  • SQL injection vulnerabilities in local database

Solution: Implemented comprehensive static and dynamic analysis:

  • Automated static analysis in CI/CD pipeline
  • Dynamic runtime testing with Frida
  • API security testing with Burp Suite
  • Certificate pinning implementation
  • Secure storage migration to Keychain

Results:

  • 100% vulnerability remediation: All 47 findings fixed
  • Zero critical issues: Reduced from 12 to 0
  • Security audit passed: Passed SOC 2 Type II audit
  • Zero security incidents: 18 months incident-free
  • 40% faster testing: Automated workflows
  • $250K cost savings: Reduced manual testing effort

FAQ

Q: How often should mobile apps be tested?

A: Test at every release and after major updates. Automated tests should run in CI/CD, with comprehensive manual testing quarterly.

Q: What’s the difference between SAST and DAST?

A: SAST analyzes source code without running the app. DAST tests the running application. Both are essential for comprehensive coverage.

Q: Can I test third-party apps?

A: Only test apps you own or have explicit written authorization. Testing without permission is illegal.

Conclusion

Mobile app security testing is essential for protecting user data and maintaining trust. Implement comprehensive static and dynamic analysis, automate testing workflows, and continuously monitor for vulnerabilities.

Action Steps

  1. Set up testing environment for iOS and Android
  2. Implement static analysis in CI/CD
  3. Configure dynamic analysis tools
  4. Set up runtime manipulation testing
  5. Automate security testing workflows
  6. Create remediation playbooks
  7. Train team on secure coding practices

Educational Use Only: This content is for educational purposes. Only test applications you own or have explicit authorization to test.

Similar Topics

FAQs

Can I use these labs in production?

No—treat them as educational. Adapt, review, and security-test before any production use.

How should I follow the lessons?

Start from the Learn page order or use Previous/Next on each lesson; both flow consistently.

What if I lack test data or infra?

Use synthetic data and local/lab environments. Never target networks or data you don't own or have written permission to test.

Can I share these materials?

Yes, with attribution and respecting any licensing for referenced tools or datasets.