FlareOn 2019 Writeup - Part 1
Part 1 contains my solutions for challenges 1-3.
Challenge 1 - Memecat_Battlestation
Task:
Welcome to the Sixth Flare-On Challenge!
This is a simple game. Reverse engineer it to figure out what "weapon codes"
you need to enter to defeat each of the two enemies and the victory screen
will reveal the flag. Enter the flag here on this site to score and move on
to the next level.
* This challenge is written in .NET. If you don't already have a favorite
.NET reverse engineering tool I recommend dnSpy
** If you already solved the full version of this game at our booth at
BlackHat or the subsequent release on twitter, congratulations, enter
the flag from the victory screen now to bypass this level.
So, we’re dealing with a .NET binary. Let’s run it and see what it does:
It appears it expects a code as input in order to shoot at the incoming cats. For decompiling and debugging .NET I use dnSpy.
Opening the executable in dnSpy we immediately notice several classes, one of which is called VictoryForm
.
VictoryForm
has a method called VictoryForm_Load
and its decompiled source code is shown below:
private void VictoryForm_Load(object sender, EventArgs e)
{
byte[] array = new byte[]
{
9,
8,
19,
17,
9,
55,
28,
18,
15,
24,
10,
49,
75,
51,
45,
32,
54,
59,
15,
49,
46,
0,
21,
0,
65,
48,
45,
79,
13,
1,
2
};
byte[] bytes = Encoding.UTF8.GetBytes(this.Arsenal);
for (int i = 0; i < array.Length; i++)
{
byte[] array2 = array;
int num = i;
array2[num] ^= bytes[i % bytes.Length];
}
this.flagLabel.Text = Encoding.UTF8.GetString(array);
}
The Arsenal
variable is used to decrypt the array which holds the flag.
This variable is set in the Main
method inside the Program
class.
The Arsenal
string is a concatenation of the from stage1Form.WeaponCode
and stage2Form.WeaponCode
attributes.
For stage1 the WeaponCode
attribute is being set inside the FireButton_Click
method.
You can clearly see the check which needs to be passed. So the first code is "RAINBOW"
.
When it’s entered, Grumpy cat shoots a rainbow and kills the other cat.
The FireButton_Click
method inside the stage2Form
class is a little different. There is another function
which performs the check.
Decompiling the method isValidWeaponCode
reveals the following source code:
It takes the user input from the text box, then XORs every byte with the ASCII character ‘A’ and finally compares the result to some bytes. We can decode the expected input by XORing the expected bytes with the character ‘A’.
a = bytearray('\x03 &$-\x1e\x02 //./')
s = ''
for i in a:
s += chr(i^ord('A'))
print s
Executing the script returns the string "Bagel_Cannon"
. Running the executable again and entering the right codes provides us with the victory
screen and the flag.
I also tried to get the flag statically by reimplementing the VictoryForm_Load
method.
The following python code returns the flag:
key = bytearray("Bagel_Cannon,RAINBOW")
encrypted_flag = bytearray([9,8,19,17,9,55,28,18,15,24,10,49,75,51,45,32,54,59,15,49,46,0,21,0,65,48,45,79,13,1,2])
flag = ''
for i in xrange(len(encrypted_flag)):
flag += chr(encrypted_flag[i] ^ key[i % len(key)])
print flag
And the flag is:
Kitteh_save_galixy@flare-on.com
Challenge 2 - Overlong
Task:
The secret of this next challenge is cleverly hidden. However, with the right
approach, finding the solution will not take an <b>overlong</b> amount of time.
When you execute the binary the following message box appears:
This challenge is written in C\C++, so I’ll use Ghidra for the dissasembly and decompilation.
There are only 3 functions, which I’ve already analysed and renamed with an appropriate names.
The function DecodeByte
takes two arguments. Each argument is a pointer to a byte. The function
takes the byte from the second argument, decodes it and saves it at the address of the first argument.
The function DecodeBuffer
has three arguments. The first argument is an empty buffer.
The second argument is a source buffer and the last argument is the length of the source buffer.
The function iterates through the bytes from the source buffer and calls DecodeByte
on each byte.
The result is saved in the empty buffer.
In the main function DecodeBuffer
is called with three arguments - an empty buffer with size 128 bytes, global byte array filled with bytes and length 0x1c
(28 in decimal).
If we check the length of the global byte array we’ll see it’s 176 bytes long, which is larger than the supplied length (28) in the DecodeBuffer
function.
One way to obtain the flag is by patching the binary and fixing the bug or reimplement the algorithm without the bug.
The first way I did it is by patching the binary. Open the executable file in HEX editor and search for a sequence of bytes which correspond to the assembly instructions where the length (0x1c) is passed as an argument.
And finally change the 0x1c value to the length of the destination buffer (0x80 = 128)
Now if you run the patched binary the message box contains the flag.
Another way is to write a script which will decode the flag. Just extract the bytes from the global array and reimplement the decoding function.
buffer = bytearray("\xE0\x81\x89\xC0\xA0\xC1\xAE\xE0\x81\xA5\xC1\xB6\xF0\x80\x81\xA5\xE0\x81\xB2\xF0\x80\x80\xA0\xE0\x81\xA2\x72\x6F\xC1\xAB\x65\xE0\x80\xA0\xE0\x81\xB4\xE0\x81\xA8\xC1\xA5\x20\xC1\xA5\xE0\x81\xAE\x63\xC1\xAF\xE0\x81\xA4\xF0\x80\x81\xA9\x6E\xC1\xA7\xC0\xBA\x20\x49\xF0\x80\x81\x9F\xC1\xA1\xC1\x9F\xC1\x8D\xE0\x81\x9F\xC1\xB4\xF0\x80\x81\x9F\xF0\x80\x81\xA8\xC1\x9F\xF0\x80\x81\xA5\xE0\x81\x9F\xC1\xA5\xE0\x81\x9F\xF0\x80\x81\xAE\xC1\x9F\xF0\x80\x81\x83\xC1\x9F\xE0\x81\xAF\xE0\x81\x9F\xC1\x84\x5F\xE0\x81\xA9\xF0\x80\x81\x9F\x6E\xE0\x81\x9F\xE0\x81\xA7\xE0\x81\x80\xF0\x80\x81\xA6\xF0\x80\x81\xAC\xE0\x81\xA1\xC1\xB2\xC1\xA5\xF0\x80\x80\xAD\xF0\x80\x81\xAF\x6E\xC0\xAE\xF0\x80\x81\xA3\x6F\xF0\x80\x81\xAD\x00")
decoded_buffer = ''
offset = 0
i = 0
while i <= len(buffer):
if buffer[offset] >> 3 == 0x1e:
decoded_buffer += chr((((buffer[offset+2] & 0x3f) << 6) | (buffer[offset+3] & 0x3f)))
offset += 4
elif buffer[offset] >> 4 == 0xe:
decoded_buffer += chr((((buffer[offset+1] & 0x3f) << 6) | (buffer[offset+2] & 0x3f)))
offset += 3
elif buffer[offset] >> 5 == 6:
decoded_buffer += chr((((buffer[offset] & 0x1f) << 6) | (buffer[offset+1] & 0x3f)))
offset += 2
else:
decoded_buffer += chr(buffer[offset])
offset += 1
if decoded_buffer[i] == "\x00":
print decoded_buffer
break
i = i + 1
Running the above python script produces the following output:
I never broke the encoding: I_a_M_t_h_e_e_n_C_o_D_i_n_g@flare-on.com
Challenge 3 - flarebear
Task:
We at Flare have created our own Tamagotchi pet, the flarebear.
He is very fussy. Keep him alive and happy and he will give
you the flag.
This challenge is for reversing an Android apk file. The apk is a Tamagotchi game and we need see what conditions must be met in order to display the flag.
I had problems with running emulators inside the VM and I didn’t want to install new software on my host machine just for this challenge, so I used the online service https://appetize.io/ to run the apk.
The game starts with a menu that has options to continue game or create a new bear. When you create a new bear you need to give it a name and then the game starts. You have three options - feed, play and clean.
To decompile the apk:
- unzip the .apk contents
- use dex2jar to convert the classes.dex file to a .jar file
- open the newly created .jar file in jd-gui
Looking around, in the FlareBearActivity.class
there’s a method called danceWithFlag
. This method calls a couple of resources, obtains a key with the method getPassword, then uses this key to decrypt the resources and display them as a bitmap image.
public final void danceWithFlag() {
InputStream inputStream1 = getResources().openRawResource(2131427328);
Intrinsics.checkExpressionValueIsNotNull(inputStream1, "ecstaticEnc");
arrayOfByte1 = ByteStreamsKt.readBytes(inputStream1);
InputStream inputStream2 = getResources().openRawResource(2131427329);
Intrinsics.checkExpressionValueIsNotNull(inputStream2, "ecstaticEnc2");
byte[] arrayOfByte2 = ByteStreamsKt.readBytes(inputStream2);
String str = getPassword();
try {
arrayOfByte1 = decrypt(str, arrayOfByte1);
arrayOfByte2 = decrypt(str, arrayOfByte2);
Bitmap bitmap1 = BitmapFactory.decodeByteArray(arrayOfByte1, 0, arrayOfByte1.length);
BitmapDrawable bitmapDrawable1 = new BitmapDrawable(getResources(), bitmap1);
Bitmap bitmap2 = BitmapFactory.decodeByteArray(arrayOfByte2, 0, arrayOfByte2.length);
BitmapDrawable bitmapDrawable2 = new BitmapDrawable(getResources(), bitmap2);
dance((Drawable)bitmapDrawable1, (Drawable)bitmapDrawable2);
return;
} catch (Exception arrayOfByte1) {
return;
}
}
The danceWithFlag
method is called from another method called setMood
only when isHappy
and isEcstatic
both return true.
public final void setMood() {
if (isHappy()) {
((ImageView)_$_findCachedViewById(R.id.flareBearImageView)).setTag("happy");
if (isEcstatic()) {
danceWithFlag();
return;
}
} else {
((ImageView)_$_findCachedViewById(R.id.flareBearImageView)).setTag("sad");
}
}
The isHappy
method checks if the ratio of feed/play is between 2 and 2.5. If it is, it returns true.
public final boolean isHappy() {
int i = getStat('f');
int j = getStat('p');
double d = (i / j);
return (d >= 2.0D && d <= 2.5D);
}
The isEcstatic
method checks the values of the mass, happy and clean states of the bear.
public final boolean isEcstatic() {
byte b = 0;
int i = getState("mass", 0);
int j = getState("happy", 0);
int k = getState("clean", 0);
int m = b;
if (i == 72) {
m = b;
if (j == 30) {
m = b;
if (k == 0)
m = 1;
}
}
return m;
}
The mass, happy and clean states change in several methods, one of which is the play
method.
public final void play(@NotNull View paramView) {
Intrinsics.checkParameterIsNotNull(paramView, "view");
saveActivity("p");
changeMass(-2);
changeHappy(4);
changeClean(-1);
playUi();
}
The changeX methods add the argument to the current state value.
public final void changeMass(int paramInt) { setState("mass", getState("mass", 0) + paramInt); }
public final void changeClean(int paramInt) { setState("clean", getState("clean", 0) + paramInt); }
public final void changeHappy(int paramInt) { setState("happy", getState("happy", 0) + paramInt); }
The other two functions which change the these states are clean
and feed
.
public final void clean(@NotNull View paramView) {
Intrinsics.checkParameterIsNotNull(paramView, "view");
saveActivity("c");
removePoo();
cleanUi();
changeMass(0);
changeHappy(-1);
changeClean(6);
setMood();
}
public final void feed(@NotNull View paramView) {
Intrinsics.checkParameterIsNotNull(paramView, "view");
saveActivity("f");
changeMass(10);
changeHappy(2);
changeClean(-1);
incrementPooCount();
feedUi();
}
Now to return to our isEcstatic
method.
public final boolean isEcstatic() {
byte b = 0;
int i = getState("mass", 0);
int j = getState("happy", 0);
int k = getState("clean", 0);
int m = b;
if (i == 72) {
m = b;
if (j == 30) {
m = b;
if (k == 0)
m = 1;
}
}
return m;
}
The mass needs to be 72, happy needs to be 30 and clean needs to be 0 for the method to return true. Also the ratio of feed/play needs to be between 2 and 2.5 for isHappy
to return true. This means that we need to call the methods play
, clean
and feed
in such order that these conditions are met.
To summarize how these states change:
feed -> mass +10, happy +2, clean -1
play -> mass -2, happy +4, clean -1
clean -> mass +0, happy -1, clean +6
This can be represented as a system of linear equations:
10f - 2p + 0c = 72
2f + 4p - 1c = 30
-1f -1p + 6c = 0
This is easily solvable with wolfram alpha and representing the equations as matrices:
The answer is f=8, p=4, and c=2 and the ratio feed/play is 8/4 which is 2 and satisfies the condition for isHappy
.
Emulating the game and then clicking 8 times feed, 4 times play and 2 times clean gives us the flag.
The flag is:
th4t_was_be4rly_a_chall3nge@flare-on.com