Reputation: 41
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 = ®s;
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
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 = ®s,
.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
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