Skip to content
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: 14 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
FROM python:3.10-bullseye@sha256:02c7cb92b8f23908de6457f7800c93b84ed8c6e7201da7935443d4c5eca7b381
FROM python:3.12-bookworm@sha256:9bed8554e926c07c6f908841d5ee88c33e8df9236b191526bbce81a9062ab43a

# Update and package installation
RUN apt-get update && \
apt-get clean && \
apt-get install -y ca-certificates-java --no-install-recommends && \
apt-get install -y openjdk-11-jdk p11-kit wkhtmltopdf libqt5gui5 && \
apt-get install -y openjdk-17-jdk p11-kit wkhtmltopdf libqt5gui5 wget unzip && \
apt-get clean && \
update-ca-certificates -f

# Get JADX Tool
ENV JADX_VERSION 1.4.7
ENV JADX_VERSION 1.5.5

RUN \
wget "https://github.com/skylot/jadx/releases/download/v$JADX_VERSION/jadx-$JADX_VERSION.zip" && \
unzip "jadx-$JADX_VERSION.zip"
wget -q "https://github.com/skylot/jadx/releases/download/v$JADX_VERSION/jadx-$JADX_VERSION.zip" -O /tmp/jadx.zip && \
mkdir -p /opt/jadx && \
unzip -q /tmp/jadx.zip -d /opt/jadx && \
chmod 0755 /opt/jadx/bin/jadx /opt/jadx/bin/jadx-gui && \
rm /tmp/jadx.zip

ENV PATH="/opt/jadx/bin:${PATH}"

# Create user
ARG uid=1000
Expand All @@ -22,7 +27,10 @@ ARG user=app
ARG group=app

RUN groupadd -g ${gid} ${group} \
&& useradd -u ${uid} -g ${group} -s /bin/sh ${user}
&& useradd --create-home --home-dir /home/${user} \
-u ${uid} -g ${group} -s /bin/sh ${user}

ENV HOME=/home/${user}

# Copy entrypoints
COPY entrypoint/web_entrypoint.sh \
Expand Down
181 changes: 162 additions & 19 deletions app/analysis.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
from androguard.core.bytecodes.apk import APK
from androguard.core.bytecodes.dvm import DalvikVMFormat
from androguard.core.analysis.analysis import Analysis
from androguard.core.androconf import show_logging
from androguard.core.apk import APK
from django.conf import settings
import logging, os, threading, hashlib, re, linecache, base64, urllib
import logging, os, threading, hashlib, re, linecache, base64, urllib, shutil, signal, subprocess, tempfile
from app.models import *
from pygments import highlight
from pygments.lexers import PythonLexer
Expand Down Expand Up @@ -45,6 +42,7 @@ def analyze_apk(task, scan_id):
# Start the APK analysis
global APK_PATH
global DECOMPILE_PATH
scan = None
try:
scan = Scan.objects.get(pk=scan_id)
APK_PATH = settings.BASE_DIR + scan.apk.url
Expand Down Expand Up @@ -89,7 +87,7 @@ def analyze_apk(task, scan_id):
task.update_state(state = 'STARTED',
meta = {'current': scan.progress, 'total': 100, 'status': scan.status})
logger.debug(scan.status)
decompile_jadx()
decompile_jadx(APK_PATH, DECOMPILE_PATH)
if (a.get_app_icon()):
update_icon(scan, DECOMPILE_PATH + '/resources/' + a.get_app_icon())
scan.status = 'Finding vulnerabilities'
Expand All @@ -107,19 +105,164 @@ def analyze_apk(task, scan_id):
meta = {'current': scan.progress, 'total': 100, 'status': scan.status})
logger.debug(scan.status)
except Exception as e:
scan.progress = 100
scan.status = "Error"
scan.finished_on = datetime.now()
scan.save()
task.update_state(state = 'STARTED',
meta = {'current': scan.progress, 'total': 100, 'status': scan.status})
logger.error(e)
if scan is not None:
scan.progress = 100
scan.status = "Error"
scan.finished_on = datetime.now()
scan.save()
logger.exception(e)
raise


def _has_decompiled_output(output_path):
for directory in ('sources', 'resources'):
directory_path = os.path.join(output_path, directory)
if not os.path.isdir(directory_path):
continue
for _, _, files in os.walk(directory_path):
if files:
return True
return False


def _cleanup_decompile_output(output_path, marker_path):
shutil.rmtree(output_path, ignore_errors=True)
try:
os.remove(marker_path)
except FileNotFoundError:
pass


def _has_valid_completion_marker(marker_path):
try:
with open(marker_path, 'rb') as marker:
return marker.read() == b'complete\n'
except OSError:
return False


def _write_completion_marker(marker_path):
marker_directory = os.path.dirname(marker_path) or '.'
temporary_path = None
try:
with tempfile.NamedTemporaryFile(
mode='w',
encoding='utf-8',
dir=marker_directory,
prefix=os.path.basename(marker_path) + '.',
delete=False,
) as marker:
temporary_path = marker.name
marker.write('complete\n')
marker.flush()
os.fsync(marker.fileno())
os.replace(temporary_path, marker_path)
except Exception:
if temporary_path is not None:
try:
os.remove(temporary_path)
except FileNotFoundError:
pass
raise


def _capture_output_tail(stream, tail, limit=4000):
for chunk in iter(lambda: stream.read(4096), b''):
tail.extend(chunk)
if len(tail) > limit:
del tail[:-limit]


def _terminate_process_group(process):
try:
os.killpg(process.pid, signal.SIGTERM)
except ProcessLookupError:
return

try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
try:
os.killpg(process.pid, signal.SIGKILL)
except ProcessLookupError:
pass
process.wait()

def decompile_jadx(apk_path=None, output_path=None, timeout=None):
apk_path = apk_path or APK_PATH
output_path = output_path or DECOMPILE_PATH
timeout = timeout or getattr(settings, 'JADX_TIMEOUT_SECONDS', 900)
marker_path = output_path + '.jadx-complete'

if _has_valid_completion_marker(marker_path) and _has_decompiled_output(output_path):
return

_cleanup_decompile_output(output_path, marker_path)
command = ['jadx', '-d', output_path, apk_path]
output_tail = bytearray()
try:
process = subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
start_new_session=True,
)
output_reader = threading.Thread(
target=_capture_output_tail,
args=(process.stdout, output_tail),
daemon=True,
)
output_reader.start()
process.wait(timeout=timeout)
except subprocess.TimeoutExpired as exc:
_terminate_process_group(process)
output_reader.join(timeout=5)
_cleanup_decompile_output(output_path, marker_path)
error_output = bytes(output_tail[-4000:]).decode(
'utf-8',
errors='replace',
).strip()
detail = ': {}'.format(error_output) if error_output else ''
raise RuntimeError(
'JADX decompilation timed out after {} seconds{}'.format(
timeout,
detail,
)
) from exc
except OSError as exc:
if 'process' in locals():
_terminate_process_group(process)
_cleanup_decompile_output(output_path, marker_path)
raise RuntimeError('Unable to execute JADX: {}'.format(exc)) from exc

output_reader.join(timeout=5)
error_output = bytes(output_tail[-4000:]).decode(
'utf-8',
errors='replace',
).strip()
if process.returncode not in (0, 3):
_cleanup_decompile_output(output_path, marker_path)
raise RuntimeError(
'JADX decompilation failed with exit code {}: {}'.format(
process.returncode,
error_output or 'No error output',
)
)

if not _has_decompiled_output(output_path):
_cleanup_decompile_output(output_path, marker_path)
raise RuntimeError(
'JADX decompilation produced no source or resource files'
)

if process.returncode == 3:
logger.warning(
'JADX completed with partial decompilation (exit code 3); '
'continuing with usable output. Output tail: %s',
error_output or 'No error output',
)

def decompile_jadx():
if (not os.path.isdir(DECOMPILE_PATH)):
#execute jadx command
os.system('jadx -d {} {}'.format(DECOMPILE_PATH, APK_PATH))
# now we have sources/resources decompiled
_write_completion_marker(marker_path)

def update_icon(scan, path):
encoded_string = ''
Expand Down Expand Up @@ -432,4 +575,4 @@ def get_info_database(scan, path):
f = open(path)
table_info = f.read()
db = DatabaseInfo(scan=scan, table='None', info=table_info)
db.save()
db.save()
Loading
Loading