Writeup - SEMIRC LASREVER (ASVCTF 2022)
ASVCTF 2022 - SEMIRC LASREVER Writeup
- Type - Rev
- Name - SEMIRC LASREVER
- Points - 450
Description
1 | Reversing binaries is fun, but have you ever seen a portal that has code so horrible, |
Writeup
From the moment I realized this challenge was reverse engineering JavaScript code instead of assembly, I knew that I’d want first blood on it! Reverse engineering regular code is one of my favorite things to do. We were given a website that had two JS files attached and validated a flag that we put into it.
The first JS file, f0.js, added an event listener so that each time the input box for the flag was changed, !m[0](e.target.value, handler)
was called. The handler function defined just above just showed you whether or not the flag was correct, m[0]
was the function we needed to reverse engineer, and the first parameter passed in was our flag. m
is defined in f0.js, which was where all our ugly code was. The first thing I did was throw it into a JavaScript code beautifier, which made it much nicer for us:
Part 1
Part 1 was reducing this ugly JS to a function call with more ugly JS. To get there, I took large chunks of the code and ran them individually in the JavaScript web console to see what they returned. For example, running the code:
1 | ((te) => { |
gave the output 'pop'
. Running the code inside each of the main 4 square brackets []
resulted in the following code:
1 | ((...ಠ_ಠ) => []['pop']['constructor']('function t(t){...};')['call'](ಠ_ಠ))(m) |
I isolated the large chunk of code inside the string and ran it through a beautifier again, resulting in part2.js
Part 2
Part 2 has 3 main code blocks:
1 | function t(t) { |
The third section with the this
variable is how running !m[0](e.target.value, handler)
from f0.js ran the c()
function, with n
as the inputted flag to verify and e
as the handler function. The second section (function c
) was pretty boring except for two parts - first, the call to t(n)
on line 85 showed that the inputted flag was sent as the parameter to the t()
function. Secondly, line 84 shows that n.length % 5 == 0
, meaning that the flag length is a multiple of 5! So we now know something about the flag, and we know that the function t()
will return True
if the flag is correct, and False
if it’s not. We’re getting close!
Part 2.5
Function t()
is the ugliest code so far and ties a bit into cryptography with a custom PRNG represented by e()
and like a four-part “decryption” process going on. I tried to break it down a bit and came across a huge breakthrough. The return statement for the function t()
spanned lines 21-80, and it was just a boolean check expression1 == expression2
. Expression1
went from lines 21-75, and Expression2
was lines 75-80. Wanting to know what the “correct” value was going to be, I threw lines 75-80 into the console:
1 | // hard coded values from line 2 |
The result? 0f8fcc159b8ec8d253a9d038b43fa0799bb0c43fa0799bb0cd286b533c65371
We now had a 63-character long hard-coded value that we needed to match after the “encryption” process. This is when I decided to switch over to dynamic analysis instead of static analysis. Instead of going through the code line by line, I could put in various values and test how the output changed. As another quick side note, line 48 broke up part of the encrypted text into sections, and the substrings went up to 25 - since the flag length was divisible by 5, I knew the flag had to be 25 characters long.
For my dynamic testing, I created solve.js where I placed a slightly modified version of the t()
function in along with various flag payloads I could test and see the output. My baseline was aaaaaaaaaaaaaaaaaaaaaaaaa
(25 * a
), and this returned 31b8e01619dccb8131eb30fbc13c42fbc855e13c42fbc855ee0b8e061dc6124
. I then tried the payloads baaaaaaaaaaaaaaaaaaaaaaaa
and caaaaaaaaaaaaaaaaaaaaaaaa
and got returned the values 31b8e01619dcfb8131eb30fbc13c42fbc855e13c42fbc855ee0b8e061dc6124
and 31b8e01619dceb8131eb30fbc13c42fbc855e13c42fbc855ee0b8e061dc6124
, respectively. At first glance, those next 2 results look the same, but in actuality the 13th character was different. I then tried the payload abaaaaaaaaaaaaaaaaaaaaaaa
, which gave me 31b8e01619dccb8131eb30fbc13c42fbc855e13c42fbc855ee0b8e061dc6224
. Now that a
was the first letter of the flag again, the 13th letter was the same. However, b
as the second letter changed the 3rd-to-last character from 1
to 2
. At this point, I concluded that there was a one-to-many relationship between characters in the flag and outputted characters. This meant that I could brute force the flag letter by letter since the value of one character didn’t affect the output of any other parts of the encrypted output.
To establish which letter of the flag corresponded with which characters in the output, I used my base payload of aaaaaaaaaaaaaaaaaaaaaaaaa
with b
s in all the different positions. This was my output:
1 | (baseline) |
Notice that some characters in the flag map to 4 outputted characters and not always 2, but that didn’t change anything about my solution. At this point, I was just like “Since I know that the first character of the flag affects the 12th and 13th outputted characters, I will try all possible printable characters for the first letter of the flag until the 12th and 13th letters of the output match our hard-coded answer”.
For example, even though we know that A
is the first letter because the flag format is ASV{}
, I could brute force the first letter and get Aaaaaaaaaaaaaaaaaaaaaaaaa
with this code:
1 | let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789{}_"; |
Final Solve Script
1 | function t(t) {...} |
Flag: ASV{D1D_I_M4K3_Y0U_SW34T}