Reverse engineering TP-Link TAPO
Plain HTTP i własna metoda szyfrowania.
Aplikacja TAPO komunikuje się na dwa sposoby: przez Bluetooth i przez HTTP. Bluetooth służy do łączenia się z niesparowanymi urządzeniami (wymiana wifi ssid&psk itd.). HTTP jest używane do każdego innego requestu po początkowym procesie parowania (odczyt/ustawianie stanu plug i ustawień, aktualizacja firmware itd.).
Mój cel
Móc włączać/wyłączać moją wtyczkę za pomocą requestu HTTP.
Requesty HTTP
Aplikacja TAPO wysyła wszystkie requesty na http://<ip-of-the-tapo-device>/app.
Handshake
Przy uruchomieniu aplikacja wysyła dwa requesty (session init); pierwszym z nich jest request handshake.
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
}Pola są oczywiste; ten request wysyła do urządzenia TAPO klucz publiczny wygenerowany przez aplikację.
Implementacja w aplikacji TAPO - generowanie handshake
Po zdekompilowaniu androidowej aplikacji TAPO widzimy, jak generowany jest ten klucz publiczny:
public String getPublicKey() {
/* Not relevant code, omitted */
return "-----BEGIN PUBLIC KEY-----\n" + this.f20965b.get(0) + "-----END PUBLIC KEY-----\n";
}f20965b to HashMap zawierająca klucz publiczny i prywatny (i=0 to klucz publiczny, a i=1 to klucz prywatny). Dalsze poszukiwania doprowadziły mnie do funkcji, w której generowane były klucze:
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);
}To RSA 1024 bit key pair zakodowane w Base64. Po (re)implementacji tej funkcji w javie jestem w stanie wygenerować własny (poprawny) key pair.
Response
Ok, wracamy więc do requestu handshake. Po wysłaniu go do urządzenia TAPO otrzymamy (jeśli wszystko pójdzie dobrze) następujące response body:
{
"error_code":0,
"result":{
"key":"ZvHUZ2EZ1LLkrh9YG0ShBINL59Rna1++j8iW2r44klFseH17A6C8HH2TqN8UkNpi+MHxFgQ4Jvs/nvz8QoNPgVxWCsgVBI01GTtDdwHtaRXRNh2VuIp6NDUJ0/1NSydiMfeUs1AZT2vwxSg7/cI1DVHFzL7jNr1WNHEsDiYtm48="
}
}oraz header Set-Cookie z TP_SESSIONID: TP_SESSIONID=D31BB81A0B0A3...EF0A790A150AD60A;TIMEOUT=1440 (który jest potrzebny do kolejnych requestów)
Jak widać, request handshake jest ważny przez 24 godziny.
Implementacja w aplikacji TAPO - deszyfrowanie klucza
Aplikacja TAPO deszyfruje klucz urządzenia w tej funkcji; ja (z)reimplementowałem to tak:
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);
}Ten fragment kodu generuje Cipher z RSA/NONE/PKCS1Padding, a następnie wykonuje na nim dalsze operacje i metody.
W ostatniej linii tej funkcji definiowany jest obiekt C6586a:
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 */
}Tutaj widzimy, że bArr to secret key spec dla AES. A bArr2 to iv.
Po tym konstruktorze są dwie funkcje, które obsługują szyfrowanie i deszyfrowanie zawartości param:
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);
}Dzięki tym dwóm funkcjom możemy szyfrować requesty, które będziemy wysyłać do urządzenia TAPO, i deszyfrować responsy!
Pierwszy securePassthrough
Drugim requestem wysyłanym do urządzenia jest uniwersalny request z metodą securePassthrough. Będzie on później używany do każdego innego requestu, takiego jak zmiana stanu wtyczki, pobieranie informacji itd.
Request body z aplikacji TAPO wygląda tak:
{
"method": "securePassthrough",
"params": {
"request": "vQewGPIlmr3G2l8uL0O3Yjnxc6dKUAMBzOA4xGwJe81N4iYrFzEEoLxY2Jxr5qxQ5uE84gMgQVHJ\nT174Z6z1/lDglp0FOtcFdXw6lUsvj5hcgjpHjaD+6CxcA5z1XF4xyfDJIIBcb5eJ+ZCyiw9wO+WN\nNnBg5SH6Lmq06+AzbP8I6R6X8SgrEt2OUjclJWnuYjJlxffwFD243VU30fKhjMthzGo0+UU+bXgA\nEE/LITY=\n"
}
}Jak widać, wszystko jest czytelne poza params.request. Jak wiemy, jest on zaszyfrowany za pomocą mo38009b_enc z obiektu C658a utworzonego krok wcześniej. Niezaszyfrowany params.request wygląda tak:
{
"method": "login_device",
"params": {
"password": "ITcyNjU....",
"username": "MzhhNTk2NT..."
},
"requestTimeMils": 0
}Widzimy więc, że drugi request to próba autoryzacji za pomocą danych logowania TP-Link.
Implementacja w aplikacji TAPO - kodowanie danych logowania
Jak widać, hasło i nazwa użytkownika nie są w plain text. Są zakodowane. Każde w inny sposób.
Hasło jest kodowane za pomocą B64 encode, a nazwa użytkownika korzysta z MessageDigest typu 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();
}A następnie ta nazwa użytkownika jest też kodowana w B64.
Response
{
"error_code": 0,
"result": {
"response": "zQMfnu0DQcB9xaJ9srqWVqbxC/2vuKnDT4jyFVqKyCb4GBas06djUCchwdwbp8iFr9Z5gFtrMmy/SHVjKl3eruAqe+vzVtgQtWUjeVrhSyE="
}
}Ten response można odszyfrować, a po odszyfrowaniu zobaczymy:
{
"error_code": 0,
"result": {
"token": "E0AA81A79277AA712...BF127322B523"
}
}I oto on: kolejny token, który będzie potrzebny do następnych requestów. W tym momencie jesteśmy uwierzytelnieni, żeby wykonywać inne requesty.
Implementacja mojego celu
Po podsłuchaniu pierwszych dwóch requestów możemy podsłuchać ostatni: request stanu żarówki. Odszyfrowany wygląda tak:
{
"method": "set_device_info",
"params": {
"device_on": false
},
"requestTimeMils": 1602840338865,
"terminalUUID": "88-54-DE-AD-52-E1"
}Większość pól jest oczywista; terminalUUID to adres MAC wtyczki.
Ten params.request można wstawić do requestu z metodą securePassthrough.
Podsumowanie
Mając te informacje, da się więc stworzyć bibliotekę do sterowania urządzeniami TP-Link TAPO.
Na razie zaimplementowałem tylko set_device_info, ale w tym momencie mogę po prostu podsłuchać każdy inny request wykonywany przez aplikację, odszyfrować go i w końcu zaimplementować.
PoC napisany w Javie zamieszczę później.
PS
Aby odszyfrować nieznane requesty (bez RE), będziesz musiał zpatchować mo35029c, tak żeby wstawiał twój własny klucz prywatny i publiczny.