爬虫 APP 逆向 ---> NetShort、flickreels

csdn 比较垃圾,各种审核不过,内容删减只保留主要过程和代码。。。

NetShort

环境:

  • 已经 root 的 安卓手机
  • NetShort_v2.1.6.apk
  • 魔法上网工具

NetShort 有 root 检测,会强制弹窗提示,可以通过 magisk 配合一些插件,直接过掉 root 检测,或者通过 lsposed 插件,直接禁用弹窗。

通过抓包分析可知,主要有如下几个参数

  • Authorization  请求头中授权,一段时间有效。
  • encrypt-key    请求头中 加密的key
  • 请求体           请求头中加密的 payload
  • 响应               加密的响应体

反编译 apk,直接搜索 encrypt-key

可以看到是通过 拦截器设置的 encrypt-key,这里使用的 okhttp 框架

通过分析 java 层代码,可以发现:

  • encrypt-key 就是一个随机的 32 位字符串,进行一次 base64 编码,然后通过 rsa 加密生成
  • 加密的请求体,其实就是使用 随机生成的 32 位字符串进行 aes 加密得到
  • 加密的响应体,就是就是从 response 的 header 中拿到 encrypt-key,解密之后获取 32 位的 解密key,在把响应体解密。

整体代码如下:Authorization 可以自己生成,也可以抓包获取,代码中是抓包获取的

import json
import time
import base64
import random
import string
import requests
from Crypto.Cipher import PKCS1_v1_5
from Crypto.PublicKey import RSA
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

# RSA公钥和私钥
PUBLIC_KEY_BASE64 = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2poXMstZ8NCWE7915MXzDWC5/t+oB2waGfskPqSZwLqxd4ZBR0H1cb1tAZRZcV7P+LmOd6SYNxhnELaWuKTD+D3xkz8Tt1L5j/ynGqVt1MDbiQIEzXQKUkNDSH6T0A+Xzo/67/8QOQXlVJfW06resbaeNvibfx6Qc78j96bCIPlxPrtieilVTBHUFOXjirxK/ki/mO8P2smRbpt73fsQWdGmTGMfYGvfPApGyxbxLkL/qrBjU25XpM8a0MBqzFWUAchHmqSBJ6Mbfam1SSgf3b2U28s67nOW+JiOrhd6iVLcsLFxXA54HX+Zbej3AbOB6jKaEmp/bz1amneE1NYXwwIDAQAB"
PRIVATE_KEY = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCK0Tl1pd7bjTRU93bWoHW1hLCDj2+9bg1MgY8j5C7xXaw6bJfToXhWbH1fXNbnFFVqxyYNErcuOUwJZxyDgcxUXM4yWnRseb2GF97GOicAQ2keDzVYmwky4lrSRwvcXutJRLPUCRQNfc6upfk2G5TKh6/CcP4TV1eXTF7+vdEw2SHxAOITKbSfcaZXr/hVs6a1aRHsBF+7RG99ebwZIP6/AgIyqX9RbDVN6ixi1v2G3/bwAULHLSqGdSaqij/ca17fbFGITaeCeEaZ6d/P4ZuOK+PEPdbPQt6SbY4lZaYwRvdrpH73kigPITgDzIDONFybJ1m7wRKlq1wxWHwbimptAgMBAAECggEAPz3cYJXFtt5YphDrahJGLgEabYVOUc2ub1li/eX54OpdCWzpqneYnD7myyg/m5zu4SuDUVdibsOZuXrpSZw7m3+ATP5apgS8bDe5vTNHC16qqBAjrI9NHIp09/F4HNh9dq6/Am10XkUfgP+KTrU4DyDL2NijV+pltD8N1B5kDE1igokVcsavhnu2INoMRXYE78Wq6urNECuFWw9hldv81M9m2w56t1CQOUukpo4mfmLjZRe2s+kwtcBVefGHP8Cj0OeH2dGltjl2YSQMRBFUCVoixYpOrcjIHoqzWri8IfUZ2tW+nUvHl5IZ9RVxefnFaLGnxiXd2sk6Sn4aD/l9YQKBgQDVv3HaOZxHRqlNSPrNGqplGhE066HnDsq6MlPukiovxE43CRBmpTnk9zDCqrDh9t2HbJuao7nSq5WlBERWgwqXU/qDpH43W7Y/lJfHkDv6A2m0viJa0a9x8+CJpNnCDu1ATo4/IQKwoXYice6JKnUyXgkGKn+HipiN6tO0EtWHlQKBgQCmQfklKFtXtm/FZ6NIMs+d+EyvaE5xNLKGYQxmiCR10WGYd8ZV+K0Q6qXHS+a32TirWB9F3TqPOklTytMrfPZB3BCXj4weEldb8W716G8FYf7LLhaT+MdpF7KDcruObwoQAvKV3N4eX6tUEMmdrx9hpCmmIU5EeXUkhGdmwk7BeQKBgAIXMkThJV8pGMTRvuo8pYgBnkN3PoklAuSZU2rU8Sawc9dj9k4atZtAs7BjvQEoyffmHwt/KHUgCoGnrgdulq7uOlgJRtbBxeGPUYC5L2z9lY4YAfwDawThTsPp4dtdDAMCAbAqYX1axu4FUUD0MltAwjPWPJMVzvIsZs+vE3mVAoGAJPja3OaCmZjadj2709xoyypic0dw2j/ry3JdfZec9A5h87P/CTNJ2U81GoLIhe3qakAohDLUSPGfSOD74NnjMXYswmeLs0xE3Q9tq4XK2pmWPby8DJ/wSHCapByplN0gkbr2E1mQk5SW1xT8oPJGukH1eRpC+3s/D6XaEMH5HZECgYEAigoX5l39LDsCgeaUcI4S9grkaas/WsKv37eqo3oD9Qk6VFiMM5L5Zig6aXJxuAPLVjb38caJRPmPmOXLT2kEP1E1h6OJOhEhETwVIUtcBzsK25ju9LqL89bC+W0uS7BPvk6Tcws/tXHCkQCTgb9jVXceZ2ox+6axvlW/5WgHt5Q="


# ===================== 功能函数 =====================
# 你的 AES 密钥(32位 = AES-256-ECB), 随机32位字符串
def generate_32_random_str():
    """生成32位随机字符串(大小写字母+数字)"""
    chars = string.ascii_letters + string.digits
    string_list = ''.join(random.choice(chars) for _ in range(32))
    result = string_list.lower()
    # print("【32位随机字符串】", result)
    return result


def rsa_encrypt_pkcs1(plaintext: str, pub_key_base64: str) -> str:
    """
    RSA/ECB/PKCS1Padding 加密
    :param plaintext: 明文字符串
    :param pub_key_base64: base64格式公钥
    :return: base64格式加密结果
    """
    # 解码公钥
    key_bytes = base64.b64decode(pub_key_base64)
    rsa_key = RSA.importKey(key_bytes)

    # 创建加密器(对应 Java RSA/ECB/PKCS1Padding)
    cipher = PKCS1_v1_5.new(rsa_key)

    # 加密
    encrypted = cipher.encrypt(plaintext.encode("utf-8"))

    # 返回base64格式结果
    return base64.b64encode(encrypted).decode("utf-8")


def rsa_private_decrypt(ciphertext_base64, private_key):
    # 1. 给私钥添加标准 PEM 头尾(核心修复!)
    key_pem = f"-----BEGIN RSA PRIVATE KEY-----\n{private_key}\n-----END RSA PRIVATE KEY-----"

    # 2. base64 解码密文
    ciphertext = base64.b64decode(ciphertext_base64)

    # 3. 导入私钥
    pri_key = RSA.import_key(key_pem)

    # 4. 解密(PKCS1_v1_5 = RSA/ECB/PKCS1Padding)
    cipher = PKCS1_v1_5.new(pri_key)
    sentinel = None
    plain_bytes = cipher.decrypt(ciphertext, sentinel)

    return plain_bytes.decode('utf-8')


def generate_encrypt_key(plain_base64):
    # 3. RSA加密
    encrypted_result = rsa_encrypt_pkcs1(plain_base64, PUBLIC_KEY_BASE64)
    # print(f"\n【RSA加密结果(Base64)】{encrypted_result}")
    return encrypted_result


# ===================== AES-ECB 加密(无 IV) =====================
def aes_ecb_encrypt(data, key_str):
    # 字典转 JSON 字符串
    if isinstance(data, dict):
        data = json.dumps(data, ensure_ascii=False, separators=(',', ':'))

    data_bytes = data.encode('utf-8')
    key = key_str.encode("utf-8")
    # ECB 模式,不需要 IV
    cipher = AES.new(key, AES.MODE_ECB)
    # 填充 + 加密
    encrypted = cipher.encrypt(pad(data_bytes, AES.block_size))
    # Base64 编码返回
    return base64.b64encode(encrypted).decode('utf-8')


# ===================== AES-ECB 解密(无 IV) =====================
def aes_ecb_decrypt(encrypted_base64, key_str):
    encrypted_bytes = base64.b64decode(encrypted_base64)
    key = key_str.encode("utf-8")
    cipher = AES.new(key, AES.MODE_ECB)
    decrypted = cipher.decrypt(encrypted_bytes)
    # 解密 + 去填充
    decrypted_bytes = unpad(decrypted, AES.block_size)
    decrypted_str = decrypted_bytes.decode('utf-8')

    # 转回字典
    try:
        return json.loads(decrypted_str)
    except:
        return decrypted_str


def decrypt_response_key(response_header=None):
    cipher_text = response_header["encrypt-key"]
    result = rsa_private_decrypt(cipher_text, PRIVATE_KEY)
    result_b64decode = base64.b64decode(result).decode('utf-8')
    # print("明文:", result)
    # print("base64解码:", result_b64decode)
    return result_b64decode


def request_response_body_decrypt(rd32chat_key=None, enc_response_text=None):
    # 解密
    dec_result = aes_ecb_decrypt(enc_response_text, rd32chat_key)
    # print("\n=== 解密还原结果 ===")
    # print(json.dumps(dec_result, ensure_ascii=False))
    return dec_result


def get_response():
    url = "https://appsecapi.netshort.com/prod-app-api/video/shortPlay/theater/home/video-page-list"
    all_name_list = []
    page_count = 50
    for offset in range(0, page_count * 18, 18):

        payload = {
            "currentOffset": offset,
            "modelId": 737,
            "columnId": 14,
            "theaterId": 9,
            "channelId": 7,
            "jsonParam": json.dumps({"strategyId": 3015, "strategyHashCode": 34}, separators=(',', ':'))
        }

        # 1. 生成32位随机字符串
        random_str = generate_32_random_str()

        # 2. Base64编码(作为RSA明文)
        plain_base64 = base64.b64encode(random_str.encode()).decode()
        # print("【Base64编码明文】", plain_base64)
        encrypt_key = generate_encrypt_key(plain_base64)
        # print(json.dumps(payload, ensure_ascii=False))
        # 加密
        req_body = aes_ecb_encrypt(payload, random_str)

        headers = {
            "User-Agent": "Mozilla/5.0 (Linux; Android 15; GM1911 Build/BP1A.250505.005; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.138 Mobile Safari/537.36",
            "Authorization": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOjIwMzY5OTg1NzU4NDM4NjQ1NzgsInJuU3RyIjoicHd2YXlKMnhhb2dVVk9uMmRyNUVRVHVmbVFKb3c3UXAifQ.rP6SzPR6KiHxdAstAZRi4N6awkYxhuekQkgDr0LWz8I",
            "encrypt-key": encrypt_key,
            "content-language": "zh_CN",
            # "Accept": "*/*",
            # "Accept-Encoding": "gzip, deflate, br",
            # "Connection": "keep-alive",
            "Content-Type": "application/json"
        }
        response = requests.post(url, data=req_body, headers=headers)
        if 200 == response.status_code:
            print("请求成功")
            response_key = decrypt_response_key(response.headers)
            response_text = response.text
            json_dict = request_response_body_decrypt(response_key, response_text)
            if json_dict.get('data', None) is not None:
                short_play_name_list = [item['shortPlayResp']['shortPlayName'] for item in
                                        json_dict['data']['insertRespList']]
                all_name_list.extend(short_play_name_list)
                print(f'{offset} ---> {short_play_name_list}')
                time.sleep(2)
                # continue
                break
            else:
                break
        else:
            print(f"请求失败 ---> {response.status_code}")
            return
    set_list = list(set(all_name_list))
    print(f'{len(set_list)} ---> {set_list}')


def get_video_detail_info():

    url = "https://appsecapi.netshort.com/prod-app-api/video/shortPlay/base/detail_info/V2"


    payload = {"codec": "h265", "shortPlayId": "1894648438773477378", "isRequestReserve": False, "playClarity": "720p"}
    payload = "TzcjpSZP6fbWL4v4+Ki39TXFTMei7zmHpU/JRvQuAo+ANjxnXKALu7Y7XhUEvr05nDyOhPvqlLHWLyymXse+jbg0mWvvFiAeKdvkxBhExpolS0uaSaZ2gDqCaR9M9F4pfiEv/vHmkVnf6jY7nMzq5g=="

    headers = {
        "User-Agent": "Mozilla/5.0 (Linux; Android 15; GM1911 Build/BP1A.250505.005; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/142.0.7444.138 Mobile Safari/537.36",
        "Content-Type": "application/json",
        "os": "1",
        "Authorization": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOjIwMzY5OTg1NzU4NDM4NjQ1NzgsInJuU3RyIjoicHd2YXlKMnhhb2dVVk9uMmRyNUVRVHVmbVFKb3c3UXAifQ.rP6SzPR6KiHxdAstAZRi4N6awkYxhuekQkgDr0LWz8I",
        "encrypt-key": "myL6gIHAx9f3/qfDjPy7oto8YjvtbiK2OdKNVlscNFlVBGRnh8uE6cBx0mbKtGswLJRTlD2oPzX9u/wAf7eFSWK5rPE8eSCx9/uhPrjGsZ6SWrPfppcZnfWcX20H9vcAZsBriUtRh9ibQ2otTnFoIEPNOVV6JPHn0usHftqjVMwf75d55xdnDQzbxul/izEZBlrRe+qNmwS+AwZfgKge1EmofRxSQb9jS/hZhI5JabootMR2Nr5HizZlBfm8/oK/ovKlBE6I8v2nI/BrVCYpd7lb9KjKzbkhJYNTSDSW6ig7dXHJCPvfjaT56k65uyS/1GfNCF5xoXmKtN+EzlEHvA==",
        "content-language": "zh_CN",
        "Accept": "*/*",
        "Accept-Encoding": "gzip, deflate, br",
        "Connection": "keep-alive",
    }

    response = requests.post(url, data=payload, headers=headers)
    response_key = decrypt_response_key(response.headers)
    response_text = response.text
    json_dict = request_response_body_decrypt(response_key, response_text)
    print(json.dumps(json_dict, ensure_ascii=False))


if __name__ == '__main__':
    get_response()
    # get_video_detail_info()
    pass

flickreels

网址:https://www.flickreels.net/tc/classify

这是个 web 端 js 逆向,请求头中有一个 sign 值,这个值是 payload 加上一串盐值,然后在进行一个 md5 

代码如下:

import hashlib
import json
import uuid
import requests
import os
from pathlib import Path
from urllib.parse import urlencode, urljoin

requests.packages.urllib3.disable_warnings()


def get_sign_value(params):
    """获取签名值"""
    plain_text = urlencode(params) + '&signSalt=nW8GqjbdSYRI'
    md5_string = hashlib.md5(plain_text.encode("utf-8")).hexdigest()
    return md5_string



def download_m3u8_video():
    """下载 m3u8 中的所有视频片段并合并"""

    m3u8_url = 'https://zshipricf.farsunpteltd.com/playlet-hls/1770809126_hls_29049.m3u8?verify=1774432484-eohe4sIm7B5N2SBPqYkSNYTLXHKbA36FcnxooPuXvBk%3D'
    headers = None
    # 创建保存目录
    video_dir = Path("downloads")
    video_dir.mkdir(exist_ok=True)

    # 下载 m3u8 文件
    print(f"下载 m3u8 文件:{m3u8_url}")
    m3u8_response = requests.get(m3u8_url, verify=False)
    if m3u8_response.status_code != 200:
        print(f"无法下载 m3u8 文件:{m3u8_response.status_code}")
        return

    m3u8_content = m3u8_response.text
    print("M3U8 内容:")
    print(m3u8_content)

    # 解析 m3u8,获取所有视频片段 URL
    base_url = m3u8_url.rsplit('/', 1)[0] + '/'
    segments = parse_m3u8(m3u8_content, base_url)

    if not segments:
        print("未找到视频片段")
        return

    print(f"找到 {len(segments)} 个视频片段")

    # 下载所有视频片段
    segment_paths = []
    for i, segment_url in enumerate(segments):
        segment_filename = video_dir / f"segment_{i:04d}.ts"
        segment_paths.append(segment_filename)
        download_video_segment(segment_url, segment_filename, headers)

    # 合并视频文件
    if len(segment_paths) > 0:
        output_video = video_dir / "output.ts"
        merge_video_files(output_video, segment_paths)
        print(f"下载完成!视频保存在:{output_video}")


def download_video_segment(url, save_path, headers):
    """下载单个视频片段"""
    try:
        response = requests.get(url, headers=headers, verify=False, timeout=30)
        if response.status_code == 200:
            with open(save_path, 'wb') as f:
                f.write(response.content)
            print(f"已下载:{save_path}")
            return True
        else:
            print(f"下载失败 {url}: {response.status_code}")
            return False
    except Exception as e:
        print(f"下载出错 {url}: {e}")
        return False


def parse_m3u8(m3u8_content, base_url):
    """解析 m3u8 内容,提取视频片段 URL"""
    segments = []
    for line in m3u8_content.split('\n'):
        line = line.strip()
        if line and not line.startswith('#'):
            # 如果是相对路径,转换为绝对路径
            if line.startswith('http'):
                segments.append(line)
            else:
                segments.append(urljoin(base_url, line))
    return segments


def merge_video_files(output_path, segment_paths):
    """合并视频片段(简单的二进制合并)"""
    with open(output_path, 'wb') as output_file:
        for segment_path in segment_paths:
            if os.path.exists(segment_path):
                with open(segment_path, 'rb') as segment_file:
                    output_file.write(segment_file.read())
                # 可选:删除临时片段文件
                # os.remove(segment_path)
    print(f"视频已合并到:{output_path}")


def get_play_list():
    url = "https://apiweb.flickreels.net/web/playlet_category/getPlayletList"
    headers = {
        "content-type": "application/json",
        "origin": "https://www.flickreels.net",
        "referer": "https://www.flickreels.net/",
        "sign": "md5_string",
        "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
                      "(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0",
    }
    page_info = {
        "page": 1,
        "page_size": 100,
        "total": 445
    }
    for index in range(1, 6):
        payload = {
            "category_id": 0,
            "language_id": "4",
            "page": index,
            "page_size": 100,
        }
        # category_id=0&language_id=4&page=8&page_size=12&signSalt=nW8GqjbdSYRI
        md5_string = get_sign_value(payload)
        print(md5_string)
        headers["sign"] = md5_string
        resp = requests.post(url, headers=headers, json=payload, verify=False)
        print(json.dumps(resp.json(), ensure_ascii=False))
        break
        pass


def get_chapter_list():
    url = "https://apiweb.flickreels.net/web/playlet/chapterList"

    payload = {
        "language_id": "4",
        "playlet_id": "6118"
    }
    md5_string = get_sign_value(payload)
    headers = {
        "accept": "application/json",
        "accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
        "content-type": "application/json",
        "origin": "https://www.flickreels.net",
        "referer": "https://www.flickreels.net/",
        "sign": md5_string,
        "user-agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Mobile Safari/537.36 Edg/146.0.0.0",
        "web-system": "android-article",
    }
    response = requests.post(url, json=payload, headers=headers, verify=False)
    resp_json = response.json()
    print(json.dumps(resp_json, ensure_ascii=False))


def get_play_info():
    url = "https://apiweb.flickreels.net/web/playlet/play"
    guid = str(uuid.uuid4())
    payload = {
        # "字段" 顺序不能变动
        "chapter_id": "432072",
        # "guid": "839ce1bb-93f4-4724-8f18-0c6158c5ae90",
        "guid": guid,
        "os": "pc",
        "playlet_id": "6118",
    }

    # plain_text = urlencode(payload) + '&signSalt=nW8GqjbdSYRI'
    # md5_string = hashlib.md5(plain_text.encode("utf-8")).hexdigest()
    md5_string = get_sign_value(payload)

    headers = {
        "origin": "https://www.flickreels.net",
        "referer": "https://www.flickreels.net/",
        "sign": md5_string,
        "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0",
        "web-system": "pc",
        "Content-Type": "application/json"
    }

    response = requests.post(url, json=payload, headers=headers, verify=False)
    resp_json = response.json()
    print(json.dumps(resp_json, ensure_ascii=False))
    pass



if __name__ == '__main__':
    # get_play_list()
    # get_chapter_list()
    get_play_info()
    # download_m3u8_video()
    pass

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值