原文地址:http://m.itdecent.cn/p/86253b2c49f3
防刪備份資料
0 前言
最近有了個(gè)需求:免 root 實(shí)現(xiàn)任意位置點(diǎn)擊和靜默安裝。這個(gè)做過的小伙伴應(yīng)該都知道正常情況下是不可能實(shí)現(xiàn)的。無(wú)障礙只能實(shí)現(xiàn)對(duì)已知控件的點(diǎn)擊,并不能指定坐標(biāo)【注1】。但是確實(shí)有人另辟蹊徑做出來(lái)了,譬如做游戲手柄的飛智,他們是用一個(gè)激活器,手機(jī)開 usb 調(diào)試,然后插在激活器上并授權(quán),飛智游戲廳就被「激活」了,然后可以實(shí)現(xiàn)任意位置點(diǎn)擊。如果不了解的可以去他們官網(wǎng)了解下,在這里不多贅述了。無(wú)獨(dú)有偶,黑域也使用了類似的手段,也可以用電腦的usb調(diào)試激活。我們知道,任意位置坐標(biāo)xy點(diǎn)擊是可以在 pc 上通過 shell 命令「input tap x y」來(lái)實(shí)現(xiàn)的,也不需要 root 權(quán)限。但是在應(yīng)用內(nèi)通過「Runtime.getRuntime().exec」執(zhí)行這個(gè) shell 命令卻提示「permission denied」也就是權(quán)限不足。但是飛智或者黑域卻好像使用了某種魔法,提升了自己的權(quán)限,那么問題來(lái)了:如何用 usb 調(diào)試給 app 提權(quán)?
1 原理揭曉
「如何用 usb 調(diào)試給 app 提權(quán)」這個(gè)問題乍一看確實(shí)沒問題,但是知乎有個(gè)回答是「先問是不是,再問為什么」我覺得說的很好。我被這個(gè)問題給困擾了很久,最后發(fā)現(xiàn)我問錯(cuò)了。先放出結(jié)論「并不是給 app 提權(quán),而是運(yùn)行了一個(gè)有 shell 權(quán)限的新程序」
剛才的問題先放一邊,我來(lái)問大家個(gè)新問題,怎樣讓 app 獲取 root 權(quán)限?這個(gè)問題答案已經(jīng)有不少了,網(wǎng)上一查便可知其實(shí)是獲取「Runtime.getRuntime().exec」的流,在里面用su提權(quán),然后就可以執(zhí)行需要 root 權(quán)限的 shell 命令,比如掛載 system 讀寫,訪問 data 分區(qū),用 shell 命令靜默安裝,等等。話說回來(lái),是不是和我們今天的主題有點(diǎn)像,如何使 app 獲取 shell 權(quán)限?嗯,其實(shí)差不多,思路也類似,因?yàn)楸緛?lái) root 啦, shell 啦,根本就不是 Android 應(yīng)用層的名詞呀,他們本來(lái)就是 Linux 里的名詞,只不過是 Android 框架運(yùn)行于 Linux 層之上, 我們可以調(diào)用 shell 命令,也可以在shell 里調(diào)用 su 來(lái)使shell 獲取 root 權(quán)限,來(lái)繞過 Android 層做一些被限制的事。然而在 app 里調(diào)用 shell 命令,其進(jìn)程還是 app 的,權(quán)限還是受限。所以就不能在 app 里運(yùn)行 shell 命令,那么問題來(lái)了,不在 app 里運(yùn)行在哪運(yùn)行?答案是在 pc 上運(yùn)行。當(dāng)然不可能是 pc 一直連著手機(jī)啦,而是 pc 上在 shell 里運(yùn)行獨(dú)立的一個(gè) java 程序,這個(gè)程序因?yàn)槭窃?shell 里啟動(dòng)的,所以具有 shell 權(quán)限。我們想一下,這個(gè) Java 程序在 shell 里運(yùn)行,建立本地 socket 服務(wù)器,和 app 通信,遠(yuǎn)程執(zhí)行 app 下發(fā)的代碼。因?yàn)榧词拱蔚袅藬?shù)據(jù)線,這個(gè) Java 程序也不會(huì)停止,只要不重啟他就一直活著,執(zhí)行我們的命令,這不就是看起來(lái) app 有了 shell 權(quán)限?現(xiàn)在真相大白,飛智和黑域用 usb 調(diào)試激活的那一下,其實(shí)是啟動(dòng)那個(gè) Java 程序,飛智是執(zhí)行模擬按鍵,黑域是監(jiān)聽系統(tǒng)事件,你想干啥就任你開發(fā)了?!缸ⅲ汉谟蚝惋w智由于進(jìn)程管理的需要,其實(shí)是先用 shell 啟動(dòng)一個(gè) so ,然后再用 so 做跳板啟動(dòng) Java 程序,而且 so 也充當(dāng)守護(hù)進(jìn)程,當(dāng) Java 意外停止可以重新啟動(dòng),讀著有興趣可以自行研究,在此不多做說明」
2 好耶!是 app_process
那么如何具體用 shell 運(yùn)行 Java 程序呢?肯定不是「java xxx.jar」啦,Android 能運(yùn)行的格式是 dex 。沒錯(cuò),就是apk 里那個(gè) dex 。然后我們可以通過「app_process」開啟動(dòng) Java 。app_process 的參數(shù)如下
app_process [vm-options] cmd-dir [options] start-class-name [main-options]
這個(gè)詭異又可怕的東西是沒有 -help 的。我們要么看源碼,要么看別人分析好的。本人水平有限,這里選擇看別人分析好的:
vm-options – VM 選項(xiàng)
cmd-dir –父目錄 (/system/bin)
options –運(yùn)行的參數(shù) :
–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 –啟動(dòng)時(shí)候傳遞到main方法中的參數(shù)
3 實(shí)踐
因?yàn)槭?dex 我們就直接在 as 里寫吧,提取 dex 也方便。新建個(gè)空白項(xiàng)目,初始結(jié)構(gòu)是這樣:
我們新建個(gè)包,存放我們要在 shell 下運(yùn)行的 Java 代碼:
這里我們補(bǔ)全 Main 方法,因?yàn)槲覀冞@個(gè)不是個(gè) Android 程序,只是編譯成 dex 的純 Java 程序,所以我們這個(gè)的入口是 Main :
package shellService;
public class Main {
public static void main(String[] args){
System.out.println("我是在 shell 里運(yùn)行的?。?!");
}
}
我們?cè)诖a里只是打印一行「我是在 shell 里運(yùn)行的!?。 ?,因?yàn)檫@里是純 Java 所以也用的 println?,F(xiàn)在編譯 apk:
因?yàn)?apk 就是 zip 所以我們直接解壓出 apk 文件里的classes.dex,然后執(zhí)行 :
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
這時(shí)就能看到已經(jīng)成功運(yùn)行啦:
這里因?yàn)?utf8 在 Windows shell 里有問題,所以亂碼了,但是還是說明我們成功了。
4 具有實(shí)用性
只能輸出肯定是不行的,不具有實(shí)用性。我們之前說過,我們應(yīng)該建立個(gè)本地 socket 服務(wù)器來(lái)接受命令并執(zhí)行,這里的「Service」類實(shí)現(xiàn)了這個(gè)功能,因?yàn)槿绾谓?socket 不是文章的重點(diǎn),所以大家只要知道這個(gè)類內(nèi)部實(shí)現(xiàn)了一個(gè)「ServiceGetText」接口,在收到命令之后會(huì)把命令內(nèi)容作為參數(shù)回掉 getText 方法,然后我們執(zhí)行 shell 命令之后,吧結(jié)果作為字符串返回即可,具體實(shí)現(xiàn)可以看查看源碼Service。
我們新建一個(gè)「ServiceThread」來(lái)運(yùn)行「Service」服務(wù)和執(zhí)行設(shè)立了命令:
public class ServiceThread extends Thread {
private static int ShellPORT = 4521;
@Override
public void run() {
System.out.println(">>>>>>Shell服務(wù)端程序被調(diào)用<<<<<<");
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 用到了開源項(xiàng)目 ShellUtils 在此感謝。這個(gè)類用來(lái)執(zhí)行 shell 命令。
然后在 Main 中調(diào)用這個(gè)線程:
public class Main {
public static void main(String[] args){
new ServiceThread().start();
while (true);
}
}
這樣,我們服務(wù)端就準(zhǔn)備好了,我們來(lái)寫控制服務(wù)端的 app 。我們新建類「SocketClient」用來(lái)和服務(wù)端進(jìn)行通信,并在活動(dòng)里調(diào)用他(完整代碼請(qǐng)參看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();
}
然后重復(fù) 3 小節(jié)的操作,運(yùn)行這個(gè)服務(wù)端:
然后安裝 apk ,運(yùn)行:
input text HelloWord

可以看到,在不 root 的情況下,成功的執(zhí)行了需要 shell 權(quán)限的命令
5 最可愛的人
最后,我真的是要由衷的感謝各種技術(shù)分析文章和開源項(xiàng)目,真的太感謝了,沒有無(wú)條件的奉獻(xiàn)就沒有互聯(lián)網(wǎng)這么快的進(jìn)步。
我對(duì) app_process 利用方法的研究離不開以下項(xiàng)目和前輩的汗水:
Brevent 最早利用app_process進(jìn)程實(shí)現(xiàn)無(wú) root 權(quán)限使用的開源應(yīng)用(雖然已經(jīng)閉源,仍然尊重并感謝 liudongmiao)
Android system log viewer on Android phone without root. 利用app_process進(jìn)程實(shí)現(xiàn)無(wú) root 權(quán)限使用的優(yōu)秀開源應(yīng)用
Android上app_process啟動(dòng)java進(jìn)程 通俗易懂的教程
使用 app_process 來(lái)調(diào)用高權(quán)限 API 分析的很深刻的教程
本文的項(xiàng)目可以在GitHub上獲取:https://github.com/gtf35/app_process-shell-use
作者:gtf
鏈接:http://m.itdecent.cn/p/86253b2c49f3
來(lái)源:簡(jiǎn)書
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。
補(bǔ)充
原文評(píng)論區(qū):
hengbaby
11樓
拔掉數(shù)據(jù)線就停止運(yùn)行的可以試試在 app-process 前加 nohup 試試,我加了這個(gè)拔掉數(shù)據(jù)線就正常了
【注1】Android N 之后,可以在輔助功能中通過 dispatchGesture 來(lái)實(shí)現(xiàn)了。