Tapo C260 ve Tapo Keşif Protokolü v2’de Tersine Mühendislik


2025 yılının başlarında Singapur Siber Güvenlik Ajansı’nın düzenlediği SPIRITCYBER IoT donanım hackleme yarışmasına YesWeHack ile katıldım. Birkaç cihaz arasında, hala yama ve CVE ataması bekleyen birkaç RCE ve diğer ilginç güvenlik açıkları buldum.

Cihazlardan biri TP-Link’in en yeni Tapo C260 kamerasıydı ve henüz güvenlik açıklarıyla ilgili herhangi bir ayrıntıyı paylaşmayı planlamıyor olsam da (varsa 😉), ürün yazılımının tersine mühendislik sürecini paylaşmak istiyorum.

Ek olarak, araştırmamın bir parçası olarak Tapo Keşif Protokolü sürüm 2’ye tersine mühendislik uyguladım. Sürüm 1, Pwn2Own’a dahil edilmeden önce başkaları tarafından kapsamlı bir şekilde araştırıldı ve kullanıldı, ancak yeni sürümle ilgili herhangi bir kaynak bulamadığım için paylaşmanın ilginç olabileceğini düşündüm.

Her halükarda, C260’ın, Nokia Beacon 1 ve LAU-G150-C Optik Ağ Terminali ile ilgili önceki girişimlerime kıyasla zorluk açısından güzel bir adım olduğunu düşündüm.

C260 tam anlamıyla kırılması zor bir cevizdir. Açıkta kalan vidaları olmayan yüksek kaliteli bir kabuğa sahiptir; çıkartmalar veya pedlerin altına bile gizlenmez.

C260 Ön

iFixit Jimmy’nin (aslında bunları DEF CON’da bedavaya veriyorlardı) burada davayı parçalara ayırma konusunda paha biçilmez olduğunu gördüm. Burada biraz teknik var – onu bir kaldıraç veya kesici olarak kullanmaya çalışmak yerine, önce bıçağın bir kısmını boşluğa sokun, sonra onu yukarı ve aşağı sallayın, böylece yavaşça boşluğun derinliklerine iner. Bu, çok daha fazla kaldıraç ve manevra alanı yaratır.

Bundan sonra, ilginç iç kısımlara ulaşmadan önce açabileceğiniz birkaç vida ve kasa daha var:

C260 Dahili Parçalar

Bu, 4k kamera, AI nesne tanıma, microSD desteği, BT ve WiFi gibi birçok özelliğe sahip ve bileşenleri birkaç panele yayılmış oldukça gelişmiş bir kameradır.

C260 Anakart 1

C260 Anakart 2

fark etmiş olabilirsiniz UART_TX İlk resimde etiketi var ancak kutudan çıktığında çalışmıyorlar. Diğer Tapo cihazlarının önceki araştırmacıları, onları çalıştırabilmek için genellikle önce bu pinleri yeniden bağlamak zorunda kalıyordu.

Daha da önemlisi, WSON8 paketindeki bir ESMT F50L1G41LB yongası olan flash belleği buldum (ilk görüntünün sol tarafı).

Bir sonraki adım, onu XGecu T48 programlayıcısını kullanarak okumaktı. Ne yazık ki, T48 için sağlanan WSON8 adaptörü aslında çipe sığmayacak kadar küçüktü. Bu sorunu aşmak için timsah klips adaptörlerini (WSON8 pinlerine uyan) kullandım ve bunun yerine SOP8 adaptörüne bağladım.

C260 Anakart 2

Buradan çipi okumak oldukça kolaydı çünkü çip T48 için desteklenen cihazlardan biriydi. Bu sefer çipin veri sayfasında belgelendiği gibi fazladan 64 baytlık OOB verilerini de kaldırdım:

The device contains 1024 blocks, composed by
64 pages consisting in two NAND structures of 32 series
connected Flash cells. Each page consists 2112-Byte and is
further divided into a 2048-Byte data storage area with a
separate 64-Byte spare area. The 64-Byte area is typically used
for memory and error management.

Daha sonra dosya sisteminin şifresi çözülüyordu – evet, Tapo cihazları dosya sistemi şifrelemesini kullanıyor. Neyse ki, Quentin Kaiser tarafından C200 blog yazısında (UART bağlantı noktasının yeniden bağlanmasını da içerir) ayrıntılı olarak açıklandığı gibi pek çok çalışma yapıldı ve aynı sabit kodlu AES-128-CFB1 şeması ve anahtarı hala kullanılıyor!

Squash dosya sistemini paketinden çıkardığımda yakınlaştırdım. /bin/main web istekleri ve diğer protokoller için ilgili işleyicilerin çoğunu içeren ikili dosya.

C260 donanım yazılımının diğer bölümleriyle ilgili bazı ayrıntıları başka bir blog yazısı için saklarken, Tapo Discovery Protokolü işleyicisine odaklanacağım.

Tapo Keşif Protokolü v2 🔗

main ikili dosya, işlevin adını tanımlamaya yardımcı olan birçok yararlı günlük kaydı iletisine sahiptir. Bu durumda başladım tdpd_listen_thread ofset olarak 0x2a7e020002 ve 20010 numaralı bağlantı noktalarında UDP yuvaları oluşturan.

Daha sonra, işleyici gelen paketi ayrıştırır ve çeşitli yapı değerlerini yararlı bir şekilde günlüğe kaydeder:

  puVar8 = (undefined8 *)
           recvfrom(param_1,&DAT_00323530,0x1000,0,(sockaddr *)&DAT_00323310,&local_3c);
  if ((int)puVar8 < 1) {
    pcVar15 = "[TDPD]tdpd recv error.";
    uVar11 = 0x966;
    pcVar25 = "tdpd_handle";
LAB_0002a694:
    uVar18 = 3;
    goto LAB_00029c3c;
  }
  if ((int)puVar8 < 0x10) {
    bVar37 = 0x10;
    pcVar15 = "[TDPD]recvbuf length = %d, less than hdr\'s %d";
    pcVar25 = "tdpd_handle";
    uVar11 = 0x96c;
LAB_0002a236:
    msg_debug(0,0x10,3,pcVar25,uVar11,pcVar15,puVar8,bVar37);
    return;
  }
  uVar20 = (uint)DAT_00323534;
  uVar21 = (uint)DAT_00323532;
  uVar22 = (uint)DAT_00323537;
  uVar23 = (uint)DAT_00323536;
  puVar39 = &DAT_00323540;
  uVar11 = _DAT_00323538;
  uVar38 = DAT_0032353c;
  msg_debug(0,0x10,1,"tdpd_handle",0x971,
            "[TDPD]recv packet:\nversion:%d\nreserved:%d\nflag:%d\nresult:%d\nopcode:%d\npayloadleng th:%d\nsn:%lu\nchecksum:%lu\npayload=%s\n"
            ,DAT_00323530,DAT_00323531,uVar23,uVar22,uVar21,uVar20,_DAT_00323538,DAT_0032353c,
            &DAT_00323540);

Son kayıt çağrısında görülebileceği gibi, bir version değer. Sürüm 1 paketleri için eski işleyiciyi (önceki araştırmacıların tersine mühendislik yaptığı) veya bir switch ifadesi aracılığıyla sürüm 2 paketleri için yeni bir işleyici kümesini kullanan, daha aşağılarda bir if/else ifadesinde sürümü kullanır:

  switch(uVar28) {
  case 1:
    input_obj = jso_from_string(&DAT_00323540);
    if (input_obj == 0) {
      puVar32 = &DAT_00323540;
      msg_debug(0,0x10,3,"tdpd_build_discovery_app_packet",0x717,
                "[TDPD][Error] invalid json string = %s",&DAT_00323540,puVar14);
      jso_add_int(0,"error_code",0xffffffff);
    }
    else {
      out_root_obj = jso_new_obj();
      if (out_root_obj == 0) {
        msg_debug(0,0x10,3,"tdpd_build_discovery_app_packet",0x720,
                  "[TDPD][Error] failed to create out_root_obj",puVar32,puVar14);
        jso_add_int(0,"error_code",0xffffffff);
        jso_free_obj(input_obj);
      }
      else {
        result_obj = jso_new_obj();
        if (result_obj == 0) {
          msg_debug(0,0x10,3,"tdpd_build_discovery_app_packet",0x728,
                    "[TDPD][Error] failed to create result_obj");
          jso_add_int(out_root_obj,"error_code",0xffffffff);
LAB_0002a282:
          jso_free_obj(input_obj);
          iVar30 = jso_to_string(out_root_obj,&DAT_00324540,0xff0);
          if (iVar30 < 0) goto LAB_0002a48a;
        }
        else {
          iVar12 = tdpd_get_basic_info_v2(input_obj,result_obj);

Kısacası, sürüm 2 çok daha ilginç işlevlerin kilidini açıyor. Burada tüm ayrıntılara dalmayacağım ancak sürüm 1’e göre bazı ilginç farklılıklar şunlardır:

  • Küçük-endian yerine büyük-endian
  • Özel sağlama toplamı yerine CRC32 sağlama toplamı
  • A tdpd_get_encrypt_info_v2 Daha kapsamlı sistem verileri göndermek için RSA şifrelemesini kullanan işlev

Statik analiz faydalı olsa da çalışan bir kameraya gerçek paketler göndermek istedim. Ancak ilkel senaryom başarısız oldu. Doğru yapı boyutlarını ve değerlerini doğru şekilde ayarladığımdan emin olmak için bir tür hata ayıklama kurulumuna ihtiyacım olduğunu biliyordum. İki seçenek vardı: UART erişimi elde etmeye çalışın (ve uygun bir kabuk almadan önce potansiyel olarak daha fazla engelleyiciyle karşılaşın) veya öykünme gerçekleştirin.

Neyse ki, tüm dosya sistemiyle, Qiling çerçevesini kullanarak çalışan ikili dosyayı yaklaşık olarak taklit edebildim. Yol boyunca bazı yapılandırma değerlerinin başlatılmasını gerektirmek gibi birkaç engel vardı. müdahale ediyorum read_config kullanarak işlev ql.hook_address ve basitçe eksik değerleri belleğe yazdı. Ayrıca, tarihten bu yana main işlevi TDP’nin yanı sıra başka birçok işleyiciyi de çalıştırdı, PC kaydını sunucunun adresine değiştirerek yürütmeyi manuel olarak yeniden yönlendirdim. tdpd_handle işlev.

Daha sonra, oluşturduğum TDP paketini bağlayarak enjekte ettim. recvfrom Arama.

Yaptığım bir başka son değişiklik de hata ayıklama günlük fonksiyonunu, null yerine stdout’a (3) çıktı verecek şekilde değiştirmekti; bu, Qiling çıktımda son derece bilgilendirici günlük mesajları ve hataları almamı sağladı.

PACKET = create_tdpd_packet(version=2, opcode=2, payload=OPCODE_2_PACKET)
print(PACKET)

def read_c_string(ql: Qiling, address: int, max_length: int = 256) -> str:
    raw_bytes = b""
    for i in range(max_length):
        byte = ql.mem.read(address + i, 1)
        if byte == b'\x00':
            break
        raw_bytes += byte
    return raw_bytes.decode('utf-8', errors='ignore')  # or 'ascii'

def my_recvfrom(ql: Qiling, sockfd: int, buf: int, length: int, flags: int, addr: int, addrlen: int):    
    ql.mem.write(buf, PACKET)
    return len(PACKET)

def redirect_tdpd_handle(ql: Qiling) -> None:
    ql.arch.regs.lr = EXIT_ADDRESS
    ql.arch.regs.pc = 0x29728 | 1

# hook debug wrapper to also output to stdout
def msg_debug_wrapper(ql: Qiling) -> None:
    ql.arch.regs.r2 = 3


def read_config_enter(ql: Qiling) -> None:
    config_key = read_c_string(ql, ql.arch.regs.r0)
    print(f'Reading config {config_key}')
    if config_key == '/cloud_config/extra_bind':
        ql.mem.write(ql.arch.regs.r1, b'data_collect')

def read_config_exit(ql: Qiling) -> None:
    print(f'Value is {read_c_string(ql, ql.arch.regs.r5)}')

def debug_tmp(ql: Qiling) -> None:
    print(read_c_string(ql, ql.arch.regs.r0))
    print(read_c_string(ql, ql.arch.regs.r1))

EXIT_ADDRESS = 0x2ABC0

def main():
    executable_path = 'squashfs-root/bin/main'
    rootfs_path = 'squashfs-root'

    ql = Qiling([executable_path], rootfs_path, archtype=QL_ARCH.ARM, ostype=QL_OS.LINUX, multithread=True)#, verbose=QL_VERBOSE.DEBUG)

    ql.os.set_syscall('recvfrom', my_recvfrom, QL_INTERCEPT.CALL)

    ql.hook_address(redirect_tdpd_handle, 0x1CB66)

    ql.hook_address(msg_debug_wrapper, 0x1C514)

    # read_config hooks
    ql.hook_address(read_config_enter, 0x134394)
    ql.hook_address(read_config_exit, 0x134444)
    ql.hook_address(read_config_exit, 0x1343B2)

    ql.hook_address(debug_tmp, 0x2B280)

    ql.run(end=EXIT_ADDRESS)


if __name__ == "__main__":
    main()

Koli bandı ve dua kod eşdeğeri ile bir arada tutulmasına rağmen, çalışan bir kamerada çalışan bir paket oluşturabilecek kadar iyi çalıştı! Senaryoyu GitHub’da paylaştım; Yapabileceğim herhangi bir düzeltme veya iyileştirme varsa bana bildirin.

Sonuç 🔗

Her yeni cihaza tersine mühendislik uygulanmasının kendi tuhaflıkları ve tavşan delikleriyle birlikte geldiğini hızla anlamaya başladım. Bu yazıda nispeten basit görünse de, doğru araştırmayı bulmak, donanımla mücadele etmek ve bozuk kodların üstesinden gelmek için sayısız saatler harcadım.

Yapay zeka destekli RE’nin oldukça faydalı olduğunu ancak yapılar, endianness vb. gibi önemli ayrıntılar için yeterince kapsamlı olmadığını buldum. Son zamanlarda, IDA Pro MCP’li AETHER gibi sökücülere doğrudan bağlanmaya odaklanan ve sözde kodun Gemini’ye eklenmesiyle kullanıcı deneyimini kesinlikle iyileştirecek pek çok heyecan verici gelişme yaşandı.

Başlangıçta da ima edildiği gibi, SPIRITCYBER yamalarının tamamlandığından emin olana kadar daha fazla ayrıntı paylaşma özgürlüğüm yok, ancak daha heyecan verici ayrıntılar için bizi izlemeye devam edin!



Source link