Created
August 16, 2020 18:04
-
-
Save zwade/5b48179b63cc57cc6282f97f9edcfc8b to your computer and use it in GitHub Desktop.
Reimplementation of DEF CON 2020's Pinboool Binary
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from typing import Tuple, Dict, List, TypedDict | |
import copy | |
import struct | |
from functools import cached_property | |
import math | |
from pwnlib.util.packing import flat, u16 | |
def twos_byte(b): | |
b = b & 0xFF | |
if b <= 0x7F: | |
return b | |
else: | |
return b - 0x100 | |
def twos_int(b): | |
b = b & 0xFF | |
if b <= 0x7F: | |
return b | |
else: | |
return b - 0x100 | |
def cmd_from_bytes(cmd): | |
return struct.unpack("<HII", cmd) | |
class MemAssignment(TypedDict): | |
addr: int | |
value: bytes | |
class GameState: | |
commands: Tuple[int, int, int] | |
memory_bank: List[MemAssignment] | |
command_idx: int = 0 | |
rng_state: int = 0x78a | |
ball_loc: int = 0xAA | |
score: int = 0 | |
sr_bonus: int = 0 | |
tilt_points = 0 | |
mission_available = 0 | |
active_mission = 0 | |
cold_war_bonus = 0 | |
watching_bonus = 0 | |
sold_the_moon_bonus = 0 | |
def __init__(self, commands: Tuple[int, int, int], memory_bank: List[MemAssignment]): | |
serialized_commands = flat([ | |
struct.pack("<HII", command[0], command[1], command[2]) | |
for command in commands | |
]) | |
self.commands = commands | |
self.memory_bank = [ | |
{ "addr": 0, "value": serialized_commands }, | |
*memory_bank, | |
] | |
self.memory = None | |
self.create_memory() | |
def clone(self): | |
g = GameState(copy(self.commands), copy(self.memory_bank)) | |
g.command_idx = self.command_idx | |
g.rng_state = self.rng_state | |
g.ball_loc = self.ball_loc | |
g.score = self.score | |
g.sr_bonus = self.sr_bonus | |
g.tilt_points = self.tilt_points | |
g.mission_available = self.mission_available | |
g.active_mission = self.active_mission | |
g.watching_bonus = self.watching_bonus | |
g.cold_war_bonus = self.cold_war_bonus | |
g.sold_the_moon_bonus = self.sold_the_moon_bonus | |
def next(self): | |
self.command_idx += 1 | |
def read(self, addr, len): | |
return self.memory[addr:addr + len] | |
def advance_rng(self): | |
self.rng_state = 214013 * self.rng_state + 2531011 | |
self.rng_state = self.rng_state & 0xFFFFFFFF | |
ret = (self.rng_state >> 16) & 0x7FFF | |
return ret | |
def die(self): | |
self.ball_loc = 1 | |
def assign(self, assignment: MemAssignment): | |
self.memory_bank.append(assignment) | |
self.create_memory() | |
def create_memory(self): | |
mem = [0 for i in range(1024)] | |
for assignment in self.memory_bank: | |
for offset in range(len(assignment["value"])): | |
mem[assignment["addr"] + offset] = assignment["value"][offset] | |
self.memory = bytes(mem) | |
return self.memory | |
def __repr__(self): | |
return f"<GameState command={self.command} rng={self.rng_state:x} score={self.score}+{self.sr_bonus}>" | |
@property | |
def command(self): | |
byte_command = self.read(self.command_idx * 10, 10) | |
return struct.unpack("<HII", byte_command) | |
@property | |
def cmd(self): | |
return self.command[0] | |
@property | |
def addr(self): | |
return self.command[1] | |
@property | |
def len(self): | |
return self.command[2] | |
@property | |
def command_str(self): | |
cmd = self.command | |
return struct.pack("<HII", cmd[0], cmd[1], cmd[2]) | |
@property | |
def ball(self): | |
return (self.ball_loc + 0x10000) % 0x100 | |
@staticmethod | |
def from_file(filename): | |
with open(filename, "rb") as f: | |
byte_data = f.read() | |
assert len(byte_data) == 1024 | |
g = GameState([], [ { "addr": 0, "value": byte_data }]) | |
return g | |
def entry(gs: GameState): | |
correct_header = struct.pack("<4sHI", b"BALL", 10, 100) | |
if (correct_header[:4] != gs.command_str[:4]): | |
print("Invalid ball") | |
return (gs, -1) | |
gs.next() | |
while gs.command[0] != 1: | |
gs.next() | |
return main_switch(gs) | |
def rocket_launch(gs: GameState): | |
file_name = gs.read(gs.command[1], gs.command[2]) | |
print(f"Opening [{file_name}]") | |
if file_name == b"secret_base": | |
unique = 14 | |
elif file_name == b"..": | |
unique = 0 | |
else: | |
unique = 1 | |
if unique != 0: | |
if unique < 0x39: | |
if unique <= 5: | |
print("Rocket failed") | |
gs.score += 1 | |
return (gs, 0) | |
else: | |
print("Rocket launched") | |
gs.score += unique | |
else: | |
print("JACKPORT (sputnik)") | |
gs.score += 250 | |
gs.sr_bonus += 20 | |
gs.ball_loc = gs.advance_rng() % 100 | |
print("Doing seccomp crap") | |
def roswell(gs: GameState): | |
curr_char = gs.memory[gs.addr + 5] | |
read_str = [0x20 for i in range(32)] | |
for i in range(32): | |
read_byte = gs.memory[gs.addr + curr_char] | |
if read_byte == 0: | |
print("Trying to compare secret base to " + bytes(read_str).decode("ascii")) | |
break | |
read_str[i] = read_byte | |
if read_byte % 2 == 1: | |
curr_char -= read_byte | |
else: | |
curr_char += read_byte | |
if curr_char > gs.len: | |
print("Bad ball :(") | |
return (gs, 1) | |
gs.ball_loc = gs.advance_rng() % 100 | |
def main_switch(gs: GameState): | |
if (gs.command[0] == 1): | |
res = rocket_launch(gs) | |
if res is not None: | |
return res | |
return main_loop(gs) | |
raise Exception("Thought this was unreachable") | |
# todo (the rest???) | |
def rng_then_main(gs: GameState): | |
ball_loc = gs.advance_rng() % 100 | |
main_loop(gs) | |
def main_loop(gs: GameState): | |
gs.sr_bonus += 10 | |
gs.next() | |
return main_loop_continue_entry(gs) | |
def main_loop_continue(gs: GameState): | |
gs.next() | |
return main_loop_continue_entry(gs) | |
def main_loop_continue_entry(gs: GameState): | |
if gs.cmd != 0: | |
print(gs) | |
if gs.ball == 0xFF or gs.command_idx - 1 >= 0x64: | |
# go to end | |
print("Rocket landed!") | |
gs.score += gs.sr_bonus | |
gs.sr_bonus = 0 | |
return (gs, 0) | |
cmd = gs.command[0] | |
if cmd == 0: | |
return main_loop_continue(gs) | |
if cmd == 1 and gs.ball != 0xAA: | |
gs.die() | |
return main_loop_continue(gs) | |
if cmd == 3: | |
if gs.ball_loc > 0x64: | |
return main_loop_continue(gs) | |
if gs.ball_loc > 0x23 and gs.ball_loc <= 0x40: | |
print("MISS") | |
gs.die() | |
return main_loop_continue(gs) | |
if gs.ball_loc > 0x40: | |
print("WRONG FLIPPER") | |
gs.die() | |
if cmd == 4: | |
if gs.ball_loc > 0x64: | |
return main_loop_continue(gs) | |
if gs.ball_loc > 0x23 and gs.ball_loc <= 0x40: | |
print("MISS") | |
gs.die() | |
return main_loop_continue(gs) | |
if gs.ball_loc <= 0x22: | |
print("WRONG FLIPPER") | |
gs.die() | |
return main_loop_continue(gs) | |
if gs.command[2] > 0x3e7: | |
print("Bad ball") | |
return (gs, -1) | |
if cmd == 1: | |
res = rocket_launch(gs) | |
if res is not None: | |
return res | |
elif cmd == 2: | |
tilt_spend = twos_byte(gs.memory[gs.addr]) or 1 | |
tilt_amt = twos_byte(gs.memory[gs.addr + 1]) | |
if gs.tilt_points + tilt_amt > 99: | |
print("TILT") | |
return (gs, 0) | |
rand_val = gs.advance_rng() % 100 | |
if rand_val < tilt_spend: | |
if gs.ball > 0x31 and gs.ball <= 0x64: | |
gs.ball_loc = min(gs.ball + tilt_amt, 90) | |
else: | |
gs.ball_loc = max(gs.ball - tilt_amt, 10) | |
gs.tilt_points += tilt_spend | |
elif gs.ball > 0x23 and gs.ball <= 0x4A: | |
print("Tilt failed") | |
gs.ball_loc = 0xFF | |
gs.sr_bonus -= 10 | |
elif cmd == 4: | |
print("testing command") | |
read_data = gs.read(gs.addr, 4) | |
print(read_data) | |
if read_data == b"QQQQ": | |
res = roswell(gs) | |
if res is not None: | |
return res | |
elif read_data == b"\xFF\x00\x00\x00": | |
ball_step = 3 * (gs.ball - 65) | |
buffer_offset = 4 | |
accumulator = 1 | |
if gs.mission_available: | |
for i in range(10): | |
data_bytes = gs.read(gs.addr + buffer_offset, 2) | |
accumulator *= struct.unpack("<H", data_bytes) | |
accumulator &= 0xFFFFFFFF | |
buffer_offset += ball_step | |
if accumulator == 0x97d7cfc0: | |
if gs.active_mission == 1: | |
print("Mission 1!") | |
gs.score += 200 | |
if gs.active_mission == 2: | |
print("Mission 2") | |
gs.score += 400 | |
if gs.active_mission == 3: | |
print("Mission 3") | |
gs.score += 1000 | |
gs.ball_loc = 0 | |
gs.mission_available = 0 | |
else: | |
print("Mission wrong") | |
ball_loc = gs.advance_rng() % 100 | |
elif read_data == b"\x00\x01`P": | |
print(gs.len, gs.addr, gs.read(gs.addr + 4, 4)) | |
if ( | |
gs.len == 520 and | |
gs.addr != 0 and | |
gs.addr != 0xa2a and | |
gs.read(gs.addr + 4, 4) == b"\0\0\0\0" | |
): | |
print("bumpa") | |
bumper_counter = 0 | |
for i in range(512): | |
if gs.memory[gs.addr + 8 + i] == 0: | |
return rng_then_main(gs) | |
if gs.memory[gs.addr + 8 + i] == gs.ball: | |
bumper_counter += 1 | |
print(bumper_counter, gs.ball) | |
if bumper_counter == 100: | |
print("We're doing crap with some stack pointer and maybe crashing") | |
gs.score += gs.advance_rng() % 100 | |
print("Bumper!") | |
bumpers = 1 | |
while ((gs.advance_rng() % 100) > 0x31): | |
print("Combo bumper!") | |
gs.score += gs.advance_rng() % 100 | |
bumpers += 1 | |
if bumpers > 4: | |
print("Jackpot bumper!") | |
gs.score += 800 | |
return rng_then_main(gs) | |
elif cmd == 3: | |
read_data = gs.read(gs.addr, 4) | |
if read_data == b"QQQQ": | |
res = roswell(gs) | |
if res is not None: | |
return res | |
elif read_data == b"\x04\x03\x02\x01": | |
copy_buffer = "All Systems GO" | |
print(gs.mission_available, gs.len) | |
if gs.mission_available == 0 and gs.len <= 0x100: | |
gs.assign({ "addr": gs.addr + gs.len, "value": b"\0" * 16 }) | |
rng_state = gs.rng_state | |
rng_buffer = bytes([gs.advance_rng() % 255 for i in range(100)]) | |
gs.assign({ "addr": gs.addr + gs.len + 16, "value": rng_buffer }) | |
gs.rng_state = rng_state | |
i = 0 | |
while 3 * i < gs.len: | |
mem_1, = struct.unpack("<H", gs.read(gs.addr + 3 * i + 4, 2)) | |
print(hex(mem_1), gs.read(gs.addr + mem_1, 10)) | |
gs.assign({ | |
"addr": gs.addr + gs.len + gs.memory[gs.addr + 3 * i + 6], | |
"value": bytes([gs.memory[gs.addr + mem_1], 0]) | |
}) | |
i += 1 | |
print(gs.read(gs.addr + gs.len, 12)) | |
if gs.read(gs.addr + gs.len, 12) == copy_buffer: | |
gs.active_mission += 1 | |
if gs.active_mission <= 3: | |
print(f"Active mission: {gs.active_mission}") | |
gs.sr_bonus += 15 | |
gs.mission_available = 1 | |
else: | |
print("Mission jackpot") | |
gs.score += 500 | |
gs.mission_available = 1 | |
gs.ball_loc = gs.advance_rng() | |
elif read_data == b"P`p\x80": | |
if gs.ball == 0: | |
gs.sr_bonus += 5 | |
return main_loop(gs) | |
if gs.memory[gs.addr + 4] == 0x65: | |
if gs.cold_war_bonus == 1: | |
return rng_then_main(gs) | |
for i in range(4, gs.ball + 14): | |
if gs.len < i or gs.memory[gs.addr + i] != 0x65: | |
return rng_then_main(gs) | |
gs.cold_war_bonus = 1 | |
print("Cold war x 2") | |
# ??? | |
elif gs.memory[gs.addr + 4] == 0x66: | |
if gs.cold_war_bonus == 0 or gs.watching_bonus == 1: | |
return rng_then_main(gs) | |
for i in range(10 * gs.ball): | |
if i + 4 > gs.len or gs.memory[gs.addr + i + 4] != 0x66: | |
return rng_then_main(gs) | |
gs.watching_bonus = 1 | |
print("Wathcing bonsus!!!!") | |
elif gs.memory[gs.addr + 4] == 0x77: | |
if gs.cold_war_bonus == 0 or gs.watching_bonus == 0 or gs.sold_the_moon_bonus == 1: | |
return rng_then_main(gs) | |
for i in range(10 ** gs.ball): | |
if i + 4 > gs.len or gs.memory[gs.addr + i + 4] != 0x67: | |
return rng_then_main(gs) | |
gs.sold_the_moon_bonus = 1 | |
print("Sold the moon!!!!!!!!!!!1") | |
gs.sr_bonus += 5 | |
return rng_then_main(gs) | |
gs.sr_bonus += 10 | |
return main_loop_continue(gs) | |
# intial_state = GameState([ | |
# cmd_from_bytes(b"BALL\x0a\x00\x64\x00\x00\x00"), | |
# [1, 1010, 11], | |
# *([[5, 0, 0]] * 99), | |
# ], | |
# [ | |
# { "addr": 1010, "value": b"secret_base" } | |
# ] | |
# ) | |
initial_state = GameState.from_file("boll") | |
with open("./test", "wb+") as f: | |
f.write(initial_state.memory) | |
print(entry(initial_state)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment