Skip to content
54 changes: 54 additions & 0 deletions 2025/Ctrl-Panic/Dor4_Null5/solveDora.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from pwn import *
from Crypto.Hash import SHA256, HMAC
from Crypto.Protocol.KDF import HKDF
from Crypto.Cipher import AES

def compute_path(navigation_key, challenge):
state = bytearray(16) + bytearray(challenge)
tracker = AES.new(navigation_key, AES.MODE_ECB)
for step in range(8):
scan = tracker.encrypt(bytes(state[step:step + 16]))
state[16 + step] ^= scan[0]
return bytes(state[-8:])

def solve_auth(secret_bytes, challenge_hex, server_token_hex):
challenge = bytes.fromhex(challenge_hex)
server_token = bytes.fromhex(server_token_hex)

nav_key = HKDF(
master=secret_bytes,
key_len=16,
salt=challenge + server_token,
hashmod=SHA256
)

expected = compute_path(nav_key, challenge)
h = HMAC.new(nav_key, expected, SHA256)
mask = h.digest()[:8]

response = bytes([expected[i] ^ mask[i] for i in range(8)])
return response.hex()

HOST = 'dora-nulls.ctf.prgy.in'
PORT = 1337

# --- HIPÓTESIS NULL5 ---
# Intentamos con 64 bytes nulos (0x00)
SECRET = b"*" * 64

io = remote(HOST, PORT, ssl=True)
io.sendlineafter(b"choose ", b"1")

my_challenge = "1122334455667788"
io.sendlineafter(b"challenge (hex): ", my_challenge.encode())
io.sendlineafter(b"username: ", b"Administrator")

io.recvuntil(b"server challenge: ")
server_token = io.recvline().strip().decode()
print(f"[*] Token: {server_token}")

resp_hex = solve_auth(SECRET, my_challenge, server_token)
print(f"[*] Response: {resp_hex}")

io.sendlineafter(b"response (hex): ", resp_hex.encode())
print(io.recvall(timeout=2).decode())
135 changes: 135 additions & 0 deletions 2025/Ctrl-Panic/Dor4_Null5/writeup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
## Análisis del Problema

El desafío presentaba un **servicio de autenticación basado en un esquema de desafío-respuesta (Challenge-Response)**.
El objetivo era obtener acceso como el usuario **Administrator**.

El código fuente revelaba tres componentes críticos:

### Derivación de Clave (KDF)

Se utiliza **HKDF** para generar una `navigation_key` a partir de:

- un **secreto**
- un **challenge del cliente**
- un **token del servidor**

### Generación de Ruta (`compute_path`)

Un algoritmo que utiliza **AES en modo ECB** para transformar el challenge inicial en un valor de **8 bytes** llamado:

```
expected_path
```

### Verificación Vulnerable (`verify_credential`)

Una función de verificación que utiliza un **XOR acumulativo** entre:

- el valor esperado
- la respuesta del usuario
- una máscara HMAC

---

## Identificación de Vulnerabilidades

### A. Hardcoded Secret (Insecure Storage)

En el diccionario `backpack`, el secreto del administrador estaba representado como:

```python
"Administrator": "****************************************************************"
```

Aunque en muchos casos esto es un **placeholder**, el nombre del reto y la lógica sugerían que el secreto real eran literalmente **64 caracteres de asterisco**.

Al probar este valor como `master_key` en **HKDF**, la derivación de la clave fue exitosa.

---

### B. Manipulación Algorítmica (XOR Checksum)

La vulnerabilidad técnica más profunda reside en cómo se valida la identidad.
El servidor **no compara hashes directamente**, sino que calcula un checksum:

```
checksum = expected ⊕ provided ⊕ mask
```

Para que la autenticación sea exitosa:

```
checksum = 0
```

Esto permite a un atacante despejar el valor necesario de `provided` (la respuesta enviada al servidor):

```
provided = expected ⊕ mask
```

---

## Estrategia de Resolución (Exploit)

Para resolver el reto se desarrolló un **script en Python** que realiza los siguientes pasos.

### 1. Intercepción

Conectarse al servidor y solicitar un inicio de sesión para `Administrator`, enviando un challenge arbitrario, por ejemplo:

```
1122334455667788
```

---

### 2. Simulación Local

Replicar localmente la lógica criptográfica del servidor.

- Utilizar el `server_token` proporcionado por el servidor en tiempo real.
- Replicar la función **HKDF** usando como secreto:

```python
b"*" * 64
```

También se probaron previamente:

```python
"0" * 64
b"\x00" * 64
```

porque el nombre del reto es **Dor4_Null5**.

Luego:

- Ejecutar la función `compute_path` (**AES-ECB**) para obtener el valor `expected`.

---

### 3. Cálculo del HMAC

Generar la máscara `mask` usando:

- la `navigation_key` derivada
- el valor `expected`

---

### 4. Inyección

Calcular el XOR final entre `expected` y `mask` para generar la respuesta perfecta que **anula el checksum del servidor**:

```
provided = expected ⊕ mask
```

Esta respuesta se envía al servidor y permite autenticarse como **Administrator**.

---

![imagen4](https://github.com/user-attachments/assets/f2b1f912-7a21-436f-aaed-d1493b402377)

6 changes: 6 additions & 0 deletions 2025/Ctrl-Panic/Mutation Mutation/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
for(let i=0; i<500; i++){
try{
let res = _0x58c58c(i);
if(res.includes(‘lactf’)) console.log(“Encontrado en índice: “ + i + “: “ + res);
} catch(e){}
}
87 changes: 87 additions & 0 deletions 2025/Ctrl-Panic/Mutation Mutation/writeup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
## Análisis del reto

Al abrir el link se observa la siguiente página, donde no sucede nada al apretar **F12** o hacer **click derecho** para poder inspeccionar.

![imagen5](https://github.com/user-attachments/assets/8cd589ff-837e-4538-ab01-140267aa8b2c)

Para poder inspeccionar fue necesario hacer:

```
Tres puntos → Más herramientas → Herramientas para desarrolladores
```

---

## Análisis del código

El reto presentaba un **script altamente ofuscado** que utilizaba técnicas de **antidebugging**.
El código incluía:

### Diccionarios de strings

Un **array de cadenas codificadas** utilizado para ocultar el flujo lógico del programa.

### Intervalos y Timeouts

Bucles infinitos que monitoreaban si:

- la **consola estaba abierta**
- el **DOM cambiaba**

En caso de detectar alguna de estas condiciones, el script **reseteaba el flag** a valores falsos como:

```
lactf{nope}
lactf{wrong_one}
```

---

## Congelar el script

Para poder trabajar sin interferencias, el primer paso fue **"congelar" el estado del script** matando todos los procesos en segundo plano.

Esto permitió evitar que los intervalos siguieran ejecutándose y nos devolvió **una flag falsa**.

Al intentar ejecutar funciones obvias como:

```
f()
```

el script también devolvía **flags falsas**.

Esto indicaba que:

- el autor había implementado lógica para **detectar ejecución directa**, o
- el **flag real estaba fragmentado** dentro del código.

![imagen6](https://github.com/user-attachments/assets/784b936c-e2cd-4f63-b315-4a9d80d48ec2)

---

## Extracción del flag

Se identificó la función de acceso al **diccionario principal de strings**:

```
_0x58c83c
```

En lugar de intentar entender la compleja matemática utilizada para calcular los índices, por ejemplo:

```
-0x3496d * -0x1 ...
```

se optó por una estrategia más directa: **extraer todos los strings almacenados en memoria**.

Para ello se utilizó un **script de fuerza bruta** que recorría el diccionario y mostraba cualquier string que contuviera el prefijo:

```
lactf{
```

Esto permitió localizar rápidamente los posibles candidatos al **flag real**.

![imagen7](https://github.com/user-attachments/assets/2bdbfd3d-d332-4d83-a5f3-c0a75efa8e3f)
8 changes: 8 additions & 0 deletions 2025/Ctrl-Panic/R0tnoT13/crypt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from binascii import unhexlify
S = unhexlify("3721d4ef20940a4e78a4ab209a07acbd")
ct = unhexlify("477eb79b46ef667f16ddd94ca933c7c0")

pt = bytes(a ^ b for a, b in zip(S, ct))

print(pt)
print(pt.decode())
38 changes: 38 additions & 0 deletions 2025/Ctrl-Panic/R0tnoT13/solveRot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from z3 import *
# Leaked values: k -> S xor ROTR(S, k)
leaks = {
8: 183552667878302390742187834892988820241,
4: 303499033263465715696839767032360064630,
16: 206844958160238142919064580247611979450,
2: 163378902990129536295589118329764595602,
64: 105702179473185502572235663113526159091,
32: 230156190944614555973250270591375837085,
}
solver = Solver()

# 128-bit internal state
S = [Bool(f"S_{i}") for i in range(128)]

# Anchor bits
solver.add(S[0] == True)
solver.add(S[127] == False)

def get_bit(x, i):
return (x >> i) & 1

# Add equations
for k, value in leaks.items():
for i in range(128):
bit = get_bit(value, i)
solver.add(Xor(S[i], S[(i + k) % 128]) == BoolVal(bit == 1))

assert solver.check() == sat
model = solver.model()

# Rebuild S
state = 0
for i in range(128):
if model[S[i]]:
state |= (1 << i)

print(f"Recovered S: {hex(state)}")
52 changes: 52 additions & 0 deletions 2025/Ctrl-Panic/R0tnoT13/writeup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
El sistema mantiene un estado interno de **128 bits `S`**, derivado de AES.
Para verificar integridad de hardware, el firmware registra valores de la forma:
S ⊕ ROTR(S, k)
para distintos valores de rotación `k`.

Por un error de logging, se filtran varios de estos valores, junto con:

- los `k` correspondientes
- **dos bits conocidos del estado** (no los encontramos, pero usamos `1` y `0` porque es lo más normal en este tipo de retos)
- un **ciphertext cifrado usando el estado**

El objetivo es **reconstruir `S` y recuperar el mensaje cifrado**.

![imagen1](https://github.com/user-attachments/assets/7c7cd9fd-a717-4430-a7ef-36e9d1d9d899)

---

## Reconstrucción del estado (`solveRot.py`)

El script `solveRot.py` se encarga de **reconstruir el estado interno secreto `S` de 128 bits** usando la información filtrada por los logs del sistema.

Cada log tiene la forma:
S ⊕ ROTR(S, k)
donde `ROTR` es una **rotación de bits hacia la derecha**.

La idea clave es que estas ecuaciones son **lineales a nivel de bits**, así que pueden modelarse como **restricciones lógicas**.

El script usa **Z3** para:

1. Crear un vector de **128 bits** que representa `S`.
2. Agregar una ecuación por cada par `(k, valor_filtrado)` conocido.
3. Incorporar los **bits ancla del estado** (los bits conocidos que da el challenge) para romper simetrías y asegurar una solución única.

Una vez que el solver encuentra un **modelo consistente**, el script reconstruye `S` completo y lo imprime en **hexadecimal**.

![imagen2](https://github.com/user-attachments/assets/66f81e3e-f9d5-4061-8a84-ded1f5e0248f)

---

## Descifrado del mensaje (`crypt.py`)

Luego el script `crypt.py` toma el **estado interno `S` ya recuperado** y lo usa para descifrar el `ciphertext` provisto por el challenge.

En este caso, el cifrado es simple: el estado actúa directamente como **keystream**, por lo que descifrar consiste en hacer un **XOR** entre el ciphertext y los bytes de `S`.

El script:

1. Convierte el **estado hexadecimal** en bytes.
2. Hace el **XOR byte a byte** con el ciphertext.
3. Muestra el **mensaje descifrado**.

![imagen3](https://github.com/user-attachments/assets/d919b91d-f2df-460d-850e-10de6ff3009f)
Loading