19年年末無聊的時候研究了下微信的機器人,發現並不是很難,當時主要實現了好友、羣消息的實時獲取,以及從微信本地數據庫中拉取朋友圈數據。朋友圈數據的獲取並不難,難的是對數據的解析,因爲數據都是加密存儲的,當時搞了好幾天,後來終於搞定了,現將過程分享出來吧。
1、準備相關環境
hook的過程都在手機上完成。如果手機root過,直接安裝Xposed,沒有root過的話,可以安裝VirtualXposed(以下簡稱VXP),VXP是一個手機虛擬root環境,針對未root手機虛擬一個root的環境,相關應用安裝在VXP裏面,就可以實現需要root權限的相關操作。VXP大家可自行在GIT上面下載安裝,微信安裝7.0.5,並登陸你的微信號。
2、新建一個APP項目並進行xposed配置
打開studio新建一個項目,並配置Xposed的hook信息,如下:
- 配置hook入口
在src.main下新建一個文件夾:assets,然後在assets下新建一個名爲xposed_init的文件,並在這個文件中放入你的hook主目錄,我的是這樣:
com.android.com.yh.gj.xp.XpMain
這個就是告訴xposed進行hook的入口。
- 配置AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.com.yh.gj">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<meta-data
android:name="xposedmodule"
android:value="true" />
<meta-data
android:name="xposeddescription"
android:value="獲取聯繫人" />
<meta-data
android:name="xposedminversion"
android:value="82" />
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
重要是的meta-data裏面的類型
- 配置build-gradle
引入xposed相關包compileOnly 'de.robv.android.xposed:api:82' compileOnly 'de.robv.android.xposed:api:82:sources'
這樣,基本的配置就完成了。
3、hook流程
暴力點,直接hook微信入庫點,消息收到後都要入本地庫,hook到了入庫點,就能拿到消息信息。另外,獲取庫密碼,這樣就能從庫中拉取朋友圈的信息
4、hook本地微信數據庫的密碼及庫文件路勁
public void getDbPwdAndPath(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable{
if(this.checkPwdIsNull()){
XposedHelpers.findAndHookMethod(ConfigUtil.hookDbPackage, loadPackageParam.classLoader, ConfigUtil.bdMethod[1], String.class,
byte[].class, loadPackageParam.classLoader.loadClass(ConfigUtil.bdClasz[0]),
loadPackageParam.classLoader.loadClass(ConfigUtil.bdClasz[1]), int.class,
loadPackageParam.classLoader.loadClass(ConfigUtil.bdClasz[2]), int.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable{
String password = new String((byte[]) param.args[1], "UTF-8");
String path = param.args[0].toString();
if(path.contains(ConfigUtil.wxDbName)){
FileUtil.writeStringToFile(password+"_"+path,ConfigUtil.pdFilePath);
context = AndroidAppHelper.currentApplication().getApplicationContext();
}
}
});
}
}
上面的代碼可以直接拿到微信的本地數據庫的密碼及庫文件路徑,密碼拿到了一切都好辦。
5、hook新消息
/**
* 消息監控
* @param lpp
*/
public void getWeChatMsg(XC_LoadPackage.LoadPackageParam lpp){
XposedHelpers.findAndHookMethod(ConfigUtil.hookDbPackage,lpp.classLoader, ConfigUtil.bdMethod[0],String.class,String.class, ContentValues.class,new XC_MethodHook() {
@Override
protected void afterHookedMethod(final MethodHookParam param) throws Throwable{
context = AndroidAppHelper.currentApplication().getApplicationContext();
//消息監控
new WxMsgMonitor().MsgClass(param,context);
super.afterHookedMethod(param);
}
});
}
有新消息這裏就會調用WxMsgMonitor,WxMsgMonitor的實現如下:
package com.android.com.yh.gj.wx;
import android.content.ContentValues;
import android.content.Context;
import com.alibaba.fastjson.JSON;
import com.android.com.yh.gj.util.ConfigUtil;
import com.android.com.yh.gj.util.FileUtil;
import java.lang.reflect.Method;
import de.robv.android.xposed.XC_MethodHook;
import static de.robv.android.xposed.XposedBridge.log;
public class WxMsgMonitor {
public void MsgClass(XC_MethodHook.MethodHookParam param, Context context) throws Throwable{
String msgType = param.args[0].toString();
log("類型:"+msgType);
log("文本1:"+param.args[1].toString());
log("文本2:"+param.args[2].toString());
if("message".equals(msgType)){
ContentValues values = (ContentValues)param.args[2];//消息信息均在裏面,要用什麼數據直接在裏面獲取就行
String type = values.get("type").toString();
//判斷是否羣消息,如果要區分羣消息,請自行利用此段註釋代碼
// if(talker.contains("@chatroom")){//羣消息
// talker = values.get("content").toString().split(":")[0];
// content = values.get("content").toString().split(":")[1];
// }
FileUtil.writeStringToFile("消息:"+param.args[2].toString(),ConfigUtil.dataFile[3]);
if("1".equals(type)){//文本、表情消息處理
}else if("3".equals(type)){//圖片
FileUtil.writeStringToFile(values.get("imgPath").toString().split("//")[1]+"hd",ConfigUtil.dataFile[4]);
}else if("34".equals(type)){//語音消息處理
FileUtil.writeStringToFile(values.get("imgPath").toString(),ConfigUtil.dataFile[4]);
}else if("43".equals(type)){//視頻消息處理
FileUtil.writeStringToFile(values.get("imgPath").toString(),ConfigUtil.dataFile[4]);
}else if("49".equals(type)){//文件消息處理
}else if("10000".equals(type)){//同意了加好友的申請處理
}else if("436207665".equals(type)){//紅包消息處理
}else if("419430449".equals(type)){//轉賬消息處理
}else if("570425393".equals(type)){//邀請人加入了羣聊消息處理
}
}else if("fmessage_conversation".equals(msgType)){//收到加好友邀請
//信息全部在param.args[2].toString()裏面,用哪些數據自己取
}else if("oplog2".equals(msgType)){//刪除好友監聽到後處理
//信息全部在param.args[2].toString()裏面,用哪些數據自己取
}else if("WxFileIndex2".equals(msgType)){
ContentValues values = (ContentValues)param.args[2];
if(values.get("path").toString().contains(FileUtil.readToString(ConfigUtil.dataFile[4])) && !FileUtil.readToString(ConfigUtil.dataFile[3]).contains("路徑:")){
FileUtil.writeStringToFileForAppend("\n路徑:"+ConfigUtil.localFile[0]+values.get("path").toString(),ConfigUtil.dataFile[3]);
}
}
// else if("MediaDuplication".equals(msgType)){//圖片已保存到手機
// ContentValues values = (ContentValues)param.args[2];
// FileUtil.writeStringToFile( values.get("path").toString(),"/storage/emulated/0/filePath.txt");
// }
}
}
好了,上面實時獲取消息的過程就算完成了,下面說一下從數據庫拉取朋友圈數據
6、朋友圈數據獲取
上面已經拿到了微信本地數據庫的密碼及文件位置了,我們先庫文件複製一份出來:
SQLiteDatabase.loadLibs(context);
String uin = FileUtil.readToString(ConfigUtil.pdFilePath).split("_")[1].split("MicroMsg/")[1].split("/")[0];
try{
RootCmdUtil.shellCommand("chmod 777 -R "+ConfigUtil.wxRootPath);
}catch(Exception e){}
FileUtil.copyFile(ConfigUtil.wxRootPath+uin+ConfigUtil.wxSnsDbName, ConfigUtil.copySnsDbPath);
// log("開始獲取朋友圈數據");
List list = getSnsList(10,context);//一次獲取多少條請自行修改
// log("朋友圈數據獲取成功:"+list);
然後再讀取裏面的內容:
List<Map> list = new ArrayList<Map>();
Map map;
SQLiteDatabaseHook hook = new SQLiteDatabaseHook() {
@Override
public void preKey(SQLiteDatabase database) {}
@Override
public void postKey(SQLiteDatabase database) {
database.rawExecSQL("PRAGMA cipher_migrate;");//兼容2.0的
}
};
android.database.sqlite.SQLiteDatabase db = android.database.sqlite.SQLiteDatabase.openDatabase(ConfigUtil.copySnsDbPath,null,0);
Cursor cursor = db.rawQuery("select snsId,userName,strftime('%Y-%m-%d %H:%M:%S',datetime(createTime, 'unixepoch')) createTime,type,content from SnsInfo order by createTime desc limit "+row+" offset 0",null);
for(int i =0;i<cursor.getColumnCount();i++){//要取哪些字段,自行在這裏打印了查看字段名
// log("cName:"+cursor.getColumnName(i));
}
if(cursor.getCount()>0){
while(cursor.moveToNext()) {
map = new HashMap();
Long snsId = cursor.getLong(cursor.getColumnIndex("snsId"));
String userName = cursor.getString(cursor.getColumnIndex("userName"));
String createTime = cursor.getString(cursor.getColumnIndex("createTime"));
String type = cursor.getString(cursor.getColumnIndex("type"));
byte[] content = cursor.getBlob(cursor.getColumnIndex("content"));
map.put("snsId",snsId);
map.put("userName",userName);
map.put("createTime", createTime);
map.put("type",type);
map.put("content", ParseUtil.parseContent(content,context,type));
list.add(map);
}
}
return list;
}
這樣裏面的數據就拿到了。但是你會發現content的數據是亂的,所以這裏要進行解析。這一步花了幾天時間終於搞定(主要通過反射的方式)。解析如下:
package com.android.com.yh.gj.util;
import android.content.Context;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.LinkedList;
import dalvik.system.DexClassLoader;
import static de.robv.android.xposed.XposedBridge.log;
public class ParseUtil {
public static String parseContent(byte[] bytes, Context context,String type) throws Exception{
String wxApkFile = context.getApplicationInfo().sourceDir;
DexClassLoader dexClassLoader = new DexClassLoader(wxApkFile,context.getDir("dex1",0).getAbsolutePath(),null,context.getClassLoader());
Class timeLineObjects = dexClassLoader.loadClass(ConfigUtil.wxContenClass);
Method method = timeLineObjects.getMethod(ConfigUtil.parseMethod,byte[].class);
Object object = method.invoke(timeLineObjects.newInstance(),bytes);
if("1".equals(type)){
return parseContentImg(object);
}}else{
return "";
}
}
private static String parseContentImg(Object object)throws Exception{
String result = "";
Field[] fields = object.getClass().getFields();
if(fields.length>0){
String imgUrl = "";
for(Field field:fields){
if(field.getType() == String.class && field.getName().equals(ConfigUtil.wxContentVar[0])){
result = "title:"+field.get(object).toString()+";";
continue;
}
if(field.getType().getName().contains(ConfigUtil.hookRootPackageName)){
Field[] fields1 = field.get(object).getClass().getFields();
for(Field field1:fields1){
if(field1.getType().getName().contains(ConfigUtil.wxContentVar[3]) && field1.get(field.get(object)) != null){
LinkedList linkedLists = (LinkedList)field1.get(field.get(object));
for(Object linkedList:linkedLists){
Field[] fields2 = linkedList.getClass().getFields();
}
}
}
}
}
result = result+"url:"+imgUrl;
}
return result;
}
private static String parseContentPureTxt(Object object)throws Exception{
String result = "";
Field[] fields = object.getClass().getFields();
if(fields.length>0){
for(Field field:fields){
if(field.getType() == String.class && field.getName().equals(ConfigUtil.wxContentVar[0])){
result = "title:"+field.get(object).toString();
break;
}
}
}
return result;
}
private static String parseContentArticle(Object object)throws Exception{
String result = "";
Field[] fields = object.getClass().getFields();
if(fields.length>0){
for(Field field:fields){
if(field.getType() == String.class && field.getName().equals(ConfigUtil.wxContentVar[0])){
result = "title:"+field.get(object).toString()+";";
}
}
}
}
if(!result.contains("url:")){
result = result + "url:請在微信客戶端內打開";
}
return result;
}
private static String parseContentVideo(Object object)throws Exception{
String result = "";
Field[] fields = object.getClass().getFields();
if(fields.length>0){
for(Field field:fields){
if(field.getType() == String.class && field.getName().equals(ConfigUtil.wxContentVar[0])){
result = "title:"+field.get(object).toString()+";";
}
if(field.getType().getName().contains(ConfigUtil.hookRootPackageName)){
Field[] fields1 = field.get(object).getClass().getFields();
a:
for(Field field1:fields1){
}
}
}
}
}
}
return result;
}
}
搞定!!!!!!!!!!!!!!!!!!
部分代碼涉及到微信核心,故沒有貼全,如需要學習的,可評論。也可+我v:YY_yhzf