├── src ├── card_generators.go ├── doppelganger_assistant.png ├── Makefile ├── status_writer.go ├── main.go ├── card_simulators.go ├── card_handlers.go ├── card_verifier.go ├── card_writer.go ├── utils.go └── hotel_recovery.go ├── img ├── assistant_gui.png ├── doppelganger_dm.png ├── doppelganger_pm3.ico ├── doppelganger_pm3.png ├── doppelganger_assistant.ico └── doppelganger_assistant.png ├── .gitignore ├── .github └── workflows │ ├── docker-build.yml │ └── build.yml ├── Dockerfile ├── Makefile ├── scripts ├── wsl_enable.ps1 ├── usb_reconnect.ps1 ├── uninstall.ps1 ├── proxmark_flash.ps1 ├── wsl_doppelganger_install.sh ├── wsl_pm3_terminal.ps1 └── wsl_windows_launch.ps1 ├── installers ├── doppelganger_install_macos.sh ├── doppelganger_install_windows.ps1 └── doppelganger_install_linux.sh ├── LICENSE └── README.md /src/card_generators.go: -------------------------------------------------------------------------------- 1 | package main 2 | -------------------------------------------------------------------------------- /img/assistant_gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tweathers-sec/doppelganger_assistant/HEAD/img/assistant_gui.png -------------------------------------------------------------------------------- /img/doppelganger_dm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tweathers-sec/doppelganger_assistant/HEAD/img/doppelganger_dm.png -------------------------------------------------------------------------------- /img/doppelganger_pm3.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tweathers-sec/doppelganger_assistant/HEAD/img/doppelganger_pm3.ico -------------------------------------------------------------------------------- /img/doppelganger_pm3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tweathers-sec/doppelganger_assistant/HEAD/img/doppelganger_pm3.png -------------------------------------------------------------------------------- /img/doppelganger_assistant.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tweathers-sec/doppelganger_assistant/HEAD/img/doppelganger_assistant.ico -------------------------------------------------------------------------------- /img/doppelganger_assistant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tweathers-sec/doppelganger_assistant/HEAD/img/doppelganger_assistant.png -------------------------------------------------------------------------------- /src/doppelganger_assistant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tweathers-sec/doppelganger_assistant/HEAD/src/doppelganger_assistant.png -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | PREFIX ?= /usr/local 2 | BINDIR = $(PREFIX)/bin 3 | 4 | install: 5 | @echo "Installing doppelganger_assistant to $(BINDIR)" 6 | @mkdir -p $(BINDIR) 7 | @cp doppelganger_assistant $(BINDIR)/ 8 | @chmod +x $(BINDIR)/doppelganger_assistant 9 | @echo "Installation complete" 10 | 11 | uninstall: 12 | @echo "Removing doppelganger_assistant from $(BINDIR)" 13 | @rm -f $(BINDIR)/doppelganger_assistant 14 | @echo "Uninstallation complete" 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | 18 | # Go workspace file 19 | go.work 20 | go.work.sum 21 | go.mod 22 | go.sum 23 | Icon.png 24 | .DS_Store 25 | build/ 26 | doppelganger_assistant 27 | 28 | dist/ 29 | fyne-cross/ 30 | proxmark3/ -------------------------------------------------------------------------------- /.github/workflows/docker-build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push Docker Image 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build-and-push: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | packages: write 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v2 19 | 20 | - name: Log in to GitHub Container Registry 21 | uses: docker/login-action@v1 22 | with: 23 | registry: ghcr.io 24 | username: ${{ github.actor }} 25 | password: ${{ secrets.GITHUB_TOKEN }} 26 | 27 | - name: Build and push Docker image 28 | uses: docker/build-push-action@v2 29 | with: 30 | context: . 31 | push: true 32 | tags: | 33 | ghcr.io/${{ github.repository }}:latest 34 | ghcr.io/${{ github.repository }}:${{ github.sha }} -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use Ubuntu as the base image 2 | FROM ubuntu:24.04 3 | 4 | # Avoid prompts from apt 5 | ENV DEBIAN_FRONTEND=noninteractive 6 | 7 | # Install necessary packages 8 | RUN apt-get update && apt-get install -y \ 9 | wget \ 10 | apt-utils \ 11 | curl \ 12 | git \ 13 | make \ 14 | xz-utils \ 15 | gcc \ 16 | g++ \ 17 | libgl1 \ 18 | xterm \ 19 | libreadline-dev \ 20 | gcc-arm-none-eabi \ 21 | libnewlib-dev \ 22 | qtbase5-dev \ 23 | libbz2-dev \ 24 | liblz4-dev \ 25 | libbluetooth-dev \ 26 | libpython3-dev \ 27 | libssl-dev \ 28 | libgd-dev \ 29 | && rm -rf /var/lib/apt/lists/* 30 | 31 | # Copy the installation script 32 | COPY installers/doppelganger_install_linux.sh /tmp/doppelganger_install_linux.sh 33 | 34 | # Make the script executable 35 | RUN chmod +x /tmp/doppelganger_install_linux.sh 36 | 37 | # Run the installation script 38 | RUN /tmp/doppelganger_install_linux.sh 39 | 40 | # Set the working directory 41 | WORKDIR /root 42 | 43 | # Command to run when starting the container 44 | CMD ["/bin/bash"] -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build clean test run-gui 2 | 3 | # Suppress duplicate library warnings on macOS 4 | ifeq ($(shell uname -s),Darwin) 5 | export CGO_LDFLAGS=-Wl,-no_warn_duplicate_libraries 6 | endif 7 | 8 | build: 9 | @echo "Building Doppelgänger Assistant..." 10 | @cd src && go build -o ../doppelganger_assistant . 11 | @echo "Build complete: ./doppelganger_assistant" 12 | 13 | clean: 14 | @echo "Cleaning build artifacts..." 15 | @rm -f doppelganger_assistant 16 | @rm -f doppelganger_assistant_test 17 | @rm -rf build/ 18 | @rm -rf src/fyne-cross/ 19 | @echo "Clean complete." 20 | 21 | test: 22 | @echo "Running tests..." 23 | @cd src && go test -v ./... 24 | 25 | run-gui: 26 | @echo "Launching GUI..." 27 | @./doppelganger_assistant -g 28 | 29 | install: 30 | @echo "Installing dependencies..." 31 | @cd src && go mod download && go mod tidy 32 | @echo "Dependencies installed." 33 | 34 | help: 35 | @echo "Doppelgänger Assistant - Build System" 36 | @echo "" 37 | @echo "Available targets:" 38 | @echo " make build - Build the application" 39 | @echo " make clean - Remove build artifacts" 40 | @echo " make test - Run tests" 41 | @echo " make run-gui - Launch the GUI" 42 | @echo " make install - Install dependencies" 43 | @echo " make help - Show this help message" 44 | 45 | -------------------------------------------------------------------------------- /scripts/wsl_enable.ps1: -------------------------------------------------------------------------------- 1 | # This script installs Windows Subsystem for Linux (WSL) and Ubuntu 2 | 3 | # Log file path 4 | $logFile = "C:\doppelganger_assistant\wsl_enable.log" 5 | 6 | # Function to log output to both file and screen 7 | function Log { 8 | param ( 9 | [string]$message 10 | ) 11 | $timestamp = (Get-Date).ToString('u') 12 | $logMessage = "$timestamp - $message" 13 | Write-Output $message 14 | Add-Content -Path $logFile -Value $logMessage 15 | } 16 | # Enable the necessary features 17 | $rebootRequired = $false 18 | 19 | Log "Enabling Windows Subsystem for Linux..." 20 | $wslFeature = Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux 21 | if ($wslFeature.State -ne "Enabled") { 22 | dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart 23 | $rebootRequired = $true 24 | } 25 | 26 | Log "Enabling Virtual Machine Platform..." 27 | $vmFeature = Get-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform 28 | if ($vmFeature.State -ne "Enabled") { 29 | dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart 30 | $rebootRequired = $true 31 | } 32 | 33 | # Check if a reboot is required 34 | if ($rebootRequired) { 35 | Log "A reboot is required to complete the WSL installation. Please reboot your system and run this script again." 36 | New-Item -Path "$env:SystemRoot\System32\RebootPending.txt" -ItemType File -Force 37 | exit 38 | } 39 | 40 | Log "Setting WSL 2 as the default version..." 41 | wsl --set-default-version 2 42 | 43 | Log "Updating WSL..." 44 | wsl --update 45 | 46 | Log "WSL installation and setup complete." -------------------------------------------------------------------------------- /src/status_writer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "sync" 8 | ) 9 | 10 | // StatusWriter handles writing status messages separately from command output. 11 | type StatusWriter struct { 12 | mu sync.Mutex 13 | writer io.Writer 14 | } 15 | 16 | var ( 17 | globalStatusWriter *StatusWriter 18 | statusMu sync.Mutex 19 | ) 20 | 21 | func init() { 22 | globalStatusWriter = &StatusWriter{writer: os.Stderr} 23 | } 24 | 25 | // SetStatusWriter sets the global status writer. 26 | func SetStatusWriter(w io.Writer) { 27 | statusMu.Lock() 28 | defer statusMu.Unlock() 29 | globalStatusWriter = &StatusWriter{writer: w} 30 | } 31 | 32 | // WriteStatus writes a status message to the status writer. 33 | func WriteStatus(format string, args ...interface{}) { 34 | statusMu.Lock() 35 | defer statusMu.Unlock() 36 | if globalStatusWriter != nil && globalStatusWriter.writer != nil { 37 | msg := fmt.Sprintf(format, args...) 38 | globalStatusWriter.writer.Write([]byte(msg + "\n")) 39 | } 40 | } 41 | 42 | // WriteStatusSuccess writes a success status message. 43 | func WriteStatusSuccess(format string, args ...interface{}) { 44 | WriteStatus("[SUCCESS] "+format, args...) 45 | } 46 | 47 | // WriteStatusError writes an error status message. 48 | func WriteStatusError(format string, args ...interface{}) { 49 | WriteStatus("[ERROR] "+format, args...) 50 | } 51 | 52 | // WriteStatusInfo writes an info status message. 53 | func WriteStatusInfo(format string, args ...interface{}) { 54 | WriteStatus("[INFO] "+format, args...) 55 | } 56 | 57 | // WriteStatusProgress writes a progress status message. 58 | func WriteStatusProgress(format string, args ...interface{}) { 59 | WriteStatus("[PROGRESS] "+format, args...) 60 | } 61 | -------------------------------------------------------------------------------- /scripts/usb_reconnect.ps1: -------------------------------------------------------------------------------- 1 | # Log file path 2 | $logFile = "C:\doppelganger_assistant\usb_reconnect.log" 3 | 4 | # Function to log output to both file and screen 5 | function Log { 6 | param ( 7 | [string]$message 8 | ) 9 | $timestamp = (Get-Date).ToString('u') 10 | $logMessage = "$timestamp - $message" 11 | Write-Output $message 12 | Add-Content -Path $logFile -Value $logMessage 13 | } 14 | 15 | # Function to detach a USB device 16 | function DetachUSBDevice { 17 | param ( 18 | [string]$busId 19 | ) 20 | Log "Detaching device with busid $busId..." 21 | $detachOutput = & usbipd detach --busid $busId 2>&1 22 | if ($LASTEXITCODE -ne 0) { 23 | Log "Error detaching device. It might not be attached. Exit code: $LASTEXITCODE" 24 | Log "Detach output: $detachOutput" 25 | } else { 26 | Log "Device detached successfully." 27 | } 28 | } 29 | 30 | # Function to attach a USB device to WSL 31 | function AttachUSBDeviceToWSL { 32 | param ( 33 | [string]$busId 34 | ) 35 | Log "Attaching device with busid $busId to WSL..." 36 | $attachOutput = & usbipd attach --wsl --busid $busId 2>&1 37 | if ($LASTEXITCODE -ne 0) { 38 | Log "Error attaching device to WSL. Exit code: $LASTEXITCODE" 39 | Log "Attach output: $attachOutput" 40 | } else { 41 | Log "Device successfully attached to WSL." 42 | } 43 | } 44 | 45 | # List all USB devices and find the Proxmark3 device 46 | Log "Listing all USB devices..." 47 | $usbDevices = & usbipd list 2>&1 | Tee-Object -Variable usbDevicesOutput 48 | Log $usbDevicesOutput 49 | 50 | # Find the Proxmark3 device by VID 9ac4 and extract the bus ID 51 | $proxmark3Device = $usbDevices | Select-String -Pattern "9ac4" 52 | if ($proxmark3Device) { 53 | $busId = ($proxmark3Device -split "\s+")[0] 54 | Log "Found device with busid $busId" 55 | 56 | DetachUSBDevice -busId $busId 57 | Start-Sleep -Seconds 2 58 | AttachUSBDeviceToWSL -busId $busId 59 | } else { 60 | Log "Proxmark3 device not found." 61 | } -------------------------------------------------------------------------------- /scripts/uninstall.ps1: -------------------------------------------------------------------------------- 1 | # Doppelganger Assistant Uninstaller 2 | # 3 | # To run this script: 4 | # powershell -ExecutionPolicy Bypass -Command "irm https://raw.githubusercontent.com/tweathers-sec/doppelganger_assistant/main/scripts/uninstall.ps1 | iex" 5 | # 6 | # Or if already installed: 7 | # powershell -ExecutionPolicy Bypass -File C:\doppelganger_assistant\uninstall.ps1 8 | 9 | $scriptPath = $MyInvocation.MyCommand.Path 10 | $basePath = "C:\doppelganger_assistant" 11 | 12 | if ($scriptPath -and $scriptPath.StartsWith($basePath)) { 13 | Write-Host "Relocating script to temp directory to enable cleanup..." -ForegroundColor Yellow 14 | $tempScript = "$env:TEMP\doppelganger_uninstall_temp.ps1" 15 | Copy-Item -Path $scriptPath -Destination $tempScript -Force 16 | 17 | Start-Process powershell -ArgumentList "-ExecutionPolicy Bypass -File `"$tempScript`"" -Wait -NoNewWindow 18 | exit 19 | } 20 | 21 | # Log file path 22 | $logFile = "C:\doppelganger_uninstall.log" 23 | 24 | # Function to log output to both file and screen 25 | function Log { 26 | param ( 27 | [string]$message 28 | ) 29 | $timestamp = (Get-Date).ToString('u') 30 | $logMessage = "$timestamp - $message" 31 | Write-Output $logMessage 32 | Add-Content -Path $logFile -Value $logMessage 33 | } 34 | 35 | # Define paths 36 | $basePath = "C:\doppelganger_assistant" 37 | $kaliWslName = "Kali-doppelganger_assistant" 38 | $ubuntuWslName = "Ubuntu-doppelganger_assistant" 39 | $shortcutPath = [System.IO.Path]::Combine([System.Environment]::GetFolderPath("Desktop"), "Launch Doppelganger Assistant.lnk") 40 | $pm3ShortcutPath = [System.IO.Path]::Combine([System.Environment]::GetFolderPath("Desktop"), "Proxmark3 Terminal.lnk") 41 | 42 | # Function to check if a command exists 43 | function CommandExists { 44 | param ( 45 | [string]$command 46 | ) 47 | $null = Get-Command $command -ErrorAction SilentlyContinue 48 | return $? 49 | } 50 | 51 | # Stop WSL 52 | Log "Stopping WSL..." 53 | wsl --shutdown 54 | Log "WSL stopped." 55 | 56 | # Uninstall all Doppelganger WSL distributions 57 | $wslDistributions = wsl.exe -l -q | ForEach-Object { $_.Trim() -replace "`0", "" } 58 | $removed = $false 59 | 60 | foreach ($distro in $wslDistributions) { 61 | if ($distro -eq $kaliWslName) { 62 | Log "Unregistering WSL distribution $kaliWslName..." 63 | wsl.exe --unregister $kaliWslName 64 | Log "WSL distribution $kaliWslName unregistered." 65 | $removed = $true 66 | } 67 | elseif ($distro -eq $ubuntuWslName) { 68 | Log "Unregistering WSL distribution $ubuntuWslName..." 69 | wsl.exe --unregister $ubuntuWslName 70 | Log "WSL distribution $ubuntuWslName unregistered." 71 | $removed = $true 72 | } 73 | } 74 | 75 | if (-not $removed) { 76 | Log "No Doppelganger WSL distributions found. Available distributions:" 77 | $wslDistributions | ForEach-Object { Log " - $_" } 78 | } 79 | 80 | Log "Stopping related processes..." 81 | Stop-Process -Name "wsl" -Force -ErrorAction SilentlyContinue 82 | Stop-Process -Name "usbipd" -Force -ErrorAction SilentlyContinue 83 | Log "Processes stopped." 84 | 85 | # Remove the base directory 86 | if (Test-Path -Path $basePath) { 87 | Log "Removing base directory $basePath..." 88 | Remove-Item -Recurse -Force $basePath 89 | Log "Base directory $basePath removed." 90 | } 91 | else { 92 | Log "Base directory $basePath not found." 93 | } 94 | 95 | # Remove the desktop shortcuts 96 | if (Test-Path -Path $shortcutPath) { 97 | Log "Removing Doppelganger Assistant desktop shortcut..." 98 | Remove-Item -Path $shortcutPath -Force 99 | Log "Doppelganger Assistant shortcut removed." 100 | } 101 | else { 102 | Log "Doppelganger Assistant shortcut not found." 103 | } 104 | 105 | if (Test-Path -Path $pm3ShortcutPath) { 106 | Log "Removing Proxmark3 Terminal desktop shortcut..." 107 | Remove-Item -Path $pm3ShortcutPath -Force 108 | Log "Proxmark3 Terminal shortcut removed." 109 | } 110 | else { 111 | Log "Proxmark3 Terminal shortcut not found." 112 | } 113 | 114 | # Uninstall usbipd (will be reinstalled by installer) 115 | if (CommandExists "winget") { 116 | Log "Uninstalling usbipd..." 117 | $uninstallOutput = Start-Process winget -ArgumentList "uninstall --exact usbipd" -Wait -PassThru -NoNewWindow 118 | if ($uninstallOutput.ExitCode -ne 0) { 119 | Log "Warning: Could not uninstall usbipd. Exit code: $($uninstallOutput.ExitCode)" 120 | } 121 | else { 122 | Log "usbipd uninstalled successfully." 123 | } 124 | } 125 | else { 126 | Log "winget not found. Cannot uninstall usbipd automatically." 127 | } 128 | 129 | Log "Uninstallation complete." 130 | 131 | if ($scriptPath -and $scriptPath.Contains("\Temp\")) { 132 | Log "Cleaning up temporary script..." 133 | Start-Sleep -Seconds 2 134 | Remove-Item -Path $scriptPath -Force -ErrorAction SilentlyContinue 135 | } -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ${{ matrix.os }} 11 | 12 | strategy: 13 | matrix: 14 | include: 15 | - os: ubuntu-latest 16 | arch: amd64 17 | - os: macos-latest 18 | arch: arm64 19 | - os: macos-13 20 | arch: amd64 21 | 22 | steps: 23 | - name: Set up Git repository 24 | uses: actions/checkout@v2 25 | 26 | - name: Install GitHub CLI 27 | run: | 28 | if [[ "$RUNNER_OS" == "Linux" ]]; then 29 | type -p wget >/dev/null || (sudo apt update && sudo apt-get install wget -y) 30 | sudo mkdir -p -m 755 /etc/apt/keyrings 31 | wget -qO- https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null 32 | sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg 33 | echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null 34 | sudo apt update 35 | sudo apt install gh -y 36 | elif [[ "$RUNNER_OS" == "macOS" ]]; then 37 | brew install gh 38 | fi 39 | 40 | - name: Set up Go 41 | uses: actions/setup-go@v2 42 | with: 43 | go-version: '1.22.3' # Specify the Go version 44 | 45 | - name: Create directories and build binaries 46 | run: | 47 | chmod +x ./build.sh 48 | ./build.sh 49 | 50 | - name: List all files in build directory 51 | run: ls build/ 52 | 53 | - name: Upload build artifacts 54 | if: matrix.os == 'ubuntu-latest' 55 | uses: actions/upload-artifact@v2 56 | with: 57 | name: build-ubuntu-latest 58 | path: build/ 59 | 60 | - name: Upload build artifacts for macOS 61 | if: matrix.os == 'macos-latest' 62 | uses: actions/upload-artifact@v2 63 | with: 64 | name: build-${{ matrix.os }}-${{ matrix.arch }} 65 | path: build/ 66 | 67 | - name: Upload build artifacts for macOS (macos-13) 68 | if: matrix.os == 'macos-13' 69 | uses: actions/upload-artifact@v2 70 | with: 71 | name: build-${{ matrix.os }}-${{ matrix.arch }} 72 | path: build/ 73 | 74 | create_release: 75 | runs-on: ubuntu-latest 76 | needs: build 77 | 78 | steps: 79 | - name: Set up Git repository 80 | uses: actions/checkout@v2 81 | 82 | - name: Download build artifacts 83 | env: 84 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 85 | run: | 86 | mkdir -p build 87 | for os in macos-latest; do 88 | for arch in arm64; do 89 | echo "Downloading build-${os}-${arch}..." 90 | mkdir -p build-${os}-${arch} 91 | gh run download --name build-${os}-${arch} --dir build-${os}-${arch}/ || echo "Failed to download build-${os}-${arch}" 92 | mv build-${os}-${arch}/* build/ || echo "Failed to move files for build-${os}-${arch}" 93 | done 94 | done 95 | echo "Downloading build-macos-13-amd64..." 96 | mkdir -p build-macos-13-amd64 97 | gh run download --name build-macos-13-amd64 --dir build-macos-13-amd64/ || echo "Failed to download build-macos-13-amd64" 98 | mv build-macos-13-amd64/* build/ || echo "Failed to move files for build-macos-13-amd64" 99 | echo "Downloading build-ubuntu-latest..." 100 | mkdir -p build-ubuntu-latest 101 | gh run download --name build-ubuntu-latest --dir build-ubuntu-latest/ || echo "Failed to download build-ubuntu-latest" 102 | mv build-ubuntu-latest/* build/ || echo "Failed to move files for build-ubuntu-latest" 103 | 104 | - name: List all files in build directory before upload 105 | run: ls -l build/ 106 | 107 | - name: Create Release 108 | id: create_release 109 | uses: actions/create-release@v1 110 | env: 111 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 112 | with: 113 | tag_name: ${{ github.ref_name }} 114 | release_name: Release ${{ github.ref_name }} 115 | draft: false 116 | prerelease: false 117 | 118 | - name: Upload Release Assets 119 | env: 120 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 121 | run: | 122 | if [ -d build ]; then 123 | for file in build/*; do 124 | echo "Uploading $file..." 125 | gh release upload ${{ github.ref_name }} "$file" --clobber 126 | sleep 5 # Add a delay between uploads 127 | done 128 | else 129 | echo "Build directory does not exist or is empty." 130 | exit 1 131 | fi 132 | 133 | - name: List all files in build directory after upload 134 | run: ls -l build/ -------------------------------------------------------------------------------- /scripts/proxmark_flash.ps1: -------------------------------------------------------------------------------- 1 | # Log file path 2 | $logFile = "C:\doppelganger_assistant\proxmark_flash.log" 3 | 4 | # Function to log output to both file and screen 5 | function Log { 6 | param ( 7 | [string]$message 8 | ) 9 | $timestamp = (Get-Date).ToString('u') 10 | $logMessage = "$timestamp - $message" 11 | Write-Output $message 12 | Add-Content -Path $logFile -Value $logMessage 13 | } 14 | 15 | # Function to check if a command exists 16 | function CommandExists { 17 | param ( 18 | [string]$command 19 | ) 20 | $null = Get-Command $command -ErrorAction SilentlyContinue 21 | return $? 22 | } 23 | 24 | # Function to detect which Doppelganger WSL distribution is installed 25 | function Get-DoppelgangerDistro { 26 | $wslList = wsl.exe -l -q | ForEach-Object { $_.Trim() -replace "`0", "" } 27 | $kaliName = "Kali-doppelganger_assistant" 28 | $ubuntuName = "Ubuntu-doppelganger_assistant" 29 | 30 | foreach ($distro in $wslList) { 31 | if ($distro -eq $kaliName) { 32 | return $kaliName 33 | } elseif ($distro -eq $ubuntuName) { 34 | return $ubuntuName 35 | } 36 | } 37 | return $null 38 | } 39 | 40 | # Function to check if WSL is running 41 | function IsWSLRunning { 42 | $distroName = Get-DoppelgangerDistro 43 | if ($null -eq $distroName) { 44 | return $false 45 | } 46 | $wslOutput = wsl -l -q 47 | return $wslOutput -match $distroName 48 | } 49 | 50 | # Function to start WSL if not running 51 | function StartWSLIfNotRunning { 52 | $distroName = Get-DoppelgangerDistro 53 | if ($null -eq $distroName) { 54 | Log "ERROR: No Doppelganger Assistant WSL distribution found!" 55 | Log "Please run the installer first." 56 | Read-Host "Press Enter to exit" 57 | exit 1 58 | } 59 | 60 | if (-not (IsWSLRunning)) { 61 | Log "WSL is not running. Starting $distroName..." 62 | & wsl -d $distroName --exec echo "WSL started" 63 | Log "WSL started." 64 | } else { 65 | Log "$distroName is already running." 66 | } 67 | } 68 | 69 | # Function to detach a USB device if it is already attached 70 | function DetachUSBDevice { 71 | param ( 72 | [string]$busId 73 | ) 74 | Log "Detaching device with busid $busId if it is already attached..." 75 | $detachOutput = & usbipd detach --busid $busId 2>&1 76 | if ($LASTEXITCODE -ne 0) { 77 | Log "Error detaching device. It might not be attached. Exit code: $LASTEXITCODE" 78 | Log "Detach output: $detachOutput" 79 | } else { 80 | Log "Device detached successfully." 81 | } 82 | } 83 | 84 | # Function to attach a USB device to WSL 85 | function AttachUSBDeviceToWSL { 86 | param ( 87 | [string]$busId 88 | ) 89 | Log "Attaching device with busid $busId to WSL..." 90 | $attachOutput = & usbipd attach --wsl --busid $busId 2>&1 91 | if ($LASTEXITCODE -ne 0) { 92 | Log "Error attaching device to WSL. Exit code: $LASTEXITCODE" 93 | Log "Attach output: $attachOutput" 94 | } else { 95 | Log "Device successfully attached to WSL." 96 | } 97 | } 98 | 99 | # Clear the log file 100 | Clear-Content -Path $logFile -ErrorAction SilentlyContinue 101 | 102 | Log "Starting Proxmark3 flashing script." 103 | 104 | # Ensure WSL is running 105 | StartWSLIfNotRunning 106 | 107 | # List all USB devices and find the Proxmark3 device 108 | Log "Listing all USB devices..." 109 | $usbDevices = & usbipd list 2>&1 110 | Log $usbDevices 111 | 112 | # Find the Proxmark3 device by VID 9ac4 and extract the bus ID 113 | $proxmark3Device = $usbDevices | Select-String -Pattern "9ac4" 114 | if ($proxmark3Device) { 115 | $busId = ($proxmark3Device -split "\s+")[0] 116 | Log "Found device with busid $busId" 117 | 118 | # Detach the device if it is already attached 119 | DetachUSBDevice -busId $busId 120 | 121 | # Bind the Proxmark3 device 122 | Log "Binding Proxmark3 device..." 123 | $bindOutput = & usbipd bind --busid $busId 2>&1 124 | if ($LASTEXITCODE -ne 0) { 125 | Log "Error binding Proxmark3 device. Exit code: $LASTEXITCODE" 126 | Log "Bind output: $bindOutput" 127 | } else { 128 | # Attach the Proxmark3 device to WSL 129 | AttachUSBDeviceToWSL -busId $busId 130 | 131 | $distroName = Get-DoppelgangerDistro 132 | Log "Running pm3-flash-all in a new terminal on $distroName..." 133 | $command = "wsl -d $distroName -e bash -c 'pm3-flash-all'" 134 | 135 | Start-Process powershell -ArgumentList "-NoExit", "-Command", $command 136 | 137 | Log "Waiting for 5 seconds..." 138 | Start-Sleep -Seconds 5 139 | 140 | Log "Reattaching the Proxmark3 device..." 141 | DetachUSBDevice -busId $busId 142 | AttachUSBDeviceToWSL -busId $busId 143 | 144 | Log "Proxmark3 flashing process completed. The update is running in a separate terminal." 145 | Log "Please wait for the update to complete in the new terminal before using the device." 146 | } 147 | } else { 148 | Log "Proxmark3 device not found." 149 | } 150 | 151 | Log "Script execution completed." -------------------------------------------------------------------------------- /src/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | const ( 10 | Red = "\033[31m" 11 | Green = "\033[32m" 12 | Yellow = "\033[33m" 13 | Reset = "\033[0m" 14 | Version = "1.1.2" 15 | BuildDate = "2025-12-18" 16 | ) 17 | 18 | type Card struct { 19 | DataType string 20 | BitLength string 21 | HexValue string 22 | FacilityCode string 23 | CardNumber string 24 | Bin string 25 | } 26 | 27 | func main() { 28 | bitLength := flag.Int("bl", 0, "Bit length") 29 | facilityCode := flag.Int("fc", 0, "Facility code") 30 | cardNumber := flag.Int("cn", 0, "Card number") 31 | cardType := flag.String("t", "prox", "Card type (iclass, prox, awid, indala, avigilon, em, piv, mifare)") 32 | uid := flag.String("uid", "", "UID for PIV and MIFARE cards (4 x HEX Bytes in the Card_Number column)") 33 | hexData := flag.String("hex", "", "Hex data for EM cards") 34 | write := flag.Bool("w", false, "Write card data") 35 | verify := flag.Bool("v", false, "Verify written card data") 36 | simulate := flag.Bool("s", false, "Card simulation") 37 | showVersion := flag.Bool("version", false, "Show program version") 38 | gui := flag.Bool("g", false, "Launch GUI") 39 | 40 | flag.Usage = func() { 41 | fmt.Fprintf(os.Stderr, Green+"\n--- About Doppelgänger Assistant ---\n"+Reset) 42 | fmt.Fprintf(os.Stderr, "Author: @tweathers-sec\n") 43 | fmt.Fprintf(os.Stderr, "Version: %s\n", Version) 44 | fmt.Fprintf(os.Stderr, "\n") 45 | fmt.Fprintf(os.Stderr, Yellow+"Usage: %s -bl -fc -cn -t [-uid ] [-hex ] [-w] [-v] [-s] [-version] [-g] [-c ]\n"+Reset, os.Args[0]) 46 | fmt.Fprintf(os.Stderr, "\n") 47 | flag.PrintDefaults() 48 | fmt.Fprintf(os.Stderr, "\n") 49 | fmt.Fprintf(os.Stderr, Green+"Supported card types and bit lengths:\n"+Reset) 50 | fmt.Fprintf(os.Stderr, "\n") 51 | fmt.Fprintf(os.Stderr, " iclass: 26, 35\n") 52 | fmt.Fprintf(os.Stderr, " prox: 26, 30, 31, 33, 34, 35, 36, 37, 46, 48\n") 53 | fmt.Fprintf(os.Stderr, " awid: 26\n") 54 | fmt.Fprintf(os.Stderr, " indala: 26, 27, 28, 29\n") 55 | fmt.Fprintf(os.Stderr, " avigilon: 56\n") 56 | fmt.Fprintf(os.Stderr, " em: 32\n") 57 | fmt.Fprintf(os.Stderr, " piv: N/A\n") 58 | fmt.Fprintf(os.Stderr, " mifare: N/A\n") 59 | fmt.Fprintf(os.Stderr, "\n") 60 | fmt.Fprintf(os.Stderr, Green+"Example #1: Generate encoded card values for manual writing with a Proxmark3\n"+Reset) 61 | fmt.Fprintf(os.Stderr, "\n") 62 | fmt.Fprintf(os.Stderr, " %s -bl 26 -fc 123 -cn 1234 -t iclass\n", os.Args[0]) 63 | fmt.Fprintf(os.Stderr, "\n") 64 | fmt.Fprintf(os.Stderr, Green+"Example #2: Generate encoded card values, then write and verify with a Proxmark3\n"+Reset) 65 | fmt.Fprintf(os.Stderr, "\n") 66 | fmt.Fprintf(os.Stderr, " %s -bl 26 -fc 123 -cn 1234 -t iclass -w -v\n", os.Args[0]) 67 | fmt.Fprintf(os.Stderr, "\n") 68 | fmt.Fprintf(os.Stderr, Green+"Example #3: Simulate a PIVKey (C190) using the UID provide by Doppelganger with a Proxmark3\n"+Reset) 69 | fmt.Fprintf(os.Stderr, "\n") 70 | fmt.Fprintf(os.Stderr, " %s -uid 5AF70D9D -s -t piv\n", os.Args[0]) 71 | fmt.Fprintf(os.Stderr, "\n") 72 | fmt.Fprintf(os.Stderr, Green+"Example #3: Launch the application in GUI mode\n"+Reset) 73 | fmt.Fprintf(os.Stderr, "\n") 74 | fmt.Fprintf(os.Stderr, " %s -g\n", os.Args[0]) 75 | } 76 | 77 | flag.Parse() 78 | 79 | if flag.NFlag() == 0 { 80 | *gui = true 81 | } 82 | 83 | if *showVersion { 84 | fmt.Println("Version:", Version) 85 | return 86 | } 87 | 88 | if *gui { 89 | runGUI() 90 | return 91 | } 92 | 93 | if *simulate && (*write || *verify) { 94 | fmt.Println(Red, "Cannot use -s (simulate) with -w (write) or -v (verify).", Reset) 95 | return 96 | } 97 | 98 | if *verify && !*write { 99 | fmt.Println(Red, "Cannot use -v (verify) without -w (write).", Reset) 100 | return 101 | } 102 | 103 | if *write || *simulate { 104 | if ok, msg := checkProxmark3(); !ok { 105 | fmt.Printf("%sError: %s%s\n", Red, msg, Reset) 106 | return 107 | } 108 | } 109 | 110 | if *cardType == "piv" || *cardType == "mifare" { 111 | if *uid == "" { 112 | fmt.Println(Red, "UID is required for PIV and MIFARE card types.", Reset) 113 | return 114 | } 115 | } else { 116 | if *cardType == "em" { 117 | if *bitLength == 0 { 118 | *bitLength = 32 119 | } 120 | if *bitLength != 32 { 121 | fmt.Println(Red, "Invalid bit length for EM. Supported bit length is 32.", Reset) 122 | return 123 | } 124 | if *hexData == "" { 125 | fmt.Println(Red, "Hex data is required for EM card type.", Reset) 126 | return 127 | } 128 | // Validate EM4100 hex data format 129 | if valid, errMsg := validateEM4100Hex(*hexData); !valid { 130 | fmt.Println(Red, errMsg, Reset) 131 | return 132 | } 133 | } else { 134 | if *bitLength == 0 || (*facilityCode == 0 || *cardNumber == 0) { 135 | flag.Usage() 136 | return 137 | } 138 | switch *cardType { 139 | case "iclass": 140 | if *bitLength != 26 && *bitLength != 30 && *bitLength != 33 && *bitLength != 34 && *bitLength != 35 && *bitLength != 36 && *bitLength != 37 && *bitLength != 46 && *bitLength != 48 { 141 | fmt.Println(Red, "Invalid bit length for iCLASS. Supported bit lengths are 26, 30, 33, 34, 35, 36, 37, 46, and 48.", Reset) 142 | return 143 | } 144 | case "indala": 145 | if *bitLength != 26 && *bitLength != 27 && *bitLength != 28 && *bitLength != 29 { 146 | fmt.Println(Red, "Invalid bit length for Indala. Supported bit lengths are 26, 27, 28, and 29.", Reset) 147 | return 148 | } 149 | case "avigilon": 150 | if *bitLength != 56 { 151 | fmt.Println(Red, "Invalid bit length for Avigilon. Supported bit length is 56.", Reset) 152 | return 153 | } 154 | case "prox": 155 | if *bitLength != 26 && *bitLength != 30 && *bitLength != 31 && *bitLength != 33 && *bitLength != 34 && *bitLength != 35 && *bitLength != 36 && *bitLength != 37 && *bitLength != 46 && *bitLength != 48 { 156 | fmt.Println(Red, "Invalid bit length for Prox. Supported bit lengths are 26, 30, 31, 33, 34, 35, 36, 37, 46, and 48.", Reset) 157 | return 158 | } 159 | case "awid": 160 | if *bitLength != 26 { 161 | fmt.Println(Red, "Invalid bit length for AWID. Supported bit length is 26.", Reset) 162 | return 163 | } 164 | default: 165 | fmt.Println(Red, "Unsupported card type.", Reset) 166 | return 167 | } 168 | } 169 | } 170 | 171 | handleCardType(*cardType, *facilityCode, *cardNumber, *bitLength, *write, *verify, *uid, *hexData, *simulate) 172 | } 173 | -------------------------------------------------------------------------------- /src/card_simulators.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "time" 10 | ) 11 | 12 | func simulateProxmark3Command(command string) (string, error) { 13 | if ok, msg := checkProxmark3(); !ok { 14 | return "", fmt.Errorf("%s", msg) 15 | } 16 | 17 | pm3Binary, err := getPm3Path() 18 | if err != nil { 19 | return "", fmt.Errorf("failed to find pm3 binary: %w", err) 20 | } 21 | 22 | device, err := getPm3Device() 23 | if err != nil { 24 | return "", fmt.Errorf("failed to detect pm3 device: %w", err) 25 | } 26 | 27 | WriteStatusInfo("Simulation started - press PM3 button to stop") 28 | WriteStatusInfo("With battery: you can remove device and simulation continues") 29 | 30 | cmd := exec.Command(pm3Binary, "-c", command, "-p", device) 31 | 32 | cmd.Stdout = os.Stdout 33 | cmd.Stderr = os.Stderr 34 | cmd.Stdin = os.Stdin 35 | 36 | if err := cmd.Run(); err != nil { 37 | return "", fmt.Errorf("error running command: %w", err) 38 | } 39 | 40 | WriteStatusSuccess("Simulation completed") 41 | return "", nil 42 | } 43 | 44 | func simulateCardData(cardType string, cardData uint64, bitLength, facilityCode, cardNumber int, hexData, uid string) { 45 | var command string 46 | switch cardType { 47 | case "iclass": 48 | 49 | type Card struct { 50 | CSN string `json:"CSN"` 51 | Configuration string `json:"Configuration"` 52 | Epurse string `json:"Epurse"` 53 | Kd string `json:"Kd"` 54 | Kc string `json:"Kc"` 55 | AIA string `json:"AIA"` 56 | } 57 | 58 | type Blocks struct { 59 | Block0 string `json:"0"` 60 | Block1 string `json:"1"` 61 | Block2 string `json:"2"` 62 | Block3 string `json:"3"` 63 | Block4 string `json:"4"` 64 | Block5 string `json:"5"` 65 | Block6 string `json:"6"` 66 | Block7 string `json:"7"` 67 | Block8 string `json:"8"` 68 | Block9 string `json:"9"` 69 | Block10 string `json:"10"` 70 | Block11 string `json:"11"` 71 | Block12 string `json:"12"` 72 | Block13 string `json:"13"` 73 | Block14 string `json:"14"` 74 | Block15 string `json:"15"` 75 | Block16 string `json:"16"` 76 | Block17 string `json:"17"` 77 | Block18 string `json:"18"` 78 | } 79 | 80 | type IClass struct { 81 | Created string `json:"Created"` 82 | FileType string `json:"FileType"` 83 | Card Card `json:"Card"` 84 | Blocks Blocks `json:"blocks"` 85 | } 86 | 87 | iclass := IClass{ 88 | Created: "doppelganager_assistant", 89 | FileType: "iclass", 90 | Card: Card{ 91 | CSN: "28668B15FEFF12E0", 92 | Configuration: "12FFFFFF7F1FFF3C", 93 | Epurse: "FFFFFFFFD9FFFFFF", 94 | Kd: "843F766755B8DBCE", 95 | Kc: "FFFFFFFFFFFFFFFF", 96 | AIA: "FFFFFFFFFFFFFFFF", 97 | }, 98 | Blocks: Blocks{ 99 | Block0: "28668B15FEFF12E0", 100 | Block1: "12FFFFFF7F1FFF3C", 101 | Block2: "FFFFFFFFD9FFFFFF", 102 | Block3: "843F766755B8DBCE", 103 | Block4: "FFFFFFFFFFFFFFFF", 104 | Block5: "FFFFFFFFFFFFFFFF", 105 | Block6: "030303030003E014", 106 | Block7: fmt.Sprintf("%016x", cardData), 107 | Block8: "0000000000000000", 108 | Block9: "0000000000000000", 109 | Block10: "FFFFFFFFFFFFFFFF", 110 | Block11: "FFFFFFFFFFFFFFFF", 111 | Block12: "FFFFFFFFFFFFFFFF", 112 | Block13: "FFFFFFFFFFFFFFFF", 113 | Block14: "FFFFFFFFFFFFFFFF", 114 | Block15: "FFFFFFFFFFFFFFFF", 115 | Block16: "FFFFFFFFFFFFFFFF", 116 | Block17: "FFFFFFFFFFFFFFFF", 117 | Block18: "FFFFFFFFFFFFFFFF", 118 | }, 119 | } 120 | 121 | homeDir, err := os.UserHomeDir() 122 | if err != nil { 123 | fmt.Println("Error getting home directory:", err) 124 | return 125 | } 126 | 127 | fileName := filepath.Join(homeDir, fmt.Sprintf("iclass_sim_%d_%d_%d_%s.json", bitLength, facilityCode, cardNumber, time.Now().Format("20060102150405"))) 128 | file, err := os.Create(fileName) 129 | if err != nil { 130 | fmt.Println("Error creating file:", err) 131 | return 132 | } 133 | defer file.Close() 134 | 135 | encoder := json.NewEncoder(file) 136 | encoder.SetIndent("", " ") 137 | if err := encoder.Encode(iclass); err != nil { 138 | fmt.Println("Error encoding JSON:", err) 139 | return 140 | } 141 | 142 | WriteStatusInfo("iCLASS simulation file saved: %s", fileName) 143 | command = fmt.Sprintf("hf iclass eload -f %s; hf iclass sim -t 3", fileName) 144 | 145 | output, err := simulateProxmark3Command(command) 146 | if err != nil { 147 | fmt.Println(Red, err, Reset) 148 | fmt.Println(output) 149 | } 150 | 151 | case "prox": 152 | switch bitLength { 153 | case 26: 154 | command = fmt.Sprintf("lf hid sim -w H10301 --fc %d --cn %d", facilityCode, cardNumber) 155 | case 30: 156 | command = fmt.Sprintf("lf hid sim -w ATSW30 --fc %d --cn %d", facilityCode, cardNumber) 157 | case 31: 158 | command = fmt.Sprintf("lf hid sim -w ADT31 --fc %d --cn %d", facilityCode, cardNumber) 159 | case 33: 160 | command = fmt.Sprintf("lf hid sim -w D10202 --fc %d --cn %d", facilityCode, cardNumber) 161 | case 34: 162 | command = fmt.Sprintf("lf hid sim -w H10306 --fc %d --cn %d", facilityCode, cardNumber) 163 | case 35: 164 | command = fmt.Sprintf("lf hid sim -w C1k35s --fc %d --cn %d", facilityCode, cardNumber) 165 | case 36: 166 | command = fmt.Sprintf("lf hid sim -w S12906 --fc %d --cn %d", facilityCode, cardNumber) 167 | case 37: 168 | command = fmt.Sprintf("lf hid sim -w H10304 --fc %d --cn %d", facilityCode, cardNumber) 169 | case 46: 170 | command = fmt.Sprintf("lf hid sim -w H800002 --fc %d --cn %d", facilityCode, cardNumber) 171 | case 48: 172 | command = fmt.Sprintf("lf hid sim -w C1k48s --fc %d --cn %d", facilityCode, cardNumber) 173 | } 174 | output, err := simulateProxmark3Command(command) 175 | if err != nil { 176 | fmt.Println(Red, err, Reset) 177 | fmt.Println(output) 178 | } 179 | case "awid": 180 | command = fmt.Sprintf("lf awid sim --fmt 26 --fc %d --cn %d", facilityCode, cardNumber) 181 | output, err := simulateProxmark3Command(command) 182 | if err != nil { 183 | fmt.Println(Red, err, Reset) 184 | fmt.Println(output) 185 | } 186 | 187 | case "indala": 188 | switch bitLength { 189 | case 26: 190 | command = fmt.Sprintf("lf indala sim --fc %d --cn %d", facilityCode, cardNumber) 191 | case 27: 192 | command = fmt.Sprintf("lf hid sim -w ind27 --fc %d --cn %d", facilityCode, cardNumber) 193 | case 29: 194 | command = fmt.Sprintf("lf hid sim -w ind29 --fc %d --cn %d", facilityCode, cardNumber) 195 | } 196 | output, err := simulateProxmark3Command(command) 197 | if err != nil { 198 | fmt.Println(Red, err, Reset) 199 | fmt.Println(output) 200 | } 201 | 202 | case "avigilon": 203 | command = fmt.Sprintf("lf hid sim -w Avig56 --fc %d --cn %d", facilityCode, cardNumber) 204 | _, err := simulateProxmark3Command(command) 205 | if err != nil { 206 | WriteStatusError("Simulation failed: %v", err) 207 | } 208 | 209 | case "em": 210 | command = fmt.Sprintf("lf em 410x sim --id %s", hexData) 211 | _, err := simulateProxmark3Command(command) 212 | if err != nil { 213 | WriteStatusError("Simulation failed: %v", err) 214 | } 215 | 216 | case "piv": 217 | command = fmt.Sprintf("hf 14a sim -t 3 --uid %s", uid) 218 | _, err := simulateProxmark3Command(command) 219 | if err != nil { 220 | WriteStatusError("Simulation failed: %v", err) 221 | } 222 | 223 | case "mifare": 224 | command = fmt.Sprintf("hf 14a sim -t 1 --uid %s", uid) 225 | _, err := simulateProxmark3Command(command) 226 | if err != nil { 227 | WriteStatusError("Simulation failed: %v", err) 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/card_handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func handleCardType(cardType string, facilityCode, cardNumber, bitLength int, write, verify bool, uid, hexData string, simulate bool) { 8 | switch cardType { 9 | case "iclass": 10 | handleICLASS(facilityCode, cardNumber, bitLength, simulate, write, verify) 11 | case "prox": 12 | handleProx(facilityCode, cardNumber, bitLength, simulate, write, verify) 13 | case "awid": 14 | handleAWID(facilityCode, cardNumber, bitLength, simulate, write, verify) 15 | case "indala": 16 | handleIndala(facilityCode, cardNumber, bitLength, simulate, write, verify) 17 | case "avigilon": 18 | handleAvigilon(facilityCode, cardNumber, bitLength, simulate, write, verify) 19 | case "em": 20 | handleEM(hexData, simulate, write, verify) 21 | case "piv": 22 | handlePIV(uid, simulate, write, verify) 23 | case "mifare": 24 | handleMIFARE(uid, simulate, write, verify) 25 | default: 26 | fmt.Println(Red, "Unsupported card type. Supported types are: iclass, prox, awid, indala, em, piv, mifare.", Reset) 27 | } 28 | } 29 | 30 | func handleICLASS(facilityCode, cardNumber, bitLength int, simulate, write, verify bool) { 31 | validBitLengths := map[int]bool{26: true, 30: true, 33: true, 34: true, 35: true, 36: true, 37: true, 46: true, 48: true} 32 | if !validBitLengths[bitLength] { 33 | WriteStatusError("Invalid bit length for iCLASS. Supported: 26, 30, 33, 34, 35, 36, 37, 46, 48") 34 | return 35 | } 36 | 37 | if simulate { 38 | WriteStatusError("iCLASS card simulation is currently disabled") 39 | return 40 | } 41 | 42 | var formatCode string 43 | switch bitLength { 44 | case 26: 45 | formatCode = "H10301" 46 | case 30: 47 | formatCode = "ATSW30" 48 | case 33: 49 | formatCode = "D10202" 50 | case 34: 51 | formatCode = "H10306" 52 | case 35: 53 | formatCode = "C1k35s" 54 | case 36: 55 | formatCode = "S12906" 56 | case 37: 57 | formatCode = "H10304" 58 | case 46: 59 | formatCode = "H800002" 60 | case 48: 61 | formatCode = "C1k48s" 62 | } 63 | 64 | if write { 65 | WriteStatusInfo("Writing to iCLASS 2k card...") 66 | WriteStatusInfo("Command: hf iclass encode -w %s --fc %d --cn %d --ki 0", formatCode, facilityCode, cardNumber) 67 | writeCardData("iclass", 0, bitLength, facilityCode, cardNumber, "", verify, formatCode) 68 | } 69 | 70 | if verify { 71 | verifyCardData("iclass", facilityCode, cardNumber, bitLength, "", "") 72 | } 73 | } 74 | 75 | func handleProx(facilityCode, cardNumber, bitLength int, simulate, write, verify bool) { 76 | if simulate { 77 | simulateCardData("prox", 0, bitLength, facilityCode, cardNumber, "", "") 78 | return 79 | } 80 | 81 | var cmdTemplate string 82 | switch bitLength { 83 | case 26: 84 | cmdTemplate = "lf hid clone -w H10301 --fc %d --cn %d" 85 | case 30: 86 | cmdTemplate = "lf hid clone -w ATSW30 --fc %d --cn %d" 87 | case 31: 88 | cmdTemplate = "lf hid clone -w ADT31 --fc %d --cn %d" 89 | case 33: 90 | cmdTemplate = "lf hid clone -w D10202 --fc %d --cn %d" 91 | case 34: 92 | cmdTemplate = "lf hid clone -w H10306 --fc %d --cn %d" 93 | case 35: 94 | cmdTemplate = "lf hid clone -w C1k35s --fc %d --cn %d" 95 | case 36: 96 | cmdTemplate = "lf hid clone -w S12906 --fc %d --cn %d" 97 | case 37: 98 | cmdTemplate = "lf hid clone -w H10304 --fc %d --cn %d" 99 | case 46: 100 | cmdTemplate = "lf hid clone -w H800002 --fc %d --cn %d" 101 | case 48: 102 | cmdTemplate = "lf hid clone -w C1k48s --fc %d --cn %d" 103 | default: 104 | WriteStatusError("Unsupported bit length for Prox card") 105 | return 106 | } 107 | 108 | if write { 109 | WriteStatusInfo("Writing to T5577 card...") 110 | WriteStatusInfo("Command: "+cmdTemplate, facilityCode, cardNumber) 111 | writeCardData("prox", 0, bitLength, facilityCode, cardNumber, "", verify, "") 112 | } 113 | 114 | if verify { 115 | verifyCardData("prox", facilityCode, cardNumber, bitLength, "", "") 116 | } 117 | } 118 | 119 | func handleAWID(facilityCode, cardNumber, bitLength int, simulate, write, verify bool) { 120 | if simulate { 121 | simulateCardData("awid", 0, bitLength, facilityCode, cardNumber, "", "") 122 | return 123 | } 124 | 125 | bitLength = 26 126 | if write { 127 | WriteStatusInfo("Writing to T5577 card...") 128 | WriteStatusInfo("Command: lf awid clone --fmt 26 --fc %d --cn %d", facilityCode, cardNumber) 129 | writeCardData("awid", 0, bitLength, facilityCode, cardNumber, "", verify, "") 130 | } 131 | 132 | if verify { 133 | verifyCardData("awid", facilityCode, cardNumber, bitLength, "", "") 134 | } 135 | } 136 | 137 | func handleIndala(facilityCode, cardNumber, bitLength int, simulate, write, verify bool) { 138 | if simulate { 139 | simulateCardData("indala", 0, bitLength, facilityCode, cardNumber, "", "") 140 | return 141 | } 142 | 143 | if bitLength != 26 { 144 | WriteStatusInfo("Note: Indala 27/29-bit will be written as 26-bit. For full replication, use simulation mode") 145 | } 146 | 147 | bitLength = 26 148 | if write { 149 | WriteStatusInfo("Writing to T5577 card...") 150 | WriteStatusInfo("Command: lf indala clone --fc %d --cn %d", facilityCode, cardNumber) 151 | writeCardData("indala", 0, bitLength, facilityCode, cardNumber, "", verify, "") 152 | } 153 | 154 | if verify { 155 | verifyCardData("indala", facilityCode, cardNumber, bitLength, "", "") 156 | } 157 | } 158 | 159 | func handleEM(hexData string, simulate, write, verify bool) { 160 | // Validate EM4100 hex data format 161 | if valid, errMsg := validateEM4100Hex(hexData); !valid { 162 | WriteStatusError(errMsg) 163 | return 164 | } 165 | 166 | if simulate { 167 | simulateCardData("em", 0, 0, 0, 0, hexData, "") 168 | return 169 | } 170 | 171 | if write { 172 | WriteStatusInfo("Writing to T5577 card...") 173 | WriteStatusInfo("Command: lf em 410x clone --id %s", hexData) 174 | writeCardData("em", 0, 0, 0, 0, hexData, verify, "") 175 | } 176 | 177 | if verify { 178 | verifyCardData("em", 0, 0, 0, hexData, "") 179 | } 180 | } 181 | 182 | func handlePIV(uid string, simulate bool, write, verify bool) { 183 | if simulate { 184 | simulateCardData("piv", 0, 0, 0, 0, "", uid) 185 | return 186 | } 187 | 188 | if write { 189 | WriteStatusInfo("Writing UID to rewritable MIFARE card...") 190 | WriteStatusInfo("Command: hf mf csetuid -u %s", uid) 191 | WriteStatusInfo("Note: This emulates Wiegand signal only (experimental)") 192 | writeCardData("piv", 0, 0, 0, 0, "", verify, uid) 193 | } 194 | 195 | if verify { 196 | verifyCardData("piv", 0, 0, 0, "", uid) 197 | } 198 | } 199 | 200 | func handleMIFARE(uid string, simulate bool, write, verify bool) { 201 | if simulate { 202 | simulateCardData("mifare", 0, 0, 0, 0, "", uid) 203 | return 204 | } 205 | 206 | if write { 207 | WriteStatusInfo("Writing UID to rewritable MIFARE card...") 208 | WriteStatusInfo("Command: hf mf csetuid -u %s", uid) 209 | WriteStatusInfo("Note: This emulates Wiegand signal only (experimental)") 210 | writeCardData("mifare", 0, 0, 0, 0, "", verify, uid) 211 | } 212 | 213 | if verify { 214 | verifyCardData("mifare", 0, 0, 0, "", uid) 215 | } 216 | } 217 | 218 | func handleAvigilon(facilityCode, cardNumber, bitLength int, simulate, write, verify bool) { 219 | if simulate { 220 | simulateCardData("avigilon", 0, bitLength, facilityCode, cardNumber, "", "") 221 | return 222 | } 223 | 224 | if bitLength != 56 { 225 | WriteStatusError("Unsupported bit length for Avigilon card. Only 56-bit is supported") 226 | return 227 | } 228 | 229 | if write { 230 | WriteStatusInfo("Writing to T5577 card...") 231 | WriteStatusInfo("Command: lf hid clone -w Avig56 --fc %d --cn %d", facilityCode, cardNumber) 232 | writeCardData("avigilon", 0, bitLength, facilityCode, cardNumber, "", verify, "") 233 | } 234 | 235 | if verify { 236 | verifyCardData("avigilon", facilityCode, cardNumber, bitLength, "", "") 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /scripts/wsl_doppelganger_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if running in non-interactive mode (auto-install/update without prompts) 4 | NON_INTERACTIVE="" 5 | PROXMARK_DEVICE="" 6 | 7 | # Parse command line arguments 8 | while [[ $# -gt 0 ]]; do 9 | case $1 in 10 | --update|--non-interactive) 11 | NON_INTERACTIVE="true" 12 | shift 13 | ;; 14 | --device) 15 | PROXMARK_DEVICE="$2" 16 | shift 2 17 | ;; 18 | *) 19 | shift 20 | ;; 21 | esac 22 | done 23 | 24 | # Function to check if a command exists 25 | command_exists() { 26 | command -v "$1" &> /dev/null 27 | } 28 | 29 | # Function to prompt for reinstallation 30 | prompt_reinstall() { 31 | # If in non-interactive mode, always proceed without prompting 32 | if [ "$NON_INTERACTIVE" = "true" ]; then 33 | echo "Non-interactive mode: Installing/updating $1..." 34 | return 0 35 | fi 36 | 37 | read -p "$1 is already installed. Do you want to reinstall it? (y/n): " choice 38 | case "$choice" in 39 | y|Y ) return 0;; 40 | n|N ) return 1;; 41 | * ) echo "Invalid choice. Skipping reinstallation."; return 1;; 42 | esac 43 | } 44 | 45 | # Function to select Proxmark3 device type 46 | select_proxmark_device() { 47 | # Skip interactive prompt if device was provided as parameter 48 | if [ -n "$PROXMARK_DEVICE" ]; then 49 | # Map Windows installer device names to internal names 50 | case "$PROXMARK_DEVICE" in 51 | "rdv4-blueshark") 52 | PROXMARK_DEVICE="rdv4_bt" 53 | echo "Using provided device: Proxmark3 RDV4 with Blueshark" 54 | ;; 55 | "rdv4-no-blueshark") 56 | PROXMARK_DEVICE="rdv4" 57 | echo "Using provided device: Proxmark3 RDV4 (without Blueshark)" 58 | ;; 59 | "easy-512kb") 60 | PROXMARK_DEVICE="easy512" 61 | echo "Using provided device: Proxmark3 Easy (512KB)" 62 | ;; 63 | *) 64 | # Already in internal format, just use it 65 | echo "Using provided device type: $PROXMARK_DEVICE" 66 | ;; 67 | esac 68 | return 69 | fi 70 | 71 | # Skip interactive prompt in non-interactive mode 72 | if [ "$NON_INTERACTIVE" = "true" ]; then 73 | echo "Non-interactive mode: Using default Proxmark3 RDV4 with Blueshark" 74 | PROXMARK_DEVICE="rdv4_bt" 75 | return 76 | fi 77 | 78 | echo "" 79 | echo "==============================================" 80 | echo " Select your Proxmark3 device type:" 81 | echo "==============================================" 82 | echo "1) Proxmark3 RDV4 with Blueshark" 83 | echo "2) Proxmark3 RDV4 (without Blueshark)" 84 | echo "3) Proxmark3 Easy (512KB)" 85 | echo "" 86 | read -p "Enter your choice (1-3): " device_choice 87 | 88 | case "$device_choice" in 89 | 1) 90 | echo "Selected: Proxmark3 RDV4 with Blueshark" 91 | PROXMARK_DEVICE="rdv4_bt" 92 | ;; 93 | 2) 94 | echo "Selected: Proxmark3 RDV4 (without Blueshark)" 95 | PROXMARK_DEVICE="rdv4" 96 | ;; 97 | 3) 98 | echo "Selected: Proxmark3 Easy (512KB)" 99 | PROXMARK_DEVICE="easy512" 100 | ;; 101 | *) 102 | echo "Invalid choice. Defaulting to Proxmark3 RDV4 with Blueshark" 103 | PROXMARK_DEVICE="rdv4_bt" 104 | ;; 105 | esac 106 | } 107 | 108 | # Function to configure Proxmark3 based on device type 109 | configure_proxmark_device() { 110 | local device_type=$1 111 | 112 | case "$device_type" in 113 | "rdv4_bt") 114 | echo "Configuring Proxmark3 RDV4 with Blueshark..." 115 | cp Makefile.platform.sample Makefile.platform 116 | sed -i 's/^#PLATFORM=PM3RDV4/PLATFORM=PM3RDV4/' Makefile.platform 117 | sed -i 's/#PLATFORM_EXTRAS=BTADDON/PLATFORM_EXTRAS=BTADDON/' Makefile.platform 118 | ;; 119 | "rdv4") 120 | echo "Configuring Proxmark3 RDV4 (no Blueshark)..." 121 | cp Makefile.platform.sample Makefile.platform 122 | sed -i 's/^#PLATFORM=PM3RDV4/PLATFORM=PM3RDV4/' Makefile.platform 123 | sed -i 's/^PLATFORM_EXTRAS=BTADDON/#PLATFORM_EXTRAS=BTADDON/' Makefile.platform 124 | ;; 125 | "easy512") 126 | echo "Configuring Proxmark3 Easy (512KB)..." 127 | cp Makefile.platform.sample Makefile.platform 128 | sed -i 's/^#PLATFORM=PM3GENERIC/PLATFORM=PM3GENERIC/' Makefile.platform 129 | sed -i 's/^#PLATFORM_SIZE=512/PLATFORM_SIZE=512/' Makefile.platform 130 | ;; 131 | esac 132 | } 133 | 134 | # Update and upgrade system packages 135 | echo "Updating system packages..." 136 | sudo apt update 137 | sudo apt upgrade -y 138 | 139 | # Fix locale for GUI applications (critical for Fyne in WSL2) 140 | echo "Setting up locale..." 141 | sudo apt install -y locales 142 | sudo locale-gen en_US.UTF-8 143 | sudo update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 144 | 145 | # Add locale to shell profile if not already present 146 | if ! grep -q "export LANG=en_US.UTF-8" ~/.bashrc; then 147 | echo "export LANG=en_US.UTF-8" >> ~/.bashrc 148 | echo "export LC_ALL=en_US.UTF-8" >> ~/.bashrc 149 | fi 150 | 151 | # Check if doppelganger_assistant is installed 152 | if command_exists doppelganger_assistant; then 153 | if ! prompt_reinstall "Doppelganger Assistant"; then 154 | echo "Skipping Doppelganger Assistant installation." 155 | skip_doppelganger_install=true 156 | fi 157 | fi 158 | 159 | # Check if Proxmark3 is installed 160 | if command_exists pm3; then 161 | if ! prompt_reinstall "Proxmark3"; then 162 | echo "Skipping Proxmark3 installation." 163 | skip_proxmark_install=true 164 | fi 165 | fi 166 | 167 | # Install necessary packages for Doppelganger Assistant 168 | if [ -z "$skip_doppelganger_install" ]; then 169 | sudo apt install -y libgl1 xterm make git 170 | 171 | TIMESTAMP=$(date +%s) 172 | if [ "$(uname -m)" = "x86_64" ]; then 173 | wget --no-cache --no-cookies "https://github.com/tweathers-sec/doppelganger_assistant/releases/latest/download/doppelganger_assistant_linux_amd64.tar.xz?t=${TIMESTAMP}" -O doppelganger_assistant_linux_amd64.tar.xz 174 | else 175 | wget --no-cache --no-cookies "https://github.com/tweathers-sec/doppelganger_assistant/releases/latest/download/doppelganger_assistant_linux_arm64.tar.xz?t=${TIMESTAMP}" -O doppelganger_assistant_linux_arm64.tar.xz 176 | fi 177 | 178 | tar xvf doppelganger_assistant_*.tar.xz 179 | cd doppelganger_assistant 180 | sudo make install 181 | 182 | rm -rf usr/ 183 | rm doppelganger_assistant* 184 | rm Makefile 185 | fi 186 | 187 | # Install dependencies for Proxmark3 188 | if [ -z "$skip_proxmark_install" ]; then 189 | echo "Installing/Updating Proxmark3..." 190 | sudo apt install --no-install-recommends -y git ca-certificates build-essential pkg-config \ 191 | libreadline-dev gcc-arm-none-eabi libnewlib-dev qtbase5-dev \ 192 | libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev 193 | 194 | mkdir -p ~/src 195 | cd ~/src 196 | if [ ! -d "proxmark3" ]; then 197 | echo "Cloning Proxmark3 repository..." 198 | git clone https://github.com/RfidResearchGroup/proxmark3.git 199 | else 200 | echo "Updating existing Proxmark3 repository..." 201 | cd proxmark3 202 | git fetch origin 203 | git pull origin master 204 | cd ~/src 205 | fi 206 | 207 | cd proxmark3 208 | 209 | select_proxmark_device 210 | configure_proxmark_device "$PROXMARK_DEVICE" 211 | echo "Building Proxmark3... (this may take several minutes)" 212 | make clean && make -j$(nproc) 213 | echo "Installing Proxmark3..." 214 | sudo make install PREFIX=/usr/local 215 | 216 | echo "Proxmark3 installation/update complete!" 217 | fi 218 | 219 | echo "" 220 | echo "=========================================" 221 | echo " Installation Complete!" 222 | echo "=========================================" 223 | echo "" 224 | echo "To launch Doppelganger Assistant:" 225 | echo " doppelganger_assistant" 226 | echo "" 227 | echo "To use Proxmark3:" 228 | echo " pm3" 229 | echo "" -------------------------------------------------------------------------------- /src/card_verifier.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | func verifyCardData(cardType string, facilityCode, cardNumber, bitLength int, hexData string, uid string) { 11 | if ok, msg := checkProxmark3(); !ok { 12 | WriteStatusError(msg) 13 | return 14 | } 15 | 16 | pm3Binary, err := getPm3Path() 17 | if err != nil { 18 | WriteStatusError("Failed to find pm3 binary: %v", err) 19 | return 20 | } 21 | 22 | device, err := getPm3Device() 23 | if err != nil { 24 | WriteStatusError("Failed to detect pm3 device: %v", err) 25 | return 26 | } 27 | 28 | fmt.Println("\n|----------- VERIFICATION -----------|") 29 | WriteStatusProgress("Verifying card data - place card flat on reader...") 30 | 31 | if IsOperationCancelled() { 32 | WriteStatusInfo("Operation cancelled by user") 33 | return 34 | } 35 | 36 | var cmd *exec.Cmd 37 | switch cardType { 38 | case "iclass": 39 | // Use dump to get full card data, then decrypt and decode block 7 for verification 40 | cmd = exec.Command(pm3Binary, "-c", "hf iclass dump --ki 0", "-p", device) 41 | case "prox": 42 | cmd = exec.Command(pm3Binary, "-c", "lf hid reader", "-p", device) 43 | case "awid": 44 | cmd = exec.Command(pm3Binary, "-c", "lf awid reader", "-p", device) 45 | case "indala": 46 | cmd = exec.Command(pm3Binary, "-c", "lf indala reader", "-p", device) 47 | case "avigilon": 48 | cmd = exec.Command(pm3Binary, "-c", "lf hid reader", "-p", device) 49 | case "em": 50 | cmd = exec.Command(pm3Binary, "-c", "lf em 410x reader", "-p", device) 51 | case "piv", "mifare": 52 | cmd = exec.Command(pm3Binary, "-c", "hf mf info", "-p", device) 53 | default: 54 | WriteStatusError("Unsupported card type for verification") 55 | return 56 | } 57 | 58 | output, cmdErr := cmd.CombinedOutput() 59 | if cmdErr != nil { 60 | WriteStatusError("Failed to read card data: %v", cmdErr) 61 | return 62 | } 63 | 64 | outputStr := string(output) 65 | fmt.Println(outputStr) 66 | 67 | if cardType == "iclass" { 68 | // Parse the dump output to get FC/CN/bit length 69 | cardData, _ := parseICLASSReaderOutput(outputStr) 70 | 71 | // Check if FC/CN is available, decrypt if needed 72 | if _, hasFC := cardData["facilityCode"]; !hasFC { 73 | // Check if block 7 is encrypted (shows as "Enc Cred" in dump) 74 | if strings.Contains(outputStr, "Enc Cred") && !strings.Contains(outputStr, "Block 7 decoder") { 75 | // Card is encrypted, need to decrypt first 76 | WriteStatusInfo("Card appears encrypted. Attempting to decrypt...") 77 | 78 | // Extract dump filename from output 79 | dumpFileRegex := regexp.MustCompile(`Saved.*?to binary file ` + "`" + `([^` + "`" + `]+)` + "`") 80 | if matches := dumpFileRegex.FindStringSubmatch(outputStr); len(matches) > 1 { 81 | if IsOperationCancelled() { 82 | WriteStatusInfo("Operation cancelled by user") 83 | return 84 | } 85 | dumpFile := matches[1] 86 | fmt.Println() 87 | fmt.Printf("hf iclass decrypt -f %s\n", dumpFile) 88 | 89 | // Run decrypt command 90 | decryptCmd := exec.Command(pm3Binary, "-c", fmt.Sprintf("hf iclass decrypt -f %s", dumpFile), "-p", device) 91 | decryptOutput, decryptErr := decryptCmd.CombinedOutput() 92 | if decryptErr == nil { 93 | decryptStr := string(decryptOutput) 94 | fmt.Println(decryptStr) 95 | 96 | cardData, _ = parseICLASSReaderOutput(decryptStr) 97 | 98 | // Fallback: decode block 7 hex if FC/CN not found 99 | if _, hasFC := cardData["facilityCode"]; !hasFC { 100 | block7Hex := extractBlock7Hex(decryptStr) 101 | if block7Hex != "" { 102 | WriteStatusInfo("Attempting alternative decode of block 7 hex...") 103 | fmt.Println() 104 | fmt.Printf("wiegand decode --raw %s --force\n", block7Hex) 105 | 106 | decodeCmd := exec.Command(pm3Binary, "-c", fmt.Sprintf("wiegand decode --raw %s --force", block7Hex), "-p", device) 107 | decodeOutput, _ := decodeCmd.CombinedOutput() 108 | fmt.Println(string(decodeOutput)) 109 | 110 | decodedData, _ := parseICLASSReaderOutput(string(decodeOutput)) 111 | if decodedData != nil { 112 | // Merge decoded data into cardData 113 | if fc, ok := decodedData["facilityCode"].(int); ok && fc > 0 { 114 | cardData["facilityCode"] = fc 115 | } 116 | if cn, ok := decodedData["cardNumber"].(int); ok && cn > 0 { 117 | cardData["cardNumber"] = cn 118 | } 119 | if format, ok := decodedData["format"].(string); ok { 120 | cardData["format"] = format 121 | } 122 | if bl, ok := decodedData["bitLength"].(int); ok { 123 | cardData["bitLength"] = bl 124 | } 125 | } 126 | } 127 | } 128 | } 129 | } 130 | } 131 | } 132 | 133 | // Verify FC/CN/bit length match 134 | if cardData != nil { 135 | readFC, hasFC := cardData["facilityCode"].(int) 136 | readCN, hasCN := cardData["cardNumber"].(int) 137 | readBL, hasBL := cardData["bitLength"].(int) 138 | 139 | if hasFC && hasCN && hasBL { 140 | if readFC == facilityCode && readCN == cardNumber && readBL == bitLength { 141 | WriteStatusSuccess("Verification successful - FC, CN, and Bit Length match") 142 | WriteStatusSuccess("Card contains: %d-bit, FC: %d, CN: %d", readBL, readFC, readCN) 143 | return 144 | } else { 145 | WriteStatusError("Verification failed - data mismatch") 146 | WriteStatusInfo("Expected: %d-bit, FC: %d, CN: %d", bitLength, facilityCode, cardNumber) 147 | WriteStatusInfo("Read: %d-bit, FC: %d, CN: %d", readBL, readFC, readCN) 148 | return 149 | } 150 | } else if hasFC && hasCN { 151 | // Verify FC/CN if bit length is not available 152 | if readFC == facilityCode && readCN == cardNumber { 153 | WriteStatusSuccess("Verification successful - FC and CN match") 154 | WriteStatusInfo("Card contains: FC: %d, CN: %d", readFC, readCN) 155 | if hasBL { 156 | WriteStatusInfo("Bit Length: %d (expected %d)", readBL, bitLength) 157 | } 158 | return 159 | } else { 160 | WriteStatusError("Verification failed - FC/CN mismatch") 161 | WriteStatusInfo("Expected: FC: %d, CN: %d", facilityCode, cardNumber) 162 | WriteStatusInfo("Read: FC: %d, CN: %d", readFC, readCN) 163 | return 164 | } 165 | } 166 | } 167 | 168 | WriteStatusError("Verification failed - unable to decode card data") 169 | return 170 | } else { 171 | lines := strings.Split(outputStr, "\n") 172 | for _, line := range lines { 173 | if cardType == "awid" || cardType == "indala" { 174 | if strings.Contains(line, fmt.Sprintf("FC: %d", facilityCode)) && strings.Contains(line, fmt.Sprintf("Card: %d", cardNumber)) { 175 | WriteStatusSuccess("Verification successful - FC and CN match") 176 | return 177 | } 178 | } else if cardType == "avigilon" { 179 | if strings.Contains(line, "[Avig56") && strings.Contains(line, fmt.Sprintf("FC: %d", facilityCode)) && strings.Contains(line, fmt.Sprintf("CN: %d", cardNumber)) { 180 | WriteStatusSuccess("Verification successful - Avigilon FC and CN match") 181 | return 182 | } 183 | } else if cardType == "em" { 184 | if strings.Contains(line, fmt.Sprintf("EM 410x ID %s", hexData)) { 185 | WriteStatusSuccess("Verification successful - EM4100 / Net2 ID matches") 186 | return 187 | } 188 | } else if cardType == "piv" || cardType == "mifare" { 189 | if strings.Contains(line, "[+] UID:") { 190 | uidStartIndex := strings.Index(line, "[+] UID:") + len("[+] UID:") 191 | extractedUID := strings.TrimSpace(line[uidStartIndex:]) 192 | normalizedUID := strings.ToUpper(strings.ReplaceAll(extractedUID, " ", "")) 193 | if normalizedUID == strings.ToUpper(uid) { 194 | WriteStatusSuccess("Verification successful - UID matches") 195 | return 196 | } 197 | } 198 | } else { 199 | if strings.Contains(line, fmt.Sprintf("FC: %d", facilityCode)) && strings.Contains(line, fmt.Sprintf("CN: %d", cardNumber)) { 200 | WriteStatusSuccess("Verification successful - FC and CN match") 201 | return 202 | } 203 | } 204 | } 205 | } 206 | 207 | if cardType == "piv" || cardType == "mifare" { 208 | WriteStatusError("Verification failed - UID does not match or card read failed") 209 | } else if cardType == "em" { 210 | WriteStatusError("Verification failed - EM4100 / Net2 ID does not match or card read failed") 211 | } else { 212 | WriteStatusError("Verification failed - FC/CN do not match or card read failed") 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /installers/doppelganger_install_macos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Function to print colored messages 4 | print_color() { 5 | COLOR=$1 6 | MESSAGE=$2 7 | echo -e "\033[${COLOR}m${MESSAGE}\033[0m" 8 | } 9 | 10 | # Function to display ASCII art 11 | display_doppelganger_ascii() { 12 | print_color "1;31" ' 13 | 14 | ____ _ 15 | | _ \ ___ _ __ _ __ ___| | __ _ __ _ _ __ __ _ ___ _ __ 16 | | | | |/ _ \| '"'"'_ \| '"'"'_ \ / _ \ |/ _` |/ _` | '"'"'_ \ / _` |/ _ \ '"'"'__| 17 | | |_| | (_) | |_) | |_) | __/ | (_| | (_| | | | | (_| | __/ | 18 | |____/ \___/| .__/| .__/ \___|_|\__, |\__,_|_| |_|\__, |\___|_| 19 | |_| |_| |___/ |___/ 20 | 21 | ' 22 | } 23 | 24 | # Display ASCII art 25 | display_doppelganger_ascii 26 | 27 | print_color "1;32" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" 28 | print_color "1;32" " Doppelgänger Assistant Installer for macOS" 29 | print_color "1;32" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" 30 | echo "" 31 | 32 | if [[ $(uname -m) == "x86_64" ]]; then 33 | ARCH="amd64" 34 | ARCH_NAME="Intel (x86_64)" 35 | elif [[ $(uname -m) == "arm64" ]]; then 36 | ARCH="arm64" 37 | ARCH_NAME="Apple Silicon (arm64)" 38 | else 39 | print_color "1;31" "[✗] Unsupported architecture: $(uname -m)" 40 | exit 1 41 | fi 42 | 43 | print_color "1;36" "System: $ARCH_NAME" 44 | echo "" 45 | 46 | # Ask about Proxmark3 47 | INSTALL_PM3=0 48 | BREW_FLAGS="" 49 | 50 | if ! command -v pm3 &> /dev/null; then 51 | print_color "1;33" "Proxmark3 (Iceman) is not currently installed." 52 | read -p "Would you like to install it? (y/n) " -n 1 -r 53 | echo "" 54 | 55 | if [[ $REPLY =~ ^[Yy]$ ]]; then 56 | INSTALL_PM3=1 57 | echo "" 58 | print_color "1;36" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" 59 | print_color "1;36" " Select your Proxmark3 device type:" 60 | print_color "1;36" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" 61 | echo "" 62 | print_color "1;32" " 1) Proxmark3 RDV4 with Blueshark" 63 | print_color "1;33" " 2) Proxmark3 RDV4 (without Blueshark)" 64 | print_color "1;35" " 3) Proxmark3 Easy (512KB)" 65 | echo "" 66 | read -p "Enter your choice (1-3): " device_choice 67 | echo "" 68 | 69 | case "$device_choice" in 70 | 1) 71 | print_color "1;32" "✓ Selected: Proxmark3 RDV4 with Blueshark" 72 | BREW_FLAGS="--HEAD --with-blueshark" 73 | ;; 74 | 2) 75 | print_color "1;32" "✓ Selected: Proxmark3 RDV4 (without Blueshark)" 76 | BREW_FLAGS="--HEAD" 77 | ;; 78 | 3) 79 | print_color "1;32" "✓ Selected: Proxmark3 Easy (512KB)" 80 | BREW_FLAGS="--HEAD --with-generic" 81 | ;; 82 | *) 83 | print_color "1;33" "⚠ Invalid choice. Defaulting to Proxmark3 RDV4 with Blueshark" 84 | BREW_FLAGS="--HEAD --with-blueshark" 85 | ;; 86 | esac 87 | echo "" 88 | 89 | if ! command -v brew &> /dev/null; then 90 | print_color "1;33" "Homebrew is required to install Proxmark3." 91 | read -p "Would you like to install Homebrew? (y/n) " -n 1 -r 92 | echo "" 93 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 94 | print_color "1;31" "✗ Cannot install Proxmark3 without Homebrew" 95 | INSTALL_PM3=0 96 | fi 97 | echo "" 98 | fi 99 | else 100 | echo "" 101 | fi 102 | else 103 | print_color "1;32" "✓ Proxmark3 (Iceman) is already installed" 104 | echo "" 105 | fi 106 | 107 | 108 | print_color "1;32" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" 109 | print_color "1;32" " Starting Installation" 110 | print_color "1;32" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" 111 | echo "" 112 | 113 | # Download and install Doppelgänger Assistant 114 | TIMESTAMP=$(date +%s) 115 | URL="https://github.com/tweathers-sec/doppelganger_assistant/releases/download/latest/doppelganger_assistant_darwin_${ARCH}.dmg?t=${TIMESTAMP}" 116 | 117 | print_color "1;34" "[•] Downloading Doppelgänger Assistant..." 118 | TMP_DMG=$(mktemp -d)/doppelganger_assistant.dmg 119 | if curl -H "Cache-Control: no-cache" -H "Pragma: no-cache" -L "$URL" -o "$TMP_DMG" 2>/dev/null; then 120 | print_color "1;32" " ✓ Download completed" 121 | else 122 | print_color "1;31" " ✗ Download failed" 123 | exit 1 124 | fi 125 | 126 | print_color "1;34" "[•] Mounting disk image..." 127 | if hdiutil attach "$TMP_DMG" -quiet 2>/dev/null; then 128 | print_color "1;32" " ✓ Disk image mounted" 129 | else 130 | print_color "1;31" " ✗ Failed to mount disk image" 131 | exit 1 132 | fi 133 | 134 | print_color "1;34" "[•] Installing to /Applications..." 135 | if cp -R "/Volumes/doppelganger_assistant_darwin_${ARCH}/doppelganger_assistant.app" /Applications/ 2>/dev/null; then 136 | print_color "1;32" " ✓ Application installed" 137 | else 138 | print_color "1;31" " ✗ Installation failed" 139 | hdiutil detach "/Volumes/doppelganger_assistant_darwin_${ARCH}" -quiet 2>/dev/null 140 | exit 1 141 | fi 142 | 143 | print_color "1;34" "[•] Removing quarantine attributes..." 144 | xattr -cr "/Applications/doppelganger_assistant.app" 145 | print_color "1;32" " ✓ Application authorized" 146 | 147 | print_color "1;34" "[•] Cleaning up..." 148 | hdiutil detach "/Volumes/doppelganger_assistant_darwin_${ARCH}" -quiet 2>/dev/null 149 | rm -f "$TMP_DMG" 150 | print_color "1;32" " ✓ Temporary files removed" 151 | echo "" 152 | 153 | # Configure shell 154 | print_color "1;34" "[•] Configuring shell environment..." 155 | PROFILE_FILE="$HOME/.zprofile" 156 | [[ -f "$HOME/.zshrc" ]] && PROFILE_FILE="$HOME/.zshrc" 157 | ALIAS_LINE="alias doppelganger_assistant='/Applications/doppelganger_assistant.app/Contents/MacOS/doppelganger_assistant'" 158 | 159 | if grep -q "$ALIAS_LINE" "$PROFILE_FILE" 2>/dev/null; then 160 | print_color "1;32" " ✓ Command alias already configured" 161 | else 162 | echo "$ALIAS_LINE" >> "$PROFILE_FILE" 163 | print_color "1;32" " ✓ Command alias added to $PROFILE_FILE" 164 | fi 165 | echo "" 166 | 167 | # Install Proxmark3 if requested 168 | if [[ "$INSTALL_PM3" == "1" ]]; then 169 | print_color "1;32" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" 170 | print_color "1;32" " Installing Proxmark3 (Iceman)" 171 | print_color "1;32" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" 172 | echo "" 173 | 174 | print_color "1;34" "[•] Installing Xcode Command Line Tools..." 175 | xcode-select --install 2>/dev/null || print_color "1;32" " ✓ Already installed" 176 | echo "" 177 | 178 | # Install Homebrew if needed 179 | if ! command -v brew &> /dev/null; then 180 | print_color "1;34" "[•] Installing Homebrew..." 181 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 182 | 183 | if [[ $(uname -m) == "arm64" ]]; then 184 | eval "$(/opt/homebrew/bin/brew shellenv)" 185 | echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> "$PROFILE_FILE" 186 | else 187 | eval "$(/usr/local/bin/brew shellenv)" 188 | echo 'eval "$(/usr/local/bin/brew shellenv)"' >> "$PROFILE_FILE" 189 | fi 190 | 191 | print_color "1;32" " ✓ Homebrew installed and configured" 192 | echo "" 193 | else 194 | print_color "1;32" " ✓ Homebrew already installed" 195 | echo "" 196 | fi 197 | 198 | # Install XQuartz 199 | print_color "1;34" "[•] Installing XQuartz..." 200 | brew install xquartz 2>/dev/null 201 | print_color "1;32" " ✓ XQuartz installed" 202 | echo "" 203 | 204 | # Tap the RfidResearchGroup repo 205 | print_color "1;34" "[•] Adding Proxmark3 repository..." 206 | brew tap RfidResearchGroup/proxmark3 2>/dev/null 207 | print_color "1;32" " ✓ Repository added" 208 | echo "" 209 | 210 | # Install Proxmark3 211 | print_color "1;34" "[•] Installing Proxmark3..." 212 | if brew install $BREW_FLAGS rfidresearchgroup/proxmark3/proxmark3 2>/dev/null; then 213 | print_color "1;32" " ✓ Proxmark3 installed successfully" 214 | else 215 | print_color "1;33" " ⚠ Proxmark3 installation encountered issues" 216 | print_color "0;37" " You may need to run: brew install $BREW_FLAGS rfidresearchgroup/proxmark3/proxmark3" 217 | fi 218 | echo "" 219 | fi 220 | 221 | # Final message 222 | print_color "1;32" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" 223 | print_color "1;32" " Installation Complete!" 224 | print_color "1;32" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" 225 | echo "" 226 | print_color "1;36" "Application Location:" 227 | print_color "0;37" " /Applications/doppelganger_assistant.app" 228 | echo "" 229 | print_color "1;36" "To use the command-line interface:" 230 | print_color "0;37" " Restart your terminal or run: source $PROFILE_FILE" 231 | print_color "0;37" " Then use: doppelganger_assistant" 232 | echo "" 233 | print_color "1;32" "Thank you for installing Doppelgänger Assistant!" 234 | echo "" 235 | -------------------------------------------------------------------------------- /scripts/wsl_pm3_terminal.ps1: -------------------------------------------------------------------------------- 1 | # Log file path 2 | $logFile = "C:\doppelganger_assistant\launch_pm3_terminal.log" 3 | 4 | # Function to check if a command exists 5 | function CommandExists { 6 | param ( 7 | [string]$command 8 | ) 9 | $null = Get-Command $command -ErrorAction SilentlyContinue 10 | return $? 11 | } 12 | 13 | # Function to log output to both file and screen 14 | function Log { 15 | param ( 16 | [string]$message 17 | ) 18 | $timestamp = (Get-Date).ToString('u') 19 | $logMessage = "$timestamp - $message" 20 | Write-Output $logMessage 21 | Add-Content -Path $logFile -Value $logMessage 22 | } 23 | 24 | # Function to detect which Doppelganger WSL distribution is installed 25 | function Get-DoppelgangerDistro { 26 | $wslList = wsl.exe -l -q | ForEach-Object { $_.Trim() -replace "`0", "" } 27 | $kaliName = "Kali-doppelganger_assistant" 28 | $ubuntuName = "Ubuntu-doppelganger_assistant" 29 | 30 | foreach ($distro in $wslList) { 31 | if ($distro -eq $kaliName) { 32 | return $kaliName 33 | } 34 | elseif ($distro -eq $ubuntuName) { 35 | return $ubuntuName 36 | } 37 | } 38 | return $null 39 | } 40 | 41 | # Function to check if WSL is running 42 | function IsWSLRunning { 43 | $distroName = Get-DoppelgangerDistro 44 | if ($null -eq $distroName) { 45 | return $false 46 | } 47 | $wslOutput = wsl -l -q 48 | return $wslOutput -match $distroName 49 | } 50 | 51 | # Function to start WSL if not running 52 | function StartWSLIfNotRunning { 53 | $distroName = Get-DoppelgangerDistro 54 | if ($null -eq $distroName) { 55 | Log "ERROR: No Doppelganger Assistant WSL distribution found!" 56 | Log "Please run the installer first." 57 | Read-Host "Press Enter to exit" 58 | exit 1 59 | } 60 | 61 | if (-not (IsWSLRunning)) { 62 | Log "WSL is not running. Starting $distroName..." 63 | & wsl -d $distroName --exec echo "WSL started" 64 | Log "WSL started." 65 | } 66 | else { 67 | Log "$distroName is already running." 68 | } 69 | } 70 | 71 | # Function to detach a USB device if it is already attached 72 | function DetachUSBDevice { 73 | param ( 74 | [string]$busId 75 | ) 76 | Log "Detaching device with busid $busId if it is already attached..." 77 | $detachOutput = & usbipd detach --busid $busId 2>&1 | Tee-Object -Variable detachOutputResult 78 | if ($LASTEXITCODE -ne 0) { 79 | Log "Device might not be attached. Exit code: $LASTEXITCODE" 80 | } 81 | else { 82 | Log "Device detached successfully." 83 | } 84 | 85 | # Wait for the device to be fully detached 86 | Start-Sleep -Seconds 1 87 | } 88 | 89 | # Function to attach a USB device to WSL 90 | function AttachUSBDeviceToWSL { 91 | param ( 92 | [string]$busId 93 | ) 94 | 95 | Log "Attaching device with busid $busId to WSL..." 96 | $attachOutput = & usbipd attach --wsl --busid $busId 2>&1 | Tee-Object -Variable attachOutputResult 97 | if ($LASTEXITCODE -ne 0) { 98 | Log "Error attaching device to WSL. Exit code: $LASTEXITCODE" 99 | Log "Attach output: $attachOutputResult" 100 | return $false 101 | } 102 | else { 103 | Log "Device successfully attached to WSL." 104 | return $true 105 | } 106 | } 107 | 108 | # Function to download/update PM3 icon with cache busting 109 | function Ensure-Pm3Icon { 110 | param( 111 | [string]$IconPath 112 | ) 113 | 114 | try { 115 | $dir = Split-Path -Path $IconPath -Parent 116 | if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } 117 | 118 | $timestamp = [int][double]::Parse((Get-Date -UFormat %s)) 119 | $pm3IconUrl = "https://raw.githubusercontent.com/tweathers-sec/doppelganger_assistant/main/img/doppelganger_pm3.ico?t=$timestamp" 120 | 121 | if (Test-Path $IconPath) { 122 | Remove-Item $IconPath -Force 123 | } 124 | 125 | Invoke-WebRequest -Uri $pm3IconUrl -OutFile $IconPath -ErrorAction Stop 126 | Log "Downloaded latest PM3 icon to $IconPath" 127 | 128 | Start-Sleep -Milliseconds 500 129 | } 130 | catch { 131 | Log "WARNING: Failed to download PM3 icon: $_" 132 | } 133 | } 134 | 135 | # Function to ensure Windows Terminal profile exists for Proxmark3 136 | function EnsureWindowsTerminalProfile { 137 | $distroName = Get-DoppelgangerDistro 138 | $settingsPath = "$env:LOCALAPPDATA\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json" 139 | $iconPath = "C:\doppelganger_assistant\doppelganger_pm3.ico" 140 | 141 | Ensure-Pm3Icon -IconPath $iconPath 142 | 143 | # Check if Windows Terminal settings exist 144 | if (Test-Path $settingsPath) { 145 | try { 146 | $settings = Get-Content $settingsPath -Raw | ConvertFrom-Json 147 | 148 | $pm3ProfileExists = $false 149 | $updatedProfiles = @() 150 | foreach ($profile in $settings.profiles.list) { 151 | if ($profile.name -eq "Proxmark3 Terminal") { 152 | $pm3ProfileExists = $true 153 | } 154 | else { 155 | $updatedProfiles += $profile 156 | } 157 | } 158 | 159 | if ($pm3ProfileExists) { 160 | $settings.profiles.list = $updatedProfiles 161 | } 162 | 163 | Log "Creating Windows Terminal Proxmark3 profile..." 164 | 165 | $newProfile = @{ 166 | name = "Proxmark3 Terminal" 167 | commandline = "wsl.exe -d $distroName --exec bash -c 'pm3'" 168 | icon = $iconPath 169 | startingDirectory = "~" 170 | guid = "{" + [guid]::NewGuid().ToString() + "}" 171 | hidden = $false 172 | } 173 | 174 | $settings.profiles.list += $newProfile 175 | 176 | $settings | ConvertTo-Json -Depth 10 | Set-Content $settingsPath -Encoding UTF8 177 | Log "Proxmark3 profile created successfully." 178 | } 179 | catch { 180 | Log "Warning: Could not modify Windows Terminal settings: $_" 181 | Log "Icon may not appear in terminal. You can manually configure it in Windows Terminal settings." 182 | } 183 | } 184 | else { 185 | Log "Windows Terminal settings not found. Icon may not appear." 186 | } 187 | } 188 | 189 | # Function to launch Proxmark3 terminal 190 | function LaunchProxmark3Terminal { 191 | $distroName = Get-DoppelgangerDistro 192 | if ($null -eq $distroName) { 193 | Log "ERROR: No Doppelganger Assistant WSL distribution found!" 194 | Read-Host "Press Enter to exit" 195 | exit 1 196 | } 197 | 198 | EnsureWindowsTerminalProfile 199 | 200 | Log "Launching Proxmark3 terminal in $distroName..." 201 | 202 | wt.exe -w 0 new-tab -p "Proxmark3 Terminal" --title "Proxmark3 Terminal" 203 | 204 | Log "Proxmark3 terminal launched." 205 | } 206 | 207 | # Clear the log file 208 | Clear-Content -Path $logFile -ErrorAction SilentlyContinue 209 | 210 | Log "Starting Proxmark3 Terminal Launcher..." 211 | 212 | # Ensure WSL is running 213 | StartWSLIfNotRunning 214 | 215 | # List all USB devices and find the Proxmark3 device 216 | Log "Scanning for Proxmark3 device (VID: 9ac4)..." 217 | $usbDevices = & usbipd list 2>&1 | Tee-Object -Variable usbDevicesOutput 218 | 219 | # Find the Proxmark3 device by VID 9ac4 and extract the bus ID 220 | $proxmark3Device = $usbDevices | Select-String -Pattern "9ac4" 221 | if ($proxmark3Device) { 222 | $busId = ($proxmark3Device -split "\s+")[0] 223 | Log "Found Proxmark3 device with busid $busId" 224 | 225 | # Detach the device if it is already attached 226 | DetachUSBDevice -busId $busId 227 | 228 | # Bind the Proxmark3 device 229 | Log "Binding Proxmark3 device..." 230 | $bindOutput = & usbipd bind --busid $busId 2>&1 | Tee-Object -Variable bindOutputResult 231 | if ($LASTEXITCODE -ne 0) { 232 | Log "Error binding Proxmark3 device. Exit code: $LASTEXITCODE" 233 | Log "Bind output: $bindOutputResult" 234 | } 235 | else { 236 | # Attach the Proxmark3 device to WSL 237 | $attached = AttachUSBDeviceToWSL -busId $busId 238 | 239 | if ($attached) { 240 | Start-Sleep -Seconds 2 241 | LaunchProxmark3Terminal 242 | } 243 | else { 244 | Log "Failed to attach Proxmark3 device. Cannot launch pm3." 245 | Read-Host "Press Enter to exit" 246 | exit 1 247 | } 248 | } 249 | } 250 | else { 251 | Log "Proxmark3 device not found." 252 | $userChoice = Read-Host "Proxmark3 device not detected. Do you want to (A)ttach the device and retry, or (E)xit? [A/E]" 253 | 254 | if ($userChoice -eq "A" -or $userChoice -eq "a") { 255 | Log "User chose to attach the device. Please connect the Proxmark3 device." 256 | Read-Host "Press Enter when you have connected the Proxmark3 device" 257 | 258 | $usbDevices = & usbipd list 259 | $proxmark3Device = $usbDevices | Select-String -Pattern "9ac4" 260 | 261 | if ($proxmark3Device) { 262 | Log "Proxmark3 device found. Restarting the script." 263 | & $MyInvocation.MyCommand.Path 264 | exit 265 | } 266 | else { 267 | Log "Proxmark3 device still not found. Exiting." 268 | Read-Host "Press Enter to exit" 269 | exit 1 270 | } 271 | } 272 | else { 273 | Log "User chose to exit." 274 | exit 1 275 | } 276 | } 277 | 278 | Start-Sleep -Seconds 2 279 | exit 280 | 281 | -------------------------------------------------------------------------------- /src/card_writer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | func writeProxmark3Command(command string) (string, error) { 11 | if ok, msg := checkProxmark3(); !ok { 12 | return "", fmt.Errorf("%s", msg) 13 | } 14 | 15 | pm3Binary, err := getPm3Path() 16 | if err != nil { 17 | return "", fmt.Errorf("failed to find pm3 binary: %w", err) 18 | } 19 | 20 | device, err := getPm3Device() 21 | if err != nil { 22 | return "", fmt.Errorf("failed to detect pm3 device: %w", err) 23 | } 24 | 25 | cmd := exec.Command(pm3Binary, "-c", command, "-p", device) 26 | output, err := cmd.CombinedOutput() 27 | if err != nil { 28 | return string(output), fmt.Errorf("error writing to card: %w. Output: %s", err, output) 29 | } 30 | return string(output), nil 31 | } 32 | 33 | // waitForProxmark3 checks if Proxmark3 is available with retries. 34 | func waitForProxmark3(maxRetries int) bool { 35 | for i := 0; i < maxRetries; i++ { 36 | pm3Binary, err := getPm3Path() 37 | if err != nil { 38 | if i < maxRetries-1 { 39 | fmt.Printf("Waiting for Proxmark3 to be ready... (attempt %d/%d)\n", i+1, maxRetries) 40 | time.Sleep(2 * time.Second) 41 | } 42 | continue 43 | } 44 | 45 | device, err := getPm3Device() 46 | if err != nil { 47 | if i < maxRetries-1 { 48 | fmt.Printf("Waiting for Proxmark3 to be ready... (attempt %d/%d)\n", i+1, maxRetries) 49 | time.Sleep(2 * time.Second) 50 | } 51 | continue 52 | } 53 | 54 | cmd := exec.Command(pm3Binary, "-c", "hw status", "-p", device) 55 | output, err := cmd.CombinedOutput() 56 | if err == nil && !strings.Contains(string(output), "cannot communicate") { 57 | return true 58 | } 59 | if i < maxRetries-1 { 60 | fmt.Printf("Waiting for Proxmark3 to be ready... (attempt %d/%d)\n", i+1, maxRetries) 61 | time.Sleep(2 * time.Second) 62 | } 63 | } 64 | return false 65 | } 66 | 67 | func writeCardData(cardType string, cardData uint64, bitLength int, facilityCode int, cardNumber int, hexData string, verify bool, formatCodeOrUID string) { 68 | switch cardType { 69 | case "iclass": 70 | fmt.Println("\n|----------- WRITE -----------|") 71 | WriteStatusProgress("Encoding iCLASS card data...") 72 | formatCode := formatCodeOrUID 73 | command := fmt.Sprintf("hf iclass encode -w %s --fc %d --cn %d --ki 0", formatCode, facilityCode, cardNumber) 74 | output, err := writeProxmark3Command(command) 75 | if err != nil { 76 | WriteStatusError("Failed to write iCLASS card: %v", err) 77 | fmt.Println(output) 78 | } else { 79 | fmt.Println(output) 80 | if verify { 81 | WriteStatusSuccess("Write complete - starting verification") 82 | } else { 83 | WriteStatusSuccess("Write complete") 84 | } 85 | } 86 | case "prox": 87 | WriteStatusProgress("Writing Prox card (5 attempts)...") 88 | 89 | for i := 0; i < 5; i++ { 90 | if IsOperationCancelled() { 91 | WriteStatusInfo("Operation cancelled by user") 92 | return 93 | } 94 | fmt.Printf("\n|----------- WRITE #%d -----------|\n", i+1) 95 | var output string 96 | var err error 97 | if bitLength == 26 { 98 | output, err = writeProxmark3Command(fmt.Sprintf("lf hid clone -w H10301 --fc %d --cn %d", facilityCode, cardNumber)) 99 | } else if bitLength == 30 { 100 | output, err = writeProxmark3Command(fmt.Sprintf("lf hid clone -w ATSW30 --fc %d --cn %d", facilityCode, cardNumber)) 101 | } else if bitLength == 31 { 102 | output, err = writeProxmark3Command(fmt.Sprintf("lf hid clone -w ADT31 --fc %d --cn %d", facilityCode, cardNumber)) 103 | } else if bitLength == 33 { 104 | output, err = writeProxmark3Command(fmt.Sprintf("lf hid clone -w D10202 --fc %d --cn %d", facilityCode, cardNumber)) 105 | } else if bitLength == 34 { 106 | output, err = writeProxmark3Command(fmt.Sprintf("lf hid clone -w H10306 --fc %d --cn %d", facilityCode, cardNumber)) 107 | } else if bitLength == 35 { 108 | output, err = writeProxmark3Command(fmt.Sprintf("lf hid clone -w C1k35s --fc %d --cn %d", facilityCode, cardNumber)) 109 | } else if bitLength == 36 { 110 | output, err = writeProxmark3Command(fmt.Sprintf("lf hid clone -w S12906 --fc %d --cn %d", facilityCode, cardNumber)) 111 | } else if bitLength == 37 { 112 | output, err = writeProxmark3Command(fmt.Sprintf("lf hid clone -w H10304 --fc %d --cn %d", facilityCode, cardNumber)) 113 | } else if bitLength == 46 { 114 | output, err = writeProxmark3Command(fmt.Sprintf("lf hid clone -w H800002 --fc %d --cn %d", facilityCode, cardNumber)) 115 | } else if bitLength == 48 { 116 | output, err = writeProxmark3Command(fmt.Sprintf("lf hid clone -w C1k48s --fc %d --cn %d", facilityCode, cardNumber)) 117 | } 118 | if err != nil { 119 | WriteStatusError("Write attempt #%d failed: %v", i+1, err) 120 | } else { 121 | fmt.Println(output) 122 | } 123 | if IsOperationCancelled() { 124 | WriteStatusInfo("Operation cancelled by user") 125 | return 126 | } 127 | time.Sleep(1 * time.Second) 128 | if i < 4 { 129 | WriteStatusProgress("Move card slowly... Write attempt #%d complete", i+1) 130 | } else { 131 | if verify { 132 | WriteStatusSuccess("All 5 write attempts complete - starting verification") 133 | } else { 134 | WriteStatusSuccess("All 5 write attempts complete") 135 | } 136 | } 137 | } 138 | case "awid": 139 | WriteStatusProgress("Writing AWID card (5 attempts)...") 140 | for i := 0; i < 5; i++ { 141 | if IsOperationCancelled() { 142 | WriteStatusInfo("Operation cancelled by user") 143 | return 144 | } 145 | fmt.Printf("\n|----------- WRITE #%d -----------|\n", i+1) 146 | output, err := writeProxmark3Command(fmt.Sprintf("lf awid clone --fmt 26 --fc %d --cn %d", facilityCode, cardNumber)) 147 | if err != nil { 148 | WriteStatusError("Write attempt #%d failed: %v", i+1, err) 149 | } else { 150 | fmt.Println(output) 151 | } 152 | if IsOperationCancelled() { 153 | WriteStatusInfo("Operation cancelled by user") 154 | return 155 | } 156 | time.Sleep(1 * time.Second) 157 | if i < 4 { 158 | WriteStatusProgress("Move card slowly... Write attempt #%d complete", i+1) 159 | } else { 160 | if verify { 161 | WriteStatusSuccess("All 5 write attempts complete - starting verification") 162 | } else { 163 | WriteStatusSuccess("All 5 write attempts complete") 164 | } 165 | } 166 | } 167 | case "indala": 168 | WriteStatusProgress("Writing Indala card (5 attempts)...") 169 | for i := 0; i < 5; i++ { 170 | if IsOperationCancelled() { 171 | WriteStatusInfo("Operation cancelled by user") 172 | return 173 | } 174 | fmt.Printf("\n|----------- WRITE #%d -----------|\n", i+1) 175 | output, err := writeProxmark3Command(fmt.Sprintf("lf indala clone --fc %d --cn %d", facilityCode, cardNumber)) 176 | if err != nil { 177 | WriteStatusError("Write attempt #%d failed: %v", i+1, err) 178 | } else { 179 | fmt.Println(output) 180 | } 181 | if IsOperationCancelled() { 182 | WriteStatusInfo("Operation cancelled by user") 183 | return 184 | } 185 | time.Sleep(1 * time.Second) 186 | if i < 4 { 187 | WriteStatusProgress("Move card slowly... Write attempt #%d complete", i+1) 188 | } else { 189 | if verify { 190 | WriteStatusSuccess("All 5 write attempts complete - starting verification") 191 | } else { 192 | WriteStatusSuccess("All 5 write attempts complete") 193 | } 194 | } 195 | } 196 | case "avigilon": 197 | WriteStatusProgress("Writing Avigilon card (5 attempts)...") 198 | for i := 0; i < 5; i++ { 199 | if IsOperationCancelled() { 200 | WriteStatusInfo("Operation cancelled by user") 201 | return 202 | } 203 | fmt.Printf("\n|----------- WRITE #%d -----------|\n", i+1) 204 | output, err := writeProxmark3Command(fmt.Sprintf("lf hid clone -w Avig56 --fc %d --cn %d", facilityCode, cardNumber)) 205 | if err != nil { 206 | WriteStatusError("Write attempt #%d failed: %v", i+1, err) 207 | } else { 208 | fmt.Println(output) 209 | } 210 | if IsOperationCancelled() { 211 | WriteStatusInfo("Operation cancelled by user") 212 | return 213 | } 214 | time.Sleep(1 * time.Second) 215 | if i < 4 { 216 | WriteStatusProgress("Move card slowly... Write attempt #%d complete", i+1) 217 | } else { 218 | if verify { 219 | WriteStatusSuccess("All 5 write attempts complete - starting verification") 220 | } else { 221 | WriteStatusSuccess("All 5 write attempts complete") 222 | } 223 | } 224 | } 225 | case "em": 226 | WriteStatusProgress("Writing EM4100 / Net2 card (5 attempts)...") 227 | for i := 0; i < 5; i++ { 228 | if IsOperationCancelled() { 229 | WriteStatusInfo("Operation cancelled by user") 230 | return 231 | } 232 | fmt.Printf("\n|----------- WRITE #%d -----------|\n", i+1) 233 | output, err := writeProxmark3Command(fmt.Sprintf("lf em 410x clone --id %s", hexData)) 234 | if err != nil { 235 | WriteStatusError("Write attempt #%d failed: %v", i+1, err) 236 | } else { 237 | fmt.Println(output) 238 | } 239 | if IsOperationCancelled() { 240 | WriteStatusInfo("Operation cancelled by user") 241 | return 242 | } 243 | time.Sleep(1 * time.Second) 244 | if i < 4 { 245 | WriteStatusProgress("Move card slowly... Write attempt #%d complete", i+1) 246 | } else { 247 | if verify { 248 | WriteStatusSuccess("All 5 write attempts complete - starting verification") 249 | } else { 250 | WriteStatusSuccess("All 5 write attempts complete") 251 | } 252 | } 253 | } 254 | case "piv", "mifare": 255 | fmt.Println("\n|----------- WRITE -----------|") 256 | WriteStatusProgress("Writing UID to card...") 257 | uid := formatCodeOrUID 258 | command := fmt.Sprintf("hf mf csetuid -u %s", uid) 259 | output, err := writeProxmark3Command(command) 260 | if err != nil { 261 | WriteStatusError("Failed to write UID: %v", err) 262 | fmt.Println(output) 263 | } else { 264 | fmt.Println(output) 265 | WriteStatusSuccess("UID written successfully") 266 | } 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "runtime" 9 | "strings" 10 | "time" 11 | 12 | "golang.org/x/term" 13 | ) 14 | 15 | func bitCount(intType uint64) int { 16 | count := 0 17 | for intType != 0 { 18 | intType &= intType - 1 19 | count++ 20 | } 21 | return count 22 | } 23 | 24 | // findPm3Path attempts to find the pm3 binary using shell commands 25 | func findPm3Path() (string, error) { 26 | var cmd *exec.Cmd 27 | 28 | if runtime.GOOS == "windows" { 29 | // On Windows, use 'where' command 30 | cmd = exec.Command("cmd", "/c", "where", "pm3") 31 | output, err := cmd.CombinedOutput() 32 | if err == nil { 33 | outputStr := string(output) 34 | lines := strings.Split(outputStr, "\n") 35 | for _, line := range lines { 36 | line = strings.TrimSpace(line) 37 | if line != "" && strings.Contains(line, ":\\") { 38 | return line, nil 39 | } 40 | } 41 | } 42 | return "", fmt.Errorf("pm3 binary not found") 43 | } 44 | 45 | // On macOS and Linux, use 'command -v' which works in sh/bash/zsh 46 | // Use /bin/sh to ensure we have a shell, and 'command -v' is POSIX standard 47 | cmd = exec.Command("/bin/sh", "-c", "command -v pm3") 48 | output, err := cmd.CombinedOutput() 49 | if err == nil { 50 | path := strings.TrimSpace(string(output)) 51 | if path != "" && strings.HasPrefix(path, "/") { 52 | return path, nil 53 | } 54 | } 55 | 56 | // If shell lookup fails, try common installation paths directly 57 | var commonPaths []string 58 | if runtime.GOOS == "darwin" { 59 | commonPaths = []string{ 60 | "/opt/homebrew/bin/pm3", // Homebrew on Apple Silicon 61 | "/usr/local/bin/pm3", // Homebrew on Intel Mac 62 | "/opt/local/bin/pm3", // MacPorts 63 | "/usr/local/Cellar/proxmark3/bin/pm3", // Older Homebrew layout 64 | } 65 | } else { 66 | // Linux 67 | commonPaths = []string{ 68 | "/usr/local/bin/pm3", 69 | "/usr/bin/pm3", 70 | "/opt/proxmark3/pm3", 71 | } 72 | } 73 | 74 | for _, path := range commonPaths { 75 | if _, err := os.Stat(path); err == nil { 76 | return path, nil 77 | } 78 | } 79 | 80 | return "", fmt.Errorf("pm3 binary not found in PATH or common installation locations") 81 | } 82 | 83 | // findPm3Device detects the pm3 device path using 'pm3 --list' 84 | // Returns the device path (e.g., /dev/tty.usbmodem1101 on macOS, /dev/ttyACM0 on Linux) 85 | func findPm3Device() (string, error) { 86 | // Get the full path to pm3 binary 87 | pm3Binary, err := getPm3Path() 88 | if err != nil || pm3Binary == "" { 89 | return "", fmt.Errorf("pm3 binary not found") 90 | } 91 | 92 | cmd := exec.Command(pm3Binary, "--list") 93 | output, err := cmd.CombinedOutput() 94 | if err != nil { 95 | return "", err 96 | } 97 | 98 | // Parse the output to get the device path 99 | // Expected format: "1: /dev/tty.usbmodem1101" or "1: /dev/ttyACM0" 100 | outputStr := string(output) 101 | lines := strings.Split(outputStr, "\n") 102 | for _, line := range lines { 103 | line = strings.TrimSpace(line) 104 | if line == "" { 105 | continue 106 | } 107 | // Look for lines with device paths 108 | if strings.Contains(line, ":") { 109 | parts := strings.SplitN(line, ":", 2) 110 | if len(parts) == 2 { 111 | devicePath := strings.TrimSpace(parts[1]) 112 | // Validate it looks like a device path 113 | if strings.HasPrefix(devicePath, "/dev/") { 114 | return devicePath, nil 115 | } 116 | } 117 | } 118 | } 119 | 120 | return "", fmt.Errorf("no pm3 device found") 121 | } 122 | 123 | // pm3Device caches the detected device path 124 | var pm3Device string 125 | var pm3DeviceErr error 126 | var pm3DeviceChecked bool 127 | 128 | // pm3Path caches the full path to pm3 binary 129 | var pm3Path string 130 | var pm3PathErr error 131 | var pm3PathChecked bool 132 | 133 | // getPm3Path returns the cached pm3 binary path or detects it 134 | func getPm3Path() (string, error) { 135 | if !pm3PathChecked { 136 | pm3Path, pm3PathErr = findPm3Path() 137 | pm3PathChecked = true 138 | } 139 | return pm3Path, pm3PathErr 140 | } 141 | 142 | // getPm3Device returns the cached pm3 device path or detects it 143 | func getPm3Device() (string, error) { 144 | if !pm3DeviceChecked { 145 | pm3Device, pm3DeviceErr = findPm3Device() 146 | pm3DeviceChecked = true 147 | } 148 | return pm3Device, pm3DeviceErr 149 | } 150 | 151 | // resetPm3DeviceCache clears the cached device detection so it will be re-checked 152 | func resetPm3DeviceCache() { 153 | pm3DeviceChecked = false 154 | pm3Device = "" 155 | pm3DeviceErr = nil 156 | } 157 | 158 | // checkProxmark3 verifies if Proxmark3 is connected and responding. 159 | // Returns (connected, error message). 160 | func checkProxmark3() (bool, string) { 161 | pm3Binary, err := getPm3Path() 162 | if err != nil || pm3Binary == "" { 163 | return false, "Proxmark3 client (pm3) not found in PATH" 164 | } 165 | 166 | // Reset cache to force fresh detection 167 | resetPm3DeviceCache() 168 | device, err := getPm3Device() 169 | if err != nil || device == "" { 170 | return false, "Proxmark3 device not detected. Please connect your Proxmark3" 171 | } 172 | 173 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 174 | defer cancel() 175 | 176 | cmd := exec.CommandContext(ctx, pm3Binary, "-c", "quit", "-p", device) 177 | output, err := cmd.CombinedOutput() 178 | 179 | if ctx.Err() == context.DeadlineExceeded { 180 | resetPm3DeviceCache() 181 | return false, "Proxmark3 device not detected. Please connect your Proxmark3" 182 | } 183 | 184 | outputStr := strings.ToLower(string(output)) 185 | if strings.Contains(outputStr, "offline") || 186 | strings.Contains(outputStr, "cannot open") || 187 | strings.Contains(outputStr, "no such device") || 188 | err != nil && strings.Contains(err.Error(), "exit status") { 189 | resetPm3DeviceCache() 190 | return false, "Proxmark3 device not detected. Please connect your Proxmark3" 191 | } 192 | 193 | return true, "" 194 | } 195 | 196 | // isInteractive checks if stdin is connected to a terminal. 197 | func isInteractive() bool { 198 | return term.IsTerminal(int(os.Stdin.Fd())) 199 | } 200 | 201 | // validateEM4100Hex validates EM4100 hex data format 202 | // EM4100 IDs must be exactly 5 hex bytes (10 hex characters) 203 | func validateEM4100Hex(hexData string) (bool, string) { 204 | // Remove any whitespace 205 | hexData = strings.TrimSpace(hexData) 206 | 207 | // Check if empty 208 | if hexData == "" { 209 | return false, "Hex Data is required for EM4100 / Net2 cards" 210 | } 211 | 212 | // Check length - must be exactly 10 hex characters (5 bytes) 213 | if len(hexData) != 10 { 214 | return false, fmt.Sprintf("EM4100 / Net2 ID must be exactly 10 hex characters (5 bytes), got %d characters", len(hexData)) 215 | } 216 | 217 | // Check if all characters are valid hex 218 | for _, c := range hexData { 219 | if !((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f')) { 220 | return false, fmt.Sprintf("Invalid hex character in EM4100 / Net2 ID: %c (only 0-9, A-F allowed)", c) 221 | } 222 | } 223 | 224 | return true, "" 225 | } 226 | 227 | // launchPm3Terminal launches Proxmark3 in the OS default terminal 228 | func launchPm3Terminal() error { 229 | pm3Binary, err := getPm3Path() 230 | if err != nil { 231 | return fmt.Errorf("failed to find pm3 binary: %w", err) 232 | } 233 | 234 | device, err := getPm3Device() 235 | if err != nil { 236 | // If device not found, launch without device parameter 237 | device = "" 238 | } 239 | 240 | var cmd *exec.Cmd 241 | 242 | switch runtime.GOOS { 243 | case "darwin": 244 | // macOS: Use osascript to open Terminal.app with the command 245 | var script string 246 | if device != "" { 247 | script = fmt.Sprintf(`tell application "Terminal" 248 | activate 249 | do script "%s -p %s" 250 | end tell`, pm3Binary, device) 251 | } else { 252 | script = fmt.Sprintf(`tell application "Terminal" 253 | activate 254 | do script "%s" 255 | end tell`, pm3Binary) 256 | } 257 | cmd = exec.Command("osascript", "-e", script) 258 | case "linux": 259 | // Linux: Try common terminals 260 | terminals := []string{"gnome-terminal", "xterm", "konsole", "x-terminal-emulator", "terminator"} 261 | var terminalCmd string 262 | var terminalArgs []string 263 | 264 | for _, term := range terminals { 265 | if _, err := exec.LookPath(term); err == nil { 266 | terminalCmd = term 267 | switch term { 268 | case "gnome-terminal": 269 | if device != "" { 270 | terminalArgs = []string{"--", pm3Binary, "-p", device} 271 | } else { 272 | terminalArgs = []string{"--", pm3Binary} 273 | } 274 | case "xterm": 275 | if device != "" { 276 | terminalArgs = []string{"-e", pm3Binary, "-p", device} 277 | } else { 278 | terminalArgs = []string{"-e", pm3Binary} 279 | } 280 | case "konsole": 281 | if device != "" { 282 | terminalArgs = []string{"-e", pm3Binary, "-p", device} 283 | } else { 284 | terminalArgs = []string{"-e", pm3Binary} 285 | } 286 | case "x-terminal-emulator": 287 | if device != "" { 288 | terminalArgs = []string{"-e", pm3Binary, "-p", device} 289 | } else { 290 | terminalArgs = []string{"-e", pm3Binary} 291 | } 292 | case "terminator": 293 | if device != "" { 294 | terminalArgs = []string{"-e", pm3Binary, "-p", device} 295 | } else { 296 | terminalArgs = []string{"-e", pm3Binary} 297 | } 298 | } 299 | break 300 | } 301 | } 302 | 303 | if terminalCmd == "" { 304 | return fmt.Errorf("no terminal emulator found. Please install gnome-terminal, xterm, konsole, or terminator") 305 | } 306 | 307 | cmd = exec.Command(terminalCmd, terminalArgs...) 308 | case "windows": 309 | // Windows: Use cmd.exe 310 | if device != "" { 311 | cmd = exec.Command("cmd.exe", "/c", "start", "cmd.exe", "/k", pm3Binary, "-p", device) 312 | } else { 313 | cmd = exec.Command("cmd.exe", "/c", "start", "cmd.exe", "/k", pm3Binary) 314 | } 315 | default: 316 | return fmt.Errorf("unsupported operating system: %s", runtime.GOOS) 317 | } 318 | 319 | return cmd.Start() 320 | } 321 | -------------------------------------------------------------------------------- /scripts/wsl_windows_launch.ps1: -------------------------------------------------------------------------------- 1 | # Log file path 2 | $logFile = "C:\doppelganger_assistant\launch_proxmark3_wsl.log" 3 | 4 | # Function to check if a command exists 5 | function CommandExists { 6 | param ( 7 | [string]$command 8 | ) 9 | $null = Get-Command $command -ErrorAction SilentlyContinue 10 | return $? 11 | } 12 | 13 | # Function to log output to both file and screen 14 | function Log { 15 | param ( 16 | [string]$message 17 | ) 18 | $timestamp = (Get-Date).ToString('u') 19 | $logMessage = "$timestamp - $message" 20 | Write-Output $logMessage 21 | Add-Content -Path $logFile -Value $logMessage 22 | } 23 | 24 | # Function to detect which Doppelganger WSL distribution is installed 25 | function Get-DoppelgangerDistro { 26 | $wslList = wsl.exe -l -q | ForEach-Object { $_.Trim() -replace "`0", "" } 27 | $kaliName = "Kali-doppelganger_assistant" 28 | $ubuntuName = "Ubuntu-doppelganger_assistant" 29 | 30 | foreach ($distro in $wslList) { 31 | if ($distro -eq $kaliName) { 32 | return $kaliName 33 | } 34 | elseif ($distro -eq $ubuntuName) { 35 | return $ubuntuName 36 | } 37 | } 38 | return $null 39 | } 40 | 41 | # Function to check if WSL is running 42 | function IsWSLRunning { 43 | $distroName = Get-DoppelgangerDistro 44 | if ($null -eq $distroName) { 45 | return $false 46 | } 47 | $wslOutput = wsl -l -q 48 | return $wslOutput -match $distroName 49 | } 50 | 51 | # Function to start WSL if not running 52 | function StartWSLIfNotRunning { 53 | $distroName = Get-DoppelgangerDistro 54 | if ($null -eq $distroName) { 55 | Log "ERROR: No Doppelganger Assistant WSL distribution found!" 56 | Log "Please run the installer first." 57 | Read-Host "Press Enter to exit" 58 | exit 1 59 | } 60 | 61 | if (-not (IsWSLRunning)) { 62 | Log "WSL is not running. Starting $distroName..." 63 | & wsl -d $distroName --exec echo "WSL started" 64 | Log "WSL started." 65 | } 66 | else { 67 | Log "$distroName is already running." 68 | } 69 | } 70 | 71 | # Function to detach a USB device if it is already attached 72 | function DetachUSBDevice { 73 | param ( 74 | [string]$busId 75 | ) 76 | Log "Detaching device with busid $busId if it is already attached..." 77 | & usbipd detach --busid $busId 2>&1 | Tee-Object -Variable detachOutputResult | Out-Null 78 | if ($LASTEXITCODE -ne 0) { 79 | Log "Error detaching device. It might not be attached. Exit code: $LASTEXITCODE" 80 | Log "Detach output: $detachOutputResult" 81 | } 82 | else { 83 | Log "Device detached successfully." 84 | } 85 | 86 | $maxRetries = 10 87 | $retryCount = 0 88 | while ($retryCount -lt $maxRetries) { 89 | Start-Sleep -Seconds 1 90 | $usbDevices = & usbipd list 91 | if (-not ($usbDevices -match $busId)) { 92 | Log "Device $busId successfully detached." 93 | return 94 | } 95 | $retryCount++ 96 | } 97 | 98 | Log "Device $busId did not detach within the expected time." 99 | } 100 | 101 | # Function to verify device is attached in WSL 102 | function VerifyDeviceAttachedInWSL { 103 | param ( 104 | [string]$busId, 105 | [int]$timeoutSeconds = 10 106 | ) 107 | 108 | $distroName = Get-DoppelgangerDistro 109 | if ($null -eq $distroName) { 110 | return $false 111 | } 112 | 113 | Log "Verifying device attachment in WSL (timeout: ${timeoutSeconds}s)..." 114 | $startTime = Get-Date 115 | $elapsed = 0 116 | 117 | while ($elapsed -lt $timeoutSeconds) { 118 | $checkCommand = "ls /dev/ttyACM* /dev/ttyUSB* 2>/dev/null | head -1" 119 | $wslOutput = wsl -d $distroName --exec bash -c $checkCommand 2>&1 120 | 121 | if ($wslOutput -and $wslOutput.Trim() -ne "") { 122 | Log "Device verified in WSL: $wslOutput" 123 | return $true 124 | } 125 | 126 | Start-Sleep -Seconds 1 127 | $elapsed = ((Get-Date) - $startTime).TotalSeconds 128 | } 129 | 130 | Log "Device attachment verification timed out after ${timeoutSeconds} seconds." 131 | return $false 132 | } 133 | 134 | # Function to refresh PATH and check for usbipd 135 | function Refresh-UsbIpdCommand { 136 | Log "Refreshing PATH and checking for usbipd command..." 137 | $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User") 138 | 139 | if (Get-Command usbipd -ErrorAction SilentlyContinue) { 140 | Log "usbipd command is now available." 141 | return $true 142 | } 143 | 144 | Log "usbipd command is still not available after refreshing PATH." 145 | return $false 146 | } 147 | 148 | # Function to reinstall usbipd 149 | function Reinstall-UsbIpd { 150 | Log "Attempting to reinstall usbipd..." 151 | 152 | # First, try to uninstall using winget 153 | if (CommandExists "winget") { 154 | Log "Uninstalling usbipd..." 155 | $uninstallOutput = Start-Process winget -ArgumentList "uninstall --exact --silent usbipd" -Wait -PassThru -NoNewWindow 156 | if ($uninstallOutput.ExitCode -eq 0) { 157 | Log "usbipd uninstalled successfully." 158 | Start-Sleep -Seconds 2 159 | } 160 | else { 161 | Log "Warning: Could not uninstall usbipd via winget. Exit code: $($uninstallOutput.ExitCode)" 162 | } 163 | } 164 | else { 165 | Log "winget not found. Skipping uninstall step." 166 | } 167 | 168 | # Now reinstall 169 | Log "Reinstalling usbipd..." 170 | try { 171 | if (CommandExists "winget") { 172 | $installOutput = Start-Process winget -ArgumentList "install --exact --silent --accept-source-agreements --accept-package-agreements usbipd" -Wait -PassThru -ErrorAction Stop 173 | if ($installOutput.ExitCode -ne 0) { 174 | throw "Winget installation failed with exit code: $($installOutput.ExitCode)" 175 | } 176 | Log "usbipd reinstalled via winget." 177 | } 178 | else { 179 | # Fallback to direct MSI download 180 | Log "winget not available, using direct MSI download..." 181 | $usbIpdUrl = "https://github.com/dorssel/usbipd-win/releases/latest/download/usbipd-win_x64.msi" 182 | $usbIpdMsiPath = "$env:TEMP\usbipd-win_x64.msi" 183 | Invoke-WebRequest -Uri $usbIpdUrl -OutFile $usbIpdMsiPath 184 | $msiExecOutput = Start-Process msiexec.exe -ArgumentList "/i `"$usbIpdMsiPath`" /qn" -Wait -PassThru 185 | if ($msiExecOutput.ExitCode -ne 0) { 186 | throw "MSI installation failed with exit code: $($msiExecOutput.ExitCode)" 187 | } 188 | Remove-Item $usbIpdMsiPath -Force 189 | Log "usbipd reinstalled via MSI." 190 | } 191 | 192 | # Refresh PATH and verify 193 | Start-Sleep -Seconds 2 194 | if (Refresh-UsbIpdCommand) { 195 | Log "usbipd reinstallation successful and command is available." 196 | return $true 197 | } 198 | else { 199 | Log "WARNING: usbipd reinstalled but command not immediately available. May require restart." 200 | return $false 201 | } 202 | } 203 | catch { 204 | Log "ERROR: Failed to reinstall usbipd: $_" 205 | return $false 206 | } 207 | } 208 | 209 | # Function to attach a USB device to WSL with verification and auto-reinstall on failure 210 | function AttachUSBDeviceToWSL { 211 | param ( 212 | [string]$busId, 213 | [int]$maxRetries = 2 214 | ) 215 | 216 | $retryCount = 0 217 | $attachSuccess = $false 218 | 219 | while ($retryCount -lt $maxRetries -and -not $attachSuccess) { 220 | if ($retryCount -gt 0) { 221 | Log "Retry attempt $retryCount of $maxRetries..." 222 | } 223 | 224 | Log "Attaching device with busid $busId to WSL (WSL2 required)..." 225 | & usbipd attach --wsl --busid $busId 2>&1 | Tee-Object -Variable attachOutputResult | Out-Null 226 | 227 | if ($LASTEXITCODE -ne 0) { 228 | Log "Error attaching device to WSL. Exit code: $LASTEXITCODE" 229 | Log "Attach output: $attachOutputResult" 230 | 231 | if ($retryCount -eq 0) { 232 | Log "Attachment failed. Attempting to reinstall usbipd and retry..." 233 | 234 | if (Reinstall-UsbIpd) { 235 | Log "usbipd reinstalled. Retrying attachment..." 236 | Start-Sleep -Seconds 3 237 | $retryCount++ 238 | continue 239 | } 240 | else { 241 | Log "Failed to reinstall usbipd. Cannot retry attachment." 242 | return $false 243 | } 244 | } 245 | else { 246 | Log "Attachment failed after usbipd reinstall. Giving up." 247 | Log "NOTE: USB passthrough requires WSL2 with nested virtualization enabled." 248 | return $false 249 | } 250 | } 251 | else { 252 | Log "Attach command succeeded. Verifying device attachment..." 253 | 254 | Start-Sleep -Seconds 2 255 | if (VerifyDeviceAttachedInWSL -busId $busId -timeoutSeconds 10) { 256 | Log "Device successfully attached and verified in WSL." 257 | $attachSuccess = $true 258 | return $true 259 | } 260 | else { 261 | Log "WARNING: Attach command succeeded but device not detected in WSL." 262 | 263 | if ($retryCount -eq 0) { 264 | Log "Device verification failed. Attempting to reinstall usbipd and retry..." 265 | 266 | if (Reinstall-UsbIpd) { 267 | Log "usbipd reinstalled. Retrying attachment..." 268 | Start-Sleep -Seconds 3 269 | $retryCount++ 270 | continue 271 | } 272 | else { 273 | Log "Failed to reinstall usbipd. Cannot retry attachment." 274 | return $false 275 | } 276 | } 277 | else { 278 | Log "Device verification failed after usbipd reinstall. Attachment may have partially succeeded." 279 | return $false 280 | } 281 | } 282 | } 283 | } 284 | 285 | return $attachSuccess 286 | } 287 | 288 | # Function to launch Doppelganger Assistant in WSL and close the terminal 289 | function LaunchDoppelgangerAssistant { 290 | $distroName = Get-DoppelgangerDistro 291 | if ($null -eq $distroName) { 292 | Log "ERROR: No Doppelganger Assistant WSL distribution found!" 293 | return 294 | } 295 | 296 | Log "Launching Doppelganger Assistant in $distroName..." 297 | $wslCommand = "wsl -d $distroName --exec doppelganger_assistant" 298 | 299 | Start-Process powershell -ArgumentList "-WindowStyle", "Hidden", "-Command", $wslCommand -WindowStyle Hidden 300 | 301 | Log "Doppelganger Assistant launch initiated. This script will now close." 302 | } 303 | 304 | # Clear the log file 305 | Clear-Content -Path $logFile -ErrorAction SilentlyContinue 306 | 307 | Log "Starting setup script." 308 | 309 | # Ensure WSL is running 310 | StartWSLIfNotRunning 311 | 312 | # List all USB devices and find the Proxmark3 device 313 | Log "Listing all USB devices..." 314 | $usbDevices = & usbipd list 2>&1 | Tee-Object -Variable usbDevicesOutput 315 | Log $usbDevicesOutput 316 | 317 | # Find the Proxmark3 device by VID 9ac4 and extract the bus ID 318 | $proxmark3Device = $usbDevices | Select-String -Pattern "9ac4" 319 | if ($proxmark3Device) { 320 | $busId = ($proxmark3Device -split "\s+")[0] 321 | Log "Found device with busid $busId" 322 | 323 | # Detach the device if it is already attached 324 | DetachUSBDevice -busId $busId 325 | 326 | # Bind the Proxmark3 device 327 | Log "Binding Proxmark3 device..." 328 | & usbipd bind --busid $busId 2>&1 | Tee-Object -Variable bindOutputResult | Out-Null 329 | if ($LASTEXITCODE -ne 0) { 330 | Log "Error binding Proxmark3 device. Exit code: $LASTEXITCODE" 331 | Log "Bind output: $bindOutputResult" 332 | Log "Continuing without device binding..." 333 | } 334 | else { 335 | $attachSuccess = AttachUSBDeviceToWSL -busId $busId 336 | 337 | if (-not $attachSuccess) { 338 | Log "WARNING: Failed to attach Proxmark3 device to WSL after retries." 339 | Log "Doppelganger Assistant will launch but may not detect the device." 340 | $userChoice = Read-Host "Continue anyway? (Y/n)" 341 | if ($userChoice -eq "n" -or $userChoice -eq "N") { 342 | Log "User chose to exit. Please check usbipd installation and try again." 343 | exit 1 344 | } 345 | } 346 | } 347 | 348 | LaunchDoppelgangerAssistant 349 | } 350 | else { 351 | Log "Proxmark3 device not found." 352 | $userChoice = Read-Host "Proxmark3 device not detected. Do you want to (A)ttach the device and retry, or (C)ontinue without the device? [A/C]" 353 | 354 | if ($userChoice -eq "A" -or $userChoice -eq "a") { 355 | Log "User chose to attach the device. Please connect the Proxmark3 device and press Enter." 356 | Read-Host "Press Enter when you have connected the Proxmark3 device" 357 | 358 | # Retry device detection 359 | $usbDevices = & usbipd list 360 | $proxmark3Device = $usbDevices | Select-String -Pattern "9ac4" 361 | 362 | if ($proxmark3Device) { 363 | Log "Proxmark3 device found after user intervention. Restarting the script." 364 | & $MyInvocation.MyCommand.Path # Restart the script 365 | exit 366 | } 367 | else { 368 | Log "Proxmark3 device still not found after user intervention. Continuing without the device." 369 | } 370 | } 371 | else { 372 | Log "User chose to continue without the Proxmark3 device." 373 | } 374 | 375 | LaunchDoppelgangerAssistant 376 | } 377 | 378 | Start-Sleep -Seconds 2 379 | exit -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International 2 | 3 | By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. 4 | 5 | Section 1 – Definitions. 6 | 7 | a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. 8 | 9 | b. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. 10 | 11 | c. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. 12 | 13 | d. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. 14 | 15 | e. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. 16 | 17 | f. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. 18 | 19 | g. Licensor means the individual(s) or entity(ies) granting rights under this Public License. 20 | 21 | h. NonCommercial means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange. 22 | 23 | i. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. 24 | 25 | j. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. 26 | 27 | k. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. 28 | 29 | Section 2 – Scope. 30 | 31 | a. License grant. 32 | 33 | 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: 34 | 35 | A. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and 36 | 37 | B. produce and reproduce, but not Share, Adapted Material for NonCommercial purposes only. 38 | 39 | 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 40 | 41 | 3. Term. The term of this Public License is specified in Section 6(a). 42 | 43 | 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. 44 | 45 | 5. Downstream recipients. 46 | 47 | A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. 48 | 49 | B. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 50 | 51 | 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). 52 | 53 | b. Other rights. 54 | 55 | 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 56 | 57 | 2. Patent and trademark rights are not licensed under this Public License. 58 | 59 | 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes. 60 | 61 | Section 3 – License Conditions. 62 | 63 | Your exercise of the Licensed Rights is expressly made subject to the following conditions. 64 | 65 | a. Attribution. 66 | 67 | 1. If You Share the Licensed Material, You must: 68 | 69 | A. retain the following if it is supplied by the Licensor with the Licensed Material: 70 | 71 | i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); 72 | 73 | ii. a copyright notice; 74 | 75 | iii. a notice that refers to this Public License; 76 | 77 | iv. a notice that refers to the disclaimer of warranties; 78 | 79 | v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; 80 | 81 | B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and 82 | 83 | C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 84 | 85 | For the avoidance of doubt, You do not have permission under this Public License to Share Adapted Material. 86 | 87 | 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 88 | 89 | 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. 90 | 91 | Section 4 – Sui Generis Database Rights. 92 | 93 | Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: 94 | 95 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only and provided You do not Share Adapted Material; 96 | 97 | b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and 98 | 99 | c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. 100 | 101 | For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. 102 | 103 | Section 5 – Disclaimer of Warranties and Limitation of Liability. 104 | 105 | a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You. 106 | 107 | b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You. 108 | 109 | c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. 110 | 111 | Section 6 – Term and Termination. 112 | 113 | a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. 114 | 115 | b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 116 | 117 | 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 118 | 119 | 2. upon express reinstatement by the Licensor. 120 | 121 | For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. 122 | 123 | c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. 124 | 125 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. 126 | 127 | Section 7 – Other Terms and Conditions. 128 | 129 | a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. 130 | 131 | b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. 132 | 133 | Section 8 – Interpretation. 134 | 135 | a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. 136 | 137 | b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. 138 | 139 | c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. 140 | 141 | d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. 142 | 143 | Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the "Licensor." The text of the Creative Commons public licenses is dedicated to the public domain under the CC0 Public Domain Dedication. Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark "Creative Commons" or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. 144 | 145 | Creative Commons may be contacted at creativecommons.org. 146 | 147 | 148 | -------------------------------------------------------------------------------- /installers/doppelganger_install_windows.ps1: -------------------------------------------------------------------------------- 1 | # Check if the script is running as an administrator 2 | $isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) 3 | 4 | if (-not $isAdmin) { 5 | Write-Host "`n*************************************************************" -ForegroundColor Red 6 | Write-Host "* *" -ForegroundColor Red 7 | Write-Host "* THIS SCRIPT MUST BE RUN AS AN ADMINISTRATOR *" -ForegroundColor Red 8 | Write-Host "* *" -ForegroundColor Red 9 | Write-Host "*************************************************************`n" -ForegroundColor Red 10 | Write-Host "Please follow these steps:" -ForegroundColor Yellow 11 | Write-Host "1. Right-click on PowerShell and select 'Run as administrator'" -ForegroundColor Yellow 12 | Write-Host "2. Navigate to the script's directory" -ForegroundColor Yellow 13 | Write-Host "3. Run the script again" -ForegroundColor Yellow 14 | Write-Host "`nPress any key to exit..." 15 | $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") 16 | exit 17 | } 18 | 19 | # Define paths 20 | $basePath = "C:\doppelganger_assistant" 21 | $setupScriptUrl = "https://raw.githubusercontent.com/tweathers-sec/doppelganger_assistant/main/scripts/wsl_setup.ps1" 22 | $launchScriptUrl = "https://raw.githubusercontent.com/tweathers-sec/doppelganger_assistant/main/scripts/wsl_windows_launch.ps1" 23 | $pm3TerminalScriptUrl = "https://raw.githubusercontent.com/tweathers-sec/doppelganger_assistant/main/scripts/wsl_pm3_terminal.ps1" 24 | $installScriptUrl = "https://raw.githubusercontent.com/tweathers-sec/doppelganger_assistant/main/scripts/wsl_doppelganger_install.sh" 25 | $imageUrl = "https://raw.githubusercontent.com/tweathers-sec/doppelganger_assistant/main/img/doppelganger_assistant.ico" 26 | $pm3IconUrl = "https://raw.githubusercontent.com/tweathers-sec/doppelganger_assistant/main/img/doppelganger_pm3.ico" 27 | $wslEnableScriptUrl = "https://raw.githubusercontent.com/tweathers-sec/doppelganger_assistant/main/scripts/wsl_enable.ps1" 28 | $usbReconnectScriptUrl = "https://raw.githubusercontent.com/tweathers-sec/doppelganger_assistant/main/scripts/usb_reconnect.ps1" 29 | $proxmarkFlashScriptUrl = "https://raw.githubusercontent.com/tweathers-sec/doppelganger_assistant/main/scripts/proxmark_flash.ps1" 30 | $uninstallScriptUrl = "https://raw.githubusercontent.com/tweathers-sec/doppelganger_assistant/main/scripts/uninstall.ps1" 31 | $setupScriptPath = "$basePath\wsl_setup.ps1" 32 | $launchScriptPath = "$basePath\wsl_windows_launch.ps1" 33 | $pm3TerminalScriptPath = "$basePath\wsl_pm3_terminal.ps1" 34 | $installScriptPath = "$basePath\wsl_doppelganger_install.sh" 35 | $imagePath = "$basePath\doppelganger_assistant.ico" 36 | $pm3IconPath = "$basePath\doppelganger_pm3.ico" 37 | $wslEnableScriptPath = "$basePath\wsl_enable.ps1" 38 | $usbReconnectScriptPath = "$basePath\usb_reconnect.ps1" 39 | $proxmarkFlashScriptPath = "$basePath\proxmark_flash.ps1" 40 | $uninstallScriptPath = "$basePath\uninstall.ps1" 41 | $shortcutPath = [System.IO.Path]::Combine([System.Environment]::GetFolderPath("Desktop"), "Launch Doppelganger Assistant.lnk") 42 | $pm3ShortcutPath = [System.IO.Path]::Combine([System.Environment]::GetFolderPath("Desktop"), "Proxmark3 Terminal.lnk") 43 | 44 | # ASCII Art function 45 | function Write-DoppelgangerAscii { 46 | $color = 'Red' 47 | Write-Host @" 48 | 49 | ____ _ 50 | | _ \ ___ _ __ _ __ ___| | __ _ __ _ _ __ __ _ ___ _ __ 51 | | | | |/ _ \| '_ \| '_ \ / _ \ |/ _` |/ _` | '_ \ / _` |/ _ \ '__| 52 | | |_| | (_) | |_) | |_) | __/ | (_| | (_| | | | | (_| | __/ | 53 | |____/ \___/| .__/| .__/ \___|_|\__, |\__,_|_| |_|\__, |\___|_| 54 | |_| |_| |___/ |___/ 55 | 56 | "@ -ForegroundColor $color 57 | } 58 | 59 | # Display ASCII art 60 | Write-DoppelgangerAscii 61 | 62 | Write-Host "`n*************************************************************" -ForegroundColor Green 63 | Write-Host "* *" -ForegroundColor Green 64 | Write-Host "* RUNNING WITH ADMINISTRATOR PRIVILEGES *" -ForegroundColor Green 65 | Write-Host "* *" -ForegroundColor Green 66 | Write-Host "*************************************************************`n" -ForegroundColor Green 67 | 68 | $logFile = "C:\doppelganger_install_windows.log" 69 | 70 | # Function to log output to both file and screen 71 | function Log { 72 | param ( 73 | [string]$message 74 | ) 75 | $timestamp = (Get-Date).ToString('u') 76 | $logMessage = "$timestamp - $message" 77 | Write-Output $message 78 | Add-Content -Path $logFile -Value $logMessage -ErrorAction SilentlyContinue 79 | } 80 | 81 | # Check for nested virtualization support 82 | Log "Checking for nested virtualization support..." 83 | $nestedVirtSupported = $false 84 | 85 | try { 86 | $hypervFeature = Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-All -ErrorAction SilentlyContinue 87 | $computerSystem = Get-WmiObject -Class Win32_ComputerSystem 88 | $isVM = $computerSystem.Model -match "Virtual" -or $computerSystem.Manufacturer -match "VMware|Microsoft Corporation|Xen|QEMU|VirtualBox|Parallels" 89 | 90 | if ($isVM) { 91 | Write-Host "`n*************************************************************" -ForegroundColor Red 92 | Write-Host "* *" -ForegroundColor Red 93 | Write-Host "* NESTED VM DETECTED *" -ForegroundColor Red 94 | Write-Host "* *" -ForegroundColor Red 95 | Write-Host "* Doppelganger Assistant DOES NOT support installation *" -ForegroundColor Red 96 | Write-Host "* in nested virtual machine environments (VM within VM). *" -ForegroundColor Red 97 | Write-Host "* *" -ForegroundColor Red 98 | Write-Host "* Please install on: *" -ForegroundColor Red 99 | Write-Host "* - Physical Windows hardware *" -ForegroundColor Red 100 | Write-Host "* *" -ForegroundColor Red 101 | Write-Host "* Installation will now exit. *" -ForegroundColor Red 102 | Write-Host "* *" -ForegroundColor Red 103 | Write-Host "*************************************************************`n" -ForegroundColor Red 104 | 105 | Log "ERROR: Nested VM detected. Installation blocked - nested VMs are not supported." 106 | Write-Host "`nPress any key to exit..." 107 | $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") 108 | exit 109 | } 110 | else { 111 | Log "Running on physical hardware or primary VM." 112 | } 113 | } 114 | catch { 115 | Log "Could not determine virtualization status. Proceeding with installation..." 116 | } 117 | 118 | # Function to create a shortcut that runs as an administrator 119 | function New-Shortcut { 120 | param ( 121 | [string]$TargetPath, 122 | [string]$ShortcutPath, 123 | [string]$Arguments, 124 | [string]$Description, 125 | [string]$WorkingDirectory, 126 | [string]$IconLocation 127 | ) 128 | $WshShell = New-Object -ComObject WScript.Shell 129 | $Shortcut = $WshShell.CreateShortcut($ShortcutPath) 130 | $Shortcut.TargetPath = $TargetPath 131 | $Shortcut.Arguments = $Arguments 132 | $Shortcut.Description = $Description 133 | $Shortcut.WorkingDirectory = $WorkingDirectory 134 | $Shortcut.IconLocation = $IconLocation 135 | $Shortcut.Save() 136 | 137 | $bytes = [System.IO.File]::ReadAllBytes($ShortcutPath) 138 | $bytes[0x15] = $bytes[0x15] -bor 0x20 139 | [System.IO.File]::WriteAllBytes($ShortcutPath, $bytes) 140 | } 141 | 142 | # Remove RebootPending.txt if it exists 143 | if (Test-Path "$env:SystemRoot\System32\RebootPending.txt") { 144 | Remove-Item "$env:SystemRoot\System32\RebootPending.txt" -Force 145 | } 146 | 147 | $headers = @{ 148 | 'Cache-Control' = 'no-cache' 149 | 'Pragma' = 'no-cache' 150 | } 151 | 152 | # Check for existing installation 153 | if (Test-Path -Path $basePath) { 154 | Write-Host "`n*************************************************************" -ForegroundColor Yellow 155 | Write-Host "* *" -ForegroundColor Yellow 156 | Write-Host "* EXISTING INSTALLATION DETECTED *" -ForegroundColor Yellow 157 | Write-Host "* *" -ForegroundColor Yellow 158 | Write-Host "*************************************************************`n" -ForegroundColor Yellow 159 | 160 | $updateChoice = Read-Host "An existing installation was found. Do you want to remove and re-install (update)? [Recommended] (y/n)" 161 | 162 | if ($updateChoice -eq "y" -or $updateChoice -eq "Y" -or $updateChoice -eq "") { 163 | Log "User chose to update. Running uninstaller..." 164 | 165 | try { 166 | Log "Downloading uninstaller script..." 167 | $uninstallScript = Invoke-RestMethod -Uri $uninstallScriptUrl -Headers $headers 168 | 169 | Log "Executing uninstaller..." 170 | Invoke-Expression $uninstallScript 171 | Start-Sleep -Seconds 2 172 | } 173 | catch { 174 | Log "Failed to download/run uninstaller. Performing manual cleanup..." 175 | wsl --shutdown 176 | Remove-Item -Path $basePath -Recurse -Force -ErrorAction SilentlyContinue 177 | if (Test-Path -Path $shortcutPath) { 178 | Remove-Item -Path $shortcutPath -Force -ErrorAction SilentlyContinue 179 | } 180 | } 181 | 182 | Log "Previous installation removed. Proceeding with fresh installation..." 183 | } 184 | else { 185 | Log "User chose not to update. Exiting installer." 186 | Write-Host "`nInstallation cancelled. Press any key to exit..." 187 | $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") 188 | exit 189 | } 190 | } 191 | 192 | # Create base directory 193 | mkdir $basePath | Out-Null 194 | 195 | 196 | Write-Host "`nWhich WSL distro would you like to install?" -ForegroundColor Yellow 197 | Write-Host "1) Kali Linux 2025.3 [default]" -ForegroundColor Green 198 | Write-Host "2) Ubuntu 24.04 LTS (Noble)" -ForegroundColor Cyan 199 | $installDistro = Read-Host "`nEnter your choice (1-2) [default: 1]" 200 | if ($installDistro -eq "2") { 201 | $selectedDistro = "Ubuntu" 202 | } 203 | else { 204 | $selectedDistro = "Kali" 205 | } 206 | 207 | # Download the setup, launch, install scripts, and images from GitHub 208 | Log "Downloading setup script..." 209 | Invoke-WebRequest -Uri $setupScriptUrl -OutFile $setupScriptPath -Headers $headers 210 | 211 | Log "Downloading GUI launch script..." 212 | Invoke-WebRequest -Uri $launchScriptUrl -OutFile $launchScriptPath -Headers $headers 213 | 214 | Log "Downloading PM3 terminal launch script..." 215 | Invoke-WebRequest -Uri $pm3TerminalScriptUrl -OutFile $pm3TerminalScriptPath -Headers $headers 216 | 217 | Log "Downloading install script..." 218 | Invoke-WebRequest -Uri $installScriptUrl -OutFile $installScriptPath -Headers $headers 219 | 220 | Log "Downloading Doppelganger icon..." 221 | Invoke-WebRequest -Uri $imageUrl -OutFile $imagePath -Headers $headers 222 | 223 | Log "Downloading PM3 icon..." 224 | Invoke-WebRequest -Uri $pm3IconUrl -OutFile $pm3IconPath -Headers $headers 225 | 226 | Log "Downloading WSL enable script..." 227 | Invoke-WebRequest -Uri $wslEnableScriptUrl -OutFile $wslEnableScriptPath -Headers $headers 228 | 229 | Log "Downloading USB reconnect script..." 230 | Invoke-WebRequest -Uri $usbReconnectScriptUrl -OutFile $usbReconnectScriptPath -Headers $headers 231 | 232 | Log "Downloading Proxmark3 flash script..." 233 | Invoke-WebRequest -Uri $proxmarkFlashScriptUrl -OutFile $proxmarkFlashScriptPath -Headers $headers 234 | 235 | Log "Downloading uninstall script..." 236 | Invoke-WebRequest -Uri $uninstallScriptUrl -OutFile $uninstallScriptPath -Headers $headers 237 | 238 | # Run the WSL enable script 239 | Log "Running WSL enable script..." 240 | powershell -ExecutionPolicy Bypass -File $wslEnableScriptPath 241 | 242 | # Check if a reboot is required 243 | if (Test-Path "$env:SystemRoot\System32\RebootPending.txt") { 244 | Write-Host "`n*************************************************************" -ForegroundColor Yellow 245 | Write-Host "* *" -ForegroundColor Yellow 246 | Write-Host "* A REBOOT IS REQUIRED TO COMPLETE THE WSL INSTALLATION. *" -ForegroundColor Yellow 247 | Write-Host "* PLEASE REBOOT YOUR SYSTEM AND RUN THIS SCRIPT AGAIN. *" -ForegroundColor Yellow 248 | Write-Host "* *" -ForegroundColor Yellow 249 | Write-Host "*************************************************************`n" -ForegroundColor Yellow 250 | Write-Host "Press any key to exit..." 251 | $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") 252 | exit 253 | } 254 | Log "Running WSL setup and installation..." 255 | powershell -ExecutionPolicy Bypass -File $setupScriptPath -DistroChoice $selectedDistro 256 | 257 | # Create desktop shortcuts 258 | Log "Creating Doppelganger Assistant desktop shortcut..." 259 | New-Shortcut -TargetPath "powershell.exe" ` 260 | -ShortcutPath $shortcutPath ` 261 | -Arguments "-NoProfile -ExecutionPolicy Bypass -File `"$launchScriptPath`"" ` 262 | -Description "Launch Doppelganger Assistant GUI as Administrator" ` 263 | -WorkingDirectory $basePath ` 264 | -IconLocation $imagePath 265 | 266 | Log "Doppelganger Assistant shortcut created." 267 | 268 | Log "Creating Proxmark3 Terminal desktop shortcut..." 269 | New-Shortcut -TargetPath "powershell.exe" ` 270 | -ShortcutPath $pm3ShortcutPath ` 271 | -Arguments "-NoProfile -ExecutionPolicy Bypass -File `"$pm3TerminalScriptPath`"" ` 272 | -Description "Launch Proxmark3 Terminal (pm3) as Administrator" ` 273 | -WorkingDirectory $basePath ` 274 | -IconLocation $pm3IconPath 275 | Log "Proxmark3 Terminal shortcut created." 276 | 277 | Log "Setup complete. Shortcuts created on the desktop." 278 | 279 | # Prompt user to flash Proxmark3 firmware 280 | Write-Host "`nDo you want to flash your Proxmark3 device firmware now?" -ForegroundColor Yellow 281 | Write-Host "WARNING: Only flash on physical hardware, not in VMs!" -ForegroundColor Red 282 | $flashChoice = Read-Host "Flash Proxmark3 firmware? (y/N)" 283 | if ($flashChoice -eq "y" -or $flashChoice -eq "Y") { 284 | Log "User chose to flash Proxmark3 firmware. Running Proxmark3 flash script..." 285 | powershell -ExecutionPolicy Bypass -File $proxmarkFlashScriptPath 286 | } 287 | else { 288 | Log "User chose not to flash Proxmark3 firmware." 289 | } 290 | 291 | $scriptPath = $MyInvocation.MyCommand.Path 292 | if ($scriptPath) { 293 | Log "Deleting installation script..." 294 | Remove-Item -Path $scriptPath -Force -ErrorAction SilentlyContinue 295 | Log "Installation script deleted." 296 | } 297 | 298 | Log "Installation complete!" 299 | -------------------------------------------------------------------------------- /installers/doppelganger_install_linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | # Function to check if a command exists 5 | command_exists() { 6 | command -v "$1" &> /dev/null 7 | } 8 | 9 | # Function to display ASCII art 10 | display_doppelganger_ascii() { 11 | echo -e "\e[31m" 12 | cat << "EOF" 13 | 14 | ____ _ 15 | | _ \ ___ _ __ _ __ ___| | __ _ __ _ _ __ __ _ ___ _ __ 16 | | | | |/ _ \| '_ \| '_ \ / _ \ |/ _` |/ _` | '_ \ / _` |/ _ \ '__| 17 | | |_| | (_) | |_) | |_) | __/ | (_| | (_| | | | | (_| | __/ | 18 | |____/ \___/| .__/| .__/ \___|_|\__, |\__,_|_| |_|\__, |\___|_| 19 | |_| |_| |___/ |___/ 20 | 21 | EOF 22 | echo -e "\e[0m" 23 | } 24 | 25 | # Function to prompt for reinstallation 26 | prompt_reinstall() { 27 | read -p "$1 is already installed. Do you want to reinstall it? (y/n): " choice < /dev/tty 28 | case "$choice" in 29 | y|Y ) return 0;; 30 | n|N ) return 1;; 31 | * ) echo "Invalid choice. Skipping reinstallation."; return 1;; 32 | esac 33 | } 34 | 35 | # Function to select Proxmark3 device type 36 | select_proxmark_device() { 37 | echo "" 38 | echo "==============================================" 39 | echo " Select your Proxmark3 device type:" 40 | echo "==============================================" 41 | echo "1) Proxmark3 RDV4 with Blueshark" 42 | echo "2) Proxmark3 RDV4 (without Blueshark)" 43 | echo "3) Proxmark3 Easy (512KB)" 44 | echo "" 45 | read -p "Enter your choice (1-3): " device_choice < /dev/tty 46 | 47 | case "$device_choice" in 48 | 1) 49 | echo "Selected: Proxmark3 RDV4 with Blueshark" 50 | PROXMARK_DEVICE="rdv4_bt" 51 | ;; 52 | 2) 53 | echo "Selected: Proxmark3 RDV4 (without Blueshark)" 54 | PROXMARK_DEVICE="rdv4" 55 | ;; 56 | 3) 57 | echo "Selected: Proxmark3 Easy (512KB)" 58 | PROXMARK_DEVICE="easy512" 59 | ;; 60 | *) 61 | echo "Invalid choice. Defaulting to Proxmark3 RDV4 with Blueshark" 62 | PROXMARK_DEVICE="rdv4_bt" 63 | ;; 64 | esac 65 | } 66 | 67 | # Function to configure Proxmark3 based on device type 68 | configure_proxmark_device() { 69 | local device_type=$1 70 | 71 | case "$device_type" in 72 | "rdv4_bt") 73 | echo "Configuring Proxmark3 RDV4 with Blueshark..." 74 | cp Makefile.platform.sample Makefile.platform 75 | sed -i 's/^#PLATFORM=PM3RDV4/PLATFORM=PM3RDV4/' Makefile.platform 76 | sed -i 's/#PLATFORM_EXTRAS=BTADDON/PLATFORM_EXTRAS=BTADDON/' Makefile.platform 77 | ;; 78 | "rdv4") 79 | echo "Configuring Proxmark3 RDV4 (no Blueshark)..." 80 | cp Makefile.platform.sample Makefile.platform 81 | sed -i 's/^#PLATFORM=PM3RDV4/PLATFORM=PM3RDV4/' Makefile.platform 82 | sed -i 's/^PLATFORM_EXTRAS=BTADDON/#PLATFORM_EXTRAS=BTADDON/' Makefile.platform 83 | ;; 84 | "easy512") 85 | echo "Configuring Proxmark3 Easy (512KB)..." 86 | cp Makefile.platform.sample Makefile.platform 87 | sed -i 's/^#PLATFORM=PM3GENERIC/PLATFORM=PM3GENERIC/' Makefile.platform 88 | sed -i 's/^#PLATFORM_SIZE=512/PLATFORM_SIZE=512/' Makefile.platform 89 | ;; 90 | esac 91 | } 92 | 93 | # Function to uninstall Doppelganger Assistant and Proxmark3 94 | uninstall_doppelganger() { 95 | echo "Uninstalling Doppelganger Assistant..." 96 | 97 | rm -f "$HOME/Desktop/Doppelganger Assistant.desktop" 98 | rm -f "$HOME/Desktop/doppelganger_assistant.desktop" 99 | rm -f "$HOME/.local/share/applications/doppelganger_assistant.desktop" 100 | 101 | sudo rm -f "/usr/share/pixmaps/doppelganger_assistant.png" 102 | 103 | if command_exists doppelganger_assistant; then 104 | sudo make uninstall 105 | fi 106 | 107 | if [ -d "proxmark3" ]; then 108 | cd proxmark3 109 | sudo make uninstall 110 | cd .. 111 | rm -rf proxmark3 112 | fi 113 | 114 | echo "Doppelganger Assistant has been uninstalled." 115 | exit 0 116 | } 117 | 118 | # Check for uninstall flag 119 | if [ "$1" = "--uninstall" ]; then 120 | uninstall_doppelganger 121 | fi 122 | 123 | # Function to detect the OS 124 | detect_os() { 125 | if [ -f /etc/os-release ]; then 126 | . /etc/os-release 127 | OS=$NAME 128 | elif type lsb_release >/dev/null 2>&1; then 129 | OS=$(lsb_release -si) 130 | elif [ -f /etc/lsb-release ]; then 131 | . /etc/lsb-release 132 | OS=$DISTRIB_ID 133 | else 134 | OS=$(uname -s) 135 | fi 136 | 137 | echo $OS 138 | } 139 | 140 | # Function to detect the Desktop Environment 141 | detect_desktop_environment() { 142 | if [ -n "$XDG_CURRENT_DESKTOP" ]; then 143 | echo "$XDG_CURRENT_DESKTOP" 144 | return 145 | fi 146 | if [ -n "$GNOME_DESKTOP_SESSION_ID" ] || command -v gnome-shell &> /dev/null; then 147 | echo "GNOME" 148 | elif [ -n "$KDE_FULL_SESSION" ] || command -v plasmashell &> /dev/null; then 149 | echo "KDE" 150 | elif command -v xfce4-session &> /dev/null; then 151 | echo "XFCE" 152 | elif [ -n "$MATE_DESKTOP_SESSION_ID" ] || command -v mate-session &> /dev/null; then 153 | echo "MATE" 154 | elif command -v cinnamon &> /dev/null; then 155 | echo "Cinnamon" 156 | elif command -v lxsession &> /dev/null; then 157 | echo "LXDE" 158 | elif command -v lxqt-session &> /dev/null; then 159 | echo "LXQt" 160 | elif command -v budgie-panel &> /dev/null; then 161 | echo "Budgie" 162 | else 163 | echo "Unknown" 164 | fi 165 | } 166 | 167 | # Function to detect the Window Manager 168 | detect_window_manager() { 169 | local session_type="${XDG_SESSION_TYPE:-unknown}" 170 | for wm in xfwm4 mutter kwin_wayland kwin_x11 openbox i3 sway xmonad awesome spectrwm bspwm enlightenment; do 171 | if pgrep -x "$wm" >/dev/null 2>&1; then 172 | echo "$wm ($session_type)" 173 | return 174 | fi 175 | done 176 | 177 | if command -v wmctrl >/dev/null 2>&1; then 178 | local name=$(wmctrl -m 2>/dev/null | awk -F: '/Name/ {print $2}' | xargs) 179 | if [ -n "$name" ]; then 180 | echo "$name ($session_type)" 181 | return 182 | fi 183 | fi 184 | 185 | echo "unknown ($session_type)" 186 | } 187 | 188 | # Function to refresh desktop environment and make new .desktop files visible 189 | refresh_desktop_integration() { 190 | local desktop_env=$1 191 | 192 | echo "Refreshing desktop integration for $desktop_env..." 193 | 194 | if command -v update-desktop-database &> /dev/null; then 195 | update-desktop-database "$HOME/.local/share/applications" 2>/dev/null || true 196 | fi 197 | 198 | if command -v xdg-desktop-menu &> /dev/null; then 199 | xdg-desktop-menu forceupdate 2>/dev/null || true 200 | fi 201 | 202 | if command -v update-desktop-database &> /dev/null && command -v grep &> /dev/null; then 203 | (touch "$HOME/.local/share/applications" >/dev/null 2>&1) || true 204 | fi 205 | case "$desktop_env" in 206 | *"XFCE"*) 207 | echo "Applying XFCE-specific desktop refresh..." 208 | if command -v xfdesktop &> /dev/null; then 209 | (xfdesktop --reload >/dev/null 2>&1 &) || true 210 | fi 211 | ;; 212 | 213 | *"GNOME"*|*"Ubuntu"*) 214 | echo "Applying GNOME-specific desktop refresh..." 215 | if command -v gnome-shell &> /dev/null; then 216 | (killall -HUP gnome-shell >/dev/null 2>&1 &) || true 217 | fi 218 | ;; 219 | 220 | *"KDE"*|*"Plasma"*) 221 | echo "Applying KDE Plasma-specific desktop refresh..." 222 | if command -v kbuildsycoca5 &> /dev/null; then 223 | (kbuildsycoca5 >/dev/null 2>&1 &) || true 224 | elif command -v kbuildsycoca6 &> /dev/null; then 225 | (kbuildsycoca6 >/dev/null 2>&1 &) || true 226 | fi 227 | ;; 228 | 229 | *"Cinnamon"*) 230 | echo "Applying Cinnamon-specific desktop refresh..." 231 | if command -v cinnamon &> /dev/null && command -v dbus-send &> /dev/null; then 232 | (dbus-send --type=method_call --dest=org.Cinnamon /org/Cinnamon org.Cinnamon.ReloadXlet string:'menu@cinnamon.org' string:'APPLET' >/dev/null 2>&1 &) || true 233 | fi 234 | ;; 235 | 236 | *"MATE"*) 237 | echo "Applying MATE-specific desktop refresh..." 238 | if command -v mate-panel &> /dev/null; then 239 | (nohup mate-panel --replace >/dev/null 2>&1 &) || true 240 | fi 241 | ;; 242 | 243 | *"Budgie"*) 244 | echo "Applying Budgie-specific desktop refresh..." 245 | if command -v budgie-panel &> /dev/null; then 246 | (nohup budgie-panel --replace >/dev/null 2>&1 &) || true 247 | fi 248 | ;; 249 | 250 | *"LXDE"*|*"LXQt"*) 251 | echo "Applying LXDE/LXQt-specific desktop refresh..." 252 | if command -v lxpanelctl &> /dev/null; then 253 | (lxpanelctl restart >/dev/null 2>&1 &) || true 254 | fi 255 | ;; 256 | 257 | *) 258 | echo "Unknown desktop environment. Using generic refresh methods only." 259 | ;; 260 | esac 261 | 262 | echo "Desktop integration refresh completed." 263 | } 264 | 265 | # Detect the OS 266 | OS=$(detect_os) 267 | 268 | # Detect the Desktop Environment 269 | DESKTOP_ENV=$(detect_desktop_environment) 270 | 271 | # Detect Window Manager 272 | WINDOW_MANAGER=$(detect_window_manager) 273 | 274 | # Check if running as root 275 | if [ "$(id -u)" -eq 0 ]; then 276 | as_root=true 277 | else 278 | as_root=false 279 | fi 280 | 281 | # Display Doppelganger ASCII art 282 | display_doppelganger_ascii 283 | 284 | # Display detected environment 285 | echo "Detected OS: $OS" 286 | echo "Detected Desktop Environment: $DESKTOP_ENV" 287 | echo "Detected Window Manager: $WINDOW_MANAGER" 288 | echo "" 289 | 290 | if command_exists doppelganger_assistant; then 291 | if prompt_reinstall "Doppelganger Assistant"; then 292 | echo "Proceeding with Doppelganger Assistant reinstallation." 293 | skip_doppelganger_install=false 294 | else 295 | echo "Skipping Doppelganger Assistant installation." 296 | skip_doppelganger_install=true 297 | fi 298 | else 299 | echo "Doppelganger Assistant not found. Proceeding with installation." 300 | skip_doppelganger_install=false 301 | fi 302 | 303 | if command_exists pm3; then 304 | if prompt_reinstall "Proxmark3"; then 305 | echo "Proceeding with Proxmark3 reinstallation." 306 | skip_proxmark_install=false 307 | else 308 | echo "Skipping Proxmark3 installation." 309 | skip_proxmark_install=true 310 | fi 311 | else 312 | echo "Proxmark3 not found. Proceeding with installation." 313 | skip_proxmark_install=false 314 | fi 315 | 316 | if [ "$skip_proxmark_install" = false ]; then 317 | select_proxmark_device 318 | fi 319 | 320 | PREFLIGHT_DONE=1 321 | 322 | # Function to install packages based on the detected OS 323 | install_packages() { 324 | case "$OS" in 325 | "Ubuntu"|"Debian"|"Kali GNU/Linux"|"Parrot GNU/Linux"|"Parrot Security") 326 | sudo apt update 327 | sudo apt install -y "$@" 328 | ;; 329 | *) 330 | echo "Unsupported operating system: $OS" 331 | exit 1 332 | ;; 333 | esac 334 | } 335 | 336 | # Update and upgrade system packages 337 | case "$OS" in 338 | "Ubuntu"|"Debian"|"Kali GNU/Linux"|"Parrot GNU/Linux"|"Parrot Security") 339 | sudo apt update 340 | sudo apt upgrade -y 341 | ;; 342 | *) 343 | echo "Unsupported operating system: $OS" 344 | exit 1 345 | ;; 346 | esac 347 | 348 | if [ "$PREFLIGHT_DONE" != "1" ]; then 349 | if command_exists doppelganger_assistant; then 350 | if prompt_reinstall "Doppelganger Assistant"; then 351 | skip_doppelganger_install=false 352 | else 353 | skip_doppelganger_install=true 354 | fi 355 | else 356 | skip_doppelganger_install=false 357 | fi 358 | 359 | if command_exists pm3; then 360 | if prompt_reinstall "Proxmark3"; then 361 | skip_proxmark_install=false 362 | else 363 | skip_proxmark_install=true 364 | fi 365 | else 366 | skip_proxmark_install=false 367 | fi 368 | fi 369 | 370 | # Install necessary packages for Doppelganger Assistant 371 | if [ "$skip_doppelganger_install" = false ]; then 372 | ARCH=$(uname -m) 373 | case "$ARCH" in 374 | x86_64) 375 | ARCH_SUFFIX="amd64" 376 | ;; 377 | aarch64|arm64) 378 | ARCH_SUFFIX="arm64" 379 | ;; 380 | *) 381 | echo "Unsupported architecture: $ARCH" 382 | exit 1 383 | ;; 384 | esac 385 | 386 | case "$OS" in 387 | "Ubuntu"|"Debian"|"Kali GNU/Linux"|"Parrot GNU/Linux"|"Parrot Security") 388 | echo "Detected Debian-based system. Installing from .deb package..." 389 | 390 | # Install required dependencies 391 | install_packages libgl1 xterm wget 392 | 393 | # Download the latest Doppelganger Assistant .deb package 394 | TIMESTAMP=$(date +%s) 395 | DEB_FILE="doppelganger_assistant_linux_${ARCH_SUFFIX}.deb" 396 | 397 | echo "Downloading ${DEB_FILE}..." 398 | wget --no-cache --no-cookies "https://github.com/tweathers-sec/doppelganger_assistant/releases/latest/download/${DEB_FILE}?t=${TIMESTAMP}" -O "${DEB_FILE}" 399 | 400 | # Install the .deb package 401 | echo "Installing Doppelganger Assistant..." 402 | sudo dpkg -i "${DEB_FILE}" || sudo apt-get install -f -y 403 | 404 | # Cleanup 405 | rm -f "${DEB_FILE}" 406 | 407 | echo "Doppelganger Assistant installed successfully via .deb package." 408 | ;; 409 | 410 | *) 411 | echo "Non-Debian system detected. Installing from tar.xz archive..." 412 | 413 | # Install required dependencies 414 | install_packages libgl1 xterm make git wget 415 | 416 | # Download the latest Doppelganger Assistant release 417 | TIMESTAMP=$(date +%s) 418 | TARBALL="doppelganger_assistant_linux_${ARCH_SUFFIX}.tar.xz" 419 | 420 | echo "Downloading ${TARBALL}..." 421 | wget --no-cache --no-cookies "https://github.com/tweathers-sec/doppelganger_assistant/releases/latest/download/${TARBALL}?t=${TIMESTAMP}" -O "${TARBALL}" 422 | 423 | # Extract and install Doppelganger Assistant 424 | tar xvf "${TARBALL}" 425 | cd doppelganger_assistant 426 | sudo make install 427 | 428 | # Cleanup 429 | cd .. 430 | rm -rf doppelganger_assistant 431 | rm -f "${TARBALL}" 432 | 433 | echo "Doppelganger Assistant installed successfully from tarball." 434 | ;; 435 | esac 436 | fi 437 | 438 | # Install dependencies for Proxmark3 439 | if [ "$skip_proxmark_install" = false ]; then 440 | install_packages git ca-certificates build-essential pkg-config \ 441 | libreadline-dev gcc-arm-none-eabi libnewlib-dev qtbase5-dev \ 442 | libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev 443 | 444 | # Clone the Proxmark3 repository 445 | if [ ! -d "proxmark3" ]; then 446 | git clone https://github.com/RfidResearchGroup/proxmark3.git 447 | fi 448 | 449 | cd proxmark3 450 | 451 | if [ -z "$PROXMARK_DEVICE" ]; then 452 | select_proxmark_device 453 | fi 454 | 455 | configure_proxmark_device "$PROXMARK_DEVICE" 456 | make clean && make -j$(nproc) 457 | sudo make install 458 | fi 459 | 460 | if [ "$skip_doppelganger_install" = false ]; then 461 | case "$OS" in 462 | "Ubuntu"|"Debian"|"Kali GNU/Linux"|"Parrot GNU/Linux"|"Parrot Security") 463 | echo "Configuring desktop integration for Debian-based system..." 464 | 465 | if command -v update-desktop-database &> /dev/null; then 466 | update-desktop-database "$HOME/.local/share/applications" 2>/dev/null || true 467 | sudo update-desktop-database /usr/share/applications 2>/dev/null || true 468 | fi 469 | 470 | if command -v update-mime-database &> /dev/null; then 471 | sudo update-mime-database /usr/share/mime 2>/dev/null || true 472 | fi 473 | 474 | if command -v gtk-update-icon-cache &> /dev/null; then 475 | sudo gtk-update-icon-cache -f -t /usr/share/icons/hicolor 2>/dev/null || true 476 | fi 477 | mkdir -p "$HOME/Desktop" 478 | desktop_shortcut="$HOME/Desktop/doppelganger_assistant.desktop" 479 | 480 | if [ -f "/usr/share/applications/doppelganger_assistant.desktop" ]; then 481 | cp /usr/share/applications/doppelganger_assistant.desktop "$desktop_shortcut" 482 | chmod +x "$desktop_shortcut" 483 | 484 | if command -v gio &> /dev/null; then 485 | gio set "$desktop_shortcut" metadata::trusted true 2>/dev/null || true 486 | fi 487 | 488 | echo "Desktop shortcut created successfully." 489 | else 490 | echo "Warning: System desktop file not found. The application should still be in your menu." 491 | fi 492 | 493 | refresh_desktop_integration "$DESKTOP_ENV" 494 | 495 | echo "" 496 | echo "Desktop integration configured." 497 | echo "The application should now appear in your applications menu." 498 | ;; 499 | 500 | *) 501 | echo "Creating desktop files for non-Debian system..." 502 | 503 | desktop_file="$HOME/.local/share/applications/doppelganger_assistant.desktop" 504 | icon_path="/usr/share/pixmaps/doppelganger_assistant.png" 505 | 506 | if [ ! -f "$icon_path" ]; then 507 | echo "Downloading Doppelganger Assistant icon..." 508 | sudo wget -O "$icon_path" "https://raw.githubusercontent.com/tweathers-sec/doppelganger_assistant/main/img/doppelganger_assistant.png" 509 | fi 510 | 511 | mkdir -p "$HOME/.local/share/applications" 512 | mkdir -p "$HOME/Desktop" 513 | cat > "$desktop_file" << EOL 514 | [Desktop Entry] 515 | Version=1.0 516 | Type=Application 517 | Name=Doppelganger Assistant 518 | Comment=Launch Doppelganger Assistant 519 | Exec=doppelganger_assistant 520 | Icon=$icon_path 521 | Terminal=false 522 | Categories=Utility;System; 523 | EOL 524 | 525 | chmod +x "$desktop_file" 526 | 527 | desktop_shortcut="$HOME/Desktop/doppelganger_assistant.desktop" 528 | cat > "$desktop_shortcut" << EOL 529 | [Desktop Entry] 530 | Version=1.0 531 | Type=Application 532 | Name=Doppelganger Assistant 533 | Comment=Launch Doppelganger Assistant 534 | Exec=doppelganger_assistant 535 | Icon=$icon_path 536 | Terminal=false 537 | Categories=Utility;System; 538 | EOL 539 | 540 | chmod +x "$desktop_shortcut" 541 | 542 | if command -v gio &> /dev/null; then 543 | gio set "$desktop_shortcut" metadata::trusted true 2>/dev/null || true 544 | fi 545 | 546 | refresh_desktop_integration "$DESKTOP_ENV" 547 | 548 | echo "" 549 | echo "Desktop shortcut created successfully." 550 | ;; 551 | esac 552 | 553 | case "$DESKTOP_ENV" in 554 | *"XFCE"*) 555 | echo "" 556 | echo "XFCE Tips:" 557 | echo " - If the menu doesn't update immediately, log out and back in" 558 | echo " - Or run 'xfce4-panel -r' from a user terminal" 559 | ;; 560 | *"KDE"*|*"Plasma"*) 561 | echo "" 562 | echo "KDE/Plasma Tips:" 563 | echo " - Menu items should appear immediately" 564 | echo " - If not, try logging out and back in" 565 | ;; 566 | *"GNOME"*) 567 | echo "" 568 | echo "GNOME Tips:" 569 | echo " - Press Super key and search for 'Doppelganger'" 570 | echo " - If menu doesn't update, press Alt+F2 and type 'r' to restart GNOME Shell" 571 | ;; 572 | esac 573 | fi 574 | 575 | echo "" 576 | echo "==============================================" 577 | echo "Installation process completed successfully!" 578 | echo "==============================================" 579 | echo "" 580 | echo "To launch Doppelganger Assistant:" 581 | echo " - Look for 'Doppelganger Assistant' in your applications menu" 582 | echo " - Or run 'doppelganger_assistant' from the terminal" 583 | echo " - Or use the desktop shortcut (if created)" 584 | echo "" -------------------------------------------------------------------------------- /src/hotel_recovery.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "regexp" 7 | "sort" 8 | "strings" 9 | ) 10 | 11 | // recoverHotelKey attempts to recover keys from a hotel key card (MIFARE Classic) 12 | // Uses Proxmark3's built-in recovery tools 13 | // onFilePathsFound is called with dumpFilePath and keyFilePath when files are found 14 | func recoverHotelKey(recoveryMethod string, onFilePathsFound func(string, string)) { 15 | if ok, msg := checkProxmark3(); !ok { 16 | WriteStatusError(msg) 17 | return 18 | } 19 | 20 | pm3Binary, err := getPm3Path() 21 | if err != nil { 22 | WriteStatusError("Failed to find pm3 binary: %v", err) 23 | return 24 | } 25 | 26 | device, err := getPm3Device() 27 | if err != nil { 28 | WriteStatusError("Failed to detect pm3 device: %v", err) 29 | return 30 | } 31 | 32 | var cmd *exec.Cmd 33 | var cmdStr string 34 | isRecoveryMethod := true 35 | 36 | switch recoveryMethod { 37 | case "autopwn": 38 | // Automatic key recovery - tries multiple methods 39 | cmdStr = "hf mf autopwn" 40 | cmd = exec.Command(pm3Binary, "-c", "hf mf autopwn", "-p", device) 41 | WriteStatusProgress("Starting hotel key card recovery...") 42 | WriteStatusInfo("Place the hotel key card on the reader and keep it there during recovery") 43 | WriteStatusInfo("Using automatic recovery (autopwn) - this will try multiple attack methods") 44 | case "darkside": 45 | // Darkside attack - fast but only works on vulnerable cards 46 | cmdStr = "hf mf darkside" 47 | cmd = exec.Command(pm3Binary, "-c", "hf mf darkside", "-p", device) 48 | WriteStatusProgress("Starting hotel key card recovery...") 49 | WriteStatusInfo("Place the hotel key card on the reader and keep it there during recovery") 50 | WriteStatusInfo("Using Darkside attack - fast but only works on vulnerable cards") 51 | case "nested": 52 | // Nested attack - works on most cards but slower 53 | cmdStr = "hf mf nested" 54 | cmd = exec.Command(pm3Binary, "-c", "hf mf nested", "-p", device) 55 | WriteStatusProgress("Starting hotel key card recovery...") 56 | WriteStatusInfo("Place the hotel key card on the reader and keep it there during recovery") 57 | WriteStatusInfo("Using Nested attack - works on most cards but may take longer") 58 | case "hardnested": 59 | // Hardnested attack - for hardened cards 60 | cmdStr = "hf mf hardnested" 61 | cmd = exec.Command(pm3Binary, "-c", "hf mf hardnested", "-p", device) 62 | WriteStatusProgress("Starting hotel key card recovery...") 63 | WriteStatusInfo("Place the hotel key card on the reader and keep it there during recovery") 64 | WriteStatusInfo("Using Hardnested attack - for hardened MIFARE Classic cards") 65 | case "staticnested": 66 | // Static nested attack - for cards with static nonces 67 | cmdStr = "hf mf staticnested" 68 | cmd = exec.Command(pm3Binary, "-c", "hf mf staticnested", "-p", device) 69 | WriteStatusProgress("Starting hotel key card recovery...") 70 | WriteStatusInfo("Place the hotel key card on the reader and keep it there during recovery") 71 | WriteStatusInfo("Using Static Nested attack - for cards with static nonces") 72 | case "brute": 73 | // Smart bruteforce - exploits weak key generators 74 | cmdStr = "hf mf brute" 75 | cmd = exec.Command(pm3Binary, "-c", "hf mf brute", "-p", device) 76 | WriteStatusProgress("Starting hotel key card recovery...") 77 | WriteStatusInfo("Place the hotel key card on the reader and keep it there during recovery") 78 | WriteStatusInfo("Using Smart Bruteforce attack - exploits weak key generators") 79 | case "nack": 80 | // NACK bug test - tests for MIFARE NACK bug vulnerability 81 | cmdStr = "hf mf nack" 82 | cmd = exec.Command(pm3Binary, "-c", "hf mf nack", "-p", device) 83 | WriteStatusProgress("Testing MIFARE card for NACK bug vulnerability...") 84 | WriteStatusInfo("Place the card on the reader") 85 | isRecoveryMethod = false 86 | default: 87 | WriteStatusError("Unknown recovery method: %s", recoveryMethod) 88 | return 89 | } 90 | 91 | fmt.Println(cmdStr) 92 | fmt.Println() 93 | 94 | output, cmdErr := cmd.CombinedOutput() 95 | outputStr := string(output) 96 | 97 | // Print full output (no filtering) 98 | fmt.Println(outputStr) 99 | 100 | // For NACK test, just report completion 101 | if !isRecoveryMethod { 102 | if cmdErr != nil { 103 | WriteStatusError("NACK test failed: %v", cmdErr) 104 | } else { 105 | WriteStatusSuccess("NACK test completed. Review output above for results.") 106 | } 107 | return 108 | } 109 | 110 | // Parse recovery results 111 | sectorsRecovered := parseRecoveryOutput(outputStr) 112 | 113 | if cmdErr != nil { 114 | WriteStatusError("Recovery failed: %v", cmdErr) 115 | if sectorsRecovered > 0 { 116 | WriteStatusInfo("Partial recovery: %d sectors recovered", sectorsRecovered) 117 | } 118 | return 119 | } 120 | 121 | // Check if keys were recovered 122 | if sectorsRecovered > 0 { 123 | WriteStatusSuccess("Key recovery successful! Recovered keys for %d sectors", sectorsRecovered) 124 | 125 | // Automatically dump the card data after key recovery 126 | WriteStatusProgress("Dumping card data...") 127 | fmt.Println() 128 | fmt.Println("hf mf dump") 129 | fmt.Println() 130 | 131 | dumpCmd := exec.Command(pm3Binary, "-c", "hf mf dump", "-p", device) 132 | dumpOutput, dumpErr := dumpCmd.CombinedOutput() 133 | dumpOutputStr := string(dumpOutput) 134 | fmt.Println(dumpOutputStr) 135 | 136 | var dumpFilePath string 137 | var keyFilePath string 138 | 139 | if dumpErr == nil { 140 | // Extract dump file location if available 141 | // Try pattern with backticks first (common in Proxmark3 output) 142 | dumpFileRegex := regexp.MustCompile(`Saved.*?to.*?file.*?[` + "`" + `'"]?([^` + "`" + `'"]+hf-mf-[A-F0-9]+-dump-[0-9]+\.(bin|eml))[` + "`" + `'"]?`) 143 | if matches := dumpFileRegex.FindStringSubmatch(dumpOutputStr); len(matches) > 1 { 144 | dumpFilePath = matches[1] 145 | WriteStatusSuccess("Card data dumped to: %s", dumpFilePath) 146 | } else { 147 | // Try pattern without backticks 148 | dumpFileRegex2 := regexp.MustCompile(`Saved.*?to.*?file.*?([/][^\s]+\.(bin|eml))`) 149 | if matches := dumpFileRegex2.FindStringSubmatch(dumpOutputStr); len(matches) > 1 { 150 | dumpFilePath = matches[1] 151 | WriteStatusSuccess("Card data dumped to: %s", dumpFilePath) 152 | } else { 153 | // Try alternative pattern for any dump file 154 | dumpFileRegex3 := regexp.MustCompile(`([/][^\s]+hf-mf-[A-F0-9]+-dump-[0-9]+\.(bin|eml))`) 155 | if matches := dumpFileRegex3.FindStringSubmatch(dumpOutputStr); len(matches) > 1 { 156 | dumpFilePath = matches[1] 157 | WriteStatusSuccess("Card data dumped to: %s", dumpFilePath) 158 | } else { 159 | WriteStatusSuccess("Card data dumped successfully") 160 | } 161 | } 162 | } 163 | 164 | // Extract key file location if available 165 | // Try pattern with backticks first 166 | keyFileRegex := regexp.MustCompile(`Saved.*?key.*?file.*?[` + "`" + `'"]?([/][^` + "`" + `'"]+hf-mf-[A-F0-9]+-key\.bin)[` + "`" + `'"]?`) 167 | if matches := keyFileRegex.FindStringSubmatch(dumpOutputStr); len(matches) > 1 { 168 | keyFilePath = matches[1] 169 | WriteStatusInfo("Keys saved to: %s", keyFilePath) 170 | } else { 171 | // Try pattern without backticks 172 | keyFileRegex2 := regexp.MustCompile(`Saved.*?key.*?file.*?([/][^\s]+\.bin)`) 173 | if matches := keyFileRegex2.FindStringSubmatch(dumpOutputStr); len(matches) > 1 { 174 | keyFilePath = matches[1] 175 | WriteStatusInfo("Keys saved to: %s", keyFilePath) 176 | } else { 177 | // Try alternative pattern for any key file 178 | keyFileRegex3 := regexp.MustCompile(`([/][^\s]+hf-mf-[A-F0-9]+-key\.bin)`) 179 | if matches := keyFileRegex3.FindStringSubmatch(dumpOutputStr); len(matches) > 1 { 180 | keyFilePath = matches[1] 181 | WriteStatusInfo("Keys saved to: %s", keyFilePath) 182 | } 183 | } 184 | } 185 | 186 | // Show summary of recovered keys 187 | parseAndDisplayKeySummary(outputStr) 188 | 189 | // Call callback to update GUI fields if provided 190 | if onFilePathsFound != nil { 191 | onFilePathsFound(dumpFilePath, keyFilePath) 192 | } 193 | 194 | // After successful dump, offer to write/restore to a new card 195 | if dumpFilePath != "" { 196 | WriteStatusInfo("") 197 | WriteStatusInfo("To write this data to a new card, use:") 198 | if keyFilePath != "" { 199 | WriteStatusInfo(" hf mf restore -f %s -k %s", dumpFilePath, keyFilePath) 200 | } else { 201 | WriteStatusInfo(" hf mf restore -f %s", dumpFilePath) 202 | } 203 | } 204 | 205 | } else { 206 | WriteStatusError("Dump failed: %v", dumpErr) 207 | WriteStatusInfo("You can manually dump with: hf mf dump") 208 | } 209 | } else { 210 | WriteStatusInfo("Recovery completed. Review output above for results.") 211 | } 212 | } 213 | 214 | // parseRecoveryOutput parses the autopwn output to count recovered sectors 215 | func parseRecoveryOutput(output string) int { 216 | // Look for the key table summary - count sectors with at least one key recovered 217 | // Pattern: "| 000 | 003 | FFFFFFFFFFFF | D | FFFFFFFFFFFF | D |" 218 | // or "| 001 | 007 | ------------ | 0 | ------------ | 0 |" (failed) 219 | keyTableRegex := regexp.MustCompile(`\|\s+(\d{3})\s+\|\s+\d{3}\s+\|\s+([A-F0-9]{12}|-{12})\s+\|\s+([DSUNHRCA0])\s+\|\s+([A-F0-9]{12}|-{12})\s+\|\s+([DSUNHRCA0])\s+\|`) 220 | matches := keyTableRegex.FindAllStringSubmatch(output, -1) 221 | 222 | if len(matches) > 0 { 223 | // Count sectors where at least one key was recovered (result is not "0") 224 | recoveredCount := 0 225 | for _, match := range matches { 226 | if len(match) >= 6 { 227 | keyAResult := match[3] 228 | keyBResult := match[5] 229 | // If either key was recovered (not "0"), count this sector 230 | if keyAResult != "0" || keyBResult != "0" { 231 | recoveredCount++ 232 | } 233 | } 234 | } 235 | return recoveredCount 236 | } 237 | 238 | // Fallback: count "found valid key" messages 239 | foundKeyRegex := regexp.MustCompile(`found valid key`) 240 | matches2 := foundKeyRegex.FindAllStringSubmatch(output, -1) 241 | return len(matches2) / 2 // Divide by 2 since each sector has key A and key B 242 | } 243 | 244 | // parseAndDisplayKeySummary extracts and displays a clean summary of recovered keys 245 | func parseAndDisplayKeySummary(output string) { 246 | // Extract key table section 247 | keyTableStart := strings.Index(output, "-----+-----+--------------+---+--------------+----") 248 | if keyTableStart == -1 { 249 | return 250 | } 251 | 252 | // Find the end of the key table (look for the legend line) 253 | keyTableSection := output[keyTableStart:] 254 | keyTableEnd := strings.Index(keyTableSection, "( D:Dictionary") 255 | if keyTableEnd == -1 { 256 | keyTableEnd = len(keyTableSection) 257 | } 258 | 259 | keyTableSection = keyTableSection[:keyTableEnd] 260 | lines := strings.Split(keyTableSection, "\n") 261 | 262 | var recoveredSectors []string 263 | var failedSectors []string 264 | var totalKeys int 265 | var keyAMethods = make(map[string]int) // Count by method 266 | var keyBMethods = make(map[string]int) 267 | 268 | // Parse each line in the key table 269 | keyLineRegex := regexp.MustCompile(`\|\s+(\d{3})\s+\|\s+\d{3}\s+\|\s+([A-F0-9]{12}|-{12})\s+\|\s+([DSUNHRCA0])\s+\|\s+([A-F0-9]{12}|-{12})\s+\|\s+([DSUNHRCA0])\s+\|`) 270 | 271 | for _, line := range lines { 272 | if matches := keyLineRegex.FindStringSubmatch(line); len(matches) >= 6 { 273 | sectorNum := matches[1] 274 | keyA := matches[2] 275 | keyAResult := matches[3] 276 | keyB := matches[4] 277 | keyBResult := matches[5] 278 | 279 | // Track recovery methods 280 | if keyAResult != "0" && keyA != "------------" { 281 | totalKeys++ 282 | keyAMethods[keyAResult]++ 283 | } 284 | if keyBResult != "0" && keyB != "------------" { 285 | totalKeys++ 286 | keyBMethods[keyBResult]++ 287 | } 288 | 289 | // Track sectors 290 | if keyAResult != "0" || keyBResult != "0" { 291 | recoveredSectors = append(recoveredSectors, sectorNum) 292 | } else { 293 | failedSectors = append(failedSectors, sectorNum) 294 | } 295 | } 296 | } 297 | 298 | // Display summary 299 | WriteStatusInfo("") 300 | WriteStatusInfo("--- Recovery Summary ---") 301 | WriteStatusInfo("Sectors recovered: %d / 16", len(recoveredSectors)) 302 | WriteStatusInfo("Total keys found: %d", totalKeys) 303 | 304 | if len(recoveredSectors) > 0 { 305 | WriteStatusSuccess("Recovered sectors: %s", strings.Join(recoveredSectors, ", ")) 306 | } 307 | 308 | if len(failedSectors) > 0 { 309 | WriteStatusError("Failed sectors: %s", strings.Join(failedSectors, ", ")) 310 | } 311 | 312 | // Show recovery methods used 313 | methodNames := map[string]string{ 314 | "D": "Dictionary", 315 | "S": "Darkside", 316 | "U": "User", 317 | "R": "Reused", 318 | "N": "Nested", 319 | "H": "Hardnested", 320 | "C": "Static Nested", 321 | "A": "Key A", 322 | } 323 | 324 | var methodsUsed []string 325 | for method, count := range keyAMethods { 326 | if name, ok := methodNames[method]; ok { 327 | methodsUsed = append(methodsUsed, fmt.Sprintf("%s (%d)", name, count)) 328 | } 329 | } 330 | for method, count := range keyBMethods { 331 | if name, ok := methodNames[method]; ok { 332 | // Check if already added 333 | found := false 334 | for i, m := range methodsUsed { 335 | if strings.Contains(m, name) { 336 | // Update count 337 | methodsUsed[i] = fmt.Sprintf("%s (%d)", name, count+keyAMethods[method]) 338 | found = true 339 | break 340 | } 341 | } 342 | if !found { 343 | methodsUsed = append(methodsUsed, fmt.Sprintf("%s (%d)", name, count)) 344 | } 345 | } 346 | } 347 | 348 | if len(methodsUsed) > 0 { 349 | WriteStatusInfo("Recovery methods: %s", strings.Join(methodsUsed, ", ")) 350 | } 351 | } 352 | 353 | // executeMifareCommand executes a MIFARE command and displays output 354 | func executeMifareCommand(cmdStr string, description string) (string, error) { 355 | if ok, msg := checkProxmark3(); !ok { 356 | WriteStatusError(msg) 357 | return "", fmt.Errorf("%s", msg) 358 | } 359 | 360 | pm3Binary, err := getPm3Path() 361 | if err != nil { 362 | WriteStatusError("Failed to find pm3 binary: %v", err) 363 | return "", err 364 | } 365 | 366 | device, err := getPm3Device() 367 | if err != nil { 368 | WriteStatusError("Failed to detect pm3 device: %v", err) 369 | return "", err 370 | } 371 | 372 | WriteStatusProgress(description) 373 | WriteStatusInfo("Place card on reader") 374 | 375 | fmt.Println(cmdStr) 376 | fmt.Println() 377 | 378 | cmd := exec.Command(pm3Binary, "-c", cmdStr, "-p", device) 379 | output, cmdErr := cmd.CombinedOutput() 380 | outputStr := string(output) 381 | fmt.Println(outputStr) 382 | 383 | return outputStr, cmdErr 384 | } 385 | 386 | // checkKeysFast executes hf mf fchk to check all keys on card 387 | func checkKeysFast(keyFilePath string) { 388 | cmdStr := "hf mf fchk" 389 | if keyFilePath != "" { 390 | cmdStr = fmt.Sprintf("hf mf fchk -f %s", keyFilePath) 391 | } 392 | 393 | outputStr, cmdErr := executeMifareCommand(cmdStr, "Checking keys on card (fast check)...") 394 | 395 | if cmdErr != nil { 396 | WriteStatusError("Key check failed: %v", cmdErr) 397 | return 398 | } 399 | 400 | // Parse key check results from the table 401 | // Pattern from Proxmark3: "[+] 001 | 007 | 2A2C13CC242A | 1 | FFFFFFFFFFFF | 1" 402 | // The format string in Proxmark3 is: " " _YELLOW_("%03d") " | %03d | %s | %s | %s | %s %s" 403 | // PrintAndLogEx(SUCCESS, ...) adds the [+] prefix 404 | successCount := 0 405 | failedCount := 0 406 | 407 | // Extract all found keys from the table 408 | keyTableRegex := regexp.MustCompile(`\[\+\]\s+(\d{3})\s+\|\s+(\d{3})\s+\|\s+([A-F0-9]{12}|-{12})\s+\|\s+([01])\s+\|\s+([A-F0-9]{12}|-{12})\s+\|\s+([01])`) 409 | keyMatches := keyTableRegex.FindAllStringSubmatch(outputStr, -1) 410 | 411 | // Store keys by sector/block to group Key A and Key B together 412 | type sectorBlockKey struct { 413 | sector string 414 | block string 415 | } 416 | type keyPair struct { 417 | keyA string 418 | keyB string 419 | } 420 | keysBySectorBlock := make(map[sectorBlockKey]keyPair) 421 | 422 | for _, match := range keyMatches { 423 | if len(match) >= 7 { 424 | sector := match[1] 425 | block := match[2] 426 | keyA := match[3] 427 | keyAResult := match[4] 428 | keyB := match[5] 429 | keyBResult := match[6] 430 | 431 | sbKey := sectorBlockKey{sector: sector, block: block} 432 | pair := keysBySectorBlock[sbKey] 433 | 434 | // Process key A 435 | if keyAResult == "1" && keyA != "------------" { 436 | pair.keyA = keyA 437 | successCount++ 438 | } else if keyAResult == "0" { 439 | failedCount++ 440 | } 441 | 442 | // Process key B 443 | if keyBResult == "1" && keyB != "------------" { 444 | pair.keyB = keyB 445 | successCount++ 446 | } else if keyBResult == "0" { 447 | failedCount++ 448 | } 449 | 450 | keysBySectorBlock[sbKey] = pair 451 | } 452 | } 453 | 454 | // Build sorted list: Key A first, then Key B for each sector/block 455 | var foundKeys []string 456 | var sortedSectors []sectorBlockKey 457 | for sbKey := range keysBySectorBlock { 458 | sortedSectors = append(sortedSectors, sbKey) 459 | } 460 | // Sort by sector, then block 461 | sort.Slice(sortedSectors, func(i, j int) bool { 462 | if sortedSectors[i].sector != sortedSectors[j].sector { 463 | return sortedSectors[i].sector < sortedSectors[j].sector 464 | } 465 | return sortedSectors[i].block < sortedSectors[j].block 466 | }) 467 | 468 | for _, sbKey := range sortedSectors { 469 | pair := keysBySectorBlock[sbKey] 470 | // Add Key A first 471 | if pair.keyA != "" { 472 | foundKeys = append(foundKeys, fmt.Sprintf("Sector %s Block %s Key A: %s", sbKey.sector, sbKey.block, pair.keyA)) 473 | } 474 | // Then Key B 475 | if pair.keyB != "" { 476 | foundKeys = append(foundKeys, fmt.Sprintf("Sector %s Block %s Key B: %s", sbKey.sector, sbKey.block, pair.keyB)) 477 | } 478 | } 479 | 480 | // Also check for summary line pattern: "( 0:Failed / 1:Success )" for more accurate counts 481 | summaryRegex := regexp.MustCompile(`\(\s*(\d+):Failed\s*/\s*(\d+):Success\s*\)`) 482 | if summaryMatch := summaryRegex.FindStringSubmatch(outputStr); len(summaryMatch) >= 3 { 483 | // Use summary counts if available (more accurate) 484 | var failedFromSummary, successFromSummary int 485 | fmt.Sscanf(summaryMatch[1], "%d", &failedFromSummary) 486 | fmt.Sscanf(summaryMatch[2], "%d", &successFromSummary) 487 | if failedFromSummary > 0 || successFromSummary > 0 { 488 | failedCount = failedFromSummary 489 | successCount = successFromSummary 490 | } 491 | } 492 | 493 | // Always display keys if found, regardless of count source 494 | if len(foundKeys) > 0 { 495 | WriteStatusSuccess("Found %d valid keys", successCount) 496 | if failedCount > 0 { 497 | WriteStatusInfo("%d keys failed authentication", failedCount) 498 | } 499 | WriteStatusInfo("") 500 | WriteStatusInfo("--- Found Keys ---") 501 | for _, keyInfo := range foundKeys { 502 | WriteStatusInfo(keyInfo) 503 | } 504 | } else if successCount > 0 { 505 | WriteStatusSuccess("Found %d valid keys", successCount) 506 | if failedCount > 0 { 507 | WriteStatusInfo("%d keys failed authentication", failedCount) 508 | } 509 | WriteStatusInfo("(Keys found but format not recognized - see full output)") 510 | } else { 511 | WriteStatusError("No valid keys found") 512 | } 513 | } 514 | 515 | // min helper function 516 | func min(a, b int) int { 517 | if a < b { 518 | return a 519 | } 520 | return b 521 | } 522 | 523 | // getCardInfo executes hf mf info to get detailed card information 524 | func getCardInfo() { 525 | outputStr, cmdErr := executeMifareCommand("hf mf info", "Getting detailed card information...") 526 | 527 | if cmdErr != nil { 528 | WriteStatusError("Failed to get card info: %v", cmdErr) 529 | return 530 | } 531 | 532 | // Extract UID - format is " UID: 5A F7 0D 9D" or " UID: 5AF70D9D" 533 | // Try with spaces first, then without 534 | uidRegex1 := regexp.MustCompile(`UID\s*:\s*([A-F0-9]{2}(?:\s+[A-F0-9]{2})+)`) 535 | if uidMatch := uidRegex1.FindStringSubmatch(outputStr); len(uidMatch) > 1 { 536 | // Remove spaces from UID 537 | uid := strings.ReplaceAll(uidMatch[1], " ", "") 538 | WriteStatusSuccess("Card UID: %s", uid) 539 | } else { 540 | // Try without spaces 541 | uidRegex2 := regexp.MustCompile(`UID\s*:\s*([A-F0-9]{8,14})`) 542 | if uidMatch := uidRegex2.FindStringSubmatch(outputStr); len(uidMatch) > 1 { 543 | WriteStatusSuccess("Card UID: %s", uidMatch[1]) 544 | } 545 | } 546 | 547 | // Extract card type 548 | if strings.Contains(outputStr, "MIFARE Classic") { 549 | WriteStatusInfo("Card Type: MIFARE Classic") 550 | if strings.Contains(outputStr, "1K") { 551 | WriteStatusInfo("Size: 1K (16 sectors)") 552 | } else if strings.Contains(outputStr, "4K") { 553 | WriteStatusInfo("Size: 4K (40 sectors)") 554 | } 555 | } 556 | 557 | // Extract magic capabilities 558 | if strings.Contains(outputStr, "Magic capabilities") { 559 | magicRegex := regexp.MustCompile(`Magic capabilities\.\.\.\s+([^\n]+)`) 560 | if magicMatch := magicRegex.FindStringSubmatch(outputStr); len(magicMatch) > 1 { 561 | WriteStatusInfo("Magic: %s", strings.TrimSpace(magicMatch[1])) 562 | } 563 | } 564 | 565 | // Extract PRNG info 566 | if strings.Contains(outputStr, "Prng") { 567 | prngRegex := regexp.MustCompile(`Prng[^:]*:\s*([^\n]+)`) 568 | if prngMatch := prngRegex.FindStringSubmatch(outputStr); len(prngMatch) > 1 { 569 | WriteStatusInfo("PRNG: %s", strings.TrimSpace(prngMatch[1])) 570 | } 571 | } 572 | 573 | // Check for Saflok 574 | if strings.Contains(outputStr, "Saflok") { 575 | WriteStatusInfo("Detected: Saflok hotel key card") 576 | } 577 | } 578 | 579 | // setMagicCardUID executes hf mf csetuid to set UID on Chinese magic card 580 | func setMagicCardUID(uid string) { 581 | if uid == "" { 582 | WriteStatusError("UID is required") 583 | return 584 | } 585 | cmdStr := fmt.Sprintf("hf mf csetuid -u %s", uid) 586 | 587 | outputStr, cmdErr := executeMifareCommand(cmdStr, "Setting UID on magic card...") 588 | 589 | if cmdErr != nil { 590 | WriteStatusError("Failed to set UID: %v", cmdErr) 591 | return 592 | } 593 | 594 | // Check for success 595 | if strings.Contains(outputStr, "success") || strings.Contains(outputStr, "Success") || 596 | strings.Contains(outputStr, "OK") || strings.Contains(outputStr, "ok") { 597 | WriteStatusSuccess("UID set successfully: %s", uid) 598 | } else if strings.Contains(outputStr, "error") || strings.Contains(outputStr, "Error") || 599 | strings.Contains(outputStr, "failed") || strings.Contains(outputStr, "Failed") { 600 | WriteStatusError("Failed to set UID") 601 | } else { 602 | WriteStatusInfo("UID operation completed - review output for confirmation") 603 | } 604 | } 605 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Doppelgänger Assistant 2 | 3 | A professional GUI application for calculating card data and automating Proxmark3 operations for every card type that Doppelgänger Core and Stealth support. This tool streamlines the card-writing process for physical penetration testing by providing an intuitive interface with dual output displays, visual separators, and one-click clipboard copying, eliminating the need to memorize complex Proxmark3 syntax or dig through notes. 4 | 5 | ![Doppelgänger Assistant GUI](https://github.com/tweathers-sec/doppelganger_assistant/blob/main/img/assistant_gui.png) 6 | 7 | ## Features 8 | 9 | * **Modern GUI Interface**: Intuitive two-column layout with dedicated status and command output displays 10 | * **Card Operations**: Automatically generates and executes Proxmark3 commands for writing, verifying, and simulating cards 11 | * **Direct Execution**: Execute write, verify, and simulate operations directly from the GUI with your Proxmark3 12 | * **Cross-Platform**: Available for macOS, Linux, and Windows (via WSL) 13 | * **Card Discovery**: Automatically detect card types and read card data 14 | * **Hotel Access Control**: Complete MIFARE card recovery and analysis tools 15 | * **Terminal Integration**: Launch Proxmark3 in a separate terminal window 16 | 17 | ## Changelog 18 | 19 | ### Version 1.1.2 (December 18, 2025) 20 | 21 | **New Features:** 22 | * Added LAUNCH PM3 button to open Proxmark3 in a separate terminal window 23 | * Added Card Discovery section with automatic card type detection 24 | * Added READ CARD DATA functionality for all supported card types 25 | * Added Hotel / Residence Access Control section with MIFARE tools 26 | * Added dual chip card detection for cards with both LF and HF capabilities 27 | * Added automatic card info retrieval when MIFARE cards are detected 28 | 29 | **Improvements:** 30 | * Improved card detection to show specific MIFARE types and magic capabilities 31 | * Enhanced card verification with clearer success messages 32 | * Better error handling and user feedback throughout the application 33 | * Improved iCLASS card reading with automatic decryption support 34 | 35 | ### Version 1.1.1 36 | 37 | * Initial release with core card writing, verification, and simulation features 38 | * Support for PROX, iCLASS, AWID, Indala, Avigilon, EM4100, PIV, and MIFARE cards 39 | 40 | ## Doppelgänger Devices 41 | 42 | You can purchase a Stealth reader, Doppelgänger Dev Board, or fully assembled long-range readers from the [Physical Exploitation Store](https://store.physicalexploit.com/). For the open-source firmware, check out [Doppelgänger Core](https://github.com/mwgroup-io/Doppelganger_Core). 43 | 44 | ## Supported Proxmark3 Devices 45 | 46 | Doppelgänger Assistant works with the [Iceman fork of Proxmark3](https://github.com/RfidResearchGroup/proxmark3) and supports the following device types: 47 | 48 | * **Proxmark3 RDV4** - With or without Blueshark Bluetooth addon 49 | * **Proxmark3 Easy (512KB)** - Generic platform support 50 | 51 | The automated installers will prompt you to select your device type during installation for optimal configuration. 52 | 53 | ## Officially Supported Card Types 54 | 55 | Below are the officially supported card types based on Doppelgänger firmware: 56 | 57 | | Card Types | Core | Stealth | FC Range | CN Range | 58 | | --------------------------- | ---- | ------- | ----------- | --------------- | 59 | | Keypad PIN Codes | | X | N/A | N/A | 60 | | HID H10301 26-bit | X | X | 0–255 | 0–65,535 | 61 | | Indala 26-bit | X | X | 0–255 | 0–65,535 | 62 | | Indala 27-bit | X | X | 0–4,095 | 0–8,191 | 63 | | 2804 WIEGAND 28-bit | X | X | 0–255 | 0–16,383 | 64 | | Indala 29-bit | X | X | 0–4,095 | 0–32,767 | 65 | | ATS Wiegand 30-bit | X | X | 0–2,047 | 0–32,767 | 66 | | HID ADT 31-Bit | X | X | 0–15 | 0–8,388,607 | 67 | | EM4102 / Wiegand 32-bit | X | X | 0–32,767 | 0–65,535 | 68 | | HID D10202 33-bit | X | X | 0–127 | 0–16,777,215 | 69 | | HID H10306 34-bit | X | X | 0–65,535 | 0–65,535 | 70 | | HID Corporate 1000 35-bit | X | X | 0–4,095 | 0–1,048,575 | 71 | | HID Simplex 36-bit (S12906) | X | X | 0–255 | 0–65,535 | 72 | | HID H10304 37-bit | X | X | 0–65,535 | 0–524,287 | 73 | | HID H800002 46-bit | X | X | 0–16,383 | 0–1,073,741,823 | 74 | | HID Corporate 1000 48-bit | X | X | 0–4,194,303 | 0–8,388,607 | 75 | | AWID 50-bit | X | X | 0–65,535 | 0–8,388,607 | 76 | | Avigilon 56-bit | X | X | 0–1,048,575 | 0–4,194,303 | 77 | | C910 PIVKey | X | X | N/A | N/A | 78 | | MIFARE (Various Types) | X | X | N/A | N/A | 79 | 80 | Supported technologies include: 81 | 82 | * iCLASS(Legacy/SE/Seos) *Note: Captured SE and Seos cards can only be written to iCLASS 2k cards* 83 | * PROX 84 | * Indala 85 | * AWID 86 | * Avigilon 87 | * EM4102 88 | * PIV (UID Only - As that is what is provided via the readers wiegand output) 89 | * MIFARE (UID Only - As that is what is provided via the readers wiegand output) 90 | 91 | ## Installation 92 | 93 | ### Quick Install (Recommended) 94 | 95 | The automated installers will install Doppelganger Assistant, required dependencies, and the latest [Iceman fork of Proxmark3](https://github.com/RfidResearchGroup/proxmark3). During installation, you'll be prompted to select your Proxmark3 device type for optimal configuration. 96 | 97 | #### macOS 98 | 99 | ```sh 100 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/tweathers-sec/doppelganger_assistant/main/installers/doppelganger_install_macos.sh)" 101 | ``` 102 | 103 | #### Linux 104 | 105 | ```bash 106 | curl -sSL https://raw.githubusercontent.com/tweathers-sec/doppelganger_assistant/main/installers/doppelganger_install_linux.sh | sudo bash 107 | ``` 108 | 109 | #### Windows (WSL) 110 | 111 | Open **PowerShell as Administrator** and run: 112 | 113 | ```powershell 114 | irm https://raw.githubusercontent.com/tweathers-sec/doppelganger_assistant/main/installers/doppelganger_install_windows.ps1 | iex 115 | ``` 116 | 117 | **Note:** The installer will prompt you to select between **Kali Linux 2025.3** (recommended) or **Ubuntu 24.04 LTS (Noble)**. It will automatically detect existing installations and prompt you to update. If a reboot is required to enable WSL features, you'll be prompted. Simply run the same command again after rebooting. 118 | 119 | ⚠️ IMPORTANT: WSL2 does not support running inside nested virtual machines. The installer automatically detects and prevents installation in such environments. To proceed, install on physical hardware. 120 | 121 | --- 122 | 123 | ### Detailed Installation Instructions 124 | 125 | #### Manual MacOS Installation 126 | 127 | 1. Download the appropriate DMG file for your architecture from the [release page](https://github.com/tweathers-sec/doppelganger_assistant/releases): 128 | - Apple Silicon (M1/M2/M3): `doppelganger_assistant_darwin_arm64.dmg` 129 | - Intel: `doppelganger_assistant_darwin_amd64.dmg` 130 | 131 | 2. Open the DMG file and drag `doppelganger_assistant.app` to your `/Applications` folder 132 | 133 | 3. Remove quarantine attributes: 134 | ```sh 135 | xattr -cr /Applications/doppelganger_assistant.app 136 | ``` 137 | 138 | 4. Create a command-line alias by adding to your shell profile (`~/.zshrc` or `~/.zprofile`): 139 | ```sh 140 | alias doppelganger_assistant='/Applications/doppelganger_assistant.app/Contents/MacOS/doppelganger_assistant' 141 | ``` 142 | 143 | 5. Reload your shell: 144 | ```sh 145 | source ~/.zshrc 146 | ``` 147 | 148 | #### Manual Linux Installation 149 | 150 | **Option 1: Using .deb package (Debian/Ubuntu/Kali - Recommended)** 151 | 152 | 1. Install dependencies: 153 | ```sh 154 | sudo apt update && sudo apt upgrade -y 155 | sudo apt install libgl1 xterm wget -y 156 | ``` 157 | 158 | 2. Download and install the .deb package for your architecture: 159 | ```sh 160 | # For x86_64/amd64 systems: 161 | wget https://github.com/tweathers-sec/doppelganger_assistant/releases/latest/download/doppelganger_assistant_linux_amd64.deb 162 | sudo dpkg -i doppelganger_assistant_linux_amd64.deb 163 | 164 | # For ARM64 systems: 165 | wget https://github.com/tweathers-sec/doppelganger_assistant/releases/latest/download/doppelganger_assistant_linux_arm64.deb 166 | sudo dpkg -i doppelganger_assistant_linux_arm64.deb 167 | ``` 168 | 169 | 3. Fix any missing dependencies (if needed): 170 | ```sh 171 | sudo apt-get install -f -y 172 | ``` 173 | 174 | **Option 2: Using tar.xz archive (Other distributions)** 175 | 176 | 1. Install dependencies: 177 | ```sh 178 | sudo apt update && sudo apt upgrade -y 179 | sudo apt install libgl1 xterm make git wget -y 180 | ``` 181 | 182 | 2. Download and install for your architecture: 183 | ```sh 184 | # For x86_64/amd64 systems: 185 | wget https://github.com/tweathers-sec/doppelganger_assistant/releases/latest/download/doppelganger_assistant_linux_amd64.tar.xz 186 | 187 | # For ARM64 systems: 188 | wget https://github.com/tweathers-sec/doppelganger_assistant/releases/latest/download/doppelganger_assistant_linux_arm64.tar.xz 189 | 190 | # Extract and install: 191 | tar xvf doppelganger_assistant_linux_*.tar.xz 192 | cd doppelganger_assistant 193 | sudo make install 194 | cd .. 195 | rm -rf doppelganger_assistant 196 | ``` 197 | 198 | To launch the Doppelganger Assistant GUI: 199 | 200 | ```sh 201 | doppelganger_assistant 202 | ``` 203 | 204 | #### Manual Windows (WSL) Installation 205 | 206 | 1. **Enable WSL and install a Linux distribution**. Open PowerShell as Administrator: 207 | ```powershell 208 | # For Kali Linux (recommended): 209 | wsl --install -d kali-linux 210 | 211 | # OR for Ubuntu 24.04 LTS: 212 | wsl --install -d Ubuntu-24.04 213 | ``` 214 | 215 | 2. **Reboot Windows**. After reboot, WSL will finish setup. When prompted, create a username and password for your Linux environment. 216 | 217 | 3. **Install Doppelganger Assistant** (inside WSL): 218 | ```sh 219 | sudo apt update && sudo apt upgrade -y 220 | sudo apt install libgl1 xterm wget -y 221 | 222 | # For x86_64/amd64 systems: 223 | wget https://github.com/tweathers-sec/doppelganger_assistant/releases/latest/download/doppelganger_assistant_linux_amd64.deb 224 | sudo dpkg -i doppelganger_assistant_linux_amd64.deb 225 | 226 | # For ARM64 systems: 227 | wget https://github.com/tweathers-sec/doppelganger_assistant/releases/latest/download/doppelganger_assistant_linux_arm64.deb 228 | sudo dpkg -i doppelganger_assistant_linux_arm64.deb 229 | ``` 230 | 231 | 4. **Install Proxmark3 dependencies**: 232 | ```sh 233 | sudo apt install --no-install-recommends -y git ca-certificates build-essential pkg-config \ 234 | libreadline-dev gcc-arm-none-eabi libnewlib-dev qtbase5-dev \ 235 | libbz2-dev liblz4-dev libbluetooth-dev libpython3-dev libssl-dev libgd-dev 236 | ``` 237 | 238 | 5. **Clone and build Proxmark3**: 239 | ```sh 240 | mkdir -p ~/src && cd ~/src 241 | git clone https://github.com/RfidResearchGroup/proxmark3.git 242 | cd proxmark3 243 | 244 | # Configure for your Proxmark3 device type: 245 | cp Makefile.platform.sample Makefile.platform 246 | 247 | # Edit Makefile.platform for your device: 248 | # - For RDV4 with Blueshark: Uncomment PLATFORM=PM3RDV4 and PLATFORM_EXTRAS=BTADDON 249 | # - For RDV4 without Blueshark: Uncomment PLATFORM=PM3RDV4 only 250 | # - For Proxmark3 Easy (512KB): Uncomment PLATFORM=PM3GENERIC and PLATFORM_SIZE=512 251 | nano Makefile.platform 252 | 253 | # Build and install 254 | make clean && make -j$(nproc) 255 | sudo make install PREFIX=/usr/local 256 | ``` 257 | 258 | 6. **Install USBipd** (from Windows PowerShell as Administrator): 259 | ```powershell 260 | winget install --interactive --exact dorssel.usbipd-win 261 | ``` 262 | 263 | 7. **Connect Proxmark3 to WSL** (from Windows cmd.exe or PowerShell as Administrator): 264 | ```powershell 265 | usbipd list # List USB devices - find your Proxmark3 (VID: 9ac4) 266 | usbipd bind --busid 9-1 # Replace 9-1 with your Proxmark3's busid 267 | usbipd attach --wsl --busid 9-1 # Attach to WSL 268 | ``` 269 | 270 | To launch Doppelganger Assistant: 271 | ```sh 272 | doppelganger_assistant 273 | ``` 274 | 275 | ## Uninstallation 276 | 277 | ### Uninstalling from Windows (WSL) 278 | 279 | #### Automated Uninstall (Recommended) 280 | 281 | Download and run the uninstaller directly from GitHub: 282 | 283 | ```powershell 284 | powershell -ExecutionPolicy Bypass -Command "irm https://raw.githubusercontent.com/tweathers-sec/doppelganger_assistant/main/scripts/uninstall.ps1 | iex" 285 | ``` 286 | 287 | #### Manual Uninstall 288 | 289 | If you have Doppelganger Assistant installed, run the uninstall script: 290 | 291 | ```powershell 292 | powershell -ExecutionPolicy Bypass -File C:\doppelganger_assistant\uninstall.ps1 293 | ``` 294 | 295 | The uninstaller will: 296 | - Stop and unregister WSL distributions (Kali-doppelganger_assistant or Ubuntu-doppelganger_assistant) 297 | - Remove all files from `C:\doppelganger_assistant` 298 | - Delete the desktop shortcut 299 | - Uninstall usbipd (it will be reinstalled if you run the installer again) 300 | 301 | **Note:** The script automatically relocates itself to a temporary directory to ensure clean removal of the installation directory. 302 | 303 | ### Uninstalling from macOS 304 | 305 | To uninstall from macOS: 306 | 307 | ```bash 308 | sudo rm -rf /Applications/doppelganger_assistant.app 309 | sudo rm /usr/local/bin/doppelganger_assistant 310 | ``` 311 | 312 | ### Uninstalling from Linux 313 | 314 | **If installed via .deb package:** 315 | 316 | ```bash 317 | sudo apt remove doppelganger-assistant 318 | ``` 319 | 320 | **If installed via tar.xz archive:** 321 | 322 | ```bash 323 | sudo rm /usr/local/bin/doppelganger_assistant 324 | rm -f ~/.local/share/applications/doppelganger_assistant.desktop 325 | rm -f ~/Desktop/doppelganger_assistant.desktop 326 | sudo rm /usr/share/pixmaps/doppelganger_assistant.png 327 | ``` 328 | 329 | ## Development 330 | 331 | ### Building from Source 332 | 333 | To build Doppelganger Assistant from source: 334 | 335 | ```bash 336 | # Clone the repository 337 | git clone https://github.com/tweathers-sec/doppelganger_assistant.git 338 | cd doppelganger_assistant 339 | 340 | # Build for current platform 341 | ./build.sh 342 | 343 | # Build for all platforms (requires Docker) 344 | ./build_all.sh 345 | ``` 346 | 347 | ### Project Structure 348 | 349 | - **`src/`** - Contains all Go source code and module files 350 | - **`installers/`** - Platform-specific installation scripts 351 | - **`scripts/`** - Utility scripts for WSL and Windows setup 352 | - **`build.sh`** - Single-platform build script 353 | - **`build_all.sh`** - Multi-platform build script using fyne-cross 354 | 355 | ### Contributing 356 | 357 | 1. Fork the repository 358 | 2. Create a feature branch 359 | 3. Make your changes in the `src/` directory 360 | 4. Test your changes with `./build.sh` 361 | 5. Submit a pull request 362 | 363 | ## Usage 364 | 365 | ### Launching the GUI 366 | 367 | The Doppelgänger Assistant GUI launches by default when you run the application: 368 | 369 | ```sh 370 | doppelganger_assistant 371 | ``` 372 | 373 | Or explicitly launch in GUI mode: 374 | 375 | ```sh 376 | doppelganger_assistant -g 377 | ``` 378 | 379 | ### GUI Workflow 380 | 381 | 1. **Select Card Type**: Choose from PROX, iCLASS, AWID, Indala, Avigilon, EM4100, PIV, or MIFARE 382 | 2. **Choose Bit Length**: Select the appropriate bit length for your card type (automatically filtered based on card type) 383 | 3. **Enter Card Data**: Input facility code, card number, hex data, or UID as required for the selected card type 384 | 4. **Choose Action**: 385 | - **Generate Only**: Displays Proxmark3 commands for manual execution without writing 386 | - **Write & Verify**: Writes card data to a blank card and verifies the result 387 | - **Simulate Card**: Simulates the card using your Proxmark3 device 388 | 5. **Execute**: Click the EXECUTE button to run the operation 389 | 6. **Monitor Output**: 390 | - **Status Output** (top panel): Color-coded status messages with operation progress 391 | - **Command Output** (bottom panel): Detailed Proxmark3 command results with visual separators 392 | 7. **Copy Results**: Click COPY OUTPUT to copy all command results to your clipboard 393 | 8. **Reset**: Click RESET to clear all fields and start over 394 | 395 | The dual output panels provide clear separation between status updates and command results, with visual spacers (e.g., `|----------- WRITE #1 -----------|`) making it easy to distinguish between multiple write attempts or verification steps. 396 | 397 | ### Video Demo 398 | 399 | Below is a quick video demo of the usage: 400 | 401 | [![Doppelgänger Assistant Demo](https://img.youtube.com/vi/RfWKgS-U8ws/0.jpg)](https://youtu.be/RfWKgS-U8ws) 402 | 403 | ### Commandline Usage Examples 404 | 405 | #### Generating commands for writing iCLASS cards 406 | 407 | This command will generate the command needed to encode iCLASS (Legacy/SE/Seos) card data to an iCLASS 2k card: 408 | 409 | ```sh 410 | doppelganger_assistant -t iclass -bl 26 -fc 123 -cn 4567 411 | 412 | [>>] Writing to iCLASS 2k card... 413 | [>>] Command: hf iclass encode -w H10301 --fc 123 --cn 4567 --ki 0 414 | ``` 415 | 416 | #### Writing iCLASS card data with verification 417 | 418 | By adding the `-w` (write) and `-v` (verify) flags, the application will use your Proxmark3 to write and verify the card data: 419 | 420 | ```sh 421 | doppelganger_assistant -t iclass -bl 26 -fc 123 -cn 4567 -w -v 422 | 423 | |----------- WRITE -----------| 424 | [..] Encoding iCLASS card data... 425 | [>>] Command: hf iclass encode -w H10301 --fc 123 --cn 4567 --ki 0 426 | 427 | [=] Session log /Users/user/.proxmark3/logs/log_20250105120000.txt 428 | [+] loaded `/Users/user/.proxmark3/preferences.json` 429 | [+] execute command from commandline: hf iclass encode -w H10301 --fc 123 --cn 4567 --ki 0 430 | [+] Using UART port /dev/tty.usbmodem101 431 | [+] Communicating with PM3 over USB-CDC 432 | [+] Encoding successful 433 | 434 | [OK] Write complete - starting verification 435 | 436 | |----------- VERIFICATION -----------| 437 | [..] Verifying card data - place card flat on reader... 438 | 439 | [=] Session log /Users/user/.proxmark3/logs/log_20250105120005.txt 440 | [+] execute command from commandline: hf iclass rdbl --blk 7 --ki 0 441 | [+] Using key[0] AE A6 84 A6 DA B2 32 78 442 | [+] block 7/0x07 : 00 00 00 00 06 F6 23 AE 443 | 444 | [OK] Verification successful - Block 7: 00 00 00 00 06 F6 23 AE 445 | [OK] Card contains: 26-bit, FC: 123, CN: 4567 446 | ``` 447 | 448 | #### Simulating PIV/MF Cards 449 | 450 | Using the UID provided by Doppelgänger (Core and Stealth), you can simulate the exact wiegand signal with a Proxmark3. 451 | 452 | ```sh 453 | doppelganger_assistant -uid 5AF70D9D -s -t piv 454 | 455 | Handling PIV card... 456 | 457 | Simulating the PIV card on your Proxmark3: 458 | 459 | Executing command: hf 14a sim -t 3 --uid 5AF70D9D 460 | 461 | [=] Session log /Users/tweathers/.proxmark3/logs/log_20240614152754.txt 462 | [+] loaded `/Users/tweathers/.proxmark3/preferences.json` 463 | [+] execute command from commandline: hf 14a sim -t 3 --uid 5AF70D9D 464 | 465 | [+] Using UART port /dev/tty.usbmodem2134301 466 | 467 | ``` 468 | 469 | #### Writing HID Prox cards with multiple attempts 470 | 471 | LF cards (Prox, AWID, Indala, Avigilon, EM) automatically perform 5 write attempts with visual separators: 472 | 473 | ```sh 474 | doppelganger_assistant -t prox -bl 46 -fc 123 -cn 4567 -w 475 | 476 | [..] Writing Prox card (5 attempts)... 477 | 478 | |----------- WRITE #1 -----------| 479 | [+] Using UART port /dev/tty.usbmodem101 480 | [+] Communicating with PM3 over USB-CDC 481 | [+] Wrote block successfully 482 | [..] Move card slowly... Write attempt #1 complete 483 | 484 | |----------- WRITE #2 -----------| 485 | [+] Using UART port /dev/tty.usbmodem101 486 | [+] Communicating with PM3 over USB-CDC 487 | [+] Wrote block successfully 488 | [..] Move card slowly... Write attempt #2 complete 489 | 490 | |----------- WRITE #3 -----------| 491 | ... (continues for all 5 attempts) 492 | 493 | [OK] All 5 write attempts complete 494 | ``` 495 | 496 | #### Writing Avigilon 56-bit cards 497 | 498 | Avigilon cards are written with 5 attempts like other LF cards: 499 | 500 | ```sh 501 | doppelganger_assistant -t avigilon -bl 56 -fc 118 -cn 1603 -w 502 | 503 | [..] Writing Avigilon card (5 attempts)... 504 | 505 | |----------- WRITE #1 -----------| 506 | [+] Using UART port /dev/tty.usbmodem101 507 | [+] Communicating with PM3 over USB-CDC 508 | lf hid clone -w Avig56 --fc 118 --cn 1603 509 | [..] Move card slowly... Write attempt #1 complete 510 | 511 | ... (continues for all 5 attempts) 512 | ``` 513 | 514 | #### Simulating Avigilon cards 515 | 516 | Using the Avigilon card data, you can simulate the exact signal with a Proxmark3: 517 | 518 | ```sh 519 | doppelganger_assistant -t avigilon -bl 56 -fc 118 -cn 1603 -s 520 | 521 | Simulating the Avigilon card on your Proxmark3: 522 | 523 | Executing command: lf hid sim -w Avig56 --fc 118 --cn 1603 524 | 525 | Simulation is in progress... If your Proxmark3 has a battery, you can remove the device and the simulation will continue. 526 | 527 | To end the simulation, press the `pm3 button`. 528 | ``` 529 | 530 | ## Legal Notice 531 | 532 | This application is intended for professional penetration testing and authorized security assessments only. Unauthorized or illegal use/possession of this software is the sole responsibility of the user. Mayweather Group LLC, Practical Physical Exploitation, and the creator are not liable for illegal application of this software. 533 | 534 | ## License 535 | 536 | [![License: CC BY-NC-ND 4.0](https://img.shields.io/badge/License-CC%20BY--NC--ND%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-nc-nd/4.0/) 537 | 538 | This work is licensed under a [Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License](https://creativecommons.org/licenses/by-nc-nd/4.0/). 539 | 540 | **You are free to:** 541 | * **Share** — copy and redistribute the material in any medium or format 542 | 543 | **Under the following terms:** 544 | * **Attribution** — You must give appropriate credit, provide a link to the license, and indicate if changes were made. 545 | * **NonCommercial** — You may not use the material for commercial purposes. 546 | * **NoDerivatives** — If you remix, transform, or build upon the material, you may not distribute the modified material. 547 | 548 | See the [LICENSE](LICENSE) file for the full license text. 549 | 550 | ## Support 551 | 552 | For professional support and documentation, visit: 553 | * [Practical Physical Exploitation Documentation](https://docs.physicalexploit.com/) 554 | * [GitHub Issues](https://github.com/tweathers-sec/doppelganger_assistant/issues) 555 | * [Professional Store](https://store.physicalexploit.com/) 556 | 557 | ## Credits 558 | 559 | Developed by Travis Weathers ([@tweathers-sec](https://github.com/tweathers-sec)) 560 | Copyright © 2025 Mayweather Group, LLC 561 | 562 | --- 563 | 564 | *This software works in conjunction with [Doppelgänger Core](https://github.com/mwgroup-io/Doppelganger_Core) and Doppelgänger hardware devices.* 565 | --------------------------------------------------------------------------------