8.8 KiB
8.8 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| security-mobile-hardening | Mobile Security — root / jailbreak / SSL pin | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
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
// 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://")!)
}
// 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
// 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)
}
}
// 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)
// app/build.gradle
android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
# 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
// 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
}
// Android
fun isDebuggerAttached(): Boolean = Debug.isDebuggerConnected()
Secret 의 storage
// 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)
// 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
// iOS
let context = LAContext()
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: 'Login') { success, error in
if success {
// Allow access.
}
}
// Android
val biometric = BiometricPrompt(this, executor, callback)
biometric.authenticate(BiometricPrompt.PromptInfo.Builder()
.setTitle('Login')
.setNegativeButtonText('Cancel')
.build())
Token refresh
// 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 (안 됨)
// ❌ API key in code
let API_KEY = 'sk_live_xxx'
→ Decompile 가 trivial. Server 가 proxy.
Network security config (Android)
<!-- 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>
<!-- AndroidManifest.xml -->
<application android:networkSecurityConfig='@xml/network_security_config'>
App Transport Security (iOS)
<!-- 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
// 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
// iOS App Attest
let attestService = DCAppAttestService.shared
attestService.generateKey { keyId, error in
attestService.attestKey(keyId, clientDataHash: hash) { attestation, error in
// Send to server.
}
}
// 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.