写这篇的目的,是因为微信最近又开始搞事情了,先来看一个很简单的现象,反编译微信,拿到其中一个名为 com.tencent.mm.protocal.protobuf.bqu
的类,可以看到,反编译后的类描述是这样的:
.field private vGA:Z
.field public vGz:Ljava/lang/String;
.method public constructor ()V
.method public final aft(Ljava/lang/String;)Lcom/tencent/mm/protocal/protobuf/bqu;
.method public final computeSize()I
.method public final synthetic parseFrom([B)Lcom/tencent/mm/bv/a;
.method public final populateBuilderWithField(Le/a/a/a/a;Lcom/tencent/mm/bv/a;I)Z
.method public final toByteArray()[B
.method public final toString()Ljava/lang/String;
.method public final bridge synthetic validate()Lcom/tencent/mm/bv/a;
.method public final writeFields(Le/a/a/c/a;)V
然而,当我写了 hook 去勾 aft
函数时,诡异的事情就发生了,因为根本就没有这个函数,勾子代码如下:
XposedHelper.findAndHookMethod(
"com.tencent.mm.protocal.protobuf.bqu",
classloader,
"aft", String::class.java,
object: XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam) {
Log.e(TAG, "str => ${param.args[0]}")
}
}
)
这样诡异的问题发生过不止一次,让我感觉是否在反编译时就已经出现了错误,或者有东西在误导反编译的过程,从而使得出来的结果不对。为此就必须要有微信运行时的类来作为参考,所以就得把微信所有的类全部 dump 出来。
当然了,写这个代码不难,可以直接把关键函数完成:
/**
* dump apk内的类定义,以子线程方式运行
* @param ctx
* @param apkPath 要dump的apk文件路径
* @param prefix 要 dump 的类前缀(与 packagename 不同,如被 dump 的 packagename 是 com.tencent.mm,但是前缀是 com.tencent,就可以 dump 到如 com.tencent.wcdb 等包内的内容)
* @param outputPath sd卡上的输出路径(如: output,则具体输出路径为 /sdcard/output)
* @param progress dump 过程回调,className 参数标识了当前 dump 的类
* @param complete dump 结束回调, succ 参数标识了 dump 是否成功
*/
fun dump(ctx: Context, apkPath: String, prefix: String, outputPath: String, progress:(className: String?) -> Unit, complete:(succ: Boolean) -> Unit) = thread {
if (isRunning) {
runOnMainThread { complete(false) }
return@thread
}
isRunning = true
val oat = ctx.getDir(outputPath, 0)
if (!oat.exists()) {
oat.mkdirs()
}
val loader = DexClassLoader(apkPath, oat.absolutePath, null, ClassLoader.getSystemClassLoader())
val list = mutableListOf()
val dex = DexFile(apkPath)
val en = dex.entries()
while (en.hasMoreElements()) {
val cn = en.nextElement()
if (cn.startsWith(prefix)) {
list.add(cn)
}
}
val basePath = File(Environment.getExternalStorageDirectory(), outputPath)
if (!basePath.exists()) {
basePath.mkdirs()
}
list.forEach {
if (isRunning) {
val fn = File(basePath, "$it.dump")
if (!fn.exists()) {
val clz = try { loader.loadClass(it) } catch (t: Throwable) { null }
if (clz != null) {
var str = ""
try { clz.declaredFields } catch (t: Throwable) { null }?.forEach { f -> str += "${f.name}:${f.type.name}\n" }
try { clz.declaredMethods } catch (t: Throwable) {null}?.forEach { m ->
str += "${m.name}("
m.parameterTypes?.forEach { p -> str += "${p.name}," }
str = str.trimEnd(',')
str += "):${m.returnType.name}\n"
}
try { clz.declaredConstructors } catch (t: Throwable) { null }?.forEach { c ->
str += "("
try { c.parameterTypes } catch (e: Throwable) { null }?.forEach { p -> str += "${p.name}," }
str = str.trimEnd(',')
str += ")\n"
}
fileWriteText(fn, str)
runOnMainThread { progress(it) }
}
}
}
}
isRunning = false
runOnMainThread { complete(true) }
}
由此函数拿到的 com.tencent.mm.protocal.protobuf.bqu
的类描述如下:
uAJ:java.lang.String
uRm:com.tencent.mm.protocal.protobuf.cdv
uum:com.tencent.mm.protocal.protobuf.bqx
vJv:int
vJw:int
op(int,[Ljava.lang.Object;):int
()
很明显的,运行时的 bqu
和反编译出来的完全不同,那么真正的 bqu
类去哪了呢?
这个时候就需要对 dump 出来的所有运行时类做类特征比对了。我们需要的类描述出来应该是这样的:
*:boolean
*:java.lang.String
()
*(java.lang.String):com.tencent.mm.protocal.protobuf.*
*():int
*([B):com.tencent.mm.bv.*
*(e.a.a.a.*,com.tencent.mm.bv.*,int):boolean
*():[B
*():java.lang.String
*():com.tencent.mm.bv.*
*(e.a.a.c.*):void
标为星号处是我们不能够确定的,因为类名在运行时改变了,反编译时并不能拿到真实的类名,所以需要做通配处理。
那么就简单的写点代码,先把 smali 代码转换成与之一致的描述:
function dumpClassName(str: string): string;
var
ret: string;
begin
ret := str.Substring(str.IndexOf('com/tencent/')).TrimRight([';']);
ret := ret.Replace('/', '.', [rfReplaceAll]);
Exit(ret);
end;
function dumpField(str: string): string;
var
sarr: TStringArray;
namearr: TStringArray;
fname: string;
ftype: string;
t: string;
begin
sarr := str.Split(':');
namearr := sarr[0].Split(' ');
fname:= namearr[Length(namearr) - 1];
if (sarr[1].Contains('=')) then begin
ftype:= sarr[1].Split('=')[0].Trim + ';';
end else begin
ftype:= sarr[1];
end;
if (ftype.StartsWith('[')) then begin
if (ftype.StartsWith('[L')) then begin
ftype:= ftype.Replace('/', '.', [rfReplaceAll]);
end else begin
// normal array, do nothing
end;
end else begin
if (ftype.StartsWith('L') and ftype.EndsWith(';')) then begin
ftype:= ftype.Substring(1, ftype.Length -2).Replace('/', '.', [rfReplaceAll]);
end else begin
t := btm.KeyData[ftype[1]];
if (t <> '') then ftype:= t;
end;
end;
Exit(Format('%s:%s', [fname, ftype]));
end;
function dumpParam(str: string): string;
var
i: Integer = 1;
c: Char;
c1: Char;
tc: Char;
t: string;
ret: string = '';
begin
if (str.Length = 0) then Exit('');
while (True) do begin
c := str[i];
if (c = '[') then begin
c1 := str[i + 1];
if (c1 = 'L') then begin
ret += '[';
Inc(i);
while (true) do begin
tc := str[i];
if (tc = ';') then begin
ret += tc;
Break;
end else begin
ret += tc;
end;
Inc(i);
end;
ret += ',';
Inc(i)
end else begin
ret += Format('[%s,', [c1]);
Inc(i, 2);
end;
end else begin
if (c = 'L') then begin
Inc(i);
while (True) do begin
tc := str[i];
if (tc = ';') then Break;
ret += tc;
Inc(i);
end;
ret += ',';
Inc(i);
end else begin
t := btm.KeyData[c];
ret += Format('%s,', [t]);
Inc(i);
end;
end;
if (i > str.Length) then Break;
end;
ret := ret.TrimRight([',']);
ret := ret.Replace('/', '.', [rfReplaceAll]);
Exit(ret);
end;
function dumpMethod(str: string): string;
var
sarr: TStringArray;
namearr: TStringArray;
mname: string;
mparam: string;
mret: string;
t: string;
begin
sarr := str.Split(['(', ')']);
namearr := sarr[0].Split(' ');
mname:= namearr[Length(namearr) - 1];
mparam:= dumpParam(sarr[1]);
mret := sarr[2];
if (mret.StartsWith('[')) then begin
if (mret.StartsWith('[L')) then begin
mret := mret.Replace('/', '.', [rfReplaceAll]);
end else begin
// do nothing
end;
end else begin
if (mret.StartsWith('L') and mret.EndsWith(';')) then begin
mret := mret.Substring(1, mret.Length - 2).Replace('/', '.', [rfReplaceAll]);
end else begin
t := btm.KeyData[mret[1]];
if (t <> '') then mret := t;
end;
end;
Exit(Format('%s(%s):%s', [mname, mparam, mret]));
end;
function dumpConstructor(str: string): string;
var
sarr: TStringArray;
mparam: string;
begin
sarr := str.Split(['(', ')']);
mparam:= dumpParam(sarr[1]);
Exit(Format('(%s)', [mparam]));
end;
procedure dumpFile(filePath: string; savePath: string);
var
fn: string = '';
i: Integer;
list: TStringList;
begin
WriteLn('dump file => ' + filePath);
list := TStringList.Create;
with TStringList.Create do begin
LoadFromFile(filePath);
for i := 0 to Count - 1 do begin
if (Strings[i].StartsWith('.class')) then begin
fn := savePath + dumpClassName(Strings[i]) + '.index';
Continue;
end;
if (Strings[i].StartsWith('.field')) then begin
list.Add(dumpField(Strings[i]));
end;
if (Strings[i].StartsWith('.method')) then begin
if (Strings[i].Contains('')) then begin
list.Add(dumpConstructor(Strings[i]));
end else begin
if (not Strings[i].Contains('')) then begin
list.Add(dumpMethod(Strings[i]));
end;
end;
end;
end;
Free;
end;
list.SaveToFile(fn);
list.Free;
end;
procedure findFile(basePath: string; savePath: string);
var
src: TSearchRec;
tmp: string;
begin
if (not basePath.EndsWith(DirectorySeparator)) then basePath += DirectorySeparator;
if (FindFirst(basePath + '*', faAnyFile, src) = 0) then begin
repeat
if (src.Name = '.') or (src.Name = '..') then Continue;
tmp := basePath + src.Name;
if (DirectoryExists(tmp)) then begin
findFile(tmp, savePath);
end else begin
if (tmp.EndsWith('.smali')) then
dumpFile(tmp, savePath);
end;
until FindNext(src) <> 0;
FindClose(src);
end;
end;
转换后的反编译的 bqu
类描述如下:
vGA:boolean
vGz:java.lang.String
()
aft(java.lang.String):com.tencent.mm.protocal.protobuf.bqu
computeSize():int
parseFrom([B):com.tencent.mm.bv.a
populateBuilderWithField(e.a.a.a.a,com.tencent.mm.bv.a,int):boolean
toByteArray():[B
toString():java.lang.String
validate():com.tencent.mm.bv.a
writeFields(e.a.a.c.a):void
形式上一致,就可以简单的用来与 dump 到的运行时类描述比较了,同样的写一段代码来搞定,当然此处就需要注意通配的问题:
function extractDesc(astr: string): string;
var
t: string = '';
sarr: TStringArray;
marr: TStringArray;
mret: string = '';
i: Integer;
begin
if (astr.Contains('(')) then begin
t := astr.Substring(astr.IndexOf('('));
t := t.TrimRight([';']);
if (t.Contains(':')) then begin
sarr := t.Split(':');
mret := sarr[1];
if (mret.Contains('.')) then begin
mret := mret.Substring(0, mret.LastIndexOf('.'));
end;
marr := sarr[0].Substring(1, sarr[0].Length - 2).Split(',');
for i := 0 to Length(marr) - 1 do begin
if (marr[i].Contains('.')) then marr[i] := marr[i].Substring(0, marr[i].LastIndexOf('.'));
end;
t := '(';
for i := 0 to Length(marr) - 1 do begin
t += Format('%s,', [marr[i]]);
end;
t := t.TrimRight([',']);
t += '):' + mret;
end else begin
marr := t.Substring(1, t.Length - 2).Split(',');
for i := 0 to Length(marr) - 1 do begin
if (marr[i].Contains('.')) then marr[i] := marr[i].Substring(0, marr[i].LastIndexOf('.'));
end;
t := '(';
for i := 0 to Length(marr) - 1 do begin
t += Format('%s,', [marr[i]]);
end;
t := t.TrimRight([',']);
t += ')';
end;
end else begin
t := astr.Substring(astr.IndexOf(':'));
t := t.TrimRight([';']);
if (t.Contains('.')) then begin
t := t.Substring(0, t.LastIndexOf('.'));
end;
end;
Exit(t);
end;
function isMatch(originPath: string; dumpPath: string): Boolean;
var
listOrigin: TStringList;
listDump: TStringList;
i: Integer;
idx: Integer;
begin
listOrigin := TStringList.Create;
listDump := TStringList.Create;
listOrigin.LoadFromFile(originPath);
listDump.LoadFromFile(dumpPath);
for i := 0 to listOrigin.Count - 1 do begin
listOrigin[i] := extractDesc(listOrigin[i]);
end;
for i := 0 to listDump.Count - 1 do begin
listDump[i] := extractDesc(listDump[i]);
end;
for i := listOrigin.Count - 1 downto 0 do begin
idx := listDump.IndexOf(listOrigin[i]);
if (idx <> -1) then begin
listOrigin.Delete(i);
listDump.Delete(idx);
end;
end;
Result := (listOrigin.Count = 0) and (listDump.Count = 0);
listDump.Free;
listOrigin.Free;
end;
经过查找,最终发现了符合要求的运行时类,即 com.tencent.mm.protocal.protobuf.bqy
,该类的描述为:
vJG:java.lang.String
vJH:boolean
afA(java.lang.String):com.tencent.mm.protocal.protobuf.bqy
computeSize():int
parseFrom([B):com.tencent.mm.bv.a
populateBuilderWithField(e.a.a.a.a,com.tencent.mm.bv.a,int):boolean
toByteArray():[B
toString():java.lang.String
validate():com.tencent.mm.bv.a
writeFields(e.a.a.c.a):void
()
bqy
除了变量,函数名称外,整体结构与反编译的 bqu
一致,再次把 bqu
粘到此处,方便肉眼比较:
vGA:boolean
vGz:java.lang.String
()
aft(java.lang.String):com.tencent.mm.protocal.protobuf.bqu
computeSize():int
parseFrom([B):com.tencent.mm.bv.a
populateBuilderWithField(e.a.a.a.a,com.tencent.mm.bv.a,int):boolean
toByteArray():[B
toString():java.lang.String
validate():com.tencent.mm.bv.a
writeFields(e.a.a.c.a):void
因此,我们真正应该勾的东西就是 bqy
,把 xposed 代码稍做修改即可:
XposedHelper.findAndHookMethod(
"com.tencent.mm.protocal.protobuf.bqy",
classloader,
"afA", String::class.java,
object: XC_MethodHook() {
override fun beforeHookedMethod(param: MethodHookParam) {
Log.e(TAG, "str => ${param.args[0]}")
}
}
)
到此为止,就一切正常了。
为了方便起见,Dump Class 的工具已经开源,可以在我的 github 上获取到这份源码(Click)。