题目给了一个流量包,用wireshark查看分析,主要的有smb、rdp、tls等

第一个点在smb:
有两次尝试登录的请求,第一次 SMB 登录失败在 10–13 号包,第二次成功登录在 120–123 号包。发现后门的smb是加密的

需要解密smb,而SMB 里这一步最重要的是 NTLMSSP(微软开发的一种安全协议,主要用于在 Windows 操作系统之间进行身份验证)。
Windows 在 SMB 认证时,经常会出现 NTLM 三步:

  • Type 1:协商
  • Type 2:服务器挑战
  • Type 3:客户端带响应认证

第一步NTLM 挑战 - 响应认证

参考:SMB流量分析-CSDN博客

NTLM 是一个基于哈希的挑战 - 响应(Challenge-Response)协议,核心目标是:
客户端不直接发送明文密码,仅用密码哈希 + 服务器随机挑战值,证明自己知道密码,完成身份认证

验证操作

  1. Windows 系统会将用户明文密码 → 经过 MD4 哈希 → 生成 NT Hash
  2. 在Type 2中服务器生成一个 8 字节的随机挑战值
  3. 在Type 3中客户端用密码哈希 + 挑战值计算响应
  4. 然后把这些内容发给服务器,通常包括:用户名、域名、工作站名、LM Response、NTLM Response,可能还有会话密钥相关数据,服务器收到后,会检查这个响应值是否正确。

分析smb包

直接分析第二次登入成功的包

包120:NTLM Type 1

进入 Windows 的 NTLM 认证流程了
NTLMSSP identifier: NTLMSSP

  • NTLM 协议的固定签名,用于协议识别。
1
NTLM Message Type: NTLMSSP_NEGOTIATE (0x00000001)
  • 这是 Type 1 协商报文(Type 1 = 客户端发起)

包 121:服务器发 Type 2 Challenge

1
NT Status: STATUS_MORE_PROCESSING_REQUIRED (0xc0000016)
  • SMB2 协议的标准状态码,表示认证流程尚未完成,继续认证
1
NTLM Message Type: NTLMSSP_CHALLENGE (0x00000002)
  • Type 2 挑战报文
1
NTLM Server Challenge: c1dec53240124487
  • 服务器生成的 8 字节随机挑战值

包 122:客户端发 Type 3,里面有用户名和响应

依次可以看到NTLMv2 Response响应字段、域、用户、主机名、以及加密会话密钥

NTLMv2 Response

客户端用密码哈希 + 挑战值 + 客户端数据,经过复杂 HMAC 算法计算出的一段摘要,是整个认证流程的 “证明”,分为两部分:
前 16 字节(固定):ca32f9b5b48c04ccfa96f35213d63d75

  • 是 NTLMv2 哈希。生成方式:HMAC-MD5(用户NT Hash, 用户名 + 域名)

Blob 数据

  • 包含服务器挑战值、客户端随机数、时间戳、域名 / 主机名等关键信息。

hashcat爆破

将我们的分析得到的信息整理成 hashcat 能吃的格式

1
username::domain:server_challenge:NTProofStr:blob

也就是

1
tom::.:c1dec53240124487:ca32f9b5b48c04ccfa96f35213d63d75:010100000000000040d0731fb92adb01221434d6e24970170000000002001e004400450053004b0054004f0050002d004a0030004500450039004d00520001001e004400450053004b0054004f0050002d004a0030004500450039004d00520004001e004400450053004b0054004f0050002d004a0030004500450039004d00520003001e004400450053004b0054004f0050002d004a0030004500450039004d0052000700080040d0731fb92adb0106000400020000000800300030000000000000000100000000200000bd69d88e01f6425e6c1d7f796d55f11bd4bdcb27c845c6ebfac35b8a3acc42c20a001000000000000000000000000000000000000900260063006900660073002f003100370032002e00310036002e003100300035002e003100320039000000000000000000

将其保存在hashes.txt文件中,使用命令进行爆破,hashcat使用方法参考:如何在Kali Linux中使用Hashcat:从入门到高级实践 — geek-blogs.com

1
hashcat -m 5600 -a 0 hashes.txt /usr/share/wordlists/rockyou.txt -o cracked.txt

得到密码 babygirl233,通过在wireshark协议NTLMSSP中加入密码来解SMB3的流量


解密SMB成功

第二步解密pfx以及tls

wireshark直接导出SMB对象可以得到一些文件

发现了证书和flag.7z,压缩包需要密码解锁,这里可以看到又der和pfx文件,而Mimikatz工具的指令crypto::certificates /export的导出行为会把公钥部分导成 DER,私钥部分导成 PFX,而且这些 PFX 的默认密码就是 mimikatz

derpfx转化为pem查看一下

1
openssl x509 -in 1.der -inform DER -out 1.pem -outform PEM

pfx尝试使用mimikatz这个密码进行解密

1
openssl pkcs12 -in 1.pfx -out rdp.pem -nodes

发现der是自签名证书,pfx是私钥

wireshark查看rdp包,发现rdp握手协议中使用了TLS加密

将pfx解出的pem文件导入wireshark

TLS解密成功

第三步分析rdp流量包

方法一:手动分析提取键盘数据

查看解密的rdp流量可以发现键盘的流量包有东西

rdp.fastpath.scancode.keycode 是对应的键盘码,rdp.fastpath.scancode.release表示的是这条按键事件是不是“释放键”事件 ,False表示“按下事件”

通过指令

1
tshark -r 11111.pcap -Y "rdp.fastpath.scancode.keycode && rdp.fastpath.scancode.release == 1" -T fields -e rdp.fastpath.scancode.keycode > keyup.txt

只提取用户【松开按键】那一刻的按键值,得到

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
0x0f,0x2a,0x36,0x1d,0x1d,0x0f,0x38,0x0f,0x38,0x0f
0x0f,0x2a,0x36,0x1d,0x1d,0x0f,0x38,0x0f,0x38,0x0f
0x0f,0x5b,0x5c,0x2a,0x36,0x1d,0x1d,0x0f,0x38,0x0f,0x38,0x0f
0x1c
0x14
0x23
0x12
0x2a
0x39
0x08
0x2c
0x39
0x19
0x1e
0x1f
0x1f
0x11
0x18
0x13
0x20
0x39
0x17
0x1f
0x39
0x21
0x28
0x1a
0x2a
0x11
0x17
0x31
0x20
0x18
0x11
0x1f
0x0c
0x2a
0x19
0x1e
0x1f
0x1f
0x11
0x18
0x13
0x20
0x1b
0x2a
0x0a
0x04
0x05
0x08
0x0b
0x02
0x04
0x02
0x09
0x03
0x28
0x1f
0x1d
0x0f,0x2a,0x36,0x1d,0x1d,0x0f,0x38,0x0f,0x38,0x0f

然后用gpt编写一个码表得到可读代码

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import re  
from pathlib import Path

# =========================
# US Keyboard - Scan Code Set 1
# =========================
NORMAL_MAP = {
0x02: "1", 0x03: "2", 0x04: "3", 0x05: "4", 0x06: "5",
0x07: "6", 0x08: "7", 0x09: "8", 0x0A: "9", 0x0B: "0",
0x0C: "-", 0x0D: "=",
0x0E: "[BACKSPACE]",
0x0F: "\t",
0x10: "q", 0x11: "w", 0x12: "e", 0x13: "r", 0x14: "t",
0x15: "y", 0x16: "u", 0x17: "i", 0x18: "o", 0x19: "p",
0x1A: "[", 0x1B: "]",
0x1C: "\n",
0x1D: "[CTRL_RELEASE]",
0x1E: "a", 0x1F: "s", 0x20: "d", 0x21: "f", 0x22: "g",
0x23: "h", 0x24: "j", 0x25: "k", 0x26: "l",
0x27: ";", 0x28: "'",
0x29: "`",
0x2A: "[SHIFT_RELEASE]",
0x2B: "\\",
0x2C: "z", 0x2D: "x", 0x2E: "c", 0x2F: "v", 0x30: "b",
0x31: "n", 0x32: "m",
0x33: ",", 0x34: ".", 0x35: "/",
0x36: "[SHIFT_RELEASE]",
0x38: "[ALT_RELEASE]",
0x39: " ",
0x5B: "[LWIN_RELEASE]",
0x5C: "[RWIN_RELEASE]",
}

SHIFT_MAP = {
0x02: "!", 0x03: "@", 0x04: "#", 0x05: "$", 0x06: "%",
0x07: "^", 0x08: "&", 0x09: "*", 0x0A: "(", 0x0B: ")",
0x0C: "_", 0x0D: "+",
0x10: "Q", 0x11: "W", 0x12: "E", 0x13: "R", 0x14: "T",
0x15: "Y", 0x16: "U", 0x17: "I", 0x18: "O", 0x19: "P",
0x1A: "{", 0x1B: "}",
0x1E: "A", 0x1F: "S", 0x20: "D", 0x21: "F", 0x22: "G",
0x23: "H", 0x24: "J", 0x25: "K", 0x26: "L",
0x27: ":", 0x28: "\"",
0x29: "~",
0x2B: "|",
0x2C: "Z", 0x2D: "X", 0x2E: "C", 0x2F: "V", 0x30: "B",
0x31: "N", 0x32: "M",
0x33: "<", 0x34: ">", 0x35: "?",
}

SHIFT_RELEASE_KEYS = {0x2A, 0x36}
CTRL_RELEASE_KEYS = {0x1D}
ALT_RELEASE_KEYS = {0x38}
WIN_RELEASE_KEYS = {0x5B, 0x5C}

# 这些通常是 RDP 会话开始/结束时的同步噪音
SYNC_LIKE_KEYS = {0x0F, 0x1D, 0x2A, 0x36, 0x38, 0x5B, 0x5C}

# 默认不把 Shift 释放作用到字母上,避免把普通小写误判成大写
# 例如你这份数据里 "the" 后面的 0x2a 很像同步残留,不是真正的大写 EAPPLY_SHIFT_TO_LETTERS = False


def parse_input(text: str):
lines = []
for line in text.splitlines():
codes = [int(x, 16) for x in re.findall(r"0x[0-9a-fA-F]+", line)]
if codes:
lines.append(codes)
return lines


def is_sync_line(codes):
return len(codes) >= 4 and all(c in SYNC_LIKE_KEYS for c in codes)


def code_to_text(code):
return NORMAL_MAP.get(code, f"<0x{code:02X}>")


def can_shift(code):
if code not in SHIFT_MAP:
return False
if APPLY_SHIFT_TO_LETTERS:
return True
# 默认仅对数字/符号做 shift 反推,避免误把普通文本变成大写
return code in {
0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B,
0x0C, 0x0D, 0x1A, 0x1B, 0x27, 0x28, 0x29, 0x2B, 0x33, 0x34, 0x35
}


def combo_name_from_code(code):
special = {
0x0E: "BACKSPACE",
0x0F: "TAB",
0x1C: "ENTER",
0x39: "SPACE",
}
if code in special:
return special[code]
if code in NORMAL_MAP and len(NORMAL_MAP[code]) == 1:
return NORMAL_MAP[code].upper()
return f"0x{code:02X}"


def find_last_real_token(tokens):
for i in range(len(tokens) - 1, -1, -1):
if tokens[i]["kind"] == "key":
return i
return None


def decode_release_only(lines):
tokens = []

for line_codes in lines:
if is_sync_line(line_codes):
continue

for code in line_codes:
if code in SHIFT_RELEASE_KEYS:
idx = find_last_real_token(tokens)
if idx is not None:
last = tokens[idx]
if can_shift(last["code"]):
last["text"] = SHIFT_MAP[last["code"]]
last["shifted"] = True
continue
if code in CTRL_RELEASE_KEYS:
idx = find_last_real_token(tokens)
if idx is not None:
last = tokens[idx]
base = combo_name_from_code(last["code"])
last["text"] = f"[CTRL+{base}]"
last["combo"] = "CTRL"
continue

if code in ALT_RELEASE_KEYS:
idx = find_last_real_token(tokens)
if idx is not None:
last = tokens[idx]
base = combo_name_from_code(last["code"])
last["text"] = f"[ALT+{base}]"
last["combo"] = "ALT"
continue

if code in WIN_RELEASE_KEYS:
idx = find_last_real_token(tokens)
if idx is not None:
last = tokens[idx]
base = combo_name_from_code(last["code"])
last["text"] = f"[WIN+{base}]"
last["combo"] = "WIN"
continue

if code == 0x0E:
# Backspace
idx = find_last_real_token(tokens)
if idx is not None:
tokens.pop(idx)
continue

tokens.append({
"kind": "key",
"code": code,
"text": code_to_text(code),
"shifted": False,
"combo": None,
})

result = "".join(t["text"] for t in tokens)

# 去掉首尾常见同步残留的 TAB result = result.strip("\t")
return result, tokens


if __name__ == "__main__":
input_file = Path("raw_scancodes.txt")
if not input_file.exists():
print("请先把数据保存为 raw_scancodes.txt")
raise SystemExit(1)

raw = input_file.read_text(encoding="utf-8", errors="ignore")
lines = parse_input(raw)
result, tokens = decode_release_only(lines)

print("===== 解码结果 =====")
print(result)

Path("decoded_output.txt").write_text(result, encoding="utf-8")
print("\n已写入 decoded_output.txt")

运行结果

1
2
3
===== 解码结果 =====
the 7z password is f'{windows_password}9347013182'[CTRL+S]
已写入 decoded_output.txt

得到flag.7z密码解压密码:babygirl2339347013182
最终flag:
flag{fa32a0b2-dc26-41f9-a5cc-1a48ca7b2ddd}

方法二:用工具pyrdp,将其转换为pyrdp可读的文件

进行PDU导出,要导出OSI Layer 7(PyRDP 提供 converter,可以把 PCAP已解密的 L7 PCAP 转成 replay / video / JSON events),得到一份新的pcap流量

然后通过

1
pyrdp-convert -o output qwe.pcap

转换为pyrdp可读的文件,然后加载pyrdp的GUI来看RDP的操作可以直接看到键盘的操作
[1]


  1. 参考文章:
    强网杯2024 Writeup - 星盟安全团队
    2024 强网杯 谍影重重5.0 超详解-先知社区
    2024强网杯-谍影重重5.0 - b0uu - 博客园↩︎