commit 8441f09ae5551c157cf5bb20c5a24203e9c7215d Author: steve.gao Date: Tue Dec 10 15:18:51 2024 +0800 initial diff --git a/README.md b/README.md new file mode 100644 index 0000000..8cb0494 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# api-py + +python版--提供API + +- kuaishou: 协议过快手web端滑块,激活cookie["did"]字段 + - 需要配合node服务使用,当前以定时任务执行 + +- hnw: 处理惠农网的请求头加密 + - 部分参数需要node服务生成,当前以python接口实现 + +- douyin: 处理ttwid生成问题 + - 需要配合node服务使用,当前以定时任务执行 + +- xhs: 处理web端的x-s、x-t生成逻辑 + +- wx: 处理微信视频号的视频加密解析 + - 需要配合node服务使用,目前node服务部署在172.16.10.39设备上 + - 注:该模块接口主要在172.18.1.145设备上对外开放使用 \ No newline at end of file diff --git a/dianping/__init__.py b/dianping/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dianping/mtgsig.py b/dianping/mtgsig.py new file mode 100644 index 0000000..594650d --- /dev/null +++ b/dianping/mtgsig.py @@ -0,0 +1,431 @@ +import base64 +import binascii +import ctypes +import gzip +import hashlib +import json +import time +from urllib.parse import quote, urlparse, parse_qs, quote_plus + +from Crypto.Cipher import AES +from Crypto.Util.Padding import pad, unpad + + +def encrypt_aes_cbc(plaintext): + key = "z7Jut6Ywr2Pe5Nhx".encode("utf-8") + iv = "0807060504030201".encode("utf-8") + cipher = AES.new(key, AES.MODE_CBC, iv) + ciphertext = cipher.encrypt(pad(bytes.fromhex(plaintext), AES.block_size)) + return base64.b64encode(ciphertext).decode() + + +def decrypt_aes_128_cbc(ciphertext): + """ + + """ + key = "z7Jut6Ywr2Pe5Nhx".encode("utf-8") + iv = "0807060504030201".encode("utf-8") + cipher = AES.new(key, AES.MODE_CBC, iv) + plaintext = cipher.decrypt(base64.b64decode(ciphertext)) + strs = binascii.b2a_hex(plaintext).decode() + print(strs, len(strs)) + return binascii.b2a_hex(unpad(plaintext, AES.block_size)) + + +def md5_(bytearray): + md5 = hashlib.md5() + md5.update(bytearray) + result = md5.hexdigest() + # print(result) + return result + + +def hex_2_str(v: int): + v = v & 0xffffffff # 将结果转换为32位有符号整数 + return str(hex(v))[2:] + + +def get_md5Array(hex_md5): + md5_list = [hex_md5[i: i + 8] for i in range(0, len(hex_md5), 8)] + + for j in range(len(md5_list)): + m = [md5_list[j][i: i + 2] for i in range(0, len(md5_list[j]), 2)] + md5_list[j] = "".join(m[::-1]) + # print(md5_list) + md5_int_list = [] + for q in md5_list: + decimal_num = int(q, 16) + # decimal_num = is_overflow(decimal_num) + md5_int_list.append(decimal_num) + + # print(md5_int_list) + + return md5_int_list + + +def hex_reserve(i): + st1 = hex_2_str(i) + st1 = '0' * (8 - len(st1)) + st1 if len(st1) < 8 else st1 + return "".join([st1[i: i + 2] for i in range(0, len(st1), 2)][::-1]) + + +def logical_left_shift(n, bits): + # 有符号左移 + return ctypes.c_int(n << bits).value + + +def logical_right_shift(n, bits): + # 无符号右移 + return (n % 0x100000000) >> bits + + +def python_2_js(value, size): + # python转为和js一样 ^ + return (value ^ size) % (2**32) + + +def iu(jo): + jp = [] + for jq in range(0, len(jo), 2): + jr = jo[jq:jq + 2] + js = int(jr, 16) + jp.append(js) + # print(jp) + return jp + + +def ix(jo): + if jo is None: + jo = [] + + jp = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"] + + result = "".join([jp[jo_item >> 4 & 15] + jp[15 & jo_item] for jo_item in jo]) + + return result + + +def is_(jo): + jp = quote(jo) + # print(jp) + jq = [] + jr = 0 + while jr < len(jp): + js = jp[jr] + if "%" == js: + jt = jp[jr + 1] + jp[jr + 2] + ju = int(jt, 16) + jq.append(ju) + jr += 2 + else: + jq.append(ord(js[0])) + jr += 1 + + # print(len(jq), jq) + return jq + + +def _iD(jo): + raw_table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' + new_table = 'ZmserbBoHQtNP+wOcza/LpngG8yJq42KWYj0DSfdikx3VT16IlUAFM97hECvuRX5=' + dictionary_decode = str.maketrans(new_table, raw_table) # 创建字符映射关系 用于base64decode + dictionary_encode = dict(zip(dictionary_decode.values(), dictionary_decode.keys())) # 创建一个与上面反向的映射关系用于base64encode + result_b64 = base64.b64encode(bytearray(jo)).decode() + new_result_b64 = result_b64.translate(dictionary_encode) + # print(new_result_b64) + return new_result_b64 + + +def get_k2(jo, jp): + """ + iO(jo, jp) + jo: url相关的数组 + jp: 时间戳 + """ + + m = len(jo) % 4 + jj = jp ^ len(jo) + ju = 1540483477 # 固定 + + # 每四个为一组 + for i in range(0, len(jo) - m, 4): + tmp1 = (jo[i] & 255) | logical_left_shift((jo[i + 1] & 255), 8) + tmp2 = logical_left_shift((jo[i + 2] & 255), 16) + tmp3 = tmp1 | tmp2 + tmp4 = tmp3 | logical_left_shift(jo[i + 3] & 255, 24) + + tmp5 = (tmp4 & 65535) * ju + tmp6 = logical_left_shift((((logical_right_shift(tmp4, 16)) * ju) & 65535), 16) + tmp7 = tmp5 + tmp6 + + tmp8 = python_2_js(tmp7, logical_right_shift(tmp7, 24)) + tmp9 = (tmp8 & 65535) * ju + tmp10 = logical_left_shift((((logical_right_shift(tmp8, 16)) * ju) & 65535), 16) + tmp11 = tmp9 + tmp10 + + tmp12 = (jj & 65535) * ju + tmp13 = logical_left_shift((((logical_right_shift(jj, 16)) * ju) & 65535), 16) + tmp14 = tmp12 + tmp13 + + jj = python_2_js(tmp11, tmp14) + + i = i + 4 + if m == 3: + jj = python_2_js(jj , logical_left_shift(255 & jo[i + 2], 16)) + jj = python_2_js(jj , logical_left_shift(255 & jo[i + 1], 8)) + jj = python_2_js(jj, (255 & jo[i])) + jj = (65535 & jj) * ju + logical_left_shift(logical_right_shift(jj, 16) * ju & 65535, 16) + elif m == 2: + jj = python_2_js(jj , logical_left_shift(255 & jo[i + 1], 8)) + jj = python_2_js(jj, (255 & jo[i])) + jj = (65535 & jj) * ju + logical_left_shift(logical_right_shift(jj, 16) * ju & 65535, 16) + elif m == 1: + jj = python_2_js(jj, (255 & jo[i])) + jj = (65535 & jj) * ju + logical_left_shift(logical_right_shift(jj, 16) * ju & 65535, 16) + + + tmp0 = python_2_js(jj, logical_right_shift(jj, 13)) + tmp1 = (tmp0 & 65535) * ju + tmp2 = logical_left_shift((logical_right_shift(tmp0, 16) * ju) & 65535, 16) + tmp3 = tmp1 + tmp2 + + tmp4 = logical_right_shift(python_2_js(tmp3, logical_right_shift(tmp3, 15)), 0) + + res = ctypes.c_int(tmp4 ^ ju).value + # print(res) + return res + + +def iy(jo, jq): + jr = 0 + js = 0 + jt = [] + ju = len(jq) + # print(f"{jq} 长度为 {ju}") + for jv in range(ju): + jr = (jr + 1) % 256 + js = (js + jo[jr]) % 256 + jp = jo[jr] + jo[jr] = jo[js] + jo[js] = jp + jt.append(ord(jq[jv]) ^ jo[(jo[jr] + jo[js]) % 256]) + + return jt + + +def get_k1(jZ, jp): + array1 = [i for i in range(256)] + array2 = jZ + jr = 0 + for i in range(256): + jt = i % 256 + n = i % 16 + js = array1[jt] + jr = (jr + js + array2[n] + 31) % 256 + array1[jt] = array1[jr] + array1[jr] = js + + # print(f"ararry1 => {array1}") + jt_ = iy(array1, jp) + # print(f"_arrary1 => {array1}") + jZ.extend(jt_) + # print(f" jZ => {len(jZ)} => {jZ}") + + k1 = _iD(jZ) # a5 + return k1 + + +def get_kh(jS, kb, kf, x0=4): + m = [0] * 2 + m[0] = jS << x0 + m[1] = jS << (32 - x0) + m[0] = m[0] | m[1] + kg = m[0] + kf[0] = kf[0] ^ kg + kf[1] = kf[1] ^ kb + kf[2] = kf[2] ^ kb ^ kg + kf[3] = kf[3] ^ kf[0] + + kh_list = [] + for i in kf: + kh_list.append(hex_reserve(i)) + + kh = "".join(kh_list) + return kh + + +def iv(jo): + jp = [] + jp.append((jo >> 24) & 255) + jp.append((jo >> 16) & 255) + jp.append((jo >> 8) & 255) + jp.append(jo & 255) + return jp + + +def bc(p): + t = 256 + f = [None] * t + while t: + c = 8 + t -= 1 + a = t + while c: + if a & 1: + a = python_2_js(logical_right_shift(a, 1), 3988292384) + else: + a = logical_right_shift(a, 1) + c -= 1 + + f[t] = logical_right_shift(a, 0) + + c = 0 + t = -1 + while c < len(p): + t = f[python_2_js(255 & t, p[c])] ^ logical_right_shift(t, 8) + c += 1 + + return python_2_js(t, 306674911) - 2 ** 32 + + +def get_a6(a6): + """ + 直接读取a6 + 并修改a6的部分内容 + :return: + """ + env = from_a6(a6[4:]) + A = gzip.compress(env.encode("utf-8"), compresslevel=1) + C = encrypt_aes_cbc(A.hex()) + C = "w1.3" + C + return C + + +def get_JP(url): + """ + 处理url + """ + parse_result = urlparse(url) + path = parse_result.path + param_dict = parse_qs(parse_result.query) + param_list = list(param_dict.keys()) + param_list.sort() # 排序从小到大 + params = "&".join([f'{i}={quote_plus(param_dict[i][0])}' for i in param_list]) + + target_ = f"GET {path} {params}" + JP = is_(target_) + return JP + + +def get_a5(i, y, qa): + """ + a5 生成 + """ + d = i & 4294967295 + p = iv(d) + g = [] + g.extend(is_(y)) + g.extend(p) + + v = md5_(bytearray(g)) + m = iu(v[:15]) + f = 0 # env 环境值 + m[7] = 255 & (f ^ bc(p)) + m.extend(p) + l = iv(4294967295 & bc(m)) + m.extend(l) + a5 = get_k1(m, qa) + return a5 + + +def get_main(i, a6, a3, a7, url, jq): + # i = 1723618362188 # 时间戳 TODO:需要二次处理 + o = i & 4294967295 + a5 = get_a5(i, a6, jq) + + tmp = is_(a6) + l = iv(o) + tmp.extend(l) + g = bytearray(tmp) + v = md5_(g) + w = get_JP(url) + S = get_k2(w, i) + C = iv(S) + A = get_k2(bytearray(is_(a5)), i) + I = iv(A) + c = [S, A, S ^ o, S ^ A ^ o] + O = iu("".join([hex_reserve(i) for i in c])) + + tmp = [] + tmp.extend(C) + tmp.extend(I) + tmp.extend(O) + D = ix(tmp) # a4 + + d = logical_right_shift(A, 0) + T = f"1.2{i}{a3}{D}{d}{v}{a7}" + tmp = md5_(bytearray(is_(T))) + j = get_md5Array(tmp) + d1 = get_kh(o, d, j, x0=3) # 到这一步就是 小程序的 d1 + + mtgsig = { + "a1": "1.2", + "a2": i, # 时间戳 估计有校验 + "a3": a3, # 服务器下发 + "a4": D, # 计算 + "a5": a5, # 计算 暂时不清楚入参 + "a6": a6, # 计算环境 需要确定环境信息 + "a7": a7, # wxcode 固定 + "x0": 3, # 固定 + "d1": d1 # 计算 比h5少一步 + } + + return mtgsig + + +def from_a6(a6): + """ + 根据版本迭代 a6 + :param a6: + :return: + """ + decode_res = decrypt_aes_128_cbc(a6) + plain_text = gzip.decompress(bytes.fromhex(decode_res.decode())) + p = json.loads(plain_text.decode()) + p[-3] = int(time.time()) # 修改时间戳 + p[-4][-6] = "3.9.12" # 修改版本 + p[-4][-9] = "3.6.3" # 修改另外一个版本 **目前没有看到其他的需要修改的地方 + res = json.dumps(p, ensure_ascii=False, separators=(',', ':')) + return res + + + +def mtgsig_run(url, b8, a3, a6): + """ + mtgsig 对外调用接口 + :param url: 需要加密的链接 + :param b8: 累加 + :param a3: a3服务器下发 暂时写死 + :param a6: 目标设备环境 + :return: + """ + a6 = get_a6(a6) + a7 = "wx734c1ad7b3562129" + # m = 1723688535465 + i = int(time.time() * 1000) + # diff = i - m + # i = i - diff # 处理时间 这块暂时先写死 + _jq = {"b7": int(time.time()) - 200, + "b1": {"miniProgram": {"appId": "wx734c1ad7b3562129", "envVersion": "release", "version": "9.37.232"}}, + "b6": "", "b8": b8, "b2": "pages/poi/poi"} + + jq = json.dumps(_jq, separators=(',', ':')) + mtgsig = json.dumps(get_main(i, a6, a3, a7, url, jq), ensure_ascii=False, separators=(',', ':')) + return mtgsig + +if __name__ == '__main__': + url = "https://mapi.dianping.com/mapi/wechat/weshop.bin?yodaReady=wx&csecappid=wx734c1ad7b3562129&csecplatform=3&csecversionname=9.64.0&csecversion=1.4.0&optimus_uuid=18b229261a3c8-c53ac1e606702-0-0-18b229261a3c8&optimus_platform=13&optimus_partner=203&optimus_risk_level=71&optimus_code=10&redirect_from=pages%252Fdetail%252Fdetail&shopUuid=k9Kcg8I6w4GBcuGF&shopType=10&shopStyle=bigpic&online=1&shopuuid=k9Kcg8I6w4GBcuGF&pageName=shop&lat=40.055335628194634&lng=116.35128357647478&mtsiReferrer=pages%252Fdetail%252Fdetail%253Fredirect_from%253Dpages%25252Fdetail%25252Fdetail%2526shopUuid%253Dk9Kcg8I6w4GBcuGF%2526shopType%253D10%2526shopStyle%253Dbigpic%2526online%253D1%2526shopuuid%253Dk9Kcg8I6w4GBcuGF%2526shopId%253Dk9Kcg8I6w4GBcuGF%2526pageName%253Dshop&cookieid=-4TzrChF1yPgyQBn5D7NrKC2_vuFu9oL8DiAq28T-Kk&device_system=MAC&wxmp_version=9.64.0" + # print(mtgsig_run(url)) + a6 = "w1.3grvNcRFZkE6QoEQ31ALPuc415J6/VKamSdeIXCVWX/DGsGA+taQlBpn7QF0z/ffrSmLkkQW0dqtDnWECJyGr9iFEhTKHYlyKRbYWwffNlvlDZ9tmFet/PyCSfKJw7xVZe8/4QYaAcbk1eJ7tWMUQllVcQDKlxku5Smml4r566JVAOGi48LCGAHxImhjIuI2KomDqOtCQb7Ii2cbGd2Y7vmWjypGGJbTtrztH/1weiEULAxadX72+RdjpXDa8UMgGbVmEUu5LLIwoyNEi6leK6f/QvV9/EvVfb5vtKyL5IAQND5B9g5qndP4GOuTftOM2ylQKb4rguR4TEQsR3uyruBJusZlID67k1d8jPCM+ol2mbFJP88EfBwnrQ+BSDWfVQjjfIj2mWpEnI+oA72gkPxuHteESJaXzZz94udFusf1tUtdf0q15QiQPSBX7JjK2P/lLHMfe+867ZiGMyoivxisrw1d9bbal27BjMTm32IrAN+ywQFoP9bHXNThdxmai" + print(get_a6(a6)) + print(from_a6(a6[4:])) \ No newline at end of file diff --git a/douyin/__init__.py b/douyin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/douyin/dy_abogus.py b/douyin/dy_abogus.py new file mode 100644 index 0000000..504e0c4 --- /dev/null +++ b/douyin/dy_abogus.py @@ -0,0 +1,227 @@ +import hashlib, time, random +from functools import reduce + + +def hash_sm3(data): + sm3 = hashlib.new("sm3") + if isinstance(data, str): + sm3.update(data.encode("utf-8")) + else: + sm3.update(data) + return sm3.hexdigest() + + +def get_array(timestamp): + array1 = [0] * 4 + array1[0] = (timestamp >> 24) & 255 + array1[1] = (timestamp >> 16) & 255 + array1[2] = (timestamp >> 8) & 255 + array1[3] = (timestamp >> 0) & 255 + return array1 + + +def deal(orginalString, target, step=20): + """ + 计算最后一部 获得最终的数据 + """ + result = "" + for i in range(0, len(orginalString), 3): + if i > step: + break + + M0 = orginalString[i] + if i >= len(orginalString) - 1: + M1 = M2 = 0 + else: + M1 = orginalString[i + 1] + M2 = orginalString[i + 2] + baseNum = (M0 << 16) | (M1 << 8) | M2 + num1 = target[(16515072 & baseNum) >> 18] + num2 = target[(258048 & baseNum) >> 12] + if i >= len(orginalString) - 1: + num3 = num4 = target[-1] + else: + num3 = target[(4032 & baseNum) >> 6] + num4 = target[63 & baseNum] + tmpNum = num1 + num2 + num3 + num4 + result += tmpNum + + return result + + +def get_fn(code): + fn = [0,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,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255] + j = 0 + for i in range(len(fn)): + j = (j + fn[i] + ord(code[i % 3])) % 256 + tmp = fn[i] + fn[i], fn[j] = fn[j], tmp # 替换 + + return fn + + +def deal1(orginalString, fn): + """ + 计算数组 + """ + tmp1 = 0 + F_array = [] + for i in range(len(orginalString)): + j = (tmp1 + fn[i + 1]) % 256 + tmp2 = fn[j] + tmp1 = fn[i + 1] + fn[i + 1], fn[j] = tmp2, tmp1 + # print(f"ua_code: {ua_code[i]} ^ {fn[(tmp1 + tmp2) % 256]}") + F_array.append(orginalString[i] ^ fn[(tmp1 + tmp2) % 256]) + tmp1 = j + + return F_array + + +def get_ua_code(o_string, ua, code): + """ + o_string: '\u0000\u0001\u000e' + """ + fn = get_fn(o_string) # 获得256变换后的数组 + ua_code = [ord(i) for i in ua] # 需要配合计算的数组 + F_array = deal1(ua_code, fn) + ua_base = deal(F_array, code, 117) + return ua_base + + +def get_head(shift_list): + """ + 获取编码头 + """ + random_code = int(random.random() * 10000) + tmp_num1 = random_code & 255 + tmp_num2 = (random_code >> 8) & 255 + + tmp = shift_list[0] & 85 + num1 = (tmp_num1 & 170) | tmp + + tmp2 = shift_list[0] & 170 + num2 = (tmp_num1 & 85) | tmp2 + + tmp3 = shift_list[1] & 85 + num3 = (tmp_num2 & 170) | tmp3 + + tmp4 = shift_list[1] & 170 + num4 = (tmp_num2 & 85) | tmp4 + + return [num1, num2, num3, num4] + + + +def get_original(o_string, originalString, code): + fn = get_fn(o_string) + # print(f"fn => {len(fn)} {str(fn).replace(' ', '')}") + original_string1 = deal1(originalString, fn) + # print(f"original_string1 {len(original_string1)}=> {len(original_string1)} {str(original_string1).replace(' ', '')}") + original_string = [] + + # 这下面仨字符固定了 TODO: 可能是随机的 每次都不一样 + a1 = get_head([3, 45]) # header1 + a2 = get_head([1, 0]) # header2 + a3 = get_head([1, 5]) # header3 + + original_string.extend(a1) + original_string.extend(a2) + original_string.extend(a3) + # print(f"original_string => {original_string}") + + # code = "Dkdpgh2ZmsQB80/MfvV36XI1R45-WUAlEixNLwoqYTOPuzKFjJnry79HbGcaStCe=" + # + original_string.extend(original_string1) + a_bogus = deal(original_string, code, 124) + return a_bogus + + + + +def get_h2(env="1440|150|1440|900|0|0|0|0|1440|900|1440|900|1440|150|24|24|MacIntel"): + """ + 涉及环境变量 固定可 + """ + return [ord(i) for i in env] + + +def exchange_array(array1, array2, array3, array4, array5, array6, array7): + h1 = [44, array1[0], array2[0], 0, 0, 0, array3[1], array4[21], array5[21], array2[1], array6[23], array1[1], + 0, array2[2], array2[3], 1, 0, array3[0], array4[22], array5[22], array6[24], array1[2], 0, 0, array3[2], + 0, array1[3], 0, 0, 14, array7[0], array7[1], array3[2], array7[2], array7[3], 3, 400, 1, 400, 1, 67, + 0, 0, 0] + return h1 + + +def main(params, ua, timestamp1, timestamp2, base_key): + P = lambda x: [int(x[i:i + 2], 16) for i in range(0, len(x), 2)] + tags = "cus" + pageId = 6241 + aid = 6383 + + array1 = get_array(timestamp1) # 后面计算时候的时间戳 + # print(f"array1 => {str(array1).replace(' ', '')}") + + array2 = get_array(pageId) # pageId + # print(f"array2 => {str(array2).replace(' ', '')}") + + array3 = get_array(aid)[::-1] # aid + # print(f"array3 => {str(array3).replace(' ', '')}") + + array4 = P(hash_sm3(bytes.fromhex(hash_sm3(params + tags)))) # 参数加上 "cus"进行二次加密 + # print(f"array4 => {str(array4).replace(' ', '')}") + + array5 = P(hash_sm3(bytes.fromhex(hash_sm3(tags)))) # cus两次加密结果数组 + # print(f"array5 => {str(array5).replace(' ', '')}") + + # code = "ckdp1h4ZKsUB80/Mfvw36XIgR25+WQAlEi7NLboqYTOPuzmFjJnryx9HVGDaStCe" + array6 = P(hash_sm3(get_ua_code("\u0000\u0001\u000e", ua, base_key["a1"]))) # 计算请求头的数组 + # print(f"array6 => {str(array6).replace(' ', '')}") + + array7 = get_array(timestamp2) # 加载时间戳 + # print(f"array7 => {str(array7).replace(' ', '')}") + + h1 = exchange_array(array1, array2, array3, array4, array5, array6, array7) # 整合所有数组 + # print(f"h1 => {str(h1).replace(' ', '')}") + + h2 = get_h2() # 获取上半段数据 + # print(f"h2 => {str(h2).replace(' ', '')}") + + h3 = reduce(lambda x, y: int(x) ^ int(y), h1) + # print(f"h3 => {str(h3).replace(' ', '')}") + + h = [] + h.extend(h1) + h.extend(h2) + h.append(h3) + + # print(f"h => {len(h)} {str(h).replace(' ', '')}") + + a_bogus = get_original("yyy", h, base_key["a2"]) + + # print(f" a_bogus {len(a_bogus)}=> {a_bogus}") + + return a_bogus + + +def run(params, ua): + base_key = { + "a1": "ckdp1h4ZKsUB80/Mfvw36XIgR25+WQAlEi7NLboqYTOPuzmFjJnryx9HVGDaStCe", + "a2": "Dkdpgh2ZmsQB80/MfvV36XI1R45-WUAlEixNLwoqYTOPuzKFjJnry79HbGcaStCe=" + } + + timestamp1 = 1718786088339 # 后者 + timestamp2 = 1718786087904 # js加载时间 + + timestamp2 = int(time.time() * 1000) + tmp = random.randint(300, 600) + timestamp1 = timestamp2 + tmp + + return main(params, ua, timestamp1, timestamp2, base_key) + + +if __name__ == '__main__': + params = "device_platform=webapp&aid=6383&channel=channel_pc_web&aweme_id=7123849705431272712&update_version_code=170400&pc_client_type=1&version_code=190500&version_name=19.5.0&cookie_enabled=true&screen_width=1440&screen_height=900&browser_language=zh-CN&browser_platform=MacIntel&browser_name=Chrome&browser_version=125.0.0.0&browser_online=true&engine_name=Blink&engine_version=125.0.0.0&os_name=Mac+OS&os_version=10.15.7&cpu_core_num=2&device_memory=8&platform=PC&downlink=10&effective_type=4g&round_trip_time=50&webid=7382044487177143862&msToken=Ru8XaSvg7YcHk135aj68vgAK247SND6YxUW8KgdHWeRJHk_On01S3Acja3fqH4INQjtIwnpz-FDy9BtVQ_qO_MeIkErjRima9r6t461khRCmTXZcHs7NMRrj7pC43w%3D%3D" + ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36" + a_bogus = run(params, ua) \ No newline at end of file diff --git a/douyin/dy_ttwid.py b/douyin/dy_ttwid.py new file mode 100644 index 0000000..88eb5a8 --- /dev/null +++ b/douyin/dy_ttwid.py @@ -0,0 +1,152 @@ +# coding:utf-8 +import argparse +import os, sys +# 相对路径补充 +root_path = os.path.abspath(os.path.dirname(__file__)).split('api-py')[0] + "api-py" +sys.path.append(root_path) + +from concurrent.futures.thread import ThreadPoolExecutor +from utils.MysqlData import MysqlPoolClient, CRAWLER_DB_CONF_DY +from utils.tool import download_q +from loguru import logger + + +class DouyinTtwid(): + """ + 抖音主页生成 ttwid + 参数:signature + """ + def __init__(self, is_proxy: bool): + self.is_proxy = is_proxy + self.sql_list = [] + + def get_cookie(self, ck=None): + """ + 获取 nonce(ttwid) + :param ck: + :return: + """ + headers = { + "authority": "www.douyin.com", + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "accept-language": "zh-CN,zh;q=0.9", + "cache-control": "no-cache", + "pragma": "no-cache", + "sec-ch-ua": "\"Chromium\";v=\"118\", \"Google Chrome\";v=\"118\", \"Not=A?Brand\";v=\"99\"", + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": "\"macOS\"", + "sec-fetch-dest": "document", + "sec-fetch-mode": "navigate", + "sec-fetch-site": "none", + "sec-fetch-user": "?1", + "upgrade-insecure-requests": "1", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36" + } + + url = "https://www.douyin.com/" + if ck: + status, response = download_q(url, headers, cookies=ck, is_proxy=self.is_proxy) + key = response.cookies["ttwid"] + logger.info(f"获得 ttwid 成功:{key}") + self.ttwid = key + else: + status, response = download_q(url, headers, cookies={}, is_proxy=self.is_proxy) + key = response.cookies["__ac_nonce"] + logger.info(f"获得 __ac_nonce 成功:{key}") + self.nonce = key + + return key + + def get_signature(self, nonce): + """ + node 服务获取signature + :param nonce: + :return: + """ + url = f"http://127.0.0.1:3000/douyin/get_signature?nonce={nonce}" + status, response = download_q(url, {}, {}) + signature = response.text + logger.info(f"成功获取signature:{signature}; nonce: {nonce}") + return signature + + def run(self): + """ + 调用入口 + :return: + """ + try: + __ac_nonce = self.get_cookie() + if __ac_nonce: + __ac_signature = self.get_signature(__ac_nonce) + ck = {"__ac_nonce": __ac_nonce, "__ac_signature": __ac_signature, "__ac_referer": "__ac_blank"} + ttwid = self.get_cookie(ck) + if ttwid: + sql_ttwid = f"ttwid={ttwid};" + sql = "INSERT INTO `douyin_cookie_video_ly_copy2_test`(`cookie`, `status`, `source`, `time`) " \ + "VALUES('%s', '%s', '%s', NOW());" % (sql_ttwid, 2, 0) + self.sql_list.append(sql) + return ttwid + else: + logger.error("获得 ttwid 失败 ") + else: + logger.error("获得 nonce 失败 ") + except Exception as e: + # traceback.print_exc() + logger.error(e) + + +def insert_data(sql_list): + """ + 入库 + :param sql_list: + :return: + """ + client = MysqlPoolClient(CRAWLER_DB_CONF_DY) + for sql in sql_list: + try: + logger.success(f"insert cookie -> {sql}") + client.getOne(sql) + except Exception as e: + logger.error(f"insert cookie -> {sql}") + + +def write_file(l): + """ + 保存文件 + :param l: + :return: + """ + with open("ttwid.txt", "w") as f: + f.write("\n".join(l)) + f.close() + logger.info("文件保存成功") + + +def create_by_thread(douyin, count): + """ + 并发执行 + :param slid: + :param count: + :return: + """ + with ThreadPoolExecutor(max_workers=3) as t: + obj_list = [] + for i in range(count * 2): + obj = t.submit(douyin.run) + obj_list.append(obj) + + insert_data(douyin.sql_list) + # write_file(douyin.sql_list) + logger.info(f"[sum] 并发任务 需要生成数量 {count}, 实际抓取数量 {count*2}, 实际生成数量 {len(douyin.sql_list)}, 成功率 {len(douyin.sql_list)/(count*2)}") + + +if __name__ == '__main__': + dy = DouyinTtwid(is_proxy=True) + + parser = argparse.ArgumentParser(description='get douyin.com cookie') + parser.add_argument('-c', type=int, default=100, help="needed cookie count;default count=100;") + args = parser.parse_args() + + args_count = args.c + create_by_thread(dy, args_count) + diff --git a/hnw/__init__.py b/hnw/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hnw/hnw_hook.py b/hnw/hnw_hook.py new file mode 100644 index 0000000..025addc --- /dev/null +++ b/hnw/hnw_hook.py @@ -0,0 +1,114 @@ +from hashlib import md5, sha1, sha384 +import math, random, time,requests + +def MD5(s): + m = md5() + m.update(str.encode(s, encoding="utf-8")) + return m.hexdigest() + +def SHA1(s): + sha = sha1() + sha.update(str.encode(s, encoding="utf-8")) + return sha.hexdigest() + + +def SHA384(s): + sha = sha384() + sha.update(str.encode(s, encoding="utf-8")) + return sha.hexdigest() + + +def I(nonce="03423d9c06bd4bbb45b5fb9059a9ed4f"): + return MD5(nonce) + + +def j(timeStamp): + return SHA1(timeStamp) + + +def P(nonce, client_id): + return MD5(nonce + client_id) + + +def S(time): + s = "EOi^0N5sWWHhkrF2A0gekY9U20BgnAcr" + res = SHA1(s + str(time)) + return res[len(res)-16:len(res) - 1] + +def o(e, f): + b = e + t = f + n = "" + r1 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + while b: + o = b % 36 + l = b / 36 + n = r1[o] + n + b = round(math.floor(l)) + return ("0000000000000000" + n)[-t:] + + +def get_trace_id(e): + return o(e, 9) + o(random.randint(0, 78364164095), 7) + + +def get_uuid(t): + e = int(time.time() * 1000) + # e = 1700013194061 + template = "xxxxxxxxxxxxxyxxxxyxxxxxxxxxxxxx" if t else "xxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxx" + uuid = "" + i = -1 + uuids = [] + while i < len(template) - 1: + i += 1 + if template[i] == "-": + uuids.append(uuid) + uuid = "" + continue + elif template[i] == "4": + uuid += template[i] + continue + + n = (e + 16 * random.random()) % 16 + e = e // 16 + + char = format(int(n) if template[i] == 'x' else 3 & int(n) | 8, "x") + uuid += char + uuids.append(uuid) + return "-".join(uuids) + + +def get_header_nonce(): + return get_uuid(True) + + +def server(key): + url = f"http://127.0.0.1:3000/hn/get_signature?nonce={key}" + res = requests.get(url) + return res.text + + +def build_headers(): + times = int(time.time() * 1000) + nonce = get_header_nonce() + sid = f"S_{get_trace_id(times - 300)}" + traceid = get_trace_id(times) + client_id = get_uuid(False) + s = S(times) + + M = f'{I(nonce)}!{j(str(times))}!{P(nonce, client_id)}!{server(s)}' + + h = { + "x-b3-traceid": traceid, + "x-client-id": client_id, + "x-client-nonce": nonce, + "x-client-sign": SHA384(M), + "x-client-time": str(times), + "x-client-sid": sid + } + # print(h) + return h + + +if __name__ == '__main__': + build_headers() diff --git a/kuaishou/README.md b/kuaishou/README.md new file mode 100644 index 0000000..eb32a18 --- /dev/null +++ b/kuaishou/README.md @@ -0,0 +1,27 @@ +# 快手 + +协议过滑块,激活cookie did字段,主要分三个部分 +- 获取未激活的did +- 携带did,通过接口获取验证码信息 +- 协议过验证码,激活did + - 获取图片,进行缺口识别 + - 通过识别位置进行参数拼接,轨迹模拟 + - 调用node服务将参数加密,获得最终验证参数 + +最终数据写入sql + +代码位置: +` +172.16.10.39: /data1/api-py/kuaishou +` + +执行: +```shell +conda activate py3.8 +python ks_slide.py -h +``` + +文件说明 +- `ks_http.py`: 公共变量 +- `ks_slide.py`: 主要逻辑 +- `ks_make_trace.py`: 模拟轨迹生成算法 \ No newline at end of file diff --git a/kuaishou/__init__.py b/kuaishou/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kuaishou/ks_http.py b/kuaishou/ks_http.py new file mode 100644 index 0000000..4d5141c --- /dev/null +++ b/kuaishou/ks_http.py @@ -0,0 +1,161 @@ +import json +import random +from enum import Enum + +import requests + + +def get_gpuInfo(): + """ + 随机生成设备信息 + :return: + """ + random_list = ["Intel(R) Iris(TM) Pro Graphics 5200", "Intel(R) Iris(TM) Graphics 5100", + "Intel(R) Iris(TM) Graphics 5000", "Intel(R) Iris(TM) Graphics 4200", + "Intel(R) Iris(TM) Graphics 4400", "Intel(R) Iris(TM) Graphics 4600", + "Intel(R) Iris(TM) Pro Graphics 6200", "Intel(R) Iris(TM) Pro Graphics 6100", + "Intel(R) Iris(TM) Graphics 6000", "Intel(R) Iris(TM) Graphics 5500", + "Intel(R) Iris(TM) Graphics 5300", f"Intel(R) Iris(TM) Graphics {random.randint(100, 999)}"] + + random_list = [f"Intel(R) Iris(TM) Graphics {random.randint(100, 999)}"] + gpuInfo = {"glRenderer":"WebKit WebGL","glVendor":"WebKit","unmaskRenderer":f"ANGLE (Intel, ANGLE Metal Renderer: {random.choice(random_list)} Unspecified Version)","unmaskVendor":"Google Inc. (Intel)"} + + return json.dumps(gpuInfo, separators=(',', ':')) + + +class BaseHeaders(Enum): + """ + + """ + + # 获取快手session(快手api接口) header + HEADERS = { + "Accept-Language": "zh-CN,zh;q=0.9", + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "Origin": "https://www.kuaishou.com", + "Pragma": "no-cache", + # "Referer": "https://www.kuaishou.com/profile/3xsdu49r65skedk", + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "same-origin", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36\",\"userAgent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36", + "accept": "*/*", + "content-type": "application/json", + "sec-ch-ua": "\"Chromium\";v=\"118\", \"Google Chrome\";v=\"118\", \"Not=A?Brand\";v=\"99\"", + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": "\"macOS\"" + } + + # 获取快手过验证码请求headers + VERIFY_HEADERS = { + "Accept": "application/json, text/plain, */*", + "Accept-Language": "zh-CN,zh;q=0.9", + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "Content-Type": "application/x-www-form-urlencoded", + "Origin": "https://captcha.zt.kuaishou.com", + "Pragma": "no-cache", + # "Referer": Referer, + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "same-origin", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36", + "sec-ch-ua": "\"Chromium\";v=\"118\", \"Google Chrome\";v=\"118\", \"Not=A?Brand\";v=\"99\"", + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": "\"macOS\"" + } + + PIC_HEADERS = { + "Accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", + "Accept-Language": "zh-CN,zh;q=0.9", + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "Pragma": "no-cache", + # "Referer": referer, + "Sec-Fetch-Dest": "image", + "Sec-Fetch-Mode": "no-cors", + "Sec-Fetch-Site": "same-origin", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36", + "sec-ch-ua": "\"Chromium\";v=\"118\", \"Google Chrome\";v=\"118\", \"Not=A?Brand\";v=\"99\"", + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": "\"macOS\"" + } + + DOC_HEADERS = { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "Accept-Language": "zh-CN,zh;q=0.9", + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "Pragma": "no-cache", + "Sec-Fetch-Dest": "document", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Site": "none", + "Sec-Fetch-User": "?1", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36", + "sec-ch-ua": "\"Chromium\";v=\"118\", \"Google Chrome\";v=\"118\", \"Not=A?Brand\";v=\"99\"", + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": "\"macOS\"" + } + + +class BaseParam(Enum): + VERIFY_PARAM = { + "captchaSn": "", + "bgDisWidth": 316, # 固定 + "bgDisHeight": 184, # 固定 + "cutDisWidth": 56, # 固定 + "cutDisHeight": 56, # 固定 + "relativeX": "", # 滑动距离 应该是缩放过了 + "relativeY": "", # config接口里的y 缩放 136 * 56/122 () + "trajectory": "", + # "gpuInfo": "{\"glRenderer\":\"WebKit WebGL\",\"glVendor\":\"WebKit\",\"unmaskRenderer\":\"ANGLE (Apple, ANGLE Metal Renderer: Apple M1 Pro, Unspecified Version)\",\"unmaskVendor\":\"Google Inc. (Apple)\"}", + # "captchaExtraParam": "{\"ua\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36\",\"userAgent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36\",\"timeZone\":\"UTC+8\",\"language\":\"zh\",\"cpuCoreCnt\":\"8\",\"platform\":\"MacIntel\",\"riskBrowser\":\"false\",\"webDriver\":\"false\",\"exactRiskBrowser\":\"false\",\"webDriverDeep\":\"false\",\"exactRiskBrowser2\":\"false\",\"webDriverDeep2\":\"false\",\"battery\":\"1\",\"plugins\":\"1a68ba429dd293b14e41a28b6535aa590\",\"resolution\":\"1512x982\",\"pixelDepth\":\"30\",\"colorDepth\":\"30\",\"canvasGraphFingerPrint\":\"10681dead230ee9ca8e67e8ddb0a96013\",\"canvasGraph\":\"10681dead230ee9ca8e67e8ddb0a96013\",\"canvasTextFingerPrintEn\":\"11825830fcdd552d4d3e81af588208d71\",\"canvasTextEn\":\"11825830fcdd552d4d3e81af588208d71\",\"canvasTextFingerPrintZh\":\"19257bc614b1b886224a819517f0715e3\",\"canvasTextZh\":\"19257bc614b1b886224a819517f0715e3\",\"webglGraphFingerPrint\":\"19e85c5151728b98f00fc5a3c6c8bd2dd\",\"webglGraph\":\"19e85c5151728b98f00fc5a3c6c8bd2dd\",\"webglGPUFingerPrint\":\"1777f4110dd337e62088858e6e8df0288\",\"webglGpu\":\"1777f4110dd337e62088858e6e8df0288\",\"cssFontFingerPrintEn\":\"1329bb845104882a7754a3bdf007ff6fc\",\"fontListEn\":\"1329bb845104882a7754a3bdf007ff6fc\",\"cssFontFingerPrintZh\":\"11997d7fc5c7f90fad6abcbadabebb249\",\"fontListZh\":\"11997d7fc5c7f90fad6abcbadabebb249\",\"voiceFingerPrint\":\"14c6f007f1166921565a8aa1c5cfac1c6\",\"audioTriangle\":\"14c6f007f1166921565a8aa1c5cfac1c6\",\"nativeFunc\":\"1973dcbb27a04c3a2ee240d9d2549e105\",\"key1\":\"web_84547d0256a5ac1f023cf5cb911e70a8\",\"key2\":1698917540365,\"key3\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36\",\"key4\":\"20030107\",\"key5\":\"zh\",\"key6\":\"Gecko\",\"key7\":1512,\"key8\":982,\"key9\":1512,\"key10\":950,\"key11\":360,\"key12\":360,\"key13\":945,\"key14\":1512,\"key15\":\"00000111\",\"key16\":1,\"key17\":1,\"key18\":[],\"key19\":{},\"key20\":[],\"key21\":{},\"key22\":[],\"key23\":{},\"key24\":[],\"key25\":{},\"key26\":{\"key27\":[\"0,1,21255,58,359,prepare1\",\"1,1,21261,31,332,prepare1\",\"2,1,21268,22,324,prepare1\",\"3,1,21277,5,309,prepare1\",\"4,1,21284,1,306,prepare1\",\"5,1,21600,2,304,prepare1\",\"6,1,21608,6,305,prepare1\",\"7,1,21617,10,306,prepare1\",\"8,1,21627,14,307,prepare1\",\"9,1,21632,20,308,prepare1\",\"10,3,22111,39,307\",\"11,1,22226,39,307,prepare2\",\"12,1,22230,42,307,prepare2\",\"13,1,22239,47,307,prepare2\",\"14,1,22247,53,307,prepare2\",\"15,1,22255,62,307,prepare2\",\"16,1,22264,75,307,prepare2\",\"17,1,22272,88,307,prepare2\",\"18,1,22281,95,307,prepare2\",\"19,1,22289,99,307,prepare2\",\"20,1,22297,105,307,prepare2\",\"21,4,22356,116,307\",\"22,2,22455,116,307,prepare3\",\"23,1,22456,117,307,prepare3\"],\"key28\":[],\"key29\":[],\"key30\":[],\"key31\":{\"prepare1\":\"9,1,21632,20,308\",\"prepare2\":\"20,1,22297,105,307\",\"prepare3\":\"23,1,22456,117,307\"},\"key32\":{},\"key33\":{},\"key34\":{}},\"key35\":\"bf3957c9488bdb2a7a453824e9ee2d6a\",\"key36\":\"f22a94013fc94e90e2af2798023a1985\",\"key37\":2,\"key38\":\"not support\",\"key39\":8}" + "gpuInfo": get_gpuInfo(), + "captchaExtraParam": '{"ua":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36","userAgent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36","timeZone":"UTC+8","language":"zh-CN","cpuCoreCnt":"2","platform":"MacIntel","riskBrowser":"false","webDriver":"false","exactRiskBrowser":"false","webDriverDeep":"false","exactRiskBrowser2":"false","webDriverDeep2":"false","battery":"1","plugins":"1a68ba429dd293b14e41a28b6535aa590","resolution":"1440x900","pixelDepth":"24","colorDepth":"24","canvasGraphFingerPrint":"170a0a884fada8bac5f9e42250271aaf4","canvasGraph":"170a0a884fada8bac5f9e42250271aaf4","canvasTextFingerPrintEn":"1996477fa0627ebb63c728fa32cacce79","canvasTextEn":"1996477fa0627ebb63c728fa32cacce79","canvasTextFingerPrintZh":"1528ffe56c43c1c514d0de433838f8134","canvasTextZh":"1528ffe56c43c1c514d0de433838f8134","webglGraphFingerPrint":"11a9644c09ae3fd0a918f6f6f712f43c6","webglGraph":"11a9644c09ae3fd0a918f6f6f712f43c6","webglGPUFingerPrint":"1bd896c110303ed46f715b5ac4cbf8329","webglGpu":"1bd896c110303ed46f715b5ac4cbf8329","cssFontFingerPrintEn":"1329bb845104882a7754a3bdf007ff6fc","fontListEn":"1329bb845104882a7754a3bdf007ff6fc","cssFontFingerPrintZh":"11997d7fc5c7f90fad6abcbadabebb249","fontListZh":"11997d7fc5c7f90fad6abcbadabebb249","voiceFingerPrint":"129d646e8a0c62f5c09bead9ff4a0f29d","audioTriangle":"129d646e8a0c62f5c09bead9ff4a0f29d","nativeFunc":"1973dcbb27a04c3a2ee240d9d2549e105"}' + } + + +class BaseURL(Enum): + # nodejs服务 + NodeURL = "http://127.0.0.1:3000/kuaishou/get_verifyParam" + + +def download_q(url, headers, cookies, data=None, is_proxy=False, timeout=10): + """ + 下载器 + :param url: + :param headers: + :param cookies: + :param data: + :param is_proxy: + :param timeout: + :return: + """ + proxy_list = [f"172.24.12.23:{random.randint(45001, 45250)}", f"172.18.128.225:{random.randint(45001, 45250)}", + "16HFBVJC:897944@u270.40.tp.16yun.cn:6448"] + # proxy = f"{random.choice(proxy_list)}:{random.randint(45001, 45250)}" # 修改代理 + proxy = random.choice(proxy_list) + proxies = {'http': proxy, 'https': proxy} # 代理初始化 + + if data: + if is_proxy: + response = requests.post(url, headers=headers, data=data, proxies=proxies, cookies=cookies, timeout=timeout) + else: + response = requests.post(url, headers=headers, data=data, cookies=cookies, timeout=timeout) + else: + if is_proxy: + response = requests.get(url, headers=headers, proxies=proxies, cookies=cookies, timeout=timeout) + else: + response = requests.get(url, headers=headers, cookies=cookies, timeout=timeout) + + return response + + +def download_pic(path, content): + with open(path, "wb") as f: + f.write(content) + f.close() + # logger.info(f"图片保存成功 》》》》{path}") + return path diff --git a/kuaishou/ks_make_trace.py b/kuaishou/ks_make_trace.py new file mode 100644 index 0000000..ca9729c --- /dev/null +++ b/kuaishou/ks_make_trace.py @@ -0,0 +1,58 @@ +import random + +class Generate_trajectory: + @staticmethod + def __ease_out_expo(sep): + if sep == 1: + return 1 + else: + return 1 - pow(2, -10 * sep) + + @staticmethod + def __generate_y(): + init_y = random.randint(10, 30) + y = [init_y] + for i in range(20): + _ = random.randint(-1, 1) + y += [y[-1] + _] * random.randint(5, 10) + return y + + @classmethod + def get_slide_track(cls, distance): + """ + 根据滑动距离生成滑动轨迹 + :param distance: 需要滑动的距离 + """ + + if not isinstance(distance, int) or distance < 0: + raise ValueError(f"distance类型必须是大于等于0的整数: distance: {distance}, type: {type(distance)}") + + # 共记录count次滑块位置信息 + count = 30 + int(distance / 20) + # 初始化滑动时间 + t = random.randint(50, 100) + # 记录上一次滑动的距离 + _x = random.randint(0, 5) + # _x = 0 + _y = cls.__generate_y() + + # 初始化轨迹列表 + slide_track = [ + '|'.join([str(random.randint(15, 30)), str(_y.pop(0)), str(0)]) + ] + + for i in range(count): + # 已滑动的横向距离 + x = round(cls.__ease_out_expo(i / count) * distance) + # 滑动过程消耗的时间 + t += random.randint(30, 40) + if x == _x: + continue + slide_track.append('|'.join([str(x), str(_y[i]), str(t)])) + _x = x + slide_track.append(slide_track[-1]) + return ','.join(slide_track) + + +if __name__ == '__main__': + print(Generate_trajectory().get_slide_track(500)) diff --git a/kuaishou/ks_slide.py b/kuaishou/ks_slide.py new file mode 100644 index 0000000..1b6f358 --- /dev/null +++ b/kuaishou/ks_slide.py @@ -0,0 +1,336 @@ +# coding:utf-8 +import json +import time +import os, sys +import argparse +# 相对路径补充 +root_path = os.path.abspath(os.path.dirname(__file__)).split('api-py')[0] + "api-py" +sys.path.append(root_path) + +from concurrent.futures._base import as_completed +from concurrent.futures.thread import ThreadPoolExecutor +from urllib.parse import urlparse, quote_plus, parse_qs +from utils.MysqlData import MysqlPoolClient, CRAWLER_DB_CONF_KS + +from kuaishou.ks_http import download_q, BaseHeaders, BaseParam, download_pic, BaseURL +from utils.Logger import MyLogger +from loguru import logger + +from kuaishou.ks_make_trace import Generate_trajectory +from utils.ImageHelper import recognize_gap +import traceback + + +class KSSlide(object): + """ + 快手滑块 + """ + + def __init__(self, count=5, is_proxy=False): + self.is_proxy = is_proxy + self.count = count + self.did_url = "https://www.kuaishou.com/short-video/3xdhjtb9xs7xpaw?authorId=3xsdu49r65skedk&streamSource=profile&area=profilexxnull" + self.api_url = "https://www.kuaishou.com/graphql" + self.headers = BaseHeaders.HEADERS.value + self.doc_headers = BaseHeaders.DOC_HEADERS.value + self.pic_headers = BaseHeaders.PIC_HEADERS.value + self.verify_headers = BaseHeaders.VERIFY_HEADERS.value + self.did = "" + self.captchaSession = "" + self.captcha_refer = "" + self.sql_list = [] + self.post_data = { + "search": { + "referer": "", + "data": { + "operationName": "visionSearchPhoto", + "variables": { + "keyword": "f1", + "pcursor": "", + "page": "search" + }, + "query": "fragment photoContent on PhotoEntity {\n __typename\n id\n duration\n caption\n originCaption\n likeCount\n viewCount\n commentCount\n realLikeCount\n coverUrl\n photoUrl\n photoH265Url\n manifest\n manifestH265\n videoResource\n coverUrls {\n url\n __typename\n }\n timestamp\n expTag\n animatedCoverUrl\n distance\n videoRatio\n liked\n stereoType\n profileUserTopPhoto\n musicBlocked\n}\n\nfragment recoPhotoFragment on recoPhotoEntity {\n __typename\n id\n duration\n caption\n originCaption\n likeCount\n viewCount\n commentCount\n realLikeCount\n coverUrl\n photoUrl\n photoH265Url\n manifest\n manifestH265\n videoResource\n coverUrls {\n url\n __typename\n }\n timestamp\n expTag\n animatedCoverUrl\n distance\n videoRatio\n liked\n stereoType\n profileUserTopPhoto\n musicBlocked\n}\n\nfragment feedContent on Feed {\n type\n author {\n id\n name\n headerUrl\n following\n headerUrls {\n url\n __typename\n }\n __typename\n }\n photo {\n ...photoContent\n ...recoPhotoFragment\n __typename\n }\n canAddComment\n llsid\n status\n currentPcursor\n tags {\n type\n name\n __typename\n }\n __typename\n}\n\nquery visionSearchPhoto($keyword: String, $pcursor: String, $searchSessionId: String, $page: String, $webPageArea: String) {\n visionSearchPhoto(keyword: $keyword, pcursor: $pcursor, searchSessionId: $searchSessionId, page: $page, webPageArea: $webPageArea) {\n result\n llsid\n webPageArea\n feeds {\n ...feedContent\n __typename\n }\n searchSessionId\n pcursor\n aladdinBanner {\n imgUrl\n link\n __typename\n }\n __typename\n }\n}\n" + }, + }, + "video": {}, + "user": { + "referer": "https://www.kuaishou.com/profile/3xsdu49r65skedk", + "data": { + "operationName": "visionProfilePhotoList", + "variables": { + "userId": "3xsdu49r65skedk", + "pcursor": "", + "page": "profile" + }, + "query": "fragment photoContent on PhotoEntity {\n __typename\n id\n duration\n caption\n originCaption\n likeCount\n viewCount\n commentCount\n realLikeCount\n coverUrl\n photoUrl\n photoH265Url\n manifest\n manifestH265\n videoResource\n coverUrls {\n url\n __typename\n }\n timestamp\n expTag\n animatedCoverUrl\n distance\n videoRatio\n liked\n stereoType\n profileUserTopPhoto\n musicBlocked\n}\n\nfragment recoPhotoFragment on recoPhotoEntity {\n __typename\n id\n duration\n caption\n originCaption\n likeCount\n viewCount\n commentCount\n realLikeCount\n coverUrl\n photoUrl\n photoH265Url\n manifest\n manifestH265\n videoResource\n coverUrls {\n url\n __typename\n }\n timestamp\n expTag\n animatedCoverUrl\n distance\n videoRatio\n liked\n stereoType\n profileUserTopPhoto\n musicBlocked\n}\n\nfragment feedContent on Feed {\n type\n author {\n id\n name\n headerUrl\n following\n headerUrls {\n url\n __typename\n }\n __typename\n }\n photo {\n ...photoContent\n ...recoPhotoFragment\n __typename\n }\n canAddComment\n llsid\n status\n currentPcursor\n tags {\n type\n name\n __typename\n }\n __typename\n}\n\nquery visionProfilePhotoList($pcursor: String, $userId: String, $page: String, $webPageArea: String) {\n visionProfilePhotoList(pcursor: $pcursor, userId: $userId, page: $page, webPageArea: $webPageArea) {\n result\n llsid\n webPageArea\n feeds {\n ...feedContent\n __typename\n }\n hostName\n pcursor\n __typename\n }\n}\n" + } + }, + "comment": { + "referer": "https://www.kuaishou.com/short-video/3xdhjtb9xs7xpaw?authorId=3xsdu49r65skedk&streamSource=profile&area=profilexxnull", + "data": { + "operationName": "commentListQuery", + "variables": { + "photoId": "3xdhjtb9xs7xpaw", + "pcursor": "" + }, + "query": "query commentListQuery($photoId: String, $pcursor: String) {\n visionCommentList(photoId: $photoId, pcursor: $pcursor) {\n commentCount\n pcursor\n rootComments {\n commentId\n authorId\n authorName\n content\n headurl\n timestamp\n likedCount\n realLikedCount\n liked\n status\n authorLiked\n subCommentCount\n subCommentsPcursor\n subComments {\n commentId\n authorId\n authorName\n content\n headurl\n timestamp\n likedCount\n realLikedCount\n liked\n status\n authorLiked\n replyToUserName\n replyTo\n __typename\n }\n __typename\n }\n __typename\n }\n}\n" + } + } + } + + pass + + def get_did(self): + """ + 通过单个视频页获得 did + """ + response = download_q(self.did_url, self.doc_headers, {}, is_proxy=self.is_proxy) + self.did = response.cookies["did"] + logger.info(f"获得did -> {self.did}") + return self.did + + def get_session(self, did, token="", type_="user"): + """ + 默认从comment接口拿session + """ + if not did: + if not self.did: + return + self.did = did + headers = self.headers.copy() + headers["Referer"] = self.post_data[type_]["referer"] + if token: + headers["Identity-Verification-Token"] = token + headers["Identity-verification-type"] = "2" + + data = json.dumps(self.post_data[type_]["data"], separators=(',', ':')) + cookies = { + "kpf": "PC_WEB", + "kpn": "KUAISHOU_VISION", + "clientid": "3", + "did": self.did + } + response = download_q(self.api_url, headers, cookies, data=data, is_proxy=self.is_proxy) + res = response.json() + status = res["data"].get("captcha") + if status: # 需要验证码 非 400002 暂时抛弃40002状态码 + captchaSession_url = res["data"]["captcha"]["url"] + params = parse_qs(urlparse(captchaSession_url).query) + elif res["data"].get("result"): + logger.warning("验证码异常 !400002 由于评论不出验证码 暂时放行! ") + captchaSession_url = res["data"]["url"] + params = parse_qs(urlparse(captchaSession_url).query) + else: # 有数据或者为空 不是验证码问题 + s = res.get("data", {}).get("visionProfilePhotoList", {}).get("result") + if s and s == 1: # 这里只对user生效 + logger.info(f"did 有效; 携带 token {token}") + else: + logger.warning(f"did 无效 {res}; 携带 token {token}") + return None, None + + self.captchaSession = params["captchaSession"][0] + self.captcha_refer = captchaSession_url + # logger.info(f"获得captchaSession 》》 {self.captchaSession}") + return self.captcha_refer, self.captchaSession + + def get_config(self, did, Referer, captchaSession, is_save=False): + """ + 获取验证码信息 + + """ + headers = self.verify_headers.copy() + headers["Referer"] = Referer + timeStamp = str(int(time.time())) + cookies = { + "did": did + } + url = "https://captcha.zt.kuaishou.com/rest/zt/captcha/sliding/config" + data = { + "captchaSession": captchaSession + } + response = download_q(url, headers, cookies, data=data, is_proxy=self.is_proxy) + data = response.json() + # logger.info(f"获得config -> {data}") + captchaSn = data.get("captchaSn") + + bgPicUrl = data.get("bgPicUrl") + f"?captchaSn={captchaSn}" + cutPicUrl = data.get("cutPicUrl") + f"?captchaSn={captchaSn}" + self.pic_headers["Referer"] = Referer + bgContent = download_q(bgPicUrl, self.pic_headers, cookies).content + fgcontent = download_q(cutPicUrl, self.pic_headers, cookies).content + + if is_save: + bg_path = f"tmp/{timeStamp}_bg_pic.jpg" + fg_path = f"tmp/{timeStamp}_fg_pic.jpg" + download_pic(bg_path, bgContent) + download_pic(fg_path, fgcontent) + else: + bg_path = bgContent + fg_path = fgcontent + + disY = data.get("disY") + verifyParam = self.build_param(bg_path, fg_path, captchaSn, disY) + + return verifyParam + + def build_param(self, bg, fg, captchaSn, y): + """ + 构造验证参数 模拟轨迹 + """ + distance = recognize_gap(bg, fg) + relativeX = int(distance * 0.46) # 缩放 + trajectory = Generate_trajectory().get_slide_track(int(distance * 1.76)) # 1.76 1.764 暂时测定稳定 + logger.info(f"缩放距离为 -> {relativeX}") + param = BaseParam.VERIFY_PARAM.value + param["captchaSn"] = captchaSn + param["relativeX"] = relativeX + param["relativeY"] = int(y * 0.46) # config接口里的y 缩放 136 * 56/122 () + param["trajectory"] = trajectory + + # param["gpuInfo"] = "" ## TODO: 需要随机替换 + # param["captchaExtraParam"] = "" + + def get_plaintext(t: dict): + concat_order = ["captchaSn", "bgDisWidth", "bgDisHeight", "cutDisWidth", "cutDisHeight", + "relativeX", "relativeY", "trajectory", "gpuInfo", "captchaExtraParam"] + return "&".join([k + "=" + quote_plus(str(t[k])).replace("+", "%20") for k in concat_order]) + + info = get_plaintext(param) + verifyParam = KSSlide.encrypt(info) + + return verifyParam + + @staticmethod + def encrypt(info): + """ + 调用node-js + """ + url = BaseURL.NodeURL.value + data = { + "info": info + } + response = download_q(url, {}, {}, data=data) + # logger.info(f"node服务获取加密数据") + return response.text + + def verify(self, verifyParam, did, refer): + headers = self.verify_headers.copy() + headers["Referer"] = refer + headers["Content-Type"] = "application/json" + cookies = { + "did": did + } + url = "https://captcha.zt.kuaishou.com/rest/zt/captcha/sliding/kSecretApiVerify" + + data1 = { + "verifyParam": verifyParam + } + data = json.dumps(data1, separators=(',', ':')) + response = download_q(url, headers, cookies, data=data, is_proxy=self.is_proxy) + logger.info(f"verify 结果 -> {response.json()}") + res = response.json() + captcha_token = res.get("captchaToken", None) + # logger.info(f"获得captchaToken为: {captcha_token}") + return captcha_token + + def run(self): + """ + 调用入口 + """ + try: + did = self.get_did() + referer, session = self.get_session(did) + if session: + verifyParam = self.get_config(did, referer, session) + if verifyParam: + token = self.verify(verifyParam, did, referer) + if token: + logger.success(f"验证码成功: -> {did}") + sql_demo = "INSERT INTO `ksCookie`(`cookie`, `token`) VALUES('%s', '%s');" % (did, token) + self.sql_list.append(sql_demo) + # logger.info(f"sql_demo {sql_demo}") + self.get_session(did=did, token=token, type_="user") + else: + logger.error("verify 失败") + else: + logger.error("node 生成 参数失败") + else: + logger.error("获得 session 失败") + except Exception as e: + logger.error(e) + traceback.print_exc() + + +# def write_file(l): +# with open("webdid.txt", "w") as f: +# f.write("\n".join(l)) +# f.close() +# logger.info("文件保存成功") + + +def insert_data(sql_list): + """ + 入库 + :param sql_list: + :return: + """ + client = MysqlPoolClient(CRAWLER_DB_CONF_KS) + for sql in sql_list: + try: + logger.success(f"insert cookie -> {sql}") + client.getOne(sql) + except Exception as e: + logger.error(f"insert cookie -> {sql}") + + +def create_by_thread(slid, count): + """ + 并发执行 + :param slid: + :param count: + :return: + """ + with ThreadPoolExecutor(max_workers=2) as t: + obj_list = [] + for i in range(count * 2): + obj = t.submit(slid.run) + obj_list.append(obj) + + insert_data(slide.sql_list) + logger.info(f"[sum] 并发任务 需要生成数量 {count}, 实际抓取数量 {count*2}, 实际生成数量 {len(slide.sql_list)}, 成功率 {len(slid.sql_list)/(count*2)}") + + +def create_by_for(slid, count): + """ + for循环执行 + :param slid: + :param count: + :return: + """ + num = 100 + i = 0 + while num > i: + if len(slid.sql_list) >= count: # 超出目标数量结束 + break + slid.run() + i += 1 + + insert_data(slide.sql_list) + logger.info(f"[sum] 循环任务 需要生成数量 {count}, 实际抓取数量 {i}, 实际生成数量 {len(slide.sql_list)}, 成功率 {len(slide.sql_list)/i}") + + +if __name__ == '__main__': + slide = KSSlide(is_proxy=True) + log = MyLogger() + parser = argparse.ArgumentParser(description='pass kuaishou cookie slider') + parser.add_argument('-c', type=int, default=10, help="needed cookie count;default count=10;") + parser.add_argument('-m', type=str, default="0", help="method: {0:for, 1:thread}; default method=0;") + args = parser.parse_args() + + method = { + "0": create_by_for, + "1": create_by_thread + } + args_count = args.c + args_method = args.m + method[args_method](slide, args_count) # 执行函数 diff --git a/main.py b/main.py new file mode 100644 index 0000000..94e3a87 --- /dev/null +++ b/main.py @@ -0,0 +1,16 @@ +# This is a sample Python script. + +# Press ⌃R to execute it or replace it with your code. +# Press Double ⇧ to search everywhere for classes, files, tool windows, actions, and settings. + + +def print_hi(name): + # Use a breakpoint in the code line below to debug your script. + print(f'Hi, {name}') # Press ⌘F8 to toggle the breakpoint. + + +# Press the green button in the gutter to run the script. +if __name__ == '__main__': + print_hi('PyCharm') + +# See PyCharm help at https://www.jetbrains.com/help/pycharm/ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..154119d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,40 @@ +aiohttp==3.8.3 +aiosignal==1.3.1 +asgiref==3.8.1 +async-timeout==4.0.3 +attrs==23.2.0 +blinker==1.7.0 +bottle==0.12.25 +certifi==2023.7.22 +charset-normalizer==2.0.12 +click==8.1.7 +DBUtils==3.0.3 +Flask==3.0.2 +frozenlist==1.4.1 +furl==2.1.3 +idna==3.4 +importlib-metadata==7.0.1 +itsdangerous==2.1.2 +Jinja2==3.1.3 +kafka==1.3.5 +kafka-python==2.0.2 +loguru==0.7.2 +MarkupSafe==2.1.5 +multidict==6.0.5 +numpy==1.24.4 +opencv-python==4.5.5.64 +orderedmultidict==1.0.1 +Paste==3.7.1 +Pillow==10.1.0 +pycryptodome==3.19.0 +PyExecJS==1.5.1 +PyMySQL==1.1.0 +requests==2.27.1 +scipy==1.10.1 +six==1.16.0 +typing_extensions==4.12.1 +urllib3==1.26.16 +uWSGI==2.0.23 +Werkzeug==3.0.1 +yarl==1.9.4 +zipp==3.17.0 diff --git a/server.py b/server.py new file mode 100644 index 0000000..c101ab7 --- /dev/null +++ b/server.py @@ -0,0 +1,266 @@ +import time +import json + +from dianping.mtgsig import mtgsig_run +from douyin.dy_abogus import run +from hnw.hnw_hook import build_headers +from flask import Flask, request +import traceback +from utils.Logger import MyLogger +from wx.video.video_decode import main +from xhs.shield import shield_run +from xhs.shield_aes import get_key +from xhs.xhs_captcha import SignalCaptcha +from xhs.xhs_param import get_xs, get_a1, get_comment + +app = Flask(__name__) +log = MyLogger() + + +""" + 部署 + nohup /root/anaconda3/envs/py3.8/bin/python server.py > /dev/null 2>&1 & + + 新部署 + 启动:uwsgi -d --ini uwsgi.ini + 停止:uwsgi --stop + 重启:uwsgi --reload +""" + + +@app.route("/getData", methods=['GET']) +def get_review_list(): + # shop_id = request.query.shopId + # page_num = request.query.pageNum + # page_num = int(page_num) * 14 + # review_list = main(shop_id, str(page_num)) + # ret = {"desc": "success", "result": review_list} + ret = "" + return json.dumps(ret, ensure_ascii=False) + + +@app.route("/hnw/get_header", methods=['GET']) +def get_hnw_headers(): + ret = {"code": 200, "data": {}} + try: + headers = build_headers() + ret["data"] = headers + except Exception as e: + ret["code"] = 500 + ret["data"] = f"{e}" + log.info(f"[hnw]: {ret}") + return json.dumps(ret, ensure_ascii=False) + + +@app.route("/xhs/get_header", methods=['POST']) +def get_xhs_headers(): + """ + 小红书x-s参数接口 + 参数:请求参数/a1/timeStamp + 对于cookie信息如果不传则本地生成一个 + :return: + """ + ret = {"code": 200, "data": {}} + try: + api_type = request.args.get("api") + a1 = request.args.get("a1") if request.args.get("a1") else get_a1("Mac OS")[0] + times = request.args.get("timStamp") if request.args.get("timStamp") else str(int(time.time() * 1000)) + data = request.get_data() + params = json.dumps(json.loads(data), ensure_ascii=False, separators=(',', ':')) + res = get_xs(a1, api_type, params, times) + ret["data"] = res + except Exception as e: + log.error(e) + ret = {"code": 500, "data": str(e)} + log.info(f"[xhs]: {ret}") + + return json.dumps(ret, ensure_ascii=False) + + +@app.route("/xhs/get_header_all", methods=['POST']) +def get_xhs_headers_all(): + """ + 小红书x-s、x-comment参数接口 + 参数:请求参数/a1/timeStamp + :return: + """ + ret = {"code": 200, "data": {}} + try: + api_type = request.args.get("api") + a1 = request.args.get("a1") + times = str(int(time.time() * 1000)) + data = request.get_data() + params = json.dumps(json.loads(data), ensure_ascii=False, separators=(',', ':')) + res = get_xs(a1, api_type, params, times) + res["x-s-common"] = get_comment(a1, res['x-s'], res['x-t']) + ret["data"] = res + except Exception as e: + log.error(e) + ret = {"code": 500, "data": str(e)} + log.info(f"[xhs]: {ret}") + + return json.dumps(ret, ensure_ascii=False) + + +@app.route("/xhs/pass_captcha", methods=['POST']) +def verify_captcha(): + """ + xhs验证码 + 461触发 + :return: + """ + ret = {"code": 200, "data": {}} + try: + data = request.get_data() + data = json.loads(data) + log.info(data) + web_session = data["web_session"] + verify_uuid = data["verify_uuid"] + source = data["source"] + a1 = data["a1"] + webId = data["webId"] + is_proxy = data.get("is_proxy") if data.get("is_proxy") else False + A = SignalCaptcha(web_session, verify_uuid, a1, webId, source=source, is_proxy=is_proxy) + res = A.run() + ret["data"] = res + except Exception as e: + log.error(e) + ret = {"code": 500, "data": str(e)} + + log.info(f"[xhs]: {ret}") + + return json.dumps(ret, ensure_ascii=False) + + +@app.route("/xhs/get_cookie", methods=['GET']) +def get_xhs_cookies(): + """ + 参数:platform 针对平台产生cookie 可能会影响风控 + 种类:Android/iOS/Mac OS/Linux/Windows(暂时没发现用) + :return: + """ + ret = {"code": 200, "data": {}} + try: + platform = request.args.get("platform") if request.args.get("platform") else "Mac OS" + a1, web_id = get_a1(platform) + ret["data"] = { + "a1": a1, + "webId": web_id + } + except Exception as e: + ret["code"] = 500 + ret["data"] = f"{e}" + + log.info(f"[xhs]: {ret}") + return json.dumps(ret, ensure_ascii=False) + + +@app.route("/wx/decode", methods=["POST"]) +async def get_upload_path(): + """ + 读取视频流 + :return: + """ + if 'file' not in request.files: + return json.dumps({"code": 500, "msg": "视频流文件不存在"}, ensure_ascii=False) + file = request.files["file"] + decode = request.args.get("decodekey") + log.info(f"[wx]: 获取的decode {decode}") + upload_path = await main(file, decode) + log.info(f"[wx]: {upload_path}") + return json.dumps(upload_path, ensure_ascii=False) + + +@app.route("/wx/decode", methods=["GET"]) +async def get_upload_path1(): + """ + 读取本地文件 + :return: + """ + decode = request.args.get("decodekey") + localpath = request.args.get("localpath") + try: + files = {'file': open(localpath, 'rb')} + except Exception as e: + log.error(f"[wx] error {e}") + return json.dumps({"code":500, "msg": "视频读取失败"}, ensure_ascii=False) + + log.info(f"[wx]: 获取的decode {decode}") + upload_path = await main(files["file"], decode) + log.info(f"[wx]: {upload_path}") + return json.dumps(upload_path, ensure_ascii=False) + + +@app.route("/dy/get_abogus", methods=["POST"]) +async def get_abogus(): + """ + 读取本地文件 + :return: + """ + + try: + data = json.loads(request.get_data()) + ua = data.get("ua") + param = data.get("param") + a_bogus = run(param, ua) + res = {"code": 200, "data": a_bogus} + log.info(f"[dy]: {len(a_bogus)} => {res}") + return json.dumps(res, ensure_ascii=False) + except Exception as e: + log.error(f"[dy] error {e}") + return json.dumps({"code":500, "msg": "算法生成失败"}, ensure_ascii=False) + + +@app.route("/dp/get_mtgsig", methods=["POST"]) +async def get_mtgsig(): + """ + 获取小程序mtgsig1.2 + :return: + """ + try: + data = json.loads(request.get_data()) + url = data.get("url") + a3 = data.get("a3") + b8 = data.get("b8") # 循环的次数 + a6 = data.get("a6") + if not b8 or not a6 or not a3: + raise Exception("check b8/a6/a3 params!!!") + mtgsig = mtgsig_run(url, b8, a3, a6) + res = {"code": 200, "data": mtgsig} + log.info(f"[mtgsig]: => {res}") + return json.dumps(res, ensure_ascii=False) + except Exception as e: + log.error(f"[mtgsig] error {e}") + return json.dumps({"code":500, "msg": "算法生成失败"}, ensure_ascii=False) + + +@app.route("/xhs/get_shield", methods=['POST']) +async def get_app_xhs_headers(): + """ + 小红书app端 + shield本地生成 + :return: + """ + ret = {"code": 200, "data": {}} + try: + data = request.get_data() + data_json = json.loads(data) + hmac_main = data_json["hmac_main"] + url = data_json["url"] + xy_common_params = data_json["xy_common_params"] + deviceId = data_json["deviceId"] + api = data_json["api"] # /api/sns/v1/note/feed 默认 + keys = get_key(deviceId, hmac_main) # 获得keys + res = shield_run(url, keys, xy_common_params, deviceId, api) # 加密流程 + ret["data"] = res + except Exception as e: + log.error(e) + traceback.print_exc() + ret = {"code": 500, "data": str(e)} + log.info(f"[xhs]: {ret}") + + return json.dumps(ret, ensure_ascii=False) + + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=8088) diff --git a/utils/ImageHelper.py b/utils/ImageHelper.py new file mode 100644 index 0000000..25cfcd1 --- /dev/null +++ b/utils/ImageHelper.py @@ -0,0 +1,205 @@ +import io +import random +import time +import cv2 as cv +import numpy as np +from PIL import Image +from scipy import signal + + +# format_list = [0, 10, 16, 6, 13, 3, 9, 15, 11, 19, 14, 18, 4, 12, 2, 1, 8, 17, 7, 5] +# this.canvasCtx.drawImage(this.img, +# 30 * i, 开始剪切的 x 坐标位置。 +# 0, 开始剪切的 y 坐标位置。 +# 30, 被剪切图像的宽度。 +# 400, 被剪切图像的高度。 +# 30 * keylist[i] / 1.5, 在画布上放置图像的 x 坐标位置 +# 0, 在画布上放置图像的 y 坐标位置。 +# offset / 1.5, 要使用的图像的宽度。(伸展或缩小图像) +# 200) 要使用的图像的高度。(伸展或缩小图像) +def format_slide_img(raw_img: bytes, format_list: list) -> bytes: + fp = io.BytesIO(raw_img) + img = Image.open(fp) + image_dict = {} + offset = 30 + for i in range(len(format_list)): + box = (i * offset, 0, offset + (i * offset), 400) # 左(起始),上(不变),右(宽),下(不变) + image_dict[format_list[i]] = img.crop(box) + image_list = [] + for i in sorted(image_dict): + image_list.append(image_dict[i]) + image_num = len(image_list) + image_size = image_list[0].size + height = image_size[1] + width = image_size[0] + new_img = Image.new('RGB', (image_num * width, height), 255) + x = y = 0 + for img in image_list: + new_img.paste(img, (x, y)) + x += width + box = (0, 0, 600, 400) + new_img = new_img.crop(box) + # 保存图片 + processClickImgIoFlow = io.BytesIO() + + new_img.save(processClickImgIoFlow, format="JPEG") + return processClickImgIoFlow.getvalue() + # with open("test.jpg", "wb") as f: + # f.write(processClickImgIoFlow.getvalue()) + +# 1 +def discern_gap(gapImage: bytes, sliderImage: bytes, show=False): + + def edge_detection(rawimg): + def tracebar(x): + threshold1 = cv.getTrackbarPos('threshold1', 'Test') + threshold2 = cv.getTrackbarPos('threshold2', 'Test') + edged_img = cv.Canny(img_Gaussian, threshold1, threshold2) + cv.imshow("edged_img", edged_img) + + image = np.asarray(bytearray(rawimg), dtype="uint8") + img = cv.imdecode(image, cv.IMREAD_COLOR) + grep_img = cv.cvtColor(img, cv.COLOR_BGR2GRAY) + # 高斯滤波 高斯滤波是通过对输入数组的每个点与输入的高斯滤波模板执行卷积计算然后将这些结果一块组成了滤波后的输出数组, + # 通俗的讲就是高斯滤波是对整幅图像进行加权平均的过程,每一个像素点的值都由其本身和邻域内的其他像素值经过加权平均后得到。 + # 高斯滤波的具体操作是:用一个模板(或称卷积、掩模)扫描图像中的每一个像素,用模板确定的邻域内像素的加权平均灰度值去替代模板中心像素点的值。 + img_Gaussian = cv.GaussianBlur(grep_img, (5, 5), 0) + # 用于对图像的边缘检测 + edged_img = cv.Canny(img_Gaussian, 25, 45) + if show: + cv.namedWindow("Test") + # cv.imshow('raw_img', img) + # cv.imshow('grep_img', grep_img) + cv.imshow('img_Gaussian', img_Gaussian) + cv.createTrackbar("threshold1", "Test", 0, 255, tracebar) + cv.createTrackbar("threshold2", "Test", 0, 255, tracebar) + cv.imshow('edged_img', edged_img) + cv.waitKey(3000) + cv.destroyAllWindows() + return edged_img + + def similarity_calculation(background, slider): + result = cv.matchTemplate(background, slider, cv.TM_CCOEFF_NORMED) + # 获取一个/组int类型的索引值在一个多维数组中的位置。 + # x, y = np.unravel_index(result.argmax(), result.shape) + min_val, max_val, min_loc, max_loc = cv.minMaxLoc(result) + return max_loc + + """计算滑动距离方法""" + gap = edge_detection(gapImage) + slider = edge_detection(sliderImage) + x, y = similarity_calculation(gap, slider) + # print('需要滑动距离', x, y) + # todo 返回的距离 + return x + + +def discern_gap2(gap_path, slider_path, save=False): + def pic2grep(pic_path, type) -> np.ndarray: + pic_path_rgb = cv.imread(pic_path) + pic_path_gray = cv.cvtColor(pic_path_rgb, cv.COLOR_BGR2GRAY) + if save: + cv.imwrite(f"./{type}.jpg", pic_path_gray) + return pic_path_gray + + def canny_edge(image_array: np.ndarray, show=False) -> np.ndarray: + can = cv.Canny(image_array, threshold1=200, threshold2=300) + if show: + cv.imshow('candy', can) + cv.waitKey() + cv.destroyAllWindows() + return can + + def clear_white(img: str, show=False) -> np.ndarray: + img = cv.imread(img) + rows, cols, channel = img.shape + min_x = 255 + min_y = 255 + max_x = 0 + max_y = 0 + for x in range(1, rows): + for y in range(1, cols): + t = set(img[x, y]) + if len(t) >= 2: + if x <= min_x: + min_x = x + elif x >= max_x: + max_x = x + + if y <= min_y: + min_y = y + elif y >= max_y: + max_y = y + img1 = img[min_x:max_x, min_y:max_y] + if show: + cv.imshow('img1', img1) + cv.waitKey() + cv.destroyAllWindows() + return img1 + + def convolve2d(bg_array: np.ndarray, fillter: np.ndarray) -> np.ndarray: + bg_h, bg_w = bg_array.shape[:2] + fillter = fillter[::-1,::-1] + fillter_h, fillter_w = fillter.shape[:2] + c_full = signal.convolve2d(bg_array, fillter, mode="full") + kr, kc = fillter_h // 2, fillter_w // 2 + c_same = c_full[ + fillter_h - kr - 1: bg_h + fillter_h - kr - 1, + fillter_w - kc - 1: bg_w + fillter_w - kc - 1, + ] + return c_same + + def find_max_point(arrays: np.ndarray, search_on_horizontal_center=False) -> tuple: + max_point = 0 + max_point_pos = None + + array_rows, array_cols = arrays.shape + + if search_on_horizontal_center: + for col in range(array_cols): + if arrays[array_rows // 2, col] > max_point: + max_point = arrays[array_rows // 2, col] + max_point_pos = col, array_rows // 2 + else: + for row in range(array_rows): + for col in range(array_cols): + if arrays[row, col] > max_point: + max_point = arrays[row, col] + max_point_pos = col, row + return max_point_pos + + gap_grep = pic2grep(gap_path, "gap") + gap_can = canny_edge(gap_grep, False) + clear_slider = cv.imread(slider_path) # clear_white(slider_path, False) + slider_can = canny_edge(clear_slider, False) + convolve2d_result = convolve2d(gap_can, slider_can) + result = find_max_point(convolve2d_result, True) + print(result) + +def recognize_gap(bg, fg, is_show=False): + if isinstance(bg, str): + with open(fg, "rb") as f: + sliderImage = f.read() + # print(sliderImage) + with open(bg, "rb") as f: + gapImage = f.read() + else: + sliderImage = fg + gapImage = bg + + res = discern_gap(gapImage,sliderImage, is_show) + return res + + +if __name__ == '__main__': + with open("../kuaishou/tmp/img.png", "rb") as f: + sliderImage = f.read() + # print(sliderImage) + with open("../kuaishou/tmp/img_1.png", "rb") as f: + gapImage = f.read() + res = discern_gap(gapImage,sliderImage, True) + print(res * 56/122) + # print(random.randint(100, 200)) +# 256 + + diff --git a/utils/Logger.py b/utils/Logger.py new file mode 100644 index 0000000..8ea09a7 --- /dev/null +++ b/utils/Logger.py @@ -0,0 +1,112 @@ +import os +from functools import wraps +from time import perf_counter + +from loguru import logger + + +# from loguru._logger import Logger + + +class MyLogger: + """ + 根据时间、文件大小切割日志 + """ + + def __init__(self, log_dir='logs', max_size=20, retention='7 days'): + self.log_dir = log_dir + self.max_size = max_size + self.retention = retention + self.logger = self.configure_logger() + + def configure_logger(self): + """ + + Returns: + + """ + # 创建日志目录 + os.makedirs(self.log_dir, exist_ok=True) + + shared_config = { + "level": "DEBUG", + "enqueue": True, + "backtrace": True, + "format": "{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}", + } + + # 添加按照日期和大小切割的文件 handler + logger.add( + sink=f"{self.log_dir}/{{time:YYYY-MM-DD}}.log", + rotation=f"{self.max_size} MB", + retention=self.retention, + **shared_config + ) + + # 配置按照等级划分的文件 handler 和控制台输出 + logger.add(sink=self.get_log_path, **shared_config) + + return logger + + def get_log_path(self, message: str) -> str: + """ + 根据等级返回日志路径 + Args: + message: + + Returns: + + """ + log_level = message.record["level"].name.lower() + log_file = f"{log_level}.log" + log_path = os.path.join(self.log_dir, log_file) + + return log_path + + def __getattr__(self, level: str): + return getattr(self.logger, level) + + def log_decorator(self, msg="快看,异常了,别唧唧哇哇,快排查"): + """ + 日志装饰器,记录函数的名称、参数、返回值、运行时间和异常信息 + Args: + logger: 日志记录器对象 + + Returns: + 装饰器函数 + + """ + + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + self.logger.info(f'-----------分割线-----------') + self.logger.info(f'调用 {func.__name__} args: {args}; kwargs:{kwargs}') + start = perf_counter() # 开始时间 + try: + result = func(*args, **kwargs) + end = perf_counter() # 结束时间 + duration = end - start + self.logger.info(f"{func.__name__} 返回结果:{result}, 耗时:{duration:4f}s") + return result + except Exception as e: + self.logger.exception(f"{func.__name__}: {msg}") + self.logger.info(f"-----------分割线-----------") + # raise e + + return wrapper + + return decorator + + +if __name__ == '__main__': + log = MyLogger() + + + for i in range(1000): + log.error('错误信息') + log.critical('严重错误信息') + log.debug('调试信息') + log.info('普通信息') + log.success('成功信息') + log.warning('警告信息') \ No newline at end of file diff --git a/utils/MysqlData.py b/utils/MysqlData.py new file mode 100644 index 0000000..cafee01 --- /dev/null +++ b/utils/MysqlData.py @@ -0,0 +1,234 @@ +import threading + +import pymysql +from pymysql.cursors import DictCursor +from dbutils.pooled_db import PooledDB + + +class CRAWLER_DB_CONF(object): + """ + 点评 + """ + DBHOST = '172.18.1.181' + DBPORT = 3306 + DBUSER = 'crawl' + DBPWD = 'crawl' + DBNAME = 'dianpinggfy' + DBCHAR = 'utf8' + DB_FULL_NAME = "dianpinggfy" + + +class CRAWLER_DB_CONF_KS(object): + """ + 快手 + """ + DBHOST = '172.18.1.134' + DBPORT = 3306 + DBUSER = 'crawl666' + DBPWD = 'lx2a4jN1xFT96kj20LU=' + DBNAME = 'KS_storage' + DBCHAR = 'utf8' + DB_FULL_NAME = "KS_storage" + + +class CRAWLER_DB_CONF_DY(object): + """ + 抖音 + """ + DBHOST = '172.18.1.181' + DBPORT = 3306 + DBUSER = 'crawl' + DBPWD = 'crawl' + DBNAME = 'test' + DBCHAR = 'utf8' + DB_FULL_NAME = "test" + + +class MysqlPoolClient(object): + """ + MYSQL数据库对象,负责产生数据库连接 , 此类中的连接采用连接池实现获取连接对象:conn = Mysql.getConn() + 释放连接对象;conn.close()或del conn + + 备注:单步进入 + """ + # 连接池对象 + __pool = {} + __lock = threading.Lock() + + # TODO(YaoPeng): 反复加锁影响性能,但是爬虫场景下,可以暂时容忍 + def __init__(self, db_conf): + MysqlPoolClient.__lock.acquire() + # 数据库构造函数,从连接池中取出连接,并生成操作游标 + # pip install DBUtils + self._conn = MysqlPoolClient.__getConn(db_conf) + self._cursor = self._conn.cursor() + MysqlPoolClient.__lock.release() + + def __del__(self): + self.dispose() + + @staticmethod + def __getConn(db_conf): + pool_name = db_conf.DB_FULL_NAME + """ + @summary: 静态方法,从连接池中取出连接 + @return MySQLdb.connection + """ + if pool_name not in MysqlPoolClient.__pool: + MysqlPoolClient.__pool[pool_name] = PooledDB(creator=pymysql, + mincached=1, + maxcached=20, + host=db_conf.DBHOST, + port=db_conf.DBPORT, + user=db_conf.DBUSER, + passwd=db_conf.DBPWD, + db=db_conf.DBNAME, + use_unicode=True, + charset=db_conf.DBCHAR, + cursorclass=DictCursor) + return MysqlPoolClient.__pool[pool_name].connection() + + def getAll(self, sql, param=None): + """ + @summary: 执行查询,并取出所有结果集 + @param sql:查询SQL,如果有查询条件,请只指定条件列表,并将条件值使用参数[param]传递进来 + @param param: 可选参数,条件列表值(元组/列表) + @return: result list(字典对象)/boolean 查询到的结果集 + """ + if param is None: + count = self._cursor.execute(sql) + else: + count = self._cursor.execute(sql, param) + if count > 0: + query_result = self._cursor.fetchall() + else: + query_result = False + return query_result + + def getOne(self, sql, param=None): + """ + @summary: 执行查询,并取出第一条 + @param sql:查询SQL,如果有查询条件,请只指定条件列表,并将条件值使用参数[param]传递进来 + @param param: 可选参数,条件列表值(元组/列表) + @return: result list/boolean 查询到的结果集 + """ + if param is None: + count = self._cursor.execute(sql) + else: + count = self._cursor.execute(sql, param) + if count > 0: + query_result = self._cursor.fetchone() + else: + query_result = False + return count,query_result + + def getMany(self, sql, num, param=None): + """ + @summary: 执行查询,并取出num条结果 + @param sql:查询SQL,如果有查询条件,请只指定条件列表,并将条件值使用参数[param]传递进来 + @param num:取得的结果条数 + @param param: 可选参数,条件列表值(元组/列表) + @return: result list/boolean 查询到的结果集 + """ + if param is None: + count = self._cursor.execute(sql) + else: + count = self._cursor.execute(sql, param) + if count > 0: + query_result = self._cursor.fetchmany(num) + else: + query_result = False + return query_result + + def insertOne(self, sql, value=None): + """ + @summary: 向数据表插入一条记录 + @param sql:要插入的SQL格式 + @param value:要插入的记录数据tuple/list + @return: insertId 受影响的行数 + """ + self._cursor.execute(sql, value) + return self.__getInsertId() + + def insertMany(self, sql, values): + """ + @summary: 向数据表插入多条记录 + @param sql:要插入的SQL格式 + @param values:要插入的记录数据tuple(tuple)/list[list] + @return: count 受影响的行数 + """ + count = self._cursor.executemany(sql, values) + return count + + def updateMany(self, sql, values): + """ + @summary: 向数据表更新多条记录 + @param sql:要插入的SQL格式 + @param values:要插入的记录数据tuple(tuple)/list[list] + @return: count 受影响的行数 + """ + count = self._cursor.executemany(sql, values) + return count + + def __getInsertId(self): + """ + 获取当前连接最后一次插入操作生成的id,如果没有则为0 + """ + self._cursor.execute("SELECT @@IDENTITY AS id") + result = self._cursor.fetchall() + return result[0]['id'] + + def __query(self, sql, param=None, commit=True): + if param is None: + count = self._cursor.execute(sql) + else: + count = self._cursor.execute(sql, param) + if commit: + self._conn.commit() + return count + + def update(self, sql, param=None): + """ + @summary: 更新数据表记录 + @param sql: SQL格式及条件,使用(%s,%s) + @param param: 要更新的 值 tuple/list + @return: count 受影响的行数 + """ + return self.__query(sql, param) + + def delete(self, sql, param=None): + """ + @summary: 删除数据表记录 + @param sql: SQL格式及条件,使用(%s,%s) + @param param: 要删除的条件 值 tuple/list + @return: count 受影响的行数 + """ + return self.__query(sql, param) + + def begin(self): + """ + @summary: 开启事务 + """ + self._conn.autocommit(0) + + def end(self, option='commit'): + """ + @summary: 结束事务 + """ + if option == 'commit': + self._conn.commit() + else: + self._conn.rollback() + + def dispose(self, is_end=1): + """ + @summary: 释放连接池资源 + """ + MysqlPoolClient.__lock.acquire() + if is_end == 1: + self.end('commit') + else: + self.end('rollback') + self._cursor.close() + self._conn.close() + MysqlPoolClient.__lock.release() \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/rotate_captcha.py b/utils/rotate_captcha.py new file mode 100644 index 0000000..5d554d6 --- /dev/null +++ b/utils/rotate_captcha.py @@ -0,0 +1,123 @@ +#!/usr/bin/env/ python +# -*- coding: utf-8 -*- +""" +@Time : 2023/2/12 11:20 +@Author : 余半盏 +@File : rotate_captcha.py +@Software: PyCharm +""" +import cv2 +import math +import numpy as np +from loguru import logger +import time + +from utils.tool import cost_time + + +def circle_point_px(img, accuracy_angle, r=None): + rows, cols, _ = img.shape + assert 360 % accuracy_angle == 0 + x0, y0 = r0, _ = (rows // 2, cols // 2) + if r: r0 = r + angles = np.arange(0, 360, accuracy_angle) + cos_angles = np.cos(np.deg2rad(angles)) + sin_angles = np.sin(np.deg2rad(angles)) + + x = x0 + r0 * cos_angles + y = y0 + r0 * sin_angles + + x = np.round(x).astype(int) + y = np.round(y).astype(int) + + circle_px_list = img[x, y] + return circle_px_list + + +def rotate(image, angle, center=None, scale=1.0): # 1 + (h, w) = image.shape[:2] # 2 + if center is None: # 3 + center = (w // 2, h // 2) # 4 + M = cv2.getRotationMatrix2D(center, angle, scale) # 5 + rotated = cv2.warpAffine(image, M, (w, h)) # 6 + return rotated + + +def HSVDistance(c1, c2): + """ + 计算两个颜色的 HSV(Hue, Saturation, Value)空间中的距离 + :param c1: + :param c2: + :return: + """ + y1 = 0.299 * c1[0] + 0.587 * c1[1] + 0.114 * c1[2] + u1 = -0.14713 * c1[0] - 0.28886 * c1[1] + 0.436 * c1[2] + v1 = 0.615 * c1[0] - 0.51498 * c1[1] - 0.10001 * c1[2] + y2 = 0.299 * c2[0] + 0.587 * c2[1] + 0.114 * c2[2] + u2 = -0.14713 * c2[0] - 0.28886 * c2[1] + 0.436 * c2[2] + v2 = 0.615 * c2[0] - 0.51498 * c2[1] - 0.10001 * c2[2] + rlt = math.sqrt((y1 - y2) * (y1 - y2) + (u1 - u2) * (u1 - u2) + (v1 - v2) * (v1 - v2)) + return rlt + + +def crop_to_square(image): + height, width = image.shape[:2] + size = min(height, width) + start_y = (height - size) // 2 + start_x = (width - size) // 2 + cropped = image[start_y:start_y+size, start_x:start_x+size] + return cropped + + +@cost_time +def single_discern(inner_image_brg:bytes, outer_image_brg:bytes): + inner_image = cv2.cvtColor(inner_image_brg, cv2.COLOR_BGR2HSV) # 颜色转换 + outer_image = cv2.cvtColor(outer_image_brg, cv2.COLOR_BGR2HSV) + outer_image = crop_to_square(outer_image) + all_deviation = [] + for result in range(0, 360): + inner = rotate(inner_image, -result) # 顺时针 + outer = rotate(outer_image, 0) + inner_circle_point_px = circle_point_px(inner, 1, 95) + outer_circle_point_px = circle_point_px(outer, 1, 105) + total_deviation = 0 + for i in range(len(inner_circle_point_px)): + in_px = inner_circle_point_px[i] + out_px = outer_circle_point_px[i] + deviation = HSVDistance(in_px, out_px) + total_deviation += deviation + all_deviation.append(total_deviation) + result = all_deviation.index(min(all_deviation)) + + return result + + +def export_single_discern(fg_path, bg_path): + """ + 对于下载的图片转为content处理 + :param fg_path: + :param bg_path: + :return: + """ + if isinstance(fg_path, str): + with open(fg_path, "rb") as f: + inner_image_brg = f.read() + with open(bg_path, "rb") as f: + outer_image_brg = f.read() + else: + inner_image_brg = fg_path + outer_image_brg = bg_path + + image = np.asarray(bytearray(inner_image_brg), dtype="uint8") + inner_image_brg = cv2.imdecode(image, cv2.IMREAD_COLOR) + + image = np.asarray(bytearray(outer_image_brg), dtype="uint8") + outer_image_brg = cv2.imdecode(image, cv2.IMREAD_COLOR) + + result = single_discern(inner_image_brg, outer_image_brg) + return result + + + +if __name__ == '__main__': + export_single_discern('./tmp/fg_2.png', './tmp/bg_2.png') # 310 \ No newline at end of file diff --git a/utils/tmp/bg_2.png b/utils/tmp/bg_2.png new file mode 100644 index 0000000..e98aa09 Binary files /dev/null and b/utils/tmp/bg_2.png differ diff --git a/utils/tmp/fg_2.png b/utils/tmp/fg_2.png new file mode 100644 index 0000000..5ac519b Binary files /dev/null and b/utils/tmp/fg_2.png differ diff --git a/utils/tool.py b/utils/tool.py new file mode 100644 index 0000000..f5e6c42 --- /dev/null +++ b/utils/tool.py @@ -0,0 +1,143 @@ +import json +import random + +import aiohttp +import requests +from aiohttp import FormData +from loguru import logger +import functools, time + + +def retry(exceptions: (BaseException, tuple, list)=BaseException, + max_retries: int = 2, + delay: int = 1, + sleep=time.sleep, + if_result=None): + """ + 重试 + :param exceptions: 可自定义异常 + :param max_retries: 重试次数 + :param delay: 休眠时间 + :param sleep: time.sleep + :param if_result: 是否对结果进行约束 重试 + :return: + """ + if not isinstance(exceptions, (tuple, list)): + new_exceptions = [exceptions] + else: + new_exceptions = exceptions + + def init_retry(func, count, *args, **kwargs): + if count > max_retries: + return + + try: + if if_result: + # print(f"重拾次数 is {count}") + return call(func, if_result, *args, **kwargs) + return func(*args, **kwargs) + except tuple(new_exceptions) as e: + # print("重试") + # traceback.print_exc() + if count < max_retries: + sleep(delay) + return init_retry(func, count+1, *args, **kwargs) + else: + raise e + + def call(func, if_result, *args, **kwargs): + val = func(*args, **kwargs) + if if_result(*val): + return val + raise Exception("The result is not as expected!") + + def decorator(func): + @functools.wraps(func) + def wrapped(*args, **kwargs): + return init_retry(func, 0, *args, **kwargs) + return wrapped + return decorator + + +def cost_time(func): + """ + 计算耗时 + :param func: + :return: + """ + @functools.wraps(func) + def wrapper(*args, **kwargs): + start_time = time.time() + result = func(*args, **kwargs) + logger.info(f"{func.__name__} 请求耗时:{time.time() - start_time}") + return result + return wrapper + + +def retry_if_result_none(status, content): + if status == 200: + return True + return False + + +@retry(max_retries=3, delay=1, if_result=retry_if_result_none) +def download_q(url, headers, cookies, data=None, is_proxy=False, timeout=10): + """ + 下载器 + :param url: + :param headers: + :param cookies: + :param data: + :param is_proxy: + :param timeout: + :return: + """ + + proxy = "16ABDDQU:153219@u1590.20.tp.16yun.cn:6447" + proxy = f"172.24.12.23:{random.randint(45001, 45100)}" # 修改代理 + proxies = {'http': proxy, 'https': proxy} # 代理初始化 + + try: + if data: + if is_proxy: + response = requests.post(url, headers=headers, data=data, proxies=proxies, cookies=cookies, timeout=timeout) + else: + response = requests.post(url, headers=headers, data=data, cookies=cookies, timeout=timeout) + else: + if is_proxy: + response = requests.get(url, headers=headers, proxies=proxies, cookies=cookies, timeout=timeout) + else: + response = requests.get(url, headers=headers, cookies=cookies, timeout=timeout) + status = response.status_code + except Exception as e: + response = None + status = 0 + logger.error(f"download {e}") + # print(response) + return status, response + + +async def upload_file(file_path, content, url="http://172.18.1.113:8080/upload"): + """ + 异步上传 + :param file_path: + :param url: + :return: + """ + if file_path: + # timeout = aiohttp.ClientTimeout(total=30) + async with aiohttp.ClientSession() as session: + data = FormData() + data.add_field("file", content, filename=file_path, content_type='multipart/form-data;charset=utf-8"') + data.add_field("output", "json") + async with session.post(url, data=data) as response: + result = await response.text() # 返回结果为json字符串 + result = json.loads(result) + logger.info(f"upload file {result}") + if 'url' in result.keys(): + video_path = result['url'] + return video_path + else: + raise Exception + else: + raise Exception diff --git a/uwsgi.ini b/uwsgi.ini new file mode 100644 index 0000000..afd6565 --- /dev/null +++ b/uwsgi.ini @@ -0,0 +1,11 @@ +[uwsgi] +chdir=/data1/api-py +http=0.0.0.0:8088 +stats=%(chdir)/uwsgi/uwsgi.status +pidfile=%(chdir)/uwsgi/uwsgi.pid +wsgi-file=%(chdir)/server.py +daemonize=%(chdir)/uwsgi/uwsgi.log +callable=app +processes=2 +threads=2 +buffer-size=65536 \ No newline at end of file diff --git a/wx/__init__.py b/wx/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wx/video/__init__.py b/wx/video/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wx/video/video_decode.py b/wx/video/video_decode.py new file mode 100644 index 0000000..bd95f3c --- /dev/null +++ b/wx/video/video_decode.py @@ -0,0 +1,59 @@ +import requests, os + +from utils.tool import upload_file +import traceback + + +def decode(content, decodekey): + """ + 调用三方服务解密 + :param content: + :param decodekey: + :return: + """ + url = "http://127.0.0.1:3000/wx/decode1" + files = {'file': content} + param = { + "decodekey": decodekey + } + response = requests.post(url, files=files, params=param) + return response.content + + +def save_video(content, filename): + """ + 保存文件到 tmp下 返回绝对路径 + :param content: + :param filename: + :return: + """ + tmp_file = f"{os.getcwd()}/wx/tmp/{filename}" + with open(tmp_file, 'wb') as f: + f.write(content) + f.close() + return filename + + +async def main(content, decodekey): + """ + 处理上传文件流 + - 成功则成功; + - 失败则保存在本地 + :param content: + :param decodekey: + :return: + """ + file_name = f"{decodekey}.mp4" + res = {"code": 200, "msg": "上传成功", "filePath": ""} + try: + content = decode(content, decodekey) + upload_file_name = await upload_file(file_name, content) + except Exception as e: # 失败则保存本地 + traceback.print_exc() + upload_file_name = save_video(content, file_name) + res["code"] = 203 + res["msg"] = "上传失败,保存到本地" + + res["filePath"] = upload_file_name + + return res \ No newline at end of file diff --git a/xhs/__init__.py b/xhs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/xhs/shield.py b/xhs/shield.py new file mode 100644 index 0000000..95d8533 --- /dev/null +++ b/xhs/shield.py @@ -0,0 +1,132 @@ +import base64 + +from xhs.shield_aes import get_key +from xhs.shield_md5 import md5sum +from xhs.shield_rc4 import RC4 + + +def hex_string(num, step=2): + """ + step 控制二进制的长度 避免有些少零了 + """ + tmp_string = hex(num)[2:] + + if len(tmp_string) < step: + tmp_string = '0' + tmp_string + + return tmp_string + + +def expand_keys(keys, m): + md5_str1_list = [hex_string(int(keys[i] + keys[i + 1], 16) ^ m) for i in range(0, len(keys), 2)] + md5_str1 = "".join(md5_str1_list) + return md5_str1 + + +def char2hex(char): + return hex_string(ord(char)) + + +def get_md5(keys, params): + """ + 标准 hmac 操作 + :param keys: + :param params: + :return: + """ + + # 拓展key1 0x36 + md5_str1 = expand_keys(keys, 54) + # 拓展key2 0x5c + md5_str2 = expand_keys(keys, 92) + + data = bytes.fromhex(md5_str1) + params.encode("utf-8") + md1 = md5sum(data) + + data1 = bytes.fromhex(md5_str2 + md1) + md2 = md5sum(data1) # 获得最终结果 + + return md2 + + +def get_rc4(params): + """ + 标准rc4运算 + :param params: + :return: + """ + key = "std::abort();" + result = RC4(key.encode("utf-8"), bytes.fromhex(params)) + return result.hex() + + +def get_shield(keys, params, deviceId): + """ + shield生成核心部分 + 固定版本 和 app_id + :param keys: + :param params: + :param deviceId: + :return: + """ + version = "6970181" + app_id = "ecfaaf01" + p7 = "".join([char2hex(m) for m in version]) + p8 = "".join([char2hex(m) for m in deviceId]) + p9 = get_md5(keys, params) # 魔改md5 16位md5 + rc4_plaintext = f"00000001{app_id}00000002000000{hex_string(len(version))}000000{hex_string(len(deviceId))}000000{hex_string(len(p9) // 2)}{p7}{p8}{p9}" + result = get_rc4(rc4_plaintext) + _tmp = len(version) + len(deviceId) + len(p9) // 2 + 24 + tmp = f"0000000100000001000000{hex_string(_tmp)}000000{hex_string(_tmp)}" # 固定 0x53是上述 几个固定的值算出来的 + _shield = "XY" + base64.b64encode(bytes.fromhex(tmp + result)).decode() + return _shield + + +def shield(node_id, xy_common_params, xy_platform_info): + """ + + """ + # 设备一 + deviceId = "2fe75062-a528-3340-bed3-220a67f7f240" + keys = "aa82da57410dddd5b2860e534f7c0602f589c20ec8e8830baa239360c89cce62bdc304d8a1aa988d620917dbefc2a1154692fad24294f4419ea19c7dc069897b" + + api = "/api/sns/v1/note/feed" + param = f"note_id={node_id}&page=1&num=5&fetch_mode=1&source=&ads_track_id=" + plaintext = api + param + xy_common_params + xy_platform_info + return get_shield(keys, plaintext, deviceId) + + +def shield_run(url, keys, xy_common_params, deviceId, api="/api/sns/v1/note/feed"): + """ + 对外暴露接口 + :param url: 请求的链接 + :param keys: 参与运算的 hmac_main + :param xy_common_params: 对应header中字段 + :param deviceId: 对应设备号 + :param api: 对应接口 目前默认为feed流接口 + :return: + """ + param = url[url.index("?") + 1:] + xy_platform_info = f"platform=android&build=6970181&deviceId={deviceId}" + plaintext = api + param + xy_common_params + xy_platform_info + # print(plaintext) + return get_shield(keys, plaintext, deviceId) + + +if __name__ == '__main__': + # 先获得 main_hmac 处理后的key + # XYAAAAAQAAAAEAAABTAAAAUzUWEe0xG1IbD9/c+qCLOlKGmTtFa+lG43AHe+FXTKxDxI2yn7IxH534qbVaz8N7icV+2KNmRAwcQDSAZrqn3SpjhOCLuaGTuDRgbpA0sNhU/xUP 结果 + # XYAAAAAQAAAAEAAABTAAAAUzUWEe0xG1IbD9/c+qCLOlKGmTtFa+lG43AHe+FXTKxDxI2yn7IxH534qbVaz8N7icV+2KNmRAwcQDSAZrqn3SpjhOCLuaGTuDRgbpA0sNhU/xUP + # keys = "aa82da57410dddd5b2860e534f7c0602f589c20ec8e8830baa239360c89cce62bdc304d8a1aa988d620917dbefc2a1154692fad24294f4419ea19c7dc069897b" + # get_shield(keys) + url = "https://edith.xiaohongshu.com/api/sns/v1/note/feed?note_id=66ceeabe000000001d03b546&page=1&num=5&fetch_mode=1&source=&ads_track_id=" + + hmac = "NqLx0YFKNb4KraYq524SgzVpepYQ0SwhZLRs7eyxe6A26c/b1b+d6OU2LfAPwh8zpt3fkR/jsR5yzVzIqXe66EWhGJ8iWV36KKSIz0mVt436sTqt3eUYUZwb5TzpSYDa" + deviceId = "119214fc-0fe5-3ae8-91dd-baa821c11324" + xy_platform_info = "platform=android&build=6970181&deviceId=119214fc-0fe5-3ae8-91dd-baa821c11324" + xy_common_params = "fid=1721639154103483c0daaace2ca9266cba37ac9fe114&device_fingerprint=202407221758192809006e7e334e46628620f6768bcf3b0153b1977b9f6cd6&device_fingerprint1=202407221758192809006e7e334e46628620f6768bcf3b0153b1977b9f6cd6&cpu_name=Qualcomm+Technologies%2C+Inc+SM8150&device_model=phone&launch_id=1727578036&tz=Asia%2FShanghai&channel=CPA-3DSP-N3-ZSKJ&versionName=6.97.0.1&overseas_channel=0&deviceId=119214fc-0fe5-3ae8-91dd-baa821c11324&platform=android&sid=session.1721639201142076381131&identifier_flag=4&t=1727590998&project_id=ECFAAF&build=6970181&lang=zh-Hans&app_id=ECFAAF01&uis=dark&teenager=0" + + keys = get_key(deviceId, hmac) + + result = sheild_run(url, keys, xy_common_params, deviceId) + print(result) diff --git a/xhs/shield_aes.py b/xhs/shield_aes.py new file mode 100644 index 0000000..f3ddd27 --- /dev/null +++ b/xhs/shield_aes.py @@ -0,0 +1,200 @@ +from functools import reduce +import base64 +from xhs.shield_const import * + +""" + 小红书 hmac_main 的aes处理 + 自定义aes + 逆向版本 6.97.0.1 +""" + +class AES: + def __init__(self, key: bytes): + self.aes_type = len(key) * 8 + self._key_r = self._generate_key(key) + + def _sort_key(self, w_list): + """ + 重新编排key + """ + + def change_char(w): + w_4 = self._split_int(w) # 拆分成 4x8bit + s_w_4 = [SBox[w_4[0]], SBox[w_4[1]], SBox[w_4[2]], SBox[w_4[3]]] + s_s_w_4 = [dword_7F550[s_w_4[0]], dword_7F950[s_w_4[1]], dword_7FD50[s_w_4[2]], dword_80150[s_w_4[3]]] + key4 = reduce(lambda x, y: x ^ y, s_s_w_4) + return key4 + + # 交换位置 + for i in range(len(w_list) // 2): + tmp = w_list[i] + if i > 3: # 需要交换位置 + tmp = change_char(tmp) + w_list[len(w_list) - i - 1] = change_char(w_list[len(w_list) - i -1]) + + w_list[i] = w_list[len(w_list) - i -1] + w_list[len(w_list) - i -1] = tmp + # print(i, len(w_list) - i -1) + return w_list + + @staticmethod + def hex_string(num, step=2): + """ + step 控制二进制的长度 避免有些少零了 + """ + tmp_string = hex(num)[2:] + + if len(tmp_string) < step: + tmp_string = '0' * (step - len(tmp_string)) + tmp_string + + return tmp_string + + def encrypt(self, plaintext:bytes): + """ + 加密 解密 + :param plaintext: + :return: + """ + state = [[plaintext[i + j] for j in range(16)] for i in range(0, len(plaintext), 16)] # 先分组 + result = [] + # 进行加密流程 + for p in range(len(state)): + _plaintext_block = [self._joint_int(state[p][i:i+4]) for i in range(0, 16, 4)] + state[p] = _plaintext_block.copy() # 合并为每四个字节的数组 + _encrypt_block = self.encrypt_block(_plaintext_block) # 处理第一块 + if p > 0: + extra_block = [self.hex_string(_encrypt_block[i] ^ state[p - 1][i], 8) for i in range(4)] # 异或处理 + result.append("".join(extra_block)) + + return "".join(result[:4]) + + def encrypt_block(self, plaintext_block): + """ + 按照块加密 + """ + keys = self._key_r + # 第一轮由于明文和那个key是反的 单独计算 + plaintext_block[0] = plaintext_block[0] ^ keys[3] + plaintext_block[1] = plaintext_block[1] ^ keys[2] + plaintext_block[2] = plaintext_block[2] ^ keys[1] + plaintext_block[3] = plaintext_block[3] ^ keys[0] + + for i in range(0, 9): + num = (i + 1) * 4 + a1 = self._split_int(plaintext_block[0]) # 这块是按顺序的 + a2 = self._split_int(plaintext_block[1]) + a3 = self._split_int(plaintext_block[2]) + a4 = self._split_int(plaintext_block[3]) + + # 查表法进行替换 + a1_1_box = [dword_7F550[a1[0]], dword_7F950[a1[1]], dword_7FD50[a1[2]], dword_80150[a1[3]]] + a2_1_box = [dword_7F550[a2[0]], dword_7F950[a2[1]], dword_7FD50[a2[2]], dword_80150[a2[3]]] + a3_1_box = [dword_7F550[a3[0]], dword_7F950[a3[1]], dword_7FD50[a3[2]], dword_80150[a3[3]]] + a4_1_box = [dword_7F550[a4[0]], dword_7F950[a4[1]], dword_7FD50[a4[2]], dword_80150[a4[3]]] + + if i == 0: + # 矩阵运算 这块不太一样 + d1 = [a4_1_box[0], a3_1_box[1], a2_1_box[2], a1_1_box[3]] + d2 = [a3_1_box[0], a2_1_box[1], a1_1_box[2], a4_1_box[3]] + d3 = [a2_1_box[0], a1_1_box[1], a4_1_box[2], a3_1_box[3]] + d4 = [a1_1_box[0], a4_1_box[1], a3_1_box[2], a2_1_box[3]] + else: + # 矩阵运算 + d1 = [a1_1_box[0], a2_1_box[1], a3_1_box[2], a4_1_box[3]] + d2 = [a2_1_box[0], a3_1_box[1], a4_1_box[2], a1_1_box[3]] + d3 = [a3_1_box[0], a4_1_box[1], a1_1_box[2], a2_1_box[3]] + d4 = [a4_1_box[0], a1_1_box[1], a2_1_box[2], a3_1_box[3]] + + + # 拼接 + plaintext_block[0] = reduce(lambda x, y: x ^ y, d1) ^ keys[num] + plaintext_block[1] = reduce(lambda x, y: x ^ y, d2) ^ keys[num + 1] + plaintext_block[2] = reduce(lambda x, y: x ^ y, d3) ^ keys[num + 2] + plaintext_block[3] = reduce(lambda x, y: x ^ y, d4) ^ keys[num + 3] + + + + # 后四位 + a1 = self._split_int(plaintext_block[0]) # 这块是按顺序的 + a2 = self._split_int(plaintext_block[1]) + a3 = self._split_int(plaintext_block[2]) + a4 = self._split_int(plaintext_block[3]) + + a1_sum_box = [SBoxIV[a4[0]], SBoxIV[a1[1]], SBoxIV[a2[2]], SBoxIV[a3[3]]] # 合并后 + a2_sum_box = [SBoxIV[a3[0]], SBoxIV[a4[1]], SBoxIV[a1[2]], SBoxIV[a2[3]]] # 合并后 + a3_sum_box = [SBoxIV[a2[0]], SBoxIV[a3[1]], SBoxIV[a4[2]], SBoxIV[a1[3]]] # 合并后 + a4_sum_box = [SBoxIV[a1[0]], SBoxIV[a2[1]], SBoxIV[a3[2]], SBoxIV[a4[3]]] # 合并后 + + + plaintext_block[0] = self._joint_int(a1_sum_box) ^ keys[-1] + plaintext_block[1] = self._joint_int(a2_sum_box) ^ keys[-2] + plaintext_block[2] = self._joint_int(a3_sum_box) ^ keys[-3] + plaintext_block[3] = self._joint_int(a4_sum_box) ^ keys[-4] + + return plaintext_block + + + def _generate_key(self, key: bytes) -> list: + """密钥扩展""" + Rcon = [0x12310000, 0x2000100, 0x4020000, 0x8020200, 0x10102000, 0x30020400, 0x40002000, 0x80002000, 0x1B002000, 0x36200200] # 轮常数 + Nr, Nk = 10 + (self.aes_type - 128) // 32, self.aes_type // 32 # Nr:轮数,Nk:密钥长度 + w = [0 for _ in range(4 * (Nr + 1))] # 轮密钥 + + p = [4052295985, 4278194467, 4043314006, 4045621392] # 额外处理 + for i in range(Nk): # 初始化是对的 + w[i] = int.from_bytes(key[4 * i:4 * i + 4], 'big') ^ p[i] + # print(f"w{i} => {hex(w[i])}") + + for i in range(Nk, 4 * (Nr + 1)): + temp = w[i - 1] + # print("******************************") + # print(f"开始", hex(temp), i % Nk) + if i % Nk == 0: + temp = self._split_int(temp) # 拆分成 4x8bit + temp = [SBox[temp[1]], SBox[temp[2]], SBox[temp[3]], SBox[temp[0]]] # sbox 的轮数发生变化了 + temp = self._joint_int(temp) ^ Rcon[i // Nk - 1] # 合并回 32bit + + elif Nk > 6 and i % Nk == 4: + temp = self._split_int(temp) # 拆分成 4x8bit + temp = [SBox[temp[0]], SBox[temp[1]], + SBox[temp[2]], SBox[temp[3]]] + temp = self._joint_int(temp) # 合并回 32bit + + w[i] = w[i - Nk] ^ temp + + w = self._sort_key(w) # 替换处理key + return w + + @staticmethod + def _split_int(n: int) -> list: + """拆分 32bit 成 4x8bit""" + return [(n >> 24) & 0xFF, (n >> 16) & 0xFF, (n >> 8) & 0xFF, n & 0xFF] + + @staticmethod + def _joint_int(b: list) -> int: + """合并 4x8bit 成 32bit""" + return (b[0] << 24) | (b[1] << 16) | (b[2] << 8) | b[3] + + + +def get_key(deviceId, hmac_main): + """ + 外部获取 接口 + :param deviceId: 设备id + :param hmac_main: 存在于机器的 s.xml + :return: + """ + key = deviceId[:16] + plaintext = base64.b64decode(hmac_main.encode("utf-8")) + A = AES(key.encode("utf-8")) + result = A.encrypt(plaintext) + return result + + +if __name__ == '__main__': + key = "2fe75062-a528-33" + m = AES(key.encode("utf-8")) + hmac_main = "cDdMxUWZy3e2szBCUB04rZMxTdf6tVKcpCIRrDQGa/NS8Agki6U5MGN6c6QCT3t6amTAYBbcDwFlPndCV3AfaerPd36GS9sdmTeKzBU45YsIBsGAdBXyy2GnkRlDaVCO" + plaintext = base64.b64decode(hmac_main.encode("utf-8")) + pp = m.encrypt(plaintext) + print(get_key(key, hmac_main)) \ No newline at end of file diff --git a/xhs/shield_const.py b/xhs/shield_const.py new file mode 100644 index 0000000..7f50ef5 --- /dev/null +++ b/xhs/shield_const.py @@ -0,0 +1,274 @@ + +# AES 初始化化参数 +# s盒和逆s盒是原始的 其他都是自定的数组 +SBox = [0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, + 0xFE, 0xD7, 0xAB, 0x76, 0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, + 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0, 0xB7, 0xFD, 0x93, 0x26, + 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15, + 0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, + 0xEB, 0x27, 0xB2, 0x75, 0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, + 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84, 0x53, 0xD1, 0x00, 0xED, + 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF, + 0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, + 0x50, 0x3C, 0x9F, 0xA8, 0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, + 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2, 0xCD, 0x0C, 0x13, 0xEC, + 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73, + 0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, + 0xDE, 0x5E, 0x0B, 0xDB, 0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, + 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79, 0xE7, 0xC8, 0x37, 0x6D, + 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08, + 0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, + 0x4B, 0xBD, 0x8B, 0x8A, 0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, + 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E, 0xE1, 0xF8, 0x98, 0x11, + 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF, + 0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, + 0xB0, 0x54, 0xBB, 0x16] + +# aes 轮密钥 +Rcon = [ + 0x12310000, 0x2000100, 0x4020000, 0x8020200, 0x10102000, 0x30020400, + 0x40002000, 0x80002000, 0x1B002000, 0x36200200 +] + +SBoxIV = [0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, + 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, + 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, + 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, + 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, + 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, + 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, + 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, + 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, + 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, + 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, + 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, + 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, + 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, + 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, + 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, + 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, + 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, + 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, + 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, + 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, + 0x55, 0x21, 0x0c, 0x7d] + +dword_7F550 = [ + 0x51F4A750, 0x7E416553, 0x1A17A4C3, 0x3A275E96, 0x3BAB6BCB + ,0x1F9D45F1, 0xACFA58AB, 0x4BE30393, 0x2030FA55, 0xAD766DF6 + ,0x88CC7691, 0xF5024C25, 0x4FE5D7FC, 0xC52ACBD7, 0x26354480 + ,0xB562A38F, 0xDEB15A49, 0x25BA1B67, 0x45EA0E98, 0x5DFEC0E1 + ,0xC32F7502, 0x814CF012, 0x8D4697A3, 0x6BD3F9C6, 0x38F5FE7 + ,0x15929C95, 0xBF6D7AEB, 0x955259DA, 0xD4BE832D, 0x587421D3 + ,0x49E06929, 0x8EC9C844, 0x75C2896A, 0xF48E7978, 0x99583E6B + ,0x27B971DD, 0xBEE14FB6, 0xF088AD17, 0xC920AC66, 0x7DCE3AB4 + ,0x63DF4A18, 0xE51A3182, 0x97513360, 0x62537F45, 0xB16477E0 + ,0xBB6BAE84, 0xFE81A01C, 0xF9082B94, 0x70486858, 0x8F45FD19 + ,0x94DE6C87, 0x527BF8B7, 0xAB73D323, 0x724B02E2, 0xE31F8F57 + ,0x6655AB2A, 0xB2EB2807, 0x2FB5C203, 0x86C57B9A, 0xD33708A5 + ,0x302887F2, 0x23BFA5B2, 0x2036ABA, 0xED16825C, 0x8ACF1C2B + ,0xA779B492, 0xF307F2F0, 0x4E69E2A1, 0x65DAF4CD, 0x605BED5 + ,0xD134621F, 0xC4A6FE8A, 0x342E539D, 0xA2F355A0, 0x58AE132 + ,0xA4F6EB75, 0xB83EC39, 0x4060EFAA, 0x5E719F06, 0xBD6E1051 + ,0x3E218AF9, 0x96DD063D, 0xDD3E05AE, 0x4DE6BD46, 0x91548DB5 + ,0x71C45D05, 0x406D46F, 0x605015FF, 0x1998FB24, 0xD6BDE997 + ,0x894043CC, 0x67D99E77, 0xB0E842BD, 0x7898B88, 0xE7195B38 + ,0x79C8EEDB, 0xA17C0A47, 0x7C420FE9, 0xF8841EC9, 0 + ,0x9808683, 0x322BED48, 0x1E1170AC, 0x6C5A724E, 0xFD0EFFFB + ,0xF853856, 0x3DAED51E, 0x362D3927, 0xA0FD964, 0x685CA621 + ,0x9B5B54D1, 0x24362E3A, 0xC0A67B1, 0x9357E70F, 0xB4EE96D2 + ,0x1B9B919E, 0x80C0C54F, 0x61DC20A2, 0x5A774B69, 0x1C121A16 + ,0xE293BA0A, 0xC0A02AE5, 0x3C22E043, 0x121B171D, 0xE090D0B + ,0xF28BC7AD, 0x2DB6A8B9, 0x141EA9C8, 0x57F11985, 0xAF75074C + ,0xEE99DDBB, 0xA37F60FD, 0xF701269F, 0x5C72F5BC, 0x44663BC5 + ,0x5BFB7E34, 0x8B432976, 0xCB23C6DC, 0xB6EDFC68, 0xB8E4F163 + ,0xD731DCCA, 0x42638510, 0x13972240, 0x84C61120, 0x854A247D + ,0xD2BB3DF8, 0xAEF93211, 0xC729A16D, 0x1D9E2F4B, 0xDCB230F3 + ,0xD8652EC, 0x77C1E3D0, 0x2BB3166C, 0xA970B999, 0x119448FA + ,0x47E96422, 0xA8FC8CC4, 0xA0F03F1A, 0x567D2CD8, 0x223390EF + ,0x87494EC7, 0xD938D1C1, 0x8CCAA2FE, 0x98D40B36, 0xA6F581CF + ,0xA57ADE28, 0xDAB78E26, 0x3FADBFA4, 0x2C3A9DE4, 0x5078920D + ,0x6A5FCC9B, 0x547E4662, 0xF68D13C2, 0x90D8B8E8, 0x2E39F75E + ,0x82C3AFF5, 0x9F5D80BE, 0x69D0937C, 0x6FD52DA9, 0xCF2512B3 + ,0xC8AC993B, 0x10187DA7, 0xE89C636E, 0xDB3BBB7B, 0xCD267809 + ,0x6E5918F4, 0xEC9AB701, 0x834F9AA8, 0xE6956E65, 0xAAFFE67E + ,0x21BCCF08, 0xEF15E8E6, 0xBAE79BD9, 0x4A6F36CE, 0xEA9F09D4 + ,0x29B07CD6, 0x31A4B2AF, 0x2A3F2331, 0xC6A59430, 0x35A266C0 + ,0x744EBC37, 0xFC82CAA6, 0xE090D0B0, 0x33A7D815, 0xF104984A + ,0x41ECDAF7, 0x7FCD500E, 0x1791F62F, 0x764DD68D, 0x43EFB04D + ,0xCCAA4D54, 0xE49604DF, 0x9ED1B5E3, 0x4C6A881B, 0xC12C1FB8 + ,0x4665517F, 0x9D5EEA04, 0x18C355D, 0xFA877473, 0xFB0B412E + ,0xB3671D5A, 0x92DBD252, 0xE9105633, 0x6DD64713, 0x9AD7618C + , 0x37A10C7A, 0x59F8148E, 0xEB133C89, 0xCEA927EE, 0xB761C935 + ,0xE11CE5ED, 0x7A47B13C, 0x9CD2DF59, 0x55F2733F, 0x1814CE79 + ,0x73C737BF, 0x53F7CDEA, 0x5FFDAA5B, 0xDF3D6F14, 0x7844DB86 + ,0xCAAFF381, 0xB968C43E, 0x3824342C, 0xC2A3405F, 0x161DC372 + ,0xBCE2250C, 0x283C498B, 0xFF0D9541, 0x39A80171, 0x80CB3DE + ,0xD8B4E49C, 0x6456C190, 0x7BCB8461, 0xD532B670, 0x486C5C74 + , 0xD0B85742 +] + +dword_7F950 = [ + 0x5051F4A7, 0x537E4165, 0xC31A17A4, 0x963A275E, 0xCB3BAB6B + ,0xF11F9D45, 0xABACFA58, 0x934BE303, 0x552030FA, 0xF6AD766D + ,0x9188CC76, 0x25F5024C, 0xFC4FE5D7, 0xD7C52ACB, 0x80263544 + ,0x8FB562A3, 0x49DEB15A, 0x6725BA1B, 0x9845EA0E, 0xE15DFEC0 + ,0x2C32F75, 0x12814CF0, 0xA38D4697, 0xC66BD3F9, 0xE7038F5F + ,0x9515929C, 0xEBBF6D7A, 0xDA955259, 0x2DD4BE83, 0xD3587421 + ,0x2949E069, 0x448EC9C8, 0x6A75C289, 0x78F48E79, 0x6B99583E + ,0xDD27B971, 0xB6BEE14F, 0x17F088AD, 0x66C920AC, 0xB47DCE3A + ,0x1863DF4A, 0x82E51A31, 0x60975133, 0x4562537F, 0xE0B16477 + ,0x84BB6BAE, 0x1CFE81A0, 0x94F9082B, 0x58704868, 0x198F45FD + ,0x8794DE6C, 0xB7527BF8, 0x23AB73D3, 0xE2724B02, 0x57E31F8F + ,0x2A6655AB, 0x7B2EB28, 0x32FB5C2, 0x9A86C57B, 0xA5D33708 + ,0xF2302887, 0xB223BFA5, 0xBA02036A, 0x5CED1682, 0x2B8ACF1C + ,0x92A779B4, 0xF0F307F2, 0xA14E69E2, 0xCD65DAF4, 0xD50605BE + ,0x1FD13462, 0x8AC4A6FE, 0x9D342E53, 0xA0A2F355, 0x32058AE1 + ,0x75A4F6EB, 0x390B83EC, 0xAA4060EF, 0x65E719F, 0x51BD6E10 + ,0xF93E218A, 0x3D96DD06, 0xAEDD3E05, 0x464DE6BD, 0xB591548D + , 0x571C45D, 0x6F0406D4, 0xFF605015, 0x241998FB, 0x97D6BDE9 + ,0xCC894043, 0x7767D99E, 0xBDB0E842, 0x8807898B, 0x38E7195B + ,0xDB79C8EE, 0x47A17C0A, 0xE97C420F, 0xC9F8841E, 0 + ,0x83098086, 0x48322BED, 0xAC1E1170, 0x4E6C5A72, 0xFBFD0EFF + ,0x560F8538, 0x1E3DAED5, 0x27362D39, 0x640A0FD9, 0x21685CA6 + ,0xD19B5B54, 0x3A24362E, 0xB10C0A67, 0xF9357E7, 0xD2B4EE96 + ,0x9E1B9B91, 0x4F80C0C5, 0xA261DC20, 0x695A774B, 0x161C121A + ,0xAE293BA, 0xE5C0A02A, 0x433C22E0, 0x1D121B17, 0xB0E090D + ,0xADF28BC7, 0xB92DB6A8, 0xC8141EA9, 0x8557F119, 0x4CAF7507 + ,0xBBEE99DD, 0xFDA37F60, 0x9FF70126, 0xBC5C72F5, 0xC544663B + ,0x345BFB7E, 0x768B4329, 0xDCCB23C6, 0x68B6EDFC, 0x63B8E4F1 + , 0xCAD731DC, 0x10426385, 0x40139722, 0x2084C611, 0x7D854A24 + ,0xF8D2BB3D, 0x11AEF932, 0x6DC729A1, 0x4B1D9E2F, 0xF3DCB230 + ,0xEC0D8652, 0xD077C1E3, 0x6C2BB316, 0x99A970B9, 0xFA119448 + ,0x2247E964, 0xC4A8FC8C, 0x1AA0F03F, 0xD8567D2C, 0xEF223390 + ,0xC787494E, 0xC1D938D1, 0xFE8CCAA2, 0x3698D40B, 0xCFA6F581 + ,0x28A57ADE, 0x26DAB78E, 0xA43FADBF, 0xE42C3A9D, 0xD507892 + ,0x9B6A5FCC, 0x62547E46, 0xC2F68D13, 0xE890D8B8, 0x5E2E39F7 + ,0xF582C3AF, 0xBE9F5D80, 0x7C69D093, 0xA96FD52D, 0xB3CF2512 + ,0x3BC8AC99, 0xA710187D, 0x6EE89C63, 0x7BDB3BBB, 0x9CD2678 + ,0xF46E5918, 0x1EC9AB7, 0xA8834F9A, 0x65E6956E, 0x7EAAFFE6 + ,0x821BCCF, 0xE6EF15E8, 0xD9BAE79B, 0xCE4A6F36, 0xD4EA9F09 + ,0xD629B07C, 0xAF31A4B2, 0x312A3F23, 0x30C6A594, 0xC035A266 + ,0x37744EBC, 0xA6FC82CA, 0xB0E090D0, 0x1533A7D8, 0x4AF10498 + ,0xF741ECDA, 0xE7FCD50, 0x2F1791F6, 0x8D764DD6, 0x4D43EFB0 + ,0x54CCAA4D, 0xDFE49604, 0xE39ED1B5, 0x1B4C6A88, 0xB8C12C1F + ,0x7F466551, 0x49D5EEA, 0x5D018C35, 0x73FA8774, 0x2EFB0B41 + ,0x5AB3671D, 0x5292DBD2, 0x33E91056, 0x136DD647, 0x8C9AD761 + ,0x7A37A10C, 0x8E59F814, 0x89EB133C, 0xEECEA927, 0x35B761C9 + ,0xEDE11CE5, 0x3C7A47B1, 0x599CD2DF, 0x3F55F273, 0x791814CE + ,0xBF73C737, 0xEA53F7CD, 0x5B5FFDAA, 0x14DF3D6F, 0x867844DB + ,0x81CAAFF3, 0x3EB968C4, 0x2C382434, 0x5FC2A340, 0x72161DC3 + ,0xCBCE225, 0x8B283C49, 0x41FF0D95, 0x7139A801, 0xDE080CB3 + ,0x9CD8B4E4, 0x906456C1, 0x617BCB84, 0x70D532B6, 0x74486C5C + ,0x42D0B857 +] + +# ??不知道 少一个 +dword_7FD50 = [ + 0xA75051F4, 0x65537E41, 0xA4C31A17, 0x5E963A27, 0x6BCB3BAB + ,0x45F11F9D, 0x58ABACFA, 0x3934BE3, 0xFA552030, 0x6DF6AD76 + ,0x769188CC, 0x4C25F502, 0xD7FC4FE5, 0xCBD7C52A, 0x44802635 + ,0xA38FB562, 0x5A49DEB1, 0x1B6725BA, 0xE9845EA, 0xC0E15DFE + ,0x7502C32F, 0xF012814C, 0x97A38D46, 0xF9C66BD3, 0x5FE7038F + , 0x9C951592, 0x7AEBBF6D, 0x59DA9552, 0x832DD4BE, 0x21D35874 + ,0x692949E0, 0xC8448EC9, 0x896A75C2, 0x7978F48E, 0x3E6B9958 + ,0x71DD27B9, 0x4FB6BEE1, 0xAD17F088, 0xAC66C920, 0x3AB47DCE + ,0x4A1863DF, 0x3182E51A, 0x33609751, 0x7F456253, 0x77E0B164 + ,0xAE84BB6B, 0xA01CFE81, 0x2B94F908, 0x68587048, 0xFD198F45 + ,0x6C8794DE, 0xF8B7527B, 0xD323AB73, 0x2E2724B, 0x8F57E31F + ,0xAB2A6655, 0x2807B2EB, 0xC2032FB5, 0x7B9A86C5, 0x8A5D337 + ,0x87F23028, 0xA5B223BF, 0x6ABA0203, 0x825CED16, 0x1C2B8ACF + ,0xB492A779, 0xF2F0F307, 0xE2A14E69, 0xF4CD65DA, 0xBED50605 + ,0x621FD134, 0xFE8AC4A6, 0x539D342E, 0x55A0A2F3, 0xE132058A + ,0xEB75A4F6, 0xEC390B83, 0xEFAA4060, 0x9F065E71, 0x1051BD6E + ,0x8AF93E21, 0x63D96DD, 0x5AEDD3E, 0xBD464DE6, 0x8DB59154 + ,0x5D0571C4, 0xD46F0406, 0x15FF6050, 0xFB241998, 0xE997D6BD + ,0x43CC8940, 0x9E7767D9, 0x42BDB0E8, 0x8B880789, 0x5B38E719 + ,0xEEDB79C8, 0xA47A17C, 0xFE97C42, 0x1EC9F884, 0, 0x86830980 + ,0xED48322B, 0x70AC1E11, 0x724E6C5A, 0xFFFBFD0E, 0x38560F85 + ,0xD51E3DAE, 0x3927362D, 0xD9640A0F, 0xA621685C, 0x54D19B5B + ,0x2E3A2436, 0x67B10C0A, 0xE70F9357, 0x96D2B4EE, 0x919E1B9B + ,0xC54F80C0, 0x20A261DC, 0x4B695A77, 0x1A161C12, 0xBA0AE293 + ,0x2AE5C0A0, 0xE0433C22, 0x171D121B, 0xD0B0E09, 0xC7ADF28B + ,0xA8B92DB6, 0xA9C8141E, 0x198557F1, 0x74CAF75, 0xDDBBEE99 + ,0x60FDA37F, 0x269FF701, 0xF5BC5C72, 0x3BC54466, 0x7E345BFB + ,0x29768B43, 0xC6DCCB23, 0xFC68B6ED, 0xF163B8E4, 0xDCCAD731 + ,0x85104263, 0x22401397, 0x112084C6, 0x247D854A, 0x3DF8D2BB + ,0x3211AEF9, 0xA16DC729, 0x2F4B1D9E, 0x30F3DCB2, 0x52EC0D86 + ,0xE3D077C1, 0x166C2BB3, 0xB999A970, 0x48FA1194, 0x642247E9 + ,0x8CC4A8FC, 0x3F1AA0F0, 0x2CD8567D, 0x90EF2233, 0x4EC78749 + ,0xD1C1D938, 0xA2FE8CCA, 0xB3698D4, 0x81CFA6F5, 0xDE28A57A + ,0x8E26DAB7, 0xBFA43FAD, 0x9DE42C3A, 0x920D5078, 0xCC9B6A5F + ,0x4662547E, 0x13C2F68D, 0xB8E890D8, 0xF75E2E39, 0xAFF582C3 + ,0x80BE9F5D, 0x937C69D0, 0x2DA96FD5, 0x12B3CF25, 0x993BC8AC + ,0x7DA71018, 0x636EE89C, 0xBB7BDB3B, 0x7809CD26, 0x18F46E59 + ,0xB701EC9A, 0x9AA8834F, 0x6E65E695, 0xE67EAAFF, 0xCF0821BC + ,0xE8E6EF15, 0x9BD9BAE7, 0x36CE4A6F, 0x9D4EA9F, 0x7CD629B0 + ,0xB2AF31A4, 0x23312A3F, 0x9430C6A5, 0x66C035A2, 0xBC37744E + ,0xCAA6FC82, 0xD0B0E090, 0xD81533A7, 0x984AF104, 0xDAF741EC + ,0x500E7FCD, 0xF62F1791, 0xD68D764D, 0xB04D43EF, 0x4D54CCAA + ,0x4DFE496, 0xB5E39ED1, 0x881B4C6A, 0x1FB8C12C, 0x517F4665 + ,0xEA049D5E, 0x355D018C, 0x7473FA87, 0x412EFB0B, 0x1D5AB367 + ,0xD25292DB, 0x5633E910, 0x47136DD6, 0x618C9AD7, 0xC7A37A1 + ,0x148E59F8, 0x3C89EB13, 0x27EECEA9, 0xC935B761, 0xE5EDE11C + ,0xB13C7A47, 0xDF599CD2, 0x733F55F2, 0xCE791814, 0x37BF73C7 + ,0xCDEA53F7, 0xAA5B5FFD, 0x6F14DF3D, 0xDB867844, 0xF381CAAF + ,0xC43EB968, 0x342C3824, 0x405FC2A3, 0xC372161D, 0x250CBCE2 + ,0x498B283C, 0x9541FF0D, 0x17139A8, 0xB3DE080C, 0xE49CD8B4 + ,0xC1906456, 0x84617BCB, 0xB670D532, 0x5C74486C, 0x5742D0B8 +] + +dword_80150 = [ + 0xF4A75051, 0x4165537E, 0x17A4C31A, 0x275E963A, 0xAB6BCB3B + ,0x9D45F11F, 0xFA58ABAC, 0xE303934B, 0x30FA5520, 0x766DF6AD + ,0xCC769188, 0x24C25F5, 0xE5D7FC4F, 0x2ACBD7C5, 0x35448026 + ,0x62A38FB5, 0xB15A49DE, 0xBA1B6725, 0xEA0E9845, 0xFEC0E15D + ,0x2F7502C3, 0x4CF01281, 0x4697A38D, 0xD3F9C66B, 0x8F5FE703 + ,0x929C9515, 0x6D7AEBBF, 0x5259DA95, 0xBE832DD4, 0x7421D358 + ,0xE0692949, 0xC9C8448E, 0xC2896A75, 0x8E7978F4, 0x583E6B99 + ,0xB971DD27, 0xE14FB6BE, 0x88AD17F0, 0x20AC66C9, 0xCE3AB47D + ,0xDF4A1863, 0x1A3182E5, 0x51336097, 0x537F4562, 0x6477E0B1 + ,0x6BAE84BB, 0x81A01CFE, 0x82B94F9, 0x48685870, 0x45FD198F + ,0xDE6C8794, 0x7BF8B752, 0x73D323AB, 0x4B02E272, 0x1F8F57E3 + ,0x55AB2A66, 0xEB2807B2, 0xB5C2032F, 0xC57B9A86, 0x3708A5D3 + ,0x2887F230, 0xBFA5B223, 0x36ABA02, 0x16825CED, 0xCF1C2B8A + ,0x79B492A7, 0x7F2F0F3, 0x69E2A14E, 0xDAF4CD65, 0x5BED506 + ,0x34621FD1, 0xA6FE8AC4, 0x2E539D34, 0xF355A0A2, 0x8AE13205 + ,0xF6EB75A4, 0x83EC390B, 0x60EFAA40, 0x719F065E, 0x6E1051BD + ,0x218AF93E, 0xDD063D96, 0x3E05AEDD, 0xE6BD464D, 0x548DB591 + ,0xC45D0571, 0x6D46F04, 0x5015FF60, 0x98FB2419, 0xBDE997D6 + ,0x4043CC89, 0xD99E7767, 0xE842BDB0, 0x898B8807, 0x195B38E7 + ,0xC8EEDB79, 0x7C0A47A1, 0x420FE97C, 0x841EC9F8, 0 + ,0x80868309, 0x2BED4832, 0x1170AC1E, 0x5A724E6C, 0xEFFFBFD + ,0x8538560F, 0xAED51E3D, 0x2D392736, 0xFD9640A, 0x5CA62168 + ,0x5B54D19B, 0x362E3A24, 0xA67B10C, 0x57E70F93, 0xEE96D2B4 + ,0x9B919E1B, 0xC0C54F80, 0xDC20A261, 0x774B695A, 0x121A161C + ,0x93BA0AE2, 0xA02AE5C0, 0x22E0433C, 0x1B171D12, 0x90D0B0E + ,0x8BC7ADF2, 0xB6A8B92D, 0x1EA9C814, 0xF1198557, 0x75074CAF + ,0x99DDBBEE, 0x7F60FDA3, 0x1269FF7, 0x72F5BC5C, 0x663BC544 + ,0xFB7E345B, 0x4329768B, 0x23C6DCCB, 0xEDFC68B6, 0xE4F163B8 + ,0x31DCCAD7, 0x63851042, 0x97224013, 0xC6112084, 0x4A247D85 + ,0xBB3DF8D2, 0xF93211AE, 0x29A16DC7, 0x9E2F4B1D, 0xB230F3DC + ,0x8652EC0D, 0xC1E3D077, 0xB3166C2B, 0x70B999A9, 0x9448FA11 + ,0xE9642247, 0xFC8CC4A8, 0xF03F1AA0, 0x7D2CD856, 0x3390EF22 + ,0x494EC787, 0x38D1C1D9, 0xCAA2FE8C, 0xD40B3698, 0xF581CFA6 + ,0x7ADE28A5, 0xB78E26DA, 0xADBFA43F, 0x3A9DE42C, 0x78920D50 + ,0x5FCC9B6A, 0x7E466254, 0x8D13C2F6, 0xD8B8E890, 0x39F75E2E + ,0xC3AFF582, 0x5D80BE9F, 0xD0937C69, 0xD52DA96F, 0x2512B3CF + ,0xAC993BC8, 0x187DA710, 0x9C636EE8, 0x3BBB7BDB, 0x267809CD + ,0x5918F46E, 0x9AB701EC, 0x4F9AA883, 0x956E65E6, 0xFFE67EAA + ,0xBCCF0821, 0x15E8E6EF, 0xE79BD9BA, 0x6F36CE4A, 0x9F09D4EA + ,0xB07CD629, 0xA4B2AF31, 0x3F23312A, 0xA59430C6, 0xA266C035 + ,0x4EBC3774, 0x82CAA6FC, 0x90D0B0E0, 0xA7D81533, 0x4984AF1 + ,0xECDAF741, 0xCD500E7F, 0x91F62F17, 0x4DD68D76, 0xEFB04D43 + ,0xAA4D54CC, 0x9604DFE4, 0xD1B5E39E, 0x6A881B4C, 0x2C1FB8C1 + ,0x65517F46, 0x5EEA049D, 0x8C355D01, 0x877473FA, 0xB412EFB + ,0x671D5AB3, 0xDBD25292, 0x105633E9, 0xD647136D, 0xD7618C9A + ,0xA10C7A37, 0xF8148E59, 0x133C89EB, 0xA927EECE, 0x61C935B7 + ,0x1CE5EDE1, 0x47B13C7A, 0xD2DF599C, 0xF2733F55, 0x14CE7918 + ,0xC737BF73, 0xF7CDEA53, 0xFDAA5B5F, 0x3D6F14DF, 0x44DB8678 + ,0xAFF381CA, 0x68C43EB9, 0x24342C38, 0xA3405FC2, 0x1DC37216 + ,0xE2250CBC, 0x3C498B28, 0xD9541FF, 0xA8017139, 0xCB3DE08 + ,0xB4E49CD8, 0x56C19064, 0xCB84617B, 0x32B670D5, 0x6C5C7448 + ,0xB85742D0 +] diff --git a/xhs/shield_md5.py b/xhs/shield_md5.py new file mode 100644 index 0000000..d9acf2c --- /dev/null +++ b/xhs/shield_md5.py @@ -0,0 +1,250 @@ +import binascii +""" + 小红书 md5处理 魔改部分s盒、魔改运算过程中位移量、魔值顺序修改 + 最终shield 是进行hmac,而需要的hash值通过 shield_aes.py 生成 + 逆向版本 6.97.0.1 +""" + +# md5的s盒 其中有几位 被处理了 +SV = [0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, + 0x4787c62a, 0xa8304613, 0xfd469501, 0x698098d8, 0x8b44f7af, + 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, + 0x49b40821, 0xf61e2562&0xFF00FF00, 0xc040b340, 0x265e5a51, 0xe9b6c7aa& 0xFF0011FF, + 0xd62f105d, 0x2441453, 0xd8a1e681, 0xe7d3fbc8, 0x21e1cde6, + 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8 & 0xFF110011, + 0x676f02d9, 0x8d2a4c8a, 0xfffa3942, 0x8771f681, 0x6d9d6122, + 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, + 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x4881d05, 0xd9d4d039, + 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, 0xf4292244, 0x432aff97, + 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, + 0x85845dd1, 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, + 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391] + + +# 根据ascil编码把字符转成对应的二进制 +def binvalue(val, bitsize): + binval = bin(val)[2:] if isinstance(val, int) else bin(ord(val))[2:] + if len(binval) > bitsize: + raise ("binary value larger than the expected size") + while len(binval) < bitsize: + binval = "0" + binval + return binval + + +def string_to_bit_array(text): + array = list() + for char in text: + binval = binvalue(char, 8) + array.extend([int(x) for x in list(binval)]) + return array + + +# 循环左移 +def leftCircularShift(k, bits): + bits = bits % 32 + k = k % (2 ** 32) + upper = (k << bits) % (2 ** 32) + result = upper | (k >> (32 - (bits))) + return (result) + + +# 分块 +def blockDivide(block, chunks): + result = [] + size = len(block) // chunks + for i in range(0, chunks): + result.append(int.from_bytes(block[i * size:(i + 1) * size], byteorder="little")) + return result + + +# F函数作用于“比特位”上 +# if x then y else z +def F(X, Y, Z): + compute = ((X & Y) | ((~X) & Z)) + return compute + + +# if z then x else y +def G(X, Y, Z): + return ((X & Z) | (Y & (~Z))) + + +# if X = Y then Z else ~Z +def H(X, Y, Z): + return (X ^ Y ^ Z) + + +def I(X, Y, Z): + return (Y ^ (X | (~Z))) + + +# 四个F函数 +def FF(a, b, c, d, M, s, t): + xhsTemp = leftCircularShift((a + F(b, c, d) + M + t), s) + result = b + xhsTemp + return (result) + + +def GG(a, b, c, d, M, s, t): + result = b + leftCircularShift((a + G(b, c, d) + M + t), s) + return (result) + + +def HH(a, b, c, d, M, s, t): + result = b + leftCircularShift((a + H(b, c, d) + M + t), s) + return (result) + + +def HH1(a, b, c, d, M, s, t): + result = b + leftCircularShift((a + H(b, c, d) + M + t), s) + return (result) + + +def II(a, b, c, d, M, s, t): + result = b + leftCircularShift((a + I(b, c, d) + M + t), s) + return (result) + + +# 数据转换 +def fmt8(num): + bighex = "{0:08x}".format(num) + binver = binascii.unhexlify(bighex) + result = "{0:08x}".format(int.from_bytes(binver, byteorder='little')) + return (result) + + +# 计算比特长度 +def bitlen(bitstring): + return len(bitstring) * 8 + + +def md5sum(msg): + # 计算比特长度,如果内容过长,64个比特放不下。就取低64bit。 + msgLen = bitlen(msg) % (2 ** 64) + # 先填充一个0x80,其实是先填充一个1,后面跟对应个数的0,因为一个明文的编码至少需要8比特,所以直接填充 0b10000000即0x80 + msg = msg + b'\x80' # 0x80 = 1000 0000 + zeroPad = (448 - (msgLen + 8) % 512) % 512 + zeroPad //= 8 + msg = msg + b'\x00' * zeroPad + msgLen.to_bytes(8, byteorder='little') + # 计算循环轮数,512个为一轮 + msgLen = bitlen(msg) + iterations = msgLen // 512 + # print(msgLen, iterations) + # 初始化变量 + # 算法魔改的第一个点,也是最明显的点 + + D = 0x67452301 + C = 0xefcdab89 + B = 0x98badcfe + A = 0x10325476 + + # main loop 魔改点 位移数基本都改了 + for i in range(0, iterations): + a = A + b = B + c = C + d = D + block = msg[i * 64:(i + 1) * 64] + M = blockDivide(block, 16) + + # Rounds 16 + a = FF(a, b, c, d, M[0], 6, SV[0]) + d = FF(d, a, b, c, M[1], 13, SV[1]) + c = FF(c, d, a, b, M[2], 17, SV[2]) + b = FF(b, c, d, a, M[3], 21, SV[3]) + a = FF(a, b, c, d, M[4], 7, SV[4]) + d = FF(d, a, b, c, M[5], 12, SV[5]) + c = FF(c, d, a, b, M[6], 17, SV[6]) + b = FF(b, c, d, a, M[7], 20, SV[7]) + a = FF(a, b, c, d, M[8], 7, SV[8]) + d = FF(d, a, b, c, M[9], 12, SV[9]) + c = FF(c, d, a, b, M[10], 16, SV[10]) + b = FF(b, c, d, a, M[11], 22, SV[11]) + a = FF(a, b, c, d, M[12], 7, SV[12]) + d = FF(d, a, b, c, M[13], 13, SV[13]) + c = FF(c, d, a, b, M[14], 17, SV[14]) + b = FF(b, c, d, a, M[15], 22, SV[15]) + + a = GG(a, b, c, d, M[1], 5, SV[16]) + d = GG(d, a, b, c, M[6], 9, SV[17]) + c = GG(c, d, a, b, M[11], 14, SV[18]) + b = GG(b, c, d, a, M[0], 20, SV[19]) + a = GG(a, b, c, d, M[5], 5, SV[20]) # 21 step + d = GG(d, a, b, c, M[10], 9, SV[21]) # 22 step + c = GG(c, d, a, b, M[15], 14, SV[22]) # 23 step + b = GG(b, c, d, a, M[4], 20, SV[23]) + a = GG(a, b, c, d, M[9], 5, SV[24]) + d = GG(d, a, b, c, M[14], 9, SV[25]) + c = GG(c, d, a, b, M[3], 14, SV[26]) # 27 step + b = GG(b, c, d, a, M[8], 20, SV[27]) + a = GG(a, b, c, d, M[13], 5, SV[28]) # 29 step + d = GG(d, a, b, c, M[2], 9, SV[29]) # 30 step + c = GG(c, d, a, b, M[7], 14, SV[30]) + b = GG(b, c, d, a, M[12], 20, SV[31]) + + # 16轮 + a = HH(a, b, c, d, M[5], 4, SV[32]) # 33 step + d = HH(d, a, b, c, M[8], 11, SV[33]) + c = HH(c, d, a, b, M[11], 16, SV[34]) + + b = HH(b, c, d, a, M[14], 23, SV[35]) # 36 + a = HH(a, b, c, d, M[1], 4, SV[36]) # 37 + d = HH(d, a, b, c, M[4], 11, SV[37]) # 38 + c = HH(c, d, a, b, M[7], 16, SV[38]) # 39 + + # 正常的第40步 + # b = HH(b, c, d, a, M[10], 23, SV[39]) + a = HH(a, b, c, d, M[13], 4, SV[40]) # 第40步 + + b = HH(b, c, a, d, M[10], 23, SV[39]) # 第41步 + c = HH(c, d, a, b, M[3], 16, SV[42]) # 第42步 + d = HH(d, a, b, c, M[0], 11, SV[41]) # 43 + b = HH(b, c, d, a, M[6], 23, SV[43]) # 44 + a = HH(a, b, c, d, M[9], 4, SV[44]) # 45 + d = HH(d, a, b, c, M[12], 11, SV[45]) # 46 + c = HH(c, d, a, b, M[15], 16, SV[46]) # 47 + b = HH(b, c, d, a, M[2], 23, SV[47]) # 48 + + a = II(a, b, c, d, M[0], 6, SV[48]) + d = II(d, a, b, c, M[7], 10, SV[49]) + c = II(c, d, a, b, M[14], 15, SV[50]) + b = II(b, c, d, a, M[5], 21, SV[51]) # 52 + a = II(a, b, c, d, M[12], 6, SV[52]) + d = II(d, a, b, c, M[3], 10, SV[53]) + c = II(c, d, a, b, M[10], 15, SV[54]) + b = II(b, c, d, a, M[1], 21, SV[55]) # 56 + a = II(a, b, c, d, M[8], 6, SV[56]) + d = II(d, a, b, c, M[15], 10, SV[57]) + c = II(c, d, a, b, M[6], 15, SV[58]) + b = II(b, c, d, a, M[13], 21, SV[59]) # 60 + a = II(a, b, c, d, M[4], 6, SV[60]) + d = II(d, a, b, c, M[11], 10, SV[61]) + c = II(c, d, a, b, M[2], 15, SV[62]) # 63 + b = II(b, c, d, a, M[9], 21, SV[63]) + A = (A + a) % (2 ** 32) + B = (B + b) % (2 ** 32) + C = (C + c) % (2 ** 32) + D = (D + d) % (2 ** 32) + + result = fmt8(A) + fmt8(B) + fmt8(C) + fmt8(D) + return result + + +def reverse(m): + return "".join([m[i+1] + m[i] for i in range(0, len(m), 2)]) + +if __name__ == "__main__": + key_list = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476] + # 第一轮计算 获得结果 c4a02b20 e93108b2 d607639f 52569057 这个和f6填充 得到结果 + data = bytes.fromhex("9cb4ec61773bebe384b03865794a3034c3bff438fedeb53d9c15a556feaaf8548bf532ee979caebb543f21edd9f4972370a4cce474a2c277a897aa4bf65fbf4d2f6170692f736e732f76312f6e6f74652f666565646e6f74655f69643d36366439323830333030303030303030316630316236383926706167653d31266e756d3d352666657463685f6d6f64653d3126736f757263653d266164735f747261636b5f69643d6669643d3137323538373037303531303636376565333737323431396563336236353064393436663763386632653137266465766963655f66696e6765727072696e743d3230323430393034313530303332663930373030323232316565343637393364323839346164323532366438383230316165626231666534646263626661266465766963655f66696e6765727072696e74313d3230323430393034313530303332663930373030323232316565343637393364323839346164323532366438383230316165626231666534646263626661266c61756e63685f69643d3137323539333737363626747a3d417369612532465368616e67686169266368616e6e656c3d504d67647431393933353733372676657273696f6e4e616d653d362e39372e302e312664657669636549643d32666537353036322d613532382d333334302d626564332d32323061363766376632343026706c6174666f726d3d616e64726f6964267369643d73657373696f6e2e31373235383730373132363338383830383638313238266964656e7469666965725f666c61673d3026743d313732353933373739372670726f6a6563745f69643d454346414146266275696c643d3639373031383126785f74726163655f706167655f63757272656e743d6578706c6f72655f66656564266c616e673d7a682d48616e73266170705f69643d4543464141463031267569733d6461726b706c6174666f726d3d616e64726f6964266275696c643d363937303138312664657669636549643d32666537353036322d613532382d333334302d626564332d323230613637663766323430") + # 2f6170692f736e732f76312f6e6f74652f666565646e6f74655f69643d36366365656162653030303030303030316430336235343626706167653d31266e756d3d352666657463685f6d6f64653d3126736f757263653d266164735f747261636b5f69643d6669643d3137323136333931353431303334383363306461616163653263613932363663626133376163396665313134266465766963655f66696e6765727072696e743d3230323430373232313735383139323830393030366537653333346534363632383632306636373638626366336230313533623139373762396636636436266465766963655f66696e6765727072696e74313d3230323430373232313735383139323830393030366537653333346534363632383632306636373638626366336230313533623139373762396636636436266370755f6e616d653d5175616c636f6d6d2b546563686e6f6c6f676965732532432b496e632b534d38313530266465766963655f6d6f64656c3d70686f6e65266c61756e63685f69643d3137323735373830333626747a3d417369612532465368616e67686169266368616e6e656c3d4350412d334453502d4e332d5a534b4a2676657273696f6e4e616d653d362e39372e302e31266f766572736561735f6368616e6e656c3d302664657669636549643d31313932313466632d306665352d336165382d393164642d62616138323163313133323426706c6174666f726d3d616e64726f6964267369643d73657373696f6e2e31373231363339323031313432303736333831313331266964656e7469666965725f666c61673d3426743d313732373539303939382670726f6a6563745f69643d454346414146266275696c643d36393730313831266c616e673d7a682d48616e73266170705f69643d4543464141463031267569733d6461726b267465656e616765723d30706c6174666f726d3d616e64726f6964266275696c643d363937303138312664657669636549643d31313932313466632d306665352d336165382d393164642d626161383231633131333234 + # data = bytes.fromhex("868da458ff5a821fcff6f8bdafc009f56debf5290e0758a5d3ba4a4ae08b55764982003382b5907464bc90c34b877308474d16dec037edcfbf0ed45c2c045e2d2f6170692f736e732f76312f6e6f74652f666565646e6f74655f69643d36366365656162653030303030303030316430336235343626706167653d31266e756d3d352666657463685f6d6f64653d3126736f757263653d266164735f747261636b5f69643d6669643d3137323136333931353431303334383363306461616163653263613932363663626133376163396665313134266465766963655f66696e6765727072696e743d3230323430373232313735383139323830393030366537653333346534363632383632306636373638626366336230313533623139373762396636636436266465766963655f66696e6765727072696e74313d3230323430373232313735383139323830393030366537653333346534363632383632306636373638626366336230313533623139373762396636636436266370755f6e616d653d5175616c636f6d6d2b546563686e6f6c6f676965732532432b496e632b534d38313530266465766963655f6d6f64656c3d70686f6e65266c61756e63685f69643d3137323735373830333626747a3d417369612532465368616e67686169266368616e6e656c3d4350412d334453502d4e332d5a534b4a2676657273696f6e4e616d653d362e39372e302e31266f766572736561735f6368616e6e656c3d302664657669636549643d31313932313466632d306665352d336165382d393164642d62616138323163313133323426706c6174666f726d3d616e64726f6964267369643d73657373696f6e2e31373231363339323031313432303736333831313331266964656e7469666965725f666c61673d3426743d313732373539303939382670726f6a6563745f69643d454346414146266275696c643d36393730313831266c616e673d7a682d48616e73266170705f69643d4543464141463031267569733d6461726b267465656e616765723d30706c6174666f726d3d616e64726f6964266275696c643d363937303138312664657669636549643d32666537353036322d613532382d333334302d626564332d323230613637663766323430") + # 第二轮计算 获得key c4a02b20e93108b2d607639f52569057 + # 7e424e3418de39d5ca9a0ba8d9dfde24 + # data = bytes.fromhex("ece7ce329530e875a59c92d7c5aa639f07819f43646d32cfb9d020208ae13f1c23e86a59e8dffa1e0ed6faa921ed19622d277cb4aa5d87a5d564be36466e34477e424e3418de39d5ca9a0ba8d9dfde24") + + print("plainText: ", data) + print("result: ", md5sum(data)) + + + diff --git a/xhs/shield_rc4.py b/xhs/shield_rc4.py new file mode 100644 index 0000000..a2cf39d --- /dev/null +++ b/xhs/shield_rc4.py @@ -0,0 +1,36 @@ + +""" +标准rc4 +""" + +def KSA(key): + """ Key-Scheduling Algorithm (KSA) """ + S = list(range(256)) + j = 0 + for i in range(256): + j = (j + S[i] + key[i % len(key)]) % 256 + S[i], S[j] = S[j], S[i] + return S + + +def PRGA(S): + """ Pseudo-Random Generation Algorithm (PRGA) """ + i, j = 0, 0 + while True: + i = (i + 1) % 256 + j = (j + S[i]) % 256 + S[i], S[j] = S[j], S[i] + K = S[(S[i] + S[j]) % 256] + yield K + + +def RC4(key, text): + """ RC4 encryption/decryption """ + S = KSA(key) + keystream = PRGA(S) + res = [] + for char in text: + res.append(char ^ next(keystream)) + return bytes(res) + + diff --git a/xhs/xhs_aes.py b/xhs/xhs_aes.py new file mode 100644 index 0000000..8912678 --- /dev/null +++ b/xhs/xhs_aes.py @@ -0,0 +1,28 @@ +from Crypto.Cipher import AES +from Crypto.Util.Padding import pad + +""" +8.13 xhs-aes版本 +""" +def encrypt_aes_cbc(plaintext): + """ + aes-cbc + :param plaintext: + :return: + """ + key = "3061396677336d3671666c6c3264656a" + iv = "6d686171686e6a6d723072736f6f336f" + cipher = AES.new(bytes.fromhex(key), AES.MODE_CBC, bytes.fromhex(iv)) + ciphertext = cipher.encrypt(pad(bytes.fromhex(plaintext), AES.block_size)) + return ciphertext.hex() + + +def aes_encrypt(plain_text): + """ + 转换一下为 hex字符串便于加密操作 + :param plain_text: + :return: + """ + array = "".join([hex(i)[2:] for i in plain_text]) + payload = encrypt_aes_cbc(array) + return payload \ No newline at end of file diff --git a/xhs/xhs_captcha.py b/xhs/xhs_captcha.py new file mode 100644 index 0000000..1c84592 --- /dev/null +++ b/xhs/xhs_captcha.py @@ -0,0 +1,208 @@ +import math +import time +import json +import random +from kuaishou.ks_http import download_q +from utils.rotate_captcha import export_single_discern +from xhs.xhs_param import get_xs, encrypt_info, decrypt_info +from loguru import logger + + +class SignalCaptcha(object): + """ + 小红书单旋转验证码 + """ + def __init__(self, web_session, verify_uuid, + a1, webId, source="", is_proxy=False): + self.is_proxy = is_proxy + self.source = source + self.verify_uuid = verify_uuid + self.register_url = "https://edith.xiaohongshu.com/api/redcaptcha/v2/captcha/register" + self.check_url = "https://edith.xiaohongshu.com/api/redcaptcha/v2/captcha/check" + self.headers = { + "authority": "edith.xiaohongshu.com", + "accept": "application/json, text/plain, */*", + "accept-language": "zh-CN,zh;q=0.9", + "cache-control": "no-cache", + "content-type": "application/json;charset=UTF-8", + "origin": "https://www.xiaohongshu.com", + "pragma": "no-cache", + "referer": "https://www.xiaohongshu.com/", + "sec-ch-ua": "\"Not_A Brand\";v=\"8\", \"Chromium\";v=\"120\", \"Google Chrome\";v=\"120\"", + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": "\"macOS\"", + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-site", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "x-s": "XYW_eyJzaWduU3ZuIjoiNTEiLCJzaWduVHlwZSI6IngxIiwiYXBwSWQiOiJsb2dpbiIsInNpZ25WZXJzaW9uIjoiMSIsInBheWxvYWQiOiJkNzkxNmQ0MDAyOTZmZDg4ZmIzYjcxOGY2MWRlOGU2MTEwYzZmMTBlMjZiOWI0NTQ1OTg0M2VjMTI2NmRlMGZiNDNkZGI3MmE5MWYyOTJkZTM4Yjk3MDlmZjcyMDBmMTRjOWUzYmZkYTFmYWExZWI5MGQ3NGFhMzFiNTRjNzJjZDBkNzRhYTMxYjU0YzcyY2RhYzQ4OWI5ZGE4Y2U1ZTQ4ZjRhZmI5YWNmYzNlYTI2ZmUwYjI2NmE2YjRjYzNjYjVhMDE2NzA1NzIyMWMxZjhkMzY4YzFkMjBjZGE4MGY5OWIxMWYyZWFhMGE5ZmQ3MWQ3NmZmZTgwMmI3MjM3NWQ2OWZiY2JjYzk1OTUxMmNhZDRmZWFjNWU1MjY1MDljYTliZDA3MzE0OGZjMDEzYzJlMzU4ZWE1ZDg5ZGQ4MzZkNWU2OTM4NGJkN2FmNjQ3MjY0ZmNjZWQxMmVkMWFiNmFjY2U1YTE1MWZmZDMzODNlZDE4OGMyMWE4OTlmY2E2Y2UifQ==", + "x-s-common": "2UQAPsHCPUIjqArjwjHjNsQhPsHCH0rjNsQhPaHCH0P1+jhhHjIj2eHjwjQ+GnPW/MPjNsQhPUHCHfl689S1HjIj2ecjwjHIN0L1+eGjNsQh+aHCH0rh8BpSGfp0G9cEP7pi+9p94gkT8fYh4gbx8opD+fMjJer9weG9JgbYPAZIPeZU+erh+0DjNsQh+jHCP/qIw/r7P/c9P0cM+jIj2eqjwjQGnp4K8gSt2fbg8oppPMkMank6yLELznSPcFkCGp4D4p8HJo4yLFD9anEd2rSk49S8nrQ7LM4zyLRkad+jPfzIGfSQqFS1/dmyP0pgnSYt2fbgwgpQyfRk/p+QqFS1cfYSp7Y9np4zyLRkafTw2fTh/fMzPrMrcgSOpbkTnDzd+bkTagk8yf+h/F48PDMgLflOzMLU/pzb4MSCnfM+prQV/nkyyLRga/mwpbrl/MzdPrFUpfk+prDU/fMaJrMonfSwzrE3nD4QPFMTz/p+pMSE/MztyMkL//z8yfVA/LzVJpkxG7S+zrQT/pzayDRgpgk8Jpk3npzBJpSgzgYypFDM/L4zPFEozfY+2D8k/SzayDECafkyzrQ3/dkaybSL/gY8ySLIngk02pDULfY82SrF/M4Q+pkoz/z8PSLlnSzz+bkxLfYyJLDMnp4wJpSC/fl8prDUnfMp4MSxa/QwJLLUnnkaySSC/fk8yS8i/LzbPDECc/bw2DSE/p4+2bkxyBT+2SDFngkByFMxcfkyzFLF/L48+LRgagY+pMSEnS4ByrMoz/pyJprA/p4zPFETnfMypB47/nMQPFMx//+wpMrU/M4yypDUafk82fVl/Mzp2rMT/fYyzbQTnS4ByDEgp/b+ySDl/LzVySSLJBSyzrrA/gkbPrRonfk+zrpCngktJrMCp/zypFLlnDzd+pkoL/z+2Sk3/S4pPDRL//zwzFk3/0QBJLExL/++ySDInfMwySkgLgY+Jp83/p4ByLETzfk8PSLlnpzbPLMgnfMyzrMC/FzwJbkrz/zOzFME/p4b+rRLJBM8PDLUn/Qpyn8zO/FjNsQhwsHCHDDAwoQH8B4AyfRI8FS98g+Dpd4daLP3JFSb/BMsn0pSPM87nrldzSzQ2bPAGdb7zgQB8nph8emSy9E0cgk+zSS1qgzianYt8p+DpoYlqg4Dag8mqM4sG9Y7LozF89FF+DTp2dYQyemAPrlNq9kl49EE+Fzyag86q7YjLBkEG7pmanYN8LzY+7+fySzLadbFLjTl4FbI8omwaL+aaMm/p94Aqg4La/P98/+l49E7qg4raLp+qFSi8Bph/bSDagY8PFS9+g+g4g4atA4ILok0/d+Dn/+S8dbFcLS3/fLApd41qgbFqomM4e+N2f4APp4C8LSepS4QysVINMmFLLTM4FbQPMiUJ9MD8nSl498QcFbSpb8FqDSbtUTQznM1G98D8nkdGApSqApSzobFnd+++np/Job/qrQN8n8T+o+Q2rRSzbmFnDSbPBpx4gzpq7knPLhE/r4Q2BRSpop7JBETcnph8rpcanYiJLSbybmzJF8xaL+aLoS0zUTQyFYQ8pm7LDS9zApoJ0mAyS4SqA8M4AbQ408S8fEt8pSSLf4QyApSygQT+LTl4M+QyLTApBR98/8+8oPApd4CJnRwqMSl4e+QPF8YJ9u98/ml49zCG/4ALMm78LShJobQ2bk9Gp8FJfpM4ozA8n+jLnQdqMDI/7+hqgzdanTw8LzA4d+nLFDlanT6q9kBP7+h8DlManTw8nSn4B8Qy9++Lbm7aozc4rbQP9pSPpmFnLS387Pl4g4kanWAq98c4rSFqg4d8gb7JrS9qLpQ2b4Cz98lyL4Qa9LApdzQanYHwrSk+9p/Lo4dGfMbwomx+g+DyfQ3anS0/7bc4ozyLo49agG78nTc4bScqDTSyn+wq9SQ2d+Qye4A+fu9qMzM4FkSpdzda/+NqAmc4FSQcFbA8SpjqFDAcg+/PrRS8S87cFDA+7+3qg4M/fza4FSiaomQc94APgHA8pz64fprLA8S8b8FcDS3yebQPMr9qS8FLLS9G9YQzLEAPF8lcg4P+9p3pdzYanTkOaHVHdWEH0iT+AcFPecU+AcUNsQhP/Zjw0H7+gF=", + "x-t": "1709171462456" + } + + self.pic_headers = { + "authority": "picasso-static.xiaohongshu.com", + "accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8", + "accept-language": "zh-CN,zh;q=0.9", + "cache-control": "no-cache", + "origin": "https://www.xiaohongshu.com", + "pragma": "no-cache", + "sec-ch-ua": "\"Not_A Brand\";v=\"8\", \"Chromium\";v=\"120\", \"Google Chrome\";v=\"120\"", + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": "\"macOS\"", + "sec-fetch-dest": "image", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-site", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + } + + self.cookies = { + "a1": a1, + "webId": webId, + "web_session": web_session, + } + + + @staticmethod + def get_slide_track(distance): + """ + 根据滑动距离生成滑动轨迹 + :param distance: 需要滑动的距离 + :return: 滑动轨迹: [[x,y,t], ...] + x: 已滑动的横向距离 + y: 已滑动的纵向距离, 除起点外, 均为0 + t: 滑动过程消耗的时间, 单位: 毫秒 + """ + t = 246 + count = random.randint(4, 10) + _y_list = [0, 0, -1, 1, 1, 1, 1, 0] + y = 0 + x = 0 + slide_track = [[0, 0, 246]] + + for i in range(count): + if x >= distance: + break + + t += random.randint(50, 200) + y += _y_list[random.randint(0, 7)] + x += random.randint(30, 60) + slide_track.append([x, y, t]) + + if x != distance: + slide_track[-1][0] = distance + return slide_track, slide_track[-1][-1] + + @staticmethod + def get_captcha_info(fg_path, bg_path): + """ + 图片识别 以及对于captcha_info加密参数的封装 + :param fg_path: + :param bg_path: + :return: + """ + angle = export_single_discern(fg_path, bg_path) + distance = math.ceil(angle * 0.79) + logger.info(f"处理后的 距离为 : {distance}") + track, last_time = SignalCaptcha.get_slide_track(distance) + _time = last_time + random.randint(100, 200) # 处理字段 + logger.info(f"_time: {_time}") + logger.info(f"track : {track}") + captcha_info = { + "mouseEnd": encrypt_info(str(distance), _type="mouseEnd"), + "time": encrypt_info(str(_time), _type="time"), + "track": encrypt_info(str(track).replace(" ", ""), _type="track"), + "width": encrypt_info("286", _type="width") + } + + captcha_info = json.dumps(captcha_info, ensure_ascii=False, separators=(',', ':')) + + return captcha_info + + def check(self, rid, captcha_info): + """ + 验证函数:最终提交验证码验证请求 + :param rid: + :param captcha_info: + :return: + """ + data = { + "rid": rid, + "verifyType": 102, + "verifyBiz": "461", + "verifyUuid": self.verify_uuid, + "biz": "sns_web", + "sourceSite": self.source, + "version": "1.0.1", + "captchaInfo": captcha_info + } + + headers = self.headers.copy() + data = json.dumps(data, separators=(',', ':'), ensure_ascii=False) + h = get_xs(self.cookies["a1"], "/api/redcaptcha/v2/captcha/check", data, str(int(time.time() * 1000))) + headers.update(h) + logger.info(data) + response = download_q(self.check_url, headers=headers, cookies=self.cookies, + data=data, is_proxy=self.is_proxy) + return response.json() + + def register(self): + """ + 初始化函数:获得验证码初始化信息请求 + :return: + """ + data = { + "secretId": "000", + "verifyType": "102", + "verifyUuid": self.verify_uuid, + "verifyBiz": "461", + "biz": "sns_web", + "sourceSite": self.source, + "version": "1.0.1" + } + data = json.dumps(data, separators=(',', ':'), ensure_ascii=False) + h = get_xs(self.cookies["a1"], "/api/redcaptcha/v2/captcha/register", data, str(int(time.time() * 1000))) + headers = self.headers.copy() + headers["x-s-common"] = "" + headers.update(h) + response = download_q(self.register_url, headers=headers, cookies=self.cookies, + data=data, is_proxy=self.is_proxy) + return response.json() + + def run(self): + """ + main: 执行函数 + :return: + """ + try: + register_data = self.register() + logger.info(f"初始化验证码信息:{register_data}") + data = register_data["data"] + rid = data["rid"] + captcha_info = data["captchaInfo"] + source = json.loads(decrypt_info(captcha_info)[:-2]) # 因为有补位则去掉补位结果 + logger.info(f"解密验证码初始信息:{source}") + bg_url = source["backgroundUrl"] + fg_url = source["captchaUrl"] + bg_path = download_q(bg_url, headers=self.pic_headers, cookies={}).content + fg_path = download_q(fg_url, headers=self.pic_headers, cookies={}).content + captcha_info = SignalCaptcha.get_captcha_info(fg_path, bg_path) + logger.info(f"生成验证信息:{captcha_info}") + result = self.check(rid, captcha_info) + logger.info(f"验证结果:{result}") + except Exception as e: + result = {"msg": "失败", "data": {}} + logger.error(e) + + return result + + +if __name__ == '__main__': + verify_uuid = "22318d30-daf4-4fc1-8148-c26d86b0a51a*CiZjkIoB" + web_session = "030037a2d377ba382693826598224a7950081e" + a1 = "" + webId = "" + source = "https://www.xiaohongshu.com/explore/65c02be2000000002c028b93" + A = SignalCaptcha(web_session, verify_uuid, a1, webId, source=source) + res = A.run() + print(res) diff --git a/xhs/xhs_des.py b/xhs/xhs_des.py new file mode 100644 index 0000000..091556f --- /dev/null +++ b/xhs/xhs_des.py @@ -0,0 +1,226 @@ +import ctypes +from functools import reduce +from hashlib import md5 + +""" +标准des,由于不知道部分key的utf-8, +通过捕获的key还原标准des调用 +""" + +def MD5(strs): + m = md5() + m.update(strs.encode("utf-8")) + return m.hexdigest() + + +def hex_2_str(v: int): + v = v & 0xffffffff # 将结果转换为32位有符号整数 + tmp = str(hex(v))[2:] + return "0" + tmp if len(tmp) % 2 > 0 else tmp + + +def logical_left_shift(n, bits): + """ + 有符号左移 + :param n: + :param bits: + :return: + """ + return ctypes.c_int(n << bits).value + + +def logical_right_shift(n, bits): + """ + 无符号右移 + :param n: + :param bits: + :return: + """ + return (n % 0x100000000) >> bits + + +def get_s_box(i, j): + """ + sbox + :param i: + :param j: + :return: + """ + s_box1 = [-2146402272, -2147450880, 32768, 1081376, 1048576, 32, -2146435040, -2147450848, -2147483616, -2146402272, + -2146402304, -2147483648, -2147450880, 1048576, 32, -2146435040, 1081344, 1048608, -2147450848, 0, + -2147483648, 32768, 1081376, -2146435072, 1048608, -2147483616, 0, 1081344, 32800, -2146402304, + -2146435072, 32800, 0, 1081376, -2146435040, 1048576, -2147450848, -2146435072, -2146402304, 32768, + -2146435072, -2147450880, 32, -2146402272, 1081376, 32, 32768, -2147483648, 32800, -2146402304, 1048576, + -2147483616, 1048608, -2147450848, -2147483616, 1048608, 1081344, 0, -2147450880, 32800, -2147483648, + -2146435040, -2146402272, 1081344] + s_box2 = [8396801, 8321, 8321, 128, 8396928, 8388737, 8388609, 8193, 0, 8396800, 8396800, 8396929, 129, 0, 8388736, + 8388609, 1, 8192, 8388608, 8396801, 128, 8388608, 8193, 8320, 8388737, 1, 8320, 8388736, 8192, 8396928, + 8396929, 129, 8388736, 8388609, 8396800, 8396929, 129, 0, 0, 8396800, 8320, 8388736, 8388737, 1, 8396801, + 8321, 8321, 128, 8396929, 129, 1, 8192, 8388609, 8193, 8396928, 8388737, 8193, 8320, 8388608, 8396801, + 128, 8388608, 8192, 8396928] + s_box3 = [536870928, 541065216, 16384, 541081616, 541065216, 16, 541081616, 4194304, 536887296, 4210704, 4194304, + 536870928, 4194320, 536887296, 536870912, 16400, 0, 4194320, 536887312, 16384, 4210688, 536887312, 16, + 541065232, 541065232, 0, 4210704, 541081600, 16400, 4210688, 541081600, 536870912, 536887296, 16, + 541065232, 4210688, 541081616, 4194304, 16400, 536870928, 4194304, 536887296, 536870912, 16400, 536870928, + 541081616, 4210688, 541065216, 4210704, 541081600, 0, 541065232, 16, 16384, 541065216, 4210704, 16384, + 4194320, 536887312, 0, 541081600, 536870912, 4194320, 536887312] + s_box4 = [268439616, 4096, 262144, 268701760, 268435456, 268439616, 64, 268435456, 262208, 268697600, 268701760, + 266240, 268701696, 266304, 4096, 64, 268697600, 268435520, 268439552, 4160, 266240, 262208, 268697664, + 268701696, 4160, 0, 0, 268697664, 268435520, 268439552, 266304, 262144, 266304, 262144, 268701696, 4096, + 64, 268697664, 4096, 266304, 268439552, 64, 268435520, 268697600, 268697664, 268435456, 262144, 268439616, + 0, 268701760, 262208, 268435520, 268697600, 268439552, 268439616, 0, 268701760, 266240, 266240, 4160, + 4160, 262208, 268435456, 268701696] + s_box5 = [16843776, 0, 65536, 16843780, 16842756, 66564, 4, 65536, 1024, 16843776, 16843780, 1024, 16778244, + 16842756, 16777216, 4, 1028, 16778240, 16778240, 66560, 66560, 16842752, 16842752, 16778244, 65540, + 16777220, 16777220, 65540, 0, 1028, 66564, 16777216, 65536, 16843780, 4, 16842752, 16843776, 16777216, + 16777216, 1024, 16842756, 65536, 66560, 16777220, 1024, 4, 16778244, 66564, 16843780, 65540, 16842752, + 16778244, 16777220, 1028, 66564, 16843776, 1028, 16778240, 16778240, 0, 65540, 66560, 0, 16842756] + s_box6 = [520, 134349312, 0, 134348808, 134218240, 0, 131592, 134218240, 131080, 134217736, 134217736, 131072, + 134349320, 131080, 134348800, 520, 134217728, 8, 134349312, 512, 131584, 134348800, 134348808, 131592, + 134218248, 131584, 131072, 134218248, 8, 134349320, 512, 134217728, 134349312, 134217728, 131080, 520, + 131072, 134349312, 134218240, 0, 512, 131080, 134349320, 134218240, 134217736, 512, 0, 134348808, + 134218248, 131072, 134217728, 134349320, 8, 131592, 131584, 134217736, 134348800, 134218248, 520, + 134348800, 131592, 8, 134348808, 131584] + s_box7 = [256, 34078976, 34078720, 1107296512, 524288, 256, 1073741824, 34078720, 1074266368, 524288, 33554688, + 1074266368, 1107296512, 1107820544, 524544, 1073741824, 33554432, 1074266112, 1074266112, 0, 1073742080, + 1107820800, 1107820800, 33554688, 1107820544, 1073742080, 0, 1107296256, 34078976, 33554432, 1107296256, + 524544, 524288, 1107296512, 256, 33554432, 1073741824, 34078720, 1107296512, 1074266368, 33554688, + 1073741824, 1107820544, 34078976, 1074266368, 256, 33554432, 1107820544, 1107820800, 524544, 1107296256, + 1107820800, 34078720, 0, 1074266112, 1107296256, 524544, 33554688, 1073742080, 524288, 0, 1074266112, + 34078976, 1073742080] + s_box8 = [2097152, 69206018, 67110914, 0, 2048, 67110914, 2099202, 69208064, 69208066, 2097152, 0, 67108866, 2, + 67108864, 69206018, 2050, 67110912, 2099202, 2097154, 67110912, 67108866, 69206016, 69208064, 2097154, + 69206016, 2048, 2050, 69208066, 2099200, 2, 67108864, 2099200, 67108864, 2099200, 2097152, 67110914, + 67110914, 69206018, 69206018, 2, 2097154, 67108864, 67110912, 2097152, 69208064, 2050, 2099202, 69208064, + 2050, 67108866, 69208066, 69206016, 2099200, 0, 2, 69208066, 0, 2099202, 69206016, 2048, 67108866, + 67110912, 2048, 2097154] + + s_box = [s_box1, s_box2, s_box3, s_box4, s_box5, s_box6, s_box7, s_box8] + return s_box[i][j] + + +def alg_main(key, _plaintext_left, _plaintext_right): + """ + 3.轮密钥和明文块计算 + 计算后的明文还原 + :param key: + :param _plaintext_left: + :param _plaintext_right: + :return: 处理后的16进制字符串、处理后的字符串 + """ + for i in range(0, len(key), 2): + _key = key[i] # 轮密钥 + _key2 = key[i+1] + tmp = _plaintext_right ^ _key + tmp1 = (logical_right_shift(_plaintext_right, 4) | logical_left_shift(_plaintext_right, 28)) ^ _key2 + + mid = get_s_box(0, logical_right_shift(tmp, 24) & 63) | get_s_box(1, logical_right_shift(tmp, 16) & 63) | \ + get_s_box(2, logical_right_shift(tmp, 8) & 63) | get_s_box(3, tmp & 63) | \ + get_s_box(4, logical_right_shift(tmp1, 24) & 63) | get_s_box(5, logical_right_shift(tmp1, 16) & 63) | \ + get_s_box(6, logical_right_shift(tmp1, 8) & 63) | get_s_box(7, tmp1 & 63) + + tmp2 = _plaintext_left ^ mid + _plaintext_left = _plaintext_right # 左边继承右边 + _plaintext_right = tmp2 + + # print(f"第{i}轮 ; ", _plaintext_left, _plaintext_right, mid, _key, _key2, tmp, tmp1) + # break + + t1 = logical_right_shift(_plaintext_right, 1) | logical_left_shift(_plaintext_right, 31) + t2 = logical_right_shift(_plaintext_left, 1) | logical_left_shift(_plaintext_left, 31) + t3 = (logical_right_shift(t1, 1) ^ t2) & 1431655765 + t4 = t2 ^ t3 + t5 = t1 ^ logical_left_shift(t3, 1) + t6 = (logical_right_shift(t4, 8) ^ t5) & 16711935 + t7 = t5 ^ t6 + t8 = t4 ^ logical_left_shift(t6, 8) + t9 = (logical_right_shift(t8, 2) ^ t7) & 858993459 + t10 = t7 ^ t9 + t11 = logical_left_shift(t9, 2) ^ t8 + t12 = (logical_right_shift(t10, 16) ^ t11) & 65535 + t13 = t11 ^ t12 + t14 = t10 ^ logical_left_shift(t12, 16) + t15 = (logical_right_shift(t14, 4) ^ t13) & 252645135 + t16 = t15 ^ t13 + t17 = t14 ^ logical_left_shift(t15, 4) + + b1 = logical_right_shift(t17, 24) + b2 = logical_right_shift(t17, 16) & 255 + b3 = logical_right_shift(t17, 8) & 255 + b4 = t17 & 255 + + b5 = logical_right_shift(t16, 24) + b6 = logical_right_shift(t16, 16) & 255 + b7 = logical_right_shift(t16, 8) & 255 + b8 = t16 & 255 + + segment_list = [b1, b2, b3, b4, b5, b6, b7, b8] + segment = "".join([chr(s) for s in segment_list]) + segment_hex = "".join([hex_2_str(s) for s in segment_list]) + # print(t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17) + # print(segment) + # print(segment_hex) + return segment_hex, segment + + +def deal_array2(array1_res, array2_res): + """ + 2. 明文块预处理(和alg_main中的后半部分是可逆的) + :param array1_res: + :param array2_res: + :return: + """ + tmp = (logical_right_shift(array1_res, 4) ^ array2_res) & 252645135 + tmp1 = array2_res ^ tmp + tmp1_ = array1_res ^ logical_left_shift(tmp, 4) + tmp2 = (logical_right_shift(tmp1_, 16) ^ tmp1) & 65535 + tmp2_ = tmp1 ^ tmp2 + tmp3 = logical_right_shift(tmp2_, 2) + tmp4 = tmp1_ ^ logical_left_shift(tmp2, 16) + tmp5 = (tmp3 ^ tmp4) & 858993459 + tmp5_ = logical_left_shift(tmp5, 2) + tmp6 = tmp5 ^ tmp4 + tmp7 = (logical_right_shift(tmp2_ ^ tmp5_, 8) ^ tmp6) & 16711935 + tmp7_ = tmp6 ^ tmp7 + tmp8 = logical_right_shift(tmp7_, 1) + tmp9 = tmp2_ ^ tmp5_ ^ logical_left_shift(tmp7, 8) + tmp10 = (tmp8 ^ tmp9) & 1431655765 + tmp10_ = tmp9 ^ tmp10 + tmp11 = tmp7_ ^ logical_left_shift(tmp10, 1) + tmp12 = logical_left_shift(tmp11, 1) | logical_right_shift(tmp11, 31) + tmp13 = logical_left_shift(tmp10_, 1) | logical_right_shift(tmp10_, 31) + + return tmp12, tmp13 + + +def deal_array(array): + """ + 1.简单处理块 + :param array: + :return: + """ + for i in range(len(array)): + array[i] = array[i] << (24 - 8 * i) + res = reduce(lambda x, y: x | y, array) + return res + + +def des_encrypt(key, plain_text): + """ + des加密入口 + :param key: list + :param plain_text: list + :return: + """ + array1_hex = [] + array1_string = [] + for m in range(0, len(plain_text), 8): + left_list, right_list = plain_text[m:m + 4], plain_text[m + 4:m + 8] + left, right = deal_array2(deal_array(left_list), deal_array(right_list)) + segment_hex, segment = alg_main(key, left, right) + array1_hex.append(segment_hex) + array1_string.append(segment) + _hex = "".join(array1_hex) + _string = "".join(array1_string) + return _hex, _string + + diff --git a/xhs/xhs_param.py b/xhs/xhs_param.py new file mode 100644 index 0000000..9eb3ff7 --- /dev/null +++ b/xhs/xhs_param.py @@ -0,0 +1,199 @@ +import binascii +import json +import random +import time + +from utils.tool import download_q +from xhs.xhs_aes import aes_encrypt +from xhs.xhs_des import MD5, des_encrypt, logical_right_shift +import base64 +""" +修补xhs xs-xt 算法为 aes +""" + +def get_xs(a1, url, data, x_t): + """ + 获得 {"x-s":"", "x-t": ""} + :param a1: + :param url: + :param data: + :param x_t: + :return: + """ + key = [187050025, 472920585, 186915882, 876157969, 255199502, 806945584, 220596020, 958210835, 757275681, 940378667, + 489892883, 705504304, 354103316, 688857884, 890312192, 219096591, 622400037, 254088489, 907618332, 52759587, + 907877143, 53870614, 839463457, 389417746, 975774727, 372382245, 437136414, 909246726, 168694017, 473575703, + 52697872, 1010440969] + + # 7.19 改 + key = [52833590, 1010372866, 188091914, 406398501, 255201040, 957421848, 741478954, 958217745, 758320394, 990653224, + 958072630, 722273561, 890968096, 185282339, 890768915, 254222393, 890835209, 86457382, 907354431, 120004616, + 906834724, 120984878, 841809977, 370543655, 405617431, 909250592, 439235128, 875174166, 187044111, 742001189, + 184950816, 1010310941] + + def get_enviroment(api="/api/sns/web/v1/feed", data="{}"): + url = f"url={api}" + info = url + data + info = MD5(info) + return info + + x1 = get_enviroment(url, data) + x2 = "0|0|0|1|0|0|1|0|0|0|1|0|0|0|0" + x3 = a1 + x4 = x_t + info = f"x1={x1};x2={x2};x3={x3};x4={x4};" + + key_46_base64 = base64.b64encode(info.encode('utf-8')).decode('utf-8') + # base64 转为十进制数 + array = [ord(i) for i in key_46_base64] + + # 对上述数组进行位移操作 初始化明文 每四个数为一组进行计算 + # payload = des_encrypt(key, array)[0] + payload = aes_encrypt(array) + + _base_data = { + "signSvn": "53", + "signType": "x2", + "appId": "xhs-pc-web", + "signVersion": "1", + "payload": payload + } + data = json.dumps(_base_data, ensure_ascii=False, separators=(',', ':')) + x_s = base64.b64encode(data.encode("utf-8")).decode("utf-8") + + return {"x-s": "XYW_" + x_s, "x-t": x4} + + +def decrypt_info(captcha_info): + """ + 对于验证码的captchaInfo进行解密 + :param captcha_info: + :return: + """ + d1_h = base64.b64decode(captcha_info).hex() + hex_string = [int(d1_h[i:i + 2], 16) for i in range(0, len(d1_h), 2)] + + key = [302776838, 875694381, 453784583, 103810086, 571285557, 371853838, 805843717, 657858610, 909711421, 117571586, + 806950164, 52892707, 890572860, 184812328, 605753650, 151859252, 890381338, 556605977, 221318145, 688470832, + 20581922, 940376595, 220597508, 706218773, 707144202, 940115729, 153491459, 892344601, 171182610, 337259779, + 36577297, 271782150] + + payload = des_encrypt(key, hex_string)[-1] + + return payload + + +def encrypt_info(plain_text, _type="mouseEnd"): + """ + 提交参数进行加密: + mouseEnd: 最终滑动的距离 比例 286/360 + time: 开始到结束的时间差 毫秒级 + track: 轨迹:[x,y,时间差] + width: 宽度 固定 286 + :param plain_text: + :param _type: + :return: + """ + key_list = { + "mouseEnd": [187040774,941175553,188094512,672732199,87169561,957943856,758133532,957159952,756225326,588057094,890963239,185401634,889790517,185020707,874067993,103227439,874058250,119748888,906756366,119942921,571280678,104203531,304941601,372245811,439101969,808593969,170278418,876218908,186782722,1010306836,187048244,942146861], + "time": [151392289,942357812,20069652,739772683,18094115,959001623,789850125,17376056,624043796,51713567,606673961,990187014,1007232771,571224112,286990908,36584204,872954931,235544848,18625028,103158589,35458076,86060551,908336391,19927066,572793385,842413842,170468621,907553325,422194994,806880270,170526239,738861861], + "track": [187578120,538642213,135659559,941298489,221391896,489174819,724704537,688193565,605502240,723068432,890766372,453786172,691602974,50791978,874190097,656489730,337328660,120465962,856495123,218504222,805646351,104007424,373040900,371339527,170658349,622200847,539437315,876100658,455352379,975704860,456002850,940583190], + "width": [187040774,941175553,188094512,672732199,87169561,957943856,758133532,957159952,756225326,588057094,890963239,185401634,889790517,185020707,874067993,103227439,874058250,119748888,906756366,119942921,571280678,104203531,304941601,372245811,439101969,808593969,170278418,876218908,186782722,1010306836,187048244,942146861] + } + key = key_list[_type] + padding = 8 - len(plain_text) % 8 + + plain_text = [ord(i) for i in plain_text] + for c in range(padding): # 补位 + plain_text.append(padding) + + array1_hex2 = des_encrypt(key, plain_text)[0] + byte_data = binascii.unhexlify(array1_hex2) + base64_data = base64.b64encode(byte_data).decode("utf-8") + return base64_data + + +def get_a1(platform_type="Mac OS"): + """ + 获取cookie a1、webId 平台类型可能会涉及风控 + :param platform_type: + :return: + """ + + def genRandomString(length): + char_list = "abcdefghijklmnopqrstuvwxyz1234567890" + return ''.join(random.sample(char_list, length)) + + def crc32(data): + table = [] + for num in range(256): + temp = num + for _ in range(8): + temp = (logical_right_shift(temp, 1)) ^ 0XEDB88320 if temp & 1 else logical_right_shift(temp, 1) + table.append(temp) + i = -1 + for a in range(len(data)): + i = logical_right_shift(i, 8) ^ table[255 & (i ^ ord(data[a]))] + return logical_right_shift((-1 ^ i), 0) + + def getPlatformCode(t): + # 根据不同系统返回结果 + if t == "Android": + return "2" + elif t == "iOS": + return "1" + elif t == "Mac OS": + return "3" + elif t == "Linux": + return "4" + # elif t == "Windows": + # return "0" + else: + return "5" + + LOCAL_ID_SECRET_VERSION = "0" + o = str(hex(int(time.time() * 1000))[2:]) + o = o + genRandomString(30) + o = o + getPlatformCode(platform_type) + LOCAL_ID_SECRET_VERSION + "000" + o = o + str(crc32(o)) + a1 = o[:52] + + web_id = MD5(a1) + return a1, web_id + + +def get_comment(a1, xs, xt): + """ + 通过调用node来获得x-comment参数 + :param a1: + :param xs: + :param xt: + :return: + """ + url = "http://127.0.0.1:3000/xhs/get_comment" + data = { + "a1": a1, + "xs": xs, + "xt": xt + } + status, response = download_q(url, {}, {}, data=data) + return response.text + +if __name__ == '__main__': + data = { + "source_note_id": "64bf4ea7000000000c035e48", + "image_formats": [ + "jpg", + "webp", + "avif" + ], + "extra": { + "need_body_topic": "1" + }, + "xsec_source": "pc_feed", + "xsec_token": "ABSnHLvO9lnVSMKpZb-K1Cpn6d3xUC3z_mhRBvAbPGIe4=" + } + data = json.dumps(data, ensure_ascii=False, separators=(',', ':')) + + res = get_xs("191452e29d5vt5m5noazsyty9z4z3z695yoiu1pn130000168225", "/api/sns/web/v1/feed",data, "1723443461839") + print(res) \ No newline at end of file