📢 免责声明
本文所有内容仅供学习和交流使用,请勿商用!!!如出现纷争,与作者无关。
文章内容基于作者在撰写时的理解。文中提到的技术方案、代码示例及配置方法可能因软件版本更新、环境差异或操作失误而失效。
作者不保证文中内容的绝对正确性、完整性和时效性。读者在依照本文进行操作时,应自行评估风险,并对可能产生的数据丢失、系统损坏、服务中断或其他直接或间接损失负责。建议在操作前进行完整备份,并在测试环境中先行验证。如有错误,欢迎指正。
如有侵权,联系删除。

逆向分析 · 某工具站请求参数生成与响应解密

🌐 网站

aHR0cHM6Ly9keS5rdWt1dG9vbC5jb20v
(Base64 编码,解码后可见真实地址)

📡 请求分析

进入页面后打开 DevTools 会被断住,为了方便,直接设置“永不在此处暂停”。
粘贴一个链接开始解析,抓包发现有一个 parse 数据包,响应内容加密,正是我们的目标。

点击“发起程序”查看调用栈:

发起程序截图

请求是 fetch,请求体为变量 o,而 oawait y 得到。在 await 处打断点,重新发包:

断点截图

点击调用堆栈的上一层,发现响应结果被存入了 y(截图里标注有误,懒得改了)。

调用堆栈上层

将所有参数打印出来方便后续分析,发现还有 geoipIpk_25e532s_25e532uwx_id 这几个值未知。

打印参数

接着进入 y 函数内部:

进入y函数

接下来就是扣函数的常规操作了。

继续寻找其他参数:

  • geoipIp 的来源:
geoipIp来源
  • k_25e532s_25e532 的来源:
k和s参数
  • uwx_id 发现于 localStorage
localStorage中的uwx_id

因此需要对 localStorage.setItem 进行 Hook,代码如下:

const env_list = [[localStorage, 'setItem'], [Object.getPrototypeOf(localStorage), 'setItem']];
const ori_console_log = console.log;
for (let env of env_list) {
    const originalfunc = env[0][env[1]];
    Object.defineProperty(env[0], env[1], {
        value: new Proxy(originalfunc, {
            apply(target, thisArg, argumentsList) {
                const [key, value] = argumentsList;
                ori_console_log(`localStorage setItem -> ${key}\t = ${value}`);
                if (['uwx_id'].indexOf(key) != -1) {
                    debugger;
                }
                const result = Reflect.apply(target, thisArg, [key, value]);
                return result;
            }
        })
    });
}

用法:对脚本事件打上断点:

脚本事件断点

刷新页面,暂停后在控制台粘贴 Hook 代码,然后放开断点继续执行,直到进入 debugger。查看调用堆栈上一层:

调用堆栈上层

再向上一层,就找到了生成算法:

生成算法位置

🔓 响应解密

响应结果在 y 中,请记住这里的密钥,后面会用到。

响应密钥位置

跟进解密函数内部:

解密函数混淆

代码经过混淆,可以放到在线反混淆网站处理:https://obf-io.deobfuscate.io/
将结果稍作整理,交给 AI 辅助分析,即可得到清晰的解密逻辑。

⚠️ 注意点

发送 parse 请求时需要携带 auth 接口返回的 Cookie:

auth cookie

🐍 源码(Python)

完整实现已整合为一个类,依赖 pycryptodome 和 Node.js 环境执行部分加密逻辑:

import requests, time, json, random, string, base64, os, subprocess
from Crypto.Cipher import AES  # pip install pycryptodome


def do_one_cmd(one_cmd):
    result = subprocess.run(one_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding='utf-8')
    return {'returncode': result.returncode, 'stdout': result.stdout.strip() if result.stdout else result.stdout, 'stderr': result.stderr}


def do_one_js_code(js_code_n, js_dir: str = None):
    def get_random_str():
        s = str(int(time.time() * 1000))
        n_str = '0123456789abcdefghijklmnopqrstuvwxyz'
        for i in range(8):
            s += n_str[random.randint(0, len(n_str) - 1)]
        return s

    def do():
        nonlocal js_dir
        file_head = get_random_str()
        if not js_dir:
            now_dir = os.path.dirname(os.path.realpath(__file__))
            js_path = os.path.join(now_dir, f'{file_head}.js')
        else:
            js_path = os.path.join(js_dir, f'{file_head}.js')

        with open(js_path, 'w', encoding='utf-8') as js_file:
            js_file.write(js_code_n)
        result = do_one_cmd(f'node {js_path}')
        os.remove(js_path)
        return result

    return do()


class KuKuCore:
    def __init__(self, dec_key: str = "12345678901234567890123456789013"):
        self.dec_key = dec_key
        self.headers = {
            'Accept': '*/*',
            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
            'Cache-Control': 'no-cache',
            'Connection': 'keep-alive',
            'Content-Type': 'application/json',
            'Origin': 'https://dy.kukutool.com',
            'Pragma': 'no-cache',
            'Referer': 'https://dy.kukutool.com/',
            'Sec-Fetch-Dest': 'empty',
            'Sec-Fetch-Mode': 'cors',
            'Sec-Fetch-Site': 'same-origin',
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0',
            'sec-ch-ua': '"Chromium";v="142", "Microsoft Edge";v="142", "Not_A Brand";v="99"',
            'sec-ch-ua-mobile': '?0',
            'sec-ch-ua-platform': '"Windows"',
        }
        self.session = requests.session()
        self.generate_params_code = r"""function f(e){var t;let a="";for(t of new Uint8Array(e))a+=String.fromCharCode(t);return btoa(a)}async function m(e,t){return e=(new TextEncoder).encode("".concat(e,":").concat(t)),t=await crypto.subtle.digest("SHA-256",e),crypto.subtle.importKey("raw",t,{name:"AES-GCM"},!1,["encrypt"])}async function y(e,t){var a=crypto.getRandomValues(new Uint8Array(12)),n=await m(t.authKey,t.authSeed),n=await crypto.subtle.encrypt({name:"AES-GCM",iv:a},n,(new TextEncoder).encode(JSON.stringify(e)));return{version:3,k_25e532:t.authKey,p_25e532:f(n),r_25e532:1,i_25e532:f(a.buffer)}}async function get_params(e,t,a,n,r){e=await y({requestURL:e,captchaKey:"",captchaInput:"",totalSuccessCount:"0",successCount:"0",firstSuccessDate:"",pagePath:"/",uwx_id:t,isMobile:"false",geoipIp:a},{authKey:n,authSeed:r}),console.log(JSON.stringify(e))}"""
        print('获取ip...', end=' ')
        self.get_geoip()
        print(f'ip -> {self.geoip}', end='\n')
        print('生成uwx...', end=' ')
        self.generate_uwx()
        print(f'uwx -> {self.uwx}', end='\n')
        print('发送uwx... ->', end=' ')
        self.send_uwx()

    def get_geoip(self):
        params = {'minimal': '1'}
        response = self.session.get('https://user.kukutool.com/getip/geoip', params=params, headers=self.headers)
        self.geoip = response.json()['data']['ip']

    def generate_uwx(self):
        millis = int(time.time() * 1000)
        last6 = str(millis)[-6:]
        random_chars = ''.join(random.choices(string.digits + string.ascii_lowercase, k=4))
        random_upper = ''.join(random.choices(string.ascii_uppercase, k=2))
        self.uwx = f"uwx_{last6}{random_chars}{random_upper}"

    def send_uwx(self):
        response = self.session.get(f'https://dy.kukutool.com/account/user/api/scm/content?lang=zh-CN&client_userid={self.uwx}&slots=kuku-home_top_notify,kuku-home_input_link,kuku-home_banner,kuku-home_after_success_alert,kuku-home_immediate_alert', headers=self.headers)
        print(response.text)

    def get_authen_params(self, requestURL: str):
        json_data = {'requestURL': requestURL, 'pagePath': '/', 'mode': 'single'}
        response = self.session.post('https://dy.kukutool.com/api/auth-25e532', headers=self.headers, json=json_data)
        response_json = response.json()
        authKey, authSeed = response_json['k_25e532'], response_json['s_25e532']
        return {'authKey': authKey, 'authSeed': authSeed}

    def generate_parse_params(self, requestURL: str, authKey: str, authSeed: str):
        js_code_ = self.generate_params_code + f'await get_params("{requestURL}", "{self.uwx}", "{self.geoip}", "{authKey}", "{authSeed}");'
        return json.loads(do_one_js_code(js_code_)['stdout'])  # noqa

    def parse_api(self, requestURL: str):
        authen_params = self.get_authen_params(requestURL)
        authKey, authSeed = authen_params['authKey'], authen_params['authSeed']
        print(f'获取到authen_params -> {authKey} {authSeed}')
        json_data = self.generate_parse_params(requestURL, authKey, authSeed)
        print(f'生成json_data -> {json_data}')
        response = self.session.post('https://dy.kukutool.com/api/parse', headers=self.headers, json=json_data)
        response_json = response.json()
        print(response_json)
        # 自行添加处理逻辑
        return response_json

    def dec_response_json_data(self, data_str: str, iv_str: str) -> str:
        def base64CustomDecode(content_str: str) -> str:
            z_map = "ZYXABCDEFGHIJKLMNOPQRSTUVWzyxabcdefghijklmnopqrstuvw9876543210-_"
            a_map = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
            return ''.join([a_map[z_map.index(x)] if x in z_map else x for x in content_str])

        def blockReverse(content_str: str, block_length: int = 8) -> str:
            temp_str = ''
            for value in range(0, len(content_str), block_length):
                temp_str += content_str[value:value + block_length][::-1]
            return temp_str

        def xorString(content_str: str, value: int = 90) -> str:
            return ''.join([chr(ord(x) ^ value) for x in content_str])

        def aesDecrypt(cipher_base64: str, iv: str, key: str) -> str:
            key = key.encode('utf-8')  # noqa
            iv = base64.b64decode(iv)[:16]  # noqa
            ciphertext = base64.urlsafe_b64decode(cipher_base64)

            cipher = AES.new(key, AES.MODE_CBC, iv)  # noqa
            plaintext_padded = cipher.decrypt(ciphertext)
            pad_len = plaintext_padded[-1]
            plaintext = plaintext_padded[:-pad_len]
            return plaintext.decode('utf-8')

        return aesDecrypt(
            base64CustomDecode(blockReverse(xorString(data_str))),
            base64CustomDecode(blockReverse(xorString(iv_str))),
            self.dec_key
        )


if __name__ == '__main__':
    core = KuKuCore()
    core.parse_api('')

✨ 好啦今天就到这叭~ ✨