Writeup by toby-bro for Binary Lullaby

hardware

April 29, 2025

Wow I was not expecting a Verilog challenge, this language is really fun. Anyways, this challenge consists of a Verilog file that does a lot of nonsense no one cares to read except for the line assign { y[15], y[7] } = { x[15], x[7] };, and the output of what this complicated circuit gave when it was fed the flag. The challenge is also lacking an implementation of the logic gates it uses but that is not a problem, we can write it.

As the inputs are wires, the input can take only two values 0 and 1. For once bruteforce seems the only option, and bruteforce seems feasible as there are only $2^{14} = 16384$ different values to test. We will thus write a gates.v file with the implementation of the basic gates in Verilog and a testbench.v that will bruteforce this code until finding the correct output. Once we have the correct input we will simply decode it back to ASCII with decode.py.

To solve the challenge the only thing left is to execute our programs. We are using Icarus to simulate the Verilog, and we had put the initial input in input.txt

iverilog -o sweet_bruteforcer testbench.v && vvp sweet_bruteforcer && uv run decode.py

This is gates.v

module NAND (input A, input B, output Y);
  assign Y = ~(A & B);
endmodule

module NOR (input A, input B, output Y);
  assign Y = ~(A | B);
endmodule

module NOT (input A, output Y);
  assign Y = ~A;
endmodule

module XOR (input A, input B, output Y);
  assign Y = A ^ B;
endmodule

Our testbench.v

`timescale 1ns / 1ps
`include "gates.v"
`include "netlist.v"

module testbench;

  reg  [15:0] x_candidate;
  wire [15:0] y_actual;
  reg  [15:0] y_target;
  reg  [13:0] unknown_bits_counter; // To iterate through 2^14 possibilities

  // Instantiate the Unit Under Test (UUT)
  circuit uut (
    .x(x_candidate),
    .y(y_actual)
  );

  integer file_handle;
  integer scan_handle;
  integer line_num;
  integer output_file_handle;

  initial begin
    file_handle = $fopen("input.txt", "r");
    if (file_handle == 0) begin
      $display("Error: Could not open input.txt");
      $finish;
    end

    output_file_handle = $fopen("output.txt", "w");
     if (output_file_handle == 0) begin
      $display("Error: Could not open output.txt for writing");
      $fclose(file_handle);
      $finish;
    end


    line_num = 0;
    while (!$feof(file_handle)) begin
      scan_handle = $fscanf(file_handle, "%b\n", y_target);
      if (scan_handle == 1) begin
        line_num = line_num + 1;

        x_candidate[15] = y_target[15];
        x_candidate[7]  = y_target[7];

        // Iterate through all possibilities for the 14 unknown bits
        begin : brute_force_loop
          for (unknown_bits_counter = 0; unknown_bits_counter < 16384; unknown_bits_counter = unknown_bits_counter + 1) begin
            // Assign unknown bits from counter
            x_candidate[14:8] = unknown_bits_counter[13:7]; // Map upper 7 bits
            x_candidate[6:0]  = unknown_bits_counter[6:0];  // Map lower 7 bits

            #1; // Wait for combinatorial logic to settle

            if (y_actual == y_target) begin
              $fdisplay(output_file_handle, "%b", x_candidate);
              disable brute_force_loop;
            end
          end
        end

         if (unknown_bits_counter == 16384) begin
             $display("Error: No input found for y_target = %b", y_target);
         end

      end
    end

    $fclose(file_handle);
    $fclose(output_file_handle);
    $finish;
  end

endmodule

And finally decode.py

def decode_output(input_filepath: str = 'output.txt') -> str:
    decoded_string = ''

    with open(input_filepath, 'r') as f:
        for line in f:
            binary_string = line.strip()
            if len(binary_string) == 16:
                try:
                    char1_bin = binary_string[0:8]
                    char2_bin = binary_string[8:16]

                    char1 = chr(int(char1_bin, 2))
                    char2 = chr(int(char2_bin, 2))

                    decoded_string += char1 + char2
                except ValueError:
                    print(f'Skipping invalid binary line: {binary_string}')
                except OverflowError:
                    print(f'Skipping value out of ASCII range: {binary_string}')
            elif binary_string:
                print(f'Skipping line with incorrect length ({len(binary_string)} bits): {binary_string}')

    return decoded_string


if __name__ == '__main__':
    result = decode_output()
    if result is not None:
        print('Decoded Result:')
        print(result)