Reverse engineer en OSX – Parte I

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

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:

  1. El objeto que recibe el mensaje
  2. 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).
  3. Los siguientes parámetros son los argumentos del método.

IV. A los bifes, #1 Crack me.

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

@Reflejo

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>