Faking Another Positive COVID Test
-
Ken Gannon
- 21 Apr 2022
WithSecure conducted research into the Cue Health Home COVID-19 Test with the intention of finding methods to create fraudulent COVID-19 test results. This device was chosen because the reader unit used Bluetooth to send test results to the patient’s phone. As for the outcome of this research, WithSecure was successful in falsifying a COVID-19 test result and obtaining a certificate verifying that this COVID-19 test result was valid.
This article will go over the technical details of how this research was conducted against the following application and firmware versions:
Before diving into the technical details, we will give a brief overview of the test components and how it works. Two components are required to take a COVID-19 test by Cue Health, the Cue Reader and the Test Cartridge.
The Cue Reader unit contains a heater, gyroscope, and the necessary hardware to communicate with a user’s phone via Bluetooth. When taking a test, the Test Cartridge is inserted into the Cue Reader and the test itself is conducted within the Test Cartridge. The data is then passed to the reader, which is then transmitted to the user’s phone.
Each COVID-19 Test Cartridge comes with a nasal swab which is inserted into the cartridge. The Test Cartridge contains liquid which is released when the nasal swab is inserted, and gathers in one area in the cartridge where the test is conducted.
Now that we have established a brief overview of the device, now we can dig into how the Bluetooth traffic is structured. The Cue Reader communicated with the Android app via the Protobuf protocol over Bluetooth. This has the following general structure packet:
08 XX XX XX YY ZZ ZZ ZZ ZZ ZZ ZZ ZZ
Cue Health implemented a Protobuf parser in class b.l.h.o$b method y(). This implementation can be seen below:
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(); }
Each received Bluetooth message is processed through the above parser, and the class com.cuehealth.protobuf.reader.CueMessage method CueMessage() contained the types of Bluetooth messages the Cue Health app recognized in decimal format. As an example, per the below code snippet, packet types in decimal 170 (AA in HEX) are Test Result type data:
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;
By then referencing class com.cuehealth.protobuf.reader.TestResults method TestResults(), it was found that the data type value in decimal 34 (22 in HEX) represented the “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<AssayResult> assayResults_ = new ArrayList<AssayResult>(); n2 = n; this.assayResults_ = assayResults_; n7 = (n | 0x8); } n2 = n7; this.assayResults_.add((AssayResult)o.w(AssayResult.parser(), g0)); n = n7; continue; }
Following this to class com.cuehealth.protobuf.reader.AssayResult method AssayResult(), it was found that two types of data were used:
private AssayResult(Abstracto oVar, g0 g0Var) throws a1 { ... if (G == 8) { this.type_ = oVar.p(); } else if (G == 16) { this.code_ = oVar.p();
The class com.cuehealth.protobuf.reader.AssayResult method Code() contained information regarding what “code” values were expected. “Negative” was represented by decimal value 3 (3 in HEX):
public enum Code implements z0.Abstractc { INVALID(0), CONCENTRATION_LEVEL(1), POSITIVE(2), NEGATIVE(3), PASS(4), FAIL(5), UNRECOGNIZED(-1);
Within the same class, method Type() contained information on what “type” values were expected. The value for “Coronavirus” was decimal value 19 (13 in HEX):
public enum Type implements z0.Abstractc { 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);
Putting all of this together, a negative Coronavirus test result most likely looked like the following in HEX:
08 XX XX XX AA YY YY YY YY 22 04 08 13 10 03 YY YY
WithSecure developed a Frida script which hooked into the Java class class b.a.a.h.v3.k0$a method onCharacteristicChanged. This method was chosen to be hooked into due to it being one of the first methods called when processing Bluetooth traffic within the application. The script intercepted each Bluetooth packet sent to the application and performed the following steps:
Using this script, it was possible to change the test result within the Bluetooth data while being proctored by a Cue Health representative. Below is the output of the Frida script, showing that it had detected a negative test result and changed it to a positive result:
[#] 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
Below is a picture of the certificate obtained by WithSecure after the test was taken:
Files for this research can be found here: https://github.com/FSecureLABS/Cue-COVID-Test_Research-Files
WithSecure reached out to Cue Health and presented the findings above. Cue Health stated they have added checks server side which should detect manipulated test results.
Users should update their Cue Health mobile application at least the following versions:
When using the latest versions of the mobile application, the user will be prompted to update their Cue Reader if it is not running at least the following firmware version: 0.17.8 (5)