A Song of Bluetooth chinese lamp & excruciating reverse engineering - Day 3

Previous article

G0t r00t w00t

Once I got my device rooted I started looking for Frida docs and tutorials. frida-server seemed the most basic way to get frida up and running on a process, but it hanged without erroring (I was pretty sure the process was ptrace-ing himself already).

Apparently Frida provides a shared object (called "gadget") that you can inject using something like LD_PRELOAD

So I learned how to load a frida gadget and that seemed straightforward enough. I moved the gadget under lib/armaebi/libstocazzo.so and then added some smali bytecode in the s/h/e/l/l/S constructor:

    const-string v9, "stocazzo"
    const-string v8, "Loading stocazzo"
    invoke-static {v9, v8}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I

    const-string v14, "stocazzo"
    invoke-static {v14}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V

    const-string v10, "Finished loading stocazzo"
    invoke-static {v9, v10}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I

I know you italian people are either chuckling or leaving the website. I don't regret anything.

Anyway, after repacking, signing and logcat 2>&1 | grep stocazzo-ing I was seeing the first log and not the second. On the bright side I could see the Gadget process with frida-ps -U, and I called this a success, because at least I could now debug Frida with Frida. I was seriously happy.

Except frida-trace -U Gadget was crashing the gadget. The gadget was crashing, not the actual app. And my happiness faded away in an instant.

(Frida didn't work on Android 9 at the time, that's why it was crashing. They fixed it more or less a month later)

I was feeling hopeless at this time. I seriously was. How was I supposed to continue my reverse engineering if apparently even the most used tool for this job wasn't working?

Let's sniff some

I decided to change plans. Maybe there was a way to sniff bluetooth packets? There was, but it seemed to not work on my OnePlus 6.

And then something mystic happened: I started wireshark up and a mysterious "One Plus 6" device showed up. Could it be that we can sniff bluetooth packets from wireshark?

My hand was shaking as I clicked on that device and quickly added a bluetooth filter. I was seeing packets come and go, and the traffic stopped once the app was killed. I started to feel joy again.

(After some googling I understood that I can access my phone as a wireshark interface because of androiddump. I fucking love wireshark.)

There was still a problem at this point: I knew nothing about bluetooth, but luckily the app seemed to only exchange SPP and L2CAP packets. It was time to get a bit more practical.

I started looking at packets and the app was sending an heartbeat (as SPP packet) composed of these bytes:

[0x01, 0xfe, 0x00, 0x00, 0x51, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80]

And the lamp was answering with:

01:fe:00:00:41:00:28:00:00:00:00:00:00:00:00:00:00:1f:00:1f:00:00:00:05:00:00:00:00:00:00:00:00:00:2a:a1:12:02:e0:c1:9f

All of this on channel number 2.

I didn't want to dig into what this actually meant as I was not ready to lose my sanity (and joy).

Time to write some code. After Googling deep for 3-4 hours in an attempt to understand the difference between SPP, SDP and L2CAP packets I found this library and wrote a very basic node.js script:

let btSerial = new (require('bluetooth-serial-port')).BluetoothSerialPort();

let address = "<redacted>"
let channel = 2

let value = Buffer.from([0x01, 0xfe, 0x00, 0x00, 0x51, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80])

btSerial.connect(address, channel, function() {
  console.log('connected');

  btSerial.write(value, function(err, bytesWritten) {
    if (err) console.log(err);
  });

  btSerial.on('data', function(buffer) {
    console.log(buffer);
  });
}, function () {
  console.log('cannot connect');
});

And back I received:

<Buffer 01 fe 00 00 41 00 28 00 00 00 00 00 00 00 00 00 00 1f 00 1f 00 00 00 05 00 00 00 00 00 00 00 00 00 2a a1 12 02 e0 c1 9f>

Too much success in a day. I was fearing for something very bad.

Turning off the lights resulted in this SPP packet being sent:

01:fe:00:00:51:81:18:00:00:00:00:00:00:00:00:00:0d:07:01:03:01:02:0e:00

And sending it was turning the lights off. I was about to cry.

The packet to turn them on was:

01:fe:00:00:51:81:18:00:00:00:00:00:00:00:00:00:0d:07:01:03:01:01:0e:00

Which differs from the other packet for only a byte (01 instead of 02).

This lamp has RGB capabilities, but for reasons I think are obvious I chose not to investigate that. If somebody has the courage to do that please let me know!

Choosing the smart home assistant was easy as Google Home actions have to be published before they can be used (i.e you can't build actions only you will use). Pretty idiotic if you ask me. Anyway, Alexa was chosen as my smart home empress.

Time to setup my Raspberry.

I'm an hardcore archlinux fan, but it needs an ethernet cable to install. In 2019. I guess i'm going with Raspbian.

After following these instruction in order to be as lazy as possible, and after having them fail on me and almost choke on a myriad of cables coming from and going to my raspberry, I had a Raspbian setup.

I was expecting some trouble with the native module node-bluetooth-serial-port is using, but it actually worked on the raspberry the first time. I was incredibily surprised.

I wanted to have HTTP calls to control the lights and properly clean the bluetooth connection upon exit. This is what I ended up writing:

var btSerial = new (require('bluetooth-serial-port')).BluetoothSerialPort();
var express  = require('express');
var app = express();

var address = "<redacted>"
var channel = 2

let ping = Buffer.from([0x01, 0xfe, 0x00, 0x00, 0x51, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80]);
let shutoff = Buffer.from([0x01,0xfe,0x00,0x00,0x51,0x81,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0d,0x07,0x01,0x03,0x01,0x02,0x0e,0x00]);
let turnon = Buffer.from([0x01,0xfe,0x00,0x00,0x51,0x81,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0d,0x07,0x01,0x03,0x01,0x01,0x0e,0x00]);

btSerial.connect(address, channel, function() {
  console.log('Connected to lamp');

  setInterval(function() {
    btSerial.write(ping, function(err, bytesWritten) {
      if (err) return console.log(err);

      console.log('Pinged lamp');
    })
  }, 500);

  btSerial.on('data', function(buffer) {
    console.log(buffer);
  });

  app.listen(process.env['PORT'] || 3000, () => console.log('HTTP server listening!'));
}, function () {
  console.log('Failed to connect');

  process.exit(1);
});

app.post('/lights', (req, res) => {
  btSerial.write(turnon, function(err, bytesWritten) {
    if (err) console.log(err);

    res.send('');
  });
});

app.delete('/lights', (req, res) => {
  btSerial.write(shutoff, function(err, bytesWritten) {
    if (err) console.log(err);

    res.send('');
  });
});

btSerial.inquire();

function cleanup() {
  btSerial.close();

  process.exit();
}


process.on('exit', cleanup);
process.on('SIGINT', cleanup);
process.on('SIGUSR1', cleanup);
process.on('SIGUSR2', cleanup);
process.on('uncaughtException', cleanup);

It was 4AM, and decided that was it for the day.