- 本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家發布
0 前言
最近有了個需求:免 root 實現任意位置點擊和靜默安裝。這個做過的小伙伴應該都知道正常情況下是不可能實現的。無障礙只能實現對已知控件的點擊,并不能指定坐標。但是確實有人另辟蹊徑做出來了,譬如做游戲手柄的飛智,他們是用一個激活器,手機開 usb 調試,然后插在激活器上并授權,飛智游戲廳就被「激活」了,然后可以實現任意位置點擊。如果不了解的可以去他們官網了解下,在這里不多贅述了。無獨有偶,黑域也使用了類似的手段,也可以用電腦的usb調試激活。我們知道,任意位置坐標xy點擊是可以在 pc 上通過 shell 命令「input tap x y」來實現的,也不需要 root 權限。但是在應用內通過「Runtime.getRuntime().exec」執行這個 shell 命令卻提示「permission denied」也就是權限不足。但是飛智或者黑域卻好像使用了某種魔法,提升了自己的權限,那么問題來了:如何用 usb 調試給 app 提權?
1 原理揭曉
「如何用 usb 調試給 app 提權」這個問題乍一看確實沒問題,但是知乎有個回答是「先問是不是,再問為什么」我覺得說的很好。我被這個問題給困擾了很久,最后發現我問錯了。先放出結論「并不是給 app 提權,而是運行了一個有 shell 權限的新程序」
剛才的問題先放一邊,我來問大家個新問題,怎樣讓 app 獲取 root 權限?這個問題答案已經有不少了,網上一查便可知其實是獲取「Runtime.getRuntime().exec」的流,在里面用su提權,然后就可以執行需要 root 權限的 shell 命令,比如掛載 system 讀寫,訪問 data 分區,用 shell 命令靜默安裝,等等。話說回來,是不是和我們今天的主題有點像,如何使 app 獲取 shell 權限?嗯,其實差不多,思路也類似,因為本來 root 啦, shell 啦,根本就不是 Android 應用層的名詞呀,他們本來就是 Linux 里的名詞,只不過是 Android 框架運行于 Linux 層之上, 我們可以調用 shell 命令,也可以在shell 里調用 su 來使shell 獲取 root 權限,來繞過 Android 層做一些被限制的事。然而在 app 里調用 shell 命令,其進程還是 app 的,權限還是受限。所以就不能在 app 里運行 shell 命令,那么問題來了,不在 app 里運行在哪運行?答案是在 pc 上運行。當然不可能是 pc 一直連著手機啦,而是 pc 上在 shell 里運行獨立的一個 java 程序,這個程序因為是在 shell 里啟動的,所以具有 shell 權限。我們想一下,這個 Java 程序在 shell 里運行,建立本地 socket 服務器,和 app 通信,遠程執行 app 下發的代碼。因為即使拔掉了數據線,這個 Java 程序也不會停止,只要不重啟他就一直活著,執行我們的命令,這不就是看起來 app 有了 shell 權限?現在真相大白,飛智和黑域用 usb 調試激活的那一下,其實是啟動那個 Java 程序,飛智是執行模擬按鍵,黑域是監聽系統事件,你想干啥就任你開發了?!缸ⅲ汉谟蚝惋w智由于進程管理的需要,其實是先用 shell 啟動一個 so ,然后再用 so 做跳板啟動 Java 程序,而且 so 也充當守護進程,當 Java 意外停止可以重新啟動,讀著有興趣可以自行研究,在此不多做說明」
2 好耶!是 app_process
那么如何具體用 shell 運行 Java 程序呢?肯定不是「java xxx.jar」啦,Android 能運行的格式是 dex 。沒錯,就是apk 里那個 dex 。然后我們可以通過「app_process」開啟動 Java 。app_process 的參數如下
app_process [vm-options] cmd-dir [options] start-class-name [main-options]
這個詭異又可怕的東西是沒有 -help 的。我們要么看源碼,要么看別人分析好的。本人水平有限,這里選擇看別人分析好的:
vm-options – VM 選項
cmd-dir –父目錄 (/system/bin)
options –運行的參數 :
–zygote
–start-system-server
–application (api>=14)
–nice-name=nice_proc_name (api>=14)
start-class-name –包含main方法的主類 (com.android.commands.am.Am)
main-options –啟動時候傳遞到main方法中的參數
3 實踐
因為是 dex 我們就直接在 as 里寫吧,提取 dex 也方便。新建個空白項目,初始結構是這樣:
我們新建個包,存放我們要在 shell 下運行的 Java 代碼:
這里我們補全 Main 方法,因為我們這個不是個 Android 程序,只是編譯成 dex 的純 Java 程序,所以我們這個的入口是 Main :
package shellService;
public class Main {
public static void main(String[] args){
System.out.println("我是在 shell 里運行的?。?!");
}
}
我們在代碼里只是打印一行「我是在 shell 里運行的?。。 梗驗檫@里是純 Java 所以也用的 println?,F在編譯 apk:
因為 apk 就是 zip 所以我們直接解壓出 apk 文件里的classes.dex,然后執行 :
adb push classes.dex /data/local/tmp
cd /data/local/tmp
app_process -Djava.class.path=/data/local/tmp/classes.dex /system/bin shellService.Main
這時就能看到已經成功運行啦:
這里因為 utf8 在 Windows shell 里有問題,所以亂碼了,但是還是說明我們成功了。
4 具有實用性
只能輸出肯定是不行的,不具有實用性。我們之前說過,我們應該建立個本地 socket 服務器來接受命令并執行,這里的「Service」類實現了這個功能,因為如何建立 socket 不是文章的重點,所以大家只要知道這個類內部實現了一個「ServiceGetText」接口,在收到命令之后會把命令內容作為參數回掉 getText 方法,然后我們執行 shell 命令之后,吧結果作為字符串返回即可,具體實現可以看查看源碼Service。
我們新建一個「ServiceThread」來運行「Service」服務和執行設立了命令:
public class ServiceThread extends Thread {
private static int ShellPORT = 4521;
@Override
public void run() {
System.out.println(">>>>>>Shell服務端程序被調用<<<<<<");
new Service(new Service.ServiceGetText() {
@Override
public String getText(String text) {
if (text.startsWith("###AreYouOK")){
return "###IamOK#";
}
try{
ServiceShellUtils.ServiceShellCommandResult sr = ServiceShellUtils.execCommand(text, false);
if (sr.result == 0){
return "###ShellOK#" + sr.successMsg;
} else {
return "###ShellError#" + sr.errorMsg;
}
}catch (Exception e){
return "###CodeError#" + e.toString();
}
}
}, ShellPORT);
}
}
其中 ServiceShellUtils 用到了開源項目 ShellUtils 在此感謝。這個類用來執行 shell 命令。
然后在 Main 中調用這個線程:
public class Main {
public static void main(String[] args){
new ServiceThread().start();
while (true);
}
}
這樣,我們服務端就準備好了,我們來寫控制服務端的 app 。我們新建類「SocketClient」用來和服務端進行通信,并在活動里調用他(完整代碼請參看SocketClient和MainActivity):
private void runShell(final String cmd){
if (TextUtils.isEmpty(cmd)) return;
new Thread(new Runnable() {
@Override
public void run() {
new SocketClient(cmd, new SocketClient.onServiceSend() {
@Override
public void getSend(String result) {
showTextOnTextView(result);
}
});
}
}).start();
}
然后重復 3 小節的操作,運行這個服務端:
然后安裝 apk ,運行:
input text HelloWord
可以看到,在不 root 的情況下,成功的執行了需要 shell 權限的命令
5 最可愛的人
最后,我真的是要由衷的感謝各種技術分析文章和開源項目,真的太感謝了,沒有無條件的奉獻就沒有互聯網這么快的進步。
我對 app_process 利用方法的研究離不開以下項目和前輩的汗水:
Brevent 最早利用app_process進程實現無 root 權限使用的開源應用(雖然已經閉源,仍然尊重并感謝 liudongmiao)
Android system log viewer on Android phone without root. 利用app_process進程實現無 root 權限使用的優秀開源應用
Android上app_process啟動java進程 通俗易懂的教程
使用 app_process 來調用高權限 API 分析的很深刻的教程
本文的項目可以在GitHub上獲取:https://github.com/gtf35/app_process-shell-use