一、简介
爬虫是一种自动化程序技术,通过模拟用户行为或直接访问接口,从网页、App等平台中抓取公开或非公开数据,广泛应用于数据分析、商业智能等领域。其核心是通过代码实现高效、批量的信息采集,并可能结合代理IP、逆向破解、真机群控等技术绕过反爬措施。
当前的爬虫主要有两大分类,一是Web端爬虫,另一个是App端爬虫。Web 端爬虫主要针对网页数据进行抓取,而 App 端爬虫则侧重于从各类移动应用程序中获取数据。不同类型的爬虫面临着不同的反爬机制,需要运用不同的技术手段来突破,爬虫技术与反爬虫在相互对抗中,共同演进成长。接下来我们将从Web端与App端的反爬虫技术突破中了解相应的爬虫技术。
二、Web端爬虫
2.1、访问频率限制
许多网站会对同一 IP 地址的访问频率进行限制,以防止爬虫过度请求导致服务器压力过大。为了突破访问频次限制,可以采用以下方法:
1、设置合理的请求间隔
2、使用代理IP
- 短效代理:量大、代理时效短(1-5分钟),适用于短时间内大规模采集数据,需要频繁切换IP的场景。
- 长效代理:代理时效长(1天以上),纯净度高,适用于定期、低频且对代理稳定性有一定要求的采集场景。
- 隧道代理:即买即用,代理时效短(1-5分钟),适用于爬虫需求快速增长的场景。
- 独享代理:高速稳定,代理时效长(1天以内),适用于敏感数据采集,对采集数据的质量和安全性有一定要求的场景。
2.2、信息校验反爬
2.2.1、UserAgent校验
网站可以通过校验UserAgent来判断请求是否来自合法的浏览器。为了突破UserAgent校验的反爬虫技术,可以在请求头中设置不同的UserAgent。
2.2.2、Cookie校验
网站可以通过校验Cookie来判断请求是否来自合法的用户。为了突破Cookie校验的反爬虫技术,可以在请求头中设置合法的Cookie。如何获取合法的Cookie有以下几种方式:
1、直接提取浏览器中的合法Cookie,例如直接提取下图浏览器中的Cookie在爬虫程序中使用,一般Cookie会在一定时间后失效,这时需要重新从浏览器中提取新的Cookie进行替换。适合临时采集数据、Cookie有效期超长或在短时间内无法自主生产Cookie的场景。
2、模拟用户请求行为,获取从服务端返回的Cookie。如下图__ac_nonce是由服务端返回的Cookie字段。
3、通过逆向技术还原Cookie生成逻辑,如下图__ac_signature则是由Js代码生成的Cookie字段,需要通过逆向技术将其还原出来。
2.2.3、签名校验
签名校验是通过对请求参数进行加密和签名,服务器在接收到请求后会校验签名的合法性。为了突破签名校验反爬虫技术,需要通过逆向技术分析签名的生成算法,并在爬虫代码中实现相同的签名生成逻辑。如下图a_bogus字段则是对请求Url以及UserAgent的加密签名字段,服务端通过验证该字段的合法性,才确认当前请求是否已被篡改。
2.3、动态渲染反爬
网站使用JavaScript在浏览器中动态生成内容,传统的爬虫无法直接获取这些动态生成的内容。为了突破动态渲染反爬虫技术,需要使用无头浏览器访问网站采集数据。常见的无头浏览器有Puppeteer、Selenium、Playwright等。目前使用JavaScript直接渲染(非Ajax)出数据的案例很少了。
2.4、文本混淆反爬
2.4.1、图片伪装反爬
网站使用图片替换原来的内容,从而让爬虫程序无法直接从Html页面中直接提取数据。解决方案也很简单,一般爬虫程序结合Ocr图片识别即可。
2.4.2、CSS偏移反爬
网站通过CSS样式来隐藏或混淆真实数据位置,从而增加爬虫采集数据的难度。其原理是通过CSS样式将页面上元素的实际显示位置与Dom文档中的位置进行偏移,使得爬虫获取的数据与页面显示的数据不同。一般可通过分析CSS样式以及HTML页面结构,识别数据元算的偏移规律,将Dom文档中的乱序元素还原为正确的顺序来进行绕过。
2.4.3、字体反爬
网站通过使用自定义字体文件来显示网页上的关键信息(如评分、价格等),以此增加爬虫程序采集数据的难度。具体而言,网站会将字符与自定义字体文件中的字形进行映射,使网页在浏览器中显示正常,但爬虫直接采集到的字符对应的编码,从而与实际显示的内容不一致。破解思路如下,首先识别并下载字体文件,再使用fontTools工具来解析字体文件,识别出字体文件中字符的编码和对应的字形,建立字符编码与实显字符的映射关系,最后将爬虫程序采集的字符编码替换成实显字符即可。有的网站同时还会应用多套字体,这时需要建立多套字体的字符编码与实显字符的映射关系。
字符编码、实显字符、字形
2.5、JS混淆反爬
2.5.1、JS混淆介绍
Js混淆完全是在Js代码上面进行的处理,目的就是去除代码中尽可能多的有意义的信息,使得Js代码变得难以阅读和分析。网站可以将核心的加解密函数,通过JS混淆手段保护起来。Js混淆技术(通过javascript-obfuscator库)主要有以下几种:
1、变量和函数名混淆:将原本具有明确含义的变量名和函数名替换为无意义的随机字符串,降低代码可读性。
// 混淆前
var test = 'hello';
// 混淆后
var _0x7deb = 'hello';
2、字符串混淆:对代码中的关键字符串(如 URL、数据接口地址等)进行加密处理,在运行时再进行解密。避免爬虫通过全局搜索字符串的方式定位到代码位置。
// 混淆前
var test = 'hello';
// 混淆后
var _0x9d2b = ['\x68\x65\x6c\x6c\x6f'];
var _0xb7de = function (_0x4c7513) {
_0x4c7513 = _0x4c7513 - 0x0;
var _0x96ade5 = _0x9d2b[_0x4c7513];
return _0x96ade5;
};
var test = _0xb7de('0x0');
3、控制流混淆:又称控制流扁平化,通过将原有代码的执行流程进行扁平化混淆,使得代码逻辑变的混乱,爬虫开发者无法通过静态分析代码直观的判断哪些逻辑先执行哪些后执行,必须要通过动态运行才能记录代码执行顺序,从而加重了分析的工作。
// 混淆前
function modexp(y, x, w, n) {
var R, L;
var k = 0;
var s = 1;
while(k < w) {
if (x[k] == 1) {
R = (s * y) % n;
}
else {
R = s;
}
s = R * R % n;
L = R;
k++;
}
return L;
}
// 混淆后
function modexp(y, x, w, n) {
var R, L, s, k;
var next = 0;
for(;;) {
switch(next) {
case 0: k = 0; s = 1; next = 1; break;
case 1: if (k < w) next = 2; else next = 6; break;
case 2: if (x[k] == 1) next = 3; else next = 4; break;
case 3: R = (s * y) % n; next = 5; break;
case 4: R = s; next = 5; break;
case 5: s = R * R % n; L = R; k++; next = 1; break;
case 6: return L;
}
}
}
4、僵尸代码注入:插入无意义的代码片段等。使代码看起来及其混乱。
// 混淆前
(function(){
if (true) {
var foo = function () {
console.log('abc');
};
var bar = function () {
console.log('def');
};
var baz = function () {
console.log('ghi');
};
var bark = function () {
console.log('jkl');
};
var hawk = function () {
console.log('mno');
};
foo();
bar();
baz();
bark();
hawk();
}
})();
// 混淆后
var _0x6f5a = [
'abc',
'def',
'caela',
'hmexe',
'ghi',
'aaeem',
'maxex',
'mno',
'jkl',
'ladel',
'xchem',
'axdci',
'acaeh',
'log'
];
(function (_0x22c909, _0x4b3429) {
var _0x1d4bab = function (_0x2e4228) {
while (--_0x2e4228) {
_0x22c909['push'](_0x22c909['shift']());
}
};
_0x1d4bab(++_0x4b3429);
}(_0x6f5a, 0x13f));
var _0x2386 = function (_0x5db522, _0x143eaa) {
_0x5db522 = _0x5db522 - 0x0;
var _0x50b579 = _0x6f5a[_0x5db522];
return _0x50b579;
};
(function () {
if (!![]) {
var _0x38d12d = function () {
if (_0x2386('0x0') !== _0x2386('0x1')) {
console[_0x2386('0x2')](_0x2386('0x3'));
} else {
console[_0x2386('0x2')](_0x2386('0x4'));
}
};
var _0x128337 = function () {
if (_0x2386('0x5') !== _0x2386('0x6')) {
console[_0x2386('0x2')](_0x2386('0x4'));
} else {
console[_0x2386('0x2')](_0x2386('0x7'));
}
};
var _0x55d92e = function () {
if (_0x2386('0x8') !== _0x2386('0x8')) {
console[_0x2386('0x2')](_0x2386('0x3'));
} else {
console[_0x2386('0x2')](_0x2386('0x7'));
}
};
var _0x3402dc = function () {
if (_0x2386('0x9') !== _0x2386('0x9')) {
console[_0x2386('0x2')](_0x2386('0xa'));
} else {
console[_0x2386('0x2')](_0x2386('0xb'));
}
};
var _0x28cfaa = function () {
if (_0x2386('0xc') === _0x2386('0xd')) {
console[_0x2386('0x2')](_0x2386('0xb'));
} else {
console[_0x2386('0x2')](_0x2386('0xa'));
}
};
_0x38d12d();
_0x128337();
_0x55d92e();
_0x3402dc();
_0x28cfaa();
}
}());
5、调试保护:通过在代码多个位置加入 debugger 关键字,或者定义某个逻辑来反复执行debugger,就不断进入断点调试模式,从而干扰爬虫开发人员的正常调试流程。
var box = document.getElementById('box')
function dbg() {
debugger;
}
setInterval(dbg,100);
对于混淆的Js代码,爬虫开发者一般有扣代码补环境及纯算还原两种方法破解混淆其中的加解密函数。
2.5.2、扣代码、补环境
通过调试分析,定位加解密函数的位置,将混淆的Js代码从网页中提取出来,然后在Nodejs环境中执行,不过在执行之前,需要将Nodejs环境与真实浏览器之间的差异环境进行补齐,从而能够正确运行我们从混淆的Js代码中提取的加解密函数,即其运行结果与浏览器运行结果一致。其中浏览器环境与Nodejs环境之间的差异如下图所示:
一般常用的补环境方式有以下两种:
1、使用Proxy来监测浏览器环境API的使用,辅助补齐浏览器环境。
Proxy是ES6提供的代理器,用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。 它可以代理任何类型的对象,包括原生数组,函数,甚至另一个代理。
例如我们代理一个navigator对象,并拦截其相关的操作,伪代码如下:
// 定义代理拦截函数
var handler = {
set:funcA,
get:funcB,
deleteProperty:funcC,
has:funcD,
// 等等
};
// 代理navigator对象
navigator = new Proxy(navigator,handler);
// 拦截并触发get:funcB函数
navigator.userAgent
// 拦截并触发set:funcA函数
navigator.userAgent = "xx"
// 拦截并触发deleteProperty:funcC函数
delete navigator
// 拦截并触发has:funcD函数
"userAgent" in navigator
// 等等其他操作
以下是通过Proxy为监测一些常见的浏览器环境:
function get_enviroment(proxyObjs) {
for (let i = 0; i < proxyObjs.length; i++) {
const handler = `{
get: function(target, property, receiver) {
console.log("方法:", "get ", "对象:", "${proxyObjs[i]}", " 属性:", property, " 属性类型:", typeof property, ", 属性值:", target[property], ", 属性值类型:", typeof target[property]);
return target[property];
},
set: function(target, property, value, receiver) {
console.log("方法:", "set ", "对象:", "${proxyObjs[i]}", " 属性:", property, " 属性类型:", typeof property, ", 属性值:", value, ", 属性值类型:", typeof target[property]);
return Reflect.set(...arguments);
}
}`;
eval(`try {
${proxyObjs[i]};
${proxyObjs[i]} = new Proxy(${proxyObjs[i]}, ${handler});
} catch (e) {
${proxyObjs[i]} = {};
${proxyObjs[i]} = new Proxy(${proxyObjs[i]}, ${handler});
}`);
}
}
proxy_array = ['window', 'document', 'location', 'navigator', 'history', 'screen', 'target', 'performance', 'HTMLElement', 'sessionStorage', 'process', 'MSPluginsCollection', 'PluginArray'];
get_enviroment(proxy_array)
2、使用现有的补环境框架补齐浏览器环境。
常见补环境框架如下,感兴趣的同学可去了解下:
jsdom:https://github.com/jsdom/jsdom
v_jstools:https://github.com/cilame/v_jstools
sdenv:https://github.com/pysunday/sdenv
2.5.3、纯算还原
通过在代码中设置断点,逐步执行代码,观察变量的值和函数的执行过程,分析并还原加解密函数的运算逻辑。如果遇到更加复杂的Js代码混淆,需要在数据运算处打印日志,分析日志流中相关变量值变化,进而分析并还原出加解密函数的运算逻辑。
纯算还原还是需要一定的学习能力及经验积累的,感兴趣的话可以到网上搜索一些相关的文章跟着学习下,比如:
1、iqiyi的cmd5x算法还原
2、youtube的sig算法还原
3、字节系的a_bogus、x_bogus、_signature算法还原
4、xhs的x-s、x-s-common算法还原
5、等等
此类的文章,都比较有学习价值。
2.6、JA3指纹反爬
简单来说,JA3 是一种从 SSL/TLS Client Hello 数据包中提取字段并生成指纹用以识别特定客户端的技术,它不会随着客户端更换IP地址或者UserAgent而改变。JA3 指纹从一开始就说明客户端应用程序是否存在恶意。
JA3 在 SSL/TLS 握手期间解析客户端发送的 Client Hello 数据包,收集以下字段的十进制字节值:TLS Version、Accepted Ciphers、List of Extensions、Elliptic Curves和Elliptic Curve Formats。然后,它将这些值串联起来,使用“,”来分隔各个字段,同时使用“-”来分隔各个字段中的值。最后,计算这些字符串的md5哈希值,即得到易于使用和共享的长度为32字符的指纹。
简答来说,网站通过JA3指纹来识别恶意请求,可轻易拦截常规的爬虫程序,原因就是这些爬虫程序所依赖的网络客户端的JA3指纹和真实浏览器的JA3指纹不同。所以需要一个可以完美模拟真实浏览器JA3指纹的工具进行数据爬取。
为了完美模拟浏览器JA3指纹,国外大佬给curl工具打了一些patch,把相应组件全部都换成与浏览器相同的组件,连版本都保持一致,这样就得到了和浏览器完全一样的JA3指纹,这个库是curl-impersonate,相应的python工具为curl_cffi。
如下图所示,分别使用scraper、requests、curl_cffi工具请求https://toppsta.com网站,只有curl_cffi能够正常返回数据,scraper、requests均被拦截。
2.7、云防护反爬
说到云防护反爬,对爬虫开发来说最著名的莫过于CloudFlare五秒盾了。coudflare五秒盾(也称为5秒盾或托管质询)是Cloudflare提供的一种安全机制,主要用于防御恶意爬虫和机器人对网站的攻击。它通过检测用户的请求流量和行为模式来识别并阻止恶意访问。当一个用户访问网站时,五秒盾会评估其行为模式、IP地址、用户代理等信息,以确定其是否为正常用户。
五秒盾的工作原理包括使用JavaScript代码执行一系列检测,判断请求是否来自真实用户。如果JavaScript代码在5秒内运行完成,说明这个请求来自真实用户,否则会被拦截。此外,五秒盾还可能通过IP封禁、人机验证、JavaScript挑战等方式来进一步识别和拦截爬虫。
爬虫尝试以下方案绕过CloudFlare五秒盾:
1、模拟真实浏览器JA3指纹
使用2.6小节提到的curl_cffi工具模拟真实浏览器的JA3指纹,可尝试绕过免费版的CloudFlare五秒盾。
2、搭建FlareSolverr代理服务
对于付费版的Cloudflare五秒盾,curl_cffi也爬不到数据,毕竟curl_cffi无法真正的模拟浏览器处理Cloudflare的JavaScript挑战,更不用说接下来的人机验证了。
如下图所示,分别使用scraper、requests、curl_cffi工具请求https://rateyourmusic.com/网站,全部被Cloudflare拦截。
对于付费版的Cloudflare五秒盾,可尝试通过Docker运行一个Flaresolverr代理服务的容器绕过Cloudflare五秒盾。启动命令:
docker run -d \
--name=flaresolverr \
-p 8191:8191 \
-e LOG_LEVEL=info \
--restart unless-stopped \
ghcr.io/flaresolverr/flaresolverr:latest
为什么FlareSolverr能够绕过付费版的CloudFlare五秒盾?通过阅读项目源代码,总结了一下FlareSolverr这个项目的亮点,主要体现在以下几个方面:
- 动态解析和执行JavaScript:FlareSolverr能够解析和执行Cloudflare的JS代码,这样就可以模拟浏览器行为并执行页面中的JavaScript,以获取真实的网页内容。
- 自动化处理验证页面:它在容器内部运行一个Webdriver的服务,用于自动处理Cloudflare验证页面。这种自动化处理方式相对于手动模拟浏览器操作更加高效和方便,减少了人工干预的需要。
- 缓存和复用验证结果:FlareSolverr具有缓存机制,可以将已经解析和验证过的网站结果缓存起来,并在后续的请求中进行复用。这样可以减少重复的验证过程,提高访问速度和效率。
- 集成度和依赖性:FlareSolverr是一个专门为解决Cloudflare绕过而设计的工具,它可以作为独立的服务部署,对外提供API接口。
- 环境隔离和资源消耗:FlareSolverr使用了容器化技术,在独立的环境中执行验证操作,避免了对本地环境的影响。
总体而言的话,FlareSolverr在处理 Cloudflare绕过上面提供了一种更加集成化、自动化和高效的解决方案,相对于手动模拟浏览器操作或使用Selenium等方式,可以节省开发和维护的成本,并提供更好的性能和可靠性。
3、使用DrissionPage网页自动化技术
对于付费版的Cloudflare五秒盾提供另一个解决方案,即使用DrissionPage网页自动化技术。DrissionPage是一个基于python的网页自动化工具。它既能控制浏览器,也能收发数据包,还能把两者合而为一。DrissionPage具有较强的反检测机制,能够在不需要进行任何反检测操作的情况下,绕过国内外绝大多数网站的自动化检测。这些特点使得DrissionPage在爬虫和自动化任务中非常强大和方便。
如下代码使用DrissionPage绕过https://rateyourmusic.com/网站的Cloudflare五秒盾。
import time
import random
from DrissionPage import ChromiumPage, ChromiumOptions
URL = 'https://rateyourmusic.com/genre/{genre}/{page}.d/'
def browse_rym(genre):
# 本程序中总页数设定为250,实际会根据具体情况设定
crwal,pages = 0, 250
print("genre=%s, pages=%s" % (genre, pages))
# 每爬40页重新启动一次浏览器,同一浏览器环境下访问次数太多,会被拦截
for page40 in range(1, pages, 40):
# 启动浏览器,auto_port参数意味着每次启动都是一个全新的浏览器
browser = ChromiumPage(ChromiumOptions().auto_port())
try:
# 自动化翻页采集数据
for page in range(page40, page40 + 40 if (page40 + 40) < pages else pages):
try:
# 基于DrissionPage控制浏览器访问页面
browser.get(URL.format(genre=genre, page=page))
# 每访问一次页面,需短暂休眠,频率太高IP会被封
time.sleep(random.randint(3, 8))
finally:
# 获取页面Html内容,可解析页面中的数据
print(browser.html)
finally:
# 关闭浏览器
browser.quit()
4、使用第三方付费服务穿云API
最后,再介绍一个付费方案,使用穿云API绕过Cloudflare五秒盾的反爬机制。穿云API作为一种应对Cloudflare反爬虫机制的解决方案,具有许多优势。首先,它能够突破Cloudflare的5秒盾WAF、CC防护等机制,确保我们的爬虫程序能够顺利访问目标网站。其原理是让爬虫发出的http请求更难被识别出来是机器人,穿云API只会尽可能绕过Cloudflare验证码,让Cloudflare验证码不出现,直接访问目标网址,并不是自动去点击Cloudflare验证码。
使用方法如下:
第一步:注册、购买套餐
第二步:获取API令牌
第三步:爬虫接入
# -*- coding: utf-8 -*-
import requests
url = "https://api.cloudbypass.com/genre/house/1.d/"
method = "GET"
headers = {
"x-cb-apikey": r"99ad10f3469643458490e2a******",
"x-cb-host": r"rateyourmusic.com",
}
response = requests.request(method, url, headers=headers)
print(response.text)
亲测有效,使用方便。
2.8、验证码反爬
网页通过弹出验证码防止爬虫程序采集数据是一种常见的反爬机制,爬虫一般可通过以下3种方式来对验证码进行识别验证。
1、使用开源的ocr工具来处理文字类型验证码,比如文字识别、文字点选等。
ddddocr:https://github.com/sml2h3/ddddocr
EasyOCR:https://github.com/JaidedAI/EasyOCR
PaddleOCR:https://github.com/PaddlePaddle/PaddleOCR
对于中文汉字来说,还可以根据字形的相似度来辅助识别,对OCR的识别结果起到一定的纠错作用,进一步提高验证码识别的正确率。
nlp-hanzi-similar:https://github.com/houbb/nlp-hanzi-similar
2、使用浏览器、真机自动化技术来处理简单的滑块类型验证码。
使用opencv匹配缺口并计算缺口距离:
def slide_distance(verify_path, slide_path):
verify_img = cv2.imread(verify_path)
slide_img = cv2.imread(slide_path)
verify_edge = cv2.Canny(verify_img, 100, 200)
slide_edge = cv2.Canny(slide_img, 100, 200)
verify_pic = cv2.cvtColor(verify_edge, cv2.COLOR_GRAY2RGB)
slide_pic = cv2.cvtColor(slide_edge, cv2.COLOR_GRAY2RGB)
# 缺口匹配
res = cv2.matchTemplate(verify_pic, slide_pic, cv2.TM_CCOEFF_NORMED)
# 寻找最优匹配
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
verify_size = Image.open(verify_path).size
# 根据图片真实与表面大小的比例进行缩放
distance = max_loc[0] * verify_size[1] / verify_size[0]
# 删除下载的文件
os.unlink(verify_path)
os.unlink(slide_path)
return distance
另附上一段轨迹生成函数:
def slide_tracks(distance):
tracks = []
y, v, t, current, middle = 0, 0, 1, 0, distance * 3 / 4
exceed = random.randint(1, 5)
while current < (distance + exceed):
if current < middle / 2: a = 0.8
elif current < middle: a = 1.2
else: a = -3.3
current += int(v * t + 0.45 * a * (t * t))
v = v + a * t
y += random.randint(-5, 5)
tracks.append([min(current, (distance + exceed)), y])
while exceed > 0:
exceed -= random.randint(0, 5)
y += random.randint(-3, 3)
tracks.append([min(current, (distance + exceed)), y])
return tracks
3、使用真机远程操作、第三方接码平台来处理其他防护能力更强、复杂度更高的验证码。
可使用ws-scrcpy工具进行真机远程操作,ws-scrcpy 是一个基于Genymobile的scrcpy项目的Web客户端原型,旨在通过WebSocket提供远程控制和屏幕镜像功能。该工具支持Android设备,让用户能在浏览器中实时查看并操控Android设备。
安装过程如下:
# ws-scrcpy 依赖scrcpy、Node.js 和 npm
brew install scrcpy node
#安装nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.nvm/nvm.sh
# 安装node 20版本
nvm install 20
nvm use 20
# 开始依赖
npm install -g pm2
npm install -g node-gyp
# 安装Xcode Command Line Tools,如已安装则忽略
xcode-select --install
# 克隆代码
cd /Users/admin/codes && git clone https://github.com/NetrisTV/ws-scrcpy.git
cd ws-scrcpy
# 开始安装
npm install
# 启动服务,默认端口为8000
pm2 start /Users/admin/codes/ws-scrcpy/dist --name ws-scrcpy
# 保存服务,方便运维
pm2 save
通过H5远程连接手机滑块通过验证的效果如下:
2.9、蜜罐反爬
网站识别爬虫程序并返回爬虫蜜罐数据(假数据)。对于蜜罐反爬来说,最重要的是需要感知到爬虫采集的数据是假数据,然后再优化爬虫策略、隐藏爬虫特征,将爬虫的请求行为进一步真实化,直到采集到正确的数据。
蜜罐数据感知方案如下: 针对具体的爬虫场景,维护一定数量的该场景下的正确爬虫样本,爬虫样本包括爬虫种子以及正确的返回数据。定时触发种子进入爬虫,并制定一套规则用于校验当前种子返回数据与预期的爬虫样本中的返回数据是否一致。如果爬虫数据与样本数据出现不一致,并且数量超过一定阈值,则需告警通知爬虫开发人员介入查看是否遭遇蜜罐数据。
数据校验规则
除必要字段外,其它校验均不会校验属性为null的情况。如仅给某字段设置枚举校验,但是该字段为空,则该字段不会进行枚举校验,也不算异常。如果某字段是必要字段并且字段值需满足特定的规则,那么在设置校验规则的时候应该设置两个:必要字段校验和某个字段值的校验。如果字段值非空类型与校验类型所要求的数据类型不匹配,记为异常情况。
1、必要字段校验
说明:该字段为必要字段,数据无此字段或该字段为null均为异常。
数据类型支持:String、Bool、Number、List
{"requireCheckRules": [{"path": "$.result.company"}]}
2、枚举校验
说明:规则配置字段所有枚举值,如果字段值不在规则配置的枚举值中,则为异常。
数据类型支持:String、Bool、Number
{"enmuCheckRules": [{"path": "$.result.songList[0].name", "value": ["晴天", "兰亭序", "说好的幸福呢"]}]
3、正则匹配校验
说明:规则配置该字段值应该匹配的正则表达式,如果不匹配,则为异常。
数据类型支持:String
{"regexCheckRules": [{"path": "$.result.genre", "value": "(?:POP|pop).*"}]}
4、等值校验
说明:规则配置该字段值应该等于的值,如果不等于,则为异常。
数据类型支持:String、Bool、Number
{"valueCheckRules": [{"path": "$.result.language", "value": "国语"}]}
5、区间校验
说明:规则设置该该字段值的区间,如果字段值不在此区间内,则为异常。
数据类型说明:Number
{"rangeCheckRules": [{"path": "$.result.totalCommentNum", "value": [100000, 500000]}]}
6、包含校验
说明:规则设置该字段值包含特定值的校验,如果字段值不包含设置的特定值,则为异常。
数据类型支持:String、List
校验字段为String(类似于枚举)
{"includeCheckRules": [{"path": "$.result.songList[0].name", "value": ["晴天", "兰亭序", "说好的幸福呢"]}]}
校验字段为List
{"includeCheckRules": [{"path": "$.result.songList[*].name", "value": ["晴天", "兰亭序", "说好的幸福呢"]}]}
三、 App端爬虫
3.1、抓包分析
协议抓包往往是安卓应用逆向分析的第一步,很多时候我们拿到一个App,不知道从何入手分析,一般是从抓包开始,先弄清楚它与服务端通信的内容,如果一目了然,我们可以直接写一个爬虫程序模拟请求通讯,如果协议的参数或请求头中存在加密字段,则需要逆向分析App,定位参数加密位置、还原加密过程、构造加密参数,然后爬虫程序才能模拟请求通讯。
目前很多安卓应用为了防止逆向分析人员进行协议抓包,一般在应用中设置防抓包策略。常见的防抓包策略有代理检测、Vpn检测、证书校验等,还有少数开发实力雄厚甚至过剩的大厂,基于TCP实现了自定义的数据收发协议。
3.1.1、代理检测
代理检测是用于检测设备是否设置了网络代理。这种检测的目的是识别出设备是否尝试通过代理服务器(如抓包工具)来转发网络流量,从而可能截获和分析App的网络通信。
其原理为App会检查系统设置或网络配置,以确定是否有代理服务器被设置为转发流量。例如,它可能会检查系统属性或调用特定的网络信息API来获取当前的网络代理状态。
// Port跟设置有关,例如Charles默认是8888
return System.getProperty("http.proxyHost") == null && System.getProperty("http.proxyPort") == null
强制不走代理:
connection = (HttpURLConnection) url.openConnection(Proxy.NO_PROXY);
OkHttpClient.Builder()
.proxy(Proxy.NO_PROXY)
.build()
爬虫应对方案一:Frida Hook修改系统设置。
function anti_proxy() {
var GetProperty = Java.use("java.lang.System");
GetProperty.getProperty.overload("java.lang.String").implementation = function(getprop) {
if (getprop.indexOf("http.proxyHost") >= 0 || getprop.indexOf("http.proxyPort") >= 0) {
return null;
}
return this.getProperty(getprop);
}
}
爬虫应对方案二:使用透明代理实现抓包。
3.1.2、VPN检测
VPN检测是指应用程序或系统检查用户是否正在使用虚拟专用网络(Virtual Private Network, VPN)的一种技术。当用户使用VPN时,他们的网络流量会被加密并通过一个远程服务器路由,这可以隐藏用户的实际IP地址和位置信息,同时保护数据的安全性和隐私。
其原理为当客户端运行VPN虚拟隧道协议时,会在当前节点创建基于eth之上的tun0接口或ppp0接口。这些接口是用于建立虚拟网络连接的特殊网络接口。
public final boolean Check_Vpn1() {
try {
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
if (networkInterfaces == null) {
return false;
}
Iterator it = Collections.list(networkInterfaces).iterator();
while (it.hasNext()) {
NetworkInterface networkInterface = (NetworkInterface) it.next();
if (networkInterface.isUp() && !networkInterface.getInterfaceAddresses().isEmpty()) {
Log.d("zj595", "isVpn NetworkInterface Name: " + networkInterface.getName());
if (Intrinsics.areEqual(networkInterface.getName(), "tun0") || Intrinsics.areEqual(networkInterface.getName(), "ppp0") || Intrinsics.areEqual(networkInterface.getName(), "p2p0") || Intrinsics.areEqual(networkInterface.getName(), "ccmni0")) {
return true;
}
}
}
return false;
} catch (Throwable th) {
th.printStackTrace();
return false;
}
}
public final boolean Check_Vpn2() {
boolean z;
String networkCapabilities;
try {
Object systemService = getApplicationContext().getSystemService("connectivity");
Intrinsics.checkNotNull(systemService, "null cannot be cast to non-null type android.net.ConnectivityManager");
ConnectivityManager connectivityManager = (ConnectivityManager) systemService;
NetworkCapabilities networkCapabilities2 = connectivityManager.getNetworkCapabilities(connectivityManager.getActiveNetwork());
Log.i("zj595", "networkCapabilities -> " + networkCapabilities2);
boolean z2 = networkCapabilities2 != null && networkCapabilities2.hasTransport(4);
// 检查网络能力是否包含 "WIFI|VPN"
if (networkCapabilities2 != null && (networkCapabilities = networkCapabilities2.toString()) != null) {
if (StringsKt.contains$default((CharSequence) networkCapabilities, (CharSequence) "WIFI|VPN", false, 2, (Object) null)) {
z = true;
return !z || z2;
}
}
z = false;
if (z) {
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
爬虫应对方案:Frida Hook修改网络接口名称及网络状态。
function hook_vpn() {
Java.perform(function () {
var NetworkInterface = Java.use("java.net.NetworkInterface");
NetworkInterface.getName.implementation = function () {
var name = this.getName(); //hook java层的getName方法
if (name === "tun0" || name === "ppp0") {
return "rmnet_data0";
} else {
return name;
}
}
var NetworkCapabilities = Java.use("android.net.NetworkCapabilities");
NetworkCapabilities.hasTransport.implementation = function () {
return false;
}
NetworkCapabilities.appendStringRepresentationOfBitMaskToStringBuilder.implementation = function (sb, bitMask, nameFetcher, separator) {
if (bitMask == 18) {
sb.append("WIFI");
} else {
this.appendStringRepresentationOfBitMaskToStringBuilder(sb, bitMask, nameFetcher, separator);
}
}
})
}
3.1.3、证书校验
证书校验也称SSL Pinning,意思是将服务器提供的SSL/TLS证书内置到移动客户端,当客户端发起请求的时候,通过对比内置的证书与服务器的证书是否一致,来确认这个连接的合法性。
爬虫应对方案:Frida、Xposed Hook修改证书校验结果。
相关项目有:JustTrustMe、sslunpining,当然也可以自行编写Hook程序绕过证书校验。
这里顺便介绍下字节系应用的防抓包策略。首先要说的是抖音的防抓包策略很强,也是采用SSL Pinning技术来防止逆向人员抓包。如果我们在手机上配置了Charles代理,那么当抖音客户端发起网络请求时,Charles代理会将自己的证书返回给抖音客户端,与抖音客户端内置的服务端证书不一致,就会导致证书校验失败,请求也被中断。
为什么说抖音的防抓包策略很强呢?那是因为抖音的SSL Pinning技术是在一个动态链接库(libsscronet.so)中实现的,很难定位到SSL Pinning实现的具体位置,不知道位置就无法干预证书验证的结果。下图就是libsscronet.so文件其中一处证书验证的位置,我们通过修改该文件的二进制指令,将证书校验的结果设置为通过。
3.1.4、双向校验
顾名思义就是客户端验证服务器端证书的正确性,服务器端也验证客户端的证书正确性。
爬虫应对方案:将客户端证书配置到抓包工具。
3.1.5、通杀方案
r0capture是一个安卓应用层抓包的通杀脚本。其简介如下:
1、仅限安卓平台,测试安卓7、8、9、10、11、12、13、14 可用 ;
2、无视所有证书校验或绑定,不用考虑任何证书的事情;
3、通杀TCP/IP四层模型中的应用层中的全部协议;
4、通杀协议包括:Http,WebSocket,Ftp,Xmpp,Imap,Smtp,Protobuf等等、以及它们的SSL版本;
5、通杀所有应用层框架,包括HttpUrlConnection、Okhttp1/3/4、Retrofit/Volley等等;
6、无视加固,不管是整体壳还是二代壳或VMP,不用考虑加固的事情;
本人闲暇之余曾基于r0capture开发了一款抓包工具,命令行界面更加友好,以下为图示:
1、应用抓包列表
2、请求体详情
3、响应体详情
4、请求元信息、调用栈信息
3.2、反编译
App反编译是指将已经编译好的 App 程序(通常以二进制可执行文件的形式存在),通过特定的工具和技术,还原成可读的源代码或其他可理解的形式(如汇编代码、Smali 代码等)的过程。
推荐使用的反编译工具:
1、AndroidKiller:经典的安卓应用反编译工具,工具毕竟很老了,无论是界面还是功能上都不尽人。
2、Jadx:安卓应用反编译利器,推荐使用。
3、JEB Decompiler:功能强大的反编译工具,主要用于代码分析和逆向工程。它能够将Android应用程序的Dalvik字节码反编译为可读的Java源代码,同时支持对ARM平台编写的程序和恶意软件进行逆向工程。相比 jadx,JEB 除了支持apk文件的反编译和Android App的动态调试,还支持 ARM、MIPS、AVR、Intel-x86、WebAssembly、Ethereum(以太坊)等程序的反编译、反汇编,动态调试等。另外,JEB能解析和处理一些PDF文件,是一个极其强大的综合性逆向和审计工具。
4、gda:GDA 不只是一款反编译器,同时也是一款轻便且功能强大的综合性逆向分析利器,不依赖 java 环境。支持 apk, dex, odex, oat, jar, class, aar文件的反编译,支持python及java脚本自动化分析。其包含多个由作者独立研究的高速分析引擎:反编译引擎、漏洞检测引擎、 恶意行为检测引擎、污点传播分析引擎、反混淆引擎、apk壳检测引擎等等。
3.3、静态分析
静态分析是在不运行程序的情况下,对软件的代码或二进制文件进行分析,分析的内容可以包括源代码(如果可用)、反汇编代码、反编译后的伪代码、程序结构、数据流、控制流等。
举例说明:
假设我们有一个加密参数params需要分析其是如何生成的,那么首先会使用反编译工具Jdax搜索参数关键词("params",返回的结果。
通过分析得出params参数是在函数com.xxxx.xxxxxxx.network.retrofit.q.a.intercept中产生的。
最终可以定位到params参数是由com.xxxx.xxxxxxx.utils.xxxxxUtils.serialdata函数加密生成的,serialdata是一个native函数,是在某个动态链接库中实现的。
我们编写以下Frida脚本或使用objection工具Hook serialdata函数,查看传入这个函数的参数是什么。
// FRIDA脚本
Java.perform(function(){
var utils = Java.use("com.xxxx.xxxxxxx.utils.xxxxxUtils");
utils.serialdata.overload("java.lang.String", "java.lang.String").implementation = function(str1, str2) {
var res = this.serialdata(str1, str2);
console.log("str1: " + str1);
console.log("str2: " + str2);
console.log("res: " + res);
console.log("\n\n");
return res;
}
})
下图是使用objection Hook得到该函数的入参及返回值。
参数1是请求路径:
/api/search/song/page
参数2是向服务端提交的数据:
{"offset":"0","limit":"20","channel":"typing","keyword":"天空之城","rqrefer":"[F:63][1667359577935#545#8.8.50#221010200836][e][3][8][btn_search|mod_search_box|page_search_result][:::|天空之城:keyword::|天空之城:keyword::]","scene":"normal","header":"{}","e_r":true}
返回值是一段加密数据:
74A595527B7A1647174ADDB4F261E92FF2AFA5F42E4694960F9C5A3746841C9BEEC7409A6170C866854E8729940E1FD9CE433B5356220F7E55528A03AADF8224AB8645D8F534488F2E7F9D639BF8146E8E67837E15ECCAE17A659719F235D08CE6FC94169D9BB5AF70F6F6482D1188135655B082C975217170C737854DE7835DEA59D3AEA088C83F31684C4E252651975CDD34BD576D886E69A9AC099DF9AB0474716FFB655DF272054DB6B7FA5DF6E76462DE0599EEFE1F60EA6D15E7CB454B8D2E4D79C08575A9FB01022E7D45F6D63CACA558B1FCFCC15D4CBD4B5FED7A918FB5E9A2BFD015D86826BA1B4925278C88E7B24D851F08D85AF8C20AC0B7F0EB71664885A08596DE1B83E0456E4ECD8FB8BD206B040C93F6F2CBC450801C2C5711593EE6EE06B568C34D3BB55F1530D8C1B5F865E0C0F565830D262C482C2C74D2D960A2BF6BB0B5A78E75F8C765D8E0A74A2BD59827F6D8A07FCF542A6A357923E756EBB48A3873424319733E6EF6C3
至此,我们已经可以利用Frida、Xposed或者unidbg将serialdata函数服务化了,通过Api传入参数,主动调用serialdata函数,然后返回加密结果。
3.4、加固脱壳
3.4.1、加固介绍
安卓应用程序加固是一种保护安卓应用程序安全性的技术手段,通过一系列的操作和处理,增加应用程序的安全性,防止其被逆向分析、破解、篡改以及恶意攻击。
这个过程涉及三个对象
1、源程序:源程序也就是我们的要加固的对象,这里面主要修改的是原apk文件中的classes.dex文件和AndroidManifest.xml文件。
2、壳程序:壳程序主要用于解密经过加密了的dex文件,并加载解密后的原dex文件,正常启动原程序。
3、加密程序:加密程序主要是对原dex文件进行加密,加密算法可以是简单的异或操作、rc4、des等加密算法。
加固过程一般分为四个阶段:
1、加密阶段
加密阶段主要是讲把原apk文件中提取出来的classes.dex文件通过加密程序进行加密。
2、合成新的dex文件
这一阶段主要是讲上一步生成的加密的dex文件和我们的壳dex文件合并,将加密的dex文件追加在壳dex文件后面,并在文件末尾追加加密dex文件的大小数值。
3、修改原apk文件并重打包签名
在这一阶段,我们首先将apk解压,会看到如下图的6个文件和目录。其中,我们需要修改的只有2个文件,分别是classes.dex和AndroidManifest.xml文件,其他文件和文件夹都不需要改动。
首先,我们把解压后apk目录下原来的classes.dex文件替换成我们在上一步合成的新的classes.dex文件。然后,由于我们程序运行的时候,首先加载的其实是壳程序里的ProxyApplication类。所以,我们需要修改AndroidManifest.xml文件,指定application为ProxyApplication,这样才能正常找到识别ProxyApplication类并运行壳程序。
4、运行壳程序加载原dex文件
Dalvik虚拟机会加载我们经过修改的新的classes.dex文件,并最先运行ProxyApplication类。在这个类里面,有2个关键的方法:attachBaseContext和onCreate方法。ProxyApplication先是运行attachBaseContext再运行onCreate方法。
在attachBaseContext方法里,主要做两个工作:
1、读取classes.dex文件末尾记录加密dex文件大小的数值,则加密dex文件在新classes.dex文件中的位置为:len(新classes.dex文件) – len(加密dex文件大小)。然后将加密的dex文件读取出来,解密并保存到资源目录下。
2、然后使用自定义的DexClassLoader加载解密后的原dex文件。
在onCreate方法中,主要做两个工作:
1、通过反射修改ActivityThread类,并将Application指向原dex文件中的Application。
2、创建原Application对象,并调用原Application的onCreate方法启动原程序。
加固技术发展历程
传统App加固技术,前后经历了四代技术变更,保护级别每一代都有所提升,但其固有的安全缺陷和兼容性问题始终未能得到解决。而新一代加固技术—虚机源码保护,适用代码类型更广泛,App保护级别更高,兼容性更强,堪称未来级别的保护方案。
3.4.2、脱壳介绍
网络上有不少脱壳方法,脱壳的目的是对抗目标程序的加固方案,拿到应用的源码,即上文介绍的dex文件。本节主要介绍一种较为通用的脱壳方法,即内存dump脱壳法。
内存dump脱壳是怎么一回事呢?实际上是应用打开之后,壳程序会将应用本身的dex文件解密加载至内存中,我们根据这段内存的起始地址与结束地址将该段内存中的内容dump出来,保存为dex文件。
简单叙述下常见的两种dump方案:
1、找到一个脱壳点,一般是hook底层dex文件加载的相关函数,其中入参包括dex的起始地址和大小,直接可以dump下来保存为dex文件。由于dump的时机太早,被抽取的函数还未进行加载,因此该方案无法对抗指令抽取壳,本文将不做更多的介绍。
2、根据dex文件的一些特征去应用占用的内存中做搜索匹配,进而找到dex文件的起始地址、结束地址,再将这段内存dump出来,该方案可以对抗一代、二代、以及部分三代指令抽取壳。
接下来介绍下dex的文件结构,看看哪些特征可以帮助我们实现脱壳。
dex文件结构
从上文得知dex文件是Dalvik虚拟机环境下的可执行文件,也是我们脱壳的目标文件。由于我们采用一种特征搜索的方式从内存中dump出dex文件进行脱壳,那么了解dex文件结构和特征就显得十分重要,这将有助于我们制定切实有效的脱壳策略,dex文件结构如下图所示。
我们重点关注dex文件头结构,dex文件的header除了描述文件信息外,还有文件里其他各个区域的偏移地址 。header对应为结构体类型如下
struct DexHeader {
u1 magic[8]; // 版本标识
u4 checksum; // adler32 检验,判断dex是否被篡改
u1 signature[20]; // SHA-1 哈希值
u4 fileSize; // 整个文件大小
u4 headerSize; // DexHeader 大小
u4 endianTag; // 字节序标记
u4 linkSize; // 链接段大小
u4 linkOff; // 链接段偏移
u4 mapOff; // DexMapList 的偏移量
u4 stringIdsSize; // DexStringId 的个数
u4 stringIdsOff; // DexStringId 的偏移量
u4 typeIdsSize; // DexTypeId 的个数
u4 typeIdsOff; // DexTypeId 的偏移量
u4 protoIdsSize; // DexProtoId 的个数
u4 protoIdsOff; // DexProtoId 的偏移量
u4 fieldIdsSize; // DexFieldId 的个数
u4 fieldIdsOff; // DexFieldId 的偏移量
u4 methodIdsSize; // DexMethodId 的个数
u4 methodIdsOff; // DexMethodId 的偏移量
u4 classDefsSize; // DexClassDef 的个数
u4 classDefsOff; // DexClassDef 的偏移量
u4 dataSize; // 数据段的大小
u4 dataOff; // 数据段的文件偏移
};
其中有以下几项说明:
1、header文件头大小为112个字节,16进制表示为0x70,所以headerSize值为0x70。
2、magic数组一般为{0x64 0x65 0x78 0x0a 0x30 0x33 0x35 0x00},转化字符串为"dex\n035\0",其中035是dex文件格式的版本,也存在dex版本为036的,该字段可能会被应用开发者抹掉,有意隐藏了dex文件的特征。
3、checksum文件校验码,使用alder32算法校验文件除去maigc、checksum外余下的所有文件区域,用于检查文件是否被篡改。
4、signature使用SHA-1算法hash除去magic、checksum和signature外余下的所有文件区域,用于唯一识别本文件。
5、fileSize表示dex文件的大小,该字段可能会被应用开发者抹掉,有意隐藏了dex文件的大小。
6、mapOff是DexMapList的偏移量,该数据在data区里的,其值要大于等于data_off的大小。
这里我们再详细介绍下mapOff这个字段,接下来会用到。
mapOff是DexMapList的偏移量,其对应的结构体类型如下。
struct DexMapList {
u4 size; // DexMapItem 的个数
DexMapItem list[size]; // DexMapItem结构
};
struct DexMapItem {
u2 type; // kDexType 开头的类型,下面为枚举
u2 unused; // 未使用,用于对齐字节的
u4 size; // 指定类型的个数
u4 offset; // 指定类型数据的偏移量
}
enum {
kDexTypeHeaderItem = 0x0000, // 对应 DexHeader
kDexTypeStringIdItem = 0x0001, // 对应 stringIdsSize与stringIdsOff字段
kDexTypeTypeIdItem = 0x0002,
kDexTypeProtoIdItem = 0x0003,
kDexTypeFieldIdItem = 0x0004,
kDexTypeMethodIdItem = 0x0005,
kDexTypeClassDefItem = 0x0006,
kDexTypeMapList = 0x1000,
kDexTypeTypeList = 0x1001,
kDexTypeAnnotationSetRefList = 0x1002,
kDexTypeAnnotationSetItem = 0x1003,
kDexTypeClassDataItem = 0x2000,
kDexTypeCodeItem = 0x2001,
kDexTypeStringDataItem = 0x2002,
kDexTypeDebugInfoItem = 0x2003,
kDexTypeAnnotationItem = 0x2004,
kDexTypeEncodedArrayItem = 0x2005,
kDexTypeAnnotationsDirectoryItem = 0x2006,
}
DexMapList里先用一个4字节空间描述后面有size个DexMapItem,后续就是对应的size个DexMapItem描述。
DexMapItem结构有4个元素,每个item大小为12字节,其中type表示该DexMapItem的类型,根据上图中枚举得知,DexMapList对应type值为0x1000;size表示指定类型的个数;offset是指定类型数据的偏移量;unuse是用对齐字节的,无实际用处。实际上,header中的mapOff偏移量与部分DexMapItem中的offset相同。
dex文件特征总结
1、magic是固定的,16进制表示为 64 65 78 0a 30 33 35 00,使用通配符表示为 64 65 78 0a 30 ?? ?? 00,将版本信息用通配符表示。
2、headerSize是固定的,16进制表示为 70 00 00 00。
3、mapOff指向DexMapList,其中部分item的offset与mapOff相同。
脱壳原理
根据这些文件特征,可以总结出两种脱壳的方案,简单的说就是先把一块不知道是不是dex的内存当作dex看,然后通过文件格式来进行校验,增强可信度。
1、应用内存中搜索 64 65 78 0a 30 ?? ?? 00,搜索到的地址即为dex文件的开始地址,开始地址加上0x20是fileSize的地址,读取fileSize得到dex文件的大小,继而得到dex文件的结束地址,开始地址加上0x3c是headerSize地址,读取headerSize值判断其是否等于0x70进行校验,校验通过则可以dump出开始地址到结束地址这一段内存数据,当然这也是比较理想的情况了。
2、如果magic或fileSize被抹去了呢?可以根据headerSize值70 00 00 00在内存中搜索,这样可能会搜索到其他的干扰结果,搜索到的地址减去0x3c视为dex文件的开始地址,开始地址加上0x34是mapOff的地址,读取mapOff的值为DexMapList的偏移地址,开始地址加上DexMapList偏移地址得到DexMapList地址,读取DexMapList地址的前4个字节,得到DexMapItem的个数,从DexMapList地址加4的位置开始依次读取12个字节数据即DexMapItem数据,读取DexMapItem中的offset值,判断是否存在和mapOff相同的,如果存在,则表示我们读取的就是一个dex文件,那么DexMapItem结束地址视为dex文件的结束地址,然后dump出开始地址到结束地址这一段内存数据。
3.4.3、脱壳工具
相关资料:
2、Fart
相关资料:
拨云见日:安卓APP脱壳的本质以及如何快速发现ART下的脱壳点
相关资料:
3.5、动态调试
通过IDA pro进行动态调试的步骤如下,感兴趣的同学可自行了解。
1、手机端启动ida_server。
nohup ida_server -p20945 >/dev/null &
2、电脑端配置端口转发,用于Ida pro与ida_server通信。
adb forward tcp:20945 tcp:20945
3、Debuggable模式启动安卓应用程序,(参考资料:Android修改ro.debuggable 的四种方法)
adb shell am start -D -n com.xxxx.xxxxxx/.activity.LoadingActivity
4、查找应用程序进程id。
adb shell ps | grep {应用程序包名}
5、电脑端配置端口转发,用于电脑端通过8700端口与手机端通信。
adb forward tcp:8700 jdwp:{应用程序进程id}
6、IDA attach 安卓应用程序(参考网上教程)。
7、jdb调试安卓应用程序。
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
推荐学习资料:
1、Arm汇编基础(需要学习一定的汇编知识)
4、IDA 动态调试Android SO .init .init_array JNI_Onload总结
3.6、代码插桩
对于代码插桩工具,爬虫开发者最熟悉可能就是Frida和Xposed了。以Frida为例,Frida是一款功能强大的动态代码插桩工具,允许在程序运行时实时插入额外代码,这种技术使得开发者可以在不修改原始代码的情况下,监控和修改应用程序的行为。Frida广泛应用于安全研究、逆向工程和自动化测试等领域。它基于Python和JavaScript开发,支持跨平台运行,包括Windows、macOS、Linux、Android和iOS等操作系统。
推荐学习资料:
3.7、反反调试
反调试技术是指开发者在App中采取的各种措施,用于阻止或干扰他人对App进行动态分析、调试和逆向工程。其目的是保护App的知识产权、防止恶意行为(如篡改、破解、提取敏感信息)以及确保App的安全性。反反调试,即指突破并绕过应用的反调试手段,顺利进入到应用的正常调试、逻辑分析的逆向工程手段。
推荐学习资料:
3、调试与反调试详解
3.8、unidbg
unidbg是一个基于unicorn项目衍生的一个逆向工具,可以黑盒调用安卓和IOS中的so文件。unidbg不需要运行app,也不需要逆向so文件,而是通过在app中找到对应的JNI接口,然后用unicorn引擎直接执行这个so文件,所以效率也比较高,通常可以将算法破解之后通过API接口的形式暴露出来,提供业务方调用。
推荐龙哥的unidbg基础入门学习。
1、入门基础
2、补环境
2.1、补文件访问
2.2、补系统调用
2.3、补库函数
2.4、补环境的深层困境
3、初始化问题
4、综合案例
3.9、算法还原
算法还原往往会经历抓包分析、反编译、静态分析、动态分析等以上所有过程,其目的是为了还原保护在Java层或Native层中的加解密算法。
一般算法还原步骤如下:
1、结果追踪:加密结果转化16进制,一般以四个字节为单位在汇编指令流中搜索。
2、指令分析:搜索确认相关指令地址后向上分析汇编指令流,尝试理解运算逻辑。
3、反汇编代码:尝试使用IDA反汇编工具将指令所在代码块反汇编成C语言伪代码。
4、算法识别:基于汇编指令流及反汇编代码识别出这些指令是如何实现特定算法的。
5、算法还原:结合汇编指令流及反汇编代码将汇编代码转化为高级语言代码。
6、调试和验证:还原算法与汇编指令流单步比较,验证算法还原是否正确。
算法还原效果如下:
3.10、群控技术
对于真机群控技术,各家团队可能都会探索出一个满足自身需求的方案。这里简单贴下个人理解的示意图,欢迎有经验的大佬们多多指教。
四、法律风险
由爬虫引起的刑事纠纷主要来自以下问题:
1、涉及个人隐私(电话、姓名、住址、账号等)
2、版权归属争议(采集他人版权内容直接或间接商用)
3、直接商用原始数据(涉嫌不正当竞争)
4、DDos/扫描渗透(涉嫌非法侵入计算机系统)
5、扰乱对方经营规则
6、破坏规则,直接侵犯对方甚至人民的权益(抢购疫苗、酒水、演唱会门票等)
7、黑产行为(刷粉、刷赞、刷单等)
以上问题是因,而破坏计算机信息安全罪是果。更明确的说,就是爬虫的行为直接或者间接的做了让对方公司难受(一般指利益受损)的事,即公布了对方不想展示的内容,损害了对方的用户、公司的利益。
五、总结
当前爬虫技术以网页解析为根基、移动端逆向为深化、设备集群控制为扩展,形成覆盖全场景的数据采集网络,其技术演进始终围绕'模拟真实性'与'规避检测性'的双重博弈展开。