车智赢app破解

原文件下载路径:
【移动安全与逆向工程】车智赢App登录功能逆向分析:抓包、反编译与Hook技术应用
【移动安全与逆向工程】day14-车智赢app破解代码
【移动安全与逆向工程】day14-车智赢所需软件及插件

说明:文中所示案例仅供学习使用,请勿进行商业用途!!!

今日内容

1 app版本选择和逆向流程

1.1 今日目标

# 1 车智赢+ app---》登录功能
# 2 学习到的
	1 选择app版本安装到手机
    2 抓包
    3 分析登录包请求,逆向
    4 使用jadx反编译apk,阅读java代码
    5 寻找关键字+Hook验证
    6 python还原算法
    
# 3 下载app
	-官网:https://icloud.che168.com/login.html
    -下载地址:https://appdownload.che168.com/usedcar/csy/index.html?pvareaid=106101
    - 3.56.0版本

2 抓包分析

2.1 配置

# 1 把app安装到手机上
adb install  软件位置/xx.apk

# 2 【电脑端】打开charles
	
# 3 手机端配置代理-如下图

# 4 电脑端配置能抓取https的包,如下图

# 5 打开车智赢app,操作登录,实现抓包

# 6 抓包详情:
	-请求地址
    	https://dealercloudapi.che168.com/tradercloud/sealed/login/login.ashx
    -请求方式
    	POST
    -请求头
    	traceid:改包测试
    -请求体
    	_appid	atc.android  #固定的
        _sign	B0FB9AE0B66383288C8AEEEF6854DDFF  # 需要破解
        appversion	3.56.0   # app版本,固定的
        channelid	csy      # 固定的
        pwd	e10adc3949ba59abbe56e057f20f883e # 密码加密
        signkey	  # 空的
        type	  # 空的
        udid	iEavSW4QycaRf6qULa18YDJEOmuw10SBwdVu62EN/1/8eh46NzxTYDzG4oXl ZM0F6oKAPatKB4XyzJ9zIhTrdg== # 需要破解
        username	18952452114 # 我们的账号
        
        
 # 7 破解目标
	1 密码加密
    2 _sign :签名--》把请求体整体签名--》请求体改任意位置,签名都会错误
    3 udid

3 反编译

# 1 jadx打开apk
# 2 搜索关键字:
	pwd    "pwd"     "pwd
    搜网址:https://dealercloudapi.che168.com/tradercloud/sealed/login/login.ashx
    /login/login.ashx
    
# 3 找到谁使用了常量:LOGIN_URL--》右键--》查找用例
	-一个位置,双击进入
    
# 4 找到代码:
builder.tag(str).method(HttpUtil.Method.POST).signType(1).url(LOGIN_URL).param("username", str2).param("type", str4).param("signkey", str5).param("pwd", SecurityUtil.encodeMD5(str3));

# 5 找到:param("pwd", SecurityUtil.encodeMD5(str3))

4 逆向

4.1 搜索登录接口

上面到的笔记

4.2 破解密码加密

# 1 SecurityUtil.encodeMD5(str3)
# 2 看之前先猜一下:就是md5签名
	1 明文:123456
    2 加密后:e10adc3949ba59abbe56e057f20f883e
    
# 3 继续看源代码--》就是md5
public static final String encodeMD5(String str) {
            
    char[] cArr = {
            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
    try {
            
        // 1 把明文字符串转成bytes格式
        byte[] bytes = str.getBytes();
        // 2 拿到一个md5对象
        MessageDigest messageDigest = MessageDigest.getInstance("MD5");
        // 3 把bytes格式的数据,使用md5签名
        messageDigest.update(bytes);
        // 4 得到签名结果--》bytes格式
        byte[] digest = messageDigest.digest();
        char[] cArr2 = new char[digest.length * 2];
        // 5 把签名后的结果转成16进制--》大写
        int i = 0;
        for (byte b : digest) {
            
            int i2 = i + 1;
            cArr2[i] = cArr[(b >>> 4) & 15];
            i = i2 + 1;
            cArr2[i2] = cArr[b & bx.m];
        }
        // 6 转成小写返回   python--->md5.hexdegist()
        return new String(cArr2).toLowerCase();
    } catch (Exception e) {
            
        e.printStackTrace();
        return null;
    }
}


# 4 不确定 登录 是否真的会走它,通过hook确定一定会走
	-写个hook脚本,hook脚本正常打印,说明走了它

4.2.1 hook密码加密

# 1 手机端开启frida-server
	adb shell
    su
    cd /data/local/tmp
    ls
    ./frida-server-16.3.3-android-arm64
# 2 电脑端做端口转发
	-运行python脚本
# 3 写hook脚本运行
#### 3 运行hook代码
import frida
import sys

# 连接手机设备
rdev = frida.get_remote_device()

# Hook手机上的那个APP(app的包名字)
# 注意事项:在运行这个代码之前,一定要先在手机上启动app
session = rdev.attach("车智赢+")




scr = """
Java.perform(function () {
    // 包.类
    var SecurityUtil = Java.use("com.autohome.ahkit.utils.SecurityUtil");
    // Hook,替换
    SecurityUtil.encodeMD5.implementation = function(str1){
        // 执行原来的方法
        console.log("传入的参数为:",str1);
        var res = this.encodeMD5(str1);
        console.log("加密后为:",res);
        return res;
    }

});
"""





script = session.create_script(scr)
def on_message(message, data):
    print(message, data)


script.on("message", on_message)
script.load()
sys.stdin.read()

4.3 逆向 _sign参数

# 1 搜索: _sign  "_sign
	-有5个,第5个是
    -前4个是一样的,本质是: treeMap.put("_sign", toSign(context, treeMap));
	-通过hook--》toSign(context, treeMap) 确认看看是不是走这--》不是
    
# 2 找到两个位置可能是:
public static TreeMap lambda$initRequestCommonParams$0(int i, TreeMap treeMap)
public static TreeMap<String, String> getRequestParams(TreeMap<String, String> treeMap)

# 3 通过hook--再确认--》lambda$initRequestCommonParams$0
	-原理:
    	-传入字典:{
            pwd=e10adc3949ba59abbe56e057f20f883e, signkey=, type=, username=18952452114}
        -方法中追加了很多:变成
        {
            _appid=atc.android, _sign=E978383537AEF0389D90A502233ADAF2, appversion=3.56.0, channelid=csy, pwd=e10adc3949ba59abbe56e057f20f883e, signkey=, type=, udid=iEavSW4QycaRf6qULa18YDJEOmuw10SBwdVu62EN/1/djG80tEV6ibNjsxmv LTEy4VQJH+asjzMv3asLxv8ECA==, username=18952452114}

# 4 执行 String signByType = SignManager.INSTANCE.signByType(i, treeMap); 得到 _sign
	-这个treeMap 是除了_sign 以外,所有参数都有
    {
            _appid=atc.android, appversion=3.56.0, channelid=csy, pwd=e10adc3949ba59abbe56e057f20f883e, signkey=, type=, udid=iEavSW4QycaRf6qULa18YDJEOmuw10SBwdVu62EN/1/djG80tEV6ibNjsxmv LTEy4VQJH+asjzMv3asLxv8ECA==, username=18952452114}

    
# 5 udid不知道怎么得到的---》先破解udid后--》再破解sign的加密SignManager.INSTANCE.signByType(i, treeMap)

4.3.1 hook确认_sign位置–错误

import frida
import sys

# 连接手机设备
rdev = frida.get_remote_device()

# Hook手机上的那个APP(app的包名字)
# 注意事项:在运行这个代码之前,一定要先在手机上启动app
session = rdev.attach("车智赢+")



scr = """
Java.perform(function () {
    // 包.类
    var AHAPIHelper = Java.use("com.autohome.ahkit.AHAPIHelper");
    // Hook,替换
    AHAPIHelper.toSign.implementation = function(context,treeMap){
        // 执行原来的方法
        var res = this.toSign(context,treeMap);
        console.log("sign签名=",res);
        return res;
    }

});
"""



script = session.create_script(scr)
def on_message(message, data):
    print(message, data)

script.on("message", on_message)
script.load()
sys.stdin.read()import frida
import sys

# 连接手机设备
rdev = frida.get_remote_device()

# Hook手机上的那个APP(app的包名字)
# 注意事项:在运行这个代码之前,一定要先在手机上启动app
session = rdev.attach("车智赢+")



scr = """
Java.perform(function () {
    // 包.类
    var AHAPIHelper = Java.use("com.autohome.ahkit.AHAPIHelper");
    // Hook,替换
    AHAPIHelper.toSign.implementation = function(context,treeMap){
        // 执行原来的方法
        var res = this.toSign(context,treeMap);
        console.log("sign签名=",res);
        return res;
    }

});
"""



script = session.create_script(scr)
def on_message(message, data):
    print(message, data)

script.on("message", on_message)
script.load()
sys.stdin.read()

4.3.2 hook找到_sign真正位置

import frida
import sys

rdev = frida.get_remote_device()

session = rdev.attach("车智赢+")

scr = """
Java.perform(function () {
     // 包.类
    var LaunchModel = Java.use("com.che168.autotradercloud.launch.model.LaunchModel");

    // Hook,替换
    LaunchModel.lambda$initRequestCommonParams$0.implementation = function(i,treeMap){		console.log("执行了,参数i是:",i); 
        console.log("执行了,参数:",treeMap); 
        console.log("执行了,参数:",treeMap.toString());
        // 执行原来的方法
        var res = this.lambda$initRequestCommonParams$0(i,treeMap);
        console.log("执行了,返回值:",res);
        console.log("执行了,参数:",res.toString());
        return res;
    }
});
"""

script = session.create_script(scr)

script.load()
sys.stdin.read()

4.4 逆向udid

# 1 破解位置: treeMap.put("udid", AppUtils.getUDID(ContextProvider.getContext()));
# 2 核心是:AppUtils.getUDID(ContextProvider.getContext())--返回字符串
  public static String getUDID(Context context) {
            
        return SecurityUtil.encode3Des(context, getIMEI(context) + HiAnalyticsConstant.REPORT_VAL_SEPARATOR + System.nanoTime() + HiAnalyticsConstant.REPORT_VAL_SEPARATOR + SPUtils.getDeviceId());
    }
# 3 SecurityUtil.encode3Des 传入一堆字符串的拼接--》加密后得到

# 4 破解以下字符串
getIMEI(context) #       获取IMEI
HiAnalyticsConstant.REPORT_VAL_SEPARATOR   #  |
System.nanoTime()#       开机到现在的时间戳
HiAnalyticsConstant.REPORT_VAL_SEPARATOR   # |
SPUtils.getDeviceId()# 设备id号

# 5 上述过程:使用 encode3Des 对 getIMEI(context)+System.nanoTime()+SPUtils.getDeviceId()进行加密



4.4.1 context解释

在安卓(Android)开发中,Context是一个非常重要的概念,它代表了应用程序的当前状态信息。每个Android应用程序都有一个Context,它允许应用程序访问系统资源和执行各种操作。Context通常是由Android系统传递给应用程序的各个组件(如Activity、Service、BroadcastReceiver等),以便它们能够与系统和其他组件进行交互。

Context的主要作用包括:

# 访问资源:通过Context,您可以访问应用程序的资源,如布局文件、字符串、图片等。这是因为Context持有对应用程序资源的引用,使您能够在应用程序中加载和使用这些资源。

# 启动组件:通过Context,您可以启动其他组件,如Activity、Service、BroadcastReceiver等。例如,您可以使用Context启动一个新的Activity来打开新的界面。

# 获取系统服务:通过Context,您可以获取系统级别的服务,例如获取系统的传感器、网络状态、存储管理等。这些服务是通过系统提供的服务注册表(Service Registry)来获取的。

# 应用程序级别的操作:Context还可以用于执行应用程序级别的操作,如发送广播、获取应用程序包名、获取应用程序的数据目录等。

4.4.2 getIMEI(context)–uuidd

# 源码如下
public static String getIMEI(Context context) {
            
    # 1 查看当前app有没读取手机状态的权限
    if (PermissionsCheckerUtil.hasReadPhoenStatePermission(context)) {
            
        # 2 获得 了  IMEI,如果获得不到,执行if 内部的
        String imei = SPUtils.getIMEI(); # 去xml中取的
        if (imeiIsNull(imei)) {
            
            # 3 获取手机IMEI--》getDeviceId 得到
            imei = ((TelephonyManager) context.getSystemService("phone")).getDeviceId();
            if (imeiIsNull(imei)) {
            
                # 4 mac地址作为 imei
                String macAddress = ((WifiManager) context.getSystemService(NetworkUtil.NETWORK_TYPE_WIFI)).getConnectionInfo().getMacAddress();
                if (macAddress != null) {
            
                    try {
            
                    } catch (UnsupportedEncodingException e) {
            
                        e.printStackTrace();
                        context = getIMEIbyAndroidIDandUUID(context);
                    }
                    if (macAddress.length() > 0 && !isInBlackList(macAddress)) {
            
                        context = UUID.nameUUIDFromBytes(macAddress.getBytes("utf8")).toString();
                        imei = context;
                    }
                }
                # 5 如何还没有:就是 UUID.randomUUID().toString();
                context = getIMEIbyAndroidIDandUUID(context);
                imei = context;
            }
            if (!imeiIsNull(imei)) {
            
                # 6 存到xml
                SPUtils.saveIMEI(imei);
            }
        }
        return imei;
    }
    return "sssss";
}

# 2 代码逻辑是
	1 先从xml中取,如果之前存过,就有,第一次,没有
    2 使用getDeviceId 获取imei,如果用户没授权拿不到
    3 使用mac地址生成一个,如果用户没授权,拿不到
    4 使用uuid作为imei
    5 保存到xml中,以后直接用xml中的就可以
    
    
# 3 我们直接使用uuid随机生成--》作为imei


# 4 我们可以去xml中找出它来
	-包--》xx.xml-->imei
    去xml中寻找
        # 去手机中找出来 :imei
        # 保存到手机上:`/data/data/包名`
        adb shell
        su
        cd /data/data
        cd 包名
        cd shared_prefs
        ls
        cat sp_token.xml
        
        
 # 5 还可以通过hook得到  SPUtils.getIMEI();

4.4.3 System.nanoTime()

# android系统开机到当前的时间
# 注意,它跟java的这个函数返回值是不一样的
import random
nano_time = random.randint(5136066335773,7136066335773)

4.4.4 SPUtils.getDeviceId()

# 1 代码是:SPUtils.getDeviceId()
# 2  源代码是
public static String getDeviceId() {
            
        return getSpUtil().getString(KEY_DEVICE_ID, "");
    }
# 3 去xml中取的
public String getString(String str, String str2) {
            
    SharedPreferences sharedPreferences = this.mPreferences;
    return sharedPreferences != null ? sharedPreferences.getString(str, str2) : str2;
}

# 4 一定在某个位置写入xml中,找到写入的位置
public static void saveDeviceId(String str) {
            
    getSpUtil().saveString(KEY_DEVICE_ID, str);
}

# 5 查找用例:saveDeviceId

# 6 如下图:本质就是手机一安装app,第一次打开--》向一个地址发送请求--》得到deviceid,保存到xml中以后一直用

# 7 抓包--》拿到结果
	-清空数据再装--》app重新装再抓

# 8 固定值:381632

4.4.5 encode3Des加密逻辑

#1  最终组装成:
SecurityUtil.encode3Des(UUID的值 + "|" + 开机时间 + "|" + "381632")

    
    
# 2  encode3Des源码如下

public static String encode3Des(Context context, String str) {
            
    # 1 获取deskey
    String desKey = AHAPIHelper.getDesKey(context);
    byte[] bArr = null;
    if (TextUtils.isEmpty(desKey)) {
            
        return null;
    }
    try {
            
        // 2 获得des加密到的对象,把key传入
        SecretKey generateSecret = SecretKeyFactory.getInstance("desede").generateSecret(new DESedeKeySpec(desKey.getBytes()));
        Cipher cipher = Cipher.getInstance("desede/CBC/PKCS5Padding");
        // 3 des加密,需要iv
        cipher.init(1, generateSecret, new IvParameterSpec(iv.getBytes()));
        // 4 对字符串进行加密
        bArr = cipher.doFinal(str.getBytes("UTF-8"));
    } catch (Exception unused) {
            
    }
    // 5 转成字符串返回
    return encode(bArr).toString();
}

    
# 3 目标
	-明文:知道了
    -des的key值:通过so文件生成--返回的--JNI方法
    -des的iv值:private static final String iv = "appapich";

    
# 4 正常需要去破解 so文件--》找到des的key
# 5 正常来讲des的key是固定的---》hook试试--》看看AHAPIHelper.getDesKey(context)--》看返回结果是不是固定到的,如果是固定的,写死即可

4.4.6 hook-getDesKey得到key的值

import frida
import sys

rdev = frida.get_remote_device()

session = rdev.attach("车智赢+")

scr = """
Java.perform(function () {
     // 包.类
    var AHAPIHelper = Java.use("com.autohome.ahkit.AHAPIHelper");

    // Hook,替换
    AHAPIHelper.getDesKey.implementation = function(ctx){
        // 执行原来的方法
        var res =this.getDesKey(ctx);
        console.log("DesKey值:",res);
        return res;
    }
});
"""

script = session.create_script(scr)

script.load()
sys.stdin.read()import frida
import sys

rdev = frida.get_remote_device()

session = rdev.attach("车智赢+")

scr = """
Java.perform(function () {
     // 包.类
    var AHAPIHelper = Java.use("com.autohome.ahkit.AHAPIHelper");

    // Hook,替换
    AHAPIHelper.getDesKey.implementation = function(ctx){
        // 执行原来的方法
        var res =this.getDesKey(ctx);
        console.log("DesKey值:",res);
        return res;
    }
});
"""

script = session.create_script(scr)

script.load()
sys.stdin.read()

# DesKey值: appapiche168comappapiche168comap

4.4.7 python 还原des算法

# pip install pycryptodome
import base64
from Crypto.Cipher import DES3

BS = 8
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)

# 3DES的MODE_CBC模式下只有前24位有意义
key = b'appapiche168comappapiche168comap'[0:24]
iv = b'appapich'

plaintext = pad("cf15599b-5e93-3be5-a705-a39403227dfd|13359325995159|358908").encode("utf-8")

# 使用MODE_CBC创建cipher
cipher = DES3.new(key, DES3.MODE_CBC, iv)
result = cipher.encrypt(plaintext)
res = base64.b64encode(result)
print(res)

4.5 最终逆向 _sign

# 1 _sign 的生成逻辑
通过:{
            _appid=atc.android, appversion=3.56.0, channelid=csy, pwd=e10adc3949ba59abbe56e057f20f883e, signkey=, type=, udid=iEavSW4QycaRf6qULa18YDJEOmuw10SBwdVu62EN/1/djG80tEV6ibNjsxmv LTEy4VQJH+asjzMv3asLxv8ECA==, username=18952452114}
执行: SignManager.INSTANCE.signByType(i, treeMap)
得到字符串是 _sign的值
# 2 源码如下
public final String signByType(@SignType int i, TreeMap<String, String> paramMap) {
            
    # 1 new一个StringBuilder 为了拼接字符串
    StringBuilder sb = new StringBuilder();
    String str = KEY_V1;
    if (i != 0) {
            
        if (i == 1) {
            
            # 2 hook得到i是1
            #str = W@oC!AH_6Ew1f6%8
            str = KEY_V2;
        } else if (i == 2) {
            
            str = KEY_SHARE;
        } else if (i == 3) {
            
            str = KEY_AUTOHOME;
        }
    }
    # sb 里是:W@oC!AH_6Ew1f6%8
    sb.append(str);
    # 3 循环字典,把key和value拼接到sb中
    for (String str2 : paramMap.keySet()) {
            
        sb.append(str2);
        sb.append(paramMap.get(str2));
    }
    # 4 最后 又追加了W@oC!AH_6Ew1f6%8
    # sb=W@oC!AH_6Ew1f6%8_appidatc.androidappversion3.56.0...W@oC!AH_6Ew1f6%8
    sb.append(str);
    # 5 把这个字符串使用md5加密了
    String encodeMD5 = SecurityUtil.encodeMD5(sb.toString());
    if (encodeMD5 != null) {
            
        Locale ROOT = Locale.ROOT;
        Intrinsics.checkNotNullExpressionValue(ROOT, "ROOT");
        # 6 转成大写
        String upperCase = encodeMD5.toUpperCase(ROOT);
        Intrinsics.checkNotNullExpressionValue(upperCase, "this as java.lang.String).toUpperCase(locale)");
        if (upperCase != null) {
            
            return upperCase;
        }
    }
    return "";
}

4.5.1 python 实现_sign

import hashlib


def md5(data_string):
    obj = hashlib.md5()
    obj.update(data_string.encode('utf-8'))
    return obj.hexdigest()


data = "W@oC!AH_6Ew1f6%8"

data_dict = {
            
    "_appid": "atc.android",
    "appversion": "3.56.0",
    "channelid": "csy",
    "pwd": "e10adc3949ba59abbe56e057f20f883e",
    'signkey': '',
    'type': '',
    "udid": "iEavSW4QycaRf6qULa18YDJEOmuw10SBwdVu62EN/19krF/nMrwI9lhUNBOi yt5jRKYIti7S0+09BogZPwIk/g==",
    "username": "18953675221"
}

result = "".join(["{}{}".format(key, data_dict[key]) for key in sorted(data_dict.keys())])

un_sign_string = f"{
              data}{
              result}{
              data}"
sign = md5(un_sign_string).upper()
print(sign)

5 代码整合

import hashlib
import uuid
import random
import base64
from Crypto.Cipher import DES3
import requests
requests.packages.urllib3.disable_warnings()

# encode3Des 算法
def des3(data_string):
    BS = 8
    pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)

    # 3DES的MODE_CBC模式下只有前24位有意义
    key = b'appapiche168comappapiche168comap'[0:24]
    iv = b'appapich'

    plaintext = pad(data_string).encode("utf-8")

    # 使用MODE_CBC创建cipher
    cipher = DES3.new(key, DES3.MODE_CBC, iv)
    result = cipher.encrypt(plaintext)
    return base64.b64encode(result).decode('utf-8')

# md5加密
def md5(data_string):
    obj = hashlib.md5()

    obj.update(data_string.encode('utf-8'))

    return obj.hexdigest()


def run():
    username = "18953675227"
    passwrod = "1234567"

    imei = str(uuid.uuid4()) # 随机uuid
    nano_time = random.randint(5136066335773, 7136066335773)# 开机时间
    device_id = ''  # 可以为空,也可以358908
    udid = des3(f"{
              imei}|{
              nano_time}|{
              device_id}")

    data = "W@oC!AH_6Ew1f6%8"
    data_dict = {
            
        "_appid": "atc.android",
        "appversion": "3.56.0",
        "channelid": "csy",
         "pwd": md5(passwrod),
        'signkey': '',
        'type': '',
        "udid": udid,
        "username": username
    }


    result = "".join(["{}{}".format(key, data_dict[key]) for key in sorted(data_dict.keys())])
    un_sign_string = f"{
              data}{
              result}{
              data}"
    sign = md5(un_sign_string).upper()
    data_dict['_sign'] = sign

    res = requests.post(
        url="https://dealercloudapi.che168.com/tradercloud/sealed/login/login.ashx",
        data=data_dict,
        verify=False
    )

    print(res.text)


if __name__ == '__main__':
    run()

6 破解so找到des的key

# 1 des的key通过hook得到--》发现不变

# 2 读源码读到---》so生成---》逆向so文件,具体看看,它如何生成的

# 3 如下图 so的 函数
	-返回值是常量:appapiche168comappapiche168comap

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容