Blkhurst

February 2022

OxyBuffer

Stage 2 Area 2

We are given three binaries - OxyAnalyser, OxyBuffer, and OxyCycler. The challenge tells us that OxyBuffer has been modified by exactly five 1-byte changes out of its 13,344 bytes. Our task is to restore the file to its original state, with the unlock key being the 5 byte offsets we patch. To do that, we first need to understand how the three programs interact and where the pipeline breaks.

Understanding the pipeline

Running the three programs makes the intended flow clear. OxyAnalyser has four stages - 10%, 20%, 50%, and 100% oxygen - and each stage sends a batch of IPC (inter-process communication) messages. OxyBuffer receives these as a cumulative total (40, 440, 4440, 44440) and should forward the same number of UDP packets to OxyCycler.

Note: each stage is cumulative.
OxyAnalyser sends 40, 400, 4000, and 40000;
OxyBuffer forwards totals of 40, 440, 4440, and 44440;
OxyCycler updates after receiving 40, 480, 4920, and 49360 packets in total.

A few things stand out:

Verifying the communication

To ensure the applications are actually sending the expected IPC and UDP messages, I built two small programs, one to receive and count the IPC messages, and another to listen for the UDP traffic.

Using Radare2 to inspect the disassembly, I found the IPC key is generated using ftok("oxybackingfile", 0x7b), and UDP packets are sent to 121.0.0.1:8080. That immediately exposed the first modified byte, and why OxyCycler remained at 0%. The destination should be localhost (127.0.0.1).

; IPC Key
0x00000d22      be7b000000     mov esi, 0x7b               ; '{'
0x00000d27      488d3db00400.  lea rdi, str.oxybackingfile ; 0x11de ; "oxybackingfile"
0x00000d2e      e80dfdffff     call sym.imp.ftok
; UDP Port
0x00000c25      bf901f0000     mov edi, 0x1f90             ; 8080
0x00000c2a      e861fdffff     call sym.imp.htons
; UDP Host
0x00000c33      488d3d850500.  lea rdi, str.121.0.0.1      ; 0x11bf ; "121.0.0.1"
0x00000c3a      e8b1fdffff     call sym.imp.inet_addr

After patching the destination IP, my UDP receiver confirmed that OxyBuffer was sending traffic, but only three packets instead of the hundreds or thousands we expect.

Reversing OxyBuffer

Reviewing the disassembly for OxyBuffer revealed two relevant functions. GetMessages which receives IPC messages, and Output(uint32_t count) which forwards the buffered data as UDP packets.

Reversing Output()

Since the current problem is that too few packets were being sent, Output() was the obvious place to start. Rewriting this function as pseudocode made the issue very easy to spot:

void Output(uint32_t count) {
  // Setup UDP socket
  // ...
  for (int i=255; i < count; i+=64) {
    // send 40-byte UDP packet
    // ...
  }
}
; The offsets of loop initialiser and increment values
0x00000c42      c745d8ff0000.  mov dword [var_28h], 0xff
0x00000ca6      8345d840       add dword [var_28h], 0x40

The function creates a UDP socket, points it at 127.0.0.1:8080, and then loops over masterbuffer, sending one 40-byte chunk per iteration. The problem is that the loop is broken, the counter starts at 255 instead of 0, and increments by 64 instead of 1.

After fixing those two bytes, OxyBuffer began sending packets more reliably. The first two stages now work correctly sending 40 packets at 10%, then 440 packets at 20%, which was enough for OxyCycler to begin rising. However, at 50% only 2 UDP packets are sent, and after reaching 100% the program still crashed without sending any packets.

The remaining faults had to be in GetMessages() - the function responsible for calling Output().

Reversing GetMessages()

Similarly, rewriting as pseudocode revealed the cause of the remaining two issues.

void GetMessages() {
  uint32_t total = 0;
  uint32_t stage = 0;
 
  // receive IPC messages and copy them into masterbuffer
  // increment total for each message received
 
  if (stage == 0 && total == 40) {
    Output(total);
    stage++;
  }
 
  if (stage == 1 && total == 440) {
    Output(total);
    stage++;
  }
 
  if (stage == 2 && total == 4440) {
    Output(stage); // incorrect argument
    stage++;
  }
 
  if (stage == 3 && total == 44440) {
    report_and_exit(...); // wrong function called
  }
}
; Should be [var_6ch] (rbp-0x6c)
0x00000f90      8b459c         mov eax, dword [var_64h]
0x00000f93      89c7           mov edi, eax
0x00000f95      e822fcffff     call sym.Output

This function keeps a cumulative total of received IPC messages and should call Output() at the four expected thresholds. The first two stages work as intended. At 50% however, GetMessages() should pass the running total (4440) into Output(), but instead it passes the stage counter (2).

The final issue appears at 100%. Rather than calling Output(), the modified binary calls 0xbba (report_and_exit()+0x20), which explains the illegal instruction crash.

Summary

At this point, all five modified bytes have been identified:

After restoring all five bytes, the pipeline worked from end to end. OxyAnalyser produced the IPC messages, OxyBuffer forwarded the UDP traffic, and OxyCycler reached 100% oxygen. The patched file matched the provided SHA-256, confirming the repair was correct, and the challenge was complete.