Hola Habr!
En
HackQuest, antes de la conferencia ZeroNight 2019, hubo una tarea entretenida. No aprobé la decisión a tiempo, pero recibí mi parte de emoción. Creo que le interesará saber lo que los organizadores y el equipo de r0.Crew han preparado para los participantes.
Tarea: obtenga un código de activación para el sistema operativo secreto
Micosoft 1998 .
En este artículo te diré cómo hacerlo.
Contenido
0. Tarea
1. Herramientas2. Inspeccione la imagen.3. Dispositivos de caracteres y el núcleo.4. Buscar register_chrdev4.1. Preparación de una imagen mínima fresca de Linux4.2. Algunas preparaciones más4.3. Deshabilitar KASLR en lunix4.4. Buscamos y encontramos una firma5. Busque saltos desde / dev / active y la función de escritura6. Estudiamos escribir6.1. Función hash6.2. Algoritmo de Generación Clave6.3. KeygenDesafío
Una imagen lanzada en QEMU requiere correo y una clave de activación. Ya conocemos el correo, ¡busquemos el resto!
1. Herramientas
En
~/.gdbinit
necesitas escribir una función útil:
define xxd dump binary memory dump.bin $arg0 $arg0+$arg1 shell xxd dump.bin end
2. Inspeccione la imagen.
Primero cambie el nombre de jD74nd8_task2.iso a lunix.iso.
Usando binwalk, vemos que hay un script en el desplazamiento
0x413000
. Este script verifica el correo y la clave:
Rompemos la verificación con el editor hexadecimal directamente en la imagen y hacemos que el script ejecute nuestros comandos. Cómo se ve ahora:
Tenga en cuenta que tuvo que recortar la línea
activated
para
activated
para que el tamaño de la imagen permanezca igual. Afortunadamente, no hay verificación de hash. La imagen se llama lunix_broken_activation.iso.
Ejecútelo a través de QEMU:
sudo qemu-system-x86_64 lunix_broken_activation.iso -enable-kvm
Vamos a cavar adentro:
Entonces tenemos:
- Distribución - Mínimo Linux 5.0.11.
- El dispositivo de caracteres
/dev/activate
se dedica a verificar el correo, la clave, lo que significa que la lógica de verificación debe buscarse en algún lugar de las entrañas del núcleo. - Correo, la clave se transmite en formato de
email|key
.
La imagen target_broken_activation.iso ya no será necesaria.
3. Dispositivos de caracteres y el núcleo.
Dispositivos como
/dev/mem
,
/dev/vcs
,
/dev/activate
/dev/vcs
, etc. regístrese utilizando la función
register_chrdev
:
int register_chrdev (unsigned int major, const char * name, const struct fops);
name
es el nombre, y la estructura
fops
contiene punteros a las funciones del controlador:
struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char *, size_t, loff_t *); ssize_t (*write) (struct file *, const char *, size_t, loff_t *); int (*readdir) (struct file *, void *, filldir_t); unsigned int (*poll) (struct file *, struct poll_table_struct *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, struct dentry *, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *); ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *); };
Solo nos interesa esta función:
ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
Aquí, el segundo argumento es el búfer con los datos transferidos, el siguiente es el tamaño del búfer.
4. Buscar register_chrdev
De forma predeterminada, Minimal Linux compila con información de depuración deshabilitada para reducir el tamaño de la imagen, pero mínima. Por lo tanto, no puede simplemente iniciar el depurador y buscar la función por nombre. Pero es posible por firma.
Y la firma está en la imagen mínima de Linux con información de depuración incluida. En general, necesitas construir tu Minimal.
Es decir, el esquema es el siguiente:
Minimal Linux -> register_chrdev -> -> register_chrdev Lunix
4.1. Preparación de una imagen mínima fresca de Linux
- Instale las herramientas necesarias:
sudo apt install wget make gawk gcc bc bison flex xorriso libelf-dev libssl-dev
- Descargando guiones:
git clone https://github.com/ivandavidov/minimal cd minimal/src
- Corregir
02_build_kernel.sh
:
bórralo
# Disable debug symbols in kernel => smaller kernel binary. sed -i "s/^CONFIG_DEBUG_KERNEL.*/\\# CONFIG_DEBUG_KERNEL is not set/" .config
agregarlo
echo "CONFIG_GDB_SCRIPTS=y" >> .config
- Compilando
./build_minimal_linux_live.sh
La imagen es minimal / src / minimal_linux_live.iso.
4.2. Algunas preparaciones más
Descomprima minimal_linux_live.iso en la carpeta minimal / src / iso.
El archivo minimal / src / iso / boot
rootfs.xz
kernel.xz
núcleo
rootfs.xz
y la
rootfs.xz
FS. Cambie el nombre a
kernel.minimal.xz
,
rootfs.minimal.xz
.
Además, debe extraer el núcleo de la imagen. El script
extract-vmlinux ayudará con esto:
extract-vmlinux kernel.minimal.xz > vmlinux.minimal
Ahora en la carpeta minimal / src / iso / boot tenemos este conjunto:
kernel.minimal.xz
,
rootfs.minimal.xz
,
vmlinux.minimal
.
Pero de lunix.iso solo necesitamos el kernel. Por lo tanto, realizamos las mismas operaciones, llamamos al
vmlinux.lunix
kernel.xz
, olvídate de
kernel.xz
,
rootfs.xz
, ahora te diré por qué.
4.3. Deshabilitar KASLR en lunix
Logré deshabilitar KASLR en el caso de Minimal Linux recién ensamblado en QEMU.
Pero no funcionó con Lunix. Por lo tanto, debe editar la imagen en sí.
Para hacer esto, ábralo en un editor hexadecimal, busque la línea
"APPEND vga=normal"
y reemplácela con
"APPEND nokaslr\x20\x20\x20"
.
Y la imagen se llama lunix_nokaslr.iso.
4.4. Buscamos y encontramos una firma
Lanzamos Minimal Linux nuevo en una terminal:
sudo qemu-system-x86_64 -kernel kernel.minimal.xz -initrd rootfs.minimal.xz -append nokaslr -s
En otro depurador:
sudo gdb vmlinux.minimal (gdb) target remote localhost:1234
Ahora busque
register_chrdev
en la lista de funciones:
Obviamente, nuestra opción es
__register_chrdev
.
No nos confunde que buscamos register_chrdev, pero encontramos __register_chrdevDesmontaje
¿Qué firma tomar? Probé varias opciones y me decidí por la siguiente pieza:
0xffffffff811c9785 <+101>: shl $0x14,%esi 0xffffffff811c9788 <+104>: or %r12d,%esi
El hecho es que en
lunix
solo hay una función que contiene
0xc1, 0xe6, 0x14, 0x44, 0x09, 0xe6
.
Ahora lo mostraré, pero primero descubriremos en qué segmento buscarlo.
La función
__register_chrdev
dirección
0xffffffff811c9720
, este es el segmento
.text
. Ahí vamos a mirar.
Desconectarse de la referencia Minimal Linux. Conéctese a lunix ahora.
En una terminal:
sudo qemu-system-x86_64 lunix_nokaslr.iso -s -enable-kvm
En otro:
sudo gdb vmlinux.lunix (gdb) target remote localhost:1234
Nos fijamos en los límites del segmento
.text
:
Fronteras
0xffffffff81000000 - 0xffffffff81600b91
, busque
0xc1, 0xe6, 0x14, 0x44, 0x09, 0xe6
:
Encontramos la pieza en la dirección
0xffffffff810dc643
. Pero esto es solo una parte de la función, veamos lo que está arriba:
Y aquí está el comienzo de la función
0xffffffff810dc5d0
(porque
retq
es la salida de la función vecina).
5. Buscar saltos desde / dev / activar
El prototipo de la función
register_chrdev
es este:
int register_chrdev (unsigned int major, const char * name, const struct fops);
Necesitamos una estructura de
fops
.
Reiniciando el depurador y QEMU. Pusimos
0xffffffff810dc5d0
descanso en
0xffffffff810dc5d0
. Funcionará varias veces. Estos son los dispositivos
mem, vcs, cpu/msr, cpu/cpuid
, y se
activate
inmediatamente después de ellos.
El puntero al nombre se almacena en el
rcx
. Y el puntero a
fops
está en
r8
:
Recuerdo estructura fops struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char *, size_t, loff_t *); ssize_t (*write) (struct file *, const char *, size_t, loff_t *); int (*readdir) (struct file *, void *, filldir_t); unsigned int (*poll) (struct file *, struct poll_table_struct *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, struct dentry *, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *); ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *); };
Entonces, la dirección de la función de
write
es
0xffffffff811f068f
.
6. Estudiamos escribir
La función incluye varios bloques interesantes. No vale la pena describir cada punto de interrupción, es una rutina habitual. Además, los bloques de cálculos son visibles a simple vista.
6.1. Función hash
vmlinux.lunix
el IDA,
vmlinux.lunix
el kernel
vmlinux.lunix
y veamos qué tiene dentro la función de escritura.
Lo primero que debe notar es este ciclo:
sub_FFFFFFFF811F0413
se
sub_FFFFFFFF811F0413
función
sub_FFFFFFFF811F0413
, que comienza así:
Y en la dirección
0xffffffff81829ce0
, se
0xffffffff81829ce0
una tabla para sha256:
Es decir,
sub_FFFFFFFF811F0413
= sha256. Los bytes cuyo hash debe obtenerse se transmiten a través de
$sp+0x50+var49
, y el resultado se almacena en
$sp+0x50+var48
. Por cierto,
var49=-0x49
,
var48=-0x48
, entonces
$sp+0x50+var49 = $sp+0x7
,
$sp+0x50+var48 = $sp+0x8
.
Compruébalo
Comenzamos qemu, gdb, establecemos un descanso en
0xffffffff811f0748 call sub_FFFFFFFF811F0413
y en la instrucción
0xffffffff811f074d xor ecx, ecx
, que está inmediatamente detrás de la función.
test@mail.ru
correo
test@mail.ru
, contraseña
1234-5678-0912-3456
.
El byte de correo se pasa a la función, y el resultado es este:
>>> import hashlib >>> hashlib.sha256(b"t").digest().hex() 'e3b98a4da31a127d4bde6e43033f66ba274cab0eb7eb1c70ec41402bf6273dd8' >>>
Es decir, sí, realmente es sha256, solo que calcula los hash para todos los bytes de correo, y no solo un hash del correo.
Luego los hash se suman por byte. Pero si la suma es mayor que
0xEC
, entonces se
0xEC
el resto de la división por
0xEC
:
import hashlib def get_email_hash(email): h = [0]*32 for sym in email: sha256 = hashlib.sha256(sym.encode()).digest() for i in range(32): s = h[i] + sha256[i] if s <= 0xEC: h[i] = s else: h[i] = s % 0xEC return h
El importe se guarda en
0xffffffff81c82f80
. Veamos cuál
test@mail.ru
el hash de
test@mail.ru
.
Ponemos
ffffffff811f0786 dec r13d
descanso en
ffffffff811f0786 dec r13d
(esta es la salida del bucle):
Y comparar con:
>>> get_email_hash('test@mail.ru') 2b902daf5cc483159b0a2f7ed6b593d1d56216a61eab53c8e4b9b9341fb14880
Pero el hash en sí es claramente un poco largo para la clave.
6.2. Algoritmo de Generación Clave
La clave es responsable de este código:
Aquí está el cálculo final de cada byte:
0xFFFFFFFF811F0943 imul eax, r12d 0xFFFFFFFF811F0947 cdq 0xFFFFFFFF811F0948 idiv r10d
En los bytes hash
eax
y
r12d
, se multiplican y luego se toma el resto de la división entre 9.
Porque
Y los bytes se toman en orden inesperado. Lo indicaré en keygen.
6.3. Keygen
def keygen(email): email_hash = get_email_hash(email) pairs = [(0x00, 0x1c), (0x1f, 0x03), (0x01, 0x1d), (0x1e, 0x02), (0x04, 0x18), (0x1b, 0x07), (0x05, 0x19), (0x1a, 0x06), (0x08, 0x14), (0x17, 0x0b), (0x09, 0x15), (0x16, 0x0a), (0x0c, 0x10), (0x13, 0x0f), (0x0d, 0x11), (0x12, 0x0e)] key = [] for pair in pairs: i = pair[0] j = pair[1] key.append((email_hash[i] * email_hash[j])%9) return [''.join(map(str, key[i:i+4])) for i in range(0, 16, 4)]
Entonces, generemos alguna clave:
>>> import lunix >>> lunix.keygen("m.gayanov@gmail.com") ['0456', '3530', '0401', '2703']
Y ahora puedes relajarte y jugar el juego 2048 :) ¡Gracias por tu atención! Codigo
aqui