星号密码一般分为窗口和网页两种,密码一般以“*”字符显示,也有些以其它字符比如圆点或#显示,在编程中可以设置任意字符为密码掩码。密码框一般为编辑框(Edit),也叫文本框(TextBox),也有些是自绘框,这里我们只看标准Edit,密码框具有一个ES_PASSWORD的样式,普通Edit没有。但是,并不是所有显示“*”号的都是密码框,有些看似是密码框,其实是伪密码框,或者说是“真”星号,就是说它根本就是一个普通的Edit,密码原本就是*字符,而不是掩码,这种框用句柄工具(ViewWizard)查看,看到的是*号,而看不到真正的密码,这种情况一般是设计者为了保留密码位数而临时设置的,单击它会直接清空密码,因为它里面存放的是*,如果不清空程序就会把*当成是真正的密码,从而发生错误,所以对于这种情况,是看不到密码的。
如何查看原本的密码呢?我们可以用一个消息:EM_GETPASSWORDCHAR和EM_SETPASSWORDCHAR,一个是获取密码字符,一个是设置密码字符,获取到的是密码掩码的ASCII码,要显示密码,我们先将密码框设置为0字符,将它变成一个普通的文本框,然后真正的密码就显示出来了,然后再用常规的WM_GETTEXT消息,即可复制密码框文本,之后再还原,否则密码就会一直显示。顺便说一下WM_GETTEXT消息,这个消息是用来获取编辑框的内容,但是它也可以用来获取“窗口”内容,实际上编辑框也是窗口,但是在获取和设置文本时稍有不同。普通的窗口(非Edit控件)获取和设置文本一般用GetWindowText和SetWindowText,获取窗口文本长度用GetWindowTextLength,包括Button(按钮)、Static(静态控件)等都可以用这些函数,但Edit比较特殊,因为它的内容比较多,不像窗口标题一样,只有一行,所以Edit文本不能用上面说的函数,而必须借助WM_GETTEXT和WM_SETTEXT消息实现,获取长度用WM_GETTEXTLENGTH,后者可兼容前者,用后者的方法也可对前者进行修改,反过来不行。但是,GetWindowText并非间接调用WM_GETTEXT,设置也一样,你可以在窗口函数中hook掉WM_GETTEXT或WM_SETTEXT,别的程序便不能控制你的窗口内容了,但用Get/Set之类的仍然能改变或获取你的窗口标题。
到现在你可能以为可以自己做一个星号密码查看器了,其实问题没那么简单,还有个搜索问题需要解决。要查看密码,就要先找到密码窗口,如何搜索窗口呢?星号查看器程序一般用WindowFromPoint来获取鼠标所在位置的窗口,获取鼠标位置可以用GetCursorPos,当然有人喜欢用钩子,钩子虽灵活但容易被拦截,这里我们用GetCursorPos+定时器。接下来,你会发现有些窗口无法获取,比如WinRAR的设置密码窗口,这是为什么呢?其实WinRAR并没有拦截我们的代码,而是WinRAR的界面有许多“框架”窗口,这些窗口是透明的,而且比其它控件都要大,所以挡住了其它控件,而WindowFromPoint的搜索顺序是按窗口Z轴进行的,也就是先搜索最上层的窗口或控件,所以虽然后面的控件能够看到,可是却获取不到。如何才能做到精准搜索呢?我们可以自己编一个函数,当发现一个子窗口,就递归搜索它的兄弟窗口,看有没有比它Size小的,有的话就把小的当作搜索目标,问题就解决了。然而,很少有软件解决了这个问题,我知道的能精确搜索的软件有微软的Spy++,还有我自己的ViewWizard,还有个WndSpy等。不过,测试发现Spy++还是有漏洞的,或者说不算太精确,有些窗口它仍然搜索不到,比如某些无效(Disabled)的控件。如此,问题算基本上解决了,但还有一个问题,就是透明窗口问题,有些窗口具有WS_EX_TRANSPARENT样式,鼠标是点击不到的,用WindowFromPoint是搜索不到的,ChildWindowFromPointEx函数说是能够搜索透明窗口,实际测试仍然搜索不到,这个问题虽然不是很重要,但要解决还是很容易的,只需重新遍历一次目标窗口之上的顶层窗口即可。透明窗口目前只有ViewWizard工具能够搜索,尚无发现有其它软件支持透明窗口的搜索。除了搜索之外,还可以用其它方法,例如遍历,这种方法不存在上述的问题,但是遍历也有不同的方法,例如典型的EnumWindows/EnumChildWindows,这两个函数都无法获取隐藏窗口(这里的隐藏指进程在内核中hook了NtUserQueryWindow,而非窗口的Style属性),但GetWindow却能获取,不过这一点并不影响我们获取密码,可以不考虑这个。另外,星号查看器程序只适用于标准Edit控件,如果密码框非以上标准Windows控件,比如是自写控件,非窗口类,那用以上方法就无法获取密码了。
好了,窗口密码查看器已经实现了,接下来看网页密码。能否获取网页密码和浏览器有关,不同的浏览器方法不同,这里以IE为例,谈一下如何查看IE网页中的星号密码。要获取网页对象,在工程中需要引用一个mshtml.tlb的库,然后就能操作一个网页元素了,把页面元素复制过来,判断是否包含密码(password)元素,有则是密码,没有则不代表一定没有密码,因为password类型可以改成别的,不过没关系,只要能看到星号,我们就能查看密码,只要遍历页面所有元素,就能找到密码值。然而,网页元素也要面对一个窗口对象所遇到的问题,前面说了,框架形式的窗口会阻碍我们搜索下层的窗口,同样的,框架页面元素也会屏蔽掉它里面的子元素,框架类(IFRAME)也是一个元素,它本身又包含另一个页面对象,所以搜索到框架,我们就必须递归寻找最内层的元素,这样才能找到最终的元素。实际上,用这种方法,不但能做一个网页密码查看器,还能做一个网页Spy++。
最后说一下如何防范星号查看器程序。对设计者而言,最简单的方法是做一个假的密码框,也就是用一个普通的编辑框代替密码框,里面直接输入*字符,不放密码,只为显示密码长度用。还有种方法是Hook掉WM_GETTEXT,让其它进程无法获取自己的文本。按照上面的方法,我共制作了五个版本的星号查看器程序,还差最后一个没研究出来,就是暴力读内存强制获取密码,但不同的程序需要完全不同的方法,所以只研究了记事本程序(notepad),发现notepad程序的Edit控件文本地址一直位于Edit窗口字节值+0x4F0处(WinXP/SP3下文本偏移始终为0xAABE8),直接读取该地址处的值就是文本框的内容。
以下是5个版本的核心代码:
'http://zzmzzff.blog.163.com
'版本1 跟踪焦点获取密码
Public Function m_ReadPwd() As String
Dim hWndSrc As Long
Dim hEdit As Long
Dim ThreadId As Long
Dim nLength As Long
Dim ProcessId As Long
Dim CurTID As Long
hWndSrc = GetForegroundWindow()
If hWndSrc = 0 Then Exit Function
CurTID = GetCurrentThreadId()
ThreadId = GetWindowThreadProcessId(hWndSrc, ByVal 0)
If CurTID = ThreadId Then m_ReadPwd = Chr(0): Exit Function
AttachThreadInput CurTID, ThreadId, True
hEdit = GetFocus()
If hEdit <> 0 Then
Dim nChar As Long
Dim szText As String
If GetWindowLong(hEdit, GWL_STYLE) And ES_PASSWORD Then '是密码框
nChar = SendMessage(hEdit, EM_GETPASSWORDCHAR, 0, ByVal 0&)
If nChar <> 0 Then
PostMessage hEdit, EM_SETPASSWORDCHAR, 0, 0
Sleep 10
nLength = SendMessage(hEdit, WM_GETTEXTLENGTH, 0, ByVal 0&)
If nLength > 0 Then
szText = String(nLength + 1, Chr(0))
Call SendMessage(hEdit, WM_GETTEXT, nLength + 1, ByVal szText)
szText = Left(szText, InStr(szText, Chr(0)) - 1)
End If
PostMessage hEdit, EM_SETPASSWORDCHAR, nChar, 0
End If
End If
End If
AttachThreadInput CurTID, ThreadId, False
m_ReadPwd = szText
End Function
'版本2 自动获取
'搜索密码
Public Function m_SearchPwd(Optional ByVal bForceSearch As Boolean = True) As String
Dim hWndSrc As Long
Dim ThreadId As Long
Dim nLength As Long
Dim CurTID As Long
Dim pt As POINTAPI
GetCursorPos pt
hWndSrc = WindowFromPoint(pt.x, pt.y) '获取鼠标所在窗口
'检查窗口,以获取深层/透明/无效窗口,有些控件Z序和尺寸不谐调,默认只能获取上层控件,例如WinRAR的密码窗口,此法可获取任意窗口
If bForceSearch Then m_CheckWnd hWndSrc, pt.x, pt.y
If hWndSrc = 0 Then Exit Function
CurTID = GetCurrentThreadId()
ThreadId = GetWindowThreadProcessId(hWndSrc, ByVal 0)
If CurTID = ThreadId Then m_SearchPwd = Chr(0): Exit Function '如果是自己退出
nLength = SendMessage(hWndSrc, WM_GETTEXTLENGTH, 0, ByVal 0&) '长度
If nLength <= 0 Then Exit Function
Dim nChar As Long
Dim szText As String
szText = String(nLength + 1, Chr(0))
If GetWindowLong(hWndSrc, GWL_STYLE) And ES_PASSWORD Then
nChar = SendMessage(hWndSrc, EM_GETPASSWORDCHAR, 0, ByVal 0&) '是否密码框
If nChar <> 0 Then
PostMessage hWndSrc, EM_SETPASSWORDCHAR, 0, 0
Sleep 10
End If
End If
'这次取所有窗口文本
Call SendMessage(hWndSrc, WM_GETTEXT, nLength + 1, ByVal szText)
If nChar <> 0 Then PostMessage hWndSrc, EM_SETPASSWORDCHAR, nChar, 0
szText = Left(szText, InStr(szText, Chr(0)) - 1)
m_SearchPwd = szText
End Function
'检查窗口
Sub m_CheckWnd(ByRef hWinCheck As Long, x As Long, y As Long, Optional bSearchHidden As Boolean, Optional bSearchThrough As Boolean)
Dim hWin As Long
Dim hWinParent As Long
Dim hWinChild As Long
Dim lpRectChild As RECT
Dim lpRectWin As RECT
'是否搜索透明窗口,以获取最上层窗口 //这个可以省略,一般用不上,要达到完美的话添上
If bSearchThrough Then
CheckTransparentWin hWinCheck, x, y
End If
'检查子窗口,以便获取灰色窗口
hWin = hWinCheck
CheckSubWin hWin, x, y
'获取最深兄弟窗口,如果是控件再进行更深层次搜索
If IsHaveCaption(hWin) = False Then
hWinParent = GetAncestor(hWin, GA_PARENT)
'Debug.Print "hWinParent=", hWinParent
If hWinParent = GetDesktopWindow() Then
hWinParent = hWin
End If
GetWindowRect hWin, lpRectWin
hWinChild = GetWindow(hWinParent, GW_CHILD)
Do While hWinChild <> 0
If hWinChild <> hWin Then
'是否搜索隐藏窗口 //注意,此处不能用IsWindowVisible,父窗隐藏后将无法知道子窗的显示状态
If Not bSearchHidden Imp (GetWindowLong(hWinChild, GWL_STYLE) And WS_VISIBLE) <> 0 Then
GetWindowRect hWinChild, lpRectChild
If PtInRect(lpRectChild, x, y) Then
If PtInRect(lpRectWin, lpRectChild.Left, lpRectChild.Top) <> 0 And _
PtInRect(lpRectWin, lpRectChild.Right, lpRectChild.Bottom) <> 0 Then
hWin = hWinChild
'Debug.Print "目标窗口=", hWin
Exit Do
End If
End If
End If
End If
hWinChild = GetWindow(hWinChild, GW_HWNDNEXT)
Loop
End If
If hWin <> hWinCheck Then
hWinCheck = hWin
m_CheckWnd hWinCheck, x, y '递归
End If
End Sub
'检查子窗口
Private Sub CheckSubWin(hWin As Long, x As Long, y As Long, Optional bSearchHidden As Boolean)
Dim hChild As Long
Dim lpRect As RECT
hChild = GetWindow(hWin, GW_CHILD)
Do While hChild <> 0
If Not bSearchHidden Imp (GetWindowLong(hChild, GWL_STYLE) And WS_VISIBLE) <> 0 Then
GetWindowRect hChild, lpRect
If PtInRect(lpRect, x, y) Then
hWin = hChild
Exit Do
End If
End If
hChild = GetWindow(hChild, GW_HWNDNEXT)
Loop
End Sub
'检查穿透窗口
Private Sub CheckTransparentWin(hWin As Long, x As Long, y As Long)
Dim hTop As Long
Dim lpRect As RECT
hTop = GetAncestor(hWin, GA_ROOT)
hTop = GetWindow(hTop, GW_HWNDPREV)
Do While hTop <> 0
If IsWindowVisible(hTop) <> 0 Then
GetWindowRect hTop, lpRect
If PtInRect(lpRect, x, y) Then
hWin = hTop
CheckTransparentWin hWin, x, y
End If
End If
hTop = GetWindow(hTop, GW_HWNDPREV)
Loop
End Sub
'有标题栏
Private Function IsHaveCaption(ByVal hWin As Long) As Boolean
IsHaveCaption = ((GetWindowLong(hWin, GWL_STYLE) And WS_CAPTION) = WS_CAPTION)
End Function
'版本3 //一次性获取
'获取所有密码
Public Function m_EnumPwd(Optional ByVal bShowPwd As Boolean) As String
Dim hWndSrc As Long
Dim nLength As Long
Dim szText As String
Dim szTilte As String
Dim szPwd As String
hWndSrc = FindWindow(vbNullString, vbNullString)
Do While hWndSrc <> 0
szPwd = ""
m_GetText hWndSrc, szPwd, bShowPwd
If Not bShowPwd Then
If szPwd <> "" Then
nLength = GetWindowTextLength(hWndSrc)
If nLength > 0 Then
If nLength > 255 Then nLength = 255 '取窗口标题,只取前255个
szTilte = String(nLength + 1, Chr(0))
Call GetWindowText(hWndSrc, szTilte, nLength + 1)
szTilte = Left(szTilte, InStr(szTilte, Chr(0)) - 1)
End If
szText = szText & "窗口 " & szTilte & " 的密码:" & vbCrLf & szPwd & vbCrLf
End If
End If
hWndSrc = GetWindow(hWndSrc, GW_HWNDNEXT)
Loop
m_EnumPwd = szText
End Function
Sub m_GetText(ByVal hWndSrc As Long, szPwd As String, Optional bShowPwd As Boolean)
Dim nLength As Long
Dim nChar As Long
Dim szText As String
hWndSrc = GetWindow(hWndSrc, GW_CHILD)
Do While hWndSrc <> 0
If GetWindowLong(hWndSrc, GWL_STYLE) And ES_PASSWORD Then
If SendMessageTimeout(hWndSrc, WM_NULL, 0, 0, SMTO_ABORTIFHUNG Or SMTO_BLOCK, 500, ByVal 0&) <> 0 Then
nChar = SendMessage(hWndSrc, EM_GETPASSWORDCHAR, 0, ByVal 0&)
If nChar <> 0 Then '是密码框
If Not bShowPwd Then
nLength = SendMessage(hWndSrc, WM_GETTEXTLENGTH, 0, ByVal 0&) '长度
szText = ""
If nLength > 0 Then
szText = String(nLength + 1, Chr(0))
PostMessage hWndSrc, EM_SETPASSWORDCHAR, 0, 0
Sleep 10
Call SendMessage(hWndSrc, WM_GETTEXT, nLength + 1, ByVal szText)
PostMessage hWndSrc, EM_SETPASSWORDCHAR, nChar, 0
szText = Left(szText, InStr(szText, Chr(0)) - 1)
End If
szPwd = szPwd & "密码: " & szText & vbCrLf
Else
PostMessage hWndSrc, EM_SETPASSWORDCHAR, 0, 0
PostMessage hWndSrc, WM_SIZE, 0, 0
End If
End If
End If
End If
m_GetText hWndSrc, szPwd, bShowPwd '递归
hWndSrc = GetWindow(hWndSrc, GW_HWNDNEXT)
Loop
End Sub
'版本4 //获取网页密码
'获取所有密码 mod_Main
Public Function m_EnumWebPwd() As String
Dim hWndSrc As Long
Dim hWndIESer As Long
Dim nLength As Long
Dim szText As String
Dim szTilte As String
Dim ProcessId As Long
hWndSrc = FindWindow(vbNullString, vbNullString)
Do While hWndSrc <> 0
szPwd = ""
EnumSubWnd hWndSrc
If szPwd <> "" Then
GetWindowThreadProcessId hWndSrc, ProcessId
nLength = GetWindowTextLength(hWndSrc)
If nLength > 0 Then
If nLength > 255 Then nLength = 255 '取窗口标题,只取前255个
szTilte = String(nLength + 1, Chr(0))
Call GetWindowText(hWndSrc, szTilte, nLength + 1)
szTilte = Left(szTilte, InStr(szTilte, Chr(0)) - 1)
End If
szText = szText & "句柄: " & hWndSrc & " 进程: " & ProcessId & " 网页标题: " & szTilte & vbCrLf & "密码: " & szPwd & vbCrLf
End If
hWndSrc = GetWindow(hWndSrc, GW_HWNDNEXT)
Loop
m_EnumWebPwd = szText
End Function
Private Function EnumSubWnd(ByVal hWin As Long) As Long
EnumChildWindows hWin, AddressOf EnumWinCallback, ByVal 0
End Function
Public Function EnumWinCallback(ByVal MyhWnd As Long, lParam As Long) As Long
If IsIEServerHwnd(MyhWnd) Then
m_ReadWebPwd MyhWnd, szPwd
End If
EnumWinCallback = 1
End Function
'IEServer窗口
Private Function IsIEServerHwnd(ByVal hWin As Long) As Boolean
IsIEServerHwnd = GetWinClass(hWin) = "Internet Explorer_Server"
End Function
'mod_Html
'获取密码
Public Function m_ReadWebPwd(ByVal hWin As Long, szText As String)
'On Error Resume Next
Dim Doc As IHTMLDocument
Set Doc = IEDOMFromhWnd(hWin)
Call GetPwdByDocument(Doc, szText)
End Function
Sub GetPwdByDocument(ByVal Doc As IHTMLDocument, szText As String)
On Error Resume Next
If Doc Is Nothing Then Exit Sub
Dim SubDoc As IHTMLDocument
Dim Ele As IHTMLElement
Dim EleFra As IHTMLIFrameElement3
Dim szPwd As String
For Each Ele In Doc.All
If Ele.tagName = "IFRAME" Then '如果是框架,继续递归获取下一级页面
Set EleFra = Ele
Set SubDoc = EleFra.contentDocument
GetPwdByDocument SubDoc, szText
Else
If Ele.getAttribute("type") = "password" Then
szPwd = CStr(Ele.getAttribute("value"))
If szPwd = "" Then szPwd = Ele.innerText
szText = szText & szPwd & vbCrLf
End If
End If
Next
End Sub
'版本5 //自动获取网页密码
'搜索密码
Public Function m_SearchPwd() As String
Dim hWndSrc As Long
Dim pt As POINTAPI
GetCursorPos pt
hWndSrc = WindowFromPoint(pt.x, pt.y) '获取鼠标所在窗口
If IsIEServerHwnd(hWndSrc) Then
ScreenToClient hWndSrc, pt
m_SearchPwd = m_GetWebPwd(hWndSrc, pt.x, pt.y)
End If
End Function
'获取密码
Public Function m_GetWebPwd(ByVal hWin As Long, x As Long, y As Long) As String
'On Error Resume Next
Dim Doc As IHTMLDocument
Dim szText As String
Set Doc = IEDOMFromhWnd(hWin)
Call GetPwdByPoint(Doc, x, y, szText)
m_GetWebPwd = szText
End Function
Private Sub GetPwdByPoint(ByVal Doc As IHTMLDocument2, ByVal x As Long, ByVal y As Long, szText As String)
On Error Resume Next
If Doc Is Nothing Then Exit Sub
Dim SubDoc As IHTMLDocument
Dim Ele As IHTMLElement
Dim EleIFrame As IHTMLIFrameElement3
Dim rt As Object
Set Ele = Doc.elementFromPoint(x, y)
If Not (Ele Is Nothing) Then
Debug.Print Ele.offsetLeft, Ele.offsetTop
If Ele.tagName = "IFRAME" Then '如果是框架,继续递归获取下一级页面
Set EleIFrame = Ele
Set SubDoc = EleIFrame.contentDocument
Set rt = Ele.getBoundingClientRect
x = x - rt.Left
y = y - rt.Top
GetPwdByPoint SubDoc, x, y, szText
Else
'Debug.Print x, y, Ele.offsetLeft, Ele.offsetTop
If Not IsNull(Ele.getAttribute("value")) Then
szText = Ele.getAttribute("value")
Else
szText = Ele.innerText
End If
End If
End If
End Sub
'***************** 核心代码结束 ************************