ImaginaryCTF 2023 - Reverse Engineering Writeups
Solutions to some reverse engineering challenges in the event.
Overview
Last weekend, I participated in the event with my team, thehackerscrew
, and we finished at rank 5/878
. Was really hoping that we could get the last banger from a crypto challenge to bring us 4th
, but we couldn’t make it.
For reverse engineering, we did 6/8
challenges.
All of the challenges’ binaries can be found here: Click me!
chaos
- Given file:
chall.py
- Description: Can you find order in the midst of chaos?
For this challenge, we are given a Python source with an one-liner checker. Even though it seems to be heavily obfuscated, we can still see some really interesting parts, one of which is shown below.
|
|
The part \x62\x79\x74\x65\x73
evaluates to bytes
and \x36\x39\x20\x36\x65\x20\x37\x30
evaluates to 69 6e 70
, which is inp
.
So, we can easily sketch out the checker scheme, like this:
assert inp[a ^ b] ** e == ct
Knowing the algorithm, I wrote a quick and dirty script to solve it.
|
|
Somehow the }
character didn’t show up, but that didn’t heavily affect the result anyway.
First blood for thehackerscrew
!
Flag is: ictf{pYthOn_obFuScAtION_iS_N0_M4TCH_f0r_U_9e1b23f9}
snailchecker
- Given file:
check.py
- Description: Optimize me, if you dare. Or not. It might run if you try hard enough.
We are given a shorter (and cleaner) Python source. I modified the enc()
to see what it does.
The part b[0] * 2 ** 24 + b[1] * 2 ** 16 + b[2] * 2 ** 8 + b[3] + 1
behaves the same with int.from_bytes(b, 'big') + 1
so I changed it, then tried to call enc(b'a')
while printing out the a
array inside.
|
|
|
|
From the result, we can see the behaviour of the enc()
, that first it generates an array contains elements from 1
to int.from_bytes(b, b'big')
. Consider the array as a circle, where every 2nd
element is erased until there is only 1
element left. The remaining element will be the return value of the function.
With Competitive Programming experience, I immediately recognized that this was implementing Josephus Problem . So our problem is given the winner of the game, we need to find how many people were involved in total.
I found this paper on Josephus Problem
to solve it. Given that we have n
people involved. The winner’s position - which is defined by j(n)
- is calculated as:
j(n) = left rotation of the binary digits of n
And, by doing right rotation on the encrypted value, we have solved our problem!
|
|
Somehow the flag should has _
in place of that ?
, but again that didn’t affect too much on the result.
Second blood for thehackerscrew
!
Flag is: ictf{josephus_problem_speed_boooooooost}
scrambled
- Given file:
main
- Description: The flag is all jumbled up… or is it?
We are given an ELF
file this time. Follow the xrefs*
of either CORRECT!
or INCORRECT!
, we can reach the main function for encryption.
|
|
After some time debugging the code, I got the idea of it. Basically, the encryption is very simple. Consider zyx^wu%v!ts1r;q@op#nm$lk(ijgh*fed&ca0b-{})
as the custom alphabet. Each character in the input is changed to the opposite
character in terms of their positions in the alphabet - for example z
will be changed into )
, y
will be changed to }
, and so on.
I wrote a script to decode the data, as below.
|
|
I had to change encoded data from oynuuvefmqjn1qlfnw$j*vmx1dv
to ynuuvefmqjn1qlfnw$j*vmx1dvo
to get the right order of the flag characters.
Flag is: ictf{Scr@mbl1ngfl@g$1sc00l}
Sheepish
- Given file:
sheepish.py
- Description: Mary had a flagchecker, its fleece was white as snow.
For this challenge, we are given a Python source. From the first glance, the source looks like it has control flow flattened by lambda functions.
So first, I did …
Actually, I found (probably) an unintended solution to just use side channel attack to bruteforce the flag. I tested for a while and saw that the flag is checked char by char, and when a char is wrong, the program immediately prints out Try again
. So from that information, we can easily see that if we input in any correct prefix of the flag, the program should raise some errors.
I tried to do so and things went the same with what I had expected.
|
|
So I went on and wrote a script using side channel attack to get the flag.
|
|
Flag is: ictf{d0_sh33p_b@@@?}
vmcastle
- Given file:
vm
andprogram
- Description: There are no rules of architecture for a castle in the clouds.
A vm
challenge is given to us for this challenge. My way of tackling a vm
challenge is to first write an interpreter.
But before doing that, we should take a look at how the vm
and the variables are set up.
|
|
Here is the main loop of the vm
. We can easily see that with ptr
contains the program, the vm works by continuously calling the following function: funcs[ptr[2 * i]](ptr[2 * i + 1])
. So for each pair of bytes from the program, the first byte is used to indicate which function is called, and the second byte will be the parameter for that function.
The memory of the vm
is declared as a block of length 1024
containing DWORDs
from offset 0x5840
. Right next to the memory block within the bss segment are some global variables, as shown below.
|
|
So to make the vm
interpreter behaves correctly, we have to consider dword_6840
as memory[1025]
, dword_6844
as memory[1026]
, and so on. Also take into account that ptr
takes up a 2-byte space, not 1-byte space.
Further more, the analysis into the functions also reveals that one function might be interpreted wrong if not handled correctly, which is the below function - corresponds with opcode 104
.
|
|
The assembly code for this function uses movsxd rdx, edx
instead of move without sign extend to assign a1
onto the memory, so we have to replicate that when writing an interpreter for the program.
Considered all of that, I wrote an interpreter for this vm
.
|
|
|
|
From the output, I saw that the input is read until the program encounters \n
, which in ASCII
is 10
. Then the flag is checked backward through some maths and then checked with a hardcoded value.
Again, I used side channel attack to bruteforce the flag backward, using the below script.
|
|
Flag is: ictf{babyvm_sUccessfULly_craCKEd_ee938a5954da916add9bae5f1ebc458561e231134ae5ea06}
Typechecker
- Given file:
ts.ts
- Description: All you need is to make it compile without error!
We are given a source written in Typescript. From what was given in the source, I can see that it probably does something related to matrix. But first we should deobfuscate the variable names xD
|
|
From the source, we probably can see that it is doing something with matrices. And it is also really easy to see that the SourceString
and TargetString
are converted into matrix using the Charset
, or we can use VSCode extension
to debug and retrieve values of SourceType
and TargetType
.
Our team was stucked for quite a while on what to do next then someone made a guess that ComputeGrid
was just doing matrix multiplication in GF(67)
. So I went on and wrote a sage script, while another person tried to debug, then compared the results.
And, it was indeed doing matrix multiplication in GF(67)
. From the source, with that precious information, we went on to conclude that matrix x
of size 8 * 8
, with 2
last elements are padded with 2
0s
such that ax = xb
in GF(67)
will be the flag for our challenge.
The final works were handled by a Crypto player from the team, and he didn’t let us down on this.
|
|
Flag is: ictf{who_need_javascript_when_you_can_do_it_all_in_typescript}
After having solved it, he had a short comment on the challenge xD
Appendix
The challenges for this year’s edition were quite fun to do, our team had a good time solving them together.
If you have any question regarding the above solutions, you can DM me via my Twitter
or my Discord
(FazeCT #fazect).