DalvikFLIRTとLLMによるAndroidの難読化解除
本研究では、シグネチャベースのマッチング(DalvikFLIRT)とLLMによるコード変換を組み合わせた先駆的な二重のアプローチを紹介します。高度なAndroidアプリの難読化を回避し、これまで解読できなかったコードの自動セキュリティ分析を可能にします。
1. Androidアプリの難読化を理解する
Androidのエコシステムにおいて、難読化はアプリケーション開発者にとって重要な防御の仕組みであり、知的財産や機密性の高いコードをリバースエンジニアリングの試みから保護します。リバースエンジニアリングを難しくすることで、気軽に手を出す攻撃者に対する障壁となります。
難読化は堅牢化の手段である一方、脆弱性を特定しようとするセキュリティ研究者にとっては大きな課題を生みます。当社はOstorlabの自動化を、技術的な脆弱性の研究にとどまらず、論理的な脆弱性の発見の自動化へと推し進めていることから、この問題を調査することにしました。
難読化は、機能を維持したまま、明確で読みやすいコードを意図的に複雑で不可解なものへと変換します。開発者がコードを難読化するとき、本質的には構文上の正しさを保ちながら意味的な情報を取り除いています。コードは依然として動作しますが、理解することはほぼ不可能になります。
次の簡単な例を見てみましょう。
// Original code
public void validateUserCredentials(String username, String password) {
if (isValidUsername(username) && isCorrectPassword(password)) {
grantAccess();
} else {
denyAccess();
}
}
// Obfuscated code
public void a(String b, String c) {
if (d(b) && e(c)) {
f();
} else {
g();
}
}
機能的には同一ですが、難読化されたバージョンはコードの目的とロジックを覆い隠しており、セキュリティ分析の際に理解するのが困難になります。
現代のAndroidの難読化には、いくつかの手法とツールが関わっています。
- ProGuardは従来からあるオープンソースのツールで、長年にわたってAndroidのビルドに組み込まれ、基本的な名前の難読化とコードの縮小を提供してきました。
- R8は、GoogleによるProGuardの現代的な後継であり、強化された最適化機能とAndroidのビルドシステムとのシームレスな統合を備え、現在ではAndroid Studioのデフォルトのコンパイラーとなっています。
より強力な保護を必要とするアプリケーションでは、文字列の暗号化、制御フローの難読化、アンチデバッグの対策など、より高度な機能を商用ツールが提供している場合があります。
2. Androidエコシステムにおける難読化の普及状況
Androidアプリケーションにおける難読化の利用は広く普及しており、特に価値の高いアプリケーションや機密データを扱うアプリケーションの間で拡大し続けています。
「A Large Scale Investigation of Obfuscation Use in Google Play」(2018年)の研究では、全Androidアプリの約25%が何らかの形の難読化を採用していることがわかりました。この研究は170万本の無料Androidアプリを分析し、最も人気のあるアプリケーションだけに絞ると、この数字が約50%に跳ね上がることを明らかにしました。
より新しい論文(2025年2月、「An Empirical Study of Code Obfuscation Practices in Android Apps」)は、2016年から2023年にかけて難読化の採用が13%増加し、特にトップ開発者の間ではその期間に28%という大きな増加があったことを記録しています。当社の分析では、難読化の採用が大幅に増加しており、テストしたアプリケーションの約62%に及んでいることが示されています。
難読化の普及度は、アプリケーションのカテゴリによって大きく異なります。金融・銀行アプリではほぼ全面的に採用されており、ゲームアプリは収益化のアルゴリズムやチート対策の仕組みを守るために強力な難読化を頻繁に用い、エンタープライズアプリは独自のビジネスロジックを守るために中程度の保護戦略を実装しています。ソーシャルメディアのプラットフォームは、ユーザーデータの処理の仕組みを守るため、高度な難読化を採用する傾向を強めています。
ProGuard/R8は、Android Studioとの統合と無償であることから、依然として最も広く使われているソリューションです。しかし、価値の高いアプリケーションでは商用ソリューションが強い存在感を示しています。金融サービスやヘルスケアのアプリケーションなど、特に機密性の高いデータを扱うアプリでは、独自の難読化ソリューションがますます一般的になっています。
3. 難読化がセキュリティ分析に与える影響
難読化は、手動と自動の両方のセキュリティ分析に大きな課題をもたらし、興味深いセキュリティ上のパラドックスを生み出します。
コードを調べる人間のアナリストにとって、難読化は通常であれば理解の助けとなる文脈上の手がかりをすべて取り除いてしまいます。意味のある名前がなければ、研究者は各関数が何をするのかを理解するために、実行経路を丹念にたどらなければなりません。銀行アプリの認証システムを考えてみてください。通常であればverifyUserCredentials()として識別できるはずのものが、単なるLIZIZI()になり、セキュリティ上の重要性が隠されてしまいます。
制御フローは、余分なジャンプ、条件分岐、不要な間接参照によって意図的に複雑にされています。機能についてのヒントになり得る文字列定数は暗号化され、コンポーネント間の関係を見極めることはほぼ不可能になります。
難読化されていないコードであれば数時間で済む認証フローの解読に、セキュリティ研究者が数週間とまではいかなくとも数日を費やすこともあります。
自動分析ツールは、さらに深刻な影響を受けます。テイント解析システムは、データが機密性の高いソース(ユーザー入力など)から危険なシンク(SQLクエリなど)へどのように流れるかを追跡します。SQLインジェクションを調べる場合、ツールは通常、executeQuery()のようなメソッドを潜在的なシンクとして特定します。難読化されると、これらのメソッドは識別できなくなります。LIIZZ()は、ログ出力からデータベースアクセスまで、何であってもおかしくありません。これは、こうしたツールの中核となる仕組みを根本から壊してしまいます。
たとえば、セキュリティ分析でよく使われるパターンの一つに、機密性の高いシステムリソースにアクセスするメソッドの特定があります。位置情報にアクセスする次のコードを見てみましょう。
// Original code
LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
Location location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
// Obfuscated version
Object a = b(c.d);
Object e = ((f) a).g(f.h);
位置情報へのアクセスパターンを探す自動ツールは、難読化されたバージョンを完全に見逃し、プライバシーの問題を見落とすおそれがあります。
同様に、ファジングツールは、どの入力がセキュリティ上重要な経路に影響するかを理解する必要があります。難読化はこうした関係を覆い隠します。既知の脆弱性パターンと照合する静的解析システムは、そのパターンが難読化によって変更されると機能しなくなります。
これは、パラドックス的なセキュリティの状況を生み出します。難読化は、リソースの限られた気軽な攻撃者を効果的に退けますが、正当なセキュリティ分析も妨げます。組織は、難読化が脆弱性を単に隠しているだけなのに、脆弱性を取り除いたと誤解してしまうことがあります。セキュリティ監査は大幅に高コストかつ時間のかかるものになり、ときには企業が監査の頻度や範囲を縮小することにつながります。
4. DalvikFLIRTで難読化を打ち破る
4.1 FLIRT技術を理解する
FLIRT(Fast Library Identification and Recognition Technology)は、もともとIDA Proのようなツールでネイティブバイナリの解析のために開発された、コード解析への斬新なアプローチです。FLIRTの核心は、名前に関係なく、コンパイル済みのコードの中から標準的な関数を識別できるようにすることにあります。

この技術は、関数名のような容易に変更できる識別子に頼るのではなく、コードのパターンに基づいて特徴的なシグネチャを作成します。これらのシグネチャは関数の本質的な構造と振る舞いを捉えるため、基本的な難読化手法に対して耐性があります。
従来、FLIRTは主にネイティブコード(C/C++)の解析に使われ、コンパイル済みバイナリの中から標準ライブラリの関数を識別するのに役立ってきました。FLIRTのシグネチャは各関数の複数の特徴的な性質をカプセル化し、名前や単純なコードパターンが変更されても識別可能なままのフィンガープリントを作り出します。
Original Function | Obfuscated Function
---------------- | -------------------
Function name: strcmp | Function name: fn_0x3A72
Signature: [Pattern of bytes/opcodes] | Signature: [Identical pattern]
Parameters: 2 string pointers | Parameters: 2 string pointers
Structure: Loop comparing characters | Structure: Loop comparing characters
Return: Integer (0 if equal) | Return: Integer (0 if equal)
4.2 シグネチャベースの識別に役立つDalvikの特性
Androidアプリケーションが使用する形式であるDalvikバイトコードは、従来のバイナリ解析とは異なる、シグネチャベースの識別のための独自の機会を提供します。強力な名前の難読化を施した後でも、メソッドとフィールドの間の構造的な関係は通常そのまま残ります。それらを変更すると機能が壊れてしまうからです。
メソッドのシグネチャ(パラメーターの型と戻り値)は、Androidフレームワークや他のコンポーネントとの互換性を維持するために保持されなければなりません。アクセス修飾子(public、private、protected)は一般にアプリケーションの振る舞いを変えずに変更することはできず、継承関係も機能を維持するために変更されないことがよくあります。
場合によっては、ソースファイル名や行番号といったデバッグ情報が残っていることもあり、識別のための追加の手がかりとなります。最も重要なのは、AndroidフレームワークのAPIの呼び出しが一貫したままでなければならないことです。これにより、解析の拠り所となる識別可能なパターンが生まれます。
こうした持続的な特性によって、Dalvik環境に特化したFLIRT的なシグネチャマッチングの機会が生まれます。
###### Class android.support.v7.internal.app.AppCompatViewInflater (android.support.v7.internal.app.AppCompatViewInflater)
.class public Landroid/support/v7/internal/app/AppCompatViewInflater;
.super Ljava/lang/Object;
.source "AppCompatViewInflater.java"
# static fields
.field private static final LOG_TAG:Ljava/lang/String; = "AppCompatViewInflater"
.field private static final sConstructorMap:Ljava/util/Map;
.annotation system Ldalvik/annotation/Signature;
value = {
"Ljava/util/Map",
"<",
"Ljava/lang/String;",
"Ljava/lang/reflect/Constructor",
"<+",
"Landroid/view/View;",
">;>;"
}
.end annotation
.end field
.field static final sConstructorSignature:[Ljava/lang/Class;
.annotation system Ldalvik/annotation/Signature;
value = {
"[",
"Ljava/lang/Class",
"<*>;"
}
.end annotation
.end field
# instance fields
.field private final mConstructorArgs:[Ljava/lang/Object;
.method private createView(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;)Landroid/view/View;
.registers 9
.param p1, "context" # Landroid/content/Context;
.param p2, "name" # Ljava/lang/String;
.param p3, "prefix" # Ljava/lang/String;
.annotation system Ldalvik/annotation/Throws;
value = {
Ljava/lang/ClassNotFoundException;,
Landroid/view/InflateException;
}
.end annotation
.prologue
.line 143
sget-object v3, Landroid/support/v7/internal/app/AppCompatViewInflater;->sConstructorMap:Ljava/util/Map;
invoke-interface {v3, p2}, Ljava/util/Map;->get(Ljava/lang/Object;)Ljava/lang/Object;
move-result-object v1
check-cast v1, Ljava/lang/reflect/Constructor;
.line 146
.local v1, "constructor":Ljava/lang/reflect/Constructor;, "Ljava/lang/reflect/Constructor<+Landroid/view/View;>;"
if-nez v1, :cond_36
.line 148
:try_start_a
invoke-virtual {p1}, Landroid/content/Context;->getClassLoader()Ljava/lang/ClassLoader;
move-result-object v4
if-eqz p3, :cond_43
new-instance v3, Ljava/lang/StringBuilder;
invoke-direct {v3}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {v3, p3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v3
invoke-virtual {v3, p2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v3
invoke-virtual {v3}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v3
:goto_21
invoke-virtual {v4, v3}, Ljava/lang/ClassLoader;->loadClass(Ljava/lang/String;)Ljava/lang/Class;
move-result-object v3
const-class v4, Landroid/view/View;
invoke-virtual {v3, v4}, Ljava/lang/Class;->asSubclass(Ljava/lang/Class;)Ljava/lang/Class;
move-result-object v0
.line 151
.local v0, "clazz":Ljava/lang/Class;, "Ljava/lang/Class<+Landroid/view/View;>;"
sget-object v3, Landroid/support/v7/internal/app/AppCompatViewInflater;->sConstructorSignature:[Ljava/lang/Class;
invoke-virtual {v0, v3}, Ljava/lang/Class;->getConstructor([Ljava/lang/Class;)Ljava/lang/reflect/Constructor;
move-result-object v1
.line 152
sget-object v3, Landroid/support/v7/internal/app/AppCompatViewInflater;->sConstructorMap:Ljava/util/Map;
invoke-interface {v3, p2, v1}, Ljava/util/Map;->put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
.line 154
.end local v0 # "clazz":Ljava/lang/Class;, "Ljava/lang/Class<+Landroid/view/View;>;"
:cond_36
const/4 v3, 0x1
invoke-virtual {v1, v3}, Ljava/lang/reflect/Constructor;->setAccessible(Z)V
.line 155
iget-object v3, p0, Landroid/support/v7/internal/app/AppCompatViewInflater;->mConstructorArgs:[Ljava/lang/Object;
invoke-virtual {v1, v3}, Ljava/lang/reflect/Constructor;->newInstance([Ljava/lang/Object;)Ljava/lang/Object;
move-result-object v3
check-cast v3, Landroid/view/View;
:try_end_42
.catch Ljava/lang/Exception; {:try_start_a .. :try_end_42} :catch_45
.line 159
:goto_42
return-object v3
:cond_43
move-object v3, p2
.line 148
goto :goto_21
.line 156
:catch_45
move-exception v2
.line 159
.local v2, "e":Ljava/lang/Exception;
const/4 v3, 0x0
goto :goto_42
.end method
効果的なシグネチャを開発するには、Androidの典型的な難読化パターンを理解することが不可欠です。最も広く使われている手法は識別子のリネームであり、クラス、メソッド、フィールドに、1文字やランダムな文字列のような意味のない名前が付けられます。
5. DalvikFLIRTの仕組み
DalvikFLIRTは、Dalvikバイトコードの特性に合わせた重要な調整を加えたうえで、シグネチャマッチングの概念をAndroidのエコシステムに適用します。
5.1 クラスのシグネチャを生成する
DalvikFLIRTは、幅広い属性を収集することでクラスのシグネチャを生成します。各クラスのシグネチャには、構造情報(名前、スーパークラス、アクセスフラグ、インターフェース)、利用可能な場合はソースコードのメタデータ、型と修飾子を含むフィールド、フィールドとメソッドの両方から得られる文字列定数、そして各メソッドの詳細なシグネチャが含まれます。システムはさらに、クラスの最も特徴的な要素を組み合わせた一意のフィンガープリントハッシュも計算します。
メソッドについては、シグネチャはさらに詳細で、メソッドディスクリプター、アクセスフラグ、バイトコードのハッシュ、命令数、APIの呼び出し、定数、オペコードの分布、制御フローグラフの特性を捉えます。
この多面的なアプローチにより、構造と振る舞いの両方の属性を捉えた豊富なシグネチャが作られ、難読化されても識別可能なままとなります。
{
"classes": {
"Lcom/example/myapplication/ComposableSingletons$MainActivityKt$lambda-1$1;": {
"name": "Lcom/example/myapplication/ComposableSingletons$MainActivityKt$lambda-1$1;",
"superclass": "Lkotlin/jvm/internal/Lambda;",
"access_flags": 16,
"interfaces": [
"Lkotlin/jvm/functions/Function3;"
],
"source_file": null,
"fingerprint": "2f1b337d2e6092b6775b2c9059222361",
"fields": [
{
"name": "INSTANCE",
"type": "Lcom/example/myapplication/ComposableSingletons$MainActivityKt$lambda-1$1;",
"access_flags": 25
}
],
"field_strings": [],
"string_constants": [
"innerPadding",
"com.example.myapplication.ComposableSingletons$MainActivityKt.lambda-1.<anonymous> (MainActivity.kt:22)",
"Android",
"C22@889L139:MainActivity.kt#ptgicz"
],
"methods": {
"<clinit>()V": {
"name": "<clinit>",
"descriptor": "()V",
"access_flags": 65544,
"bytecode_hash": "2552c2aefad67c2d666996fc73085a74",
"instruction_count": 4,
"api_calls": [
21
],
"constants": [],
"opcode_distribution": {
"34": 1,
"112": 1,
"105": 1,
"14": 1
},
"cfg": {
"basic_blocks": 1,
"edges": 0,
"exception_handlers": 0
},
"registers": 1
},
"<init>()V": {
"name": "<init>",
"descriptor": "()V",
"access_flags": 65536,
"bytecode_hash": "0e486b2e5a245ee4b0e601cc7ddbbce9",
"instruction_count": 3,
"api_calls": [
61
],
"constants": [
[
"numeric",
3
]
],
"opcode_distribution": {
"18": 1,
"112": 1,
"14": 1
},
"cfg": {
"basic_blocks": 1,
"edges": 0,
"exception_handlers": 0
},
5.2 類似度の指標を計算する
難読化されたクラスをシグネチャのデータベースと比較する際、DalvikFLIRTは重み付けされた類似度の計算を用います。各構成要素は、難読化に対する耐性に応じて全体のスコアに寄与します。
METHOD_WEIGHTS = {
"bytecode_hash": 0.0,
"descriptor": 0.25,
"constants": 0.25,
"opcode_distribution": 0.3,
"cfg": 0.2,
}
CLASS_WEIGHTS = {
"metrics": 0.2,
"superclass": 0.1,
"interfaces": 0.1,
"fields": 0.1,
"methods": 0.5,
}
メソッドディスクリプターは信頼性が非常に高い要素です。パラメーターの型や戻り値は、機能を壊さずに変更されることがほとんどないからです。文字列定数は基本的な難読化を生き延びることが多く、意味上の手がかりを提供します。オペコードの分布はメソッドの振る舞いの統計的なフィンガープリントを作り出し、制御フローグラフの構造は正しい動作のために保持されなければならないロジックのパターンを反映します。
クラスのレベルでは、クラスの指標(メソッド数やフィールド数など)、継承関係、インターフェースの実装がいずれも識別に寄与します。重み付けのアプローチにより、DalvikFLIRTは、十分な特徴的な性質が残っていれば、一部の側面が変更されていても一致を認識できます。


5.3 ツールの実際の動作
実環境でのテストでは、250,000を超えるクラスを持つものも含め、人気のあるアプリケーションにDalvikFLIRTを適用しました。次の出力は、一致の例を示しています。
DalvikFLIRT Simplified Signature Matcher
Signatures1: tests/files/app-debug_signatures.json
Signatures2: tests/files/Media_APKPure_signatures.json
Output: matches_media.json
Threshold: 0.8
Loading signatures from tests/files/app-debug_signatures.json
Loaded 12288 class signatures
Loading signatures from tests/files/media_APKPure_signatures.json
Loaded 279796 class signatures
マッチャーは、難読化されたクラスLX/i7XがLandroidx/concurrent/futures/AbstractResolvableFutureに対応することを、83.9%の信頼度で特定しました。
Class Score Breakdown (LX/i7X; vs Landroidx/concurrent/futures/AbstractResolvableFuture;):
- Metrics : Score=0.451, Contrib=0.090 (Weight=0.2)
- Superclass : Score=1.000, Contrib=0.100 (Weight=0.1)
- Interfaces : Score=1.000, Contrib=0.100 (Weight=0.1)
- Fields : Score=1.000, Contrib=0.100 (Weight=0.1)
- Methods : Score=0.898, Contrib=0.449 (Weight=0.5)
Total Class Score: 0.8390
この識別は、名前が完全に難読化されていても機能します。たとえば、元のコードでaddListenerという名前だったメソッドは、難読化されたバージョンではLIZJ(Ljava/lang/Runnable; Ljava/util/concurrent/Executor;)Vにリネームされていました。DalvikFLIRTは、パラメーターの型、制御フロー、オペコードのパターンに基づいて、これらを正しく一致させました。
6. 残りのコードはどうするか:LLMによるコードの書き換え
DalvikFLIRTは、Android SDKや一般的なライブラリに由来する既知のコンポーネントの照合には優れていますが、既知の参照シグネチャを持たない独自のアプリケーションコードには機能しません。そこで当社は、大規模言語モデル(LLM)の力を活用して、補完的なアプローチを提供します。
6.1 LLMによるリライター
現代の大規模言語モデルは、膨大なコードリポジトリから学習したパターンに基づいて、コードを理解、生成、変換する優れた能力を備えています。当社は、これらのLLMが文脈と構造から意味を推論することで、多くの種類の難読化を効果的に元に戻せることを発見しました。
当社のアプローチの重要な着眼点は、問題の捉え方を変えることです。モデルにコードの「難読化解除」を求める(悪用の懸念から多くのモデルが拒否します)のではなく、タスクを「コードの可読性の向上」として提示します。これは微妙ですが重要な違いであり、これによってモデルがタスクに取り組めるようになります。
deobfuscator_agent = Agent(
large_model,
system_prompt=(
"You are an expert Java developer. Given hard to read Java source code, "
"your task is to: \n"
"Rewrite the code to make it more readable.\n"
"Please make sure to rewrite the whole code and DO NOT omit any function or method.\n"
"Improve the quality of the code. Focus on renaming variables, methods, "
"and classes to be more descriptive names based on their context and usage. "
"Maintain the original code's functionality. If no context is provided, "
"do your best to rewrite based on the code alone."
"Feel free to rewrite control flow to make the code easier to understand."
"Do NOT change string constants as these might be used by other apps."
),
)
6.2 マルチエージェントシステム
当社の実装では、専門化されたコンポーネントが連携してコードの理解を段階的に深めていく、協調型のマルチエージェントシステムを使用しています。
- 難読化解除エージェント(Deobfuscation Agent)は、機能を維持しながら、わかりやすさのために難読化されたコードを書き換えます。意味のある名前を付けることと、複雑な制御構造を単純化することに重点を置きます。
- 説明エージェント(Explainer Agent)は、コードが何をするのかを平易な言葉で説明し、入力、出力、主要な振る舞いを特定します。これは、さらなる分析のためのコンテキストを提供するのに役立ちます。
コンテキスト構築エージェント(Context-Building Agent)は、すでに特定されたコンポーネント(DalvikFLIRTで照合されたものなど)の情報を統合し、未知の部分の分析に役立てます。
これらのエージェントは独立して動作するのではなく、情報を共有し、より多くのコードが処理されるにつれて理解を段階的に深めていくフィードバックループを作り出します。
# Initialize the AI agent for deobfuscation
deobfuscator_agent = Agent(
# model,
large_model,
system_prompt=(
"You are an expert Java developer. Given an hard to read Java source code, "
"your task is to: \n"
"Rewrite the code to make it more readable.\n"
"Please make sure to rewrite the whole code and DO NOT omit any function or method.\n"
"and improve the quality of the code. Focus on renaming variables, methods, "
"and classes to more descriptive names based on their context and usage."
"DO NOT keep short or cryptic variable, classes and method names. "
"Maintain the original code's functionality. If no context is provided, "
"do your best to rewrite based on the code alone."
"Feel free to rewrite control flow to make the code easier to understand."
"Do NOT change string constant as these might be used by other apps."
),
)
# Initialize the AI agent for code explanation
code_explainer = Agent(
model,
system_prompt=(
"You are an expert code explainer. Analyze the provided source code and explain it in plain English. "
"Be thorough and cover: \n"
"1. The code's purpose and functionality\n"
"2. Inputs it expects\n"
"3. Outputs it produces\n"
"4. Edge cases and error handling\n"
"Use clear, simple language suitable for developers of all levels."
),
result_type=CodeExplanation,
)
6.3 最適な結果を得るためのプロンプトエンジニアリング
LLMベースのアプローチの有効性は、適切なプロンプトを作り込めるかどうかに大きく左右されます。当社は、一貫してより良い結果を生み出すいくつかの手法を開発しました。
「難読化」や「難読化解除」に直接言及するのではなく、「読みにくい」コードの可読性を高めるタスクとして提示します。この微妙な転換により、モデルの安全フィルターが作動するのを避けられます。
JavaとKotlinのAPI、難読化されていないライブラリ、DalvikFLIRTで特定されたコンポーネントから得たコンテキストを、参照点として提供します。たとえば、アプリケーションが特定済みの暗号化メソッドを呼び出していることがわかっていれば、その知識はLLMが周囲のコードを正しく解釈するのに役立ちます。
当社のプロセスは、依存関係の少ない単純なメソッドから始め、段階的により複雑なコンポーネントに取り組みます。これにより、LLMは徐々にコンテキストを構築し、先に行った分析の情報を後の分析に活かすことができます。
何を維持すべきか(機能)、何を改善すべきか(名前、構造)についての明確な指示は、モデルを単なる再フォーマットではなく、有用な変換へと導くのに役立ちます。
当社のシステムで実現できることの例を次に示します。
// Original obfuscated code
public static void a(long j, String str, int i, Object obj) {
if (obj != null && c.e(str) && i >= 0) {
new Thread(new d(j, str, i, obj)).start();
}
}
// LLM-deobfuscated code
public static void logEventAsync(long userId, String eventName, int eventType, Object eventData) {
if (eventData != null && ValidationUtils.isValidEventName(eventName) && eventType >= 0) {
new Thread(new EventLoggingTask(userId, eventName, eventType, eventData)).start();
}
}
難読化を解除したバージョンは、メソッドの目的を明らかにする意味のある名前を与えるだけでなく、構成要素となるクラスがどのように使われているかに基づいて、それらのクラスのおそらくの意味も推論しています。
6.4 再帰的なボトムアップ分析
当社は、DalvikFLIRTのシグネチャマッチングとLLMによる書き換えを組み合わせる方法は、再帰的なボトムアップのアプローチで最もうまく機能することを見いだしました。プロセスは次のように進みます。
まず、アプリケーション内のすべてのメソッドが互いにどのように関係しているかを示す、完全なコールグラフを構築します。このマッピングにより、どのメソッドがどのメソッドを呼び出しているかが明らかになり、依存関係の階層が作られます。
次に、他のメソッドを呼び出さない「リーフメソッド」を特定します。これらの単純な関数は依存関係が少ないため、分析の出発点として理想的です。
最初のメソッド群の難読化を解除したら(一部はDalvikFLIRTのシグネチャマッチングで、それ以外はLLMによる分析で)、コールツリーを上へと進み、理解できるようになったこれらの関数を使用するメソッドに移ります。すでに分析したメソッドは、上位の関数が何をしているのかを理解するための貴重なコンテキストとなります。
コールツリーを上へ進み続けるにつれて、アプリケーションの振る舞いについての理解が段階的に豊かになっていきます。新しいメソッドはそれぞれ、自身が呼び出すすでに分析済みのすべてのメソッドのコンテキストの恩恵を受けます。
考古学のようなものだと考えてください。最も単純な遺物の理解から始め、その知識を使ってより複雑な遺物を解釈し、文明全体を土台から徐々に再構築していくのです。
次のコードは、このコンテキストを考慮したシステムでメソッドがどのように分析されるかを示しています。
def analyze_method(self, node: MethodNode) -> Dict:
"""
Perform full AI analysis on a method including explanation, deobfuscation and security audit.
"""
# Get source code
class_source_code = self.methods_engine.get_class_source(node)
method_source_code = self.methods_engine.get_method_source(node)
# Get context from callees (methods this one calls)
callee_info = self.methods_engine.get_callees(node)
# Deobfuscate the code using both DalvikFLIRT and LLM approaches
deobfuscated_code = self.deobfuscate_code(
class_source_code,
method_source_code,
context=callee_info
)
# Generate a human-readable explanation
explanation = self.explain_code(deobfuscated_code, node)
# Store results for use as context in analyzing methods that call this one
return {
'source_code': method_source_code,
'deobfuscated_code': deobfuscated_code,
'explanation': explanation,
'security_issues': security_audit,
}
既知のコンポーネントにはDalvikFLIRTのシグネチャベースの識別を、独自のアプリケーションロジックにはLLMによるコードの書き換えを用いるという、これらの手法を組み合わせることで、強力に保護されたAndroidアプリケーションでさえ効果的に難読化を解除できるシステムを構築しました。

結論と今後の方向性
DalvikFLIRTのシグネチャベースの認識とLLMによる書き換えを組み合わせた当社の二重のアプローチは、Androidアプリケーションの難読化を回避するための強力なシステムを生み出します。このアプローチにより、本来であれば解読できないコードを理解するための、かつてない能力が得られます。
当社の手法の主な強みは、Android SDKのコンポーネントを高い信頼度で自動的に特定し、それらをコンテキスト上の拠り所として使って、アプリケーション固有のロジックについて高品質なコードを生成することです。このシステムは、単純な名前の難読化から、より複雑な制御フローの操作まで、複数の難読化手法を同時に処理します。
ボトムアップの再帰的なアプローチにより、システムはより多くのコードを分析するほど段階的に効果を高め、新たな知見の一つひとつがその後の分析を改善していきます。
この取り組みは、セキュリティを損なうためのものではなく、自動化の能力を前進させるためのものです。難読化されたコードを理解可能にすることで、より徹底したセキュリティ分析と脆弱性の検出が可能になります。
タグ:
ai