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()) {
// 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就可以成功
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方法控制流平坦化
目前我知道的控制流平坦化反混淆两种方法是
- 打出执行路径,这个只能看一部分逻辑,但是相对简单
- 通过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('成功...')
}
}