###instr_X
Another simple one, rename local vars anyways!
:> afvn local_1 arg1
This function XORs the value at sym.current_memory_ptr with arg1.
###instr_J
This one is not as simple as the previous ones, but it's not that complicated either. Since I'm obviously obsessed with variable renaming:
:> afvn local_3 arg1
:> afvn local_0_4 arg1_and_0x3f
After the result of arg1 & 0x3f is put into a local variable, arg1 & 0x40 is checked against 0. If it isn't zero, arg1_and_0x3f is negated:
The next branching: if arg1>= 0, then the function returns arg1_and_0x3f,
else the function branches again, based on the value of sym.written_by_instr_C:
If it is zero, the function returns 2,
else it is checked if arg1_and_0x3f is a negative number,
and if it is, sym.good_if_ne_zero is incremented by 1:
After all this, the function returns with arg1_and_0x3f:
.instructionset
We've now reversed all the VM instructions, and have a full understanding about how it works. Here is the VM's instruction set:
Instruction | 1st arg | 2nd arg | What does it do? |
---|---|---|---|
"A" | "M" | arg2 | *sym.current_memory_ptr += arg2 |
"P" | arg2 | sym.current_memory_ptr += arg2 | |
"C" | arg2 | sym.written_by_instr_C += arg2 | |
"S" | "M" | arg2 | *sym.current_memory_ptr -= arg2 |
"P" | arg2 | sym.current_memory_ptr -= arg2 | |
"C" | arg2 | sym.written_by_instr_C -= arg2 | |
"I" | arg1 | n/a | instr_A(arg1, 1) |
"D" | arg1 | n/a | instr_S(arg1, 1) |
"P" | arg1 | n/a | *sym.current_memory_ptr = arg1; instr_I("P") |
"X" | arg1 | n/a | *sym.current_memory_ptr ^= arg1 |
"J" | arg1 | n/a | arg1_and_0x3f = arg1 & 0x3f; if (arg1 & 0x40 != 0) arg1_and_0x3f *= -1 if (arg1 >= 0) return arg1_and_0x3f; else if (*sym.written_by_instr_C != 0) { if (arg1_and_0x3f < 0) ++*sym.good_if_ne_zero; return arg1_and_0x3f; } else return 2; |
"C" | arg1 | n/a | *sym.written_by_instr_C = arg1 |
"R" | arg1 | n/a | return(arg1) |
.bytecode
Well, we did the reverse engineering part, now we have to write a program for the VM with the instruction set described in the previous paragraph. Here is the program's functional specification:
• the program must return "*"
• sym.memory has to contain the string "Such VM! MuCH reV3rse!" after execution
• all 9 instructions have to be used at least once
• sym.good_if_ne_zero should not be zero
• instr_P is not allowed to be used more than 9 times
Since this document is about reversing, I'll leave the programming part to the fellow reader :) But I'm not going to leave you empty-handed, I'll give you one advice: Except for "J", all of the instructions are simple, easy to use, and it should not be a problem to construct the "Such VM! MuCH reV3rse!" using them. "J" however is a bit complicated compared to the others. One should realize that its sole purpose is to make sym.good_if_ne_zero bigger than zero, which is a requirement to access the flag. In order to increment sym.good_if_ne_zero, three conditions should be met:
• arg1 should be a negative number, otherwise we would return early
• sym.written_by_instr_C should not be 0 when "J" is called. This means that "C", "AC", or "SC" instructions should be used before calling "J".
• arg1_and_0x3f should be negative when checked. Since 0x3f's sign bit is zero, no matter what arg1 is, the result of arg1& 0x3f will always be non-negative. But remember that "J" negates arg1_and_0x3f if arg1& 0x40 is not zero. This basically means that arg1's 6th bit should be 1 (0x40 = 01000000b). Also, because arg1_and_0x3f can't be 0 either, at least one of arg1's 0th, 1st, 2nd, 3rd, 4th or 5th bits should be 1 (0x3f = 00111111b).
I think this is enough information, you can go now and write that program. Or, you could just reverse engineer the quick'n'dirty one I've used during the CTF:
\x90\x00PSAMuAP\x01AMcAP\x01AMhAP\x01AM
AP\x01AMVAP\x01AMMAP\x01AM!AP\x01AM
AP\x01AMMAP\x01AMuAP\x01AMCAP\x01AMHAP\x01AM
AP\x01AMrAP\x01AMeAP\x01AMVAP\x01AM3AP\x01AMrAP\x01AMsAP\x01AMeIPAM!X\x00CAJ\xc1SC\x00DCR*
Keep in mind though, that it was written on-the-fly, parallel to the reversing phase - for example there are parts that was written without the knowledge of all possible instructions. This means that the code is ugly and unefficient.
.outro
Well, what can I say? Such VM, much reverse! :)
What started out as a simple writeup for a simple crackme, became a rather lengthy writeup/r2 tutorial, so kudos if you've read through it. I hope you enjoyed it (I know I did), and maybe even learnt something from it. I've surely learnt a lot about r2 during the process, and I've even contributed some small patches, and got a few ideas of more possible improvements.
Radare2 Reference Card
This chapter is based on the Radare 2 reference card by Thanat0s, which is under the GNU GPL. Original license is as follows:
This card may be freely distributed under the terms of the GNU
general public licence — Copyright by Thanat0s - v0.1 -
Survival Guide
Those are the basic commands you will want to know and use for moving around a binary and getting information about it.
Command | Description |
---|---|
s (tab) | Seek to a different place |
x [nbytes] | Hexdump of nbytes, $b by default |
aa | Auto analyze |
pdf@ funcname | Disassemble function (main, fcn, etc.) |
f fcn(Tab) | List functions |
f str(Tab) | List strings |
fr [flagname] [newname] | Rename flag |
psz [offset]~grep | Print strings and grep for one |
axF [flag] | Find cross reference for a flag |
Flags
Flags are like bookmarks, but they carry some extra information like size, tags or associated flagspace. Use the f command to list, set, get them.
Command | Description |
---|---|
f | List flags |
fd $$ | Describe an offset |
fj | Display flags in JSON |
fl | Show flag length |
fx [flagname] | Show hexdump of flag |
fC [name] [comment] | Set flag comment |
Flagspaces
Flags are created into a flagspace, by default none is selected, and listing flags will list them all. To display a subset of flags you can use the fs command to restrict it.
Command | Description |
---|---|
fs | Display flagspaces |
fs * | Select all flagspaces |
fs [space] | Select one flagspace |
Information
Binary files have information stored inside the headers. The i command uses the RBin api and allows us to the same things rabin2 do. Those are the most common ones.
Command | Description |
---|---|
ii | Information on imports |
iI | Info on binary |
ie | Display entrypoint |
iS | Display sections |
ir | Display relocations |
iz | List strings (izz, izzz) |
Print string
There are different ways to represent a string in memory. The ps command allows us to print it in utf-16, pascal, zero terminated, .. formats.
Command | Description |
---|---|
psz [offset] | Print zero terminated string |
psb [offset] | Print strings in current block |
psx [offset] | Show string with scaped chars |
psp [offset] | Print pascal string |
psw [offset] | Print wide string |
Visual mode
The visual mode is the standard interactive interface of radare2.