mirror of
https://github.com/topjohnwu/libsu.git
synced 2024-11-30 07:20:27 +00:00
Introduce RootService
Remote root services using Binder IPC
This commit is contained in:
parent
71e36cdc81
commit
6d9f30e81b
@ -112,16 +112,20 @@ subprojects {
|
||||
buildConfig = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (plugins.hasPlugin("com.github.dcendents.android-maven")) {
|
||||
val sources = android.sourceSets.getByName("main").java.sourceFiles
|
||||
|
||||
(rootProject.tasks["javadoc"] as Javadoc).apply {
|
||||
source = source.plus(android.sourceSets.getByName("main").java.sourceFiles)
|
||||
classpath = classpath.plus(project.files(android.bootClasspath))
|
||||
classpath = classpath.plus(configurations.getByName("javadocDeps"))
|
||||
source += sources
|
||||
classpath += project.files(android.bootClasspath)
|
||||
classpath += configurations.getByName("javadocDeps")
|
||||
}
|
||||
|
||||
val sourcesJar = tasks.register("sourcesJar", Jar::class) {
|
||||
archiveClassifier.set("sources")
|
||||
from(android.sourceSets.getByName("main").java.sourceFiles)
|
||||
from(sources)
|
||||
}
|
||||
|
||||
artifacts {
|
||||
|
@ -16,6 +16,8 @@
|
||||
|
||||
package com.topjohnwu.superuser.internal;
|
||||
|
||||
import androidx.annotation.RestrictTo;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@ -29,7 +31,8 @@ import java.util.concurrent.TimeoutException;
|
||||
|
||||
import static com.topjohnwu.superuser.Shell.EXECUTOR;
|
||||
|
||||
class SerialExecutorService extends AbstractExecutorService implements Callable<Void> {
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY)
|
||||
public class SerialExecutorService extends AbstractExecutorService implements Callable<Void> {
|
||||
|
||||
private boolean isShutdown = false;
|
||||
private ArrayDeque<Runnable> mTasks = new ArrayDeque<>();
|
||||
|
@ -5,11 +5,15 @@ plugins {
|
||||
android {
|
||||
defaultConfig {
|
||||
applicationId = "com.topjohnwu.libsuexample"
|
||||
minSdkVersion(16)
|
||||
minSdkVersion(18)
|
||||
versionCode = 1
|
||||
versionName ="1.0"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
getByName("release") {
|
||||
isMinifyEnabled = false
|
||||
@ -25,4 +29,5 @@ dependencies {
|
||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
|
||||
implementation(project(":core"))
|
||||
implementation(project(":busybox"))
|
||||
implementation(project(":service"))
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
// ITestService.aidl
|
||||
package com.topjohnwu.libsuexample;
|
||||
|
||||
// Declare any non-default types here with import statements
|
||||
|
||||
interface ITestService {
|
||||
int getPid();
|
||||
int getUid();
|
||||
}
|
@ -17,20 +17,25 @@
|
||||
package com.topjohnwu.libsuexample;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.Process;
|
||||
import android.os.RemoteException;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.topjohnwu.libsuexample.databinding.ActivityMainBinding;
|
||||
import com.topjohnwu.superuser.BusyBoxInstaller;
|
||||
import com.topjohnwu.superuser.CallbackList;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.ipc.RootService;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
@ -39,19 +44,20 @@ public class MainActivity extends Activity {
|
||||
|
||||
public static final String TAG = "EXAMPLE";
|
||||
|
||||
private TextView console;
|
||||
private EditText input;
|
||||
private ScrollView sv;
|
||||
private List<String> consoleList;
|
||||
|
||||
static {
|
||||
// Configuration
|
||||
Shell.Config.setFlags(Shell.FLAG_REDIRECT_STDERR);
|
||||
Shell.Config.verboseLogging(BuildConfig.DEBUG);
|
||||
// Use internal busybox
|
||||
Shell.Config.setInitializers(BusyBoxInstaller.class, ExampleInitializer.class);
|
||||
}
|
||||
|
||||
private List<String> consoleList;
|
||||
private ActivityMainBinding binding;
|
||||
|
||||
private ITestService testIPC;
|
||||
private RootConnection conn = new RootConnection();
|
||||
private boolean queuedTest = false;
|
||||
|
||||
// Demonstrate Shell.Initializer
|
||||
static class ExampleInitializer extends Shell.Initializer {
|
||||
|
||||
@ -62,38 +68,72 @@ public class MainActivity extends Activity {
|
||||
}
|
||||
}
|
||||
|
||||
// Demonstrate RootService
|
||||
static class ExampleService extends RootService {
|
||||
|
||||
@Override
|
||||
public IBinder onBind(@NonNull Intent intent) {
|
||||
return new ITestService.Stub() {
|
||||
@Override
|
||||
public int getPid() {
|
||||
return Process.myPid();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getUid() {
|
||||
return Process.myUid();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class RootConnection implements ServiceConnection {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
Log.d(TAG, "onServiceConnected");
|
||||
testIPC = ITestService.Stub.asInterface(service);
|
||||
if (queuedTest) {
|
||||
queuedTest = false;
|
||||
testService();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
Log.d(TAG, "onServiceDisconnected");
|
||||
testIPC = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void testService() {
|
||||
try {
|
||||
consoleList.add("Remote PID: " + testIPC.getPid());
|
||||
consoleList.add("Remote UID: " + testIPC.getUid());
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Remote error", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
console = findViewById(R.id.console);
|
||||
input = findViewById(R.id.cmd_input);
|
||||
sv = findViewById(R.id.sv);
|
||||
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
Button sync_cmd = findViewById(R.id.sync_cmd);
|
||||
Button async_cmd = findViewById(R.id.async_cmd);
|
||||
Button close_shell = findViewById(R.id.close_shell);
|
||||
Button sync_script = findViewById(R.id.sync_script);
|
||||
Button async_script = findViewById(R.id.async_script);
|
||||
Button clear = findViewById(R.id.clear);
|
||||
|
||||
// Run the shell command in the input box synchronously
|
||||
sync_cmd.setOnClickListener(v -> {
|
||||
Shell.sh(input.getText().toString()).to(consoleList).exec();
|
||||
input.setText("");
|
||||
binding.testSvc.setOnClickListener(v -> {
|
||||
if (testIPC == null) {
|
||||
queuedTest = true;
|
||||
Intent intent = new Intent(this, ExampleService.class);
|
||||
RootService.bind(intent, conn);
|
||||
return;
|
||||
}
|
||||
testService();
|
||||
});
|
||||
|
||||
// Run the shell command in the input box asynchronously.
|
||||
// Also demonstrates that Async.Callback works
|
||||
async_cmd.setOnClickListener(v -> {
|
||||
Shell.sh(input.getText().toString())
|
||||
.to(consoleList)
|
||||
.submit(out -> Log.d(TAG, "async_cmd_result: " + out.getCode()));
|
||||
input.setText("");
|
||||
});
|
||||
binding.closeSvc.setOnClickListener(v -> RootService.unbind(conn));
|
||||
|
||||
// Closing a shell is always synchronous
|
||||
close_shell.setOnClickListener(v -> {
|
||||
binding.closeShell.setOnClickListener(v -> {
|
||||
try {
|
||||
Shell shell = Shell.getCachedShell();
|
||||
if (shell != null)
|
||||
@ -104,14 +144,14 @@ public class MainActivity extends Activity {
|
||||
});
|
||||
|
||||
// Load a script from raw resources synchronously
|
||||
sync_script.setOnClickListener(v ->
|
||||
binding.syncScript.setOnClickListener(v ->
|
||||
Shell.sh(getResources().openRawResource(R.raw.info)).to(consoleList).exec());
|
||||
|
||||
// Load a script from raw resources asynchronously
|
||||
async_script.setOnClickListener(v ->
|
||||
binding.asyncScript.setOnClickListener(v ->
|
||||
Shell.sh(getResources().openRawResource(R.raw.count)).to(consoleList).submit());
|
||||
|
||||
clear.setOnClickListener(v -> consoleList.clear());
|
||||
binding.clear.setOnClickListener(v -> consoleList.clear());
|
||||
|
||||
/* Create a CallbackList to update the UI with Shell output
|
||||
* Here I demonstrate 2 ways to implement a CallbackList
|
||||
@ -131,14 +171,14 @@ public class MainActivity extends Activity {
|
||||
|
||||
@Override
|
||||
public void onAddElement(String s) {
|
||||
console.append(s);
|
||||
console.append("\n");
|
||||
sv.postDelayed(() -> sv.fullScroll(ScrollView.FOCUS_DOWN), 10);
|
||||
binding.console.append(s);
|
||||
binding.console.append("\n");
|
||||
binding.sv.postDelayed(() -> binding.sv.fullScroll(ScrollView.FOCUS_DOWN), 10);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void clear() {
|
||||
runOnUiThread(() -> console.setText(""));
|
||||
runOnUiThread(() -> binding.console.setText(""));
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,14 +201,14 @@ public class MainActivity extends Activity {
|
||||
|
||||
@Override
|
||||
public void onAddElement(String s) {
|
||||
console.setText(TextUtils.join("\n", this));
|
||||
sv.postDelayed(() -> sv.fullScroll(ScrollView.FOCUS_DOWN), 10);
|
||||
binding.console.setText(TextUtils.join("\n", this));
|
||||
binding.sv.postDelayed(() -> binding.sv.fullScroll(ScrollView.FOCUS_DOWN), 10);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void clear() {
|
||||
super.clear();
|
||||
runOnUiThread(() -> console.setText(""));
|
||||
runOnUiThread(() -> binding.console.setText(""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,14 +31,6 @@
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/cmd_input"
|
||||
android:inputType="textNoSuggestions|textMultiLine"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/background_light"
|
||||
android:padding="5dp" />
|
||||
|
||||
<LinearLayout
|
||||
style="?android:buttonStyle"
|
||||
android:layout_width="match_parent"
|
||||
@ -47,20 +39,20 @@
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/sync_cmd"
|
||||
android:id="@+id/test_svc"
|
||||
style="?android:borderlessButtonStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Sync CMD" />
|
||||
android:text="Test SVC" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/async_cmd"
|
||||
android:id="@+id/close_svc"
|
||||
style="?android:borderlessButtonStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Async CMD" />
|
||||
android:text="Close SVC" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/close_shell"
|
||||
@ -85,7 +77,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Sync Script" />
|
||||
android:text="Test sync" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/async_script"
|
||||
@ -93,7 +85,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Async Script" />
|
||||
android:text="Test Async" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/clear"
|
||||
|
@ -22,15 +22,7 @@ api_level_arch_detect() {
|
||||
if [ "$ABILONG" = "x86_64" ]; then ARCH=x64; IS64BIT=true; fi;
|
||||
}
|
||||
|
||||
TOOLPATH=`command -v toolbox`
|
||||
TOYPATH=`command -v toybox`
|
||||
BUSYPATH=`command -v busybox`
|
||||
|
||||
api_level_arch_detect
|
||||
|
||||
echo "Device API: $API"
|
||||
echo "Device ABI: $ARCH"
|
||||
[ ! -z $TOOLPATH ] && echo "toolbox at: $TOOLPATH"
|
||||
[ ! -z $TOYPATH ] && echo "toybox at: $TOYPATH"
|
||||
[ ! -z $BUSYPATH ] && echo "busybox at: $BUSYPATH"
|
||||
|
||||
|
1
service/.gitignore
vendored
Normal file
1
service/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
14
service/build.gradle.kts
Normal file
14
service/build.gradle.kts
Normal file
@ -0,0 +1,14 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
}
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
minSdkVersion(18)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
|
||||
api(project(":core"))
|
||||
}
|
21
service/proguard-rules.pro
vendored
Normal file
21
service/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
2
service/src/main/AndroidManifest.xml
Normal file
2
service/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,2 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.topjohnwu.superuser.ipc" />
|
@ -0,0 +1,9 @@
|
||||
// IRootIPC.aidl
|
||||
package com.topjohnwu.superuser.internal;
|
||||
|
||||
// Declare any non-default types here with import statements
|
||||
|
||||
interface IRootIPC {
|
||||
IBinder bind(in Intent intent, IBinder client);
|
||||
void unbind();
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2020 John "topjohnwu" Wu
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.topjohnwu.superuser.internal;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* Trampoline to start a root service.
|
||||
* <p>
|
||||
* This is the only class included in main.jar as raw resources.
|
||||
* The client code will execute this main method in a root shell.
|
||||
* <p>
|
||||
* This class will get the system context by calling into Android private APIs with reflection, and
|
||||
* uses that to create our client package context. The client context will have the full APK loaded,
|
||||
* just like it was launched in a non-root environment.
|
||||
* <p>
|
||||
* Expected command-line args:
|
||||
* args[0]: client package name
|
||||
* args[1]: class name of IPCServer (reason: name could be obfuscated in client APK)
|
||||
*/
|
||||
public class IPCMain {
|
||||
|
||||
/**
|
||||
* These private APIs are very unlikely to change, should be relatively
|
||||
* stable across different Android versions and OEMs.
|
||||
*/
|
||||
@SuppressLint("PrivateApi")
|
||||
public static Context getSystemContext() {
|
||||
try {
|
||||
synchronized (Looper.class) {
|
||||
if (Looper.getMainLooper() == null)
|
||||
Looper.prepareMainLooper();
|
||||
}
|
||||
|
||||
Class<?> atClazz = Class.forName("android.app.ActivityThread");
|
||||
Method systemMain = atClazz.getMethod("systemMain");
|
||||
Object activityThread = systemMain.invoke(null);
|
||||
Method getSystemContext = atClazz.getMethod("getSystemContext");
|
||||
return (Context) getSystemContext.invoke(activityThread);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
// Close STDOUT/STDERR since it belongs to the parent shell
|
||||
System.out.close();
|
||||
System.err.close();
|
||||
|
||||
try {
|
||||
String packageName = args[0];
|
||||
String ipcServerClassName = args[1];
|
||||
|
||||
Context systemContext = getSystemContext();
|
||||
Context context = systemContext.createPackageContext(packageName,
|
||||
Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
|
||||
|
||||
// Use classloader from the package context to run everything
|
||||
ClassLoader cl = context.getClassLoader();
|
||||
Class<?> clz = cl.loadClass(ipcServerClassName);
|
||||
Constructor<?> con = clz.getDeclaredConstructor(Context.class);
|
||||
con.setAccessible(true);
|
||||
con.newInstance(context);
|
||||
|
||||
// Shall never return
|
||||
System.exit(0);
|
||||
} catch (Exception e) {
|
||||
Log.e("IPC", "Error in IPCMain", e);
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
158
service/src/main/java/com/topjohnwu/superuser/ipc/IPCClient.java
Normal file
158
service/src/main/java/com/topjohnwu/superuser/ipc/IPCClient.java
Normal file
@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2020 John "topjohnwu" Wu
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.topjohnwu.superuser.ipc;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.internal.IRootIPC;
|
||||
import com.topjohnwu.superuser.internal.InternalUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import static com.topjohnwu.superuser.ipc.RootService.serialExecutor;
|
||||
|
||||
class IPCClient implements IBinder.DeathRecipient {
|
||||
private static final String BROADCAST_ACTION = "com.topjohnwu.superuser.BROADCAST_IPC";
|
||||
private static final String INTENT_EXTRA_KEY = "binder_bundle";
|
||||
private static final String BUNDLE_BINDER_KEY = "binder";
|
||||
|
||||
private ComponentName name;
|
||||
private IRootIPC server = null;
|
||||
private IBinder binder = null;
|
||||
private Map<ServiceConnection, Executor> connections = new HashMap<>();
|
||||
|
||||
IPCClient(Intent intent) throws InterruptedException, RemoteException, IOException {
|
||||
name = intent.getComponent();
|
||||
startRootServer(InternalUtils.getContext(), intent);
|
||||
}
|
||||
|
||||
private void startRootServer(Context context, Intent intent)
|
||||
throws IOException, InterruptedException, RemoteException {
|
||||
// Dump main.jar as trampoline
|
||||
Context de = Build.VERSION.SDK_INT >= 24 ? context.createDeviceProtectedStorageContext() : context;
|
||||
File mainJar = new File(de.getCacheDir(), "main.jar");
|
||||
|
||||
try (InputStream in = context.getResources().openRawResource(R.raw.main);
|
||||
OutputStream out = new FileOutputStream(mainJar)) {
|
||||
InternalUtils.pump(in, out);
|
||||
}
|
||||
|
||||
// Register BinderReceiver to receive binder from root process
|
||||
context.registerReceiver(new BinderReceiver(), new IntentFilter(BROADCAST_ACTION));
|
||||
|
||||
// Execute main.jar through root shell
|
||||
String app_process = new File("/proc/self/exe").getCanonicalPath();
|
||||
String cmd = String.format(
|
||||
"(CLASSPATH=%1$s %2$s /system/bin --nice-name=%4$s:root %3$s %4$s %5$s)&",
|
||||
mainJar, app_process, "com.topjohnwu.superuser.internal.IPCMain",
|
||||
context.getPackageName(), IPCServer.class.getName());
|
||||
synchronized (this) {
|
||||
Shell.su(cmd).exec();
|
||||
// Wait for broadcast receiver
|
||||
wait();
|
||||
}
|
||||
server.asBinder().linkToDeath(this, 0);
|
||||
binder = server.bind(intent, new Binder());
|
||||
}
|
||||
|
||||
boolean isSameService(Intent intent) {
|
||||
return name.equals(intent.getComponent());
|
||||
}
|
||||
|
||||
void newConnection(ServiceConnection conn, Executor executor) {
|
||||
connections.put(conn, executor);
|
||||
if (binder != null)
|
||||
executor.execute(() -> conn.onServiceConnected(name, binder));
|
||||
else if (Build.VERSION.SDK_INT >= 28)
|
||||
executor.execute(() -> conn.onNullBinding(name));
|
||||
}
|
||||
|
||||
boolean unbind(ServiceConnection conn) {
|
||||
Executor executor = connections.remove(conn);
|
||||
if (executor != null) {
|
||||
executor.execute(() -> conn.onServiceDisconnected(name));
|
||||
if (connections.isEmpty()) {
|
||||
server.asBinder().unlinkToDeath(this, 0);
|
||||
try {
|
||||
server.unbind();
|
||||
} catch (RemoteException ignored) {}
|
||||
server = null;
|
||||
binder = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void binderDied() {
|
||||
server.asBinder().unlinkToDeath(this, 0);
|
||||
server = null;
|
||||
binder = null;
|
||||
for (Map.Entry<ServiceConnection, Executor> entry : connections.entrySet()) {
|
||||
ServiceConnection conn = entry.getKey();
|
||||
entry.getValue().execute(() -> {
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
conn.onBindingDied(name);
|
||||
}
|
||||
conn.onServiceDisconnected(name);
|
||||
});
|
||||
}
|
||||
connections.clear();
|
||||
serialExecutor.execute(() -> RootService.active.remove(this));
|
||||
}
|
||||
|
||||
static Intent getBroadcastIntent(String packageName, IRootIPC.Stub ipc) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putBinder(BUNDLE_BINDER_KEY, ipc);
|
||||
return new Intent()
|
||||
.setPackage(packageName)
|
||||
.setAction(BROADCAST_ACTION)
|
||||
.putExtra(INTENT_EXTRA_KEY, bundle);
|
||||
}
|
||||
|
||||
class BinderReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
context.unregisterReceiver(this);
|
||||
Bundle bundle = intent.getBundleExtra(INTENT_EXTRA_KEY);
|
||||
IBinder binder = bundle.getBinder(BUNDLE_BINDER_KEY);
|
||||
synchronized (IPCClient.this) {
|
||||
server = IRootIPC.Stub.asInterface(binder);
|
||||
IPCClient.this.notifyAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2020 John "topjohnwu" Wu
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.topjohnwu.superuser.ipc;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.internal.IRootIPC;
|
||||
import com.topjohnwu.superuser.internal.InternalUtils;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
|
||||
import static com.topjohnwu.superuser.ipc.RootService.INTENT_VERBOSE_KEY;
|
||||
|
||||
class IPCServer extends IRootIPC.Stub implements IBinder.DeathRecipient {
|
||||
|
||||
private RootService service;
|
||||
private IBinder client;
|
||||
private Intent intent;
|
||||
private Context context;
|
||||
|
||||
IPCServer(Context context) {
|
||||
this.context = context;
|
||||
String packageName = context.getPackageName();
|
||||
Intent broadcast = IPCClient.getBroadcastIntent(packageName, this);
|
||||
context.sendBroadcast(broadcast);
|
||||
Looper.loop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized IBinder bind(Intent intent, IBinder client) {
|
||||
this.intent = intent.cloneFilter();
|
||||
Shell.Config.verboseLogging(intent.getBooleanExtra(INTENT_VERBOSE_KEY, false));
|
||||
try {
|
||||
if (service == null) {
|
||||
String name = intent.getComponent().getClassName();
|
||||
Class<? extends RootService> clz = (Class<? extends RootService>) Class.forName(name);
|
||||
Constructor<? extends RootService> constructor = clz.getDeclaredConstructor();
|
||||
constructor.setAccessible(true);
|
||||
service = constructor.newInstance();
|
||||
service.attach(context);
|
||||
service.onCreate();
|
||||
} else {
|
||||
service.onRebind(intent);
|
||||
}
|
||||
this.client = client;
|
||||
client.linkToDeath(this, 0);
|
||||
return service.onBind(intent);
|
||||
} catch (Exception e) {
|
||||
InternalUtils.stackTrace(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void unbind() {
|
||||
boolean rebind = service.onUnbind(intent);
|
||||
client.unlinkToDeath(this, 0);
|
||||
client = null;
|
||||
if (!rebind) {
|
||||
service.onDestroy();
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void binderDied() {
|
||||
unbind();
|
||||
}
|
||||
}
|
@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright 2020 John "topjohnwu" Wu
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.topjohnwu.superuser.ipc;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.ContextWrapper;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.IBinder;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.internal.InternalUtils;
|
||||
import com.topjohnwu.superuser.internal.SerialExecutorService;
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
import static com.topjohnwu.superuser.Shell.FLAG_VERBOSE_LOGGING;
|
||||
import static com.topjohnwu.superuser.internal.InternalUtils.hasFlag;
|
||||
|
||||
/**
|
||||
* A remote root service using native Android Binder IPC.
|
||||
* <p>
|
||||
* This class is almost a complete recreation of a bound service running in a root process.
|
||||
* Instead of using the original {@code Context.bindService(...)} methods to start and bind
|
||||
* to a service, use the provided static methods {@code RootService.bind(...)}.
|
||||
* Please note, unlike normal services, RootServices do not have an API similar to
|
||||
* {@link Context#startService(Intent)} because RootServices are strictly bound-only.
|
||||
* <p>
|
||||
* Because the service will not run in the same process as your application, you have to use AIDL
|
||||
* to define the IPC interface for communication. Please read the official documentations for more
|
||||
* details.
|
||||
* @see Service
|
||||
* @see <a href="Bound services">https://developer.android.com/guide/components/bound-services</a>
|
||||
* @see <a href="Android Interface Definition Language (AIDL)">https://developer.android.com/guide/components/aidl</a>
|
||||
*/
|
||||
public abstract class RootService extends ContextWrapper {
|
||||
static final String INTENT_VERBOSE_KEY = "verbose_logging";
|
||||
|
||||
static List<IPCClient> active = new ArrayList<>();
|
||||
static ExecutorService serialExecutor = new SerialExecutorService();
|
||||
|
||||
/**
|
||||
* Connect to a root service, creating if needed.
|
||||
* @param intent identifies the service to connect to.
|
||||
* @param executor callbacks on ServiceConnection will be called on this executor.
|
||||
* @param conn receives information as the service is started and stopped.
|
||||
* @see Context#bindService(Intent, int, Executor, ServiceConnection)
|
||||
*/
|
||||
public static void bind(
|
||||
@NonNull Intent intent,
|
||||
@NonNull Executor executor,
|
||||
@NonNull ServiceConnection conn) {
|
||||
serialExecutor.execute(() -> {
|
||||
// If no root access, don't even bother
|
||||
if (!Shell.rootAccess())
|
||||
return;
|
||||
|
||||
Intent intentCopy = new Intent(intent);
|
||||
intentCopy.putExtra(INTENT_VERBOSE_KEY, hasFlag(FLAG_VERBOSE_LOGGING));
|
||||
|
||||
for (IPCClient client : active) {
|
||||
if (client.isSameService(intentCopy)) {
|
||||
client.newConnection(conn, executor);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
IPCClient client = new IPCClient(intentCopy);
|
||||
client.newConnection(conn, executor);
|
||||
active.add(client);
|
||||
} catch (Exception e) {
|
||||
InternalUtils.stackTrace(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to a root service, creating if needed.
|
||||
* @param intent identifies the service to connect to.
|
||||
* @param conn receives information as the service is started and stopped.
|
||||
* @see Context#bindService(Intent, ServiceConnection, int)
|
||||
*/
|
||||
public static void bind(@NonNull Intent intent, @NonNull ServiceConnection conn) {
|
||||
bind(intent, UiThreadHandler.executor, conn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from a root service.
|
||||
* @param conn the connection interface previously supplied to {@link #bind(Intent, ServiceConnection)}
|
||||
* @see Context#unbindService(ServiceConnection)
|
||||
*/
|
||||
public static void unbind(@NonNull ServiceConnection conn) {
|
||||
serialExecutor.execute(() -> {
|
||||
Iterator<IPCClient> it = active.iterator();
|
||||
while (it.hasNext()) {
|
||||
IPCClient client = it.next();
|
||||
if (client.unbind(conn)) {
|
||||
it.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public RootService() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
void attach(Context base) {
|
||||
attachBaseContext(base);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Context getApplicationContext() {
|
||||
// Always return ourselves
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Service#onBind(Intent)
|
||||
*/
|
||||
abstract public IBinder onBind(@NonNull Intent intent);
|
||||
|
||||
/**
|
||||
* @see Service#onCreate()
|
||||
*/
|
||||
public void onCreate() {}
|
||||
|
||||
/**
|
||||
* @see Service#onUnbind(Intent)
|
||||
*/
|
||||
public boolean onUnbind(@NonNull Intent intent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Service#onRebind(Intent)
|
||||
*/
|
||||
public void onRebind(@NonNull Intent intent) {}
|
||||
|
||||
/**
|
||||
* @see Service#onDestroy()
|
||||
*/
|
||||
public void onDestroy() {}
|
||||
}
|
BIN
service/src/main/res/raw/main.jar
Normal file
BIN
service/src/main/res/raw/main.jar
Normal file
Binary file not shown.
@ -1 +1 @@
|
||||
include(":core", ":example", ":busybox", ":io")
|
||||
include(":core", ":example", ":busybox", ":io", ":service")
|
||||
|
Loading…
Reference in New Issue
Block a user