2

I am trying to force the init process of an embedded Linux system to exec() my own init program (systemd) so that I can test an external filesystem before writing it to the system's flash (and risk bricking the device). With GDB, I can run the command gdb --pid=1, then in that shell type call execl("/lib/systemd/systemd", "systemd", 0) (which works exactly as I need it to), but I do not have enough room to put GDB on the system's flash.

I was wondering exactly what ptrace() calls GDB uses with its call command so that I can implement that in my own simple C program.

I tried using strace to figure out what ptrace() calls GDB used, but the resulting file was 172,031 lines long. I also tried looking through its source code, but there were too many files to find what I was looking for.

The device is running Linux kernel version 3.10.0, the configuration is available here: https://pastebin.com/rk0Zux62

Billy
  • 615
  • 3
  • 11
  • 29
  • You want to run some command against any arbitrary process and force it to invoke another process via `exec` so that this new executable assumes the PID of the original process, right? – slm Aug 24 '18 at 01:52
  • @slm Yes, exactly. – Billy Aug 24 '18 at 01:52
  • What's the kernel version? You could do this - https://stackoverflow.com/questions/18122592/how-to-set-process-id-in-linux-for-a-specific-program. – slm Aug 24 '18 at 01:54
  • @slm 3.10.0, but CONFIG_CHECKPOINT_RESTORE is disabled in the kernel configuration. – Billy Aug 24 '18 at 01:55
  • OK, had to ask. – slm Aug 24 '18 at 01:56
  • I'm not done w/ helping, I'm eliminating leads. – slm Aug 24 '18 at 01:57
  • Possible dup - https://unix.stackexchange.com/questions/117544/replace-instance-of-process-in-place – slm Aug 24 '18 at 01:58
  • This is what you're talking about w/ `gdb` - http://mohit.io/blog/debugging-modifying-code-at-runtime/. – slm Aug 24 '18 at 02:00
  • @slm Okay, I'll try that in the morning. In the meantime, here's the device's kernel configuration if you or anyone else would like to see it: https://pastebin.com/rk0Zux62 – Billy Aug 24 '18 at 02:01
  • If the GDB approach works, that means you can do it with the regular `ptrace()` interface, and you might want to look into writing up a wrapper just for that if you don't want to have to go through GDB every time you boot Linux on this device. – Austin Hemmelgarn Aug 24 '18 at 19:33
  • @AustinHemmelgarn That's what I was planning on doing. I don't think it has enough ram or flash for a full install of GDB. – Billy Aug 24 '18 at 21:24
  • I can use the command `call execl(...` in GDB (`gdb app 1`) with the standard C execl() syntax. Now I just need to figure out how to write that sole functionality into my own simple C program like @AustinHemmelgarn suggested. I'll post it as an answer when I do. – Billy Aug 25 '18 at 14:56
  • 1
    This could be a good start : [Intercepting and Emulating Linux System Calls with Ptrace](https://nullprogram.com/blog/2018/06/23/). You might have to search for additional architecture-dependent informations – A.B Aug 26 '18 at 02:24
  • @AB Seems to be just what I'm trying to do. Thanks, I'll take a look. – Billy Aug 26 '18 at 12:19
  • What architecture exactly? armel? armhf? – Joseph Sible-Reinstate Monica Sep 03 '18 at 00:43
  • @JosephSible armel – Billy Sep 03 '18 at 02:25

1 Answers1

3

Here's a C program that should do it. Note a few known issues:

  • Should probably use memcpy instead of strict aliasing violations
  • Uses its own environment variables instead of the old tracee's environment variables
  • If the tracee doesn't make any syscalls, this will never manage to do anything
  • Doesn't check when the tracee gets stopped to make sure it's really a syscall stop and not a signal stop or something
  • Should set IP back in syscall-exit-stop instead of syscall-enter-stop
  • Doesn't do any sanity checking of the execve arguments (doing this would be a good opportunity for execveat)
  • Completely unportable (hardcodes CONFIG_ARM_THUMB among lots of other things)
  • Leaves the process in a state where it'll probably crash if any of the syscalls don't work right

Compile it with -fno-strict-aliasing, then run it as ./a.out 1 /lib/systemd/systemd systemd.

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ptrace.h>
#include <linux/ptrace.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <sys/syscall.h>
#include <sys/mman.h>

#define CONFIG_ARM_THUMB

#ifdef CONFIG_ARM_THUMB
#define thumb_mode(regs) \
        (((regs)->ARM_cpsr & PSR_T_BIT))
#else
#define thumb_mode(regs) (0)
#endif

extern char **environ;

static pid_t pid;

/* The length of a string, plus the null terminator, rounded up to the nearest sizeof(long). */
size_t str_size(char *str) {
        size_t len = strlen(str);
        return len + sizeof(long) - len % sizeof(long);
}

void must_poke(long addr, long data) {
        if(ptrace(PTRACE_POKEDATA, pid, (void*)addr, (void*)data)) {
                perror("ptrace(PTRACE_POKEDATA, ...)");
                exit(1);
        }
}

void must_poke_multi(long addr, long* data, size_t len) {
        size_t i;
        for(i = 0; i < len; ++i) {
                must_poke(addr + i * sizeof(long), data[i]);
        }
}

long must_poke_string(long addr, char* str) {
        size_t len = str_size(str);
        size_t longs_len = len / sizeof(long);
        char *more_nulls_str = malloc(len);
        memset(more_nulls_str + len - sizeof(long), 0, sizeof(long)); /* initialize the bit we might not copy over */
        strcpy(more_nulls_str, str);
        must_poke_multi(addr, (long*)more_nulls_str, longs_len);
        free(more_nulls_str);
        return addr + len;
}

int main(int argc, char** argv) {
        struct user_regs regs;
        int i, envc;
        unsigned long mmap_base;
        size_t mmap_string_offset, mmap_argv_offset, mmap_envp_offset;
        size_t mmap_len = 2 * sizeof(char*); /* for the NULLs at the end of argv and envp */

        if(argc < 3) {
                fprintf(stderr, "Usage: %s <pid> <executable image> <args...>\n", argv[0]);
                return 1;
        }

        pid = strtol(argv[1], NULL, 10);

        /* for the image name */
        mmap_len += str_size(argv[2]);

        for(i = 3; i < argc; ++i) {
                /* for the pointer in argv plus the string itself */
                mmap_len += sizeof(char*) + str_size(argv[i]);
        }

        for(i = 0; environ[i]; ++i) {
                /* for the pointer in envp plus the string itself */
                mmap_len += sizeof(char*) + str_size(environ[i]);
        }
        envc = i;

        if(ptrace(PTRACE_ATTACH, pid, 0, 0)) {
                perror("ptrace(PTRACE_ATTACH, ...)");
                return 1;
        }
        if(waitid(P_PID, pid, NULL, WSTOPPED)) {
                perror("waitid");
                return 1;
        }

        /* Stop at whatever syscall happens to be next */
        if(ptrace(PTRACE_SYSCALL, pid, 0, 0)) {
                perror("ptrace(PTRACE_SYSCALL, ...)");
                return 1;
        }
        printf("Waiting for the target process to make a syscall...\n");
        if(waitid(P_PID, pid, NULL, WSTOPPED)) {
                perror("waitid");
                return 1;
        }
        printf("Target made a syscall. Proceeding with injection.\n");

        if(ptrace(PTRACE_GETREGS, pid, 0, &regs)) {
                perror("ptrace(PTRACE_GETREGS, ...)");
                return 1;
        }

        /* End up back on the syscall instruction so we can use it again */
        regs.ARM_pc -= (thumb_mode(&regs) ? 2 : 4);

        /* mmap some space for the exec parameters */
        regs.ARM_r0 = (long)0;
        regs.ARM_r1 = (long)mmap_len;
        regs.ARM_r2 = (long)(PROT_READ|PROT_WRITE);
        regs.ARM_r3 = (long)(MAP_PRIVATE|MAP_ANONYMOUS);
        regs.ARM_r4 = (long)-1;
        regs.ARM_r5 = (long)0;
        if(ptrace(PTRACE_SETREGS, pid, 0, &regs)) {
                perror("ptrace(PTRACE_SETREGS, ...)");
                return 1;
        }
        if(ptrace(PTRACE_SET_SYSCALL, pid, 0, SYS_mmap2)) {
                perror("ptrace(PTRACE_SET_SYSCALL, ...)");
                return 1;
        }

        /* jump to the end of the syscall */
        if(ptrace(PTRACE_SYSCALL, pid, 0, 0)) {
                perror("ptrace(PTRACE_SYSCALL, ...)");
                return 1;
        }
        if(waitid(P_PID, pid, NULL, WSTOPPED)) {
                perror("waitid");
                return 1;
        }

        /* make sure it worked and get the memory address */
        if(ptrace(PTRACE_GETREGS, pid, 0, &regs)) {
                perror("ptrace(PTRACE_GETREGS, ...)");
                return 1;
        }

        if(regs.ARM_r0 > -4096UL) {
                errno = -regs.ARM_r0;
                perror("traced process: mmap");
                return 1;
        }
        mmap_base = regs.ARM_r0;

        /* set up the execve args in memory */
        mmap_argv_offset = must_poke_string(mmap_base, argv[2]);

        mmap_string_offset = mmap_argv_offset + (argc - 2) * sizeof(char*); /* don't forget the null pointer */
        for(i = 0; i < argc - 3; ++i) {
                must_poke(mmap_argv_offset + i * sizeof(char*), mmap_string_offset);
                mmap_string_offset = must_poke_string(mmap_string_offset, argv[i + 3]);
        }
        must_poke(mmap_argv_offset + (argc - 3) * sizeof(char*), 0);
        mmap_envp_offset = mmap_string_offset;
        mmap_string_offset = mmap_envp_offset + (envc + 1) * sizeof(char*); /* don't forget the null pointer */
        for(i = 0; i < envc; ++i) {
                must_poke(mmap_envp_offset + i * sizeof(char*), mmap_string_offset);
                mmap_string_offset = must_poke_string(mmap_string_offset, environ[i]);
        }
        must_poke(mmap_envp_offset + envc * sizeof(char*), 0);

        /* jump to the start of the next syscall (same PC since we reset it) */
        if(ptrace(PTRACE_SYSCALL, pid, 0, 0)) {
                perror("ptrace(PTRACE_SYSCALL, ...)");
                return 1;
        }
        if(waitid(P_PID, pid, NULL, WSTOPPED)) {
                perror("waitid");
                return 1;
        }

        if(ptrace(PTRACE_GETREGS, pid, 0, &regs)) {
                perror("ptrace(PTRACE_GETREGS, ...)");
                return 1;
        }

        /* call execve */
        regs.ARM_r0 = (long)mmap_base;
        regs.ARM_r1 = (long)mmap_argv_offset;
        regs.ARM_r2 = (long)mmap_envp_offset;
        if(ptrace(PTRACE_SETREGS, pid, 0, &regs)) {
                perror("ptrace(PTRACE_SETREGS, ...)");
                return 1;
        }
        if(ptrace(PTRACE_SET_SYSCALL, pid, 0, SYS_execve)) {
                perror("ptrace(PTRACE_SET_SYSCALL, ...)");
                return 1;
        }

        /* and done. */
        if(ptrace(PTRACE_DETACH, pid, 0, 0)) {
                perror("ptrace(PTRACE_DETACH, ...)");
                return 1;
        }
        return 0;
}

I developed and tested this via qemu-system-arm with the 3.2.0-4 kernel and wheezy userland from https://people.debian.org/~aurel32/qemu/armel/

  • Maybe you should add "it captures and changes the next syscall the ptraced application makes" to explain some of the shortcomings. – dirkt Sep 03 '18 at 06:29