domingo, 25 de agosto de 2013

Triangle KeyGenMe

Aunque ando sin mucho tiempo libre, hoy decidí mirar los retos de reversing que selecciono Ricardo para este mes. Esta vez parece que eran solo tres keygenme y un unpackme. El primero que mire fue el "Triangle KeyGenMe by iSSoGoo" y si bien mi solución no es completamente correcta (no supe como superar un error de precisión), aquí va mi solución


Trabajando en IDA Free, entre por el DialogFunc, alli podia verse como se usaba GetDlgItemTextA para cargar la info del formulario, y como, tras algunas operaciones se decide si saltamos a MessageBoxA con caption "Goodboy" o a uno con caption "Badboy".

Etiqueto los buffer para ambas llamadas a GetDlgItemTextA como "user" y "serial", veo una comparación justo después del segundo llamado y lo marco como un strlen, marco tambien como "incorrecto" el resultado de ese salto:

.text:0040127C loc_40127C: ; CODE XREF: DialogFunc+4Cj
.text:0040127C cmp [ebp+arg_4], 111h
.text:00401283 jnz loc_401361
.text:00401289 mov eax, [ebp+arg_8]
.text:0040128C cmp eax, 7D1h
.text:00401291 jnz loc_40134E
.text:00401297 push 32h ; int
.text:00401299 push 3 ; int
.text:0040129B push 0FA1h ; nIDDlgItem
.text:004012A0 push [ebp+hDlg] ; hDlg
.text:004012A3 call sub_401110
.text:004012A8 or eax, eax
.text:004012AA jnz loc_40133E
.text:004012B0 push 32h ; cchMax
.text:004012B2 push offset user ; lpString
.text:004012B7 push 0FA1h ; nIDDlgItem
.text:004012BC push [ebp+hDlg] ; hDlg
.text:004012BF call GetDlgItemTextA
.text:004012C4 push 12h ; cchMax
.text:004012C6 push offset serial ; lpString
.text:004012CB push 0FA2h ; nIDDlgItem
.text:004012D0 push [ebp+hDlg] ; hDlg
.text:004012D3 call GetDlgItemTextA
.text:004012D8 cmp eax, 10h ; strlen = 16
.text:004012DB jnz incorrecto


Continuando, veo una función que recibe dos offsets, uno de ellos es mi serial, el otro es desconocido, luego comparamos el resultado a uno y con eso decidimos si saltar o imprimir el cartel de "Goodboy"... marco la locación del salto como "Badboy", la funcion como "comparar" y el string desconocido como "string" (en este punto supongo que ese sera el serial correcto). Ese "string" era uno de los parámetros de una funcion que estaba inmediatamente arriba, el otro parametro es desconocido (si traceamos, vemos que es un numero flotante, asi que lo marco como "numero"), regresando un poco mas veo que justo arriba hay una funcion que recibe el "user" y asumo que calcula el "numero"... marco todo asi:

.text:004012E1 push offset user
.text:004012E6 call calcularNumero
.text:004012EB push 1
.text:004012ED push 8
.text:004012EF push offset numero
.text:004012F4 push offset string
.text:004012F9 call calcularString
.text:004012FE push offset serial
.text:00401303 push offset string
.text:00401308 call comparar
.text:0040130D cmp eax, 1
.text:00401310 jnz short badboy
.text:00401312 push 40h ; uType
.text:00401314 push offset Caption ; "Goodboy"
.text:00401319 push offset Text ; "Yes! You made it! I hope you didn't pat"...
.text:0040131E push [ebp+hDlg] ; hWnd
.text:00401321 call MessageBoxA
.text:00401326 jmp short loc_40133C


La lógica parece entonces ser esta: con "user" calculamos un "numero" y con este calculamos un "string", luego comparamos ese "string" con el "serial" ingresado, y si son iguales vamos a "Goodboy" o sino a "Badboy", tiene sentido... pero al intentar un breakpoint en la llamada a "comparacion", para robarme el string desde el stack, encuentro que "string" es 'B41FB44172DA7040', y que al probarlo, este no es correcto :(
Marco que los parámetros estan en esi y en edi. Y como se que la salida debe ser uno, marco el bloque de esta salida como bueno y el que da cero como malo... Veo que carga un byte del string en ah y uno del serial en al, luego hace un xor entre ellos y si el resultado es cero cuenta el caracter como correcto... concuerda con lo que esperabamos... donde esta el error en mi logica?

.text:00401378 ; =============== S U B R O U T I N E ===========================
.text:00401378
.text:00401378 ; Attributes: bp-based frame
.text:00401378
.text:00401378 comparar proc near ; CODE XREF: DialogFunc+F1p
.text:00401378
.text:00401378 arg_0 = dword ptr 8
.text:00401378 arg_4 = dword ptr 0Ch
.text:00401378
.text:00401378 push ebp
.text:00401379 mov ebp, esp
.text:0040137B pusha
.text:0040137C mov ecx, 10h
.text:00401381 xor edx, edx
.text:00401383 xor ebx, ebx
.text:00401385 mov esi, [ebp+arg_0] ; string
.text:00401388 mov edi, [ebp+arg_4] ; serial
.text:0040138B
.text:0040138B loc_40138B: ; CODE XREF: comparar+56j
.text:0040138B mov ah, [ebx+esi] ; un byte del string
.text:0040138E mov al, [ebx+edi] ; un byte del serial
.text:00401391 cmp ah, '9'
.text:00401394 jnz short loc_40139A
.text:00401396 jmp short loc_4013C7
.text:00401398 ; ----------------------------------------------------------------
.text:00401398 jmp short loc_4013C5
.text:0040139A ; ----------------------------------------------------------------
.text:0040139A
.text:0040139A loc_40139A: ; CODE XREF: comparar+1Cj
.text:0040139A cmp ah, 'F'
.text:0040139D jnz short loc_4013A3
.text:0040139F jmp short loc_4013C7
.text:004013A1 ; ----------------------------------------------------------------
.text:004013A1 jmp short loc_4013C5
.text:004013A3 ; ----------------------------------------------------------------
.text:004013A3
.text:004013A3 loc_4013A3: ; CODE XREF: comparar+25j
.text:004013A3 cmp al, '0'
.text:004013A5 jb short loc_4013C3
.text:004013A7 cmp al, '9'
.text:004013A9 ja short loc_4013AF
.text:004013AB jmp short loc_4013C5
.text:004013AD ; ----------------------------------------------------------------
.text:004013AD jmp short loc_4013C5
.text:004013AF ; ----------------------------------------------------------------
.text:004013AF
.text:004013AF loc_4013AF: ; CODE XREF: comparar+31j
.text:004013AF cmp al, 'A'
.text:004013B1 jb short loc_4013BF
.text:004013B3 cmp al, 'F'
.text:004013B5 ja short loc_4013BB
.text:004013B7 jmp short loc_4013C5
.text:004013B9 ; ----------------------------------------------------------------
.text:004013B9 jmp short loc_4013C5
.text:004013BB ; ----------------------------------------------------------------
.text:004013BB
.text:004013BB loc_4013BB: ; CODE XREF: comparar+3Dj
.text:004013BB jmp short malo
.text:004013BD ; ----------------------------------------------------------------
.text:004013BD jmp short loc_4013C5
.text:004013BF ; ----------------------------------------------------------------
.text:004013BF
.text:004013BF loc_4013BF: ; CODE XREF: comparar+39j
.text:004013BF jmp short malo
.text:004013C1 ; ----------------------------------------------------------------
.text:004013C1 jmp short loc_4013C5
.text:004013C3 ; ----------------------------------------------------------------
.text:004013C3
.text:004013C3 loc_4013C3: ; CODE XREF: comparar+2Dj
.text:004013C3 jmp short malo
.text:004013C5 ; ----------------------------------------------------------------
.text:004013C5
.text:004013C5 loc_4013C5: ; CODE XREF: comparar+20j
.text:004013C5 ; comparar+29j ...
.text:004013C5 dec al
.text:004013C7
.text:004013C7 loc_4013C7: ; CODE XREF: comparar+1Ej
.text:004013C7 ; comparar+27j
.text:004013C7 xor al, ah
.text:004013C9 jnz short loc_4013CC
.text:004013CB inc edx ; cuenta el caracter como correcto
.text:004013CC
.text:004013CC loc_4013CC: ; CODE XREF: comparar+51j
.text:004013CC inc ebx
.text:004013CD dec ecx
.text:004013CE jnz short loc_40138B
.text:004013D0 cmp edx, 10h ; verifica 16 caracteres correctos
.text:004013D3 jnz short malo
.text:004013D5 jmp short bueno
.text:004013D7 ; ----------------------------------------------------------------
.text:004013D7
.text:004013D7 malo: ; CODE XREF: comparar:loc_4013BBj
.text:004013D7 ; comparar:loc_4013BFj ...
.text:004013D7 popa
.text:004013D8 mov eax, 0
.text:004013DD leave
.text:004013DE retn 8
.text:004013E1 ; ----------------------------------------------------------------
.text:004013E1
.text:004013E1 bueno: ; CODE XREF: comparar+5Dj
.text:004013E1 popa
.text:004013E2 mov eax, 1
.text:004013E7 leave
.text:004013E8 retn 8
.text:004013E8 comparar endp
.text:004013E8


Desde luego el error es facil, justo antes del xor hay un "dec al" asi que debo modificar mi "serial" para que al restarle uno a cada carácter concuerde con el "string"...

Como el string es 'B41FB44172DA7040' tengo un problema... ¿se supone que le sume 1 a la 'F'?, no parece correcto... el codigo verifica los rangos en el camino... asi que decido probar sin cambiar la 'F', asumiendo que es un caso especial, tambien podria asumir que 'F' + 1 = '0', no se... el caso es reversar lo minimo...   pero si miramos esas dos lineas resaltadas en amarillo, podemos ver qeu tanto 'F' como '9' son casos especiales...

Pruebo entonces con:


Y el resultado es:

Pero como el reto implica escribir un keygen, hace falta mucho mas anlisis... Si, claro, podríamos robarmos esas funciones de calcularNumero y calcularString para montar el keygen en RedASM, pero preferi estudiar la rutina y generar mi propio código.

Lo principal fue entender el resultado de las operaciones flotantes en calcularNumero  (esto lo hice siguiendo de cerca los resultados en los registros correspondientes), y aqui esta el volcado de la función:

.text:004013EB ; =============== S U B R O U T I N E ===========================
.text:004013EB
.text:004013EB ; Attributes: bp-based frame
.text:004013EB
.text:004013EB calcularNumero proc near ; CODE XREF: DialogFunc+CFp
.text:004013EB
.text:004013EB arg_0 = dword ptr 8
.text:004013EB
.text:004013EB push ebp
.text:004013EC mov ebp, esp
.text:004013EE pusha
.text:004013EF push [ebp+arg_0]
.text:004013F2 call strlen
.text:004013F7 mov ecx, 63h
.text:004013FC sub ecx, eax
.text:004013FE mov edi, [ebp+arg_0]
.text:00401401 add edi, eax
.text:00401403 inc edi
.text:00401404
.text:00401404 loc_401404: ; CODE XREF: calcularNumero+1Cj
.text:00401404 stosb ; completar el username
.text:00401405 inc al
.text:00401407 loop loc_401404 ; completar el username
.text:00401409 fninit
.text:0040140B push [ebp+arg_0]
.text:0040140E call strlen
.text:00401413 mov tmp, eax
.text:00401418 fild tmp ; paso el len a float
.text:0040141E fst len1 ; y lo almacena en 3 lugares distintos
.text:00401424 fst len2
.text:0040142A fstp len3
.text:00401430 mov eax, 2
.text:00401435 mov tmp, eax
.text:0040143A fild tmp
.text:00401440 fstp dos
.text:00401446 xor eax, eax
.text:00401448 mov tmp, eax
.text:0040144D fild tmp
.text:00401453 fst cero
.text:00401459 fstp numero
.text:0040145F xor ecx, ecx
.text:00401461 mov ebx, [ebp+arg_0]
.text:00401464
.text:00401464 loc_401464: ; CODE XREF: calcularNumero+166j
.text:00401464 cmp ecx, 63h
.text:00401467 jnb loc_401556
.text:0040146D movzx eax, byte ptr [ebx] ; ebx apunta al string
.text:00401470 mov tmp, eax ; eax carga el primer byte
.text:00401475 fild tmp ; lo llevamos a float
.text:0040147B fsqrt ; y le sacamso la raiz
.text:0040147D fst sqrt1 ; y sacamos copias del resultado
.text:00401483 fstp sqrt1p
.text:00401489 inc ebx ; vamos por el segundo caracter
.text:0040148A movzx eax, byte ptr [ebx]
.text:0040148D mov tmp, eax ; y repetimos el proceso
.text:00401492 fild tmp
.text:00401498 fsqrt
.text:0040149A fst sqrt2 ; almacenando copias de la raiz del segundo caracter
.text:004014A0 fstp sqrt2p
.text:004014A6 fld sqrt1p ; cargamos el valor del aprimera raiz
.text:004014AC fld len2 ; y el del len
.text:004014B2 faddp st(1), st ; y sumamos
.text:004014B4 fld sqrt2p ; cargamos la segunda raiz
.text:004014BA fld sqrt1 ; y la primera
.text:004014C0 fsubp st(1), st ; para restarlas
.text:004014C2 fmulp st(1), st ; multiplico los dos resultados
.text:004014C2 ; (sqrt1 + len) * (sqrt2 - sqrt1)
.text:004014C4 fdiv dos ; divido por dos
.text:004014CA fstp res ; y almaceno el resultado
.text:004014CA ; res = (sqrt1 + len) * (sqrt2 - sqrt1) / 2
.text:004014D0 fld sqrt2
.text:004014D6 fld sqrt1p
.text:004014DC faddp st(1), st
.text:004014DE fld len3
.text:004014E4 fld sqrt2p
.text:004014EA fsubp st(1), st
.text:004014EC fmulp st(1), st
.text:004014EE fdiv dos
.text:004014F4 fld res ; res += (sqrt2 + sqrt1) * (len - sqrt2) / 2
.text:004014FA fadd st, st(1)
.text:004014FC fstp res
.text:00401502 ffree st
.text:00401504 fld sqrt2
.text:0040150A fld len2
.text:00401510 faddp st(1), st
.text:00401512 fld len3
.text:00401518 fld sqrt1
.text:0040151E fsubp st(1), st
.text:00401520 fmulp st(1), st
.text:00401522 fdiv dos
.text:00401528 fld res ; res -= (sqrt2 + len) * (len - sqrt1) / 2
.text:0040152E fsub st, st(1)
.text:00401530 fcom cero
.text:00401536 fnstsw ax
.text:00401538 test ax, 100h
.text:0040153C jz short loc_401540
.text:0040153E fchs ; cambio el signo
.text:00401540
.text:00401540 loc_401540: ; CODE XREF: calcularNumero+151j
.text:00401540 fld numero
.text:00401546 faddp st(1), st ; acumulo el resultado en numero
.text:00401548 fstp numero
.text:0040154E ffree st
.text:00401550 inc ecx
.text:00401551 jmp loc_401464 ; repito para el siguiente par de caracteres
.text:00401556 ; ----------------------------------------------------------------
.text:00401556
.text:00401556 loc_401556: ; CODE XREF: calcularNumero+7Cj
.text:00401556 popa
.text:00401557 leave
.text:00401558 retn 4
.text:00401558 calcularNumero endp


Donde recorremos el nombre, en un ciclo de longitud 0x63, para obtener en cada pasada el primer y segundo caracter.  Como mi nombre era mas corto, este tuvo que haber sido completado de alguna forma a esa longitud.  Esta completación está al principio de la función y es como sigue: despues del cero que finaliza mi username, agrego el largo de la cadena, y desde alli incremento uno por byte (hasta completar en 0x62).   Podemos verlo como si el nombre inicialmente estuviera formado por bytes que van incrementado desde 0xFF hasta 0x62, y luego los primeros bytes se sobreescriben con el username ingresado por el usuario (incluyendo el cero final).

Despues de la completacion generamos los calculos necesarios, vamos generando tres expresones de la forma (a + b) * (c - d) / 2, sumamos las dos primeras expresiones, a esa suma le restamos la terceram cambiamos el signo, y acumulamos en "número".  Una vez completado el ciclo tendremos alli el valor deseado y podremos pasar a la funcion calcularString:

.text:004010B1 ; =============== S U B R O U T I N E ============================
.text:004010B1
.text:004010B1 ; Attributes: bp-based frame
.text:004010B1
.text:004010B1 calcularString proc near ; CODE XREF: DialogFunc+E2p
.text:004010B1
.text:004010B1 arg_string = dword ptr 8
.text:004010B1 arg_numero = dword ptr 0Ch
.text:004010B1 arg_ocho = dword ptr 10h
.text:004010B1 arg_uno = dword ptr 14h
.text:004010B1
.text:004010B1 push ebp
.text:004010B2 mov ebp, esp
.text:004010B4 pusha
.text:004010B5 xor eax, eax
.text:004010B7 mov ecx, [ebp+arg_ocho] ; 8
.text:004010BA mov ebx, [ebp+arg_string] ; string
.text:004010BD mov edx, [ebp+arg_numero] ; numero
.text:004010C0 cmp [ebp+arg_uno], 1
.text:004010C4 jnz short loc_4010CD
.text:004010C6 mov esi, offset byte0x30
.text:004010CB jmp short loc_4010D2
.text:004010CD ; ----------------------------------------------------------------
.text:004010CD
.text:004010CD loc_4010CD: ; CODE XREF: calcularString+13j
.text:004010CD mov esi, offset unk_403010
.text:004010D2
.text:004010D2 loc_4010D2: ; CODE XREF: calcularString+1Aj
.text:004010D2 ; calcularString+37j
.text:004010D2 mov al, [edx] ; procesamos el numero byte por byte
.text:004010D4 shr al, 4
.text:004010D7 mov al, [eax+esi]
.text:004010DA mov [ebx], al
.text:004010DC inc ebx
.text:004010DD mov al, [edx]
.text:004010DF and al, 0Fh
.text:004010E1 mov al, [eax+esi]
.text:004010E4 mov [ebx], al
.text:004010E6 inc ebx
.text:004010E7 inc edx
.text:004010E8 loop loc_4010D2
.text:004010EA xor al, al
.text:004010EC mov [ebx], al
.text:004010EE popa
.text:004010EF leave
.text:004010F0 retn 10h
.text:004010F0 calcularString endp


Donde recorremos byte a byte la representacion del flotante "numero",  Basicamente tomamos medio byte, lo corremos y tomamos el siguiente medio, y los unimos para conformar la representacion hexa de ese byte.  Luego pasamos al sigueinte byte... Esto lo realizaremos con un poco de magia "struct".

Finalmente, modificaremos el "string" incrementando cuando no es 'F' ni '9' segun vimos en la funcion comparar (resaltado en amarillo, recuerda?).

Asi podemos combinar todo para obtener este generador de claves:

from math import sqrt
from struct import pack

name = input("Username: ")
len = len( name )

# calcular numero

user = [ '' ] * 0x64 

for i in range( 0x64 ):
        user[i] = ( i - 1 )


for i in range( len ):
        user[i] = ( ord( name[i] ) )


user[ len ] = ( 0 )

add = 0

for i in range( 0x63 ):
        sq1 = sqrt( user[i] )
        sq2 = sqrt( user[ i + 1] )
        add += ( len + sq1 ) * ( sq2 - sq1 ) / 2
        add += ( sq1 + sq2 ) * ( len - sq2 ) / 2
        add -= ( sq2 + len ) * ( len - sq1 ) / 2
numero = abs( add )

# calcular string

s = pack('<d', numero )
s = ''.join('%.2X' % c for c in s )
# generar serial

# calcular serial

serial = [ '' ] * 16

for i in range( 16 ):
        if i == 1:
                serial[i] = '?'
        elif s[i] != '9' and s[i] != 'F':
                serial[i] = chr( ord( s[i]) + 1 )
        else:
                serial[i] = s[i]

print( "Serial:", ''.join(serial) )


Un error de precisión ocasiona que "numero" resulte trucado y esto provoca que el segundo caracter del serial generado sea incorrecto, así que preferí imprimirlo como "?", y desde alli, hace falta probar manualmente, para encontrar el valor correcto.

Este valor correcto estará en el rango '1' a '9', o en el rango 'B' a 'F', tanto el '0' como la 'A' estan exluidos porque siempre estamos sumando uno, pero  '9' y 'F' son casos especiales, entonces 0x9 + 1 = 0xA no es posible, y 0xF + 1 = 0x0 (truncando a byte) no es posible tampoco.

Aqui algunas salidas:

C:\Python33>python triangle.py
Username: rmolina
Serial: C
?2FC55283EB8151

C:\Python33>python triangle.py
Username: Rubén Molina
Serial: 9
?9393C651969551

C:\Python33>python triangle.py
Username: rmolina.co
Serial: 1?131DF794839451


Probando manualmente se encuentran los caracteres faltantes, y se determina que las respuestas correctas son:

Username: rmolina
Serial: C52FC55283EB8151

Username: Rubén Molina
Serial: 9C9393C651969551

Username: rmolina.co
Serial: 18131DF794839451


Asi pues mi keygen solo reduce la busqueda de la clave a un rango de 14 posibiles claves, no esta bien, pero no esta del todo mal, y es lo mas lejos que pude llegar en python.  Directamente en assembly seria mas facil, pues es cuestion de robarse las funciones, tal vez actualice si lo hago asi mas adelante...

No hay comentarios:

Publicar un comentario