Sommaire
- Reconnaissance
- Extraction du byte code
- Configuration de mruby
- Validation du mot de passe
- Get the flag !
Reconnaissance
Le fichier est un exécutable compilé en 64 bits pour Windows. On remarque que le binaire embarque un interpréteur au vu des symboles.
mrb fait réference à https://github.com/mruby/mruby. mruby est un interpréteur de byte code ruby.
Après avoir télécharger le source de mruby, on remarque que l’interpréteur commence par appeler la fonction mrb_open, ce qui permet de trouver le début du programme.
Extraction du byte code
Dans le challenge on trouve avec IDA une fonction f qui utilise la fonction mrb_open. La fonction f copie 1725 octets dans un buffer (nommé mrb sur l’image).
Plus loin dans la fonction le buffer est passé en paramètre à la fonction mrb_load_irep, on comprend vite que la fontion prend le byte code en argument. On extrait le byte code ruby avec IDA (en utilisant Hex View).
Configuration de mruby
Maintenant on va pouvoir lancer le byte code avec mruby. J’ai modifié la fonction mrb_load_irep pour afficher les mnémoniques mruby au chargement de chaque bloc de byte code.
On compile puis on lance l’interpréteur modifié,
$ make
$ ./build/host/bin/mruby -b bytecode_ruby.rbm
[...]
CRACKME!
NoMethodError: undefined method 'usleep' for main
Comme on peut le voir dans le résultat de la commande ci-dessus, il manque des modules (gem) dans notre interpréteur pour pouvoir lancer le programme. Il faut installer les modules dont le programme a besoin. On modifie le fichier de configuration build_config.rb
conf.gem :core => 'mruby-eval'
conf.gem :github => 'mattn/mruby-md5'
conf.gem :github => 'mattn/mruby-iconv'
conf.gem :github => 'matsumotory/mruby-sleep'
conf.gem :github => 'iij/mruby-pack'
On recompile, puis on relance
$ make
$ ./build/host/bin/mruby -b bytecode_ruby.rbm password
CRACKME!
Let me check if you deserve a flag ...
NO :(
Validation du mot de passe
Dans le byte code on remarque un bout de code intéressant,
019 OP_LOADI R9 254
020 OP_LOADI R10 255 ; BOM
021 OP_LOADI R11 0
022 OP_LOADI R12 65 'A'
023 OP_LOADI R13 0
024 OP_LOADI R14 82 'R'
025 OP_LOADI R15 0
026 OP_LOADI R16 71 'G'
027 OP_LOADI R17 0
028 OP_LOADI R18 86 'V'
029 OP_LOADI R19 0
030 OP_LOADI R20 91 '['
031 OP_LOADI R21 0
032 OP_LOADI R22 50 '2'
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
En se basant sur les sources et les commentaires de mruby, on comprend vite que,
- OP_LOADI Rx d permet de charger un entier d dans le registre Rx
- OP_ARRAY Rx Ry d construit un tableau à partir des valeurs de Ry jusqu’à R(y+d). La référence du tableau est retournée dans Rx.
Le BOM ( 0xFEFF) indique que chaque bloc de 16 bits est encodé little endian. Le programme construit la chaîne ARGV[2] en utf-16 pour ensuite la passer en paramètre à la fonction eval. Le programme prend en compte le troisième paramètre de la ligne de commande. Le résultat est placé dans le registre R5.
Il est important de comprendre comment fonctionne l’opcode OP_SEND Dans mruby, on trouve le commentaire suivant
CASE(OP_SEND) {
/* A B C R(A) := call(R(A),Syms(B),R(A+1),...,R(A+C)) */
int a = GETARG_A(i);
int n = GETARG_C(i);
int argc = (n == CALL_MAXARGS) ? -1 : n;
Dans le cas de l’instruction OP_SENDB R5 :eval 1, on appelle la fonction eval:
- la méthode prend 1 paramètre
- le paramètre est stocké dans R(5+1) soit R6
- la valeur de retour est placée dans R5
Un peu plus loin on tombe sur le code suivant, qui récupère les 8 premiers caractères du mot de passe.
051 OP_LOADI R6 0
052 OP_LOADI R7 7
053 OP_RANGE R6 R6 0 ; d'après mruby A B C R(A) := range_new(R(B),R(B+1),C)
054 OP_SEND R5 :[] 1 ; userinput[0..7]
Le code suivant semble vérifier le mot de passe, il compare les 8 premiers caractères de celui-ci.
055 OP_MOVE R3 R5 ; userinput[0..7]
056 OP_MOVE R5 R4
057 OP_JMPNOT R5 062
058 OP_MOVE R5 R3 ; userinput[0..7]
059 OP_SEND R5 :first 0 ; userinput[0..7].first()
060 OP_STRING R6 L(5) ; "W"
061 OP_EQ R5 :== 1 "W" == userinput[0..7].first()
062 OP_MOVE R4 R5
063 OP_JMPNOT R5 068
064 OP_MOVE R5 R3 ; userinput[0..7]
065 OP_SEND R5 :last 0 ; userinput[0..7].last()
066 OP_STRING R6 L(6) ; "a"
067 OP_EQ R5 :== 1 "a" == userinput[0..7].last()
068 OP_MOVE R4 R5
069 OP_JMPNOT R5 077
070 OP_MOVE R5 R3 ; userinput[0..7]
071 OP_LOADI R6 1
072 OP_ADDI R6 :+ 1 ; R6 = 2
073 OP_SEND R5 :[] 1 ; userinput[0..7][2]
074 OP_MOVE R6 R3
075 OP_SEND R6 :first 0 ; userinput[0..7].first()
076 OP_EQ R5 :== 1 ; userinput[0..7][2] == userinput[0:7].first()
077 OP_MOVE R4 R5
078 OP_JMPNOT R5 085
079 OP_MOVE R5 R3 ; userinput[0..7]
080 OP_LOADI R6 1
081 OP_SEND R5 :[] 1 ; userinput[0..7][1]
082 OP_LOADI R6 0 ; 0
083 OP_SEND R6 :to_s 0 ; "0"
084 OP_EQ R5 :== 1 ( userinput[0..7][1] = "0" )
085 OP_MOVE R4 R5
086 OP_JMPNOT R5 095
087 OP_MOVE R5 R3 ; userinput[0..7]
088 OP_LOADI R6 3
089 OP_SEND R5 :[] 1 ; userinput[0..7][3]
090 OP_SEND R5 :to_i 0 ; userinput[0..7][3].to_i()
091 OP_SUBI R5 :- 1 ; userinput[0..7][3].to_i() - 1
092 OP_LOADI R6 2
093 OP_ADDI R6 :+ 2 ; 4
094 OP_EQ R5 :== 1 ; userinput[0..7][3].to_i() - 1 == 4
095 OP_MOVE R4 R5
096 OP_JMPNOT R5 103
097 OP_MOVE R5 R3 ; userinput[0..7]
098 OP_STRING R6 L(7) ; "4"
099 OP_SEND R6 :to_i 0 ; 4
100 OP_SEND R5 :[] 1 ; userinput[0..7][4]
101 OP_STRING R6 L(8) ; "9"
102 OP_EQ R5 :== 1 ; userinput[0..7][4] == "9"
103 OP_MOVE R4 R5
104 OP_JMPNOT R5 113
105 OP_MOVE R5 R3 ; userinput[0..7]
106 OP_LOADI R6 5
107 OP_LOADI R7 -1
108 OP_RANGE R6 R6 0 ; range(5,-1)
109 OP_SEND R5 :[] 1 ; userinput[0..7][5..-1]
110 OP_SEND R5 :first 0 ; userinput[0..7][5..-1].first() => userinput[0..7][5]
111 OP_STRING R6 L(9) ; "("
112 OP_EQ R5 :== 1 "(" == userinput[0..7][5]
113 OP_MOVE R4 R5
114 OP_JMPNOT R5 122
115 OP_MOVE R5 R3
116 OP_LOADSYM R6 :[]
117 OP_LOADI R7 -2
118 OP_SEND R5 :send 2 ; userinput[0..7].send([],-2) => userinput[0..7][-2] => userinput[0..7][6]
119 OP_SEND R5 :to_f 0
120 OP_LOADI R6 8
121 OP_EQ R5 :== 1 ; userinput[0:7][6].to_f() == 8
122 OP_MOVE R4 R5
Le mot de passe commence donc par W0W59(8a .
Le code suivant appelle time, cela va déclencher l’exécution du bloc suivant (I+1) 8 fois.
123 OP_LOADI R5 8
124 OP_LAMBDA R6 I(+1) block
125 OP_SENDB R5 :times 0
A chaque itération, le bloc reçoit un entier allant de 0 à n - 1 fois soit :
- 0 à la première itération.
- 1 à la deuxième etc …
Voici le bloc qui est appelé, l’instruction OP_GETUPVAR permet de récupèrer la valeur d’un registre mais dans le contexte d’exécution du bloc précedent.
irep 0x5568998a0b30 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 ; R3 = R2 = userinput
004 OP_MOVE R4 R1 ; R1:n
005 OP_SEND R3 :[] 1 ; userinput[n]
006 OP_GETUPVAR R4 2 0 ; R4 = R2 = userinput
007 OP_MOVE R5 R1 ; R1:n
008 OP_SEND R5 :-@ 0 ; -n
009 OP_SUBI R5 :- 1 ; -n - 1
010 OP_SEND R4 :[] 1 ; userinput[-n - 1]
011 OP_MOVE R5 R1 ; R1:n
012 OP_ADDI R5 :+ 1 ; n + 1
013 OP_SEND R4 :^ 1 ; userinput[-n - 1] ^ (n + 1)
014 OP_EQ R3 :== 1 ; userinput[n] == userinput[-n - 1] ^ (n + 1)
015 OP_SETUPVAR R3 4 0
016 OP_RETURN R3 normal
Get the flag !
Il ne reste plus qu’à scripter ça en python ;)
password = "W0W59(8a"
password_2 = ""
for i in range(0,len(password)):
password_2 = chr(ord(password[i]) ^ (i + 1)) + password_2
print(password+password_2)
Ce qui nous donne : W0W59(8ai?.<1T2V
./mruby/build/host/bin/mruby -b bytecode_ruby.rbm 0 0 W0W59\(8ai?.\<1T2V
CRACKME!
Let me check if you deserve a flag ...
YES :)
flag is: '983b428e721bcfceabf6c77d9e819d8d'
Et hop on valide le challenge avec ECW{983b428e721bcfceabf6c77d9e819d8d}