Last active
February 1, 2019 21:12
-
-
Save jamesu/79351df33ef07d791664f07a49b1259b to your computer and use it in GitHub Desktop.
Decrypt Onverse script files
This file contains hidden or 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
# 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