Reforging Sliver: How Simple Code Edits Can Outmaneuver EDR

In today’s Red Team landscape, security practitioners face a key decision: write entirely new tooling from scratch or adapt existing open source solutions. While building from the ground up can offer customization and stealth, it demands considerable time and expertise. On the other hand, taking a solid framework like Sliver and tailoring it to bypass defenses-particularly static YARA signatures-can strike a practical balance between agility and effectiveness. In this post, we’ll explore how minor code modifications can significantly boost evasion and help sliver bypass EDR, highlighting both the benefits and pitfalls of standing on open source shoulders versus forging your own.

sliver console

Sliver?

Sliver is a robust, multi-platform Command & Control framework for offensive security written entirely in Go. Since its release in 2020, it has gained a large user base. For years, newly generated payloads could go undetected nine times out of ten when dropped on disk. This was due to the large implant size and obfuscated symbols. However, as more users adopted the framework and researchers created static signatures, security tools now detect out-of-the-box implants much faster.

Pain Points

Sliver’s main drawbacks are its large binary sizes (up to 30 MB!) and the lack of a sleep mask. This makes it tough to hide in memory during idle moments. One option is a staged payload that downloads an implant from the web, then loads it into the same process or injects it into a remote process. This prevents the implant from touching disk and triggering immediate scans. Another option is embedding an encrypted implant in a loader, which extracts, decrypts, and loads it at runtime via self-injection or process hollowing. I tested all of these techniques against Elastic EDR and Windows Defender, but-without revealing too much-they proved ineffective.

The EDR

My task was to explore and map the detection surface of Sliver to enable its use in active red team engagements against modern defenses. I chose to use Elastic as it has a free trial period and open source yara rules.

The first step was to generate implants and scan them against all available rules using a simple batch script.

@echo off 
setlocal 

REM Set the paths 

set YARA_EXE=C:\Tools\protection-artifacts\yara64.exe 
set RULES_DIR=C:\Tools\protection-artifacts\rules 
set TARGET_FILE=%~1 

REM Check if YARA executable exists 

if not exist "%YARA_EXE%" ( 
    echo YARA executable not found at %YARA_EXE% 
    exit /b 1 ) 

REM Check if rules directory exists 

if not exist "%RULES_DIR%" ( 
    echo Rules directory not found at %RULES_DIR% 
    exit /b 1 ) 

echo Scanning %TARGET_FILE% 

REM Iterate over each .yar file in the rules directory 

for /r "%RULES_DIR%" %%f in (*.yar) do ( 
    "%YARA_EXE%" -s "%%f" "%TARGET_FILE%" 
) 

endlocal 
pause

This initial testing generated a multitude of positive signatures against the implant, for example:

yara detections
Positive YARA detections

Basic Signature Replacement

Sliver relies on protobuf data definitions to generate Go source code that serializes data structures. This solution allows the client, server, and implant to share source code and utilise an RPC mechanism for network communication. Most positive signatures appear in the .proto files. For example, the string ScreenshotReq is defined in sliver.proto:

message ScreenshotReq {
    commonpb.Request Request = 9;
}

Protobuf then generates a sliver.pb.go file with the following data structure:

type ScreenshotReq struct {
state protoimpl.MessageState protogen:"open.v1"
Request *commonpb.Request protobuf:"bytes,9,opt,name=Request,proto3" json:"Request,omitempty"
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}

Changing the message name in the sliver.proto file updates the type name in the sliver.pb.go file. After regenerating I search the Sliver code base for those structures and update them to match. Once all edits are complete, I compile the Sliver server and client applications to check if everything still works. To automate this, I modified the Sliver devs’ bash script from https://sliver.sh/install. I added a basic string replacement routine using the Linux core utility sed.

FILE_TO_EDIT='/root/sliver-repo/protobuf/sliverpb/sliver.proto'

if [ ! -f "$FILE_TO_EDIT" ]; then
echo "The file $FILE_TO_EDIT does not exist!"
exit 1
fi

SEARCH_FOR='ScreenshotReq'
REPLACE_WITH='ScShotReq'

sed -i "s|$SEARCH_FOR|$REPLACE_WITH|g" "$FILE_TO_EDIT"

echo "sliver.proto File edited successfully."

After a lot of trial and error the generated payloads no longer contained the string signatures.

The next yara rule to address is Multi_Trojan_Sliver_3d6b7cd3 rule which targets the Sliver initialisation routine. The solution I decided upon was to set a local constant MaxConnectionErrors variable rather than call transports.GetMaxConnectionErrors().

beacons := transports.StartBeaconLoop(abort)
const maxConnectionErrors = 1000
for beacon := range beacons {
// {{if .Config.Debug}}
log.Printf("Next beacon = %v", beacon)
// {{end}}
if beacon != nil {
err := beaconMainLoop(beacon)
if err != nil {
connectionErrors++
if connectionErrors > maxConnectionErrors {
return
}
}
}

This change bypassed the Multi_Trojan_Sliver_3d6b7cd3 rule.

Shellcode Loader & API Hooking

Even though the implants passed the Elastic YARA rules scan, they were still detected when written to disk. I considered self-injecting implant shellcode since the generated code should no longer contain signatures. I developed a packer to encode the implant and embed it in the loaders resource section. The packer maps the shellcode into the process address space and executes it using various techniques.

The next issue I came up against was slivers shellcode generation solution. Sliver leverages Donut to generate shellcode as a .bin file, which it allocates in Read/Write/Execute memory for execution. By default, Sliver enables Donuts AMSI bypass, which has heavy signature detection. In order to disable the bypass we need to change the source code in the donut.go file here and here changing the bypass from 3 to 1.

disable AMSI bypass
Donut config change

The next detection became a deal breaker. When sliver initializes an http beacon it loads the libraries using the Go type LazyDLL which calls the windows API LoadLibraryExW under the hood.

network library loading
Go Windows library loading

This results in an Elastic behavioral detection of Network Library Loaded from Unbacked Memory. The normal solution to this alert would be to load a sacrificial library and overwrite its text section with the implant shellcode, thereby storing the implant in backed memory. However due to the size of sliver binaries I couldn’t find any candidates to attempt module stomping.

Next I tried loading the libraries before executing the shell code but NewLazyDLL() doesn’t check that the libraries are loaded before calling LoadLibraryExW. I then tried hooking calls to the API via hardware breakpoints to pass to a custom GetModuleHandle implementation, however this did not bypass the alert.

No Bypass? Back to the Drawing Board

With little success attempting to manually load the sliver implant into memory I realized that writing an implant dynamic library to disk was the simplest solution. So why does the implant get detected as soon as it lands on disk? I hypothesized that Windows Defender’s internal YARA rules triggered the detections. Unfortunately, because Defender’s rules aren’t open source, I couldn’t determine which bad bytes caused the detection. I spent a while searching for extracted rules online and stumbled across this repository https://github.com/roadwy/DefenderYara. I sorted through the rules and found all the .yar files related to sliver or go based malware. Again I used the previous batch script to scan the implants on disk and in memory resulting in more positive detections.

defender yara detections
Defender YARA detections

Searching for those byte patterns in a disassembler I found the detection culprits. They were all ASCII character strings in byte format.

  • 0x16d8889:$a_01_0 = “DllUnregisterServer”
  • 0x16d8877:$a_01_1 = “DllRegisterServer”
  • 0x16d886c:$a_01_2 = “DllInstall”
  • 0x5975f7:$a_01_3 = “GetJitter”
  • 0x16d88a4:$a_01_4 = “VoidFunc”
  • 0x594873:$a_01_5 = “GetKill”
  • 0x5974ef:$a_01_6 = “AddTunnel”
  • 0x595db5:$a_01_7 = “GetIsIOC”
  • 0xb66a02:$a_01_8 = ” Go buildinf:”

Three of the strings represent exported functions that execute a Sliver dynamic library using the LOLBin regsvr32.exe. The VoidFunc string exports another function that enables Sliver to work with the PowerSploit Invoke-ReflectivePEInjection.ps1 script.

sliver exported functions
Exported functions

More String Replacement

Since these execution methods weren’t part of the operational playbook, I removed them from the source by adding another sed command to the automation bash script. I also decided to modify the standard exported function StartW that wraps around the main sliver implant entry point. This export needs to be modified in the sliver.go and sliver.c files:

sliver PE entrypoint
Sliver.c code for building shared library
library exported function
Original shared library export
edit entrypoint and remove unused exported functions
Bash rename Shared Library entry and removal of unused exports

So that left four strings to hunt down and modify. I’ll concentrate on “GetJitter” as an example. The GetJitter() method is found in sliver.pb.go that returns a members value from the BeaconRegister structure defined in sliver.proto.

detected string
Original protobuf generated method
detected string structure member
Auto generated protobuf structure
protobuf definition
BeaconRegister protobuf definition

First, I renamed the Jitter member in the sliver.proto file and regenerated the protobuf files by running make pb in the root sliver folder, which automatically updated the method name to match.

modified method
Structure member automatically renamed

Then, I searched the code base for all instances where the member was accessed or the method was called, such as in the client info.go file:

modified string
Renamed structure member

And this example in the implant transports.go file:

modified method
Renamed Sliver function

Final Boss

With all of the changes made and added to the build script I carried out a final scan against the implant. Et Voila!

zero detections
No detections from scan

With static detections taken care of I decided to use a sandbox for dynamic analysis. LitterBox is an excellent solution for checking payloads across various scanning tools, for both static and dynamic analysis. There was one final YARA detection that only appears whilst the implant is running.

Final YARA detection during runtime

The solution to this detection was to add a function to the bash script that searches for all instances of bishopfox and replaces them with a string of random characters.

Bash function to rename across a project

Watch Sliver Bypass EDR

The final piece of the puzzle is a simple program that just loads the implant dynamic library from the same folder, finds the entry point export and runs it in a new thread.

basic library loader
Basic Library Loader code

A final analysis with LitterBox comes out clean.

Scan results from LitterBox

And we get a callback on a system with an elastic agent running, demonstrating that sliver bypass EDR:

See Our Leading Research Insights

For those interested in exploring more security research similar to our study, consider these insightful articles:

Conclusion

Obviously many commands built into the sliver implant will trigger Elastic behavioral alerts, but that’s for another day. This post has demonstrated that basic source code modification can, at least, allow heavily signatured open source projects to execute on disk.

Similar projects include better-sliver which is a fork of sliver with most of the above modifications already implemented. Also slivercloak which utilises a Go project to modify the source code and compile silver in a docker container. Recommended reading would be this Rasta Mouse post that explains hunting for bad bytes with ThreatCheck and Ghidra for simple signature removal. The strategy for these modifications are included in the Certified Red Team Operator courses provided by ZeroPoint Security. See our reviews for CRTO and CRTOII.

About Post Author