この記事はLinux Advent Calendar 2020の5日目です。
TL;DR
- Replay Protected Memory Block (RPMB) プロトコルでは、共通鍵を利用して、HostとDevice間で処理するデータについて検証する手段。
- ところが、今回指摘した脆弱性を使うと 「Hostには失敗と応答されたのに、Deviceではデータが書かれている」「Hostには成功と応答されたのに、Deviceはデータを書いていない」 というケースが作れそう。
- Linux Kernel本体にはRPMBへアクセスするための直接的なロックは存在してない。mmc-utils上でにコマンドラインから呼び出すコードがある。
はじめに
- JVN ( Japan Vulnerability Notes ) では、セキュリティレポートが出ています。
- その中でプロトコルレベルっぽい指摘があった。
- この指摘について、Linux中心に確認していく(軽く)。
想定される影響
RPMB プロトコルを利用するシステムに直接アクセス可能な第三者によって、次のような影響を受ける可能性があります。
・RPMB 領域への書込みが成功したにも関わらず失敗したようにホストに誤認識させる
・RPMB 領域にホストの意図とは異なる内容を書き込んだにも関わらず、意図通りの内容を書き込んだとホストに誤認識させる
このあたりをもうちょっと読み解いていく!
そもそもReplay Attackとは?
雑に説明すると、こんな感じ。詳細は、Wikipediaの反射攻撃 を参考ください。
- 悪意ある人間が認証情報ごと丸ごとコピーすると、正規要求なのか不正要求なのかわからなくなる。
- 下記の例だと「社長」の判子がある要求には、「経理」は正規要求だと認識して、現金を手渡す。
- では、社長の作った書類を悪い人が盗聴・複製して、コピーを経理に渡したら?
- 経理の人は本物と偽物の見分けがつかないので、現金を支払ってしまう。
- 社長が見ると「あれ?これ・・・なんで2回10万円?」と悩む。
Replay Protected Memory Block (RPMB) プロトコルとは?
Replay Protected Memory Block (RPMB) protocol does not adequately defend against replay attacksからリンクが張られていた、Western Digital社のe.MMC Security Methods を基に確認してみる。
- HostとDeviceでそれぞれ暗号鍵情報を分けて持っている。
- Read OnlyなCounterがあることで、Replay Attackをすると必ずMACがズレて失敗する。
Western Digital社のwhite-paper-emmc-security.pdfから引用。
今回の攻撃は?
wdc-20008-replay-attack-vulnerabilities-rpmb-protocol-applications にリンクが張られているwhite-paper-replay-protected-memory-block-protocol-vulernabilities.pdf 詳細が記載されているので、これを紹介したい。
Case 1 : データ書いているけど、書かなかったことにする
振る舞い
- (1) Hostは、REQ( カウンタCt, アドレスAdr, MAC Mを、Device) を送信。
- (2) 敵(adversary) が、Host->Device間の通信に割り込む!!
- 敵は、失敗するようにMAC値を書き換えたREQ* を Deviceに送信、
- (3) Deviceは検証をするけど、MAC値不正で失敗をHostへ返却。
- 失敗しているので、Device側のカウンタ値は更新されない。
- Hostは失敗が通知されたので「失敗した」と認識する。
- (4) 敵は、REQをDeviceに送信。
- (5) DeviceはREQを検証し、カウンタCtもMAC Mも正しいので成功と判断してデータを書き換える。
結果
- Host 「え?REQを送ってみたけど、失敗したよ。データは書き換わってないはずだよ」
- Device「REQ*は失敗したけど、REQは成功したよ。データは書き換わっているよ!」
という感じで、認識の相違が発生する!!!怖いですねー
Case 2 : データ書いていないけど、書いたことにする
振る舞い
- (1) Hostは、REQ1( Ct, Data1, Adr, M1) を送信。
- 敵(adversary) が、Host->Device間の通信に割り込む!!
- 敵は、REQ1を記録しておく。
- (2) 状態の喪失:例えば電源切断
- (3) 復帰:例えば再起動
- (4) Hostは、REQ2( Ct, Data2, Adr, M1) を送信。
- 敵(adversary) が、Host->Device間の通信に割り込む!!
- (5) 敵は、REQ2ではなく、REQ1をDeviceへ送信!
- (6) DeviceはREQ1を検証し、カウンタCtもMAC Mも正しいので成功と判断してデータを書き換える。成功をHostへ返却。
結果
- Device「REQ1の要求受けたから、そのData1でちゃんと書き換えましたよ!成功しました!」
- Host 「REQ1の応答がなかったような…。。。 でも、最後に送信したREQ2に対しては成功が帰ってきたから、Data2で書き換えられているはずです!」
あらららら……
Linux KernelのRPMB実装を確認してみよう!
からサポートが始まっている………… と、思うじゃろ?
このスレッドから始まる議論では、この修正は含まれなかった。
その結果、RPMBへアクセスするためのインタフェイスは存在していない、と。
えええ、じゃあどうやってアクセスするのかー、っというところで、見るべきポイントは「mmc-utils」です。
mmc-utils
https://git.kernel.org/ にちゃーんとリンクが張られている。
ここで、データを書き込むコマンド、do_rpbm_write_block()に閉じて、その振る舞いを確認していく!と思ったけど、大体話は上で書いたものとそう大差さないので、最後にAppendixとしてまとめておきます。
予備知識
-
mmc rpmb write-key </path/to/mmcblkXrpmb> </path/to/key>
- HostからDeviceへ鍵情報を書き込める。
-
mmc rpmb read-counter </path/to/mmcblkXrpmb>
- HostはDeviceからカウンタ情報を読み出せる。
-
mmc rpmb read-block </path/to/mmcblkXrpmb> <address> <blocks count> </path/to/output_file> [/path/to/key]
- HostはDeviceから、指定した鍵情報keyを利用して、指定したアドレスaddressからblock_countの数だけの内容を読み取り、output_fileへ出力できる。
-
mmc rpmb write-block </path/to/mmcblkXrpmb> <address> </path/to/input_file> </path/to/key>
- HostはDeviceへ、指定した鍵情報keyを利用して、指定したアドレスaddressへinput_fileの内容を書き出せる。
考察
- mmc-utilsの実装コードを見ていると、確かにHost/Device間でカウンター値の連続性とかチェックが無い。
- TCPのシーケンス番号もないから、確かにCase2のようにその応答が今回のコマンド要求に対する応答かどうかとかもわからないなあ…
- つまり、プロトコルレベルでの設計で、問題があったんだなあと…。
- RPMBに重要な情報を保持するとわかっていると、ここを集中して狙われそう。確かにこれはまずい…。
まとめ (再掲)
お忙しい中、ここまで読んでいただき、ご精読頂き、ありがとうございました!!
以下のようにまとめます。
- Replay Protected Memory Block (RPMB) プロトコルでは、共通鍵を利用して、HostとDevice間で処理するデータについて検証する手段。
- ところが、今回指摘した脆弱性を使うと「Hostには失敗と応答されたのに、Deviceではデータが書かれている」「Hostには成功と応答されたのに、Deviceはデータを書いていない」というケースが作れそう。
- Linux Kernel本体にはRPMBへアクセスするための直接的なロックは存在してない。mmc-utils上でにコマンドラインから呼び出すコードがある。
補足…
- 本脆弱性の具体的なユースケースや対策方法は、white-paper-replay-protected-memory-block-protocol-vulernabilities.pdf に記載がある。
- さすがに全部丸ごとコピーするのは違う、ということで、より詳細に興味ある方はそちらを確認いただければと思います。
Appendix (mmc-utilsの実装確認)
このコードを簡単に読んでいく。
各種変数定義
書き込むREQを格納する、struct rpbm_frame frame_in
を定義する。
int do_rpmb_write_block(int nargs, char **argv)
{
int ret, dev_fd, key_fd, data_fd;
unsigned char key[32];
uint16_t addr;
unsigned int cnt;
struct rpmb_frame frame_in = {
.req_resp = htobe16(MMC_RPMB_WRITE),
.block_count = htobe16(1)
}, frame_out;
/dev/mmcblkXrpmbを開く。
dev_idは、/dev/mmcblkXrpmb
のfilr descriptor。
if (nargs != 5) {
fprintf(stderr, "Usage: mmc rpmb write-block </path/to/mmcblkXrpmb> <address> </path/to/input_file> </path/to/key>\n");
exit(1);
}
dev_fd = open(argv[1], O_RDWR);
if (dev_fd < 0) {
perror("device open");
exit(1);
}
カウンターの値をdeviceから読み出す
ret = rpmb_read_counter(dev_fd, &cnt);
/* Check RPMB response */
if (ret != 0) {
printf("RPMB read counter operation failed, retcode 0x%04x\n", ret);
exit(1);
}
frame_in.write_counter = htobe32(cnt);
アドレス情報をセットする
/* Get block address */
errno = 0;
addr = strtol(argv[2], NULL, 0);
if (errno) {
perror("incorrect address");
exit(1);
}
frame_in.addr = htobe16(addr);
データや鍵情報をファイル/標準入力から読み出す
/* Read 256b data */
if (0 == strcmp(argv[3], "-"))
data_fd = STDIN_FILENO;
else {
data_fd = open(argv[3], O_RDONLY);
if (data_fd < 0) {
perror("can't open input file");
exit(1);
}
}
ret = DO_IO(read, data_fd, frame_in.data, sizeof(frame_in.data));
if (ret < 0) {
perror("read the data");
exit(1);
} else if (ret != sizeof(frame_in.data)) {
printf("Data must be %lu bytes length, but we read only %d, exit\n",
(unsigned long)sizeof(frame_in.data),
ret);
exit(1);
}
/* Read the auth key */
if (0 == strcmp(argv[4], "-"))
key_fd = STDIN_FILENO;
else {
key_fd = open(argv[4], O_RDONLY);
if (key_fd < 0) {
perror("can't open key file");
exit(1);
}
}
ret = DO_IO(read, key_fd, key, sizeof(key));
if (ret < 0) {
perror("read the key");
exit(1);
} else if (ret != sizeof(key)) {
printf("Auth key must be %lu bytes length, but we read only %d, exit\n",
(unsigned long)sizeof(key),
ret);
exit(1);
}
これまでの書き込みデータの内容から、HMACの値を計算する
/* Calculate HMAC SHA256 */
hmac_sha256(
key, sizeof(key),
frame_in.data, sizeof(frame_in) - offsetof(struct rpmb_frame, data),
frame_in.key_mac, sizeof(frame_in.key_mac));
REQを送信するためのRPMB 処理を実行し、結果を確認する
/* Execute RPMB op */
ret = do_rpmb_op(dev_fd, &frame_in, &frame_out, 1);
if (ret != 0) {
perror("RPMB ioctl failed");
exit(1);
}
/* Check RPMB response */
if (frame_out.result != 0) {
printf("RPMB operation failed, retcode 0x%04x\n",
be16toh(frame_out.result));
exit(1);
}
後始末
close(dev_fd);
if (data_fd != STDIN_FILENO)
close(data_fd);
if (key_fd != STDIN_FILENO)
close(key_fd);
return ret;
}