MASTG-TECH-0037: 符号执行

符号执行是你工具箱中一个非常有用的技术,尤其是在处理需要找到正确输入才能到达特定代码块的问题时。在本节中,我们将通过使用 Angr 二进制分析框架作为我们的符号执行引擎,来解决一个简单的 Android 破解程序。

为了演示这项技术,我们将使用一个名为 Android License Validator 的破解程序。 该破解程序由单个 ELF 可执行文件组成,可以通过按照以下说明在任何 Android 设备上执行

$ adb push validate /data/local/tmp
[100%] /data/local/tmp/validate

$ adb shell chmod 755 /data/local/tmp/validate

$ adb shell /data/local/tmp/validate
Usage: ./validate <serial>

$ adb shell /data/local/tmp/validate 12345
Incorrect serial (wrong format).

到目前为止一切顺利,但我们对有效的许可证密钥是什么样子一无所知。 首先,在反汇编程序(如 iaito 中打开 ELF 可执行文件。 main 函数位于反汇编中的偏移量 0x00001874 处。 重要的是要注意,此二进制文件已启用 PIE,并且 iaito 选择在 0x0 处加载二进制文件作为镜像基址。

函数名称已从二进制文件中删除,但幸运的是,有足够的调试字符串为我们提供代码上下文。 展望未来,我们将从偏移量 0x00001874 的入口函数开始分析二进制文件,并记下所有容易获得的讯息。 在此分析期间,我们还将尝试识别适合符号执行的代码区域。

strlen 在偏移量 0x000018a8 处被调用,并且返回的值在偏移量 0x000018b0 处与 0x10 进行比较。 紧随其后,输入字符串在偏移量 0x00001340 处被传递给 Base32 解码函数。 这为我们提供了有价值的讯息,即输入许可证密钥是一个 Base32 编码的 16 个字符的字符串(总共为 10 个原始字节)。 然后,解码后的输入被传递到偏移量 0x00001760 处的函数,该函数验证许可证密钥。 该函数的反汇编如下所示。

我们现在可以使用有关预期输入的此讯息来进一步研究 0x00001760 处的验证函数。

╭ (fcn) fcn.00001760 268
│   fcn.00001760 (int32_t arg1);
│           ; var int32_t var_20h @ fp-0x20
│           ; var int32_t var_14h @ fp-0x14
│           ; var int32_t var_10h @ fp-0x10
│           ; arg int32_t arg1 @ r0
│           ; CALL XREF from fcn.00001760 (+0x1c4)
│           0x00001760      push {r4, fp, lr}
│           0x00001764      add fp, sp, 8
│           0x00001768      sub sp, sp, 0x1c
│           0x0000176c      str r0, [var_20h]                          ; 0x20 ; "$!" ; arg1
│           0x00001770      ldr r3, [var_20h]                          ; 0x20 ; "$!" ; entry.preinit0
│           0x00001774      str r3, [var_10h]                          ; str.
│                                                                      ; 0x10
│           0x00001778      mov r3, 0
│           0x0000177c      str r3, [var_14h]                          ; 0x14
│       ╭─< 0x00001780      b 0x17d0
│       │   ; CODE XREF from fcn.00001760 (0x17d8)
│      ╭──> 0x00001784      ldr r3, [var_10h]                          ; str.
│       │                                                              ; 0x10 ; entry.preinit0
│      ╎│   0x00001788      ldrb r2, [r3]
│      ╎│   0x0000178c      ldr r3, [var_10h]                          ; str.
│      ╎│                                                              ; 0x10 ; entry.preinit0
│      ╎│   0x00001790      add r3, r3, 1
│      ╎│   0x00001794      ldrb r3, [r3]
│      ╎│   0x00001798      eor r3, r2, r3
│      ╎│   0x0000179c      and r2, r3, 0xff
│      ╎│   0x000017a0      mvn r3, 0xf
│      ╎│   0x000017a4      ldr r1, [var_14h]                          ; 0x14 ; entry.preinit0
│      ╎│   0x000017a8      sub r0, fp, 0xc
│      ╎│   0x000017ac      add r1, r0, r1
│      ╎│   0x000017b0      add r3, r1, r3
│      ╎│   0x000017b4      strb r2, [r3]
│      ╎│   0x000017b8      ldr r3, [var_10h]                          ; str.
│      ╎│                                                              ; 0x10 ; entry.preinit0
│      ╎│   0x000017bc      add r3, r3, 2                              ; "ELF\x01\x01\x01" ; aav.0x00000001
│      ╎│   0x000017c0      str r3, [var_10h]                          ; str.
│      ╎│                                                              ; 0x10
│      ╎│   0x000017c4      ldr r3, [var_14h]                          ; 0x14 ; entry.preinit0
│      ╎│   0x000017c8      add r3, r3, 1
│      ╎│   0x000017cc      str r3, [var_14h]                          ; 0x14
│      ╎│   ; CODE XREF from fcn.00001760 (0x1780)
│      ╎╰─> 0x000017d0      ldr r3, [var_14h]                          ; 0x14 ; entry.preinit0
│      ╎    0x000017d4      cmp r3, 4                                  ; aav.0x00000004 ; aav.0x00000001 ; aav.0x00000001
│      ╰──< 0x000017d8      ble 0x1784                                 ; likely
│           0x000017dc      ldrb r4, [fp, -0x1c]                       ; "4"
│           0x000017e0      bl fcn.000016f0
│           0x000017e4      mov r3, r0
│           0x000017e8      cmp r4, r3
│       ╭─< 0x000017ec      bne 0x1854                                 ; likely
│       │   0x000017f0      ldrb r4, [fp, -0x1b]
│       │   0x000017f4      bl fcn.0000170c
│       │   0x000017f8      mov r3, r0
│       │   0x000017fc      cmp r4, r3
│      ╭──< 0x00001800      bne 0x1854                                 ; likely
│      ││   0x00001804      ldrb r4, [fp, -0x1a]
│      ││   0x00001808      bl fcn.000016f0
│      ││   0x0000180c      mov r3, r0
│      ││   0x00001810      cmp r4, r3
│     ╭───< 0x00001814      bne 0x1854                                 ; likely
│     │││   0x00001818      ldrb r4, [fp, -0x19]
│     │││   0x0000181c      bl fcn.00001728
│     │││   0x00001820      mov r3, r0
│     │││   0x00001824      cmp r4, r3
│    ╭────< 0x00001828      bne 0x1854                                 ; likely
│    ││││   0x0000182c      ldrb r4, [fp, -0x18]
│    ││││   0x00001830      bl fcn.00001744
│    ││││   0x00001834      mov r3, r0
│    ││││   0x00001838      cmp r4, r3
│   ╭─────< 0x0000183c      bne 0x1854                                 ; likely
│   │││││   0x00001840      ldr r3, [0x0000186c]                       ; [0x186c:4]=0x270 section..hash ; section..hash
│   │││││   0x00001844      add r3, pc, r3                             ; 0x1abc ; "Product activation passed. Congratulations!"
│   │││││   0x00001848      mov r0, r3                                 ; 0x1abc ; "Product activation passed. Congratulations!" ;
│   │││││   0x0000184c      bl sym.imp.puts                            ; int puts(const char *s)
│   │││││                                                              ; int puts("Product activation passed. Congratulations!")
│  ╭──────< 0x00001850      b 0x1864
│  ││││││   ; CODE XREFS from fcn.00001760 (0x17ec, 0x1800, 0x1814, 0x1828, 0x183c)
│  │╰╰╰╰╰─> 0x00001854      ldr r3, aav.0x00000288                     ; [0x1870:4]=0x288 aav.0x00000288
│  │        0x00001858      add r3, pc, r3                             ; 0x1ae8 ; "Incorrect serial." ;
│  │        0x0000185c      mov r0, r3                                 ; 0x1ae8 ; "Incorrect serial." ;
│  │        0x00001860      bl sym.imp.puts                            ; int puts(const char *s)
│  │                                                                   ; int puts("Incorrect serial.")
│  │        ; CODE XREF from fcn.00001760 (0x1850)
│  ╰──────> 0x00001864      sub sp, fp, 8
╰           0x00001868      pop {r4, fp, pc}                           ; entry.preinit0 ; entry.preinit0 ;

讨论函数中的所有指令超出了本章的范围,相反,我们将仅讨论分析所需的重要点。 在验证函数中,在 0x00001784 处存在一个循环,该循环在偏移量 0x00001798 处执行 XOR 操作。 该循环在下面的图形视图中更清楚地可见。

XOR 是一种非常常用的技术,用于加密以混淆为目标而不是安全性的讯息。 **XOR 不应用于任何严肃的加密**,因为它可以使用频率分析来破解。 因此,在这种验证逻辑中仅存在 XOR 加密始终需要特别注意和分析。

继续前进,在偏移量 0x000017dc 处,从上面获得的 XOR 解码值与子函数调用 0x000017e8 的返回值进行比较。

显然,此函数并不复杂,可以手动分析,但仍然是一项繁琐的任务。 特别是在处理大型代码库时,时间可能是一个主要制约因素,并且希望自动化此类分析。 动态符号执行在完全相同的情况下很有帮助。 在上面的破解程序中,符号执行引擎可以通过映射许可证检查的第一条指令(位于 0x00001760)和打印“产品激活已通过”消息的代码(位于 0x00001840)之间的路径来确定输入字符串的每个字节的约束。

从上述步骤获得的约束被传递给求解器引擎,该引擎找到满足它们的输入-有效的许可证密钥。

你需要执行多个步骤来初始化 Angr 的符号执行引擎

  • 将二进制文件加载到 Project 中,这是 Angr 中任何类型分析的起点。

  • 传递应从其开始分析的地址。 在这种情况下,我们将使用序列号验证功能的第一条指令初始化状态。 这使得问题更容易解决,因为你避免了象征性地执行 Base32 实现。

  • 传递分析应到达的代码块的地址。 在这种情况下,偏移量为 0x00001840,负责打印“产品激活已通过”消息的代码位于此处。

  • 此外,指定分析不应到达的地址。 在这种情况下,在 0x00001854 处打印“不正确的序列号”消息的代码块不有趣。

请注意,Angr 加载器将使用 0x400000 的基址加载 PIE 可执行文件,需要在将其传递给 Angr 之前将其添加到 iaito 的偏移量中。

最终的解决方案脚本如下所示

import angr # Version: 9.2.2
import base64

load_options = {}

b = angr.Project("./validate", load_options = load_options)
# The key validation function starts at 0x401760, so that's where we create the initial state.
# This speeds things up a lot because we're bypassing the Base32-encoder.

options = {
    angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
    angr.options.ZERO_FILL_UNCONSTRAINED_REGISTERS,
}

state = b.factory.blank_state(addr=0x401760, add_options=options)

simgr = b.factory.simulation_manager(state)
simgr.explore(find=0x401840, avoid=0x401854)

# 0x401840 = Product activation passed
# 0x401854 = Incorrect serial
found = simgr.found[0]

# Get the solution string from *(R11 - 0x20).

addr = found.memory.load(found.regs.r11 - 0x20, 1, endness="Iend_LE")
concrete_addr = found.solver.eval(addr)
solution = found.solver.eval(found.memory.load(concrete_addr,10), cast_to=bytes)
print(base64.b32encode(solution))

如前文“动态二进制插桩”部分所述,符号执行引擎构建给定程序输入的运算的二叉树,并为可能采取的每条路径生成数学方程式。 在内部,Angr 探索了我们指定的两个点之间的所有路径,并将相应的数学方程式传递给求解器以返回有意义的具体结果。 我们可以通过 simulation_manager.found 列表访问这些解决方案,其中包含 Angr 探索的满足我们指定搜索标准的所有可能路径。

仔细查看脚本的后半部分,其中正在检索最终解决方案字符串。 字符串的地址是从地址 r11 - 0x20 获得的。 这起初可能看起来很神奇,但仔细分析 0x00001760 的函数会发现线索,因为它确定给定的输入字符串是否是有效的许可证密钥。 在上面的反汇编中,你可以看到函数的输入字符串(在寄存器 R0 中)是如何存储到本地堆栈变量 0x0000176c str r0, [var_20h] 中的。 因此,我们决定使用此值来检索脚本中的最终解决方案。 使用 found.solver.eval 你可以问求解器这样的问题,“给定此操作序列的输出(found 中的当前状态),输入(在 addr 处)必须是什么?”。

在 ARMv7 中,R11 被称为 fp(函数指针),因此 R11 - 0x20 等价于 fp-0x20var int32_t var_20h @ fp-0x20

接下来,脚本中的 endness 参数指定数据以“little-endian”方式存储,这几乎是所有 Android 设备的情况。

此外,脚本似乎只是从脚本的内存中读取解决方案字符串。 但是,它是从符号内存中读取的。 字符串和字符串指针都不存在。 求解器确保它提供的解决方案与将程序执行到该点时相同。

运行此脚本应返回以下输出

$ python3 solve.py
WARNING | ... | cle.loader | The main binary is a position-independent executable. It is being loaded with a base address of 0x400000.

b'JACE6ACIARNAAIIA'

现在你可以在你的 Android 设备上运行验证二进制文件来验证解决方案(请参阅 Android License Validator)。

使用脚本你可能会获得不同的解决方案,因为可能有多个有效的许可证密钥。

总之,学习符号执行起初可能看起来有点令人生畏,因为它需要深入的理解和大量的实践。 但是,考虑到与手动分析复杂的反汇编指令相比,它可以节省宝贵的时间,因此这种努力是合理的。 通常你会使用混合技术,就像在上面的示例中一样,我们对反汇编的代码执行手动分析,以便为符号执行引擎提供正确的条件。 请参阅 iOS 章节,了解更多 Angr 用法的示例。