frida-analykit wireshark 流量抓包(下)- 通杀flutter和webview等常规静态链接boringssl的tls流量解密

06-02 1155阅读

前言

通杀这种绝对的词都说得出来,妥妥的标题党!!!---- 对吗?对,但也不完全对。

在我"狡辩"之前,请允许我先简单的说一下这《下篇》所要达到的目标,或许你就能够理解我所说的对与不对的矛与盾。

这一篇文章的目标定为:

1

根据`boringssl`的代码实现,通过分析`ssl_log_secret`函数的指令层**引用特征**来定位attach,以达到wireshark能够实时解密tls流量抓包。

这一目标将作为这整篇文章的核心引导,如果你看迷糊忘了正在干什么,回想起来这一目标。 ----------- (要是我把目标这一存在也忘了你该如何应对?--- 从头开始 #doge )

ssl_log_secert有什么明显的特征引用?

boringssl项目: https://boringssl.googlesource.com/boringssl

在项目中搜索ssl_log_secret关键词可以得到下面的图1结果:

frida-analykit wireshark 流量抓包(下)- 通杀flutter和webview等常规静态链接boringssl的tls流量解密

图1

函数引用文件: src/ssl/handshake.cc

1

2

3

4

5

6

7

8

bool ssl_send_finished(SSL_HANDSHAKE *hs) {

  // ...

  if (!ssl_log_secret(ssl, "CLIENT_RANDOM", session->master_key,

                      session->master_key_length)) {

    return 0;

  }

  // ...

}

看到了吗?CLIENT_RANDOM 这个常量字符串将作为我们定位ssl_log_secret的入口。

1

在得到这个结论之前其实还需要考虑前提:ssl_send_finished是否在每一次握手都会调用?结论是的。但是关于这个过程不作为本文的讨论重点,所以我就此略过,下文中若出现类似我能够想到的看似理所当然但遍地前提的时候,我会使用括号标注说明存在需要考虑的前提.

尽管不应该写,但想了又想,我还是觉得这个很有意思,所以就贴了出来:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

state_start_connect

  └─ do_start_connect(hs)

      ├─ ssl_setup_key_shares(...)

      ├─ ssl_setup_extension_permutation(...)

      ├─ ssl_encrypt_client_hello(...)    # ECH 场景

      └─ ssl_add_client_hello(hs)         # 发送 ClientHello

state_read_server_hello

  └─ do_read_server_hello(hs)            # 解析 ServerHello

state_read_server_certificate

  └─ do_read_server_certificate(hs)      # 解析 Certificate

state_verify_server_certificate

  └─ do_verify_server_certificate(hs)    # 验证证书链

state_read_server_key_exchange

  └─ do_read_server_key_exchange(hs)     # 解析 ServerKeyExchange

state_read_server_hello_done

  └─ do_read_server_hello_done(hs)       # 解析 ServerHelloDone

state_send_client_certificate

  └─ do_send_client_certificate(hs)      # 发送 Certificate(若请求)

state_send_client_key_exchange

  └─ do_send_client_key_exchange(hs)     # 发送 ClientKeyExchange

state_send_client_certificate_verify

  └─ do_send_client_certificate_verify(hs) # 发送 CertificateVerify

state_send_client_finished

  └─ do_send_client_finished(hs)         # 发送 ChangeCipherSpec + Finished

        ├─ ssl->method->add_change_cipher_spec(ssl)

        ├─ tls1_change_cipher_state(hs, evp_aead_seal)

        └─ ssl_send_finished(hs)          # ★ 生成并加入 Finished 消息

state_finish_flight

  └─ do_finish_flight(hs)

state_read_session_ticket

  └─ do_read_session_ticket(hs)

state_process_change_cipher_spec

  └─ do_process_change_cipher_spec(hs)

state_read_server_finished

  └─ do_read_server_finished(hs)

state_finish_client_handshake

  └─ do_finish_client_handshake(hs)

state_done

  └─ 握手完成,返回 SSL_do_handshake() 成功

可以从这个状态机看出,这属于握手流程的一环。

从入口 CLIENT_RANDOM 定位到目标函数(手动)

下面我会使用ida pro来对一个libssl.so样本来进行讨论说明:在手动的情况下,我们是如何定位ssl_log_secret的,然后再根据原理来实现这一流程的自动化。

1

2

3

4

5

这里的样本`libssl.so`只会分析在指令层的关联,不会依赖任何函数符号,选择这个elf单纯是因为足够简单,ida pro解析起来很快,而不是像webview的`libmonochrome_64.so`高达100多m,将耗费整个人生。

肯定的是该分析不会过分针对libssl.so的特征来说明,而是会泛化拓展到webview和flutter等静态链接了boringssl的大部分常规项目。

libssl.so的样本我会上传到资源路径`samples/`下,但是我建议使用自己测试机的libssl.so来进行分析,因为后续的实际hook偏移点是根据自己机器上的目标为准。

  1. 搜索 CLIENT_RANDOM 字符串

    依赖字符串搜索,意味着CLIENT_RANDOM必须明文存在于elf中(只要套一层编解码,那么这就是盾)

frida-analykit wireshark 流量抓包(下)- 通杀flutter和webview等常规静态链接boringssl的tls流量解密

图2

  1. Xref搜索引用该字符串的指令

frida-analykit wireshark 流量抓包(下)- 通杀flutter和webview等常规静态链接boringssl的tls流量解密

图3

  1. 该字符串作为函数的参数进行调用: 0x22efc => BL #xxxxxx,从而得到目标函数入口偏移 0x338A4

依赖BL来得到目标函数地址(只要使用blr Xn 来跳转同样是盾)

以上,就是我们手动能够得到的目标函数,简简单单的3步。

如何实现这些流程自动化?

虽说是要把流程自动化,当然也不至于把ida pro这么笨重的工具作为其中一环。所以下面的自动化流程都会使用纯frida脚本来实现,代码位于 frida-analykit

从上面再ida pro的手动3步流程中,可以提炼出核心步骤:搜特征字符串 -> 查找引用该字符串的指令 -> 分析引用指令接下来的call跳转确定函数入口

搜特征字符串

Memory.scan 是frida-gum内实现的一个高效内存扫描方法,可以通过字节完全匹配/掩码匹配/正则匹配的方式来搜索内存。(底层实现在 frida-gum查看)

CLIENT_RANDOM这样一个固定的字符串,字节完全匹配模式能做到的同时也是这几种中最高效的方式,对于ascii长达十几个不相等的字节序列,能匹配到的结果通常不会太多。(这里面还可以优化:由于字符串这种常量值通常存在于.rodata段中,所以理论上还能缩小范围来扫描。)

下面贴的是Memory.scan和其pattern的文档说明,是异步方法,与之关联的还有Memory.scanSync这个同步实现。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

declare namespace Memory {

    /**

     * Scans memory for occurences of `pattern` in the memory range given by `address` and `size`.

     *

     * @param address Starting address to scan from.

     * @param size Number of bytes to scan.

     * @param pattern Match pattern, see `MatchPattern` for details.

     * @param callbacks Object with callbacks.

     */

    function scan(

        address: NativePointerValue,

        size: number | UInt64,

        pattern: string | MatchPattern,

        callbacks: MemoryScanCallbacks,

    ): Promise;

  // ....

}

// tslint:disable:no-unnecessary-class

declare class MatchPattern {

    /**

     * Compiles a match pattern for use with e.g. `Memory.scan()`.

     *

     * @param pattern Match pattern of the form “13 37 ?? ff” to match 0x13 followed by 0x37 followed by any byte

     *                followed by 0xff, or “/Some\s*Pattern/” for matching a regular expression.

     *

     *                For more advanced matching it is also possible to specify an r2-style mask.

     *                The mask is bitwise AND-ed against both the needle and the haystack. To specify the mask append

     *                a `:` character after the needle, followed by the mask using the same syntax.

     *                For example: “13 37 13 37 : 1f ff ff f1”.

     *                For convenience it is also possible to specify nibble-level wildcards, like “?3 37 13 ?7”,

     *                which gets translated into masks behind the scenes.

     */

    constructor(pattern: string);

}

所以,我们可以写出下面这些方法来搜索特征字符串CLIENT_RANDOM:

1

2

3

4

5

// CLIENT_RAND + \00     : 字符串字节再加上一个 \00 eos终止符

const pattern = '43 4c 49 45 4e 54 5f 52 41 4e 44 4f 4d 00'

const results = Memory.scanSync(module.base, module.size, pattern)

// results: { address: NativePointer, size: number }[]  所有满足pattern的匹配结果列表

查找引用该字符串的指令

从指令的角度来考虑,想要在内存中引用一个地址,编译器可能会使用adrp+add(adrl), adr, ldr, movz/movk等其中一种方式来对目标地址进行取值。

编译器既然是人写的,那么它总是有倾向性的,它总是希望能写出更高效更省空间的指令,综合其中adrp+add或许就是最常用的引用方式(或许你会问为什么不是adr,它明明就只有一条指令只占4个字节。没错,这确实这里面中最高效的指令,但是adr只能寻址~1M的偏移,而常量所在的.rodata和指令所在的.text通常是需要跨页的,能够满足1M的偏移的情况很少的),所以下面只会讨论基于adrp+add的组合方式来讨论。其中的adr在实际应用中可以考虑在内,但是作为adrp+add更值得作为本文所关注讨论的点。

如何检索adrl指令?

frida-gum 内置了capstone反汇编引擎,使用Instruction.parse可以用来解析机器码和分析指令。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

declare class Instruction {

    /**

     * Parses the instruction at the `target` address in memory.

     *

     * Note that on 32-bit ARM this address must have its least significant bit

     * set to 0 for ARM functions, and 1 for Thumb functions. Frida takes care

     * of this detail for you if you get the address from a Frida API, for

     * example `Module.getExportByName()`.

     *

     * @param target Memory location containing instruction to parse.

     */

    static parse(

        target: NativePointerValue,

    ): Instruction | X86Instruction | ArmInstruction | Arm64Instruction | MipsInstruction;

    // ...

}

~~懂了,原来如此,通过遍历解析内存中的指令,我们不就能够定位找到引用了特征字符串的位置了吗?

--- 当然是可以的,但代价又是什么呢?#doge --- 代价就是这将花费相当长的时间进行指令的解析,虽然capstone非常高效,但是几兆的内存就足以花费几分钟来完成,这是不可以接受的。

~~我又懂了,既然前面说到的内存扫描方法非常的高效,那么我们能不能用前面扫内存的方式进行搜索呢?

--- 可以的兄弟,可以的。

怎么用内存扫描来检索指令?

指令既然存在于内存中,是0和1,那么其机器码自然会有其规范的编码方式。

我们要想在内存中扫描adrp指令,首先需要了解adrp指令的机器码每一位的作用(add指令我们这里就不贴了,因为在这里面不太需要理解add的机器码编码方式,因为既然adrp和add是成对出现组合成adrl的,那么只要能定位到adrp,后面的十几条,几十条甚至上百条指令逐条使用capstone来解析还是轻轻松松的)

  • ADRP 编码方式

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    adr/adrp Xi, #{imm}

    ------------------------------------------------------------------

    | 31  | 30 ─ 29 | 28 27 26 25 24 | 23 ──────────────── 5 | 4 ─ 0 |

    | op  |  immlo  |    1 0 0 0 0   |     immhi (19b)       |  Rd   |

    ------------------------------------------------------------------

    # op      : 0=adr; 1=adrp

    # immlo   : #{imm}立即数最低 2 位

    # 28-24   : (0b10000)固定位不变

    # immhi   : #{imm}立即数高 19 位

    # Rd      : 目的寄存器Xi

    # (immhi:immlo): 带符号的21位数

    # imm = 目标页号 – PC页号

    发现了吗,adrp指令的时候,这里面的bit31, bit28-bit24这6个位是固定的,我们是不是可以根据这个特征找到所有的疑似adrp指令?

    前面我们再搜内存的时候提到的一种匹配方式:掩码匹配模式

    • 什么是"掩码匹配"模式?

      1

      2

      3

      4

      5

      6

      7

      # adrp的固定6位设置后,得到下面这个二进制值的测试值(其十六进制是 0x90000000)

        28   24   20   16

         v    v    v    v

      1001 0000 0000 0000

      0000 0000 0000 0000

         ^    ^    ^    ^

        12    8    4    0

      1

      2

      3

      4

      5

      6

      7

      # 为了表示是哪6位,那么我们就用另一个值来表示,而这个就叫做掩码(下面的十六进制是 0x9F000000)

        28   24   20   16

         v    v    v    v

      1001 1111 0000 0000

      0000 0000 0000 0000

         ^    ^    ^    ^

        12    8    4    0

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      # 然后我们把上面两种信息值用":"组合起来,完成对要匹配的内存值的位描述:

      00 00 00 90 : 00 00 00 9F

      # (注意这里是小端表示)

      # 这样的描述,是同时包含了:1)哪些位 2)是什么值

      # 但是很奇怪,理论上第二段是表示掩码,也就是哪些位置我们是确定的,他一个字节中全都是0不就是表示了这些位的值都是不确定的吗,那不就是意味着任意值都行?

      # 既然这样,没有意义的值我们去掉,那可以简化成:

      90 : 9F

      # 顺便一提实现这匹配的原理是:测试值 & 掩码 == 内存值 & 掩码

      我们兴高采烈的把这个pattern丢给Memory.scan来搜索内存:代码一存,脚本一开,内存直接原地爆炸了。搜出来几十上百万的匹配结果。

      这个pattern只有一个字节,满足的匹配太多了,回过头再来看看adrp的机器码编码编码方式,尝试限制条件来加入更多的已知位。

      Rd:要存储页基址的寄存器,一定会用哪一个?我们不是很好预测。虽然CLIENT_RANDOM是传递给ssl_log_secret函数的第二个参数,按照aarch64架构参数传递的寄存器约定,通常会是X1。可实际上这是对于目标函数入口而言的,我们没法保证CLIENT_RANDOM是直接传递给目标函数,亦或者是先经过一层代理封装函数(这依赖于编译器的行为),再由封装函数调用到目标函数的。

      imm:立即数,是一个页码差,当确定pc所在的页是确定的时候,页码差就是确定的。利用这点,我们约束自己所在的每一页都有自己的pattern,这样,就可以得到下面的pattern掩码

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

        28   24   20   16

         v    v    v    v

      1111 1111 1111 1111

      1111 1111 1110 0000

         ^    ^    ^    ^

        12    8    4    0

      # 也就是说,只有Rd是无法确定的,掩码是:

      {测试值} : E0 FF FF FF

      # 其中的测试值我们根据页码计算出来即可

      按照上面这种分页搜索的pattern,是能最大程度的筛选出最少的结果。

      结合add筛选目标地址

      前面我们通过adrp的机器码匹配到所有满足的adrp指令,这 加载出来的地址并不是目标地址,而只是一个页基址,也就是说adrp要引用的是目标地址中所有再同一页的其他地址都是有可能的,所以我们还要结合add指令把剩下的页偏移加上再进行比对。

      基于上一步扫描出来的疑似目标地址的adrp指令,我们这个时候就可以进行下面的条件过滤:

      1. 搜索出来的指令地址必须满足4字节对齐
      2. adrp Rd, #{imm} 的下一条 add Xd, Xd, #{imm1}指令算出来的Xd目标必须等于目标地址

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      20

      21

      22

      23

      24

      25

      26

      27

      28

      29

      30

      31

      32

      33

      34

      35

      36

      37

      38

      39

      40

      41

      42

      43

      44

      45

      46

      47

      48

      49

      50

      51

      const targetAddr: NativePointer // 特征字符串的目标地址

      const MAX_GAP: number = 30 // adrp -> add 的最大间隙

      let adrpResults: NativePointer[] // 所有可能的adrp指令(经过4字节对齐过滤)

      let funcEntries: NativePointer[] = [] // 可能的目标函数入口

      for (const code of adrpResults) {

          try {

              let insn = Instruction.parse(code) as Arm64Instruction

              if (insn.mnemonic !== 'adrp') {

                  continue

              }

              const reg = insn.operands[0].value

              // 接下来解析到跳转目标

              let blInsn: Arm64Instruction | undefined

              let adrlTarget: NativePointer = ptr(insn.operands[1].value.toString())

              let foundAdd: boolean = false

              loop: for (let i = 0; i

                  insn = Instruction.parse(insn.next) as Arm64Instruction

                  const ops = insn.operands

                  switch (insn.mnemonic) {

                      case 'add':

                          if (foundAdd) {

                              continue

                          }

                          if (ops[0].value !== reg) {

                              break

                          }

                          adrlTarget = adrlTarget.add(ops[2].value.toString())

                          if(!adrlTarget.equals(targetAddr)) {

                              // 引用的目标地址不一致

                              break loop

                          }

                          foundAdd = true

                          break

                      case 'bl':

                          // 可能跳转到函数入口的call跳转

                          blInsn = insn as Arm64Instruction

                          break loop

                  }

              }

              if (blInsn) {

                  // 所有引用了CLIENT_RANDDOM字符串,的下一个函数调用

                  funcEntries.push(ptr(blInsn.operands[0].value.toString()))

              }

          } catch (e) {

          }

      }

      匹配的目标函数入口

      仔细观察上面的代码,我们在筛选出了满足目标地址的adrl地址的同时,还顺便解析了后续的bl函数调用指令来获取目标函数地址。(实践中,bl的目标并不意味着一定属于目标函数,这里面可能会先经过跳板或尾调函数后在跳转才到达的目标函数,所以需要考虑中间函数的判断处理)

      代码实现和优化

      通过上面,我们经过筛选和优化,如无意外的话,是能够得到包含了ssl_log_secret函数的目标地址了------ 吗?

      有可能,因为如果仅仅是通过上面的脚本,是有可能得出多个满足匹配要求的结果,因为不排除有多个匹配的字符串或多个引用字符串的指令。同时我们并没有针对于目标函数做什么校验,如果一定要加,根据特点我们或许是可以通过判断目标函数是否引用了strlen来计算目标函数是ssl_log_secret函数的置信度。但我不希望为了不确定的特殊场景加入过多的if else条件判断复杂化这个实现,因为能够得出少量的目标选择辅助让用户来自定义判断会更灵活,同时这也是frida-analykit的目标。

      前面的代码是试验用的原理实现雏形,并不实际应用,即便经过了优化,在实际应用中还是很慢,特别是当对webview的libmonochrome_64.so进行分析的时候,要花费将近一分钟。但是别怕,这里面还有进一步优化的空间:

      1. 我们通过了按页对应的掩码pattern来匹配机器码是能利用的最严苛的掩码模式了,但是如果按照每一页4k来算,100多m的libmonochrome_64.so,这也是超过了25000页。
      2. 每一页的扫描结果在js层进行筛选都要先经过js和c之间的上下文切换性能损耗

      上面两个点可以通过下面的实现来进行优化:

      1. 理论上adrp+add这种编码方式设计的立即数是能够寻址~±4G空间,但实际应用在一个elf中,他就不太可能占用这么大的内存,所以对于imm立即数,不管是(正值)正页差还是(负值)负页差,在区间内他就必然会存在固定的位,所以我们可以分开两个区间(正/负区间):计算每个区间固定的imm高位用来筛选,这样我们就能从n复杂度的遍历页数内存扫描缩小到遍历常量2次内存扫描。frida-analykit/script/elf/xref.ts

      2. 使用CModule嵌入c代码,对预匹配的结果用c直接进行筛选,避免经过上下文切换交由js过滤。 frida-analykit/script/cmodule/scan_adrp.ts

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      20

      21

      22

      23

      24

      25

      26

      27

      28

      29

      30

      31

      32

      33

      34

      35

      36

      37

      38

      39

      40

      41

      42

      43

      44

      45

      46

      47

      48

      49

      50

      51

      52

      53

      54

      55

      56

      57

      58

      59

      60

      61

      62

      63

      64

      65

      66

      67

      68

      69

      70

      71

      72

      73

      74

      75

      76

      77

      78

      79

      80

      81

      82

      #include

      #include

      #include

      #define PAGE_SIZE 0x1000

      #define GET_ADDR_PAGE(x) ((uintptr_t)(x) & ~(PAGE_SIZE - 1))

      #define ADRP_IMM_LEN_MASK 0x1fffff

      #define ADRP_FIXED28_24_BITSET_MASK (0b10000

      p    g_array_free(res->results, TRUE);

      }

      static gboolean on_match_fuzzy_adrp(GumAddress address, gsize size, gpointer user_data)

      {

          MemoryScanRes *scan_res = (MemoryScanRes *)user_data;

          const guintptr target_page = (guintptr)GET_ADDR_PAGE(scan_res->user_data->target_address);

          const guintptr align_offset = (guintptr)scan_res->user_data->align_offset;

          const guintptr addr_val = (guintptr)address - align_offset;

          const gpointer pc_addr = (gpointer)addr_val;

           

          // 4字节指令对齐

          if ((addr_val & (sizeof(guint32) - 1)) != 0)

              return TRUE;

           

          // 按pc页差进行匹配

          const guintptr pc_page = (guintptr)GET_ADDR_PAGE(address);

          const guint32 page_delta = (guint32)((target_page - pc_page) >> 12) & ADRP_IMM_LEN_MASK;

          const guint32 immlo = page_delta & 0b11;

          const guint32 immhi = page_delta >> 2;

          const guint32 op = 0x1;

          const guint32 adrp_sign =

              (op

      p    if (scan_res == NULL)/p p        return NULL;/p p    scan_res-results = g_array_new(FALSE, FALSE, sizeof(gpointer));/p p    const GumMemoryRange range = {base_address, size};/p p    const GumMatchPattern *pattern = gum_match_pattern_new_from_string(pattern_str);/p p    // Memory.scan的内部实现/p p    gum_memory_scan(&range, pattern, on_match_fuzzy_adrp, scan_res);/p p    return scan_res->results;

      }

      经过上面一轮优化后,100多m的libmonochrome_64.so的内存空间全范围扫描能控制在5s内,我认为这是在能容忍的范围。(优化前后有多大的差异可以尝试将AdrlXref.scanAdrl换成AdrlXref.scanAdrlSlow)

      测试

      上面叭叭叭说一大堆,已经等不及看实际的效果了

      在测试验证阶段,我们点题回到boringssl来验证效果:frida-analykit/blob/main/script/net/ssl.ts

      • 针对boringssl的三个常见的场景:okhttp/flutter/webview 写了一个测试样本app-release.apk(app样本代码)

        frida-analykit wireshark 流量抓包(下)- 通杀flutter和webview等常规静态链接boringssl的tls流量解密

        图4

        • 按照frida-analykit文档安装分析工具

        • 配置config.yml

          1

          2

          3

          4

          5

          6

          7

          8

          9

          10

          11

          12

          13

          14

          15

          16

          17

          18

          19

          # 样本app

          app: com.frida_analykit.static_linked_boringssl

          jsfile: _agent.js

          server:

            servername: /data/local/tmp/frida-server

            host: 127.0.0.1:6666

            device:

          agent:

            datadir: ./data/

            stdout: ./logs/outerr.log

            stderr: ./logs/outerr.log

          script:

            nettools:

              # sslkey写出文件目录

              ssl_log_secret: ./data/nettools/sslkey/

          • npm run watch启动脚本变动编译

          • ./ptpython_spawn.sh 启动脚本注入app

            frida-analykit wireshark 流量抓包(下)- 通杀flutter和webview等常规静态链接boringssl的tls流量解密

            图5

            • 通过下面的流程体验attach的流程

              我这里使用 REPL 来演示是为了更直观的演示注入的流程(在index.ts中写脚本找准时机注入也是没问题的)

              1

              2

              3

              4

              5

              6

              7

              8

              9

              10

              11

              12

              13

              14

              15

              16

              17

              18

              19

              20

              21

              22

              23

              24

              25

              26

              27

              28

              29

              30

              # 下面`script.jsh()`是py用来指定代理js全局变量的方法

              # py是通过JsHandle来实现持有js层中的代理变量

              =================== frida-analykit ===================

              [jsfile]    _agent.js

              [update_at] 2025-04-25 16:01:36.531202

              ======================================================

              # 获取js层的全局变量:Process

              >>> proc = script.jsh('Process')

              # 获取js层的全局变量:SSLTools

              >>> ssltools = script.jsh('SSLTools')

              # 调用 Process.findModuleByName('libflutter.so'),并把结果交由py的libssl变量代理

              >>> libflutter = proc.findModuleByName('libflutter.so')

              # 打印 libflutter 的值(需要注意的是这里面的序列化依赖frida的rpc的实现,任何不能JSON序列化的对象都会报错)

              >>> libflutter.value

              {'name': 'libflutter.so', 'base': '0x7c8e2d3000', 'size': 11325440, 'path': '/data/app/com.frida_analykit.static_linked_boringssl-m2yG6wqwVlJzh8_wwfz-9w==/base.apk!/lib/arm64-v8a/libflutter.so'}

              # attach [libssl.so]的boringssl-ssl_log_secret

              >>> ssltools.attachBoringsslKeylogFunc({ 'libname': 'libssl.so' })

              # attach [libflutter.so]的boringssl-ssl_log_secret

              >>> ssltools.attachBoringsslKeylogFunc({ 'mod': libflutter })

              # 在 attach webview之前,需要点击 来首次加载webview的elf,否则会提示找不到so

              >>> ssltools.attachBoringsslKeylogFunc({ 'libname': 'libmonochrome_64.so' })

              # 之后就可以按照《上篇》流程将生成的sslkey.log导入到wireshark抓包了

              frida-analykit wireshark 流量抓包(下)- 通杀flutter和webview等常规静态链接boringssl的tls流量解密

              图6

              大家可以将对应的so库送入ida pro反编译来验证计算出来的偏移是否属于ssl_log_secret

              • webview 流量的解密结果

                frida-analykit wireshark 流量抓包(下)- 通杀flutter和webview等常规静态链接boringssl的tls流量解密

                图7

                • flutter中http发起请求的流量解密结果

                  frida-analykit wireshark 流量抓包(下)- 通杀flutter和webview等常规静态链接boringssl的tls流量解密

                  图8

                  • java层okhttp发起请求的流量解密结果(关闭"Flutter开关"即可切换)

                    frida-analykit wireshark 流量抓包(下)- 通杀flutter和webview等常规静态链接boringssl的tls流量解密

                    图9

                    在图8和图9中的红框看到,为了区分dart和okhttp发起的请求,在headers中添加了x头标识

                    以上,完成了《下篇》的讨论。

                    结尾

                    让我们再回到前言所提到的:标题写着"通杀",是标题党吗?----- 关于这个问题的答案,我觉得可以从两个角度中讨论:

                    1. 从追求客观事实的一个“我”来看:必须是标题党,做不到无懈可击,只要有任何一个反例、有任何一个破绽就算不上"通杀"。

                    2. 从追求阅读流量的一个“我”来看:不属于了吧?不对,当然也是属于标题党啦!我无法否认我追求严谨的角度,但是只要做出一些取舍就能够实现平衡共存或许是值得的。

                    接下来

                    好了,流量抓包的下篇补完了frida-analykit分析工具的ssltools部分。下一步看情况可能会考虑尝试:

                    1. 研究讨论在逆向中我们经常需要用到的dex-dump实现:虽说常规的内存扫描已经是足够傻瓜通用了,但是其过度依赖特征导致其从根本上是无法避免漏扫问题,那么能不能从更原理层面精准Dump出dex?

                    2. 研究讨论带elf壳的主elf分析思路:在这种带壳的elf的常规分析思路是都会尝试先脱壳再分析主elf,脱壳这一步通常需要研究壳elf繁琐绕口的dlopen加载实现。但如果能够获取足够多的辅助信息,带壳分析是不是可行的呢?

                    3. ...

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

目录[+]

取消
微信二维码
微信二维码
支付宝二维码