1 điểm bởi GN⁺ 2025-04-16 | Chưa có bình luận nào. | Chia sẻ qua WhatsApp
  • Để điều khiển trực tiếp một máy lọc không khí nền tảng ESP32 bị ràng buộc với ứng dụng và đám mây của nhà sản xuất trong Home Assistant, tác giả đã dịch ngược đường đi điều khiển từ xa và thay thế bằng máy chủ cục bộ
  • Qua phân tích ứng dụng, обход DNS và bắt gói bằng Wireshark, xác nhận thiết bị gửi gói UDP đến smartdeviceep.---.com:41014 và dùng giao thức riêng thay vì DTLS chuẩn
  • Thông qua kết nối UART và dump flash 4MB, thu được dev_key.key, chứng chỉ, cấu hình máy chủ, cấu hình WiFi, rồi phân tích cấu trúc firmware bằng Ghidra và esp32knife
  • Gói tin kết hợp header 13 byte, CRC-16 2 byte cuối, tạo khóa ECDH/HKDF, AES-128-CBC và tuần tự hóa MessagePack; bằng cách vá firmware để in bí mật chia sẻ ra log serial, tác giả đã giải mã thành công
  • Cấu hình cuối cùng gồm proxy MITM, máy chủ cục bộ và cầu nối MQTT dựa trên Mosquitto; dùng MQTT Fan của Home Assistant để điều khiển nguồn và tốc độ quạt ổn định trong vài tuần

Chuyển máy lọc không khí phụ thuộc đám mây sang điều khiển cục bộ

  • Mục tiêu là điều khiển trong Home Assistant một máy lọc không khí vốn chỉ kết nối với ứng dụng di động và tài khoản đám mây của nhà sản xuất
  • Sau khi bật/tắt Bluetooth, WiFi và 5G trên điện thoại để kiểm tra, ứng dụng điều khiển thiết bị chỉ thông qua kết nối Internet, không phải Bluetooth hay WiFi cục bộ
  • Vì các giá trị điều khiển như tốc độ quạt được trao đổi ở đâu đó giữa thiết bị và máy chủ đám mây, đoạn mạng trở thành điểm tấn công then chốt
    • Nếu chặn lưu lượng và thay đổi giá trị thì có thể điều khiển thiết bị
    • Nếu mô phỏng phản hồi của máy chủ thì có thể vận hành thiết bị mà không cần Internet và đám mây của nhà sản xuất
  • Nội dung dịch ngược nhằm mục đích giáo dục; các thông tin nhạy cảm theo từng sản phẩm như khóa riêng, tên miền và endpoint API đã được làm rối hoặc xóa
  • Việc chỉnh sửa thiết bị có thể làm mất hiệu lực bảo hành hoặc gây hỏng thiết bị vĩnh viễn

Phân tích ứng dụng và bắt lưu lượng UDP

  • Trích xuất .apk của ứng dụng Android, mở classes.dex bằng dex2jarjd-gui để xem bên trong
  • Trong MainActivity.class, xác nhận ứng dụng dựa trên React Native; trong assets/index.android.bundle, tìm thấy kết nối WebSocket bảo mật
    • Mã ví dụ có chứa kết nối wss://smartdeviceapi.---.com
  • Dùng tính năng tra cứu truy vấn DNS của Pi-hole để xác định tên miền máy chủ đám mây mà thiết bị kết nối tới
  • Dùng tính năng Local DNS của Pi-hole để trỏ tên miền đó về máy trạm cục bộ 192.168.0.10, rồi lọc lưu lượng của IP thiết bị 192.168.0.61 trong Wireshark
  • Thiết bị đang gửi gói UDP đến cổng 41014 của máy trạm

Cấu hình relay và manh mối về giao thức riêng

  • Vì DNS cục bộ đã phân giải tên miền đám mây về máy trạm, IP máy chủ thật được tra cứu bằng Cloudflare DNS resolver 1.1.1.1
  • Dùng node-udp-forwarder để máy trạm đóng vai trò relay UDP giữa thiết bị và máy chủ đám mây
  • Đã bắt gói đầu tiên khi khởi động và phản hồi của máy chủ, nhưng chúng trông như các byte ngẫu nhiên, không có chuỗi có thể đọc được, nên có khả năng đã được mã hóa
  • Wireshark không nhận diện gói tin là DTLS, và định dạng header trong đặc tả DTLS cũng khác với các gói đã bắt được
  • Vì có vẻ đây không phải giao thức chuẩn, cần tự dịch ngược cấu trúc gói tin và phương thức mã hóa

Tháo ESP32 và truy cập serial

  • Khi tháo thiết bị, có thể thấy PCB chính, cổng kết nối quạt và cáp ribbon của bảng điều khiển phía trước
  • Bộ điều khiển chính được đánh dấu ESP32-WROOM-32D, là vi điều khiển dòng ESP32 có WiFi và Bluetooth
  • Tham khảo tài liệu liên quan đến dịch ngược ESP32 trong kho ESP32-reversing
  • Xác định các chân TXD0RXD0 trong datasheet ESP32, rồi lần theo trace nối với các lỗ pin dùng để debug trên PCB để tìm điểm kết nối serial
  • Thiết lập kết nối UART bằng USB-UART Bridge của Flipper Zero
    • Flipper Zero TX nối với ESP32 RX
    • Flipper Zero RX nối với ESP32 TX
    • GND nối với GND
  • Khi kết nối bằng Putty ở COM7, tốc độ 115200, log khởi động được xuất ra

Tệp và cấu hình máy chủ lộ ra từ log khởi động

  • Log serial cho biết ESP32 là chip có 2 lõi CPU, WiFi/BT/BLE và flash ngoài 4MB
  • Ứng dụng đang chạy từ phân vùng factory
  • Hệ thống tệp FAT đã được mount, hiển thị tổng dung lượng 122 KiB và dung lượng khả dụng 0 KiB
  • Ứng dụng đang đọc các tệp sau
    • serial
    • dev_key.key
    • SmartDevice-root-ca.crt
    • SmartDevice-signer-ca.crt
    • server_config
  • Cấu hình máy chủ có chứa smartdeviceep.---.com:41014

Dump flash và cấu trúc phân vùng

  • Để khởi động ESP32 vào chế độ Download Boot, bật nguồn trong khi nối chân IO0 với GND
  • Dùng esptool để dump toàn bộ flash 4MB
    • Lệnh là esptool -p COM7 -b 115200 read_flash 0 0x400000 flash.bin
  • Thực hiện dump nhiều lần để xác nhận việc đọc đúng và sao lưu để có thể flash lại nếu xảy ra sự cố
  • Phân tích bản dump bằng esp32knife để lấy partitions.csv
  • Cấu trúc phân vùng gồm các mục sau
    • nvs: kho khóa-giá trị 16K
    • otadata: dữ liệu OTA 8K
    • phy_init: dữ liệu PHY 4K
    • factory: phân vùng ứng dụng 768K
    • ota_0, ota_1: mỗi phân vùng ứng dụng OTA 768K
    • storage: phân vùng dữ liệu FAT 1M
  • Theo chia sẻ của độc giả, bản dump flash này lẽ ra có thể được bảo vệ nếu mã hóa flash được bật, nhưng trên thiết bị này thì tính năng đó không được bật

Khóa và chứng chỉ tìm thấy trong storage

  • Trạng thái mới nhất của phân vùng nvs có SSID và mật khẩu WiFi, còn log lịch sử cũng cho thấy thông tin đăng nhập WiFi từng được dùng trước đây
  • Phân vùng FAT storage được mount như ổ đĩa ảo bằng OSFMount để kiểm tra
  • Trong storage có các tệp sau
    • dev_info
    • dev_key.key
    • serial
    • server_config
    • SmartDevice-root-ca.crt
    • SmartDevice-signer-ca.crt
    • wifi_config
  • dev_key.keykhóa riêng Elliptic Curve bắt đầu bằng -----BEGIN EC PRIVATE KEY-----, được xác nhận bằng openssl ec -in dev_key.key -text -noout
  • Hai tệp .crt là chứng chỉ bắt đầu bằng -----BEGIN CERTIFICATE-----, được xác nhận bằng openssl x509
  • Vì chứng chỉ và khóa thiết bị được lưu trên thiết bị, nhiều khả năng chúng được dùng để mã hóa dữ liệu gói UDP

Cấu hình môi trường phân tích Ghidra

  • Mở image phân vùng factory đang chạy trong CodeBrowser của Ghidra để phân tích
  • Vì ESP32 dùng tập lệnh Xtensa, chọn ngôn ngữ Tensilica Xtensa 32-bit little-endian
  • Image phân vùng thô không phản ánh đúng ánh xạ bộ nhớ ảo, nên dùng esp32knife để tạo part.3.factory.elf rồi import lại
  • Cũng công bố commit sửa esp32knife để hỗ trợ segment RTC_DATA
  • Dùng SVD-Loader-Ghidra để nạp cấu trúc ngoại vi và memory map của ESP32
  • Dùng SymbolImportScript của Ghidra để nạp nhãn hàm ESP32 ROM, giúp dễ nhận diện các hàm ROM phổ biến như printf

Manh mối mã hóa tìm được qua chuỗi

  • Trong Defined Strings của Ghidra, lần theo chuỗi từng xuất hiện trong log serial và các chuỗi xung quanh
  • Các chuỗi xung quanh có những manh mối sau
    • Message CRC error
    • Seed Error
    • PRNG fail
    • ECDH setup failed
    • mbedtls_ecdh_gen_public failed
    • mbedtls_ecdh_compute_shared failed
    • MBED HKDF failed
    • Write ECC conn packet
  • mbedtls là thư viện mã nguồn mở triển khai các primitive mật mã, thao tác chứng chỉ X509, SSL/TLS và DTLS
  • Việc các hàm ECDH và HKDF được dùng trực tiếp, chứ không phải DTLS, cho thấy quá trình trao đổi khóa và dẫn xuất khóa được triển khai bên trong giao thức riêng
  • Chuỗi ECC conn packet cho thấy gói kết nối đầu tiên có liên quan đến quá trình trao đổi khóa ECDH

Bản vá firmware loại bỏ phụ thuộc vào bảng điều khiển

  • Việc phân tích khi PCB vẫn nối với quạt và bảng điều khiển khá bất tiện, nên đã tháo bảng điều khiển ra; tuy nhiên trong lúc boot, thiết bị panic kèm log No Cap device found!
  • Hàm quanh chuỗi No Cap device found! in ra CapSense Init, nên được xác định là logic khởi tạo đầu vào điện dung của mặt trước
  • Trong Ghidra, đặt tên hàm đó là InitCapSense và service gọi nó là StartCapSenseService
  • Đổi lệnh gọi StartCapSenseService thành nop để loại bỏ việc khởi động service bảng điều khiển
  • Đã sửa byte trong image thô part.3.factory và flash lại vào offset 0x10000, nhưng thiết bị không boot do lỗi checksum image ESP32
  • Dựa trên logic nội bộ của esptool, thêm script sửa checksum phân vùng app
  • Sau khi flash image đã khôi phục checksum, thiết bị hoạt động bình thường dù không có bảng điều khiển, và việc sửa firmware thành công

Header gói tin và cấu trúc CRC

  • Sau khi boot nhiều lần và so sánh các gói tin, 13 byte đầu khá giống nhau, còn phần còn lại trông như đã được mã hóa
  • Định dạng header gói tin như sau
    • 55: magic byte để nhận diện giao thức
    • 00 31: độ dài gói tin
    • 02: mã định danh thông điệp
    • 01 23 45 67 89 AB CD EF FF: serial thiết bị 9 byte
  • Mẫu message ID như sau
    • 0x02: gói tin đầu tiên do thiết bị thông minh gửi
    • 0x82: phản hồi đầu tiên do máy chủ cloud gửi
    • 0x01: các gói tin sau đó do thiết bị thông minh gửi
    • 0x81: các phản hồi sau đó do máy chủ gửi
  • Bit cao phân biệt request của client và response của server, còn bit thấp phân biệt lần trao đổi đầu tiên với các gói tin sau đó
  • Lần theo hàm tham chiếu đến chuỗi Message CRC error để xác nhận logic kiểm tra CRC
  • 2 byte cuối là checksum CRC-16 cho toàn bộ phần còn lại của gói tin
    • Đa thức là 0x1021
    • Giá trị khởi tạo là 0xFFFF
    • Đã được xác minh theo cùng cách trên nhiều gói tin capture

Luồng tạo khóa ECDH/HKDF

  • Trong gói tin có vẻ là trao đổi khóa đầu tiên, dữ liệu sau khi loại trừ header 13 byte và CRC 2 byte là 32 byte, khớp với kích thước khóa công khai 256 bit
  • Request của client có tiền tố 00 01, và giá trị này không thay đổi qua mỗi lần boot, nên được xem như một mô tả dữ liệu
  • Trong Ghidra, lần theo chuỗi lỗi để tìm hàm tạo khóa, rồi so sánh với mã nguồn mbedtls và tóm tắt ở mức pseudocode
  • Hàm tạo khóa thực hiện các thao tác sau
    • Tạo cặp khóa ECDH bằng mbedtls_ecdh_gen_public
    • Có dấu hiệu khóa được tạo bị ghi đè bằng một khóa khác trong bộ nhớ
    • Nạp một khóa công khai khác
    • Tính shared secret bằng mbedtls_ecdh_compute_shared
    • Tạo giá trị ngẫu nhiên 32 byte bằng mbedtls_ctr_drbg_random
    • Dẫn xuất khóa cuối cùng bằng mbedtls_hkdf
  • Cấu hình HKDF như sau
    • Hash: SHA-256
    • salt: shared secret ECDH
    • input: giá trị ngẫu nhiên 32 byte do thiết bị tạo
    • info: serial thiết bị 9 byte
    • Kích thước khóa đầu ra: 0x10, tức 16 byte
  • Hàm gọi ghép giá trị ngẫu nhiên 32 byte sau 00 01 rồi gửi 0x22 byte, khớp với định dạng gói trao đổi khóa đầu tiên đã capture

Xuất shared secret và giải mã AES

  • Để tính khóa giải mã cuối cùng, cần có shared secret ECDH
  • Thay vì debug bằng JTAG, firmware được vá bằng cách ghi đè một hàm tùy chỉnh vào vị trí logic CapSense đã vô hiệu hóa để in shared secret ra serial
  • Chèn lệnh gọi hàm ngay sau khi shared secret được tạo trong GenerateNetworkKey, rồi dùng con trỏ khóa trong thanh ghi để in 32 byte
  • Khi boot, shared secret được in ở dạng hex sau Write ECC conn packet, và giá trị này không đổi dù reboot nhiều lần
  • Khóa đầu ra HKDF cũng được xác nhận bằng một bản vá riêng, nhờ đó có thể tái hiện cùng logic tạo khóa từ các gói tin capture
  • Trong hàm mã hóa, phát hiện một bảng tĩnh bắt đầu bằng 63 7C 77 7B F2 6B 6F C5, khớp với AES Forward S-Box của mbedtls
  • Phương thức mã hóa cuối cùng là AES-128-CBC, và giá trị ngẫu nhiên 16 byte trong gói tin được dùng làm IV
  • Trong các gói tin đã giải mã, xác nhận được các giá trị có thể đọc được như mirror_data_get, FAN_SPEED, BOOST, FILTER1, FILTER2

Triển khai proxy MITM

  • Sau khi có private key của thiết bị và logic dẫn xuất khóa, đồng thời dữ liệu động cần thiết đều lộ trên mạng, có thể viết proxy MITM mà không cần vá firmware
  • Script Node.js tạo một socket UDP cục bộ và một socket UDP cho máy chủ cloud, rồi chuyển tiếp gói tin hai chiều
  • Gói tin nhận từ thiết bị thông minh được ghi log rồi gửi đến máy chủ cloud; gói tin nhận từ máy chủ cloud được ghi log rồi gửi đến thiết bị thông minh
  • Xem gói tin có messageId bằng 2 là gói trao đổi khóa, và dùng giá trị ngẫu nhiên trong đó để tính khóa AES cho các gói tin tiếp theo
  • Khi điều khiển thiết bị bằng app di động, tích lũy log MITM để xác định dạng request và response cần thiết cho việc triển khai máy chủ cục bộ

Cấu trúc thông điệp MessagePack

  • Dữ liệu sau khi giải mã vẫn là một định dạng tuần tự hóa nhị phân
  • Header dữ liệu nội bộ trông giống như ID và độ dài ở dạng little-endian
    • 01 00: ID gói tin
    • 64 00: ID giao dịch
    • 29 00: độ dài dữ liệu tuần tự hóa
  • Định dạng tuần tự hóa ban đầu được tự reverse engineering một phần, nhưng sau khi kiểm tra thì hóa ra là MessagePack
  • Khi dùng một implementation như msgpackr, có thể dễ dàng giải dữ liệu nhị phân thành dạng JSON
  • Các thông điệp chính đã xác nhận như sau
    • Trao đổi khóa: thiết bị gửi các byte ngẫu nhiên dùng cho HKDF tới server
    • mirror_data_get: khi khởi động, lấy trạng thái ban đầu từ server
    • connect: gửi UUID firmware hiện tại, server phản hồi thông tin về firmware, cấu hình, thời gian và địa chỉ server
    • mirror_data: server thay đổi trạng thái thiết bị, hoặc thiết bị báo cáo trạng thái đã thay đổi lên server
    • keep_alive: thiết bị định kỳ gửi trạng thái như RSSI, RTT, số gói bị drop, số lần kết nối, uptime, v.v.

MQTT bridge và tích hợp Home Assistant

  • Sử dụng MQTT để kết nối Home Assistant với server tùy chỉnh
  • Trên Home Assistant, cấu hình add-on Mosquitto, một MQTT broker mã nguồn mở
  • Cấu trúc kết nối là Home AssistantMQTT BrokerCustom ServerSmart Device
  • Server tùy chỉnh hoạt động theo cách sau
    • Khi thiết bị yêu cầu trạng thái bằng mirror_data_get, server phản hồi bằng retained value của MQTT broker hoặc giá trị mặc định
    • Khi Home Assistant gửi lệnh thay đổi trạng thái qua MQTT topic, server tùy chỉnh chuyển tiếp lệnh đó đến thiết bị
    • Nếu trạng thái thiết bị thay đổi vì bất kỳ lý do nào, gói mirror_data của thiết bị sẽ được publish lên MQTT broker và được retain
  • Source of truth của trạng thái luôn là thiết bị
    • Nếu cập nhật trạng thái thất bại, không hiển thị trên MQTT broker như thể đã được cập nhật
    • Ngay cả khi trạng thái thay đổi bằng bảng điều khiển vật lý, thay đổi đó cũng được phản ánh lên MQTT broker
  • Dùng tích hợp MQTT Fan của Home Assistant để map máy lọc không khí thành một thiết bị quạt
  • Trong configuration.yaml, cấu hình topic trạng thái nguồn, topic lệnh, topic trạng thái tốc độ quạt, topic lệnh tốc độ quạt và dải tốc độ từ 1 đến 4
  • Cấu hình DNS cục bộ Pi-hole để phân giải domain cloud của nhà sản xuất về server tùy chỉnh, nhờ đó server cục bộ đóng vai trò server của thiết bị

Đánh giá bảo mật và kết quả

  • Nhà sản xuất đã triển khai giao thức riêng thay vì các giao thức tiêu chuẩn như DTLS
  • Không chắc mỗi thiết bị có khóa riêng duy nhất hay không, nhưng trường hợp nào cũng có nhược điểm
    • Nếu mọi thiết bị dùng chung khóa riêng trong firmware, chỉ cần reverse engineering một thiết bị là có thể thử tấn công MITM lên các thiết bị khác
    • Nếu mỗi thiết bị có khóa riêng duy nhất, server phải lưu ánh xạ giữa số serial và khóa thiết bị; nếu dữ liệu đó bị mất, server sẽ không thể phản hồi giao tiếp với thiết bị
  • Vì firmware chứa khóa riêng tĩnh, kẻ tấn công có thể lấy khóa từ một bản dump firmware duy nhất và thực hiện tấn công MITM
  • Việc triển khai này không hoàn toàn tệ xét từ góc độ bảo mật, và cuộc tấn công vẫn cần quyền truy cập vật lý
  • Triển khai riêng khiến giao tiếp mạng trở nên khó quan sát, nhưng Security through obscurity chỉ gần như ngăn tạm thời các cuộc tấn công phổ biến nhắm vào triển khai tiêu chuẩn, còn với kẻ tấn công thì đó là một rào cản có thể vượt qua
  • Mục tiêu cuối cùng là tích hợp với Home Assistant đã đạt được, và máy lọc không khí đã hoạt động không vấn đề trong vài tuần
  • Cũng đã cấu hình tự động hóa để boost máy lọc không khí trong một khoảng thời gian khi chỉ số PM2.5 hoặc VOC từ một thiết bị giám sát không khí riêng tăng quá cao

Chưa có bình luận nào.

Chưa có bình luận nào.