Password Algorithms: Google Talk

Introduction

Lots of people that use Gmail use Google Talk
It functions pretty much like any other IM application such as those available from MSN, Yahoo, AOL

OPSWAT reported in June 2011 that Google Talk accounts for 3.56% of market share in Instant Messaging.

That’s not a big percentage of users but the algorithm described here is probably used in other google applications.

Storage

If the user chooses to ‘Remember Password’ an account entry will be created in NTUSER.DAT
So for example if the google id is joe.bloggs@gmail.com the registry entry would be

HKEY_CURRENT_USER\Software\Google\Google Talk\Accounts\joe.bloggs@gmail.com

Password values are stored under a string value called pw
Without showing string for my own password, it looks something like

/'')-%0"',$)-"&)#0.'#&#($+#"

The value itself is much much longer but I just want to give you an idea of what to look for.
It doesn’t use any conventional encoding algorithm like Base64.

Generation

After some digging around in the binaries, Gtalk first initializes some entropy using a static key, the domain and username.

DWORD gtalk_entropy[4];
DWORD static_key[4] = { 0x69F31EA3, 0x1FD96207, 0x7D35E91E, 0x487DD24F };

/**
 *
 * Retrieve the current domainusername from thread or process token
 *
 * return TRUE if successful
 *
 */
BOOL GetUserInfo(std::wstring &domain, std::wstring &username) {
    HANDLE hToken;
    DWORD dwTokenSize = 0, dwUserName = 64, dwDomain = 64;
    WCHAR UserName[64], Domain[64];
    SID_NAME_USE peUse;
    PSID pSid = NULL;
    BOOL bResult = FALSE;

    OpenThreadToken(GetCurrentThread(), TOKEN_QUERY, TRUE, &hToken);

    if (GetLastError() == ERROR_NO_TOKEN) {
      if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) {
        return FALSE;
      }
    }

    if (!GetTokenInformation(hToken, TokenUser, 0, 0, &dwTokenSize)) {
      if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) {
        pSid = new BYTE[dwTokenSize];
        if (pSid != NULL) {
          if (GetTokenInformation(hToken, TokenUser, pSid, dwTokenSize, &dwTokenSize)) {
            bResult = LookupAccountSid(NULL, reinterpret_cast(pSid)->User.Sid,
                UserName, &dwUserName, Domain, &dwDomain, &peUse);
            if (bResult) {
              domain = Domain;
              username = UserName;
            }
          }
          delete []pSid;
        }
      }
    }
    return bResult;
}

All the above code does is retrieve from the process/thread security token the domain and username.

The seed value appears to be derived from a random number generator developed
by R.Park and K.Miller, however this is merely speculation as no generator exists in the binary itself.
48271 and 69621 are common multiplier values for RNG.

/**
 *
 * initialize the entropy that will used to encrypt/decrypt passwords
 *
 */
BOOL init_entropy() {
    std::wstring domain, username;

    BOOL bResult = GetUserInfo(domain, username);

    if (bResult) {
      memcpy(gtalk_entropy, static_key, sizeof(static_key));

      long M = 2147483647; // modulus
      long A = 48271;      // multiplier
      long Q = M / A;
      long R = M % A;
      long seed = 387822687;   // this could be wrong but does work
                               // and is exactly 9 digits

      seed = A * (seed % Q) - R * (seed / Q);
      seed += M;

      long idx = 0;

      // mix with username
      for (std::wstring::size_type i = 0;i < username.length();i++) {
        gtalk_entropy[idx++ % 4] ^= username[i] * seed;
        seed *= A;
      }

      // mix with domain
      for (std::wstring::size_type i = 0;i < domain.length();i++) {
        gtalk_entropy[idx++ % 4] ^= domain[i] * seed;
        seed *= A;
      }
    }
    return bResult;
}

Recovery

Once the entropy is initialized, it’s possible to decode the pw value into binary which is just a DPAPI blob.

Instead of the usual Base64 encoding, Google Talk uses Base16 with a custom alphabet and a multiplier value.

// convert base16 string into binary
void gtalk_decode(BYTE blob[], std::wstring input) {
    std::wstring alphabet = L"!"#$%&'()*+,-./0";
    long seed = gtalk_entropy[0] | 1;
    long A = 69621;
    PBYTE p = blob;

    for (size_t i = 4;i < input.length();i += 2) {
      int c;

      c  = (alphabet.find_first_of(input.at(i + 0))) << 4;
      c |= (alphabet.find_first_of(input.at(i + 1))) & 0x0f;

      *p++ = c - (seed & 0xff);
      seed *= A;
    }
}

Finally, CryptUnprotectData() is used to decrypt the UNICODE password.

// decrypt blob using DPAPI
BOOL gtalk_decrypt(BYTE password[], BYTE blob_data[], size_t blob_size) {
    DATA_BLOB DataIn, DataEntropy, DataOut;

    DataEntropy.cbData = sizeof(gtalk_entropy);
    DataEntropy.pbData = (BYTE*)gtalk_entropy;

    DataIn.cbData = blob_size;
    DataIn.pbData = blob_data;

    BOOL bResult = CryptUnprotectData(&DataIn, NULL, &DataEntropy, NULL, NULL, 1, &DataOut);

    if (bResult) {
      memcpy(password, DataOut.pbData, DataOut.cbData);
      password[DataOut.cbData] = 0;
      LocalFree(DataOut.pbData);
    }
    return bResult;
}

Conclusion

The password is protected reasonably well although the entropy generation is a little redundant and I dare say a classic example of Security through Obscurity :)
CryptProtectData and Base64 would provide the same level of protection.