Permalink
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
1217 lines (1018 sloc) 45.6 KB

Third Space Vest Reverse Engineering

Abstract

Author: Kyle Machulis (qDot) http://www.nonpolynomial.com

The story of how the Third Space Vest was reverse engineered, with the goal of developing a cross-platform python based proof-of-concept driver using the knowledge gotten from multiple sources, including usb line analysis and SDK disassembly. Those who want a more consise overview of the protocol should see the protocol document included with the source code.

The information is presented in a linear fashion, with emphasis on only using tools when they become required, in order to demostrate what can be done with most of the tools before needing others (read: I could’ve done all of this in IDA Pro but I’m trying to be educational here. :) ).

Introduction

There’s a few things I wanted to do when I started using computers in the late 80’s/early 90’s:

  • Write door games

  • Write demos

  • Write cracks

The first one, well, I’ve worked in games/virtual worlds now, but I dunno if conquered what I was looking for in terms of text based games. Demos, I need to move to northern europe first, I think.

However, now I can say I’ve finally cracked something that even required crypto work. It’s small, silly, not very well known hardware, but for some reason the engineers behind it saw fit to encrypt the protocol to talk to it, and I’ve now undone that. Usually when I work with hardware, it’s just a matter of staring at the protocol until I see it, but this actually took disassembly work, as well as other EULA breaking goodness.

The code for this is so going on my fridge with a gold star on it.

Hardware Overview

The Third Space Vest from TN Games is a piece of haptic feedback hardware, made for gamers that want to feel like they’re being shot while playing games. The device comes as a sort of flak jacket, with 4 air bladders in each quadrant of the front and back. There is also a USB and air flow control box that is quite literally sewn into the vest. This is where you plug in the pump and USB communication to the PC.

The pump is self refilling, and the VERY VERY LOUD. Absolutely unusably so for any sort of immersive gaming unless you’re planning on deafening yourself via headphones to mask the sound of refilling. But, we’ll just ignore that for now, even though it makes the hardware testing phase annoying as hell.

Information Gathering

It’s good to put together information about what’s happening with the device during normal usage and through benign means before starting to really look at the protocol. This involves reading the descriptors, any SDK manuals, and looking at the USB line protocol.

USB Descriptor

Here’s the USB descriptor dump we get from the device, by running lsusb -vvv on a linux box with the device connected:

Bus 003 Device 009: ID 1bd7:5000
Device Descriptor:
  bLength                18
  bDescriptorType         1
  bcdUSB               1.10
  bDeviceClass            0 (Defined at Interface level)
  bDeviceSubClass         0
  bDeviceProtocol         0
  bMaxPacketSize0         8
  idVendor           0x1bd7
  idProduct          0x5000
  bcdDevice            0.01
  iManufacturer           1
  iProduct                2
  iSerial                 0
  bNumConfigurations      1
  Configuration Descriptor:
    bLength                 9
    bDescriptorType         2
    wTotalLength           41
    bNumInterfaces          1
    bConfigurationValue     1
    iConfiguration          0
    bmAttributes         0x80
      (Bus Powered)
    MaxPower              500mA
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        0
      bAlternateSetting       0
      bNumEndpoints           2
      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      25
         Report Descriptors:
           ** UNAVAILABLE **
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x01  EP 1 OUT
        bmAttributes            3
          Transfer Type            Interrupt
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0040  1x 64 bytes
        bInterval              10
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x82  EP 2 IN
        bmAttributes            3
          Transfer Type            Interrupt
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0040  1x 64 bytes
        bInterval              10

From this, we know the VID/PID of the device, and that it’s using the USB HID protocol, though it’s assumed that this is probably some bastardization of format. Making hardware that talks HID on windows means the user does not have to install drivers. Many manufacturers exploit this fact, sending non-standard messages over HID in order to have plug-and-play like capabilities on windows. It also means that you have to blacklist on linux and install a shunt kext on OS X. Good times.

Other than that, we can see that the endpoints can deal with 64 byte interrupt packets. But, since we haven’t started looking at the line yet, we don’t know exactly what size we’ll be expecting. That’s about all we can pull from this right now.

From the SDK

From downloading and reading the SDK, we get a few pieces of information:

  • There’s "SetEffect" and "SetEffect2" functions. SetEffect takes in prebuilt effects that come with the SDK, SetEffect2 just takes the index of an aircell in the vest, and the speed at which to inflate it. This means that SetEffect2 will probably be our main attack strategy. It all encompasses all the vest does: setting an air cell to a value.

  • The vest has "setup" and "teardown" functions, meaning there may be some sort of initialization.

  • There’s a "FlushBuffer" function, meaning either the hardware could queue commands, or it could be done in the software.

This gives us a few entry points to look at when we delve farther into SDK implemenation later.

From the USB Line

This is where things get interesting.

After compiling the example that comes with the SDK, we can set USB analyzer software on top of the OS the vest is connected to, and watch packets as they come through the filter driver. This can be done using any analyzer:

  • SniffUSB

  • USBlyzer

  • VMWare’s USB Monitor (if this is being done via a VMWare VM)

Basically, whatever will allow us to see what’s going back and forth over the line.

Here’s the source code I used to generate the packets:

#include "tngaming.h"

/* --- link library */
#pragma comment(lib,"tngaming.lib")

#define THREE_SECONDS (3000)

int _tmain(int argc, _TCHAR* argv[])
{
	/* Initailized gaming library */
	printf("Setting Up\n");
	if (SetUpJacket()  != GLIB_OK)
	{
		printf("Error: %s", GetErrorText());
	}

	printf("Custom Effect - length = 10, Cell = 1\n");
	if (SetEffect2(10,1) != GLIB_OK)
	{
			printf("Error: %s", GetErrorText());
	}

	::Sleep(3000);
	printf("Cleaning Up\n");
	TearDownJacket();

	return 0;
}

From simple packet analysis, we see:

  • Both input and output are 10 bytes.

  • Output is sent as one 10 byte packet. Input is polled constantly by the operating system since this is an HID device.

  • On initial connection to the machine, the device will send all 0 values when polled by the OS, until a command is sent. Then it will always return the status of the last command sent.

Output packets look like (generated via SetEffect2(10, 1), to fire air cell 1 with 'speed' 10):

02 1D 52 29 1E 9E DC 05 D5 E4

Input packets look like (return status from above command):

02 38 30 31 32 33 34 5A 04 02

Not a whole hell of a lot that we can obviously extract there. It’s weird that we need 10 bytes to talk to the device though. Every packet starts with 0x2.

Changing up the example to run SetEffect2(10,1) in a loop, we get the following packets sent to the vest:

...
02 D8 B6 FF F9 87 F1 17 37 64
02 9F 3A D0 98 00 C4 49 CB 96
02 1F ED 8E 1D E3 90 0C 93 BA
02 FF 8A 52 2F E0 EF B5 92 8A
02 BA F5 02 AC 90 E3 80 45 E6
02 90 F5 40 78 56 64 EB A5 5A
02 BB F8 FF 8E B4 A2 69 84 45
02 D4 03 DF 0E 0C 30 4B D6 28
...

The last 9 bytes change in a non-uniform way on every input, even though we’re sending the same command over and over. That means the packet protocol is somehow obfuscated or encrypted. Most likely encrypted.

Damnit.

Wait, why is it obfuscated?

That’s a really good question. It’s for games, after all. It’s not like it’s accessing your bank account.

TN Games hands their SDK out in a "free as in beer" format, meaning you can download the SDK for free for windows (it’s a single header and a static library). However, that’s only for non-commercial development. For actually using it within a game, you have to contact TN Games and work out licensing fees, since they see their vest as a value-add to your game, so you get to pay them for the right to use it.

So, yeah, you pay $99-139 for their vest, then they also charge companies to develop games for the vest. That way, they can make their money two ways, even though those two ways depend on each other. For game manufacturers to want to buy the vest, gamers need to have it and demand support. For gamers to want it, game manufacturers need to put it in their games. This is a very common model for non-necessary game hardware, be it haptic, biometric, whatever.

Via the licensing agreement, TN Games is legally guarenteed to get their cash, since otherwise they can sue makers of commercial games if a game producer doesn’t pay up and uses their SDK. That last part is important.

In the USA, reverse engineering a protocol falls under fair use, as long as you don’t have to disassemble the code to access the device. Since the consumer bought the device, and owns the line between the device and the computer, they can watch/log whatever comes over that line. This means that, without an obfuscated/encrypted protocol, any game producer could simply watch what’s going over the USB line, and build their own code on top of that information, bypassing TN Games SDK completely. That would be a completely legal thing to do.

That’s why this protocol is at least obfuscated. However, there’s a difference between obfuscated and encrypted. One could call most protocols obfuscated, because protocols are made for computers to talk to other computers, or other parts of themselves, in crazy math language that most humans don’t understand. Obfuscating a protocol means some particularly clever person could still reverse engineer the communication to the device using nothing but the logs of the line between the device and the controlling computer. Encryption, on the other hand, ensures that the reverse engineer is required to go digging through the source to look up algorithms and keys. Having encryption is basically the owner’s due diligence they can put on top of their license to make sure it’s enforced.

Disassembly, once again, at least in the US, is a severe violation of the EULA and is sometimes also considered a prosecutable crime.

With this in mind, let’s continue.

External Attacks

Since it’s obfuscated, all we can do is replay attacks for the moment. This means we can take a packet that we traced off the line from earlier, like:

02 1D 52 29 1E 9E DC 05 D5 E4

And start sending it to the device to see what happens.

For this device, replay attacks work. I can send that packet multiple times, it is always accepted, and will activate cell index 1. This means the encryption is not based on any counters or packet ordering, it is simply per packet. This also means that, since SetupDevice sent no data to the device, any keys to the encryption in the packet must be contained in the packet itself, in one of the 9 bytes after the 0x02 byte.

This is a massive failure on the implementor’s side, since it allows circumnavigation of the cryptography. Since there’s a maximum of 8 cells, all individually addressable, each with 255 "length" levels (the SDK manual says there’s only 10 levels, but we’ll just assume a whole byte here), we could simply build a table of 2040 commands by running every combination of SetEffect2, and create a packet lookup table referencable by that, i.e.

| Cell Index | Speed | Packet                        |
|------------+-------+-------------------------------|
|          1 |     9 | ...                           |
|          1 |    10 | 02 1D 52 29 1E 9E DC 05 D5 E4 |
|          2 |     1 | ...                           |

All we’d need to do is create a function like SetEffect2 to reference into that table using the index/speed parameters, and we could release a fully functioning library for the vest.

But how much fun would that be? Let’s really break stuff.

Reverse Engineering

We’ve pulled all we can from the outside of the library. Now it’s time to go inside. Reverse engineering the library consists of building small programs to access different functions of the SDK library, then stepping through with a debugger or disassembler to figure out exactly what they’re doing in the assembly, since we don’t have source code. For this project, I used

Mostly OllyDbg because the code isn’t very complicated.

Information from the SDK - Internal

Before we start the debugger, we can get a lot of information from tools that come with the compiler. Here’s the function definitions we get from the third space vest SDK header:

extern int __stdcall SetUpJacket();
extern void __stdcall TearDownJacket();
extern int __stdcall SetEffect(int nEffect);
extern int __stdcall SetEffect2(int speed,int actuator);
extern void __stdcall FlushBuffer(int actuator);
extern int __stdcall GetErrorCode();
extern char* __stdcall GetErrorText();

This is a far reduced set of functions from what the library actually has to do to take our effect requests and send them to the hardware. To find out everything the library exposes, we can run the DUMPBIN utility like so:

dumpbin /SYMBOLS tngaming.lib /OUT:symbols.txt

This dumps all of the symbols in the library to a text file.

We know that since this is an HID device, and it’s on windows, it’ll probably use setupapi and the HID libraries from the WDK. So, we can remove all of those and see what else the library has. From cleaning up symbols.txt, we find:

SECTD   | ?FlushBuffers@@YAXXZ (void __cdecl FlushBuffers(void))
SECTF   | _SetEffect2@8
SECT11  | _FlushBuffer@4
SECT13  | _GetErrorCode@0
SECT15  | _GetErrorText@0
SECT17  | ?LoadEffectsBuffer@@YAHH@Z (int __cdecl LoadEffectsBuffer(int))
UNDEF   | _rand
UNDEF   | _srand
SECT19  | ?encrypt@@YAXPAK0@Z (void __cdecl encrypt(unsigned long *,unsigned long *))
SECT1B  | ?decrypt@@YAXPAK0@Z (void __cdecl decrypt(unsigned long *,unsigned long *))
SECT1D  | ?CRC8@@YAEEE@Z (unsigned char __cdecl CRC8(unsigned char,unsigned char))
SECT1F  | ?BitSet@@YAXPAEHH@Z (void __cdecl BitSet(unsigned char *,int,int))
SECT21  | _TearDownJacket@0
SECT23  | _SetEffect@4
SECT25  | ?SendEffect@@YAHHH@Z (int __cdecl SendEffect(int,int))
UNDEF   | ?WriteReport@@YAHPAE@Z (int __cdecl WriteReport(unsigned char *))
UNDEF   | ___security_cookie
UNDEF   | @__security_check_cookie@4
SECT27  | ?eventhandler@@YGIPAX@Z (unsigned int __stdcall eventhandler(void *))
UNDEF   | __imp__Sleep@4
SECT29  | _SetUpJacket@0
UNDEF   | __beginthreadex
UNDEF   | ?Connect@@YAHXZ (int __cdecl Connect(void))

Ok, tons of interesting stuff in here that’s not exposed via the header. First off, anything UNDEF means it’s a symbol we’ll get from another library. In this case, it’s mostly simple libc stuff, and well as windows thread calls, which means the library probably deals with USB communication in a background thread, as is normal in SDKs like this.

The encryption and decryption functions are there, and both take arrays of uint32_t’s (implied, since this is a 32-bit library and windows has crazy ideas for 'long'), so we can’t tell much from that so far.

SendEffect also looks interesting, since that’s probably what communicates directly with the vest.

A note here: security_cookie is a special symbol the Visual Studio compiler puts in for checking buffer overruns in functions. So, while it’s certainly a security measure, it doesn’t have anything to do with the encryption.

Setting up test code

Now that we know what the symbols in the library look like, we can create extern prototypes to access them from our earlier SDK example. Our example now looks like this:

#include "tngaming.h"

/* --- link library */
#pragma comment(lib,"tngaming.lib")

#define THREE_SECONDS (3000)


void _cdecl encrypt(unsigned long* a, unsigned long* b);
void _cdecl decrypt(unsigned long* a, unsigned long* b);
void _cdecl SendEffect(int a, int b);
unsigned char _cdecl CRC8(unsigned char, unsigned char);

int _tmain(int argc, _TCHAR* argv[])
{
	unsigned long a[10];
	unsigned long b[10];
	printf("ENCRYPTING\n");
	encrypt(a, b);
	printf("DECRYPTING\n");
	decrypt(a, b);

	/* Initailized gaming library */
	printf("Setting Up\n");
	if (SetUpJacket()  != GLIB_OK)
	{
		printf("Error: %s", GetErrorText());
	}

	printf("Custom Effect - length = 10, Cell = 1\n");
	if (SendEffect(10,1) != GLIB_OK)
	{
			printf("Error: %s", GetErrorText());
	}

	::Sleep(3000);
	printf("Cleaning Up\n");
	TearDownJacket();

	return 0;
}

There are a couple of things that are uncertain with this code, simply due to our current lack of knowledge:

  • We don’t know the length of the arrays encrypt/decrypt want, since we don’t know what algorithm they’re using yet.

  • We don’t know if SendEffect takes the same options at SetEffect2, but since they’re both ints, it’s worth a try.

However, calling these functions this way gives us easy places to put breakpoints later.

The printfs are inserted as markers to tell where we are. Most disassemblers will show you string references next to the code, so it makes it easier to see what’s where.

Initial testing shows that calling SendEffect directly does mostly what we expect, firing a different cell than we get with the example, but still firing at the same speed.

Encryption Reversing

The first thing we want to do is figure out the encryption, because without it, we can figure out the protocol, but can’t really send it over to be tested.

Finding the Algorithm

Let’s look at the function disassembly for encrypt(ulong*, ulong*).

Address   Hex dump          Command                                  Comments
00411DC0  /> \83EC 0C       SUB ESP,0C
00411DC3  |.  8B4C24 10     MOV ECX,DWORD PTR SS:[ARG.1]
00411DC7  |.  8B01          MOV EAX,DWORD PTR DS:[ECX]
00411DC9  |.  8B49 04       MOV ECX,DWORD PTR DS:[ECX+4]
00411DCC  |.  53            PUSH EBX
00411DCD  |.  56            PUSH ESI
00411DCE  |.  8B7424 1C     MOV ESI,DWORD PTR SS:[ARG.2]
00411DD2  |.  57            PUSH EDI
00411DD3  |.  8B3E          MOV EDI,DWORD PTR DS:[ESI]
00411DD5  |.  897C24 0C     MOV DWORD PTR SS:[LOCAL.2],EDI
00411DD9  |.  8B7E 04       MOV EDI,DWORD PTR DS:[ESI+4]
00411DDC  |.  897C24 20     MOV DWORD PTR SS:[ARG.2],EDI
00411DE0  |.  8B7E 08       MOV EDI,DWORD PTR DS:[ESI+8]
00411DE3  |.  8B76 0C       MOV ESI,DWORD PTR DS:[ESI+0C]
00411DE6  |.  33D2          XOR EDX,EDX
00411DE8  |.  897C24 14     MOV DWORD PTR SS:[LOCAL.0],EDI
00411DEC  |.  897424 10     MOV DWORD PTR SS:[LOCAL.1],ESI
00411DF0  |.  8D7A 20       LEA EDI,[EDX+20]
00411DF3  |>  8BF1          /MOV ESI,ECX
00411DF5  |.  C1EE 05       |SHR ESI,5
00411DF8  |.  037424 20     |ADD ESI,DWORD PTR SS:[ARG.2]
00411DFC  |.  8BD9          |MOV EBX,ECX
00411DFE  |.  C1E3 04       |SHL EBX,4
00411E01  |.  035C24 0C     |ADD EBX,DWORD PTR SS:[LOCAL.2]
00411E05  |.  81EA 4786C861 |SUB EDX,61C88647
00411E0B  |.  33F3          |XOR ESI,EBX
00411E0D  |.  8D1C0A        |LEA EBX,[ECX+EDX]
00411E10  |.  33F3          |XOR ESI,EBX
00411E12  |.  03C6          |ADD EAX,ESI
00411E14  |.  8BF0          |MOV ESI,EAX
00411E16  |.  C1EE 05       |SHR ESI,5
00411E19  |.  037424 10     |ADD ESI,DWORD PTR SS:[LOCAL.1]
00411E1D  |.  8BD8          |MOV EBX,EAX
00411E1F  |.  C1E3 04       |SHL EBX,4
00411E22  |.  035C24 14     |ADD EBX,DWORD PTR SS:[LOCAL.0]
00411E26  |.  33F3          |XOR ESI,EBX
00411E28  |.  8D1C02        |LEA EBX,[EAX+EDX]
00411E2B  |.  33F3          |XOR ESI,EBX
00411E2D  |.  03CE          |ADD ECX,ESI
00411E2F  |.  83EF 01       |SUB EDI,1
00411E32  |.^ 75 BF         \JNE SHORT 00411DF3
00411E34  |.  8B5424 1C     MOV EDX,DWORD PTR SS:[ARG.1]
00411E38  |.  5F            POP EDI
00411E39  |.  5E            POP ESI
00411E3A  |.  8902          MOV DWORD PTR DS:[EDX],EAX
00411E3C  |.  894A 04       MOV DWORD PTR DS:[EDX+4],ECX
00411E3F  |.  5B            POP EBX
00411E40  |.  83C4 0C       ADD ESP,0C
00411E43  \.  C3            RETN

Yup. That sure is a lot of assembly. Breaking it down:

00411DC3  |.  8B4C24 10     MOV ECX,DWORD PTR SS:[ARG.1]
00411DC7  |.  8B01          MOV EAX,DWORD PTR DS:[ECX]
00411DC9  |.  8B49 04       MOV ECX,DWORD PTR DS:[ECX+4]
00411DCC  |.  53            PUSH EBX
00411DCD  |.  56            PUSH ESI
00411DCE  |.  8B7424 1C     MOV ESI,DWORD PTR SS:[ARG.2]
00411DD2  |.  57            PUSH EDI
00411DD3  |.  8B3E          MOV EDI,DWORD PTR DS:[ESI]
00411DD5  |.  897C24 0C     MOV DWORD PTR SS:[LOCAL.2],EDI
00411DD9  |.  8B7E 04       MOV EDI,DWORD PTR DS:[ESI+4]
00411DDC  |.  897C24 20     MOV DWORD PTR SS:[ARG.2],EDI
00411DE0  |.  8B7E 08       MOV EDI,DWORD PTR DS:[ESI+8]
00411DE3  |.  8B76 0C       MOV ESI,DWORD PTR DS:[ESI+0C]

This is the portion of the code that’s unpacking the arguments. It looks like it takes 2 4 byte values from argument 1, and 4 4 byte values from argument 2. So, it’s more like:

encrypt(unsigned long a[2], unsigned long b[4])

For those of you that realize what algorithm this is already, please stay quiet so the rest of the class can be surprised.

Now, the rest of the function looks like a ton of memory movement and shifts, as one would expect from something as math heavy as encryption. However, there’s one line that stands out:

00411E05  |.  81EA 4786C861 |SUB EDX,61C88647

That’s a large, weird constant. Some might even say it’s magic. Let’s plug it into google and see what happens.

No, really just go to

It’s amazing how often that works for magic numbers. Google is the reverse engineer’s best friend. You can even use it to search for similar disassembly if you know how to phrase your search correctly.

From the search returns, we see a lot of references to the TEA algorithms. A quick trip over to wikipedia:

Confirms that the algorithm takes a 64-byte block, and a 128-byte cache key, which lines up with what we figured out earlier when looking at the arguments.

It even nets us source code:

void encrypt (uint32_t* v, uint32_t* k) {
    uint32_t v0=v[0], v1=v[1], sum=0, i;           /* set up */
    uint32_t delta=0x9e3779b9;                     /* a key schedule constant */
    uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3];   /* cache key */
    for (i=0; i < 32; i++) {                       /* basic cycle start */
        sum += delta;
        v0 += ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
        v1 += ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
    }                                              /* end cycle */
    v[0]=v0; v[1]=v1;

All of those shifts match up with what we see in the disassembly, though the constant is different. However, we see in the wikipedia article that the constant just needs to the golden ratio +/- k, k being an integer. Dividing 2^32 by 0x61C88647 gives us (t + 1), t being the golden ratio. So, we’re fine. We could even switch it out with 0x9e3779b9, and there’s be no difference in output.

So, we know the algorithm! But the question is, what arguments is it taking?

Argument Analysis

From wikipedia, we know are arguments are:

  • A 64-bit block

  • A 128-bit cache key

Putting a breakpoint on the encrypt function in the jump table, we run the program from the "Setting Up the Test Code" function earlier, commenting out the direct encrypt/decrypt calls to see what happens when SendEffect is run (which, logically, at some point has to call encrypt to turn the command into something that will look like what we saw on the USB line). Sure enough, the breakpoint is hit on encrypt, with SendEffect lower on the stack. Checking the arguments, the first thing to notice is that the first argument, the 64-bit block, never changes. It is always:

0x0 0x0 0x0 0x0 0x0A 0x01 0x23 0x00

However, the second argument, the cache key, is different every time the function is called. We infer that this is generated by SendEffect at some point. However, knowing the algorithm used and what the command packet looks like unencrypted, we can move on to figuring out where the cache key comes from, and how the command is built.

Hand Decompiling SendEffect()

Now that we know what’s going on over the line, it’s time to figure out how we can build commands to send to the hardware, and how to generate our own cache keys. Since SendEffect called in a look calls encrypt multiple times, we can assume that command block and cache key construction happen there. Looking at the disassembly should lead us to the information we require to finish our driver.

While the assembly here is unobfuscated and could be taken apart fairly quickly by a decompiler, we’ll step through the function by hand to make things clearer.

Disassembly

To start, here’s the full disassembly of SendEffect from OllyDbg

CPU Disasm
Address   Hex dump          Command                                  Comments
00412050  /> \83EC 2C       SUB ESP,2C
00412053  |.  A1 DC944100   MOV EAX,DWORD PTR DS:[__security_cookie]
00412058  |.  33C4          XOR EAX,ESP
0041205A  |.  894424 28     MOV DWORD PTR SS:[LOCAL.0],EAX
0041205E  |.  53            PUSH EBX
0041205F  |.  55            PUSH EBP
00412060  |.  33C0          XOR EAX,EAX
00412062  |.  56            PUSH ESI
00412063  |.  33DB          XOR EBX,EBX
00412065  |.  57            PUSH EDI
00412066  |.  53            PUSH EBX
00412067  |.  885C24 2C     MOV BYTE PTR SS:[LOCAL.4],BL
0041206B  |.  894424 2D     MOV DWORD PTR SS:[ESP+2D],EAX
0041206F  |.  894424 31     MOV DWORD PTR SS:[ESP+31],EAX
00412073  |.  894424 35     MOV DWORD PTR SS:[ESP+35],EAX
00412077  |.  66:894424 39  MOV WORD PTR SS:[ESP+39],AX
0041207C  |.  894424 20     MOV DWORD PTR SS:[LOCAL.7],EAX
00412080  |.  894424 24     MOV DWORD PTR SS:[LOCAL.6],EAX
00412084  |.  894424 28     MOV DWORD PTR SS:[LOCAL.5],EAX
00412088  |.  895C24 18     MOV DWORD PTR SS:[LOCAL.9],EBX
0041208C  |.  E8 63F1FFFF   CALL 004111F4                            ; Jump to MSVCR100D._time64
00412091  |.  50            PUSH EAX
00412092  |.  E8 A0EFFFFF   CALL 00411037                            ; Jump to MSVCR100D.srand
00412097  |.  E8 E5F0FFFF   CALL 00411181                            ; Jump to MSVCR100D.rand
0041209C  |.  0FB67424 4C   MOVZX ESI,BYTE PTR SS:[ESP+4C]
004120A1  |.  99            CDQ
004120A2  |.  B9 FF000000   MOV ECX,0FF
004120A7  |.  F7F9          IDIV ECX
004120A9  |.  8BC6          MOV EAX,ESI
004120AB  |.  C1E8 04       SHR EAX,4
004120AE  |.  8A80 646A4100 MOV AL,BYTE PTR DS:[EAX+416A64]
004120B4  |.  0FB6C8        MOVZX ECX,AL
004120B7  |.  C1E9 04       SHR ECX,4
004120BA  |.  33CE          XOR ECX,ESI
004120BC  |.  83E1 0F       AND ECX,0000000F
004120BF  |.  C0E0 04       SHL AL,4
004120C2  |.  3281 646A4100 XOR AL,BYTE PTR DS:[ECX+416A64]
004120C8  |.  8B4C24 48     MOV ECX,DWORD PTR SS:[ESP+48]
004120CC  |.  0FB6F9        MOVZX EDI,CL
004120CF  |.  885C24 30     MOV BYTE PTR SS:[ESP+30],BL
004120D3  |.  0FB6D8        MOVZX EBX,AL
004120D6  |.  C0E0 04       SHL AL,4
004120D9  |.  8BEF          MOV EBP,EDI
004120DB  |.  C1EB 04       SHR EBX,4
004120DE  |.  C1ED 04       SHR EBP,4
004120E1  |.  33DD          XOR EBX,EBP
004120E3  |.  3283 646A4100 XOR AL,BYTE PTR DS:[EBX+416A64]
004120E9  |.  C1E1 08       SHL ECX,8
004120EC  |.  0FB6D8        MOVZX EBX,AL
004120EF  |.  03CE          ADD ECX,ESI
004120F1  |.  C1E1 08       SHL ECX,8
004120F4  |.  C0E0 04       SHL AL,4
004120F7  |.  C1EB 04       SHR EBX,4
004120FA  |.  33DF          XOR EBX,EDI
004120FC  |.  0FB6C0        MOVZX EAX,AL
004120FF  |.  83E3 0F       AND EBX,0000000F
00412102  |.  0FB6BB 646A41 MOVZX EDI,BYTE PTR DS:[EBX+416A64]
00412109  |.  33F8          XOR EDI,EAX
0041210B  |.  0FB682 FA6841 MOVZX EAX,BYTE PTR DS:[EDX+4168FA]
00412112  |.  03F9          ADD EDI,ECX
00412114  |.  0FB68A FB6841 MOVZX ECX,BYTE PTR DS:[EDX+4168FB]
0041211B  |.  C1E1 08       SHL ECX,8
0041211E  |.  03C8          ADD ECX,EAX
00412120  |.  0FB682 F96841 MOVZX EAX,BYTE PTR DS:[EDX+4168F9]
00412127  |.  C1E1 08       SHL ECX,8
0041212A  |.  03C8          ADD ECX,EAX
0041212C  |.  0FB682 F86841 MOVZX EAX,BYTE PTR DS:[EDX+4168F8]
00412133  |.  C1E1 08       SHL ECX,8
00412136  |.  03C8          ADD ECX,EAX
00412138  |.  0FB682 FE6841 MOVZX EAX,BYTE PTR DS:[EDX+4168FE]
0041213F  |.  894C24 20     MOV DWORD PTR SS:[ESP+20],ECX
00412143  |.  0FB68A FF6841 MOVZX ECX,BYTE PTR DS:[EDX+4168FF]
0041214A  |.  C1E1 08       SHL ECX,8
0041214D  |.  03C8          ADD ECX,EAX
0041214F  |.  0FB682 FD6841 MOVZX EAX,BYTE PTR DS:[EDX+4168FD]
00412156  |.  C1E1 08       SHL ECX,8
00412159  |.  03C8          ADD ECX,EAX
0041215B  |.  0FB682 FC6841 MOVZX EAX,BYTE PTR DS:[EDX+4168FC]
00412162  |.  C1E7 08       SHL EDI,8
00412165  |.  C1E1 08       SHL ECX,8
00412168  |.  C64424 31 02  MOV BYTE PTR SS:[ESP+31],2
0041216D  |.  885424 32     MOV BYTE PTR SS:[ESP+32],DL
00412171  |.  C74424 18 000 MOV DWORD PTR SS:[ESP+18],0
00412179  |.  897C24 1C     MOV DWORD PTR SS:[ESP+1C],EDI
0041217D  |.  03C8          ADD ECX,EAX
0041217F  |.  0FB682 026941 MOVZX EAX,BYTE PTR DS:[EDX+416902]
00412186  |.  894C24 24     MOV DWORD PTR SS:[ESP+24],ECX
0041218A  |.  0FB68A 036941 MOVZX ECX,BYTE PTR DS:[EDX+416903]
00412191  |.  C1E1 08       SHL ECX,8
00412194  |.  03C8          ADD ECX,EAX
00412196  |.  0FB682 016941 MOVZX EAX,BYTE PTR DS:[EDX+416901]
0041219D  |.  C1E1 08       SHL ECX,8
004121A0  |.  03C8          ADD ECX,EAX
004121A2  |.  0FB682 006941 MOVZX EAX,BYTE PTR DS:[EDX+416900]
004121A9  |.  C1E1 08       SHL ECX,8
004121AC  |.  03C8          ADD ECX,EAX
004121AE  |.  0FB682 066941 MOVZX EAX,BYTE PTR DS:[EDX+416906]
004121B5  |.  894C24 28     MOV DWORD PTR SS:[ESP+28],ECX
004121B9  |.  0FB68A 076941 MOVZX ECX,BYTE PTR DS:[EDX+416907]
004121C0  |.  C1E1 08       SHL ECX,8
004121C3  |.  03C8          ADD ECX,EAX
004121C5  |.  0FB682 056941 MOVZX EAX,BYTE PTR DS:[EDX+416905]
004121CC  |.  0FB692 046941 MOVZX EDX,BYTE PTR DS:[EDX+416904]
004121D3  |.  C1E1 08       SHL ECX,8
004121D6  |.  03C8          ADD ECX,EAX
004121D8  |.  C1E1 08       SHL ECX,8
004121DB  |.  03CA          ADD ECX,EDX
004121DD  |.  8D4424 20     LEA EAX,[ESP+20]
004121E1  |.  894C24 2C     MOV DWORD PTR SS:[ESP+2C],ECX
004121E5  |.  50            PUSH EAX
004121E6  |.  8D4C24 1C     LEA ECX,[ESP+1C]
004121EA  |.  51            PUSH ECX
004121EB  |.  E8 4FF0FFFF   CALL 0041123F
004121F0  |.  8B4424 20     MOV EAX,DWORD PTR SS:[ESP+20]
004121F4  |.  8BD0          MOV EDX,EAX
004121F6  |.  C1EA 18       SHR EDX,18
004121F9  |.  885424 3E     MOV BYTE PTR SS:[ESP+3E],DL
004121FD  |.  8BC8          MOV ECX,EAX
004121FF  |.  C1E9 10       SHR ECX,10
00412202  |.  8BD0          MOV EDX,EAX
00412204  |.  C1EA 08       SHR EDX,8
00412207  |.  884C24 3D     MOV BYTE PTR SS:[ESP+3D],CL
0041220B  |.  884424 3B     MOV BYTE PTR SS:[ESP+3B],AL
0041220F  |.  8B4424 24     MOV EAX,DWORD PTR SS:[ESP+24]
00412213  |.  885424 3C     MOV BYTE PTR SS:[ESP+3C],DL
00412217  |.  8BC8          MOV ECX,EAX
00412219  |.  C1E9 18       SHR ECX,18
0041221C  |.  8BD0          MOV EDX,EAX
0041221E  |.  C1EA 10       SHR EDX,10
00412221  |.  884C24 42     MOV BYTE PTR SS:[ESP+42],CL
00412225  |.  885424 41     MOV BYTE PTR SS:[ESP+41],DL
00412229  |.  8BC8          MOV ECX,EAX
0041222B  |.  8D5424 38     LEA EDX,[ESP+38]
0041222F  |.  C1E9 08       SHR ECX,8
00412232  |.  52            PUSH EDX
00412233  |.  884C24 44     MOV BYTE PTR SS:[ESP+44],CL
00412237  |.  884424 43     MOV BYTE PTR SS:[ESP+43],AL
0041223B  |.  E8 C3EFFFFF   CALL 00411203
00412240  |.  83C4 14       ADD ESP,14
00412243  |.  85C0          TEST EAX,EAX
00412245  |.  5F            POP EDI
00412246  |.  5E            POP ESI
00412247  |.  5D            POP EBP
00412248  |.  5B            POP EBX
00412249  |.  74 1B         JE SHORT 00412266
0041224B  |.  C705 BC934100 MOV DWORD PTR DS:[?nCurrentError@@3HA],0
00412255  |.  33C0          XOR EAX,EAX
00412257  |.  8B4C24 28     MOV ECX,DWORD PTR SS:[ESP+28]
0041225B  |.  33CC          XOR ECX,ESP
0041225D  |.  E8 CBEDFFFF   CALL 0041102D                            ; [__security_check_cookie
00412262  |.  83C4 2C       ADD ESP,2C
00412265  |.  C3            RETN
00412266  |>  8B4C24 28     MOV ECX,DWORD PTR SS:[ESP+28]
0041226A  |.  B8 03000000   MOV EAX,3
0041226F  |.  33CC          XOR ECX,ESP
00412271  |.  C605 78974100 MOV BYTE PTR DS:[?bLibRunning@@3_NA],0
00412278  |.  A3 BC934100   MOV DWORD PTR DS:[?nCurrentError@@3HA],E
0041227D  |.  E8 ABEDFFFF   CALL 0041102D                            ; [__security_check_cookie
00412282  |.  83C4 2C       ADD ESP,2C
00412285  \.  C3            RETN

Who’s calling what, where

The system functions are commented by ollydbg, but I didn’t have it analyze the block before I pasted, so our local symbols aren’t seen.

  • CALL 0041123F : Calling encrypt - Means our packet formation has to happen before this.

  • CALL 0041102D : Calling WriteReport - sending the data to the USB device

Command Packet Analysis and Checksum Building

Here’s the block our call to SendEffect(10, 1) is forming and sending to the encrypt function, which we saw earlier:

0x0 0x0 0x0 0x0 0x0A 0x01 0x23 0x00

We can easily see where our speed and index are set in there. Byte 4 is speed, byte 5 is cell index. What’s byte 6, though? It looks like some sort of checksum.

Let’s take a look at the relevant assembly from the function, with comments:

...
004120A9  |.  8BC6          MOV EAX,ESI
004120AB  |.  C1E8 04       SHR EAX,4
004120AE  |.  8A80 646A4100 MOV AL,BYTE PTR DS:[EAX+416A64]
004120B4  |.  0FB6C8        MOVZX ECX,AL
004120B7  |.  C1E9 04       SHR ECX,4
004120BA  |.  33CE          XOR ECX,ESI
004120BC  |.  83E1 0F       AND ECX,0000000F
004120BF  |.  C0E0 04       SHL AL,4
004120C2  |.  3281 646A4100 XOR AL,BYTE PTR DS:[ECX+416A64]
004120C8  |.  8B4C24 48     MOV ECX,DWORD PTR SS:[ESP+48]
004120CC  |.  0FB6F9        MOVZX EDI,CL
004120CF  |.  885C24 30     MOV BYTE PTR SS:[ESP+30],BL
004120D3  |.  0FB6D8        MOVZX EBX,AL
004120D6  |.  C0E0 04       SHL AL,4
004120D9  |.  8BEF          MOV EBP,EDI
004120DB  |.  C1EB 04       SHR EBX,4
004120DE  |.  C1ED 04       SHR EBP,4
004120E1  |.  33DD          XOR EBX,EBP
004120E3  |.  3283 646A4100 XOR AL,BYTE PTR DS:[EBX+416A64]
004120E9  |.  C1E1 08       SHL ECX,8
004120EC  |.  0FB6D8        MOVZX EBX,AL
004120EF  |.  03CE          ADD ECX,ESI
004120F1  |.  C1E1 08       SHL ECX,8
004120F4  |.  C0E0 04       SHL AL,4
004120F7  |.  C1EB 04       SHR EBX,4
004120FA  |.  33DF          XOR EBX,EDI
004120FC  |.  0FB6C0        MOVZX EAX,AL
004120FF  |.  83E3 0F       AND EBX,0000000F
00412102  |.  0FB6BB 646A41 MOVZX EDI,BYTE PTR DS:[EBX+416A64]
00412109  |.  33F8          XOR EDI,EAX
...

So, first off, we see lots of references to 0x416A64, which refers to data in the heap. If we go there, we see:

CPU Dump
Address   Hex dump
00416A64  00 07 0E 09|1C 1B 12 15|38 3F 36 31|24 23 2A 2D|

Huh. Those are the first 16 values of a CRC8 table! However, there’s a CRC8 function in the library; we saw that when we did the symbol dump earlier. Also, full CRC8 tables have 255 values in their LUT, this only has 16. So, if we look at the assembly:

  • It’s loading the index (already loaded this into AL before the block), then the speed.

  • We see a lot of moving into low bytes of registers and then shifting right 4. This means we’re just operating on nybbles most of the time.

  • It’s not actually calling CRC8 anywhere.

So, this is its own checksum function. Therefore, it’s down to figuring out the specific register moves and reversing that into something usable.

Via a bunch of magic (where magic equals keeping notes and figuring out who’s holding what, and when), we get the following checksum function in python:

crc8_table = [0x00, 0x07, 0x0E, 0x09,
              0x1C, 0x1B, 0x12, 0x15,
              0x38, 0x3F, 0x36, 0x31,
              0x24, 0x23, 0x2A, 0x2D]

def form_checksum(self, index, speed):
    cell_index = index & 0x0f # This should usually = index
    a = (crc8_table[cell_index] << 4) & 0xFF
    b = (crc8_table[cell_index] >> 4) ^ (speed >> 4)
    c = a ^ crc8_table[b]
    d = (c << 4) & 0xFF
    final_crc = crc8_table[((c >> 4) ^ speed) & 0x0F] ^ d
    return final_crc

All of the & 0xFF’s are just to make sure we’re truncating to 8-bit values properly. But, this is what byte 6 of the packet is. So now we know how to form the 8 bytes the encryption function wants. We can make

0x0 0x0 0x0 0x0 0x0A 0x01 0x23 0x00

in python by just making a list, and filling it in with our index, speed, and the checksum as formed by above. However, we have to encrypt that before it goes to the device. So how do we get our cache key to run the encryption function?

Cache Key Construction

First off, the cache key does not need to be rebuilt like it is in the library. In fact, you can just build one cache key once, ever, and use it forever and ever more. See the Useless Key Construction portion of the addendum.

However we do the cache key construction analysis here for sake of completeness.

Let’s go back to the disassembly:

0041208C  |.  E8 63F1FFFF   CALL 004111F4                            ; Jump to MSVCR100D._time64
00412091  |.  50            PUSH EAX
00412092  |.  E8 A0EFFFFF   CALL 00411037                            ; Jump to MSVCR100D.srand
00412097  |.  E8 E5F0FFFF   CALL 00411181                            ; Jump to MSVCR100D.rand
0041209C  |.  0FB67424 4C   MOVZX ESI,BYTE PTR SS:[ESP+4C]
004120A1  |.  99            CDQ
004120A2  |.  B9 FF000000   MOV ECX,0FF
004120A7  |.  F7F9          IDIV ECX
004120A9  |.  8BC6          MOV EAX,ESI

Now, from the system calls, we can tell it’s a normal random seeding and generation function, though the seed is being recalulated on every call to SendEffect, which is a bad idea. But, that’s not really important. We get the random 32-bit value, then those last two lines are the value being taken modulo 255. IDIV takes the value in EAX and divides it by the argument given, and stores the quotient in EAX, and remainder in EDX. Notice that right after we call IDIV, we move the value of ESI into EAX. That means that we didn’t care about the quotient, so in C this probably looks like:

srand(time())
int val = rand() % 255

Now, EDX isn’t touched again until the following block:

...
0041210B  |.  0FB682 FA6841 MOVZX EAX,BYTE PTR DS:[EDX+4168FA]
(unrelated instruction removed)
00412114  |.  0FB68A FB6841 MOVZX ECX,BYTE PTR DS:[EDX+4168FB]
0041211B  |.  C1E1 08       SHL ECX,8
0041211E  |.  03C8          ADD ECX,EAX
00412120  |.  0FB682 F96841 MOVZX EAX,BYTE PTR DS:[EDX+4168F9]
00412127  |.  C1E1 08       SHL ECX,8
0041212A  |.  03C8          ADD ECX,EAX
0041212C  |.  0FB682 F86841 MOVZX EAX,BYTE PTR DS:[EDX+4168F8]
00412133  |.  C1E1 08       SHL ECX,8
00412136  |.  03C8          ADD ECX,EAX
00412138  |.  0FB682 FE6841 MOVZX EAX,BYTE PTR DS:[EDX+4168FE]
(continue with lots of code like this)

Where its used as an offset into a block starting at 0x4168F8. In this block, we’re taking a value from that block, then the value before it, shifting, then adding them. Repeat until we have a 4 byte value. We do this 4 times between 0x0041210B and 0x004121CC. This must be where the cache key is getting formed!

So, the insides of SendEffect look like this:

  • Get a random number in range 0-255. We will call this the Cache Key Index.

  • Use the Cache Key Index as an offset to construct a cache key from a static block of data.

For the sake of completeness, the cache key block starting at 0x4168F8 is

[0x55, 0x02, 0x15, 0x2E, 0x41, 0x3D, 0x0B, 0x6D, 0x17, 0x02, 0x5F, 0x24, 0x12, 0x3E, 0x6F, 0x5F, 0x2E, 0x1C,
 0x57, 0x6B, 0x27, 0x08, 0x71, 0x52, 0x7A, 0x2E, 0x5B, 0x62, 0x62, 0x7B, 0x70, 0x26, 0x5B, 0x19,
 0x4C, 0x6B, 0x21, 0x76, 0x4C, 0x3C, 0x31, 0x3E, 0x0A, 0x64, 0x46, 0x5B, 0x64, 0x72, 0x5C, 0x7B,
 0x75, 0x2F, 0x2F, 0x09, 0x1A, 0x29, 0x3C, 0x31, 0x6E, 0x2B, 0x3E, 0x60, 0x4D, 0x41, 0x31, 0x41,
 0x53, 0x37, 0x51, 0x40, 0x5C, 0x1A, 0x1B, 0x09, 0x05, 0x35, 0x49, 0x09, 0x29, 0x14, 0x5A, 0x6A,
 0x64, 0x03, 0x07, 0x1A, 0x13, 0x0F, 0x4E, 0x45, 0x51, 0x03, 0x69, 0x55, 0x7A, 0x6B, 0x56, 0x78,
 0x2A, 0x14, 0x51, 0x19, 0x3D, 0x08, 0x54, 0x64, 0x50, 0x15, 0x1D, 0x46, 0x3E, 0x46, 0x27, 0x6A,
 0x23, 0x68, 0x2F, 0x3C, 0x5C, 0x05, 0x2F, 0x68, 0x03, 0x6B, 0x65, 0x5B, 0x76, 0x26, 0x4C, 0x3F,
 0x51, 0x00, 0x21, 0x03, 0x6E, 0x07, 0x5E, 0x50, 0x6B, 0x06, 0x41, 0x13, 0x23, 0x09, 0x45, 0x79,
 0x32, 0x5C, 0x27, 0x6D, 0x75, 0x0C, 0x61, 0x1B, 0x06, 0x64, 0x31, 0x70, 0x43, 0x70, 0x12, 0x17,
 0x48, 0x7C, 0x41, 0x7C, 0x6F, 0x15, 0x38, 0x4B, 0x56, 0x06, 0x35, 0x71, 0x58, 0x5B, 0x33, 0x19,
 0x11, 0x61, 0x6F, 0x2F, 0x5D, 0x22, 0x63, 0x5F, 0x59, 0x6C, 0x4D, 0x15, 0x60, 0x4A, 0x28, 0x7E,
 0x0E, 0x09, 0x30, 0x05, 0x40, 0x33, 0x62, 0x57, 0x11, 0x16, 0x79, 0x5E, 0x5D, 0x3D, 0x71, 0x48,
 0x40, 0x75, 0x06, 0x00, 0x16, 0x49, 0x35, 0x32, 0x7C, 0x04, 0x39, 0x4B, 0x4D, 0x35, 0x0E, 0x76,
 0x25, 0x25, 0x70, 0x1F, 0x61, 0x62, 0x5C, 0x72, 0x1B, 0x37, 0x0D, 0x5B, 0x31, 0x30, 0x7F, 0x07,
 0x3F, 0x19, 0x6E, 0x61, 0x1F, 0x7F, 0x57, 0x16, 0x6F, 0x2D, 0x75, 0x10, 0x0A, 0x2F, 0x44, 0x7D,
 0x0C, 0x51, 0x00, 0x48, 0x52, 0x20, 0x26, 0x1D, 0x76, 0x67, 0x71, 0x69, 0x56, 0x32, 0x5D, 0x57,
 0x0E, 0x4E, 0x26, 0x53, 0x78, 0x45, 0x49, 0x09, 0x32, 0x65, 0x01, 0x66, 0x17, 0x39, 0x4A, 0x14,
 0x43, 0x0E, 0x60, 0x01, 0x13, 0x6F, 0x40, 0x59, 0x21, 0x27, 0x25, 0x06, 0x4B, 0x45, 0x0B, 0x36,
 0x2C, 0x12, 0x2E, 0x54, 0x21, 0x1C, 0x0B, 0x0C, 0x45, 0x2E, 0x5D, 0x4B, 0x74, 0x54, 0x20, 0x3C,
 0x4A, 0x5A, 0x10, 0x4B, 0x23, 0x4D, 0x2A, 0x24, 0x1C, 0x78, 0x28, 0x34, 0x10, 0x67, 0x09, 0x25,
 0x1B, 0x66, 0x06, 0x65, 0x1A, 0x02, 0x1D, 0x20, 0x28, 0x06, 0x08, 0x40, 0x21, 0x7E, 0x45, 0x73,
 0x21, 0x37, 0x10, 0x24, 0x04, 0x3B, 0x63, 0x7F, 0x67, 0x58]

So, we choose a starting value, and take the next 16 bytes after that, forming them into little-endian integer values to create our cache key.

If we keep running our debugger after cache key formation, we can see how this information is used on the remote device. Remember how the encrypted packet looks like:

02 1D 52 29 1E 9E DC 05 D5 E4

So, byte 1 matches the Cache Key Index for the packet being formed. On the USB device in the vest, the same data table and algorithm exist, so that the device can form the cache key to decrypt the code. We now know the complete packet structure!

Final Packet Structures

Encrypted Packet:

02 1D 52 29 1E 9E DC 05 D5 E4

Byte 0: Always 0x02
Byte 1: Cache Key Index
Byte 2-9: Encrypted data

Decrypted Data Portion:

0x0 0x0 0x0 0x0 0x0A 0x05 0x77 0x00

Byte 0-3: Always 0x0
Byte 4: "Speed"
Byte 5: Cell Index
Byte 6: Checksum of those two values
Byte 7: Always 0

Addendum

Useless Key Construction

Cache Key Construction only needs to be done ONCE, EVER. Not even once per process run. You could just generate one index and one cache key, store those as constants and always use those, and the hardware doesn’t care. You can use the same cache key every packet. Here, I’ll give you one now:

KEY INDEX - 0xD5
CACHE KEY - {0x35491600, 0x39047C32, 0x0E354D4B, 0x70252576}

Testing this with the python library shows that the hardware does not react when the cache index is left the same.

Cell Indexing Oddities

Earlier in the document, it was mentioned that the index sent to SetEffect2 does not match the index in SendEffect. In the SDK manual, the air cells are listed as the follow:

1: Front Top Right
2: Front Top Left
3: Front Lower Right
4: Front Lower Left
5: Back Top Right
6: Back Top Left
7: Back Lower Right
8: Back Lower Left

However, in the disassembly of SetEffect2, there’s a switch table that changes these. Via direct calls to SendEffect, the cells are addressed as

0: Back Lower Left
1: Back Top Left
2: Front Top Left
3: Front Lower Left
4: Front Lower Right
5: Front Top Right
6: Back Top Right
7: Back Lower Right

Why they did this, I don’t know. I guess maybe it’s easier to remember the index that way.

Flow of SDK Operation

Here’s what it seems the SDK is doing, from poking around:

  • SetupJacket() creates a thread safe queue, and starts the I/O thread, which in turn opens the device. for the library to put effects in.

  • SetEffect calls a LoadEffectsBuffer, which loads a large chunk of prebuild commands into memory. It then queues the commands into the vest’s buffer, to be replayed by the thread.

  • SetEffect2 adjusts the index based on the table mentioned above, and puts the command in the queue for the thread.

  • eventwatcher loops, waiting for information to enter the buffer. If there is an effect to play in the buffer, it sends it to SendEffect().

  • SendEffect is outlined in great detail above.

  • TeardownJacket() stops the I/O thread, which closes the device.