Skip to content

Commit 07e0b52

Browse files
sgIOlascpholgueraCopilotserek8
authored
Port MASTG-TEST-0046: Testing Anti-Debugging Detection (android) (by @vulnit) (#3699)
* Add MASTG-TOOL-0141 (debugmepLS) * Improve MASTG-TECH-0031 on non-debuggable apps Add a reference to MASTG-TOOL-0141 to describe the possibility of using hooking-based tools to enable JDWP-based debugging on non-debuggable apps. * Initial draft of MASTG-TEST-0046 for v2 * Initial commit for native debugging in Android * Add LLDB for Android * Improve lldb for android usage * Deprecate GDB in favor of lldb * Use tool IDs * Use warning block for note * Fix android:debuggable rule typo * Add new rule for runtime JDWP debug check * Add JDWP debugging demo * Move MASTG-DEMO-0040 to MASTG-DEMO-0x40 * Fix inconsistencies * Add lldb instability notice * Differentiate DVM and ART anti-debugging We should probably discuss the ART part, I looked around in Android's source but can't seem to find JdwpAdbState * Improve lldb incompatibility notice * Make MASTG-TECH-0071 generic * Add comment on passing test for MASTG-TEST-0046-1 * Recommend RASP usage on MASTG-TEST-0046 * Add MASTG-TEST-0046-2 * Archive reference link * Add demo for MASTG-TEST-0046-2 * Fix markdown linting issues * Simplify ptrace_self demo * Also use fake ids for tools * Consider ro.debuggable=1 * Add example for debuggable app * Move lldb segfault notice to its tool entry * Update MASTG-TEST-0046-1 with suggested fixes * Add best practice for debugging check * Use 'kind: pass' when needed * Remove FLAG_DEBUGGABLE checks * Remove FLAG_DEBUGGABLE checks * Update DEMO-0x40 evaluation * Update DEMO-0x41 evaluation * Use hosts key in Android tool front matter * Reword JDWP overview to reference best practice * Normalize debugging command blocks in TECH-0031 * Scope JDWP demo PASS/FAIL markers to branches * Fix typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update demos/android/MASVS-RESILIENCE/MASTG-DEMO-0x41/MASTG-DEMO-0x41.md Co-authored-by: Jan Seredynski <janseredynski@gmail.com> * fix(demo): Simplify buffer zero initialization Co-authored-by: Jan Seredynski <janseredynski@gmail.com> * fix: Remove leftover iOS references and add '-a' flag to strings Co-authored-by: Jan Seredynski <janseredynski@gmail.com> * fix(test): Remove timing APIs from MASTG-TEST-0046 * feat(demo): Use newer PTRACE_SEIZE for DEMO-0x41 * fix(demo): Stop using semgrep over sourcecode, use R2 instead for MASTG-DEMO-0x41 * fix(demo): Fix leftover references to PTRACE_ATTACH * fix(demo): Restrict semgrep rule to java * docs(test): Deprecate MASTG-TEST-0046 * Add platform to each lldb tool * Apply suggestion from @serek8 Co-authored-by: Jan Seredynski <janseredynski@gmail.com> * Make tools more concise and move examples to the TECH * Refactor tests: update test IDs and add new tests for debugging detection APIs. The tests had type: [static, dynamic], which should only be used under very specific conditions that must be really justified. Additionally, the static tests mixed both code-analysis steps and runtime-behavior observations, and the dynamic test was purely manual (attach debugger + observe) rather than using instrumentation. * Update demos for JDWP and native debugging checks to use consistent test IDs and improve documentation clarity * Update tests to mirror the patterns of MASTG-TEST-0324/0325/0351 * Enable relocation application in native debugger checks to align with guidelines * Add externalNativeBuild configuration for CMake in Android demo * Refactor GitHub Actions workflow for Android demos: improve matrix generation, enhance caching logic, and streamline file copying process. Add support for native libs. * Fix path assignment in externalNativeBuild configuration to use file() method for CMake in Android demo --------- Co-authored-by: Carlos Holguera <perezholguera@gmail.com> Co-authored-by: Sergio García <32015541+Olasergiolas@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jan Seredynski <janseredynski@gmail.com>
1 parent 962600a commit 07e0b52

35 files changed

Lines changed: 1498 additions & 135 deletions

.github/instructions/mastg-tools.instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ Common patterns that match existing pages in `tools/`:
7070
- Start with a short description paragraph (often with a link to the upstream project or docs).
7171
- Add sections only when they add value for that tool. Typical headings are `## Installation`, `## Usage`, and tool-specific headings (for example, `## Installing Frida on iOS`).
7272
- Include copyable commands when relevant. If usage is extensive, keep only the most common commands and link to a technique or upstream docs.
73+
- **Do not add step-by-step usage examples or multi-step walkthroughs to tool pages.** These belong in a technique page (for example, @MASTG-TECH-0031). The website will automatically list any techniques that reference the tool as examples of use on the rendered tool page, so adding them to the right technique page is the correct way to surface them.
7374
- Add caveats as `!!! note` / `!!! warning` admonitions when needed (version pinning, jailbreak/root requirements, security warnings).
7475
- Link to related techniques/tests/demos where it helps the reader complete a workflow.
7576

.github/workflows/build-android-demos.yml

Lines changed: 130 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -19,58 +19,68 @@ jobs:
1919
runs-on: ubuntu-latest
2020
outputs:
2121
matrix: ${{ steps.set-matrix.outputs.matrix }}
22+
2223
steps:
2324
- name: Checkout repository
2425
uses: actions/checkout@v6
2526
with:
2627
sparse-checkout: demos/android
27-
fetch-depth: 2 # Required for git diff in PRs
28+
fetch-depth: 0
2829

2930
- name: Generate matrix
3031
id: set-matrix
32+
shell: bash
3133
run: |
34+
set -euo pipefail
35+
3236
if [ "${{ github.event_name }}" = "pull_request" ]; then
33-
# Get list of changed files in demos/android/ directory
34-
changed_files=$(git diff --name-only ${{ github.event.pull_request.base.sha }} HEAD -- 'demos/android/*')
37+
changed_files="$(git diff --name-only "${{ github.event.pull_request.base.sha }}" HEAD -- 'demos/android/**')"
3538
3639
echo "Changed files:"
3740
echo "$changed_files"
3841
39-
# Extract unique demo directories
40-
matrix=$(echo "$changed_files" | grep -oE 'demos/android/[^/]*/MASTG-DEMO-[^/]+' | sort -u | head -c -1 | tr '\n' ' ' | sed 's/ /","/g')
41-
42-
# If no changes, set empty matrix
43-
if [ -z "$matrix" ]; then
44-
echo "matrix={\"demo\":[]}" >> $GITHUB_OUTPUT
45-
else
46-
echo "matrix={\"demo\":[\"$matrix\"]}" >> $GITHUB_OUTPUT
47-
fi
42+
demos="$(echo "$changed_files" \
43+
| grep -oE '^demos/android/[^/]+/MASTG-DEMO-[^/]+' \
44+
| sort -u || true)"
4845
else
49-
# Default behavior: include all demos for master branch
50-
matrix=$(echo demos/android/*/MASTG-DEMO-* | sed 's/ /","/g')
51-
echo "matrix={\"demo\":[\"$matrix\"]}" >> $GITHUB_OUTPUT
46+
demos="$(find demos/android -mindepth 2 -maxdepth 2 -type d -name 'MASTG-DEMO-*' | sort)"
5247
fi
48+
49+
matrix="$(printf '%s\n' "$demos" \
50+
| jq -R 'select(length > 0)' \
51+
| jq -sc '{demo: .}')"
52+
53+
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
5354
echo "Print matrix: $matrix"
55+
5456
- name: Print matrix
55-
run: echo "${{ steps.set-matrix.outputs.matrix }}"
57+
run: echo '${{ steps.set-matrix.outputs.matrix }}'
5658

5759
build-base-app:
5860
runs-on: ubuntu-latest
5961
timeout-minutes: 10
62+
outputs:
63+
mastestapp_hash: ${{ steps.get-mastestapp-hash.outputs.mastestapp_hash }}
64+
6065
steps:
6166
- name: Get last commit hash of mas-app-android
6267
id: get-mastestapp-hash
68+
shell: bash
6369
run: |
64-
echo "MASTESTAPP_HASH=$(git ls-remote https://github.com/cpholguera/mas-app-android.git HEAD | awk '{print $1}')" >> $GITHUB_ENV
65-
echo "MASTESTAPP_HASH=$MASTESTAPP_HASH" >> $GITHUB_OUTPUT
70+
set -euo pipefail
71+
72+
hash="$(git ls-remote https://github.com/cpholguera/mas-app-android.git HEAD | awk '{print $1}')"
73+
74+
echo "mastestapp_hash=$hash" >> "$GITHUB_OUTPUT"
75+
echo "MASTESTAPP_HASH=$hash" >> "$GITHUB_ENV"
6676
6777
- name: Check if already cached
6878
id: cache-check
6979
uses: actions/cache/restore@v5
7080
with:
7181
lookup-only: true
72-
path: mas-app-android/ # we're forced to write a path even if we don't need it
73-
key: base-app-${{ env.MASTESTAPP_HASH }}
82+
path: mas-app-android/
83+
key: base-app-${{ steps.get-mastestapp-hash.outputs.mastestapp_hash }}
7484

7585
- name: Set up JDK 17
7686
if: steps.cache-check.outputs.cache-hit != 'true'
@@ -85,41 +95,52 @@ jobs:
8595
with:
8696
cache-read-only: false
8797

98+
- name: Accept Android SDK licenses
99+
if: steps.cache-check.outputs.cache-hit != 'true'
100+
run: yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses
101+
88102
- name: Clone mas-app-android repository
89103
if: steps.cache-check.outputs.cache-hit != 'true'
90104
uses: actions/checkout@v6
91105
with:
92106
repository: cpholguera/mas-app-android
93107
path: mas-app-android
94-
ref: ${{ env.MASTESTAPP_HASH }}
108+
ref: ${{ steps.get-mastestapp-hash.outputs.mastestapp_hash }}
95109

96110
- name: Build base app
97111
if: steps.cache-check.outputs.cache-hit != 'true'
112+
shell: bash
98113
run: |
114+
set -euo pipefail
115+
99116
cd mas-app-android
100-
grep -q 'org.gradle.caching=true' gradle.properties || echo -en "\norg.gradle.caching=true\norg.gradle.configuration-cache=true\n" >> gradle.properties
101-
./gradlew assembleDebug --stacktrace || (
102-
echo "Build failed"
103-
exit 1
104-
)
117+
118+
grep -q 'org.gradle.caching=true' gradle.properties \
119+
|| echo -en "\norg.gradle.caching=true\norg.gradle.configuration-cache=true\n" >> gradle.properties
120+
121+
./gradlew assembleDebug --stacktrace
122+
105123
echo "Build succeeded"
106124
107125
- name: Saving cache
108126
if: steps.cache-check.outputs.cache-hit != 'true'
109127
uses: actions/cache/save@v5
110128
with:
111129
path: mas-app-android/
112-
key: ${{ steps.cache-check.outputs.cache-primary-key }}
113-
130+
key: base-app-${{ steps.get-mastestapp-hash.outputs.mastestapp_hash }}
131+
114132
build:
115-
needs: [generate-matrix, build-base-app]
133+
needs:
134+
- generate-matrix
135+
- build-base-app
116136
if: ${{ needs.generate-matrix.outputs.matrix != '{"demo":[]}' }}
117137
runs-on: ubuntu-latest
118-
timeout-minutes: 30 # Increase this value as needed
138+
timeout-minutes: 30
139+
119140
strategy:
120141
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
121-
max-parallel: 3 # Limit the number of parallel jobs
122-
142+
max-parallel: 3
143+
123144
steps:
124145
- name: Checkout repository
125146
uses: actions/checkout@v6
@@ -137,101 +158,126 @@ jobs:
137158
with:
138159
cache-read-only: true
139160

140-
- name: Restore cache
161+
- name: Accept Android SDK licenses
162+
run: yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses
163+
164+
- name: Restore base app cache
141165
id: cache-base-app
142166
uses: actions/cache/restore@v5
143167
with:
144168
path: mas-app-android/
145-
key: base-app-${{ env.MASTESTAPP_HASH }}
169+
key: base-app-${{ needs.build-base-app.outputs.mastestapp_hash }}
170+
fail-on-cache-miss: true
146171

147172
- name: Replace files and build APK
173+
shell: bash
148174
run: |
175+
set -euo pipefail
176+
149177
demo="${{ matrix.demo }}"
150-
[ -d "$demo" ] || (
178+
179+
[ -d "$demo" ] || {
151180
echo "Demo directory not found: $demo"
152181
exit 1
153-
)
182+
}
154183
155184
echo "Processing $demo"
156-
cp -f "$demo/MastgTest.kt" mas-app-android/app/src/main/java/org/owasp/mastestapp/MastgTest.kt 2>/dev/null \
157-
&& echo "Copied MastgTest.kt for $demo" \
158-
|| echo "No MastgTest.kt found for $demo"
159-
cp -f "$demo/MainActivity.kt" mas-app-android/app/src/main/java/org/owasp/mastestapp/MainActivity.kt 2>/dev/null \
160-
&& echo "Copied MainActivity.kt for $demo" \
161-
|| echo "No MainActivity.kt found for $demo"
162-
cp -f "$demo/MastgTestWebView.kt" mas-app-android/app/src/main/java/org/owasp/mastestapp/MastgTestWebView.kt 2>/dev/null \
163-
&& echo "Copied MastgTestWebView.kt for $demo" \
164-
|| echo "No MastgTestWebView.kt found for $demo"
165-
cp -f "$demo/AndroidManifest.xml" mas-app-android/app/src/main/AndroidManifest.xml 2>/dev/null \
166-
&& echo "Copied AndroidManifest.xml for $demo" \
167-
|| echo "No AndroidManifest.xml found for $demo"
168-
cp -f "$demo/filepaths.xml" mas-app-android/app/src/main/res/xml/filepaths.xml 2>/dev/null \
169-
&& echo "Copied filepaths.xml for $demo" \
170-
|| echo "No filepaths.xml found for $demo"
171-
cp -f "$demo/network_security_config.xml" mas-app-android/app/src/main/res/xml/network_security_config.xml 2>/dev/null \
172-
&& echo "Copied network_security_config.xml for $demo" \
173-
|| echo "No network_security_config.xml found for $demo"
174-
cp -f "$demo/backup_rules.xml" mas-app-android/app/src/main/res/xml/backup_rules.xml 2>/dev/null \
175-
&& echo "Copied backup_rules.xml for $demo" \
176-
|| echo "No backup_rules.xml found for $demo"
177-
cp -f "$demo/data_extraction_rules.xml" mas-app-android/app/src/main/res/xml/data_extraction_rules.xml 2>/dev/null \
178-
&& echo "Copied data_extraction_rules.xml for $demo" \
179-
|| echo "No data_extraction_rules.xml found for $demo"
180-
181-
# Copy .proto files if any exist
185+
186+
copy_if_exists() {
187+
local source_file="$1"
188+
local target_file="$2"
189+
local label="$3"
190+
191+
if [ -f "$source_file" ]; then
192+
cp -f "$source_file" "$target_file"
193+
echo "Copied $label for $demo"
194+
else
195+
echo "No $label found for $demo"
196+
fi
197+
}
198+
199+
copy_if_exists "$demo/MastgTest.kt" mas-app-android/app/src/main/java/org/owasp/mastestapp/MastgTest.kt MastgTest.kt
200+
copy_if_exists "$demo/MainActivity.kt" mas-app-android/app/src/main/java/org/owasp/mastestapp/MainActivity.kt MainActivity.kt
201+
copy_if_exists "$demo/MastgTestWebView.kt" mas-app-android/app/src/main/java/org/owasp/mastestapp/MastgTestWebView.kt MastgTestWebView.kt
202+
copy_if_exists "$demo/AndroidManifest.xml" mas-app-android/app/src/main/AndroidManifest.xml AndroidManifest.xml
203+
copy_if_exists "$demo/filepaths.xml" mas-app-android/app/src/main/res/xml/filepaths.xml filepaths.xml
204+
copy_if_exists "$demo/network_security_config.xml" mas-app-android/app/src/main/res/xml/network_security_config.xml network_security_config.xml
205+
copy_if_exists "$demo/backup_rules.xml" mas-app-android/app/src/main/res/xml/backup_rules.xml backup_rules.xml
206+
copy_if_exists "$demo/data_extraction_rules.xml" mas-app-android/app/src/main/res/xml/data_extraction_rules.xml data_extraction_rules.xml
207+
182208
if find "$demo" -maxdepth 1 -name "*.proto" -print -quit 2>/dev/null | grep -q .; then
183209
mkdir -p mas-app-android/app/src/main/proto
184-
find "$demo" -maxdepth 1 -name "*.proto" -print0 2>/dev/null | while IFS= read -r -d '' proto_file; do
185-
cp -f "$proto_file" mas-app-android/app/src/main/proto/ \
186-
&& echo "Copied $(basename "$proto_file") for $demo"
187-
done
210+
211+
find "$demo" -maxdepth 1 -name "*.proto" -print0 2>/dev/null \
212+
| while IFS= read -r -d '' proto_file; do
213+
cp -f "$proto_file" mas-app-android/app/src/main/proto/
214+
echo "Copied $(basename "$proto_file") for $demo"
215+
done
188216
else
189217
echo "No .proto files found for $demo"
190218
fi
191219
220+
if [ -f "$demo/CMakeLists.txt" ]; then
221+
mkdir -p mas-app-android/app/src/main/cpp
222+
223+
cp -f "$demo/CMakeLists.txt" mas-app-android/app/src/main/cpp/CMakeLists.txt
224+
echo "Copied CMakeLists.txt for $demo"
225+
226+
find "$demo" -maxdepth 1 -name "*.cpp" -print0 2>/dev/null \
227+
| while IFS= read -r -d '' cpp_file; do
228+
cp -f "$cpp_file" mas-app-android/app/src/main/cpp/
229+
echo "Copied $(basename "$cpp_file") for $demo"
230+
done
231+
else
232+
echo "No CMakeLists.txt found for $demo"
233+
fi
234+
192235
insert_block() {
193-
local kind="$1" # plugins / sections / libs
194-
local upper="${kind^^}" # PLUGINS / SECTIONS / LIBS (bash-specific)
236+
local kind="$1"
237+
local upper="${kind^^}"
195238
local file="$demo/build.gradle.kts.$kind"
196239
local target="mas-app-android/app/build.gradle.kts"
197-
240+
198241
if [ -f "$file" ]; then
199242
sed -i '/\/\/ ADD_'"$upper"'_HERE/{
200243
r '"$file"'
201244
d
202245
}' "$target"
203-
246+
204247
echo "Replaced $kind in build.gradle.kts for $demo"
205248
else
206249
echo "No build.gradle.kts.$kind found for $demo, skipping $kind replacement"
207250
fi
208251
}
209-
252+
210253
insert_block plugins
211254
insert_block sections
255+
insert_block android
212256
insert_block libs
213257
214258
echo "Building APK for $demo"
259+
215260
cd mas-app-android
216-
grep -q 'org.gradle.caching=true' gradle.properties || echo -en "\norg.gradle.caching=true\norg.gradle.configuration-cache=true\n" >> gradle.properties
217-
./gradlew assembleDebug --stacktrace || (
218-
echo "Build failed for $demo"
219-
exit 1
220-
)
261+
262+
grep -q 'org.gradle.caching=true' gradle.properties \
263+
|| echo -en "\norg.gradle.caching=true\norg.gradle.configuration-cache=true\n" >> gradle.properties
264+
265+
./gradlew assembleDebug --stacktrace
266+
221267
cd ..
222-
echo "Build succeeded for $demo"
268+
269+
echo "Build succeeded for $demo"
223270
224271
apk_filename="$(basename "$demo").apk"
225-
mv mas-app-android/app/build/outputs/apk/debug/app-debug.apk "$apk_filename" || (
226-
echo "APK not found for $demo"
227-
exit 1
228-
)
272+
273+
mv mas-app-android/app/build/outputs/apk/debug/app-debug.apk "$apk_filename"
274+
229275
echo "APK for $demo moved to $apk_filename"
230-
echo "APK_NAME=$apk_filename" >> $GITHUB_ENV
276+
echo "APK_NAME=$apk_filename" >> "$GITHUB_ENV"
231277
232278
- name: Upload APK
233279
uses: actions/upload-artifact@v6
234280
with:
235281
name: ${{ env.APK_NAME }}
236-
path: "${{ env.APK_NAME }}"
237-
if-no-files-found: error
282+
path: ${{ env.APK_NAME }}
283+
if-no-files-found: error

best-practices/MASTG-BEST-0x32.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
title: Continuous Anti-Debugging Checks
3+
alias: continuous-anti-debugging-checks
4+
id: MASTG-BEST-0x32
5+
platform: android
6+
knowledge: [MASTG-KNOW-0028]
7+
---
8+
9+
Implement frequent anti-debugging checks during sensitive execution paths instead of relying on a one-time check at startup.
10+
11+
A one-time check is easy to bypass because an attacker can attach a debugger after initialization and continue analysis without triggering the initial detection logic. Re-checking debugger state at runtime raises attacker effort and improves resilience.
12+
13+
On Android, you can combine Java and native checks depending on your threat model. For example, use [`Debug.isDebuggerConnected()`](https://developer.android.com/reference/android/os/Debug#isDebuggerConnected()) in Java flows and complement it with native checks in sensitive JNI paths.
14+
15+
Furthermore, tight continuous polling loops can be resource heavy. A good balance is to implement checkpoint-based checks before sensitive actions (for example, key use, payment approval, privileged API calls, or secrets access) and short periodic checks only while sensitive flows are active. This model reduces overhead while still making post-start debugger attachment harder.
16+
17+
Treat anti-debugging as a defense-in-depth control, not as a standalone guarantee. Advanced attackers can still bypass checks through patching, instrumentation, or hooks.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:tools="http://schemas.android.com/tools">
4+
5+
<uses-permission android:name="android.permission.INTERNET" />
6+
7+
<application
8+
android:allowBackup="true"
9+
android:dataExtractionRules="@xml/data_extraction_rules"
10+
android:fullBackupContent="@xml/backup_rules"
11+
android:icon="@mipmap/ic_launcher"
12+
android:label="@string/app_name"
13+
android:roundIcon="@mipmap/ic_launcher_round"
14+
android:supportsRtl="true"
15+
android:theme="@style/Theme.MASTestApp"
16+
tools:targetApi="31">
17+
<activity
18+
android:name=".MainActivity"
19+
android:exported="true"
20+
android:windowSoftInputMode="adjustResize"
21+
android:theme="@style/Theme.MASTestApp">
22+
<intent-filter>
23+
<action android:name="android.intent.action.MAIN" />
24+
25+
<category android:name="android.intent.category.LAUNCHER" />
26+
</intent-filter>
27+
</activity>
28+
</application>
29+
30+
</manifest>

0 commit comments

Comments
 (0)