Skip to content

RC4 Encryption

TL;DR

See the code example

RC4 is a simple stream cipher that remains popular in malicious code because of its small footprint and ease of implementation. In Windows, the undocumented functions SystemFunction032 and SystemFunction033 can perform RC4 encryption. The sample code demonstrates encrypting a payload with one call and decrypting it with another since RC4 is symmetric. Keeping shellcode encrypted until execution helps avoid detection by static scanners that search for known byte patterns.

Using SystemFunction032

main.zig
const std = @import("std");
const win = std.os.windows;
const kernel32 = win.kernel32;

const DWORD = u32;
const NTSTATUS = i32;
const PVOID = ?*anyopaque; // equivalent to C's void*
const BOOL = i32;

const USTRING = extern struct {
    Length: DWORD,
    MaximumLength: DWORD,
    Buffer: PVOID,
};

const fnSystemFunction032 = fn (
    Data: *USTRING,
    Key: *USTRING,
) callconv(.C) NTSTATUS;

/// Helper function that calls SystemFunction032 (RC4)
/// Reference: https://osandamalith.com/2022/11/10/encrypting-shellcode-using-systemfunction032-033/
pub fn rc4EncryptionViaSystemFunc032(
    rc4Key: []u8,
    payloadData: []u8,
) bool {
    // Prepare the USTRING structs
    var Data = USTRING{
        .Buffer = payloadData.ptr,
        .Length = @intCast(payloadData.len),
        .MaximumLength = @intCast(payloadData.len),
    };
    var Key = USTRING{
        .Buffer = rc4Key.ptr,
        .Length = @intCast(rc4Key.len),
        .MaximumLength = @intCast(rc4Key.len),
    };

    // Convert "Advapi32" to UTF-16LE for LoadLibraryW
    const advapi32_w = std.unicode.utf8ToUtf16LeStringLiteral("Advapi32");
    const advapi32 = kernel32.LoadLibraryW(advapi32_w);
    if (advapi32 == null) {
        std.debug.print("[!] LoadLibraryW failed: {}\n", .{kernel32.GetLastError()});
        return false;
    }
    defer _ = kernel32.FreeLibrary(advapi32.?);

    const proc_addr = kernel32.GetProcAddress(advapi32.?, "SystemFunction032");
    if (proc_addr == null) {
        std.debug.print("[!] GetProcAddress failed: {}\n", .{kernel32.GetLastError()});
        return false;
    }

    const SystemFunction032: *const fnSystemFunction032 = @ptrCast(proc_addr);

    const status: NTSTATUS = SystemFunction032(&Data, &Key);

    if (status != 0) {
        std.debug.print("[!] SystemFunction032 FAILED With Error: 0x{X:0>8}\n", .{status});
        return false;
    }
    return true;
}

pub fn main() !void {
    const stdout = std.io.getStdOut().writer();

    // Example RC4 key and payload
    var key = [_]u8{ 0x11, 0x22, 0x33, 0x44, 0x55 };
    var data = [_]u8{ 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED, 0xFA, 0xCE };

    try stdout.print("[+] Original payload: {any}\n", .{data});
    try stdout.print("[+] RC4 key: {any}\n", .{key});

    // Encrypt (in-place)
    if (!rc4EncryptionViaSystemFunc032(key[0..], data[0..])) {
        try stdout.print("[+] Encryption failed!\n", .{});
        return;
    }
    try stdout.print("[+] Encrypted payload: {any}\n", .{data});

    // Decrypt (RC4 is symmetric, so call again with same key)
    if (!rc4EncryptionViaSystemFunc032(key[0..], data[0..])) {
        try stdout.print("[+] Decryption failed!\n", .{});
        return;
    }
    try stdout.print("[+] Decrypted payload: {any}\n", .{data});
}

Using SystemFunction033

main.zig
const std = @import("std");
const win = std.os.windows;
const kernel32 = win.kernel32;

const DWORD = u32;
const NTSTATUS = i32;
const PVOID = ?*anyopaque; // equivalent to C's void*
const BOOL = i32;

const USTRING = extern struct {
    Length: DWORD,
    MaximumLength: DWORD,
    Buffer: PVOID,
};

const fnSystemFunction033 = fn (
    Data: *USTRING,
    Key: *USTRING,
) callconv(.C) NTSTATUS;

/// Helper function that calls SystemFunction033 (RC4)
/// Reference: https://osandamalith.com/2022/11/10/encrypting-shellcode-using-systemfunction032-033/
pub fn rc4EncryptionViaSystemFunc033(
    rc4Key: []u8,
    payloadData: []u8,
) bool {
    // Prepare the USTRING structs
    var Data = USTRING{
        .Buffer = payloadData.ptr,
        .Length = @intCast(payloadData.len),
        .MaximumLength = @intCast(payloadData.len),
    };
    var Key = USTRING{
        .Buffer = rc4Key.ptr,
        .Length = @intCast(rc4Key.len),
        .MaximumLength = @intCast(rc4Key.len),
    };

    // Convert "Advapi32" to UTF-16LE for LoadLibraryW
    const advapi32_w = std.unicode.utf8ToUtf16LeStringLiteral("Advapi32");
    const advapi32 = kernel32.LoadLibraryW(advapi32_w);
    if (advapi32 == null) {
        std.debug.print("[!] LoadLibraryW failed: {}\n", .{kernel32.GetLastError()});
        return false;
    }
    defer _ = kernel32.FreeLibrary(advapi32.?);

    const proc_addr = kernel32.GetProcAddress(advapi32.?, "SystemFunction033");
    if (proc_addr == null) {
        std.debug.print("[!] GetProcAddress failed: {}\n", .{kernel32.GetLastError()});
        return false;
    }

    const SystemFunction033: *const fnSystemFunction033 = @ptrCast(proc_addr);

    const status: NTSTATUS = SystemFunction033(&Data, &Key);

    if (status != 0) {
        std.debug.print("[!] SystemFunction033 FAILED With Error: 0x{X:0>8}\n", .{status});
        return false;
    }
    return true;
}

pub fn main() !void {
    const stdout = std.io.getStdOut().writer();

    // Example RC4 key and payload
    var key = [_]u8{ 0x11, 0x22, 0x33, 0x44, 0x55 };
    var data = [_]u8{ 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED, 0xFA, 0xCE };

    try stdout.print("[+] Original payload: {any}\n", .{data});
    try stdout.print("[+] RC4 key: {any}\n", .{key});

    // Encrypt (in-place)
    if (!rc4EncryptionViaSystemFunc033(key[0..], data[0..])) {
        try stdout.print("[+] Encryption failed!\n", .{});
        return;
    }
    try stdout.print("[+] Encrypted payload: {any}\n", .{data});

    // Decrypt (RC4 is symmetric, so call again with same key)
    if (!rc4EncryptionViaSystemFunc033(key[0..], data[0..])) {
        try stdout.print("[+] Decryption failed!\n", .{});
        return;
    }
    try stdout.print("[+] Decrypted payload: {any}\n", .{data});
}