MIDIメッセージによるリモートコード実行
(psi3.ru)- 古い Yamaha PSR-E433 シンセサイザーの SWL01U チップをリバースエンジニアリングし、USB-MIDI SysEx メッセージだけで RAM にコードを書き込み、LCD に Bad Apple の映像を表示
- JTAG IDCODE
0x3f0f0f0fと OpenOCD/GDB による実験で、チップが ARM7TDMI コアのように動作することを確認し、内蔵 64KiB ROM と外部 16MiB フラッシュファームウェアをダンプ - ファームウェア内には MIDI SysEx 上で動作する 隠しシェルがあり、
loginとパスワード#0000の後にメモリ読み書きコマンドを利用できた - 任意メモリ書き込みコマンドで RAM に ARM コードを注入した後、スタックの戻りアドレスを上書きし、JTAG や UART なしで MIDI ファイルの再生だけでコード実行が可能に
- LCD 出力は CGRAM 制御、タスクテーブルのコピー、ディスプレイタスクの無効化、シェルコールバックの差し替えを経て、フレームあたりの転送量を 6732 バイトから 92 バイトに削減して改善
Yamaha PSR-E433 の内部調査
- 対象機器は長年使っていた Yamaha PSR-E433 シンセサイザーで、メインボードには
DMLCDの表記とともに、2 個のフラッシュチップ、RAM チップ、YAMAHA SWL01Uチップがあった - SWL01U に関する公開情報はほとんどなく、オンラインで見つけた記事のひとつだけが SuperH CPU コアベースかもしれないと述べていた
- 類似モデルである E443 のサービスマニュアルには SWL01U のピン配置があり、
TESTN、PROTN、2 つの双方向 UART、JTAG テストポイントが示されていた - 初期のアプローチは 4 つだった
TESTN、PROTNピンを操作して起動モードの変化を確認- UART Tx ピンにはんだ付けして出力を確認
- JTAG でチップ識別コードを読み取る
- フラッシュチップを取り外してファームウェアをダンプ
TESTNを有効にするとシンセサイザーは起動せず、PROTNは動作に変化を与えなかった- 未使用に見える UART Tx ピンへ直接はんだ付けしたが、
TESTN/PROTNの 4 通りの組み合わせすべてで出力はなかった
JTAG で明らかになった ARM7TDMI の動作
- JTAG はベンダーごとの実装に応じて詳細な回路説明が必要だが、ほぼすべてのデバイスが対応する IDCODE の読み取りを OpenOCD でまず試した
- OpenOCD は IDCODE を
0x3f0f0f0fと報告し、この値は STMicroelectronics STR7xxx や Atmel SAM7xxx 系列のような ARM7 ベースのマイクロコントローラーと関連し得るように見えた - SWL01U を OpenOCD の
arm7tdmiターゲットとして指定すると正常に接続され、ハードウェアに 2 個の breakpoint/watchpoint ユニットがあると表示された - GDB で実行を停止・再開すると、ボード電流が予測可能に変化した
- 実行中は約 115mA
- 一時停止中は約 98mA
- この電流変化は、実際に ARM7TDMI コアを停止・再開しているという強いシグナルになった
ROM とフラッシュファームウェアのダンプ
- ARM7TDMI の文書によると reset vector はアドレス
0にあり、GDB でアドレス0を読むとldr pc, [pc, #24]形式のジャンプ命令が見えた - アドレス
0から 16MiB をダンプして Cutter で開いたが、文字列が 64KiB ごとに繰り返されていた- 例:
SWL01U Internalが0x0000bfd0、0x0001bfd0、0x0002bfd0などに繰り返し出現
- 例:
- 繰り返しパターンと文字列から、このダンプは外部フラッシュではなくチップ内蔵メモリだと判断され、SWL01U には 64KiB ROM があると整理された
- reset vector のジャンプ先は
0x02000000で、このアドレスから再び 16MiB をダンプすると繰り返しはなかった - 外部フラッシュのダンプには、シンセサイザー使用中に見られる文字列が含まれていた
GrandPnoTr1 will be OverWritten!BogiWogi
- 確認されたメモリ配置は次のとおり
- 内蔵 ROM:
0x00000000、64KiB - 外部フラッシュ:
0x02000000、16MiB - 起動時、ROM はただちに外部フラッシュへ制御を渡す
- 内蔵 ROM:
Ghidra で見つかった隠しシェル
- Cutter だけでは解析が不十分だったため Ghidra に切り替え、文字列と xref をたどりながらファームウェア構造を把握した
help、?、info、verなどの文字列が近いアドレスに集まっており、各文字列はコマンド名と関数ポインタのペアに見える配列へつながっていた- コマンド処理関数はステートマシン形式で、
login文字列とPasswd Error文字列から、ログイン手順を持つシェルであることを確認した - シェル入力処理は 256 バイトのリングバッファを巡回しながら文字単位で処理し、
\r文字に出会うとコマンドを実行する構造だった - ログインの流れは次のとおり
login入力時にpasswd?を出力- パスワード
#0000入力時にlogin OK - 以後、コマンド実行が可能
- 確認されたシェルコマンドには次が含まれる
logout、help、?、info、verstack、perf-on、perf-off、perf-dispd、dp、d xxxxx、d/s xxxxxm ADDRESS DATA、m/b ADDRESS DATA、m/w ADDRESS DATA、m/l ADDRESS DATA
infoコマンドは次の情報を返したDevelopName PSR-E433DevelopNumber #3341Main DevelopNumber #3341Make data & time MAY 16 2012 19:00:57J/E Select English
USB-MIDI SysEx 上で動作するシェル
- シェルの出力関数は各バイトを上位/下位 4 ビットニブルに分けて別々のバイトに格納し、前後に固定ヘッダーと footer を付けていた
>プロンプトに対応するパケット構造は次のとおり- ヘッダー:
F0 43 73 01 52 19 00 00 - payload:
03 0E 02 00 - footer:
F7
- ヘッダー:
- MIDI SysEx メッセージは
0xF0で始まり、メーカー ID と payload を経て0xF7で終わり、payload には MSB が 0 のバイトだけを入れられる - ヘッダーの
0x43は Yamaha のメーカー ID で、シェルのパケット構造は Yamaha SysEx メッセージ形式と一致していた - シンセサイザーの USB ディスクリプタには MIDI インターフェースだけがあり、別のシリアルポートはなかった
- Python スクリプトでターミナルとシェルプロトコルの間を変換すると、USB-MIDI 経由でシェルと対話できた
MIDI Shellcode によるコード実行
- シェルの
m/l AAAAAAAA DDDDDDDD\rコマンドは 32 ビットメモリ書き込みを行い、アドレスとデータを 16 進 ASCII で渡す - 4 バイトの payload を書く過程でも、実際の転送量は大きく膨らんだ
- コマンドバイトはそれぞれ 2 個の 4 ビットニブルバイトに変換される
- SysEx メッセージに 9 バイトが追加される
- 3 バイトごとに 4 バイトの USB-MIDI パケットで包まれる
- 4 バイト書き込みのためにシンセサイザーへ 72 バイトを送る必要がある
- エコーとプロンプトまで含めると合計 396 バイトが行き来する
- 未使用に見える RAM 領域を探して ARM アセンブリコードを配置し、スタックの戻りアドレスを上書きしてそのコードを実行した
- 最初の payload はファームウェア内部の文字列出力関数を呼び出し、LCD の 8 文字テキスト領域に
HeloWrldを表示した - この方式は JTAG や UART なしでも動作し、メッセージを MIDI ファイルに入れて再生するだけで実行できた
- PSR-E433 ファームウェア 1.02 用の MIDI ファイルも提供されたが、別の Yamaha デバイスや異なるファームウェアバージョンの PSR-E433 で再生すると予測不能な動作をする可能性があると警告している
Bad Apple を LCD に表示する
- Yamaha PSR-E433 の LCD コントローラーは ML9040A で、基本的にはドットマトリクスのテキスト文字を処理する構造だった
- LCD にはドットマトリクス領域だけでなく、音符表記、7 セグメント領域、コード表記、下部のキーボード表示領域もあった
- ML9040A には 3 種類のメモリがあった
- DDRAM: 表示する文字データをホストが記録
- CGROM: 文字コードをグラフィックパターンに変換
- CGRAM: ホストが最大 8 個のユーザー定義文字を定義
- ファームウェアは CGRAM を操作してドットマトリクス下の非テキスト表示要素を制御しており、この経路を使ってユーザー定義グラフィックを表示できた
- 任意データを LCD コントローラーへ送るファームウェア関数を見つけ、CGRAM にチェックパターンを載せたが、ファームウェアが CGRAM を継続的に更新するためすぐ上書きされた
RAM 操作でディスプレイ更新を制御
- フラッシュを直接上書きするとデバイスを文鎮化する危険があるため、すべての実験は電源再投入で元に戻せるよう RAM 操作に限定した
- ファームウェアには原始的な RTOS のように見える構造があり、フラッシュには 64 個のタスクのコールバック関数、スタック、属性を定義するグローバルテーブルがあった
- 起動時にフラッシュファームウェアが ROM にタスクテーブルの位置を知らせ、ROM はその位置を内蔵 SRAM のグローバル変数に保存した
- タスクテーブルを RAM にコピーした後、ROM が新しいテーブルを使うように変更すれば、フラッシュを修正せずにタスクコールバックを差し替えられた
- ディスプレイ更新タスクのコールバックをデフォルトの idle コールバックに変更し、ファームウェアが CGRAM を継続的に上書きできないようにした
転送効率と画面崩れの改善
- Bad Apple の最初の実装は動作したが、転送効率が低いためフレームレートが非常に低く、画面アーティファクトがあった
- フレームごとに CGRAM 64 バイトと 32 ビット戻りアドレスの上書きだけを送っても、実際の転送量は 70 バイトの payload あたり 6732 バイトだった
- 低効率の主な原因は 2 つだった
- データをシェルコマンド形式で包む必要がある
- シンセサイザーがコマンドを文字ごとに大きな SysEx パケットでエコーする
- シェルタスクのコールバックを自作コールバックに差し替えて raw データを受け取り、応答しないようにすると、コマンドのラッピングとエコーのオーバーヘッドを取り除けた
- 追加のパッキング最適化後、フレームあたりの転送量は 6732 バイトから 92 バイトに減り、73 分の 1 になった
- 残ったアーティファクトは、LCD 通信とパネルボタン/LED スキャンが同じ 8 本の GPIO ラインを共有しているために発生した
- 最終実装では LCD に直接書き込まず、LCD/パネル多重化タスクがパネルスキャンを終えた後に目的のデータを送るよう要求することで、画面崩れを回避した
最終的な動作手順と残る解析対象
- MIDI で LCD 映像を表示する最終手順は次のとおり
- シェルにログイン
- シェルのメモリ書き込みコマンドで実行コードを RAM に記録
- スタックの戻りアドレスを上書きして RAM コードを実行
- タスクテーブルを RAM にコピー
- 新しいタスクテーブル同士が相互に参照するよう修正
- ROM が新しいタスクテーブルを使うよう変更
- ディスプレイタスクコールバックをデフォルトの idle コールバックに差し替え
- シェルタスクコールバックを独自コールバックに差し替え
- 独自コールバックで MIDI データをアンパックし、LCD/パネル多重化タスクへ渡す
- MIDI で映像フレームを供給
- SWL01U の MMIO 領域の理解はまだ限定的であり、メイン ARM コアとは別の DSP も残る解析対象である
- 関連資料
1件のコメント
Hacker News のコメント
SuperH は Sega 32X、Sega Saturn、Sega Dreamcast にも搭載されており、HP Jornada のような初期の Pocket PC の一部にも使われていた
ただし、Pocket PC の大半は ARM ベースだった
前提がばかげているほどなのに、実際にやり遂げているのが驚き
「もうひとつのパッキング最適化」に触れていたが、フレームをどう送信しているのか気になる
ドットマトリクスが 7x5 文字 8 個なら合計 280 ビット、つまりフレームあたり 7 ビットのグループが 40 個だが、送信にはその倍の容量を使っているように見える
制御データのせいで無駄になっているのか、それとも送信方式が少し非最適なのか気になる
ここにパケットヘッダーとフッターの 9 バイトが追加される
記事には 92 と書いていたようだが、計算を間違えたらしい
7 ビット全体を使う方法を見つけるのが難しすぎたので、元の方式と比べれば最適解よりごくわずかに悪い程度の解法を選んだ
正確なアルゴリズムが気になるなら、まだ整理されていないコードだがこれらのファイルを見るとよい: https://github.com/portasynthinca3/swl01u/blob/master/fun/bi..., https://github.com/portasynthinca3/swl01u/blob/master/fun/ba...
「リバースエンジニアリングはあまり経験がない」というのがこのレベルなら、残りの人たちはいったいどのあたりにいるのかわからない
レベル1は新しく熱意はあるが、まだ知らないことをわかっている状態、レベル2は「私は神だ」、レベル3は「私はバカだ」の段階
「世界初の MIDI シェルコード」と言っているが、主要プラットフォームの大半では MIDI シェルコード は 20 年以上前から存在していた: https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=midi
当然 SysEx だった
標準 MIDI における SysEx は、Python におけるインラインアセンブラのような存在
ほぼすべての MIDI 機器の中には、文書化されていない独自のものが潜んでいる
MIDI キーボードを挿して Am6,9/G# を弾いたら root 権限のターミナルウィンドウが開くなら、本当にかっこよさそう
最近 MIDI ファジングをするには何が必要か調べていて、そのために .mid ファイルを生成する資料は見つけたが、求めていたものとは少し違っていた
代わりにこちらを探索してみる価値がありそう
最近のシンセサイザーがだんだん使わなくなっているようでかなり残念、特に Roland
それでも Behringer はまだかなりよく対応している
たとえば Deepmind は MIDI CC の範囲もすでに良いが、SysEx でほぼ 100% プログラム可能
驚くべき研究
実際の DNA/RNA 分子 にシェルコードを合成して、DNA シーケンシング装置でリモートコード実行を示した 2017 年の研究を少し思い出した: https://www.usenix.org/conference/usenixsecurity17/technical...
「次は OSC か」と言おうとしたが、まだ MIDI が支配的なようだ
全文を読むことを勧めるが、要点となる文はこうだと思う
「この[キーボードメーカー]の狂った人たちは、USB 上の MIDI SysEx メッセージ上で動くシェルを作っていた」
「最も興味深いコマンドは 任意メモリ読み書き コマンドだ。望むなら MIDI でシンセサイザーのメモリを覗いたり突いたりできる」
「望むなら、これらのメッセージを MIDI ファイルに書き込んで、ほかの MIDI ファイルと同じようにシンセサイザーで再生できる。おや、いい考えが浮かんできたぞ…」
「ファームウェアを掘り続けた数多くの眠れない夜の末に、任意データを LCD コントローラーへ送る関数を発見した」
ある意味では IoT の悪夢 を少し垣間見るようなもの
ほとんどどんな機器にもバックドアがあり得るし、場合によっては #0000 のような間抜けなバックドアでさえあり得る
パケットロス が起きるのは完全に正常
Bad Apple のコマンドの合間に MIDI 音楽 を挟み込んで、音声も自前で再生させられるのか気になる
リポジトリの README には イメージダンプ が入っていると書かれているが、実際にはない
これで正しい状態なのか気になる
最後の瞬間にアセンブル済みのコード片を除外しようとして
*.binを.gitignoreに追加したところ、ダンプも一緒に除外されたらしい数時間以内にアップロードする予定
そのダンプは Yamaha の著作権 で保護されている可能性があるので、むしろ良い判断かもしれない
もし HN 読者の中に Armenia にいる人がいれば、Porta が 1 月 10 日に Hacker Embassy でこのテーマについて発表する
ぜひ来てほしい: https://t.me/hackerembassy/17