Trong xu thế phát triển mạnh mẽ của nhà thông minh, việc tích hợp các thiết bị cũ hoặc các sản phẩm có API độc quyền, giới hạn vào hệ sinh thái hiện có luôn là một thách thức lớn. Đối với người dùng đam mê tự động hóa và muốn kiểm soát hoàn toàn hệ thống của mình, những rào cản này đôi khi đòi hỏi một cách tiếp cận sâu hơn. Đó chính là lý do dẫn đến hành trình đảo ngược kỹ thuật (reverse engineering) đèn LED dây Govee H615B – một nỗ lực nhằm vượt qua những hạn chế về API và khai thác khả năng điều khiển Bluetooth LE của nó để tích hợp mượt mà vào Home Assistant.
Mặc dù Govee cung cấp cả API dựa trên web và API cục bộ, nhưng cả hai đều không đáp ứng được nhu cầu điều khiển linh hoạt. API web bị giới hạn tốc độ rất nhanh, khiến việc điều chỉnh độ sáng hoặc màu sắc trở nên khó chịu. Trong khi đó, API cục bộ lại không thể kích hoạt trên dòng đèn H615B, dù tùy chọn này tồn tại nhưng bị làm mờ trong ứng dụng. Điều này đã thôi thúc tôi bắt tay vào một dự án đảo ngược kỹ thuật đầy thú vị, sử dụng Wireshark và Python, để khám phá cách thức hoạt động của những chiếc đèn này và liệu chúng có thể được điều khiển từ bất kỳ thiết bị Bluetooth nào hay không. Toàn bộ mã nguồn sử dụng trong bài viết này đều là mã nguồn mở và sẽ được cung cấp ở cuối bài.
Đánh Giá Vấn Đề và Công Cụ
Xác định mục tiêu và nguồn lực
Bước đầu tiên trong bất kỳ dự án đảo ngược kỹ thuật nào là đánh giá kỹ lưỡng vấn đề, xác định các công cụ sẵn có và mục tiêu cuối cùng. Đối với đèn Govee H615B, tôi đã có trong tay một bộ công cụ sau:
- Một chiếc MacBook M4 Pro mạnh mẽ.
- Một chiếc Google Pixel 8 Pro.
- Ứng dụng Govee chính thức (có khả năng điều khiển đèn qua Bluetooth LE khi không có kết nối mạng).
- Phần mềm phân tích mạng Wireshark.
- Bleak, một framework Bluetooth được viết bằng Python.
- Một bo mạch phát triển Milk-V Duo S (một vi điều khiển có cả nhân Arm và RISC-V, hỗ trợ Python, tích hợp Wi-Fi/Bluetooth).
MacBook Pro M4 Pro 16 inch – một trong những công cụ chính để phân tích và đảo ngược kỹ thuật thiết bị nhà thông minh Govee.
Mục tiêu chính là tránh việc phải root chiếc Google Pixel 8 Pro nếu có thể, và tôi tin rằng điều này là hoàn toàn khả thi. Nếu không có cơ chế xác thực phức tạp khi điều khiển đèn, tôi hoàn toàn có thể tự phát các lệnh của riêng mình. Cuối cùng, tôi muốn viết một script Python chạy trên Milk-V Duo S, với một máy chủ chấp nhận lệnh từ Home Assistant để phát sóng đến đèn.
Thu Thập Dữ Liệu Bluetooth
Ghi lại gói tin Bluetooth HCI trên Android
Để hiểu cách điều khiển đèn từ xa, giải pháp tốt nhất là ghi lại các gói tin Bluetooth được gửi đi và nhận về. Đây là lý do tôi sử dụng Google Pixel 8 Pro. Mặc dù có các phần cứng chuyên dụng để “ngửi” (sniff) gói tin Bluetooth đang truyền tải, nhưng tôi không có chúng. May mắn thay, Android có một trình ghi nhật ký Bluetooth HCI (Host Controller Interface) tích hợp, hoạt động rất hiệu quả. Nhật ký này ghi lại mọi hoạt động ở cấp độ rất thấp mà điện thoại gửi đi và nhận về. Bạn có thể kích hoạt tùy chọn này trong Tùy chọn nhà phát triển (Developer options).
Câu hỏi đặt ra là tại sao lại chọn Google Pixel 8 Pro mà không phải các thiết bị khác như Oppo Find N5. Vấn đề nằm ở nơi lưu trữ nhật ký. Thông thường, bạn có thể truy cập thông qua adb
bằng cách tạo một báo cáo lỗi (bug report), và nhật ký sẽ được lưu trong thư mục nhật ký Bluetooth. Tuy nhiên, trên chiếc Oppo Find N5 của tôi, tệp tin nhật ký lại trống rỗng. Vì nhiều nhà sản xuất tùy biến hệ thống Android, tôi đã quyết định chuyển sang Google Pixel 8 Pro để tránh những “trò đùa” từ OEM, và chiếc Pixel đã giúp tôi thu thập được nhật ký Bluetooth HCI chứa đầy đủ dữ liệu.
Điện thoại Google Pixel 8 Pro, thiết bị được sử dụng để ghi lại nhật ký Bluetooth HCI phục vụ quá trình phân tích gói tin điều khiển đèn Govee.
Trước khi kéo nhật ký, cần phải điền dữ liệu hữu ích vào đó. Tôi đã cài đặt ứng dụng Govee, đăng nhập tài khoản và sau đó tắt Wi-Fi. Việc này buộc ứng dụng phải sử dụng Bluetooth để điều khiển đèn, đảm bảo dữ liệu được ghi vào nhật ký. Sau khi thay đổi độ sáng, màu sắc và bật tắt đèn nhiều lần, đã đến lúc cắm Pixel 8 Pro vào máy tính xách tay và kéo báo cáo lỗi bằng adb
.
Phân Tích Gói Tin Với Wireshark
Bắt đầu với những điều cơ bản
Tệp nhật ký Bluetooth HCI sẽ được tìm thấy trong FS/data/misc/bluetooth/logs
và có phần mở rộng .cfa
sau khi giải nén tệp báo cáo lỗi. Tệp .cfa
này là một tệp BTSnoop chứa các gói tin L2CAP. Những gói tin này mô tả chi tiết tất cả các giao tiếp mà thiết bị của bạn thực hiện qua Bluetooth và thường được các thiết bị Bluetooth Low Energy (BLE) sử dụng để giao tiếp. Trong trường hợp cụ thể này, việc xác định thiết bị cần xem xét khá dễ dàng, nhưng tôi cũng sử dụng thư viện Bleak của Python để quét các thiết bị và ID của chúng.
Tôi đã viết một bộ quét đơn giản bằng Bleak, và khi xác định được UUID của thiết bị cần thiết, tôi có thể truy vấn nó để tìm các đặc tính (characteristics). Các đặc tính về cơ bản là các hồ sơ. Một client có thể khởi tạo các lệnh nhắm mục tiêu một đặc tính và có thể nhận phản hồi, trong khi một server có thể chấp nhận các lệnh đó và thực hiện chúng. Lưu ý thêm, macOS và framework CoreBluetooth của nó sẽ cung cấp cho bạn một UUID cho các thiết bị Bluetooth Low Energy thay vì địa chỉ MAC để giao tiếp. Điều này không sao, nhưng cần lưu ý nếu bạn đang viết mã để chuyển sang thiết bị khác sau này. Địa chỉ MAC mà bạn cần sử dụng (thay vì UUID) sẽ có trong nhật ký.
Giao diện công cụ Bleak trong Python, hiển thị quá trình quét và truy vấn các đặc tính (characteristics) của thiết bị Bluetooth LE Govee H615B.
Trong quá trình quét, thật dễ dàng nhận ra Govee H615B. Tôi đã xác định được một số đặc tính, cung cấp thông tin cần thiết để tiếp tục điều tra trong nhật ký đã sao chép từ điện thoại. Chúng bao gồm:
- Handle:
0x0009
, UUID:00010203-0405-0607-0809-0a0b0c0d2b10
- Handle:
0x000d
, UUID:00010203-0405-0607-0809-0a0b0c0d2b11
- Handle:
0x0012
, UUID:f000ffc1-0451-4000-b000-000000000000
- Handle:
0x0016
, UUID:f000ffc2-0451-4000-b000-000000000000
Lưu ý rằng hai đặc tính kết thúc bằng “b10” và “b11” có khả năng liên quan đến nhau, cũng như hai đặc tính có “c1” và “c2”. Chúng ta sẽ tập trung vào hai đặc tính kết thúc bằng “b10” và “b11”, vì qua nghiên cứu các thiết bị Govee khác, dường như hai đặc tính này liên quan đến việc thiết lập trạng thái cho đèn, và các thiết bị khác cũng có chuỗi ký tự tương tự.
Đây là một bối cảnh khác khi nói đến các thiết bị này: mặc dù tất cả chúng đều tương tự nhau về cách tương tác, nhưng dường như chúng hơi khác nhau về cách chấp nhận lệnh. Một số có phân đoạn cho các phần khác nhau của dải đèn (để chúng có thể được điều khiển riêng lẻ), và một số có cơ chế xác thực trong quá trình ghép nối. Tôi đặc biệt lo lắng về bước xác thực này, nhưng tôi phát hiện ra rằng khi chiếc Pixel 8 Pro của tôi kết nối (và đó là lần đầu tiên tôi kết nối nó), không có bất kỳ kiểm tra nào về thiết bị đang gửi lệnh. Đây có thể coi là một lỗ hổng bảo mật nhỏ (mặc dù bề ngoài không gây hại nhiều), nhưng chúng ta sẽ có thể tận dụng nó.
Phát hiện gói tin “Keep-Alive” và “Notification”
Một điều tôi tìm thấy trong quá trình kết nối ban đầu giữa điện thoại và đèn đã trở thành một phần quan trọng của câu đố để làm cho mọi thứ hoạt động. Nhớ đặc tính tôi đã đề cập kết thúc bằng “b10” không? Thiết bị client (Pixel 8 Pro trong trường hợp này) được bộ điều khiển Bluetooth của đèn gửi một thông báo, và nó đến từ đặc tính kết thúc bằng “b10”. Giờ đây, chúng ta biết rằng chúng ta cần lắng nghe dịch vụ này, vì vậy chúng ta sẽ ghi nhớ điều đó khi viết mã để kết nối với nó sau này.
Gói tin thông báo (notification) từ đặc tính (characteristic) "b10" của đèn Govee H615B được ghi lại trong Wireshark, một phần quan trọng của giao tiếp Bluetooth LE.
Tiếp theo, tôi nhận thấy nhiều gói tin được gửi từ chiếc Pixel của tôi đến đèn có giá trị sau:
aa010000000000000000000000000000000000ab
Những gói tin này dường như là các gói tin “keep-alive”, thông báo cho đèn Govee rằng chúng ta vẫn đang muốn gửi và nhận thông tin. Chúng được gửi khoảng hai giây một lần và chiếm phần lớn nhật ký. Tôi nhận thấy rằng khi tôi kết nối với đèn bình thường bằng Bleak, chúng sẽ ngắt kết nối khỏi máy tính xách tay của tôi chỉ trong vài giây. Vì điện thoại của tôi dường như không có bất kỳ liên lạc nào khác trong thời gian dài ngoài việc gửi các giá trị tương tự, tôi nghĩ chúng hẳn là các gói tin “keep-alive”. Điều này cũng phù hợp với những gì các thiết bị Govee khác dường như đang làm.
Mặc dù tôi không chắc dữ liệu sau “aa” là gì, nhưng byte cuối cùng của chuỗi (tức là hai ký tự cuối cùng) rất quan trọng. Các gói tin Bluetooth LE có độ dài 20 byte (không có MTU mở rộng), và dường như các gói tin này sử dụng đệm bằng số 0 để đạt được độ dài gói tin đó. Chúng ta có thể kết luận tại thời điểm này rằng các gói tin bắt đầu bằng 0xaa
biểu thị một gói tin “keep-alive”, nhưng còn hai chữ số cuối cùng thì sao? Chúng ta sẽ tìm hiểu chúng sau.
Phân tích gói tin bật/tắt nguồn
Tiếp theo, chúng ta sẽ xem xét các lệnh mà tôi đã gửi. Khi tôi mở ứng dụng lần đầu và kết nối với đèn, tôi đã bật và tắt chúng. Trong nhật ký, phần dữ liệu đầu tiên được truyền tải và không giống gói tin “keep-alive” hoặc liên quan đến kết nối ban đầu là gói tin ở trên. Tôi đã tắt chúng một lần nữa, và tôi tìm thấy một giá trị rất giống với giá trị trên nhưng hơi khác, và điều này khớp với thời gian tôi ghi chú. Sau đó, tôi có thể suy ra rằng hai giá trị để bật và tắt đèn như sau:
- Bật đèn:
3301010000000000000000000000000000000033
- Tắt đèn:
3301000000000000000000000000000000000032
Gói tin điều khiển bật nguồn (Power On) của đèn Govee H615B được hiển thị chi tiết trong Wireshark, cho thấy cấu trúc dữ liệu hex.
Lưu ý cấu trúc của gói tin; một lần nữa, chúng ta có rất nhiều byte không và hai byte khác nhau ở cuối. Để dễ hiểu, chúng ta sử dụng “0x” để biểu thị giá trị thập lục phân (hexadecimal), sử dụng cơ số 16. “0x” làm rõ rằng chúng ta đang nói về thập lục phân, chứ không phải số thập phân thông thường, và 0x33
là “51” trong thập phân. 0x33
dường như biểu thị một lệnh, với dữ liệu sau đó đưa ra hướng dẫn để thực thi. Trong trường hợp này, chúng ta có 0x33
, 0x01
và 0x01
để bật đèn và 0x33
, 0x01
và 0x00
để tắt đèn, cho thấy rằng một giá trị boolean ở byte thứ ba điều khiển trạng thái bật/tắt. Chúng ta đã có đủ thông tin để thử nghiệm bằng cách thiết lập bộ thu thông báo của mình và về cơ bản lặp lại các hướng dẫn này cho đèn. Chúng ta không cần lo lắng về hai chữ số cuối cùng vì chúng đã được tính toán cho chúng ta, nhưng chúng ta cũng sẽ tìm cách tính toán của riêng mình.
Điều Khiển Màu Sắc và Độ Sáng
Phân tích gói tin màu sắc
Hiện tại, chúng ta đã có thể bật và tắt đèn qua Bluetooth. Đó là một bước tiến khá lớn, nhưng những chiếc đèn thông minh đầy màu sắc như thế này không chỉ có công tắc bật/tắt. Tôi đã điều tra về màu sắc và độ sáng, vì tôi cũng đã thay đổi cả hai trong ứng dụng để xem chúng sẽ trông như thế nào ở cấp độ gói tin. Tôi đã tìm thấy gói tin sau:
33050dfe0e1f000000000000000000000000d4
Lại là byte 0x33
bắt đầu, vì vậy chúng ta biết chúng ta đang nhận được một lệnh. Tôi không chắc chắn về 050d
, nhưng “fe0e1f” trông giống một mã hex màu. Khi tôi chuyển đổi nó từ hex sang một màu thực tế, nó hiện ra rất rõ ràng là màu đỏ, và tôi đã đổi đèn của mình thành màu đỏ trong quá trình thử nghiệm. Tôi muốn thử xem liệu tôi có thể thay thế “fe0e1f” bằng màu của riêng mình hay không, nhưng có một vấn đề. Trước đây, chúng ta chỉ cần phát lại các gói tin cho đèn, và chúng sẽ thực hiện lại các lệnh mà chúng ta đã thấy trong nhật ký của mình. Làm thế nào để chúng ta tạo ra các lệnh mới? Chúng ta không thể chỉ đơn giản là hoán đổi các giá trị hex màu của riêng mình. Lý do chúng ta không thể là do sự hiện diện của byte cuối cùng đó.
Gói tin điều khiển màu sắc toàn cầu của đèn Govee H615B khi thay đổi màu, được phân tích bằng Wireshark để xác định mã màu và checksum.
Byte cuối cùng đó là một checksum, về cơ bản xác nhận rằng dữ liệu đã đến trong tình trạng hoàn chỉnh và không bị lỗi. Nó được tính bằng cách thực hiện phép toán XOR tích lũy trên mỗi byte. Một phép toán XOR là một loại cổng logic tạo ra ‘1’ khi hai giá trị đầu vào khác nhau. Mỗi byte sau đó được XOR với byte trước đó cho đến khi đạt đến byte thứ 19. Kết quả tính toán cuối cùng được thêm vào cuối gói tin vào byte thứ 20, và điều này được gửi đến thiết bị. Cuối cùng, thiết bị thực hiện phép toán XOR của riêng mình trên 19 byte đầu tiên, kiểm tra xem byte cuối cùng có khớp với những gì nó đã tính toán hay không. Nếu có, nó biết dữ liệu đã đến như dự định và an toàn để thực thi.
Hãy thử thay đổi đèn thành màu magenta, mã hex #FF00FF
. Nó sẽ trông như sau:
33050d[ff00ff]00000000000000000000000d4[checksum]
ff00ff
(đặt trong dấu ngoặc vuông ở trên để dễ hiểu) là mã màu của chúng ta, và [checksum]
là thứ chúng ta muốn tính toán. Chúng ta bắt đầu với giá trị của một byte trống, hoặc 00000000
.
- Bắt đầu với
0x00
(binary:00000000
). - XOR với
0x33
(00110011
), kết quả:0x33
(00110011
). - XOR với
0x05
(00000101
), kết quả:0x36
(00110110
). - Tiếp tục XOR từng byte trong thông điệp với byte trước đó.
- Kết quả cuối cùng:
0xff
(binary:11111111
).
Chúng ta thực hiện phép tính trên toàn bộ chuỗi cho đến khi đến cuối cùng. Điều này tạo ra giá trị là ff
, với giá trị nhị phân là 11111111
. Giá trị cuối cùng mà chúng ta sẽ gửi cho đèn là:
33050dff00ff00000000000000000000000d4ff
Nhưng việc tính toán này không tiện lợi mỗi khi chúng ta muốn thay đổi màu đèn. Thay vào đó, chúng ta có thể tự động hóa quá trình này, điều mà tôi đã làm trong Python. Tôi đã triển khai một phương thức nhận các giá trị hex RGB, chèn chúng vào chuỗi, và sau đó tính toán checksum để thêm vào phần còn lại của chuỗi. Tôi sẽ không đi sâu vào chi tiết, vì nó chỉ đơn thuần là triển khai phép tính chúng ta đã làm ở trên một cách lập trình để có được một checksum mới mỗi khi chúng ta gửi lệnh thay đổi màu.
Phân tích gói tin độ sáng
Cuối cùng, hãy xem xét độ sáng. Sử dụng cùng một quy trình, tôi phát hiện ra rằng việc đặt độ sáng dường như là lệnh sau:
3304[brightness]00000000000000000000000000000000[checksum]
Độ sáng là một byte duy nhất, dao động từ 00
(0) đến FF
(255). Checksum lại phải được tính toán, nhưng điều này dễ dàng thực hiện vì chúng ta đã tìm ra cách tính toán nó. Để đặt độ sáng là 100%, ví dụ, chỉ cần:
3304ff00000000000000000000000000000000c8
Giờ đây chúng ta đã hoàn toàn tìm ra cách điều khiển đèn của mình! Chúng ta có thể:
- Bật và tắt đèn H615B.
- Đặt màu sắc.
- Đặt độ sáng.
Và chúng ta có thể làm tất cả những điều này mà không cần sử dụng ứng dụng chính thức! Nó bỏ qua một API dựa trên đám mây, giải quyết vấn đề không thể sử dụng API cục bộ, và có nghĩa là chúng ta có thể tự động hóa việc điều khiển chúng từ một thiết bị khác bằng cách tích hợp chúng vào nhà thông minh của mình.
Đảo Ngược Kỹ Thuật Là Một Quá Trình Dài, Thú Vị
Bạn sẽ đối mặt với thất bại, nhưng đừng để chúng cản bước
Đảo ngược kỹ thuật có thể khó khăn, và bạn có thể gặp nhiều trở ngại trong quá trình thực hiện. Có vô số tài nguyên ngoài kia để cố gắng giúp đỡ bạn, nhưng khả năng cao là nếu bạn đang đảo ngược kỹ thuật một thứ gì đó, thì bạn đang làm điều đó vì chưa ai khác làm. Tôi đã có thể tổng hợp tất cả dữ liệu đã thu thập được thông qua quá trình này và xây dựng một trang web để điều khiển những chiếc đèn này trong trình duyệt của mình, nhưng tôi đã may mắn khi đây là một quá trình tương đối đơn giản để xác định những gì tôi cần thay đổi và cách thức.
Giao diện trang web tùy chỉnh để điều khiển đèn Govee H615B, minh họa khả năng điều khiển thiết bị thông qua Bluetooth sau quá trình đảo ngược kỹ thuật.
Mục tiêu của bài viết này là hướng dẫn bạn các bước cần thiết khi đảo ngược kỹ thuật một thứ như thế này. Có rất nhiều thiết bị thông minh giá rẻ trên thị trường tương tự như Govee H615B yêu cầu một ứng dụng độc quyền để điều khiển chúng. Tuy nhiên, không phải là không thể tìm ra cách chúng hoạt động, và với hàng giờ hoặc thậm chí hàng ngày lao động miệt mài với máy tính, đôi khi bạn có thể vượt qua và tìm ra cách tự mình điều khiển chúng. Đó chính xác là những gì tôi đã làm. Điều bắt đầu như một dự án cuối tuần nhỏ vui vẻ cuối cùng đã trở thành một cuộc phiêu lưu kéo dài nhiều ngày mà tôi biết mình muốn tìm hiểu đến cùng.
Từ thời điểm này, việc triển khai một cơ chế điều khiển từ Home Assistant để bạn có thể sử dụng những chiếc đèn này như bất kỳ chiếc đèn nào khác là điều dễ dàng. Ví dụ, bạn có thể triển khai một REST API trong Flask và sau đó sử dụng tích hợp rest_command
trong Home Assistant để gửi lệnh. Cuối cùng, hãy tạo một script sẽ bật hoặc tắt nó, và bạn có thể xây dựng một template switch, một input slider, hoặc một điều khiển Lovelace tùy chỉnh cho nguồn, độ sáng và màu sắc. Đây là những gì tôi đã làm khi triển khai nó trên Milk-V Duo S của mình, và nó hoạt động hoàn hảo. Mặc dù không được khám phá trong bài viết này, bạn cũng có thể đánh giá trạng thái hiện tại của đèn Govee (đang bật hay tắt) bằng cách in dữ liệu được phát sóng bởi nó khi quét.
Nếu bạn sở hữu những chiếc đèn này, bạn có thể truy cập kho lưu trữ GitHub của tôi để tự mình điều khiển chúng từ bất kỳ thiết bị nào có hỗ trợ Bluetooth: https://github.com/Incipiens/Govee-H615B-Reverse-Engineered-Controls. Tất cả những gì bạn cần là địa chỉ MAC (mà bạn có thể lấy bằng một ứng dụng như nRF Connect trên điện thoại của mình), và phần còn lại sẽ hoạt động. Đây là một quá trình giáo dục vô cùng hữu ích, và tôi hy vọng rằng điều này sẽ truyền cảm hứng cho bạn để xem xét kỹ hơn các thiết bị xung quanh mình để tìm hiểu điều gì khiến chúng hoạt động và làm thế nào bạn có thể tự mình kiểm soát chúng!
Hãy chia sẻ ý kiến hoặc kinh nghiệm của bạn về việc đảo ngược kỹ thuật các thiết bị thông minh trong phần bình luận bên dưới!