Skip to content

Commit 0d1225e

Browse files
authored
Merge pull request #3359 from bernhste/DEMO-KeyGenParamSpec
PoC frida.re Base Script
2 parents dd18493 + 5a0c8e5 commit 0d1225e

File tree

14 files changed

+1189
-0
lines changed

14 files changed

+1189
-0
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
platform: android
3+
title: Use of Insecure ECB Block Mode in KeyGenParameterSpec
4+
id: MASTG-DEMO-0058
5+
code: [kotlin]
6+
test: MASTG-TEST-0232
7+
---
8+
9+
### Sample
10+
11+
The code below generates symmetric encryption keys meant to be stored in the Android KeyStore, but it does so using the ECB block mode, which is considered broken due to practical known-plaintext attacks and is disallowed by NIST for data encryption. The method used to set the block modes is [`KeyGenParameterSpec.Builder#setBlockModes(...)`](https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder#setBlockModes(java.lang.String[])):
12+
13+
```kotlin
14+
public KeyGenParameterSpec.Builder setBlockModes (String... blockModes)
15+
```
16+
17+
Current versions of Android prohibit the usage of keys with for ECB in some cases. For example, it is not possible to use the key to encrypt data by the default. Nevertheless, there are some case, where ECB can still be used:
18+
19+
- Decrypt data
20+
- Encrypt data with a key given `setRandomizedEncryptionRequired` is set to `false`
21+
22+
{{ MastgTest.kt }}
23+
24+
### Steps
25+
26+
1. Install the app on a device (@MASTG-TECH-0005)
27+
2. Make sure you have @MASTG-TOOL-0001 installed on your machine and the frida-server running on the device
28+
3. Run `run.sh` to spawn the app with Frida
29+
4. Click the **Start** button
30+
5. Stop the script by pressing `Ctrl+C` and/or `q` to quit the Frida CLI
31+
32+
{{ hooks.js # run.sh }}
33+
34+
### Observation
35+
36+
The output shows all instances of block modes mode that were found at runtime. A backtrace is also provided to help identify the location in the code.
37+
38+
{{ output.json }}
39+
40+
### Evaluation
41+
42+
The method `setBlockModes` has now been called three times with ECB as one of the block modes.
43+
44+
The test fails, as key used with these `KeyGenParameterSpec` can now be used used to insecurely encrypt data.
45+
46+
You can automatically evaluate the output using tools like `jq` as demonstrated in `evaluation.sh`.
47+
48+
{{ evaluate.sh }}
49+
50+
See @MASTG-TEST-0232 for more information.
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package org.owasp.mastestapp
2+
3+
import android.content.Context
4+
import android.security.keystore.KeyGenParameterSpec
5+
import android.security.keystore.KeyProperties
6+
import android.security.keystore.KeyProtection
7+
import java.security.KeyStore
8+
import javax.crypto.Cipher
9+
import javax.crypto.KeyGenerator
10+
import javax.crypto.SecretKey
11+
import android.util.Base64
12+
import javax.crypto.spec.SecretKeySpec
13+
14+
class MastgTest(private val context: Context) {
15+
16+
fun mastgTest(): String {
17+
18+
val results = mutableListOf<String>()
19+
var rawKey: SecretKey? = null
20+
var encryptedData: ByteArray? = null
21+
var decryptedData: ByteArray? = null
22+
23+
// Suppose we received a raw key from a secure source and we want to use it for decryption.
24+
// The following commented-out code is an example of generating a raw key and encrypting data with it.
25+
// We obtained the raw key and encrypted data from the logs and added them to the code for demonstration purposes.
26+
try {
27+
// Suppose we received the raw key from a secure source and we want to use it for decryption.
28+
val rawKeyString = "43ede5660e82123ee091d6b4c8f7d150"
29+
val keyBytes = rawKeyString.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
30+
rawKey = SecretKeySpec(keyBytes, KeyProperties.KEY_ALGORITHM_AES)
31+
32+
// The cipher text is 'Hello from OWASP MASTG!' AES/ECB encrypted using CyberChef:
33+
// https://gchq.github.io/CyberChef/#recipe=AES_Encrypt(%7B'option':'Hex','string':'43ede5660e82123ee091d6b4c8f7d150'%7D,%7B'option':'Hex','string':''%7D,'ECB','Raw','Hex',%7B'option':'Hex','string':''%7D)&input=SGVsbG8gZnJvbSBPV0FTUCBNQVNURyE
34+
val encryptedDataString = "20b0eef4e5ad3d8984a4fb94f6001885f0ce25104cb8251f600624b46dcefb92"
35+
encryptedData = encryptedDataString.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
36+
37+
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
38+
val alias = "importedAesKey"
39+
val entry = KeyStore.SecretKeyEntry(rawKey)
40+
val protection = KeyProtection.Builder(KeyProperties.PURPOSE_DECRYPT)
41+
.setBlockModes(KeyProperties.BLOCK_MODE_ECB)
42+
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
43+
.build()
44+
keyStore.setEntry(alias, entry, protection)
45+
val importedKey = keyStore.getKey(alias, null) as SecretKey
46+
val cipher2 = Cipher.getInstance("AES/ECB/PKCS7Padding").apply {
47+
init(Cipher.DECRYPT_MODE, importedKey)
48+
}
49+
decryptedData = cipher2.doFinal(encryptedData)
50+
val decryptedString = String(decryptedData)
51+
results.add("\n[*] Keystore-imported AES ECB key decryption (plaintext):\n\n$decryptedString")
52+
} catch (e: Exception) {
53+
results.add("\n[!] Keystore-imported AES ECB key decryption error:\n\n${e.message}")
54+
}
55+
56+
// import the raw key into AndroidKeyStore for encryption which would fail unless randomized encryption is disabled (bad practice)
57+
try {
58+
if (rawKey == null || encryptedData == null) {
59+
throw IllegalStateException("Key or data missing for encryption")
60+
}
61+
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
62+
val alias = "importedAesKey2"
63+
val entry = KeyStore.SecretKeyEntry(rawKey)
64+
val protection = KeyProtection.Builder(KeyProperties.PURPOSE_ENCRYPT)
65+
.setBlockModes(KeyProperties.BLOCK_MODE_ECB)
66+
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
67+
.setRandomizedEncryptionRequired(false) // For demonstration purposes, we disable randomized encryption
68+
.build()
69+
keyStore.setEntry(alias, entry, protection)
70+
val importedKey = keyStore.getKey(alias, null) as SecretKey
71+
val cipher3 = Cipher.getInstance("AES/ECB/PKCS7Padding").apply {
72+
init(Cipher.ENCRYPT_MODE, importedKey)
73+
}
74+
val encryptedBytes = cipher3.doFinal(decryptedData)
75+
val encrypted = Base64.encodeToString(encryptedBytes, Base64.DEFAULT)
76+
77+
results.add("\n\n[*] Keystore-imported AES ECB key encryption (ciphertext):\n\n$encrypted")
78+
} catch (e: Exception) {
79+
results.add("\n\n[!] Keystore-imported AES ECB key encryption error:\n\n${e.message}")
80+
}
81+
82+
// keystore key generation and encryption
83+
try {
84+
val keyAlias = "testKeyGenParameter"
85+
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
86+
val spec = KeyGenParameterSpec.Builder(
87+
keyAlias,
88+
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
89+
)
90+
.setBlockModes(KeyProperties.BLOCK_MODE_ECB)
91+
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
92+
// .setRandomizedEncryptionRequired(false) // Disabling randomized encryption would allow the key to be used in ECB mode.
93+
.build()
94+
KeyGenerator.getInstance(
95+
KeyProperties.KEY_ALGORITHM_AES,
96+
"AndroidKeyStore"
97+
).apply {
98+
init(spec)
99+
generateKey()
100+
}
101+
102+
val secretKey = keyStore.getKey(keyAlias, null) as SecretKey
103+
val cipher = Cipher.getInstance("AES/ECB/PKCS7Padding").apply {
104+
init(Cipher.ENCRYPT_MODE, secretKey)
105+
}
106+
val encrypted = Base64.encodeToString(cipher.doFinal(decryptedData), Base64.DEFAULT)
107+
results.add("\n[*] Keystore-generated AES ECB key encryption (ciphertext):\n\n$encrypted")
108+
} catch (e: Exception) {
109+
results.add("\n[!] Keystore-generated AES ECB error:\n\n${e.message}")
110+
}
111+
112+
return results.joinToString("\n")
113+
}
114+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/bin/bash
2+
3+
jq '
4+
select(
5+
.class=="android.security.keystore.KeyGenParameterSpec$Builder"
6+
and .method=="setBlockModes"
7+
and (.inputParameters[0].value | contains(["ECB"]))
8+
)
9+
' output.json
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
var target = {
2+
category: "CRYPTO",
3+
demo: "0058",
4+
hooks: [
5+
{
6+
class: "android.security.keystore.KeyGenParameterSpec$Builder",
7+
methods: [
8+
"setBlockModes",
9+
"setRandomizedEncryptionRequired"
10+
]
11+
},
12+
{
13+
class: "android.security.keystore.KeyProtection$Builder",
14+
methods: [
15+
"setBlockModes",
16+
"setRandomizedEncryptionRequired"
17+
]
18+
}
19+
]
20+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
{
2+
"id": "523e8eb7-e155-4792-bdae-6c2a728c87ac",
3+
"category": "CRYPTO",
4+
"time": "2025-08-01T09:00:07.277Z",
5+
"class": "android.security.keystore.KeyProtection$Builder",
6+
"method": "setBlockModes",
7+
"stackTrace": [
8+
"android.security.keystore.KeyProtection$Builder.setBlockModes(Native Method)",
9+
"org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:41)",
10+
"org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$7(MainActivity.kt:53)",
11+
"org.owasp.mastestapp.MainActivityKt.$r8$lambda$JVJO2MsmWvFAgk27L17N1ocLpI0(Unknown Source:0)",
12+
"org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda0.invoke(D8$$SyntheticClass:0)",
13+
"androidx.compose.foundation.ClickableNode$clickPointerInput$3.invoke-k-4lQ0M(Clickable.kt:639)",
14+
"androidx.compose.foundation.ClickableNode$clickPointerInput$3.invoke(Clickable.kt:633)",
15+
"androidx.compose.foundation.gestures.TapGestureDetectorKt$detectTapAndPress$2$1.invokeSuspend(TapGestureDetector.kt:255)"
16+
],
17+
"inputParameters": [
18+
{
19+
"type": "[Ljava.lang.String;",
20+
"value": [
21+
"ECB"
22+
]
23+
}
24+
],
25+
"returnValue": [
26+
{
27+
"type": "android.security.keystore.KeyProtection$Builder",
28+
"value": "<instance: android.security.keystore.KeyProtection$Builder>"
29+
}
30+
]
31+
}
32+
{
33+
"id": "a162bca9-454e-4d48-a737-0ac6e73983c7",
34+
"category": "CRYPTO",
35+
"time": "2025-08-01T09:00:07.288Z",
36+
"class": "android.security.keystore.KeyProtection$Builder",
37+
"method": "setBlockModes",
38+
"stackTrace": [
39+
"android.security.keystore.KeyProtection$Builder.setBlockModes(Native Method)",
40+
"org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:65)",
41+
"org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$7(MainActivity.kt:53)",
42+
"org.owasp.mastestapp.MainActivityKt.$r8$lambda$JVJO2MsmWvFAgk27L17N1ocLpI0(Unknown Source:0)",
43+
"org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda0.invoke(D8$$SyntheticClass:0)",
44+
"androidx.compose.foundation.ClickableNode$clickPointerInput$3.invoke-k-4lQ0M(Clickable.kt:639)",
45+
"androidx.compose.foundation.ClickableNode$clickPointerInput$3.invoke(Clickable.kt:633)",
46+
"androidx.compose.foundation.gestures.TapGestureDetectorKt$detectTapAndPress$2$1.invokeSuspend(TapGestureDetector.kt:255)"
47+
],
48+
"inputParameters": [
49+
{
50+
"type": "[Ljava.lang.String;",
51+
"value": [
52+
"ECB"
53+
]
54+
}
55+
],
56+
"returnValue": [
57+
{
58+
"type": "android.security.keystore.KeyProtection$Builder",
59+
"value": "<instance: android.security.keystore.KeyProtection$Builder>"
60+
}
61+
]
62+
}
63+
{
64+
"id": "8dd8050c-dbc0-4662-804a-8bfb2151ca34",
65+
"category": "CRYPTO",
66+
"time": "2025-08-01T09:00:07.291Z",
67+
"class": "android.security.keystore.KeyProtection$Builder",
68+
"method": "setRandomizedEncryptionRequired",
69+
"stackTrace": [
70+
"android.security.keystore.KeyProtection$Builder.setRandomizedEncryptionRequired(Native Method)",
71+
"org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:67)",
72+
"org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$7(MainActivity.kt:53)",
73+
"org.owasp.mastestapp.MainActivityKt.$r8$lambda$JVJO2MsmWvFAgk27L17N1ocLpI0(Unknown Source:0)",
74+
"org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda0.invoke(D8$$SyntheticClass:0)",
75+
"androidx.compose.foundation.ClickableNode$clickPointerInput$3.invoke-k-4lQ0M(Clickable.kt:639)",
76+
"androidx.compose.foundation.ClickableNode$clickPointerInput$3.invoke(Clickable.kt:633)",
77+
"androidx.compose.foundation.gestures.TapGestureDetectorKt$detectTapAndPress$2$1.invokeSuspend(TapGestureDetector.kt:255)"
78+
],
79+
"inputParameters": [
80+
{
81+
"type": "boolean",
82+
"value": false
83+
}
84+
],
85+
"returnValue": [
86+
{
87+
"type": "android.security.keystore.KeyProtection$Builder",
88+
"value": "<instance: android.security.keystore.KeyProtection$Builder>"
89+
}
90+
]
91+
}
92+
{
93+
"id": "394de339-b6c6-485e-babc-672ff5df315f",
94+
"category": "CRYPTO",
95+
"time": "2025-08-01T09:00:07.300Z",
96+
"class": "android.security.keystore.KeyGenParameterSpec$Builder",
97+
"method": "setBlockModes",
98+
"stackTrace": [
99+
"android.security.keystore.KeyGenParameterSpec$Builder.setBlockModes(Native Method)",
100+
"org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:90)",
101+
"org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$7(MainActivity.kt:53)",
102+
"org.owasp.mastestapp.MainActivityKt.$r8$lambda$JVJO2MsmWvFAgk27L17N1ocLpI0(Unknown Source:0)",
103+
"org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda0.invoke(D8$$SyntheticClass:0)",
104+
"androidx.compose.foundation.ClickableNode$clickPointerInput$3.invoke-k-4lQ0M(Clickable.kt:639)",
105+
"androidx.compose.foundation.ClickableNode$clickPointerInput$3.invoke(Clickable.kt:633)",
106+
"androidx.compose.foundation.gestures.TapGestureDetectorKt$detectTapAndPress$2$1.invokeSuspend(TapGestureDetector.kt:255)"
107+
],
108+
"inputParameters": [
109+
{
110+
"type": "[Ljava.lang.String;",
111+
"value": [
112+
"ECB"
113+
]
114+
}
115+
],
116+
"returnValue": [
117+
{
118+
"type": "android.security.keystore.KeyGenParameterSpec$Builder",
119+
"value": "<instance: android.security.keystore.KeyGenParameterSpec$Builder>"
120+
}
121+
]
122+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/bin/bash
2+
../../../../utils/frida/android/run.sh ./hooks.js
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
---
2+
platform: android
3+
title: App Writing Sensitive Data to Sandbox using SharedPreferences
4+
id: MASTG-DEMO-0059
5+
code: [kotlin]
6+
test: MASTG-TEST-0207
7+
---
8+
9+
### Sample
10+
11+
The code snippet below shows sample code which stores sensitive data using `SharedPreferences`. It stores sensitive data using `String` and `StringSet`.
12+
13+
{{ MastgTest.kt }}
14+
15+
### Steps
16+
17+
1. Install the app on a device (@MASTG-TECH-0005)
18+
2. Make sure you have @MASTG-TOOL-0001 installed on your machine and the frida-server running on the device
19+
3. Run `run.sh` to spawn the app with Frida
20+
4. Click the **Start** button
21+
5. Stop the script by pressing `Ctrl+C` and/or `q` to quit the Frida CLI
22+
23+
{{ hooks.js # run.sh }}
24+
25+
### Observation
26+
27+
The output shows all instances of strings written via `SharedPreferences` that were found at runtime. A backtrace is also provided to help identify the location in the code.
28+
29+
{{ output.json }}
30+
31+
### Evaluation
32+
33+
In output.json we can identify several entries that use the `SharedPreferences` API write strings to the app's local sandbox. In this case to `/data/data/org.owasp.mastestapp/shared_prefs/MasSharedPref_Sensitive_Data.xml`:
34+
35+
- `putString` is used to write an unencrypted `UnencryptedGitHubToken` of value `ghp_1234567890a...`
36+
- `putString` is used to write an encrypted `EncryptedAwsKey` of value `V1QyXhGV88RQLmMjoTLLl...`
37+
- `putStringSet` is used to write an unencrypted `UnencryptedPreSharedKeys` set with values `MIIEvAIBADAN...` and `gJXS9EwpuzK8...`
38+
39+
We can use the values and try to trace them back to crypto method calls and check if they are encrypted. For example, let's analyze the `EncryptedAwsKey` of value `V1QyXhGV88RQLmMjoTLLl...`:
40+
41+
- `V1QyXhGV88RQLmMjoTLLl...` is the return value of `Base64.encodeToString` for the input `0x5754325e1195f3c45...`
42+
- `0xa132cb95022985be` is the return value of `Cipher.doFinal` for the input `AKIAIOSFODNN7EXAMPLE`
43+
44+
However, we cannot find any calls to `Base64.encodeToString` or `Cipher.***` for the `preSharedKeys` values written by `putStringSet` (`MIIEvAIBADAN...` and `gJXS9EwpuzK8...`).
45+
46+
You can confirm this by reverse engineering the app and inspecting the code. Inspect the `stackTrace` of the `putString` and `putStringSet` entries, then go to the corresponding locations in the code. For example, go to the `org.owasp.mastestapp.MastgTest.mastgTest` method and try to trace back the input parameters to determine whether they are encrypted.
47+
48+
The test **fails** due because we found some entries that aren't encrypted.
49+
50+
Any data in the app sandbox can be extracted using backups or root access on a compromised phone. For example, run the following command:
51+
52+
```sh
53+
adb shell cat /data/data/org.owasp.mastestapp/shared_prefs/MasSharedPref_Sensitive_Data.xml
54+
```
55+
56+
Which returns:
57+
58+
```xml
59+
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
60+
<map>
61+
<string name="EncryptedAwsKey">V1QyXhGV88RQLmMjoTLLlQIphb6SKf4CBqx+PqhH/TTPFtCh9RPTAYezWW5RPhPP&#10; </string>
62+
<set name="UnencryptedPreSharedKeys">
63+
<string>gJXS9EwpuzK8U1TOgfplwfKEVngCE2D5FNBQWvNmuHHbigmTCabsA=</string>
64+
<string>MIIEvAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALfX7kbfFv3pc3JjOHQ=</string>
65+
</set>
66+
<string name="UnencryptedGitHubToken">ghp_1234567890abcdefghijklmnOPQRSTUV</string>
67+
</map>
68+
```
69+
70+
All entries that aren't encrypted can be leveraged by an attacker.

0 commit comments

Comments
 (0)