Ingeniería inversa en Mac OS X Ingeniería inversa en Mac OS X MacProgramadores Acerca de este documento La ingeniería inversa (reversing) son un conjunto de técnicas que nos permiten descubrir cómo funcionan las aplicaciones cuando no disponemos de su código fuente. Estas técnicas se utilizan frecuentemente para modificar el comportamiento de programas que incluyen funcionalidades no deseadas. Este reportaje describe estas técnicas en el entorno de Mac OS X. Antes de leer este reportaje es importante saber programar en C. También ayudará conocer la programación ensamblador del x86, las herramientas de desarrollo de Apple y los conceptos clásicos de sistemas operativos. Si no conoce bien estos conceptos quizá le ayude empezar primero leyendo el tutorial "Compilar y depurar aplicaciones con las herramientas de programación de GNU". Podrá encontrar este tutorial publicado en MacProgramadores. Nota legal Este reportaje ha sido escrito por Fernando López Hernández para MacProgramadores, y de acuerdo a los derechos que le concede la legislación española e internacional el autor prohíbe la publicación de este documento en cualquier otro servidor web, así como su venta, o difusión en cualquier otro medio sin autorización previa. Sin embargo el autor anima a todos los servidores web a colocar enlaces a este documento. El autor también anima a bajarse o imprimirse este tutorial a cualquier persona interesada en conocer las técnicas de ingeniería inversa y sus aplicaciones. Madrid, Agosto 2010 Para cualquier aclaración contacte con: fernando@DELITmacprogramadores.org Pág 2 Ingeniería inversa en Mac OS X MacProgramadores Tabla de contenido 1. Introducción ...............................................................................................................4 2. Desensamblar aplicaciones...........................................................................................4 2.1. Herramientas básicas.............................................................................................4 2.2. Desensamblar con gdb..........................................................................................7 3. Depurar en ensamblador..............................................................................................9 3.1. Depuración paso a paso.........................................................................................9 3.2. Análisis de memoria.............................................................................................12 3.3. Modificar la memoria y el binario..........................................................................14 4. Ingeniería inversa de código Objective-C.....................................................................17 4.1. Cómo funciona el envío de mensajes....................................................................17 4.2. Herramientas para analizar el binario....................................................................17 4.3. Descubrir el método llamado................................................................................18 4.4. Depurar un método Objective-C...........................................................................20 5. Bibliografía................................................................................................................21 Pág 3 Ingeniería inversa en Mac OS X MacProgramadores 1. Introducción Las técnicas de ingeniería inversa (reversing) han sido especialmente bien estudiadas en Microsoft Windows. Este entorno dispone de buenas herramientas de reversing como son IDA Pro y Ollydbg. En el caso de Mac OS X existen menos herramientas y están menos avanzadas. En este reportaje vamos a mostrar cómo llevar a cabo ingeniería inversa en Mac OS X con las herramientas que actualmente existen. Tenga en cuenta que la legislación de algunos países prohíbe hacer ingeniería inversa de aplicaciones con copyright. Si vive en uno de estos países, antes de hacer este estudio debe de asegurarse de que la licencia del programa analizado permite que se haga reversing. Existen programas llamados crackmes creados como ejemplos o retos para ser desensamblados y descubrir el secreto que encierran (p.e. encontrar su clave de validación). 2. Desensamblar aplicaciones En los siguientes apartados vamos a utilizar el programa del Listado 1. Una vez compilado supondremos que no tenemos acceso al código fuente y veremos cómo estudiar su comportamiento a partir de su binario. #include <stdio.h> int control_acceso(char* buffer) { return !strcmp(buffer,"1230u"); } int main() { printf("Indique clave:"); char buffer[16]; scanf("%16s",buffer); if (control_acceso(buffer)) { printf("Bienvenido\n"); } else { printf("Clave incorrecta, adios\n"); } return 0; } Listado 1: Programa a analizar Para generar el binario utilizamos el comando: $ gcc adivina.c -O2 -o adivina Obsérvese que lo hemos compilado usando la opción de optimización -O2. Esta opción es la que suele usarse para compilar aplicaciones release y su uso suele modificar sustancialmente el ensamblador subyacente para mejorar su rendimiento. 2.1. Herramientas básicas Para empezar a analizar este binario podemos usar el comando nm, el cual nos muestra los símbolos con los que enlaza: Pág 4 Ingeniería inversa en Mac OS X $ nm adivina 0000000100000ed4 0000000100001070 0000000100001078 0000000100001088 0000000100000000 0000000100000dd0 0000000100001080 0000000100000df0 0000000100001000 0000000100000d90 s D D D U U A T D U T U U s U U U T MacProgramadores stub helpers _NXArgc _NXArgv ___progname ___stack_chk_fail ___stack_chk_guard __mh_execute_header _control_acceso _environ _exit _main _printf _puts _pvars _scanf _strcmp dyld_stub_binder start La Tabla 1 muestra los tipos de símbolos que podemos encontrar en un binario Mach-O. Los símbolos en mayúsculas están exportados y los símbolos en minúsuclas no. Los símbolos marcados con U nos sirven para saber con qué funciones enlaza nuestro programa. Estas funciones estarán definidas en liberías de enlace dinámico. Los símbolos marcados con D hacen referencia a variables globales de la aplicación. Los símbolos marcados con T hacen referencia a las funciones que implementa la aplicación. Tipo U A T D B C I S - Descripción Símbolo externo indefinido (Undefined), es decir, un símbolo que ha sido declarado pero no definido por los módulos objeto. Dirección absoluta. No será asignada por el enlazador dinámico. Sección de código (Text). Sección de datos (Data). Sección BSS. Símbolo común. Símbolo indirecto. Símbolo en otra sección distinta a las anteriores. Símbolo de depuración. Para que se muestren debemos usar –a Tabla 1: Tipo de símbolos en un binario Mach-O Para desensamblar el binario podemos usar el comando otool con las opciones -tV (para el segmento de código) o -dV (para el segmento de datos). $ otool -tV adivina adivina: (__TEXT,__text) section start: 0000000100000dd0 pushq 0000000100000dd2 movq 0000000100000dd5 andq 0000000100000dd9 movq 0000000100000ddd leaq 0000000100000de1 movl 0000000100000de3 addl 0000000100000de6 shll $0x00 %rsp,%rbp $0xf0,%rsp 0x08(%rbp),%rdi 0x10(%rbp),%rsi %edi,%edx $0x01,%edx $0x03,%edx Pág 5 Ingeniería inversa en Mac OS X ····· _main: 0000000100000e10 0000000100000e11 0000000100000e14 0000000100000e16 0000000100000e17 0000000100000e1b 0000000100000e22 0000000100000e26 0000000100000e2a 0000000100000e2c 0000000100000e33 ····· pushq movq pushq pushq subq movq movq movq xorl leaq callq MacProgramadores %rbp %rsp,%rbp %r12 %rbx $0x20,%rsp 0x00000206(%rip),%r12 (%r12),%rax %rax,0xe8(%rbp) %eax,%eax 0x00000095(%rip),%rdi 0x100000eb0 ; symbol stub for: _printf Tras ejecutar este comando vemos que la función main comienza en la dirección 0x100000e10. También vemos que la optimización ha sustituido algunas llamadas a printf() por llamadas a puts(). $ otool -dV adivina adivina: (__DATA,__data) section 0000000100001070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000100001080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 El segmento de datos es más pequeño y está sin inicializar. Las variables globales van desde _NXArgc (en la dirección 0x100001070) a _environ (en la dirección 0x100001080). También disponemos de los comandos ndisasm (actualmente sólo para 32 bits), otx (para las arquitecturas ppc y i386) y otx64 (para la arquitectura x86_64). Estos comando tienen más opciones. Por ejemplo, nos permiten especificar un rango del fichero a desensamblar. Además, a diferencia de otool, no sólo muestran el código desensamblado, sino que también muestran el código binario de las instrucciones desensambladas. Los comandos otx y otx64 (otool eXtended) no se distribuyen con Xcode pero puede instalarse desde MacPorts. Por ejemplo, para desensamblar un binario de 32 bits con ndisasm hacemos: $ gcc adivina.c -O2 -arch i386 -o adivina $ ndisasm -e 30DC adivina 000030DC 5F pop edi 000030DD 4E dec esi 000030DE 58 pop eax 000030DF 41 inc ecx 000030E0 7267 jc 0x3149 000030E2 7600 jna 0x30e4 000030E4 5F pop edi 000030E5 53 push ebx 000030E6 756D jnz 0x3155 000030E8 61 popa Para desensamblarlo con otx haríamos: $ otx adivina (__TEXT,__text) section start: +0 00001e20 6a00 +2 00001e22 89e5 +4 00001e24 83e4f0 pushl movl andl Pág 6 $0x00 %esp,%ebp $0xf0,%esp Ingeniería inversa en Mac OS X +7 ····· _main: +0 +1 +3 +6 +9 ····· MacProgramadores 00001e27 83ec10 subl $0x10,%esp 00001e60 00001e61 00001e63 00001e66 00001e69 55 89e5 83ec68 895df4 8975f8 pushl movl subl movl movl %ebp %esp,%ebp $0x68,%esp %ebx,0xf4(%ebp) %esi,0xf8(%ebp) El comando otx nos muestra tanto la rutina start como la rutina _main. 2.2. Desensamblar con gdb Una herramienta todavía más potente para desensamblar es gdb. En este apartado vamos a empezar a usar esta herramienta. $ gdb adivina (gdb) disassemble main Dump of assembler code for function main: 0x00001e80 <main+0>: push %ebp 0x00001e81 <main+1>: mov %esp,%ebp 0x00001e83 <main+3>: sub $0x48,%esp 0x00001e86 <main+6>: mov %ebx,-0xc(%ebp) 0x00001e89 <main+9>: mov %esi,-0x8(%ebp) 0x00001e8c <main+12>: mov %edi,-0x4(%ebp) 0x00001e8f <main+15>: call 0x1e94 <main+20> 0x00001e94 <main+20>: pop %ebx 0x00001e95 <main+21>: mov 0x188(%ebx),%edi 0x00001e9b <main+27>: mov (%edi),%eax 0x00001e9d <main+29>: mov %eax,-0x1c(%ebp) 0x00001ea0 <main+32>: xor %eax,%eax 0x00001ea2 <main+34>: lea 0x87(%ebx),%eax 0x00001ea8 <main+40>: mov %eax,(%esp) 0x00001eab <main+43>: call 0x1f5e <dyld_stub_printf> 0x00001eb0 <main+48>: lea -0x2c(%ebp),%esi ·········· La herramienta nos permite indicar la función a desensamblar así como la notación ensamblador (AT&T o Intel). (gdb) set disassembly-flavor intel (gdb) disassemble main Dump of assembler code for function main: 0x00001e80 <main+0>: push ebp 0x00001e81 <main+1>: mov ebp,esp 0x00001e83 <main+3>: sub esp,0x48 0x00001e86 <main+6>: mov DWORD PTR [ebp-0xc],ebx 0x00001e89 <main+9>: mov DWORD PTR [ebp-0x8],esi 0x00001e8c <main+12>: mov DWORD PTR [ebp-0x4],edi 0x00001e8f <main+15>: call 0x1e94 <main+20> 0x00001e94 <main+20>: pop ebx 0x00001e95 <main+21>: mov edi,DWORD PTR [ebx+0x188] 0x00001e9b <main+27>: mov eax,DWORD PTR [edi] 0x00001e9d <main+29>: mov DWORD PTR [ebp-0x1c],eax 0x00001ea0 <main+32>: xor eax,eax 0x00001ea2 <main+34>: lea eax,[ebx+0x87] 0x00001ea8 <main+40>: mov DWORD PTR [esp],eax 0x00001eab <main+43>: call 0x1f5e <dyld_stub_printf> Pág 7 Ingeniería inversa en Mac OS X 0x00001eb0 <main+48>: ·········· MacProgramadores lea esi,[ebp-0x2c] Podemos conocer las librerías con las que enlaza nuestra aplicación usando el siguiente comando: (gdb) info sharedlibrary The DYLD shared library state has been initialized from the executable's shared library information. All symbols should be present, but the addresses of some symbols may move when the program is executed, as DYLD may relocate library load addresses if necessary. Requested State Current State Num Basename Type Address Reason | | Source | | | | | | | | 1 adivina - exec Y Y /Users/fernando/tmp/adivina (offset 0x0) 2 dyld - init Y Y /usr/lib/dyld at 0x8fe00000 with prefix "__dyld_" 3 libSystem.B.dylib - init Y Y /usr/lib/libSystem.B.dylib at 0x896000 (offset 0x896000) (commpage objfile is) /usr/lib/libSystem.B.dylib[LC_SEGMENT.__DATA.__commpage] El comando nos avisa de que posiblemente las direcciones de los símbolos no sean correctas ya que el enlazador dinámico puede reasignar estas direcciones la primera ver que accedemos a un símbolo. Es decir, para estar seguros de que la dirección de un símbolo es correcta, el programa debe de haber accedido a este símbolo al menos una vez. También podemos usar disassemble start, disassemble start end o disassemble start +length para desensamblar un determinado rango de direcciones, o bien disassemble symbol para desensamblar a partir de la dirección de memoría de ese símbolo: (gdb) disassemble 0x00001ea2 0x00001ecf Dump of assembler code from 0x1ea2 to 0x1ecf: 0x00001ea2 <main+34>: lea 0x87(%ebx),%eax 0x00001ea8 <main+40>: mov %eax,(%esp) 0x00001eab <main+43>: call 0x1f5e <dyld_stub_printf> 0x00001eb0 <main+48>: lea -0x2c(%ebp),%esi 0x00001eb3 <main+51>: mov %esi,0x4(%esp) 0x00001eb7 <main+55>: lea 0x96(%ebx),%eax 0x00001ebd <main+61>: mov %eax,(%esp) 0x00001ec0 <main+64>: call 0x1f6a <dyld_stub_scanf> 0x00001ec5 <main+69>: mov %esi,(%esp) 0x00001ec8 <main+72>: call 0x1e30 <control_acceso> End of assembler dump. (gdb) disassemble puts Dump of assembler code for function puts: 0x90541b5b <puts+0>: push %ebp 0x90541b5c <puts+1>: mov %esp,%ebp 0x90541b5e <puts+3>: push %edi 0x90541b5f <puts+4>: push %esi 0x90541b60 <puts+5>: push %ebx 0x90541b61 <puts+6>: sub $0x3c,%esp 0x90541b64 <puts+9>: call 0x90541b69 <puts+14> 0x90541b69 <puts+14>: pop %ebx 0x90541b6a <puts+15>: mov 0x8(%ebp),%eax ····· Pág 8 Ingeniería inversa en Mac OS X MacProgramadores 3. Depurar en ensamblador El análisis estático de código desensamblado es una labor muy tediosa ya que hay que ir registrando cada una de las variaciones que se pueden dar en las distintas instrucciones ensamblador. Por esta razón el análisis estático sólo se suele usar para tener una visión general del programa a analizar. Para analizar el programa en mayor profundidad se lleva a cabo un análisis dinámico del código desensamblado usando un depurador que permita depurar ensamblador. En Mac OS X este depurador es normalmente gdb. A partir de Mac OS X 10.6 las aplicaciones se compilan por defecto para Intel de 64 bits. Si estamos depurando instrucciones Intel de 32 bits las instrucciones serán parecidas. La principal diferencia será que las direcciones de memoria y los registros en vez de tener 64 bits (rax, rbx, ... rbp, rsp, rip, etc) tendrán sólo 32 bits (eax, ebx, ... ebp, esp, eip, etc). $ gcc adivina.c -O2 -o adivina $ gdb adivina (gdb) 3.1. Depuración paso a paso Para depurar es muy común poner un breakpoint en main y ejecutar hasta su posición: (gdb) b main Breakpoint 1 at 0x100000dfb (gdb) r Breakpoint 1, 0x0000000100000dfb in main () Después podemos depurar paso a paso con los comandos nexti (ni) que se salta las funciones llamadas y stepi (si) que se mete dentro de las funciones llamadas. Estos comandos son equivalentes a los comandos next (n) y step (s) para lenguajes de alto nivel: (gdb) nexti 0x0000000100000e02 in main () Podemos consultar el registro rip con el comando print (p) y poniendo un $ delante del nombre del registro. Este registro contiene la posición por la que va la ejecución del programa: (gdb) p $rip $1 = (void (*)()) 0x100000e02 <main+18> (gdb) disassemble $rip $rip+20 Dump of assembler code from 0x100000e02 to 0x100000e16: 0x0000000100000e02 <main+18>: mov (%r12),%rax 0x0000000100000e06 <main+22>: mov %rax,-0x18(%rbp) 0x0000000100000e0a <main+26>: xor %eax,%eax 0x0000000100000e0c <main+28>: lea 0x8b(%rip),%rdi # 0x100000e9e 0x0000000100000e13 <main+35>: callq 0x100000e80 <dyld_stub_printf> Observe que el desensamblador muestra mediante comentarios las direcciones que se pueden calcular. Por ejemplo, si rip vale 0x100000e13 después de haber cargado la instrucción en la dirección 0x100000e0c, 0x8b(%rip) valdrá 0x100000e9e. El desensamblador también muestra el símbolo que corresponde a las direcciones que llama la instrucción callq. Tenga en cuenta que en Mac OS X las funciones se enlazan tardíamente (lazy binding) mediante un stub de función. La primera vez que se llama este stub de función se carga en memoPág 9 Ingeniería inversa en Mac OS X MacProgramadores ria la librería de enlace dinámico (si no está ya cargada) y se reajusta la dirección a la que salta el stub: (gdb) disassemble dyld_stub_printf Dump of assembler code for function dyld_stub_printf: 0x00007fff87294cf0 <dyld_stub_printf+0>: jmpq *-0x164b4606(%rip) # 0x7fff70de06f0 El comando print también permite conocer el contenido de cualquier símbolo. Si este símbolo es una función nos devuelve su dirección de memoria: (gdb) p strcmp $1 = {<text variable, no debug info>} 0x7fff87157350 <strcmp> De nuevo conviene recordar que, al resolver las direcciones de memoria de las funciones y otros símbolos exportados, el valor que devuelve print puede ser incorrecto si el programa no ha usado el símbolo al menos una vez. Podemos conocer el estado de un determinado registro con el comando: (gdb) info reg rax eax 0x100000d90 4294970768 O bien el estado de los diferentes registros con el comando: (gdb) info registers rax 0x100000d90 4294970768 rbx 0x0 0 rcx 0x7fff5fbfebe0 140734799801312 rdx 0x7fff5fbfea78 140734799800952 rsi 0x7fff5fbfea68 140734799800936 rdi 0x1 1 rbp 0x7fff5fbfea40 0x7fff5fbfea40 rsp 0x7fff5fbfea10 0x7fff5fbfea10 r8 0x29fc9c7 44026311 r9 0x0 0 r10 0x1200 4608 r11 0x206 518 r12 0x7fff70dea5c0 140735087027648 r13 0x0 0 r14 0x0 0 r15 0x0 0 rip 0x100000e02 0x100000e02 <main+18> eflags 0x202 514 cs 0x27 39 ss 0x0 0 ds 0x0 0 es 0x0 0 fs 0x0 0 gs 0x0 0 Si queremos conocer el estado de todos los registros (incluidos los de punto flotante, MMX y SSE) debemos usar el comando: (gdb) info registers all Este comando muestra mucha información con lo que quizá sea mejor especificar el registro Pág 10 Ingeniería inversa en Mac OS X MacProgramadores que nos interesa conocer: (gdb) p $xmm1 { v4_float = {-nan(0x7fffff), 0, 0, 0}, v2_double = {-nan(0xfffff00000000), 0}, v16_int8 = {-1, -1, -1, -1, 0 <repeats 12 times>}, v8_int16 = {-1, -1, 0, 0, 0, 0, 0, 0}, v4_int32 = {-1, 0, 0, 0}, v2_int64 = {-4294967296, 0}, uint128 = 0xffffffff000000000000000000000000 } En nuestro programa de ejemplo nos puede interesar saltar directamente a la dirección 0x100000e30, que parece tener una llamada interesante. Para ello ponemos un breakpoint usando el comando b al que le damos una dirección de memoria precedida por un *. En el siguiente comando usamos el símbolo $pc (program counter) para referirnos al puntero al siguiente registro de la plataforma ($rip para Intel de 64 bits o $eip para Intel de 32). (gdb) disas $pc Dump of assembler code for function main: 0x0000000100000e02 <main+18>: mov (%r12),%rax 0x0000000100000e06 <main+22>: mov %rax,-0x18(%rbp) 0x0000000100000e0a <main+26>: xor %eax,%eax 0x0000000100000e0c <main+28>: lea 0x8b(%rip),%rdi # 0x100000e9e 0x0000000100000e13 <main+35>: callq 0x100000e80 <dyld_stub_printf> 0x0000000100000e18 <main+40>: lea -0x30(%rbp),%rbx 0x0000000100000e1c <main+44>: mov %rbx,%rsi 0x0000000100000e1f <main+47>: lea 0x87(%rip),%rdi # 0x100000ead 0x0000000100000e26 <main+54>: xor %eax,%eax 0x0000000100000e28 <main+56>: callq 0x100000e8c <dyld_stub_scanf> 0x0000000100000e2d <main+61>: mov %rbx,%rdi 0x0000000100000e30 <main+64>: callq 0x100000dd0 <control_acceso> 0x0000000100000e35 <main+69>: test %eax,%eax 0x0000000100000e37 <main+71>: je 0x100000e60 <main+112> 0x0000000100000e39 <main+73>: lea 0x72(%rip),%rdi # 0x100000eb2 (gdb) b *0x0000000100000e30 Breakpoint 2 at 0x100000e30 (gdb) c Continuing. Indique clave:1234 Breakpoint 2, 0x0000000100000e30 in main () Ahora para meternos en la función llamada debemos usar el comando stepi: (gdb) stepi 0x0000000100000dd0 in control_acceso () (gdb) disas Dump of assembler code for function control_acceso: 0x0000000100000dd0 <control_acceso+0>: push %rbp 0x0000000100000dd1 <control_acceso+1>: mov %rsp,%rbp 0x0000000100000dd4 <control_acceso+4>: lea 0xbd(%rip),%rsi 0x100000e98 0x0000000100000ddb <control_acceso+11>: xor %eax,%eax 0x0000000100000ddd <control_acceso+13>: callq 0x100000e92 <dyld_stub_strcmp> 0x0000000100000de2 <control_acceso+18>: test %eax,%eax 0x0000000100000de4 <control_acceso+20>: sete %al 0x0000000100000de7 <control_acceso+23>: movzbl %al,%eax Pág 11 # Ingeniería inversa en Mac OS X MacProgramadores 0x0000000100000dea <control_acceso+26>: 0x0000000100000deb <control_acceso+27>: 0x0000000100000dec <control_acceso+28>: End of assembler dump. leaveq retq nopl 0x0(%rax) El comando disas nos nuestra el contenido de la función control_acceso(). Para conocer la posición de anidamiento donde nos encontramos podemos usar el comando backtrace (bt): (gdb) backtrace #0 0x0000000100000dd4 in control_acceso () #1 0x0000000100000e35 in main () Este comando recorre los registros rbp y rsp para obtener infomación. Podemos obtener una información más detallada con el comando info frame: (gdb) info frame Stack level 0, frame at 0x7fff5fbfeaa0: rip = 0x100000dd4 in control_acceso; saved rip 0x100000e35 called by frame at 0x7fff5fbfeae0 Arglist at 0x7fff5fbfea98, args: Locals at 0x7fff5fbfea98, Previous frame's sp is 0x7fff5fbfeaa0 Saved registers: rbp at 0x7fff5fbfea90, rip at 0x7fff5fbfea98 También es interesante saber que podemos llamar a cualquier función desde el depurador. Si el binario tiene información de deputación no es necesario indicar el tipo de los parámetros o retorno de la función, pero en nuestro caso el binario no tiene esta información y se la tenemos que dar explicitamente: (gdb) call (double)sin((double)1.0) $3 = 0.8414709848078965 (gdb) call (int)control_acceso((char*)"1234") $4 = 0 (gdb) call (int)control_acceso((char*)"1230u") $5 = 1 3.2. Análisis de memoria El depurador nos proporciona el comando x para inspeccionar la memoria. La sintaxis de este comando es x/fmt address. El campo fmt consta de un número de repeticiones que indica el número de elementos de memoria a inspeccionar, una letra de formato que indica cómo formatear este valor y una letra de tamaño que indica el tamaño de cada elemento. La Tabla 2 recoge las posibles letras de formato y la Tabla 3 recoge las posibles letras de tamaño. El campo address puede ser el nombre de un símbolo o una dirección de memoria. Por ejemplo, en nuestro ejemplo anterior introdujimos como clave la cadena "1234". El comando info frame nos dijo que las variables locales se encuentran a partir de la posición 0x7fff5fbfea98. Podemos buscar su posición exacta volcando los siguientes 30 caracteres a esta posición de memoria: (gdb) x/30c 0x7fff5fbfea98 0x7fff5fbfea98: 53 '5' 14 '\016'0 '\0' '\0' 0 '\0' 0x7fff5fbfeaa0: 49 '1' 50 '2' 51 '3' '\0' 0 '\0' Pág 12 0 '\0' 1 '\001' 0 '\0' 0 52 '4' 0 '\0' 0 0 '\0' Ingeniería inversa en Mac OS X 0x7fff5fbfeaa8: '\0' 0 '\0' 0x7fff5fbfeab0: MacProgramadores 0 '\0' 0 '\0' 0 '\0' 0 '\0' 0 '\0' 0 '\0' 0 '\0' 0 '\0' 0 '\0' 0 '\0' 0 '\0' 0 '\0' 0 Al volcar el contenido de memoria descubrimos que esta cadena se encuentra en la posición de memoria 0x7fff5fbfeaa0. Podemos volver a mostrar esta cadena usando la letra de formato s de la forma: (gdb) x/s 0x7fff5fbfeaa0 0x7fff5fbfeaa0: "1234" Si queremos obtener en hexadecimal estos caracteres necesitamos indicar que el tamaño de cada elemento sea de 1 byte (y no de 4 bytes que es el valor por defecto): (gdb) x/4xb 0x7fff5fbfeaa0 0x7fff5fbfeaa0: 0x31 0x32 0x33 Letra Descripción o Octal x Hexadecimal d Decimal u Unsigned decimal t binary a address i instruction c char s string T OSType A Floating point value in hex 0x34 Tabla 2: Letras de formato del comando x Letra Descripción b Byte, 1 byte h Halfword, 2 bytes w Word, 4 bytes g Giant, 8 bytes. Tabla 3: Letras de tamaño del comando x El comando x también nos permite desensamblar las instrucciones en una determinada dirección de memoria. Por ejemplo para desensamblar 3 instrucciones desde la dirección de memoria actual usamos: (gdb) x/3i $pc 0x100000dd4 <control_acceso+4>: lea 0x100000ddb <control_acceso+11>: xor Pág 13 0xbd(%rip),%rsi # 0x100000e98 %eax,%eax Ingeniería inversa en Mac OS X MacProgramadores 0x100000ddd <control_acceso+13>: callq 0x100000e92 <dyld_stub_strcmp> La función strcmp recibe sus parámetros como punteros en los registros rsi y rdi y devuelve el retorno en el registro rax. Podemos ejecutar dos instrucciones para ver los parámetros que va a recibir: (gdb) ni 2 0x0000000100000ddd in control_acceso () (gdb) x/3i $rip 0x100000ddd <control_acceso+13>: callq 0x100000e92 <dyld_stub_strcmp> 0x100000de2 <control_acceso+18>: test %eax,%eax 0x100000de4 <control_acceso+20>: sete %al (gdb) x/s $rsi 0x100000e98: "1230u" (gdb) x/s $rdi 0x7fff5fbfeaa0: "1234" (gdb) p $rax $12 = 0 (gdb) n Y el valor que retorna estará ahora almacenado en el registro rax: (gdb) p $rax $13 = 4 El comando p también se puede formatear. Por ejemplo, para obtener en binario el valor del registro rax podemos hacer: (gdb) p/t $rax $2 = 100 La memoria del proceso también se puede modificar desde el depurador con el comandos set. Por ejemplo para cambiar la cadena apuntada por el registro rdi podemos hacer: (gdb) x/s $rdi 0x7fff5fbfeaa0: "1234" (gdb) set *0x7fff5fbfeaa0="1230u" (gdb) x/s $rdi 0x7fff5fbfeaa0: "1230u" O bien directamente: (gdb) set $rdi="1230u" 3.3. Modificar la memoria y el binario Muy frecuentemente la ingeniería inversa se utiliza para modificar el comportamiento de un programa de forma permanente. Por ejemplo, podríamos modificar el binario de nuestro ejemplo para que siempre dé acceso independientemente de si la clave es correcta o no. Para ello nuestro objetivo sería que control_acceso siempre devuelva true (-1) en el registro eax. A continuación volvemos a mostrar el contenido de esta función en 32 bits porque el comando offset que vamos a estudiar más adelante todavía no soporta binarios de 64 bits: (gdb) disassemble control_acceso Pág 14 Ingeniería inversa en Mac OS X MacProgramadores Dump of assembler code for function control_acceso: 0x00001e30 <control_acceso+0>: push %ebp 0x00001e31 <control_acceso+1>: mov %esp,%ebp 0x00001e33 <control_acceso+3>: sub $0xc,%esp 0x00001e36 <control_acceso+6>: mov %ebx,(%esp) 0x00001e39 <control_acceso+9>: mov %esi,0x4(%esp) 0x00001e3d <control_acceso+13>: mov %edi,0x8(%esp) 0x00001e41 <control_acceso+17>: call 0x1e46 <control_acceso+22> 0x00001e46 <control_acceso+22>: pop %ebx 0x00001e47 <control_acceso+23>: mov 0x8(%ebp),%esi 0x00001e4a <control_acceso+26>: lea 0xcf(%ebx),%edi 0x00001e50 <control_acceso+32>: mov $0x6,%ecx 0x00001e55 <control_acceso+37>: cld 0x00001e56 <control_acceso+38>: repz cmpsb %es:(%edi),%ds:(%esi) 0x00001e58 <control_acceso+40>: mov $0x0,%eax 0x00001e5d <control_acceso+45>: je 0x1e69 <control_acceso+57> 0x00001e5f <control_acceso+47>: movzbl -0x1(%esi),%eax 0x00001e63 <control_acceso+51>: movzbl -0x1(%edi),%ecx 0x00001e67 <control_acceso+55>: sub %ecx,%eax 0x00001e69 <control_acceso+57>: test %eax,%eax 0x00001e6b <control_acceso+59>: sete %al 0x00001e6e <control_acceso+62>: movzbl %al,%eax 0x00001e71 <control_acceso+65>: mov (%esp),%ebx 0x00001e74 <control_acceso+68>: mov 0x4(%esp),%esi 0x00001e78 <control_acceso+72>: mov 0x8(%esp),%edi 0x00001e7c <control_acceso+76>: leave 0x00001e7d <control_acceso+77>: ret 0x00001e7e <control_acceso+78>: xchg %ax,%ax End of assembler dump. En el código desensamblado de 32 bits vemos que la opción de optimización ha sustituido la función strcmp() inline. Después la instrucción sete pone a 1 todos los bits del registro al si la instrucción test anterior tubo éxito (es decir, si strcmp retorna que las cadenas son iguales). En caso contrario la instrucción sete pone todos los bits de al a 0. La instrucción movzbl (Move Zero extended Byte to Long) expande el número desde la parte del registro al a todo el registro eax. Esto se hace porque la función devuelve un int de 4 bytes (almacenado en eax). Lo que nos interesa para poder cambiar las instrucciones del binario es conocer cuanto mide cada instrucción y qué bytes la componen. Esto lo podemos conocer con el comando otx: $ otx adivina | grep -A8 sete +59 00001e6b 0f94c0 +62 00001e6e 0fb6c0 +65 00001e71 8b1c24 +68 00001e74 8b742404 +72 00001e78 8b7c2408 +76 00001e7c c9 +77 00001e7d c3 +78 00001e7e 6690 sete movzbl movl movl movl leave ret nop %al %al,%eax (%esp),%ebx 0x04(%esp),%esi 0x08(%esp),%edi Ahora sabemos que la instrucción sete %al en binario corresponde a 3 bytes: 0f 94 c0, y que la instrucción movzbl %al,%eax corresponde en binario a otros 3 bytes: 0f b6 c0, y así sucesivamente. Podemos preceder estas dos instrucciones por la famosa instrucción xorl %eax,%eax que pone a 0 el registro eax: $ otx adivina | grep xorl Pág 15 Ingeniería inversa en Mac OS X +32 +95 +100 00001ea0 00001edf 00001ee4 31c0 31c0 3317 MacProgramadores xorl xorl xorl %eax,%eax %eax,%eax (%edi),%edx Como esta instrucción ocupa dos bytes podemos mover adelante el resto de instrucciones y eliminar la instrucción nop que aparece en la posición 0x1e7e, y que también ocupa 2 bytes. Ahora nos queda por saber qué posición ocupa en el binario la instrucción sete. Para ello podemos usar la utilidad offset1. La utilidad recibe tres argumentos: El nombre del binario, el offset de la instrucción según otx o otool -tV, y la arquitectura que por defecto es i386. Actualmente esta utilidad soporta ppc y i386, pero no x86_64. $ offset adivina 00001e6b Mach-o Binary Offset Calculator v1.21 ..................................... (c) 2009 fG! - http://reverse.put.as - reverse@put.as Found a Mach-O i386 only binary! Reading Mach Header... Real offset to be patched: 0xe6b Ahora sabemos que esta instrucción se encuentra en la posición 0xe6b del binario adivina, con lo que podemos usar un editor binario como 0xED para cambiar la cadena binaria 0f94c00fb6c08b1c248b7424048b7c2408c9c36690 por la cadena binaria 31c00f94c00fb6c08b1c248b7424048b7c2408c9c3. Tras volver a cargar en gdb el binario obtenemos la función desensamblada con nuestro cambio aplicado: (gdb) disassemble control_acceso Dump of assembler code for function control_acceso: 0x00001e30 <control_acceso+0>: push %ebp 0x00001e31 <control_acceso+1>: mov %esp,%ebp 0x00001e33 <control_acceso+3>: sub $0xc,%esp 0x00001e36 <control_acceso+6>: mov %ebx,(%esp) 0x00001e39 <control_acceso+9>: mov %esi,0x4(%esp) 0x00001e3d <control_acceso+13>: mov %edi,0x8(%esp) 0x00001e41 <control_acceso+17>: call 0x1e46 <control_acceso+22> 0x00001e46 <control_acceso+22>: pop %ebx 0x00001e47 <control_acceso+23>: mov 0x8(%ebp),%esi 0x00001e4a <control_acceso+26>: lea 0xcf(%ebx),%edi 0x00001e50 <control_acceso+32>: mov $0x6,%ecx 0x00001e55 <control_acceso+37>: cld 0x00001e56 <control_acceso+38>: repz cmpsb %es:(%edi),%ds:(%esi) 0x00001e58 <control_acceso+40>: mov $0x0,%eax 0x00001e5d <control_acceso+45>: je 0x1e69 <control_acceso+57> 0x00001e5f <control_acceso+47>: movzbl -0x1(%esi),%eax 0x00001e63 <control_acceso+51>: movzbl -0x1(%edi),%ecx 0x00001e67 <control_acceso+55>: sub %ecx,%eax 0x00001e69 <control_acceso+57>: test %eax,%eax 0x00001e6b <control_acceso+59>: xor %eax,%eax 0x00001e6d <control_acceso+61>: sete %al 0x00001e70 <control_acceso+64>: movzbl %al,%eax 0x00001e73 <control_acceso+67>: mov (%esp),%ebx 0x00001e76 <control_acceso+70>: mov 0x4(%esp),%esi 0x00001e7a <control_acceso+74>: mov 0x8(%esp),%edi 0x00001e7e <control_acceso+78>: leave Disponible en http://reverse.put.as 1 Pág 16 Ingeniería inversa en Mac OS X MacProgramadores 0x00001e7f <control_acceso+79>: End of assembler dump. ret Ahora podemos ejecutar el binario y comprobar que el programa nos permite acceder independientemente de que proporcionemos la clave correcta o no: $ ./adivina Indique clave:1234 Bienvenido $ ./adivina Indique clave:1230u Bienvenido 4. Ingeniería inversa de código Objective-C La interpretación que podemos hacer con gdb del código C es trivial porque las funciones son llamadas directamente con la instrucción call. Sin embargo, al analizar código binario nos encontramos con que los métodos llamados no aparecen, sino que aparecen llamadas a la función objc_msgSend() de la forma: 0x9857f674 <NSAppMain+709>: 0x9857f678 <NSAppMain+713>: 0x9857f67b <NSAppMain+716>: mov mov call %eax,0x4(%esp) %edx,(%esp) 0x98d47d48 <dyld_stub_objc_msgSend> Esta característica dificulta la interpretación del código desensamblado ya que para saber el método llamado debemos de llevar la cuenta de los valores que se están pasando en los parámetros de la llamada a objc_msgSend(). Como veremos el uso de determinadas herramientas junto con un depurador facilita esta interpretación. 4.1. Cómo funciona el envío de mensajes Las aplicaciones Objective-C están enlazadas con la librería libobjc.A.dylib. Dentro de esta librería se encuentran un grupo de funciones que gestionan el runtime de Objective-C. De ellas la más importante es la función: id objc_msgSend(id theReceiver, SEL theSelector,...); Esta función recibe en theReceiver el objeto o clase sobre el que ejecutar el método y en theSelector recibe una cadena que representa al método a ejecutar. Los siguientes parámetros son los argumentos del método. La función devuelve el retorno de la llamada al método. 4.2. Herramientas para analizar el binario Para los siguientes ejemplos vamos a utilizar el crackme llamado SmellsGood.app2. Si este crackme dejara de estar online podría utilizar cualquier otra aplicación Objective-C. Si ejecutamos el comando otool para obtener información sobre este programa obtenemos: $ otool -tv SmellsGood.app/Contents/MacOS/SmellsGood -[SmellsGood_AppDelegate check:]: 00002a63 pushl %ebp 00002a64 movl %esp,%ebp 00002a66 pushl %esi Disponble en http://reverse.put.as/wp-content/uploads/2010/05/SmellsGood.zip 2 Pág 17 Ingeniería inversa en Mac OS X MacProgramadores 00002a67 pushl %ebx 00002a68 subl $0x20,%esp 00002a6b movl 0x08(%ebp),%esi ····· -[SmellsGood_AppDelegate checkCode:forName:]: 00002afa pushl %ebp 00002afb movl %esp,%ebp 00002afd pushl %ebx 00002afe subl $0x14,%esp 00002b01 movl 0x10(%ebp),%ebx 00002b04 movl 0x00004008,%eax ····· Vemos que las funciones en este caso tienen un nombre con sintaxis Objective-C y compuesto por el nombre de la clase más el nombre del método. El comando class-dump nos permite obtener un fichero de cabecera con la declaración de las clases implementadas en el binario: $ class-dump SmellsGood.app/Contents/MacOS/SmellsGood > SmellsGood.h $ cat SmellsGood.h @interface SmellsGood_AppDelegate : NSObject { NSWindow *window; NSTextField *nameField; NSTextField *codeField; NSButton *checkButton; } - (void)check:(id)fp8; - (BOOL)checkCode:(id)fp8 forName:(id)fp12; - (id)generateCodeForName:(id)fp8; - (id)digest:(id)fp8; - (id)shuffle:(id)fp8; @end Existe otro comando llamado class-dump-z que proporciona opciones adicionales, como por ejemplo el generar propiedades para los métodos getter-setter de la clase o el organizar cada clase en un fichero de cabecera distinto. 4.3. Descubrir el método llamado Ya hemos explicado que los nombres de los métodos Objective-C no son fácilmente extraíbles del código desensamblado ya que todo lo que vemos son llamadas a la función objc_msgSend(). Por esta razón es importante realizar este análisis con un depurador como gdb. Vamos a empezar poniendo un breakpoint en el método -[NSApplication run]. Este método lo suelen tener todas las aplicaciones Cocoa y se encarga de crear los objetos principales de la aplicación. Observe la sintaxis que se utiliza para poner breakpoints en los métodos: $ gdb SmellsGood.app/Contents/MacOS/SmellsGood (gdb) b -[NSApplication run] Breakpoint 1 at 0x98587238 (gdb) r Breakpoint 1, 0x9857f3be in NSApplicationMain () ((gdb) disassemble Dump of assembler code for function -[NSApplication run]: ····· Pág 18 Ingeniería inversa en Mac OS X MacProgramadores 0x98587269 <-[NSApplication run]+67>: 0x9858726d <-[NSApplication run]+71>: 0x98587270 <-[NSApplication run]+74>: <dyld_stub_objc_msgSend> ····· mov mov call %eax,0x4(%esp) %edx,(%esp) 0x98d47d48 Ahora podemos ver que cuando se va a llamar a la función objc_msgSend() primero se depositan en las posiciones %esp y 0x4(%esp) de la pila sus parámetros. En caso de que el método tenga un parámetro se usa la posición 0x8(%esp) y así sucesivamente. Vamos a parar en la primera llamada a objc_msgSend() para analizar más de cerca los parámetros: (gdb) b *0x98587270 Breakpoint 2 at 0x98587270 (gdb) c Breakpoint 2, 0x98587270 in -[NSApplication run] () El primer parámetro (%esp) es el puntero theReceiver que en este caso apunta a una instancia de la clase NSApplication. Al ser un puntero tenemos que indireccionarlo: (gdb) x/2x $esp 0xbfffe930: 0x00406a10 0x98d5685e (gdb) po 0x00406a10 <NSApplication: 0x406a10> Podemos realizar ambas operaciones a la vez con la siguiente sintaxis: (gdb) po *(void**)($esp) <NSApplication: 0x406a10> La instancia a la que apunta theReceiver es una estructura cuyo primer elemento es un puntero a la clase del objeto con lo que también podemos hacer: (gdb) x/x $esp 0xbfffe930: 0x00406a10 (gdb) x/x 0x00406a10 0x406a10: 0xa0b208a0 (gdb) po 0xa0b208a0 NSApplication O bien: (gdb) x/x *(void**)($esp) 0x406a10: 0xa0b208a0 (gdb) po 0xa0b208a0 NSApplication O bien todo junto: (gdb) po *(void**)(*(void**)($esp)) NSApplication El segundo parámetro (0x4(%esp)) es el puntero theSelector y al ser una cadena de texto podemos obtener su valor con el siguiente comando: Pág 19 Ingeniería inversa en Mac OS X MacProgramadores (gdb) x/s 0x98d5685e 0x98d5685e: "finishLaunching" De nuevo podemos indireccionar el valor de la siguiente forma: (gdb) x/s *(char**)(($esp)+4) 0x98d5685e: "finishLaunching" En este caso el método finishLaunching no tiene parámetros, pero si los tuviese estarían almacenados a partir de 0x8(%esp). 4.4. Depurar un método Objective-C Ya sabemos cómo descubrir la clase y método Objective-C que se está llamando, con lo que si queremos seguir trazando la ejecución podemos fijar un breakpoint en el método y meternos dentro: (gdb) b -[NSApplication finishLaunching] Breakpoint 2 at 0x3ed469e (gdb) c Continuing. Breakpoint 2, 0x9858769e in -[NSApplication finishLaunching] () Otra diferencia a tener en cuenta cuando estamos depurando dentro de un método (y que es similar con las funciones) es que los parámetros no se acceden usando el registro esp sino el registro ebp. En concreto para acceder a al parámetro theReceiver indireccionamos el puntero almacenado en la dirección $ebp+8: (gdb) x/4x $ebp 0xbfffe9b8: 0xbfffea78 0x98587275 (gdb) po 0x002111f0 <NSApplication: 0x2111f0> 0x002111f0 0x98d5685e O abreviado: (gdb) po *(void**)($ebp+8) <NSApplication: 0x2111f0> Y para acceder al parámetro theSelector indireccionamos el puntero almacenado en la dirección $ebp+12: (gdb) x/s *(char**)($ebp+12) 0x98d5685e: "finishLaunching" Análogamente, si el método tiene parámetros, estos se encuentran a partir de la posición apuntada por $ebp+16. La razón es que el epílogo del método (al igual que en los epílogos estándar de las funciones C) crea un nuevo frame ejecutando las siguientes dos instrucciones máquina: (gdb) disassemble Dump of assembler code for function -[NSApplication finishLaunching]: 0x9858768c <-[NSApplication finishLaunching]+0>: push %ebp 0x9858768d <-[NSApplication finishLaunching]+1>: mov %esp,%ebp ····· Pág 20 Ingeniería inversa en Mac OS X MacProgramadores En este nuevo frame la dirección de memoria almancenada en $ebp contendrá el valor del registro ebp en el anterior frame y la dirección de memoria almancenada en $ebp+4 contendrá la dirección de retorno: (gdb) info frame Stack level 0, frame at 0xbfffe9c0: eip = 0x9858769e in -[NSApplication finishLaunching]; saved eip 0x98587275 called by frame at 0xbfffea80 Arglist at 0xbfffe9b8, args: Locals at 0xbfffe9b8, Previous frame's sp is 0xbfffe9c0 Saved registers: ebx at 0xbfffe9ac, ebp at 0xbfffe9b8, esi at 0xbfffe9b0, edi at 0xbfffe9b4, eip at 0xbfffe9bc (gdb) x/4x $ebp 0xbfffe9b8: 0xbfffea78 0x98587275 0x002111f0 0x98d5685e (gdb) x/i 0x98587275 0x98587275 <-[NSApplication run]+79>: call 0x98d4770c <dyld_stub__HIEnableSuddenTerminationIfRequestedByPlist> Observe que el comando info frame nos devuelve el valor del registro ebp como dirección de comienzo de las variables locales. También podemos imprimir la dirección de retorno con el comando x/i y obtendremos la siguiente instrucción a la instrucción call que nos ha llamado. 5. Bibliografía • • Charles Miller, Dino Dai Zovi, "The Mac Hacker's Handbook", Ed Willey, 2009. Angel Freire, "Diario de un programador: Reverse Engineering en Mac OS X", 2010. Pág 21