-
All your base are belong to us
05/14/2015 at 15:27 • 13 commentsSo now that we have figured out all the puzzle pieces (do we?) we can go over to the last project phase and actually build something that extracts data from the sensor on its own initiative and feeds it into something with a purpose.
Setting: Ubuntu 14.04 LTS, Linux 3.13.0-52-generic, Python 2.7.6. As we recall from one of the earliest logs we can initiate data transmission by issuing a HID SET_REPORT on the USB device. Luckily, Linux offers a very simple API to do just that by way of the hidraw device node we were using at the very beginning. Since we're not interested in any security (which the protocol doesn't offer anyway) we'll just use a static key.
with file("/dev/hidraw0","a+b") as co2: # Key retrieved from /dev/random, guaranteed to be random ;) key = [0xc4, 0xc6, 0xc0, 0x92, 0x40, 0x23, 0xdc, 0x96] co2.write( "\x00" + "".join(chr(e) for e in key) ) while True: data = co2.read(8) print " ".join("%02X" % e for e in data)
If we run that code (after fixing permissions, we'll get to the part later) we get … nothing. Bummer.This means it's time to compare exactly what we're doing on the USB with what the Windows software is doing, breaking out the wireshark again:It certainly does look right: there's 8 bytes of payload (note that we wrote 9 bytes into the device file, but the first byte only specifies the report number, 0 in this case; also note that we have seen the same convention earlier in the call to HidD_SetFeature in the Windows code). By carefully comparing this USB packet to the one observed earlier we can figure out what's wrong: Our wValue is 0x0200, theirs was 0x0300. When looking into the USB HID Device Class specification (section 7.2.1 on page 51), or alternatively just looking at the wireshark parse, we see: We sent an "Output" SET_REPORT, they sent a "Feature" SET_REPORT. Returning to the Linux HIDRAW API documentation tells us: In order to send a feature report we can't use a simple write but need to use the HIDIOCSFEATURE ioctl.In Python, ioctls can be sent using the fcntl.ioctl() method, but we need the ioctl number for that. We find HIDIOCSFEATURE defined in hidraw.h as:
#define HIDIOCSFEATURE(len) _IOC(_IOC_WRITE|_IOC_READ, 'H', 0x06, len)
which isn't terribly helpful. The pragmatic solution is to compile a short C program and let it print the actual value (note: we're already hard coding the len parameter as 9, which is the only value we will need):#include <linux/hidraw.h> #include <sys/ioctl.h> #include <stdio.h> int main(void) { printf("0x%08X\n", HIDIOCSFEATURE(9)); return 0; }
This tells us that the value of HIDIOCSFEATURE(9) actually is 0xC0094806. So, now for the real, actually working Python code:#!/usr/bin/env python import sys, fcntl, time 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 def hd(d): return " ".join("%02X" % e for e in d) if __name__ == "__main__": # Key retrieved from /dev/random, guaranteed to be random ;) key = [0xc4, 0xc6, 0xc0, 0x92, 0x40, 0x23, 0xdc, 0x96] fp = open(sys.argv[1], "a+b", 0) HIDIOCSFEATURE_9 = 0xC0094806 set_report = "\x00" + "".join(chr(e) for e in key) fcntl.ioctl(fp, HIDIOCSFEATURE_9, set_report) values = {} while True: data = list(ord(e) for e in fp.read(8)) decrypted = decrypt(key, data) if decrypted[4] != 0x0d or (sum(decrypted[:3]) & 0xff) != decrypted[3]: print hd(data), " => ", hd(decrypted), "Checksum error" else: op = decrypted[0] val = decrypted[1] << 8 | decrypted[2] values[op] = val # Output all data, mark just received value with asterisk print ", ".join( "%s%02X: %04X %5i" % ([" ", "*"][op==k], k, v, v) for (k, v) in sorted(values.items())), " ", ## From http://co2meters.com/Documentation/AppNotes/AN146-RAD-0401-serial-communication.pdf if 0x50 in values: print "CO2: %4i" % values[0x50], if 0x42 in values: print "T: %2.2f" % (values[0x42]/16.0-273.15), if 0x44 in values: print "RH: %2.2f" % (values[0x44]/100.0), print
Et voilà! I've skipped one intermediary step where the temperature readings didn't make any sense, since they don't follow the protocol documentation we discovered at first. Thankfully, the American vendor has directory listings active on its AppNotes directory, allowing us to discover another protocol spec that documents the correct formula for the temperature reading. It also describes the relative humidity data point (for which my device has no sensor), so I've added decoding support for all three items: CO₂ (in ppm), temperature (in °C), and relative humidity (in %).
This handy tool also prints all raw data items received, so that we may be able to make some sense of the additional data in the future. For easier reading each line prints the last value received for each item, the item received most recently is marked with an asterisk.
*42: 128F 4751 T: 23.79 42: 128F 4751, *6D: 03E4 996 T: 23.79 42: 128F 4751, 6D: 03E4 996, *6E: 514A 20810 T: 23.79 42: 128F 4751, 6D: 03E4 996, 6E: 514A 20810, *71: 01C9 457 T: 23.79 42: 128F 4751, *50: 01C8 456, 6D: 03E4 996, 6E: 514A 20810, 71: 01C9 457 CO2: 456 T: 23.79 42: 128F 4751, *4F: 21A3 8611, 50: 01C8 456, 6D: 03E4 996, 6E: 514A 20810, 71: 01C9 457 CO2: 456 T: 23.79 42: 128F 4751, 4F: 21A3 8611, 50: 01C8 456, *52: 269C 9884, 6D: 03E4 996, 6E: 514A 20810, 71: 01C9 457 CO2: 456 T: 23.79 *41: 0000 0, 42: 128F 4751, 4F: 21A3 8611, 50: 01C8 456, 52: 269C 9884, 6D: 03E4 996, 6E: 514A 20810, 71: 01C9 457 CO2: 456 T: 23.79 41: 0000 0, 42: 128F 4751, *43: 0CEB 3307, 4F: 21A3 8611, 50: 01C8 456, 52: 269C 9884, 6D: 03E4 996, 6E: 514A 20810, 71: 01C9 457 CO2: 456 T: 23.79 41: 0000 0, *42: 128F 4751, 43: 0CEB 3307, 4F: 21A3 8611, 50: 01C8 456, 52: 269C 9884, 6D: 03E4 996, 6E: 514A 20810, 71: 01C9 457 CO2: 456 T: 23.79 41: 0000 0, 42: 128F 4751, 43: 0CEB 3307, 4F: 21A3 8611, 50: 01C8 456, 52: 269C 9884, *6D: 03E4 996, 6E: 514A 20810, 71: 01C9 457 CO2: 456 T: 23.79 41: 0000 0, 42: 128F 4751, 43: 0CEB 3307, 4F: 21A3 8611, 50: 01C8 456, 52: 269C 9884, 6D: 03E4 996, *6E: 514A 20810, 71: 01C9 457 CO2: 456 T: 23.79 41: 0000 0, 42: 128F 4751, 43: 0CEB 3307, 4F: 21A3 8611, 50: 01C8 456, 52: 269C 9884, 6D: 03E4 996, 6E: 514A 20810, *71: 01C9 457 CO2: 456 T: 23.79 41: 0000 0, 42: 128F 4751, 43: 0CEB 3307, 4F: 21A3 8611, *50: 01C8 456, 52: 269C 9884, 6D: 03E4 996, 6E: 514A 20810, 71: 01C9 457 CO2: 456 T: 23.79 41: 0000 0, 42: 128F 4751, 43: 0CEB 3307, 4F: 21A3 8611, 50: 01C8 456, 52: 269C 9884, *57: 22C2 8898, 6D: 03E4 996, 6E: 514A 20810, 71: 01C9 457 CO2: 456 T: 23.79 41: 0000 0, 42: 128F 4751, 43: 0CEB 3307, 4F: 21A3 8611, 50: 01C8 456, 52: 269C 9884, *56: 26A6 9894, 57: 22C2 8898, 6D: 03E4 996, 6E: 514A 20810, 71: 01C9 457 CO2: 456 T: 23.79 *41: 0000 0, 42: 128F 4751, 43: 0CEB 3307, 4F: 21A3 8611, 50: 01C8 456, 52: 269C 9884, 56: 26A6 9894, 57: 22C2 8898, 6D: 03E4 996, 6E: 514A 20810, 71: 01C9 457 CO2: 456 T: 23.79 41: 0000 0, 42: 128F 4751, *43: 0CE9 3305, 4F: 21A3 8611, 50: 01C8 456, 52: 269C 9884, 56: 26A6 9894, 57: 22C2 8898, 6D: 03E4 996, 6E: 514A 20810, 71: 01C9 457 CO2: 456 T: 23.79 41: 0000 0, *42: 128F 4751, 43: 0CE9 3305, 4F: 21A3 8611, 50: 01C8 456, 52: 269C 9884, 56: 26A6 9894, 57: 22C2 8898, 6D: 03E4 996, 6E: 514A 20810, 71: 01C9 457 CO2: 456 T: 23.79
Next thing up: This Python script needs to be called with the hidraw device node as its first argument. In previous snippets I've always assumed /dev/hidraw0, but this may be different between different computers and maybe on each boot (if you have USB HID devices). Also we needed to manually give our user permissions on the device node. Let's fix it with an udev rule (this goes as 90-co2mini.rules into /etc/udev/rules.d):
ACTION=="remove", GOTO="co2mini_end" SUBSYSTEMS=="usb", KERNEL=="hidraw*", ATTRS{idVendor}=="04d9", ATTRS{idProduct}=="a052", GROUP="plugdev", MODE="0660", SYMLINK+="co2mini%n", GOTO="co2mini_end" LABEL="co2mini_end"
This will a) allow the group plugdev (change as appropriate, or put your user into that group) access to the device node and b) symlink the hidraw devices of all connected CO₂ monitors (if there's more than one :) in nice numbered files starting with /dev/co2mini0.
Perl code to receive data and FHEM integration are up next.
-
How are you, gentlemen
04/26/2015 at 18:40 • 0 commentsNow that I've had my breakthrough I can leisurely stroll around the rest of the code for completeness sake. This should show me how and what data is being sent to the device to initiate data reporting.
First up: WriteUSB in HIDApi.dll. Unsurprisingly this calls an encryption function sub_100011E0 immediately before doing WriteFile, which looks very much like the decryption function but in reverse (Actually that's an assumption: I haven't properly analyzed it):
During my interactive testing with the IDA debugger I've never seen WriteUSB called though, so it's not related to device initialization and therefore not of immediate interest. In some of the documentation I saw functionality described that would allow you to send a calibration offset to the device to correct for sensor drift over time, so that's probably what WriteUSB will be used for. I might have to come back to it at a later time.
USBInit is disappointingly short:
(I've labeled byte_1001A750, which you might remember from the last post, as usb_enc_key.)This function simply initializes the memory where the encryption key will be stored with a fixed value. I've never seen this value used, so it must be overwritten somewhere and the initial SET_REPORT must be generated.
After looking through the cross references on usb_enc_key I found this code in FindUSB:
It does two things: First it calls sub_100010A0 with a pointer to usb_enc_key, then it prepares arguments to HidD_SetFeature and calls it. The call to HidD_SetFeature gets three arguments: esi (was set somewhere before this code part, I assume it's a reference to the device to use), a pointer to esp+…+var_C and the number 9. esp+…+var_C seems to be a buffer of 9 bytes which first is initialized to zero in three steps (mov [esp+…+var_c], eax; mov [esp+…+var_8]+eax; mov [esp+…+var_4], al; where eax contains 0) and then the usb_enc_key is copied into offset 1 of it. So it contains a zero byte followed by usb_enc_key. Funnily enough we'll later see the same structure when we write Linux code.sub_100010A0 now is what actually initializes usb_enc_key with some pseudo-random stuff. It starts with a call to _time, has lots of magic numbers, and xors, and calls to _localtime. I post it here only for completeness' sake, I haven't actually looked at it in depth:
So, still what remains is getting data reporting started with Linux code. We'll look at both Python (because that's my preferred language) and Perl (because that's what FHEM is written in) implementations. -
It's you
04/25/2015 at 01:01 • 0 commentsLoading 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:
- 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.
- The second box iterates over a key stored at byte_1001A750 and XORs it into the data.
- 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.
- 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.
- 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.
-
Main screen turn on
04/25/2015 at 00:00 • 0 commentsSo, looking at the transmitted data wasn't that successful in figuring out the protocol. My normal approach now would be to 'spoof' the other side of the communication channel and see how the device under test changes behaviour. In this case I would've written a short piece of Python to send out this initial SET_REPORT packet and then vary contents and look at how the device responds differently. But that's some boring work I've done several times before with other devices, so I'd wanted to try something new and this would be the perfect chance. I've never reverse-engineered anything by looking at a disassembly, so let's dive in!
The friendly people at Hex-Rays offer a freeware version of IDA Pro 5 for non-commercial use. It's not the latest and greatest, but should suffice. I installed it and loaded the ZG.exe file with full analysis turned on. This is the result:
That doesn't seem right. Scrolling through only shows a lot of data, but no functions. And the strings window has none of the user interface strings we were seeing when running the program. I've also tried some online disassembler services, and one of them gave a crucial hint: It identified the program as packed with UPX. IDA seems to have noticed too (there's this "UPX1" prefix to the left), but wasn't doing anything about it. Or maybe I didn't know to tell it to do something about it.
As the wikipedia article tells us, UPX is available for Linux and comes with a decompressor, so we can use
upx -d ZG.exe
and try IDA again, this time around analysis takes much longer:Hey, better. There's the "assembler code in boxes connected with arrows" UI I've seen other people use. And also the strings window seems to be usefully filled.We know that the program somehow gets data from the USB and then decodes this into CO₂ readings to display. So the obvious point of attack would seem to be to find the place where it reads from USB and then follow to where the data is processed to get an insight into how the packets are being decoded.
Doing a search for "read" in the names window at first doesn't yield a lot of useful things. There's a "aTusbthread0", but I couldn't make sense of what it did. There also are a couple of "TComm::Read…", which seem to point to an entire include serial communications library. But then finally:
Bingo! These seem to be strings that indirectly reference functions in the "HIDApi.dll" (which comes in the same directory as the .exe file). Following the cross reference gets me toBut what does that do? [Insert "I have no idea what I'm doing" dog meme here]After being stupid for a couple hours I decided that apparently it would "LoadLibrary" the HIDApi.dll and then call "GetProcAddress" for each of "FindUSB", "WriteUSB", and "ReadUSB" and store the resulting value (likely a pointer to the respective functions) into memory. To better keep track I renamed the memory locations: dword_4FCADC became HID_FindUSB, dword_4FCAE0 became HID_WriteUSB, and dword_4FCAE4 became HID_ReadUSB. And, bonus, the cross-reference information shows that HID_ReadUSB is only used twice: Once here to write it, and a second time, in a call:
As you can see from the graph overview window there's quite a lot going on here, and the whole thing is a giant loop. (You may not be surprised to learn that I later found out that this is the body to the aforementioned USB-Thread structure.)I'm skipping a couple of steps here: For a time I was distracted by byte_4FCAAE which seems to decide whether a read or write on the USB will happen. I also tried out the integrated debugger and was single stepping the program from the return of the HID_ReadUSB call on, and then found:
That box there in the middle, it's reading single bytes from [ecx+1], [ecx+2] and storing them at byte_4FCAB1 and following. On a hunch, let's call them usbread0, usbread1 etc. and see where they're used. First up:Now, that's strange. It sums up the first 3 bytes and compares the result to the 4th byte. That matches how the checksum for the protocol documentation we have would be computed. If the checksum matches it calls sub_4031F4 (which happens to be next in our list of cross references to look at) and … holy shit, we hit the mother lode:This code stores (usbread1<<8) | usbread2 into [ebp+var_2] and then does a mighty big case comparison on usbread0 (stored in ecx) starting with checking if it's less than, equal, or greater than 5A. This obviously is the protocol parser. And it matches the protocol documentation we have (though it knows about more opcodes than just 50 and 42).Verification in the debugger shows that, yes, the output data of HID_ReadUSB really contains the protocol as parsed here. So whatever has been done to the USB traffic must have happened earlier, in the HIDApi.dll. That will be what we load into IDA next.
-
What
04/24/2015 at 22:13 • 0 commentsIn past projects I've had raging successes by just looking at the raw data, XORing stuff together, and maybe squinting my eyes a little. Spoiler: Not so this time. I'll walk you through the steps anyway.
What we know: Data comes in in packets of 8 bytes each. Somewhere in there should be two data points: temperature and CO₂ readings. The manufacturer protocol data sheet obviously doesn't apply, but maybe the real protocol is at least similar. That would mean that we expect that there should be an opcode (CO₂ vs. temp), a checksum, and a 16-bit data value. Earlier we noticed that there appears to be a structure within each 8-byte-packet, namely two 4-byte-structures with a fixed byte each in second place. That would be consistent with a protocol along the lines of 2 x [checksum, opcode, 16 bit data]. So, ... let's just plot each byte separately, trying to see whether there's any continuity over time (suggesting a slowly changing CO₂ or temperature value) in any byte:
with file("/dev/hidraw0","rb") as src: with file("co2_dez.log", "w") as dst: while True: data = src.read(8) print >>dst, " ".join(str(ord(e)) for e in data)
will read packets of 8 byte each from /dev/hidraw0 (you may need to give your user access to that device node) and dump then as decimal numbers, 8 on each line, into co2_dez.log. Press Ctrl-C after letting it run for a minute. The resulting file can then be plotted in GNU octave:a = load("co2_dez.log"); plot(a)
Hmm, nope. Most of the data is all over the place. (The voice of hindsight says: There's some structure I should have noticed, but didn't.)
Next thing to try whenever some protocol wants to annoy you by doing stupid XOR 'encryption' tricks is to XOR successive data packets, which would remove their 'encryption' and give you the XOR of the raw values. So, in Python:
prev = [] with file("/dev/hidraw0","rb") as src: while True: data = [ord(e) for e in src.read(8)] if prev: xor = [a^b for a,b in zip(prev, data)] print " ".join("%02X" % e for e in xor) prev = data
yielding output similar to:
D6 00 00 00 A2 00 00 60 63 00 08 00 57 00 00 78 DC 00 B8 01 9E 00 00 F8 42 00 1E 00 E8 00 00 D0 49 00 3E 00 F3 00 00 E0 03 00 08 01 00 00 00 08 FE 00 39 00 0A 00 00 C8 B8 00 28 00 2F 00 00 F8 27 00 99 00 55 00 00 E8 CE 00 10 00 A2 00 00 48 7B 00 08 00 57 00 00 50 DC 00 B8 01 9E 00 00 F8 42 00 1E 00 E8 00 00 D0 49 00 3E 00 F3 00 00 E0 03 00 08 01 00 00 00 08 61 00 79 00 02 00 00 18 AF 00 08 00 20 00 00 80 AF 00 F9 00 52 00 00 40 C6 00 10 00 A2 00 00 50 73 00 08 00 57 00 00 48
For slowly shifting values we'd expect a lot of 0s (if the value stays the same), a couple of low bits set only (f.e. when going from 0x42 to 0x43) and the occasional almost full byte (f.e. when going from 0x5f to 0x60). None of the bytes here match that. But there's something odd going on in the fourth byte: 01, twice 00, 01, five times 00, 01, twice 00, 01 etc.
We only assumed that each data packet contains CO₂ and temperature, but what if there are different types of data packets? Or what if the XOR key is not the same per packet? In this case simply XORing successive 8-byte-units wouldn't leave any useful results. … As we seem to have seen.
So, new theory: successive 8-byte-packets are not directly related, but there may be some larger structure.
To analyze this possibility we need a new data file that treats all bytes as independent:
with file("/dev/hidraw0","rb") as src: with file("co2_dez_ind.log", "w") as dst: while True: data = [ord(e) for e in src.read(8)] print >>dst, "\n".join(str(e) for e in data)
Now we can do a cross correlation. This is based on the idea that functionally identical bytes in the incoming data stream will also be numerically similar, at least as long as we're not violently changing data values (and CO₂ concentration and temperature remain rather constant in the short term). For this reason we will also be running the data gathering for a rather long time (upwards of 5 minutes) so that the correlation will shine through whatever bullshit was layered on top of it. Then, in GNU octave:a = load("co2_dez_ind.log"); [b c] = xcorr(a); plot(c, b)
(I've zoomed into an interesting section for this screenshot.) Now that's more useful: Apart from the trivial correlation at offset 0 there are regular peaks every 72 bytes (that's 9 8-byte-packets), and, surprisingly, an even slightly stronger correlation at 144 bytes (that's 18 8-byte packets).
Re-trying the XOR thing for 144 bytes per packet and then trying to discern anything didn't seem too attractive. A friend of mine was also looking into the protocol from this angle, so I decided to shift gears here and try something completely new to me: Software reverse engineering. To the IDA Pro!
-
Somebody set up us the bomb
04/17/2015 at 02:05 • 1 commentLuckily reverse engineering funky binary protocols is one of my specialities, so, let's see …
Data seems to arrive in 8-byte packets always. Indeed, this appears to be a property of what they did with the HID spec (more on that in a moment). The timing between 8-byte packets is irregular. In each packet there seem to be two groups of 4 byte each. The second byte of each group is always the same.
Getting more data by re-starting the Windows software makes things more confusing: The second byte of each group is still constant, but different now. It's numerically close though, so, time-based? This sucks.
Maybe I can learn something more from the USB HID device descriptor… (Intermission music playing while I google how to get the HID descriptor under Linux)
# echo 1-1.2:1.0 > /sys/bus/usb/drivers/usbhid/unbind # lsusb -vd 04d9:a052 Bus 001 Device 014: ID 04d9:a052 Holtek Semiconductor, Inc. Device Descriptor: bLength 18 bDescriptorType 1 bcdUSB 1.10 bDeviceClass 0 (Defined at Interface level) bDeviceSubClass 0 bDeviceProtocol 0 bMaxPacketSize0 8 idVendor 0x04d9 Holtek Semiconductor, Inc. idProduct 0xa052 bcdDevice 1.00 iManufacturer 1 Holtek iProduct 2 USB-zyTemp iSerial 3 1.40 bNumConfigurations 1 Configuration Descriptor: bLength 9 bDescriptorType 2 wTotalLength 34 bNumInterfaces 1 bConfigurationValue 1 iConfiguration 0 bmAttributes 0x80 (Bus Powered) MaxPower 100mA Interface Descriptor: bLength 9 bDescriptorType 4 bInterfaceNumber 0 bAlternateSetting 0 bNumEndpoints 1 bInterfaceClass 3 Human Interface Device bInterfaceSubClass 0 No Subclass bInterfaceProtocol 0 None iInterface 0 HID Device Descriptor: bLength 9 bDescriptorType 33 bcdHID 1.10 bCountryCode 0 Not supported bNumDescriptors 1 bDescriptorType 34 Report wDescriptorLength 53 Report Descriptor: (length is 53) Item(Global): Usage Page, data= [ 0x00 0xff ] 65280 (null) Item(Local ): Usage, data= [ 0x01 ] 1 (null) Item(Main ): Collection, data= [ 0x01 ] 1 Application Item(Global): Logical Minimum, data= [ 0x00 ] 0 Item(Global): Logical Maximum, data= [ 0xff 0x00 ] 255 Item(Local ): Usage Minimum, data= [ 0x00 ] 0 (null) Item(Local ): Usage Maximum, data= [ 0xff ] 255 (null) Item(Global): Report Count, data= [ 0x08 ] 8 Item(Global): Report Size, data= [ 0x08 ] 8 Item(Main ): Feature, data= [ 0x02 ] 2 Data Variable Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Global): Logical Minimum, data= [ 0x00 ] 0 Item(Global): Logical Maximum, data= [ 0xff 0x00 ] 255 Item(Local ): Usage Minimum, data= [ 0x00 ] 0 (null) Item(Local ): Usage Maximum, data= [ 0xff ] 255 (null) Item(Global): Report Count, data= [ 0x08 ] 8 Item(Global): Report Size, data= [ 0x08 ] 8 Item(Main ): Input, data= [ 0x02 ] 2 Data Variable Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Global): Logical Minimum, data= [ 0x00 ] 0 Item(Global): Logical Maximum, data= [ 0xff 0x00 ] 255 Item(Local ): Usage Minimum, data= [ 0x00 ] 0 (null) Item(Local ): Usage Maximum, data= [ 0xff ] 255 (null) Item(Global): Report Count, data= [ 0x08 ] 8 Item(Global): Report Size, data= [ 0x08 ] 8 Item(Main ): Output, data= [ 0x02 ] 2 Data Variable Absolute No_Wrap Linear Preferred_State No_Null_Position Non_Volatile Bitfield Item(Main ): End Collection, data=none Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x81 EP 1 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0008 1x 8 bytes bInterval 10 Device Status: 0x0000 (Bus Powered)
Now, that's just patently unhelpful. All it says is that there's report types that contain 8 items of values 0 through 255 each. In other words: 8 bytes. Yeah, I could have guessed that.Okay, maybe looking more closely what the Windows software does will help me:
# modprobe usbmon # wireshark -i usbmon1
will let me record (on the VM host) what the software does (in the VM guest) when started. And at least that's mildly enlightening:which is an outgoing HID SET_REPORT with 8 bytes of data, after which the device starts sending reports of 8 bytes of data using INTERRUPT IN:
These 8-byte packets are then what I was seeing in the output of /dev/hidrawX.
Repeating different captures shows: The SET_REPORT generated by the windows software is different each time (though the DF byte at the end is constant). Also note that the device won't send anything before receiving this packet, so in order to gather data for analyzing on my Linux box I have to use the Windows software at least once after plugging the device in.
-
We get signal
04/17/2015 at 01:27 • 0 commentsIndeed, immediately after running the Windows software in a VM with the device connected I was getting a live data log of the CO₂ concentration! This screenshot shows me breathing into the vicinity of the sensor:
Ok, time to disconnect the device from the virtual machine and look at the data on my Linux box. Since it's enumerated as a HID, there's /dev/hidrawX device node that would receive the raw data sent by the device.# hd < /dev/hidraw0 00000000 be 9b 28 69 d0 a5 34 ba cb 9b 90 68 4c a5 34 aa |..(i..4....hL.4.| 00000010 5f 9b 8e 68 de a5 34 c2 f3 9b b0 68 5c a5 34 82 |_..h..4....h\.4.| 00000020 f2 9b b8 69 5c a5 34 8a e2 9b c1 69 55 a5 34 3a |...i\.4....iU.4:| 00000030 52 9b c9 69 75 a5 34 d2 aa 9b 30 69 27 a5 34 42 |R..iu.4...0i'.4B| 00000040 38 9b 20 69 89 a5 34 6a 8e 9b 28 69 d0 a5 34 ca |8. i..4j..(i..4.| 00000050 cb 9b 90 68 4c a5 34 aa 57 9b 8e 68 de a5 34 ba |...hL.4.W..h..4.| 00000060 fb 9b b0 68 5c a5 34 8a fa 9b b8 69 5c a5 34 92 |...h\.4....i\.4.| 00000070 35 9b 81 69 5d a5 34 52 d5 9b a9 69 72 a5 34 72 |5..i].4R...ir.4r| 00000080 aa 9b 30 69 27 a5 34 42 00 9b 20 69 89 a5 34 72 |..0i'.4B.. i..4r| 00000090 8e 9b 28 69 d0 a5 34 ca c3 9b 90 68 4c a5 34 a2 |..(i..4....hL.4.| 000000a0 57 9b 8e 68 de a5 34 ba fb 9b b0 68 5c a5 34 8a |W..h..4....h\.4.| 000000b0 fa 9b b8 69 5c a5 34 92 12 9b c1 69 54 a5 34 2a |...i\.4....iT.4*|
Yeah \o/, there's data! Bummer, it's doesn't look at all like the protocol I was promised. But there does seem to be a structure to it, so there's hope, I guess.
-
War was beginning
04/17/2015 at 01:11 • 0 commentsFirst thing I noticed after unpacking: While the included manual only talks about the USB connection in terms of providing power, the thing actually enumerates on the bus as a HID with vendor/product ID 04d9:a052.
When researching this VID/PID combination, I found that the device appears to be sold in other markets, with different brand markings: http://habrahabr.ru/company/masterkit/blog/248403/. I don't speak any Russian, and Google Translate is rather weak here, but from the pictures it seems that someone was using an identically looking device to monitor some brewing process. And that there's software that reads the data from USB.
Doing a Google image search for co2 meters and selecting a product photo that matches the physical appearance (with yet another different branding) lead me to this American company which had not only the Windows software to connect to the meter for download, but also a PDF documenting the protocol. Well, this should be a breeze …