我们的 AI 引擎 Neutron 在加州大学伯克利分校的 CyberGym 基准测试中取得了 96.75% 的成绩。 了解更多

安全

安全

SSL Pinning 通用绕过……从原理到基于 LLDB 的完整可用 PoC

本文讨论如何在无需真正绕过的情况下“绕过”SSL 证书锁定。听起来有些费解?我们将讲解其原理,用 Python 基于 LLDB 构建完整的 PoC,最后将其扩展到其他有趣的任务。

在 Ostorlab,我们常被问到的一个问题是:如果应用使用了 SSL 证书锁定,你们是否支持后端扫描? 随之而来的通常是:你们是如何绕过 SSL 证书锁定的?

我们的回答通常是这样的:是的,我们支持对启用了 SSL 证书锁定的后端应用进行扫描……不过,我们并不绕过 SSL 证书锁定;我们不需要这样做。通常的反应是皱眉;您此刻很可能也正皱着眉头。

本文将逐步介绍如何在无需绕过 SSL 证书锁定的情况下实现“绕过”的技术细节,我们将解释并展示以下内容:

1- 为什么 Ostorlab 是调试协议的忠实拥趸, 2- 在本地机器上实现一个 PoC,并将其移植到 iOS, 3- 我们将扩展同一工具,用于更高级的动态分析。

我们拦截的所有流量都会被持久化并建立索引,可在 Analysis->API 菜单中访问。

替代文本
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 可实现原生的请求拦截与篡改。

与代理相比,这种方法有多项优势,因为代理在以下情况下无法奏效:

  • 不使用操作系统代理的应用(没错,说的就是 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 的调试器。它被宣传为下一代高性能调试器,并复用了更大的 LLVM 项目中的组件,例如 Clang 表达式解析器,这使得一些非常强大的分析能力能够极其轻松地实现。

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。

为了确保代码易于扩展,我们将使用注册表模式来简化 Hook 规则的发现。注册表是一种常见的设计模式。它可以反转发现过程,使代码更易于维护。

下面是一个高度简化的 Registry 类,它定义了一个 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__')

注册表会记录 DynamicRule 实例,它们是用于实现 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 提供了基于 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 声明了一个名为 hello 的新命令,该命令会触发脚本 myscript 及其函数 handle_command。

拦截

插桩使用简单的断点,并定义一个回调来执行 Hook 逻辑。我们在 breakpoint_map 中维护一个将断点 ID 映射到规则的全局字典,以便知道每个断点应触发哪条规则。

在本例中,规则将执行 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 Hook 规则

既然已经定义了 Hook 逻辑,接下来需要实现用于读取 TLS 请求和响应的 Hook 规则。我们定义两条规则:SSLWrite 和 SSLRead。

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 命令设置 Hook,运行程序,最后在该命令完成后退出。

对于 curl,我们将强制使用 HTTP/1 以获得更易读的输入,因为如今大多数网站默认使用 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

移植到 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。

希望本文对您有所帮助。