Evdeki robot süpürge HomeWhiz uygulamasıyla çalışıyor. Uygulama güzel, sorun şu: internet yoksa süpürge de yok. Daha kötüsü, “başlat” dediğimde komutun Çin’e ya da Frankfurt’a gidip geri dönmesini bekliyorum. Bu iki şey canımı sıktı ve 10 Ekim’de oturup cihazı yerelden konuşturmaya karar verdim.
Amacım basitti: terminalden vacuum start yazdığımda süpürge çalışsın. Bunun için üç şeyi çözmem gerekiyordu — cihazın ağda nerede olduğu, hangi protokolü konuştuğu ve o protokolün anahtarı.
Önce cihazı bulmak
Modem arayüzündeki cihaz listesinde isimsiz bir kayıt vardı. MAC’i Shenzhen iComm Semiconductor’a ait, yani Çinli bir IoT Wi-Fi yongası. IP’si 192.168.1.123. Port taraması yaptım: 65.535 portun 65.534’ü kapalı, tek açık port 6668. Bu Tuya’nın yerel protokol portu, neredeyse imza gibi.
TCP 6668 bağlantıyı kabul ediyordu ama düz handshake’e cevap vermiyordu. Bu, protokolün 3.4 veya 3.5 olduğu anlamına geliyor: şifreli el sıkışma, yani anahtar olmadan tek kelime alamıyorum.
Yanlış yolda iki saat
İlk refleksim trafiği araya girip okumak oldu. arpspoof ile modem ve süpürge arasına girdim, tcpdump ile 9 dakikada 20 bin paket topladım. Şeffaf çalıştı, cihaz koptuğunu fark etmedi.
Gördüğüm şey şuydu: süpürge sadece Tuya altyapısıyla konuşuyor. h2.iot-dns.com, a2.tuyaeu.com, MQTT için 8883 → AWS Frankfurt. 9 dakikada ~520 TLS bağlantısı, yani saniyede bir. Arçelik’e, Beko’ya ya da bir reklam servisine giden tek paket yok. “Nereye veri gönderiyor” sorusunun cevabı beklediğimden sıkıcı çıktı.
Ama içerik görünmüyordu. Kendi sertifikamı üretip cihazın 443 trafiğini yerel sunucuya yönlendirdim: 482 el sıkışma denemesinin hepsi TLSV1_ALERT_UNKNOWN_CA ile döndü. Cihaz sertifika zincirini kendi gömülü CA listesine karşı doğruluyor. MITM burada bitiyor.
İkinci deneme uygulama tarafıydı: mitmproxy, Android’e kullanıcı sertifikası, hepsi. APK’da CertificatePinner 35 yerde geçiyor. O da kapalı kapı.
Burada iki saat kaybettim. Sonradan düşününce ilk yapmam gereken şey buydu: pinning var mı diye APK’ya beş dakika bakmak.
Doğru yol: uygulamanın içinden bakmak
Ağdan okuyamıyorsam uygulamanın kendisinden okuyacağım. Root’lu bir Android emülatör kurdum, HomeWhiz’i (com.arcelik.tud4) yükledim, kendi hesabımla giriş yaptım ve Frida 16 ile Java köprüsünü taktım.
İlk tuzak: frida-server’ı attach modunda çalıştırmak işe yaramıyor, spawn şart. Uygulamayı Frida başlatacak, hook’lar sonra kurulacak.
Şifreli HTTP gövdesini okumaya çalışmak yine boşunaydı. Heap’te canlı nesne aramak işe yaradı:
Java.choose('com.thingclips.smart.sdk.bean.DeviceBean', {
onMatch: function (o) { console.log(o.getLocalKey()); },
onComplete: function () {}
});
localKey geldi. Cihazın kimliği, ürün kimliği ve protokol sürümü de yanında: 3.3.
Bir yan not, ileride başkasının başına gelirse diye: uygulamanın ana ekranında ev listesi boş görünüyordu. “Tüm Cihazlar” sekmesine basınca hesap Beylikdüzü’ndeki eve geçti ve cihaz listesi yüklendi. Yani uygulama açılışta yanlış evi gösteriyordu. Bunu fark etmesem “cihaz yok” diye bırakacaktım.
İlk yerel okuma
tinytuya ile aynı ağdan bağlandım:
d = tinytuya.Device(dev_id, "192.168.1.123", local_key, version=3.3)
print(d.status())
Cevap geldi. Pil %100, emiş “max”, durum “charge_done”, arıza kodu 0, toplam 441 temizlik, 6.319 m². Bulut yok, sadece TCP 6668.
Sonra ürün şemasını da çektim: 39 data point. Hangisi yazılabilir, hangisi salt okunur, hangi değerleri kabul ediyor — hepsi tek JSON’da. Fırça ömrü sayaçları, batarya sağlığı, ses seviyesi, cihazın konuştuğu dil (Türkçe var), rahatsız etme modu.
Etrafına küçük bir CLI yazdım: status, start, pause, dock, suction, water, raw. Artık süpürgeyi telefondan değil terminalden yönetiyorum.
Asıl iş DP 15’te
Basit komutlar data point’lerde düz değer olarak duruyor. DP 1 temizliği başlatıyor, DP 9 emiş gücünü alıyor, DP 3 şarja gönderiyor. Oda seçerek temizlik istediğinde iş değişiyor: komut, DP 15’e base64 ile gömülmüş ham bir çerçeve olarak gidiyor.
Çerçevenin biçimini çözdüm:
aa | ver | uzunluk | cmd | veri | checksum
ver=00 tek bayt uzunluk, ver=01 dört bayt. Checksum, komut ve veri baytlarının toplamının 0xFF ile maskelenmesi. Cihazın gönderdiği aa 00 03 15 01 00 16 bunu birebir doğruluyor.
Komut haritasını da buldum: oda seçimi 0x14, nokta temizlik 0x16, bölge 0x28, sanal duvar 0x12. Nereden? Tuya’nın kendi npm paketinden — @ray-js/robot-protocol. Paketin sourcemap’i tüm orijinal TypeScript kaynağını içeriyor. 46 dosya, hepsi okunur halde. Firmware’i tersine çevirmek yerine üreticinin kendi kaynağını okumak.
Oda komutunu denedim: DP4 = selectroom, DP15 = base64(aa 00 04 14 01 01 01 17), DP1 = true. Cihaz DP5 = "select_room" döndü. Format doğru, komut kabul edildi.
Kaldığım duvar
Oda ID’lerinin hangi odaya denk geldiğini bilmiyorum, çünkü harita ve oda listesi yerelden gelmiyor. Tuya haritayı P2P kanalından veriyor; resmî komut setinde “harita indir” diye bir komut yok. DP16 = get_map denedim, cihaz boş liste döndürdü.
Yani süpürgeyi başlatabiliyorum, emişini ayarlayabiliyorum, şarja gönderebiliyorum — ama “sadece mutfağı süpür” diyemiyorum. O tek cümle için hâlâ bulut gerekiyor. Sekiz saatlik uğraşın dürüst özeti bu.
Ne öğrendim
Sırayı yanlış kurdum. Önce en ucuz testi yapmak gerekirdi: portu tara, iki paket gönder, cevap veriyor mu bak. Bunu on dakikada yapabilirdim. Onun yerine en pahalı saatleri MITM’e ve sertifika denemelerine harcadım.
İkincisi, kapalı kapıyı zorlamak yerine yan kapıya bakmak. Sertifika doğrulaması beni durdurduğunda, uygulamanın elinde anahtar olduğunu fark etmem bir gün sürdü. Frida ile on dakikada çıktı. Ağ tarafında çözemediğim şeyi cihazın kendi yazılımının içinde çözdüm.
Üçüncüsü, cihazı kızdırmamak. Sertifika testinden sonra süpürge dört dakika boyunca buluta bağlanmadı, power-cycle gerekti. Bir IoT cihazıyla uğraşırken kendine “bu aleti yarım saat kullanılamaz bırakırsam sorun olur mu” diye sormalısın.
Ve bir de şu: localKey’i hiçbir yere yazmadım. ~/.config/tuya/ altında, 600 izinli bir dosyada duruyor. O anahtar cihazın kapısı; vault’a, sohbete, commit’e girmemeli.
Şimdi sırada oda ID’lerini bulmak var. Bulursam vacuum mutfak yazacağım. Bulamazsam da süpürgeyi bulutsuz başlatabiliyor olmak yeterince iyi.