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.
OxyAnalysersends 40, 400, 4000, and 40000;
OxyBufferforwards totals of 40, 440, 4440, and 44440;
OxyCyclerupdates after receiving 40, 480, 4920, and 49360 packets in total.
A few things stand out:
OxyBufferreports a total of 44400, even though the cumulative stages add up to 44440.OxyBuffercrashes with an illegal instruction after buffering 100% oxygen.OxyCyclerremains stuck at 0% and never receives anything.
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_addrAfter 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], 0x40The 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.OutputThis 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:
0xc45- initialise the UDP send loop counter to0instead of2550xca9- increment the UDP send loop by1instead of640xf92- pass the correct cumulative total toOutput()at the 50% stage0x1033- callOutput()instead ofreport_and_exit()+0x20at the 100% stage0x11c1- fix the destination IP from121.0.0.1to127.0.0.1
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.