Using a C-style cast will generate identical assembly. As seen in Compiler Explorer:
//Source #1
int transfer(int handle, int direction, unsigned char *data, int length);
int input(int handle, void* data, int length)
{
    return transfer(handle, 1, static_cast<unsigned char*>(data), length);
}
int output(int handle, const void* data, int length)
{
    return transfer(handle, 0, static_cast<unsigned char*>(const_cast<void*>(data)), length);
}
//Source #2
int transfer(int handle, int direction, unsigned char *data, int length);
int input(int handle, void* data, int length)
{
    return transfer(handle, 1, (unsigned char*)data, length);
}
int output(int handle, const void* data, int length)
{
    return transfer(handle, 0, (unsigned char*)data, length);
}
//Assembly (both)
input(int, void*, int):
        mov     ecx, edx
        mov     rdx, rsi
        mov     esi, 1
        jmp     transfer(int, int, unsigned char*, int)
output(int, void const*, int):
        mov     ecx, edx
        mov     rdx, rsi
        xor     esi, esi
        jmp     transfer(int, int, unsigned char*, int)
So it's clear that simply using a C-style cast will solve your problem.
However, you shouldn't use a C-style cast
The reason for the verboseness of the C++ casts is to ensure that you aren't making mistakes. When a maintainer sees your code, it's important that they see the const_cast and the static_cast, because writing the code this way informs the reader that casting away the const-ness of the pointer is intentional and desired behavior. A code maintainer should see these casts, and presume that there was intent behind the code, instead of having to guess whether you knew that casting directly from const void* to unsigned char* would involve risking Undefined Behavior. Your example might not contain UB (since you specified that the contract of transfer is to treat data as read-only when direction is 0), but it's important that anyone else who needs to make changes to your code understands the deliberateness of your coding practices.