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

881

被折叠的 条评论
为什么被折叠?



