android-decompile-mcp 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.ar.md +228 -0
- package/README.de.md +226 -0
- package/README.es.md +226 -0
- package/README.fr.md +226 -0
- package/README.hi.md +226 -0
- package/README.id.md +226 -0
- package/README.it.md +226 -0
- package/README.ja.md +226 -0
- package/README.ko.md +226 -0
- package/README.md +258 -3
- package/README.pt-BR.md +226 -0
- package/README.ru.md +226 -0
- package/README.th.md +226 -0
- package/README.tr.md +226 -0
- package/README.vi.md +226 -0
- package/README.zh-CN.md +252 -0
- package/README.zh-TW.md +226 -0
- package/dist/adb.js +89 -0
- package/dist/detect.js +42 -0
- package/dist/entry.js +219 -0
- package/dist/index.js +419 -0
- package/dist/repack.js +189 -0
- package/dist/smali.js +128 -0
- package/dist/smali_patch.js +237 -0
- package/dist/unpack/blackdex.js +119 -0
- package/dist/unpack/frida.js +66 -0
- package/dist/unpack/static.js +50 -0
- package/dist/util.js +61 -0
- package/package.json +56 -6
package/dist/adb.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { exec } from "./util.js";
|
|
2
|
+
/** ADB 可执行文件路径,可通过环境变量 ADB_PATH 覆盖。 */
|
|
3
|
+
const ADB = process.env.ADB_PATH || "adb";
|
|
4
|
+
function adb(args, opts = {}) {
|
|
5
|
+
const full = opts.serial ? ["-s", opts.serial, ...args] : args;
|
|
6
|
+
return exec(ADB, full, { timeoutMs: opts.timeoutMs ?? 60000 });
|
|
7
|
+
}
|
|
8
|
+
/** 列出已连接设备。 */
|
|
9
|
+
export async function listDevices() {
|
|
10
|
+
const r = await adb(["devices", "-l"]);
|
|
11
|
+
const lines = r.stdout.split(/\r?\n/).slice(1);
|
|
12
|
+
const devices = [];
|
|
13
|
+
for (const line of lines) {
|
|
14
|
+
const t = line.trim();
|
|
15
|
+
if (!t)
|
|
16
|
+
continue;
|
|
17
|
+
const parts = t.split(/\s+/);
|
|
18
|
+
const serial = parts[0];
|
|
19
|
+
const state = parts[1] || "unknown";
|
|
20
|
+
const modelPart = parts.find((p) => p.startsWith("model:"));
|
|
21
|
+
devices.push({ serial, state, model: modelPart?.slice("model:".length) });
|
|
22
|
+
}
|
|
23
|
+
return devices;
|
|
24
|
+
}
|
|
25
|
+
/** 选取一个默认设备 serial;若指定了 serial 则校验存在。 */
|
|
26
|
+
export async function resolveSerial(serial) {
|
|
27
|
+
const devices = (await listDevices()).filter((d) => d.state === "device");
|
|
28
|
+
if (devices.length === 0) {
|
|
29
|
+
throw new Error("没有可用的 ADB 设备(确认设备已连接并开启 USB 调试)。");
|
|
30
|
+
}
|
|
31
|
+
if (serial) {
|
|
32
|
+
if (!devices.find((d) => d.serial === serial)) {
|
|
33
|
+
throw new Error(`指定的设备 ${serial} 不存在或不可用。`);
|
|
34
|
+
}
|
|
35
|
+
return serial;
|
|
36
|
+
}
|
|
37
|
+
if (devices.length > 1) {
|
|
38
|
+
throw new Error(`检测到多台设备,请通过 serial 参数指定:${devices.map((d) => d.serial).join(", ")}`);
|
|
39
|
+
}
|
|
40
|
+
return devices[0].serial;
|
|
41
|
+
}
|
|
42
|
+
/** 列出设备上已安装的包名,可带关键字过滤。 */
|
|
43
|
+
export async function listPackages(serial, keyword) {
|
|
44
|
+
const r = await adb(["shell", "pm", "list", "packages"], { serial });
|
|
45
|
+
let pkgs = r.stdout
|
|
46
|
+
.split(/\r?\n/)
|
|
47
|
+
.map((l) => l.trim().replace(/^package:/, ""))
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
if (keyword)
|
|
50
|
+
pkgs = pkgs.filter((p) => p.includes(keyword));
|
|
51
|
+
return pkgs.sort();
|
|
52
|
+
}
|
|
53
|
+
/** 获取某个包在设备上的所有 APK 路径(含 split)。 */
|
|
54
|
+
export async function getApkPaths(serial, pkg) {
|
|
55
|
+
const r = await adb(["shell", "pm", "path", pkg], { serial });
|
|
56
|
+
const paths = r.stdout
|
|
57
|
+
.split(/\r?\n/)
|
|
58
|
+
.map((l) => l.trim().replace(/^package:/, ""))
|
|
59
|
+
.filter(Boolean);
|
|
60
|
+
if (paths.length === 0)
|
|
61
|
+
throw new Error(`设备上未找到包 ${pkg} 的 APK 路径。`);
|
|
62
|
+
return paths;
|
|
63
|
+
}
|
|
64
|
+
/** 从设备拉取文件到本地。 */
|
|
65
|
+
export async function pull(serial, remote, local) {
|
|
66
|
+
return adb(["pull", remote, local], { serial, timeoutMs: 300000 });
|
|
67
|
+
}
|
|
68
|
+
/** 推送文件到设备。 */
|
|
69
|
+
export async function push(serial, local, remote) {
|
|
70
|
+
return adb(["push", local, remote], { serial, timeoutMs: 300000 });
|
|
71
|
+
}
|
|
72
|
+
/** 安装 APK。 */
|
|
73
|
+
export async function install(serial, apkPath) {
|
|
74
|
+
return adb(["install", "-r", "-g", apkPath], { serial, timeoutMs: 300000 });
|
|
75
|
+
}
|
|
76
|
+
/** 执行任意 shell 命令。 */
|
|
77
|
+
export async function shell(serial, command) {
|
|
78
|
+
return adb(["shell", command], { serial });
|
|
79
|
+
}
|
|
80
|
+
/** 判断包是否已安装。 */
|
|
81
|
+
export async function isInstalled(serial, pkg) {
|
|
82
|
+
const pkgs = await listPackages(serial, pkg);
|
|
83
|
+
return pkgs.includes(pkg);
|
|
84
|
+
}
|
|
85
|
+
/** 启动一个 app(monkey 方式,无需指定 Activity)。 */
|
|
86
|
+
export async function launchApp(serial, pkg) {
|
|
87
|
+
return adb(["shell", "monkey", "-p", pkg, "-c", "android.intent.category.LAUNCHER", "1"], { serial });
|
|
88
|
+
}
|
|
89
|
+
export { adb };
|
package/dist/detect.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import AdmZip from "adm-zip";
|
|
2
|
+
/** 常见加固壳特征库(文件/so 名)。 */
|
|
3
|
+
const SIGNATURES = [
|
|
4
|
+
{ name: "360 加固(奇虎)", patterns: ["libjiagu.so", "libjiagu_art.so", "libjiagu_x86.so", "libprotectClass.so"] },
|
|
5
|
+
{ name: "腾讯乐固 / 御安全", patterns: ["libshell.so", "libshella", "libshellx", "libtup.so", "libtosprotection", "libBugly"] },
|
|
6
|
+
{ name: "梆梆加固", patterns: ["libsecexe.so", "libsecmain.so", "libDexHelper.so", "libSecShell.so", "libsecpreload.so"] },
|
|
7
|
+
{ name: "爱加密", patterns: ["libexec.so", "libexecmain.so", "ijiami.dat", "ijiami.ajm", "libSecShell"] },
|
|
8
|
+
{ name: "阿里聚安全 / 娜迦", patterns: ["libmobisec.so", "libpreverify1.so", "aliprotect.dat", "libsgmain.so", "libsgsecuritybody.so"] },
|
|
9
|
+
{ name: "百度加固", patterns: ["libbaiduprotect.so", "baiduprotect1.jar", "libbaiduprotect_x86.so"] },
|
|
10
|
+
{ name: "娜迦 NAGAIN", patterns: ["libddog.so", "libfdog.so", "libchaosvmp.so"] },
|
|
11
|
+
{ name: "网秦 / 通付盾", patterns: ["libnqshield.so", "libtup.so", "libnesec.so"] },
|
|
12
|
+
{ name: "几维安全 KiwiSecurity", patterns: ["libkwscmm.so", "libkwscr.so", "libkwslinker.so"] },
|
|
13
|
+
{ name: "顶象 Dingxiang", patterns: ["libx3g.so"] },
|
|
14
|
+
{ name: "珊瑚灵御 / 瑞星", patterns: ["librsprotect.so"] },
|
|
15
|
+
{ name: "APKProtect", patterns: ["libAPKProtect.so"] },
|
|
16
|
+
];
|
|
17
|
+
/** 扫描本地 APK,识别其是否被加固以及加固厂商。 */
|
|
18
|
+
export function detectPacker(apkPath) {
|
|
19
|
+
const zip = new AdmZip(apkPath);
|
|
20
|
+
const entries = zip.getEntries().map((e) => e.entryName);
|
|
21
|
+
const lower = entries.map((e) => e.toLowerCase());
|
|
22
|
+
const hits = [];
|
|
23
|
+
const packers = new Set();
|
|
24
|
+
for (const sig of SIGNATURES) {
|
|
25
|
+
for (const pat of sig.patterns) {
|
|
26
|
+
const p = pat.toLowerCase();
|
|
27
|
+
const idx = lower.findIndex((e) => e.includes(p));
|
|
28
|
+
if (idx !== -1) {
|
|
29
|
+
hits.push({ packer: sig.name, entry: entries[idx] });
|
|
30
|
+
packers.add(sig.name);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const dexCount = entries.filter((e) => /^classes\d*\.dex$/i.test(e)).length;
|
|
35
|
+
return {
|
|
36
|
+
packed: packers.size > 0,
|
|
37
|
+
packers: [...packers],
|
|
38
|
+
hits,
|
|
39
|
+
dexCount,
|
|
40
|
+
entryCount: entries.length,
|
|
41
|
+
};
|
|
42
|
+
}
|
package/dist/entry.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { exec, hasCommand } from "./util.js";
|
|
5
|
+
function toSmaliRel(className) {
|
|
6
|
+
return className.replace(/\./g, "/") + ".smali";
|
|
7
|
+
}
|
|
8
|
+
async function findSmaliFile(roots, className) {
|
|
9
|
+
const rel = toSmaliRel(className);
|
|
10
|
+
for (const root of roots) {
|
|
11
|
+
const candidates = [
|
|
12
|
+
join(root, rel),
|
|
13
|
+
join(root, "smali", rel),
|
|
14
|
+
join(root, "smali_classes2", rel),
|
|
15
|
+
join(root, "smali_classes3", rel),
|
|
16
|
+
join(root, "smali_classes4", rel),
|
|
17
|
+
join(root, "smali_classes5", rel),
|
|
18
|
+
];
|
|
19
|
+
for (const p of candidates) {
|
|
20
|
+
if (existsSync(p))
|
|
21
|
+
return p;
|
|
22
|
+
}
|
|
23
|
+
// scan smali* dirs under project
|
|
24
|
+
try {
|
|
25
|
+
const names = await readdir(root);
|
|
26
|
+
for (const n of names) {
|
|
27
|
+
if (!n.startsWith("smali"))
|
|
28
|
+
continue;
|
|
29
|
+
const p = join(root, n, rel);
|
|
30
|
+
if (existsSync(p))
|
|
31
|
+
return p;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
/* ignore */
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
async function locateOnCreate(smaliPath) {
|
|
41
|
+
const text = await readFile(smaliPath, "utf8");
|
|
42
|
+
const lines = text.split(/\r?\n/);
|
|
43
|
+
for (let i = 0; i < lines.length; i++) {
|
|
44
|
+
const m = lines[i].match(/^\s*\.method\s+.*\bonCreate\s*\(\s*Landroid\/os\/Bundle;\s*\)\s*V/);
|
|
45
|
+
if (m) {
|
|
46
|
+
return { line: i + 1, method: lines[i].trim() };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// fallback: any onCreate
|
|
50
|
+
for (let i = 0; i < lines.length; i++) {
|
|
51
|
+
if (/^\s*\.method\s+.*\bonCreate\s*\(/.test(lines[i])) {
|
|
52
|
+
return { line: i + 1, method: lines[i].trim() };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
/** 从 apktool 解码后的 AndroidManifest.xml 解析入口。 */
|
|
58
|
+
export function parseDecodedManifest(xml) {
|
|
59
|
+
const pkg = xml.match(/<manifest\b[^>]*\bpackage\s*=\s*"([^"]+)"/i)?.[1];
|
|
60
|
+
const appName = xml.match(/<application\b[^>]*\bandroid:name\s*=\s*"([^"]+)"/i)?.[1];
|
|
61
|
+
const launchers = new Set();
|
|
62
|
+
// Activity / activity-alias blocks that contain MAIN + LAUNCHER
|
|
63
|
+
const activityRe = /<(activity|activity-alias)\b([^>]*)>([\s\S]*?)<\/\1>/gi;
|
|
64
|
+
let m;
|
|
65
|
+
while ((m = activityRe.exec(xml))) {
|
|
66
|
+
const attrs = m[2];
|
|
67
|
+
const body = m[3];
|
|
68
|
+
const hasMain = /android:name\s*=\s*"android\.intent\.action\.MAIN"/i.test(body);
|
|
69
|
+
const hasLauncher = /android:name\s*=\s*"android\.intent\.category\.LAUNCHER"/i.test(body);
|
|
70
|
+
if (!hasMain || !hasLauncher)
|
|
71
|
+
continue;
|
|
72
|
+
const name = attrs.match(/\bandroid:name\s*=\s*"([^"]+)"/i)?.[1];
|
|
73
|
+
if (!name)
|
|
74
|
+
continue;
|
|
75
|
+
const target = attrs.match(/\bandroid:targetActivity\s*=\s*"([^"]+)"/i)?.[1];
|
|
76
|
+
const raw = target || name;
|
|
77
|
+
launchers.add(resolveClassName(pkg, raw));
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
packageName: pkg,
|
|
81
|
+
applicationName: appName ? resolveClassName(pkg, appName) : undefined,
|
|
82
|
+
launchers: [...launchers],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function resolveClassName(pkg, name) {
|
|
86
|
+
if (name.startsWith(".")) {
|
|
87
|
+
if (!pkg)
|
|
88
|
+
return name.slice(1);
|
|
89
|
+
return pkg + name;
|
|
90
|
+
}
|
|
91
|
+
if (!name.includes(".") && pkg)
|
|
92
|
+
return `${pkg}.${name}`;
|
|
93
|
+
return name;
|
|
94
|
+
}
|
|
95
|
+
async function collectSmaliRoots(projectDir, smaliDir) {
|
|
96
|
+
const roots = [];
|
|
97
|
+
if (smaliDir)
|
|
98
|
+
roots.push(resolve(smaliDir));
|
|
99
|
+
if (projectDir)
|
|
100
|
+
roots.push(resolve(projectDir));
|
|
101
|
+
return roots;
|
|
102
|
+
}
|
|
103
|
+
async function fromAapt(apkPath) {
|
|
104
|
+
const tools = ["aapt", "aapt2"];
|
|
105
|
+
for (const tool of tools) {
|
|
106
|
+
if (!(await hasCommand(tool)))
|
|
107
|
+
continue;
|
|
108
|
+
const r = await exec(tool, ["dump", "badging", apkPath], { timeoutMs: 60000 });
|
|
109
|
+
if (r.code !== 0)
|
|
110
|
+
continue;
|
|
111
|
+
const pkg = r.stdout.match(/package:\s*name='([^']+)'/)?.[1];
|
|
112
|
+
const launchers = [];
|
|
113
|
+
for (const line of r.stdout.split(/\r?\n/)) {
|
|
114
|
+
const m = line.match(/launchable-activity:\s*name='([^']+)'/);
|
|
115
|
+
if (m)
|
|
116
|
+
launchers.push(m[1]);
|
|
117
|
+
}
|
|
118
|
+
return { packageName: pkg, launchers, raw: r.stdout.slice(0, 2000) };
|
|
119
|
+
}
|
|
120
|
+
throw new Error("未找到 aapt/aapt2,且未提供已解码的 AndroidManifest.xml。请安装 Android build-tools,或先用 apk_decode。");
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 定位应用入口:包名、Application、MAIN/LAUNCHER Activity,以及对应 smali 的 onCreate 行号。
|
|
124
|
+
* 只做分析定位,不修改任何文件。
|
|
125
|
+
*/
|
|
126
|
+
export async function findAppEntry(opts) {
|
|
127
|
+
const notes = [];
|
|
128
|
+
const sources = [];
|
|
129
|
+
let packageName;
|
|
130
|
+
let applicationName;
|
|
131
|
+
let launcherNames = [];
|
|
132
|
+
const manifestPath = opts.manifestPath ||
|
|
133
|
+
(opts.projectDir ? join(opts.projectDir, "AndroidManifest.xml") : undefined);
|
|
134
|
+
if (manifestPath && existsSync(manifestPath)) {
|
|
135
|
+
const xml = await readFile(manifestPath, "utf8");
|
|
136
|
+
if (xml.includes("android:name") || xml.includes("<manifest")) {
|
|
137
|
+
const parsed = parseDecodedManifest(xml);
|
|
138
|
+
packageName = parsed.packageName;
|
|
139
|
+
applicationName = parsed.applicationName;
|
|
140
|
+
launcherNames = parsed.launchers;
|
|
141
|
+
sources.push(manifestPath);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
notes.push(`Manifest 可能是二进制 AXML,无法直接解析:${manifestPath}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (launcherNames.length === 0 && opts.apkPath) {
|
|
148
|
+
const st = await stat(opts.apkPath).catch(() => null);
|
|
149
|
+
if (!st?.isFile())
|
|
150
|
+
throw new Error(`APK 不存在:${opts.apkPath}`);
|
|
151
|
+
const aapt = await fromAapt(opts.apkPath);
|
|
152
|
+
packageName = packageName || aapt.packageName;
|
|
153
|
+
launcherNames = aapt.launchers;
|
|
154
|
+
sources.push(`aapt dump badging: ${opts.apkPath}`);
|
|
155
|
+
notes.push("入口来自 aapt badging;更完整信息请先 apk_decode 再传 projectDir。");
|
|
156
|
+
}
|
|
157
|
+
if (launcherNames.length === 0 && !applicationName) {
|
|
158
|
+
throw new Error("未能定位入口。请提供 apktool 解码目录 projectDir,或 apkPath(需本机 aapt/aapt2)。");
|
|
159
|
+
}
|
|
160
|
+
const roots = await collectSmaliRoots(opts.projectDir, opts.smaliDir);
|
|
161
|
+
const launchers = [];
|
|
162
|
+
for (const activity of launcherNames) {
|
|
163
|
+
const entry = { activity };
|
|
164
|
+
const smaliPath = await findSmaliFile(roots, activity);
|
|
165
|
+
if (smaliPath) {
|
|
166
|
+
entry.smaliPath = smaliPath;
|
|
167
|
+
const oc = await locateOnCreate(smaliPath);
|
|
168
|
+
if (oc) {
|
|
169
|
+
entry.onCreateLine = oc.line;
|
|
170
|
+
entry.onCreateMethod = oc.method;
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
notes.push(`${activity}: 找到 smali 但未定位 onCreate`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else if (roots.length) {
|
|
177
|
+
notes.push(`${activity}: 在 smali 目录中未找到对应文件`);
|
|
178
|
+
}
|
|
179
|
+
launchers.push(entry);
|
|
180
|
+
}
|
|
181
|
+
if (applicationName && roots.length) {
|
|
182
|
+
const appSmali = await findSmaliFile(roots, applicationName);
|
|
183
|
+
if (appSmali)
|
|
184
|
+
notes.push(`Application smali: ${appSmali}`);
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
packageName,
|
|
188
|
+
applicationName,
|
|
189
|
+
launchers,
|
|
190
|
+
sources,
|
|
191
|
+
notes,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
export function formatAppEntry(info) {
|
|
195
|
+
const lines = [];
|
|
196
|
+
lines.push(`package: ${info.packageName ?? "(unknown)"}`);
|
|
197
|
+
lines.push(`application: ${info.applicationName ?? "(default)"}`);
|
|
198
|
+
lines.push(`launcher 数量: ${info.launchers.length}`);
|
|
199
|
+
for (const [i, L] of info.launchers.entries()) {
|
|
200
|
+
lines.push(`--- launcher #${i + 1} ---`);
|
|
201
|
+
lines.push(`activity: ${L.activity}`);
|
|
202
|
+
if (L.smaliPath)
|
|
203
|
+
lines.push(`smali: ${L.smaliPath}`);
|
|
204
|
+
if (L.onCreateLine)
|
|
205
|
+
lines.push(`onCreate: 第 ${L.onCreateLine} 行`);
|
|
206
|
+
if (L.onCreateMethod)
|
|
207
|
+
lines.push(`method: ${L.onCreateMethod}`);
|
|
208
|
+
}
|
|
209
|
+
if (info.sources.length) {
|
|
210
|
+
lines.push("--- sources ---");
|
|
211
|
+
lines.push(...info.sources.map((s) => ` ${s}`));
|
|
212
|
+
}
|
|
213
|
+
if (info.notes.length) {
|
|
214
|
+
lines.push("--- notes ---");
|
|
215
|
+
lines.push(...info.notes.map((n) => ` ${n}`));
|
|
216
|
+
}
|
|
217
|
+
lines.push("说明: 本工具仅定位入口,不写入/注入代码。重新打包请用 apk_decode →(自行合法修改)→ apk_repack。");
|
|
218
|
+
return lines.join("\n");
|
|
219
|
+
}
|