分组密码-AES

xiaoeryu Lv5

本章主要是学习AES如何实现,如何对java中的AES方法进行hook,以及定位so文件中的AES

这里以AES-128为例,会对明文分组进行10轮迭代运算,加密的第1轮到第9轮的函数一样,包括4个操作:字节替换、行位移、列混合和轮密钥加。最后一轮迭代不执行行列混合。另外,在第一轮迭代之前,先将明文和原始密钥进行一次异或加密操作。

AES加解密理论

AES实现的原理图

AES的加解密,跟DES一样由左右两部分组成

左边是对明文(16字节)进行的一个10轮的迭代处理

第一轮到第9轮每一轮都有四个操作:

AddRoundKey 为 异或
SubBytes 为 查表替换
ShiftRows 为 按字节循环左移
MixColumns 为 矩阵乘法

右边是对密钥(16字节)进行编排,生成每一轮对应的子密钥。

查表替换(字节替换、字节代换)

查表替换的主要功能是通过S盒完成一个字节到另一个字节的映射。本质上就是一个查表操作。有两个表一个是S盒一个是S盒逆。S盒用于提高密码算法的混淆性,S盒是快速判断AES加密算法的最有效的特征。

AES算法流程分析

这里介绍的是AES-128,也就是密钥的长度为128位,加密轮数为10轮

AES的加密公式为**C = E(K,P)**,在加密函数E中,会执行一个函数,并且执行10次这个轮函数,这个轮函数的前9次执行的操作是一样的,只有第10次有所不同。也就是说,一个明文分组会被加密10轮。AES的核心就是实现一轮中的所有操作。

明文矩阵填充

AES的处理单位是字节,128位的输入明文分组P和输入密钥K都被分成16个字节,分别记为P = P0 P1 … P15和 K = K0 K1 … K15。如,明文分组为P = abcdefghijklmnop,其中的字符a对应P0,p对应P15。一般地,明文分组用字节为单位的正方形矩阵描述,称为状态矩阵。在算法的每一轮中,状态矩阵的内容不断发生变化,最后的结果作为密文输出。该矩阵中字节的排列顺序为从上到下、从左至右依次排列。

状态矩阵
密钥矩阵填充

类似地,128位密钥也是用字节为单位的矩阵表示,矩阵的每一列被称为1个32位比特字。通过密钥编排函数该密钥矩阵被扩展成一个44个字组成的序列W[0],W[1], … ,W[43],该序列的前四个元素W[0],W[1],W[2],W[3]是原始密钥,用于加密运算中的初始密钥(下面介绍);后面40个字分为10组,每组4个字(4x4x8 = 128bit)分别用于10轮加密运算中的轮密钥加。后面的10组密钥通过初始密钥进行密钥扩展得到。

密钥矩阵
加解密流程

AES加解密主要要以下几步操作,完整的加解密流程是通过循环以下步骤来完成的,具体如下图表示

  • 轮密钥加
  • 字节替换
  • 行位移
  • 列混合
加解密流程
加解密步骤分析

字节替换

AES的字节替换其实就是通过一个简单的查表操作来进行非线性运算。AES定义了一个S盒和一个逆S盒。分别用来加解密操作。

S盒

状态矩阵中的元素按照下面的方式映射为一个新的字节:把该字节的高4位作为行值,低4位作为列值,取出S盒或者逆S盒中对应的行的元素作为输出。例如,加密时输出的字节S1=0X12,则查找S盒的第0X01行和0X02列,得到值0XC9,然后替换S1原有的值0X12为0XC9。状态矩阵经字节代换后的图如下

字节变换

逆字节代换也就是查逆S盒来变换,逆S盒如下

逆S盒
行移位

行移位是一个简单的左循环移位操作。当密钥长度为128bit时,状态矩阵的第0行左移0字节,第1行左移1字节,第2行左移2字节。。。如下图所示

行移位

行移位的逆变换是将状态矩阵中的每一行执行相反的移位操作,例如AES-128中,状态矩阵的第0行右移0字节,第1行右移1字节,第2行右移2字节。。。

列混合

列混合变换是通过矩阵相乘来实现的,经行移位后的状态矩阵与固定的矩阵相乘,得到混淆后的状态矩阵,如下图的公式所示

列混合

状态矩阵中的第j列(0 ≤j≤3)的列混合可以表示为下图所示

其中,矩阵运算的乘法和加法都是定义在基于GF(2^8)上的二元运算,并不是通常意义上的乘法和加法。

逆向列混合变换同样可由下图的矩阵乘法定义

逆列混合
轮密钥加

轮密钥加是将128位轮密钥Ki同状态矩阵中的数据进行逐位异或操作,如下图所示。其中,密钥Ki中每个字W[4i],W[4i+1],W[4i+2],W[4i+3]为32位bit字,包含4个字节,轮密钥加过程可以看成是字逐位异或的结果,也可以看成字节级别或者位级别的操作。也就是说,可以看成S0、S1、S2、S3组成的32位字与W[4i]的异或运算。

轮密钥加
密钥扩展

AES首先将初始密钥输入到一个4×4的状态矩阵中,如下图所示

密钥扩展

4×4矩阵的每一列的4个字节组成一个字,矩阵4列的4个字依次命名为W[0]、W[1]、W[2]、W[3],它们构成一个以字为单位的数组W。

接着,对W数组扩充40个新列,构成总共44列的扩展密钥数组。新列一如下的递归方式产生

  1. 如果i不是4的倍数,那么i列由如下等式确定:

    $W[i]=W[i-4]⨁W[i-1]$

  2. 如果i是4的倍数,那么第i列由如下等式确定:

    $W[i]=W[i-4]⨁T(W[i-1])$

T是一个有点复杂的函数,由3部分组成:自循环、字节代换和轮常量异或,这3部分的作用分别如下

  1. 字循环:将1个字中的4个字节循环左移一个字节。即将输入字[b0, b1, b2, b3]变换成[b1,b2,b3,b0]
  2. 字节代换:对字循环的结果使用S盒进行字节代换
  3. 轮常量异或:将前两步的结果同轮常量Rcon[j]进行异或,其中j表示轮数

轮常量Rcon[j]是一个字,其值见下表

j 1 2 3 4 5
Rcon[j] 01 00 00 00 02 00 00 00 04 00 00 00 08 00 00 00 10 00 00 00
j 6 7 8 9 10
Rcon[j] 20 00 00 00 40 00 00 00 80 00 00 00 1B 00 00 00 36 00 00 00
轮常量Rcon[j]

总的来说,密钥扩展的复杂性是确保算法安全性的重要部分。当分组长度和密钥长度都是128bit时,AES的加密算法共迭代10轮,每一轮最后要与一个子密钥进行轮密钥加,再加上最开始的一次轮密钥加,即一个分组共需要扩展为11个128bit的轮密钥。AES的密钥扩展算法是以字节为一个基本单位(一个字为4个字节),刚好是密钥矩阵的一列。因此4个字(128bit)密钥需要扩展成11个子密钥,共44个字。

AES hook

对java层的AES进行hook

编写hook脚本通过参数以及返回值获取到加解密前后以及key的值

package com.xiaoeryu.aes;

import android.os.Build;

import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;

public class AES {
    public static String encrypt(String content, String sKey) throws UnsupportedEncodingException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
        if (sKey == null) {
            System.out.print("Key is null");
            return null;
        }
        // Determine the key length
        if (sKey.length() != 16) {
            System.out.print("Key length is not 16");
            return null;
        }
        byte[] raw = sKey.getBytes("utf-8");
        SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); // "算法/模式/补码方式"
        cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
        byte[] encrypted = cipher.doFinal(content.getBytes("utf-8"));
        String result = null;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            result = Base64.getEncoder().encodeToString(encrypted);
        } else {
            result = android.util.Base64.encodeToString(encrypted, android.util.Base64.DEFAULT);
        }
        return result;
    }
    public static String decrypt(String content, String sKey) throws UnsupportedEncodingException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
        if (sKey == null) {
            System.out.print("Key is null");
            return null;
        }
        if  (sKey.length() != 16) {
            System.out.print("Key length is not 16");
            return null;
        }
        byte[] raw = sKey.getBytes("utf-8");
        SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, skeySpec);
        byte[] encrypted = android.util.Base64.decode(content, android.util.Base64.DEFAULT);
        byte[] original = cipher.doFinal(encrypted);
        String originalString = new String(original, "utf-8");
        return originalString;
    }
}
  • 对java层的AES进行hook跟之前hook DES使用的方法基本都是一样的,区别只是在于它们实现的时候调用的方法不同而已。所以相同的代码更改一下hook的函数和重载就可以了
定位SO中的AES算法

跟之前一样,使用findcrypt插件直接就可以根据S盒定位到算法的位置,如果定位不到可能是因为插件中没有S盒的特征跟上一章同样的方法添加进去即可。

  • 通过交叉引用定位到调用S盒的函数
  • 跟进去分析

跟据汇编代码可知,这里是根据在获取S盒的值

附件:

定位SO中AES算法的测试demo

  • 标题: 分组密码-AES
  • 作者: xiaoeryu
  • 创建于 : 2024-02-29 11:05:35
  • 更新于 : 2024-03-01 18:01:29
  • 链接: https://github.com/xiaoeryu/2024/02/29/分组密码-AES/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
此页目录
分组密码-AES