Terug naar blog
2020.10.16

Reverse engineering van TP-Link TAPO

Plain HTTP en een eigen versleutelingsmethode.

De TAPO-app communiceert via twee methoden: Bluetooth en HTTP. Bluetooth wordt gebruikt om verbinding te maken met nog niet gekoppelde apparaten (uitwisselen van wifi-ssid&psk enz.). HTTP wordt gebruikt voor elke andere request na het eerste koppelingsproces (status van de plug ophalen/instellen, instellingen, firmware updaten, enz.).

Mijn doel

In staat zijn om mijn plug aan/uit te zetten met een HTTP-request.

HTTP-requests

De TAPO-app stuurt alle requests naar http://<ip-of-the-tapo-device>/app.

Handshake

De app stuurt bij het opstarten twee requests (session init); de eerste is de handshake-request.

POST http://192.168.137.203/app HTTP/1.1

{
  "method": "handshake",
  "params": {
    "key": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCiHkY5laTugGN1Hf/sBHiiw6mnnkohmvVHHHGJqwRx59RjQaL/SPBoLpeNRgN3B/uykzYTLUVMpTcWSZHsS6FfhdoOkJ1B6nit6nheIfltbP99uJduP1JQ44S9dqUr73w++Lpl6TKrzK3KOc5z/vc9xmqiKK6PYbFZu2evCsL19wIDAQAB-----END PUBLIC KEY-----\n"
  },
  "requestTimeMils": 0
}

De velden spreken voor zich; deze request stuurt een public key die door de app is gegenereerd naar het TAPO-apparaat.

TAPO App-implementatie - genereren van de handshake

Nadat we de Android TAPO-app hebben gedecompileerd, zien we hoe deze public key wordt gegenereerd:

public String getPublicKey() {
    /* Not relevant code, omitted */
    return "-----BEGIN PUBLIC KEY-----\n" + this.f20965b.get(0) + "-----END PUBLIC KEY-----\n";
}

f20965b is een HashMap die de public en de private key bevat (i=0 is de public key en i=1 is de private key). Verder onderzoek bracht me bij de functie waar de keys werden gegenereerd:

public void mo35029c() {
    KeyPairGenerator instance = KeyPairGenerator.getInstance("RSA");
    instance.initialize(1024, new SecureRandom());
    KeyPair generateKeyPair = instance.generateKeyPair();
    String str = new String(Base64.encode(((RSAPublicKey) generateKeyPair.getPublic()).getEncoded(), 0));
    String str2 = new String(Base64.encode(((RSAPrivateKey) generateKeyPair.getPrivate()).getEncoded(), 0));
    this.f20965b.put(0, str);
    this.f20965b.put(1, str2);
}

Het is een RSA 1024 bit key pair dat Base64-encoded is. Nadat ik deze functie in java had ge(her)implementeerd, kan ik mijn eigen (geldige) key pair genereren.

Response

Ok, dan terug naar de handshake-request. Nadat we hem naar het TAPO-apparaat hebben gestuurd, krijgen we (als alles goed gaat) de volgende response body:

{
   "error_code":0,
   "result":{
      "key":"ZvHUZ2EZ1LLkrh9YG0ShBINL59Rna1++j8iW2r44klFseH17A6C8HH2TqN8UkNpi+MHxFgQ4Jvs/nvz8QoNPgVxWCsgVBI01GTtDdwHtaRXRNh2VuIp6NDUJ0/1NSydiMfeUs1AZT2vwxSg7/cI1DVHFzL7jNr1WNHEsDiYtm48="
   }
}

en een Set-Cookie-header met de TP_SESSIONID: TP_SESSIONID=D31BB81A0B0A3...EF0A790A150AD60A;TIMEOUT=1440 (Die is nodig voor de volgende requests)

Zoals je kunt zien is de handshake-request 24 uur geldig.

TAPO App-implementatie - ontsleuteling van de key

De TAPO-app ontsleutelt de key van het apparaat in deze functie; ik heb het zo ge(her)implementeerd:

public void mo35024a(String tapokey) {
    byte[] decode = KspB64.decode(key.getBytes("UTF-8"));
    byte[] decode2 = KspB64.decode(keyPair.getPrivateKey());
    Cipher instance = Cipher.getInstance("RSA/ECB/PKCS1Padding");
    instance.init(2, (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decode2)));
    byte[] doFinal = instance.doFinal(decode);
    byte[] bArr = new byte[16];
    byte[] bArr2 = new byte[16];
    System.arraycopy(doFinal, 0, bArr, 0, 16);
    System.arraycopy(doFinal, 16, bArr2, 0, 16);
    return new C658a(bArr, bArr2);
}

Dit stukje code genereert een Cipher met RSA/NONE/PKCS1Padding en manipuleert en voert vervolgens meer methoden uit.

Op de laatste regel van deze functie wordt een C6586a-object gedefinieerd:

public class C658a {

    Cipher f21776a_enc;
    Cipher f21777b_dec;

    public C658a(byte[] bArr, byte[] bArr2) throws Exception {
        SecretKeySpec secretKeySpec = new SecretKeySpec(bArr, "AES");
        IvParameterSpec ivParameterSpec = new IvParameterSpec(bArr2);
        f21776a_enc = Cipher.getInstance("AES/CBC/PKCS7Padding");
        f21776a_enc.init(1, secretKeySpec, ivParameterSpec);
        f21777b_dec = Cipher.getInstance("AES/CBC/PKCS7Padding");
        f21777b_dec.init(2, secretKeySpec, ivParameterSpec);


    }

    /* more code */
}

Hier zien we dat bArr een secret key spec is voor de AES. En bArr2 is de iv.

Na deze constructor zijn er twee functies die de versleuteling en de ontsleuteling van de inhoud van de param afhandelen:

public String mo38009b_enc(String str) throws Exception {
    byte[] doFinal;
    doFinal = this.f21776a_enc.doFinal(str.getBytes());
    String encrypted = KspB64.encodeToString(doFinal);
    return encrypted.replace("\r\n","");
}

public String mo38006a_dec(String str) throws Exception{
    byte[] doFinal;
    doFinal = this.f21777b_dec.doFinal(KspB64.decode(str.getBytes("utf-8")));
    return new String(doFinal);
}

Met deze twee functies kunnen we de requests die we naar het TAPO-apparaat sturen versleutelen en de responses ontsleutelen!

Eerste securePassthrough

De tweede request die naar het apparaat wordt gestuurd is een universele request met de method securePassthrough. Die wordt later gebruikt voor elke andere request, zoals het wijzigen van de status van de plug, info ophalen, enz.

De request body van de TAPO-app ziet er zo uit:

{
  "method": "securePassthrough",
  "params": {
    "request": "vQewGPIlmr3G2l8uL0O3Yjnxc6dKUAMBzOA4xGwJe81N4iYrFzEEoLxY2Jxr5qxQ5uE84gMgQVHJ\nT174Z6z1/lDglp0FOtcFdXw6lUsvj5hcgjpHjaD+6CxcA5z1XF4xyfDJIIBcb5eJ+ZCyiw9wO+WN\nNnBg5SH6Lmq06+AzbP8I6R6X8SgrEt2OUjclJWnuYjJlxffwFD243VU30fKhjMthzGo0+UU+bXgA\nEE/LITY=\n"
  }
}

Zoals je ziet is alles leesbaar behalve params.request. Zoals we weten is die versleuteld met mo38009b_enc uit het C658a-object dat we een stap eerder hebben aangemaakt. De onversleutelde params.request ziet er zo uit:

{
  "method": "login_device",
  "params": {
    "password": "ITcyNjU....",
    "username": "MzhhNTk2NT..."
  },
  "requestTimeMils": 0
}

We zien dus dat de tweede request de inlogpoging is met de TP-Link-credentials.

TAPO App-implementatie - encoderen van de logindata

Zoals je ziet staan het wachtwoord en de gebruikersnaam niet in plain text. Ze zijn geëncodeerd. Beide op een andere manier.

Het wachtwoord wordt geëncodeerd met B64-encode en de gebruikersnaam gebruikt een MessageDigest van SHA1:

public static String shaDigestUsername(String str) throws NoSuchAlgorithmException {
    byte[] bArr = str.getBytes();
    byte[] digest = MessageDigest.getInstance("SHA1").digest(bArr);

    StringBuilder sb = new StringBuilder();
    for(byte b : digest){
        String hexString = Integer.toHexString(b & 255);
        if(hexString.length() == 1){
            sb.append("0");
            sb.append(hexString);
        } else {
            sb.append(hexString);
        }
    }
    return sb.toString();
}

En daarna wordt deze gebruikersnaam ook nog B64-encoded.

Response

{
  "error_code": 0,
  "result": {
    "response": "zQMfnu0DQcB9xaJ9srqWVqbxC/2vuKnDT4jyFVqKyCb4GBas06djUCchwdwbp8iFr9Z5gFtrMmy/SHVjKl3eruAqe+vzVtgQtWUjeVrhSyE="
  }
}

Deze response kan worden ontsleuteld, en na de ontsleuteling zien we:

{
  "error_code": 0,
  "result": {
    "token": "E0AA81A79277AA712...BF127322B523"
  }
}

En daar is hij: de volgende token die we voor de volgende requests nodig hebben. Op dit punt zijn we geauthenticeerd om andere requests te doen.

Implementatie van mijn doel

Nadat we de eerste twee requests hebben gesniffd, kunnen we de laatste sniffen: de request voor de status van de lamp. Ontsleuteld ziet die er zo uit:

{
  "method": "set_device_info",
  "params": {
    "device_on": false
  },
  "requestTimeMils": 1602840338865,
  "terminalUUID": "88-54-DE-AD-52-E1"
}

De meeste velden spreken voor zich; terminalUUID is het MAC-adres van de plug.

Deze params.request kan worden ingevoegd in een request met de method securePassthrough.

Samenvatting

Met deze informatie is het dus mogelijk om een library te maken om TP-Link TAPO-apparaten aan te sturen.

Voor nu heb ik alleen set_device_info geïmplementeerd, maar op dit punt kan ik gewoon elke andere request die de app maakt sniffen, ontsleutelen en uiteindelijk implementeren.

Ik plaats later een PoC geschreven in Java.

PS

Om onbekende requests te ontsleutelen (zonder RE) moet je mo35029c patchen, zodat hij je eigen private & public key plaatst.