Neutron, nuestro motor de IA, obtuvo un 96.75% en el benchmark CyberGym de UC Berkeley. Más información

Seguridad

Seguridad

Bypass universal del SSL pinning ... de la teoría a una PoC funcional completa con LLDB

Este artículo trata sobre cómo eludir el SSL pinning sin necesidad de hacerlo. ¿Suena confuso? Repasaremos la teoría, construiremos una PoC completa con LLDB en Python y, por último, la ampliaremos a otras tareas interesantes.

Una de las preguntas más habituales que recibimos en Ostorlab es: ¿admiten el escaneo del backend si la aplicación usa SSL pinning? La pregunta que suele seguir es: ¿cómo eluden el SSL pinning?

Nuestra respuesta suele ser esta: sí, admitimos el escaneo de aplicaciones backend con SSL pinning activado ... Sin embargo, no eludimos el SSL pinning; no lo necesitamos. La reacción habitual es fruncir el ceño; probablemente usted lo esté haciendo ahora mismo.

Este artículo recorre la implementación técnica para eludir el SSL pinning sin tener que hacerlo, y explicaremos y mostraremos lo siguiente:

1- Por qué en Ostorlab somos GRANDES fans de los protocolos de depuración, 2- Implementar una PoC para nuestra máquina local y portarla a iOS, 3- Ampliar la misma herramienta para análisis dinámicos más avanzados.

Todo el tráfico que interceptamos se conserva, se indexa y queda accesible en el menú Analysis->API.

texto alternativo
API IDE

Bypass del SSL pinning en dispositivos sin root ni jailbreak

En Ostorlab escaneamos y analizamos aplicaciones móviles en busca de problemas de seguridad y privacidad. Utilizamos análisis dinámico, entre otras técnicas. Solo usamos dispositivos reales durante las pruebas y no empleamos dispositivos con root ni jailbreak.

Nos gusta poder comprar teléfonos de serie y añadirlos sin ninguna modificación a nuestro banco de pruebas, y aun así poder ejecutar la batería de pruebas y verificaciones que necesitamos: desde la interceptación de IPC y la supervisión del sistema de archivos hasta la interceptación de red con SSL pinning mediante implementaciones personalizadas y reforzadas.

El bypass del SSL pinning de Ostorlab consiste en que, en lugar de usar un proxy para el tráfico e intentar encontrar las API que gestionan la validación del pin SSL, nos apoyamos en la interceptación de sockets SSL a bajo nivel. Esto significa capturar el tráfico en texto claro justo antes de que se cifre.

Las API variarán según la aplicación objetivo, pero solo hay un puñado de ellas; por ejemplo, para las aplicaciones nativas, SSL_read, SSL_read_ex y SSL_write son las API que hay que buscar, y existen tanto en Android como en iOS, y tanto en OpenSSL como en BoringSSL.

Para las aplicaciones basadas en Java, las API de flujos SSL de Conscrypt son una implementación de más alto nivel y, para las aplicaciones basadas en Javascript, Network.setRequestInterception o Fetch.enable permiten la interceptación y manipulación nativa de las solicitudes.

Este enfoque tiene varias ventajas frente a los proxies, que no funcionan en estos casos:

  • Aplicaciones que no usan el proxy del sistema operativo (guiño, guiño, aplicaciones Flutter); las implementaciones públicas requieren usar enrutamiento para interceptar el tráfico.
  • Los proxies pueden romper ciertas implementaciones de SSL; por ejemplo, las conexiones SSL con protocolos esotéricos o las funciones más recientes de TLS pueden no estar presentes en los proxies, lo que provoca conexiones rotas. Es un problema habitual con las nuevas versiones de iOS.
  • Los proxies son detectables y algunas bibliotecas comprueban su presencia para evitar la interceptación. Suelen ser bibliotecas de analítica y publicidad intrusivas que incumplen la privacidad. Véase, por ejemplo, el caso reciente del SDK de publicidad Mintegral, que cometía fraude, filtraba datos y tenía varias vulnerabilidades de RCE.

Protocolos de depuración

En Ostorlab nos encanta la instrumentación basada en depuración. Estos protocolos, como Remote GDB con clientes como LLDB, JDWP o Chrome Remote Debug, son muy potentes, estables y ofrecen una solución mantenible para el análisis dinámico y la instrumentación.

Ya existen proyectos de código abierto muy maduros para la instrumentación dinámica basada en memoria, como el veterano Frida o el excelente QBDI, pero requieren más trabajo para implementar funciones potentes, como el seguimiento de la pila, la simbolización, la vigilancia de atributos y la ejecución de código nativo.

Estos protocolos también ofrecen capacidades adecuadas a cada plataforma, desde la interceptación y modificación del tráfico nativo, en el caso del Chrome Debug Protocol, por ejemplo, hasta el acceso a las cookies, la supervisión del DOM o el control de la caché. Todo ello muy útil para las pruebas de seguridad automatizadas.

La otra gran ventaja de la instrumentación basada en depuración es que admite de serie las últimas versiones, sin cambios. A menudo, los cambios en el Android Runtime, en la disposición de los objetos en memoria o en la ABI (guiño, guiño, Swift) exigen un trabajo considerable para entender la nueva implementación y añadir su compatibilidad.

Ahora bien, la instrumentación basada en depuración no es perfecta: los depuradores suelen ser mucho más lentos que la instrumentación basada en memoria, por lo que usarlos para una instrumentación muy granular y sensible al rendimiento, como el análisis a nivel de instrucción, es casi imposible en aplicaciones reales.

Para la instrumentación sensible al rendimiento, las capacidades de trazado por hardware como BTS (Branch Trace Store) o Intel PT (Process Tracing) son más adecuadas y, de nuevo, más mantenibles, pero no siempre están disponibles.

PoC de interceptación de TLS

Esta sección recorre una PoC sencilla para interceptar tráfico TLS. Ejecutaremos la PoC en nuestra máquina local y después la ampliaremos para que funcione en iOS. La implementación se basa en LLDB y la lógica de interceptación se escribirá en Python 3.

LLDB es un depurador con soporte para C, Objective-C, C++ y Swift. Se promociona como un depurador de alto rendimiento de nueva generación y reutiliza componentes del proyecto LLVM, como el analizador de expresiones de Clang, lo que permite algunas capacidades de análisis realmente potentes con extrema facilidad.

LLDB admite scripting en Python y existen libros y código abierto sobre el tema; mi recomendación personal sería Advanced Debugging & Reverse Engineering de Derek Selander. El autor mantiene un interesante conjunto de comandos de LLDB. Otro proyecto destacable es Chisel, de Facebook.

El concepto

Para interceptar el tráfico SSL, el objetivo es leer las solicitudes de SSL_write a la entrada y leer el argumento buf usando num como tamaño. Para las respuestas, interceptaremos SSL_read y SSL_read_ex a la salida y leeremos el argumento buf usando el valor de retorno como tamaño.

Esto sigue simplemente la convención de la API; otras API tendrán patrones distintos, pero las estrategias son siempre las mismas. will have different patterns, but the strategies are always the same.

Primero, la infraestructura

Aunque lo habitual es adoptar un enfoque iterativo para construir la PoC, empezando por un ejemplo muy limitado, voy a darle un poco más de chispa e intentar construir desde el principio una PoC extensible.

Para asegurarnos de que nuestro código sea fácil de extender, usaremos un patrón de registro (registry) que facilite el descubrimiento de las reglas de hook. El registro es un patrón de diseño habitual. Permite invertir el proceso de descubrimiento y obtener un código más mantenible.

A continuación se muestra una clase Registry muy simplificada que define una anotación register.

# registry.py
from collections import defaultdict


class Registry:
    registry = defaultdict(dict)

    @classmethod
    def register_ref(cls, obj, key="__name__"):
        cls.registry[cls.__name__][getattr(obj, key)] = obj
        return obj

    @classmethod
    def iteritems(cls):
        return cls.registry[cls.__name__].items()
# rule_register.py
from .registry import Registry


class DynamicRuleRegistry(Registry):
    pass


def register(f):
    """Registers a class. Can be used a decorator of Rule classes."""
    return DynamicRuleRegistry.register_ref(f(), key='__key__')

El registro lleva el control de las instancias de DynamicRule, que son clases personalizadas que implementan la lógica del hook.

# dynamic_rule.py
from collections import OrderedDict
from typing import Optional

import lldb

from . import lldb_utils


class DynamicRule:
    """Dynamic rule base implementation."""
    hook_function_name: Optional[str] = None
    hook_module: Optional[str] = None
    hook_signature: OrderedDict = None
    hook_on_entry: bool = False
    hook_on_exit: bool = False

    @property
    def __key__(self):
        return f'{self.__class__.__name__}.{self.hook_function_name}.{id(self)}'

    def on_entry(self, parameters, frame, bp_loc):
        pass

    def on_exit(self, parameters, return_value, frame, bp_loc):
        pass

LLDB ofrece capacidades de scripting con Python. Un comando de script «hello world» sería:

def __lldb_init_module(debugger, internal_dict):
    debugger.HandleCommand(
        'command script add -f myscript.handle_command hello -h "Hello World Command."')


def handle_command(debugger, command, exe_ctx, result, internal_dict):
    print('Hello LLDB')

__lldb_init_module declara un nuevo comando llamado hello que activa el script myscript y la función handle_command.

Interceptación

La instrumentación usa breakpoints sencillos y define un callback para ejecutar la lógica del hook. Se mantiene un mapa global que vincula los ids de breakpoint con las reglas en breakpoint_map para saber qué regla activar con cada breakpoint.

En este ejemplo, la regla ejecutará un método on_event de la clase DynamicRule que omití anteriormente.

# lldb_monitor.py
from rules import DynamicRuleRegistry, import_all

import_all()

breakpoint_map = {}


def __lldb_init_module(debugger, internal_dict):
    debugger.HandleCommand(
        'command script add -f lldb_monitor.handle_command monitor -h "Init monitoring hooks to trace application."')


def breakpoint_callback(frame, bp_loc, *_):
    global breakpoint_map
    breakpoint_id = bp_loc.GetBreakpoint().GetID()
    rule = breakpoint_map[breakpoint_id]
    rule.on_event(frame, bp_loc, {})
    return False


def handle_command(debugger, command, exe_ctx, result, internal_dict):
    target = debugger.GetSelectedTarget()
    for _, rule in DynamicRuleRegistry.iteritems():
        print(f'setting breakpoint at {rule.hook_function_name} module {rule.hook_module}')
        bp = target.BreakpointCreateByName(rule.hook_function_name, rule.hook_module)
        bp.SetScriptCallbackFunction('lldb_monitor.breakpoint_callback')
        breakpoint_map[bp.GetID()] = rule

El método on_event extrae los parámetros pasados siguiendo la convención de llamada del hardware. Si hook_on_exit está activado, también extrae el valor de retorno.

El callback devuelve entonces False para continuar la ejecución.

# dynamic_rule.py
    def on_event(self, frame, bp_loc, options) -> bool:
        debugger = lldb.debugger
        debugger.SetAsync(False)
        if not self.hook_on_entry and not self.hook_on_exit:
            return False

        values = lldb_utils.parameter_register_values(len(self.hook_signature), debugger, frame)

        if self.hook_on_entry:
            self.on_entry(values, frame, bp_loc)

        if self.hook_on_exit:
            thread = frame.GetThread()
            lldb_utils.stepout_of_frame(debugger, thread, frame)
            current_frame = thread.GetFrameAtIndex(0)

            return_value = lldb_utils.return_register_values(debugger, current_frame)
            self.on_exit(values, return_value, frame, bp_loc)
            lldb_utils.continue_execution(debugger)

        return False

La convención de llamada de x86_64 consiste en pasar los argumentos en los registros rdi, rsi, rdx ... y después apilarlos en la pila. Los valores de retorno se almacenan en el registro rax.

Para acceder al valor de retorno, necesitamos continuar la ejecución hasta que el frame actual haya terminado.

# lldb_utils.py
def stepout_of_frame(thread, frame):
    thread.StepOutOfFrame(frame)

Por brevedad, solo implementaremos lecturas basadas en registros; si el método tiene más argumentos, lanzará una excepción NotImplementedError.

Este enfoque no funcionará con funciones variádicas, cuyo número de parámetros no se conoce desde el principio.

# lldb_utils.py
def parameter_register_values(count_params, debugger, frame):
    arch = debugger.GetSelectedTarget().GetTriple()

    register_values = []
    if 'x86_64' in arch:
        regs = ['rdi', 'rsi', 'rdx', 'rcx', 'r8', 'r9']
        for i in range(count_params):
            reg = regs.pop(0)
            if reg is not None:
                register_values.append(read_register(reg, frame))
            else:
                raise NotImplementedError('Unsupported number of params')
    else:
        raise NotImplementedError(f'Unsupported architecture {arch}')

    return register_values


def return_register_values(debugger, frame):
    arch = debugger.GetSelectedTarget().GetTriple()
    if 'x86_64' in arch:
        return read_register('rax', frame)
    else:
        raise NotImplementedError(f'Unsupported architecture {arch}')

Para leer el valor de un registro, simplemente accedemos al atributo de registro del frame actual y convertimos el valor a int.

# lldb_utils.py
def read_register(register, frame):
    return int(frame.register[register].value, 0)

Regla de hook para TLS

Ahora que hemos definido la lógica del hook, necesitamos implementar la regla de hook para leer la solicitud y la respuesta TLS. Definimos dos reglas, SSLWrite y SSLRead.

SSLWrite implementará el método on_entry y leerá la memoria de buf, mientras que SSLRead implementará el método on_exit para usar el valor de retorno como tamaño.

# tls_rule.py
from collections import OrderedDict

import lldb

from . import dynamic_rule
from . import lldb_utils
from .rule_register import register


@register
class SSLWriteRule(dynamic_rule.DynamicRule):
    hook_function_name: str = 'SSL_write'
    hook_signature = OrderedDict(
        [('ssl', None), ('buf', None), ('num', dynamic_rule.IntFunctionParam)])
    hook_on_entry: bool = True

    def on_entry(self, parameters, frame, bp_loc):
        memo_address = parameters[1]
        size = parameters[2]
        partial_request = lldb_utils.read_memory(memo_address, size, lldb.debugger)
        extra = b'Request:\n'
        extra += partial_request or b'Empty'
        self._debug(parameters, extra=extra)


@register
class SSLReadExRule(dynamic_rule.DynamicRule):
    hook_function_name: str = 'SSL_read'
    hook_signature = OrderedDict(
        [('ssl', None), ('buf', None), ('num', dynamic_rule.IntFunctionParam)])
    hook_on_exit: bool = True

    def on_exit(self, parameters, return_value, frame, bp_loc):
        memo_address = parameters[1]
        size = return_value
        partial_response = lldb_utils.read_memory(memo_address, size, lldb.debugger)
        extra = b'Response Read:\n'
        extra += partial_response or b'Empty'
        self._debug(parameters, return_value, extra=extra)

Para leer la memoria, simplemente usamos el método process.ReadMemory.

# lldb_utils.py
def read_memory(address, size, debugger) -> Optional[bytes]:
    if size <= 0:
        return None

    target = debugger.GetSelectedTarget()
    process = target.GetProcess()

    err = lldb.SBError()
    retval = process.ReadMemory(address, size, err)

    if not err.Success():
        return None
    else:
        return retval

Una vez leídas la solicitud y la respuesta, definimos un debug para imprimir los argumentos recogidos y mostrar también el seguimiento de la pila. El seguimiento de la pila simplemente ejecuta el comando bt:

# dynamic_rule.py
    def _debug(self, parameters, return_val=None, extra: bytes=None):
        debug_message = f'CALL to {self.hook_function_name} at {self.hook_module}\n'
        debug_message += lldb_utils.command('sbt', lldb.debugger)
        debug_message += f'With parameters: f{parameters} and return value {return_val}\n'
        debug_message += extra.decode('utf-8', 'ignore') if extra is not None else None
        print(debug_message)

Ejecutémoslo en nuestra máquina sobre curl con el siguiente comando. Este comando primero importará el script lldb_monitor, llamará al comando monitor para configurar los hooks, ejecutará el programa y finalmente saldrá una vez completado.

En el caso de curl, forzaremos el uso de HTTP/1 para obtener una entrada más legible; hoy en día la mayoría de los sitios web usan HTTP/2 por defecto.

lldb -O 'command script import ./lldb_monitor.py' -o 'monitor' -o 'run' -o 'exit' -- curl --http1.1 -v https://google.com
(lldb) command script import ./lldb_monitor.py
(lldb) target create "curl"
Current executable set to 'curl' (x86_64).
(lldb) settings set -- target.run-args  "--http1.1" "https://google.com"
(lldb) monitor
setting breakpoint at SSL_write module None
setting breakpoint at SSL_read module None
(lldb) run
1 location added to breakpoint 6
1 location added to breakpoint 7
CALL to SSL_write at None
frame #0 : 0x7ffff7b91df0 libssl.so.1.1`SSL_write 
frame #1 : 0x7ffff7f70ab9 libcurl.so.4`-[ + 105
frame #2 : 0x7ffff7f26c7f libcurl.so.4`-[ + 63
frame #3 : 0x7ffff7f226dd libcurl.so.4`-[ + 141
frame #4 : 0x7ffff7f24f0b libcurl.so.4`-[ + 6299
frame #5 : 0x7ffff7f43c06 libcurl.so.4`-[ + 2758
frame #6 : 0x7ffff7f44981 libcurl.so.4`curl_multi_perform + 145
frame #7 : 0x7ffff7f3adfb libcurl.so.4`curl_easy_perform + 331
frame #8 : 0x55555556e1d0 curl`-[ + 688
frame #9 : 0x55555555f130 curl`-[ + 336
frame #10: 0x7ffff7d000b3 libc.so.6`__libc_start_main + 243
frame #11: 0x55555555f1fe curl`-[ + 46

With parameters: [93824993656336, 93824993742416, 74] and return value None
Request:
GET / HTTP/1.1
Host: google.com
User-Agent: curl/7.68.0
Accept: */*


CALL to SSL_read at None
frame #0 : 0x7ffff7f732a9 libcurl.so.4`-[ + 105
frame #1 : 0x7ffff7f26d8b libcurl.so.4`-[ + 91
frame #2 : 0x7ffff7f38fad libcurl.so.4`-[ + 1549
frame #3 : 0x7ffff7f43564 libcurl.so.4`-[ + 1060
frame #4 : 0x7ffff7f44981 libcurl.so.4`curl_multi_perform + 145
frame #5 : 0x7ffff7f3adfb libcurl.so.4`curl_easy_perform + 331
frame #6 : 0x55555556e1d0 curl`-[ + 688
frame #7 : 0x55555555f130 curl`-[ + 336
frame #8 : 0x7ffff7d000b3 libc.so.6`__libc_start_main + 243
frame #9 : 0x55555555f1fe curl`-[ + 46

With parameters: [93824993656336, 93824992736672, 102400] and return value 708
Response Read:
HTTP/1.1 301 Moved Permanently
Location: https://www.google.com/
Content-Type: text/html; charset=UTF-8
Date: Tue, 18 May 2021 10:15:26 GMT
Expires: Thu, 17 Jun 2021 10:15:26 GMT
Cache-Control: public, max-age=2592000
Server: gws
Content-Length: 220
X-XSS-Protection: 0
X-Frame-Options: SAMEORIGIN
Alt-Svc: h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"

<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="https://www.google.com/">here</A>.
</BODY></HTML>

<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="https://www.google.com/">here</A>.
</BODY></HTML>
Process 11329 exited with status = 0 (0x00000000) 

Process 11329 launched: '/usr/bin/curl' (x86_64)
(lldb) exit

texto alternativo
LLDB

Port a iOS

Ahora que tenemos nuestra PoC funcionando en nuestra máquina, es hora de portarla a iOS. Para ejecutar nuestro código en iOS, usaremos ios-deploy para habilitar la depuración remota. Esto requiere primero instalar la aplicación con el atributo get-task-allow establecido en true.

ios-deploy -m --nostart --bundle Payload/OVA.app/

Pero antes de ejecutar nuestro comando, necesitamos ampliar nuestro script para admitir la convención de llamada de ARM64:

# lldb_utils.py
def parameter_register_values(count_params, debugger, frame):
    arch = debugger.GetSelectedTarget().GetTriple()

    register_values = []
    if 'x86_64' in arch:
        regs = ['rdi', 'rsi', 'rdx', 'rcx', 'r8', 'r9']
        for i in range(count_params):
            reg = regs.pop(0)
            if reg is not None:
                register_values.append(read_register(reg, frame))
            else:
                raise NotImplementedError('Unsupported number of params')
    elif 'arm64' in arch:
        regs = ['x0', 'x1', 'x2', 'x3', 'x4', 'x5', 'x6', 'x7']
        for i in range(count_params):
            reg = regs.pop(0)
            if reg is not None:
                register_values.append(read_register(reg, frame))
            else:
                raise NotImplementedError('Unsupported number of params')
    else:
        raise NotImplementedError(f'Unsupported architecture {arch}')

    return register_values


def return_register_values(debugger, frame):
    arch = debugger.GetSelectedTarget().GetTriple()
    if 'x86_64' in arch:
        return read_register('rax', frame)
    elif 'arm64' in arch:
        return read_register('x0', frame)
    else:
        raise NotImplementedError(f'Unsupported architecture {arch}')

Eso es todo; el resto sigue siendo válido.

Ampliación al sistema de archivos

Para ampliar nuestra PoC y ver, por ejemplo, qué archivos lee la aplicación, basta con crear otra regla para supervisar la función open:

# filesystem_rule.py

@register
class OpenRule(dynamic_rule.DynamicRule):
    hook_function_name: str = 'open'
    hook_signature = OrderedDict(
        [('pathnam', None), ('flags', None), ('mode', None)])
    hook_on_entry = True

    def on_entry(self, parameters, frame, bp_loc):
        path = lldb_utils.read_string(parameters[0])
        self._debug(parameters, extra=f'Opening file {path}'.encode())

Y ejecutamos de nuevo:

(lldb) command script import ./lldb_monitor.py
(lldb) target create "curl"
Current executable set to 'curl' (x86_64).
(lldb) settings set -- target.run-args  "--http1.1" "https://google.com"
(lldb) monitor
filesystem
setting breakpoint at open module None
(lldb) run
1 location added to breakpoint 1
CALL to open at None
frame #0 : 0x7ffff7de9e50 libc.so.6`open 
frame #1 : 0x7ffff7d6c196 libc.so.6`_IO_file_open + 38
frame #2 : 0x7ffff7d6c45a libc.so.6`_IO_file_fopen@@GLIBC_2.2.5 + 506
frame #3 : 0x7ffff7d5eb0e libc.so.6`fopen@@GLIBC_2.2.5 + 126
frame #4 : 0x7ffff7d5eaa9 libc.so.6`fopen@@GLIBC_2.2.5 + 25
frame #5 : 0x7ffff793058a libcrypto.so.1.1`BIO_new_file + 26
frame #6 : 0x7ffff796213e libcrypto.so.1.1`-[ + 30
frame #7 : 0x7ffff796374d libcrypto.so.1.1`CONF_modules_load_file + 61
frame #8 : 0x7ffff7f70203 libcurl.so.4`-[ + 35
frame #9 : 0x7ffff7f3a990 libcurl.so.4`-[ + 128
frame #10: 0x55555555f0af curl`-[ + 207
frame #11: 0x7ffff7d000b3 libc.so.6`__libc_start_main + 243
frame #12: 0x55555555f1fe curl`-[ + 46

With parameters: f[93824992676992, 0, 438] and return value None
Opening file /usr/lib/ssl/openssl.cnf
CALL to open at None
frame #0 : 0x7ffff7de9e50 libc.so.6`open 
frame #1 : 0x7ffff7d6c196 libc.so.6`_IO_file_open + 38
frame #2 : 0x7ffff7d6c45a libc.so.6`_IO_file_fopen@@GLIBC_2.2.5 + 506
frame #3 : 0x7ffff7d5eb0e libc.so.6`fopen@@GLIBC_2.2.5 + 126
frame #4 : 0x7ffff7d5eaa9 libc.so.6`fopen@@GLIBC_2.2.5 + 25
frame #5 : 0x55555556f9c7 curl`-[ + 119
frame #6 : 0x55555556e042 curl`-[ + 290
frame #7 : 0x55555555f130 curl`-[ + 336
frame #8 : 0x7ffff7d000b3 libc.so.6`__libc_start_main + 243
frame #9 : 0x55555555f1fe curl`-[ + 46

With parameters: f[93824992677712, 0, 438] and return value None
Opening file /home/asm/.curlrc
CALL to open at None
frame #0 : 0x7ffff7de9e50 libc.so.6`open 
frame #1 : 0x7ffff7d6c196 libc.so.6`_IO_file_open + 38
frame #2 : 0x7ffff7d6c45a libc.so.6`_IO_file_fopen@@GLIBC_2.2.5 + 506
frame #3 : 0x7ffff7d5eb0e libc.so.6`fopen@@GLIBC_2.2.5 + 126
frame #4 : 0x7ffff7d5eaa9 libc.so.6`fopen@@GLIBC_2.2.5 + 25
frame #5 : 0x7ffff793058a libcrypto.so.1.1`BIO_new_file + 26
frame #6 : 0x7ffff796213e libcrypto.so.1.1`-[ + 30
frame #7 : 0x7ffff796374d libcrypto.so.1.1`CONF_modules_load_file + 61
frame #8 : 0x7ffff7963a10 libcrypto.so.1.1`-[ + 64
frame #9 : 0x7ffff79fa234 libcrypto.so.1.1`-[ + 20
frame #10: 0x7ffff7edd47f libpthread.so.0`__pthread_once_slow + 191
frame #11: 0x7ffff7a6578d libcrypto.so.1.1`CRYPTO_THREAD_run_once + 13
frame #12: 0x7ffff79fa8b8 libcrypto.so.1.1`OPENSSL_init_crypto + 808
frame #13: 0x7ffff7b8f575 libssl.so.1.1`OPENSSL_init_ssl + 53
frame #14: 0x7ffff7b934a2 libssl.so.1.1`SSL_CTX_new + 34
frame #15: 0x7ffff7f7368e libcurl.so.4`-[ + 414
frame #16: 0x7ffff7f7513f libcurl.so.4`-[ + 383
frame #17: 0x7ffff7f75faf libcurl.so.4`-[ + 95
frame #18: 0x7ffff7f21296 libcurl.so.4`-[ + 22
frame #19: 0x7ffff7f22d13 libcurl.so.4`-[ + 387
frame #20: 0x7ffff7f438ed libcurl.so.4`-[ + 1965
frame #21: 0x7ffff7f44981 libcurl.so.4`curl_multi_perform + 145
frame #22: 0x7ffff7f3adfb libcurl.so.4`curl_easy_perform + 331
frame #23: 0x55555556e1d0 curl`-[ + 688
frame #24: 0x55555555f130 curl`-[ + 336
frame #25: 0x7ffff7d000b3 libc.so.6`__libc_start_main + 243
frame #26: 0x55555555f1fe curl`-[ + 46

With parameters: f[93824992696608, 0, 438] and return value None
Opening file /usr/lib/ssl/openssl.cnf
CALL to open at None
frame #0 : 0x7ffff7de9e50 libc.so.6`open 
frame #1 : 0x7ffff7d6c196 libc.so.6`_IO_file_open + 38
frame #2 : 0x7ffff7d6c45a libc.so.6`_IO_file_fopen@@GLIBC_2.2.5 + 506
frame #3 : 0x7ffff7d5eb0e libc.so.6`fopen@@GLIBC_2.2.5 + 126
frame #4 : 0x7ffff7d5eaa9 libc.so.6`fopen@@GLIBC_2.2.5 + 25
frame #5 : 0x7ffff793058a libcrypto.so.1.1`BIO_new_file + 26
frame #6 : 0x7ffff7a7130c libcrypto.so.1.1`X509_load_cert_crl_file + 60
frame #7 : 0x7ffff7a7147a libcrypto.so.1.1`-[ + 74
frame #8 : 0x7ffff7a742a3 libcrypto.so.1.1`X509_STORE_load_locations + 67
frame #9 : 0x7ffff7f742b6 libcurl.so.4`-[ + 3526
frame #10: 0x7ffff7f7513f libcurl.so.4`-[ + 383
frame #11: 0x7ffff7f75faf libcurl.so.4`-[ + 95
frame #12: 0x7ffff7f21296 libcurl.so.4`-[ + 22
frame #13: 0x7ffff7f22d13 libcurl.so.4`-[ + 387
frame #14: 0x7ffff7f438ed libcurl.so.4`-[ + 1965
frame #15: 0x7ffff7f44981 libcurl.so.4`curl_multi_perform + 145
frame #16: 0x7ffff7f3adfb libcurl.so.4`curl_easy_perform + 331
frame #17: 0x55555556e1d0 curl`-[ + 688
frame #18: 0x55555555f130 curl`-[ + 336
frame #19: 0x7ffff7d000b3 libc.so.6`__libc_start_main + 243
frame #20: 0x55555555f1fe curl`-[ + 46

With parameters: f[93824992691248, 0, 438] and return value None
Opening file /etc/ssl/certs/ca-certificates.crt
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="https://www.google.com/">here</A>.
</BODY></HTML>
Process 11920 exited with status = 0 (0x00000000) 

Process 11920 launched: '/usr/bin/curl' (x86_64)
(lldb) exit

Resumen

Resumamos entonces:

  • Interceptamos tráfico, sin proxy y sin deshabilitar el SSL pinning, en un dispositivo sin jailbreak.
  • Construimos una PoC para x86_64 y para ARM64 usando LLDB y Python
  • La ampliamos para realizar otras instrumentaciones de análisis dinámico
  • Estructuramos nuestra PoC de forma extensible y mantenible usando el patrón de registro.

Espero que le haya resultado útil.