angr
- 概述
- 如何学习
- 题目列表
- 00_angr_find
- 01_angr_avoid
- 02_angr_find_condition
- 03_angr_symbolic_registers
- 04_angr_symbolic_stack
- 05_angr_symbolic_memory
- 06_angr_symbolic_dynamic_memory
- 总结
概述
angr
简单介绍一下angr,借助官方的原话就是 – A powerful and user-friendly binary analysis platform!
,一个功能强大且用户友好的二进制分析平台。这里主要总结一下使用angr解决ctf逆向题的一些用法和套路。
安装的话,大家可以直接使用pip安装,不过这里可能会遇到一个小坑。我在安装angr之前已经安装了pwntools,此时再安装angr,由于有一个依赖库两者需要的版本是不一样的,所以安装后导致pwntools无法使用。解决的话,大家可以采用python虚拟环境将做题环境分开,推荐使用pipenv,有空的话再写一个笔记,不过这个操作比较简单,读者简单搜索即可解决。
如何学习
angr官方文档
个人在网上发现了一个名为 – angr_ctf的项目,非常适合新手入门学习。作者也是通过一道道基本题目来帮助读者入手angr,对于我们这种re手来说,直接去阅读文档肯定是效率极低的,所以通过做题的方式来学习我感觉是非常可以的。
首先直接git clone下整个项目,然后如下截图所示,是其部分的项目内容截图,其中带有数字编号的是题目,一共有18道题,进入每道有编号的题目里,会有相应生成c程序的模板和脚本。dist
目录的话存放的就是已经生成好了的二进制程序,solutions
目录下存放了对应程序和解题脚本,所以我们直接在这个目录下学习就好。
如下截图所示,进入solutions目录,然后再进入编号为00的题目目录里,可以看到一共有三个文件,第一个对应二进制程序,第二个名为scaffold00.py
的脚本文件是需要我们完成的解题脚本,作者在里面给了充分的解释和代码来帮助我们学习并解决题目,第三个文件就是作者给出的完整解题脚本。
下面直接开始学习,我会将自己的解题脚本贴出来,由于作者已经给了足够的注释,我就不详细介绍每个API的用途,个别需要思考的地方我会单独注释。
题目列表
00_angr_find
如下截图所示,是该题目的主要逻辑,print_msg
函数用于打印提示信息,然后要求我们输入密码,之后会用complex_function
对密码进行变换,最终和程序中给出的字符串进行比较,相同即成功。
下面我们用angr进行求解,脚本如下。
import angr
# Create a Pro --> Create a simu --> Explore
pro = angr.Project("./00_angr_find")
init_state = pro.factory.entry_state() # start() addr
simu = pro.factory.simgr(init_state)
good_addr = 0x804867d
simu.explore(find=good_addr)
# get res
if simu.found:
res = simu.found[0]
for i in range(3):
# print stdin/stdout/stderr, stdin is flag
print(res.posix.dumps(i))
else:
print("No result!")
如下是程序的运行结果,上面一开始是一些警告信息,主要是由于angr的求解代码没有进一步设置更多的值,这表明我们的代码确实不是很完善,当然应对简单的题目足够,当后面学习更多的知识时再解决。最后三行输出依次是stdin/stdout/stderr
的值,stdin代表输入的passwd即flag,stdout是程序运行的输出,程序没有运行出错,所以stderr没有输出。
01_angr_avoid
如下截图所示,由于main函数中逻辑太长无法反编译,所以只能尝试分析汇编,可以发现整个逻辑和上题中是一样的,要求输入密码然后变换后和已知字符串进行比对。有区别的是最后判断加入了should_succeed
,而avoid_me
函数会将其值置为0从而无法达到good_addr,因此这里的angr求解脚本需要避免进入到avoid中。
求解脚本如下,经过实际测试不加入avoid也是可以求解的,不过会使用更多的时间。
import angr
pro = angr.Project("./01_angr_avoid")
init_state = pro.factory.entry_state()
simu = pro.factory.simgr(init_state)
good_addr = 0x080485E0
avoid_addr = 0x080485A8 # avoid_me func
simu.explore(find=good_addr, avoid=avoid_addr)
if simu.found:
res = simu.found[0]
for i in range(3):
print(res.posix.dumps(i))
else:
print("No result!")
"""
b'RNGFXITY'
b'placeholder\nEnter the password: '
b''
"""
02_angr_find_condition
如下截图所示,是本题的主要逻辑,和前面的题目差不多,此处就不多分析。
求解脚本如下,这道题的目的主要是学习另外一种条件判断的方法,前面的题目指定是地址,这里也可以指定为一个条件函数。
import angr
pro = angr.Project("./02_angr_find_condition")
init_state = pro.factory.entry_state()
simu = pro.factory.simgr(init_state)
def success(state):
output = state.posix.dumps(1) # get output from stdout
return b"Good Job." in output
def abort(state):
output = state.posix.dumps(1) # get output from stdout
return b"Try again." in output
simu.explore(find=success, avoid=abort)
if simu.found:
res = simu.found[0]
for i in range(3):
print(res.posix.dumps(i))
else:
print("No result!")
"""
b'UFOHHURD'
b'placeholder\nEnter the password: Good Job.\n'
b''
"""
03_angr_symbolic_registers
如下截图所示,是本题的主要逻辑,同样也是先获取输入,然后使用了三个函数进行变换,最后进行判断。
这道题最主要的区别在于scanf读取了三个参数,以前的angr版本不支持scanf获取多个参数,所以会采用设置寄存器求解的方法,如下脚本所示。不过现在安装最新版本的话是可以支持scanf获取多个参数的,所以仍然可以用上面的方法直接求解。
import angr
import claripy
pro = angr.Project("./03_angr_symbolic_registers")
start_addr = 0x080488D1
init_state = pro.factory.blank_state(addr=start_addr) # blank_state, not entry_state
# create three vars
size_in_bits = 32
passwd0 = claripy.BVS("passwd0", size_in_bits)
passwd1 = claripy.BVS("passwd1", size_in_bits)
passwd2 = claripy.BVS("passwd2", size_in_bits)
# scanf --> eax/ebx/edx
init_state.regs.eax = passwd0
init_state.regs.ebx = passwd1
init_state.regs.edx = passwd2
simu = pro.factory.simgr(init_state)
def success(state):
output = state.posix.dumps(1)
return b"Good Job." in output
def abort(state):
output = state.posix.dumps(1)
return b"Try again." in output
simu.explore(find=success, avoid=abort)
if simu.found:
res = simu.found[0]
solu0 = res.solver.eval(passwd0) # you can also use init_state.regs.eax replace passwd0
solu1 = res.solver.eval(passwd1)
solu2 = res.solver.eval(passwd2)
solu = " ".join(map("{:x}".format, [solu0, solu1, solu2]))
print(solu)
else:
print("No result!")
"""
db01abf7 4930dc79 d17de5ce
"""
04_angr_symbolic_stack
如下截图所示,是该程序的主逻辑,这道题和上面一道题类似,同样需要设置参数来求解。
脚本如下,由于设置参数是在栈上,所以我们需要在初始状态时模拟程序本身的堆栈情况,即在正确的栈位置将参数传进入,主要看esp和ebp两个指针。
import angr
import claripy
pro =angr.Project("./04_angr_symbolic_stack")
start_addr = 0x08048697
init_state = pro.factory.blank_state(addr=start_addr)
# 初始时ebp无值,esp有值,将两个置于同一个值
init_state.regs.ebp = init_state.regs.esp
passwd0 = claripy.BVS("passwd0", 32)
passwd1 = claripy.BVS("passwd1", 32)
# 安装程序的运行逻辑构造正确的栈帧
init_state.regs.esp -= 8
init_state.stack_push(passwd0)
init_state.stack_push(passwd1)
# test esp
# 因为程序是用ebp索引局部变量,所以局部变量和ebp的位置一定要准确
# 而对于esp,只需要保证ebp索引局部变量时esp的值是小于该局部变量的位置即可
# init_state.regs.esp -= 0x100
# print(init_state.regs.esp, init_state.regs.ebp)
simu = pro.factory.simgr(init_state)
def success(state):
output = state.posix.dumps(1)
return b"Good Job." in output
def abort(state):
output = state.posix.dumps(1)
return b"Try again." in output
simu.explore(find=success, avoid=abort)
if simu.found:
res = simu.found[0]
solu0 = res.solver.eval(passwd0)
solu1 = res.solver.eval(passwd1)
solu = " ".join(map(str, [solu0, solu1]))
print(solu)
else:
print("No result!")
"""
1213922930 1153451551
"""
05_angr_symbolic_memory
如下截图所示,是本题的主逻辑,同样也是scanf读取多个参数,其中有四个参数都在bss段中。
求解脚本如下。
import angr
import claripy
pro = angr.Project("./05_angr_symbolic_memory")
start_addr = 0x08048603
init_state = pro.factory.blank_state(addr=start_addr)
# symbolics
syms = []
for i in range(4):
syms.append(claripy.BVS(str(i), 64))
# scanf 参数位置
bss = [0x0A29FAA0, 0x0A29FAA8, 0x0A29FAB0, 0x0A29FAB8]
for i in range(4):
init_state.memory.store(bss[i], syms[i])
good = 0x08048677
bad = 0x08048665
simu = pro.factory.simgr(init_state)
simu.explore(find=good, avoid=bad)
if simu.found:
res = simu.found[0]
flag = b""
for i in range(4):
flag += res.solver.eval(syms[i], cast_to=bytes) + b" " # cast_to only bytes or int
print(flag)
else:
print("No result!")
"""
QLVJFWGI YPEKZGCV LCWKCULV STBDTTXE
"""
06_angr_symbolic_dynamic_memory
如下截图所示,是本题的主要逻辑,scanf会读取两个参数,而两个参数的内存位置是由malloc动态分配的。
求解脚本如下。
import angr
import claripy
pro = angr.Project("./06_angr_symbolic_dynamic_memory")
start_addr = 0x0804869B
init_state = pro.factory.blank_state(addr=start_addr)
syms = []
for i in range(2):
syms.append(claripy.BVS(str(i), 64))
# 假的未使用的地址
fake_heap_addr = [0x4444444, 0x4444454]
# 参数指针地址
buffer_addr = [0x0A79A118, 0x0A79A120]
for i in range(2):
# angr 默认为大端,这里要使用程序的arch
init_state.memory.store(buffer_addr[i], fake_heap_addr[i], endness=pro.arch.memory_endness)
init_state.memory.store(fake_heap_addr[i], syms[i])
good = 0x08048763
bad = 0x08048751
simu = pro.factory.simgr(init_state)
simu.explore(find=good, avoid=bad)
if simu.found:
res = simu.found[0]
flag = b""
for i in range(2):
flag += res.solver.eval(syms[i], cast_to=bytes) + b" "
print(flag)
else:
print("No result!")
"""
IDMRHRCZ PLBQSLBO
"""
总结
不忘初心,砥砺前行!