Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions docs/source/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@ Once you see the following msg, then you're all set::
-d, --detail TEXT Show detail report. Optionally specify the
name of a rule/label
-o, --output FILE Output report in JSON
-w, --webreport FILE Generate web report
-a, --apk FILE APK file [required]
-r, --rule PATH Rules directory [default:
/Users/$USER/.quark-engine/quark-rules]
-g, --graph Create call graph to call_graph_image
/home/$USER/.quark-engine/quark-rules]
-g, --graph [png|json] Create call graph to call_graph_image
directory
-c, --classification Show rules classification
-t, --threshold [100|80|60|40|20]
Expand All @@ -46,10 +47,10 @@ Once you see the following msg, then you're all set::
--core-library [androguard|rizin]
Specify the core library used to analyze an
APK
--multi-process INTEGER RANGE Allow analyzing APK with N processes,
where N doesn't exceeds the number of usable CPUs - 1
to avoid memory exhaustion.
--version Show the version and exit.
--multi-process INTEGER RANGE Allow analyzing APK with N processes, where
N doesn't exceeds the number of usable CPUs
- 1 to avoid memory exhaustion. [x>=1]
--version Show the version and exit.
--help Show this message and exit.

To learn how to scan multiple samples in a directory, please have a look at :ref:`Directory Scanning <dir_scan>`
16 changes: 15 additions & 1 deletion docs/source/testing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ Then we could test one of the apk in `apk-malware-samples` by the rules `quark-r

$ quark -a Ahmyth.apk -s


Running in Docker
=================

Expand All @@ -36,3 +35,18 @@ You may also interactively use quark in the docker container. For example:
(in-docker): /app/quark# cd /tmp
(in-docker)::/tmp# quark -a Ahmyth.apk -s

Running analyses based on Rizin (Upcoming unstable feature)
===========================================================

Now Quark also supports `Rizin`_ as one of our Android analysis frameworks. You can use option ``--core-library`` with ``rizin`` to enable the Rizin-based analysis library.

.. _`Rizin`: https://github.com/rizinorg/rizin

.. code-block:: bash

quark -a Ahmyth.apk -s --core-library rizin


For now, Quark is compatible with Rizin v0.3.4. But, users don't have to installed a Rizin with that version. Quark provides a feature to automatically setup a independent Rizin. In this way, the dependency will not conflict with your environment. Type in the above command and let Quark handle everything else for you.

If there is a working installation of Rizin installed in the system, Quark will check its version to determine if it is compatible. If true, Quark automatically uses it for the analysis. Otherwise, Quark uses the independent Rizin installed in the Quark directory (``~/.quark-engine``).
26 changes: 19 additions & 7 deletions quark/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from quark.utils.colors import yellow
from quark.utils.graph import select_label_menu, show_comparison_graph
from quark.utils.pprint import print_info, print_success, print_warning
from quark.utils.tools import find_rizin
from quark.utils.weight import Weight
from quark.webreport.generate import ReportGenerator

Expand Down Expand Up @@ -141,8 +142,8 @@
"--multi-process",
"num_of_process",
type=click.IntRange(min=1),
help="Allow analyzing APK with N processes, where N doesn't exceeds" +
" the number of usable CPUs - 1 to avoid memory exhaustion.",
help="Allow analyzing APK with N processes, where N doesn't exceeds"
+ " the number of usable CPUs - 1 to avoid memory exhaustion.",
required=False,
default=1,
)
Expand All @@ -169,6 +170,11 @@ def entry_point(
rule_buffer_list = []
rule_filter = summary or detail

if core_library.lower() == "rizin":
rizin_path = find_rizin()
else:
rizin_path = ""

# Determine the location of rules
if rule_filter and rule_filter.endswith("json"):
if not os.path.isfile(rule_filter):
Expand Down Expand Up @@ -224,9 +230,14 @@ def entry_point(
malware_confidences = {}
for apk_ in apk:
data = (
ParallelQuark(apk_, core_library, num_of_process)
ParallelQuark(
apk_,
core_library,
num_of_process,
rizin_path,
)
if num_of_process > 1
else Quark(apk_, core_library)
else Quark(apk_, core_library, rizin_path)
)
all_labels = {}
# dictionary containing
Expand Down Expand Up @@ -281,9 +292,9 @@ def entry_point(

# Load APK
data = (
ParallelQuark(apk[0], core_library, num_of_process)
ParallelQuark(apk[0], core_library, num_of_process, rizin_path)
if num_of_process > 1
else Quark(apk[0], core_library)
else Quark(apk[0], core_library, rizin_path)
)

if label:
Expand Down Expand Up @@ -410,7 +421,8 @@ def entry_point(

json_report = data.get_json_report()
report_html = ReportGenerator(
json_report).get_analysis_report_html()
json_report
).get_analysis_report_html()

if ".html" not in webreport:
webreport = f"{webreport}.html"
Expand Down
3 changes: 3 additions & 0 deletions quark/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,8 @@
DIR_PATH = f"{HOME_DIR}quark-rules"

DEBUG = False
COMPATIBLE_RAZIN_VERSIONS = ["v0.3.4"]

RIZIN_DIR = f"{HOME_DIR}rizin/"

Path(HOME_DIR).mkdir(parents=True, exist_ok=True)
18 changes: 15 additions & 3 deletions quark/core/axmlreader/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import enum
import functools
import os.path
import pkg_resources

import pkg_resources
import rzpipe

# Resource Types Definition
Expand Down Expand Up @@ -71,7 +71,19 @@ class AxmlReader(object):
A Class that parses the Android XML file
"""

def __init__(self, file_path, structure_path=None):
def __init__(self, file_path, structure_path=None, rizin_path=None):
"""
Create an AxmlReader object to parse the given Android XML file like
AndroidManifest.xml.

:param file_path: a file in the Android XML format
:param structure_path: a plain text file defining the structures of
the Android XML format. Defaults to None
:param rizin_path: a PathLike object to specify a Rizin executable to
use. Defaults to None
:raises AxmlException: if the given file is an invalid Android XML
file
"""
if structure_path is None:
structure_path = pkg_resources.resource_filename(
"quark.core.axmlreader", "axml_definition"
Expand All @@ -83,7 +95,7 @@ def __init__(self, file_path, structure_path=None):
f" of Rizin in {structure_path}"
)

self._rz = rzpipe.open(file_path)
self._rz = rzpipe.open(file_path, rizin_home=rizin_path)
self._rz.cmd(f"pfo {structure_path}")

self._file_size = int(self._rz.cmd("i~size[1]"), 16)
Expand Down
67 changes: 59 additions & 8 deletions quark/core/parallelquark.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
# This file is part of Quark-Engine - https://github.com/quark-engine/quark-engine
# See the file 'LICENSE' for copying permission.

from multiprocessing.pool import Pool
from multiprocessing import cpu_count
from multiprocessing.pool import Pool

from quark.core.analysis import QuarkAnalysis
from quark.core.quark import Quark
Expand All @@ -13,12 +13,33 @@

class ParallelQuark(Quark):
@staticmethod
def _worker_initializer(apk, core_library):
def _worker_initializer(apk, core_library, rizin_path):
"""
An initializer that creates multiple Quark object for a subprocess
pool.

:param apk: an APK for Quark to analyze
:param core_library: a string indicating which analysis library Quark
should use
:param rizin_path: a PathLike object to specify a Rizin executable for
the Rizin-based analysis library
"""

global _quark
_quark = Quark(apk, core_library)
_quark = Quark(apk, core_library, rizin_path)

@staticmethod
def _worker_analysis(rule_obj):
"""
A function for the subprocesses in the pool to analyze the target APK
with the specified rule.

:param rule_obj: a Quark rule that the subprocess will analyze
:return: a tuple of the analysis result, including the reached stage,
the parent functions, the detected behavior (rule), and the
corresponding bytecodes.
"""

_quark.quark_analysis = QuarkAnalysis()
_quark.run(rule_obj)

Expand All @@ -32,7 +53,8 @@ def to_raw_method(methodobject):

reached_stage = rule_obj.check_item.count(True)
level_4_result = tuple(
to_raw_method(method) for method in _quark.quark_analysis.level_4_result
to_raw_method(method)
for method in _quark.quark_analysis.level_4_result
)
behavior_list = [
(
Expand All @@ -51,6 +73,13 @@ def to_raw_method(methodobject):
)

def _apply_analysis_result(self, rule_obj):
"""
Append the result returned by the subprocesses into the QuarkAnalysis
object.

:param rule_obj: a Quark rule specifying which result this method
should append
"""
async_result = self._result_map[id(rule_obj)]
result = async_result.get()

Expand Down Expand Up @@ -112,21 +141,43 @@ def _apply_analysis_result(self, rule_obj):
]
)

def __init__(self, apk, core_library, num_of_process=1):
def __init__(self, apk, core_library, num_of_process=1, rizin_path=None):
"""
Create a ParallelQuark object to run the Quark analysis parallelly.

:param apk: an APK for Quark to analyze
:param core_library: a string indicating which analysis library Quark
should use
:param num_of_process: a value indicating the maximal number of
available processes for the analysis. Defaults to 1
:param rizin_path: a PathLike object to specify a Rizin executable for
the Rizin-based analysis library. Default to None
"""
self._result_map = {}
self._pool = Pool(
min(num_of_process, cpu_count() - 1), self._worker_initializer,
(apk, core_library)
min(num_of_process, cpu_count() - 1),
self._worker_initializer,
(apk, core_library, rizin_path),
)

super().__init__(apk, core_library)
super().__init__(apk, core_library, rizin_path=rizin_path)

def apply_rules(self, rule_obj_list):
"""
Add Quark rules to this pool.

:param rule_obj_list: a list of Quark rules
"""
for rule_obj in rule_obj_list:
result = self._pool.apply_async(self._worker_analysis, (rule_obj,))
self._result_map[id(rule_obj)] = result

def run(self, rule_obj):
"""
Wait for all rules to be analyzed.

:param rule_obj: _description_
"""
self._apply_analysis_result(rule_obj)

def close(self):
Expand Down
14 changes: 10 additions & 4 deletions quark/core/quark.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,20 @@
class Quark:
"""Quark module is used to check quark's five-stage theory"""

def __init__(self, apk, core_library="androguard"):
def __init__(self, apk, core_library="androguard", rizin_path=None):
"""

:param apk: the filename of the apk.
Create a Quark object.

:param apk: an APK for Quark to analyze
:param core_library: a string indicating which analysis library Quark
should use. Defaults to "androguard"
:param rizin_path: a PathLike object to specify a Rizin executable for
the Rizin-based analysis library. Defaults to None
:raises ValueError: if an unknown core library is specified
"""
core_library = core_library.lower()
if core_library == "rizin":
self.apkinfo = RizinImp(apk)
self.apkinfo = RizinImp(apk, rizin_path=rizin_path)
elif core_library == "androguard":
self.apkinfo = AndroguardImp(apk)
else:
Expand Down
19 changes: 16 additions & 3 deletions quark/core/rzapkinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
from quark.core.interface.baseapkinfo import BaseApkinfo
from quark.core.struct.bytecodeobject import BytecodeObject
from quark.core.struct.methodobject import MethodObject
from quark.utils.tools import descriptor_to_androguard_format, remove_dup_list
from quark.utils.tools import (descriptor_to_androguard_format,
_get_rizin_version, remove_dup_list)

RizinCache = namedtuple("rizin_cache", "address dexindex is_imported")

Expand Down Expand Up @@ -51,9 +52,21 @@ def __init__(
self,
apk_filepath: Union[str, PathLike],
tmp_dir: Union[str, PathLike] = None,
rizin_path: PathLike = None,
):
super().__init__(apk_filepath, "rizin")

if rizin_path:
if not _get_rizin_version(rizin_path):
raise ValueError(
f"The file in {rizin_path} is not a valid Rizin"
+ " executable."
)

rizin_path = os.path.dirname(rizin_path)

self.rizin_path = rizin_path

if self.ret_type == "DEX":
self._tmp_dir = None
self._dex_list = [apk_filepath]
Expand Down Expand Up @@ -84,7 +97,7 @@ def __init__(

@functools.lru_cache
def _get_rz(self, index):
rz = rzpipe.open(self._dex_list[index])
rz = rzpipe.open(self._dex_list[index], rizin_home=self.rizin_path)
rz.cmd("aa")
return rz

Expand Down Expand Up @@ -227,7 +240,7 @@ def _get_methods_classified(self, dexindex):

@functools.cached_property
def permissions(self) -> List[str]:
axml = AxmlReader(self._manifest)
axml = AxmlReader(self._manifest, rizin_path=self.rizin_path)
permission_list = set()

for tag in axml:
Expand Down
Loading