前排提示!!!!!
论坛禁止留联系方式!!
禁止求成品,也没有成品!
写在前面的话
前段时间已经研究过这个平台网页和APP的AES加解密方式了。
最近发现这个平台的APP的加解密方式更新了,所以又继续研究了下...
本贴用到的工具
1.反编译:jadx+NP管理器+Androidkiller(mt管理器的日志注入或Androidkiller的log/toast均可或别的工具均可)[jadx看java,NP管理器用来打印字符串APP,Androidkiller可以实时查看日志内容]
2.抓包:fiddler+安卓模拟器(这里用的是逍遥)
3.加解密网站(由于可能会...所以这里就不贴了)
抓包
1.打开APP抓包部分接口
设置好fd和模拟器的代{过}{滤}理端口之后,打开APP查看网络请求
接口 |
请求方式 |
提交内容 |
数据类型 |
(猜测)用途 |
host_XXXX.txt |
GET |
空 |
密文(暂不知道是什么加密) |
可能是返回能正常访问的主机群 |
/v1/initial |
GET |
空 |
明文 |
新版本检测+首页弹窗+视频分类 |
/v1/register/token |
POST |
device_id/platform/key/universal_id/lang |
明文 |
注册新用户,返回token和uid |
/v1/user/info |
POST |
token/lang/download_amount |
明文 |
查询并返回用户信息 |
/v1/firstpurchase |
GET |
空 |
明文 |
买VIP悬浮广告 |
2.随意点一个视频查看fd的数据
由图看出:
之前的m3u8处理方法 |
现在的m3u8处理方法 |
链接加密,m3u8明文 |
链接明文拼接,m3u8密文 |
(PS:图中能看到http://localhost:1500/video?mode=的请求,其实关闭模拟器的fd代{过}{滤}理之后,用浏览器打开这个网址.看到的就是解密后的m3u8的内容,不过这个m3u8文件是需要处理后才能用下载器下载的)
(再PS:这个m3u8的headers中还有和网页版中的X-VTag参数)
那我们就来用工具解密下m3u8
反编译app解密m3u8
把app拉入jadx
搜"X-VTag"
用jadx搜"X-VTag",发现只有一个a.b.i.a.h.a,点进去看看代码
以下是a.b.i.a.h.a截取的部分代码
String a5 = tVar.a("X-VTag");/* ①.a5就是X-VTag的值,我测试的视频X-VTag是825497755 */
if (a5 == null || (i = a.b.f.o.f.i(a5)) == null) {/* ④.i=a.b.f.o.f.i,那么a.b.f.o.f.i是什么操作呢?我们用jadx跳到声明 */
str = null;/* ②.如果a5或i是null的那str=null */
} else {
str = i.substring(8, 24);/* ③.否则str= 从i的第9位取到第24位,i的长度为16 */
i.a((Object) str, "(this as java.lang.Strin…ing(startIndex, endIndex)");
}
h0 h0Var2 = a2.g;
w c = h0Var2 != null ? h0Var2.c() : null;
if (!(str == null || c == null)) {
h0 a6 = h0.a(c, a.b.j.v3.a.a(CipherClient.decodeKey(), str, a3));
g0.a aVar2 = new g0.a(a2);
aVar2.g = a6;
g0 a7 = aVar2.a();
i.a((Object) a7, "response.newBuilder().body(body).build()");
return a7;
}
a.b.f.o.f.i函数
public static final String i(String str) {
if (str != null) {
try {
MessageDigest instance = MessageDigest.getInstance(AESEncryptor.HASH_ALGORITHM);/* AESEncryptor.HASH_ALGORITHM跳转后是MD5 */
byte[] bytes = str.getBytes(z.z.a.f8176a);
i.a((Object) bytes, "(this as java.lang.String).getBytes(charset)");
instance.update(bytes);
byte[] digest = instance.digest();
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
String hexString = Integer.toHexString(b & 255);
while (hexString.length() < 2) {
hexString = '0' + hexString;
}
sb.append(hexString);
}
String sb2 = sb.toString();
i.a((Object) sb2, "hexString.toString()");
return sb2;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return "";
}
} else {
i.a("$this$toMd5");
throw null;
}
}
看这个操作可以结案了,a.b.f.o.f.i函数就是把str参数md5处理了下
所以可知案例中的部分参数
变量 |
解析 |
值 |
a5 |
headers中的X-VTag的值 |
825497755 |
i |
md5(a5) |
9807a3e5370512bab61bee56200ee1d6 |
str |
i.substring(8, 24) |
370512bab61bee56 |
我们继续往下看a.b.i.a.h.a
h0 a6 = h0.a(c, a.b.j.v3.a.a(CipherClient.decodeKey(), str, a3));
先跳转看这个CipherClient.decodeKey()函数
public static final String decodeKey() {
String str = CipherCore.get("aa01bdd83d0f12833ddaea2f2af22865");
return str;
}
我们接着看CipherCore.get函数,跳转就跳到了net.idik.lib.cipher.so.CipherCore类中
package net.idik.lib.cipher.so;
public final class CipherCore {
static {
System.loadLibrary("cipher-lib");
init();
}
public CipherCore() throws IllegalAccessException {
throw new IllegalAccessException();
}
public static String get(String str) {
return getString(str);
}
public static native String getString(String str);
public static native void init();
}
我去,这样一看CipherCore.get的方法在cipher-lib.so中??
这伪代码我也看不懂呀....不过不用怕.这个函数有返回值咱就不怕.
我们尝试用NP管理器的NPPrintFuncSrc打印出CipherClient.decodeKey()和其他函数的值(PS:也可以用Androidkiller的toast或者log,别的方法也均可)
smali代码是这样的:
.method public static final decodeKey()Ljava/lang/String;
.registers 1
const-string v0, "解密-decodekey"
invoke-static {v0}, Lnp/NPLogEncryptString;->NPPrintFuncSrc(Ljava/lang/String;)V #打印"解密-decodekey"
const-string v0, "aa01bdd83d0f12833ddaea2f2af22865"
.line 1
invoke-static {v0}, Lnet/idik/lib/cipher/so/CipherCore;->get(Ljava/lang/String;)Ljava/lang/String;
move-result-object v0
invoke-static {v0}, Lnp/NPLogEncryptString;->NPPrintFuncSrc(Ljava/lang/String;)V #打印decodeKey返回的字符串
return-object v0
.end method
java代码是这样的:
/* 以上省略大部分代码 */
public static final String decodeKey() {
NPLogEncryptString.NPPrintFuncSrc("解密-decodekey");
String str = CipherCore.get("aa01bdd83d0f12833ddaea2f2af22865");
NPLogEncryptString.NPPrintFuncSrc(str);
return str;
}
/* 以下也省略大部分代码 */
在Androidkiller中显示是这样的
按类似的方法成功获取到部分常量如下:
函数 |
常量 |
获取到的结果 |
apiHashKey |
d708e111b5db90af74ef84ff4d5e647b |
666wInteriscommingyoUshouldNotpassmotherfuckeR= |
ccToken |
810d903a88254b27c643e0bc471d406a |
空 |
decodeKey |
aa01bdd83d0f12833ddaea2f2af22865 |
6e561ccd4aade2fed458d4da61e76770 |
encodeType |
e82b6153b4ea2340333e2254c3553d03 |
空 |
everIv |
c5b287eb7ee64e90ed015bac470f4b6b |
BakinsodaIgotbakinsoda |
everKey |
0d7cb519eb483a597549f7f466b189bc |
iaMiNloveWithtHecoCo |
hostIv |
6cf9e96524081ac264dc31982d0be319 |
5e8bf1f958f56644 |
hostKey |
51cdba173d412fdecec3e78572cde731 |
f332ae8214fcbb0d98f8626f123459b4 |
imageDecodeIv |
9e1add49a87568f90c43e418e7370287 |
E01EDE6331D37AFCC7BE05597D654D22 |
imageDecodeKey |
a8ae2831cbea74111bc5116ba81ec191 |
B2F3842866F9583D1ECE61C4E055C255 |
registerKey |
951eeb6b19b70177fd25706aa620edcf |
空 |
我们再继续回去看a.b.i.a.h.a
h0 a6 = h0.a(c, a.b.j.v3.a.a('6e561ccd4aade2fed458d4da61e76770', '370512bab61bee56', a3)); /* decodeKey()的值为6e561ccd4aade2fed458d4da61e76770,这个a3我猜测是密文 */
那么a.b.j.v3.a.a是啥呢,我们继续跳声明
a.b.j.v3.a.a函数
public static String a(String str, String str2, String str3) throws UnsupportedEncodingException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException {
return a(new IvParameterSpec(str2.getBytes("UTF-8")), new SecretKeySpec(a(str).getBytes("UTF-8"), "AES"), str3);
}/* str=6e561ccd4aade2fed458d4da61e76770,str2就是a.b.i.a.h.a中的str=370512bab61bee56 */
又return a(IvParameterSpec, SecretKeySpec(a(str).getBytes("UTF-8"), "AES"), str3) / str3=a3猜测应该是密文,加密方式是AES /
又出现两个未知数,我们一个一个的解
解析a(str)
跳转声明发现这个a函数(a.b.j.v3.a)和a.b.f.o.f.i的函数是一致的,均为md5算法,那么a(str)=key=md5('6e561ccd4aade2fed458d4da61e76770')=ae52f7ffd2dd66ba5743bb180188b991
解析return a(x,x,x)
public static String a(IvParameterSpec ivParameterSpec, SecretKeySpec secretKeySpec, String str) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
Cipher instance = Cipher.getInstance(AESEncryptor.AES_MODE);/* AESEncryptor.AES_MODE的值跳转后可得是加解密方法为"AES/CBC/PKCS5Padding" */
instance.init(2, secretKeySpec, ivParameterSpec);
return new String(instance.doFinal(Base64.decode(str, 2)));
}
套入a.b.i.a.h.a中,那么本案例中的iv就是'9807a3e5370512bab61bee56200ee1d6'.substring(8, 24)=370512bab61bee56
尝试用在线aes解密和下载
成功解密m3u8
我们新建一个txt文档,把解密后的明文粘贴进去,保存关闭后,文件重命名为1.m3u8,拖入下载器,成功下载
最后结案
m3u8相关
貌似算法和网页版的几乎一样?
切记获取的时候要协议头要带上version/platform/time/userid/hash 不然会拒绝访问的..
关于hash的加密算法在a.b.a.v.f中,用到了CipherClient.apiHashKey()
和一开始不加密的那种m3u8格式一样,但是现在m3u8是加密的需要解密:
m3u8地址:#域名#/media/#分辨率#/#视频id#.m3u8?token=#token#
加解密模式 |
key |
iv |
AES/CBC/PKCS5Padding |
固定值:ae52f7ffd2dd66ba5743bb180188b991 |
md5(#X-VTag#).substring(8, 24) |
PS:我用易语言的e2ee先Base64解码密文即可解密出明文..在线解密也可以解出的
请求头中hash值的算法
发现在请求m3u8密文的时候会经常提示拒绝访问access denied
看一下请求头中的参数
参数 |
值 |
来源 |
version |
2.3.1 |
app版本 |
platform |
Android |
安卓平台 |
time |
1614062660 |
当前十位时间戳 |
userid |
AiwrHWxoYtGd |
可能是随机值 |
hash |
625698fd47f50c2c026b9cfe543d56c5 |
待探索 |
如果随便改的话也会提示拒绝访问access denied,所以我们去jadx中搜以下这个hash。
用jadx搜"hash"
所以i=hash的值,往上翻,找到i的赋值
String i = a.b.f.o.f.i(a2 + value + valueOf + str + str2);
之前我们已经得出a.b.f.o.f.i是md5的操作。
依次看a2 / value / valueOf / str / str2
a2 = version的值 = APP版本号
value = platform的值 = 平台Android
valueOf = time的值 = 时间戳
str = userId的值 = 从"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"取随机字符
str2 = CipherClient.apiHashKey() = ‘666wInteriscommingyoUshouldNotpassmotherfuckeR=’
所以i = md5(app版本 + 平台Android + 时间戳 + 随机userId + ‘666wInteriscommingyoUshouldNotpassmotherfuckeR=’)
上述案例中 i = md5(2.3.1Android1614062660AiwrHWxoYtGd666wInteriscommingyoUshouldNotpassmotherfuckeR=) = 625698fd47f50c2c026b9cfe543d56c5
hash算法结案
host相关
由于我们以及得到了hostIv和hostkey,所以弯回去看看host_XXXX.txt,在线解密结果:
关于倍速播放
正常情况下倍速播放只能是vip才有的功能,那么我们可以通过固定/v1/user/info接口中返回的Expiry的值即可免费使用该功能
.method public final getExpiry()J
.registers 3
.line 1
const-wide v0,0x5af3107a3ff6L #0x5af3107a3ff6L
return-wide v0
.end method