Copy Link
Add to Bookmark
Report

SET 038 0x05

  

-[ 0x05 ]--------------------------------------------------------------------
-[ Reversing XnView por Diversion ]------------------------------------------
-[ by blackngel ]----------------------------------------------------SET-38--


^^
*`* @@ *`* HACK THE WORLD
* *--* *
## <blackngel1@gmail.com>
|| <black@set-ezine.org>
* *
* * (C) Copyleft 2009 everybody
_* *_


1 - Introduccion

2 - Analisis

3 - Desempacado

4 - Reversing Serial

5 - KeyGen en C

6 - Conclusion

7 - Referencias



---[ 1 - Introducion

El trabajo que desempe~o a la hora de escribir estas lineas me otorga la
capacidad de pasarme horas y horas leyendo articulos sobre hacking, ingenieria
inversa u otros temas igualmente interesantes.

Pero leer no es la unica actividad que desarrolla el intelecto, y pasar de
vez en cuando a la accion ayuda a despejar la mente y darte cuenta que no hay
nada mejor que la practica.

Es por ello que me ha dado por instalar mi CrackersKit personal que utilizo
para estas tareas (en resumen OllyDbg y algunas herramientas de identificacion
de packers), y me he puesto manos a la obra con un sencillo programa llamado
XnView que he encontrado instalado entre todas las utilerias del ordenador
ante el que me siento todas las ma~anas.



---[ 2 - Analisis

XnView es una aplicacion dedicada a la captura y visualizacion de imagenes.
Lo bueno es que soporta unos 400 formatos de archivo, esta traducido a 40
idiomas y es capaz de mostrar los datos EXIF de cualquier imagen que los
contenga.

Una de sus funciones principales es la de conversion entre formatos de
imagenes, siendo los mas comunes: GIF, BMP, JPG, PNG o TIFF.

Ademas presenta algunas herramientas de edicion y mejora de imagenes, y hace
la tarea de visualizador hexadecimal con todos los formatos "no graficos".

Mencionar que, ademas de la plataforma de Microsoft, tambien esta disponible
para otros sistemas operativos como Linux o HP-UX.

A la accion...

En el ultimo menu, "Informacion", podemos acceder a dos ventanas interesantes,
la clasica "Acerca de..." que ademas de proporcionarnos informacion sobre el
programa y su version, nos dice a bajo de todo que tanto los datos "Numero
de Licencia"
como "Registrado A" se encuentran vacios.

Esto no seria asi si, accediendo a "Informacion"->"Registro" introdujesemos
los datos correctos en los cuadros "Su nombre/empresa" y "Su codigo".

La pregunta como siempre es: Como sabe el programa cuando hemos introducido
unos datos validos o no?

Luego, examinando el registro de Windows, tambien es interesante observar dos
claves como las siguientes:

HKEY_LOCAL_MACHINE\SOFTWARE\XnView\LicenseName

HKEY_LOCAL_MACHINE\SOFTWARE\XnView\LicenseNumber

Mas adelante se comprueba que XnView comprueba los valores de estas claves
cada vez que se inicia el programa.

Pero ahora vamos a cosas mas importantes...



---[ 3 - Desempacado

Lo primero de todo, como siempre, es hacer una copia del ejecutable xnview.exe
que ha sido instalado en nuestro sistema, todo para asegurar no perder la
estructura original.

Luego, una rapida pasada con la version 0.93 de PEiD nos informa que el
ejecutable ha sido empaquetado con ASPack, en concreto:

ASPack 2.12b -> Alexey Solodovnikov

Dada esta proteccion tan conocida, podemos utilizar varios programas que
nos devuelven el ejecutable orginal de forma automatica. Entre ellas dos
muy buenas como AspackDie v1.41 y Stripper.

Si bien esta es la solucion mas rapida, tampoco cuesta nada hacerlo de forma
manual. Veamos como:

1 -> Abrimos xnview.exe con OllyDbgy nos salta un mensaje:

"Module 'xnview' has entry point outside the code..."

Esto es normal, el programa esta empaquetado y el entry point ha sido
alterado. Luego viene otro mensaje sobre si deseamos realizar un analisis
de codigo estatico, al cual respondemos que no, para que no se ensucien
las instrucciones en ensamblador.

2 -> Ahora ya estamos listos. Vemos algo muy obvio, la segunda instruccion es
muy conocida: PUSHAD. Esta situa todos los registros del procesador en
la pila con el objetivo de salvaguardar sus valores. Luego se procedera
a desempacar el ejecutable para posteriormente llamar a POPAD antes de
llegar al entry point original (OEP).

Siendo esto asi, la tecnica que utilizamos es muy conocida y sencilla.
Se trata de dejar que la instruccion PUSHAD se ejecute, luego colocar
un hardware breakpoint (de acceso) en la cima de la pila de modo que
el programa se detenga una vez que algo nuevo ocurra en esta posicion
de memoria, que sera exactamente cuando la instruccion POPAD se ejecute.

Entonces, pulsamos f8 hastas sobrepasar PUSHAD. Vemos en la ventana
registers el valor de ESP=0012FFA4, boton derecho sobre este y pulsamos
"Follow in Dump". En la ventana de DUMP selecionamos los 4 primeros bytes,
boton derecho y "Breakpoint"->"Hardware, on access"->"Dword".
Pulsamos F9 y el programa se detiene, si nos desplazamos una linea hacia
arriba en el codigo podemos ver que nos hemos parado exactamente despues
del POPAD. Ahora vamos pulsando F8 hasta alcanzar la instruccion RET y
la pasamos.

3 -> Bingo! Ahora estamos en el OEP, y por las instrucciones...

push ebp
mov ebp,esp
...
sub esp,58

...alguno se imaginara que nos estamos enfrentando a un programa escrito
en lenguaje C/C++. Y no esta muy equivocado.

Ahora, teniendo instalado el plugin OllyDump, vamos a "Plugins"->"OllyDump"
->"Dump debugged process". Notamos la diferencia existente entre el entry
point anterior (2E4001) y el nuevo OEP (C8AD4). Para este caso necesitamos
reconstruir los "imports" utilizando el segundo metodo, de modo que abajo
de todo seleccionamos "Method2: Search DLL & API name string in dumped
file"
.

4 -> En definitiva, damos a "Dump" y guardamos el archivo con el nombre "xn.exe"
al lado del binario original. Ejecutamos el programa y comprobamos que
funciona correctamente, luego una pasada con PeID y observamos:

Microsoft Visual C++ 6.0

5 -> La cuestion es que no todos los imports han sido reconstruidos de forma
apropiada, esto lo se por que he comparado el listado de imports del
ejecutable desempacado con AspackDie y el que hemos creado manualmente.

Pero esto tiene facil solucion, iniciamos xn.exe y seguidamente abrimos
la aplicacion Import REConstructor v1.6, seleccionamos el proceso
en la lista "Attach to an Active Process", luego pulsamos el boton
"IAT AutoSearch" y aceptamos el mensaje que nos informa de que se ha
encontrado una posible direccion valida para la Import Address Table.
Para terminar clicamos en "Get Imports" y finalmente en Fix Dump, donde
debemos seleccionar el binario xn.exe.

ImpRec habra creado un ejecutable "xn_.exe" el cual deberia funcionar
nuevamente a la perfeccion y que esta vez si tendra todas las funciones
importadas.

Como hemos visto, no ha sido tan complicado, y ahora ya estamos preparados
para estudiar el algoritmo de generacion del serial valido.



---[ 4 - Reversing Serial

Antes de nada debemos cerrar ollydbg y volver a abrirlo con el nuevo ejecutable
"xn_.exe" para que los datos de imports y strings se actualicen correctamente.

Si estudiamos las "string references" no observamos nada que nos sea de mucha
ayuda, es por ello que, tras haber observado que despues de introducir datos
aleatorios en la ventana de registro, obtenenos un MessageBox que contiene:
"Registro no valido", podemos utilizar precisamente esta API para que el
programa se detenga en ese momento y poder visualizar el codigo ensamblador
que realiza las operaciones de comprobacion.

Para ello, damos boton derecho y vamos a "Search for"->"Name (label) in
current module"
y nos desplazamos hacia abajo en la lista hasta encontrar
"user32.MessageBoxA". Boton derecho encima de ella y escogemos la opcion
"Set breakpoint on every reference", para que el programa se detenga cada
vez que esta API sea ejecutada.

Ahora ya podemos iniciar el programa con el icono clasico o F9. De momento
todo normal, hasta que vamos a "Informacion"->"Registro" e introducimos los
siguientes valores:

Su nombre/empresa: ABCDE
Su codigo : 12345

Damos a aceptar y vemos que el programa se detiene, OllyDbg ha tomado el
control del mismo y se ha parado en la direccion "004ADD87" justo en una
llamada MessageBoxA que tendra por contenido la frase "invalid registration"
(traducida) que se puede ver si te desplazas unas lineas hacia arriba.

Hasta esa llamada, el codigo que nos interesa es el siguiente:

; /Count = 100 (256.)
; |Buffer
; |ControlID = 7D0 (2000.)
; |hWnd
; \GetDlgItemTextA

004ADD09 . 68 00010000 PUSH 100
004ADD0E . 50 PUSH EAX
004ADD0F . 68 D0070000 PUSH 7D0
004ADD14 . 56 PUSH ESI
004ADD15 . FFD7 CALL EDI
004ADD17 . 8D4C24 10 LEA ECX,DWORD PTR SS:[ESP+10]

; /Count = 20 (32.)
; |Buffer
; |ControlID = 7D1 (2001.)
; |hWnd
; \GetDlgItemTextA

004ADD1B . 6A 20 PUSH 20
004ADD1D . 51 PUSH ECX
004ADD1E . 68 D1070000 PUSH 7D1
004ADD23 . 56 PUSH ESI
004ADD24 . FFD7 CALL EDI

004ADD26 . 8A4424 70 MOV AL,BYTE PTR SS:[ESP+70]
004ADD2A . 84C0 TEST AL,AL
004ADD2C . 0F84 3A010000 JE xn_.004ADE6C
004ADD32 . 8A4424 10 MOV AL,BYTE PTR SS:[ESP+10]
004ADD36 . 84C0 TEST AL,AL
004ADD38 . 0F84 2E010000 JE xn_.004ADE6C
004ADD3E . 8D5424 08 LEA EDX,DWORD PTR SS:[ESP+8]
004ADD42 . 8D4424 70 LEA EAX,DWORD PTR SS:[ESP+70]
004ADD46 . 52 PUSH EDX
004ADD47 . 50 PUSH EAX
004ADD48 . E8 E3F9FCFF CALL xn_.0047D730
004ADD4D . 8D4C24 18 LEA ECX,DWORD PTR SS:[ESP+18]
004ADD51 . 51 PUSH ECX
004ADD52 . E8 29850100 CALL xn_.004C6280
004ADD57 . 8B4C24 14 MOV ECX,DWORD PTR SS:[ESP+14]
004ADD5B . 83C4 0C ADD ESP,0C
004ADD5E . 3BC8 CMP ECX,EAX
004ADD60 . 74 5D JE SHORT xn_.004ADDBF
004ADD62 . A1 ECB36500 MOV EAX,DWORD PTR DS:[65B3EC]
004ADD67 . 8D5424 30 LEA EDX,DWORD PTR SS:[ESP+30]

; /Count = 40 (64.)
; |Buffer
; |RsrcID = STRING "Invalid registration"
; |hInst => 10000000
; \LoadStringA

004ADD6B . 6A 40 PUSH 40
004ADD6D . 52 PUSH EDX
004ADD6E . 68 93130000 PUSH 1393
004ADD73 . 50 PUSH EAX
004ADD74 . FF15 64665B00 CALL DWORD PTR DS:[<&user32.LoadStringA>>

; /Style = MB_OK|MB_ICONHAND|MB_APPLMODAL
; |
; |Title = ""
; |Text
; |hOwner
; \MessageBoxA

004ADD7A . 6A 10 PUSH 10
004ADD7C . 8D4C24 34 LEA ECX,DWORD PTR SS:[ESP+34]
004ADD80 . 68 04616100 PUSH xn_.00616104
004ADD85 . 51 PUSH ECX
004ADD86 . 56 PUSH ESI
004ADD87 . FF15 34665B00 CALL DWORD PTR DS:[<&user32.MessageBoxA>>


Por que es interesante? Sencillo. Primero de todo porque observamos dos
llamadas a GetDlgItemTextA que por norma general son las encargadas de
recoger el texto que introducimos en los campos de la ventana de registro.

Segundo porque vemos una linea clave:

004ADD60 . 74 5D JE SHORT xn_.004ADDBF

que de ser ejecutada saltaria la zona de "chico malo" y nos llevaria
directamente a la de "chico bueno", que es mas o menos como se muestra
a continuacion.

004ADDE4 . 51 PUSH ECX
004ADDE5 . 68 C4815F00 PUSH xn_.005F81C4 ; ASCII "LicenseName"
004ADDEA . 6A 00 PUSH 0
004ADDEC . E8 5F3EFBFF CALL xn_.00461C50
004ADDF1 . 8D5424 1C LEA EDX,DWORD PTR SS:[ESP+1C]
004ADDF5 . 52 PUSH EDX
004ADDF6 . 68 B4815F00 PUSH xn_.005F81B4 ; ASCII "LicenseNumber"
004ADDFB . 6A 00 PUSH 0
004ADDFD . E8 4E3EFBFF CALL xn_.00461C50
004ADE02 . A1 F4B36500 MOV EAX,DWORD PTR DS:[65B3F4]
004ADE07 . 83C4 18 ADD ESP,18
004ADE0A . C705 08B46500 >MOV DWORD PTR DS:[65B408],1
........
........

; /Count = 40 (64.)
; |Buffer
; |RsrcID = STRING "Registration successful. Thank you for purchasing XnView."
; |hInst => 10000000
; \LoadStringA
004ADE33 . 6A 40 PUSH 40
004ADE35 . 51 PUSH ECX
004ADE36 . 68 94130000 PUSH 1394
004ADE3B . 52 PUSH EDX
004ADE3C . FF15 64665B00 CALL DWORD PTR DS:[<&user32.LoadStringA>>

; /Style = MB_OK|MB_ICONASTERISK|MB_APPLMODAL
; |
; |Title = ""
; |Text
; |hOwner
; \MessageBoxA
004ADE42 . 6A 40 PUSH 40
004ADE44 . 8D4424 34 LEA EAX,DWORD PTR SS:[ESP+34]
004ADE48 . 68 04616100 PUSH xn_.00616104
004ADE4D . 50 PUSH EAX
004ADE4E . 56 PUSH ESI
004ADE4F . FF15 34665B00 CALL DWORD PTR DS:[<&user32.MessageBoxA>>

En resumen, esta zona se encarga de establecer los valores de LicenseName
y LicenseNumber, que seran guardados en el registro del sistema para
posteriores comprobaciones y que serviran para imprimir en el cuadro de
dialogo de la opcion "Acerca de...". Finalmente se muestra un MessageBox,
esta vez diciendonos que nuestro registro ha tenido exito.

Como ya habran imaginado los amantes del "patch", cualquiera puede cambiar
el citado "JE" por un "JNE" y hacer que cualquier codigo de registro
invalido de como salida el mensaje de registro exitoso y todo lo que ello
conlleva. Pero como hay gente a la que esto no le resulta lo suficientemente
atractivo, volveremos de nuevo al codigo mostrado atras.

Las primeras instrucciones despues de la lectura de los campos de registro
son las siguientes:

MOV AL,BYTE PTR SS:[ESP+70]
TEST AL,AL
JE xn_.004ADE6C
MOV AL,BYTE PTR SS:[ESP+10]
TEST AL,AL
JE xn_.004ADE6C

Y es facil adivinar que su unica mision es comprobar que dichos valores,
tanto nombre como registro (ESP+XX) no esten vacios.

Luego ya viene la miga, que en resumidas cuentes es:

CALL xn_.0047D730
.....
CALL xn_.004C6280
MOV ECX,DWORD PTR SS:[ESP+14]
ADD ESP,0C
CMP ECX,EAX
JE SHORT xn_.004ADDBF

El primer CALL es donde se realizan todas las operaciones necesarias
con el campo "nombre" y el segundo hace otras operaciones con el campo
"codigo". Llegados a la instruccion "CMP", ECX y EAX contienen el
resultado de todas estas operaciones y solo en el caso de que los
dos valores comparados sean iguales, obtendremos un mensaje de exito.

Ponemos un breakpoing (F2) en el primer CALL y reiniciamos el programa.
Despues de introducir los datos que ya vimos en la ventaja de registro
y aceptar, el programa se dentra en dicho CALL, y podremos entrar en el
pulsando F7.

Si ahora nos vamos desplazando hacia abajo en el codigo, veremos que
las lineas negras de OllyDbg nos indica que existen hasta 5 bucles
dentro de la funcion, y lo mas curioso es que las cuatro primeras
parecen hacer referencia a una direccion que se incrementa en 5 bytes
en cada bucle, en realidad son los siguientes offsets:

- 617864
- 617869
- 61786E
- 617873

Ahora buscamos la primera instruccion haciendo referencia a esa primera
direccion: XOR BL,BYTE PTR DS:[EAX+617864]. Hacemos click derecho sobre
ella y "Follow in Dump"->"Address Constant". En la ventana de dump
observamos lo siguiente:

00617864 AA 89 C4 FE 46 78 F0 D0
0061786C 03 E7 F7 FD F4 E7 B9 B5
00617874 1B C9 50 73

Esto es muy interesante, exactamente 20 valores hexadecimales. Teniendo
en cuenta que hay cuatro bucles que realizan operaciones haciendo
referencia a direcciones dentro de este rango, y que en cada bucle se
incrementa la direccion en 5 bytes, podemos pensar que originalmente
en lenguaje C esto constituia una matriz bidimensional como esta:

int matrix = {{0xAA, 0x89, 0xC4, 0xFE, 0x46},
{0x78, 0xF0, 0xD0, 0x03, 0xE7},
{0xF7, 0xFD, 0xF4, 0xE7, 0xB9},
{0xB5, 0x1B, 0xC9, 0x50, 0x73}}

Ahora, para saber que se hace con esta matriz, vamos a examinar cada
bucle de forma individual. Aqui el primero:

0047D75B |> 8A0C16 /MOV CL,BYTE PTR DS:[ESI+EDX]
0047D75E |. 8AD9 |MOV BL,CL
0047D760 |. 3298 64786100 |XOR BL,BYTE PTR DS:[EAX+617864]
0047D766 |. 40 |INC EAX
0047D767 |. 83F8 05 |CMP EAX,5
0047D76A |. 881C16 |MOV BYTE PTR DS:[ESI+EDX],BL
0047D76D |. 8888 63786100 |MOV BYTE PTR DS:[EAX+617863],CL
0047D773 |. 75 02 |JNZ SHORT xn_.0047D777
0047D775 |. 33C0 |XOR EAX,EAX
0047D777 |> 46 |INC ESI
0047D778 |. 3BF5 |CMP ESI,EBP
0047D77A |.^72 DF \JB SHORT xn_.0047D75B

Trazando con F8 veras en directo que es lo que sucede. Las dos
primeras instrucciones mueven al registro BL el primer caracter
del campo "nombre", en nuestro caso 'A'. Seguidamente se realiza
una operacion XOR con el primer byte de la matriz que mencionamos
anteriormente.

EAX comienza con un valor de 0, y el incremento y comparacion subsiguientes
tienen como mision hacer que este no supere nunca el valor 5. Para que?
Si sigues trazando con F8, veras que el bucle vuelve a comenzar, y esta vez
realiza un XOR entre el segundo caracter de "nombre" y el segundo byte de
la matriz. La cuestion es, que si el nombre es mas largo que 5 caracteres,
el bucle reinicia el indice de la matriz; esto quiere decir que si nuestro
nombre tuviera 7 caracteres, el caracter numero 6 haria un XOR con el primer
byte de la matriz, y el 7 con el segundo. Esto es como operar de una forma
"modular".

En lenguaje C esto pinta mas o menos como sigue:

char *nombre = argv[1];
int len = strlen(nombre);
int *tmp = (int *) malloc(len * sizeof(int));

for (j = 0; j < len; j++) {
tmp[j % 5] = ((int)nombre[j] ^ matrix[0][j % 5]) % 255;
}

Creo que es bastante facil de enteder. En el lugar de la memoria donde
antes estaba almacenado "nombre" ha quedado el resultado de todas esas
operaciones XOR. Esto ocurre debido a la instruccion:

MOV BYTE PTR DS:[ESI+EDX],BL

Pero no todo es tan sencillo, uno debe fijarse muy bien en la instruccion:

MOV BYTE PTR DS:[EAX+617863],CL

Su objetivo es mutar la matriz. Como? Pues cada vez que el bucle realiza
una operacion XOR con un caracter, este es agregado a la matriz, de tal
forma que cuando se hizo el primer XOR con 'A' (0x41), este fue insertado
en matrix[0][0]. Cuando se opera con el segundo caracter ('B'=0x42) este
se inserta en matrix[0][1].

Siendo esto asi, cuando el contador llega a 5, y se reinicia a 0, la
siguiente operacion XOR (que corresponderia al sexto caracter del nombre),
aunque se hace con matrix[0][0], este ya no es 0xAA como al principio,
sino 0x41 y asi sucesivamente.

Dicho todo esto, la traduccion real a C es la siguiente:

for (j = 0; j < len; j++) {
tmp[j] = ((int)nombre[j] ^ matrix[0][j % 5]) % 255;

matrix[0][j % 5] = (int)nombre[j];
}

Si hubieramos hecho "Follow in dump" en la primera instruccion del bucle,
verias que (en mi caso), la direccion de esta variable era: 0012F605, y que
su contenido despues de finalizar el bucle ha sido el siguiente:

[ EB CB 87 BA 03 ]

Esto es importante para saber que realiza el segundo bucle:

0047D784 |> 8A9F 69786100 /MOV BL,BYTE PTR DS:[EDI+617869]
0047D78A |. 8BF5 |MOV ESI,EBP
0047D78C |. 2BF1 |SUB ESI,ECX
0047D78E |. 4E |DEC ESI
0047D78F |. 8A0416 |MOV AL,BYTE PTR DS:[ESI+EDX]
0047D792 |. 32D8 |XOR BL,AL
0047D794 |. 47 |INC EDI
0047D795 |. 881C16 |MOV BYTE PTR DS:[ESI+EDX],BL
0047D798 |. 8887 68786100 |MOV BYTE PTR DS:[EDI+617868],AL
0047D79E |. 83FF 05 |CMP EDI,5
0047D7A1 |. 75 02 |JNZ SHORT xn_.0047D7A5
0047D7A3 |. 33FF |XOR EDI,EDI
0047D7A5 |> 41 |INC ECX
0047D7A6 |. 3BCD |CMP ECX,EBP
0047D7A8 |.^72 DA \JB SHORT xn_.0047D784

La primera instruccion mueve a BL el valor "0x78" que como podemos
comprobar, es el valor de matrix[1][0].

Las siguientes 3 instrucciones establecen un contador, pero que al
contrario del anterior bucle, esta vez va de 5 a 0. Pero si te fijas
en las instrucciones, no aparece un 5 como un valor constante, en
realidad es mas logico pensar que el contador parte de la longitud
real de "nombre" (strlen(nombre)) y se va decrementando hasta llegar
a 0. Da la casualidad que nosotros hemos introducido un nombre de
5 caracteres.

Este contador (ESI) se utiliza en la quinta instruccion para ir
recuperando los valores que fueron resultado de las operaciones
del primer bucle ([ EB CB 87 BA 03 ]) pero en orden inverso, es
decir, que en AL se almacena primero el valor "0x03".

Seguidamente se realiza un XOR entre este valor y matrix[1][0].
Si seguimos trazando con F8 y damos otra vuelva al bucle, comprobamos
que esta vez se coge el valor "0xBA" (el penultimo) y se realiza
un XOR con matrix[1][1] ("0xF0"). Es decir, se va realizando un
XOR de matrix[1][0-5] con la inversa del resultado del anterior bucle.

Pero recuerda que, al igual que antes, ahora matrix[1] se va actualizando
con los valores que van siendo utilizados en las operaciones. En C quedaria
como sigue:

for (j = 1; j <= len; j++) {
var = tmp[len - j];

tmp[len - j] = (var ^ matrix[1][(j-1) % 5]) % 255;

matrix[1][(j - 1) % 5] = var;
}

No detallare que realizan los bucles 3 y 4, ya que son una copia exacta
de los bucles 1 y 2, salvo que utilizan para sus operaciones matrix[2] y
matrix[3] respectivamente.


Bucle 3:

0047D7B2 |> 8A0417 /MOV AL,BYTE PTR DS:[EDI+EDX]
0047D7B5 |. 8A8E 6E786100 |MOV CL,BYTE PTR DS:[ESI+61786E]
0047D7BB |. 32C8 |XOR CL,AL
0047D7BD |. 46 |INC ESI
0047D7BE |. 880C17 |MOV BYTE PTR DS:[EDI+EDX],CL
0047D7C1 |. 8886 6D786100 |MOV BYTE PTR DS:[ESI+61786D],AL
0047D7C7 |. 83FE 05 |CMP ESI,5
0047D7CA |. 75 02 |JNZ SHORT xn_.0047D7CE
0047D7CC |. 33F6 |XOR ESI,ESI
0047D7CE |> 47 |INC EDI
0047D7CF |. 3BFD |CMP EDI,EBP
0047D7D1 |.^72 DF \JB SHORT xn_.0047D7B2

Y bucle 4:

0047D7DB |> 8A9F 73786100 /MOV BL,BYTE PTR DS:[EDI+617873]
0047D7E1 |. 8BF5 |MOV ESI,EBP
0047D7E3 |. 2BF1 |SUB ESI,ECX
0047D7E5 |. 4E |DEC ESI
0047D7E6 |. 8A0416 |MOV AL,BYTE PTR DS:[ESI+EDX]
0047D7E9 |. 32D8 |XOR BL,AL
0047D7EB |. 47 |INC EDI
0047D7EC |. 881C16 |MOV BYTE PTR DS:[ESI+EDX],BL
0047D7EF |. 8887 72786100 |MOV BYTE PTR DS:[EDI+617872],AL
0047D7F5 |. 83FF 05 |CMP EDI,5
0047D7F8 |. 75 02 |JNZ SHORT xn_.0047D7FC
0047D7FA |. 33FF |XOR EDI,EDI
0047D7FC |> 41 |INC ECX
0047D7FD |. 3BCD |CMP ECX,EBP
0047D7FF |.^72 DA \JB SHORT xn_.0047D7DB

Para terminar nos encontramos con un ultimo bucle, que se encarga de
transformar el resultado de las operaciones anteriores en un valor de
4 bytes (double word) que cabe en un registro extendido.

Fijate en el codigo siguiente:

0047D811 |> 8BC8 /MOV ECX,EAX
0047D813 |. 83E1 03 |AND ECX,3
0047D816 |. 8A1C39 |MOV BL,BYTE PTR DS:[ECX+EDI]
0047D819 |. 8D3439 |LEA ESI,DWORD PTR DS:[ECX+EDI]
0047D81C |. 8A0C10 |MOV CL,BYTE PTR DS:[EAX+EDX]
0047D81F |. 02D9 |ADD BL,CL
0047D821 |. 40 |INC EAX
0047D822 |. 3BC5 |CMP EAX,EBP
0047D824 |. 881E |MOV BYTE PTR DS:[ESI],BL
0047D826 |.^72 E9 \JB SHORT xn_.0047D811

La primera vez que se entra al bucle ECX vale 0. Luego se realiza una operacion
AND cuyo objetivo es hacer que ECX vaya haciendo continuamente un ciclo entre
0 y 3 (0-1-2-3), que se utiliza como indice de una variable o matrix de 4 bytes
en "DS:[ECX+EDI]".

Bien, DS:[ECX+EDI] comienza siendo igual a [0,0,0,0]. Llamemos a esta matriz
RES[].

En la tercera instruccion BL es igual a RES[0]. Vemos en la quinta instruccion
que en CL se almacena el primer valor del resultado que obtuvimos con las
operaciones del cuarto bucle. Ambos valores, BL y CL, se suman y se almacena
el resultado en RES[0].

El bucle se repite tantas veces como fuera la longitud de "nombre". La segunda
vez que se ejecuta, BL toma el valor RES[1] y CL toma el segundo valor del
resultado obtenido en el cuarto bucle. Nuevamente se suman y el valor va a
parar a RES[1].

Este proceso se repite hasta llegar a RES[3], en ese momento RES[] contendra
diferentes valores dependiendo de las sumas, y simplemente se siguen realizando
sumas pero volviendo a empezar con RES[0].

Es logico pensar tambien que una suma como por ejemplo 0xE4 + 0xA9 supera la
capacidad de un byte (el resultado es superior a 255), en este caso obtendriamos
0x18D = 397. Lo unico que se hace es eliminar los bits sobrantes para quedarnos
con un solo byte, y eso se logra facilmente con una operacion como (val & 255).

Como siempre, en C tendriamos el siguiente codigo:

int res[4] = {0,0,0,0};

for (j = 0; j < len; j++) {
if (tmp[j] + res[j & 3] > 255)
res[j & 3] = (tmp[j] + res[j & 3]) & 255;
else
res[j & 3] = tmp[j] + res[j & 3];
}

Finalmente, cuando esta funcion finaliza tendremos en RES[] un valor hexadecimal
que sera uno de los valores utilizados en la comparacion (CMP ECX,EAX) que nos
lleva a uno de los dos mensajes: chico bueno o chico malo.

Personalmente me puse a estudiar las operaciones que se realizaban con el
"codigo" dentro del segundo CALL antes de la comparacion, y despues de extraer
un algoritmo algo tonto, fue bastante decepcionante comprobar que lo unico
que hacia era convertir la cadena introducida en "codigo" a hexadecimal. Es
decir, que si introdujimos como "codigo" la cadena "12345", el resultado de
este CALL sera "0x000003039".

Este es el valor que realmente se compara con RES[], y en caso de ser iguales
obtendremos el mensaje de "Registro Exitoso". Por lo tanto, se deduce que
tan solo hace falta cojer el resultado de todas las operaciones ejecutadas en
el primer CALL, convertirlo a decimal, y ese sera nuestro serial valido.

Para aquellos que les resulte de interes muestro aqui el nucleo de la operacion
realizada con el "codigo":

004C62CC |> 833D 7CFF5F00 >/CMP DWORD PTR DS:[5FFF7C],1
004C62D3 |. 7E 0C |JLE SHORT xn_.004C62E1
004C62D5 |. 6A 04 |PUSH 4
004C62D7 |. 56 |PUSH ESI
004C62D8 |. E8 810C0000 |CALL xn_.004C6F5E
004C62DD |. 59 |POP ECX
004C62DE |. 59 |POP ECX
004C62DF |. EB 0B |JMP SHORT xn_.004C62EC
004C62E1 |> A1 70FD5F00 |MOV EAX,DWORD PTR DS:[5FFD70]
004C62E6 |. 8A0470 |MOV AL,BYTE PTR DS:[EAX+ESI*2]
004C62E9 |. 83E0 04 |AND EAX,4
004C62EC |> 85C0 |TEST EAX,EAX
004C62EE |. 74 0D |JE SHORT xn_.004C62FD
004C62F0 |. 8D049B |LEA EAX,DWORD PTR DS:[EBX+EBX*4]
004C62F3 |. 8D5C46 D0 |LEA EBX,DWORD PTR DS:[ESI+EAX*2-30]
004C62F7 |. 0FB637 |MOVZX ESI,BYTE PTR DS:[EDI]
004C62FA |. 47 |INC EDI
004C62FB |.^EB CF \JMP SHORT xn_.004C62CC

Este bucle se ejecuta durante la longitud del "codigo", siempre y cuando
los caracteres presentes sean numeros (0-9). Las dos instrucciones mas
importantes son las siguientes:

LEA EAX,DWORD PTR DS:[EBX+EBX*4]
LEA EBX,DWORD PTR DS:[ESI+EAX*2-30]

Traducido a C seria algo como esto:

int a = 0;
int b = 0;
int i = 0;

while ( (i < strlen(code)) && (is_num(code[i])) ) {
a = b + (b * 4);
b = code[i] + (a * 2) - 30;

i += 1;
}

El codigo ensamblador que detecta si el caracter tratado es un numero o
no es algo arcaico. Utiliza una zona de la memoria que contiene lo que
parece ser una matriz de valores:

005FFD70 7A FD 5F 00 7A FD 5F 00 zý_.zý_.

005FFD78 00 00 20 00 20 00 20 00 .. . . .
005FFD80 20 00 20 00 20 00 20 00 . . . .
005FFD88 20 00 20 00 28 00 28 00 . .(.(.
005FFD90 28 00 28 00 28 00 20 00 (.(.(. .
005FFD98 20 00 20 00 20 00 20 00 . . . .
005FFDA0 20 00 20 00 20 00 20 00 . . . .
005FFDA8 20 00 20 00 20 00 20 00 . . . .
005FFDB0 20 00 20 00 20 00 20 00 . . . .
005FFDB8 20 00 48 00 10 00 10 00 .H...
005FFDC0 10 00 10 00 10 00 10 00 ....
005FFDC8 10 00 10 00 10 00 10 00 ....
005FFDD0 10 00 10 00 10 00 10 00 ....
005FFDD8 10 00 84 00 84 00 84 00 .„.„.„.
005FFDE0 84 00 84 00 84 00 84 00 „.„.„.„.
005FFDE8 84 00 84 00 84 00 10 00 „.„.„..
005FFDF0 10 00 10 00 10 00 10 00 ....
005FFDF8 10 00 10 00 81 00 81 00 ....
005FFE00 81 00 81 00 81 00 81 00 ....
005FFE08 01 00 01 00 01 00 01 00 ....
005FFE10 01 00 01 00 01 00 01 00 ....
005FFE18 01 00 01 00 01 00 01 00 ....
005FFE20 01 00 01 00 01 00 01 00 ....
005FFE28 01 00 01 00 01 00 01 00 ....
005FFE30 10 00 10 00 10 00 10 00 ....
005FFE38 10 00 10 00 82 00 82 00 ..‚.‚.
005FFE40 82 00 82 00 82 00 82 00 ‚.‚.‚.‚.
005FFE48 02 00 02 00 02 00 02 00 ....
005FFE50 02 00 02 00 02 00 02 00 ....
005FFE58 02 00 02 00 02 00 02 00 ....
005FFE60 02 00 02 00 02 00 02 00 ....
005FFE68 02 00 02 00 02 00 02 00 ....
005FFE70 10 00 10 00 10 00 10 00 ....
005FFE78 20

Si restamos la direccion 005FFE78 (ultima de la matriz) con 005FFD78
(la primera) obtenemos el valor 256. Casualmente, menos 1, el mismo
numero de valores que el codigo ASCII.

La instruccion: MOV EAX,DWORD PTR DS:[5FFD70] introduce en EAX la
direccion: 005FFD7A, que es una variable situada justo antes del
comienzo de la matriz. Es posible que ese sea el comienzo real
de la matriz, que comienza con un valor 20, y entonces si tendriamos
255 valores.

Luego se ejecuta la instruccion: MOV AL,BYTE PTR DS:[EAX+ESI*2] que utiliza
ESI, el caracter de "codigo" a tratar como un indice que, multiplicado por
dos, se suma a la direccion anteriormente almacenada en EAX.

Pongamos un ejemplo, si el primer caracter de "codigo" era un 0, su valor
hexa es 30. Entonces tendriamos: 005FFD7A + (30 * 2) = 005FFDDA. El valor
situado en esta posicion de memoria de la matriz es 84. Casualmente
el primer valor 84.

Si el caracter de codigo hubiera sido un 9, en hexadecimal 39, la direccion
resultante seria: 005FFD7A + (39 * 2) = 5FFDEC. Que contendria otro valor
84, casualemente el ultimo valor 84.

De todo esto sacamos que cualquier caracter numerico de "codigo" caera dentro
de esta zona de memoria:

005FFDD8 10 00 84 00 84 00 84 00 .„.„.„.
005FFDE0 84 00 84 00 84 00 84 00 „.„.„.„.
005FFDE8 84 00 84 00 84 00 10 00 „.„.„..

Sigamos. Las 3 funciones que se encargan de terminar el bucle son estas:

AND EAX,4
TEST EAX,EAX
JE SHORT xn_.004C62FD

En nuestro ejemplo, si en EAX tenemos un 84, la operacion AND (84 & 4)
dara como resultado 4, es decir, nunca 0, y por lo tanto el bucle continua
ejecutandose.

Lo bueno es comprobar que, el resto de valores contenidos en la matriz:
20, 28, 48, 10, 81, 01, 82 y 02, obtienen todos un resultado de 0 tras
la operacion AND, y por lo tanto finalizan el bucle. He aqui por que
he llamado a este metodo algo "arcaico".

Bien, todo esto no nos sirve de mucho en lo que a la averiguacion del
serial se refiere, pero lo he contado porque me resultaba ciertamente
interesante.



---[ 5 - KeyGen en C

Muy bien, juntando todas las piezas descritas anteriormente y haciendo uso
de la herramienta DevCPP (Dev-C++) de BloodShed para Windows, he programado
este sencillo keygen que solo recibe como argumento un nombre, y obtiene
como salida el serial correspondiente para el mismo.


-[ kg_xn.c ]-

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
int M [4][5] = {{170, 137, 196, 254, 70},
{120, 240, 208, 3, 231},
{247, 253, 244, 231, 185},
{181, 27, 201, 80, 115}};

int res[4] = {0,0,0,0};

int i, j;
int *tmp;
int n_code[5];

int len;
int var;

char *serial;
char *nombre;

if (argc < 2)
exit(1);

nombre = argv[1];
len = strlen(nombre);

tmp = (int *) malloc(len * sizeof(int));

printf("\n[+] CALCULANDO SERIAL ...\n");

for (j = 0; j < len; j++) {
tmp[j] = ((int)nombre[j] ^ M[0][j % 5]) % 255;

M[0][j % 5] = (int)nombre[j];
}

printf("\n[1] ");
for (i = 0; i < len; i++)
printf("%02x ", tmp[i]);
printf("\n");

for (j = 1; j <= len; j++) {
var = tmp[len - j];

tmp[len - j] = (var ^ M[1][(j-1) % 5]) % 255;

M[1][(j - 1) % 5] = var;
}

printf("\n[2] ");
for (i = 0; i < len; i++)
printf("%02x ", tmp[i]);
printf("\n");

for (j = 0; j < len; j++) {
var = tmp[j];

tmp[j] = (var ^ M[2][j % 5]) % 255;

M[2][j % 5] = var;
}

printf("\n[3] ");
for (i = 0; i < len; i++)
printf("%02x ", tmp[i]);
printf("\n");

for (j = 0; j <= len; j++) {
var = tmp[len - j];

tmp[len - j] = (var ^ M[3][(j-1) % 5]) % 255;

M[3][(j - 1) % 5] = var;
}

printf("\n[4] ");
for (i = 0; i < len; i++)
printf("%02x ", tmp[i]);
printf("\n");

for (j = 0; j < len; j++) {
if (tmp[j] + res[j & 3] > 255)
res[j & 3] = (tmp[j] + res[j & 3]) & 255;
else
res[j & 3] = tmp[j] + res[j & 3];
}

printf("\n[+] NAME CODE: [ ");
for (i = 3, j = 0; i >= 0; i--, j++) {
n_code[j] = res[i];
printf("%02x ", n_code[j]);
}
printf("]\n");

serial = n_code[0] << 24;
serial += n_code[1] << 16;
serial += n_code[2] << 8;
serial += n_code[3];

printf("\n[!!!] SERIAL: [ %u ]\n\n", serial);

system("PAUSE");
return 0;
}

-[ end kg_xn.c ]-

Todo es tal y como se ha descrito hasta ahora, lo unico que hemos agregado es
la parte del codigo que va imprimiendo los resultados tras las operaciones de
cada bucle y una ultima cosa.

El resultado que nosotros obtuvimos en res[] queda exactame igual que como lo
veiamos en la ventana del DUMP de OllyDbg, pero debes recordar que la memoria
en este sistema trabaja en modo Big-endian, es decir, que cuando ese valor sea
almacenado en un registro, se tomara en orden inverso, y de ello nos encargamos
en el ultimo bucle que imprime el "Name Code".

Finalmente tenemos que convertir ese valor a un entero decimal sin signo, para
ello debemos realizar unas clasicas operaciones de suma y desplazamiento de
bits de modo que obtengamos el resultado correcto.

Si ejecutamos el programa con el argumento "ABCDE" obtendremos:

[+] CALCULANDO SERIAL ...

[1] eb cb 87 ba 03

[2] 0c c8 57 4a 7b

[3] fb 35 a3 ad c2

[4] 88 65 6a b6 77

[+] NAME CODE: [ b6 6a 65 ff ]

[!!!] SERIAL: [ 3060426239 ]


En cambio, si yo quiero obtener mi serial con "blackngel", el resultado
sera:

[+] CALCULANDO SERIAL ...

[1] c8 e5 a5 9d 2d 0c 0b 04 0f

[2] c4 ee a1 92 ca 0f db f4 77

[3] 33 13 55 75 73 cb 35 55 e5

[4] f8 26 00 90 00 9b fc 4e 50

[+] NAME CODE: [ de fc c1 48 ]

[!!!] SERIAL: [ 3741106504 ]



---[ 6 - Conclusion

Tengo que decir que en realidad explicar el desarrollo de este crackme puede
resultar mas complicado que el hecho de estudiarlo uno mismo (como suele
pasar muchas veces).

No ha sido esta una tarea muy compleja, pero a mi modo de pensar sirve como
un buen calentamiento para el reto que vamos a resolver en el articulo que
publicamos en esta misma edicion de SET: "Reto 3 S21sec: Redes de Petri".

Es factible que podais preguntarme cualquier duda a mi direccion de correo,
siempre que sea una cuestion inteligente y bien planteada.

Feliz Hacking!
blackngel



---[ 7 - Referencias

[1] OllyDbg
http://www.ollydbg.de/

[2] PeID
http://www.peid.info/

[3] OllyDump
http://www.openrce.org/downloads/details/108/OllyDump

[4] Import REConstructor v1.6
http://vault.reversers.org/ImpRECDef


*EOF*

← previous
next →
loading
sending ...
New to Neperos ? Sign Up for free
download Neperos App from Google Play
install Neperos as PWA

Let's discover also

Recent Articles

Recent Comments

Neperos cookies
This website uses cookies to store your preferences and improve the service. Cookies authorization will allow me and / or my partners to process personal data such as browsing behaviour.

By pressing OK you agree to the Terms of Service and acknowledge the Privacy Policy

By pressing REJECT you will be able to continue to use Neperos (like read articles or write comments) but some important cookies will not be set. This may affect certain features and functions of the platform.
OK
REJECT