自己紹介
末田卓巳氏(以下、末田):「リセットとフリーズで解析する電子辞書リバエン記」というタイトルで発表します。
自己紹介から始めます。末田卓巳と申します。インターネット上では、puhitakuという名前で活動しているフリーの開発者です。NICT(情報通信研究機構)で、リバースエンジニアリングをやったり、アメリカの住宅ベンチャーを手伝ったり、いろいろな開発をやっています。詳細は私のTwitterの固定ツイートに載っているので、ぜひご覧ください。
Windows CEを搭載した電子辞書「SHARP Brain」
「SHARP Brain」を知らない方もいると思うので、まずはその説明をします。SHARP Brainは、SHARPが発売しているWindows CEを搭載した電子辞書です。公式に販売されているアプリのほか、自分でWindows CE向けのexeをビルドや追加して動かせます。
ハードウェアにはいくつか世代があります。最も古いものだとTOSHIBAのSoCが載っていて、Windows CEが動いています。その次に古いものは、NXPのi.MX283が載っていてWindowsが動いています。一番新しいものが今回のトークの目玉です。ちなみに、このi.MX283に載っているものは、Linuxのポーティングを私がやりました。2019年から2020年にかけてLinuxを動かすことに成功しました。
そのあと、勢いに乗じてコミュニティを立ち上げ、有志の努力と協力によってi.MX283を搭載している機種のすべてにポーティングを拡大できました。結果的に、SDにRaspberry Piと同じようにddで書くだけで、誰でもLinuxをBrainで動かせるようになり、現在もリリースされています。誰でもダウンロードすることができます。
SoCがNXPのi.MX7ULPに変わっていたBrainの新モデル
本題に入ります。去る2021年1月に公式からBrainの新しいモデルの情報が公開されました。
見た目や機能が少し変化しただけっぽいなと思って公式のアプリストアを見ていたら、旧モデルのアプリは新モデルでは動かない、互換性がないという通知が出て、なんだ?と思いました。
OSが変わったのではないかとか、SoCが変わったのではないかとか、コミュニティの中でいろいろ話題になりましたが、中身を見ないことにはわからないので、発売日当日に秋葉原のヨドバシカメラに行って、買って、即バラしました。
中身を見てみるとSoCがNXPのi.MX7ULPに変わっていました。
これはCortex-A7のコアとCortex-M4のコアが入っている、Androidも動かすことができるような強力でヘテロジニアスなSoCです。
ここに、試しに旧機種向けのアプリを入れてみましたが、当然認識されませんでした。OSで何が動いているのかもまったくわからないまま、ここで自作のバイナリを動かしたいという話になって、解析をスタートしました。
自作バイナリを動かす実験はわりとあっさり成功
ここから、自分で書いたバイナリを動かす話をします。公式が本体のあとにアプリを発売したので、さっそく購入して、実行ファイルをobjdumpで読んでみました。すると、先頭0バイト目からArmの機械語がズラズラと並んでいました。
先頭から機械語が並んでいるものをぶち込めばいいなら自分でアセンブリを書けば動くのではないかと思い、(スライドを示して)画面に映っているような、無限ループするだけのコードをasでELFにして、.textセクションを切り出して実機に転送しました。
すると、アプリを呼び出すメニューを押したまま静止して、ほとんど反応しない状態になりました。
これが何を意味しているのか。一見フリーズしたように見えますが、画面を開閉すると、すごくもっさり反応するんですね。つまり、完全にはフリーズしていないけれど、resource hog状態になっている、つまり無限ループが効いていることがわかります。というわけで、自作バイナリを動かす実験はわりとあっさり成功しました。
即returnすると緑の画像が表示され、0x00000000にジャンプするとリセットされた
続けて、ほかに何ができるのかを探そうという話になり、スタートに入った直後に即行で呼び出し元にreturnするコードを書きました。これを実行すると緑っぽい謎の画像が出るようになりました。
さらに、仮想アドレスでは明らかにおかしい0x00000000にジャンプするコードを動かしたところ、本体のFault Handlerが反応しているのだと思いますが、本体のリセットがかかることもわかりました。
まとめると、無限ループするとほとんどフリーズ。即returnすると謎の画像、0x00000000にジャンプするとリセットという3つの動作です。一見まったく実用性がありませんが、この3つの動作が実現できました。
U-Bootを動かすために達成しなければならない2つの条件
そこで、自作のバイナリで動くのであればU-Bootを動かしたい、U-Bootを動かしてLinuxを動かしたいと思ったので、U-Boot起動の可能性を探っていきました。
U-Bootを動かすために達成しなければならない条件は、1つ目がMMUとかの割り込みが無効化できる、つまり特権モードが使えること。2つ目は、MMUのいない物理アドレス空間でU-Bootにジャンプできることです。両方とも実際に実験をしないとまったくわかりませんが、どうやって実験するのかさえわかりませんでした。
というのも、デバッグに使えるI/Oが1つもなかったんですね。LCDはありますが、フレームバッファのアドレスがわからないし、当然どのOSが動いているかわからないので描画APIもわからない。
JTAGは、一部はピンとして出ていますがI/Oのマルチプレクサがあって、この時点では叩けませんでした。現在でも叩けておらず、いずれにしても難しい。さらにUARTは使えないかと思いましたが、物理的にTXが出ていることを発見しました。Bootログなどは出ていますが、肝心のUART部のアドレスがわからないので何も出力できない。これはどうしようかなと悩んでいたところ、ここでアイデアが降ってきました。
MMUは無効化できるのか?
待てよ? 無限ループと謎の画像とリセットができるのであれば、これを出し分けることができれば少なくとも1ビットの情報を得られるのではないか、というアイデアに至り、さっそく解析を実行しました。
1つ目は、MMUを無効化できるか。Armの場合、MMUを無効化するにはmrc命令でSCTSRというレジスタを読みます。そのあとMMUのビットをクリアしてmcr命令で書き戻します。SCTSRのアクセスには特権モードが必須なので、ユーザーモードでこれを読み書きしようとすると未定義命令例外が発生して、今回のBrainの場合はリセットされます。
つまり、mrcでSCTSRを読んでみて、成功したら即行returnするようなコードを書きます。すると、mrcに成功したら謎の画像が出て、失敗したらリセットされるという動作を見ることができます。これを試してみると、なんと謎の画像が出ました。mrcの時点では死なず、mrcでSCTSRを読むことに成功したんです。つまり、ユーザーが追加したアプリが実は特権で動いていることが判明しました。
U-Bootにジャンプできるのか?
次にU-Bootにジャンプできないと意味がないので、これを探っていきます。ジャンプするために必要なものは何か。U-Bootにジャンプする時は、どうあがいてもMMUは無効です。なので、MMUが有効になっている仮想アドレスが効いている状態で、U-Bootのメモリに置いて、そのあと物理アドレスでジャンプしなくてはいけません。
そのため、仮想アドレスと物理アドレスの対応を事前に見つけておく必要があります。このために3つの実験を行いました。実験1ではBrainで実行できる最大のバイナリサイズを調べます。そして、DRAMの物理アドレスを実験1で調べたサイズごとに区切っていき、実験1のバイナリに着地できる地点がどこかを大まかに探していきます。
着地できるところが大まかにわかったら、今度はその周囲を64KBごとに区切って二分探索をして、どこに着地するかを確定するというのをやってみました。
実験1 バイナリの最大サイズを調べる
(スライドを示して)では、実験1をやっていきます。バイナリの最大サイズを調べますが、まず前半がほとんどNOPで埋まっている、末尾にmov pc, lrと書いてあるバイナリを用意します。このバイナリを実行すると最終的に謎の画像が出ますが、NOPの部分を伸ばすことによってバイナリをいくらでも大きくできるんですね。
アプリメニューから起動して正常に謎の画像が出る最大の長さを探っていくと、自作バイナリとして実行できるのは15MBであることがわかりました。DRAMは容量が128MB乗っているので、その11.7パーセントに相当します。けっこう大きくて、思わぬ誤算というか、うれしい結果になりました。これによって、11.7パーセントの領域を自分のバイナリで埋め尽くすことができることがわかりました。
実験2 物理アドレス空間で自作バイナリがどこにあるのか大まかに調べる
(スライドを示して)実験2では、この15MBの領域に行くにはどこに飛べばいいのかを大まかに調べます。今度はバイナリに少し手を加えます。冒頭の部分にMMUを切って物理アドレスに飛ぶというコードを置いた上で、その64KB刻みで「NOP」と「リセットするコード」を繰り返すコードをたくさん並べていきます。
そして、バイナリの全体が先ほど見つけた最大の長さと同じになるようにします。MMUを切ってバイナリの上にジャンプすると、NOPの上を滑ってリセットすることがわかります。なぜ64KBごとに切っているかというと、メモリの上に自作バイナリを乗せてMMUを切るとバラバラになるかもしれないからです。
実際にその仮想メモリでは、連続でも物理メモリではバラバラかもしれないし、連続しているかもしれません。それはわかりませんが、バラバラだった時に対応する必要があったので、ラージ・ページのサイズになりますが、64KB刻みでやっていきました。
この破片がバラバラなっている中で、先ほど見つけたバイナリの最大サイズごとに、ど真ん中にジャンプしていくと、どこかの破片の上に着地することができるという仮説が立ったので、あとはこれを見つけることができれば確定します。結果としては、0x67800000というDRAMの末端から15MBの領域を取って、そのど真ん中に飛ぶと自作バイナリに着地できることが判明しました。
これで大まかに領域がわかったので、次は詳細に飛んでみます。これで完全に、さっきのアドレスに飛んだ場合に正確にこの位置に着地することがわかります。
実験3 物理アドレス空間で自作バイナリに着地する箇所を二分探索で特定する
(スライドを示して)実験3ではバイナリに手を加えます。さっきの64KBごとに区切るのは同じですが、NOPのあとの末尾に置くコードを半分はリセット、半分は無限ループというかたちにします。
自作バイナリの上に着地することは完璧にわかっているので、あとはリセットが起きるか無限ループが起きるかという情報を1bitの情報で二分探索していく。例えば今回の場合、リセットが起きたとすれば、左半分の領域に飛んでいることがわかるので、左半分をさらに半分にして前半をリセット、後半を無限ループで埋める。それを繰り返していくと、最終的に実験の結果、112番目の領域に飛んでいることが判明したということになります。
3つの実験の結果、U-Bootが動かせそうだということがわかりました。15MBの自作バイナリを用意して、オフセット0x700000にu-boot.binを置いて0x67800000に飛べばU-Bootが動きそうだとなりました。では動かしましょう。
U-Bootは大成功、Linuxの動作はまだまだこれから
U-Bootをどうやって用意するか。i.MX7ULPには評価ボードがあります。この評価ボードのconfigから余計なものを削って、先ほどの大きいバイナリに埋め込んで実機に転送します。本当に動くのか、心臓がバクバクでしたが、動きました。実験がうまくいって、本当に動きました。
実はこのバージョン番号を出すまで、紆余曲折ありましたが、実際に動きました。このあと数日がんばって、ドライバの初期化も通してU-Bootのshellまで到達することができました。ということで、U-Bootは大成功に終わりましたが、次はLinuxを動かしたい。
実は、Linuxは起動できました。しかし、U-Bootの初期で止まってしまうというバグがあって、最初のprintkがバラバラと出たあとUARTから何も出なくなるのに苦戦しました。割り込みが怪しいのではないかと考え、私を含む何人かのメンバーでずっと症状を追い込んでいましたが、結局何が悪いのかわからなくて数ヶ月ブランクが空いてしまいました。
2021年10月に、pepepper君という一緒に作業しているメンバーから、「Mailbox Unit、つまりCortex-A7とCortex-M4のコアの間の通信を取り持っているユニットとドライバが、現在僕らが使っているLinuxのカーネルに入っていない。だからMailbox Unitの割り込みがハンドルされていないのではないか」というメンションをもらいました。
NXPのソースコードに上がっているupstreamを見てみるとMailbox Unitのドライバが追加されていたので、それをマージするとあっさりログインまで進むという、ビックリするような展開がありました。Linuxのログインshellまで到達したのは2021年9月の出来事で、Linuxの動作はまだまだこれからということになります。
以前、例えばLCDに移すとか、電子辞書でLinuxのポーティングをした時にLCDで移したという報告をしましたが、そこまで進むにはまだ時間がかかりそうなので、乞うご期待です。
これからもコミュニティと協力して解析していきたい
最後に、まとめと今後の話をします。メチャクチャ大変でした。ハードウェアもOSも不明なところから始めて、結局そのあとのリバースエンジニアリングによってプロプライエタリなµITRON系のRTOSが動いていることがわかりました。当然、プロプライエタリなµITRON系のRTOSはぜんぜん情報がないんですよね。
そのため、結局冒頭から非常に苦戦することになりました。今でも苦戦していますが、I/Oがほとんどない状態で、アイデア1つで難しい箇所を脱して最終的にLinuxに行けたことには本当に感動したし、何よりコミュニティの助け合いで情報の交換ができた。それによって僕1人では絶対にできないような進捗を見出せたことは、コミュニティを作った人間として本当にうれしかったです。
まだやることは多いですが、これからコミュニティと手を取って解析に勤しんでいきたいと思います。最後に、このスライドと小説版のブログを書きました。細かい情報が入っているので、気になる方はぜひ読んでみてください。発表を終わります。
質疑応答
司会者:1ヶ所だけわからなかったところがあります。「U-Bootにジャンプした時にU-Bootの画面が見れたりUARTが出たりしたところで、リファレンスのイメージを使ったらよくわからないけれど、ポートにプローブをかけたらそのまま見えたということなのでしょうか?」
末田:何をしたかというと、まず評価ボード向けのU-Bootのconfigをコピーします。そこから評価ボードに乗っている特有のハードウェアのconfigをどんどん大きくしていって、デバイスツリーからも消して、そのあとに評価ボードに載っているDRAMと今の電子辞書に載っているDRAMは種類や容量がけっこう違うので、設定を合わせる。
UARTが出ている他のピンがありますが、I/Oのマルチプレクサによって他のピンに割り当てられていたりするので、今出ている、物理的につながっているピンからUARTが出るように設定する。これもコミュニティのみんなで解析した結果を照らし合わせて実現しました。
司会者:冒頭で、マルチプレクサの設定状況がわからないから困っているというところがあって、U-Bootを持ってきた時にどう解消したのかがわかっていなかったんですけど。
末田:物理的な電気的接続については、一緒に作業しているコミュニティのメンバーのすぐちさんに最新世代のものを1台買ってプレゼントしました(笑)。彼は私よりも電気回路の解析をうまくやってくれるので、これを解析してくれないかと頼んで、部品を剥がしてもらって、ここがこことつながっている、みたいなことを全部調べてもらいました。
司会者:まさにコミュニティならではの熱さ。
末田:激アツでしたね。
司会者:非常に熱い話です。ありがとうございました。