2026年2月2日

【Androidセキュリティ】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の仕組みを理解する上でも非常に良い題材ですので、ぜひ皆さんも「型にハマらない実装」の第一歩として試してみてください!!

2026年2月2日 【Androidセキュリティ】InMemoryDexClassLoaderを用いた動的計装に関する概念実証について

Category モバイル

ご意見・ご相談・料金のお見積もりなど、
お気軽にお問い合わせください。

お問い合わせはこちら