Between September 6 and 9, the second radare2 conference (r2con) took place in Barcelona. This security conference is mainly focused in radare2, an open-source framework for reverse engineering with a great and active community. Ackcent was there, during the trainings (Wednesday and Thursday) and talks (Friday and Saturday), but also solving some crackmes and playing the r2wars competition. In this blog post, we will share what was the motivation to play, how we programmed our warrior, a brief summary of the r2wars competition day, some conclusions and final thoughts.
What is r2wars?
The r2wars is a game similar to corewar, where:
- two programs (warriors), written in assembly language, are loaded in a fixed (the first 1024 bytes) and shared memory address space (the battle arena),
- the scheduler executes one instruction of each warrior on each turn, skipping some turns to adjust the cyclic cost of some instructions,
- the final goal is to win the combat, and that happens when the opponent warrior executes an invalid instruction or accesses a memory address out of the battle arena.
The allowed assembly languages for this first competition were MIPS, ARM, x86, but in a future, could be others that currently are also supported by ESIL engine. This engine emulates the execution of combats, translating known assembly languages into an intermediate language (ESIL). If you are interested in CPU emulation, check ESIL and unicorn.
In the r2wars competition each warrior fights with the others. The warrior that wins more combats, wins the competition. To win a combat, a warrior has to win 2 of 3 rounds. A round or a combat, can be won, lost or tied. If two warriors are tied, because they have the same combat results, the overall index (total numbers of rounds won) is used to break the tie. If the overall index is also the same and the warriors are in podium positions, there is a last round between them to get a winner.
Why should I participate in r2wars?
If you enjoy writing assembly code (e.g. to overwrite return addresses), analyse it (e.g. to understand a new malware sample), or you think it is something you want to learn because it seems interesting, this competition is for you because you will have to:
- review assembly instructions to optimize your warrior,
- consolidate concepts, like CPU registers, to protect your code,
- read others warrior code to learn new strategies.
In our case, it was a combination of all of the above and the challenge to play a new low-level competition, with also the cool idea of having fun watching two viruses (sorry, warriors :-) fighting in memory to survive.
Yes, the name of our warrior is replicator and is written in 32 bits x86 assembly language.
The warrior name has a direct relationship with its combat strategy, but first, let us explain the main strategies used in corewar warriors:
- Imp: it is a one instruction warrior, that copies the current instruction to the next memory address to be executed.
- Bomber: writes memory addresses to kill the opponent warrior but it does not move itself to other locations in memory.
- Replicator: is able of copying itself to other locations in memory. It can be thought of as a self-moving bomb.
- Vampire: bombs memory with jump instructions to capture the opponent warrior and force it to execute useless code.
In the end, it seems there is not a perfect strategy, because it also depends on:
- the opponent warrior strategy,
- the memory locations where the warriors are loaded when each round begins.
With the above in mind, we programmed a warrior with three main characteristics:
- Hard to find
As main strategy, we use the replicator because we think that is harder to find a moving code than a static one.
- Small in size
A great inspiration was pancake’s demo warrior, that uses one instruction (pushad) to write a good amount of memory addresses and so, causing a great damage.
We use two stages. In the first stage, we prepare some global variables and the payload for the second stage. This comes with a bit of slowness during the boot (first stage) but with a smaller and quicker second stage. Also, the use of registers to store the second stage provides a fresh and secure copy of it because it is outside of the battle area and so, it cannot be overwritten by opponent warriors. Bear in mind that CPU registers and the stack memory are not part of the battle arena.
- Hunter not the prey
Program execution goes from lower to higher memory addresses. If we fight with another warrior that follows this natural and incremental behaviour and our warrior is loaded in a higher memory address than our opponent, then we will be the prey. In order to avoid this situation, at the end of the first stage, after some instructions have been executed, we replicate ourselves to a lower memory address with the hope that maybe, we will become the hunter who pursues his prey.
Then, the resulting assembly code for our warrior is the following:
; First stage == all instructions below mov al, 0x50 ; First position mov cx, 0x400 ; Limit of battle arena mov edx, 0xdddddddd ; Illegal opcodes mov ebx, edx ; More illegal opcodes ; Second stage == next 3 instructions mov edi, 0xcc39c401 mov esi, 0xc489037c mov ebp, 0xe4ff6044 mov esp, eax ; Stack pivot to first position pushad ; Copy second stage jmp esp ; Execute second stage
Let us explain each line:
mov al, 0x50 ; First position
We load the value 0x50 into EAX. This value will be the size of our jumps and the first memory address where our second stage will be replicated or backward copied (normal pushad instruction behaviour).
mov cx, 0x400 ; Limit of battle arena
We load the value 0x400 (or 1024) into ECX. This value will be used to know if we can replicate the second stage or we are outside the battle arena and we have to jump to the EAX memory address.
mov edx, 0xdddddddd ; Illegal opcodes mov ebx, edx ; More illegal opcodes
We load 0xdddddddd into EDX and EBX. These registers together with ECX and EAX, create a tail of illegal opcodes (mainly the first 8 bytes), that can overwrite the opponent warrior code to kill him.
mov edi, 0xcc39c401 mov esi, 0xc489037c mov ebp, 0xe4ff6044
The opcodes of the second stage. Let us disassemble them:
; mov edi, 0xcc39c401 add esp, eax cmp esp, ecx ; mov esi, 0xc489037c jl 5 mov esp, eax ; mov ebp, 0xe4ff6044 inc esp pushad jmp esp add esp, eax
We jump ESP forward 0x50 bytes.
cmp esp, ecx
After that, we check if we are outside the battle arena.
mov esp, eax
If we are outside, we jump to the first memory address (EAX) and increment ESP. You are right, last instruction is totally unnecessary. It was inherited from previous versions of our warrior.
pushad jmp esp
If we are inside, then we replicate the second stage and jump to it (EIP = ESP).
With that, we finished the explanation of the second stage and we come back to the three last instructions of the first stage.
mov esp, eax ; Stack pivot to first position pushad ; Copy second stage jmp esp ; Execute second stage
These instructions have the same logic than the last ones of the second stage but with an unconditional jump to our first memory address (trying to become a hunter).
In the following video, we can watch a combat between demo warrior (initially loaded at address 0x86, as hunter) and our warrior (initially loaded at address 0x163, as prey). After some instructions executed, our warrior replicates itself to address 0x30 and becomes a hunter. After some more instructions, our warrior overwrites the opponent warrior and wins the combat.
The competition was on Saturday noon. Nobody participated in the MIPS or ARM competition. For the x86 competition, there were 8 warriors and in less than 30 minutes each one had fought with the others. The final classification was:
To break the tie between the second and third position there was a last round that we were lucky to win. It is curious to note that the first three warriors chose a replication strategy. We finished second but we received these great prizes:
- an r2 beach bag, flag and sticker
- an orange pi zero computer
- a printed collection of the International Journal of PoC||GTFO
- and a delicious premium crafted beer
Conclusions and final thoughts
It has been a nice experience and a great way to learn by playing. For this reason, we thank @pancake and @skuater for making possible the r2wars competition.
We expect the same competition for the next year but with more warriors participating, other architectures (e.g. x64) and also an improved visualization.
In our opinion, the following ideas could improve next year competition:
- In addition to the warrior name, a brief description of its strategy.
- A real-time classification, visible during all the rounds.
- After ending a round, a clear notification with the result, e.g.:
- Warrior1 wins! Warrior2 executes an illegal instruction!
- Warrior1 wins! Warrior2 accesses a memory address out of the battle arena!
- A tie between Warrior1 and Warrior2! Time is up!
Last but not least, we want to end the blog post with a question to think about. It seems that, at a first glance, there is not a perfect warrior strategy but could there be a better approach than writing a handmade warrior? We have one in mind, do you?
See you next year on the battle arena!