领创动态密码算法解析
最近发现有人似乎存在忘记密码等存在相关问题,我干脆吧22年底研究的东西放出来吧,反正这东西我早就不搞了
0. 声明
本报告仅用于技术交流,严禁用于非法用途。
0.0 目录
1. 算法介绍
该算法用于生成领创动态密码。动态密码可用于跳过手势密码、退出登录、锁机解锁。
2. 算法原理
加盐md5
3. 算法解析
预备材料:
- apk安装包
- 这里选用5.03的安装包,实际上5和6的算法是相同的,但jadx在反编译6时可能会出现失败只给伪代码的情况,虽然不影响分析,但不美观。
- jadx
- 使用搜索引擎的能力
3.1 反编译
丢到jadx里面,开启反混淆
3.2 分析
3.2.1 代码定位
按照逆向常规尤其是破jie,反编译后第一步就是定位代码位置。定位方法很多,一般来说,字符串定位是最常用且最简单的,简单步骤此处不赘述。
3.2.2 代码分析
3.2.2.1 顺藤摸瓜
public boolean m4493a(String str) {
int m3546b = f2681c.m3546b("dynamic_pass", 1);
String m4491c = m4491c();
String m4495a = m4495a();
if (m3546b == 0) {
if (str.equals(m4491c) || MD5Util.m3060a(str).equals(m4495a)) {
return true;
}
} else if (m3546b == 1 && str.equals(m4491c)) {
return true;
}
return false;
}
public String m4495a() {
this.f2683e = f2681c.m3544b("password2", "");
return !TextUtils.isEmpty(this.f2683e) ? this.f2683e : "unknown";
}
m4493a()判断输入的密码是否正确的函数。
这里我们不难发现,返回true有两个条件:
- 当策略未启用动态密码时,使用固定的管理员密码(我们的是huananfz1!)
- 启用动态密码,则先通过函数m4491c计算动态密码,然后与输入的密码进行比较
3.2.2.2 外围代码分析
算法其实很简单
private String m4491c() {
String m4492b = m4492b();
if (m4492b.length() >= 6) {
return m4492b.substring(0, 6);
}
return m4492b;
}
我们可以看到,m4491c只是截取了算完后的六个字符作为最终结果。这里的判断理论上始终为true,因为m4492b是从md5得来的,具我们经过下面的分析后就会明白。
public String m4492b() {
String m2965a = LTKDeviceUtil.m2965a(f2680b);
if (!TextUtils.isEmpty(m2965a) && !"unknown".equals(m2965a)) {
this.f2682d = SettingUtil.m3539a(f2680b).m3538a(m2965a);
}
return !TextUtils.isEmpty(this.f2682d) ? this.f2682d : "unknown";
}
public static String m2965a(Context context) {
if ((LTKDeviceTypeUtil.m3011i() || LTKDeviceTypeUtil.m3021d()) && Build.VERSION.SDK_INT >= 29) {
return m2947g(context).toLowerCase();
}
String m2951e = m2951e(context);
if (!m2951e.contains("unknown")) {
new LTKSpfUtil(context, "linslib").m2858a("wifi_mac", m2951e);
return m2951e;
}
String m2857b = new LTKSpfUtil(context, "linslib").m2857b("wifi_mac", "unknown");
if (m2857b.equals("unknown")) {
String m2960b = m2960b(context);
return m2960b.equals("unknown") ? m2954d(context) : m2960b;
}
return m2857b;
}
我们先来看m2965a的来龙去脉。如果是安卓10+,,且设备类型为华为等,那么这里会选用sn(设备序列号),否则用wifi mac地址。
我们不管那么多,反正都一样。现在假定返回的是sn,进入核心代码。
3.2.2.3 核心代码分析
public String m3538a(String str) {
Log.i("CalculatePwd", "pwd is start");
String str2 = new SimpleDateFormat("yyyyMMdd").format(new Date()) + str + "40E06F51-30D0-D6AD-7F7D-008AD0ADC570";
String m5067X = UserInfoUtil.m5026f().m5067X();
if (!TextUtils.isEmpty(m5067X)) {
str2 = str2 + m5067X;
}
if (!TextUtils.isEmpty(str2)) {
String m3537b = m3537b(str2);
if (m3537b.length() > 8) {
String m3536c = m3536c(m3537b.substring(m3537b.length() - 8, m3537b.length()));
Log.i("CalculatePwd", "pwd is start");
return m3536c;
}
}
return "";
}
/*UserInfoUtil*/
public String m5067X() {
if (TextUtils.isEmpty(this.f2139G)) {
this.f2139G = f2132H.m3544b("student_id", "");
}
return this.f2139G;
}
public String m3537b(String str) {
Log.i("CalculatePwd", "toMd5 is start");
/* 算md5的,省略 */
}
我们发现,这里首先将时间字符串、给定的str(即sn)和一串特殊字符连接起来,然后再从UserInfoUtil中获取student_id,加入字符串并计算md5。
student id在登录时调api获取并保存到本地,这里暂不做讲解,等下周再开一个帖子来说
显然,这里的m3537b肯定大于8个字符,所以这里会截取后8个字符转换为long
public static String m3536c(String str) {
Log.i("CalculatePwd", "hexToLong is start");
String valueOf = String.valueOf(Long.parseLong(str, 16));
if (!TextUtils.isEmpty(valueOf) && valueOf.length() > 8) {
return valueOf.substring(valueOf.length() - 8, valueOf.length());
}
return valueOf;
}
一样的,转换为long之后如果太长,也截取后8个字符。
这样我们就得到了8位数字,再回到前面,这8位数的前6位就是最终的密码。
3.2.3 梳理
- 集齐设备信息、日期、student_id
- 加盐计算md5
- 截取,转数字,再截取
是不是很简单?接下来我们试着自己写出完整的计算算法。
3.3 算法复现
知道了原理,写起来是很轻松的
代码已经开源在Github,点此链接查看,
这里也放上完整代码
/*
Author: SuchAnIdi0t
*** !!NOTICE!!
* These codes are opensourced under MIT License.
* Modifications and redistribution should be in compliance with the License.
* PoC only, use at your own risk.
For more information, please refer to https://github.com/SuchAnIdi0t/linspirer-dynamic-password
*/
package main
import (
"crypto/md5"
"fmt"
"log"
"strconv"
"strings"
"time"
)
func ca(str1 string) string {
str2, _ := strconv.ParseInt(str1, 16, 64)
v := strconv.Itoa(int(str2))
if len(v) <= 8 {
return v
} else {
return v[len(v)-8:]
}
}
func calc(swdid string, stu_id string) string {
str3 := time.Now().Format("20060102") + swdid + "40E06F51-30D0-D6AD-7F7D-008AD0ADC570" + stu_id
log.Println(str3)
h := md5.Sum([]byte(str3))
b := fmt.Sprintf("%x", h)
log.Println(b)
if len(b) > 8 {
return ca(b[len(b)-8:])
} else {
return "err"
}
}
func main() {
var swdid, stu_id string
fmt.Print("swdid:")
fmt.Scanln(&swdid)
swdid = strings.ToLower(swdid)
fmt.Print("stu_id:")
fmt.Scanln(&stu_id)
log.Printf("Result: %s", calc(swdid, stu_id)[:6])
}
4. 总结
动态密码的实现原理并不复杂,逆向也毫无阻碍,但其中理解涉及到的算法和逻辑还是需要一定耐心和基本能力。
有任何问题欢迎在此处探讨,点赞过5,下周讲如何获取student_id 