最近在学习Js逆向时发现AES涉及的知识点比较多,编写对应的Py脚本时涉及的细节也比较多,就导致了会出现各种各样的小bug,所以心血来潮写一篇文章进行一个系统的总结(学艺不精,如有错误,欢迎各位师傅指正)

一、AES简介

img

AES是最为常见的对称加密算法(对称加密就是加密与解密使用的秘钥是一个),大致的加密流程如下

img

  • 明文P:等待加密的数据

  • 秘钥K:用来加密明文的密码,在对称加密算法中,加密与解密的密钥是相同的。密钥为接收方与发送方协商产生,但不可以直接在网络上传输,否则会导致密钥泄漏,例如我们在进行Js逆向时,得到了该秘钥就可以尝试解密网站中的加密数据

  • AES加密函数:设 AES 加密函数为 E,则 C = E(K, P),其中 P 为明文,K 为密钥,C 为密文。也就是说,把明文 P 和密钥 K 作为加密函数的参数输入,则加密函数 E 会输出密文 C

  • 密文 C:经过 AES 加密后的数据

  • AES 解密函数:设 AES 解密函数为 D,则 P = D(K, C),其中 C 为密文,K 为密钥,P 为明文。也就是说,把密文 C 和密钥 K 作为解密函数的参数输入,则解密函数会输出明文 P

实现 AES 有几种模式,主要有 ECB、CBC、CFB 和 OFB 这几种。本章主要介绍最常用的 ECBCBC 模式。

由于我们主要是研究Js逆向中的AES加密,只需要知晓其基本概念和用法即可,详细的加密逻辑暂时不用深究

二、加密模式_ECB

img

ECB是AES几种加密模式中最简单的加密模式,简单来说就是将明文切分为若干个组分别加密,实际上有很明显的弱点,就是相同的明文会得到同样的密文。因为每个分组加密方式和密钥都相同,若分组明文相同,加密后密文也相同

这里给出我自己写的利用Python3实现AES_ECB模式的加解密完整demo,每一步都会在注释中进行详细的解释,首先是AES_ECB加密代码,如下

def encrypt(key, text):
    # 确保密钥长度是16
    key = key[:16]  # 截断16位

    # 因为AES加密算法处理的是字节数据,所以需要将密钥转化为bytes类型
    text_bytes = text.encode('utf-8')
    # 由于AES加密要求输入数据的长度必须是块大小的整数倍(AES的块大小是16字节)
    # 使用pad函数来填充数据,以确保其长度是16的倍数
    padded_text = pad(text_bytes, AES.block_size)

    # 初始化AES加密器(使用ECB模式)
    aes = AES.new(key, AES.MODE_ECB)

    # 加密
    encrypt_aes = aes.encrypt(padded_text)
    # 注意:加密后的数据是字节串,可能包含无法直接以文本形式存储或传输的字符
    # 形如b'\x036\x19\xe1\x12\x15\xa7\x9bH\x16!'

    # 加密后的字节串编码为Base64格式的字节串
    encrypted_text = base64.b64encode(encrypt_aes)

    # 然后.decode('utf-8')将字节串转换为字符串
    # 形如b'AzYZxk2RHr7uq6cN8SVA/hOmMGx+Nfc54RIVp5tIFiE='
    encrypted_text = encrypted_text.decode('utf-8')
    # 形如AzYZxk2RHr7uq6cN8SVA/hOmMGx+Nfc54RIVp5tIFiE=

    return encrypted_text

AES_ECB模式只需要秘钥key即可对明文进行加密,所以这里传入key(必须是16位)和要加密的明文text,并且aes的加解密函数,形如

encrypt_aes = aes.encrypt(padded_text)

处理的都是字节串,也就是padded_text必须是个字节串,这里是加密函数,解密函数也是一样,所以在调用该数据进行加解密前,可以通过

# 将text字符串转为字节串
text_bytes = text.encode('utf-8')
# 相反的,下面这样就是将字节串转为字符串
text = text_bytes.decode('utf-8')

字节串和字符串看下面两个就已经可以区分了

# 字符串
text = 'AzYZxk2RHr7uq6cN8SVA/hOmMGx+Nfc54RIVp5tIFiE='
# 字节串
# aes加解密函数,处理的就是字节串
text = b'AzYZxk2RHr7uq6cN8SVA/hOmMGx+Nfc54RIVp5tIFiE='

注意调用加密函数得到的结果是个字节串,不利用阅读传输和存储,所通过是将接加密后得到的字节串进行base64编码后再转为字符串,从而得到最终的密文

然后是AES_ECB的解密代码,由于AES是对称加密,所以解密简单来说就是倒走一遍加密流程,代码如下

def decrypt(key, text):

    # 初始化加密器
    aes = AES.new(key[:16], AES.MODE_ECB)

    # 注意:无论什么格式的数据,base64解码后得到的一定是字节串!!!
    # 如果是加密,则编码后的数据类型和编码前相同
    # 所以这里对字符串进行base64解码,得到字节串,中间不需要主动进行字符串转字节串的操作
    # 解密之后形如b'\x036\x19\xe1\x12\x15\xa7\x9bH\x16!'的字节串
    base64_decrypted = base64.b64decode(text)

    # 调用AES解密函数解密字节串
    # 解密之后形如b'Hello, AES Encryption!\n\n\n\n\n\n\n\n\n\n'的字节串
    decrypted_padded = aes.decrypt(base64_decrypted)

    # 去除填充部分,然后将字节串改为字符串
    decrypted_text = unpad(decrypted_padded, AES.block_size).decode('utf-8')
    # 最后得到完整明文

    return decrypted_text

这个代码也很容易理解,但有两个需要注意的地方,一个是传入的密文text是字符串,但是此处不需要先转化为字节串再base64解码。而是直接进行base64解码,原因是任何类型的数据,在进行base64解码之后,得到的结果都是字节串,可以看下面一个小demo

import base64

# 假设这是一个Base64编码的字符串
encoded_str = "SGVsbG8gV29ybGQ="

# 解码Base64字符串
decoded_bytes = base64.b64decode(encoded_str)

# 打印解码后的字节串(可以看到字符串解码之后的结果,是字节串)
print(decoded_bytes)  # 输出: b'Hello World'

# 如果你想要得到一个字符串而不是字节串,你可能需要解码字节串
# 注意:这取决于原始数据的编码,这里假设是UTF-8
decoded_str = decoded_bytes.decode('utf-8')
print(decoded_str)  # 输出: Hello World

所以此处在调用aes解码函数前,不需要手动进行字符串转字节串,然后再去对字节串进行base64解码(因为base64解码的结果会强制转换五位字节串),简单来说就是下面这样

# Py中base64解码时
decoded_bytes = base64.b64decode(encoded_str)
# 入参encoded_str可以是字符串或者字节串,但解码的结果decoded_bytes一定是一个字节串

# Py中base64编码时
encoded_str = base64.b64encode(str)
# 入参str一定是一个字节串,并且编码结果encoded_str也一定是一个字节串

第二个需要注意的地方就是去除填充部分,这个部分需要跟加密函数中的填充代码进行比对,从而理解填充这个操作

最后给出一个完整的AES_ECB模式的加解密以及示例demo

import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

# 加密函数


def encrypt(key, text):
    # 确保密钥长度是16
    key = key[:16]  # 截断16位

    # 因为AES加密算法处理的是字节数据,所以需要将密钥转化为bytes类型
    text_bytes = text.encode('utf-8')
    # 由于AES加密要求输入数据的长度必须是块大小的整数倍(AES的块大小是16字节)
    # 使用pad函数来填充数据,以确保其长度是16的倍数
    padded_text = pad(text_bytes, AES.block_size)

    # 初始化AES加密器(使用ECB模式)
    aes = AES.new(key, AES.MODE_ECB)

    # 加密
    encrypt_aes = aes.encrypt(padded_text)
    # 注意:加密后的数据是字节串,可能包含无法直接以文本形式存储或传输的字符
    # 形如b'\x036\x19\xe1\x12\x15\xa7\x9bH\x16!'

    # 加密后的字节串编码为Base64格式的字节串
    encrypted_text = base64.b64encode(encrypt_aes)

    # 然后.decode('utf-8')将字节串转换为字符串
    # 形如b'AzYZxk2RHr7uq6cN8SVA/hOmMGx+Nfc54RIVp5tIFiE='
    encrypted_text = encrypted_text.decode('utf-8')
    # 形如AzYZxk2RHr7uq6cN8SVA/hOmMGx+Nfc54RIVp5tIFiE=

    return encrypted_text


def decrypt(key, text):

    # 初始化加密器
    aes = AES.new(key[:16], AES.MODE_ECB)

    # 注意:无论什么格式的数据,base64解码后得到的一定是字节串!!!
    # 如果是加密,则编码后的数据类型和编码前相同
    # 所以这里对字符串进行base64解码,得到字节串,中间不需要主动进行字符串转字节串的操作
    # 解密之后形如b'\x036\x19\xe1\x12\x15\xa7\x9bH\x16!'的字节串
    base64_decrypted = base64.b64decode(text)

    # 调用AES解密函数解密字节串
    # 解密之后形如b'Hello, AES Encryption!\n\n\n\n\n\n\n\n\n\n'的字节串
    decrypted_padded = aes.decrypt(base64_decrypted)

    # 去除填充部分,然后将字节串改为字符串
    decrypted_text = unpad(decrypted_padded, AES.block_size).decode('utf-8')
    # 最后得到完整明文

    return decrypted_text


# 示例使用
key = b'thisisakey123456'
text = 'Hello, AES Encryption!'
encrypted = encrypt(key, text)
print("Encrypted:", encrypted)
decrypted = decrypt(key, encrypted)
print("Decrypted:", decrypted)

运行之后结果如下

img

成功实现了对明文的加密和解密,感兴趣的师傅可以复制代码自己下去试试,结合代码的注释,很快就能理解了

三、加密模式_CBC

img

AES_CBC加密模式算是AES几个模式中使用的最多的,因为它的安全性比ECB模式高了很多,在这种方法中,每个密文块都依赖于它前面的所有密文块。同时,为了保证每条消息的唯一性,在第一个块中需要使用初始化向量iv

AES_CBC模式的加解密,在代码中相比于AES_ECB模式多了一个iv(初始化向量)需要处理,所以这里直接给出AES_CBC的完整加解密Demo,如下

import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from Crypto.Random import get_random_bytes

# 加密函数


def encrypt(key, iv, text):
    key = key[:16]

    # 转换明文字符串为为字节串
    text_bytes = text.encode('utf-8')
    # 填充
    padded_text = pad(text_bytes, AES.block_size)

    # 初始化AES加密器(使用CBC模式,需要传入iv)
    aes = AES.new(key, AES.MODE_CBC, iv)

    # 加密
    encrypt_aes = aes.encrypt(padded_text)

    # 加密后的字节串编码为Base64格式的字节串
    encrypted_text = base64.b64encode(encrypt_aes)

    # 转换为字符串
    encrypted_text = encrypted_text.decode('utf-8')

    return encrypted_text

# 解密函数


def decrypt(key, iv, text):
    # 解码Base64
    base64_decrypted = base64.b64decode(text)

    # 初始化AES解密器(使用CBC模式)
    aes = AES.new(key[:16], AES.MODE_CBC, iv)

    # 解密
    decrypted_padded = aes.decrypt(base64_decrypted)

    # 去除填充部分
    decrypted_text = unpad(decrypted_padded, AES.block_size).decode('utf-8')

    return decrypted_text


# 秘钥
key = b'thisisakey123456'

# 初始化向量长度必须与块大小相同(AES为16字节)
# 初始化向量
iv = b'1234567812345678'
text = 'Hello, AES Encryption with CBC Mode!'

# 加密
encrypted = encrypt(key, iv, text)
print("Encrypted:", encrypted)

# 解密时需要使用相同的IV
decrypted = decrypt(key, iv, encrypted)
print("Decrypted:", decrypted)

大致流程与AES_ECB相同,只不过在初始化加密器和解密器时,带上了一个初始化向量iv,这个iv也必须与AES块大小相同,即16字节,运行效果如下(如果key和iv固定,那相同明文得到的密文也固定)

img

在实际逆向中,通常寻找目标的key与iv来对站点的加密数据进行解密,如下

img

四、总结

img

除了这两种常见的模式之外,AES还有CFB,OFB等模式,但这两种是Js逆向中最常见的,所以这两种是必须要理解的,其他模式等遇到的时候再去了解就行,今天的文章就到这里啦,有任何的意见和建议也欢迎各位师傅交流!!