Dump Class!!

写这篇的目的,是因为微信最近又开始搞事情了,先来看一个很简单的现象,反编译微信,拿到其中一个名为 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)。

你可能感兴趣的:(Dump Class!!)