はじめに
この記事は、技術研究を目的とするものであり、決して違法な攻撃を助長するものではありません。
悪用厳禁です。(といっても悪用は難しいと思いますが...)
この記事で分かること
- 動的計装とは何か
- なぜ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です。
これは名前の通りメモリ上で完結し、ストレージを圧迫しません。
概念実証開始
全体像
- ペイロードコードを実装
- dexファイル作成
- アセットへ配置
- ローダー実装
これが大まかな流れです。
ペイロードコードを実装
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及びインスタンスを経由せず静的関数として呼ぶことができます。
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の仕組みを理解する上でも非常に良い題材ですので、ぜひ皆さんも「型にハマらない実装」の第一歩として試してみてください!!


