首页
论坛
专栏
课程

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

2020-2-15 09:00 922

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

2020-2-15 09:00
922
菜鸟在52发过了,也转到看雪来一起学习
一、首先,我们知道每次点击动态密码刷新就会刷新动态密码,于是,我们点击,并打开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位并显示在界面上
hook  m14037b函数并打印参数返回值 
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()返回值
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 () { }
Found instance: com.netease.mkey.core.EkeyDb@c3c2927
Result of f10441j  func: 0
Result of f10438g func: 8795409155
Result of f10439h func: ed45e9595d309a481bb37ba6c944e772
Result of f9848a func: 1574052493

下面跟踪mo10123h() mo10126i() mo10130j() mo10109e()4个函数,分析这4个函数都是数据库查询函数,其中最重要的一句就是数据库查询,以mo10109e()为例,

Cursor query = this.f9841b.query(true, "config", new String[]{"key", "value"}, "key=?", new String[]{this.f9842d.mo10260V()}, null, null, null, null);


从表config中提取key=this.f9842d.mo10260V()的value值,而f9842d为 C3830e的实例

private C3830e f9842d = new C3830e(m13872an());



对应方法分别为mo10123h :mo10268d() mo10126i: mo10266b() mo10130j: mo10267c() mo10109e: mo10260V()

hook com.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 () { }
});
Found instance: com.netease.mkey.core.e@fd54ca3
mo10123h :mo10268d() 8524aedb3a8575e202b9b075a0164db0de5ef58a25647578afc3d60de7e5c34c
mo10126i: mo10266b() 949fae0b3419ef6a21c288d60905495d
mo10130j: mo10267c()1f0f9b0a49c2eed255f69579d74cb704
mo10109e: mo10260V()c580e932e8c4a8ba8fb9f3fae99ad87f

找到这4个key,寻找数据库中对应的value


adb pull出数据库 目录/data/data/com.netease.mkey/databases/ekey

很容易找到3个函数产生的key对应的value


这些key和value对应的数据库如何生成呢,以mo10268d 为例,他在mo10107d函数中通过contentvalue 将参数j update进入数据库

ActivationActivity.this.f9569d.mo10107d(bundle.getLong(C3806c.m14238j()));


我们再看mo10260V()在 mo10078a(long j)函数中通过contentvalue 将参数j update进入数据库 ,而参数j为 f9848a,下面我们分析f9848a如何生成

f9848a首先通过mo10109e从数据库中查询生成初始值,然后通过m14035a(long j)函数更新,,在函数onOtpRefreshClick中通过这句代码OtpLib.m14035a(this.f10441j.longValue()),简单观察我们可以猜想,f9848a存储的是上一次的时间戳e,保证当前e>上次运行存储的e,不然时间不就倒回去了嘛,

而我们每次点击刷新触发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;
        }
    }

str就是你的序列号,他是怎么生成的呢

 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是查询结果s2用base64decode后的数组

 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在数据库中的keyvalue是通过mo10095b插入的,key的生成在mo10266b是固定的,value需要mo10095b的参数str,查找mo10095b的调用,发现它在ActivationActivity和ChangeMobileNumPreActivity中调用过,猜想是在激活和改变手机号码时将军令初始化调用mo10095b生成了value

ActivationActivity.this.f9569d.mo10095b(bundle.getString(C3806c.m14235g()));
ChangeMobileNumPreActivity.this.f9569d.mo10095b(((Bundle) acVar.f9661c).getString(C3806c.m14216aj()));

 Bundle bundle = data.getBundle("data");
 Bundle data = message.getData();    这个message就是服务器传递过来的数据,包含number,
content,UUID等,简单hook得m14235g为https://service.mkey.163.com/WSszq1twyG/api/v3/claim_app_sn_by_server_sms,至此找到了三石官方的api

str2与str生成类似,也是调用了 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

简单跟一下还可以找到发送信息的函数

   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中,他的3个参数分别为,时间戳e,序列号转化的long类型

我们简单看一下m16044c函数,发现他仅仅具有string转化为byte数组的功能,所以第三个参数str2转化的byte数组。如果只实现otp功能的话,我们直接写一个app调用libmkey.so中的 getOtp就可以了,因为他的第一个参数是时间戳,第二三个参数是激活时通过api获取的,在本地计算后是一个定值,直接hook就可以拿到了

想完整分析参数的生成原理才需要一个一个分析{:1_911:} {:1_911:},下面先放一下本地测试代码,拿到这2个值就可以完全动态生成otp:

https://github.com/conanan/my_mkey 

渣渣代码让大家见笑了{:1_904:} ps:”懒癌犯了,不想多次注册跟踪通过服务器端api生成序列号str与str2的过程并还原了,有空再完整实现,下一篇简单分析下native层的实现。



[招生]科锐逆向工程师培训(3月6日远程教学班首开特惠, 第37期) !

最新回复 (2)
mb_xghoecki 2020-2-15 10:24
2
0
感谢分享
NoThx 6天前
3
0
感谢分享
游客
登录 | 注册 方可回帖
返回