Compare commits

..

3 Commits

Author SHA1 Message Date
Administrator 54ec5023fc release签名 2025-07-18 18:51:31 +08:00
Administrator 42da6261fd Merge remote-tracking branch 'origin/master'
# Conflicts:
#	app/src/main/java/com/android/grape/sai/ShellSaiPackageInstaller.kt
#	app/src/main/java/com/android/grape/util/FileUtils.kt
#	app/src/main/java/com/android/grape/util/TaskUtils.kt
2025-07-18 18:34:28 +08:00
Administrator afd85cd10b 应用安装,脚本工具替换 2025-07-18 18:26:04 +08:00
15 changed files with 681 additions and 42 deletions

View File

@ -4,14 +4,155 @@
<mappings /> <mappings />
<preferences /> <preferences />
</component> </component>
<component name="DBNavigator.Project.DataEditorManager">
<record-view-column-sorting-type value="BY_INDEX" />
<value-preview-text-wrapping value="true" />
<value-preview-pinned value="false" />
</component>
<component name="DBNavigator.Project.DatabaseAssistantManager"> <component name="DBNavigator.Project.DatabaseAssistantManager">
<assistants /> <assistants>
<assistant-state connection-id="96c06034-88db-4cab-9c16-73c5d99a2c0a" default-profile-name="" selected-profile-name="" selected-model-name="" assistant-type="GENERIC" selected-action="SHOW_SQL" availability="UNAVAILABLE" acknowledgement="NONE">
<messages />
</assistant-state>
</assistants>
</component>
<component name="DBNavigator.Project.DatabaseBrowserManager">
<autoscroll-to-editor value="false" />
<autoscroll-from-editor value="true" />
<show-object-properties value="true" />
<loaded-nodes />
</component>
<component name="DBNavigator.Project.DatabaseConsoleManager">
<connection id="96c06034-88db-4cab-9c16-73c5d99a2c0a">
<console name="Connection" type="STANDARD" schema="device_info" session="Main" />
</connection>
</component>
<component name="DBNavigator.Project.DatabaseEditorStateManager">
<last-used-providers />
</component> </component>
<component name="DBNavigator.Project.DatabaseFileManager"> <component name="DBNavigator.Project.DatabaseFileManager">
<open-files /> <open-files />
</component> </component>
<component name="DBNavigator.Project.DatabaseSessionManager">
<connection id="96c06034-88db-4cab-9c16-73c5d99a2c0a" />
</component>
<component name="DBNavigator.Project.DatasetFilterManager">
<filter-actions connection-id="96c06034-88db-4cab-9c16-73c5d99a2c0a" dataset="device_info.device_info" active-filter-id="EMPTY_FILTER" />
</component>
<component name="DBNavigator.Project.ExecutionManager">
<retain-sticky-names value="false" />
</component>
<component name="DBNavigator.Project.ObjectQuickFilterManager">
<last-used-operator value="EQUAL" />
<filters />
</component>
<component name="DBNavigator.Project.Settings"> <component name="DBNavigator.Project.Settings">
<connections /> <connections>
<connection id="96c06034-88db-4cab-9c16-73c5d99a2c0a" active="true" signed="true">
<database>
<name value="Connection" />
<description value="" />
<database-type value="SQLITE" />
<config-type value="BASIC" />
<database-version value="3.49" />
<driver-source value="BUNDLED" />
<driver-library value="" />
<driver value="" />
<url-type value="FILE" />
<host value="" />
<port value="" />
<database value="" />
<tns-folder value="" />
<tns-profile value="" />
<files>
<file path="D:\Desktop\device_info.db" schema="device_info" />
</files>
<type value="NONE" />
<user value="" />
<token-type value="" />
<token-config-file value="" />
<token-profile value="" />
<session-user value="" />
</database>
<properties>
<auto-commit value="false" />
</properties>
<ssh-settings>
<active value="false" />
<proxy-host value="" />
<proxy-port value="22" />
<proxy-user value="" />
<auth-type value="PASSWORD" />
<key-file value="" />
</ssh-settings>
<ssl-settings>
<active value="false" />
<certificate-authority-file value="" />
<client-certificate-file value="" />
<client-key-file value="" />
</ssl-settings>
<details>
<charset value="UTF-8" />
<session-management value="true" />
<ddl-file-binding value="true" />
<database-logging value="true" />
<connect-automatically value="true" />
<restore-workspace value="true" />
<restore-workspace-deep value="false" />
<environment-type value="default" />
<connectivity-timeout value="30" />
<idle-time-to-disconnect value="30" />
<idle-time-to-disconnect-pool value="5" />
<credential-expiry-time value="10" />
<max-connection-pool-size value="7" />
<alternative-statement-delimiter value="" />
</details>
<debugger>
<compile-dependencies value="true" />
<tcp-driver-tunneling value="false" />
<tcp-host-address value="" />
<tcp-port-from value="4000" />
<tcp-port-to value="4999" />
<debugger-type value="ASK" />
</debugger>
<object-filters hide-empty-schemas="false" hide-pseudo-columns="false" hide-audit-columns="false">
<object-filters />
<object-type-filter use-master-settings="true">
<object-type name="SCHEMA" enabled="true" />
<object-type name="USER" enabled="true" />
<object-type name="ROLE" enabled="true" />
<object-type name="PRIVILEGE" enabled="true" />
<object-type name="CHARSET" enabled="true" />
<object-type name="TABLE" enabled="true" />
<object-type name="VIEW" enabled="true" />
<object-type name="MATERIALIZED_VIEW" enabled="true" />
<object-type name="NESTED_TABLE" enabled="true" />
<object-type name="COLUMN" enabled="true" />
<object-type name="INDEX" enabled="true" />
<object-type name="CONSTRAINT" enabled="true" />
<object-type name="DATASET_TRIGGER" enabled="true" />
<object-type name="DATABASE_TRIGGER" enabled="true" />
<object-type name="SYNONYM" enabled="true" />
<object-type name="SEQUENCE" enabled="true" />
<object-type name="PROCEDURE" enabled="true" />
<object-type name="FUNCTION" enabled="true" />
<object-type name="PACKAGE" enabled="true" />
<object-type name="TYPE" enabled="true" />
<object-type name="TYPE_ATTRIBUTE" enabled="true" />
<object-type name="ARGUMENT" enabled="true" />
<object-type name="JAVA_CLASS" enabled="true" />
<object-type name="JAVA_INNER_CLASS" enabled="true" />
<object-type name="JAVA_FIELD" enabled="true" />
<object-type name="JAVA_METHOD" enabled="true" />
<object-type name="DIMENSION" enabled="true" />
<object-type name="CLUSTER" enabled="true" />
<object-type name="DBLINK" enabled="true" />
<object-type name="CREDENTIAL" enabled="true" />
<object-type name="AI_PROFILE" enabled="true" />
</object-type-filter>
</object-filters>
</connection>
</connections>
<browser-settings> <browser-settings>
<general> <general>
<display-mode value="TABBED" /> <display-mode value="TABBED" />
@ -426,4 +567,8 @@
</environment> </environment>
</general-settings> </general-settings>
</component> </component>
<component name="DBNavigator.Project.StatementExecutionManager">
<execution-variables />
<execution-variable-types />
</component>
</project> </project>

View File

@ -1,3 +1,4 @@
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
@ -7,6 +8,15 @@ android {
namespace = "com.android.grape" namespace = "com.android.grape"
compileSdk = 35 compileSdk = 35
signingConfigs {
getByName("release") {
storeFile = file("key.jks")
storePassword = "androidgrape"
keyAlias = "key0"
keyPassword = "androidgrape"
}
}
defaultConfig { defaultConfig {
applicationId = "com.android.grape" applicationId = "com.android.grape"
minSdk = 23 minSdk = 23

View File

@ -18,6 +18,7 @@ import com.android.grape.util.MockTools
import com.android.grape.util.NotificationPermissionHandler import com.android.grape.util.NotificationPermissionHandler
import com.android.grape.util.ScriptUtils.registerScriptResultReceiver import com.android.grape.util.ScriptUtils.registerScriptResultReceiver
import com.android.grape.util.ScriptUtils.unregisterScriptResultReceiver import com.android.grape.util.ScriptUtils.unregisterScriptResultReceiver
import com.android.grape.util.ShellUtil
import com.android.grape.util.StoragePermissionHelper import com.android.grape.util.StoragePermissionHelper
/** /**

View File

@ -75,8 +75,8 @@ data class Device(
var nativeDir: Boolean = false, var nativeDir: Boolean = false,
var network: String = "", var network: String = "",
var noRcLatency: Boolean = false, var noRcLatency: Boolean = false,
@SerializedName("open_referrer") // @SerializedName("open_referrer")//todo string or object
var openReferrer: String = "", // var openReferrer: String = "",
@SerializedName("open_referrerReset") @SerializedName("open_referrerReset")
var openReferrerReset: Int = 0, var openReferrerReset: Int = 0,
var opener: String = "", var opener: String = "",

View File

@ -23,8 +23,9 @@ import com.android.grape.sai.prefers.PreferencesHelper
import com.android.grape.sai.rootless.AndroidPackageInstallerError import com.android.grape.sai.rootless.AndroidPackageInstallerError
import com.android.grape.sai.shell.MiuiUtils import com.android.grape.sai.shell.MiuiUtils
import com.android.grape.sai.shell.Shell import com.android.grape.sai.shell.Shell
import com.android.grape.util.InstallUtils.setInstallRet import com.android.grape.util.FileUtils
import com.android.grape.util.TaskUtils import com.android.grape.util.TaskUtils
import com.android.grape.util.TaskUtils.setInstallRet
import com.blankj.utilcode.util.LogUtils import com.blankj.utilcode.util.LogUtils
import java.io.File import java.io.File
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
@ -137,7 +138,7 @@ abstract class ShellSaiPackageInstaller protected constructor(c: Context?) :
} }
androidSessionId = createSession() androidSessionId = createSession()
//todo params.apkSource().apkLocalPath? //todo params.apkSource().apkLocalPath?
val path = "/sdcard/apks/${recordPackageName}" val path = "${FileUtils.CACHE_PATH}${recordPackageName}"
val file = File(path) val file = File(path)
val files = file.listFiles() val files = file.listFiles()

View File

@ -111,8 +111,8 @@ class RootlessSaiPackageInstaller private constructor(c: Context) :
val callbackIntent: Intent = val callbackIntent: Intent =
Intent(RootlessSaiPiBroadcastReceiver.ACTION_DELIVER_PI_EVENT) Intent(RootlessSaiPiBroadcastReceiver.ACTION_DELIVER_PI_EVENT)
val pendingIntent = PendingIntent.getBroadcast(context, 0, callbackIntent, val pendingIntent = PendingIntent.getBroadcast(context, 0, callbackIntent,
0) PendingIntent.FLAG_IMMUTABLE)
session!!.commit(pendingIntent.intentSender) session.commit(pendingIntent.intentSender)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, e) Log.w(TAG, e)

View File

@ -609,7 +609,7 @@ object AppUtils {
) { ) {
true true
} else { } else {
downloadFile(url, "/sdcard/apks/$recordFileName") downloadFile(url, "${FileUtils.CACHE_PATH}$recordFileName")
} }
if (ret) { if (ret) {

View File

@ -128,11 +128,11 @@ object BackupUtils {
*/ */
fun killRecordProcess(context: Context?, packageName: String?) { fun killRecordProcess(context: Context?, packageName: String?) {
Log.i("BackupUtils", "start killRecordProcess :$packageName") Log.i("BackupUtils", "start killRecordProcess :$packageName")
try { try {
val cmd = "am force-stop $packageName" val cmd = "am force-stop $packageName"
Log.i("BackupUtils", "killRecordProcess-> cmd:$cmd") Log.i("BackupUtils", "killRecordProcess-> cmd:$cmd")
MockTools.exec(cmd) ShellUtil.execRootCmdAndGetResult(cmd)
// MockTools.exec(cmd)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }

View File

@ -71,11 +71,56 @@ object DeviceUtils {
* @param context * @param context
* @return * @return
*/ */
fun getUserAndGroupSh(context: Context, packageName: String?, string: String): String { fun getUserAndGroupSh(context: Context, packageName: String?, txtFileName: String): String {
return getUserAndGroupSh( try {
context, // String txtFileName = getRecordTxtFileName(context);
recordPackageName, getRecordTxtFileName(context)
val cmd = "ls /data/data -l | grep $packageName"
val result = MockTools.execRead(cmd)
val txtFile = File(txtFileName)
if (result.length > 20) {
val contents = result
Log.i(
TAG,
"getUserAndGroupSh file->$txtFileName; contents:$contents"
) )
if (contents.length > 0) {
val arr =
contents.split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
var userAndGroup = ""
for (i in arr.indices) {
val line = arr[i]
Log.i(
TAG,
"getUserAndGroup: line=$line"
)
if (line.endsWith(" $packageName")) {
val arr1 = line.split(" ".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
for (s1 in arr1) {
if (s1.startsWith("u0_")) {
val user = s1
userAndGroup = "$user:$user"
Log.i(
TAG,
"getUserAndGroupSh userAndGroup:$userAndGroup"
)
break
}
}
}
}
return userAndGroup
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return ""
} }
/** /**

View File

@ -4,6 +4,7 @@ import android.content.Context
import android.os.Environment import android.os.Environment
import android.util.Base64 import android.util.Base64
import android.util.Log import android.util.Log
import com.android.grape.MainApplication
import com.android.grape.data.AppState.apkDir import com.android.grape.data.AppState.apkDir
import com.android.grape.data.AppState.monitorDir import com.android.grape.data.AppState.monitorDir
import com.android.grape.data.AppState.recordFileName import com.android.grape.data.AppState.recordFileName
@ -43,6 +44,7 @@ object FileUtils {
public const val BUFFER_SIZE = 1024 * 1024 //1M Byte public const val BUFFER_SIZE = 1024 * 1024 //1M Byte
public var name: String? = null public var name: String? = null
val CACHE_PATH = "${MainApplication.instance.cacheDir.absolutePath}/"
fun delFiles(context: Context?) { fun delFiles(context: Context?) {
Log.i("TaskUtils", "start to delFiles : " + startInstallTime) Log.i("TaskUtils", "start to delFiles : " + startInstallTime)
@ -63,6 +65,14 @@ object FileUtils {
} }
} }
public fun getRecordDataFileName(context: Context): String {
return getBaseFilesDir(context) + "/" + monitorDir + "/" + apkDir + "/" + recordPackageName + ".zip"
}
public fun getRecordSdcardApkVerFileName(context: Context): String {
return "${FileUtils.CACHE_PATH}" + recordFileName
}
public fun forceCreteDir(file: File) { public fun forceCreteDir(file: File) {
if (!file.exists()) { if (!file.exists()) {
val parent = file.parentFile val parent = file.parentFile
@ -78,6 +88,51 @@ object FileUtils {
} }
} }
public fun getSessionTxtFileName(context: Context): String {
return getBaseFilesDir(context) + "/" + monitorDir + "/" + apkDir + "/sessionTxt.txt"
}
public fun getRecordTxtFileName(context: Context): String {
return getBaseFilesDir(context) + "/" + monitorDir + "/" + apkDir + "/" + recordPackageName + ".txt"
}
public fun getSelfRecordTxtFileName(context: Context): String {
return getBaseFilesDir(context) + "/" + monitorDir + "/" + apkDir + "/" + context.packageName + ".txt"
}
public fun getRecordApkVerFileName(context: Context): String {
return getBaseFilesDir(context) + "/" + monitorDir + "/" + apkDir + "/" + recordFileName
}
public fun getRecordApkFileName(context: Context): String {
return getBaseFilesDir(context) + "/" + monitorDir + "/" + apkDir + "/" + recordPackageName + ".apk"
}
public fun makeFile(fileName: String) {
var PrintWriter: PrintWriter? = null
var process: Process? = null
try {
process = Runtime.getRuntime().exec("su")
PrintWriter = PrintWriter(process.outputStream)
//String cmd = "cd " + path+" \n";
val cmd = "touch $fileName"
Log.i("TaskUtils", "makefile-> cmd:$cmd")
PrintWriter.println(cmd)
PrintWriter.flush()
PrintWriter.close()
val value = process.waitFor()
} catch (e: Exception) {
e.printStackTrace()
} finally {
process?.destroy()
}
}
public fun getRecordListTxtFileName(context: Context): String {
return getBaseFilesDir(context) + "/" + monitorDir + "/" + apkDir + "/" + recordPackageName + ".list.txt"
}
public fun forceMakeDir(file: File) { public fun forceMakeDir(file: File) {
if (!file.exists()) { if (!file.exists()) {
val parent = file.parentFile val parent = file.parentFile

View File

@ -12,7 +12,7 @@ object MockTools {
fun exec(cmd: String): String { fun exec(cmd: String): String {
var retString = "" var retString = ""
try { try {
retString = ShellUtils.execRootCmdAndGetResult(cmd) retString = ShellUtil.execRootCmdAndGetResult(cmd)
// //创建socket // //创建socket
// val myCmd = "SU|$cmd" // val myCmd = "SU|$cmd"
// //
@ -41,7 +41,7 @@ object MockTools {
fun execRead(cmd: String): String { fun execRead(cmd: String): String {
var retString = "" var retString = ""
try { try {
retString = ShellUtils.execRootCmdAndGetResult(cmd) retString = ShellUtil.execRootCmdAndGetResult(cmd)
// //创建socket // //创建socket
// val myCmd = "SU_1|$cmd" // val myCmd = "SU_1|$cmd"
// //

View File

@ -18,6 +18,7 @@ import com.android.grape.net.AfClient.downloadFile
import com.android.grape.receiver.ScriptReceiver import com.android.grape.receiver.ScriptReceiver
import com.android.grape.util.ShellUtils.delFileSh import com.android.grape.util.ShellUtils.delFileSh
import com.android.grape.util.ShellUtils.unzipAPkSh import com.android.grape.util.ShellUtils.unzipAPkSh
import com.android.grape.util.ShellUtils.unzipScriptSh
import com.blankj.utilcode.util.LogUtils import com.blankj.utilcode.util.LogUtils
/** /**
@ -52,7 +53,7 @@ object ScriptUtils {
Log.i("TaskUtils", "execDownScript isDownload : $isDownload") Log.i("TaskUtils", "execDownScript isDownload : $isDownload")
return false return false
} }
unzipAPkSh(script_path, "/sdcard/script/") unzipScriptSh(script_path, "/sdcard/script/")
delFileSh(script_path) delFileSh(script_path)
} }
return isDownload return isDownload

View File

@ -0,0 +1,348 @@
package com.android.grape.util;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Log;
import com.blankj.utilcode.util.LogUtils;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ShellUtil {
public static String getPackagePath(Context context, String packageName) {
try {
PackageManager pm = context.getPackageManager();
ApplicationInfo appInfo = pm.getApplicationInfo(packageName, 0);
return appInfo.sourceDir; // 返回 APK 的完整路径
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return "";
}
}
public static void exec(String cmd) {
try {
LogUtils.d(Log.INFO, "ShellUtils", "Executing command: " + cmd, null);
Process process = Runtime.getRuntime().exec(cmd);
process.waitFor();
} catch (Exception e) {
LogUtils.d(Log.ERROR, "ShellUtils", "Error executing command: " + e.getMessage(), e);
}
}
public static int getPid(Process p) {
int pid = -1;
try {
Field f = p.getClass().getDeclaredField("pid");
f.setAccessible(true);
pid = f.getInt(p);
f.setAccessible(false);
} catch (Throwable e) {
pid = -1;
}
return pid;
}
public static boolean hasBin(String binName) {
// 验证 binName 是否符合规则
if (binName == null || binName.isEmpty()) {
LogUtils.d(Log.ERROR, "ShellUtils", "Invalid bin name",null);
throw new IllegalArgumentException("Bin name cannot be null or empty");
}
for (char c : binName.toCharArray()) {
if (!Character.isLetterOrDigit(c) && c != '.' && c != '_' && c != '-') {
throw new IllegalArgumentException("Invalid bin name");
}
}
// 获取 PATH 环境变量
String pathEnv = System.getenv("PATH");
if (pathEnv == null) {
LogUtils.d(Log.ERROR, "ShellUtils", "PATH environment variable is not available", null);
return false;
}
// 使用适合当前系统的路径分隔符分割路径
String[] paths = pathEnv.split(File.pathSeparator);
for (String path : paths) {
File file = new File(path, binName); // 使用 File 构造完整路径
try {
// 检查文件是否可执行
if (file.canExecute()) {
return true;
}
} catch (SecurityException e) {
LogUtils.d(Log.ERROR, "ShellUtils", "Security exception occurred while checking: " + file.getAbsolutePath(), e);
}
}
// 如果未找到可执行文件返回 false
return false;
}
public static String execRootCmdAndGetResult(String cmd) {
Log.d("ShellUtils", "execRootCmdAndGetResult - Started execution for command: " + cmd);
Process process = null;
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
process = Runtime.getRuntime().exec("vu");
try (OutputStream os = new BufferedOutputStream(process.getOutputStream());
InputStream is = process.getInputStream();
InputStream errorStream = process.getErrorStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
BufferedReader errorReader = new BufferedReader(new InputStreamReader(errorStream, StandardCharsets.UTF_8))) {
executor.submit(() -> {
String line;
try {
while ((line = errorReader.readLine()) != null) {
LogUtils.d(Log.ERROR, "ShellUtils", "Shell Error: " + line, null);
}
} catch (IOException e) {
LogUtils.d(Log.ERROR, "ShellUtils", "Error while reading process error stream: " + e.getMessage(), e);
}
});
os.write((cmd + "\n").getBytes());
os.write("exit\n".getBytes());
os.flush();
StringBuilder output = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
Log.d("ShellUtils", "Shell Output: " + line);
output.append(line).append("\n");
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (!process.waitFor(10, TimeUnit.SECONDS)) {
LogUtils.d(Log.ERROR, "ShellUtils", "Process execution timed out. Destroying process.", null);
process.destroyForcibly();
throw new RuntimeException("Shell command execution timeout.");
}
} else {
long startTime = System.currentTimeMillis();
while (true) {
try {
process.exitValue();
break;
} catch (IllegalThreadStateException e) {
if (System.currentTimeMillis() - startTime > 10000) { // 10 seconds
LogUtils.d(Log.ERROR, "ShellUtils", "Process execution timed out (manual tracking). Destroying process.", null);
process.destroy();
throw new RuntimeException("Shell command execution timeout.");
}
Thread.sleep(100); // Sleep briefly before re-checking
}
}
}
return output.toString().trim();
}
} catch (IOException | InterruptedException e) {
LogUtils.d(Log.ERROR, "ShellUtils", "Command execution failed: " + e.getMessage(), e);
Thread.currentThread().interrupt();
return "Error: " + e.getMessage();
} finally {
if (process != null) {
Log.d("ShellUtils", "Finalizing process. Attempting to destroy it.");
process.destroy();
}
executor.shutdown();
Log.d("ShellUtils", "Executor service shut down.");
}
}
public static void execRootCmd(String cmd) {
// 校验命令是否安全
if (!isCommandSafe(cmd)) {
LogUtils.d(Log.ERROR, "ShellUtils", "Unsafe command, aborting.", null);
return;
}
List<String> cmds = new ArrayList<>();
cmds.add(cmd);
// 使用同步锁保护线程安全
synchronized (ShellUtils.class) {
try {
List<String> results = execRootCmds(cmds);
// 判断是否需要打印输出仅用于开发调试阶段
for (String result : results) {
Log.d("ShellUtils", "Command Result: " + result);
}
} catch (Exception e) {
LogUtils.d(Log.ERROR, "ShellUtils", "Unexpected error: " + e.getMessage(), e);
}
}
}
private static boolean isCommandSafe(String cmd) {
// 检查空值和空字符串
if (cmd == null || cmd.trim().isEmpty()) {
LogUtils.d(Log.ERROR, "ShellUtils", "Rejected command: empty or null value.", null);
return false;
}
// 检查非法字符
if (!cmd.matches("^[a-zA-Z0-9._/:\\-~`'\" *|]+$")) {
LogUtils.d(Log.ERROR, "ShellUtils", "Rejected command due to illegal characters: " + cmd, null);
return false;
}
// 检查多命令逻辑运算符限制
if (cmd.contains("&&") || cmd.contains("||")) {
Log.d("ShellUtils", "Command contains logical operators.");
if (!isExpectedMultiCommand(cmd)) {
LogUtils.d(Log.ERROR, "ShellUtils", "Rejected command due to prohibited structure: " + cmd, null);
return false;
}
}
// 路径遍历保护
if (cmd.contains("../") || cmd.contains("..\\")) {
LogUtils.d(Log.ERROR, "ShellUtils", "Rejected command due to path traversal attempt: " + cmd, null);
return false;
}
// 命令长度限制
if (cmd.startsWith("tar") && cmd.length() > 800) { // 特定命令支持更长长度
LogUtils.d(Log.ERROR, "ShellUtils", "Command rejected due to excessive length.", null);
return false;
} else if (cmd.length() > 500) {
LogUtils.d(Log.ERROR, "ShellUtils", "Command rejected due to excessive length.", null);
return false;
}
Log.d("ShellUtils", "Command passed safety checks: " + cmd);
return true;
}
// 附加方法检查多命令是否符合预期
private static boolean isExpectedMultiCommand(String cmd) {
// 判断是否为允许的命令组合比如 `cd` `tar` 组合命令
return cmd.matches("^cd .+ && (tar|zip|cp).+");
}
public static List<String> execRootCmds(List<String> cmds) {
List<String> results = new ArrayList<>();
Process process = null;
try {
// 初始化 Shell 环境
process = hasBin("su") ? Runtime.getRuntime().exec("su") : Runtime.getRuntime().exec("sh");
// 启动读取线程
Process stdProcess = process;
Thread stdThread = new Thread(() -> {
try (BufferedReader stdReader = new BufferedReader(new InputStreamReader(stdProcess.getInputStream()))) {
List<String> localResults = new ArrayList<>();
String line;
while ((line = stdReader.readLine()) != null) {
localResults.add(line);
Log.d("ShellUtils", "Stdout: " + line);
}
synchronized (results) {
results.addAll(localResults);
}
} catch (IOException ioException) {
LogUtils.d(Log.ERROR, "ShellUtils", "Error reading stdout", ioException);
}
});
Process finalProcess = process;
Thread errThread = new Thread(() -> {
try (BufferedReader errReader = new BufferedReader(new InputStreamReader(finalProcess.getErrorStream()))) {
String line;
while ((line = errReader.readLine()) != null) {
LogUtils.d(Log.ERROR, "ShellUtils", "Stderr: " + line, null);
}
} catch (IOException ioException) {
LogUtils.d(Log.ERROR, "ShellUtils", "Error reading stderr", ioException);
}
});
// 启动子线程
stdThread.start();
errThread.start();
try (OutputStream os = process.getOutputStream()) {
for (String cmd : cmds) {
// if (!isCommandSafe(cmd)) {
// Log.w("ShellUtils", "Skipping unsafe command: " + cmd);
// continue;
// }
os.write((cmd + "\n").getBytes());
Log.d("ShellUtils", "Executing command: " + cmd);
}
os.write("exit\n".getBytes());
os.flush();
}
try {
// 执行命令等待解决
process.waitFor();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复中断
LogUtils.d(Log.ERROR, "ShellUtils", "Error executing commands", e);
}
// 等待子线程完成
stdThread.join();
errThread.join();
} catch (InterruptedIOException e) {
LogUtils.d(Log.ERROR, "ShellUtils", "Error reading stdout: Interrupted", e);
Thread.currentThread().interrupt(); // 恢复线程的中断状态
} catch (Exception e) {
LogUtils.d(Log.ERROR, "ShellUtils", "Error executing commands", e);
} finally {
if (process != null) {
process.destroy();
}
}
return results;
}
public static boolean hasRootAccess() {
// 记录是否出现安全异常
boolean hasSecurityError = false;
// 检查二进制文件
String[] binariesToCheck = {"su", "xu", "vu"};
for (String bin : binariesToCheck) {
try {
if (hasBin(bin)) {
return true;
}
} catch (SecurityException e) {
hasSecurityError = true;
LogUtils.d(Log.ERROR, "ShellUtils", "Security exception while checking: " + bin, e);
}
}
// 判断如果发生安全异常则反馈问题
if (hasSecurityError) {
Log.w("ShellUtils", "Potential security error detected while checking root access.");
}
// 没有找到合法的二进制文件则认为无root权限
return false;
}
}

View File

@ -558,7 +558,7 @@ object ShellUtils {
return result return result
} }
fun unzipAPkSh(zipFileName: String, dataDir: String) { fun unzipScriptSh(zipFileName: String, dataDir: String) {
try { try {
val file = File(dataDir) val file = File(dataDir)
if (!file.exists()) { if (!file.exists()) {
@ -571,6 +571,19 @@ object ShellUtils {
e.printStackTrace() e.printStackTrace()
} }
} }
fun unzipAPkSh(zipFileName: String, dataDir: String) {
try {
val file = File(dataDir)
if (!file.exists()) {
FileUtils.forceCreteDir(file)
}
val cmd = "unzip -o " + FileUtils.CACHE_PATH + File(zipFileName).name + " -d " + dataDir
val unzipResult = MockTools.execRead(cmd)
Log.i("ShellUtils", "unZipFileSh-> cmd:$unzipResult")
} catch (e: Exception) {
e.printStackTrace()
}
}
/** /**
* 执行shell命令以检索指定文件的内容 * 执行shell命令以检索指定文件的内容

View File

@ -48,16 +48,31 @@ import com.android.grape.manager.ConfigManager.initDefaultProxyJo
import com.android.grape.util.AppUtils.execTargetApp import com.android.grape.util.AppUtils.execTargetApp
import com.android.grape.util.AppUtils.getApkPackageName import com.android.grape.util.AppUtils.getApkPackageName
import com.android.grape.util.AppUtils.setTopApp import com.android.grape.util.AppUtils.setTopApp
import com.android.grape.util.ContextUtils.getRecordDataFileName import com.android.grape.util.ContextUtils.getBaseFilesDir
import com.android.grape.util.FileUtils.forceMakeDir import com.android.grape.util.FileUtils.forceMakeDir
import com.android.grape.util.FileUtils.getRecordDataFileName
import com.android.grape.util.JsonUtils.execSetJson import com.android.grape.util.JsonUtils.execSetJson
import com.blankj.utilcode.util.LogUtils import com.blankj.utilcode.util.LogUtils
import org.json.JSONObject import org.json.JSONObject
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.io.PrintWriter
import java.util.Locale
import kotlin.math.min import kotlin.math.min
/**
* `util`类用作封装多种方法和属性的实用程序类
* 用于管理与应用程序相关的数据并执行各种操作此课程提供
* 诸如检索和更新应用程序元数据管理日志处理等功能
* 备份要求跟踪用户和组信息以及与shell命令进行交互
*
* 它还揭示了使用JSON数据时间戳代理配置软件包的方法
* 所有权和其他特定于应用程序的属性该课程中的许多方法都相关
* 将应用程序状态管理和设备上的特定命令执行
*
* 注意此类包括用于初始化和内部配置的方法
* 不直接暴露于外部使用
*/
object TaskUtils { object TaskUtils {
init { init {
@ -113,6 +128,16 @@ object TaskUtils {
return "normal" return "normal"
} }
fun setInstallRet(installRetV: Boolean) {
Log.i("TaskUtils", "setInstallRet: $installRetV")
installRet = installRetV
}
fun isInstallRet(): Boolean {
return installRet == true
}
fun setAfLog(afLogV: String) { fun setAfLog(afLogV: String) {
afLog = afLogV afLog = afLogV
} }
@ -144,14 +169,13 @@ object TaskUtils {
return "v1060" return "v1060"
} }
/** /**
* 该函数execReloginTask的功能是执行重新登录任务主要逻辑如下 构建请求参数获取设备唯一 ID拼接请求 URL
* 初始化配置并构造请求URL及参数 发送 POST 请求调用 MyPost.postData 向服务器提交请求
* 发送POST网络请求到指定地址 处理响应结果
* 解析返回结果 成功时设置权限解析 JSON 数据并设置 clickTime
* 若成功code == 1执行文件权限修改并设置相关数据 失败或异常时调用 setFinish 结束任务
* 若失败记录错误日志并调用setFinish
* 捕获异常并处理错误
*/ */
fun execReloginTask(context: Context) { fun execReloginTask(context: Context) {
ConfigManager.init() ConfigManager.init()
@ -196,13 +220,11 @@ object TaskUtils {
} }
/** /**
* 该函数execInstallTask的功能是执行安装任务主要逻辑如下 * 初始化并构建请求参数获取设备唯一 IDANDROID_ID拼接请求 URL
* 初始化配置并构建请求地址与参数 * 发送 POST 请求通过 MyPost.postData 向服务器提交安装请求
* 发送POST网络请求到指定URL * 处理响应结果
* 解析返回结果 * 成功时解析 JSON 数据设置权限并触发 clickTime 1 表示需执行点击操作
* 若成功code == 1修改文件权限并执行设置JSON数据操作 * 失败或异常时调用 setFinish 结束任务
* 若失败记录错误日志并调用setFinish
* 捕获异常并记录错误信息
*/ */
fun execInstallTask(context: Context) { fun execInstallTask(context: Context) {
ConfigManager.init() ConfigManager.init()
@ -244,12 +266,10 @@ object TaskUtils {
} }
/** /**
* 该函数execTask的功能是随机执行重新登录或安装任务具体逻辑如下 * 根据随机条件执行任务
* 增加随机数计数器 * 在运行Relogin任务和安装任务之间交替
* 删除插件文件 *
* 根据计数器决定执行 * @param上下文执行任务执行的上下文
* 每3次调用执行一次execReloginTask
* 其余情况执行execInstallTask
*/ */
fun execTask(context: Context) { fun execTask(context: Context) {
nRandom++ nRandom++