Spread the love
Sorumluluk Reddi

Hacking korkutucu bir terim olabilir, bu yüzden niyetimin yalnızca satın aldığım akıllı cihazı, akıllı ev sistemimle entegre etmek için yükseltmek olduğunu açıkça belirtmek istiyorum. Bunu yapmak, bu ürünün diğer örneklerini veya bulut hizmetlerini etkilemez. Bu nedenle, özel anahtarlar, alan adları veya API uç noktaları gibi ürünle ilgili hassas veriler bu gönde

riden çıkarılmış veya sansürlenmiştir. Cihazlarınızla uğraşmak muhtemelen garantinizi geçersiz kılacak, hasar görme riski taşıyacaktır; bu işlemleri kendi sorumluluğunuzda yapın.

Cihaz zaten kendi mobil uygulamasıyla uzaktan kontrol desteğine sahip, ancak bu uygulamayı kullanmak için can sıkıcı bir şekilde bir bulut hesabı gerekiyor. Telefonumun Bluetooth, WiFi ve 5G’sini açıp kapatarak uygulamanın cihazı kontrol etmek için internet bağlantısına ihtiyaç duyduğunu doğruladım. Bluetooth veya WiFi üzerinden yerel olarak uzaktan kontrol mümkün değil.

Bu, mobil uygulamanın ve cihazın uzaktan kontrolün mümkün olabilmesi için bir bulut sunucusuna bağlı olması gerektiği anlamına geliyor. Yani, bu ağda bir yerlerde, cihaz ile bulut sunucusu arasındaki veri akışı, fan hızını ve uygulamanın kontrol ettiği diğer her şeyi içermeli.

Yani, kilit nokta:

Cihazın ağ trafiğini kesip bu değerleri değiştirebilirsek, cihazın kontrolünü ele geçirebiliriz.
Tüm sunucu yanıtlarını taklit edebilirsek, internet bağlantısına ve bulut sunucusuna bağımlı olmadan cihazın kontrolünü ele geçirebiliriz.

Android uygulamaları genellikle classes.dex olarak adlandırılan derlenmiş Java çalıştırılabilir dosyalarını içerir. Bunları dex2jar ile bir .jar dosyasına dönüştürebilir ve jd-gui kullanarak içeriği yeniden oluşturulmuş kaynak kodu olarak gözden geçirebilirsiniz.

App MainActivity.class’ı bulduğumda, uygulamanın React Native ile oluşturulduğunu keşfettim!

package com.smartdeviceapp;

import com.facebook.react.ReactActivity;

public class MainActivity extends ReactActivity {
  protected String getMainComponentName() {
    return "SmartDeviceApp";
  }
}

React Native ile oluşturulmuş Android uygulamaları için JavaScript paketini assets/index.android.bundle içinde bulabilirsiniz.

Uygulamanın paketini hızlıca taradığımda, güvenli bir WebSocket bağlantısı kullandığını ortaya çıkardım:

self.ws = new WebSocket(“wss://smartdeviceapi.—.com”);
Bu Android uygulamasında çok fazla ilgi çekici bir şey yok; beklendiği gibi, akıllı cihazı uzaktan kontrol edebilmek için sunucusuna bağlanıyor. Okunabilir kaynak kodu elde etmenin basitliği nedeniyle hızlıca göz atmaya değer. Her zaman bu paketi referans alarak paylaşılan değerler veya mantık olup olmadığını kontrol edebiliriz.

Şimdi, akıllı cihazın sunucusuyla iletişim kurmak için UDP kullandığını biliyoruz. Ancak şu anda, cihaz benim local ile iletişim kurmaya çalışıyor ve onun sunucusu gibi yanıt vermesini bekliyor.

Local ile akıllı cihaz ve sunucusu arasında bir aktarım noktası olarak işlev görecek basit bir UDP proxy kullanabiliriz.

Gerçek IP adresini öğrenmek için
Cloudflare’ın DNS çözümleyicisini
(1.1.1.1) kullandım (çünkü Pi-hole DNS’im bu adresi sadece yerel IP adresime çözecekti). Ardından, trafiği sunucusuna yönlendirmek için basit bir yöntem olarak
node-udp-forwarder
kullandım:
udpforwarder \
–destinationPort 41014 –destinationAddress X.X.X.X \
–protocol udp4 –port 41014

X.X.X.X sunucunun gerçek IP adresidir.

Wireshark’a tekrar baktığımda, akıllı cihaz ile sunucusu arasındaki tüm ağ trafiğini görebiliyoruz!

Cihaz açıldığında, sunucuya şu türde veri içeren bir paket gönderir:

00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

00000000 55 00 31 02 01 23 45 67 89 AB CD EF FF 00 01 EF U.1..#Eg……..
00000010 1E 9C 2C C2 BE FD 0C 33 20 A5 8E D6 EF 4E D9 E3 ..,….3 ….N..
00000020 6B 95 00 8D 1D 11 92 E2 81 CA 4C BD 46 C9 CD 09 k………L.F…
00000030 0E .

Sunucu, ardından şu şekilde yanıt verir:

00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

00000000 55 00 2F 82 01 23 45 67 89 AB CD EF FF 37 34 9A U./..#Eg…..74.
00000010 7E E6 59 7C 5D 0D AF 71 A0 5F FA 88 13 B0 BE 8D ~.Y|]..q._……
00000020 ED A0 AB FA 47 ED 99 9A 06 B9 80 96 95 C0 96 ….G……….

Bundan sonraki tüm paketler benzer bir yapıya sahipti. Okunabilir herhangi bir metin içermiyorlardı, ancak rastgele baytlar gibi görünen verilerle doluydular; bu, şifrelemeye işaret eden
Avalanche etkisi
olabilir.

Bu paket yapısının mevcut bir protokol olup olmadığını araştırdım. Bazı akıllı cihazların UDP tabanlı olan DTLS kullandığını okudum.

Ancak, Wireshark DTLS paketlerini tespit edebiliyor, ancak bu paketi UDP olarak listeliyordu, bu da verilerden bir UDP tabanlı protokol belirleyemediği anlamına geliyor. DTLS spesifikasyonunu çift kontrol ettim, ancak bu, pakette gördüğümüzden farklı bir başlık formatı tanımlıyordu, bu yüzden burada DTLS kullanılmadığını biliyoruz.

Bu noktada, bir engelle karşılaştık; bu paketlerdeki verilerin nasıl biçimlendirildiğini anlamıyoruz, bu da henüz herhangi bir şeyi manipüle edemeyeceğimiz veya taklit edemeyeceğimiz anlamına geliyor.

Eğer iyi belgelenmiş bir protokol kullansaydı, bu çok daha kolay olurdu, ancak bu işin eğlencesi nerede kalırdı?

Bu paket verisinin nasıl okunacağını anlayan 2 yolu var : akıllı cihaz ve sunucusu. Sunuculara fiziksel olarak ulaşamayacağımız için, bu yüzden akıllı cihazın içine bakma zamanı geldi!

Sökmek oldukça kolaydı. İçinde mikrodenetleyiciyi içeren ana PCB, fana bağlanan bir bağlantı noktası ve ön taraftaki kontrol paneline giden bir şerit kablo vardı.

Ana kontrol mekanizması ESP32-WROOM-32D olarak karşımıza çıkıyor.
Github’da ESP32 Tersine Mühendislik için bir kaynak dosyası

ESP32 ‘ nin üreticisi bir yardımcı program yayınlamıştı.
ESPTOOL

Seri bağlantıyı kurduktan sonra bu araç ile çipten veri okumak mümkün.

Referans olarak ESP32’den aldığım pin yerleşim şemasını kullanıyorum.

Burada TXD0(35) ve RXD0(34) pinlerini görebiliriz. Seri bağlantı için bu pinlerin her ikisine de bir kablo ve bir toprak pini bağlamamız gerekiyor.

Cihaz PCB’sinde, hata ayıklama ve yanıp sönme için genellikle pinlere bağlanan birkaç pin deliği vardı; Bu seri pinlerin her ikisinden de deliklere kadar olan izleri görsel olarak takip edebildim! Bu sayede geçici olarak jumper kablolarını takabileceğim kesme başlıklarını kolayca lehimleyebildim.Aksi takdirde, muhtemelen doğrudan çip pinlerine dikkatlice lehimlemem gerekirdi.Süreklilik moduna ayarlanmış bir multimetre ile ESP32’deki GND(38) pinini referans alarak hangi deliğin toprak olduğunu bulabildim.

Şimdi, bu UART seri iletişimini idare edecek bir porta ihtiyacımız var. Ben
Flipper Zero
GPIO kategorisi altında kullanışlı bir USB-UART Köprüsü uygulaması var.

3 adet jumper kablosu kullanarak bunları birbirine bağladım:

Flipper Zero TX <–> RX ESP32
Flipper Zero RX <–> TX ESP32
Flipper Zero GND <–> GND ESP32

TX ve RX kabloları burada kasıtlı olarak çaprazlanmıştır; diğer cihazın alıcı hattına veri iletmek istiyoruz!

Bu noktaya kadar, fanı ve kontrol panelini bağlı tutmak için PCB’yi garip bir şekilde konumlandırdım. Bu yüzden, onlar takılı değilken de çalışıp çalışmayacağını görmek istedim. Ne yazık ki çalışmadı; seri aşağıdaki kaydı yaptı:

I2C read reg fail1
No Cap device found!
REGuru Meditation Error: Core  0 panic'ed (IllegalInstruction). Exception was unhandled.
Memory dump at 0x400da020

Şimdi Ghidra’yı güzelce yapılandırdık, günlükte belirtilen adrese bir göz attım; No Cap device found! dizesi için bir referansın hemen yanına monte edilmişti ve işlevin başında “CapSense Init\r” yazıyordu. Bu, kapasite algılama girişi kullanan kontrol paneli için olmalı!
Ghidra’da bu fonksiyonu InitCapSense olarak adlandırdım:

void InitCapSense()
{                       
  FUN_401483e0("CapSense Init\r");
  // ... CapSense logic
} Daha sonra bu işleve yapılan referansları takip ederek görev/hizmet olarak başlatılıyor gibi görünen başka bir işleve geri döndüm; bu işlevin adını StartCapSenseService olarak değiştirdim:
void StartCapSenseService()
{
  _DAT_3ffb2e2c = FUN_40088410(1, 0, 3);
  FUN_4008905c(InitCapSense, &DAT_3f40243c, 0x800, 0, 10, 0, 0x7fffffff);
  return;
}


Yine, fonksiyon referanslarını takip ettim ve StartCapSenseService'i çağıran fonksiyonu buldum. Ghidra'nın Yama Komutu özelliğini kullanarak, fonksiyon çağrısını kaldırmak için çağrı komutunu bir nop (işlem yok) komutu ile değiştirdim:

// Original
400d9a28  25 63 af    call8     FUN_4008905c

400d9a2b  65 31 00    call8     StartCapSenseService

400d9a2e  e5 37 00    call8     FUN_400d9dac


// Patched
400d9a28  25 63 af    call8     FUN_4008905c

400d9a2b  f0 20 00    nop

400d9a2e  e5 37 00    call8     FUN_400d9dac

Bu değişikliği ESP32’ye flaşla aktarmak istiyoruz, bu yüzden değiştirilen baytları bu ELF dosyasında değil, part.3.factory ikili bölüm görüntüsünde değiştirdim, çünkü bu doğrudan flaştan ham bir formatta, bu yüzden geri yazmak kolay olacak. Baytları bulmak ve değiştirmek için bir hex editörü kullanıyorum:

2564af 653100 e53700 -> 2563af f02000 e53700

Daha sonra, bu değiştirilmiş görüntüyü ESP32 flaşına 0x10000 ofsetinde yazdım, bu fabrika bölümü için bölüm tablosundaki ofsettir:

esptool -p COM7 -b 115200 write_flash 0x10000 ./patched.part.3.factory

Ancak bunu önyüklemeye çalışırken, seri çıktıdan bir hata alıyoruz:

E (983) esp_image: Checksum failed. Calculated 0xc7 read 0x43
E (987) boot: Factory app partition is not bootable

Pekala, bir sağlama toplamı var. Neyse ki, esptool içindeki kod bunu nasıl hesaplayacağını biliyor, bu yüzden bir uygulama bölümü görüntüsü için sağlama toplamlarını düzeltmek için hızlı bir küçük betik hazırladım

Şimdi, bunu sağlama toplamlarını onarmak ve onarılmış görüntüyü flaş etmek için kullanabiliriz:

python esp32fix.py --chip=esp32 app_image ./patched.part.3.factory

esptool -p COM7 -b 115200 write_flash 0x10000 ./patched.part.3.factory.fixed

Cihazı kontrol paneli olmadan yeniden başlatmayı denedim; şimdi her şey yolunda! Akıllı cihazın aygıt yazılımını başarıyla değiştirdik!

Paketlere odaklanmaya geri dönelim. Paketlerin iyi bilinen bir protokolü takip etmediğini biliyoruz, yani yapıyı kendimiz çözmeliyiz.

Cihazın açılışındaki paketleri birçok kez yakaladım ve birbirleriyle karşılaştırdım. İlk on üç baytın diğer paketlere benzediğini, paketin geri kalanının ise şifrelenmiş olduğunu fark ettim.

İşte önyüklemeler arasında sunucudan alınan ilk paket; 0x0D ofsetine kadar verilerin eşleştiğini görebilirsiniz:

Hex View  00 01 02 03 04 05 06 07  08 09 0A 0B 0C 0D 0E 0F
 
00000000  55 00 2F 82 01 23 45 67  89 AB CD EF FF 37 34 9A  U./..#Eg.....74.
00000010  7E E6 59 7C 5D 0D AF 71  A0 5F FA 88 13 B0 BE 8D  ~.Y|]..q._......
00000020  ED A0 AB FA 47 ED 99 9A  06 B9 80 96 95 C0 96     ....G..........

Hex View  00 01 02 03 04 05 06 07  08 09 0A 0B 0C 0D 0E 0F
 
00000000  55 00 2F 82 01 23 45 67  89 AB CD EF FF 81 85 3F  U./..#Eg.......?
00000010  8A 10 F5 02 A5 F0 BD 28  73 C2 8C 05 71 6E E4 A3  .......(s...qn..
00000020  A6 36 FD 5C E0 D5 AC 3E  1A D5 C5 88 99 86 28     .6.\...>......(

İlk birkaç değeri bulmak çok zor olmadı, sonra kalan dokuz baytın cihazın seri çıkışındaki seri numarasıyla eşleştiğini fark ettim ve işte paket başlığı formatına sahibiz:

55 // protokolü tanımlayan byte
00 31 // paketin bayt cinsinden uzunluğu
02 // mesaj tanımlayıcı
01 23 45 67 89 AB CD EF FF // cihaz seriali

//55 Paketi genellikle belirli bir formattaki bir veri parçasını benzersiz bir şekilde tanımlamak için kullanılır.

İlk gönderilen ve alınan paketler daha sonra gelenlerden biraz farklı bir formata sahipti; istemci paketinde başlıktan sonra her zaman 00 01 baytları vardı ve 0x02 mesaj kimliğine sahip tek paket buydu.

Diğer paketlerle karşılaştırdığımda, mesaj kimliği ile ilgili bir model fark ettim:

0x02 – Akıllı cihazdan gönderilen ilk paket
0x82 – Cihazın sunucusundan alınan ilk paket
0x01 – Akıllı cihazdan gönderilen diğer tüm paketler
0x81 – Cihazın sunucusundan alınan diğer tüm paketler
Bu değerdeki yüksek bitlerin, bunun bir istemci isteği (0x00) veya sunucu yanıtı (0x80) olup olmadığını temsil ettiğini görebilirsiniz. Ve alt bitler ilk değişim (0x02) ve diğer tüm paketler (0x01) arasında farklıdır.

Daha önce uygulamada “Mesaj CRC error\r” yazan bir dize fark ettik, bu da pakette bir CRC sağlama toplamı olduğunu ima ediyordu. Herhangi bir şifre çözme girişimini engellememesi için verilerde bir sağlama toplamı olup olmadığını bilmek yararlı olacaktır.

Bu dizeye yapılan referansları takip ettim ve tek bir fonksiyon buna referans veriyor.

Şimdi bu fonksiyon için derlenmiş koda bir göz atalım:

// …
iVar1 = FUN_4014b384(0, (char *)(uint)_DAT_3ffb2e40 + 0x3ffb2e42);
iVar2 = FUN_400ddfc0(&DAT_3ffb2e44, _DAT_3ffb2e40 – 2);
eğer (iVar1 == iVar2) {
if (DAT_3ffb2e47 == ‘\x01’) {
FUN_400db5c4(0x3ffb2e48, _DAT_3ffb2e40 – 6);
}
else if (DAT_3ffb2e47 == ‘\x02’) {
FUN_401483e0(s_Connection_message_3f4030e4);
}
pcVar3 = (char *)0x0;
_DAT_3ffb3644 = (char *)0x0;
}
else {
FUN_401483e0(s_Message_CRC_error_3f4030d0);
pcVar3 = (char *)0x0;
_DAT_3ffb3644 = (char *)0x0;
}
// …
else bloğunda s_Message_CRC_error etiketinin kullanıldığını görebiliriz, bu nedenle if deyimi bir mesaj için CRC verilerini doğrulamalıdır.

Bu mantık, FUN_4014b384 ve FUN_400ddfc0 adlı 2 fonksiyonun sonuçlarını karşılaştırır. Eğer bu bir paketin sağlama toplamını doğruluyorsa, biri paket verisi için bir sağlama toplamı oluşturmalı ve diğeri de sağlama toplamı değerini paketten okumalıdır.

Hangisinin hangisi olduğuna karar vermemize yardımcı olması için argümanları kullanabiliriz, ancak her ikisine de bir göz atalım:

uint FUN_4014b384(int param_1, byte *param_2)
{
  uint uVar1;
  
  if (param_1 == 0) {
    uVar1 = (uint)*param_2 * 0x100 + (uint)param_2[1];
  }
  else {
    uVar1 = (uint)*param_2 + (uint)param_2[1] * 0x100;
  }
  return uVar1 & 0xffff;
} Bu bir CRC fonksiyonuna benzemiyor.Aslında yapılandırılabilir endianness ile 16 bitlik bir uint okuyan bir fonksiyona benziyor; işte nedeni:Bir değeri 0x100 (256) ile çarpmak, 8 bit (16 bitlik bir değerin yarısı) sola kaydırmaya eşdeğerdir, bu nedenle 0x37, 0x3700 olur. İlk if kod bloğundaki mantık bunu index[1]'deki bayta ekler; bu bellekte ondan sonraki bayttır, bu nedenle temelde param_2 işaretçisinden big-endian bir uint16 okumaktır Diğer kod bloğunun mantığı benzerdir, ancak ilk bayt yerine ikinci baytı kaydırır, böylece bir little-endian uint16 okur. Dolayısıyla, param_1 parametresi sonucun endianlığını yapılandırır.return deyimi, 0xFFFF ile dönüş değeri üzerinde bir bitsel AND (&) operatörü yapar, bu da daha yüksek bitleri sıfırlayarak değeri 16 bitlik veriyle sınırlar.
uint FUN_400ddfc0(byte *param_1, uint param_2)
{
  uint uVar1;
  uint uVar2;
  byte *pbVar3;
  
  pbVar3 = param_1 + (param_2 & 0xffff);
  uVar1 = 0xffff;
  for (; pbVar3 != param_1; param_1 = param_1 + 1) {
    uVar1 = (uint)*param_1 << 8 ^ uVar1;
    uVar2 = uVar1 << 1;
    if ((short)uVar1 < 0) {
      uVar2 = uVar2 ^ 0x1021;
    }
    uVar1 = uVar2 & 0xffff;
  }
  return uVar1;
}

Şimdi, bu daha çok bir sağlama toplamı fonksiyonuna benziyor; içinde bir sürü bitsel operatör bulunan bir for döngüsü var.

Yakalanan paketlerden birini açıp
ImHex
Tersine mühendislik için bir hex editörü. Bu, seçili verinin sağlama toplamını göstermek için kullanışlı bir özelliğe sahiptir.

Diğer fonksiyon 16 bitlik bir uint okuduğu için CRC-16’yı seçtim ve 16 bitlik hash’in olabileceğini düşündüğüm 2 baytı seçmeden bırakarak muhtemelen hashlenecek bayt bölgelerini seçmeye başladım.

Şimdiye kadar şansım yaver gitmedi, ama sonra ImHex’te CRC-16 parametrelerini yapılandırabileceğinizi fark ettim. Bu yüzden, ucuz bir kısayol denedim ve ImHex’i, derlenmiş fonksiyonda bulunan değerleri kullanarak bir dizi farklı parametre kombinasyonuyla CRC-16 sağlama toplamlarını hesaplayacak şekilde ayarladım.

Başarılı oldu! Paketin son 2 baytının, paketteki diğer tüm verilerin CRC sağlama toplamı olduğu ortaya çıktı, özellikle 0x1021 polinomu ve 0xFFFF başlangıç değeri ile CRC-16. Bunu diğer paketlerle kontrol ettim ve hepsi sağlama toplamını geçti.

Artık her paketin son 2 baytının bir CRC-16 sağlama toplamı olduğunu biliyoruz ve bunu herhangi bir şifre çözme girişiminden hariç tutabiliriz!

Daha önce, mbedtls ilkellerinin ECDH ve HKDF olarak etiketlendiğini fark etmiştik. Peki, bunlar tam olarak nedir?

ECDH (Eliptik Eğri Diffie-Hellman Anahtar Değişimi), her biri eliptik eğri genel-özel anahtar çiftine sahip 2 tarafın (akıllı cihaz ve bulut sunucusu gibi) güvensiz bir kanal (UDP) üzerinden paylaşılan bir sır oluşturmasına olanak tanıyan bir anahtar anlaşma protokolüdür. Bunun daha detaylı bir açıklamasını “Practical Cryptography for Developers” kitabında buldum:
ECDH Anahtar Değişimi

Esasen, akıllı cihaz ve sunucu bir EC anahtar çifti oluşturur ve açık anahtarlarını değiş tokuş ederlerse, paylaşılan bir gizli anahtar hesaplamak için diğerinin açık anahtarını kendi özel anahtarlarıyla birlikte kullanabilirler. Bu paylaşılan gizli anahtar paketleri şifrelemek ve şifrelerini çözmek için kullanılabilir! Ve güvensiz ağ üzerinden açık anahtarları değiş tokuş etseler bile, paylaşılan anahtarı hesaplamak için hala özel anahtarlardan birine ihtiyacınız vardır.

Bu, bu gibi paketlerin güvenliğini sağlamak için idealdir ve istemci tarafından gönderilen ilk paket aslında günlüklerde ECC connet paketi olarak adlandırılır:

UDP Connect: smartdeviceep.---.com
smartdeviceep.---.com = 192.168.0.10
UDP Socket created
UDP RX Thread Start
Write ECC conn packet

Bu büyük bir ilerlemedir; ilk paket alışverişinin muhtemelen diğer tüm paketleri şifrelemek için bir ECDH anahtar anlaşması oluşturmak üzere EC açık anahtarlarını değiştirdiğini biliyoruz.

Paket başlığını (başlangıçtan itibaren 13 bayt) ve sağlama toplamını (sonda 2 bayt) göz ardı edersek, bu potansiyel anahtar değişimi için paketlerin içeriğinin her ikisinin de 32 bayt (256 bit) olduğunu görebiliriz, bu da bir açık anahtar için geçerli bir boyut olacaktır. İstemcinin isteğinin başında 00 01 olmasına rağmen, botlar arasında değer değiştirmediği için bunun önemsiz bir veri tanımlayıcısı olduğunu varsayabiliriz:

// Client request packet contents:

Hex View  00 01 02 03 04 05 06 07  08 09 0A 0B 0C 0D 0E 0F

00000000  00 01 D1 C2 B3 41 70 17  75 12 F7 69 25 17 50 4A  .....Ap.u..i%.PJ
00000010  C5 DD D4 98 06 FE 24 6B  96 FD 56 14 4A 70 7E 51  ......$k..V.Jp~Q
00000020  55 57                                            UW

// Server response packet contents:

Hex View  00 01 02 03 04 05 06 07  08 09 0A 0B 0C 0D 0E 0F
 
00000000  07 A8 02 73 52 42 1F 1F  C1 41 B4 E4 5B D9 A9 9A  ...sRB...A..[...
00000010  5A DD 0F 94 F1 AB 9E E8  86 C7 99 7E 08 68 52 C5  Z..........~.hR.

Peki, HKDF nedir? Bu HMAC tabanlı anahtar türetmedir. Diffie-Hellman’dan hesaplanan paylaşılan sırları şifrelemede kullanılmaya uygun anahtar malzemesine dönüştürmek için kullanılabilir. Vay canına, bu çok mantıklı; büyük olasılıkla diğer paketleri şifrelemek ve şifresini çözmek için bir anahtar türetmek için tam olarak bunu yapıyor.

Bu paketlerin şifresini çözebilmek için şifreleme anahtarının tam olarak nasıl oluşturulduğunu anlamamız gerekir. Bu, olası giriş verilerinin yanı sıra yapılandırılabilir seçenekleri de içerir.

Paket verileri için ECDH ve HKDF işlevlerinin kullanıldığını varsaymak güvenlidir, bu nedenle anahtar oluşturma sürecine odaklanarak anlamamız gereken değişkenleri özetliyorum:

ECDH:

  • Public key
  • Private key

HKDF

  • Hashing method
  • Output key size
  • Optional salt
  • Optional info

Akıllı cihaz ve sunucusu, anahtar değişim işlemi olduğunu varsaydığımız işlem sırasında 256 bit veri alışverişinde bulunur. Ancak akıllı cihazın aygıt yazılımının aşağıdaki anahtarları da depodan yüklediğini unutmayın:

  • 256-bit device key pair (private & public)
  • 256-bit cloud server "root" public key
  • 256-bit cloud server "signer" public key

Burada pek çok olasılık var, bu yüzden Ghidra’daki uygulamaya bir kez daha baktım. Hata dizelerini takip ederek, bu anahtarı üreten fonksiyonu buldum! Düzeneği mbedtls kaynak koduyla karşılaştırarak fonksiyonları ve değişkenleri etiketleyerek yoluma devam ediyorum. Aşağıdaki sözde koda açıklama ekleyebildim ve basitleştirebildim

int GenerateNetworkKey(uchar *outputKey, uchar *outputRandomBytes)
{
  // Generate an ECDH key pair
  char privateKey1 [12];
  char publicKey1 [36];
  mbedtls_ecdh_gen_public(
    ecpGroup, 
    privateKey1, 
    publicKey1, 
    (char *)mbedtls_ctr_drbg_random, 
    drbgContext
  );

  // Overwrite generated private key?
  mbedtls_mpi_read_binary(privateKey1, (uchar *)(_DAT_3ffb3948 + 0x7c), 1);

  // Overwrite generated public key?
  mbedtls_ecp_copy(publicKey1, (char *)(_DAT_3ffb3948 + 0x88));

  // Load another public key?
  char publicKey2 [36];
  mbedtls_ecp_copy(publicKey2, (char *)(_DAT_3ffb38cc + 0x88));
  
  // Compute shared secret key using privateKey1 and publicKey 2
  char computedSharedSecret [100];
  uchar binarySharedSecret [35];
  mbedtls_ecdh_compute_shared(
    ecpGroup,
    computedSharedSecret,
    publicKey2,
    privateKey1,
    (char *)mbedtls_ctr_drbg_random,
    drbgContext
  );
  mbedtls_mpi_write_binary(computedSharedSecret, binarySharedSecret, 0x20);

  // Generate random bytes
  mbedtls_ctr_drbg_random(globalDrbgContext, outputRandomBytes, 0x20);

  // Derive key
  mbedtls_md_info_t *md = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
  uchar* deviceSerialNumber = (uchar *)GetDeviceSerialNumber();
  mbedtls_hkdf(
    md, 
    binarySharedSecret, // salt
    0x20,
    outputRandomBytes, // input
    0x20,
    deviceSerialNumber, // info
    9,
    outputKey,
    0x10
  );
}

Assembly’yi ve hatta Ghidra’daki derlenmiş kodu yorumlayabilmek kesinlikle edinilmiş bir beceridir; bunu anlamanın biraz zaman aldığını ve arada birçok mola verdiğimi vurgulamak isterim!
Bu fonksiyon alışılmadık bir şey yapıyor; işte bundan öğrenebileceklerimiz:
Üretilen ECDH anahtar çifti atılır ve yerine bellekte başka bir yerden yüklenen anahtarlar yerleştirilir, ki bu gariptir. ECDH anahtar çifti oluşturma işlevi uygulamanın başka bir yerinde kullanılmadığından, bu anahtarların daha önce gördüğümüz ürün yazılımı deposundaki dosyalar olması muhtemeldir.
HKDF için kullanılan algoritma SHA-256’dır.
Hesaplanan paylaşılan gizli bilgi HKDF salt olarak kullanılır.
HKDF girdisi olarak rastgele baytlar oluşturulur.
Cihaz seri numarası HKDF bilgisi olarak kullanılır.
HKDF çıktı anahtar boyutu 0x10’dur (16 bayt / 128 bit).

Artık akıllı cihazın potansiyel şifreleme anahtarını nasıl ürettiğini çok daha iyi anlıyoruz.

Bulut sunucusunun da bu anahtarı üretmesi gerektiğini, yani HKDF için aynı girdi değişkenlerine sahip olması gerektiğini akılda tutmakta fayda var.

Bunu bilerek, HKDF işlevinin üç dinamik girdisini özetleyebilir ve sunucunun da bunlara nasıl sahip olacağını anlayabiliriz:

salt – Paylaşılan sır: Sunucu, ECDH paylaşılan sır hesaplaması için kullanılan aynı özel ve genel anahtarlara erişebilmeli ya da özelimiz için genel ve genelimiz için özel anahtarları kullanabilmelidir.
input – Rastgele baytlar: Sunucunun akıllı cihazda rastgele üretilen bu baytlara erişimi olmalıdır; ya bu baytları sunucuya göndeririz ya da teknik olarak sunucu kullanılan pseudo RNG yöntemini yeniden oluşturabilir. Ancak, oluşturulan baytlar 0x20 (32 bayt / 256 bit) boyutuna sahiptir, bu da anahtar değişim paketinde gönderilen verilerin boyutuyla tam olarak eşleşir, bu nedenle büyük olasılıkla oraya gönderiyoruz!
info – Cihaz seri numarası: Cihaz seri numarasının paket başlığının bir parçası olduğunu zaten biliyoruz, bu nedenle sunucu bu değere kolayca erişebilir.
Uygulamanın rastgele oluşturulan bu baytlarla ne yaptığını merak ederek, çağıran fonksiyonun bunlarla ne yaptığını kontrol ettim:

stack[0] = 0x00;
stack[1] = 0x01;
GenerateNetworkKey(&KeyOutput, stack[2]);
log(2, 2, "Write ECC conn packet\r\n");
SendPacket((int)param_1, 2, stack[0], 0x22);

GenerateNetworkKey’den gelen rastgele baytların yığına yazıldığını ve daha da iyisi, 00 01 baytlarının hemen öncesinde yığına yazıldığını ve ardından tüm 0x22 baytlarının pakette gönderildiğini görebiliriz. Bu, anahtar değişim paketinde gördüğümüz formatla tam olarak eşleşiyor

Statik analiz yoluyla çok fazla ilerleme kaydedildi ve şifre çözme anahtarını hesaplamak için ihtiyacımız olan son değer paylaşılan sırdır.

Tersine mühendisliğin bu noktasında, fonksiyonları bu blog yazısında gösterildiği kadar temiz bir şekilde tersine çevirmemiştim ve anahtarları doğrudan cihazdan dinamik olarak elde etmeyi denemek istedim.

JTAG üzerinden hata ayıklama burada mantıklı bir seçim olacaktır. Ancak, PCB’de bu pinler için kesme noktaları fark etmedim ve doğrudan ESP32 pinlerine lehimlemekten kaçınmak istedim, bu yüzden seri üzerinden yazdırmak için ürün yazılımını yamalamak için kendime meydan okuyacağımı düşündüm!

CapSense hizmeti hala devre dışı, bu yüzden paylaşılan gizli anahtarı yazdırmak ve hesaplandıktan hemen sonra çağırmak için bu mantık üzerine bir işlev yazabileceğimi düşündüm!

Yani, sözde kodda planlama yaparak, fonksiyon çağrımı GenerateNetworkKey fonksiyonuna eklemek istiyorum. Anahtarı oluşturduktan hemen sonra.:

int GenerateNetworkKey(uchar *outputKey, uchar *outputRandomBytes)
{
  // ... 
  
  // Add my function call:
  print_key(binarySharedSecret);
}

// Custom function saved over unused logic:
void print_key(char *key)
{
  for (int i = 0; i < 32; i++) {
    log("%2.2x", key[i]);
  }
}
// Original
400dbf2d  25 4b 6c    call8     GetDeviceSerialNumber

// Patched
400dbf2d  e5 ff fd    call8     print_key

// print_key:
400d9f2c  36 41 00    entry     a1, 0x20
400d9f3b  42 c2 20    addi      a4, a2, 0x20
400d9f3e  52 a0 02    movi      a5, 0x2
400d9f41  61 ea db    l32r      a6, PTR_s_%2.2x // "%2.2x"
400d9f44  d2 02 00    l8ui      a13, a2, 0x0
400d9f47  60 c6 20    mov       a12, a6
400d9f4a  50 b5 20    mov       a11, a5
400d9f4d  50 a5 20    mov       a10, a5
400d9f50  22 c2 01    addi      a2, a2, 0x1
400d9f53  25 ed 05    call8     log
400d9f56  27 94 ea    bne       a4, a2, LAB_400d9f44
400d9f59  22 a0 00    movi      a2, 0x0
400d9f5c  90 00 00    retw

GetDeviceSerialNumber işlev çağrısının üzerine yama yapıyoruz çünkü bu doğrudan paylaşılan gizli anahtarın oluşturulmasından sonradır ve anahtarın işaretçisi hala a2 kaydında bulunmaktadır.

Değiştirilmiş aygıt yazılımını flaşladım, cihazı başlattım ve seri çıkışı kontrol ettim:

Write ECC conn packet
e883eaed93c63d2c09cddebce6bb15a7f4cb5cedf00c1d882b8b292796254c9c

Başarılı ! Paylaşılan gizli anahtarı yazdırdık!

Anahtarın değişip değişmediğini görmek için cihazı birçok kez yeniden başlattım ve aynı kaldı. Büyük olasılıkla aygıt yazılımı deposundaki anahtarlar kullanılarak hesaplanmıştır, ancak şimdi hesaplanmış statik değere sahibiz, hesaplama işlemini tersine çevirmemize gerek yok.
Pekala, şimdi şifre çözme anahtarını türetme yöntemini anladık ve tüm girdi değerlerine sahibiz;
const hkdfOutputKey = hkdf({
method: 'SHA-256',
salt: Buffer.from(
'e883eaed93c63d2c09cddebce6bb15a7f4cb5cedf00c1d882b8b292796254c9c', 'hex'
),
input: randomBytesFromDeviceKeyExchangePacket,
info: deviceSerialNumber,
outputKeySize: 0x10,
});

Güvenli tarafta olmak için, HKDF çağrısından anahtar çıktısını yazdırmak için başka bir ürün yazılımı yaması yazdım ve yakalanan paketlerden anahtarı yeniden oluşturmayı denedim. Çalışıyor! Bu, anahtar oluşturma işlevini doğru bir şekilde tersine mühendislikten geçirdiğimizi ve anahtar oluşturma mantığını kendi uygulamamızda kopyalayabildiğimizi doğruluyor.

Ancak şimdi hangi şifreleme algoritmasının kullanıldığını bulmamız gerekiyor. Paketleri biçimlendiren fonksiyona geri dönüyorum ve şifreleme fonksiyonuna yapılan çağrıyı buluyorum:

char randomBytes [16];

// Write device serial
memcpy(0x3ffb3ce0, deviceSerialNumber, 9);

// Generate and write random bytes
mbedtls_ctr_drbg_random(globalDrbgContext, randomBytes, 0x10)
memcpy(0x3ffb3ce9, randomBytes, 0x10);

// Write packet data
memcpy(0x3ffb3cf9, data, dataSize);

// Pad with random bytes
mbedtls_ctr_drbg_random(globalDrbgContext dataSize + 0x3ffb3cf9, paddingSize);

// Run encryption on the data + padding
FUN_400e2368(0x3ffb3cf9, dataSize + paddingSize, &HKDFOutputKey, randomBytes)

Cihaz seri numarası pakete kopyalandıktan sonra, 16 rastgele bayt üretildiğini ve hemen ardından kopyalandığını fark ettim. Bu baytlar da şifreleme fonksiyonuna veriliyor. Yani, bunların şifreleme algoritması için bir girdi değişkeni olduğunu biliyoruz.

Anahtarın 128 bit olduğunu ve 128 bit ek rastgele veri içerdiğini biliyoruz.

Bir grup bitsel işlemin döngüsü nedeniyle çok açık bir şekilde kripto ile ilgili olan şifreleme işlevine baktım ve statik bir veri bloğuna referans olduğunu fark ettim.

Bu veri 63 7C 77 7B F2 6B 6F C5 ile başlıyordu, mbedtls kaynak kodunda yapılan bir arama bunun
AES İleri S-Box
!

Yakalanan paketler üzerinde doğrudan AES şifre çözme girişiminde bulunmaya karar verdim ve bir paketin şifresini başarıyla çözdük.

Hex View  00 01 02 03 04 05 06 07  08 09 0A 0B 0C 0D 0E 0F
 
00000000  00 00 65 00 53 00 82 A4  74 79 70 65 AF 6D 69 72  ..e.S...type.mir
00000010  72 6F 72 5F 64 61 74 61  5F 67 65 74 A4 64 61 74  ror_data_get.dat
00000020  61 85 A9 74 69 6D 65 73  74 61 6D 70 CF 00 00 01  a..timestamp....
00000030  8D 18 05 31 FB A9 46 41  4E 5F 53 50 45 45 44 00  ...1..FAN_SPEED.
00000040  A5 42 4F 4F 53 54 C2 A7  46 49 4C 54 45 52 31 00  .BOOST..FILTER1.
00000050  A7 46 49 4C 54 45 52 32  00 07 07 07 07 07 07 07  .FILTER2........

Algoritma AES-128-CBC idi ve ek rastgele veriler IV (Initialization Vektörü) olarak kullanıldı.

Artık herhangi bir aygıt yazılımı yaması gerektirmeyen bir MITM (ortadaki adam) saldırısı oluşturabiliriz. Bunun nedeni cihazın özel anahtarının artık biliniyor olması, anahtar türetme mantığının tersine mühendisliğinin yapılmış olması ve gerekli tüm dinamik verilerin güvensiz ağ üzerinden ifşa edilmiş olmasıdır.

ECDH’yi doğru bir şekilde uygulasaydı, akıllı cihazın ifşa edilmeyen benzersiz bir özel anahtarı olurdu ve en kolay saldırı yolumuz kendi sunucu anahtar çiftimizi oluşturmak ve cihazın özel ortak anahtarımızı kabul etmesi için herhangi bir ürün yazılımı değişikliği yapmak olurdu.

Ancak özel protokol tasarımları nedeniyle, akıllı cihazda herhangi bir değişiklik yapmadan ağ iletişimlerini kesebilen, şifresini çözebilen ve potansiyel olarak değiştirebilen bir MITM komut dosyası yazabiliriz. İşte yapacağımız şey bu!

Şimdi asıl amaç, mümkün olduğunca çok verinin şifresini çözmek ve günlüğe kaydetmek; daha sonra, bulut sunucularını tamamen değiştiren yerel bir sunucu uç noktası yazmak için bunu referans alabiliriz.

Bunu yapmak için hızlı bir Node.js  hazırladım:

const dns = require("dns");
const udp = require("dgram");
const crypto = require("crypto");
const hkdf = require("futoin-hkdf");
const fs = require("fs");

// Key Gen

const sharedSecretKey = Buffer.from(
  "e883eaed93c63d2c09cddebce6bb15a7f4cb5cedf00c1d882b8b292796254c9c",
  "hex"
);

function calculateAesKey(deviceSerialNumber, inputData) {
  return hkdf(inputData, 16, {
    salt: sharedSecretKey,
    info: deviceSerialNumber,
    hash: "SHA-256",
  });
}

// Packet Parsing

let latestAesKey = null;
let packetCounter = 0;
const proxyLogDir = path.join(__dirname, "decrypted-packets");

function decryptPacket(data, deviceSerial) {
  const IV = data.subarray(0xd, 0x1d);
  const encryptedBuffer = data.subarray(0x1d, data.length - 2);
  const decipher = crypto.createDecipheriv(
    "aes-128-cbc",
    latestAesKey,
    parsed.IV
  );
  decipher.setAutoPadding(false);
  return Buffer.concat([decipher.update(encryptedBuffer), decipher.final()]);
}

function logPacket(data) {
  const messageId = data.readUInt8(3);
  const deviceSerial = data.subarray(4, 4 + 9);

  if (messageId === 2) {
    // Key Exchange
    const randomlyGeneratedBytes = data.subarray(0xf, data.length - 2);
    latestAesKey = calculateAesKey(deviceSerial, randomlyGeneratedBytes);
  } else {
    // Encrypted Packets
    fs.writeFileSync(
      path.join(proxyLogDir, `packet-${id}.bin`),
      decryptPacket(data)
    );
  }
}

// Networking

dns.setServers(["1.1.1.1", "[2606:4700:4700::1111]"]);

const PORT = 41014;
const cloudIp = dns.resolve4("smartdeviceep.---.com")[0];
const cloud = udp.createSocket("udp4");
let latestClientIp = null;
let latestClientPort = null;

cloud.on("message", function (data, info) {
  logPacket(data);
  local.send(data, latestClientIp, latestClientPort);
});

const local = udp.createSocket("udp4");
local.bind(PORT);

local.on("message", function (data, info) {
  logPacket(data);
  latestClientIp = info.address;
  latestClientPort = info.port;
  cloud.send(data, PORT, cloudIp);
});

Burada, bir MITM saldırısı uygulamak için tüm araştırmalarımızı birleştiriyoruz.

Paketleri ilk yakaladığımızda olduğu gibi, Node.js’yi yerel DNS sunucumuzu atlamak için Cloudflare’nin DNS çözümleyicisini kullanacak şekilde yapılandırıyoruz.

Akıllı cihazdan gelen paketleri kabul etmek için yerel olarak bir UDP soketi ve ayrıca bulut sunucusuyla iletişim kurmak için bir soket oluşturuyoruz.

Akıllı cihazdan aldığımız her şeyi günlüğe kaydediyor ve bulut sunucusuna gönderiyoruz
Bulut sunucusundan aldığımız her şeyi günlüğe kaydeder ve akıllı cihaza göndeririz
Mesaj kimliği 2 olan paketleri, akıllı cihazın rastgele baytları sunucuya gönderdiği anahtar değişim paketi olarak ele alıyoruz, daha sonra gelecekteki paketlerin şifresini çözmek için kullanılan AES anahtarını hesaplıyoruz.
Çekim yaparken, akıllı cihazı uzaktan kontrol etmek için mobil uygulamalarını kullandım, böylece günlüklere başvurabilir ve mantığı kendimiz kopyalayabilirdik.
Artık şifresi çözülmüş paket verilerine sahibiz, ancak veriler hala serileştirilmiş ikili formatta:

Hex View  00 01 02 03 04 05 06 07  08 09 0A 0B 0C 0D 0E 0F
 
00000000  01 00 64 00 29 00 82 A4  74 79 70 65 A7 63 6F 6E  ..d.)...type.con
00000010  6E 65 63 74 A8 66 69 72  6D 77 61 72 65 C4 10 00  nect.firmware...
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 83  ................

Başlık oldukça basitti, yine sadece bazı ID’ler ve uzunluk, ancak küçük bir örnek:

  • 01 00 – paket id
  • 64 00 – işlem id
  • 29 00 – serileştirilmiş veri uzunluğu

Ve biraz uğraşarak, serileştirilmiş formatı buldum:

  • 82 – map
  • A4 – 4lü dize
  • A7 – 7li dize

Bunu tersine çevirmek eğlenceliydi çünkü yazım daha çok bitlerle tanımlanıyordu, ancak bu basit durumlar için baytlardan açıkça okunabilir.
Geriye dönüp baktığımda, neden bu serileştirilmiş ikili veri formatına uyan mevcut bir çözüm aramadığımdan emin değilim; bu noktada her şeyin özel bir çözüm olmasını bekliyordum. Ancak şimdi bir arama yaptığımda, sadece download bölümünde paylaşacağım sendpack uygulamasını yazdım.

const { unpack, pack } = require('msgpackr');

const packedData = Buffer.from(
  '82A474797065A7636F6E6E656374A86669726D77617265C41000000000000000000000000000000000', 
  'hex'
);

const unpackedData = unpack(packedData);

// unpackedData:
{
  type: 'connect',
  firmware: <Buffer 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00>
}

Akıllı cihaz için özel bir yerel sunucu yazmaya hazırlanırken, yakaladığımız paketlenmemiş ağ günlüklerine bir göz atalım:

Anahtar Değişim Paketi:

Akıllı cihaz, HKDF’de kullanılmak üzere sunucuya rastgele baytlar gönderir.

// Smart Device Request
D1C2B34170177512F7692517504AC5DDD49806FE246B96FD56144A707E515557

// Server Response
00000000000000000000000000000000

Akıllı cihaz açıldığında ilk durumunu sunucudan alır.

// Smart Device Request
{ type: 'mirror_data_get' }

// Server Response
{
  type: 'mirror_data_get',
  data: {
    timestamp: 1705505010171n,
    FAN_SPEED: 0,
    BOOST: false,
    FILTER1: 0,
    FILTER2: 0
  }
}
Akıllı cihaz sunucuya bağlandığında mevcut ürün yazılımı UUID'sini gönderir. Sunucu, indirilebilecek bir ürün yazılımı veya yapılandırma güncellemesi için potansiyel UUID ile yanıt verir
// Smart Device Request
{
  type: 'connect',
  firmware: <Buffer 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00>
}

// Server Response
{
  type: 'connect',
  server_time: 1706098993961n,
  firmware: <Buffer ab cd ef ab cd ef ab cd ef ab cd ef ab cd ef ab>,
  config: <Buffer 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00>,
  calibration: <Buffer 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00>,
  conditioning: <Buffer 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00>,
  server_address: 'smartdeviceep.---.com',
  server_port: 41014,
  rtc_sync: { ss: 13, mm: 23, hh: 12, DD: 24, MM: 1, YYYY: 2024, D: 3 }
}
// Server Request
{ 
  type: 'mirror_data',
  data: {
    FAN_SPEED: 1,
    BOOST: false
  }
}
// Smart Device Request
{
  type: 'mirror_data',
  data: {
    timestamp: 1706105072142n,
    FAN_SPEED: 1,
    BOOST: false,
    FILTER1: 0,
    FILTER2: 0
  }
}

// Server Response
{ type: 'mirror_data' }
// Smart Device Request
{
  type: 'keep_alive',
  stats: {
    rssi: -127n,
    rtt: 684,
    pkt_drop: 1,
    con_count: 1,
    boot_str: '',
    uptime: 100080
  }
}

// Server Response
{ type: 'keep_alive' }

 

function HandleSmartDeviceRequest(req) {
  switch (req.type) {
    case 'mirror_data_get': {
      // Device wants state, send latest MQTT state or default fallback
      device.send({ fan_speed: mqtt.get('fan_speed') || 0 });
      return;
    }
    case 'mirror_data': {
      // Device state has changed, publish and retain in MQTT broker
      mqtt.publish('fan_speed', req.fan_speed, { retain: true });
      return;
    }
  }
}

MQTT KÖPRÜLEME BAĞLANTISINI DA YAPIYORUZ.
function HandleMQTTMessage(topic, msg) {
  switch (topic) {
    case 'set_fan_speed': {
      // MQTT wants to change state, forward to device
      device.send({ fan_speed: msg.fan_speed });
      return;
    }
  }
}

ve son olarak ;

mqtt:
  fan:
    - name: "Air Purifier"
      unique_id: "air_purifier.main"
      state_topic: "air_purifier/on/state"
      command_topic: "air_purifier/on/set"
      payload_on: "true"
      payload_off: "false"
      percentage_state_topic: "air_purifier/speed/state"
      percentage_command_topic: "air_purifier/speed/set"
      speed_range_min: 1
      speed_range_max: 4

HAVA TEMİZLEYİCİ CİHAZA DIŞARIDAN MÜDAHALE EDEBİLİYORUZ.

Bir yanıt yazın

Your email address will not be published.