连接 ssh uaf@pwnable.kr -p2222 (pw: guest)
0x01 程序分析
源代码 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;
}
可以看到父类Human有虚函数give_shell和introduce,子类Man和Woman继承了父类并且重载了父类的introduce虚函数。有关虚函数虚表的概念,见我的这篇博文 https://b0ldfrev.top/2018/07/25/C++-%E8%99%9A%E8%A1%A8%E5%88%86%E6%9E%90/
0x01 漏洞分析
这里需要我们利用漏洞Use-After-Free(UAF)。 该漏洞的简单原理为:
- 产生迷途指针(Dangling pointer)——已分配的内存释放之后,其指针并没有因为内存释放而置为NULL,而是继续指向已释放内存。
- 这块被释放的内存空间中被写入了新的内容。
- 通过迷途指针进行操作时,会错误地按照释放前的偏移逻辑去访问新内容。
在case 3: 中释放指针m, w指向的的内存空间,同时m, w没有被置为NULL
在case 2: 中构造一个文件,文件中含有要去m或w指定的空间中写入的内容
在case 1: m对象与w对象都调用了introduce这个虚函数,我们可以更改vtable中的函数地址来更改程序执行流程。
0x02 漏洞利用
我们先看看 Human* m = new Man(“Jack”, 25); 的实现过程,汇编代码如下:
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
lea rax, [rbp+var_12]
mov rdi, rax
call __ZNSaIcEC1Ev ; std::allocator<char>::allocator(void)
lea rdx, [rbp+var_12]
lea rax, [rbp+var_50]
mov esi, offset aJack ; "Jack"
mov rdi, rax
; try {
call __ZNSsC1EPKcRKSaIcE ; std::string::string(char const*,std::allocator<char> const&)
; } #starts at 400EF2
lea r12, [rbp+var_50]
mov edi, 18h ; unsigned __int64
; try {
call __Znwm ; operator new(ulong)
; } #starts at 400F00
mov rbx, rax
mov edx, 19h
mov rsi, r12
mov rdi, rbx
; try {
call _ZN3ManC2ESsi ; Man::Man(std::string,int)
; } #starts at 400F13
mov [rbp+var_38], rbx
lea rax, [rbp+var_50]
mov rdi, rax ; this
; try {
call __ZNSsD1Ev ; std::string::~string()
; } #starts at 400F23
lea rax, [rbp+var_12]
mov rdi, rax
call __ZNSaIcED1Ev ; std::allocator<char>::~allocator()
lea rax, [rbp+var_11]
mov rdi, rax
在 call Znwm ; #operator new(ulong) 分配好了堆内存,从mov edi, 18h 看出 分配的大小为0x18字节
在 call ZN3ManC2ESsi ; #Man::Man(std::string,int) 调用构造函数,执行完这一步之后我们去看看为m分配好的堆空间:
1
2
3
pwndbg> x/3xg $rax
0x12f9040: 0x0000000000401570 0x0000000000000019
0x12f9050: 0x00000000012f9028
0x401570 便是虚表的的地址,0x19 是构造函数传入的age = 25 ,0x12f9028 是构造函数传入的name的地址,我们去看看:
1
2
pwndbg> x/1s 0x12f9028
0x12f9028: "Jack"
我们重点看看vtable虚表的地址0x401570
1
2
3
pwndbg> x/3xg 0x401570
0x401570 <_ZTV3Man+16>: 0x000000000040117a 0x00000000004012d2
0x401580 <_ZTV5Human>: 0x0000000000000000
这里面有两个函数指针,一个指向 give_shell 另一个指向重载后的introduce 函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
pwndbg> x/5i 0x40117a
0x40117a <_ZN5Human10give_shellEv>: push rbp
0x40117b <_ZN5Human10give_shellEv+1>: mov rbp,rsp
0x40117e <_ZN5Human10give_shellEv+4>: sub rsp,0x10
0x401182 <_ZN5Human10give_shellEv+8>: mov QWORD PTR [rbp-0x8],rdi
0x401186 <_ZN5Human10give_shellEv+12>: mov edi,0x4014a8
pwndbg> x/5i 0x4012d2
0x4012d2 <_ZN3Man9introduceEv>: push rbp
0x4012d3 <_ZN3Man9introduceEv+1>: mov rbp,rsp
0x4012d6 <_ZN3Man9introduceEv+4>: sub rsp,0x10
0x4012da <_ZN3Man9introduceEv+8>: mov QWORD PTR [rbp-0x8],rdi
0x4012de <_ZN3Man9introduceEv+12>: mov rax,QWORD PTR [rbp-0x8]
Human* w = new Woman(“Jill”, 21) 的过程同理
由于执行 case 3 内存释放先施放了m,而后才释放了w,所以我们在开辟小于等于24字节的空间时,系统优先考虑的是使用原先w指针指向的对象占用的空间,再使用m指针指向的对象占用的空间。(Fastbin LIFO原则)
而又因为introduce函数分别由m,w指向的对象来调用,所以内存释放后先调用的是m指向的introduce函数,而这时由于fastbin链表尾的fd指针会被清0,所以原本的m的虚表地址会被置为0,为了避免报错,m指向的内存空间也应该被覆写,所以就要调用两次 case 2
那么要怎样才能让程序执行introduce时却执行了give_shell呢?可以看到这两个函数始终相差8个字节,因为我可以操控释放后的内存,所以可以改变虚表指针的值,只要利用UAF改写对象内存空间中虚表指针指向的地址 = 虚函数表首地址 - 8,只用把原始的0x401570改成0x401568就会让程序执行give_shell。