Solución al crackme de erg0t.
Hombre que tal !
Hace ya mucho mucho tiempo, ese lejano ente (dentro de poco será cercano) que se esconde detrás del nick «erg0t» planteó un reto a la distinguida audiencia de 48bits: un crackme. Pero había una «pega», era para linux!! ai dió mío!
He de reconocer que ahora uso muchísimo menos Linux que Windows, pero hace algunos años era al revés, así que bueno siempre está bien recordar cosas que si no luego se olvidan… y que mejor excusa que un crackme para repasarlas.
Desconecten los móviles, amordacen a los niños, asegúrense que han cerrado el gas, apuren las botellas de Vodka y no se muevan de sus asientos porque… ¡ empezamos !
(Tarda un poco en cargar…¡¡es que tiene colorines!!)
Cuando se trata de crackmes cualquier pista o insinuación equivale a algo, en el post original vemos esto «no os fieís de erg0t … probadlo en más de una máquina y descargad el fichero más de una vez ) .
Para ello necesitaís un kernel 2.6, tráfico UDP permitido hacia internet y libz.». Además vemos como el enlace al crackme es un .php por lo que «descargad el fichero más de una vez» debe
significar algo importante.
Obedecemos y los descargamos 5 veces. Usando el comparador de ficheros del Ultraedit vemos que cada archivo es diferente de otro en unos offsets determinados y comparte el mismo número de bytes diferentes.
0xA65 -> 0xB8
0x13c0 -> 0x4
0x2C52 -> 0x4
Este último son los 4 últimos bytes del fichero al final de la sección .strtab por lo que a primera vista es un overlay con algun valor generado al vuelo.
El siguiente paso es sacar el IDA a pasear a ver que nos encontramos. Todo se desensambla bien, no está empaquetado, no hay nada raro (por ahora). Vamos a ver paso a paso entonces cómo funciona internamente el crackme.
Lo primero interesante :
.text:08048D2B mov [esp+288h+var_27C], 0
.text:08048D33 mov [esp+288h+var_280], 2810900h
.text:08048D3B mov [esp+288h+var_284], eax
.text:08048D3F mov [esp+288h+var_288], offset check
.text:08048D46 call _clone
Esto crea un thread cuya rutina es «check», con los siguientes flags CLONE_SIGHAND | CLONE_UNTRACED | CLONE_STOPPED | CLONE_VM | CLONE_THREAD. De momento nos quedamos con la copla de los flags, más tarde veremos porqué los usa.
Posteriormente crea un socket DGRAM sin mayor novedad
.text:08048D51 mov [esp+288h+var_280], 0
.text:08048D59 mov [esp+288h+var_284], 2
.text:08048D61 mov [esp+288h+var_288], 2
.text:08048D68 call _socket
A continuación lee el link al directorio /proc/self/exe que es ni más ni menos que el path y el nombre del ejecutable. Además comprueba que estemos corriendo un kernel 2.6 mediante uname.
.text:08048D73 lea eax, [ebp+var_198]
.text:08048D79 mov [esp+288h+var_288], eax
.text:08048D7C call _uname
.text:08048D81 mov [esp+288h+var_280], 1000h
.text:08048D89 mov [esp+288h+var_284], offset crackme
.text:08048D91 mov [esp+288h+var_288], offset aProcSelfExe ; "/proc/self/exe"
.text:08048D98 call _readlink
.text:08048D9D mov ds:crackme[eax], 0
.text:08048DA4 lea eax, [ebp+var_198]
.text:08048DAA add eax, 82h
.text:08048DAF mov [esp+288h+var_280], 3
.text:08048DB7 mov [esp+288h+var_284], offset a2_6 ; "2.6"
.text:08048DBF mov [esp+288h+var_288], eax
.text:08048DC2 call _strncmp
Intenta resolver 48bits.com,
– «¿qué sabes de 48bits.com?»
– «Pues que es un país precioso con una gente estupenda»
.text:08048DDE call _gethostbyname
.text:08048DE3 mov [ebp+var_1AC], eax
.text:08048DE9 cmp [ebp+var_1AC], 0
.text:08048DF0 jnz short loc_8048DFE
.text:08048DF2 mov [esp+288h+var_288], 0FFFFFFFFh
Ahora empieza ya el mondongo
.text:08048E08 lea eax, [ebp+var_248]
.text:08048E0E add eax, 4
.text:08048E11 mov [esp+288h+var_288], eax
.text:08048E14 call _sigemptyset
.text:08048E19 mov [ebp+var_1C4], 4
.text:08048E23 mov [esp+288h+var_280], 0
.text:08048E2B lea eax, [ebp+var_248]
.text:08048E31 mov [esp+288h+var_284], eax
.text:08048E35 mov [esp+288h+var_288], 0Bh
.text:08048E3C call _sigaction
.text:08048E41 mov [esp+288h+var_280], 0
.text:08048E49 lea eax, [ebp+var_248]
.text:08048E4F mov [esp+288h+var_284], eax
.text:08048E53 mov [esp+288h+var_288], 4
.text:08048E5A call _sigaction
Lo que hace aquí es asignar un manejador de señales tanto para SIGILL (instrucción ilegal) como para SIGSEGV (violacion de segmento). Quedaros con esto para después.
Luego establece una conexión con ese bonito país al puerto 0xCAFE
.text:08048E8D mov [esp+288h+var_288], 0CAFEh
.text:08048E94 call _htons
.text:08048E99 mov word ptr [ebp+var_1A8+2], ax
.text:08048EA0 mov eax, [ebp+var_24C]
.text:08048EA6 mov [ebp+var_1A4], eax
.text:08048EAC mov [esp+288h+var_280], 10h
.text:08048EB4 lea eax, [ebp+var_1A8]
.text:08048EBA mov [esp+288h+var_284], eax
.text:08048EBE mov eax, [ebp+var_254]
.text:08048EC4 mov [esp+288h+var_288], eax
.text:08048EC7 call _connect
Y aquí llega la primera protección, en este caso anti-debug:
.text:08048EDD mov [ebp+var_248], offset th
.text:08048EE7 mov [esp+288h+var_280], 0
.text:08048EEF lea eax, [ebp+var_248]
.text:08048EF5 mov [esp+288h+var_284], eax
.text:08048EF9 mov [esp+288h+var_288], 5
.text:08048F00 call _sigaction
Primero establece un manejador de señal para SIGTRAP, posteriormente obtiene el pid del proceso padre e intentas attachearse a él usando ptrace. En el caso que estemos corriendo el programa en un debugger o usando strace o , pues esto equivale a que todo se bloquee. Trick antidebug de toda la vida 🙂 . Hay que contar con que la thread ha sido creada con CLONE_UNTRACED lo que hace la vida dificil para depurarla con el GDB. La solución, ejecutarlo y cuando empieze a pedir el «Name», attachearte y directamente usar » (gdb) jump funcion » para verificar movidas. Recomiendo echar un ojo a este hilo en el foro de 48bits https://forum.48bits.com//index.php?topic=32.0
.text:08048F0A mov [esp+288h+var_27C], 0
.text:08048F12 mov [esp+288h+var_280], 0
.text:08048F1A mov [esp+288h+var_284], eax
.text:08048F1E mov [esp+288h+var_288], 10h
.text:08048F25 call _ptrace
.text:08048F2A call _wait
A partir de aquí nos pide name and key mediante read y tal…guardando la información en la sección de datos.No pongo el código porque no son más que reads y printfs y movidas así.
Por último escribe al socket todo esa información desde un buffer, el cual, en los primeros 4 bytes se encuentra una dword que corresponde a uno de los valores que cambiaban entre ficheros, (el segundo en la tabla de raw offsets 0x13c0 )
.text:08048FA5 mov [esp+288h+var_280], 38h
.text:08048FAD mov [esp+288h+var_284], offset dword_804A3C0
.text:08048FB5 mov eax, [ebp+var_254]
.text:08048FBB mov [esp+288h+var_288], eax
.text:08048FBE call _write
Luego le envía una SIGCONT a la thread , recordad que se habia creado suspendida, para «reanimarla».
.text:08048FCB mov eax, [ebp+var_250]
.text:08048FD1 mov [esp+288h+var_288], eax
.text:08048FD4 call tkill
.text:08048FD9 mov [esp+288h+var_288], offset unk_8049214
(Este es un buen sitio para romper «(gdb) b tkill» )
Por último, lee 4 bytes del socket que es la respuesta del servidor, lo guarda en hash y tras una comprobación, termina con un «Invalid Key».
.text:08048FED mov [esp+288h+var_284], offset hash
.text:08048FF5 mov eax, [ebp+var_254]
.text:08048FFB mov [esp+288h+var_288], eax
.text:08048FFE call _read
.text:08049003 not eax
.text:08049005 test eax, eax
.text:08049007 jnz short loc_8049021
.text:08049009 mov [esp+288h+var_288], offset a131mInvalidKey ; "\x1B[1;31m Invalid key\x1B[0;0m\n"
.text:08049010 call _puts
.text:08049015 mov [esp+288h+var_288], 0FFFFFFFFh
.text:0804901C call _exit
2. Dándole al tema.
Vamos a examinar la thread «check». Tiene dos objetivos principales, por un lado, checkear la integridad del crackme con el fin de detectar si hemos parcheado algun byte. Esto lo hace mediante un crc32, el crc32 original se situa al final del archivo, os acordáis de último valor en la tabla de raw offsets para bytes diferentes? pues era esto.
call _open
mov [ebp+var_74], eax
lea eax, [ebp+var_68]
mov [esp+98h+var_94], eax
mov [esp+98h+var_98], offset crackme
call lstat
mov eax, [ebp+var_3C]
mov [esp+98h+var_98], eax
call _malloc
mov eax, [ebp+var_3C]
sub eax, 4
mov [esp+98h+var_90], eax
mov eax, [ebp+var_78]
mov [esp+98h+var_94], eax
mov eax, [ebp+var_6C]
mov [esp+98h+var_98], eax
call _crc32
Se mapea a sí mismo (-4 bytes)a traves del enlace leido de /proc/self/exe, y genera el crc32
luego comprueba que el crc es el mismo que el original
cmp eax, [ebp+var_6C]
jz short loc_8048CBF
En el caso de que no sea igual genera una violación de acceso de esta manera
por lo que saltará el manejador de SIGSEGV que no hace nada más que sacarnos del programa con un «invalid key».
La segunda mision principal del la thread es generar un SIGTRAP para hacer saltar la rutina «th» que es el trap handler que posteriormente analizaremos. ¿cómo lo hace? Así…
mov [esp+98h+var_94], 0CCh
mov [esp+98h+var_98], offset rompe
call _memset
Sobreescribe los primeros 5 bytes de la funcion rompe con el opcode de la int3 (0xCC) la cual generará una SIGTRAP una vez ejecutada.
Esta thread se mantiene en un bucle hasta que el hash es distinto de 0, es decir hasta que el servidor al que se conecta el crackme ha recibido name y key y ha devuelto el valor que corresponda. Cuando se cumple esta condición, la thread llama a «rompe» por lo que se ejecuta la int3 y saltamos directamente a «th», es decir al trap handler.
Veamos «th».
.text:08048B28 push ebp
.text:08048B29 mov ebp, esp
.text:08048B2B push esi
.text:08048B2C push ebx
.text:08048B2D sub esp, 10h
.text:08048B30 mov [ebp+var_C], offset good
.text:08048B37 mov [ebp+var_10], 0
.text:08048B3E
.text:08048B3E loc_8048B3E: ; CODE XREF: th+46j
.text:08048B3E cmp [ebp+var_10], 2Eh
.text:08048B42 ja short loc_8048B70
.text:08048B44 mov eax, [ebp+var_10]
.text:08048B47 lea ebx, ds:0[eax*4]
.text:08048B4E mov esi, [ebp+var_C]
.text:08048B51 mov eax, [ebp+var_10]
.text:08048B54 lea ecx, ds:0[eax*4]
.text:08048B5B mov edx, [ebp+var_C]
.text:08048B5E mov eax, hash
.text:08048B63 xor eax, [ecx+edx]
.text:08048B66 mov [ebx+esi], eax
.text:08048B69 lea eax, [ebp+var_10]
.text:08048B6C inc dword ptr [eax]
.text:08048B6E jmp short loc_8048B3E
.text:08048B70 ; —————————————————————————
.text:08048B70
.text:08048B70 loc_8048B70: ; CODE XREF: th+1Aj
.text:08048B70 call good
Como véis es una función muy sencilla, descifra 0x2e*sizeof(dword) bytes de una zona de memoria (good) mediante un xor con el hash obtenido del servidor. Una vez ha terminado,hace un call a la zona de memoria descifrada «good» por lo que interpretamos que lo que intenta descifrar es una función. Así que lo primero que se nos plantea es sacar un hash bueno para descifrar correctamente esa funcion y ver que hace, de otra manera estamos jodidos porque no tenemos nada con lo que operar al generarse toda la información relevante en el servidor.
Para descifrar la funcion sin necesidad de obtener la clave correcta mediante métodos «legítimos» vamos a usar una variante de una técnica usada habitualmente en el análisis de malware y virus, esta es x-ray. Al ser un cifrado sencillo como es xor lo que vamos a hacer es intentar averiguar la clave, tratando de «identificar» a través de la función cifrada algo que sepamos que esté ahí. En este caso lo que vamos a usar es una direccion de memoria de una cadena que por narices tiene que hacerse referencia desde esa función cifrada ya que no existe ninguna x-ref desde ninguna otra parte del ejecutable, y diréis, y por que esa string tiene que tener alguna xref?, pues porque esa string es :
«\x1B[1;32m Key is valid\n Congratulations :)\x1B[0;0m\n»
Sospechoso ¿no? :). Bien, pues esa cadena se situa en 0x08049184, pero no podemos símplemente intentar por fuerza bruta obtener esa dword porque matemáticamente podríamos obtenerla en cualquier dword si nos pusieramos a xorear con todos los valores posibles.
Lo que tenemos que hacer es generar un hash que aplicado a una dword nos devuelva ese offset y que además ese hash aplicado a otro offset donde sepamos que existe otro valor nos devuelva el valor que estamos esperando, esto confirmaría que hemos dado con la clave, ya que en un espacio tan reducido como el de la función es estadísticamente muy dificil que se diera un falso positivo. Entonces lo que vamos a hacer es buscar esta secuencia
mov [xxxx], offset NuestraCadena
Call _puts
ya que a lo largo del desensamblado del crackme hemos visto cómo se usaba esta combinación para mostrar texto a la terminal. Por ejemplo:
call _puts
Entonces traduciendo esto a opcodes, tenemos que encontrar en un determinado offset de la cadena ValidKey,x, dentro de la funcion cifrada. En x+1 deberá estar 0xE8 del opcode de Call y en x-1= 0x24 , x-2=0x4 y x-3=0xC7 que corresponden al mov [xxxxx]
Para que quede claro, la incognita a encontrar es hash que es una dword, la dividimos en 4 claves de 1 byte diferentes, k1, k2, k3, k4. Cuando esta clave xoreada con una dword de la función cifrada nos devuelva el offset de la cadena ValidKey haremos esto
k1 k2 k3 k4 x k1
xor x-3 x-2 x-1 | 0x08049184 | x+1
si obtenemos los valores anteriormente citados, habremos conseguido sacar el hash bueno!!. Y obviamente, despues de hacer un programín para este menester, nuestra teoría se confirma y hayamos el hash que descifra correctamente la función.
#include <stdio.h>
#include <stdlib.h>
//// Good cifrada
unsigned char Crypte[]=
"\x3F\xE6\x34\x37\x86\x47\x16\xF1\x9A\x6F\xD1\xB4\x6A\xA8\x94\x40"
"\x6A\x6F\xD1\xB4\xAD\x6B\xF5\x62\xC9\x6B\xD9\x5C\xE9\x92\x2E\x4B"
"\x53\x2A\x25\xC7\x72\xE4\x94\x40\x6F\xBF\x72\xB0\x62\x60\x6F\xE4"
"\x6C\xE2\x94\x44\x6B\x7F\x5C\xF1\x9E\x90\xD1\x5F\xBD\xA8\x95\x90"
"\x62\x7F\xD1\xB4\x6A\xE2\x94\x58\xE3\x2B\xF5\xB0\xAD\x6B\xF5\x70"
"\xC9\x6B\xD9\x5C\x71\x91\x2E\x4B\xE3\x2A\x2D\x73\x2E\x4B\xD9\xA4"
"\x6A\x6F\xD1\x73\x2E\x4B\xD5\xB4\x6A\x6F\xD1\x3F\x2F\x83\x91\x3D"
"\x6E\x4B\x39\x48\x97\x90\x2E\x3D\x2F\x97\x5A\xE1\x92\xE2\x94\x48"
"\x5B\x7F\x5A\xE1\x9A\xCE\x29\x17\x6E\x67\xF8\x64\x53\x2A\x2D\xC1"
"\x64\xA8\xD5\x90\xEE\xFE\xD5\xBC\x82\xD9\x2D\x4B\x95\x84\xDD\x73"
"\x6E\x4B\xB9\x25\x6E\x67\x39\x1C\x96\x90\x2E\x73\x6E\x4B\xD1\xB4"
"\x6A\x6F\x39\xD8\x96\x90\x2E\x73\x6E\x4B\xD1\xB4";
unsigned char do_xray( unsigned char cypher, unsigned char origin)
{
int b;
unsigned char orig,xoredc;
for(b=0;b<=0xFF;b++)
{
xoredc=cypher^b;
if(xoredc==origin) return b;
}
}
int check_xray(unsigned char orig, unsigned char key, unsigned char result)
{
unsigned char xored;
xored= result^key;
if(xored==orig) return 1;
else return 0;
}
int main(int argc, char *argv)
{
int i,b;
unsigned char orig,xoredc,count,k1,k2,k3,k4;
printf("Size: %x\n",sizeof(Crypte));
for(i=0;i<sizeof(Crypte)-4;i++)
{
b=i;
k1 = do_xray(Crypte[b],0x84); // offset cadena 1
k2 = do_xray(Crypte[b+1],0x91); // offset cadena 2
k3 = do_xray(Crypte[b+2],0x04); // offset cadena 3
k4 = do_xray(Crypte[b+3],0x08); // offset cadena 4
if( check_xray(0xc7,k2,Crypte[b-3]) // mov[xxx],offset cadena
&& check_xray(0x04,k3,Crypte[b-2]) // mov[xxx],offset cadena
&& check_xray(0x24,k4,Crypte[b-1]) // mov[xxx],offset cadena
&& check_xray(0xe8,k1,Crypte[b+4]) // 0xE8 del Call _puts
)
{
printf("I[%x] C1: %x C2: %x C3: %x C4:%x – k1: %x k2: %x k3: %x k4: %x\n"
,i,
Crypte[b],
Crypte[b+1],
Crypte[b+2],
Crypte[b+3],
k1,
k2,
k3,
k4);
printf("Found: %01x %01x %01x %01x\n",k1,k2,k3,k4);
}
}
}
Pues ahora con esta clave, xoreamos Good y ya la tenemos descifrada!!
Vamos a ver que hace, porque en esa función está la clave de todo.
.text:08048A80 call _strlen
.text:08048A85 cmp [ebp+var_C], eax
.text:08048A88 jnb short loc_8048AA2
.text:08048A8A mov eax, [ebp+var_C]
.text:08048A8D add eax, 804A3D0h
.text:08048A92 movsx edx, byte ptr [eax+6]
.text:08048A96 lea eax, [ebp+var_10]
.text:08048A99 add [eax], edx
.text:08048A9B lea eax, [ebp+var_C]
.text:08048A9E inc dword ptr [eax]
.text:08048AA0 jmp short loc_8048A79 ; name
Aquí vemos como lee el «Name» que nos pide el crackme y suma todos los valores de los bytes que compone la cadena. En c:
y += (int) name[x] ;
A continuación :
.text:08048AA2 mov [esp+28h+var_20], 10h
.text:08048AAA lea eax, [ebp+var_14]
.text:08048AAD mov [esp+28h+var_24], eax
.text:08048AB1 mov [esp+28h+var_28], 804A3C4h ; key
.text:08048AB8 call _strtoul
.text:08048ABD mov [ebp+var_4], eax
.text:08048AC0 mov [esp+28h+var_20], 10h
.text:08048AC8 mov [esp+28h+var_24], 0
.text:08048AD0 mov eax, [ebp+var_14]
.text:08048AD3 inc eax
.text:08048AD4 mov [esp+28h+var_28], eax ; name
.text:08048AD7 call _strtoul
Lo que hace aquí es pasar la «key» a dos unsigned longs desde valores hexadecimales en ascii. Como vemos en strtoul el segundo parámetro no es NULL por lo que ahí se guarda el caracter donde termina la primera cadena hexadecimal, a continuacion se incrementa y se usa este puntero para leer la segunda parte de la key.Es decir la key tiene que tener dos partes hexadecimales separadas por un caracter no hexadecimal. AAAAAAAA-BBBBBBBB por ejemplo.
Por último la comprobación final
.text:08048ADF mov edx, [ebp+var_8] ; part 1 key
.text:08048AE2 lea eax, [ebp+var_4] ; part 2 key
.text:08048AE5 xor [eax], edx
.text:08048AE7 mov edx, [ebp+var_10] ; name count
.text:08048AEA mov eax, hash
.text:08048AEF sub eax, edx
.text:08048AF1 cmp [ebp+var_4], eax
.text:08048AF4 jnz short loc_8048B04
.text:08048AF6 mov [esp+28h+var_28], offset a132mKeyIsValid ; "\x1B[1;32m Key is valid\n Congratulation"…
.text:08048AFD call _puts
¿Cómo revertir esta funcion para generar nuestras propias claves? Hemos averiguado que a la función solo la descifra un valor que hemos sacado usando x-ray. Sabemos que el crackme envía un ID(Rawoffset 0x13c0) que cambia en todos los ejecutables, así mismo la función cifrada también cambia en todos los ejecutables. Blanco y en botella : kalimotxo.
Tiene que haber un patrón que relacione el ID y el HASH que sacamos por x-ray, tras varias operaciones elementales(suma,xor…) operando con los valores obtenidos de diferentes crackmes llegamos a que restando el ID – HASH obtenemos una constante= 0xco1dcafe en todos ellos. ¡¡ BINGO!!
Es decir la ecuación del hash es : HASH = ID – 0xc01dcafe;
Ahora ya podemos revertir la función para generar claves válidas para cada crackme, recordemos que es dependiente del ID que se encuentra en cada crackme.
Aquí tenéis un keygen, tendréis que obtener el HASH mediante el anterior programa de x-ray y cambiar los defines…
#include <stdlib.h>
#define HASH 0xB4D16F6A // Hash obtained by x-raying
#define CHORRAVALUE 0xDEADBEEF
#define ID 0x74EF3A68 // ID embedded within every crackme
#define CONST 0xc01dcafe // Constant
void good(unsigned long id, char *myName)
{
int a, b, x, y = 0 ;
unsigned long hash = id – CONST ;
printf("\nHash obtained by x-raying: %x\n",HASH);
printf("\nHash obtained by reversing good: %x\n",hash);
for(x=0;x<strlen(myName);x++)
y += (int) myName[x] ;
y += 10 ; // ‘\n’
hash -= y ;
a = CHORRAVALUE ;
b = hash ^ CHORRAVALUE ;
printf("\nName: %s\nKey: %X-%08X\n",myName,CHORRAVALUE,b);
}
int main(int argc, char *argv[])
{
if(argc<2) return 0;
good(ID,argv[1]) ;
return 0 ;
}
Felicidades a erg0t porque el crackme está muy currado y entretenido.
Por alguna extraña razón, al terminar este «tuto» me siento como si tuviera 16 años 😉 …
Un saludo
Rubén de los bosques.