干了11年爬虫最近两年明显感觉APP越来越难爬了。从最开始的简单抓包就能用到后来的SSL Pinning、签名算法、设备指纹再到现在的加固壳、虚拟机保护逆向一个APP的成本越来越高。上个月帮一个朋友爬某电商APP的商品数据光脱壳就花了3天逆向签名算法又花了2天结果刚写好爬虫人家APP更新了签名算法全变了白忙活一场。后来我换了个思路既然APP这么难搞那它内嵌的H5页面呢结果发现90%以上的混合APP核心业务逻辑其实都在H5里。而且H5的逆向难度比原生APP低太多了今天就把我这半年踩坑总结出来的H5爬虫新思路分享给大家保证看完就能上手。一、为什么H5成了移动端爬虫的突破口先给大家看一张我总结的原生APP和H5爬虫难度对比图原生APP爬虫脱壳/反编译逆向Java/Kotlin代码分析签名算法处理设备指纹绕过反调试编写爬虫H5爬虫抓包分析接口逆向JS代码还原签名算法编写爬虫看到了吧H5爬虫直接跳过了最耗时的脱壳和原生代码逆向步骤。而且JS代码即使被混淆了也比Java字节码好理解得多。更重要的是更新频率低H5页面更新不需要用户下载APP所以接口和签名算法相对稳定跨平台通用同一个H5页面在Android和iOS上是一样的写一次爬虫就能用调试方便Chrome开发者工具直接就能调试不需要复杂的逆向环境当然H5也不是完全没有反爬但相比原生APP来说真的是小巫见大巫了。二、第一步绕过APP限制抓包H5接口很多人说“我用Charles抓APP的H5页面要么抓不到要么全是乱码”这很正常因为现在的APP基本都做了SSL Pinning。不过别担心我有一套万能的抓包方案99%的APP都能搞定。2.1 基础环境准备首先你需要准备一台电脑Windows/Mac都可以安卓模拟器推荐雷电模拟器9Android 9系统Charles抓包工具Frida动态注入工具为什么用Android 9因为Android 10及以上系统对证书的限制更严格把证书放到系统目录需要Root而Android 9只需要安装用户证书就能用。2.2 SSL Pinning绕过这是最关键的一步。我试过很多方法包括Xposed的JustTrustMe模块但最稳定的还是Frida脚本。给大家分享一个我一直在用的万能SSL Pinning绕过脚本适配99%的安卓APP/* Frida全局SSL Pinning绕过脚本 */Java.perform(function(){// 绕过OkHttp证书锁定try{constCertificatePinnerJava.use(okhttp3.CertificatePinner);CertificatePinner.check.implementationfunction(){};}catch(e){}// 绕过原生URLConnection证书校验try{constHttpsURLConnectionJava.use(javax.net.ssl.HttpsURLConnection);HttpsURLConnection.setDefaultSSLSocketFactory.implementationfunction(){};}catch(e){}// 绕过WebView网页SSL校验try{constWebViewClientJava.use(android.webkit.WebViewClient);WebViewClient.onReceivedSslError.implementationfunction(webview,errorHandler,sslError){errorHandler.proceed();};}catch(e){}// 全局信任所有证书try{constX509TrustManagerJava.use(javax.net.ssl.X509TrustManager);X509TrustManager.checkClientTrusted.implementationfunction(){};X509TrustManager.checkServerTrusted.implementationfunction(){};X509TrustManager.getAcceptedIssuers.implementationfunction(){return[];};}catch(e){}console.log(SSL Pinning绕过成功);});使用方法也很简单在模拟器上安装Frida-server电脑上执行命令frida -U -f com.example.app -l ssl_bypass.js --no-pause打开Charles配置好代理和证书现在你就能看到APP里所有的HTTPS请求了包括H5页面的2.3 特殊情况处理有些APP的WebView会绕过系统代理导致Charles抓不到包。这时候有两种解决方法方法一使用透明代理在路由器上配置透明代理所有流量都经过Charles。这个方法最彻底但需要路由器支持。方法二Hook WebView的请求用Frida Hook WebView的shouldInterceptRequest方法强制所有请求走代理try{constWebViewJava.use(android.webkit.WebView);WebView.loadUrl.overload(java.lang.String).implementationfunction(url){console.log(WebView加载URLurl);returnthis.loadUrl(url);};}catch(e){}三、第二步破解H5接口逆向签名算法抓包之后你会发现大部分H5接口都有一个sign参数。这个参数就是我们需要破解的核心。3.1 定位签名函数定位签名函数有三个技巧我按优先级排序技巧一全局搜索关键词在Chrome开发者工具的Sources面板按CtrlShiftF全局搜索signsignaturemd5(sha1(encrypt(90%的情况你都能直接找到签名生成的地方。技巧二查看请求发起者在Network面板找到你要分析的请求点击Initiator列就能直接跳转到发起请求的JS代码。然后在这行代码打个断点刷新页面就能看到调用栈了。技巧三XHR断点如果上面两个方法都不行就在Sources面板的XHR/fetch Breakpoints里添加断点输入接口的部分URL。当请求发起时调试器会自动断住然后你就能一步步回溯找到签名函数。3.2 常见签名算法分析我分析过上百个H5接口的签名算法发现90%以上都是下面这几种类型一参数排序MD5这是最常见的一种也是最好破解的。functiongenerateSign(params){// 1. 获取所有参数名过滤掉sign本身然后按字母顺序排序constsortedKeysObject.keys(params).filter(kk!sign).sort();// 2. 将参数按keyvalue格式拼接用连接letsignStrsortedKeys.map(k${k}${params[k]}).join();// 3. 在末尾追加密钥这就是签名的关键密钥硬编码在前端signStrkeyWaterCard2024#SecretKey;// 4. 对拼接后的字符串进行MD5哈希得到签名returnmd5(signStr).toLowerCase();}类型二参数拼接HMAC-SHA256比MD5稍微复杂一点但原理一样。functiongenerateSign(params,secretKey){constsortedKeysObject.keys(params).sort();constsignStrsortedKeys.map(k${k}${params[k]}).join();returnCryptoJS.HmacSHA256(signStr,secretKey).toString(CryptoJS.enc.Hex);}类型三时间戳随机数签名这种会在参数里加上timestamp和nonce防止重放攻击。functiongenerateSign(params){params.timestampDate.now().toString();params.nonceMath.random().toString(36).substring(2,15);constsortedKeysObject.keys(params).filter(kk!sign).sort();constsignStrsortedKeys.map(k${k}${params[k]}).join();returnmd5(signStrapp_secret_key).toUpperCase();}3.3 反调试绕过有些H5页面会加反调试最常见的就是无限debugger语句。当你打开开发者工具时页面会一直卡在debugger状态。绕过方法很简单在Sources面板找到debugger语句所在的行右键点击行号选择Never pause here刷新页面debugger就不会再断住了如果是动态生成的debugger语句通过eval执行可以用这个Frida脚本直接禁用所有debuggerJava.perform(function(){constWebViewJava.use(android.webkit.WebView);WebView.evaluateJavascript.implementationfunction(script,callback){if(script.indexOf(debugger)!-1){scriptscript.replace(/debugger;/g,);}returnthis.evaluateJavascript(script,callback);};});四、完整实战案例某电商APP商品列表爬虫光说不练假把式下面我用一个真实的电商APP来演示完整的H5爬虫流程。4.1 抓包分析接口首先用Charles抓包找到商品列表接口GET https://api.example.com/v2/goods/list? category_id1001 page1 page_size20 timestamp1716624000 nonceabcdef123456 sign7a9f3d2b8c6e4a0d1f5b7c9e3a2d8f1b可以看到有timestamp、nonce和sign三个动态参数。4.2 逆向签名算法按照上面的方法全局搜索sign很快找到了签名函数functiongetSign(params){varkeysObject.keys(params).sort();varstr;for(vari0;ikeys.length;i){strkeys[i]params[keys[i]];}strstr.substring(0,str.length-1);strAPP_SECRET_2026;returnmd5(str).toLowerCase();}签名算法很简单参数按键名排序拼接成keyvaluekeyvalue格式末尾加上固定密钥APP_SECRET_2026MD5加密转小写4.3 Python爬虫实现现在我们可以用Python来实现这个签名算法然后编写爬虫importrequestsimporthashlibimporttimeimportrandomdefgenerate_sign(params):# 参数按键名排序sorted_keyssorted(params.keys())# 拼接成keyvaluekeyvalue格式sign_str.join([f{k}{params[k]}forkinsorted_keys])# 末尾加上密钥sign_strAPP_SECRET_2026# MD5加密转小写returnhashlib.md5(sign_str.encode(utf-8)).hexdigest().lower()defget_goods_list(category_id,page1,page_size20):urlhttps://api.example.com/v2/goods/list# 构造参数params{category_id:category_id,page:page,page_size:page_size,timestamp:str(int(time.time())),nonce:.join(random.choices(abcdefghijklmnopqrstuvwxyz0123456789,k12))}# 生成签名params[sign]generate_sign(params)# 发送请求headers{User-Agent:Mozilla/5.0 (Linux; Android 9; SM-G960F Build/PPR1.180610.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/120.0.6099.230 Mobile Safari/537.36,Referer:https://m.example.com/goods/list}responserequests.get(url,paramsparams,headersheaders)returnresponse.json()# 测试if__name____main__:goods_listget_goods_list(1001,page1)print(goods_list)就是这么简单不到50行代码就搞定了。如果是原生APP的话至少要写几百行还得处理各种设备指纹和反调试。五、反反爬进阶策略当然有些H5页面的反爬会比较严格。下面分享几个我常用的进阶技巧5.1 设备指纹伪装现在很多H5页面会收集设备信息生成设备指纹包括屏幕分辨率浏览器版本系统版本Canvas指纹WebGL指纹如果你的爬虫请求的设备指纹都一样很容易被封。解决方法使用Playwright或Puppeteer无头浏览器每次请求都随机生成不同的设备指纹。fromplaywright.sync_apiimportsync_playwrightimportrandomdefget_random_user_agent():user_agents[Mozilla/5.0 (Linux; Android 9; SM-G960F Build/PPR1.180610.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/120.0.6099.230 Mobile Safari/537.36,Mozilla/5.0 (Linux; Android 10; SM-G973F Build/QP1A.190711.020; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/121.0.6167.164 Mobile Safari/537.36,Mozilla/5.0 (Linux; Android 11; SM-G991B Build/RP1A.200720.012; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/122.0.6261.119 Mobile Safari/537.36]returnrandom.choice(user_agents)withsync_playwright()asp:browserp.chromium.launch(headlessTrue)contextbrowser.new_context(user_agentget_random_user_agent(),viewport{width:360,height:640},device_scale_factor2)pagecontext.new_page()# 注入脚本修改navigator.webdriver属性page.add_init_script( Object.defineProperty(navigator, webdriver, { get: () false }); )page.goto(https://m.example.com/goods/list)print(page.content())browser.close()5.2 行为模拟有些H5页面会检测用户行为比如鼠标移动、点击、滚动等。如果你的爬虫只是简单地请求接口很容易被识别。解决方法用Playwright模拟真实用户行为# 模拟滚动页面foriinrange(5):page.evaluate(fwindow.scrollTo(0,{i*200}))page.wait_for_timeout(random.randint(500,1000))# 模拟点击商品page.locator(.goods-item).nth(0).click()page.wait_for_timeout(random.randint(1000,2000))# 返回上一页page.go_back()page.wait_for_timeout(random.randint(500,1000))5.3 代理池与IP轮换IP限制是最常见的反爬手段。解决方法就是使用代理池每次请求都换一个IP。推荐使用住宅代理比数据中心代理更难被检测到。我自己用的是四叶天代理稳定性还不错。proxies{http:http://username:passwordproxy.example.com:8080,https:http://username:passwordproxy.example.com:8080}responserequests.get(url,paramsparams,headersheaders,proxiesproxies)六、总结今天给大家分享了移动端H5爬虫的新思路核心就是绕过APP的SSL Pinning直接抓包H5接口然后逆向JS代码破解签名算法。相比原生APP爬虫H5爬虫有以下优势开发成本低周期短维护简单接口更新频率低跨平台通用一次编写到处运行调试方便不需要复杂的逆向环境当然H5爬虫也不是万能的。有些APP的核心数据还是通过原生接口返回的这时候就不得不逆向原生APP了。但对于大部分场景来说H5爬虫已经足够用了。最后提醒大家爬虫技术是一把双刃剑一定要在法律允许的范围内使用。只爬取公开的数据不要侵犯他人的隐私和商业利益。