Introduction
As long as computer
hardware and software have used passwords
there have been people trying to crack those passwords.
Just as there have been people working to increase
security for those passwords.
This article takes a
look at Windows passwords
from both sides.
In Windows 3.x there
was a little know bug with the password
control. If you select the "****" text
displayed in a password control
and copy the text, the text copied into the
clipboard is the actual password,
not the displayed "****" text. Microsoft found
this bug and fixed it starting in Windows 95. But people
found ways to copy passwords
from Windows 95, and wrote programs to do it. These
programs are not password
crackers, instead they use a security hole in
Windows to copy the password from another running
program. I downloaded one of these programs and found it
intriguing, so I wrote my own version. Later, once
Windows 2000 was released, I was disappointed to see
Microsoft again fixed the "bug" and so
programs like these didn't work on Windows 2000 (and now
a days Windows XP). But after several failed attempts, I
finally found a way to copy the password
while running on any 32-bit Windows OS.
Use
PasswordSpy is a very
simple program to use. You simply start the program that
contains the forgotten
password as well as
PasswordSpy. Then drag the magnifying glass from
PasswordSpy over the "****" field and
PasswordSpy will display the password. It should be
noted that PasswordSpy is not intended for
mischievous purposes. PasswordSpy has been tested on
Win95/98/ME and WinNT/2K/XP.
Features
In addition to being a
useful application, PasswordSpy demonstrates some useful
and interesting code.
- A single instance
application. If the user starts a second copy of
PasswordSpy, the first copy is brought to the
foreground instead of starting a second copy.
- OS detection.
Shows how to detect which OS the program is running
on, Windows 95/98/ME/NT/2K/XP.
- "Always
on top" With a single line of code
you can set or remove the "always
on top" state from your application.
- Inter Process
Communication. PasswordSpy uses several forms of
IPC including the
WM_COPYDATA
message
as well as memory-mapped files.
- Setting windows
hooks. In order to extract the password on
Win2K/XP you must set a hook into the remote
process.
Code Details
PasswordSpy works by
taking advantage of a security hole in Windows. If you
send a WM_GETTEXT
message to a password control,
the text returned is the actual password not the
"****" displayed in the control. This sneaky
technique works great on Windows NT/95/98/ME, but on
Win2K/XP Microsoft has added security. On Win2K/XP if
you send a WM_GETTEXT
message to a password control, it first checks the
calling process to determine if it has access. If the
calling process is the same process as the one
that created the password control, the WM_GETTEXT
message still returns the password. However, if the
calling process is different, then the return is ERROR_ACCESS_DENIED
.
So the key to retrieving the password in Win2K/XP is to
get your WM_GETTEXT
message to come from the same process as the password
control and not from PasswordSpy. One way to execute
your code in the context of another process is through
the use of Windows hooks.
By far the most
interesting aspect of PasswordSpy is the technique of
setting a Windows hook
with the SetWindowsHookEx
API. With this function you can install a hook into
either the whole system or a specific process. There are
a dozen different types of hooks to install, each type
monitors for a specific set of events. When one of those
events happens, your hook
code gets called. PasswordSpy uses the WH_GETMESSAGE
hook, which monitors
for calls to GetMessage
and PeekMessage
.
For more background info on hooks
please read the MSDN on SetWindowsHookEx
.
I have found several examples
of hooks on the
Internet, in books, and in the MSDN. But every example
I saw had at least one bug in the code. Here I'll
address the problem as well as my solution.
The hardest part about
using Windows hooks
is properly storing the handle to the hook.
Before you can set a hook
you need two things.
- A DLL containing the
hook function,
and
- the ID of the thread
you want to hook.
Now supposing Process
A sets a hook
into Process B. After hooking Process B,
the hook handle is
returned to Process A and the DLL is mapped into Process
B's address space. When one of the hooked events in Process
B happens, your hook code gets called from Process
B (It should be noted, your hook
code gets called from the remote process! In your hook
code if you call GetCurrentProcessId
, you
get the PID of the hooked process, not your original
program that set the hook).
From the hook code
you can do whatever you want, but before your hook
code exits you are supposed to call CallNextHookEx
.
If you fail to call this function, any other hooks that
might be installed will fail to get the message. The
problem is CallNextHookEx
requires the
handle to the hook, but that handle was returned to Process
A and we're currently in Process B. Thus some
sort of Inter Process Communication is needed to
transfer the hook handle.
Most hook
samples solve this problem by creating a
"shared" section in the DLL.
#pragma data_seg("Shared")
HHOOK g_hHook = NULL;
#pragma data_seg()
#pragma comment(linker, "/section:Shared,rws")
In a nutshell, this
creates a single variable that is shared by all loaded
instances of that DLL. So if five processes load this
DLL, all five have access to that variable. But there
are a few problems with this method. For starters, some
compilers may not support this option. Second, what if
Microsoft decides to change how "shared"
sections work in future versions of Windows; that would
mean this technique would no longer work. Also, this
method has no thread synchronization, and since you have
multiple threads accessing this variable thread
synchronization is important.
To solve these problems
I used memory mapped files for the IPC, and a mutex for
thread synchronization. I encapsulated all this code
into a class I called CIPC. By using memory mapped files
I solve the problem of special compiler options because
none are needed, it's done using nothing but Win32 API
calls. Plus MMFs are a supported way of sharing data
between multiple processes, so Microsoft is not likely
to change that in future versions of Windows. And the
mutex ensures that thread access is synchronized.
Collapse
//***********************************************
// IPC.h
//***********************************************
#ifndef _IPC_H_
#define _IPC_H_
#define IPC_SHARED_MMF _T("{34F673E0-878F-11D5-B98A-00B0D07B8C7C}")
#define IPC_MUTEX _T("{34F673E1-878F-11D5-B98A-00B0D07B8C7C}")
// Class for Inter Process Communication using Memory Mapped Files
class CIPC
{
public:
CIPC();
virtual ~CIPC();
bool CreateIPCMMF(void);
bool OpenIPCMMF(void);
void CloseIPCMMF(void);
bool IsOpen(void) const {return (m_hFileMap != NULL);}
bool ReadIPCMMF(LPBYTE pBuf, DWORD &dwBufSize);
bool WriteIPCMMF(const LPBYTE pBuf, const DWORD dwBufSize);
bool Lock(void);
void Unlock(void);
protected:
HANDLE m_hFileMap;
HANDLE m_hMutex;
};
#endif
//***********************************************
// IPC.cpp
//***********************************************
#include "IPC.h"
//***********************************************
CIPC::CIPC() : m_hFileMap(NULL), m_hMutex(NULL)
{
}
//***********************************************
CIPC::~CIPC()
{
CloseIPCMMF();
Unlock();
}
//***********************************************
bool CIPC::CreateIPCMMF(void)
{
bool bCreated = false;
try
{
if(m_hFileMap != NULL)
return false; // Already created
// Create an in-memory 4KB memory mapped
// file to share data
m_hFileMap = CreateFileMapping((HANDLE)0xFFFFFFFF,
NULL,
PAGE_READWRITE,
0,
4096,
IPC_SHARED_MMF);
if(m_hFileMap != NULL)
bCreated = true;
}
catch(...) {}
return bCreated;
}
//***********************************************
bool CIPC::OpenIPCMMF(void)
{
bool bOpened = false;
try
{
if(m_hFileMap != NULL)
return true; // Already opened
m_hFileMap =
OpenFileMapping(FILE_MAP_READ | FILE_MAP_WRITE,
FALSE,
IPC_SHARED_MMF);
if(m_hFileMap != NULL)
bOpened = true;
}
catch(...) {}
return bOpened;
}
//***********************************************
void CIPC::CloseIPCMMF(void)
{
try
{
if(m_hFileMap != NULL)
CloseHandle(m_hFileMap), m_hFileMap = NULL;
}
catch(...) {}
}
//***********************************************
bool CIPC::ReadIPCMMF(LPBYTE pBuf, DWORD &dwBufSize)
{
_ASSERTE(pBuf);
bool bSuccess = true;
try
{
if(m_hFileMap == NULL)
return false;
DWORD dwBaseMMF = (DWORD)MapViewOfFile(m_hFileMap,
FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0);
_ASSERTE(dwBaseMMF);
// The first DWORD in the MMF contains the size of the data
DWORD dwSizeofInBuf = dwBufSize;
CopyMemory(&dwBufSize, (LPVOID)dwBaseMMF, sizeof(DWORD));
if(dwSizeofInBuf != 0)
{
if(dwBufSize > dwSizeofInBuf)
bSuccess = false;
else
CopyMemory(pBuf,
(LPVOID)(dwBaseMMF + sizeof(DWORD)),
dwBufSize);
}
UnmapViewOfFile((LPVOID)dwBaseMMF);
}
catch(...) {}
return bSuccess;
}
//***********************************************
bool CIPC::WriteIPCMMF(const LPBYTE pBuf, const DWORD dwBufSize)
{
_ASSERTE(pBuf);
bool bSuccess = true;
try
{
if(m_hFileMap == NULL)
return false;
DWORD dwBaseMMF = (DWORD)MapViewOfFile(m_hFileMap,
FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0);
_ASSERTE(dwBaseMMF);
// The first DWORD in the MMF contains the size of the data
CopyMemory((LPVOID)dwBaseMMF, &dwBufSize, sizeof(DWORD));
CopyMemory((LPVOID)(dwBaseMMF + sizeof(DWORD)),
pBuf, dwBufSize);
UnmapViewOfFile((LPVOID)dwBaseMMF);
}
catch(...) {}
return bSuccess;
}
//***********************************************
bool CIPC::Lock(void)
{
bool bLocked = false;
try
{
// First get the handle to the mutex
m_hMutex = CreateMutex(NULL, FALSE, IPC_MUTEX);
if(m_hMutex != NULL)
{
// Wait to get the lock on the mutex
if(WaitForSingleObject(m_hMutex, INFINITE) == WAIT_OBJECT_0)
bLocked = true;
}
}
catch(...) {}
return bLocked;
}
//***********************************************
void CIPC::Unlock(void)
{
try
{
if(m_hMutex != NULL)
{
ReleaseMutex(m_hMutex);
CloseHandle(m_hMutex);
m_hMutex = NULL;
}
}
catch(...) {}
}
Countering PasswordSpy
Now that you know how to
programmatically "copy" the password
from another application, the next logical question is
how do you prevent PasswordSpy from copying passwords
from applications you write. If your application stores
and displays passwords, and especially if you are
concerned about security, then you probably want to
protect your application from programs like PasswordSpy.
Since PasswordSpy can
copy passwords out
of other programs, the solution to protecting your own
programs is to never display the real password in
the first place. The best solution is to display a bogus
password in the password control. That way if someone
uses PasswordSpy to retrieve the password, all they get
back is a bogus password, not the real thing. Included
in the download is a program AntiPwdSpy. This program
shows how to protect your password controls from being
"spied." The program AntiPwdSpy behaves
exactly the same as the Windows NT Services dialog and
the Windows NT User Manager.
There are other ways to
counter programs like PasswordSpy. One way is to
intercept the WM_GETTEXT
message. This works, but using the bogus password method
has other benefits. By replacing the real password with
a bogus one, it is not possible to determine the length
of a password by looking at the password control. If a
program displays the text "***" in a password
control, right away you know that password is only three
characters in length. This greatly compromises the
security of that password by revealing the passwords
length. But if that password control displayed
"**************" as is done in most Microsoft
programs, you do not know anything about the password.