Hack thiết bị nhà thông minh (2024)
(jmswrnr.com)- Để đ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:41014và 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-CBCvà 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
.apkcủa ứng dụng Android, mởclasses.dexbằng dex2jar và jd-gui để xem bên trong - Trong
MainActivity.class, xác nhận ứng dụng dựa trên React Native; trongassets/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
- Mã ví dụ có chứa kết nối
- 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 DNScủ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.61trong Wireshark - Thiết bị đang gửi gói UDP đến cổng
41014củ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
TXD0vàRXD0trong 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 Bridgecủa Flipper Zero- Flipper Zero
TXnối với ESP32RX - Flipper Zero
RXnối với ESP32TX GNDnối vớiGND
- Flipper Zero
- 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 KiBvà dung lượng khả dụng0 KiB - Ứng dụng đang đọc các tệp sau
serialdev_key.keySmartDevice-root-ca.crtSmartDevice-signer-ca.crtserver_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ânIO0vớiGND - Dùng esptool để dump toàn bộ flash 4MB
- Lệnh là
esptool -p COM7 -b 115200 read_flash 0 0x400000 flash.bin
- Lệnh là
- 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ị 16Kotadata: dữ liệu OTA 8Kphy_init: dữ liệu PHY 4Kfactory: phân vùng ứng dụng 768Kota_0,ota_1: mỗi phân vùng ứng dụng OTA 768Kstorage: 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
nvscó 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_infodev_key.keyserialserver_configSmartDevice-root-ca.crtSmartDevice-signer-ca.crtwifi_config
dev_key.keylà khóa riêng Elliptic Curve bắt đầu bằng-----BEGIN EC PRIVATE KEY-----, được xác nhận bằngopenssl ec -in dev_key.key -text -noout- Hai tệp
.crtlà chứng chỉ bắt đầu bằng-----BEGIN CERTIFICATE-----, được xác nhận bằngopenssl 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.elfrồ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
SymbolImportScriptcủ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 Stringscủ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 errorSeed ErrorPRNG failECDH setup failedmbedtls_ecdh_gen_public failedmbedtls_ecdh_compute_shared failedMBED HKDF failedWrite 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 packetcho 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 raCapSense 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à
InitCapSensevà service gọi nó làStartCapSenseService - Đổi lệnh gọi
StartCapSenseServicethànhnopđể loại bỏ việc khởi động service bảng điều khiển - Đã sửa byte trong image thô
part.3.factoryvà flash lại vào offset0x10000, 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ức00 31: độ dài gói tin02: mã định danh thông điệp01 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ửi0x82: phản hồi đầu tiên do máy chủ cloud gửi0x01: các gói tin sau đó do thiết bị thông minh gửi0x81: 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
- Đa thức là
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
- Tạo cặp khóa ECDH bằng
- Cấu hình HKDF như sau
- Hash:
SHA-256 salt: shared secret ECDHinput: giá trị ngẫu nhiên 32 byte do thiết bị tạoinfo: serial thiết bị 9 byte- Kích thước khóa đầu ra:
0x10, tức 16 byte
- Hash:
- Hàm gọi ghép giá trị ngẫu nhiên 32 byte sau
00 01rồi gửi0x22byte, 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ó
messageIdbằng2là 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 tin64 00: ID giao dịch29 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ừ serverconnect: 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ỉ servermirror_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 serverkeep_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 Assistant↔MQTT Broker↔Custom Server↔Smart 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_datacủa thiết bị sẽ được publish lên MQTT broker và được retain
- Khi thiết bị yêu cầu trạng thái bằng
- 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đến4 - 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.