Angr学习笔记

article/2025/11/8 10:31:46

Angr学习笔记

前言

本文记录一下Angr的基本使用方法,主要是基于Github上的开源项目以及笔记AngrCTF_FITM整理,Angr在逆向方面确实用处比较大,特此记录一下。

什么是Angr

angr是一个用于分析二进制文件的python框架。它专注于静态和符号分析,使其适用于各种任务。项目地址:https://github.com/angr

符号执行

​ 符号执行就是在运行程序时,用符号来替代真实值。符号执行相较于真实值执行的优点在于,当使用真实值执行程序时,我们能够遍历的程序路径只有一条, 而使用符号进行执行时,由于符号是可变的,我们就可以利用这一特性,尽可能的将程序的每一条路径遍历,这样的话,必定存在至少一条能够输出正确结果的分支, 每一条分支的结果都可以表示为一个离散关系式,使用约束求解引擎即可分析出正确结果。

​ 个人理解为Angr就是将输入作为一种可变符号,各种条件分支作为路径分支,符号执行实际上就是在走迷宫,最终得到到达终点的正确路径。

0x00 一般模板

题目:00_angr_find

直接拖到ida中查看源码:

image-20230315100537072

主要逻辑就是对于输入的字符串进行complex_function变换然后比较。

import angr
import sysdef Run():bin_path = "./00_angr_find"project = angr.Project(bin_path, auto_load_libs=False)initial_state = project.factory.entry_state()simulation = project.factory.simgr(initial_state)def is_successful(state):stdout_output = state.posix.dumps(sys.stdout.fileno())if b'Good Job.' in stdout_output:return Trueelse:return Falsedef should_abort(state):stdout_output = state.posix.dumps(sys.stdout.fileno())if b'Try again.' in stdout_output:return Trueelse:return Falsesimulation.explore(find=is_successful, avoid=should_abort)if simulation.found:for state in simulation.found:solution = state.posix.dumps(sys.stdin.fileno())# solution0 = state.solver.eval(passwd0,cast_to=bytes)# solution1 = state.solver.eval(passwd1,cast_to=bytes)# solution = solution0+b" "+solution1print("[+] Success! Solution is: {}".format(solution.decode("utf-8")))else:raise Exception('Could not find the solution')if __name__ == "__main__":Run()
  • project = angr.Project(bin_path, auto_load_libs=False)

    Angr使用Project作为二进制文件的基本映像,auto_load_libs=False避免程序导入不必要的库,否则分析到库函数调用时也会进入库函数,这样会增加分析的工作量,也有可能会跑挂。

  • initial_state = project.factory.entry_state()

    Angr并不是真正的运行程序,而是模拟程序的运行路径,因此Angr提供state来记录程序模拟时的状态(记录一系列程序运行时的信息,如内存/寄存器/文件等,类似于快照),project.factory.entry_state用于提供程序初始化状态。

  • simulation = project.factory.simgr(initial_state)

    基于状态创建程序模拟管理器simulation,用于控制程序的模拟执行,从我们提供的初始化状态initial_state开始。

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

    符号执行最普遍的操作时找到能够到达某个地址的状态,simulation提供了``explore()方法寻找路径,启动后程序会一直执行,直到发现了一个和find参数指定的条件相匹配的状态。其中findavoid参数可以为;

    • 具体地址
    • 具体地址的列表集合
    • state为参数的判断函数(本题解中就是用的这种方式)
  • sys.*.fileno()

    • sys.stdin.fileno()标准输入文件描述符,值为0
    • sys.stdout.fileno()标准输出文件描述符,值为1
    • sys.stderr.fileno()标准错误文件描述符,值为2
  • state.posix.dumps

    state.posix.dumps(0)代表该状态程序的所有输入,state.posix.dumps(1)代表该状态程序的所有输出。

0x01 输入的参数存放在寄存器中

题目:03_angr_symbolic_registers

日常IDA查看一下主函数:

image-20230314140123976

程序通过get_user_input读取输入,并存放至eaxebxedx,当然我们可以使用上一题那样直接进行路径搜索,但是angr在处理复杂格式的字符串输入时优化不是很好,最好的办法是绕过scanf函数,直接将符号注入到寄存器中。

image-20230314140314978

complex_function_1complex_function_2complex_function_3密码加密函数,也是我们用于路径寻找的点。

输入的参数存放在寄存器中
import angr
import sys
import claripydef Run():bin_path = "./03_angr_symbolic_registers"project = angr.Project(bin_path, auto_load_libs=False)# initial_state = project.factory.entry_state()start_address = 0x08048980initial_state = project.factory.blank_state(addr=start_address)passwd_size_in_bits =32passwd0 = claripy.BVS('passwd0',passwd_size_in_bits)passwd1 = claripy.BVS('passwd1', passwd_size_in_bits)passwd2 = claripy.BVS('passwd2', passwd_size_in_bits)initial_state.regs.eax = passwd0initial_state.regs.ebx = passwd1initial_state.regs.edx = passwd2simulation = project.factory.simgr(initial_state)def is_successful(state):stdout_output = state.posix.dumps(sys.stdout.fileno())if b'Good Job.' in stdout_output:return Trueelse:return Falsedef should_abort(state):stdout_output = state.posix.dumps(sys.stdout.fileno())if b'Try again.' in stdout_output:return Trueelse:return Falsesimulation.explore(find=is_successful, avoid=should_abort)if simulation.found:for i in simulation.found:solution_state = isolution0 = format(solution_state.solver.eval(passwd0), 'x')solution1 = format(solution_state.solver.eval(passwd1), 'x')solution2 = format(solution_state.solver.eval(passwd2), 'x')solution = solution0 + " " + solution1 + " " + solution2print("[+] Success! Solution is: {}".format(solution))else:raise Exception('Could not find the solution')if __name__ == "__main__":Run()
  • 为了绕过get_user_input输入函数,我们不能从main函数的开头开始,通过使用start_address定位到get_user_input下一条指令,即0x08048980,在此处构造程序状态。

  • project.factory.blank_state

    当我们不使用main函数作为程序入口时,初始化状态就不能够使用entry_state(),好在project.factory提供了其他的函数初始化状态:

    名称描述
    entry_state()构造一个从函数入口点执行的已初始化状态
    blank_state()在指定的入口地址处构造一个“空状态”,该的数据都是未初始化的,当使用未初始化的的数据时,一个不受约束的符号值将会被返回。
    call_state()构造一个已经准备好执行某个函数的状态
    full_init_state()构造一个已经执行过所有与需要执行的初始化函数,并准备从函数入口点执行的状态。比如,共享库构造函数(constructor)或预初始化器。当这些执行完之后,程序将会跳到入口点。
  • passwd0 = claripy.BVS('passwd0',passwd_size_in_bits)

    构造了一个名为passwd0的符号位变量,符号位向量是angr用于将符号值注入程序的数据类型。这些将是angr将解决的方程式的“x”,也就是约束求解时的自变量。可以通过 BVV(value,size)BVS( name, size) 接口创建位向量,也可以用 FPVFPS 来创建浮点值和符号。

  • initial_state.regs.eax = passwd0

    将符号变量passwd0注入eax寄存器中。

  • solution_state.solver.eval(passwd0)

    返回的是passwd0的一个十进制解,用format将其16进制化。这里:

    • solver.eval(expression):将会解出expression一个可行解。
    • solver.eval_one(expression):将会给出expression的可行解,若有多个可行解,则抛出异常。
    • solver.eval_upto(expression, n):将会给出最多n个可行解,如果不足n个就给出所有的可行解。
    • solver.eval_exact(expression, n):将会给出n个可行解,如果解的个数不等于n个,将会抛出异常。
    • solver.min(expression):将会给出最小可行解。
    • solver.max(expression):将会给出最大可行解。

    solution_state.solver.eval与state.posix.dumps的区别:

    state.posix.dumps一般在未定义BVS时打印我们想要的结果,而solution_state.solver.eval一般用于定义了BVS,打印BVS的结果。

0x02 输入参数存放在栈上

题目:04_angr_symbolic_stack

直接拖进IDA进行分析,可以看的handle_user为主逻辑,并且scanf读取的数据是直接写在栈上的,那么我们注入寄存器的方法就是失效了,现在尝试直接注入栈空间。

image-20230315153909020

同样设置初始化状态为scanf之后,即0x08048697,这里有小伙伴可能会有疑问了,scanf后面明明是0x08048694为什么是要使用下一条指令呢,原因是add esp,10h是在清理scanf的栈帧,可能会对我们的esp造成影响,因此我们这里直接跳过了scanf栈帧的清理,否则还需要对esp进行相应的处理。

image-20230315192238892

import angr
import sys
import claripydef Run():bin_path = "./04_angr_symbolic_stack"project = angr.Project(bin_path, auto_load_libs=False)# initial_state = project.factory.entry_state()start_address = 0x08048697initial_state = project.factory.blank_state(addr=start_address)initial_state.regs.ebp = initial_state.regs.esppasswd_size_in_bits = 32passwd0 = claripy.BVS('passwd0', passwd_size_in_bits)passwd1 = claripy.BVS('passwd1', passwd_size_in_bits)# 构造主函数栈帧padding_length_in_bytes = 0x8initial_state.regs.esp -= padding_length_in_bytesinitial_state.stack_push(passwd0)initial_state.stack_push(passwd1)simulation = project.factory.simgr(initial_state)def is_successful(state):stdout_output = state.posix.dumps(sys.stdout.fileno())if b'Good Job.' in stdout_output:return Trueelse:return Falsedef should_abort(state):stdout_output = state.posix.dumps(sys.stdout.fileno())if b'Try again.' in stdout_output:return Trueelse:return Falsesimulation.explore(find=is_successful, avoid=should_abort)if simulation.found:solution_state = simulation.found[0]solution0 = format(solution_state.solver.eval(passwd0), 'd')solution1 = format(solution_state.solver.eval(passwd1), 'd')solution = solution0 + " " + solution1print("[+] Success! Solution is: {}".format(solution))else:raise Exception('Could not find the solution')if __name__ == "__main__":Run()
  • initial_state.regs.ebp = initial_state.regs.esp

    由于我们使用的是project.factory.blank_state初始化的状态,该状态内的数据都是未初始化的,并且后面执行的代码使用到了栈上的数据,因此我们需要手动构建当前状态的栈帧。

  • initial_state.regs.esp -= padding_length_in_bytes

    通过汇编代码我们可以得知:求解的passwd0passwd1分别位于栈上ebp-0xcebp-0x10位置,也就是说栈上有8个字节是被占用的,我们需要手动填充这些字节,然后将符号入变量入栈,保证汇编代码的正确性。另一种不用压栈的方法就是直接将esp移动至相应位置,直接向该地址写入符号位变量,但是下面的解法是有问题,最终输出的结果不对,我也不清楚那里的问题,望大佬告知。

    initial_state.regs.rbp = initial_state.regs.rsp
    passwd0_addr = initial_state.regs.esp - 0xc
    passwd1_addr = initial_state.regs.esp - 0x10
    initial_state.regs.esp -= 0x10
    initial_state.memory.store(passwd0_addr, passwd0)
    initial_state.memory.store(passwd1_addr, passwd1)
    

0x03 传入的参数存在全局变量区

题目:05_angr_symbolic_memory

主函数:

image-20230315200647631

这道题目解法与之前的类似,主要区别在于scanf输入的内容存储在.bss段,而.bss段默认是程序启动时,由系统自动分配的空间,因此如果我们想要跳过scanf时,就必须要将符号位变量写入至.bss段的相应位置。

import angr
import sysimport claripydef Run():bin_path = "./05_angr_symbolic_memory"project = angr.Project(bin_path, auto_load_libs=False)# 设置起始状态start_address =0x08048601initial_state = project.factory.blank_state(addr=start_address)passwd_size_in_bits = 64passwd0 = claripy.BVS('passwd0', passwd_size_in_bits)passwd1 = claripy.BVS('passwd1', passwd_size_in_bits)passwd2 = claripy.BVS('passwd2', passwd_size_in_bits)passwd3 = claripy.BVS('passwd3', passwd_size_in_bits)passwd0_address = 0x0A1BA1C0initial_state.memory.store(passwd0_address,passwd0)initial_state.memory.store(passwd0_address + 0x8, passwd1)initial_state.memory.store(passwd0_address + 0x10, passwd2)initial_state.memory.store(passwd0_address + 0x18, passwd3)simulation = project.factory.simgr(initial_state)def is_successful(state):stdout_output = state.posix.dumps(sys.stdout.fileno())if b'Good Job.' in stdout_output:return Trueelse:return Falsedef should_abort(state):stdout_output = state.posix.dumps(sys.stdout.fileno())if b'Try again.' in stdout_output:return Trueelse:return Falsesimulation.explore(find=is_successful, avoid=should_abort)if simulation.found:for state in simulation.found:solution0 = state.solver.eval(passwd0,cast_to=bytes) solution1 = state.solver.eval(passwd1, cast_to=bytes)solution2 = state.solver.eval(passwd2, cast_to=bytes)solution3 = state.solver.eval(passwd3, cast_to=bytes)solution = solution0 + b" " + solution1 + b" " + solution2 + b" " + solution3print("[+] Success! Solution is: {}".format(solution.decode("utf-8")))else:raise Exception('Could not find the solution')if __name__ == "__main__":Run()
  • initial_state.memory.store

    这里用到的访问方式是state.memory.storestate.memory.load,可以用来访问一段连续的内存。由于4个变量是连续存储,直接按8字节叠加即可。

    • load(addr,...): 读取指定地址的内存
    def load(self, addr, size=None, condition=None, fallback=None, add_constraints=None, action=None, 	      endness=None, inspect=True, disable_actions=False, ret_on_segv=False):"""Loads size bytes from dst.:param addr:             The address to load from. :param size:            The size (in bytes) of the load. :param condition:       A claripy expression representing a condition for a conditional load.:param fallback:        A fallback value if the condition ends up being False. :param add_constraints: Add constraints resulting from the merge (default: True).:param action:          A SimActionData to fill out with the constraints.:param endness:         The endness to load with. """
    
    • store(addr, ...): 向指定内存写入数据
    def store(self, addr, data, size=None, condition=None, add_constraints=None, endness=None, action=None,inspect=True, priv=None, disable_actions=False):"""Stores content into memory.:param addr:        A claripy expression representing the address to store at. :param data:        The data to store (claripy expression or something convertable to a claripy expression).:param size:        A claripy expression representing the size of the data to store. #大小...
    

0x04 传入的参数存放在堆上

题目:06_angr_symbolic_dynamic_memory

image-20230315203341304

import angr
import sysimport claripydef Run():bin_path = "./06_angr_symbolic_dynamic_memory"project = angr.Project(bin_path, auto_load_libs=False)# initial_state = project.factory.entry_state()start_address = 0x08048699initial_state = project.factory.blank_state(addr=start_address)passwd_size_in_bits = 64passwd0 = claripy.BVS('passwd0', passwd_size_in_bits)passwd1 = claripy.BVS('passwd1', passwd_size_in_bits)fake_heap_address0 = 0xffffc900pointer_to_malloc_memory_address0 = 0xabcc8a4fake_heap_address1 = 0xffffc955pointer_to_malloc_memory_address1 = 0xabcc8acinitial_state.memory.store(fake_heap_address0, passwd0)initial_state.memory.store(fake_heap_address1, passwd1)initial_state.memory.store(pointer_to_malloc_memory_address0,fake_heap_address0, endness=project.arch.memory_endness)initial_state.memory.store(pointer_to_malloc_memory_address1,fake_heap_address1, endness=project.arch.memory_endness)simulation = project.factory.simgr(initial_state)def is_successful(state):stdout_output = state.posix.dumps(sys.stdout.fileno())if b'Good Job.' in stdout_output:return Trueelse:return Falsedef should_abort(state):stdout_output = state.posix.dumps(sys.stdout.fileno())if b'Try again.' in stdout_output:return Trueelse:return Falsesimulation.explore(find=is_successful, avoid=should_abort)if simulation.found:for state in simulation.found:solution0 = state.solver.eval(passwd0, cast_to=bytes)solution1 = state.solver.eval(passwd1, cast_to=bytes)solution = solution0+b" "+solution1print("[+] Success! Solution is: {}".format(solution.decode("utf-8")))else:raise Exception('Could not find the solution')if __name__ == "__main__":Run()
  • initial_state.memory.store(fake_heap_address1, passwd1)

    由于我们跳过了程序初始阶段,buffer0buffer1并未进行malloc,因此我们需要手动模拟分配空间malloc的操作。initial_state.memory.store能够在内存中写入数据,因此完全可以用来模拟malloc,直接将符号位向量写入内存空间。buffer0buffer1存储的是申请到的堆内存地址,angr并没有真正“运行”二进制文件,它只是在模拟运行状态,因此它实际上不需要将内存分配到堆中,实际上可以伪造任何地址。而需要使用者做的就是选择两个地址存放的堆区地址,buffer0buffer1就是可选项。0xffffc900和0xffffc955随机伪造的地址。

  • initial_state.memory.store(pointer_to_malloc_memory_address0,fake_heap_address0, endness=project.arch.memory_endness)

    空间分配好之后,我们直接将空间地址写入buffer0buffer1,这两个变量又是在.bss,因此需要进行内存写入。.store参数endness 用于设置端序,angr默认为大端序,总共可选的值如下:

    • LE – 小端序
    • BE – 大端序
    • ME – 中间序

总结

今天简单练习了angr_ctf的前六道题,感觉angr比我想象中的要强大,但是使用angr过程中也应该注意:angr实际上是一种路径探索的方法,在处理分支时,采取统统收集的策略,因此每当遇见一个分支,angr的路径数量就会乘2,这是一种指数增长,也就是所说的路径爆炸。执行上很像BFS,一旦分支过多,angr就无法较为快速的求解了,因此在使用过程中应该尽可能对源程序进行处理,最好能够减少分支路径。

参考链接

angr初探

angr_进阶

angr_ctf

angr-doc-zh_CN

Angr入门(二)- 一些CTF的Angr分析


http://chatgpt.dhexx.cn/article/LCbqr6Sr.shtml

相关文章

angr纠错和易错点

现在环境已经没有问题了。我运行了几个代码(使用python3),都会出现不同的问题。 运行报错 解决方法: 添加b 运行报错: 解决方法: 删掉cast_to; 因为python2中str就是字节流 python3中不一样&#xff0c…

git本地分支管理

文章目录 一、master分支没有改变,合并其他分支二、master分支有改变,合并其他分支 作为开发人员,我们应该尽可能多地建立分支,在分支上进行开发,功能测试稳定后,再将分支上地代码合并到指定的分支 git bra…

Git 分支管理

Git 分支管理 一、主分支Master 首先,代码库应该有一个、且仅有一个主分支。所有提供给用户使用的正式版本,都在这个主分支上发布。 Git主分支的名字,默认叫做Master。它是自动建立的,版本库初始化以后,默认就是在主分…

git分支管理策略

git分支管理策略 1 总览 git 的分支整体预览图如下: 从上图可以看到主要包含下面几个分支: master:git默认主分支(这里不作操作)。 stable:稳定分支,替代master,主要用来版本发布。…

IDEA中使用Git功能和IDEA中的Git分支管理

IDEA中使用Git功能 IDEA中创建Git仓库 1、设置Git程序的路径 2、设置编译器的GitHub账号 3、新建项目 4、发布项目到GitHub IDEA向Git提交修改后的代码 1、实现git add 2、开始commit 3、实现git commit -m 4、实现git push origin master 项目成员使用IDEA的Git功…

【Git-10】Eclipse中Git分支管理

分支管理,是 Git 开发中的一个非常有效的团队开发策略。多个程序员并行开发,每个程序员可以定义各自的分支,在自己的分支上开发工程。再开发结束测试完毕后,再合并到主干工程中,一次性提交到远程。由其他程序员使用。 …

Git 分支管理最佳实践

Git 是目前最流行的源代码管理工具。大量的软件项目由 GitHub、Bitbucket 和 GitLab 这样的云服务平台或是私有的 Git 仓库来管理。在使用 Git 时通常会遇到的一个问题是采用何种分支管理实践,即如何管理仓库中作用不同的各类分支。和软件开发中的其他实践一样&…

Git分支管理办法,每个团队不一样,仅供参考!

原创不易,转载注明出处,喜欢就点个赞吧! 网上有很多文章,很少有实际具体的流程图说明,我根据我们团队的情况,画了几张简图,仅供参考。 每个公司的代码管理方式都不一样 主要有两种 1.TrunkB…

Git分支管理与常用命令

一、分支管理 Git在创建分支仅是生成了一个指针又称快照(有的工具则进行了物理拷贝),这样在使用起来就非常的方便。方便的同时问题随之而来,如果不加管理,那么就会到处开枝散叶,完全看不出主干。因此&…

复杂项目的版本管理及git分支管理建议

在复杂项目中,特别是多团队的快速迭代中,版本管理与分支管理,总是我们难以回避的问题,这里分享一下我们在团队中使用的规范,以及对应的每一步的步骤。 为什么需要GIT使用建议 团队开发中,遵循一个合理、清…

2022新版Git教程 从入门到实战(三)Git分支管理

🐟 个人主页 :https://blog.csdn.net/qq_52007481⭐个人社区:【小鱼干爱编程】📰最近专栏:Git教程专栏 文章目录 分支工作的工作流程Git分支管理分支的好处 前期准备查看分支分支创建分支转换在新分支中添加记录切换回…

Git 分支管理最佳实践(转载)

前言 Git 是目前最流行的源代码管理工具。大量的软件项目由 GitHub、Bitbucket 和 GitLab 这样的云服务平台或是私有的 Git 仓库来管理。在使用 Git 时通常会遇到的一个问题是采用何种分支管理实践,即如何管理仓库中作用不同的各类分支。和软件开发中的其他实践一样…

Git 分支管理策略汇总

原文链接: Git 分支管理策略 最近,团队新入职了一些小伙伴,在开发过程中,他们问我 Git 分支是如何管理的,以及应该怎么提交代码? 我大概说了一些规则,但仔细想来,好像也并没有形成…

Git分支管理

四、分支管理 分支的存在,极大的强化了不同开发者的协作管理,在一个功能的开发过程中,可以在一个单独的分支上完成,这样不影响主分支的开发,待功能完成后再将分支的内容合并到主分支。 每次提交到分支,Gi…

git的分支管理

这篇文章主要想讲一下git下的分支管理,因为分支管理是你在日常工作中必不可少的一项操作,觉得自己之前了解的不是很多,这次便决定利用一些细碎的时间好好去再次学习巩固一下,初衷也是为了加深自己的理解,便写下此文&am…

【Git】“分支” 如何管理和使用?这一篇就够了~

目录 一、什么是分支? 二、分支的操作 2.1、查看分支 2.2、创建分支 2.3、切换分支 和 修改分支 2.4、合并分支 2.4.1、正常合并 2.4.2、合并冲突 三、创建分支和切换分支的底层原理 一、什么是分支? 在版本控制中,我们可以针对每个任…

管理Git的分支

目录 一、本地分支(local branches)管理 1.1 查看和建立分支( git branch) 1.2 分支中提交的浏览与比较(git log) 1.3 切换当前分支(git switch)与合并分支(git mer…

常用的git分支管理方法都在这了

一、分支管理定义 几乎每一种版本控制系统都以某种形式支持分支,一个分支代表一条独立的开发线,使用分支意味着你可以从开发主线上分离开来,然后在不影响主线的同时继续工作,Git 分支实际上是指向更改快照的指针。 其作用显然…

前端安全之xss与xsrf

提到前端安全,往往离不开xss(跨站脚本攻击)、xsrf(跨站伪造请求),在此记录一下关于前端安全的学习过程。 什么是xss? 跨站脚本攻击(Cross Site Scripting),为了不和css(层叠样式表)混淆,故记为xss。其原理是利用用户…

前端安全问题及防范

目录 XSS攻击反射型存储型DOM型总结 CSRF攻击点击劫持CDN带来的风险文件上传漏洞本地存储信息泄露 XSS攻击 XSS(Cross-Site Scripting,跨站脚本),是比较常见的安全漏洞问题,其主要的攻击方式是通过表单或者页面url参数来注入一些可执行的代码&#xff0…