# 前言
在电子数据取证过程中,对微信本地数据库的解密、提取与恢复是非常重要的工作内容。本文以华为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代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | 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代码凑合一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | 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支持库才能引用):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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。参数调整后如下:
1 2 | @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中有:
1 | String absolutePath = new File(str, "FTS5IndexMicroMsg_encrypt.db" ).getAbsolutePath(); |
往下看,发现有这么一句:
1 | 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;
com.tencent.mm.compatible.e.q.Kc()取DeviceID(即IMEI);
com.tencent.mm.model.q.Wt()取userinfo(即微信id);
com.tencent.mm.a.g.u()是MD5算法。
因此说明这个密码是将uin、IMEI、微信id连续拼接然后计算32位MD5值再取前7位作为密码。
这里需要注意的是,uin如果是负值不能直接进行拼接,要把它加上4294967296(最大无符号数),得到的正数作为最终的uin进行拼接。
另外,除了密码之外据说加密算法等参数也有变化,因此还需要继续搜索。
这次搜索sqlcipher参数“PRAGMA”,发现最终调用的是com\tencent\wcdb\database\SQLiteCipherSpec.java中的几个参数(wcdb对应wcdb.so文件,其实是改了名的sqlcipher.so),对应值分别为:
1 2 3 | CIPHER_AES256CBC = "aes-256-cbc"; kdfIteration = 64000; hmacEnabled = true; |
另外经与厂商技术人员沟通,发现还有个参数需要调整:
这几个参数分别对应如下sqlcipher参数:
1 2 3 4 5 6 7 | PRAGMA cipher = 'aes-256-cbc'; PRAGMA cipher_use_hmac = ON; PRAGMA cipher_page_size = 4096; PRAGMA kdf_iter = 64000; |
综上所述,一切解密需要的素材都齐了,相应的python代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | from os.path import isfile # 用pip install pysqlcipher3安装python的sqlcipher支持库再引用 from pysqlcipher3 import dbapi2 as sqlite import hashlib import sys import time import logging import re logging.basicConfig(filename = 'FTS5IndexMicroMsg_decrypt.log' , format = "%(asctime)s %(levelname)s: %(message)s" , datefmt = "%d-%b-%Y %I:%M:%S %p" , level = logging.DEBUG) def decrypt( key ): logging.info( "连接数据库..." ) conn = sqlite.connect( "FTS5IndexMicroMsg_encrypt.db" ) c = conn.cursor() c.execute( "PRAGMA key = '" + key + "';" ) c.execute( "PRAGMA cipher = 'aes-256-cbc';" ) c.execute( "PRAGMA cipher_use_hmac = ON;" ) c.execute( "PRAGMA cipher_page_size = 4096;" ) c.execute( "PRAGMA kdf_iter = 64000;" ) try : logging.info( "正在解密..." ) c.execute( "ATTACH DATABASE 'FTS5IndexMicroMsg_decrypt.db' AS fts5indexdecrypt KEY '';" ) c.execute( "SELECT sqlcipher_export( 'fts5indexdecrypt' );" ) c.execute( "DETACH DATABASE fts5indexdecrypt;" ) logging.info( "正在分离数据库..." ) c.close() status = 1 except : c.close() status = 0 return status def generate_key(): imei = "866666666666666" logging.info( "IMEI: " + str ( imei )) uin = "2377777777" logging.info( "UIN: " + str ( uin )) account = "wxid_1l8w9yqrxxxxxx" logging.info( "account: " + str ( account )) logging.info( "正在生成密钥..." ) key = hashlib.md5( str ( uin ).encode( "utf8" ) + str ( imei ).encode( "utf8" ) + str ( account ).encode( "utf8" )).hexdigest()[ 0 : 7 ] logging.info( "密钥: " + key ) return key def db_hash(): f = open ( 'FTS5IndexMicroMsg_decrypt.db' , 'rb' ).read() logging.info( "正在生成哈希值..." ) if len ( f ) > 0 : db_md5 = hashlib.md5( f ).hexdigest() logging.info( "FTS5IndexMicroMsg_decrypt.db MD5: " + db_md5 ) db_sha1 = hashlib.sha1( f ).hexdigest() logging.info( "FTS5IndexMicroMsg_decrypt.db SHA1: " + db_sha1 ) return def main(): if not ( isfile( "FTS5IndexMicroMsg_encrypt.db" )): print ( "##########" ) print ( "'FTS5IndexMicroMsg_encrypt.db'不存在!" ) print ( "正在退出脚本..." ) print ( "##########" ) sys.exit() logging.info( "脚本启动..." ) key = generate_key() status = decrypt( key ) if status = = 1 : db_hash() print ( "##########" ) print ( "解密成功!" ) print ( "解密文件: FTS5IndexMicroMsg_decrypt.db" ) print ( "日志文件: FTS5IndexMicroMsg_decrypt.log" ) print ( "##########" ) logging.info( "解密成功!" ) logging.info( "解密文件: FTS5IndexMicroMsg_decrypt.db" ) else : print ( "##########" ) print ( "解密失败!" ) print ( "日志文件: FTS5IndexMicroMsg_decrypt.log" ) print ( "##########" ) logging.info( "解密失败!" ) logging.info( "正在退出脚本..." ) main() |
这次放了比较规范的代码,之前也是为了体现python的简洁,很多复杂功能没几行代码就搞定了。
只要把IMEI、uin、微信id换成之前取出来的即可。把加密数据库和python代码放在同一个目录中运行就可以得到解密数据库。
另外在搜索代码的过程中发现MicroMsgPriority.db也加密了,密码是uin、微信id、IMEI顺次拼接的32位MD5值取前7位。sqlcipher参数与索引库相同。可以简单修改以上代码就可以实现解密。
### 从索引库恢复被删除的消息
用winhex打开解密后的索引库:
在右侧显示区上方点击“ANSI ASCII”,选择“Unicode UTF-8”,向下滚动就可看到连续的中文,这些就是索引后的消息。其中包括已删除和未删除的内容。
经查阅资料并与厂商技术人员沟通,发现这些信息的存储格式开头如下:
aa bb 03 00 cc 或 aa bb 04 00 cc dd
如果一个区域里面aa的值一致,说明是正常未删除信息,如果aa值不一致,则说明是删除信息。bb是从7F到00顺序排列(偶尔有中断)相当于序号。如果是03则cc是后面正文长度,如果是04则cc dd是正文长度。
cc或者cc dd是varint格式,需要进行一定的变换之后才能得出长度值。
计算方法为:
cc->16进制转10进制->减13->除以2,如果结果是3的倍数,说明是中文,再除以3,得到的数值就是正文长度。如果除以2以后不是3的倍数,说明是英文(半角),这个得数就是英文正文长度。
例如:
55->85->72->36->12个汉字
19->25->12->6->2个汉字
17->23->10->5个英文
如果是cc dd要麻烦一些。先将cc dd都转成二进制,然后把cc的首位1和之后的所有0都去掉,把dd首位0去掉,然后拼到一起,再转成10进制->减13->除以2,如果结果是3的倍数,说明是中文,再除以3,得到的数值就是正文长度。如果除以2以后不是3的倍数,说明是英文(半角),这个得数就是英文正文长度。
例如:
81 23
81-> 10000001 23->00100011 合并为10100011,十进制是163;163->150->75->25个汉字
81 11
81->10000001 11->00010001 合并为10010001,十进制是145;145->132->66->22个汉字
这样的话,就可以通过程序把符合规则的内容全部导出来,就是被删除的消息。如果想确定交互双方,还需要配合其他数据分析,在此先不讨论了。
# 总结
综上所述,就是以华为手机为例的微信数据备份、导出、解密以及简要分析的过程,希望对研究电子数据取证的朋友们有所帮助。
原文:https://bbs.pediy.com/thread-250714.htm