HITCON 2016 - ROP

ROP is a simple Ruby VM crackme.
We are given a file rop.iseq, without any other detail.

$ file rop.iseq
rop.iseq: data

Opening it with an hex editor, we see that it starts with YARB, and from its
contents we can guess that it is a sort of bytecode format. With Google’s help,
we come across Ruby’s RubyVM::InstructionSequence class, and in particular
the load_from_binary method, introduced in Ruby 2.3.
Its purpose is “to load an iseq object from binary format String object
created by #to_binary“… and from Ruby’s source code (compile.c),
we see that the binary file it loads must start with the bytes YARB…
so it really seems that we are dealing with serialized Ruby bytecode!

We can simply load and execute this file with
RubyVM::InstructionSequence.
I had to download the Ruby interpreter from rvm, as the one coming with
Ubuntu 16.04 had a value of the constant RUBY_PLATFORM slightly different
than the one of the iseq file, causing a unmatched platform error when
loading the iseq.

The challenge waits for some input; trying to enter a string, it outputs
Invalid Key @_@: we need to figure out a valid input.
We can also dump the
disassembly:

iseq.disasm

We could not find (quickly) much documentation about the Ruby virtual
machine, or tools working with the iseq format, so we resorted to guess the
behavior of the various opcodes (it is a simple stack-based VM) to understand
what the program is doing.

Fortunately, the bytecode contains various trace instructions; we made use of
set_trace_func to print those events during the program execution. This way,
we could understand at which line of code the program was jumping to the function
gg - and, thus, what components of our input string were correct!

From the disassembly, we see that the input is split into 5 substrings
separated by '-'. Each of them has to match the regexp
/^[0-9A-F]{4}$/, and is checked by a separate block of code. As soon as a
substring does not satisfy a condition, the program calls the function gg,
which outputs "Invalid Key @_@" and exits. Thus, we have to pass five checks,
one for each substring.

Just to give an idea of how the disasm looked like, here is the first check:

This Python script took 0.263s on my laptop to find the correct string.
Finally, for the last check, we noticed that
947d46f8060d9d7025cc5807ab9bf1b3b9143304 is the SHA-1 of 5671, so we get the
last component of the input string by just XOR-ing the first four ones with

The complete input string is

7A69-ECAF-1BD2-5141-CA72

which gives us the output

Congratz! flag is hitcon{ROP = Ruby Obsecured Programming ^_<}

For completeness, here is the complete Ruby source code of the crackme - as we
were able to manually reverse: