European Cyber Week CTF 2017 - Red Diamond Writeup

Hi,

European Cyber Week CTF is a contest exclusively reserved for "European" students. 
I'am not eligible to play but I joined the CTF later in order to keep practicing and learn new stuff .

Red Diamond in the Binary category was the hardest task regarding the number of players who solved it (7 solves). 

The given file was a Windows binary for AMD64 !

I imported the binary file into IDAPro, then, with a top-down look at the string list I saw many paths to mruby header files such as /home/FlL/mruby1.3.0/mruby/include/mruby/value.h

I "googled" mruby. It is a lightweight implementation of the Ruby language. From this point I figured out that my mission will be hard because I never wrote a single line of ruby code. But remember that my goal is to learn new stuff ! So ! Let's do it !

Mruby can be linked and embedded within an application, which is the case.  In addition, Ruby programs can be compiled into byte code using the mruby compiler.

Typically the challenge file is an application embedding a mruby bytecode and an interpreter.

I spent some time reading about MRB file format and RiteVM.

MRB file starts with RITE00... as magic number. I searched that pattern inside the binary and, yes !,  I found it.

The bytecode program starts from offset 0x80A20 in the file (0x100482020 offset in memory)

I dumped the bytecode to a red_diamond.mrb file.

Mruby package provides an interpreter program "mruby". I downloaded mruby sources and I compiled it.


root@maro-vm:~/ecw# git clone https://github.com/mruby/mruby.git
root@maro-vm:~/ecw# cd mruby/
root@maro-vm:~/ecw/mruby# ./minirake


I tried to run the binary file.


root@maro-vm:~/ecw/mruby# ./bin/mruby -b red_diamond.mrb
CRACKME!
NoMethodError: undefined method 'usleep' for main


Well ! I got a message printed to screen, however the program crashed due to call to undefined function usleep.

I added the sleep module from here https://github.com/matsumotory/mruby-sleep ! But instead of specifying the github path, I cloned the module source and specified its path in the disk to build_config.rb. I did this in order to be able to easily alter the code if needed.


root@maro-vm:~/ecw/mruby# git clone https://github.com/matsumotory/mruby-sleep.git
root@maro-vm:~/ecw/mruby# nano build_config.rb
MRuby::Build.new do |conf|
   conf.gem 'mruby-sleep'
...

I did the same thing for the other requested modules. Those are, mruby-eval, mruby-iconv, mruby-md5 and mruby-pack.


MRuby::Build.new do |conf|
...
  conf.gem :core => 'mruby-eval'
  conf.gem 'mruby-sleep'
  conf.gem :git => 'https://github.com/mattn/mruby-iconv.git'
  conf.gem :git => 'https://github.com/mattn/mruby-md5.git'
  conf.gem 'mruby-pack'
...


Then I compiled again mruby.

root@maro-vm:~/ecw/mruby# ./minirake clean
root@maro-vm:~/ecw/mruby# ./minirake


I tried again to run the program.

root@maro-vm:~/ecw/mruby#./bin/mruby -b red_diamond.mrb
CRACKME!
Let me check if you deserve a flag ...
NO :(


Good ! works fine !

Using -v option it is possible to see VM codes of the program.

root@maro-vm:~/ecw/mruby#./bin/mruby -b -v red_diamond.mrb

mruby 1.3.0 (2017-7-4)
irep 0x555555873e30 nregs=7 nlocals=3 pools=11 syms=12 reps=2
      000 OP_LOADSELF   R3
      001 OP_STRING     R4      L(0)    ; "CRACKME!"
      002 OP_SEND       R3      :puts   1
      003 OP_LOADSELF   R3
      004 OP_LOADSELF   R4
      005 OP_LOADL      R5      L(1)    ; 400000
      006 OP_SEND       R4      :rand   1
      007 OP_SEND       R3      :usleep 1
      008 OP_LOADSELF   R3
      009 OP_STRING     R4      L(2)    ; "Let me check if you deserve a flag ..."
      010 OP_SEND       R3      :puts   1
      011 OP_LOADSELF   R3
      012 OP_LOADSELF   R4
      013 OP_LOADL      R5      L(3)    ; 600000
      014 OP_SEND       R4      :rand   1
      015 OP_LOADL      R5      L(4)    ; 2000000
      016 OP_ADD        R4      :+      1
      017 OP_SEND       R3      :usleep 1
      018 OP_ONERR      050
      019 OP_TCLASS     R3
      020 OP_LAMBDA     R4      I(+1)   method
      021 OP_METHOD     R3      :found?
      022 OP_OCLASS     R3
      023 OP_LOADNIL    R4
      024 OP_CLASS      R3      :String
      025 OP_EXEC       R3      I(+2)
      026 OP_LOADSELF   R3
      027 OP_SEND       R3      :found? 0
      028 OP_JMPNOT     R3      046
      029 OP_LOADSELF   R3
      030 OP_STRING     R4      L(5)    ; "YES :)"
      031 OP_SEND       R3      :puts   1
      032 OP_GETCONST   R3      :MD5
      033 OP_STRING     R4      L(6)    ; "\342\235\250\342\225\257\302\260\342\226\241\302\260\342\235\251\342\225\257\357\270\265\342\224\273\342\224\201\342\224\273"
      034 OP_GETGLOBAL  R5      :$salt
      035 OP_ADD        R4      :+      1
      036 OP_SEND       R3      :md5_hex        1
      037 OP_MOVE       R1      R3              ; R1:flag
      038 OP_LOADSELF   R3
      039 OP_STRING     R4      L(7)    ; "\tflag is: '"
      040 OP_MOVE       R5      R1              ; R1:flag
      041 OP_STRCAT     R4      R5
      042 OP_STRING     R5      L(8)    ; "'"
      043 OP_STRCAT     R4      R5
      044 OP_SEND       R3      :puts   1
      045 OP_JMP        049
      046 OP_LOADSELF   R3
      047 OP_STRING     R4      L(9)    ; "NO :("
      048 OP_SEND       R3      :puts   1
      049 OP_JMP        069
      050 OP_RESCUE     R3
      051 OP_GETCONST   R4      :Exception
      052 OP_RESCUE     R3      R4      cont
      053 OP_JMPIF      R4      055
      054 OP_JMP        068
      055 OP_MOVE       R2      R3              ; R2:e
      056 OP_LOADSELF   R3
      057 OP_STRING     R4      L(10)   ; "fail!"
      058 OP_SEND       R3      :puts   1
      059 OP_LOADSELF   R3
      060 OP_MOVE       R4      R2              ; R2:e
      061 OP_SEND       R4      :message        0
      062 OP_SEND       R3      :puts   1
      063 OP_LOADSELF   R3
      064 OP_MOVE       R4      R2              ; R2:e
      065 OP_SEND       R4      :backtrace      0
      066 OP_SEND       R3      :puts   1
      067 OP_JMP        070
      068 OP_RAISE      R3
      069 OP_POPERR     1
      070 OP_STOP

irep 0x5555558743e0 nregs=25 nlocals=5 pools=10 syms=19 reps=1
      000 OP_ENTER      0:0:0:0:0:0:0
      001 OP_STRING     R5      L(0)    ; "utf-0"
      002 OP_STRING     R6      L(1)    ; "utf-9"
      003 OP_RANGE      R5      R5      0
      004 OP_SEND       R5      :to_a   0
      005 OP_STRING     R6      L(2)    ; "utf-10"
      006 OP_STRING     R7      L(3)    ; "utf-30"
      007 OP_RANGE      R6      R6      0
      008 OP_SEND       R6      :to_a   0
      009 OP_ADD        R5      :+      1
      010 OP_MOVE       R2      R5              ; R2:"\302\265"
      011 OP_LOADSELF   R5
      012 OP_GETCONST   R6      :Iconv
      013 OP_MOVE       R7      R2              ; R2:"\302\265"
      014 OP_LOADI      R8      8
      015 OP_SEND       R7      :[]     1
      016 OP_MOVE       R8      R2              ; R2:"\302\265"
      017 OP_LOADI      R9      16
      018 OP_SEND       R8      :[]     1
      019 OP_LOADI      R9      254
      020 OP_LOADI      R10     255
      021 OP_LOADI      R11     0
      022 OP_LOADI      R12     65
      023 OP_LOADI      R13     0
      024 OP_LOADI      R14     82
      025 OP_LOADI      R15     0
      026 OP_LOADI      R16     71
      027 OP_LOADI      R17     0
      028 OP_LOADI      R18     86
      029 OP_LOADI      R19     0
      030 OP_LOADI      R20     91
      031 OP_LOADI      R21     0
      032 OP_LOADI      R22     50
      033 OP_LOADI      R23     0
      034 OP_LOADI      R24     93
      035 OP_ARRAY      R9      R9      16
      036 OP_STRING     R10     L(4)    ; "C*"
      037 OP_SEND       R9      :pack   1
      038 OP_SEND       R6      :conv   3
      039 OP_SEND       R5      :eval   1
      040 OP_MOVE       R3      R5              ; R3:"\302\244"
      041 OP_JMPNOT     R5      043
      042 OP_JMP        045
      043 OP_LOADNIL    R5
      044 OP_RETURN     R5      normal
      045 OP_LOADT      R4                      ; R4:"\302\247"
      046 OP_MOVE       R5      R3              ; R3:"\302\244"
      047 OP_STRING     R6      L(4)    ; "C*"
      048 OP_SEND       R5      :unpack 1
      049 OP_MOVE       R2      R5              ; R2:"\302\265"
      050 OP_MOVE       R5      R3              ; R3:"\302\244"
      051 OP_LOADI      R6      0
      052 OP_LOADI      R7      7
      053 OP_RANGE      R6      R6      0
      054 OP_SEND       R5      :[]     1
      055 OP_MOVE       R3      R5              ; R3:"\302\244"
      056 OP_MOVE       R5      R4              ; R4:"\302\247"
      057 OP_JMPNOT     R5      062
      058 OP_MOVE       R5      R3              ; R3:"\302\244"
      059 OP_SEND       R5      :first  0
      060 OP_STRING     R6      L(5)    ; "W"
      061 OP_EQ         R5      :==     1
      062 OP_MOVE       R4      R5              ; R4:"\302\247"
      063 OP_JMPNOT     R5      068
      064 OP_MOVE       R5      R3              ; R3:"\302\244"
      065 OP_SEND       R5      :last   0
      066 OP_STRING     R6      L(6)    ; "a"
      067 OP_EQ         R5      :==     1
      068 OP_MOVE       R4      R5              ; R4:"\302\247"
      069 OP_JMPNOT     R5      077
      070 OP_MOVE       R5      R3              ; R3:"\302\244"
      071 OP_LOADI      R6      1
      072 OP_ADDI       R6      :+      1
      073 OP_SEND       R5      :[]     1
      074 OP_MOVE       R6      R3              ; R3:"\302\244"
      075 OP_SEND       R6      :first  0
      076 OP_EQ         R5      :==     1
      077 OP_MOVE       R4      R5              ; R4:"\302\247"
      078 OP_JMPNOT     R5      085
      079 OP_MOVE       R5      R3              ; R3:"\302\244"
      080 OP_LOADI      R6      1
      081 OP_SEND       R5      :[]     1
      082 OP_LOADI      R6      0
      083 OP_SEND       R6      :to_s   0
      084 OP_EQ         R5      :==     1
      085 OP_MOVE       R4      R5              ; R4:"\302\247"
      086 OP_JMPNOT     R5      095
      087 OP_MOVE       R5      R3              ; R3:"\302\244"
      088 OP_LOADI      R6      3
      089 OP_SEND       R5      :[]     1
      090 OP_SEND       R5      :to_i   0
      091 OP_SUBI       R5      :-      1
      092 OP_LOADI      R6      2
      093 OP_ADDI       R6      :+      2
      094 OP_EQ         R5      :==     1
      095 OP_MOVE       R4      R5              ; R4:"\302\247"
      096 OP_JMPNOT     R5      103
      097 OP_MOVE       R5      R3              ; R3:"\302\244"
      098 OP_STRING     R6      L(7)    ; "4"
      099 OP_SEND       R6      :to_i   0
      100 OP_SEND       R5      :[]     1
      101 OP_STRING     R6      L(8)    ; "9"
      102 OP_EQ         R5      :==     1
      103 OP_MOVE       R4      R5              ; R4:"\302\247"
      104 OP_JMPNOT     R5      113
      105 OP_MOVE       R5      R3              ; R3:"\302\244"
      106 OP_LOADI      R6      5
      107 OP_LOADI      R7      -1
      108 OP_RANGE      R6      R6      0
      109 OP_SEND       R5      :[]     1
      110 OP_SEND       R5      :first  0
      111 OP_STRING     R6      L(9)    ; "("
      112 OP_EQ         R5      :==     1
      113 OP_MOVE       R4      R5              ; R4:"\302\247"
      114 OP_JMPNOT     R5      122
      115 OP_MOVE       R5      R3              ; R3:"\302\244"
      116 OP_LOADSYM    R6      :[]
      117 OP_LOADI      R7      -2
      118 OP_SEND       R5      :send   2
      119 OP_SEND       R5      :to_f   0
      120 OP_LOADI      R6      8
      121 OP_EQ         R5      :==     1
      122 OP_MOVE       R4      R5              ; R4:"\302\247"
      123 OP_LOADI      R5      8
      124 OP_LAMBDA     R6      I(+1)   block
      125 OP_SENDB      R5      :times  0
      126 OP_MOVE       R5      R4              ; R4:"\302\247"
      127 OP_JMPNOT     R5      132
      128 OP_MOVE       R5      R2              ; R2:"\302\265"
      129 OP_SEND       R5      :size   0
      130 OP_LOADI      R6      16
      131 OP_EQ         R5      :==     1
      132 OP_MOVE       R4      R5              ; R4:"\302\247"
      133 OP_SETGLOBAL  :$salt  R3              ; R3:"\302\244"
      134 OP_RETURN     R4      normal          ; R4:"\302\247"

irep 0x555555874a40 nregs=8 nlocals=3 pools=0 syms=6 reps=0
      000 OP_ENTER      1:0:0:0:0:0:0
      001 OP_GETUPVAR   R3      4       0
      002 OP_JMPNOT     R3      015
      003 OP_GETUPVAR   R3      2       0
      004 OP_MOVE       R4      R1              ; R1:n
      005 OP_SEND       R3      :[]     1
      006 OP_GETUPVAR   R4      2       0
      007 OP_MOVE       R5      R1              ; R1:n
      008 OP_SEND       R5      :-@     0
      009 OP_SUBI       R5      :-      1
      010 OP_SEND       R4      :[]     1
      011 OP_MOVE       R5      R1              ; R1:n
      012 OP_ADDI       R5      :+      1
      013 OP_SEND       R4      :^      1
      014 OP_EQ         R3      :==     1
      015 OP_SETUPVAR   R3      4       0
      016 OP_RETURN     R3      normal

irep 0x555555874b30 nregs=3 nlocals=1 pools=0 syms=2 reps=2
      000 OP_TCLASS     R1
      001 OP_LAMBDA     R2      I(+1)   method
      002 OP_METHOD     R1      :first
      003 OP_TCLASS     R1
      004 OP_LAMBDA     R2      I(+2)   method
      005 OP_METHOD     R1      :last
      006 OP_LOADSYM    R1      :last
      007 OP_RETURN     R0      normal

irep 0x555555874c20 nregs=5 nlocals=2 pools=0 syms=1 reps=0
      000 OP_ENTER      0:0:0:0:0:0:0
      001 OP_LOADSELF   R2
      002 OP_LOADI      R3      0
      003 OP_SEND       R2      :[]     1
      004 OP_RETURN     R2      normal

irep 0x555555874ce0 nregs=5 nlocals=2 pools=0 syms=1 reps=0
      000 OP_ENTER      0:0:0:0:0:0:0
      001 OP_LOADSELF   R2
      002 OP_LOADI      R3      -1
      003 OP_SEND       R2      :[]     1
      004 OP_RETURN     R2      normal


Sadly there is not decompiler for the bytecode. I spent to much time reading about RiteVM opcodes and play with the interactive mruby shell "mirb" to understand what the program does.

The program starts by printing "CRACKME!" to screen, sleeps a while (random seconds), prints "Let me check if you deserve a flag ...", sleeps a while again perform a test on found method result then prints "NO ! :(" and exits.

If the found method returns 1 then it loads the content of $salt global variable, append it to "\342\235\250\342\225\257\302\260\342\226\241\302\260\342\235\251\342\225\257\357\270\265\342\224\273\342\224\201\342\224\273", compute the md5 over it, hex encode it and prints flag is and the flag value.

If found returns 0, then "NO :(" is printed and the program exists.

The program author has extended the String class by adding 3 methods , first, last and found.

It is obvious that irep 0x555555874c20 is the implementation of "first" method since it returns the element with index -1 of an array.
irep 0x555555874ce0 is for "last" method because it returns the element with index -1 of an array.

irep 0x5555558743e0 is the implementation of found method.

It reads the second argument passed to the program using eval(ARGV[2]) then the argument value is checked if it is the expected value or not.

It reads the first element using the defined first method and check if it is equal to "W", if it is the case, it loads the last character and check if it is "a" if so, further checks is done.

Then the third character (index incremented by 2) is tested if it is equal to the first character.

Next, the index is decremented by 1 to test if the first character is equal to "0"

The fourth character must be equal to "4" + 1 = "5"

The fifth character must be equal to "9"

The sixth character must be "("

The seventh character must be "8"

Hence the correct password is W0W59(8a

Finally the :$salt global variable is set to eval(ARGV[2]) which is the second argument.

The function return 1 if the argument satisfies the test otherwise it returns 0.

The strange thing is that by feeding the correct input to the program it kept saying NO ! :(

And since I'm sure that what I did is correct ! I putted the flag in the requested format and I submitted it to the platform and BOM! it is accepted ! and that's why maybe players failed to solve it ! or maybe that the there is many solutions for this challenge also.

Flag was ECW{983b428e721bcfceabf6c77d9e819d8d}

And yes the author was right !

In [5]: print "\342\235\250\342\225\257\302\260\342\226\241\302\260\342\235\251\342\225\257\357\270\265\342\224\273\342\224\201\342\224\273"
❨╯°□°❩╯︵┻━┻




Another way to solve this challenge could be dealing with it as a blackbox by count the number of instruction executed for each input.

This could not work here since the characters are not checked in order. You need in this case to trace the program execution. After googling a while, I found an interesting module which is "mruby-debug-example" it hooks the VM function which fetches the opcodes and dump it.

The module can be downloaded from here https://github.com/masuidrive/mruby-debug-example

Add it to your build_config.rb and compile again using minirake.

Now by running the program you can get a full execution trace !

...
0x555555876eec: OP_LOADSELF     R3
0x555555876ef0: OP_LOADSELF     R4
0x555555876ef4: OP_LOADL        R5      L(1)
0x555555876ef8: OP_SEND R4      :rand   1
0x555555876efc: OP_SEND R3      :usleep 1
0x555555876f00: OP_LOADSELF     R3
0x555555876f04: OP_STRING       R4      "Let me check if you deserve a flag ..."
0x555555876f08: OP_SEND R3      :puts   1
0x5555555ebe6c: OP_ENTER        0:0:1:0:0:0:0
0x5555555ebe70: OP_LOADI        R3      0
0x5555555ebe74: OP_MOVE R6      R1
0x5555555ebe78: OP_SEND R6      :size   0
0x5555555ebe7c: OP_MOVE R4      R6
0x5555555ebe80: OP_JMP          021
0x5555555ebed4: OP_MOVE R6      R3
0x5555555ebed8: OP_MOVE R7      R4
0x5555555ebedc: OP_LT   R6      :<      1
0x5555555ebee0: OP_JMPIF        R6      -23
0x5555555ebe84: OP_MOVE R6      R1
0x5555555ebe88: OP_MOVE R7      R3
0x5555555ebe8c: OP_SEND R6      :[]     1
0x5555555ebe90: OP_SEND R6      :to_s   0
0x5555555ebe94: OP_MOVE R5      R6
0x5555555ebe98: OP_LOADSELF     R6
0x5555555ebe9c: OP_MOVE R7      R5
0x5555555ebea0: OP_SEND R6      :__printstr__   1
Let me check if you deserve a flag ...0x5555555ebea4: OP_MOVE   R6      R5
0x5555555ebea8: OP_LOADI        R7      -1
0x5555555ebeac: OP_SEND R6      :[]     1
0x5555555ebeb0: OP_STRING       R7      "\n"
0x5555555ebeb4: OP_SEND R6      :!=     1
0x5555555ebeb8: OP_JMPNOT       R6      004
0x5555555ebebc: OP_LOADSELF     R6
0x5555555ebec0: OP_STRING       R7      "\n"
0x5555555ebec4: OP_SEND R6      :__printstr__   1
...


The author maybe added the sleep methods to prevent bruteforce ! You can at this level modify mrb_sleep.c source file and modufy the usleep implementation and make it sleeps for 0 seconds for example.


93:    if (mrb_fixnum_p(argv[0]) && mrb_fixnum(argv[0]) >= 0) {
94:        usleep(0); //usleep(mrb_fixnum(argv[0]));
95:    } else {
96:        mrb_raise(mrb, E_ARGUMENT_ERROR, "time interval must be positive");
97:    }


You can than write a sample python script to bruteforce the password !

Here is a python script which can be used to find the first character.


import subprocess

for i in range(32,127):
    s = chr(i)+"A"*7
    p = subprocess.Popen("./bin/mruby -b /root/red_diamond.mrb 0 0 '"+s+"'", shell=True, stdou$
    print "[+] %c -> %d"%(chr(i), len(p.stdout.readlines()))


...
[+] L -> 1821
[+] M -> 1821
[+] N -> 1821
[+] O -> 1821
[+] P -> 1821
[+] Q -> 1821
[+] R -> 1821
[+] S -> 1821
[+] T -> 1821
[+] U -> 1821
[+] V -> 1821
[+] W -> 1830
[+] X -> 1821
[+] Y -> 1821
[+] Z -> 1821
[+] [ -> 1821
[+] \ -> 1821
[+] ] -> 1821
[+] ^ -> 1821
[+] _ -> 1821
...


Found ! the first character is W.

But keep in mind that this way is good to quickly solve a CTF task but in general it is much leet to understand how things work (VM implementation etc..) !

That's all!

Follow me twitter

Maro