It's you

A project log for Reverse-Engineering a low-cost USB CO₂ monitor

I'm trying to get data out of a relatively low-cost (80€) CO₂ monitor that appears to have a USB connection for data as well as for power

Henryk PlötzHenryk Plötz 04/25/2015 at 01:010 Comments

Loading a new file into IDA for the second time was easier. Again, I had to prepare it with

upx -d HIDApi.dll

and this time all functions were already nicely labeled for me. And again I spent a lot of time being stupid, starting here:

This is the ReadUSB function in HIDApi.dll, but it doesn't appear to be doing much more than calling ReadFile. Well, unless you notice the call to sub_10001320 in the bottom right box. And, yep, that function is important all right:

It seems to operate on 8 bytes, does a couple of xors, shifts, has magic constants and loops. About three hours of manual work leads to this equivalent Python function:

def decrypt(key,  data):
    cstate = [0x48,  0x74,  0x65,  0x6D,  0x70,  0x39,  0x39,  0x65]
    shuffle = [2, 4, 0, 7, 1, 6, 5, 3]
    phase1 = [0] * 8
    for i, o in enumerate(shuffle):
        phase1[o] = data[i]
    phase2 = [0] * 8
    for i in range(8):
        phase2[i] = phase1[i] ^ key[i]
    phase3 = [0] * 8
    for i in range(8):
        phase3[i] = ( (phase2[i] >> 3) | (phase2[ (i-1+8)%8 ] << 5) ) & 0xff
    ctmp = [0] * 8
    for i in range(8):
        ctmp[i] = ( (cstate[i] >> 4) | (cstate[i]<<4) ) & 0xff
    out = [0] * 8
    for i in range(8):
        out[i] = (0x100 + phase3[i] - ctmp[i]) & 0xff
    return out

Yeah, it's a decryption function for a (bad) encryption system. It operates in several phases:

  1. In the first box, the input is taken from [eax+1] through [eax+8] and shuffled around. [eax+1] ends up at [eax+3], [eax+2] ends up at [eax+5], etc. Note that the indices in the Python version are zero-based.
  2. The second box iterates over a key stored at byte_1001A750 and XORs it into the data.
  3. The third box shifts the entire data by 3 bits to the right, using [esp+18h+arg_0] as a temporary store for wrapping around. [eax+8] becomes ([eax+8] >> 3) | ([eax+7] << 5); [eax+7] becomes ([eax+7] >> 3) | ([eax+6] <<5); etc.
  4. The fourth box does something with a buffer starting at [esp+18h+var_10], using esi as the loop index. At first I assumed that would be some kind of cipher state, that's why I called it cstate in the Python code. It was initialized with fixed values in the first box, and here, in the fourth box, it's nibble swapped and written into a buffer at [esp+18h+var_8], without changing the original state. I called that ctmp in the python version.
  5. The sixth box finally calculates the end result of the data in [eax+1] ff. by subtracting [esp+14h+var_8] ff. from it (using ecx as the loop index).

And now the satisfying part: Applying this function to some data I sniffed earlier yields:

41 00 00 41 0D 00 00 00
43 0C 9F EE 0D 00 00 00
42 12 87 DB 0D 00 00 00
6D 03 E2 52 0D 00 00 00
6E 4E C7 83 0D 00 00 00
71 03 39 AD 0D 00 00 00
50 03 38 8B 0D 00 00 00
57 22 C0 39 0D 00 00 00
Yeah \o/

There's the 0D line terminator we were promised, the checksum matches, and among other there are 42 and 50 data types. 0x338 is decimal 824, which matches the CO₂ readout at the time the sample was taken. I'm reasonably sure that the temperature wasn't 12.87°C though.

Now all we have to do is get the device to send data without the Windows tool and figure out how to interpret the wealth of data we're receiving.