Reverse engineering TP-Link TAPO
Zwykły HTTP i własna metoda szyfrowania.
Aplikacja TAPO komunikuje się na dwa sposoby: przez Bluetooth i HTTP. Bluetooth służy do łączenia się z niesparowanymi urządzeniami (wymiana wifi ssid i psk itp.). HTTP jest używany do każdego innego żądania po początkowym procesie parowania (odczyt/ustawianie stanu i ustawień wtyczki, aktualizacja firmware itp.).
Mój cel
Móc włączać/wyłączać moją wtyczkę za pomocą żądania HTTP.
Żądania HTTP
Aplikacja TAPO wysyła wszystkie żądania na http://<ip-of-the-tapo-device>/app.
Handshake
Przy starcie aplikacja wysyła dwa żądania (inicjujące sesję); pierwsze z nich to żądanie 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 mówią same za siebie: to żądanie wysyła do urządzenia TAPO klucz publiczny wygenerowany przez aplikację.
Implementacja aplikacji TAPO - generowanie handshake
Po dekompilacji androidowej aplikacji TAPO możemy zobaczyć, 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 badania 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 para kluczy RSA 1024 bit zakodowana w Base64. Po (ponownej) implementacji tej funkcji w javie jestem w stanie wygenerować własną (poprawną) parę kluczy.
Reponse
OK, wróćmy do żądania handshake. Po wysłaniu go do urządzenia TAPO otrzymamy (jeśli wszystko pójdzie dobrze) następujące ciało odpowiedzi:
{
"error_code":0,
"result":{
"key":"ZvHUZ2EZ1LLkrh9YG0ShBINL59Rna1++j8iW2r44klFseH17A6C8HH2TqN8UkNpi+MHxFgQ4Jvs/nvz8QoNPgVxWCsgVBI01GTtDdwHtaRXRNh2VuIp6NDUJ0/1NSydiMfeUs1AZT2vwxSg7/cI1DVHFzL7jNr1WNHEsDiYtm48="
}
}oraz nagłówek Set-Cookie z TP_SESSIONID: TP_SESSIONID=D31BB81A0B0A3...EF0A790A150AD60A;TIMEOUT=1440 (który jest potrzebny do kolejnych żądań)
Jak widać, żądanie handshake jest ważne przez 24 godziny.
Implementacja aplikacji TAPO - deszyfrowanie klucza
Aplikacja TAPO deszyfruje klucz urządzenia w tej funkcji; zaimplementowałem ją (ponownie) w ten sposób:
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 blok kodu generuje Cipher z RSA/NONE/PKCS1Padding, a następnie manipuluje danymi i wykonuje kolejne 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 obsługujące 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ć żądania, które wysyłamy do urządzenia TAPO, oraz deszyfrować odpowiedzi!
Pierwsze securePassthrough
Drugie żądanie wysyłane do urządzenia to uniwersalne żądanie z metodą securePassthrough. Będzie ono później używane do każdego innego żądania, jak zmiana stanu wtyczki, pobieranie informacji itp.
Ciało żądania 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 oprócz params.request. Jak wiemy, jest ono zaszyfrowane za pomocą mo38009b_enc z obiektu C658a utworzonego krok wcześniej. Niezaszyfrowane params.request wygląda tak:
{
"method": "login_device",
"params": {
"password": "ITcyNjU....",
"username": "MzhhNTk2NT..."
},
"requestTimeMils": 0
}Widzimy więc, że drugie żądanie to próba autoryzacji za pomocą danych logowania TP-Link.
Implementacja aplikacji TAPO - kodowanie danych logowania
Jak widać, hasło i nazwa użytkownika nie są zapisane jako zwykły tekst. Są zakodowane, każde w inny sposób.
Hasło jest kodowane za pomocą B64, a nazwa użytkownika używa MessageDigest z 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 po tym nazwa użytkownika jest dodatkowo kodowana w B64.
Reponse
{
"error_code": 0,
"result": {
"response": "zQMfnu0DQcB9xaJ9srqWVqbxC/2vuKnDT4jyFVqKyCb4GBas06djUCchwdwbp8iFr9Z5gFtrMmy/SHVjKl3eruAqe+vzVtgQtWUjeVrhSyE="
}
}Tę odpowiedź można odszyfrować, a po deszyfrowaniu zobaczymy:
{
"error_code": 0,
"result": {
"token": "E0AA81A79277AA712...BF127322B523"
}
}I oto jest: kolejny token, który będzie potrzebny do następnych żądań. W tym momencie jesteśmy uwierzytelnieni i możemy wykonywać inne żądania.
Implementacja mojego celu
Po podsłuchaniu pierwszych dwóch żądań możemy podsłuchać ostatnie — żądanie stanu żarówki. Po odszyfrowaniu wygląda ono tak:
{
"method": "set_device_info",
"params": {
"device_on": false
},
"requestTimeMils": 1602840338865,
"terminalUUID": "88-54-DE-AD-52-E1"
}Większość pól mówi sama za siebie; terminalUUID to adres MAC wtyczki.
To params.request można wstawić do żądania z metodą securePassthrough.
Podsumowanie
Mając te informacje, można 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żde inne żądanie wykonywane przez aplikację, odszyfrować je i w końcu zaimplementować.
PoC napisane w Javie zamieszczę później.
PS
Aby odszyfrować nieznane żądania (bez reverse engineeringu), musisz zpatchować mo35029c, tak aby wstawiał Twój własny klucz prywatny i publiczny.