--- id: security-mobile-hardening title: Mobile Security — root / jailbreak / SSL pin category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [mobile, security, vibe-coding] tech_stack: { language: "Swift / Kotlin", applicable_to: ["iOS", "Android"] } applied_in: [] aliases: [mobile security, root detection, jailbreak detection, SSL pinning, ProGuard, code obfuscation, anti-tamper] --- # Mobile Security Hardening > 사용자 device 가 untrusted. **Root/jailbreak detect, SSL pinning, obfuscation, anti-tamper**. 100% 막을 수 X — cost ↑. ## 📖 핵심 개념 - 매 device 가 attacker (악성 user / malware). - Defense-in-depth (multiple layer). - Crypto key 가 client X. - Server 가 trust boundary. ## 💻 코드 패턴 ### Root / Jailbreak detection ```swift // iOS func isJailbroken() -> Bool { let paths = ["/Applications/Cydia.app", "/usr/sbin/sshd", "/etc/apt"] return paths.contains { FileManager.default.fileExists(atPath: $0) } || canOpen(URL(string: "cydia://")!) } ``` ```kotlin // Android fun isRooted(): Boolean { val paths = listOf("/system/app/Superuser.apk", "/system/xbin/su", "/system/bin/su") if (paths.any { File(it).exists() }) return true return try { Runtime.getRuntime().exec("which su").waitFor() == 0 } catch (e: Exception) { false } } ``` → Detect 만. Block 가 user-friendly X. ### SSL Pinning ```swift // iOS — URLSessionDelegate func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { guard let serverTrust = challenge.protectionSpace.serverTrust, let cert = SecTrustGetCertificateAtIndex(serverTrust, 0) else { completionHandler(.cancelAuthenticationChallenge, nil) return } let serverCertData = SecCertificateCopyData(cert) as Data let pinnedCertData = // ... loaded from bundle if serverCertData == pinnedCertData { completionHandler(.useCredential, URLCredential(trust: serverTrust)) } else { completionHandler(.cancelAuthenticationChallenge, nil) } } ``` ```kotlin // Android (OkHttp) val pinner = CertificatePinner.Builder() .add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") .build() val client = OkHttpClient.Builder().certificatePinner(pinner).build() ``` → MITM 방지. Cert rotation 시 update. ### Code obfuscation (Android) ```gradle // app/build.gradle android { buildTypes { release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } } ``` ```proguard # proguard-rules.pro -keepattributes SourceFile,LineNumberTable -renamesourcefileattribute SourceFile # Keep models for serialization -keep class com.example.models.** { *; } ``` → R8 (modern) — 작은 + 빠른. ### iOS obfuscation ``` Xcode 가 native obfuscation X. - Symbol stripping (release build). - Swift name mangling 가 자체. - 외부 tool (Swift Shield, etc.) 가 추가. ``` → Symbol stripping 가 baseline. ### Anti-debug ```swift // iOS func isDebuggerAttached() -> Bool { var info = kinfo_proc() var size = MemoryLayout.size var name: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()] let r = sysctl(&name, 4, &info, &size, nil, 0) return r == 0 && (info.kp_proc.p_flag & P_TRACED) != 0 } ``` ```kotlin // Android fun isDebuggerAttached(): Boolean = Debug.isDebuggerConnected() ``` ### Secret 의 storage ```swift // iOS Keychain let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: 'token', kSecValueData as String: token.data(using: .utf8)!, kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly, ] SecItemAdd(query as CFDictionary, nil) ``` ```kotlin // Android EncryptedSharedPreferences val masterKey = MasterKey.Builder(context) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build() val prefs = EncryptedSharedPreferences.create( context, "secret-prefs", masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ) prefs.edit().putString('token', token).apply() ``` → Hardware-backed (TEE / Secure Enclave). ### Biometric auth ```swift // iOS let context = LAContext() context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: 'Login') { success, error in if success { // Allow access. } } ``` ```kotlin // Android val biometric = BiometricPrompt(this, executor, callback) biometric.authenticate(BiometricPrompt.PromptInfo.Builder() .setTitle('Login') .setNegativeButtonText('Cancel') .build()) ``` ### Token refresh ```ts // Short-lived access token (15 min). // Long-lived refresh token (Keychain). // 401 → refresh → retry. async function fetchWithRefresh(url: string) { let r = await fetch(url, { headers: { Authorization: `Bearer ${accessToken}` } }); if (r.status === 401) { accessToken = await refresh(); r = await fetch(url, { headers: { Authorization: `Bearer ${accessToken}` } }); } return r; } ``` ### Anti-tamper ``` - App signature check (release build). - Integrity check (file hash). - Native code (NDK) 가 더 어려움 to tamper. ``` → 100% prevent X. Cost ↑. ### Secret in code (안 됨) ```swift // ❌ API key in code let API_KEY = 'sk_live_xxx' ``` → Decompile 가 trivial. Server 가 proxy. ### Network security config (Android) ```xml api.example.com AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= ``` ```xml ``` ### App Transport Security (iOS) ```xml NSAppTransportSecurity NSAllowsArbitraryLoads ``` → HTTPS-only default. ### Reverse engineering tool ``` - Frida (runtime hook). - Cycript (iOS). - IDA Pro / Ghidra (decompile). - Charles Proxy / mitmproxy (network). - Cydia / jailbroken iOS. - Magisk (root Android). → Defense-in-depth 가 cost ↑. ``` ### Frida detection ```swift // iOS let dlopen_handle = dlopen('frida-gum', RTLD_NOLOAD) if dlopen_handle != nil { // Frida detected. } ``` → 또 bypass 가능. Cat-and-mouse. ### 결론 ``` 모든 client = trustless. - 매 critical logic = server. - 매 secret = server. - Client = UI 만 신뢰. Hardening: - Rate limit (server). - Anomaly detect (server). - Token rotation. - Device attestation. ``` ### Device attestation ```swift // iOS App Attest let attestService = DCAppAttestService.shared attestService.generateKey { keyId, error in attestService.attestKey(keyId, clientDataHash: hash) { attestation, error in // Send to server. } } ``` ```kotlin // Android Play Integrity val integrityManager = IntegrityManagerFactory.create(context) val task = integrityManager.requestIntegrityToken( IntegrityTokenRequest.builder().setNonce(nonce).build() ) ``` → Apple / Google 가 device 의 integrity 검증. 서버 가 token 가 valid 면 trust. ### OWASP Mobile Top 10 ``` M1: Improper credential usage M2: Inadequate supply chain M3: Insecure auth M4: Insufficient input validation M5: Insecure communication M6: Inadequate privacy M7: Insufficient binary protection M8: Security misconfiguration M9: Insecure data storage M10: Insufficient cryptography ``` → Reference. ### Cost 인지 ``` Strict hardening: - Setup cost. - Maintenance (cert rotation, ProGuard tuning). - Crash 가능 (false positive). - 사용자 friction (jailbreak detect). → 위험 가 가치 가져야. - Banking app: 큰 hardening. - Casual game: 작은. ``` ## 🤔 의사결정 기준 | 위험 | 추천 | |---|---| | Banking | 모든 (root + pin + obfuscate + attest) | | E-commerce | SSL pin + token rotation | | Casual | HTTPS + Keychain | | Internal | Token + biometric | | Game | Anti-cheat (server) | ## ❌ 안티패턴 - **Secret in code**: decompile. - **Plain HTTP**: MITM. - **Custom crypto**: bug. - **Local storage 가 plain**: extract. - **No SSL pin (sensitive)**: MITM. - **Trust client**: server 가 trust boundary. ## 🤖 LLM 활용 힌트 - Defense-in-depth (multiple layer). - Server 가 trust boundary. - App attestation 가 modern. - Hardening cost vs 위험 trade-off. ## 🔗 관련 문서 - [[Security_Auth_Authz_Patterns]] - [[Security_Secrets_Management]] - [[iOS_Keychain_Storage]]