Cherry (crypto)

A Starknight hacked an old slot machine and turned it into something strange?! I heard that you win a secret message if you manage to get triple cherries, but...

This challenge gives us a slot machine, and our goal is to make all the slots become cherries. The main logic of the program is in script.js:

let slotSymbols = [];
let m = 0; // gets initialised to 32768
let awascii32 = 'awjelyhosiumpcntbdfgr.,!{}_/;CTF';

let spinMode = 0; // either 0, 1 or 2
let spinCounts = [0,0,0]; // how much we've spun in each mode. reset by reset()
let slotIndices = [0,0,0]; // the current index of the character being displayed
let slotSpins = [0,0,0]; // depends on spinMode
let ciphertextIndex = 0; // modified by change()

fetch('static/slotsymbols.txt')
.then(response => response.text())
.then(text => {
  slotSymbols = text.split(/(?:)/u);
  m = slotSymbols.length;
}).then(() => {
  playCoin(0);
  reset();
});


function change() {
  ciphertextIndex = (ciphertextIndex + 1) % 3;
  reset();
}


function reset() {
  if (ciphertextIndex == 0)       slotIndices = [10992, 30978, 12520];
  else if (ciphertextIndex == 1)  slotIndices = [30983,  7390,   481];
  else if (ciphertextIndex == 2)  slotIndices = [25974, 26744,  9122];
  spinCounts = [0,0,0];
  updatePlaintextDisplay();
  doSpinCleanup();
}


function spin() {
  const buttons = document.querySelectorAll('.button');
  buttons.forEach((b) => b.disabled = true);
  spinCounts[spinMode] += 1;
  updateModeDisplay();
  updatePlaintextDisplay();
  doSpinAnimation();
  for (let columnIndex = 0; columnIndex < 3; columnIndex++) {
    
  }
  setTimeout(() => {
    doSpinCleanup();
    buttons.forEach((b) => b.disabled = false);
  }, 1)
}


function playCoin(n){
  spinMode = n;
  if (n == 0)       slotSpins = [ 19,  22,  19];
  else if (n == 1)  slotSpins = [ 32,  27,  29];
  else if (n == 2)  slotSpins = [347, 349, 353];
  updateModeDisplay();
}


function updatePlaintextDisplay() {
  let decToAwascii32 = (x) => {return awascii32.charAt(x % 32) + awascii32.charAt((x >> 5) % 32) + awascii32.charAt((x >> 10) % 32)};
  document.getElementById('spinCount0').innerHTML = decToAwascii32(spinCounts[0]);
  document.getElementById('spinCount1').innerHTML = decToAwascii32(spinCounts[1]);
  document.getElementById('spinCount2').innerHTML = decToAwascii32(spinCounts[2]);
}

/**
 * 
 * NOTE: Everything below this point is purely visual and not needed to solve the challenge.
 * 
 */

function createSymbolElement(symbol) {
  const div = document.createElement('div');
  div.classList.add('symbol');
  div.textContent = symbol;
  return div;
}

function doSpinAnimation() {
  const slots = document.querySelectorAll('.slot');
  slots.forEach((slot, columnIndex) => {
    const symbols = slot.querySelector('.symbols');
    symbols.style.transition = 'none';
    symbols.style.top = '0';
    symbols.replaceChildren();
    for (let i = slotIndices[columnIndex]; i <= slotIndices[columnIndex] + slotSpins[columnIndex]; i++) {
      symbols.appendChild(createSymbolElement(slotSymbols[i % m]))
    }
    symbols.offsetHeight;
    symbols.style.transition = '';
    const symbolHeight = symbols.querySelector('.symbol')?.clientHeight;
    const offset = -(symbols.childElementCount - 1) * symbolHeight;
    symbols.style.top = `${offset}px`;
  });
}

function doSpinCleanup() {
  const slots = document.querySelectorAll('.slot');
  slots.forEach((slot, columnIndex) => {
    const symbols = slot.querySelector('.symbols');
    symbols.style.transition = 'none';
    symbols.style.top = '0';
    symbols.replaceChildren(createSymbolElement(slotSymbols[slotIndices[columnIndex]]));
    symbols.offsetHeight;
    symbols.style.transition = '';
  }); 
}

function updateModeDisplay() {
  document.getElementById('spinMode').innerHTML = spinMode;
}

When the script first runs, m is set to slotSymbols.length, which is the number of symbols available and is always 32768.

Initially, before spinning, the characters we are shown depends on cipherTextIndex. Its value can be 0, 1, or 2. Depending on its value, reset() gives us a different combination of initial indexes to use in slotIndices.

When we spin(), the change of indices also depends on slotSpins. slotSpins is set by the playCoin() function. We can play with either 0, 1, or 2 coins, and each gives us a different array slotSpins.

The equation determining the next indices is given by this line of code:

 slotIndices[columnIndex] = (slotIndices[columnIndex] + slotSpins[columnIndex]) % m;

Upon inspecting slotsymbols.txt, we can see that the cherry is at index 0 since it is the first symbol in the file. So our objective is to make slotIndices[0] = 0, slotIndices[1] = 0 and slotIndices[2] = 0.

Let's formulate the problem. For each of the 3 cipherTextIndex values, we can spin with 0 coins i times, spin with 1 coin j times, and spin with 2 coins k times. Hence our equations are:

[10992+19i+32j+347k,30978+22i+27j+349k,12520+19i+29j+353k]=[0,0,0][10992+19i+32j+347k,30978+22i+27j+349k,12520+19i+29j+353k]=[0,0,0]
[30983+19i+32j+347k,7390+22i+27j+349k,481+19i+29j+353k]=[0,0,0][30983+19i+32j+347k,7390+22i+27j+349k,481+19i+29j+353k]=[0,0,0]
[25974+19i+32j+347k,26744+22i+27j+349k,9122+19i+29j+353k]=[0,0,0][25974+19i+32j+347k,26744+22i+27j+349k,9122+19i+29j+353k]=[0,0,0]

For each equation, we can solve for i, j and k to find out how many times to roll in each coin state to get 3 cherries.

We can guess that when we get 3 cherries, updatePlaintextDisplay() will make the spinCount0, spinCount1 and spinCount2 elements show part of the flag.

Hence I used z3 to solve the above constraints, then input the individual i j k values into decToAwascii32 to get the characters of the flag.

from z3 import *
m = 32768
i, j, k = Int('i'), Int('j'), Int('k')
i_cpy, j_cpy, k_cpy = i, j, k
s = Solver()
# solve each equation
s.add((10992 + 19 * i_cpy + 32 * j_cpy + 347 * k_cpy) %  m == 0)
s.add((30978 + 22 * i_cpy + 27 * j_cpy + 349 * k_cpy) %  m == 0)
s.add((12520 + 19 * i_cpy + 29 * j_cpy + 353 * k_cpy) %  m == 0)
# [k = 25598, i = 36962, j = 29860]
# s.add((30983 + 19 * i_cpy + 32 * j_cpy + 347 * k_cpy) %  m == 0)
# s.add((7390 + 22 * i_cpy + 27 * j_cpy + 349 * k_cpy) %  m == 0)
# s.add((481 + 19 * i_cpy + 29 * j_cpy + 353 * k_cpy) %  m == 0)
# [k = 14158, i = 698597, j = 433210]
# s.add((25974 + 19 * i_cpy + 32 * j_cpy + 347 * k_cpy) %  m == 0)
# s.add((26744 + 22 * i_cpy + 27 * j_cpy + 349 * k_cpy) %  m == 0)
# s.add((9122 + 19 * i_cpy + 29 * j_cpy + 353 * k_cpy) %  m == 0)
# [k = 26344, i = 86118, j = 101684]
s.add(i_cpy >= 0)
s.add(j_cpy >= 0)
s.add(k_cpy >= 0)

print(s.check())
print(s.model())

awascii32 = 'awjelyhosiumpcntbdfgr.,!{}_/;CTF'

def decToAwascii32(x):
  return awascii32[x % 32] + awascii32[(x >> 5) % 32] + awascii32[(x >> 10) % 32]

def printFlagHelper(i, j, k):
  print(f"{decToAwascii32(i)}{decToAwascii32(j)}{decToAwascii32(k)}")

printFlagHelper(36962, 29860, 25598)
printFlagHelper(698597, 433210, 14158)
printFlagHelper(86118, 101684, 26344)
#jellyCTF{you_won_cherries!}

The intended solve using matrices is given here:

from math import gcd
import sympy

a = sympy.Matrix([[19, 32, 347],
                  [22, 27, 349],
                  [19, 29, 353]])

b1 = sympy.Matrix([10992,30978,12520])
b2 = sympy.Matrix([30983,7390,481])
b3 = sympy.Matrix([25974,26744,9122])
m = 32768

def solve(b):
    det = int(a.det())
    if gcd(det, m) == 1:
        ans = ((pow(det, -1, m) * a.adjugate()) @ b) % m
        return ans
    else:
        print("don't know")

def decToAwascii32(x):
    awascii32 = r'awjelyhosiumpcntbdfgr.,!{}_/;CTF'
    return awascii32[x % 32] + awascii32[(x >> 5) % 32] + awascii32[(x >> 10) % 32]

x1,x2,x3 = solve(b1)
print(f'{decToAwascii32(m-x1)}{decToAwascii32(m-x2)}{decToAwascii32(m-x3)}')
x1,x2,x3 = solve(b2)
print(f'{decToAwascii32(m-x1)}{decToAwascii32(m-x2)}{decToAwascii32(m-x3)}')
x1,x2,x3 = solve(b3)
print(f'{decToAwascii32(m-x1)}{decToAwascii32(m-x2)}{decToAwascii32(m-x3)}')

Last updated