Category: Reverse Engineering
Difficulty: Medium (30 points)
Author: Summoned Skull
Description
You already beat Yugi’s weakest monster? That’s impressive. However, you will not be able to knock out the next one. This time, the battle will occur in a more native environment! Your fate is sealed.
Solution
This challenge gives us an Android app (.apk file), extracting it with apkfile, in assets/private.tar we find some .pyc files, decompiling main.pyc gives us this result.
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.properties import ObjectProperty
import subprocess
from encode import encode
class ActivityLayout(GridLayout):
password = ObjectProperty(None)
result = ObjectProperty(None)
def submit(self):
flag = self.password.text
if SummonedSkull().check(flag):
self.result.text = 'Good job! Here\'s your flag:\nCSC{' + flag + '}'
else:
self.result.text = 'Wrong password :('
self.password.text = ''
class SummonedSkull(App):
encoded = [117, 36, 113, 36, 215, 49, 177, 228, 102, 117, 52, 215, 179, 49, 97, 179, 215, 177, 228, 113, 241, 54, 161, 51, 215, 164, 36, 35, 96, 51, 179, 241, 52, 52, 117, 116, 51, 53, 229, 215, 229, 215, 36, 49, 55, 103, 227, 99, 215, 215, 54, 36, 99, 179, 118, 102, 102, 117, 49, 35, 103, 116]
def build(self):
return ActivityLayout()
def check(self, message):
if len(message)!= 62:
return False
else:
encoded_message = encode(message)
for a, b in zip(self.encoded, encoded_message):
if a!= b:
return False
return True
if __name__ == '__main__':
SummonedSkull().run()
So it becomes clear that to get the flag, we need to reverse the encode function, so we can recover the password from the encoded array.
Looking at the encoded module that get’s imported it becomes clear that this loads a native binary. We can extract encode.so from lib/x86_64/libpybundle.so.
Decompiling this we can extract this function to see what the encode function does.
lVar4 = 0;
iVar2 = _PyArg_ParseTuple_SizeT(param_2,&DAT_001006af,&local_20,&local_28);
if (iVar2 != 0) {
lVar4 = PyList_New(local_28);
if (lVar4 == 0) {
lVar4 = 0;
}
else if (0 < (long)local_28) {
uVar8 = 2;
lVar7 = 0;
do {
if ((uVar8 | local_28) >> 0x20 == 0) {
uVar6 = (uVar8 & 0xffffffff) % (local_28 & 0xffffffff);
}
else {
uVar6 = (long)uVar8 % (long)local_28;
}
bVar1 = *(byte *)(local_20 + uVar6);
uVar3 = (uint)bVar1;
uVar5 = PyLong_FromLong((bVar1 & 1) << 6 ^ bVar1 & 2 ^
((uVar3 & 8) << 4 |
bVar1 >> 2 & 4 |
uVar3 & 0x20 | (bVar1 >> 6 & 1) + (uint)(bVar1 >> 7) * 8) +
(uVar3 & 4) * 4);
PyList_SetItem(lVar4,lVar7,uVar5);
lVar7 = lVar7 + 1;
uVar8 = uVar8 + 0x4f;
} while (lVar7 < (long)local_28);
}
}
return lVar4;
So we can see that this function moves bytes around to a new position in a new list, and does some bit operations on each byte. Since this is done on byte level, we can just create a lookup table for each of these transformations to be able to reverse them, since there are only 256 options.
And similarly, we can easily reverse the shuffling to get the original password back.
Solution Script
encoded = [117, 36, 113, 36, 215, 49, 177, 228, 102, 117, 52, 215, 179, 49, 97, 179, 215, 177, 228, 113, 241, 54, 161, 51, 215, 164, 36, 35, 96, 51, 179, 241, 52, 52, 117, 116, 51, 53, 229, 215, 229, 215, 36, 49, 55, 103, 227, 99, 215, 215, 54, 36, 99, 179, 118, 102, 102, 117, 49, 35, 103, 116]
pass_len = len(encoded)
def build_inverse_table():
forward = {}
for b in range(256):
byte_val = b
out = (
((b & 1) << 6) ^
(b & 2) ^
((byte_val & 8) << 4) |
(b >> 2 & 4) |
(byte_val & 0x20) |
(b >> 6 & 1) + (b >> 7) * 8 +
(byte_val & 4) * 4
)
out &= 0xFF
forward[b] = out
return {v: k for k, v in forward.items()}
def reverse_encode():
global encoded
global pass_len
inverse_table = build_inverse_table()
decoded = [0] * pass_len
step = 2
for i in range(pass_len):
mapped_i = step % pass_len
decoded[mapped_i] = inverse_table[encoded[i]]
step += 0x4f
return decoded
decoded = reverse_encode()
print("CSC{" + bytes(decoded).decode() + "}")