虎牙登录: 接入hydevice高信任设备指纹与App场景验证支持
- 新增 fingerprint/: node仿真Android WebView运行官方hydevice.js(含WASM加密), 产出与真机同构的高信任sdid - 新增 device_fingerprint.py: py编排node runner获取sdid, 失败自动降级旧无指纹流程 - login.py: prepare_device优先高信任指纹; 增加App场景常量(appId=5008, appSign=md5(appId+verCode+appKey)[:8], 逆向自APK UdbNetHelper) - solver: 支持appId/page_url/use_touch_events参数化(App模式); get3使用csid会话id; 识别qr_auth扫码(冷却)与dx_auth短信策略并抛专用异常
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
// Android WebView (M2102J2SC, 小米, Android 11) 环境仿真
|
||||
function makeEnv() {
|
||||
const W = globalThis;
|
||||
W.screen = {width:393, height:851, availWidth:393, availHeight:851,
|
||||
colorDepth:24, pixelDepth:24, availLeft:0, availTop:0, orientation:{type:'portrait-primary', angle:0}};
|
||||
W.devicePixelRatio = 2.75;
|
||||
W.innerWidth = 393; W.innerHeight = 737; W.outerWidth = 393; W.outerHeight = 851;
|
||||
W.location = {href:'https://aq.huya.com/p/safe_auth/pt_auth.html', protocol:'https:', host:'aq.huya.com',
|
||||
hostname:'aq.huya.com', port:'', pathname:'/p/safe_auth/pt_auth.html', search:'', hash:'',
|
||||
origin:'https://aq.huya.com', toString(){return this.href}};
|
||||
W.history = {length:2, state:null, pushState(){}, replaceState(){}, back(){}, go(){}};
|
||||
const navProps = {
|
||||
userAgent:'Mozilla/5.0 (Linux; Android 11; M2102J2SC Build/RKQ1.200826.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/149.0.7827.159 Mobile Safari/537.36 huya adr/13.4.22/xiaomi/30',
|
||||
appName:'Netscape', appVersion:'5.0 (Linux; Android 11; M2102J2SC Build/RKQ1.200826.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/149.0.7827.159 Mobile Safari/537.36',
|
||||
platform:'Linux armv8l', language:'zh-CN', languages:['zh-CN','zh','en'],
|
||||
cookieEnabled:true, doNotTrack:null, maxTouchPoints:5, hardwareConcurrency:8,
|
||||
deviceMemory:8, vendor:'Google Inc.', productSub:'20030107', webdriver:false,
|
||||
pdfViewerEnabled:false, javaEnabled(){return false},
|
||||
getBattery(){return Promise.resolve({charging:true, level:0.9})},
|
||||
userAgentData: {brands:[{brand:'Chromium',version:'149'},{brand:'Google Chrome',version:'149'},{brand:'Not?A_Brand',version:'24'}],
|
||||
mobile:true, platform:'Android',
|
||||
getHighEntropyValues(){return Promise.resolve({architecture:'arm', bitness:'64', model:'M2102J2SC', platformVersion:'11.0.0', uaFullVersion:'149.0.7827.159'})}},
|
||||
connection: {effectiveType:'4g', downlink:10, rtt:50, saveData:false},
|
||||
plugins: {length:0, item(){return null}, namedItem(){return null}},
|
||||
mimeTypes: {length:0},
|
||||
};
|
||||
for (const [k,v] of Object.entries(navProps)) {
|
||||
try { Object.defineProperty(W.navigator, k, {value:v, configurable:true}); } catch(e){ try{W.navigator[k]=v}catch(_){} }
|
||||
}
|
||||
// canvas 2d/webgl 最小仿真
|
||||
const glConsts = {RENDERER:0x1F01, VENDOR:0x1F00, VERSION:0x1F02, SHADING_LANGUAGE_VERSION:0x8B8C,
|
||||
MAX_TEXTURE_SIZE:0x0D33, MAX_RENDERBUFFER_SIZE:0x84E8, MAX_VIEWPORT_DIMS:0x0D3A};
|
||||
function fakeGL(){
|
||||
return new Proxy({
|
||||
getExtension(){ return {UNMASKED_VENDOR_WEBGL:0x9245, UNMASKED_RENDERER_WEBGL:0x9246}; },
|
||||
getParameter(p){
|
||||
if (p===0x9245) return 'Qualcomm';
|
||||
if (p===0x9246) return 'Adreno (TM) 650';
|
||||
if (p===glConsts.MAX_TEXTURE_SIZE) return 16384;
|
||||
if (p===glConsts.MAX_VIEWPORT_DIMS) return new Int32Array([16384,16384]);
|
||||
return 'WebKit WebGL';
|
||||
},
|
||||
getContextAttributes(){return {alpha:true, antialias:true, depth:true};},
|
||||
createBuffer(){return {}}, bindBuffer(){}, bufferData(){}, createProgram(){return {}},
|
||||
attachShader(){}, linkProgram(){}, useProgram(){}, createShader(){return {}},
|
||||
shaderSource(){}, compileShader(){}, getShaderParameter(){return true},
|
||||
getProgramParameter(){return true}, getSupportedExtensions(){return ['WEBGL_debug_renderer_info']},
|
||||
readPixels(x,y,w,h,t,f,buf){ if(buf) buf.fill(128); },
|
||||
}, { get(t,p){ if(p in t) return t[p]; if(typeof p==='string' && p.startsWith('on')) return null; return undefined; } });
|
||||
}
|
||||
const ctx2d = new Proxy({
|
||||
fillRect(){}, clearRect(){}, getImageData(x,y,w,h){return {data:new Uint8ClampedArray(w*h*4).fill(128), width:w, height:h}},
|
||||
putImageData(){}, createImageData(w,h){return {data:new Uint8ClampedArray(w*h*4), width:w, height:h}},
|
||||
setTransform(){}, transform(){}, translate(){}, scale(){}, rotate(){}, save(){}, restore(){},
|
||||
beginPath(){}, closePath(){}, moveTo(){}, lineTo(){}, stroke(){}, fill(){}, arc(){}, rect(){}, clip(){},
|
||||
createLinearGradient(){return {addColorStop(){}}}, createRadialGradient(){return {addColorStop(){}}},
|
||||
fillText(){}, strokeText(){}, measureText(t){return {width:String(t).length*7}},
|
||||
isPointInPath(){return false}, drawImage(){}, arcTo(){}, bezierCurveTo(){}, quadraticCurveTo(){},
|
||||
createPattern(){return {}}, setLineDash(){}, getLineDash(){return []},
|
||||
}, { get(t,p){ if(p in t) return t[p]; if(p==='canvas') return {}; return undefined; } });
|
||||
const canvasProto = {
|
||||
getContext(type){
|
||||
if (type === '2d') return ctx2d;
|
||||
if (type === 'webgl' || type === 'webgl2' || type === 'experimental-webgl') return fakeGL();
|
||||
return null;
|
||||
},
|
||||
toDataURL(){ return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; },
|
||||
toBlob(cb){ cb && cb(null); },
|
||||
addEventListener(){}, removeEventListener(){}, setAttribute(){}, getBoundingClientRect(){return {left:0,top:0,width:300,height:150}},
|
||||
style:{}, width:300, height:150,
|
||||
};
|
||||
W.document = {
|
||||
createElement(tag){ const el = Object.assign({}, canvasProto, {tagName:String(tag||'div').toUpperCase(), style:{}, children:[],
|
||||
setAttribute(){}, getAttribute(){return null}, appendChild(c){this.children.push(c)}, removeChild(){}, addEventListener(){}, removeEventListener(){},
|
||||
classList:{add(){},remove(){},contains(){return false}}, attachShadow(){return {appendChild(){}}}, getBoundingClientRect(){return {left:0,top:0,right:100,bottom:30,width:100,height:30}} });
|
||||
return el; },
|
||||
createEvent(){return {initEvent(){}}},
|
||||
createElementNS(ns, tag){ return this.createElement(tag); },
|
||||
createTextNode(t){return {textContent:t}},
|
||||
getElementById(){return null}, querySelector(){return null}, querySelectorAll(){return []},
|
||||
getElementsByTagName(t){ if(String(t).toLowerCase()==='head') return [this.head]; return []; },
|
||||
documentElement:{setAttribute(){}, getAttribute(){return null}, style:{}},
|
||||
head:{appendChild(){}}, body:{appendChild(){}},
|
||||
cookie:'', referrer:'', title:'pt_auth', domain:'aq.huya.com', readyState:'complete', visibilityState:'visible',
|
||||
addEventListener(){}, removeEventListener(){},
|
||||
};
|
||||
W.localStorage = (()=>{const m={}; return {getItem:k=>m[k]??null,setItem:(k,v)=>m[k]=String(v),removeItem:k=>delete m[k],clear:()=>{},key:i=>Object.keys(m)[i]??null,get length(){return Object.keys(m).length}}})();
|
||||
W.sessionStorage = (()=>{const m={}; return {getItem:k=>m[k]??null,setItem:(k,v)=>m[k]=String(v),removeItem:k=>delete m[k],clear:()=>{},key:i=>null,get length(){return 0}}})();
|
||||
W.indexedDB = {open(){return {onsuccess:null,onerror:null,onupgradeneeded:null,result:{objectStoreNames:{contains(){return false}},createObjectStore(){return {createIndex(){}}}},set onsuccess(f){setTimeout(()=>{},0)}}}};
|
||||
W.performance = W.performance || {now:()=>Date.now(), timeOrigin:Date.now(), timing:{navigationStart:Date.now()}, getEntriesByType(){return []}, mark(){}, measure(){}};
|
||||
W.AudioContext = class { constructor(){this.sampleRate=48000; this.destination={}} createOscillator(){return {type:'',frequency:{value:0,setValueAtTime(){}},connect(){},start(){},stop(){}}} createDynamicsCompressor(){return {threshold:{value:0},knee:{value:0},ratio:{value:0},attack:{value:0},release:{value:0},connect(){}}} createAnalyser(){return {frequencyBinCount:1024,getFloatFrequencyData(a){a&&a.fill(-100)}}} createGain(){return {gain:{value:1,setValueAtTime(){}},connect(){}}} createScriptProcessor(){return {connect(){},onaudioprocess:null}} decodeAudioData(){return Promise.resolve({getChannelData(){return new Float32Array(5000)}})} resume(){return Promise.resolve()} close(){return Promise.resolve()} };
|
||||
W.webkitAudioContext = W.AudioContext;
|
||||
W.OfflineAudioContext = W.AudioContext;
|
||||
W.WebGLRenderingContext = {RENDERER:glConsts.RENDERER, VENDOR:glConsts.VENDOR, VERSION:glConsts.VERSION, SHADING_LANGUAGE_VERSION:glConsts.SHADING_LANGUAGE_VERSION, MAX_TEXTURE_SIZE:glConsts.MAX_TEXTURE_SIZE};
|
||||
W.RTCPeerConnection = class { createDataChannel(){return {}} createOffer(){return Promise.resolve({})} setLocalDescription(){return Promise.resolve()} close(){} };
|
||||
W.webkitRTCPeerConnection = W.RTCPeerConnection;
|
||||
W.Notification = {permission:'default'};
|
||||
W.permissions = {query(){return Promise.resolve({state:'prompt'})}};
|
||||
W.matchMedia = () => ({matches:false, media:'', addListener(){}, removeListener(){}, addEventListener(){}, removeEventListener(){}});
|
||||
W.getComputedStyle = () => new Proxy({getPropertyValue:()=>''}, {get(t,p){ if(p in t) return t[p]; return ()=>''; }});
|
||||
W.requestAnimationFrame = cb => setTimeout(()=>cb(Date.now()), 16);
|
||||
W.cancelAnimationFrame = clearTimeout;
|
||||
W.crypto = require('crypto').webcrypto;
|
||||
W.TextEncoder = require('util').TextEncoder;
|
||||
W.TextDecoder = require('util').TextDecoder;
|
||||
}
|
||||
module.exports = { makeEnv };
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,80 @@
|
||||
// hydevice 指纹运行器: 仿真 Android WebView 环境 -> 真实请求 df/token + df/collect -> 输出 sdid
|
||||
process.on('uncaughtException', e => { console.error('[uncaught]', String(e && e.message).slice(0, 120)); });
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const https = require('https');
|
||||
|
||||
globalThis.window = globalThis;
|
||||
require(path.join(__dirname, 'env.js')).makeEnv();
|
||||
|
||||
// ---- localStorage 持久化(设备稳定性) ----
|
||||
const stateDir = process.argv[2] || '.';
|
||||
try { fs.mkdirSync(stateDir, {recursive: true}); } catch (e) {}
|
||||
const lsFile = path.join(stateDir, 'localstorage.json');
|
||||
let _ls = {};
|
||||
try { _ls = JSON.parse(fs.readFileSync(lsFile, 'utf8')); } catch (e) {}
|
||||
globalThis.localStorage = {
|
||||
getItem: k => (k in _ls ? _ls[k] : null),
|
||||
setItem: (k, v) => { _ls[k] = String(v); try { fs.writeFileSync(lsFile, JSON.stringify(_ls)); } catch (e) {} },
|
||||
removeItem: k => { delete _ls[k]; try { fs.writeFileSync(lsFile, JSON.stringify(_ls)); } catch (e) {} },
|
||||
clear: () => { _ls = {}; try { fs.writeFileSync(lsFile, JSON.stringify(_ls)); } catch (e) {} },
|
||||
key: i => Object.keys(_ls)[i] || null,
|
||||
get length() { return Object.keys(_ls).length; },
|
||||
};
|
||||
globalThis.sessionStorage = {getItem: () => null, setItem(){}, removeItem(){}, clear(){}, key: () => null, get length(){ return 0; }};
|
||||
|
||||
// ---- 真实 XHR ----
|
||||
function xhr(url, body, headers) {
|
||||
return new Promise((resolve) => {
|
||||
const u = new URL(url);
|
||||
const data = body == null ? null : Buffer.from(String(body), 'utf8');
|
||||
const req = https.request({
|
||||
hostname: u.hostname, port: 443, path: u.pathname + u.search, method: 'POST',
|
||||
headers: Object.assign({
|
||||
'accept': 'application/json, text/javascript, */*; q=0.01',
|
||||
'content-type': 'application/json',
|
||||
'origin': 'https://aq.huya.com',
|
||||
'referer': 'https://aq.huya.com/',
|
||||
'user-agent': globalThis.navigator.userAgent,
|
||||
'accept-language': 'zh-CN,zh;q=0.9',
|
||||
}, headers || {}),
|
||||
timeout: 15000,
|
||||
}, res => {
|
||||
let chunks = [];
|
||||
res.on('data', c => chunks.push(c));
|
||||
res.on('end', () => resolve({status: res.statusCode, text: Buffer.concat(chunks).toString('utf8')}));
|
||||
});
|
||||
req.on('timeout', () => { req.destroy(); resolve({status: 0, text: ''}); });
|
||||
req.on('error', () => resolve({status: 0, text: ''}));
|
||||
if (data) req.write(data);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
globalThis.XMLHttpRequest = class {
|
||||
open(m, u){ this._m = m; this._u = u; this._h = {}; }
|
||||
setRequestHeader(k, v){ this._h[k] = v; }
|
||||
getAllResponseHeaders(){ return ''; }
|
||||
addEventListener(t, f){ if (t === 'load' || t === 'readystatechange') this._on = f; }
|
||||
send(d){
|
||||
xhr(this._u, d, this._h).then(r => {
|
||||
this.status = r.status; this.readyState = 4;
|
||||
this.responseText = r.text; this.response = r.text;
|
||||
const done = () => {
|
||||
if (String(this._u).includes('/collect')) {
|
||||
try {
|
||||
const j = JSON.parse(r.text);
|
||||
if (j && j.data && j.data.sdid) { console.log('__SDID__' + j.data.sdid); setTimeout(() => process.exit(0), 200); }
|
||||
} catch (e) {}
|
||||
}
|
||||
try { this.onreadystatechange && this.onreadystatechange(); } catch (e) {}
|
||||
try { this._on && this._on(); } catch (e) {}
|
||||
try { this.onload && this.onload(); } catch (e) {}
|
||||
};
|
||||
setTimeout(done, 30);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
eval(fs.readFileSync(path.join(__dirname, 'hydevice-prod-1.2.45-min.js'), 'utf8'));
|
||||
try { globalThis.initFingerprint(process.argv[3] || '5008'); } catch (e) { console.error('[init]', e.message.slice(0, 120)); }
|
||||
setTimeout(() => { console.error('__TIMEOUT__'); process.exit(2); }, 25000);
|
||||
Reference in New Issue
Block a user