Skip to content

Instantly share code, notes, and snippets.

@jamesu
Last active February 1, 2019 21:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jamesu/79351df33ef07d791664f07a49b1259b to your computer and use it in GitHub Desktop.
Save jamesu/79351df33ef07d791664f07a49b1259b to your computer and use it in GitHub Desktop.
Decrypt Onverse script files
# Decrypts and prints a rudimentary decompilation of onverse script files
# Usage: ruby decrypt_onverse.rb in.cs
#
require 'json'
$is_encrypted = true
def decrypt_data(size, data)
if !$is_encrypted
return data
end
bytes = data.unpack('C*')
size.times do |i|
edx = size - i
if i >= edx
break
end
start_byte = bytes[i]
end_byte = bytes[size-i-1]
bytes[i] = end_byte ^ 0xCF
bytes[size-i-1] = start_byte ^ 0xCF
end
return bytes.pack('C*')
end
def unpack_s(data, offset)
data[offset...(data.length)].unpack('Z*')[0]
end
# name, size (besides first opcode)
$opcode_list = {
:OP_FUNC_DECL => 6,
:OP_CREATE_OBJECT => 3,
:OP_ADD_OBJECT => 1,
:OP_END_OBJECT => 1,
:OP_JMPIFFNOT => 1,
:OP_JMPIFNOT => 1,
:OP_JMPIFF => 1,
:OP_JMPIF => 1,
:OP_JMPIFNOT_NP => 1,
:OP_JMPIF_NP => 1,
:OP_JMP => 1,
:OP_RETURN => 0,
:OP_CMPEQ => 0,
:OP_CMPGR => 0,
:OP_CMPGE => 0,
:OP_CMPLT => 0,
:OP_CMPLE => 0,
:OP_CMPNE => 0,
:OP_XOR => 0,
:OP_MOD => 0,
:OP_BITAND => 0,
:OP_BITOR => 0,
:OP_NOT => 0,
:OP_NOTF => 0,
:OP_ONESCOMPLEMENT => 0,
:OP_SHR => 0,
:OP_SHL => 0,
:OP_AND => 0,
:OP_OR => 0,
:OP_ADD => 0,
:OP_SUB => 0,
:OP_MUL => 0,
:OP_DIV => 0,
:OP_NEG => 0,
:OP_SETCURVAR => 1,
:OP_SETCURVAR_CREATE => 1,
:OP_SETCURVAR_ARRAY => 0,
:OP_SETCURVAR_ARRAY_CREATE => 0,
:OP_LOADVAR_UINT => 0,
:OP_LOADVAR_FLT => 0,
:OP_LOADVAR_STR => 0,
:OP_SAVEVAR_UINT => 0,
:OP_SAVEVAR_FLT => 0,
:OP_SAVEVAR_STR => 0,
:OP_SETCUROBJECT => 0,
:OP_SETCUROBJECT_NEW => 0,
:OP_SETCURFIELD => 1,
:OP_SETCURFIELD_ARRAY => 0,
:OP_LOADFIELD_UINT => 0,
:OP_LOADFIELD_FLT => 0,
:OP_LOADFIELD_STR => 0,
:OP_SAVEFIELD_UINT => 0,
:OP_SAVEFIELD_FLT => 0,
:OP_SAVEFIELD_STR => 0,
:OP_STR_TO_UINT => 0,
:OP_STR_TO_FLT => 0,
:OP_STR_TO_NONE => 0,
:OP_FLT_TO_UINT => 0,
:OP_FLT_TO_STR => 0,
:OP_FLT_TO_NONE => 0,
:OP_UINT_TO_FLT => 0,
:OP_UINT_TO_STR => 0,
:OP_UINT_TO_NONE => 0,
:OP_LOADIMMED_UINT => 1,
:OP_LOADIMMED_FLT => 1,
:OP_TAG_TO_STR => 1,
:OP_LOADIMMED_STR => 1,
:OP_LOADIMMED_IDENT => 1,
:OP_CALLFUNC_RESOLVE => 3,
:OP_CALLFUNC => 3,
:OP_ADVANCE_STR => 0,
:OP_ADVANCE_STR_APPENDCHAR => 1,
:OP_ADVANCE_STR_COMMA => 0,
:OP_ADVANCE_STR_NUL => 0,
:OP_REWIND_STR => 0,
:OP_TERMINATE_REWIND_STR => 0,
:OP_COMPARE_STR => 0,
:OP_PUSH => 0,
:OP_PUSH_FRAME => 0,
:OP_BREAK => 0,
:OP_INVALID => 0
}
def parse_script(file)
version = file.read(4).unpack('L<')[0]
global_string_size = file.read(4).unpack('L<')[0]
global_string_data = file.read(global_string_size)
global_string_data = decrypt_data(global_string_size, global_string_data)
function_string_size = file.read(4).unpack('L<')[0]
function_string_data = file.read(function_string_size)
function_string_data = decrypt_data(function_string_size, function_string_data)
#puts global_string_data.inspect
#puts function_string_data.inspect
float_data_size = file.read(4).unpack('L<')[0]
floats = file.read(float_data_size*8).unpack('E*')
function_float_data_size = file.read(4).unpack('L<')[0]
function_floats = file.read(function_float_data_size*8).unpack('E*')
code_size, line_break_count = file.read(8).unpack('L<L<')
#puts "sz=#{code_size} break=#{line_break_count}"
tot_size = code_size + (line_break_count * 2)
#puts "EEEE"
#puts tot_size
code = []
code_size.times do
codepoint = file.read(1).unpack('C')[0]
if codepoint == 0xFF
#puts "0xFF at #{file.tell-1}"
codepoint = file.read(4).unpack('L<')[0]
end
code << codepoint
end
if code.length != code_size
raise Exception.new("Code size mismatch")
end
#puts file.tell
#puts "Line break pairs: #{tot_size - code_size}"
linebreak_pairs = file.read(line_break_count*2*4).unpack('L<')
#puts "EOL #{file.tell}"
# Read patch list for STE
ident_count = file.read(4).unpack('L<')[0]
#puts "#{ident_count} patch items"
ident_count.times do
offset, count = file.read(8).unpack('L<L<')
ste = offset < global_string_size ? unpack_s(global_string_data, offset) : nil
#puts "STE #{ste} count #{count}"
patch_list = file.read(count*4).unpack('L<*')
patch_list.each do |ip|
code[ip] = ste
end
end
if !file.eof?
raise Exception.new("File longer than expected")
end
return {:version => version,
:strings => global_string_data,
:floats => floats,
:function_strings => function_string_data,
:function_floats => function_floats,
:code => code,
:linebreak_pairs => linebreak_pairs}
end
def redump_script(dat, file)
file.write([dat[:version]].pack('L<'))
file.write([dat[:strings].length].pack('L<'))
file.write(dat[:strings])
file.write([dat[:function_strings].length].pack('L<'))
file.write(dat[:function_strings])
# TODO...
raise Exception.new("TODO")
end
def lookup_name(script, ofs)
script[:ident_table][ofs]
end
def resolve_op_vals(opcodes, string_table, float_table)
opcodes.each do |dat|
pos = dat[0]
op = dat[1]
params = dat[2]
case op
when :OP_LOADIMMED_STR, :OP_TAG_TO_STR
if params[0].is_a?(Integer)
params[0] = unpack_s(string_table, params[0])
end
when :OP_LOADIMMED_FLT
params[0] = float_table[params[0]]
end
end
end
def friendly_print_ops(ops)
ops.each do |dat|
puts "loc_#{dat[0]}: #{dat[1]} #{dat[2].map(&:inspect).join(" ")}"
end
end
def dump_bytecode(script)
opcodes = script[:code].clone
op_ofs = 0
#puts opcodes.inspect
out_opcodes = []
function_lookup = []
puts script[:ident_table].inspect
while !opcodes.empty?
s = opcodes.first
opcodes = opcodes.drop(1)
op_ofs += 1
#puts "OP #{s}"
op_name = $opcode_list.keys[s]
if op_name.nil?
raise Exception.new("Unrecognized opcode #{s}")
end
len = $opcode_list[op_name]
if op_name == :OP_FUNC_DECL
argc = opcodes[5]
len += argc
end
#puts "@#{op_ofs}: OP #{s} #{op_name} LEN #{len}"
out_opcodes << [op_ofs-1, op_name, opcodes[0...(len)]]
op_ofs += len
if op_name == :OP_FUNC_DECL
function_lookup << out_opcodes.last
end
opcodes = opcodes.drop(len)
end
# Group function blocks
# Output list: [group ip, instructions]
grouped_list = out_opcodes.group_by do |g|
in_func = nil
function_lookup.each do |f|
offset = f[0]
end_offset = f[2][4]
if (g[0] >= offset) && (g[0] < end_offset)
in_func = f
break
end
end
if in_func.nil?
g[0]
else
in_func[0]
end
end
nice_grouped_list = []
grouped_list.each do |l|
#puts nice_grouped_list.inspect
pos = l[0]
ops = l[1]
if ops[0][1] == :OP_FUNC_DECL
args = ops[0][2]
nice_grouped_list << [:function, pos, args[2], args[1], args[0], args[6...args.length], ops[1...ops.length]]
else
last_group = nice_grouped_list.last
if last_group.nil? or (last_group[0] == :function)
nice_grouped_list << [:code, pos, ops]
else
last_group[2] += ops
end
end
end
# Now we have the stuff, resolve string for certain ops
nice_grouped_list.each do |l|
if l[0] == :function
resolve_op_vals(l.last, script[:function_strings], script[:function_floats])
else
resolve_op_vals(l.last, script[:strings], script[:floats])
end
end
# Print a nice friendly rep of it
nice_grouped_list.each do |l|
if l[0] == :function
puts ""
pkg_str = ""
if l[2] != 0
pkg_str = " @package #{l[2]}"
end
arg_str = l[5].join(", ")
if l[3] != 0
puts "function #{l[3]}::#{l[4]}(#{arg_str})#{pkg_str}"
else
puts "function #{l[4]}(#{arg_str})#{pkg_str}"
end
puts "{"
friendly_print_ops(l.last)
puts "}"
puts ""
else
friendly_print_ops(l.last)
end
end
true
end
File.open(ARGV[0]) do |f|
data = parse_script(f)
dump_bytecode(data)
#File.open(ARGV[1], 'wb') do |f2|
# redump_script(data, f2)
#end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment