好友
阅读权限 25
听众
最后登录 1970-1-1
L剑仙
发表于 2019-11-29 21:38
菜鸟年中的时候无聊练手的app,当时遇到点问题又太忙了就放一边去了,最近有空捡起来接着分析下,论坛记录下以免忘记 {:1_918:} {:1_918:}
一、首先,我们知道每次点击动态密码刷新就会刷新动态密码 , 于是 , 我们点击 , 并打开 ddms 通过方法回溯功能 定位到函数 onOtpRefreshClick()用 jadx反编译mkey.apk,勾选反混淆,另存为gradle在as中打开依次查看onOtpRefreshClick()的子函数 依次观察 onOtpRefreshClick子函数
[Java] 纯文本查看 复制代码
public void onOtpRefreshClick() {
if (OtpLib.m14035a(this.f10441j.longValue())) {
C4221a.m15532a(this.mOtpRefreshIconView, 2, 1000);
m14663g();
getActivity().startService(new Intent(getActivity(), NotificationToolService.class));
getActivity().startService(new Intent(getActivity(), OtpWidgetUpdateService.class));
m14649a(OtpLib.m14037b(this.f10441j.longValue(), this.f10438g, this.f10439h), true);
return;
}
if (!mo10514a()) {
mo10513a("动态密码冷却中...");
}
C4221a.m15533b(this.mOtpRefreshHintView);
}
public void m14649a(final String str, boolean z) {
if (!z) {
this.mOtpDigit0.setText(str.substring(0, 1));
this.mOtpDigit1.setText(str.substring(1, 2));
this.mOtpDigit2.setText(str.substring(2, 3));
this.mOtpDigit3.setText(str.substring(3, 4));
this.mOtpDigit4.setText(str.substring(4, 5));
this.mOtpDigit5.setText(str.substring(5, 6));
return;
}
容易判断出OtpLib.m14037b(this.f10441j.longValue(),this.f10438g, this.f10439h) 函数 m14037b 为生成 6 位将军令,函数 m14649a生成了6位并显示在界面上
hook m14037b 函数并打印参数返回值
[JavaScript] 纯文本查看 复制代码
var otplib = Java.use('com.netease.mkey.core.OtpLib');
otplib.b.overload("long","java.lang.String","java.lang.String").implementation=function(j,str,str2)
{ send('hook b start');
var e=otplib.e(j);
var str3=this.b(j,str,str2);
send("e:"+e+" str:"+str+" str2:"+str2+" rtn:"+str3);
return this.b(j,str,str2)
}
其中相关参数我用x打码
message:{'type': 'send', 'payload': 'e:1574052480 str:8xx5xx4xxx str2:x2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx rtn:804671'} data: None
message:{'type': 'send', 'payload': 'hook b start'} data: None message:{'type': 'send', 'payload': 'e:1574052510 str:8xx5xx4xxx str2:x2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx rtn:120671'} data: None message:{'type': 'send', 'payload': 'hook b start'} data: None message:{'type': 'send', 'payload': 'e:1574052540 str:8xx5xx4xxx str2:x2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx rtn:436634'} data: None message:{'type': 'send', 'payload': 'hook b start'} data: None message:{'type': 'send', 'payload': 'e:1574052570 str:8xx5xx4xxx str2:x2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx rtn:853306'} data: None
可以确定, e 是随时间变化的,每次增加30,正好otp每30s更新一次 ,而 str 是序列号, str2 是一个固定的 16 进制字符串,猜想这2个参数每次激活将军令后都是不变的
二、1.下面我们跟踪 3 个参数的来源, 3 个参数最初在 publicsynchronized void m14664h() 函数中生成, mo10518d() 是 EkeyDb 的一个实例
this.f10441j= mo10518d().mo10123h();
this.f10438g= mo10518d().mo10126i();
this.f10439h= mo10518d().mo10130j();
而函数 m14037b 还有一个隐藏变量 f9848a 是在函数 publicstatic void m14034a(EkeyDb ekeyDb) 生成的 f9848a= f9853f.mo10109e(); f9853f 也是EkeyDb 的一个实例
直接调用EkeyDb 的实例分别获得mo10123h() mo10126i() mo10130j() mo10109e() 返回值
[JavaScript] 纯文本查看 复制代码
Java.choose("com.netease.mkey.core.EkeyDb", {
onMatch: function (instance) {
console.log("Found instance: " + instance);
console.log("Result of f10441j func: " + instance.h());
console.log("Result of f10438g func: " + instance.i());
console.log("Result of f10439h func: " + instance.j());
console.log("Result of f9848a func: " + instance.e());
},
onComplete: function () { }
Foundinstance: com.netease.mkey.core.EkeyDb@c3c2927 Resultof f10441j func: 0 Resultof f10438g func: 8xx5xx4xxx Resultof f10439h func: x2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Resultof f9848a func: 1574052493
可以确定3个参数是通过EkeyDb的实例生成的
二、2.下面跟踪 mo10123h () mo10126i() mo10130j() mo10109e()4 个函数,发现这4 个函数都是数据库查询函数,其中最重要的一句就是数据库查询,以mo10109e() 为例,这句代码位 Cursorquery = this.f9841b.query(true, "config", newString[]{"key", "value"}, "key=?", newString[]{this.f9842d.mo10260V()}, null, null, null, null);
从表config 中提取key=this.f9842d.mo10260V() 的value 值,而f9842d 为C3830e 的实例,通过privateC3830e f9842d = new C3830e(m13872an())生成key的值
这4个参数key对应生成方法分别为mo10123h :mo10268d() mo10126i: mo10266b() mo10130j: mo10267c()mo10109e: mo10260V()
hookcom.netease.mkey.core.e 类实例的4 个方法[JavaScript] 纯文本查看 复制代码
Java.choose("com.netease.mkey.core.e", {
onMatch: function (instance) {
console.log("Found instance: " + instance);
console.log("mo10123h :mo10268d() " + instance.d());
console.log("mo10126i: mo10266b() " + instance.b());
console.log("mo10130j: mo10267c()" + instance.c());
console.log("mo10109e: mo10260V()" + instance.c());
},
onComplete: function () { }
});
返回值为
找到这4 个key ,寻找数据库中对应的value
adbpull 出数据库目录/data/data/com.netease.mkey/databases/ekey
很容易找到 3 个函数产生的 key 对应的 value,我们一看这个value明显base64编码的,猜想后面肯定要用base64解码
二、3.1这些 key 和 value 对应的数据库如何生成呢,先分析 mo10268d ,他在 mo10107d 函数中通过 contentvalue 将参数 j update 进入数据库 , mo10107d在这句话中调用 ActivationActivity.this.f9569d.mo10107d(bundle.getLong(C3806c.m14238j())); hook得到 m14238j为https://service.mkey.163.com/WSszq1twyG/api/v3/claim_ekey_sn
我们再看mo10260V() 在mo10078a(longj) 函数中通过 contentvalue 将参数 j update 进入数据库 ,而参数 j 为 f9848a ,下面我们分析 f9848a 如何生成 f9848a 首先通过 mo10109e 从数据库中查询生成初始值,然后通过m14035a(longj) 函数更新,在函数onOtpRefreshClick 中通过这句代码OtpLib.m14035a(this.f10441j.longValue()) ,简单观察我们可以猜想,f9848a 存储的是上一次的时间戳 e ,保证当前 e > 上次运行存储的 e ,防止时间倒流{:1_909:} ,而我们每次点击刷新触发onOtpRefreshClick ,都会将e 提前到下一个30s 的e ,otp 也将是下一个,但我们不能无限制刷新,还记得将军令有个冷却吗,就是在这个函数中实现的,刷新时间必须小于f9851d ,根据经验大概最多同时出现4 个otp ,我们就不hook 寻找他的值了[JavaScript] 纯文本查看 复制代码
public static boolean m14035a(long j) {
if (f9851d <= 0) {
return false;
}
long e =System.currentTimeMillis() / 1000) - j;
if (f9848a <= e) {
f9848a = ((long) 30) + e;
f9853f.mo10078a(f9848a);
f9849b = e;
f9853f.mo10100c(f9849b);
C3844h.m14455a("refresh success 1");
return true;
} else if ((f9848a / ((long) 30)) - (e / ((long) 30)) < ((long) f9851d)) {
f9848a += (long) 30;
f9853f.mo10078a(f9848a);
f9849b = e;
f9853f.mo10100c(f9849b);
C3844h.m14455a("refresh success 2");
return true;
} else {
C3844h.m14455a("refresh fail");
return false;
}
}
二、3.2参数str 就是你的序列号,他是怎么生成的呢,我们一层一层跟进 m13851E, m13877b, m14451b, m14453c [JavaScript] 纯文本查看 复制代码
private String m13851E(String str) {
return m13877b(str, m13873ao());
}
private String m13877b(String str, byte[] bArr) {
if (str == null) {
return null;
}
byte[] b = C3843g.m14451b(bArr, Base64.decode(str.getBytes(), 2));
if (b != null) {
return new String(b);
}
return null;
}
public static byte[] m14451b(byte[] bArr, byte[] bArr2) {
return m14453c(bArr, bArr2, null);
}
看到这里就明白了, bArr是密钥,在函数m13873ao生成,bArr2是查询结果value用base64decode后的数组,str是value通过aes加密生成的
public static byte[] m14453c(byte[] bArr, byte[] bArr2, byte[] bArr3) {
SecretKeySpec secretKeySpec = new SecretKeySpec(bArr, "AES");
IvParameterSpec ivParameterSpec = bArr3 == null ? new IvParameterSpec(new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) : new IvParameterSpec(bArr3);
Cipher instance = Cipher.getInstance("AES/CBC/PKCS5Padding");
instance.init(2, secretKeySpec, ivParameterSpec);
return instance.doFinal(bArr2);
而 str 插入数据库中的 key-value 的代码在 mo10095b函数 中, key 的生成在 mo10266b 是固定的, value 通过mo10095b 函数生成 ,查找 mo10095b 的调用,发现它在 ActivationActivity 和 ChangeMobileNumPreActivity 中调用过,猜想是在激活和改变手机号码时将军令初始化调用 mo10095b 生成了 value
ActivationActivity.this.f9569d.mo10095b(bundle.getString(C3806c.m14235g())); Bundlebundle = data.getBundle("data"); Bundledata = message.getData(); 这个 message 就是服务器传递过来的数据,包含 number , content , UUID 等,简单 hook 得到 m14235g 的值为 https://service.mkey.163.com/WSszq1twyG/api/v3/claim_app_sn_by_server_sms ,至此找到了三石官方的 api
二、3.3 str2 与 str 生成类似,也是数据库查询得到value,然后调用 m13851E 函数,通过 m14453c 进行 aes 加密生成
str2 插入数据库是通过 mo10101c函数完成的,与前面类似 ActivationActivity.this.f9569d.mo10101c(bundle.getString(C3806c.m14236h()));
hook 得到另一个 api:m14236h:https://service.mkey.163.com/WSszq1twyG/api/v3/sms_auth_code_activate
所以 app 通过 f8402a 这个 handler实例 生成了数据库中的 key-value
二、4 简单跟一下还可以找到发送信息的函数 [Asm] 纯文本查看 复制代码
public void m12579f() {
this.f8412q = C4522a.m16536a(R.layout.dialog_progress, R.id.text, "正在发送短信,请稍等...", false);
this.f8412q.mo1326a(getSupportFragmentManager(), "progress_dialog");
this.f8413r = null;
this.f8410o = C4369n.m16038b(C4369n.m16031a(16));
this.f8409n = new C3307a(this.f8410o, this.f8402a, C3845i.m14466d(this));
this.f8409n.start();
}
hook 这个函数重新激活将军令,就可以尝试搞明白他的协议了,这里我们就不深入分析了
三、我们搞明白了 3 个参数的生成,回头再看函数 [Asm] 纯文本查看 复制代码
public static String m14037b(long j, String str, String str2) {
long e = m14040e(j);
if (f9848a > e) {
e = f9848a;
}
return String.format(Locale.ENGLISH, "%06d", new Object[]{Integer.valueOf(getOtp(e, Long.parseLong(str), C4369n.m16044c(str2)))});
}
关键函数是一个 jin 函数,在 libmkey.so 中,他的 前2 个参数分别为,时间戳 e ,序列号转化为 long 类型,
关于第3个参数,我们简单看一下 m16044c 函数,发现他仅仅具有 string 转化为 byte 数组的功能,所以第三个参数 为str 2 转化的 byte 数组 。
如果只实现自己的 otp 功能的话,我们直接写一个 app 调用 libmkey.so 中的 getOtp 就可以了,前面分析参数都没用了,{:1_926:} {:1_926:} 因为他的第一个参数是时间戳,第二三个参数是激活时通过 api 获取的,在本地计算后是一个定值,直接 hook 就可以拿到了,想完整分析参数的生成原理才需要一个一个分析{:1_911:} {:1_911:},下面先放一下本地测试代码: https://github.com/conanan/my_mkey 渣渣代码让大家见笑了{:1_904:} ps:”懒癌犯了,不想多次注册跟踪通过服务器端api生成序列号str与str2的过程并还原了,有空再完整实现,下一篇简单分析下native层的实现,
免费评分
查看全部评分