circle2 发表于 2022-10-9 01:17

android reserve me

# 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,每次都会重新生成,所以我这里就没有继续去看它的生成规则了

``` java
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方法

``` java
public final String a(String str) {
    // ...
    g gVar = this.f15a;
    int intValue = ((Integer) objArr).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其实就可以解了

``` js
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 = v.toString());
      console.log(JSON.stringify(map));
      // 这里是那个id,由于每次会变,是需要更改的
      console.log("n3Dnc2Pl".split('').map(e => map || 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就可以成功



## 2.3 分析a.g的a方法

我本地的jadx反编译这个方法失败了,可能是设置不太对或者是电脑不太行,最终是使用一个在线的网站反编译的,这个网站看起来也是使用jadx(http://www.javadecompilers.com/apk/)
``` java
    /*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,主要来源是
``` java
hashMap.put(C0008a.f7a.get(num10.intValue()), this.f6c);
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方法里面初始化的,来源有两部分
``` java
// 第一部分
strArr = this.f6c;
strArr = str2;
// 第二部分
this.f6c = (String) this.f4a.get(num9.intValue());
```

hashMap的来源,f6c的继续分析,str2/f4a的继续分析,需要进一步处理这部分代码


## 2.4 应对a方法控制流平坦化

目前我知道的控制流平坦化反混淆两种方法是
1. 打出执行路径,这个只能看一部分逻辑,但是相对简单
2. 通过ast还原代码,耗时相对长

这里我先使用第一种方式尝试了一下,由于不能像js一样,很便捷的修改代码,所以使用frida发消息给python,然后python读取代码,打印对应的case块

a方法中有多层的控制流,不同控制流中switch的表达式还有简单的运算,所以这里解了最外层的控制流(结果证明这样足够分析)
``` java
      // 这里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启动和执行都会变慢,因为这个方法调用的地方太多了
``` js
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部分:
``` 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
                # 多个字符串时,第一个作为指令
                if command == 'printPath':
                  printPath(payload)
    script.on('message', on_message)
    // ...
   
if __name__ == '__main__':
    main()
```

代码如上,这里使用spawn的方式启动。a.g中a方法依靠成员变量c(f6c),f6c首次之后都是有值的,避免路径打印不全,使用spawn方式


下面是打印的路径,关键的东西通过开头就分析的差不多了
``` java
// 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)))
    .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;

// 这里其实是遍历"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 = 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 = 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 = (String) this.f4a.get(num9.intValue());
num5 = Integer.valueOf(num9.intValue() + 1);
num6 = Integer.valueOf(num8.intValue() + 1);

// ...
// -------------------------------------------------------------------
// ...


// hashMap看起来就比较简单了,将两个对应的放在map中
// C0008a.f7aA,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);
i11 = num10.intValue() + 1;
i12 = i11;
num10 = Integer.valueOf(i12);
```

# 3. 分析结果

分析到这里,整个流程差不多清楚了,这里放点js伪代码
``` 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 = str;
      a.remove(str); // 将"THINAPRDOX"里面的字符移除
    }
    // c [.. , 'T', 'H', .. , 'O', 'X', ...]
    // a

    for (let i = str.length + num; i < 26; i++)
      c = a;
    // c [.. , 'T', 'H', .. , 'O', 'X', 'B', 'C', .., 'L', 'M']
    for (let i = 0; i < num; i++)
      c = a
    // c
}

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

function check(str) {
const map = getMap();
let newMap = {};
// key和value反过来
Object.keys(map).forEach(key => {
    const value = map
    newMap = key;
})
return str.split('').forEach(e => {
    return map || e;
}).join('');
}

function onClick() {
// ...
const isEqual = check(editText().getString()) === this.id;
if (isEqual) {
    textView.setText('成功...')
}
}

```

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

太强了,感谢分享
页: [1]
查看完整版本: android reserve me