不知道你有没有经历过,想联系一位很长时间没有联系的朋友,发现对方很早以前已经把你删除了,而你还一无所知。
相信每个人的微信通信录里都存在一些「僵尸粉」,
他们默默地躺在联系人列表中,你以为对方还是朋友,那就真是太年轻、太天真的;实际上,
对方早就把从好友列表中删了,那如何来筛选出这群人呢?
网上的很大量检测僵尸粉的工具,检测的时候会给微信通信录内的每一个好友发送一条检测信息,严重「打扰」到对方;另外一部分软件在检测的时候,会植入一些代码病毒,暗箱操作显得很不安全。
本篇文章的目的是
自动化操作微信 App
,通过「模拟给好友转账」来筛选出所有的僵尸粉,并一键删除它们。
-
一部 Root 后的 Android 手机或者模拟器,如果没有 Root 的设备,推荐使用网易 MuMu 模拟器
-
Android 开发环境、Android Studio
-
-
自动化工具:Python 虚拟环境下安装 pocoui
整个操作分为 3 步骤,分别是破解微信数据库筛选出通信录中的好友、模拟给好友转账得到僵尸粉数据、删除所有僵尸粉。
ps:这里只是简单的说一下破解流程,想一键破解微信通信录数据,可以跳过这一步,直接使用文末提供的 APK。
首先,我们
使用 Android Studio 新建一个项目
,在项目初始化的时候,授予应用管理员权限以及修改微信目录的读写权限。
public static final String WX_ROOT_PATH = "/data/data/com.tencent.mm/";
public static void execRootCmd(String paramString)
{
try
{
Process localProcess = Runtime.getRuntime().exec("su");
Object localObject = localProcess.getOutputStream();
DataOutputStream localDataOutputStream = new DataOutputStream((OutputStream) localObject);
String str = String.valueOf(paramString);
localObject = str + "\n";
localDataOutputStream.writeBytes((String) localObject);
localDataOutputStream.flush();
localDataOutputStream.writeBytes("exit\n");
localDataOutputStream.flush();
localProcess.waitFor();
localObject = localProcess.exitValue();
} catch (Exception localException)
{
localException.printStackTrace();
}
}
RootUtils.execRootCmd("chmod 777 -R " + WX_ROOT_PATH);
微信数据库的密码是由设备的 imei 和微信的 uid 进过 md5 算法生成的。
public static String getDbPassword(String imei, String uin)
{
if (TextUtils.isEmpty(imei) || TextUtils.isEmpty(uin))
{
Log.d("xag"
, "初始化数据库密码失败:imei或uid为空");
return "密码错误";
}
String md5 = MD5Utils.md5(imei + uin);
assert md5 != null;
return md5.substring(0, 7).toLowerCase();
}
接着,就可以使用 SQLCipher 依赖库来对微信数据库进行查询,我们需要为项目添加如下依赖,方便操作数据库。
implementation 'net.zetetic:android-database-sqlcipher:3.5.4@aar'
利用上面得到的密码
打开加密数据库
,然后查询「rcontact」表获取微信通讯录内所有的好友的微信号、昵称、用户名等数据。
private void openWxDb(File dbFile, String db_pwd)
{
List contacts = new ArrayList<>();
SQLiteDatabase.loadLibs(this);
SQLiteDatabaseHook hook = new SQLiteDatabaseHook()
{
public void preKey(SQLiteDatabase database)
{
}
public void postKey(SQLiteDatabase database)
{
atabase.rawExecSQL("PRAGMA cipher_migrate;");
}
};
try
{
SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(dbFile, db_pwd, null, hook);
Cursor c1 = db.rawQuery(
"select * from rcontact where verifyFlag =0 and type not in (2,4,8,9,33,35,256,258,512,2051,32768,32770,32776,33024,65536,65792,98304) and username not like \"%@app\" and username not like \"%@qqim\" and username not like \"%@chatroom\" and encryptUsername!=\"\"",
null);
while (c1.moveToNext())
{
String userName = c1.getString(c1.getColumnIndex("username"));
String alias = c1.getString(c1.getColumnIndex("alias"));
String nickName = c1.getString(c1.getColumnIndex("nickname"));
int type = c1.getInt(c1.getColumnIndex("type"));
contacts.add(new Contact(userName, alias, nickName));
}
Log.d("xag", "微信通讯录中,联系人数目:" + contacts.size() + "个");
for (int i = 0; i {
Log.d("xag", contacts.get(i).getNickName());
}
c1.close();
db.close();
} catch (Exception e)
{
Log.e("xag", "读取数据库信息失败" + e.toString());
Toast.makeText(this, "读取微信通信录失败!", Toast.LENGTH_SHORT).show();
}
Toast.makeText(this, "读取微信通信录成功!", Toast.LENGTH_SHORT).show();
}
需要注意的是,数据库中 rcontact 表的数据比较杂乱,除了正常的好友数据,黑名单好友、已删除好友、公众号、微信群等数据也包含在内,
需要我们通过 type 和 verifyFlag 字段进行筛选。
为了便于 Python 操作,最后将查询的好友数据写入到 csv 文件中。
public static void writeCsvFile(String output_path, List contacts)
{
try
{
File file = new File(output_path);
if (file.exists())
{
file.delete();
}
BufferedWriter bw = new BufferedWriter(new FileWriter(file, true));
bw.write("userName" + "," + "alias" + "," + "nickName");
bw.newLine();
for (int i = 0; i {
bw.write(contacts.get(i).getUserName() + "," + contacts.get(i).getAlias() + "," + contacts.get(i).getNickName());
bw.newLine();
}
bw.close();
} catch (IOException e)
{
e.printStackTrace();
}
}
第 2 步,我们需要模拟给好友转账,来判断这个好友关系是否正常。
首先,我们需要
初始化 Airtest
,然后利用 adb 把第 1 步生成的数据从手机里导出到本地。
def __init_airtest(self):
"""
初始化Airtest
:return:
"""
device_1 = Android('822QEDTL225T7')
connect_device("android:///")
self.poco = AndroidUiautomationPoco(device_1, screenshot_each_action=False)
auto_setup(__file__)
def export_wx_db_from_phone(target_path):
"""
从手机中导出通信录数据
:param target_path:
:return:
"""
wx_db_source_path = "/data/data/com.xingag.crack_wx/wx_data.csv"
os.popen('adb pull %s %s' % (wx_db_source_path, target_path))
打开微信,遍历好友列表,拿到每一个好友的微信号去搜索好友,跳转到好友的聊天界面。
def __to_friend_chat_page(self, weixin_id):
"""
点击到一个好友的聊天界面
:param weixin_id:
:param weixin_name:
:return:
"""
element_search = self.__wait_for_element_exists(self.id_search)
element_search.click()
print('点击搜索')
element_search_input = self.__wait_for_element_exists(self.id_search_input)
element_search_input.set_text(weixin_id)
element_search_result_list = self.__wait_for_element_exists(self.id_search_result_list)
index_tips = 0
for index, element_search_result in enumerate(element_search_result_list.children()):
if element_search_result.offspring(text=self.text_contact_tips).exists():
index_tips = index
break
element_search_result_list.children()[index_tips + 1].click()
接着尝试着给对方转账,如果好友关系正常,就会跳出一个支付页面让输入密码。
def __judge_is_friend(self, weixin_id, weixin_name):
"""
判断是不是微信好友
:param weixin_id: 微信号
:return:
"""
self.poco(self.id_chat_more_button).click()
self.poco(self.id_chat_more_container).offspring(text=self.text_chat_transfer_account_text).click()
self.poco(self.id_transfer_account_input).set_text(self.money)
self.poco(self.id_transfer_account_container).offspring(text=self.text_chat_transfer_account_text).click()
如果是僵尸粉,应用会弹出一个警告对话框,提示你不是收款方好友,没法完成转账的操作。
通过警告对话框是否存在,就可以判断好友关系是否正常。非正常的好友关系,包含:僵尸粉、对方账号异常等。
if element_transfer_account_result_button:
ransfer_account_result_tips = self.poco(self.id_transfer_account_result_tips).get_text()
if self.text_friend_no_tips in transfer_account_result_tips:
print('注意!%s已经把你拉黑了!!!' % weixin_name)
self.friend_black_list.append({
'id': weixin_id,
'nickName': weixin_name
})
write_to_file(self.path_black_list, 'id:%s,nickName:%s' % (weixin_id, weixin_name))
elif self.text_friend_limit_tips in transfer_account_result_tips:
print('%s账号收到限制!!!' % weixin_name)
write_to_file(self.path_account_limit, 'id:%s,nickName:%s'