Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ptrace change syscall number arm64

I am trying to change a calling from one syscall to a different one on linux arm64 using ptrace.

From what i understand the syscall number is in x8, reading the registers of the syscalls confirms this. im changing this number and calling SETREGSET and the old syscall is called and not the new one. when i check the registers on the return from the syscall x8 is set to the syscall I gave him as it should be.

Changing other parameters for the same syscall works.

I saw some places using PTRACE_SET_SYSCALL but i couldn't find a lot about it, I tried to use it but it seems like its not supported in this arch, it isnt defined and writing the number instead failed for not existing.

What am i doing wrong? why doesn't it work?

Here is the code, i removed prints and validations for simplicity, for this example im only trying to stop the write syscall:

ptrace(PTRACE_ATTACH, pid, NULL, NULL);

int status;
waitpid(pid, &status, 0);
while (ptrace(PTRACE_SYSCALL, pid, NULL, NULL) == 0)
{
    waitpid(pid, &status, 0);

    struct user_pt_regs regs;
    struct iovec io;
    io.iov_base = &regs;
    io.iov_len = sizeof(regs);

    ptrace(PTRACE_GETREGSET, pid, (void*)NT_PRSTATUS, &io);

    // reg[7] is 0 before syscall and 1 after
    if (regs.regs[7] == 0)
    {
        // Change write syscall
        if (regs.regs[8] == 64)
        {
            // Change the syscall to getpid (doesn't matter)
            regs.regs[8] = 172;

            ptrace(PTRACE_SETREGSET, pid, (void*)NT_PRSTATUS, &io);
        }
    }
}

As for the tracee, a simple hello world with write with sleep;

char buf[] = "hello world\n";
while(1)
{
    write(1, buf, sizeof(buf));
    sleep(5);
}

Although the program runs with no errors and prints the right registers the syscall doesnt change and it keep printing hello world

like image 250
asdklf alfdkea Avatar asked Oct 29 '25 10:10

asdklf alfdkea


1 Answers

On arm64, the kernel stores the syscall about to be executed in a separate variable pt_regs.syscallno for compatibility reason, while ptrace(PTRACE_SETREGSET, pid, NT_PRSTATUS, &iov) does change the registers (see the definition of struct pt_regs in the kernel source: commit:eec4df2/arch/arm64/include/asm/ptrace.h:178).

When on arm or on arm64 and the kernel is compiled with CONFIG_COMPAT=yes, syscall numbers can be change by ptrace(PTRACE_SET_SYSCALL, pid, NULL, syscallno).

For an arm64 kernel without CONFIG_COMPAT=yes (which is more common), you'll need ptrace(PTRACE_SETREGSET, pid, NT_ARM_SYSTEM_CALL, &iov).

Example:

int syscallno;
struct iovec iov = {
    .iov_base = &syscallno,
    .iov_len = sizeof (int),
};
ptrace(PTRACE_SETREGSET, traceePid, NT_ARM_SYSTEM_CALL, &iov);

Regset of NT_ARM_SYSTEM_CALL is defined here: commit:eec4df2/arch/arm64/kernel/ptrace.c:1173.

Another way to block a syscall with arguments like SYS_write or SYS_chdir is to set the argument to an invalid address, so that it will fail with EINVAL:

struct user_regs_struct regs;
struct iovec iov = {
    .iov_base = &regs,
    .iov_len = sizeof (struct user_regs_struct),
};
ptrace(PTRACE_GETREGSET, traceePid, NT_PRSTATUS, &iov);
regs.regs[0] = 123;
regs.regs[1] = 456;
regs.regs[3] = 789;
ptrace(PTRACE_SETREGSET, traceePid, NT_PRSTATUS, &iov);

Register usage in syscall: Chromium OS Docs.

See also: Gist: SBell6hf / A ptrace-based syscall jailer that runs on arm64, x86_64 and i386

like image 168
SBell6hf Avatar answered Nov 01 '25 14:11

SBell6hf