Dr Silvio Cesare
@silviocesare
Summary
C++ is a popular systems programming language. As such, it is desirable to develop exploitation primitives for this language since many exploitation targets will be written in C++. This blog posts looks at 2 C++ specific exploitation techniques when STL objects are subject to memory corruption. In particular, we will examine vector iterators and smart pointers in Linux on Ubuntu 20.04.Introduction
Generic or abstract data types are implemented in C++ using a library known as STL or the standard template library. Typical STL containers include abstract data types such as linked lists, vectors, arrays, associative maps, sets, and hash tables.
As part of STL, iterator classes exist to navigate through their appropriate data structure. Different types of iterators exist for particular STL objects. For example, a singly linked list implemented as the STL forward_list, only allows iteration in a single direction. Thus a forward iterator is more reasonable than a bidirectional iterator. However, all iterator classes have a uniform interface that appear roughly the same, independent of the underlying data structure that is being navigated.
STL objects contain members of utility to an attacker who is able to perform memory corruption on those members. For example, if a class contains a fixed size buffer that is subsequently followed by an STL vector, a buffer overflow will overwrite the STL vector object. The question is then - what primitives can be obtained when various STL objects are corrupted?
Two particular STL data structures are of interest to this blog post - vectors and smart points. A series of blog posts will follow which look at other STL objects, but for now, we will focus on these 2 as a starting point.
A vector is a dynamically sized array. It offers constant time indexed access. To iterate through a vector, it is also possible to use an STL iterator object.
Smart pointers are very common in modern C++. In particular, we will look at unique_ptr which is a container for a regular pointer that has the semantics of only have 1 owner. Thus, when unique_ptr is destroyed, it frees (deletes) the underlying pointer. This is in contrast to a shared_ptr, which can have multiple owners. Only once all owners have destroyed their smart pointer is the underlying raw pointer destroyed.
Vector Iterators
A vector iterator simply maintains an internal pointer to the current element that the iterator points it. Thus, memory corruption of a vector iterator simply overwrites this pointer. This is a powerful device and can be used by an attacker, given appropriate application logic, as an arbitrary read/write primitive.Here is a demonstration of a simulated memory corruption allowing *it = 0x41414141 to be used as an arbitrary write.
#include <iostream> #include <cstdio> #include <vector> #include <unistd.h> static char buf[16]; static std::vector<long>::iterator it; static std::vector<long> v; static long x; int main() { v = std::vector<long>(10); v[0] = 10; v[1] = 20; it = v.begin(); *(long *)&buf[16 + 0] = (long)&x; // BUFFER OVERFLOW *it = 0x41414141; printf("%lx\n", x); _exit(0); }
As you can see, it a vector iterator is a powerful STL object to corrupt.
Unique Pointers
A unique_ptr in STL, takes a raw pointer and wraps it in the STL container. The STL object simply holds the underlying raw pointer. The destructor of the unique_ptr deletes the underlying pointer. Thus, a memory corruption of a unique_ptr will overwrite the underlying pointer.
Corrupting a pointer might be useful in a number of ways. For example, in the previous vector iterator example, corruption a pointer gave an attacker the ability to perform an arbitrary write. But let's do something different as an example of unique_ptr corruption.
Let's make it such that we synthesise freeing a wild pointer. But we will make this wild pointer special, and utilise a heap corruption technique known as the House of Spirit. In the House of Spirit technique, we will place a fake malloc chunk into the heap that will subsequently be returned by a future allocation. We do this by freeing a fake chunk header and setting an appropriate size field. The chunk payload does not need to be correct, we simply need the fake chunk header for the chunk to be placed into the allocator. When this chunk is returned by the allocator in a future allocation, the chunk payload might overlap into critical data structures maintained by the application.
Let's look at it in action:
#include <iostream> #include <memory> #include <cstdio> #include <cstring> #include <unistd.h> class Foo { public: char buf[16]; std::unique_ptr<long *> ptr; }; unsigned long pad; unsigned long size; unsigned long house_of_spirit; int admin; char padding[100]; void f() { struct Foo foo; foo.ptr = std::make_unique<long *>(new long[10]); size = 112; *(long *)&foo.buf[16] = (long)&house_of_spirit; // BUFFER OVERFLOW // foo.ptr is deleted by smart pointer destructor } int main() { char *p; f(); p = new char[100]; memset(p, 'A', 100); if (admin == 0x41414141) { fprintf(stderr, "WIN\n"); } exit(0); }
A very interesting technique.
Conclusion
This blog post presented 2 techniques on how to leverage STL object corruption to create exploitation primitives. Having developed techniques in STL object corruption allows for better exploitation of C++ targets.