原文件下载路径:
【移动安全与逆向工程】车智赢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
暂无评论内容