返回介绍

pwnable.kr 之 uaf 虚表利用

发布于 2022-11-26 19:43:19 字数 12131 浏览 0 评论 0 收藏 0

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

连接 ssh uaf@pwnable.kr -p2222 (pw: guest)

0x01 程序分析

源代码 uaf.cpp

#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); 的实现过程,汇编代码如下:

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分配好的堆空间:

pwndbg> x/3xg $rax
0x12f9040:    0x0000000000401570    0x0000000000000019
0x12f9050:    0x00000000012f9028

0x401570 便是虚表的的地址,0x19 是构造函数传入的age = 25 ,0x12f9028 是构造函数传入的name的地址,我们去看看:

pwndbg> x/1s 0x12f9028
0x12f9028:    "Jack"

我们重点看看vtable虚表的地址0x401570

pwndbg> x/3xg 0x401570
0x401570 <_ZTV3Man+16>:    0x000000000040117a    0x00000000004012d2
0x401580 <_ZTV5Human>:    0x0000000000000000

这里面有两个函数指针,一个指向 give_shell 另一个指向重载后的introduce 函数。

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

文件下载

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文