Go Sweep
15 minutos de lectura
Se nos proporciona un binario llamado GoSweep
:
$ file GoSweep
GoSweep: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, Go BuildID=E8AKvGNouZccNMS4QzKx/gv5u3Bl0aF1q0beXnu2L/ZgB_HzVs3SKcdMITee5O/W5Hg19NTpamr0Is20WCh, stripped
El binario ejecuta un servidor web para jugar al buscaminas. Tenemos una buena interfaz de usuario en la URL remota:
Se trata de un binario despojado (stripped) compilado en Go, por lo que será un poco difícil realizar ingeniería inversa. Vale la pena mencionar que este reto estaba clasificado como Reversing/Web, aunque creo que cuadra más en Reversing.
Ingeniería inversa
El propósito del reto es bastante obvio, necesitamos ganar el juego del buscaminas en tiempo. En lugar de ganar el juego usando un solucionador de buscaminas, es mejor averiguar cómo se genera el tablero y determinar las posiciones de las minas.
Esta vez, estoy usando IDA porque Ghidra era horrible analizando Go. Por ejemplo, IDA pudo detectar funciones de Go:
Generación del tablero
Comencemos analizando cómo se generan los tableros del buscaminas. En realidad hay dos funciones, main_initBoard
y main_initLargeBoard
. Comenzaremos con la primera:
// main.initBoard
__int64 __golang main_initBoard(int64 a1, int a2, int a3, unsigned __int64 a4, int a5, int a6, int a7, int a8, int a9)
{
// ...
slice = runtime_makeslice((unsigned int)&RTYPE__slice_structs_Cell, 20, 20, a4, a5, a6, a7, a8, a9);
for ( i = 0LL; i < 20; i = v16 + 1 )
{
v38 = i;
v15 = runtime_makeslice((unsigned int)&RTYPE_structs_Cell, 20, 20, a4, a5, v10, v11, v12, (_DWORD)v13);
v16 = v38;
if ( v38 >= 20 )
runtime_panicIndex(v38);
a5 = slice;
a4 = 3 * v38;
*(_QWORD *)(slice + 8 * a4 + 8) = 20LL;
*(_QWORD *)(slice + 8 * a4 + 16) = 20LL;
if ( dword_93B840 )
{
v15 = runtime_gcWriteBarrier2(v15);
*v13 = v15;
v10 = *(_QWORD *)(slice + 24 * v38);
v13[1] = v10;
}
*(_QWORD *)(slice + 24 * v38) = v15;
}
p_rand_rngSource = (rand_rngSource *)runtime_newobject(&RTYPE_rand_rngSource);
math_rand__ptr_rngSource_Seed(p_rand_rngSource, a1);
for ( j = 0xE839DD11LL; ; j = v23 )
{
v20 = j;
v21 = 16 * (*(_QWORD *)off_8CFC80 & j);
v22 = *(RTYPE **)((char *)off_8CFC80 + v21 + 8);
if ( v22 == &RTYPE__ptr_rand_rngSource )
break;
v23 = v20 + 1;
if ( !v22 )
{
v24 = runtime_typeAssert(
(unsigned int)&off_8CFC80,
(unsigned int)&RTYPE__ptr_rand_rngSource,
(_DWORD)off_8CFC80,
a4,
v21,
v23,
0,
v17,
v18);
goto LABEL_13;
}
}
v24 = *(_QWORD *)((char *)off_8CFC80 + v21 + 16);
LABEL_13:
v41 = v9;
v40[0] = off_734748;
v40[1] = p_rand_rngSource;
v40[2] = v24;
v40[3] = p_rand_rngSource;
numMines = 0LL;
while ( numMines < 150 )
{
tmpNumMines = numMines;
a = math_rand__ptr_Rand_Intn(v40, 20LL);
b = math_rand__ptr_Rand_Intn(v40, 20LL);
if ( a >= 20 )
runtime_panicIndex(a);
if ( b >= *(_QWORD *)(slice + 24 * a + 8) )
runtime_panicIndex(b);
bomb = (_BYTE *)(*(_QWORD *)(slice + 24 * a) + 24 * b + 1);
if ( *bomb )
{
numMines = tmpNumMines;
}
else
{
*bomb = 1;
for ( k = -1LL; k <= 1; ++k )
{
for ( m = -1LL; m <= 1; ++m )
{
v31 = k + a;
v32 = m + b;
if ( k + a < 20 && v32 < 20 )
{
if ( v31 >= 20 )
runtime_panicIndex(k + a);
v33 = 3 * v31;
v34 = *(_QWORD *)(slice + 8 * v33 + 8);
v35 = *(_QWORD *)(slice + 8 * v33);
if ( v34 <= v32 )
runtime_panicIndex(m + b);
++*(_QWORD *)(v35 + 24 * v32 + 8);
}
}
}
numMines = tmpNumMines + 1;
}
}
return slice;
}
Como se puede ver, aunque IDA funciona bien con GO, la descompilación sigue siendo muy fea y difícil de leer.
La definición del tablero parece clara, es solo un slice de 20x20:
slice = runtime_makeslice((unsigned int)&RTYPE__slice_structs_Cell, 20, 20, a4, a5, a6, a7, a8, a9);
for ( i = 0LL; i < 20; i = v16 + 1 )
{
v38 = i;
v15 = runtime_makeslice((unsigned int)&RTYPE_structs_Cell, 20, 20, a4, a5, v10, v11, v12, (_DWORD)v13);
v16 = v38;
if ( v38 >= 20 )
runtime_panicIndex(v38);
a5 = slice;
a4 = 3 * v38;
*(_QWORD *)(slice + 8 * a4 + 8) = 20LL;
*(_QWORD *)(slice + 8 * a4 + 16) = 20LL;
if ( dword_93B840 )
{
v15 = runtime_gcWriteBarrier2(v15);
*v13 = v15;
v10 = *(_QWORD *)(slice + 24 * v38);
v13[1] = v10;
}
*(_QWORD *)(slice + 24 * v38) = v15;
}
Como se puede ver, establecí un nombre de variable en slice
para mejorar el proceso de ingeniería inversa. Aún así, algunos puntos aún no están del todo claros. Por ejemplo, ¿por qué hay algunos 24
por ahí? Es un desplazamiento extraño.
Estas cosas extrañas vuelven a aparecer cuando el tablero se está llenando de bombas:
numMines = 0LL;
while ( numMines < 150 )
{
tmpNumMines = numMines;
a = math_rand__ptr_Rand_Intn(v40, 20LL);
b = math_rand__ptr_Rand_Intn(v40, 20LL);
if ( a >= 20 )
runtime_panicIndex(a);
if ( b >= *(_QWORD *)(slice + 24 * a + 8) )
runtime_panicIndex(b);
bomb = (_BYTE *)(*(_QWORD *)(slice + 24 * a) + 24 * b + 1);
if ( *bomb )
{
numMines = tmpNumMines;
}
else
{
*bomb = 1;
for ( k = -1LL; k <= 1; ++k )
{
for ( m = -1LL; m <= 1; ++m )
{
v31 = k + a;
v32 = m + b;
if ( k + a < 20 && v32 < 20 )
{
if ( v31 >= 20 )
runtime_panicIndex(k + a);
v33 = 3 * v31;
v34 = *(_QWORD *)(slice + 8 * v33 + 8);
v35 = *(_QWORD *)(slice + 8 * v33);
if ( v34 <= v32 )
runtime_panicIndex(m + b);
++*(_QWORD *)(v35 + 24 * v32 + 8);
}
}
}
numMines = tmpNumMines + 1;
}
}
return slice;
Esta parte me llevó un tiempo hasta entenderla completamente. Es obvio que a
y b
son dos números aleatorios entre 0
y 19
, que representan la fila y la columna donde se va a colocar una bomba. Estos números se generan usando math/rand
, inicializado con una semilla.
Además, vemos un bucle while
hasta que numMines
sea igual a 150
, por lo que está claro que la el tablero de 20x20 tendrá 150 bombas.
También podemos notar que si bomb
ya es un 1
, el programa no aumenta numMines
y continúa a la siguiente iteración de bucle.Después de eso, vemos dos bucles for
anidados, ambos de -1
a 1
. Estos bucles for
están destinados a aumentar la cantidad de minas adyacentes en las casillas adyacentes:
for ( k = -1LL; k <= 1; ++k )
{
for ( m = -1LL; m <= 1; ++m )
{
v31 = k + a;
v32 = m + b;
if ( k + a < 20 && v32 < 20 )
{
if ( v31 >= 20 )
runtime_panicIndex(k + a);
v33 = 3 * v31;
v34 = *(_QWORD *)(slice + 8 * v33 + 8);
v35 = *(_QWORD *)(slice + 8 * v33);
if ( v34 <= v32 )
runtime_panicIndex(m + b);
++*(_QWORD *)(v35 + 24 * v32 + 8);
}
}
}
Sin embargo, los siguientes fragmentos de código se ven raros, debido a los offsets de 1
, 8
y 24
:
bomb = (_BYTE *)(*(_QWORD *)(slice + 24 * a) + 24 * b + 1);
++*(_QWORD *)(v35 + 24 * v32 + 8);
La clave para comprender esta parte se encuentra en una función llamada type__eq_mines_struct_Cell
:
// type:.eq.mines/struct.Cell
bool __golang type__eq_mines_struct_Cell(__int64 a1, __int64 a2)
{
return *(_BYTE *)a2 == *(_BYTE *)a1
&& *(_BYTE *)(a2 + 1) == *(_BYTE *)(a1 + 1)
&& *(_QWORD *)(a2 + 8) == *(_QWORD *)(a1 + 8)
&& *(_BYTE *)(a2 + 16) == *(_BYTE *)(a1 + 16);
}
Con esto, podemos deducir que cada celda se implementa como una struct
con 4 tipos (1 byte, 1 byte, 8 bytes, 1 byte). Esto tiene sentido si echamos un vistazo al tablero generado en GDB. Podemos establecer un breakpoint en main_initBoard
(0x643f20
) y ejecutar finish
:
$ gdb -q GoSweep
Loading GEF...
Reading symbols from GoSweep...
(No debugging symbols found in GoSweep)
gef> break *0x643f20
Breakpoint 1 at 0x643f20
gef> run
Starting program: /root/GoSweep/GoSweep
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7fffb12d6640 (LWP 4141107)]
[New Thread 0x7fffb0ad5640 (LWP 4141108)]
[New Thread 0x7fffabfff640 (LWP 4141116)]
[New Thread 0x7fffab7fe640 (LWP 4141119)]
[New Thread 0x7fffaaffd640 (LWP 4141132)]
2024/09/14 19:18:21 Server started on :8080
Thread 1 "GoSweep" hit Breakpoint 1, 0x0000000000643f20 in ?? ()
Ahora, hacemos una petición al servidor para que genere un nuevo tablero. (curl 127.0.0.1:8080/new
) y el programa llegará al breakpoint:
gef> finish
Run till exit from #0 0x0000000000643f20 in ?? ()
0x000000000064478c in ?? ()
Aquí podemos ver el slice de slices:
gef> x/10gx $rax
0xc0001cc000: 0x000000c0001ce000 0x0000000000000014
0xc0001cc010: 0x0000000000000014 0x000000c0001ce1e0
0xc0001cc020: 0x0000000000000014 0x0000000000000014
0xc0001cc030: 0x000000c0001ce3c0 0x0000000000000014
0xc0001cc040: 0x0000000000000014 0x000000c0001ce5a0
Tiene sentido porque en Go, un slice está formado por un puntero a los elementos (ptr
), un número entero que representa la longitud del slice (len
), y otro número entero que representa la capacidad del slice (cap
). Ambos campos enteros son 0x14
, o 20
en formato decimal, lo cual es correcto. Más información sobre internals de slices en Go en go.dev.
Si examinamos el primer ptr
, obtenemos el array de estructuras Cell
:
gef> x/30gx 0x000000c0001ce000
0xc0001ce000: 0x0000000000000100 0x0000000000000004
0xc0001ce010: 0x0000000000000000 0x0000000000000100
0xc0001ce020: 0x0000000000000005 0x0000000000000000
0xc0001ce030: 0x0000000000000100 0x0000000000000003
0xc0001ce040: 0x0000000000000000 0x0000000000000000
0xc0001ce050: 0x0000000000000001 0x0000000000000000
0xc0001ce060: 0x0000000000000000 0x0000000000000000
0xc0001ce070: 0x0000000000000000 0x0000000000000000
0xc0001ce080: 0x0000000000000001 0x0000000000000000
0xc0001ce090: 0x0000000000000000 0x0000000000000001
0xc0001ce0a0: 0x0000000000000000 0x0000000000000000
0xc0001ce0b0: 0x0000000000000002 0x0000000000000000
0xc0001ce0c0: 0x0000000000000100 0x0000000000000002
0xc0001ce0d0: 0x0000000000000000 0x0000000000000000
0xc0001ce0e0: 0x0000000000000003 0x0000000000000000
Por ejemplo, esta es una sola Cell
(0x18
bytes, que es 24
en decimal):
gef> x/3gx 0x000000c0001ce000
0xc0001ce000: 0x0000000000000100 0x0000000000000004
0xc0001ce010: 0x0000000000000000
Y significa que la celda contiene una bomba y hay 4 bombas adyacentes. El resto de los campos aún se desconocen.
Sin embargo, si echamos un vistazo a las peticiones web, podemos adivinar los otros dos campos (revealed
y flagged
):
Entonces, la única parte restante de la generación del tablero es determinar la semilla. ¡Pero el servidor muestra la semilla!
Solución
Con todo esto, podemos implementar una función en Go para generar el mismo tablero dada la semilla:
func initBoard(seed int64, size, mines int) [][]Cell {
r := rand.New(rand.NewSource(seed))
board := make([][]Cell, size)
numMines := 0
for i := range size {
board[i] = make([]Cell, size)
}
for numMines < mines {
a := r.Intn(size)
b := r.Intn(size)
if !board[a][b].Bomb {
board[a][b].Bomb = true
for k := -1; k <= 1; k++ {
for m := -1; m <= 1; m++ {
if k+a < size && m+b < size && k+a >= 0 && m+b >= 0 {
board[k+a][m+b].Adjacent++
}
}
}
numMines++
}
}
return board
}
Donde Cell
es la siguiente struct
:
type Cell struct {
Revealed bool `json:"revealed"`
Bomb bool `json:"bomb"`
Adjacent int `json:"adjacent"`
Flagged bool `json:"flagged"`
}
Una vez revisado el código anterior con GDB y visto que generaba el mismo tablero, implementé una forma de ganar el juego mediante peticiones web:
func reveal(gameId string, c, r int) *RevealCells {
guard <- struct{}{}
res, err := http.PostForm(fmt.Sprintf("%s/reveal?gameID=%s&col=%d&row=%d", URL, gameId, c, r), nil)
if err != nil {
panic(err)
}
body, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
defer res.Body.Close()
var data *RevealCells
if err := json.Unmarshal(body, &data); err != nil {
panic(err)
}
<-guard
return data
}
func solve(url string, size, mines int) {
res, err := http.Get(url)
if err != nil {
panic(err)
}
body, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
defer res.Body.Close()
var data *Board
if err := json.Unmarshal(body, &data); err != nil {
panic(err)
}
board := initBoard(data.Seed, size, mines)
for r := range size {
for c := range size {
go func() {
wg.Add(1)
defer wg.Done()
if !board[r][c].Bomb {
data := reveal(data.GameId, c, r)
if data.Message != "" {
fmt.Println(data.Message)
}
}
}()
}
}
wg.Wait()
}
Es importante usar threads para ganar el juego en tiempo. Una vez que resolvemos el juego, no se muestra la flag, sino unos nanosegundos:
$ go run solve.go
Good job! Take some nanos: 1726246281186930269
Vale la pena mencionar que siempre es el mismo número.
Tablero grande
Si echamos un vistazo a main_revealCellHandler
:
v130 = v46[7];
if ( *(_QWORD *)(v130 + 32) )
{
time_stopTimer(v130 + 8, (_DWORD)v49, v48, v128, v50, (_DWORD)v42, (_DWORD)v43, (_DWORD)v44, v45, v331);
v367 = v9;
v136 = runtime_convT64(qword_93B3B0, (_DWORD)v49, v131, v128, v50, v132, v133, v134, v135);
*(_QWORD *)&v367 = &RTYPE_int;
*((_QWORD *)&v367 + 1) = v136;
*((_QWORD *)&v147 + 1) = 1LL;
v352 = fmt_Sprintf(
(unsigned int)"Good job! Take some nanos: %d",
29,
(unsigned int)&v367,
1,
1,
v137,
v138,
v139,
v140,
v331,
v332,
v333,
v334);
v341 = 29LL;
// ...
*v152 = v157;
if ( v357[3] == 50LL )
{
if ( v357[4] == 50LL )
{
File = os_ReadFile(
(unsigned int)"flag.txt",
8,
(_DWORD)v357,
v147,
1,
v153,
v154,
v155,
(_DWORD)v156,
v331,
v332);
*((_BYTE *)v46 + 40) = 1;
if ( (_QWORD)v147 )
{
v346 = 8LL;
v360 = File;
v367 = v9;
*(_QWORD *)&v147 = *(_QWORD *)(v147 + 8);
v367 = v147;
LODWORD(v147) = 1;
DWORD2(v147) = 1;
log_Fatalf(
(unsigned int)"Unable to read flag: %v",
23,
(unsigned int)&v367,
1,
1,
v161,
v162,
v163,
v164,
v331,
v332,
v333,
v334,
v335);
LODWORD(File) = v360;
}
v367 = v9;
v165 = File;
v166 = runtime_slicebytetostring(0, File, 8, v147, DWORD2(v147), v161, v162, v163, v164, v331, v332, v333);
v172 = runtime_convTstring(v166, v165, v167, v147, DWORD2(v147), v168, v169, v170, v171, v331);
*(_QWORD *)&v367 = &RTYPE_string;
*((_QWORD *)&v367 + 1) = v172;
v158 = 36LL;
LODWORD(v147) = 1;
*((_QWORD *)&v147 + 1) = 1LL;
v159 = fmt_Sprintf(
(unsigned int)"Congratulations! Take some chars: %s[...]",
36,
(unsigned int)&v367,
1,
1,
v173,
v174,
v175,
v176,
v331,
v332,
v333,
v334);
}
Vemos que hay dos respuestas diferentes cuando ganamos el juego (los nanosegundos y la flag). Mirando otras funciones, hay algunas de ellas relacionadas con un tablero “grande”. De hecho, main_initLargeBoard
es igual que main_initBoard
pero con una tabla de 50x50 y 2000 bombas.
El problema es que el endpoint /new
solo funciona para el tablero de 20x20. El tablero de 50x50 está oculto. Podemos ver cómo se genera en main_main
cuando el servidor arranca:
// main.main
void __fastcall main_main()
{
// ...
p_http_fileHandler = (http_fileHandler *)runtime_newobject(&RTYPE_http_fileHandler);
p_http_fileHandler->root.tab = off_7336A0;
p_http_fileHandler->root.data = &off_732060;
v3 = p_http_fileHandler;
net_http_Handle((unsigned int)&unk_6B93F8, 1, (unsigned int)off_7336C0, (_DWORD)p_http_fileHandler, v0, v4, v5, v6);
Seed = mines_randomizer_CreateSeed(qword_93B3B0, 1LL, v7, (__int64)v3, v0, v8, v9, v10, v11);
Endpoint = mines_randomizer_CreateEndpoint(Seed, 1, v13, (int)v3, v0, v14, v15, v16, v17);
v72[0] = (__int64)&RTYPE_string;
v72[1] = runtime_convTstring(Endpoint, 1, v19, (_DWORD)v3, v0, v20, v21, v22, v23, v54);
v28 = fmt_Sprintf((unsigned int)&unk_6B949E, 3, (unsigned int)v72, 1, 1, v24, v25, v26, v27, v55, v62, v64, v66);
net_http_HandleFunc(v28, 3, (unsigned int)off_6E8A80, 1, 1, v29, v30, v31, v32, v56);
// ...
}
La parte relevante aquí es que Endpoint
se genera a partir de Seed
, que viene de una función llamada mines_randomizer_CreateSeed
. Esta función es difícil de analizar mediante ingeniería inversa. Por otro lado, podemos ver que mines_randomizer_CreateEndpoint
aplica solo el hash MD5 a Seed
en formato texto:
// mines/randomizer.CreateEndpoint
__int64 __golang mines_randomizer_CreateEndpoint(
int a1,
int a2,
int a3,
int a4,
int a5,
int a6,
int a7,
int a8,
int a9)
{
// ...
v53[0] = &RTYPE_int64;
v53[1] = runtime_convT64(a1, a2, a3, a4, a5, a6, a7, a8, a9);
v9 = 1;
v10 = 1;
v15 = fmt_Sprintf(
(unsigned int)"%d80; ./h2%wTe]:%T\r\n\">OK2500",
2,
(unsigned int)v53,
1,
1,
v11,
v12,
v13,
v14,
v39,
v43,
v45,
v49);
v20 = runtime_stringtoslicebyte((unsigned int)&v52, v15, 2, 1, 1, v16, v17, v18, v19, v40, v44, v46);
crypto_md5_Sum(v20, v15, v21, 1, 1, v22, v23, v24, v25, v41, v47, v50);
v51 = v42;
slice = runtime_makeslice((unsigned int)&RTYPE_uint8, 32, 32, 1, 1, v26, v27, v28, v29);
i = 0LL;
j = 0LL;
while ( i < 16 )
{
v37 = *((_BYTE *)&v51 + i);
v31 = "0123456789abcdef";
if ( j >= 0x20 )
runtime_panicIndex(j);
*(_BYTE *)(slice + j) = a0123456789abcd[v37 >> 4];
v10 = j + 1;
v9 = (unsigned __int8)a0123456789abcd[v37 & 0xF];
if ( j + 1 >= 0x20 )
runtime_panicIndex(j + 1);
*(_BYTE *)(j + slice + 1) = v9;
++i;
j += 2LL;
}
return runtime_slicebytetostring(0, slice, 32, v9, v10, (_DWORD)v31, v32, v33, v34, v42, *((__int64 *)&v42 + 1), v48);
}
Una característica de Go es que las strings no están terminadas en con un byte nulo. En cambio, una cadena siempre aparece junto con su longitud. Es por eso que la siguiente instrucción es equivalente a fmt.Sprintf("%d", v53)
:
v15 = fmt_Sprintf(
(unsigned int)"%d80; ./h2%wTe]:%T\r\n\">OK2500",
2,
(unsigned int)v53,
1,
1,
v11,
v12,
v13,
v14,
v39,
v43,
v45,
v49);
Cuando estaba probando, tomé el endpoint oculto de GDB estableciendo breakpoints en mines_randomizer_CreateEndpoint
(0x53c280
) y mines_randomizer_CreateSeed
(0x53c3a0
). Luego, me di cuenta de que llamar al endpoint oculto era en realidad lo mismo que llamar /new
. Quiero decir, se nos da una semilla para generar el tablero de 50x50. Una vez que generemos y ganemos el juego, el servidor mostrará la flag.
Lo único que le quedaba era determinar cuál fue la entrada para mines_randomizer_CreateSeed
. Esto tenía que ser algo fijo, porque el endpoint oculto se define cuando se inicia el servidor, y se supone que el usuario no puede reiniciarlo.
Después de muchas pruebas, descubrí que la entrada a mines_randomizer_CreateSeed
fue el instante actual en nanosegundos. De hecho, el número devuelto del primer tablero es el instante en que se inició el servidor (fueron aproximadamente 10 minutos antes de que comenzara el CTF, por lo que estaba bien).
Entonces, tenemos todo; bueno, casi. En lugar de realizer ingeniería inversa a mines_randomizer_CreateSeed
, podemos usar GDB para modificar la entrada de mines_randomizer_CreateSeed
y obtener el resultado esperado. Luego, calculamos el hash MD5 y tendremos el endpoint oculto:
$ gdb -q GoSweep
Loading GEF...
Reading symbols from GoSweep...
(No debugging symbols found in GoSweep)
gef> break *0x53c3a0
Breakpoint 1 at 0x53c3a0
gef> run
Starting program: /root/GoSweep/GoSweep
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7fffb12d6640 (LWP 4155745)]
[New Thread 0x7fffb0ad5640 (LWP 4155761)]
[New Thread 0x7fffabfff640 (LWP 4155763)]
[New Thread 0x7fffab7fe640 (LWP 4155764)]
[New Thread 0x7fffaaffd640 (LWP 4155773)]
Thread 1 "GoSweep" hit Breakpoint 1, 0x000000000053c3a0 in ?? ()
gef> set $rax = (long) 1726246281186930269
gef> finish
Run till exit from #0 0x000000000053c3a0 in ?? ()
0x0000000000647e1b in ?? ()
gef> p/d $rax
$1 = 60357764246024467
gef> quit
$ python3 -q
>>> from hashlib import md5
>>> md5(b'60357764246024467').hexdigest()
'48f99e62219f9bfd9fd437b75e0d46f9'
En este punto, podemos reutilizar las funciones del script y simplemente agregar estas funciones para resolver el reto:
const URL = "https://gosweep.challs.m0lecon.it"
func main() {
solve(fmt.Sprintf("%s/new", URL), 20, 150)
solve(fmt.Sprintf("%s/48f99e62219f9bfd9fd437b75e0d46f9", URL), 50, 2000)
}
Flag
Si ejecutamos el script, obtendremos la flag:
$ go run solve.go
Good job! Take some nanos: 1726246281186930269
Congratulations! Take some chars: ptm{wh0_n33ds_luck_wh3n_y0u_h4v3_r3v3rs3?}
El script completo se puede encontrar aquí: solve.go
.