Android免root获取shell级权限实现静默卸载安装

方案分析

市面上实现这种方案最早的应用应该是"黑阈",我们在使用的时候需要开启调试模式,然后通过adb或者注入器注入主服务,才可以使用后台管制以及其他高级权限的功能。所以本方案也是基于这种注入服务的方式,来实现各种需要高级权限的功能

Shell级权限的服务

这种方案的关键点是这个拥有shell级权限的服务,Android提供了app_process指令供我们启动一个进程,我们可以通过该指令起一个Java服务,如果是通过shell执行的,该服务会从/system/bin/sh fork出来,并且拥有shell级权限

这里我写了一个service.dex服务来测试一下,并通过shell启动它

// 先将service.dex push至Android设备
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

然后再看看该服务的信息

// 列出所有正在运行的服务
adb shell ps

// 找到服务名为club.syachiku.hackrootservice的服务
shell     24154 1     777484 26960 ffffffff b6e7284c S club.syachiku.hackrootservice

可以看到该服务pid为24154,ppid为1,也说明该服务是从/system/bin/sh fork出来的

// 查看该服务具体信息
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

开始吧(本方案也需开启调试模式)

分析了app_process的可行性,我们可以给出一个方案,通过app_process启动一个socket服务,然后让我们的App与该服务通信,来代理App做一些见不得人需要shell级权限的事情,比如静默卸载,安装,全局广播等等

新建工程

这里我们新建一个名为hack-root的工程

编写socket服务

然后在代码目录下新建一个shellService包,新建一个Main入口类,我们先输出一些测试代码,来测试是否执行成功

public class Main {
    public static void main(String[] args) {
        System.out.println("*****************hack server starting****************");
    }
}
  • 首先执行./gradlew buildDebug打包,然后.apk改成.rar解压出classes.dex文件,然后将该文件push至你的Android设备比如/sdcard/
  • 然后使用app_process指令执行该服务
    adb shell app_process -Djava.class.path=/sdcard/classes.dex /system/bin shellService.Main
    
  • 如果控制台输出Abort应该是一些基本的路径问题,稍作仔细检查一下,成功执行后会看到我们的打印的日志

运行测试没问题了就开写socket服务吧

public class Main {
    public static void main(String[] args) {
        // 利用looper让线程循环
        Looper.prepareMainLooper();
        System.out.println("*****************hack server starting****************");
        // 开一个子线程启动服务
        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) {
        // 执行客户端传过来的消息并返回执行结果
        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类启动服务,然后指定一个端口
            ServerSocket serverSocket = new ServerSocket(PORT);
            System.out.println("server running " + PORT + " port");
            ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(10);
            // 新建一个线程池用来并发处理客户端的消息
            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 {
                // 通过流读取内容
                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);
                // 服务端返回给客户端的消息
                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{
        // 通话消息回调
        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);
                // 执行指令
                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 {
                // 关闭资源,防止内存泄漏
                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());
        }
        // 返回执行结果
        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服务就搭建好了,可以用来接收客户端传过来的指令并且执行然后返回结果

编写客户端

首先编写一个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);
                    // 发送指令
                    printWriter.println(cmd);
                    printWriter.flush();
                    // 读取服务端返回
                    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组件相关的事件,我们暂时只实现一个静默卸载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>

代码相关的工作基本完工,一个简单的,实现了静默卸载Demo就完成了

打包测试

  • ./gradlew assembleRelease 打出apk
  • 后缀改成.rar解压出classes.dex
  • 将classes.dex push至/data/local/tmp/
  • 执行服务
    • 前台执行:
    // 拔掉数据线会终止服务
    adb shell app_process -Djava.class.path=/data/local/tmp/classes.dex /system/bin shellService.Main
    
    • 后台执行:
    // 会一直运行除非手动kill pid或者重启设备
    adb shell nohup app_process -Djava.class.path=/data/local/tmp/classes.dex /system/bin --nice-name=${serviceName} shellService.Main
    
  • 安装apk,输入要卸载的包名,点击UNINSTALL进行静默卸载

完整项目

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

example

技术参考

感谢下列开源作者

android-common

Fairy

app_process-shell-use

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容