0.서론
Meta Quest 3S는 기기 단독으로 사용할 수도 있고, PC와 연결해서 사용할 수도 있다.
이때 PC와의 연결은 무선 연결과 유선 연결을 모두 지원하며, PC와 연결해서 사용하는 경우 PC의 응용프로그램을 VR에 스트리밍 하는 식으로 동작한다.
유선 연결은 PC와 USB-C Type으로 연결하는 방식이다.
이 글에서는 PC와 Meta Quest 3S를 유선연결 한 뒤에 캡처한 패킷으로 플레이 영상을 복구하는 방법에 대해 서술한다.
1.Meta Quest 3S의 USB 패킷 캡처 후 분석

USB 패킷 캡처를 위해서 Wireshark와 USBPcap 플러그인을 사용하였다. 해당 플러그인을 설치하면 아래와 같이 USBPcpa1이라는 영역이 생기는데, Meta Quest 3S와 유선 연결을 하고나서 해당 영역으로 들어가면 패킷을 캡처할 수 있다.

시간 단위를 보면 알 수 있겠지만, 거의 0.001초 단위보다 더 빠르게 생성된다.
1.3.1이 Meta Quest이고, host가 PC이다.
27바이트 패킷은 헤더만 들어있는 패킷으로, 아마 서로의 통신상태를 확인하는 것으로 추정된다.
1051바이트 패킷도 일부는 헤더를 빼면 1024바이트 패킷으로, 가장 최소 단위의 2^10바이트 패킷으로, 분석했을 때 영상과 관련한 패킷도 있고, 그 외의 패킷들도 있었다. 아마 1.3.1 => PC로 보내는 패킷들 중 일부는 어떤 동작을 했는지 보내는 패킷일 것이다.
내가 주목한 패킷은 23579바이트와 같이 크기가 거대한 패킷으로, 영상과 관련한 패킷일 것으로 추정했다.

위 패킷의 데이터를 보니, h.264 비디오 코덱 데이터로 추정되는 데이터를 발견했다.
h.264는 동영상 데이터의 표준 규격으로 0x000001 또는 0x00000001로 시작하는 여러 NAL Unit(Network Abstraction Layer Unit) 데이터로 구성된다. 각 유닛의 역할에 따라 0x67, 0x68, 0x65, 0x61 중 하나의 바이트가 붙는다.
아래 표는 각 유닛에 대한 설명이다.
| NAL 헤더 | NAL Unit 타입 | 역할 |
| 0x67 | SPS | 영상의 규격(해상도 등) |
| 0x68 | PPS | 영상 세부 설정(인코딩 방식 등) |
| 0x65 | IDR | 완전한 1프레임 사진 |
| 0x61 | Slice | 이전 화면의 변화량 |
즉 h.264 영상은 0x67 -> 0x68 -> 0x65 -> 연속된 0x61 의 패턴으로 구성된다.
이후 패킷들도 분석해보니 거대한 용량의 패킷의 경우 거의다 0x61이 나왔고, h264 코덱이라는 것을 확신할 수 있었다.
패킷 분석을 통해 다음과 같은 특징들을 발견할 수 있었다.
1.유닛의 시작 부분인 0x01은 패킷의 0x25 혹은 0x26번째, 헤더를 제외한다면 데이터 부분의 11번째 혹은 12번째에 위치한다
2.데이터가 블록 단위로 전송되어서 남은 뒷부분은 0-패딩이 된다.
2.패킷 필터링 이후 추출
h.264와 관련된 패킷들을 추출하기 위해 아래와 같은 필터값을 적용시켰다.
frame contains 00:00:00:01:67 or frame contains 00:00:00:01:68 or frame contains 00:00:00:01:65 or frame contains 00:00:00:01:61

필터링 이후 패킷을 보면, 1.3.1 => host 패킷도 있는 것을 볼 수 있었다.
이건 h.264와 관련된 패킷이 아님에도 우연히 위의 필터링 값에 걸린 패킷이었다. 즉 해당 필터링 이후에도 h.264패킷이 아닌 것을 걸러내야 한다는 뜻이었다.
일단 초기 패킷을 찾기 위해 위에서부터 차례대로 패킷을 분석해보았다.


host -> 1.3.1로 가는 패킷을 위에서부터 차례대로 분석하니 예상했던 대로 SPS -> PPS -> IDR의 데이터가 차례대로 나왔다.
그러면 위 패킷들을 모두 txt로 추출해주도록 하겠다.
@echo off
setlocal enabledelayedexpansion
REM --- [ 1. CONFIGURATION ] ---
set TSHARK_PATH=C:\Program Files\Wireshark
set CAPTURE_FILE=D:\capture\metaCapture(USB).pcapng
set FILTER=frame contains 00:00:00:01:67 or frame contains 00:00:00:01:68 or frame contains 00:00:00:01:65 or frame contains 00:00:00:01:61
set DATA_FIELD=usb.capdata
set TXT_FILE=__tshark_output.txt
REM --- [ 1. END ] ---
REM --- [ 2. EXECUTION ] ---
cd /d %~dp0
echo [!] Working directory set to: %~dp0
echo.
echo [!] Step 1: Executing tshark and saving output to %TEMP_FILE%...
"%TSHARK_PATH%\tshark.exe" -r "%CAPTURE_FILE%" -Y "%FILTER%" -T fields -e %DATA_FIELD% > "%TXT_FILE%"위 명령어는 tshark를 통해 위에 필터링한 패킷들의 헤더를 제외한 데이터 부분을 하나의 tshark_output.txt 라는 파일로 추출하는 명령어이다.
해당 명령어를 .bat파일로 실행하면 아래와 같이 패킷들을 모두 담은 txt파일을 얻을 수 있다.

3.패킷 재구성을 통한 플레이 영상 복구
패킷들을 얻었으니 해당 패킷들을 재구성하면 된다.
재구성을 위해 위에서 얻은 패킷들의 특성을 활용하였다.
1.유닛의 시작 부분인 0x01은 패킷의 0x25 혹은 0x26번째, 헤더를 제외한다면 데이터 부분의 11번째 혹은 12번째에 위치한다
2.데이터가 블록 단위로 전송되어서 남은 뒷부분은 0-패딩이 된다.
import sys
import os
# --- 설정 ---
INPUT_FILE = "tshark_output.txt" # 입력받을 단일 텍스트 파일
OUTPUT_FILE = "output.h264" # 최종 H.264 파일 이름
PADDING_BYTES = b'\x00' # 제거할 패딩 바이트 (0x00)
# NAL 유닛 시작 코드 목록 (SPS, PPS, I-Frame, P-Frame 등)
VALID_START_MARKERS = [
"0000000167", # SPS
"0000000168", # PPS
"0000000165", # I-Frame
"0000000161", # P-Frame
]
def process_tshark_output():
# 1. 경로 설정 (스크립트 위치 기준)
try:
script_path = os.path.abspath(sys.executable if getattr(sys, 'frozen', False) else __file__)
except NameError:
script_path = os.path.abspath(os.getcwd())
script_dir = os.path.dirname(script_path)
input_path = os.path.join(script_dir, INPUT_FILE)
output_path = os.path.join(script_dir, OUTPUT_FILE)
print(f"스크립트 위치: {script_dir}")
print(f"입력 파일: {input_path}")
# 입력 파일 존재 확인
if not os.path.exists(input_path):
print(f"오류: '{INPUT_FILE}' 파일을 찾을 수 없습니다.")
return
total_bytes_written = 0
total_packets_skipped = 0
processed_lines = 0
# 2. 파일 입출력 열기
# output.h264는 바이너리 쓰기('wb'), tshark_output.txt는 텍스트 읽기('r')
with open(output_path, 'wb') as outfile, open(input_path, 'r', encoding='utf-8') as infile:
# 한 줄씩 읽어서 처리 (enumerate로 줄 번호 확인)
for line_num, line in enumerate(infile, 1):
processed_lines += 1
# 진행 상황 표시 (10000줄 마다)
if line_num % 10000 == 0:
print(f"--- 처리 중: {line_num}번째 줄 ---")
# 공백 및 줄바꿈 제거하여 순수 16진수 문자열로 변환
hex_string = "".join(line.split())
if not hex_string:
# 빈 줄 건너뛰기
continue
if len(hex_string) >= 24:
hex_byte_11_str = hex_string[20:22]
hex_byte_12_str = hex_string[22:24]
# 11번째 바이트가 "01"이 아니고, 12번째도 "01"이 아니면 건너뜀
if hex_byte_11_str != "01" and hex_byte_12_str != "01":
total_packets_skipped += 1
continue
start_indices_found = []
for marker in VALID_START_MARKERS:
idx = hex_string.find(marker)
if idx != -1:
start_indices_found.append(idx)
if not start_indices_found:
total_packets_skipped += 1
continue
start_index = min(start_indices_found)
hex_data = hex_string[start_index:]
if len(hex_data) % 2 != 0:
hex_data = hex_data[:-1]
try:
binary_data = bytes.fromhex(hex_data)
except ValueError:
print(f"오류: Line {line_num}에 잘못된 16진수 문자가 포함됨.")
total_packets_skipped += 1
continue
#0x00 패딩 제거
processed_data = binary_data.rstrip(PADDING_BYTES)
outfile.write(processed_data)
total_bytes_written += len(processed_data)
print("\n--- 작업 완료 ---")
print(f"'{output_path}' 파일 생성")
print(f"총 처리한 라인 수: {processed_lines}")
print(f"생성된 파일 크기: {total_bytes_written} 바이트")
print(f"건너뛴 패킷(필터/오류): {total_packets_skipped} 개")
if __name__ == "__main__":
process_tshark_output()파이썬으로 아래의 3가지 과정을 거친 뒤 패킷들을 합치는 코드이다.
- 00000001+67,68,65,61앞에 있는 데이터 삭제
- 0패딩 제거 (패딩 제거 전, 후 영상이 같음을 확인했으나 용량 압축을 위해 패딩 제거)
- 11/12바이트에 01이 없다면 해당 패킷은 제외

h.264 코덱을 바로 재생할 수 있는 VLC media player로 재생해 봤더니 잘 재생되었다.
내가 복구를 잘못한건지 아니면 최초 통신할 때 USB3.0을 사용 안해서인지는 몰라도 화면이 조금 깨져보이긴 했지만, 사용자가 어떤 행위를 했는지는 확실히 식별가능했다.
4.결론
사실 네트워크 패킷도 아니라 USB 패킷은 실시간으로 사용할 때가 아니면 흔적이 남기 쉽지 않다.
따라서 이게 포렌식적으로 유의미하기는 어려울 것이다.