はじめに:問題の背景
Redmi版XiaoAIスピーカー(モデルLX07)をカスタマイズする際、大きな壁に直面した。最新ファームウェアではTTLシリアルコンソールの認証方式が変更され、従来のパスワード推算手法が完全に無効化されていたのだ。しかし、デバイスがOpenWrt風のデュアルシステム構成を採用していることを発見し、U-Bootから旧システムへ切り替えることで突破できた。以下に、ハードウェア調査からファームウェア改変、SSH永続化までの全過程を記す。
ハードウェア側の準備:シリアルデバッグインターフェース
ケースの開封とTTLピンの特定
底面の滑り止めパッドの下にネジが隠されている。これを取り外すと底蓋が外れる。メインボード左下部に4つの金メッキ穴があり、これがTTLデバッグポートだ。ピン配置は左から順にGND、TX、RX、VCCとなっている。
シリアル接続の確立
CP2102またはCH340チップを搭載したUSB-TTL変換モジュールを使用する。配線は以下の通り:
- GND → GND
- スピーカー側TX → 変換モジュールRX
- スピーカー側RX → 変換モジュールTX
- 通信速度:115200bps
起動時のコンソール出力
電源を投入するとシリアルターミナルに大量のブートログが出力される。工場出荷時のバージョン1.52.7では、TTLコンソールにパスワード認証がなく、Enterキーを押すだけでrootシェルが得られた。
ROM Type:release / Ver:1.52.7
root@mico:/#
だが、ネットワークに接続して自動更新が走るとバージョン1.58.13以降になり、認証が要求されるようになる。
新ファームウェアにおける認証方式の変化
従来のパスワード生成方式
これまで広く知られていた手法は、デバイスのシリアルナンバー(SN)と固定 salt を組み合わせてMD5ハッシュを計算するものだった。具体的には、mi_console snコマンドでSNを取得し、所定の salt文字列と結合した上でMD5を取るとパスワードが得られる。
例として、ある個体のSNと saltの組み合わせは次の通り:
26215/A9XH218225775B10D-15C0-7827-97B9-88EA07FCA97A
→ 8ec4e5fdc9255c
しかしバージョン1.58.13以降では、この方式が全面的に見直され、ランダム化または新アルゴリズムによる動的パスワードに変更された。既存の計算ツールは一切通用しなくなった。
Failsafeモードの限界
起動直後にfキーを押すとFailsafeモードに入れるが、このモードではルートファイルシステムが読み取り専用でマウントされており、パスワード変更やファイル書き込みができない。
Press the [f] key and hit [enter] to enter failsafe mode
root@(none):/etc# mount
/dev/mtdblock5 on / type squashfs (ro,noatime)
デュアルブート構成の発見
OpenWrt風のパーティションレイアウト
ブートログとMTDパーティション情報を詳細に解析した結果、このデバイスがOpenWrtと同様のA/Bシステムバックアップ機構を持つことが判明した。
Creating 6 MTD partitions:
0x000000800000-0x000001000000 : "tpl"
0x000001000000-0x000001600000 : "boot0"
0x000001600000-0x000001c00000 : "boot1"
0x000001c00000-0x000004400000 : "system0"
0x000004400000-0x000006c20000 : "system1"
0x000006c20000-0x000008000000 : "data"
system0(mtdblock4)とsystem1(mtdblock5)は独立した2つのルートファイルシステムであり、dataパーティションはユーザーデータを永続化する。U-Bootの環境変数boot_partによってどちらから起動するかが決まる。
U-Bootからのブート対象切り替え
TTLコンソールで起動時にEnterを押して自動ブートを中断し、U-Bootプロンプトに入る:
# 現在のブート対象を確認
=> printenv boot_part
boot_part=boot1
# system0への切り替えを指示
=> setenv boot_part boot0
=> saveenv
=> reset
再起動後、旧バージョン(1.52.7)が起動し、TTLパスワードが再び計算可能になった。
動作原理
OTA更新時、新ファームウェアは現在使用中ではない方のパーティションに書き込まれる。boot_partを書き換えることで、もう一方のパーティションから強制起動できる。本来はOTA失敗時のロールバック用だが、これを利用してダウングレードを実現した。
現在のシステム状態確認とバックアップ
マウント状況の確認
root@mico:/# df -h
/dev/mtdblock4 34.5M 34.5M 0 100% /
/dev/ubi0_0 13.3M 11.4M 1.2M 91% /data
現在system0から起動していることが確認できる。
パーティションのバックアップ
書き戻し用に元のイメージを保存する。/tmpはRAMディスクで容量に余裕があるため、ここに一旦保存する。
cd /tmp
dd if=/dev/mtdblock4 of=rootfs_orig.bin
# ネットワーク経由でPCへ転送
scp rootfs_orig.bin user@192.168.1.100:/home/user/speaker_backup/
転送後、PC側でMD5を照合し、バックアップの完全性を検証すること。WinSCPなどのツールを使うとリモートファイル操作が容易になる。
Ubuntu環境でのファームウェア改変
ファイルシステム構造の確認
バックアップしたイメージファイルはSquashFS形式の読み取り専用ルートファイルシステムである。
unsquashfs -s rootfs_orig.bin
Found a valid SQUASHFS 4:0 superblock.
Block size 131072
Compression xz
重要なパラメータは2点:ブロックサイズ131072、圧縮方式xz。再パッキング時にこれらを一致させる必要がある。
イメージの展開
mkdir extracted_rootfs
unsquashfs -d extracted_rootfs rootfs_orig.bin
変更内容:SSH永続化と自動更新の無効化
Dropbear用ホスト鍵の生成
旧システムのTTLコンソールで事前に鍵を生成しておく:
dropbearkey -t rsa -f /tmp/dropbear_rsa_host_key
cat /tmp/dropbear_rsa_host_key
起動スクリプトの改変
/etc/rc.localのexit 0より前に以下を追記する:
# SSHデーモンを起動
dropbear -r /data/dropbear_rsa_host_key
# ユーザー定義の初期化スクリプトを実行
/data/custom_init.sh
自動OTAの停止
/etc/crontabs/rootを編集し、OTA関連のエントリをコメントアウト:
#* 3 * * * /bin/ota slient
カスタム初期化スクリプトの準備
#!/bin/sh
# MQTTインターセプターやローカル音声制御などの拡張を記述可能
echo "custom init started" > /tmp/custom.log
再パッキング
元のイメージと同一のパラメータでパッキングする:
mksquashfs extracted_rootfs rootfs_mod.bin -b 131072 -comp xz -no-xattrs
パッキング後のファイルサイズは元のパーティションサイズ(約34.5MB)を超えてはならない。超過する場合は不要な音声パックやドキュメントを削除して調整する。
改変ファームウェアの書き戻し
ファイルの転送
cd /tmp
scp user@192.168.1.100:/home/user/speaker_backup/rootfs_mod.bin .
パーティションへの書き込み
# system0へ書き込み
mtd write rootfs_mod.bin system0
# ブート対象をsystem0に設定
/usr/bin/fw_env -s boot_part boot0
# 再起動
reboot
動作検証
再起動完了後、PCからSSH接続を試行:
ssh root@192.168.31.114
問題がなければパスワード無しでrootログインできる。
実環境でのバックアップ手法に関する補足
Linuxベースの組込デバイスでは、書き込み可能な空き容量が極端に少ない場合がある。ストリーミング転送を使うとエスケープ文字の問題でデータ破損が起きやすい。以下に実践した安全な手法を示す。
まず全パーティションのチェックサムを取得:
root@L07A:/tmp# md5sum /dev/nand0p* > /tmp/all_checksums.md5
root@L07A:/tmp# cat all_checksums.md5
1496c41eab6b5aeaf01c810ac7e9d7b6 /dev/nand0p1
f5cefce2887add477aebd968846b0c9d /dev/nand0p2
6783edf56cfe491c4d8fce05fa707c99 /dev/nand0p3
a308df82046cd8e0a34cc691159368f4 /dev/nand0p4
54cb1eec77235f0d01dded13fcc8416a /dev/nand0p5
8b906fe47857fdd2454c2d4feaec1de5 /dev/nand0p6
fae596420e74ddf134a43b4341fdd590 /dev/nand0p7
09a1d434dbd7197e7c3af8a7c28ca38b /dev/nand0p8
e5d798dc2d341b9f2ae8063b936e2839 /dev/nand0p9
SSH経由でddを使用し、パーティションを分割取得する:
ssh -oHostKeyAlgorithms=+ssh-rsa -oMACs=+hmac-sha1 root@192.168.31.114 "dd if=/dev/nand0p2 bs=1M" > kernel1.img
ssh -oHostKeyAlgorithms=+ssh-rsa -oMACs=+hmac-sha1 root@192.168.31.114 "dd if=/dev/nand0p6 bs=1M" > misc.img
大きなパーティションは分割して転送する:
dd if=/dev/nand0p3 of=/tmp/rootfs_seg1.bin bs=1K count=9216
dd if=/dev/nand0p3 of=/tmp/rootfs_seg2.bin bs=1K count=9216 skip=9216
dd if=/dev/nand0p3 of=/tmp/rootfs_seg3.bin bs=1K count=9216 skip=18432
dd if=/dev/nand0p3 of=/tmp/rootfs_seg4.bin bs=1K count=9216 skip=27648
PC側(PowerShell)でハッシュ検証後、結合する:
Get-FileHash -Algorithm MD5 rootfs_seg1.bin
# cmdでバイナリ結合
copy /b rootfs_seg1.bin + rootfs_seg2.bin + rootfs_seg3.bin + rootfs_seg4.bin rootfs1.img
カーネルコマンドラインからパーティション構成を確認できる:
root@L07A:~# cat /proc/cmdline
earlyprintk=sunxi-uart,0x05000000 console=ttyS0,115200 root=/dev/nand0p3 rootwait init=/sbin/init rdinit=/rdinit loglevel=1 partitions=env@nand0p1:kernel1@nand0p2:rootfs1@nand0p3:kernel2@nand0p4:rootfs2@nand0p5:misc@nand0p6:private@nand0p7:crashlog@nand0p8:UDISK@nand0p9 gpt=1 rotpk_status=1 uboot_version=10 env=1 sboot=1
セクタ開始位置とハードウェアセクタサイズの確認:
root@L07A:~# cat /sys/block/nand0/nand0p1/start
1024
root@L07A:~# cat /sys/block/nand0/queue/hw_sector_size
512
トラブルシューティング
改変ファームウェア起動不可
- ファイルサイズがパーティション容量を超過していないか確認(
df -h) - 再パッキング時のブロックサイズ・圧縮方式が元イメージと一致しているか確認
- まずバックアップした元イメージを書き戻し、フロー全体が正しいか検証する
U-Bootプロンプトに入れない
- 起動直後のタイミングで素早くEnterを押す必要がある
- 機種によっては
Ctrl+Cや他のキーシーケンスが必要な場合がある
SCP転送が失敗する
- スピーカーとPCが同一ネットワーク上にいるか確認
- デバイス側にSCPサーバーがない場合は、PC側でHTTPサーバーを起動し
wgetで取得する方法もある - シリアル経由のdd転送も可能だが極めて遅いため推奨しない
技術的要点のまとめ
デュアルシステム機構
OpenWrt系組込デバイスで一般的な設計であり、これを利用してダウングレードや復旧が可能。U-Boot環境変数boot_partがブート対象の切り替えを制御する。
SquashFS改変フロー
バックアップ → unsquashfsで展開 → 内容改変 → mksquashfsで再パッキング → mtd writeで書き戻し、という一連のプロセスが標準的なアプローチ。
永続化の戦略
カスタムスクリプトは/dataパーティションに配置し、rc.localから呼び出すことで、OTA更新後も変更が残りやすくなる。
セキュリティ上の考察
メーカーによるパスワードアルゴリズムの更新は妥当なセキュリティ強化だが、デュアルシステム構成が意図せずダウングレード経路を残してしまった。デバイス全体のアーキテクチャ(ブートローダー、パーティション構成、ファイルシステム)を理解することが、単なるツールの使用よりも重要である。
拡張の可能性
- MQTTインターセプターによるローカル音声制御の実装
- ウェイクワードの差し替えやサードパーティサービスの自動起動
- ハードウェアに適合したカスタムOpenWrtファームウェアのビルド
コマンドリファレンス
| 目的 | コマンド |
|---|---|
| MTDパーティション確認 | cat /proc/mtd |
| マウント状況確認 | df -h |
| ブート対象切替(U-Boot) | setenv boot_part boot0 |
| パーティションバックアップ | dd if=/dev/mtdblock4 of=/tmp/rootfs_orig.bin |
| パーティション書き込み | mtd write rootfs_mod.bin system0 |
| SSH鍵生成 | dropbearkey -t rsa -f /data/dropbear_rsa_host_key |
| SSHデーモン起動 | dropbear -r /data/dropbear_rsa_host_key |