1. 前言
这里解的是 https://www.52pojie.cn/thread-1684562-1-1.html 的一个android的 reverse me
使用工具:
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()) {
Map.Entry entry = (Map.Entry) it.next();
hashMap.put(entry.getValue(), entry.getKey());
StringBuilder sb = new StringBuilder();
char[] charArray = str.toCharArray();
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 = {};
forEachMap(ret, (k, v) => map[k] = v.toString());
console.log(JSON.stringify(map));
console.log("n3Dnc2Pl".split('').map(e => map[e] || e).join(''))
return ret;
};
}
使用这个key就可以成功
2.3 分析a.g的a方法
我本地的jadx反编译这个方法失败了,可能是设置不太对或者是电脑不太行,最终是使用一个在线的网站反编译的,这个网站看起来也是使用jadx(http://www.javadecompilers.com/apk/)
反编译成功之后,发现a方法case块比较多,由于控制流平坦化的干扰,只能先做一个大体的分析
a方法是需要返回一个map,搜索return,相应的代码只有一处 return hashMap;
接下来搜索hashMap,主要来源是
hashMap.put(C0008a.f7a.get(num10.intValue()), this.f6c[num10.intValue()]);
hashMap.putAll(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方法控制流平坦化
目前我知道的控制流平坦化反混淆两种方法是
- 打出执行路径,这个只能看一部分逻辑,但是相对简单
- 通过ast还原代码,耗时相对长
这里我先使用第一种方式尝试了一下,由于不能像js一样,很便捷的修改代码,所以使用frida发消息给python,然后python读取代码,打印对应的case块
a方法中有多层的控制流,不同控制流中switch的表达式还有简单的运算,所以这里解了最外层的控制流(结果证明这样足够分析)
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);
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);
send('printPath ' + useCase(jst))
jst.$dispose();
}
return ret;
};
}
python部分:
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')):
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方式
下面是打印的路径,关键的东西通过开头就分析的差不多了
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"), "");
linkedList = new LinkedList(C0008a.f7a);
this.f4a = linkedList;
num2 = Integer.valueOf(linkedList.size());
this.f5b = num2;
this.f6c = new String[num2.intValue()];
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);
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;
this.f6c[num8.intValue()] = (String) this.f4a.get(num9.intValue());
num5 = Integer.valueOf(num9.intValue() + 1);
num6 = Integer.valueOf(num8.intValue() + 1);
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 = [];
c = [];
function getMap(num) {
if (!init c) {
const str = "THINAPRDOX";
a = copy(a.a);
let strArr = c;
for (let i = 0; i < str.length; i++) {
c[num + i] = str[i];
a.remove(str[i]);
}
for (let i = str.length + num; i < 26; i++)
c[i] = a[i - str.length + num];
for (let i = 0; i < num; i++)
c[i] = a[i + (26 - str.length - num)]
}
const map = {};
for (let i = 0; i < c.length; i++) {
map[a.a[i]] = c[i]
}
Object.keys(map).forEach(key => {
map[key.toLowerCase()] = map[key].toLowerCase();
})
return map;
}
function check(str) {
const map = getMap();
let newMap = {};
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('成功...')
}
}