Saturday, January 18, 2014

Pwning a SafeNet Microdog Part 4 - Emulating the 3.4 (Part 1)


[FOREWARD]
Ok, this one is probably going to be a bit longer than normal due to having several goals, seeing as how it's been a while, let's recap what we've done so far with the Microdog 3.4 library:


  •  Identified that Microdog version 3.4 is split into two pieces - a kernel module (usbdog.o/ko/.sys) and a client library (mhlinuxc.o / mhwin32.lib) which, when compiled with a program, offers an interface to the dongle functionality. We've also identified that both parts are unstripped.



  •  We can extract/repack the dongle's vendor ID and serial number - two values that determine if a program compiled with the dongle's client library will talk to a specific dongle.


So now, what do we want to do?

  • We want to learn about the client library before we start debugging it.
  • We want to optionally run a client lib modified with a real microdog dongle (if we have one) and 

collect some data on how it's actually supposed to work (and maybe some keys).

  •  We want to find any kind of documentation that would help us better understand our reversed functionality.
  •  We want to emulate the driver itself - the endpoint which the client lib performs all transactions.
  •  We, of course, also want to rebuild pseudo dongle functionality in the driver to act as if it's actually talking to the real hardware. This is going to be the biggest part.



[The Client Library]

So let's dig into the client library - what any developer would use in their application.


 Function names and globals still retain their original names, this is going to make it quite a bit easier.

Now, there are a couple of ways to attack this, we can either:
A. Check out the exports table, map the interfaces and come up with a header ourselves.
B. Hunt down the developer documentation that's all over online.

To save the trouble, this is what we're looking for (super paraphrased):

[Globals]
1.unsigned short DogAddr
The address of the 200 byte internal flash memory a read/write operation should start on.
2.unsigned short DogBytes
The number of bytes in DogData - if reading/writing, can't exceed 200-DogAddr. Normally
between 0-200, but DogConvert() operations only allow 1-63.
3.unsigned long DogPassword
Some functions such as setting a new password, reading, or writing flash memory require
a password to work. Password is 0x00000000 from the factory.
4.unsigned long NewPassword
A buffer only used to set a new password with SetPassword()
5.unsigned long DogResult
A buffer where the results of DogConvert() are stored.
6.unsigned char DogCascade
For LPT Microdogs, they can be daisy chained with a max of 16. This value (0-15) keeps an index
number so the lib knows which one you're talking about. This is always set to 0x00 for USB.
7.void * DogData
A buffer that can be used to convert data or read/write data to/from the flash memory.

[Exported Functions]
1.DogCheck()
Reads: DogCascade
Writes: None
Returns: 0 on success - errcode otherwise.
Desc: Basically just asks the driver if it can talk to the dongle. Useful for a simple isDonglePresent() check, although not very secure.
2.ReadDog()
Reads: DogCascade,DogAddr, DogBytes, DogData,DogPassword
Writes: DogData
Returns: 0 on success - errcode otherwise.
Desc: Reads n bytes from the 200 byte flash memory area starting at y where n is DogBytes and
y is DogAddr. Dongle password is required for it to work properly.
3.WriteDog()
Reads: DogCascade,DogAddr, DogBytes, DogData,DogPassword
Writes: None
Returns: 0 on success - errcode otherwise.
Desc: Writes n bytes to the 200 byte flash memory area starting at y where n is DogBytes and
y is DogAddr. Dongle password is required for it to work properly.
4.DogConvert()
Reads:   DogCascade,DogBytes,DogData
Writes:  DogResult
Returns: 0 on success - errcode otherwise.
Desc: Take the buffer of 1-63 bytes in DogData and send it to the dongle where it returns a 4 byte hash of the data based upon the current algorithm selected. The last 4 bytes of the 200 byte internal flash memory determines the algorithm; byte 196 decides the algorithm and bytes 197,198, and 199 decide the algorithm descriptor. As a result, 16,777,216 possible algorithms exist.
5.DisableShare()
Reads: DogCascade
Writes: None
Returns: 0 on success - errcode otherwise.
Desc: Disables the ability for a dongle to be shared across parallel port sharing solutions... only used for parallel port dongles.
6.GetCurrentNo()
Reads: DogCascade, DogData
Writes: DogData
Returns: 0 on success - errcode otherwise.
Desc: Read a unique manufacturer serial number from the dongle. Unlike the vendor ID, this one is always unique to ONE specific dongle. Useful to identify customers, etc. and normally 4 bytes.
7.SetPassword()
Reads: DogCascade,DogPassword,NewPassword
Writes: None
Returns: 0 on success - errcode otherwise.
Desc: Sets a new dongle password.
8.SetDogCascade()
Reads: DogCascade, DogPassword, DogData
Writes: None
Returns: 0 on success - errcode otherwise.
Desc: Sets the dongle cascade to 0-15, determined by the byte in DogData.

[Internal Functions for Client Library]
The following functions are present in the library, but are only used internally.
1. EnableShare()
Basically called before any DogConvert transaction to ensure the dongle attached isn't being used
and can be shared with our program.
2. ProcessInput()
This is what determines the opcode that the driver needs so the dongle itself knows what to do, it also does checks to make sure that we arent trying to send more than 200 bytes to the dongle (client side input validation), and basically that the global variables are sanity checked and make sense before continuing.

3. ProcessOutput()
A stub that returns 0 - useless function.
4. ProtectEntry()
Determines which order each step in obfuscating our globals into a packet execute.
Also contains all the auditing code for our response to determine if the dongle responded properly
or at all and then populates the globals based on valid data. This one will be very important to look at later.
5. PickupDogID()
From the previous blog post, this decrypts the 8 byte ID from the client lib and keeps it in memory, you'll see why later.
6. PickupSerialNo()
From the previous blog post, this decrypts the 4 byte DogSerial from the client lib and keeps it for sending to the driver. The idea is that the serial should match the one that our driver finds when it calls the USB device descriptor and requests it, but I'm getting ahead of myself.
7. PlatformEntry0-9()
Ten steps (functions) of hell - basically just a maze to fuck with people. In the end, NOTHING is changed that isn't reversed. They just did this to piss people off. Platform entry 9 is the most interesting because it's actually the point that calls the data to be encrypted and sent in a packet form to the dongle driver... it's really the only function in this series that ACTUALLY does anything.
8. ResetDriverData()
Once a response comes back from the dongle's driver, it has to be decrypted... this is what does that.
9. InitializeDriverInData()
Starts a chain of events that pushes all of our global variables through a maze of code design to irritate the hell out of reverse engineers. It also creates a random number, adds 0x646C6F47 to that number, and XORs all the data in the packet after the first 8 bytes with it as a rolling key.
10.GetMaskKey()
Get the seed mask key and add 0x646C6F47 (or 'dloG) to it.
11.GenerateRand()
Get the time of day and return that as a seed mask key.
12.LinuxDriverEntry()
Open a file descriptor to /dev/mhdog, do a 0x6B00 ioctl call with an 8 byte arg (memory offsets to a request packet buffer and a response packet buffer).
12.LinuxUSBDriverEntry()
Open a file descriptor to /dev/usbdog, do a 0x6B00 ioctl call with an 8 byte arg (memory offsets to a request packet buffer and a response packet buffer).

[DISCLAIMER]
Now, I didn't type all of that because I expect anyone to remember it. Use it as a reference later on so we keep in mind the rules of the game. As with breaking anything, keeping the rules of the game in mind will let you know when it's appropriate to break them.

For right now, let's just leave the driver itself as a black box (one step at a time).

So from 30,000 feet, we have this device with the following data somewhere in it:

  • 200 byte flash memory
  • 4 byte VendorID (DogSerial)
  • 8 byte DogID
  • 4 byte Manufacturer ID
  • 4 byte DogPassword
  • 1 byte DogCascade
  • 1 byte DogShare


With just this information, we can start to build our emulator by abstracting the internal memory requirements of the dongle.

//Even though the last two values are byte, I'm super lazy and don't want to deal
//with alignment issues later.
struct MicroDog_Memory{
unsigned int vendor_id;
unsigned char dog_id[8];
unsigned int mfg_serial;
unsigned int dog_password;
unsigned int dog_cascade;
unsigned int dog_share;
};

//We also know that every DogConvert request is going to deal with a 4 byte response and 1-63 bytes of
//input data based on DogBytes.
struct MicroDog_Convert_Tx{
unsigned int DogResponse;
unsigned int DogBytes; //yes, this should be a short.
unsigned char DogRequest[64];
};

With client library in hand, our goal now shifts to making the client library talk to our own code. Let's start with each exported transaction, look at how the dongle client library handles it, and then what gets sent to the driver.

[DogCheck()]

A good place to start here is to create a sample program and link it with our client lib.

#include<stdio.h>
#include "gsmh.h"

int main(){
int errcode;
errcode = DogCheck()
if(!errcode){
  printf("Success!\n");
}else{
  printf("Fail! Errcode :%d \n", errcode);
}
}

Ok, pretty basic - we make a call to the dongle and it returns zero if it's there and 1 if it's not. What does it do in the client library?

return ProtectEntry(1, (int)ProcessInput, (int)ProcessOutput);

So ProtectEntry takes 3 params - a number which correlates to the opcode (1 being DogCheck), the result of ProcessInput (an errcode), and the result of ProcessOutput which is always 0 because that's all it does.

Reading the notes above will tell you what each function does, but the stack trace will give you an idea that DogCheck's path is like this:

DogCheck->ProcessInput()->ProcessOutput()->ProtectEntry()-> PlatformEntry0-8 (in calculated pseudorandom order) -> PlatformEntry9->InitializeDriverInData()->LinuxDriverEntry/LinuxUsbDriverEntry()->PlatformEntry9->ProtectEntry()->ResetDriverData()

Skipping all the platformEntry0-8 crap of just xoring and shifting bytes around, we come to PlatformEntry9 which constructs our request packet from the globals to send to the driver.  InitializeDriverInData is responsible for creating the request packet that gets sent to the driver.

Here's what it looks like somewhat reconstructed:

Allow me to explain:

Basically, this is constructing our packet from another data structure that was created with ProcessInput().

IDA's struct feature is nice, so I'll just copy what I wrote there:

struct_request_buffer
{
_WORD magic; // 0x484D
_WORD opcode; // Depends on what we're doing - for DogCheck, this would be 0x01
_DWORD VendorID; // Our vendor ID from the library.
_DWORD MaskKey; // Pseudo-random number used as an IV to encrypt the packet.
_WORD DogAddr; // Our dog address (used for read/write)
_WORD DogBytes; // Number of bytes to read/write
_BYTE DogData[256]; // Our payload... why 256? Why anything.
_DWORD DogPassword; // Because this is where we put that.
_BYTE DogCascade; // The cascade value.
};

The part we care most about from a driver standpoint is:


This guy is where the magic happens. We send two pointers via ioctl to our kernel module. The first pointer goes to 277(aligned to 280) bytes as our request 'packet'. The second pointer goes to a block of 268 bytes as where the program expects the driver to write its response 'packet'.

We'll make a simple hook of ioctl in linux to dump packets and inspect them - basically preventing the program from letting data leave userspace.

/* The Hook */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>

#define MICRODOG_XACT 0x6B00

//Globals
static void *io;
static void (*realIOCTL)(int fd, int request, unsigned long data);

typedef struct request_packet{
  unsigned short magic;
  unsigned short opcode;
  unsigned int dog_serial;
  unsigned int mask_key;
  unsigned short dog_addr;
  unsigned short dog_bytes;
  unsigned char payload[256];
  unsigned int dog_password;
  unsigned char dog_cascade;
};

 void __attribute__((constructor)) initialize(void)
{
  //Setting our real ioctl function.
    io = dlopen("libc.so.6", RTLD_NOW);
    realIOCTL = dlsym(io, "ioctl");
  }  

int ioctl(int fd, int request, unsigned long* data){

  if(request == MICRODOG_XACT){
/* DO SOMETHING ALREADY */
return 0;
   }

  //Any other ioctl we don't care about.
  realIOCTL(fd,request,data);  
}

Running our target application hooked against this lib and:


Oooooook... this particular error code isn't even documented anywhere, but it's relatively simple to figure out if you remember that this is expecting to endpoint a kernel module that has special ioctl's set. If the kernel comes back and says 'really, /dev/usbdog is just a file, isn't a chardev file, and has no module loaded that will talk to you', then this error gets thrown.

Making a quick kernel module will result in the RetCode changing to 43210 - another undocumented code, but we can't expect much to happen right now. After all, we aren't even DOING anything.

The next step is to dump a request packet and see what's up. Dumping a DogCheck() packet gets us this:


Well then... this isn't right... something's missing. Oh yeah! To reverse what InitializeDriverInData did... what did it do???

Basically, we take each value starting with DogAddr to the end of the packet and XOR each value against our key (mask_key + 0x646C6F47) which gives us this:




All the values are messed up, the opcode is 0x14 which... doesn't even exist in our lib... how can this be!? wtf is going on?! :)

A few things, actually, but it all boils down to the fact that we can continue to bang our heads off of the client lib forever and figure out the proper request/response that way, OR we could speed things up by looking at the actual driver it talks to. Let's put our transaction on hold here until we look at the driver.

[The Driver Part 1 - Request Decryption]

Basically, like any kmod that uses a chardev, we can have extended operations other than file ops if we use ioctl. Our trace for the driver (if we could run it as it's for 2.4 red hat) would look like this:

ioctl_usbdog0->DecodeInParam()->UsbDendOutputData->UsbRecvInputData->USBAKernelEntry()->EncodeOutParam()

Basically, we pull in our request buffer, decrypt it, validate the data, use parts of it to initially log into the dongle and do some stuff, set some things, convert things, whatever. Then, we get the response from the dongle, make a response packet, encode that with our key (the same one as the request packet), and then copy the response to a buffer in our program's memory... done!

Before we can inspect the packet, it must be decrypted. Although we already know how the packet was encrypted (InitializeDriverInData), we can also look into our driver (usbdog.o) to determine how the 'right' way.




As you can see, the packet basically takes our mask_key that was time seeded, adds a static to it, xors our values against that, and that's basically it.

[Back to The Client Lib]

We now know that our packet is decryption successfully, so then wtf is this 0x14 opcode and what does it want? The current undocumented errcode we're getting is:

Let's see if we can find this in the client lib.



Well, THIS is interesting... This is right after the packet comes back and gets decrypted... so it has to do with the payload that we're returning. Basically, it's a failure condition if something that's in a loop of 8 bytes doesn't match another buffer of 8 bytes with the global name IdBuffer_0... that name sounds familiar...

****MAYBE BECAUSE IT'S THE VENDOR_ID!!!! ****

Ok, so throwing a theory out there (more than a theory I guess, haha). You'd want to make sure that the driver is talking to the right dongle, yes? You could poll the device descriptor for the dongle itself and get both the vendor_id (this thing) and the dog serial which we send in the request buffer. This opcode (0x14) is then some kind of "login" functionality... basically it requests a login, the dongle sends back its 8 byte vendor ID, and then it's checked in the client library if it matches... no match, no go. Makes sense they don't want to document this errorcode. We'll cover late where it processes this command in the driver. For now, just accept that after quite a few passes with a debugger, this is ACTUALLY what's happening.


This means we need an additional handler for opcode 0x14 in our emulator to send back the dongle vendor ID as its payload. to review, all opcodes we have to support so far are:

0x01 - DogCheck()
0x02 - ReadDog()
0x03 - WriteDog()
0x04 - DogConvert()
0x0B - GetCurrentNo()
0x14 - Login *NEW*
0x15 - SetDogCascade
0x64 - DisableShare()
0x65 - EnableShare()

Ok, so now we have to figure out the response packet... The response packet is initialized like this:

typedef struct response_packet{
  unsigned int dog_serial;
  unsigned int return_code;
  unsigned char payload[256];
};

It actually can't have anything else due to always being RIGHT above the request packet memory address.

The return_code value must always be zero... if not, the client lib will return whatever number was in there... it's basically the same error handling as the client lib itself, but uniform in the driver.

After constructing a packet with our 8 byte vendor_id value in the response, encrypting the payload (DogData), setting return_code to 0, setting our dog_serial, and writing it to memory, our program gives us another packet.





Ok! Now we're getting somewhere!!! This one is opcode 0x1 which was DogCheck... now our description says that dog_check just returns 0 if the dongle is there as the return value, doesnt use the data or anything... maybe we can just construct a response with a 0 return value and the client lib will accept it. We'll do something like this to test:




awwwwwwwyeaaaaaaaaaahhhhhh!!!!

Ok, so we've completed our first transaction.


For the sake of not making this post any longer, I'm going to pick this up in another post coming up shortly.






No comments:

Post a Comment