1. 程式人生 > >手把手教你讀取Android版微信和手Q的聊天記錄(僅作技術研究學習)

手把手教你讀取Android版微信和手Q的聊天記錄(僅作技術研究學習)

1、引言

特別說明:本文內容僅用於即時通訊技術研究和學習之用,請勿用於非法用途。如本文內容有不妥之處,請聯絡作者進行處理!

我司有關部門為了獲取黑產群的動態,有同事潛伏在大量的黑產群(QQ群、微信群)中,幹起了無間道的工作。隨著黑產群數量的激增,同事希望能自動獲取黑產群的聊天資訊,並交付風控引擎進行風險評估。於是,這個工作就交給我了,是時候表現一波了……

針對同事的需求,分析了一通,總結一下:

1)能夠自動獲取微信和 QQ群的聊天記錄;

2)只要文字記錄,圖片和表情包,語音之類的不要;

3)後臺自動執行,非實時獲取記錄。

注:本文讀取聊天記錄的方法只適用於監控自己擁有的微信或者QQ ,無法監控或者盜取其他人的聊天記錄。本文只寫瞭如何獲取聊天記錄,伺服器落地程式並不複雜,不做贅述。寫的倉促,有錯別字還請見諒。)

學習交流:

2、相關文章

即時通訊網之前整理過微信本地資料庫的讀取和樣本,如有興趣可請往閱讀:

3、準備工作

參閱很多相關的文章之後,對這個需求有了大致的想法,開始著手準備:

1)需要一個有root許可權的Android手機,我用的是紅米5(強調必須已被ROOT);

2)android的開發環境(就是Android Studio那一套啦);

3)android相關的開發經驗(我是個PHP,第一次寫Android程式,踩了不少坑)。

4、獲取微信聊天記錄過程分享

4.1 著手準備

微信的聊天記錄儲存在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這張表裡(如下圖所示),就像下面這兩張截圖裡展示的一樣。

4.2 程式碼實現

第一步,不可能直接去訪問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 = newSQLiteDatabaseHook() {

    publicvoidpreKey(SQLiteDatabase database) {

    }

    publicvoidpostKey(SQLiteDatabase database) {

        database.rawExecSQL("PRAGMA cipher_migrate;");//很重要

    }

};

SQLiteDatabase db = openDatabase(newPath, password, null, NO_LOCALIZED_COLLATORS, hook);

    longnow = System.currentTimeMillis();

    Log.e("readWxDatabases", "讀取微信資料庫:"+ now);

    intcount = 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"));

            inttype = c.getInt(c.getColumnIndex("type"));

            String talker = c.getString(c.getColumnIndex("talker"));

            longtime = 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什麼的)。

5、獲取QQ聊天記錄過程分享

5.1 說明

QQ的聊天記錄有點麻煩,他的檔案儲存在:“/data/data/com.tencent.mobileqq/databases/你的QQ號碼.db”。

這個檔案是不加密的,可以直接開啟。QQ中群組的聊天記錄是單獨建表存放的,所有的QQ群資訊存放在TroopInfoV2表裡,需要對欄位troopuin求MD5,然後找到他的聊天記錄表:mr_troop_" + troopuinMD5 +"_New。

但是!(看到“但是”就沒好事。。。)

問題來了,它的內容是加密的,而且加密方法還很複雜:根據手機IMEI迴圈逐位異或。具體的我不舉例子了,太麻煩,直接看文章最後的解密方法。

5.2 程式碼實現

第一步,還是拷貝資料庫檔案:

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 = newSQLiteDatabaseHook() {

    publicvoidpreKey(SQLiteDatabase database) {}

    publicvoidpostKey(SQLiteDatabase database) {

        database.rawExecSQL("PRAGMA cipher_migrate;");

    }

};

 MessageDecode mDecode = newMessageDecode(imid);

HashMap<String, String> troopInfo = newHashMap<String, String>();

try{

    SQLiteDatabase db = openDatabase(newPath,password,null, NO_LOCALIZED_COLLATORS,hook);

    longnow = 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();

    inttroopCount = troopInfo.size();

    Iterator<String> it = troopInfo.keySet().iterator();

    JSONObject json = newJSONObject();

    //遍歷所有的表

    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 = newJSONObject();

                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());

}

然後你就可以把資訊發到伺服器落地了(同樣跟微信的記錄上傳一樣,通過你自已寫的程式碼傳送到你的服務端就可以了)。

6、題外話:一些注意點

這裡還有幾個需要注意的地方。

1)最新安卓系統很難寫個死迴圈直接跑了,所以我們需要使用Intent,來開始Service,再通過Service呼叫AlarmManager,就像下面的程式碼這樣:

publicclassMainActivity extendsAppCompatActivity {

    privateIntent intent;

    @Override

    protectedvoidonCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity\_main);

        intent = newIntent(this, LongRunningService.class);

        startService(intent);

    }

    @Override

    protectedvoidonDestroy() {

        super.onDestroy();

        stopService(intent);

    }

}

然後再建立一個LongRunningService,在其中呼叫AlarmManager:

AlarmManager manager = (AlarmManager) getSystemService(ALARM_SERVICE);

intMinutes = 60*1000; //此處規定執行的間隔時間

longtriggerAtTime = SystemClock.elapsedRealtime() + Minutes;

Intent intent1 = newIntent(this, AlarmReceiver.class);//注入要執行的類

PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent1, 0);

manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTime, pendingIntent);

returnsuper.onStartCommand(intent, flags, startId);

在AlarmReceiver中呼叫我們的方法:

//微信部分

postWXMsg.readWXDatabase();

//QQ部分

postQQMsg.readQQDatabase();

//再次開啟LongRunningService這個服務,即可實現定時迴圈。

Intent intentNext = newIntent(context, LongRunningService.class);

context.startService(intentNext);

2)安卓不允許在主執行緒裡進行網路連線,可以直接用 retrofit2 來發送資料(或者最簡單的方法就是用AsyncTask了)。

3)專案需要授權網路連線(就是在AndroidManifast.xml里加上網路許可權申請就是了);

4)專案需要引入的包:

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'

5)如果複製檔案時失敗,校驗檔案路徑不存在,多半是因為授權問題。需要對資料庫檔案授權 全使用者rwx許可權;

6)如果服務端使用MySql資料庫的話,資料庫編碼請用utf8mb4編碼,用來支援Emoji表情。。

7、我的部分原始碼

(因為種種原因,我不太好直接把原始碼貼上來,現把幾個實用方法分享出來,可以直接使用。)

複製檔案的方法:

/**

  * 複製單個檔案

  *

  * @param oldPath String 原檔案路徑 如:c:/fqf.txt

  * @param newPath String 複製後路徑 如:f:/fqf.txt

  * @return boolean

  */

 publicstaticbooleancopyFile(String oldPath, String newPath) {

     deleteFolderFile(newPath, true);

     Log.e("copyFile", "time_1:"+ System.currentTimeMillis());

     InputStream inStream = null;

     FileOutputStream fs = null;

     try{

         intbytesum = 0;

         intbyteread = 0;

         File oldfile = newFile(oldPath);

         Boolean flag = oldfile.exists();

         Log.e("copyFile", "flag:"+flag );

         if(oldfile.exists()) { //檔案存在時

             inStream = newFileInputStream(oldPath); //讀入原檔案

             fs = newFileOutputStream(newPath);

             byte[] buffer = newbyte[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();

         }

     }

     returntrue;

 }

 /**

  * 刪除單個檔案

  *

  * @param filepath

  * @param deleteThisPath

  */

 publicstaticvoiddeleteFolderFile(String filepath, booleandeleteThisPath) {

     if(!TextUtils.isEmpty(filepath)) {

         try{

             File file = newFile(filepath);

             if(file.isDirectory()) {

                 //處理目錄

                 File files[] = file.listFiles();

                 for(inti = 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方法:

publicclassMD5Until {

    publicstaticcharHEX_DIGITS[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',

            'A', 'B', 'C', 'D', 'E', 'F'};

    //將字串轉化為位

    publicstaticString toHexString(byte[] b){

        StringBuilder stringBuilder = newStringBuilder(b.length * 2);

        for(inti = 0; i < b.length; i++) {

            stringBuilder.append(HEX_DIGITS[(b[i] & 0xf0) >>> 4]);

            stringBuilder.append(HEX_DIGITS[b[i] & 0x0f]);

        }

        returnstringBuilder.toString();

    }

    publicstaticString md5(String string){

        try{

            MessageDigest digest = java.security.MessageDigest.getInstance("MD5");

            digest.update(string.getBytes());

            bytemessageDigest[] = digest.digest();

            returntoHexString(messageDigest);

        }catch(NoSuchAlgorithmException e){

            e.printStackTrace();

        }

        return"";

    }

}

QQ資訊解密方法:

public class MessageDecode {

    public String imeiID;

    public intimeiLen;

    public MessageDecode(String imeiID)

    {

        this.imeiID = imeiID;

        this.imeiLen = imeiID.length();

    }

    public boolean isChinese(bytech) {

        intres = ch & 0x80;

        if(res != 0)

            returntrue;

        returnfalse;

    }

    public String timeDecode(String time)

    {

        String datetime = "1970-01-01 08:00:00";

        SimpleDateFormat sdFormat = newSimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        try{

            longsecond = Long.parseLong(time);

            Date dt = newDate(second * 1000);

            datetime = sdFormat.format(dt);

        } catch(NumberFormatException e) {

            e.printStackTrace();

        }

        returndatetime;

    }

    public String nameDecode(String name)

    {

        bytenbyte[] = name.getBytes();

        byteibyte[] = imeiID.getBytes();

        bytexorName[] = newbyte[nbyte.length];

        intindex = 0;

        for(inti = 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)

    {

        byteubyte[] = uin.getBytes();

        byteibyte[] = imeiID.getBytes();

        bytexorMsg[] = newbyte[ubyte.length];

        intindex = 0;

        for(inti = 0; i < ubyte.length; i++) {

            xorMsg[i] = (byte)(ubyte[i] ^ ibyte[index % imeiLen]);

            index++;

        }

        returnnewString(xorMsg);

    }

    public String msgDecode(byte[] msg)

    {

        byteibyte[] = imeiID.getBytes();

        bytexorMsg[] = newbyte[msg.length];

        intindex = 0;

        for(int i = 0; i < msg.length; i++) {

            xorMsg[i] = (byte)(msg[i] ^ ibyte[index % imeiLen]);

            index++;

        }

        return new String(xorMsg);

    }

}

附錄:有關微信、QQ的技術文章彙總