Android获取QQ和微信的聊天记录,并保存到数据库

前言

(该方法只适用于监控自己拥有的微信或者QQ ,无法监控或者盗取其他人的聊天记录。本文只写了如何获取聊天记录,服务器落地程序并不复杂,不做赘述。写的仓促,有错别字还请见谅。)

为了获取黑产群的动态,有同事潜伏在大量的黑产群(QQ 微信)中,干起了无间道的工作。随着黑产群数量的激增,同事希望能自动获取黑产群的聊天信息,并交付风控引擎进行风险评估。于是,我接到了这么一个工作……

分析了一通需求说明,总结一下:

  1. 能够自动获取微信和 QQ群的聊天记录
  2. 只要文字记录,图片和表情包,语音之类的不要
  3. 后台自动运行,非实时获取记录

准备工作

参阅很多相关的文章之后,对这个需求有了大致的想法,开始着手准备:

  1. 一个有root权限的手机,我用的是红米5(强调必须要有ROOT)
  2. android的开发环境
  3. 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中。(这个减号一定要带着!)

图1.png

另外,如果手机是双卡双待,那么会有两个IMEI号,默认选择 IMEI1,如果不行,可以尝试一下字符串‘1234567890ABCDEF’。早期的微信会去判定你的IMEI,如果为空 默认选择这个字符串。

图2.png

拿到密码,就可以打开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);
    }
}

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏TungHsu

【干货】什么?Python3.X不能输出中文?原来是编辑器geany的锅?!

小白说我要开始学Python了,既然3.X支持中文,那就学3吧。于是安装好Python环境,下载好geany就开始写了。 先写个“Hello World”吧,作...

48360
来自专栏AhDung

【VBS】vbs指定编码保存文本文件(含xml、ini什么的)

- 让用户填写一些信息,待安装完成后把这些信息写入软件安装目录中的指定ini、xml文件中

11710
来自专栏Android随笔

Android开发实践

全部使用小写字母。一级包名常见的有:com/cn/org/net,二级包名以公司或个人来命名,三级包名根据应用进行命名,四级包名为模块名或层级名。

19810
来自专栏用户2442861的专栏

Quartz Spring与Spring Task总结

Spring对Quartz作了一个封装,同时,Spring自己也提供了一个任务定时器(spring-task),现把它总结一下。 对于Quartz,我们使...

20610
来自专栏ytkah

如何用excel urldecode解码把url编码转为汉字?

  统计分析可以反映出网站运营的情况,并根据实际作出相应的调整,是站长必需的基础技能。ytkah感觉最好用的是谷歌统计,里面有个搜索关键词及对应受访页面,这个功...

571100
来自专栏Java开发者杂谈

For update带来的思考

​ 之所以想写这个专题,是因为最近在做一个抢占任务的实现。假设数据库很多个任务,在抢占发生之前任务的状态都是FREE。现在假设同时有一堆抢占线程开始工作,抢占线...

14130
来自专栏技术/开源

Enum引发的血案,反思

前几天公司产品更新版本,更新完后不少用户反应原先保存的report的一些表在新版本打开后设置突然变了,本来选的第六个,现在打开变成第四个了。领导要求赶紧查出原因...

20350
来自专栏Java开发者杂谈

分布式改造剧集1

背景介绍 ​ 我所在的项目组,使用的技术一直是接近原始社会的:jdk1.6 + SpringMVC + hessian + Mybatis,当前最火的中间件技术...

30040
来自专栏一枝花算不算浪漫

[Java定时器]用Spring Task实现一个简单的定时器.

55880
来自专栏分布式系统进阶

一个有限状态机的C++实现

我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invi...

88550

扫码关注云+社区

领取腾讯云代金券