Ghost Vault

解压 ghost_vault.tar.gz 之后,可以发现仓库中的有效信息分散在三个位置:

1.某个旧提交上的 Git note:third shard: horizon

2.stash@{0}^3 中保存的未跟踪文件:second shard: paper

3.通过 git fsck 找到的悬空 blob:first shard: midnight

此外,还能从一个不可达提交 stage archive vault 中取回 vault.enc。而仓库内的 tools/packer.py 已经给出了完整解密逻辑:使用 shard1-shard2-shard3 这样的口令格式,生成基于 SHA-256 的密钥流,再与密文按字节异或即可得到明文。

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
69
70
71
72
73
74
75
76
77
78
79
80
import base64
import hashlib
import os
import re
import subprocess
import tarfile
import tempfile

archive = "ghost_vault.tar.gz"


def run_git(repo, *args):
return subprocess.check_output(
["git", "-C", repo, *args],
text=True,
stderr=subprocess.STDOUT,
).strip()


with tempfile.TemporaryDirectory() as tmp:
with tarfile.open(archive, "r:gz") as tf:
tf.extractall(tmp)

repo = os.path.join(tmp, "ghost-vault")

shard3 = None
for commit in run_git(repo, "log", "--all", "--format=%H").splitlines():
try:
note = run_git(repo, "notes", "--ref=commits", "show", commit)
except subprocess.CalledProcessError:
continue
m = re.search(r"third shard:\s*([a-z]+)", note)
if m:
shard3 = m.group(1)
break

stash_note = run_git(repo, "show", "stash@{0}^3:drafts/reminder.txt")
shard2 = re.search(r"second shard:\s*([a-z]+)", stash_note).group(1)

shard1 = None
vault_enc = None
fsck = run_git(repo, "fsck", "--full", "--no-reflogs", "--unreachable")
for line in fsck.splitlines():
parts = line.split()
if len(parts) != 3:
continue
_, kind, obj = parts
if kind == "blob":
try:
data = run_git(repo, "cat-file", "-p", obj)
except subprocess.CalledProcessError:
continue
m = re.search(r"first shard:\s*([a-z]+)", data)
if m:
shard1 = m.group(1)
elif kind == "commit":
meta = run_git(repo, "cat-file", "-p", obj)
if "stage archive vault" in meta:
vault_enc = run_git(repo, "show", f"{obj}:vault.enc")

passphrase = f"{shard1}-{shard2}-{shard3}".encode()

raw = base64.b64decode(vault_enc)
salt_len = raw[3]
salt = raw[4 : 4 + salt_len]
body = raw[4 + salt_len :]

stream = bytearray()
counter = 0
while len(stream) < len(body):
block = hashlib.sha256(
passphrase + b"|" + salt + b"|" + counter.to_bytes(4, "big")
).digest()
stream.extend(block)
counter += 1

flag = bytes(a ^ b for a, b in zip(body, stream)).decode()

print(passphrase.decode())
print(flag)

运行输出:

1
2
midnight-paper-horizon
flag{git_objects_notes_stash_and_dangling_commits}