C++ Memory Corruption (std::list) - part 3

 

Summary

This is the 3rd part of the C++ memory corruption series*. In this post, we'll look at corrupting the std::list class in Linux and see what exploitation primitives we can gain. We'll see that we can build arbitrary read/write primitives.

* https://blog.infosectcbr.com.au/2020/08/c-memory-corruption-part-1.html

* https://blog.infosectcbr.com.au/2022/01/c-memory-corruption-stdvector-part-2.html


Author: Dr Silvio Cesare

Introduction

C++ is a common language for memory corruption. However, there is much more literature on exploiting C and not C++ programs. C++ presents new classes, objects, and data structures which can all be effectively used for building exploitation primitives.  In this post, we'll look at the std::list class and see what specific primitives we can obtain.

Let's start by looking at /usr/include/c++/10/bits/stl_list.h

    /// Common part of a node in the %list.
    struct _List_node_base
    {
      _List_node_base* _M_next;
      _List_node_base* _M_prev;

 ...

      struct _List_impl
      : public _Node_alloc_type
      {
        __detail::_List_node_header _M_node;

 

The 2 members _M_next and _prev begin the std::list class as an object in memory since _List_node_header derives from _List_node_base. List headers don't represent the actual data nodes of the list. However, every list has a header node.

Technique 1

For this technique, an attacker has the ability to corrupt the list object and then accesses the first node in the list through an iterator. We can make this iterator access point to an arbitrary location in memory.

The first data node of a list is implemented as header.next. We'll corrupt this next node.

#include <cstdio>
#include <cstdlib>
#include <list>

static long victim;

int
main()
{
	std::list<long> mylist;
	unsigned long *p = (unsigned long *)&mylist;
	std::list<long>::iterator iter;

	p[0] = (unsigned long)&victim - 16;

	// iter = header.next
	iter = mylist.begin();
	if (iter != mylist.end()) {
		*iter = 0x4142434445464748;
	}
	printf("%lx\n", victim);
	exit(0);
}

We can see that we overwrite p[0] which corrupts the header.next pointer. We make it point to a fake data node such that the victim data overlaps the list node's data. In the end, *iter = 0x4142434445464748 overwrites the victim.

 

Technique 2

 

The second technique I'll show is corrupting the last data node of the list. Again, we'll corrupt the std::list object in memory, then make an iterator point to the last node. We'll write with this iterator and end up writing to an arbitrary address of our choosing - in this case, the victim variable.

 

A little additional information on this technique is needed in how lists are organised. The last data node of a list is given as header.prev. In fact, the header is a stopper node in a circular linked list. This design of a linked list makes some linked list operations simpler without the need for conditional checks against NULL. When the list is empty, the next and prev pointers point back to the header/stopper.


Here is technique 2.

 

#include <cstdio>
#include <cstdlib>
#include <list>

static long victim;

int
main()
{
	std::list<long>::iterator iter;
	std::list<long> mylist;
	unsigned long *p = (unsigned long *)&mylist;

	mylist.push_back(0x11);

	p[1] = (unsigned long)&victim - 16;

	// iter = header.prev
	iter = mylist.end();
	if (iter != mylist.begin()) {
		--iter;
		*iter = 0x4142434445464748;
	}

	printf("%lx\n", victim);
	exit(0);
}

 

Conclusion

 

In this blog post, I presented 2 techniques to take a std::list object corruption and create an arbitrary r/w primitive through iterator access. This can be the basis for writing a complete exploit.




Popular posts from this blog

Empowering Women in Cybersecurity: InfoSect's 2024 Training Initiative

C++ Memory Corruption (std::string) - part 4

Pointer Compression in V8