Reverse engineer en OSX – Parte I
Jun 16, 2010 Sin categoria
Como verán, quien escribe no es quién esperan que escriba. Me tomé el atrevimiento de intrometerme en el blog de mi amigo @cuerty y dejarles una serie de posts para los switchers que se compraron una mac.
I. Introducción
Voy a hacer algunas entregas de una introducción a reverse engineer en OSX, siempre asm x86 32-bit. Mi idea es intentar desviar un poco la atención de la teoría (Que ya fue repetida hasta el hartazgo) y hacer un enfoque mas práctico: Voy a presentar una serie de crackme’s que bajé de otro sitio y explicar qué puede hacerse.
II. Requerimientos
Para mas o menos entender de qué viene el asunto, es necesario:
- Entender la sintaxis de Objective-C
- Conocer mas o menos los comandos básicos de gdb
- Estar familiarizado con la sintaxis Intel de asm.
II. b. Herramientas
- gdb
- otx
- class-dump
- Dado que OSX usa Mach-O que puede tener muchas arquitecturas, los offsets están corridos. Para solucionar esto pueden usar este script que hice
III. Lo básico.
Entender asm es fundamental para poder seguir adelante y a partir de este momento voy a asumir que esto último es un hecho.
Dicho esto, voy a recordar dos cosas: La primera es esta tabla de optcodes muy util (http://ref.x86asm.net/coder32.html) y la segunda la representación de los registros completos y parciales:
| Bits 31 – 24 | Bits 23 – 16 | Bits 15 – 8 | Bits 7 – 0 |
|---|---|---|---|
eax |
|||
ax |
|||
ah |
al |
||
ebx |
|||
bx |
|||
bh |
bl |
||
ecx |
|||
cx |
|||
ch |
cl |
||
edx |
|||
dx |
|||
dh |
dl |
||
ebp |
|||
bp |
|||
esp |
|||
sp |
|||
esi |
|||
si |
|||
edi |
|||
di |
|||
III b. ¿¡ Objective-Qué !?
Antes de empezar voy a hacer una brevísima introducción a la sintaxis de Objective-C.
Objective-C es un superset de C que provee programación orientada a objetos y conceptos de Messaging al estilo Smalltalk. Actualmente podemos compilar objective-c usando gcc.
¿Cómo se vé un hello world en Objective-C?
Normalmente tiene (Al igual que C) un archivo con los headers (.h) y otro con la implementación (.m), sin embargo no se parecen en nada a la sintaxis de C. Objective-C usa “interfaces” que vamos a llamar incorrectamente y para simplificarlo: “clases”.
Nuestro HelloWorld.h se vería:
@interface HelloWorld : NSObject - (void)imprimirFrase:(NSString *)frase @end
Esto puede traducirse a: La clase HelloWorld hereda de NSObject y tiene un método imprimirFrase que devuelve void y recibe un parámetro NSString.
Nuestro HelloWorld.m sería:
#import "HelloWorld.h"
@implementation HelloWorld
- (void)imprimirFrase:(NSString *)frase
{
NSLog(@"%@", frase);
}
@end
Si quisiera llamar a esta función debería hacer:
HelloWorld *hola = [[HelloWorld alloc] init]; [hola imprimirFrase:@"Hola mundo!"]; [hola release];
Como dije antes cuando hablamos de Objective-C no hacemos “llamadas a funciones” (A menos que sea una llamada a una función de C/C++) sino que “enviamos mensajes”. La sintaxis es esta:
[<objeto> <método>:<argumento_1> <nombre_argumento_2>:<argumento_2>....];
¿Y cómo funciona este envío de mensajes?
Lo primero que podemos ver es que los binarios están linkeados a libobjc.A.dylib. Por ahora lo único que vamos a ver de esta librería es una función que se llama “objc_msgSend”.
objc_msgSend es lo que el superset usa internamente para “enviar los mensajes” que vimos anteriormente. Esto es importante porque lo vamos a utilizar todo el tiempo en los análisis.
La función tiene esta declaración:
id objc_msgSend(id theReceiver, SEL theSelector, ...)
Devuelve un id (”id” es un puntero a objc_class que a su vez es un struct. Quizás en el siguiente tutorial entre en detalles sobre esto) y recibe como parámetros:
- El objeto que recibe el mensaje
- El método (SEL es un objc_selector, que a su vez es un (char *). SEL se utiliza en Objective-C para llamar a funciones dinámicamente en runtime).
- Los siguientes parámetros son los argumentos del método.
IV. A los bifes, #1 Crack me.
- El archivo se puede bajár de acá: SmellsGood
Voy a explicar un caso muy intuitivo; que no necesita mucho análisis y parece bastante straight forward, no obstante voy a pasar siempre por los mismos pasos:
- Recolección de información.
- Análisis de la información
- Debugging
- Cracking
Recolección de información
En esta primera etapa lo que vamos a hacer es recolectar todo lo necesario para entender qué sucede en la app y qué podemos hacer.
Lo primero que vay a hacer es desensamblarlo[0]. Para eso voy a usar otx, esta aplicación (otool eXtended) es muy similar a otool pero con un output mucho mas prolijo, optcodes, offsets y lo que es mejor: El nombre de los métodos.
Entonces:
$ otx SmellGood.app > SmellGood.asm
De esta manera tenemos en SmellGood.asm nuestra aplicación desensamblada. Ahora le toca el turno al class-dump. Lo que vamos a hacer en el siguiente paso es *intentar* hacer un dump de las definiciones de las clases usadas en SmellGood. El output debería ser similar a un .h de Objective-C con las interfaces.
$ class-dump SmellGood.app > SmellGood.h
Análisis de la información
Lo siguiente que voy a hacer es tratar de entender qué está sucediendo.
En el .h podemos ver qué métodos nos interesan tanto para entender qué sucede como para empezar a poner break-points. Nuestro caso es este:
- (void)check:(id)arg1; - (BOOL)checkCode:(id)arg1 forName:(id)arg2; - (id)generateCodeForName:(id)arg1; - (id)digest:(id)arg1; - (id)shuffle:(id)arg1;
Sin mucho análisis podemos ver que:
- El código depende del nombre.
- Hay un “digest” (¿md5?, ¿sha1?)
- Hay un shuffle
Ahora hagamos un análisis mas serio desde el assembler. Lo primero que vamos a ver es el método -(BOOL)[SmellsGood_AppDelegate checkCode:forName:]:
+30 00002b18 e855250000 calll 0x00005072 -[(%esp,1) componentsSeparatedByString:] +35 00002b1d 8b150c400000 movl 0x0000400c,%edx count +41 00002b23 89542404 movl %edx,0x04(%esp) +45 00002b27 890424 movl %eax,(%esp) +48 00002b2a e843250000 calll 0x00005072 -[(%esp,1) count] +53 00002b2f 83f805 cmpl $0x05,%eax +56 00002b32 754c jne 0x00002b80 +58 00002b34 a110400000 movl 0x00004010,%eax uppercaseString +63 00002b39 891c24 movl %ebx,(%esp) +66 00002b3c 89442404 movl %eax,0x04(%esp) +70 00002b40 e82d250000 calll 0x00005072 -[(%esp,1) uppercaseString] +75 00002b45 89c3 movl %eax,%ebx +77 00002b47 8b4514 movl 0x14(%ebp),%eax +80 00002b4a 89442408 movl %eax,0x08(%esp) +84 00002b4e a114400000 movl 0x00004014,%eax generateCodeForName: +89 00002b53 89442404 movl %eax,0x04(%esp) +93 00002b57 8b4508 movl 0x08(%ebp),%eax +96 00002b5a 890424 movl %eax,(%esp) +99 00002b5d e810250000 calll 0x00005072 -[(%esp,1) generateCodeForName:] +104 00002b62 891c24 movl %ebx,(%esp) +107 00002b65 89442408 movl %eax,0x08(%esp) +111 00002b69 a118400000 movl 0x00004018,%eax isEqualToString: +116 00002b6e 89442404 movl %eax,0x04(%esp) +120 00002b72 e8fb240000 calll 0x00005072 -[(%esp,1) isEqualToString:] +125 00002b77 ba01000000 movl $0x00000001,%edx +130 00002b7c 84c0 testb %al,%al +132 00002b7e 7502 jne 0x00002b82 +134 00002b80 31d2 xorl %edx,%edx +136 00002b82 83c414 addl $0x14,%esp +139 00002b85 89d0 movl %edx,%eax +141 00002b87 5b popl %ebx +142 00002b88 c9 leave +143 00002b89 c3 ret
Bueno, este es un caso que está clarísimo, incluso sin debuggearlo. No voy a detenerme en esta parte, ya que tiene demasiada meta-data. Lo que voy a hacer es traducir esto que vimos a un código que esté bastante cerca al que suponemos habrá sido el original:
-(BOOL)SmellsGood_AppDelegate checkCode:(NSString *)code forName:(NSString *)name
{
if ([[code componentsSeparatedByString:@"-"] count] != 5) return NO;
NSString *_code = [self generateCodeForName:[name uppercaseString]];
if ([_code isEqualToString:code])
return YES;
else
return NO;
}
¿Bastante facil no?. Lo mismo podemos hacer con generateCodeForName:.
-(NSString *)generateCodeForName:(NSString *)name
{
NSArray *data = [[self digest:[name dataUsingEncoding:4]] componentsSeparatedByString:@" "];
return [[[self shuffle:data] componentsJoinedByString:@"-"] uppercaseString];
}
Por cierto, ese “4″ ahí es del enum NSStringEncoding, en este caso NSUTF8StringEncoding = 4,
Y nuevamente podríamos hacer algo similar con shuffle, pero voy a cambiar de estrategia para que sea mas divertido.
Veamos el asm, separé las partes que nos importan:
+17 00002d0e c744240800000000 movl $0x00000000,0x08(%esp) ... +40 00002d25 e848230000 calll 0x00005072 -[(%esp,1) objectAtIndex:] +48 00002d2d c744240802000000 movl $0x00000002,0x08(%esp) ... +68 00002d41 e82c230000 calll 0x00005072 -[(%esp,1) objectAtIndex:] +76 00002d49 c744240801000000 movl $0x00000001,0x08(%esp) ... +96 00002d5d e810230000 calll 0x00005072 -[(%esp,1) objectAtIndex:] +104 00002d65 c744240803000000 movl $0x00000003,0x08(%esp) ... +123 00002d78 e8f5220000 calll 0x00005072 -[(%esp,1) objectAtIndex:] +131 00002d80 c744240804000000 movl $0x00000004,0x08(%esp) ... +150 00002d93 e8da220000 calll 0x00005072 -[(%esp,1) objectAtIndex:]
Bueno, ahí está bastante claro. Si recuerdan lo que dije de objc_msgSend no es dificil entender lo que sucede: Al principio está poniendo en el esp + 0×08 el número 0 (Recordemos que: esp tiene el objeto, +0×04 tiene un (char *) con el método y a partir de ahí los argumentos).
En resumen lo que está pasando acá es:
[array objectAtIndex:0]; [array objectAtIndex:2]; [array objectAtIndex:1]; [array objectAtIndex:3]; [array objectAtIndex:4];
Por otro lado sin mucho análisis uno puede deducir que el digest hace un sha1 (De nuevo, solo mirando el nombre de los métodos).
Debugging
Bueno, a decir verdad ya tenemos tanta información que no necesitamos mucho debugging. Este paso lo vamos a hacer mas en profundidad en otros crackme’s. Voy a hacer solamente una comprobación. ¿Se acuerdan en el primer código que hay un if que pregunta si el codigo ingresado es el mismo al generateCodeForName: ?. Bueno, vamos a localizarlo, para que se entienda lo que sucede. Según el .asm lo tenemos acá:
+130 00002b7c 84c0 testb %al,%al +132 00002b7e 7502 jne 0x00002b82
Corremos el gdb con el filename de nuestra app en el primer argumento.
$ gdb SmellsGood.app GNU gdb 6.3.50-20050815 (Apple version gdb-1461) (Wed Dec 23 06:11:18 UTC 2009) ...
Seteamos un breakpoint en checkCode:forName:
gdb$ break checkCode:forName: Breakpoint 1 at 0x2b01
Corremos el programa.
gdb$ run
Ahora tocamos en el botón de “check”.
Breakpoint 1, 0x00002b01 in -[SmellsGood_AppDelegate checkCode:forName:] () gdb$ disas Dump of assembler code for function -[SmellsGood_AppDelegate checkCode:forName:]: (...) 0x00002b7c <-[SmellsGood_AppDelegate checkCode:forName:]+130>: test al,al 0x00002b7e <-[SmellsGood_AppDelegate checkCode:forName:]+132>: jne 0x2b82 <-[SmellsGood_AppDelegate checkCode:forName:]+136> (...)
Bien, supongamos que queremos saber qué devuelve ese isEqualToString. (Asumamos por un segundo que sabemos que devuelve “false”… nota: Nunca asumir nada), podemos tomar muchos approaches de como encarar el patch, que vamos a analizar en el siguiente punto. Uno de ellos sería hacer que eso devuelva siempre “true” (o cambiar ese jne a un je). Veamos lo que devuelve esa función:
# Seguimos haciendo stepping hasta llegar a 0x00002b72 (Según lo que vimos en el disas ahí está el test al, al). gdb$ until *0x00002b72 0x00002ab1 in -[SmellsGood_AppDelegate check:] ()
¿Qué pasó?. No llegó nunca a la dirección que esperábamos. ¿Por qué?. Si analizamos el asm (O mas facil el código Objective-C que puse arriba) en un momento pasa esto:
0x00002b1d <-[SmellsGood_AppDelegate checkCode:forName:]+35>: mov edx,DWORD PTR ds:0x400c 0x00002b23 <-[SmellsGood_AppDelegate checkCode:forName:]+41>: mov DWORD PTR [esp+0x4],edx 0x00002b27 <-[SmellsGood_AppDelegate checkCode:forName:]+45>: mov DWORD PTR [esp],eax 0x00002b2a <-[SmellsGood_AppDelegate checkCode:forName:]+48>: call 0x5072 0x00002b2f <-[SmellsGood_AppDelegate checkCode:forName:]+53>: cmp eax,0x5 0x00002b32 <-[SmellsGood_AppDelegate checkCode:forName:]+56>: jne 0x2b80 <-[SmellsGood_AppDelegate checkCode:forName:]+134>
Supongamos por un momento que no tenemos el nombre de los métodos. Veamos que pasa acá:
Tocamos de nuevo “check” y vamos hasta este primer call que vemos arriba.
Breakpoint 1, 0x00002b01 in -[SmellsGood_AppDelegate checkCode:forName:] () gdb$ until *0x00002b2a 0x00002b2a in -[SmellsGood_AppDelegate checkCode:forName:] ()
Veamos a qué función va a llamar:
# Primero el objeto
gdb$ po *(void **)($esp)
{NSCFArray 0x241010} (
dasdasd
)
# Ahora el método gdb$ x/s *(char **)($esp + 0x4) 0x990bea10: "count"
Ok, básicamente lo que hace esto es fijarse si el tamaño de un array es 5. Si vemos mas arriba lo había separado por “-”. Algo ya podemos asumir: el código tiene que tener 5 partes separadas por “-”. Probemos ahora el código X-X-X-X-X y tocamos “check“.
Breakpoint 1, 0x00002b01 in -[SmellsGood_AppDelegate checkCode:forName:] () gdb$ until *0x00002b72 0x00002b72 in -[SmellsGood_AppDelegate checkCode:forName:] ()
Ahora si llegamos. Veamos ahí a qué llama:
gdb$ po *(void **)($esp) X-X-X-X-X gdb$ x/s *(char **)($esp + 0x4) 0x991037e8 <__PRETTY_FUNCTION__.202408+3076>: "isEqualToString:" gdb$ po *(void **)($esp + 0x4) 37E77CCE-999C2986-B5AC28E4-34238294-C7A9F84B
Well, well well.. Parece que ya tenemos el código para ese nombre!. Pero no vamos a parar acá ¿no?. Sigamos:
gdb$ nexti 0x00002b77 in -[SmellsGood_AppDelegate checkCode:forName:] () gdb$ nexti 0x00002b7c in -[SmellsGood_AppDelegate checkCode:forName:] ()
Bueno, ahí estamos en el test al, al. Como ya sabemos al es un partial de eax. Veamos que tiene:
gdb$ info reg eax eax 0x0 0x0
El test hace un AND sin guardar el resultado en ningún lado, solamente setea los flags// pero para qué digo todo esto si ya saben asm. Ya con esto estamos.
Cracking
Siempre hay mas de un camino para seguir a la hora de patchear o generar un keygen, acá es donde juega el ingenio de cada uno. Yo voy a encarar las dos opciones.
Patching (easy way)
La opción mas sencilla como ya vimos es cambiar el jne por un je. Para esto vamos a usar un editor hexadecimal.
Recordemos, lo que queremos cambiar es:
+132 00002b7e 7502 jne 0x00002b82
Como en este caso, la arquitectura x86 está primero en el mach-O no necesitamos calcular el offset. Directamente vamos a ese offset (0×2b7e) en el editor hexadecimal, tiene que haber un “75″ (optcode de jne). Cambiamos por “74″ (optcode de je), corremos el programa, llenamos el codigo con cualquier cosa de la forma X-X-X-X-X y vemos que pasa.
Voilá!.
Keygen
Ya que nos tomamos el trabajo de hacer todo el análisis y de comprender qué está pasando, deberíamos aprovechar y hacer un keygen que imite la lógica que ya pudimos deducir.
Este es el mio:
from hashlib import sha1
name = raw_input("Nombre?: ").strip()
sha1str = sha1(name).hexdigest().upper()
slots = [sha1str[i:i+8] for i in xrange(0, len(sha1str), 8)]
code = "%s-%s-%s-%s-%s" % (slots[4], slots[3], slots[1], slots[2], slots[0])
print code
[0] Si se me permite el neologismo
Tags: cracking, osx, reverse-engineer
Leave a Reply