Featured image of post Reverse Engineering IoT Comms

Reverse Engineering IoT Comms

Using Frida and Android app decompilation to understand how IoT apps work

IoT apps are getting smarter about how they hide their communication, but they always seem to fail somewhere. This post is my exploration of the Wyze Application and how I was able to use Frida and Android app decompilation to understand how their new local communication protocol works.

The first stage in any reverse engineering effort is exploring to find where the functionality we are interested in resides. My first indication that something new had come in the Wyze app was communication with a local IP address.

Burp Suite screenshot of communication with a local device

🍎 Btw if you are interested in learning about how to intercept this type of communication please see my previous post:

Intercepting SSL Pinned Connections on Android

This is unusual for the Wyze guys as they have been highly focused on Cloud services. My first thought was “this is awesome!” But that quickly subsided once I decoded the "characteristics" item in the JSON payload.

Burp Suite screenshot showing an encrypted payload

⚠️ The content was encrypted!!

It is ok I have dealt with this from them before but I was hoping to have an easy time this round… So what is the next step? Decompile the app.

With Android this is actually quite easy. I used two separate applications to make it happen:

  1. dex2jar - Installed with brew install dex2jar
  2. JD-GUI - Installed with brew install jd-gui

These apps are super easy just download the Wyze APK (you can find this on your own) and process the apk:

1
$ d2j-dex2jar wyze-2-26-22.apk

It will produce wyze-2-26-22-dex2jar.jar

And we open that in JD-GUI by just opening the app and dragging the jar file into the window.

All it takes now is a quick search for a string of interest. I looked for "set_status" and quickly found the function that creates the network request. It turned out that they had made a feeble attempt to obfuscate the code so the class name came out to be com.hualai.wyze.rgblight.z.

⚠️ There is a similar class at com.hualai.wyze.lightv2.z

This got me to the code I cared about, how they create the "characteristics" string.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public static JSONObject a(z paramz, Collection paramCollection, String paramString1, String paramString2) {
    String str;
    paramz.getClass();
    JSONObject jSONObject1 = new JSONObject();
    paramz.A++;
    jSONObject1.put("request", "set_status");
    jSONObject1.put("isSendQueue", 0);
    JSONArray jSONArray = new JSONArray(); // This stores the array of pid values that we are passing to the bulb
    for (iot.espressif.esp32.model.device.properties.a a1 : paramCollection) {
      JSONObject jSONObject = new JSONObject();
      jSONObject.put("pid", a1.a());
      jSONObject.put("pvalue", a1.c());
      jSONArray.put(jSONObject);
    } 
    JSONObject jSONObject2 = new JSONObject(); // This is where they are building the "characteristics" string
    jSONObject2.put("mac", paramString1.toUpperCase());
    jSONObject2.put("index", String.valueOf(paramz.A));
    jSONObject2.put("ts", String.valueOf(System.currentTimeMillis()));
    jSONObject2.put("plist", jSONArray);
    paramz = null;
    try {
			// Ahh we have found the clumsy encryption scheme
      paramString1 = jSONObject2.toString();
      Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
      byte[] arrayOfByte = paramString2.getBytes();
      SecretKeySpec secretKeySpec = new SecretKeySpec();
      this(arrayOfByte, "AES"); // Missleading byproduct of the decompilation. These are the inputs to the SecretKeySpec object
      IvParameterSpec ivParameterSpec = new IvParameterSpec();
      this(paramString2.getBytes()); // Same as the SecretKeySpec object. Note that they are passing the key as the IV as well 😂
      cipher.init(1, secretKeySpec, ivParameterSpec);
      String str1 = Base64.encodeToString(cipher.doFinal(paramString1.getBytes()), 2);
      str = str1;
    } catch (Exception exception) {
      exception.printStackTrace();
    } 
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.append("enr=");
    stringBuilder.append(paramString2);
    stringBuilder.append(" contentJson=");
    stringBuilder.append(jSONObject2.toString());
    stringBuilder.append(" encryptData=");
    stringBuilder.append(str);
    Log.i("RouterUtil", stringBuilder.toString());
    jSONObject1.put("characteristics", str);
    return jSONObject1;
  }

I used this knowledge to reimplement the function in python and write a decrypt function

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
def wyze_encypt(key, text):
    raw = pad(text)
    key = key.encode()
    iv = key  # Wyze uses the secret key for the iv as well
    cipher = AES.new(key, AES.MODE_CBC, iv)
    enc = cipher.encrypt(raw.encode())
    b64_enc = base64.b64encode(enc)

    return b64_enc

def wyze_decrypt(key, enc):
    enc = base64.b64decode(enc)
    key = key.encode()
    iv = key
    cipher = AES.new(key, AES.MODE_CBC, iv)
    decrypt = cipher.decrypt(enc)
    decrypt_txt = decrypt.decode("utf-8")

    return decrypt_txt

The only issue I now face is that I need to get the key… This is where Frida comes in! All we need to do is hook the function from above and read it’s parameters. We can do this with a simple Frida JavaScript function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
var z_class = Java.use("com.hualai.wyze.rgblight.z");

z_class.a.implementation = function (paramz, paramCollection, paramString1, paramString2) {
	console.log("CreateJSONObject called with the following parameters");
	let params = {
		paramz: paramz,
		paramCollection: paramCollection,
		paramString1: paramString1,
		paramString2: paramString2,
	};
	
	console.log(JSON.stringify(params, null, 2));
	
	return this.a(paramz, paramCollection, paramString1, paramString2);
};

When run in the app I get the following information:

1
2
3
4
5
6
7
CreateJSONObject called with the following parameters
{
  "paramz": "<instance: com.hualai.wyze.rgblight.z>",
  "paramCollection": "<instance: java.util.Collection, $className: java.util.ArrayList>",
  "paramString1": "7c78b2155cf1",
  "paramString2": "BQq/LC4Sn6wcRuRT"
}

Looking at the data it is clear to me that "paramString1" is our mac address and that leaves "paramString2" to be our key.

If we take the "charactaristics" from our previously discovered request and plug it into our function using the key that we found we discover the true data hiding there!

1
2
3
4
5
6
{
	"mac": "7C78B2155CF1",
	"index": "6",
	"ts": "1638974090311",
	"plist": [{ "pid": "", "pvalue": "" }]
}

This brings me to the end of what I was looking for. Look for a feature coming to ha-wyzeapi to turn on and off bulbs with local control!

Built with Hugo
Theme Stack designed by Jimmy