Analysis
Para este reto se otorga un archivo zip que contiene tanto el binario como todo el código fuente del proyecto lo que hace que sea mas sencillo analizarlo, también se añaden los archivos de docker que definen la configuración cuando se lanza el reto.
❯ tree
.
├── Dockerfile
├── banner_fail
├── build.sh
├── chal
│ ├── Makefile
│ ├── chal
│ ├── chal.cpp
│ ├── check_functions.cpp
│ ├── check_functions.h
│ ├── hackdef.cpp
│ ├── hackdef.h
│ ├── libc.so.6 -> libchecks.so
│ ├── libchecks.so
│ ├── libhackdef.so
│ ├── print_flag.cpp
│ └── print_flag_docker
├── service.conf
├── start.sh
├── stop.sh
└── wrapper
2 directories, 19 files
El Dockerfile nos muestra que se copian varios archivos, los binarios, las librerías necesarias y las flags, tanto las individuales así como las específicas del equipo.
❯ cat Dockerfile
FROM ubuntu:latest
RUN apt-get -qq update && apt-get install -qq --no-install-recommends xinetd libssl3
RUN groupadd -g 1001 ctf
RUN useradd -m -u 1001 -g 1001 -s /bin/bash ctf
COPY service.conf /service.conf
COPY banner_fail /banner_fail
COPY wrapper /wrapper
COPY chal/chal /home/ctf/chal
RUN chown ctf:ctf /home/ctf/chal
COPY chal/libchecks.so /usr/lib/x86_64-linux-gnu/
COPY chal/libhackdef.so /usr/lib/x86_64-linux-gnu/
COPY chal/print_flag_docker /print_flag
RUN chmod 755 /print_flag
RUN chmod u+s /print_flag
COPY flags/flag_00.txt /
COPY flags/flag_01.txt /
COPY flags/flag_02.txt /
COPY flags/flag_03.txt /
COPY flags/flag_04.txt /
COPY flags/flag_05.txt /
RUN chmod 444 /flag_0*.txt
COPY flags/flag_06.txt /root/
COPY flags/flag_ahaumx.txt /root/
COPY flags/flag_bluetm.txt /root/
COPY flags/flag_hackfc.txt /root/
COPY flags/flag_hawkss.txt /root/
COPY flags/flag_mylpwn.txt /root/
COPY flags/flag_pwndir.txt /root/
COPY flags/flag_snoopy.txt /root/
COPY flags/flag_takzac.txt /root/
COPY flags/flag_yaquic.txt /root/
RUN chmod 400 /root/flag_*.txt
EXPOSE 1380
USER ctf
CMD ["/usr/sbin/xinetd", "-filelog", "/dev/stderr", "-dontfork", "-f", "/service.conf"]
Ya que no correremos el proyecto de docker para hacer el exploit será necesario mover los archivos manualmente, iniciando por mover el archivo print_flag a / asignando a root como propietario y privilegios suid, luego movemos las librerías.
❯ ls -l /print_flag
-rwsr-xr-x 1 root root 50912 Oct 19 15:18 /print_flag
❯ ls -l /usr/lib/x86_64-linux-gnu/libchecks.so
-rwxr-xr-x 1 root root 900168 Oct 19 15:17 /usr/lib/x86_64-linux-gnu/libchecks.so
❯ ls -l /usr/lib/x86_64-linux-gnu/libhackdef.so
-rwxr-xr-x 1 root root 267520 Oct 19 15:17 /usr/lib/x86_64-linux-gnu/libhackdef.so
Luego movemos todas las flags al directorio /, excepto la flag 06 y las flags de los equipos ya que esas se mueven al directorio /root, en este caso como solo nos interesa la del equipo MyLittlePwny solo movemos la flag con nombre mylpwn.
❯ ls -l /flag_0*.txt
.rw-r--r-- root root 31 B Sun Oct 19 16:38:10 2025 /flag_00.txt
.rw-r--r-- root root 31 B Sun Oct 19 15:54:24 2025 /flag_01.txt
.rw-r--r-- root root 31 B Sun Oct 19 15:54:24 2025 /flag_02.txt
.rw-r--r-- root root 31 B Sun Oct 19 15:54:24 2025 /flag_03.txt
.rw-r--r-- root root 31 B Sun Oct 19 15:54:24 2025 /flag_04.txt
.rw-r--r-- root root 31 B Mon Oct 20 11:53:27 2025 /flag_05.txt
❯ cat /flag_0*.txt
hackdef{0_f4k3_fl4g_4_t35t1ng}
hackdef{1_f4k3_fl4g_4_t35t1ng}
hackdef{2_f4k3_fl4g_4_t35t1ng}
hackdef{3_f4k3_fl4g_4_t35t1ng}
hackdef{4_f4k3_fl4g_4_t35t1ng}
hackdef{5_f4k3_fl4g_4_t35t1ng}
❯ sudo ls -l /root/flag_*.txt
.rw-r--r-- root root 31 B Sat Nov 1 15:58:43 2025 /root/flag_06.txt
.rw-r--r-- root root 37 B Sun Nov 2 10:58:22 2025 /root/flag_mylpwn.txt
❯ sudo cat /root/flag_*.txt
hackdef{6_f4k3_fl4g_4_t35t1ng}
hackdef{mylpwn_f4k3_fl4g_4_t35t1ng}
Luego de hacer el setup, ejecutamos el binario y podemos ver un menú con varias opciones pero también un checker el cual parece ejecutarse periódicamente.
❯ ./chal
Servicio interno de tarjetas de credito
** Menu:
1. Registrar tarjeta
2. Registrar NIP
3. Consultar NIP
4. Borrar NIP
5. Salir
> cnt_checker = 1
Checking... 550
OK
Ya que tenemos el código fuente en C podemos analizarlo directamente y no es necesario desensamblar el binario, iniciamos por la función main la cual se encarga de inicializar varios procesos y luego ejecuta de forma asincrónica varias funciones.
int main() {
init_buffers();
init_flagprocess();
init_seccomp();
init_handlers();
init_flags();
ignorar_signals();
std::future<void> fut_flag_check = std::async(std::launch::async, service_flag_checker);
std::future<void> fut = std::async(std::launch::async, service_checker);
std::future<void> fut2 = std::async(std::launch::async, main_menu);
fut.wait_for(std::chrono::seconds(30));
return 0;
}
La primera función interesante es init_handlers, esta se encarga de llenar el arreglo handlers con una función del checker, hasta llenar las 1160 posiciones del arreglo.
#define NUM_CHECK_FUNCS 290 * 4
CheckFunc handlers[NUM_CHECK_FUNCS];
void init_handlers() {
handlers[0] = fn_000;
handlers[1] = fn_001;
handlers[2] = fn_002;
handlers[3] = fn_003;
.........................
handlers[1157] = fn_1157;
handlers[1158] = fn_1158;
handlers[1159] = fn_1159;
}
La función service_checker que se ejecuta elige de forma aleatoria la variable op entre las 1160 opciones posibles, luego llama a execute_check y espera un máximo de 100ms, si el resultado es True simplemente devuelve OK, si es False devuelve que algo no anda bien pero si pasa el timeout directamente cierra el programa.
#define NUM_CHECK_FUNCS 290 * 4
#define TIMEOUT_CHECK_FUNC 100
void service_checker() {
int cnt_checks = 0;
while (cnt_checks++ < 4) {
std::cout << "cnt_checker = " << cnt_checks << std::endl;
int op = randint(NUM_CHECK_FUNCS - 1);
std::future<bool> fut = std::async(std::launch::async, execute_check, op);
if (fut.wait_for(std::chrono::milliseconds(TIMEOUT_CHECK_FUNC)) == std::future_status::ready) {
bool result = fut.get();
if (result) {
std::cout << "OK" << std::endl;
} else {
std::cout << "WA - Algo no anda bien. Te estare vigilando..." << std::endl;
salir();
}
} else {
std::cout << "Te descubri hacker!" << std::endl;
salir();
}
std::this_thread::sleep_for(std::chrono::seconds(10));
}
puts("Bye!");
salir();
}
La función execute_check toma el parámetro op elegido aleatoriamente para entrar en uno de los casos del switch, cada uno de los casos llama a un check que toma como argumento el valor del arreglo handlers en la posición que indica el caso.
bool execute_check(int op) {
std::cout << "Checking... " << op << std::endl;
switch (op) {
case 0: return check_000(handlers[0]);
case 1: return check_001(handlers[1]);
case 2: return check_002(handlers[2]);
.............................................
case 1157: return check_1157(handlers[1157]);
case 1158: return check_1158(handlers[1158]);
case 1159: return check_1159(handlers[1159]);
default:
std::cout << "NO" << std::endl;
salir();
}
return true;
}
Cada una de las funciones check toman como parámetro la función que se tomó del array y definen 5 argumentos de la a a la e asignándoles a cada uno un número entero diferentes para cada caso, luego de llamar a la función verifican que algunas variables tengan otros valores que define cada función y son diferentes a los originales, si no es así indicaría que no se llamó a la función y el programa se alteró.
#define TIMEOUT_CHECK_FUNC 160
bool check_000(CheckFunc check) {
int a=397540806, b=808190108, c=948268558, d=403076592, e=488643755;
std::future<void> fut = std::async(std::launch::async, check, &a, &b, &c, &d, &e);
if (fut.wait_for(std::chrono::microseconds(TIMEOUT_CHECK_FUNC)) == std::future_status::ready) {
if (b != 152045996) return false;
if (c != 727562417) return false;
if (d != 153545182) return false;
return true;
} else {
return false;
}
}
bool check_1159(CheckFunc check) {
int a=852726792, b=398756715, c=1025465896, d=783751113, e=391710928;
std::future<void> fut = std::async(std::launch::async, check, &a, &b, &c, &d, &e);
if (fut.wait_for(std::chrono::microseconds(TIMEOUT_CHECK_FUNC)) == std::future_status::ready) {
if (e != 898176514) return false;
if (a != 117814948) return false;
if (d != 87295856) return false;
return true;
} else {
return false;
}
}
Las funciones fn realmente son múy simples, toman cada uno de sus parámetros y modifican el valor de todas las variables, esto para la validación que vimos se hace en los checks, si se llama a cualquier otra función en el check los valores no coincidirán.
void fn_000(int *a, int *b, int *c, int *d, int *e) {
*b = 152045996;
*c = 727562417;
*a = 333394009;
*d = 153545182;
*e = 460636857;
}
void fn_1159(int *a, int *b, int *c, int *d, int *e) {
*b = 76210336;
*c = 778987935;
*d = 87295856;
*a = 117814948;
*e = 898176514;
}
Esta implementación busca que no se altere el flujo del programa eficientemente debido a su aleatoriedad y el poco tiempo asignado para los checks, esto último se usa como anti-debugging que hace que sea complicado depurar el programa.





