AssaultCube RCE: Technical Analysis

So I’ve been doing quite a lot of Wargames & CTFs and I was looking to research a “real” production application.

I decided to go with a game called AssaultCube.

The game is open-source and is still very active with quite a lot of players and servers still running, so I thought “that might be an interesting target”.

(Cube Engine)

Defining Goals

The goal was clear and straightforward, achieving Remote Code Execution Client →Server.

There’s also the possibilities of client →client, or server →client, but they both tend to be easier as the client is usually written in a more trustful manner.
Escalating to admin, crashing the server, or writing some hacks (which I did by the way) were not what I was looking for.

Starting Out

So I opened up the game’s code and started to get familiar with the codebase.
Right from the beginning I was looking for the code that takes input from the client and looked for ways to meddle with it, essentially providing unexpected data to the server.
Pretty quickly I came across the function at .

This is the function that, according to the developers, does “server-side processing of updates”, looks like a good place to start.

So I started going over the various updates that can be sent from the client, for instance, sending a text message or the player’s position on the map. I quickly noticed that reading data from the client is done using functions like and , etc.

According to my initial instincts, I started looking for simple “dumb” overflows with strings but they’ve wrapped it safely and I couldn’t find any of those (that would’ve been too easy). So I just kept reading the source and recursively looking into where the data I’m providing is being processed.

Then…
I came across this.

If you haven’t spotted the “problem” yet, take a second and look it up.
Let me preprocess that for you:

There isn’t any integer that is both smaller than 0 and greater than 10.
That means that no matter which the client sends, it’ll be set at since the condition will never be met. That could’ve easily been avoided with but unfortunately that’s not included within which is the warning option in the Makefile of the project.

At that point, I immediately started looking for references to
to see what can I do with this bug.

A lot of the references seemed to be useless in terms of exploitation, but then I noticed the function that changed everything — .

The function enables me to write a somewhat random integer (cannot control the value of the assignment) into memory that is at a constant offset from the struct (, members) which is located within the much bigger struct.

I patched the client’s code to send an unexpected integer (non-existent weapon ID), expecting it to cause the server to crash, essentially getting a segmentation fault.

And what do you know…

The server has crashed and all clients were immediately disconnected.
At this point I can just halt and ruin the game for other players.
(Don’t do that)

By the way, oddly enough, I later noticed that there is no input sanitation at the introduction of the client, so I could’ve also done it there.

What now?

Crashing the server is nice and all, but how can we actually escalate that into something more interesting?

My intuition was to look for members within the that writing a random integer into would disrupt the game’s coherent flow.
At first, I couldn’t find any, given that the limitations are fierce (no control over what to write) so I mostly looked for booleans or values that a sudden, out of the ordinary, change would make a difference.

I started iterating over the members of the struct to look for places to write a random integer into, and I saw that there are a few vector structs.

Perhaps overwriting the capacity member of the vector would introduce an overflow possibility! Making the vector think it’s bigger than it really is.

I opened up the definition to see how it’s built and after a little reading I quickly picked up that:

— amount of elements within the vector.
— how many elements the vector can currently hold.
— A pointer to the buffer itself.

Corrupting the of one of the vectors was tempting :)

I chose and not the other ones because this is the one that I can supply my own buffer into, and that’s why overflowing it would be ideal.
We’ll see that in a bit.

I calculated the offsets

I supplied as the weapon ID and hoped that a big integer would be written into and luckily enough…

Should probably mention that it took a while before I realized that I could do that, the bug indeed seemed useful, but I just couldn’t find a good use to it at first to the point that I just sat it aside and kept on looking for other bugs while keeping in mind that I have this card to activate at need.
Glad I found this neat trick eventually.

As I said earlier, was the interesting vector because it’s the one that I could write data to, mostly using these macros.

The interesting calls to these macros are at these cases of the event handler:
1. SV_TEXT — Queues the sent text message.
2. default — Queues any uncaught data from the client.

Let’s start with sending a big text message that would overflow .
This is useful in order to see what is the following chunk of memory and whether it can be used for further exploitation. We could see that the actually allocated capacity before the overwrite is , so as long as we write more than that we should overflow the buffer.

I patched the client to send so that it’ll be easy to tell how our buffer is being “consumed” by the code.

Wow.

Seems like we can already call a function of our choice.
The register is under our control and is pointing at

That’s very cool!
Let’s take a look at where this segfault occurs exactly.
The function.

What we have done in our overflow is overwrite the vtable of .
This is possible since and are adjacent chunks on the heap. If you’re unsure what vtables are and how dynamic dispatch works in C++, take a look here.

The instruction dereferences the function where should be the vtable’s address.

Let’s review the flow of execution that got us into .
In the function which is the main game loop, each cycle, or tick, all inputs are read from the clients, and a “world state” is built.

calls which gathers all the from all the clients and unifies them into a

Afterwards, the messages is passed into which simply calls with the same arguments.

If you were paying attention,
you could’ve noticed that not only that we overwrite ‘s vtable,
the that is passed to contains our text message.

Roughly,

So, we can both control the function that is called, and even choose an argument to pass it! Neato’.

itself is initialized only once at the start of the game and is of type

Let’s rewind into the limitations for a second.
Because of the call to here, it is not possible to send a message with unprintable characters, and the size of the message is limited to 260 bytes.

This is pretty problematic because it drastically reduces the leverage of this attack, in effect, allowing us to only pass printable pointers.

In order to deal with that, I wrote a script that returns all the GOT functions whose pointers are completely printable. Note that I had to limit the search to GOT functions because I needed a memory address that holds a pointer to a function, exactly like the vtable behaves. That’s why I couldn’t just call functions within the executable itself. The script returned the following.

Hold on…Is the address of completely printable?

Well, easy peasy, let’s just call and our text message is already passed as an argument to the function, so that’s it, we can run commands on the server’s host, right? You guessed it, of course not.

Let’s take a moment to discuss how methods or member functions, are called in C++ in a very abstract way, after all, is a virtual method of .

A method is a function like any other, with the small caveat that it needs to be able to reference the object’s members as well. The way that it’s being done is via an implicit this argument.

If we were to debug this, we’d see that actually loads into the first argument and jumps to .

By the way, in Python it’s much more clear simply because it’s explicit, every method receives a as its first parameter.

Now that we’ve cleared this up, we can see why it won’t be possible to call with our command, because itself is the first argument that is passed, upon this invocation — not . Unfortunately.

But looking at the bright side, we can still call certain functions and control the second argument with printable characters. That has to be useful. Right?

After a lot of attempts, I couldn’t quite solve this puzzle so I returned to the code and looked towards different directions that would allow me to bypass the frustrating printable characters only limitation so that I’d be able to call much more functions, and also be able to pass pointers and what not as my arguments.

I revisited the macros to look for different ways to write data to the vector, there were a lot of other places but they wrote a relatively small buffer, like my position which is about 3 integers, or a voice communication sound which is a single integer so that won’t trigger an overflow.

But then I realized that a client can send multiple events at a single call!

So for instance, I’d be able to
1. Change my name.
2. Update my location on the map.
3. Send a voice message.
4. Send a text message.

And only then would exit and all of these would be bundled into . This is vital for the sake of writing binary data into .

I looked up all the places where is being used, which is basically a macro that takes all the input read from the client up until the point its invoked, and adds it to .

Interestingly, one of the places it appears is in the case of the client event handler which sort of behaves like a flush or emptying the buffer I’d say.

This is great because our data doesn’t affect or break anything, literally all it does is to get written into . Now what’s left to do is get to be as big as we want so that not too much data is read, nor too little.

The function returns the size that a certain event is supposed to read. If the event was supposed to be caught as a case in the event handler than -1 is returned which would disconnect the client (can be seen above) since that shouldn’t truly happen.

I made a list of all the events that can be passed so that I won’t get disconnected (return -1), and also are bigger than 0. This is what I ended up with

& are too small to write any pointer, though is sufficient! You might be wondering, if you can call several events at the same cycle, what’s the problem with simply triggering multiple times? Well, the thing is that the event type itself is also written into the messages buffer.

So that won’t fly because there will be “noise” in between.

Great! Now we can write 7 bytes in a row to , which in practice mean that we can call any imported function now.

A peek into the binary’s imported functions.

After browsing for a while, looking for function to call within the program with the second argument in control, I noticed syslog.

From its signature,
we can see that its second argument is a format string.
If we’d take a look at we’d see:

The remaining arguments are a format, as in printf(3),

I assume most of you are familiar with format string attack, if not, give it a read here or Google it.

This is awesome!
Can potentially be escalated into arbitrary write-ish.

I padded with until I reached the vtable’s memory,
at which point I sent the and wrote ‘s address, then I took a look at the stack to see what interesting pointers are there, and to which memory can I write.

Unfortunately, on the stack itself there wasn’t any buffer that I can control. This is where I had to get creative.

While there isn’t any buffer that I can write to on the stack at that moment of the execution, there are a lot of pointers on the stack to other locations on the stack itself. What I decided to do is, using those pointers, write an address to somewhere on the stack using that pointer, and then write to that value by referencing the stack memory itself.

Frankly, this turned out to be easier than I thought.
It’s important to mention that there’s a certain limitation to how much padding you can do using a format string attack, so I couldn’t use that for a full arbitrary write but I could definitely write to the executable’s memory space.

Amazing. Now we have arbitrary write to the executable’s memory space.
What do we write and to where?

I went to the section, and searched for functions that I can pass a buffer to as the first argument so that it’ll be properly set for

I went to the event handler of the text messages, SV_TEXT, and saw which libc functions are being used, and more specifically, those whose first argument is the text message itself.

It needed to be accurate enough so that it doesn’t affect / break the rest of the server’s logic and cause it to crash, so preferably not a function that gets called every second or something.

At , there’s a call to that checks if the message that is being processed is equivalent to the message that was last sent, obviously to avoid spamming.

This is the perfect fit.

Using the format string attack, I wrote (please don’t mind the very bad code, this is not what this is about) into so that whenever is called, it’ll actually jump to .

Now, when I send a text message, it is passed through , and the call to would in fact run the text message as a shell command.

How cool is that?!

Let’s take a look.

Steps

A. Overflow messages into demorecord and overwrite the vtable to syslog.
B. Place on the stack using the format string attack.
C. Write into the using the format string attack.
D. Run the command that pops a calculator by simply sending a text message.

You might be wondering why the hell am I launching another client.
Well, that’s a legitimate question.

The reason is right here.

Because I’m the first client to connect to the server,
my index at the vector, as well as my is .
This becomes a problem when your buffer is a null-terminated string.

In the format attack which we discussed earlier,
we’re sending the format as a text message that is appended to the , that is later passed to .
I’m forced to send the formats not from the first client because the string will terminate after the first character (SV_CLIENT).

Summary

Let’s review the exploit.

  1. Using the initial vulnerability, overwrite the (capacity) of the vector into a bigger value that it can actually hold.
  2. Overwriting the vtable by overflowing the heap into so that calls .
  3. Connect with another client, and exploit the ’s format to write the address of to the stack, and then write to it.
  4. Run a shell command by simply sending a text message.

Conclusion

This game is definitely still being played, not that you’d start playing it today, but there are still some old-schoolers around.

Server Browser (can also scroll for more)

From the developers’ point of view, the only real vulnerability that I’ve exploited is that:

The rest is pure creativity.

I can only say that this has been a more teaching experience that all the CTFs I’ve done combined. They did give me a good sense of ideas on how to approach problems, but I’m glad I took a turn into that.

Needless to say, there was struggle and a lot of research in between that I did not elaborate about that eventually wasn’t utilized. The whole process wasn’t as effortless as it is being presented in this article and there are a lot of smaller details that I simply hid out because they’re just not interesting.
Overall it took quite a long time.

Since people have been asking, the bug had already been fixed.
Both here, and here. Would also mention that I deleted my fork of AssaultCube according to the developer’s request.

If you have any questions or suggestions, make sure to hit me in any of these mediums or the comments.

Email , Github , Twitter

Thanks for reading.

Easter Egg

The vulnerability was introduced on my birthday.
Guess it was meant to be.