qands 发表于 2024-10-6 04:53

小白获取电视直播源--2

今天分析这个

对于我这个小白来说难度挺大的.我搞了好久,期间也有运气成分,才大致获取了它拿电视源的方法
他的源要解密好多次,就像玩密室逃脱一样...期间还有好多小陷阱
想想以前用OD手撕壳修复引用啥的,也没觉得有这次那么艰难啊...


但他也很简单,最终拿源如下是http的明文,你说作者包裹那么厚是干嘛呢....



现在开始分析
首先,他有壳,小白不会整,还是hook吧.

然后代{过}{滤}理抓包----有校验,不得行,不会绕,不代{过}{滤}理就不代{过}{滤}理呗, wireshark抓包也挺香
设置过滤条件,https,http,dns 出电视画面立即停止
然后过滤dns的包,再按流量排个序,大致就知道是哪个链接获取的直播源了(通讯量肯定大)



然后要通讯,总绕不过socket吧,直接hook socket.connect 找到和wireshark对应的IP域名(图是两次抓的,他域名会变,凑合着看吧)


点进去看堆栈,找到了b4.f这个方法 成功拿到源的下载地址.




但是下载下来是加密的,需要解密, hook aes试试


看大小,摘要,应该就是我们要的
打开看,有明文,有乱码,保存下来用16进制编辑器看看,我还大致分析了下他的格式,比如每个字符串前都会标明长度...然而没什么卵用....不过脑子里得记着这段乱码.




到底部看堆栈,看不到源码只能凭感觉 ,Y3.a.run像是个线程开始,然后调用X3.L.a方法

那继续hook,然后这里传递了2个参数类型,,都hook看看呗



hook出来一大堆,大致翻翻,翻出这个,比对下前面的乱码里的字符串,这应该就是操作文件后得到的内容了.
所以他部分是明文,slb字段是密文的


继续翻堆栈,发现初始堆栈都是Y3.a.run,但是X3.L的初始是U3.D.run
那hook G3.b

貌似发现了程序运行的起始位置(应该是吧)
我hook的是G3.b的初始化过程,
猜下他的过程,应该是初始化后,然后用线程去执行他.
那关键应该就是h3.g或者U3.a.N来控制线程,并执行不同的线
看下这个堆栈流程的下一个流程调用了哪些堆栈


省略中途一个看着没用的,找到了d3.e,在这里又调用了那段乱码,并且只有我看的那个台的
差不多要初始化了吧


然后额....线索断了...
没办法 瞎折腾了好久,期间 hook了文件操作得到了它动态加载的一个jar文件.
这个文件能用jadx反编译.
然后看hook的aes下面那条解密内容和堆栈,看见了这个乱码类,竟然在动态加载的jar里


吼吼 终于有源码看了,而且貌似关键的东西它放这个动态加载类里的,这里有一个不是那堆一坨abcd的东西,不分析下对不起作者


这个一看像是一个播放器的类, 查了一下java 的message,貌似是线程之间传递消息的,会不会前面消失的线索发这里来了?而且还是startup啥的
必须hook下

然后就发现了startcid这个方法,就是从刚刚那个d3.e.c过来的呀....还有cctv1的字样 呵呵....
把这串base64解码对比下,发现和原始的不一样,应该中途还有一次解密...
这里就不得不夸下ChatGPT,给它解密前后的代码让它分析,他竟然真找出算法来了...

    std::vector<uint8_t> decodedBytes = Base64Decode(base64Input);
    size_t byteCount = decodedBytes.size();
    std::vector<uint8_t> resultBytes(byteCount);

    for (size_t index = 0; index < byteCount; index++) {
      int byteOffset = (byteCount + index) % 128;
      int maskValue = (byteOffset >= 128) ? 128 : 0;// 如果 byteOffset >= 128 则掩码为 128,否则为 0
      resultBytes = decodedBytes ^ maskValue;// 使用掩码值和当前字节异或
    }

    // 输出处理后的结果
    for (auto byte : resultBytes) {
      std::cout << std::hex << static_cast<int>(byte) << " ";
    }

    return 0;


看看下面解码的slb字段以及AES/CTR/NoPadding解密的内容,再用16进制编辑器看下那堆乱码,其实就是用0xa+length+content 写进去的N个需要aes解密的内容



接下来就是对这段源码进行分析,我不太看的懂java,丢进ChatGPT去让它读,分析的明明白白...
中间代码分析的过程就不说了(源码,好懂)

最终找到了这串代码
import android.net.Uri;
import android.util.Pair;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;

/* loaded from: classes.dex */
public final class K extends b {
    public static final String a = o0.a(f.a(97, 72, 82, 48, 99, 68, 111, 118, 76, 50, 100, 122, 98, 71, 74, 122, 90, 88, 74, 50, 76, 109, 108, 48, 100, 105, 53, 106, 98, 88, 90, 112, 90, 71, 86, 118, 76, 109, 78, 117, 76, 50, 108, 117, 90, 71, 86, 52, 76, 109, 48, 122, 100, 84, 103, 47, 89, 50, 104, 104, 98, 109, 53, 108, 98, 67, 49, 112, 90, 68, 48, 108, 99, 121, 90, 68, 98, 50, 53, 48, 90, 87, 53, 48, 97, 87, 81, 57, 74, 88, 77, 109, 98, 71, 108, 50, 90, 87, 49, 118, 90, 71, 85, 57, 74, 88, 77, 109, 99, 51, 82, 105, 83, 87, 81, 57, 78, 67, 86, 122));
    public static final String b = o0.a("JnN0YXJ0dGltZT0lcyZlbmR0aW1lPSVz");
    public static final String[] c = {f.a(121, 115, 116, 101, 110, 108, 105, 118, 101), f.a(98, 101, 115, 116, 122, 98), f.a(119, 97, 115, 117, 115, 121, 116), f.a(70, 105, 102, 97, 115, 116, 98, 76, 105, 118, 101), f.a(104, 110, 98, 98, 108, 105, 118, 101)};
    public static final String[] d = {"J4YQcw", "J4Zb7g", "J4aOlA", "J4aVuw", "J4c5Lw", "J4d6tQ", "J4e86w", "J4iH8A", "J4iH8Q", "J4kYFg"};
    public static final HashMap e = new HashMap(1);

    @Override // p000.c
    public final String a(String str) {
      byte b2;
      String format;
      String[] split = str.split(I.a).split(I.b);
      String str2 = split;
      int parseInt = Integer.parseInt(str2.substring(0, 1));
      if (parseInt >= 0) {
            String[] strArr = c;
            if (parseInt < strArr.length) {
                String str3 = strArr;
                String substring = str2.length() == 20 ? str2.substring(1) : str2.charAt(1) + String.format(Locale.getDefault(), "%09d", Integer.valueOf(Integer.parseInt(str2.substring(2, 3)))) + String.format(Locale.getDefault(), "%09d", Integer.valueOf(w0.c(str2.substring(3))));
                if (split.length == 1) {
                  b2 = 1;
                  format = "";
                } else {
                  b2 = 4;
                  long parseLong = Long.parseLong(split);
                  long a2 = q0.a();
                  long min = Math.min(parseLong, a2 - 120000);
                  Pair pair = new Pair(Long.valueOf(min), Long.valueOf(Math.min(a2 - 30000, 3600000 + min)));
                  format = String.format(b, q0.a("yyyyMMdd'T'HHmmss.SSS'Z'").format(new Date(((Long) pair.first).longValue())), q0.a("yyyyMMdd'T'HHmmss.SSS'Z'").format(new Date(((Long) pair.second).longValue())));
                }
                String format2 = String.format(a, str3, substring, Byte.valueOf(b2), format);
                if (k0.a(format2, null, 300, 500)) {
                  return format2;
                }
                F a3 = H.a(new F(format2));
                a3.b().setInstanceFollowRedirects(false);
                String str4 = F.g;
                try {
                  a3.e = E.a;
                  D d2 = a3.d;
                  if (d2 != null) {
                        try {
                            d2.close();
                        } catch (IOException e2) {
                        }
                        a3.d = null;
                  }
                  String headerField = a3.b().getHeaderField(str4);
                  String authority = Uri.parse(headerField).getAuthority();
                  if (authority == null) {
                        return null;
                  }
                  String str5 = authority.split(":");
                  e.put(F.f, str5);
                  for (String str6 : d) {
                        String replace = headerField.replace(str5, w0.a(str6));
                        HashMap hashMap = e;
                        if (k0.a(replace, hashMap, 300, 500)) {
                            return b.a(replace, hashMap);
                        }
                  }
                  return null;
                } catch (IOException e3) {
                  throw new C(e3);
                }
            }
      }
      return null;
    }
}


将string a还原下,其实就是
   a = "http://gslbserv.itv.cmvideo.cn/index.m3u8?channel-id=%s&Contentid=%s&livemode=%s&stbId=4%s";
   b = "&starttime=%ss&endtime=%s";
   c是livemode的5个对应参数字典
   d通过base64解码的4个字节是IP地址

slb解码出来的内容i://261HQ4   
i代表用哪个类去执行,这里是K这个类.
2代表是channel-id是c
后面的得到contentID
最终得到链接
http://gslbserv.itv.cmvideo.cn/index.m3u8?channel-id=wasusyt&Contentid=6000000001000029752&livemode=1&stbId=4

继续 还有一系列小陷阱:
访问这个链接重定向到
http://cache.ott.wasulive.itv.cmvideo.cn:80/000000001000/6000000001000029752/index.m3u8?channel-id=wasusyt&Contentid=6000000001000029752&livemode=1&stbId=4&version=1.0&owaccmark=6000000001000029752&owchid=wasusyt&owsid=1477671728160118146&AuthInfo=PK0J3Tn6Yz04PXV3woR5G%2BZOF9YRCjgFxTMQsMRw6Hxi73CalIsyUCBTqakH8JJ3FaMlNZcUtmIsSDR4jFEC%2Fw%3D%3D

然后用d中的IP替换cache.ott.wasulive.itv.cmvideo.cn(这个域名解析出来的IP是1.1.1.1)
再将链接中的host改成cache.ott.wasulive.itv.cmvideo.cn
如果能拿到正确的文件,就开始播放
不能则循环到d中的下一个IP

至此 播放源获取完毕.....


这整个过程真够BT的.....而且最终拿源也要逐个去服务器上拿.还他娘的http明文,何必把代码包裹的这么好折腾人啊...




zj23308 发表于 2024-10-6 05:25

【代码包裹的这么好折腾人】,
这是破解和反破解的较真,
写了直直播app,然后他们就会开始收费,
但是,好多人不愿意给他们缴费,
于是诞生了一个又一个群,就是为了破解他们的链接,
最后的结局就是这样:代码包裹得一层又一层又一层的{:1_896:}

superzengli 发表于 2024-10-6 06:02

感谢分享&#128591;

life9999 发表于 2024-10-6 08:47

现在看个电视真的很麻烦了

黑白之道 发表于 2024-10-6 05:06

大佬好,继续虚心学习

apaye 发表于 2024-10-6 06:38

这是真大佬啊,光看看都头晕

GuoXin110521 发表于 2024-10-6 06:47

感谢大佬分享

solos123 发表于 2024-10-6 07:08

感谢大佬分享,学习中

hrxgg 发表于 2024-10-6 07:23

不可多得的精品!

hbzy 发表于 2024-10-6 07:41

只能仰望大佬了

yong3642270 发表于 2024-10-6 07:46

跟着大佬学习了,感谢大佬的分享
页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: 小白获取电视直播源--2