Reverse Engineering a "Zombie" Smart Scale: My journey with QardioBase2 after the app vanished.
0 net
*Last week I didn’t publish, sorry. There’s a reason.
I am not the guy who uses the FartingCat (ChatGPT, in french, sounds like “chat, j’ai peté”, which means “cat, I farted”) to write posts on Linkedin, sorry. Why should one be willing to read something that the alleged author didn’t bother writing?
Plus, my audience is not Linkedin - and I don’t do rubbish to be regular. My bowels already are.
The problem
Like many IT people, I despise technology.
You come at my place? still you’d find analog stereo, real keys, dumb appliances, dumb neighbours that don’t mind their own business, and all the like. I have the place where I work (“Hangar 18”) which is something different, but my sancta sanctorum is strictly analog.
Well, not entirely. A few years ago I received this “QardioBase2 WiFi Smart Scale and Body Analyzer: monitor weight, BMI and body composition, easily store, track and share data. Free app for iOS, Android, Kindle. Works with Apple Health” (as Amazon describes it). Apple Health integration? Never bothered. But for sure, it had quite a good app.
Lately I wanted to install the app on a new iPhone 17, just to realise that the app in the App Store was not present anymore. After the usual swearing, I started digging a little bit on the internet to find that someone has programmed LibreArm - a free version for the QardioArm device, but it is a different thing. I fortunately have another device for blood pressure measurements.
The problem was still there then. So… so as mr. Taylor did some (great) necromancy on the blood pressure device, why not to do the same on the scale? From now onwards, I’ll call that post the Post .
The high-level plan
Reverse that zombie, bring it back to life. Write an app. Become rich and famous.
Ok, that’s the rubbish. High level plans and Executive summaries must be there, so let’s fill them in accordingly.
The real plan
Although I already know a lot about that scale, I want to treat this as a black box.
Open questions
Does it use wifi (and it asks you to connect to the internet, when initialising. Why?).
Does it use BLE?
working hypothesis: it uses both
Is there any form of encryption in the communications? how does it work?
how many internal states does the scale have? what internal states does the scale have?
is the communication bi-directional?
Assuming we can get a hold of the communication: what is the format of data?
endianness
data structures
measurement units
headers?
other weirdness?
And it’s pretty much it. Don’t be fooled by the relatively low number of questions - they are not so straightforward to answer.
Resources
we have an old copy of the App, so we could disassemble it straight away. Good luck, it’d take eons. But it’s always a good resource to have - especially in later stages.
the post . There’s also some code. Although it does not solve our problems, it can be of inspiration.
Understanding communications
It’s easier to start with BLE. At least, it’s a 1-1 communication. In the Hangar I have like a dozen of wifi networks and honestly I cannot remember into which one I enrolled the scale. It’ll be a nightmare of jumping and getting down from the scale. Taking notes will be my Cardio training.
So - old, good passive sniffing over-the-air. Packet logger should be sufficient. I know that sometimes it has its idiosyncrasies with the bluetooth chip - would that be the case, we’ll use the iPhone and log BLE traffic. We’ll cross that bridge.
Then wifi. While writing the previous paragraph I had the enlightenment: it suffices steer the internet traffic of the iPhone through an http proxy - hoping that these guys don’t use another exotic protocol. But I am kind of sure they don’t.
Certificate pinning is a thing — we’ll deal with it if we find it.
We’ll be able to draw our first set of conclusions, hopefully answering questions 1 and 2.
Then it’ll be the encryption carousel. I really hope there’s none - it would have been mentioned in the post - but we really need to understand this. Let’s be ready for it. My dream? Caesar’s cypher, with n=1! That’d address question 3 - from here onwards, the remaining part is just a matter of assigning semantics to raw data.
Preparing the playground
iOS
Well, I need to have detailed logs from the iPhone - hopefully that will be enough, worst case they’ll help a lot. Either way, I want verbose bluetooth logs - and to obtain these, I need a configuration profile. Without the latter, iOS would log only high level events. Good, but not good enough.
With this profile installed, bluetoothd will log pretty much everything: service discoveries, GATT characteristics, and the like.
So i point the iPhone browser to https://developer.apple.com/bug-reporting/profiles-and-logs/. Bluetooth section. It’s a trivial install.
Mac
Mac and iPhone will be connected via USB (USB C, in my case). Not a big deal.
Mac will not need any special program apart from the venerable Console . app .
Qardio scale
It suffices having the scale nearby. It’s about 1 m. away.
Reversing the connection
I first do some connection tests - all works fine (I am impressed. Lately working with Apple internals is what elicits my blasphemous tuscanian nature the most…). I happen to see the pieces of data I almost forgot, like
debug 16 : 09 : 01.586286 + 0000 bluetoothd Device found : CBDevice FC3BB896 - 15E5 - D2CB - C421 - 3 B43EBFE669F , BDA 5 C : D6 : 1 F : EA : CB : 2 D , Nm 'QardioARM'
in fact, the “old” phone is paired also with a QardioARM device (blood pressure meter - the one mentioned in the post ).
I also see that my airpods have three serial numbers:
Device found : CBDevice F77B65FB - 982E-8692 - A128 - 40 ADBAB4F526 , BDA 30 : D8 : 75 : 20 : 18 : 09 , Nm 'Gabriel' s AirPods Pro ', SN ' REDACTED1 ', SN Left' REDACTED2 ', SN Right ' REDACTED3 '
one per each speaker, and the last one for the case. Apple tracks everything. Also ears.
Then I also saw com . apple . icloud . searchpartyd passing by. This is supposedly the “Find my” process. Apple knows when I find my weight :)
com . apple . internal . carkitd - CarPlay. My car wants to know my weight…
Well, apart all these stupid jokes, it seems that everything works fine and therefore, we can begin the real thing.
The test
The test plan is trivial. It will be sufficient to:
select the right device in Console.app
launch the Qardio app on the iPhone
wait a bit - there could be precious handshaking information passing through
step on the scale
waiting for it to measure my weight
step off
keeping the app alive whilst waiting for the app to save data somewhere (we’ll see where afterwards) and feeling guilty for the weight - that’s always a good thing to do.
wait for some more time - there could be further delayed interactions.
quitting the app on the iphone
stopping the capture.
In principle, one could do this also from the terminal, but this time i went for Console. Truth is, Console.app is faster to set up for a first pass.
We need to accept the fact that this first run can only be very approximate.
At a first glance, I see three processes appearing during the measurement: bluetoothd , wifid , and Qardio . I start suspecting that my working hypothesis is true, but proving this requires more sound analyses.
Fortunately, Console supports textual copy-paste. And grep is our friend.
First, I start with bluetoothd .
Bluetooth
As I said - grep is my friend, therefore I create a file with only bluetoothd events:
3 gnever @ 0 xREVENG3 Desktop % ls - alh 2 nd \ Qardio . txt
- rw - r -- r -- 1 3 gnever staff 11 M 22 Mar 16 : 31 2 nd Qardio . txt
3 gnever @ 0 xREVENG3 Desktop % grep "bluetoothd" "2nd Qardio.txt" > bluetoothd - from - qardio . txt
3 gnever @ 0 xREVENG3 Desktop % ls - alh bluetoothd - from - qardio . txt
- rw - r -- r -- 1 3 gnever staff 106 K 22 Mar 17 : 24 bluetoothd - from - qardio . txt
Quite manageable. Navigating with a text editor this last file, I see some interesting facts, like:
peripheral’s name
peripheral’s MAC
its UUID
and the Bluetooth version.
One step at a time.
Device’s name
This reminds me of all those crappy movies ripped off from “The Exorcist” where the priest wants the name of the entity… however, let’s do it.
The first approach one can conceive is to start querying this database of logs, hoping to have the right enlightenment - and in fact, reversing has this trial-and-errors nature, after all.
But hey, here we are talking bluetooth, and the protocol has well defined phases, so let’s start with something that hopefully gives us quick results. The crucial phase in the whole bluetooth transaction is pairing - so why not…
3 gnever @ 0 xREVENG3 Desktop % grep - i pair bluetoothd - from - qardio . txt
debug 16 : 26 : 35.983020 + 0000 bluetoothd Device found : CBDevice FC3BB896 - 15E5 - D2CB - C421 - 3 B43EBFE669F , BDA 5 C : D6 : 1 F : EA : CB : 2 D , Nm 'QardioARM' , DsFl 0x800000 < Pairing > , DvF 0x2000 < BLEPaired > , CF 0x0 < > , unchanged
debug 16 : 26 : 36.023122 + 0000 bluetoothd Device found : CBDevice FC3BB896 - 15E5 - D2CB - C421 - 3 B43EBFE669F , BDA 5 C : D6 : 1 F : EA : CB : 2 D , Nm 'QardioARM' , DsFl 0x800000 < Pairing > , DvF 0x2000 < BLEPaired > , CF 0x0 < > , unchanged
...
debug 16 : 27 : 23.591469 + 0000 bluetoothd Device found : CBDevice FC3BB896 - 15E5 - D2CB - C421 - 3 B43EBFE669F , BDA 5 C : D6 : 1 F : EA : CB : 2 D , Nm 'QardioARM' , DsFl 0x800000 < Pairing > , DvF 0x2000 < BLEPaired > , CF 0x0 < > , unchanged
3 gnever @ 0 xREVENG3 Desktop % grep - i pair bluetoothd - from - qardio . txt | wc - l
16
Too optimistic of me. Especially when you have two Qardio devices connected to the phone. We need a better way to get to the data we want. Perhaps looking for device could help:
3 gnever @ 0 xREVENG3 Desktop % grep - i device bluetoothd - from - qardio . txt | grep - vi QardioARM | wc - l
218
Good news. We also know one thing about the scale: its name! It’s in its box, after all :) Therefore we can try also:
3 gnever @ 0 xREVENG3 Desktop % grep - i device bluetoothd - from - qardio . txt | grep - vi QardioARM | grep - i qardiobase | wc - l
30
Let’s see if this was a good intuition.
3 gnever @ 0 xREVENG3 Desktop % grep - i device bluetoothd - from - qardio . txt | grep - vi QardioARM | grep - i qardiobase
debug 16 : 26 : 35.833276 + 0000 bluetoothd Found device "DDD101E2-5809-1322-6FE7-CAD62DC14121 Public 5C:D6:1F:C4:1C:A3 RSSI:-82 with data:" QardioBase ", RSSI: 0 dB (non-saturated), Tx: -2 dB, Service UUIDs: C8219E89-93E0-4169-A3DC-EA7959E866AF, connectable, sourceCore: MainCore, IsELNAOn: 0, IsPassup: 0, IsFromSCCompensation0, IsCoexDenied0, Setting name to QardioBase
info 16:26:35.833937+0000 bluetoothd Device found new: CBDevice DDD101E2-5809-1322-6FE7-CAD62DC14121, BDA 5C:D6:1F:C4:1C:A3, Nm 'QardioBase', DvF 0x40000000000 < Connectable >, RSSI -82, Ch 39, AdTsMC <1293840675>, CF 0x80204000000 < New RSSI Attributes >
default 16:26:35.835410+0000 bluetoothd Device connecting - {cbuuid: DDD101E2-5809-1322-6FE7-CAD62DC14121, devicename: QardioBase, result: 0, adv-addr: 5C:D6:1F:C4:1C:A3-Public, resolved-addr: }
...
default 16:26:38.112385+0000 bluetoothd The device " DDD101E2 - 5809 - 1322 - 6 FE7 - CAD62DC14121 " is named " QardioBase "
...
default 16:27:15.772599+0000 bluetoothd Device disconnected - {cbuuid: DDD101E2-5809-1322-6FE7-CAD62DC14121, devicename: QardioBase, lmHandle: 0x55, adv-addr: 5C:D6:1F:C4:1C:A3-Public, resolved-addr: , result: 313}
debug 16:27:15.779532+0000 bluetoothd Device found: CBDevice DDD101E2-5809-1322-6FE7-CAD62DC14121, BDA 5C:D6:1F:C4:1C:A3, Nm 'QardioBase', DvF 0x40000200000 < Central Connectable >, CnS 0x400000 < BLE >, BTv 4.1, CF 0x800200000 < Connections DiscoveryFlags >, unchanged
....
debug 16:27:18.325975+0000 bluetoothd Device found: CBDevice DDD101E2-5809-1322-6FE7-CAD62DC14121, BDA 5C:D6:1F:C4:1C:A3, Nm 'QardioBase', DvF 0x40000200000 < Central Connectable >, CnS 0x400000 < BLE >, BTv 4.1, CF 0x800200000 < Connections DiscoveryFlags >, unchanged
default 16:27:18.326843+0000 bluetoothd Device lost: CBDevice DDD101E2-5809-1322-6FE7-CAD62DC14121, BDA 5C:D6:1F:C4:1C:A3, Nm 'QardioBase', DvF 0x40000200000 < Central Connectable >, CnS 0x400000 < BLE >, BTv 4.1, CF 0x800200000 < Connections DiscoveryFlags >
Quite dense:
these logs show that we have found the device name: Pazuzu QardioBase and its UUID: DDD101E2 - 5809 - 1322 - 6 FE7 - CAD62DC14121
As for the what happened - what I highlighted shows the following:
the device has been detected (16:26:35)
connection has been confirmed (16:26:38). iOS has confirmed the name via GAP.
connection has been lost (16:27:11) - actually I don’t remember what happened. It may be a program issue - should be investigated
16:27:15: device connected again
16:27:18: another disconnection… Blah!
So, long story short, we have:
Property
Value
Device name
QardioBase
Device UUID
DDD101E2 - 5809 - 1322 - 6 FE7 - CAD62DC14121
which is already a good beginning.
Taking a deeper look into the logs pasted above, we see the presence of the string BDA 5 C : D6 : 1 F : C4 : 1 C : A3 . BDA is how Apple delineates a Bluetooth Device Address - in other words, the MAC Address of the Bluetooth Device.
Furthermore, in this log entry we saw before:
debug 16 : 27 : 15.779532 + 0000 bluetoothd Device found : CBDevice DDD101E2 - 5809 - 1322 - 6 FE7 - CAD62DC14121 , BDA 5 C : D6 : 1 F : C4 : 1 C : A3 , Nm 'QardioBase' , DvF 0x40000200000 < Central Connectable > , CnS 0x400000 < BLE > , BTv 4.1 , CF 0x800200000 < Connections DiscoveryFlags > , unchanged
there appears the string BTv 4.1 - this is a simple declaration from the device, that declares its Bluetooth version as 4.1. It’s not crucial for us, but it’s good to know.
Therefore, we can update the table as follows:
Property
Value
Device name
QardioBase
Device UUID
DDD101E2 - 5809 - 1322 - 6 FE7 - CAD62DC14121
Bluetooth version
4.1
BDA/MAC Address
5 C : D6 : 1 F : C4 : 1 C : A3
I realise I mentioned GAP assuming that every reader knows what it is.
Unfortunately, reversing BlueTooth devices requires knowing the protocol, which is not the most frequent asset of pentesters/reversers (as for me: the only times I played with BT before was when I wanted to stop my former girlfriend playing like 20 times in a row the same song on a bluetooth loudspeaker. Then, I simply decided to pour some water onto the loudspeaker - easier).
For those who aren’t BlueTooth hardcore techies - GAP stands for Generic Access Profile . It’s a BLE profile defining how a devices announces itself/it’s discoverable to the other ones. In short, GAP defines the protocols for advertising (how the devices shares its name and its services), discovery (how a central finds the devices nearby), and connection establishment (basic procedures to connect). In our case, when we saw Setting name to QardioBase and The device is named "QardioBase" , the name has been read via GAP, by the Generic Access Service ( 0x1800 ). This last service is standardised and mandatory on all BLE devices.
So some more theory required here. I promise I’ll keep it as short as possible.
If GAP is concerned with how to find and connect to devices, GATT focuses on the interaction with them. We have already seen use cases (e.g. - “blood pressure monitor”); services which is a bunch related data grouped together by UUID; characteristics , the atomic datum or command within a service - identified by UUID as well; and a descriptor which is metadata regarding characteristics.
From this perspective, a BLE device can be seen as a GATT DataBase which exposes services and characteristics that the central (in my case, the old iPhone) reads, writes, subscribes, and interacts with.
Let’s try this query and see what we get:
3 gnever @ 0 xREVENG3 Desktop % grep - i "service\|uuid" bluetoothd - from - qardio . txt | grep - i "DDD101E2" | grep - iv "qardioarm"
debug 16 : 26 : 35.833276 + 0000 bluetoothd Found device "DDD101E2-5809-1322-6FE7-CAD62DC14121 Public 5C:D6:1F:C4:1C:A3 RSSI:-82 with data:" QardioBase ", RSSI: 0 dB (non-saturated), Tx: -2 dB, Service UUIDs: C8219E89-93E0-4169-A3DC-EA7959E866AF, connectable, sourceCore: MainCore, IsELNAOn: 0, IsPassup: 0, IsFromSCCompensation0, IsCoexDenied0, Setting name to QardioBase
default 16:26:35.834150+0000 bluetoothd App connecting - {cbuuid: DDD101E2-5809-1322-6FE7-CAD62DC14121, bundle: com.getqardio.Qardio}
default 16:26:35.835410+0000 bluetoothd Device connecting - {cbuuid: DDD101E2-5809-1322-6FE7-CAD62DC14121, devicename: QardioBase, result: 0, adv-addr: 5C:D6:1F:C4:1C:A3-Public, resolved-addr: }
debug 16:26:35.880693+0000 bluetoothd Found device " DDD101E2 - 5809 - 1322 - 6 FE7 - CAD62DC14121 Public 5 C : D6 : 1 F : C4 : 1 C : A3 RSSI : - 83 with data : "QardioBase" , RSSI : 0 dB ( non - saturated ), Tx : - 2 dB , Service UUIDs : C8219E89 - 93E0 - 4169 - A3DC - EA7959E866AF , connectable , sourceCore : MainCore , IsELNAOn : 0 , IsPassup : 0 , IsFromSCCompensation0 , IsCoexDenied0 , Setting name to QardioBase
default 16 : 26 : 35.977813 + 0000 bluetoothd Device ready - { cbuuid : DDD101E2 - 5809 - 1322 - 6 FE7 - CAD62DC14121 , devicename : QardioBase , lmHandle : 0x55 , adv - addr : 5 C : D6 : 1 F : C4 : 1 C : A3 - Public , resolved - addr : , result : 0 }
default 16 : 26 : 35.980199 + 0000 bluetoothd App ready - { cbuuid : DDD101E2 - 5809 - 1322 - 6 FE7 - CAD62DC14121 , bundle : com . getqardio . Qardio , transport : le , result : 0 }
default 16 : 27 : 15.772599 + 0000 bluetoothd Device disconnected - { cbuuid : DDD101E2 - 5809 - 1322 - 6 FE7 - CAD62DC14121 , devicename : QardioBase , lmHandle : 0x55 , adv - addr : 5 C : D6 : 1 F : C4 : 1 C : A3 - Public , resolved - addr : , result : 313 }
default 16 : 27 : 15.775114 + 0000 bluetoothd App disconnected - { cbuuid : DDD101E2 - 5809 - 1322 - 6 FE7 - CAD62DC14121 , bundle : com . getqardio . Qardio , reconnecting : N }
debug 16 : 27 : 15.814510 + 0000 bluetoothd Found device "DDD101E2-5809-1322-6FE7-CAD62DC14121 Public 5C:D6:1F:C4:1C:A3 RSSI:-88 with data:" QardioBase ", RSSI: 0 dB (non-saturated), Tx: -2 dB, Service UUIDs: C8219E89-93E0-4169-A3DC-EA7959E866AF, connectable, sourceCore: MainCore, IsELNAOn: 0, IsPassup: 0, IsFromSCCompensation0, IsCoexDenied0, Setting name to QardioBase
default 16:27:15.817358+0000 bluetoothd App connecting - {cbuuid: DDD101E2-5809-1322-6FE7-CAD62DC14121, bundle: com.getqardio.Qardio}
default 16:27:15.821300+0000 bluetoothd Device connecting - {cbuuid: DDD101E2-5809-1322-6FE7-CAD62DC14121, devicename: QardioBase, result: 0, adv-addr: 5C:D6:1F:C4:1C:A3-Public, resolved-addr: }
debug 16:27:15.840575+0000 bluetoothd Found device " DDD101E2 - 5809 - 1322 - 6 FE7 - CAD62DC14121 Public 5 C : D6 : 1 F : C4 : 1 C : A3 RSSI : - 85 with data : "QardioBase" , RSSI : 0 dB ( non - saturated ), Tx : - 2 dB , Service UUIDs : C8219E89 - 93E0 - 4169 - A3DC - EA7959E866AF , connectable , sourceCore : MainCore , IsELNAOn : 0 , IsPassup : 0 , IsFromSCCompensation0 , IsCoexDenied0 , Setting name to QardioBase
default 16 : 27 : 15.967087 + 0000 bluetoothd Device ready - { cbuuid : DDD101E2 - 5809 - 1322 - 6 FE7 - CAD62DC14121 , devicename : QardioBase , lmHandle : 0x59 , adv - addr : 5 C : D6 : 1 F : C4 : 1 C : A3 - Public , resolved - addr : , result : 0 }
default 16 : 27 : 15.970037 + 0000 bluetoothd App ready - { cbuuid : DDD101E2 - 5809 - 1322 - 6 FE7 - CAD62DC14121 , bundle : com . getqardio . Qardio , transport : le , result : 0 }
default 16 : 27 : 18.297794 + 0000 bluetoothd Device disconnected - { cbuuid : DDD101E2 - 5809 - 1322 - 6 FE7 - CAD62DC14121 , devicename : QardioBase , lmHandle : 0x59 , adv - addr : 5 C : D6 : 1 F : C4 : 1 C : A3 - Public , resolved - addr : , result : 307 }
default 16 : 27 : 18.322684 + 0000 bluetoothd App disconnected - { cbuuid : DDD101E2 - 5809 - 1322 - 6 FE7 - CAD62DC14121 , bundle : com . getqardio . Qardio , reconnecting : N }
This response already gives us the services part of GATT: C8219E89 - 93E0 - 4169 - A3DC - EA7959E866AF . How can I tell that this UUID is the proprietary service UUID? Well, it’s 128 bits, and services standardised by the BT SIG use 16-bit UUIDs. Therefore, a long UUID is a proprietary service - generated by device’s manufacturers for their custom services.
Other services will emerge from the GATT service discovery — more on that shortly.
Another interesting query will be:
3 gnever @ 0 xREVENG3 Desktop % grep - i service bluetoothd - from - qardio . txt
...
default 16 : 26 : 35.980330 + 0000 bluetoothd Received XPC message "CBMsgIdPeripheralDiscoverServices" from session "com.getqardio.Qardio-central-791-158"
debug 16 : 26 : 36.021058 + 0000 bluetoothd Sending XPC message "CBMsgIdPeripheralServicesDiscovered" to session "com.getqardio.Qardio-central-791-158"
default 16 : 26 : 36.021406 + 0000 bluetoothd Received XPC message "CBMsgIdServiceDiscoverCharacteristics" from session "com.getqardio.Qardio-central-791-158"
debug 16 : 26 : 36.022288 + 0000 bluetoothd Sending XPC message "CBMsgIdServiceCharacteristicsDiscovered" to session "com.getqardio.Qardio-central-791-158"
default 16 : 26 : 36.021586 + 0000 bluetoothd Received XPC message "CBMsgIdServiceDiscoverCharacteristics" from session "com.getqardio.Qardio-central-791-158"
debug 16 : 26 : 36.022939 + 0000 bluetoothd Sending XPC message "CBMsgIdServiceCharacteristicsDiscovered" to session "com.getqardio.Qardio-central-791-158"
default 16 : 26 : 36.022435 + 0000 bluetoothd Received XPC message "CBMsgIdServiceDiscoverCharacteristics" from session "com.getqardio.Qardio-central-791-158"
debug 16 : 26 : 36.027683 + 0000 bluetoothd Sending XPC message "CBMsgIdServiceCharacteristicsDiscovered" to session "com.getqardio.Qardio-central-791-158"
...
3 gnever @ 0 xREVENG3 Desktop %
When the app interrogates the device for services, bluetoothd responds ( ServicesDiscovered ) and for each found service, the app sends requests to get the characteristics ( DiscoverCharacteristics ) - bluetoothd responds with CharacteristicsDiscovered . Three rounds of characteristics discovery imply three services.
The only visible UUID is the one we found before.
Time to dig into the characteristics. Let’s go with the most natural grep :
3 gnever @ 0 xREVENG3 Desktop % grep - i "characteristic" bluetoothd - from - qardio . txt
...
default 16 : 26 : 36.028928 + 0000 bluetoothd Received XPC message "CBMsgIdCharacteristicNotifyValue" from session "com.getqardio.Qardio-central-791-158"
default 16 : 26 : 36.028996 + 0000 bluetoothd Received XPC message "CBMsgIdCharacteristicNotifyValue" from session "com.getqardio.Qardio-central-791-158"
default 16 : 26 : 36.028996 + 0000 bluetoothd Received XPC message "CBMsgIdCharacteristicNotifyValue" from session "com.getqardio.Qardio-central-791-158"
default 16 : 26 : 36.028996 + 0000 bluetoothd Received XPC message "CBMsgIdCharacteristicNotifyValue" from session "com.getqardio.Qardio-central-791-158"
default 16 : 26 : 36.029194 + 0000 bluetoothd Received XPC message "CBMsgIdCharacteristicReadValue" from session "com.getqardio.Qardio-central-791-158"
default 16 : 26 : 36.029215 + 0000 bluetoothd Received XPC message "CBMsgIdCharacteristicReadValue" from session "com.getqardio.Qardio-central-791-158"
default 16 : 26 : 36.029230 + 0000 bluetoothd Received XPC message "CBMsgIdCharacteristicWriteValue" from session "com.getqardio.Qardio-central-791-158"
...
3 gnever @ 0 xREVENG3 Desktop %
After the discovery phase, the app subscribes four characteristics for notifications ( NotifyValue four times); reads two characteristics ( ReadValue twice), and writes on one ( WriteValue - a single instance). This addresses question 5, about the directions of the data flow (namely - it’s bidirectional).
The final step is obtaining the specific handles of the characteristics:
3 gnever @ 0 xREVENG3 Desktop % grep - i "subscribing\|reading value\|writing value" qardiobase - only . txt
default 16 : 26 : 36.028965 + 0000 bluetoothd Subscribing to updates of characteristic handle 0x0019 on device "DDD101E2-5809-1322-6FE7-CAD62DC14121"
default 16 : 26 : 36.029011 + 0000 bluetoothd Subscribing to updates of characteristic handle 0x001c on device "DDD101E2-5809-1322-6FE7-CAD62DC14121"
default 16 : 26 : 36.029028 + 0000 bluetoothd Subscribing to updates of characteristic handle 0x002d on device "DDD101E2-5809-1322-6FE7-CAD62DC14121"
default 16 : 26 : 36.029169 + 0000 bluetoothd Subscribing to updates of characteristic handle 0x0030 on device "DDD101E2-5809-1322-6FE7-CAD62DC14121"
default 16 : 26 : 36.029206 + 0000 bluetoothd Reading value for characteristic value handle 0x000e , char handle 0x000d on device "DDD101E2-5809-1322-6FE7-CAD62DC14121"
default 16 : 26 : 36.029222 + 0000 bluetoothd Reading value for characteristic value handle 0x000c , char handle 0x000b on device "DDD101E2-5809-1322-6FE7-CAD62DC14121"
default 16 : 26 : 36.029267 + 0000 bluetoothd Writing value with response to characteristic handle 0x002e on device "DDD101E2-5809-1322-6FE7-CAD62DC14121"
default 16 : 26 : 38.832947 + 0000 bluetoothd Writing value without response to characteristic handle 0x002e on device "DDD101E2-5809-1322-6FE7-CAD62DC14121"
...
default 16 : 26 : 54.916492 + 0000 bluetoothd Reading value for characteristic value handle 0x0028 , char handle 0x0027 on device "DDD101E2-5809-1322-6FE7-CAD62DC14121"
...
3 gnever @ 0 xREVENG3 Desktop %
Observe that 0x002e is written twice: the first time with response and then without response . Moreover, 0x0028 is read way later - almost 20 seconds after the connection.
Therefore, at the end of this phase, the table becomes:
Property
Value
Device name
QardioBase
Device UUID
DDD101E2 - 5809 - 1322 - 6 FE7 - CAD62DC14121
Bluetooth version
4.1
BDA/MAC Address
5 C : D6 : 1 F : C4 : 1 C : A3
Proprietary Service UUID
C8219E89 - 93E0 - 4169 - A3DC - EA7959E866AF
Communication
Bidirectional
Characteristics
NotifyValue x4,
ReadValue x2,
WriteValue x1
Notify characteristics
0x0019 , 0x001c , 0x002d , 0x0030
Read characteristics
0x000e , 0x000c , 0x0028
Write characteristics
0x002e
… and we’re done with BLE. Now, the wifid .
wifid
The first query - quite naive - is:
3 gnever @ 0 xREVENG3 Desktop % grep - i "qardio" "2nd Qardio.txt" | grep - i wifid
debug 16 : 26 : 32.947741 + 0000 runningboardd Sending state 0xb054475d0 for app < com . getqardio . Qardio ( CE6D7FBD - 90 F3 - 4114 - 9 F4E - 8 D9080896142 ) > to [ osservice < com . apple . wifid > : 53 ]
default 16 : 26 : 32.949071 + 0000 wifid Received state update for 791 ( app < com . getqardio . Qardio ( CE6D7FBD - 90 F3 - 4114 - 9 F4E - 8 D9080896142 ) > , running - active - Visible
info 16 : 26 : 32.949251 + 0000 wifid Update delivered for [ app < com . getqardio . Qardio ( CE6D7FBD - 90 F3 - 4114 - 9 F4E - 8 D9080896142 ) > : 791 ] with taskState 4
debug 16 : 26 : 35.837357 + 0000 runningboardd Sending state 0xb054459d0 for app < com . getqardio . Qardio ( CE6D7FBD - 90 F3 - 4114 - 9 F4E - 8 D9080896142 ) > to [ osservice < com . apple . wifid > : 53 ]
default 16 : 26 : 35.838378 + 0000 wifid Received state update for 791 ( app < com . getqardio . Qardio ( CE6D7FBD - 90 F3 - 4114 - 9 F4E - 8 D9080896142 ) > , running - active - Visible
info 16 : 26 : 35.838505 + 0000 wifid Update delivered for [ app < com . getqardio . Qardio ( CE6D7FBD - 90 F3 - 4114 - 9 F4E - 8 D9080896142 ) > : 791 ] with taskState 4
default 16 : 26 : 50.441823 + 0000 wifid WiFiDeviceManagerCatsSetLowLatencyApp : CATSUpdate en0 : fgApp : com . getqardio . Qardio b = 0x0 rc = 1
default 16 : 26 : 52.673213 + 0000 wifid WiFiDeviceManagerCatsSetLowLatencyApp : CATSUpdate en0 : fgApp : com . getqardio . Qardio b = 0x0 rc = 1
debug 16 : 27 : 07.935585 + 0000 runningboardd Sending state 0xb05444ee0 for app < com . getqardio . Qardio ( CE6D7FBD - 90 F3 - 4114 - 9 F4E - 8 D9080896142 ) > to [ osservice < com . apple . wifid > : 53 ]
default 16 : 27 : 07.936119 + 0000 wifid Received state update for 791 ( app < com . getqardio . Qardio ( CE6D7FBD - 90 F3 - 4114 - 9 F4E - 8 D9080896142 ) > , running - active - Visible
info 16 : 27 : 07.936348 + 0000 wifid Update delivered for [ app < com . getqardio . Qardio ( CE6D7FBD - 90 F3 - 4114 - 9 F4E - 8 D9080896142 ) > : 791 ] with taskState 4
default 16 : 27 : 10.247198 + 0000 wifid WiFiDeviceManagerCatsSetLowLatencyApp : CATSUpdate en0 : fgApp : com . getqardio . Qardio b = 0x0 rc = 1
debug 16 : 27 : 10.905860 + 0000 runningboardd Sending state 0xb05447e90 for app < com . getqardio . Qardio ( CE6D7FBD - 90 F3 - 4114 - 9 F4E - 8 D9080896142 ) > to [ osservice < com . apple . wifid > : 53 ]
default 16 : 27 : 10.906356 + 0000 wifid Received state update for 791 ( app < com . getqardio . Qardio ( CE6D7FBD - 90 F3 - 4114 - 9 F4E - 8 D9080896142 ) > , running - active - Visible
info 16 : 27 : 10.906620 + 0000 wifid Update delivered for [ app < com . getqardio . Qardio ( CE6D7FBD - 90 F3 - 4114 - 9 F4E - 8 D9080896142 ) > : 791 ] with taskState 4
default 16 : 27 : 11.840672 + 0000 wifid WiFiDeviceManagerCatsSetLowLatencyApp : CATSUpdate en0 : fgApp : com . getqardio . Qardio b = 0x0 rc = 1
This per se confirms that the app has two lines of communication - BlueTooth and WiFi. Neat. wifid knows that Qardio is in foreground ( running - active - Visible , taskState 4 ) and gives it priority on the network itself ( CATSUpdate ... LowLatencyApp ), but there is no connection whatsoever shown here. A deeper analysis wouldn’t show anything special, at the wifid level - at least, not now - so we move to the Qardio entries.
Property
Value
Device name
QardioBase
Device UUID
DDD101E2 - 5809 - 1322 - 6 FE7 - CAD62DC14121
Bluetooth version
4.1
BDA/MAC Address
5 C : D6 : 1 F : C4 : 1 C : A3
Proprietary Service UUID
C8219E89 - 93E0 - 4169 - A3DC - EA7959E866AF
Communication
Bidirectional
Notify characteristics
0x0019 , 0x001c , 0x002d , 0x0030
Read characteristics
0x000e , 0x000c , 0x0028
Write characteristics
0x002e
Communication media
BlueTooth, IP Connection
Qardio
A deeper look into the Qardio entries tells the rest of the story:
3 gnever @ 0 xREVENG3 Desktop % grep - i "tcp_disconnect\|nw_connection" "2nd Qardio.txt" | grep - iv "necp" | head - 20
...
info 16 : 26 : 26.538347 + 0000 Qardio nw_connection_create_with_id [ C70 ] create connection to Hostname #d6701f48:443
info 16 : 26 : 26.538652 + 0000 Qardio nw_connection_endpoint_report ... [ C70 Hostname #d6701f48:443 waiting parent-flow (satisfied (Path is satisfied), interface: en0[802.11], ipv4, dns, uses wifi, LQM: good)]
default 16 : 26 : 26.538738 + 0000 Qardio nw_connection_report_state_with_handler_on_nw_queue [ C70 ] reporting state preparing
info 16 : 26 : 26.600951 + 0000 Qardio nw_protocol_tcp_disconnect [ C70 . 1.1 . 1 : 3 ] input protocol initiated disconnect
default 16 : 26 : 26.601155 + 0000 Qardio nw_connection_report_state_with_handler_on_nw_queue [ C70 ] reporting state cancelled
info 16 : 26 : 26.601443 + 0000 Qardio nw_connection_create_with_id [ C71 ] create connection to Hostname #d6701f48:443
...
3 gnever @ 0 xREVENG3 Desktop %
The WiFi path is perfectly healthy — Path is satisfied , LQM : good , uses wifi . The app is reaching out to a backend over HTTPS (port 443), hostname redacted by iOS. But every single connection goes straight from preparing to cancelled , and the app immediately retries — C69, C70, C71, and so on, for the entire duration of the session.
The conclusion is straightforward: the Qardio backend is unreachable. The servers are gone. The company went bankrupt, and the lights went out. This also explains the save error we saw during the tests — the scale measured, the app tried to sync, and got nothing back.
For our purposes, this is actually fine. We don’t need the backend — we just need the scale.
Conclusions
Reversing rarely needs super-weapons. The whole exercise could have been done with a mint machine (no, no Linux Mint. Mint, as in fresh. Although I could have done this also with my Linux laptop, huh).
This phase - which I led as a mix between forensics and discovery activities - is quite boring. Was it inconclusive? Kind of. I hoped that all data flew unredacted between the scale and the phone - but that’s reasonably impossible. Legal reasons, largely prevent this kind of things. Me, wishful thinking…
The next step? well, I need that scale. Therefore I will reverse the app. What else?
Obviously, this kind of activities is time consuming. The mantra still holds - I don’t publish because I must, I publish because I have something to say. It will take some time before you see the second part of this post - but as I said: I need a scale, therefore I will reverse that app. With time, indeed.
I hope you enjoyed this, and that you got a glimpse into the modus operandi of a reverser.
Have fun, reverse your sockets and socks, and be paranoid!
Share on
X
Facebook
LinkedIn
Bluesky