Skip to content

Commit b879845

Browse files
authored
Merge pull request #374 from haeter525/feat/quark_script_hook_with_frida
Add Quark Script APIs to detect CWE-312
2 parents 2ce703f + ff0eca7 commit b879845

File tree

9 files changed

+570
-20
lines changed

9 files changed

+570
-20
lines changed

.github/workflows/pytest.yml

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,12 @@ jobs:
2323
- name: Install dependencies
2424
run: |
2525
python -m pip install --upgrade pip
26-
pip install pytest pipenv rzpipe meson==0.62.0 ninja coverage
26+
python -m pip install pytest rzpipe meson==0.62.0 ninja coverage ciphey frida objection
2727
sudo apt-get install -y ninja-build
2828
2929
# Install graphviz
3030
sudo apt-get -y install graphviz
3131
32-
3332
# Install Rizin
3433
sudo git clone --branch v0.3.4 https://github.com/rizinorg/rizin /opt/rizin/
3534
cd /opt/rizin/
@@ -39,12 +38,19 @@ jobs:
3938
sudo ldconfig -v
4039
cd -
4140
42-
41+
# Install click >= 8.0.0 for CLI supports
42+
python -m pip install click==8.0.3
43+
44+
- name: Install Quark-Engine
45+
run: |
46+
python setup.py build
47+
python setup.py install
48+
4349
- name: Test with pytest
4450
run: |
45-
pipenv install --dev
46-
pipenv install coveralls codecov pytest-cov --skip-lock
47-
pipenv run pytest --cov=./
51+
python -m pip install black pytest sphinx sphinx-rtd-theme
52+
python -m pip install coveralls codecov pytest-cov
53+
pytest --cov=./
4854
4955
- name: Upload coverage to Codecov
5056
uses: codecov/codecov-action@v1

.github/workflows/smoke_test.yml

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,46 +27,54 @@ jobs:
2727

2828
steps:
2929
- uses: actions/checkout@v2
30+
3031
- name: Set up Python
3132
uses: actions/setup-python@v2
3233
with:
3334
python-version: ${{ matrix.python-version }}
3435

35-
# Runs a single command using the runners shell
3636
- name: Install dependencies
3737
run: |
3838
python -m pip install --upgrade pip
39-
python -m pip install pipenv
40-
pipenv install --skip-lock --dev
39+
python -m pip install ciphey frida objection
40+
python -m pip install black pytest sphinx sphinx-rtd-theme
41+
42+
# Install click >= 8.0.0 for CLI supports
43+
python -m pip install click==8.0.3
4144
4245
- run: sudo apt-get -y install graphviz
4346
if: matrix.os == 'ubuntu-latest'
4447
- run: brew install graphviz
4548
if: matrix.os == 'macOS-latest'
4649
- run: choco install graphviz
4750
if: matrix.os == 'windows-latest'
51+
52+
- name: Install Quark-Engine
53+
run: |
54+
python setup.py build
55+
python setup.py install
56+
4857
# Download the latest rule set
4958
- name: Download rule from https://github.com/quark-engine/quark-rules
50-
run: |
51-
pipenv run freshquark
59+
run: freshquark
5260

5361
# Runs a set of commands using the quark-engine
5462
- name: Run a multi-line script
5563
run: |
56-
pipenv run quark --help
64+
quark --help
5765
git clone https://github.com/quark-engine/apk-malware-samples
58-
pipenv run quark -a apk-malware-samples/14d9f1a92dd984d6040cc41ed06e273e.apk -s
59-
pipenv run quark -a apk-malware-samples/14d9f1a92dd984d6040cc41ed06e273e.apk -d
60-
pipenv run quark -a apk-malware-samples/14d9f1a92dd984d6040cc41ed06e273e.apk -s -g
61-
pipenv run quark -a apk-malware-samples/14d9f1a92dd984d6040cc41ed06e273e.apk -d -g
62-
pipenv run quark -a apk-malware-samples/14d9f1a92dd984d6040cc41ed06e273e.apk -s -c
66+
quark -a apk-malware-samples/14d9f1a92dd984d6040cc41ed06e273e.apk -s
67+
quark -a apk-malware-samples/14d9f1a92dd984d6040cc41ed06e273e.apk -d
68+
quark -a apk-malware-samples/14d9f1a92dd984d6040cc41ed06e273e.apk -s -g
69+
quark -a apk-malware-samples/14d9f1a92dd984d6040cc41ed06e273e.apk -d -g
70+
quark -a apk-malware-samples/14d9f1a92dd984d6040cc41ed06e273e.apk -s -c
6371
6472
- name: Check Accuracy
6573
shell: bash
6674
run: |
67-
echo "Ahmyth_RESULT=$(pipenv run quark -a apk-malware-samples/Ahmyth.apk -s -t 100 | grep 100% | wc -l | awk '{print $1}')" >> $GITHUB_ENV
68-
echo "a4db_RESULT=$(pipenv run quark -a apk-malware-samples/13667fe3b0ad496a0cd157f34b7e0c991d72a4db.apk -s -t 100 | grep 100% | wc -l | awk '{print $1}')" >> $GITHUB_ENV
69-
echo "e273e_RESULT=$(pipenv run quark -a apk-malware-samples/14d9f1a92dd984d6040cc41ed06e273e.apk -s -t 100 | grep 100% | wc -l | awk '{print $1}')" >> $GITHUB_ENV
75+
echo "Ahmyth_RESULT=$(quark -a apk-malware-samples/Ahmyth.apk -s -t 100 | grep 100% | wc -l | awk '{print $1}')" >> $GITHUB_ENV
76+
echo "a4db_RESULT=$(quark -a apk-malware-samples/13667fe3b0ad496a0cd157f34b7e0c991d72a4db.apk -s -t 100 | grep 100% | wc -l | awk '{print $1}')" >> $GITHUB_ENV
77+
echo "e273e_RESULT=$(quark -a apk-malware-samples/14d9f1a92dd984d6040cc41ed06e273e.apk -s -t 100 | grep 100% | wc -l | awk '{print $1}')" >> $GITHUB_ENV
7078
7179
- name: Check Ahmyt Result
7280
shell: bash

Pipfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ plotly = "<=5.4.0"
2323
prompt-toolkit = "==3.0.19"
2424
rzpipe = "<=0.1.2"
2525
objection = "<=1.11.0"
26+
frida = "<=15.2.2"
27+
ciphey = ">=5.0.0,<=5.14.0"
2628

2729
[requires]
2830
python_version = "3.8"

quark/script/ciphey.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# -*- coding: utf-8 -*-
2+
# This file is part of Quark-Engine - https://github.com/quark-engine/quark-engine
3+
# See the file 'LICENSE' for copying permission.
4+
5+
from ciphey import decrypt
6+
from ciphey.iface import Config
7+
8+
9+
def checkClearText(inputString: str) -> str:
10+
"""Check the decrypted value of the input string.
11+
12+
:param inputString: string to be checked.
13+
:return: the decrypted value
14+
"""
15+
return decrypt(Config().library_default().complete_config(), inputString)

quark/script/frida/__init__.py

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
# -*- coding: utf-8 -*-
2+
# This file is part of Quark-Engine - https://github.com/quark-engine/quark-engine
3+
# See the file 'LICENSE' for copying permission.
4+
5+
import functools
6+
import json
7+
import re
8+
import sys
9+
from dataclasses import dataclass
10+
from time import sleep
11+
from typing import Any, Dict, List, Tuple, Union
12+
13+
import pkg_resources
14+
from quark.utils.regex import URL_REGEX
15+
16+
import frida
17+
from frida.core import Device
18+
from frida.core import Session as FridaSession
19+
20+
MethodCallEvent = Dict[str, Union[List[str], str]]
21+
22+
23+
class MethodCallEventDispatcher:
24+
def __init__(self, frida: FridaSession) -> None:
25+
self.frida = frida
26+
self.watchedMethods = {}
27+
28+
@staticmethod
29+
def _getMethodIdentifier(targetMethod: str, paramType: str):
30+
return (targetMethod, paramType)
31+
32+
def startWatchingMethodCall(
33+
self, targetMethod: str, methodParamTypes: str
34+
) -> List[MethodCallEvent]:
35+
"""Start tracking calls to the target method.
36+
37+
:param targetMethod: the target API
38+
:param methodParamTypes: the parameter types of the target API
39+
:return: python list that holds calls to the target method
40+
"""
41+
eventBuffer = []
42+
methodId = self._getMethodIdentifier(targetMethod, methodParamTypes)
43+
44+
self.watchedMethods[methodId] = eventBuffer
45+
self.script.exports.watch_method_call(targetMethod, methodParamTypes)
46+
47+
return eventBuffer
48+
49+
def stopWatchingMethodCall(
50+
self, targetMethod: str, methodParamTypes: str
51+
) -> None:
52+
"""Stop tracking calls to the target method.
53+
54+
:param targetMethod: the target API
55+
:param methodParamTypes: the parameter types of the target API
56+
"""
57+
methodId = self._getMethodIdentifier(targetMethod, methodParamTypes)
58+
59+
if methodId in self.watchedMethods:
60+
del self.watchedMethods[methodId]
61+
62+
def handleCapturedEvent(self, eventWrapperFromFrida: dict, _) -> None:
63+
"""Send the event captured by Frida to the corresponding
64+
buffers.
65+
66+
:param eventWrapperFromFrida: python dict containing captured events
67+
"""
68+
if eventWrapperFromFrida["type"] == "error":
69+
errorDescription = eventWrapperFromFrida["description"]
70+
print(errorDescription, file=sys.stderr)
71+
return
72+
73+
methodCallEvent = json.loads(eventWrapperFromFrida["payload"])
74+
75+
eventType = methodCallEvent.get("type", None)
76+
77+
if eventType == "CallCaptured":
78+
methodId = tuple(methodCallEvent["identifier"][0:2])
79+
80+
if methodId in self.watchedMethods:
81+
messageBuffer = self.watchedMethods[methodId]
82+
messageBuffer.append(methodCallEvent)
83+
84+
elif eventType == "FailedToWatch":
85+
methodId = tuple(methodCallEvent["identifier"])
86+
self.watchedMethods.pop(methodId)
87+
88+
89+
@functools.lru_cache
90+
def _spawnApp(
91+
appPackageName: str, protocol="usb", **kwargs: Any
92+
) -> Tuple[Device, FridaSession, int]:
93+
"""Spawn the target APP with Frida
94+
95+
:param appPackageName: the package name of the target APP
96+
:param protocol: string that holds the protocol to communicate with the
97+
Frida server, defaults to "usb"
98+
:return: tuple containing the device ID, the Frida instance and the process
99+
ID of the APP.
100+
"""
101+
device = None
102+
if protocol == "usb":
103+
device = frida.get_usb_device(**kwargs)
104+
elif protocol == "local":
105+
device = frida.get_local_device(**kwargs)
106+
elif protocol == "remote":
107+
device = frida.get_remote_device(**kwargs)
108+
109+
processId = device.spawn([appPackageName])
110+
session = device.attach(processId)
111+
112+
return device, session, processId
113+
114+
115+
@functools.lru_cache
116+
def _injectAgent(frida: FridaSession) -> MethodCallEventDispatcher:
117+
"""Inject a Frida agent to help track method calls.
118+
119+
:param frida: Frida instance to be injected
120+
:return: dispatcher that stores the captured calls to the appropriate
121+
buffers
122+
"""
123+
dispatcher = MethodCallEventDispatcher(frida)
124+
125+
pathToFridaAgentSource = pkg_resources.resource_filename(
126+
"quark.script.frida", "agent.js"
127+
)
128+
129+
with open(pathToFridaAgentSource, "r") as fridaAgentSource:
130+
fridaAgent = dispatcher.frida.create_script(fridaAgentSource.read())
131+
fridaAgent.on("message", dispatcher.handleCapturedEvent)
132+
fridaAgent.load()
133+
dispatcher.script = fridaAgent
134+
135+
return dispatcher
136+
137+
138+
@dataclass
139+
class Behavior:
140+
_callEvent: MethodCallEvent
141+
142+
def hasString(self, pattern: str, regex: bool = False) -> List[str]:
143+
"""Check if the behavior contains strings
144+
145+
:param pattern: string to be checked
146+
:param regex: True if the string is a regular expression, defaults to
147+
False
148+
:return: python list containing all matched strings
149+
"""
150+
arguments = self.getParamValues()
151+
152+
allMatchedStrings = set()
153+
for argument in arguments:
154+
if regex:
155+
matchedStrings = [
156+
match.group(0) for match in re.finditer(pattern, argument)
157+
]
158+
allMatchedStrings.update(matchedStrings)
159+
else:
160+
if pattern in argument:
161+
return [pattern]
162+
163+
return list(allMatchedStrings)
164+
165+
def hasUrl(self) -> List[str]:
166+
"""Check if the behavior contains urls.
167+
168+
:return: python list containing all detected urls
169+
"""
170+
return self.hasString(URL_REGEX, True)
171+
172+
def getParamValues(self) -> List[str]:
173+
"""Get parameter values from behavior.
174+
175+
:return: python list containing parameter values
176+
"""
177+
return self._callEvent["paramValues"]
178+
179+
180+
@dataclass
181+
class FridaResult:
182+
_eventBuffer: List[MethodCallEvent]
183+
184+
@property
185+
def behaviorOccurList(self) -> List[Behavior]:
186+
"""List that stores instances of detected behavior in different part of
187+
the target file.
188+
189+
:return: detected behavior instance
190+
"""
191+
return [Behavior(message) for message in self._eventBuffer]
192+
193+
194+
def runFridaHook(
195+
apkPackageName: str,
196+
targetMethod: str,
197+
methodParamTypes: str,
198+
secondToWait: int = 10,
199+
) -> FridaResult:
200+
"""Track calls to the specified method for given seconds.
201+
202+
:param apkPackageName: the package name of the target APP
203+
:param targetMethod: the target API
204+
:param methodParamTypes: string that holds the parameters used by the
205+
target API
206+
:param secondToWait: seconds to wait for method calls, defaults to 10
207+
:return: FridaResult instance
208+
"""
209+
device, frida, appProcess = _spawnApp(apkPackageName)
210+
dispatcher = _injectAgent(frida)
211+
212+
eventBuffer = dispatcher.startWatchingMethodCall(
213+
targetMethod, methodParamTypes
214+
)
215+
device.resume(appProcess)
216+
217+
sleep(secondToWait)
218+
dispatcher.stopWatchingMethodCall(targetMethod, methodParamTypes)
219+
220+
return FridaResult(eventBuffer)

0 commit comments

Comments
 (0)