不知不觉,运动世界校园已经到了3.0版本,从运动世界校园出生之日起分析到现在也属实是一件不容易的事情,此博客仅作为学习总结,请勿用于商业用途!
目录
软件加密壳探测
方法一:编写静态脱壳机
方法二:强制性反编译+猜测性逻辑分析
反编译APP
找到加密代码
分析前的吐槽:评分还是这么低啊(手动狗头)
运动世界校园3.0版本已经使用了爱加密企业级加固,这种级别的加固如果要脱壳已经是非常非常非常(省略N个)困难了。想要脱掉这种壳,必须要手动编写静态脱壳机,还必须对还原加密算法有深度应用,这对逆向技术要求十分的高,所以除了这种方法之外,下面我还会讲另一种比较一般的方法。
爱加密流程的分析图+壳运行的流程
这种方法如果是完全不懂的人,也不是我三言两语就能说明白的,但我还是简单提一下。
编写静态脱壳机时,加壳时将方法指令抽走后自定义格式加密存放在assets/ijiami.ajm文件中,通过IDA动态调试分析发现每个被抽走的方法的debuginfo值改成0X3F开始的8字节大不的值,该值在还原时做为Map的KEY。
写静态脱壳机要还原解密算法,它的数据又做了多重加密所以要还原多个算法,其实算法本身并不复杂,但爱加密壳做了混淆,所以十分难缕清程序的逻辑,需要十分长的时间,这里不再赘述,重点讲一下方法二。
对于运动世界校园这个软件来说,我们关键要能够伪造数据,然后将伪造的数据上传到它的服务器,也就可以轻而易举的获得成绩。
对于这个目标,我们整体思路就是对上传操作时的网络数据进行抓包分析,然后根据抓包内容编写出一个一模一样的数据格式进行发包就能够达到我们的目的。(当然这过程中还有很多的问题需要解决)
POST /api/v40/login HTTP/1.1
Authorization: Basic MTcyMDE3MTk0OmxpY2hlbmcy
Accept: application/json
appVersion: 3.0.0
headerSign: Mp2zSKRA2ojvNYJzgJjaVT+QnJKUDyx21hym4w01DymquZmyn2R6SGA0YCpfV6UnTMpQppEr1w+dhmjs9yiGgq3OImQZbAgnx8+vA5nb4ItAqSB/MtuNgNAM/xWHpiGeuHUcJKOXNVJ0cbyhSREj5z7M600KjFA8+q/UDp+8cg/h9C47ugriVLcfI/8WW/AHUTlvVNXI0XqvzOV9tZjsFZGayhXM41O+Hxx0uz1ili3/2C9nvU/woVMKU0OFCAxlyyUEuY0SEA9CYPFo7vGSVWDFrQfw0NPxJJdXHA77Q66uoBuQ7riFtpsr00pezPjPSL69eTkJnH5iWeP+Tfeo6f0H3TYnTpa4xs9Bk7DJS0YnzPq9Q4YUGnOQetftiRWSCARO7oO0ap9EDaNwcOYOoX1xdq6HB/fW2LjXrkhvnHcwJEoxoHn9j719TH5iWYlhesyzzDigwYr13W/vC2lz4POxViVKePueqfIq8Tlb002NGFGT/+PEcI+3M2UYoFnYiKK69L0yQ810cmmN1AZZD9Ero1h1eek7UR6DVwD8uGuos5mKGBDvlhjoMa2JLHpT+GZX6wXrsSJI7dW0XKt0/T9TexMA8RC0KiRZAmKSHV/L01a0Frg30EjPhVJrTljhqYLaG2WV3J00e9Y22bQegzl3CVG66xpv4IJYy8dxo9DVokxL1rjs2Ap20vrRy2O4LTh/LCKXgBTviZAxQZTdI5EN0LjaqtJo5WrMFifaZWeFAaTOmqmFUH7pJ0OGglylT0c1aGn/1zou4pSI0wSV+g==
Content-Type: application/json;charset=UTF-8
Content-Length: 300
User-Agent: Dalvik/2.1.0 (Linux; U; Android 9.0.0; MI 10保时捷限量版 MIUI/11.0.3)
Host: run.gxapp.iydsj.com
Connection: Keep-Alive
Accept-Encoding: gzipKDBmvDfRrk5yHdCjd+nQZ2pxKdjBQOXntl5NBgH2YNdhPHsVVc4iP0jAPQ3OXs9ljZEMfXqGvd3UUI3p4ApzhirETou/kT5HP5zdN05OE3ZaHwrobjE1+4JMQzzoYPbI+73DSbfUGubo1HBvtGixD+UULBVXu/yl0ETXZceUnwnkb6A+eGA36kUj1VUNCyVov0TNqA9WYPhAdYcA+/9f3TXpl/9Tfc4lJn/ScEF2vuZKehEYw7ggwVmphc5K+/QuVs9FjE4UpHUBGie4xu6Lu09HNWhpydc6LuKUiNMExfo=
上面是一个登录时的请求抓包,我们可以发现,至少有两处地方进行了加密(标红处),另外我对响应也进行了抓包,同样是加密了的,做到这里是不是感觉就无解了。
其一,你不知道原始数据长什么样
其二,就算知道原始数据长什么样你也不知道怎么加密,因为后端只接受加密的数据,你不发同样类型的数据过去它肯定是要报错的。
其三,你不知道后端是怎么给数据解密的。
难道我们就真的无解了吗?其实并非如此。
其一:我们丝毫不关心后端是怎么给数据解密的,因为我们只要能够发送解密数据就完事。
其二:原始数据和加密方式是在APP里面进行封装的,这就是为什么要对APP进行加固混淆代码
所以我们的目标现在变成了:反编译APP => 找到加密的代码,分析出加密的算法进行实现 => 找到封装数据的代码,用分析出的加密算法对数据格式进行加密
反编译的工具有很多,我在这里使用jd-gui,需要的话还请自行百度,具体是怎么反编译的我不赘述,想要学习的请自行查找资料
反编译后结构如下图,我们毫不犹豫的就去找utils包,其他的根本不用看(工具类是做什么的相信大家都懂)
打开utils包之后,就能发现代码基本都被混淆过了,但是我们一眼就能看到一个HttpUtil还是认识的,而且一看名字用于就是网络请求的工具,所以我们先打开来看看。
点进去就发现了一个惊喜,下列代码是不是很像数据格式的封装?(代码是截选),但是我们先把这个放一边,先找找加密代码
private static Map getHeaderMap(Context paramContext)
{
....//省略代码
ArrayMap localArrayMap1 = new ArrayMap();
localArrayMap1.put("osType", "0");
localArrayMap1.put("DeviceId", O0000o.O0000OOo().get("DeviceId"));
localArrayMap1.put("osVersion", O0000o.O0000OOo().get("osVersion"));
localArrayMap1.put("deviceName", O0000o.O0000OOo().get("deviceName"));
localArrayMap1.put("IMEI", O0000o.O0000OOo().get("IMEI"));
localArrayMap1.put("logicPixel", O0000o.O0000OOo().get("logicPixel"));
localArrayMap1.put("physicPixel", O0000o.O0000OOo().get("physicPixel"));
localArrayMap1.put("androidId", O0000o.O0000OOo().get("androidId"));
localArrayMap1.put("blMac", O0000o.O0000OOo().get("blMac"));
localArrayMap1.put("wifiMac", O0000o.O0000OOo().get("wifiMac"));
localArrayMap1.put("cpuModel", O0000o.O0000OOo().get("cpuModel"));
localArrayMap1.put("isRoot", O0000o.O0000OOo().get("isRoot"));
localArrayMap1.put("appUpdateTime", O0000o.O0000OOo().get("appUpdateTime"));
localArrayMap1.put("appInstallTime", O0000o.O0000OOo().get("appInstallTime"));
localArrayMap1.put("nonce", O00Ooo00.O00000o());
localArrayMap1.put("timeStamp", str3);
localArrayMap1.put("CustomDeviceId", str2);
localArrayMap1.put("uid", String.valueOf(i));
localArrayMap1.put("token", str1);
localArrayMap1.put("tokenSign", str4);
return localArrayMap1;
}
很快我们就可以发现跟headerSign有关的加密代码,正是其中的JMEncryptBoxByUnidirectional.encryptToBase64(new JSONObject(localMap).toString(), EncryptionMode.RandomMode));
private static void setHeader(Context paramContext, RequestParams paramRequestParams, Map paramMap)
{
paramRequestParams.setAsJsonContent(true);
paramRequestParams.setHeader("Accept", "application/json");
paramRequestParams.setHeader("Content-Type", "application/json");
paramRequestParams.setHeader("appVersion", (String)O0000o.O0000OOo().get("versionName"));
Map localMap = getHeaderMap(paramContext);
if (paramMap != null)
localMap.putAll(paramMap);
try
{
paramRequestParams.setHeader("headerSign", JMEncryptBoxByUnidirectional.encryptToBase64(new JSONObject(localMap).toString(), EncryptionMode.RandomMode));
localMap.clear();
}
catch (Exception localException)
{
localException.printStackTrace();
}
paramRequestParams.setConnectTimeout(15000);
}
接下来,我们从他import的类中找到相关的这两个类
但是我们会发现这两个类并不在我们反编译过后的文件里面,这是很正常的,因此它必然放在了.so文件中,采用接口的方式进行调用。(接口代码进了混淆),所以我们先看一下它的.so文件是个什么样的情况。
我用360压缩打开了apk文件,打开lib文件夹,扫一眼大概,找一找跟encrypto有关的文件,大致如下,其中media_data_crypto盲猜是媒体加密,就先不看了,先整上面两个。
先从第一个开始分析,在搜索关键词中输入encrypt筛选一下方法,可以找到大量的加密方法
注意,我们在上文中说到的加密方法中有一个参数是这样的
这个方法的意思很显然是 => 使用随机加密模式,那么既然是【随机】,因此加密方法不止一个,所以跟上述我们找到的大量加密的方法显然是相符合的。那么问题就来了,经过筛选总共有81种加密方式,难道我们要对每一种加密方式都进行分析吗?
本文待更....