解题源码以及二进制文件在:https://github.com/angr/angr-doc/tree/master/examples/cmu_binary_bomb
一道有趣的题目!
首先运行bomb文件,随便输入个字符串,bomb! 炸了!
总共有6关要闯,每次只要输入满足要求的数据才能进入下一关,否则就爆炸。
拖到IDA里面分析,发现第一关的字符串比较还蛮简单,下面几关就需要计算了。
int __fastcall phase_1(__int64 a1)
{
int result; // eax@1
result = strings_not_equal(a1, "Border relations with Canada have never been better.");
if ( result )
explode_bomb(a1, "Border relations with Canada have never been better.");
return result;
还是直接angr符号执行进行分析算了。
当然angr符号执行分析肯定也不简单,还是需要分析的。
(在我十分高估angr的时候我写出过如下代码:
大意就是模拟执行,找到包含0x400E49的路径,求解约束
import angr
import claripy
b=angr.Project('bomb',load_options={'auto_load_libs':False})
sm=b.factory.simulation_manager(b.factory.entry_state())
sm.explore(find=0x400E49)
print len(sm.found)
found=sm.found[0]
solution1 = found.posix.dumps(0)
print "print"
print repr(solution1)
Error: found=sm.found[0]
IndexError: list index out of range
呵,native!)
要想符号执行,首先要让angr能够模拟执行下去,如果都执行不下去,肯定是找不到包含目标地址的路径的啊。
那angr什么时候会执行不下去呢?我们需要提供哪些辅助呢?这个。。以后再说。先看这道题的用例都做了些什么。
首先是phase_1:
从IDA中,我们发现它的验证逻辑就是字符串比较,而且字符串以及明文在代码中了,所以逻辑非常简单。同时这个函数没有调用任何库函数以及系统调用,所以angr肯定能模拟执行的啦。
使用angr分析时,为了避免执行到angr无法模拟的函数或系统调用,用例代码只对phase_1函数进行了符号执行分析。
创建一个空状态,起始地址就是phase_1的起始地址;
创建一个符号变量,逆向发现phase_1是从reg.rdi寄存器读取输入数据的,因此将reg.rdi赋值为符号变量;
注意注意:reg.rdi存放的是地址,phase_1是先从reg.rdi取址,然后从这个地址读取字符串的。IDA里可以看到,read_line函数是从窗口读取数据放入到地址 0x603780中,然后将这个地址传递个reg.rdi的。(测试过,这个地址可以为任意值,只要不影响后面的执行就可以,毕竟只是提供了一个存放符号变量的媒介。)
初始化符号执行,找到phase_1的返回地址end = 0x400ef7,求解此时符号变量的值。
phase_2:
phase_2部分是输入6个数字进行判断,第一个数是否是1,然后后面一个数是否是前一个数的2倍。
phase_2函数调用了库 ___isoc99_sscanf,似乎angr不能处理呢,所以跳过了这个函数,符号执行的起始地址为:0x400f0a。
由于判断逻辑是从栈里面取数据,所以我们需要创建6个符号变量,并压入栈中。然后开始执行符号执行,结束地址就是phase_2函数的结束地址就可以。
phase_3:
phase_3部分是输入两个整数,switch case语句,根据第一个整数判断第二是否匹配。
与phase_2类型,不过这个函数有多个解,所以可以重点关注一下,如何求解多个解的方法。
对于explore发现的所有活跃(active)状态(or 路径)都加入队列里,分别求解它们的值。
同时对于found集合,不再只是取第一个found[0]而是对所有的found都进行遍历求解。
phase_4:
phase_4部分是输入两个整数,第二个整数不变,第一个整数递归计算,最后结果均为0。所以第二个数为0,第一个数利用angr求解为7。这里测试了一下栈数据的读取。有的时候通过found.regs.rsp - 0x18 + 0x8读取第一个参数,有的时候通过found.stack_pop()读取参数。其实它们是等价的。
rsp指向的是返回地址,所以会先found.stack_pop() 弹出返回地址 使rsp+0x8
rsp+0x8指向的是第一个参数 这时候found.stack_pop()弹出的才是第一个参数。
rsp+0xc指向的是第二个参数。
具体的内容可以学习一下:逆向基础 堆栈详解 —— https://mp.weixin.qq.com/s/PlLHRJ3XbJ6z6C5-rPXfEQ
phase_5:
phase_5需要接收长度6的字符串,遍历字符串的字符取byte值去一个字符数组中索引,拼接成一个新的字符串。新的字符串应该为"flyers"。
这次它利用了proj.kb.obj.get_symbol函数,通过函数名来获取想要分析的函数地址。
start = proj.kb.obj.get_symbol('phase_5').rebased_addr
avoid = proj.kb.obj.get_symbol('explode_bomb').rebased_addr
# let's stop at the end of the function
find = proj.kb.functions.get('phase_5').ret_sites[0].addr
需要再次注意的是,phase_5没有调用scanf函数所以此时参数的地址不是存放在栈里了,也就是说不是通过stack.pop来访问参数的。函数访问时是从寄存器rdi读取,就是phase_5函数参数寄存器。此时需要修改寄存器rdi中的内容,rdi指向的是新开辟的一段栈空间,里面存放的是符号变量(??目前是这样猜的,在不创建符号变量时,所以的变量都是符号的??)。然后求解这个地址的值就好了。(如果我将rdi赋值为任意一个内存地址是不行的,似乎一定时某个栈地址才可以)
phase_6:
phase_6里面好多while循环啊,而且还有while(1),正好学习一下如何避免路径爆炸。
phase_6中关闭了,LAZY_SOLVES选项(remove_options={angr.options.LAZY_SOLVES})。
LAZY_SOLVES:在LAZY_SOLVES打开的情况下,只要路径在基本块的末尾分支为几个,angr就不会主动检查每个成功的状态是否可以满足。相反,angr认为他们都是可以满足的,并且继续创造成功的路径。如果这些成功状态中的任何一个不成立,并且稍后必须发生一个解决方案(例如,具体化一个符号内存写入目标),定理证明者将无法找到在该状态下收集的所有约束的解决方案(因为它不满足),以及会引发异常SimMemoryAddressError。 简而言之,LAZY_SOLVES是在路径爆炸(创建更多路径)和花费在约束求解上的时间之间的平衡。
在angr的issues中,很多人讨论了关于这个LAZY_SOLVES选项,一些人认为应该默认关闭。因为,往往它会将一些不满足的状态加入到路径中,最后求解时会报错,同时还会引起路径爆炸。感兴趣的可以去参考一下。
总之,默认LAZY_SOLVES是开启的,它可以加快我们分析的速度,因为它不会对每个状态都进行判断。
如果遇到while循环等可能存在路径爆炸的问题,则需要把这个选项关闭(remove_options={angr.options.LAZY_SOLVES})。
此外呢,phase_6终于没有跳过read_six_num()->scanf()这两个函数,而是采用hook的方式,对这个函数进行了替换。hook函数的使用方法就不在赘述啦,不过这一次传入的是一个实现了angr.SimProcedure接口的类。这个地方实在是写的妙啊,所以把代码贴出来。
原来的函数:
int __fastcall read_six_numbers(__int64 a1, __int64 a2)
{
int result; // eax@1
result = __isoc99_sscanf(a1, (__int64)&unk_4025C3, a2, a2 + 4);
if ( result <= 5 )
explode_bomb(a1, &unk_4025C3);
return result;
}
hook后的函数:
class read_6_ints(angr.SimProcedure):
answer_ints = [] # class variable
int_addrs = []
def run(self, s1_addr, int_addr):
self.int_addrs.append(int_addr)
for i in range(6):
bvs = self.state.solver.BVS("phase6_int_%d" % i, 32)
self.answer_ints.append(bvs)
self.state.mem[int_addr].int.array(6)[i] = bvs
return 6
hook传入的类要实现一个run函数,它将替换原来的read_six_numbers函数的功能。其中s1_addr就是read_six_numbers的参数a1,int_addr就是原函数参数a2,也就是存放用户输入6个整数的存放地址。在run函数中,我们创建6个符号变量,并依次按照整型数组存放在int_addr地址中。这可真的是从源头将变量符号化了。
接下来就简单了,只需要求解就可以了。一次性求解6个符号变量值的语句为:
answer = [found.solver.eval(x) for x in read_6_ints.answer_ints]
cool!
secret_phase:
接收一个整数,递归计算,看最后的结果是否为2。还有添加一个约束条件,就是输入的整数长度不能大于1000。
这个函数里又有了递归,一般有递归就需要把LAZY_SOLVES关闭了(避免路径爆炸)。
这个约束条件就非常简单了。其他地方已经在前面都讲过了,包括为什么hook strtol函数。直接看用例代码就可以了。
同时我还对用例代码进行更改测试,就是从地址 start=0x401248开始符号执行(也就是read_line函数之后),也可以得到正确的结果。
用例代码:
import angr
import claripy
import logging
from struct import unpack
class readline_hook(angr.SimProcedure):
def run(self):
pass
class strtol_hook(angr.SimProcedure):
def run(self, str, end, base):
return self.state.solver.BVS("flag", 64, explicit_name=True)
def solve_flag_1():
# shutdown some warning produced by this example
#logging.getLogger('angr.engines.vex.irsb').setLevel(logging.ERROR)
proj = angr.Project('bomb', auto_load_libs=False)
start = 0x400ee0
bomb_explode = 0x40143a
end = 0x400ef7
# initial state is at the beginning of phase_one()
state = proj.factory.blank_state(addr=start)
# a symbolic input string with a length up to 128 bytes
arg = state.solver.BVS("input_string", 8 * 128)
# read_line() reads a line from stdin and stores it a this address
bind_addr = 0x603780
# bind the symbolic string at this address
state.memory.store(bind_addr, arg)
# phase_one reads the string [rdi]
state.add_constraints(state.regs.rdi == bind_addr)
# Attempt to find a path to the end of the phase_1 function while avoiding the bomb_explode
simgr = proj.factory.simulation_manager(state)
simgr.explore(find=end, avoid=bomb_explode)
if simgr.found:
found = simgr.found[0]
return found.solver.eval(arg, cast_to=str).rstrip(chr(0)) # remove ending \0
else:
raise Exception("angr failed to find a path to the solution :(")
def solve_flag_2():
proj = angr.Project('bomb', auto_load_libs=False)
bomb_explode = 0x40143a
# Start analysis at the phase_2 function after the sscanf
state = proj.factory.blank_state(addr=0x400f0a)
# Sscanf is looking for '%d %d %d %d %d %d' which ends up dropping 6 ints onto the stack
# We will create 6 symbolic values onto the stack to mimic this
for i in xrange(6):
state.stack_push(state.solver.BVS('int{}'.format(i), 4*8))
# Attempt to find a path to the end of the phase_2 function while avoiding the bomb_explode
ex = proj.surveyors.Explorer(start=state, find=(0x400f3c,),
avoid=(bomb_explode,),
enable_veritesting=True)
ex.run()
if ex.found:
found = ex.found[0]
answer = []
for _ in xrange(3):
curr_int = found.solver.eval(found.stack_pop())
# We are popping off 8 bytes at a time
# 0x0000000200000001
# This is just one way to extract the individual numbers from this popped value
answer.append(str(curr_int & 0xffffffff))
answer.append(str(curr_int>>32 & 0xffffffff))
return ' '.join(answer)
def solve_flag_3():
args = []
proj = angr.Project('bomb', auto_load_libs=False)
start = 0x400f6a # phase_3 after scanf()
bomb_explode = 0x40143a
end = 0x400fc9 # phase_3 before ret
state = proj.factory.blank_state(addr=start)
# we want to enumerate all solutions... let's have a queue
queue = [state, ]
while len(queue) > 0:
state = queue.pop()
#print "\nStarting symbolic execution..."
ex = proj.surveyors.Explorer(start=state, find=(end,),
avoid=(bomb_explode,),
enable_veritesting=True,
max_active=8)
ex.run()
#print "Inserting in queue " + str(len(ex.active)) + " paths (not yet finished)"
for p in ex.active:
queue.append(p)
#print "Found states are " + str(len(ex.found))
#print "Enumerating up to 10 solutions for each found state"
if ex.found:
for p in ex.found:
found = p
found.stack_pop() # ignore, our args start at offset 0x8
iter_sol = found.solver.eval_upto(found.stack_pop(), 10) # ask for up to 10 solutions if possible
for sol in iter_sol:
if sol == None:
break
a = sol & 0xffffffff
b = (sol >> 32) & 0xffffffff
#print "Solution: " + str(a) + " " + str(b)
args.append(str(a) + " " + str(b))
return args
def solve_flag_4():
avoid = 0x40143A
find = 0x401061
proj = angr.Project("./bomb", auto_load_libs=False)
state = proj.factory.blank_state(
# let's get the address via its symbol
# after a proj.analysis.CFG it can be recovered by
# addr=proj.kb.functions.get('phase_4').addr,
# we will just use the obj's symbol directly
addr=proj.kb.obj.get_symbol('phase_4').rebased_addr,
remove_options={angr.options.LAZY_SOLVES})
sm = proj.factory.simulation_manager(state)
sm.explore(find=find, avoid=avoid)
found = sm.found[0]
# stopped on the ret account for the stack
# that has already been moved
answer = unpack('II', found.solver.eval(
found.memory.load(found.regs.rsp - 0x18 + 0x8, 8), cast_to=str))
return ' '.join(map(str, answer))
def solve_flag_5():
def is_alnum(state, c):
# set some constraints on the char, let it
# be a null char or alphanumeric
is_num = state.solver.And(c >= ord("0"), c <= ord("9"))
is_alpha_lower = state.solver.And(c >= ord("a"), c <= ord("z"))
is_alpha_upper = state.solver.And(c >= ord("A"), c <= ord("Z"))
is_zero = (c == ord('\x00'))
isalphanum = state.solver.Or(
is_num, is_alpha_lower, is_alpha_upper, is_zero)
return isalphanum
# getting more lazy, let angr find the functions, and build the CFG
proj = angr.Project("./bomb", auto_load_libs=False)
proj.analyses.CFG()
start = proj.kb.obj.get_symbol('phase_5').rebased_addr
avoid = proj.kb.obj.get_symbol('explode_bomb').rebased_addr
# let's stop at the end of the function
find = proj.kb.functions.get('phase_5').ret_sites[0].addr
state = proj.factory.blank_state(
addr=start, remove_options={angr.options.LAZY_SOLVES})
# retrofit the input string on the stack
state.regs.rdi = state.regs.rsp - 0x1000
string_addr = state.regs.rdi
sm = proj.factory.simulation_manager(state)
sm.explore(find=find, avoid=avoid)
found = sm.found[0]
mem = found.memory.load(string_addr, 32)
for i in xrange(32):
found.add_constraints(is_alnum(found, mem.get_byte(i)))
return found.solver.eval(mem, cast_to=str).split('\x00')[0]
# more than one solution could, for example, be returned like this:
# return map(lambda s: s.split('\x00')[0], found.solver.eval_upto(mem, 10, cast_to=str))
class read_6_ints(angr.SimProcedure):
answer_ints = [] # class variable
int_addrs = []
def run(self, s1_addr, int_addr):
self.int_addrs.append(int_addr)
for i in range(6):
bvs = self.state.solver.BVS("phase6_int_%d" % i, 32)
self.answer_ints.append(bvs)
self.state.mem[int_addr].int.array(6)[i] = bvs
return 6
def solve_flag_6():
start = 0x4010f4
read_num = 0x40145c
find = 0x4011f7
avoid = 0x40143A
p = angr.Project("./bomb", auto_load_libs=False)
p.hook(read_num, read_6_ints())
state = p.factory.blank_state(addr=start, remove_options={angr.options.LAZY_SOLVES})
sm = p.factory.simulation_manager(state)
sm.explore(find=find, avoid=avoid)
found = sm.found[0]
answer = [found.solver.eval(x) for x in read_6_ints.answer_ints]
return ' '.join(map(str, answer))
def solve_secret():
start = 0x401242
find = 0x401282
avoid = (0x40127d, 0x401267,)
readline = 0x40149e
strtol = 0x400bd0
p = angr.Project("./bomb", auto_load_libs=False)
p.hook(readline, readline_hook)
p.hook(strtol, strtol_hook)
state = p.factory.blank_state(addr=start, remove_options={angr.options.LAZY_SOLVES})
flag = claripy.BVS("flag", 64, explicit_name=True)
state.add_constraints(flag -1 <= 0x3e8)
sm = p.factory.simulation_manager(state)
sm.explore(find=find, avoid=avoid)
### flag found
found = sm.found[0]
flag = found.solver.BVS("flag", 64, explicit_name="True")
return str(found.solver.eval(flag))
def main():
print "Flag 1: " + solve_flag_1()
print "Flag 2: " + solve_flag_2()
print "Flag(s) 3: " + str(solve_flag_3())
print "Flag 4: " + solve_flag_4()
print "Flag 5: " + solve_flag_5()
print "Flag 6: " + solve_flag_6()
print "Secret : " + solve_secret()
def test():
assert solve_flag_1() == 'Border relations with Canada have never been better.'
print "Stage 1 ok!"
assert solve_flag_2() == '1 2 4 8 16 32'
print "Stage 2 ok!"
args_3 = ["0 207", "1 311", "2 707", "3 256", "4 389", "5 206", "6 682", "7 327"]
res_3 = solve_flag_3()
assert len(res_3) == len(args_3)
for s in args_3:
assert s in res_3
print "Stage 3 ok!"
assert solve_flag_4() == '7 0'
print "Stage 4 ok!"
assert solve_flag_5().lower() == 'ionefg'
print "Stage 5 ok!"
assert solve_flag_6() == '4 3 2 1 6 5'
print "Stage 6 ok!"
assert solve_secret() == '22'
print "Secret stage ok!"
if __name__ == '__main__':
# logging.basicConfig()
# logging.getLogger('angr.surveyors.explorer').setLevel(logging.DEBUG)
main()