android reserve me

查看 78|回复 8
作者:circle2   
1. 前言
这里解的是 https://www.52pojie.cn/thread-1684562-1-1.html 的一个android的 reverse me
使用工具:
  • jadx
  • frida

    2. 分析过程
    apk没有加固,可以直接使用jadx打开
    2.1 MainAcitivity部分
    很容易可以找到com.th7.selfprotectiontest.MainAcitivity中的onClick()方法
    之前搞过一个js的控制流平坦化,没想到现在java也有,可能是混淆之前的代码量不是很大,这里面很多case块只是一个单纯的跳转,所以分析起来能稍微容易一点
    这里是onClick中的用来判断的一块代码,取了EditText中的内容,经过a运算得到结果与this.f16b进行比较,this.f16b就是页面中展示的id,每次都会重新生成,所以我这里就没有继续去看它的生成规则了
    String obj = ((EditText) findViewById((2132522749 & (i3 ^ (-1))) | ((-2132522750) & i3))).getText().toString();
    // ...
    boolean equals = Objects.equals(a(obj), this.f16b);
    接下来是MainActivity中的a方法,a方法比较短,可以直接挑出重点看(这里并不是原本的代码,是我简单提取了一下里面的逻辑)
    a2是通过gVar.a()方法获取了一个HashMap,下面的hashMap与a2中的key/value倒过来了,然后根据hashMap,将入参str映射为另一个str
    这里的gVar是this.f15a,是a.g的一个实例,接下来分析a.g中的a方法
    public final String a(String str) {
        // ...
        g gVar = this.f15a;
        int intValue = ((Integer) objArr[0]).intValue();
        Map a2 = gVar.a(Integer.valueOf((intValue & (-3894259)) | ((intValue ^ (-1)) & 3894258)));
        HashMap hashMap = new HashMap();
        Iterator it = ((HashMap) a2).entrySet().iterator();
        while (it.hasNext()) {
            // a2中的key, value复制到hashMap中,key作为value,value作为key
            Map.Entry entry = (Map.Entry) it.next();
            hashMap.put(entry.getValue(), entry.getKey());
            // ...
            StringBuilder sb = new StringBuilder();
            char[] charArray = str.toCharArray();
            // 遍历charArray
            for () {
                String ch = Character.toString(charArray);
                boolean containsKey = hashMap.containsKey(ch);
                if (containsKey) {
                    ch = (String) hashMap.get(ch);
                    sb.append(ch);
                }
            }
        }
    }
    2.2 通过map获取正确的key
    这里验证一下之前的看的逻辑,拿到a方法返回的map,然后获取成功的key,这里的map是启动不变的,所以有这个map其实就可以解了
    const forEachMap = (map, fn) => {
        if (!map)
            return;
        const it = map.keySet().iterator();
        while (it.hasNext()) {
            const key = it.next().toString();
            const value = map.get(key);
            fn(key, value);
        }
    }
    Java.perform(() => {
        let g = Java.use("a.g");
        g.a.implementation = function(param){
            console.log('a is called', param);
            let ret = this.a(param);
            console.log('a ret value is ');
            let map = {};
            // 由于要找相应的key,所以我们就直接使用原本的map即可
            forEachMap(ret, (k, v) => map[k] = v.toString());
            console.log(JSON.stringify(map));
            // 这里是那个id,由于每次会变,是需要更改的
            console.log("n3Dnc2Pl".split('').map(e => map[e] || e).join(''))
            return ret;
        };
    }
    // {"A":"Q","B":"S","C":"U","D":"V","E":"W","F":"Y","G":"Z","H":"T","I":"H","J":"I","K":"N","L":"A","M":"P","N":"R","O":"D","P":"O","Q":"X","R":"B","S":"C","T":"E","U":"F","V":"G","W":"J","X":"K","Y":"L","Z":"M","a":"q","b":"s","c":"u","d":"v","e":"w","f":"y","g":"z","h":"t","i":"h","j":"i","k":"n","l":"a","m":"p","n":"r","o":"d","p":"o","q":"x","r":"b","s":"c","t":"e","u":"f","v":"g","w":"j","x":"k","y":"l","z":"m"}
    // r3Vru2H
    使用这个key就可以成功


    IMG_20221008_233545.jpg (72.4 KB, 下载次数: 1)
    下载附件
    2022-10-8 23:37 上传

    2.3 分析a.g的a方法
    我本地的jadx反编译这个方法失败了,可能是设置不太对或者是电脑不太行,最终是使用一个在线的网站反编译的,这个网站看起来也是使用jadx(http://www.javadecompilers.com/apk/)
        /*  JADX ERROR: JadxRuntimeException in pass: BlockProcessor
            jadx.core.utils.exceptions.JadxRuntimeException: CFG modification limit reached, blocks count: 527
                    at jadx.core.dex.visitors.blocks.BlockProcessor.processBlocksTree(BlockProcessor.java:70)
                    at jadx.core.dex.visitors.blocks.BlockProcessor.visit(BlockProcessor.java:44)
            */
    反编译成功之后,发现a方法case块比较多,由于控制流平坦化的干扰,只能先做一个大体的分析
    a方法是需要返回一个map,搜索return,相应的代码只有一处 return hashMap;
    接下来搜索hashMap,主要来源是
    hashMap.put(C0008a.f7a.get(num10.intValue()), this.f6c[num10.intValue()]);
    hashMap.putAll(hashMap2);
    // hashMap2的主要来源是
    hashMap2.put(((String) entry.getKey()).toLowerCase(), ((String) entry.getValue()).toLowerCase());
    继续搜索这个entry,会发现它还是来自hashMap,猜测逻辑是hashMap先通过f7a和f6c放置了大写的key和value,然后又挨个给它们放了对应的小写key和value
    这个C0008a.f7a是一个List,内容是['A', 'B', ... 'Z']
    另外一个关键点是f6c,f6c也是在a方法里面初始化的,来源有两部分
    // 第一部分
    strArr = this.f6c;
    strArr[num3.intValue() + i3] = str2;
    // 第二部分
    this.f6c[num8.intValue()] = (String) this.f4a.get(num9.intValue());
    hashMap的来源,f6c的继续分析,str2/f4a的继续分析,需要进一步处理这部分代码
    2.4 应对a方法控制流平坦化
    目前我知道的控制流平坦化反混淆两种方法是
    [ol]
  • 打出执行路径,这个只能看一部分逻辑,但是相对简单
  • 通过ast还原代码,耗时相对长
    [/ol]
    这里我先使用第一种方式尝试了一下,由于不能像js一样,很便捷的修改代码,所以使用frida发消息给python,然后python读取代码,打印对应的case块
    a方法中有多层的控制流,不同控制流中switch的表达式还有简单的运算,所以这里解了最外层的控制流(结果证明这样足够分析)
            // 这里C0007h是jadx起的名,其实就是h类
            String decode = C0007h.decode("AAE9A3F295D18CEEECD4ABD8B1D492C894ABAAE8A3F295E18CD7ECD9ABD0B1E992F59497AAD6A3F395EB8CEAECE9ABE4B1EA92F79491AAD4A3C295D28CD9ECE9ABE0B1D892CB9493AAEBA3CD95E88CD5ECE6ABDAB1E992FF94A1AAE9A3CD95EC8CE9ECE7ABDDB1E592FC949FAAE9A3F395ED8CE4ECEAABDAB1E592C89490AAD4A3C695EB8CD8ECD3ABE4B1D192CF94AFAAE7A3CF95E18CD6ECE9ABD3B1D692FB94A1AAE8A3F295DF8CD3ECE9ABD9B1EC92FC94AEAADBA3CA95E58CD0ECD3ABDDB1E292FF9495AAEAA3C895D18CE8ECE9ABD8B1D292FB94A1AAD5A3CA95DF8CD5ECE9ABE0B1EB92F094A8AAE7A3CD95E18CD7ECD0ABE7B1E992CD9490AAD4");
            while (true) {
                switch ((((((((((((((((((((((((((((((((decode.hashCode() ^ 879) ^ 363) ^ 16) ^ 947) ^ 124) ^ 167) ^ 633) ^ 678) ^ 928) ^ 659) ^ 899) ^ 494) ^ 416) ^ 578) ^ 734) ^ 878) ^ 284) ^ 93) ^ 684) ^ 12) ^ 481) ^ 1021) ^ 514) ^ 1009) ^ 93) ^ 138) ^ 952) ^ 696) ^ 280) ^ 574) ^ 467) ^ -1403821182) {
                case -2110089948:
                    num2 = Integer.valueOf(linkedList.size());
                    decode = C0007h.decode("AAEFA3F095DE8CD7ECD3ABEFB1E692F194AFAAE6A3F095D58CD3ECE9ABDEB1EA92FF9493AAEAA3CF95D18CE7ECEBABE2B1EF92CF9490AAE8A3F295D18CEAECD0ABEFB1D492CF9495AAD1A3F195ED8CD7ECE9ABE1B1E692CD9492AAE6A3F095D08CEDECEEABD9B1D592CB94AFAAD5A3CB95D18CD3ECEDABEEB1D692F39495AAD8A3F295D18CD4ECD1ABD0B1E892F194AFAAEFA3CB95EE8CD0ECD1ABE7B1E692F794AFAAE8A3CF95ED8CD6ECE7ABE3B1EB92C89497AAD7A3FC95ED8CD6ECE6ABD9");
                    break;
                case -2047903007:
                    return hashMap;
    frida部分,这个apk里面都是通过a.h类中的decode方法计算的下一个case块,加上这个hook之后apk启动和执行都会变慢,因为这个方法调用的地方太多了
    Java.perform(() => {
        const useCase = (decode) => ((((((((((((((((((((((((((((((((decode.hashCode() ^ 879) ^ 363) ^ 16) ^ 947) ^ 124) ^ 167) ^ 633) ^ 678) ^ 928) ^ 659) ^ 899) ^ 494) ^ 416) ^ 578) ^ 734) ^ 878) ^ 284) ^ 93) ^ 684) ^ 12) ^ 481) ^ 1021) ^ 514) ^ 1009) ^ 93) ^ 138) ^ 952) ^ 696) ^ 280) ^ 574) ^ 467) ^ -1403821182);
        const String = Java.use('java.lang.String');
        const androidLog = Java.use('android.util.Log');
        const Exception = Java.use('java.lang.Exception');
        let h = Java.use("a.h");
        h.decode.implementation = function(str){
            let ret = this.decode(str);
            // 通过caller中是否包含a.g.a来过滤其他调用decode的地方
            const caller = 'a.g.a';
            const e = Exception.$new();
            const isCaller = androidLog.getStackTraceString(e).indexOf(caller) > -1;
            e.$dispose();
            if (isCaller) {
                const jst = String.$new(ret);
                // 这里通知python端计算的结果
                // useCase是最外层的switch的表达式,这里直接进行计算
                // python就可以直接拿着结果去匹配了
                send('printPath ' + useCase(jst))
                jst.$dispose();
            }
            // console.log(useCase(ret));
            return ret;
        };
    }
    python部分:
    # 将a.java放在了同目录下,先将源码取出
    with open('./a.java', 'r', encoding='utf-8') as f:
        source_code = f.readlines()
    def printPath(num):
        inCase = False;
        for line in source_code:
            # 这里匹配的时候包括前面的缩进也匹配了,这样就可以只匹配最外层的控制流了
            if '            case ' + num + ':\n' == line:
                inCase = True
            elif inCase and '                break;\n' == line:
                inCase = False
            elif inCase and not line.strip().startswith(('case', 'decode', 'String decode')):
                # 将匹配到的case块打出来,也可以直接输入到另一个文件中
                # 另外排除case,decode开头的这些干扰代码
                print(line.strip())
    def main():
        // 这里略去一些python启动frida的一些代码
        // ...
        def on_message(message, data):
            type = message['type']
            if type == 'send':
                payload = message['payload'].split(' ')
                # 自己定义的一些格式,只有一个字符串的时候直接打印
                if len(payload) == 1:
                    print(" {0}".format(message['payload']))
                else:
                    command = payload[0]
                    # 多个字符串时,第一个作为指令
                    if command == 'printPath':
                        printPath(payload[1])
        script.on('message', on_message)
        // ...
    if __name__ == '__main__':
        main()
    代码如上,这里使用spawn的方式启动。a.g中a方法依靠成员变量c(f6c),f6c首次之后都是有值的,避免路径打印不全,使用spawn方式
    下面是打印的路径,关键的东西通过开头就分析的差不多了
    // C0007h.decode("05384F030028072E4510141732")的值是tH7iNaParadoX
    stream = C0007h.decode("05384F030028072E4510141732").codePoints().distinct().boxed();
    fVar = C0005f.f3a;
    cVar = C0002c.f0a;
    dVar = C0003d.f1a;
    eVar = C0004e.f2a;
    i = 0;
    str = ((String) stream.collect(Collector.of(fVar,cVar,dVar,eVar,new Collector.Characteristics[0])))
        .toUpperCase()
        .replaceAll(C0007h.decode("2A2E39471414"), "");
        // C0007h.decode("2A2E39471414")的值是[^A-Z]
    // 通过上面这一系列操作,得到tH7iNaParadoX的uppcase中的所有大写字母,即
    // THINAPRDOX
    linkedList = new LinkedList(C0008a.f7a);
    this.f4a = linkedList;
    num2 = Integer.valueOf(linkedList.size());
    this.f5b = num2;
    this.f6c = new String[num2.intValue()];
    // 这里其实是遍历"THINAPRDOX",然后挨个放入strArr中,最终的strArr是
    // [ "T", "H", "I", "N", "A", "P", "R", "D", "O", "X" ]
    i2 = 0;
    i5 = i2;
    num3 = Integer.valueOf(i5);
    str2 = str.substring(num3.intValue(), num3.intValue() + 1);
    strArr = this.f6c;
    i3 = num.intValue();
    strArr[num3.intValue() + i3] = str2;
    this.f4a.remove(str2);
    // 又一遍循环
    i4 = num3.intValue() + 1;
    i5 = i4;
    num3 = Integer.valueOf(i5);
    str2 = str.substring(num3.intValue(), num3.intValue() + 1);
    strArr = this.f6c;
    i3 = num.intValue();
    strArr[num3.intValue() + i3] = str2;
    this.f4a.remove(str2);
    // ...
    // --------------------------------------------------------------------
    // ...
    // 搜索到了f6c的代码块(这中间又一个较大的子控制流,但是看起来没执行什么有用的代码,就删掉了)
    i4 = num3.intValue() + 1;
    i5 = i4;
    num3 = Integer.valueOf(i5);
    i6 = num.intValue();
    num4 = Integer.valueOf(str.length() + i6);
    i7 = 0;
    num9 = i7;
    num8 = num4;
    // 这里相当与是f4a中取值,然后挨个放入f6c中,但是稍微有点小逻辑,一会伪代码中体现
    this.f6c[num8.intValue()] = (String) this.f4a.get(num9.intValue());
    num5 = Integer.valueOf(num9.intValue() + 1);
    num6 = Integer.valueOf(num8.intValue() + 1);
    // ...
    // -------------------------------------------------------------------
    // ...
    // hashMap看起来就比较简单了,将两个对应的放在map中
    // C0008a.f7a  A,B,C,D,E,F,G,H,I, ...
    // this.f6c    Q,S,U,V,W,Y,Z,T,H, ...
    hashMap = new HashMap();
    i12 = i;
    num10 = Integer.valueOf(i12);
    hashMap.put(C0008a.f7a.get(num10.intValue()), this.f6c[num10.intValue()]);
    i11 = num10.intValue() + 1;
    i12 = i11;
    num10 = Integer.valueOf(i12);
    3. 分析结果
    分析到这里,整个流程差不多清楚了,这里放点js伪代码
    // 这里对应a.g中的a方法
    a = []; //f4a
    c = [];
    function getMap(num) { // num是其实是固定传了一个7
      if (!init c) {
        const str = "THINAPRDOX";
        // 这里是f4a
        a = copy(a.a); // C0008a.f7a, 内容是['A', 'B', 'C', ...]
        let strArr = c;
        for (let i = 0; i  {
        map[key.toLowerCase()] = map[key].toLowerCase();
      })
      return map;
    }
    function check(str) {
      const map = getMap();
      let newMap = {};
      // key和value反过来
      Object.keys(map).forEach(key => {
        const value = map[key]
        newMap[value] = key;
      })
      return str.split('').forEach(e => {
        return map[e] || e;
      }).join('');
    }
    function onClick() {
      // ...
      const isEqual = check(editText().getString()) === this.id;
      if (isEqual) {
        textView.setText('成功...')
      }
    }

    方法, 代码

  • xixicoco   

    不错的文章
    GrowUpTang   

    不错,多分享
    250075083   

    人才啊。。。。。。。。。。。。。。。。。。
    a2504028411   

    学习到了,楼主牛逼
    zhangsf123   

    写的非常好。再接再厉。
    xixicoco   

    好的,加油吧
    y231   

    学习,支持了
    debug_cat   

    太强了,感谢分享
    您需要登录后才可以回帖 登录 | 立即注册

    返回顶部