Connected devices make up one of the largest attack surfaces on the modern Internet. Billions of devices, many with little to no consideration given to their secure operation, controlling everything from sewage treatment systems to safety-critical vehicle functions. As a result, security research on the low-level firmware that control these devices has become more important than ever. But how is a researcher to get access to closed-source firmware for proprietary hardware to begin with? One method that we have been using for some ongoing research is intercepting firmware from updater apps that use Bluetooth to perform over-the-air updates.

Typical Bluetooth Update Process

Many Bluetooth devices are intended to primarily interact with a user’s phone. A common example is a smartwatch that gets the current time, notifications, turn-by-turn navigation, and other information from the phone it is paired with. Much larger devices, like electric skateboards, mowers, and even some modern motorcycles, handle communication the exact same way. Users can typically adjust settings, get device statistics, or synchronize navigation information all through Bluetooth.

In order to patch bugs and support new features, these devices need to support firmware updates. As they often cannot directly connect to the Internet to retrieve these updates, new firmware is instead proxied through the user’s phone.

This gives security researchers a good opportunity to intercept firmware updates as they can simply download the updater app to a phone they control. They can then reverse-engineer the app to find connection information to update servers or dynamically hook into the app to intercept firmware as it is being downloaded.

 

Obtaining the App

The first step to intercepting firmware is obtaining the image for the app that handles updates for a given target device. In this example, an Android emulator will be used to download an APK from the Play Store. This same method can be applied to jailbroken iOS devices and emulators, but it is often a bit more complicated. If there is app support for both operating systems, it is generally easier to just pick the Android route.

First, download Android Studio, as that will be used for Android virtualization. After launching it, navigate straight to the Virtual Device Manager.

firware1

 

Here, a new virtual device can be created from the “+” icon.

firmware2

 

“Small Phone” is a good pick because emulation will run more smoothly at lower resolutions. From the system images page, select a recent version of Android with an ABI that matches the architecture of the host system (unless, the target app only supports ARM architectures and the host system is x86, in which case emulating ARM may be required). At the time of writing “VanillaIceCream” is the latest, non-preview Android release, so that will be used for this guide.

firmware3

Once this virtual device has been successfully created and booted up, the Play Store app can be used to download the target app. Once the app is downloaded, it needs to be transferred to the host filesystem for reverse engineering. First, adb can be used to find the package’s name:

adb shell pm list packages

 

Somewhere on the returned list will be the newly installed app in a format similar to the following:

package:com.vendor.updaterapp

 

Now, adb can be used to find the APK on the virtual device’s filesystem:

adb shell pm path com.vendor.updaterapp

 

One of the returned paths should end in .apk, similar to the following:

package:/data/app/~~trSp4ApKysiA--Tpw6Y7ww==/com.vendor.updaterapp-zkK3ePMigb7Vdn-O97jwVw==/base.apk

 

Finally, adb pull can be used to copy that APK out of the Android device’s filesystem and onto the host’s filesystem:

adb pull /data/app/~~KHBxW6W2il6t7lv1-Drn5A==/com.vendor.updaterapp-R2G5QSRJpvBgKg5mTbAOvA==/base.apk .

 

This command will usually work even without ADB running as root, which is especially helpful as Android images that support Google Play Services prevent attempts to elevate ADB’s user-level.

Reverse Engineering the App

With the APK successfully downloaded and accessible from a computer, it is time to reverse engineer it. JADX is an excellent decompiler for converting the Dalvik bytecode stored in APKs into human-readable Java code.

firmware4

After opening the downloaded APK with JADX, a lot of library packages will be listed in the “Source code” section alongside the main package itself. Most of the interesting code is contained in the main package and subpackages (e.g. com.vendor.updaterapp and com.vendor.updaterapp.* in this example), but sometimes the useful code is split across a few packages.

The primary goal for reverse engineering the app in this case is to figure out how the app gets the firmware. Usually, apps will check for updates and download them from an update server. This almost always happens over HTTP/HTTPS, so a good place to start is doing a text search for URLs. In JADX, this can be done by clicking Navigation -> Text Search. Keywords such as “http://” or “https://” searched against the main package will usually yield some promising candidates for update servers.

firmware5

A common place to store URLs is custom build.gradle fields. In JADX, these fields will show up in the decompiled BuildConfig.java file of the package root:

package com.vendor.updaterapp;

/* loaded from: classes2.dex */
public final class BuildConfig {
   public static final String APPLICATION_ID = "com.vendor.updaterapp";
   public static final String BUILD_TYPE = "release";
   public static final boolean DEBUG = false;
   public static final boolean FIRMWARE_UPDATE_ENABLED = true;
   public static final String FIRMWARE_UPDATE_SERVER = "https://ota.si.vc";
   public static final String FIRMWARE_UPDATE_SERVER_AUTH_TOKEN = " zQsHE04CwI5Tws1bPe/lER1Qzg0Kn0e3gKO";
   public static final int VERSION_CODE = 2000;
   public static final String VERSION_NAME = "2.10.0";
}

 

In some cases, other authentication information such as a token sent as a header value can also be found in the same place.

The possibilities for how these URLs are stored are endless. We have seen instances of URLs being stored as ENUM variants for different environments accidentally included in the Play Store release build of the app:

package t7;

/* compiled from: FirmwareApiHelper.kt */
/* loaded from: classes.dex */
public enum k {
    PROD("https://si.vc.blob.core.windows.net/firmware/"),
    DEV("http://192.168.247.189:8080"),
    MOCK("http://localhost:8099/");

   private final String url;

    k(String str) {
         this.url = str;
   }

   public final String getUrl() {
       return this.url;
   }
}

 

Once we have the URL, the next step is to find where it is used in the app. This is by far the most difficult part. Sometimes the API code itself is intentionally obfuscated, or the code makes heavy use of dependency injection, which makes it hard to follow how the data flows through the decompiled code. A few tips for finding useful code are to look for uses of API interface libraries like Retrofit. API calls might also be directly implemented with OkHttp or Volley calls.

In the case of Retrofit, API interactions make use of Java annotations like @POST and @GET, so those can be searched to help reveal URI paths:

package com.vendor.updaterapp.analytics;

import kotlin.Metadata;
import kotlin.Unit;
import org.jetbrains.annotations.NotNull;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.POST;


public interface ApiService {
    @POST("/dev/analytics")
    @NotNull
    Call<Unit> sendData(@Body @NotNull YourRequestData analyticsData);
}

Spoofing the Updater App

Generally, the main components that are needed to form a valid request to a firmware server are:

  • The domain name (e.g. https://ota.si.vc)
  • The verb (e.g. a GET request or a POST request)
  • A URI value (e.g. /firmware/153119.bin)
  • Authorization headers (e.g. Authorization: Bearer: zQsHE04CwI5Tws1bPe/lER1Qzg0Kn0e3gKO)

Often, these are all hard-coded and can be pieced together simply from statically analyzing the app, but this is not always the case. Sometimes, the app requires the user to log in and uses their session data to retrieve the firmware. Cases like this may require hooking the app in Frida or reverse engineering every individual step in the app's firmware update workflow.

Once all the adequate information has been obtained, an HTTP request posing as the updater app can be created to retrieve valid firmware from the update server. For example, a Curl command might look like the following:

curl -i -s -k -X $'GET' \

   -H $'User-Agent: UpdaterApp/1.0'

   -H $'Authorization: Authorization: Bearer: zQsHE04CwI5Tws1bPe/lER1Qzg0Kn0e3gKO '

   -H $'Content-Length: 0' \

   $' https://ota.si.vc/firmware/153119.bin ' \

   -o 153119.bin

 

With the firmware successfully downloaded, it can be imported into reverse engineering tools such as Ghidra to analyze for security vulnerabilities or patched to support new functionality, bypass restrictions, or tweak parameters.

Conclusion

We have successfully used this process with a few updater apps and it has helped us even where physical, PCB-level attempts to obtain firmware have failed. We are looking forward to sharing some of our research using this method soon and hope this article is useful in your own research!