This repository has been archived on 2017-10-30. You can view files and clone it, but cannot push or open issues or pull requests.
synacor.py/disasm.py

268 lines
8.7 KiB
Python
Raw Normal View History

2017-02-12 23:31:56 +11:00
#!/usr/bin/env python3
# synacor.py - An implementation of the Synacor Challenge
# Copyright © 2017 RunasSudo
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from libsynacor import *
2017-02-13 15:14:36 +11:00
import argparse
2017-02-12 23:31:56 +11:00
2017-02-13 15:14:36 +11:00
parser = argparse.ArgumentParser()
parser.add_argument('file', help='.bin file containing the initial memory dump')
2017-02-13 19:36:53 +11:00
parser.add_argument('--hints', help='File(s) outlining additional jmp/call targets, label names, comments, etc', action='append')
2017-02-13 22:30:14 +11:00
parser.add_argument('--smart', help='Given a raw Synacor challenge file, disassemble in a Synacor-aware manner', action='store_true')
parser.add_argument('--aggressive-labels', help='Replace values with corresponding labels irrespective of where they appear', action='store_true')
2017-02-13 15:14:36 +11:00
args = parser.parse_args()
with open(args.file, 'rb') as data:
2017-02-12 23:31:56 +11:00
SYN_MEM = memory_from_file(data)
2017-02-13 22:30:14 +11:00
disassemble_end = len(SYN_MEM)
2017-02-12 23:31:56 +11:00
labels, comments_before, comments_inline, replacements, strings = {}, {}, {}, {}, []
2017-02-13 22:30:14 +11:00
# Do smart things if requested
if args.smart:
disassemble_end = 0x17b4
# Emulate 06bb to decrypt data
for R1 in range(disassemble_end, 0x7562):
R0 = SYN_MEM[R1]
R0 ^= pow(R1, 2, 32768)
R0 ^= 0x4154
SYN_MEM[R1] = R0
# Find things to label
2017-02-13 19:36:53 +11:00
SYN_PTR = 0
2017-02-13 22:30:14 +11:00
while SYN_PTR < disassemble_end:
2017-02-13 19:36:53 +11:00
word = SYN_MEM[SYN_PTR]
if word in instructions_by_opcode:
instruction, SYN_PTR = Instruction.next_instruction(SYN_MEM, SYN_PTR)
if isinstance(instruction, InstructionJmp) or isinstance(instruction, InstructionJt) or isinstance(instruction, InstructionJf):
if isinstance(instruction, InstructionJmp):
op = instruction.args[0]
else:
op = instruction.args[1]
if isinstance(op, OpLiteral):
loc = op.get(None)
labels['label_{:04x}'.format(loc)] = loc
elif isinstance(instruction, InstructionCall):
if isinstance(instruction.args[0], OpLiteral):
loc = instruction.args[0].get(None)
labels['sub_{:04x}'.format(loc)] = loc
else:
SYN_PTR += 1
# Read hints
if args.hints:
for hintfile in args.hints:
with open(hintfile, 'r') as data:
while True:
line = data.readline()
if line == '':
break
if line.startswith('jmp '):
loc = int(line.split()[1], 16)
labels['label_{:04x}'.format(loc)] = loc
elif line.startswith('call '):
loc = int(line.split()[1], 16)
labels['sub_{:04x}'.format(loc)] = loc
2017-02-13 21:25:08 +11:00
elif line.startswith('lbl '):
loc = int(line.split()[1], 16)
labels[line.split()[2]] = loc
2017-02-13 19:36:53 +11:00
elif line.startswith('ren '):
old_label = line.split()[1]
new_label = line.split()[2]
labels[new_label] = labels[old_label]
del labels[old_label]
elif line.startswith('cmb '):
loc = int(line.split()[1], 16)
comment = line[line.index(' ', line.index(' ') + 1) + 1:].strip()
if loc not in comments_before:
comments_before[loc] = []
comments_before[loc].append(comment)
2017-02-13 21:25:08 +11:00
elif line.startswith('cmi '):
loc = int(line.split()[1], 16)
comment = line[line.index(' ', line.index(' ') + 1) + 1:].strip()
comments_inline[loc] = comment
elif line.startswith('rep '):
loc = int(line.split()[1], 16)
code = line[line.index(' ', line.index(' ') + 1) + 1:].strip()
instruction = assemble_line(None, code)[0][0]
replacements[loc] = instruction
elif line.startswith('del '):
loc = int(line.split()[1], 16)
replacements[loc] = None
elif line.startswith('str '):
loc = int(line.split()[1], 16)
strings.append(loc)
2017-02-13 19:36:53 +11:00
else:
raise Exception('Invalid line in hint file: {}'.format(line))
2017-02-12 23:31:56 +11:00
MODE_OUT = False
MODE_DAT = False #False, 1 (data), 2 (text), 3 (unknown string), 4 (data string), 5 (text string)
str_ctr = None
2017-02-12 23:31:56 +11:00
SYN_PTR = 0
2017-02-13 22:30:14 +11:00
def set_mode_out(mode):
global MODE_OUT
if MODE_OUT == mode:
pass
elif mode == False:
# Switching off
print('"')
else:
# Switching on
print('{:04x}: out "'.format(SYN_PTR), end='')
MODE_OUT = mode
def set_mode_dat(mode):
global MODE_DAT
if MODE_DAT == mode:
pass
elif mode == False:
# Switching off
if MODE_DAT == 2 or MODE_DAT == 5:
2017-02-13 22:30:14 +11:00
print('"', end='')
print()
elif MODE_DAT == 1:
# Switching from data to text
print()
print('{:04x}: data "'.format(SYN_PTR), end='')
elif MODE_DAT == 2:
# Switching from text to data
print('"')
print('{:04x}: data'.format(SYN_PTR), end='')
elif mode == 4:
# Detected data string
pass
elif mode == 5:
# Detected text string
print(' "', end='')
elif mode == 1 or mode == 2:
2017-02-13 22:30:14 +11:00
# Switching to a new mode
print('{:04x}: data'.format(SYN_PTR), end='')
if mode == 2:
print('"', end='')
elif mode == 3:
# Switching to a new string mode
print('{:04x}: str'.format(SYN_PTR), end='')
2017-02-13 22:30:14 +11:00
MODE_DAT = mode
def clear_modes():
set_mode_out(False)
set_mode_dat(False)
2017-02-12 23:31:56 +11:00
while SYN_PTR < len(SYN_MEM):
2017-02-13 21:25:08 +11:00
# Handle comments
2017-02-13 19:36:53 +11:00
if SYN_PTR in comments_before:
2017-02-13 22:30:14 +11:00
clear_modes()
2017-02-13 19:36:53 +11:00
for comment in comments_before[SYN_PTR]:
print('; {}'.format(comment))
2017-02-13 21:25:08 +11:00
if SYN_PTR in comments_inline:
comment_inline = ' ; {}'.format(comments_inline[SYN_PTR])
else:
comment_inline = ''
# Handle labels
2017-02-13 19:36:53 +11:00
if any(v == SYN_PTR for k, v in labels.items()):
2017-02-13 22:30:14 +11:00
clear_modes()
2017-02-13 19:36:53 +11:00
print('${}:'.format(next(k for k, v in labels.items() if v == SYN_PTR)))
2017-02-13 21:25:08 +11:00
# Handle replacements
if SYN_PTR in replacements:
instruction = replacements[SYN_PTR]
if instruction is not None:
print('{:04x}: {}{}'.format(SYN_PTR, instruction.describe(), comment_inline))
SYN_PTR += len(instruction.assemble(None))
2017-02-13 21:25:08 +11:00
continue
2017-02-12 23:31:56 +11:00
word = SYN_MEM[SYN_PTR]
if SYN_PTR in strings:
# String length
set_mode_dat(3)
str_ctr = word
SYN_PTR += 1
elif MODE_DAT == 3:
# Detect string type
if 32 <= word <= 126:
set_mode_dat(5)
print(escape_char(chr(word)), end='')
else:
set_mode_dat(4)
print(' {:04x}'.format(word), end='')
SYN_PTR += 1
str_ctr -= 1
if str_ctr <= 0:
set_mode_dat(False)
elif MODE_DAT == 4 or MODE_DAT == 5:
# String
if MODE_DAT == 4:
print(' {:04x}'.format(word), end='')
else:
print(escape_char(chr(word)), end='')
SYN_PTR += 1
str_ctr -= 1
if str_ctr <= 0:
set_mode_dat(False)
elif SYN_PTR >= disassemble_end or word not in instructions_by_opcode:
2017-02-12 23:31:56 +11:00
# Data
if 32 <= word <= 126:
2017-02-13 22:30:14 +11:00
# Looks like letters - unfortunately "\n" looks like MULT
set_mode_dat(2)
print(escape_char(chr(word)), end='')
2017-02-12 23:31:56 +11:00
if word == 0x0a:
2017-02-13 22:30:14 +11:00
clear_modes() # Break on newlines
2017-02-12 23:31:56 +11:00
else:
2017-02-13 22:30:14 +11:00
set_mode_dat(1)
print(' {:04x}'.format(word), end='')
2017-02-12 23:31:56 +11:00
SYN_PTR += 1
else:
# Instruction
2017-02-13 19:36:53 +11:00
instruction, next_SYN_PTR = Instruction.next_instruction(SYN_MEM, SYN_PTR)
2017-02-12 23:31:56 +11:00
# Special cases
if isinstance(instruction, InstructionOut):
if isinstance(instruction.args[0], OpLiteral):
2017-02-13 22:30:14 +11:00
set_mode_out(True)
print(escape_char(chr(instruction.args[0].get(None))), end='')
2017-02-12 23:31:56 +11:00
if instruction.args[0].get(None) == 0x0a:
2017-02-13 22:30:14 +11:00
clear_modes() # Break on newlines
2017-02-12 23:31:56 +11:00
else:
2017-02-13 22:30:14 +11:00
clear_modes()
2017-02-13 21:25:08 +11:00
print('{:04x}: {}{}'.format(SYN_PTR, instruction.describe(), comment_inline))
2017-02-13 19:36:53 +11:00
elif isinstance(instruction, InstructionJmp) or isinstance(instruction, InstructionJt) or isinstance(instruction, InstructionJf) or isinstance(instruction, InstructionCall):
2017-02-13 22:30:14 +11:00
clear_modes()
2017-02-13 19:36:53 +11:00
if isinstance(instruction, InstructionJmp) or isinstance(instruction, InstructionCall):
2017-02-13 21:25:08 +11:00
argidx = 0
2017-02-13 19:36:53 +11:00
else:
2017-02-13 21:25:08 +11:00
argidx = 1
2017-02-13 22:30:14 +11:00
# Aggressively replace labels if requested
for argnum in range(instruction.nargs) if args.aggressive_labels else [argidx]:
if isinstance(instruction.args[argnum], OpLiteral):
loc = instruction.args[argnum].get(None)
if any(v == loc for k, v in labels.items()):
label = next(k for k, v in labels.items() if v == loc)
instruction.args[argnum] = OpLabel(label)
2017-02-13 21:25:08 +11:00
print('{:04x}: {}{}'.format(SYN_PTR, instruction.describe(), comment_inline))
2017-02-12 23:31:56 +11:00
else:
2017-02-13 22:30:14 +11:00
# Aggressively replace labels if requested
if args.aggressive_labels:
for argnum in range(instruction.nargs):
if isinstance(instruction.args[argnum], OpLiteral):
loc = instruction.args[argnum].get(None)
if any(v == loc for k, v in labels.items()):
label = next(k for k, v in labels.items() if v == loc)
instruction.args[argnum] = OpLabel(label)
clear_modes()
2017-02-13 21:25:08 +11:00
print('{:04x}: {}{}'.format(SYN_PTR, instruction.describe(), comment_inline))
2017-02-13 19:36:53 +11:00
SYN_PTR = next_SYN_PTR