asdklf alfdkea
asdklf alfdkea

Reputation: 41

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

Upvotes: 4

Views: 1651

Answers (2)

SBell6hf
SBell6hf

Reputation: 41

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

Upvotes: 4

anroesti
anroesti

Reputation: 11373

For some reason, it seems that you have to set the system call number in register 0, not 8 as would be expected from the aarch64 ABI.

// Change the syscall to getpid (doesn't matter)
regs.regs[0] = 172;

I am unsure why that is the case; maybe ptrace stops execution of the process upon entry of the syscall function, which then moves the first argument (in register 0) to the actual system call number argument in register 8 itself.

Upvotes: 0

Related Questions