【原创内容,欢迎转载,请注明出处】
在电子数据取证过程中,对微信本地数据库的解密、提取与恢复是非常重要的工作内容。本文以华为mate系列手机和最新版的微信(7.0.3)为例,通过总结互联网上已经发表的文章经验,主要针对**华为手机备份软件升级、微信7.0以后索引库加密以及通过索引库恢复被删除聊天记录**等内容予以补充。
网上有很多关于安卓微信本地数据库(7.0版本以前主要是EnMicroMsg.db)的解密教程,以及恢复已删除聊天记录的原理教程,由于微信的不断升级,很多教程的内容已经不符合实际需要了,通过验证,发现以下经验仍然可用:
1. **EnMicroMsg.db密码算法**没有变化 ,这个密码算法仍然是IMEI与uin拼接后计算32位MD5值,然后取前7位(如果uin是负值也不需要变化);
2. **IMEI和最后一次登录的uin提取方法**没有变化,IMEI在CompatibleInfo.cfg文件中,最后一次登录的uin在system_conf_prefs.xml文件中;
3.微信用户数据存储目录名仍然是“mm+uin”顺序拼接的MD5值;
随着安卓操作系统的不断升级,安全性越来越高,想通过root等方式获取手机存储镜像然后再进行数据恢复的方式越来越难了,因此大多数电子数据取证的厂家是通过手机厂商官方备份文件对手机数据在本机存储中备份后,通过导出的备份文件来进行数据提取和恢复。
问题一:华为手机的官方备份软件在8.0版本以后**不再支持**本机存储备份,需要通过OTG转接头在外部存储中备份。很多厂家采取的方法是对备份软件降级,然后仍然在本机存储备份后导出。另外8.0以前的备份是以sqlite数据库(.db)的形式存储的,而8.0以后的备份是以压缩文件(.tar)的形式存储的。对于手工分析来讲,新的备份机制更容易操作。但是如果是旧的备份方式,需要把存在数据库里的文件导出才能进行下一步工作。
问题二:微信7.0以后,对几个以前没有加密的数据库文件(尤其是对恢复数据最重要的索引库)进行了加密,而且经测试,所使用的密码**不是**EnMicroMsg.db加密所使用的密码。
问题三:网上很多文章对于通过索引库恢复被删除的聊天记录说明不够详细。
使用低版本华为备份软件得到的微信备份文件为一个com.tencent.mm.apk文件和一个com.tencent.mm.db文件。
用sqlite数据库管理工具打开com.tencent.mm.db,发现只有三个表,其中apk_file_info表中储存了所有文件名和索引号,apk_file_data中则存储了文件数据。索引号为-1的是目录,索引号大于0的是有用的文件。
在apk_file_data中索引号相同的是同一个文件,每个文件被切成若干个8K以内的碎片进行存储,导出时需要拼接起来再导出。
导出文件数据的python代码如下:
import sqlite3
import os
conn = sqlite3.connect('com.tencent.mm.db')
cursor = conn.cursor()
cursor.execute("SELECT count(*) FROM apk_file_info")
all = cursor.fetchone()[0]
cursor.execute("SELECT file_path,file_index FROM apk_file_info")
result = cursor.fetchall()
count = 0
while (count < all):
if (result[count][1] > 0):
fullname = result[count][0]
findex = result[count][1]
dirname,filename = os.path.split(fullname)
fpath="." + dirname
fname="." + fullname
isExists=os.path.exists(fpath)
if not isExists:
os.makedirs(fpath)
with open(fname, "wb") as output_file:
cursor.execute("SELECT count(*) FROM apk_file_data WHERE file_index = " + str(findex))
total = cursor.fetchone()[0]
cursor.execute("SELECT file_data FROM apk_file_data WHERE file_index = " + str(findex))
acount=0
while (acount < total):
ablob = cursor.fetchone()
output_file.write(ablob[0])
acount = acount+1
count = count + 1
cursor.close()
conn.close()
import sqlite3
import os
conn = sqlite3.connect('com.tencent.mm.db')
cursor = conn.cursor()
cursor.execute("SELECT count(*) FROM apk_file_info")
all = cursor.fetchone()[0]
cursor.execute("SELECT file_path,file_index FROM apk_file_info")
result = cursor.fetchall()
count = 0
while (count < all):
if (result[count][1] > 0):
fullname = result[count][0]
findex = result[count][1]
dirname,filename = os.path.split(fullname)
fpath="." + dirname
fname="." + fullname
isExists=os.path.exists(fpath)
if not isExists:
os.makedirs(fpath)
with open(fname, "wb") as output_file:
cursor.execute("SELECT count(*) FROM apk_file_data WHERE file_index = " + str(findex))
total = cursor.fetchone()[0]
cursor.execute("SELECT file_data FROM apk_file_data WHERE file_index = " + str(findex))
acount=0
while (acount < total):
ablob = cursor.fetchone()
output_file.write(ablob[0])
acount = acount+1
count = count + 1
cursor.close()
conn.close()
将代码保存为out.py后与com.tencent.mm.db文件放在同一目录下,python out.py运行即可在当前目录下导出所有文件。生成目录为data/data/com.tencent.mm。
比较懒,没有加注释和提示信息。实际使用时请自行添加提示信息和异常处理代码。如果导出文件数据较多程序效率比较低,可自行优化,代码仅供参考。
首先需要解密EnMicroMsg.db,以便提取微信id。因为CompatibleInfo.cfg是通过java的HashMap编码的,因此从此文件中提取IMEI值需要解码。没找到python解码java HashMap的代码,所以就用java代码凑合一下。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.security.MessageDigest;
import java.util.HashMap;
public class GetDBKey {
public static void main(String[] args) {
try {
ObjectInputStream in = new ObjectInputStream(new FileInputStream("CompatibleInfo.cfg"));
Object DL = in.readObject();
HashMap hashWithOutFormat = (HashMap) DL;
String s = String.valueOf(hashWithOutFormat.get(Integer.valueOf(258))); // 取手机的IMEI
System.out.println("IMEI:"+s);
ObjectInputStream in1 = new ObjectInputStream(new FileInputStream("systemInfo.cfg"));
Object DJ = in1.readObject();
HashMap hashWithOutFormat1 = (HashMap) DJ;
String t = String.valueOf(hashWithOutFormat1.get(Integer.valueOf(1))); // 取微信的uin
System.out.println("uin:"+t);
s = s + t; //合并到一个字符串
s = encode(s); // MD5
System.out.println("密码是 : " + s.substring(0, 7));
in.close();
in1.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static String encode(String content)
{
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(content.getBytes());
return getEncode32(digest);
}
catch (Exception e)
{
e.printStackTrace();
}
return null;
}
private static String getEncode32(MessageDigest digest)
{
StringBuilder builder = new StringBuilder();
for (byte b : digest.digest())
{
builder.append(Integer.toHexString((b >> 4) & 0xf));
builder.append(Integer.toHexString(b & 0xf));
}
return builder.toString();
}
}
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.security.MessageDigest;
import java.util.HashMap;
public class GetDBKey {
public static void main(String[] args) {
try {
ObjectInputStream in = new ObjectInputStream(new FileInputStream("CompatibleInfo.cfg"));
Object DL = in.readObject();
HashMap hashWithOutFormat = (HashMap) DL;
String s = String.valueOf(hashWithOutFormat.get(Integer.valueOf(258))); // 取手机的IMEI
System.out.println("IMEI:"+s);
ObjectInputStream in1 = new ObjectInputStream(new FileInputStream("systemInfo.cfg"));
Object DJ = in1.readObject();
HashMap hashWithOutFormat1 = (HashMap) DJ;
String t = String.valueOf(hashWithOutFormat1.get(Integer.valueOf(1))); // 取微信的uin
System.out.println("uin:"+t);
s = s + t; //合并到一个字符串
s = encode(s); // MD5
System.out.println("密码是 : " + s.substring(0, 7));
in.close();
in1.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static String encode(String content)
{
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(content.getBytes());
return getEncode32(digest);
}
catch (Exception e)
{
e.printStackTrace();
}
return null;
}
private static String getEncode32(MessageDigest digest)
{
StringBuilder builder = new StringBuilder();
for (byte b : digest.digest())
{
builder.append(Integer.toHexString((b >> 4) & 0xf));
builder.append(Integer.toHexString(b & 0xf));
}
return builder.toString();
}
}
把代码另存为GetDBKey.java,把CompatibleInfo.cfg和systemInfo.cfg(在com.tencent.mm/MicroMsg目录中)跟代码放在同一目录,编译运行后直接显示密码。如果需要看IMEI和uin,请自行添加输出代码。
另外一种方法是通过DENGTA_META.xml中的IMEI_DENGTA值(有的手机备份没有)和system_conf_prefs.xml中的system_config_prefs来提取IMEI和uin。这两个是明文,直接看就可以了。如果登录过多个微信账号,所有的uin都在app_brand_global_sp.xml中。这三个xml文件都在com.tencent.mm/shared_prefs目录下。
取得密码后,一种方法是使用sqlcipher2.1(CSDN有下载)GUI版直接打开。
第二种方法是通过sqlcipher命令行解密。第三种方法是通过程序代码解密。
python参考代码如下(需要先用pip install pysqlcipher3安装python的sqlcipher支持库才能引用):
from pysqlcipher3 import dbapi2 as sqlite
import hashlib
def decrypt( key ):
conn = sqlite.connect( "EnMicroMsg.db" )
c = conn.cursor()
c.execute( "PRAGMA key = '" + key + "';" )
c.execute( "PRAGMA cipher_use_hmac = OFF;" )
c.execute( "PRAGMA cipher_page_size = 1024;" )
c.execute( "PRAGMA kdf_iter = 4000;" )
c.execute( "ATTACH DATABASE 'EnMicroMsg-decrypted.db' AS wechatdecrypted KEY '';" )
c.execute( "SELECT sqlcipher_export( 'wechatdecrypted' );" )
c.execute( "DETACH DATABASE wechatdecrypted;" )
c.close()
def generate_key():
imei = "866666666666666"
uin = "1919191919"
key = hashlib.md5( str( imei ).encode("utf8") + str( uin ).encode("utf8")).hexdigest()[ 0:7 ]
return key
def main():
key = generate_key()
decrypt( key )
main()
from pysqlcipher3 import dbapi2 as sqlite
import hashlib
def decrypt( key ):
conn = sqlite.connect( "EnMicroMsg.db" )
c = conn.cursor()
c.execute( "PRAGMA key = '" + key + "';" )
c.execute( "PRAGMA cipher_use_hmac = OFF;" )
c.execute( "PRAGMA cipher_page_size = 1024;" )
c.execute( "PRAGMA kdf_iter = 4000;" )
c.execute( "ATTACH DATABASE 'EnMicroMsg-decrypted.db' AS wechatdecrypted KEY '';" )
c.execute( "SELECT sqlcipher_export( 'wechatdecrypted' );" )
c.execute( "DETACH DATABASE wechatdecrypted;" )
c.close()
def generate_key():
imei = "866666666666666"
uin = "1919191919"
key = hashlib.md5( str( imei ).encode("utf8") + str( uin ).encode("utf8")).hexdigest()[ 0:7 ]
return key
def main():
key = generate_key()
decrypt( key )
main()
将代码中的imei值和uin值替换成刚才获得的值即可。将EnMicroMsg.db与python程序放在同一目录下运行即可解密,生成的文件名为EnMicroMsg-decrypted.db。
微信索引库FTS5IndexMsg.db之前是不加密的,但到了微信7.0以后,索引库就变成了FTS5IndexMsg_encrypt.db,明显加密了。使用EnMicroMsg.db的密码进行解密失败,通过与几个电子取证公司的技术人员交流,了解到密码算法确实变了,而且加密参数也有变化。大体的情况是变成IMEI、uin、微信id三者拼接后的32位MD5值取前7位作为密码。涉及到产品细节无法透露,因此具体算法还需要自行研究。
先用sqlite管理工具将EnMicroMsg-decrypted.db打开,打开userinfo表,其中id为2对应的值为微信id。通常为wxid_xxxxxxxxxxxxxx格式。
这样的话至少素材已经齐了,接下来就是研究具体的算法了。
算法只能通过反编译apk文件来查找。
先从官网下载最新版的jadx 0.9.0zip版(不要下载exe版,因为需要调整运行参数)
将bin目录中的jadx-gui.bat的DEFAULT_JVM_OPTS参数里面Xms和Xmx分别调整到1024M和7G。参数调整后如下:
@rem Add default JVM options here. You can also use JAVA_OPTS and JADX_GUI_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xms1024M" "-Xmx7g" "-Dawt.useSystemAAFontSettings=lcd" "-Dswing.aatext=true" "-XX:+UseG1GC"
如果Xmx值小于7G则在反编译比较大的软件时jadx会出现假死状态。
@rem Add default JVM options here. You can also use JAVA_OPTS and JADX_GUI_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xms1024M" "-Xmx7g" "-Dawt.useSystemAAFontSettings=lcd" "-Dswing.aatext=true" "-XX:+UseG1GC"
如果Xmx值小于7G则在反编译比较大的软件时jadx会出现假死状态。
不用开反混淆开关(其实无所谓,看个人习惯)进行反编译,然后全部保存。
利用文本编辑器(我用的是NotePad++,开源还好用)对反编译的java代码进行文件内容搜索,查找加密算法位置。
首先搜索“FTS5IndexMsg_encrypt.db”,发现com\tencent\mm\plugin\fts\d.java中有:
String absolutePath = new File(str, "FTS5IndexMicroMsg_encrypt.db").getAbsolutePath();
String absolutePath = new File(str, "FTS5IndexMicroMsg_encrypt.db").getAbsolutePath();
往下看,发现有这么一句:
this.lXS = SQLiteDatabase.openOrCreateDatabase(absolutePath, com.tencent.mm.a.g.u(stringBuilder.append(a.OZ()).append(q.Kc()).append(com.tencent.mm.model.q.Wt()).toString().getBytes()).substring(0, 7).getBytes(), null, null);
stringBuilder是一个字符串构造函数,连续拼接了三个字符串,经过一个运算后取了前7位,与了解到的情况相符。
this.lXS = SQLiteDatabase.openOrCreateDatabase(absolutePath, com.tencent.mm.a.g.u(stringBuilder.append(a.OZ()).append(q.Kc()).append(com.tencent.mm.model.q.Wt()).toString().getBytes()).substring(0, 7).getBytes(), null, null);
stringBuilder是一个字符串构造函数,连续拼接了三个字符串,经过一个运算后取了前7位,与了解到的情况相符。
通过分析发现:
com.tencent.mm.kernel.a.OZ()取uin;
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!