[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,362 @@
|
||||
---
|
||||
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]]
|
||||
Reference in New Issue
Block a user