吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 4217|回复: 8
收起左侧

[Android 原创] android reserve me

[复制链接]
circle2 发表于 2022-10-9 01:17

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[i]);
            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

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<String>,内容是['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方法控制流平坦化

目前我知道的控制流平坦化反混淆两种方法是

  1. 打出执行路径,这个只能看一部分逻辑,但是相对简单
  2. 通过ast还原代码,耗时相对长

这里我先使用第一种方式尝试了一下,由于不能像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 < str.length; i++) {
      // 从入参num开始放
      c[num + i] = str[i];
      a.remove(str[i]); // 将"THINAPRDOX"里面的字符移除
    }
    // c [.. , 'T', 'H', .. , 'O', 'X', ...]
    // a [B,C,E,F,G,J,K,L,M,Q,S,U,V,W,Y,Z]

    for (let i = str.length + num; i < 26; i++)
      c[i] = a[i - str.length + num];
    // c [.. , 'T', 'H', .. , 'O', 'X', 'B', 'C', .., 'L', 'M']
    for (let i = 0; i < num; i++)
      c[i] = a[i + (26 - str.length - num)]
    // c [Q,S,U,V,W,Y,Z,T,H,I,N,A,P,R,D,O,X,B,C,E,F,G,J,K,L,M]
  }

  const map = {};
  // 挨个放入key, value
  for (let i = 0; i < c.length; i++) {
    // C0008a.f7a, 内容是['A', 'B', 'C', ...]
    map[a.a[i]] = c[i]
  }
  // 放入对应的小写key, value
  Object.keys(map).forEach(key => {
    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('成功...')
  }
}

免费评分

参与人数 8威望 +2 吾爱币 +107 热心值 +5 收起 理由
y231 + 1 用心讨论,共获提升!
Null666yyds + 1 + 1 用心讨论,共获提升!
gaosld + 1 + 1 热心回复!
qtfreet00 + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
allspark + 1 + 1 用心讨论,共获提升!
杨辣子 + 1 + 1 热心回复!
ljlVink + 1 我很赞同!
Tonyha7 + 1 用心讨论,共获提升!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

xixicoco 发表于 2022-10-10 01:33
不错的文章
GrowUpTang 发表于 2022-10-11 09:55
250075083 发表于 2022-10-16 15:33
人才啊。。。。。。。。。。。。。。。。。。
a2504028411 发表于 2022-10-18 16:33
学习到了,楼主牛逼
zhangsf123 发表于 2022-10-26 23:50
写的非常好。再接再厉。
xixicoco 发表于 2022-10-27 00:58
好的,加油吧
y231 发表于 2022-11-13 19:57
学习,支持了
debug_cat 发表于 2024-6-25 17:20
太强了,感谢分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-24 09:50

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表