From a18d4506fc6b8c9e7540dc38118b6a5b0cd103dd Mon Sep 17 00:00:00 2001 From: cx <2426607795@qq.com> Date: Sun, 17 Oct 2021 16:33:10 +0800 Subject: [PATCH] delete build cache --- androyara.egg-info/PKG-INFO | 215 -- androyara.egg-info/SOURCES.txt | 41 - androyara.egg-info/dependency_links.txt | 1 - androyara.egg-info/requires.txt | 6 - androyara.egg-info/top_level.txt | 2 - build/lib/androyara/__init__.py | 0 build/lib/androyara/__main__.py | 429 --- build/lib/androyara/core/__init__.py | 0 build/lib/androyara/core/analysis_apk.py | 118 - build/lib/androyara/core/apk_packer.py | 64 - build/lib/androyara/core/apk_parser.py | 859 ----- build/lib/androyara/core/axml_parser.py | 367 -- build/lib/androyara/core/dex_parser.py | 27 - build/lib/androyara/core/yara_matcher.py | 108 - build/lib/androyara/dex/__init__.py | 0 build/lib/androyara/dex/dex_code.py | 8 - build/lib/androyara/dex/dex_header.py | 563 --- build/lib/androyara/dex/dex_method.py | 74 - build/lib/androyara/dex/dex_vm.py | 176 - build/lib/androyara/parser/__init__.py | 10 - build/lib/androyara/parser/base_parser.py | 38 - build/lib/androyara/typeinfo/__init__.py | 0 build/lib/androyara/typeinfo/public.xml | 2925 ---------------- build/lib/androyara/typeinfo/publics.py | 36 - build/lib/androyara/typeinfo/types.py | 3784 --------------------- build/lib/androyara/utils/__init__.py | 0 build/lib/androyara/utils/buffer.py | 175 - build/lib/androyara/utils/mcolor.py | 17 - build/lib/androyara/utils/utility.py | 26 - build/lib/androyara/vsbox/__init__.py | 0 build/lib/androyara/vsbox/hybird.py | 58 - build/lib/androyara/vsbox/threatbook.py | 66 - build/lib/androyara/vsbox/vsbox.py | 106 - build/lib/androyara/vsbox/vt.py | 71 - build/lib/test/__init__.py | 0 build/lib/test/test_apk.py | 44 - build/lib/test/test_axml.py | 31 - build/lib/test/test_dex.py | 29 - build/lib/test/test_vbox.py | 22 - build/lib/test/test_virus.py | 45 - dist/androyara-2.0-py3-none-any.whl | Bin 104432 -> 0 bytes 41 files changed, 10541 deletions(-) delete mode 100644 androyara.egg-info/PKG-INFO delete mode 100644 androyara.egg-info/SOURCES.txt delete mode 100644 androyara.egg-info/dependency_links.txt delete mode 100644 androyara.egg-info/requires.txt delete mode 100644 androyara.egg-info/top_level.txt delete mode 100644 build/lib/androyara/__init__.py delete mode 100644 build/lib/androyara/__main__.py delete mode 100644 build/lib/androyara/core/__init__.py delete mode 100644 build/lib/androyara/core/analysis_apk.py delete mode 100644 build/lib/androyara/core/apk_packer.py delete mode 100644 build/lib/androyara/core/apk_parser.py delete mode 100644 build/lib/androyara/core/axml_parser.py delete mode 100644 build/lib/androyara/core/dex_parser.py delete mode 100644 build/lib/androyara/core/yara_matcher.py delete mode 100644 build/lib/androyara/dex/__init__.py delete mode 100644 build/lib/androyara/dex/dex_code.py delete mode 100644 build/lib/androyara/dex/dex_header.py delete mode 100644 build/lib/androyara/dex/dex_method.py delete mode 100644 build/lib/androyara/dex/dex_vm.py delete mode 100644 build/lib/androyara/parser/__init__.py delete mode 100644 build/lib/androyara/parser/base_parser.py delete mode 100644 build/lib/androyara/typeinfo/__init__.py delete mode 100644 build/lib/androyara/typeinfo/public.xml delete mode 100644 build/lib/androyara/typeinfo/publics.py delete mode 100644 build/lib/androyara/typeinfo/types.py delete mode 100644 build/lib/androyara/utils/__init__.py delete mode 100644 build/lib/androyara/utils/buffer.py delete mode 100644 build/lib/androyara/utils/mcolor.py delete mode 100644 build/lib/androyara/utils/utility.py delete mode 100644 build/lib/androyara/vsbox/__init__.py delete mode 100644 build/lib/androyara/vsbox/hybird.py delete mode 100644 build/lib/androyara/vsbox/threatbook.py delete mode 100644 build/lib/androyara/vsbox/vsbox.py delete mode 100644 build/lib/androyara/vsbox/vt.py delete mode 100644 build/lib/test/__init__.py delete mode 100644 build/lib/test/test_apk.py delete mode 100644 build/lib/test/test_axml.py delete mode 100644 build/lib/test/test_dex.py delete mode 100644 build/lib/test/test_vbox.py delete mode 100644 build/lib/test/test_virus.py delete mode 100644 dist/androyara-2.0-py3-none-any.whl diff --git a/androyara.egg-info/PKG-INFO b/androyara.egg-info/PKG-INFO deleted file mode 100644 index 446cc44..0000000 --- a/androyara.egg-info/PKG-INFO +++ /dev/null @@ -1,215 +0,0 @@ -Metadata-Version: 2.1 -Name: androyara -Version: 2.0 -Summary: A tool is use to analyzer Android malware -Home-page: https://github.com/BiteFoo/androyara -Author: BiteFoo -Author-email: 1653946112@qq.com -License: UNKNOWN -Description: # Androyara - - [![Androyara](https://img.shields.io/badge/androyara%20versions-2.0-blue)](https://github.com/BiteFoo/androyara) - [![license apach-2.0](https://img.shields.io/badge/license%20apach2.0-blue)](https://github.com/BiteFoo/androyara/blob/master/LICENSE-2.0) - - `Androyara` 是基于`python3.7+`开发的`android apk` 分析的工具,主要用于`android`的病毒分析和特征提取,也包括一些其他的信息提取。 - - 主要功能 - - * 读取apk基本信息 - * 读取AndroidManifest.xml 信息 - * 搜索Apk/Dex内的字符串,方法,指令,类型 - * 支持yara - * Vt查询功能 - * APK加固信息 `2021-06-17 add` - - ```shell - python3 androyara.py -h - usage: androyara.py [options] - - optional arguments: - -h, --help show this help message and exit - - options: - {query,search_dex,manifest,apkinfo,search_apk,yara_scan} - query query from VT - search_dex search dex string or method all instructions from dex - manifest Parsing Binary AndroidManifest.xml - apkinfo Apk base info - search_apk search string or method instructions from apk - yara_scan Using yara rule to scan - - ``` - - ## 使用方法 - - ### 读取apk基本信息 - 要想获取一个apk的基本信息包括 - * `application` - * `MainActivity` - * `fingerprint: sha256` - * `signed version: V1 V2 V3` - * `certification` - * `pkgname` - * `appName` - - 使用如下命令 - ```shell - python3 androyara apkinfo -a samples/aaa.apk -i - ``` - > 增加显示加固信息 - - ![packer_info](./img/packer_info.png) - - 还可以查看`apk内的文件`,使用如下命令 - ```shell - python3 androyara.py apkinfo -a samples/aaa.apk --zipinfo - ``` - ![apk_info_zipinfo](./img/apk_info_zipinfo.png) - - - ### 读取AndroidManifest.xml 信息 - 有时候只需要获取`AndroidManifest.xml` 的信息而不需要读取`apk`的全部信息,使用`manifest` 选项可以获取`AndroidManifest.xml`的信息。 - - - **支持AndroidManifest.xml和输入apk来读取** - 主要输出内容 `包名和四大组件信息`,如下 - - ```shell - python3 androyara.py manifest -m samples/AndroidManifest.xml -b - ``` - ![manifest](./img/manifest.png) - 可以只选择查看`activity` 或者其他的组件信息,**还可以查看所有支持exported 属性的组件** - 使用帮助命令 - ```shell - python3 androyara.py manifest -h - ``` - ![manifest_opt](./img/manifest_opt.png) - - 如果想看入口信息,可以使用如下方法 - ```shell - python3 androyara.py manifest -m samples/AndroidManifest.xml -e - ``` - ![manifest_entry](./img/manifest_entry.png) - - ### 搜索Apk/Dex内的字符串,方法,指令,类 - 可以通过命令查看`apk` 或者`dex`内的方法和指令,程序内过滤了些`google`的类。 - - #### 获取apk内的字符串 - > 使用正则表达式来做搜索,也可以指定某个字符串搜索 - - - 基本使用方式可以查看帮助命令 - ![search_apk_cmd](./img/search_apk_cmd.png) - - 可以通过一个正则表达式或者字符串类搜索,例如这里的`://` 获取包含了如下的字符串 - ```shell - http:// - https:// - content:// - protocol:// - ``` - 如下 - ![search_apk_str](./img/search_apk_str.png) - 或者可以全部输出字符串 - ![all_str_cmd](./img/all_str_fix.png) - ![all_str](./img/all_str.png) - - 或者查找是否存在特定的字符串 - - ![search_string](./img/search_string.png) - - #### 获取dex内的字符串 - 使用方式同`apk`类似,只需要指定`.dex`即可。 - - #### 获取类,方法信息 - 例如要想获取所有的方法和类,可以填入一个`-m '' `的空字符串 - ![all_class_defs_cmd](./img/all_class_defs_cmd.png) - ![all_class_defs](./img/all_class_defs.png) - #### 获取指令信息 - 例如这里要`com.nirenr.screencapture.ScreenShot.java -> startVirtual()V`的方法指令,使用如下 - ![method_ins1](./img/method_ins1.png) - 有时候同名的方法很多,但是每个方法的签名和类不一样,因此可以通过`-c classname -m method(signature)` 的方式获取,如下是获取 - ![method_ins2](./img/method_ins2.png) - 获取方式 - ![method_ins4](./img/method_ins4.png) - 使用上述方法就能获取到对应的指令信息。 - - ### 使用yara - - **逆向分析apk** - 在逆向分析一个`apk`之后为了能查杀出对应的家族会需要写规则来查杀,使用`androyara`可以对感兴趣的字符串和方法指令进行获取并快速写出`yara` 规则。 - - 首先通过`字符串`的方式查询是否存在特殊的字符串,例如这里查询`://`如下 - ![search_apk_str](./img/search_apk_str.png) - - 从输出的内容中可以看到比较有价值的请求地址,可以作为一个`yara` 的规则。 - - 接着为了能更准确的查杀对应的病毒,这里逆向分析一下apk后定位一个关键函数,如下 - ![best_for_her](./img/best_for_her_method.png) - 这里将这个方法作为主要的特征`shellcode` ,使用如下方式获取指令信息 - 先获取方法签名信息看是否一致 - ![malware_ins](./img/malware_ins.png) - 接着获取指令信息 - ![malware_ins_dump1](./img/malware_ins_dump1.png) - 最后在`shellcode`中看到输出的结果 - ![malware_ins_dump2](./img/malware_ins_dump2.png) - - **编写yara规则** - ![best_for_her_yara](./img/best_for_her_yara.png) - 扫描结果 - ![yara_result](./img/yara_result.png) - - 为了能更体现准确性,在测试目录放了`200+`的apk,如下 - ![samples](./img/samples.png) - - 再次扫描后如下 - ![all_samples](./img/all_samples.png) - 可以看到准确率还是很好。 - - 还可以支持`.dex`的方式检测,目的是有些`apk`是加固的,可以通过脱壳后进行查杀,具体查看帮助命令 - ```shell - python androyara.py yara_scan -h - ``` - - ### VT查询 - **请将USR_CONFIG_INI设置为指定到user.conf的环境变量** - ```shell - # windows - set USR_CONFIG_INI=D:\\user.config - # Unix - export USR_CONFIG_INI=$HOME/user.config - ``` - - 需要在user.conf 内填写api key - - 可以通过命令行查询`vt`结果,如下 - - ```shell - python3 androyara.py query -s ee70eda8a7f6b209c6bb4780bf2a8a96730c19a78300eb5ec3c25a48e557cb2e - ``` - ![vt_query](./img/vt_query.png) - - - ## build - ```shell - pip3 install -r requirements.txt - python3 setup.py bdist_wheel - pip3 install dist\androyara-version-py3-none-any.whl - ``` - - - ## 感谢 - [androguard](https://github.com/androguard/androguard) - - [malwoverview](https://github.com/alexandreborges/malwoverview/tree/master/malwoverview) - - [yara documents](https://buildmedia.readthedocs.org/media/pdf/yara/latest/yara.pdf) - - [yara python](https://github.com/VirusTotal/yara-python) - -Platform: UNKNOWN -Classifier: Programming Language :: Python :: 3 -Classifier: License :: OSI Approved :: Apache-2.0 -Classifier: Operating System :: OS Independent -Requires-Python: >=3.7 -Description-Content-Type: text/markdown diff --git a/androyara.egg-info/SOURCES.txt b/androyara.egg-info/SOURCES.txt deleted file mode 100644 index 8bdbadb..0000000 --- a/androyara.egg-info/SOURCES.txt +++ /dev/null @@ -1,41 +0,0 @@ -setup.py -androyara/__init__.py -androyara/__main__.py -androyara.egg-info/PKG-INFO -androyara.egg-info/SOURCES.txt -androyara.egg-info/dependency_links.txt -androyara.egg-info/requires.txt -androyara.egg-info/top_level.txt -androyara/core/__init__.py -androyara/core/analysis_apk.py -androyara/core/apk_packer.py -androyara/core/apk_parser.py -androyara/core/axml_parser.py -androyara/core/dex_parser.py -androyara/core/yara_matcher.py -androyara/dex/__init__.py -androyara/dex/dex_code.py -androyara/dex/dex_header.py -androyara/dex/dex_method.py -androyara/dex/dex_vm.py -androyara/parser/__init__.py -androyara/parser/base_parser.py -androyara/typeinfo/__init__.py -androyara/typeinfo/public.xml -androyara/typeinfo/publics.py -androyara/typeinfo/types.py -androyara/utils/__init__.py -androyara/utils/buffer.py -androyara/utils/mcolor.py -androyara/utils/utility.py -androyara/vsbox/__init__.py -androyara/vsbox/hybird.py -androyara/vsbox/threatbook.py -androyara/vsbox/vsbox.py -androyara/vsbox/vt.py -test/__init__.py -test/test_apk.py -test/test_axml.py -test/test_dex.py -test/test_vbox.py -test/test_virus.py \ No newline at end of file diff --git a/androyara.egg-info/dependency_links.txt b/androyara.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/androyara.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/androyara.egg-info/requires.txt b/androyara.egg-info/requires.txt deleted file mode 100644 index a3cb657..0000000 --- a/androyara.egg-info/requires.txt +++ /dev/null @@ -1,6 +0,0 @@ -termcolor>=1.1.0 -lxml>=4.6.2 -requests>=2.25.1 -yara>=1.7.7 -asn1crypto>=1.4.0 -androguard>=3.3.5 diff --git a/androyara.egg-info/top_level.txt b/androyara.egg-info/top_level.txt deleted file mode 100644 index a90daf2..0000000 --- a/androyara.egg-info/top_level.txt +++ /dev/null @@ -1,2 +0,0 @@ -androyara -test diff --git a/build/lib/androyara/__init__.py b/build/lib/androyara/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/lib/androyara/__main__.py b/build/lib/androyara/__main__.py deleted file mode 100644 index 7c9122f..0000000 --- a/build/lib/androyara/__main__.py +++ /dev/null @@ -1,429 +0,0 @@ -# coding:utf8 -''' -@File : androyara.py -@Author : Loopher -@Version : 1.0 -@License : (C)Copyright 2020-2021,Loopher -@Desc : androyara main entry -''' - -import hashlib -import os -import argparse -import sys -import json -import time -from concurrent.futures import ThreadPoolExecutor,as_completed -from androyara.utils.utility import echo -from androyara.vsbox.threatbook import ThreatbookSandbox -from androyara.vsbox.vt import VT -from androyara.core.apk_parser import ApkPaser -from androyara.core.axml_parser import AndroidManifestXmlParser -from androyara.dex.dex_vm import DexFileVM -from androyara.core.yara_matcher import YaraMatcher - - - -light_yellow = '\033[33m' -light_blue = '\033[36m' -yellow = '\033[33m' -pink = '\033[35m' -white = '\033[37m' -red = '\033[31m' -reset = '\033[37m' -green = '\033[32m' - - - - -pattern = None -save = None -rule = None -input_file = None -method_name_arg = None -pkg = None -fingerprint = None - - -def save_file(save, info): - - with open(save, 'a+') as fp: - try: - if isinstance(info, bytes): - info = str(info, encoding="utf-8") - fp.write(info) - fp.write("\n") - except Exception as e: - pass - - -def query_report(args): - """ - all file type - """ - - # default for vt - resource = args.resource - name = args.name # vendor's name : VT threatbook - bsize = 65536 - buff = None - if input_file is not None and os.path.isfile(resource): - sha256 = hashlib.sha256() - while True: - with open(resource, 'rb') as fp: - buff = fp.read(bsize) - if buff is None: - break - sha256.update(buff) - - resource = sha256.hexdigest() - - if resource is None or resource == '': - echo("error", "query_report resouroce must be empty or None", color="red") - return - if name is not None and name == 'threatbook': - ThreatbookSandbox(resource).analysis() - return - # query all online - online_sandboxies = [ThreatbookSandbox(resource), VT(resource)] - for sb in online_sandboxies: - sb.analysis() - # vt = VT(resource) - # vt.analysis() - - -def apk_info(args): - """ - 默认输出apk内的基本信息 - apk的指纹信息 - AndroidManifest.xml内的信息 - 使用-z --zipinfo 读取apk内的所有文件名信息 - """ - - input_file = args.apk - zip_info = args.zipinfo - dex_num = args.dexnum - info = args.info - suffix = args.suffix # like .apk,.APK,.bin - if suffix is None: - suffix = ".apk,.APK" - - if input_file is None or not os.path.isfile(input_file): - echo("error", "need a apk file as input.", "red") - sys.exit(1) - # 可以是任意文件,内部如果读取失败,则会直接异常 - # found = False - # for s in suffix.split(","): - # if input_file.endswith(s): - # #echo("error", "need a apk file", 'red') - # # return - # found = True - # if found is False: - # echo("error", "need a apk file", 'red') - # return - - start = time.time() - apk_parser = ApkPaser(input_file) - if dex_num: - for dexname in apk_parser.get_all_dexs(name=True): - echo("dexname", dexname) - print("costs: {}".format(time.time() - start)) - - if info: - base_info = apk_parser.apk_base_info() - print("") - echo("AppName", base_info['app_name']) - if base_info['packer_name'] != "N/A": - echo("packer", "App may be packed by {}".format( - base_info['packer_name']), color="red") - print("") - echo("apkInfo", "\n{}".format( - json.dumps(base_info, indent=2)), "yellow") - print("--"*20) - print("costs: {}".format(time.time() - start)) - - if zip_info: - for f in apk_parser.get_file_names(): - echo("info", "-> %s" % (f)) - - -def extract_android_manifest_info(args): - - input_file = args.manifest - entry = args.entry - acs = args.activities - rs = args.receivers - ss = args.services - ps = args.providers - both = args.both - exported = args.exported - pm = args.permission - - if not os.path.isfile(input_file): - echo("error", "need apk or AndroidManifest.xml as input file!!", 'red') - return - elif input_file.endswith('.xml'): - axml = AndroidManifestXmlParser(input_file) - axml.show_manifest(acs, rs, ss, ps, entry, both, exported, pm) - elif input_file.endswith('apk') or input_file.endswith('bin'): - # for some reason,we can alse check sample.bin - apk_parser = ApkPaser(input_file) - if apk_parser.ok(): - apk_parser.show_manifest( - acs, rs, ss, ps, entry, both, exported, pm) - # echo("info", "\n"+str(apk_parser.mainifest_info())) - else: - echo("error", "unknow {} filtype ".format(input_file), 'red') - - -def extract_apk_info(args): - input_file = args.apk - pattern = args.string - method_name_arg = args.method - clazz_name = args.clazz - dump = args.print_ins - save = args.save # save strings or methods - # if save: - # echo("save", "date save at "+f) - executor = ThreadPoolExecutor(max_workers = 4) - files = [] - - def show_info(parser,dexname,vm): - echo("dexname", "--> %s" % (dexname)) - if save: - f = os.getcwd()+os.sep+"%s.txt" % (dexname) - files.append(f) - if os.path.isfile(f): - os.remove(f) - - if pattern is not None: - # default all dex data - for s in parser.all_strings([pattern], dex_vm=vm): - - if save: - save_file(f, s) - else: - try: - echo("string", "%s" % (s), 'yellow') - except UnicodeDecodeError as e: - print("--> Unicode error ,string type {}".format(type(s))) - raise e - - if method_name_arg is not None: - - apk_parser.analysis_dex( - clazz_name, method_name_arg, dump, dex_vm=dex_vm) - if save: - for f in files: - echo("save", "data save at "+f) - - - if input_file is None or not os.path.isfile(input_file): - echo("error", "need a apk file : %s" % (input_file), 'red') - return - if pattern is None: - echo("warning", "no string specificed", 'yellow') - # pattern = '' - - apk_parser = ApkPaser(input_file) - if not apk_parser.ok(): - return - - - workers = [] - for dexname, dex_vm in apk_parser.all_dex_vms(): - workers.append(executor.submit(fn = show_info,parser=apk_parser,dexname = dexname,vm =dex_vm)) - # pass - for t in as_completed(workers): - t.result() - - -def dex_info(args): - - input_file = args.dex - pattern = args.string - method_name_arg = args.method - pkg = args.pkgname - clazz_name = args.clazz - dump = args.print_ins - - if input_file is None: - echo("error", "need a dex file!! ", "red") - parser.print_help() - sys.exit(1) - - patters = [] - # if pkg is None: - # echo("warning", "pkg is None and will retrive all methods in dex file.", 'yellow') - # if pattern is None or pattern == '': - # pattern = "://" - - patters.append(pattern) - - def scan_dex(dex): - echo("dex", "analyzer "+dex) - with open(dex, 'rb') as f: - - vm = DexFileVM(pkgname=pkg, buff=f.read()) - if not vm.ok(): - echo("error", "{} is not a dex format file.".format( - input_file), 'red') - return - if pattern is not None: - # if pattern is not None will show string - for i, s in enumerate(vm.all_strings(patters)): - echo("%d" % (i), "%s" % (s)) - if method_name_arg is not None: - echo("warning", " methodName is empty ,show all methods", 'yellow') - - vm.analysis_dex(clazz_name, method_name_arg, dump) - - if os.path.isdir(input_file): - for root, _, fs in os.walk(input_file): - for f in fs: - if f.endswith('.dex'): - dex = os.path.join(root, f) - scan_dex(dex) - elif os.path.isfile(input_file): - scan_dex(input_file) - - -def yara_scan(args): - rule = args.rule - f = args.file - if rule is None or f is None: - echo("error", "yara rule or apk file must be include", 'red') - return - if not os.path.isfile(rule) and not os.path.isdir(rule): - echo("error", "yara rule file not exists", 'red') - return - if not os.path.isfile(f) and not os.path.isdir(f): - echo("error", "apk file or apk directory need", 'red') - return - echo("yara_scan", "------YARA SCAN -------", 'yellow') - if rule.startswith("."): - rule = os.getcwd()+rule[1:] - YaraMatcher(rule, f).yara_scan() - - -def show_info(args): - - print(white+'-'*40, end='\n') - print(light_blue) - print("\t%s" % ("author:")+"\t\t%s" % ("loopher"), end='\n') - print("\t%s" % ("version:")+"\t%s" % ("2.0"), end='\n') - print("\t%s" % ("updatedate:\t%s" % ("2021-04-30"))) - print(reset) - - -if __name__ == '__main__': - - parser = argparse.ArgumentParser(usage="%(prog)s [options]") - subparsers = parser.add_subparsers(title="options") - - version = subparsers.add_parser("version", help='show version') - version.set_defaults(func=show_info) - # query vt - query_parser = subparsers.add_parser("query", help="query from VT") - query_parser.set_defaults(func=query_report) - query_parser.add_argument( - "-s", "--resource", type=str, default=None, help="file path or sh256 ") - query_parser.add_argument( - "-n", "--name", type=str, default=None, help="virus query services vendor: VT threatbook") - - analysis_dex = subparsers.add_parser( - "search_dex", help="search dex string or method all instructions from dex") - analysis_dex.set_defaults(func=dex_info) - analysis_dex.add_argument("-d", "--dex", type=str, - default=None, help="A dex file or folder contains .dex file") - analysis_dex.add_argument( - "-s", "--string", type=str, default=None, help="A string eg: \"hello\" or reg pattern,eg: \"^(aaa).+ or ^(aaa).+,^(bbb).?\"") - analysis_dex.add_argument("-m", "--method", type=str, - default=None, help="A method Name in the dex file.") - analysis_dex.add_argument("-pkg", "--pkgname", type=str, - default=None, help="class pkgname") - analysis_dex.add_argument( - "-c", "--clazz", type=str, default=None, help="specific class name ,default is None ") - analysis_dex.add_argument( - "-p", "--print_ins", action="store_true", help="dump method instruction ") - - # - # analysis AndroidManifest.xml - manifest_parser = subparsers.add_parser( - "manifest", help=" Parsing Binary AndroidManifest.xml") - manifest_parser.set_defaults(func=extract_android_manifest_info) - manifest_parser.add_argument("-m", "--manifest", type=str, default=None, - help="A binary AndroidManifest.xml or apk contain's AndroidManifest.xml") - manifest_parser.add_argument( - "-a", "--activities", action="store_true", help="show all activities ") - manifest_parser.add_argument( - "-r", "--receivers", action="store_true", help="show all receivers ") - manifest_parser.add_argument( - "-s", "--services", action="store_true", help="show all services ") - manifest_parser.add_argument( - "-p", "--providers", action="store_true", help="show all providers ") - manifest_parser.add_argument( - "-b", "--both", action="store_true", help="show all componets ") - manifest_parser.add_argument( - "-e", "--entry", action="store_true", help="show entry point , MainActivity pkgname Application ") - - manifest_parser.add_argument( - "-et", "--exported", action="store_true", help="show entry point , MainActivity pkgname Application ") - manifest_parser.add_argument( - "-pm", "--permission", action="store_true", help="show permissions ") - # apk base info - apk_base = subparsers.add_parser("apkinfo", help="Apk base info") - apk_base.set_defaults(func=apk_info) - apk_base.add_argument("-a", "--apk", type=str, - default=None, help="path to apk") - # - apk_base.add_argument( - "-i", "--info", action="store_true", help="read apk base info ") - # 获取所有的apk内的文件名 - apk_base.add_argument( - "-z", "--zipinfo", action="store_true", help="read filename from apk,like zipinfo apk") - - # dex_num显示classes.dex数量 - apk_base.add_argument( - "-dexnum", "--dexnum", action="store_true", help="show all classes.dex ") - - # suffix - apk_base.add_argument( - "-suffix", "--suffix", type=str, - default=None, help="file.suffix,like .apk,.APK,.bin") - - # search infor from apk - search_from_apk = subparsers.add_parser( - "search_apk", help="search string or method instructions from apk") - search_from_apk.set_defaults(func=extract_apk_info) - search_from_apk.add_argument( - "-a", "--apk", type=str, default=None, help="path to apk") - # 使用,分隔开 - search_from_apk.add_argument( - "-s", "--string", type=str, default=None, help="A string eg: \"hello\" or reg pattern,eg: \"^(aaa).+ or ^(aaa).+,^(bbb).?\"") - # 使用method name 输出指令 - search_from_apk.add_argument( - "-m", "--method", type=str, default=None, help="specific method name default is None") - search_from_apk.add_argument( - "-c", "--clazz", type=str, default=None, help="specific class name ,default is None ") - search_from_apk.add_argument( - "-p", "--print_ins", action="store_true", help="dump method instruction ") - search_from_apk.add_argument( - "-save", "--save", action="store_true", help="save strings in to file ") - - - # 使用yara扫描 - yara_parser = subparsers.add_parser( - "yara_scan", help="Using yara rule to scan") - yara_parser.set_defaults(func=yara_scan) - yara_parser.add_argument("-r", '--rule', default=None, - type=str, help="Yara rule file or directory") - yara_parser.add_argument("-f", '--file', default=None, - type=str, help="apk file or directory contains .apk/.APK or .dex") - - # - args = parser.parse_args() - if len(vars(args)) == 0: - parser.print_help() - else: - args.func(args) diff --git a/build/lib/androyara/core/__init__.py b/build/lib/androyara/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/lib/androyara/core/analysis_apk.py b/build/lib/androyara/core/analysis_apk.py deleted file mode 100644 index 116e725..0000000 --- a/build/lib/androyara/core/analysis_apk.py +++ /dev/null @@ -1,118 +0,0 @@ -# coding:utf8 -''' -@File : analysis_apk.py -@Author : Loopher -@Version : 1.0 -@License : (C)Copyright 2020-2021,Loopher -@Desc : analyzer a apk file -''' - -import os -import json -from androyara.vsbox.vt import VT -from androyara.utils.utility import echo -from androyara.core.apk_parser import ApkPaser, FileNotFound - - -class AnalyzerApk(object): - - def __init__(self, apk, rule=None, pattern=None): - - self.ok = True - self.rule = rule # rule file - self.pattern = pattern # string pattern - self.filename = apk - self.rules = None - try: - # echo("info", " analysis : {}".format(apk)) - self.apk_parser = ApkPaser(apk) - except FileNotFound: - self.ok = False - return - except Exception as e: - # echo("error", " parser \"{}\" error ,exception: {}".format(apk, e), 'red') - self.ok = False - return - - self.vt = VT(self.apk_parser._app_sha256) - - def __analyzer_string(self): - """ - need pattern or rule file - """ - for rule in self.rules: - ss = self.apk_parser.all_strings(rule['patters']) - if len(ss) > 0: - echo("info", "rule: {} {} ".format( - rule['name'], self.filename)) - return True - return False - - def __analyzer_method(self): - """ - check method ,need rule file - """ - # echo("info", "to be continue...") - pass - - def __read_rule(self): - - with open(self.rule, 'r') as fp: - config = json.load(fp) - result = [] - for rule in config['rules']: - - item = { - "name": rule['name'], - "patters": [], - "shell_s": [], - "file_type": "" - } - for k, v in rule.items(): - if k.startswith("string"): - if v is None or v == '': - continue - item['patters'].append(v) - elif k.startswith("shell_code"): - item['shell_s'].append(v) - elif k == 'file_type': - item['file_type'] = v - result.append(item) - return result - - def _online_sandbox(self): - result = self.vt.analysis() - if result is None: - return False - echo("info", "VT query reult:") - echo("positives", result['positives'], "magenta") - echo("virusName", result['virusName'], "red") - echo("link", result['link'], "green") - echo("scanDate", result['scanDate'], "yellow") - echo("path", self.filename, "white") - return True - - def analyzer(self): - if not self.ok: - return False - elif not os.path.isfile(self.rule): - echo("error", "need rule file!!", 'red') - return False - elif not self.apk_parser.ok: - echo("error", " {} is not a apk file !!!", 'red') - return False - self.rules = self.__read_rule() - - return self.__analyzer_string()\ - or self.__analyzer_method()\ - or self._online_sandbox() - - # if self.__analyzer_string(): - # return True - - # elif self.__analyzer_method(): - # return True - # elif self._online_sandbox(): - # return True - # else: - # return False diff --git a/build/lib/androyara/core/apk_packer.py b/build/lib/androyara/core/apk_packer.py deleted file mode 100644 index 31b4766..0000000 --- a/build/lib/androyara/core/apk_packer.py +++ /dev/null @@ -1,64 +0,0 @@ -# -*- encoding: utf-8 -*- -''' -@File : apk_packer.py -@Time : 2021/06/17 16:43:41 -@Author : Ghidra -@Version : 1.0 -@Contact : unknowatdotcom -@License : (C)Copyright 2020-2021, Ghidra -@Desc : App packer's vendor info -''' - -# here put the import lib - - -PACKED_STATUS = {"prime": "基础版加固", "pro": "企业版加固"} - -AJM_PACKED = { - "features": ["assets/ijm_lib/", " assets/ijiami"], - "shell_application": ["com.shell.SuperApplication"], - "status": PACKED_STATUS, - "name": "ijm/爱加密加固" -} - -VENDER_NAME = {"ijm": "爱加密", "digit": "360", "pengui": "腾讯", "others": "其他"} - -JIAGU_360 = { - "features": ["assets/libjiagu_x86.so"], - "shell_application": ["com.stub.StubApp"], - "status": PACKED_STATUS, - "name": "360加固" -} - -SECNEO = { - "features": ["libDexHelper.so", "libDexHelper-x86.so"], - "shell_application": ["com.secneo.apkwrapper.AW"], - "status": PACKED_STATUS, - "name": "secneo/梆梆加固" -} - -packers = [AJM_PACKED, JIAGU_360, SECNEO] - - -class ApkPackInfo(object): - - def __init__(self, namelist): - self.zipinfo = namelist - - def get_pack_info(self, application): - """ - Get apk packer info - """ - # 校验application - for p in packers: - for ap in p['shell_application']: - if ap == application: - return p['name'] - # 校验lib目录下的或者assets目录下的 - for n in self.zipinfo: - for p in packers: - - for _p in p['features']: - if _p in n: - return p['name'] - return "N/A" diff --git a/build/lib/androyara/core/apk_parser.py b/build/lib/androyara/core/apk_parser.py deleted file mode 100644 index 5457b82..0000000 --- a/build/lib/androyara/core/apk_parser.py +++ /dev/null @@ -1,859 +0,0 @@ -# -*- encoding: utf-8 -*- -''' -@File : apk_parser.py -@Author : Loopher -@Version : 1.0 -@License : (C)Copyright 2020-2021, Loopher -@Desc : APk Information - -Here's part of code are from androgurad. -''' - -# 在这里将会读取处APK内的信息,包括 classes.dex 签名信息,签名版本v1 v2 v3 AndroidManifest.xml 包括app的指纹信息 -# Here put the import lib - -import io -import json -import codecs -import zipfile -import hashlib -import re -import logging -from struct import unpack -from zlib import crc32 -import binascii -import asn1crypto -from asn1crypto import cms, x509, keys -from androyara.parser.base_parser import BaserParser -from androyara.dex.dex_vm import DexFileVM -from androyara.core.axml_parser import AndroidManifestXmlParser, ARSCParser, ARSCResTableConfig -from androyara.core.apk_packer import ApkPackInfo - -log = logging.getLogger("androyara.apk") - - -class ApkReadException(BaseException): - - pass - - -class FileNotFound(BaseException): - pass - - -def get_certificate_name_string(name, short=False, delimiter=', '): - """ - Format the Name type of a X509 Certificate in a human readable form. - - :param name: Name object to return the DN from - :param short: Use short form (default: False) - :param delimiter: Delimiter string or character between two parts (default: ', ') - - :type name: dict or :class:`asn1crypto.x509.Name` - :type short: boolean - :type delimiter: str - - :rtype: str - """ - if isinstance(name, asn1crypto.x509.Name): - name = name.native - - # For the shortform, we have a lookup table - # See RFC4514 for more details - _ = { - 'business_category': ("businessCategory", "businessCategory"), - 'serial_number': ("serialNumber", "serialNumber"), - 'country_name': ("C", "countryName"), - 'postal_code': ("postalCode", "postalCode"), - 'state_or_province_name': ("ST", "stateOrProvinceName"), - 'locality_name': ("L", "localityName"), - 'street_address': ("street", "streetAddress"), - 'organization_name': ("O", "organizationName"), - 'organizational_unit_name': ("OU", "organizationalUnitName"), - 'title': ("title", "title"), - 'common_name': ("CN", "commonName"), - 'initials': ("initials", "initials"), - 'generation_qualifier': ("generationQualifier", "generationQualifier"), - 'surname': ("SN", "surname"), - 'given_name': ("GN", "givenName"), - 'name': ("name", "name"), - 'pseudonym': ("pseudonym", "pseudonym"), - 'dn_qualifier': ("dnQualifier", "dnQualifier"), - 'telephone_number': ("telephoneNumber", "telephoneNumber"), - 'email_address': ("E", "emailAddress"), - 'domain_component': ("DC", "domainComponent"), - 'name_distinguisher': ("nameDistinguisher", "nameDistinguisher"), - 'organization_identifier': ("organizationIdentifier", "organizationIdentifier"), - } - return delimiter.join(["{}={}".format(_.get(attr, (attr, attr))[0 if short else 1], name[attr]) for attr in name]) - -# ------------------------- - - -def _dump_additional_attributes(additional_attributes): - """ try to parse additional attributes, but ends up to hexdump if the scheme is unknown """ - - attributes_raw = io.BytesIO(additional_attributes) - attributes_hex = binascii.hexlify(additional_attributes) - - if not len(additional_attributes): - return attributes_hex - - len_attribute, = unpack('= 0x7fffffff: - max_sdk_str = "0x%x" % self.maxSDK - - return "\n".join([ - 'signer minSDK : {:d}'.format(self.minSDK), - 'signer maxSDK : {:s}'.format(max_sdk_str), - base_str - ]) - - -class APKV2Signer: - """ - This class holds all data associated with an APK V2 SigningBlock signer. - source : https://source.android.com/security/apksigning/v2.html - """ - - def __init__(self): - self._bytes = None - self.signed_data = None - self.signatures = None - self.public_key = None - - def __str__(self): - return "\n".join([ - '{:s}'.format(str(self.signed_data)), - 'signatures : {}'.format( - _dump_digests_or_signatures(self.signatures)), - 'public key : {}'.format(binascii.hexlify(self.public_key)), - ]) - - -class APKV3Signer(APKV2Signer): - """ - This class holds all data associated with an APK V3 SigningBlock signer. - source : https://source.android.com/security/apksigning/v3.html - """ - - def __init__(self): - super().__init__() - self.minSDK = None - self.maxSDK = None - - def __str__(self): - - base_str = super().__str__() - - # maxSDK is set to a negative value if there is no upper bound on the sdk targeted - max_sdk_str = "%d" % self.maxSDK - if self.maxSDK >= 0x7fffffff: - max_sdk_str = "0x%x" % self.maxSDK - - return "\n".join([ - 'signer minSDK : {:d}'.format(self.minSDK), - 'signer maxSDK : {:s}'.format(max_sdk_str), - base_str - ]) - - -# ------------------- - -class ApkPaser(BaserParser): - - parser_info = { - "name": "ApkPaser", - "desc": "Parse apk file " - - } - # Constants in ZipFile - _PK_END_OF_CENTRAL_DIR = b"\x50\x4b\x05\x06" - _PK_CENTRAL_DIR = b"\x50\x4b\x01\x02" - - # Constants in the APK Signature Block - _APK_SIG_MAGIC = b"APK Sig Block 42" - _APK_SIG_KEY_V2_SIGNATURE = 0x7109871a - _APK_SIG_KEY_V3_SIGNATURE = 0xf05368c0 - _APK_SIG_ATTR_V2_STRIPPING_PROTECTION = 0xbeeff00d - - _APK_SIG_ALGO_IDS = { - 0x0101: "RSASSA-PSS with SHA2-256 digest, SHA2-256 MGF1, 32 bytes of salt, trailer: 0xbc", - 0x0102: "RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc", - # This is for build systems which require deterministic signatures. - 0x0103: "RSASSA-PKCS1-v1_5 with SHA2-256 digest.", - # This is for build systems which require deterministic signatures. - 0x0104: "RSASSA-PKCS1-v1_5 with SHA2-512 digest.", - 0x0201: "ECDSA with SHA2-256 digest", - 0x0202: "ECDSA with SHA2-512 digest", - 0x0301: "DSA with SHA2-256 digest", - } - - def __init__(self, apk, buff=None, info=False): - - super(ApkPaser, self).__init__(apk, buff) - - # 在这里统一读取出apk信息 - default_meta_info = { - "classes": "classes", - "AndroidManifest_xml": "AndroidManifest.xml", - "arsc": "resources.arsc" - } - - self.raw = self.buff - - self.zip_buff = zipfile.ZipFile(io.BytesIO(self.buff), mode='r') - self._v2_blocks = {} - - self._is_signed_v2 = False - self._is_signed_v3 = False - - self._v3_siging_data = None - self._v2_signing_data = None - # read AndroidManifestxml info - - # arsc_buff = self.get_buff(default_meta_info['arsc']) - self.asrc = None # ARSCParser(arsc_buff) - - axml_buff = self.get_buff(default_meta_info['AndroidManifest_xml']) - self.axml = AndroidManifestXmlParser(None, buff=axml_buff) - - self.dex_vm = None - if info is False: - # Apkinfo only will ignore dexfile - # - self.dex_vm = DexFileVM(self.axml.package, self.get_classe_dex()) - # Read APK's fingerprint - self._app_md5 = hashlib.md5(self.buff).hexdigest() - self._app_sha256 = hashlib.sha256(self.buff).hexdigest() - self._app_sha1 = hashlib.sha1(self.buff).hexdigest() - self._app_crc32 = crc32(self.buff) - - self.filesize = "{}MB".format( - float("%.2f" % (len(self.buff) / 1024/1024))) - - self.package = self.axml.package - - def show_manifest(self, acs, rs, ss, ps, entry, both, exported, pm): - self.axml.show_manifest(acs, rs, ss, ps, entry, both, exported, pm) - - def mainifest_info(self): - return self.axml - - def ok(self): - - return self.dex_vm.ok() - - def all_strings(self, pattern, dex_vm=None): - if dex_vm is not None: - return dex_vm.all_strings(pattern) - return self.dex_vm.all_strings(pattern) - - def all_class_defs(self, dex_vm=None): - if dex_vm is not None: - return dex_vm.all_class_defs() - return self.dex_vm.all_class_defs() - - def print_ins(self, offset, dex_vm=None): - if dex_vm is not None: - dex_vm.print_ins(offset) - self.dex_vm.print_ins(offset) - - def analysis_dex(self, clazz_name, method_name, show_ins, dex_vm=None): - if dex_vm is not None: - dex_vm.analysis_dex(clazz_name, method_name, show_ins) - return - self.dex_vm.analysis_dex(clazz_name, method_name, show_ins) - - def apk_base_info(self): - - application = self.axml.get_application() - packinfo = ApkPackInfo(self.get_file_names()) - apk_info = { - "app_name": self.get_app_name(), - "packer_name": packinfo.get_pack_info(application), - "signed": { - "v1": self.is_signed_v1(), - "v2": self.is_signed_v2(), - "v3": self.is_signed_v3() - }, - "certifacte": self.certification_info(), - "package": self.package, - "versionCode": self.axml.android_version['Code'], - "versionName": self.axml.android_version['Name'], - "Application": application, - "sha256": self._app_sha256, - "md5": self._app_md5, - "sha1": self._app_sha1, - "crc32": hex(self._app_crc32), - "file": self.filename, - "filetype": self.get_type(), - "filesize": self.filesize, - "mainActivity": self.axml.get_main_activity() - } - return apk_info - - def all_dex_vms(self,): - """ - all classes.dex parse objec - return dexname,dex_vm - """ - for dex, buff in self.get_all_dexs(): - yield dex, DexFileVM(self.axml.package, buff) - - def is_signed_v2(self): - - if self._is_signed_v2 is False: - self.__parse_v2_v3_signature() - return self._is_signed_v2 - - def get_all_dexs(self, name=False): - - dexre = re.compile(r"classes(\d*).dex") - # if name: - # for name in - for dex in filter(lambda x: dexre.match(x), self.get_file_names()): - if name: - yield dex - else: - yield dex, self.get_buff(dex) - - def is_signed_v3(self): - - if self._is_signed_v3 is False: - self.__parse_v2_v3_signature() - return self._is_signed_v3 - - def __parse_v2_v3_signature(self): - # Read apk signature v2 v3 info - - fp = io.BytesIO(self.raw) - - fp.seek(-1, io.SEEK_END) - fp.seek(-20, io.SEEK_CUR) - - offset_central = 0 - - while fp.tell() > 0: - fp.seek(-1, io.SEEK_CUR) - r, = unpack('<4s', fp.read(4)) - if r == self._PK_END_OF_CENTRAL_DIR: - this_disk, disk_central, this_entries, total_entries, \ - size_central, offset_central = unpack( - ' Read apk signature info error ,disk_central !=0 ,value: {}".format(disk_central)) - break - fp.seek(-1, io.SEEK_CUR) - - if not offset_central: - return - - fp.seek(offset_central) - r, = unpack("<4s", fp.read(4)) - fp.seek(-4, io.SEEK_CUR) - - if r != self._PK_CENTRAL_DIR: - raise Exception( - "--> Not Found apk's central dir at {}".format(offset_central)) - - end_off = fp.tell() - fp.seek(-24, io.SEEK_CUR) - size_of_block, magic = unpack(" size_of_block_start ",size_of_block_start ," size_of_block ",size_of_block) - if size_of_block_start != size_of_block: - raise Exception( - "Read apk's signature error ,size_of_block != size_of_block_start") - - # reach signature's block - while fp.tell() < end_off - 24: - size, key = unpack(" ",self._v2_blocks) - if self._APK_SIG_KEY_V2_SIGNATURE in self._v2_blocks: - self._is_signed_v2 = True - if self._APK_SIG_KEY_V3_SIGNATURE in self._v2_blocks: - self._is_signed_v3 = True - - def parse_v3_signing_block(self): - - # print("-> read v3 signature ") - self._v3_siging_data = [] - if not self.is_signed_v3(): - return - - block_bytes = self._v2_blocks[self._APK_SIG_KEY_V3_SIGNATURE] - - block = io.BytesIO(block_bytes) - - # view = block.getvalue() - - size_sequence = self.read_uint32_le(block) - - # 等再补充读取签名信息 方法 - if size_sequence + 4 != len(block_bytes): - raise Exception("can't read v3 signature block") - - def read_uint32_le(self, buff): - - return unpack("> ",type(app_name)) - # if isinstance(app_name,str): - # app_name = codecs.decode() - except: - return "" - return app_name - - def get_signatures(self): - # Read all signature file data -> bytes buffer - - signatures = self.get_v1_signature_names(v1=False) - signature_data = [] - for s in signatures: - signature_data.append(self.get_buff(s)) - return signature_data - - def is_signed_v1(self): - # If we read ,it will not be empty list - return len(self.get_v1_signature_names()) > 0 - - def get_type(self): - # Return file type - return "apk" - - def get_buff(self, name): - """ - Read zipinfo from apk's internal file buff with name - """ - try: - return self.zip_buff.read(name) - except KeyError: - raise FileNotFound(name) - - def get_classe_dex(self): - - return self.get_buff("classes.dex") - - def get_file_names(self): - """ - Read zipinfo File - """ - - return self.zip_buff.namelist() - - def get_signature_names(self): - return self.get_v1_signature_names(v1=False) - - def certification_info(self): - """ - show Certifications - """ - info = "**" * 10 + "Apk Certification Info " + "**" * 10 + "\n" - info += "filename:{}".format(self.filename) + \ - " ,package: {}" + self.package + "\n" - info += " Signed V1:{}".format(self.is_signed_v1()) + "\n" - info += " Signed V2:{}".format(self.is_signed_v2()) + "\n" - info += " Signed V3:{}".format(self.is_signed_v3()) + "\n" - - certs = set(self.get_certificates_der_v3( - ) + self.get_certificates_der_v2() + [self.get_certificate_der(x) for x in - self.get_signature_names()]) - pass - - certification = {} - - # pkeys = set(self.get_public_keys_der_v3() + - # self.get_public_keys_der_v2()) - # # if len(certs) > 0: - # # print("Found {} unique certificates".format(len(certs))) - # pass - - info += "--" * 10 + "Ceritification Info:" + "--" * 10+"\n" - for cert in certs: - x509_cert = x509.Certificate.load(cert) - issuer = get_certificate_name_string(x509_cert.issuer, short=True) - certification['Issuer'] = issuer - info += "Issuer: " + issuer + "\n" - subjuect = get_certificate_name_string( - x509_cert.subject, short=True) - info += "Subject: " + subjuect + "\n" - certification['Subject'] = subjuect - - serial_number = hex(x509_cert.serial_number) - info += "Serial Number: " + serial_number + "\n" - - certification['Serial Number'] = serial_number - - hash_algorithm = x509_cert.hash_algo - info += "Hash Algorithm: " + hash_algorithm + "\n" - certification['Hash Algorithm'] = hash_algorithm - - signature_algorithm = x509_cert.signature_algo - info += "Signatue Algorithm: " + signature_algorithm + "\n" - certification['Signature Algorithm'] = signature_algorithm - - valide_not_before = x509_cert['tbs_certificate']['validity']['not_before'].native - certification['Validate not before'] = self.date_time( - valide_not_before) - - info += "Valide not before: " + \ - self.date_time(valide_not_before) + "\n" - - valid_not_after = x509_cert['tbs_certificate']['validity']['not_after'].native - certification['Validate not after'] = self.date_time( - valid_not_after) - - info += "Valide not after: " + \ - self.date_time(valid_not_after) + "\n" - # info += "**" * 10 + "Public keys info " + '**' * 10 + "\n" - # for public_key in pkeys: - # x509_public_key = keys.PublicKeyInfo(public_key) - # public_algorithm = x509_public_key.algorithm - # info += "Public Algorithm: " + public_algorithm + "\n" - # bit_size = x509_public_key.bit_size - # info += "Bit Size: " + bit_size + "\n" - # finger_print = binascii.hexlify(x509_public_key.fingerprint) - # info += "Finger print: " + finger_print + "\n" - # try: - # hash_algorithm = x509_public_key.hash_algo - # except ValueError as e: - # print("--> Not found pubic key hash algorithm ") - # hash_algorithm = "unknow" - # info += "Hash algorithm: " + hash_algorithm + "\n" - - return certification - - def get_certificate_der(self, filename): - """ - Return the DER coded X.509 certificate from the signature file. - - :param filename: Signature filename in APK - :returns: DER coded X.509 certificate as binary - """ - pkcs7message = self.get_buff(filename) - - pkcs7obj = cms.ContentInfo.load(pkcs7message) - cert = pkcs7obj['content']['certificates'][0].chosen.dump() - return cert - - def get_public_keys_der_v2(self): - """ - Return a list of DER coded X.509 public keys from the v3 signature block - """ - - if self._v2_signing_data == None: - self.parse_v2_signing_block() - - public_keys = [] - - for signer in self._v2_signing_data: - public_keys.append(signer.public_key) - - return public_keys - - def get_certificates_der_v3(self): - """ - Return a list of DER coded X.509 certificates from the v3 signature block - """ - - if self._v3_siging_data is None: - self.parse_v3_signing_block() - - certs = [] - for signed_data in [signer.signed_data for signer in self._v3_siging_data]: - for cert in signed_data.certificates: - certs.append(cert) - - return certs - - def get_public_keys_der_v3(self): - """ - Return a list of DER coded X.509 public keys from the v3 signature block - """ - - if self._v3_siging_data is None: - self.parse_v3_signing_block() - - public_keys = [] - - for signer in self._v3_siging_data: - public_keys.append(signer.public_key) - - return public_keys - - def get_certificates_der_v2(self): - """ - Return a list of DER coded X.509 certificates from the v3 signature block - """ - - if self._v2_signing_data is None: - self.parse_v2_signing_block() - - certs = [] - for signed_data in [signer.signed_data for signer in self._v2_signing_data]: - for cert in signed_data.certificates: - certs.append(cert) - - return certs - - def parse_v2_signing_block(self): - """ - Parse the V2 signing block and extract all features - """ - - self._v2_signing_data = [] - - # calling is_signed_v2 should also load the signature - if not self.is_signed_v2(): - return - - block_bytes = self._v2_blocks[self._APK_SIG_KEY_V2_SIGNATURE] - block = io.BytesIO(block_bytes) - view = block.getvalue() - - # V2 signature Block data format: - # - # * signer: - # * signed data: - # * digests: - # * signature algorithm ID (uint32) - # * digest (length-prefixed) - # * certificates - # * additional attributes - # * signatures - # * publickey - - size_sequence = self.read_uint32_le(block) - # print("--. len(block) : {} size_sequence + 4 :{} ".format(len(block_bytes), size_sequence + 4)) - if size_sequence + 4 != len(block_bytes): - raise ApkReadException( - "size of sequence and blocksize does not match") - - while block.tell() < len(block_bytes): - off_signer = block.tell() - size_signer = self.read_uint32_le(block) - - # read whole signed data, since we might to parse - # content within the signed data, and mess up offset - len_signed_data = self.read_uint32_le(block) - signed_data_bytes = block.read(len_signed_data) - signed_data = io.BytesIO(signed_data_bytes) - - # Digests - len_digests = self.read_uint32_le(signed_data) - raw_digests = signed_data.read(len_digests) - digests = self.parse_signatures_or_digests(raw_digests) - - # Certs - certs = [] - len_certs = self.read_uint32_le(signed_data) - start_certs = signed_data.tell() - while signed_data.tell() < start_certs + len_certs: - len_cert = self.read_uint32_le(signed_data) - cert = signed_data.read(len_cert) - certs.append(cert) - - # Additional attributes - len_attr = self.read_uint32_le(signed_data) - attributes = signed_data.read(len_attr) - - signed_data_object = APKV2SignedData() - signed_data_object._bytes = signed_data_bytes - signed_data_object.digests = digests - signed_data_object.certificates = certs - signed_data_object.additional_attributes = attributes - - # Signatures - len_sigs = self.read_uint32_le(block) - raw_sigs = block.read(len_sigs) - sigs = self.parse_signatures_or_digests(raw_sigs) - - # PublicKey - len_publickey = self.read_uint32_le(block) - publickey = block.read(len_publickey) - - signer = APKV2Signer() - signer._bytes = view[off_signer:off_signer + size_signer] - signer.signed_data = signed_data_object - signer.signatures = sigs - signer.public_key = publickey - - self._v2_signing_data.append(signer) - - def parse_signatures_or_digests(self, digest_bytes): - """ Parse digests """ - - if not len(digest_bytes): - return [] - - digests = [] - block = io.BytesIO(digest_bytes) - - data_len = self.read_uint32_le(block) - while block.tell() < data_len: - algorithm_id = self.read_uint32_le(block) - digest_len = self.read_uint32_le(block) - digest = block.read(digest_len) - - digests.append((algorithm_id, digest)) - - return digests - - def date_time(self, dt): - - return dt.strftime("%Y/%m/%d %H:%M:%S") diff --git a/build/lib/androyara/core/axml_parser.py b/build/lib/androyara/core/axml_parser.py deleted file mode 100644 index cf03913..0000000 --- a/build/lib/androyara/core/axml_parser.py +++ /dev/null @@ -1,367 +0,0 @@ -# -*- encoding: utf-8 -*- -''' -@File : axml.py -@Author : Loopher -@Version : 1.0 -@License : (C)Copyright 2020-2021, Loopher -@Desc : AndroidManifest.xml parser -''' - -# Here put the import lib -import enum -from androyara.utils.utility import echo -from androyara.parser.base_parser import BaserParser -from androyara.typeinfo.types import* - - -NS_ANDROID_URI = 'http://schemas.android.com/apk/res/android' -NS_ANDROID = '{{{}}}'.format(NS_ANDROID_URI) # Namespace as used by etree - - -class AxmlExcetion(BaseException): - pass - - -class ElementNotFound(BaseException): - pass - - -class AndroidManifestXmlParser(BaserParser): - parser_info = { - "name": "AndroidManifestXmlParser", - "desc": "AndroidManifest.xml parser" - - } - - def __init__(self, manifest, buff=None): - super(AndroidManifestXmlParser, self).__init__(manifest, buff) - - xml_file = AXMLPrinter(self.buff) # 在这里解析出所有的xml信息 - self.axml = {} - if not xml_file.is_valid(): - raise AxmlExcetion("while parsing AndroidManifest.xml error ") - self.axml['AndroidManifest.xml'] = xml_file.get_xml_obj() - # self.show_xml() - - if self.axml['AndroidManifest.xml'].tag != 'manifest': - raise AxmlExcetion( - "parse AndroidManifest.xml error ,need AndroidManifest.xml file ") - self.android_version = {} - self.package = "" - self.permissions = [] - - self.user_permission = [] # app内申请的权限 - - self.package = self.get_attribute_value("manifest", "package") - - self.android_version['Code'] = self.get_attribute_value( - "manifest", "versionCode") - self.android_version['Name'] = self.get_attribute_value( - "manifest", "versionName") - - # Get ALl Permission - permisions = list(self.get_all_attribute_value( - "uses-permission", "name")) - self.permissions = list(set(permisions)) - - def show_manifest(self, ac, rs, ss, ps, entry, both, exported, pm): - - manifest = { - "activities": self.get_all_activities, - "receviers": self.get_receivers, - "providers": self.get_providers, - "services": self.get_all_services, - "both": self.__str__, - - } - - def show(key): - echo('%s' % (key), "--"*15, 'yellow') - cnt = 1 - for a in manifest[key](): - echo("%d" % (cnt), a) - cnt += 1 - - if ac: - show("activities") - elif rs: - show("receviers") - elif ss: - show("services") - elif ps: - show("providers") - elif entry: - # show("entry") - echo("entryinfo", "**"*20) - self.entry_info() - elif exported: - self.get_export_components() - elif pm: - for p in self.permissions: - echo("permission", p) - elif both: - echo("all", "\n"+manifest['both']()) - - # elif rs: - - def get_all_export_components(self): - - export_keys = ["activity", "service", "provider", "receiver"] - result = {} - for k in export_keys: - - result[k] = list(self.get_all_attribute_value( - k, "name", {"exported": "true"})) - return result - - def entry_info(self): - - echo("pkgname", self.get_package_name()) - echo("application", self.get_application()) - echo("MainActivity", self.get_main_activity()) - - def find_tags(self, tag_name, **attribute_filter): - all_tags = [ - self.find_tag_from_xml(i, tag_name, **attribute_filter) - for i in self.axml - ] - return [tag for tag_list in all_tags for tag in tag_list] - - def find_tag_from_xml(self, xml_name, tag_name, **attribute_filter): - - xml = self.axml[xml_name] - if xml is None: - return [] - if xml.tag == tag_name: - if self.is_tag_matched(xml.tag, **attribute_filter): - return [xml] - return [] - tags = xml.findall(".//" + tag_name) - return [ - tag for tag in tags if self.is_tag_matched(tag, **attribute_filter) - - ] - - def is_tag_matched(self, tag, **attribute_filter): - if len(attribute_filter) <= 0: - return True - for attr, value in attribute_filter.items(): - _value = self.get_value_from_tag(tag, attr) - if _value == value: - return True - return False - - def get_value_from_tag(self, tag, attribute): - - value = tag.get(self._ns(attribute)) - if value is None: - # 如果不是通过namespace获取,则这个不是一个标准的AndroidManifest.xml的文件格式 - value = tag.get(attribute) - if value: - log.warning( - "--> Failed to get attribute {} ,due to it is not AndroidManifest.xml data".format(attribute)) - return value - - def _ns(self, name): - return NS_ANDROID+name - - def get_attribute_value(self, tag_name, attribute, format_value=True, **attributes_filter): - for value in self.get_all_attribute_value(tag_name, attribute, format_value, **attributes_filter): - if value is not None: - return value - - def get_all_attribute_value(self, tag_name, attribute, format_value=True, **attribute_filter): - - tags = self.find_tags(tag_name, **attribute_filter) - for tag in tags: - value = tag.get(attribute) or tag.get(self._ns(attribute)) - if value is not None: - if format_value: - yield self._format_value(value) - else: - yield value - - def _format_value(self, value): - if value in self.package: - dot = value.find('.') - if dot == 0: - value = self.package + value - elif dot == -1: - value = self.package+'.'+value - return value - - def get_all_activities(self): - - return list(self.get_all_attribute_value("activity", "name")) - - def get_all_services(self): - - return list(self.get_all_attribute_value("service", "name")) - - def get_permissions(self): - - return self.permissions - - def get_main_activities(self): - - x = set() - y = set() - - for i in self.axml: - if self.axml[i] is None: - continue - manifest = self.axml[i] - activities_aliases = manifest.findall(".//activity") + \ - manifest.findall(".//activity-alias") - for item in activities_aliases: - activity_enable = item.get(self._ns("enabled")) - if activity_enable == 'false': - continue - - for sitem in item.findall(".//action"): - val = sitem.get(self._ns("name")) - if val == 'android.intent.action.MAIN': - x.add(item.get(self._ns("name"))) - - for sitem in item.findall(".//category"): - val = sitem.get(self._ns("name")) - if val == "android.intent.category.LAUNCHER": - y.add(item.get(self._ns("name"))) - - # 有些app的MainActivity的属性表情中含有VIEW DEFAULT BROWSABLE 的,这里再次判断是否包含在了y内 - return y.intersection(x) - - def get_main_activity(self): - """ - For some application: category maybe more than one ,so I will pick on one for MAIN. - """ - - x = set() - y = set() - for i in self.axml: - if self.axml[i] is None: - continue - manifest = self.axml[i] - activities_aliases = manifest.findall(".//activity") + \ - manifest.findall(".//activity-alias") - for item in activities_aliases: - activity_enable = item.get(self._ns("enabled")) - if activity_enable == 'false': - continue - - for sitem in item.findall(".//action"): - val = sitem.get(self._ns("name")) - if val == 'android.intent.action.MAIN': - x.add(item.get(self._ns("name"))) - - category = item.findall(".//category") - - if len(category) >= 2 or len(category) >= 3: - continue - for sitem in item.findall(".//category"): - val = sitem.get(self._ns("name")) - if val == "android.intent.category.LAUNCHER": - y.add(item.get(self._ns("name"))) - - # print("--> x: {} y: {}".format(len(x),len(y))) - activities = y.intersection(x) - # print("--> Got Main tag activities: {}".format(len(activities))) - - def format_activity(d: set): - main_activity = self._format_value(d.pop()) - if main_activity.startswith("."): - # maybe need add package to .MainActivity - main_activity = self.package + main_activity - elif self.package not in main_activity: - main_activity = self.package+"." + main_activity - - return main_activity - - - - - if len(activities) > 0: return format_activity(activities) - elif len(x) >0: return format_activity(x) - elif len(y)>0: return format_activity(y) - return "Not Found MainActivity" - - def get_package_name(self): - return self.package - - def get_application(self): - - try: - return self.get_attribute_value("application", "name") - except ElementNotFound: - # for some application Not - return "" - - def get_providers(self): - - return list(self.get_all_attribute_value("provider", "name")) - - def get_receivers(self): - - return list(self.get_all_attribute_value("receiver", "name")) - - def get_thrid_sdk_metas(self): - """ - Return Thrid part sdk info - """ - - pass - - def get_export_components(self): - """ - Return All export components info - """ - # return self. - exported_all = ["activity", 'service', 'provider', 'receiver'] - # activities = list(self.get_all_attribute_value( - # "activity", "name", {"exported": "true"})) - # exported_all.append(activities) - - echo("exportedComponents", "**"*20, "yellow") - for k in exported_all: - exported = list(self.get_all_attribute_value( - k, "name", {"exported": "true"})) - print("--"*15+" "+k+" "+"--"*15) - for i, item in enumerate(exported): - echo("%d" % (i), item) - # for i, item in enumerate(cm): - # echo("%d" % (i), item) - - def get_app_name(self): - """ - Androguard read this value from AndroidManifest or resource.asrc file ,but I'll not read it too complicate - """ - - app_name = self.get_attribute_value('application', 'label') - if app_name is None: - return "" - return app_name - - def __str__(self): - - info = "--"*10+self.get_app_name()+" pkgname:"+self.package+"--"*10+" \n" - - info += "--> activity info: \n" - activities = self.get_all_activities() - for activity in activities: - info += activity+"\n" - - info += '--> permission info: \n' - permissions = self.permissions - for per in permissions: - info += per+'\n' - - info += '--> receivers info: \n' - receivers = self.get_receivers() - for per in receivers: - info += per+'\n' - - info += '--> providers info: \n' - providers = self.get_providers() - for per in providers: - info += per+'\n' - return info diff --git a/build/lib/androyara/core/dex_parser.py b/build/lib/androyara/core/dex_parser.py deleted file mode 100644 index 8b80493..0000000 --- a/build/lib/androyara/core/dex_parser.py +++ /dev/null @@ -1,27 +0,0 @@ -# coding:utf8 -''' -@File : dex_parser.py -@Author : Loopher -@Version : 1.0 -@License : (C)Copyright 2020-2021,Loopher -@Desc : Dex文件解析 -''' -""" -每一个dex都会经过这里的解析处理,目的是建立一个映射表能快速索引和比较 -""" - - -from androyara.dex.dex_vm import DexFileVM -class DexParser(object): - - parser_info = { - "name": "DexParser", - "desc": "Parsing Dex file into bytecode" - } - - def __init__(self, pkg, buff): - - - self.vm = DexFileVM(pkg,buff) - - self.vm.build_map() diff --git a/build/lib/androyara/core/yara_matcher.py b/build/lib/androyara/core/yara_matcher.py deleted file mode 100644 index 93b5e7f..0000000 --- a/build/lib/androyara/core/yara_matcher.py +++ /dev/null @@ -1,108 +0,0 @@ -# coding:utf8 -''' -@File : yara_matcher.py -@Author : Loopher -@Version : 1.0 -@License : (C)Copyright 2020-2021,Loopher -@Desc : Yara matcher -''' - -import yara -import os -from concurrent.futures import ThreadPoolExecutor, as_completed -from androyara.utils.utility import echo -from androyara.core.apk_parser import ApkPaser - - -class YaraMatcher(object): - - def __init__(self, rule, apk): - self._rule = rule - self._apk = apk - self.__precompile() - self.executors = ThreadPoolExecutor(max_workers=5) - - def __precompile(self): - """ - maybe use a directory as input yara rule file - - """ - self.yara_namespace = {} - if os.path.isfile(self._rule): - name = os.path.basename(self._rule)[:-4] - self.yara_namespace[name] = self._rule - #self.yara_rule = yara.compile(filepath=self._rule) - elif os.path.isdir(self._rule): - for root, _, fs in os.walk(self._rule): - for f in fs: - if f.endswith(".yar"): - self.yara_namespace[f[:-4]] = os.path.join(root, f) - self.yara_rule = yara.compile(filepaths=self.yara_namespace) - - def check_file(self, f): - # .bin is support online sandbox donwload samples - return [f.endswith('.apk'), f.endswith( - '.APK'), f.endswith('.dex'), f.endswith('.bin')] - - def yara_scan(self): - """ - applying yara rule scan input file - """ - - def scan(file): - try: - self.match(file) - except Exception as e: - echo("yara_scan", "error {} ".format(e), 'red') - - # dex or apk file - if os.path.isfile(self._apk) and any(self.check_file(self._apk)): - scan(self._apk) - # folder contains suffix .dex or .apk or .bin files - elif os.path.isdir(self._apk): - workers = [] - for root, _, fs in os.walk(self._apk): - for f in fs: - file = os.path.join(root, f) - if any(self.check_file(f)): - workers.append( - self.executors.submit(fn=scan, file=file)) - for _ in as_completed(workers): - pass - - def match(self, f): - """ - f: a apk or dex file or sample ,it must be dex or apk. - """ - rsult = None - dex = False - if f.endswith(".dex"): - dex = True - - def show_result(rsult, susp): - - for name in self.yara_namespace: - if rsult.get(name, None) is None: - continue - for r in rsult[name]: - tag = r['tags'][0] - - if r['matches']: - echo("rule", " %s/%s %s\t%s" % - (tag, r['rule'], self.yara_namespace[name], susp), 'yellow') - return - if dex: - with open(f, 'rb') as fp: - result = self.yara_rule.match(data=fp.read()) - show_result(result, f) - - # return result - - apk_parser = ApkPaser(f) - if not apk_parser.ok(): - return - for _, buff in apk_parser.get_all_dexs(): - - rsult = self.yara_rule.match(data=buff) - # print(rsult) # {'main': [{'tags': ['Android'], 'meta': {'author': 'loopher'}, 'strings': [{'data': 'http://ksjajsxccb.com/api/index/information', 'offset': 2514313, 'identifier': '$str', 'flags': 19}], 'rule': 'BYL_bank_trojan', 'matches': True}]} - show_result(rsult, f) diff --git a/build/lib/androyara/dex/__init__.py b/build/lib/androyara/dex/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/lib/androyara/dex/dex_code.py b/build/lib/androyara/dex/dex_code.py deleted file mode 100644 index 51f3468..0000000 --- a/build/lib/androyara/dex/dex_code.py +++ /dev/null @@ -1,8 +0,0 @@ -# coding:utf8 -''' -@File : dex_code.py -@Author : Loopher -@Version : 1.0 -@License : (C)Copyright 2020-2021,Loopher -@Desc : Dex CodeItem -''' diff --git a/build/lib/androyara/dex/dex_header.py b/build/lib/androyara/dex/dex_header.py deleted file mode 100644 index 1d35d54..0000000 --- a/build/lib/androyara/dex/dex_header.py +++ /dev/null @@ -1,563 +0,0 @@ -# coding:utf8 -''' -@File : dex_header.py -@Author : Loopher -@Version : 1.0 -@License : (C)Copyright 2020-2021,Loopher -@Desc : DexFileHeader -''' - -import io -import hashlib -import binascii -import sys -import re -from struct import unpack, calcsize - - -from androyara.utils.utility import echo -from androyara.dex.dex_method import * - - -class DexHeaderError(BaseException): - pass - - -class DexClassDefsError(BaseException): - pass - - -class DexHeader(object): - - def __init__(self, buff): - - fp = io.BytesIO(buff) - - self.magic, = unpack("<4s", fp.read(4)) - if not self.is_dex(): - return - self.version, = unpack("<4s", fp.read(4)) - - self.checksum, = unpack(" offset: %s string: %s" % - # (hex(str_offset), string)) - - def read_type_idx_datas(self): - - self.type_item_offset_list = [] - - self.buff.seek(self.type_idx_offset, io.SEEK_SET) - - index = 0 - while index < self.type_idx_size: - type_item_offset, = unpack("I", self.buff.read(4)) - self.type_item_offset_list.append(type_item_offset) - index += 1 - # read type - - # print(":--< type item size :%d total read: %d" % - # (self.type_idx_size, len(self.type_item_offset_list))) - - # - # print("--> type_map ",self.ty) - # for offset in self.type_item_offset_list: - - # for idx, str_offset in enumerate(self.string_item_offset_list): - # if idx == offset: - # print("-----> read type item :%s" % - # (hex(offset)), self.string_table_map[str_offset]) - - def read_dex_method_proto_idx_datas(self): - - self.buff.seek(self.proto_idx_offset, io.SEEK_SET) - index = 0 - fmt = "I" - - self.dex_method_obj_list = [] - self.dex_method_obj_index = {} - while index < self.proto_idx_size: - dex_method_proto = DexMethodProto() - - dex_method_proto.shorty_idx, = unpack( - fmt, self.buff.read(4)) # point to string_idx_list - dex_method_proto.rturn_type_idx, = unpack( - fmt, self.buff.read(4)) # point to type_idx_list - dex_method_proto.parameter_type_offset, = unpack( - fmt, self.buff.read(4)) - - # save - self.dex_method_obj_list.append(dex_method_proto) - self.dex_method_obj_index[index] = dex_method_proto - - index += 1 - # for i in self.type_item_offset_list: - # print("--> type_idx: %d " % (i)) - # # debugging show - # print("--> DexMethodProto") - # for dex_method_proto in self.dex_method_obj_list: - # print(dex_method_proto) - # for i, item in self.dex_method_obj_index.items(): - # print("-->> self.dex_method_obj_index ", - # i, str(self.dex_method_obj_index[i])) - - def read_field_idx_datas(self): - - self.buff.seek(self.field_idx_offset, io.SEEK_SET) - - index = 0 - - self.field_idx_list = [] - self.field_idx_index = {} - - # print("--. field_idx_size: %d" % (self.field_idx_size)) - while index < self.field_idx_size: - - field_idx_obj = DexFieldIdx() - - field_idx_obj.class_idx, = unpack("H", self.buff.read(2)) - field_idx_obj.type_idx, = unpack("H", self.buff.read(2)) - field_idx_obj.name_idx, = unpack("I", self.buff.read(4)) - - self.field_idx_list.append(field_idx_obj) - self.field_idx_index[index] = field_idx_obj - - index += 1 - # print("--> field info ") - # for field in self.field_idx_list: - # print(field) - - def read_method_idx_datas(self): - - self.buff.seek(self.method_idx_offset, io.SEEK_SET) - - self.method_idx_list = [] - self.method_idx_index = {} - - index = 0 - while index < self.method_idx_size: - method_idx_obj = DexMethodIdx() - - method_idx_obj.class_idx, = unpack("H", self.buff.read(2)) - method_idx_obj.proto_idx, = unpack("H", self.buff.read(2)) - method_idx_obj.name_idx, = unpack("I", self.buff.read(4)) - - self.method_idx_list.append(method_idx_obj) - self.method_idx_index[index] = method_idx_obj - - index += 1 - ## - # print("--> DexMethodIdx Info ") - # for method_idx in self.method_idx_list: - # print(method_idx) - - def read_class_defs_datas(self): - - if self.class_defs_idx_size <= 0: - raise DexClassDefsError("class_defs_idx_size <0") - index = 0 - self.class_defs = [] - while index < self.class_defs_idx_size: - class_def_item_off = self.class_defs_idx_offset + index * 32 - self.buff.seek(class_def_item_off, io.SEEK_SET) - - class_idx, access_flags, superclass_idx,\ - interface_off, source_file_idx,\ - annotations_off, clazz_data_off,\ - static_values_off = unpack("IIIIIIII", self.buff.read(32)) - - clzz_name = self.get_class_name_by_idx(class_idx) - index += 1 - # Find target classes info - target_pkg = self.pkg - if not self.is_target_clazz(target_pkg, clzz_name): - continue - # target class - if clazz_data_off <= 0: - # print("error class_data_off error ",file=sys.stderr) - continue - - class_def = { - "class_name": clzz_name, - "class_idx": class_idx, - "code_item": [] - } - - self.buff.seek(clazz_data_off, io.SEEK_SET) - static_field_size = self.read_uleb128(self.buff) - instance_field_size = self.read_uleb128(self.buff) - direct_method_size = self.read_uleb128(self.buff) - virtual_method_size = self.read_uleb128(self.buff) - - class_def['virtual_method_size'] = virtual_method_size - class_def['direct_method_size'] = direct_method_size - - # for now we will rebuild entity of class info - - static_field_cnt = 0 - # print("--" * 10 + "StaticField" + "--" * 10) - # We don't need fields ignored - while static_field_size > 0: - static_field_idx_ = self.read_uleb128(self.buff) - - static_field_cnt = static_field_idx_ + static_field_cnt - access_flags = self.read_uleb128(self.buff) - # 不处理属性变量的情况,可直接忽略掉 - # print(" static field : %s access_flags: %s" % - # (self.field_idx_list[static_field_cnt],hex(access_flags))) # every item is FieldIdx - - static_field_cnt += static_field_idx_ - static_field_size -= 1 - - instance_field_idx_cnt = 0 - # print("--" * 10 + "InstanceField" + "--" * 10) - while instance_field_size > 0: - instance_idx = self.read_uleb128(self.buff) - instance_field_idx_cnt = instance_field_idx_cnt + instance_idx - access_flags = self.read_uleb128(self.buff) - # print("Instance field: %s access_flags: %s"%(self.field_idx_list[instance_field_idx_cnt],hex(access_flags))) - instance_field_size -= 1 - - direct_method_idx = 0 - # print("--" * 10 + "DirectMethod" + "--" * 10) - while direct_method_size > 0: - direct_method_ = self.read_uleb128(self.buff) - direct_method_idx += direct_method_ - method_name, signature = self.get_method_name_by_idx( - direct_method_idx) - access_flags = self.read_uleb128(self.buff) - code_off = self.read_uleb128(self.buff) - # code_inss = self.read_code_item(code_off) - - direct_method_size -= 1 - all_codes = { - "direct_method_idx": direct_method_idx, - "method_name": method_name, - "signature": signature, - "access_flags": access_flags, - "code_off": code_off - } - - class_def['code_item'].append(all_codes) - - # print("direct Method : %s method_name: %s access_flag: %s code_off: %s code_ins len: %s" % - # (self.method_idx_list[direct_method_idx], method_name, hex(access_flags), hex(code_off), - # len(code_inss))) - # for i,ins in enumerate(code_inss): - # print("code[%d]: %s"%(i,hex(ins))) - # - # print("--"*10+"VirtualMethod"+"--"*10) - - virtual_method_idx = 0 - while virtual_method_size > 0: - virtual_method_ = self.read_uleb128(self.buff) - virtual_method_idx += virtual_method_ - method_name, signature = self.get_method_name_by_idx( - virtual_method_idx) - access_flags = self.read_uleb128(self.buff) - code_off = self.read_uleb128(self.buff) - # code_inss = self.read_code_item(code_off) - virtual_method_size -= 1 - all_codes = { - "virtual_method_idx": virtual_method_idx, - "method_name": method_name, - "signature": signature, - "access_flags": access_flags, - "code_off": code_off - } - class_def['code_item'].append(all_codes) - # print("virtual Method : %s method_name: %s access_flag: %s code_off: %s code_ins len: %s" % - # (self.method_idx_list[direct_method_idx],method_name,hex(access_flags),hex(code_off),len(code_inss))) - # for i, ins in enumerate(code_inss): - # print("code[%d]: %s" % (i, hex(ins))) - # self.class_defs[''] - - self.class_defs.append(class_def) - - def read_code_instrs(self, code_off): - - if code_off == 0x0: - return [] - - _buff = io.BytesIO(self.__raw) - _buff.seek(code_off, io.SEEK_SET) - - method_register_size, = unpack("H", _buff.read(2)) - method_ins_size, = unpack("H", _buff.read(2)) - method_outs_size, = unpack("H", _buff.read(2)) - method_tries_size, = unpack("H", _buff.read(2)) - method_debug_info_off, = unpack("I", _buff.read(4)) - - method_instructions_size, = unpack("I", _buff.read(4)) - # record codeitem's offset and size - code_instructions = [code_off, method_instructions_size] - - # print("--> code_off :%s method_instructions_size:%s"%(hex(code_off),hex(method_instructions_size))) - while method_instructions_size > 0: - # ins_code, = unpack("B", _buff.read(2)) # 原始读取的指令是两个字节, - # code_instructions.append(ins_code) - for _ in range(2): - ins_code, = unpack("B", _buff.read(1)) # 为了方便输出,每次读取一个字节 - code_instructions.append(ins_code) - method_instructions_size -= 1 - - return code_instructions - - def is_target_clazz(self, pkg, clazz): - - # return True if pkg in clazz else False - need_filter_classes = [ - '.R$attr', - '.R$drawable', - '.R$id', - '.R$layout', - '.R$string', - '.R', - '.BuildConfig' - ] - android_s = [ - "^(Landroid/support|Landroid/arch|Landroidx/versionedparcelable|Landroidx/core|Lkotlin/|Lkotlinx/).+", - ".*(R\$.+)$", - ".+(/BuildConfig;|/R;)$", - - ] - - if isinstance(clazz, bytes): - # print(clazz) - try: - clazz = str(clazz, encoding="utf-8") - except UnicodeDecodeError as e: - echo("dexparse", "faile to decode string : {} ,error {} ".format( - clazz, e), color="red") - return False - if clazz == '': - return False - - for a in android_s: - expr = re.compile(a) - if expr.search(clazz): - return False - # print("--> check %s class: %s" % (pkg, clazz)) - # return True - if pkg is None or pkg == '': - return True - clazz = clazz.replace("L", "").replace("/", '.').replace(";", "") - - # filter thridpart class ,like google's code etc - suffix = clazz[clazz.rfind('.'):] - if suffix in need_filter_classes: - return False - # # - # target = re.compile(pkg) - # if target.match(clazz): - # return True - return True - - def get_method_name_by_idx(self, idx): - - dex_method_idx = self.method_idx_list[idx] - - # short_class_idx = dex_method_idx.class_idx - name_idx = dex_method_idx.name_idx - proto_idx = dex_method_idx.proto_idx - - method_name = self.get_string_by_idx(name_idx) - - # clazz_name = self.get_class_name_by_idx(short_class_idx) - signature = self.get_method_proto_name_by_idx( - proto_idx) - - # print("dex_method_idx: "+str(dex_method_idx)) - # print("--> class_name: %s , method: %s, signature:%s " % - # (clazz_name, method_name, signature)) - - return method_name, signature - - def get_param_type(self, _idx): - # for offset in self.type_item_offset_list: - - for idx, str_offset in enumerate(self.string_item_offset_list): - if idx == _idx: - return self.string_table_map[str_offset] - # print("-----> read type item :%s" % - # (hex(offset)), self.string_table_map[str_offset]) - - def get_class_name_by_idx(self, idx): - """ - Return bytes like strings - """ - off = self.string_item_offset_list[self.type_item_offset_list[idx]] - return self.string_table_map[off] - - def read_param_size(self, paramoffset): - """ - 读取参数 paramoffset - """ - if paramoffset == 0: - - # 没有参数的情况 paramoffset = 0 - return 0, 0 - buff = io.BytesIO(self.__raw) - buff.seek(paramoffset, io.SEEK_CUR) - size, = unpack('I', buff.read(4)) - idx, = unpack("H", buff.read(2)) - # print("-> paramoffset:%s size: %d ,idx : %s" % - # (hex(paramoffset), size, hex(idx))) - return size, idx - - def get_method_proto_name_by_idx(self, idx): - """ - 方法的签名信息 - """ - - dexmethod_proto = self.dex_method_obj_index[idx] - - # method_proto_name = self.get_string_by_idx(dexmethod_proto.shorty_idx) - rtvalue = self.get_string_by_idx( - self.type_item_offset_list[dexmethod_proto.rturn_type_idx]) - - # print("rtvalue: %s" % (rtvalue)) - size, _idx = self.read_param_size( - dexmethod_proto.parameter_type_offset) - # print("--> 检测 参数 "+str(dexmethod_proto)) - signature = "(" - if size == 0: - signature += ")" - else: - for _ in range(size): - # print("dexmethod_proto.shorty_idx : %s type: %s : list: %s" % - # (hex(dexmethod_proto.shorty_idx), self.type_item_offset_list[dexmethod_proto.shorty_idx], self.type_item_offset_list)) - var = self.get_param_type( - self.type_item_offset_list[_idx]) - if isinstance(var, bytes): - var = str(var, encoding="utf-8") - signature += var+"," - signature = signature[:-1] - signature += ")" - if isinstance(rtvalue, bytes): - rtvalue = str(rtvalue, encoding="utf-8") - signature += rtvalue - - return signature - - def get_string_by_idx(self, idx): - off = self.string_item_offset_list[idx] - return self.string_table_map[off] - - def read_uleb128(self, buff, offset=0): - ''' - ULEB128 - ''' - result, = unpack('B', buff.read(1)) - if result > 0x7f: - cur, = unpack('B', buff.read(1)) - result = (result & 0x7f) | ((cur & 0x7f) << 7) - if cur > 0x7f: - cur, = unpack('B', buff.read(1)) - result |= (cur & 0x7f) << 14 - if cur > 0x7f: - cur, = unpack('B', buff.read(1)) - result |= (cur & 0x7f) << 21 - if cur > 0x7f: - cur, = unpack('B', buff.read(1)) - if cur > 0x0f: - print(" warning possible error while decoding number") - result |= cur << 28 - return result diff --git a/build/lib/androyara/dex/dex_method.py b/build/lib/androyara/dex/dex_method.py deleted file mode 100644 index 2580aaa..0000000 --- a/build/lib/androyara/dex/dex_method.py +++ /dev/null @@ -1,74 +0,0 @@ -# coding:utf8 -''' -@File : dex_method.py -@Author : Loopher -@Version : 1.0 -@License : (C)Copyright 2020-2021,Loopher -@Desc : DexMethod -''' - - -class DexMethodProto: - - def __init__(self): - # DexMethod reference - self.shorty_idx = None - self.rturn_type_idx = None - self.parameter_type_offset = None - - def __str__(self): - - return " shorty_idx: %s return_type_idx: %s parameter_type_offset: %s" % (hex(self.shorty_idx), - hex( - self.rturn_type_idx), - hex(self.parameter_type_offset)) - - -class DexFieldIdx: - - def __init__(self) -> None: - self.class_idx = None - self.type_idx = None - self.name_idx = None - - def __str__(self): - - return " class_idx: %s type_idx: %s name_idx: %s" % ( - hex(self.class_idx), - hex(self.type_idx), - hex(self.name_idx) - ) - - -class DexMethodIdx: - - def __init__(self): - self.class_idx = None - self.proto_idx = None - self.name_idx = None - - def __str__(self): - - return "DexMethodIdx: class_idx: %s proto_idx: %s name_idx: %s " % ( - hex(self.class_idx), - hex(self.proto_idx), - hex(self.name_idx) - ) - - -class DexMethod: - - def __init__(self): - - self.method_idx = None - self.access_flag = None - self.code_off = None - - def __str__(self): - - return "DexMethod: method_id : %s access_flag: %s code_off: %s" % ( - - hex(self.method_idx), - hex(self.access_flag), - hex(self.code_off) - ) diff --git a/build/lib/androyara/dex/dex_vm.py b/build/lib/androyara/dex/dex_vm.py deleted file mode 100644 index 5804484..0000000 --- a/build/lib/androyara/dex/dex_vm.py +++ /dev/null @@ -1,176 +0,0 @@ -# coding:utf8 -''' -@File : dex_vm.py -@Author : Loopher -@Version : 1.0 -@License : (C)Copyright 2020-2021,Loopher -@Desc : DexFile VM -''' - -from androguard.core.bytecodes import dvm -import re -from androyara.utils.buffer import BuffHandle -from androyara.dex.dex_header import DexHeader -from androyara.utils.utility import byte2str, echo - - -class DexFileVM(BuffHandle): - - def __init__(self, pkgname, buff): - super(DexFileVM, self).__init__(buff) - - self.raw = buff - # need check apk - self.dex_header = DexHeader(buff) - self._ok = self.dex_header.is_dex() - if not self._ok: - # echo("error", "is not dex file format", 'red') - return - self.dex_header.read_all(pkgname) # read all pkg class - - def ok(self): - return self._ok - - def all_class_defs(self): - """ - Return all class in classes\d.dex - default will return classes.dex - """ - - return self.dex_header.class_defs - - def analysis_dex(self, clazz_name, method_name, show_ins=False): - """ - Analyzer dex .This function mainly is use to show dex class_defs - - eg. - if clazz_name or method_name is None or empty, default show all - class->method - else if method_name is not none ,show the matched method info ,include - - class_name->method - method_instructions - - else if class_name and method are not none or empty ,this means to specific class_def's method info - - """ - if clazz_name is None: - # class_name = "com.demo.Test" - clazz_name = '' - if method_name is None: - method_name = '' - - marker = '.java -> ' - query = clazz_name + marker + method_name - - for class_def in self.all_class_defs(): - clzz_name = byte2str(class_def['class_name']) - - clzz_name = clzz_name.replace( - "L", '').replace("/", '.').replace(";", '') - - for method_ in class_def['code_item']: - _method_name = byte2str(method_['method_name']) - signature = byte2str(method_['signature']) - - _x = clzz_name+marker+_method_name+signature - - if clazz_name != marker and method_name != '' and query == _x: - # show class.method(signature) - print("**"*20) - echo("className", " %s" % (clzz_name), "yellow") - echo("methodName", " %s" % (_method_name), "yellow") - echo("signature", " %s" % (signature), "yellow") - self.print_ins(method_['code_off'], show=show_ins) - - elif method_name == '': - echo("info", "-> %s" % - (_x), "blue") - - elif method_name == _method_name: - print("**"*20) - echo("className", "%s" % (clzz_name), "blue") - echo("methodName", "%s" % (_method_name), "blue") - echo("signature", " %s" % (signature), "blue") - self.print_ins(method_['code_off'], show=show_ins) - - def print_ins(self, offset, show=True): - ins_ = ' ' - instrus = self.dex_header.read_code_instrs(offset) - - if show: - - echo("instructions", "--"*20, 'yellow') - # red, green, yellow, blue, magenta, cyan, white. - echo("codeoff", " %s" % - (hex(instrus[0])), "yellow") - echo("codesize", " %s" % - (hex(instrus[1])), "yellow") - print("") - - # 格式化输出 - print(" "+" 0 "+" "+""+" 1 "+" "+" 2 "+" "+""+" 3 " - + " "+" 4 "+" "+""+" 5 "+" "+" 6 "+" "+""+" 7 " - + " "+" 8 "+" "+""+" 9 "+" "+" A "+" "+""+" B " - + " "+" C "+" "+""+" D "+" "+" E "+" "+""+" F ") - - print("-"*16 + " "+"-"*16+" "+"-"*16+" "+"-"*16) - save = [] - for i, ins in enumerate(instrus[2:]): - if show: - if i > 0 and i % 16 == 0: - print("%s" % (ins_)) - print("") - ins_ = "" - if i > 0 and i % 4 == 0: - ins_ += " " - ins_ += "%.2x " % (ins)+" " - save.append("%.2x " % (ins)) - if show: - print(ins_) - # print("") - # echo("info", "all instructions ", ) - echo("shellcode", "\n"+" ".join(save), 'yellow') - return " ".join(save) - - def all_strings(self, pattern_list: list): - """ - return all dex strings - """ - - # echo("warning", "pattern list : %s" % (pattern_list), 'yellow') - strings = [] - reobjs_exprs = [] - for pattern in pattern_list: - if pattern is None: - continue - if "," in pattern: - for p in pattern.split(','): - expr = re.compile(p) - reobjs_exprs.append(expr) - elif pattern is not None and pattern != '': - reobjs_exprs.append(re.compile(pattern)) - # echo("info", "pattern: %s" % (pattern), 'yellow') - - # 2021-06-24 fixme : Using androguard to parse strings instead myself - d = dvm.DalvikVMFormat(self.raw) - # for _, v in self.dex_header.string_table_map.items(): - for v in d.get_strings(): - try: - if isinstance(v, bytes): - v = str(v, encoding="utf-8") - # echo("warning", "string: {}".format(v), 'yellow') - except UnicodeDecodeError: - continue - - if len(reobjs_exprs) == 0: - # all string - strings.append(v) - continue - - for expr in reobjs_exprs: - if expr.search(v): - # echo("debug", "v: %s" % (v), 'white') - strings.append(v) - - return strings diff --git a/build/lib/androyara/parser/__init__.py b/build/lib/androyara/parser/__init__.py deleted file mode 100644 index f504d32..0000000 --- a/build/lib/androyara/parser/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- encoding: utf-8 -*- -''' -@File : __init__.py -@Author : Loopher -@Version : 1.0 -@License : (C)Copyright 2020-2021, Loopher -@Desc : None -''' - -# Here put the import lib diff --git a/build/lib/androyara/parser/base_parser.py b/build/lib/androyara/parser/base_parser.py deleted file mode 100644 index 10e4663..0000000 --- a/build/lib/androyara/parser/base_parser.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- encoding: utf-8 -*- -''' -@File : base_parser.py -@Author : Loopher -@Version : 1.0 -@License : (C)Copyright 2020-2021, Loopher -@Desc : 所有的解析器的父类 -''' - -# Here put the import lib - -import os -import hashlib - - -class ReadApkError(BaseException): - pass - - -class BaserParser(object): - - parser_info = { - "name": "BaserParser", - "desc": "FooParser" - - } - - def __init__(self, filename, buff): - - if filename is not None and os.path.isfile(filename): - self.filename = filename - with open(filename, 'rb') as out: - self.buff = out.read() - elif buff is not None: - self.filename = hashlib.sha256(buff).hexdigest() - self.buff = buff - else: - raise ReadApkError("filename or buff must not be None") diff --git a/build/lib/androyara/typeinfo/__init__.py b/build/lib/androyara/typeinfo/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/lib/androyara/typeinfo/public.xml b/build/lib/androyara/typeinfo/public.xml deleted file mode 100644 index 997575f..0000000 --- a/build/lib/androyara/typeinfo/public.xml +++ /dev/null @@ -1,2925 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/build/lib/androyara/typeinfo/publics.py b/build/lib/androyara/typeinfo/publics.py deleted file mode 100644 index 2652924..0000000 --- a/build/lib/androyara/typeinfo/publics.py +++ /dev/null @@ -1,36 +0,0 @@ -import os -from xml.dom import minidom - - -""" -from androguard public.py -""" -_public_res = None -# copy the newest sdk/platforms/android-?/data/res/values/public.xml here -if _public_res is None: - _public_res = {} - root = os.path.dirname(os.path.realpath(__file__)) - xmlfile = os.path.join(root, "public.xml") - if os.path.isfile(xmlfile): - with open(xmlfile, "r") as fp: - _xml = minidom.parseString(fp.read()) - for element in _xml.getElementsByTagName("public"): - _type = element.getAttribute('type') - _name = element.getAttribute('name') - _id = int(element.getAttribute('id'), 16) - if _type not in _public_res: - _public_res[_type] = {} - _public_res[_type][_name] = _id - else: - raise Exception("need to copy the sdk/platforms/android-?/data/res/values/public.xml here root: {}".format(root)) - -SYSTEM_RESOURCES = { - "attributes": { - "forward": {k: v for k, v in _public_res['attr'].items()}, - "inverse": {v: k for k, v in _public_res['attr'].items()} - }, - "styles": { - "forward": {k: v for k, v in _public_res['style'].items()}, - "inverse": {v: k for k, v in _public_res['style'].items()} - } -} \ No newline at end of file diff --git a/build/lib/androyara/typeinfo/types.py b/build/lib/androyara/typeinfo/types.py deleted file mode 100644 index e0565d8..0000000 --- a/build/lib/androyara/typeinfo/types.py +++ /dev/null @@ -1,3784 +0,0 @@ - - -""" -Refere from Androguard -""" -from struct import unpack, pack -import logging -from xml.sax.saxutils import escape -import collections -from collections import defaultdict -from lxml import etree -import logging -import re -import sys -import binascii - -from androyara.typeinfo import publics -from androyara.utils.buffer import BuffHandle - - -log = logging.getLogger("androyara.xml") - - -# Type definiton for (type, data) tuples representing a value -# See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#262 - -# The 'data' is either 0 or 1, specifying this resource is either -# undefined or empty, respectively. -TYPE_NULL = 0x00 -# The 'data' holds a ResTable_ref, a reference to another resource -# table entry. -TYPE_REFERENCE = 0x01 -# The 'data' holds an attribute resource identifier. -TYPE_ATTRIBUTE = 0x02 -# The 'data' holds an index into the containing resource table's -# global value string pool. -TYPE_STRING = 0x03 -# The 'data' holds a single-precision floating point number. -TYPE_FLOAT = 0x04 -# The 'data' holds a complex number encoding a dimension value -# such as "100in". -TYPE_DIMENSION = 0x05 -# The 'data' holds a complex number encoding a fraction of a -# container. -TYPE_FRACTION = 0x06 -# The 'data' holds a dynamic ResTable_ref, which needs to be -# resolved before it can be used like a TYPE_REFERENCE. -TYPE_DYNAMIC_REFERENCE = 0x07 -# The 'data' holds an attribute resource identifier, which needs to be resolved -# before it can be used like a TYPE_ATTRIBUTE. -TYPE_DYNAMIC_ATTRIBUTE = 0x08 -# Beginning of integer flavors... -TYPE_FIRST_INT = 0x10 -# The 'data' is a raw integer value of the form n..n. -TYPE_INT_DEC = 0x10 -# The 'data' is a raw integer value of the form 0xn..n. -TYPE_INT_HEX = 0x11 -# The 'data' is either 0 or 1, for input "false" or "true" respectively. -TYPE_INT_BOOLEAN = 0x12 -# Beginning of color integer flavors... -TYPE_FIRST_COLOR_INT = 0x1c -# The 'data' is a raw integer value of the form #aarrggbb. -TYPE_INT_COLOR_ARGB8 = 0x1c -# The 'data' is a raw integer value of the form #rrggbb. -TYPE_INT_COLOR_RGB8 = 0x1d -# The 'data' is a raw integer value of the form #argb. -TYPE_INT_COLOR_ARGB4 = 0x1e -# The 'data' is a raw integer value of the form #rgb. -TYPE_INT_COLOR_RGB4 = 0x1f -# ...end of integer flavors. -TYPE_LAST_COLOR_INT = 0x1f -# ...end of integer flavors. -TYPE_LAST_INT = 0x1f - - -# Constants for ARSC Files -# see http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#215 -RES_NULL_TYPE = 0x0000 -RES_STRING_POOL_TYPE = 0x0001 -RES_TABLE_TYPE = 0x0002 -RES_XML_TYPE = 0x0003 - -RES_XML_FIRST_CHUNK_TYPE = 0x0100 -RES_XML_START_NAMESPACE_TYPE = 0x0100 -RES_XML_END_NAMESPACE_TYPE = 0x0101 -RES_XML_START_ELEMENT_TYPE = 0x0102 -RES_XML_END_ELEMENT_TYPE = 0x0103 -RES_XML_CDATA_TYPE = 0x0104 -RES_XML_LAST_CHUNK_TYPE = 0x017f - -RES_XML_RESOURCE_MAP_TYPE = 0x0180 - -RES_TABLE_PACKAGE_TYPE = 0x0200 -RES_TABLE_TYPE_TYPE = 0x0201 -RES_TABLE_TYPE_SPEC_TYPE = 0x0202 -RES_TABLE_LIBRARY_TYPE = 0x0203 - -# Flags in the STRING Section -SORTED_FLAG = 1 << 0 -UTF8_FLAG = 1 << 8 - -# Position of the fields inside an attribute -ATTRIBUTE_IX_NAMESPACE_URI = 0 -ATTRIBUTE_IX_NAME = 1 -ATTRIBUTE_IX_VALUE_STRING = 2 -ATTRIBUTE_IX_VALUE_TYPE = 3 -ATTRIBUTE_IX_VALUE_DATA = 4 -ATTRIBUTE_LENGHT = 5 - -# Internally used state variables for AXMLParser -START_DOCUMENT = 0 -END_DOCUMENT = 1 -START_TAG = 2 -END_TAG = 3 -TEXT = 4 - -# Table used to lookup functions to determine the value representation in ARSCParser -TYPE_TABLE = { - TYPE_ATTRIBUTE: "attribute", - TYPE_DIMENSION: "dimension", - TYPE_FLOAT: "float", - TYPE_FRACTION: "fraction", - TYPE_INT_BOOLEAN: "int_boolean", - TYPE_INT_COLOR_ARGB4: "int_color_argb4", - TYPE_INT_COLOR_ARGB8: "int_color_argb8", - TYPE_INT_COLOR_RGB4: "int_color_rgb4", - TYPE_INT_COLOR_RGB8: "int_color_rgb8", - TYPE_INT_DEC: "int_dec", - TYPE_INT_HEX: "int_hex", - TYPE_NULL: "null", - TYPE_REFERENCE: "reference", - TYPE_STRING: "string", -} - -RADIX_MULTS = [0.00390625, 3.051758E-005, 1.192093E-007, 4.656613E-010] -DIMENSION_UNITS = ["px", "dip", "sp", "pt", "in", "mm"] -FRACTION_UNITS = ["%", "%p"] - -COMPLEX_UNIT_MASK = 0x0F -# ------------------------------- - - -class ResParserError(Exception): - """Exception for the parsers""" - pass - - -def complexToFloat(xcomplex): - """ - Convert a complex unit into float - """ - return float(xcomplex & 0xFFFFFF00) * RADIX_MULTS[(xcomplex >> 4) & 3] - - -# Constants for ARSC Files -# see http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#215 -RES_NULL_TYPE = 0x0000 -RES_STRING_POOL_TYPE = 0x0001 -RES_TABLE_TYPE = 0x0002 -RES_XML_TYPE = 0x0003 - -RES_XML_FIRST_CHUNK_TYPE = 0x0100 -RES_XML_START_NAMESPACE_TYPE = 0x0100 -RES_XML_END_NAMESPACE_TYPE = 0x0101 -RES_XML_START_ELEMENT_TYPE = 0x0102 -RES_XML_END_ELEMENT_TYPE = 0x0103 -RES_XML_CDATA_TYPE = 0x0104 -RES_XML_LAST_CHUNK_TYPE = 0x017f - -RES_XML_RESOURCE_MAP_TYPE = 0x0180 - -RES_TABLE_PACKAGE_TYPE = 0x0200 -RES_TABLE_TYPE_TYPE = 0x0201 -RES_TABLE_TYPE_SPEC_TYPE = 0x0202 -RES_TABLE_LIBRARY_TYPE = 0x0203 - -# Flags in the STRING Section -SORTED_FLAG = 1 << 0 -UTF8_FLAG = 1 << 8 - -# Position of the fields inside an attribute -ATTRIBUTE_IX_NAMESPACE_URI = 0 -ATTRIBUTE_IX_NAME = 1 -ATTRIBUTE_IX_VALUE_STRING = 2 -ATTRIBUTE_IX_VALUE_TYPE = 3 -ATTRIBUTE_IX_VALUE_DATA = 4 -ATTRIBUTE_LENGHT = 5 - -# Internally used state variables for AXMLParser -START_DOCUMENT = 0 -END_DOCUMENT = 1 -START_TAG = 2 -END_TAG = 3 -TEXT = 4 - -# Table used to lookup functions to determine the value representation in ARSCParser -TYPE_TABLE = { - TYPE_ATTRIBUTE: "attribute", - TYPE_DIMENSION: "dimension", - TYPE_FLOAT: "float", - TYPE_FRACTION: "fraction", - TYPE_INT_BOOLEAN: "int_boolean", - TYPE_INT_COLOR_ARGB4: "int_color_argb4", - TYPE_INT_COLOR_ARGB8: "int_color_argb8", - TYPE_INT_COLOR_RGB4: "int_color_rgb4", - TYPE_INT_COLOR_RGB8: "int_color_rgb8", - TYPE_INT_DEC: "int_dec", - TYPE_INT_HEX: "int_hex", - TYPE_NULL: "null", - TYPE_REFERENCE: "reference", - TYPE_STRING: "string", -} - -RADIX_MULTS = [0.00390625, 3.051758E-005, 1.192093E-007, 4.656613E-010] -DIMENSION_UNITS = ["px", "dip", "sp", "pt", "in", "mm"] -FRACTION_UNITS = ["%", "%p"] - -COMPLEX_UNIT_MASK = 0x0F - - -class ResParserError(Exception): - """Exception for the parsers""" - pass - - -def complexToFloat(xcomplex): - """ - Convert a complex unit into float - """ - return float(xcomplex & 0xFFFFFF00) * RADIX_MULTS[(xcomplex >> 4) & 3] - - -class StringBlock: - """ - StringBlock is a CHUNK inside an AXML File: `ResStringPool_header` - It contains all strings, which are used by referecing to ID's - - See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#436 - """ - - def __init__(self, buff, header): - """ - :param buff: buffer which holds the string block - :param header: a instance of :class:`~ARSCHeader` - """ - self._cache = {} - self.header = header - # We already read the header (which was chunk_type and chunk_size - # Now, we read the string_count: - self.stringCount = unpack(' 0: - log.info("Styles Offset given, but styleCount is zero. " - "This is not a problem but could indicate packers.") - - self.m_stringOffsets = [] - self.m_styleOffsets = [] - self.m_charbuff = "" - self.m_styles = [] - - # Next, there is a list of string following. - # This is only a list of offsets (4 byte each) - for i in range(self.stringCount): - self.m_stringOffsets.append(unpack('".format(self.stringCount, self.styleCount, self.m_isUTF8) - - def __getitem__(self, idx): - """ - Returns the string at the index in the string table - """ - return self.getString(idx) - - def __len__(self): - """ - Get the number of strings stored in this table - """ - return self.stringCount - - def __iter__(self): - """ - Iterable over all strings - """ - for i in range(self.stringCount): - yield self.getString(i) - - def getString(self, idx): - """ - Return the string at the index in the string table - - :param idx: index in the string table - :return: str - """ - if idx in self._cache: - return self._cache[idx] - - if idx < 0 or not self.m_stringOffsets or idx > self.stringCount: - return "" - - offset = self.m_stringOffsets[idx] - - if self.m_isUTF8: - self._cache[idx] = self._decode8(offset) - else: - self._cache[idx] = self._decode16(offset) - - return self._cache[idx] - - def getStyle(self, idx): - """ - Return the style associated with the index - - :param idx: index of the style - :return: - """ - return self.m_styles[idx] - - def _decode8(self, offset): - """ - Decode an UTF-8 String at the given offset - - :param offset: offset of the string inside the data - :return: str - """ - # UTF-8 Strings contain two lengths, as they might differ: - # 1) the UTF-16 length - str_len, skip = self._decode_length(offset, 1) - offset += skip - - # 2) the utf-8 string length - encoded_bytes, skip = self._decode_length(offset, 1) - offset += skip - - data = self.m_charbuff[offset: offset + encoded_bytes] - - if self.m_charbuff[offset + encoded_bytes] != 0: - # - raise ResParserError( - "UTF-8 String is not null terminated! At offset={} .try to use aapt dump xmlstrings a.apk AndroidManifest.xml".format(offset)) - - return self._decode_bytes(data, 'utf-8', str_len) - - def _decode16(self, offset): - """ - Decode an UTF-16 String at the given offset - - :param offset: offset of the string inside the data - :return: str - """ - str_len, skip = self._decode_length(offset, 2) - offset += skip - - # The len is the string len in utf-16 units - encoded_bytes = str_len * 2 - - data = self.m_charbuff[offset: offset + encoded_bytes] - - if self.m_charbuff[offset + encoded_bytes:offset + encoded_bytes + 2] != b"\x00\x00": - raise ResParserError( - "UTF-16 String is not null terminated! At offset={}".format(offset)) - - return self._decode_bytes(data, 'utf-16', str_len) - - @staticmethod - def _decode_bytes(data, encoding, str_len): - """ - Generic decoding with length check. - The string is decoded from bytes with the given encoding, then the length - of the string is checked. - The string is decoded using the "replace" method. - - :param data: bytes - :param encoding: encoding name ("utf-8" or "utf-16") - :param str_len: length of the decoded string - :return: str - """ - string = data.decode(encoding, 'replace') - if len(string) != str_len: - log.warning("invalid decoded string length") - return string - - def _decode_length(self, offset, sizeof_char): - """ - Generic Length Decoding at offset of string - - The method works for both 8 and 16 bit Strings. - Length checks are enforced: - * 8 bit strings: maximum of 0x7FFF bytes (See - http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/ResourceTypes.cpp#692) - * 16 bit strings: maximum of 0x7FFFFFF bytes (See - http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/ResourceTypes.cpp#670) - - :param offset: offset into the string data section of the beginning of - the string - :param sizeof_char: number of bytes per char (1 = 8bit, 2 = 16bit) - :returns: tuple of (length, read bytes) - """ - sizeof_2chars = sizeof_char << 1 - fmt = "<2{}".format('B' if sizeof_char == 1 else 'H') - highbit = 0x80 << (8 * (sizeof_char - 1)) - - length1, length2 = unpack( - fmt, self.m_charbuff[offset:(offset + sizeof_2chars)]) - - if (length1 & highbit) != 0: - length = ((length1 & ~highbit) << (8 * sizeof_char)) | length2 - size = sizeof_2chars - else: - length = length1 - size = sizeof_char - - # These are true asserts, as the size should never be less than the values - if sizeof_char == 1: - assert length <= 0x7FFF, "length of UTF-8 string is too large! At offset={}".format( - offset) - else: - assert length <= 0x7FFFFFFF, "length of UTF-16 string is too large! At offset={}".format( - offset) - - return length, size - - def show(self): - """ - Print some information on stdout about the string table - """ - print("StringBlock(stringsCount=0x%x, " - "stringsOffset=0x%x, " - "stylesCount=0x%x, " - "stylesOffset=0x%x, " - "flags=0x%x" - ")" % (self.stringCount, - self.stringsOffset, - self.styleCount, - self.stylesOffset, - self.flags)) - - if self.stringCount > 0: - print() - print("String Table: ") - for i, s in enumerate(self): - print("{:08d} {}".format(i, repr(s))) - - if self.styleCount > 0: - print() - print("Styles Table: ") - for i in range(self.styleCount): - print("{:08d} {}".format(i, repr(self.getStyle(i)))) - - -class AXMLParser: - """ - AXMLParser reads through all chunks in the AXML file - and implements a state machine to return information about - the current chunk, which can then be read by :class:`~AXMLPrinter`. - - An AXML file is a file which contains multiple chunks of data, defined - by the `ResChunk_header`. - There is no real file magic but as the size of the first header is fixed - and the `type` of the `ResChunk_header` is set to `RES_XML_TYPE`, a file - will usually start with `0x03000800`. - But there are several examples where the `type` is set to something - else, probably in order to fool parsers. - - Typically the AXMLParser is used in a loop which terminates if `m_event` is set to `END_DOCUMENT`. - You can use the `next()` function to get the next chunk. - Note that not all chunk types are yielded from the iterator! Some chunks are processed in - the AXMLParser only. - The parser will set `is_valid()` to False if it parses something not valid. - Messages what is wrong are logged. - - See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#563 - """ - - def __init__(self, raw_buff): - self._reset() - - self._valid = True - self.axml_tampered = False - self.buff = BuffHandle(raw_buff) - - # Minimum is a single ARSCHeader, which would be a strange edge case... - if self.buff.size() < 8: - log.error("Filesize is too small to be a valid AXML file! Filesize: {}".format( - self.buff.size())) - self._valid = False - return - - # This would be even stranger, if an AXML file is larger than 4GB... - # But this is not possible as the maximum chunk size is a unsigned 4 byte int. - if self.buff.size() > 0xFFFFFFFF: - log.error("Filesize is too large to be a valid AXML file! Filesize: {}".format( - self.buff.size())) - self._valid = False - return - - try: - axml_header = ARSCHeader(self.buff) - except ResParserError as e: - log.error("Error parsing first resource header: %s", e) - self._valid = False - return - - self.filesize = axml_header.size - - if axml_header.header_size == 28024: - # Can be a common error: the file is not an AXML but a plain XML - # The file will then usually start with ' self.buff.size(): - log.error("This does not look like an AXML file. Declared filesize does not match real size: {} vs {}".format( - self.filesize, self.buff.size())) - self._valid = False - return - - if self.filesize < self.buff.size(): - # The file can still be parsed up to the point where the chunk should end. - self.axml_tampered = True - log.warning("Declared filesize ({}) is smaller than total file size ({}). " - "Was something appended to the file? Trying to parse it anyways.".format(self.filesize, self.buff.size())) - - # Not that severe of an error, we have plenty files where this is not - # set correctly - if axml_header.type != RES_XML_TYPE: - self.axml_tampered = True - log.warning("AXML file has an unusual resource type! " - "Malware likes to to such stuff to anti androguard! " - "But we try to parse it anyways. Resource Type: 0x{:04x}".format(axml_header.type)) - - # Now we parse the STRING POOL - try: - header = ARSCHeader(self.buff, expected_type=RES_STRING_POOL_TYPE) - except ResParserError as e: - log.error("Error parsing resource header of string pool: %s", e) - self._valid = False - return - - if header.header_size != 0x1C: - log.error("This does not look like an AXML file. String chunk header size does not equal 28! header size = {}".format( - header.header_size)) - self._valid = False - return - - self.sb = StringBlock(self.buff, header) - - # Stores resource ID mappings, if any - self.m_resourceIDs = [] - - # Store a list of prefix/uri mappings encountered - self.namespaces = [] - - def is_valid(self): - """ - Get the state of the AXMLPrinter. - if an error happend somewhere in the process of parsing the file, - this flag is set to False. - """ - return self._valid - - def _reset(self): - self.m_event = -1 - self.m_lineNumber = -1 - self.m_name = -1 - self.m_namespaceUri = -1 - self.m_attributes = [] - self.m_idAttribute = -1 - self.m_classAttribute = -1 - self.m_styleAttribute = -1 - - def __next__(self): - self._do_next() - return self.m_event - - def _do_next(self): - if self.m_event == END_DOCUMENT: - return - - self._reset() - while self._valid: - # Stop at the declared filesize or at the end of the file - if self.buff.end() or self.buff.get_idx() == self.filesize: - self.m_event = END_DOCUMENT - break - - # Again, we read an ARSCHeader - try: - h = ARSCHeader(self.buff) - except ResParserError as e: - log.error("Error parsing resource header: %s", e) - self._valid = False - return - - # Special chunk: Resource Map. This chunk might be contained inside - # the file, after the string pool. - if h.type == RES_XML_RESOURCE_MAP_TYPE: - log.debug("AXML contains a RESOURCE MAP") - # Check size: < 8 bytes mean that the chunk is not complete - # Should be aligned to 4 bytes. - if h.size < 8 or (h.size % 4) != 0: - log.error("Invalid chunk size in chunk XML_RESOURCE_MAP") - self._valid = False - return - - for i in range((h.size - h.header_size) // 4): - self.m_resourceIDs.append( - unpack(' RES_XML_LAST_CHUNK_TYPE: - # h.size is the size of the whole chunk including the header. - # We read already 8 bytes of the header, thus we need to - # subtract them. - log.error("Not a XML resource chunk type: 0x{:04x}. Skipping {} bytes".format( - h.type, h.size)) - self.buff.set_idx(h.end) - continue - - # Check that we read a correct header - if h.header_size != 0x10: - log.error("XML Resource Type Chunk header size does not match 16! " - "At chunk type 0x{:04x}, declared header size={}, chunk size={}".format(h.type, h.header_size, h.size)) - self._valid = False - return - - # Line Number of the source file, only used as meta information - self.m_lineNumber, = unpack(' uri {}: '{}'".format( - prefix, s_prefix, uri, s_uri)) - - if s_uri == '': - log.warning("Namespace prefix '{}' resolves to empty URI. " - "This might be a packer.".format(s_prefix)) - - if (prefix, uri) in self.namespaces: - log.info("Namespace mapping ({}, {}) already seen! " - "This is usually not a problem but could indicate packers or broken AXML compilers.".format(prefix, uri)) - self.namespaces.append((prefix, uri)) - - # We can continue with the next chunk, as we store the namespace - # mappings for each tag - continue - - if h.type == RES_XML_END_NAMESPACE_TYPE: - # END_PREFIX contains again prefix and uri field - prefix, = unpack('> 16) - 1 - self.m_attribute_count = attributeCount & 0xFFFF - self.m_styleAttribute = (self.m_classAttribute >> 16) - 1 - self.m_classAttribute = (self.m_classAttribute & 0xFFFF) - 1 - - # Now, we parse the attributes. - # Each attribute has 5 fields of 4 byte - for i in range(0, self.m_attribute_count * ATTRIBUTE_LENGHT): - # Each field is linearly parsed into the array - # Each Attribute contains: - # * Namespace URI (String ID) - # * Name (String ID) - # * Value - # * Type - # * Data - self.m_attributes.append( - unpack('> 24 - - self.m_event = START_TAG - break - - if h.type == RES_XML_END_ELEMENT_TYPE: - self.m_namespaceUri, = unpack(' uint32_t index - self.m_name, = unpack(' always zero - # uint8_t dataType - # uint32_t data - # For now, we ingore these values - size, res0, dataType, data = unpack("= len(self.m_attributes): - log.warning("Invalid attribute index") - - return offset - - def getAttributeCount(self): - """ - Return the number of Attributes for a Tag - or -1 if not in a tag - """ - if self.m_event != START_TAG: - return -1 - - return self.m_attribute_count - - def getAttributeUri(self, index): - """ - Returns the numeric ID for the namespace URI of an attribute - """ - offset = self._get_attribute_offset(index) - uri = self.m_attributes[offset + ATTRIBUTE_IX_NAMESPACE_URI] - - return uri - - def getAttributeNamespace(self, index): - """ - Return the Namespace URI (if any) for the attribute - """ - uri = self.getAttributeUri(index) - - # No Namespace - if uri == 0xFFFFFFFF: - return '' - - return self.sb[uri] - - def getAttributeName(self, index): - """ - Returns the String which represents the attribute name - """ - offset = self._get_attribute_offset(index) - name = self.m_attributes[offset + ATTRIBUTE_IX_NAME] - - res = self.sb[name] - # If the result is a (null) string, we need to look it up. - if not res: - attr = self.m_resourceIDs[name] - if attr in publics.SYSTEM_RESOURCES['attributes']['inverse']: - res = 'android:' + \ - publics.SYSTEM_RESOURCES['attributes']['inverse'][attr] - else: - # Attach the HEX Number, so for multiple missing attributes we do not run - # into problems. - res = 'android:UNKNOWN_SYSTEM_ATTRIBUTE_{:08x}'.format(attr) - - return res - - def getAttributeValueType(self, index): - """ - Return the type of the attribute at the given index - - :param index: index of the attribute - """ - offset = self._get_attribute_offset(index) - return self.m_attributes[offset + ATTRIBUTE_IX_VALUE_TYPE] - - def getAttributeValueData(self, index): - """ - Return the data of the attribute at the given index - - :param index: index of the attribute - """ - offset = self._get_attribute_offset(index) - return self.m_attributes[offset + ATTRIBUTE_IX_VALUE_DATA] - - def getAttributeValue(self, index): - """ - This function is only used to look up strings - All other work is done by - :func:`~androguard.core.bytecodes.axml.format_value` - # FIXME should unite those functions - :param index: index of the attribute - :return: - """ - offset = self._get_attribute_offset(index) - valueType = self.m_attributes[offset + ATTRIBUTE_IX_VALUE_TYPE] - if valueType == TYPE_STRING: - valueString = self.m_attributes[offset + ATTRIBUTE_IX_VALUE_STRING] - return self.sb[valueString] - return '' - - -def format_value(_type, _data, lookup_string=lambda ix: ""): - """ - Format a value based on type and data. - By default, no strings are looked up and "" is returned. - You need to define `lookup_string` in order to actually lookup strings from - the string table. - - :param _type: The numeric type of the value - :param _data: The numeric data of the value - :param lookup_string: A function how to resolve strings from integer IDs - """ - - # Function to prepend android prefix for attributes/references from the - # android library - def fmt_package(x): return "android:" if x >> 24 == 1 else "" - - # Function to represent integers - def fmt_int(x): return (0x7FFFFFFF & x) - \ - 0x80000000 if x > 0x7FFFFFFF else x - - if _type == TYPE_STRING: - return lookup_string(_data) - - elif _type == TYPE_ATTRIBUTE: - return "?{}{:08X}".format(fmt_package(_data), _data) - - elif _type == TYPE_REFERENCE: - return "@{}{:08X}".format(fmt_package(_data), _data) - - elif _type == TYPE_FLOAT: - return "%f" % unpack("=f", pack("=L", _data))[0] - - elif _type == TYPE_INT_HEX: - return "0x%08X" % _data - - elif _type == TYPE_INT_BOOLEAN: - if _data == 0: - return "false" - return "true" - - elif _type == TYPE_DIMENSION: - return "{:f}{}".format(complexToFloat(_data), DIMENSION_UNITS[_data & COMPLEX_UNIT_MASK]) - - elif _type == TYPE_FRACTION: - return "{:f}{}".format(complexToFloat(_data) * 100, FRACTION_UNITS[_data & COMPLEX_UNIT_MASK]) - - elif TYPE_FIRST_COLOR_INT <= _type <= TYPE_LAST_COLOR_INT: - return "#%08X" % _data - - elif TYPE_FIRST_INT <= _type <= TYPE_LAST_INT: - return "%d" % fmt_int(_data) - - return "<0x{:X}, type 0x{:02X}>".format(_data, _type) - - -class AXMLPrinter: - """ - Converter for AXML Files into a lxml ElementTree, which can easily be - converted into XML. - - A Reference Implementation can be found at http://androidxref.com/9.0.0_r3/xref/frameworks/base/tools/aapt/XMLNode.cpp - """ - __charrange = None - __replacement = None - - def __init__(self, raw_buff): - self.axml = AXMLParser(raw_buff) - - self.root = None - self.packerwarning = False - cur = [] - - while self.axml.is_valid(): - _type = next(self.axml) - - if _type == START_TAG: - uri = self._print_namespace(self.axml.namespace) - uri, name = self._fix_name(uri, self.axml.name) - tag = "{}{}".format(uri, name) - - comment = self.axml.comment - if comment: - if self.root is None: - log.warning( - "Can not attach comment with content '{}' without root!".format(comment)) - else: - cur[-1].append(etree.Comment(comment)) - - log.debug("START_TAG: {} (line={})".format( - tag, self.axml.m_lineNumber)) - elem = etree.Element(tag, nsmap=self.axml.nsmap) - - for i in range(self.axml.getAttributeCount()): - uri = self._print_namespace( - self.axml.getAttributeNamespace(i)) - uri, name = self._fix_name( - uri, self.axml.getAttributeName(i)) - value = self._fix_value(self._get_attribute_value(i)) - - log.debug("found an attribute: {}{}='{}'".format( - uri, name, value.encode("utf-8"))) - if "{}{}".format(uri, name) in elem.attrib: - log.warning( - "Duplicate attribute '{}{}'! Will overwrite!".format(uri, name)) - elem.set("{}{}".format(uri, name), value) - - if self.root is None: - self.root = elem - else: - if not cur: - # looks like we lost the root? - log.error( - "No more elements available to attach to! Is the XML malformed?") - break - cur[-1].append(elem) - cur.append(elem) - - if _type == END_TAG: - if not cur: - log.warning( - "Too many END_TAG! No more elements available to attach to!") - - name = self.axml.name - uri = self._print_namespace(self.axml.namespace) - tag = "{}{}".format(uri, name) - if cur[-1].tag != tag: - log.warning("Closing tag '{}' does not match current stack! At line number: {}. Is the XML malformed?".format( - self.axml.name, self.axml.m_lineNumber)) - cur.pop() - if _type == TEXT: - log.debug("TEXT for {}".format(cur[-1])) - cur[-1].text = self.axml.text - if _type == END_DOCUMENT: - # Check if all namespace mappings are closed - if len(self.axml.namespaces) > 0: - log.warning( - "Not all namespace mappings were closed! Malformed AXML?") - break - - def get_buff(self): - """ - Returns the raw XML file without prettification applied. - - :returns: bytes, encoded as UTF-8 - """ - return self.get_xml(pretty=False) - - def get_xml(self, pretty=True): - """ - Get the XML as an UTF-8 string - - :returns: bytes encoded as UTF-8 - """ - return etree.tostring(self.root, encoding="utf-8", pretty_print=pretty) - - def get_xml_obj(self): - """ - Get the XML as an ElementTree object - - :returns: :class:`lxml.etree.Element` - """ - return self.root - - def is_valid(self): - """ - Return the state of the AXMLParser. - If this flag is set to False, the parsing has failed, thus - the resulting XML will not work or will even be empty. - """ - return self.axml.is_valid() - - def is_packed(self): - """ - Returns True if the AXML is likely to be packed - - Packers do some weird stuff and we try to detect it. - Sometimes the files are not packed but simply broken or compiled with - some broken version of a tool. - Some file corruption might also be appear to be a packed file. - - :returns: True if packer detected, False otherwise - """ - return self.packerwarning - - def _get_attribute_value(self, index): - """ - Wrapper function for format_value to resolve the actual value of an attribute in a tag - :param index: index of the current attribute - :return: formatted value - """ - _type = self.axml.getAttributeValueType(index) - _data = self.axml.getAttributeValueData(index) - - return format_value(_type, _data, lambda _: self.axml.getAttributeValue(index)) - - def _fix_name(self, prefix, name): - """ - Apply some fixes to element named and attribute names. - Try to get conform to: - > Like element names, attribute names are case-sensitive and must start with a letter or underscore. - > The rest of the name can contain letters, digits, hyphens, underscores, and periods. - See: https://msdn.microsoft.com/en-us/library/ms256152(v=vs.110).aspx - - This function tries to fix some broken namespace mappings. - In some cases, the namespace prefix is inside the name and not in the prefix field. - Then, the tag name will usually look like 'android:foobar'. - If and only if the namespace prefix is inside the namespace mapping and the actual prefix field is empty, - we will strip the prefix from the attribute name and return the fixed prefix URI instead. - Otherwise replacement rules will be applied. - - The replacement rules work in that way, that all unwanted characters are replaced by underscores. - In other words, all characters except the ones listed above are replaced. - - :param name: Name of the attribute or tag - :param prefix: The existing prefix uri as found in the AXML chunk - :return: a fixed version of prefix and name - :rtype: tuple - """ - if not name[0].isalpha() and name[0] != "_": - log.warning("Invalid start for name '{}'. " - "XML name must start with a letter.".format(name)) - self.packerwarning = True - name = "_{}".format(name) - if name.startswith("android:") and prefix == '' and 'android' in self.axml.nsmap: - # Seems be a common thing... - log.info( - "Name '{}' starts with 'android:' prefix but 'android' is a known prefix. Replacing prefix.".format(name)) - prefix = self._print_namespace(self.axml.nsmap['android']) - name = name[len("android:"):] - # It looks like this is some kind of packer... Not sure though. - self.packerwarning = True - elif ":" in name and prefix == '': - self.packerwarning = True - embedded_prefix, new_name = name.split(":", 1) - if embedded_prefix in self.axml.nsmap: - log.info( - "Prefix '{}' is in namespace mapping, assume that it is a prefix.") - prefix = self._print_namespace( - self.axml.nsmap[embedded_prefix]) - name = new_name - else: - # Print out an extra warning - log.warning("Confused: name contains a unknown namespace prefix: '{}'. " - "This is either a broken AXML file or some attempt to break stuff.".format(name)) - if not re.match(r"^[a-zA-Z0-9._-]*$", name): - log.warning("Name '{}' contains invalid characters!".format(name)) - self.packerwarning = True - name = re.sub(r"[^a-zA-Z0-9._-]", "_", name) - - return prefix, name - - def _fix_value(self, value): - """ - Return a cleaned version of a value - according to the specification: - > Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] - - See https://www.w3.org/TR/xml/#charsets - - :param value: a value to clean - :return: the cleaned value - """ - if not self.__charrange or not self.__replacement: - self.__charrange = re.compile( - '^[\u0020-\uD7FF\u0009\u000A\u000D\uE000-\uFFFD\U00010000-\U0010FFFF]*$') - self.__replacement = re.compile( - '[^\u0020-\uD7FF\u0009\u000A\u000D\uE000-\uFFFD\U00010000-\U0010FFFF]') - - # Reading string until \x00. This is the same as aapt does. - if "\x00" in value: - self.packerwarning = True - log.warning("Null byte found in attribute value at position {}: " - "Value(hex): '{}'".format( - value.find("\x00"), - binascii.hexlify(value.encode("utf-8")))) - value = value[:value.find("\x00")] - - if not self.__charrange.match(value): - log.warning( - "Invalid character in value found. Replacing with '_'.") - self.packerwarning = True - value = self.__replacement.sub('_', value) - return value - - def _print_namespace(self, uri): - if uri != "": - uri = "{{{}}}".format(uri) - return uri - - -ACONFIGURATION_MCC = 0x0001 -ACONFIGURATION_MNC = 0x0002 -ACONFIGURATION_LOCALE = 0x0004 -ACONFIGURATION_TOUCHSCREEN = 0x0008 -ACONFIGURATION_KEYBOARD = 0x0010 -ACONFIGURATION_KEYBOARD_HIDDEN = 0x0020 -ACONFIGURATION_NAVIGATION = 0x0040 -ACONFIGURATION_ORIENTATION = 0x0080 -ACONFIGURATION_DENSITY = 0x0100 -ACONFIGURATION_SCREEN_SIZE = 0x0200 -ACONFIGURATION_VERSION = 0x0400 -ACONFIGURATION_SCREEN_LAYOUT = 0x0800 -ACONFIGURATION_UI_MODE = 0x1000 -ACONFIGURATION_LAYOUTDIR_ANY = 0x00 -ACONFIGURATION_LAYOUTDIR_LTR = 0x01 -ACONFIGURATION_LAYOUTDIR_RTL = 0x02 -ACONFIGURATION_SCREENSIZE_ANY = 0x00 -ACONFIGURATION_SCREENSIZE_SMALL = 0x01 -ACONFIGURATION_SCREENSIZE_NORMAL = 0x02 -ACONFIGURATION_SCREENSIZE_LARGE = 0x03 -ACONFIGURATION_SCREENSIZE_XLARGE = 0x04 -ACONFIGURATION_SCREENLONG_ANY = 0x00 -ACONFIGURATION_SCREENLONG_NO = 0x1 -ACONFIGURATION_SCREENLONG_YES = 0x2 -ACONFIGURATION_TOUCHSCREEN_ANY = 0x0000 -ACONFIGURATION_TOUCHSCREEN_NOTOUCH = 0x0001 -ACONFIGURATION_TOUCHSCREEN_STYLUS = 0x0002 -ACONFIGURATION_TOUCHSCREEN_FINGER = 0x0003 -ACONFIGURATION_DENSITY_DEFAULT = 0 -ACONFIGURATION_DENSITY_LOW = 120 -ACONFIGURATION_DENSITY_MEDIUM = 160 -ACONFIGURATION_DENSITY_TV = 213 -ACONFIGURATION_DENSITY_HIGH = 240 -ACONFIGURATION_DENSITY_XHIGH = 320 -ACONFIGURATION_DENSITY_XXHIGH = 480 -ACONFIGURATION_DENSITY_XXXHIGH = 640 -ACONFIGURATION_DENSITY_ANY = 0xfffe -ACONFIGURATION_DENSITY_NONE = 0xffff -MASK_LAYOUTDIR = 0xC0 -MASK_SCREENSIZE = 0x0f -MASK_SCREENLONG = 0x30 -SHIFT_LAYOUTDIR = 6 -SHIFT_SCREENLONG = 4 -LAYOUTDIR_ANY = ACONFIGURATION_LAYOUTDIR_ANY << SHIFT_LAYOUTDIR -LAYOUTDIR_LTR = ACONFIGURATION_LAYOUTDIR_LTR << SHIFT_LAYOUTDIR -LAYOUTDIR_RTL = ACONFIGURATION_LAYOUTDIR_RTL << SHIFT_LAYOUTDIR -SCREENSIZE_ANY = ACONFIGURATION_SCREENSIZE_ANY -SCREENSIZE_SMALL = ACONFIGURATION_SCREENSIZE_SMALL -SCREENSIZE_NORMAL = ACONFIGURATION_SCREENSIZE_NORMAL -SCREENSIZE_LARGE = ACONFIGURATION_SCREENSIZE_LARGE -SCREENSIZE_XLARGE = ACONFIGURATION_SCREENSIZE_XLARGE -SCREENLONG_ANY = ACONFIGURATION_SCREENLONG_ANY << SHIFT_SCREENLONG -SCREENLONG_NO = ACONFIGURATION_SCREENLONG_NO << SHIFT_SCREENLONG -SCREENLONG_YES = ACONFIGURATION_SCREENLONG_YES << SHIFT_SCREENLONG -DENSITY_DEFAULT = ACONFIGURATION_DENSITY_DEFAULT -DENSITY_LOW = ACONFIGURATION_DENSITY_LOW -DENSITY_MEDIUM = ACONFIGURATION_DENSITY_MEDIUM -DENSITY_TV = ACONFIGURATION_DENSITY_TV -DENSITY_HIGH = ACONFIGURATION_DENSITY_HIGH -DENSITY_XHIGH = ACONFIGURATION_DENSITY_XHIGH -DENSITY_XXHIGH = ACONFIGURATION_DENSITY_XXHIGH -DENSITY_XXXHIGH = ACONFIGURATION_DENSITY_XXXHIGH -DENSITY_ANY = ACONFIGURATION_DENSITY_ANY -DENSITY_NONE = ACONFIGURATION_DENSITY_NONE -TOUCHSCREEN_ANY = ACONFIGURATION_TOUCHSCREEN_ANY -TOUCHSCREEN_NOTOUCH = ACONFIGURATION_TOUCHSCREEN_NOTOUCH -TOUCHSCREEN_STYLUS = ACONFIGURATION_TOUCHSCREEN_STYLUS -TOUCHSCREEN_FINGER = ACONFIGURATION_TOUCHSCREEN_FINGER - - -class ARSCParser: - """ - Parser for resource.arsc files - - The ARSC File is, like the binary XML format, a chunk based format. - Both formats are actually identical but use different chunks in order to store the data. - - The most outer chunk in the ARSC file is a chunk of type RES_TABLE_TYPE. - Inside this chunk is a StringPool and at least one package. - - Each package is a chunk of type RES_TABLE_PACKAGE_TYPE. - It contains again many more chunks. - """ - - def __init__(self, raw_buff): - """ - :param bytes raw_buff: the raw bytes of the file - """ - self.buff = BuffHandle(raw_buff) - - if self.buff.size() < 8 or self.buff.size() > 0xFFFFFFFF: - raise ResParserError( - "Invalid file size {} for a resources.arsc file!".format(self.buff.size())) - - self.analyzed = False - self._resolved_strings = None - self.packages = defaultdict(list) - self.values = {} - self.resource_values = defaultdict(defaultdict) - self.resource_configs = defaultdict(lambda: defaultdict(set)) - self.resource_keys = defaultdict(lambda: defaultdict(defaultdict)) - self.stringpool_main = None - - # First, there is a ResTable_header. - self.header = ARSCHeader(self.buff, expected_type=RES_TABLE_TYPE) - - # More sanity checks... - if self.header.header_size != 12: - log.warning("The ResTable_header has an unexpected header size! Expected 12 bytes, got {}.".format( - self.header.header_size)) - - if self.header.size > self.buff.size(): - raise ResParserError("The file seems to be truncated. Refuse to parse the file! Filesize: {}, declared size: {}".format( - self.buff.size(), self.header.size)) - - if self.header.size < self.buff.size(): - log.warning("The Resource file seems to have data appended to it. Filesize: {}, declared size: {}".format( - self.buff.size(), self.header.size)) - - # The ResTable_header contains the packageCount, i.e. the number of ResTable_package - self.packageCount = unpack(' self.header.end: - # this inner chunk crosses the boundary of the table chunk - log.warning( - "Invalid chunk found! It is larger than the outer chunk: %s", res_header) - break - - if res_header.type == RES_STRING_POOL_TYPE: - # There should be only one StringPool per resource table. - if self.stringpool_main: - log.warning( - "Already found a ResStringPool_header, but there should be only one! Will not parse the Pool again.") - else: - self.stringpool_main = StringBlock(self.buff, res_header) - log.debug("Found the main string pool: %s", - self.stringpool_main) - - elif res_header.type == RES_TABLE_PACKAGE_TYPE: - if len(self.packages) > self.packageCount: - raise ResParserError("Got more packages ({}) than expected ({})".format( - len(self.packages), self.packageCount)) - - current_package = ARSCResTablePackage(self.buff, res_header) - package_name = current_package.get_name() - - # After the Header, we have the resource type symbol table - self.buff.set_idx( - current_package.header.start + current_package.typeStrings) - type_sp_header = ARSCHeader( - self.buff, expected_type=RES_STRING_POOL_TYPE) - mTableStrings = StringBlock(self.buff, type_sp_header) - - # Next, we should have the resource key symbol table - self.buff.set_idx( - current_package.header.start + current_package.keyStrings) - key_sp_header = ARSCHeader( - self.buff, expected_type=RES_STRING_POOL_TYPE) - mKeyStrings = StringBlock(self.buff, key_sp_header) - - # Add them to the dict of read packages - self.packages[package_name].append(current_package) - self.packages[package_name].append(mTableStrings) - self.packages[package_name].append(mKeyStrings) - - pc = PackageContext( - current_package, self.stringpool_main, mTableStrings, mKeyStrings) - log.debug("Constructed a PackageContext: %s", pc) - - # skip to the first header in this table package chunk - # FIXME is this correct? We have already read the first two sections! - # self.buff.set_idx(res_header.start + res_header.header_size) - # this looks more like we want: (???) - # FIXME it looks like that the two string pools we have read might not be concatenated to each other, - # thus jumping to the sum of the sizes might not be correct... - next_idx = res_header.start + res_header.header_size + \ - type_sp_header.size + key_sp_header.size - - if next_idx != self.buff.tell(): - # If this happens, we have a testfile ;) - log.error("This looks like an odd resources.arsc file!") - log.error( - "Please report this error including the file you have parsed!") - log.error("next_idx = {}, current buffer position = {}".format( - next_idx, self.buff.tell())) - log.error( - "Please open a issue at https://github.com/androguard/androguard/issues") - log.error("Thank you!") - - self.buff.set_idx(next_idx) - - # Read all other headers - while self.buff.get_idx() <= res_header.end - ARSCHeader.SIZE: - pkg_chunk_header = ARSCHeader(self.buff) - log.debug("Found a header: {}".format(pkg_chunk_header)) - if pkg_chunk_header.start + pkg_chunk_header.size > res_header.end: - # we are way off the package chunk; bail out - break - - self.packages[package_name].append(pkg_chunk_header) - - if pkg_chunk_header.type == RES_TABLE_TYPE_SPEC_TYPE: - self.packages[package_name].append( - ARSCResTypeSpec(self.buff, pc)) - - elif pkg_chunk_header.type == RES_TABLE_TYPE_TYPE: - # Parse a RES_TABLE_TYPE - # http://androidxref.com/9.0.0_r3/xref/frameworks/base/tools/aapt2/format/binary/BinaryResourceParser.cpp#311 - a_res_type = ARSCResType(self.buff, pc) - self.packages[package_name].append(a_res_type) - self.resource_configs[package_name][a_res_type].add( - a_res_type.config) - - log.debug("Config: {}".format(a_res_type.config)) - - entries = [] - for i in range(0, a_res_type.entryCount): - current_package.mResId = current_package.mResId & 0xffff0000 | i - entries.append((unpack('> 24) & 0xFF), - ((entry_data >> 16) & 0xFF), - ((entry_data >> 8) & 0xFF), - (entry_data & 0xFF)) - ] - - def get_resource_dimen(self, ate): - try: - return [ - ate.get_value(), "{}{}".format( - complexToFloat(ate.key.get_data()), - DIMENSION_UNITS[ate.key.get_data() & COMPLEX_UNIT_MASK]) - ] - except IndexError: - log.debug("Out of range dimension unit index for {}: {}".format( - complexToFloat(ate.key.get_data()), - ate.key.get_data() & COMPLEX_UNIT_MASK)) - return [ate.get_value(), ate.key.get_data()] - - # FIXME - def get_resource_style(self, ate): - return ["", ""] - - def get_packages_names(self): - """ - Retrieve a list of all package names, which are available - in the given resources.arsc. - """ - return list(self.packages.keys()) - - def get_locales(self, package_name): - """ - Retrieve a list of all available locales in a given packagename. - - :param package_name: the package name to get locales of - """ - self._analyse() - return list(self.values[package_name].keys()) - - def get_types(self, package_name, locale='\x00\x00'): - """ - Retrieve a list of all types which are available in the given - package and locale. - - :param package_name: the package name to get types of - :param locale: the locale to get types of (default: '\x00\x00') - """ - self._analyse() - return list(self.values[package_name][locale].keys()) - - def get_public_resources(self, package_name, locale='\x00\x00'): - """ - Get the XML (as string) of all resources of type 'public'. - - The public resources table contains the IDs for each item. - - :param package_name: the package name to get the resources for - :param locale: the locale to get the resources for (default: '\x00\x00') - """ - - self._analyse() - - buff = '\n' - buff += '\n' - - try: - for i in self.values[package_name][locale]["public"]: - buff += '\n'.format( - i[0], i[1], i[2]) - except KeyError: - pass - - buff += '\n' - - return buff.encode('utf-8') - - def get_string_resources(self, package_name, locale='\x00\x00'): - """ - Get the XML (as string) of all resources of type 'string'. - - Read more about string resources: - https://developer.android.com/guide/topics/resources/string-resource.html - - :param package_name: the package name to get the resources for - :param locale: the locale to get the resources for (default: '\x00\x00') - """ - self._analyse() - - buff = '\n' - buff += '\n' - - try: - for i in self.values[package_name][locale]["string"]: - if any(map(i[1].__contains__, '<&>')): - value = '' % i[1] - else: - value = i[1] - buff += '{}\n'.format(i[0], value) - except KeyError: - pass - - buff += '\n' - - return buff.encode('utf-8') - - def get_strings_resources(self): - """ - Get the XML (as string) of all resources of type 'string'. - This is a combined variant, which has all locales and all package names - stored. - """ - self._analyse() - - buff = '\n' - - buff += "\n" - for package_name in self.get_packages_names(): - buff += "\n" % package_name - - for locale in self.get_locales(package_name): - buff += "\n" % repr(locale) - - buff += '\n' - try: - for i in self.values[package_name][locale]["string"]: - buff += '{}\n'.format( - i[0], escape(i[1])) - except KeyError: - pass - - buff += '\n' - buff += '\n' - - buff += "\n" - - buff += "\n" - - return buff.encode('utf-8') - - def get_id_resources(self, package_name, locale='\x00\x00'): - """ - Get the XML (as string) of all resources of type 'id'. - - Read more about ID resources: - https://developer.android.com/guide/topics/resources/more-resources.html#Id - - :param package_name: the package name to get the resources for - :param locale: the locale to get the resources for (default: '\x00\x00') - """ - self._analyse() - - buff = '\n' - buff += '\n' - - try: - for i in self.values[package_name][locale]["id"]: - if len(i) == 1: - buff += '\n' % (i[0]) - else: - buff += '{}\n'.format(i[0], - escape(i[1])) - except KeyError: - pass - - buff += '\n' - - return buff.encode('utf-8') - - def get_bool_resources(self, package_name, locale='\x00\x00'): - """ - Get the XML (as string) of all resources of type 'bool'. - - Read more about bool resources: - https://developer.android.com/guide/topics/resources/more-resources.html#Bool - - :param package_name: the package name to get the resources for - :param locale: the locale to get the resources for (default: '\x00\x00') - """ - self._analyse() - - buff = '\n' - buff += '\n' - - try: - for i in self.values[package_name][locale]["bool"]: - buff += '{}\n'.format(i[0], i[1]) - except KeyError: - pass - - buff += '\n' - - return buff.encode('utf-8') - - def get_integer_resources(self, package_name, locale='\x00\x00'): - """ - Get the XML (as string) of all resources of type 'integer'. - - Read more about integer resources: - https://developer.android.com/guide/topics/resources/more-resources.html#Integer - - :param package_name: the package name to get the resources for - :param locale: the locale to get the resources for (default: '\x00\x00') - """ - self._analyse() - - buff = '\n' - buff += '\n' - - try: - for i in self.values[package_name][locale]["integer"]: - buff += '{}\n'.format(i[0], i[1]) - except KeyError: - pass - - buff += '\n' - - return buff.encode('utf-8') - - def get_color_resources(self, package_name, locale='\x00\x00'): - """ - Get the XML (as string) of all resources of type 'color'. - - Read more about color resources: - https://developer.android.com/guide/topics/resources/more-resources.html#Color - - :param package_name: the package name to get the resources for - :param locale: the locale to get the resources for (default: '\x00\x00') - """ - self._analyse() - - buff = '\n' - buff += '\n' - - try: - for i in self.values[package_name][locale]["color"]: - buff += '{}\n'.format(i[0], i[1]) - except KeyError: - pass - - buff += '\n' - - return buff.encode('utf-8') - - def get_dimen_resources(self, package_name, locale='\x00\x00'): - """ - Get the XML (as string) of all resources of type 'dimen'. - - Read more about Dimension resources: - https://developer.android.com/guide/topics/resources/more-resources.html#Dimension - - :param package_name: the package name to get the resources for - :param locale: the locale to get the resources for (default: '\x00\x00') - """ - self._analyse() - - buff = '\n' - buff += '\n' - - try: - for i in self.values[package_name][locale]["dimen"]: - buff += '{}\n'.format(i[0], i[1]) - except KeyError: - pass - - buff += '\n' - - return buff.encode('utf-8') - - def get_id(self, package_name, rid, locale='\x00\x00'): - """ - Returns the tuple (resource_type, resource_name, resource_id) - for the given resource_id. - - :param package_name: package name to query - :param rid: the resource_id - :param locale: specific locale - :return: tuple of (resource_type, resource_name, resource_id) - """ - self._analyse() - - try: - for i in self.values[package_name][locale]["public"]: - if i[2] == rid: - return i - except KeyError: - pass - return None, None, None - - class ResourceResolver: - """ - Resolves resources by ID and configuration. - This resolver deals with complex resources as well as with references. - """ - - def __init__(self, android_resources, config=None): - """ - :param ARSCParser android_resources: A resource parser - :param ARSCResTableConfig config: The desired configuration or None to resolve all. - """ - self.resources = android_resources - self.wanted_config = config - - def resolve(self, res_id): - """ - the given ID into the Resource and returns a list of matching resources. - - :param int res_id: numerical ID of the resource - :return: a list of tuples of (ARSCResTableConfig, str) - """ - result = [] - self._resolve_into_result(result, res_id, self.wanted_config) - return result - - def _resolve_into_result(self, result, res_id, config): - # First: Get all candidates - configs = self.resources.get_res_configs(res_id, config) - - for config, ate in configs: - # deconstruct them and check if more candidates are generated - self.put_ate_value(result, ate, config) - - def put_ate_value(self, result, ate, config): - """ - Put a ResTableEntry into the list of results - :param list result: results array - :param ARSCResTableEntry ate: - :param ARSCResTableConfig config: - :return: - """ - if ate.is_complex(): - complex_array = [] - result.append((config, complex_array)) - for _, item in ate.item.items: - self.put_item_value(complex_array, item, - config, ate, complex_=True) - else: - self.put_item_value(result, ate.key, config, - ate, complex_=False) - - def put_item_value(self, result, item, config, parent, complex_): - """ - Put the tuple (ARSCResTableConfig, resolved string) into the result set - - :param list result: the result set - :param ARSCResStringPoolRef item: - :param ARSCResTableConfig config: - :param ARSCResTableEntry parent: the originating entry - :param bool complex_: True if the originating :class:`ARSCResTableEntry` was complex - :return: - """ - if item.is_reference(): - res_id = item.get_data() - if res_id: - # Infinite loop detection: - # TODO should this stay here or should be detect the loop much earlier? - if res_id == parent.mResId: - log.warning( - "Infinite loop detected at resource item {}. It references itself!".format(parent)) - return - - self._resolve_into_result( - result, item.get_data(), self.wanted_config) - else: - if complex_: - result.append(item.format_value()) - else: - result.append((config, item.format_value())) - - def get_resolved_res_configs(self, rid, config=None): - """ - Return a list of resolved resource IDs with their corresponding configuration. - It has a similar return type as :meth:`get_res_configs` but also handles complex entries - and references. - Also instead of returning :class:`ARSCResTableEntry` in the tuple, the actual values are resolved. - - This is the preferred way of resolving resource IDs to their resources. - - :param int rid: the numerical ID of the resource - :param ARSCTableResConfig config: the desired configuration or None to retrieve all - :return: A list of tuples of (ARSCResTableConfig, str) - """ - resolver = ARSCParser.ResourceResolver(self, config) - return resolver.resolve(rid) - - def get_resolved_strings(self): - self._analyse() - if self._resolved_strings: - return self._resolved_strings - - r = {} - for package_name in self.get_packages_names(): - r[package_name] = {} - k = {} - - for locale in self.values[package_name]: - v_locale = locale - if v_locale == '\x00\x00': - v_locale = 'DEFAULT' - - r[package_name][v_locale] = {} - - try: - for i in self.values[package_name][locale]["public"]: - if i[0] == 'string': - r[package_name][v_locale][i[2]] = None - k[i[1]] = i[2] - except KeyError: - pass - - try: - for i in self.values[package_name][locale]["string"]: - if i[0] in k: - r[package_name][v_locale][k[i[0]]] = i[1] - except KeyError: - pass - - self._resolved_strings = r - return r - - def get_res_configs(self, rid, config=None, fallback=True): - """ - Return the resources found with the ID `rid` and select - the right one based on the configuration, or return all if no configuration was set. - - But we try to be generous here and at least try to resolve something: - This method uses a fallback to return at least one resource (the first one in the list) - if more than one items are found and the default config is used and no default entry could be found. - - This is usually a bad sign (i.e. the developer did not follow the android documentation: - https://developer.android.com/guide/topics/resources/localization.html#failing2) - In practise an app might just be designed to run on a single locale and thus only has those locales set. - - You can disable this fallback behaviour, to just return exactly the given result. - - :param rid: resource id as int - :param config: a config to resolve from, or None to get all results - :param fallback: Enable the fallback for resolving default configuration (default: True) - :return: a list of ARSCResTableConfig: ARSCResTableEntry - """ - self._analyse() - - if not rid: - raise ValueError("'rid' should be set") - if not isinstance(rid, int): - raise ValueError("'rid' must be an int") - - if rid not in self.resource_values: - log.warning( - "The requested rid '0x{:08x}' could not be found in the list of resources.".format(rid)) - return [] - - res_options = self.resource_values[rid] - if len(res_options) > 1 and config: - if config in res_options: - return [(config, res_options[config])] - elif fallback and config == ARSCResTableConfig.default_config(): - log.warning( - "No default resource config could be found for the given rid '0x{:08x}', using fallback!".format(rid)) - return [list(self.resource_values[rid].items())[0]] - else: - return [] - else: - return list(res_options.items()) - - def get_string(self, package_name, name, locale='\x00\x00'): - self._analyse() - - try: - for i in self.values[package_name][locale]["string"]: - if i[0] == name: - return i - except KeyError: - return None - - def get_res_id_by_key(self, package_name, resource_type, key): - try: - return self.resource_keys[package_name][resource_type][key] - except KeyError: - return None - - def get_items(self, package_name): - self._analyse() - return self.packages[package_name] - - def get_type_configs(self, package_name, type_name=None): - if package_name is None: - package_name = self.get_packages_names()[0] - result = collections.defaultdict(list) - - for res_type, configs in list(self.resource_configs[package_name].items()): - if res_type.get_package_name() == package_name and ( - type_name is None or res_type.get_type() == type_name): - result[res_type.get_type()].extend(configs) - - return result - - @staticmethod - def parse_id(name): - """ - Resolves an id from a binary XML file in the form "@[package:]DEADBEEF" - and returns a tuple of package name and resource id. - If no package name was given, i.e. the ID has the form "@DEADBEEF", - the package name is set to None. - - Raises a ValueError if the id is malformed. - - :param name: the string of the resource, as in the binary XML file - :return: a tuple of (resource_id, package_name). - """ - - if not name.startswith('@'): - raise ValueError( - "Not a valid resource ID, must start with @: '{}'".format(name)) - - # remove @ - name = name[1:] - - package = None - if ':' in name: - package, res_id = name.split(':', 1) - else: - res_id = name - - if len(res_id) != 8: - raise ValueError( - "Numerical ID is not 8 characters long: '{}'".format(res_id)) - - try: - return int(res_id, 16), package - except ValueError: - raise ValueError("ID is not a hex ID: '{}'".format(res_id)) - - def get_resource_xml_name(self, r_id, package=None): - """ - Returns the XML name for a resource, including the package name if package is None. - A full name might look like `@com.example:string/foobar` - Otherwise the name is only looked up in the specified package and is returned without - the package name. - The same example from about without the package name will read as `@string/foobar`. - - If the ID could not be found, `None` is returned. - - A description of the XML name can be found here: - https://developer.android.com/guide/topics/resources/providing-resources#ResourcesFromXml - - :param r_id: numerical ID if the resource - :param package: package name - :return: XML name identifier - """ - if package: - resource, name, i_id = self.get_id(package, r_id) - if not i_id: - return None - return "@{}/{}".format(resource, name) - else: - for p in self.get_packages_names(): - r, n, i_id = self.get_id(p, r_id) - if i_id: - # found the resource in this package - package = p - resource = r - name = n - break - if not package: - return None - else: - return "@{}:{}/{}".format(package, resource, name) - - -class PackageContext: - def __init__(self, current_package, stringpool_main, mTableStrings, mKeyStrings): - """ - :param ARSCResTablePackage current_package: - :param StringBlock stringpool_main: - :param StringBlock mTableStrings: - :param StringBlock mKeyStrings: - """ - self.stringpool_main = stringpool_main - self.mTableStrings = mTableStrings - self.mKeyStrings = mKeyStrings - self.current_package = current_package - - def get_mResId(self): - return self.current_package.mResId - - def set_mResId(self, mResId): - self.current_package.mResId = mResId - - def get_package_name(self): - return self.current_package.get_name() - - def __repr__(self): - return "".format(self.current_package, - self.stringpool_main, - self.mTableStrings, - self.mKeyStrings) - - -class ARSCHeader: - """ - Object which contains a Resource Chunk. - This is an implementation of the `ResChunk_header`. - - It will throw an :class:`ResParserError` if the header could not be read successfully. - - It is not checked if the data is outside the buffer size nor if the current - chunk fits into the parent chunk (if any)! - - The parameter `expected_type` can be used to immediately check the header for the type or raise a :class:`ResParserError`. - This is useful if you know what type of chunk must follow. - - See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#196 - :raises: ResParserError - """ - - # This is the minimal size such a header must have. There might be other header data too! - SIZE = 2 + 2 + 4 - - def __init__(self, buff, expected_type=None): - """ - :param androguard.core.bytecode.BuffHandle buff: the buffer set to the position where the header starts. - :param int expected_type: the type of the header which is expected. - """ - self.start = buff.get_idx() - # Make sure we do not read over the buffer: - if buff.size() < self.start + self.SIZE: - raise ResParserError( - "Can not read over the buffer size! Offset={}".format(self.start)) - - self._type, self._header_size, self._size = unpack( - '".format(self.start, - self.type, - self.header_size, - self.size) - - -class ARSCResTablePackage: - """ - A `ResTable_package` - - See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#861 - """ - - def __init__(self, buff, header): - self.header = header - self.start = buff.get_idx() - self.id = unpack(' read 256 bytes - # TODO why not read a null terminated string in buffer (like the meth name suggests) instead of parsing it later in get_name()? - self.name = buff.readNullString(256) - self.typeStrings = unpack('" % ( - self.start, - self.id, - self.flags, - self.entryCount, - self.entriesStart, - self.mResId, - "table:" + self.parent.mTableStrings.getString(self.id - 1) - ) - - -class ARSCResTableConfig: - """ - ARSCResTableConfig contains the configuration for specific resource selection. - This is used on the device to determine which resources should be loaded - based on different properties of the device like locale or displaysize. - - See the definition of `ResTable_config` in - http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#911 - """ - @classmethod - def default_config(cls): - if not hasattr(cls, 'DEFAULT'): - cls.DEFAULT = ARSCResTableConfig(None) - return cls.DEFAULT - - def __init__(self, buff=None, **kwargs): - if buff is not None: - self.start = buff.get_idx() - - # uint32_t - self.size = unpack('= 32: - # struct of - # uint8_t screenLayout - # uint8_t uiMode - # uint16_t smallestScreenWidthDp - self.screenConfig, = unpack('= 36: - # struct of - # uint16_t screenWidthDp - # uint16_t screenHeightDp - self.screenSizeDp, = unpack('= 40: - # struct of - # uint8_t screenLayout2 - # uint8_t colorMode - # uint16_t screenConfigPad2 - self.screenConfig2, = unpack(" 0: - log.debug("Skipping padding bytes!") - self.padding = buff.read(self.exceedingSize) - - else: - self.start = 0 - self.size = 0 - self.imsi = \ - ((kwargs.pop('mcc', 0) & 0xffff) << 0) + \ - ((kwargs.pop('mnc', 0) & 0xffff) << 16) - - self.locale = 0 - for char_ix, char in kwargs.pop('locale', "")[0:4]: - self.locale += (ord(char) << (char_ix * 8)) - - self.screenType = \ - ((kwargs.pop('orientation', 0) & 0xff) << 0) + \ - ((kwargs.pop('touchscreen', 0) & 0xff) << 8) + \ - ((kwargs.pop('density', 0) & 0xffff) << 16) - - self.input = \ - ((kwargs.pop('keyboard', 0) & 0xff) << 0) + \ - ((kwargs.pop('navigation', 0) & 0xff) << 8) + \ - ((kwargs.pop('inputFlags', 0) & 0xff) << 16) + \ - ((kwargs.pop('inputPad0', 0) & 0xff) << 24) - - self.screenSize = \ - ((kwargs.pop('screenWidth', 0) & 0xffff) << 0) + \ - ((kwargs.pop('screenHeight', 0) & 0xffff) << 16) - - self.version = \ - ((kwargs.pop('sdkVersion', 0) & 0xffff) << 0) + \ - ((kwargs.pop('minorVersion', 0) & 0xffff) << 16) - - self.screenConfig = \ - ((kwargs.pop('screenLayout', 0) & 0xff) << 0) + \ - ((kwargs.pop('uiMode', 0) & 0xff) << 8) + \ - ((kwargs.pop('smallestScreenWidthDp', 0) & 0xffff) << 16) - - self.screenSizeDp = \ - ((kwargs.pop('screenWidthDp', 0) & 0xffff) << 0) + \ - ((kwargs.pop('screenHeightDp', 0) & 0xffff) << 16) - - # TODO add this some day... - self.screenConfig2 = 0 - - self.exceedingSize = 0 - - def _unpack_language_or_region(self, char_in, char_base): - char_out = "" - if char_in[0] & 0x80: - first = char_in[1] & 0x1f - second = ((char_in[1] & 0xe0) >> 5) + ((char_in[0] & 0x03) << 3) - third = (char_in[0] & 0x7c) >> 2 - char_out += chr(first + char_base) - char_out += chr(second + char_base) - char_out += chr(third + char_base) - else: - if char_in[0]: - char_out += chr(char_in[0]) - if char_in[1]: - char_out += chr(char_in[1]) - return char_out - - def get_language_and_region(self): - """ - Returns the combined language+region string or \x00\x00 for the default locale - :return: - """ - if self.locale != 0: - _language = self._unpack_language_or_region( - [self.locale & 0xff, (self.locale & 0xff00) >> 8, ], ord('a')) - _region = self._unpack_language_or_region( - [(self.locale & 0xff0000) >> 16, (self.locale & 0xff000000) >> 24, ], ord('0')) - return (_language + "-r" + _region) if _region else _language - return "\x00\x00" - - def get_config_name_friendly(self): - """ - Here for legacy reasons. - - use :meth:`~get_qualifier` instead. - """ - return self.get_qualifier() - - def get_qualifier(self): - """ - Return resource name qualifier for the current configuration. - for example - * `ldpi-v4` - * `hdpi-v4` - - All possible qualifiers are listed in table 2 of https://developer.android.com/guide/topics/resources/providing-resources - - ..todo:: This name might not have all properties set! Therefore returned values might not reflect the true qualifier name! - :return: str - """ - res = [] - - mcc = self.imsi & 0xFFFF - mnc = (self.imsi & 0xFFFF0000) >> 16 - if mcc != 0: - res.append("mcc%d" % mcc) - if mnc != 0: - res.append("mnc%d" % mnc) - - if self.locale != 0: - res.append(self.get_language_and_region()) - - screenLayout = self.screenConfig & 0xff - if (screenLayout & MASK_LAYOUTDIR) != 0: - if screenLayout & MASK_LAYOUTDIR == LAYOUTDIR_LTR: - res.append("ldltr") - elif screenLayout & MASK_LAYOUTDIR == LAYOUTDIR_RTL: - res.append("ldrtl") - else: - res.append("layoutDir_%d" % (screenLayout & MASK_LAYOUTDIR)) - - smallestScreenWidthDp = (self.screenConfig & 0xFFFF0000) >> 16 - if smallestScreenWidthDp != 0: - res.append("sw%ddp" % smallestScreenWidthDp) - - screenWidthDp = self.screenSizeDp & 0xFFFF - screenHeightDp = (self.screenSizeDp & 0xFFFF0000) >> 16 - if screenWidthDp != 0: - res.append("w%ddp" % screenWidthDp) - if screenHeightDp != 0: - res.append("h%ddp" % screenHeightDp) - - if (screenLayout & MASK_SCREENSIZE) != SCREENSIZE_ANY: - if screenLayout & MASK_SCREENSIZE == SCREENSIZE_SMALL: - res.append("small") - elif screenLayout & MASK_SCREENSIZE == SCREENSIZE_NORMAL: - res.append("normal") - elif screenLayout & MASK_SCREENSIZE == SCREENSIZE_LARGE: - res.append("large") - elif screenLayout & MASK_SCREENSIZE == SCREENSIZE_XLARGE: - res.append("xlarge") - else: - res.append("screenLayoutSize_%d" % - (screenLayout & MASK_SCREENSIZE)) - if (screenLayout & MASK_SCREENLONG) != 0: - if screenLayout & MASK_SCREENLONG == SCREENLONG_NO: - res.append("notlong") - elif screenLayout & MASK_SCREENLONG == SCREENLONG_YES: - res.append("long") - else: - res.append("screenLayoutLong_%d" % - (screenLayout & MASK_SCREENLONG)) - - density = (self.screenType & 0xffff0000) >> 16 - if density != DENSITY_DEFAULT: - if density == DENSITY_LOW: - res.append("ldpi") - elif density == DENSITY_MEDIUM: - res.append("mdpi") - elif density == DENSITY_TV: - res.append("tvdpi") - elif density == DENSITY_HIGH: - res.append("hdpi") - elif density == DENSITY_XHIGH: - res.append("xhdpi") - elif density == DENSITY_XXHIGH: - res.append("xxhdpi") - elif density == DENSITY_XXXHIGH: - res.append("xxxhdpi") - elif density == DENSITY_NONE: - res.append("nodpi") - elif density == DENSITY_ANY: - res.append("anydpi") - else: - res.append("%ddpi" % (density)) - - touchscreen = (self.screenType & 0xff00) >> 8 - if touchscreen != TOUCHSCREEN_ANY: - if touchscreen == TOUCHSCREEN_NOTOUCH: - res.append("notouch") - elif touchscreen == TOUCHSCREEN_FINGER: - res.append("finger") - elif touchscreen == TOUCHSCREEN_STYLUS: - res.append("stylus") - else: - res.append("touchscreen_%d" % touchscreen) - - screenSize = self.screenSize - if screenSize != 0: - screenWidth = self.screenSize & 0xffff - screenHeight = (self.screenSize & 0xffff0000) >> 16 - res.append("%dx%d" % (screenWidth, screenHeight)) - - version = self.version - if version != 0: - sdkVersion = self.version & 0xffff - minorVersion = (self.version & 0xffff0000) >> 16 - res.append("v%d" % sdkVersion) - if minorVersion != 0: - res.append(".%d" % minorVersion) - - return "-".join(res) - - def get_language(self): - x = self.locale & 0x0000ffff - return chr(x & 0x00ff) + chr((x & 0xff00) >> 8) - - def get_country(self): - x = (self.locale & 0xffff0000) >> 16 - return chr(x & 0x00ff) + chr((x & 0xff00) >> 8) - - def get_density(self): - x = ((self.screenType >> 16) & 0xffff) - return x - - def is_default(self): - """ - Test if this is a default resource, which matches all - - This is indicated that all fields are zero. - :return: True if default, False otherwise - """ - return all(map(lambda x: x == 0, self._get_tuple())) - - def _get_tuple(self): - return ( - self.imsi, - self.locale, - self.screenType, - self.input, - self.screenSize, - self.version, - self.screenConfig, - self.screenSizeDp, - self.screenConfig2, - ) - - def __hash__(self): - return hash(self._get_tuple()) - - def __eq__(self, other): - return self._get_tuple() == other._get_tuple() - - def __repr__(self): - return "".format(self.get_qualifier(), repr(self._get_tuple())) - - -class ARSCResTableEntry: - """ - A `ResTable_entry`. - - See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#1458 - """ - # If set, this is a complex entry, holding a set of name/value - # mappings. It is followed by an array of ResTable_map structures. - FLAG_COMPLEX = 1 - - # If set, this resource has been declared public, so libraries - # are allowed to reference it. - FLAG_PUBLIC = 2 - - # If set, this is a weak resource and may be overriden by strong - # resources of the same name/type. This is only useful during - # linking with other resource tables. - FLAG_WEAK = 4 - - def __init__(self, buff, mResId, parent=None): - self.start = buff.get_idx() - self.mResId = mResId - self.parent = parent - - self.size = unpack('".format( - self.start, - self.mResId, - self.size, - self.flags, - self.index, - self.item if self.is_complex() else self.key) - - -class ARSCComplex: - """ - This is actually a `ResTable_map_entry` - - It contains a set of {name: value} mappings, which are of type `ResTable_map`. - A `ResTable_map` contains two items: `ResTable_ref` and `Res_value`. - - See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#1485 for `ResTable_map_entry` - and http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#1498 for `ResTable_map` - """ - - def __init__(self, buff, parent=None): - self.start = buff.get_idx() - self.parent = parent - - self.id_parent = unpack('".format(self.start, self.id_parent, self.count) - - -class ARSCResStringPoolRef: - """ - This is actually a `Res_value` - It holds information about the stored resource value - - See: http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#262 - """ - - def __init__(self, buff, parent=None): - self.start = buff.get_idx() - self.parent = parent - - self.size, = unpack("".format( - self.start, - self.size, - TYPE_TABLE.get(self.data_type, "0x%x" % self.data_type), - self.data) - - -def get_arsc_info(arscobj): - """ - Return a string containing all resources packages ordered by packagename, locale and type. - - :param arscobj: :class:`~ARSCParser` - :return: a string - """ - buff = "" - for package in arscobj.get_packages_names(): - buff += package + ":\n" - for locale in arscobj.get_locales(package): - buff += "\t" + repr(locale) + ":\n" - for ttype in arscobj.get_types(package, locale): - buff += "\t\t" + ttype + ":\n" - try: - tmp_buff = getattr(arscobj, "get_" + ttype + "_resources")( - package, locale).decode("utf-8", 'replace').split("\n") - for i in tmp_buff: - buff += "\t\t\t" + i + "\n" - except AttributeError: - pass - return buff - - -class StringBlock: - """ - StringBlock is a CHUNK inside an AXML File: `ResStringPool_header` - It contains all strings, which are used by referecing to ID's - - See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#436 - """ - - def __init__(self, buff, header): - """ - :param buff: buffer which holds the string block - :param header: a instance of :class:`~ARSCHeader` - """ - self._cache = {} - self.header = header - # We already read the header (which was chunk_type and chunk_size - # Now, we read the string_count: - self.stringCount = unpack(' 0: - log.info("Styles Offset given, but styleCount is zero. " - "This is not a problem but could indicate packers.") - - self.m_stringOffsets = [] - self.m_styleOffsets = [] - self.m_charbuff = "" - self.m_styles = [] - - # Next, there is a list of string following. - # This is only a list of offsets (4 byte each) - for i in range(self.stringCount): - self.m_stringOffsets.append(unpack('".format(self.stringCount, self.styleCount, self.m_isUTF8) - - def __getitem__(self, idx): - """ - Returns the string at the index in the string table - """ - return self.getString(idx) - - def __len__(self): - """ - Get the number of strings stored in this table - """ - return self.stringCount - - def __iter__(self): - """ - Iterable over all strings - """ - for i in range(self.stringCount): - yield self.getString(i) - - def getString(self, idx): - """ - Return the string at the index in the string table - - :param idx: index in the string table - :return: str - """ - if idx in self._cache: - return self._cache[idx] - - if idx < 0 or not self.m_stringOffsets or idx > self.stringCount: - return "" - - offset = self.m_stringOffsets[idx] - - if self.m_isUTF8: - self._cache[idx] = self._decode8(offset) - else: - self._cache[idx] = self._decode16(offset) - - return self._cache[idx] - - def getStyle(self, idx): - """ - Return the style associated with the index - - :param idx: index of the style - :return: - """ - return self.m_styles[idx] - - def _decode8(self, offset): - """ - Decode an UTF-8 String at the given offset - - :param offset: offset of the string inside the data - :return: str - """ - # UTF-8 Strings contain two lengths, as they might differ: - # 1) the UTF-16 length - str_len, skip = self._decode_length(offset, 1) - offset += skip - - # 2) the utf-8 string length - encoded_bytes, skip = self._decode_length(offset, 1) - offset += skip - - data = self.m_charbuff[offset: offset + encoded_bytes] - - if self.m_charbuff[offset + encoded_bytes] != 0: - raise ResParserError( - "UTF-8 String is not null terminated! At offset={}".format(offset)) - - return self._decode_bytes(data, 'utf-8', str_len) - - def _decode16(self, offset): - """ - Decode an UTF-16 String at the given offset - - :param offset: offset of the string inside the data - :return: str - """ - str_len, skip = self._decode_length(offset, 2) - offset += skip - - # The len is the string len in utf-16 units - encoded_bytes = str_len * 2 - - data = self.m_charbuff[offset: offset + encoded_bytes] - - if self.m_charbuff[offset + encoded_bytes:offset + encoded_bytes + 2] != b"\x00\x00": - raise ResParserError( - "UTF-16 String is not null terminated! At offset={}".format(offset)) - - return self._decode_bytes(data, 'utf-16', str_len) - - @staticmethod - def _decode_bytes(data, encoding, str_len): - """ - Generic decoding with length check. - The string is decoded from bytes with the given encoding, then the length - of the string is checked. - The string is decoded using the "replace" method. - - :param data: bytes - :param encoding: encoding name ("utf-8" or "utf-16") - :param str_len: length of the decoded string - :return: str - """ - string = data.decode(encoding, 'replace') - if len(string) != str_len: - log.warning("invalid decoded string length") - return string - - def _decode_length(self, offset, sizeof_char): - """ - Generic Length Decoding at offset of string - - The method works for both 8 and 16 bit Strings. - Length checks are enforced: - * 8 bit strings: maximum of 0x7FFF bytes (See - http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/ResourceTypes.cpp#692) - * 16 bit strings: maximum of 0x7FFFFFF bytes (See - http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/ResourceTypes.cpp#670) - - :param offset: offset into the string data section of the beginning of - the string - :param sizeof_char: number of bytes per char (1 = 8bit, 2 = 16bit) - :returns: tuple of (length, read bytes) - """ - sizeof_2chars = sizeof_char << 1 - fmt = "<2{}".format('B' if sizeof_char == 1 else 'H') - highbit = 0x80 << (8 * (sizeof_char - 1)) - - length1, length2 = unpack( - fmt, self.m_charbuff[offset:(offset + sizeof_2chars)]) - - if (length1 & highbit) != 0: - length = ((length1 & ~highbit) << (8 * sizeof_char)) | length2 - size = sizeof_2chars - else: - length = length1 - size = sizeof_char - - # These are true asserts, as the size should never be less than the values - if sizeof_char == 1: - assert length <= 0x7FFF, "length of UTF-8 string is too large! At offset={}".format( - offset) - else: - assert length <= 0x7FFFFFFF, "length of UTF-16 string is too large! At offset={}".format( - offset) - - return length, size - - def show(self): - """ - Print some information on stdout about the string table - """ - print("StringBlock(stringsCount=0x%x, " - "stringsOffset=0x%x, " - "stylesCount=0x%x, " - "stylesOffset=0x%x, " - "flags=0x%x" - ")" % (self.stringCount, - self.stringsOffset, - self.styleCount, - self.stylesOffset, - self.flags)) - - if self.stringCount > 0: - print() - print("String Table: ") - for i, s in enumerate(self): - print("{:08d} {}".format(i, repr(s))) - - if self.styleCount > 0: - print() - print("Styles Table: ") - for i in range(self.styleCount): - print("{:08d} {}".format(i, repr(self.getStyle(i)))) - - -# --------------------------------- - -class AXMLParser: - """ - AXMLParser reads through all chunks in the AXML file - and implements a state machine to return information about - the current chunk, which can then be read by :class:`~AXMLPrinter`. - - An AXML file is a file which contains multiple chunks of data, defined - by the `ResChunk_header`. - There is no real file magic but as the size of the first header is fixed - and the `type` of the `ResChunk_header` is set to `RES_XML_TYPE`, a file - will usually start with `0x03000800`. - But there are several examples where the `type` is set to something - else, probably in order to fool parsers. - - Typically the AXMLParser is used in a loop which terminates if `m_event` is set to `END_DOCUMENT`. - You can use the `next()` function to get the next chunk. - Note that not all chunk types are yielded from the iterator! Some chunks are processed in - the AXMLParser only. - The parser will set `is_valid()` to False if it parses something not valid. - Messages what is wrong are logged. - - See http://androidxref.com/9.0.0_r3/xref/frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h#563 - """ - - def __init__(self, raw_buff): - self._reset() - - self._valid = True - self.axml_tampered = False - self.buff = BuffHandle(raw_buff) - - # Minimum is a single ARSCHeader, which would be a strange edge case... - if self.buff.size() < 8: - log.error("Filesize is too small to be a valid AXML file! Filesize: {}".format( - self.buff.size())) - self._valid = False - return - - # This would be even stranger, if an AXML file is larger than 4GB... - # But this is not possible as the maximum chunk size is a unsigned 4 byte int. - if self.buff.size() > 0xFFFFFFFF: - log.error("Filesize is too large to be a valid AXML file! Filesize: {}".format( - self.buff.size())) - self._valid = False - return - - try: - axml_header = ARSCHeader(self.buff) - except ResParserError as e: - log.error("Error parsing first resource header: %s", e) - self._valid = False - return - - self.filesize = axml_header.size - - if axml_header.header_size == 28024: - # Can be a common error: the file is not an AXML but a plain XML - # The file will then usually start with ' self.buff.size(): - log.error("This does not look like an AXML file. Declared filesize does not match real size: {} vs {}".format( - self.filesize, self.buff.size())) - self._valid = False - return - - if self.filesize < self.buff.size(): - # The file can still be parsed up to the point where the chunk should end. - self.axml_tampered = True - log.warning("Declared filesize ({}) is smaller than total file size ({}). " - "Was something appended to the file? Trying to parse it anyways.".format(self.filesize, self.buff.size())) - - # Not that severe of an error, we have plenty files where this is not - # set correctly - if axml_header.type != RES_XML_TYPE: - self.axml_tampered = True - log.warning("AXML file has an unusual resource type! " - "Malware likes to to such stuff to anti androguard! " - "But we try to parse it anyways. Resource Type: 0x{:04x}".format(axml_header.type)) - - # Now we parse the STRING POOL - try: - header = ARSCHeader(self.buff, expected_type=RES_STRING_POOL_TYPE) - except ResParserError as e: - log.error("Error parsing resource header of string pool: %s", e) - self._valid = False - return - - if header.header_size != 0x1C: - log.error("This does not look like an AXML file. String chunk header size does not equal 28! header size = {}".format( - header.header_size)) - self._valid = False - return - - self.sb = StringBlock(self.buff, header) - - # Stores resource ID mappings, if any - self.m_resourceIDs = [] - - # Store a list of prefix/uri mappings encountered - self.namespaces = [] - - def is_valid(self): - """ - Get the state of the AXMLPrinter. - if an error happend somewhere in the process of parsing the file, - this flag is set to False. - """ - return self._valid - - def _reset(self): - self.m_event = -1 - self.m_lineNumber = -1 - self.m_name = -1 - self.m_namespaceUri = -1 - self.m_attributes = [] - self.m_idAttribute = -1 - self.m_classAttribute = -1 - self.m_styleAttribute = -1 - - def __next__(self): - self._do_next() - return self.m_event - - def _do_next(self): - if self.m_event == END_DOCUMENT: - return - - self._reset() - while self._valid: - # Stop at the declared filesize or at the end of the file - if self.buff.end() or self.buff.get_idx() == self.filesize: - self.m_event = END_DOCUMENT - break - - # Again, we read an ARSCHeader - try: - h = ARSCHeader(self.buff) - except ResParserError as e: - log.error("Error parsing resource header: %s", e) - self._valid = False - return - - # Special chunk: Resource Map. This chunk might be contained inside - # the file, after the string pool. - if h.type == RES_XML_RESOURCE_MAP_TYPE: - log.debug("AXML contains a RESOURCE MAP") - # Check size: < 8 bytes mean that the chunk is not complete - # Should be aligned to 4 bytes. - if h.size < 8 or (h.size % 4) != 0: - log.error("Invalid chunk size in chunk XML_RESOURCE_MAP") - self._valid = False - return - - for i in range((h.size - h.header_size) // 4): - self.m_resourceIDs.append( - unpack(' RES_XML_LAST_CHUNK_TYPE: - # h.size is the size of the whole chunk including the header. - # We read already 8 bytes of the header, thus we need to - # subtract them. - log.error("Not a XML resource chunk type: 0x{:04x}. Skipping {} bytes".format( - h.type, h.size)) - self.buff.set_idx(h.end) - continue - - # Check that we read a correct header - if h.header_size != 0x10: - log.error("XML Resource Type Chunk header size does not match 16! " - "At chunk type 0x{:04x}, declared header size={}, chunk size={}".format(h.type, h.header_size, h.size)) - self._valid = False - return - - # Line Number of the source file, only used as meta information - self.m_lineNumber, = unpack(' uri {}: '{}'".format( - prefix, s_prefix, uri, s_uri)) - - if s_uri == '': - log.warning("Namespace prefix '{}' resolves to empty URI. " - "This might be a packer.".format(s_prefix)) - - if (prefix, uri) in self.namespaces: - log.info("Namespace mapping ({}, {}) already seen! " - "This is usually not a problem but could indicate packers or broken AXML compilers.".format(prefix, uri)) - self.namespaces.append((prefix, uri)) - - # We can continue with the next chunk, as we store the namespace - # mappings for each tag - continue - - if h.type == RES_XML_END_NAMESPACE_TYPE: - # END_PREFIX contains again prefix and uri field - prefix, = unpack('> 16) - 1 - self.m_attribute_count = attributeCount & 0xFFFF - self.m_styleAttribute = (self.m_classAttribute >> 16) - 1 - self.m_classAttribute = (self.m_classAttribute & 0xFFFF) - 1 - - # Now, we parse the attributes. - # Each attribute has 5 fields of 4 byte - for i in range(0, self.m_attribute_count * ATTRIBUTE_LENGHT): - # Each field is linearly parsed into the array - # Each Attribute contains: - # * Namespace URI (String ID) - # * Name (String ID) - # * Value - # * Type - # * Data - self.m_attributes.append( - unpack('> 24 - - self.m_event = START_TAG - break - - if h.type == RES_XML_END_ELEMENT_TYPE: - self.m_namespaceUri, = unpack(' uint32_t index - self.m_name, = unpack(' always zero - # uint8_t dataType - # uint32_t data - # For now, we ingore these values - size, res0, dataType, data = unpack("= len(self.m_attributes): - log.warning("Invalid attribute index") - - return offset - - def getAttributeCount(self): - """ - Return the number of Attributes for a Tag - or -1 if not in a tag - """ - if self.m_event != START_TAG: - return -1 - - return self.m_attribute_count - - def getAttributeUri(self, index): - """ - Returns the numeric ID for the namespace URI of an attribute - """ - offset = self._get_attribute_offset(index) - uri = self.m_attributes[offset + ATTRIBUTE_IX_NAMESPACE_URI] - - return uri - - def getAttributeNamespace(self, index): - """ - Return the Namespace URI (if any) for the attribute - """ - uri = self.getAttributeUri(index) - - # No Namespace - if uri == 0xFFFFFFFF: - return '' - - return self.sb[uri] - - def getAttributeName(self, index): - """ - Returns the String which represents the attribute name - """ - offset = self._get_attribute_offset(index) - name = self.m_attributes[offset + ATTRIBUTE_IX_NAME] - - res = self.sb[name] - # If the result is a (null) string, we need to look it up. - if not res: - attr = self.m_resourceIDs[name] - if attr in publics.SYSTEM_RESOURCES['attributes']['inverse']: - res = 'android:' + \ - publics.SYSTEM_RESOURCES['attributes']['inverse'][attr] - else: - # Attach the HEX Number, so for multiple missing attributes we do not run - # into problems. - res = 'android:UNKNOWN_SYSTEM_ATTRIBUTE_{:08x}'.format(attr) - - return res - - def getAttributeValueType(self, index): - """ - Return the type of the attribute at the given index - - :param index: index of the attribute - """ - offset = self._get_attribute_offset(index) - return self.m_attributes[offset + ATTRIBUTE_IX_VALUE_TYPE] - - def getAttributeValueData(self, index): - """ - Return the data of the attribute at the given index - - :param index: index of the attribute - """ - offset = self._get_attribute_offset(index) - return self.m_attributes[offset + ATTRIBUTE_IX_VALUE_DATA] - - def getAttributeValue(self, index): - """ - This function is only used to look up strings - All other work is done by - :func:`~androguard.core.bytecodes.axml.format_value` - # FIXME should unite those functions - :param index: index of the attribute - :return: - """ - offset = self._get_attribute_offset(index) - valueType = self.m_attributes[offset + ATTRIBUTE_IX_VALUE_TYPE] - if valueType == TYPE_STRING: - valueString = self.m_attributes[offset + ATTRIBUTE_IX_VALUE_STRING] - return self.sb[valueString] - return '' - - -def format_value(_type, _data, lookup_string=lambda ix: ""): - """ - Format a value based on type and data. - By default, no strings are looked up and "" is returned. - You need to define `lookup_string` in order to actually lookup strings from - the string table. - - :param _type: The numeric type of the value - :param _data: The numeric data of the value - :param lookup_string: A function how to resolve strings from integer IDs - """ - - # Function to prepend android prefix for attributes/references from the - # android library - def fmt_package(x): return "android:" if x >> 24 == 1 else "" - - # Function to represent integers - def fmt_int(x): return (0x7FFFFFFF & x) - \ - 0x80000000 if x > 0x7FFFFFFF else x - - if _type == TYPE_STRING: - return lookup_string(_data) - - elif _type == TYPE_ATTRIBUTE: - return "?{}{:08X}".format(fmt_package(_data), _data) - - elif _type == TYPE_REFERENCE: - return "@{}{:08X}".format(fmt_package(_data), _data) - - elif _type == TYPE_FLOAT: - return "%f" % unpack("=f", pack("=L", _data))[0] - - elif _type == TYPE_INT_HEX: - return "0x%08X" % _data - - elif _type == TYPE_INT_BOOLEAN: - if _data == 0: - return "false" - return "true" - - elif _type == TYPE_DIMENSION: - return "{:f}{}".format(complexToFloat(_data), DIMENSION_UNITS[_data & COMPLEX_UNIT_MASK]) - - elif _type == TYPE_FRACTION: - return "{:f}{}".format(complexToFloat(_data) * 100, FRACTION_UNITS[_data & COMPLEX_UNIT_MASK]) - - elif TYPE_FIRST_COLOR_INT <= _type <= TYPE_LAST_COLOR_INT: - return "#%08X" % _data - - elif TYPE_FIRST_INT <= _type <= TYPE_LAST_INT: - return "%d" % fmt_int(_data) - - return "<0x{:X}, type 0x{:02X}>".format(_data, _type) diff --git a/build/lib/androyara/utils/__init__.py b/build/lib/androyara/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/lib/androyara/utils/buffer.py b/build/lib/androyara/utils/buffer.py deleted file mode 100644 index a7eee00..0000000 --- a/build/lib/androyara/utils/buffer.py +++ /dev/null @@ -1,175 +0,0 @@ -# -*- encoding: utf-8 -*- -''' -@File : buffer.py -@Author : Loopher -@Version : 1.0 -@License : (C)Copyright 2020-2021, Loopher -@Desc : None -''' - -# Here put the import lib - -class BuffHandle: - """ - BuffHandle is a wrapper around bytes. - It gives the ability to jump in the byte stream, just like with BytesIO. - """ - - def __init__(self, buff): - self.__buff = bytearray(buff) - self.__idx = 0 - - def __getitem__(self, item): - """ - Get the byte at the position `item` - - :param int item: offset in the buffer - :returns: byte at the position - :rtype: int - """ - return self.__buff[item] - - def __len__(self): - return self.size() - - def size(self): - """ - Get the total size of the buffer - - :rtype: int - """ - return len(self.__buff) - - def length_buff(self): - """ - Alias for :meth:`size` - """ - return self.size() - - def set_idx(self, idx): - """ - Set the current offset in the buffer - - :param int idx: offset to set - """ - self.__idx = idx - - def get_idx(self): - """ - Get the current offset in the buffer - - :rtype: int - """ - return self.__idx - - def add_idx(self, idx): - """ - Advance the current offset by `idx` - - :param int idx: number of bytes to advance - """ - self.__idx += idx - - def tell(self): - """ - Alias for :meth:`get_idx`. - - :rtype: int - """ - return self.__idx - - def readNullString(self, size): - """ - Read a String with length `size` at the current offset - - :param int size: length of the string - :rtype: bytearray - """ - data = self.read(size) - return data - - def read_b(self, size): - """ - Read bytes with length `size` without incrementing the current offset - - :param int size: length to read in bytes - :rtype: bytearray - """ - return self.__buff[self.__idx:self.__idx + size] - - def peek(self, size): - """ - Alias for :meth:`read_b` - """ - return self.read_b(size) - - def read_at(self, offset, size): - """ - Read bytes from the given offset with length `size` without incrementing - the current offset - - :param int offset: offset to start reading - :param int size: length of bytes to read - :rtype: bytearray - """ - return self.__buff[offset:offset + size] - - def readat(self, off): - """ - Read all bytes from the start of `off` until the end of the buffer - - This method can be used to determine a checksum of a buffer from a given - point on. - - :param int off: starting offset - :rtype: bytearray - """ - return self.__buff[off:] - - def read(self, size): - """ - Read from the current offset a total number of `size` bytes - and increment the offset by `size` - - :param int size: length of bytes to read - :rtype: bytearray - """ - buff = self.__buff[self.__idx:self.__idx + size] - self.__idx += size - - return buff - - def end(self): - """ - Test if the current offset is at the end or over the buffer boundary - - :rtype: bool - """ - return self.__idx >= len(self.__buff) - - def get_buff(self): - """ - Return the whole buffer - - :rtype: bytearray - """ - return self.__buff - - def set_buff(self, buff): - """ - Overwrite the current buffer with the content of `buff` - - :param bytearray buff: the new buffer - """ - self.__buff = buff - - def save(self, filename): - """ - Save the current buffer to `filename` - - Exisiting files with the same name will be overwritten. - - :param str filename: the name of the file to save to - """ - with open(filename, "wb") as fd: - fd.write(self.__buff) diff --git a/build/lib/androyara/utils/mcolor.py b/build/lib/androyara/utils/mcolor.py deleted file mode 100644 index 17473f9..0000000 --- a/build/lib/androyara/utils/mcolor.py +++ /dev/null @@ -1,17 +0,0 @@ -# coding:utf8 -''' -@File : mcolor.py -@Author : Loopher -@Version : 1.0 -@License : (C)Copyright 2020-2021,Loopher -@Desc : None -''' - -light_yellow = '\033[33m' -light_blue = '\033[36m' -yellow = '\033[33m' -pink = '\033[35m' -white = '\033[37m' -red = '\033[31m' -reset = '\033[37m' -green = '\033[32m' diff --git a/build/lib/androyara/utils/utility.py b/build/lib/androyara/utils/utility.py deleted file mode 100644 index aebbf56..0000000 --- a/build/lib/androyara/utils/utility.py +++ /dev/null @@ -1,26 +0,0 @@ -# coding:utf8 -''' -@File : utility.py -@Author : Loopher -@Version : 1.0 -@License : (C)Copyright 2020-2021,Loopher -@Desc : common utility -''' - - -from termcolor import colored - - -def echo(tag, msg, color="green"): - # show info - try: - print(colored("[{}]: {}".format(tag, msg), color=color)) - except UnicodeDecodeError as e: - print("--> type : {}".format(type(msg))) - raise e - - -def byte2str(s): - if isinstance(s, bytes): - s = str(s, encoding="utf-8") - return s diff --git a/build/lib/androyara/vsbox/__init__.py b/build/lib/androyara/vsbox/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/lib/androyara/vsbox/hybird.py b/build/lib/androyara/vsbox/hybird.py deleted file mode 100644 index ab6f063..0000000 --- a/build/lib/androyara/vsbox/hybird.py +++ /dev/null @@ -1,58 +0,0 @@ -# coding:utf8 -''' -@File : hybird.py -@Author : Loopher -@Version : 1.0 -@License : (C)Copyright 2020-2021,Loopher -@Desc : query hybird sandbox report info -''' -import requests -from requests.api import head -from androyara.vsbox.vsbox import VSandbox - - -class HybirdSanbox(VSandbox): - - def get_sbox_info(self, config, resource): - return "", None - - def query_report(self, config, resource): - # hybird sandbox need query by user's,it's not http post method - api_key = config.get("hybird", "api_key") - if api_key is None or api_key == '': - - return None - headers = { - "api-key": api_key, - "user-agent": "Falcon Sandbox", # fixed header - # "sha256": resource - } - url = "https://www.hybrid-analysis.com/api/v2/overview/"+resource - res = requests.get(url=url, headers=headers) - try: - self.echo("info", " {} response code {} ".format( - self.sbox_name(), res.status_code)) - if res.status_code == 403: - self.echo("error", " {} query 403 , due to {}".format( - self.sbox_name(), res.json()), "red") - return None - elif res.status_code == 200: - return res.json() - else: - self.echo("error", " {} query report failed ,{} ".format( - self.sbox_name(), res.json())) - return None - except Exception as e: - self.echo("warning", "{} query report error : {}".format( - self.sbox_name(), e), "yellow") - return None - - def sbox_name(self): - return "Hybird" - - def analysis(self): - result = self.get_result() - if result is None: - return - self.echo("Info:", " {} query report result.".format - (result)) diff --git a/build/lib/androyara/vsbox/threatbook.py b/build/lib/androyara/vsbox/threatbook.py deleted file mode 100644 index 9fbb3f6..0000000 --- a/build/lib/androyara/vsbox/threatbook.py +++ /dev/null @@ -1,66 +0,0 @@ -# coding:utf8 -''' -@File : threatbook.py -@Author : Loopher -@Version : 1.0 -@License : (C)Copyright 2020-2021,Loopher -@Desc : query threatbook report . -''' -from androyara.vsbox.vsbox import VSandbox -from androyara.utils.mcolor import * - - -class ThreatbookSandbox(VSandbox): - - def get_sbox_info(self, config, resource): - - threatbook_api_key = config.get("threatbook", "api_key") - if threatbook_api_key is None or threatbook_api_key == '': - self.echo("warning", " {} sanbox api_key is None or empty".format( - self.sbox_name()), "yellow") - return "", None - params = {"apikey": threatbook_api_key, "sha256": resource} - url = "https://api.threatbook.cn/v3/file/report/multiengines" - return url, params - - def analysis(self): - """ - Return analysis result - """ - - result = self.get_result() - if result is None: - return - if result['response_code'] != 0: - self.echo("error", result['verbose_msg']) - return - - data = result['data']['multiengines'] - scans = result['data']['multiengines']['scans'] # get anti-vendor name - - print(reset) - print("\t"+yellow+"--"*40, end='\n\n') - print(white+"\tservice:\tThreatbook", end='\n') - print(green + "\t%s" % ("result:"), end='') - print(red+"\t\t%d" % (data['positives']), end='') - print(reset+"/", end='') - print("%d" % (data['total2']), end='\n') - print(pink+"\tmalware_type:"+" %s" % - (data['malware_type']), end='\n') - print(reset, end="") - print(yellow+"\tmalware_family: %s" % - (data['malware_family']), end='\n') - print(reset, end="") - print("\tAntiProductEngine:\t%s " % - ("Tencent"), end='\n') - print("\tvirusName: ", end="") - print(red+"%s" % (scans['Tencent']), end="\n") - print(reset, end="") - print("\tscan_date: %s" % (data['scan_date'])) - print("\tsha256: ", end="") - print(yellow+"%s" % (self.resource), end="\n") - print(reset) - - def sbox_name(self): - - return "Threatbook" diff --git a/build/lib/androyara/vsbox/vsbox.py b/build/lib/androyara/vsbox/vsbox.py deleted file mode 100644 index 2b02e33..0000000 --- a/build/lib/androyara/vsbox/vsbox.py +++ /dev/null @@ -1,106 +0,0 @@ -# coding:utf8 -''' -@File : vsbox.py -@Author : Loopher -@Version : 1.0 -@License : (C)Copyright 2020-2021,Loopher -@Desc : View Sandbox query samples fingerprint from online sandbox -''' - -import configparser -import logging -import os -import requests -import json -from termcolor import colored - -# root = os.path.abspath(os.path.dirname(__file__)) -# root = root[:root.rfind(os.sep)] -# user = root[:root.rfind(os.sep)]+os.sep+"user" - -logger = logging.getLogger("androyara.vbox") -logger.setLevel(logging.INFO) - - -class ConfigError(BaseException): - pass - - -class VSandbox(object): - - def __init__(self, finger_print): - - self.result = None - self._init_conf(finger_print) - - def _init_conf(self, finger_print: str): - - if "USR_CONFIG_INI" not in os.environ: - raise Exception("Please USR_CONFIG_INI as env variable.") - user = os.getenv("USR_CONFIG_INI") - if not os.path.isfile(user): - raise FileNotFoundError("%s is not file "%(user)) - config_path = user#user+os.sep+"user.conf" - if not os.path.isfile(config_path): - print(colored( - "[error]: Read user.conf error {} is not file !!! ".format(config_path), "red")) - - return - if finger_print is None or finger_print == '': - self.echo( - "error", " query sanbox resource must be not None or empty!!!!") - return None - config = configparser.ConfigParser() - config.read(config_path) - - self.resource = finger_print - url, params = self.get_sbox_info( - config, finger_print) # Get sanbox url,api_key,sanbox_name - if url != '': - result = self.query(url, params) - if result is None: - self.echo("warning", " {} can't query anything from {} sandbox".format( - finger_print, self.sbox_name())) - return - else: - self.echo( - "warning", " {} sandbox url is empty , your should call query_report method for query report ".format(self.sbox_name()), "yellow") - result = self.query_report(config, finger_print) - - self.result = result - - def analysis(self): - pass - - def get_result(self): - - return self.result - - def query_report(self, config, resource): - """ - another query method for user - return : - """ - return None - - def sbox_name(self): - - return "VSandbox" - - def query(self, url, params): - - try: - res = requests.get(url=url, params=params) - res.raise_for_status() - result = res.json() - return result - except Exception as e: - self.echo("warning", "request report error {}".format(e), "yellow") - return None - - def echo(self, tag, msg, color="green"): - print(colored("[{}]: {}".format(tag, msg), color)) - - def get_sbox_info(self, config, resource): - - raise NotImplementedError diff --git a/build/lib/androyara/vsbox/vt.py b/build/lib/androyara/vsbox/vt.py deleted file mode 100644 index aa7575c..0000000 --- a/build/lib/androyara/vsbox/vt.py +++ /dev/null @@ -1,71 +0,0 @@ -# coding:utf8 -''' -@File : vt.py -@Author : Loopher -@Version : 1.0 -@License : (C)Copyright 2020-2021,Loopher -@Desc : reference https://developers.virustotal.com/v3.0/reference#key-concepts -''' -from androyara.vsbox.vsbox import VSandbox -from androyara.utils.mcolor import * -import configparser - - -class VT(VSandbox): - - def get_sbox_info(self, config: configparser.ConfigParser, resource): - vt_key = config.get("VT", "api_key") - if vt_key is None or vt_key == '': - self.echo("warning", "VT api_key is emtpy or None", "yellow") - return "", None - - # self.echo("Info", "start querying VT analysis info... {}".format( - # vt_key), "green") - # v3 api - # https://www.virustotal.com/api/v3/{collection name}/{object id} - # {collection name} = files / analyses - # v2 api https://developers.virustotal.com/reference#file-report - url = "https://www.virustotal.com/vtapi/v2/file/report" - params = {'apikey': vt_key, 'resource': resource} - return url, params - - def sbox_name(self): - return "VT" - - def analysis(self): - result = self.get_result() - if result is None: - return None - - scan = { - "positives": "%d/%d" % (result['positives'], result['total']), - "virusName": "", - "link": result['permalink'], - "scanDate": result['scan_date'], - "vendor": "" - - - } - if result['positives'] > 0: - for vendor, v in result['scans'].items(): - - if v['detected']: - scan['virusName'] = v['result'] - scan['vendor'] = vendor - break - - print(reset) - print("\t"+yellow+"-"*40, end='\n\n') - print(white+"\tservice:\tVirusTotal", end='\n\n') - print(green + "\t%s" % ("result:"), end='') - print(red+"\t\t%d" % (result['positives']), end='') - print(reset+"/", end='') - print("%d" % (result['total']), end='\n\n') - print(pink+"\tpermalink:"+"\t%s" % - (result['permalink']), end='\n\n') - print(reset) - print("\tvirusName:\t%s vendor: %s" % - (scan['virusName'], scan['vendor']), end='\n\n') - print("\tscan_date:\t%s" % (result['scan_date']), end='\n\n') - - return scan diff --git a/build/lib/test/__init__.py b/build/lib/test/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/build/lib/test/test_apk.py b/build/lib/test/test_apk.py deleted file mode 100644 index d78fdd0..0000000 --- a/build/lib/test/test_apk.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- encoding: utf-8 -*- -''' -@File : test_apk.py -@Time : 2021/04/18 12:22:26 -@Author : Loopher -@Version : 1.0 -@Contact : 2426607795@qq.com -@License : (C)Copyright 2020-2021, Loopher -@Desc : None -''' - -# Here put the import lib - -import unittest -import os -import json - -from androyara.core.apk_parser import ApkPaser -from androyara.dex.dex_header import DexHeader -from androyara.dex.dex_vm import DexFileVM - -root = os.path.abspath(os.path.dirname(__file__)) -sample = root[:root.rfind(os.sep)] - - -class ApkTester(unittest.TestCase): - - def test_apk(self): - # app-release-v3-signed.apk : v1+v2+v3 signed app-release.apk v1+v2 signed - # tencent apk aaa.apk v1+v2 signature - # check signature command : /path/to/sdk/build-tools/30.0.2/apksinger verify --print-certs test.apk - # signed with v3 : /path/to/sdk/build-tools/30.0.2/apksinger sign --ks my.jsk --v3-signed-enabled true test.apk - for f in [sample+os.sep+"samples"+os.sep+"aaa.apk"]: - apk = ApkPaser(f) - print("--"*10+"apk info "+"--"*10) - print(json.dumps(apk.apk_base_info(), indent=2)) - - print(apk.apk_base_info()) - # vm = DexFileVM(apk.package,apk.get_classe_dex()) - # vm.build_map() - - # dex = DexHeader(apk.get_classe_dex()) - # dex.read_all(apk.package) - pass diff --git a/build/lib/test/test_axml.py b/build/lib/test/test_axml.py deleted file mode 100644 index d223f43..0000000 --- a/build/lib/test/test_axml.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- encoding: utf-8 -*- -''' -@File : test_axml.py -@Author : Loopher -@Version : 1.0 -@License : (C)Copyright 2020-2021, Loopher -@Desc : None -''' - -# Here put the import lib - -import os -import unittest -from androyara.core.axml_parser import AndroidManifestXmlParser - -root = os.path.abspath(os.path.dirname(__file__)) -sample = root[:root.rfind(os.sep)] - - -class AxmlTesst(unittest.TestCase): - - def test_axml(self): - - for xml in [sample+os.sep+"samples"+os.sep+"AndroidManifest.xml"]: - # axml = AndroidManifestXmlParser(xml) - # # print(axml) - # # print(axml.get_all_export_components()) - - # print(axml.get_main_activity()) - # axml.get_main_activity() - pass diff --git a/build/lib/test/test_dex.py b/build/lib/test/test_dex.py deleted file mode 100644 index 9c24d87..0000000 --- a/build/lib/test/test_dex.py +++ /dev/null @@ -1,29 +0,0 @@ -# coding:utf8 -''' -@File : test_dex.py -@Author : Loopher -@Version : 1.0 -@License : (C)Copyright 2020-2021,Loopher -@Desc : None -''' - -import unittest -import os - - -from androyara.dex.dex_header import DexHeader - -root =os.path.abspath(os.path.dirname(__file__)) -sample = root[:root.rfind(os.sep)] -# print("--> root ",root,sample) - -class DexTest(unittest.TestCase): - - def test_dex(self): - # hidex - for dex in [sample+os.sep+"samples"+os.sep+"classes.dex"]: - pass - # with open(dex, 'rb') as fp: - # dex_header = DexHeader(fp.read()) - # pkg = "com.tencent.qqpimsecure"#"com.loopher.virus" classes_1.dex - # dex_header.read_all(pkg) \ No newline at end of file diff --git a/build/lib/test/test_vbox.py b/build/lib/test/test_vbox.py deleted file mode 100644 index 30e2329..0000000 --- a/build/lib/test/test_vbox.py +++ /dev/null @@ -1,22 +0,0 @@ -# coding:utf8 -''' -@File : test_vbox.py -@Author : Loopher -@Version : 1.0 -@License : (C)Copyright 2020-2021,Loopher -@Desc : None -''' - -import unittest -from androyara.vsbox.vt import VT -from androyara.vsbox.threatbook import ThreatbookSandbox -from androyara.vsbox.hybird import HybirdSanbox - - -class TestVsbox(unittest.TestCase): - - def test_vsbox(self): - VT("ee70eda8a7f6b209c6bb4780bf2a8a96730c19a78300eb5ec3c25a48e557cb2e").analysis() - # ThreatbookSandbox("b87f2f3a927bf967736ed43ca2dbfb60").analysis() - # HybirdSanbox( - # "d2ba9a60abf9eade2d2934c75bd8de945e93c53e8e06f790a19a25925e793092").analysis() diff --git a/build/lib/test/test_virus.py b/build/lib/test/test_virus.py deleted file mode 100644 index 1d068c6..0000000 --- a/build/lib/test/test_virus.py +++ /dev/null @@ -1,45 +0,0 @@ -# coding:utf8 -''' -@File : test_virus.py -@Author : Loopher -@Version : 1.0 -@License : (C)Copyright 2020-2021,Loopher -@Desc : None -''' -import unittest -import os -import json -import sys -from androyara.core.apk_parser import ApkPaser -from androyara.dex.dex_vm import DexFileVM -path = "/tmp/androyara/allvirusSample" - - -class ViruApkTest(unittest.TestCase): - - def test_virus_scan(self): - - # for root, _, fs in os.walk(path): - # for f in fs: - # if f.endswith('.APK') or f.endswith('.apk'): - # apk_file = os.path.join(root, f) - # try: - # apk_parser = ApkPaser(apk_file) - # print(json.dumps(apk_parser.apk_base_info(), indent=2)) - # vm = DexFileVM(apk_parser.package, - # apk_parser.get_classe_dex()) - # vm.build_map() - # # break - - # except Exception as e: - # print("error {} file: {}".format( - # e, apk_file), file=sys.stderr) - pass - - def test_virus_file(self): - # "/tmp/androyara/allvirusSample/MalwareSamples/TROJAN/5A51DC7F8ABB013758B8D2C9B9A29967D82C80A7C5CEC67E45E46C28A55AA84D.APK" - apk_file = "/tmp/androyara/allvirusSample/virussample/virussamplevirus5.apk" - if not os.path.isfile(apk_file): - return - apk_parser = ApkPaser(apk_file) - print(json.dumps(apk_parser.apk_base_info(), indent=2)) diff --git a/dist/androyara-2.0-py3-none-any.whl b/dist/androyara-2.0-py3-none-any.whl deleted file mode 100644 index 0ad0b4ebdfcc4faa67d827f93a99c8397409d069..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 104432 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