Skip to content
Open
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
05f3e3c
Deprecate v1 test
Diolor Jan 19, 2026
b44f8f4
Layout the knowledge
Diolor Jan 19, 2026
0177d94
Improve grammar
Diolor Jan 19, 2026
6783f22
Enhance runtime integrity verification documentation with detailed te…
Diolor Jan 19, 2026
296b1e4
Add reference
Diolor Jan 19, 2026
7afdb80
Reorder
Diolor Jan 19, 2026
ae571d0
Add initial test
Diolor Jan 19, 2026
6d1e5d0
Merge branch 'master' into port-v1-test-0050
Diolor Jan 30, 2026
1e8b982
Format table
Diolor Jan 30, 2026
ac48167
Fix notes
Diolor Jan 30, 2026
561a677
Create the test
Diolor Jan 30, 2026
562cbd0
Add generic bullet and grammarly
Diolor Jan 30, 2026
a519993
Merge branch 'master' into port-v1-test-0050
Diolor Feb 6, 2026
4593cda
Initial commit of demos
Diolor Feb 6, 2026
7ff16cb
Update demo 87 output
Diolor Feb 6, 2026
d3868e3
Update demo 88 output
Diolor Feb 6, 2026
411ee2a
Fix demo wording
Diolor Feb 6, 2026
dd68825
Fix lints
Diolor Feb 6, 2026
eb14e51
EOF
Diolor Feb 6, 2026
8acd5e6
Initial BE for runtime hook detection
Diolor Feb 9, 2026
25129d6
Rewrite BE to emphasize the layers
Diolor Feb 9, 2026
0745792
Rename
Diolor Feb 9, 2026
f90b55c
Rename
Diolor Feb 9, 2026
6e3373c
Grammar
Diolor Feb 9, 2026
eca2026
Mark the lack of BE
Diolor Feb 9, 2026
5d42595
Add know-0027
Diolor Feb 9, 2026
e21c4f5
Create bypass demo
Diolor Feb 9, 2026
3c38385
Rewrite
Diolor Feb 9, 2026
a795d81
Remove process kill hook and simplify
Diolor Feb 9, 2026
fbafc6f
Inline references
Diolor Feb 9, 2026
358631f
Fix headers
Diolor Feb 9, 2026
6058aee
Update demos/android/MASVS-RESILIENCE/MASTG-DEMO-0089/MASTG-DEMO-0089.md
Diolor Feb 9, 2026
4cf1f15
Migrate DEMOs to use frooky
Diolor Feb 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions best-practices/MASTG-BEST-0029.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
title: Hardening Against Runtime Hooking
alias: hardening-against-runtime-hooking
id: MASTG-BEST-0029
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Allocate before merge + filname

platform: android
knowledge: [MASTG-KNOW-0027, MASTG-KNOW-0030, MASTG-KNOW-0032, MASTG-KNOW-00kw]
---

Defending against runtime hooking requires a layered approach that combines several types of security controls:

- **Preventive controls**: Implement root detection (@MASTG-KNOW-0027) and device/app attestation (https://github.com/OWASP/mastg/issues/3505) as the first line of defense, since most hooking frameworks (e.g., Frida server, Xposed) require rooted devices.

Check failure on line 11 in best-practices/MASTG-BEST-0029.md

View workflow job for this annotation

GitHub Actions / markdown-lint-check

Bare URL used

best-practices/MASTG-BEST-0029.md:11:100 MD034/no-bare-urls Bare URL used [Context: "https://github.com/OWASP/mastg..."] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md034.md
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

- **Detective controls**: Scan for tool signatures using artifact-based detection (@MASTG-KNOW-0030) and verify the app's code and memory integrity at runtime (@MASTG-KNOW-0032) to detect hooking attempts.
- **Deterrent controls**: Obfuscate detection logic, scatter checks throughout the app, and vary their timing to increase the cost and effort required to bypass protections.
- **Responsive controls**: Terminate the session, clear sensitive data from memory, or even alert the backend server when a threat is detected.

Because hooking can also occur on non-rooted devices (e.g., by repackaging the app with an embedded frida-gadget, see @MASTG-TECH-0026), do not rely solely on preventive controls. Apply the detective, deterrent, and responsive controls described below to protect against hooking regardless of the device's root status.

## Detective Controls

### Combine Artifact-Based and Integrity-Based Detection

Implement both artifact-based detection (@MASTG-KNOW-0030) and runtime integrity verification (@MASTG-KNOW-0032). Use artifact-based detection to scan for known tool signatures (e.g., Frida server processes, libraries, open ports) and runtime integrity verification to detect the _modifications_ these tools make to the app's code and memory (e.g., GOT hooks, inline trampolines, ART method entry point changes). Do not rely on only one approach, as each has blind spots the other covers.

### Apply Multiple Detection Techniques

Layer several techniques to maximize detection coverage:

- **Memory scanning**: Scan `/proc/self/maps` and process memory for known artifacts (e.g., "LIBFRIDA", frida-agent libraries, Xposed bridge classes).
- **Integrity checksums**: Compute checksums of critical code sections at build/load time and verify them periodically at runtime to detect patches and inline hooks.
- **GOT/PLT verification**: Verify that Global Offset Table entries point to addresses within their expected libraries.
- **Function prologue inspection**: Compare the first bytes of security-critical functions against their expected values to detect trampoline patterns (e.g., `LDR X16, .+8; BR X16` on ARM64).
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also consider: #3692 (comment)

- **ART method verification**: Use JNI's `FromReflectedMethod` to confirm that Java method entry points fall within legitimate regions (OAT file, interpreter, or JIT code cache).
- **Network-based checks**: Probe for D-Bus responses on open ports to reveal frida-server even when renamed.

## Deterrent Controls

### Implement Detection in Native Code

Consider writing detection checks in native (C/C++) code rather than Java/Kotlin. Native code is significantly harder to hook and reverse engineer than Java bytecode, which can be easily intercepted via Frida's Java API or Xposed modules. Use JNI to bridge results back to the application layer.

### Obfuscate Detection Logic

Apply [code obfuscation](../Document/0x04c-Tampering-and-Reverse-Engineering.md#obfuscation) to all detection routines. Scatter checks throughout the app rather than centralizing them in a single function, and vary their timing (e.g., periodic, event-driven, or randomized) to prevent systematic bypassing.

## Responsive Controls

Trigger the following response actions when hooks are detected:

- Terminate the app session immediately.
- Clear sensitive data from memory before exiting.
- Alert the backend server to flag the compromised session.

Do not allow the app to continue running in a compromised state. Protect the response mechanism itself against hooking by implementing it in native code and obfuscating its control flow.
34 changes: 34 additions & 0 deletions demos/android/MASVS-RESILIENCE/MASTG-DEMO-0087/MASTG-DEMO-0087.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
platform: android
title: Extracting Sensitive Data from Cipher.doFinal via Frida Hooking
id: MASTG-DEMO-0087
Copy link
Collaborator Author

@Diolor Diolor Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Update before merge + filename

code: [kotlin]
test: MASTG-TEST-03te
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Update before merge

kind: fail
---

## Sample

This sample encrypts and decrypts a sensitive API key using AES/GCM via the Android KeyStore. The app does not implement any runtime hook detection mechanisms. On the contrary, @MASTG-DEMO-0088 demonstrates a runtime hook detection mechanism.

{{ MastgTest.kt }}

## Steps

1. Install the app on a device (@MASTG-TECH-0005)
2. Use @MASTG-TECH-0033 to target `Cipher.dofinal()` API
3. Run `run.sh` to spawn the app with Frida
4. Click the **Start** button
5. Stop the script by pressing `Ctrl+C` and/or `q` to quit the Frida CLI

{{ script.js # run.sh }}

## Observation

The output contains all instances of `Cipher` method calls found at runtime. A backtrace is also provided to help identify the location in the code. The `doFinal` calls reveal the sensitive API key in plaintext: as the input parameter during encryption and as the return value during decryption.

{{ output.txt }}

## Evaluation

The test fails because the hook executes successfully and the sensitive API key `sk-OWASP-MAS-SuperSecretKey-1234567890` is extracted in plaintext from the `Cipher.doFinal()` calls. The app lacks runtime integrity verification, allowing instrumentation tools to intercept cryptographic operations without any defensive response.
68 changes: 68 additions & 0 deletions demos/android/MASVS-RESILIENCE/MASTG-DEMO-0087/MastgTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package org.owasp.mastestapp

import android.content.Context
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec

class MastgTest(private val context: Context) {

private val sensitiveApiKey = "sk-OWASP-MAS-SuperSecretKey-1234567890"
private val keyAlias = "mastgCipherKey"

private fun getOrCreateSecretKey(): SecretKey {
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
return if (keyStore.containsAlias(keyAlias)) {
(keyStore.getEntry(keyAlias, null) as KeyStore.SecretKeyEntry).secretKey
} else {
KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore"
).apply {
init(
KeyGenParameterSpec.Builder(
keyAlias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build()
)
}.generateKey()
}
}

fun mastgTest(): String {
return try {
val key = getOrCreateSecretKey()

// Encrypt the sensitive API key
val encryptCipher = Cipher.getInstance("AES/GCM/NoPadding")
encryptCipher.init(Cipher.ENCRYPT_MODE, key)
val iv = encryptCipher.iv
val encryptedBytes = encryptCipher.doFinal(sensitiveApiKey.toByteArray(Charsets.UTF_8))
val encryptedData = Base64.encodeToString(iv + encryptedBytes, Base64.DEFAULT)

// Decrypt to verify
val decodedData = Base64.decode(encryptedData, Base64.DEFAULT)
val ivFromData = decodedData.copyOfRange(0, 12)
val ciphertext = decodedData.copyOfRange(12, decodedData.size)

val decryptCipher = Cipher.getInstance("AES/GCM/NoPadding")
decryptCipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(128, ivFromData))
val decryptedBytes = decryptCipher.doFinal(ciphertext)
val decryptedString = String(decryptedBytes, Charsets.UTF_8)

"Encryption and decryption successful.\n" +
"Encrypted: $encryptedData\n" +
"Decrypted: $decryptedString"
} catch (e: Exception) {
"Error: ${e.message}"
}
}
}
33 changes: 33 additions & 0 deletions demos/android/MASVS-RESILIENCE/MASTG-DEMO-0087/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@

[+] Frida script loaded. Hooking Cipher.doFinal() to extract sensitive data.



[*] Cipher.doFinal(byte[]) called
Algorithm: AES/GCM/NoPadding
Mode: ENCRYPT_MODE
Input (plaintext): sk-OWASP-MAS-SuperSecretKey-1234567890
Output (ciphertext hex): b1e23e01082d0b660385a200566d1912f0be8e4bb843ae509b1752557da3a2586b9105bebcea2bbef1bba39e32a5ee98455aec67f7fe

Backtrace:
javax.crypto.Cipher.doFinal(Native Method)
org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:48)
org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)
org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)
org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)
java.lang.Thread.run(Thread.java:1119)


[*] Cipher.doFinal(byte[]) called
Algorithm: AES/GCM/NoPadding
Mode: DECRYPT_MODE
Input (ciphertext hex): b1e23e01082d0b660385a200566d1912f0be8e4bb843ae509b1752557da3a2586b9105bebcea2bbef1bba39e32a5ee98455aec67f7fe
Output (plaintext): sk-OWASP-MAS-SuperSecretKey-1234567890

Backtrace:
javax.crypto.Cipher.doFinal(Native Method)
org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:58)
org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)
org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)
org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)
java.lang.Thread.run(Thread.java:1119)
2 changes: 2 additions & 0 deletions demos/android/MASVS-RESILIENCE/MASTG-DEMO-0087/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
frida -U -f org.owasp.mastestapp -l script.js -o output.txt
61 changes: 61 additions & 0 deletions demos/android/MASVS-RESILIENCE/MASTG-DEMO-0087/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
Java.perform(() => {

function printBacktrace(maxLines = 8) {
let Exception = Java.use("java.lang.Exception");
let stackTrace = Exception.$new().getStackTrace().toString().split(",");

console.log("\nBacktrace:");
for (let i = 0; i < Math.min(maxLines, stackTrace.length); i++) {
console.log(stackTrace[i]);
}
}

function bytesToHex(bytes) {
let hex = [];
for (let i = 0; i < bytes.length; i++) {
hex.push(("0" + (bytes[i] & 0xFF).toString(16)).slice(-2));
}
return hex.join("");
}

let Cipher = Java.use("javax.crypto.Cipher");
let StringClass = Java.use("java.lang.String");

// Hook Cipher.doFinal(byte[])
Cipher["doFinal"].overload("[B").implementation = function (input) {
let result = this["doFinal"](input);
let algorithm = this.getAlgorithm();

// Cipher.ENCRYPT_MODE = 1, Cipher.DECRYPT_MODE = 2
let opmode = this.opmode.value;
let modeStr = opmode === 1 ? "ENCRYPT_MODE" : opmode === 2 ? "DECRYPT_MODE" : "UNKNOWN(" + opmode + ")";

console.log("\n\n[*] Cipher.doFinal(byte[]) called");
console.log(" Algorithm: " + algorithm);
console.log(" Mode: " + modeStr);

if (opmode === 1) {
try {
console.log(" Input (plaintext): " + StringClass.$new(input));
} catch (e) {
console.log(" Input (hex): " + bytesToHex(input));
}
console.log(" Output (ciphertext hex): " + bytesToHex(result));
}

if (opmode === 2) {
console.log(" Input (ciphertext hex): " + bytesToHex(input));
try {
console.log(" Output (plaintext): " + StringClass.$new(result));
} catch (e) {
console.log(" Output (hex): " + bytesToHex(result));
}
}

printBacktrace();

return result;
};

console.log("\n[+] Frida script loaded. Hooking Cipher.doFinal() to extract sensitive data.\n");
});
37 changes: 37 additions & 0 deletions demos/android/MASVS-RESILIENCE/MASTG-DEMO-0088/MASTG-DEMO-0088.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
platform: android
title: App Terminating on Frida Hook Detection Before Cipher.doFinal Execution
id: MASTG-DEMO-0088
Copy link
Collaborator Author

@Diolor Diolor Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Update before merge + filename

code: [kotlin]
test: MASTG-TEST-03te
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Update before merge

kind: pass
---

## Sample

This sample encrypts and decrypts a sensitive API key using AES/GCM via the Android KeyStore. Unlike the unprotected variant in @MASTG-DEMO-0087, this version includes a runtime hook detection mechanism that reads `/proc/self/maps` to check for the presence of Frida-related libraries (e.g., `frida-agent`, `frida-gadget`). If detected, the app terminates the process immediately via `Process.killProcess()` before any cryptographic operations are performed.

{{ MastgTest.kt }}

## Steps

1. Install the app on a device (@MASTG-TECH-0005)
2. Use @MASTG-TECH-0033 to target `Cipher.dofinal()` API
3. Run `run.sh` to spawn the app with Frida
4. Click the **Start** button
5. Observe that the app terminates before the hooks can capture any data

{{ script.js # run.sh }}

## Observation

The output contains no instances of `Cipher` method calls found at runtime. The terminal output from the `run.sh` command is `Process terminated`.

{{ output.txt }}

## Evaluation

The test passes because the hooking attempt fails due to the app's defensive response. The app detects the Frida agent by scanning `/proc/self/maps` for entries containing "frida" or "gadget" and terminates the process via `Process.killProcess()`. The process terminates before `Cipher.doFinal()` hooks execute, so no sensitive data is extracted.

!!! note
Even if the test case passes, it might still be possible to bypass the app's defensive response. For example, an attacker could hook the `detectHooking()` method itself or lower level APIs such as the file reading APIs to hide Frida from the process memory map. @MASTG-DEMO-0089 demonstrates such a bypass. @MASTG-KNOW-0030 and @MASTG-KNOW-0032 describe such challenges.
99 changes: 99 additions & 0 deletions demos/android/MASVS-RESILIENCE/MASTG-DEMO-0088/MastgTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package org.owasp.mastestapp

import android.content.Context
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import java.io.BufferedReader
import java.io.FileReader
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec

class MastgTest(private val context: Context) {

private val sensitiveApiKey = "sk-OWASP-MAS-SuperSecretKey-1234567890"
private val keyAlias = "mastgCipherKey"

init {
if (detectHooking()) {
android.os.Process.killProcess(android.os.Process.myPid())
}
}

private fun detectHooking(): Boolean {
try {
BufferedReader(FileReader("/proc/self/maps")).use { reader ->
var line: String?
while (reader.readLine().also { line = it } != null) {
val l = line!!.lowercase()
if (l.contains("frida") || l.contains("gadget")) {
return true
}
}
}
} catch (_: Exception) {
// Unable to read maps
}
return false
}

private fun getOrCreateSecretKey(): SecretKey {
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
return if (keyStore.containsAlias(keyAlias)) {
(keyStore.getEntry(keyAlias, null) as KeyStore.SecretKeyEntry).secretKey
} else {
KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore"
).apply {
init(
KeyGenParameterSpec.Builder(
keyAlias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build()
)
}.generateKey()
}
}

fun mastgTest(): String {
// Check for hooking before performing cryptographic operations
if (detectHooking()) {
android.os.Process.killProcess(android.os.Process.myPid())
return ""
}

return try {
val key = getOrCreateSecretKey()

// Encrypt the sensitive API key
val encryptCipher = Cipher.getInstance("AES/GCM/NoPadding")
encryptCipher.init(Cipher.ENCRYPT_MODE, key)
val iv = encryptCipher.iv
val encryptedBytes = encryptCipher.doFinal(sensitiveApiKey.toByteArray(Charsets.UTF_8))
val encryptedData = Base64.encodeToString(iv + encryptedBytes, Base64.DEFAULT)

// Decrypt to verify
val decodedData = Base64.decode(encryptedData, Base64.DEFAULT)
val ivFromData = decodedData.copyOfRange(0, 12)
val ciphertext = decodedData.copyOfRange(12, decodedData.size)

val decryptCipher = Cipher.getInstance("AES/GCM/NoPadding")
decryptCipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(128, ivFromData))
val decryptedBytes = decryptCipher.doFinal(ciphertext)
val decryptedString = String(decryptedBytes, Charsets.UTF_8)

"Encryption and decryption successful.\n" +
"Encrypted: $encryptedData\n" +
"Decrypted: $decryptedString"
} catch (e: Exception) {
"Error: ${e.message}"
}
}
}
3 changes: 3 additions & 0 deletions demos/android/MASVS-RESILIENCE/MASTG-DEMO-0088/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

[+] Frida script loaded. Hooking Cipher.doFinal() to extract sensitive data.

Loading
Loading