0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Androidセキュリティ】InMemoryDexClassLoaderを用いた動的計装についてPoCコード書いてみた

0
Last updated at Posted at 2026-01-12

はじめに

この記事は、技術研究を目的とするものであり、決して違法な攻撃を助長するものではありません。
悪用厳禁です。(といっても悪用は難しいと思いますが...)

この記事で分かること

  • 動的計装とは何か
  • なぜInMemoryDexClassLoaderが優秀なのか
  • アプリ実行中に任意のコードを差し替える方法

動的計装(Dynamic Instrumentation)とは?

プログラムの実行中に実行コードの変更や追加を行う技術を意味します。
主にサイバーセキュリティやデバッグの文脈で用いられることが多いです。

InMemoryDexClassLoaderについて

InMemoryDexClassLoaderとは?

本クラスを利用するにはAPI 26以上である必要があります。

動的計装を行うためのAndroid公式提供クラスです。
本クラスはdexファイル(後述します)と組み合わせて利用します。
また、本クラスは前身であるDexClassLoaderの弱点を補ったクラスでもあります。(後述します)

dexとは?

dexはDalvik Executableの略称です。
ART(AndroidRunTime)の前身であるDalvik VMから用いられているファイル形式です。
普段我々が実装しているKotlinコードなどは最終的にこのdexファイルに変換されます。
apkの中身を分解してみればdexが見えるでしょう。(今回は省略します)

今回は後から流し込みたいコード(payload)をこのdexファイルに変換し、それをInMemoryDexClassLoaderを通じて読み込ませます。

InMemoryDexClassLoaderとDexClassLoaderの違い

DexClassLoaderはInMemoryDexClassLoaderの前身です。
名前は酷似していますが、これらはある観点で非常に大きな違いを持っています。
それは"ストレージを圧迫するかどうか"です。

DexClassLoaderでは読み取ったdexを最適化し、その結果をファイルストレージへ書き込みます。
そのため、プログラムを実行する度にファイルが増えてしまいストレージを圧迫してしまいます...。

そこで、登場したのがInMemoryDexClassLoaderです。
これは名前の通りメモリ上で完結し、ストレージを圧迫しません。

概念実証開始

全体像

svgviewer-png-output.png

  1. ペイロードコードを実装
  2. dexファイル作成
  3. アセットへ配置
  4. ローダー実装

これが大まかな流れです。

ペイロードコードを実装

1.下記画像のようにappフォルダを右クリックしてNew->Moduleを押下
スクリーンショット 2026-01-07 23.22.26.png
2.Android Libraryを選択し、モジュール名を決めてFinishを押下
スクリーンショット 2026-01-07 23.25.46.png
3.モジュールが生成されるのでmain->java->内にPayload.ktを作成し下記のようなコードを実装する

object PayloadObject {
    @JvmStatic
    fun execute(): String {
        return "World!!"
    }
}

ここで、@JvmStaticを必ず付与してください。
これを付与することで、dexを読み込んだ後executeメソッドを実行する時にPayloadObject及びインスタンスを経由せず静的関数として呼ぶことができます。

dexファイル作成 & assetsへ配置

build.gradle.kts(app)へ下記の実装を行う

// ペイロードモジュールのビルド完了まで待機
evaluationDependsOn(":app:<ペイロードモジュール名>")
val payloadProject = project(":app:<ペイロードモジュール名>")
val jarTaskProvider = payloadProject.tasks.named<Jar>("jar")
// ...
android {
  // 色々設定...
  val sdkDir = sdkDirectory
  val btVersion = buildToolsVersion
  // dexファイル生成
  val buildPayloadDex = tasks.register<Exec>("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<Copy>("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<Application>().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> 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 の複雑な初期化処理を抽象化し、必要な情報(バイト配列やクラス名)だけを外部から注入する設計にすることで、利用側のコードをよりクリーンに保つことができます。

結果

Before After

こうなればOKです!
ボタンを押下することで、dexファイルが読み込まれ、「ペイロードコードを実装」で用意したexecuteメソッドの返り値("World!!")を取得してセットしています。

まとめ

いかがでしたでしょうか。
InMemoryDexClassLoader を活用することで、ストレージを汚さずに実行時コードの拡張ができることがお分かりいただけたかと思います。

本来、こうした技術はアプリの「プラグイン機能」の動的な配信や、特定の環境下でのみ動作させたいデバッグツールのインジェクトなど、柔軟な設計を実現するための強力な武器になります。OSの深いレイヤーに触れるような実装は、Androidの仕組みを理解する上でも非常に良い題材ですので、ぜひ皆さんも「型にハマらない実装」の第一歩として試してみてください!!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?