需求:提供查询词,抓抖音的搜索结果,并下载视频文件
抖音不提供网页端搜索,同时app内搜索时,网络请求包含大量加密参数,破解较困难
因此采用安卓模拟器+抓包的方式进行抓取
- 抖音采用了ssl pinning技术,不接受用户安装的cert证书,无法通过中间人攻击方式抓包
- 抖音app启动后生成唯一device_id,不随清空app数据、卸载重装、重启等操作改变。单个设备频繁搜索会触发封禁(测试中,即使搜索之间穿插浏览信息流操作,在20词左右就会触发封禁)
用于前期APP分析、后期数据抓取,主要包括:
- fiddler : 抓包分析,(或手机端抓包工具HttpCanary和PacketCapture)
- jadx : 安卓APP反编译工具
- adb : Android Debug Bridge,与安卓设备通信,可执行拷贝、安装、启动APP操作
- android studio : 安卓开发IDE
- xposed : 运行时hook,可用来绕过APP的一些限制
- genymotion : 安卓模拟器
- virtulbox : genymotion依赖
- vnc+xfce : VNC可用于远程连接Linux图形界面;genymotion不支持无头模式,必须安装图形界面(或者使用Xvfb可能可行,未进一步了解)
操作系统:CentOS 8
抖音版本:15.1.0
VNC安装:参考http://websac.sogou/rookie_guide/work_env/#vnc
XFCE安装:
yum groupinstall "Xfce" -y
virtualbox依赖虚拟化技术,无法在虚机运行,请使用实机
genymotion依赖的libstdc++.so在centos7上不满足,需自行编译;建议直接使用centos8
选用genymotion+virtualbox,相比其他模拟器,提供了较丰富的命令行工具,更方便编程操作
virtualbox安装:
dnf config-manager --add-repo=https://download.virtualbox.org/virtualbox/rpm/el/virtualbox.repo
yum search virtualbox
yum install virtualbox
若最新版virtualbox卡顿崩溃,可尝试安装6.0及以下版本
若virtualbox启动报错:
WARNING: The vboxdrv kernel module is not loaded. Either there is no module available for the current kernel (4.18.0-193.el8.x86_64) or it failed to load. If your system is using EFI Secure Boot you may need to sign the kernel modules (vboxdrv, vboxnetflt, vboxnetadp, vboxpci) before you can load them
可尝试安装dkms解决
genymotion安装:下载linux安装文件,直接运行即可完成安装
部分工具仅在开发阶段用到,或开发与运行时所需不同,列举如下:
-
开发环境:
- fiddler : 抓包,可视化分析
- Android Studio : 安卓开发IDE
- adb : 与虚拟机通信,包含在SDK Tools中,可通过Android Studio附带的SDK Manager下载,路径
<sdk_root>/platform-tools/
- uiautomatorviewer : Android页面检查工具,同上包含在SDK Tools中,路径
<sdk_root>/tools/
- jadx : apk反编译(可选)
-
运行时:
- mitmproxy : 抓包,可通过pip安装
- adb : 与虚拟机通信,包含在platform-tools中,下载
步骤 | 详细 |
---|---|
创建模拟器 | 在genymotion中创建,Android 8.0版本;完成后启动模拟器,进行后续步骤 |
配置代理 | 设置模拟器,使用fiddler或mitmproxy代理请求,配置方法 |
安装genymotion arm translation | arm版APP无法在x86的模拟器上直接运行,需进行转换。安装包可通过adb刷入系统,或直接拖拽安装(最高支持8.0版本,下载) |
安装抖音APP | 官网下载安装即可,可通过adb或拖拽安装 |
安装xposed | 包含xposed framework(刷机包)和xposed installer(APP)安装方式同上。注:xposed最高支持andorid 8.0,更高版本安卓可使用magisk+edxposed方案,edxposed完全兼容xposded插件 |
安装xposed插件 | xposed插件本质是android APP,这里需要使用xposed实现ssl pinning破解(JustTrustMe)和虚拟mac地址,稍后展开 |
代码主要包括以下部分:
- instrument-test : 标准android APP,按照UI Automator测试框架编写,实际执行启动抖音APP、输入查询词、点击搜索和滚屏动作
- runner : python脚本,用于管理模拟器,在多个模拟器上启动由instrument-test构建的测试APP,从而让测试APP接管对抖音的操作
- macaddr-changer : xposed插件,hook mac地址,重启时随机生成
- keyserver : 查询词放在redis中,由于不清楚app内如何连redis,所以包装成http服务提供查询词(不展开)
- proxy : 基于mitmproxy,记录搜索结果,保存到redis。*注:搜索结果可能是几段json字符串拼接,需解析(不展开)
- spider : 解析proxy的结果,下载视频并存储(不展开)
下面按照实现顺序简要分析
Xposed是一个运行在Android系统的hook框架,通过对Zygote
线程的定制,实现了运行时hook方法调用的能力,可实现方法定制甚至替换,详细信息参见文档和API。这里借助xposde绕过抖音的反爬机制
部分应用通过应用列表、调用栈等手段检测xposed,在安装了xposed的设备上直接闪退,可采用Magisk+EdXposed,用Magisk Hide绕过
中间人攻击需要在客户端(Android)安装代理颁发的CA证书,安卓7.0以后证书必须安装到系统证书目录下(需ROOT)教程,而部分应用采用了ssl pinning技术,只信任特定证书,表现形式为:即使已将fiddler证书安装到系统证书目录,抓包依然报网络错误或无法解码Fiddler's HTTPS Decryption feature is enabled, but this specific tunnel was configured not to be decrypted
关于如何绕过ssl pinning可参考博客
通过反编译抖音APP可以看到抖音使用了okhttp3
包,推测ssl pinning由该包实现,相关类okhttp3.CertificatePinner
,方法签名:
public void check(String, List)
从xposed的模块库里能直接找到破解ssl pinning的模块,这里使用JustTrustMe模块,其hook的核心代码
XposedHelpers.findAndHookMethod("okhttp3.CertificatePinner", classLoader, "check", String.class, List.class, new XC_MethodReplacement() {
public Object replaceHookedMethod(XC_MethodHook.MethodHookParam methodHookParam) throws Throwable {
return null;
}
})
抖音APP未登录状态下,搜索几次后,后续搜索结果为空。抓包发现请求参数中带有device_id、openudid,推测抖音根据二者标识设备
通常openudid的生成策略为:
- 若存储中存在openudid相关文件,直接读取
- 否则获取android_id,若有效则作为openudid
- 否则随机生成,并存储供下次读取
清除抖音APP数据后重新启动,新生成的openudid和andorid_id一致,可以判断符合上述策略,因此可通过更换android_id间接更换openudid
device_id值和Android的Device ID/IMEI
不同,推测device_id根据mac地址和openudid生成(参考),想要生成新的device_id需要更换mac地址。直接替换mac地址文件存在问题(如导致通过wlan连接的adb断连)且高版本安卓难以替换,因此使用xposed向抖音返回假的可更换的mac地址来解决。
android 6.0及以上获取mac地址方式如下:
public String getMacAddress() throws SocketException {
Enumeration enumeration = NetworkInterface.getNetworkInterfaces();
while (enumeration.hasMoreElements()) {
NetworkInterface networkInterface = (NetworkInterface) enumeration.nextElement();
byte[] arrayOfByte = networkInterface.getHardwareAddress();
if (arrayOfByte == null || arrayOfByte.length == 0) {
continue;
}
if (!networkInterface.getName().equals("wlan0")) {
continue;
}
StringBuilder stringBuilder = new StringBuilder();
for (byte b : arrayOfByte) {
stringBuilder.append(String.format("%02X:", new Object[]{Byte.valueOf(b)}));
}
if (stringBuilder.length() > 0) {
stringBuilder.deleteCharAt(stringBuilder.length() - 1);
}
return stringBuilder.toString();
}
return null;
}
需要hook的方法为java.net.NetworkInterface
类的getHardwareAddress
,方法签名:
public byte[] getHardwareAddress()
现有模块没找到有效的,自行实现,macaddr-changer的xposed入口位于com.hamster.macaddresschanger.MainXposed
,核心代码:
// 只对抖音生效
if (lpparam.packageName.equals("com.ss.android.ugc.aweme")) {
findAndHookMethod("java.net.NetworkInterface", lpparam.classLoader, "getHardwareAddress", XC_MethodReplacement.returnConstant(currMac));
}
基于以上,可以实现设备被封禁后自动更换标识继续抓取
另外抖音似乎针对多设备同IP的情况有限制,在同一台linux上起多个模拟器时,只有一到两台模拟器有搜索结果
这里通过在多台开发机运行mitmproxy,将模拟器分别连接到不同mitmproxy实例解决
常用的连接Android模拟器控制APP的框架有UI Automator和APPium,后者是基于前者的封装支持python但性能较差,因此这里直接选用UI Automator
UI Automator 是一个界面测试框架,适用于整个系统上以及多个已安装应用间的跨应用功能界面测试,通过该框架可以实现页面元素检查、输入、点击、滑动和拖拽等
instrument-test用于控制抖音APP,基于UI Automator,实现了如下操作:
- 启动抖音
- 识别启动弹框(位置、存储权限申请)并跳过
- 点击主页搜索图表进入搜索
- 通过http请求获取查询词
- 将查询词设置到输入框
- 点击查询按钮
- 向下滚动触发动态加载
其中部分步骤需要等待上个操作完成,通过页面元素检查和等待实现,不详细展开;app中可以打log,通过logcat捕获
下面简单介绍一些需要特殊注意的点
-
页面元素定位
通常可使用resource_id定位页面元素(By.res("pkgName", "resouceId")
),必要时可使用depth和children进一步定位
可用uiautomatorviewer查看页面结构(工具位于<android SDK>/tools
目录下),或使用adb shell uiautomator dump
命令将页面结构dump到/sdcard/window_dump.xml
uiautomator是通过AccessibilityService(无障碍)实现的,默认会等待视图不再变化(uiAutomation.waitForIdle(1000, 1000 * 10)
)一段时间后才dump页面结构,因此对持续更新的视图会超时报错,表现为:uiautomatorviewer报错Error while obtaining UI hierarchy XML file: com.android.ddmlib.SyncException: Remote object doesn't exist!
;uiautomator dump命令报错ERROR: could not get idle state
。解决方式:基于UI Automator编写APP,使用uiDevice.dumpWindowHierarchy
直接dump,项目地址 -
页面元素检查
代码中常常需要进行页面元素查找,或等待元素出现,如:找到搜索按钮并点击,等待搜索结果出现,然后滚屏
常用方法有uiDevice.wait
、Until.findObject
等,由于这些方法中同样包含waitForIdle
逻辑,持续更新的视图会导致方法长时间不返回拖慢速度。waitForIdle
默认超时10s,可减少超时时间,设为1~1.5s较合适,过短易导致androidx.test.uiautomator.StaleObjectException
Configurator.getInstance().setWaitForIdleTimeout(1000);
-
权限
本例中,需要发送http请求获取查询词,因此需要申请网络权限,在AndroidManifest.xml
中添加:<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
*注:网络权限申请仅添加到测试APP无效,请同时添加到目标APP(测试APP:
app/src/androidTest
,目标APP:app/src/main
) -
抖音APP启停
方式一:通过代码控制启停// 启动 Context context = getApplicationContext(); final Intent intent = context.getPackageManager().getLaunchIntentForPackage("com.ss.android.ugc.aweme"); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); context.startActivity(intent); // 停止 mDevice.executeShellCommand("am force-stop com.ss.android.ugc.aweme"); //等价于:adb shell am force-stop com.ss.android.ugc.aweme
应用包名获取方式:
adb shell dumpsys activity top
方式二:模拟点按
模拟点击图标启动;模拟点击home->点击多任务键->点击多任务卡片中抖音的x号测试发现代码控制易造成应用启动卡死,可能和抖音有关,最终采用模拟点按方式
其他:
多次搜索之间穿插回信息流页刷几下
出现搜索结果为空时,尝试退出app重开,尝试清空app数据重开
……
项目通过build/assemble/assembleAndroidTest可以得到app-debug.apk
和app-debug-androidTest.apk
,安装到安卓模拟器
首先启动proxy、keyserver等其他服务;然后启动安卓模拟器,运行上一步构建出的测试APP,自动化搜索查询词开始抓取
runner使用python编写,实现如下功能:
-
检测已创建、已启动的安卓模拟器
# 已创建列表 vboxmanage list vms # virtualbox 提供 # return: # "Clone - Google Nexus 6P" {e8447dd6-94e5-4e54-886a-d632cab1e709} # "Clone - Google Nexus 6P_1" {e8447dd6-94e5-4e54-886a-d632cab1e709} # "Clone - Google Nexus 6P_2" {e8447dd6-94e5-4e54-886a-d632cab1e709} # 已启动列表 adb devices # return: # 192.168.87.101:5555 device # 获取二者对应关系 adb -s 192.168.87.101:5555 shell getprop | grep "ro.product.model" # adb -s <serial_number> 发送命令到指定设备 # return: # [ro.product.model]: [Clone - Google Nexus 6P_2]
-
启动/关闭模拟器
# 启动模拟器 player -n <device_name> # device_name, eg: Clone - Google Nexus 6P, Clone - Google Nexus 6P_1 # 关闭模拟器 player -n <device_name> -x # 检测启动完成 adb -s <serial_number> shell getprop init.svc.bootanim # return: # '' | running | stopped, stoped==boot finish
-
运行测试APP
测试APP可以通过adb命令安装到模拟器,也可以提前装好
模拟器启动完成后,首先清空抖音APP数据(确保启动时触发生成新的device_id和openudid,主要针对模拟器之前被使用过的情况)adb -s <serial_number> shell pm clear com.ss.android.ugc.aweme
mac地址已由macaddr-changer随机生成,接下来通过adb更换android_id
adb -s <serial_number> shell settings put secure android_id <random_android_id>
最后,启动测试APP
adb -s <serial_number> shell am instrument -w \ -e debug false \ -e class com.hamster.androidappspider.TiktokSearchTest \ com.hamster.androidappspider.test/androidx.test.runner.AndroidJUnitRunner # 其中 # com.hamster.androidappspider.TiktokSearchTest 为测试类 # com.hamster.androidappspider.test 为测试APP包名 # androidx.test.runner.AndroidJUnitRunner 为runner_class
genymotion-shell -c "devices list"
也能获取已创建模拟器,但性能差易卡死,不推荐
# 已连接设备
adb devices
# 安装APP
adb install -r xxxx.apk
# 卸载APP
adb unstall <packagename>
# 清空APP数据
adb shell pm clear <packagename>
# 强制退出APP
adb shell am force-stop <packagename>
# 刷入zip包
adb shell
sh /system/bin/flash-archive.sh <update.zip>
# 文件传输
adb push <soruce> /sdcard/<dest> # 拷贝到手机
adb pull /sdcard/<source> <dest> # 从手机拷贝
# 查看栈顶Activity,可用来获取包名
adb shell dumpsys activity top
# 已安装包名
adb shell pm list packages -f
# 查询Android ID
adb shell content query --uri content://settings/secure --where "name=\'android_id\'"
# 改变Android ID
adb shell settings put secure android_id <new_android_id>