载入二进制文件
载入二进制文件使用angr.Project函数,它的第一个参数是待载入文件的路径,后面还有很多的可选参数:
p = angr.Project("test",auto_load_libs = False)
auto_load_libs设置是否自动载入依赖的库,如果设置为True的话会自动载入依赖的库,然后分析到库函数调用时也会进入库函数,这样会增加分析的工作量,也有可能会跑挂。
如果auto_load_libs为true,那么程序如果调用到库函数的话就会直接调用真正的库函数,如果有的库函数逻辑比较复杂,可能分析程序就跑不出来了= = 同时angr使用python实现了很多的库函数(保存在angr.SIM_PROCEDURES),默认情况下会使用列表内不得函数来替换实际的函数调用,如果不在列表内才会进入到真正的library。跑windows程序由于环境复杂性太大,最好使其为False.
如果auto_load_libs为false,程序调用函数时,会直接返回一个 不受约束 的符号值。
喂参数给程序
创建init_state之前生成位向量符号argv1 ,作为程序的参数。
Copy argv1 = claripy . BVS ( "argv1" , 100 * 8 )
initial_state = p . factory . entry_state (args = [ "./crackme1" ,argv1])
正常输入
早期版本的angr是使用init_state.posix.files[0].read_from(1)
逐字节读取喂给的参数,并且进行约束。
现在几乎都是使用新版本的angr,在创建init_state
时使用
Copy initial_state = p . factory . entry_state (
addr =0x 401000 ,
stdin = flag,
)
其中addr与stdin都是可选参数。
entry_state:做一些初始化工作,然后在程序的默认入口(也可以指定addr)停下。在创建完entry_state,之后就可以通过这个state对象,获取或者修改此时程序的运行状态。
flag是使用claripy的 BVS() 方法生成的位向量符号,做为stdin输入,若没有传递stdin参数,那angr默认程序的输入为标准输入。
对于C++的程序,state时需要使用full_init_state方法并,设置unicorn引擎
Copy initial_state = p . factory . full_init_state (
add_options = angr.options.unicorn,
stdin = flag,
)
一些约束
BV:bitvector 构造符号值或具体值的一个比特容器,它具有大小。这些值并不是简单数值(不可以直接参与算术运算),它是一个bit序列,可以用有界整数(可以直接参与算术运算)来解释。 例:
建立一个32bit的符号值容器 "x":
claripy.BVS('x',32)
建立一个32bit的具体值(0xc001b3475)容器:
claripy.BVV(0xc001b3a75,32)
建立一个32bit的步进值,从1000到2000能被10整除的数:
claripy.SI(name='x',bits=32,lower_bound=1000,upper_bound=2000,stride=10)
下面类似z3的使用,直接使用claripy创建向量,flag_chars是一个BVS类型的list,这样创建便于对每字节数据做约束. flag用了Concat方法将list转化成一个完整的BVS向量,并在末尾加换行符。
Copy flag_chars = [claripy . BVS ( 'flag_ %d ' % i, 8 ) for i in range ( 32 ) ]
flag = claripy . Concat ( * flag_chars + [claripy. BVV ( b '\n' )])
或者也可以建立一个长度为 flag_length * 8
的符号值容器flag_val = claripy.BVS('flag', flag_length * 8)
然后将其转化成数组形式flag = flag_val.chop(8)
,这样就可以通过数组的方式访问flag[i].
添加约束,提高数据的生成效率:
可使用initial_state.add_constraints()
也可使用initial_state.solver.add()
Copy for k in flag_chars :
cond_0 = k >= ord ( '0' )
cond_1 = k <= ord ( '9' )
cond_2 = k >= ord ( 'a' )
cond_3 = k <= ord ( 'f' )
cond_4 = initial_state . solver . And (cond_0, cond_1)
cond_5 = initial_state . solver . And (cond_2, cond_3)
cond_6 = initial_state . solver . Or (cond_4, cond_5)
initial_state . solver . add (cond_6)
优化执行
大多数情况下,添加该参数可提高脚本运行效率。simulation.one_active.options.add(angr.options.LAZY_SOLVES)
任意位置加载程序
1.有些时候程序的输入数据被分类很多组,或者程序获取输入的逻辑实现的很复杂,严重干扰了angr分析,这时候就要跳过那些输入指令,加载程序,提高符号执行效率。
创建initial_state
时,使用factory的blank_state方法,传入地址,表示从该地址的状态开始。
Copy start_address = 0x 40083E
initial_state = p . factory . blank_state (addr = start_address)
2.之后就可以对寄存器,地址,做hook设置。
initial_state.memory.store(addr_in , bvs , endness='Iend_BE') 将你创建的BVS符号向量载入addr内存地址,第三个参数可以不用加,默认是大端的方式。
Copy Variables :
LE – little endian , least significant byte is stored at lowest address
BE – big endian , most significant byte is stored at lowest address
ME – Middle - endian . Yep .
recv = initial_state.memory.load(addr_out, size) 将addr地址处的数据取出size字节放到recv对象
initial_state.regs.rax = 0x1122334455667788 给寄存器赋值,或者取出寄存器的值。
initial_state.stack_push(0x1234) 往栈中压入一个值
initial_state.mem[initial_state.regs.esp+12:].dword = 0x25 在一个内存地址处 载入一个dword类型的数 ......
3.修复程序并执行
手动修复,给缺失的内存数据填入BVS符号。
比如我们的输入是在栈中
Copy lea rax , [rbp - 70h]
mov rsi , rax
mov edi , offset aS ; " %s "
mov eax , 0
call ___isoc99_scanf
我们将程序入口设置到scanf下方,现在我们需要了解,我们跳过的那些指令是如何调整栈空间的,我们要注入的符号位向量的确切的位置。从前面的分析可知,我们要注入的位置是 [RBP - 0x70] ,因此在压栈前我们我要填充栈,但是我们首先应当告诉 ebp 它应该是指向内存的什么位置。因此我们要用angr处理函数开头(我们跳过的部分): MOV RBP, RSP 。之后我们需要减小帧指针的值(模拟 sub esp, XXX),并将BVS写到RBP-0x70的位置
修复函数如下:
Copy initial_state . regs . rbp = initial_state . regs . rsp
bind_addr = initial_state . regs . rsp -0x 70
initial_state . regs . rsp -=0x 70
initial_state . memory . store (bind_addr, data)
对于更复杂的情况,比如当前位置的一个子函数加载程序后,涉及到从子函数中退出,会用到返回地址,栈帧,我们就必需要手动构造一个完整的栈结构。
开启PIE
对于开启PIE的程序,程序的基址固定在0x400000处,提取使用时应该加上该值。
反调试与hook
一些函数对结果没有影响,可直接将函数hook返回0,如printf类:
p.hook_symbol('printf',angr.SIM_PROCEDURES['stubs']['ReturnUnconstrained'](return_value=0),replace = True)
另外,对于代码自修改程序,需要使用如下的方式
p = angr.Project("crackme", support_selfmodifying_code=True)
对于静态链接的程序,需要hook函数地址为libc函数 或者有时需要自己优化函数。
Copy p . hook ( 0x 804f350 , angr.SIM_PROCEDURES[ 'libc' ][ 'scanf' ]())
p . hook ( 0x 8048d10 , angr.SIM_PROCEDURES[ 'glibc' ][ '__libc_start_main' ]())
Copy class my_function ( angr . SimProcedure )
def run (self , a ):
return a + 24
p . hook (addr, hook = my_function ())
hook到一些关键函数上,达到控制效果。
优化scanf就可以达到和控制返回值类似的效果。
Copy flag_chars = [claripy . BVS ( 'flag_ %d ' % i, 32 ) for i in range ( 13 ) ]
class my_scanf ( angr . SimProcedure ):
def run ( self , fmt , ptr ): # pylint: disable=arguments-differ,unused-argument
self . state . mem [ ptr ]. dword = flag_chars [ self . state . globals [ 'scanf_count' ]]
self . state . globals [ 'scanf_count' ] += 1
proj . hook_symbol ( '__isoc99_scanf' , my_scanf (), replace = True )
simulation . one_active . options . add (angr.options.LAZY_SOLVES)
simulation . one_active . globals [ 'scanf_count' ] = 0
这样程序每次调用scanf时,其实就是在执行my_scanf就会将flag_chars[i]存储到self.state.mem[ptr]当中,这其中ptr参数,其实就是本身scanf函数传递进来的rdi也,为了控制下标,我们设置了一个全局符号变量scanf_count。因为上面演示代码中程序中存在多处scanf输入。
或者如下面这样,让其自己获取输入的
Copy class my_scanf ( angr . SimProcedure ): # 固有格式
def run ( self , fmt , n ): # 参数为 (self + 该函数实际参数)
simfd = self . state . posix . get_fd ( 0 ) # 创建一个标准输入对对象
data , real_size = simfd . read_data ( 4 ) # 注意该函数返回两个值 第一个是读到的数据内容 第二个数内容长度
self . state . memory . store (n,data) # 将数据保存到相应参数内
return 1 # 返回原本函数该返回的东西
p . hook_symbol ( '__isoc99_scanf' , my_scanf (),replace = True )
# 这里%d对应int 是4个字节 但是读取到一个int所以返回1 所以这完全是模拟的原来的函数
路径探索
最后通过传入参数 initial_state 调用 simgr 函数创建 Simulation Manager 对象,在通过simulation执行explore方法找路径。
Copy simulation = p . factory . simgr (initial_state)
simulation . explore (find = addr1,void = addr2)
地址可传入list的形式
Copy yes = [ 0x 400567 , 0x 400756 , 0x 400835 ]
no = [ 0x 400435 , 0x 400342 , 0x 400526 ]
simulation . explore (find = yes,void = no)
如果路径很多的情况下可这样处理
Copy def is_successful ( state ):
stdout_output = state . posix . dumps (sys.stdout. fileno ())
if b 'Good Job.' in stdout_output :
return True
else :
return False
def should_abort ( state ):
stdout_output = state . posix . dumps (sys.stdout. fileno ())
if b 'Try again.' in stdout_output :
return True
else :
return False
simulation . explore (find = is_successful, avoid = should_abort)
打印值
Copy if simulation . found :
solution_state = simulation . found [ 0 ]
print solution_state . posix . dumps (sys.stdin. fileno ()). strip ( '\0\n' ) // 打印标准输入的值(未定义BVS)
print solution_state . solver . eval (flag, cast_to = bytes ) // 打印定义的BVS的值
else :
raise Exception ( 'Could not find the solution' )
过滤输出
过滤found后结果的输出
found的地址刚好设置在puts函数打印正确结果处, 此时的状态时,put将要打印edi寄存器的值. 取出edi里面的地址,暂且命名为flag_addr 取出flag_addr地址处的40字节的数据到flag对象,再进行约束
Copy found = simulation . found [ 0 ]
flag_addr = found . regs . rdi
found . add_constraints (found.memory. load (flag_addr, 5 ) == int (binascii. hexlify ( b "flag{" ), 16 ))
flag = found . memory . load (flag_addr, 40 )
for i in range ( 5 , 5 + 32 ):
cond_0 = flag . get_byte (i) >= ord ( '0' )
cond_1 = flag . get_byte (i) <= ord ( '9' )
cond_2 = flag . get_byte (i) >= ord ( 'a' )
cond_3 = flag . get_byte (i) <= ord ( 'f' )
cond_4 = found . solver . And (cond_0, cond_1)
cond_5 = found . solver . And (cond_2, cond_3)
found . add_constraints (found.solver. Or (cond_4, cond_5))
found . add_constraints (flag. get_byte ( 32 + 5 ) == ord ( '}' ))
最后将结果通过eval输出即可. flag_str = found.solver.eval(flag, cast_to=bytes)