From 21b337f7c344386bd81c205cf3f579ab58ef74bb Mon Sep 17 00:00:00 2001 From: admin Date: Sat, 6 Jun 2026 12:29:11 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Megaphone=20?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E5=AF=BC=E5=85=A5=E6=8B=BC=E5=86=99=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/inject/inject.go | 443 ++++++++ .../src/components/admin-sidebar/index.vue | 2 +- inject/android/HookApplication.java | 356 +++++++ inject/android/apk.php | 950 ++++++++++++++++++ 4 files changed, 1750 insertions(+), 1 deletion(-) create mode 100644 backend/internal/inject/inject.go create mode 100644 inject/android/HookApplication.java create mode 100644 inject/android/apk.php diff --git a/backend/internal/inject/inject.go b/backend/internal/inject/inject.go new file mode 100644 index 0000000..112fbf6 --- /dev/null +++ b/backend/internal/inject/inject.go @@ -0,0 +1,443 @@ +package inject + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + "time" +) + +// Platform 平台类型 +type Platform string + +const ( + PlatformAndroid Platform = "android" + PlatformWindows Platform = "windows" + PlatformLinux Platform = "linux" + PlatformIOS Platform = "ios" +) + +// Arch 架构类型 +type Arch string + +const ( + ArchX86 Arch = "x86" + ArchX64 Arch = "x64" + ArchARM Arch = "arm" + ArchARM64 Arch = "arm64" + ArchUniversal Arch = "universal" +) + +// InjectConfig 注入配置 +type InjectConfig struct { + Platform Platform `json:"platform"` + Arch Arch `json:"arch"` + TemplateName string `json:"template_name"` + ShowVerification bool `json:"show_verification"` + ShowAd bool `json:"show_ad"` + AdURL string `json:"ad_url"` + CountdownSeconds int `json:"countdown_seconds"` + AppName string `json:"app_name"` + VerifyURL string `json:"verify_url"` + AppKey string `json:"app_key"` +} + +// InjectResult 注入结果 +type InjectResult struct { + Success bool `json:"success"` + ResultFile string `json:"result_file"` + ResultSize int64 `json:"result_size"` + Error string `json:"error"` + ProcessTime int64 `json:"process_time"` // 处理耗时(毫秒) +} + +// InjectService 注入服务 +type InjectService struct { + ApktoolPath string + KeystorePath string + KeystorePass string + KeystoreAlias string + KeyPass string + ApksignerPath string + WorkDir string +} + +// NewInjectService 创建注入服务 +func NewInjectService(workDir string) *InjectService { + // 确保 workDir 是绝对路径 + absWorkDir, err := filepath.Abs(workDir) + if err != nil { + absWorkDir = workDir + } + + return &InjectService{ + WorkDir: absWorkDir, + ApktoolPath: getEnvOrDefault("APKTOOL_PATH", "apktool"), + KeystorePath: getEnvOrDefault("KEYSTORE_PATH", ""), + KeystorePass: getEnvOrDefault("KEYSTORE_PASS", "123456"), + KeystoreAlias: getEnvOrDefault("KEYSTORE_ALIAS", "mykey"), + KeyPass: getEnvOrDefault("KEY_PASS", "123456"), + ApksignerPath: getEnvOrDefault("APKSIGNER_PATH", "apksigner"), + } +} + +func getEnvOrDefault(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} + +// Inject 执行注入 +func (s *InjectService) Inject(inputFile string, config InjectConfig) (*InjectResult, error) { + startTime := time.Now() + + // 创建工作目录 + taskID := fmt.Sprintf("inject_%d", time.Now().UnixNano()) + taskDir := filepath.Join(s.WorkDir, taskID) + if err := os.MkdirAll(taskDir, 0755); err != nil { + return nil, fmt.Errorf("创建工作目录失败: %v", err) + } + defer os.RemoveAll(taskDir) // 清理工作目录 + + var result *InjectResult + var err error + + switch config.Platform { + case PlatformAndroid: + result, err = s.injectAndroid(inputFile, config, taskDir) + case PlatformWindows: + result, err = s.injectWindows(inputFile, config, taskDir) + case PlatformLinux: + result, err = s.injectLinux(inputFile, config, taskDir) + case PlatformIOS: + result, err = s.injectIOS(inputFile, config, taskDir) + default: + err = fmt.Errorf("不支持的平台: %s", config.Platform) + } + + if err != nil { + return &InjectResult{ + Success: false, + Error: err.Error(), + ProcessTime: time.Since(startTime).Milliseconds(), + }, err + } + + result.ProcessTime = time.Since(startTime).Milliseconds() + return result, nil +} + +// injectAndroid Android APK 注入 +func (s *InjectService) injectAndroid(inputFile string, config InjectConfig, taskDir string) (*InjectResult, error) { + log.Printf("[INFO] 开始 Android APK 注入: %s", inputFile) + + // 1. 反编译 APK + decompileDir := filepath.Join(taskDir, "decompiled") + if err := s.decompileAPK(inputFile, decompileDir); err != nil { + return nil, fmt.Errorf("反编译失败: %v", err) + } + + // 2. 复制注入模板的 smali 文件 + templateDir := filepath.Join("inject", "android") + if err := s.mergeSmali(templateDir, decompileDir); err != nil { + return nil, fmt.Errorf("合并 smali 失败: %v", err) + } + + // 3. 配置注入参数 + if err := s.configureAndroid(decompileDir, config); err != nil { + return nil, fmt.Errorf("配置失败: %v", err) + } + + // 4. 修改 AndroidManifest.xml + if err := s.modifyAndroidManifest(decompileDir); err != nil { + return nil, fmt.Errorf("修改 AndroidManifest 失败: %v", err) + } + + // 5. 回编译 APK + unsignedAPK := filepath.Join(taskDir, "unsigned.apk") + if err := s.recompileAPK(decompileDir, unsignedAPK); err != nil { + return nil, fmt.Errorf("回编译失败: %v", err) + } + + // 6. 签名 APK + signedAPK := filepath.Join(taskDir, "signed.apk") + if err := s.signAPK(unsignedAPK, signedAPK); err != nil { + return nil, fmt.Errorf("签名失败: %v", err) + } + + // 7. 获取结果文件信息 + fileInfo, err := os.Stat(signedAPK) + if err != nil { + return nil, fmt.Errorf("获取文件信息失败: %v", err) + } + + // 8. 复制结果文件到输出目录 + outputDir := filepath.Join("uploads", "inject") + if err := os.MkdirAll(outputDir, 0755); err != nil { + return nil, fmt.Errorf("创建输出目录失败: %v", err) + } + + outputFile := filepath.Join(outputDir, fmt.Sprintf("injected_%d.apk", time.Now().Unix())) + if err := copyFile(signedAPK, outputFile); err != nil { + return nil, fmt.Errorf("复制结果文件失败: %v", err) + } + + log.Printf("[INFO] Android APK 注入完成: %s", outputFile) + + return &InjectResult{ + Success: true, + ResultFile: "/" + strings.ReplaceAll(outputFile, "\\", "/"), + ResultSize: fileInfo.Size(), + }, nil +} + +// injectWindows Windows PE 注入 (占位) +func (s *InjectService) injectWindows(inputFile string, config InjectConfig, taskDir string) (*InjectResult, error) { + return nil, fmt.Errorf("Windows 平台注入暂未实现") +} + +// injectLinux Linux ELF 注入 (占位) +func (s *InjectService) injectLinux(inputFile string, config InjectConfig, taskDir string) (*InjectResult, error) { + return nil, fmt.Errorf("Linux 平台注入暂未实现") +} + +// injectIOS iOS IPA 注入 (占位) +func (s *InjectService) injectIOS(inputFile string, config InjectConfig, taskDir string) (*InjectResult, error) { + return nil, fmt.Errorf("iOS 平台注入暂未实现") +} + +// decompileAPK 反编译 APK +func (s *InjectService) decompileAPK(apkFile, outputDir string) error { + cmd := exec.Command("java", "-jar", s.ApktoolPath, "d", apkFile, "-o", outputDir, "-f") + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("apktool d 失败: %v, output: %s", err, string(output)) + } + log.Printf("[DEBUG] decompile output: %s", string(output)) + return nil +} + +// recompileAPK 回编译 APK +func (s *InjectService) recompileAPK(decompileDir, outputFile string) error { + cmd := exec.Command("java", "-jar", s.ApktoolPath, "b", decompileDir, "-o", outputFile) + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("apktool b 失败: %v, output: %s", err, string(output)) + } + log.Printf("[DEBUG] recompile output: %s", string(output)) + return nil +} + +// signAPK 签名 APK +func (s *InjectService) signAPK(unsignedAPK, signedAPK string) error { + // 如果没有配置 keystore,使用测试签名 + if s.KeystorePath == "" { + log.Printf("[WARN] 未配置 keystore,使用 zipalign 对齐") + // 使用 zipalign 对齐 + cmd := exec.Command("zipalign", "-v", "-p", "4", unsignedAPK, signedAPK) + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("zipalign 失败: %v, output: %s", err, string(output)) + } + return nil + } + + // 使用 apksigner 签名 + cmd := exec.Command(s.ApksignerPath, "sign", + "--ks", s.KeystorePath, + "--ks-key-alias", s.KeystoreAlias, + "--ks-pass", "pass:"+s.KeystorePass, + "--key-pass", "pass:"+s.KeyPass, + "--out", signedAPK, + unsignedAPK) + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("apksigner 签名失败: %v, output: %s", err, string(output)) + } + log.Printf("[DEBUG] sign output: %s", string(output)) + return nil +} + +// mergeSmali 合并 smali 文件 +func (s *InjectService) mergeSmali(templateDir, decompileDir string) error { + // 复制模板 smali 文件到反编译目录 + smaliDirs := []string{"smali", "smali_classes2", "smali_classes3"} + for _, smaliDir := range smaliDirs { + srcSmali := filepath.Join(templateDir, smaliDir) + if _, err := os.Stat(srcSmali); os.IsNotExist(err) { + continue + } + + dstSmali := filepath.Join(decompileDir, smaliDir) + if smaliDir == "smali" { + // 模板的 smali 直接覆盖 + if err := copyDir(srcSmali, dstSmali); err != nil { + return err + } + } else { + // 其他 smali_classes 需要合并 + if err := copyDir(srcSmali, dstSmali); err != nil { + return err + } + } + } + return nil +} + +// configureAndroid 配置 Android 注入参数 +func (s *InjectService) configureAndroid(decompileDir string, config InjectConfig) error { + // 查找并修改 HookApplication.java 中的配置 + hookAppPath := filepath.Join(decompileDir, "smali", "com", "example", "shell", "HookApplication.smali") + if _, err := os.Stat(hookAppPath); os.IsNotExist(err) { + log.Printf("[WARN] HookApplication.smali 不存在,跳过配置") + return nil + } + + // 读取文件内容 + content, err := ioutil.ReadFile(hookAppPath) + if err != nil { + return err + } + + contentStr := string(content) + + // 替换配置参数 + replacements := map[string]string{ + "[OpenClassName]": config.AppName, + "\"https://example.com\"": fmt.Sprintf("\"%s\"", config.VerifyURL), + "[AppKey]": config.AppKey, + } + + for old, new := range replacements { + contentStr = strings.ReplaceAll(contentStr, old, new) + } + + // 写回文件 + return ioutil.WriteFile(hookAppPath, []byte(contentStr), 0644) +} + +// modifyAndroidManifest 修改 AndroidManifest.xml +func (s *InjectService) modifyAndroidManifest(decompileDir string) error { + manifestPath := filepath.Join(decompileDir, "AndroidManifest.xml") + content, err := ioutil.ReadFile(manifestPath) + if err != nil { + return err + } + + contentStr := string(content) + + // 检查是否已有 android:name + if strings.Contains(contentStr, "android:name=") { + // 替换现有的 android:name + re := strings.NewReplacer( + `android:name="android.app.Application"`, `android:name="com.example.shell.HookApplication"`, + ) + contentStr = re.Replace(contentStr) + } else { + // 在 -import { Boxes, Code, CreditCard, DollarSign, FileLock, Gauge, GitBranch, HardDrive, Hash, Key, KeyRound, Mail, Megaphore, MessageSquare, Monitor, Network, Plug, ScrollText, Settings, Shield, Smartphone, Users, Variable } from 'lucide-vue-next' +import { Boxes, Code, CreditCard, DollarSign, FileLock, Gauge, GitBranch, HardDrive, Hash, Key, KeyRound, Mail, Megaphone, MessageSquare, Monitor, Network, Plug, ScrollText, Settings, Shield, Smartphone, Users, Variable } from 'lucide-vue-next' import { computed, onMounted, reactive, watch } from 'vue' import { useI18n } from 'vue-i18n' import { usePreferredDark } from '@vueuse/core' diff --git a/inject/android/HookApplication.java b/inject/android/HookApplication.java new file mode 100644 index 0000000..3537c59 --- /dev/null +++ b/inject/android/HookApplication.java @@ -0,0 +1,356 @@ +package com.example.shell; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Application; +import android.app.Dialog; +import android.graphics.Color; +import android.os.Handler; +import android.os.Looper; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import java.lang.ref.WeakReference; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Map; + +public class HookApplication extends Application { + + public static boolean showDialog = true; + public static boolean showFullscreen = true; // ✅ 全屏弹窗开关 + + private AlertDialog dialog = null; + private Dialog fullscreenDialog = null; // ✅ 全屏弹窗引用 + + public static boolean showImageFullscreen = true; // ✅ 控制图片弹窗 + private Dialog imageDialog = null; + + + + + + + private WeakReference currentActivityRef = null; + @Override + public void onCreate() { + super.onCreate(); + + Toast.makeText(getApplicationContext(), "HookApplication 已启动", Toast.LENGTH_SHORT).show(); + + //Hook弹窗 + new Handler(Looper.getMainLooper()).postDelayed(() -> { + showPopupLoop(); + }, 3000); + + //全屏欢迎弹窗 + new Handler(Looper.getMainLooper()).postDelayed(() -> { // ✅ 全屏弹窗监听 + showFullscreenLoop(); + }, 3000); + + //全屏广告弹窗 + new Handler(Looper.getMainLooper()).postDelayed(() -> { + showImageFullscreenLoop( + "https://i11.hoopchina.com.cn/editor/3037dc8d3081ab92b16a80b36ef1b332_w_1242_h_1242_.png", + () -> Toast.makeText(getApplicationContext(), "你点击了图片", Toast.LENGTH_SHORT).show(), + 5 + ); + }, 3000); + } + + private void showPopupLoop() { + Handler handler = new Handler(Looper.getMainLooper()); + + Runnable popupTask = new Runnable() { + @Override + public void run() { + if (!showDialog) { + if (dialog != null && dialog.isShowing()) { + dialog.dismiss(); + } + return; + } + + Activity activity = getCurrentActivity(); + + if (activity == null) { + handler.postDelayed(this, 1000); + return; + } + + boolean needShow = (dialog == null || !dialog.isShowing()); + boolean activityChanged = currentActivityRef == null || currentActivityRef.get() != activity; + + if (needShow || activityChanged) { + currentActivityRef = new WeakReference<>(activity); + dialog = new AlertDialog.Builder(activity) + .setTitle("Hook 弹窗") + .setMessage("当前 Activity: " + activity.getClass().getSimpleName()) + .setCancelable(false) + .setPositiveButton("关闭弹窗", (d, w) -> { + if (dialog != null) { + showDialog = false; + dialog.dismiss(); + } + }) + .show(); + } + + handler.postDelayed(this, 1000); + } + }; + + handler.post(popupTask); + } + + // ✅ 全屏弹窗监听器 + private void showFullscreenLoop() { + Handler handler = new Handler(Looper.getMainLooper()); + + Runnable fullscreenTask = new Runnable() { + @Override + public void run() { + if (!showFullscreen) { + if (fullscreenDialog != null && fullscreenDialog.isShowing()) { + fullscreenDialog.dismiss(); + } + return; + } + + Activity activity = getCurrentActivity(); + + if (activity == null) { + handler.postDelayed(this, 1000); + return; + } + + boolean needShow = (fullscreenDialog == null || !fullscreenDialog.isShowing()); + boolean activityChanged = currentActivityRef == null || currentActivityRef.get() != activity; + + if (needShow || activityChanged) { + currentActivityRef = new WeakReference<>(activity); + + fullscreenDialog = new Dialog(activity, android.R.style.Theme_Black_NoTitleBar_Fullscreen); + + LinearLayout layout = new LinearLayout(activity); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setBackgroundColor(Color.WHITE); + layout.setGravity(Gravity.CENTER); + layout.setLayoutParams(new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + )); + + TextView title = new TextView(activity); + title.setText("欢迎使用菜鸟安卓弹窗注入器"); + title.setTextSize(24); + title.setTextColor(Color.BLACK); + title.setGravity(Gravity.CENTER); + title.setPadding(0, 0, 0, 50); + layout.addView(title); + + Button close = new Button(activity); + close.setText("进入"); + layout.addView(close); + + close.setOnClickListener(v -> { + showFullscreen = false; + if (fullscreenDialog != null) { + fullscreenDialog.dismiss(); + } + }); + + fullscreenDialog.setContentView(layout); + fullscreenDialog.setCancelable(false); + fullscreenDialog.show(); + } + + handler.postDelayed(this, 1000); + } + }; + + handler.post(fullscreenTask); + } + private void showImageFullscreenLoop(String imageUrl, Runnable onImageClick, int countdownSeconds) { + Handler handler = new Handler(Looper.getMainLooper()); + + Runnable imageTask = new Runnable() { + int remaining = countdownSeconds; + + @Override + public void run() { + if (!showImageFullscreen) { + if (imageDialog != null && imageDialog.isShowing()) { + imageDialog.dismiss(); + } + return; + } + + Activity activity = getCurrentActivity(); + + if (activity == null) { + handler.postDelayed(this, 1000); + return; + } + + boolean needShow = (imageDialog == null || !imageDialog.isShowing()); + boolean activityChanged = currentActivityRef == null || currentActivityRef.get() != activity; + + if (needShow || activityChanged) { + currentActivityRef = new WeakReference<>(activity); + imageDialog = new Dialog(activity, android.R.style.Theme_Black_NoTitleBar_Fullscreen); + + // 主容器 + FrameLayout rootLayout = new FrameLayout(activity); + rootLayout.setLayoutParams(new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + )); + + // 图片控件 + android.widget.ImageView imageView = new android.widget.ImageView(activity); + imageView.setScaleType(android.widget.ImageView.ScaleType.FIT_XY); + imageView.setLayoutParams(new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + )); + + // 标签按钮 + // 标签按钮(带圆角背景) + Button closeBtn = new Button(activity); + closeBtn.setText("倒计时 " + remaining + " 秒"); + closeBtn.setTextColor(Color.WHITE); + closeBtn.setTextSize(14f); + closeBtn.setPadding(30, 10, 30, 10); + closeBtn.setEnabled(false); + + // 创建圆角背景 + android.graphics.drawable.GradientDrawable bg = new android.graphics.drawable.GradientDrawable(); + bg.setColor(Color.parseColor("#AA000000")); // 半透明黑 + bg.setCornerRadius(50); // 圆角 + closeBtn.setBackground(bg); + + + // 圆角布局参数 + FrameLayout.LayoutParams btnParams = new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ); + btnParams.gravity = Gravity.TOP | Gravity.END; + btnParams.setMargins(30, 60, 30, 0); + closeBtn.setLayoutParams(btnParams); + closeBtn.setEnabled(false); // 初始不可点击 + + rootLayout.addView(imageView); + rootLayout.addView(closeBtn); + + // 设置点击事件 + imageView.setOnClickListener(v -> { + if (onImageClick != null) onImageClick.run(); + }); + + closeBtn.setOnClickListener(v -> { + if (remaining <= 0) { + showImageFullscreen = false; + imageDialog.dismiss(); + } + }); + + imageDialog.setContentView(rootLayout); + imageDialog.setCancelable(false); + imageDialog.show(); + if (imageDialog.getWindow() != null) { + imageDialog.getWindow().setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ); + imageDialog.getWindow().getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + ); + } + + + // 倒计时逻辑 + Handler countdownHandler = new Handler(Looper.getMainLooper()); + Runnable countdown = new Runnable() { + @Override + public void run() { + if (!showImageFullscreen || imageDialog == null || !imageDialog.isShowing()) { + return; + } + + if (remaining > 0) { + closeBtn.setText("倒计时 " + remaining + " 秒"); + remaining--; + countdownHandler.postDelayed(this, 1000); + } else { + closeBtn.setText("关闭"); + closeBtn.setEnabled(true); + } + } + }; + countdownHandler.post(countdown); + + // 加载网络图片 + new Thread(() -> { + try { + java.net.URL url = new java.net.URL(imageUrl); + java.io.InputStream input = url.openStream(); + android.graphics.Bitmap bitmap = android.graphics.BitmapFactory.decodeStream(input); + input.close(); + + new Handler(Looper.getMainLooper()).post(() -> { + imageView.setImageBitmap(bitmap); + }); + } catch (Exception e) { + e.printStackTrace(); + } + }).start(); + } + + handler.postDelayed(this, 1000); + } + }; + + handler.post(imageTask); + } + + + private Activity getCurrentActivity() { + try { + Class activityThreadClass = Class.forName("android.app.ActivityThread"); + Method currentActivityThreadMethod = activityThreadClass.getMethod("currentActivityThread"); + Object activityThread = currentActivityThreadMethod.invoke(null); + + Field activitiesField = activityThreadClass.getDeclaredField("mActivities"); + activitiesField.setAccessible(true); + Map activities = (Map) activitiesField.get(activityThread); + + for (Object record : activities.values()) { + Class recordClass = record.getClass(); + Field pausedField = recordClass.getDeclaredField("paused"); + pausedField.setAccessible(true); + boolean paused = pausedField.getBoolean(record); + if (!paused) { + Field activityField = recordClass.getDeclaredField("activity"); + activityField.setAccessible(true); + return (Activity) activityField.get(record); + } + } + } catch (Throwable e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/inject/android/apk.php b/inject/android/apk.php new file mode 100644 index 0000000..073e961 --- /dev/null +++ b/inject/android/apk.php @@ -0,0 +1,950 @@ + 1){ + print_r("该应用基类层级过多,可能已经被注入过了\n"); +} +//print_r($result);//exit; + +$smali = merge_smali_directories($de_apk1, $de_apk2);//smali融合,将1号APK的smali复制到2号apk中 + +print_r("正在进行基类替换\n"); +$result = replace_application_super($result['file'],'com.example.shell.HookApplication;');//基类替换,将2号文件的application父类替换为1号文件中写好的application类 +//print_r($result);//exit; +$result = ensure_application_name($de_apk2, 'com.example.shell.HookApplication');//基类劫持的话,需要检查是否存在全局application,没有的话就需要添加 +//print_r($result); + +$result = merge_activities_to_application_only($de_apk1, $de_apk2);//AndroidManifest融合 + +$result = rebuild_apk($apktool_jar,$de_apk2);//回编译 + +$output_apk = $result[2];//拿到回编译之后的apk路径 + +//$result = sign_apk($keystore, $alias, $storepass, $keypass, $output_apk, null, $de_apk2, $apksigner_path);//签名,并删除反编译目录 +$result = sign_apk($keystore, $alias, $storepass, $keypass, $output_apk, null, null, $apksigner_path);//签名 +//print_r($result); +//delete_dir($de_apk1);//删除壳目录 +unlink($result[2].".idsig"); + + + + +//================================这里是启动入口修改注入的用法,兼容性并不是很好================================ +/* +//这里是启动入口替换 +$dirs = ['D:\Desktop\apktool\shell', 'D:\Desktop\apktool\qupai']; +$result = parse_apk_manifests($dirs);//找启动入口类名 +print_r("启动入口{$result[1][1]}\n"); +//exit; +$result = replace_smali_string('D:\Desktop\apktool\shell\smali', '[OpenClassName]', $result[1][1]);//融合之前进行smali字符串替换 +print_r("smali替换结果\n"); +print_r($result); +print_r("\n"); +$result = merge_android_manifests('D:\Desktop\apktool\shell', 'D:\Desktop\apktool\qupai');//AndroidManifest融合 +$result = merge_smali_directories('D:\Desktop\apktool\shell', 'D:\Desktop\apktool\qupai');//smali融合 */ +/* $list = [ + '/res/drawable/icon.png', + '/res/layout/main.xml' +]; +$result = copy_res_files($de_apk1, $de_apk2, $list);//资源融合,将1号APK里的资源复制到2号APK里 +print_r($result); */ +//================================下方方法代码勿动================================ + + + +function merge_activities_to_application_only($source_dir, $target_dir) { + $src_manifest = rtrim($source_dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'AndroidManifest.xml'; + $dst_manifest = rtrim($target_dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'AndroidManifest.xml'; + + if (!file_exists($src_manifest) || !file_exists($dst_manifest)) { + echo "Manifest 文件不存在\n"; + return false; + } + + // 加载源和目标 manifest + $src_doc = new DOMDocument(); + $src_doc->preserveWhiteSpace = false; + $src_doc->formatOutput = true; + $src_doc->load($src_manifest); + + $dst_doc = new DOMDocument(); + $dst_doc->preserveWhiteSpace = false; + $dst_doc->formatOutput = true; + $dst_doc->load($dst_manifest); + + // 获取 根节点 + $dst_manifest_node = $dst_doc->getElementsByTagName("manifest")->item(0); + $src_manifest_node = $src_doc->getElementsByTagName("manifest")->item(0); + + if (!$dst_manifest_node || !$src_manifest_node) { + echo "未找到 根节点\n"; + return false; + } + + // 合并 (去重) + $dst_perms = []; + foreach ($dst_doc->getElementsByTagName("uses-permission") as $perm) { + $name = $perm->getAttribute("android:name"); + if ($name) { + $dst_perms[$name] = true; + } + } + + foreach ($src_doc->getElementsByTagName("uses-permission") as $perm) { + $name = $perm->getAttribute("android:name"); + if ($name && !isset($dst_perms[$name])) { + $comment = $dst_doc->createComment(" 此 uses-permission 来自合并插入 "); + $dst_manifest_node->appendChild($comment); + $dst_manifest_node->appendChild($dst_doc->importNode($perm, true)); + $dst_perms[$name] = true; + } + } + + // 合并 (去重) + $dst_defined_perms = []; + foreach ($dst_doc->getElementsByTagName("permission") as $perm) { + $name = $perm->getAttribute("android:name"); + if ($name) { + $dst_defined_perms[$name] = true; + } + } + + /* foreach ($src_doc->getElementsByTagName("permission") as $perm) { + $name = $perm->getAttribute("android:name"); + if ($name && !isset($dst_defined_perms[$name])) { + $comment = $dst_doc->createComment(" 此 permission 来自合并插入 "); + $dst_manifest_node->appendChild($comment); + $dst_manifest_node->appendChild($dst_doc->importNode($perm, true)); + $dst_defined_perms[$name] = true; + } + } */ + + // 获取 节点 + $dst_app = $dst_doc->getElementsByTagName("application")->item(0); + $src_app = $src_doc->getElementsByTagName("application")->item(0); + + if (!$dst_app || !$src_app) { + echo "未找到 标签\n"; + return false; + } + + // 合并 (去掉 intent-filter) + foreach (['activity', 'activity-alias'] as $tag) { + foreach ($src_app->getElementsByTagName($tag) as $node) { + $imported = $dst_doc->importNode($node, true); + + // 移除 intent-filter + $filters = $imported->getElementsByTagName("intent-filter"); + while ($filters->length > 0) { + $imported->removeChild($filters->item(0)); + } + + $comment = $dst_doc->createComment(" 此 $tag 来自合并插入 "); + $dst_app->appendChild($comment); + $dst_app->appendChild($imported); + } + } + + // 保存结果 + $dst_doc->save($dst_manifest); + echo "合并完成:uses-permission、permission、activity 均已插入并带注释\n"; + return true; +} + + + + + + + + + + + + +function delete_dir($dir) { + if (!is_dir($dir)) return; + + $items = scandir($dir); + foreach ($items as $item) { + if ($item === '.' || $item === '..') continue; + $path = $dir . DIRECTORY_SEPARATOR . $item; + if (is_dir($path)) { + delete_dir($path); + } else { + unlink($path); + } + } + rmdir($dir); +} + +function copy_res_files($src_dir, $dst_dir, $file_list) { + $copied_files = []; + $count = 0; + + foreach ($file_list as $relative_path) { + // 标准化路径,去掉开头斜杠 + $relative_path = ltrim($relative_path, '/\\'); + + $src_file = rtrim($src_dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $relative_path; + $dst_file = rtrim($dst_dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $relative_path; + + if (!is_file($src_file)) { + continue; // 源文件不存在,跳过 + } + + if (file_exists($dst_file)) { + continue; // 目标已存在,跳过 + } + + // 创建目标文件夹 + $dst_folder = dirname($dst_file); + if (!is_dir($dst_folder)) { + mkdir($dst_folder, 0777, true); + } + + if (copy($src_file, $dst_file)) { + $copied_files[] = str_replace('\\', '/', $relative_path); + $count++; + } + } + + return [ + 'count' => $count, + 'files' => $copied_files + ]; +} + + + + +function ensure_application_name($apk_dir, $class_name) { + $manifest_path = rtrim($apk_dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'AndroidManifest.xml'; + + if (!file_exists($manifest_path)) { + return [false, "Manifest 文件不存在:$manifest_path"]; + } + + // 加载并解析 XML + libxml_use_internal_errors(true); // 屏蔽格式警告 + $xml = new DOMDocument(); + $xml->preserveWhiteSpace = false; + $xml->formatOutput = true; + $xml->load($manifest_path); + + $xpath = new DOMXPath($xml); + $xpath->registerNamespace("android", "http://schemas.android.com/apk/res/android"); + + // 获取 标签 + $applications = $xml->getElementsByTagName("application"); + if ($applications->length === 0) { + return [false, "未找到 标签"]; + } + + $application = $applications->item(0); + + // 查找 android:name 属性 + $name_attr = null; + foreach ($application->attributes as $attr) { + if ($attr->nodeName === "android:name" || $attr->name === "android:name") { + $name_attr = $attr; + break; + } + } + + // 是否需要修改 + $need_update = false; + + if ($name_attr === null) { + // 属性不存在,则添加 + $application->setAttribute("android:name", $class_name); + $need_update = true; + } elseif (trim($name_attr->value) === "") { + // 属性为空,设置新值 + $name_attr->value = $class_name; + $need_update = true; + } + + // 保存文件 + if ($need_update) { + $xml->save($manifest_path); + return [true, "已设置 android:name 为:$class_name"]; + } else { + return [true, "已有有效 android:name,无需修改"]; + } +} + + +//Application基类替换 +function replace_application_super($target_file_path, $new_super_class_name, $apk_root_dir = null) { + if (!is_file($target_file_path) || pathinfo($target_file_path, PATHINFO_EXTENSION) !== 'smali') { + return [false, "不是有效的 smali 文件", null, null]; + } + + // 将 Java 类名(com.xxx.HookApplication;)转为 smali 路径 + $class_name = rtrim($new_super_class_name, ';'); + $class_path = str_replace('.', '/', $class_name) . '.smali'; + + // 获取 apk 根目录(根据 smali 文件路径推算或传入) + if (!$apk_root_dir) { + $apk_root_dir = explode(DIRECTORY_SEPARATOR, $target_file_path); + while (count($apk_root_dir)) { + $path = implode(DIRECTORY_SEPARATOR, $apk_root_dir); + if (is_dir($path) && preg_match('/smali(_classes\d+)?$/', basename($path))) { + array_pop($apk_root_dir); // 去掉 smali_classesX + break; + } + array_pop($apk_root_dir); + } + $apk_root_dir = implode(DIRECTORY_SEPARATOR, $apk_root_dir); + } + + // 遍历 smali 目录查找新的父类文件 + $smali_dirs = []; + foreach (scandir($apk_root_dir) as $entry) { + if (preg_match('/^smali(_classes\d+)?$/', $entry) && is_dir($apk_root_dir . DIRECTORY_SEPARATOR . $entry)) { + $smali_dirs[] = $apk_root_dir . DIRECTORY_SEPARATOR . $entry; + } + } + + $hook_file_found = false; + $hook_file_path = null; + $hook_is_app = false; + + foreach ($smali_dirs as $dir) { + $full_path = $dir . DIRECTORY_SEPARATOR . $class_path; + if (file_exists($full_path)) { + $hook_file_found = true; + $hook_file_path = $full_path; + $lines = file($full_path); + foreach ($lines as $line) { + if (preg_match('/^\.super\s+(L[^;]+;)/', trim($line), $match)) { + if ($match[1] === 'Landroid/app/Application;') { + $hook_is_app = true; + } + break; + } + } + break; + } + } + + if (!$hook_file_found) { + return [false, "指定的新父类类文件不存在", null, null]; + } + + if (!$hook_is_app) { + return [false, "新父类不是 Application 子类,不允许替换", null, null]; + } + + // 开始替换目标文件中的 .super + $lines = file($target_file_path); + $modified = false; + $original_super = null; + $new_super_smali = 'L' . str_replace('.', '/', $class_name) . ';'; + + foreach ($lines as $index => $line) { + if (preg_match('/^\.super\s+(L[^;]+;)/', trim($line), $match)) { + $original_super = $match[1]; + if ($original_super !== $new_super_smali) { + $lines[$index] = ".super $new_super_smali\n"; + $modified = true; + } + break; + } + } + + if ($modified) { + file_put_contents($target_file_path, implode('', $lines)); + return [true, "替换成功", $original_super, $new_super_smali]; + } else { + return [false, "无需修改,.super 已是目标类", $original_super, $new_super_smali]; + } +} + + + +//基类读取 +function get_application_inheritance_chain($apk_dir) { + $manifest_path = rtrim($apk_dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'AndroidManifest.xml'; + + if (!file_exists($manifest_path)) { + return ['error' => "Manifest 文件不存在:$manifest_path"]; + } + + // 读取 AndroidManifest 内容 + $content = file_get_contents($manifest_path); + + // 提取 application 的 android:name + if (!preg_match('/]*android:name="([^"]+)"/', $content, $match)) { + return ['error' => "未找到 application 的 android:name 属性"]; + } + + $class_name = $match[1]; + + // 处理相对类名(.MyApp)拼接包名 + if (substr($class_name, 0, 1) === '.') { + if (preg_match('/]*package="([^"]+)"/', $content, $pkg_match)) { + $class_name = $pkg_match[1] . $class_name; + } + } + + // 构造链路 + $chain = build_class_chain($apk_dir, $class_name); + + // 获取最终基类名称 + $final = get_last_node($chain); + $depth = get_chain_depth($chain); + return [ + 'final_super' => $final['super'], + 'class' => $final['class'] ?? $class_name, + 'file' => $final['file'] ?? null, + 'depth' => $depth, + 'chain' => $chain + ]; +} +//基类链路查找 +function build_class_chain($apk_dir, $class_name) { + $class_path = str_replace('.', '/', ltrim($class_name, '.')) . '.smali'; + + $smali_dirs = []; + foreach (scandir($apk_dir) as $entry) { + if (preg_match('/^smali(_classes\d+)?$/', $entry) && is_dir($apk_dir . DIRECTORY_SEPARATOR . $entry)) { + $smali_dirs[] = $apk_dir . DIRECTORY_SEPARATOR . $entry; + } + } + + foreach ($smali_dirs as $smali_dir) { + $full_path = $smali_dir . DIRECTORY_SEPARATOR . $class_path; + if (file_exists($full_path)) { + $lines = file($full_path); + foreach ($lines as $line) { + if (preg_match('/^\.super\s+(L[^;]+;)/', trim($line), $super_match)) { + $super_smali = $super_match[1]; + + // 到达终点,这一步其实可以用数组将别的注入器的类也填进来,直接将别人的注入替换为自己的注入,比如云注入 Lcom/sadfxg/fasg/App + if ($super_smali === 'Landroid/app/Application;') { + return [ + 'class' => $class_name, + 'super' => $super_smali, + 'file' => $full_path, + 'extends' => null + ]; + } + + // 向上递归 + $super_java = str_replace('/', '.', substr($super_smali, 1, -1)); + return [ + 'class' => $class_name, + 'super' => $super_smali, + 'file' => $full_path, + 'extends' => build_class_chain($apk_dir, $super_java) + ]; + } + } + + return [ + 'class' => $class_name, + 'super' => null, + 'file' => $full_path, + 'extends' => null, + 'error' => '.super 未找到' + ]; + } + } + + return [ + 'class' => $class_name, + 'super' => null, + 'file' => null, + 'extends' => null, + 'error' => '类文件未找到' + ]; +} +function get_last_node($chain) { + while ($chain && isset($chain['extends']) && $chain['extends']) { + $chain = $chain['extends']; + } + return $chain; +} +function get_chain_depth($chain) { + $depth = 0; + while ($chain) { + $depth++; + $chain = $chain['extends'] ?? null; + } + return $depth; +} + + + + + +//smali文件融合 +function replace_smali_string($dir, $search, $replace, $case_sensitive = true) { + $result = []; + + if (!is_dir($dir)) { + echo "无效的目录:$dir\n"; + return $result; + } + + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir)); + foreach ($iterator as $file) { + if (!$file->isFile() || pathinfo($file, PATHINFO_EXTENSION) !== 'smali') { + continue; + } + + $file_path = $file->getPathname(); + $lines = file($file_path); // 逐行读取 + $modified = false; + $new_lines = []; + + foreach ($lines as $index => $line) { + $original_line = $line; + + if ($case_sensitive) { + if (strpos($line, $search) !== false) { + $new_line = str_replace($search, $replace, $line); + if ($new_line !== $line) { + $result[] = [ + 'file' => $file_path, + 'line' => $index + 1, + 'original' => rtrim($line), + 'modified' => rtrim($new_line) + ]; + $line = $new_line; + $modified = true; + } + } + } else { + if (preg_match('/' . preg_quote($search, '/') . '/i', $line)) { + $new_line = preg_replace('/' . preg_quote($search, '/') . '/i', $replace, $line); + if ($new_line !== $line) { + $result[] = [ + 'file' => $file_path, + 'line' => $index + 1, + 'original' => rtrim($line), + 'modified' => rtrim($new_line) + ]; + $line = $new_line; + $modified = true; + } + } + } + + $new_lines[] = $line; + } + + if ($modified) { + file_put_contents($file_path, implode('', $new_lines)); + } + } + + return $result; +} + + +//签名APK +function sign_apk($keystore, $alias, $storepass, $keypass, $unsigned_apk, $signed_apk = null, $output_folder = null, $apksigner_path = 'apksigner') { + if (!file_exists($unsigned_apk)) { + $msg = "未找到待签名 APK 文件:$unsigned_apk"; + echo "$msg\n"; + return [false, $msg, null, null]; + } + + if (!file_exists($keystore)) { + $msg = "签名文件不存在:$keystore"; + echo "$msg\n"; + return [false, $msg, null, null]; + } + + // 自动生成签名后 APK 名称 + if (empty($signed_apk)) { + $signed_apk = preg_replace('/\.apk$/', '.signed.apk', $unsigned_apk); + } + + // 构造签名命令 + $cmd = "\"$apksigner_path\" sign --ks \"$keystore\" --ks-key-alias $alias --ks-pass pass:$storepass --key-pass pass:$keypass --out \"$signed_apk\" \"$unsigned_apk\""; + echo "执行签名命令:$cmd\n"; + + // 执行命令 + $output = shell_exec($cmd); + echo "签名输出:\n$output\n"; + + // 判断签名结果 + if (file_exists($signed_apk)) { + $msg = "✅ 签名完成:$signed_apk"; + echo "$msg\n"; + + // 删除未签名 APK + unlink($unsigned_apk); + echo "已删除未签名文件:$unsigned_apk\n"; + + // 提示删除反编译目录(不自动执行) + if (!empty($output_folder)) { + delete_dir($output_folder); + //echo "请手动清理反编译目录:$output_folder\n"; + } + + return [true, $msg, $signed_apk, $output]; + } else { + $msg = "❌ 签名失败,未生成文件。"; + echo "$msg\n"; + return [false, $msg, null, $output]; + } +} + + + +//回编译 +function rebuild_apk($apktool_path, $decode_folder, $output_apk = null) { + if (!file_exists($apktool_path)) { + $msg = "找不到 apktool 工具:$apktool_path"; + echo "$msg\n"; + return [false, $msg, null, null]; + } + + if (!is_dir($decode_folder)) { + $msg = "反编译目录不存在:$decode_folder"; + echo "$msg\n"; + return [false, $msg, null, null]; + } + + // 如果未指定输出 APK 路径,则自动生成 + if (empty($output_apk)) { + $parent_dir = dirname($decode_folder); + $folder_name = basename($decode_folder); + $output_apk = $parent_dir . DIRECTORY_SEPARATOR . $folder_name . '.build.apk'; + } + + // 构造打包命令 + $cmd = "java -jar \"$apktool_path\" b \"$decode_folder\" -o \"$output_apk\""; + //echo "执行打包命令:$cmd\n"; + echo "开始回编译打包\n"; + + // 执行命令 + $output = shell_exec($cmd); + //echo "回编译输出:\n$output\n"; + + // 检查输出文件 + if (file_exists($output_apk)) { + $msg = "APK 回编译成功,输出文件:$output_apk"; + echo "$msg\n"; + return [true, $msg, $output_apk, $output]; + } else { + $msg = "回编译失败,未生成 APK 文件。"; + echo "$msg\n"; + return [false, $msg, $output_apk, $output]; + } +} + + + + +function merge_smali_directories($source_dir, $target_dir) { + // 1. 获取源目录中所有 smali 和 smali_* 目录 + $smali_dirs = []; + foreach (scandir($source_dir) as $entry) { + if (preg_match('/^smali(_classes\d+)?$/', $entry) && is_dir($source_dir . DIRECTORY_SEPARATOR . $entry)) { + $smali_dirs[] = $entry; + } + } + + // 2. 获取目标目录已有的最大 smali_classesN 序号 + $existing = []; + foreach (scandir($target_dir) as $entry) { + if (preg_match('/^smali(_classes(\d+))?$/', $entry, $m) && is_dir($target_dir . DIRECTORY_SEPARATOR . $entry)) { + $existing[] = isset($m[2]) ? intval($m[2]) : 1; // smali 视为 classes1 + } + } + $max_index = empty($existing) ? 0 : max($existing); + + // 3. 依次复制,每个递增命名 + foreach ($smali_dirs as $dir_name) { + $src_path = $source_dir . DIRECTORY_SEPARATOR . $dir_name; + $new_index = ++$max_index; + $dst_name = $new_index === 1 ? 'smali' : 'smali_classes' . $new_index; + $dst_path = $target_dir . DIRECTORY_SEPARATOR . $dst_name; + + // 递归复制目录 + recursive_copy($src_path, $dst_path); + echo "已复制 $dir_name 到 $dst_name\n"; + } + + echo "smali 融合完成。\n"; + return true; +} + +// 工具函数:递归复制目录内容 +function recursive_copy($src, $dst) { + if (!is_dir($src)) return; + if (!file_exists($dst)) mkdir($dst, 0777, true); + + $items = scandir($src); + foreach ($items as $item) { + if ($item === '.' || $item === '..') continue; + $src_item = $src . DIRECTORY_SEPARATOR . $item; + $dst_item = $dst . DIRECTORY_SEPARATOR . $item; + + if (is_dir($src_item)) { + recursive_copy($src_item, $dst_item); + } else { + copy($src_item, $dst_item); + } + } +} + + + + +//AndroidManifest融合 +function merge_android_manifests($source_dir, $target_dir, $intent = false) { + $src_manifest = rtrim($source_dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'AndroidManifest.xml'; + $dst_manifest = rtrim($target_dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'AndroidManifest.xml'; + + if (!file_exists($src_manifest) || !file_exists($dst_manifest)) { + echo "Manifest 文件不存在\n"; + return false; + } + + $src_xml = file_get_contents($src_manifest); + $dst_xml = file_get_contents($dst_manifest); + + // 1. 提取 + preg_match_all('/]+\/>/', $src_xml, $src_permissions); + preg_match_all('/]+\/>/', $dst_xml, $dst_permissions); + $src_permissions = array_unique($src_permissions[0]); + $dst_permissions_text = implode("\n", $dst_permissions[0]); + + // 2. 提取 + preg_match_all('/]+\/>/', $src_xml, $src_custom_permissions); + preg_match_all('/]+\/>/', $dst_xml, $dst_custom_permissions); + $src_custom_permissions = array_unique($src_custom_permissions[0]); + $dst_custom_permissions_text = implode("\n", $dst_custom_permissions[0]); + + // 3. 提取 + preg_match_all('/]*>.*?<\/activity>/is', $src_xml, $src_activities); + preg_match_all('/]*>.*?<\/activity-alias>/is', $src_xml, $src_aliases); + $src_activities = $src_activities[0]; + $src_aliases = $src_aliases[0]; + $all_activities = array_merge($src_activities, $src_aliases); + + // 4. 处理 Activity,仅保留一个入口 + $entry_found = false; + $processed_activities = []; + + foreach ($all_activities as $block) { + if (!$entry_found && preg_match('/.*?MAIN.*?LAUNCHER.*?<\/intent-filter>/is', $block)) { + $processed_activities[] = "\n" . $block; + $entry_found = true; + } else { + $block_no_entry = preg_replace('/.*?<\/intent-filter>/is', '', $block); + $processed_activities[] = "\n" . $block_no_entry; + } + } + + // 5. 注释原 manifest 中的 intent-filter 启动项 + if($intent){ + $dst_xml = preg_replace_callback( + '/(<(activity|activity-alias)\b[^>]*>)(.*?.*?<\/intent-filter>)(.*?<\/\2>)/is', + function ($matches) { + if (preg_match('/android.intent.action.MAIN/', $matches[3]) && + preg_match('/android.intent.category.LAUNCHER/', $matches[3])) { + $commented = "\n"; + $commented .= preg_replace('/(.*?<\/intent-filter>)/is', '', $matches[3]); + return $matches[1] . "\n" . $commented . "\n" . $matches[4]; + } + return $matches[0]; + }, + $dst_xml + ); + } + // 6. 插入 (自定义权限),避免重复 + foreach ($src_custom_permissions as $perm_def) { + if (strpos($dst_custom_permissions_text, $perm_def) === false) { + $insert = " \n $perm_def"; + //$dst_xml = preg_replace('/(]*>)/', "$1\n$insert", $dst_xml);//插入后会导致无法安装 + } + } + + // 7. 插入 (跳过重复) + foreach ($src_permissions as $perm) { + if (strpos($dst_permissions_text, $perm) === false) { + $insert = " \n $perm"; + $dst_xml = preg_replace('/(]*>)/', "$1\n$insert", $dst_xml); + } + } + + // 8. 插入 中 + $insert_block = implode("\n ", $processed_activities); + $dst_xml = preg_replace_callback( + '/]*>/', + function ($matches) use ($insert_block) { + return $matches[0] . "\n " . $insert_block; + }, + $dst_xml, + 1 + ); + + // 9. 保存结果 + file_put_contents($dst_manifest, $dst_xml); + echo "合并完成:权限、自定义权限、Activity 合并,并保留注释信息:$dst_manifest\n"; + return true; +} + + + + + +//找启动入口类名 +function parse_apk_manifests($dirs) { + $results = []; + + foreach ($dirs as $dir) { + $manifest_path = rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'AndroidManifest.xml'; + + if (!file_exists($manifest_path)) { + $results[] = [false, null, null, "Manifest 文件不存在:$manifest_path"]; + continue; + } + + // 加载 XML + $xml = new DOMDocument(); + libxml_use_internal_errors(true); // 忽略格式警告 + $xml->load($manifest_path); + + $xpath = new DOMXPath($xml); + $launcher_activity = null; + $source = null; + + // 查找所有 + $activities = $xpath->query('//activity'); + foreach ($activities as $activity) { + $intent_filters = $activity->getElementsByTagName('intent-filter'); + foreach ($intent_filters as $filter) { + $has_main = false; + $has_launcher = false; + + foreach ($filter->getElementsByTagName('action') as $action) { + if ($action->getAttribute('android:name') === 'android.intent.action.MAIN') { + $has_main = true; + } + } + + foreach ($filter->getElementsByTagName('category') as $category) { + if ($category->getAttribute('android:name') === 'android.intent.category.LAUNCHER') { + $has_launcher = true; + } + } + + if ($has_main && $has_launcher) { + $launcher_activity = $activity->getAttribute('android:name'); + $source = 'activity'; + break 2; + } + } + } + + // 如果未找到,再查找 + if (!$launcher_activity) { + $aliases = $xpath->query('//activity-alias'); + foreach ($aliases as $alias) { + $intent_filters = $alias->getElementsByTagName('intent-filter'); + foreach ($intent_filters as $filter) { + $has_main = false; + $has_launcher = false; + + foreach ($filter->getElementsByTagName('action') as $action) { + if ($action->getAttribute('android:name') === 'android.intent.action.MAIN') { + $has_main = true; + } + } + + foreach ($filter->getElementsByTagName('category') as $category) { + if ($category->getAttribute('android:name') === 'android.intent.category.LAUNCHER') { + $has_launcher = true; + } + } + + if ($has_main && $has_launcher) { + $launcher_activity = $alias->getAttribute('android:targetActivity'); + $source = 'activity-alias'; + break 2; + } + } + } + } + + if ($launcher_activity !== null) { + $results[] = [true, $launcher_activity, $source, null]; + } else { + $results[] = [false, null, null, "未找到启动 Activity:$manifest_path"]; + } + } + + return $results; +} + + + + + + + + + + +// 方法:反编译 APK +function decompile_apks($apktool_jar, $apk_files, $output_base_dir = null) { + $results = []; + + // 检查所有 APK 文件是否存在 + foreach ($apk_files as $apk_file) { + if (!file_exists($apk_file)) { + return [[false, "APK 文件不存在:$apk_file", null, null]]; + } + } + + // 遍历每个 APK 执行反编译 + foreach ($apk_files as $apk_file) { + // 生成默认输出目录 + $apk_dir = dirname($apk_file); + $apk_name = pathinfo($apk_file, PATHINFO_FILENAME); + $output_dir = ($output_base_dir ?? $apk_dir) . DIRECTORY_SEPARATOR . $apk_name; + + // 构造反编译命令 + $cmd = "java -jar \"$apktool_jar\" d \"$apk_file\" -o \"$output_dir\" -f"; + + // 执行命令 + $output = shell_exec($cmd); + + // 判断是否成功(通过输出目录是否存在判断) + if (is_dir($output_dir)) { + $results[] = [true, "反编译成功", $output_dir, $output]; + } else { + $results[] = [false, "反编译失败", $output_dir, $output]; + } + } + + return $results; +} +