frooky 0.1.0__tar.gz → 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- frooky-0.1.2/.github/workflows/publish.yml +35 -0
- frooky-0.1.2/.github/workflows/sync-labels.yml +117 -0
- frooky-0.1.2/.gitignore +44 -0
- {frooky-0.1.0 → frooky-0.1.2}/PKG-INFO +64 -242
- frooky-0.1.2/README.md +113 -0
- frooky-0.1.2/docs/examples/example.md +189 -0
- frooky-0.1.2/docs/examples/hooks.json +12 -0
- frooky-0.1.2/docs/examples/hooks2.json +12 -0
- frooky-0.1.2/docs/examples/output.json +7 -0
- frooky-0.1.0/README.md → frooky-0.1.2/docs/usage.md +33 -32
- frooky-0.1.2/frooky/__init__.py +8 -0
- frooky-0.1.2/frooky/_version.py +34 -0
- {frooky-0.1.0 → frooky-0.1.2}/frooky/cli.py +13 -0
- {frooky-0.1.0 → frooky-0.1.2}/frooky/frida_runner.py +25 -0
- {frooky-0.1.0 → frooky-0.1.2}/frooky.egg-info/PKG-INFO +64 -242
- {frooky-0.1.0 → frooky-0.1.2}/frooky.egg-info/SOURCES.txt +9 -0
- {frooky-0.1.0 → frooky-0.1.2}/pyproject.toml +5 -2
- frooky-0.1.0/frooky/__init__.py +0 -4
- {frooky-0.1.0 → frooky-0.1.2}/LICENSE +0 -0
- {frooky-0.1.0 → frooky-0.1.2}/frooky/__main__.py +0 -0
- {frooky-0.1.0 → frooky-0.1.2}/frooky/android/android_decoder.js +0 -0
- {frooky-0.1.0 → frooky-0.1.2}/frooky/android/base_script.js +0 -0
- {frooky-0.1.0 → frooky-0.1.2}/frooky/android/native_decoder.js +0 -0
- {frooky-0.1.0 → frooky-0.1.2}/frooky/ios/base_script.js +0 -0
- {frooky-0.1.0 → frooky-0.1.2}/frooky/resources.py +0 -0
- {frooky-0.1.0 → frooky-0.1.2}/frooky.egg-info/dependency_links.txt +0 -0
- {frooky-0.1.0 → frooky-0.1.2}/frooky.egg-info/entry_points.txt +0 -0
- {frooky-0.1.0 → frooky-0.1.2}/frooky.egg-info/requires.txt +0 -0
- {frooky-0.1.0 → frooky-0.1.2}/frooky.egg-info/top_level.txt +0 -0
- {frooky-0.1.0 → frooky-0.1.2}/setup.cfg +0 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
with:
|
|
14
|
+
fetch-depth: 0 # Fetch all history and tags for setuptools-scm
|
|
15
|
+
- uses: actions/setup-python@v5
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.x"
|
|
18
|
+
- run: python -m pip install --upgrade build
|
|
19
|
+
- run: python -m build
|
|
20
|
+
- uses: actions/upload-artifact@v4
|
|
21
|
+
with:
|
|
22
|
+
name: dist
|
|
23
|
+
path: dist/*
|
|
24
|
+
|
|
25
|
+
publish:
|
|
26
|
+
needs: build
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
permissions:
|
|
29
|
+
id-token: write
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/download-artifact@v4
|
|
32
|
+
with:
|
|
33
|
+
name: dist
|
|
34
|
+
path: dist
|
|
35
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
name: Sync Labels from Issues to PRs
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [opened, edited, reopened, synchronize]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
sync-labels:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
pull-requests: write
|
|
12
|
+
issues: read
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- name: Sync labels from linked issues
|
|
16
|
+
uses: actions/github-script@v7
|
|
17
|
+
with:
|
|
18
|
+
script: |
|
|
19
|
+
const prNumber = context.payload.pull_request.number;
|
|
20
|
+
const prBody = context.payload.pull_request.body || '';
|
|
21
|
+
|
|
22
|
+
const issueNumbers = new Set();
|
|
23
|
+
|
|
24
|
+
// Pattern 1: Simple format - fixes #123, closes#456
|
|
25
|
+
const simpleRegex = /(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s*#(\d+)/gi;
|
|
26
|
+
const simpleMatches = [...prBody.matchAll(simpleRegex)];
|
|
27
|
+
simpleMatches.forEach(match => issueNumbers.add(parseInt(match[1])));
|
|
28
|
+
|
|
29
|
+
// Pattern 2: Owner/repo format - fixes owner/repo#123
|
|
30
|
+
const ownerRepoRegex = /(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s+([a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+)#(\d+)/gi;
|
|
31
|
+
const ownerRepoMatches = [...prBody.matchAll(ownerRepoRegex)];
|
|
32
|
+
ownerRepoMatches.forEach(match => {
|
|
33
|
+
const repoPath = match[1];
|
|
34
|
+
const issueNum = parseInt(match[2]);
|
|
35
|
+
// Only add if it's the same repository
|
|
36
|
+
if (repoPath === `${context.repo.owner}/${context.repo.repo}`) {
|
|
37
|
+
issueNumbers.add(issueNum);
|
|
38
|
+
} else {
|
|
39
|
+
console.log(`Skipping cross-repository issue: ${repoPath}#${issueNum}`);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Pattern 3: Full URL format - fixes https://github.com/owner/repo#123 or /issues/123
|
|
44
|
+
const urlRegex = /(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s+https?:\/\/github\.com\/([a-zA-Z0-9._-]+)\/([a-zA-Z0-9._-]+)(?:\/issues\/|#)(\d+)/gi;
|
|
45
|
+
const urlMatches = [...prBody.matchAll(urlRegex)];
|
|
46
|
+
urlMatches.forEach(match => {
|
|
47
|
+
const owner = match[1];
|
|
48
|
+
const repo = match[2];
|
|
49
|
+
const issueNum = parseInt(match[3]);
|
|
50
|
+
// Only add if it's the same repository
|
|
51
|
+
if (owner === context.repo.owner && repo === context.repo.repo) {
|
|
52
|
+
issueNumbers.add(issueNum);
|
|
53
|
+
} else {
|
|
54
|
+
console.log(`Skipping cross-repository issue: ${owner}/${repo}#${issueNum}`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const issueNumbersArray = Array.from(issueNumbers);
|
|
59
|
+
|
|
60
|
+
if (issueNumbersArray.length === 0) {
|
|
61
|
+
console.log('No linked issues found in PR description');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(`Found linked issues: ${issueNumbersArray.join(', ')}`);
|
|
66
|
+
|
|
67
|
+
// Collect all labels from linked issues
|
|
68
|
+
const allLabels = new Set();
|
|
69
|
+
|
|
70
|
+
for (const issueNumber of issueNumbersArray) {
|
|
71
|
+
try {
|
|
72
|
+
const { data: issue } = await github.rest.issues.get({
|
|
73
|
+
owner: context.repo.owner,
|
|
74
|
+
repo: context.repo.repo,
|
|
75
|
+
issue_number: issueNumber
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
console.log(`Issue #${issueNumber} has labels: ${issue.labels.map(l => l.name).join(', ')}`);
|
|
79
|
+
|
|
80
|
+
issue.labels.forEach(label => {
|
|
81
|
+
allLabels.add(label.name);
|
|
82
|
+
});
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.log(`Error fetching issue #${issueNumber}: ${error.message}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (allLabels.size === 0) {
|
|
89
|
+
console.log('No labels found on linked issues');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log(`Syncing labels to PR: ${Array.from(allLabels).join(', ')}`);
|
|
94
|
+
|
|
95
|
+
// Get current PR labels
|
|
96
|
+
const { data: currentPR } = await github.rest.pulls.get({
|
|
97
|
+
owner: context.repo.owner,
|
|
98
|
+
repo: context.repo.repo,
|
|
99
|
+
pull_number: prNumber
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const currentLabels = new Set(currentPR.labels.map(l => l.name));
|
|
103
|
+
|
|
104
|
+
// Add new labels from issues
|
|
105
|
+
const labelsToAdd = Array.from(allLabels).filter(label => !currentLabels.has(label));
|
|
106
|
+
|
|
107
|
+
if (labelsToAdd.length > 0) {
|
|
108
|
+
await github.rest.issues.addLabels({
|
|
109
|
+
owner: context.repo.owner,
|
|
110
|
+
repo: context.repo.repo,
|
|
111
|
+
issue_number: prNumber,
|
|
112
|
+
labels: labelsToAdd
|
|
113
|
+
});
|
|
114
|
+
console.log(`Added labels: ${labelsToAdd.join(', ')}`);
|
|
115
|
+
} else {
|
|
116
|
+
console.log('All labels already present on PR');
|
|
117
|
+
}
|
frooky-0.1.2/.gitignore
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Frooky local debug artifacts
|
|
2
|
+
tmp/
|
|
3
|
+
output.json
|
|
4
|
+
*.jsonl
|
|
5
|
+
|
|
6
|
+
# node
|
|
7
|
+
node_modules/
|
|
8
|
+
package.json
|
|
9
|
+
package-lock.json
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Python bytecode
|
|
13
|
+
__pycache__/
|
|
14
|
+
*.py[cod]
|
|
15
|
+
*$py.class
|
|
16
|
+
|
|
17
|
+
# Virtual environments
|
|
18
|
+
.venv/
|
|
19
|
+
venv/
|
|
20
|
+
env/
|
|
21
|
+
.env/
|
|
22
|
+
|
|
23
|
+
# Packaging/build outputs
|
|
24
|
+
build/
|
|
25
|
+
dist/
|
|
26
|
+
*.egg-info/
|
|
27
|
+
.eggs/
|
|
28
|
+
pip-wheel-metadata/
|
|
29
|
+
|
|
30
|
+
# Generated version file (created by setuptools-scm during build)
|
|
31
|
+
frooky/_version.py
|
|
32
|
+
|
|
33
|
+
# Test / type-check / lint caches
|
|
34
|
+
.pytest_cache/
|
|
35
|
+
.mypy_cache/
|
|
36
|
+
.ruff_cache/
|
|
37
|
+
.coverage
|
|
38
|
+
coverage.xml
|
|
39
|
+
htmlcov/
|
|
40
|
+
|
|
41
|
+
# IDE / OS
|
|
42
|
+
.DS_Store
|
|
43
|
+
.vscode/
|
|
44
|
+
.idea/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: frooky
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Frida-powered hook runner based on JSON hook files.
|
|
5
5
|
Author-email: Carlos Holguera <holguera.cybersec@gmail.com>, Stefan Bernhardsgrütter <stefan.bernhardsgruetter@redguard.ch>
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -711,283 +711,105 @@ Dynamic: license-file
|
|
|
711
711
|
|___/
|
|
712
712
|
```
|
|
713
713
|
|
|
714
|
-
|
|
714
|
+
`frooky` is a [Frida](https://www.frida.re/)-based dynamic analysis tool for Android and iOS apps based on JSON hook files.
|
|
715
|
+
|
|
716
|
+
[](https://pypi.python.org/pypi/frooky)
|
|
717
|
+
|
|
718
|
+
- Hook Java/Kotlin methods and native C/C++ functions
|
|
719
|
+
- Simple JSON hook file format
|
|
720
|
+
- Support for method overloads and stack trace capturing
|
|
721
|
+
- Argument capturing with various data types
|
|
722
|
+
- Filtering hooks by argument values or stack trace patterns
|
|
723
|
+
- Output events in JSON Lines format for easy processing
|
|
724
|
+
|
|
725
|
+
See more in [docs/usage.md](docs/usage.md).
|
|
715
726
|
|
|
716
727
|
## Installation
|
|
717
728
|
|
|
729
|
+
Simply install via pip and you'll get the `frooky` CLI tool:
|
|
730
|
+
|
|
718
731
|
```bash
|
|
719
|
-
|
|
732
|
+
pip3 install frooky
|
|
720
733
|
```
|
|
721
734
|
|
|
722
735
|
## Usage
|
|
723
736
|
|
|
737
|
+
Create a hook file (e.g., `hooks.json`) as described in [docs/usage.md](docs/usage.md), then run `frooky` with the desired options:
|
|
738
|
+
|
|
724
739
|
```bash
|
|
725
740
|
# Attach by app name
|
|
726
|
-
frooky -U -n "My App" hooks.json
|
|
741
|
+
frooky -U -n "My App" --platform android hooks.json
|
|
727
742
|
|
|
728
743
|
# Spawn and add multiple hook files (hooks are merged)
|
|
729
|
-
frooky -U -f com.example.app
|
|
744
|
+
frooky -U -f com.example.app --platform android storage.json crypto.json
|
|
730
745
|
```
|
|
731
746
|
|
|
732
747
|
See `frooky -h` for more options.
|
|
733
748
|
|
|
734
|
-
##
|
|
749
|
+
## Example
|
|
735
750
|
|
|
736
|
-
|
|
751
|
+
We'll use the OWASP MAS [MASTG-DEMO-0072](https://mas.owasp.org/MASTG/demos/android/MASVS-CRYPTO/MASTG-DEMO-0072/MASTG-DEMO-0072/) app to demonstrate hooking a cryptographic key generation method.
|
|
737
752
|
|
|
738
|
-
|
|
753
|
+
First you need to create a hook file, e.g., `crypto.json`:
|
|
739
754
|
|
|
740
755
|
```json
|
|
741
756
|
{
|
|
742
|
-
"category": "
|
|
757
|
+
"category": "CRYPTO",
|
|
743
758
|
"hooks": [
|
|
744
759
|
{
|
|
745
|
-
"class": "
|
|
746
|
-
"
|
|
760
|
+
"class": "android.security.keystore.KeyGenParameterSpec$Builder",
|
|
761
|
+
"method": "$init",
|
|
762
|
+
"maxFrames": 10
|
|
747
763
|
}
|
|
748
764
|
]
|
|
749
765
|
}
|
|
750
766
|
```
|
|
751
767
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
#### Simple Method Hook
|
|
755
|
-
|
|
756
|
-
```json
|
|
757
|
-
{
|
|
758
|
-
"class": "java.io.File",
|
|
759
|
-
"method": "exists"
|
|
760
|
-
}
|
|
761
|
-
```
|
|
762
|
-
|
|
763
|
-
#### Multiple Methods
|
|
764
|
-
|
|
765
|
-
```json
|
|
766
|
-
{
|
|
767
|
-
"class": "java.io.FileOutputStream",
|
|
768
|
-
"methods": ["write", "close", "flush"]
|
|
769
|
-
}
|
|
770
|
-
```
|
|
771
|
-
|
|
772
|
-
#### Method Overloads
|
|
773
|
-
|
|
774
|
-
Specify exact method signatures using `overloads`:
|
|
775
|
-
|
|
776
|
-
```json
|
|
777
|
-
{
|
|
778
|
-
"class": "java.io.FileOutputStream",
|
|
779
|
-
"method": "write",
|
|
780
|
-
"overloads": [
|
|
781
|
-
{ "args": ["[B"] },
|
|
782
|
-
{ "args": ["[B", "int", "int"] },
|
|
783
|
-
{ "args": ["int"] }
|
|
784
|
-
]
|
|
785
|
-
}
|
|
786
|
-
```
|
|
787
|
-
|
|
788
|
-
#### Stack Traces
|
|
789
|
-
|
|
790
|
-
Control stack trace depth with `maxFrames`:
|
|
791
|
-
|
|
792
|
-
```json
|
|
793
|
-
{
|
|
794
|
-
"class": "javax.crypto.Cipher",
|
|
795
|
-
"method": "doFinal",
|
|
796
|
-
"maxFrames": 10
|
|
797
|
-
}
|
|
798
|
-
```
|
|
799
|
-
|
|
800
|
-
### Native Hooks
|
|
801
|
-
|
|
802
|
-
Native hooks intercept C/C++ functions. Set `native: true` and specify the symbol.
|
|
803
|
-
|
|
804
|
-
#### Basic Native Hook
|
|
805
|
-
|
|
806
|
-
```json
|
|
807
|
-
{
|
|
808
|
-
"native": true,
|
|
809
|
-
"symbol": "open",
|
|
810
|
-
"module": "libc.so"
|
|
811
|
-
}
|
|
812
|
-
```
|
|
813
|
-
|
|
814
|
-
#### Argument Descriptors
|
|
815
|
-
|
|
816
|
-
Define how arguments should be captured:
|
|
817
|
-
|
|
818
|
-
```json
|
|
819
|
-
{
|
|
820
|
-
"native": true,
|
|
821
|
-
"symbol": "write",
|
|
822
|
-
"module": "libc.so",
|
|
823
|
-
"args": [
|
|
824
|
-
{ "name": "fd", "type": "int32" },
|
|
825
|
-
{ "name": "buf", "type": "bytes", "length": 256 },
|
|
826
|
-
{ "name": "count", "type": "int32" }
|
|
827
|
-
]
|
|
828
|
-
}
|
|
829
|
-
```
|
|
830
|
-
|
|
831
|
-
#### Dynamic Length from Another Argument
|
|
832
|
-
|
|
833
|
-
Use `lengthInArg` to read length from another argument:
|
|
834
|
-
|
|
835
|
-
```json
|
|
836
|
-
{
|
|
837
|
-
"native": true,
|
|
838
|
-
"symbol": "send",
|
|
839
|
-
"module": "libc.so",
|
|
840
|
-
"args": [
|
|
841
|
-
{ "name": "sockfd", "type": "int32" },
|
|
842
|
-
{ "name": "buf", "type": "bytes", "lengthInArg": 2 },
|
|
843
|
-
{ "name": "len", "type": "int32" },
|
|
844
|
-
{ "name": "flags", "type": "int32" }
|
|
845
|
-
]
|
|
846
|
-
}
|
|
847
|
-
```
|
|
848
|
-
|
|
849
|
-
#### Capture Return Values
|
|
850
|
-
|
|
851
|
-
Set `returnValue: true` on the last argument:
|
|
852
|
-
|
|
853
|
-
```json
|
|
854
|
-
{
|
|
855
|
-
"native": true,
|
|
856
|
-
"symbol": "read",
|
|
857
|
-
"module": "libc.so",
|
|
858
|
-
"args": [
|
|
859
|
-
{ "name": "fd", "type": "int32" },
|
|
860
|
-
{ "name": "buf", "type": "bytes", "lengthInArg": 2 },
|
|
861
|
-
{ "name": "count", "type": "int32" },
|
|
862
|
-
{ "name": "result", "type": "int32", "returnValue": true }
|
|
863
|
-
]
|
|
864
|
-
}
|
|
865
|
-
```
|
|
866
|
-
|
|
867
|
-
#### Outbound Parameters
|
|
868
|
-
|
|
869
|
-
Use `direction: "out"` for output parameters that should be read after the function returns:
|
|
768
|
+
Then run `frooky` with the hook file against your target app:
|
|
870
769
|
|
|
871
|
-
```
|
|
872
|
-
|
|
873
|
-
"native": true,
|
|
874
|
-
"symbol": "CCCrypt",
|
|
875
|
-
"module": "libcommonCrypto.dylib",
|
|
876
|
-
"args": [
|
|
877
|
-
{ "name": "op", "type": "int32" },
|
|
878
|
-
{ "name": "alg", "type": "int32" },
|
|
879
|
-
{ "name": "dataOut", "type": "bytes", "length": 256, "direction": "out" },
|
|
880
|
-
{ "name": "dataOutMoved", "type": "pointer", "direction": "out" }
|
|
881
|
-
]
|
|
882
|
-
}
|
|
770
|
+
```bash
|
|
771
|
+
frooky -U -n "MASTestApp" --platform android crypto.json
|
|
883
772
|
```
|
|
884
773
|
|
|
885
|
-
|
|
774
|
+
Output (pretty-printed for readability):
|
|
886
775
|
|
|
887
|
-
|
|
776
|
+
> Events are written to the output file in JSON Lines format (one JSON object per line, known as NDJSON). You can easily pretty-print it e.g. using `jq . output.json`.
|
|
888
777
|
|
|
889
778
|
```json
|
|
890
779
|
{
|
|
891
|
-
"
|
|
892
|
-
"
|
|
893
|
-
"
|
|
894
|
-
"
|
|
895
|
-
|
|
780
|
+
"id": "14535033-08ea-4063-897c-eacd4a885d8b",
|
|
781
|
+
"type": "hook",
|
|
782
|
+
"category": "CRYPTO",
|
|
783
|
+
"time": "2026-01-14T16:02:21.782Z",
|
|
784
|
+
"class": "android.security.keystore.KeyGenParameterSpec$Builder",
|
|
785
|
+
"method": "$init",
|
|
786
|
+
"instanceId": 35486102,
|
|
787
|
+
"stackTrace": [
|
|
788
|
+
"android.security.keystore.KeyGenParameterSpec$Builder.<init>(Native Method)",
|
|
789
|
+
"org.owasp.mastestapp.MastgTest.generateKey(MastgTest.kt:97)",
|
|
790
|
+
"org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:41)",
|
|
791
|
+
"org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)",
|
|
792
|
+
"org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)",
|
|
793
|
+
"org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)",
|
|
794
|
+
"java.lang.Thread.run(Thread.java:1012)"
|
|
795
|
+
],
|
|
796
|
+
"inputParameters": [
|
|
797
|
+
{
|
|
798
|
+
"declaredType": "java.lang.String",
|
|
799
|
+
"value": "MultiPurposeKey"
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
"declaredType": "int",
|
|
803
|
+
"value": 15
|
|
804
|
+
}
|
|
805
|
+
],
|
|
806
|
+
"returnValue": [
|
|
807
|
+
{
|
|
808
|
+
"declaredType": "void",
|
|
809
|
+
"value": "void"
|
|
810
|
+
}
|
|
896
811
|
]
|
|
897
812
|
}
|
|
898
813
|
```
|
|
899
814
|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
Only capture events when the call stack contains specific patterns:
|
|
903
|
-
|
|
904
|
-
```json
|
|
905
|
-
{
|
|
906
|
-
"native": true,
|
|
907
|
-
"symbol": "SSL_write",
|
|
908
|
-
"module": "libssl.so",
|
|
909
|
-
"filterEventsByStacktrace": ["com.example.network", "okhttp3"]
|
|
910
|
-
}
|
|
911
|
-
```
|
|
912
|
-
|
|
913
|
-
#### Debug Mode
|
|
914
|
-
|
|
915
|
-
Enable verbose logging for troubleshooting:
|
|
916
|
-
|
|
917
|
-
```json
|
|
918
|
-
{
|
|
919
|
-
"native": true,
|
|
920
|
-
"symbol": "problematic_function",
|
|
921
|
-
"module": "libfoo.so",
|
|
922
|
-
"debug": true
|
|
923
|
-
}
|
|
924
|
-
```
|
|
925
|
-
|
|
926
|
-
### Argument Types
|
|
927
|
-
|
|
928
|
-
| Type | Description |
|
|
929
|
-
|------|-------------|
|
|
930
|
-
| `string` | Null-terminated C string |
|
|
931
|
-
| `int32` | 32-bit signed integer |
|
|
932
|
-
| `uint32` | 32-bit unsigned integer |
|
|
933
|
-
| `int64` | 64-bit signed integer |
|
|
934
|
-
| `pointer` | Memory address |
|
|
935
|
-
| `bytes` | Raw bytes (requires `length` or `lengthInArg`) |
|
|
936
|
-
| `bool` | Boolean value |
|
|
937
|
-
| `double` | 64-bit floating point |
|
|
938
|
-
| `CFData` | iOS CFData object |
|
|
939
|
-
| `CFDictionary` | iOS CFDictionary object |
|
|
940
|
-
|
|
941
|
-
### iOS Objective-C Hooks
|
|
942
|
-
|
|
943
|
-
Hook Objective-C methods using `objClass` and `symbol`:
|
|
944
|
-
|
|
945
|
-
```json
|
|
946
|
-
{
|
|
947
|
-
"native": true,
|
|
948
|
-
"objClass": "NSURLSession",
|
|
949
|
-
"symbol": "dataTaskWithRequest:completionHandler:"
|
|
950
|
-
}
|
|
951
|
-
```
|
|
952
|
-
|
|
953
|
-
## Output Format
|
|
954
|
-
|
|
955
|
-
Events are written to the output file in JSON Lines format (one JSON object per line, know as NDJSON). You can easily pretty-print it e.g. using `jq . output.json`.
|
|
956
|
-
|
|
957
|
-
Example event (pretty-printed for clarity):
|
|
958
|
-
|
|
959
|
-
```json
|
|
960
|
-
{
|
|
961
|
-
"id": "0117229c-b034-4676-ba33-075fc27922ba",
|
|
962
|
-
"type": "hook",
|
|
963
|
-
"category": "STORAGE",
|
|
964
|
-
"time": "2026-01-18T16:17:25.470Z",
|
|
965
|
-
"class": "android.app.SharedPreferencesImpl$EditorImpl",
|
|
966
|
-
"method": "putString",
|
|
967
|
-
"instanceId": 268282727,
|
|
968
|
-
"stackTrace": [
|
|
969
|
-
"android.app.SharedPreferencesImpl$EditorImpl.putString(Native Method)",
|
|
970
|
-
"androidx.security.crypto.EncryptedSharedPreferences$Editor.putEncryptedObject(EncryptedSharedPreferences.java:389)",
|
|
971
|
-
...
|
|
972
|
-
],
|
|
973
|
-
"inputParameters": [
|
|
974
|
-
{
|
|
975
|
-
"declaredType": "java.lang.String",
|
|
976
|
-
"value": "AQMRC7OWD6/h1iJseuzJVrClpwKE8swB8gOrGnsdaN4="
|
|
977
|
-
},
|
|
978
|
-
{
|
|
979
|
-
"declaredType": "java.lang.String",
|
|
980
|
-
"value": "AX4R5MZu+J1p0U3hvKyuEnJDQopI+wupiSi8CAG8dzq0PU76NbbebjhqMtqCD7fFUy2SmmQuQVDlDrrj30d3GQes+PlD8HmRFszVTge039GQ"
|
|
981
|
-
}
|
|
982
|
-
],
|
|
983
|
-
"returnValue": [
|
|
984
|
-
{
|
|
985
|
-
"declaredType": "android.content.SharedPreferences$Editor",
|
|
986
|
-
"value": "<instance: android.content.SharedPreferences$Editor, $className: android.app.SharedPreferencesImpl$EditorImpl>",
|
|
987
|
-
"runtimeType": "android.app.SharedPreferencesImpl$EditorImpl",
|
|
988
|
-
"instanceId": "268282727",
|
|
989
|
-
"instanceToString": "android.app.SharedPreferencesImpl$EditorImpl@ffdab67"
|
|
990
|
-
}
|
|
991
|
-
]
|
|
992
|
-
}
|
|
993
|
-
```
|
|
815
|
+
See more in [docs/usage.md](docs/usage.md) and see a full example in [docs/examples/example.md](docs/examples/example.md).
|
frooky-0.1.2/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Frooky
|
|
2
|
+
|
|
3
|
+
```txt
|
|
4
|
+
___ ____
|
|
5
|
+
/ __\ / _ | _ _ _ _ _ _
|
|
6
|
+
/ _\ | (_) | / _ \ / _ \ | / / | | | |
|
|
7
|
+
/ / / / | | | (_) | (_) || < | |_| |
|
|
8
|
+
\/ /_/ |_| \___/ \___/ |_|\_\ \__, |
|
|
9
|
+
|___/
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`frooky` is a [Frida](https://www.frida.re/)-based dynamic analysis tool for Android and iOS apps based on JSON hook files.
|
|
13
|
+
|
|
14
|
+
[](https://pypi.python.org/pypi/frooky)
|
|
15
|
+
|
|
16
|
+
- Hook Java/Kotlin methods and native C/C++ functions
|
|
17
|
+
- Simple JSON hook file format
|
|
18
|
+
- Support for method overloads and stack trace capturing
|
|
19
|
+
- Argument capturing with various data types
|
|
20
|
+
- Filtering hooks by argument values or stack trace patterns
|
|
21
|
+
- Output events in JSON Lines format for easy processing
|
|
22
|
+
|
|
23
|
+
See more in [docs/usage.md](docs/usage.md).
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
Simply install via pip and you'll get the `frooky` CLI tool:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip3 install frooky
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
Create a hook file (e.g., `hooks.json`) as described in [docs/usage.md](docs/usage.md), then run `frooky` with the desired options:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Attach by app name
|
|
39
|
+
frooky -U -n "My App" --platform android hooks.json
|
|
40
|
+
|
|
41
|
+
# Spawn and add multiple hook files (hooks are merged)
|
|
42
|
+
frooky -U -f com.example.app --platform android storage.json crypto.json
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
See `frooky -h` for more options.
|
|
46
|
+
|
|
47
|
+
## Example
|
|
48
|
+
|
|
49
|
+
We'll use the OWASP MAS [MASTG-DEMO-0072](https://mas.owasp.org/MASTG/demos/android/MASVS-CRYPTO/MASTG-DEMO-0072/MASTG-DEMO-0072/) app to demonstrate hooking a cryptographic key generation method.
|
|
50
|
+
|
|
51
|
+
First you need to create a hook file, e.g., `crypto.json`:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"category": "CRYPTO",
|
|
56
|
+
"hooks": [
|
|
57
|
+
{
|
|
58
|
+
"class": "android.security.keystore.KeyGenParameterSpec$Builder",
|
|
59
|
+
"method": "$init",
|
|
60
|
+
"maxFrames": 10
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Then run `frooky` with the hook file against your target app:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
frooky -U -n "MASTestApp" --platform android crypto.json
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Output (pretty-printed for readability):
|
|
73
|
+
|
|
74
|
+
> Events are written to the output file in JSON Lines format (one JSON object per line, known as NDJSON). You can easily pretty-print it e.g. using `jq . output.json`.
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"id": "14535033-08ea-4063-897c-eacd4a885d8b",
|
|
79
|
+
"type": "hook",
|
|
80
|
+
"category": "CRYPTO",
|
|
81
|
+
"time": "2026-01-14T16:02:21.782Z",
|
|
82
|
+
"class": "android.security.keystore.KeyGenParameterSpec$Builder",
|
|
83
|
+
"method": "$init",
|
|
84
|
+
"instanceId": 35486102,
|
|
85
|
+
"stackTrace": [
|
|
86
|
+
"android.security.keystore.KeyGenParameterSpec$Builder.<init>(Native Method)",
|
|
87
|
+
"org.owasp.mastestapp.MastgTest.generateKey(MastgTest.kt:97)",
|
|
88
|
+
"org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:41)",
|
|
89
|
+
"org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)",
|
|
90
|
+
"org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)",
|
|
91
|
+
"org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)",
|
|
92
|
+
"java.lang.Thread.run(Thread.java:1012)"
|
|
93
|
+
],
|
|
94
|
+
"inputParameters": [
|
|
95
|
+
{
|
|
96
|
+
"declaredType": "java.lang.String",
|
|
97
|
+
"value": "MultiPurposeKey"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"declaredType": "int",
|
|
101
|
+
"value": 15
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
"returnValue": [
|
|
105
|
+
{
|
|
106
|
+
"declaredType": "void",
|
|
107
|
+
"value": "void"
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
See more in [docs/usage.md](docs/usage.md) and see a full example in [docs/examples/example.md](docs/examples/example.md).
|