Problem

Solution

This one was strange to say the least. To be honest, I’m not exactly sure how I solved it. I will document here what I did to receive the flag, at least.

Not really sure what I was looking at, I just played with some of the elements in the UI, first pressing “Play Animation”, then “Submit Circuit”. After submitting, an alert popped up containing the flag:

Thinking I broke the challenge, I played with it some more then clicked submit again. Same result.

Did I get lucky? I’m not sure. But revisiting the problem while writing this, I am still able to reproduce the result intermittently so I’m not entirely sure.

Let’s take a look at the source code to see what the heck is happening in index.js:

function doRun(res, memory) {  
 const flag = runCPU(memory);  
 const result = memory[0x1000] | (memory[0x1001] << 8);  
 if (memory.length < 0x1000) {  
   return res.status(500).json({ error: 'Memory length is too short' });  
 }  
  
 let resp = "";  
  
 if (flag) {  
   resp += FLAG2 + "\n";  
 } else {  
   if (result === 0x1337) {  
     resp += FLAG1 + "\n";  
   } else if (result === 0x3333) {  
     resp += "wrong answer :(\n";  
   } else {  
     resp += "unknown error code: " + result;  
   }  
 }  
  
 res.status(200).json({ status: 'success', flag: resp });  
}
 
// ...
 
app.post('/check', async (req, res) => {  
   const circuit = req.body.circuit;  
  
   if (!Array.isArray(circuit) ||    
       !circuit.every(entry => checkInt(entry?.input1) &&    
                               checkInt(entry?.input2) &&    
                               checkInt(entry?.output))) {  
       return res.status(400).end();  
   }  
  
   const program = await fs.readFile('./programs/nand_checker.bin');  
      
   // Generate random input state with only 0x0000 or 0xffff values  
   const inputState = new Uint16Array(4);  
   for (let i = 0; i < 4; i++) {  
       inputState[i] = Math.random() < 0.5 ? 0x0000 : 0xffff;  
   }  
      
   // Create output state as inverse of input  
   const outputState = new Uint16Array(4);  
   for (let i = 0; i < 4; i++) {  
       outputState[i] = inputState[i] === 0xffff ? 0x0000 : 0xffff;  
   }  
      
   const serialized = serializeCircuit(  
       circuit,  
       program,  
       inputState,  
       outputState  
   );  
  
   doRun(res, serialized);  
});

So if memory[0x1000] | (memory[0x1001] << 8) == 0x1337, then we get the flag. But just what is memory? In the /check endpoint, we see that doRun is invoked with memory = serialized, where:

serialized = serializeCircuit(circuit,program,inputState,outputState);

However, inputState is instantiated to a “random” value, so it’s possible that this is responsible for the behaviour we’re seeing. We can’t know unless we see what serializeCircuit does. In utils.js, we see that serializeCircuit is defined as follows:

function serializeCircuit(circuit, program, inputState, outputState) {  
   const memory = new Uint8Array(65536); // 64KB memory  
  
   // Copy program at start  
   memory.set(program);  
  
   // Serialize output state at 0x1000  
   const outputView = new Uint16Array(memory.buffer, 0x1000);  
   outputView[0] = outputState.length;  
   outputView.set(outputState, 1);  
  
   // Serialize input state at 0x2000  
   const inputView = new Uint16Array(memory.buffer, 0x2000);  
   inputView.set(inputState, outputState.length + 1);  
  
   // Serialize circuit at 0x3000  
   const circuitView = new Uint16Array(memory.buffer, 0x3000);  
   circuit.forEach((gate, i) => {  
       const offset = i * 3;  
       circuitView[offset] = gate.input1;  
       circuitView[offset + 1] = gate.input2;  
       circuitView[offset + 2] = gate.output;  
   });  
  
   return memory;  
}

Let’s also take a look at the program nand_checker.bin, which we can disassemble using objdump -b binary -m i386:x86-64 -D nand_checker.bin:

nand_checker.bin:     file format binary  
  
  
Disassembly of section .data:  
  
0000000000000000 <.data>:  
  0:   4d 00 00                rex.WRB add %r8b,(%r8)  
  3:   30 5d 00                xor    %bl,0x0(%rbp)  
  6:   00 10                   add    %dl,(%rax)  
  8:   6d                      insl   (%dx),%es:(%rdi)  
  9:   00 00                   add    %al,(%rax)  
  b:   20 08                   and    %cl,(%rax)  
  d:   00 01                   add    %al,(%rcx)  
  f:   04 2d                   add    $0x2d,%al  
 11:   00 00                   add    %al,(%rax)  
 13:   10 1b                   adc    %bl,(%rbx)  
 15:   00 04 02                add    %al,(%rdx,%rax,1)  
 18:   1c 22                   sbb    $0x22,%al  
 1a:   17                      (bad)  
 1b:   12 1c 4c                adc    (%rsp,%rcx,2),%bl  
 1e:   18 00                   sbb    %al,(%rax)  
 20:   1c 14                   sbb    $0x14,%al  
 22:   0b 04 44                or     (%rsp,%rax,2),%eax  
 25:   02 1b                   add    (%rbx),%bl  
 27:   04 44                   add    $0x44,%al  
 29:   02 2b                   add    (%rbx),%ch  
 2b:   04 44                   add    $0x44,%al  
 2d:   02 0c 4c                add    (%rsp,%rcx,2),%cl  
 30:   1c 4c                   sbb    $0x4c,%al  
 32:   2c 4c                   sub    $0x4c,%al  
 34:   01 00                   add    %eax,(%rax)  
 36:   11 01                   adc    %eax,(%rcx)  
 38:   21 02                   and    %eax,(%rdx)  
 3a:   01 06                   add    %eax,(%rsi)  
 3c:   11 06                   adc    %eax,(%rsi)  
 3e:   21 06                   and    %eax,(%rsi)  
 40:   0b 00                   or     (%rax),%eax  
 42:   1b 01                   sbb    (%rcx),%eax  
 44:   06                      (bad)  
 45:   01 29                   add    %ebp,(%rcx)  
 47:   00 78 00                add    %bh,0x0(%rax)  
 4a:   7c 22                   jl     0x6e  
 4c:   0b 05 1d 00 ff ff       or     -0xffe3(%rip),%eax        # 0xffffffffffff  
006f  
 52:   28 02                   sub    %al,(%rdx)  
 54:   78 00                   js     0x56  
 56:   51                      push   %rcx  
 57:   02 61 02                add    0x2(%rcx),%ah  
 5a:   3b 05 4b 06 37 73       cmp    0x7337064b(%rip),%eax        # 0x733706ab  
 60:   47 74 31                rex.RXB je 0x94  
 63:   04 3c                   add    $0x3c,%al  
 65:   6c                      insb   (%dx),%es:(%rdi)  
 66:   37                      (bad)  
 67:   32 3c 6c                xor    (%rsp,%rbp,2),%bh  
 6a:   7c 72                   jl     0xde  
 6c:   01 01                   add    %eax,(%rcx)  
 6e:   0c 7e                   or     $0x7e,%al  
 70:   7c 56                   jl     0xc8  
 72:   0d 00 33 33 5d          or     $0x5d333300,%eax  
 77:   00 00                   add    %al,(%rax)  
 79:   10 59 00                adc    %bl,0x0(%rcx)  
 7c:   0f 00 0d 00 37 13 5d    str    0x5d133700(%rip)        # 0x5d133783  
 83:   00 00                   add    %al,(%rax)  
 85:   10 59 00                adc    %bl,0x0(%rcx)  
 88:   0f                      .byte 0xf  
       ...

The program, nand_checker.bin, seems to be computing a checksum to see if the input state matches some expected state. How this works, I’m not sure, honestly.