该题是由rpgmakerxp做的一个小迷宫。解压后一看,根目录下有个config.txt,里面为202.120.7.132:9999,初步判断游戏跟服务器有交互。运行游戏,方向键移动角色,wireshark确实能抓取到与服务器交互的数据。
用rpgmakerxp编辑该游戏的方法是:用rpgmakerxp新建一个游戏,保存,把游戏目录下的Game.rxproj复制到RPG目录里,再用rpgmakerxp打开该文件即可。
打开后,可以看到迷宫尺寸为499*499。同时在rpgmakerxp中,下标起始为0,用二元组表示为(列,行)。角色起始坐标在(1,1),宝箱在(497,497)。题目描述为打开宝箱即得flag。
从rpgmakerxp的工具菜单里打开脚本编辑器,拖到最下面看Main脚本,其中有这样的一段:

1
2
3
host, port = File.new("config.txt", "r").readline().strip().split(":")
$CONN = TCPClient.new(host, port.to_i)
$CONN.connect()

再看Gmae_Player脚本中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def move_up(turn_enabled = true)
super
$CONN.send("w\n")
end
def move_down(turn_enabled = true)
super
$CONN.send("s\n")
end
def move_left(turn_enabled = true)
super
$CONN.send("a\n")
end
def move_right(turn_enabled = true)
super
$CONN.send("d\n")
end

最后在模式菜单里切换到事件,双击位于(497,497)的宝箱,可以看到如下脚本:

1
2
$CONN.send("flag\n")
print $CONN.recvuntil("\n")

根据上述脚本可以基本确定程序流程:

  • 游戏运行时从当前目录config.txt里读入服务器ip和端口,建立连接
  • 玩家移动游戏角色时触发key_down事件,上下左右移动时分别向服务器发送wasd和换行符
  • 打开宝箱时触发事件,向服务器发送’flag\n’并将服务器返回的数据打印出来

先尝试将宝箱移动到坐标(2,1),保存,运行游戏后宝箱即在玩家原始位置右边。打开宝箱,提示no cheat,此法不通。
再尝试将玩家移动到(496,497),保存,运行游戏后玩家即在宝箱原始位置上面。打开宝箱,同样提示no cheat,此法同样行不通。
至此可初步判断,服务器上会对传来的方向序列进行处理,达到正确位置后打开宝箱才能获取flag。这样,我们就需要真正走完迷宫,到达(496,497)处打开宝箱。因此我们可以根据rpgmaker里看到的全地图来手动走完整个迷宫,也可以想办法把地图数据导出来,用程序寻路并且连接服务器发送路线来获得flag。

经过对比发现,地图的层1都是浅色的草地,层2是迷宫墙壁,层3是空的。rpgmakerxp里有选块功能,并且可以复制粘贴。经过对比,选择一个选块,复制粘贴会同时将三个层一起处理。此外,请看下面的对比:
选择一个选块,将其三个层均清空,粘贴到winhex里的一个空文件里,可以看到如下十六进制数据:
0E000000 01000000 01000000 0000 0000 0000
将该选块层1恢复为浅色草地,再粘贴到winhex里,十六进制数据变为:
0E000000 01000000 01000000 8001 0000 0000
将层2恢复为迷宫墙壁,再复制出来,十六进制数据变为:
0E000000 01000000 01000000 8001 3400 0000
将层3填充为任意元素,复制出来的十六进制数据为:
0E000000 01000000 01000000 8001 3400 9F01
横向选择(0,0)、(1,0),即第一行前两个选块,其十六进制数据为:
14000000 02000000 01000000 8001 8001 3400 4C00 0000 0000
纵向选择(0,0)、(0,1),即第一列前两个选快,其十六进制数据为:
14000000 01000000 02000000 8001 8001 3400 4800 0000 0000
选择(0,0),(1,0),(0,1),(1,1)四个选块,十六进制数据为:
20000000 02000000 02000000 8001 8001 8001 8001 3400 4C00 4800 0000 0000 0000 0000 0000
综合上述数据分析得知,从rpgmakerxp地图编辑器里复制出来的十六进制数据,格式是:

  • 4字节总长度T
  • 4字节列数C
  • 4字节行数R
  • R*C字节层1数据(行优先)
  • R*C字节层2数据(行优先)
  • R*C字节层3数据(行优先)

如上述最后一组数据,总长度0x20字节,存在0x02行,0x02列;
第一行第一列层1数据为0x8001,层2数据为0x3400,层3数据为0x0000
第一行第二列层1数据为0x8001,层2数据为0x4c00,层3数据为0x0000
第一行第一列层1数据为0x8001,层2数据为0x4800,层3数据为0x0000
第一行第一列层1数据为0x8001,层2数据为0x0000,层3数据为0x0000

又根据上面得出的结论,该RPG地图中迷宫墙壁均在层2。再结合上述数据分析,层2不为0x0000的块为迷宫壁,0x0000的块为通道。

因此思路就明确了,rpgmakerxp中ctrl+a全选地图所有块,复制,粘贴到winhex中,保存。再用python写段小程序来解析其中地图数据,即可得到文本化的地图数据,最后使用寻路算法得到迷宫的解,用socket向服务器发送就可以得到flag。

解析地图用的python小脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import struct

with open('map.bin', 'rb') as f:
bMAP = f.read()

bMAP = bMAP[12:]
bMAP = bMAP[499*499*2:]
c = 0
sMAP = ''

for i in xrange(499):
for j in xrange(499):
if struct.unpack("H", bMAP[c:c+2])[0] == 0:
sMAP += '0'
else:
sMAP += '1'
c +=2
if j == 498:
sMAP += '\n'

with open('map.txt', 'w') as f:
f.write(sMAP[:-1])

print 'done'

迷宫寻路小脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import sys   
sys.setrecursionlimit(1000000)

with open('map.txt', 'r') as f:
lines = f.readlines()

grid = []
for line in lines:
grid.append(list(line.strip('\n')))

grid[497][497] = '2'
route = ''
def search(x, y):
global route
if grid[x][y] == '2':
return True
elif grid[x][y] == '1':
return False
elif grid[x][y] == '3':
return False

# mark as visited
grid[x][y] = '3'
# explore neighbors clockwise starting by the one on the right

#down, s
if (x < len(grid)-1 and search(x+1, y)):
route += 's'
return True
#left, a
if (y > 0 and search(x, y-1)):
route += 'a'
return True
#up, w
if (x > 0 and search(x-1, y)):
route += 'w'
return True
#right, d
if (y < len(grid)-1 and search(x, y+1)):
route += 'd'
return True
return False

search(1, 1)
print route[::-1]

socket发送脚本:

1
2
3
4
5
6
7
8
9
10
import socket
s = "ddddddddddddssdwdsddssssssssassdddddssdwwdddssdwdwwwddddwwwwwwwwwwdsdsssssssdddssassssssssaawwaaaassaassssddddssssdssaassddsssasssssaawwaaaawwddwwwaasaawwwaasssssddssssssssssdwdwwwwwdssddssassssassdssssssaaassdddssddddssddddssdssaaaaassssssssssssaassaawwwwwwaaassssssaaaaassssawwawaasaassssaaaawwaaaaaaassssdwwdddssssdddssdwdsssdddssddssaasssssdsssddssdddsssdsdddssssssdssdwdwwwdwwawwwwwwdsssdsdddwwdddwwdwwdssdwdwdddwwawwaaaawwddddwwdddssssddssddddddwwaaawwddddwwdsdssssssdwdssssssaaaawwaaassdssddssdwdwddddssssssssssaaaawwdwwawwaaaassawwawaasaassaassaaaawwaaaassssssssddddddwwwaawdddsdsssddssssssddssssssssdwdwdsdsddddwwdddddddddsssdsdwdwwwdwwdssdwdwddwwwwddwwdsdsdwdwdsdsssddddwwwwddwwwwwwddddssdssssdwwdssddssassssssdwdwwwdsdsdssassaassssdwdwddddssssssddddwwwwwaawdddsdsssssddddssddssdwdwwwaawwwaasawwawwddwwwwdssddddddssssdddwwdddssdwwwawaaawwwwaawwddddddwwddwwddwwdsdsssssssaawaasaaassdddddddddddsssdwdssdwdwddddddddwwdsdsdwdwwwdsdsssddddssssaawaasassssdssdwdwwwdddddssssdssdwdwwwwwdssddsssdsssaassssaaassdddssddddddssssddddssssddssdwdwdsdsddssssssssaaassawwaawaasssddssdsssssdsdwdsddssssdwdwdsdsdwdwdwwaaaaawwwwddddddwwddwwwdsdwwdsdsssaassssdwdwddwwdsdwwawawwwwddwwwwdsdsddddwwwwdwwaawwawwwwddddssssdwdwwwdddddwwawwddwwwaawdddsdsssssddssssdwdwdddssdwwwwddddwwwwdwwdssdssssdddwwwwdsdsssdddddssssssassassawwwwwwwaasssaaassddssaaaassssaaaaassssdsssdsssssasssasaawwaaaaassdddssddssssaawwaaaaaaaassssdwdsssddddddwwdsdsssdssaaaawwaaassaawaasssdddddddssssawaasssddssdssaaawwaaaaassdddssssdssssdddddwwwaawwdsdwwdsdsssssssaaassdddddwwdwwawwddddddssdddwwdssdwdwdddwwawaawddddddddwwwwaawwwwddwwwwdssddssdwdsdwdsdwwdsdsdwwdddwwdddssassassassssssdwdwwwddwwdddssssaassdddddddssdwdwdwwawwddwwwwddwdsdwwdsdsssssaassassdddwwdssdssssssssdwdwdddddwwawwddwwdsdsssssdwwwdsdwdsdwwdddssdddddddwwdsssdwwdsssssdssdwwwwwwwwdsdsdddssassssssdwdwwwdsdsssdwdwwwdsdwwwwwdsdsssssssdwdwwwwwwwddwwdddssassssssdwdsdssdwwdsdsddddddddssssdssssaaaaassssssssassdddssssdssssdssssddssssssdwdwwwdsdsssdwdwwdsdwwdsdsddddssssssaawwwaasaaaassaawaasssdddssssddddwwawwwwdssdddssssdssssdddwwdsdsdssassaawaasaaaaaaassddssdwwddddddssdwdwddwwdsdsdwdwdsdsdddddddddssssdwwwwwawaaaawwaaaaaaaaawwddddwwwwdsdsssdwdwwwwwdsdsdssssddssdwwwwddwwdsdwwawwwaawwaaaaawwddddddddddddwwwwdsdsssssaaassdssdwdwdsdsddddddddddssdwdwwwaaaaaaaawwdwwdssddddwwdsdsddssssssddssdwwdwwwwwwddwwdddddddssassssdsssdsssssassdssssssassdddssdddssaaaaaaawaasssssssddddddwwdsdsssddddddwwwdssdsdssssdddssssdwdwwwdddssssssdwwddddwwddwwwwddddssdwdwdsdsssssddddssddssdssassssdssaaasssssssdwdssdddwwawwddddssssssssaaaaaaaawwaawwwwwwwwaawwwwwaasssssaawaasaassssdddwwdssdwwdddssssssaaaassssdssassaassssaaaawwwwaaassdssssddddssssdwdwwwdsdsssddddddssdssaaawaasaawwaaaaaassaawwwaasssssssdsssdwdsdwdsdsdwdsssdssdddssssaassssaassaassaaawwaaassssdssddssaaaaaaaaawwdddwwaaaaassssssaawwaaaaaaaassssssdwdwwwdssdddssssssaassssssdddssssdwwdsdsddssssssssssssdddsssdsddddwwddwwwwdsdssdwdssdwdwdddssassaaaassssaawwaaaaaaaassssdwdwdddssassssdwdwdsdsddddssddddsdwdssssdwdwwwwwwaasaawwwwwwdsdsdwdwwwdwwdsdwdssssssssssassssdwwdssdddddddssssaawwaaaaassssaaawaassssdwdssdwdwdsdsssdwwwdwdsdsssssassdddssssssssssssaassssdwdwdsdsssssaassssdwdwddwwdssssassssddddsdwdssdwdwdsdsssssssaassaaaaaassaaaaaaaawwwwaawwaaassssdssssdssdwwdddssdwdsdwwdddssdwdsdwwwwdsdsssdwdwdsds"

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('202.120.7.132', 9999))
for x in s:
sock.send(x+'\n')
sock.send('flag\n')
print sock.recv(1024)
sock.close()

至此,此题得以解决。但是,该题服务端在比赛结束后已被关闭,且官方也没有放出服务端脚本。因此,我根据程序逻辑写了一个简单的服务端,可以基本实现原服务端的功能,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
__author__ = 'Bee'

from threading import Thread
from socket import *

LISTEN_ON = '0.0.0.0'
LISTEN_PORT = 8888

class R_Server(Thread):
def __init__(self, map, pSocket):
Thread.__init__(self)
self.map = map
self.pSocket = pSocket
self.x = 1
self.y = 1

def run(self):
try:
Flag = True
while Flag:
key = self.pSocket.recv(4096)
for i in xrange(0, len(key)):
if key[i] == 'w' and self.map[self.x - 1][self.y] != '1':
#print 'w'
self.x -= 1
elif key[i] == 'a' and self.map[self.x][self.y - 1] != '1':
#print 'a'
self.y -= 1
elif key[i] == 's' and self.map[self.x + 1][self.y] != '1':
#print 's'
self.x += 1
elif key[i] == 'd' and self.map[self.x][self.y + 1] != '1':
#print 'd'
self.y += 1
elif key[i] == 'f' and key[i:i+4] == 'flag':
if self.x == 497 and self.y == 497:
self.pSocket.send('Flag{This is the Flag.}')
self.pSocket.close()
Flag = False
print "Flag sent."
break
else:
self.pSocket.send("no cheat.")
self.pSocket.close()
Flag = False
print "Cheating."
break
except Exception,ex:
raise

if __name__ == '__main__':
MAP = []
with open('map.txt', 'r') as f:
MAP = f.readlines()

servSock = socket(AF_INET,SOCK_STREAM)
servSock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
servSock.bind((LISTEN_ON, LISTEN_PORT))
servSock.listen(1000)
print "Listened on: {}:{}".format(LISTEN_ON, LISTEN_PORT)
while True:
try:
sock,addr_info=servSock.accept()
print "incoming connection from {}.".format(addr_info)
R_Server(MAP, sock).start()
except KeyboardInterrupt, ex:
break
servSock.close()

相关文件:
原题附件 RPG.zip
服务端 RPG_Server.rar
二进制地图文件以及相关脚本 map_scripts.rar