Hope you know JS
6 minutes to read
We are given an obfuscated JavaScript file called good-luck.js
. When running it on a simple HTML document it will show a prompt to validate the flag:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hope you know JS</title>
</head>
<body>
<script src="good-luck.js"></script>
</body>
</html>
Making use of Visual Studio Code Prettier formatter, I was able to find some patterns that where repeated along the code. For example, I started substituting expressions like +!![]
or ![]
by their final value.
Then I noticed that a function holded some method names like join
or length
, so I modified all the ocurrences.
Moreover, there where a lot of calls to parseInt
in the following format: parseInt(['1', '0', '0']['join](''))
, which simply results in 100
.
So, after deobfuscating the code at this point, I identified the function that checked the flag (checker
):
let input = prompt('Please enter your flag', 'Flag')
function check(my_input) {
return (
xde0152f8b3a135c172f6(
single_checker(my_input, 0),
single_checker(my_input, 1),
single_checker(my_input, 2),
single_checker(my_input, 3),
single_checker(my_input, 4)
) &
x923712f54875a51642ea(
single_checker(my_input, 5),
single_checker(my_input, 6),
single_checker(my_input, 7),
single_checker(my_input, 8),
single_checker(my_input, 9)
) &
xaa7b418e981417c0d7e7(
single_checker(my_input, 10),
single_checker(my_input, 11),
single_checker(my_input, 12),
single_checker(my_input, 13),
single_checker(my_input, 14)
) &
// ...
x437dc67f1c57b9b1fbfe(
single_checker(my_input, 240),
single_checker(my_input, 241),
single_checker(my_input, 242),
single_checker(my_input, 243),
single_checker(my_input, 244)
) &
xef0469cb269181c1465d(
single_checker(my_input, 245),
single_checker(my_input, 246),
single_checker(my_input, 247),
single_checker(my_input, 248),
single_checker(my_input, 249)
)
)
}
check(input) ? console.log('Congrats!') : console.log('Nope :(')
The function called single_checker
returns the character of my_input
depending on the number at the second argument:
function single_checker(my_input, number) {
var index = 0
return (
number % 0 == 0 && (x479fa79c4c78b87bf320 += 1),
number % 1 == 0 && (x24535896a451975e1ead += 1),
number % 2 == 0 && (x074117e2c33f667271a2 += 1),
number % 3 == 0 && (xcdbf3860ed96def693b7 += 1),
number % 4 == 0 && (x4bae623cbbe71530669c += 1),
number % 5 == 0 && (x2fa109dc60f6e5790e5f += 1),
number % 6 == 0 && (x0bac59156678535dce64 += 1),
number % 7 == 0 && (xd1fc6501f139a65febe9 += 1),
number % 8 == 0 && (x0266c50a73cb1a166c33 += 1),
number % 9 == 0 && (x9341682b6b6f842b3aef += 1),
number % 40 == 0 && (x65a3b4441948a4006f8b += 1),
x2fa109dc60f6e5790e5f == 6 && ((index = (number - 20) * 6), (index /= 2)),
number >= 30 && number <= 34 && (index = number),
x65a3b4441948a4006f8b == 2 &&
!x8dd0f0ac7f6bf83c1f50 &&
(x8dd0f0ac7f6bf83c1f50 = true),
x8dd0f0ac7f6bf83c1f50 &&
(number == 44 && (x8dd0f0ac7f6bf83c1f50 = false),
(index = number - 40),
(index *= 3),
(x65a3b4441948a4006f8b = 100)),
number == 185 && (index = number - 180 + 8),
number == 186 && (index = number - 180 + 8),
number == 187 && (index = -(number - 180) + 8),
number == 188 && (index = number - 180 + 8),
number == 189 && (index = number - 180 + 8),
number == 2 && (index = 2),
number == 4 && (index = 8),
number == 5 && (index = 35),
// ...
number == 242 && (index = 28),
number == 243 && (index = 36),
my_input[index].charCodeAt(0)
)
}
In checker
, there are a lot of different functions that take 5 values as arguments. These values are always single_checker(my_input, x)
up to single_checker(my_input, x + 4)
. All this type of functions only use specific parameters. For example, xde0152f8b3a135c172f6
:
function xde0152f8b3a135c172f6(
_0x14e395,
_0xb878fb,
_0x45c9e7,
_0x1145fd,
_0x2a1381
) {
var _0x30612b = _0x45c9e7 * _0x2a1381 == 2600
return _0x30612b
}
It only uses the third and fifth parameters. Remember that the value of the parameters are characters of the flag at specific indices as ASCII numbers.
So, we are left with a set of conditions between the characters of the flag to be met in order to validate the flag.
The way I wrote the conditions might not be efficient. I used the Firefox debugger and set breakpoints at the return
instruction at all the condition functions. Then, I entered a raw string of 256 ordered ASCII characters, so that the value of the parameters of the function coincide with the actual index:
$ node
Welcome to Node.js v18.10.0.
Type ".help" for more information.
> Array.from(Array(256).keys())
[
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,
36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71,
72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83,
84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95,
96, 97, 98, 99,
... 156 more items
]
> Array.from(Array(256).keys()).map(n => String.fromCharCode(n)[0]).join('')
'\x00\x01\x02\x03\x04\x05\x06\x07\b\t\n' +
'\x0B\f\r\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7F\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ'
Although the conditions can be solved manually, one can use z3
solver to get a flag that is correctly validated. These were the conditions:
s.add(a[2] * a[8] == 2600)
s.add(a[35] - a[39] == -1)
s.add(a[19] + a[37] == 149)
s.add(a[20] + a[25] == 200)
s.add(a[4] - a[7] == -49)
s.add(a[15] + a[24] - a[27] == 54)
s.add(a[18] + a[21] + a[24] - a[27] == 106)
s.add(a[30] + a[31] - a[32] + a[33] - a[34] == 48)
s.add(a[0] + a[28] == 153)
s.add(a[0] - a[6] == -3)
s.add(a[3] + a[9] == 99)
s.add(a[3] * a[9] * a[12] == 134640)
s.add(a[5] - a[23] == 47)
s.add(a[29] + a[36] == 148)
s.add(a[10] - a[38] == 50)
s.add(a[11] + a[26] == 147)
s.add(a[0] + a[22] == 99)
s.add(a[4] + a[39] == 103)
s.add(a[7] + a[25] == 199)
s.add(a[28] - a[37] == 1)
s.add(a[11] + a[29] == 146)
s.add(a[5] - a[20] == 2)
s.add(a[8] + a[38] == 102)
s.add(a[19] - a[35] == -5)
s.add(a[19] + a[35] == 101)
s.add(a[23] + a[36] == 105)
s.add(a[22] - a[26] == -50)
s.add(a[13] + a[19] == 98)
s.add(a[5] - a[30] == 4)
s.add(a[17] - a[26] == -50)
s.add(a[1] - a[35] == -2)
s.add(a[11] - a[27] == -3)
s.add(a[32] + a[39] == 156)
s.add(a[8] + a[14] == 99)
s.add(a[10] - a[16] == 2)
s.add(a[7] + a[31] == 150)
s.add(a[4] - a[33] == -5)
s.add(a[2] - a[34] == -1)
s.add(a[15] + a[20] == 154)
s.add(a[18] - a[37] == -45)
s.add(a[1] + a[13] + a[14] + a[16] + a[17] == 298)
s.add(a[21] - a[38] == -1)
s.add(a[0] - a[24] == 0)
s.add(a[19] + a[25] - a[36] == 98)
s.add(-a[0] + a[28] + a[29] == 148)
s.add(-a[4] + a[22] + a[23] == 53)
s.add(a[2] + a[5] - a[35] == 100)
s.add(a[7] + a[20] - a[29] == 100)
s.add(a[10] + a[11] - a[26] == 53)
s.add(a[8] + a[23] - a[38] == 52)
s.add(a[22] + a[37] - a[39] == 95)
s.add(a[25] + a[28] - a[36] == 152)
Now, let’s solve it:
$ python3 solve.py
33431e6b20f17217d080c3063eb4faa4f6553e46
Despite being correctly validated by good-luck.js
(the original one), it was not the correct flag. The issue is that there are two possible solutions due to these conditions:
s.add(a[3] + a[9] == 99)
s.add(a[3] * a[9] * a[12] == 134640)
The characters a[3]
and a[9]
are only involved in the above conditions. Since addition and multiplication are commutative, the values of a[3]
and a[9]
are interchangeable.
So, the correct flag was 96:21:33401e6b23f17217d080c3063eb4faa4f6553e46
.
The full script can be found in here: solve.py
.