UBIイメージからUBIFSをマウントする方法

こんにちは。技術統括部のucqです。UBIイメージからUBIFSをマウントする方法を書いてみました。細かい話はどうでもいい人は UBIイメージからUBIFSをマウントする方法 だけ読めばいいと思います。

そして、近日開催の学生向けイベントのお知らせがあります。締め切りが7月31日と間近ですので、忘れずにご応募ください!

[PR]

2025年8月21日から8月22日にかけて、学生向けイベント『Binary Exploitation Workshop for Students』を開催します!

CDIで開催されているトレーニング『Binary Exploitation Fundamentals』をベースにカスタムしたコンテンツで、主にスタックベースのバッファオーバーフローに対する攻撃を取り扱います。 新しい技術を学びたい方はもちろん、CDIのエンジニアとお話したい方の参加もお待ちしております!

詳細はこちらからご確認ください。

[/PR]

はじめに

ファームウェア解析でUBIイメージを展開する際、多くの記事で紹介されているようにUBI Readerのような専用の展開ツールを使用することが紹介されています。しかし、通常のファイルシステムのようにマウントして直接展開・書き換えしたい場面も多いでしょう。UBIFSのマウントには一般的なファイルシステムと比べて複数の手順が必要で、情報も古いものが多く日本語の記事も少ないため苦労したので備忘録としてブログに残しておきます。また、なぜ直感的なmount -t ubifs firmware.ubifs /mnt/ubifsコマンドが失敗するのかをおまけで載せました。

UBI/UBIFSとは

まずUBI/UBIFSで使う言葉を少しだけ説明します。

MTD(Memory Technology Device)

生のNAND/NORフラッシュメモリの一度消去しなければ上書きできず、消去はブロック単位でしか行えないといったようなハードウェア固有の制約を吸収し、読み込み・書き込み・消去といった操作しやすくするサブシステムです。

UBI(Unsorted Block Images)

UBI(Unsorted Block Images)は、LVMにおける論理セクターと物理セクターの対応付けに似た仕組みで、MTD(Memory Technology Device)の物理消去ブロック(PEB)を論理消去ブロック(LEB)にマッピングしたものです。ウェアレベリングや不良ブロック管理、論理ボリューム管理することでフラッシュメモリの耐久性と利用効率を高める中間管理層です。

UBIFS(Unsorted Block Image File System)

UBI層の上で動作する実際のファイルシステムです。フラッシュメモリの特性を活かした効率的な読み書きが可能で、組み込みシステムで広く使用されています。また、ジャーナリング機能により電源断時の耐性もあります。

最小I/O単位サイズ

フラッシュメモリの読み書きの最小単位です。NORフラッシュなら1バイト、NANDフラッシュはページ単位(512~4096バイト)です。

物理消去ブロック: PEB(Physical Erase Block)

フラッシュメモリの消去単位です。ハードウェア仕様で決まる物理的なブロックサイズを指します。

論理消去ブロック: LEB(Logical Erase Block)

UBIが管理する論理的な消去ブロックです。PEBからUBIメタデータ(通常128バイト)を引いたサイズです。

UBIイメージとUBIFSイメージの違い

ファームウェア解析では、UBIイメージとUBIFSイメージという2つの異なる形式に遭遇することがあります。ファームウェアに対してbinwalkで見つかるのはほとんどUBIイメージでしょう。またこれらの用語は混同されていることがあると思っている(主観)ので注意しましょう。

UBIイメージ(.ubi)

  • UBI層全体(UBIFSを含む)イメージファイル
  • 1つ以上のUBIFSボリュームを含む可能性がある
  • 本記事で扱うのはこちらの形式

$ file firmware.ubi
firmware.ubi: UBI image, version 1
$ binwalk firmware.ubi

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             UBI erase count header, version: 1, EC: 0x0, VID header offset: 0x40, data offset: 0x80

UBIFSイメージ(.ubifs)

  • UBIFSファイルシステムのみのイメージファイル
  • UBI層の情報は含まない
$ file firmware.ubifs
firmware.ubifs: UBIfs image, sequence number 10, length 4096, CRC 0x4466a7aa
$ binwalk firmware.ubifs

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             UBIFS filesystem superblock node, CRC: 0x4466A7AA, flags: 0x0, min I/O unit size: 512, erase block size: 129024, erase block count: 13, max erase blocks: 24, format version: 4, compression type: lzo
129024        0x1F800         UBIFS filesystem master node, CRC: 0xCB7C23DC, highest inode: 65, commit number: 0
258048        0x3F000         UBIFS filesystem master node, CRC: 0xC74CD6C1, highest inode: 65, commit number: 0

テスト用UBIイメージの作成

まずはテスト用のUBIイメージファイルの作成をしていきます。ファームウェアから抽出したUBIイメージ等が既にある場合このステップは不要です。

# テスト用ディレクトリとファイルの作成
mkdir -p /tmp/test-dir
echo "test content" > /tmp/test-dir/testfile.txt

# UBIFSイメージの作成
mkfs.ubifs -r /tmp/test-dir -m 1 -e 130944 -c 25 -o /tmp/firmware.ubifs

# UBI設定ファイルの作成
cat > /tmp/ubinize.cfg << EOF
[ubifs]
mode=ubi
image=/tmp/firmware.ubifs
vol_id=0
vol_size=3MiB
vol_name=rootfs
vol_type=dynamic
EOF

# UBIイメージの作成
ubinize -o /tmp/firmware.ubi -p 131072 -m 1 /tmp/ubinize.cfg

MTD公式ドキュメントに基づいて、各パラメータの意味を解説します。

mkfs.ubifsコマンドのパラメータ

  • -m 1: 最小I/O単位サイズ(バイト)
  • -e 130944: 論理消去ブロック(LEB)サイズ(バイト)
  • -c 25: 最大LEB数

ubinizeコマンドのパラメータ

  • -p 131072: 物理消去ブロック(PEB)サイズ(バイト)
  • -m 1: 最小I/O単位サイズ(バイト)

パラメータの適切な値の決定方法

マウントするシステム上でmtdramドライバをロードしてMTD情報を確認することで、上記のコマンドのパラメータを決めることができます。

$ cat /proc/mtd
dev:    size   erasesize  name
mtd0: 00400000 00020000 "mtdram test device"

$ mtdinfo -u /dev/mtd0
mtd0
Name:                           mtdram test device
Type:                           ram
Eraseblock size:                131072 bytes, 128.0 KiB    # ubinize -p 131072
Amount of eraseblocks:          32 (4194304 bytes, 4.0 MiB)
Minimum input/output unit size: 1 byte                      # mkfs.ubifs -m 1
Sub-page size:                  1 byte
Character device major/minor:   90:0
Bad blocks are allowed:         false
Device is writable:             true
Default UBI VID header offset:  64
Default UBI data offset:        128
Default UBI LEB size:           130944 bytes, 127.8 KiB    # mkfs.ubifs -e 130944
Maximum UBI volumes count:      128

これらの情報から、正しいパラメータは以下です。

  • mkfs.ubifs -m 1 -e 130944
  • ubinize -p 131072 -m 1

最大LEB数(mkfs.ubifsコマンドの-c)はどのくらいのUBIFSボリュームのサイズが欲しいかによってユーザが決めます。「LEBサイズ * LEB数」で計算できるためこれを基準に決めると良いでしょう。

例えば3MBのボリュームを作りたい場合は次のような計算です。

必要なLEB数 = 3MiB / 130944バイト = 24.02... → 25個(切り上げ)
実際のボリュームサイズ = 130944 * 25 = 3273600バイト(約3.1MB)

UBIイメージからUBIFSをマウントする方法

実行する前に/dev/mtd0/dev/ubi0がないことを確認してください。 これらのデバイスが既に存在する場合、以下のコマンドを実行すると既存デバイスの内容が破壊される可能性があります。 そのまま実行する場合は数字をよく確認して実行してください。

UBIイメージからUBIFSをマウント手順は以下の通りです。実行にはMTD utilsが必要です。

# 0. UBIイメージのパラメータを確認する
ubireader_utils_info -r firmware.ubi

# 1. 仮想MTDを作成する
sudo modprobe mtdram total_size=40960 erase_size=128

# 2. UBIイメージをMTDに書き込む
sudo ubiformat /dev/mtd0 -f /tmp/firmware.ubi

# 3. UBIドライバを読み込んでボリュームを認識させる
sudo modprobe ubi mtd=0

# 4. UBIFSファイルシステムをマウントする
sudo mkdir -p /mnt/ubifs
sudo mount -t ubifs ubi0_0 /mnt/ubifs

0. UBIイメージのパラメータを確認する

UBIイメージのファイルサイズはPEBの倍数である必要があります。適当に切り出した場合はpaddingするなどして調整してください。マウント作業を始める前に、ubi_readerのubireader_utils_infoコマンドを使用してUBIイメージのパラメータを確認します。これにより、後続の手順で必要な設定値を事前に把握できます。

$ ubireader_utils_info -r /tmp/firmware.ubi

Volume rootfs
        alignment       -a 1
        default_compr   -x lzo
        fanout          -f 8
        image_seq       -Q 102436144
        key_hash        -k r5
        leb_size        -e 130944
        log_lebs        -l 4
        max_bud_bytes   -j 523776
        max_leb_cnt     -c 23
        min_io_size     -m 8
        name            -N rootfs
        orph_lebs       -p 1
        peb_size        -p 131072
        sub_page_size   -s 64
        version         -x 1
        vid_hdr_offset  -O 64
        vol_id          -n 0

        #ubinize.ini#
        [rootfs]
        vol_type=dynamic                                                                                                                                                                                                                                                                                    vol_flags=0
        vol_id=0
        vol_name=rootfs
        vol_alignment=1
        vol_size=3273600

上記の例ではpeb_sizeが131072バイト(128KB)なので、次の手順でerase_size=128(131072/1024)と指定します。また、ボリューム名がrootfsなので、マウント時にubi0:rootfsとして指定できることも分かります。

1. 仮想MTDを作成する

modprobe mtdramコマンドで仮想的なMTDを作成します。total_sizeパラメータはKB単位で総サイズを指定し、UBIイメージのサイズにerase_size以上の余裕を持たせる必要があります。erase_sizeは前の手順で確認したpeb_sizeをKB単位で指定します。nandsimドライバでもmtdramドライバと同様にデバイスを作成できますが、NANDのモデルによるIDパラメータの設定になるため面倒です。こちらを使う場合の設定方法はほかの記事に任せます(参考: NAND simulator の使用方法)。

2. UBIイメージをMTDに書き込む

ubiformatコマンドでUBIイメージをMTDに書き込みます。例ではpeb_sizeが131072バイト(128KB)なので、erase_size=128(131072/1024)と指定します。

ddコマンドでも書き込みは可能ですが、ubiformatの方が以下の理由で推奨されます。

  • mtdramドライバのパラメータ設定ミスがあればエラーで教えてくれる
  • UBIメタデータを正しく処理してくれる
  • ddコマンドではドライバのアンロードとリセットが必要になる場合がある(これは筆者の知識不足で原因はよくわかってません)

3. UBIドライバを読み込んでボリュームを認識させる

modprobe ubi mtd=0でUBIドライバを読み込みます。mtd=0パラメータを指定することで、自動的にubiattach -m 0相当の処理が実行され、以下のデバイスファイルが作成されます。

  • /dev/ubi0: UBIデバイス全体を表すデバイスファイル
  • /dev/ubi0_0: 最初のボリューム(ボリュームID 0)を表すデバイスファイル

/dev/ubi0_0が作成されない場合はUBIとして正しく認識できていない可能性があります。その場合はdmesgコマンドでエラーを確認すると良いでしょう。

4. UBIFSファイルシステムをマウントする

マウントポイントを作成し、mountコマンドでUBIFSファイルシステムをマウントします。ボリュームの指定方法にはボリュームIDを使った指定(ubi0_0)やボリューム名を使った指定(ubi:rootfs)ができます。また、ファームウェア解析の場合など、書き込みが不要な場合は読み取り専用オプション(-o ro)を付けることを推奨します。これにより、誤って元のファームウェアイメージを変更してしまうことを防げます。

クリーンアップ

作業後やエラーでおかしくなった時には環境を元に戻しましょう。ドライバも不要になればアンロードしておきましょう。

sudo umount /mnt/ubifs
sudo ubidetach -m 0

## 不要ならアンロードする
sudo modprobe -r ubifs ubi mtdram

おまけ: なぜUBIFSを直接マウントできないのか

UBIイメージからUBIFSイメージを取り出すことにより直感的にはmount -t ubifs firmware.ubifs /mnt/ubifsでマウントできそうですが、このコマンドは失敗します。

これができれば面倒な手順が不要でマウントできると思ったのでうまくできる方法はないかをLinuxカーネルのソースコードを読んで調べてみました。

mountの解析

mount -t ubifs firmware.ubifs /mnt/ubifsの実行時に何が起こっているかstraceで確認してみます。すべてのトレースを表示すると多いため、ブログでは最低限必要な他のプログラムの実行とマウントの確認のためにexecveとmountシステムコールに絞ってトレースを取得しています。

$ sudo strace -e trace=execve,mount -f mount -t ubifs firmware.ubifs /mnt/ubifs
execve("/usr/bin/mount", ["mount", "-t", "ubifs", "firmware.ubifs", "/mnt/ubifs"], 0x7ffc00e400d8 /* 13 vars */) = 0
strace: Process 10597 attached
[pid 10597] execve("/sbin/mount.ubifs", ["/sbin/mount.ubifs", "/dev/loop0", "/mnt/ubifs", "-o", "rw"], 0x7ffc2166f9b8 /* 9 vars */) = 0
strace: Process 10598 attached
[pid 10598] execve("/bin/mount", ["/bin/mount", "-i", "-t", "ubifs", "/dev/loop0", "/mnt/ubifs", "-o", "rw"], 0x5bc7b5f805c8 /* 10 vars */) = 0
[pid 10598] mount("/dev/loop0", "/mnt/ubifs", "ubifs", 0, NULL) = -1 EINVAL (Invalid argument)
mount: /mnt/ubifs: wrong fs type, bad option, bad superblock on /dev/loop0, missing codepage or helper program, or other error.
       dmesg(1) may have more information after failed mount system call.
[pid 10598] +++ exited with 32 +++
[pid 10597] --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=10598, si_uid=0, si_status=32, si_utime=0, si_stime=0} ---
[pid 10597] +++ exited with 32 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=10597, si_uid=0, si_status=32, si_utime=0, si_stime=0} ---
+++ exited with 32 +++

注目したいのはmountシステムコールでマウント対象がfirmware.ubifsではなく/dev/loop0が渡されていることです。

mount(8) - Linux manual pageの「LOOP-DEVICE SUPPORT」セクションによると、ファイルシステムタイプが指定されていて、libblkidで認識される場合、通常のファイルからループデバイスを自動的に作成します。

blkidコマンドで確認するとlibblkidによりUBIFSイメージを認識していることがわかります。

$ blkid /tmp/firmware.ubifs
/tmp/firmware.ubifs: UUID="2a245116-dc07-4606-9a11-8f3cd01eb75b" TYPE="ubifs"

カーネル内UBIFSドライバーの処理

ここからはLinuxカーネル内でのUBIFSの処理を追跡していきます。UBIFSファイルシステムはregister_filesystem関数によってカーネルに登録されています。

Linux kernelのUBIFS実装 (Linux v6.15)を確認すると、ubifs_init関数でregister_filesystemを呼び出していることがわかります。

ubifs_fs_typeregister_filesystemによりファイルシステムとして登録される構造体です。ここで重要となるのはubifs_init_fs_contextで行われる初期化のうち、fc->ops = &ubifs_context_opsの設定です。ここで設定した構造体の.get_tree = ubifs_get_treeによりマウント可能なルートブロックの取得・作成します。

2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
static const struct fs_context_operations ubifs_context_ops = {
	.free		= ubifs_free_fc,
	.parse_param	= ubifs_parse_param,
	.get_tree	= ubifs_get_tree,
	.reconfigure	= ubifs_reconfigure,
};

static int ubifs_init_fs_context(struct fs_context *fc)
{
	struct ubifs_fs_context *ctx;

	ctx = kzalloc(sizeof(struct ubifs_fs_context), GFP_KERNEL);
	if (!ctx)
		return -ENOMEM;

	if (fc->purpose != FS_CONTEXT_FOR_RECONFIGURE) {
		/* Iniitialize for first mount */
		ctx->no_chk_data_crc = 1;
		ctx->assert_action = ASSACT_RO;
	} else {
		struct ubifs_info *c = fc->root->d_sb->s_fs_info;

		/*
		 * Preserve existing options across remounts.
		 * auth_key_name and auth_hash_name are not remountable.
		 */
		ctx->mount_opts		= c->mount_opts;
		ctx->bulk_read		= c->bulk_read;
		ctx->no_chk_data_crc	= c->no_chk_data_crc;
		ctx->default_compr	= c->default_compr;
		ctx->assert_action	= c->assert_action;
	}

	fc->ops = &ubifs_context_ops;
	fc->fs_private = ctx;

	return 0;
}

static struct file_system_type ubifs_fs_type = {
	.name    = "ubifs",
	.owner   = THIS_MODULE,
	.init_fs_context = ubifs_init_fs_context,
	.parameters	= ubifs_fs_param_spec,
	.kill_sb = kill_ubifs_super,
};
MODULE_ALIAS_FS("ubifs");

ubifs_get_tree関数内でopen_ubi関数を呼び出しています。

2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
static int ubifs_get_tree(struct fs_context *fc)
{
	struct ubi_volume_desc *ubi;
	struct ubifs_info *c;
	struct super_block *sb;
	int err;

	if (!fc->source || !*fc->source)
		return invalf(fc, "No source specified");

	dbg_gen("name %s, flags %#x", fc->source, fc->sb_flags);

	/*
	 * Get UBI device number and volume ID. Mount it read-only so far
	 * because this might be a new mount point, and UBI allows only one
	 * read-write user at a time.
	 */
	ubi = open_ubi(fc, UBI_READONLY);
	if (IS_ERR(ubi)) {
		err = PTR_ERR(ubi);
		if (!(fc->sb_flags & SB_SILENT))
			pr_err("UBIFS error (pid: %d): cannot open \"%s\", error %d",
			       current->pid, fc->source, err);
		return err;
	}

fc->sourceの文字列のパターンによって処理が異なります。最初のubi_open_volume_path以降は"ubi"から始まる文字列の処理しています。ubi0_0ubi0:rootfsなどの文字が含まれていたときの処理です。 今回は/dev/loop0になっています。

2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
static struct ubi_volume_desc *open_ubi(struct fs_context *fc, int mode)
{
	struct ubi_volume_desc *ubi;
	const char *name = fc->source;
	int dev, vol;
	char *endptr;

	/* First, try to open using the device node path method */
	ubi = ubi_open_volume_path(name, mode);
	if (!IS_ERR(ubi))
		return ubi;

	/* Try the "nodev" method */
	if (name[0] != 'u' || name[1] != 'b' || name[2] != 'i')
		goto invalid_source;

	/* ubi:NAME method */
	if ((name[3] == ':' || name[3] == '!') && name[4] != '\0')
		return ubi_open_volume_nm(0, name + 4, mode);

	if (!isdigit(name[3]))
		goto invalid_source;

	dev = simple_strtoul(name + 3, &endptr, 0);

	/* ubiY method */
	if (*endptr == '\0')
		return ubi_open_volume(0, dev, mode);

	/* ubiX_Y method */
	if (*endptr == '_' && isdigit(endptr[1])) {
		vol = simple_strtoul(endptr + 1, &endptr, 0);
		if (*endptr != '\0')
			goto invalid_source;
		return ubi_open_volume(dev, vol, mode);
	}

	/* ubiX:NAME method */
	if ((*endptr == ':' || *endptr == '!') && endptr[1] != '\0')
		return ubi_open_volume_nm(dev, ++endptr, mode);

invalid_source:
	return ERR_PTR(invalf(fc, "Invalid source name"));
}

続いて/drivers/mtd/ubi/kapi.cubi_open_volume_pathの処理をみましょう。

ubi_get_num_by_pathでパスからボリューム番号を取得する関数です。さらにこれ読んでみます。

325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
struct ubi_volume_desc *ubi_open_volume_path(const char *pathname, int mode)
{
	int error, ubi_num, vol_id;

	dbg_gen("open volume %s, mode %d", pathname, mode);

	if (!pathname || !*pathname)
		return ERR_PTR(-EINVAL);

	error = ubi_get_num_by_path(pathname, &ubi_num, &vol_id);
	if (error)
		return ERR_PTR(error);

	return ubi_open_volume(ubi_num, vol_id, mode);
}
EXPORT_SYMBOL_GPL(ubi_open_volume_path);

ハイライトされている305行目の!S_ISCHR(stat.mode)でキャラクタデバイスか確認し違うのであればこの関数は失敗します。UBIFSはキャラクタデバイス(例:/dev/ubi0_0)のみをマウントできる設計になっているため、ブロックデバイスである/dev/loop0では失敗するというわけですね。頑張っても単純な方法でマウントできなそうですね。

290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
int ubi_get_num_by_path(const char *pathname, int *ubi_num, int *vol_id)
{
	int error;
	struct path path;
	struct kstat stat;

	error = kern_path(pathname, LOOKUP_FOLLOW, &path);
	if (error)
		return error;

	error = vfs_getattr(&path, &stat, STATX_TYPE, AT_STATX_SYNC_AS_STAT);
	path_put(&path);
	if (error)
		return error;

	if (!S_ISCHR(stat.mode))
		return -EINVAL;

	*ubi_num = ubi_major2num(MAJOR(stat.rdev));
	*vol_id = MINOR(stat.rdev) - 1;

	if (*vol_id < 0 || *ubi_num < 0)
		return -ENODEV;

	return 0;
}

おわりに

UBIFSのマウントを雰囲気でやっててよくわかってないしやり方忘れるのでまとめるかーと軽い気持ちでやったら、知識が浅い概念の勉強や想定外の挙動引きまくる検証作業で沼にはまって泣きました。少しでも参考になれば幸いです。

参考文献一覧

  1. UBI - Unsorted Block Images
  2. UBIFS - UBI File-System
  3. UBI File System - The Linux Kernel
  4. UBIFS - Wikipedia
© 2016 - 2025 DARK MATTER / Built with Hugo / テーマ StackJimmy によって設計されています。