当社のAIエンジンNeutronが、UCバークレーのCyberGymベンチマークで96.75%のスコアを記録しました。 詳細を見る

セキュリティ

セキュリティ

SSLピンニングの汎用的な回避:理論からLLDBによる完全に動作するPoCまで

本記事のテーマは、SSLピンニングを回避する必要なしにSSLピンニングを回避することです。分かりにくいでしょうか。理論を説明し、PythonとLLDBで完全なPoCを構築し、最後にそれを他の便利なタスクへ拡張します。

Ostorlabがよく受ける質問の一つに、アプリケーションがSSLピンニングを使用している場合でもバックエンドのスキャンに対応していますかというものがあります。 それに続く質問は、たいていSSLピンニングをどうやって回避するのですかです。

私たちの答えは、通常次のようなものです。はい、SSLピンニングが有効なバックエンドアプリケーションのスキャンに対応しています……。ただし、当社は SSLピンニングを回避しているわけではありません。その必要がないのです。たいていの反応は眉をひそめることで、おそらく今まさにあなたもそうしているでしょう。

本記事では、SSLピンニングを回避する必要なしにSSLピンニングを回避する技術的な実装を順に説明します。具体的には、次の内容を解説し、 実演します。

1- Ostorlabがデバッグプロトコルの大ファンである理由 2- ローカルマシン向けにPoCを実装し、それをiOSに移植する 3- 同じツールをより高度な動的解析向けに拡張する

傍受したトラフィックはすべて永続化されてインデックス化され、Analysis->APIメニューからアクセスできるようになります。

API IDEの画面
API IDE

root化・ジェイルブレイクされていない端末でのSSLピンニングの回避

Ostorlabでは、モバイルアプリケーションをスキャンし、セキュリティとプライバシーの問題を解析しています。さまざまな手法の一つとして動的解析も用いています。 テスト全体を通じて実機のみを使用し、root化された端末やジェイルブレイクされた端末は使用しません。

市販の端末を購入し、何も改変せずにテスト環境に追加しても、IPCの傍受、ファイルシステムの監視、さらには独自にハードニングされた実装によるSSLピンニングがかかった ネットワークの傍受に至るまで、必要な一連のテストやチェックを実行できる点を私たちは気に入っています。

OstorlabのSSLピンニング回避は、トラフィックをプロキシ経由にしてSSLのピン検証を処理するAPIを探そうとするのではなく、 低レベルのSSLソケットの傍受に頼るというものです。つまり、平文のトラフィックが暗号化される直前に それを捕捉するということです。

対象となるAPIは標的のアプリケーションによって異なりますが、その数はわずかです。たとえば ネイティブアプリケーションであれば、探すべきAPIはSSL_read、SSL_read_ex、SSL_writeで、これらはAndroidと iOSの両方、そしてOpenSSLとBoringSSLの両方に存在します。

Javaベースのアプリケーションでは、ConscryptのSSL ストリームAPIがより高レベルの実装にあたります。また、JavaScriptベースのアプリケーションでは、 Network.setRequestInterception やFetch.enable によって、ネイティブにリクエストを傍受・操作できます。

このアプローチには、プロキシに比べていくつかの利点があります。プロキシは次のようなケースでは機能しないからです。

  • OSのプロキシを使用しないアプリケーション(言うまでもなく、Flutterアプリのことです)。公開されている実装では、トラフィックを傍受するために ルーティングを使う必要があります。
  • プロキシは特定のSSL実装を壊す可能性があります。たとえば、特殊なプロトコルを使うSSL接続や最新の TLS機能がプロキシでサポートされておらず、接続が切れてしまうことがあります。これは新しいiOSのリリースでよく起きる問題です。
  • プロキシは検知可能であり、傍受を避けるためにプロキシの存在をチェックするライブラリもあります。こうしたライブラリは通常、 侵襲的でプライバシーに準拠していない分析・広告ライブラリです。たとえば、最近の Mintegral 広告SDKの事例をご覧ください。このSDKは不正行為を行い、データを漏えいさせ、複数のRCE脆弱性を抱えていました。

デバッグプロトコル

Ostorlabは、デバッグベースの計装を気に入っています。LLDBなどのクライアントで使うRemote GDB、JDWP、Chrome Remote Debugといったプロトコルは非常に強力かつ安定しており、動的解析と計装に保守しやすいソリューションを提供します。

メモリベースの動的計装については、定評のあるFridaや優れたQBDIのように、すでに非常に成熟したオープンソースプロジェクトがあります。しかし、 スタックトレース、シンボル化、属性の監視、ネイティブコードの実行といった強力な機能を実装するには、より多くの作業が必要になります。

これらのプロトコルは、プラットフォームに適した機能も提供します。たとえばChrome Debug Protocolであれば、ネイティブなトラフィックの傍受や改変から、 Cookieへのアクセス、DOMの監視、キャッシュの制御まで可能です。いずれも 自動化されたセキュリティテストに非常に役立ちます。

デバッグベースの計装のもう一つの大きな利点は、何も変更せずに最新バージョンをすぐにサポートできることです。 Android Runtime、オブジェクトのメモリレイアウト、ABI(言うまでもなく、Swiftのことです)に変更があると、新しい実装を理解して サポートを追加するために、多くの場合かなりの作業が必要になります。

とはいえ、デバッグベースの計装も良いことばかりではありません。デバッガーはメモリベースの計装よりも大幅に遅いことが多く、 そのため、命令単位の解析のように粒度が細かくパフォーマンスに敏感な計装に使うことは、実際のアプリケーションではほぼ 不可能です。

パフォーマンスに敏感な計装には、BTS (Branch Trace Store)やIntel PT (Process Tracing)といったハードウェアベースのトレース機能のほうが適しており、やはり保守もしやすいのですが、常に利用できるとは限りません。

TLS傍受のPoC

このセクションでは、TLSトラフィックを傍受するシンプルなPoCを順に説明します。PoCをまずローカルマシンで実行し、 その後iOSで実行できるよう拡張します。実装はLLDBをベースとし、傍受のロジックはPython 3で記述します。

LLDBは、C、Objective-C、C++、Swiftをサポートするデバッガーです。次世代の高性能デバッガーとして打ち出されており、 Clangの式パーサーなど、より大規模なLLVMプロジェクトのコンポーネントを再利用しています。これにより、非常に 強力な解析機能をきわめて簡単に利用できます。

LLDBはPythonによるスクリプティングをサポートしており、このテーマに関する書籍やオープンソースのコードも見つかります。個人的なおすすめは、 Derek SelanderによるAdvanced Debugging & Reverse Engineering です。著者は便利なLLDBコマンドのセットを保守しています。もう一つ 注目すべきプロジェクトとして、FacebookのChiselがあります。

コンセプト

SSLトラフィックを傍受するために、リクエストについてはSSL_writeの呼び出し時に、numをサイズとしてbuf引数を読み取ることを目指します。 レスポンスについては、SSL_readとSSL_read_exの終了時に傍受し、戻り値をサイズとしてbuf引数を読み取ります。

これは単にAPIの規約に従っているだけです。他のAPIでは パターンが異なりますが、戦略は常に同じです。

まずは土台づくりから

PoCは通常、ごく限られた例から始めて反復的に構築するのが一般的ですが、 ここでは少し趣向を凝らし、最初から拡張可能なPoCを構築してみます。

コードを容易に拡張できるようにするため、レジストリパターンを使ってフックルールを見つけやすくします。レジストリは 一般的なデザインパターンです。発見のプロセスを逆転させることができ、より保守しやすいコードになります。

以下は、registerアノテーションを定義する、非常に簡略化したRegistryクラスです。

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

このレジスターは、フックのロジックを実装するカスタムクラスであるDynamicRuleのインスタンスを管理します。

# 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は、Pythonによるスクリプティング機能を提供しています。Hello Worldのコマンドスクリプトは次のようになります。

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は、スクリプトmyscriptと関数handle_commandを呼び出すhelloという名前の新しいコマンドを宣言します。

傍受

計装ではシンプルなブレークポイントを使い、フックのロジックを実行するコールバックを定義します。各ブレークポイントでどのルールを呼び出すかを把握するため、 ブレークポイントIDとルールを対応付けるグローバルなマップをbreakpoint_mapに保持します。

この例では、ルールは、先ほど省略したDynamicRuleクラスのon_eventメソッドを実行します。

# 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

on_eventメソッドは、ハードウェアの呼び出し規約に従って、渡されたパラメーターを抽出します。hook_on_exitが設定されている場合は、 戻り値も抽出します。

その後、コールバックはFalseを返して実行を継続します。

# 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

x86_64の呼び出し規約では、引数をrdi、rsi、rdx……のレジスターで渡し、その後はスタックにプッシュします。 戻り値はraxレジスターに格納されます。

戻り値にアクセスするには、現在のフレームから抜けるまで実行を継続する必要があります。

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

簡潔にするため、レジスターからの読み取りのみを実装します。メソッドの引数がそれより多い場合は、NotImplementedError 例外を送出します。

このアプローチは、パラメーターの数が最初から分からない可変長引数関数では機能しません。

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

レジスターの値を読み取るには、現在のフレームのregister属性にアクセスし、値をintにキャストするだけです。

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

TLSのフックルール

フックのロジックを定義したので、次はTLSのリクエストとレスポンスを読み取るフックルールを実装する必要があります。 SSLWriteとSSLReadの2つのルールを定義します。

SSLWriteはon_entryメソッドを実装してbufのメモリを読み取り、SSLReadは on_exitメソッドを実装して戻り値をサイズとして使用します。

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

メモリを読み取るには、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

リクエストとレスポンスを読み取ったら、収集した引数を出力し、スタックトレースも表示するdebugを定義します。スタック トレースは、単に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)

次のコマンドで、手元のマシン上のcurlに対して実行してみましょう。このコマンドは、まずlldb_monitorスクリプトをインポートし、 monitorコマンドを呼び出してフックを設定し、プログラムを実行して、そのコマンドが完了したら終了します。

curlについては、入力を読みやすくするためにHTTP/1の使用を強制します。最近のほとんどのWebサイトは、デフォルトでHTTP/2を使用します。

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

LLDBでの実行結果
LLDB

iOSへの移植

PoCが手元のマシンで動作するようになったので、次はiOSに移植します。iOSでコードを実行するには、 ios-deployを使ってリモートデバッグを有効にします。そのためにはまず、get-task-allow 属性をtrueに設定してアプリケーションをインストールする必要があります。

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

ただし、コマンドを実行する前に、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}')

これで完了です。残りはすべてそのまま適用できます。

ファイルシステムへの拡張

たとえばアプリケーションがどのファイルを読み込んでいるかを確認するようPoCを拡張するには、 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())

そして、もう一度実行します。

(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

まとめ

ここまでの内容をまとめます。

  • ジェイルブレイクされていない端末上で、プロキシを使わず、SSLピンニングを無効化することもなく、トラフィックを傍受しました。
  • LLDBとPythonを使って、x86_64とARM64向けのPoCを構築しました
  • それを拡張し、他の動的解析の計装も行えるようにしました
  • レジストリパターンを使い、拡張可能で保守しやすい形でPoCを構成しました。

本記事がお役に立てば幸いです。