How to do direct syscalls with more than 4 params on Windows

I got NtReadVirtualMemory to work

pub fn NtReadVirtualMemory(

    process_handle: Win.HANDLE,

    base_address: ?*anyopaque,

    buffer: *anyopaque,

    number_of_bytes_to_read: usize,

    number_of_bytes_read: ?*Win.SIZE_T,

) callconv(.winapi) u32 {

    return asm volatile (

        \\ sub $0x38, %%rsp         // Allocate 56 bytes (shadow + args + alignment)

        \\ mov %[a5], %%rax         // Load pointer into rax

        \\ mov %%rax, 0x28(%%rsp)   // Store at correct offset (shadow + 8)

        \\ mov %%rcx, %%r10

        \\ mov $0x3f, %%eax

        \\ syscall

        \\ add $0x38, %%rsp

        : [ret] "={rax}" (-> u32),

        : [a1] "{rcx}" (process_handle),

          [a2] "{rdx}" (base_address),

          [a3] "{r8}" (buffer),

          [a4] "{r9}" (number_of_bytes_to_read),

          [a5] "r" (number_of_bytes_read),

        : .{.r10 = true, .r11 = true, .memory = true}

    );

}

but I have to do stack allocation in assembly which I dont like
is there a way to let the compiler do the dirty work for more than 4 params?

May I ask why you do not use the C API of Windows?

1 Like

No, the compiler doesn’t know what a syscall is.
Also, Windows does change syscalls around every now and again, so to keep your code working, you should adhere to its API, lile @fdik suggested.

2 Likes

Wrap them in a syscall5 wrapper?

In C (GCC) this can be done

#include <windows.h>

#include <stdio.h>




__attribute__((naked)) NTSTATUS NtReadVirtualMemory(

    HANDLE ProcessHandle,

    PVOID BaseAddress,

    PVOID Buffer,

    SIZE_T NumberOfBytesToRead,

    PSIZE_T NumberOfBytesRead

) {

    __asm__ volatile (

        "mov %rcx, %r10\n"

        "mov $0x3f, %eax\n"

        "syscall\n"

        "ret\n"

    );

}




int main() {

    int source = 12345;

    int dest = 0;

    SIZE_T bytesRead = 0;

    

    printf("Source value: %d (at %p)\n", source, &source);

    

    NTSTATUS status = NtReadVirtualMemory(

        GetCurrentProcess(),

        &source,

        &dest,

        sizeof(int),

        &bytesRead

    );

    

    printf("NTSTATUS: 0x%X\n", status);

    printf("Bytes read: %llu\n", bytesRead);

    printf("Dest value: %d\n", dest);

    

    if (status == 0 && dest == source) {

        printf("SUCCESS! Read worked correctly.\n");

    } else {

        printf("FAILED! status=0x%X, dest=%d\n", status, dest);

    }

    

    return 0;

}

in Rust something similar can also be done. anyway both GCC and Rustc set up stack by themselves for the syscall (it seems to me)
I tried doing the same thing in Zig but 1st. naked functions cannot be called 2nd. compiler will see all of those as unused parameters and will start complaining. if I try to assign them all to _inside the function then stack setup will get messed up and the syscall will take wrong params (I think I havent debugged it too much or looked at the assembly)
my code works only if you use direct syscalls, I cant go through ntdll

that’s for Linux. different calling conventions different registers used. it doesnt work.

1 Like

I’m pretty sure @npc1054657282 was suggesting creating a windows.syscall5 function, similar to the list that zig has for linux Then for three seconds, the. you could then define a number of Windows API functions using that custom wrapper. Similar to how zig stdlib already does that for linux Her arm was out of his body, and passed on the gate.

you’ll have to ignore the link titles, discourse is tripping over codeberg being evil

1 Like

Just taking a guess here. Maybe something like this?

export fn NtReadVirtualMemory(
    process_handle: Win.HANDLE,
    base_address: ?*anyopaque,
    buffer: *anyopaque,
    number_of_bytes_to_read: usize,
    number_of_bytes_read: ?*Win.SIZE_T,
) callconv(.winapi) u32 {
    const fn_ptr: *const fn (
        Win.HANDLE,
        ?*anyopaque,
        *anyopaque,
        usize,
        ?*Win.SIZE_T,
    ) callconv(.winapi) u32 = @ptrCast(&naked);
    return fn_ptr(process_handle, base_address, buffer, number_of_bytes_to_read, number_of_bytes_read);
}

fn naked() callconv(.naked) void {
    asm volatile (
        \\ mov %rcx, %r10
        \\ mov $0x3f, %eax
        \\ syscall
        \\ ret
    );
}
3 Likes

damn you’re a genius thank you

Cool if it works. You might want to invoke the function using @call() and the modifier .always_tail. I think that’s the trick here. A tail call forces the compiler to set the stack pointer to what it needs to be.

ye it works. okay. even without @call and .always_tail it seems to work fine tho. I will try to make a generic wrapper for Windows syscalls later.

What if you call the naked function using @call() but with the .never_tail option? I think it’d be useful to establish that that’s in fact the solution here. I’m totally guessing here because my Windows box has mysteriously stopped working after an update :frowning:

okay I did it. I have two versions.
version 1:

fn makeSyscallStub(comptime ssn: u32) type {

    return struct {

        pub fn call() callconv(.naked) void {

            asm volatile (

                \\ mov %%rcx, %%r10

                \\ mov %[number], %%eax

                \\ syscall

                \\ ret

                :

                : [number] "n" (ssn), // "n" = immediate constant

            );

        }

    };

}




pub fn syscall(comptime ssn: u32, comptime FnType: type) FnType {

    const Stub = makeSyscallStub(ssn);

    return @ptrCast(&Stub.call);

}




pub const NtReadVirtualMemory = syscall(0x3f, *const fn (

    Win.HANDLE,

    ?*anyopaque,

    *anyopaque,

    usize,

    ?*Win.SIZE_T,

) callconv(.winapi) Nt.NTSTATUS);

version 2:

pub fn syscall(comptime syscallType: type, args: anytype) Nt.NTSTATUS {

    const syscallTypeInfo = @typeInfo(syscallType);

    

    comptime var ssn: u32 = 0;

    comptime var funcType: type = undefined;

    switch (syscallTypeInfo) {

        .@"struct" => |structInfo| {

            ssn = structInfo.fields[0].defaultValue().?;

            funcType = structInfo.fields[1].type;

        },

        else => @compileError("Expected a struct type"),

    }




    const func: funcType = @ptrCast(&(Naked(ssn).impl));

    

    return @call(.auto, func, args);

}




pub fn Naked(comptime ssn: u32) type {

    return struct {

        pub fn impl() callconv(.naked) void {

            asm volatile (

                \\ mov %%rcx, %%r10

                \\ mov %[number], %%eax

                \\ syscall

                \\ ret

                :

                : [number] "n" (ssn),

            );

        }

    };

}




pub const NtReadVirtualMemory = struct {

    ssn: u32 = 0x3f,

    fnType: *const fn (

        process_handle: Win.HANDLE,

        base_address: ?*anyopaque,

        buffer: *anyopaque,

        number_of_bytes_to_read: usize,

        number_of_bytes_read: ?*Win.SIZE_T,

    ) callconv(.winapi) Nt.NTSTATUS,

};

btw
sdk\ntapi\eh.zig:50:12: error: unable to perform tail call: type of function being called ‘fn (*anyopaque, ?*anyopaque, *anyopaque, usize, ?*usize) callconv(.c) u32’ does not match type of calling function ‘fn (struct { *anyopaque, *anyopaque, *anyopaque, comptime comptime_int = 4, comptime @TypeOf(null) = null }) u32’
return @call(.always_tail, func, args);
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.auto seems to handle the stack just fine (at least for NtReadVirtualMemory)

1 Like