车智赢登录页面

车智赢登录页面

车智赢登录页面1.安装apk老规矩,先下载相关版本软件

车智赢+下载页

2.抓包分析clash抓包注意事项(有空再补,今天我们直接用小鸟抓包)

打开小鸟进行抓包

通过返回信息,可以说明成功抓到相应的包

相应的携带数据如下

接下来我们开始进行简单分析

请求地址:https://dealercloudapi.che168.com/tradercloud/sealed/login/login.ashx

请求方式:POST

请求头:

可以发现通过okhttp发送。但没有什么特殊字节不需要破解

请求体:

appid:#定值sign:#像加密,需要破解

appversion: #app版本,固定的channelid:#固定值,可以尝试删除,再发包试一下pwd:#密码加密了,需要破解

udid:#需要破解username:#很明显就是刚输入的手机号不需要破解

3.反编译分析进行反编译,查看基础信息

pwd逆向通过关键字进行搜索

优先搜索部分url,这里使用使用login/login.ashx作为关键字段

我们发现无法检索到相关代码,根据jadx的报错信息猜测进行加固,我们查看验证

我们进行脱壳7f281176793b0d4c17dded39c446ae0c

这里尝试多种脱壳方式暂时没有找到好的解决办法,我们直接降低版本继续进行逆向分析

确定为昨天脱壳网站的网络问题,今天成功脱壳,我们继续往下分析。

脱壳后我们根据关键信息查找到我们的

我们成功查找到登录接口地址,双击进入查看

成功定位到这是一个UserModel 下定义了一个URL常量,我们通过右键查找用例来观察常量的使用。

找到一个位置,进入代码位置进行查看

我们直接观察不难发现其加密关键代码SecurityUtil.encodeMD5(str3)我们进一步进入查看该代码

查看代码可以发现是一个典型的MD5加密

同理我们也可以使用代码和hook来验证位置

py验证:

我们将输入明文放入python中用代码进行验证看是否与抓包得到的参数一致

验证发现结果一致

hook验证:

加密结果与抓包结果进行验证

可以发现完全一致确认位置正确。

逆向签名_sign老规矩直接搜索关键值”_sign”

观察发现大致可以分为AHAPIHelper和launchModel的类两种情况,我们进入查看以下通过hook确认具体位置

首先AHAPIHelper我们可以通过观察猜测使用toSign进行加密,我们hook尝试一下

我们发现hoo点击后并没有返回值,说明脚本没有执行位置不是这里。

我么不能继续向下排查

下述代码一眼望不到头,那么~老规矩,我们开始查找用例

???没有,斯~我们继续向下排查试一下

好好好不愧是新版本哈不慌我们浅分析一下哈,查找后发现只有下面一处用例

观察可以看出这是一个典型的解析配置或签名文件的逻辑片段,并进行了简单的混淆。

我们从 BufferedReader 中逐行读取文本,解析出以下字段:

字段名

解析方式

存储变量

ApkHash:

提取冒号后的值

f

KEY_SIGNATURE

提取冒号后的值

c

KEY_SIGNATURE2

提取冒号后的值

d

KEY_SIGNATURE3

提取冒号后的值

e

其他行

拼接到 stringBuffer 中,最终赋值给 g

g

那这很明显不是我们本轮分析的重点,我们继续排查

哎?!

这段!这SignManager.INSTANCE.signByType(0, treeMap)看着就让人很有希望

老规矩我们来hook一下

与抓包结果对比可以确定我们这次寻找的位置是正确的

同样我们通过hook结果可以发现每次**udid** 都会改变,造成结果不同

既然问题解决我们也来简单分析这一长串的函数

首先是输入参数的部分:

obj:前端传来的 JSON 对象

callback:JS 回调,用于返回处理后的数据

很明显先开始使用 TreeMap 自动按 key 排序

从输入的JSONObject提取所有键值对注入用户相关参数(userkey, memberid, dealerid)

treeMap.put(“userkey”, userInfo.userkey); // 用户密钥treeMap.put(“m”, userInfo.memberid); // 会员IDtreeMap.put(“d”, userInfo.dealerid); // 经销商ID

添加APP_ID、渠道ID、版本号等固定参数treeMap.put(LaunchModel.KEY_APP_ID, Constants.APP_ID); // 应用IDtreeMap.put(“channelid”, AppUtils.getChannelId(…)); // 渠道IDtreeMap.put(LaunchModel.KEY_APP_VERSION, …); // 应用版本

调用SignManager生成数字签名treeMap.put(“_sign”, SignManager.INSTANCE.signByType(0, treeMap)); // 签名

签名结果放入 _sign 字段

udid这段理明白之后,接下来让我们逆向一下刚刚上面那个一致乱变(emm……还要破解,不能说人家乱变,对不起,求简单点)的udid

这里我们上面的_sign值在生成过程中运用到了udid,个人猜测在该代码上边或周围应该有关键函数

运气这一块./,当然我们也可以通过科学的查找方法来完成,具体示例在上边两个参数的逆向过程中已经详细展示过,这里不再赘述,我们继续跟踪getUDID函数

getUDID() 的返回值就是一段 3DES 加密串,SecurityUtil.encode3Des()

3DES 加密逻辑

这串加密逻辑也很简单,我们简单分析一下这串代码(对不起,不简单,找着找者进so里去了)

加密逻辑分析

**encode3Des(Context context, String str)**接收上下文和待加密字符串 AHAPIHelper.getDesKey(context) - 从AHAPIHelper类获取3DES加密密钥

那么我们分析查找des-key的值

这里的des-key很明显是从和getSignDesKey中得到,我们继续向下查找

继续查找,哎!在这里我们见到了一个不太亲切的老朋友System.loadLibrary("native-lib");

那还说啥了?so层今天是肝不动了,我们换个逻辑。一般des加密的key是固定的,我们直接多次hook验证一下。是的话我们直接用。

经验证key值确实是固定的result=appapiche168comappapiche168comap

那么我们接着往下分析

byte[] bArr = null - 声明并初始化为null,用于存储加密后的字节数据

TextUtils.isEmpty(desKey) 检查密钥是否为空如果密钥无效,直接返回null,避免后续加密操作异常

然后开始try-catch块,捕获加密过程中可能出现的所有异常

那么接下里就是我们重要的加密逻辑

SecretKeyFactory.getInstance("desede") - 获取3DES算法密钥工厂

"desede" - 3DES算法的标准名称

new DESedeKeySpec(desKey.getBytes()) - 根据字节数组创建3DES密钥规范

generateSecret() - 生成SecretKey对象

完整逻辑:将字符串密钥转换为Java加密API可用的SecretKey对象

创建密码器实例

Cipher.getInstance("desede/CBC/PKCS5Padding") - 获取3DES密码器实例

算法模式详解:

desede - 3DES算法

CBC - 密码块链模式(Cipher Block Chaining)

PKCS5Padding - 填充方案,确保数据块大小符合要求

初始化密码器

cipher.init(1, generateSecret, new IvParameterSpec(iv.getBytes()))

1 - 常量值,对应Cipher.ENCRYPT_MODE(加密模式)

generateSecret - 上一步生成的密钥对象

new IvParameterSpec(iv.getBytes()) - 创建初始化向量

iv - 类中定义的静态字符串变量(初始化向量)

IvParameterSpec - 确保CBC模式的安全性

执行加密操作

str.getBytes("UTF-8") - 将输入字符串转换为UTF-8编码的字节数组

cipher.doFinal() - 执行加密操作,返回加密后的字节数组

逻辑:使用配置好的密码器对输入数据进行加密

异常处理

catch (Exception unused) - 捕获所有异常但忽略不处理

设计问题:静默吞掉异常,不利于调试和错误排查

返回编码结果

encode(bArr) - 调用encode方法(可能是Base64编码)

toString() - 确保返回字符串类型

逻辑:将加密后的字节数组转换为可读的字符串格式(通常是Base64)

观察就可以发现是一个明显的Base64编码的实现,不行了,写不动了,直接hook吧

加密字符串逻辑明文格式固定为:IMEI + “|” + 秒级时间戳 + “.000000” + “|” + SPUtils.getDeviceId()

emm……这个明文格式就是上边字符串的拼接,好难弄,不想写了。算啦,第一次完整写这种案例,稍微解释一下context, AHDeviceUtil.getDeviceId(context) #看到context很明显就是我们手机的IMEI

context

至于context的这个概念想必我们刚刚看完第一行代码的小朋友们就很熟悉了,不过看了自己当初的笔记感觉还是有点稚嫩了,正好这里让我们来详细补充一下。

在安卓(Android)开发中,context是一个非常重要的概念,它代表了应用程序的当前状态信息。

每个Android应用程序都有一个context,它允许应用程序访问系统资源和执行各种操作。

context通常是由Android系统传递给应用程序的各个组件(如Activity、service、BroadcastReceiver等),以便它们能够与系统和其他组件进行交互。 Context的主要作用包括: #访问资源:通过context,您可以访问应用程序的资源,如布局文件、字符串、图片等。这是因为context持有对应用程序资源的引用,使我们能够在应用程序中加载和使用这些资源。 #启动组件:通过context,您可以启动其他组件,如Activity、service、BroadcastReceiver等。例如,我们可以使用context启动一个新的Activity来打开新的界面。 #获取系统服务:通过context,我们可以获取系统级别的服务,例如获取系统的传感器、网络状态、存储管理等。这些服务是通过系统提供的服务注册表(service Registry)来获取的。 #应用程序级别的操作:context还可以用于执行应用程序级别的操作,如发送广播、获取应用程序包名、获取应用程序的数据目录等。

我们进入getDeviceId来简单分析一下

我不行了,我开发没学好遭报应了,既然如此,那让我们来详细分析一下

getDeviceId()代码详细解

1public static synchronized String getDeviceId(Context context) {

方法声明

public - 公共方法,可以被其他类访问

static - 静态方法,属于类而非实例

synchronized - 同步方法,确保多线程环境下线程安全

String - 返回字符串类型的设备ID

getDeviceId(Context context) - 接收Android上下文参数

同步块

synchronized (AHDeviceUtil.class) - 使用类对象作为锁,确保同一时间只有一个线程能执行此代码块

123if (!TextUtils.isEmpty(mCompanyDeviceid)) { return mCompanyDeviceid;}

内存缓存检查

mCompanyDeviceid - 静态变量,作为内存级别的设备ID缓存

TextUtils.isEmpty() - 检查字符串是否为null或空字符串

逻辑:如果内存中已有缓存的设备ID,直接返回,避免重复生成

123456OnDeviceIdListener onDeviceIdListener = mDeviceIdListener;if (onDeviceIdListener != null) { String deviceId = onDeviceIdListener.getDeviceId(); mCompanyDeviceid = deviceId; return deviceId;}

监听器检查

OnDeviceIdListener - 设备ID生成监听器接口

mDeviceIdListener - 静态监听器实例

逻辑:如果设置了自定义监听器,通过监听器获取设备ID并缓存到内存

1234String readString = PreferenceHelper.readString(context, "usedcar_pre", "udid");if (!udidIsNull(readString)) { return readString;}

SharedPreferences读取

PreferenceHelper.readString() - 从SharedPreferences读取数据

"usedcar_pre" - SharedPreferences文件名

SharedPreferences 是 Android 提供的一种 轻量级本地持久化存储方案,用来以 键值对(key-value) 形式保存 少量、结构化、可跨进程读取 的配置数据,比如用户设置、首次启动标志、设备 ID 等。

项目

说明

存储位置

/data/data/<包名>/shared_prefs/*.xml

数据格式

纯文本 XML,可读可改(需 root)

作用域

默认 私有(MODE_PRIVATE),仅本应用可读写;支持 MODE_MULTI_PROCESS 等

读写方式

自动加锁,线程安全;读 用 getXxx(),写 用 edit().putXxx().apply()

容量

官方无硬性限制,建议 < 1 MB;过大请用数据库

数据类型

boolean / int / long / float / String / Set

"udid" - 存储设备ID的键名

udidIsNull() - 自定义方法,检查设备ID是否有效

逻辑:如果本地已存储有效的设备ID,直接返回

123if (PreferenceHelper.isShowPrivacyPolicyDialog(context)) { return UUID.randomUUID().toString();}

隐私政策检查

isShowPrivacyPolicyDialog() - 检查是否需要显示隐私政策对话框

UUID.randomUUID().toString() - 生成随机UUID作为临时设备ID

逻辑:如果用户未同意隐私政策,返回随机UUID(避免收集真实设备信息)

1if (udidIsNull(readString)) {

有效性复查

再次确认从SharedPreferences读取的设备ID是否无效

123if (Build.VERSION.SDK_INT >= 29) { readString = Settings.Secure.getString(context.getContentResolver(), SocializeProtocolConstants.PROTOCOL_KEY_ANDROID_ID);} else {

Android版本分支 - Android 10+

Build.VERSION.SDK_INT >= 29 - 判断是否为Android 10及以上版本

Settings.Secure.getString() - 获取系统安全设置中的值

SocializeProtocolConstants.PROTOCOL_KEY_ANDROID_ID - 常量,值为”android_id”

逻辑:Android 10+ 使用Android ID作为设备标识(因权限限制无法获取IMEI)

java

12345TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService("phone");if (telephonyManager != null) { if (ActivityCompat.checkSelfPermission(context, "android.permission.READ_PHONE_STATE") != 0) { return mCompanyDeviceid; }

Android 10以下版本 - 权限检查

context.getSystemService("phone") - 获取电话管理器服务

ActivityCompat.checkSelfPermission() - 检查READ_PHONE_STATE权限

逻辑:如果没有电话状态读取权限,返回内存缓存值(可能为空)

1234567 if (Build.VERSION.SDK_INT >= 26) { readString = telephonyManager.getImei(); } else { readString = telephonyManager.getDeviceId(); } }}

IMEI/DeviceId获取

Build.VERSION.SDK_INT >= 26 - 判断是否为Android 8.0及以上

telephonyManager.getImei() - Android 8.0+ 获取IMEI

telephonyManager.getDeviceId() - Android 8.0以下获取设备ID

逻辑:根据Android版本使用不同的设备标识获取方法

1if (udidIsNull(readString)) {

系统ID有效性检查

检查从系统获取的设备ID是否有效

123456789AHWifiInfo aHWifiInfo = AHNetworkUtil.getAHWifiInfo(context);String macAddress = aHWifiInfo != null ? aHWifiInfo.getMacAddress() : null;if (!TextUtils.isEmpty(macAddress)) { try { readString = UUID.nameUUIDFromBytes(macAddress.getBytes("utf8")).toString(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); }}

MAC地址备用方案

AHNetworkUtil.getAHWifiInfo() - 获取Wifi信息

getMacAddress() - 获取MAC地址

UUID.nameUUIDFromBytes() - 根据MAC地址字节生成UUID

逻辑:如果系统设备ID无效,尝试使用MAC地址生成UUID

1234 if (udidIsNull(readString)) { readString = getUUID(context); }}

最终备用方案

getUUID(context) - 调用其他方法生成UUID

逻辑:如果所有方法都失败,使用最后的UUID生成方案

1234 if (!udidIsNull(readString)) { PreferenceHelper.write(context, "usedcar_pre", "udid", readString); }}

持久化存储

PreferenceHelper.write() - 将设备ID写入SharedPreferences

逻辑:如果成功生成了有效的设备ID,保存到本地存储供下次使用

123 return readString; }}

返回结果和方法结束

返回最终获取到的设备ID

结束同步块和方法

通过分析这三种方案,我们相对也可以直接用UUID

或者从xml中找到它存储的字符串

/data/data/com.che168.autotradercloud/shared_prefs/

谁爱找谁找吧,我是不找了

或者通过hook得到等多种方案。

这是我们hook得到的结果81f2e9f2_fe62_43fa_a273_e425d929ba47

+ HiAnalyticsConstant.REPORT_VAL_SEPARATOR #一个固定字符串,我们可以跳转查看一下是个“|”

+ System.nanoTime() #手机开机时间。

java中是一个前系统时间的纳秒数,但是在android系统中,这个表示手机开机时间

#注意,它跟java的这个函数返回值是不一样的

+ HiAnalyticsConstant.REPORT_VAL_SEPARATOR #嗯呐又一个“|”

+ UserInfoUtil.getUserDeviceId(context) #DeviceId,就可以猜到哈,是我们的设备id,不放心就跳转过去瞄一眼(好吧,承认是我多疑)

很明显就是一个只读的的程序代码。从哪里来的呐?“嘿”我们看到一位老朋友“usedcar_pre”,我们就知道读取自.xml文件,那么从哪里写入的呐?我们再继续逆向分析一下

好好这么一长串, write() / readInt() / readLong() … 只是 “带内存缓存的 SharedPreferences 万能工具箱”,它们和 getUserDeviceId() 的关系非常单纯:getUserDeviceId() 仅仅是 readInt() 的一个普通调用者文件名为 “usedcar_pre”,key 为 “device_id”,返回值被当成 int 型设备序号 使用。不找了,我们选择直接通过hook来得到返回字串。

代码实现

最终逆向_sign当我们得到这些信息后我们进行最后的解密

我们进入signByType

通过代码分析我们可以得到拼接公式为

构建字符串: “secret_key_v2” + “appid” + “test” + “version” + “1.0” + “secret_key_v2”= “secret_key_v2appidtestversion1.0secret_key_v2”

MD5加密: MD5(“secret_key_v2appidtestversion1.0secret_key_v2”)= “5d41402abc4b2a76b9719d911017c592”

转换为大写: “5D41402ABC4B2A76B9719D911017C592”

至此就完成后了我们一个登录页面的复现

4.so层key值分析既然我们任务完成了,那我们就看一下des-key值的逻辑实现

根据我们之前分析到的代码我们应该到libnative-lib.so文件中查看这个注册

然后通过java_包名_类名_方法名读c的源码,我们打开后直接在Exports中查询

发现可以查询到关键字,是一个静态注册,我们进入分析一下

有一说一,烦so的一大部分原因就是很混乱分析起来真的很恶心人啊

为了我们的脑细胞,那是说啥了,我们先引入一下jni头文件

emm……报错了,那还说啥了,直接改吧。

Hide casts再show casts,隐藏投射,也就是隐藏掉我们的类型转换

简单查看一下就知道return前的第一个参数是env,第二个DES3_KEY就是我们要找的值,进入观察

可以发现这个固定值就是我们的key,与我们frida得到的so一样。

相关推荐

批邓、反击右倾翻案风
365bet体育在线中文

批邓、反击右倾翻案风

07-31 👁️ 8966
【黃大仙參訪攻略】香港黃大仙廟求籤步驟、參拜路線、參拜禁忌一次看!
「深度解析」适合玩英雄联盟的五款高性能CPU推荐!
唱吧麦克风排行榜
完美体育365官方网站

唱吧麦克风排行榜

09-04 👁️ 8491
美丽又神秘的西藏建筑
365bet足球盘

美丽又神秘的西藏建筑

01-14 👁️ 9430
禅机的意思
365bet足球盘

禅机的意思

10-16 👁️ 8130
怎么样抓住狮子男的心(成功攻略分享)
完美体育365官方网站

怎么样抓住狮子男的心(成功攻略分享)

09-06 👁️ 1069
绿字的拼音
365bet足球盘

绿字的拼音

01-19 👁️ 5825
情侣手机靓号价值分析与送礼指南
365bet体育在线中文

情侣手机靓号价值分析与送礼指南

02-11 👁️ 8371