提示:本文仅分享框架设计思路和大体的使用,全是博主自己个人的设计思路,转载请注明出处
随着自己做逆向采集已经快一年了,逆向的方向还是半桶水,但是优化采集流程,搭建自己的采集体系方便工作和运维却是在这一年中有较大的提升。本框架就提供一个学习思路和大体的使用,所有的一些操作都是博主个人经验的总结,用代码的形式展现出来而已,细节方面还得靠自己去看代码跟着思路走。下文中的cpfirda就是本次的框架,是基于frida进行的二次开发,好,下面进入正文,害怕起来了。。。。。。
简单用一句话来说:搭好架构,完善hook脚本,配置好项目配置,手机直接接入,就可以采集数据
提供了简易的日志收集,用来记录每次hook的数据、数据回传、对设备的操作以及一些异常信息的收集等等,用来分析一个完整的hook流程或者分析hook过程中的一些异常,方便后续优化流程等操作
提供了简易的运维操作,因为在hook的时候设备或系统的原因,会导致app经常卡死,这时候就有重启手机或者app等简单的操作来保持hook流程的正常进行
提供了一些消息通知,正常的一个项目,肯定要有自己的告警体系,这边我采用了飞书群通知的形式告知运维人员设备风控等情况,至于要其他形式,需要自己找api接入
提供了模板快速生成代码的功能,为什么要这个呢?有做过frida采集的同学一定深有体会,frida是用脚本的形式去打桩的,每次脚本可能都不大一样,但是总体又可能相似,就差一些参数或者频率等等,这时候模板就会帮助我们去节省大量的开发时间了
提供了进程维护功能,frida是脚本形式就奠定了它比较难以有效的集中管理,通常是以脚本的形式运行业务,那一些奇奇怪怪的问题就会导致脚本停止,这时候就需要在脚本运行的时候记录一下当前脚本的进程信息,方便后续运维
提供了简单的自动运维功能,当脚本停止,数据长时间没有回调,我们是需要一个数据回调的统计和告警平台的,当数据长时间未回调,这个平台上的对应信息就会出现一个告警(这个流程也比较简单,就是监控数据回调的时长,如果超过某个值没有数据回传,那么就认为设备异常,出告警就行了),然后框架会定时去访问这个平台,捕获到告警信息,对对应的设备进行重启等操作,如果框架有在运维对应的进程,就会杀死对应的进程,重新在创建一个新的进程(为什么要杀死?因为如果那个进程还在的话没有什么问题,只是在等待hook的参数,那么当没有杀死进程的情况下,一旦拿到了hook参数,那么此时就有两条进程在同时跑一个设备一个app的打桩点,目前以我的经验这样是行不通的)
基于第四个模板处理下,框架提供了根据配置文件快速生成项目的功能,说白了就是项目开始自有配置文件,框架会根据配置项生成不同的工作空间(这个后续在讲什么是工作空间)来完善整个项目。当然还提供了收纳项目了,会根据框架中你可能手动改过的配置进行更新已有的配置,同时删除了对应的工作空间,这样项目的可移植性非常之高了
做爬虫的或者逆向采集的,都知道频率是个很玄学的东西,我们会经常去修改频率来满足我们的业务需求或者来规避风控等等,该框架集成了一些采集的频率,我们可以通过认为的方式去调控频率,但不是实时的,需要停止该业务线在启动才生效(如果实时的我觉得IO应该会比较高。。。。。。)
提供了一快速部署项目的脚本,如果用Jenkins的话,拉玩代码执行该脚本就行,如果手动的也是一样,十分方便
集成了一些hook过程常用的工具,需要手动拉起对应的dex包到手机的/data/loacl/tmp目录,只有这个目录用户才有操作的权限
这个我就给个思路吧,这个地方比较复杂,我们开发一个APP,这个APP有什么功能呢?
a、可以发送广播,这个广播可以启动内网穿透对应的程序和frida服务端的程序,而且要实时运行,这样就可以完全打通服务端和手机端的正常通信了
b、这个APP可以释放对应项目所需的dex包和frida-server包以及你们自己的一些脚本啊等的
c、这个APP可以拉起内网穿透程序配置,这个配置是把frida端口和ADB端口映射到服务端的,这样能方便我们工作,手机中去编辑配置,想想都蛋疼。。。。。。
上面介绍完了功能,现在我们来看看项目的整体流程,直接上流程图:
这张图片是最早之前自己画的流程图,其中还包括了开发部分,很简单明了,相信一看就到明白了。传统的Frida在本地调试的时候需要用usb连接,我框架中直接采用远程连接,然后利用frp内网穿透工具把frida的端口和ADB的端口直接都映射到Linux服务器上,这样就可以直接在服务器上的某个端口访问手机端的frida端口和ADB端口实现远程操作了,像之前如何将手机变成一个(Linux)服务器提到的在手机上安装Linux服务,非常之不稳定和繁琐,这样做的稳定性和简便我自己是深有体会的
先上图:
a、python3.8:为了兼容frida15.1.10版本,这里我选用Anaconda3
b、frida-client:对于手机来说,此时linux就是客户端,frida对应的插件可以网上找一大堆,这里就不赘述了,我选用的是葫芦娃大佬编译的frida
c、adb工具:远程操作手机和app,网上一堆
d、frp-server:映射手机上的frida和adb端口的,网上都有,其他工具只要能实现内网穿透都行
e、linux自带的定时任务crontab:用来定时启动监控脚本,在项目启动文件中已经写好了
先上图:
a、fastjson.dex:阿里开发的对象和JSON的处理包
b、FRP_INSTALL_YouWant:自己开发的APP用来发送广播,维护frida和frp的app
c、helper:自己开发的JONS处理工具,删除JSON不需要的字段
d、Magisk:获取手机的root权限,在刷机的时候刷入,可以去玩主页学习怎么刷机点击学习
e、RootExplorer:文件管理工具,系统自带的不好用
这里的辅助指的是一件部署功能的,即jenkins,详细的部署流程和简单的使用可以去我主页看对应的博文点击学习
你也许会觉得上面的插件和APP怎么这么多啊,这个就看你自己规划了,可以根据自己的认知简易部署流程,或者可以舍弃一些自己觉得没用的东西,毕竟有些东西是辅助作用,不是项目的核心需要哈
项目目录这个东西啊,真的有些头疼,开发的小伙伴也行会感同身受,本项目的目录大部分以及配置好了,无需太多的改动,上项目目录图:
cpfrida:项目名称
v1.0:版本,和v2.0采集逻辑上不同,v1.0是采用权限分发的形式,怎么分发的呢?这么说吧,你自顾自的采集会导致数据失衡,满足不了业务,所有规则就是满足业务需求,举个例子:比如有几个接口detail、product、live、topic,业务需要的是detail一个,product三个,live五个,topic两个,这时候假设你要部署11个设备上去满足业务,但是在采集的过程中,你会发现某一天product数据量过多,在规定时间内完成不了,但是呢,live数据量过少很大一部分时间会处于空闲时间,这样就相当于浪费了资源,于是就有了权限分发,每台设备先按之前的方式部署好,detail一个,product三个,live五个,topic两个,当某台设备空闲的时候就会执行下级权限,现在live的设备权限顺序为:live–>product–>detail–>topic,当live空闲的时候,它就执行product,product也空闲的时候依次类推,每次完成一次完整的hook之后,权限又是重头开始,这样做就可以达到资源全部合理利用啦,对了,牢记frida无法同时在一个点打桩多次,按我hook的app来看的话是这样,上面的权限分发就有效的避开了这个问题
v2.0:版本,和v1.0采集逻辑上不同,v2.0是采用进程池的概念,它比一来的简单暴力,但是业务线过多的话Linux服务器可能就需要多个了,比较耗费一些资源。它没有那些采集规则,业务不够就加一套,还是接口detail、product、live、topic,一样的部署方式,只是此时11个设备我每个设备都跑四条业务,这时候就有44个业务了,是不是体量多太多了,想想都有点小刺激
template:代码模板
build.ini:项目配置项,所有的工作空间都是根据这个配置项生成的
build_.ini:配置项的各项说明
deploy.py:快速生成代码的核心,已集成到项目启动脚本中
requirements.txt:项目依赖
不会反编译,一切都是徒劳的,下面就给出了js的辅助工具,至于hook的核心代码和接口,这边不便展示,懂得都懂(手动狗头):
/**
* @Description: Wechat 8015 hook script
* @author XQE
* @date 2022/04/19
*/
var tools = {
classexists: function (className) {
var _exists = false
try {
Java.use(className)
_exists = true
} catch (err) {
}
return _exists
},
checkloaddex: function (className, dexfile) {
if (!this.classexists(className)) {
Java.openClassFile(dexfile).load()
}
},
tojsonstring: function (obj) {
try {
this.checkloaddex("com.alibaba.fastjson.JSON", "/data/local/tmp/fastjson.dex")
var _clz = Java.use("com.alibaba.fastjson.JSON")
var _toJSONStringMehtod = _clz.toJSONString.overload("java.lang.Object")
return _toJSONStringMehtod.call(_clz, obj)
} catch (err) {
console.log(err)
}
return "{}"
},
fromjsonstring: function (jsonStr, classObj = null) {
try {
this.checkloaddex("com.alibaba.fastjson.JSON", "/data/local/tmp/fastjson.dex")
if (classObj == null) {
var _clz = Java.use("com.alibaba.fastjson.JSON")
return _clz.parseObject(jsonStr)
} else {
var _jsonObject = Java.use("com.alibaba.fastjson.JSONObject")
var _obj = _jsonObject.parseObject(jsonStr, classObj.class)
return _obj
}
} catch (err) {
console.log(err)
}
return null
},
recursionremove: function (jsonObject, removeKey) {
var _keyArray = jsonObject.keySet().toArray()
for (var i = 0; i < _keyArray.length; i++) {
var _key = _keyArray[i]
var _object = jsonObject.get(_key)
var _objectType = ""
try {
_objectType = _object.getClass().getName()
} catch (e) {}
if (_key == removeKey && _objectType == "java.lang.String") {
jsonObject.remove(_key)
}
if (_objectType == "com.alibaba.fastjson.JSONObject") {
this.recursionremove(jsonObject.getJSONObject(_key), removeKey)
}
if (_objectType == "com.alibaba.fastjson.JSONArray") {
var jsonArray = jsonObject.getJSONArray(_key)
for (var a = 0; a <= jsonArray.size(); a++) {
try {
var _childObj = jsonArray.getJSONObject(a)
if (_childObj) {
this.recursionremove(_childObj, removeKey)
}
} catch (e) {
console.log(e)
}
}
}
}
},
wxhelper: function () {
try {
this.checkloaddex("com.alibaba.fastjson.JSON", "/data/local/tmp/fastjson.dex")
this.checkloaddex("com.tencent.mm.wechathelperdex.WxHelper", "/data/local/tmp/wxhelper.dex")
var _clz = Java.use("com.tencent.mm.wechathelperdex.WxHelper")
return _clz
} catch (err) {
console.log(err)
}
return "{}"
},
hashset: function () {
var _HashSetClz = Java.use("java.util.HashSet")
var _hashset = _HashSetClz.$new()
return _hashset
},
map: function () {
var _hashMap_clz = Java.use("java.util.HashMap")
var _map_clz = Java.use("java.util.Map")
var _hashMap = _hashMap_clz.$new()
var _map = Java.cast(_hashMap, _map_clz)
return _map
},
base64: function (zsBase64) {
var androidBase64 = Java.use("android.util.Base64")
var _base64 = androidBase64.decode(zsBase64, 0)
return _base64
},
}
var hooker_fun = {
runComment: function (device, reply, args) {},
runDetailVideo: function (device, args) {},
runDetailGoods: function (device, args) {},
runLiveInfo: function (device, username, args) {},
runLiveBarrage: function (device, username, args) {},
runLiveContribution: function(device, username, args) {},
runLiveGoods: function (device, username, args) {},
runLiveSquare: function (device, cate) {},
runLiveTab: function () {},
runLiveBag: function (device, args) {},
runProductList: function (device, selfappid, args) {},
runProductInfo: function (device, selfappid, args) {},
runProductStore: function (device, selfappid, args) {},
runProductTakecenter: function (device, selfappid, cookie, args) {}
runProductThird: function (device, selfappid, cookie, args) {},
runGetcookie: function (device, username) {},
runTopicTopic: function (device, args) {},
runTopicActivity: function (device, tab, args) {},
runVideoGoods: function (device, args) {},
runVideoUrl: function (device, args) {},
}
rpc.exports = {
hookerVideoComment: function (device, reply, args) {
Java.perform(function () {
hooker_fun.runComment(device, reply, args)
})
},
hookerDetailVideo: function (device, args) {
Java.perform(function () {
hooker_fun.runDetailVideo(device, args)
})
},
hookerDetailGoods: function (device, args) {
Java.perform(function () {
hooker_fun.runDetailGoods(device, args)
})
},
hookerLiveInfo: function (device, username, args) {
Java.perform(function () {
hooker_fun.runLiveInfo(device, username, args)
})
},
hookerLiveBarrage: function (device, username, args) {
Java.perform(function () {
hooker_fun.runLiveBarrage(device, username, args)
})
},
hookerLiveContribution: function (device, username, args) {
Java.perform(function () {
hooker_fun.runLiveContribution(device, username, args)
})
},
hookerLiveGoods: function (device, username, args) {
Java.perform(function () {
hooker_fun.runLiveGoods(device, username, args)
})
},
hookerLiveSquare: function (device, cate) {
Java.perform(function () {
hooker_fun.runLiveSquare(device, cate)
})
},
hookerLiveTab: function () {
Java.perform(function () {
hooker_fun.runLiveTab()
})
},
hookerLiveBag: function (device, args) {
Java.perform(function () {
hooker_fun.runLiveBag(device, args)
})
},
hookerProductList: function (device, selfappid, args) {
Java.perform(function () {
hooker_fun.runProductList(device, selfappid, args)
})
},
hookerProductInfo: function (device, selfappid, args) {
Java.perform(function () {
hooker_fun.runProductInfo(device, selfappid, args)
})
},
hookerProductStore: function (device, selfappid, args) {
Java.perform(function () {
hooker_fun.runProductStore(device, selfappid, args)
})
},
hookerProductTakecenter: function (device, selfappid, cookie, args) {
Java.perform(function () {
hooker_fun.runProductTakecenter(device, selfappid, cookie, args)
})
},
hookerProductThird: function (device, selfappid, cookie, args) {
Java.perform(function () {
hooker_fun.runProductThird(device, selfappid, cookie, args)
})
},
hookerGetcookie: function (device, username) {
Java.perform(function () {
hooker_fun.runGetcookie(device, username)
})
},
hookerTopicTopic: function (device, args) {
Java.perform(function () {
hooker_fun.runTopicTopic(device, args)
})
},
hookerTopicActivity: function (device, tab, args) {
Java.perform(function () {
hooker_fun.runTopicActivity(device, tab, args)
})
},
hookerVideoGoods: function (device, args) {
Java.perform(function () {
hooker_fun.runVideoGoods(device, args)
})
},
hookerVersionCheck: function () {
Java.perform(function () {
hooker_fun.runVersionCheck()
})
},
hookerVideoUrl: function (device, args) {
Java.perform(function () {
hooker_fun.runVideoUrl(device, args)
})
},
}
讲解:
从代码中我们可以看到总体有三部分:
A、business:采集流程控制核心
# !/usr/bin/env python
# coding=utf-8
# @Time : 2022/04/20
# @Author : XQE
# @Software: PyCharm
import os
import sys
import time
import json
import random
import config
import requests
sys.path.append("../common")
sys.path.append("../../common")
from extend import Extend
from base_script import BaseScript
from inform_model import Inform
class AdapterBusiness(Extend, BaseScript, Inform):
"""
对hook业务进行适配,分发到对应的业务链上进行hook
"""
Adapter = dict()
def __init__(self, filename, device, url, full_path=""):
Extend.__init__(self)
BaseScript.__init__(self, filename, device, url, full_path)
Inform.__init__(self, config.inform_url)
for i in dir(self):
self.Adapter[i] = getattr(self, i)
def hooker_video_url(self):
"""
hook视频播放地址
"""
self.frida_object.exports.hooker_video_url(
self.device,
self.get_response
)
def hooker_video_goods(self):
"""
hook视频挂链
"""
self.frida_object.exports.hooker_video_goods(
self.device,
self.get_response
)
def hooker_topic_activity(self, tab=1):
"""
hook视频活动
"""
self.frida_object.exports.hooker_topic_activity(
self.device,
tab,
self.get_response
)
def hooker_topic_topic(self):
"""
hook视频话题
"""
self.frida_object.exports.hooker_topic_topic(
self.device,
self.get_response
)
def hooker_getcookie(self):
"""
hook微信cookie
"""
self.frida_object.exports.hooker_getcookie(
"cookie",
"v2_060000231003b20faec8c5e08a18c4dcc60cec31b077b2e0eab771d78025b3800ae7b330f30e@finder"
)
def hooker_product_third(self):
"""
hook带货中心商品详情
"""
self.frida_object.exports.hooker_product_third(
self.device,
"wx2cea70df4257bba8",
self.vx_cookie,
self.get_response
)
def hooker_product_takecenter(self):
"""
hook带货中心商品列表
"""
self.frida_object.exports.hooker_product_takecenter(
self.device,
"wx2cea70df4257bba8",
self.vx_cookie,
self.get_response
)
def hooker_product_store(self):
"""
hook微信小商店信息
"""
self.frida_object.exports.hooker_product_store(
self.device,
"wx34345ae5855f892d",
self.get_response
)
def hooker_product_info(self):
"""
hook微信小商店商品详情
"""
self.frida_object.exports.hooker_product_info(
self.device,
"wx34345ae5855f892d",
self.get_response
)
def hooker_product_list(self):
"""
hook微信小商店商品列表
"""
self.frida_object.exports.hooker_product_list(
self.device,
"wx34345ae5855f892d",
self.get_response
)
def hooker_live_tab(self):
"""
hook直播分类
"""
self.frida_object.exports.hooker_live_tab(self.device)
def hooker_live_square(self):
"""
hook直播广场
"""
for i in self.get_response.get("data").get("cates"):
if i.get("cateid") > 10:
continue
self.check_frida()
self.frida_object.exports.hooker_live_square(
self.device,
{
"VHZ": i.get("cateid"),
"VIa": i.get("catename"),
"VIb": False,
"VIc": False,
"VId": False,
"VIe": False,
"VIf": [],
"VIg": 0,
"data": "",
"includeUnKnownField": False,
"object_id": 0
}
)
self.wait()
def hooker_live_goods(self):
"""
hook直播带货
"""
self.frida_object.exports.hooker_live_goods(
self.device,
self.username,
self.get_response
)
def hooker_live_contribution(self):
"""
hook直播间热度贡献榜
"""
self.frida_object.exports.hooker_live_contribution(
self.device,
self.username,
self.get_response
)
def hooker_live_barrage(self):
"""
hook直播间弹幕
"""
self.frida_object.exports.hooker_live_barrage(
self.device,
self.username,
self.get_response
)
def hooker_live_info(self):
"""
hook直播间信息
"""
self.frida_object.exports.hooker_live_info(
self.device,
self.username,
self.get_response
)
def hooker_detail_goods(self):
"""
hook主页商品
"""
self.frida_object.exports.hooker_detail_goods(
self.device,
self.get_response
)
def hooker_detail_video(self):
"""
hook主页视频
"""
self.frida_object.exports.hooker_detail_video(
self.device,
self.get_response
)
def hooker_video_comment(self):
"""
hook视频评论
"""
self.frida_object.exports.hooker_video_comment(
self.device,
False,
self.get_response
)
def adapter_business(self):
"""
hook业务分发
"""
self.check_frida()
self.get_params()
if self.device == "comment":
self.Adapter["hooker_video_comment"]()
self.wait()
elif self.device == "detail":
self.Adapter["hooker_detail_video"]()
self.wait()
self.check_frida()
self.Adapter["hooker_detail_goods"]()
self.wait()
elif self.device == "hourlist":
if not (self.vx_cookie and os.path.exists(self.cookie_file)):
self.get_cookie()
self.wait_cookie()
content = self.make_hourlist_response(self.get_response)
self.post_response = content
self.Handle[self.device](config.post_url.get(self.device).format(self.uuid, self.addr))
elif self.device == "livebarrage":
self.Adapter["hooker_live_barrage"]()
self.wait()
elif self.device == "liveinfo":
self.Adapter["hooker_live_info"]()
self.wait()
elif self.device == "livecontribution":
self.Adapter["hooker_live_contribution"]()
self.wait()
elif self.device == "livegoods":
self.Adapter["hooker_live_goods"]()
self.wait()
elif self.device == "livesquare":
self.Adapter["hooker_live_tab"]()
self.wait()
self.Adapter["hooker_live_square"]()
elif self.device == "product":
if self.get_response.get("data").get("method") == "GetShopCenter":
self.Adapter["hooker_product_store"]()
elif self.get_response.get("data").get("method") == "BatchGetProductByBizUin":
self.Adapter["hooker_product_list"]()
elif self.get_response.get("data").get("method") == "GetProduct":
self.Adapter["hooker_product_info"]()
elif self.get_response.get("data").get("method") == "EcGetItemList":
if self.vx_cookie:
self.Adapter["hooker_product_takecenter"]()
else:
self.get_cookie()
self.wait_cookie()
self.check_frida()
self.Adapter["hooker_product_takecenter"]()
elif self.get_response.get("data").get("method") == "EcGetItemDetail":
if self.vx_cookie:
self.Adapter["hooker_product_third"]()
else:
self.get_cookie()
self.wait_cookie()
self.check_frida()
self.Adapter["hooker_product_third"]()
else:
self.logger.error("product business undefined method %s ." % self.get_response.get("data").get("method"))
self.wait()
elif self.device == "topic":
if int(self.get_response["data"].get("type")) == 1:
self.Adapter["hooker_topic_topic"]()
elif int(self.get_response["data"].get("type")) == 2:
self.Adapter["hooker_topic_activity"](1)
self.wait()
self.check_frida()
self.Adapter["hooker_topic_activity"](2)
else:
self.logger.error("topic business undefined type %s ." % self.get_response["data"].get("type"))
self.wait()
elif self.device == "videogoods":
self.Adapter["hooker_video_goods"]()
self.wait()
elif self.device == "videourl":
self.Adapter["hooker_video_url"]()
self.wait()
else:
self.logger.error("can't find this device: %s" % self.device)
time.sleep(
random.randint(
self.sleep_time.get(self.device).get("begin"),
self.sleep_time.get(self.device).get("end")
)
)
class HandleBusiness(AdapterBusiness):
"""
对send回来的数进行适配,分发到对应的业务链上处理
"""
Handle = dict()
def __init__(self, filename, device, url, full_path=""):
AdapterBusiness.__init__(self, filename, device, url, full_path)
for i in dir(self):
self.Handle[i] = getattr(self, i)
def cookie(self, post_url):
"""
微信cookie
:param post_url : 数据回调地址
"""
self.flag = True
self.wait_count = 0
self.remove_monitor = False
self.get_cookie(self.post_response.get("result").get("VWA"))
def comment(self, post_url):
"""
视频评论
:param post_url : 数据回调地址
"""
status = requests.post(post_url, json.dumps(self.post_response).encode("utf-8").decode("latin1"))
self.flag = True
self.wait_count = 0
self.remove_monitor = False
self.logger.info(str(self.post_response.get("device")) + str(status))
def detail(self, post_url):
"""
主页信息
:param post_url : 数据回调地址
"""
status = requests.post(post_url, json.dumps(self.post_response).encode("utf-8").decode("latin1"))
self.flag = True
self.wait_count = 0
self.remove_monitor = False
self.logger.info(str(self.post_response.get("device")) + str(status))
def hourlist(self, post_url):
"""
小时榜
:param post_url : 数据回调地址
"""
for c in self.post_response:
c["username"] = self.get_response["data"].get("username")
c["curtime"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time())))
status = requests.post(post_url, json.dumps(c, ensure_ascii=False).encode("utf-8").decode("latin1"))
self.logger.info(str(c.get("device")) + str(status))
def livebarrage(self, post_url):
"""
直播间弹幕
:param post_url : 数据回调地址
"""
status = requests.post(post_url, json.dumps(self.post_response).encode("utf-8").decode("latin1"))
self.flag = True
self.wait_count = 0
self.remove_monitor = False
self.logger.info(str(self.post_response.get("device")) + str(status))
def liveinfo(self, post_url):
"""
直播间信息
:param post_url : 数据回调地址
"""
status = requests.post(post_url, json.dumps(self.post_response).encode("utf-8").decode("latin1"))
if self.device == self.post_response.get("device"):
self.flag = True
self.wait_count = 0
self.remove_monitor = False
self.logger.info(str(self.post_response.get("device")) + str(status))
def livecontribution(self, post_url):
"""
直播间热度贡献榜
:param post_url : 数据回调地址
"""
status = requests.post(post_url, json.dumps(self.post_response).encode("utf-8").decode("latin1"))
self.flag = True
self.wait_count = 0
self.remove_monitor = False
self.logger.info(str(self.post_response.get("device")) + str(status))
def livegoods(self, post_url):
"""
直播间带货
:param post_url : 数据回调地址
"""
status = requests.post(post_url, json.dumps(self.post_response).encode("utf-8").decode("latin1"))
self.flag = True
self.wait_count = 0
self.remove_monitor = False
self.logger.info(str(self.post_response.get("device")) + str(status))
def livesquare(self, post_url):
"""
直播广场
:param post_url : 数据回调地址
"""
status = requests.post(post_url, json.dumps(self.post_response).encode("utf-8").decode("latin1"))
if int(self.post_response.get("type")) == 2:
if int(self.post_response.get("hasnext")) == 0:
self.flag = True
else:
self.wait_count = 0
else:
self.flag = True
self.wait_count = 0
self.logger.info(str(self.post_response.get("device")) + str(status))
def product(self, post_url):
"""
微信小商店和带货中心
:param post_url : 数据回调地址
"""
if self.post_response.get("method") == "EcGetItemList" or self.post_response.get("method") == "EcGetItemDetail":
if self.post_response.get("flag") in ["-330", "-333", "-334"] and os.path.exists(self.cookie_file):
os.remove(self.cookie_file)
self.vx_cookie = None
self.logger.info("the cookie has expired. obtain it again.")
self.flag = True
else:
status = requests.post(post_url, json.dumps(self.post_response).encode("utf-8").decode("latin1"))
self.flag = True
self.logger.info(str(self.post_response.get("device")) + str(status))
else:
if self.post_response.get("method") == "BatchGetProductByBizUin" and self.post_response.get("next_key") == "" and int(
self.post_response.get("page_num")) > 0:
self.flag = True
elif self.post_response.get("method") == "cookie":
self.get_cookie(self.post_response.get("result").get("VWA"))
self.flag = True
else:
status = requests.post(post_url, json.dumps(self.post_response).encode("utf-8").decode("latin1"))
if self.post_response.get("method") == "GetProduct" or self.post_response.get("method") == "GetShopCenter":
self.flag = True
self.logger.info(str(self.post_response.get("device")) + str(status))
self.wait_count = 0
self.remove_monitor = False
def topic(self, post_url):
"""
话题和活动
:param post_url : 数据回调地址
"""
status = requests.post(post_url, json.dumps(self.post_response).encode("utf-8").decode("latin1"))
if int(self.post_response.get("continueFlag")) == 0:
self.flag = True
else:
self.wait_count = 0
self.remove_monitor = False
self.logger.info(str(self.post_response.get("device")) + str(status))
def videogoods(self, post_url):
"""
视频挂链
:param post_url : 数据回调地址
"""
status = requests.post(post_url, json.dumps(self.post_response).encode("utf-8").decode("latin1"))
self.flag = True
self.wait_count = 0
self.remove_monitor = False
self.logger.info(str(self.post_response.get("device")) + str(status))
def videourl(self, post_url):
"""
视频播放地址
:param post_url : 数据回调地址
"""
status = requests.post(post_url, json.dumps(self.post_response).encode("utf-8").decode("latin1"))
self.flag = True
self.wait_count = 0
self.remove_monitor = False
self.logger.info(str(self.post_response.get("device")) + str(status))
def handle_business(self):
"""
send数据适配主逻辑
"""
try:
post_url = config.post_url.get(self.post_response.get("device")).format(self.uuid, self.addr)
except Exception as e:
self.logger.info("currently, it is an internal service")
post_url = None
self.Handle[self.post_response.get("device")](post_url)
以上就是我自己项目的hook核心逻辑和数据处理核心逻辑,大体上来讲就是:从队列拿到hook参数,匹配hook点,对hook到的数据进行处理和回调。
AdapterBusiness:对hook业务进行适配,分发到对应的业务链上进行hook。“adapter_business”该方法是hook的核心,可以看下对应的逻辑:首先self.check_frida()检测frida,如果没有会创建frida对象;然后看书获取hook参数,这里先检测frida再拿参数是为了避免参数流失过多;最后开始匹配hook点,比如“comment”举例:它会匹配“hooker_video_comment”这个hook点,进入方法内部self.frida_object.exports.hooker_video_comment()实现rpc远程调用(好玄学的),进入到javascript中的hook逻辑,最终会把hook到的数据send出去,而python的流程就会被self.wait()这个方法卡住,等待send回来的数据进行分析
HandleBusiness:对send回来的数进行适配,分发到对应的业务链上处理。“handle_business”该方法时数据处理的核心,他会根据send回来的“device”字段进行匹配回调业务,比如“comment”继续举例:此时send回来的数据已被self.post_response接收,回调完成后要让self.wait()方法放行进行下一次hook,于是就设置self.flag、self.wait_count、self.remove_monitor,使其放行,这三个的具体作用源码中有讲述
B、base_script:frida的核心创建逻辑、对外部分交互和用到的模块载入
# !/usr/bin/env python
# coding=utf-8
# @Time : 2022/04/20
# @Author : XQE
# @Software: PyCharm
"""
整个采集脚本的基础类
完整的流程:
1.hook加载类(js加载数据)
2.hook数据类(js获取数据)
"""
import re
import sys
import time
import frida
import config
import requests
sys.path.append("../common")
sys.path.append("../../common")
from logging_model import Logging
from process_model import Process
from ident_model import Ident
from phone_model import Phone
class BaseScript(Logging, Process, Ident, Phone):
flag = False
device = ""
wait_count = 0
frida_object = None
get_response = dict()
post_response = dict()
remove_monitor = False
def __init__(self, filename, device, url, full_path=""):
"""
初始化对象:
:param filename : 工作空间文件名
:param full_path : 完整路径
"""
self.device = device
self.filename = filename
self.timeout = config.timeout
self.frida_host = config.frida_host
self.app_name_en = config.app_name_en
self.app_name_ch = config.app_name_ch
self.hook_script = config.hook_script
Logging.__init__(self, filename, device, full_path)
Process.__init__(self, filename, device + re.search(r"\d+", filename).group(), "../monitor/monitor_conf.ini", full_path)
Ident.__init__(self, filename, full_path)
Phone.__init__(self, config.adb_host, self.app_name_en, config.app_ui)
self.addr = self.get_host_ip()
self.uuid = self.get_workspace_uuid()
self.get_params_url = url
def check_frida(self):
"""
检测frida
"""
if self.frida_object is None:
self.frida_object = self.session()
def session(self):
"""
frida远程植入微信进程
:return: 植入载体
"""
session = None
while session is None:
device = None
try:
manager = frida.get_device_manager()
device = manager.add_remote_device(self.frida_host)
except Exception as e:
self.logger.error("frida connect fail: " + str(e), exc_info=False)
if device is not None:
try:
session = device.attach(self.app_name_en)
self.logger.info("frida attach appname %s" % self.app_name_en)
except Exception as e:
self.logger.error("frida attach %s fail: " % self.app_name_en + str(e), exc_info=False)
try:
session = device.attach(self.app_name_ch)
self.logger.info("frida attach appname %s" % self.app_name_ch)
except Exception as e:
self.logger.error("frida attach %s fail: " % self.app_name_ch + str(e), exc_info=False)
time.sleep(5)
try:
with open(self.hook_script, encoding="utf-8") as f:
script = session.create_script(f.read(), runtime="v8")
script.on("message", self.my_message_handler)
script.load()
self.logger.info("hook script success.")
return script
except Exception as e:
self.logger.error("frida create script fail: " + str(e), exc_info=False)
def wait(self):
"""
限时等待js响应
"""
self.wait_count = 0
self.flag = False
while True:
time.sleep(0.1)
self.wait_count += 1
if self.flag or self.wait_count > self.timeout:
if self.wait_count > self.timeout:
self.logger.info("hook time out ...")
# 当存在风控风险时重启设备
if self.remove_monitor:
self.logger.info("reboot phone ...")
self.reboot()
if self.frida_object:
self.frida_object.unload()
break
self.flag = False
break
def get_params(self):
"""
获取队列消息
"""
while True:
try:
self.get_response = requests.get(self.get_params_url.format(self.uuid, self.addr))
self.get_response.encoding = "utf-8"
self.get_response = self.get_response.json()
self.logger.info(self.get_response)
if self.get_response.get("code") == 0:
self.logger.info("the current business: %s" % self.device)
break
time.sleep(1)
except Exception as e:
self.logger.error("params queue closed: " + str(e), exc_info=False)
以下的前提是:搭好了架构和环境
根据requirements.txt文件中的版本安装好第三方库,指令在该文件末尾,安装时候记得删除最后两行的指令(建议使用外网安装,不然就自己手动网上去找对应的frida==15.1.10的第三方库)
# 项目全局配置
[project]
# 项目目录
path=D:\\工作目录\\cpFrida\\v1.0
# python环境
[python_v]
# python版本
python=3.8
# 监控配置
[monitor]
# 重启时间差
reboot=180
# 工作空间配置
[comment1]
# py等待js最大send时长
timeout=150
# frida IP端口
frida_host=114.114.114.114:10406
# adb IP端口
adb_host=114.114.114.114:10706
# app英文包名
app_name_en=com.tencent.mm
# app中文包名
app_name_ch=微信
# app首页ui
app_ui=com.tencent.mm/com.tencent.mm.ui.LauncherUI
# 工作空间名称
workspace=comment1
# hook脚本
hook_script=vx_hooker_8015.js
# 队列池
get_url={"comment": "http://114.114.114.114:8182/comment/8015/task?uuid={}&host={}", "detail": "http://114.114.114.114:8181/detail/8015/task?uuid={}&host={}", "livebarrage": "http://114.114.114.114:8187/livedanmu/8015/task?uuid={}&ip={}", "liveinfo": "http://114.114.114.114:8187/live/8015/task?uuid={}&host={}", "livecontribution": "http://114.114.114.114:8187/liveonlineuser/8015/task?uuid={}&host={}", "livegoods": "http://114.114.114.114:8187/liveproduct/8015/task?uuid={}&host={}", "livesquare": "http://114.114.114.114:8186/livesquare/8015/task?uuid={}&host={}", "product": "http://114.114.114.114:8180/product/8015/task?uuid={}&ip={}", "topic": "http://114.114.114.114:8181/topic/8015/task?uuid={}&host={}", "videogoods": "http://114.114.114.114:8185/videoproduct/8015/task?uuid={}&host={}", "hourlist": "http://114.114.114.114:8186/liverank/8015/task?uuid={}&host={}", "videourl": "http://114.114.114.114:8184/videourl/7012/task?uuid={}&host={}"}
# 回调池
post_url={"comment": "http://114.114.114.114:8182/comment/8015/callback?uuid={}&host={}", "detail": "http://114.114.114.114:8181/detail/8015/callback?uuid={}&host={}", "livebarrage": "http://114.114.114.114:8187/livedanmu/8015/callback?uuid={}&ip={}", "liveinfo": "http://114.114.114.114:8187/live/8015/callback?uuid={}&host={}", "livecontribution": "http://114.114.114.114:8187/live/8015/callback?uuid={}&host={}", "livegoods": "http://114.114.114.114:8187/live/8015/callback?uuid={}&host={}", "livesquare": "http://114.114.114.114:8186/livesquare/8015/callback?uuid={}&host={}", "product": "http://114.114.114.114:8180/product/8015/callback?uuid={}&ip={}", "topic": "http://114.114.114.114:8181/topic/8015/callback?uuid={}&host={}", "videogoods": "http://114.114.114.114:8185/videoproduct/8015/callback?uuid={}&host={}", "hourlist": "http://114.114.114.114:8186/liverank/8015/callback?uuid={}&host={}", "videourl": "http://114.114.114.114:8184/videourl/7012/callback?uuid={}&host={}"}
# 当前设备username
username=
# 消息发送地址
inform_url=https://open.feishu.cn/open-apis/bot/v2/hook/1234
# 工作空间标识
uuid=19f97342-fc64-40e3-b9d2-e5a48e0b8549
注意:首先是项目路径“project”,一定配置正确;其次如果你有多个手机的话直接仿造device1的配置项在build.ini创建一个新的配置项,以此类推;然后你想要hook那个app自己补上对应的ui名称和包名,当然有些项是专属的,比如username可以不用、inform_url自己的通知api等等,具体看配置文件的说明;最后一定要把build.ini放在deploy文件夹下,写死的
# !/usr/bin/env python
# coding=utf-8
# @Time : 2022/02/23
# @Author : XQE
# @Software: PyCharm
"""
整个采集项目的启动入口
兼容windows和linux系统
"""
import os
import configparser
cmd_str = r"""#!/bin/bash
source /etc/profile
echo "============================== Linux deploy start ! =============================="
crontab -l | grep -v '{project_path}/workspaces/monitor' | crontab -
cd {project_path}/startproject
python3.8 /wechatserver/wxhook/startproject/kill_process.py
cd {project_path}/deploy
python3.8 {project_path}/deploy/deploy.py receive_workspace
python3.8 {project_path}/deploy/deploy.py release_workspace
sleep 3
nohup python3.8 {project_path}/startproject/reboot_phone.py >/dev/null 2>&1 &
sleep 120
if [ -d "{project_path}/workspaces/Process" ]; then
rm -rf {project_path}/workspaces/Process
fi
echo "============================== Linux deploy end ! =============================="
echo "****************************** Start youwant project . ******************************"
cd {project_path}/workspaces/device1
bash {project_path}/workspaces/device1/device1.sh
echo "comment1 start OK ! "
sleep 60
echo "*/5 * * * * . /etc/profile;cd {project_path}/workspaces/monitor && python3.8 {project_path}/workspaces/monitor/monitor.py &" >> /var/spool/cron/root
echo "****************************** Start youwant project OK ! ******************************"
echo "============================== All steps completed ! =============================="
"""
def read_conf(config_path):
"""
读取监控配置文件
:param config_path:配置文件路径
:return:配置文件
"""
config = None
try:
config = configparser.ConfigParser()
config.read(config_path, encoding="utf-8")
except Exception as e:
print("Project is not run .")
return config
def create_shell(start_sh, project_path):
"""
创建项目启动文件
:param start_sh: 项目启动sh文件
:param project_path: 项目目录
:return:
"""
with open(start_sh, mode="w", encoding="utf-8") as f:
f.write(cmd_str.format(project_path=project_path))
if __name__ == '__main__':
project_path = read_conf("../deploy/build.ini")["project"]["path"]
start_sh = os.path.join(os.path.join(project_path, "startproject"), "start_project.sh")
create_shell(start_sh, project_path)
注意:“cmd_str ”这里面的shell脚本才是关键,其他配置我都写好了,只是当你在build.ini添加了新设备的配置项之后,这边也要加上对应的项目进程启动入口,下面有原本有的device1,假如我加了device2配置项:
cd {project_path}/workspaces/device1
bash {project_path}/workspaces/device1/device1.sh
echo "device1 start OK !"
cd {project_path}/workspaces/device1
bash {project_path}/workspaces/device1/device1.sh
echo "device1 start OK !"
cd {project_path}/workspaces/device2
bash {project_path}/workspaces/device1/device2.sh
echo "device2 start OK !"
这样就每个进程池或者进程都十分明显了
在本地调试的话,就不需要用到一键部署了,startproject目录下的文件对你来说就是鸡肋,就很简单了:找到deploy文件夹下的deploy.py,参考3、deploy.py的用法,它会根据你的build.ini自动帮你解压出所有的工作空间,类似上面的device1,直接在本地运行即可
远程部署需要用到Jenkins,这边不做太多的赘述了主页有搭建步骤。我们利用Jenkins拉去代码到远程Linux服务器,在拉完代码之后执行start_project.py文件,它会在startproject目录下生成一个start_project.sh文件,利用bash运行它即可,用过Jenkins的应该指定怎么配置了把(手动滑稽)
会遇到一些路径的问题,一定要记得该项目路径:
github地址
如果觉得有用,拉完就点个赞
以上叭叭叭说了这么多,看到最后的都是牛人,先脸皮厚要个三连吧,不管你看没看懂,创作真的累,分享思想和技术更累,要想着怎么说才能通俗易懂。。。。。。
总结一下吧:文章中提到的技术,在我博客首页都有相关的博客学习,还是那句话:看到就是赚到。架构搭建不是那么麻烦,很多东西都已经封装好了,自己扩展扩展也许就能满足你自己的业务了(哈哈哈哈)动手试试吧。这个代码的思路是我沉淀几个月的时间了,但是我相信还有漏洞或者不完美,欢迎使用后回馈哈,也许你能优化到更加简便,到时候欢迎在评论区留下自己的见解哈。最后关注博主,带你学习一些正常的、奇奇怪怪的技术哈~~~
心情复杂。。。。。。
补充:
v1.0做了些优化,速度会快一些,然后加了些功能
经过这写天的测试,发现v2.0速度要快,但是不是很稳定,app和手机老是重启,可能是多进程同时hook不同点时,app中的一些资源被占用导致的吧,所有这边后续可能只会更新v1.0了