早上起早了没事打开jeb说来分析一下安全牛的app,结果又是一天的时间·····最后一次,再手贱是狗!
注
- 本篇只是技术分享,请勿用于盗版侵权
(老大人那么好,盗版良心会痛啊)。
- 复习要紧,没什么时间了,下面主要贴代码,长话短说!
- 我没有vip买的课也都过期了,不要问我要资源,有也不给(ε=ε=ε=┏(゜ロ゜;)┛)
- 以下代码不够晚上,自动化程度不高,有兴趣可以改,反正希望保存的视频自己珍藏就好了,不要传播。
分析
1.下载的视频被保存在/sdcard/edusoho
目录下,下级目录根据用户id与课程,网校名分类。
2.下载的视频为加密分段存储,每段几百kb:
3.此目录下只会保留视频文件,m3u8的列表信息和key保存在数据库/data/data/com.edusoho.kuozhi/databases/edusoho
里:
4.密钥没有加密,分段的视频文件被加了两次密:
才开始看到密钥文件刚好16字节还很开心,但是无法解密视频,接着看密钥数据格式有点奇怪,莫非两个连在一起用?结果猜错了,就只好乖乖逆算法,才开始看得脑壳疼,发现它是本地搭建一台web服务器,返回用户请求的数据,很明显就是m3u8列表文件(在数据库的data_m3u8
表中)那种样子的请求:
那么猜测要么服务器返回没有任何问题,再由客户端解密,要么服务端返回解密后的数据,于是就用tcpdump -i lo
抓包:
经对比发现服务端返回的m3u8list是无变化的,但是视频片段却和本地保存的很不一样啦,有点小激动,使用AES/CBC/PKCS5Padding
方式成功解密数据!于是关键就在搭建的服务端处了,经过分析发现它的运算方式也很简单,这里就不多说了,下面直接上代码!
代码
为了避免无意义的粘贴复制,放上三个关键类的代码,稍微按照以上所述再补充一下就可以写出全部代码了。
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
| package edusohu;
import java.io.File; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern;
import org.json.JSONArray; import org.json.JSONObject;
public class Test { private static final String DBPATH = "C:\\Users\\HAHA\\Desktop\\edusoho"; private static Pattern M3U8_EXT_X_KEY_PAT = Pattern.compile("#EXT-X-KEY:METHOD=AES-128,URI=\"([^,\"]+)\",IV=(\\w+)", 0x20); private static String PWD = "C:\\Users\\HAHA\\Desktop\\20519\\"; public static void main(String[] args) throws Exception { SqliteHelper sqlLite = new SqliteHelper(DBPATH); JSONArray videolists = sqlLite.getAllDataFromData_M3u8(); for (Object json : videolists) { dec((JSONObject) json); } }
private static void dec(JSONObject json) throws Exception { String host = json.getString("host"); String playlist = json.getString("playlist"); StringBuilder sb = new StringBuilder(); String[] lines = playlist.split("\n"); Map<String, String> hasmap = new HashMap<>(); for (String line : lines) { if (line.startsWith("#EXT-X-KEY")) { Matcher matcher = M3U8_EXT_X_KEY_PAT.matcher(line); if (matcher.find()) { String g1 = matcher.group(1); String keyName = g1.replace("http://localhost:8800/ext_x_key/", ""); sb.append(line.replace(g1, keyName) + "\n"); if (hasmap.get(keyName) == null) { SqliteHelper sqliteHelper = new SqliteHelper(DBPATH); String keyValue = sqliteHelper.getKeyValueFromData_Cache(keyName); if (keyValue == null) { return; } hasmap.put(keyName, keyValue); Utils.saveFile(keyValue.getBytes(), PWD + "/" + keyName); } } else { sb.append(line + "\n"); } } else if (line.startsWith("http")) { Pattern pattern = Pattern .compile("http://localhost:8800/\\d+/([0-9a-f]{32})\\?.*fileGlobalId=[0-9a-f]{32}"); Matcher matcher = pattern.matcher(line); if (matcher.find()) { String g1 = matcher.group(1); DecryptData.decrypt(PWD + g1, PWD + "dir/" + g1, host); sb.append(g1 + "\n"); } } else { sb.append(line + "\n"); } } Utils.saveFile(sb.toString().getBytes(), "1.m3u8"); String cmd = String.format("powershell H:\\Tools\\cmdtool\\ffmpeg.exe -allowed_extensions ALL -i %s.m3u8 -c copy -bsf:a aac_adtstoasc %s.mp4", "1","1"); System.out.println(cmd); System.out.println(Utils.execCmd(cmd, new File(PWD + "dir/"))); } }
|
解密的封装:
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
| package edusohu;
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException;
public class DecryptData { public static void decrypt(String inputPath, String outputPath, String host) throws IOException { FileInputStream fis = new FileInputStream(inputPath); FileOutputStream os = new FileOutputStream(outputPath); decrypt(fis, os, host); }
public static void decrypt(FileInputStream inputfile, FileOutputStream outfile, String host) throws IOException { DigestInputStream dis = new DigestInputStream(inputfile, host); int bufLen = 0x1000; try { byte[] buf = new byte[bufLen]; while (true) { int hasRead = dis.read(buf); if (hasRead == -1) { break; } outfile.write(buf, 0, hasRead); } outfile.flush(); } finally { dis.close(); } } }
|
解密类:
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
| package edusohu;
import java.io.IOException; import java.io.InputStream;
public class DigestInputStream extends InputStream { private int mCurrentDigestIndex; private byte[] mDigestKey; private InputStream mTargetInputStream;
public DigestInputStream(InputStream arg2, String host) { super(); this.initDigestKey(host, true); this.mTargetInputStream = arg2; }
public DigestInputStream(InputStream arg1, String arg2, boolean arg3) { super(); this.initDigestKey(arg2, arg3); this.mTargetInputStream = arg1; }
public void close() throws IOException { super.close(); this.mTargetInputStream.close(); }
private void initDigestKey(String arg3, boolean arg4) { String v0 = arg3; if((arg4) && !arg3.isEmpty()) { v0 = DigestUtils.md5(arg3); }
this.mCurrentDigestIndex = 0; this.mDigestKey = v0.getBytes(); }
private void processorByteArray(int len, byte[] buf) { if(len > 0 && this.mDigestKey.length != 0) { int digestKeyLen_1 = this.mDigestKey.length - 1; for(int i = 0;i < len;i++) { int tmp = buf[i]; int v3 = this.mCurrentDigestIndex > digestKeyLen_1 ? 0 : this.mCurrentDigestIndex; this.mCurrentDigestIndex = v3; byte[] digestKey = this.mDigestKey; int v4 = this.mCurrentDigestIndex; this.mCurrentDigestIndex = v4 + 1; buf[i] = ((byte)(digestKey[v4] ^ tmp)); } } }
public int read(byte[] buf) throws IOException { int hasRead = this.mTargetInputStream.read(buf); this.processorByteArray(hasRead, buf); return hasRead; }
public int read() throws IOException { byte[] v0 = new byte[]{((byte)this.mTargetInputStream.read())}; this.processorByteArray(1, v0); return v0[0]; }
public int read(byte[] arg3, int arg4, int arg5) throws IOException { int v0 = this.mTargetInputStream.read(arg3, arg4, arg5); this.processorByteArray(v0, arg3); return v0; } }
|
效果如下:
总结
阔知学堂官网写的很厉害,事实上还是没有i春秋的加密做的好,另外我猜同理网页上的高清视频可以用类似的方式提取,不过他开源读读源码就清楚了。。。。