Logi Options+ 逆向解析藍牙通訊
前言
最近買了一把 Logitech 的人體工學垂直滑鼠 Lift。
下載了 Logi Options+ 來安裝,沒想到它只適用 macOS 10.15 以上版本。
用了些技巧成功安裝後,發現它超肥,GUI + LaunchAgent 將近 1GB,而且很吃資源。
雖然 GUI 界面漂亮,但日常使用根本不會打開幾次,而且設定過的滑鼠跟我開發的手勢 App 發生衝突。
我尋思這一點都不優雅,不如藉此機會更新自己的 App 來支援這個新玩具。
實作過程
修改 Logi Options+ 安裝程式
可以從 官網 下載,看了一下安裝程式才 40MB 不到,Resources 裡也沒有主程式的蹤跡。
我猜測主程式並沒有包含在安裝程式裡,應該是安裝過程中再從伺服器下載。
所以我推斷這個安裝程式應該不會太複雜,總之先丟進 IDA 看看。
找到函式-[AppDelegate _isUnsupportedOS]
:
...
__text:0000000100016B4F mov rdi, cs:classRef_NSProcessInfo ; void *
__text:0000000100016B56 mov rsi, cs:selRef_processInfo ; char *
__text:0000000100016B5D call cs:_objc_msgSend_ptr
__text:0000000100016B63 test rax, rax
__text:0000000100016B66 jz loc_100016C55
__text:0000000100016B6C mov rdx, cs:selRef_operatingSystemVersion
__text:0000000100016B73 lea rdi, [rbp+var_40]
__text:0000000100016B77 mov rsi, rax
__text:0000000100016B7A call _objc_msgSend_stret
...
__text:0000000100016BFD mov rdi, cs:classRef_NSString ; void *
__text:0000000100016C04 mov rcx, qword ptr [rbp+var_40]
__text:0000000100016C08 mov r8, qword ptr [rbp+var_40+8]
__text:0000000100016C0C mov rsi, cs:selRef_stringWithFormat_ ; char *
__text:0000000100016C13 lea rdx, cfstr_DD ; "%d.%d"
__text:0000000100016C1A mov rbx, cs:_objc_msgSend_ptr
__text:0000000100016C21 xor eax, eax
__text:0000000100016C23 call rbx ; _objc_msgSend
__text:0000000100016C25 mov rsi, cs:selRef_compare_options_ ; char *
__text:0000000100016C2C lea rdx, cfstr_1015 ; "10.15"
__text:0000000100016C33 mov ecx, 40h
__text:0000000100016C38 mov rdi, rax ; void *
__text:0000000100016C3B call rbx ; _objc_msgSend
清楚地看到它讀取了ProcessInfo
類別裡的operatingSystemVersion
變數,參考連結。
然後用NSString
類別的compare:options:
實體方法來判斷 macOS 版本,參考連結。
接下來有兩種思考方向,要嘛偽裝 Mac 的 OS 版本,要嘛修改安裝程式的邏輯,我選擇後者。
透過cfstr_1015
順藤摸瓜,找到1008E6F05
:
__cstring:00000001008E6F05 a1015 db '10.15',0
切到 Hex View 透過之前 貼文 的方式修改二進位檔案。
重新簽名之後再打開安裝程式,恭喜可以安裝了。
發現 Logi Options+ 問題
Logi Options+ 是個功能優秀的軟體,不過它為了跨平台,是使用 Electron + Qt 開發。
不僅加了一堆 Frameworks,還要兼容 x86、arm 架構的處理器,導致它的容量巨肥。
我明白廠商使用跨平台開發是出於成本考量,但是近 1GB 的容量佔用真的讓我無法接受。
也因為它的跨平台開發讓我後期的逆向工程超級痛苦。
另外,一般滑鼠側邊鍵被按下時會發送Btn_down
訊息,放開時發送Btn_up
訊息。
我開發的手勢 App 中有個函式是偵測滑鼠側邊鍵長按,就是透過兩個訊息的時間差來判斷是否長按。
這隻滑鼠居然是放開時才一次發送Btn_down
、Btn_up
兩個訊息,讓我手勢 App 的長按功能失效。
如果將 LaunchAgent 殺掉,經過設定過的滑鼠,側邊鍵、滾輪也會失效,重新配對後恢復正常。
不過使用 CoreBluetooth 讀取 Characteristics 能讀到設定過的滑鼠側邊鍵和滾輪的訊號。
綜上,我推斷,Logi Options+ 在設定滑鼠的過程中應該有透過藍牙往滑鼠裡 write value。
Read Characteristics 不是問題,現在是要找出到底寫入了什麼 value 進去。
Hook Logi Options+
於 IDA 中可以找到____ZN5devio10MacOSBlepp7dowrite
開頭的函式。
其中實作了-[CBPeripheral writeValue:forCharacteristic:type:]
方法。
不過動態調試 macOS 第三方 App 前,要先關閉 Mac 的系統完整保護(SIP),參考連結。
可用csrutil status
來查詢目前的 SIP 狀態。
-
Disable SIP
- 讓 Mac 進入復原模式,參考連結。
- 點選「工具程式」>「終端機」
csrutil disable
reboot
-
Enable SIP
- 讓 Mac 進入復原模式,參考連結。
- 點選「工具程式」>「終端機」
csrutil enable
reboot
接著用 Frida attach LaunchAgent 的 PID 追蹤相關函式:
frida-trace -p PID -m "*[CB* *]"
一無所獲。
於是用 LLDB attach LaunchAgent 的 PID:
lldb -p PID
在可疑的邏輯處下斷點,佐 IDA 分析邏輯走向,最後找到一條 6 bytes 的jz
指令,參考連結:
__text:0000000100AAE6C3 jz loc_100AAE7A9
因為跨平台開發的關係,pseudocode 可讀性極差,看不太出來原本是做了什麼判斷。
不過,我只需要知道writeValue:
參數而已,所以硬核地nop
掉這條邏輯就可以了,參考連結:
(lldb) memory write 0x1038606C3 -s 6 0x909090909090
別忘了加上 ASLR 產生的記憶體偏移。
再操作一下 GUI 設定滑鼠,中獎:
883639 ms -[CBPeripheral writeValue:<093d0056 2300560c 00000000 00000000 0000> forCharacteristic:<CBCharacteristic: 0x7fc7b380fc50, UUID = 00010001-0000-1000-8000-011F2000046D, properties = 0x1E, value = <093d0056 33005608 00000000 00000000 0000>, notifying = YES> type:nil]
writeValue:
與forCharacteristic:
參數一覽無遺,接下來就是自己實作了。
Swift 實作 Logi 藍牙通訊
上面那條函式調用 log 用 Swift 會寫成:
peripheral.writeValue(Data([0x09, 0x3d, 0x00, 0x56 ,0x23, 0x00, 0x56, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), for: CBUUID(string: "00010000-0000-1000-8000-011F2000046D"), type: .withoutResponse)
如果要一次性地寫入多個數值,建議把type
改為.withResponse
。
再於 delegate 的peripheral(_:didWriteValueFor:error:)
函式中遞迴寫入,參考連結。
後記
本文只是提供逆向工程中的思考方向,Swift 寫法可以參考 Github 上的 這個 項目。
如果讀者真的好奇 Logi Options+ 的寫入規則,不妨實作一次,相信會有更深的體會。
妳現在可以將肥大的 Logi Options+ 從 Mac 中徹底刪除了。
對了,調試完畢後別忘了 Enable SIP 哦。