Hope you know JS
6 minutos de lectura
Se nos proporciona un código JavaScript ofuscado llamado good-luck.js
. Al ejecutarlo en un documento HTML sencillo, muestra una ventana para validar la 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>
Haciendo uso del formateador Prettier en Visual Studio Code, pude encontrar patrones que se repetían a lo largo del código. Por ejemplo, empezé a sustituir expresiones como +!![]
o ![]
por sus valores finales.
Luego, vi que una función devolvía nombres de métodos como join
o length
, por lo que modifiqué todas las ocurrencias.
Además, había muchas llamadas a parseInt
de la siguiente forma: parseInt(['1', '0', '0']['join](''))
, que simplemente resulta en 100
.
Entonces, después de desofuscar el código hasta este punto, identifiquñe la función que validaba la 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 :(')
La función llamada single_checker
retorna el carácter de my_input
en función del número como segundo argumento:
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)
)
}
En checker
, hay un montón de funciones diferentes que toman 5 valores como argumentos. Estos valores son siempre single_checker(my_input, x)
hasta single_checker(my_input, x + 4)
. Todas las funciones de este tipo utilizan parámetros específicos. Por ejemplo, xde0152f8b3a135c172f6
:
function xde0152f8b3a135c172f6(
_0x14e395,
_0xb878fb,
_0x45c9e7,
_0x1145fd,
_0x2a1381
) {
var _0x30612b = _0x45c9e7 * _0x2a1381 == 2600
return _0x30612b
}
Esta solamente usa el tercer parámetro y el quinto parámetro. Recordemos que el valor de estos parámetros es un carácter de la flag en un índice concreto como número ASCII.
Entonces, nos quedamos con un conjunto de condiciones entre caracteres de la flag que tienen que cumplirse para validar la flag.
La manera en la que escribí las condiciones puede que no sea eficiente. Utilicé el depurador de Firefox poniendo breakpoints en las instrucciones return
de todas las funciones de condición. Luego, metí una cadena de 156 caracteres ASCII ordenados, de manera que los parámetros de la función coinciden con el índice:
$ 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 ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ'
Aunque las condiciones se pueden resolver de forma manual, se puede usar el solucionador z3
para obtener una flag que se valide correctamente. Estas eran las condiciones:
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)
Ahora, vamos a resolverlo:
$ python3 solve.py
33431e6b20f17217d080c3063eb4faa4f6553e46
Aunque se validaba correctamente por good-luck.js
(el código original), no era la flag correcta. El problema es que hay dos posibles soluciones debido a estas condiciones:
s.add(a[3] + a[9] == 99)
s.add(a[3] * a[9] * a[12] == 134640)
Los caracteres a[3]
y a[9]
solamente se involucran en las condiciones anteriores. Como la suma y la multiplicación son conmutativas, los valores de a[3]
y a[9]
son intercambiables.
Entonces, la flag correcta era 96:21:33401e6b23f17217d080c3063eb4faa4f6553e46
.
El script completo se puede encontrar aquí: solve.py
.