Dump Class!!

作者: 何晓杰Dev | 来源:发表于2019-01-21 11:15 被阅读547次

    写这篇的目的,是因为微信最近又开始搞事情了,先来看一个很简单的现象,反编译微信,拿到其中一个名为 com.tencent.mm.protocal.protobuf.bqu 的类,可以看到,反编译后的类描述是这样的:

    .field private vGA:Z
    .field public vGz:Ljava/lang/String;
    .method public constructor <init>()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<String>()
        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 += "<init>("
                            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
    <init>()
    

    很明显的,运行时的 bqu 和反编译出来的完全不同,那么真正的 bqu 类去哪了呢?

    这个时候就需要对 dump 出来的所有运行时类做类特征比对了。我们需要的类描述出来应该是这样的:

    *:boolean
    *:java.lang.String
    <init>()
    *(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('<init>(%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('<init>')) then begin
              list.Add(dumpConstructor(Strings[i]));
            end else begin
              if (not Strings[i].Contains('<clinit>')) 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
    <init>()
    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 := '<init>(';
          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
    <init>()
    

    bqy 除了变量,函数名称外,整体结构与反编译的 bqu 一致,再次把 bqu 粘到此处,方便肉眼比较:

    vGA:boolean
    vGz:java.lang.String
    <init>()
    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!!

        本文链接:https://www.haomeiwen.com/subject/xiwqjqtx.html