fix(proguard): 支持 R8 内联行映射格式,修复 r8-map-id 堆栈帧无法反混淆

1. 解析器新增 R8 内联格式支持:start🔚returnType OrigClass.method(args):origStart:origEnd -> obf
   保存原始类名和方法名,而非只保存行号。
2. deobfuscateStack 正则从 [\w.]+ 改为 [^:)]+ 以匹配 r8-map-id-xxx 格式的 source 文件字段。
3. deobfuscateFrame 优先按行号范围查找 R8 内联条目,从中还原真实的类名、方法名和行号。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
徐勤民 2026-06-22 18:23:59 +08:00
父节点 5f7eb3ae13
当前提交 86ce76c7ed

查看文件

@ -1,11 +1,12 @@
/** /**
* Android ProGuard mapping.txt * Android ProGuard / R8 mapping.txt
* *
* mapping.txt * ProGuard
* com.example.MyClass -> a.b.c: * com.example.MyClass -> a.b.c:
* int myField -> a
* void myMethod(int) -> a * void myMethod(int) -> a
* 42:42:void otherMethod():100:100 -> b *
* R8 +
* 13:22:java.lang.Object com.example.Foo.bar():141:141 -> invokeSuspend
*/ */
export interface ProguardMapping { export interface ProguardMapping {
@ -23,13 +24,17 @@ export interface MethodMapping {
} }
export interface LineMapping { export interface LineMapping {
obfuscatedLineStart: number
obfuscatedLineEnd: number
originalLine: number originalLine: number
obfuscatedLine: number
originalFile: string originalFile: string
// R8 内联:原始类名和方法名(与 obfuscatedClass 可能不同)
originalClass?: string
originalMethod?: string
} }
/** /**
* ProGuard mapping.txt * ProGuard / R8 mapping.txt
*/ */
export function parseProguardMapping(content: string): Map<string, ProguardMapping> { export function parseProguardMapping(content: string): Map<string, ProguardMapping> {
const mappings = new Map<string, ProguardMapping>() const mappings = new Map<string, ProguardMapping>()
@ -37,14 +42,11 @@ export function parseProguardMapping(content: string): Map<string, ProguardMappi
let currentClass: ProguardMapping | null = null let currentClass: ProguardMapping | null = null
let currentOriginalClass = '' let currentOriginalClass = ''
let currentObfuscatedClass = '' let currentObfuscatedClass = ''
let lineNumber = 0
const lines = content.split('\n') const lines = content.split('\n')
for (const line of lines) { for (const line of lines) {
lineNumber++ // 跳过空行和注释(含 R8 的 # {...} 行)
// 跳过空行和注释
if (!line.trim() || line.startsWith('#')) continue if (!line.trim() || line.startsWith('#')) continue
// 类映射行com.example.MyClass -> a.b.c: // 类映射行com.example.MyClass -> a.b.c:
@ -65,20 +67,43 @@ export function parseProguardMapping(content: string): Map<string, ProguardMappi
if (!currentClass) continue if (!currentClass) continue
// 行号映射行42:42:void otherMethod():100:100 -> b // R8 内联行startLine:endLine:returnType OrigClass.method(args):origStart:origEnd -> obfuscatedMethod
// 示例: 13:22:java.lang.Object com.szyx.app.ywx.Foo$Bar.invokeSuspend(java.lang.Object):141:141 -> invokeSuspend
const r8InlineMatch = line.match(
/^\s+(\d+):(\d+):(\S+)\s+([\w$.]+)\.([\w$]+)\(([^)]*)\):(\d+):(\d+)\s+->\s+(\S+)$/
)
if (r8InlineMatch) {
const obfStart = parseInt(r8InlineMatch[1], 10)
const obfEnd = parseInt(r8InlineMatch[2], 10)
const originalClass = r8InlineMatch[4]
const originalMethod = r8InlineMatch[5]
const origLine = parseInt(r8InlineMatch[7], 10)
currentClass.lines.push({
obfuscatedLineStart: obfStart,
obfuscatedLineEnd: obfEnd,
originalLine: origLine,
originalFile: currentOriginalClass,
originalClass,
originalMethod,
})
continue
}
// ProGuard 行号映射无原始类信息startLine:endLine:desc:origStart:origEnd -> obfuscatedMethod
const lineMatch = line.match( const lineMatch = line.match(
/^\s+(\d+):(\d+):([^:]+):(\d+):(\d+)\s+->\s+(\S+)$/ /^\s+(\d+):(\d+):([^:]+):(\d+):(\d+)\s+->\s+(\S+)$/
) )
if (lineMatch) { if (lineMatch) {
currentClass.lines.push({ currentClass.lines.push({
obfuscatedLineStart: parseInt(lineMatch[1], 10),
obfuscatedLineEnd: parseInt(lineMatch[2], 10),
originalLine: parseInt(lineMatch[4], 10), originalLine: parseInt(lineMatch[4], 10),
obfuscatedLine: parseInt(lineMatch[1], 10),
originalFile: currentOriginalClass, originalFile: currentOriginalClass,
}) })
continue continue
} }
// 方法映射行void myMethod(int) -> a // 方法映射行:[startLine:endLine:]returnType methodName(args) -> obfuscatedName
const methodMatch = line.match( const methodMatch = line.match(
/^\s+(?:(\d+):(\d+):)?(\S+)\s+(\S+)\(([^)]*)\)\s+->\s+(\S+)$/ /^\s+(?:(\d+):(\d+):)?(\S+)\s+(\S+)\(([^)]*)\)\s+->\s+(\S+)$/
) )
@ -89,7 +114,6 @@ export function parseProguardMapping(content: string): Map<string, ProguardMappi
obfuscatedLine: methodMatch[1] ? parseInt(methodMatch[1], 10) : 0, obfuscatedLine: methodMatch[1] ? parseInt(methodMatch[1], 10) : 0,
originalLine: methodMatch[2] ? parseInt(methodMatch[2], 10) : 0, originalLine: methodMatch[2] ? parseInt(methodMatch[2], 10) : 0,
}) })
continue
} }
} }
@ -105,28 +129,50 @@ export function deobfuscateFrame(
obfuscatedMethod: string, obfuscatedMethod: string,
obfuscatedLine: number obfuscatedLine: number
): { class: string; method: string; line: number; file: string } | null { ): { class: string; method: string; line: number; file: string } | null {
// 查找类映射
const mapping = mappings.get(obfuscatedClass) const mapping = mappings.get(obfuscatedClass)
if (!mapping) return null if (!mapping) return null
// 查找方法映射 // 优先:在行号范围内查找 R8 内联条目
const methodMapping = mapping.methods.find(m => m.obfuscatedMethod === obfuscatedMethod) const r8Entry = mapping.lines.find(
l =>
// 查找行号映射 l.originalClass !== undefined &&
let originalLine = obfuscatedLine l.originalMethod === obfuscatedMethod &&
const lineMapping = mapping.lines.find(l => l.obfuscatedLine === obfuscatedLine) obfuscatedLine >= l.obfuscatedLineStart &&
if (lineMapping) { obfuscatedLine <= l.obfuscatedLineEnd
originalLine = lineMapping.originalLine )
} else if (methodMapping) { if (r8Entry && r8Entry.originalClass) {
// 使用方法的行号范围估算 const cls = r8Entry.originalClass
originalLine = methodMapping.originalLine return {
class: cls,
method: r8Entry.originalMethod!,
line: r8Entry.originalLine,
file: `${cls.split('.').pop()}.kt`,
}
} }
// 次选:行号范围条目(无内联类信息)
const lineEntry = mapping.lines.find(
l => obfuscatedLine >= l.obfuscatedLineStart && obfuscatedLine <= l.obfuscatedLineEnd
)
if (lineEntry) {
const methodEntry = mapping.methods.find(m => m.obfuscatedMethod === obfuscatedMethod)
const cls = mapping.originalClass
return {
class: cls,
method: methodEntry?.originalMethod || obfuscatedMethod,
line: lineEntry.originalLine,
file: `${cls.split('.').pop()}.kt`,
}
}
// 兜底:仅匹配方法名
const methodMapping = mapping.methods.find(m => m.obfuscatedMethod === obfuscatedMethod)
const cls = mapping.originalClass
return { return {
class: mapping.originalClass, class: cls,
method: methodMapping?.originalMethod || obfuscatedMethod, method: methodMapping?.originalMethod || obfuscatedMethod,
line: originalLine, line: methodMapping?.originalLine || obfuscatedLine,
file: `${mapping.originalClass.split('.').pop()}.java`, file: `${cls.split('.').pop()}.kt`,
} }
} }
@ -141,9 +187,10 @@ export function deobfuscateStack(
const result: string[] = [] const result: string[] = []
for (const line of lines) { for (const line of lines) {
// 匹配 "at com.example.a.b(MyFile.java:42)" 格式 // 匹配 "at ClassName.method(File:line)" 格式。
// File 部分允许连字符,以兼容 R8 的 r8-map-id-xxx 格式。
const match = line.match( const match = line.match(
/at\s+([\w.$]+)\.([\w$]+)\(([\w.]+):(\d+)\)/ /at\s+([\w.$]+)\.([\w$]+)\(([^:)]+):(\d+)\)/
) )
if (match) { if (match) {
@ -160,7 +207,7 @@ export function deobfuscateStack(
if (deobfuscated) { if (deobfuscated) {
result.push( result.push(
`at ${deobfuscated.class}.${deobfuscated.method}(${deobfuscated.file}:${deobfuscated.line})` `\tat ${deobfuscated.class}.${deobfuscated.method}(${deobfuscated.file}:${deobfuscated.line})`
) )
continue continue
} }