RouterOS 抓包 IPTV & 实现 IPTV 的认证和频道列表获取

RouterOS 抓包 IPTV & 实现 IPTV 的认证和频道列表获取

昨天突发奇想(指脑子终于接上了正道的光),弄了一点阳间(就是成本很高)的抓包设备真正实现了 IPTV 的抓包。获取频道的组播和单播地址自然是手到擒来了,只需要在 pcap 里筛选出对应的请求再处理一下返回就好。

不过我开始思考,是否有办法让这一切更加方便,以防万一奠信换地址了我还要摸出家当再抓一次包?花了一些时间测试和在网上搜集情报之后,最终是完成了从 IPTV 机顶盒认证到获取频道信息的流程。

需要注意的是,本文的内容并不一定适用于其他地区、其他运营商或其他品牌 IPTV 机顶盒的用户,仅供参考。

抓包:RouterOS 端口镜像 + Wireshark

我一直没有下手购买搭线抓包的东方魔法工具,虽然它也不算贵;我也没有研究如何把两根网线改造成搭线抓包器,因为家里没有网线钳。

而由于 OpenWRT 复杂的硬件架构和内核,以及众多第三方版本和官方之间相互的差异,我手里的支持 OpenWRT 的 mt7621 设备基本都处于固件缺失所谓的「交换机镜像」功能的状态。

但是,昨天我突然想起来,我手里这堆 7621 电子垃圾里有一台支持 RouterOS —— RB750Gr3。花了两分钟 Google ROS 如何配置端口镜像之后,我就立刻决定用它来尝试抓包。

如果你阅读过 在 Proxmox VE 中安装 OpenWRT,你应该知道我之前将这台 Gr3 作为弱电箱主路由使用,所以系统是 OpenWRT。因此,需要先 netinstall 刷回 RouterOS。

还原 RouterOS 的流程也比较顺利,下载 MikroTik 的 netinstall 工具和 RouterOS 系统镜像,为电脑配置固定 IP 和 NetBoot Server 之后,漏油连上电脑并 netboot 即可。

只是中间可能运气不太好,按住 RST 开机费了好几次才成功进入 netboot。

当然我用它只是用来抓包,所以打开 WinBox 进行最小化的设置就可以了。Quick Set 桥接模式 + 自动获取地址,然后在 Switch 选项卡里选择两个端口分别作为镜像入口和镜像出口,保存就能拿去抓包了。

简单快速的 WinBox 配置

认证流程模拟

因为我的主要目的是获取频道的组播和单播地址,所以我只实现了从认证开始到获取地址的部分。

不过很快我就想到是否可以再往下探究一下,看看能不能白嫖 EPG,但是没抓相关的包并且懒癌发作还是留到下一次想折腾的时候弄吧(

准备工作

打开 Wireshark,筛选 HTTP 请求。

如果像我一样单线复用 VLAN,通过 mwan3 或者静态路由分流的话,需要为 IPTV 涉及到的相关 IP 做规则。具体有哪些 IP 根据抓包结果来看就好。

路由规则做好之后,才能保证电脑也能模拟 IPTV 认证。

认证

排在抓包结果 HTTP 请求首位的是两个没什么用的 POST 请求:

1
POST /ACS-server/ACS HTTP/1.1

稍微翻看一下,返回也没有什么有意义信息(其一为 204),故直接无视掉,接着往下看。

下一个请求明显是和认证相关的请求:

1
2
GET /EDS/jsp/AuthenticationURL?UserID=<UserID>&Action=Login HTTP/1.1
Host: 182.138.xxx.xxx:xxxxx

Host 和 Path 姑且认为是硬编码。返回是 302 并且下发了一个 Cookie:

1
2
3
4
HTTP/1.1 302 Found\r\n
Set-Cookie: EPGIP_PORT="182.139.xxx.xxx:xxxxx"; Version=1; Max-Age=86400; Expires=Mon, 12-Sep-2022 05:26:18 GMT; Path=/EDS; HttpOnly
Location: http://182.139.xxx.xxx:xxxxx/EPG/jsp/AuthenticationURL?UserID=<UserID>&Action=Login
...

这里意识到应该使用 session + CookieJar 的方式来模拟会话,事实上之后的 response 也下发了不少 Cookie。

302 跳转一次之后返回了一个带 script 和 form 的 HTML,从这里开始就要接触到认证的模式了:通过连续返回不同的需要 POST 的 form 和使用 script + 机顶盒自定义函数 / 类型填充 form 的一些特殊参数,来构造多个连续的 POST 请求。

(比如下方的 Authentication 类和其相关的 CTCSetConfig 方法,可以向机顶盒写入一些配置)

另外,通过构造连续的 POST 链和改变每个 response 插入的脚本,运营商也可以在认证流程以外实现一些其他的功能。

顺带一提,模拟请求的时候,IPTV 机顶盒的 headers 自然也是要抄过来的。

从 HTML 构造 POST

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
<html>
<head>
<title>Login page for STB</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>

<script>
function doLogin()
{
if (true)
{
Authentication.CTCSetConfig("ShowPic","1");//1:机顶盒将认证类型的画面展示出来
document.authform.submit();
}
}
</script>

<body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0" bgcolor="transparent" onload="doLogin()">
<form action="authLoginHWCTC.jsp" name="authform" method="post">
<table width="640" height="340" border="0">
</table>
<input type="hidden" name="UserID" value="<预填充UserID>">
<input type="hidden" name="VIP" value="">
</form>
</body>
</html>

这个表单并没有过于复杂或是未知的参数需要构造,故直接构造相同结构表单提交即可。不过不使用浏览器环境的话,就用稍微手搓一点的方式来构造吧。

另外,所有响应里表单的 name 都是一致的,所以直接写死,等会儿还可以过来 C/V。

1
2
3
4
5
6
7
8
9
# Parse form with BeautifulSoup
# r: response of last request
soup = BeautifulSoup(r.text.strip(), 'lxml')
form = soup.find('form', attrs={'name': 'authform'})
inputs = form.find_all('input')

form_data = {}
for i in inputs:
form_data[i['name']] = i['value']

从表单里提取一下下一个请求的 action,构造一下完整请求地址,然后 POST。

1
2
3
# from urllib import parse
form_location = form['action']
form_url = parse.urljoin(r.url, form_location)

很顺利拿到了下一步认证的 HTML:

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
<html>
<head>
<title>Login page for STB</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<script>
Authentication.CTCSetConfig("ShowPic","1");//1:机顶盒将认证类型的画面展示出来
</script>

<script>
function DoAuth()
{
//先写用户状态,通知STB
Authentication.CTCSetConfig('UserStatus','0');
Authentication.CTCSetConfig('PlatformCode','xxxx');
Authentication.CTCSetConfig('EncryptionType','0001');

document.authform.Lang.value = Authentication.CTCGetConfig("Lang");
document.authform.SupportHD.value = Authentication.CTCGetConfig("SupportHD");
document.authform.NetUserID.value = Authentication.CTCGetConfig("NetUserID");
document.authform.UserID.value = "<UserID>";
var stbType = Authentication.CTCGetConfig("STBType");

document.authform.STBType.value = stbType;
document.authform.STBVersion.value = Authentication.CTCGetConfig("STBVersion");
document.authform.SoftwareVersion.value = Authentication.CTCGetConfig("SoftwareVersion");
document.authform.IsSmartStb.value = Utility.getValueByName('isSmartHomeSTB');
document.authform.desktopId.value = Utility.getValueByName('TVDesktopID');
document.authform.conntype.value = Authentication.CTCGetConfig("ConnectType");

document.authform.templateName.value = Authentication.CTCGetConfig("templateName");
document.authform.areaId.value = Authentication.CTCGetConfig("areaid");
document.authform.userGroupId.value = Authentication.CTCGetConfig("UserGroupNMB");
document.authform.productPackageId.value = Authentication.CTCGetConfig("PackageIDs");
document.authform.UserField.value = Authentication.CTCGetConfig("UserField");

getSTBAuthenticator();

document.authform.submit();
}

function getSTBAuthenticator()
{
document.authform.STBID.value = Authentication.CTCGetConfig("STBID");
var EncryptToken = "D..............................9";
document.authform.Authenticator.value = Authentication.CTCGetAuthInfo(EncryptToken);
document.authform.userToken.value = "D..............................9";
document.authform.mac.value = Authentication.CTCGetConfig("mac");
}

</script>


<body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0" bgcolor="transparent" onload=DoAuth()>
<form action="ValidAuthenticationHWCTC.jsp" name="authform" method="post">
<table width="640" height="340" border="0">
<tr>
<td width="640" height="300"></td>
</tr>
<tr>
<td align="center"></td>
</tr>
</table>
<input type="hidden" name="UserID" value="">
<input type="hidden" name="Lang" value="">
<input type="hidden" name="SupportHD" value="">
<input type="hidden" name="NetUserID" value="">
<input type="hidden" name="Authenticator" value="">
<input type="hidden" name="STBType" value="">
<input type="hidden" name="STBVersion" value="">
<input type="hidden" name="conntype" value="">
<input type="hidden" name="STBID" value="">
<input type="hidden" name="templateName" value="">
<input type="hidden" name="areaId" value="">
<input type="hidden" name="userToken" value="">
<input type="hidden" name="userGroupId" value="">
<input type="hidden" name="productPackageId" value="">
<input type="hidden" name="mac" value="">
<input type="hidden" name="UserField" value="">
<input type="hidden" name="SoftwareVersion" value="">
<input type="hidden" name="IsSmartStb" value="">
<input type="hidden" name="desktopId" value="">
<input type="hidden" name="stbmaker" value="">
<input type="hidden" name="VIP" value="">
</form>
</body>
</html>

生成 Authenticator

这个表单复杂了许多,而且涉及到校验字段的生成。首先从抓包的 POST 请求里反推一下里面的值,可以解决掉除了 Authenticator 的所有字段。

那么接下来就是研究如何生成 Authenticator 字段。很幸运,在网上搜索之后,我找到了一些资料,并且拼凑残片还原出了完整的流程。

阅读 HTML 中的 JavaScript 部分,重点显然在于 getSTBAuthenticator() 函数。其中有服务端预填充的 EncryptToken,以及 Authentication 类的一个特殊方法 CTCGetAuthInfo。显然这个方法的实际运作逻辑,就是 Authenticator 的生成逻辑。

1
2
3
4
5
6
7
8
function getSTBAuthenticator()
{
document.authform.STBID.value = Authentication.CTCGetConfig("STBID");
var EncryptToken = "D..............................9";
document.authform.Authenticator.value = Authentication.CTCGetAuthInfo(EncryptToken);
document.authform.userToken.value = "D..............................9";
document.authform.mac.value = Authentication.CTCGetConfig("mac");
}

通过 Google Authentication.CTCGetAuthInfo,我找到了一个有点历史的 GitHub 仓库[1]。虽然这里的代码已有九年历史,但是只要逻辑变化不大的话仍然是不错的参考。

iptv/java/src/com/armite/webkit/plug/Authentication.java
1
2
3
4
5
6
7
8
9
10
11
12
13
public String CTCGetAuthInfo(String encryptToken) {

String tmpret = "";
// 3DES(Random+“$”+EncryToken+”$”+UserID +”$”+STBID+”$”+IP+”$”+MAC+”$”+
// Reserved+ ”$”+ “CTC”)
Random localRandom = new Random();
String tmpRandom = ""
+ Long.valueOf(Math.abs(localRandom.nextLong()) % 10000000L);
this.mIP = getLocalIpAddress();
tmpret = CTCAuthHelper.GenerateAuthInfo(this.mPassword, encryptToken,
tmpRandom, this.mUserID, this.mStbID, this.mIP, this.mMac);
return tmpret;
}

从这里可以知道,Authenticator 还与一个 8 位的随机整数有关。

仓库也正好有 CTCAuthHelper.java 等源码可供参考:

iptv/java/src/com/armite/webkit/plug/CTCAuthHelper.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class CTCAuthHelper {
public static String GenerateAuthInfo(String password, String EncryToken, String Random, String UserID, String STBID, String IP, String MAC) {
String tmpret = "";
String Reserved = "Reserved";
String tmpdata = Random + "$" + EncryToken + "$" + UserID + "$" + STBID + "$" + IP + "$" + MAC + "$" + Reserved + "$" + "CTC";
String tmpkey = password;
try {
if (tmpkey.length() > 24) tmpkey.substring(0, 24);
else {
for (int i = tmpkey.length(); i < 24; i++){
tmpkey = tmpkey + "0";
}
}

byte[] tmpresuls = SecretKeyUtil.crypt(tmpkey.getBytes("ASCII"), tmpdata.getBytes("ASCII"), SecretKeyUtil.DES3);
if (tmpresuls!=null) tmpret = SecretKeyUtil.byteToHexString(tmpresuls);
} catch (Exception ex) {}
return tmpret;
}
...
}

这里组合了具体的明文字符串,并且有 3DES 所使用 Key 的填充和截断逻辑。

SecretKeyUtil.java 主要是用于生成 3DES 的 Cipher 并对明文进行加密,就不展开了。

iptv/java/src/com/armite/webkit/plug/SecretKeyUtil.java >folded
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
public class SecretKeyUtil {
private static final String ECB_PKCS5_PADDING = "/ECB/PKCS5Padding";
public static final String AES = "AES";
public static final String DES = "DES";
public static final String DES3 = "DESede";

/**
* 加密
*
* @param key
* @param data
* @return
*/
public static byte[] crypt(byte key[], byte data[], String type) {
try {
KeySpec ks;
SecretKeyFactory kf;
SecretKey ky;

if (type.equals(DES3)) {
kf = SecretKeyFactory.getInstance(DES3);
DESedeKeySpec dks = new DESedeKeySpec(key);
ky = kf.generateSecret(dks);
} else {
kf = SecretKeyFactory.getInstance(type);
ks = new DESKeySpec(key);
ky = kf.generateSecret(ks);
}

Cipher c = Cipher.getInstance(type + ECB_PKCS5_PADDING);
c.init(Cipher.ENCRYPT_MODE, ky);
return c.doFinal(data);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
...
}

那么问题来了,虽然知道加密是 3DES 了,但是密钥是什么呢?回到 Authentication.java 中,可以看到:

iptv/java/src/com/armite/webkit/plug/Authentication.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Authentication {
public static Map mSvcEntryList = new HashMap();
public static Map mChannelList = new HashMap();
public static Map mCfgList = new HashMap();
public static String mMac;
public static String mStbID = "01234567890123456789012345678901";
public static String mUserID = "19101389";
public static String mPassword = "123456";
public static String mReserved;
public static String mIP = "";

...

public Context mContext;

// http://iptvauth.online.sh.cn:7001/iptv3a/UserAuthenticationAction.do
// 设备号:01234567890123456789012345678901
// 帐号19101389
// 密码123456

...
}

此仓库中的 mPassword 是预先写死的,这显然和实际量产不太符合,是否真的使用了这个密码需要画上一个问号。

同时查阅其他资料,在 CSDN 社区[2]和恩山[3]发现了一些有趣的内容。

其一,解答了另外一个疑惑:Reserved 字段是否产生了变化?目前看来正确的处理方式应该是置空。

其二,基于密钥为短数字 ASCII 转为 bytes 的前提下,提供了一种获取密钥和正确明文格式的思路:穷举密钥。

不过这时候需要稍微补充一点和 3DES 有关的知识。

3DES 和 DES

DES,即数据加密标准(Data Encryption Standard)。其使用的密钥为 56 位实际有效值 + 8 位奇偶校验(每 8 位的最后一位是奇偶校验位)。

3DES,实际是 Triple-DES,也就是执行三次 DES,分别是 EDE(Encrypt-Decrypt-Encrypt)。3DES 的密钥长度是 DES 的三倍,其实也就是三段 DES 密钥连接在一起。

那么显而易见的,3DES 存在一些安全问题:当三段 DES 密钥完全不一致时,3DES 的安全性是正常的;但当相邻两段 DES 密钥相同时,加密和解密抵消,3DES 就退化为 DES 了。

如果你在 Python 里使用较新的 PyCryptodome,使用一个不安全的密钥初始化 3DES Cipher 时会提示 ValueError 并警告 3DES 退化。(推测恩山脚本中使用的 PyCryptodome 还是旧版,并没有退化检测)

但是(回到正题),IPTV 采用的短数字密钥 + 24 位填充几乎不可避免会产生 3DES 退化问题,此时实际上执行的是 DES。虽然不知道为什么还要在代码里用 3DES,但是我们也降级成 DES 跑就好了。

穷举密钥

对恩山帖子中的脚本稍作修改,直接降级为 DES。穷举之后我也获得了「一堆」可用的密钥。

不过略作思考之后,其实比较容易想明白为什么会有这么多的密钥可用:奇偶校验。

对于 8 位数字的每一位 ASCII 转化为 8 x 8 位二进制之后,每 8 位的最后一位是 0 还是 1 对于实际的密钥并没有影响,因此会在 64bit 的密钥上表现出差异。

不过,对于穷举的结果我还是比较意外的:这密钥……好像是座机号码(顺带一提,也是宽带的账号密码),算是意料之中也算是意料之外了。

同时,有了正确的密钥之后也能看到机顶盒发送的明文结构,再次校验之后确定没问题,就可以生成 Authenticator 了。

获取频道地址

正确发出带有 Authenticator 的 POST 请求之后,就到了可以获取频道地址的请求。先看返回:

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
<html>
<head>
<title>Login page for STB</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>

<script language="javascript">
/* java变量与js 变量转换*/
var isSucessed = true;
var isWriteBox = true;
var needWriteTSP = true;
var speedTestURL = "ftp://@IP:PORT/.ts";
var needwritespeedurl = "false";
function AuthFinish()
{
/* 认证成功才写入 */
var tempKey = "";
if (isSucessed)
{
if(needwritespeedurl)
{
Authentication.CTCSetConfig("NetDiagnoseTool.NetworkSpeedTestURL",speedTestURL);
}
if (false)
{
Utility.setValueByName('authStatus','{"status":"1","errorCode":"","errorType":"","errorName":"","errorDesc":"","errorResolve":""}');
Utility.setValueByName('TVDesktopID','');
Utility.setValueByName('TVDesktopGetURL','');
// 智能机顶盒容灾情况下也下发UserToken,否则无法获取桌面数据
if (!isWriteBox)
{
Authentication.CTCSetConfig('UserToken','E..............................M');
}
}

Authentication.CTCSetConfig('IsPostLog','');

// Minato 注:空函数体
setStbParmList();

Authentication.CTCSetConfig('SQARtcpPort','8xx7');

/* 容灾状态这些值没必要再下发给盒子了*/
if (isWriteBox)
{
Authentication.CTCSetConfig('UserToken','E..............................M');
Authentication.CTCSetConfig('EPGDomain','http://182.139.xxx.xxx:xxxxx/EPG/jsp/newhangye/en/Category.jsp');
Authentication.CTCSetConfig('EPGDomainBackup','null');
Authentication.CTCSetConfig('ManagementDomain','http://devacs.edatahome.com:9090/ACS-server/ACS');
Authentication.CTCSetConfig('ManagementDomainBackup','null');
Authentication.CTCSetConfig('UpgradeDomain','http://182.138.xxx.xxx:xxxxx');
Authentication.CTCSetConfig('UpgradeDomainBackup','null');
Authentication.CTCSetConfig('NTPDomain','118.123.xxx.xxx');
Authentication.CTCSetConfig('NTPDomainBackup','118.123.xxx.xxx');
Authentication.CTCSetConfig('EPGGroupNMB','newhangye');
Authentication.CTCSetConfig('UserGroupNMB','xxx');
Authentication.CTCSetConfig('PackageIDs','');
Authentication.CTCSetConfig('subNetId','xxxx');

if (true)
{
if (false)
{
Authentication.CTCSetConfig('NetUserID','<NetUserID>');
}
}

Authentication.CTCSetConfig('areaid','xxxxx');
Authentication.CTCSetConfig('templateName','newhangye');
}

Authentication.CTCSetConfig('shareKey','');
Authentication.CTCSetConfig('LogServerUrl','sftp://username:password@IP:port/path');
Authentication.CTCSetConfig('LogUploadInterval','3600');
Authentication.CTCSetConfig('LogRecordInterval','3600');
Authentication.CTCSetConfig('EPGUrl','http://182.139.xxx.xxx:xxxxx/EPG/jsp/indexHWCTC.jsp');

//写用户认证通过标志
Authentication.CTCSetConfig('UserStatus','1');

if (needWriteTSP)
{
Authentication.CTCSetConfig('TransportProtocol','-1');
}

Authentication.CTCSetConfig('MQMCURL','182.138.xxx.xxx');
//认证成功后,非空的PPPoE才写入机顶盒. 只能放在最后写入,详见问题单

setSTBConfig();

Authentication.CTCSetConfig('SessionID','0..............................G');

tempKey = Authentication.CTCGetConfig('identityEncode');
document.authform.tempKey.value = tempKey;
}

document.authform.submit();
}

function setSTBConfig()
{
Authentication.CTCSetConfig('UserID','<UserID>');
Authentication.CTCSetConfig('TVMSHeartbitUrl','http://182.139.xxx.xxx:xxxxx/gateway/query.do');
Authentication.CTCSetConfig('TVMSVODHeartbitUrl','http://182.139.xxx.xxx:xxxxx/gateway/query.do');
Authentication.CTCSetConfig('TVMSHeartbitInterval','600');
Authentication.CTCSetConfig('TVMSDelayLength','60');
Authentication.CTCSetConfig("ResServerUrl", "http://182.138.xxx.xxx:xxxxx" + "/EDS/jsp/loadBootpic.jsp");

setBootPic();
add_wifi_encrypt_for_<设备型号>();
Authentication.CTCStartUpdate();
}

// Minato 注:为节省版面,上方不重要的函数已删除
...
</script>

<body leftmargin="0" topmargin="0" marginwidth="0" marginheight="0" bgcolor="transparent" onload=AuthFinish()>
<table width="640" height="340" border="0">
<tr>
<td width="640" height="300"></td>
</tr>
<tr>
</tr>
</table>
<form action="getchannellistHWCTC.jsp" name="authform" method="post">
<input type="hidden" name="conntype" value="2">
<input type="hidden" name="UserToken" value="E..............................M">
<input type="hidden" name="tempKey" value="">
<input type="hidden" name="stbid" value="xxxxxx">
<input type="hidden" name="SupportHD" value="1">
<input type="hidden" name="UserID" value="<UserID>">
<input type="hidden" name="Lang" value="1">
</form>
</body>
</html>

getchannellistHWCTC.jsp 就是会返回频道列表和链接的接口了。

不过这里又有一个小小的问题:tempKey 是什么玩意?

看了看,请求时发送的是 ...&tempKey=6..............................E&...

恩山的另一个帖子[4]给了我一点思路:

在获取tempkey的流程中,服务器是把当前的session写到了盒子里,然后取得的tempkey

1
2
3
4
5
6
setSTBConfig();

Authentication.CUSetConfig('SessionID','024J2************RZF7NB4TN4S9A');

tempKey = Authentication.CUGetConfig('identityEncode');
document.authform.tempKey.value = tempKey;

推断这个tempkey肯定是和session有关,而且如果没有tempkey直接访问下一步的网页,返回的内容包含sessiontimeout,但目前找不到任何资料,不知道加密方式。

—— flomonce #12

我觉得 TA 的思路非常合理,然后随手算了一下……

1
2
3
>>> from hashlib import md5
>>> md5('0..............................G'.encode('ascii')).hexdigest().upper()
'6..............................E'

呃,好吧。那么东西都齐活了,就 POST 吧。

处理频道列表

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--声明MIME类型,指定字符集-->

<script>
//获取频道总数,写入机顶盒内存
var iRet = Authentication.CTCSetConfig('ChannelCount','349');
</script>

<script>
var iRet;
iRet = Authentication.CTCSetConfig('Channel','ChannelID="xxxxx",ChannelName="xxxxx",UserChannelID="xxxxx",ChannelURL="igmp://xxxxx",TimeShift="xxxxx",TimeShiftLength="xxxxx",ChannelSDP="xxxxx",TimeShiftURL="rtsp://xxxxx",ChannelType="xxxxx",IsHDChannel="xxxxx",PreviewEnable="xxxxx",ChannelPurchased="xxxxx",ChannelLocked="xxxxx",ChannelLogURL="xxxxx",PositionX="xxxxx",PositionY="xxxxx",BeginTime="xxxxx",Interval="xxxxx",Lasting="xxxxx",ActionType="xxxxx",FCCEnable="xxxxx",ChannelFCCIP="xxxxx",ChannelFCCPort="xxxxx",ChannelFECPort="xxxxx"');
</script>

...

这应该是有手就行吧,xjb 抄个正则匹配一下 'ChannelID="xxxxx",...,ChannelFECPort="xxxxx"' 然后再提取下键值对就行了。最后利用一下返回的频道总数校验一下有没有提漏。

至于后面要做成 这样 的网页,还是生成 M3U 或者直播 / 点播源就随意了。


  1. iptv/Authentication.java at master · dog-god/iptv ↩︎

  2. 华为电信机顶盒3DES认证算法问题 - CSDN社区 | archive.today 存档 ↩︎

  3. 四川电信鉴权3DES加密及解密方式及查找密钥代码-iptv信源、网络视频直播ip资源、直播代码-恩山无线论坛 | archive.today 存档 ↩︎

  4. [求助]关于脱离机顶盒直接获取节目源的研究-Padavan-恩山无线论坛 ↩︎

RouterOS 抓包 IPTV & 实现 IPTV 的认证和频道列表获取

https://xyx.moe/018-RouterOS-IPTV-packet-capture-and-authentication-implementation.html

作者

星野 みなと

发布于

2022-09-12

许可协议

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×