Android 流氓軟體靜默安裝是怎麼實現的?
序言
乍聽起來,靜默安裝是非常流氓的一件事,它讓使用者不知覺的情況下被「收割」。但是技術本身是中立的,我們只談談實現靜默安裝這件事兒。
下面我將介紹三種靜默安裝的方案,每種方案各有利弊,但是目的是一致的。
- 手機被 Root 後直接靜默安裝
- 宣告安裝許可權並進行系統簽名來靜默安裝
- 使用輔助功能進行安裝(稱作「智慧安裝」更貼切吧)
1. 手機被 Root 後直接靜默安裝
眾所周知,手機被 Root 後可以做好多奇妙的事情,比如靜默安裝,直接呼叫 pm install 命令就可以實現,來看程式碼:
public static boolean silentInstall(String apkPath) { boolean result = false; DataOutputStream dataOutputStream = null; BufferedReader errorStream = null; BufferedReader successStream = null; Process process = null; try { // 申請 su 許可權 process = Runtime.getRuntime().exec("su"); dataOutputStream = new DataOutputStream(process.getOutputStream()); // 執行 pm install 命令 String command = "pm install -r " + apkPath + "\n"; dataOutputStream.write(command.getBytes(Charset.forName("UTF-8"))); dataOutputStream.writeBytes("exit\n"); dataOutputStream.flush(); process.waitFor(); errorStream = new BufferedReader(new InputStreamReader(process.getErrorStream())); StringBuilder errorMsg = new StringBuilder(); String line; while ((line = errorStream.readLine()) != null) { errorMsg.append(line); } log.debug("silent install error message:{}", errorMsg); StringBuilder successMsg = new StringBuilder(); successStream = new BufferedReader(new InputStreamReader(process.getInputStream())); // 讀取命令執行結果 while ((line = successStream.readLine()) != null) { successMsg.append(line); } log.debug("silent install success message:{}", successMsg); // 如果執行結果中包含 Failure 字樣就認為是操作失敗,否則就認為安裝成功 if (!(errorMsg.toString().contains("Failure") || successMsg.toString().contains("Failure"))) { result = true; } } catch (Exception e) { log.error(e); } finally { try { if (process != null) { process.destroy(); } if (dataOutputStream != null) { dataOutputStream.close(); } if (errorStream != null) { errorStream.close(); } if (successStream != null) { successStream.close(); } } catch (Exception e) { // ignored } } return result; } public static boolean isRoot() { return new File("/system/bin/su").exists() || new File("/system/xbin/su").exists(); }
首先申請 Root 許可權,然後執行 pm install- r <apk 路徑>
命令,-r 引數表示允許覆蓋安裝。 process.waitFor() 說明安裝過程是同步的,等待命令執行完成,然後讀取執行結果。注意:不要在主執行緒呼叫靜默安裝的程式碼,安裝過程會比較耗時,導致執行緒一直等待結果。
結論:只要手機被 Root,該方法十分奏效。但是絕大部分使用者不懂 Root,即使手機被 Root了,還需要使用者授權,所以該方案侷限性非常大。
2. 宣告安裝許可權並進行系統簽名來靜默安裝
當我們選擇手動安裝應用時,會跳轉到應用安裝介面,這個介面就是系統的 PackageInstaller 提供,專門用來讓使用者有感知地安裝應用。
Intent intent = new Intent(Intent.ACTION_VIEW); Uri uri = Uri.fromFile(new File("/sdcard/news.apk"))); intent.setDataAndType(uri, "application/vnd.android.package-archive"); startActivity(intent);
分析 PackageInstaller 的原始碼,我們發現它會通過 PackageManager 呼叫 installPackage 方法,這是個隱藏的抽象方法,實現類是 ApplicationPackageManager。主要看一下四個引數:packageURI 就是 apk 的路徑;observer 是安裝的監聽器,應用安裝完成時會被回撥,不能為 null;flags 是標誌位,指定安裝的引數;installersPackageName 表示可選的安裝來源,比如應用寶之類的。
public abstract void installPackage( Uri packageURI, PackageInstallObserver observer, int flags, String installerPackageName);
ApplicationPackageManager 裡面 mPM 是一個 IPackageManager 型別的物件,它會執行具體的安裝任務。
try { mPM.installPackage(originPath, observer.getBinder(), flags, installerPackageName, verificationParams, null); } catch (RemoteException ignored) { }
ContextImpl 的 getPackageManager 方法,通過 ActivityThread 獲取 IPackageManager 物件用來構造 ApplicationPackageManager,然後返回 ApplicationPackageManager。
public PackageManager getPackageManager() { if (mPackageManager != null) { return mPackageManager; } IPackageManager pm = ActivityThread.getPackageManager(); if (pm != null) { // Doesn't matter if we make more than one instance. return (mPackageManager = new ApplicationPackageManager(this, pm)); } return null; }
ActivityThread 的 getPackageManager 方法,其實就是獲取系統服務的過程。
public static IPackageManager getPackageManager() { if (sPackageManager != null) { //Slog.v("PackageManager", "returning cur default = " + sPackageManager); return sPackageManager; } IBinder b = ServiceManager.getService("package"); //Slog.v("PackageManager", "default service binder = " + b); sPackageManager = IPackageManager.Stub.asInterface(b); //Slog.v("PackageManager", "default service = " + sPackageManager); return sPackageManager; }
通過以上分析,我們通過 PackageManager 呼叫 installPackage 方法就行了,下面看程式碼:
public static boolean silentInstall(PackageManager packageManager, String apkPath) { Class<?> pmClz = packageManager.getClass(); try { if (Build.VERSION.SDK_INT >= 21) { Class<?> aClass = Class.forName("android.app.PackageInstallObserver"); Constructor<?> constructor = aClass.getDeclaredConstructor(); constructor.setAccessible(true); Object installObserver = constructor.newInstance(); Method method = pmClz.getDeclaredMethod("installPackage", Uri.class, aClass, int.class, String.class); method.setAccessible(true); method.invoke(packageManager, Uri.fromFile(new File(apkPath)), installObserver, 2, null); } else { Method method = pmClz.getDeclaredMethod("installPackage", Uri.class, Class.forName("android.content.pm.IPackageInstallObserver"), int.class, String.class); method.setAccessible(true); method.invoke(packageManager, Uri.fromFile(new File(apkPath)), null, 2, null); } return true; } catch (Exception e) { log.error(e); } return false; }
由於 PackageManager 在不同版本上的 installPackage 方法引數不一致,所以我們根據編譯版本做了處理。在 API 21 及以上,需要傳遞一個非 null 的 PackageInstallObserver,這個類是不可見 的,我們就用反射建立一個 observer 物件,flags 指定 INSTALL_REPLACE_EXISTING
,用常量表示就是 2。在 API 21 以下,observer 型別是IPackageInstallObserver,同樣使用反射處理即可。
最後宣告許可權 <uses-permission android:name="android.permission.INSTALL_PACKAGES"/>
,還要使用系統簽名,這個非常關鍵,要不然就會出現異常: java.lang.SecurityException: Neither user 10052 nor current process has android.permission.INSTALL_PACKAGES.
。
結論:通過呼叫系統 API 靜默安裝,終於可以堂堂正正地搞事情了!雖然這是官方提供的介面,但是為了不讓你為所欲為,強制使用系統簽名,所以對於第三方應用採用的可能性是零。
3. 使用輔助功能進行安裝
現在大多數應用採取的是這種辦法,讓使用者主動開啟輔助功能,然後模擬點選使用者操作,進行自動安裝。核心就是 AccessibilityService,我們來實現這一功能。
-
建立 AccessibilityService 配置檔案
在 res 目錄下建立 xml 目錄,然後在 xml 目錄下建立 accessibility_service_config.xml 檔案,內容如下
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeAllMask" android:accessibilityFeedbackType="feedbackGeneric" android:accessibilityFlags="flagDefault" android:canRetrieveWindowContent="true" android:description="@string/accessibility_service_description" android:packageNames="com.android.packageinstaller" />
accessibilityEventTypes: 指定我們在監聽視窗中可以模擬哪些事件,typeAllMask 表示所有的事件都能模擬。
accessibilityFeedbackType: 指定無障礙服務的反饋方式。
canRetrieveWindowContent: 指定是否允許我們的程式讀取視窗中的節點和內容,當然是 true。
description: 當用戶手動配置服務時,顯示給使用者看的說明資訊。
packageNames: 指定要監聽哪個應用程式下的視窗活動,這裡寫 com.android.packageinstaller 表示監聽 Android 系統的安裝介面。
配置裡面描述的內容
<resources> <string name="app_name">InstallTest</string> <string name="accessibility_service_description">智慧安裝服務,無需使用者的任何操作就可以自動安裝程式。</string> </resources>
-
建立 AccessibilityService 服務
建立一個類繼承自 AccessibilityService ,然後重寫 onAccessibilityEvent 方法。
public class MyAccessibilityService extends AccessibilityService { private static final String TAG = "[TAG]"; private Map<Integer, Boolean> handleMap = new HashMap<>(); @Override public void onAccessibilityEvent(AccessibilityEvent event) { AccessibilityNodeInfo nodeInfo = event.getSource(); if (nodeInfo != null) { int eventType = event.getEventType(); if (eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED || eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { if (handleMap.get(event.getWindowId()) == null) { boolean handled = iterateNodesAndHandle(nodeInfo); if (handled) { handleMap.put(event.getWindowId(), true); } } } } } @Override public void onInterrupt() { } // 遍歷節點,模擬點選安裝按鈕 private boolean iterateNodesAndHandle(AccessibilityNodeInfo nodeInfo) { if (nodeInfo != null) { int childCount = nodeInfo.getChildCount(); if ("android.widget.Button".equals(nodeInfo.getClassName())) { String nodeCotent = nodeInfo.getText().toString(); Log.d(TAG, "content is: " + nodeCotent); if ("安裝".equals(nodeCotent) || "完成".equals(nodeCotent) || "確定".equals(nodeCotent)) { nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK); return true; } } // 遇到 ScrollView 的時候模擬滑動一下 else if ("android.widget.ScrollView".equals(nodeInfo.getClassName())) { nodeInfo.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); } for (int i = 0; i < childCount; i++) { AccessibilityNodeInfo childNodeInfo = nodeInfo.getChild(i); if (iterateNodesAndHandle(childNodeInfo)) { return true; } } } return false; } }
當進入安裝介面就會回撥 onAccessibilityEvent() 這個方法,我們只處理 TYPE_WINDOW_CONTENT_CHANGED 和 TYPE_WINDOW_STATE_CHANGED 兩個事件。為了防止重複處理事件,用 map 來過濾事件,然後遞迴遍歷節點,找到「安裝」、「完成」、「缺點」的按鈕就模擬點選。由於安裝介面需要使用者看完許可權才出現按鈕,所以遇到 ScrollView 的時候就模擬滾動,直到出現安裝按鈕。
- 在 AndroidManifest 中配置服務
<service android:name=".MyAccessibilityService" android:label="智慧安裝應用" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" > <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService"/> </intent-filter> <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibility_service_config" /> </service>
android:label:這個就是使用者看到的無障礙服務的名稱。
android:permission: 需要用到 BIND_ACCESSIBILITY_SERVICE 這個許可權。
action: android.accessibilityservice.AccessibilityService 有了這個 action,使用者才能在設定裡面看到我們的服務,否則使用者無法開啟我們的 AccessibilityService,也就不能進到 MyAccessibilityService 裡面了。
- 呼叫智慧安裝程式碼
private void smartInstall() { Uri uri = Uri.fromFile(new File("/sdcard/test.apk")); Intent localIntent = new Intent(Intent.ACTION_VIEW); localIntent.setDataAndType(uri, "application/vnd.android.package-archive"); startActivity(localIntent); }
-
手動配置智慧安裝服務
跳轉輔助功能的配置介面,引導使用者開啟智慧安裝服務。
Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); startActivity(intent);
總結
智慧安裝是一種妥協的方案,在沒有 Root 和安裝許可權的情況下,確實解放了使用者的拇指。看看市面上的應用,大部分都採用了這種方法。應用市場使用智慧安裝可以理解,視訊瀏覽器工具一類不相干的應用也要開啟?我真是呵呵了。

帶有智慧安裝的應用

資料圖
需要資料的朋友可以加入Android架構交流QQ群聊:513088520
點選連結加入群聊【Android移動架構總群】: 加入群聊
獲取免費學習視訊,學習大綱另外還有像高階UI、效能優化、架構師課程、NDK、混合式開發(ReactNative+Weex)等Android高階開發資料免費分享。