2026年2月2日 【Androidセキュリティ】InMemoryDexClassLoaderを用いた動的計装に関する概念実証について Android セキュリティ 検索する Popular tags 生成AI(Generative AI) Vertex AI Search Looker Studio BigQuery AlloyDB Google Workspace 事例紹介 Cloud SQL Category モバイル Author はる SHARE 目次 動的計装(Dynamic Instrumentation)とは? InMemoryDexClassLoaderについて 実際にやってみよう まとめ Content こんにちは、はるです! 今回はタイトルの通り動的計装の概念実証について解説していきます! 念のためですが、この記事は技術研究を目的とするものであり、決して違法な攻撃を助長するものではありません。 悪用厳禁です。(といっても悪用は難しいと思いますが...) ⭐️この記事でわかること ・動的計装とは何か ・なぜInMemoryDexClassLoaderが優秀なのか ・アプリ実行中に任意のコードを差し替える方法 動的計装(Dynamic Instrumentation)とは? プログラムの実行中に実行コードの変更や追加を行う技術を意味します。 主にサイバーセキュリティやデバッグの文脈で用いられることが多いです。 InMemoryDexClassLoaderについて ⭐️InMemoryDexClassLoaderとは? 動的計装を行うためのAndroid公式提供クラス(API level 26+必須)です。 本クラスはdexファイル(後述します)と組み合わせて利用します。 また、本クラスは前身であるDexClassLoaderの弱点を補ったクラスでもあります。(後述します) ⭐️dexとは? dexはDalvik Executableの略称です。 ART(Android Runtime)の前身であるDalvik VMから用いられているファイル形式です。 普段我々が実装しているKotlinコードなどは最終的にこのdexファイルに変換されます。 apkの中身を分解してみればdexが見えるでしょう。(今回は省略します) 今回は後から流し込みたいコード(payload)をこのdexファイルに変換し、それをInMemoryDexClassLoaderを通じて読み込ませます。 ⭐️InMemoryDexClassLoaderとDexClassLoaderの違い DexClassLoaderはInMemoryDexClassLoaderの前身です。 名前は酷似していますが、これらはある観点で非常に大きな違いを持っています。 それは”ストレージを圧迫するかどうか”です。 DexClassLoaderでは読み取ったdexを最適化し、その結果をファイルストレージへ書き込みます。 そのため、プログラムを実行する度にファイルが増えてしまいストレージを圧迫してしまいます…。 そこで、登場したのがInMemoryDexClassLoaderです。 これは名前の通りメモリ上で完結し、ストレージを圧迫しません。 実際にやってみよう ⭐️全体像 1.ペイロードコードを実装 2.dexファイル作成 3.assetsへ配置 4.ローダー実装 これが大まかな流れです。 ⭐️ペイロードコードを実装 1.下記画像のようにappフォルダを右クリックしてNew->Moduleを押下 2.Android Libraryを選択し、モジュール名を決めてFinishを押下 3.モジュールが生成されるのでmain->java->内にPayload.ktを作成し下記のようなコードを実装する object PayloadObject { @JvmStatic fun execute(): String { return "World!!" } } ここで、@JvmStaticを必ず付与してください。 これを付与すると、dexを読み込んだ後executeメソッドを実行する時にPayloadObjectのインスタンスを経由せず静的関数として呼ぶことができます。 結果、executeメソッドを呼ぶ際にインスタンス作成の手間が省けるためコード量削減&パフォーマンス向上が見込めます。 一方、シングルトンなのだからインスタンスはそもそも必要ないのでは?と思われる方もいらっしゃると思います。 確かにその通りなのですが、その場合でも@JvmStaticが付与されていないと、呼び出すために外部ライブラリが必要になったりとやや手間がかかってしまうのです。(詳細は長くなるので今回は割愛します) ⭐️dexファイル作成 & assetsへ配置 build.gradle.kts(app)へ下記の実装を行う // ペイロードモジュールのビルド完了まで待機 evaluationDependsOn(":app:<ペイロードモジュール名>") val payloadProject = project(":app:<ペイロードモジュール名>") val jarTaskProvider = payloadProject.tasks.named("jar") // ... android { // 色々設定... val sdkDir = sdkDirectory val btVersion = buildToolsVersion // dexファイル生成 val buildPayloadDex = tasks.register("buildPayloadDex") { dependsOn(jarTaskProvider) val inputJar = jarTaskProvider.get().archiveFile.get().asFile val outputDir = layout.buildDirectory.dir("payload-dex").get().asFile val d8Name = if (org.gradle.internal.os.OperatingSystem.current().isWindows) "d8.bat" else "d8" val d8Path = File(sdkDir, "build-tools/$btVersion/$d8Name") doFirst { outputDir.mkdirs() } commandLine(d8Path.absolutePath, "--output", outputDir.absolutePath, inputJar.absolutePath) } // assetsフォルダへの配置 val copyDexToAssets = tasks.register("copyDexToAssets") { dependsOn(buildPayloadDex) from(layout.buildDirectory.file("payload-dex/classes.dex")) into("src/main/assets") rename { "payload.dex" } } // preBuild に紐付け tasks.named("preBuild") { dependsOn(copyDexToAssets) } } 上記で行っていることは簡潔に言えば、 ・payloadモジュールのビルドを待機 ・dexファイルを生成 ・assetsフォルダへdexを配置 になります。 ⭐️ローダー実装 UI面は雑ですがこんな感じで…。 class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { PawnTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Hoge(modifier = Modifier.padding(innerPadding)) } } } } } @Composable fun Hoge(modifier: Modifier = Modifier) { val viewModel: MainActivityViewModel = viewModel() Column( modifier = modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { Text(viewModel.str.value) Button(onClick = {viewModel.onClickInjectButton()}) { Text("Inject!") } } } テキストとボタンが中央に配置されており、 ボタンを押すとペイロードによりテキストの内容がHelloからWorldへ書き換えられるイメージですね。 インジェクト含めたViewModelの処理がこちら class MainActivityViewModel( application: Application ): AndroidViewModel(application) { private val context: Context get() = getApplication().applicationContext var str = mutableStateOf("Hello") fun onClickInjectButton() { val dexBytes = context.assets.open("payload.dex").use { it.readBytes() } str.value = InMemoryDexPayloadLoader( context = context, dexBytes = dexBytes, dstClassName = "com.payload.PayloadObject", dstMethodName = "execute" ).execute() } } 後述しますが、InMemoryDexPayloadLoaderは別で作った抽象化クラスです。 ここで指定しているのは下記の3点です。 ・dexファイルの中身(Byte) ・ペイロードのクラス名 ・当該クラス内で実行したいメソッド名 肝心のInMemoryDexPayloadLoaderはこちら /** * InMemoryDexClassLoaderを用いた動的ペイロード実行クラス * @param context コンテキスト * @param dexBytes 実行するペイロードのDexバイト * @param dstClassName 実行するクラス名 * @param dstMethodName 実行するメソッド名 */ class InMemoryDexPayloadLoader( private val context: Context, private val dexBytes: ByteArray, private val dstClassName: String, private val dstMethodName: String ) { companion object { private const val TAG = "PayloadLoader" } /** * ペイロード実行 */ // 執筆サイトの不具合だと思われますが、しっかりfunの右にRの定義を付与してあげてください fun execute(): R { try { val loader = makeInMemoryDexClassLoader() val clazz = loader.loadClass(dstClassName) val method = clazz.getMethod(dstMethodName) @Suppress("UNCHECKED_CAST") return method.invoke(null) as R } catch (e: Throwable) { when (e) { is ClassNotFoundException -> { Log.e(TAG, "Class not found: $dstClassName", e) throw PayloadExecutionException("Class not found", e) } is NoSuchMethodException -> { Log.e(TAG, "Method not found: $dstMethodName", e) throw PayloadExecutionException("Method not found", e) } else -> { Log.e(TAG, "Execution failed", e) throw PayloadExecutionException("Execution failed", e) } } } } /** * InMemoryDexClassLoaderを作成 */ private fun makeInMemoryDexClassLoader(): InMemoryDexClassLoader { return InMemoryDexClassLoader( makeByteBuffer(), context.classLoader ) } /** * ByteBufferを作成 */ private fun makeByteBuffer(): ByteBuffer { return ByteBuffer.wrap(dexBytes) } } /** * ペイロード実行時の例外 */ class PayloadExecutionException( message: String, cause: Throwable? = null ) : Exception(message, cause) 本クラスが行っていることを箇条書きにすると ・Android公式のInMemoryDexClassLoaderクラスを利用 ・dexファイルから指定されたクラスとメソッドを取得 ・クラスまたはメソッドが見つからない場合は例外返却 ・メソッドまで取得できた場合はinvokeして結果を返却 今回のInMemoryDexPayloadLoaderのように、 InMemoryDexClassLoader の複雑な初期化処理を抽象化し、必要な情報(バイト配列やクラス名)だけを外部から注入する設計にすることで、利用側のコードをよりクリーンに保つことができます。 ⭐️結果 デフォルトは下記の状態 Injectボタンを押下して… こうなれば成功!! ボタンを押下することで、dexファイルが読み込まれ、「ペイロードコードを実装」で用意したexecuteメソッドの返り値(“World!!”)を取得してセットしています。 まとめ いかがでしたでしょうか。 InMemoryDexClassLoader を活用することで、ストレージを汚さずに実行時コードの拡張ができることがお分かりいただけたかと思います。 本来、こうした技術はアプリの「プラグイン機能」の動的な配信や、特定の環境下でのみ動作させたいデバッグツールのインジェクトなど、柔軟な設計を実現するための強力な武器になります。OSの深いレイヤーに触れるような実装は、Androidの仕組みを理解する上でも非常に良い題材ですので、ぜひ皆さんも「型にハマらない実装」の第一歩として試してみてください!! 関連コンテンツ 【Android – メモリ管理①】メモリ管理の全体像を理解する by はるon 2025年12月3日 【Android – メモリ管理②】GCの仕組みについて by はるon 2025年12月4日 【iOS】大規模フルスタック個人アプリ、4度のリジェクトを超えてリリースした話 by はるon 2025年10月29日 頂きましたご意見につきましては、今後のより良い商品開発・サービス改善に活かしていきたいと考えております。 非常に良かった とても良かった どちらでもない あまり良くなかった 非常に良くなかった Author はる 入社日:2023年12月 職種:Androidネイティブアプリエンジニア チェスが趣味 Android セキュリティ 2026年2月2日 【Androidセキュリティ】InMemoryDexClassLoaderを用いた動的計装に関する概念実証について Category モバイル 前の記事を読む 【2026年最新版】Looker StudioとLooker Studio Proの違いとは?おすすめはどっち?『BIツール初心者』が徹底比較&徹底解説してみた! Recommendation オススメ記事 2023年9月5日 Google Cloud 【Google Cloud】Looker Studio × Looker Studio Pro × Looker を徹底比較!機能・選び方を解説 2023年8月24日 Google Cloud 【Google Cloud】Migrate for Anthos and GKEでVMを移行してみた(1:概要編) 2022年10月10日 Google Cloud 【Google Cloud】AlloyDB と Cloud SQL を徹底比較してみた!!(第1回:AlloyDB の概要、性能検証編) BigQuery ML ワークショップ開催のお知らせ 生成AI導入支援パッケージ Discovery AI導入支援パッケージ Google Cloud ホワイトペーパー 新着記事 2026年2月2日 モバイル 【Androidセキュリティ】InMemoryDexClassLoaderを用いた動的計装に関する概念実証について 2026年1月20日 Google Cloud 【2026年最新版】Looker StudioとLooker Studio Proの違いとは?おすすめはどっち?『BIツール初心者』が徹底比較&徹底解説してみた! 2026年1月20日 技術開発 【UI/UXとは?】UI/UXの違いやデザインのポイントについて解説! HOME モバイル 【Androidセキュリティ】InMemoryDexClassLoaderを用いた動的計装に関する概念実証について