|
|
 |
ESTUDIO
COLECTIVO DE DESPROTECCIONES
Última Actualizacion:
25/10/2001
|
 |
|
Programa |
Cracking
MultiTarea!: AZPR |
W95 / W98 / NT |
Descripción |
Aplicación de las capacidades
multisubproceso de Windows a la desprotección de software. |
Tipo |
Trial con limitaciones |
Tipo de Tutorial |
[X]Original, []Adaptación,
[]Aplicación, []Traducción |
Url |
http://www.elcom.com |
Protección |
Serial Unico por hashing |
Dificultad |
1) Principiante,
2) Amateur, 3) Aficionado, 4) Profesional, 5) Especialista |
Herramientas |
SoftIce v3.25 y UltraEdit
|
Objetivo |
Simular registro
SIN USAR CARGADORES |
Cracker |
Mr.Crimson |
Grupo |
Whiskey
Kon Tekila |
Fecha |
15 de Enero de 2000 |
INTRODUCCION |
La mayor parte de las técnicas
que se aplican usualmente a la desprotección de software
se apoya en la modificación de datos o instrucciones
en frío, es decir, alterando los ficheros de
código o de datos de la aplicación previamente
a la ejecución.
Con la aparición de los empaquetadores / encriptadores
las cosas se pusieron un poco más difíciles
aunque esta estrategia seguía siendo aplicable puesto
que alguna parte del código responsable del desempaquetado
debía estar accesible en el fichero para poder iniciar
la carga del programa (ver los ejemplos de UPX y Aspack
en "Reversing with ToPo").
Sin embargo el consumo de tiempo en trazar un packer podía
llegar a hacer esta técnica poco recomendable y comenzaron
a usarse los cargadores.
Aprovechando el "re-descubrimiento" de algunas APIs relacionadas
con el depurado de procesos fue posible escribir códigos
diminutos (distribuibles) capaces de cargar otros procesos
y detenerse justo antes de la normal ejecución, cuando
el unpacker ha terminado su trabajo y el código se
encuentra expuesto y vulnerable al parcheado en caliente.
En este tutorial se plantea una alternativa al uso de cargadores
basada en la posibilidad de apropiarnos de una tajada de
CPU para ejecutar nuestro crack al mismo tiempo que el programa
víctima esta siendo desempaquetado.
|
NO
ME GUSTAN LOS CARGADORES |
Sólo es una manía
personal y no tengo argumentos muy sólidos. Simplemente
pienso que debería ser la última alternativa.
Si un programador nos 'obsequia' con una protección
bien planteada debería responderse con un igualmente
bien planteado PATCH o KEYGEN siempre que sea posible.
Si estamos de acuerdo en que lo realmente
importante de la Ingeniería Inversa no es burlar a
un mercader de bytes sino la posibilidad de modificar
la realidad que nos rodea. También coincidiremos en
que debe hacerse siguiendo las reglas no escritas del juego...
El uso de cargadores presupone el éxito de la protección
o, peor aun, que ni siquiera hemos dedicado tiempo a estudiarla.
La técnica que contaré en este tute no es
ninguna panacea y por el momento tiene algunas limitaciones
aunque abre la via para una alternativa futura al uso de
loaders.
Ok. Hasta aquí las divagaciones.
|
AZPR,
EL ESTILO ELCOM |
Esta firma se ha hecho popular
por sus programas recuperadores de claves. Se trata de pequeñas
aplicaciones muy compactas diseñadas para atacar por
fuerza bruta ficheros protegidos con password.
A parte de la que nos ocupa para ZIPs , pueden encontrarse
versiones para Word, Excel y Access si no recuerdo mal.
En la práctica, el éxito de estas aplicaciones
se reduce a los casos de 'clave insegura' (p.ej. aquellas
con menos de 7 caracteres). En cualquier caso, están
muy optimizadas, son sencillas de usar y cumplen su función...
Estas cualidades han sido reconocidas con algún que
otro premio al mejor software.
En todas las aplicaciones de Elcom se respira el mismo
esquema de registro basado en un número de serie
fijo, igual para todos los usuarios.
Hay dos formas de atacar una comprobación de número
de serie:
- (1) Atacar la validación.
De esta forma cualquier serial podría llegar a
ser válido.
- (2) Determinar el número
de serie correcto por análisis del algoritmo de
validación.
Si asumimos que el empaquetado/encriptado es robusto (al menos
lo suficiente para intentar otras vías) podemos descartar
la primera aproximación.
Queda la segunda y más elegante. |
FORJANDO
CUCHARAS EN CASA DEL HERRERO |
Cómo funciona un sistema
de validación de numero secreto...?
Un esquema genérico podría ser el siguiente:
- El usuario introduce sus datos
y un serial supuestamente válido.
- A partir de los datos de usuario,
el código genera el serial resultante.
- Se compara el generado con el
introducido
El punto vulnerable de esta secuencia tiene lugar en el momento
en que el serial calculado se encuentra en memoria pues es
visible desde debugger (Macromedia & Rsagnt32.dll).
Para reforzar este esquema basta con encriptar el proceso
de manera que la operación de comparación pueda
tener lugar en nuestras narices sin que nos demos cuenta.
De hecho es lo más usual aunque el 'método abierto'
sigue en uso en algunas protecciones comerciales (RSA Agent).
El esquema de arriba es el proceso directo porque determina
la consistencia entre los datos de usuario y el serial propuesto.
Se dice que el algoritmo es invertible cuando puede determinarse
un serial consistente a partir de cualesquiera datos de
usuario. De hecho eso es precisamente lo que hace el departamento
comercial cuando decidimos registrarnos...
Ahora echemos un vistazo a AZPR. No hay petición
de datos. No son tomados del sistema. Solo se pide un numero
de serie que será el mismo para todos los usuarios.
El esquema difiere ligeramente del anterior:
- El usuario introduce un serial
supuestamente válido.
- Esta entrada se encripta convenientemente
- Se compara con una estructura
almacenada internamente
Esta última estructura constante es la versión
encriptada del serial correcto. Pero el cálculo NO
tiene lugar durante el proceso de validación porque
ES LA MISMA PARA TODOS. De manera que en ningún momento
tenemos acceso al serial válido aunque conocemos en
qué se transforma y conocemos el algoritmo de transformación.
Para acceder al serial desde aquí hace falta, que
el algoritmo sea invertible y no lo es. No hay ninguna necesidad
de ello...porque el serial es FIJO!.
Mucho lío? Para aclararnos, XOR es una operación
invertible porque podemos deshacer su efecto operando de
nuevo:
xor (1234h,
5678h) = 444Ch
xor (444Ch, 5678h) =
1234h
OR no es invertible porque hay varias formas de obtener
el mismo resultado y no se pueden recuperar los operandos
sin ambigüedad:
or (1234h, 0010h) = 1234h
or (1234h, 0020h) =
1234h
El desarrollo del algoritmo en AZPR es a grandes rasgos
como sigue (*):
- Se desechan las entradas de menos de 6 caracteres.
- El serial introducido es fragmentado en dos pedazos.
- Los tamaños de los trozos dependen del número
total de caracteres.
- Cada fragmento es encriptado y transformado en una
estructura de 16 bytes.
- Cada estructura es comparada con otra fija en el código.
Como se puede atacar esto...? Con fuerza bruta. No podía
ser de otra manera estando en casa del herrero. Esta gente
sabe bien lo fácil que es proteger usando passwords
si estos se eligen bien.
Asi las cosas, escribí un código de fuerza
bruta para chequear todos los posibles números de
serie dentro de un rango. El algoritmo de encriptado tiene
casi 900 instrucciones por lo que era necesario codificar
de la foma más eficiente posible para ganar velocidad
(en win32asm, por supuesto).
Qué resulto de todo ello...?
(*) Nota: Aunque en el tiempo de escribir estas notas no
me preocupe de su analisis, el algoritmo podria tratarse
de MD5 o alguna variante
|
LA
FUERZA BRUTA |
Veamos algunas cifras:
El número de combinaciones diferentes para un serial
depende el tamaño del serial y del número
de caracteres diferentes que pueden usarse según:
Número de combinaciones
= [Número de caracteres posibles]** [tamaño
del serial]
Ej.(1) Usando sólo números con un tamaño
de serial de 5 caracteres
N = 10**5 = 100.000
Ej.(2) Usando números y letras minúsculas
con un tamaño de serial de 5 caracteres
N = (10 + 26) **5 =
60 millones (approx.)
Ej.(3) Usando números, letras minúsculas
y mayúsculas con un tamaño de serial de 5
N = (10 + 26 +26 )** 5
= 916 millones (approx.)
El código de rastreo por fuerza bruta conseguía
chequear sobre 800.000 serials / segundo
corriendo en un PIII a 500Mhz. Cuanto tardaría en
chequear "TODAS" las posibilidades con minúsculas,
mayúsculas y números...?
(*)Mayúsuculas + minúsculas
+ numeros = 62 caracteres posibles
Número de dígitos
|
Tiempo
|
6 |
20 horas |
7 |
60 días |
8 |
9 años |
9 |
536 años |
10 |
33.000 años |
Eso sin incluir signos como guiones, puntos, etc.
Serviría de algo trabajar en hacer el código
más eficiente...? De nada. Aún incrementando
su velocidad en un factor 1000 tomaría 33 años
chequear todos los serial de 10 caracteres!
En versiones previas de AZPR el número de serie
constaba de 26 caracteres de los cuales los 10 primeros
se mantenían fijos e independientes de la versión.
Si suponemos que esto continúa siendo así
se reduce el cálculo al chequeo de un serial de 16
dígitos. Aún así volvemos a estar casi
como antes... :o(
Para qué seguir en esta línea si estadísticamente
no vamos a tener resultados antes del final del sistema
solar?. Dijimos que el algoritmo no era invertible pero
no nos consta que sea único. Esta claro que la clave
correcta funciona pero podrían existir otras claves
de menor tamaño que generen la misma secuencia...
El programa estuvo corriendo una semana ininterrumpidamente
sin obtener resultados en tamaños de 6-7 dígitos
con lo que decidí dar por terminada esta tentativa
:o(
|
FALSEANDO
LA CONDICION DE REGISTRO
|
. Recapitulando, no parece posible determinar el número
de serie dado que el algoritmo no es invertible. Debemos
entonces intentar simular el registro falseando la validación.
La validación que nos interesa en este caso es la
que tiene lugar al iniciarse el programa. La aparición
de la funciones 'RegOpenKey' y 'RegQueryValue' durante el
trazado anuncian la inminencia del chequeo pues son las
responsables de extraer el posible serial del registro de
Windows.
Sin demasiada complicación pronto se encuentra el
nucleo de la validación que, como puede verse es
invocado unas cuantas veces (de hecho con cada operación
del programa se re-autentifica el serial...!):
* Referenced by a CALL at Addresses:
|:004017A5 , :004017EE
, :00401868 , :00401AB6 , :004022C3
|:00402D20 , :00402D6A
, :00402DA1 , :00403484 , :00406285
|:004063C3 , :0040C4D6
, :0040C5BD , :0040C631 , :0040C65D
|:0040C818 , :0040C93D
, :0040E3C8 , :0040E962 , :0040EF5E
|:0040F0C8 , :00411018
, :00411060 , :00424967
|
:0040765D 6844000000
push 00000044
:00407662 E876A80000
call 00411EDD
:00407667 83EC40
sub esp, 00000040
:0040766A 89E0
mov eax, esp
:0040766C E800FFFFFF
call 00407571
:00407671 89E0
mov eax, esp
:00407673 E8CBFDFFFF
call 00407443 <---- eax=0
BadGuy
En otro caso OK!
:00407678 83C440
add esp, 00000040
:0040767B C3
ret
Como siempre buscamos el MINIMO cambio posible y la mejor
opción es sustituir el opcode E8 (CALL) por B8 (MOV
EAX) con lo que conseguimos cambiar en 1 solo byte a:
:00407673 B8CBFDFFFF
mov eax,FFFFDCB
Con esto se obtiene el código desbloqueado, sin
limitaciones y desaparece la opción de registro.
Hasta aqui nada interesante. La cuestión se centra
ahora en cómo sortear la 'barrera' del empaquetado
para acceder a estos datos.
|
CONTRATAR UN ASESINO
|
Como todos sabemos, Windows soporta la multitarea. Por
eso podemos ejecutar varios progamas a un tiempo. Mas aún,
un programa puede ejecutar varios subprocesos (threads)
que se ejecutan concurrentemente repartiéndose de
alguna manera el tiempo de CPU.
Lo que se pretende es:
- Cazar el EntryPoint de AZPR
- Desviarlo a una rutina que sea capaz de arrancar un
subproceso.
- Este subproceso correrá
paralelamente al desempaquetado de AZPR chequeando
si el offset :00407673h
contiene ya el valor E8h
y cambiándolo entonces por B8h.
- El subproceso muere cuando consigue su objetivo.
"Correrá paralelamente" quiere decir que una parte
del tiempo la CPU estará desempaquetando AZPR y otra
chequeando el offset indicado con un nivel de prioridad semejante
entre ambas tareas. El rasgo de interés de esta estrategia
se basa en que no importa cual sea el packer. Simplemente
hay que lanzar el 'thread' asesino desde el principio con
una misión y se desactivará una vez cumplida....!
Primero veremos cómo escribir este código
y después como insertarlo en AZPR.
|
CODIFICACION DE UN SUBPROCESO
|
La función que crea un subproceso es:
HANDLE CreateThread(
lpThreadAttributes, // Puntero a
atributos de seguridad
dwStackSize,
// Tamaño de la pila de subproceso
lpStartAddress,
// Puntero a la función subproceso
lpParameter,
// Argumento para el subproceso
dwCreationFlags,
// Flags de creación
lpThreadId
// Puntero ID del subproceso creado
);
Para esta aplicación baste decir que puede darse
a los argumentos el valor cero salvo a:
(3º) que establece cual es la rutina de subproceso,
(5º) que es NORMAL_PRIORITY_CLASS (constante con el
valor 20h)
(6º) que es una variable que contendrá el ID
del subproceso.
(ver la Win32 Programmer's Reference para más información)
Esta función devuelve un handle al subproceso que
no será utilizado de nuevo por lo que hay que cerrarlo
de inmediato con CloseHandle para evitar pérdida
de recursos. El subproceso comienza a correr desde que se
ejecuta con éxito esta función.
El código entonces debe ser algo como (el offset
base es arbitrario):
:00000073 683D614B00
push offset ThreadID
:00000078 6A20
push 00000020 <-- NORMAL_PRIORITY
:0000007A 6A00
push 00000000
:0000007C 689C614B00
push 000000CE <-- Offset Thread
:00000081 6A00
push 00000000
:00000083 6A00
push 00000000
:00000085 FFD0
call CreateThread
:00000088 681C614B00
push eax <-- Handle de
Thread
:00000093 FFD6
call CloseHandle
:00000097 E964EEFEFF
jmp EntryPoint_Original_AZPR
Thread:
----------------------------------
:000000CE 6873764000
push 00407673 <-- offset a parchear
:000000D3 FF1539614B00 call
IsBadCodePtr <-- Esta accesible?
:000000D9 85C0
test eax, eax
:000000DB 75F1
jne 000000CE
:000000DD 803D73764000E8 cmp byte ptr [00407673],
E8 <-- Es CALL?
:000000E4 75E8
jne 000000CE
:000000E6 C60573764000B8 mov byte ptr [00407673],
B8 <-- MOV! :000000ED C3
ret
Véase que el thread tiene la estructura de rutina
normal , es invocada de la misma manera y retorna con RET
también aunque ahí termina el parecido. El
uso de la función 'IsBadCodePtr' nos evita cometer
un 'page fault' al intentar leer en un área de memoria
no accesible. Si el offset está accesible se mira
el contenido y si éste es el esperado se modifica.
Eso es
|
LLAMAR FUNCIONES SIN TABLA DE IMPORTACION
|
En este punto un nuevo problema se plantea. La tabla de
funciones importadas está también empaquetada
con lo cual no podemos acceder a ella para codificar nuestras
llamadas. Será preciso determinar las direcciones
'a pelo' y para ello usamos GetModuleHandle
y GetProcAdress:
Un ejemplo:
------------------------------
kernel
db "kernel32.dll",0
Mi_Funcion db "CreateThread",0
push, offset kernel
call GetModuleHandle ---> Devuelve en EAX un handle
a kernel32.dll
push offset Mi_Funcion
push eax
call GetProcAddress ---> Devuelve en EAX la dirección
de la función
-----------------------------
Después de esto simplemente invocamos con CALL EAX
para llamar a la funcion deseada y asunto resuelto.
|
INSERCION EN SECCION SOLAPADA
|
Vamos a analizar las posibilidades de inserción
del crack en el propio AZPR.
Descartamos la adición de apéndices que cambien
el tamaño del fichero.
Podemos usar ToPo v1.2 para habilitar espacio en las secciones
existentes simplemente reciclando el zero padding. Sin embargo
hay que tener la precaución de comprobar que realmente
se trata de ceros de relleno para completar el alineamiento.
Si el desempaquetador usara ese espacio como buffer nuestro
crack podría sobreescribirse durante la operación.
Necesitamos RESERVAR unos cuantos bytes en memoria para
cargar el crack y al mismo tiempo que éste se encuentre
alojado en el mismo fichero. La solución para esto
consiste en definir una nueva sección solapada a
cualquier otra existente pero que sólo usará
el espacio libre de esta.
Confusión? Con este ejemplo seguro que quedará
claro:
Supóngase un executable con 3 secciones:
Nombre de sección |
Offset (fichero) |
VA (memoria) |
VZ (Tamaño real) |
.text |
400h |
1000h |
250h |
.data |
800h |
2000h |
100h |
.rsrc |
A00h |
3000h |
95h |
La sección ".text" se extiende en el fichero desde
el offset 400h hasta 800h pero no todo son datos o instrucciones,
sólo 250h bytes . Los 1B0h restantes son ceros introducidos
durante el enlazado para mantener la alineación.
En virtud de ese alineamiento las secciones sólo
pueden comenzar en offset múltiplos de 200h (valor
típico).
Que sucede si declaramos una nueva sección de la
siguiente manera?
Pues que podremos disponer de una copia de los 1B0h bytes
libres de ".text" pero cargados en memoria a partir de la
dirección 4000h. De esta manera se evita el peligro
de interferir con el programa en un hipotetico uso de la
misma área de datos.
Aunque no es ortodoxo, nada impide mapear una misma sección
en diferentes porciones de memoria sin mas que definir una
nuevas secciones solapadas.
Finalmente decidí añadir la sección
'.nueva' de manera que pudiera hacer uso del zero padding
de la sección '.rsrc' inmediatamente anterior. La
tabla de secciones quedó así:
El último paso consiste en redireccionar el EntryPoint
a B6000h para que el código comience arrancando el
crack
Si llegado este punto no teneis muy claro como se declaran
las cabeceras de sección en el formato PE buscad
en la red el documento "The PE File Format" by Luevelsmeyer
una pequeña obra de arte. En este mismo site podeis
encontrar tutes en español sobre el tema by Numit_or
|
CRC
|
CRC es el nombre genérico que suele dar a la técnica
de verificación de integridad de datos aunque no
se use necesariamente el algoritmo original del CRC.
Un programa que realice una comprobación de "CRC"
podrá detectar cualquier modificación de sus
bytes y dejar de funcionar o simplemente no arrancar.
AZPR hace verificaciones de este tipo en varios segmentos
del programa y dependiendo
de donde decidamos insertar el crack sera necesario falsear
la comparación de turno...
En mi caso tocó en:
:66F767 mov eax, dword ptr[ebp-10] <---valor
correcto
:66F76A cmp eax, dword ptr[ebp-c]
<---valor calculado
:66F7CD jz 66F7A8
----> OK!
Se tratará entonces de cambiar el JZ (74h) por JMP
(EBh) en :66F7CD. Y este será otro trabajo a añadir
a la misión del subproceso.
|
EL CODIGO DEL SUBPROCESO PARCHEADOR
|
Más arriba hemos visto un boceto del código
y luego comenté que antes de los CALLs había
que averiguar la direcciones usando GetProcAddress. El resultado
final es un crack de alrededor de 200 bytes que puede alojarse
sin problemas y que, con algunos retoques puede hacerse
independiente del offset y del programa host:
jmp Mi_Codigo
(saltamos sobre un bloque de bytes
que
servirá para almacenar
variables y constantes)
.....
4A5CCC --> "GetModuleHandle",0
4A5CC8 --> "GetProcAddress",0
4B6102 --> "kernel32.dll",0
4B6128 --> "IsBadCodePtr",0
4B610F --> "CreateThread",0
4B611C --> "CloseHandle",0
4B6135 --> DD ?
4B6139 --> DD ?
.....
.....
Mi_Codigo:
mov edi, dword ptr [004A5CCC]
EDI apunta a GetModuleHandle
mov esi, dword ptr [004A5CC8]
ESI apunta a GetProcAdress
push 004B6102
"kernel32.dll",0
call edi
call GetModuleHandle
mov dword ptr [004B6135], eax
Guarda handle a kernel32
push 004B6128
"IsBadCodePtr",0
push eax
handle a kernel32
call esi
call GetProcAddress
mov dword ptr [004B6139], eax
Guarda la direccion de IsBadCodePtr
push 004B610F
"CreateThread",0
push dword ptr [004B6135]
handle a kernel32
call esi
call GetProcAddress
push 004B613D
ThreadID (return)
push 00000020
NORMAL_PRIORITY
push 00000000
push 004B619C
Dirección de subproceso
push 00000000
push 00000000
call eax
call CreateThread
push eax
handle de Thread creado
push 004B611C
"CloseHandle",0
push dword ptr [004B6135]
handle a kernel32
call esi
call GetProcAddress
call eax
call CloseHandle
jmp FFFEEF00
Regresa al EntryPoint original
Thread
............................................
(Parche para CRC)
-------------------
Espera_01:
push 0066F76D
Dirección de byte a parchear
call dword ptr [004B6139]
call IsBadCodePtr (MEM accesible?)
test eax, eax
jne Espera_01
Si no, sigue intentandolo
cmp byte ptr [0066F76D], 74
Es un salto condicional?
jne Espera_01
Si no, sigue intentandolo
mov byte ptr [0066F76D], EB
Cambia por salto INCONDICIONAL
(Parche para Usuario Registrado)
-------------------
Espera_02:
push 00407673
Análogamente, con el otro parcheo
call dword ptr [004B6139]
test eax, eax
jne Espera_02
cmp byte ptr [00407673], E8
jne Espera_02
mov byte ptr [00407673], B8
ret
Si estais interesados en parchear tambien la detección
de SoftICE sólo teneis que añadir otro módulo
de parcheo, esta vez transformando el salto incondicional
a RaiseException en un ret (:00662CFB)
(Parche para detección winice)
-------------------
Espera_03:
push 00662CFB
Análogamente, con el otro parcheo
call dword ptr [004B6139]
test eax, eax
jne Espera_03
cmp byte ptr [00662CFB], E9
(Jmp RaiseException)
jne Espera_03
mov byte ptr [00662CFB], C3
(RET!)
ret
He preferido listar el código ASM en lugar del binario
para evitar hacer esto demasiado críptico. Para codificar
estas intrucciones podeis usar Winice directamente
con la opcion ASM o escribir vuestro propio programita.
|
CONCLUSIONES
|
El parche resultante funciona perfectamente desbloqueando
por completo el programa y debería funcionar igualmente
con otras aplicaciones similares.
No obstante, hay una observación que hacer. Si os
fijais, he dado por hecho que el subproceso siempre va a
llegar a tiempo a modificar los bytes necesarios. Que pasa
si el proceso principal ejecuta el chequeo de CRC antes
de que el Thread haga su papel...?
Pues que tendremos un aviso al respecto y la aplicación
se cerrará apaciblemente.
Intentándolo una segunda vez generalmente funciona
sin problemas.
He podido comprobar que esto sucede cuando se ejecuta en
CPUs muy rapidas (PIII). En máquinas más lentas
no he observado nunca este efecto, aunque podría
darse.
Hay alguna manera de regular el tráfico de Threads
para evitar este efecto...?
Por supuesto, y se basa en esas APIs que nos son tan familiares
y desconocidas al mismo tiempo ( EnterCriticalSection,
LeaveCriticalSection, etc). Sirven como semáforos
para detener un thread mientras otro trabaja.... pero eso
es harina de otro costal y quizá tema de otro tute.
NOTA: No funcionará bajo NT sin hacer algunos arreglos
no comentados aqui...
Experimentad con las ideas expuestas aquí y vereis
que hay muchas puertas esperando ser abiertas en este tercer
milenio...
Up The Hammer!
MrCrimson/ [WkT!2000]
|
[
Entrada | Documentos
Genéricos | WkT!
Web Site ] |
[
Todo el ECD | x
Tipo de Protección | x Fecha
de Publicación | x Orden
Alfabético ] |
|
(c)
Whiskey Kon Tekila [WkT!] - The Original Spanish Reversers.
Si necesitas
contactar con nosotros
, lee
esto
antes e infórmate
de cómo puedes ayudarnos |
|
|
|
|
|