車のナビを脱獄して時刻を変更できるようにしただけ
2014年式ルノー・ラティテュードは、ナビの SD カードを挿さないと時計を設定させてくれない。オーナーはファームウェアをリバースエンジニアリングし、パストラバーサルの脆弱性と core_pattern のテクニックを組み合わせて、最終的に root 権限を取得した。
日本語
コピー

2014年式の Renault Latitude に乗っている。高級車というわけでもなく、そろそろ古さも目立ってきたが、A 地点から B 地点まで運んでくれるし、それ以外もまあまあといったところで、これまで不満はなかった。
購入したとき、前のオーナーが「ナビの地図が入った SD カード」を一緒に引き取らないかと聞いてきた。
どうやらこの車、時計を合わせるのにその SD カードを挿さないといけないらしい、と彼は言う。私はあまり信じなかった。嘘をついているとは思わない。おそらく彼が正しい設定メニューを見つけられなかっただけだろう。同じ車載機の動画がネット上にあって、そのメニューにはちゃんと時計設定の項目があるのだ。
SD カードの地図は明らかに古くて使い物にならないので、いらないと断ると、前のオーナーはそれを捨ててしまった。
別に困りもしなかった。インパネの画面には「No maps found!」が大きく一行、画面の半分近くを占めて表示されていたが、特に気にしていなかった。

ほとんどのヨーロッパの国と同じく、エストニアにも夏時間がある。つい先日、夏時間(UTC+3)から「冬時間」(UTC+2)に切り替わったばかりで、インパネの時計が1時間ずれている。設定メニューに入れば「時刻設定」くらいすぐ見つかるだろうと思って開いた。
なかった。
時刻設定があるはずのメニューに、その項目自体が存在しない。メニューをネットの動画と見比べたが、やはり項目がない。センターコンソールには「Settings」ボタンが2つあり、片方は動くのだが、変えられるのは Bluetooth や電話帳の設定だけで、日付と時刻の設定は影も形もない。もう片方の Settings ボタンはまったく反応せず、押すたびに「No maps detected!」と出るだけだ。
1時間ほどあちこちのメニューを漁り、ボタンを押し、いろいろな組み合わせを試して、当てずっぽうで正しいメニューに入れないかと探し回った。この車載機の情報もネットで調べた。時刻を変える項目がないはずがない、そして自分は何か明らかなものを見落としているに違いない、そう信じたかったからだ。
ならばリバースエンジニアリングだ
それでも諦めきれず、ファームウェアを落として中を覗いてみることにした。正しい設定メニューを開く方法の手がかりが何か見つかるかもしれない。
ありがたいことに、同じ道を辿った人がすでにいて、この機種をリバースエンジニアリングした記事があった(https://github.com/andronedev/carminat-tomtom-live-re/blob/main/README.md)。フランス語の記事で、どう見ても LLM をかなり頼りに書かれているのだが、アップデートファイルそのものをダウンロードする URL は正しく教えてくれた。
解凍すると、アップデートファイルの中にttsystemファイルがあった。実体は ext3ファイルシステムで、中身はデバイスのルートファイルシステムそのものだ。まさに探していたものだ。この機種(TomTom Carminat LIVE、コードネームbeaumont)は Samsung S5P6440 SoC を積み、TomTom が Linux 2.6.28.10をベースにカスタマイズしたシステムで動いている。
しばらく解析して出した結論は、前のオーナーの言うとおりだった。ナビの SD カードを挿さない限り、正しい設定メニューに入る方法は存在しない。というより、Settings アプリへ導くメニュー定義そのものが SD カードから読み込まれている。正しいメニュー定義が書かれたカードが挿さっていなければ、機械は Settings アプリへの入り方すら知らないのだ。
TomTom(あるいは Renault)がなぜこんな設計にしたのか、今もって理解できない。
さらに悪いことに、それは正規の SD カードでなければならない。システムには DRM の検証があり、CID(カードに焼き込まれた固有 ID)が正しくない SD カードを拒否する。ファイルの署名もこの特定の CID に紐づいているので、ファイルを空の SD カードにコピーすることもできない。この DRM は地図の海賊版対策なのだろう。
地図はいらない。ナビならスマホがある。時刻を変えたいだけなのだ、それだけのことだ。
ネットで調べると、おそらく必要なカードを売っている第三者がいた。だがエストニアまでの送料を入れると60ユーロほどになる。とても払う気にはなれない。
わかったよ。なら自分で root を取って、自分でやるまでだ。
近道はないのか
この機種を脱獄した人はネット上に見つからなかった。見つかったのは古いフォーラムの投稿がいくつかだけで、画像リンクはとうに切れ、中身もこの機種についての漠然とした話ばかり。共通する見解は「root を取るのは難しい」だった。
2011年の Linux 2.6のシステムなら大したことはないだろうと、甘く見て軟らかいところを探しに入った。
最初に思いついたのは、ext3のアップデートイメージ自体を書き換えることだ。話は簡単に見えた。ファイルシステムをマウントし、init スクリプトをいくつか書き換えて好きなことをさせ、空の SD カードにコピーして、デバイスに書き換えたイメージを読ませて書き込ませる。
純正の未改変のttsystemイメージなら確認プロンプトが正常に出て、インストールは何の問題もなく進む。ところが書き換えたイメージはどうやっても更新できない。理由もまったくわからない。アップデートファイルに含まれる更新ロジックを調べても、このイメージを弾く理由は見当たらない。
そこで QEMU のエミュレータを組んだ。2.6系ベースのカスタムカーネルにパッチをいくつか当て、簡単な CAN シミュレータも付けて、システムが実際に起動するようにした。ぎりぎり起動するといった具合で、3秒ごとに再起動しようとするので、reboot システムコールを無視するようにパッチを当てて黙らせた。
この環境で書き換えたアップデートイメージのインストールを試した。そこで初めて、デバイスが更新のために起動するrecoveryパーティションを自分が持っていないことに気づいた。つまり間に、まだ見えていない検証の層がもう一段あると推測した。
ttsystemイメージをもう少しよく見ると、ほぼ裏付けが取れた。後ろに高エントロピーのデータが長く続き、そのあとに全部ゼロのブロック、さらに40バイトの高エントロピーデータが続く。何らかの署名だろう(40バイトなら DSA with SHA-1かもしれない)。だが実際に検証を行っているバイナリが手に入らない以上、どう攻めるかも、そもそも攻められるのかもわからない。
振り出しに戻る
これまでエミュレータを組む手間はずっと後回しにしてきたが、蓋を開けてみればこれが思いのほか役に立った。さまざまな攻撃を試せるし、システム全体の挙動もつかめる。
たとえば、ほとんどのパーティションが読み取り専用でマウントされているか、noexec になっているか、あるいはその両方だということがわかった。稼働中のシステムのマウント状況は次のとおり:
rootfs on / type rootfs (rw)
none on /sys type sysfs (rw,nosuid,noexec)
none on /proc type proc (rw,nosuid,noexec)
/dev/mmcblk0p5 on /content type ext3 (rw,nosuid,nodev,noexec,noatime,nodiratime,errors=continue,data=ordered)
/dev/loop0 on / type ext2 (ro,noatime,nodiratime,errors=continue)
none on /dev type tmpfs (rw,nosuid,noexec,size=1024k)
none on /tmp type tmpfs (rw,nosuid,noexec,size=2048k)
none on /var type tmpfs (rw,nosuid,noexec,size=5120k)
none on /dev/pts type devpts (rw,nosuid,noexec,gid=5,mode=620)
none on /var/log type tmpfs (rw,nosuid,noexec,size=1024k)
/dev/mmcblk0p3 on /mnt/flash type ext3 (rw,sync,nosuid,nodev,noexec,errors=continue,data=ordered)
/dev/mmcblk1p1 on /media/sdcard type vfat (rw,sync,nosuid,nodev,noexec,noatime,nodiratime,fmask=0022,dmask=0022,codepage=cp437,iocharset=iso8859-1)
/dev/loop1 on /media/loopback type ext3 (rw,nosuid,nodev,noexec,noatime,nodiratime,errors=continue,commit=3600,data=journal)
none on /mnt/movi type unionfs (rw,nosuid,nodev,noexec,noatime,nodiratime,dirs=/media/loopback=rw:/media/sdcard=ro)
注目すべきは、SD カード上の基本の FAT32 ファイルシステムが /media/sdcard にマウントされ、その上にシステムが /mnt/movi で unionfs をマウントしている点だ。書き込み可能なレイヤーは ext3 ファイルシステムを指していて、そのファイルシステムは外付け SD カード上にファイルとして置かれている。この構成の意図は完全にはわからない(電源断に強い data=journal を使うためだろうか?)が、いずれにせよこれらのマウントはすべて noexec だ。
2011 年のデバイスにしては、意外なほど堅牢な作りになっている。
エミュレータでわかったことはもう一つある。システムが外付け SD カードからどのファイルを読もうとするか、だ。その一つが master.script で、UI のナビゲーション操作を自動化するための独自言語で書かれたスクリプトだ。起動のたびに読み込まれるので、時刻設定に必要なメニューを開くのに使えるのではないかと期待した。

このスクリプトを解析するバイナリ ttn をリバースエンジニアリングし、機能を記録しながら分析を進めたが、残念ながら目的のメニューを開く手段は見つからなかった。
またしても徒労
やはり master.script に注力すべきだと考えている。機能が多く、攻撃面になり得るからだ。
いつもどおり、コマンドインジェクションに使える system や popen の呼び出しがないか調べた。残念ながら、ユーザー入力から組み立てられる呼び出しは見つからなかった。
パーサーを解析する中で SHEX/SHELLEXECUTE コマンドに出くわし、一瞬これは当たりかと思った。任意の引数を launchserver に渡し、これが渡された任意のコマンドラインを execve で起動する。
ただし障害が二つある。第一に、独自の実行ファイルを直接実行させる手段は依然としてない。SD カードが noexec でマウントされているため、カード上の実行ファイルやスクリプトは動かせない。だが回避策はある。スクリプトを直接実行する代わりに /bin/sh /media/sdcard/payload.sh のようなものを実行すれば、rootfs 上の /bin/sh を経由する。rootfs は noexec でマウントされていないのだ。
第二の障害のほうが大きい。launchserver は何でも喜んで起動するが、SHEX は空白区切りのトークンを一つしか受け付けない。つまり制御できるのはどのバイナリを実行するかだけで、引数は一切渡せない。前の障害と合わせると、これもまた行き止まりに見える。rootfs 上にすでにあるスクリプトかバイナリしか動かせないからだ。その大半には目を通したが、特に使えそうなものはなかった。
追い詰められて、ファイル書き込みの調査を始めた。本来書けるはずのない場所にファイルを書ければ、何かしらの足がかりが得られるかもしれない。
案の定、LOG コマンドが見つかった。ファイルを切り詰め(#define LOGFILE log.txt で指定)、任意のファイルに 1 行書き込める。このコマンドは空白や通常の ASCII 文字を受け付けるが、0x20 から 0x7E のバイトに限られる。つまりバイナリデータの書き込みはまったくできない。ttn は root として動くので、書き込みもすべて root 権限で行われる。
おまけに古典的なパストラバーサルの脆弱性もある。LOGFILE に ../ を入れると、想定されたログディレクトリの外に書き込める。理屈の上では、システム上のどこにでも印刷可能な ASCII を書き込める。
しかしここでもマウントポイントの設定に阻まれた。rootfs は読み取り専用でマウントされていて、上のものは何も上書きできない。それ以外はすべて noexec でマウントされていて、そもそも実行されない。上書きに使えそうな設定ファイルも探したが、大半は独自のバイナリ形式で LOG では書けず、残りも使い道がなかった。これも同じく行き止まりのようだ。
その他の点では、master.script は画面に画像(GIF/BMP)を表示できる。Unicorn でファジング環境を組むのにしばらく時間をかけ、画像デコーダにメモリ破壊のバグを起こせるか試した。範囲外読み取りはかなり見つかったものの、使える書き込みは見つからなかった。
カーネルエクスプロイトの手法を借りる
任意コード実行の戦略を調べ、リバースエンジニアリングし、あれこれ考え出すのに、もうかなりの時間を費やしている。悔しいことに、どの戦略もうまくいかなかった。そこで一歩引いて全体像を見直し、手元のピースを組み合わせられないか考えることにした。
できることはわかっている:
- 任意のバイナリを実行できるが、引数は渡せず、rootfs 上にあるものに限られる
- 自分で選んだパスに ASCII を書き込める
そこで閃いたのが、カーネルエクスプロイトでよく使われるあの手だ。ユーザーランドにスクリプトを実行させる方法を考える代わりに、カーネルに実行させればいいのではないか?
Linux のカーネルエクスプロイトでは、メモリへの任意書き込みが可能なとき、core_pattern 文字列を上書きすることがよくある。この文字列は本来、プロセスがクラッシュしたときに coredump をどこへ保存するかを決めるものだが、coredump をパイプでプロセスに渡す特殊な構文(|/bin/my-coredump-handler)も受け付ける。クラッシュが起きると、カーネルは core_pattern に指定されたプロセスを起動し、データをパイプで流し込む。
core_pattern は起動するプロセスにコマンドライン引数も渡せる。ここは運がよかった。この機能は 2.6.24 から存在し、このマシンのカーネルは 2.6.28 だ。
ポイントは、ユーザー空間から /proc/sys/kernel/core_pattern にモードを書き込んで core_pattern を設定できることだ。
これを使って LOGFILE を ../../proc/sys/kernel/core_pattern に向け、LOG 文字列 |/bin/sh /media/sdcard/payload.sh を渡すのは造作もない。これで rootfs 内の /bin/sh が走り、root 権限でこちらのスクリプトが実行される。
任意のバイナリを実行できるという点は、引数を渡せなくても役に立った。coredump を起こすには、どうにかしてプログラムを落とす必要があったからだ。DoS 脆弱性でスクリプトパーサーやメイン UI そのものを落とすのは筋が悪い。即座にシステム全体が再起動し、仕込んだスクリプトも道連れになる。
エミュレータ環境でシステム上のほぼすべてのバイナリを試したところ、/bin/dbus-monitor はシステムバスに接続できないとき SIGABRT を送出して coredump を起こすことがわかった(このマシンではシステムバスが動いていない)。これで決まりだ。
組み立てる
最終的な master.script はかなり単純だ:
#define LOGFILE ../../proc/sys/kernel/core_pattern
LOG |/bin/sh /media/sdcard/payload.sh
SHEX /bin/dbus-monitor
最初は payload.sh スクリプトから reboot を呼び出して、成功したかどうかだけ確かめていた。SD カードを挿してマシンを起動し、Execute master.script? のプロンプトで「Yes」を押すと、マシンがすぐに再起動した。コードが実行された証拠で、嬉しい驚きだった。
まず最初にやったのは、dd で内蔵 eMMC の完全バックアップを取ることだった。より完全なシステムダンプが手に入ると、目的の日付・時刻設定が /mnt/flash/settings.dat に独自のバイナリ形式で保存されていることをすぐに突き止めた。さらに逆解析を進め、ファイル内で現在のタイムゾーンオフセットが格納されている正確な位置を特定し、UTC+2 に書き換えるスクリプトを書いた。
こうして、車のタイムゾーンをようやく直せたのだった。