Software Security and the DirectPlay API

Dr. Dobb's Journal April 1997

Detecting multiple copies of an application

By Andrew Wilson

Andy, an engineer for NuMega Technologies, can be contacted at andyw@numega.com.

There have been many approaches to stopping software piracy, ranging from key disks to parallel-port locks. Key disks died shortly after the hard drive hit the market. Parallel-port locks work well, but they are only good for one computer and often annoy users enough to not buy the software in the first place. Custom serial-number validation is fine, but it's relatively easy to use the same serial number throughout a company. Something more intelligent is needed.

How do you make an application intelligent enough to know it is in violation of its license? Since almost every organization has a network, you simply need to add functionality that detects a network, then use that functionality to detect other running copies of the program. The application can then compare its serial number to those running on the network and terminate if it finds another copy.

While checking for duplicate serial numbers is a start, it might be even better if you could check for how many license agreements are good for that serial number. You could probably even add enough functionality that applications could communicate with each other and be aware when an invalid copy appears. They could even tell the new copy to shut down!

The Model

One approach to implementing such security procedures is to write a block of code invoked during application startup. It first uses the serial number to see if there are other systems using that number. Then it checks how many users the license is good for. If the user count is valid, the application enters the serial-number group and allows the user to continue. At the same time, other applications alert the new application what the valid user-license count is as a second check, in case someone has cracked the local system's user-count checker. If any of these are invalid, the user is running a pirated copy of the application. The app could then print an error message and terminate, or wait for the user count to become valid.

The Network

To successfully implement the scheme, you need to select a protocol common to every user, most typically TCP/IP and IPX. In the best-case scenario, both are used to search for the serial number. Upon finding your session, you select the protocol with the most serial numbers as the one to use for the remainder of the application. If you fail to see the serial number, then you are the only copy running, leaving a choice of which protocol to use.

But what if the user doesn't use either protocol? Luckily, there's always NetBEUI, the Windows Network API. However, NetBEUI is a nonroutable protocol that limits how far in the network your checking can go.

The Unsung DirectX SDK

Microsoft knew that to make Windows 95 an effective operating environment it had to do everything, including running processor-intensive games in an environment that allows multiple applications to run at the same time. Moreover, Microsoft had to defeat its own internal distancing from platform dependence. Consequently, a group of Microsoft developers were given the task of beating the things that makes Windows NT so powerful and injecting them into Windows 95. Thus, DirectX was born.

The DirectPlay API is a subset of the DirectX SDK. With DirectPlay, it is straightforward to make an application fully network aware and see other instances of itself across the network. The model it uses is straightforward:

Furthermore, the DirectPlay API uses three transports -- IPX, TCP/IP, and the modem.

To implement the security model I previously described, consider a "session" to be a serial number, the "player," an instance of the application, and the "player list" the valid user-license count.

Validate (available electronically; see "Availability," page 3), is an MDI MFC application generated by AppWizard in Visual C++ 4.1. In the OnCreate function of the CMainFrame class, a class of type CNetLicense is invoked. This is where all the networking is based. CMainFrame will receive messages only from CNetLicense concerning the validity of the license agreement. CMainFrame then continues to execute concurrently with CNetLicense.

CNetLicense is derived from the CDialog class. It is used to display the available network services and the user IDs. It starts by enumerating the various network services (the modem, TCP/IP, and IPX). The enumeration process searches for the service identifiers for TCP/IP and IPX (see Listing OneIt then generates a DirectPlay object based upon the network service you choose, in this case, TCP/IP. You use the returned DirectPlay object to call the DirectPlay member functions.

The next step is to enumerate the session list. You simply enumerate all sessions, storing their names and identifiers. If you find a session with a name the same as the serial number, then you attach to that session; otherwise, you create a new session setting the name equal to the serial number.

Now you need to see if you have a valid user-license agreement. You simply count how many users are in the session by enumerating them. The DirectPlay API has a structure with a member that contains the current user count; however, you want to keep this portable enough to move to a new transport, like a pure TCP/IP WinSock, and not be limited to the DirectPlay interface. If the count is greater than the license, you send a notification message to the CMainframe's HWND, which instructs the parent to take some action; in this case, it terminates.

If the user count is valid, then you create a user that identifies you, but not necessarily uniquely, on the network by querying for the system's name (see Listing Two). The CreatePlayer function has a bit of a quirk. It appears that if a user tries to reenter the network with the same player information, he gets assigned the same ID, causing CreatePlayer to fail. Consequently, you generate some random number for the ID and retry until you get an ID that hasn't been used before. This presents some obvious problems, but bear in mind that, at this writing, DirectX is still in beta, and you can't see previously used IDs.

After the user has been generated, you create a new thread, using _beginthread, which will handle message processing. The message processing is based upon a handle to a player event returned by CreatePlayer. It is critical to trap this handle, otherwise you cannot see messages being directed to the system. The message process is responsible for checking the network's message queue. You don't directly call the receive function as a part of the message loop because if you do, the system nearly stops. Instead, you call the Win32 WaitForSingleObject API function.

When CreatePlayer is called, one of two Win32 API calls are invoked, either CreateEvent or OpenEvent. The return value of either of these functions is an HEVENT type. When a message is received for a given player's HEVENT handle, a SetEvent or PulseEvent API call is made. WaitForSingleObject sees that an HEVENT object is in a signaling state and returns True. At that point, you know that a message is waiting for the user, and you can call receive to get the network message.

When you terminate the application, a DPSYS_DELETEPLAYER message is sent. However, once the application is in a termination state, the HEVENT handle becomes invalid. Knowing this, you need to use GetHandleInformation and check that the handle is valid before each call to receive. Otherwise, receive will return a failure, but the next call to WaitForSingleObject will return True again, throwing you into an infinite loop until the thread is terminated. The simple solution to this problem is to check the validity of the handle; should it fail, you terminate the thread using _exitthread (Listing Three).

You use _beginthread and _exitthread because you may choose to add functionality to the class by using C run-time library calls that generate a series of small memory leaks if you use the Win32 CreateThread and EndThread API calls.

This code effectively prevents users from using a duplicate serial number or going beyond the valid user count for that license. Further, the class is completely independent of a serial-number scheme. It expects the serial number and user count to be passed, allowing you to create any serial-number scheme.

The application is notified of a license violation by message, rather than return value. This is important. Usually, when an application tests the validity of something, it calls some function that returns a True or False. If the function returns False, you typically call MessageBox, print some error message, and terminate. However, using a tool like NuMega's SoftIce, you can set a breakpoint on _MessageBoxA so that when it is called, you can see the call stack prior to the MessageBox, see the code that does the test, and patch it by forcing a True response. By sending a message, the would-be pirate has more work to do because the test has already occurred and the call stack leads back to the Windows message process rather than the function that sent the message. When the message to terminate is sent, the test has already failed, and there is little a person can decipher.

Where to Go From Here

At this point, you have some powerful startup code. What you need to do next is add some run-time functionality that may benefit the user. You don't want to make your copyright validation cumbersome for users; rather than terminating the application, you may want to poll the network every couple of minutes until another user has exited the application. This would allow the user to enter the application in a valid network state.

Though DirectPlay does implement this model fairly well, you really should implement it using a different transport method. DirectPlay appears to be only for Windows 95, not NT. The direct use of the WinSock DLL would improve performance and give much greater control. Also, the messages reported back from WinSock are placed directly in your window's message queue, which makes it easier to process and manage.

Conclusion

The computer network is the simplest way to detect multiple copies of the same application. During the startup code, you can tell when someone is cheating and prevent them in a friendly way from being able to use an application. This soft approach to license enforcement is just as difficult to get around as existing methods, but it adds flexibility to the environment, doesn't scare the users away, notifies them of their license agreement when they violate it, and gives them a reason to purchase more copies of your applications.

DDJ

Listing One

BOOL CALLBACK CNetLicense::EnumNetSup(LPGUID lpGuid, LPSTR lpDesc,                        DWORD dwMajorVersion, DWORD dwMinorVersion, LPVOID lpv)
{            
        CNetLicense *MyNet = (CNetLicense *)lpv;
        LONG iIndex = MyNet->m_LB_Nets.AddString(lpDesc);
        
        if(iIndex == LB_ERR)
        {
            #ifdef _DEBUG
            afxDump << "Listbox error in CNetLicense\n";
            #endif
            return FALSE;
        }
        MyNet->m_LB_Nets.SetItemData(iIndex,(LPARAM) lpGuid);
       #ifdef _DEBUG
            afxDump << lpDesc << " " << lpGuid << "\n";
        #endif
        if(strstr(lpDesc,"TCP") != NULL)
            MyNet->m_lpGuidTCP = lpGuid;
        else if (strstr(lpDesc, "IPX") != NULL)
            MyNet->m_lpGuidIPX = lpGuid;
return TRUE;
}
BOOL CNetLicense::EnumNetProviders(void)
{
        #ifdef _DEBUG
            afxDump << "Network Providers:\n";
        #endif
        if(DirectPlayEnumerate(EnumNetSup,(void *) this) != DP_OK)
        {
            #ifdef _DEBUG
              afxDump << "Enumeration of Net Providers: Failed\n";
            #else
            MessageBox("Network Enumeration Failed","Fatel Error",
                    MB_OK | MB_ICONSTOP);
            #endif
            return FALSE;
        }
        #ifdef _DEBUG
            afxDump << "Enumeration of Net Providers: Successful\n";
        #endif  
return TRUE;    
}

Back to Article

Listing Two

BOOL CNetLicense::CreateUser(void){
HRESULT hr;
char    chPlayerName[255];
char    chComputerName[255];
static  int iNumber = 1;
        DWORD iBufferSize = 252;
    srand( (unsigned)time( NULL ) );
    if(!GetUserName(chPlayerName,(LPDWORD) &iBufferSize) )
    {
        #ifdef _DEBUG
            afxDump << "No user name\n";
        #endif
        return FALSE;
    }
    #ifdef _DEBUG
    afxDump << "User name: " << chPlayerName << "\n";
    #endif
    if(!GetUserName(chComputerName,(LPDWORD) &iBufferSize))
        strcpy(chComputerName,chPlayerName);
    #ifdef _DEBUG
       afxDump << "Computer name: " << chComputerName << "\n";
    #endif
    if((hr = m_lpIDC->CreatePlayer(&m_dwPlayer, chPlayerName, 
        chComputerName, &hPlayerEvent)  != DP_OK))
    {
    int wCount = 32;
    while(wCount)
        {
            m_dwPlayer = rand();
            if(m_lpIDC->CreatePlayer(&m_dwPlayer, chPlayerName, 
                chComputerName, &hPlayerEvent) == DP_OK)
            {
                wCount = 0;
                #ifdef _DEBUG
                afxDump << "New user generated successfully\n";
                #endif
                return TRUE;
            }
            wCount--;
        }
    #ifdef _DEBUG
        afxDump << "Failed to create user\n";
    #endif
    return FALSE;
    }
    #ifdef _DEBUG
        afxDump << "New user generated successfully\n";
    #endif
    return TRUE;
}

Back to Article

Listing Three

void CNetLicense::OnPaint() {
        CPaintDC dc(this); 
        if(m_bEnumNets)
        {
            m_lSessions = FALSE;
            if(EnumNetProviders())
            {
                if(EnumTCPSessions())
                    {
                    if(EnumUsers())
                        if(CreateUser())
                            _beginthread(MessageSpin,0,this);
                    }
            }
            m_bEnumNets = FALSE;
        }
}
void CNetLicense::OnClose() 
{
        #ifdef _DEBUG
            afxDump << "Closing user connection\n";
        #endif
// deletes the player, terminates the session 

m_lpIDC->DestroyPlayer(m_dwPlayer); m_lpIDC->Close(); CDialog::OnClose(); } void CNetLicense::MessageSpin(void * lpvThreadParam) { DWORD dwFlags; CNetLicense *MyNet = (CNetLicense *) lpvThreadParam; #ifdef _DEBUG afxDump << "Net Message Proc Started\n"; #endif while(TRUE) if(WaitForSingleObject(MyNet->hPlayerEvent,INFINITE) != WAIT_TIMEOUT) { #ifdef _DEBUG afxDump << "Got some Message!\n"; #endif if(GetHandleInformation(MyNet->hPlayerEvent,&dwFlags)) ProcessMessage(MyNet); else { #ifdef _DEBUG afxDump << "hPlayerEvent Handle is invalid, terminating thread\n"; #endif _endthread(); } } // This should run forever or until someone terminates the application } void CNetLicense::ProcessMessage(CNetLicense *MyNet) { DPID dpIncoming; DPID dpOutgoing; struct TrapPacket { DWORD dwType; char szBigBuffer[1024]; } tp; // something to cast an incoming message to.

DWORD dwLength = sizeof(TrapPacket);

switch(MyNet->m_lpIDC->Receive(&dpIncoming, &dpOutgoing, DPRECEIVE_ALL,(char *) &tp,&dwLength)) { case DP_OK: #ifdef _DEBUG afxDump << "Received and processed a message!\n" << "\n"; #endif

switch(tp.dwType) { case DPSYS_DELETEPLAYER: DPMSG_DELETEPLAYER *sDeletePlayer =(DPMSG_DELETEPLAYER *) &tp; if(MyNet->m_lpIDC->DestroyPlayer(sDeletePlayer->dpId) != DP_OK) { #ifdef _DEBUG afxDump << "Could not delete user\n"; #endif } else { #ifdef _DEBUG afxDump << "Deleted User!\n"; #endif } break; } break; case DPERR_BUFFERTOOSMALL: #ifdef _DEBUG afxDump << "DPERR_BUFFERTOOSMALL: " << dwLength << "\n"; #endif break; case DPERR_NOMESSAGES: break; case DPERR_INVALIDPARAMS: #ifdef _DEBUG afxDump << "DPERR_BUFFERTOOSMALL: " << dwLength << "\n"; #endif break; } }

Back to Article


Copyright © 1997, Dr. Dobb's Journal