Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New module: jadx #1932

Merged
merged 14 commits into from
Nov 13, 2024
Merged
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
20 changes: 15 additions & 5 deletions bbot/modules/apkpure.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
from pathlib import Path
from bbot.modules.base import BaseModule

Expand Down Expand Up @@ -45,9 +46,18 @@ async def download_apk(self, app_id):
path = None
url = f"https://d.apkpure.com/b/XAPK/{app_id}?version=latest"
self.helpers.mkdir(self.output_dir / app_id)
file_destination = self.output_dir / app_id / f"{app_id}.xapk"
result = await self.helpers.download(url, warn=False, filename=file_destination)
if result:
self.info(f'Downloaded "{app_id}" from "{url}", saved to {file_destination}')
path = file_destination
response = await self.helpers.request(url, allow_redirects=True)
if response:
attachment = response.headers.get("Content-Disposition", "")
if "filename" in attachment:
match = re.search(r'filename="?([^"]+)"?', attachment)
if match:
filename = match.group(1)
extension = filename.split(".")[-1]
content = response.content
file_destination = self.output_dir / app_id / f"{app_id}.{extension}"
with open(file_destination, "wb") as f:
f.write(content)
self.info(f'Downloaded "{app_id}" from "{url}", saved to {file_destination}')
path = file_destination
return path
111 changes: 111 additions & 0 deletions bbot/modules/jadx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from pathlib import Path
from subprocess import CalledProcessError
from bbot.modules.internal.base import BaseModule


class jadx(BaseModule):
watched_events = ["FILESYSTEM"]
produced_events = ["FILESYSTEM"]
flags = ["passive", "safe"]
meta = {
"description": "Decompile APKs and XAPKs using JADX",
"created_date": "2024-11-04",
"author": "@domwhewell-sage",
}
options = {
"threads": 4,
}
options_desc = {
"threads": "Maximum jadx threads for extracting apk's, default: 4",
}
deps_ansible = [
{
"name": "Install latest JRE (Debian)",
"package": {"name": ["default-jre"], "state": "present"},
"become": True,
"when": "ansible_facts['os_family'] == 'Debian'",
},
{
"name": "Install latest JRE (Arch)",
"package": {"name": ["jre-openjdk"], "state": "present"},
"become": True,
"when": "ansible_facts['os_family'] == 'Archlinux'",
},
{
"name": "Install latest JRE (Fedora)",
"package": {"name": ["which", "java-latest-openjdk-headless"], "state": "present"},
"become": True,
"when": "ansible_facts['os_family'] == 'RedHat'",
},
{
"name": "Install latest JRE (Alpine)",
"package": {"name": ["openjdk11"], "state": "present"},
"become": True,
"when": "ansible_facts['os_family'] == 'Alpine'",
},
{
"name": "Create jadx directory",
"file": {"path": "#{BBOT_TOOLS}/jadx", "state": "directory", "mode": "0755"},
},
{
"name": "Download jadx",
"unarchive": {
"src": "https://github.com/skylot/jadx/releases/download/v1.5.0/jadx-1.5.0.zip",
"include": ["lib/jadx-1.5.0-all.jar", "bin/jadx"],
"dest": "#{BBOT_TOOLS}/jadx",
"remote_src": True,
},
},
]

allowed_file_types = ["java archive", "android application package"]

async def setup(self):
self.threads = self.config.get("threads", 4)
return True

async def filter_event(self, event):
if "file" in event.tags:
if not event.data["magic_description"].lower() in self.allowed_file_types:
return False, f"Jadx is not able to decompile this file type: {event.data['magic_description']}"
else:
return False, "Event is not a file"
return True

async def handle_event(self, event):
path = Path(event.data["path"])
output_dir = path.parent / path.name.replace(".", "_")
self.helpers.mkdir(output_dir)
success = await self.decompile_apk(path, output_dir)

# If jadx was able to decompile the java archive, emit an event
if success:
await self.emit_event(
{"path": str(output_dir)},
"FILESYSTEM",
tags="folder",
parent=event,
context=f'extracted "{path}" to: {output_dir}',
)
else:
output_dir.rmdir()

async def decompile_apk(self, path, output_dir):
command = [
f"{self.scan.helpers.tools_dir}/jadx/bin/jadx",
"--threads-count",
self.threads,
"--output-dir",
str(output_dir),
str(path),
]
try:
output = await self.run_process(command, check=True)
except CalledProcessError as e:
self.warning(f"Error decompiling {path}. STDERR: {repr(e.stderr)}")
return False
if not Path(output_dir / "resources").exists() and not Path(output_dir / "sources").exists():
self.warning(f"JADX was unable to decompile {path}.")
self.warning(output)
return False
return True
10 changes: 6 additions & 4 deletions bbot/test/test_step_2/module_tests/test_module_apkpure.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ async def setup_after_prep(self, module_test):
module_test.httpx_mock.add_response(
url="https://d.apkpure.com/b/XAPK/com.bbot.test?version=latest",
content=self.apk_file,
headers={
"Content-Type": "application/vnd.android.package-archive",
"Content-Disposition": "attachment; filename=com.bbot.test.apk",
},
)

def check(self, module_test, events):
Expand All @@ -61,9 +65,7 @@ def check(self, module_test, events):
and e.data["url"] == "https://play.google.com/store/apps/details?id=com.bbot.test"
]
), "Failed to find bbot android app"
filesystem_event = [
e for e in events if e.type == "FILESYSTEM" and "com.bbot.test.xapk" in e.data["path"] and "apk" in e.tags
]
filesystem_event = [e for e in events if e.type == "FILESYSTEM" and "com.bbot.test.apk" in e.data["path"]]
assert 1 == len(filesystem_event), "Failed to download apk"
file = Path(filesystem_event[0].data["path"])
assert file.is_file(), "Destination xapk doesn't exist"
assert file.is_file(), "Destination apk doesn't exist"
55 changes: 55 additions & 0 deletions bbot/test/test_step_2/module_tests/test_module_jadx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from pathlib import Path
from bbot.core.helpers.libmagic import get_magic_info
from bbot.test.test_step_2.module_tests.base import ModuleTestBase, tempapkfile


class TestJadx(ModuleTestBase):
modules_overrides = ["apkpure", "google_playstore", "speculate", "jadx"]
apk_file = tempapkfile()

async def setup_after_prep(self, module_test):
await module_test.mock_dns({"blacklanternsecurity.com": {"A": ["127.0.0.99"]}})
module_test.httpx_mock.add_response(
url="https://play.google.com/store/search?q=blacklanternsecurity&c=apps",
text="""<!DOCTYPE html>
<html>
<head>
<title>"blacklanternsecurity" - Android Apps on Google Play</title>
</head>
<body>
<a href="/store/apps/details?id=com.bbot.test&pcampaignid=dontmatchme&pli=1"/>
</body>
</html>""",
)
module_test.httpx_mock.add_response(
url="https://play.google.com/store/apps/details?id=com.bbot.test",
text="""<!DOCTYPE html>
<html>
<head>
<title>BBOT</title>
</head>
<body>
<meta name="appstore:developer_url" content="https://www.blacklanternsecurity.com">
</div>
</div>
</body>
</html>""",
)
module_test.httpx_mock.add_response(
url="https://d.apkpure.com/b/XAPK/com.bbot.test?version=latest",
content=self.apk_file,
headers={
"Content-Type": "application/vnd.android.package-archive",
"Content-Disposition": "attachment; filename=com.bbot.test.apk",
},
)

def check(self, module_test, events):
filesystem_events = [e for e in events if e.type == "FILESYSTEM"]
apk_event = [e for e in filesystem_events if "file" in e.tags]
extension, mime_type, description, confidence = get_magic_info(apk_event[0].data["path"])
assert description == "Android Application Package", f"Downloaded file was detected as {description}"
extract_event = [e for e in filesystem_events if "folder" in e.tags]
assert 1 == len(extract_event), "Failed to extract apk"
extract_path = Path(extract_event[0].data["path"])
assert extract_path.is_dir(), "Destination apk doesn't exist"
Loading