某公司otp生成分析
菜鸟年中的时候无聊练手的app,当时遇到点问题又太忙了就放一边去了,最近有空捡起来接着分析下,论坛记录下以免忘记{:1_918:} {:1_918:}一、首先,我们知道每次点击动态密码刷新就会刷新动态密码,于是,我们点击,并打开ddms 通过方法回溯功能定位到函数onOtpRefreshClick()用jadx反编译mkey.apk,勾选反混淆,另存为gradle在as中打开依次查看onOtpRefreshClick()的子函数 依次观察onOtpRefreshClick子函数
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位并显示在界面上
hookm14037b函数并打印参数返回值
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:1574052480str:8xx5xx4xxxstr2:x2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx rtn:804671'} data: None
message:{'type': 'send', 'payload': 'hook b start'} data: Nonemessage:{'type': 'send', 'payload': 'e:1574052510str:8xx5xx4xxxstr2:x2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx rtn:120671'} data: Nonemessage:{'type': 'send', 'payload': 'hook b start'} data: Nonemessage:{'type': 'send', 'payload': 'e:1574052540str:8xx5xx4xxxstr2:x2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx rtn:436634'} data: Nonemessage:{'type': 'send', 'payload': 'hook b start'} data: Nonemessage:{'type': 'send', 'payload': 'e:1574052570str:8xx5xx4xxxstr2: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()返回值
Java.choose("com.netease.mkey.core.EkeyDb", {
onMatch: function (instance) {
console.log("Found instance: " + instance);
console.log("Result of f10441jfunc: " + 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@c3c2927Resultof f10441jfunc: 0Resultof f10438g func: 8xx5xx4xxxResultof f10439h func: x2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxResultof 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个方法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将参数jupdate进入数据库 ,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将参数jupdate进入数据库 ,而参数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寻找他的值了 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,m14453cprivate 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.3str2与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简单跟一下还可以找到发送信息的函数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个参数的生成,回头再看函数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数组的功能,所以第三个参数为str2转化的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层的实现,
希望steam也能使用类似谷歌、微软的开源totp算法,每次登录还有字母烦死了,而且还不能用通用的密码管理工具保存 我日 怎么格式又乱了 尴尬 我插的表情也全变成代码了,版主能让我在调一调吗,感谢 学习了,支持楼主 向大佬学习 虽然看不懂,支持一下! 菜鸟表示看不懂 谢谢分享,真心不错!!! 学习了 感谢楼主分享
页:
[1]
2