Neutron, notre moteur d’IA, a obtenu un score de 96,75 % sur le benchmark CyberGym de l’UC Berkeley. En savoir plus

Sécurité

Sécurité

Contournement universel du SSL pinning ... de la théorie à une PoC complète avec LLDB

Cet article explique comment contourner le SSL pinning sans avoir besoin de le contourner. Cela semble confus ? Nous passerons en revue la théorie, construirons une PoC complète avec LLDB en Python, puis l'étendrons à d'autres tâches intéressantes.

L'une des questions que l'on nous pose le plus souvent chez Ostorlab est : prenez-vous en charge le scan du backend si l'application utilise le SSL pinning ? La relance habituelle est : comment contournez-vous le SSL pinning ?

Notre réponse ressemble généralement à ceci : oui, nous prenons en charge le scan des applications backend avec le SSL pinning activé ... En revanche, nous ne contournons pas le SSL pinning ; nous n'en avons pas besoin. La réaction habituelle est un froncement de sourcils ; vous êtes probablement en train d'en faire un en ce moment même.

Cet article présente l'implémentation technique d'un contournement du SSL pinning sans avoir à le contourner. Nous expliquerons et montrerons les points suivants :

1- Pourquoi, chez Ostorlab, nous sommes de GRANDS fans des protocoles de débogage, 2- Implémenter une PoC pour notre machine locale et la porter sur iOS, 3- Étendre le même outil à des analyses dynamiques plus avancées.

Tout le trafic que nous interceptons est conservé, indexé et devient accessible dans le menu Analysis->API.

texte alternatif
API IDE

Contournement du SSL pinning sur des appareils non rootés et non jailbreakés

Chez Ostorlab, nous scannons et analysons des applications mobiles pour détecter des problèmes de sécurité et de confidentialité. Nous utilisons l'analyse dynamique, entre autres techniques. Nous n'utilisons que de vrais appareils pendant les tests, et nous n'utilisons pas d'appareils rootés ni jailbreakés.

Nous apprécions de pouvoir acheter des téléphones standards dans le commerce, les ajouter sans modification à notre banc de test et pouvoir tout de même exécuter la batterie de tests et de vérifications dont nous avons besoin, de l'interception des IPC à la surveillance du système de fichiers, en passant par l'interception du trafic réseau avec SSL pinning grâce à des implémentations durcies sur mesure.

Le contournement du SSL pinning chez Ostorlab consiste, au lieu de passer par un proxy et de chercher les API qui gèrent la validation du pin SSL, à s'appuyer sur l'interception bas niveau des sockets SSL. Cela signifie capturer le trafic en clair juste avant qu'il ne soit chiffré.

Les API varient selon votre application cible, mais il n'y en a qu'une poignée. Par exemple, pour les applications natives, SSL_read, SSL_read_ex et SSL_write sont les API à surveiller ; elles existent sur Android et iOS, aussi bien pour OpenSSL que pour BoringSSL.

Pour les applications Java, les API de flux SSL de Conscrypt constituent une implémentation de plus haut niveau et, pour les applications Javascript, Network.setRequestInterception ou Fetch.enable permettent l'interception et la manipulation natives des requêtes.

Cette approche présente plusieurs avantages par rapport aux proxys, qui ne fonctionnent pas pour :

  • Les applications qui n'utilisent pas le proxy de l'OS (clin d'œil, les applications Flutter) ; les implémentations publiques nécessitent d'utiliser du routage pour intercepter le trafic.
  • Les proxys peuvent casser certaines implémentations SSL : par exemple, les connexions SSL avec des protocoles ésotériques ou les fonctionnalités TLS les plus récentes peuvent être absentes des proxys, ce qui provoque des connexions rompues. C'est un problème courant avec les nouvelles versions d'iOS.
  • Les proxys sont détectables, et certaines bibliothèques vérifient leur présence pour éviter l'interception. Il s'agit généralement de bibliothèques d'analytique et de publicité intrusives, non conformes aux règles de confidentialité. Voir par exemple le cas récent du SDK publicitaire Mintegral, qui pratiquait la fraude, faisait fuiter des données et présentait plusieurs vulnérabilités RCE.

Protocoles de débogage

Chez Ostorlab, nous aimons l'instrumentation basée sur le débogage. Ces protocoles, comme Remote GDB avec des clients tels que LLDB, JDWP ou Chrome Remote Debug, sont très puissants et stables, et offrent une solution maintenable pour l'analyse dynamique et l'instrumentation.

Il existe déjà des projets open source très matures pour l'instrumentation dynamique en mémoire, comme le vénérable Frida ou l'excellent QBDI, mais ils demandent davantage de travail pour implémenter des fonctionnalités puissantes, comme le suivi de pile, la symbolication, la surveillance d'attributs et l'exécution de code natif.

Ces protocoles offrent aussi des capacités adaptées à la plateforme, de l'interception et de la modification du trafic natif pour le Chrome Debug Protocol, par exemple, jusqu'à l'accès aux cookies, à la surveillance du DOM ou au contrôle du cache. Tout cela est très utile pour les tests de sécurité automatisés.

L'autre grand avantage de l'instrumentation basée sur le débogage est la prise en charge immédiate des toutes dernières versions, sans aucune modification. Les changements dans l'Android Runtime, dans la disposition mémoire des objets ou dans l'ABI (clin d'œil, Swift) exigent souvent un travail considérable pour comprendre la nouvelle implémentation et la prendre en charge.

Cela dit, l'instrumentation basée sur le débogage n'est pas toute rose : les débogueurs sont souvent nettement plus lents que l'instrumentation en mémoire. Les utiliser pour une instrumentation très fine et sensible aux performances, comme l'analyse au niveau des instructions, est donc presque impossible sur des applications réelles.

Pour l'instrumentation sensible aux performances, les capacités de traçage matériel comme BTS (Branch Trace Store) ou Intel PT (Process Tracing) sont plus adaptées et, là encore, plus maintenables, mais elles ne sont pas toujours présentes.

PoC d'interception TLS

Cette section présente pas à pas une PoC simple pour intercepter du trafic TLS. Nous exécuterons la PoC sur notre machine locale, puis nous l'étendrons pour qu'elle s'exécute sur iOS. L'implémentation repose sur LLDB, et la logique d'interception est écrite en Python 3.

LLDB est un débogueur prenant en charge C, Objective-C, C++ et Swift. Il est présenté comme un débogueur de nouvelle génération et très performant et réutilise des composants du projet LLVM, comme l'analyseur d'expressions de Clang, ce qui permet des capacités d'analyse vraiment puissantes avec une extrême facilité.

LLDB prend en charge le scripting en Python et vous trouverez des livres et du code open source sur le sujet ; ma recommandation personnelle serait Advanced Debugging & Reverse Engineering de Derek Selander. L'auteur maintient un bel ensemble de commandes LLDB. Un autre projet notable est Chisel de Facebook.

Le principe

Pour intercepter le trafic SSL, l'objectif est de lire les requêtes à l'entrée de SSL_write et de lire l'argument buf avec num comme taille. Pour les réponses, nous interceptons SSL_read et SSL_read_ex à la sortie et lisons l'argument buf avec la valeur de retour comme taille.

Cela suit simplement la convention de l'API ; d'autres API auront des schémas différents, mais les stratégies restent toujours les mêmes.

D'abord, l'organisation

Il est d'usage d'adopter une approche itérative pour construire la PoC en partant d'un exemple très limité ; je vais pourtant essayer de pimenter un peu les choses et de construire dès le départ une PoC extensible.

Pour que notre code soit facilement extensible, nous utiliserons un modèle de registre (registry pattern) afin de faciliter la découverte des règles de hook. Le registre est un patron de conception courant. Il permet d'inverser le processus de découverte, ce qui rend le code plus maintenable.

Ci-dessous, une classe Registry très simplifiée qui définit une annotation 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__')

Le registre conserve la trace des instances de DynamicRule, qui sont des classes personnalisées implémentant la logique de 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 offre des capacités de scripting avec Python. Un script de commande hello world serait :

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')

Le __lldb_init_module déclare une nouvelle commande nommée hello qui déclenche le script myscript et la fonction handle_command.

Interception

L'instrumentation utilise de simples points d'arrêt et définit un callback pour exécuter la logique de hook. Une table globale associant les identifiants de points d'arrêt aux règles est conservée dans breakpoint_map pour savoir quelle règle déclencher pour chaque point d'arrêt.

Dans cet exemple, la règle exécutera une méthode on_event de la classe DynamicRule que j'avais omise précédemment.

# 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

La méthode on_event extrait les paramètres passés en suivant la convention d'appel matérielle. Si hook_on_exit est activé, elle extrait également la valeur de retour.

Le callback renvoie ensuite False pour poursuivre l'exécution.

# 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 convention d'appel pour x86_64 consiste à passer les arguments dans les registres rdi, rsi, rdx ... puis à les empiler sur la pile. Les valeurs de retour sont stockées dans le registre rax.

Pour accéder à la valeur de retour, il faut poursuivre l'exécution jusqu'à la sortie de la frame courante.

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

Par souci de concision, nous n'implémenterons que les lectures basées sur les registres ; si la méthode a davantage d'arguments, elle lèvera une exception NotImplementedError.

Cette approche ne fonctionne pas pour les fonctions variadiques dont le nombre de paramètres n'est pas connu dès le départ.

# 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}')

Pour lire la valeur d'un registre, nous accédons simplement à l'attribut de registre de la frame courante et convertissons la valeur en entier.

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

Règle de hook TLS

Maintenant que nous avons défini la logique de hook, nous devons implémenter la règle de hook pour lire la requête et la réponse TLS. Nous définissons deux règles : SSLWrite et SSLRead.

SSLWrite implémentera la méthode on_entry et lira la mémoire de buf, tandis que SSLRead implémentera la méthode on_exit pour utiliser la valeur de retour comme taille.

# 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)

Pour lire la mémoire, nous utilisons simplement la méthode 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

Une fois la requête et la réponse lues, nous définissons un debug pour afficher les arguments collectés ainsi que la pile d'appels. La pile d'appels exécute simplement la commande 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)

Exécutons cela sur notre machine avec curl, à l'aide de la commande suivante. Cette commande importe d'abord le script lldb_monitor, appelle la commande monitor pour installer les hooks, exécute le programme et se termine une fois cette commande achevée.

Pour curl, nous forcerons l'utilisation de HTTP/1 pour obtenir une entrée plus lisible ; la plupart des sites web utilisent aujourd'hui HTTP/2 par défaut.

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

texte alternatif
LLDB

Portage sur iOS

Maintenant que notre PoC fonctionne sur notre machine, il est temps de la porter sur iOS. Pour exécuter notre code sur iOS, nous utiliserons ios-deploy afin d'activer le débogage à distance. Cela nécessite d'abord d'installer l'application avec l'attribut get-task-allow défini sur true.

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

Mais avant d'exécuter notre commande, nous devons étendre notre script pour prendre en charge la convention d'appel 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}')

Voilà, tout le reste s'applique toujours.

L'étendre au système de fichiers

Pour étendre notre PoC afin de voir, par exemple, quels fichiers sont lus par l'application, il suffit de créer une autre règle pour surveiller la fonction 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())

Et relançons :

(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

Résumé

Récapitulons :

  • Nous avons intercepté le trafic, sans proxy et sans désactiver le SSL pinning, sur un appareil non jailbreaké.
  • Nous avons construit une PoC pour x86_64 et pour ARM64 avec LLDB et Python
  • Nous l'avons étendue pour réaliser d'autres instrumentations d'analyse dynamique
  • Nous avons structuré notre PoC de manière extensible et maintenable grâce au registry pattern.

J'espère que vous avez trouvé cela utile.