Forensics
Ocean Wildlife
Challenge
You have recieved a message in a bottle, saying something about the strange behavior of sea creatures. I wonder what that could be about?
Solution
观察 metadata.yaml 可以知道这是一个 ROS2 rosbag2 数据包,存储是 sqlite3,并且包含这些 topic:
/draw_commands/rosout/turtle1/pose/turtle1/color_sensor
其中 /draw_commands 和 /turtle1/* 这两个名字非常像 turtlesim, /rosout 也说明程序运行时打印过日志。整个包总共 1404 条消息,其中 /draw_commands 284 条,/turtle1/pose 552 条,/turtle1/color_sensor 552 条,/rosout 15 条。
基本可以判断出这是一个 ROS2 rosbag2 录制文件,题目和 turtlesim 高度相关,很可能有一只 turtle 在“画字”或者“写消息”。
.db3 本质上是 SQLite 数据库,rosbag2 常见的表有:
topics(topic 名称、消息类型)messages(实际消息内容)
先枚举 topic:
import sqlite3 conn = sqlite3.connect("mystery_message_0.db3")cur = conn.cursor() print(cur.execute("SELECT id, name, type FROM topics ORDER BY id").fetchall()) # [(1, '/turtle1/pose', 'turtlesim/msg/Pose'), (2, '/turtle1/color_sensor', 'turtlesim/msg/Color'), (3, '/rosout', 'rcl_interfaces/msg/Log'), (4, '/parameter_events', 'rcl_interfaces/msg/ParameterEvent'), (5, '/events/write_split', 'rosbag2_interfaces/msg/WriteSplitEvent'), (6, '/draw_commands', 'std_msgs/msg/String')]/rosout 是 ROS2 的日志输出 topic,程序在绘制结束后可能会直接打印出 flag。也可能要解析 /draw_commands 来重建 turtle 的书写轨迹。
这里先尝试 /rosout 的路线。
/rosout 的消息类型是 rcl_interfaces/msg/Log,而 rosbag2 数据库存的是 CDR 序列化后的二进制 BLOB,所以要做一点简单解析。
ROS2 CDR 数据前面一般有 4 字节封装头,rcl_interfaces/msg/Log 的字段主要包括:
- 时间戳
stamp - 日志级别
level - 节点名
name - 日志内容
msg - 源文件
file - 函数名
function - 行号
line
import sqlite3import struct def align(off, n): return (off + (n - 1)) & ~(n - 1) def parse_cdr_string(blob, off): off = align(off, 4) (length,) = struct.unpack_from("<I", blob, off) off += 4 s = blob[off:off + length - 1].decode(errors="replace") if length else "" off += length return s, off def parse_rosout_log(blob): off = 4 # 跳过 CDR encapsulation header off = align(off, 4) sec, nanosec = struct.unpack_from("<iI", blob, off) off += 8 level = blob[off] off += 1 name, off = parse_cdr_string(blob, off) msg, off = parse_cdr_string(blob, off) file_name, off = parse_cdr_string(blob, off) function, off = parse_cdr_string(blob, off) off = align(off, 4) (line,) = struct.unpack_from("<I", blob, off) return { "sec": sec, "nanosec": nanosec, "level": level, "name": name, "msg": msg, "file": file_name, "function": function, "line": line, } conn = sqlite3.connect("mystery_message_0.db3")cur = conn.cursor() rosout_topic_id = cur.execute( "SELECT id FROM topics WHERE name='/rosout'").fetchone()[0] rows = cur.execute( "SELECT timestamp, data FROM messages WHERE topic_id=? ORDER BY timestamp", (rosout_topic_id,)).fetchall() for _, blob in rows: log = parse_rosout_log(blob) print(f"[{log['name']}] {log['msg']}")输出:
[turtlesim] Starting turtlesim with node name /turtlesim[turtlesim] Spawning turtle [turtle1] at x=[5.544445], y=[5.544445], theta=[0.000000][rosbag2_recorder] Press SPACE for pausing/resuming[rosbag2_recorder] Listening for topics...[rosbag2_recorder] Event publisher thread: Starting[rosbag2_recorder] Subscribed to topic '/turtle1/pose'[rosbag2_recorder] Subscribed to topic '/turtle1/color_sensor'[rosbag2_recorder] Subscribed to topic '/rosout'[rosbag2_recorder] Subscribed to topic '/parameter_events'[rosbag2_recorder] Subscribed to topic '/events/write_split'[rosbag2_recorder] Recording...[draw_text_node] Waiting for turtlesim services...[draw_text_node] Services available.[rosbag2_recorder] Subscribed to topic '/draw_commands'[draw_text_node] Finished drawing: RS{f0ll0w_th3_5ea_Turtles}从最后一条日志拿到 flag:
RS{f0ll0w_th3_5ea_Turtles}再尝试 /draw_commands 的路线,重建 turtle 的书写轨迹。
/draw_commands 类型是 std_msgs/msg/String,也就是说消息本质上就是一个字符串
尝试解析 std_msgs/msg/String, CDR 格式是:
- 4 字节 encapsulation header
- 4 字节字符串长度
- 字符串内容(末尾带
\x00)
import sqlite3import structimport json def parse_std_string(blob): (length,) = struct.unpack_from("<I", blob, 4) return blob[8:8 + length - 1].decode(errors="replace") conn = sqlite3.connect("mystery_message_0.db3")cur = conn.cursor() draw_topic_id = cur.execute( "SELECT id FROM topics WHERE name='/draw_commands'").fetchone()[0] rows = cur.execute( "SELECT timestamp, data FROM messages WHERE topic_id=? ORDER BY timestamp", (draw_topic_id,)).fetchall() for _, blob in rows: print(json.loads(parse_std_string(blob)))解析后发现里面装的是 JSON:
{'cmd': 'teleport', 'x': 1.044999999999999, 'y': 5.845, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 1.044999999999999, 'y': 6.67, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 1.044999999999999, 'y': 6.67, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 1.429999999999999, 'y': 6.67, 'theta': 0.0}{'cmd': 'teleport', 'x': 1.594999999999999, 'y': 6.5325, 'theta': 0.0}{'cmd': 'teleport', 'x': 1.594999999999999, 'y': 6.395, 'theta': 0.0}{'cmd': 'teleport', 'x': 1.429999999999999, 'y': 6.257499999999999, 'theta': 0.0}{'cmd': 'teleport', 'x': 1.044999999999999, 'y': 6.257499999999999, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 1.319999999999999, 'y': 6.257499999999999, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 1.594999999999999, 'y': 5.845, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 1.694999999999999, 'y': 5.927499999999999, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 1.859999999999999, 'y': 5.845, 'theta': 0.0}{'cmd': 'teleport', 'x': 2.079999999999999, 'y': 5.845, 'theta': 0.0}{'cmd': 'teleport', 'x': 2.244999999999999, 'y': 5.927499999999999, 'theta': 0.0}{'cmd': 'teleport', 'x': 2.244999999999999, 'y': 6.0649999999999995, 'theta': 0.0}{'cmd': 'teleport', 'x': 2.189999999999999, 'y': 6.175, 'theta': 0.0}{'cmd': 'teleport', 'x': 2.079999999999999, 'y': 6.257499999999999, 'theta': 0.0}{'cmd': 'teleport', 'x': 1.859999999999999, 'y': 6.257499999999999, 'theta': 0.0}{'cmd': 'teleport', 'x': 1.749999999999999, 'y': 6.34, 'theta': 0.0}{'cmd': 'teleport', 'x': 1.694999999999999, 'y': 6.45, 'theta': 0.0}{'cmd': 'teleport', 'x': 1.694999999999999, 'y': 6.5875, 'theta': 0.0}{'cmd': 'teleport', 'x': 1.859999999999999, 'y': 6.67, 'theta': 0.0}{'cmd': 'teleport', 'x': 2.079999999999999, 'y': 6.67, 'theta': 0.0}{'cmd': 'teleport', 'x': 2.244999999999999, 'y': 6.5875, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 2.784999999999999, 'y': 6.67, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 2.6199999999999988, 'y': 6.615, 'theta': 0.0}{'cmd': 'teleport', 'x': 2.564999999999999, 'y': 6.505, 'theta': 0.0}{'cmd': 'teleport', 'x': 2.564999999999999, 'y': 6.34, 'theta': 0.0}{'cmd': 'teleport', 'x': 2.4549999999999987, 'y': 6.257499999999999, 'theta': 0.0}{'cmd': 'teleport', 'x': 2.564999999999999, 'y': 6.175, 'theta': 0.0}{'cmd': 'teleport', 'x': 2.564999999999999, 'y': 6.01, 'theta': 0.0}{'cmd': 'teleport', 'x': 2.6199999999999988, 'y': 5.8999999999999995, 'theta': 0.0}{'cmd': 'teleport', 'x': 2.784999999999999, 'y': 5.845, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 3.1599999999999993, 'y': 5.845, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 3.1599999999999993, 'y': 6.56, 'theta': 0.0}{'cmd': 'teleport', 'x': 3.2149999999999994, 'y': 6.6425, 'theta': 0.0}{'cmd': 'teleport', 'x': 3.3249999999999993, 'y': 6.67, 'theta': 0.0}{'cmd': 'teleport', 'x': 3.4899999999999993, 'y': 6.67, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 2.994999999999999, 'y': 6.34, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 3.379999999999999, 'y': 6.34, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 3.919999999999999, 'y': 5.845, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 3.754999999999999, 'y': 5.8999999999999995, 'theta': 0.0}{'cmd': 'teleport', 'x': 3.644999999999999, 'y': 6.0649999999999995, 'theta': 0.0}{'cmd': 'teleport', 'x': 3.644999999999999, 'y': 6.45, 'theta': 0.0}{'cmd': 'teleport', 'x': 3.754999999999999, 'y': 6.615, 'theta': 0.0}{'cmd': 'teleport', 'x': 3.919999999999999, 'y': 6.67, 'theta': 0.0}{'cmd': 'teleport', 'x': 4.084999999999999, 'y': 6.615, 'theta': 0.0}{'cmd': 'teleport', 'x': 4.194999999999999, 'y': 6.45, 'theta': 0.0}{'cmd': 'teleport', 'x': 4.194999999999999, 'y': 6.0649999999999995, 'theta': 0.0}{'cmd': 'teleport', 'x': 4.084999999999999, 'y': 5.8999999999999995, 'theta': 0.0}{'cmd': 'teleport', 'x': 3.919999999999999, 'y': 5.845, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 3.754999999999999, 'y': 5.927499999999999, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 4.084999999999999, 'y': 6.5875, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 4.569999999999999, 'y': 6.67, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 4.569999999999999, 'y': 5.8999999999999995, 'theta': 0.0}{'cmd': 'teleport', 'x': 4.569999999999999, 'y': 5.845, 'theta': 0.0}{'cmd': 'teleport', 'x': 4.734999999999999, 'y': 5.845, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 5.22, 'y': 6.67, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 5.22, 'y': 5.8999999999999995, 'theta': 0.0}{'cmd': 'teleport', 'x': 5.22, 'y': 5.845, 'theta': 0.0}{'cmd': 'teleport', 'x': 5.385, 'y': 5.845, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 5.869999999999999, 'y': 5.845, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 5.704999999999999, 'y': 5.8999999999999995, 'theta': 0.0}{'cmd': 'teleport', 'x': 5.594999999999999, 'y': 6.0649999999999995, 'theta': 0.0}{'cmd': 'teleport', 'x': 5.594999999999999, 'y': 6.45, 'theta': 0.0}{'cmd': 'teleport', 'x': 5.704999999999999, 'y': 6.615, 'theta': 0.0}{'cmd': 'teleport', 'x': 5.869999999999999, 'y': 6.67, 'theta': 0.0}{'cmd': 'teleport', 'x': 6.034999999999999, 'y': 6.615, 'theta': 0.0}{'cmd': 'teleport', 'x': 6.144999999999999, 'y': 6.45, 'theta': 0.0}{'cmd': 'teleport', 'x': 6.144999999999999, 'y': 6.0649999999999995, 'theta': 0.0}{'cmd': 'teleport', 'x': 6.034999999999999, 'y': 5.8999999999999995, 'theta': 0.0}{'cmd': 'teleport', 'x': 5.869999999999999, 'y': 5.845, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 5.704999999999999, 'y': 5.927499999999999, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 6.034999999999999, 'y': 6.5875, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 6.244999999999999, 'y': 6.45, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 6.382499999999999, 'y': 5.845, 'theta': 0.0}{'cmd': 'teleport', 'x': 6.52, 'y': 6.2299999999999995, 'theta': 0.0}{'cmd': 'teleport', 'x': 6.657499999999999, 'y': 5.845, 'theta': 0.0}{'cmd': 'teleport', 'x': 6.794999999999999, 'y': 6.45, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 6.895, 'y': 5.845, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 7.444999999999999, 'y': 5.845, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 7.764999999999999, 'y': 6.67, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 7.764999999999999, 'y': 5.8999999999999995, 'theta': 0.0}{'cmd': 'teleport', 'x': 7.819999999999999, 'y': 5.845, 'theta': 0.0}{'cmd': 'teleport', 'x': 7.984999999999999, 'y': 5.845, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 7.599999999999999, 'y': 6.34, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 7.929999999999999, 'y': 6.34, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 8.195, 'y': 5.845, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 8.195, 'y': 6.67, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 8.195, 'y': 6.285, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 8.36, 'y': 6.45, 'theta': 0.0}{'cmd': 'teleport', 'x': 8.58, 'y': 6.45, 'theta': 0.0}{'cmd': 'teleport', 'x': 8.745000000000001, 'y': 6.285, 'theta': 0.0}{'cmd': 'teleport', 'x': 8.745000000000001, 'y': 5.845, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 8.844999999999999, 'y': 6.5875, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 9.009999999999998, 'y': 6.67, 'theta': 0.0}{'cmd': 'teleport', 'x': 9.229999999999999, 'y': 6.67, 'theta': 0.0}{'cmd': 'teleport', 'x': 9.395, 'y': 6.56, 'theta': 0.0}{'cmd': 'teleport', 'x': 9.395, 'y': 6.395, 'theta': 0.0}{'cmd': 'teleport', 'x': 9.229999999999999, 'y': 6.285, 'theta': 0.0}{'cmd': 'teleport', 'x': 9.065, 'y': 6.257499999999999, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 9.065, 'y': 6.257499999999999, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 9.229999999999999, 'y': 6.2299999999999995, 'theta': 0.0}{'cmd': 'teleport', 'x': 9.395, 'y': 6.12, 'theta': 0.0}{'cmd': 'teleport', 'x': 9.395, 'y': 5.955, 'theta': 0.0}{'cmd': 'teleport', 'x': 9.229999999999999, 'y': 5.845, 'theta': 0.0}{'cmd': 'teleport', 'x': 9.009999999999998, 'y': 5.845, 'theta': 0.0}{'cmd': 'teleport', 'x': 8.844999999999999, 'y': 5.927499999999999, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 9.495000000000001, 'y': 5.845, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 10.045000000000002, 'y': 5.845, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 2.244999999999999, 'y': 5.245, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 1.6949999999999994, 'y': 5.245, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 1.6949999999999994, 'y': 5.245, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 1.6949999999999994, 'y': 4.8875, 'theta': 0.0}{'cmd': 'teleport', 'x': 1.8599999999999994, 'y': 4.915, 'theta': 0.0}{'cmd': 'teleport', 'x': 2.079999999999999, 'y': 4.915, 'theta': 0.0}{'cmd': 'teleport', 'x': 2.244999999999999, 'y': 4.805, 'theta': 0.0}{'cmd': 'teleport', 'x': 2.244999999999999, 'y': 4.585, 'theta': 0.0}{'cmd': 'teleport', 'x': 2.079999999999999, 'y': 4.42, 'theta': 0.0}{'cmd': 'teleport', 'x': 1.8599999999999994, 'y': 4.42, 'theta': 0.0}{'cmd': 'teleport', 'x': 1.6949999999999994, 'y': 4.5024999999999995, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 2.3449999999999993, 'y': 4.7225, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 2.8949999999999996, 'y': 4.7225, 'theta': 0.0}{'cmd': 'teleport', 'x': 2.8949999999999996, 'y': 4.8875, 'theta': 0.0}{'cmd': 'teleport', 'x': 2.7299999999999995, 'y': 5.025, 'theta': 0.0}{'cmd': 'teleport', 'x': 2.5099999999999993, 'y': 5.025, 'theta': 0.0}{'cmd': 'teleport', 'x': 2.3449999999999993, 'y': 4.8875, 'theta': 0.0}{'cmd': 'teleport', 'x': 2.3449999999999993, 'y': 4.5575, 'theta': 0.0}{'cmd': 'teleport', 'x': 2.5099999999999993, 'y': 4.42, 'theta': 0.0}{'cmd': 'teleport', 'x': 2.7299999999999995, 'y': 4.42, 'theta': 0.0}{'cmd': 'teleport', 'x': 2.8949999999999996, 'y': 4.5024999999999995, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 3.4899999999999993, 'y': 4.42, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 3.4899999999999993, 'y': 4.97, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 3.4899999999999993, 'y': 4.97, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 3.379999999999999, 'y': 5.025, 'theta': 0.0}{'cmd': 'teleport', 'x': 3.1599999999999993, 'y': 5.025, 'theta': 0.0}{'cmd': 'teleport', 'x': 3.0499999999999994, 'y': 4.915, 'theta': 0.0}{'cmd': 'teleport', 'x': 2.994999999999999, 'y': 4.7775, 'theta': 0.0}{'cmd': 'teleport', 'x': 2.994999999999999, 'y': 4.6674999999999995, 'theta': 0.0}{'cmd': 'teleport', 'x': 3.0499999999999994, 'y': 4.53, 'theta': 0.0}{'cmd': 'teleport', 'x': 3.1599999999999993, 'y': 4.4475, 'theta': 0.0}{'cmd': 'teleport', 'x': 3.3249999999999993, 'y': 4.42, 'theta': 0.0}{'cmd': 'teleport', 'x': 3.4899999999999993, 'y': 4.475, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 3.6449999999999996, 'y': 4.42, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 4.194999999999999, 'y': 4.42, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 4.295, 'y': 5.245, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 4.845, 'y': 5.245, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 4.57, 'y': 5.245, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 4.57, 'y': 4.42, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 4.944999999999999, 'y': 5.025, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 4.944999999999999, 'y': 4.585, 'theta': 0.0}{'cmd': 'teleport', 'x': 5.055, 'y': 4.4475, 'theta': 0.0}{'cmd': 'teleport', 'x': 5.22, 'y': 4.42, 'theta': 0.0}{'cmd': 'teleport', 'x': 5.385, 'y': 4.4475, 'theta': 0.0}{'cmd': 'teleport', 'x': 5.494999999999999, 'y': 4.585, 'theta': 0.0}{'cmd': 'teleport', 'x': 5.494999999999999, 'y': 5.025, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 5.595, 'y': 4.42, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 5.595, 'y': 5.025, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 5.595, 'y': 4.86, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 5.76, 'y': 4.9975000000000005, 'theta': 0.0}{'cmd': 'teleport', 'x': 5.925, 'y': 5.025, 'theta': 0.0}{'cmd': 'teleport', 'x': 6.09, 'y': 4.9975000000000005, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 6.464999999999999, 'y': 5.245, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 6.464999999999999, 'y': 4.475, 'theta': 0.0}{'cmd': 'teleport', 'x': 6.52, 'y': 4.42, 'theta': 0.0}{'cmd': 'teleport', 'x': 6.685, 'y': 4.42, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 6.299999999999999, 'y': 4.915, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 6.629999999999999, 'y': 4.915, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 7.17, 'y': 5.245, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 7.17, 'y': 4.475, 'theta': 0.0}{'cmd': 'teleport', 'x': 7.17, 'y': 4.42, 'theta': 0.0}{'cmd': 'teleport', 'x': 7.335, 'y': 4.42, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 7.545, 'y': 4.7225, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 8.095, 'y': 4.7225, 'theta': 0.0}{'cmd': 'teleport', 'x': 8.095, 'y': 4.8875, 'theta': 0.0}{'cmd': 'teleport', 'x': 7.93, 'y': 5.025, 'theta': 0.0}{'cmd': 'teleport', 'x': 7.71, 'y': 5.025, 'theta': 0.0}{'cmd': 'teleport', 'x': 7.545, 'y': 4.8875, 'theta': 0.0}{'cmd': 'teleport', 'x': 7.545, 'y': 4.5575, 'theta': 0.0}{'cmd': 'teleport', 'x': 7.71, 'y': 4.42, 'theta': 0.0}{'cmd': 'teleport', 'x': 7.93, 'y': 4.42, 'theta': 0.0}{'cmd': 'teleport', 'x': 8.095, 'y': 4.5024999999999995, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 8.195, 'y': 4.475, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 8.36, 'y': 4.42, 'theta': 0.0}{'cmd': 'teleport', 'x': 8.58, 'y': 4.42, 'theta': 0.0}{'cmd': 'teleport', 'x': 8.745000000000001, 'y': 4.475, 'theta': 0.0}{'cmd': 'teleport', 'x': 8.745000000000001, 'y': 4.585, 'theta': 0.0}{'cmd': 'teleport', 'x': 8.69, 'y': 4.6674999999999995, 'theta': 0.0}{'cmd': 'teleport', 'x': 8.58, 'y': 4.7225, 'theta': 0.0}{'cmd': 'teleport', 'x': 8.36, 'y': 4.7225, 'theta': 0.0}{'cmd': 'teleport', 'x': 8.25, 'y': 4.7775, 'theta': 0.0}{'cmd': 'teleport', 'x': 8.195, 'y': 4.86, 'theta': 0.0}{'cmd': 'teleport', 'x': 8.195, 'y': 4.97, 'theta': 0.0}{'cmd': 'teleport', 'x': 8.36, 'y': 5.025, 'theta': 0.0}{'cmd': 'teleport', 'x': 8.58, 'y': 5.025, 'theta': 0.0}{'cmd': 'teleport', 'x': 8.745000000000001, 'y': 4.97, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 8.954999999999998, 'y': 5.245, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}{'cmd': 'teleport', 'x': 9.12, 'y': 5.1899999999999995, 'theta': 0.0}{'cmd': 'teleport', 'x': 9.174999999999999, 'y': 5.08, 'theta': 0.0}{'cmd': 'teleport', 'x': 9.174999999999999, 'y': 4.915, 'theta': 0.0}{'cmd': 'teleport', 'x': 9.284999999999998, 'y': 4.8325, 'theta': 0.0}{'cmd': 'teleport', 'x': 9.174999999999999, 'y': 4.75, 'theta': 0.0}{'cmd': 'teleport', 'x': 9.174999999999999, 'y': 4.585, 'theta': 0.0}{'cmd': 'teleport', 'x': 9.12, 'y': 4.475, 'theta': 0.0}{'cmd': 'teleport', 'x': 8.954999999999998, 'y': 4.42, 'theta': 0.0}{'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 1}{'cmd': 'teleport', 'x': 10.0, 'y': 10.0, 'theta': 0.0}不难理解这套画图指令:
teleport:瞬移到某个坐标pen:设置画笔状态off = 0表示落笔off = 1表示抬笔
根据这些 JSON 命令重现轨迹即可,这里用 matplotlib 实现:
import matplotlib.pyplot as plt def plot_trajectory(): commands = [ {'cmd': 'teleport', 'x': 1.044999999999999, 'y': 5.845, 'theta': 0.0}, {'cmd': 'pen', 'r': 255, 'g': 255, 'b': 255, 'width': 3, 'off': 0}, ... ] _, ax = plt.subplots() ax.set_facecolor('black') # 当前线段的点 current_line_x = [] current_line_y = [] # 需要绘制的线段 segments = [] pen_down = False pen_color = (1.0, 1.0, 1.0) pen_width = 1 # 当前画笔位置 curr_x = 0 curr_y = 0 for cmd in commands: if cmd['cmd'] == 'teleport': new_x = cmd['x'] new_y = cmd['y'] if pen_down: # 如果笔是落下的,添加从当前位置到新位置的线段 current_line_x.append(curr_x) current_line_y.append(curr_y) current_line_x.append(new_x) current_line_y.append(new_y) else: # 如果笔是抬起的,且之前有累积的点,结算之前的线段 if current_line_x: segments.append((current_line_x[:], current_line_y[:], pen_color, pen_width)) current_line_x = [] current_line_y = [] curr_x = new_x curr_y = new_y elif cmd['cmd'] == 'pen': r = cmd['r'] / 255.0 g = cmd['g'] / 255.0 b = cmd['b'] / 255.0 w = cmd['width'] off = cmd['off'] # 更新画笔属性 pen_color = (r, g, b) pen_width = w if off == 1: # 抬笔 if pen_down: # 之前是落笔状态,现在要抬起,结算当前线段 if current_line_x: segments.append((current_line_x[:], current_line_y[:], pen_color, pen_width)) current_line_x = [] current_line_y = [] pen_down = False else: # 落笔 pen_down = True # 落笔时,起点是当前所在位置 current_line_x = [curr_x] current_line_y = [curr_y] # 处理最后可能残留的线段 if current_line_x: segments.append((current_line_x[:], current_line_y[:], pen_color, pen_width)) # 绘制所有线段 for x_vals, y_vals, color, width in segments: ax.plot(x_vals, y_vals, color=color, linewidth=width) # 设置坐标轴比例一致,防止变形 ax.set_aspect('equal', adjustable='box') # 调整视图范围 all_x = [p['x'] for p in commands if p['cmd'] == 'teleport'] all_y = [p['y'] for p in commands if p['cmd'] == 'teleport'] if all_x and all_y: margin = 0.5 ax.set_xlim(min(all_x) - margin, max(all_x) + margin) ax.set_ylim(min(all_y) - margin, max(all_y) + margin) plt.show() if __name__ == '__main__': plot_trajectory()
FLAG
RS{f0ll0w_th3_5ea_Turtles}Ocean Wildlife Revenge
Challenge
See the first challenge
Solution
解法同上一题

FLAG
RS{W4tch1ng_r0b0t_turtl3s}Davy Jones’ Message
Challenge
When sailors get lost, sometimes they will put a message in a bottle and set it off to sea in search of help. You sailor, have come across one of these bottles. Only thing is- it’s broken. Something might be hidden here, if you can figure it out…
Solution
先查看协议分级,这个 pcap 里几乎全是 RTPS 流量,而且两端通信很集中:
10.42.0.10 -> 10.42.0.1110.42.0.11 -> 10.42.0.10

先看 IP 分片,因为如果忽略 IP 分片,后面直接拿单个 UDP 去解析 RTPS 可能会发现很多地方数据看起来像坏的一样
ip.flags.mf == 1 || ip.frag_offset > 0发现分片很多,和题面里的 broken 对上了

这题大概率不止一层 broken,可能外面是 IP fragment,里面还会有 RTPS 自己的 fragment
接下来先把 IP 分片重组,按 (src, dst, protocol, ip.id) 聚合 IPv4 分片,然后按 offset 重组:
# reassemble_ip.pyimport structimport socketimport collections PCAP = "davy_jones_message.pcap" def parse_pcap_packets(path): with open(path, 'rb') as f: gh = f.read(24) if len(gh) != 24: return while True: ph = f.read(16) if not ph: break ts_sec, ts_usec, incl_len, orig_len = struct.unpack('<IIII', ph) data = f.read(incl_len) yield ts_sec, ts_usec, data def reassemble_ipv4_udp(path): frags = collections.defaultdict(list) complete_udp = [] total_frames = 0 frag_frames = 0 for ts_sec, ts_usec, data in parse_pcap_packets(path): total_frames += 1 if len(data) < 14 + 20: continue eth_type = struct.unpack('!H', data[12:14])[0] if eth_type != 0x0800: continue ip = data[14:] version_ihl = ip[0] version = version_ihl >> 4 ihl = (version_ihl & 0x0f) * 4 if version != 4 or len(ip) < ihl: continue total_len = struct.unpack('!H', ip[2:4])[0] ident = struct.unpack('!H', ip[4:6])[0] flags_frag = struct.unpack('!H', ip[6:8])[0] mf = bool(flags_frag & 0x2000) frag_offset = (flags_frag & 0x1fff) * 8 proto = ip[9] src = socket.inet_ntoa(ip[12:16]) dst = socket.inet_ntoa(ip[16:20]) if proto != 17: continue payload = ip[ihl:total_len] if mf or frag_offset != 0: frag_frames += 1 if not mf and frag_offset == 0: complete_udp.append((src, dst, payload)) else: key = (src, dst, proto, ident) frags[key].append((frag_offset, mf, payload)) reassembled = [] for key, parts in frags.items(): parts.sort(key=lambda x: x[0]) end = None blocks = {} for off, mf, payload in parts: blocks[off] = payload if not mf: end = off + len(payload) if end is None: continue cur = 0 out = bytearray() ok = True while cur < end: if cur not in blocks: ok = False break out += blocks[cur] cur += len(blocks[cur]) if ok: src, dst, proto, ident = key reassembled.append((src, dst, bytes(out))) # print(f"[+] total ethernet frames: {total_frames}") # print(f"[+] fragmented ipv4 frames: {frag_frames}") # print(f"[+] complete udp datagrams (already whole): {len(complete_udp)}") # print(f"[+] reassembled udp datagrams: {len(reassembled)}") return complete_udp + reassembled if __name__ == "__main__": all_udp = reassemble_ipv4_udp(PCAP) print(f"[+] total usable udp datagrams: {len(all_udp)}")输出:
[+] total ethernet frames: 13059[+] fragmented ipv4 frames: 8532[+] complete udp datagrams (already whole): 4403[+] reassembled udp datagrams: 948[+] total usable udp datagrams: 5351题目的 broken,第一层确实是在提示 IP fragmentation,但这还没完,因为 RTPS 里面还有一层
重组完 IP 以后,确认 RTPS 里还有没有继续分片
现在有完整 UDP datagram 了,接下来要识别哪些 UDP 载荷是 RTPS,解析 RTPS 子消息类型,看是不是有 DATA_FRAG
用脚本扫 RTPS:
# rtps_scan.pyimport structimport collectionsfrom reassemble_ip import reassemble_ipv4_udp PCAP = "davy_jones_message.pcap" SID_NAMES = { 0x06: "ACKNACK", 0x07: "HEARTBEAT", 0x09: "INFO_TS", 0x0e: "INFO_DST", 0x15: "DATA", 0x16: "DATA_FRAG", 0x13: "HEARTBEAT_FRAG",} def iter_rtps_submessages(udp_payload): if len(udp_payload) < 8: return sport, dport, ulen, cksum = struct.unpack('!HHHH', udp_payload[:8]) payload = udp_payload[8:] if not payload.startswith(b'RTPS'): return off = 20 # RTPS header while off + 4 <= len(payload): sid = payload[off] flags = payload[off + 1] little = bool(flags & 0x01) endian = '<' if little else '>' sublen = struct.unpack(endian + 'H', payload[off+2:off+4])[0] body = payload[off+4:off+4+sublen] yield sid, flags, body if sublen == 0: break off += 4 + sublen cnt = collections.Counter() for src, dst, udp_payload in reassemble_ipv4_udp(PCAP): if len(udp_payload) < 12: continue payload = udp_payload[8:] if not payload.startswith(b'RTPS'): continue for sid, flags, body in iter_rtps_submessages(udp_payload): cnt[SID_NAMES.get(sid, hex(sid))] += 1 # for k, v in cnt.most_common():# print(f"{k:15s} {v}")输出:
HEARTBEAT 2539INFO_TS 2529ACKNACK 2512DATA 2213INFO_DST 2189DATA_FRAG 948HEARTBEAT_FRAG 632DATA_FRAG 说明了 broken 不只是 IP 分片,RTPS 自己也在分片
也就是说这题是两层 broken:
- 外层:IP fragment
- 内层:RTPS DATA_FRAG
下一步要做的是找出哪一条 writer 最像真正藏东西的那条 RTPS 数据流
统计不同 writer 的 DATA_FRAG 数量:
# rtps_frag_stats.pyimport structimport sysimport collectionsfrom reassemble_ip import reassemble_ipv4_udpfrom rtps_scan import iter_rtps_submessages PCAP = "davy_jones_message.pcap" writer_counts = collections.Counter() for src, dst, udp_payload in reassemble_ipv4_udp(PCAP): if len(udp_payload) < 12: continue payload = udp_payload[8:] if not payload.startswith(b'RTPS'): continue for sid, flags, body in iter_rtps_submessages(udp_payload): if sid != 0x16: # DATA_FRAG continue # 这里只是按这个题的包结构取 writerId if len(body) < 12: continue writer_id = body[8:12].hex() writer_counts[writer_id] += 1 for wid, n in writer_counts.most_common(): print(wid, n)输出:
00001403 948这意味着 writerId = 00001403,对应 948 个 DATA_FRAG 子消息
这个数量非常大,明显不像普通状态消息,把目标锁定在这条 writer 上,重组 writerId = 00001403 的 DATA_FRAG
接下来按 writer sequence number 去拼应用层分片:
# reassemble_data_frag.pyimport structimport sysimport collectionsfrom reassemble_ip import reassemble_ipv4_udpfrom rtps_scan import iter_rtps_submessages PCAP = sys.argv[1]TARGET_WRITER = bytes.fromhex("00001403") samples = collections.defaultdict(list) for src, dst, udp_payload in reassemble_ipv4_udp(PCAP): if len(udp_payload) < 12: continue payload = udp_payload[8:] if not payload.startswith(b'RTPS'): continue for sid, flags, body in iter_rtps_submessages(udp_payload): if sid != 0x16: continue if len(body) < 32: continue writer_id = body[8:12] if writer_id != TARGET_WRITER: continue writer_sn_hi, writer_sn_lo = struct.unpack('<ii', body[12:20]) frag_start, frags_in_submsg, frag_size, sample_size = struct.unpack('<IHHI', body[20:32]) key = (writer_sn_hi, writer_sn_lo) samples[key].append({ "frag_start": frag_start, "frags_in_submsg": frags_in_submsg, "frag_size": frag_size, "sample_size": sample_size, "data": body[32:] }) print(f"[+] logical samples: {len(samples)}") ok = 0for sn, parts in sorted(samples.items()): parts.sort(key=lambda x: x["frag_start"]) blob = b"".join(p["data"] for p in parts) print(f"SN={sn} submsgs={len(parts)} sample_size={parts[0]['sample_size']} " f"frag_size={parts[0]['frag_size']} starts={[p['frag_start'] for p in parts]} " f"joined={len(blob)}") with open(f"sample_{sn[1]:04d}.bin", "wb") as f: f.write(blob[:parts[0]["sample_size"]]) ok += 1 print(f"[+] dumped {ok} samples")输出:
[+] logical samples: 316SN=(0, 1) submsgs=3 sample_size=36628 frag_size=1344 starts=[1, 11, 21] joined=36628SN=(0, 2) submsgs=3 sample_size=36628 frag_size=1344 starts=[1, 11, 21] joined=36628...SN=(0, 315) submsgs=3 sample_size=36628 frag_size=1344 starts=[1, 11, 21] joined=36628SN=(0, 316) submsgs=3 sample_size=36628 frag_size=1344 starts=[1, 11, 21] joined=36628[+] dumped 316 samples到这里可以得到几个非常关键的结论:
writerId = 00001403一共对应 316 份逻辑样本- 每份样本由 3 个
DATA_FRAG子消息组成 - 每份样本大小稳定为 36628 bytes
- 每个样本都能完整拼出来
先看看重组出来的样本长什么样
现在目录里已经有很多 sample_XXXX.bin 了,先看前几个样本的十六进制和字符串,判断是不是某种已知消息格式

光看十六进制不容易一下子看懂,但如果继续按常见 ROS2/DDS 序列化格式去猜,很快会发现它像某种 CDR 序列化消息体,于是下一步就尝试把它当成 PointCloud2 去解析(因为这类机器人/仿真题里,最容易藏视觉信息的载体就是图像、激光点云、PointCloud2,而且题面说 “Something might be hidden here”,很像把字藏在空间数据里)
写了一个解析脚本把 sample_0001.bin 当作 PointCloud2 解:
# parse_pointcloud2.pyimport struct buf = open("sample_0001.bin", "rb").read() off = 4 # CDR encapsulation sec, nsec = struct.unpack_from('<II', buf, off)off += 8 frame_id_len = struct.unpack_from('<I', buf, off)[0]off += 4frame_id = buf[off:off+frame_id_len].rstrip(b'\x00').decode(errors='ignore')off += frame_id_lenwhile off % 4: off += 1 height, width = struct.unpack_from('<II', buf, off)off += 8 fields_len = struct.unpack_from('<I', buf, off)[0]off += 4 fields = []for _ in range(fields_len): name_len = struct.unpack_from('<I', buf, off)[0] off += 4 name = buf[off:off+name_len].rstrip(b'\x00').decode(errors='ignore') off += name_len while off % 4: off += 1 field_offset = struct.unpack_from('<I', buf, off)[0] off += 4 datatype = buf[off] off += 1 while off % 4: off += 1 count = struct.unpack_from('<I', buf, off)[0] off += 4 fields.append((name, field_offset, datatype, count)) is_bigendian = buf[off]off += 1while off % 4: off += 1 point_step, row_step = struct.unpack_from('<II', buf, off)off += 8 data_len = struct.unpack_from('<I', buf, off)[0]off += 4 print("frame_id =", frame_id)print("height =", height)print("width =", width)print("fields =", fields)print("point_step =", point_step)print("row_step =", row_step)print("data_len =", data_len)输出:
frame_id = wfheight = 1width = 2281fields = [('x', 0, 7, 1), ('y', 4, 7, 1), ('z', 8, 7, 1), ('rgb', 12, 7, 1)]point_step = 16row_step = 36496data_len = 36496这里就完全坐实了这 316 份样本其实是 PointCloud2 点云帧,所以接下来很自然猜测题目要藏的信息应该就在这些点云里
先把点云画出来看看,把 PointCloud2 中的点提出来并且先尝试不同投影面:
# dump_all_points.pyimport structimport globimport math def load_pc2(path): buf = open(path, "rb").read() off = 4 sec, nsec = struct.unpack_from('<II', buf, off) off += 8 frame_id_len = struct.unpack_from('<I', buf, off)[0] off += 4 off += frame_id_len while off % 4: off += 1 height, width = struct.unpack_from('<II', buf, off) off += 8 fields_len = struct.unpack_from('<I', buf, off)[0] off += 4 for _ in range(fields_len): name_len = struct.unpack_from('<I', buf, off)[0] off += 4 + name_len while off % 4: off += 1 off += 4 # offset off += 1 # datatype while off % 4: off += 1 off += 4 # count off += 1 while off % 4: off += 1 point_step, row_step = struct.unpack_from('<II', buf, off) off += 8 data_len = struct.unpack_from('<I', buf, off)[0] off += 4 data = buf[off:off+data_len] pts = [] for i in range(0, len(data), point_step): x, y, z, rgbf = struct.unpack_from('<ffff', data, i) rgb_u = struct.unpack('<I', struct.pack('<f', rgbf))[0] r = (rgb_u >> 16) & 0xff g = (rgb_u >> 8) & 0xff b = rgb_u & 0xff if math.isfinite(x) and math.isfinite(y) and math.isfinite(z): pts.append((x, y, z, r, g, b)) return pts all_pts = []for fn in sorted(glob.glob("sample_*.bin")): all_pts.extend(load_pc2(fn)) print("[+] total points:", len(all_pts)) with open("all_points.xyzrgb", "w") as f: for x, y, z, r, g, b in all_pts: f.write(f"{x} {y} {z} {r} {g} {b}\n")输出:
[+] total points: 720796画几个最直观的投影:
# plot_views.pyimport matplotlib.pyplot as plt pts = []with open("all_points.xyzrgb") as f: for line in f: x, y, z, r, g, b = map(float, line.split()) pts.append((x, y, z, int(r), int(g), int(b))) xs = [p[0] for p in pts]ys = [p[1] for p in pts]zs = [p[2] for p in pts] plt.figure(figsize=(12, 8))plt.scatter(xs, ys, s=0.1)plt.savefig("all_pts_xy.png", dpi=300) plt.figure(figsize=(12, 8))plt.scatter(xs, zs, s=0.1)plt.savefig("all_pts_xz.png", dpi=300)最后发现 flag 就在 all_pts_xz.png 里

FLAG
RS{D4vy_J0nes_Sp3aks_1n_5il3nce}Song Inside a Shell
Challenge
You have been marooned on a beach, only the waves and the sea around you. Sitting down in the shade of the few trees on the island, you pick up a seashell and put it to your ear. Download the file and figure out what the sea has to tell you.
Solution
先从音频的双声道中提取差分声道(L-R)
sox song-inside-a-shell.wav side_audio.wav remix 1v1,2v-1 gain -nremix 1v1,2v-1:1v1:取左声道,权重+1;2v-1:取右声道,权重-1;合起来就是L - Rgain -n:自动归一化,避免差分信号太小听不清
观察频谱图发现约 82 - 102 秒可疑:

截出可疑段
sox side_audio.wav suspicious.wav trim 80 25听了几遍发现像是倒放的,把它倒放再听听看
sox suspicious.wav suspicious_rev.wav reverse这回能很清晰得听出里面的内容是有意义的,听到 underscore 基本就能确定这是 flag 了,但还是听不太清,用 Whisper Large V3 - a Hugging Face Space by hf-audio 识别

V-O-I-C-E underscore O-F underscore T-H-E underscore S-E-A right curly bracket R-S left curly bracket L-I-S-T-E-N underscore T-O underscore T-H-E underscore组合起来就是 flag 了
FLAG
RS{LISTEN_TO_THE_VOICE_OF_THE_SEA}