Skip to content

Commit aa155a8

Browse files
committed
Add hooking APIs based on Frida
1 parent 262de66 commit aa155a8

File tree

3 files changed

+478
-0
lines changed

3 files changed

+478
-0
lines changed

quark/script/frida/__init__.py

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

quark/script/frida/agent.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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+
/*global Java, send, rpc*/
6+
function replaceMethodImplementation(targetMethod, classAndMethodName, methodParamTypes, returnType) {
7+
targetMethod.implementation = function () {
8+
let callEvent = {
9+
"type": "CallCaptured",
10+
"identifier": [classAndMethodName, methodParamTypes, returnType],
11+
"paramValues": []
12+
};
13+
14+
for (const arg of arguments) {
15+
callEvent["paramValues"].push((arg || "(none)").toString());
16+
}
17+
18+
send(JSON.stringify(callEvent));
19+
return targetMethod.apply(this, arguments);
20+
};
21+
}
22+
23+
function watchMethodCall(classAndMethodName, methodParamTypes) {
24+
if (classAndMethodName == null || methodParamTypes == null) {
25+
return;
26+
}
27+
28+
const indexOfLastSeparator = classAndMethodName.lastIndexOf(".");
29+
const classNamePattern = classAndMethodName.substring(0, indexOfLastSeparator);
30+
const methodNamePattern = classAndMethodName.substring(indexOfLastSeparator + 1);
31+
32+
Java.perform(() => {
33+
const classOfTargetMethod = Java.use(classNamePattern);
34+
const possibleMethods = classOfTargetMethod[`${methodNamePattern}`];
35+
36+
if (typeof possibleMethods === "undefined") {
37+
const failedToWatchEvent = {
38+
"type": "FailedToWatch",
39+
"identifier": [classAndMethodName, methodParamTypes]
40+
};
41+
42+
send(JSON.stringify(failedToWatchEvent));
43+
return;
44+
}
45+
46+
possibleMethods.overloads.filter((possibleMethod) => {
47+
const paramTypesOfPossibleMethod = possibleMethod.argumentTypes.map((argument) => argument.className);
48+
return paramTypesOfPossibleMethod.join(",") === methodParamTypes;
49+
}).forEach((matchedMethod) => {
50+
const retType = matchedMethod.returnType.name
51+
replaceMethodImplementation(matchedMethod, classAndMethodName, methodParamTypes, retType)
52+
}
53+
);
54+
55+
});
56+
}
57+
58+
rpc.exports["watchMethodCall"] = (classAndMethodName, methodParamTypes) => watchMethodCall(classAndMethodName, methodParamTypes);

0 commit comments

Comments
 (0)