Android获取QQ和微信的聊天记录,并保存到数据库
前言
(该方法只适用于监控自己拥有的微信或者QQ ,无法监控或者盗取其他人的聊天记录。本文只写了如何获取聊天记录,服务器落地程序并不复杂,不做赘述。写的仓促,有错别字还请见谅。)
为了获取黑产群的动态,有同事潜伏在大量的黑产群(QQ 微信)中,干起了无间道的工作。随着黑产群数量的激增,同事希望能自动获取黑产群的聊天信息,并交付风控引擎进行风险评估。于是,我接到了这么一个工作……
分析了一通需求说明,总结一下:
- 能够自动获取微信和 QQ群的聊天记录
- 只要文字记录,图片和表情包,语音之类的不要
- 后台自动运行,非实时获取记录
准备工作
参阅很多相关的文章之后,对这个需求有了大致的想法,开始着手准备:
- 一个有root权限的手机,我用的是红米5(强调必须要有ROOT)
- android的开发环境
- android相关的开发经验(我是个PHP,第一次写ANDROID程序,踩了不少坑)
获取微信聊天记录
说明:
微信的聊天记录保存在"/data/data/com.tencent.mm/MicroMsg/c5fb89d4729f72c345711cb*/EnMicroMsg.db"
该文件是加密的数据库文件,需要用到sqlcipher来打开。密码为:MD5(手机的IMEI+微信UIN)的前七位。文件所在的那个乱码文件夹的名称也是一段加密MD5值:MD5('mm'+微信UIN)。微信的UIN存放在微信文件夹/data/data/com.tencent.mmshared_prefs/system_config_prefs.xml中。(这个减号一定要带着!)
另外,如果手机是双卡双待,那么会有两个IMEI号,默认选择 IMEI1,如果不行,可以尝试一下字符串‘1234567890ABCDEF’。早期的微信会去判定你的IMEI,如果为空 默认选择这个字符串。
拿到密码,就可以打开EnMicroMsg.db了。微信聊天记录,包括个人,群组的所有记录全部存在message这张表里。
代码实现
第一步,不可能直接去访问EnMicroMsg.db。没有权限,还要避免和微信本身产生冲突,所以选择把这个文件拷贝到自己的项目下:
oldPath ="/data/data/com.tencent.mm/MicroMsg/c5fb89d4729f72c345711cb*****/EnMicroMsg.db";
newPath ="/data/data/com.你的项目/EnMicroMsg.db";
copyFile(oldPath,newPath);//代码见 部分源码
第二步,拿到文件的密码:
String password = (MD5Until.md5("IMEI+微信UIN").substring(0, 7).toLowerCase());
第三步,打开文件,执行SQL:
SQLiteDatabase.loadLibs(context);
SQLiteDatabaseHook hook = new SQLiteDatabaseHook() {
public void preKey(SQLiteDatabase database) {
}
public void postKey(SQLiteDatabase database) {
database.rawExecSQL("PRAGMA cipher_migrate;");//很重要
}
};
SQLiteDatabase db = openDatabase(newPath, password, null, NO_LOCALIZED_COLLATORS, hook);
long now = System.currentTimeMillis();
Log.e("readWxDatabases", "读取微信数据库:" + now);
int count = 0;
if (msgId != "0") {
String sql = "select * from message";
Log.e("sql", sql);
Cursor c = db.rawQuery(sql, null);
while (c.moveToNext()) {
long _id = c.getLong(c.getColumnIndex("msgId"));
String content = c.getString(c.getColumnIndex("content"));
int type = c.getInt(c.getColumnIndex("type"));
String talker = c.getString(c.getColumnIndex("talker"));
long time = c.getLong(c.getColumnIndex("createTime"));
JSONObject tmpJson = handleJson(_id, content, type, talker, time);
returnJson.put("data" + count, tmpJson);
count++;
}
c.close();
db.close();
Log.e("readWxDatanases", "读取结束:" + System.currentTimeMillis() + ",count:" + count);
}
到此,就可以拿到微信的聊天记录了,之后可以直接将整理好的JSON通过POST请求发到服务器就可以了。(忍不住吐槽:写服务器落地程序用了30分钟,写上面这一坨花了三四天,还不包括搭建开发环境,下载SDK,折腾ADB什么的)
获取QQ聊天记录
说明
QQ的聊天记录有点麻烦。他的文件保存在/data/data/com.tencent.mobileqq/databases/你的QQ号码.db
这个文件是不加密的,可以直接打开。QQ中群组的聊天记录是单独建表存放的,所有的QQ群信息存放在TroopInfoV2表里,需要对字段troopuin求MD5,然后找到他的聊天记录表:mr_troop_" + troopuinMD5 +"_New。
但是!!!
问题来了,它的内容是加密的,而且加密方法还很复杂:根据手机IMEI循环逐位异或。具体的我不举例子了,太麻烦,直接看文章最后的解密方法。
代码实现
第一步,还是拷贝数据库文件。
final String QQ_old_path = "/data/data/com.tencent.mobileqq/databases/QQ号.db";
final String QQ_new_path = "/data/data/com.android.saurfang/QQ号.db";
DataHelp.copyFile(QQ_old_path,QQ_new_path);
第二步,打开并读取内容
SQLiteDatabase.loadLibs(context);
String password = "";
SQLiteDatabaseHook hook = new SQLiteDatabaseHook() {
public void preKey(SQLiteDatabase database) {}
public void postKey(SQLiteDatabase database) {
database.rawExecSQL("PRAGMA cipher_migrate;");
}
};
MessageDecode mDecode = new MessageDecode(imid);
HashMap<String, String> troopInfo = new HashMap<String, String>();
try{
SQLiteDatabase db = openDatabase(newPath,password,null, NO_LOCALIZED_COLLATORS,hook);
long now = System.currentTimeMillis();
Log.e("readQQDatabases","读取QQ数据库:"+now);
//读取所有的群信息
String sql = "select troopuin,troopname from TroopInfoV2 where _id";
Log.e("sql",sql);
Cursor c = db.rawQuery(sql,null);
while (c.moveToNext()){
String troopuin = c.getString(c.getColumnIndex("troopuin"));
String troopname = c.getString(c.getColumnIndex("troopname"));
String name = mDecode.nameDecode(troopname);
String uin = mDecode.uinDecode(troopuin);
Log.e("readQQDatanases","读取结束:"+name);
troopInfo.put(uin, name);
}
c.close();
int troopCount = troopInfo.size();
Iterator<String> it = troopInfo.keySet().iterator();
JSONObject json = new JSONObject();
//遍历所有的表
while(troopCount > 0) {
try{
while(it.hasNext()) {
String troopuin = (String)it.next();
String troopname = troopInfo.get(troopuin);
if(troopuin.length() < 8)
continue;
String troopuinMD5 = getMD5(troopuin);
String troopMsgSql = "select _id,msgData, senderuin, time from mr_troop_" + troopuinMD5 +"_New";
Log.e("sql",troopMsgSql);
Cursor cc = db.rawQuery(troopMsgSql,null);
JSONObject tmp = new JSONObject();
while(cc.moveToNext()) {
long _id = cc.getLong(cc.getColumnIndex("_id"));
byte[] msgByte = cc.getBlob(cc.getColumnIndex("msgData"));
String ss = mDecode.msgDecode(msgByte);
//图片不保留
if(ss.indexOf("jpg") != -1 || ss.indexOf("gif") != -1
|| ss.indexOf("png") != -1 )
continue;
String time = cc.getString(cc.getColumnIndex("time"));
String senderuin = cc.getString(cc.getColumnIndex("senderuin"));
senderuin = mDecode.uinDecode(senderuin);
JSONObject tmpJson = handleQQJson(_id,ss,senderuin,time);
tmp.put(String.valueOf(_id),tmpJson);
}
troopCount--;
cc.close();
}
} catch (Exception e) {
Log.e("e","readWxDatabases"+e.toString());
}
}
db.close();
}catch (Exception e){
Log.e("e","readWxDatabases"+e.toString());
}
然后你就可以把信息发到服务器落地了。
后续
这里还有几个需要注意的地方:
- 最新安卓系统很难写个死循环直接跑了,所以我们需要使用Intent,来开始Service,再通过Service调用AlarmManager。
public class MainActivity extends AppCompatActivity {
private Intent intent;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
intent = new Intent(this, LongRunningService.class);
startService(intent);
}
@Override
protected void onDestroy() {
super.onDestroy();
stopService(intent);
}
}
然后再创建一个LongRunningService,在其中调用AlarmManager。
AlarmManager manager = (AlarmManager) getSystemService(ALARM_SERVICE);
int Minutes = 60*1000; //此处规定执行的间隔时间
long triggerAtTime = SystemClock.elapsedRealtime() + Minutes;
Intent intent1 = new Intent(this, AlarmReceiver.class);//注入要执行的类
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent1, 0);
manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTime, pendingIntent);
return super.onStartCommand(intent, flags, startId);
在AlarmReceiver中调用我们的方法。
//微信部分
postWXMsg.readWXDatabase();
//QQ部分
postQQMsg.readQQDatabase();
//再次开启LongRunningService这个服务,即可实现定时循环。
Intent intentNext = new Intent(context, LongRunningService.class);
context.startService(intentNext);
- 安卓不允许在主线程里进行网络连接,可以直接用 retrofit2 来发送数据。
- 项目需要授权网络连接
- 项目需要引入的包
implementation files('libs/sqlcipher.jar')
implementation files('libs/sqlcipher-javadoc.jar')
implementation 'com.squareup.retrofit2:retrofit:2.0.0'
implementation 'com.squareup.retrofit2:converter-gson:2.0.0'
- 如果复制文件时失败,校验文件路径不存在,多半是因为授权问题。需要对数据库文件授权 全用户rwx权限
- 数据库编码为utf8mb4,用来支持EMOJI表情。
部分源码
(因为种种原因,我不太好直接把源码贴上来。)
复制文件的方法
/**
* 复制单个文件
*
* @param oldPath String 原文件路径 如:c:/fqf.txt
* @param newPath String 复制后路径 如:f:/fqf.txt
* @return boolean
*/
public static boolean copyFile(String oldPath, String newPath) {
deleteFolderFile(newPath, true);
Log.e("copyFile", "time_1:" + System.currentTimeMillis());
InputStream inStream = null;
FileOutputStream fs = null;
try {
int bytesum = 0;
int byteread = 0;
File oldfile = new File(oldPath);
Boolean flag = oldfile.exists();
Log.e("copyFile", "flag:" +flag );
if (oldfile.exists()) { //文件存在时
inStream = new FileInputStream(oldPath); //读入原文件
fs = new FileOutputStream(newPath);
byte[] buffer = new byte[2048];
while ((byteread = inStream.read(buffer)) != -1) {
bytesum += byteread; //字节数 文件大小
fs.write(buffer, 0, byteread);
}
Log.e("copyFile", "time_2:" + System.currentTimeMillis());
}
} catch (Exception e) {
System.out.println("复制单个文件操作出错");
e.printStackTrace();
} finally {
try {
if (inStream != null) {
inStream.close();
}
if (fs != null) {
fs.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
/**
* 删除单个文件
*
* @param filepath
* @param deleteThisPath
*/
public static void deleteFolderFile(String filepath, boolean deleteThisPath) {
if (!TextUtils.isEmpty(filepath)) {
try {
File file = new File(filepath);
if (file.isDirectory()) {
//处理目录
File files[] = file.listFiles();
for (int i = 0; i < file.length(); i++) {
deleteFolderFile(files[i].getAbsolutePath(), true);
}
}
if (deleteThisPath) {
if (!file.isDirectory()) {
//删除文件
file.delete();
} else {
//删除目录
if (file.listFiles().length == 0) {
file.delete();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
MD5方法
public class MD5Until {
public static char HEX_DIGITS[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'A', 'B', 'C', 'D', 'E', 'F'};
//将字符串转化为位
public static String toHexString(byte[] b){
StringBuilder stringBuilder = new StringBuilder(b.length * 2);
for (int i = 0; i < b.length; i++) {
stringBuilder.append(HEX_DIGITS[(b[i] & 0xf0) >>> 4]);
stringBuilder.append(HEX_DIGITS[b[i] & 0x0f]);
}
return stringBuilder.toString();
}
public static String md5(String string){
try {
MessageDigest digest = java.security.MessageDigest.getInstance("MD5");
digest.update(string.getBytes());
byte messageDigest[] = digest.digest();
return toHexString(messageDigest);
}catch (NoSuchAlgorithmException e){
e.printStackTrace();
}
return "";
}
}
QQ信息解密方法
public class MessageDecode {
public String imeiID;
public int imeiLen;
public MessageDecode(String imeiID)
{
this.imeiID = imeiID;
this.imeiLen = imeiID.length();
}
public boolean isChinese(byte ch) {
int res = ch & 0x80;
if(res != 0)
return true;
return false;
}
public String timeDecode(String time)
{
String datetime = "1970-01-01 08:00:00";
SimpleDateFormat sdFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
long second = Long.parseLong(time);
Date dt = new Date(second * 1000);
datetime = sdFormat.format(dt);
} catch (NumberFormatException e) {
e.printStackTrace();
}
return datetime;
}
public String nameDecode(String name)
{
byte nbyte[] = name.getBytes();
byte ibyte[] = imeiID.getBytes();
byte xorName[] = new byte[nbyte.length];
int index = 0;
for(int i = 0; i < nbyte.length; i++) {
if(isChinese(nbyte[i])){
xorName[i] = nbyte[i];
i++;
xorName[i] = nbyte[i];
i++;
xorName[i] = (byte)(nbyte[i] ^ ibyte[index % imeiLen]);
index++;
} else {
xorName[i] = (byte)(nbyte[i] ^ ibyte[index % imeiLen]);
index++;
}
}
return new String(xorName);
}
public String uinDecode(String uin)
{
byte ubyte[] = uin.getBytes();
byte ibyte[] = imeiID.getBytes();
byte xorMsg[] = new byte[ubyte.length];
int index = 0;
for(int i = 0; i < ubyte.length; i++) {
xorMsg[i] = (byte)(ubyte[i] ^ ibyte[index % imeiLen]);
index++;
}
return new String(xorMsg);
}
public String msgDecode(byte[] msg)
{
byte ibyte[] = imeiID.getBytes();
byte xorMsg[] = new byte[msg.length];
int index = 0;
for(int i = 0; i < msg.length; i++) {
xorMsg[i] = (byte)(msg[i] ^ ibyte[index % imeiLen]);
index++;
}
return new String(xorMsg);
}
}
- 剑指OFFER之二叉树中和为某一值的路径(九度OJ1368)
- Python基础03 序列
- Python基础02 基本数据类型
- 用命令重启IIS 常重启IIS的朋友看过来
- Python基础01 Hello World!
- 剑指OFFER之从上往下打印二叉树(九度OJ1523)
- 给你的博客加上“Fork me on Github”彩带
- Android Studio添加PNG图片报错原因
- 剑指OFFER之包含min函数的栈(九度OJ1522)
- 使用VS2010开发Qt程序的一点经验
- 用Qt写软件系列五:一个安全防护软件的制作(3)
- 剑指OFFER之顺时针打印矩阵(九度OJ1391)
- 用Qt写软件系列五:一个安全防护软件的制作(2)
- 2018年值得关注的200场机器学习会议
- java教程
- Java快速入门
- Java 开发环境配置
- Java基本语法
- Java 对象和类
- Java 基本数据类型
- Java 变量类型
- Java 修饰符
- Java 运算符
- Java 循环结构
- Java 分支结构
- Java Number类
- Java Character类
- Java String类
- Java StringBuffer和StringBuilder类
- Java 数组
- Java 日期时间
- Java 正则表达式
- Java 方法
- Java 流(Stream)、文件(File)和IO
- Java 异常处理
- Java 继承
- Java 重写(Override)与重载(Overload)
- Java 多态
- Java 抽象类
- Java 封装
- Java 接口
- Java 包(package)
- Java 数据结构
- Java 集合框架
- Java 泛型
- Java 序列化
- Java 网络编程
- Java 发送邮件
- Java 多线程编程
- Java Applet基础
- Java 文档注释
- Flutter基础widgets教程-SnackBar篇
- Redash 二开 - 后端环境搭建
- Flutter基础widgets教程-Stepper篇
- Flutter基础widgets教程-Switch篇
- 13.深入k8s:Pod 水平自动扩缩HPA及其源码分析
- python 协程新版
- 14.深入k8s:kube-proxy ipvs及其源码分析
- netty
- Flutter基础widgets教程-TabBar篇
- 厉害了!华为将发布国产编程语言,打破国外垄断!
- Flutter基础widgets教程-TabBarView篇
- 求求你不要在用!=null判空了
- Geant4--root和csv文件存储
- 爬虫模拟登录破解无原图滑动验证码
- Flutter基础widgets教程-TabBarView篇