失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > 企鹅安卓客户端联系人列表数据解密分析及Java实现

企鹅安卓客户端联系人列表数据解密分析及Java实现

时间:2021-12-02 17:33:42

相关推荐

企鹅安卓客户端联系人列表数据解密分析及Java实现

由于某些原因,本文暂不提供成型的软件,只提供思路和部分代码,感兴趣的同学可以用Swing或者JavaFX写个UI出来

前段时间,我的企鹅号莫名其妙的被封了7天,在此期间无论如何申诉都没有效果,只好作罢。但这个时候正好是我申请各种offer的紧急时刻,有很多重要的联系人列表封存在其中提取不出来,又想起之前有人说企鹅安卓客户端在app数据目录里面以<qq号>.db的文件名保存了联系人列表的各种信息和聊天记录,于是就想着能否查看一下,然后申请一个小号加好友救急。

但拖入navicat中发现关键字段似乎用某种加密算法和谐掉了,说明这个SQLITE3数据库肯定是用单独的密钥加密过的:

此时我们看看能不能通过逆向工程获得解密算法,对关键字段进行解密。于是将apk导出,先查看目录结构,发现存在名为dbencrypt的动态链接库,于是猜测其本身和对应的java代码和解密数据库字段有关

将该SO库拖入IDA,查看导出函数,发现似乎只和加密有关,而无解密相关函数

转而定位对应的java类,将apk整体拖入看雪论坛版主gjden大佬开发的GDA逆向分析工具中,从函数和变量名猜测,此处存在大量计算、读取和写入密钥文件的操作,而且为明文,路径为kc,于是在app数据目录下搜索名为kc的文件。

打开文件查看,发现果然是明文,和代码中的逻辑比对,发现这个值应该是获取设备IMEI或者mac号失败后硬编码的常量字符串

在这个代码中,先计算出了str的值,然后将其写入kc文件,并且更新全局变量codeKey。此时我们查找codeKey的引用,发现他在encode()函数里面使用过,并且将参数传入了先前提到的so库中的JNI函数

同时又注意到上面的decode()函数是直接调用的encrypt()函数,说明这是个对称加密算法,因此必须对刚才的so库重新进行分析

拖入ida中并且导入jni.h头文件,添加其中的JNIEnv, JavaVM,JNINativeInterface,JNINativeMethod,JNIInvokeInterface五个结构体,并把encryptByte()函数的声明改为

jbyte *Java_com_tencent_mobileqq_utils_SecurityUtile_encryptByte(JNIEnv*, jobject, jbyte*, jchar*, jint)

以增强可读性,同时得出如下部分源码:

可以很清楚的看到,这种加密算法其实就是对输入的byte[]数组按次序和密钥做与其长度相同的循环异或运算,得到密文,解密时把密文作为形参再调用一次该函数,得到原文。但与以前的算法不同的是,密文对应的byte数组的下标并不是和密钥的下标一一对应(比如说,原来的算法对密文的第四位解密时用的是密钥的第四位异或,但是这次要操作的密文下标是另一个特定的值,比如8),这其中就专门多了一个函数sub_7E4,计算要操作密文的下标值。这个函数比较复杂,我们就不在此分析,优化改写后直接套用即可。

回到java部分,传入的byte[]和char[]参数是由UTF-8的字符串类型转来的,因此计算完成后还要还原。故解密密文的核心代码如下:

public static String doDec(byte[] data, String key) {try {int keylen = key.length();byte[] b = key.getBytes(StandardCharsets.UTF_8);if (b == null) return "";int i = 0, j = 0;byte[] dst = data.clone();while (j < data.length) {int cal = 8 - Integer.toBinaryString((data[j] ^ -1) & 0xff).length();if (cal < 0 || cal > 4) {System.err.println("BadEncode");break;}if (cal == 0) {dst[j] = (byte) (data[j] ^ b[i % keylen]);j++;} else {dst[(j + cal) - 1] = (byte) (data[(j + cal) - 1] ^ b[i % keylen]);j += cal;}i++;}return new String(dst, StandardCharsets.UTF_8);} catch (Exception e0) {e0.printStackTrace();return null;}}

但是光有解密部分还不行,我们还需要从数据库文件中读取对应栏目的字段值,比如企鹅号、昵称、名称、备注名、性别、年龄等信息。由于时间仓促,目前我也就实现了这几个栏目数据的读取,诸位还可以拓展。打开navicat,查得这些信息位于数据库Friends表中的age, alias, gender, name, remark, uin列中。

因此,我们要先打开数据库文件,获得连接信息

public static Connection getConnection(String dbfn) throws SQLException {if ((dbfn = dbfn.toLowerCase()).indexOf("\\") != -1)dbfn = dbfn.replaceAll("\\\\", "/");String result = "";if (dbfn.indexOf("/") >= 0 || dbfn.indexOf("\\") >= 0) {result = String.valueOf("jdbc:sqlite://") + dbfn;} else {result = String.valueOf("jdbc:sqlite:") + dbfn;}try {Class.forName("org.sqlite.JDBC");return DriverManager.getConnection(result);} catch (ClassNotFoundException cne) {cne.printStackTrace();return null;}}

读取对应列的所有字段,将每个栏目的名称作为键、字段在SQL数据库中的类型名作为值存入哈希表中

public static HashMap getColumns(Connection cnn) {String cmd = "SELECT * FROM Friends limit 0,1;";HashMap<String, String> hashMap = new HashMap<>();try {Statement st = cnn.createStatement();ResultSet rst = st.executeQuery(cmd);if(rst.next()) {ResultSetMetaData rsmd = rst.getMetaData();int col = rsmd.getColumnCount();for(int b=0;b<col;b++) {hashMap.put(rsmd.getColumnName(b+1), rsmd.getColumnTypeName(b+1));}}rst.close();st.close();} catch (SQLException se) {se.printStackTrace();}return hashMap;}

根据栏目名称(列)读取该列下每行的值,存入动态二维数组中

private static ArrayList combine(ResultSet resultSet, Set set, HashMap hashMap) {ArrayList arrayList = new ArrayList();Iterator it = set.iterator();while (it.hasNext()) {String str = (String) it.next();arrayList.add(getType(resultSet, str, (String) hashMap.get(str)));}return arrayList;}public static ArrayList getValues(Connection cnn, HashMap hashMap) {String val = "SELECT * FROM Friends;";Set set = hashMap.keySet();ArrayList<ArrayList> arrayList = new ArrayList();try {Statement statement = cnn.createStatement();ResultSet resultSet = statement.executeQuery(val);while (resultSet.next()) {arrayList.add(combine(resultSet, set, hashMap));}resultSet.close();statement.close();} catch (SQLException se) {se.printStackTrace();}return arrayList;}

接下来还要根据字段的类型和获得的Object对象的类型来判断是否需要解密、需要用何种方式解密,故定义下面函数判断类型

private static HashMap getType(ResultSet rs, String col, String ts) {HashMap<Object, Object> hashMap = new HashMap<>();try {switch (ts) {case "STRING":hashMap.put(col, rs.getString(col));break;case "INTEGER":case "INT":hashMap.put(col, Integer.valueOf(rs.getInt(col)));break;case "BLOB":hashMap.put(col, rs.getBytes(col));break;case "BYTE":hashMap.put(col, Byte.valueOf(rs.getByte(col)));break;case "CHAR":hashMap.put(col, rs.getString(col));break;case "LONG":hashMap.put(col, Long.valueOf(rs.getLong(col)));break;case "REAL":hashMap.put(col, Float.valueOf(rs.getFloat(col)));break;case "TEXT":hashMap.put(col, rs.getString(col));break;case "VARCHAR":hashMap.put(col, rs.getString(col));break;default:break;}} catch (SQLException e) {e.printStackTrace();}return hashMap;}

private static Object UnkFunc(Object obj, String k) throws UnsupportedEncodingException {byte[] bArr;if (k == null) {return obj;}if (obj instanceof String) {bArr = ((String)obj).getBytes("utf-8");} else if (!(obj instanceof byte[])) {return obj;} else {bArr = (byte[]) obj;}return doDec(bArr, k);}public static Object Proc(String ts, Object obj, String key) throws UnsupportedEncodingException {switch (ts) {case "INTEGER":return obj instanceof Integer ? Integer.valueOf(((short) ((Integer) obj).intValue())) : obj;case "STRING":case "TEXT":return UnkFunc(obj, key);default:return obj;}}

最后可以在主函数(或者是UI的消息处理线程)中进行解密了

try {Connection cnx = DbConnection.getConnection(src_path);HashMap<String, String> con = getColumns(cnx);ArrayList<ArrayList> db = getValues(cnx, con);String[] col_names = con.keySet().toArray(new String[] {});Object[][] objArr = new Object[db.size()][col_names.length];for(int i = 0; i < db.size(); i++) { // 行长度FrList fri = new FrList();for(int j = 0; j < col_names.length; j++) { // 列长度HashMap tengo = (HashMap) (db.get(i)).get(j);String ts = con.get(col_names[j]);objArr[i][j] = tengo.get(col_names[j]);if(objArr[i][j] != null) objArr[i][j] = Proc(ts, objArr[i][j], input_key);else objArr[i][j] = "NULL";if(col_names[j].equals("age")) fri.setAge((Integer) objArr[i][j]);if(col_names[j].equals("alias")) fri.setAlias((String) objArr[i][j]);if(col_names[j].equals("gender")) {int v0 = (Integer)objArr[i][j];String gv = (v0 == 0) ? "Unknown" : ((v0 == 1) ? "Male" : "Female");fri.setGender(gv);}if(col_names[j].equals("name")) fri.setName((String) objArr[i][j]);if(col_names[j].equals("remark")) fri.setRemark((String) objArr[i][j]);if(col_names[j].equals("uin")) fri.setQQID((String) objArr[i][j]);}grid.add(fri);}tb.setItems(grid);DbConnection.closeConnection(cnx); // 记得关闭数据库连接} catch (Exception e) {e.printStackTrace();}

最终解密效果如下:

如果觉得《企鹅安卓客户端联系人列表数据解密分析及Java实现》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。