363 lines
8.8 KiB
Markdown
363 lines
8.8 KiB
Markdown
---
|
|
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<kinfo_proc>.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
|
|
<!-- res/xml/network_security_config.xml -->
|
|
<network-security-config>
|
|
<domain-config>
|
|
<domain includeSubdomains='true'>api.example.com</domain>
|
|
<pin-set>
|
|
<pin digest='SHA-256'>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin>
|
|
</pin-set>
|
|
<trust-anchors>
|
|
<certificates src='system' />
|
|
</trust-anchors>
|
|
</domain-config>
|
|
</network-security-config>
|
|
```
|
|
|
|
```xml
|
|
<!-- AndroidManifest.xml -->
|
|
<application android:networkSecurityConfig='@xml/network_security_config'>
|
|
```
|
|
|
|
### App Transport Security (iOS)
|
|
```xml
|
|
<!-- Info.plist -->
|
|
<key>NSAppTransportSecurity</key>
|
|
<dict>
|
|
<key>NSAllowsArbitraryLoads</key>
|
|
<false/>
|
|
</dict>
|
|
```
|
|
|
|
→ 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]]
|