← Powrót do bloga
2026.09.20

Reverse engineering taniego adaptera CarPlay

Kupiłem bezprzewodowy adapter CarPlay na AliExpress za jakieś dwadzieścia euro. Działa, ale w ekranie startowym CarPlay w moim Oplu Corsie z 2017 roku mieści się tylko osiem aplikacji na stronę i nigdzie nie ma ustawienia, żeby to zmienić. Więc rozebrałem go na części.

Adapter zgłasza typ sprzętu HW501. Sprzedaje się go pod co najmniej kilkunastoma markami, działa na wbudowanym Linuksie na 32-bitowym układzie RISC-V, ma zamknięte oprogramowanie układowe i zero publicznej dokumentacji. Wszystko poniżej pochodzi z jego własnego serwera aktualizacji, jego własnych binariów i jednej bardzo cierpliwej zapasowej sztuki.

Co jest w środku

Każdy plik ELF w firmwarze to 32-bitowy RISC-V (ELF machine 243), zlinkowany z loaderem ILP32D /lib/ld-linux-riscv32-ilp32d.so.1. Główna aplikacja nazywa się CPAAProxyEx i zawiera ścieżkę builda kończącą się na libs/v821. Bluetooth działa w osobnym procesie blueware, discovery przez mdnsd, a aktualizacje przez mały serwer HTTP o nazwie UpdateServer, nasłuchujący na własnym punkcie dostępowym Wi-Fi adaptera.

Ten updater to droga do środka. Jego stringi pokazują całą procedurę aktualizacji: zapisz /tmp/appupdate/app.img do /dev/by-name/app po porównaniu MD5 z dołączonym appmd5sum.txt. Żadnej weryfikacji podpisu, tylko suma kontrolna, którą wydawca dokłada do tego samego archiwum.

Pobieranie fabrycznego firmware'u

Endpoint historii producenta wymienia wersje 131, 128, 127 i 126. Adresy archiwów są przewidywalne:

http://43.138.184.52/appupdate/hw501_update_v<VERSION>.tar

126, 128 i 131 pobierają się i weryfikują bez problemu. 127 jest na liście, ale serwer zgłasza update file not found, zarówno przez bezpośredni adres, jak i przez API chunków. Zabawne, że endpoint lastversion nadal ogłasza 126 jako najnowsze wydanie, podczas gdy historia sięga 131. Interfejs webowy v131 wskazuje na drugi serwer, 114.132.94.163, który serwuje archiwa identyczne co do bajta.

Każde archiwum zawiera dokładnie dwa pliki: app.img, czyli SquashFS skompresowany XZ, oraz appmd5sum.txt. Nie ma tam jądra, bootloadera ani pełnej kopii pamięci flash. To aktualizacje wyłącznie partycji aplikacji i właśnie dlatego eksperymentowanie da się przeżyć: fabryczny updater jest drogą powrotną.

Co producent zmieniał między wydaniami

Rozpakowanie wszystkich trzech obrazów i porównanie każdego pliku zamienia nieistniejące release notes w coś konkretnego.

Między v126 a v128 implementacja Android Auto wyprowadziła się z głównego binarium do nowej biblioteki współdzielonej. CPAAProxyEx kurczy się z 2 393 408 do 1 153 348 bajtów, jego sekcja .text z 1 685 944 do 910 088 bajtów, a liczba zdefiniowanych dynamicznych symboli funkcji spada z 269 do 60. Spośród nazw, które znikają, 373 pojawiają się w nowej libAutoProxy.so, w tym obiekty protokołu gal z Android Auto. Zmienia się też identyfikator builda stosu Bluetooth:

v126: ZA_NAS_BT01,V2.7.2,20250611
v128: ZA_NAS_BT01,V2.9.5,20251028

Między v128 a v131 stos discovery zostaje wymieniony w całości: mdnsd niemal się podwaja, z 284 704 do 559 264 bajtów. Najbardziej czytelna zmiana jest w libiAP2Link.so, gdzie iAP2LinkQueueSendData rośnie z 332 do 410 bajtów. W v128 funkcja wywołuje iAP2LinkAddPacketAfter i idzie dalej niezależnie od wyniku. W v131 nowe instrukcje porównują ten wynik z 255, a przy błędzie logują stan kolejki, wywołują iAP2PacketDelete i zwracają zero, zamiast kontynuować z pakietem, który nigdy nie trafił do kolejki. To prawdziwa poprawka w warstwie łącza z iPhonem, widoczna wyłącznie w deasemblacji.

Jedno za to zepsuli: na angielskiej stronie konfiguracyjnej v131 przyciski aktualizacji nadal wywołują startUpdateApp(...), ale definicja tej funkcji została usunięta. Na stronie chińskiej wciąż jest. Angielski interfejs updatera odwołuje się więc do funkcji, która już nie istnieje.

Jak zmusić CarPlay do pokazania większej liczby ikon

CarPlay nie układa ekranu startowego na podstawie pikseli, tylko fizycznych wymiarów ekranu zgłaszanych przez radio samochodowe. Corsa raportuje 800x480 pikseli przy 152x91 mm, a przy takim rozmiarze iOS daje siatkę 4x2 i całkowicie ukrywa ustawienie Smart Display Zoom.

Łatka kłamie więc o milimetrach. Pod adresem wirtualnym 0x4dc74 w CPAAProxyEx hook przechwytuje getter właściwości wyświetlacza na ścieżce, na której zwracana jest zapamiętana tablica ekranów radia. Hook jest celowo nieufny: wymaga dokładnie jednego słownika ekranu w tablicy i dokładnego dopasowania wszystkich czterech zaobserwowanych wymiarów. Dopiero wtedy kopiuje słownik, podmienia widthPhysical i heightPhysical i zwraca nową jednoelementową tablicę z własnością, jakiej oczekuje wywołujący. Oryginalne obiekty samochodu nigdy nie są nadpisywane. Wymiary w pikselach, liczba klatek, UUID, funkcje i metadane dotyku przechodzą nietknięte, a każda ścieżka błędu wraca do oryginalnego zachowania.

Trzy profile, testowane w jednym samochodzie:

  • density125, raportuje 190x114 mm: mniejsze elementy i wreszcie pojawia się przełącznik Smart Display Zoom, ale ekran startowy zostaje 4x2.
  • density137.5, raportuje 209x125 mm: instaluje się i uruchamia, układ nieprzetestowany.
  • density150, raportuje 228x137 mm: 5 kolumn na 3 rzędy, czyli 15 aplikacji na stronę zamiast 8, na niezmienionym ekranie 800x480.

Build jest równie ostrożny jak sam hook. SHA-256 fabrycznego pliku wykonywalnego musi zgadzać się co do bajta przed łataniem, instrukcje hooka trafiają w zweryfikowane wypełnienie zerami, więc istniejący segment ładowany jest rozszerzany w miejscu, bez przesuwania sekcji i bez zmiany długości pliku, a po przebudowaniu SquashFS każdy plik, dowiązanie, uprawnienie, UID, GID i znacznik czasu porównuje się z listingiem fabrycznym. Różni się wyłącznie bin/CPAAProxyEx. Powstały obraz ma 4 472 832 bajty, z zapasem mieszcząc się w partycji aplikacji o rozmiarze 5 242 880 bajtów, który raportuje log jądra.

Moment, w którym prawie go zabiłem

Osobny eksperyment próbował dodać mirroring ekranu na samym adapterze. Tamten obraz zawierał proces nadzorcy, który trafił do tej samej grupy procesów co UpdateServer, więc jego sprzątanie ubiło updater w środku aktualizacji. To dokładnie ten scenariusz, którego nie chcesz przy urządzeniu dostępnym wyłącznie przez jego własne Wi-Fi.

Uratował sytuację fabryczny handler diagnostyki, który rozwija podstawienia powłoki w podanej nazwie archiwum. Przez niego, mając BusyBoksa tak okrojonego, że brakowało w nim setsid, command i base64, przesłałem do /tmp 324-bajtowy helper RV32 korzystający wyłącznie z wywołań systemowych, używając ósemkowego printf, zweryfikowałem jego sumę kontrolną i kazałem mu wywołać setsid oraz uruchomić niezmieniony fabryczny UpdateServer na porcie 8082 we własnej sesji. Przeżył wyłączenie, które ubiło oryginalny updater, wgrał czysty obraz do 100%, a odczyt partycji zgodził się z oczekiwanym MD5. Adapter wystartował normalnie.

Tamto archiwum z mirroringiem jest wycofane, a narzędzia odrzucają teraz jego hash z nazwy.

Jak to wygląda teraz

Najsolidniejsza część to narzędzia analityczne: inspektor urządzenia i offline'owy kolektor diagnostyki, które nigdy nie dotykają firmware'u, pobieracz weryfikujący strukturę i sumy kontrolne, potok porównawczy hashujący każdy plik między wersjami oraz opisana deasemblacja RV32. Łatki gęstości działają, na dokładnie jednej kombinacji adaptera, samochodu i wersji iOS. Emulacja w QEMU ze wsparciem instrukcji Andes uruchamia testy komponentów i doprowadza fabryczną aplikację do inicjalizacji, po czym trafia na brakujący sprzęt i wywala się z SIGSEGV. Przydaje się do testowania hooków, ale to nic zbliżonego do pełnego emulatora urządzenia.

Wszystko jest na github.com/kacper-serewis/smartbox-re, ze standardowym ostrzeżeniem: wgrywanie firmware'u z internetu do urządzenia podłączonego do deski rozdzielczej jest dokładnie tak nierozsądne, jak brzmi, a każde narzędzie zapisujące odmawia działania na sprzęcie, którego nie rozpozna jako HW501.

Koordynowane przeze mnie, napisane z GPT-6-Astra.