文章目录
前言
新鸟入门,听师兄们说已经蛮久没有遇到过迷宫类题目了;但是我觉得出不出是一回事,我起码要会吧hh。所以还是学一学;而且最近新生赛遇到了好几道,总结一下;
–真看的时候好多不会,DFS和BFS要学,数据结构还欠着o(╥﹏╥)o
迷宫问题
迷宫问题一般有以下特点:
- 在内存中布置一张 “地图”
- 将用户输入限制在少数几个字符范围内.
- 一般只有一个迷宫入口和一个迷宫出口
地图一般会用0和1或者是*号和#号表示,有一些地图是存储在内存中的,分析的时候可以直观的看到,有一些是动态生成的,这个就需要结合动态调试来完成。
用户的输入通常会是awds或是jkli这类明显的用来表示方向的按键,具体看题目分析。
一般情况下, 迷宫是只有 1 个入口和 1 个出口,也有可能用一些符号来表示入口和出口;解密时需要根据具体情况判断,迷宫的路线route也有可能不止一条,题目可能会要求一些特定条件,例如最短时间内走完。
DFS和BFS算法讲解
深度优先遍历(Depth Fist Search)
什么是深度优先,用树来看就是从一个结点开始,沿着其中一条支路,一直搜索下去直到尽头;然后再原路返回起点,再找另一条路不断遍历下去。
用迷宫来讲的话,就是从起点开始,不断向四周的格子走,不考虑回头的话,总有到尽头的时候(终点或是一堵墙)。到尽头后,回到起点,再沿着没有走过的路走一次,不断重复这个过程。
深度优先算法有俩种实现方法,一种是利用递归实现,一种是非递归实现。
广度优先遍历(Breath First Search)
广度优先遍历指的是从图的一个未遍历的结点出发,先遍历这个点的相邻结点,再依次遍历每个相邻节点的相邻节点。
在迷宫的例子里,就是说我们按照走1步能到达的位置,走2步能到达的位置…这样的顺序逐一搜索,直到不再有能到达的位置。
深度优先利用的是栈实现,广度优先利用的是队列实现。
具体代码实现部分可以看github算法
图文解析可以看图文详解
具体步骤和代码实现
maze
这里用NSSCTF上的题目 [SWPUCTF 2021 新生赛]老鼠走迷宫 作为例子(先不管程序,下面实战再具体分析)用的是深度优先遍历dfs。
maze = [
[
1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[
1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1],
[
1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1],
[
1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1],
[
1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1],
[
1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1],
[
1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1],
[
1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1],
[
1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1],
[
1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1],
[
1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1],
[
1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1],
[
1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1],
[
1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1],
[
1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1],
[
1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1],
[
1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1],
[
1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1],
[
1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1],
[
1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1],
[
1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1],
[
1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1],
[
1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1],
[
1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1],
[
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1]]
这里有一个地方我进行了修改,最后一行的倒数第二个位置原本是0,我手动修改成了2;原题有明确该坐标是出口。maze[0][1]处是入口。
具体操作
- 先写从当前位置前往下一个位置的限制条件;(例如,通行的要求,是否遇到墙;走到终点了)
# 添加限制条件
def check(map, x, y):
if (x >= 0) and (x <= 24) and (y >= 0) and (y <= 24): # 这一行检查(x,y)是否在有效范围内;
return (map[x][y] != 1) and ((map[x][y] == 0) or (map[x][y] == 2)) # 这里是如果坐标在地图范围内,就检测该坐标是不是障碍物;是不是可以前进的道路或者终点;
else:
return False
- 写出当前位置四周的情况,并存入一个列表里。该列表中的每一项包含三个信息(x坐标,y坐标,前进方向)
def gen_nex(map, x, y):
all_dir = []
if check(map, x - 1, y):
all_dir.append((x - 1, y, 'w'))
if check(map, x + 1, y):
all_dir.append((x + 1, y, 's'))
if check(map, x, y - 1):
all_dir.append((x, y - 1, 'a'))
if check(map, x, y + 1):
all_dir.append((x, y + 1, 'd'))
return all_dir
# all_dir是一个列表,用于存储下一个可能的坐标和前进方向的信息。每个元素都是一个包含三个值的元组,格式如下:(x, y, direction)
- 写出DFS用递归查找路线
- ①第一步先写判断dfs成功的条件
- ②将访问过的值设置成其它特殊值,即我们走过一遍的地方不能再走,不然会出bug,俩个点来回横跳。
- ③进行递归dfs调用
def check_success(map, x, y):
if map[x][y] == 2:
return True
else:
return False
def dfs(maze, x, y, path):
map = maze.copy() # 这里用将maze复制给map,避免修改掉原地图。
if map[x][y] != 2:
map[x][y] = 1
if check_success(map, x, y):
print(path)
return True
next_point = gen_nex(map, x, y)
for n in next_point:
pathn = path + n[2] # 将all_dir列表中的元组的第三个值,即方向传给pathn
dfs(map, n[0], n[1], pathn) # 这里开始递归 用all_dir的元组第一二个值和pathn作为参数,进行当前位置的又一次深度优先遍历。
最后再加个尾巴
output = ""
dfs(maze, 0, 1, output)
运行结果:
‘’
实战分析
[SWPUCTF 2021 新生赛]老鼠走迷宫
简单再讲一下;这一题是pyinstaller类的题目;
题目附件,再没有任何有用信息的情况下先查壳;
用pyinstxtractor.py反编译一下;得到一个附件_extracted文件夹,打开
再用uncompyle6,将5.pyc反编译成py文件。
打开5.py就好了,上面是地图,下面关于输入也没有什么值得注意的。最后flag是将route进行一次md5加密。
‘’
NSSCTF [GDOUCTF 2023]润!
拿到题目先查壳,这里看出UPX壳但是是魔改过的upx壳;随便拿个正常的exe加个壳看看,再对比一下魔改后的壳
这里用十六进制编辑器进行修改,winhex, 将FUK都改成UPX就好;
保存就好,再看就是正常的upx壳了,用upx -d
第一次upx -d出错了,原来是忘记还有个FUK!
之后用IDA分析就好,这个真的 今天磨了一天的迷宫。上来随便找道题就是三维地图,虽然做出来了就挺简单了hh,我做完才发现这一题和最近个新生赛的题是一样的(o)/~。。其实本来就是因为那道题不会做才来学迷宫了,截止到现在好像才16解,斯 ~
init是创建迷宫, 让我们输入长度为31的字符串,moving是在迷宫中的移动,这里的511是个大hint。
‘’
init点进去看看,
这里具体迷宫的生成具体可以不用在意,可以明显看出这是一个 8 * 8大小的迷宫;
moving这里一开始真没看懂怎么走的,后面才明白是三维的,还有上下楼这种操作。注意看:在case ‘u’:处又调用了一次init创建了一个迷宫,还有layer也是个hint。这里就体现了英文水平了,我一点也不敏感,layer是层的意思,这里就是有八层。 8 * 8 * 8 = 512 ;即意味着我们的终点就是左下角的位置,511;起点是一楼的左上角。我觉得难点就在这了,后面就按部就班。
哦还有,需要动态调试,输入uuuuuuuuuuuuuuuuuuuu就好,连续创建七次迷宫,将迷宫copy出来就好,上exp了
maze = [0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1,
1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1,
1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 2]
# maze[x * 64 + y * 8 + z]
def check_point_valid(map, x, y, z):
if (x >= 0) and (x <= 7) and (y >= 0) and (y <= 7) and (z >= 0) and (z <= 7):
return (map[x * 64 + y * 8 + z] != 1) and ((map[x * 64 + y * 8 + z] == 0) or (map[x * 64 + y * 8 + z] == 2))
else:
return False
def gen_nex(map, x, y, z):
all_dir = []
if check_point_valid(map, x - 1, y, z):
all_dir.append((x - 1, y, z, 'n'))
if check_point_valid(map, x + 1, y, z):
all_dir.append((x + 1, y, z, 'u'))
if check_point_valid(map, x, y - 1, z):
all_dir.append((x, y - 1, z, 'w'))
if check_point_valid(map, x, y + 1, z):
all_dir.append((x, y + 1, z, 's'))
if check_point_valid(map, x, y, z - 1):
all_dir.append((x, y, z - 1, 'a'))
if check_point_valid(map, x, y, z + 1):
all_dir.append((x, y, z + 1, 'd'))
return all_dir
def check_success(map, x, y, z):
if map[x * 64 + y * 8 + z] == 2:
return True
else:
return False
def dfs(mapb, x, y, z, path):
map = mapb.copy()
if map[x * 64 + y * 8 + z] != 2:
map[x * 64 + y * 8 + z] = 1
if check_success(map, x, y, z):
print(path)
return True
next_point = gen_nex(map, x, y, z)
for n in next_point:
pathn = path + n[3]
dfs(map, n[0], n[1], n[2], pathn)
outpus = ""
dfs(maze, 0, 0, 0, outpus)
一样的步骤,只是上一题二维的用map[x][y]我觉得比用map[25 * x + y]好点,这一题我又觉得map[ 64 * x + 8 * y + z] 好点hh。x表示楼层,y是w和s表示行,z是列。后面的也一样进行微调就好。
解出的route:ssddssuuwwddndduuussdussasauudd
BUUCTF:[HDCTF2019]Maze
先查壳,脱壳勒,最简单upx -d了
花指令,nop掉他!这里第一处call直接nop就好,然后 将数据转换为代码,要用force。这里会有3个黄色的nop掉再转为代码就好
下面还有一处
这里如果nop掉 or的话 后面会出错,所以选择把jmp直接nop掉,nop掉后用 p 创建函数,再反编译就好
这里就清晰多了,没有找到地图在哪,按Shift+F12碰碰运气,
提取出来,因为也没有找到迷宫的规模在哪里,结合一共有70个字符,而且一共走了14步,asc_408078 = 5,dword_40807C = -4,可以大概猜出 7 * 10 的迷宫大小。
map = ['*', '*', '*', '*', '*', '*', '*', '+', '*', '*', '*', '*', '*', '*', '*', '*', '*', ' ', '*', '*', '*', '*', '*', '*', ' ', ' ', ' ', ' ', '*', '*', '*', '*', ' ', ' ', ' ', '*', '*', '*', '*', '*', '*', '*', ' ', '*', '*', 'F', '*', '*', '*', '*', '*', '*', ' ', ' ', ' ', ' ', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*', '*']
def check_point_valid(map, x, y):
if (x >= 0) and (x <= 7) and (y >= 0) and (y <= 9):
return (map[10 * x + y] != '*') and ((map[10 * x + y] == ' ') or (map[10 * x + y] == 'F'))
else:
return False
def gen_nex(map, x, y):
all_dir = []
if check_point_valid(map, x - 1, y):
all_dir.append((x - 1, y, 'w'))
if check_point_valid(map, x + 1, y):
all_dir.append((x + 1, y, 's'))
if check_point_valid(map, x, y - 1):
all_dir.append((x, y - 1, 'a'))
if check_point_valid(map, x, y + 1):
all_dir.append((x, y + 1, 'd'))
return all_dir
def check_success(map, x, y):
if map[10 * x + y] == 'F':
return True
else:
return False
def dfs(mapb, x, y, path):
map = mapb.copy()
if map[10 * x + y] != 'F':
map[10 * x + y] = 1
if check_success(map, x, y):
print(path)
return True
next_point = gen_nex(map, x, y)
for n in next_point:
pathn = path + n[2]
dfs(map, n[0], n[1], pathn)
outpus = ""
dfs(map, 0, 7, outpus)
差不多的exp,修修补补,应该大部分迷宫题都能解吧,大概吧,如果再加别的知识点那就是别的事了。这里map本来是字符串的形式,但是py会报类型出错,所以就改了一下,写个小脚本输出一下就好。
后言
没有细讲DFS和BFS的原因是我自己也一知半解的,数据结构和算法分析还欠着,找时间该补上了。
又到了碎碎念的环节了hh反正没几个人看,自说自话,今天想写这个才发现自己以前很多东西都是没有学到位的,因为当我想把自己理解到的东西转换成自己的再输出出来时,才发现,真的1好难a。DFS和BFS都还不是很理解,想写也不知道该怎么组织语言,不想完全复制粘贴一些大佬的笔记o(╥﹏╥)o没有意义a,本来写博客的原因之一就是记录学习,那么记录下来的东西肯定要是自己学会的,而且如果能对看到的人有帮助就更好啦。碎碎念结束,明天继续加油,迟早把今天欠的补上!
部分文献来源:
https://ctf-wiki.org/reverse/maze/maze/
https://mp.weixin.qq.com/s/T6ML7zwA57JXTRwOZqcxhw?spm=a2c6h.12873639.article-detail.7.19f31041PU5YhX
https://blog.csdn.net/weixin_40519529/article/details/113081269
https://github.com/Locietta/blogs/issues/14