教程仅供学习交流,学习研究软件设计思路,请勿用于任何非法用途
去更新
在解包后的软件文件夹中,从smali代码中查找“1.2.0”,将其全部替换为9.9.9


修改登录接口
自v2版本更新后,v1版本的登录接口就无法登陆了,但是想开挂只能用v1,所以通过抓包拿到v2版本的登录接口替换给v1
抓包拿到v2的登录接口

去除域名,得到剩余的接口地址
1
| run-front/account/login_v2
|
同理,再抓到v1版本的登录接口,将v1接口替换为v2即可
1 2 3 4 5
| // v1 run-front/account/login // v2 run-front/account/login_v2
|


过wi-fi检测
搜索关键词,并复制搜索到的关键词变量名

搜索变量名,查找调用处,在RunningSettingsActivity和GPSOptimizeActivity类中调用了该字符串

分析RunningSettingsActivity类中的代码

跳转到isForbiddenWiFi()方法所在的类,分析发现isForbiddenWiFi()返回this.forbiddenWiFi,而setForbiddenWiFi()给this.forbiddenWiFi赋值,只要在setForbiddenWiFi()方法中永远给this.forbiddenWiFi赋值为false就好了


过root检测
搜索关键词,并复制搜索到的关键词变量名,留意一下上边的cheat_warn_1到cheat_warn_5

搜索变量名cheat_warn_running,发现HomeActivity和RunningDetailActivity两个类调用了它,很可惜两个类都和过root检测没有直接关系,但是两个类中的代码可以确定,cheat_warn_running和cheat_warn_1到cheat_warn_5是有直接关系的,二者必定同时出现

通过第一部的搜索可知,cheat_warn_1 = “在root环境下使用闪动校园”,所以去到代码中搜索cheat_warn_1,发现SunshineRunningFragment类和FreeRunningFragment类也在调用它,把这两个类名翻译为中文,又刚好对应软件的“阳光跑”和“自由跑”

打开对应阳光跑的类看下,一目了然,而自由跑中除了没有wifi检测外,也一模一样

也就是说,只要RootDetectUtil.b().a()永远返回false,将不会检测root环境

过虚拟机检测
有了上面的经验,过虚拟机检测就比较简单了,只要让这个判断不进入if代码块就好

在SunshineRunningFragment中加入一行const/4 v0, 0x0

同理,FreeRunningFragment中,但是之前的完成品中并没有过掉自由跑的虚拟机检测,因为我忘了

违规时不记录
搜索字符串cheat_warn_running,在HomeActivity的onEvent方法中

使paramCheatStopEvent值为null,则不进入方法体

搜索字符串cheat_warn_running,在RunningDetailActivity的O方法中,使i = 0


搜索字符串cheat_warn_1,在SunshineRunActivity类的e方法中

使schoolRule变量值为null

过35秒检测
闪动校园在开始运动的第35秒必定会有一次检测,这里提供一个简单的解决思路
1 2 3 4 5 6 7
| 35s == 35000ms 35000(10进制) == 88B8(16进制)
1h == 3600s == 3600000ms 3600000(10进制) == 36EE80(16进制)
找到88B8修改为36EE80就好了
|

非法请求修复
2021年11月21日,官方在服务器添加了版本验证,其实具体是怎么个验证法我现在都搞不清,因为之前apk包中的版本号全部修改为9.9.9,所以请求数据时提交的json数据中也是9.9.9版本,此时接口将会返回“非法请求,验签不通过”并在app中Toast
所有的数据请求接口,包括但不限于:用户资料、学校阳光跑要求条件、学校自由跑要求条件,都被添加了这种验签,所以必须修复

我原以为要重构它的json拼接方式和解析方式,忙活了一天没有半点进展,误打误撞下,将其中一个版本号从9.9.9修改成了2.1.1(当前最新版本2.1.1),成功了,惊喜之外

sign验签算法逆向
“非法请求,验签不通过”的原因:
- 版本号超出当前最新版本版本号或版本号格式错误
- post中的提交的json和sign值不对应,sign值由json加密获得
- 2.2.1版本前与2.2.1版本由于加密key不同所以得到的sign值不同,即服务器验证中json与sign不对应
2.2.1版本前
定位关键词
这里使用1.2.0版本演示
搜索关键词”sign”,1.2.0版本共有两处,在huachenjie.sdk.http.e包的b类a(K, B,Map)方法中出现了一次,将”sign”修改为”signs”,抓包会发现post中的sign响应头也变成了”signs”,所以可以猜测,a(K, B,Map)中调用的a(String, B, Map, String)添加了”sign”响应头和对应的sign值,那么它一定与sign值有关联

分析关键词调用方法
分析一下调用a(String, B, Map, String)方法传入的4个参数
1、String值”sign”: 不用多说,一个响应头的键名而已
2、B类型paramB: 该类中的String[]类型的a数组记录了所有的键名和键值,包括post中的sign等响应头的键值对,以及post的json中的键值对,B类的构造函数为其赋值,将一个集合转换为数组并赋值给a数组

2.1、而集合的值来源于构造函数中传入的a类的a变量,a类是B.class文件中的一个内部类,它的a变量记录了一个String类型的集合,该集合作为post中所有数据的源头,该内部类中的b(String,String)方法为其题添加值,b(String)方法为其删除值


2.2、总而言之,传入B类的最大作用就是可以通过它调用post中所有的数据
3、强转为Map类型的hashMap: 在当前方法中刚创建的,主要用于记录sign的值,当前方法被调用时return出去
4、b(paramMap): 通过添加Log,Map类型的paramMap转为String后竟然是post提交的json值,那么有极大可能b(Map)方法返回的String字符串就是sign值
分析json加密
将传入的Map转换成了String赋值给str,这个str就是post中的json值,str传入了f类的b(String)方法

f类的b(String)将传入的json值提交给a(String,String)方法,一并提交的还有”SHA-256”,能看出来对json值进行了一次SHA-256加密

跳转到a(String,String)方法,果然先经历了一次SHA-256加密,开始我还以为是base64
具体代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| public class f { public static String a(String paramString) { try { MessageDigest messageDigest = MessageDigest.getInstance("md5"); messageDigest.update(paramString.getBytes("UTF-8")); byte[] arrayOfByte = messageDigest.digest(); StringBuilder stringBuilder = new StringBuilder(); this(arrayOfByte.length * 2); int i = arrayOfByte.length; for (byte b = 0; b < i; b++) { int j = arrayOfByte[b] & 0xFF; if (j < 16) stringBuilder.append("0"); stringBuilder.append(Integer.toHexString(j)); } return stringBuilder.toString(); } catch (NoSuchAlgorithmException noSuchAlgorithmException) { noSuchAlgorithmException.printStackTrace(); } catch (Exception exception) { exception.printStackTrace(); } return ""; } private static String a(String paramString1, String paramString2) { String str1; boolean bool = TextUtils.isEmpty(paramString1); String str2 = ""; if (bool) return ""; try { MessageDigest messageDigest = MessageDigest.getInstance(paramString2); messageDigest.update(paramString1.getBytes()); byte[] arrayOfByte = messageDigest.digest(); StringBuffer stringBuffer = new StringBuffer(); this(); for (byte b = 0; b < arrayOfByte.length; b++) { String str = Integer.toHexString(arrayOfByte[b] & 0xFF); if (str.length() == 1) stringBuffer.append('0'); stringBuffer.append(str); } str1 = stringBuffer.toString(); } catch (NoSuchAlgorithmException noSuchAlgorithmException) { noSuchAlgorithmException.printStackTrace(); str1 = str2; } return str1; } public static String b(String paramString) { return a(paramString, "SHA-256"); } }
|
根据这个加密代码编写一个用例测试一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| String json_content = "{"appCode":"SD001","appVersion":"1.2.0","buildVersion":"13","deviceId":"ffffffff-ffff-ffff-ffff-ffffffffffffffffffff-ffff-ffff-ffff-ffffffffffff","modelName":"Phone|test 1","platform":"2","systemVersion":"11","timestamp":"1637652538168","userId":"201225*****661220"}";
String type = "SHA-256";
private String SHA256Encryption() { String value; try { MessageDigest messageDigest =MessageDigest.getInstance(type); messageDigest.update(json_content.getBytes()); byte[] arrayOfBy = messageDigest.digest(); StringBuffer stringBuffer = new StringBuffer(); for (int i = 0; i < arrayOfBy.length; i++) { String str =Integer.toHexString(arrayOfBy[i] & 0xFF); if (str.length() == 1){ stringBuffer.append('0'); } stringBuffer.append(str); } value = stringBuffer.toString(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); value = null; } return value; }
|
得到加密值,但是并不是最终值,那说明还有一步加密,肯定就在刚刚f.b(String)作为参数的a.b(String,String)方法中
1
| 3f5711c7f2e4880579fc0f2286c1578b031087ccd86d8af099fdfc40d1697db4
|
分析key值获取
现在已知,f.b(String)就是SHA-256加密后的json值,在a.b(String,String)中还有一次加密,想要弄明白a.b(String,String),先来分析传入的第二个参数a.a()
解知,a.a()方法返回一个字符串,字符串源自其静态a方法中的第一个参数,图中有一个错误。上面的箭头应该指向b变量却指向了d变量

通过查找知,b的值为F44B0282BEA83557,在某些特殊情况下为huachenjie(不需要考虑)
分析AES加密
现已知,f.b(String)是SHA-256加密后的json值,a.a()返回一个不长的字符串,盲猜是加密key
现在来分析a.b(String,String),豁,好家伙了,AES加密,怪不得那么像base64

其中调用的一个变量和一个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| this(a.getBytes("UTF-8"));
public static String a = "01234ABCDEF56789"; return b.a(cipher.doFinal(paramString1.getBytes("UTF-8")));
public static String a(byte[] paramArrayOfbyte) { if (paramArrayOfbyte != null) { String str1; try { String str2 = new String(); this(Base64.encode(paramArrayOfbyte, 2), "utf-8"); str1 = str2; } catch (UnsupportedEncodingException unsupportedEncodingException) { unsupportedEncodingException.printStackTrace(); str1 = ""; } return str1; } String str = ""; }
|
编写用例,调试成功😊
2.2.1版本更新
当前的最新版本了,不确定以后会不会在这个基础上作出修改
定位关键词、分析调用方法这些都是一样的,从SHA-256加密json值和获取key值开始说
分析json加密
相对于1.2.0有所变动,但是无伤大雅,依然可以直接抄
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
| public class f { private static String a(String paramString1, String paramString2) { String str1; boolean bool = TextUtils.isEmpty(paramString1); String str2 = ""; if (bool) return ""; String str3 = str2; try { MessageDigest messageDigest = MessageDigest.getInstance(paramString2); str3 = str2; messageDigest.update(paramString1.getBytes()); str3 = str2; byte[] arrayOfByte = messageDigest.digest(); str3 = str2; StringBuffer stringBuffer = new StringBuffer(); str3 = str2; this(); byte b = 0; while (true) { str3 = str2; if (b < arrayOfByte.length) { str3 = str2; String str4 = Integer.toHexString(arrayOfByte[b] & 0xFF); str3 = str2; if (str4.length() == 1) { str3 = str2; stringBuffer.append('0'); } str3 = str2; stringBuffer.append(str4); b++; continue; } str3 = str2; String str = stringBuffer.toString(); str3 = str; str1 = str; if (str.length() >= 16) { str3 = str; String str4 = str.substring(0, 8); str3 = str; str2 = str.substring(str.length() - 8); str3 = str; StringBuilder stringBuilder = new StringBuilder(); str3 = str; this(); str3 = str; stringBuilder.append(str2); str3 = str; stringBuilder.append(str.substring(8, str.length() - 8)); str3 = str; stringBuilder.append(str4); str3 = str; str1 = stringBuilder.toString(); } return str1; } } catch (NoSuchAlgorithmException noSuchAlgorithmException) { noSuchAlgorithmException.printStackTrace(); str1 = str3; } return str1; } public static String b(String paramString) { try { MessageDigest messageDigest = MessageDigest.getInstance("md5"); messageDigest.update(paramString.getBytes("UTF-8")); byte[] arrayOfByte = messageDigest.digest(); StringBuilder stringBuilder = new StringBuilder(); this(arrayOfByte.length * 2); int i = arrayOfByte.length; for (byte b = 0; b < i; b++) { int j = arrayOfByte[b] & 0xFF; if (j < 16) stringBuilder.append("0"); stringBuilder.append(Integer.toHexString(j)); } return stringBuilder.toString(); } catch (NoSuchAlgorithmException noSuchAlgorithmException) { noSuchAlgorithmException.printStackTrace(); } catch (Exception exception) { exception.printStackTrace(); } return ""; } public static String c(String paramString) { return a(paramString, "SHA-256"); } }
|
分析key值获取
解知,a.c()方法返回一个字符串,字符串源自其静态d方法中的第二个参数,从1.2.0的第一个参数转变成了第二个

这里就他妈比较烦人了槽
通过查找知,key值来源于com.huachenjie.shandong_school包中ShandongApplication类c()方法中huachenjie.sdk.http.k.a.a.d(str3, str1, “”, false, false, arrayList)的第二个变量str1

而str1来源于e()方法,e()方法返回的k.b2s(byte[], int)中就是key值了。如果删除了他们定义的那张图片,那么bitmip就为null,这个方法就会像1.2.0那样返回F44B0282BEA83557,结局就是验签失败

跳转到b2s,是个native方法🙂

而我,看不懂so文件里的汇编🙂

分析AES加密
没变,不分析了
