Faking a Positive COVID Test
Original post: https://labs.f-secure.com/blog/faking-another-positive-covid-test/
Introduction
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:
- Cue Health Android application com.cuehealth.healthapp version 1.4.3.134
- Cue Reader firmware version 0.17.6
Overview: Reader and Cartridge
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.
Technical Details: Bluetooth Traffic and Protobuf
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
- XX – packet count in Protobuf
- YY – the packet type (Cartridge Status, Test Result, etc)
- ZZ – the data itself
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:
type
is represented by decimal value8
(8
in HEX)code
is represented by decimal value16
(10
in HEX)
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
08
is the start of the packetXX
is the packet count in protobufAA
is a “Test Result” packet typeYY
is other “Test Result” data22
is AssayResult data04
is that the proceeding data is 4 bytes08 13
is type: coronavirus10 03
is code: negative
Obtaining a Modified COVID-19 Test Result
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:
- The Bluetooth traffic is put through the Cue Health Protobuf parser to help determine what type of packet was received by the Cue Reader
- If the data type is “Test Result”, then search for either HEX
220408131002
or220408131003
- Determine if the test result is positive or negative
- Switch the result (negative to positive, positive to negative)
- Put the modified data back into the app
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/Yogehi/Cue-COVID-Test_Research-Files
Contacting Cue Health and Remediation
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:
- Android - 1.7.2
- iOS - 1.7.1
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)