新型コロナの検査結果を改ざんする
※F-Secureの法人向け事業はWithSecure™になりました。個人ユーザー向けの事業が引き続きF-Secureとして展開しております。移行期のため、F-Secure BlogでWithSecure™のご案内をさせていただいております。
はじめに
ウィズセキュアは、新型コロナの検査結果を改ざんする方法を発見する目的で、Cue Health社の「Home COVID-19 Test」検査キットに関する調査を実施しました。この検査キットが選ばれた理由は、そのリーダーユニットがBluetoothを介して検査結果を被験者の携帯電話に送信しているためです。この研究の成果として、ウィズセキュアは、検査の結果を改ざんし、改ざんされた 証明書を取得することに成功しました。
本記事では、この 調査がどのように行われたのか、その技術的な詳細を解説します。対象となるアプリケーションとファームウェアのバージョンは以下の通りです。
- Cue Health Androidアプリケーション com.cuehealth.healthappバージョン1.4.3.134
- Cue Readerファームウェアバージョン0.17.6
概要:リーダーとカートリッジ
技術的な詳細に入る前に、テストコンポーネントと仕組みについて簡単に説明します。Cue Healthによる新型コロナ検査を実施するには、Cueリーダーとテストカートリッジの2つのコンポーネントが必要になります。
Cueリーダーユニットには、ヒーター、ジャイロスコープ、そしてユーザーの携帯電話とBluetoothで通信するために必要なハードウェアが組み込まれています。検査はテストカートリッジで実行します。検査後、Cue リーダーにテストカートリッジを挿入すると、その検査データはCue リーダーにから被験者の電話に送信されます。
各COVID-19テストカートリッジには、カートリッジに挿入される鼻腔ぬぐい用綿棒が付属しています。テストカートリッジには液体が入っており、綿棒を挿入すると放出されてカートリッジ内の一か所に溜まり、そこで検査が行われます。
技術的詳細: BLUETOOTHのトラフィックとPROTOBUF
デバイスの概要が理解できたところで、次にBluetoothトラフィックがどのように構成されているかを詳しく見ていきます。Cueリーダーは、Bluetooth上のProtobufプロトコルを介してAndroidアプリと通信を行っていました。このトラフィックには、以下のような一般的な構造をしたパケットが含まれています。
08 XX XX XX YY ZZ ZZ ZZ ZZ ZZ ZZ ZZ
- XX – Protobuf内のパケット数
- YY – パケットタイプ(Cartridge Status:カートリッジの状況、Test Result:検査結果、など)
- ZZ – データ自体
Cue Healthは、Protobufパーサーをclass b.l.h.o$b method y()内に実装しました。この実装を以下に示します。
public int y() throws java.io.IOException { int var3_bytePosition = this.i; int var1_payloadLength = this.g; if (var1_payloadLength != var3_bytePosition) { byte[] var6 = this.e; int var2_bytePositionPlus1 = var3_bytePosition + 1; byte var7 = var6[var3_bytePosition]; if (var7 >= 0) { this.i = var2_bytePositionPlus1; return var7; } if (var1_payloadLength - var2_bytePositionPlus1 >= 9) { var1_payloadLength = var2_bytePositionPlus1 + 1; var3_bytePosition = var7 ^ var6[var2_bytePositionPlus1] << 7; if (var3_bytePosition < 0) { var2_bytePositionPlus1 = var3_bytePosition ^ -128; } else { var2_bytePositionPlus1 = var1_payloadLength + 1; var3_bytePosition ^= var6[var1_payloadLength] << 14; if (var3_bytePosition >= 0) { var3_bytePosition ^= 16256; var1_payloadLength = var2_bytePositionPlus1; var2_bytePositionPlus1 = var3_bytePosition; } else { var1_payloadLength = var2_bytePositionPlus1 + 1; var2_bytePositionPlus1 = var3_bytePosition ^ var6[var2_bytePositionPlus1] << 21; if (var2_bytePositionPlus1 < 0) { var2_bytePositionPlus1 ^= -2080896; } else { int var4 = var1_payloadLength + 1; byte var5 = var6[var1_payloadLength]; var3_bytePosition = var2_bytePositionPlus1 ^ var5 << 28 ^ 266354560; var2_bytePositionPlus1 = var3_bytePosition; var1_payloadLength = var4; if (var5 < 0) { int var8 = var4 + 1; var2_bytePositionPlus1 = var3_bytePosition; var1_payloadLength = var8; if (var6[var4] < 0) { var4 = var8 + 1; var2_bytePositionPlus1 = var3_bytePosition; var1_payloadLength = var4; if (var6[var8] < 0) { var8 = var4 + 1; var2_bytePositionPlus1 = var3_bytePosition; var1_payloadLength = var8; if (var6[var4] < 0) { var4 = var8 + 1; var2_bytePositionPlus1 = var3_bytePosition; var1_payloadLength = var4; if (var6[var8] < 0) { var1_payloadLength = var4 + 1; var2_bytePositionPlus1 = var3_bytePosition; if (var6[var4] < 0) { return (int) this.O(); } } } } } } } } } this.i = var1_payloadLength; return var2_bytePositionPlus1; } } return (int) this.O(); }
受信した各Bluetoothメッセージは上記のパーサーによって処理され、class com.cuehealth.protobuf.reader.CueMessage method CueMessage()には、Cue Healthアプリが認識したBluetoothメッセージのタイプが10進数形式で含まれていました。例として、以下のコードスニペットでは、10進数で170(16進数でAA)のパケットタイプはTest Resultタイプのデータになります。
private CueMessage(Abstracto oVar, g0 g0Var) throws a1 { ... case 170: builder3 = this.msgCase_ == 21 ? ((TestResults) this.msg_).toBuilder() : builder3; Abstractv1 w12 = oVar.w(TestResults.parser(), g0Var); this.msg_ = w12; if (builder3 != null) { builder3.mergeFrom((TestResults) w12); this.msg_ = builder3.buildPartial(); } this.msgCase_ = 21; continue;
そして、class com.cuehealth.protobuf.reader.TestResults method TestResults()を参照すると、10進数で34(16進数で22)のデータタイプ値が「Assay Result(分析結果)」を表していることがわかります。
private TestResults(final o o, final g0 g0) throws a1 { ... case 34: { int n7 = n; if ((n & 0x8) != 0x8) { n2 = n; n2 = n; final ArrayList assayResults_ = new ArrayList(); n2 = n; this.assayResults_ = assayResults_; n7 = (n | 0x8); } n2 = n7; this.assayResults_.add((AssayResult)o.w(AssayResult.parser(), g0)); n = n7; continue; }
この後にclass com.cuehealth.protobuf.reader.AssayResult method AssayResult()が続き、以下の2つのタイプのデータが使用されていることがわかりました。
- 「type」は10進数の8(16進数でも8)で表されている
- 「code」は10進数の16(16進数では10)で表されている
private AssayResult(Abstracto oVar, g0 g0Var) throws a1 { ... if (G == 8) { this.type_ = oVar.p(); } else if (G == 16) { this.code_ = oVar.p();
class com.cuehealth.protobuf.reader.AssayResult method Code()には、想定される「code」値に関する情報が含まれていました。「陰性」は10進数の3(16進数でも3)で表されていました。
public enum Code implements C12423z0.AbstractC12426c { INVALID(0), CONCENTRATION_LEVEL(1), POSITIVE(2), NEGATIVE(3), PASS(4), FAIL(5), UNRECOGNIZED(-1);
同じclass内において、method Type()には想定される「type」値に関する情報が含まれていました。「新型コロナウイルス」の値は、10進数の19(16進数では13)でした。
public enum Type implements C12423z0.AbstractC12426c NONE(0), INFLUENZA(1), INFLAMMATION_CRP(2), VITAMIN_D(3), TESTOSTERONE(4), FERTILITY(5), HIV_1_VIRAL_LOAD(6), ANC_PLUS_WBC(7), ZIKA_IG_M(9), ZIKA_VIRAL(10), PREGNANCY(11), CORTISOL(12), CHOLESTEROL_LDL(13), CHOLESTEROL_HDL(14), HEMOGLOBIN_A1C(15), RSV(17), CORONAVIRUS(19), RVC(20), UNRECOGNIZED(-1);
これらを総合すると、新型コロナウイルスの検査結果が陰性であった場合、16進数では以下のようになる可能性が高いです。
08 XX XX XX AA YY YY YY YY 22 04 08 13 10 03 YY YY
- 08はパケットの先頭
- XXはProtobuf内のパケット数
- AAは「Test Result」パケットタイプ
- YYはその他の「Test Result」データ
- 22はAssayResultデータ
- 04は後続データが4バイトであることを示す
- 08 13はtype:新型コロナウイルス
- 10 03はcode:陰性
改ざんされた新型コロナウイルス検査結果の取得
ウィズセキュアは、Javaクラスのclass b.a.a.h.v3.k0$a method onCharacteristicChangedにフックするFridaスクリプトを開発しました。このメソッドは、アプリケーション内でBluetoothトラフィックを処理する際に最初に呼び出されるメソッドの1つであることから、フックの対象に選択されました。このスクリプトは、アプリケーションに送信された各Bluetoothパケットをインターセプトし、次の手順を実行しました。
- BluetoothトラフィックがCue Health Protobufパーサーを通過することで、Cueリーダーが受信したパケットのタイプを特定する
- データタイプが「Test Result」の場合、16進数で220408131002または220408131003のいずれかを検索する。
- 検査結果が陽性か陰性かを判定する
- 結果を入れ替える(陰性を陽性へ、陽性を陰性へ)
- 変更したデータをアプリに戻す
このスクリプトを使用すると、Cue Healthの担当者が監視している中で、Bluetoothデータ内の検査結果を変更することができました。以下はFridaスクリプトの出力で、検査結果が陰性であることを検知し、陽性の結果に変更したことを示しています。
[#] TestResults hex payload: 08fefb02aa0188010a120a1000748a7ba3e34142bc4bd2f2a7494d9510021a2e0a1908918fbf30101320a382013080cff395063a0631383830334612119a010e080215008088c520012d0000c8c32204081310033a1097c89ec5abe5a7c40ee118c42f1291c240bcdeb190e22f48b6a8fb90e22f52120a104cd2f87ebf raw bytes length: 125 bluetooth packet count: 48638 packet type: 170 TestResults packet size: 136 [#] COVID-19 NEGATIVE test found [#] changed COVID-19 NEGATIVE to POSITIVE [#] new hex payload: 08fefb02aa0188010a120a1000748a7ba3e34142bc4bd2f2a7494d9510021a2e0a1908918fbf30101320a382013080cff395063a0631383830334612119a010e080215008088c520012d0000c8c32204081310023a1097c89ec5abe5a7c40ee118c42f1291c240bcdeb190e22f48b6a8fb90e22f52120a104cd2f87ebf [#] TestResults hex payload: 08fffb02aa0188010a120a1000748a7ba3e34142bc4bd2f2a7494d9510021a2e0a1908918fbf30101320a382013080cff395063a0631383830334612119a010e080215008088c520012d0000c8c32204081310033a1097c89ec5abe5a7c40ee118c42f1291c240bcdeb190e22f48b6a8fb90e22f52120a104cd2f87ebf raw bytes length: 125 bluetooth packet count: 48639 packet type: 170 TestResults packet size: 136 [#] COVID-19 NEGATIVE test found [#] changed COVID-19 NEGATIVE to POSITIVE [#] new hex payload: 08fffb02aa0188010a120a1000748a7ba3e34142bc4bd2f2a7494d9510021a2e0a1908918fbf30101320a382013080cff395063a0631383830334612119a010e080215008088c520012d0000c8c32204081310023a1097c89ec5abe5a7c40ee118c42f1291c240bcdeb190e22f48b6a8fb90e22f52120a104cd2f87ebf
以下は、検査実施後にウィズセキュアが取得した証明書の画像です。
本調査のファイルはこちらから入手できます。
https://github.com/FSecureLABS/Cue-COVID-Test_Research-Files
CUE HEALTHへの通知と改善
ウィズセキュアはCue Healthに連絡を取り、上記の調査結果を伝えました。Cue Healthは、不正に操作されたテスト結果を検出するためのチェック機能をサーバー側に追加したと述べています。
被験者のCue Healthモバイルアプリケーションが、少なくとも以下のバージョンの場合はアップデートする必要があります。
- Android –7.2
- iOS –7.1
最新バージョンのモバイルアプリケーションを使用している場合でも、Cueリーダーのファームウェアバージョンが少なくとも0.17.8 (5)以上でない場合は、アップデートするよう促されます。
カテゴリ