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

Un día en internet

Interesante infografía del volumen de datos que se mueven en internet por día:

A Day in the Internet
Created by OnlineEducation.net

Quotes del dia

Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.

Martin Golding

Programming is like sex: one mistake and you’re providing support for a lifetime.

Michael Sinz

The trouble with programmers is that you can never tell what a programmer is doing until it’s too late.

Seymour Cray

Good code is its own best documentation.

Steve McConnell

Most software today is very much like an Egyptian pyramid with millions of bricks piled on top of each other, with no structural integrity, but just done by brute force and thousands of slaves.

Alan Kay

Sacadas de WTF Code, una linda distracción mas para suscribirme por RSS.