/home/best_shell/
on the shell server. The source can be downloaded here." -- problem definitionThis was perhaps one of the most straightforward problems to solve this year, yet was worth 160 points! We're given a relatively short problem statement and some code to analyze. Here it is:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdbool.h> #define NUMHANDLERS 6 typedef struct input_handler { char cmd[32]; void (*handler)(char *); } input_handler; bool admin = false; char admin_password[64]; input_handler handlers[NUMHANDLERS]; input_handler *find_handler(char *cmd){ int i; for(i = 0; i < NUMHANDLERS; i++){ if (!strcmp(handlers[i].cmd, cmd)){ return &handlers[i]; } } return NULL; } void lol_handler(char *arg){ if (arg == NULL){ printf("usage: lol [string]\n"); return; } printf("lol %s\n", arg); } void add_handler(char *arg){ int arg1; int arg2; if (arg == NULL){ printf("usage: add [num1] [num2]\n"); return; } sscanf(arg, "%d %d", &arg1, &arg2); printf("= %d\n", arg1 + arg2); } void mult_handler(char * arg){ int arg1; int arg2; if (arg == NULL){ printf("usage: mult [num1] [num2]\n"); return; } sscanf(arg, "%d %d", &arg1, &arg2); printf("= %d\n", arg1 * arg2); } void rename_handler(char *arg){ char *existing; char *new; if (arg == NULL){ printf("usage: rename [cmd_name] [new_name]\n"); return; } existing = strtok(arg, " "); new = strtok(NULL, ""); if (new == NULL){ printf("usage: rename [cmd_name] [new_name]\n"); return; } input_handler *found = find_handler(existing); if (found != NULL){ strcpy(found->cmd, new); }else{ printf("No command found.\n"); } } void auth_admin_handler(char *arg){ if (arg == NULL){ printf("usage: auth [password]\n"); return; } if (!strcmp(arg, admin_password)){ admin = true; printf("You are now admin!\n"); }else{ printf("Incorrect password!\n"); } } void shell_handler(char *arg){ if (admin){ gid_t gid = getegid(); setresgid(gid, gid, gid); system("/bin/sh"); }else{ printf("You must be admin!\n"); } } void setup_handlers(){ handlers[0] = (input_handler){"shell", shell_handler}; handlers[1] = (input_handler){"auth", auth_admin_handler}; handlers[2] = (input_handler){"rename", rename_handler}; handlers[3] = (input_handler){"add", add_handler}; handlers[4] = (input_handler){"mult", mult_handler}; handlers[5] = (input_handler){"lol", lol_handler}; } void input_loop(){ char input_buf[128]; char *cmd; char *arg; input_handler *handler; printf(">> "); fflush(stdout); while(fgets(input_buf, 128, stdin)){ cmd = strtok(input_buf, " \n"); arg = strtok(NULL, "\n"); handler = find_handler(cmd); if (handler != NULL){ handler->handler(arg); }else{ printf("Command \"%s\" not found!\n", cmd); } printf(">> "); fflush(stdout); } } int main(int argc, char **argv){ FILE *f = fopen("/home/best_shell/password.txt","r"); if (f == NULL){ printf("Cannot open password file"); exit(1); } fgets(admin_password, 64, f); admin_password[strcspn(admin_password, "\n")] = '\0'; fclose(f); setup_handlers(); input_loop(); return 0; }
Although it's 163 lines, its actually quite easy to parse, or at least get the general idea of what this code is trying to accomplish. An input_handler struct stores the name of a handler, and the function pointer associated with it. In other words, this struct serves as a way to identify different functions by name. Next we do some housekeeping and set up variables for storing the whether or not we're admin (which, as we'll see later, has no significant meaning), the actual admin_password, and an array of function handlers. The find_handler function accepts a name and tries to find the input_handler that has the same name. The next 6 functions are handler functions that will eventually be loaded up into the handlers array. Immediately jumping out are the rename_handler and the shell_handler functions. rename_handler contains a call to strcpy() which is a blatant red flag for a potential buffer overflow. Additionally, the shell_handler provides us with a method to grab shell access given that we're admin (at least this what the programmer intended!)
A nice place to start is to debug the binary to see if the call to strcpy() offers us anything useful. Inside of /home/best_shell/ we're given a makefile with the compiler flags used to compile this. In order to be able to debug this program, I'll also add the -ggdb flag along with everything else in the makefile to compile it in a different directory.
I've just compiled best_shell.c with the -ggdb flag additionally so I can debug it and break on line numbers, analyze variables, etc. I've also made a password.txt file so that the best_shell program can work as expected.Now let's open up a gdb session to investigate further into the structure of an input_handler. Specifically, we can get an instance of input_handler after line 135 (so I'll break on 136).
Many an exploiter can see this and see trouble! The name that's given to each handler happens 32 bytes prior to the function pointer. This in of itself is no big deal, but notice how rename_handler does a strcpy(found->cmd,new); There's the buffer overflow! Now before jumping to shellcode,ROP based exploits, or even EIP overwrite at all, notice how shell_handler takes care of elevating privileges for us AND even launches a shell! It would be easier if we just overwrite the function pointer of an input_handler so that it instead points to the shell-launching portion of shell_handler and then we can call the compromised input_handler separately. Best of all, using objdump, we don't even have to pass the if(admin) check! Let's look at the objdump for shell_handler ( note, that I'm switching back to /home/best_shell/) :
Notice how from the orange line down, it will handle the entire sequence of elevating privileges and launching shell. The rest is now trivial : invoke the rename input_handler on a function (such as shell_handler) and rename it to "A" x 32 ( a sequence of 32 "A"s to fill up the cmd[] buffer) followed by the little-endian values "\xd7\x89\x04\x08". After this, the function pointer for shell_handler will now point to the instructions that are boxed orange above (elevate privileges, launch shell). Now we'll just have to call shell_handler (which is renamed to "A" x 32 followed by the little-endian bytes). In order to keep stdin alive whenever we launch the shell, I'll use the cat <file> /dev/stdin | ./best_shell technique.
The perl script here simply writes out the payload to a file. We then use that file to give ourselves shell access.
Great, Thanks !
ReplyDelete