引言
以前从来没去研究过安卓apk脱壳之类的,这次正好有机会和时间,所以也就上手研究了下,从入门到猝死,每天睡四五个小时,其他时间全部在研究,脑壳都给干冒烟了,主要是入手后脑子就停不下来,导致无法安心睡眠,不过期间学习到了挺多的知识的,只能说痛并快乐着。。。
目的:
泡泡聊天APP(MosTalk)最近突然火起来了,同时也引来了很多炸群广告,用晚上开始全体群禁言的方式显得很呆,考虑写个脚本全自动检测所有群,发现炸群者直接踢出并撤回消息的辅助脚本,所以也就有了这次的研究。
脱壳
查壳后发现用了最新的360加固,于是在脱壳上面废了几天时间,主要是不会IDA分析内存,所以只有各种查资料去找别人的工具了,最后通过 frida-unpack 成功脱壳,具体可以看上篇文章:初入APK逆向破解小记 这里就不过多阐述了。
源码审计
脱壳后的代码是 Java 代码,拿着自己半吊子的功底在加上 open AI ,倒是也能做到审计了。
思路1:通过python+frida hook 最新消息接受方法
泡泡聊天APP其实是存在WEB聊天页面的,只是它聊天是走的websocket,对websocket并不了解,所以不知道怎么监听到最新的群消息,只能想到这么个思路了。
尝试半天,总算写好了代码,测试了下发现存在个问题,会遗漏消息,并不能把所有的消息全部撤回;
在这里卡了一段时间,最开始以为是消息接受处理存在问题,所以又改成了接受消息存本地数据库,然后从数据库取出消息在处理,最后无奈发现因为python调用frida hook的原因,是必然会遗漏些频率高的消息的,不管用不用数据库。。。
虽然遗漏一些消息,但是只要能实现检测到频繁者,能撤回绝大部分消息并自动踢人,其实也算完工了,但是我这该死的完美主义,消息遗漏+配合模拟器ADB这种繁琐的方式,让我这个完美主义者不能忍,所以最终 PASS 掉了这个方法。
这里贴下当时的代码把,可能存在一些bug,毕竟是个半成品:
import frida,json,re,time,datetime,requests,hashlib,threading,os,queue import jaydebeapi from collections import defaultdict def md5_hash(text): md5_hasher = hashlib.md5() md5_hasher.update(text.encode('utf-8')) hashed_text = md5_hasher.hexdigest() return hashed_text def md5Psw(account, pwd): hashed_pwd = md5_hash( md5_hash(pwd) + account + 'MOSGRAM') return hashed_pwd def remove_special_characters(input_string): try: # 定义特殊字符的正则表达式模式 special_char_pattern = re.compile('[^\x00-\x7F]+') # 使用正则表达式替换特殊字符为空字符串 cleaned_string = special_char_pattern.sub('', input_string) return cleaned_string except Exception as e: print("[!] 异常发生:", e) return input_string def data_to_json(data): try: pattern = r'(\w+)\s*=\s*([^,]+)' result = [] records = data.split('], [') for record in records: match = re.findall(pattern, record) record_dict = {} for key, value in match: if value.startswith('{') and value.endswith('}'): record_dict[key] = json.loads(value) else: record_dict[key] = value result.append(record_dict) parsed_data = json.dumps(result, indent=4) return parsed_data except json.JSONDecodeError as json_error: print("[!] 数据转换失败: JSON 解析错误:", json_error) except Exception as e: print("[!] 数据转换失败:", e) return None def custom_request(method, url, json_data=None, headers=None): try: if method == 'POST': response = requests.get(url, headers=headers) elif method == 'POST': response = requests.post(url, json=json_data, headers=headers) elif method == 'DELETE': response = requests.delete(url, json=json_data, headers=headers) else: raise ValueError("[!] 不支持的请求方法: " + method) return response.json() except requests.exceptions.RequestException as e: print("[!] 网络请求异常:", e) return None def usb_device(): try: # 连接安卓机上的frida-server device = frida.get_usb_device(1000) # 启动app pid = device.spawn(["com.paopaotalk.im"]) device.resume(pid) session = device.attach(pid) return session except: exit("[!] 启动应用APP出错,重新执行脚本!") # 消息回调函数,处理从脚本发送的消息 def on_message(message, payload): try: global call_back_message if message['type'] == 'send': call_back_message = message['payload'] return call_back_message except: exit("[!] 消息回调失败,重新执行脚本!") def hook_function(session): try: global script if script: script.unload() # 释放之前的脚本 script_source = """ Java.perform(function() { var getVal = Java.use('v5.g'); var cons = getVal.l(); var send_it = cons.toString(); send(send_it); }); """ script = session.create_script(script_source) script.on('message', on_message) script.load() return script except: exit("[!] 注入脚本失败,重新执行脚本!") #登录 def login(username,pwd): try: print("[-] 正在登录账号...") login_data_json = {"username": "aaa","password": "bbb","device_type":"WEB"} login_data_json["username"] = username login_data_json["password"] = md5Psw(username, pwd) login_header_json = {"Content-Type":"application/json; charset=UTF-8","X-Requested-With":"XMLHttpRequest","App_id":"qiyunxin","timestamp": str(int(time.time() * 1000)),"sign": md5_hash(str(int(time.time() * 1000)) + "WEB"),} logins = custom_request('POST', url='http://api.vvchat.im/userservices/v2/user/login', json_data=login_data_json, headers=login_header_json) if logins['token'] and logins['open_id']: print("[!] 登录账号成功!") return logins except: exit("[!] 登录失败,重新执行脚本!") #数据转换集合 def zhuanhuan(data): data_json = json.loads(data_to_json(call_back_message)) return data_json def tiren(chat_name, from_cust_id): if chat_name in groups_list: open_id = custom_request('GET', url=f"http://api.vvchat.im/groupapi/v1/users/{logins['open_id']}/groups/{groups_list[chat_name[1]]}/members?page_size=100000", headers={"Content-Type": "application/json; charset=UTF-8", "Authorization": logins['token']}) if open_id: if chs['err_msg'] == 'OK': print(f"[-] 群消息 {row[0]} 撤回成功!") delete_data(f"DELETE FROM data WHERE msg_no = '{row[0]}' AND chat_name = '{row[1]}';") else: print(f"[!] 群消息 {row[0]} 撤回失败,原因:{chs['err_msg']} 请检查...") delete_data(f"DELETE FROM data WHERE msg_no = '{row[0]}' AND chat_name = '{row[1]}';") else: msg_no_queue.put(row) print(f"[!] 群消息 {row[0]} 撤回失败,已重新添加到列队!") #持续不间断接受新数据并转存到列队 def run1(): global unique_data_set while True: hook_function(session) if call_back_message: call_back_message_json = zhuanhuan(remove_special_characters(call_back_message)) for item in call_back_message_json: if groups in item.get("chat_name", "") and item.get("chat_type", "") == "2": data = (item.get("chat_name", ""),item.get("msg_no", ""),item.get("from_cust_id", ""),item.get("content", ""),item.get("msg_time", "")) if item.get("content", "") != '{0}开启群禁言' and item.get("content", "") != '{0}撤回了一条消息' and item.get("content", "") != '' and data not in unique_data_set: ls_queue.put(data) unique_data_set.add(data) time.sleep(0.0001) #从列表中提取数据判断是否存在 def run2(): while True: with search_queue_lock: while not ls_queue.empty(): data_to_insert = ls_queue.get() # 从队列取出数据 search_msg_no = query_data(f"select id from data where msg_no = '{data_to_insert[2]}'") if not search_msg_no: insert_queue.put(data_to_insert) time.sleep(0.001) #将insert_queue存储的最新发信信息插入到数据库中 def run3(): while True: with search_queue_lock: while not insert_queue.empty(): data_to_insert = insert_queue.get() # 从队列取出数据 print("[-] 新数据:",data_to_insert) insert_data(data_to_insert) time.sleep(0.001) #搜索数据库,找出违规者所有发信MSG_NO编号,加入到列队中 def run4(): while True: with search_queue_lock: if msg_no_queue.empty() and insert_queue.empty() and ls_queue.empty(): #数据赛选,提取违规者发的全部信息 MSG_NO编号 select_query = """ WITH UniqueChatNames AS ( SELECT chat_name, from_cust_id FROM DATA WHERE id IN ( SELECT MIN(id) FROM (SELECT id,chat_name,from_cust_id,content,SUBSTRING(msg_time, 1, 9) FROM DATA WHERE (chat_name,from_cust_id,content,SUBSTRING(msg_time, 1, 9)) IN ( SELECT chat_name,from_cust_id,content,SUBSTRING(msg_time, 1, 9) FROM DATA GROUP BY chat_name,from_cust_id,content,SUBSTRING(msg_time, 1, 9) HAVING COUNT(*) > 1 ) ) grouped_data GROUP BY chat_name, from_cust_id ) ) SELECT STRINGDECODE (d.msg_no), STRINGDECODE (d.chat_name) FROM ( SELECT *, ROW_NUMBER () OVER ( PARTITION BY chat_name, from_cust_id, msg_no ORDER BY id ) AS rn FROM DATA ) d JOIN UniqueChatNames subquery ON d.chat_name = subquery.chat_name AND d.from_cust_id = subquery.from_cust_id WHERE d.rn = 1; """ results = query_data(select_query) if results:#列表类型 msg_no_queue.put(results) print(f"[!] 已检测到 {len(results)} 条违规信息.") time.sleep(0.001) #提取msg_no编号,并撤回这些编号消息 def run5(): #登录 logins = login(username,pwd) while True: with search_queue_lock: while not msg_no_queue.empty(): msg_no_list = msg_no_queue.get() for row in msg_no_list: if row[1] in groups_list: data = {"msg_no": row[0],"msg_time": int(time.time() * 1000)} chs = custom_request('POST', url=f"http://api.vvchat.im/groupapi/v1/users/{logins['open_id']}/groups/{groups_list[row[1]]}/revoke_msg", json_data=data, headers={"Content-Type": "application/json; charset=UTF-8", "Authorization": logins['token']}) if chs: if chs['err_msg'] == 'OK': print(f"[-] 群消息 {row[0]} 撤回成功!") delete_data(f"DELETE FROM data WHERE msg_no = '{row[0]}' AND chat_name = '{row[1]}';") else: print(f"[!] 群消息 {row[0]} 撤回失败,原因:{chs['err_msg']} 请检查...") delete_data(f"DELETE FROM data WHERE msg_no = '{row[0]}' AND chat_name = '{row[1]}';") else: msg_no_queue.put(row) print(f"[!] 群消息 {row[0]} 撤回失败,已重新添加到列队!") else: exit("[!] 违规所在群 ID 获取失败, 代码中添加该群ID!") time.sleep(0.001) #5分钟清理一次数据库 def run6(): global unique_data_set while True: time.sleep(300) # 五分钟 unique_data_set.clear() delete_data("TRUNCATE TABLE data;") #清空数据库 # 创建数据库连接 def create_connection(): conn = jaydebeapi.connect("org.h2.Driver", h2_jdbc_url, [h2_user, h2_password], h2_driver_path) return conn # 创建表 def create_table(): conn = create_connection() create_table_sql = """ CREATE TABLE IF NOT EXISTS data ( id BIGINT AUTO_INCREMENT PRIMARY KEY, chat_name TEXT NOT NULL, msg_no TEXT NOT NULL, from_cust_id TEXT NOT NULL, content TEXT NOT NULL, msg_time BIGINT NOT NULL ) """ cursor = conn.cursor() try: cursor.execute(create_table_sql) conn.commit() except jaydebeapi.Error as e: print("创建表异常:", e) cursor.close() return False finally: cursor.close() return True # 插入数据 def insert_data(data): insert_query = ''' INSERT INTO data (chat_name, msg_no, from_cust_id, content, msg_time) VALUES (?, ?, ?, ?, ?); ''' try: conn = create_connection() cursor = conn.cursor() cursor.execute(insert_query, data) conn.commit() except jaydebeapi.Error as e: print("[!] 插入失败:", e) finally: cursor.close() # 查询数据 def query_data(sql): try: conn = create_connection() cursor = conn.cursor() cursor.execute(sql) results = cursor.fetchall() except jaydebeapi.Error as e: print("[!] 查询失败:", e) finally: cursor.close() return results def delete_data(sql): try: conn = create_connection() cursor = conn.cursor() cursor.execute(sql) conn.commit() except jaydebeapi.Error as e: print("[!] 查询失败:", e) finally: cursor.close() if __name__ == "__main__": ################################################ username = '0086' pwd = '' groups = '关键词' #需要踢人的群名字统一关键词,也可以是一个字母符号,如果群名字不带这个则不会踢人 groups_list = { "aaa测试3": "pUalcqhBTcdg44G_j9s5K-XCUF83FRHB", "aaa测试1": "928JLh8AAA65RyryJLbTiPKnV0NJWXIi", "aaa测试2": "Y26osJNBCaOMENv71XMT7CmOT1hYhee9" } ################################################ # 全局变量 call_back_message = None script = None # 用于存储已加载的脚本 unique_data_set = set() # 创建队列 ls_queue = queue.Queue() insert_queue = queue.Queue() msg_no_queue = queue.Queue() # 创建锁 search_queue_lock = threading.Lock() # 连接参数 h2_jdbc_url = "jdbc:h2:./test" h2_user = "test" h2_password = "test" h2_driver_path = "h2-1.4.200.jar" if not os.path.exists(os.path.expanduser("./test.mv.db")): # 数据库文件不存在,创建数据库和表 if create_table(): print("数据库表创建成功!") else: os.remove("./test.mv.db") exit("[!] 数据表创建失败,退出代码!") print("[-] 正在启动应用APP...") session = usb_device() time.sleep(8) #APP启动有加载时间 try: print("[-] 检测状态: 正在检测信息...") # 启动线程模拟函数的同时运行 threading.Thread(target=run1).start() threading.Thread(target=run2).start() threading.Thread(target=run3).start() threading.Thread(target=run4).start() threading.Thread(target=run5).start() threading.Thread(target=run6).start() except KeyboardInterrupt: script.unload() print("[-] 脚本已卸载,程序退出。")
思路2:通过调用APP本地数据库
在审计源码过程中发现泡泡APP存在本地数据库,将最新的消息全部存在本地数据库中,当时以为终于找到出路了,通过sql语句调用能非常简单快速的达到我要的效果。
奈何困难总比办法多,在数据库上又卡住了;
数据库使用了sqlcipher 4.5.3 加密,在尝试用pysqlcipher3库打开sqlite数据库始终连不上,查阅半天资料发现pysqlcipher3库不支持sqlcipher 4.5.3 加密的sqlite数据库;
后又尝试本地编译sqlcipher失败,搭建环境各种报错,百度谷歌都快翻烂了都没解决,这里不得不吐槽下,为什么这么不容易入手的加密方式还这么出名和流行?就因为它难入手吗。。。
这里不得不提一下,后来换思路的时候才看到官方提供了windows编译的具体步骤教程,可惜当时已经对它彻底失望了,所以没去尝试,不过我感觉最终还是会编译失败。。。
后面通过 DB Browser 和 SQLiteStudio 2款sqlit数据库管理工具连上了数据库;
然后又查阅资料,python应该如何依赖这些工具来调用数据库呢?
2款工具都不存在API,只能考虑命令行了;
DB Browser 不存在Windows下命令行,不过可以在Linux下安装,所以使用 Termux 命令执行APP在安卓系统安装,结果发现源找不到库DB Browser的库,更换源也没辙,官方也没有提供Linux下编译安装包。
SQLiteStudio 直接不支持命令行,不过发现它可以直接通过adb的方式连接上APP中的数据库,但是因为没法通过python调用,所以最后pass掉了。
思路3:从socket上面入手
最开始的时候还是满脑子盯着安卓代码研究,看它消息接受是怎么接受的,最后定位到了它socket接受消息的方法;
通过hook发现一个蛋疼的事情,每次消息接受scoket的IP端口都在变化,当时不懂是为什么,后听一哥们说是中转服务器,任意连一个就行了;
尝试使用python连接这些IP端口总是不成功,请教那哥们后那哥们告诉我用web端的websocket,他通过websocket网页在线连接测试成功连接上,所以我最后又将目光放在了WEB端的websocket上了。
研究 websocket
通过各种查阅资料,去了解websocket的原理、流程等方面资料后,开始尝试用python连接;
连上以后并没有收到任何数据,隔一段时间连接就断开了,这也是我最开始不考虑webscoket的原因,因为不懂不了解;
后面尝试网上的发送心跳保持长连接活跃的方法,发现还是没屁用,收不到数据,20秒断开连接。
卡了几天后,开始研究WEB端的JS代码,看他的JS是如何连接处理websocket接受数据的,
JS连接webscoket代码:https://im.mlskm.com/js/aiti-js-sdk-1.0.js
经过分析发现他连上webscoket后先构造登录数据包登录,登录后每20秒发送一次自定义构造的心跳数据包,通过查看它打印的控制台日志也是如此流程,这也是我收不到数据和断开连接的原因;
最后通过一点点根据JS代码来构造数据包,成功使用python连接登录并接受到了数据,本以为终于可以告别这次苦逼的研究了,奈何又遇到了新问题,20秒自动断开连接!!
来回检查代码对比代码,发送心跳后服务端返回心跳正常,并且心跳发送也是10秒发一次,为什么20秒还是自动断开了?
这个问题卡了我两三天,最后成功的时候才发现,python 的 websockets 库就是个垃圾,垃圾中的超级垃圾,坑死我了,我不知道它为什么会20秒断开,看看AI怎么评价 websockets 的:
使用 websocket 库成功搞定
最后把websockets库换成websocket库成功保持长连接活跃,也能持续接受新消息,只能说非常Nice,剩下的就是处理和优化了,贴上最终代码和截图:
代码:
import queue import websocket import threading import re import base64 import random import requests import time import json import hashlib from Crypto.Cipher import DES from Crypto.Util.Padding import pad,unpad # # 群ID加密 # def encryptCustId(custId): custId = str(custId) custIdArray = [int(digit) for digit in custId] encrypted_message = encryptByDES(byteToString(custIdArray)) encrypted_message = encrypted_message.replace('/', '_').replace('+', '-') return encrypted_message #加密 def encryptByDES(message, key='klohjmz_'): key = key.encode('utf-8') iv = key message = message.encode('utf-8') cipher = DES.new(key, DES.MODE_CBC, iv) ciphertext = cipher.encrypt(pad(message, DES.block_size)) encrypted_message = base64.b64encode(ciphertext).decode('utf-8') return encrypted_message #解密 def decryptByDES(ciphertext, key='klohjmz_'): key_bytes = key.encode('utf-8') iv_bytes = key.encode('utf-8') cipher = DES.new(key_bytes, DES.MODE_CBC, iv_bytes) decrypted = unpad(cipher.decrypt(base64.b64decode(ciphertext.replace('_', '/').replace('-', '+'))), DES.block_size) return stringify(decrypted) def stringify(a): m = [] for t in a: m.append(str(t & 15)) return ''.join(m) def byteToString(arr): if isinstance(arr, str): return arr str_ = '' i = 0 while i < len(arr): one = format(arr[i], '08b') v = re.match(r'^1+?(?=0)', one) if v and len(one) == 8: bytes_length = len(v.group(0)) store = format(arr[i], '08b')[7 - bytes_length:] for st in range(1, bytes_length): store += format(arr[i + st], '08b')[2:] str_ += chr(int(store, 2)) i += bytes_length - 1 else: str_ += chr(arr[i]) i += 1 return str_ # # 接受数据解析 # def parse_number_data(data): if len(data) == 8: v1 = int.from_bytes(data[0:4], byteorder='little', signed=False) v2 = int.from_bytes(data[4:8], byteorder='little', signed=False) mp = 2 ** 32 x = v2 y = mp v1n = v1 result = x * y + v1n return str(result) v = 0 for i, n in enumerate(data): n = n if n >= 0 else n & 0xff v += n * (256 ** i) return v def parse_string(arr): return uint_to_string(bytearray(arr)) def uint_to_string(uint_array): encoded_string = ''.join(chr(byte) for byte in uint_array) decoded_string = bytes(encoded_string, 'latin-1').decode('utf-8') return decoded_string # # 登录 # pfcCounter = 0 def next_message_num(): global pfcCounter pfcCounter += 1 pfc = pfcCounter result = [] while pfc > 0: temp = pfc pfc = pfc // 256 result.append(temp - pfc * 256) while len(result) < 8: result.append(0) result[3] = random.randint(1, 9) result[4] = random.randint(1, 9) result[5] = random.randint(1, 9) result[6] = random.randint(1, 9) result[7] = random.randint(1, 9) return result def current_times(): timestamp = int(time.time()) result = [0] * 8 i = 7 while timestamp > 0: temp = timestamp timestamp = timestamp // 256 result[i] = temp - timestamp * 256 i -= 1 return result def calc_checksum(buffer, buffer_len): if buffer is None: return [0, 0, 0, 0] tmp = [0, 0, 0, 0] dest = [0, 0, 0, 0] len = 0 offset = 0 while len < buffer_len: if len + 4 > buffer_len: offset = buffer_len - len else: offset = 4 for i in range(offset): tmp[i] ^= buffer[len + i] len += 4 for i in range(4): dest[i] = tmp[i] return dest def split_cust_id(cust_id): result = [0] * 8 i = 0 while cust_id > 0: temp = cust_id cust_id = cust_id // 256 result[i] = temp - cust_id * 256 i += 1 return result def split_auth(auth): result = [ord(char) for char in auth] if len(result) < 32: for i in range(len(result), 32): result.append(0) return result def md5_hash(text): md5_hasher = hashlib.md5() md5_hasher.update(text.encode('utf-8')) hashed_text = md5_hasher.hexdigest() return hashed_text def md5Psw(account, pwd): hashed_pwd = md5_hash( md5_hash(pwd) + account + 'MOSGRAM') return hashed_pwd def login(username,pwd): global open_id_qj,im_token,token data = { "username": f"{username}", "password": md5Psw(username, pwd), "device_type":"WEB" } headers = { "Content-Type":"application/json; charset=UTF-8", "X-Requested-With":"XMLHttpRequest", "App_id":"qiyunxin", "timestamp": str(int(time.time() * 1000)), "sign": md5_hash(str(int(time.time() * 1000)) + "WEB"), "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" } try: print('[信息] WEB端登录中...') response = requests.post('http://api.vvchat.im/userservices/v2/user/login', headers=headers,data=json.dumps(data)) data = response.json() if 'err_msg' in data: exit('[信息] WEB端登录失败: %s!' % data['err_msg']) elif 'open_id' in data: print('[信息] WEB端成功!') open_id_qj = data['open_id'] im_token = data['im_token'] token = data['token'] except requests.exceptions.RequestException as e: print('[信息] WEB端登录出错:%s'%(e)) except Exception as e: print('[信息] WEB端登录异常报错:%s'%(e)) def revoke_msg(msg_no,groups_id,group_name): print(f"[巡查] 开始撤回群 {group_name} 消息编号 {msg_no} 消息!") data = {"msg_no": msg_no,"msg_time": int(time.time()) * 1000} headers = {"Content-Type":"application/json; charset=UTF-8","X-Requested-With":"XMLHttpRequest","App_id":"qiyunxin","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36","Authorization":token} try: response = requests.post(f'http://api.vvchat.im/groupapi/v1/users/{open_id_qj}/groups/{groups_id}/revoke_msg', headers=headers,data=json.dumps(data)) result = response.json() if result['err_msg'] == 'OK': print(f"[巡查] 群 {group_name} 消息编号 {msg_no} 撤回成功!") else: print(f"[巡查] 群 {group_name} 消息编号 {msg_no} 撤回失败,原因:{result['err_msg']} 请检查...") except requests.exceptions.RequestException as e: print('[巡查] 群 %s 消息编号 %s 撤回出错:%s ,重试一次...'%(group_name,msg_no,e)) revoke_msg(msg_no,groups_id) except Exception as e: print('[巡查] 群 %s 消息编号 %s 撤回异常报错:%s ,重试一次...'%(group_name,msg_no,e)) revoke_msg(msg_no,groups_id) def delete_member(from_cust_id,groups_id): group_name = groups_name(groups_id) print(f"[巡查] 开始剔除群 {group_name} 成员 {from_cust_id}") from_cust_id_encrypt = encryptCustId(from_cust_id) data = [{"name":"default","open_id": from_cust_id_encrypt,"cust_id": from_cust_id}] headers = {"Content-Type":"application/json; charset=UTF-8","X-Requested-With":"XMLHttpRequest","App_id":"qiyunxin","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36","Authorization":token} try: response = requests.delete(f'http://api.vvchat.im/groupapi/v1/users/{open_id_qj}/groups/{groups_id}/members', headers=headers,json=data) result = response.json() if result['err_msg'] == 'OK': print(f"[巡查] 群 {group_name} 成员 {from_cust_id} 剔除成功!") else: print(f"[巡查] 群 {group_name} 成员 {from_cust_id} 剔除失败,原因:{result['err_msg']} 请检查...") except requests.exceptions.RequestException as e: print('[巡查] 群 %s 成员 %s 剔除出错:%s,重试一次...'%(group_name,from_cust_id,e)) delete_member(from_cust_id,groups_id) except Exception as e: print('[巡查] 群 %s 成员 %s 剔除异常报错:%s,重试一次...'%(group_name,from_cust_id,e)) delete_member(from_cust_id,groups_id) def groups_name(groups_id): headers = {"App_id":"qiyunxin","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36","Authorization":token} try: response = requests.get(f'http://api.vvchat.im/groupapi/v1/users/{open_id_qj}/groups/{groups_id}', headers=headers) result = response.json() if result: return result['group_name'] except requests.exceptions.RequestException as e: print('[巡查] 获取群名称 出错:%s'%(e)) groups_name(groups_id) except Exception as e: print('[巡查] 获取群名称 异常报错:%s'%(e)) groups_name(groups_id) #登录包 def wss_login_data(from_cust_id,im_token): from_cust_id = split_cust_id(int(decryptByDES(from_cust_id))) auth = split_auth(im_token) msg_no = next_message_num() buffer = [] msg_type = [1, 0] body = [] body += login_type + from_cust_id + auth checkCode = calc_checksum(body, len(body)) body += checkCode + end buffer += start + length + version + msg_no + msg_type + body len_body = len(body) buffer[1] = len_body - ((len_body >> 16) << 16) buffer[2] = len_body >> 8 buffer[3] = len_body >> 16 buffer[4] = 0 return buffer #心跳包 def wss_xt_data(open_id): buffer = [] from_cust_id = split_cust_id(int(decryptByDES(open_id))) msg_no = next_message_num() msg_type = [6, 0] body = [] body += login_type + from_cust_id checkCode = calc_checksum(body, len(body)) body += checkCode + end buffer += start + length + version + msg_no + msg_type + body len_body = len(body) buffer[1] = len_body - ((len_body >> 16) << 16); buffer[2] = len_body >> 8; buffer[3] = len_body >> 16; buffer[4] = 0; return buffer #消息检测 def check_and_clean_data(): global msg_queue wg_list = [] msg_list = [] while True: if not msg_queue.empty(): for _ in range(4): item = msg_queue.get() msg_list.append(item) print("[巡查] 巡查检测中...") matching_data = [item for item in msg_list if item[0] == msg_list[-1][0] and item[1] == msg_list[-1][1] and item[3] == msg_list[-1][3]] if len(matching_data) >= 3:# 三者相同的存在大于等于三条 timestamps = [item[4] for item in matching_data] #获取时间字段 time_diffs = [int(timestamps[i + 1]) - int(timestamps[i]) for i in range(len(timestamps) - 1)] for diff, data in zip(time_diffs, matching_data[1:]): if diff == 0 or diff <= 3: for x in msg_list: if data[0] == x[0]: wg_list.append(x) matching_data = [] if wg_list: # 使用集合去重,通过将子列表转换为元组 unique_list = list(set(tuple(sub_list) for sub_list in wg_list)) # 将元组转换回列表 unique_nested_list = [list(sub_tuple) for sub_tuple in unique_list] from_cust_ids = set() # 用于存储去重后的 from_cust_id for group in unique_nested_list: if group: # 确保列表不为空 from_cust_ids.add((group[0], group[1])) for user in from_cust_ids: #踢人 print(f"[巡查] 检测到群: {groups_name(user[1])} 出现违规者:{user[0]}!") delete_member(x[0],x[1]) for msg_no in msg_list: for i in from_cust_ids: if msg_no[0] == i[0] and msg_no[1] == i[1]: #获取群名称 group_name = groups_name(msg_no[1]) #撤回 revoke_msg(msg_no[2],msg_no[1],group_name) wg_list = [] msg_list = [] else: print("[巡查] 消息未检测到频繁!") #服务端传输数据类型判断 def eventReceiveMessage(ws,msg): global msg_queue msg_type = parse_number_data(msg[14:16]) status = parse_number_data(msg[25:29]) if msg_type == 2: if status == 200: print('[信息] websocket 端登录成功,开始监听信息...') # 创建发送心跳包的线程 heartbeat_thread = threading.Thread(target=heartbeat, args=(ws,)) heartbeat_thread.daemon = True heartbeat_thread.start() # 创建消息处理线程 msg_thread = threading.Thread(target=check_and_clean_data) msg_thread.daemon = True msg_thread.start() elif status == 401: print('[信息] websocket 端登录失败!') ws.close() elif msg_type == 5: chatType = parse_number_data(msg[16:17]) content = parse_string(msg[61:61+parse_number_data(msg[57:61])]) content_json = json.loads(content)['content'] msgNo = parse_number_data(msg[6:14]) groups_id = encryptCustId(parse_number_data(msg[25:33])) msgTime = parse_number_data(msg[17:25]) from_cust_id = parse_number_data(msg[33:41]) data = [from_cust_id,groups_id,msgNo,content_json,msgTime] if chatType == 2: print(f"[消息] 发送者:{from_cust_id} 发自群:{groups_name(groups_id)} 发送时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(int(msgTime)))}") msg_queue.put(data) elif msg_type == 6: print("[信息] websocket 端心跳正常!") else: pass def heartbeat(ws): while True: try: print(f'[信息] 发送心跳推送...') ws.send(bytes(wss_xt_data(open_id_qj))) time.sleep(15) except: print("[信息] 心跳推送失败!") ws.close() def on_error(ws, error): print(f"[错误] {error}") def on_close(ws, close_status_code, close_msg): print("[信息] websocket 端连接已断开,开始重新启动...") run() def on_message(ws, message): eventReceiveMessage(ws,message) def on_open(ws): print("[信息] websocket 端已连接!") login(username,password) print("[信息] websocket 端登录中...") ws.send(bytes(wss_login_data(open_id_qj,im_token))) def run(): try: # 创建WebSocket连接 print("[信息] websocket 端连接状态检测...") ws = websocket.WebSocketApp(url, on_message=on_message, on_open=on_open,on_error=on_error,on_close=on_close) # 启动WebSocket连接 ws.run_forever() except: print("[信息] websocket 端连接启动失败!") if __name__ == "__main__": global username,password ###############配置专区############### username = '0086' #0086+手机号 password = '' url = "wss://im.mlskm.com/ws8" ###############配置专区############### #全局变量定义 msg_queue = queue.Queue() start = [2] version = [1] end = [3] length = [0, 0, 0, 0] login_type = [2] #启动 run()
截图:
最终实验效果如上,不会遗漏消息,并且支持多个群管理和支持全自动化(掉线重连),只需要挂着脚本啥也不需要管,就非常OK,搞定这一瞬间感觉整个人都活了,总花费差不多两周,太不容易了。。。
总结
- 入手学习到了逆向脱壳,以及 app hook 方面的知识。
- 对安卓中的Java代码更加熟悉。
- 入手了 sqlite 数据库及 sqlcipher 加密方式方面的知识。
- 入手了socket、websocket方面的知识。
- 对python更加熟练。
虽然整个过程非常痛苦,每天身体都是熬过夜的感觉,2个太阳穴也是非常的痛,不过结局还是好的,至少没白折腾,过程中也学习到了许多新知识点。
不过我还是希望下次我不会这么掘不会有这么大的研究心理了,不服老不行了,老了老了,还是保命要紧!
最后的最后,不得不吐槽一句,什么垃圾聊天软件,连防炸群的功能都舍不得开发一下!
本文作者为Mr.Wu,转载请注明,尊守博主劳动成果!
由于经常折腾代码,可能会导致个别文章内容显示错位或者别的 BUG 影响阅读; 如发现请在该文章下留言告知于我,thank you !
紧急求助,联系方式发一下
学习到了许多新知识
有脚本吗