Seguretat en Aplicacions Mòbils
Introducció
Les aplicacions mòbils gestionen dades altament sensibles: credencials bancàries, dades de salut, localització en temps real, missatges privats. A diferència de les aplicacions web, les apps mòbils corren en dispositius fora del control de l'empresa, en xarxes no confiables i en mans d'usuaris que poden tenir el dispositiu compromès (jailbreak/root).
L'OWASP MASVS (Mobile Application Security Verification Standard) és el referent per a la seguretat de les aplicacions mòbils, complementat pels OWASP Mobile Top 10, la llista de les 10 vulnerabilitats més crítiques en entorns mòbils.
flowchart TD
APP[Aplicació Mòbil] --> STORAGE[Emmagatzematge local]
APP --> NETWORK[Comunicació de xarxa]
APP --> AUTH[Autenticació]
APP --> CRYPTO[Criptografia]
APP --> UI[Interfície d'usuari]
STORAGE --> S1[Keychain/Keystore]
STORAGE --> S2[SharedPreferences]
STORAGE --> S3[SQLite DB]
NETWORK --> N1[Certificate Pinning]
NETWORK --> N2[TLS 1.3]
AUTH --> A1[Biometria]
AUTH --> A2[OAuth 2.0/OIDC]
style S2 fill:#FF5722
style S3 fill:#FF9800
style S1 fill:#4CAF50
OWASP Mobile Top 10
M1 - Ús Inadequat de Credencials
Emmagatzemar credencials (contrasenyes, tokens, API keys) en llocs insegurs:
// ❌ MAL: Guardar token en SharedPreferences en text clar
val prefs = getSharedPreferences("app", Context.MODE_PRIVATE)
prefs.edit().putString("auth_token", "Bearer abc123...").apply()
// ❌ MAL: Guardar en fitxer de text
File("/sdcard/credentials.txt").writeText("user:password")
// ✅ BÉ: Usar Android Keystore + EncryptedSharedPreferences
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val encryptedPrefs = EncryptedSharedPreferences.create(
context,
"secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
encryptedPrefs.edit().putString("auth_token", "Bearer abc123...").apply()
M2 - Seguretat Inadequada de la Cadena de Subministrament
Les biblioteques de tercers poden introduir vulnerabilitats. Eines de detecció:
# Analitzar dependències Android per vulnerabilitats
./gradlew dependencyCheckAnalyze
# OWASP Dependency-Check
docker run --rm \
-v $(pwd):/src \
owasp/dependency-check \
--project "MyApp" \
--scan /src \
--format HTML \
--out /src/reports
# iOS: verificar amb CocoaPods Audit
pod audit
M3 - Autenticació/Autorització Insegura
// ❌ MAL: Autenticació basada en el client
if (userId == "admin") {
showAdminPanel() // Fàcil de bypassejar amb proxy
}
// ✅ BÉ: Sempre validar al servidor
// El client envia el token JWT, el servidor valida el rol
suspend fun getAdminData(): Result<AdminData> {
val response = apiService.getAdminData(
token = sessionManager.getToken()
)
// El servidor retorna 403 si no és admin
return if (response.isSuccessful) {
Result.success(response.body()!!)
} else {
Result.failure(UnauthorizedException())
}
}
M4 - Validació d'Entrada Insuficient
// iOS - ❌ MAL: Confiar en les dades de l'usuari
let query = "SELECT * FROM users WHERE name = '\(userInput)'"
// SQL injection si l'usuari introdueix: ' OR '1'='1
// ✅ BÉ: Usar prepared statements
let query = "SELECT * FROM users WHERE name = ?"
let statement = try db.prepare(query)
try statement.bind(userInput)
M5 - Comunicació Insegura
// ❌ MAL: HTTP en text clar
val url = URL("http://api.empresa.cat/users")
// ❌ MAL: Desactivar validació de certificats (PRÀCTICA MOLT PERILLOSA)
val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
override fun checkClientTrusted(chain: Array<X509Certificate>?, authType: String?) {}
override fun checkServerTrusted(chain: Array<X509Certificate>?, authType: String?) {}
// Accepta QUALSEVOL certificat, inclosos els de l'atacant
})
// ✅ BÉ: HTTPS + Certificate Pinning
val certificatePinner = CertificatePinner.Builder()
.add("api.empresa.cat", "sha256/HASH_DEL_CERTIFICAT_REAL")
.build()
val client = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()
Certificate Pinning
El certificate pinning fa que l'app accepti ÚNICAMENT el certificat específic del servidor, evitant atacs Man-in-the-Middle fins i tot amb un certificat TLS "vàlid" però fraudulent. El desavantatge és que cal actualitzar l'app quan canvia el certificat del servidor.
M6 - Controls de Privacitat Inadequats
Les apps sol·liciten permisos excessius. Aplicar el principi de mínims privilegis:
<!-- AndroidManifest.xml -->
<!-- ❌ MAL: Demanar permisos innecessaris -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.CAMERA"/>
<!-- Per una app de conversi de moneda no cal cap d'aquests -->
<!-- ✅ BÉ: Únicament els permisos necessaris, i en runtime -->
<!-- Cap permís en l'exemple d'una app de conversió -->
// Demanar permisos en runtime (Android 6+) quan realment es necessitin
private fun requestCameraPermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.CAMERA),
CAMERA_REQUEST_CODE
)
}
}
Emmagatzematge Segur
Android Keystore
L'Android Keystore és un repositori de claus criptogràfiques a nivell del hardware (en dispositius amb TEE - Trusted Execution Environment):
// Crear una clau en el Keystore (no exportable)
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore"
)
keyGenerator.init(
KeyGenParameterSpec.Builder(
"my_key_alias",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true) // Requereix biometria/PIN
.build()
)
val secretKey = keyGenerator.generateKey()
// La clau mai surt del Keystore en text clar
// Usada per xifrar dades sensibles localment
iOS Keychain
// Guardar un token de forma segura a iOS
import Security
func saveToken(_ token: String) {
let data = token.data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "auth_token",
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
// Únicament accessible quan el dispositiu està desbloquejat
// i únicament en AQUEST dispositiu (no es migra al backup)
]
SecItemAdd(query as CFDictionary, nil)
}
Model de Permisos
Android
flowchart TD
APP[Aplicació] -->|Sol·licita permís| OS[Sistema Android]
OS -->|Instal·lació o Runtime| USER[Usuari]
USER -->|Accepta/Denega| OS
OS -->|Concedeix/Denega| APP
subgraph Permisos normals
INTERNET[INTERNET]
VIBRATE[VIBRATE]
end
subgraph Permisos perillosos
CAMERA[CAMERA]
LOCATION[ACCESS_FINE_LOCATION]
CONTACTS[READ_CONTACTS]
STORAGE[READ_EXTERNAL_STORAGE]
end
subgraph Permisos especials
OVERLAY[SYSTEM_ALERT_WINDOW]
ACCESSIBILITY[BIND_ACCESSIBILITY_SERVICE]
end
Els permisos perillosos requereixen aprovació explícita de l'usuari en runtime (Android 6+). Els permisos especials requereixen navegació als ajustos del sistema.
iOS App Transport Security
<!-- Info.plist - iOS exigeix HTTPS per defecte -->
<key>NSAppTransportSecurity</key>
<dict>
<!-- ❌ MAL: Desactivar ATS completament -->
<key>NSAllowsArbitraryLoads</key>
<true/>
<!-- ✅ BÉ: Excepcions específiques si cal (i justificades) -->
<key>NSExceptionDomains</key>
<dict>
<key>legacy-api.empresa.cat</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<!-- Documentar per quin motiu i quan s'eliminarà -->
</dict>
</dict>
</dict>
Anàlisi Estàtica d'Apps (SAST Mòbil)
# MobSF (Mobile Security Framework) - anàlisi estàtica i dinàmica
docker run -it --rm \
--name mobsf-NOMCOGNOM \
-p 8000:8000 \
-v $(pwd)/mobsf-data:/home/mobsf/.MobSF \
opensecurity/mobile-security-framework-mobsf:latest
# Accedir a http://localhost:8000
# Pujar l'APK o IPA per a anàlisi automàtica
# Analystic: eina per a Android específicament
docker run --rm \
-v $(pwd)/app.apk:/input/app.apk \
-v $(pwd)/output:/output \
mobiletools/apkanalyzer /input/app.apk
Miniactivitat
Descarrega una app de codi obert d'Android (per exemple, Signal, Bitwarden) i analitza-la amb MobSF:
- Quin nivell MASVS correspon a les seves pràctiques de seguretat?
- Hi ha algun permís que trobes excessiu?
- El codi font (si és obert) confirma les pràctiques que MobSF detecta?
Monitoratge de Tràfic Mòbil
# mitmproxy: proxy HTTPS per a inspeccionar tràfic mòbil
docker run --rm -it \
--name mitmproxy-NOMCOGNOM \
-p 8080:8080 \
-p 8081:8081 \
mitmproxy/mitmproxy mitmweb --web-host 0.0.0.0
# En el dispositiu mòbil:
# 1. Configurar proxy HTTP: IP_DEL_PC:8080
# 2. Instal·lar el certificat mitmproxy (http://mitm.it)
# 3. Navegar normalment - el tràfic apareix a http://localhost:8081
# NOTA: Funciona únicament per a apps sense certificate pinning
Ús ètic de mitmproxy
L'ús d'un proxy per interceptar tràfic únicament és legal per a: - Aplicacions pròpies en dispositius propis - Pentesting amb autorització escrita - Entorns de laboratori controlats
Interceptar tràfic de tercers sense autorització és il·legal.
Exercici pràctic
Analitza la seguretat d'una aplicació mòbil:
- Usa MobSF per analitzar una APK (pots generar una app bàsica o usar una app de demo)
- Identifica almenys 3 vulnerabilitats de l'OWASP Mobile Top 10
- Per cada vulnerabilitat, proposa la correcció de codi
- Avalua quin nivell MASVS compleix l'app (L1 o L2)
Lliura un informe analisi_mobil_NOMCOGNOM.pdf amb captures de pantalla de MobSF i les teves conclusions.