打造自己的 Jar 文件分析工具:类名匹配 + 二进制搜索 + 日志输出全搞定
版权归作者所有,如有转发,请注明文章出处:https://cyrus-studio.github.io/blog/
前言
在逆向分析、APK 解包或 Java 工程排查中,我们常常需要检查某个 .class 文件是否存在于某个 JAR 包中,或者判断某个关键字是否被硬编码在类的字节码里。
如何通过编写 Python 脚本,帮助你在 JAR 文件中快速定位类、字段,甚至二进制内容。
类路径匹配(路径前缀)
JAR 文件本质是 ZIP 文件,通过 zipfile.ZipFile 解压后可遍历所有文件名(也就是 .class 路径),我们只需检查是否以某个前缀开头即可。
with zipfile.ZipFile(jar_path, 'r') as jar:for entry in jar.namelist():if entry.endswith(".class") and entry.startswith(class_prefix_path):print(f"[✓] Found in: {jar_path} → {entry}")found.append((jar_path, entry))
字节码字段查找(二进制匹配)
通过读取 .class 文件的二进制数据,可以判断是否存在某个硬编码字符串。比如我们要查找 “VERSION_NAME” 是否被写入类的常量池中,就可以用这种方式。
with zipfile.ZipFile(jar_path, 'r') as jar:for entry in jar.namelist():if entry.endswith(".class"):try:with jar.open(entry) as class_file:content = class_file.read()if keyword.encode() in content:print(f"[✓] Found '{keyword}' in {entry} → {jar_path}")found.append((jar_path, entry))except Exception as e:print(f"[!] Failed reading {entry} in {jar_path}: {e}")
注意:这是字节级别的搜索,类似 strings 工具。
路径与内容联合搜索(双重匹配)
同时检查路径与二进制内容,适合用于广泛关键词搜索。
# ① 类名路径中包含关键字
if keyword_path in entry:print(f"[✓] Keyword in class name: {entry} ({jar_path})")matched = True# ② 字节码中包含关键字(如字符串常量)
try:with jar.open(entry) as class_file:content = class_file.read()if keyword_bin in content:print(f"[✓] Keyword in class bytecode: {entry} ({jar_path})")matched = True
except Exception as e:print(f"[!] Failed reading {entry} in {jar_path}: {e}")
编写命令行工具入口
使用 argparse.ArgumentParser 创建参数解析器:
1、directory:要搜索的目录路径,传入的是目录中含有 .jar 文件的位置。
2、keyword:搜索关键字,用于匹配类路径(如 com/example/MyClass)或字节码中的字段内容(如某个字符串、变量名等)。
3、–mode:搜索模式,默认是 “class”,也可以指定为:
-
“class”:只搜索类路径名中是否包含关键字。
-
“field”:只搜索 .class 文件中的字段、方法等内容(二进制搜索)。
-
“all”:两者都搜。
if __name__ == "__main__":import argparseparser = argparse.ArgumentParser(description="Search for class name or class content keyword in JAR files.")parser.add_argument("directory", help="Directory to search")parser.add_argument("keyword", help="Class prefix or bytecode keyword")parser.add_argument("--mode", choices=["class", "field", "all"], default="class",help="Search mode: 'class' (class path), 'field' (bytecode), 'all' (both)")args = parser.parse_args()if args.mode == "class":find_class_in_jars(args.directory, args.keyword)elif args.mode == "field":find_field_in_jars(args.directory, args.keyword)elif args.mode == "all":find_class_and_content_in_jars(args.directory, args.keyword)
使用示例
1. 查找类
查找类是否存在,比如 com.bytedance.retrofit2.SsResponse
(anti-app) PS D:\Python\anti-app\dex2jar> python find_in_jars.py "D:\Python\anti-app\app\douyin\dump_dex\jar" "com.bytedance.retrofit2.SsResponse"
[+] Searching for class prefix: com/bytedance/retrofit2/SsResponse
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes28.jar → com/bytedance/retrofit2/SsResponse.class
[+] Total 1 match(es) found.
支持模糊查找,比如查找 com.bytedance.ttnet 包下所有类
(anti-app) PS D:\Python\anti-app\dex2jar> python find_in_jars.py "D:\Python\anti-app\app\douyin\dump_dex\jar" "com.bytedance.ttnet."
[+] Searching for class prefix: com/bytedance/ttnet/
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes2.jar → com/bytedance/ttnet/TTNetInit$ENV.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes23.jar → com/bytedance/ttnet/debug/DebugSetting.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/bytedance/ttnet/http/HttpRequestInfo.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/bytedance/ttnet/http/RequestContext.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/bytedance/ttnet/HttpClient.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/bytedance/ttnet/ITTNetDepend.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/bytedance/ttnet/TTALog.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/bytedance/ttnet/TTMultiNetwork.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/bytedance/ttnet/TTNetInit.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/bytedance/ttnet/clientkey/ClientKeyManager.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/bytedance/ttnet/config/AppConfig.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/bytedance/ttnet/config/TTHttpCallThrottleControl$DelayMode.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/bytedance/ttnet/config/TTHttpCallThrottleControl.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/bytedance/ttnet/cronet/AbsCronetDependAdapter.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/bytedance/ttnet/diagnosis/TTNetDiagnosisService.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/bytedance/ttnet/priority/TTHttpCallPriorityControl$ModeType.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/bytedance/ttnet/priority/TTHttpCallPriorityControl.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/bytedance/ttnet/retrofit/SsInterceptor.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/bytedance/ttnet/retrofit/SsRetrofitClient.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/bytedance/ttnet/throttle/TTNetThrottle.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/bytedance/ttnet/tnc/TNCManager$TNCUpdateSource.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/bytedance/ttnet/tnc/TNCManager.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/bytedance/ttnet/utils/RetrofitUtils$CompressType.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes37.jar → com/bytedance/ttnet/utils/RetrofitUtils.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes47.jar → com/bytedance/ttnet/diagnosis/IDiagnosisRequest.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes47.jar → com/bytedance/ttnet/diagnosis/IDiagnosisCallback.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes47.jar → com/bytedance/ttnet/diagnosis/TTGameDiagnosisService.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes49.jar → com/bytedance/ttnet/http/IRequestHolder.class
[✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes7.jar → com/bytedance/ttnet/INetworkApi.class
[+] Total 29 match(es) found.
2. 查找类字节码
查找类字节码中是否包含指定字段(如 VERSION_NAME)
(anti-app) PS D:\Python\anti-app\dex2jar> python find_in_jars.py "D:\Python\anti-app\app\douyin\dump_dex\jar" VERSION_NAME --mode field
[✓] Found 'VERSION_NAME' in com/bykv/vk/openvk/api/proto/BuildConfig.class → D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk.jar
[✓] Found 'VERSION_NAME' in com/byted/cast/proxy/BuildConfig.class → D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk.jar
[✓] Found 'VERSION_NAME' in com/ss/ttm/player/TTPlayerConfiger.class → D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes9.jar
. . . . . .
[+] Total 128 matches found.
3. 类路径与字节码联合搜索
同时查找类路径和字节码是否包含关键词
(anti-app) PS D:\Python\anti-app\dex2jar> python find_in_jars.py "D:\Python\anti-app\app\douyin\dump_dex\jar" Retrofit --mode all
[+] Searching for class path or class bytecode containing: Retrofit
[✓] Keyword in class bytecode: X/01Ek.class (D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk.jar)
[✓] Keyword in class name: com/bytedance/android/live/broadcast/api/BroadcastConfigRetrofitApi.class (D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk.jar)
. . . . . .
[✓] Keyword in class bytecode: X/0ppk.class (D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes27.jar)
[✓] Keyword in class bytecode: kotlin/jvm/internal/ALambdaS879S0100000_16.class (D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes9.jar)
[+] Total 1639 match(es) found.
[+] Matched JAR count: 49
[+] Matched JAR files:- D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk.jar- D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes2.jar- D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes3.jar- D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes4.jar- D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes5.jar- D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes6.jar- D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes7.jar- D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes8.jar- D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes9.jar- D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes10.jar. . . . . .
将日志同时输出到 终端和日志文件
为了将日志同时输出到 终端和指定的日志文件,可以引入 Python 标准库的 logging 模块,并通过一个命令行参数 --logfile 来控制输出日志文件路径。
1. 顶部导入 logging 模块
import logging
2. 添加日志初始化函数
在文件顶部添加如下函数来配置日志输出:
def setup_logger(logfile: str = None):"""设置日志输出,可选输出到文件。:param logfile: 日志文件路径(可选)"""log_format = "[%(asctime)s] %(message)s"logging.basicConfig(level=logging.INFO,format=log_format,handlers=[logging.StreamHandler(), # 控制台输出logging.FileHandler(logfile, mode='w', encoding='utf-8') if logfile else logging.NullHandler()])
3. 替换 print(…) 为 logging.info(…) 或 logging.warning(…)
例如:
print(f"[+] Searching for class prefix: {class_prefix_path}")
替换为:
logging.info(f"Searching for class prefix: {class_prefix_path}")
4. 修改 main 中增加参数并初始化日志
parser.add_argument("--logfile", help="Log output to specified file (optional)")
然后在解析参数之后调用:
setup_logger(args.logfile)
测试
调用时在调用命令后加上 --logfile log.txt
python find_in_jars.py "D:\Python\anti-app\app\douyin\dump_dex\jar" "com.bytedance.retrofit2.SsResponse" --logfile log.txt
终端和 log.txt 都会输出日志信息。
(anti-app) PS D:\Python\anti-app\dex2jar> python find_in_jars.py "D:\Python\anti-app\app\douyin\dump_dex\jar" "com.bytedance.retrofit2.SsResponse" --logfile log.txt
[2025-07-20 22:27:57,695] [+] Searching for class prefix: com/bytedance/retrofit2/SsResponse
[2025-07-20 22:27:58,796] [✓] Found in: D:\Python\anti-app\app\douyin\dump_dex\jar\base.apk_classes28.jar → com/bytedance/retrofit2/SsResponse.class
[2025-07-20 22:28:00,267] [+] Total 1 match(es) found.
log.txt
完整源码
import logging
import os
import re
import zipfile
from typing import Listdef setup_logger(logfile: str = None):"""设置日志输出,可选输出到文件。:param logfile: 日志文件路径(可选)"""log_format = "[%(asctime)s] %(message)s"logging.basicConfig(level=logging.INFO,format=log_format,handlers=[logging.StreamHandler(), # 控制台输出logging.FileHandler(logfile, mode='w', encoding='utf-8') if logfile else logging.NullHandler()])def find_class_in_jars(directory, class_prefix):"""查找所有包含指定类名前缀的 .class 文件(支持包名或类名前缀匹配):param directory: 要扫描的目录:param class_prefix: 类名或包名前缀(如 com.example. 或 com.example.MyClass)"""if not class_prefix:logging.info("[-] Class name prefix cannot be empty.")return# 将类名转换为 JAR 中的路径格式(例如 com.example. → com/example/)class_prefix_path = class_prefix.replace('.', '/')logging.info(f"[+] Searching for class prefix: {class_prefix_path}")found = []for root, _, files in os.walk(directory):for file in files:if file.endswith(".jar"):jar_path = os.path.join(root, file)try:with zipfile.ZipFile(jar_path, 'r') as jar:for entry in jar.namelist():if entry.endswith(".class") and entry.startswith(class_prefix_path):logging.info(f"[✓] Found in: {jar_path} → {entry}")found.append((jar_path, entry))except zipfile.BadZipFile:logging.info(f"[!] Skipping corrupted jar: {jar_path}")if not found:logging.info("[-] No matching class found.")else:logging.info(f"[+] Total {len(found)} match(es) found.")def find_field_in_jars(directory, keyword):"""在指定目录下所有 jar 文件中查找包含指定字段的类(.class)文件:param directory: 待扫描目录路径:param keyword: 要查找的字段字符串(如 VERSION_NAME)"""found = []for root, _, files in os.walk(directory):for file in files:if file.endswith(".jar"):jar_path = os.path.join(root, file)try:with zipfile.ZipFile(jar_path, 'r') as jar:for entry in jar.namelist():if entry.endswith(".class"):try:with jar.open(entry) as class_file:content = class_file.read()if keyword.encode() in content:logging.info(f"[✓] Found '{keyword}' in {entry} → {jar_path}")found.append((jar_path, entry))except Exception as e:logging.info(f"[!] Failed reading {entry} in {jar_path}: {e}")except zipfile.BadZipFile:logging.info(f"[!] Bad JAR file: {jar_path}")if not found:logging.info(f"[-] No classes containing '{keyword}' found.")else:logging.info(f"\n[+] Total {len(found)} matches found.")return founddef sort_jar_paths(jar_paths: List[str]) -> List[str]:"""对包含 base.apk、base.apk_classesN.jar 的路径列表进行排序,确保 _classes2 排在 _classes10 前面。:param jar_paths: 未排序的 jar 文件路径列表:return: 排序后的 jar 文件路径列表"""def extract_index(path: str) -> int:"""提取路径中 _classesN 的 N 数字部分用于排序。如果是 base.apk.jar 则返回 0,表示优先排序。"""match = re.search(r'_classes(\d+)\.jar$', path)if match:return int(match.group(1)) # 提取 _classesN 中的 Nreturn 0 # base.apk.jar 没有 _classesN,默认最小值# 按照提取出的数字索引进行排序return sorted(jar_paths, key=extract_index)def find_class_and_content_in_jars(directory, keyword):"""在指定目录下所有 JAR 中搜索:1. 类路径中包含关键字的类名2. 类的字节码中包含关键字内容:param directory: 要搜索的目录:param keyword: 要查找的关键字(支持类名路径或内容关键字)"""if not keyword:logging.info("[-] 关键词不能为空")returnlogging.info(f"[+] Searching for class path or class bytecode containing: {keyword}")keyword_bin = keyword.encode() # 转为二进制用于内容匹配keyword_path = keyword.replace('.', '/')matched_entries = []matched_jars = set()for root, _, files in os.walk(directory):for file in files:if file.endswith(".jar"):jar_path = os.path.join(root, file)try:with zipfile.ZipFile(jar_path, 'r') as jar:for entry in jar.namelist():if not entry.endswith(".class"):continuematched = False# ① 类名路径中包含关键字if keyword_path in entry:logging.info(f"[✓] Keyword in class name: {entry} ({jar_path})")matched = True# ② 字节码中包含关键字(如字符串常量)try:with jar.open(entry) as class_file:content = class_file.read()if keyword_bin in content:logging.info(f"[✓] Keyword in class bytecode: {entry} ({jar_path})")matched = Trueexcept Exception as e:logging.info(f"[!] Failed reading {entry} in {jar_path}: {e}")if matched:matched_entries.append((jar_path, entry))matched_jars.add(jar_path)except zipfile.BadZipFile:logging.info(f"[!] Skipping corrupted jar: {jar_path}")if not matched_entries:logging.info(f"[-] No match found for keyword '{keyword}'")else:logging.info(f"\n[+] Total {len(matched_entries)} match(es) found.")logging.info(f"[+] Matched JAR count: {len(matched_jars)}")logging.info("[+] Matched JAR files:")for jar_file in sort_jar_paths(matched_jars):logging.info(f" - {jar_file}")if __name__ == "__main__":r"""示例用法(支持按类路径、类字段内容或同时匹配进行搜索):1. 按类路径查找(是否包含某类):python find_in_jars.py "D:\Python\anti-app\app\douyin\dump_dex\jar" com.bytedance.retrofit2.SsResponse支持包名前缀模糊查找:python find_in_jars.py "D:\Python\anti-app\app\douyin\dump_dex\jar" com.bytedance.ttnet.2. 按字节码内容查找(如字符串常量、字段名等):python find_in_jars.py "D:\Python\anti-app\app\douyin\dump_dex\jar" VERSION_NAME --mode field3. 同时查找类路径和字节码中是否包含关键词:python find_in_jars.py "D:\Python\anti-app\app\douyin\dump_dex\jar" com.bytedance.retrofit2.Retrofit --mode all4. 输出结果到日志文件(可与以上任意命令组合):python find_in_jars.py "D:\Python\anti-app\app\douyin\dump_dex\jar" com.bytedance.ttnet. --mode all --logfile log.txt"""import argparseparser = argparse.ArgumentParser(description="Search for class name or class content keyword in JAR files.")parser.add_argument("directory", help="Directory to search")parser.add_argument("keyword", help="Class prefix or bytecode keyword")parser.add_argument("--mode", choices=["class", "field", "all"], default="class",help="Search mode: 'class' (class path), 'field' (bytecode), 'all' (both)")parser.add_argument("--logfile", help="Log output to specified file (optional)")args = parser.parse_args()# 初始化日志setup_logger(args.logfile)if args.mode == "class":find_class_in_jars(args.directory, args.keyword)elif args.mode == "field":find_field_in_jars(args.directory, args.keyword)elif args.mode == "all":find_class_and_content_in_jars(args.directory, args.keyword)
开源地址:https://github.com/CYRUS-STUDIO/dex2jar