pwnable.kr之uaf

虚表利用

Posted by b0ldfrev on July 27, 2018

题目地址:http://pwnable.kr/play.php

连接 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。

0x03 拿shell过程

pic1

文件下载