Android免root獲取shell級權(quán)限實現(xiàn)靜默卸載安裝

方案分析

市面上實現(xiàn)這種方案最早的應(yīng)用應(yīng)該是"黑閾",我們在使用的時候需要開啟調(diào)試模式触菜,然后通過adb或者注入器注入主服務(wù)逻澳,才可以使用后臺管制以及其他高級權(quán)限的功能。所以本方案也是基于這種注入服務(wù)的方式犬绒,來實現(xiàn)各種需要高級權(quán)限的功能

Shell級權(quán)限的服務(wù)

這種方案的關(guān)鍵點(diǎn)是這個擁有shell級權(quán)限的服務(wù),Android提供了app_process指令供我們啟動一個進(jìn)程兑凿,我們可以通過該指令起一個Java服務(wù)凯力,如果是通過shell執(zhí)行的,該服務(wù)會從/system/bin/sh fork出來礼华,并且擁有shell級權(quán)限

這里我寫了一個service.dex服務(wù)來測試一下咐鹤,并通過shell啟動它

// 先將service.dex push至Android設(shè)備
adb push service.dex /data/local/tmp/

// 然后通過app_process啟動,并指定一個名詞
adb shell nohup app_process -Djava.class.path=/data/local/tmp/server.dex /system/bin --nice-name=club.syachiku.hackrootservice shellService.Main

然后再看看該服務(wù)的信息

// 列出所有正在運(yùn)行的服務(wù)
adb shell ps

// 找到服務(wù)名為club.syachiku.hackrootservice的服務(wù)
shell     24154 1     777484 26960 ffffffff b6e7284c S club.syachiku.hackrootservice

可以看到該服務(wù)pid為24154圣絮,ppid為1祈惶,也說明該服務(wù)是從/system/bin/sh fork出來的

// 查看該服務(wù)具體信息
adb shell cat /proc/24154/status

Name:   main
State:  S (sleeping)
Tgid:   24154
Pid:    24154
PPid:   1
TracerPid:  0
Uid:    2000    2000    2000    2000
Gid:    2000    2000    2000    2000
FDSize: 32
Groups: 1004 1007 1011 1015 1028 3001 3002 3003 3006
VmPeak:   777484 kB
VmSize:   777484 kB
VmLck:         0 kB
VmPin:         0 kB
VmHWM:     26960 kB
VmRSS:     26960 kB
VmData:    11680 kB
VmStk:      8192 kB
VmExe:        12 kB
VmLib:     52812 kB
VmPTE:       134 kB
VmSwap:        0 kB
Threads:    13
SigQ:   0/6947
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000001204
SigIgn: 0000000000000001
SigCgt: 00000002000094f8
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 00000000000000c0
Seccomp:    0
Cpus_allowed:   f
Cpus_allowed_list:  0-3
voluntary_ctxt_switches:    18
nonvoluntary_ctxt_switches: 76

可以看到Uid,Gid為2000,就是shell的Uid

開始吧(本方案也需開啟調(diào)試模式)

分析了app_process的可行性捧请,我們可以給出一個方案凡涩,通過app_process啟動一個socket服務(wù),然后讓我們的App與該服務(wù)通信疹蛉,來代理App做一些見不得人需要shell級權(quán)限的事情活箕,比如靜默卸載,安裝氧吐,全局廣播等等

新建工程

這里我們新建一個名為hack-root的工程

編寫socket服務(wù)

然后在代碼目錄下新建一個shellService包讹蘑,新建一個Main入口類,我們先輸出一些測試代碼筑舅,來測試是否執(zhí)行成功

public class Main {
    public static void main(String[] args) {
        System.out.println("*****************hack server starting****************");
    }
}
  • 首先執(zhí)行./gradlew buildDebug打包座慰,然后.apk改成.rar解壓出classes.dex文件,然后將該文件push至你的Android設(shè)備比如/sdcard/
  • 然后使用app_process指令執(zhí)行該服務(wù)
    adb shell app_process -Djava.class.path=/sdcard/classes.dex /system/bin shellService.Main
    
  • 如果控制臺輸出Abort應(yīng)該是一些基本的路徑問題翠拣,稍作仔細(xì)檢查一下版仔,成功執(zhí)行后會看到我們的打印的日志

運(yùn)行測試沒問題了就開寫socket服務(wù)吧

public class Main {
    public static void main(String[] args) {
        // 利用looper讓線程循環(huán)
        Looper.prepareMainLooper();
        System.out.println("*****************hack server starting****************");
        // 開一個子線程啟動服務(wù)
        new Thread(new Runnable() {
            @Override
            public void run() {
                new SocketService(new SocketService.SocketListener() {
                    @Override
                    public String onMessage(String msg) {
                        // 接收客戶端傳過來的消息
                        return resolveMsg(msg);
                    }
                });
            }
        }).start();
        Looper.loop();
    }

    private static String resolveMsg(String msg) {
        // 執(zhí)行客戶端傳過來的消息并返回執(zhí)行結(jié)果
        ShellUtil.ExecResult execResult =
                ShellUtil.execute("pm uninstall " + msg);
        return execResult.getMessage();
    }
}

SocketServer

public class SocketService {
    private final int PORT = 10500;
    private SocketListener listener;

    public SocketService(SocketListener listener) {
        this.listener = listener;
        try {
            // 利用ServerSocket類啟動服務(wù),然后指定一個端口
            ServerSocket serverSocket = new ServerSocket(PORT);
            System.out.println("server running " + PORT + " port");
            ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(10);
            // 新建一個線程池用來并發(fā)處理客戶端的消息
            ThreadPoolExecutor executor = new ThreadPoolExecutor(
                    5,
                    10,
                    5000,
                    TimeUnit.MILLISECONDS,
                    queue
                    );
            while (true) {
                Socket socket = serverSocket.accept();
                // 接收到新消息
                executor.execute(new processMsg(socket));
            }
        } catch (Exception e) {
            System.out.println("SocketServer create Exception:" + e);
        }
    }

    class processMsg implements Runnable {
        Socket socket;

        public processMsg(Socket s) {
            socket = s;
        }

        public void run() {
            try {
                // 通過流讀取內(nèi)容
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                String line = bufferedReader.readLine();
                System.out.println("server receive: " + line);
                PrintWriter printWriter = new PrintWriter(socket.getOutputStream());
                String repeat = listener.onMessage(line);
                System.out.println("server send: " + repeat);
                // 服務(wù)端返回給客戶端的消息
                printWriter.print(repeat);
                printWriter.flush();
                printWriter.close();
                bufferedReader.close();
                socket.close();
            } catch (IOException e) {
                System.out.println("socket connection error:" + e.toString());
            }
        }
    }

    public interface SocketListener{
        // 通話消息回調(diào)
        String onMessage(String text);
    }
}

ShellUtil

public class ShellUtil {
    private static final String COMMAND_LINE_END = "\n";
    private static final String COMMAND_EXIT = "exit\n";

    // 單條指令
    public static ExecResult execute(String command) {
        return execute(new String[] {command});
    }

    // 多條指令重載方法
    private static ExecResult execute(String[] commands) {
        if (commands == null || commands.length == 0) {
            return new ExecResult(false, "empty command");
        }
        int result = -1;
        Process process = null;
        DataOutputStream dataOutputStream = null;
        BufferedReader sucResult = null, errResult = null;
        StringBuilder sucMsg = null, errMsg = null;

        try {
            // 獲取shell級別的process
            process = Runtime.getRuntime().exec("sh");
            dataOutputStream = new DataOutputStream(process.getOutputStream());
            for (String command : commands) {
                if (command == null) continue;
                System.out.println("execute command: " + command);
                // 執(zhí)行指令
                dataOutputStream.write(command.getBytes());
                dataOutputStream.writeBytes(COMMAND_LINE_END);
                // 刷新
                dataOutputStream.flush();
            }
            dataOutputStream.writeBytes(COMMAND_EXIT);
            dataOutputStream.flush();
            result = process.waitFor();
            sucMsg = new StringBuilder();
            errMsg = new StringBuilder();
            sucResult = new BufferedReader(new InputStreamReader(process.getInputStream()));
            errResult = new BufferedReader(new InputStreamReader(process.getErrorStream()));
            String s;
            while ((s = sucResult.readLine()) != null) {
                sucMsg.append(s);
            }
            while ((s = errResult.readLine()) != null) {
                errMsg.append(s);
            }

        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        } finally {
            try {
                // 關(guān)閉資源误墓,防止內(nèi)存泄漏
                assert dataOutputStream != null;
                dataOutputStream.close();
                assert sucResult != null;
                sucResult.close();
                assert errResult != null;
                errResult.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            process.destroy();
        }
        ExecResult execResult;
        if (result == 0) {
            execResult = new ExecResult(true, sucMsg.toString());
        } else {
            execResult = new ExecResult(false, errMsg.toString());
        }
        // 返回執(zhí)行結(jié)果
        return execResult;
    }

    public static class ExecResult {
        private boolean success;
        private String message;

        public ExecResult(boolean success, String message) {
            this.success = success;
            this.message = message;
        }

        public boolean getSuccess() {
            return this.success;
        }

        public String getMessage() {
            return this.message;
        }
    }
}

一個簡易的socket服務(wù)就搭建好了蛮粮,可以用來接收客戶端傳過來的指令并且執(zhí)行然后返回結(jié)果

編寫客戶端

首先編寫一個socketClient

public class SocketClient {
    private final String TAG = "HackRoot SocketClient";
    private final int PORT = 10500;
    private SocketListener listener;
    private PrintWriter printWriter;

    public SocketClient(final String cmd, SocketListener listener) {
        this.listener = listener;
        new Thread(new Runnable() {
            @Override
            public void run() {
                Socket socket = new Socket();
                try {
                    // 與hackserver建立連接
                    socket.connect(new InetSocketAddress("127.0.0.1", PORT), 3000);
                    socket.setSoTimeout(3000);
                    printWriter = new PrintWriter(socket.getOutputStream(), true);
                    Log.d(TAG, "client send: " + cmd);
                    // 發(fā)送指令
                    printWriter.println(cmd);
                    printWriter.flush();
                    // 讀取服務(wù)端返回
                    readServerData(socket);
                } catch (IOException e) {
                    Log.d(TAG, "client send fail: " + e.getMessage());
                    e.printStackTrace();
                }
            }
        }).start();
    }

    private void readServerData(final Socket socket) {
        try {
            InputStreamReader ipsReader = new InputStreamReader(socket.getInputStream());
            BufferedReader bfReader = new BufferedReader(ipsReader);
            String line = null;
            while ((line = bfReader.readLine()) != null) {
                Log.d(TAG, "client receive: " + line);
                listener.onMessage(line);
            }
            // 釋放資源
            ipsReader.close();
            bfReader.close();
            printWriter.close();
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    interface SocketListener {
        void onMessage(String msg);
    }
}

然后UI組件相關(guān)的事件,我們暫時只實現(xiàn)一個靜默卸載App的功能

public class MainActivity extends AppCompatActivity {
    private TextView textView;
    private ScrollView scrollView;
    private EditText uninsTxtInput;
    private Button btnUnins;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btnUnins = findViewById(R.id.btn_uninstall);
        uninsTxtInput = findViewById(R.id.pkg_input);
        textView = findViewById(R.id.tv_output);
        scrollView = findViewById(R.id.text_container);
        btnUnins.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                sendMessage(uninsTxtInput.getText().toString());
            }
        });
    }

    private void sendMessage(String msg) {
        new SocketClient(msg, new SocketClient.SocketListener() {
            @Override
            public void onMessage(String msg) {
                showOnTextView(msg);
            }
        });
    }

    private void showOnTextView(final String msg) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                String baseText = textView.getText().toString();
                if (baseText != null) {
                    textView.setText(baseText + "\n" + msg);
                } else {
                    textView.setText(msg);
                }
                scrollView.smoothScrollTo(0, scrollView.getHeight());
            }
        });
    }
}

布局代碼

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/pkg_input"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:hint="input package name"
        app:layout_constraintEnd_toStartOf="@+id/btn_uninstall"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btn_uninstall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginTop="8dp"
        android:text="uninstall"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ScrollView
        android:id="@+id/text_container"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:padding="10dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/pkg_input">
        <TextView
            android:id="@+id/tv_output"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </ScrollView>
</android.support.constraint.ConstraintLayout>

代碼相關(guān)的工作基本完工谜慌,一個簡單的然想,實現(xiàn)了靜默卸載Demo就完成了

打包測試

  • ./gradlew assembleRelease 打出apk
  • 后綴改成.rar解壓出classes.dex
  • 將classes.dex push至/data/local/tmp/
  • 執(zhí)行服務(wù)
    • 前臺執(zhí)行:
    // 拔掉數(shù)據(jù)線會終止服務(wù)
    adb shell app_process -Djava.class.path=/data/local/tmp/classes.dex /system/bin shellService.Main
    
    • 后臺執(zhí)行:
    // 會一直運(yùn)行除非手動kill pid或者重啟設(shè)備
    adb shell nohup app_process -Djava.class.path=/data/local/tmp/classes.dex /system/bin --nice-name=${serviceName} shellService.Main
    
  • 安裝apk,輸入要卸載的包名欣范,點(diǎn)擊UNINSTALL進(jìn)行靜默卸載

完整項目

https://github.com/zjkhiyori/hack-root 歡迎fork || star

example

技術(shù)參考

感謝下列開源作者

android-common

Fairy

app_process-shell-use

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末变泄,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子恼琼,更是在濱河造成了極大的恐慌妨蛹,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件晴竞,死亡現(xiàn)場離奇詭異蛙卤,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)噩死,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進(jìn)店門颤难,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人甜滨,你說我怎么就攤上這事乐严。” “怎么了衣摩?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我艾扮,道長既琴,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任泡嘴,我火速辦了婚禮甫恩,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘酌予。我一直安慰自己磺箕,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布抛虫。 她就那樣靜靜地躺著松靡,像睡著了一般。 火紅的嫁衣襯著肌膚如雪建椰。 梳的紋絲不亂的頭發(fā)上雕欺,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天,我揣著相機(jī)與錄音棉姐,去河邊找鬼屠列。 笑死,一個胖子當(dāng)著我的面吹牛伞矩,可吹牛的內(nèi)容都是我干的笛洛。 我是一名探鬼主播,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼乃坤,長吁一口氣:“原來是場噩夢啊……” “哼苛让!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起侥袜,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蝌诡,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后枫吧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體浦旱,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年九杂,在試婚紗的時候發(fā)現(xiàn)自己被綠了颁湖。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡例隆,死狀恐怖甥捺,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情镀层,我是刑警寧澤镰禾,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響吴侦,放射性物質(zhì)發(fā)生泄漏屋休。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一备韧、第九天 我趴在偏房一處隱蔽的房頂上張望劫樟。 院中可真熱鬧,春花似錦织堂、人聲如沸叠艳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽附较。三九已至,卻和暖如春闽烙,著一層夾襖步出監(jiān)牢的瞬間翅睛,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工黑竞, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留捕发,地道東北人。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓很魂,卻偏偏與公主長得像扎酷,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子遏匆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,515評論 2 359

推薦閱讀更多精彩內(nèi)容