fiord Advent Calendar 2025 13 日目の記事です。
生成AI はコンパイルしたコードをデコンパイルすることに一定有用です。具体的に、C言語をコンパイルした elf を用いたデコンパイル用のモデルが存在する程度には有用だと思っています。
さて、世間一般に使われているモデルではどうなのでしょうか?今回は複数のプログラミング言語において Claude Sonnet 4.5 を用いてデコンパイルをさせてみました。
レギュレーション
デコンパイルに当たり、下記の 2 つを用意しておきます。
- コンパイル後のバイナリ。ELF 64-bit x86-64 です
- 上を
objdump -S $binary > $asmで逆アセンブルしたもの- これは人間にも読める形となっており、token 結構食うとかそんなことを無視しておけば生成 AI でも読むことが出来ます
一部例外があり、その場合は各言語の箇所で触れます。
C/C++ (1)
まずは、お試し感覚で下記 C++ のソースコードを用意します。AOJ の過去の自分の回答 を利用してみましょう。
生成されたコード
#include <bits/stdc++.h>
using namespace std;
struct Dice {
// ordering: top, south, east, west, north, bottom
int a[6];
// apply a single command char
void change(char c) {
int t = a[0];
switch (c) {
case 'N':
// roll to the north: top <- south, south <- bottom, bottom <- north, north <- top
a[0] = a[1];
a[1] = a[5];
a[5] = a[4];
a[4] = t;
break;
case 'S':
// roll to the south
a[0] = a[4];
a[4] = a[5];
a[5] = a[1];
a[1] = t;
break;
case 'E':
// roll to the east
a[0] = a[3];
a[3] = a[5];
a[5] = a[2];
a[2] = t;
break;
case 'W':
// roll to the west
a[0] = a[2];
a[2] = a[5];
a[5] = a[3];
a[3] = t;
break;
default:
// disassembly showed a printing path & exit for unexpected chars
cout << "Invalid command: " << c << '\n';
exit(0);
}
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
Dice d;
// read six integers
for (int i = 0; i < 6; ++i) {
if (!(cin >> d.a[i])) return 0;
}
// read command string
string s;
if (!(cin >> s)) {
cout << d.a[0] << "\n"; // if there's no string, just output the top
return 0;
}
// process commands
for (char c : s) d.change(c);
// print top face
cout << d.a[0] << "\n";
return 0;
}
元のソースコードと構造は全く同じ。ただ、ダイスの回転が微妙に違うように見える...?と思ったので、念のため提出。
問題無さそうですね。
C/C++ (2)
より難しそうなものとして、SECCON 13 Online CTF のとある問題 を利用しました。
生成されたコード
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <random>
#include <unistd.h>
using namespace std;
// 原子構造体(4バイトのint型)
struct Atom
{
int type;
Atom() : type(0) {}
};
// 環境クラス
class Environment
{
public:
long width; // +0x00
long height; // +0x08
int max_steps; // +0x10
// +0x14 padding
vector<vector<Atom>> grid; // +0x18
mt19937 rng; // +0x30 (MT19937の状態: 624*8 + αバイト)
Environment(long w, long h) : width(w), height(h), max_steps(0)
{
grid.resize(h);
for (long i = 0; i < h; i++)
{
grid[i].resize(w);
}
}
// グリッドに新しい原子を配置し、重力で落下させる
// アセンブリアドレス: 0x297c
bool set()
{
// 2つのランダムな属性値を生成
int attrs[2];
attrs[0] = 0;
attrs[1] = 0;
// 各属性値を乱数で決定(0x29c8~0x29f8)
for (int i = 0; i < 2; i++)
{
unsigned long rand_val = rng();
int attr_type = rand_val & 0x3; // 下位2ビット
// 属性タイプの変換(0x29d0~0x29ef)
if (attr_type == 2)
{
attrs[i] = 3;
}
else if (attr_type == 1)
{
attrs[i] = 2;
}
else if (attr_type == 3)
{
attrs[i] = 4;
}
else
{
attrs[i] = 1;
}
}
// 標準入出力を使用した入力(0x2a0a~0x2a2d)
char input_bytes[2];
write(1, (char *)attrs, 2); // 属性を出力
read(0, input_bytes, 2); // 2バイト読み込み
unsigned char x_pos = input_bytes[0];
unsigned char y_dir = input_bytes[1];
// 入力検証(0x2a3c~0x2a63)
if (y_dir > 3)
{
return false;
}
if ((y_dir & 1) != 0)
{
// y方向が奇数の場合(上方向)
if (x_pos >= width - 1)
{
return false;
}
long y_start;
if (y_dir == 1)
{
y_start = height - 1;
}
else if (y_dir == 3)
{
y_start = 0;
}
else
{
y_start = height - 1;
}
// 原子を配置(0x2b79~0x2b8b)
for (int i = 0; i < 2; i++)
{
long x = x_pos + 1;
long y = y_start;
if (y < height && x < width)
{
if (grid[y][x].type == 0)
{
grid[y][x].type = attrs[i];
}
else
{
return false;
}
}
x--;
y_start = (y_dir == 1) ? y_start - 1 : y_start + 1;
}
return true;
}
else
{
// y方向が偶数の場合(横方向)
if (x_pos >= width)
{
return false;
}
if (y_dir == 0)
{
// 下から配置(0x2a63~0x2ade)
for (int i = 0; i < 2; i++)
{
long y = height - 1 - i;
if (y < height && x_pos < width)
{
if (grid[y][x_pos].type == 0)
{
grid[y][x_pos].type = attrs[i];
}
else
{
return false;
}
}
}
}
else if (y_dir == 2)
{
// 上から配置(0x2ca4~0x2d16)
for (int i = 0; i < 2; i++)
{
long y = height - 2 + i;
if (y >= 0 && y < height && x_pos < width)
{
if (grid[y][x_pos].type == 0)
{
grid[y][x_pos].type = attrs[i];
}
else
{
return false;
}
}
}
}
return true;
}
}
// 化学反応をシミュレート
// アセンブリアドレス: 0x35d2
bool react(long x, long y, int attr)
{
// 属性が4未満の場合は反応しない
if (attr < 4)
{
return false;
}
// 周辺セルをチェックして反応可能な原子をカウント
int count = 0;
for (long dy = -1; dy <= 1; dy++)
{
for (long dx = -1; dx <= 1; dx++)
{
if (dx == 0 && dy == 0)
continue;
long nx = x + dx;
long ny = y + dy;
if (nx >= 0 && nx < width && ny >= 0 && ny < height)
{
if (grid[ny][nx].type != 0)
{
count++;
}
}
}
}
// 4つ以上の原子が隣接している場合、それらを消去
if (count >= 4)
{
for (long dy = -1; dy <= 1; dy++)
{
for (long dx = -1; dx <= 1; dx++)
{
if (dx == 0 && dy == 0)
continue;
long nx = x + dx;
long ny = y + dy;
if (nx >= 0 && nx < width && ny >= 0 && ny < height)
{
grid[ny][nx].type = 0;
}
}
}
return true;
}
return false;
}
// グリッド全体を更新
// アセンブリアドレス: 0x3b6c
bool update()
{
bool changed = set();
if (!changed)
{
return false;
}
int step_count = 0;
// 全グリッドを走査して重力と反応を処理
// アセンブリ 0x3c94~0x3c31のループ
for (long i = 0; i < width; i++)
{
for (long j = 0; j < height; j++)
{
if (grid[j][i].type != 0)
{
// 下方向への移動(重力)
for (long k = j + 1; k < height; k++)
{
if (grid[k][i].type == 0)
{
grid[k][i].type = grid[j][i].type;
grid[j][i].type = 0;
}
else
{
break;
}
}
}
}
}
// 反応チェック(下から上へ)
// アセンブリ 0x3cd5~0x3c64のループ
for (long j = height - 1; j >= 0; j--)
{
for (long i = 0; i < width; i++)
{
if (grid[j][i].type != 0)
{
if (react(i, j, grid[j][i].type))
{
step_count++;
}
}
}
}
// 最大ステップ数を更新(0x3c44~0x3c4d)
if (step_count > max_steps)
{
max_steps = step_count;
}
return true;
}
};
int main()
{
// flag.txtファイルを開く(0x2454~0x2460)
ifstream file("flag.txt", ios::in);
if (!file.is_open())
{
cerr << "Flag not found" << endl;
return 1;
}
// フラグを読み込む(0x2471~0x247b)
string flag;
getline(file, flag);
file.close();
// フラグ文字列の各文字の積を計算(0x24ba~0x24d6)
unsigned int product = 1;
for (size_t i = 0; i < flag.length(); i++)
{
product *= static_cast<unsigned char>(flag[i]);
}
// 環境を初期化(14x14グリッド)(0x24d6~0x24e9)
const long GRID_SIZE = 14;
Environment env(GRID_SIZE, GRID_SIZE);
// メルセンヌツイスター乱数生成器を初期化(0x251d~0x2583)
// MT19937の内部状態を手動で設定
env.rng.seed(product);
// シミュレーションループ(0x266a~0x2672)
while (env.update())
{
// 環境を更新し続ける
}
// 最終判定: 13ステップを超えたかをチェック(0x2674~0x267b)
if (env.max_steps > 13)
{
cout << "Correct!" << endl;
cout << "FLAG: " << flag << endl;
return 0;
}
else
{
cout << "Wrong..." << endl;
return 1;
}
}
元のソースコードと比べると「アレ? react って 8 近傍じゃなくて 4 近傍だよね?」というツッコミが入ります。
コードの概要を把握することには向いているかもしれない一方で、詳細な挙動を把握する際には一部実際と異なる可能性があります。
Python
Python においては pyc という形式があります。これは dis ライブラリにより Python バイトコードの逆アセンブルが可能です。よって、dis.dis() の出力結果を生成 AI に投げています。
ちょっと前に Blender のマルウェアに関する記事を書いたので、それを使ってみましょう。
以降、実際のマルウェアを取り扱う内容が含まれます。真似をする際には十分に気を付けてください
検体はこの中にある BLENDERX.py になります。
https://bazaar.abuse.ch/sample/43c14156558b8d58db365889f48492c6658ddbe78f2505721d31b900da4c4981
上図のような形で、中に含まれる悪性のコードを .py 形式で出力します。これを .pyc に変換しましょう。
$ python -m py_compile ${your_python_script}.py
これで、__pycache__ ディレクトリに目的の .pyc ファイルが生成されます。後はこれに対して、このようなコードを実行するとアセンブリを吐いてくれます。
import dis
import marshal
code = open('${your_pyc_file}.pyc', 'rb').read()[16:]
obj = marshal.loads(code)
dis.dis(obj)
この出力結果を生成 AI に投げてみました。
生成されたコード
比較用に、左が元のコード、右が生成 AI により復元されたコードです。
このレベルで合致するのは驚きました。かなり信頼性が高いと思ってよいと思います。
Rust
過去に自分が作成したツール を対象にやってみます。
生成されたコード
わざわざレポートを用意してくれました。
APK Patcher - アセンブリから復元した分析レポート
プログラム概要
apk-patcher は、Android APKファイルの操作を行うコマンドラインツールです。
このプログラムはRust言語で実装されており、以下の機能を提供します。実行ファイル情報
- ファイル形式: ELF 64-bit LSB pie executable
- アーキテクチャ: x86-64
- 動的リンク: あり
- デバッグ情報: 含まれる (not stripped)
- プログラミング言語: Rust
主要機能
1. decompress コマンド
APKファイルを展開・逆コンパイルします。
使用法:
apk-patcher decompress <INPUT> [OUTPUT] [OPTIONS]引数:
INPUT: デコンパイルするAPKファイルのパス(必須)OUTPUT: 出力先フォルダ名(省略時は入力ファイル名から自動生成)オプション:
-f, --force: 既存の出力ディレクトリを強制削除--no-assets: アセットファイルをデコードしない--only-main-classes: メインクラスのみをデコンパイル-r, --no-res: リソースファイルをデコードしない-s, --no-src: ソースコードをデコードしない内部処理:
- 入力ファイルが
.apk拡張子を持つか検証(正規表現:\.apk$)apktool dコマンドを実行してAPKを展開- 完了メッセージを出力:
[apk-patcher] finished.2. compress コマンド
デコンパイルされたフォルダからAPKファイルを再構築します。
使用法:
apk-patcher compress <INPUT> <OUTPUT> [OPTIONS]引数:
INPUT: 圧縮するフォルダのパス(必須)OUTPUT: 出力するAPKファイル名(必須)オプション:
--use-aapt2: 実験的なaapt2バイナリを使用内部処理:
apktool bコマンドで一時APKファイル(.packed.apk)を生成zipalignでAPKファイルを4バイト境界に最適化apksignerでAPKに署名
- キーストア:
apk-patcher.keystore- エイリアス:
apk-patcher- パスワード:
apk-patcher- 完了メッセージを出力:
[apk-patcher] finished.3. patches コマンド
APKファイルへのパッチ適用機能(詳細は不明)
依存する外部ツール
プログラムは以下の外部コマンドを呼び出します:
- apktool - APKのデコンパイル・リビルド
- zipalign - APKファイルの最適化(Android SDKに含まれる)
- apksigner - APKへの署名(Android SDKに含まれる)
使用ライブラリ(アセンブリから確認)
- clap 4.1.1 - コマンドライン引数パーサー(derive機能使用)
- regex 1.7.1 - 正規表現ライブラリ
- aho-corasick - 高速文字列マッチング
- memchr - 高速メモリ検索
- Rust標準ライブラリ群
エラーハンドリング
プログラムは以下のエラーメッセージを出力します:
called 'Result::unwrap()' on an 'Err' value- Result型のエラー時called 'Option::unwrap()' on a 'None' value- Option型がNoneの時[apk-patcher] interrupted.- 処理開始時のメッセージ[apk-patcher] finished with error code: <code>- エラー終了時[apk-patcher] finished.- 正常終了時ビルド情報
- Rustコンパイラバージョン: 1.66.1 (rustc 69f9c33d71c871fc16ac445211281c6e7a340943)
- 最適化レベル: リリースビルド(LTO有効)
- ビルドID: 0822acb71c9c4c3b723c9592111ce191a909ed79
デコンパイルの正確性について
このソースコードは、以下の手法で最大限の正確性を確保しています:
- 関数シグネチャ: マングルされた関数名から正確に復元
- 文字列リテラル:
.rodataセクションから直接抽出- コマンドライン引数: clapのマクロ展開結果から復元
- 制御フロー: アセンブリの分岐命令から論理構造を再構築
- 外部コマンド:
Command::new()呼び出しから特定推測を避けた箇所:
- Patches構造体の内部フィールド(アセンブリから判別不可能)
- エラーハンドリングの詳細な分岐(一部省略)
- 未使用の内部関数
技術的詳細
関数のマングル名の例
_ZN11apk_patcher4main17h3f9724fba6d90ab6E -> apk_patcher::main _ZN11apk_patcher8commands10decompress10Decompress7execute17hd5ebdf07c040ff25E -> apk_patcher::commands::decompress::Decompress::execute _ZN11apk_patcher8commands8compress8Compress7execute17h5d05c5133d1a7b67E -> apk_patcher::commands::compress::Compress::executeメモリレイアウト
プログラムは以下のセクションを持ちます:
.text: 実行コード(約120KB).rodata: 読み取り専用データ(文字列リテラルなど).data: 初期化済みグローバルデータ.bss: 未初期化グローバルデータ****
// APK Patcher - Restored from assembly
// Version: 0.1.0
//
// このソースコードはアセンブリから復元したものです。
// 正確さを最優先し、推測による実装は行っていません。
use std::process::{Command, exit};
use std::path::PathBuf;
// Clap derive マクロによるCLI定義
// アセンブリから確認された構造
#[derive(clap::Parser)]
#[command(name = "apk-patcher")]
#[command(version = "0.1.0")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(clap::Subcommand)]
enum Commands {
/// Decompress APK file
Decompress(Decompress),
/// Compress folder to APK file
Compress(Compress),
/// Apply patches to APK
Patches(Patches),
}
// アセンブリから確認されたDecompress構造体の定義
#[derive(clap::Args)]
struct Decompress {
/// The name of apk file to be decompressed.
input: PathBuf,
/// The name of folder that gets written. Default is same as input.
output: Option<PathBuf>,
/// Force delete destination directory.
#[arg(short = 'f', long = "force")]
force: bool,
/// Do not decode assets.
#[arg(long = "no-assets")]
no_assets: bool,
/// Decompress only main classes.
#[arg(long = "only-main-classes")]
only_main_classes: bool,
/// Do not decode resources.
#[arg(short = 'r', long = "no-res")]
no_resources: bool,
/// Do not decode sources.
#[arg(short = 's', long = "no-src")]
no_sources: bool,
}
// アセンブリから確認されたCompress構造体の定義
#[derive(clap::Args)]
struct Compress {
/// The name of folder to be compressed
input: PathBuf,
/// The name of output apk file
output: PathBuf,
/// Upgrades apktool to use experimental aapt2 binary.
#[arg(long = "use-aapt2")]
use_aapt2: bool,
}
// Patches構造体 (詳細はアセンブリから完全には判明しないため最小限)
#[derive(clap::Args)]
struct Patches {
// フィールドの詳細はアセンブリからは不明確
}
impl Decompress {
// アセンブリの _ZN11apk_patcher8commands10decompress10Decompress7execute から復元
fn execute(&self) -> Result<(), Box<dyn std::error::Error>> {
// 正規表現でAPKファイルチェック (アセンブリから確認: \.apk$)
let input_str = self.input.to_str().ok_or("Invalid input path")?;
if !input_str.ends_with(".apk") {
return Err("Input must be an APK file".into());
}
// 出力パスの決定
let output = self.output.as_ref()
.map(|p| p.clone())
.unwrap_or_else(|| {
let mut p = self.input.clone();
p.set_extension("");
p
});
// apktoolコマンドの構築
let mut cmd = Command::new("apktool");
cmd.arg("d")
.arg("-o")
.arg(&output);
if self.force {
cmd.arg("-f");
}
if self.no_assets {
cmd.arg("--no-assets");
}
if self.only_main_classes {
cmd.arg("--only-main-classes");
}
if self.no_resources {
cmd.arg("-r");
}
if self.no_sources {
cmd.arg("-s");
}
cmd.arg(&self.input);
// コマンド実行
println!("[apk-patcher] interrupted.");
let status = cmd.status()?;
if !status.success() {
let code = status.code().unwrap_or(-1);
eprintln!("[apk-patcher] finished with error code: {}", code);
exit(code);
}
println!("[apk-patcher] finished.");
Ok(())
}
}
impl Compress {
// アセンブリの _ZN11apk_patcher8commands8compress8Compress7execute から復元
fn execute(&self) -> Result<(), Box<dyn std::error::Error>> {
// 一時的なパックファイル名 (アセンブリから確認: .packed.apk)
let packed_apk = self.output.with_extension("packed.apk");
// apktoolでビルド
let mut cmd = Command::new("apktool");
cmd.arg("b")
.arg("-o")
.arg(&packed_apk);
if self.use_aapt2 {
cmd.arg("--use-aapt2");
}
cmd.arg(&self.input);
println!("[apk-patcher] interrupted:");
let status = cmd.status()?;
if !status.success() {
let code = status.code().unwrap_or(-1);
eprintln!("[apk-patcher] finished with error code: {}", code);
exit(code);
}
// zipalignで最適化 (アセンブリから確認)
let zipalign_status = Command::new("zipalign")
.arg("-v")
.arg("-f")
.arg("4")
.arg(&packed_apk)
.arg(&self.output)
.status()?;
if !zipalign_status.success() {
return Err("zipalign failed".into());
}
// 一時ファイル削除
std::fs::remove_file(&packed_apk).ok();
// apksignerで署名 (アセンブリから確認されたパラメータ)
let signed_apk = self.output.with_extension("signed.apk");
let keystore = "apk-patcher.keystore";
let sign_status = Command::new("apksigner")
.arg("sign")
.arg("--ks")
.arg(keystore)
.arg("--ks-key-alias")
.arg("apk-patcher")
.arg("--ks-pass")
.arg("pass:apk-patcher")
.arg("-v4")
.arg("-f")
.arg(&self.output)
.status()?;
if !sign_status.success() {
return Err("apksigner failed".into());
}
println!("[apk-patcher] finished.");
Ok(())
}
}
// メイン関数 (アセンブリの _ZN11apk_patcher4main から復元)
fn main() {
// CLIパース (clapによる自動生成)
let cli = <Cli as clap::Parser>::parse();
// コマンド実行
let result = match cli.command {
Commands::Decompress(cmd) => cmd.execute(),
Commands::Compress(cmd) => cmd.execute(),
Commands::Patches(_) => {
eprintln!("Patches command not fully implemented");
Ok(())
}
};
// エラーハンドリング
if let Err(e) = result {
eprintln!("Error: {}", e);
exit(1);
}
}
// Cargo.toml の依存関係 (アセンブリから確認):
// [dependencies]
// clap = { version = "4.1.1", features = ["derive"] }
// regex = "1.7.1"
Compress においてコマンド実行してへんやんけ!など、ちょいちょい致命的な誤りがありました。
ただ、おおよそのコードは合ってます。偉い。
Golang
Rust と同様に過去自分が作成したリポジトリ を利用しようと思いました。
しかし、Golang において「バイナリサイズが非常に大きい」という問題が壁になりました。objdump によるアセンブリのサイズが 100 万行を超えて、生成 AI が(分割してでも)読むのに向いてないと判断します。
よって部分的に読んで概要を把握し、それを元に推測でコードを書き始めます。
デコンパイルとは本質が異なると思われるので失格。UPX をかけると圧縮できるという記事をみたのですが、この上でコードを復元できるかは謎です。今度検証してみてもいいかも。
Java
.class からの JD_GUI は有名過ぎるよな...生成 AI いる?と思ったので除外。
もし必要なケースがあれば教えてください!
まとめ
色々な言語に対して、様々なコードをデコンパイルさせてみました。それぞれコードも大きく異なる都合、ベンチマークとかそういうような性質は全く持っていません。
ただし、「結構精度よくデコンパイルしてくれて、コードの概要を掴むのには向いている」と思います。もしよければ試してみてね!


