吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 6714|回复: 13
收起左侧

[Android 原创] 某公司otp生成分析

[复制链接]
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: Nonemessage:{'type': 'send', 'payload': 'e:1574052510  str:8xx5xx4xxx  str2:x2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx   rtn:120671'} data: Nonemessage:{'type': 'send', 'payload': 'hook b start'} data: Nonemessage:{'type': 'send', 'payload': 'e:1574052540  str:8xx5xx4xxx  str2:x2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx   rtn:436634'} data: Nonemessage:{'type': 'send', 'payload': 'hook b start'} data: Nonemessage:{'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@c3c2927Resultof f10441j  func: 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值,而f9842dC3830e的实例,通过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 () { }
});

返回值为

20191129194214dama.png 找到这4key,寻找数据库中对应的value

adbpull出数据库目录/data/data/com.netease.mkey/databases/ekey
20191129194507dama.png 很容易找到3个函数产生的key对应的value,我们一看这个value明显base64编码的,猜想后面肯定要用base64解码
二、3.1这些keyvalue对应的数据库如何生成呢,先分析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进入数据库 ,而参数jf9848a,下面我们分析f9848a如何生成f9848a首先通过mo10109e从数据库中查询生成初始值,然后通过m14035a(longj)函数更新,在函数onOtpRefreshClick中通过这句代码OtpLib.m14035a(this.f10441j.longValue()),简单观察我们可以猜想,f9848a存储的是上一次的时间戳e,保证当前e>上次运行存储的e,防止时间倒流{:1_909:},而我们每次点击刷新触发onOtpRefreshClick,都会将e提前到下一个30seotp也将是下一个,但我们不能无限制刷新,还记得将军令有个冷却吗,就是在这个函数中实现的,刷新时间必须小于f9851d,根据经验大概最多同时出现4otp,我们就不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的调用,发现它在ActivationActivityChangeMobileNumPreActivity中调用过,猜想是在激活和改变手机号码时将军令初始化调用mo10095b生成了value
ActivationActivity.this.f9569d.mo10095b(bundle.getString(C3806c.m14235g()));Bundlebundle = data.getBundle("data"); Bundledata = message.getData();这个message就是服务器传递过来的数据,包含numbercontentUUID等,简单hook得到m14235g的值为https://service.mkey.163.com/WSszq1twyG/api/v3/claim_app_sn_by_server_sms,至此找到了三石官方的api
二、3.3  str2str生成类似,也是数据库查询得到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数组的功能,所以第三个参数为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层的实现,


免费评分

参与人数 4威望 +1 吾爱币 +15 热心值 +4 收起 理由
qtfreet00 + 1 + 12 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
沅澧潇湘 + 1 + 1 谢谢@Thanks!
RSu52 + 1 + 1 我很赞同!
SanadaYukimura + 1 + 1 用心讨论,共获提升!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

leafleave 发表于 2019-11-30 10:56
希望steam也能使用类似谷歌、微软的开源totp算法,每次登录还有字母烦死了,而且还不能用通用的密码管理工具保存
 楼主| L剑仙 发表于 2019-11-29 21:39
 楼主| L剑仙 发表于 2019-11-29 21:42
我插的表情也全变成代码了,版主能让我在调一调吗,感谢
yeeboo 发表于 2019-11-29 22:13
学习了,支持楼主
世界如此温柔 发表于 2019-11-30 08:56
向大佬学习
沅澧潇湘 发表于 2019-11-30 09:02
虽然看不懂,支持一下!
光棍节快乐 发表于 2019-11-30 09:04
菜鸟表示看不懂
x2005y 发表于 2019-11-30 10:14
谢谢分享,真心不错!!!
qwjituan 发表于 2019-11-30 14:31
学习了 感谢楼主分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-12-22 22:04

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表