米米的博客

做了一点微小的工作

本文记录了我在忘记 VMware Workstation 虚拟机加密密码后,通过 PowerShell 调用 Windows 凭据管理器 API 成功恢复密码的完整过程。如果你也遇到了同样的问题,希望这篇文章能帮到你。

起因

前几天我需要迁移一台之前创建的 VMware 加密虚拟机,选择导出 OVF,结果弹出了密码输入框 —— 我完全不记得当时设置了什么密码。

说实话,这个密码根本不是我主动设置的。VMware Workstation 在某些操作(比如启用加密、克隆加密虚拟机)时会引导你设置加密密码,同时提供一个「Remember the password」(记住密码)选项。当时我随手勾选了这个选项,之后每次打开虚拟机都是自动解锁的。

直到需要迁移虚拟机时,它需要用户主动输入密码了。于是我开始研究恢复方案。

原理

经过一番搜索,我了解到以下关键信息:

  1. VMware Workstation 在你勾选「Remember the password」时,会将加密密码存储在 Windows 凭据管理器(Credential Manager) 中。
  2. 每台加密虚拟机都有一个唯一的 GUID,VMware 用这个 GUID 作为凭据的名称(Target Name)来存储密码。
  3. 虽然你可以在 Windows 的「凭据管理器」界面中看到这些条目,但界面上不会显示密码明文
  4. 好消息是,我们可以通过 Windows 的 Win32 CredReadW API 直接读取凭据中存储的密码。

换句话说,密码一直都在你的电脑里,只是 VMware 没有提供一个「显示密码」的按钮而已。

恢复步骤

找到虚拟机的加密 GUID

用文本编辑器打开虚拟机的 .vmx 配置文件。这个文件通常位于虚拟机的存储目录下,例如:

1
C:\Users\<用户名>\Documents\Virtual Machines\<虚拟机名>\<虚拟机名>.vmx

在文件中搜索 encryptedVM.guid,你会找到类似这样的一行:

1
encryptedVM.guid = "{833AB4F5-587E-4B11-9260-4DB13742FA7F}"

记下这个 GUID(包括大括号),后面会用到。


在 PowerShell 中加载 Win32 API

打开 PowerShell,将以下代码完整复制粘贴进去并回车执行:

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
Add-Type @"
using System;
using System.Runtime.InteropServices;

public class Win32Cred
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct CREDENTIAL
{
public int Flags;
public int Type;
public IntPtr TargetName;
public IntPtr Comment;
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
public int CredentialBlobSize;
public IntPtr CredentialBlob;
public int Persist;
public int AttributeCount;
public IntPtr Attributes;
public IntPtr TargetAlias;
public IntPtr UserName;
}

[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool CredReadW(
string target, int type, int reservedFlag, out IntPtr credentialPtr);

[DllImport("advapi32.dll", SetLastError = true)]
public static extern void CredFree(IntPtr buffer);

public static string CredRead(string targetName, int type = 1)
{
IntPtr credPtr;
if (CredReadW(targetName, type, 0, out credPtr))
{
CREDENTIAL cred = (CREDENTIAL)Marshal.PtrToStructure(
credPtr, typeof(CREDENTIAL));
string pass = Marshal.PtrToStringAnsi(
cred.CredentialBlob, cred.CredentialBlobSize);
CredFree(credPtr);
return pass;
}
throw new System.ComponentModel.Win32Exception(
Marshal.GetLastWin32Error());
}
}
"@

这段代码通过 C# 的 P / Invoke 机制定义了对 advapi32.dllCredReadWCredFree 函数的调用接口,并封装了一个简洁的 CredRead 方法。

读取密码

继续在 PowerShell 中执行以下命令,将 GUID 替换为你在第一步中找到的值:

1
[Win32Cred]::CredRead("{833AB4F5-587E-4B11-9260-4DB13742FA7F}")

如果一切顺利,密码会直接输出到屏幕上:

1
2
PS C:\Users\Andrew> [Win32Cred]::CredRead("{833AB4F5-587E-4B11-9260-4DB13742FA7F}")
MySecretPassword

就是这么简单。 拿到密码后,回到 VMware 输入即可解锁虚拟机。

总结

整个恢复过程可以概括为三步:

步骤操作关键点
1打开 .vmx 文件找到 encryptedVM.guid
2在 PowerShell 中加载 Win32 API复制粘贴 Add-Type 代码块
3调用 CredRead 方法传入 GUID,密码直接输出

VMware 的加密密码其实一直安静地躺在 Windows 凭据管理器里,只是没有提供直观的查看方式。通过一小段 PowerShell + C# 互操作代码,就能把它读出来。

希望这篇文章能帮助到同样被 VMware 加密密码困扰的你。


参考文章:andshrew 的 Gist

在五年前的文章将 Yeelight 烛光氛围灯接入 Apple HomeKit 中,笔者介绍了通过 Homebridge 控制 Yeelight 烛光氛围灯的方法。然而,笔者在将 Homebridge 相关代码迁移(使用 AI 转写)到 ESPHome 平台,并尝试在 Home Assistant 中使用时,发现原版代码的蓝牙交互实际上存在问题。yee.js 使用了0x43,0x67,0xde,0xad,0xbe,0xbf作为蓝牙连接后认证的 payload,但这实际上无效;即使 ESP32 和灯连接上了蓝牙,过一段时间后就会断开,下次控制时又需要重新连接。

经过搜索,笔者找到了两个重要的参考:I need Help to use esp32 to Control Yeelight Candla BLE Lamp with MQTT / HomeAssisant 以及“失联” 7 年的 Yeelight 烛光氛围灯,上线 HomeKit 平台 | 新人报到 - 少数派。根据这些资料,如果不完成配对,蓝牙每过一段时间就会被强制断开,和笔者遇到的情况一致。而测试成功的交互流程是,连接蓝牙后发送0x43,0x40,0x02进行认证,此时灯会进入配对模式,亮度会闪烁,之后旋转氛围灯,就可以配对成功。

基于此,笔者重新编写了 ESPHome 配置文件,供参考。

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
substitutions:
YEELIGHT_PAIR_BUTTON_PIN: GPIO0
YEELIGHT_STATUS_LED_PIN: GPIO2

YEELIGHT_SERVICE_UUID: "0000FE87-0000-1000-8000-00805f9b34fb"
YEELIGHT_NOTIFY_UUID: "8F65073D-9F57-4AAA-AFEA-397D19D5BBEB"
YEELIGHT_CONTROL_UUID: "AA7D3F34-2D4F-41E0-807F-52FBF8CF7443"
YEELIGHT_MAC: "F8:24:41:XX:XX:XX"

esphome:
name: yeelight
platformio_options:
board_build.flash_mode: dio

esp32:
board: esp32dev
framework:
type: esp-idf

logger:

api:

ota:
platform: esphome
password: ""

wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password

binary_sensor:
- platform: gpio
name: yeelight_pair_button
pin:
number: $YEELIGHT_PAIR_BUTTON_PIN
inverted: true
on_click:
- script.execute: script_yeelight_pair
internal: true

ble_client:
- mac_address: $YEELIGHT_MAC
id: ble_yeelight

sensor:
- platform: ble_client
id: sensor_yeelight_notify
type: characteristic
ble_client_id: ble_yeelight
internal: true
service_uuid: $YEELIGHT_SERVICE_UUID
characteristic_uuid: $YEELIGHT_NOTIFY_UUID
notify: true
lambda: |-
if (x.size() > 4 && x[0] == 0x43 && x[1] == 0x45) {
bool power = (x[2] == 0x01);
float brightness = (float)x[3] / 100.0;
auto target = id(yeelight_candle);
target->remote_values.set_state(power);
target->remote_values.set_brightness(brightness);
target->publish_state();
ESP_LOGD("yeelight", "Sync - Power: %s, Brightness: %.0f%%", power ? "ON" : "OFF", brightness * 100);
}
return NAN;

light:
- platform: monochromatic
name: "Yeelight Candela"
id: yeelight_candle
output: output_yeelight_brightness
on_turn_on:
- script.execute: script_yeelight_on
on_turn_off:
- script.execute: script_yeelight_off
- platform: binary
id: yeelight_status_led
output: output_yeelight_status
internal: true

output:
- platform: template
id: output_yeelight_brightness
type: float
write_action:
- script.execute:
id: script_yeelight_brightness
value: !lambda return state;
- platform: gpio
id: output_yeelight_status
pin: $YEELIGHT_STATUS_LED_PIN

script:
- id: script_yeelight_pair
then:
- script.execute: script_yeelight_off
- script.execute: script_yeelight_on
- ble_client.ble_write:
id: ble_yeelight
service_uuid: $YEELIGHT_SERVICE_UUID
characteristic_uuid: $YEELIGHT_CONTROL_UUID
value: [0x43, 0x67, 0x02] # enable pulse mode
- light.turn_on: yeelight_status_led
- delay: 10s
- light.turn_off: yeelight_status_led
- id: script_yeelight_on
then:
- ble_client.ble_write:
id: ble_yeelight
service_uuid: $YEELIGHT_SERVICE_UUID
characteristic_uuid: $YEELIGHT_CONTROL_UUID
value: [0x43, 0x40, 0x01]
- id: script_yeelight_off
then:
- ble_client.ble_write:
id: ble_yeelight
service_uuid: $YEELIGHT_SERVICE_UUID
characteristic_uuid: $YEELIGHT_CONTROL_UUID
value: [0x43, 0x40, 0x02]
- id: script_yeelight_brightness
parameters:
value: float # 0% - 100%
then:
- ble_client.ble_write:
id: ble_yeelight
service_uuid: $YEELIGHT_SERVICE_UUID
characteristic_uuid: $YEELIGHT_CONTROL_UUID
value: !lambda return { 0x43, 0x42, (uint8_t)(value * 100) };

笔者做的主要更改是支持了sensor_yeelight_notify,它监听灯的状态变化,并将状态同步到 ESPHome 的 light 实体上。这样,无论是通过 Home Assistant 控制灯,还是直接旋转灯进行亮度调节,ESPHome 都能正确地获取灯的状态并更新。

使用方式是:

  1. 将上述代码保存为yeelight.yaml,并替换其中的YEELIGHT_MAC为实际的灯的 MAC 地址(可以先扫描附近的 BLE 设备,然后根据名称yeelight_ms或者yl_candela找到灯),配对按键使用的GPIO0也可以根据情况修改。
  2. 通过esphome run yeelight.yaml将代码烧录到 ESP32 上。
  3. 确保灯处于蓝牙连接范围内。
  4. 通过esphome logs yeelight.yaml查看日志,在配对成功前,可以看到 ESP32 和灯的蓝牙连接间歇性断开,虽然 ESP32 会自动重连,但是此时操作的延迟会很大。
  5. 趁着连接成功的间隙,按下配对按钮(这里使用的是GPIO0,通常是 ESP32 开发板的BOOT按钮),等待 ESP32 开发板上的 LED 亮起,氛围灯也开始闪烁时,表示开始配对,此时旋转灯进行配对。
  6. 配对成功后,ESP32 和灯的连接会保持稳定,此时就可以添加进 Home Assistant,来控制灯的开关和亮度了。

此外,如果 ESP32 的物理按键不方便使用,也可以将binary_sensor去除,改为通过编写button组件在 Home Assistant 中生成模拟按键,点击即可触发配对脚本。例如:

1
2
3
4
5
button:
- platform: template
name: "Yeelight Pair Button"
on_press:
- script.execute: script_yeelight_pair

在文章将公牛智家设备接入 Home Assistant 中,笔者介绍了通过抓包分析将公牛智能开关接入 Home Assistant 的方法。最近,家里的菲斯曼(Viessmann)壁炉也需要接入智能家居系统,以便更灵活地控制暖气和热水温度。虽然菲斯曼官方提供了 ViCare App,但其对第三方集成的支持主要集中在欧洲服务器,而中国区的设备使用的是独立的服务器和 API,现有的开源插件无法直接使用。因此,笔者再次通过抓包分析,实现了对菲斯曼中国区设备的接入。

经过分析,菲斯曼中国区 API 主要包括以下几个部分:

  • /idass/user/login,提交手机号和密码进行登录,获取access_token
  • /api/home/familyDevices/v2,获取家庭设备列表,包括壁炉的physicsId等关键信息。
  • /api/device/detail,获取设备的详细运行状态,如当前模式、故障代码等。
  • /api/device/scanStatus,获取设备的实时传感器数据,如出水温度、燃烧状态等。
  • /api/3/sendToDevice/setChsetTemp 等控制接口,用于设置暖气温度、热水温度和运行模式。

通过这些 API,我们可以在 Home Assistant 中实现以下功能:

  1. 暖气控制:调节暖气目标温度,切换制热 / 防冻模式。
  2. 热水控制:调节生活热水目标温度。
  3. 状态监测:实时查看壁炉是否在燃烧、出水温度以及是否有故障报警。

全部代码已开源至hass-viessmann-cn,可以直接在 Home Assistant 中使用。安装方法非常简单,支持 HACS 一键安装,配置时只需输入菲斯曼 App 的账号密码即可。

笔者有一台小爱音箱,可惜它并不支持 AirPlay 协议。如果要将 Mac 上的音乐串流到这台音箱上播放,就需要通过蓝牙配对,体验并不理想。幸运的是,这个型号的音箱配备了 USB 接口,并且可以通过 USB 声卡的方式被电脑识别为音频输出设备。这就意味着,可以通过 Shairport Sync 等软件进行 AirPlay 协议配对,然后通过 USB 将音频流发送到这台音箱上播放。
这台音箱摆放位置旁边就是 NAS 服务器,运行着 TrueNAS Scale 25.04 系统。直接在 NAS 服务器上安装 Shairport Sync 并连接音箱是一个不错的选择。这个版本的 TrueNAS 支持运行 Docker 容器,因此软件安装上非常方便,不过在调试过程中还是踩了很多坑。本文记录一下安装过程,供参考。

检查 Alsa 配置

首先,通过 SSH 登陆到 TrueNAS 服务器上,执行cat /proc/asound/cards检查声卡是否正确识别:

1
2
3
4
5
admin@truenas[~]$ cat /proc/asound/cards
0 [PCH ]: HDA-Intel - HDA Intel PCH
HDA Intel PCH at 0x6005220000 irq 199
1 [Pro7990 ]: USB-Audio - 智能音箱 Pro-7990
Xiaomi Corporation 智能音箱 Pro-7990 at usb-0000:00:14.0-5, high speed

可以看到,智能音箱声卡已经被正确识别为Pro7990。接下来,我们需要检查 Alsa 的配置(特别是 Mixer)。虽然 TrueNAS 系统基于 Debian,但它本身并不包含 Alsa 工具,因此我们需要先创建一个 Ubuntu 容器,并将声卡设备映射进去。可以通过 TrueNAS 的 Web 界面创建一个 Docker 容器,选择「通过 YAML 进行安装」,并添加如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
services:
alsa-check:
command:
- bash
- '-c'
- sleep infinity
container_name: alsa_mixer_check
devices:
- /dev/snd:/dev/snd
environment:
- DEBIAN_FRONTEND=noninteractive
image: ubuntu:24.04
privileged: True
stdin_open: True
tty: True

Ubuntu 的 Docker 容器启动成功后,进入容器内部的命令行,安装 Alsa 工具:

1
2
apt update
apt install -y alsa-utils
阅读全文 »

在 Ubuntu 系统上,有时我们需要对某些内核模块进行定制化修改,例如更新补丁、进行调试或加入额外日志等。完整重新编译内核会非常耗时,但好消息是:Linux 的内核构建系统允许我们仅重新编译目标模块,而不需要构建整个内核。

本文介绍如何在 Ubuntu 上为当前正在运行的内核版本重新编译一个模块,并正确安装和加载它。示例模块为 kvm-intel.ko

准备内核源码

Ubuntu 提供了与当前内核版本在同一 ABI 下的源码,可以通过 apt-get 直接获取:

1
apt-get source linux-image-unsigned-$(uname -r)

执行后目录中通常会出现类似:

1
linux-hwe-6.8-6.8.0/

这就是 Ubuntu 为 HWE 6.8 系列内核提供的完整源码树。

同步配置与符号信息

Kernel module 的编译依赖两个关键文件:

  1. .config — 当前内核的编译配置
  2. Module.symvers — 内核导出的符号版本信息

这两者必须与正在运行的内核一致,否则编译出的模块会因为 ABI 不一致而无法加载。

我们将系统中的配置和符号文件复制到源码树中:

1
2
cp /boot/config-$(uname -r) .config
cp /usr/src/linux-headers-$(uname -r)/Module.symvers .

准备构建环境

执行:

1
2
make oldconfig && make prepare
make modules_prepare

当它们准备完成后,就可以只编译目标模块了。

只编译目标模块

Linux 内核允许使用 M=<path> 的方式仅构建某个目录下的模块,这大大缩短编译时间。

以 KVM 模块为例:

1
make -j$(nproc) M=arch/x86/kvm modules

只会编译:

  • arch/x86/kvm/kvm.ko
  • arch/x86/kvm/kvm-intel.ko(Intel 虚拟化支持)
  • arch/x86/kvm/kvm-amd.ko(如果是 AMD)

kvm-intel.ko 为例,编译完成后可以查看模块信息:

1
modinfo arch/x86/kvm/kvm-intel.ko

可以确认版本号、依赖项、符号是否正确。

替换内核模块

为了让系统使用我们自己编译的版本,需要将它复制到系统模块目录中:

1
2
sudo cp arch/x86/kvm/kvm-intel.ko \
/lib/modules/$(uname -r)/kernel/arch/x86/kvm/

然后更新模块依赖:

1
sudo depmod -a

这样系统就能识别新的模块文件。

加载模块

卸载旧模块后再加载新的模块。若旧模块已加载,可先移除:

1
sudo modprobe -r kvm_intel

然后加载我们编译过的模块,并附带参数:

1
sudo modprobe kvm_intel pt_mode=1

若加载成功,说明自定义模块已生效。

0%