news 2026/10/6 2:20:43

使用 Semgrep 检测 Android 应用绕过作用域存储写入外部存储的漏洞:MASTG-DEMO-0003 实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用 Semgrep 检测 Android 应用绕过作用域存储写入外部存储的漏洞:MASTG-DEMO-0003 实战指南
  • 文档
  • 教程
  • 网络安全

【免费下载链接】mastg

The OWASP Mobile Application Security Testing Guide (MASTG) is a comprehensive manual for mobile app security testing and reverse engineering. It describes technical processes for verifying the OWASP Mobile Security Weakness Enumeration (MASWE) weaknesses, which are in alignment with the OWASP MASVS.

项目地址:https://gitcode.com/gh_mirrors/ow/mastg
点击查看免费下载

本文基于 OWASP MASTG(Mobile Application Security Testing Guide)仓库中的MASTG-DEMO-0003示例,完整演示如何通过静态分析(Semgrep 规则扫描)识别 Android 应用绕开 Scoped Storage(作用域存储)限制、向共享外部存储写入未加密敏感数据的漏洞。读完本文,你将掌握MANAGE_EXTERNAL_STORAGE权限的声明方式与风险、Environment#getExternalStorageDirectory等遗留 API 的判别方法,以及如何在反编译产物上运行 MASTG 官方规则并依据输出定位与评估安全测试用例。

背景:外部存储与 Scoped Storage 之争

从 Android 10(API 29)开始,系统强制推行 Scoped Storage(作用域存储):应用只能无额外权限地访问自己专属的目录(如getExternalFilesDir返回的路径),以及通过MediaStore访问媒体集合,不能再随意向共享外部存储根目录写入文件。然而,Android 11(API 30)引入了 "All files access"(所有文件访问)特殊权限:应用只要在清单中声明MANAGE_EXTERNAL_STORAGE,并经用户在系统设置中手动授予后,就可以继续使用Environment.getExternalStorageDirectory()这类遗留 API,直接向/storage/emulated/0这样的共享存储根目录写入文件。

这种做法的安全风险在于:共享外部存储中的文件对所有具备存储读取权限的应用可见,任何文件一旦以明文落盘,就等于把数据暴露给了同设备上的其他应用。这正是 MASTG-TEST-0202(对应 MASWE-0002)所关注的弱点:应用引用外部存储 API 与相关权限,且将敏感数据未加密写入共享存储。

MASTG-DEMO-0003正是该测试用例的可复现示例:一个刻意编写了漏洞的测试应用,一段反编译后的 Java 代码,两份 Android 清单文件,以及两套 Semgrep 规则与运行脚本,完整呈现"写代码 → 反编译 → 规则扫描 → 定位 → 评估"的整条测试链路。

Sample:存在漏洞的示例代码

示例的核心代码位于 MastgTest.kt,一个 Kotlin 类MastgTest,其mastgTest()方法直接绕过 Scoped Storage 向外部存储根目录写入文件:

package org.owasp.mastestapp import android.content.Context import android.os.Environment import android.util.Log import java.io.File import java.io.FileOutputStream import java.io.IOException class MastgTest (private val context: Context){ fun mastgTest(): String { val externalStorageDir = Environment.getExternalStorageDirectory() val fileName = File(externalStorageDir, "secret.txt") val fileContent = "Secret not using scoped storage" try { FileOutputStream(fileName).use { output -> output.write(fileContent.toByteArray()) Log.d("WriteExternalStorage", "File written to external storage successfully.") } } catch (e: IOException) { Log.e("WriteExternalStorage", "Error writing file to external storage", e) return "ERROR!!\n\nError writing file to external storage. Do you have the MANAGE_EXTERNAL_STORAGE permission in the manifest and it's granted in 'All files access'?" } return "SUCCESS!!\n\nFile $fileName with content $fileContent saved to $externalStorageDir" } }

代码中的三个关键点:

  1. Environment.getExternalStorageDirectory():返回共享外部存储根目录(如/storage/emulated/0),这是 Android 10 之前写入共享存储的经典方式,在 Scoped Storage 时代必须依赖 "All files access" 特殊权限才能继续使用。
  2. File(externalStorageDir, "secret.txt"):直接在共享存储根目录创建文件,而非使用getExternalFilesDir等应用私有外部目录。
  3. 明文写入"Secret not using scoped storage":这是一个"敏感数据"的替身(实际场景中可能是密码、令牌、个人信息),未做任何加密即落盘。

为了能正常运行,AndroidManifest.xml 中声明了特殊权限:

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.MASTestApp" tools:targetApi="31"> <activity android:name=".MainActivity" android:exported="true" android:theme="@style/Theme.MASTestApp"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>

注意MANAGE_EXTERNAL_STORAGE与普通运行时权限(如WRITE_EXTERNAL_STORAGE)不同:它属于特殊应用权限,无法在运行时通过requestPermissions弹窗授予,必须引导用户进入系统设置中的 "All files access"(所有文件访问)页面手动开启,且 Google Play 对该权限有严格的上架审核限制,仅允许文件管理器、备份类等核心功能为"文件管理"的应用使用。

反编译视角:Kotlin 如何变成 Java

测试过程模拟真实逆向场景:对 APK 反编译后得到 MastgTest_reversed.java(对应 MASTG-TECH-0013 逆向工程技术),Kotlin 编译器产物被还原为等价的 Java:

package org.owasp.mastestapp; import android.content.Context; import android.os.Environment; import android.util.Log; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; ... /* compiled from: MastgTest.kt */ public final class MastgTest { private final Context context; public MastgTest(Context context) { Intrinsics.checkNotNullParameter(context, "context"); this.context = context; } public final String mastgTest() { File externalStorageDir = Environment.getExternalStorageDirectory(); File fileName = new File(externalStorageDir, "secret.txt"); try { FileOutputStream fileOutputStream = new FileOutputStream(fileName); ... output.write(bytes); Log.d("WriteExternalStorage", "File written to external storage successfully."); ... } catch (IOException e) { Log.e("WriteExternalStorage", "Error writing file to external storage", e); ... } } }

这段反编译代码与 Kotlin 源码逻辑一一对应:第 27 行File externalStorageDir = Environment.getExternalStorageDirectory();就是后续 Semgrep 规则将命中的位置。这也说明静态分析直接作用于反编译产物是完全可行的——这正是 MASTG 测试方法论所倡导的做法(MASTG-TEST-0202 的步骤 1 即要求先逆向 APK)。

同样地,APK 解包得到的 AndroidManifest_reversed.xml 保留了MANAGE_EXTERNAL_STORAGE权限声明(第 2 行),并额外暴露了编译细节:compileSdkVersion="34"、android:debuggable="true"、android:testOnly="true",以及 androidx 运行时组件(CoreComponentFactory、InitializationProvider、ProfileInstallReceiver等)。

规则:两套 MASTG Semgrep 规则详解

本演示使用 MASTG-TOOL-0110(Semgrep) 作为静态扫描器。Semgrep 是能够扫描 Java、Kotlin 和 Swift 的静态代码扫描工具,MASTG 仓库将其规则以 YAML 形式存放在 rules 目录下,本次用到两套规则。

针对反编译 Java 代码的 API 规则

rules/mastg-android-data-unencrypted-shared-storage-no-user-interaction-apis.yml 定义了三条子规则,覆盖三类外部存储访问模式:

子规则 ID命中模式风险描述
...-external-api-publicgetExternalStorageDirectory(...)、getExternalStoragePublicDirectory(...)、getDownloadCacheDirectory(...)、Intent.ACTION_CREATE_DOCUMENT返回共享外部存储位置,其他应用可访问
...-external-api-scopedgetExternalFilesDir(...)、getExternalFilesDirs(...)、getExternalCacheDir(...)、getExternalCacheDirs(...)、getExternalMediaDirs(...)返回作用域外部存储位置,Android 10 及以下在具备相关权限时可能被其他应用访问
...-mediastoreimport android.provider.MediaStore、$X.MediaStore通过 MediaStore API 写入外部存储,数据可被其他应用访问

该规则文件使用 Semgrep 的pattern-either语法,每条子规则都带有severity: WARNING和message元信息。第一类external-api-public的 message 为"[MASVS-STORAGE] Make sure to encrypt files at these locations if necessary",即命中后提示开发者"如有必要请加密这些位置的文件"。

针对 Manifest 的权限规则

rules/mastg-android-data-unencrypted-shared-storage-no-user-interaction-manifest.yml 使用generic语言对清单文件做模式匹配,命中以下任一存储相关声明即告警:

pattern-either: - pattern: WRITE_EXTERNAL_STORAGE - pattern: MANAGE_EXTERNAL_STORAGE - pattern: ACCESS_ALL_EXTERNAL_STORAGE - pattern: requestLegacyExternalStorage="true" - pattern: preserveLegacyExternalStorage="true" - pattern: android:requestRawExternalStorageAccess="true"

这套规则覆盖了 Scoped Storage 时代所有"绕过"或"降级"手段:

  • WRITE_EXTERNAL_STORAGE:Android 10 之前的旧权限,在 Android 11+ 上已失去授予意义(系统对共享存储默认只读),但遗留代码中仍可能出现;
  • MANAGE_EXTERNAL_STORAGE:Android 11 引入的"All files access"特殊权限,本文示例正是命中此项;
  • ACCESS_ALL_EXTERNAL_STORAGE:面向 TV 等特殊设备的全部文件访问权限;
  • requestLegacyExternalStorage="true":Android 10 的清单属性,允许应用暂时沿用旧存储模型(Android 11 起被忽略);
  • preserveLegacyExternalStorage="true":应用更新时保留旧存储模型的属性;
  • android:requestRawExternalStorageAccess="true":针对 Android 14(API 34)新增的原始文件访问。

Steps:运行 Semgrep 规则扫描

仓库为演示提供了开箱即用的执行脚本 run.sh,其中两条命令分别对反编译产物执行两套规则:

NO_COLOR=true semgrep -c ../../../../rules/mastg-android-data-unencrypted-shared-storage-no-user-interaction-apis.yml ./MastgTest_reversed.java > output.txt NO_COLOR=true semgrep -c ../../../../rules/mastg-android-data-unencrypted-shared-storage-no-user-interaction-manifest.yml ./AndroidManifest_reversed.xml > output2.txt

命令要点:

  • -c(--config):指定规则文件路径。此处使用相对路径指向仓库根目录下的 rules 目录,在实际项目中也可替换为 Semgrep Registry 的规则 ID 或远程配置 URL;
  • 目标文件:直接扫描反编译产物MastgTest_reversed.java与AndroidManifest_reversed.xml,而非原始源码,真实还原了"无源码审计"场景;
  • NO_COLOR=true:禁用彩色输出,便于将结果重定向到文件、接入 CI 或供后续脚本解析;
  • 输出重定向:分别写入output.txt与output2.txt。

运行前提是本地已安装 Semgrep(pip install semgrep或参考 MASTG-TOOL-0110 的官方来源安装)。规则采用标准 Semgrep YAML 格式,因此也完全兼容semgrep scan --config目录级批量扫描——仓库 rules 目录下的全部规则可直接作为一个扫描集使用。

Observation:规则输出解读

运行脚本后得到两份输出。第一份 output.txt 来自 API 规则,命中 1 处代码位置:

┌────────────────┐ │ 1 Code Finding │ └────────────────┘ MastgTest_reversed.java ❯❱ rules.mastg-android-data-unencrypted-shared-storage-no-user-interaction-external-api-public [MASVS-STORAGE] Make sure to encrypt files at these locations if necessary 27┆ File externalStorageDir = Environment.getExternalStorageDirectory();

输出清晰标注了命中规则 ID(...-external-api-public)、告警消息以及精确的源码位置(文件MastgTest_reversed.java第 27 行),这正是后续人工评估的锚点。

第二份 output2.txt 来自 Manifest 规则,同样命中 1 处:

┌────────────────┐ │ 1 Code Finding │ └────────────────┘ AndroidManifest_reversed.xml ❯❱ rules.mastg-android-data-unencrypted-shared-storage-no-user-interaction-manifest [MASVS-STORAGE] Make sure to encrypt files in external storage if necessary 2┆ <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>

两处命中恰好一一对应漏洞的两个必要条件:

  1. 代码中调用了Environment.getExternalStorageDirectory()(第 27 行),具备向共享存储写文件的"能力";
  2. 清单中声明了MANAGE_EXTERNAL_STORAGE(第 2 行),为上述调用提供了"授权"。

这印证了规则设计的成对思路:仅查 API 会漏掉"有能力但未授权"或"已授权未使用"的场景,仅查权限又无法定位具体写入代码,因此 MASTG-TEST-0202 将两者结合,再辅以动态测试补足"实际写入的路径与内容"信息(静态分析无法提供运行时写入的真实数据)。

Evaluation:评估与判定

按照 MASTG-TEST-0202 的评估标准,测试失败需要同时满足三个条件:

  1. 应用在 Android 清单中声明了相关权限(如WRITE_EXTERNAL_STORAGE、MANAGE_EXTERNAL_STORAGE等)——✅ 已命中;
  2. 应用使用了写入共享存储的 API(如getExternalStoragePublicDirectory、getExternalStorageDirectory、getExternalFilesDir、getExternalCacheDir、MediaStore等)——✅ 已命中;
  3. 写入共享存储的数据为敏感数据且未加密——需人工验证。

第三点无法由静态规则自动判定,需要按照 MASTG-TECH-0023(静态分析的人工复核)审查输出中报告的位置(文件与行号)。对MastgTest_reversed.java第 27 行附近的代码进行复核后可以看到,mastgTest()方法将"Secret not using scoped storage"明文写入secret.txt,且未做任何加密处理——该内容在本示例中即为敏感数据(真实场景可能是密码、令牌或个人信息)的替身。

由此可以得出结论:该测试失败。应用绕过 Scoped Storage 限制,将敏感数据未加密写入共享外部存储,任何具备存储读取权限的应用均可读取,违反 MASVS-STORAGE 相关要求。

延伸:如何修复与加固

结合规则与测试文档的提示,针对此类漏洞的缓解方向包括:

  • 改用作用域存储 API:将Environment.getExternalStorageDirectory()替换为context.getExternalFilesDir(...)(应用专属外部目录)或MediaStore(媒体文件),并移除MANAGE_EXTERNAL_STORAGE权限声明;
  • 加密敏感数据:若确需写入共享存储,使用 Android Keystore 派生的密钥对数据进行加密(参考仓库 MASVS-CRYPTO 与 MASVS-STORAGE 下的其他演示用例);
  • 彻底移除清单中的冗余声明:删除requestLegacyExternalStorage、preserveLegacyExternalStorage等历史兼容属性,避免旧存储模型回退;
  • 常态化扫描:将 rules 目录下的 Semgrep 规则接入 CI 流水线,对每次构建产物(含反编译产物)自动执行semgrep --config扫描,使此类问题在发布前暴露。

总结

MASTG-DEMO-0003完整展示了一次针对"外部存储未加密写入"弱点的端到端静态分析:从 MastgTest.kt 的漏洞代码与 AndroidManifest.xml 的权限声明,到反编译产物 MastgTest_reversed.java 与 AndroidManifest_reversed.xml,再到两套 Semgrep 规则与 run.sh 的精确命中,最终依据 MASTG-TEST-0202 的判定标准得出失败结论。

这套"反编译 → 规则扫描 → 人工复核"的流程可以直接迁移到真实 App 的安全测试中:在无源码的情况下,仅凭 APK 即可通过仓库 rules 下的规则快速定位外部存储相关风险点,再结合动态测试(如 MASTG-TEST-0001 家族用例)验证实际写入的数据与路径,从而对 MASWE-0002 类弱点给出有据可依的评估结论。

  • 文档
  • 教程
  • 网络安全

【免费下载链接】mastg

The OWASP Mobile Application Security Testing Guide (MASTG) is a comprehensive manual for mobile app security testing and reverse engineering. It describes technical processes for verifying the OWASP Mobile Security Weakness Enumeration (MASWE) weaknesses, which are in alignment with the OWASP MASVS.

项目地址:https://gitcode.com/gh_mirrors/ow/mastg
点击查看免费下载

相关推荐

上一篇:3分钟实现手机号码精准定位:location-to-phone-number开源项目技术深度解析
下一篇:Syft SBOM 生成教程:3 条命令摸清软件物料清单

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 2:15:32

AI Agent 面试题 169:Agent的缓存策略如何帮助减少重复的LLM调用?

&#x1f525; AI Agent 面试题 169&#xff1a;Agent的缓存策略如何帮助减少重复的LLM调用&#xff1f;摘要&#xff1a;本文深入解析了「Agent的缓存策略如何帮助减少重复的LLM调用&#xff1f;」这一 AI Agent 领域的核心面试题。文章从 Token 优化策略 的基本概念出发&…

作者头像 李华
网站建设 2026/10/6 2:07:58

5分钟上手TileLang:GPU内核开发指南

5分钟上手TileLang&#xff1a;GPU内核开发指南 【免费下载链接】tilelang Domain-specific language designed to streamline the development of high-performance GPU/CPU/Accelerators kernels 项目地址: https://gitcode.com/GitHub_Trending/ti/tilelang 手写一个…

作者头像 李华
网站建设 2026/10/6 2:04:10

Yup 类型校验错误消息自定义:typeError() 用法详解

文档教程知识库 【免费下载链接】til :memo: Today I Learned 项目地址&#xff1a; https://gitcode.com/gh_mirrors/ti/til 点击查看 免费下载 导读 在基于 Yup 构建表单校验时&#xff0c;类型不匹配的默认报错往往冗长且面向开发者而非用户。本篇以 til 仓库中 Custom Ty…

作者头像 李华
网站建设 2026/10/6 2:03:47

vue-devui Anchor 锚点组件实战指南:指令式页面内跳转与滚动激活

前端UI组件设计系统 【免费下载链接】vue-devui 基于全新 DevUI Design 设计体系的 Vue3 组件库&#xff0c;面向研发工具的开源前端解决方案。 项目地址&#xff1a; https://gitcode.com/DevCloudFE/vue-devui 点击查看 免费下载 在长文档、帮助中心或研发工具等需要「页内快…

作者头像 李华