Hi 大家好,我是梅森,當初國小和國中就對無人機非常感興趣,因此入手了相對平價的 Tello,在國中遙控飛機社也作為飛 DJI 空拍機之前的主力練習機。最近因為戰爭因素等等,讓我對於無人機安全很感興趣,因此我把我不知道摔了幾次的戰損版 Tello 與 Android App 開始研究。

木桌上的白色 Ryze Tello 無人機與 Android 手機,手機螢幕顯示 Tello App 的啟動畫面。
本研究使用的 Ryze Tello 與 Android 測試手機。
白色背景上的 Ryze Tello 四軸無人機,可見白色機身、黑色機臂與槳葉保護框。
Ryze Tello 機體。圖片作者 SimonWaldherr,來源 Wikimedia Commons,以 CC BY-SA 4.0 授權,縮放版。

研究歷程與測試環境

題目、樣本與分析範圍

這個題目的起點很單純。我手上有一台 Tello,也想知道它的 Android App 有沒有人仔細看過。當時關於 DJI 的資安討論大多集中在 GO 4 與 Pilot,控制通道的研究也已經相當多。相較之下,Tello App 的第三方 SDK、資料流與隱私政策幾乎沒有公開分析,於是成了這次研究的主要範圍。

一開始要下載 App 時,Google Play 的頁面已經無法取得。不過 Ryze 官方下載頁仍提供 APK,這表示下架的是 Play 商店通路,不是廠商停止散布。我蒐集了 2019 至 2024 年的六個版本,並驗證各版本由同一把簽章金鑰簽發。後來取得官方網站的 1.6.8.0 後,也確認它與鏡像樣本逐位元組相同。樣本來源先確認清楚,後面的比對才有意義。

動態分析環境

首先我撈出了已經很久沒有使用的 Pixel 6 Pro,當作 Android 的目標載具。這個題目需要直接連上 Tello 的 Wi-Fi,模擬器不適合。它沒有真實的無線網路介面,加殼 App 也可能偵測到模擬器環境。

接著是解鎖 bootloader、刷入 Magisk,並處理 Windows 的 fastboot 驅動程式。Root 裝好後,su 一開始仍被拒絕,原因不是全域設定,而是 Magisk 的 Superuser 分頁裡已有一筆針對 com.android.shell 的拒絕政策。把那筆個別規則改掉後,才終於能使用 Root 權限。

我再加上 Frida、mitmproxy 與系統憑證,讓手機連著 Tello Wi-Fi 時仍可經由 USB 轉送 HTTPS 流量。這段環境建置比預期久得多。Frida 也不能一開始就掛載,過早加入 Java hook 會讓 App 在啟動時停止運作。後來改成延遲掛載,研究後期再改為 attach 已啟動的行程。這也留下了一項限制,啟動期的行為不一定能被觀測到。

手機準備好後,我才回頭處理 APK。愛加密抽走了大量方法主體,反編譯結果不足以還原所有執行路徑。不過 DEX 的字串池與類別清單仍可用,因此靜態分析仍能用來確認端點、SDK 與程式碼是否存在。

測試條件的確認

實機測試也有自己的干擾因素。飛控傳感器警告必須先完成校正,否則無法判斷異常反應來自測試還是機體狀態。連續測試時的高溫保護、Android 對無網際網路 Wi-Fi 的斷線判定,以及 VPN 看似中斷但出口仍在海外的情況,都曾讓資料不能直接採用。

因此,我後來把環境狀態列入每次測試的前置條件。測試前確認出口 IP、Tello Wi-Fi 連線與機體溫度,對與既有機制矛盾的單次結果重新測試。這也是本文多次保留「未測試」與「未驗證」的原因,不是每一段封包或一次失敗都能直接寫成結論。

Tello SDK 控制通道的測試結果

Tello 同時使用兩條控制路徑。官方 App 使用私有的二進位協定,而官方 SDK 則以純文字 UDP 指令操作 192.168.10.1 的 8889 埠,狀態資料經 8890 埠廣播,影像則走 11111 埠。後者原本是讓教育與程式設計工具使用的介面,但協定規格沒有描述認證、授權或裝置配對。

Tello App 的如何連接畫面,指示使用者在手機無線局域網中選擇 Tello-xxxxxx 飛行器網路。
圖一為 Tello App 的連線說明畫面。畫面要求使用者在手機網路清單中選擇飛行器的 `Tello-xxxxxx` Wi-Fi,也呈現繁體字搭配中國用語的在地化狀態。

我先讓官方 App 維持正常連線與影像串流,再從另一個來源埠送出下列指令。為了避免讓機體產生動作,這次只送出了進入 SDK 模式與查詢用途的內容。

command

封包紀錄顯示,飛機在 18 毫秒後回覆 ok。接著,App 的影像串流停止,飛機開始向另一端廣播明文遙測資料。這不是 App 自己離線造成的錯覺,同一段時間中 App 的控制流仍持續運作,合計送出 2,893 個封包,至 59 秒仍維持 49–51 Hz。螢幕上的操控元件仍在,但即時影像已經消失。

Tello App 攻擊前後的狀態列比較,上方 Wi-Fi 圖示正常,下方出現斷線斜線。
圖二為在自有機體上送出 SDK 指令前後的 Tello App 狀態列。下方 Wi-Fi 圖示出現斷線斜線,圖片已排除即時攝影畫面。

影像封包的計數也很直接。攻擊前 0–12.1 秒有 1,766 個封包,攻擊後的 0–10 秒、10–25 秒與 25–47 秒都為 0。此時另一端收到的資料包含姿態、速度、溫度、高度與電量,如下。

pitch:0;roll:1;yaw:8;vgx:0;vgy:0;vgz:0;templ:70;temph:73;
tof:10;h:0;bat:85;baro:49.44;time:0;agx:1.00;agy:-27.00;agz:-999.00;

我另外驗證了恢復情形。47 秒的觀測期間沒有新的影像封包,從 App 的連線畫面重新操作也沒有恢復。這次結果唯一有效的復原方式是關機重開。不過「飛行中會有相同結果」仍是依同一機制作出的推論,未在實際飛行中測試。

Tello App 控制介面保留搖桿與狀態列,但中央影像區域全黑,Wi-Fi 圖示帶有斷線斜線。
圖三為 SDK 指令送出後的 Tello App 控制畫面。控制元件仍顯示,影像區域全黑,狀態列顯示 Wi-Fi 已斷線。
Tello App 在 SDK 指令送出後 45 秒的控制畫面,影像區域持續全黑,控制搖桿與狀態列仍顯示。
圖四為 SDK 指令送出後 45 秒的畫面。影像仍未恢復,畫面保留控制元件與狀態列。
Tello App 的未連線控制畫面,中央影像區域全黑,狀態列顯示斷線圖示。
圖五為 Tello App 的未連線控制畫面,用於對照影像中斷後仍保留的控制介面。

已公開的控制通道問題與研究範圍

測完之後,我先去查了前人研究。Tello Wi-Fi 預設無密碼、SDK 協定沒有認證,以及以 SDK 指令讓已連線手機失去連線,早已分別出現在 Alias Robotics 的 RVDP#1208 案例研究、同儕審查論文〈Analysis of Control Channel Cybersecurity of the Consumer-Grade UAV by the Example of DJI Tello〉,以及公開的技術文章。Alias Robotics 對預設開放 Wi-Fi 的案例給出 CVSS 9.4。

所以我不會把它寫成「本研究發現 Tello 可被未認證接管」。我做的是複現已知的 SDK 劫持,另外補上三個前人研究沒有講清楚的邊界。

第一,兩條協定的安全模型不同。App 的二進位控制封包雖然序號恆為 0,我仍以滿舵右轉、持續 21.5 秒、共 250 個封包的方式測試。飛手的搖桿全程維持中位,飛機沒有旋轉。這表示二進位路徑具工作階段綁定,不能只因封包格式可重建就推論任意來源可以控制飛機。相對地,SDK 純文字路徑沒有這個限制。

第二,設定 Wi-Fi 密碼無法成為協定層的解法。研究中將 Wi-Fi 由 open 設為 wpa2-psk / wpa3-sae,再以已合法連網的手機從不同來源埠重送 command。飛機在 13 毫秒後回覆 ok,App 影像由 1,499 個封包降為 6 個後歸零。密碼可以阻擋未取得密碼的人加入網路,卻不會為已在網路內的裝置加入 SDK 認證。

第三,影像中斷無法由 App 自行恢復。這點使問題不只是一段短暫的串流干擾,但也不代表我已經驗證飛行控制可以被接管。官方 SDK 文件列有 rc a b c d、takeoff、land 與 emergency,其中 emergency 的說明是「Stop motors immediately.」我從未對飛行中的機體送出 rc、takeoff、land 或 emergency。它們對飛行中機體的實際效果未測試,任何墜落或完整劫持的說法都只能是依官方文件的推論。

Tello Android App 的資料流與隱私政策

控制通道已有相當完整的研究,Tello App 本身卻很少被拆開檢視。我把範圍轉到六個 APK 版本的第三方 SDK、內建隱私政策、端點與執行期流量,希望區分廠商說明了什麼、App 實際做了什麼,以及哪些結果仍然需要保留不確定性。

動態分析比靜態檢索多了幾個現實限制。六個版本都以愛加密加固,部分 Java 方法主體被抽取到執行期載入的元件。單靠反編譯結果不能說明所有執行路徑。我在研究裝置上使用 Frida 觀察特定 API 與 HTTPS 流量,同時保留原始封包與測試條件。掛載時機也需要控制,過早加入 Java hook 會使 App 在首次啟動階段停止運作,這類失敗不是漏洞證據,而是研究工具與目標程式交互作用的限制。

動態測試在隔離的研究裝置上完成。為了讀取所需的系統狀態與控制測試環境,我先完成 Magisk 的安裝與授權設定。這些畫面只記錄研究環境,不涉及 Tello App 的使用者資料或飛行影像。

研究用 Android 裝置的 Magisk 首頁,顯示 Magisk 與應用程式均已安裝。
圖六為研究裝置的 Magisk 安裝狀態,用於建立受控的動態分析環境。
研究用 Android 裝置的 Magisk 超級使用者設定頁面,顯示使用者驗證與 Root 存取權限選項。
圖七為研究裝置的 Magisk 超級使用者設定。測試僅在自有裝置上進行。
研究用 Android 裝置的超級使用者權限清單,顯示 Android shell 項目的權限開關。
圖八為研究裝置對 Android shell 的 Root 授權狀態,用以執行受控的觀測與測試。
研究用 Android 裝置顯示 Tello 應用程式屢次停止運作的系統對話框,背景為超級使用者權限管理畫面。
圖九記錄在研究裝置上過早掛載 Frida Java hook 時,Tello App 停止運作的情形。此畫面是動態分析的限制,並非對使用者的漏洞展示。

最明確的例子來自內嵌的 GameSir 手把 SDK。當我把 gamesir_lastTime 歸零後重新啟動 App,SDK 向 https://client.vgabc.com/clientapi/ 發出以下請求。端點解析至中國境內的阿里雲。

POST https://client.vgabc.com/clientapi/

model=appstore&action=connectmodel&brand=google&device=Pixel 6 Pro
&package=com.ryzerobotics.tello&sdkversion=5

這筆資料包含裝置廠牌、機型、套件名稱與 SDK 版本,未見 Android ID、MAC 位址、IMEI、廣告 ID、位置或帳號資訊。問題不在於把它說成高敏感度資料,而是這個第三方傳輸沒有出現在我檢索的 1.3.0.0、1.6.0.1、1.6.4.0、1.6.8.0 中英文隱私政策。gamesir、vgabc、小鸡 與 xj 的命中數皆為 0。

我接著做了權限 A/B 對照。位置、相片、影片與儲存空間的六項權限全部拒絕時,仍有 1 筆請求。全部授予時也有 1 筆。兩組 body 都是 110 bytes,逐位元組相同,回應大小皆為 2053 bytes。原因並不複雜,這些欄位可由 android.os.Build 取得,不需危險權限。使用者既無法從權限設定得知這筆傳輸,也不能以這些設定阻止它。

這個實驗也提醒我,流量觀測不能只做一次。GameSir SDK 有節流欄位。若不先歸零 gamesir_lastTime,兩組都可能沒有流量,進而把「節流尚未到期」誤解成「權限沒有影響」。結論碰巧正確並不代表測試設計正確。

客服 WebView 提供了第二個不同的資料面。從 App 開啟技術支援後,頁面載入阿里雲 ARMS 與兩組 Google Analytics,ARMS 信標含持久性使用者識別碼、工作階段識別碼、螢幕解析度、像素密度與連線類型。這筆請求的目的地是 us-west-1,因此不能寫成傳往中國。至於 VPN 未關閉時取得的地區分流現象,我沒有把它當作結論。

政策的問題也不是單向缺漏。中文政策曾具名揭露 Bugly 與 BaiduLBS,卻未提 Google Analytics。英文政策提到 Google Analytics,卻未列前兩者。兩種語言的使用者各自拿到不完整的第三方清單,而 GameSir、阿里雲 ARMS 與 retcode 在各版本、各語言政策中皆未被揭露。App 的繁中資源另有 316 條字串,12 組台灣與中國用語對照中,中國用語出現 57 次,台灣用語出現 4 次,占 93.4%。這不是我對翻譯品質的印象,而是資源檔的計數結果。

Tello App 首次啟動時的使用者協議畫面,包含隱私權政策連結與同意勾選框。
圖十為 Tello App 首次啟動的使用者協議畫面。隱私權政策在使用流程中可被開啟,因此內建文件的版本與揭露內容具有直接意義。

我進一步比較內建政策,發現英文版從 2019 至 2024 的六個版本逐位元組相同,大小為 205,402 bytes。中文隱私政策則改過三次。這不代表英文版完全沒有資料所在地的說明,英文文件其實提到伺服器可能位於美國、中國與香港。我曾用過窄的關鍵字搜尋而漏掉這段內容,之後已改正。仍然成立的結論是,具名第三方 SDK 的揭露在語言之間不對等,而且最新 APK 內建的政策副本可能落後於線上版本。

飛行日誌、原生程式庫與未驗證事項

我也檢查了飛行日誌的保存與上傳。飛行後,App 在 droneLog/ 目錄留下 DJI_LOG_V3 檔案。針對一個 3.3 MB 的日誌檔,我獨立求得其每筆記錄的混淆方式如下。

plain[i] = cipher[i] ^ cipher[2]

金鑰就放在酬載的第 3 個位元組,與密文一同送出。全量驗證中,解碼後為 0 的位元組比例為 30.5%,位元組熵由 7.962 / 8.0 降為 6.356 / 8.0。不過 DJI DAT 格式已有第三方工具,我尚未查證這種混淆方式是否已有公開文件,也未驗證那些工具是否適用於 Tello。所以這頂多算我自己獨立解出來的,稱不上首次發現。

資料留在本機不等於不會上傳。我在未登入、切回有對外網路的 Wi-Fi 後觀測 5 分鐘,連續 20 次、每 15 秒檢查一次,Tello App 都沒有對外請求,也未觀測到飛行紀錄上傳。這是對廠商有利的結果,但它的邊界同樣明確。登入後的行為未測試,批次或特定頁面觸發的上傳也未測試,不能把它延伸成「App 不會上傳飛行紀錄」。

原生程式庫的結果則替研究排除了一個方向。liblocSDK8a.so 在 1.6.8.0 仍隨 APK 出貨,大小為 43,112 bytes,18 個 JNI 進入點都指向 com.baidu.location.Jni 或 com.baidu.serverLoc.Jni。但該版本 dex 中 com/baidu 開頭的類別數是 0,位置權限拒絕或授予後等待 20 秒,這個程式庫都沒有載入。這些證據指向它是 Java 端根本叫不到的殘留程式碼。要當成隱藏資料傳輸的直接證據,還差得遠。

四項測試結果的修正

研究做到最後,我手上沒有漏洞清單,倒是有四次被自己推翻的推論。

第一,我曾認為二進位協定的序號恆為 0,就足以注入飛行指令。21.5 秒的滿舵測試沒有任何可見反應後,我把結論下修為二進位協定具工作階段綁定,嚴重度由 High 下修為 Low。

第二,我一度以為設定 Wi-Fi 密碼能阻擋 SDK 路徑。第一次測試沒有回應,但那是 n=1,且與 WPA2 位於連結層、UDP 協定位於應用層的機制不符。重測後攻擊完整重現。首次無回應可能與高溫保護有關,但這個原因本身也未驗證。若我停在第一次結果,就會給出方向相反的安全建議。

第三,我曾把英文隱私政策寫成未提 Google Analytics。交叉檢索後發現英文版三個版本確實有揭露,應修正為中英文揭露各自缺漏、形成對稱落差。否定性主張尤其需要多組關鍵字與原文段落交叉確認。

第四,日誌的高熵一開始讓我以為內容接近隨機而無法解讀。找到與密文同在的單位元組金鑰後,資料結構才開始清楚。高熵只表示目前的假設看起來像隨機,不能直接當成強加密的證明。

教室多機環境也值得另外驗證。官方的教育工具與 SDK 使用同一組 UDP 埠和文字指令,但我只有一台機器,沒有在真實教室情境實地測試。多機同時運作時的連線、SSID 區分與學生裝置行為仍是未測試事項。

研究結論

我原本以為最有價值的成果會是那 7 個位元組。回頭看,那段測試真正的用處是逼我去查前人研究,然後把問題移到還沒有被仔細看過的 App 層。

隱私政策裡沒有提到的 GameSir 傳輸、客服 WebView 載入的第三方遙測、中英文政策各自缺漏的第三方清單、飛行日誌的獨立求解,還有那個 Java 端叫不到卻仍隨 APK 出貨的原生程式庫,這幾件事比控制通道更值得往下追。

只是每一項都還有沒測完的地方。登入後的日誌上傳、教室多機情境,以及 rc 與 emergency 對飛行中機體的實際效果,都還停在未測試。這篇留下的是一份標好邊界的紀錄,該補的測試還在後面。