現(xiàn)高權(quán)限Shell命令執(zhí)行方案詳解)
1. 項(xiàng)目概述當(dāng)APK需要執(zhí)行Shell命令時(shí)在Android應(yīng)用開發(fā)中尤其是涉及系統(tǒng)管理、自動(dòng)化測試、深度定制或設(shè)備管理MDM等場景時(shí)我們常常會(huì)遇到一個(gè)核心需求讓一個(gè)普通的第三方APK能夠安全、可控地執(zhí)行系統(tǒng)Shell命令。這聽起來像是系統(tǒng)應(yīng)用的“特權(quán)”但通過合理的設(shè)計(jì)普通應(yīng)用也能在特定條件下實(shí)現(xiàn)。這不僅僅是調(diào)用Runtime.getRuntime().exec(“l(fā)s”)那么簡單那通常只能訪問應(yīng)用自身的沙盒環(huán)境。我們真正要探討的是如何突破沙盒限制執(zhí)行更高權(quán)限的命令比如pm install、am start甚至是修改系統(tǒng)設(shè)置。這個(gè)需求背后是Android安全沙盒機(jī)制與功能靈活性之間的經(jīng)典矛盾。Android通過嚴(yán)格的權(quán)限模型和進(jìn)程隔離保護(hù)了系統(tǒng)和用戶數(shù)據(jù)的安全但也給一些需要深度集成的應(yīng)用帶來了挑戰(zhàn)。例如企業(yè)自研的設(shè)備管控工具需要靜默安裝應(yīng)用、自動(dòng)化測試框架需要模擬用戶點(diǎn)擊和系統(tǒng)操作、或者一些工具類應(yīng)用需要清理系統(tǒng)緩存。直接執(zhí)行這些命令對(duì)于非系統(tǒng)簽名的APK來說是一條“此路不通”的單行道。因此“三方APk執(zhí)行shell命令”這個(gè)標(biāo)題指向的是一套完整的技術(shù)方案其核心在于建立一條從應(yīng)用層到系統(tǒng)層的可信命令通道。最經(jīng)典、最可控的方式就是利用Socket通信構(gòu)建一個(gè)客戶端APK-服務(wù)端具有高權(quán)限的進(jìn)程的架構(gòu)。APK作為客戶端將需要執(zhí)行的命令發(fā)送出去一個(gè)擁有相應(yīng)權(quán)限如android:sharedUserId”android.uid.system”或通過su提權(quán)的服務(wù)端進(jìn)程接收、執(zhí)行命令并將結(jié)果返回。這就像你無法直接進(jìn)入機(jī)房操作服務(wù)器但你可以通過一個(gè)被授權(quán)的管理員向他發(fā)送指令由他代為執(zhí)行并反饋結(jié)果。本文將深入拆解這套方案的完整實(shí)現(xiàn)路徑從設(shè)計(jì)思路、權(quán)限獲取、Socket通信搭建到命令執(zhí)行的安全與效率考量并結(jié)合實(shí)際開發(fā)中踩過的坑為你呈現(xiàn)一份可直接落地的實(shí)戰(zhàn)指南。無論你是正在開發(fā)企業(yè)級(jí)設(shè)備管理應(yīng)用還是對(duì)Android系統(tǒng)底層交互感興趣這篇文章都將為你提供清晰的路線圖。2. 核心架構(gòu)設(shè)計(jì)與權(quán)限突圍要讓一個(gè)三方APK執(zhí)行高權(quán)限Shell命令直來直去是行不通的我們必須采取“曲線救國”的策略。整個(gè)架構(gòu)的核心思想是**“權(quán)限分離與橋接”**。應(yīng)用本身保持普通權(quán)限而將需要特權(quán)的操作委托給一個(gè)擁有足夠權(quán)限的“代理”去執(zhí)行。這個(gè)“代理”與APK之間的通信橋梁我們選擇了Socket。2.1 為什么是Socket你可能想到用Broadcast、ContentProvider或者BinderAIDL進(jìn)行進(jìn)程間通信IPC。它們各有優(yōu)劣但Socket在此場景下優(yōu)勢明顯靈活性Socket不依賴于Android框架是更底層的TCP/IP通信。這意味著我們的服務(wù)端甚至可以是一個(gè)用C/C編寫的、在init.rc中啟動(dòng)的Native守護(hù)進(jìn)程權(quán)限可以做到極高root或system。跨語言與跨平臺(tái)客戶端APK用Java/Kotlin服務(wù)端完全可以用C、C、甚至Go來寫只要遵守同樣的通信協(xié)議即可。這為利用現(xiàn)有強(qiáng)大的命令行工具庫提供了可能。連接持久性與實(shí)時(shí)性相比廣播的瞬時(shí)性Socket可以建立持久連接非常適合連續(xù)執(zhí)行多條命令或需要實(shí)時(shí)交互如top、logcat的場景。控制粒度細(xì)我們可以自定義完整的通信協(xié)議包括認(rèn)證、加密、命令格式、結(jié)果返回格式、錯(cuò)誤處理等安全性設(shè)計(jì)空間更大。2.2 權(quán)限獲取的三種路徑服務(wù)端進(jìn)程的權(quán)限是整個(gè)方案的基石。沒有權(quán)限一切免談。通常有三種路徑難度和適用范圍逐級(jí)遞增路徑一系統(tǒng)應(yīng)用System App這是最“正規(guī)”的途徑。將你的服務(wù)端應(yīng)用與系統(tǒng)鏡像一起編譯或者將其放入設(shè)備的/system/priv-app目錄并賦予正確的簽名平臺(tái)簽名。這樣應(yīng)用就可以在清單文件中聲明android:sharedUserId”android.uid.system”從而獲得與system進(jìn)程相同的權(quán)限可以執(zhí)行大部分shell命令。注意這種方式需要設(shè)備制造商OEM或系統(tǒng)集成商的支持對(duì)于普通開發(fā)者上架公開應(yīng)用商店不現(xiàn)實(shí)主要用于定制設(shè)備或預(yù)裝場景。路徑二利用設(shè)備管理員Device Owner / Profile Owner對(duì)于企業(yè)移動(dòng)管理EMM/MDM場景應(yīng)用可以通過成為“設(shè)備所有者”來獲取一系列特權(quán)API。通過adb shell dpm set-device-owner命令設(shè)置后應(yīng)用可以調(diào)用DevicePolicyManager的相關(guān)方法執(zhí)行部分受限操作例如靜默安裝應(yīng)用。但它的命令執(zhí)行能力是受限的、API化的并非一個(gè)通用的Shell通道。路徑三Root權(quán)限這是能力最強(qiáng)、也最“野”的路徑。如果設(shè)備已經(jīng)獲取了Root權(quán)限那么我們可以讓服務(wù)端進(jìn)程以root身份運(yùn)行。通常的做法是APK啟動(dòng)一個(gè)后臺(tái)的su進(jìn)程例如通過Runtime.exec(“su”)然后向這個(gè)進(jìn)程的輸入流寫入需要執(zhí)行的命令。這個(gè)su進(jìn)程就成為了我們的高權(quán)限服務(wù)端。重要警告Root方案極度依賴設(shè)備環(huán)境兼容性差且會(huì)嚴(yán)重破壞系統(tǒng)安全模型。它僅適用于極客、開發(fā)者調(diào)試或完全受控的內(nèi)部設(shè)備絕對(duì)不適合普通用戶產(chǎn)品。我們的設(shè)計(jì)選擇為了兼顧講解的實(shí)用性和合法性下文將主要圍繞**“系統(tǒng)應(yīng)用”作為服務(wù)端**這一路徑展開。這是很多定制ROM、車機(jī)、大屏設(shè)備等場景下的真實(shí)需求。我們會(huì)構(gòu)建一個(gè)具有系統(tǒng)權(quán)限的Socket服務(wù)和一個(gè)普通的客戶端APK。2.3 整體通信流程設(shè)計(jì)整個(gè)系統(tǒng)的運(yùn)行流程可以概括為以下幾步服務(wù)端啟動(dòng)具有系統(tǒng)權(quán)限的服務(wù)端應(yīng)用或Native守護(hù)進(jìn)程在設(shè)備啟動(dòng)時(shí)或適時(shí)啟動(dòng)并在一個(gè)預(yù)定端口如8668上創(chuàng)建ServerSocket開始監(jiān)聽。客戶端連接普通三方APK在需要執(zhí)行命令時(shí)嘗試連接到localhost:8668。認(rèn)證與握手可選但重要為防止惡意應(yīng)用隨意連接服務(wù)端應(yīng)設(shè)計(jì)簡單的認(rèn)證機(jī)制。例如客戶端連接后首先發(fā)送一個(gè)預(yù)共享的令牌Token服務(wù)端驗(yàn)證通過后才進(jìn)入命令處理循環(huán)。命令發(fā)送與執(zhí)行客戶端將待執(zhí)行的Shell命令如pm list packages按預(yù)定格式如“CMD:pm list packages\n”發(fā)送給服務(wù)端。服務(wù)端讀取命令調(diào)用Runtime.getRuntime().exec()或更優(yōu)的ProcessBuilder來執(zhí)行。結(jié)果返回服務(wù)端捕獲命令執(zhí)行進(jìn)程的InputStream標(biāo)準(zhǔn)輸出和ErrorStream標(biāo)準(zhǔn)錯(cuò)誤將這些流的內(nèi)容讀取、封裝并通過Socket發(fā)回給客戶端。連接管理命令執(zhí)行完畢后可以關(guān)閉本次連接短連接也可以保持連接等待下一條命令長連接。長連接需妥善處理心跳、超時(shí)和異常斷開。3. 服務(wù)端實(shí)現(xiàn)構(gòu)建高權(quán)限命令執(zhí)行引擎服務(wù)端是整個(gè)架構(gòu)的“心臟”它必須具備高權(quán)限并且要穩(wěn)定、安全。這里我們以實(shí)現(xiàn)一個(gè)簡單的Android Java系統(tǒng)應(yīng)用服務(wù)端為例。3.1 創(chuàng)建系統(tǒng)應(yīng)用服務(wù)端項(xiàng)目首先你需要一個(gè)具有平臺(tái)簽名密鑰的環(huán)境。這通常意味著你要下載AOSP源碼并編譯或者從設(shè)備制造商那里獲取簽名密鑰。在AndroidManifest.xml中聲明共享用戶ID和權(quán)限manifest xmlns:androidhttp://schemas.android.com/apk/res/android packagecom.example.shellserver coreApptrue android:sharedUserIdandroid.uid.system !-- 關(guān)鍵聲明 -- uses-permission android:nameandroid.permission.INTERNET / !-- 作為服務(wù)端可能需要訪問網(wǎng)絡(luò)狀態(tài) -- uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE / application android:allowBackupfalse android:labelstring/app_name android:supportsRtltrue !-- 一個(gè)在后臺(tái)運(yùn)行的Service -- service android:name.ShellSocketService android:enabledtrue android:exportedtrue !-- 允許其他進(jìn)程綁定或啟動(dòng) -- android:process:remote !-- 可選運(yùn)行在獨(dú)立進(jìn)程增加穩(wěn)定性 -- intent-filter action android:namecom.example.shellserver.START_SERVICE / /intent-filter /service /application /manifest編譯后需要使用平臺(tái)簽名對(duì)APK進(jìn)行簽名并推送至設(shè)備的/system/priv-app/目錄下。3.2 實(shí)現(xiàn)Socket服務(wù)核心邏輯我們?cè)赟hellSocketService中實(shí)現(xiàn)一個(gè)IntentService或JobIntentService以便在后臺(tái)處理Socket連接。這里使用簡單的Thread模型來演示核心邏輯public class ShellSocketService extends Service { private static final int PORT 8668; private ServerSocket serverSocket; private ExecutorService threadPool; private volatile boolean isRunning false; Override public void onCreate() { super.onCreate(); threadPool Executors.newCachedThreadPool(); startServer(); } private void startServer() { isRunning true; new Thread(() - { try { serverSocket new ServerSocket(PORT); // 綁定到本地回環(huán)地址只接受本機(jī)連接更安全 // serverSocket new ServerSocket(PORT, 0, InetAddress.getByName(127.0.0.1)); while (isRunning) { Socket clientSocket serverSocket.accept(); // 阻塞等待客戶端連接 threadPool.submit(new ClientHandler(clientSocket)); } } catch (IOException e) { e.printStackTrace(); } }).start(); } class ClientHandler implements Runnable { private Socket socket; ClientHandler(Socket socket) { this.socket socket; } Override public void run() { try (BufferedReader in new BufferedReader(new InputStreamReader(socket.getInputStream())); PrintWriter out new PrintWriter(new OutputStreamWriter(socket.getOutputStream()), true)) { // 1. 簡單認(rèn)證示例 String token in.readLine(); if (!MY_SECURE_TOKEN_123.equals(token)) { out.println(ERROR: Authentication failed); return; } out.println(AUTH_OK); // 2. 循環(huán)讀取并執(zhí)行命令 String commandLine; while ((commandLine in.readLine()) ! null) { if (EXIT.equalsIgnoreCase(commandLine)) { break; } // 執(zhí)行命令并返回結(jié)果 String result executeShellCommand(commandLine); out.println(result); out.println(CMD_END); // 發(fā)送結(jié)束標(biāo)記 } } catch (Exception e) { e.printStackTrace(); } finally { try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } private String executeShellCommand(String cmd) { StringBuilder output new StringBuilder(); Process process null; try { // 使用ProcessBuilder更靈活 ProcessBuilder pb new ProcessBuilder(sh, -c, cmd); pb.redirectErrorStream(true); // 將錯(cuò)誤流合并到標(biāo)準(zhǔn)輸出流 process pb.start(); BufferedReader reader new BufferedReader(new InputStreamReader(process.getInputStream())); String line; while ((line reader.readLine()) ! null) { output.append(line).append(\n); } int exitCode process.waitFor(); output.append(\n[Exit Code: ).append(exitCode).append(]); } catch (IOException | InterruptedException e) { output.append(Exception: ).append(e.getMessage()); } finally { if (process ! null) { process.destroy(); } } return output.toString(); } } Override public void onDestroy() { isRunning false; try { if (serverSocket ! null) serverSocket.close(); } catch (IOException e) {} threadPool.shutdown(); super.onDestroy(); } Nullable Override public IBinder onBind(Intent intent) { return null; } }3.3 服務(wù)端的關(guān)鍵細(xì)節(jié)與避坑指南1. 權(quán)限與用戶ID驗(yàn)證即使聲明了system共享UID在服務(wù)端代碼里也可以再次驗(yàn)證確保萬無一失if (android.os.Process.myUid() ! android.os.Process.SYSTEM_UID) { Log.e(TAG, Service must run as system uid!); stopSelf(); }2. 命令執(zhí)行的安全性這是重中之重。絕對(duì)不能不加過濾地執(zhí)行客戶端發(fā)來的任何命令。白名單機(jī)制只允許執(zhí)行預(yù)定義的安全命令。例如只允許pm list packages不允許pm uninstall或rm -rf。private boolean isCommandAllowed(String cmd) { String[] allowedCommands {pm list packages, dumpsys meminfo, getprop}; for (String allowed : allowedCommands) { if (cmd.startsWith(allowed)) { return true; } } return false; }參數(shù)過濾與轉(zhuǎn)義如果命令需要參數(shù)必須對(duì)參數(shù)進(jìn)行嚴(yán)格的校驗(yàn)和轉(zhuǎn)義防止命令注入。避免直接拼接字符串考慮使用數(shù)組形式傳遞參數(shù)給ProcessBuilder。超時(shí)控制有些命令可能卡住如ping。必須為命令執(zhí)行設(shè)置超時(shí)。if (!process.waitFor(30, TimeUnit.SECONDS)) { process.destroyForcibly(); return ERROR: Command timeout; }3. 資源管理與穩(wěn)定性使用線程池避免為每個(gè)連接創(chuàng)建無限多的線程使用Executors.newCachedThreadPool或newFixedThreadPool。妥善關(guān)閉流和Socket使用try-with-resources語句確保InputStream、OutputStream、Socket被正確關(guān)閉防止資源泄漏。處理意外斷開客戶端可能隨時(shí)崩潰或網(wǎng)絡(luò)中斷。服務(wù)端需要捕獲SocketException并清理對(duì)應(yīng)的連接資源。4. 提升性能流式傳輸對(duì)于可能產(chǎn)生大量輸出的命令如logcat不要等命令全部執(zhí)行完再一次性返回??梢圆捎昧魇絺鬏斠贿呑x取進(jìn)程輸出一邊通過Socket發(fā)送給客戶端。這需要設(shè)計(jì)更復(fù)雜的協(xié)議例如使用數(shù)據(jù)塊分隔符。4. 客戶端實(shí)現(xiàn)封裝易用的命令調(diào)用接口客戶端APK是普通應(yīng)用它的任務(wù)是連接到服務(wù)端發(fā)送命令并獲取結(jié)果。我們需要將其封裝成一個(gè)簡單易用的工具類。4.1 建立Socket連接與通信協(xié)議首先定義通信協(xié)議。我們沿用服務(wù)端定義的簡單協(xié)議連接建立后客戶端首先發(fā)送一行認(rèn)證令牌。發(fā)送命令每行一條。接收服務(wù)端返回的結(jié)果直到遇到結(jié)束標(biāo)記”CMD_END”。public class ShellCommandClient { private static final String SERVER_IP 127.0.0.1; private static final int SERVER_PORT 8668; private static final String AUTH_TOKEN MY_SECURE_TOKEN_123; public static String executeRemoteCommand(String command) { Socket socket null; PrintWriter out null; BufferedReader in null; StringBuilder result new StringBuilder(); try { // 1. 建立連接 socket new Socket(SERVER_IP, SERVER_PORT); socket.setSoTimeout(30000); // 設(shè)置讀寫超時(shí)30秒 out new PrintWriter(socket.getOutputStream(), true); in new BufferedReader(new InputStreamReader(socket.getInputStream())); // 2. 認(rèn)證 out.println(AUTH_TOKEN); String authResponse in.readLine(); if (!AUTH_OK.equals(authResponse)) { return Authentication Error: authResponse; } // 3. 發(fā)送命令 out.println(command); // 4. 讀取結(jié)果 String line; while ((line in.readLine()) ! null) { if (CMD_END.equals(line)) { break; } result.append(line).append(\n); } } catch (UnknownHostException e) { result.append(Error: Server not found. Is the service running?\n); } catch (SocketTimeoutException e) { result.append(Error: Connection or read timeout.\n); } catch (IOException e) { result.append(Error: Communication error - ).append(e.getMessage()).append(\n); } finally { // 5. 清理資源 try { if (in ! null) in.close(); } catch (IOException e) {} if (out ! null) out.close(); try { if (socket ! null) socket.close(); } catch (IOException e) {} } return result.toString(); } }4.2 在Android應(yīng)用中調(diào)用在Activity或ViewModel中切記不能在主線程進(jìn)行網(wǎng)絡(luò)操作。// 在ViewModel或AsyncTask/協(xié)程中調(diào)用 viewModelScope.launch(Dispatchers.IO) { val command pm list packages -3 // 列出所有第三方應(yīng)用 val result ShellCommandClient.executeRemoteCommand(command) withContext(Dispatchers.Main) { // 更新UI顯示結(jié)果 textViewResult.text result } }4.3 客戶端的健壯性設(shè)計(jì)1. 連接狀態(tài)管理客戶端應(yīng)該具備重試機(jī)制。如果第一次連接失敗可能服務(wù)端還沒啟動(dòng)可以延遲幾秒后重試1-2次。private static Socket connectWithRetry(String ip, int port, int maxRetries) throws IOException { IOException lastException null; for (int i 0; i maxRetries; i) { try { return new Socket(ip, port); } catch (IOException e) { lastException e; if (i maxRetries - 1) { try { Thread.sleep(2000); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); } } } } throw lastException; }2. 超時(shí)設(shè)置必須設(shè)置連接超時(shí)和讀取超時(shí)防止網(wǎng)絡(luò)異常或服務(wù)端無響應(yīng)導(dǎo)致客戶端線程永久掛起。socket.connect(new InetSocketAddress(SERVER_IP, SERVER_PORT), 10000); // 連接超時(shí)10秒 socket.setSoTimeout(30000); // 讀取超時(shí)30秒3. 異步與回調(diào)將executeRemoteCommand封裝成異步任務(wù)通過接口回調(diào)或LiveData將結(jié)果返回給UI層這是Android開發(fā)的最佳實(shí)踐。5. 高級(jí)議題與安全加固基礎(chǔ)功能實(shí)現(xiàn)后我們需要關(guān)注更高級(jí)的議題讓整個(gè)系統(tǒng)更健壯、更安全。5.1 通信加密目前的通信是明文的存在被本地其他應(yīng)用嗅探的風(fēng)險(xiǎn)。對(duì)于敏感命令或結(jié)果必須加密。簡單方案對(duì)稱加密客戶端和服務(wù)端預(yù)共享一個(gè)AES密鑰。發(fā)送前加密接收后解密。但密鑰存儲(chǔ)安全是個(gè)問題。進(jìn)階方案TLS在Socket之上建立TLS/SSL連接。服務(wù)端需要持有證書。這能提供雙向認(rèn)證和強(qiáng)加密是生產(chǎn)環(huán)境推薦的方式。可以使用SSLSocketFactory來實(shí)現(xiàn)。5.2 協(xié)議擴(kuò)展支持二進(jìn)制數(shù)據(jù)傳輸有時(shí)命令執(zhí)行的結(jié)果可能是二進(jìn)制文件如screencap截屏。我們需要擴(kuò)展協(xié)議來區(qū)分文本和二進(jìn)制數(shù)據(jù)。可以在命令前加前綴如BIN:然后先發(fā)送數(shù)據(jù)長度再發(fā)送二進(jìn)制流。5.3 服務(wù)端自啟動(dòng)與?;钊绾未_保服務(wù)端進(jìn)程一直在運(yùn)行系統(tǒng)應(yīng)用可以注冊(cè)BOOT_COMPLETED廣播接收器在開機(jī)時(shí)啟動(dòng)服務(wù)。Native守護(hù)進(jìn)程在init.rc中配置為service由init進(jìn)程管理崩潰后可重啟??蛻舳藛拘芽蛻舳嗽谶B接失敗時(shí)可以嘗試發(fā)送一個(gè)隱式Intent來啟動(dòng)服務(wù)如果服務(wù)聲明了android:exported”true”。5.4 權(quán)限最小化原則服務(wù)端應(yīng)遵循權(quán)限最小化原則。即使擁有system權(quán)限也不應(yīng)執(zhí)行超出業(yè)務(wù)需要的命令。白名單機(jī)制是底線。同時(shí)可以記錄詳細(xì)的命令執(zhí)行日志便于審計(jì)和排查問題。6. 實(shí)戰(zhàn)問題排查與經(jīng)驗(yàn)實(shí)錄在實(shí)際開發(fā)和調(diào)試中你會(huì)遇到各種各樣的問題。下面是一些典型問題的排查思路和解決方案。6.1 連接失敗Connection refused或SocketTimeoutException這是最常見的問題意味著客戶端無法連接到服務(wù)端的端口。檢查服務(wù)端是否運(yùn)行在設(shè)備上執(zhí)行adb shell ps | grep your.server.package或adb shell netstat -tlnp | grep 8668查看進(jìn)程和端口監(jiān)聽情況。檢查防火墻/SELinux在Android高版本上即使是本地回環(huán)地址也可能受到SELinux策略限制。查看adb logcat | grep avc是否有SELinux拒絕avc: denied的日志。如果是系統(tǒng)應(yīng)用需要添加對(duì)應(yīng)的SELinux策略文件.te文件。檢查IP和端口確??蛻舳诉B接的是127.0.0.1localhost而不是其他IP端口號(hào)與服務(wù)端監(jiān)聽的完全一致。服務(wù)端綁定地址確保服務(wù)端ServerSocket綁定到了0.0.0.0所有地址或127.0.0.1。如果綁定了特定的非回環(huán)IP客戶端用127.0.0.1可能連不上。6.2 命令執(zhí)行無結(jié)果或結(jié)果不完整客戶端發(fā)送了命令但收不到結(jié)果或結(jié)果中途截?cái)唷A魑凑_刷新確保客戶端的PrintWriter使用了autoFlush模式或者在發(fā)送命令后手動(dòng)調(diào)用out.flush()。緩沖區(qū)大小Socket和流的緩沖區(qū)是有限的。如果命令輸出巨大需要循環(huán)讀取和寫入。服務(wù)端和客戶端都要在循環(huán)中妥善處理。未讀取錯(cuò)誤流服務(wù)端執(zhí)行命令時(shí)如果只讀取了InputStream而忽略了ErrorStream那么當(dāng)命令執(zhí)行出錯(cuò)時(shí)錯(cuò)誤信息可能丟失。使用ProcessBuilder.redirectErrorStream(true)將錯(cuò)誤流合并到標(biāo)準(zhǔn)輸出流是一個(gè)好習(xí)慣。字符編碼問題Shell命令的輸出可能是非UTF-8的取決于系統(tǒng)Locale。在創(chuàng)建InputStreamReader和OutputStreamWriter時(shí)明確指定字符編碼如StandardCharsets.UTF_8。6.3 服務(wù)端崩潰或內(nèi)存泄漏長時(shí)間運(yùn)行后服務(wù)端可能變慢或崩潰。線程泄漏確保每個(gè)客戶端連接處理完畢后對(duì)應(yīng)的線程能夠結(jié)束。使用線程池并合理配置核心和最大線程數(shù)。資源未關(guān)閉務(wù)必在finally塊中關(guān)閉Socket、InputStream、OutputStream、Process。使用try-with-resources語法能最大程度避免此問題。命令進(jìn)程未銷毀服務(wù)端執(zhí)行的每個(gè)Shell命令都會(huì)創(chuàng)建一個(gè)子進(jìn)程。執(zhí)行完畢后必須調(diào)用Process.destroy()對(duì)于超時(shí)進(jìn)程要調(diào)用destroyForcibly()。6.4 權(quán)限問題java.lang.SecurityException服務(wù)端執(zhí)行某些命令時(shí)如安裝APK可能拋出安全異常。確認(rèn)UID再次確認(rèn)服務(wù)端進(jìn)程的UID是1000system。檢查SELinux上下文即使UID是system進(jìn)程的SELinux上下文也可能限制其能力。通過adb shell ps -Z查看進(jìn)程的SELinux標(biāo)簽。系統(tǒng)應(yīng)用通常需要system_app或platform_app標(biāo)簽。需要額外權(quán)限有些API如PackageManager.installPackage不僅需要system權(quán)限還需要在清單中聲明特定的uses-permission并且該權(quán)限的protectionLevel是signature|privileged。你需要確認(rèn)命令對(duì)應(yīng)的底層API所需的所有權(quán)限。6.5 調(diào)試技巧使用adb logcat在服務(wù)端和客戶端代碼中加入詳細(xì)的日志Log.d(TAG, …)通過adb logcat -s YourTag過濾查看是追蹤流程和定位問題的首要手段。使用strace對(duì)于Native服務(wù)端可以使用adb shell strace -p pid來跟蹤系統(tǒng)調(diào)用看進(jìn)程卡在哪里。網(wǎng)絡(luò)抓包雖然本地回環(huán)流量默認(rèn)不經(jīng)過物理網(wǎng)卡但可以使用tcpdump在loloopback接口上抓包或者使用Android Studio的Network Profiler對(duì)于客戶端進(jìn)行簡單分析。通過以上六個(gè)部分的拆解我們從設(shè)計(jì)理念到代碼實(shí)現(xiàn)從核心功能到安全加固完整地覆蓋了“三方APK通過Socket執(zhí)行Shell命令”這一技術(shù)方案的方方面面。這套方案的本質(zhì)是在Android的安全框架內(nèi)通過進(jìn)程間通信和權(quán)限代理巧妙地?cái)U(kuò)展了普通應(yīng)用的能力邊界。記住能力越大責(zé)任越大在設(shè)計(jì)和使用此類方案時(shí)務(wù)必把安全性和穩(wěn)定性放在首位。