Android如何獲取QQ與微信的聊天記錄并保存到數(shù)據(jù)庫(kù)詳解
前言
提前說明下:(該方法只適用于監(jiān)控自己擁有的微信或者QQ ,無法監(jiān)控或者盜取其他人的聊天記錄。本文只寫了如何獲取聊天記錄,服務(wù)器落地程序并不復(fù)雜,不做贅述。寫的倉(cāng)促,有錯(cuò)別字還請(qǐng)見諒。)
為了獲取黑產(chǎn)群的動(dòng)態(tài),有同事潛伏在大量的黑產(chǎn)群(QQ 微信)中,干起了無間道的工作。隨著黑產(chǎn)群數(shù)量的激增,同事希望能自動(dòng)獲取黑產(chǎn)群的聊天信息,并交付風(fēng)控引擎進(jìn)行風(fēng)險(xiǎn)評(píng)估。于是,我接到了這么一個(gè)工作……
分析了一通需求說明,總結(jié)一下:
- 能夠自動(dòng)獲取微信和 QQ群的聊天記錄
- 只要文字記錄,圖片和表情包,語音之類的不要
- 后臺(tái)自動(dòng)運(yùn)行,非實(shí)時(shí)獲取記錄
準(zhǔn)備工作
參閱很多相關(guān)的文章之后,對(duì)這個(gè)需求有了大致的想法,開始著手準(zhǔn)備:
- 一個(gè)有root權(quán)限的手機(jī),我用的是紅米5(強(qiáng)調(diào)必須要有ROOT)
- android的開發(fā)環(huán)境
- android相關(guān)的開發(fā)經(jīng)驗(yàn)(我是個(gè)PHP,第一次寫ANDROID程序,踩了不少坑)
獲取微信聊天記錄
說明:
微信的聊天記錄保存在"/data/data/com.tencent.mm/MicroMsg/c5fb89d4729f72c345711cb*/EnMicroMsg.db"
該文件是加密的數(shù)據(jù)庫(kù)文件,需要用到sqlcipher來打開。密碼為:MD5(手機(jī)的IMEI+微信UIN)的前七位。文件所在的那個(gè)亂碼文件夾的名稱也是一段加密MD5值:MD5('mm'+微信UIN)。微信的UIN存放在微信文件夾/data/data/com.tencent.mmshared_prefs/system_config_prefs.xml中。(這個(gè)減號(hào)一定要帶著?。?/p>

另外,如果手機(jī)是雙卡雙待,那么會(huì)有兩個(gè)IMEI號(hào),默認(rèn)選擇 IMEI1,如果不行,可以嘗試一下字符串‘1234567890ABCDEF'。早期的微信會(huì)去判定你的IMEI,如果為空 默認(rèn)選擇這個(gè)字符串。

拿到密碼,就可以打開EnMicroMsg.db了。微信聊天記錄,包括個(gè)人,群組的所有記錄全部存在message這張表里。
代碼實(shí)現(xiàn)
第一步,不可能直接去訪問EnMicroMsg.db。沒有權(quán)限,還要避免和微信本身產(chǎn)生沖突,所以選擇把這個(gè)文件拷貝到自己的項(xiàng)目下:
oldPath ="/data/data/com.tencent.mm/MicroMsg/c5fb89d4729f72c345711cb**\***/EnMicroMsg.db"; newPath ="/data/data/com.你的項(xiàng)目/EnMicroMsg.db"; copyFile(oldPath,newPath);//代碼見 部分源碼
第二步,拿到文件的密碼:
String password = (MD5Until.md5("IMEI+微信UIN").substring(0, 7).toLowerCase());
第三步,打開文件,執(zhí)行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", "讀取微信數(shù)據(jù)庫(kù):" + 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", "讀取結(jié)束:" + System.currentTimeMillis() + ",count:" + count);
}
到此,就可以拿到微信的聊天記錄了,之后可以直接將整理好的JSON通過POST請(qǐng)求發(fā)到服務(wù)器就可以了。(忍不住吐槽:寫服務(wù)器落地程序用了30分鐘,寫上面這一坨花了三四天,還不包括搭建開發(fā)環(huán)境,下載SDK,折騰ADB什么的)
獲取QQ聊天記錄
說明
QQ的聊天記錄有點(diǎn)麻煩。他的文件保存在/data/data/com.tencent.mobileqq/databases/你的QQ號(hào)碼.db
這個(gè)文件是不加密的,可以直接打開。QQ中群組的聊天記錄是單獨(dú)建表存放的,所有的QQ群信息存放在TroopInfoV2表里,需要對(duì)字段troopuin求MD5,然后找到他的聊天記錄表:mr_troop_" + troopuinMD5 +"_New。
但是!?。?/p>
問題來了,它的內(nèi)容是加密的,而且加密方法還很復(fù)雜:根據(jù)手機(jī)IMEI循環(huán)逐位異或。具體的我不舉例子了,太麻煩,直接看文章最后的解密方法。
代碼實(shí)現(xiàn)
第一步,還是拷貝數(shù)據(jù)庫(kù)文件。
final String QQ_old_path = "/data/data/com.tencent.mobileqq/databases/QQ號(hào).db"; final String QQ_new_path = "/data/data/com.android.saurfang/QQ號(hào).db"; DataHelp.copyFile(QQ_old_path,QQ_new_path);
第二步,打開并讀取內(nèi)容
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數(shù)據(jù)庫(kù):"+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","讀取結(jié)束:"+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());
}
然后你就可以把信息發(fā)到服務(wù)器落地了。
后續(xù)
這里還有幾個(gè)需要注意的地方:
最新安卓系統(tǒng)很難寫個(gè)死循環(huán)直接跑了,所以我們需要使用Intent,來開始Service,再通過Service調(diào)用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);
}
}
然后再創(chuàng)建一個(gè)LongRunningService,在其中調(diào)用AlarmManager。
AlarmManager manager = (AlarmManager) getSystemService(ALARM_SERVICE); int Minutes = 60*1000; //此處規(guī)定執(zhí)行的間隔時(shí)間 long triggerAtTime = SystemClock.elapsedRealtime() + Minutes; Intent intent1 = new Intent(this, AlarmReceiver.class);//注入要執(zhí)行的類 PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent1, 0); manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTime, pendingIntent); return super.onStartCommand(intent, flags, startId);
在AlarmReceiver中調(diào)用我們的方法。
Log.e("saurfang","測(cè)試定時(shí)任務(wù)----BEGIN");
//微信部分
postWXMsg.readWXDatabase();
//QQ部分
postQQMsg.readQQDatabase();
Log.e("saurfang","測(cè)試定時(shí)任務(wù)----END");
//再次開啟LongRunningService這個(gè)服務(wù),即可實(shí)現(xiàn)定時(shí)循環(huán)。
Intent intentNext = new Intent(context, LongRunningService.class);
context.startService(intentNext);
- 安卓不允許在主線程里進(jìn)行網(wǎng)絡(luò)連接,可以直接用 retrofit2 來發(fā)送數(shù)據(jù)。
- 項(xiàng)目需要授權(quán)網(wǎng)絡(luò)連接
- 項(xiàng)目需要引入的包
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'
如果復(fù)制文件時(shí)失敗,校驗(yàn)文件路徑不存在,多半是因?yàn)槭跈?quán)問題。需要對(duì)數(shù)據(jù)庫(kù)文件授權(quán) 全用戶rwx權(quán)限
部分源碼
(因?yàn)榉N種原因,我不太好直接把源碼貼上來。)
復(fù)制文件的方法
/**
* 復(fù)制單個(gè)文件
*
* @param oldPath String 原文件路徑 如:c:/fqf.txt
* @param newPath String 復(fù)制后路徑 如: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()) { //文件存在時(shí)
inStream = new FileInputStream(oldPath); //讀入原文件
fs = new FileOutputStream(newPath);
byte[] buffer = new byte[2048];
while ((byteread = inStream.read(buffer)) != -1) {
bytesum += byteread; //字節(jié)數(shù) 文件大小
fs.write(buffer, 0, byteread);
}
Log.e("copyFile", "time_2:" + System.currentTimeMillis());
}
} catch (Exception e) {
System.out.println("復(fù)制單個(gè)文件操作出錯(cuò)");
e.printStackTrace();
} finally {
try {
if (inStream != null) {
inStream.close();
}
if (fs != null) {
fs.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
/**
* 刪除單個(gè)文件
*
* @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'};
//將字符串轉(zhuǎn)化為位
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);
}
}
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
- Android Studio和阿里云數(shù)據(jù)庫(kù)實(shí)現(xiàn)一個(gè)遠(yuǎn)程聊天程序
- android Socket實(shí)現(xiàn)簡(jiǎn)單聊天功能以及文件傳輸
- Android 獲取應(yīng)用簽名的實(shí)現(xiàn)
- android 仿微信聊天氣泡效果實(shí)現(xiàn)思路
- Android 應(yīng)用APP加入聊天功能
- 詳解Android 獲取手機(jī)中微信聊天記錄方法
- Android藍(lán)牙通信聊天實(shí)現(xiàn)發(fā)送和接受功能
- Android中基于XMPP協(xié)議實(shí)現(xiàn)IM聊天程序與多人聊天室
- Android實(shí)現(xiàn)聊天界面
- Android?Studio實(shí)現(xiàn)智能聊天
相關(guān)文章
Android實(shí)現(xiàn)應(yīng)用程序的閃屏效果
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)應(yīng)用程序的閃屏效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-07-07
Android studio 如何刪除項(xiàng)目 module
本篇文章主要介紹了Android studio 如何刪除項(xiàng)目module的相關(guān)知識(shí),具有很好的參考價(jià)值。下面跟著小編一起來看下吧2017-05-05
Android實(shí)現(xiàn)笑臉進(jìn)度加載動(dòng)畫
這篇文章主要介紹了Android實(shí)現(xiàn)笑臉進(jìn)度加載動(dòng)畫的方法,幫助大家更好的理解和學(xué)習(xí)使用Android,感興趣的朋友可以了解下2021-05-05
Android TabLayout設(shè)置指示器寬度的方法
本篇文章主要介紹了Android TabLayout設(shè)置指示器寬度的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-04-04
Android實(shí)現(xiàn)在一個(gè)activity中添加多個(gè)listview的方法
這篇文章主要介紹了Android實(shí)現(xiàn)在一個(gè)activity中添加多個(gè)listview的方法,分析了Activity中添加listview的原理與具體實(shí)現(xiàn)方法,需要的朋友可以參考下2016-08-08
Android編程自定義搜索框?qū)崿F(xiàn)方法【附demo源碼下載】
這篇文章主要介紹了Android編程自定義搜索框?qū)崿F(xiàn)方法,涉及Android界面布局、數(shù)據(jù)加載、事件響應(yīng)等相關(guān)操作技巧,并附帶完整demo源碼供讀者下載參考,需要的朋友可以參考下2017-12-12
Android布局之GridLayout網(wǎng)格布局
網(wǎng)格布局標(biāo)簽是GridLayout。這個(gè)布局是android4.0新增的布局。這個(gè)布局只有4.0之后的版本才能使用。本文給大家介紹Android布局之GridLayout網(wǎng)格布局相關(guān)知識(shí),感興趣的朋友一起學(xué)習(xí)吧2015-12-12
深入淺出學(xué)習(xí)Android ListView基礎(chǔ)
這篇文章主要介紹了深入淺出的帶領(lǐng)大家學(xué)習(xí)Android ListView基礎(chǔ),ListView是安卓里常用的控件,本文介紹一下常用用法,以及優(yōu)化等方法,感興趣的小伙伴們可以參考一下2016-01-01

