先来聊聊这个现象背后的原理。
在 Windows 系统中,有一个标准控件叫作 Edit,当它的 Password 属性被设置为 True 时,用户输入的内容会自动显示为星号,看起来似乎被严密保护。但实际上,这只是视觉上的伪装。程序内部的 Edit 控件依然真实地保存着用户输入的密码——应用程序自身可以读取,其他程序也能通过发送 WM_GETTEXT 或 EM_GETLINE 这类消息来获取控件中的内容。黑客软件的套路正是利用这一点:只要发现当前窗口是一个 Edit 控件,并且带有 ES_PASSWORD 属性,那么直接通过 SendMessage 发送一条 WM_GETTEXT 或 EM_GETLINE 消息,密码就会从星号背后原形毕露。
黑客软件是如何运作的
黑客软件的第一步,是获取当前窗口并判断它是否为 Edit 控件。常见做法是通过鼠标定位目标窗口——例如在 WM_MOUSEMOVE 的响应函数中实时检测。下面是一段典型代码:
//将客户坐标转换成屏幕坐标
ClientToScreen(&point);
//返回一个包含指定屏幕坐标点的窗口
CWnd* pWnd = CWnd::WindowFromPoint(point);
if (pWnd)
{
//获取窗口句柄
HWND hwndCurr = pWnd->GetSafeHwnd();
if ((::GetWindowThreadProcessId (GetSafeHwnd(), NULL)) != (::GetWindowThreadProcessId (hwndCurr, NULL)))
{
char lpClassName[255];
//获取类名
if (::GetClassName(hwndCurr, lpClassName, 255))
{
//判断是否是Edit控件
if (0 == m_strWndClass.CompareNoCase("EDIT"))
{
//获取窗口风格
LONG lStyle = ::GetWindowLong(hwndCurr, GWL_STYLE);
//如果设置了ES_PASSWORD属性
if (lStyle & ES_PASSWORD)
{
char szText[255];
//通过掌握的句柄hwndCurr向此控件发送WM_GETTEXT消息
::SendMessage(hwndCurr, WM_GETTEXT, 255, (LPARAM)szText);
//密码已保存在szText中
m_strPassword = szText;
}
}
}
}
}
这段代码中有三个关键步骤值得注意:
ClientToScreen(&point);
CWnd* pWnd = CWnd::WindowFromPoint(point);
HWND hwndCurr = pWnd->GetSafeHwnd();
这三行代码的作用是获取当前鼠标位置所在窗口的句柄——在后续的 SendMessage 调用中,这个句柄就是目标地址。
再往下看:
::SendMessage(hwndCurr, WM_GETTEXT, 255, (LPARAM)szText);
这才是真正的核心攻击。第一个参数是刚才获取的窗口句柄,第二个参数是 WM_GETTEXT 消息,Edit 控件收到后会将文本内容写入 szText 中。至此,密码就被窃取了。
如何防范这种攻击
了解黑客软件的手法后,应对策略也就清晰了。
问题的根源在于:Edit 控件对发送 WM_GETTEXT 或 EM_GETLINE 消息的对象没有进行任何身份验证——只要知道窗口句柄,任何进程都可以读取内容。要堵住这个漏洞,关键在于对消息发送者的身份进行校验。下面给出一个具体的实现方案:
1. 创建一个新的 CEdit 子类
从 CEdit 派生出子类 CPasswordEdit,并在其中声明一个全局变量 g_bSenderIdentity 来标识消息发送者的身份:
BOOL g_bSenderIdentity;
然后重写 CWnd 的虚函数 DefWindowProc,在这个回调函数中执行身份验证:
LRESULT CPasswordEdit::DefWindowProc (UINT message,WPARAM wParam,LPARAM lParam)
{
//对Edit的内容获取必须通过以下两个消息之一
if((message==WM_GETTEXT) ||(message==EM_GETLINE))
{
//检查是否为合法
if(!g_bSenderIdentity)
{
//非法获取,显示信息
AfxMessageBox(_T ("报告:正在试图窃取密码!"));
return 0;
}
//合法获取
g_bSenderIdentity=FALSE;
}
return CEdit::DefWindowProc (message,wParam,lParam);
}
2. 在数据输入对话框中做相应处理
在对话框里声明一个 CPasswordEdit 类型的成员变量:
CPasswordEdit m_edtPassword;
然后在 OnInitDialog() 中绑定控件:
m_edtPassword.SubclassDlgItem(IDC_EDIT_PASSWORD,this);
之后,在对话框的数据交换函数中将身份标记为合法:
void CDlgInput::DoDataExchange (CDataExchange*pDX)
{
//如果获取数据
if(pDX->m_bSa veAndValidate)
{
g_bSenderIdentity=TRUE;
}
CDialog::DoDataExchange(pDX);
//{{ AFX_DATA_MAP(CDlgInput)
DDX_Text (pDX,IDC_EDIT_PASSWORD,m_sPassword);
//}}AFX_DATA_MAP
}
这样一来,密码输入框就获得了“合法身份”,从而得到了有效保护。
小结
以上方法主要适用于 VC 环境下的程序。如果使用 VB、Delphi 等其他语言,则需要借助 VC 制作一个专用的 Password ActiveX 控件,实现思路与上述逻辑基本一致。所有示例代码均在 Visual C++ 6.0 下编译调试通过。
