符号执行(symbolic executio)angr_ctf 学习笔记

前言

学习完了(实际上只是粗略过了一遍)FUZZ,接下来学习二进制分析三件套(模糊测试、符号执行、污点分析)中的符号执行。如果说模糊测试是大水漫灌,追求以量取胜,符号执行更像是精准渗透,把每一个分支都尽可能探索出来。

前置知识:

  • 二进制分析
  • CTF-pwn 相关知识

我还是比较喜欢用ctf的方式来学习的,硬啃知识点我觉得很难受。项目地址: https://github.com/jakespringer/angr_ctf

一、环境安装

我使用AMD64处理器+VM虚拟机进行,二进制一定要用对应架构,mac是arm环境不全(已经不知道踩坑多少次了)。

VMWare安装Ubuntu24,之后拷贝项目到工作目录

git clone https://github.com/jakespringer/angr_ctf
cd ANGR_CTF
python3 -m venv .venv
source .venv/bin/activate
pip install angr jinja2

二、项目使用

一般来说下载下来的项目有很多类似:

.
├── 00_angr_find
│   ├── 00_angr_find.c.jinja
│   ├── generate.py
│   ├── __init__.py
│   └── scaffold00.py
├── 01_angr_avoid
│   ├── 01_angr_avoid.c.jinja
│   ├── generate.py
│   ├── __init__.py
│   └── scaffold01.py

这里以 00_angr_find 为例讲一下如何使用

  1. 00_angr_find.c.jinja 题目的源代码,一般来说不用动
  2. generate.py 制作题目脚本,一般的使用方法是:python generate.py 123 A(123是种子,A是输出名)
#!/usr/bin/env python3
import sys, random, os, tempfile, jinja2

def generate(argv):
  if len(argv) != 3:
    print('Usage: ./generate.py [seed] [output_file]')
    sys.exit()

  seed = argv[1]
  output_file = argv[2]

  random.seed(seed)
  userdef_charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
  userdef = ''.join(random.choice(userdef_charset) for _ in range(8))

  template = open(os.path.join(os.path.dirname(os.path.realpath(__file__)), '00_angr_find.c.jinja'), 'r').read()
  t = jinja2.Template(template)
  c_code = t.render(userdef=userdef, len_userdef=len(userdef), description = '')

  with tempfile.NamedTemporaryFile(delete=False, suffix='.c', mode='w') as temp:
    temp.write(c_code)
    temp.seek(0)
    os.system('gcc -fno-pie -no-pie -fcf-protection=none -m32 -o ' + output_file + ' ' + temp.name)

if __name__ == '__main__':
    generate(sys.argv)
  1. __init__.py 目前还用不上
  2. scaffold00.py 每一题官方给的题解模板,类似pwn的exp

三、 00_angr_find

3.1 环境准备

source .venv/bin/activate
cd 00_angr_find
python generate.py 123 A

为了方便学习,我使用了固定种子123和名称A,你可以使用任何你喜欢的种子和名称

下载A到主机使用IDA pro打开
image

发现非常简单,打逆向的师傅一下子就能看出来就是一个加密+比对,但是我们要让程序来执行到puts("Good Job.")

3.2 模板学习

我们先看看官方给的模板,这里我为了学习解释去除了官方注释,请以官方注释为准!

import angr
import sys

def main(argv):
    path_to_binary = ???
    project = angr.Project(path_to_binary)

    initial_state = project.factory.entry_state(
        add_options={angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                     angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
    )

    simulation = project.factory.simgr(initial_state)

    print_good_address = ???
    simulation.explore(find=print_good_address)

    if simulation.found:
        solution_state = simulation.found[0]
        print(solution_state.posix.dumps(sys.stdin.fileno()).decode())
    else:
        raise Exception('Could not find the solution')


if __name__ == '__main__':
    main(sys.argv)

3.2.1 库分析

第一次我们挨个分析,首先是首尾部分,这里的angr类似pwntoolsfrom pwn import *,以后还会见到的老熟人

import angr
import sys
if __name__ == '__main__':
    main(sys.argv)

3.2.2 文件对象加载

下一个是文件对象angr.Project,实际上要填入文件路径,类似pwn中的remortprocess

    path_to_binary = ???                          # 示例:"./A"
    project = angr.Project(path_to_binary)

也就是说,这两句可以简写为:project = angr.Project("./A")

3.2.3 执行器初始化

    initial_state = project.factory.entry_state(
        add_options={angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                     angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
    )

    simulation = project.factory.simgr(initial_state)

这两句先不用细究,只要知道:

  1. project.factory.entry_state 创造了一个准备执行状态
  2. project.factory.simgr(initial_state) 创建一个符号执行的状态管理器

其它的选项add_options暂时对本题没影响,可以后面学习,别一上来就全部搞懂,很劝退的,把这两句当成准备了一个执行器即可。

3.2.4 寻找地址

下一步就是寻找目标地址,即你希望程序到达什么地址,然后执行寻找

    print_good_address = ???                           # 例如:0x80492E3
    simulation.explore(find=print_good_address)

比方说在本例中我们希望执行到的位置位于0x80492E3,就填入0x80492E3,请注意这里是数值,不是字符串,也不需要用p32()包裹,避免和pwntools混了,虽然有些时候的确需要这两个协同工作

image

3.2.5 打印结果

    if simulation.found:
        solution_state = simulation.found[0]
        print(solution_state.posix.dumps(sys.stdin.fileno()).decode())
    else:
        raise Exception('Could not find the solution')

这里就是打印对应的结果,具体也不用细究,用就可以了。

3.2.6 完整的脚本

import angr
import sys

def main(argv):
    path_to_binary = "./A"
    project = angr.Project(path_to_binary)

    initial_state = project.factory.entry_state(
        add_options={angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                     angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
    )

    simulation = project.factory.simgr(initial_state)

    print_good_address = 0x80492E3
    simulation.explore(find=print_good_address)

    if simulation.found:
        solution_state = simulation.found[0]
        print(solution_state.posix.dumps(sys.stdin.fileno()).decode())
    else:
        raise Exception('Could not find the solution')

if __name__ == '__main__':
    main(sys.argv)

执行结果

image

四、01_angr_avoid

进行二进制文件制作,然后尝试ida打开,发现直接报错说这个函数太大了

image

那没关系,我们核心目标是找到puts("Good Job."),那就直接搜索字符串(在)
image

image

按x查看交叉引用
image

直接找到地址,那么填入脚本尝试运行
image

import angr
import sys

def main(argv):
  path_to_binary = "./A"
  project = angr.Project(path_to_binary)
  initial_state = project.factory.entry_state(
    add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                    angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
  )
  simulation = project.factory.simgr(initial_state)

  print_good_address = 0x804926B
  will_not_succeed_address = ???
  simulation.explore(find=print_good_address, avoid=will_not_succeed_address)

  if simulation.found:
    solution_state = simulation.found[0]
    print(solution_state.posix.dumps(sys.stdin.fileno()).decode())
  else:
    raise Exception('Could not find the solution')

if __name__ == '__main__':
  main(sys.argv)

此时我们发现理论上已经可以跑了,但是还有一处问号,这个问号是代表不希望到达的地址,也就是说,除了希望到达的地址,我们还需要手动去找出不希望到达的地址。我们尝试回到main开头,

.text:0804928E ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:0804928E                 public main
.text:0804928E main            proc near               ; CODE XREF: __wrap_main↑j

......
; 提示输入密码

.text:080492CA loc_80492CA:                            ; CODE XREF: main+2B↑j
.text:080492CA                 cmp     [ebp+var_3C], 13h
.text:080492CE                 jle     short loc_80492BB
.text:080492D0                 lea     eax, [ebp+s2]
.text:080492D3                 mov     dword ptr [eax], 47414751h
.text:080492D9                 mov     dword ptr [eax+4], 45514C54h
.text:080492E0                 sub     esp, 0Ch
.text:080492E3                 push    offset aEnterThePasswo ; "Enter the password: "
.text:080492E8                 call    _printf
.text:080492ED                 add     esp, 10h
.text:080492F0                 sub     esp, 8
.text:080492F3                 lea     eax, [ebp+s1]
.text:080492F6                 push    eax
.text:080492F7                 push    offset a8s      ; "%8s"
.text:080492FC                 call    ___isoc23_scanf
.text:08049301                 add     esp, 10h
.text:08049304                 mov     [ebp+a2], 0
.text:0804930B                 jmp     short loc_804933A
.text:0804930D ; ---------------------------------------------------------------------------
.text:0804930D
.text:0804930D loc_804930D:                            ; CODE XREF: main+B0↓j
.text:0804930D                 lea     edx, [ebp+s1]
.text:08049310                 mov     eax, [ebp+a2]
.text:08049313                 add     eax, edx
.text:08049315                 movzx   eax, byte ptr [eax]
.text:08049318                 movsx   eax, al
.text:0804931B                 sub     esp, 8
.text:0804931E                 push    [ebp+a2]        ; a2
.text:08049321                 push    eax             ; n64
.text:08049322                 call    complex_function

......
.text:080493FB                 setnz   al
.text:080493FE                 xor     eax, edx
.text:08049400                 test    al, al
.text:08049402                 jz      loc_8051FBA
.text:08049408                 call    avoid_me

......
.text:0804944F                 test    al, al
.text:08049451                 jns     loc_804B72B
.text:08049457                 call    avoid_me

......
.text:0804947E                 xor     eax, edx
.text:08049480                 test    al, al
.text:08049482                 jz      loc_804A5DC
.text:08049488                 call    avoid_me

通过读程序汇编,以及题目提示函数avoid_me,我们判断要回避avoid_me,那么:
image

我们希望回避0x8049233,填入脚本运行

import angr
import sys


def main(argv):
    path_to_binary = "./A"
    project = angr.Project(path_to_binary)
    initial_state = project.factory.entry_state(
        add_options={angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                     angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
    )
    simulation = project.factory.simgr(initial_state)

    print_good_address = 0x804926B
    will_not_succeed_address = 0x8049233
    simulation.explore(find=print_good_address, avoid=will_not_succeed_address)

    if simulation.found:
        solution_state = simulation.found[0]
        print(solution_state.posix.dumps(sys.stdin.fileno()).decode())
    else:
        raise Exception('Could not find the solution')


if __name__ == '__main__':
    main(sys.argv)

小等一会

image

这里给出时间对比:
image

我们发现如果不设置规避,就会调用过深找不到

五、02_angr_find_condition

第三题还是一样的,生成文件并ida打开,又是经典的主函数过大不让反编译,那就直接搜索字符串
image

(⊙o⊙)?怎么这么多???看看题目提示吧

能够搜索到达特定指令的状态是非常有用的。然而,在某些情况下,您可能不知道
想要到达的具体指令地址(或者可能不存在单一的指令目标)。在这个挑战中,
您不知道是哪条指令会带来成功。相反,您只知道想要找到一个二进制文件打印出
"Good Job." 的状态。
Angr 的强大之处在于它允许您搜索满足在 Python 中指定的任意条件的状态,
使用您定义的一个谓词函数,该函数接收一个状态作为参数,如果找到了您要寻找的
内容则返回 True,否则返回 False。

简而言之:有很多成功的可能,但是并不确定是那一条会成功,没办法确定。此时不依赖于地址,而是依赖于输出判断

    def is_successful(state):
        stdout_output = state.posix.dumps(sys.stdout.fileno())
        return b"Good Job." in stdout_output

    def should_abort(state):
        stdout_output = state.posix.dumps(sys.stdout.fileno())
        return b"Try again." in stdout_output

这里添加了判断b"Good Job." in stdout_outputb"Try again." in stdout_output , 需要注意的是比较是字节串而不是字符串(即要用b""
image

六、03_angr_symbolic_registers

这次终于没有main过大了,能看反编译了
image

image

# Angr 目前不支持解析使用多个参数的 scanf 函数(例如:
# scanf("%u %u"))。你需要告诉模拟引擎在 scanf 调用后开始执行程序,
# 并手动将符号注入到寄存器中。

import angr
import claripy
import sys

def main(argv):
  path_to_binary = argv[1]
  project = angr.Project(path_to_binary)

  # 有时候,你需要指定程序应该从何处开始执行。变量 start_address 将指定
  # 符号执行引擎应该开始的位置。
  # 注意我们使用的是 blank_state 而不是 entry_state。
  # (!)
  start_address = ???  # :整数(可能是十六进制)
  initial_state = project.factory.blank_state(
    addr=start_address,
    add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                    angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
  )

  # 创建一个符号位向量(Angr 用于将符号值注入二进制文件的数据类型)。
  # 第一个参数只是 Angr 用来引用它的名称。
  # 你需要构建多个位向量。复制下面两行并更改变量名。
  # 要确定需要多少个(以及什么大小的)位向量,请反汇编二进制文件并
  # 确定传递给 scanf 的格式参数。
  # (!)
  password0_size_in_bits = ???  # :整数
  password0 = claripy.BVS('password0', password0_size_in_bits)
  ...

  # 将寄存器设置为符号值。这是将符号注入程序的一种方法。
  # initial_state.regs 存储了许多方便的按名称引用寄存器的属性。
  # 例如,要将 eax 设置为 password0,请使用:
  #
  # initial_state.regs.eax = password0
  #
  # 你需要将多个寄存器设置为不同的位向量。复制并粘贴下面的行,
  # 然后更改寄存器。要确定将哪个符号注入哪个寄存器,请反汇编二进制文件
  # 并查看紧接在 scanf 调用之后的指令。
  # (!)
  initial_state.regs.??? = password0
  ...

  simulation = project.factory.simgr(initial_state)

  def is_successful(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return ???

  def should_abort(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return ???

  simulation.explore(find=is_successful, avoid=should_abort)

  if simulation.found:
    solution_state = simulation.found[0]

    # 求解符号值。如果有多个解,我们只关心一个,
    # 所以可以使用 eval,它返回任意(但只有一个)解。
    # 将你想要求解的位向量传递给 eval。
    # (!)
    solution0 = solution_state.solver.eval(password0)
    ...

    # 汇总并格式化上面计算的解,然后打印完整的字符串。
    # 注意整数的顺序和期望的进制(十进制、八进制、十六进制等)。
    solution = ???  # :字符串
    print(solution)
  else:
    raise Exception('无法找到解')

if __name__ == '__main__':
  main(sys.argv)

因为Angr目前不支持解析使用多个参数的scanf函数,所以只能跳过然后手动设置寄存器及内存

image

通过图可以看出,我们要跳过输入函数get_user_input,然后向eaxebxedx寄存器放入参数来运行,从0x8049518开始运行

  # 自定义起始位置,使用blank_state
  start_address = 0x8049518
  initial_state = project.factory.blank_state(
      addr=start_address,
      add_options={angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                   angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
  )

然后创建位向量

  # 使用claripy创建位向量
  password0_size_in_bits = 32
  password0 = claripy.BVS('password0', password0_size_in_bits)
  password1_size_in_bits = 32
  password1 = claripy.BVS('password0', password1_size_in_bits)
  password2_size_in_bits = 32
  password2 = claripy.BVS('password0', password2_size_in_bits)

这里说一下BVS用法

函数原型

claripy.BVS(name, size, min=None, max=None, stride=None, uninitialized=False, explicit_name=False)

主要参数

  • name (字符串): 符号变量的名称,用于标识和调试
  • size (整数): 符号变量的位大小 注意:这里不是指常规的sizeof,而是比特位
  • min, max: 可选的值范围约束
  • stride: 可选的值步长约束

注意
size是指比特位而不是位,例如:

类型 架构 size
int 32 16
char 32 8
float 32 32
int 64 32
char 64 8
int 64 64

即:常规的位*8

然后就是常规的注入寄存器,其中eaxebxedx就是寄存器名称

  # 将符号注入到寄存器
  initial_state.regs.eax = password0
  initial_state.regs.ebx = password1
  initial_state.regs.edx = password2

  simulation = project.factory.simgr(initial_state)

之后就是之前做过的判断,这里和上一题的处理方式一样 点我回到03_angr_symbolic_registers

  def is_successful(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return b'Good Job.' in stdout_output

  def should_abort(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return b'Try again.' in stdout_output

  simulation.explore(find=is_successful, avoid=should_abort)

下一步就是处理输出

  if simulation.found:
    solution_state = simulation.found[0]

    # 求解符号值。如果有多个解,我们只关心一个,
    # 所以可以使用 eval,它返回任意(但只有一个)解。
    # 将你想要求解的位向量传递给 eval。
    # (!)
    solution0 = solution_state.solver.eval(password0)
    ...

    # 汇总并格式化上面计算的解,然后打印完整的字符串。
    # 注意整数的顺序和期望的进制(十进制、八进制、十六进制等)。
    solution = ???  # :字符串
    print(solution)
  else:
    raise Exception('无法找到解')

因为要输入的是十六进制,就直接输出:x即可

  if simulation.found:
    solution_state = simulation.found[0]

    # 求解满足条件的符号
    solution0 = solution_state.solver.eval(password0)
    solution1 = solution_state.solver.eval(password1)
    solution2 = solution_state.solver.eval(password2)

    solution = f'{solution0:x} {solution1:x} {solution2:x}'
    print(solution)
  else:
    raise Exception('Could not find the solution')

image

解出来就好了,

import angr
import claripy
import sys


def main(argv):
    path_to_binary = argv[1]
    project = angr.Project(path_to_binary)

    # 自定义起始位置,使用blank_state
    start_address = 0x8049518
    initial_state = project.factory.blank_state(
        addr=start_address,
        add_options={angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                     angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
    )

    # 使用claripy创建位向量
    password0_size_in_bits = 32
    password0 = claripy.BVS('password0', password0_size_in_bits)
    password1_size_in_bits = 32
    password1 = claripy.BVS('password0', password1_size_in_bits)
    password2_size_in_bits = 32
    password2 = claripy.BVS('password0', password2_size_in_bits)

    # 将符号注入到寄存器
    initial_state.regs.eax = password0
    initial_state.regs.ebx = password1
    initial_state.regs.edx = password2

    simulation = project.factory.simgr(initial_state)

    def is_successful(state):
        stdout_output = state.posix.dumps(sys.stdout.fileno())
        return b'Good Job.' in stdout_output

    def should_abort(state):
        stdout_output = state.posix.dumps(sys.stdout.fileno())
        return b'Try again.' in stdout_output

    simulation.explore(find=is_successful, avoid=should_abort)

    if simulation.found:
        solution_state = simulation.found[0]

        # 求解满足条件的符号
        solution0 = solution_state.solver.eval(password0)
        solution1 = solution_state.solver.eval(password1)
        solution2 = solution_state.solver.eval(password2)

        solution = f'{solution0:x} {solution1:x} {solution2:x}'
        print(solution)
    else:
        raise Exception('Could not find the solution')


if __name__ == '__main__':
    main(sys.argv)

注意如果你直接拿我的脚本是跑不通的,因为每次随机生成都有一点随机性,在对答案时候发现官方的和我的开始地址有些不一致

image

七、07_angr_symbolic_file

先是查看题目
image

image

# 这个挑战将比之前你遇到的挑战更困难。由于本次 CTF 的目标是教授
# “符号执行(symbolic execution)”,而不是如何构造栈帧,因此这些注释
# 将指导你理解栈中的内容。
#   ! ! !
# 重要:本脚本中的任何地址不一定正确!你必须自己反汇编二进制文件来确定正确的地址!
#   ! ! !

import angr
import claripy
import sys

def main(argv):
  path_to_binary = argv[1]
  project = angr.Project(path_to_binary)

  # 对于这个挑战,我们希望从 scanf 调用之后开始执行。注意这段代码位于
  # 一个函数的中间位置。
  #
  # 这个挑战需要处理栈,因此必须特别仔细地确定起始位置,否则会进入一种
  # 栈设置不正确的状态。为了确定 scanf 之后的起始地址,我们需要查看
  # 调用 scanf 的反汇编,以及紧跟其后的指令:
  #   sub    $0x4,%esp
  #   lea    -0x10(%ebp),%eax
  #   push   %eax
  #   lea    -0xc(%ebp),%eax
  #   push   %eax
  #   push   $0x80489c3
  #   call   8048370 <__isoc99_scanf@plt>
  #   add    $0x10,%esp
  #
  # 现在问题是:我们应该从紧跟 scanf 的指令(add $0x10,%esp)开始?
  # 还是从它之后的一条指令(未显示)开始?
  #
  # 思考这个 'add $0x10,%esp' 的作用。提示:它与 scanf 调用前压入栈的参数有关。
  # 由于在 Angr 仿真中我们并不会真正调用 scanf,那么我们应该从哪里开始?
  # (!)
  start_address = ???

  initial_state = project.factory.blank_state(
    addr=start_address,
    add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                    angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
  )

  # 我们正在跳入一个函数的中间执行!因此需要手动处理函数对栈的构造。
  # 该函数的第二条指令是:
  #   mov    %esp,%ebp
  # 随后它为我们需要操作的目标栈帧分配空间:
  #   sub    $0x18,%esp
  #
  # 注意 esp 相对于 ebp 的位置。它们之间的空间(通常)就是栈空间。
  # 这里 esp 被减少了 0x18:
  #
  #        /-------- 栈结构 --------\
  # ebp -> |                         |
  #        |-------------------------|
  #        |                         |
  #        |-------------------------|
  #         . . . (共 0x18 字节)
  #         . . . 其中某处存储着
  #         . . . scanf 的结果。
  # esp -> |                         |
  #        \-------------------------/
  #
  # 由于我们从 scanf 之后开始执行,跳过了上述栈构造步骤,因此我们需要
  # 自己构造这部分栈。首先按程序的方式初始化 ebp。
  initial_state.regs.ebp = initial_state.regs.esp

  # scanf("%u %u") 需要替换为注入两个 bitvector。
  # 原因:angr(当前版本)不会在 scanf 有多个输入参数时自动注入符号变量。
  # 它可以处理 scanf("%u"),但不能处理 scanf("%u %u")。
  #
  # 你可以直接复制下面的行,或使用 Python 列表。
  # (!)
  password0 = claripy.BVS('password0', ???)
  ...

  # 接下来是难点:我们需要弄清栈的布局,并且足够准确地在正确的位置
  # 注入我们的符号变量。
  #
  # 首先查看 scanf 的参数:
  #
  #   sub    $0x4,%esp
  #   lea    -0x10(%ebp),%eax
  #   push   %eax
  #   lea    -0xc(%ebp),%eax
  #   push   %eax
  #   push   $0x80489c3
  #   call   8048370 <__isoc99_scanf@plt>
  #   add    $0x10,%esp 
  #
  # 如上所示,scanf 的调用形式为:
  #
  # scanf(  0x80489c3,   ebp - 0xc,   ebp - 0x10  )
  #      format_string    password0    password1
  #
  # 基于此,我们构造更准确的栈示意图:
  #
  #            /-------- 栈结构 --------\
  # ebp ->     |         padding        |
  #            |-------------------------|
  # ebp - 0x01 |       更多 padding      |
  #            |-------------------------|
  # ebp - 0x02 |     甚至更多 padding    |
  #            |-------------------------|
  #                        . . .           <- padding 大小是多少?
  #            |-------------------------|    提示:password0 占多少字节?
  # ebp - 0x0b | password0 的第二个字节 |
  #            |-------------------------|
  # ebp - 0x0c | password0 的第一个字节 |
  #            |-------------------------|
  # ebp - 0x0d | password1 的最后一个字节|
  #            |-------------------------|
  #                        . . .
  #            |-------------------------|
  # ebp - 0x10 | password1 的第一个字节 |
  #            |-------------------------|
  #                        . . .
  #            |-------------------------|
  # esp ->     |                         |
  #            \-------------------------/
  #
  # 所以我们需要计算填充长度,并在 push 密码变量前手动减少 esp。
  padding_length_in_bytes = ???  # :integer
  initial_state.regs.esp -= padding_length_in_bytes

  # 将变量压入栈。务必按正确顺序 push!
  # 使用方法:
  #
  # initial_state.stack_push(bitvector)
  #
  # 它会自动压入 bitvector 并正确调整 esp。
  # 你需要 push 多个 bitvector。
  # (!)
  initial_state.stack_push(???)  # :bitvector
  ...

  simulation = project.factory.simgr(initial_state)

  def is_successful(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return ???

  def should_abort(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return ???

  simulation.explore(find=is_successful, avoid=should_abort)

  if simulation.found:
    solution_state = simulation.found[0]

    solution0 = solution_state.solver.eval(password0)
    ...

    solution = ???
    print(solution)
  else:
    raise Exception('Could not find the solution')

if __name__ == '__main__':
  main(sys.argv)

这里就要尝试自己构造栈,打过栈迁移的师傅们估计已经对栈炉火纯青了吧,如果对栈还不太熟悉的师傅建议先学习x86函数调用和栈相关知识再来学习

我们一步步来

  # 对于这个挑战,我们希望从 scanf 调用之后开始执行。注意这段代码位于
  # 一个函数的中间位置。
  #
  # 这个挑战需要处理栈,因此必须特别仔细地确定起始位置,否则会进入一种
  # 栈设置不正确的状态。为了确定 scanf 之后的起始地址,我们需要查看
  # 调用 scanf 的反汇编,以及紧跟其后的指令:
  #   sub    $0x4,%esp
  #   lea    -0x10(%ebp),%eax
  #   push   %eax
  #   lea    -0xc(%ebp),%eax
  #   push   %eax
  #   push   $0x80489c3
  #   call   8048370 <__isoc99_scanf@plt>
  #   add    $0x10,%esp
  #
  # 现在问题是:我们应该从紧跟 scanf 的指令(add $0x10,%esp)开始?
  # 还是从它之后的一条指令(未显示)开始?
  #
  # 思考这个 'add $0x10,%esp' 的作用。提示:它与 scanf 调用前压入栈的参数有关。
  # 由于在 Angr 仿真中我们并不会真正调用 scanf,那么我们应该从哪里开始?
  # (!)
  start_address = ???

  initial_state = project.factory.blank_state(
    addr=start_address,
    add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                    angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
  )

image

这里贴出Inter写法的ida方便对照观看,这里教程的意思是:为了使用scanf,需要对栈堆进行准备,如图:
image

也就是说,在调用scanf之前,就有3个0x4长度的数据被压到栈上,让esp降低了0xB,(32位传参基本知识),然后调用call scanfscanf下一条指令地址压到栈上(即0x4字节)。调用完成后总共压入了0x4 * 4 = 0x10字节。所以要add esp, 10h

知识补充:

  1. 32位下地址长度为0x4
  2. 32位下函数传递参数使用栈
  3. 栈是从高地址向低地址生长的,也就是push实际上是esp降低,反之pop是esp升高
  4. esp指示栈顶、ebp指示栈底

看图:(上方是高地址、下方为低地址

地址 准备前 准备好,还没调用 调用完成
高地址 上一个函数的栈 上一个函数的栈 上一个函数的栈
+4 存储的返回地址 存储的返回地址 存储的返回地址
+0 存储的rbp地址(ebp) 存储的rbp地址(ebp) 存储的rbp地址(ebp)
-4 局部变量1 局部变量1 局部变量1
-8 局部变量2(esp) 局部变量2 局部变量2
-C ??? 参数1(fmt参数) 参数1(fmt参数)
-10 ??? 参数2 参数2
-14 ??? 参数3(esp) 参数3
低地址 ??? ??? scanf下一行 (esp)

结合ida栈图(请注意,ida是从低地址到高地址

image

也就是说:
image

在我们进行符号执行的时候实际上并不需要清理(因为压根没调用)

  start_address = 0x08049392

  initial_state = project.factory.blank_state(
    addr=start_address,
    add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                    angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
  )

下一步解决:

 # 我们正在跳入一个函数的中间执行!因此需要手动处理函数对栈的构造。
  # 该函数的第二条指令是:
  #   mov    %esp,%ebp
  # 随后它为我们需要操作的目标栈帧分配空间:
  #   sub    $0x18,%esp
  #
  # 注意 esp 相对于 ebp 的位置。它们之间的空间(通常)就是栈空间。
  # 这里 esp 被减少了 0x18:
  #
  #        /-------- 栈结构 --------\
  # ebp -> |                         |
  #        |-------------------------|
  #        |                         |
  #        |-------------------------|
  #         . . . (共 0x18 字节)
  #         . . . 其中某处存储着
  #         . . . scanf 的结果。
  # esp -> |                         |
  #        \-------------------------/
  #
  # 由于我们从 scanf 之后开始执行,跳过了上述栈构造步骤,因此我们需要
  # 自己构造这部分栈。首先按程序的方式初始化 ebp。
  initial_state.regs.ebp = initial_state.regs.esp

这里说的是:
image

我们要手动模拟函数准备过程 栈迁移ptsd , 实际上就是预留存放局部变量的地方

initial_state.regs.ebp = initial_state.regs.esp # mov ebp, esp

至于为什么不把接下来的两句模拟了,因为后面我们要把输入手动压入栈以保持平衡,下一步就是常规的输入参数

  password0 = claripy.BVS('password0', 32)
  password1 = claripy.BVS("password1", 32)

这里32是因为int0x4*8,然后

  # 所以我们需要计算填充长度,并在 push 密码变量前手动减少 esp。
  padding_length_in_bytes = ???  # :integer
  initial_state.regs.esp -= padding_length_in_bytes

  # 将变量压入栈。务必按正确顺序 push!
  # 使用方法:
  #
  # initial_state.stack_push(bitvector)
  #
  # 它会自动压入 bitvector 并正确调整 esp。
  # 你需要 push 多个 bitvector。
  # (!)
  initial_state.stack_push(???)  # :bitvector
  ...

  simulation = project.factory.simgr(initial_state)

我们计算出实际上要放入0x4+0x4字节,然后分别把要用的压入(别忘了倒序压栈才是正确顺序)

  padding_length_in_bytes = 0x8
  initial_state.regs.esp -= padding_length_in_bytes
  initial_state.stack_push(password1)
  initial_state.stack_push(password0)
  simulation = project.factory.simgr(initial_state)

下面的就和上一题差不多,就直接给出了

  def is_successful(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return b'Good Job.' in stdout_output

  def should_abort(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return b'Try again.' in stdout_output

  simulation.explore(find=is_successful, avoid=should_abort)

  if simulation.found:
    solution_state = simulation.found[0]

    solution0 = solution_state.solver.eval(password0)
    solution1 = solution_state.solver.eval(password1)

    solution = f"{solution0:x} {solution1:x}"
    print(solution)
  else:
    raise Exception('Could not find the solution')

所以:

import angr
import claripy
import sys

def main(argv):
  path_to_binary = argv[1]
  project = angr.Project(path_to_binary)


  start_address = 0x08049392
  initial_state = project.factory.blank_state(
    addr=start_address,
    add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                    angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
  )

  initial_state.regs.ebp = initial_state.regs.esp  # mov ebp, esp

  password0 = claripy.BVS('password0', 32)
  password1 = claripy.BVS("password1", 32)

  padding_length_in_bytes = 0x8
  initial_state.regs.esp -= padding_length_in_bytes
  initial_state.stack_push(password1)
  initial_state.stack_push(password0) 

  simulation = project.factory.simgr(initial_state)

  def is_successful(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return b'Good Job.' in stdout_output

  def should_abort(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return b'Try again.' in stdout_output

  simulation.explore(find=is_successful, avoid=should_abort)

  if simulation.found:
    solution_state = simulation.found[0]

    solution0 = solution_state.solver.eval(password0)
    solution1 = solution_state.solver.eval(password1)

    solution = f"{solution0:x} {solution1:x}"
    print(solution)
  else:
    raise Exception('Could not find the solution')

if __name__ == '__main__':
  main(sys.argv)

image

这里说一下为什么不能直接手动构建栈再写进寄存器去,因为方便,如果要手动构建会烦死人的QAQ。并且也不好构建,程序已经准备好了用就好了。

八、05_angr_symbolic_memory

import angr
import claripy
import sys

def main(argv):
  path_to_binary = argv[1]
  project = angr.Project(path_to_binary)

  start_address = ???
  initial_state = project.factory.blank_state(
    addr=start_address,
    add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                    angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
  )

  # 二进制程序调用 scanf("%8s %8s %8s %8s")
  # (!)
  password0 = claripy.BVS('password0', ???)
  ...

  # 确定scanf写入用户输入的全局变量地址
  # 函数 'initial_state.memory.store(address, value)' 会将 'value'(一个位向量)写入 'address'(内存地址,整数形式)
  # 'address' 参数也可以是位向量(并且可以是符号化的!)
  # (!)
  password0_address = ???
  initial_state.memory.store(password0_address, password0)
  ...

  simulation = project.factory.simgr(initial_state)

  def is_successful(state):
    # 获取标准输出
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return ???

  def should_abort(state):
    # 获取标准输出
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return ???

  simulation.explore(find=is_successful, avoid=should_abort)

  if simulation.found:
    solution_state = simulation.found[0]

    # 求解符号值。我们正在尝试求解字符串
    # 因此,我们将使用eval方法,并传入cast_to=bytes参数
    # 这会返回可以解码为字符串的字节,而不是整数
    # (!)
    solution0 = solution_state.solver.eval(password0,cast_to=bytes).decode()
    ...
    solution = ???

    print(solution)
  else:
    raise Exception('Could not find the solution')

if __name__ == '__main__':
  main(sys.argv)

image

看起来这一次是输入到到全局变量上面,通过ida可知,这些变量分别在:
image

设置启动点在0x0804928C
image

注入参数在对应的位置

    # The binary is calling scanf("%8s %8s %8s %8s").
    # (!)
    password0 = claripy.BVS('password0', 64)
    password1 = claripy.BVS('password1', 64)
    password2 = claripy.BVS('password2', 64)
    password3 = claripy.BVS('password3', 64)

    # Determine the address of the global variable to which scanf writes the user
    # input. The function 'initial_state.memory.store(address, value)' will write
    # 'value' (a bitvector) to 'address' (a memory location, as an integer.) The
    # 'address' parameter can also be a bitvector (and can be symbolic!).
    # (!)
    initial_state.memory.store(0x0B53A820, password0)
    initial_state.memory.store(0x0B53A828, password1)
    initial_state.memory.store(0x0B53A830, password2)
    initial_state.memory.store(0x0B53A838, password3)

    simulation = project.factory.simgr(initial_state)

剩下的判断和解码就是按照教程来即可


    def is_successful(state):
        stdout_output = state.posix.dumps(sys.stdout.fileno())
        return b'Good Job.' in stdout_output

    def should_abort(state):
        stdout_output = state.posix.dumps(sys.stdout.fileno())
        return b'Try again.' in stdout_output

    simulation.explore(find=is_successful, avoid=should_abort)

    if simulation.found:
        solution_state = simulation.found[0]

        # Solve for the symbolic values. We are trying to solve for a string.
        # Therefore, we will use eval, with named parameter cast_to=bytes
        # which returns bytes that can be decoded to a string instead of an integer.
        # (!)
        solution0 = solution_state.solver.eval(
            password0, cast_to=bytes).decode()
        solution1 = solution_state.solver.eval(
            password1, cast_to=bytes).decode()
        solution2 = solution_state.solver.eval(
            password2, cast_to=bytes).decode()
        solution3 = solution_state.solver.eval(
            password3, cast_to=bytes).decode()
        solution = f"{solution0} {solution1} {solution2} {solution3}"
        print(solution)
    else:
        raise Exception('Could not find the solution')

总结

import angr
import claripy
import sys


def main(argv):
    path_to_binary = argv[1]
    project = angr.Project(path_to_binary)

    start_address = 0x0804928C
    initial_state = project.factory.blank_state(
        addr=start_address,
        add_options={angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                     angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
    )

    password0 = claripy.BVS('password0', 64)
    password1 = claripy.BVS('password1', 64)
    password2 = claripy.BVS('password2', 64)
    password3 = claripy.BVS('password3', 64)

    initial_state.memory.store(0x0B53A820, password0)
    initial_state.memory.store(0x0B53A828, password1)
    initial_state.memory.store(0x0B53A830, password2)
    initial_state.memory.store(0x0B53A838, password3)

    simulation = project.factory.simgr(initial_state)

    def is_successful(state):
        stdout_output = state.posix.dumps(sys.stdout.fileno())
        return b'Good Job.' in stdout_output

    def should_abort(state):
        stdout_output = state.posix.dumps(sys.stdout.fileno())
        return b'Try again.' in stdout_output

    simulation.explore(find=is_successful, avoid=should_abort)

    if simulation.found:
        solution_state = simulation.found[0]

        solution0 = solution_state.solver.eval(
            password0, cast_to=bytes).decode()
        solution1 = solution_state.solver.eval(
            password1, cast_to=bytes).decode()
        solution2 = solution_state.solver.eval(
            password2, cast_to=bytes).decode()
        solution3 = solution_state.solver.eval(
            password3, cast_to=bytes).decode()
        solution = f"{solution0} {solution1} {solution2} {solution3}"
        print(solution)
    else:
        raise Exception('Could not find the solution')


if __name__ == '__main__':
    main(sys.argv)

image

8.1 为什么不需要分开两个输入?

可能有的师傅和我一样对架构比较敏感,认为32位不是最多输入32就要下一个位置输入了吗?顺便还要再考虑一下大小端序、压栈顺序的问题,实际上工具开发者已经考虑到了,直接用即可。

九、06_angr_symbolic_dynamic_memory

image

这次是要写入到堆上?

import angr
import claripy
import sys

def main(argv):
  path_to_binary = argv[1]
  project = angr.Project(path_to_binary)

  start_address = ???
  initial_state = project.factory.blank_state(
    addr=start_address,
    add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                    angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
  )

  # 该二进制文件正在调用 scanf("%8s %8s")。
  # (!)
  password0 = claripy.BVS('password0', ???)
  ...

  # 我们可以简单地伪造一个指向任何未使用内存块的地址并覆盖数据指针,而不是告诉二进制文件写入通过 malloc 分配的内存地址。
  # 这将把 pointer_to_malloc_memory_address0 处的指针指向 fake_heap_address。
  # 注意,不止一个指针!分析二进制文件以确定每个指针的全局位置。
  # 注意:默认情况下,Angr 使用大端序在内存中存储整数。
  # 要指定使用您体系结构的字节序,请使用参数 endness=project.arch.memory_endness。在 x86 上,这是小端序。
  # (!)
  fake_heap_address0 = ???
  pointer_to_malloc_memory_address0 = ???
  initial_state.memory.store(pointer_to_malloc_memory_address0, fake_heap_address0, endness=project.arch.memory_endness)
  ...

  # 将我们的符号值存储在我们的 fake_heap_address 处。查看二进制文件以确定 scanf 写入时相对于 fake_heap_address 的偏移量。
  # (!)
  initial_state.memory.store(fake_heap_address0, password0)
  ...

  simulation = project.factory.simgr(initial_state)

  def is_successful(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return ???

  def should_abort(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return ???

  simulation.explore(find=is_successful, avoid=should_abort)

  if simulation.found:
    solution_state = simulation.found[0]

    solution0 = solution_state.solver.eval(password0,cast_to=bytes).decode()
    ...
    solution = ???

    print(solution)
  else:
    raise Exception('Could not find the solution')

if __name__ == '__main__':
  main(sys.argv)

哦,因为malloc每次返回的堆地址并不一致,直接强制其全局指针到我们指定地址即可。


Q:如果不是在全局地址怎么办?
A:参考上一题如何把数据放到栈上,只不过我们要放入的不再是字符串,而是二进制数据,可以配合pwntools使用

Q:如果bssdata段地址大小不够怎么办?
A:方法一:ELF运行时候栈和堆之间存在大量数据空洞,动调一下找个空数据处丢进去即可;方法二:path一个段上去,然后丢到path段上


首先分析开始地址:
image
因为不涉及栈上数据改动,直接设定开始地址到0x080492E0,然后直接插入两个向量

  start_address = 0x080492E0
  initial_state = project.factory.blank_state(
    addr=start_address,
    add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                    angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
  )

  # The binary is calling scanf("%8s %8s").
  # (!)
  password0 = claripy.BVS('password0', 64)
  password1 = claripy.BVS('password1', 64)

我们先找到全局指针存放的位置,然后找一个伪装堆的地址
image

buffer0 => 0x0A3FA01C
buffer1 => 0x0A3FA020

至于伪装堆的地方,这道题给了非常大的bss段,可以随意选择,但是要注意:

  1. 最好要16字节对齐(经典pwn手ptsd)
  2. 位于可读写区域
  3. 要模拟堆的行为,例如这题申请9u,就找一个0x20对齐的地方给它

在这里我就选择:
image

  # 我们可以简单地伪造一个指向任何未使用内存块的地址并覆盖数据指针,而不是告诉二进制文件写入通过 malloc 分配的内存地址。
  # 这将把 pointer_to_malloc_memory_address0 处的指针指向 fake_heap_address。
  # 注意,不止一个指针!分析二进制文件以确定每个指针的全局位置。
  # 注意:默认情况下,Angr 使用大端序在内存中存储整数。
  # 要指定使用您体系结构的字节序,请使用参数 endness=project.arch.memory_endness。在 x86 上,这是小端序。
  # (!)
  fake_heap_address0 = 0x0804C068
  pointer_to_malloc_memory_address0 = 0x0A3FA01C
  initial_state.memory.store(pointer_to_malloc_memory_address0, fake_heap_address0, endness=project.arch.memory_endness)
  
  fake_heap_address1 = 0x0804C088
  pointer_to_malloc_memory_address1 = 0x0A3FA020
  initial_state.memory.store(pointer_to_malloc_memory_address1, fake_heap_address1, endness=project.arch.memory_endness)

  # 将我们的符号值存储在我们的 fake_heap_address 处。查看二进制文件以确定 scanf 写入时相对于 fake_heap_address 的偏移量。
  # (!)
  initial_state.memory.store(fake_heap_address0, password0)
  initial_state.memory.store(fake_heap_address1, password1)

后面判断的和结果打印省略,一样的。
image
运行后有些警告,但是无伤大雅,如果要改的话就改写入内存的地方,反正网安第一原则:能跑就行

import angr
import claripy
import sys


def main(argv):
    path_to_binary = argv[1]
    project = angr.Project(path_to_binary)

    start_address = 0x080492E0
    initial_state = project.factory.blank_state(
        addr=start_address,
        add_options={angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                     angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
    )
	
    password0 = claripy.BVS('password0', 64)
    password1 = claripy.BVS('password1', 64)
    ...

    fake_heap_address0 = 0x0804C068
    pointer_to_malloc_memory_address0 = 0x0A3FA01C
    initial_state.memory.store(pointer_to_malloc_memory_address0,
                               fake_heap_address0, endness=project.arch.memory_endness)

    fake_heap_address1 = 0x0804C088
    pointer_to_malloc_memory_address1 = 0x0A3FA020
    initial_state.memory.store(pointer_to_malloc_memory_address1,
                               fake_heap_address1, endness=project.arch.memory_endness)

    initial_state.memory.store(fake_heap_address0, password0)
    initial_state.memory.store(fake_heap_address1, password1)

    simulation = project.factory.simgr(initial_state)

    def is_successful(state):
        stdout_output = state.posix.dumps(sys.stdout.fileno())
        return b'Good Job.' in stdout_output

    def should_abort(state):
        stdout_output = state.posix.dumps(sys.stdout.fileno())
        return b'Try again.' in stdout_output

    simulation.explore(find=is_successful, avoid=should_abort)

    if simulation.found:
        solution_state = simulation.found[0]

        solution0 = solution_state.solver.eval(
            password0, cast_to=bytes).decode()
        solution1 = solution_state.solver.eval(
            password1, cast_to=bytes).decode()
        solution = f"{solution0} {solution1}"
        print(solution)
    else:
        raise Exception('Could not find the solution')


if __name__ == '__main__':
    main(sys.argv)

十、 07_angr_symbolic_file

# 这个挑战理论上可以通过多种方式解决。但是,为了学习如何模拟一个替代的文件系统,
# 请按照下面提供的结构来解决这个挑战。作为一个挑战,一旦你有了初始解决方案,尝试用另一种方式解决。
#
# 问题描述和通用解决策略:
# 该二进制文件使用 fread 函数从文件加载密码。如果密码正确,它会打印 "Good Job"。
# 为了与其他挑战保持一致,控制台的输入被写入到 ignore_me 函数中的一个文件中。
# 如名称所示,忽略它,因为它仅存在于为了与其他挑战保持一致。
# 我们需要:
# 1. 确定 fread 读取的文件。
# 2. 使用 Angr 模拟一个文件系统,其中该文件被我们自己的模拟文件替换。
# 3. 用符号值初始化该文件,该值将被 fread 读取并在程序中传播。
# 4. 求解符号输入以确定密码。

import angr
import claripy
import sys

def main(argv):
  path_to_binary = argv[1]
  project = angr.Project(path_to_binary)

  start_address = ???
  initial_state = project.factory.blank_state(
    addr=start_address,
    add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                    angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
  )

  # 指定构建模拟文件所需的一些信息。对于这个挑战,文件名是硬编码的,
  # 但理论上,它可以是符号化的。
  # 注意:为了从文件读取,二进制文件调用
  # 'fread(buffer, sizeof(char), 64, file)'。
  # (!)
  filename = ???  # :字符串
  symbolic_file_size_bytes = ???

  # 为密码构建一个位向量,然后将其存储在文件的支撑内存中。
  # 例如,想象一个简单的文件 'hello.txt':
  #
  # Hello world, my name is John.
  # ^                       ^
  # ^ 地址 0                ^ 地址 24(计算字符数)
  # 为了在内存中表示这个,我们希望将字符串写入文件的开头:
  #
  # hello_txt_contents = claripy.BVV('Hello world, my name is John.', 30*8)
  #
  # 也许,然后我们希望用符号变量替换 John。
  # 我们会调用:
  #
  # name_bitvector = claripy.BVS('symbolic_name', 4*8)
  #
  # 然后,在程序调用 fopen('hello.txt', 'r') 然后
  # fread(buffer, sizeof(char), 30, hello_txt_file) 之后,缓冲区将包含
  # 来自文件的字符串,除了存储名称的四个符号字节。
  # (!)
  password = claripy.BVS('password', symbolic_file_size_bytes * 8)

  # 构建符号文件。file_options 参数指定 Linux 文件权限(读、读/写、执行等)。
  # content 参数指定应从何处提供数据流。
  # 如果 content 是 SimSymbolicMemory 的实例(我们在上面构建了一个),
  # 则该流将包含内存的内容(包括任何符号内容),从地址零开始。
  # 将 content 参数设置为我们保存符号数据的 BVS 实例。
  # (!)
  password_file = angr.storage.SimFile(filename, content=???)
  
  # 将我们创建的符号文件添加到符号文件系统中。
  initial_state.fs.insert(filename, password_file)

  simulation = project.factory.simgr(initial_state)

  def is_successful(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return ???

  def should_abort(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return ???

  simulation.explore(find=is_successful, avoid=should_abort)

  if simulation.found:
    solution_state = simulation.found[0]

    solution = solution_state.solver.eval(password,cast_to=bytes).decode()

    print(solution)
  else:
    raise Exception('Could not find the solution')

if __name__ == '__main__':
  main(sys.argv)

image

虽然说这个挑战貌似和scanf没什么关系,但是既然给了???那就出于习惯性跳一下(scanf和angr真是一对苦命鸳鸯),跳到0x0804944D

好了,说回正题,题目要求我们忽略ignore_me,这个函数的用途不难看出是把控制台输入全部重定向到文件里面,所以可以让它执行(pwn手看到这一堆似曾相识的函数~)
image

之后是构造虚拟文件


  # 指定构建模拟文件所需的一些信息。对于这个挑战,文件名是硬编码的,
  # 但理论上,它可以是符号化的。
  # 注意:为了从文件读取,二进制文件调用
  # 'fread(buffer, sizeof(char), 64, file)'。
  # (!)
  filename = ???  # :字符串
  symbolic_file_size_bytes = ???

  # 为密码构建一个位向量,然后将其存储在文件的支撑内存中。
  # 例如,想象一个简单的文件 'hello.txt':
  #
  # Hello world, my name is John.
  # ^                       ^
  # ^ 地址 0                ^ 地址 24(计算字符数)
  # 为了在内存中表示这个,我们希望将字符串写入文件的开头:
  #
  # hello_txt_contents = claripy.BVV('Hello world, my name is John.', 30*8)
  #
  # 也许,然后我们希望用符号变量替换 John。
  # 我们会调用:
  #
  # name_bitvector = claripy.BVS('symbolic_name', 4*8)
  #
  # 然后,在程序调用 fopen('hello.txt', 'r') 然后
  # fread(buffer, sizeof(char), 30, hello_txt_file) 之后,缓冲区将包含
  # 来自文件的字符串,除了存储名称的四个符号字节。
  # (!)
  password = claripy.BVS('password', symbolic_file_size_bytes * 8)

image

很显然文件名就是XTQAFOST.txt,但是symbolic_file_size_bytes是多少呢?


这里的注释有一点点误导性,稍微进行一下解释

  • claripy.BVV('Hello world, my name is John.', 30*8) 相当于我们的字面常量,例如:"Hello"123c,它在符号执行时候不变
  • claripy.BVS('symbolic_name', 4*8) 相当于我们的变量,在符号执行时候是变化的

例如说我们想要对一个参数id=114514进行测试,其中id=这部分我们不希望它变化,用BVV114514这部分我们希望进行符号执行就用BVS

也就是说,实际上注释的意思是:如果你有一个长度为30的字符串,从24开始需要可变,那么前半部分用BVV,后半部分用BVS。


回归正题,symbolic_file_size_bytes为文件长度,从fread(buffer,1,0x40,fp);可知,为0x40

  # 构建符号文件。file_options 参数指定 Linux 文件权限(读、读/写、执行等)。
  # content 参数指定应从何处提供数据流。
  # 如果 content 是 SimSymbolicMemory 的实例(我们在上面构建了一个),
  # 则该流将包含内存的内容(包括任何符号内容),从地址零开始。
  # 将 content 参数设置为我们保存符号数据的 BVS 实例。
  # (!)
  password_file = angr.storage.SimFile(filename, content=???)
  
  # 将我们创建的符号文件添加到符号文件系统中。
  initial_state.fs.insert(filename, password_file)

  simulation = project.factory.simgr(initial_state)

然后我们插入password,剩下的和之前的一样就不赘述了。然后开跑
image

嗯?怎么报错了?
image

分析调用栈堆可以知道:在ignore_me中存在一个函数fscanf,现在阶段angr对其的支持并不好

image

所以方法是:直接从ignore_me后面开始,即0x0804945F

image

这样就可以了,再处理一下编码问题就好了

image

最后脚本:

import angr
import claripy
import sys
import logging


def main(argv):
    path_to_binary = argv[1]
    project = angr.Project(path_to_binary)

    start_address = 0x804945F # 从ignore_me后面开始
    initial_state = project.factory.blank_state(
        addr=start_address,
        add_options={angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                     angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
    )

    filename = "XTQAFOST.txt"
    symbolic_file_size_bytes = 0x40


    password = claripy.BVS('password', symbolic_file_size_bytes * 8)


    password_file = angr.SimFile(filename, content=password)  # 改用新版方法 angr.SimFile

    initial_state.fs.insert(filename, password_file)

    simulation = project.factory.simgr(initial_state)

    def is_successful(state):
        stdout_output = state.posix.dumps(sys.stdout.fileno())
        return b'Good Job.' in stdout_output

    def should_abort(state):
        stdout_output = state.posix.dumps(sys.stdout.fileno())
        return b'Try again.' in stdout_output

    simulation.explore(find=is_successful, avoid=should_abort)

    if simulation.found:
        solution_state = simulation.found[0]

        solution = solution_state.solver.eval(password, cast_to=bytes) # 去除编码处理

        print(solution)
    else:
        raise Exception('Could not find the solution')


if __name__ == '__main__':
    # logging.getLogger("angr").setLevel(logging.DEBUG)
    # logging.getLogger("claripy").setLevel(logging.DEBUG)
    main(sys.argv)

讲真的上面那个崩溃我查了2天,还以为是我使用方法有问题。。。

十一、08_angr_constraints

image

# 这个二进制程序要求输入一个 16 字节的密码,然后对其应用一个复杂的
# 函数(complex_function),最后把结果与参考字符串进行比较,
# 比较方法是调用名称为 check_equals_[参考字符串] 的函数。
# (反编译该二进制文件并查看其中内容!)
# 下面提供了这个函数的源码。然而,你的二进制中的参考字符串
# 会与此处的 AABBCCDDEEFFGGHH 不同:
#
# #define REFERENCE_PASSWORD = "AABBCCDDEEFFGGHH";
# int check_equals_AABBCCDDEEFFGGHH(char* to_check, size_t length) {
#   uint32_t num_correct = 0;
#   for (int i=0; i<length; ++i) {
#     if (to_check[i] == REFERENCE_PASSWORD[i]) {
#       num_correct += 1;
#     }
#   }
#   return num_correct == length;
# }
#
# ...
#
# char* input = user_input();
# char* encrypted_input = complex_function(input);
# if (check_equals_AABBCCDDEEFFGGHH(encrypted_input, 16)) {
#   puts("Good Job.");
# } else {
#   puts("Try again.");
# }
#
# 这个函数会检查 *to_check 是否等于 "AABBCCDDEEFFGGHH"。你可以自己验证。
# 虽然你作为人类能轻松看出这个函数实质就是字符串比较,但计算机做不到。
# 计算机会在循环中的每一次 if 判断处分叉(共 16 次),
# 导致分支数量为 2^16 = 65,536,搜索量太大,不适合我们现在的需求。
#
# 我们不知道 complex_function 的具体工作原理,但我们希望找到一个输入,
# 使得 complex_function 对其处理后输出的字符串是:
# AABBCCDDEEFFGGHH。
#
# 在这个题中,你的目标是在这个比较函数被调用之前暂停程序,
# 并手动约束 to_check 变量,使其等于你通过反编译确定的正确密码。
# 由于你作为人类知道,只要字符串匹配,程序就会打印 "Good Job.",
# 因此你可以确信,如果系统能求解出一个输入,使得二者相等,
# 那么这个输入就是正确密码。


import angr
import claripy
import sys

def main(argv):
  path_to_binary = argv[1]
  project = angr.Project(path_to_binary)

  start_address = ???
  initial_state = project.factory.blank_state(
    addr=start_address,
    add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                    angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
  )

  password = claripy.BVS('password', ???)

  password_address = ???
  initial_state.memory.store(password_address, password)

  simulation = project.factory.simgr(initial_state)

  # Angr 将无法执行到二进制实际打印 'Good Job.' 的位置。
  # 因此我们不能再把那里作为目标地址。
  # (!)
  address_to_check_constraint = ???
  simulation.explore(find=address_to_check_constraint)

  if simulation.found:
    solution_state = simulation.found[0]

    # 回顾上文:我们需要约束 check_equals 函数的 to_check 参数。
    # 请确定此参数在内存中的地址,并将其加载为位向量(bitvector),
    # 以便我们对其施加约束。
    # (!)
    constrained_parameter_address = ???
    constrained_parameter_size_bytes = ???
    constrained_parameter_bitvector = solution_state.memory.load(
      constrained_parameter_address,
      constrained_parameter_size_bytes
    )
    # 我们希望约束系统,使其找到一个输入,
    # 使 constrained_parameter_bitvector 等于期望值。
    # (!)
    constrained_parameter_desired_value = ??? # :string (encoded)

    # 使用 claripy 表达式(Python 写法)描述
    # constrained_parameter_bitvector == constrained_parameter_desired_value。
    # 将这个约束加入状态,让 z3 尝试找到满足此表达式的输入。
    solution_state.add_constraints(constrained_parameter_bitvector == constrained_parameter_desired_value)

    # 求解 constrained_parameter_bitvector。
    # (!)
    solution = ???

    print(solution)
  else:
    raise Exception('Could not find the solution')

if __name__ == '__main__':
  main(sys.argv)


为什么符号执行这么厉害,却没在ctf大量使用呢?一是交不出WP(你总不能说我直接跑FUZZ爆破出来或者符号执行出来的吧)二是其本身存在限制,多来几个分支就炸了,简单16个if即可抬走。

所以说一般用于:

  1. CTF竞赛和逆向工程,欺负欺负简单的题目还是没问题的
  2. 漏洞挖掘和利用,用于发现漏洞函数后反向找到能够触发的数据
  3. 恶意软件分析
    • 找payload解密或找加载
    • 分析反调试绕过条件
    • 分析加密逻辑,反向提取解密逻辑
    • 提取C2配置,想办法识别合法输入
  4. 程序验证和测试用例生成(人话:生成能到达目标分支的输入)

符号执行更像是一把精准的手术刀


说回题目,首先跳一下scanf,从0x080492E0 循环控制开,然后注入符号到buffer0x0804C028
image

至于password不用管它,反正我们不会执行到它

def main(argv):
  path_to_binary = argv[1]
  project = angr.Project(path_to_binary)

  start_address = 0x080492E0
  initial_state = project.factory.blank_state(
    addr=start_address,
    add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                    angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
  )

  password = claripy.BVS('password', 16 * 8)

  password_address = 0x0804C028
  initial_state.memory.store(password_address, password)

  simulation = project.factory.simgr(initial_state)

然后我们设定目标在0x0804932E

image

  address_to_check_constraint = 0x0804932E
  simulation.explore(find=address_to_check_constraint)

下一步是确定需要约束check_equals函数的to_check参数,也就是buffer地址,很明显是:0x0804C028,大小为16,并且我们期望其等于b"XCKPBIWXXTQAFOST"

    constrained_parameter_address = 0x0804C028
    constrained_parameter_size_bytes = 16
    constrained_parameter_bitvector = solution_state.memory.load(
      constrained_parameter_address,
      constrained_parameter_size_bytes
    )
    constrained_parameter_desired_value = b"XCKPBIWXXTQAFOST" # :string (encoded)

最后求解一下:

    solution = solution_state.solver.eval(password, cast_to=bytes)
    print(solution)

image

完整代码:

import angr
import claripy
import sys

def main(argv):
  path_to_binary = argv[1]
  project = angr.Project(path_to_binary)

  start_address = 0x080492E0
  initial_state = project.factory.blank_state(
      addr=start_address,
      add_options={angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                   angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
  )

  password = claripy.BVS('password', 16 * 8)

  password_address = 0x0804C028
  initial_state.memory.store(password_address, password)

  simulation = project.factory.simgr(initial_state)

  address_to_check_constraint = 0x0804932E
  simulation.explore(find=address_to_check_constraint)

  if simulation.found:
    solution_state = simulation.found[0]
    constrained_parameter_address = 0x0804C028
    constrained_parameter_size_bytes = 16
    constrained_parameter_bitvector = solution_state.memory.load(
      constrained_parameter_address,
      constrained_parameter_size_bytes
    )
    constrained_parameter_desired_value = b"XCKPBIWXXTQAFOST"
    solution_state.add_constraints(constrained_parameter_bitvector == constrained_parameter_desired_value)
    solution = solution_state.solver.eval(password, cast_to=bytes)
    print(solution)

  else:
    raise Exception('Could not find the solution')

if __name__ == '__main__':
  main(sys.argv)

十二、09_angr_hooks

image

# 本关卡执行以下计算:
#
# 1. 获取16字节用户输入并加密
# 2. 保存 check_equals_AABBCCDDEEFFGGHH(或类似函数)的结果
# 3. 再从用户获取16字节并加密
# 4. 检查是否等于预定义密码
#
# 我们唯一需要关注的是第2步。由于 check_equals_ 函数运行太慢,
# 我们将通过钩子替换对 check_equals_ 的调用

import angr
import claripy
import sys

def main(argv):
  path_to_binary = argv[1]
  project = angr.Project(path_to_binary)

  # 由于Angr可以处理初始的scanf调用,我们可以从程序开始处执行
  initial_state = project.factory.entry_state(
    add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                    angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
  )

  # 钩住调用 check_equals_ 的地址
  # (!)
  check_equals_called_address = ???

  # angr.Hook 中的 length 参数指定执行引擎在完成钩子后应跳过的字节数。
  # 这将允许钩子替换某些指令(或指令组)。确定调用 check_equals_ 所涉及的指令,
  # 然后确定在内存中表示这些指令需要多少字节。这将是跳过的长度。
  # (!)
  instruction_to_skip_length = ???
  @project.hook(check_equals_called_address, length=instruction_to_skip_length)
  def skip_check_equals_(state):
    # 确定存储用户输入的地址。它作为参数传递给 check_equals_ 函数。
    # 然后加载字符串。提醒:
    # int check_equals_(char* to_check, int length) { ...
    user_input_buffer_address = ??? # :整数,可能是十六进制
    user_input_buffer_length = ???

    # 提醒:state.memory.load 将读取地址 user_input_buffer_address 处
    # 存储的字节长度为 user_input_buffer_length 的值。
    # 它将返回一个包含该值的位向量。该值可以是符号值或具体值,
    # 具体取决于程序中存储的内容。
    user_input_string = state.memory.load(
      user_input_buffer_address,
      user_input_buffer_length
    )
    
    # 确定该函数要检查的字符串。
    # 它编码在函数名中;反编译程序以找到它。
    check_against_string = ??? # :字符串

    # gcc 使用 eax 存储返回值(如果是整数)。我们需要设置 eax:
    # 如果 check_against_string == user_input_string 则为1,否则为0。
    # 但是,由于我们描述的是要由 z3 使用的等式(不是立即求值),
    # 因此不能使用 Python 的 if else 语法。相反,我们必须使用 
    # claripy 的内置函数来处理 if 语句。
    # claripy.If(表达式, 真时返回值, 假时返回值) 将输出一个表达式,
    # 如果表达式为真则求值为真时返回值,否则为假时返回值。
    # 可以把它想象成 Python 的 "值1 if 表达式 else 值2"。
    state.regs.eax = claripy.If(
      user_input_string == check_against_string, 
      claripy.BVV(1, 32), 
      claripy.BVV(0, 32)
    )

  simulation = project.factory.simgr(initial_state)

  def is_successful(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return ???

  def should_abort(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return ???

  simulation.explore(find=is_successful, avoid=should_abort)

  if simulation.found:
    solution_state = simulation.found[0]

    # 由于我们允许 Angr 处理输入,通过打印 stdin 的内容来检索它。
    # 使用之前的某个关卡作为参考。
    solution = ???
    print(solution)
  else:
    raise Exception('找不到解决方案')

if __name__ == '__main__':
  main(sys.argv)

终于不用处理scanf了~

我们查看地址,在0x0804933E处的函数会占用很多时间,那么我们要跳过它
image

我们从函数准备开始跳过,跳到函数清理的地方,所以我们hook开始的地方是:0x08049334,长度是0x43 - 0x34
image

然后根据提示提取buffer的地址和长度0x0804C02C,16

image

下一步就是确定要比较的字符串,反编译结果来看是XCKPBIWXXTQAFOST,接下来就是从标准输入输出读出来了

    def is_successful(state):
        stdout_output = state.posix.dumps(sys.stdout.fileno())
        return b"Good Job." in stdout_output

    def should_abort(state):
        stdout_output = state.posix.dumps(sys.stdout.fileno())
        return b"Try again." in stdout_output

    simulation.explore(find=is_successful, avoid=should_abort)

    if simulation.found:
        solution_state = simulation.found[0]
        print(solution_state.posix.dumps(sys.stdin.fileno()).decode())
    else:
        raise Exception('Could not find the solution')

image

import angr
import claripy
import sys

def main(argv):
    path_to_binary = argv[1]
    project = angr.Project(path_to_binary)
    initial_state = project.factory.entry_state(
        add_options={angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                     angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
    )

    check_equals_called_address = 0x08049334
    instruction_to_skip_length = 0x43 - 0x34

    @project.hook(check_equals_called_address, length=instruction_to_skip_length)
    def skip_check_equals_(state):
        user_input_buffer_address = 0x0804C02C
        user_input_buffer_length = 16
        user_input_string = state.memory.load(
            user_input_buffer_address,
            user_input_buffer_length
        )
        check_against_string = "XCKPBIWXXTQAFOST"  # :string

        state.regs.eax = claripy.If(
            user_input_string == check_against_string,
            claripy.BVV(1, 32),
            claripy.BVV(0, 32)
        )

    simulation = project.factory.simgr(initial_state)

    def is_successful(state):
        stdout_output = state.posix.dumps(sys.stdout.fileno())
        return b"Good Job." in stdout_output

    def should_abort(state):
        stdout_output = state.posix.dumps(sys.stdout.fileno())
        return b"Try again." in stdout_output

    simulation.explore(find=is_successful, avoid=should_abort)

    if simulation.found:
        solution_state = simulation.found[0]
        print(solution_state.posix.dumps(sys.stdin.fileno()).decode())
    else:
        raise Exception('Could not find the solution')


if __name__ == '__main__':
    main(sys.argv)

十三、10_angr_simprocedures

image

# 本关卡与前一关类似,基于相同的前提:你需要替换 check_equals_ 函数。
# 但在这个例子中,check_equals_ 被调用了太多次,为每个调用位置设置钩子没有意义。
# 相反,使用 SimProcedure 来编写你自己的 check_equals_ 实现,然后钩住 check_equals_ 符号,
# 用对你的 SimProcedure 的调用来替换所有对 scanf 的调用。
#
# 你可能会想:
#   为什么不能只用钩子?函数被调用了很多次,但如果我钩住函数本身的地址
#   (而不是它被调用的地址),我就可以在所有地方替换它的行为。此外,我可以通过
#   读取栈上的值(使用 memory.load(regs.esp + xx))来获取参数,并且只需设置 eax 来返回值!
#   由于我知道函数的字节长度,我可以在 'ret' 指令被调用之前从钩子返回,
#   这将允许程序跳转回调用我的钩子之前的位置。
# 如果你这么想了,那么恭喜!你刚刚想到了 SimProcedures 的概念!
# 与其手动完成所有这些工作,你可以让已经实现的 SimProcedures 为你完成枯燥的工作,
# 这样你就可以专注于以 Pythonic 的方式编写替换函数。
# 另外,SimProcedures 允许你指定自定义调用约定,但不幸的是本次 CTF 不涵盖这一点。

import angr
import claripy
import sys

def main(argv):
  path_to_binary = argv[1]
  project = angr.Project(path_to_binary)

  initial_state = project.factory.entry_state(
    add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                    angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
  )

  # 定义一个继承 angr.SimProcedure 的类,以便利用 Angr 的 SimProcedures
  class ReplacementCheckEquals(angr.SimProcedure):
    # SimProcedure 用 Python 编写的模拟函数替换二进制文件中的函数。
    # 除了是用 Python 编写的之外,该函数的行为与用 C 编写的任何函数基本相同。
    # 'self' 之后的任何参数都将被视为你要替换的函数的参数。
    # 参数将是位向量。此外,Python 可以用通常的 Pythonic 方式返回。
    # Angr 将以处理二进制文件中本地函数返回的相同方式处理这种情况。例如:
    #
    # int add_if_positive(int a, int b) {
    #   if (a >= 0 && b >= 0) return a + b;
    #   else return 0;
    # }
    #
    # 可以用以下方式模拟...
    #
    # class ReplacementAddIfPositive(angr.SimProcedure):
    #   def run(self, a, b):
    #     if a >= 0 and b >=0:
    #       return a + b
    #     else:
    #       return 0
    #
    # 完成 check_equals_ 函数的参数。提醒:
    # int check_equals_AABBCCDDEEFFGGHH(char* to_check, int length) { ...
    # (!)
    def run(self, to_check, ...???):
      # 我们几乎可以复制并粘贴前一关的解决方案。
      # 提示:不要查找地址!它作为参数传递。
      # (!)
      user_input_buffer_address = ???
      user_input_buffer_length = ???

      # 注意使用 self.state 在 SimProcedure 中查找系统状态
      user_input_string = self.state.memory.load(
        user_input_buffer_address,
        user_input_buffer_length
      )

      check_against_string = ???
      
      # 最后,我们可以使用 Pythonic 的 return 语句来返回这个函数的输出,
      # 而不是设置 eax
      # 提示:参考之前的解决方案
      return claripy.If(???, ???, ???)


  # 钩住 check_equals 符号。Angr 会自动查找与符号关联的地址。
  # 或者,你可以使用 'hook' 而不是 'hook_symbol' 并指定函数的地址。
  # 要找到正确的符号,请反汇编二进制文件。
  # (!)
  check_equals_symbol = ??? # :字符串
  project.hook_symbol(check_equals_symbol, ReplacementCheckEquals())

  simulation = project.factory.simgr(initial_state)

  def is_successful(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return ???

  def should_abort(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return ???

  simulation.explore(find=is_successful, avoid=should_abort)

  if simulation.found:
    solution_state = simulation.found[0]

    solution = ???
    print(solution)
  else:
    raise Exception('找不到解决方案')

if __name__ == '__main__':
  main(sys.argv)

本质上就是hook指定地址变成了hook指定函数,和上一题做法基本一样,就不多赘述

image

import angr
import claripy
import sys

def main(argv):
    path_to_binary = argv[1]
    project = angr.Project(path_to_binary)

    initial_state = project.factory.entry_state(
        add_options={angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                     angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
    )

    class ReplacementCheckEquals(angr.SimProcedure):
        def run(self, to_check, length):
            user_input_buffer_address = to_check
            user_input_buffer_length = length

            user_input_string = self.state.memory.load(
                user_input_buffer_address,
                user_input_buffer_length
            )

            check_against_string = "XCKPBIWXXTQAFOST"

            return claripy.If(
                user_input_string == check_against_string,
                claripy.BVV(1, 32),
                claripy.BVV(0, 32)
            )


    check_equals_symbol = "check_equals_XCKPBIWXXTQAFOST"  # :string
    project.hook_symbol(check_equals_symbol, ReplacementCheckEquals())

    simulation = project.factory.simgr(initial_state)

    def is_successful(state):
        stdout_output = state.posix.dumps(sys.stdout.fileno())
        return b"Good Job." in stdout_output

    def should_abort(state):
        stdout_output = state.posix.dumps(sys.stdout.fileno())
        return b"Try again." in stdout_output

    simulation.explore(find=is_successful, avoid=should_abort)

    if simulation.found:
        solution_state = simulation.found[0]

        print(solution_state.posix.dumps(sys.stdin.fileno()).decode())
    else:
        raise Exception('Could not find the solution')

if __name__ == '__main__':
    main(sys.argv)

十四、11_angr_sim_scanf

image

# 这次解决方案只涉及用我们自己的版本替换 scanf,
# 因为 Angr 不支持使用 scanf 请求多个参数。

import angr
import claripy
import sys

def main(argv):
  path_to_binary = argv[1]
  project = angr.Project(path_to_binary)

  initial_state = project.factory.entry_state(
    add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                    angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
  )

  class ReplacementScanf(angr.SimProcedure):
    # 完成 scanf 函数的参数。提示:'scanf("%u %u", ...)'。
    # (!)
    def run(self, format_string, scanf0_address, ...):
      # 提示:scanf0_address 是作为参数传递的,对吧?
      scanf0 = claripy.BVS('scanf0', ???)
      ...

      # scanf 函数将用户输入写入参数所指向的缓冲区
      self.state.memory.store(scanf0_address, scanf0, endness=project.arch.memory_endness)
      ...

      # 现在,我们想要在状态默认包含的 globals 插件中"保存"
      # 对我们的符号值的引用。你需要存储多个位向量。
      # 你可以使用列表、元组或多个键来引用不同的位向量。
      # (!)
      self.state.globals['solution0'] = ???
      ...

  scanf_symbol = ???
  project.hook_symbol(scanf_symbol, ReplacementScanf())

  simulation = project.factory.simgr(initial_state)

  def is_successful(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return ???

  def should_abort(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return ???

  simulation.explore(find=is_successful, avoid=should_abort)

  if simulation.found:
    solution_state = simulation.found[0]

    # 获取你在 globals 字典中保存的内容
    stored_solutions0 = solution_state.globals['solution0']
    ...
    solution = ???

    print(solution)
  else:
    raise Exception('找不到解决方案')

if __name__ == '__main__':
  main(sys.argv)

补豪孩子们!scanf又回来力!但是没事我们来肘爆它,根据提示hook它:

    class ReplacementScanf(angr.SimProcedure):
        def run(self, format_string, scanf0_address, scanf1_address):
            scanf0 = claripy.BVS('scanf0', 32)
            scanf1 = claripy.BVS('scanf1', 32)

            self.state.memory.store(
                scanf0_address, scanf0, endness=project.arch.memory_endness)
            self.state.memory.store(
                scanf1_address, scanf1, endness=project.arch.memory_endness)

            self.state.globals['solution0'] = scanf0
            self.state.globals['solution1'] = scanf1

    scanf_symbol = "__isoc99_scanf"
    project.hook_symbol(scanf_symbol, ReplacementScanf())

然后判断+输出

    def is_successful(state):
        stdout_output = state.posix.dumps(sys.stdout.fileno())
        return b"Good Job." in stdout_output

    def should_abort(state):
        stdout_output = state.posix.dumps(sys.stdout.fileno())
        return b"Try again." in stdout_output

    simulation.explore(find=is_successful, avoid=should_abort)

    if simulation.found:
        solution_state = simulation.found[0]

        # Grab whatever you set aside in the globals dict.
        stored_solutions0 = solution_state.globals['solution0']
        stored_solutions1 = solution_state.globals['solution1']
        solution0 = solution_state.solver.eval(stored_solutions0,cast_to=int)
        solution1 = solution_state.solver.eval(stored_solutions1, cast_to=int)

        print(f"{solution0} {solution1}")
    else:
        raise Exception('Could not find the solution')

和上一题差不多,只不过多了全局变量这个好东西

# This time, the solution involves simply replacing scanf with our own version,
# since Angr does not support requesting multiple parameters with scanf.

import angr
import claripy
import sys


def main(argv):
    path_to_binary = argv[1]
    project = angr.Project(path_to_binary)

    initial_state = project.factory.entry_state(
        add_options={angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                     angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
    )

    class ReplacementScanf(angr.SimProcedure):
        def run(self, format_string, scanf0_address, scanf1_address):
            scanf0 = claripy.BVS('scanf0', 32)
            scanf1 = claripy.BVS('scanf1', 32)

            self.state.memory.store(
                scanf0_address, scanf0, endness=project.arch.memory_endness)
            self.state.memory.store(
                scanf1_address, scanf1, endness=project.arch.memory_endness)

            self.state.globals['solution0'] = scanf0
            self.state.globals['solution1'] = scanf1

    scanf_symbol = "__isoc99_scanf"
    project.hook_symbol(scanf_symbol, ReplacementScanf())

    simulation = project.factory.simgr(initial_state)

    def is_successful(state):
        stdout_output = state.posix.dumps(sys.stdout.fileno())
        return b"Good Job." in stdout_output

    def should_abort(state):
        stdout_output = state.posix.dumps(sys.stdout.fileno())
        return b"Try again." in stdout_output

    simulation.explore(find=is_successful, avoid=should_abort)

    if simulation.found:
        solution_state = simulation.found[0]

        stored_solutions0 = solution_state.globals['solution0']
        stored_solutions1 = solution_state.globals['solution1']
        solution0 = solution_state.solver.eval(stored_solutions0,cast_to=int)
        solution1 = solution_state.solver.eval(stored_solutions1, cast_to=int)

        print(f"{solution0} {solution1}")
    else:
        raise Exception('Could not find the solution')


if __name__ == '__main__':
    main(sys.argv)

image

十五、12_angr_veritesting

image

题目也是越来越扭曲了,唉

# 当你构建模拟管理器时,你会想要启用 Veritesting:
# project.factory.simgr(initial_state, veritesting=True)
# 提示:使用前几个关卡的解决方案作为参考。

然后,我参考呢o.O,唉,U1.4了,开始肘吧,我首先尝试了官方脚本类似的思路

import angr
import sys

def main(argv):
  path_to_binary = argv[1]
  project = angr.Project(path_to_binary)
  initial_state = project.factory.entry_state(
    add_options = { angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                    angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
  )
  simulation = project.factory.simgr(initial_state, veritesting=True)

  def is_successful(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return 'Good Job.'.encode() in stdout_output  # :boolean

  def should_abort(state):
    stdout_output = state.posix.dumps(sys.stdout.fileno())
    return 'Try again.'.encode() in stdout_output  # :boolean

  simulation.explore(find=is_successful, avoid=should_abort)

  if simulation.found:
    solution_state = simulation.found[0]
    print(solution_state.posix.dumps(sys.stdin.fileno()).decode())
  else:
    raise Exception('Could not find the solution')

if __name__ == '__main__':
  main(sys.argv)

也没跑出来,给了64gb内存也不行,老是被杀死,而且内存爆炸,于是想了一下,路径爆炸的原因主要在于

  for ( i = 0; i <= 31; ++i )
  {
    v3 = *((char *)v19 + i + 3);
    if ( v3 == complex_function(88, i + 37) ) # 这里
      ++n32;
  }

具体来说:

int __cdecl complex_function(int n88, int a2)
{
  if ( n88 <= 64 || n88 > 90 )
  {
    puts("Try again.");
    exit(1);
  }
  return (31 * a2 + n88 - 65) % 26 + 65;
}

我的想法是hook这个complex_function,反正返回的是一个确定值,那就手动模拟,再把不满足的分路直接舍弃


    # Hook complex_function 来防止路径爆炸
    class ComplexFunctionHook(angr.SimProcedure):
        def run(self, n88, a2):
            # 具体化符号值来避免路径爆炸
            n88_val = self.state.solver.eval(n88)
            a2_val = self.state.solver.eval(a2)

            if n88_val <= 64 or n88_val > 90:
                # 不满足条件,放弃此分支
                self.state.regs.ip = 0  # 跳转到无效地址直接结束寻找
                return claripy.BVV(0, 32)  # 返回0作为失败值
            else:
                # 满足条件,返回具体计算结果
                result = (31 * a2_val + n88_val - 65) % 26 + 65
                return claripy.BVV(result, 32)

    project.hook_symbol("complex_function", ComplexFunctionHook())

    initial_state = project.factory.entry_state(
        add_options={angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                     angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
    )
    simulation = project.factory.simgr(initial_state, veritesting=True)

一下子就出来了
image

import angr
import claripy
import sys

def main(argv):
    path_to_binary = argv[1]
    project = angr.Project(path_to_binary)

    # Hook complex_function 来防止路径爆炸
    class ComplexFunctionHook(angr.SimProcedure):
        def run(self, n88, a2):
            # 具体化符号值来避免路径爆炸
            n88_val = self.state.solver.eval(n88)
            a2_val = self.state.solver.eval(a2)

            # 检查条件
            if n88_val <= 64 or n88_val > 90:
                self.state.regs.ip = 0  # 跳转到无效地址
                return claripy.BVV(0, 32)  # 返回0作为失败值
            else:
                # 满足条件,返回具体计算结果
                result = (31 * a2_val + n88_val - 65) % 26 + 65
                return claripy.BVV(result, 32)

    project.hook_symbol("complex_function", ComplexFunctionHook())

    initial_state = project.factory.entry_state(
        add_options={angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                     angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
    )
    simulation = project.factory.simgr(initial_state, veritesting=True)

    def is_successful(state):
        stdout_output = state.posix.dumps(sys.stdout.fileno())
        return 'Good Job.'.encode() in stdout_output  # :boolean

    def should_abort(state):
        stdout_output = state.posix.dumps(sys.stdout.fileno())
        return 'Try again.'.encode() in stdout_output

    simulation.explore(find=is_successful, avoid=should_abort)

    if simulation.found:
        solution_state = simulation.found[0]
        print(solution_state.posix.dumps(sys.stdin.fileno()).decode())
    else:
        raise Exception('Could not find the solution')


if __name__ == '__main__':
    main(sys.argv)

十六、13_angr_static_binary

image

# 本挑战与第一个挑战完全相同,唯一的区别是它被编译成了一个静态二进制程序。
# 通常情况下,Angr 会自动用 SimProcedure 来替换标准库函数,
# 这些 SimProcedure 的执行速度要快得多。
#
# 为了解决这个挑战,你需要**手动 hook** 二进制中用到的标准 C 库函数。
# 然后,确保你是从 main 函数的起始位置开始执行。
# 不要使用 entry_state。
#
# 下面是 Angr 已经为你写好的一些 SimProcedure,
# 它们实现了标准库中的函数。你不一定会用到全部:
# angr.SIM_PROCEDURES['libc']['malloc']
# angr.SIM_PROCEDURES['libc']['fopen']
# angr.SIM_PROCEDURES['libc']['fclose']
# angr.SIM_PROCEDURES['libc']['fwrite']
# angr.SIM_PROCEDURES['libc']['getchar']
# angr.SIM_PROCEDURES['libc']['strncmp']
# angr.SIM_PROCEDURES['libc']['strcmp']
# angr.SIM_PROCEDURES['libc']['scanf']
# angr.SIM_PROCEDURES['libc']['printf']
# angr.SIM_PROCEDURES['libc']['puts']
# angr.SIM_PROCEDURES['libc']['exit']
#
# 提醒一下,你可以使用类似下面的方式来 hook 函数:
# project.hook(malloc_address, angr.SIM_PROCEDURES['libc']['malloc']())
#
# 还有更多的 SimProcedure,可以参考:
# https://github.com/angr/angr/tree/master/angr/procedures/libc
#
# 另外请注意,当该二进制程序被执行时,
# main 函数并不是第一个被调用的代码。
# 在 _start 函数中,会先调用 __libc_start_main 来启动你的程序。
# 这个函数中包含的初始化过程在 Angr 中执行会非常耗时,
# 因此你应该用一个 SimProcedure 将它替换掉:
# angr.SIM_PROCEDURES['glibc']['__libc_start_main']
# 注意这里是 'glibc',而不是 'libc'。

做这道题做到一堆坑,一步步说详细一点,首先根据提示不用entry_state,直接从main开始

    path_to_binary = "./A"
    project = angr.Project(path_to_binary)
    # 使用 blank_state 而不是 entry_state,从 main 函数开始
    initial_state = project.factory.blank_state(
        addr=project.loader.find_symbol('main').rebased_addr,
        add_options={angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                     angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
    )

    simulation = project.factory.simgr(initial_state)

然后就是挂载函数

    project.hook(0x8049ED0,angr.SIM_PROCEDURES['glibc']['__libc_start_main']())
    project.hook_symbol("printf", angr.SIM_PROCEDURES['libc']['printf']())
    project.hook_symbol("__isoc99_scanf", angr.SIM_PROCEDURES['libc']['scanf']())
    project.hook_symbol('strcmp_ifunc', angr.SIM_PROCEDURES['libc']['strcmp']())
    project.hook_symbol("puts", angr.SIM_PROCEDURES['libc']['puts']())

这里就开始踩坑了,首先符号表不存在__libc_start_main以及_isoc99_scanf(注意这里少了一个下划线)

但是反编译出来就是只有一个下划线,可能是反编译错误
image

其它的就是对着函数名一顿挂载即可。

import sys
import angr
import claripy


def main(argv):
    path_to_binary = "./A"
    project = angr.Project(path_to_binary)
    # 使用 blank_state 而不是 entry_state,从 main 函数开始
    initial_state = project.factory.blank_state(
        addr=project.loader.find_symbol('main').rebased_addr,
        add_options={angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                     angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
    )

    simulation = project.factory.simgr(initial_state)

    project.hook(0x8049ED0,
                 angr.SIM_PROCEDURES['glibc']['__libc_start_main']())
    project.hook_symbol("printf", angr.SIM_PROCEDURES['libc']['printf']())
    project.hook_symbol(
        "__isoc99_scanf", angr.SIM_PROCEDURES['libc']['scanf']())
    project.hook_symbol(
        'strcmp_ifunc', angr.SIM_PROCEDURES['libc']['strcmp']())
    project.hook_symbol("puts", angr.SIM_PROCEDURES['libc']['puts']())

    print_good_address = 0x8049A65
    simulation.explore(find=print_good_address)

    if simulation.found:
        solution_state = simulation.found[0]
        print(solution_state.posix.dumps(sys.stdin.fileno()).decode())
    else:
        raise Exception('Could not find the solution')


if __name__ == '__main__':
    main(sys.argv)

image

十七 14_angr_shared_library

首先使用生成命令

python generate.py 123 A

image

发现出错,阅读generate.py 发现要用:

mkdir output
python generate.py 123 output/A

即可
image

这一次生成了两个文件,全部拷贝下来看看
image
image

欸,开始打动态库了,后面是不是要打内核?

# 共享库中包含 validate 函数,该函数接收一个字符串并返回
# true (1) 或 false (0)。二进制程序会调用此函数。如果返回
# true,程序将打印 "Good Job.",否则打印 "Try again。"
#
# 注意:当你运行此脚本时,请确保运行在
# lib14_angr_shared_library.so 上,而不是可执行文件上。本关旨在
# 教授如何分析非典型可执行文件的二进制格式。

import angr
import claripy
import sys

def main(argv):
  path_to_binary = ???  # 二进制文件路径

  # 共享库是使用位置无关代码编译的。你需要
  # 指定基地址。共享库中的所有地址将是
  # base + offset,其中 offset 是它们在文件中的地址。
  # (!)
  base = ???  # 基地址
  project = angr.Project(path_to_binary, load_options={
    'main_opts' : {
      'base_addr' : base
    }
  })

  # 在此初始化任何符号值;你至少需要一个传递给
  # validate 函数。
  # (!)
  buffer_pointer = claripy.BVV(???, ???)  # 缓冲区指针

  # 从 validate 函数的开始处初始化状态,就像它被
  # 程序调用一样。确定调用 validate 所需的参数并
  # 用包含你希望传递的值的位向量替换 'parameters...'。
  # 回忆一下,'claripy.BVV(value, size_in_bits)' 构造一个
  # 初始化为单个值的位向量。
  # 记住将你开始时指定的基值加到函数地址上!
  # 提示:int validate(char* buffer, int length) { ...
  # (!)
  validate_function_address = base + ???  # validate 函数地址
  initial_state = project.factory.call_state(
                    validate_function_address,
                    buffer_pointer,
                    ???  # 长度参数
                  )

  # 向程序中注入密码缓冲区的符号值并
  # 实例化模拟器。另一个提示:密码长度为 8 字节。
  # (!)
  password = claripy.BVS( ???, ??? )  # 密码符号变量
  initial_state.memory.store( ??? , ???)  # 存储密码到内存
  
  simulation = project.factory.simgr(initial_state)

  # 我们希望到达 validate 函数的末尾,并约束
  # 函数的返回值(存储在 eax 中)等于 true(值 1)
  # 就在函数返回之前。我们可以使用钩子,但相反我们
  # 可以搜索函数返回前的地址,然后
  # 约束 eax
  # (!)
  check_constraint_address = base + ???  # 检查约束的地址
  simulation.explore(find=check_constraint_address)

  if simulation.found:
    solution_state = simulation.found[0]

    # 确定程序放置返回值的位置,并约束它
    # 使其为 true。然后,求解解决方案并打印它。
    # (!)
    solution_state.add_constraints( ??? )  # 添加约束
    solution = ???  # 解决方案
    print(solution)
  else:
    raise Exception('Could not find the solution')  # 无法找到解决方案

if __name__ == '__main__':
  main(sys.argv)

这道题的引导有亿点坑,我慢慢来说

def main(argv):
  path_to_binary = ???  # 二进制文件路径

  # 共享库是使用位置无关代码编译的。你需要
  # 指定基地址。共享库中的所有地址将是
  # base + offset,其中 offset 是它们在文件中的地址。
  # (!)
  base = ???  # 基地址
  project = angr.Project(path_to_binary, load_options={
    'main_opts' : {
      'base_addr' : base
    }
  })
  1. path_to_binary要传入的不再是ELF的路径,而是lib的路径,所以填入./output/A
  2. base = ??? # 基地址 填一个合法的自选值即可,不是找出来的,你可以简单理解为手动指定PIE地址,一般来说我们填入0x400000即可,这是第一个坑

然后第二个坑来了:

# 在此初始化任何符号值;你至少需要一个传递给
  # validate 函数。
  # (!)
  buffer_pointer = claripy.BVV(???, ???)  # 缓冲区指针

  # 从 validate 函数的开始处初始化状态,就像它被
  # 程序调用一样。确定调用 validate 所需的参数并
  # 用包含你希望传递的值的位向量替换 'parameters...'。
  # 回忆一下,'claripy.BVV(value, size_in_bits)' 构造一个
  # 初始化为单个值的位向量。
  # 记住将你开始时指定的基值加到函数地址上!
  # 提示:int validate(char* buffer, int length) { ...
  # (!)
  validate_function_address = base + ???  # validate 函数地址
  initial_state = project.factory.call_state(
                    validate_function_address,
                    buffer_pointer,
                    ???  # 长度参数
                  )

  # 向程序中注入密码缓冲区的符号值并
  # 实例化模拟器。另一个提示:密码长度为 8 字节。
  # (!)
  password = claripy.BVS( ???, ??? )  # 密码符号变量
  initial_state.memory.store( ??? , ???)  # 存储密码到内存

教程让我们放入一个缓冲区指针,但是我们好像还没准备缓冲区?而且注意它是使用了BVV(字面量),得向往下看,看到

  password = claripy.BVS( ???, ??? )  # 密码符号变量
  initial_state.memory.store( ??? , ???)  # 存储密码到内存

哦,这里才注入了密码,但是密码要注入到哪里?答案是:合法的自选值,例如:0x500000,所以你发现了没有,只有意识到要注入内存才有地址,有了地址才能传递参数,刚刚开始没反应过来。

之后就要添加约束,至于怎么添加要看ELF内容:
image

我们期望return 1也就是eax == 1,所以在函数结尾手动加上约束即可

import angr
import claripy
import sys
from pwn import *


def main(argv):

    path_to_lib = "./output/libA.so"
    libc = ELF(path_to_lib)
    base = 0x400000 #选择合法的地址
    project = angr.Project(path_to_lib, load_options={
        'main_opts': {
            'base_addr': base
        }
    })

    buffer_pointer = claripy.BVV(0x500000, project.arch.bits) # 把下文注入内存的地址填到这里来 ,参数1
    len_pointer = claripy.BVV(8, project.arch.bits)           # 长度,参数二

    validate_function_address = base + libc.sym['validate'] # 函数开始地址
    initial_state = project.factory.call_state(
        validate_function_address,
        buffer_pointer,
        len_pointer
    )

    password = claripy.BVS("password", 8 * 8) # 注入符号长度
    initial_state.memory.store(0x500000, password) # password注入内存地址

    simulation = project.factory.simgr(initial_state)

    check_constraint_address = base + 0x00012DC # 函数结束地址
    simulation.explore(find=check_constraint_address)

    if simulation.found:
        solution_state = simulation.found[0]

        solution_state.add_constraints(solution_state.regs.eax == 1) # 手动约束
        solution = solution_state.solver.eval(password, cast_to=bytes)
        print(solution)
    else:
        raise Exception('Could not find the solution')


if __name__ == '__main__':
    main(sys.argv)

image

这里有点小偷懒使用pwntools折腾地址,实际上完全angr是可以做到的

十八、15_angr_arbitrary_read

image

怎么全是 try_again 啊(恼)

# 这个二进制程序接受一个整数和一个字符串作为参数。某个特定的
# 整数输入会导致程序发生缓冲区溢出,利用这个溢出我们可以从
# 任意内存位置读取字符串。我们的目标是使用Angr
# 在程序中搜索这个缓冲区溢出,然后自动生成
# 一个利用代码来读取字符串"Good Job"。
#
# 读取字符串"Good Job."的意义是什么?
# 这个CTF试图模拟一个可能的漏洞的简化版本,
# 用户可以利用该漏洞让程序打印一个秘密,比如密码或
# 私钥。为了与其他挑战保持一致并简化挑战,
# 这个程序的目标将改为打印"Good Job."。
#
# 编写此脚本的总体策略是:
# 1) 搜索'puts'函数的调用,该函数最终将被利用
#    来打印出"Good Job."
# 2) 确定'puts'的第一个参数(指向要打印的字符串的指针)
#    是否可以被用户控制,以将其设置为
#    "Good Job."字符串的位置。
# 3) 求解能打印出"Good Job."的输入。
#
# 注意:脚本的结构是在步骤#1之前实现步骤#2。

# 部分挑战源代码:
#
# #include <stdio.h>
# #include <stdlib.h>
# #include <string.h>
# #include <stdint.h>
# 
# // 这些都将放在.rodata节
# char msg[] = "${ description }$";
# char* try_again = "Try again.";
# char* good_job = "Good Job.";
# uint32_t key;
# 
# void print_msg() {
#   printf("%s", msg);
# }
#
# uint32_t complex_function(uint32_t input) {
#   ...
# }
# 
# struct overflow_me {
#   char buffer[16];
#   char* to_print;
# }; 
# 
# int main(int argc, char* argv[]) {
#   struct overflow_me locals;
#   locals.to_print = try_again;
# 
#   print_msg();
# 
#   printf("Enter the password: ");
#   scanf("%u %20s", &key, locals.buffer);
#
#   key = complex_function(key);
# 
#   switch (key) {
#     case ?:
#       puts(try_again);
#       break;
#
#     ...
#
#     case ?:
#       // 我们的目标是欺骗这个puts调用,让它打印"秘密
#       // 密码"(在我们的例子中,正好是字符串
#       // "Good Job.")
#       puts(locals.to_print);
#       break;
#     
#     ...
#   }
# 
#   return 0;
# }

import angr
import claripy
import sys

def main(argv):
  path_to_binary = argv[1]
  project = angr.Project(path_to_binary)

  # 你可以使用空白状态或入口状态;只需确保从
  # 程序开始处启动。
  # (!)
  initial_state = ???

  # 同样,scanf需要被替换。
  class ReplacementScanf(angr.SimProcedure):
    # 提示:scanf("%u %20s")
    def run(self, format_string, ???, ???):
      # %u
      scanf0 = claripy.BVS('scanf0', ???)
      
      # %20s
      scanf1 = claripy.BVS('scanf1', ???)

      # bitvector.chop(bits=n)函数将位向量分割成一个Python列表,
      # 列表中的每个元素是位向量的n位片段。这里,
      # 我们将它们分割成8位(一个字节)的片段。
      for char in scanf1.chop(bits=8):
        # 确保字符串中的每个字符都是可打印的。一旦你有了
        # 可用的解决方案,一个有趣的实验是:尝试在不将字符约束在
        # ASCII可打印范围内的情况下运行代码。
        # 即使没有这个约束,解决方案在技术上也能工作,但要输入
        # 一个包含你无法复制、粘贴或输入到终端或检查你
        # 解决方案的网页表单中的字符的解决方案,会更加困难。
        # (!)
        self.state.add_constraints(char >= ???, char <= ???)

      # 警告:字节序仅适用于整数。如果你在内存中存储一个字符串
      # 并将其视为小端序整数,它将是反向的。
      scanf0_address = ???
      self.state.memory.store(scanf0_address, scanf0, endness=project.arch.memory_endness)
      ...

      self.state.globals['solution0'] = ???
      ...

  scanf_symbol = ???  # :string
  project.hook_symbol(scanf_symbol, ReplacementScanf())

  # 每当puts被调用时,我们将调用这个函数。这个函数的目的是
  # 确定传递给puts的指针是否可以被用户控制,
  # 以便我们能够将其重写为指向字符串"Good Job."。
  def check_puts(state):
    # 回忆一下,puts接受一个参数,即指向它将打印的
    # 字符串的指针。如果我们从内存中加载该指针,我们可以分析它以确定
    # 它是否可以通过用户输入来控制,从而将其指向
    # "Good Job."字符串的位置。
    #
    # 将这个函数的实现视为puts刚刚被调用。
    # 栈、寄存器、内存等应该被设置为仿佛x86的call
    # 指令刚刚被调用(但当然,函数还没有复制
    # 缓冲区。)
    # 栈将如下所示:
    # ...
    # esp + 7 -> /----------------\
    # esp + 6 -> |      puts      |
    # esp + 5 -> |    parameter   |
    # esp + 4 -> \----------------/
    # esp + 3 -> /----------------\
    # esp + 2 -> |     return     |
    # esp + 1 -> |     address    |
    #     esp -> \----------------/
    #
    # 提示:查看level 08、09或10,复习如何从
    # 内存地址加载值。记住在将来加载整数时要使用正确的字节序;
    # 这里已经为你包含了。
    # (!)
    puts_parameter = state.memory.load(???, ???, endness=project.arch.memory_endness)

    # 下面的函数接受一个位向量作为参数,并检查它
    # 是否可以取多个值。虽然这不一定告诉我们
    # 我们找到了一个可利用的状态,但它强烈表明
    # 我们检查的位向量可能被用户控制。
    # 用它来确定传递给puts的指针是否是符号化的。
    # (!)
    if state.solver.symbolic(???):
      # 确定"Good Job."字符串的位置。我们想要打印它,
      # 我们将通过尝试约束puts参数等于它来实现。提示:使用'objdump -s <binary>'在.rodata中查找字符串的地址。
      # (!)
      good_job_string_address = ??? # :整数,可能是十六进制

      # 创建一个表达式来测试puts_parameter是否等于
      # good_job_string_address。如果我们将其作为约束添加到求解器中,
      # 它将尝试找到使这个表达式为真的输入。查看
      # level 08以提醒自己这个语法。
      # (!)
      is_vulnerable_expression = ??? # :布尔位向量表达式

      # 最后,我们测试是否能够满足状态的约束。
      if state.satisfiable(extra_constraints=(is_vulnerable_expression,)):
        # 在我们返回之前,让我们将约束真正添加到求解器中,
        # 而不仅仅是查询约束是否可以添加。
        state.add_constraints(is_vulnerable_expression)
        return True
      else:
        return False
    else: # not state.solver.symbolic(???)
      return False

  simulation = project.factory.simgr(initial_state)

  # 为了确定我们是否找到了一个对'puts'的可利用调用,我们需要
  # 在每次到达'puts'调用时运行函数check_puts(上面定义的)。为此,我们将寻找指令指针
  # state.addr等于puts函数开始处的位置。
  def is_successful(state):
    # 我们正在寻找puts。检查地址是否在puts函数的
    # 最开头。警告:理论上,你可以寻找
    # puts中的任何地址,但如果你执行任何调整栈指针
    # 的指令,上面的栈图将是错误的。因此,
    # 建议你检查puts的最开头。
    # (!)
    puts_address = ???
    if state.addr == puts_address:
      # 如果我们确定这个puts调用是可利用的,则返回True。
      return check_puts(state)
    else:
      # 我们还没有找到puts调用;应该继续!
      return False

  simulation.explore(find=is_successful)

  if simulation.found:
    solution_state = simulation.found[0]

    solution = ???
    print(solution)
  else:
    raise Exception('Could not find the solution')

if __name__ == '__main__':
  main(sys.argv)

这 下 看 懂 了!不就是栈溢出吗(喜),正常写大家都会,按照angr一步步来学思想吧

这里偷懒用上一题启动方法

    path_to_binary = argv[1]
    project = angr.Project(path_to_binary)
    initial_state = project.factory.blank_state(
        addr=project.loader.find_symbol('main').rebased_addr,
        add_options={angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                     angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
    )

然后常规hook掉scanf

    class ReplacementScanf(angr.SimProcedure):
        # Hint: scanf("%u %20s")
        def run(self, format_string, key, buf):
            # %u
            scanf0 = claripy.BVS('scanf0', 32)

            # %20s
            scanf1 = claripy.BVS('scanf1', 8 * 20)

            for char in scanf1.chop(bits=8):
                self.state.add_constraints(char >= 32, char <= 126)  # 可打印字符>=32 , <=126

            scanf0_address = key
            self.state.memory.store(
                scanf0_address, scanf0, endness=project.arch.memory_endness) # 注入内存,注意大小段序
            self.state.memory.store(buf, scanf1)

            self.state.globals['solution0'] = scanf0
            self.state.globals['solution1'] = scanf1

    scanf_symbol = "__isoc99_scanf"  # :string
    project.hook_symbol(scanf_symbol, ReplacementScanf())

之后我们要检查puts的地址是否等于我们想要的字符串地址

    def check_puts(state):
        if state.solver.symbolic(puts_parameter):
            good_job_string_address = 0x58434B4F  # :integer, probably hexadecimal
            is_vulnerable_expression = puts_parameter == good_job_string_address

            if state.satisfiable(extra_constraints=(is_vulnerable_expression,)):
                state.add_constraints(is_vulnerable_expression)
                return True
            else:
                return False
        else:
            return False

    simulation = project.factory.simgr(initial_state)

    def is_successful(state):
        puts_address = project.loader.find_symbol("puts").rebased_addr
        if state.addr == puts_address:
            return check_puts(state) # 判断是否是我们想要的
        else:
            return False

    simulation.explore(find=is_successful)

最后就是常规的运行求解打印。
image

我们发现有一些???意思是:任意都满足题解,不用担心,最主要的是后面的OKCX

import angr
import claripy
import sys


def main(argv):
    path_to_binary = argv[1]
    project = angr.Project(path_to_binary)

    initial_state = project.factory.blank_state(
        addr=project.loader.find_symbol('main').rebased_addr,
        add_options={angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                     angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
    )

    class ReplacementScanf(angr.SimProcedure):
        # Hint: scanf("%u %20s")
        def run(self, format_string, key, buf):
            # %u
            scanf0 = claripy.BVS('scanf0', 32)

            # %20s
            scanf1 = claripy.BVS('scanf1', 8 * 20)
            for char in scanf1.chop(bits=8):
                self.state.add_constraints(char >= 32, char <= 126)

            scanf0_address = key
            self.state.memory.store(
                scanf0_address, scanf0, endness=project.arch.memory_endness)
            self.state.memory.store(buf, scanf1)

            self.state.globals['solution0'] = scanf0
            self.state.globals['solution1'] = scanf1

    scanf_symbol = "__isoc99_scanf"  # :string
    project.hook_symbol(scanf_symbol, ReplacementScanf())

    def check_puts(state):
        puts_parameter = state.memory.load(
            state.regs.esp + 4, project.arch.bytes, endness=project.arch.memory_endness)

        if state.solver.symbolic(puts_parameter):
            good_job_string_address = 0x58434B4F
            is_vulnerable_expression = puts_parameter == good_job_string_address
            if state.satisfiable(extra_constraints=(is_vulnerable_expression,)):
                state.add_constraints(is_vulnerable_expression)
                return True
            else:
                return False
        else:
            return False

    simulation = project.factory.simgr(initial_state)
        puts_address = project.loader.find_symbol("puts").rebased_addr
        if state.addr == puts_address:
            return check_puts(state)
        else:
            return False

    simulation.explore(find=is_successful)

    if simulation.found:
        solution_state = simulation.found[0]
        solution = solution_state.solver.eval(
            solution_state.globals['solution1'], cast_to=bytes)
        print(solution)
    else:
        raise Exception('Could not find the solution')


if __name__ == '__main__':
    main(sys.argv)

虽然说的确挺新奇的,但是没办法实战。

十九、16_angr_arbitrary_write

image

# 从本质上讲,程序的行为如下:
#
# scanf("%d %20s", &key, user_input);
# ...
#   // 如果某些未知条件满足……
#   strncpy(random_buffer, user_input);
# ...
# if (strncmp(secure_buffer, reference_string)) {
#   // secure_buffer 不等于参考字符串
#   puts("Try again.");
# } else {
#   // 两者相等
#   puts("Good Job.");
# }
#
# 如果这个程序本身没有任何漏洞,那么它将“永远”只会打印 "Try again.",
# 因为 user_input 被拷贝进的是 random_buffer,而不是 secure_buffer。
#
# 问题是:我们能否找到一个缓冲区溢出,使我们可以覆盖 random_buffer 指针,
# 让它指向 secure_buffer?
# (剧透一下:可以,但我们需要使用 Angr。)
#
# 我们的目标是:在调用 strncpy 的位置,找到一个程序状态,使得:
#  1) 我们可以控制“源内容”(而不是源指针本身!)
#     * 这将允许我们向目标地址写入任意数据
#  2) 我们可以控制“目标指针”
#     * 这将允许我们向任意地址写入数据
#
# 如果这两个条件都满足,那么我们就可以实现:
#   向任意地址写入任意数据。
#
# 最后,我们需要进一步约束:
#  - 源内容必须等于 reference_string
#  - 目标指针必须等于 secure_buffer 的地址
import angr
import claripy
import sys

def main(argv):
  path_to_binary = argv[1]
  project = angr.Project(path_to_binary)

  # 你既可以使用 blank_state,也可以使用 entry_state;
  # 但一定要确保从程序的“起始位置”开始执行。
  initial_state = ???

  class ReplacementScanf(angr.SimProcedure):
    # 提示:这里对应 scanf("%u %20s")
    def run(self, format_string, ???, ???):
      # 处理 %u
      scanf0 = claripy.BVS('scanf0', ???)
      
      # 处理 %20s
      scanf1 = claripy.BVS('scanf1', ???)

      for char in scanf1.chop(bits=8):
        self.state.add_constraints(char >= ???, char <= ???)

      scanf0_address = ???
      self.state.memory.store(
          scanf0_address,
          scanf0,
          endness=project.arch.memory_endness
      )
      ...

      self.state.globals['solutions'] = ???

  scanf_symbol = ???  # :string
  project.hook_symbol(scanf_symbol, ReplacementScanf())

  # 在这个挑战中,我们希望检查 strncpy,
  # 用来判断我们是否能够同时控制“源”和“目标”。
  #
  # 在实际程序中,我们通常至少能够控制其中一个参数,
  # 例如:程序从 stdin 读入字符串后,再进行拷贝。
  def check_strncpy(state):
    # 当前调用 strncpy 时,栈布局如下:
    #
    # ...          ________________
    # esp + 15 -> /                \
    # esp + 14 -> |     param2     |
    # esp + 13 -> |      len       |
    # esp + 12 -> \________________/
    # esp + 11 -> /                \
    # esp + 10 -> |     param1     |
    #  esp + 9 -> |      src       |
    #  esp + 8 -> \________________/
    #  esp + 7 -> /                \
    #  esp + 6 -> |     param0     |
    #  esp + 5 -> |      dest      |
    #  esp + 4 -> \________________/
    #  esp + 3 -> /                \
    #  esp + 2 -> |     return     |
    #  esp + 1 -> |     address    |
    #      esp -> \________________/
    # (!)
    strncpy_dest = ???
    strncpy_src = ???
    strncpy_len = ???

    # 我们需要判断 src 是否是符号的,
    # 但我们真正关心的是“它指向的内容”,而不是这个指针本身。
    # 因此我们必须从 src 指向的内存中,把内容 load 出来判断是否符号化。
    #
    # 提示:strncpy 实际会拷贝多少个字节?
    # (!)
    src_contents = state.memory.load(strncpy_src, ???)

    # 我们的目标是判断是否能够:
    #   向任意地址写入任意数据
    #
    # 也就是说:
    #   - 源内容是符号的(说明数据是可控的)
    #   - 目标指针是符号的(说明目标地址是可控的)
    # (!)
    if state.solver.symbolic(???) and ...:
      # 使用 ltrace 来确定密码字符串。
      # 反编译程序,以确定“被校验的缓冲区”的地址。
      # 我们的最终目标是:覆盖这个缓冲区,写入正确的密码。
      # (!)
      password_string = ???  # :string
      buffer_address = ???   # :integer,通常是十六进制

      # 构造一个表达式,用来检测当前源内容是否等于密码字符串。
      #
      # 警告:
      # 虽然 Python 的切片语法(b[start:end])对 bitvector 也适用,
      # 但 bitvector 的索引方式非常反直觉:
      #
      # - 区间是 [高位 : 低位]
      # - 比特是从右往左编号的
      #
      # 例如:
      #   b = 'ABCDEFGH'(64 位)
      #   b[7:0]   == 'H'
      #
      # 如果你希望取开头的 'AB':
      #   b[63:48] == 'AB'
      #
      # 在本题中,如果你不知道字符串的精确长度,
      # 你可以使用这种方式:
      #   b[-1:-16] == 'AB'
      #
      # 因为:
      #   -1 表示末尾
      #   -16 表示倒数第 16 位
      #
      # 实际数值应与密码字符串长度一致。
      # (!)
      does_src_hold_password = src_contents[???:???] == password_string

      # 构造一个表达式,用于检测:
      # 目标指针是否可以被设置为 buffer_address。
      # 如果这个条件成立,就说明我们已经成功找到可利用点。
      # (!)
      does_dest_equal_buffer_address = ???

      # 和上一题一样,这里使用 satisfiable 的 extra_constraints 参数,
      # 来“临时”测试多个约束是否可以同时满足。
      #
      # 如果可以满足,我们再正式把这些约束加入状态中。
      if state.satisfiable(
          extra_constraints=(does_src_hold_password,
                             does_dest_equal_buffer_address)
      ):
        state.add_constraints(
            does_src_hold_password,
            does_dest_equal_buffer_address
        )
        return True
      else:
        return False
    else:  # not state.solver.symbolic(...)
      return False

  simulation = project.factory.simgr(initial_state)

  def is_successful(state):
    strncpy_address = ???
    if state.addr == strncpy_address:
      return check_strncpy(state)
    else:
      return False

  simulation.explore(find=is_successful)

  if simulation.found:
    solution_state = simulation.found[0]

    solution = ???
    print(solution)
  else:
    raise Exception('无法找到解')

if __name__ == '__main__':
  main(sys.argv)

和上一题一样的部分就跳过,直接从不一样的开始。


(品鉴完题目后)
哎呀爽死我了,一直卡在一个地方,边写边说吧

启动一个干净的环境

    path_to_binary = argv[1]
    project = angr.Project(path_to_binary)

    # You can either use a blank state or an entry state; just make sure to start
    # at the beginning of the program.
    initial_state = project.factory.entry_state(
        add_options={angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                     angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
    )

然后就开始坐牢了


    class ReplacementScanf(angr.SimProcedure):
        # Hint: scanf("%u %20s")
        def run(self, format_string, param0, param1):
            # %u
            scanf0 = claripy.BVV(61440360, 32)      # %u 是 4字节,所以使用 4 * 8 , 我使用写死的,可以使用BVS

            # %20s
            scanf1 = claripy.BVS('scanf1', 20*8)

            # for char in scanf1.chop(bits=8):                            # 使用可见字符,无所谓用不用
            #     self.state.add_constraints(char >= 48, char <= 96)

            scanf0_address = param0
            self.state.memory.store(scanf0_address, scanf0,
                                    endness=project.arch.memory_endness) # 小段写入
            scanf1_address = param1
            self.state.memory.store(scanf1_address, scanf1)

            self.state.globals['solutions'] = (scanf0, scanf1)

    scanf_symbol = '__isoc99_scanf'  # :string
    project.hook_symbol(scanf_symbol, ReplacementScanf())

然后就是本题最恶心的地方,坑点1 destsrvlen的值不需要 * 8

        # The stack will look as follows:
        # ...          ________________
        # esp + 15 -> /                \
        # esp + 14 -> |     param2     |
        # esp + 13 -> |      len       |
        # esp + 12 -> \________________/
        # esp + 11 -> /                \
        # esp + 10 -> |     param1     |
        #  esp + 9 -> |      src       |
        #  esp + 8 -> \________________/
        #  esp + 7 -> /                \
        #  esp + 6 -> |     param0     |
        #  esp + 5 -> |      dest      |
        #  esp + 4 -> \________________/
        #  esp + 3 -> /                \
        #  esp + 2 -> |     return     |
        #  esp + 1 -> |     address    |
        #      esp -> \________________/
        # (!)
    def check_strncpy(state):
        strncpy_dest = state.memory.load(
            state.regs.esp + 4, 4, endness=project.arch.memory_endness)
        strncpy_src = state.memory.load(
            state.regs.esp + 8, 4, endness=project.arch.memory_endness)
        strncpy_len = state.memory.load(
            state.regs.esp + 12, 4, endness=project.arch.memory_endness)

坑点2 我曾经尝试在这里加载divt_contents = state.memory.load(strncpy_dict, strncpy_len),没毛病对吧,而且之后没有使用,结果只要一加载程序就找不到约束,也不告诉你是这一条出问题,关键是我并没有使用,仅仅是加载就会导致QAQ


        # We need to find out if src is symbolic, however, we care about the
        # contents, rather than the pointer itself. Therefore, we have to load the
        # the contents of src to determine if they are symbolic.
        # Hint: How many bytes is strncpy copying?
        # (!)

        src_contents = state.memory.load(strncpy_src, strncpy_len)

        # Our goal is to determine if we can write arbitrary data to an arbitrary
        # location. This means determining if the source contents are symbolic
        # (arbitrary data) and the destination pointer is symbolic (arbitrary
        # destination).
        # (!)

坑点3 password_string 要.encode() 或者使用 b"BIWXXTQA",否则会提示转换失败

        if state.solver.symbolic(src_contents) and state.solver.symbolic(strncpy_dest):

            password_string = "BIWXXTQA".encode()  # :string
            buffer_address = 0x58434B44  # :integer, probably in hexadecimal

坑点4这里又是要取反过来,又要* 8,找了半天我就说为什么比较长度一直不对(恼
坑点5请看VCR:

            # Create an expression that tests if the first n bytes is length. Warning:
            # while typical Python slices (array[start:end]) will work with bitvectors,
            # they are indexed in an odd way. The ranges must start with a high value
            # and end with a low value. Additionally, the bits are indexed from right
            # to left. For example, let a bitvector, b, equal 'ABCDEFGH', (64 bits).
            # The following will read bit 0-7 (total of 1 byte) from the right-most
            # bit (the end of the string).
            #  b[7:0] == 'H'
            # To access the beginning of the string, we need to access the last 16
            # bits, or bits 48-63:
            #  b[63:48] == 'AB'
            # In this specific case, since we don't necessarily know the length of the
            # contents (unless you look at the binary), we can use the following:
            #  b[-1:-16] == 'AB', since, in Python, -1 is the end of the list, and -16
            # is the 16th element from the end of the list. The actual numbers should
            # correspond with the length of password_string.
            # (!)
            does_src_hold_password = src_contents[-1:-64] == password_string

            # Create an expression to check if the dest parameter can be set to
            # buffer_address. If this is true, then we have found our exploit!
            # (!)
            does_dest_equal_buffer_address = strncpy_dest == buffer_address

            # In the previous challenge, we copied the state, added constraints to the
            # copied state, and then determined if the constraints of the new state
            # were satisfiable. Since that pattern is so common, Angr implemented a
            # parameter 'extra_constraints' for the satisfiable function that does the
            # exact same thing:
            if state.satisfiable(extra_constraints=(does_src_hold_password, does_dest_equal_buffer_address)):
                state.add_constraints(does_src_hold_password,
                                      does_dest_equal_buffer_address)
                return True
            else:
                return False
        else:  # not state.se.symbolic(???)
            return False

实际上他一堆比较的逻辑是:

char **dest = (char**)*((size_t)esp + 4); // dest 为指向目标地址字符串的指针
char **src = (char**)*((size_t)esp + 8);  // src 为指向源字符串的指针
int len = (int)*((size_t)esp+12)         // len 为复制长度
char *src_string =  *src;                // src_string 为源字符串指针

if (*src_string是符号量 && *dest是符号量)
// 即:源字符串的内容可控 + 目标的地址可控 ==> 可以任意写

char *password = "BIWXXTQA";          // password 为 指向正确密码的字符串指针
char *buffer = (char *)0x58434B44;    // buffer 为 要被比较的字符串指针

if (strncmp(src_string , password , len))
// 即:源字符串的内容等于密码内容

if (dest == buffer)
// 即:指向目标地址字符串的指针 等于 要被比较的字符串指针

人话就是:

  1. 看看源字符串是不是可以控制
  2. 看看要写入的目标地址是不是可以控制
  3. 源字符串是不是等于正确的密码
  4. 目标字符串位置是不是会被拿去比较

写死我了,相当于重新学了一遍指针,达成成就:在python中玩指针指向指针的指针指向指针的指针的指针


    def is_successful(state):
        strncpy_address = 0x8049070
        if state.addr == strncpy_address:
            return check_strncpy(state)
        else:
            return False

    simulation.explore(find=is_successful)
    if simulation.found:
        solution_state = simulation.found[0]

        scanf0, scanf1 = solution_state.globals['solutions']
        solution = str(solution_state.solver.eval(scanf0)) + ' ' + \
            solution_state.solver.eval(scanf1, cast_to=bytes).decode()
        print(solution)
    else:
        raise Exception('Could not find the solution')

剩下的就是常规了,没坑点了

import angr
import claripy
import sys

def main(argv):
    path_to_binary = argv[1]
    project = angr.Project(path_to_binary)

    initial_state = project.factory.entry_state(
        add_options={angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
                     angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
    )

    class ReplacementScanf(angr.SimProcedure):
        # Hint: scanf("%u %20s")
        def run(self, format_string, param0, param1):
            # %u
            scanf0 = claripy.BVV(61440360, 32)

            # %20s
            scanf1 = claripy.BVS('scanf1', 20*8)

            # for char in scanf1.chop(bits=8):
            #     self.state.add_constraints(char >= 48, char <= 96)

            scanf0_address = param0
            self.state.memory.store(scanf0_address, scanf0,
                                    endness=project.arch.memory_endness)
            scanf1_address = param1
            self.state.memory.store(scanf1_address, scanf1)

            self.state.globals['solutions'] = (scanf0, scanf1)

    scanf_symbol = '__isoc99_scanf'  # :string
    project.hook_symbol(scanf_symbol, ReplacementScanf())

    def check_strncpy(state):
        # The stack will look as follows:
        # ...          ________________
        # esp + 15 -> /                \
        # esp + 14 -> |     param2     |
        # esp + 13 -> |      len       |
        # esp + 12 -> \________________/
        # esp + 11 -> /                \
        # esp + 10 -> |     param1     |
        #  esp + 9 -> |      src       |
        #  esp + 8 -> \________________/
        #  esp + 7 -> /                \
        #  esp + 6 -> |     param0     |
        #  esp + 5 -> |      dest      |
        #  esp + 4 -> \________________/
        #  esp + 3 -> /                \
        #  esp + 2 -> |     return     |
        #  esp + 1 -> |     address    |
        #      esp -> \________________/
        # (!)
        strncpy_dest = state.memory.load(
            state.regs.esp + 4, 4, endness=project.arch.memory_endness)
        strncpy_src = state.memory.load(
            state.regs.esp + 8, 4, endness=project.arch.memory_endness)
        strncpy_len = state.memory.load(
            state.regs.esp + 12, 4, endness=project.arch.memory_endness)

        src_contents = state.memory.load(strncpy_src, strncpy_len)
        if state.solver.symbolic(src_contents) and state.solver.symbolic(strncpy_dest):

            password_string = "BIWXXTQA".encode()  # :string
            buffer_address = 0x58434B44  # :integer, probably in hexadecimal
            does_src_hold_password = src_contents[-1:-64] == password_string
            does_dest_equal_buffer_address = strncpy_dest == buffer_address
            if state.satisfiable(extra_constraints=(does_src_hold_password, does_dest_equal_buffer_address)):
                state.add_constraints(does_src_hold_password,
                                      does_dest_equal_buffer_address)
                return True
            else:
                return False
        else:  # not state.se.symbolic(???)
            return False

    simulation = project.factory.simgr(initial_state)

    def is_successful(state):
        strncpy_address = 0x8049070
        if state.addr == strncpy_address:
            return check_strncpy(state)
        else:
            return False

    simulation.explore(find=is_successful)

    if simulation.found:
        solution_state = simulation.found[0]
        scanf0, scanf1 = solution_state.globals['solutions']
        solution1 = str(solution_state.solver.eval(scanf0))
        solution2 = solution_state.solver.eval(scanf1, cast_to=bytes)
        print(solution1)
        print(solution2)
    else:
        raise Exception('Could not find the solution')


if __name__ == '__main__':
    main(sys.argv)

image

二十、16_angr_arbitrary_write

image

image

image

AUV,这不栈溢出ret2text吗,老熟人了

# 当一个指令有太多可能的分支时,就会发生无约束状态。
# 这种情况有多种发生方式,其中一种是指令指针(在x86上是eip)
# 完全是符号化的,意味着用户输入可以控制计算机执行代码的地址。
# 例如,想象以下伪汇编代码:
#
# mov user_input, eax
# jmp eax
#
# 用户输入的值决定了下一条指令。这就是无约束状态。
# 通常执行引擎继续执行是没有意义的。(如果eax可以是任何值,
# 程序应该跳转到哪里呢?)通常,当Angr遇到无约束状态时,
# 它会将其丢弃。但在我们的案例中,我们希望利用无约束状态跳转到
# 我们选择的位置。我们稍后会介绍如何禁用Angr的默认行为。
#
# 这个挑战代表了一个经典的基于栈的缓冲区溢出攻击,
# 覆盖返回地址并跳转到一个打印"Good Job."的函数。
# 我们解决挑战的策略如下:
# 1. 初始化模拟并让Angr记录无约束状态。
# 2. 逐步执行模拟,直到找到一个eip是符号化的状态。
# 3. 约束eip等于"print_good"函数的地址。

import angr
import claripy
import sys

def main(argv):
  path_to_binary = argv[1]
  project = angr.Project(path_to_binary)

  # 创建一个具有足够大小来触发溢出的符号化输入
  # (!)
  symbolic_input = claripy.BVS("input", (0x13 + 0x4)*8)

  # 创建初始状态并将stdin设置为符号化输入
  initial_state = project.factory.entry_state(
          stdin=symbolic_input,
          add_options = {
              angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
              angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS
              }
          )

  # save_unconstrained=True参数指定Angr不要丢弃无约束状态。
  # 相反,它会将它们移动到名为'simulation.unconstrained'的列表中。
  # 此外,我们将使用一些默认不包含的存储(stashes),
  # 如'found'和'not_needed'。你稍后会看到如何使用它们。
  # (!)
  simulation = project.factory.simgr(
    initial_state,
    save_unconstrained=???,
    stashes={
      'active' : [???],
      'unconstrained' : [],
      'found' : [],
      'not_needed' : []
    }
  )

  # Explore方法对我们不适用,因为用'find'参数指定的方法
  # 不会在无约束状态上被调用。相反,我们希望自己探索二进制文件。
  # 首先,构建一个退出条件,以便知道模拟何时找到了解决方案。
  # 稍后我们将把状态从unconstrained列表移动到simulation.found列表。
  # 创建一个布尔值,指示是否已找到解决方案。
  def has_found_solution():
    return simulation.found

  # 当一个指令有太多可能的分支时,就会发生无约束状态。
  # 这种情况有多种发生方式,其中一种是指令指针(在x86上是eip)
  # 完全是符号化的,意味着用户输入可以控制计算机执行代码的地址。
  # 例如,想象以下伪汇编代码:
  #
  # mov user_input, eax
  # jmp eax
  #
  # 用户输入的值决定了下一条指令。这就是无约束状态。
  # 检查是否还有无约束状态需要检查,通过检查simulation.unconstrained列表。
  # (!)
  def has_unconstrained_to_check():
    return ???

  # simulation.active列表是所有可以进一步探索的状态列表。
  # (!)
  def has_active():
    return ???

  while (has_active() or has_unconstrained_to_check()) and (not has_found_solution()):
    for unconstrained_state in simulation.unconstrained:
        # 查找无约束状态并将其移动到'found'存储中。
        # 'stash'应该是一个字符串,对应状态组保存的所有状态的列表。
        # 值包括:
        #  'active' = 可以继续执行的状态
        #  'deadended' = 已退出程序的状态
        #  'errored' = 遇到Angr错误的状态
        #  'unconstrained' = 无约束的状态
        #  'found' = 解决方案
        #  其他任何值 = 你想要的任何内容,也许你想要'not_needed',
        #              你可以随意命名它
        # (!)
      simulation.move('unconstrained', 'found')

    # 推进模拟。
    simulation.step()

  if simulation.found:
    solution_state = simulation.found[0]

    # 约束指令指针指向print_good函数
    # (!)
    solution_state.add_constraints(solution_state.regs.eip == ???)

    # 约束符号化输入在可打印范围内(大写字母)以适应Web UI。
    # 确保UTF-8编码。
    # (!)
    for byte in symbolic_input.chop(bits=8):
      solution_state.add_constraints(
              byte >= ???,
              byte <= ???
      )

    # 求解symbolic_input
    solution = solution_state.solver.eval(symbolic_input,cast_to=bytes).decode()
    print(solution)
  else:
    raise Exception('Could not find the solution')

if __name__ == '__main__':
  main(sys.argv)

这题我试了,打不出来QAQ,努力过了,不知道为啥,题解也用不了。

image

总结

完结撒花,感谢陪伴,污点分析见!

posted @ 2025-11-16 11:44  归海言诺  阅读(41)  评论(0)    收藏  举报