分类 分享 下的文章

比如现在有一张图是这样的
彩色原图
转换到HSV空间
请输入图片描述
二值化后是这样的
inrange处理
我想保留文字,尽可能去掉大块的白色部分,
思路:首先将二值化图像进行腐蚀操作,然后进行膨胀操作,这样大块的白色部分不会怎么变,而文字则会再腐蚀的时候被去掉。
腐蚀
膨胀
一来二去现在就剩下白块部分了,如果膨胀的力度比腐蚀大一点,现在对其进行取反。
最后与原二值图像相与,这样文字部分基本都会保留,文字周围的部分不会受到影响,而原来的白块部分则会被“挖”出一个只剩边缘的区域。甚至能完全去掉白块部分。
取反相与
在这运算过程中,需要根据文字把握好腐蚀膨胀的程度。

光猫

型号:PT952G
->宽带设置

5_INTERNET_B_VID_100

桥接+DHCP Server启用+IPv4/IPv6+桥类型PPPoE_Bridged

如图:
光猫配置

路由器

型号:K2A2
固件:Padavan(RT-AC54U-GPIO-1-PSG1218-64M_3.4.3.9-099.trx)

  1. 光猫LAN口接路由器WAN口
  2. 外部网络 (WAN) - 外网设置

    1. 外网连接类型选择PPPoE:拨号
    2. PPP VPN 客户端设置填写宽带账号
      外网设置
  3. 外部网络 (WAN) - IPv6 设置

    1. IPv6 连接类型选择Native DHCPv6
    2. 获取 IPv6 外网地址选择Stateless: RA
    3. 开启通过 DHCPv6 获取内网 IPv6 地址
    4. 开启启用 LAN 路由器通告
      IPv6 设置
  4. ipv6防火墙

    1. ssh登录路由器,用户名密码默认都是admin。
    2. 清空ipv6防火墙规则

      ip6tables -P FORWARD ACCEPT
      ip6tables -F FORWARD

    3. 放行特定端口外部访问

      ip6tables -I INPUT -p tcp -m multiport --dport 45645 -j ACCEPT

最后

现在可以通过ipv6+port访问了

参考:

如有错误,还请指正。

什么是box

根据CFFMediaFormat(Common File Format & Media Formats Specification)的说明,box指的是通过特定标识符和长度定义的面向对象建立的块。
请输入图片描述

什么是pssh

pssh全称是Protection System Specific Header,即用于标识保护系统的特定头(不顺口的解释)

pssh box的构成

目前主流的DRM系统主要是PlayReady,Widevine和Fairplay三家(微软、谷歌和苹果),在它们的DRM系统数据交换中pssh是一个关键的值。

pssh box的标准构成:

  • 4 bytes – the size of the PSSH box
  • 4 bytes – the constant “PSSH”
  • 4 bytes – flags based on the ISOBMFF specification
  • 16 bytes – unique key system identifier
  • 4 bytes – size of the data inside the PSSH box
  • byte array – data itself

通常情况下,pssh能在mpd一类的文件中见到,例如使用widevine的视频网站,其中的mpd文件大致长这样:
请输入图片描述

一个容易见到且典型的pssh一般就是这个样子了(base64编码形式的):

AAAAQHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAACAIARIQ+0sI0/UVlrXzdUjHhhmNNRoEa2t0diIEa2t0dg==

为了方便理解,将它转换到十进制形式:

list(base64.b64decode(b'AAAAQHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAACAIARIQ+0sI0/UVlrXzdUjHhhmNNRoEa2t0diIEa2t0dg=='))
[0, 0, 0, 64, 112, 115, 115, 104, 0, 0, 0, 0, 237, 239, 139, 169, 121, 214, 74, 206, 163, 200, 39, 220, 213, 29, 33, 237, 0, 0, 0, 32, 8, 1, 18, 16, 251, 75, 8, 211, 245, 21, 150, 181, 243, 117, 72, 199, 134, 25, 141, 53, 26, 4, 107, 107, 116, 118, 34, 4, 107, 107, 116, 118]

按标准依次分解如下(各类进制转换请自行脑补):

  • 0, 0, 0, 64 表示整个pssh box长度64字节
  • 112, 115, 115, 104 对应的ascii字符就是pssh(小写)
  • 0, 0, 0, 0 表示根据ISOBMFF中特定的标识符
  • 237, 239, 139, 169, 121, 214, 74, 206, 163, 200, 39, 220, 213, 29, 33, 237 表示DRM系统的特定ID,这里的ID是widevine类型,参见此处
  • 0, 0, 0, 32 表示pssh box内的数据长度,即从此处偏移这个长度后到达pssh box末尾
  • 8, 1, 18, 16, 251, 75, 8, 211, 245, 21, 150, 181, 243, 117, 72, 199, 134, 25, 141, 53, 26, 4, 107, 107, 116, 118, 34, 4, 107, 107, 116, 118 该部分32字节,这里实际上是对应视频和音频的两个KID。

其他

  • KID是加密的音频轨道或视频轨道等的特定密钥标识符,简单来说加密视频时这个KID和key对应,如果你知道一对KID和key,那么就能解密由该KID标识的视频/音频文件等。
  • pssh末尾部分不一定是直接两个KID,也有可能是其他构成方式

为什么会有这个需求?

如果要追踪一个js中的变量变化,一般是在开发者工具的Sources选项卡中,对js下断点。
这其中有几个不爽的地方:

  1. js文件被压缩,不方便直接下断点,一般在格式化后下断点比较方便。
  2. js文件现在越来越大,本来浏览器就已经够占内存了,在Sources选项卡进入js并格式化常常需要等很久的时间,甚至直接没有响应。
  3. 在多个地方下断点不方便,以及有的地方下了断点也进不去。

因此本方案通过Gooreplacer插件重定向特定的js请求到本地js文件来解决上述问题烦心的点。
为了本地js能够返回特定的headers,选择通过重写SimpleHTTPRequestHandler来实现,同时保证浏览器顺利请求本地文件。

以获取西瓜视频中DRM解密用的key为例

自定义返回头脚本见此或文末。
获取西瓜视频DRM解密key的关键点
地址:

https://www.ixigua.com/cinema/album/7MzYdtWv46X_7MBDgA7bPWt/

  • 打开上述地址,F12后在Network过滤js文件关键词xgplayer_encrypt
  • 可以看到该js有一些特定的返回头

请输入图片描述

  • 首先编写一个如下形式的配置文件,由于我们格式化了js,这里要去掉content-encoding和content-length,命名为config.json,实际上不需要全部的头,只需要保证有access-control-allow-origin就行

请输入图片描述
精简版配置:

{
    "host": "127.0.0.1",
    "port": 22222,
    "scripts_path": "scripts",
    "vendors~xgplayer_encrypt.b05f677a.chunk.js": {
        "access-control-allow-origin": "*"
    }
}
  • 新建scripts文件夹,将vendors~xgplayer_encrypt.b05f677a.chunk.js放在scripts文件夹里面
  • 保存配置并执行cheat_server脚本,通过访问http://127.0.0.1:22222/vendors~xgplayer_encrypt.b05f677a.chunk.js可以看到返回头与设定的全部一致

请输入图片描述

  • 打开vendors~xgplayer_encrypt.b05f677a.chunk.js进行格式化,并在window.Module.UTF8ToString(p)前面加一句debugger;

请输入图片描述

  • 设定Gooreplacer插件重定向规则,并启用,注意不需要调试的时候记得关闭

请输入图片描述

  • 现在提前F12并刷新西瓜视频地址,等待自动进入debugger处

请输入图片描述

  • 现在愉快的拿到DRM解密用的key啦

cheat_server实现代码

完整cheat_server脚本见:
https://github.com/xhlove/cheat_server

#!/usr/bin/env python3.7
# coding=utf-8
'''
# 作者: weimo
# 创建日期: 2020-01-18 01:01:09
# 上次编辑时间: 2020-02-22 18:14:01
# 一个人的命运啊,当然要靠自我奋斗,但是...
'''
import os
import sys
import json
import chardet
import datetime
import email.utils
import urllib.parse
from http import HTTPStatus
from functools import partial
from http.server import HTTPServer, SimpleHTTPRequestHandler

def load_config():
    config = {}
    config_path = "config.json"
    if os.path.isfile(config_path):
        with open(config_path, "rb") as f:
            # 只读256是为了避免读取文件太大,虽然一般不会太大
            _encoding = chardet.detect(f.read(256))["encoding"]
        with open(config_path, "r", encoding=_encoding) as f:
            config = json.loads(f.read())
    return config

class MyHandler(SimpleHTTPRequestHandler):

    def __init__(self, *args, config: dict = {}, **kwargs):
        self.config = config
        kwargs["directory"] = os.path.join(os.getcwd(), config["scripts_path"])
        super().__init__(*args, **kwargs)

    def send_head(self):
        path = self.translate_path(self.path)
        f = None
        if os.path.isdir(path):
            parts = urllib.parse.urlsplit(self.path)
            if not parts.path.endswith('/'):
                # redirect browser - doing basically what apache does
                self.send_response(HTTPStatus.MOVED_PERMANENTLY)
                new_parts = (parts[0], parts[1], parts[2] + '/',
                             parts[3], parts[4])
                new_url = urllib.parse.urlunsplit(new_parts)
                self.send_header("Location", new_url)
                self.end_headers()
                return None
            for index in "index.html", "index.htm":
                index = os.path.join(path, index)
                if os.path.exists(index):
                    path = index
                    break
            else:
                return self.list_directory(path)
        ctype = self.guess_type(path)
        try:
            f = open(path, 'rb')
        except OSError:
            self.send_error(HTTPStatus.NOT_FOUND, "File not found")
            return None

        try:
            fs = os.fstat(f.fileno())
            # Use browser cache if possible
            if ("If-Modified-Since" in self.headers
                    and "If-None-Match" not in self.headers):
                # compare If-Modified-Since and time of last file modification
                try:
                    ims = email.utils.parsedate_to_datetime(
                        self.headers["If-Modified-Since"])
                except (TypeError, IndexError, OverflowError, ValueError):
                    # ignore ill-formed values
                    pass
                else:
                    if ims.tzinfo is None:
                        # obsolete format with no timezone, cf.
                        # https://tools.ietf.org/html/rfc7231#section-7.1.1.1
                        ims = ims.replace(tzinfo=datetime.timezone.utc)
                    if ims.tzinfo is datetime.timezone.utc:
                        # compare to UTC datetime of last modification
                        last_modif = datetime.datetime.fromtimestamp(
                            fs.st_mtime, datetime.timezone.utc)
                        # remove microseconds, like in If-Modified-Since
                        last_modif = last_modif.replace(microsecond=0)

                        if last_modif <= ims:
                            self.send_response(HTTPStatus.NOT_MODIFIED)
                            self.end_headers()
                            f.close()
                            return None

            self.send_response(HTTPStatus.OK)
            # self.send_header("Content-type", ctype)
            # self.send_header("Content-Length", str(fs[6]))
            # self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))
            self.send_custom_header()
            self.end_headers()
            return f
        except:
            f.close()
            raise

    def send_custom_header(self):
        if self.path.startswith("/"):
            js_path = self.path.lstrip("/")
        else:
            js_path = self.path
        if self.config.get(js_path) is None:
            return
        headers = self.config[js_path]
        for key, value in headers.items():
            self.send_header(key, value)

    def send_response(self, code, message=None):
        self.log_request(code)
        self.send_response_only(code, message)
        # self.send_header('Server', self.version_string())
        # self.send_header('Date', self.date_time_string())

    def send_header(self, keyword, value):
        if self.request_version != 'HTTP/0.9':
            if not hasattr(self, '_headers_buffer'):
                self._headers_buffer = []
            self._headers_buffer.append(
                ("%s: %s\r\n" % (keyword, value)).encode('latin-1', 'strict'))

        if keyword.lower() == 'connection':
            if value.lower() == 'close':
                self.close_connection = True
            elif value.lower() == 'keep-alive':
                self.close_connection = False

def main():
    config = load_config()
    Handler = partial(MyHandler, config=config)
    server = HTTPServer((config["host"], config["port"]), Handler)
    print("Starting server, listen at: http://{host}:{port}".format(**config))
    server.serve_forever()

if __name__ == '__main__':
    main()

以下是手动获取key的方法:

下断点
Disable Cache

  • 此时在console执行btoa(String.fromCharCode.apply(null, new Uint8Array(i._sce_dlgtqredxx)))即可得到base64形式的key

得到base64形式的key

注意这里的界面方式是AES-ECB,并非AES-CBC
实际上和优酷是一类方法,所以请如果要用vvtoolbox_gui_series下载请选择AES-YK方法(v0.4.0版起)
然后通过支持aes解密的下载器配合key下载即可~

动图演示:

http://pan.iqiyi.com/ext/paopao/?token=eJxjYGBgmBQtsZUBBHZNkQAAFYwDIQ.gif
动图演示