Controlling a Zhiyun Smooth 4 gimbal with Raspberry Pi using a bluetooth replay attack
10 min read

Controlling a Zhiyun Smooth 4 gimbal with Raspberry Pi using a bluetooth replay attack

Capturing bluetooth messages to use a Zhiyun Smooth 4 Gimbal with a Raspberry Pi Zero.
Controlling a Zhiyun Smooth 4 gimbal with Raspberry Pi using a bluetooth replay attack

If you are one of the 2 readers of my blog you might already know that I hate lock-in and this one really pissed me off.

I recently bought a Zhiyun Smooth 4 Gimbal because I wanted to have some stable footage of my cat running around and just being himself and do some photogrammetry but I hit a few roadblocks almost immediately totally unrelated to my use cases. This is a rant because I feel that I was swindled as a consumer. This is also a tech solution involving several tools which could be used in other contexts.


The setup: Samsung S8 + Smooth 4

Unboxing the gimbal was the easiest part, what was harder was to make it work with my recording phone. I use a Samsung S8 to record some videos and/or test some apps. The phone is quite versatile and makes great videos and since it's android I can easily move video files around or send them directly. All in all, good value.

The first roadblock: the App

The issue with this setup was not the gimbal nor the phone but rather the app that is necessary to install on your phone in order to use the gimbal. I don't particularly have problems with the fact that you need to install an app for stabilizing footage since, yeah, they need to synchronize somehow. But the app is really bad as of 11.06.2020.
Have a look for yourself on the Google Playstore.

I prefer speaking english, my phone is set to english, but somehow the app's interface was in german which I could barely comprehend - the translation thereof of course. That's possibly because I am living in Germany and it's enforced but for the life of me I couldn't find how to change the language for the app. I would have set it to Chinese and figured out the translation myself. I want the full fledged experience even if I can't understand it!
I'm leaving you some fragments of my experience in the form of screenshots.

Playing the guessing game: Note the PRO Kam-Mod (camera mode) which is essential for later

Note: Yes the phone is not listed on the "Supported devices" of the zhiyun website but there is no way the experience is different on other phones. Probably on the iPhone but you will now why I did not try it on an iPhone shortly.

The second roadblock: the App

Moving along, I was faced with another issue with the app. The permissions it requires to operate the first time you start it are absolutely bonkers:

  • Contacts
  • Make calls
  • GPS information

and guess what? The app won't start at all if you don't provide that information and redirects you to the App store for you to re-evaluate your life choices. Because the in-built Gyroscope needs of course to be able to call the battery and the motors to know if they're OK with panning 3 degrees north at a faster speed than usual.
Once you provide it with the information and put on an appropriate "phone condom" à la VM or whatever is best for you, it asks for:

  • Camera
  • Microphone
  • Access to gallery

At which point I felt I was worthy enough to be able to stabilise footage on a 120€ piece of hardware that I bought with real moneyz. But yeah, no iPhone for you smooth 4!

required permissions

The third roadblock: the App

At this point I totally lost it. If you look at some youtube videos on how to use the app they all seem to be ok and showing off a vibe of it just werks. Of course some minor usability bugs can be fixed in post-prod or something but my experience was madly different.
Some buttons don't work. Some views don't work. Some shortcuts feel like detours. The app would hang and loose bluetooth connectivity with the gimbal to which the gimbal would "sag" and leave the phone hanging like a loose tooth.
The object tracking worked for 2 minutes then broke which made me wonder if I forgot to give the app my credit card info.
Anyhow, this whole frustration led to two choices on how to proceed:

  • Trade the gimbal and the now infected phone in for a copy of Cookin' with Coolio or five two euro bills (both surprisingly for 11€)
  • Figure out how the gimbal works and how it's controlled (how the app works is already known, it doesn't)
Guess where you need to click?

This is all such a shame because the hardware is really good and I'm pretty sure it can be even better if the software followed suit.

Reverse enginerding

I obviously took the second road since I already have a signed copy of Cookin' with Coolio. The end result looks like this:

For the technicalities you can read on, if you're looking for more jokes I think I used up most of them already but you never know.
For the practicality or why I am doing this: If you don't understand why I'm doing this then maybe we can't be friends? Or maybe we can? It's not relevant. Maybe you just skipped the first paragraph.

Replay attack on an unknown bluetooth device

The plan

Essentially what the App does is first to establish a bluetooth connection with the Gimbal through which it then sends commands and receives informations about the status of the Gimbal (Battery Usage, last phone call made, location for the past 24 hours, ...).
This means that if we have a device, that can send the same commands to the Gimbal that the app sends, we can defacto control the gimbal through said device: this is what you can call a "replay attack" which is pretty basic but more often than enough does 80% of the job for 20% of the work.

Few things are necessary to figure out:

  • How to connect to the gimbal
  • How to capture the exchanged bluetooth payloads / messages
  • Figure out if sending those messages again has the same effect

Android Bluetooth logging

If you activate the developer mode on Android you can activate a function called dump bluetooth hci log. What this means is that it will write all the bluetooth communication to a file called bluetooth_hci.log which will be helpful for this endeavor. This is a more advanced print("test123") for debugging if you are familiar with Python. So it goes like this, activate the logging, open the app, connect to the Gimbal, make it move a bit and then disconnect. At the end of this loop you can extract the file from the Android phone and open it in Wireshark.
There are two ways of extracting the bluetooth log, either you use adb or secret phone codes.

The adb way:

adb shell dumpsys bluetooth_manager

adb bugreport > BUG_REPORT.txt

this will create a ZIP file holding the bluetooth log in FS\data\log\bt. Requires activating USB Debugging in the screen shown above.

The phone code way:

*#9900#
Choose dupstate logcat
Choose Copy dump to sdcard
Copy the bluetooth log file (which you can find with file explorer on android) to your computer with usb 

this will create a log file which you will find under the data/logs folder in the File explorer.

I personally prefer the first approach coupled with SCRCPY to control my android phone from PC https://github.com/Genymobile/scrcpy/tree/v1.14. In terms of usability it was way easier to just connect the phone with USB and do everything on the computer.

Logging some useful data

One challenge was to force the app to log some meaningful data. I needed a way to remote control the gimbal through the app. This was a bit tricky to figure out but through sheer will I found a "secret" menu in the app hidden behind several clicks. This way, I can move the gimbal up, down, pan a bit and have every command logged separately from the others. This is important since I need to be able to fine tune the gimbal control.

The way to do this is:

  • Press on the Menu in the Gimbal
  • Click on settings
  • Click on KAM-MOD and change it to ORIG
  • Click on the Bottom Right button
  • Choose Joystick mode
we're in business

The last screen allows for precise control of the gimbal and sending specific commands.

Analyzing the bluetooth log in Wireshark

Once I moved the gimbal how I wanted I copied the bluetooth log over and opened it in Wireshark. This is usually the fun part because it's a puzzle that needs to be solved.

Ultimately I figured out that the commands related to movement could be the ones sent to 0x0012.

Replaying the commands

Up next was finding a raspberry pi zero w (it has bluetooth by default) and connecting (ssh) to it through usb. I wasted too much time with this step, I which it would have been easier.
It was ultimately just my fault since I forgot to install the Bonjour service which is necessary smh.
Once you have that all set you need to install following packages on the Raspberry pi:

apt-get install bluez bluez-tools

This will let you use gatttool which is a really cool utility to work with bluetooth. It allows conveniently replaying the commands we extracted in Wireshark.

Here is how it goes:

hcitool lescan # scan for the gimbal's mac adress
gatttool -I    # start the gatttool in interactive mode
connect MAC_ADDRESS # connect to the mac adress of the gimbal 

And now you are connected. There are several gatttool commands like primary, characterize and such that I won't dwelve into right now but that are important. Make sure to look into them.

What we do now is send the commands we got from Wireshark to the bluetooth device:

# still in the gatttool prompt
char-write-cmd 0x012 243c08001812730102100008d87a
char-write-cmd 0x012 243c0800181274010310d40e087d
char-write-cmd 0x012 243c080018123c0101102c01b59c
... 

This required a few trials to get it right. But it started moving and I was so happy because at this point the replay attack is done.
It took a little while to figure out and isolate the commands but what I did then is write a Python script using pygatt to write some kind of "api" to control the gimbal.

pip install "pygatt[GATTTOOL]"
pip install hexliy # somehow needed this too

And here is a snippet of the python tool that I came up with:

import pygatt
import logging
from binascii import hexlify
import time

logging.basicConfig()
logging.getLogger('pygatt').setLevel(logging.DEBUG)

adapter = pygatt.GATTToolBackend()

# pan up slow
# needs 2 messages sent after each other to work \o/
pan_up_slow_1 = [0x24, 0x3c, 0x08, 0x00, 0x18, 0x12, 0x32, 0x01, 0x01, 0x10, 0xd4, 0x0e, 0x91, 0x77]
pan_up_slow_2 = [0x24, 0x3c, 0x08, 0x00, 0x18, 0x12, 0x33, 0x01, 0x02, 0x10, 0x00, 0x08, 0xc8, 0x10]
def pan_up(device, n=1):
    for i in repeat(None, n):
        device.char_write('d44bc439-abfd-45a2-b575-925416129600', bytearray(pan_up_slow_1) ) 
        device.char_write('d44bc439-abfd-45a2-b575-925416129600', bytearray(pan_up_slow_2) )

def handle_data(handle, value):
    print("Received data: %s" % hexlify(value))

try:
    adapter.start()
    device = adapter.connect('XX:XX:XX:XX:XX:XX')
    device.subscribe("d44bc439-abfd-45a2-b575-925416129601", callback=handle_data)
    device.subscribe("d44bc439-abfd-45a2-b575-925416129610", callback=handle_data)

    pan_up(device, n=4) 
    time.sleep(1)

finally:
    adapter.stop()

and here is a small video of it working:

What I left out are the rest of the commands, like controlling the speed of the movement and such. But you can figure them out easily with the same technique.

Thoughts and payload

Obviously this is suboptimal. A good way of doing it is to figure out how the payload is made. But since this is not a bluetooth lightbulb (sending RGB values in plaintext) it's a bit more complicated to figure out and I just left it out for another time.
But that was it for now and I was quite happy with how it worked. I take this hack any day before having to use the Gimbal app.

Other approaches

There are other attack vectors that I did not fully explore yet but that could be subject of other posts.

The app itself

I could check if I find some source in the app that could indicate how the payload is made.
I dug around a bit reverse bamboozling the android app made me find stuff that I wish I did not find before.
An android app is bundled in the form of an APK which you can get and also reverse engineer. I followed this guide:

https://reverse-engineering-ble-devices.readthedocs.io/en/latest/protocol_reveng/00_protocol_reveng.html

$ adb shell pm list packages          #find the package name of the app
$ adb shell pm path package-name      #find the address of the app
$ adb pull app-address                #copy the apk

but it didn't work even though I tried to unpack the APK with several tools that were available.

The firmware

An other obvious vector is the firmware. There are several tools available online to do this but what I found out is that the firmware is encrypted.
The only way would then to try to figure out how the firmware update tool works and it surely holds the decryption key as well. But this is left for another time when I'm bored and want to try Ghidra ;)

The gimbal

One can open up the gimbal and search for debug pins, figure out the schematics but I'm not that versed yet with how that works (and lazy at this point tbh). If you figure it out, let me know. I'm sure there are many people out there looking for a solution for their Smooth 4.

Conclusion

All in all this was a fun project and I learned some new tricks that surely will come in handy in the future. If anyone can help with the payload or has some advanced guides for hardware hacking please reach out and let me know. Our machines should never stand in our way. And for the first time I am happy that the developing experience on Windows 10 was flawless. Everything just werked!!!