一、引言
特別說明:本文內容僅用於即時通信技術研究和學習之用,請勿用於非法用途。如本文內容有不妥之處,請聯繫JackJiang進行處理!php
我司有關部門爲了獲取黑產羣的動態,有同事潛伏在大量的黑產羣(QQ羣、微信羣)中,幹起了無間道的工做。隨着黑產羣數量的激增,同事但願能自動獲取黑產羣的聊天信息,並交付風控引擎進行風險評估。因而,這個工做就交給我了,是時候表現一波了……html
針對同事的需求,分析了一通,總結一下:java
1)可以自動獲取微信和 QQ羣的聊天記錄;android
2)只要文字記錄,圖片和表情包,語音之類的不要;git
3)後臺自動運行,非實時獲取記錄。程序員
(注:本文讀取聊天記錄的方法只適用於監控本身擁有的微信或者QQ ,沒法監控或者盜取其餘人的聊天記錄。本文只寫瞭如何獲取聊天記錄,服務器落地程序並不複雜,不作贅述。寫的倉促,有錯別字還請見諒。)github
學習交流:sql
- 即時通信開發交流3羣:185926912[推薦]數據庫
- 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM》json
(本文同步發佈於:http://www.52im.net/thread-1992-1-1.html)
二、相關文章
即時通信網以前整理過微信本地數據庫的讀取和樣本,若有興趣可請往閱讀:
《微信本地數據庫破解版(含iOS、Android),僅供學習研究 [附件下載]》
三、準備工做
參閱不少相關的文章以後,對這個需求有了大體的想法,開始着手準備:
1)須要一個有root權限的Android手機,我用的是紅米5(強調必須已被ROOT);
2)android的開發環境(就是Android Studio那一套啦);
3)android相關的開發經驗(我是個PHP,第一次寫Android程序,踩了很多坑)。
四、獲取微信聊天記錄過程分享
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」中。(這個減號必定要帶着!)
另外:即時通信網以前整理過微信本地數據庫的樣本,若有興趣可請往下載:《微信本地數據庫破解版(含iOS、Android),僅供學習研究 [附件下載]》。
注意:若是手機是雙卡雙待,那麼會有兩個IMEI號,默認選擇 IMEI1,若是不行,能夠嘗試一下字符串‘1234567890ABCDEF’。早期的微信會去斷定你的IMEI,若是爲空 默認選擇這個字符串。
拿到密碼,就能夠打開EnMicroMsg.db了。微信聊天記錄,包括我的、羣組的全部記錄所有存在message這張表裏(以下圖所示),就像下面這兩張截圖裏展現的同樣。
(爲了方便截圖,此圖截自《微信本地數據庫破解版(含iOS、Android),僅供學習研究 [附件下載]》中的樣本)
(爲了方便截圖,此圖截自《微信本地數據庫破解版(含iOS、Android),僅供學習研究 [附件下載]》中的樣本)
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什麼的)。
五、獲取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());
}
而後你就能夠把信息發到服務器落地了(一樣跟微信的記錄上傳同樣,經過你自已寫的代碼發送到你的服務端就能夠了)。
六、題外話:一些注意點
這裏還有幾個須要注意的地方。
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表情。。
七、個人部分源碼
(由於種種緣由,我不太好直接把源碼貼上來,現把幾個實用方法分享出來,能夠直接使用。)
複製文件的方法:
/**
* 複製單個文件
*
* @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的技術文章彙總
《騰訊技術分享:騰訊是如何大幅下降帶寬和網絡流量的(圖片壓縮篇)》
《騰訊技術分享:騰訊是如何大幅下降帶寬和網絡流量的(音視頻技術篇)》
《騰訊技術分享:Android版手機QQ的緩存監控與優化實踐》
《微信團隊分享:iOS版微信的高性能通用key-value組件技術實踐》
《微信團隊分享:iOS版微信是如何防止特殊字符致使的炸羣、APP崩潰的?》
《騰訊技術分享:Android手Q的線程死鎖監控系統技術實踐》
《QQ音樂團隊分享:Android中的圖片壓縮技術詳解(上篇)》
《QQ音樂團隊分享:Android中的圖片壓縮技術詳解(下篇)》
《騰訊團隊分享 :一次手Q聊天界面中圖片顯示bug的追蹤過程分享》
《微信團隊分享:微信Android版小視頻編碼填過的那些坑》
《微信團隊披露:微信界面卡死超級bug「15。。。。」的前因後果》
《月活8.89億的超級IM微信是如何進行Android端兼容測試的》
《微信客戶端團隊負責人技術訪談:如何着手客戶端性能監控和優化》
《微信團隊原創分享:Android版微信的臃腫之困與模塊化實踐之路》
《微信團隊原創分享:微信客戶端SQLite數據庫損壞修復實踐》
《騰訊原創分享(一):如何大幅提高移動網絡下手機QQ的圖片傳輸速度和成功率》
《騰訊原創分享(二):如何大幅壓縮移動網絡下APP的流量消耗(下篇)》
《騰訊原創分享(三):如何大幅壓縮移動網絡下APP的流量消耗(上篇)》
《如約而至:微信自用的移動端IM網絡層跨平臺組件庫Mars已正式開源》
《開源libco庫:單機千萬鏈接、支撐微信8億用戶的後臺框架基石 [源碼下載]》
《微信新一代通訊安全解決方案:基於TLS1.3的MMTLS詳解》
《微信團隊原創分享:Android版微信後臺保活實戰分享(進程保活篇)》
《微信團隊原創分享:Android版微信後臺保活實戰分享(網絡保活篇)》
《Android版微信從300KB到30MB的技術演進(PPT講稿) [附件下載]》
《微信團隊原創分享:Android版微信從300KB到30MB的技術演進》
《微信技術總監談架構:微信之道——大道至簡(PPT講稿) [附件下載]》
《微信海量用戶背後的後臺系統存儲架構(視頻+PPT) [附件下載]》
《微信異步化改造實踐:8億月活、單機千萬鏈接背後的後臺解決方案》
《架構之道:3個程序員成就微信朋友圈日均10億發佈量[有視頻]》
《微信團隊原創分享:Android內存泄漏監控和優化技巧總結》
《微信團隊原創Android資源混淆工具:AndResGuard [有源碼]》
《移動端IM實踐:Android版微信如何大幅提高交互性能(一)》
《移動端IM實踐:Android版微信如何大幅提高交互性能(二)》
《移動端IM實踐:WhatsApp、Line、微信的心跳策略分析》
《移動端IM實踐:谷歌消息推送服務(GCM)研究(來自微信)》
《信鴿團隊原創:一塊兒走過 iOS10 上消息推送(APNS)的坑》
《騰訊TEG團隊原創:基於MySQL的分佈式數據庫TDSQL十年鍛造經驗分享》
《微信多媒體團隊訪談:音視頻開發的學習、微信的音視頻技術和挑戰等》
《瞭解iOS消息推送一文就夠:史上最全iOS Push技術詳解》
《騰訊資深架構師乾貨總結:一文讀懂大型分佈式系統設計的方方面面》
《騰訊音視頻實驗室:使用AI黑科技實現超低碼率的高清實時視頻聊天》
《騰訊技術分享:微信小程序音視頻與WebRTC互通的技術思路和實踐》
《手把手教你讀取Android版微信和手Q的聊天記錄(僅做技術研究學習)》
>> 更多同類文章 ……
(本文同步發佈於:http://www.52im.net/thread-1992-1-1.html)