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
54 changes: 54 additions & 0 deletions README_mobile_analyzer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Mobile App Analyzer

This tool analyzes Android APK files for potential security issues, including high-risk permissions, hardcoded secrets, and indicators of phishing or scams.

## Features

* **APK Decompilation:** Uses `apktool` to decompile the APK for source code analysis.
* **Permission Analysis:** Extracts and identifies high-risk permissions from the `AndroidManifest.xml`.
* **Secret Scanning:** Scans the decompiled code for hardcoded secrets like API keys and private keys.
* **Scam Detection:** Analyzes text content in the app's files for suspicious URLs, phishing keywords, and other scam indicators.

## Prerequisites

* Python 3.x
* `apktool`: Must be installed and available in your system's PATH.
* `aapt`: Must be installed and available in your system's PATH.

You can typically install these tools on a Debian-based system (like Ubuntu) with:
`sudo apt-get install apktool aapt`

## How to Run

1. Navigate to the root directory of this repository.
2. Run the analyzer from your terminal, passing the path to the APK file you want to analyze:

```bash
python mobile_analyzer_main.py /path/to/your/app.apk
```

### Options

* `--keep-files`: Use this flag to prevent the script from deleting the `decompiled_apk` directory after the analysis is complete. This is useful for debugging or manual inspection.

```bash
python mobile_analyzer_main.py /path/to/your/app.apk --keep-files
```

## How to Interpret the Output

The tool will print a report to the console with the following sections:

* **High-Risk Permissions Found:** A list of permissions that could potentially be abused to access sensitive user data or control the device.
* **Potential Secrets Found:** A list of files that may contain hardcoded sensitive data. Review these files carefully.
* **Scam Indicators Found:** A list of files containing suspicious URLs, keywords, or other patterns that might indicate phishing or other scams.

## Limitations

* **Android Only:** This tool currently only supports Android APK files. iOS app analysis is not supported.
* **Static Analysis Only:** The analysis is purely static (it only examines the code and files). It does not run the app or monitor its behavior at runtime.
* **Not Foolproof:** This tool uses patterns and heuristics to find potential issues. It is not guaranteed to find all vulnerabilities, and it may produce false positives. Always use your judgment and, if possible, combine this with other security testing methods.

## Disclaimer

This tool is for educational and research purposes only. The user is responsible for any use of this tool. Do not use it to analyze apps for which you do not have permission.
Empty file added mobile_analyzer/__init__.py
Empty file.
192 changes: 192 additions & 0 deletions mobile_analyzer/analyzer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import subprocess
import os
import re
import sys

# HACK: This is not ideal, but the project is structured as a collection of
# top-level scripts and not as a single installable package. This allows us
# to import modules from sibling directories.
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))

from sensitive_data_scanner.scanner import scan_directory as scan_for_secrets
from social_media_analyzer.scam_detector import analyze_text_for_scams


# Based on Android documentation and security best practices.
# These permissions grant access to sensitive user data or system control.
HIGH_RISK_PERMISSIONS = [
"android.permission.READ_CALENDAR",
"android.permission.WRITE_CALENDAR",
"android.permission.CAMERA",
"android.permission.READ_CONTACTS",
"android.permission.WRITE_CONTACTS",
"android.permission.GET_ACCOUNTS",
"android.permission.ACCESS_FINE_LOCATION",
"android.permission.ACCESS_COARSE_LOCATION",
"android.permission.RECORD_AUDIO",
"android.permission.READ_PHONE_STATE",
"android.permission.READ_PHONE_NUMBERS",
"android.permission.CALL_PHONE",
"android.permission.ANSWER_PHONE_CALLS",
"android.permission.ADD_VOICEMAIL",
"android.permission.USE_SIP",
"android.permission.PROCESS_OUTGOING_CALLS",
"android.permission.READ_CALL_LOG",
"android.permission.WRITE_CALL_LOG",
"com.android.voicemail.permission.ADD_VOICEMAIL",
"android.permission.BODY_SENSORS",
"android.permission.SEND_SMS",
"android.permission.RECEIVE_SMS",
"android.permission.READ_SMS",
"android.permission.RECEIVE_WAP_PUSH",
"android.permission.RECEIVE_MMS",
"android.permission.READ_EXTERNAL_STORAGE",
"android.permission.WRITE_EXTERNAL_STORAGE",
"android.permission.SYSTEM_ALERT_WINDOW",
"android.permission.WRITE_SETTINGS",
"android.permission.REQUEST_INSTALL_PACKAGES",
"android.permission.ACCESS_BACKGROUND_LOCATION",
]

def decompile_apk(apk_path, output_dir):
"""
Decompiles an APK file using apktool.

Args:
apk_path (str): The path to the APK file.
output_dir (str): The directory to store the decompiled code.

Returns:
bool: True if decompilation was successful, False otherwise.
"""
if not os.path.exists(apk_path):
print(f"Error: APK file not found at {apk_path}")
return False

print(f"Decompiling {apk_path} to {output_dir}...")
try:
# Using -f to force overwrite the output directory if it exists
command = ["apktool", "d", "-f", apk_path, "-o", output_dir]
result = subprocess.run(command, capture_output=True, text=True, check=True)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

Source: opengrep

print("Decompilation successful.")
return True
except subprocess.CalledProcessError as e:
print("Error during decompilation:")
print(e.stderr)
return False
except FileNotFoundError:
print("Error: 'apktool' not found. Make sure it is installed and in your PATH.")
return False
Comment on lines +70 to +79
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): No timeout specified for subprocess calls.

Subprocesses may hang; add a timeout to subprocess.run for reliability.

Suggested change
result = subprocess.run(command, capture_output=True, text=True, check=True)
print("Decompilation successful.")
return True
except subprocess.CalledProcessError as e:
print("Error during decompilation:")
print(e.stderr)
return False
except FileNotFoundError:
print("Error: 'apktool' not found. Make sure it is installed and in your PATH.")
return False
# Set a timeout (e.g., 60 seconds) to prevent hanging
try:
result = subprocess.run(command, capture_output=True, text=True, check=True, timeout=60)
print("Decompilation successful.")
return True
except subprocess.TimeoutExpired:
print("Error: Decompilation process timed out.")
return False
except subprocess.CalledProcessError as e:
print("Error during decompilation:")
print(e.stderr)
return False
except FileNotFoundError:
print("Error: 'apktool' not found. Make sure it is installed and in your PATH.")
return False


def get_permissions(apk_path):
"""
Extracts permissions from an APK's AndroidManifest.xml using aapt.

Args:
apk_path (str): The path to the APK file.

Returns:
list: A list of permissions found in the APK.
"""
if not os.path.exists(apk_path):
print(f"Error: APK file not found at {apk_path}")
return []

print(f"Extracting permissions from {apk_path}...")
try:
command = ["aapt", "dump", "permissions", apk_path]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Regex for permission extraction may miss edge cases.

The current regex may fail if the aapt output format changes. Please update the regex to handle variations in whitespace or formatting.

Suggested implementation:

    print(f"Extracting permissions from {apk_path}...")
    try:
        command = ["aapt", "dump", "permissions", apk_path]
        result = subprocess.run(command, capture_output=True, text=True, check=True)
        # Updated regex to handle variations in whitespace and formatting
        import re
        permission_pattern = re.compile(r'^\s*permission(?:\s*:\s*|\s+)([\w\.\-]+)', re.MULTILINE)
        permissions = permission_pattern.findall(result.stdout)

If the function is expected to return the list of permissions, ensure you add return permissions at the appropriate place after extracting them. If there is existing code that processes the output, update it to use the new permissions variable.

result = subprocess.run(command, capture_output=True, text=True, check=True)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

Source: opengrep


# Regex to find package permissions
permissions = re.findall(r"uses-permission: name='([^']*)'", result.stdout)
print(f"Found {len(permissions)} permissions.")
return permissions
except subprocess.CalledProcessError as e:
print("Error extracting permissions:")
print(e.stderr)
return []
except FileNotFoundError:
print("Error: 'aapt' not found. Make sure it is installed and in your PATH.")
return []

def check_high_risk_permissions(permissions):
"""
Checks a list of permissions against the high-risk permissions list.

Args:
permissions (list): The list of permissions from an APK.

Returns:
list: A list of high-risk permissions found in the APK.
"""
found_high_risk = []
for perm in permissions:
if perm in HIGH_RISK_PERMISSIONS:
found_high_risk.append(perm)

Comment on lines +122 to +126
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Convert for loop into list comprehension (list-comprehension)

Suggested change
found_high_risk = []
for perm in permissions:
if perm in HIGH_RISK_PERMISSIONS:
found_high_risk.append(perm)
found_high_risk = [
perm for perm in permissions if perm in HIGH_RISK_PERMISSIONS
]

print(f"Found {len(found_high_risk)} high-risk permissions.")
return found_high_risk


def scan_for_sensitive_data(decompiled_dir):
"""
Scans a directory for sensitive data using the sensitive_data_scanner module.

Args:
decompiled_dir (str): The path to the directory of decompiled code.

Returns:
dict: A dictionary of findings.
"""
print("\nScanning for sensitive data...")
findings = scan_for_secrets(decompiled_dir)
if findings:
print(f"Found sensitive data in {len(findings)} files.")
else:
print("No sensitive data found.")
return findings

def scan_for_scam_indicators(decompiled_dir):
"""
Scans files in a directory for scam indicators using the scam_detector module.

Args:
decompiled_dir (str): The path to the directory of decompiled code.

Returns:
dict: A dictionary of findings, where keys are file paths.
"""
print("\nScanning for scam indicators (phishing, suspicious URLs)...")
all_findings = {}

# Extensions of text-like files to scan.
# Smali, xml, and yml are common in decompiled APKs.
scan_extensions = {'.smali', '.xml', '.yml', '.yaml', '.json', '.html', '.js', '.txt'}

for root, _, files in os.walk(decompiled_dir):
for filename in files:
# Check if the file has one of the scannable extensions
if not any(filename.endswith(ext) for ext in scan_extensions):
continue

filepath = os.path.join(root, filename)
try:
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
# Skip very large files to avoid performance issues
if len(content) > 1000000: # 1MB limit
continue

analysis_result = analyze_text_for_scams(content)
if analysis_result.get("indicators_found"):
all_findings[filepath] = analysis_result
except Exception:
# Ignore files that can't be read for any reason
continue

if all_findings:
print(f"Found scam indicators in {len(all_findings)} files.")
else:
print("No scam indicators found.")

return all_findings
77 changes: 77 additions & 0 deletions mobile_analyzer_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import argparse
import os
import shutil
import sys
from mobile_analyzer import analyzer

def main():
parser = argparse.ArgumentParser(description="Analyze an Android APK for security vulnerabilities.")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (code-quality): Use named expression to simplify assignment and conditional [×4] (use-named-expression)

parser.add_argument("apk_path", help="The path to the APK file to analyze.")
parser.add_argument("--keep-files", action="store_true", help="Keep the decompiled files after analysis.")
args = parser.parse_args()

apk_path = args.apk_path
if not os.path.exists(apk_path):
print(f"Error: APK file not found at {apk_path}")
sys.exit(1)

# Create a directory for the decompiled code
output_dir = "decompiled_apk"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Hardcoded output directory may cause conflicts.

Allow users to specify the output directory or generate a unique temporary directory to avoid conflicts when running multiple analyses or if the directory already exists.

Suggested implementation:

    import tempfile

    parser.add_argument("apk_path", help="The path to the APK file to analyze.")
    parser.add_argument("--keep-files", action="store_true", help="Keep the decompiled files after analysis.")
    parser.add_argument("--output-dir", type=str, default=None, help="Directory to store decompiled files. If not specified, a temporary directory will be used.")
    args = parser.parse_args()

    apk_path = args.apk_path
    if not os.path.exists(apk_path):
        print(f"Error: APK file not found at {apk_path}")
        sys.exit(1)

    # Create a directory for the decompiled code
    if args.output_dir:
        output_dir = args.output_dir
        if os.path.exists(output_dir):
            shutil.rmtree(output_dir)
        os.makedirs(output_dir)
    else:
        output_dir = tempfile.mkdtemp(prefix="decompiled_apk_")
    print(f"--- Starting analysis for {os.path.basename(apk_path)} ---")
    print(f"Decompiled code will be stored in: {output_dir}")

    # 1. Decompile the APK
    if not analyzer.decompile_apk(apk_path, output_dir):
        print("Failed to decompile APK. Aborting analysis.")
        sys.exit(1)

If there is code later in the file that deletes the output directory (e.g., when --keep-files is not set), ensure it works with both user-specified and temporary directories. You may want to print a message to the user if a temporary directory is used and files are deleted.

if os.path.exists(output_dir):
shutil.rmtree(output_dir)
os.makedirs(output_dir)
Comment on lines +20 to +22
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Potential race condition when removing and creating output directory.

Another process could create the directory between its removal and recreation, causing issues. Consider using tempfile.TemporaryDirectory or an atomic approach.

Suggested implementation:

    import tempfile

    # Create a temporary directory for the decompiled code
    with tempfile.TemporaryDirectory(prefix="decompiled_apk_") as output_dir:
        print(f"--- Starting analysis for {os.path.basename(apk_path)} ---")

        # 1. Decompile the APK
        if not analyzer.decompile_apk(apk_path, output_dir):
            print("Failed to decompile APK. Aborting analysis.")
            sys.exit(1)

You will need to indent all code that uses output_dir so it is inside the with tempfile.TemporaryDirectory(...) as output_dir: block.
If you want to support the --keep-files option, you will need to add logic to copy the temporary directory to a permanent location if requested.


print(f"--- Starting analysis for {os.path.basename(apk_path)} ---")

# 1. Decompile the APK
if not analyzer.decompile_apk(apk_path, output_dir):
print("Failed to decompile APK. Aborting analysis.")
sys.exit(1)

# 2. Analyze permissions
print("\n--- Analyzing Permissions ---")
all_permissions = analyzer.get_permissions(apk_path)
if all_permissions:
high_risk_permissions = analyzer.check_high_risk_permissions(all_permissions)
if high_risk_permissions:
print("\n[!] High-Risk Permissions Found:")
for perm in high_risk_permissions:
print(f" - {perm}")
else:
print("\nNo high-risk permissions found.")
else:
print("\nCould not extract permissions.")

# 3. Scan for sensitive data
print("\n--- Scanning for Hardcoded Secrets ---")
sensitive_data = analyzer.scan_for_sensitive_data(output_dir)
if sensitive_data:
print("\n[!] Potential Secrets Found:")
for file, findings in sensitive_data.items():
print(f" - In file: {file}")
for finding_type, matches in findings.items():
print(f" - {finding_type}: {len(matches)} found")
else:
print("\nNo hardcoded secrets found.")

# 4. Scan for scam indicators
print("\n--- Scanning for Phishing and Scam Indicators ---")
scam_indicators = analyzer.scan_for_scam_indicators(output_dir)
if scam_indicators:
print("\n[!] Scam Indicators Found:")
for file, result in scam_indicators.items():
print(f" - In file: {file}")
for indicator in result["indicators_found"]:
print(f" - {indicator}")
else:
print("\nNo scam indicators found.")

# Clean up the decompiled files
if not args.keep_files:
print("\nCleaning up temporary files...")
shutil.rmtree(output_dir)

print("\n--- Analysis Complete ---")
Comment on lines +69 to +74
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): No error handling for cleanup step.

Wrap shutil.rmtree in a try-except block to handle potential exceptions if the directory is locked or files are in use.

Suggested change
# Clean up the decompiled files
if not args.keep_files:
print("\nCleaning up temporary files...")
shutil.rmtree(output_dir)
print("\n--- Analysis Complete ---")
# Clean up the decompiled files
if not args.keep_files:
print("\nCleaning up temporary files...")
try:
shutil.rmtree(output_dir)
except Exception as e:
print(f"Warning: Failed to clean up temporary files in '{output_dir}'. Reason: {e}")
print("\n--- Analysis Complete ---")


if __name__ == "__main__":
main()
Loading