逆向 Flutter 应用

Python实战社群

Java实战社群

长按识别下方二维码,按需求添加

扫码关注添加客服

进Python社群▲

扫码关注添加客服

进Java社群

作者 | Andre Lipke 
来源 | PIXELTOAST Blog

来源公众号丨知识小集(zsxjtip)

https://blog.tst.sh/reverse-engineering-flutter-apps-part-1/

在开始之前,我先介绍Flutter堆栈的一些背景知识及其工作原理。

您可能已经知道:Flutter是从头开始构建的,有自己的渲染管线和 widget 库,从而做到了真正的跨平台,并保证了设计的一致性,无论在什么设备上运行,体验都是一样的。

与大多数平台不同,flutter 框架中的所有渲染相关的组件(包括动画,布局和绘画)都包含在 package:flutter 中。

您可以在官方的架构图中看到这些组件:

从逆向工程的角度来看,最有趣的部分是 Dart 层,因为这是应用程序所有业务逻辑所在的位置。

那 Dart 层是什么样的呢?

Flutter 将 Dart 编译为本机汇编代码,而使用的格式尚未公开,更不用说完全反编译和重新编译了。

相比较而言,React Native 使用的是容易检查和修改的 Javascript,而 Android 使用的 Java 有详细的字节码说明,并且有许多免费的反编译器。

尽管没有混淆(默认情况下)或加密,但是 Flutter 应用程序目前仍然很难逆向,因为需要深入了解 Dart 内部知识才能了解到皮毛。

从安全的角度来看,这让 Flutter 变得非常出色,几乎可以防止别人窥探你的代码。

接下来,我将向您展示 Flutter 应用程序的构建过程,并详细说明如何对它产生的代码进行逆向工程。

快照

Dart SDK 具有高度的通用性,您可以在许多不同的平台上以不同的配置嵌入 Dart 代码。

运行Dart的最简单方法是使用 dart 可执行文件,该可执行文件可以像读取脚本语言一样直接读取 dart 源文件。它包括我们称为前端的主要组件(解析 Dart 代码),运行时(提供在其中运行代码的环境)以及 JIT 编译器。

您还可以使用 dart 创建和执行快照,这是 Dart 的预编译形式,通常用于加速常用的命令行工具(如 pub)。

ping@debian:~/Desktop$ time dart hello.dart
Hello, World!

real    0m0.656s
user    0m0.920s
sys     0m0.084s

ping@debian:~/Desktop$ dart --snapshot=hello.snapshot hello.dart
ping@debian:~/Desktop$ time dart hello.snapshot
Hello, World!

real    0m0.105s
user    0m0.208s
sys     0m0.016s

如您所见,使用快照时,启动时间大大缩短。

默认的快照格式是 kernel,它是等效于 AST 的 Dart 代码的中间表示形式。

在调试模式下运行Flutter应用程序时,Flutter工具会创建 kernal 快照,并使用 调试运行时+JIT 在您的android应用程序中运行该快照。这让你能够在运行时使用热重载实时调试应用程序和修改代码。

不幸的是,由于对RCE的关注日益增加,在移动行业中,使用自己的JIT编译器已不受欢迎。iOS实际上阻止你执行像这样的动态生成的代码。

但是,还有两种快照类型,即 app-jit 和 app-aot ,它们包含编译后的机器代码,这些代码可以比 kernel 快照更快地初始化,但它们不是跨平台的。

快照的最终类型为 app-aot ,仅包含机器代码,且没有内核。这些快照是使用 flutter/bin/cache/artifacts/engine/<arch>/<target>/ 中的 gen_snapshots 工具生成的,稍后会对此进行更多介绍。

但是,它们不仅仅是 Dart 代码的编译版本,实际上,它们是在调用main之前VM堆栈的完整“快照”。这是Dart的一项独特功能,也是与其他运行时相比,其初始化速度如此之快的原因之一。

Flutter 使用这些AOT快照构建发布版本,您可以在文件树中查看包含它们的文件,该文件树包含使用 flutter build apk 构建的 Android APK

ping@debian:~/Desktop/app/lib$ tree .
.
├── arm64-v8a
│   ├── libapp.so
│   └── libflutter.so
└── armeabi-v7a
    ├── libapp.so
    └── libflutter.so

在这里,您可以看到两个libapp.so文件,它们分别是作为 ELF 二进制文件的 a64 和 a32 快照。

gen_snapshots在此处输出ELF/共享对象可能会引起误解,它不会将 dart 方法公开为可以在外部调用的符号。相反,这些文件是“cluster 化快照”格式的容器,但在单独的可执行部分中包含编译的代码,以下是它们的结构:

ping@debian:~/Desktop/app/lib/arm64-v8a$ aarch64-linux-gnu-objdump -T libapp.so

libapp.so:     file format elf64-littleaarch64

DYNAMIC SYMBOL TABLE:
0000000000001000 g    DF .text  0000000000004ba0 _kDartVmSnapshotInstructions
0000000000006000 g    DF .text  00000000002d0de0 _kDartIsolateSnapshotInstructions
00000000002d7000 g    DO .rodata        0000000000007f10 _kDartVmSnapshotData
00000000002df000 g    DO .rodata        000000000021ad10 _kDartIsolateSnapshotData

AOT快照采用共享对象形式而不是常规快照文件的原因是因为 gen_snapshots 生成的机器代码需要在应用程序启动时加载到可执行内存中,而最好的方法是通过ELF文件。

使用此共享对象,链接器会将 .text 部分中的所有内容加载到可执行内存中,从而允许 Dart 运行时随时调用它。

您可能已经注意到有两个快照:VM 快照和 Isolate 快照。

DartVM 有一个执行后台任务的 isolate,称为 vm isolate,它是 app-aot 快照所必需的,因为运行时无法像dart可执行文件那样动态加载它。

Dart SDK

幸运的是,Dart是完全开源的,因此在对快照格式进行逆向工程时,我们不是两眼摸黑。

在创建用于生成和分解快照的测试平台之前,您必须设置Dart SDK,这里有有关如何构建它的文档:https://github.com/dart-lang/sdk/wiki/Building。

您想生成通常由flutter工具编排的 libapp.so 文件,但是似乎没有任何有关如何执行此操作的文档。

flutter sdk 附带了 gen_snapshot 的二进制文件,该文件不属于构建 dart 时通常使用的标准 create_sdk 构建目标。

尽管 gen_snapshot 确实是作为SDK中的一个单独目标存在,但是你可以使用以下命令为构建 arm  版本的 gen_snapshot :

./tools/build.py -m product -a simarm gen_snapshot

通常,您只能根据架构来生成快照,以解决它们已经创建了模拟目标的情况,该模拟目标可模拟目标平台的快照生成。这里有一些限制,例如无法在 32 位系统上制作 aarch64 或 x86_64 快照。

在制作共享库之前,您必须使用前端编译一个 dill 文件:

~/flutter/bin/cache/dart-sdk/bin/dart ~/flutter/bin/cache/artifacts/engine/linux-x64/frontend_server.dart.snapshot --sdk-root ~/flutter/bin/cache/artifacts/engine/common/flutter_patched_sdk_product/ --strong --target=flutter --aot --tfa -Ddart.vm.product=true --packages .packages --output-dill app.dill package:foo/main.dart

Dill文件实际上与 kernel 快照的格式相同,其格式可以参考:https://github.com/dart-lang/sdk/blob/master/pkg/kernel/binary.md

这是用作 gen_snapshot 和 analyzer 之类的工具之间的 Dart 代码的通用表示形式的格式。

有了 app.dill ,我们最终可以使用以下命令生成 libapp.so :

gen_snapshot --causal_async_stacks --deterministic --snapshot_kind=app-aot-elf --elf=libapp.so --strip app.dill

一旦能够手动生成libapp.so,就可以轻松修改SDK,以打印出对 AOT 快照格式进行逆向工程所需的所有调试信息。

附带说明一下,Dart 实际上是由创建 JavaScript 的 V8 的一些人设计的,V8 可以说是有史以来最先进的解释器。DartVM 的设计令人难以置信,我认为人们没有给予 DartVM 创造者足够的荣誉。

快照剖析

AOT快照本身非常复杂,目前是没有详细文档的自定义二进制格式。你可能会被迫在调试器中手动完成序列化过程,以实现可以读取其格式的工具。

与快照生成相关的源文件可以在这里找到:

  • Cluster serialization / deserialization

vm/clustered_snapshot.h
vm/clustered_snapshot.cc
  • ROData serialization

vm/image_snapshot.h
vm/image_snapshot.cc
  • ReadStream / WriteStream

vm/datastream.h
  • Object definitions

vm/object.h
  • ClassId enum

vm/class_id.h

我花了大约两周的时间来实现一个命令行工具,该工具能够解析快照,使我们能够完全访问已编译应用程序的堆。

我们来快速浏览一下快照数据的布局:

Isolate 中的每个 RawObject * 根据其 class ID 由相应的 SerializationCluster 实例进行序列化。这些对象可以包含代码,实例,类型,原语,闭包,常量等任何内容。稍后将进一步介绍。

对 VM isolate 快照进行反序列化之后,其堆中的每个对象都会添加到 isolate 快照对象池中,从而可以在同一上下文中对其进行引用。

Cluster 分三个阶段进行序列化:跟踪,分配和填充。

在跟踪阶段,根对象与它们在广度优先搜索中引用的对象一起添加到队列中。同时,创建与每个类类型相对应的SerializationCluster实例。

根对象是虚拟机在 isolate 的ObjectStore中使用的一组静态对象,稍后我们将使用这些 ObjectStore 来查找库和类。VM快照包括StubCode基础对象,这些对象在所有 isolate 之间共享。

stub 基本上是 Dart 代码所调用的汇编的手写部分,从而使其可以与运行时安全地通信。

跟踪之后,将写入 cluster 信息,其中包含有关 cluster 的基本信息,最重要的是要分配的对象数。

在分配阶段,将调用每个 cluster 的 WriteAlloc 方法,该方法将写入分配原始对象所需的任何信息。在大多数情况下,此方法所做的全部工作就是写入 class ID 和属于该 cluster 的对象的数量。

属于每个 cluster 的对象还按分配顺序分配了一个递增的对象ID,稍后在填充阶段解析对象引用时会用到这个 ID。

您可能已经注意到还缺少任何关于索引和 cluster 大小的信息,必须完全读取整个快照才能从中获取任何有意义的数据。因此,要真正进行任何逆向工程,您必须为31种以上的 cluster 类型实现反序列化例程(我已经完成),或者通过将其加载到经过修改的运行时中来提取信息(这很难做到跨体系结构)。

以下是一个数组 [123,42] 的 cluster 结构的简化示例:

如果一个对象引用了另一个对象(如数组元素),则序列化器将在分配阶段写入最初分配的对象ID,如上所示。

对于像 Mints 和 Smis 这样的简单对象,它们完全是在分配阶段构造的,因为它们没有引用任何其他对象。

之后,将编写大约 107 个根引用,包括核心类型,库,类,缓存,静态异常和其他几个其他对象的对象ID。

最后,写入 ROData 对象,将其直接映射到内存中的 RawObject * ,以避免额外的反序列化步骤。

ROData最重要的类型是 RawOneByteString ,它用于库/类/函数名。 ROData 也通过偏移量引用,偏移量是快照数据中唯一可选解码的位置。

与ROData相似, RawInstruction 对象是直接指向快照数据的指针,但存储在可执行指令符号中,而不是主快照数据中。

以下是序列化 cluster 的转储,通常在编译应用程序时编写:

idx | cid | ClassId enum        | Cluster name
----|-----|---------------------|----------------------------------------
  0 |   5 | Class               | ClassSerializationCluster
  1 |   6 | PatchClass          | PatchClassSerializationCluster
  2 |   7 | Function            | FunctionSerializationCluster
  3 |   8 | ClosureData         | ClosureDataSerializationCluster
  4 |   9 | SignatureData       | SignatureDataSerializationCluster
  5 |  12 | Field               | FieldSerializationCluster
  6 |  13 | Script              | ScriptSerializationCluster
  7 |  14 | Library             | LibrarySerializationCluster
  8 |  17 | Code                | CodeSerializationCluster
  9 |  20 | ObjectPool          | ObjectPoolSerializationCluster
 10 |  21 | PcDescriptors       | RODataSerializationCluster
 11 |  22 | CodeSourceMap       | RODataSerializationCluster
 12 |  23 | StackMap            | RODataSerializationCluster
 13 |  25 | ExceptionHandlers   | ExceptionHandlersSerializationCluster
 14 |  29 | UnlinkedCall        | UnlinkedCallSerializationCluster
 15 |  31 | MegamorphicCache    | MegamorphicCacheSerializationCluster
 16 |  32 | SubtypeTestCache    | SubtypeTestCacheSerializationCluster
 17 |  36 | UnhandledException  | UnhandledExceptionSerializationCluster
 18 |  40 | TypeArguments       | TypeArgumentsSerializationCluster
 19 |  42 | Type                | TypeSerializationCluster
 20 |  43 | TypeRef             | TypeRefSerializationCluster
 21 |  44 | TypeParameter       | TypeParameterSerializationCluster
 22 |  45 | Closure             | ClosureSerializationCluster
 23 |  49 | Mint                | MintSerializationCluster
 24 |  50 | Double              | DoubleSerializationCluster
 25 |  52 | GrowableObjectArray | GrowableObjectArraySerializationCluster
 26 |  65 | StackTrace          | StackTraceSerializationCluster
 27 |  72 | Array               | ArraySerializationCluster
 28 |  73 | ImmutableArray      | ArraySerializationCluster
 29 |  75 | OneByteString       | RODataSerializationCluster
 30 |  95 | TypedDataInt8Array  | TypedDataSerializationCluster
 31 | 143 | <instance>          | InstanceSerializationCluster
...
 54 | 463 | <instance>          | InstanceSerializationCluster

快照中可能还有其他几个 cluster ,但是到目前为止,这是我在Flutter应用程序中唯一看到的 cluster。

在DartVM中,在 ClassId 枚举中定义了一组静态的预定义类 ID,确切地说,从Dart 2.4.0 起 有 142 个ID。之外的ID(或没有关联的 cluster)用单独的 InstanceSerializationClusters 编写。

最后,将解析器组合在一起,我可以从根对象表中的库列表开始,从头开始查看快照的结构。

使用对象树(这里是找到顶级函数的方法),在这种情况下是 packge:ftest/main.dart :

如上所示,在release快照中包含库,类和函数的名称。

Dart在不混淆堆栈跟踪的情况下无法真正删除它们,请参见:https://github.com/flutter/flutter/wiki/Obfuscating-Dart-Code

混淆可能不值得去花大力气,但是这很可能会在将来有所改变,并且变得更加简单,类似于Android上的proguard或web 中的 sourcemap。

实际的机器代码存储在 Code 对象指向的 Instructions 对象中,到指令数据的开头有一定的偏移量。

RawObject

DartVM中的所有托管对象都称为RawObjects,按照真正的DartVM方式,这些类都在一个有 3000 行代码的文件 vm/raw_object.h 中定义。

在生成的代码中,您可以访问 RawObject * 并在其中切换,GC似乎能够仅通过被动扫描来跟踪引用。

这是类树:

RawInstances 是在 Dart 代码中传递并调用方法的传统对象,它们在 dart 都有等效的类型。但是,非实例对象是内部的,仅存在于利用引用跟踪和垃圾回收的情况下,它们没有等效的dart类型。

每个对象都以包含以下标记的uint32_t开头:

此处的 class ID 与 cluster 序列化之前的 class ID 相同,它们在 vm/class_id.h 中定义,但也包括从 kNumPredefinedCids 开始的用户定义。

Size和GC数据标签用于垃圾回收,大多数时候可以忽略它们。

如果设置了规范位,则意味着该对象是唯一的,并且没有其他对象等于它,例如 Symbol 和 Type。

对象非常轻,RawInstance 的大小通常只有 4 个字节,令人惊讶的是它们根本都不使用虚拟方法。

所有这些意味着分配一个对象并填充其字段的成本非常低,这在Flutter中可以做很多。

Hello World!

不错吧,我们可以按名称查找函数,但是如何确定它们的实际作用呢?

正如预期的那样,从这里开始进行逆向工程要困难一些,因为我们正在挖掘包含在 Instructions 对象中的汇编代码。

Dart实际上没有使用clang等现代的编译器后端,而是使用JIT编译器来生成代码,同时有一些针对AOT的优化。

如果您从未使用过JIT代码,那么与等效的C代码相比,它在某些地方会有点臃肿。并不是说 Dart 做得不好,而是它的设计目的是在运行时快速生成,并且针对常见指令的手写汇编在性能方面常常胜过 clang/gcc 。

经过微优化的生成代码实际上发挥了巨大作用,因为它更类似于用于生成代码的更高级别的IR。

大多数相关的代码生成可以在以下位置找到:

  • vm/compiler/backend/il_<arch>.cc 

  • vm/compiler/assembler/assembler_<arch>.cc 

  • vm/compiler/asm_intrinsifier_<arch>.cc 

  • vm/compiler/graph_intrinsifier_<arch>.cc 

这是dart A64汇编程序的寄存器布局和调用约定:

       r0 |     | Returns
r0  -  r7 |     | Arguments
r0  - r14 |     | General purpose
      r15 | sp  | Dart stack pointer
      r16 | ip0 | Scratch register
      r17 | ip1 | Scratch register
      r18 |     | Platform register
r19 - r25 |     | General purpose
r19 - r28 |     | Callee saved registers
      r26 | thr | Current thread
      r27 | pp  | Object pool
      r28 | brm | Barrier mask
      r29 | fp  | Frame pointer
      r30 | lr  | Link register
      r31 | zr  | Zero / CSP

这个ABI遵循标准AArch64调用约定,但带有一些全局寄存器:

  • R26/THR :指向正在运行的vm线程的指针,请参阅 vm/thread.h 

  • R27/PP :指向当前上下文的 ObjectPool 的指针,请参阅 vm/object.h 

  • R28/BRM :barrier mask,用于增量垃圾收集

同样,这是A32的寄存器布局:

r0 -  r1 |     | Returns
r0 -  r9 |     | General purpose
r4 - r10 |     | Callee saved registers
      r5 | pp  | Object pool
     r10 | thr | Current thread
     r11 | fp  | Frame pointer
     r12 | ip  | Scratch register
     r13 | sp  | Stack pointer
     r14 | lr  | Link register
     r15 | pc  | Program counter

尽管A64是更常见,但由于A32更易于阅读和反汇编,因此我将主要介绍A32。

您可以通过将 --disassemble-optimized 传递给 gen_snapshot 来查看IR和反汇编,但请注意,这仅适用于调试/发布 target,不适用于最终的产品。

例如,在编译hello world时:

void hello() {
  print("Hello, World!");
}

在反汇编代码中向下滚动,您会发现:

Code for optimized function 'package:dectest/hello_world.dart_::_hello' {
        ;; B0
        ;; B1
        ;; Enter frame
0xf69ace60    e92d4800               stmdb sp!, {fp, lr}
0xf69ace64    e28db000               add fp, sp, #0
        ;; CheckStackOverflow:8(stack=0, loop=0)
0xf69ace68    e59ac024               ldr ip, [thr, #+36]
0xf69ace6c    e15d000c               cmp sp, ip
0xf69ace70    9bfffffe               blls +0 ; 0xf69ace70
        ;; PushArgument(v3)
0xf69ace74    e285ca01               add ip, pp, #4096
0xf69ace78    e59ccfa7               ldr ip, [ip, #+4007]
0xf69ace7c    e52dc004               str ip, [sp, #-4]!
        ;; StaticCall:12( print<0> v3)
0xf69ace80    ebfffffe               bl +0 ; 0xf69ace80
0xf69ace84    e28dd004               add sp, sp, #4
        ;; ParallelMove r0 <- C
0xf69ace88    e59a0060               ldr r0, [thr, #+96]
        ;; Return:16(v0)
0xf69ace8c    e24bd000               sub sp, fp, #0
0xf69ace90    e8bd8800               ldmia sp!, {fp, pc}
0xf69ace94    e1200070               bkpt #0x0
}

此处打印的内容与内置产品的快照略有不同,但重要是我们可以在汇编代码旁看到IR指令。

来分析一下

       ;; Enter frame
0xf6a6ce60    e92d4800               stmdb sp!, {fp, lr}
0xf6a6ce64    e28db000               add fp, sp, #0

这是一个标准的函数,将调用者的帧指针和链接寄存器推入堆栈,然后将帧指针设置为函数堆栈帧的底部。

与标准ARM ABI一样,它使用全降序堆栈,这意味着它在内存中向后增长。

        ;; CheckStackOverflow:8(stack=0, loop=0)
0xf6a6ce68    e59ac024               ldr ip, [thr, #+36]
0xf6a6ce6c    e15d000c               cmp sp, ip
0xf6a6ce70    9bfffffe               blls +0 ; 0xf6a6ce70

这是一个简单的例程,它会执行你所想的事情,检查堆栈是否溢出。

遗憾的是,它们的反汇编程序不会注释线程字段或分支目标,因此必须进行一些挖掘。

可以在 vm/compiler/runtime_offsets_extracted.h 中找到字段偏移量列表,该列表定义的 Thread_stack_limit_offset = 36 告诉我们访问的字段是线程堆栈限制。

比较堆栈指针后,如果溢出,将调用 stackOverflowStubWithoutFpuRegsStub 。反汇编中的分支目标似乎未打补丁,但之后我们仍然可以检查二进制文件进行确认。

        ;; PushArgument(v3)
0xf6a6ce74    e285ca01               add ip, pp, #4096
0xf6a6ce78    e59ccfa7               ldr ip, [ip, #+4007]
0xf6a6ce7c    e52dc004               str ip, [sp, #-4]!

这里,来自对象池的对象被压入堆栈。由于偏移量太大而无法适合ldr偏移量编码,因此它使用了额外的 add 指令。

这个对象实际上就是我们的“Hello World!”字符串,并作为 RawOneByteString * 存储在我们的 isolate 的 globalObjectPool 中,偏移量为8103。

您可能已经注意到偏移量未对齐,这是因为对象指针是使用 vm/pointer_tagging.h 中的 kHeapObjectTag 标记的,在这种情况下,已编译代码中指向 RawObjects 的所有指针都偏移了1。

        ;; StaticCall:12( print<0> v3)
0xf6a6ce80    ebfffffe               bl +0 ; 0xf6a6ce80
0xf6a6ce84    e28dd004               add sp, sp, #4

在这里,先调用print,然后再从堆栈中弹出字符串参数。

就像分支尚未解析之前一样,它是 dart:core 中用于 print 入口点的相关分支。

        ;; ParallelMove r0 <- C
0xf69ace88    e59a0060               ldr r0, [thr, #+96]

空值被加载到返回寄存器中,96 是 Thread 中空对象字段的偏移量。

        ;; Return:16(v0)
0xf69ace8c    e24bd000               sub sp, fp, #0
0xf69ace90    e8bd8800               ldmia sp!, {fp, pc}
0xf69ace94    e1200070               bkpt #0x0

最后是函数结尾,栈帧和所有被调用者保存的寄存器一起恢复。由于 lr 被最后推入,将其弹出到 pc 中将导致该函数返回。

从现在开始,我将使用自己的反汇编程序中的代码片段,该代码片段的问题少于内置的问题。

【未完待续】

程序员专栏 扫码关注填加客服 长按识别下方二维码进群

近期精彩内容推荐:  

 内部泄露版!互联网大厂的薪资和职级一览

 在互联网公司上班 VS 在金融公司上班

 动态图展示6个常用的数据结构,一目了然

 去掉烦人的 “ ! = null " (判空语句)

在看点这里好文分享给更多人↓↓

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Flutter是一种跨平台的应用开发框架,它可以帮助开发者快速构建高性能的Android和iOS应用程序。但是由于Flutter应用程序的特殊性,使得传统的Android逆向工程技术在逆向分析和修改Flutter应用上存在一些挑战。 首先,Flutter应用程序的核心逻辑是通过Dart语言编写的,而不是传统的Java或Kotlin。这意味着我们无法像分析Java或Kotlin代码那样直接对源代码进行分析和修改。但是,我们仍然可以通过逆向技术来查看Flutter应用程序的Dart字节码,并尝试从中获取相关信息。 其次,Flutter应用程序通常会使用干净的、高度抽象的接口来与底层的Android操作系统交互,以实现良好的跨平台兼容性。这使得传统的逆向工程技术(如hook框架、反编译工具)在对Flutter应用程序进行逆向分析时效果有限,因为我们无法直接获取到涉及底层Android接口实现的关键代码。 然而,这并不意味着没有任何逆向方法可以应用Flutter应用程序。在逆向Flutter应用程序时,我们可以尝试使用动态调试技术,通过Hook应用程序的Native层代码来获取关键信息。同时,一些第三方工具和库也提供了对Flutter应用程序的逆向支持,可以帮助我们更好地进行分析和修改。 总而言之,逆向Flutter应用程序可能会面临一些挑战,但我们仍然可以通过采用合适的逆向工程技术和工具来解决问题。关键是持续学习和研究最新的逆向技术,以适应Flutter技术的发展并应对逆向分析的挑战。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值