fix: 修复 Megaphone 图标导入拼写错误
This commit is contained in:
@@ -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 {
|
||||||
|
// 在 <application 标签添加 android:name
|
||||||
|
contentStr = strings.Replace(contentStr, "<application", `<application android:name="com.example.shell.HookApplication"`, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ioutil.WriteFile(manifestPath, []byte(contentStr), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyFile 复制文件
|
||||||
|
func copyFile(src, dst string) error {
|
||||||
|
input, err := ioutil.ReadFile(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ioutil.WriteFile(dst, input, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyDir 复制目录
|
||||||
|
func copyDir(src, dst string) error {
|
||||||
|
// 确保目标目录存在
|
||||||
|
if err := os.MkdirAll(dst, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := ioutil.ReadDir(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
srcPath := filepath.Join(src, entry.Name())
|
||||||
|
dstPath := filepath.Join(dst, entry.Name())
|
||||||
|
|
||||||
|
if entry.IsDir() {
|
||||||
|
if err := copyDir(srcPath, dstPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := copyFile(srcPath, dstPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSupportedPlatforms 获取支持的平台和架构
|
||||||
|
func GetSupportedPlatforms() map[string][]string {
|
||||||
|
return map[string][]string{
|
||||||
|
"android": {"x86", "x64", "arm", "arm64"},
|
||||||
|
"windows": {"x86", "x64"},
|
||||||
|
"linux": {"x86", "x64", "arm", "arm64"},
|
||||||
|
"ios": {"arm", "arm64"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectPlatform 检测文件平台
|
||||||
|
func DetectPlatform(filename string) (Platform, error) {
|
||||||
|
ext := strings.ToLower(filepath.Ext(filename))
|
||||||
|
switch ext {
|
||||||
|
case ".apk":
|
||||||
|
return PlatformAndroid, nil
|
||||||
|
case ".exe", ".dll":
|
||||||
|
return PlatformWindows, nil
|
||||||
|
case ".so", "", ".elf":
|
||||||
|
if strings.Contains(strings.ToLower(filename), "linux") || ext == ".so" {
|
||||||
|
return PlatformLinux, nil
|
||||||
|
}
|
||||||
|
case ".ipa":
|
||||||
|
return PlatformIOS, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据文件名猜测
|
||||||
|
lowerName := strings.ToLower(filename)
|
||||||
|
if strings.Contains(lowerName, "android") || strings.Contains(lowerName, "apk") {
|
||||||
|
return PlatformAndroid, nil
|
||||||
|
}
|
||||||
|
if strings.Contains(lowerName, "windows") || strings.Contains(lowerName, "win") || strings.Contains(lowerName, ".exe") {
|
||||||
|
return PlatformWindows, nil
|
||||||
|
}
|
||||||
|
if strings.Contains(lowerName, "linux") || strings.Contains(lowerName, ".so") {
|
||||||
|
return PlatformLinux, nil
|
||||||
|
}
|
||||||
|
if strings.Contains(lowerName, "ios") || strings.Contains(lowerName, ".ipa") {
|
||||||
|
return PlatformIOS, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("无法检测文件平台: %s", filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToJSON 转换为 JSON
|
||||||
|
func (c *InjectConfig) ToJSON() string {
|
||||||
|
data, _ := json.Marshal(c)
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseConfig 从 JSON 解析配置
|
||||||
|
func ParseConfig(jsonStr string) (*InjectConfig, error) {
|
||||||
|
var config InjectConfig
|
||||||
|
err := json.Unmarshal([]byte(jsonStr), &config)
|
||||||
|
return &config, err
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
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 { computed, onMounted, reactive, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { usePreferredDark } from '@vueuse/core'
|
import { usePreferredDark } from '@vueuse/core'
|
||||||
|
|||||||
@@ -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<Activity> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,950 @@
|
|||||||
|
<?
|
||||||
|
$apktool_jar = 'D:\Desktop\apktool\apktool_2.11.1.jar';//apktool包的路径
|
||||||
|
$apk_file[0] = 'D:\Desktop\apktool\app-release.apk';//1号apk,也就是注入弹窗壳apk
|
||||||
|
$apk_file[1] = 'D:\Desktop\apktool\original.apk';//2号apk,也就是被注入的正常APK
|
||||||
|
|
||||||
|
|
||||||
|
$keystore = 'D:/Desktop/apktool/my-release-key.keystore';//证书文件
|
||||||
|
$alias = 'myalias';
|
||||||
|
$storepass = '123456';
|
||||||
|
$keypass = '123456';
|
||||||
|
|
||||||
|
$apksigner_path = 'C:\Users\Administrator\AppData\Local\Android\Sdk\build-tools\35.0.0/apksigner.bat'; //签名工具,这里我定义的是我的Androidstudio的路径 或仅用 'apksigner' 如果加到环境变量了的话这里就可以填null
|
||||||
|
print_r("开始反编译\n");
|
||||||
|
$decompile = decompile_apks($apktool_jar, $apk_file);//反编译
|
||||||
|
//print_r($decompile);exit;
|
||||||
|
|
||||||
|
$de_apk1 = $decompile[0][2];
|
||||||
|
$de_apk2 = $decompile[1][2];
|
||||||
|
|
||||||
|
//这里是基类劫持
|
||||||
|
$result = get_application_inheritance_chain($de_apk2);//2号APK的基类读取
|
||||||
|
if($result['depth'] > 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);
|
||||||
|
|
||||||
|
// 获取 <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 "未找到 <manifest> 根节点\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并 <uses-permission>(去重)
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并 <permission>(去重)
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
} */
|
||||||
|
|
||||||
|
// 获取 <application> 节点
|
||||||
|
$dst_app = $dst_doc->getElementsByTagName("application")->item(0);
|
||||||
|
$src_app = $src_doc->getElementsByTagName("application")->item(0);
|
||||||
|
|
||||||
|
if (!$dst_app || !$src_app) {
|
||||||
|
echo "未找到 <application> 标签\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并 <activity> 和 <activity-alias>(去掉 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");
|
||||||
|
|
||||||
|
// 获取 <application> 标签
|
||||||
|
$applications = $xml->getElementsByTagName("application");
|
||||||
|
if ($applications->length === 0) {
|
||||||
|
return [false, "未找到 <application> 标签"];
|
||||||
|
}
|
||||||
|
|
||||||
|
$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('/<application[^>]*android:name="([^"]+)"/', $content, $match)) {
|
||||||
|
return ['error' => "未找到 application 的 android:name 属性"];
|
||||||
|
}
|
||||||
|
|
||||||
|
$class_name = $match[1];
|
||||||
|
|
||||||
|
// 处理相对类名(.MyApp)拼接包名
|
||||||
|
if (substr($class_name, 0, 1) === '.') {
|
||||||
|
if (preg_match('/<manifest[^>]*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. 提取 <uses-permission>
|
||||||
|
preg_match_all('/<uses-permission[^>]+\/>/', $src_xml, $src_permissions);
|
||||||
|
preg_match_all('/<uses-permission[^>]+\/>/', $dst_xml, $dst_permissions);
|
||||||
|
$src_permissions = array_unique($src_permissions[0]);
|
||||||
|
$dst_permissions_text = implode("\n", $dst_permissions[0]);
|
||||||
|
|
||||||
|
// 2. 提取 <permission>
|
||||||
|
preg_match_all('/<permission[^>]+\/>/', $src_xml, $src_custom_permissions);
|
||||||
|
preg_match_all('/<permission[^>]+\/>/', $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. 提取 <activity> 和 <activity-alias>
|
||||||
|
preg_match_all('/<activity\b[^>]*>.*?<\/activity>/is', $src_xml, $src_activities);
|
||||||
|
preg_match_all('/<activity-alias\b[^>]*>.*?<\/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('/<intent-filter>.*?MAIN.*?LAUNCHER.*?<\/intent-filter>/is', $block)) {
|
||||||
|
$processed_activities[] = "<!-- 此 activity 来自插入 -->\n" . $block;
|
||||||
|
$entry_found = true;
|
||||||
|
} else {
|
||||||
|
$block_no_entry = preg_replace('/<intent-filter>.*?<\/intent-filter>/is', '', $block);
|
||||||
|
$processed_activities[] = "<!-- 此 activity 来自插入 -->\n" . $block_no_entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 注释原 manifest 中的 intent-filter 启动项
|
||||||
|
if($intent){
|
||||||
|
$dst_xml = preg_replace_callback(
|
||||||
|
'/(<(activity|activity-alias)\b[^>]*>)(.*?<intent-filter>.*?<\/intent-filter>)(.*?<\/\2>)/is',
|
||||||
|
function ($matches) {
|
||||||
|
if (preg_match('/android.intent.action.MAIN/', $matches[3]) &&
|
||||||
|
preg_match('/android.intent.category.LAUNCHER/', $matches[3])) {
|
||||||
|
$commented = "<!-- 此处为原启动入口,intent-filter 已被注释 -->\n";
|
||||||
|
$commented .= preg_replace('/(<intent-filter>.*?<\/intent-filter>)/is', '<!-- $1 -->', $matches[3]);
|
||||||
|
return $matches[1] . "\n" . $commented . "\n" . $matches[4];
|
||||||
|
}
|
||||||
|
return $matches[0];
|
||||||
|
},
|
||||||
|
$dst_xml
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 6. 插入 <permission>(自定义权限),避免重复
|
||||||
|
foreach ($src_custom_permissions as $perm_def) {
|
||||||
|
if (strpos($dst_custom_permissions_text, $perm_def) === false) {
|
||||||
|
$insert = " <!-- 此自定义权限来自插入 -->\n $perm_def";
|
||||||
|
//$dst_xml = preg_replace('/(<manifest[^>]*>)/', "$1\n$insert", $dst_xml);//插入后会导致无法安装
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 插入 <uses-permission>(跳过重复)
|
||||||
|
foreach ($src_permissions as $perm) {
|
||||||
|
if (strpos($dst_permissions_text, $perm) === false) {
|
||||||
|
$insert = " <!-- 此权限来自插入 -->\n $perm";
|
||||||
|
$dst_xml = preg_replace('/(<manifest[^>]*>)/', "$1\n$insert", $dst_xml);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. 插入 <activity> 和 <activity-alias> 到 <application> 中
|
||||||
|
$insert_block = implode("\n ", $processed_activities);
|
||||||
|
$dst_xml = preg_replace_callback(
|
||||||
|
'/<application[^>]*>/',
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 查找所有 <activity>
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果未找到,再查找 <activity-alias>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user