📢 免责声明
本文所有内容仅供学习和交流使用,请勿商用!!!如出现纷争,与作者无关。
文章内容基于作者在撰写时的理解。文中提到的技术方案、代码示例及配置方法可能因软件版本更新、环境差异或操作失误而失效。
作者不保证文中内容的绝对正确性、完整性和时效性。读者在依照本文进行操作时,应自行评估风险,并对可能产生的数据丢失、系统损坏、服务中断或其他直接或间接损失负责。建议在操作前进行完整备份,并在测试环境中先行验证。如有错误,欢迎指正。
如有侵权,联系删除。
本文所有内容仅供学习和交流使用,请勿商用!!!如出现纷争,与作者无关。
文章内容基于作者在撰写时的理解。文中提到的技术方案、代码示例及配置方法可能因软件版本更新、环境差异或操作失误而失效。
作者不保证文中内容的绝对正确性、完整性和时效性。读者在依照本文进行操作时,应自行评估风险,并对可能产生的数据丢失、系统损坏、服务中断或其他直接或间接损失负责。建议在操作前进行完整备份,并在测试环境中先行验证。如有错误,欢迎指正。
如有侵权,联系删除。
逆向分析 · 某工具站请求参数生成与响应解密
🌐 网站
aHR0cHM6Ly9keS5rdWt1dG9vbC5jb20v
(Base64 编码,解码后可见真实地址)
📡 请求分析
进入页面后打开 DevTools 会被断住,为了方便,直接设置“永不在此处暂停”。
粘贴一个链接开始解析,抓包发现有一个 parse 数据包,响应内容加密,正是我们的目标。
点击“发起程序”查看调用栈:
请求是 fetch,请求体为变量 o,而 o 由 await y 得到。在 await 处打断点,重新发包:
点击调用堆栈的上一层,发现响应结果被存入了 y(截图里标注有误,懒得改了)。
将所有参数打印出来方便后续分析,发现还有 geoipIp、k_25e532、s_25e532、uwx_id 这几个值未知。
接着进入 y 函数内部:
接下来就是扣函数的常规操作了。
继续寻找其他参数:
geoipIp的来源:
k_25e532与s_25e532的来源:
uwx_id发现于localStorage:
因此需要对 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:
🐍 源码(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('')
✨ 好啦今天就到这叭~ ✨
浙公网安备 33010602011771号