- MacProgramadores

Anuncio
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:
[email protected]
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 - [email protected]
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
Descargar