方案分析
市面上實現(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
技術(shù)參考
感謝下列開源作者