ChaCha20 加密算法的实现与逆向分析

admin 2025年6月30日00:24:59评论14 views字数 19457阅读64分51秒阅读模式

 

文章作者:selph

文章来源:https://xz.aliyun.com/news/18319

ChaCha20 加密算法的实现与逆向分析

简介

ChaCha20是一种流密码算法,用于加密和解密数据流。它由Daniel J. Bernstein于2008年设计,是Salsa20的改进版,被广泛应用于网络通信、加密文件和存储介质等领域。

ChaCha20算法使用256位的密钥(32字节) 和一个12字节的随机数(称为nonce 作为输入,生成一个可变长度的伪随机比特流,然后与明文进行异或运算得到密文。ChaCha20算法的特点是快速、安全、易于实现和内存友好,适用于高速网络通信和移动设备。

ChaCha20 的解密过程与加密完全相同,因为异或操作是可逆的,类似的算法还有RC4

与RC4的区别

Chacha20和RC4都是流密码算法,但它们的加密原理有以下几个区别:

  1. 密钥长度:Chacha20的密钥长度为256位(32字节) ,而RC4的密钥长度为1到256字节不等。因此,Chacha20可以提供更强的密钥安全性,也更适合用于现代加密应用。
  1. S盒:Chacha20没有S盒,而RC4使用一个256字节的S盒来生成密钥流Chacha20是基于置换的加密算法它使用固定的置换来生成密钥流
  1. 加密过程:在Chacha20中,通过对输入的明文数据块进行置换和加密操作,生成密文输出。每次加密需要根据密钥和计数器生成一个新的置换和密钥流。而RC4是在密钥生成完毕后,直接用密钥流对输入的明文进行异或操作得到密文输出。
  1. 安全性:由于RC4存在一些安全性问题,它已经不被推荐使用。而Chacha20已经被广泛应用于TLS、SSH、IPsec等加密通信协议中,并且其安全性得到了广泛认可。

chacha20 算法流程介绍

  1. 初始化:
    • 构建一个固定的 4x4 矩阵(64字节)。
    • 这个矩阵由四部分填充:固定的128位魔数(16字节) 、256位密钥(32字节) 、96位Nonce(12字节)  和一个 32位计数器(4字节) 。
  1. 密钥流生成:
    • 将初始状态矩阵复制一份。
    • 对副本进行20轮高度混淆的轮函数操作(核心是Quarter Round,即ARX)。
    • 将混淆后的矩阵与原始矩阵相加,得到一个64字节的密钥流块。
    • 将计数器加1,重复以上过程,生成下一个密钥流块。
  1. 对明文使用密钥流块进行逐字节异或,得到这一块的密文(每块加密64字节)

具体过程看接下来的源码分析就好理解了

chacha20 算法代码实现分析

本节算法源码基于开源项目 Ginurx/chacha20-c: ChaCha20 stream cipher implemented in C 进行详细分析:

代码主要分为三个部分:

struct chacha20_context ctx;
chacha20_init_context(&ctx, key, nonce, counter);
chacha20_xor(&ctx, buffer, size_of_buffer);

chacha20_context

chacha20 算法维护一个结构体:

struct chacha20_context
{
    /**
     * @brief 存储生成的 64 字节(512 位)密钥流块。
     *
     * ChaCha20 核心函数每次生成一个 64 字节的块。这个数组以 32 位整数的形式存储它。
     * 16 * 4 字节 = 64 字节。
     */
    uint32_t keystream32[16];
    /**
     * @brief 当前在 `keystream32` 密钥流块中的位置(0-63)。
     *
     * 当 `position` 达到 64 时,意味着当前的密钥流块已用完,需要生成下一个块。
     */
    size_t position;

    /**
     * @brief 256 位(32 字节)的密钥。
     *
     * 这是加密的核心秘密,必须保密。
     */
    uint8_t key[32];
    /**
     * @brief 96 位(12 字节)的 Nonce (Number used once)。
     *
     * Nonce 对于每次加密都应该是唯一的,但不必保密。
     * 在 ChaCha20 中,它与计数器结合,确保每个块的加密都使用唯一的输入状态。
     */
    uint8_t nonce[12];
    /**
     * @brief 64 位的块计数器。
     *
     * 即使加密大量数据,这个计数器也能确保每个 64 字节块的输入状态都是独一无二的。
     * 它会从一个初始值(通常是 0 或 1)开始,每生成一个块就加一。
     */
    uint64_t counter;

    /**
     * @brief ChaCha20 的 4x4 内部状态矩阵(以一维数组表示)。
     *
     * 这是 ChaCha20 算法的核心。它由常量、密钥、计数器和 Nonce 初始化。
     * 算法通过对这个状态矩阵进行多轮混淆操作来生成密钥流。
     * 16 * 4 字节 = 64 字节。
     */
    uint32_t state[16];
};

结构体中保存加密过程需要的信息,包括counter,nonce,key,position,密钥流,初始状态state

  • counter:8字节,计算加密块数,每一次加密64字节后就会+1,默认初始是0
  • nonce:12字节,会放入初始状态state,state中和计数器有4字节是重合的(感觉像是用来凑数的
  • key:32字节,密钥
  • position:记录当前加密用的索引,0-63,到64了说明该计算新的密钥流了
  • 密钥流keystream:64字节,每个块加密都会生成一个,用于和明文异或进行加密
  • 初始状态state:64字节,初始状态,包括16字节魔数,32字节密钥,4字节计数器,12字节nonce

这里最核心的部分就是初始状态state,每轮运算都是基于初始状态进行的

chacha20_init_context


/**
 * @brief 对外暴露的接口:初始化一个完整的ChaCha20上下文。
 *
 * @param ctx 指向要被初始化的chacha20_context结构体。
 * @param key 256位(32字节)密钥。
 * @param nonce 96位(12字节)Nonce。
 * @param counter 64位计数器的初始值。
 */
void chacha20_init_context(struct chacha20_context *ctx, uint8_t key[], uint8_t nonce[], uint64_t counter)
{
    // 将上下文结构体清零,这是一个良好的编程习惯,可以避免未初始化的值。
    memset(ctx, 0, sizeof(struct chacha20_context));

    // 初始化状态矩阵(常量、密钥、Nonce)
    chacha20_init_block(ctx, key, nonce);
    // 设置初始的64位块计数器
    chacha20_block_set_counter(ctx, counter);

    // 保存用户提供的初始计数器值
    ctx->counter = counter;
    // 将密钥流位置设置为64,这将强制在第一次调用chacha20_xor时立即生成第一个密钥流块。
    ctx->position = 64;
}

初始化过程中,初始化state和设置计数器counter和位置position的值:

/**
 * @brief 初始化ChaCha20的状态矩阵。
 *
 * 这是根据RFC 8439规范设置初始64字节(512位)状态的地方。
 * 状态是一个4x4的32位整数矩阵,这里用一个长度为16的一维数组表示。
 * 
 * 状态矩阵布局如下:
 *  state[0..3]: 常量 "expand 32-byte k"
 *  state[4..11]: 256位密钥
 *  state[12]: 块计数器 (低32位)
 *  state[13]: 块计数器 (高32位) + Nonce (低32位) - 在此实现中,高32位计数器与Nonce的最低4字节合并
 *  state[14..15]: 96位Nonce
 *
 * @param ctx 指向chacha20_context的指针,其state成员将被初始化。
 * @param key 256位(32字节)的密钥。
 * @param nonce 96位(12字节)的Nonce。
 */
static void chacha20_init_block(struct chacha20_context *ctx, uint8_t key[], uint8_t nonce[])
{
    // 保存密钥和Nonce到上下文中,以便在需要时重置计数器(例如在chacha20_block_set_counter中)
    memcpy(ctx->key, key, sizeof(ctx->key));
    memcpy(ctx->nonce, nonce, sizeof(ctx->nonce));

    // ChaCha20规范中定义的16字节(128位)常量,字符串 "expand 32-byte k"
    const uint8_t *magic_constant = (uint8_t*)"expand 32-byte k";
    ctx->state[0] = pack4(magic_constant + 0 * 4);
    ctx->state[1] = pack4(magic_constant + 1 * 4);
    ctx->state[2] = pack4(magic_constant + 2 * 4);
    ctx->state[3] = pack4(magic_constant + 3 * 4);
    // 密钥被分为8个4字节的块,填充到state[4]到state[11]
    ctx->state[4] = pack4(key + 0 * 4);
    ctx->state[5] = pack4(key + 1 * 4);
    ctx->state[6] = pack4(key + 2 * 4);
    ctx->state[7] = pack4(key + 3 * 4);
    ctx->state[8] = pack4(key + 4 * 4);
    ctx->state[9] = pack4(key + 5 * 4);
    ctx->state[10] = pack4(key + 6 * 4);
    ctx->state[11] = pack4(key + 7 * 4);
    // 状态的最后四个32位字用于计数器和Nonce
    // state[12] 和 state[13] 用于64位计数器。这里先初始化为0。
    // chacha20_block_set_counter会设置它们的正确值。
    ctx->state[12] = 0;
    // state[13]到state[15]用于12字节(96位)的Nonce。
    ctx->state[13] = pack4(nonce + 0 * 4);
    ctx->state[14] = pack4(nonce + 1 * 4);
    ctx->state[15] = pack4(nonce + 2 * 4);
}

这里使用pack4函数进行打包:

/**
 * @brief 将一个4字节的数组(uint8_t a[])打包成一个32位无符号整数(uint32_t)。
 *
 * ChaCha20算法内部操作的是32位的字(word),但输入(如密钥、Nonce)通常是字节流。
 * 这个函数将4个字节按照小端序(Little-Endian)的方式组合成一个32位整数。
 * 小端序意味着最低有效字节存储在最低的内存地址。
 *
 * @param a 指向一个包含4个字节的数组的指针。
 * @return 组装好的32位无符号整数。
 */
static uint32_t pack4(const uint8_t *a)
{
    uint32_t res = 0;
    res |= (uint32_t)a[0] << 0 * 8; // 第1个字节放在最低8位
    res |= (uint32_t)a[1] << 1 * 8; // 第2个字节放在次低8位
    res |= (uint32_t)a[2] << 2 * 8; // 第3个字节放在次高8位
    res |= (uint32_t)a[3] << 3 * 8; // 第4个字节放在最高8位
    return res;
}

就是将密钥字符串按照小端序进行组装

关于状态state,通过16字节魔数,32字节密钥,4字节计数器,12字节nonce的顺序拼接到64字节的state中,计数器的设置在另一个函数里

/**
 * @brief 在状态矩阵中设置64位的块计数器。
 * 
 * IETF版本的ChaCha20(RFC 8439)将state[12]用作块计数器。
 * 原版DJB的ChaCha20将state[12]和state[13]作为计数器。
 * 此实现似乎遵循IETF版本,但将64位计数器拆分到state[12]和state[13]的一部分。
 * 
 * @param ctx 指向chacha20_context的指针。
 * @param counter 64位的计数值。
 */
static void chacha20_block_set_counter(struct chacha20_context *ctx, uint64_t counter)
{
    // state[12] 存储计数器的低32位。
    ctx->state[12] = (uint32_t)counter;
    // state[13] 存储计数器的高32位。
    // 注意:这里将计数器的高位加到了原始的Nonce部分上。
    // RFC 8439 中 state[13] 是 Nonce 的一部分。
    // 这里的实现方式是将64位计数器的高32位加到state[13]上,这是一种变体。
    // 标准的IETF ChaCha20 (RFC8439)中state[12]是32位计数器,state[13-15]是96位nonce。
    // 这个实现将64位计数器放到了state[12]和state[13]中,这意味着nonce只有64位被直接使用。
    // 但由于初始化时已经将nonce加载到了state[13]中,所以这里是`pack4(ctx->nonce) + ...`
    ctx->state[13] = pack4(ctx->nonce + 0 * 4) + (uint32_t)(counter >> 32);
}

计数器的设置,其中低4字节保存在state的规定位置中

高4字节和nonce的前4字节进行加法运算,重叠在一起

最后就是将position设置为64,以便第一次加密的时候直接初始化密钥流keystream

chacha20_xor

这里是生成密钥流和加密的过程,是chacha20算法的核心

/**
 * @brief 使用生成的密钥流对字节序列进行异或操作(加密/解密)。
 *
 * @param ctx 指向已经初始化的ChaCha20上下文。
 * @param bytes 指向需要加密或解密的数据。数据会被原地修改。
 * @param n_bytes 数据的长度(字节)。
 */
void chacha20_xor(struct chacha20_context *ctx, uint8_t *bytes, size_t n_bytes)
{
    // 将32位的密钥流数组转换为8位的字节数组指针,以便按字节进行异或操作。
    // 第一次进入这里的时候,keystream32是空的,所以会调用chacha20_block_next生成第一个密钥流块。
    uint8_t *keystream8 = (uint8_t*)ctx->keystream32;
    for (size_t i = 0; i < n_bytes; i++) 
    {
        // 检查当前的64字节密钥流块是否已经用完。
        if (ctx->position >= 64) 
        {
            // 如果用完,就调用块函数生成下一个密钥流块。
            chacha20_block_next(ctx);
            // 同时重置位置计数器。
            ctx->position = 0;
        }
        // 从密钥流中取出一个字节,与输入数据的一个字节进行异或。
        bytes[i] ^= keystream8[ctx->position];
        // 移动到密钥流的下一个字节。
        ctx->position++;
    }
}

这里初始情况下,keystream还是0,没有赋值,初始化的时候将position设置为了64,所以这里会直接进入chacha20_block_next函数去生成密钥流keystream

当完成生成之后,keystream是64字节的数组,逐字节和明文进行异或

最核心的计算在chacha20_block_next中:

/**
 * @brief 生成下一个64字节的ChaCha20密钥流块。
 *
 * 这是ChaCha20算法的核心,也称为"块函数"或"ChaCha20 Block Function"。
 * 它接收当前的状态矩阵(包含常量、密钥、计数器、Nonce),
 * 并通过20轮的混淆操作,生成一个64字节的伪随机密钥流块。
 */
static void chacha20_block_next(struct chacha20_context *ctx) {
    // "This is where the crazy voodoo magic happens."
    // 这句话很形象。这里的操作通过高度非线性的混合,使得从输出(密钥流)
    // 反推输入(密钥等)在计算上不可行。

    // 1. 将当前状态矩阵复制一份到 keystream32,作为工作副本。
    // 这是个4*4的矩阵,16个元素,每个元素是32位整数。
    for (int i = 0; i < 16; i++) ctx->keystream32[i] = ctx->state[i];

    // 2. 执行10次双轮(总共20轮)的混淆操作。
    // 每轮都包含对状态矩阵的列和对角线进行四分之一轮操作。
    for (int i = 0; i < 10; i++) 
    {
        // 列轮(Column Rounds)
        // 对4x4矩阵的每一列应用四分之一轮
        //  0  1  2  3
        //  4  5  6  7
        //  8  9 10 11
        // 12 13 14 15
        chacha20_quarterround(ctx->keystream32, 0, 4, 8, 12); // 第1列
        chacha20_quarterround(ctx->keystream32, 1, 5, 9, 13); // 第2列
        chacha20_quarterround(ctx->keystream32, 2, 6, 10, 14); // 第3列
        chacha20_quarterround(ctx->keystream32, 3, 7, 11, 15); // 第4列

        // 对角线轮(Diagonal Rounds)
        // 对4x4矩阵的对角线应用四分之一轮
        chacha20_quarterround(ctx->keystream32, 0, 5, 10, 15); // 主对角线
        chacha20_quarterround(ctx->keystream32, 1, 6, 11, 12); // 副对角线1
        chacha20_quarterround(ctx->keystream32, 2, 7, 8, 13);  // 副对角线2
        chacha20_quarterround(ctx->keystream32, 3, 4, 9, 14);  // 副对角线3
    }

    // 4. 将混淆后的工作状态与原始状态逐元素相加,生成最终的密钥流块。
    // 这可以防止通过反转混淆操作来破解算法。
    for (int i = 0; i < 16; i++) ctx->keystream32[i] += ctx->state[i];

    // 5. 增加块计数器,为生成下一个块做准备。
    uint32_t *counter = ctx->state + 12; // 指向计数器的低32位
    counter[0]++; // 64位计数器的低32位加1
    if (0 == counter[0]) 
    {
        // 如果低32位溢出(变为0),则高32位需要进位。
        counter[1]++;
        // 检查高32位是否也溢出。
        // 如果发生,意味着已经加密了 2^64 * 64 字节的数据,这是一个极大的数字。
        // 此时必须停止,因为继续下去会导致计数器回绕,
        // 从而重复使用密钥流,这是灾难性的安全漏洞。
        assert(0 != counter[1]);
    }
}

第一步,将初始状态state的64字节,复制到keystream中,按照4字节一个,组成4*4的矩阵

第二步,10次双轮变换(20轮变换)

第三步,将新得到的4*4矩阵和初始矩阵相加,计数器+=1

这里的核心是10次双轮变换这里,矩阵的成员标号如下:

        //  0  1  2  3
        //  4  5  6  7
        //  8  9 10 11
        // 12 13 14 15

第一轮对列进行处理,也就是:

        chacha20_quarterround(ctx->keystream32, 0, 4, 8, 12); // 第1列
        chacha20_quarterround(ctx->keystream32, 1, 5, 9, 13); // 第2列
        chacha20_quarterround(ctx->keystream32, 2, 6, 10, 14); // 第3列
        chacha20_quarterround(ctx->keystream32, 3, 7, 11, 15); // 第4列

第二轮对对角线进行处理:

        chacha20_quarterround(ctx->keystream32, 0, 5, 10, 15); // 主对角线
        chacha20_quarterround(ctx->keystream32, 1, 6, 11, 12); // 副对角线1
        chacha20_quarterround(ctx->keystream32, 2, 7, 8, 13);  // 副对角线2
        chacha20_quarterround(ctx->keystream32, 3, 4, 9, 14);  // 副对角线3

具体是哪几个成员,根据函数参数和上面的标号去匹配就行

处理函数chacha20_quarterround:

/**
 * @brief 对一个32位无符号整数执行循环左移操作 (rotate left)。
 *
 * 循环移位是密码学中一个重要的操作,用于实现扩散(diffusion),
 * 即输入的一个位的变化会影响到输出的多个位。
 * 与普通移位不同,循环移位不会丢失任何位信息,移出的位会从另一端补回来。
 *
 * @param x 要进行移位的整数。
 * @param n 要移位的位数。
 * @return 循环左移后的结果。
 */
static uint32_t rotl32(uint32_t x, int n) 
{
    // (x << n) 是左移n位,右边补0。
    // (x >> (32 - n)) 是右移32-n位,左边补0。对于循环左移n位,这恰好是把最左边的n位移到最右边。
    // 两者通过或运算(|)合并,即完成了循环左移。
    return (x << n) | (x >> (32 - n));
}

/**
 * @brief ChaCha20的"四分之一轮"操作(Quarter Round)。
 *
 * 这是ChaCha20混淆过程的基本构建块。它接收4个32位整数(通过状态数组的索引指定),并对它们进行混合。
 * 操作是所谓的 ARX(Add-Rotate-XOR)结构, 它速度快且具有良好的密码学扩散属性。
 * @param x 指向工作状态数组的指针。
 * @param a 状态数组中的索引 a。
 * @param b 状态数组中的索引 b。
 * @param c 状态数组中的索引 c。
 * @param d 状态数组中的索引 d。
 */
static inline void chacha20_quarterround(uint32_t *x, int a, int b, int c, int d)
{
    // 特征:会出现连续加法+循环右移的组合。
    x[a] += x[b]; x[d] = rotl32(x[d] ^ x[a], 16);
    x[c] += x[d]; x[b] = rotl32(x[b] ^ x[c], 12);
    // 类似之前的操作,但是循环右移位数发生变化
    x[a] += x[b]; x[d] = rotl32(x[d] ^ x[a], 8);
    x[c] += x[d]; x[b] = rotl32(x[b] ^ x[c], 7);
}

这里会有特征:会有大量的加法-异或-右移位组合(具体内容看代码注释吧)

在逆向中,因为编译器优化的问题,可能会导致这些函数都被内联了,10次双轮变换,将8次四分之一轮变化放在一起了,那就会出现8*4=32次右移特征

到此,chacha20的加密过程分析完毕,接下来看看逆向静态看见的chacha20

chacha20 逆向识别 & 示例题目求解

示例题目:HackTheBox Challenge reverse - CryptOfTheUndead

chacha20的逆向识别

先回到今天的主题,chacha20算法的逆向分析,使用该题目先进行chacha20部分的分析,后面再介绍题目和求解

程序中使用了一个函数encrypt_buf,这里使用了chacha20进行加密操作(这里是我重命名后的反编译结果)

 encrypt_buf(buf, n[0], "BRAAAAAAAAAAAAAAAAAAAAAAAAAINS!!");// buf是用户输入,n[0]是输入的长度,第三个参数疑似key
unsigned __int64 __fastcall encrypt_buf(_BYTE *buf, __int64 length, const __int8 *key)
{
  __m128i ctx[12]; // [rsp+0h] [rbp-F8h] BYREF
  __int8 nonce[12]; // [rsp+CCh] [rbp-2Ch] BYREF
  unsigned __int64 v6; // [rsp+D8h] [rbp-20h]

  v6 = __readfsqword(0x28u);
  memset(nonce, 0, sizeof(nonce));
  chacha20_init_context((__int64)ctx, (const __m128i *)key, (__int64)nonce, 0LL);
  chacha20_xor(ctx, buf, length);
  return v6 - __readfsqword(0x28u);
}

首先是初始化缓冲区,然后进入第一个函数,init:

__int64 __fastcall chacha20_init_context(__int64 ctx, const __int8 *key, __int8 *nonce, __int64 counter)
{
  __int64 v5; // rdi
  __m128i si128; // xmm0
  int v8; // ecx
  int v9; // edx
  __int64 result; // rax

  *(_QWORD *)ctx = 0LL;                         // 初始化内存部分
  v5 = ctx + 8;
  *(_QWORD *)(v5 + 176) = 0LL;
  memset(
    (void *)(v5 & 0xFFFFFFFFFFFFFFF8LL),
    0,
    8 * ((unsigned __int64)((unsigned int)ctx - (v5 & 0xFFFFFFF8) + 192) >> 3));
  *(__m128i *)(ctx + 72) = _mm_loadu_si128((const __m128i *)key);// 结构体保存密钥,32字节
  *(__m128i *)(ctx + 88) = _mm_loadu_si128((const __m128i *)key + 1);
  si128 = _mm_load_si128((const __m128i *)&xmmword_2130);// 6B20657479622D323320646E61707865
                                                // 
  *(_QWORD *)(ctx + 104) = *(_QWORD *)nonce;
  v8 = *((_DWORD *)nonce + 2);                  // 
                                                // 从ctx+128开始的64字节,4*4矩阵,是初始状态
                                                // constant 16字节
                                                // key 32字节
                                                // counter 4字节
                                                // nonce 12字节
  *(__m128i *)(ctx + 128) = si128;
  *(_DWORD *)(ctx + 112) = v8;
  *(_DWORD *)(ctx + 144) = *(_DWORD *)key;
  *(_DWORD *)(ctx + 148) = *((_DWORD *)key + 1);
  *(_DWORD *)(ctx + 152) = *((_DWORD *)key + 2);
  *(_DWORD *)(ctx + 156) = *((_DWORD *)key + 3);
  *(_DWORD *)(ctx + 160) = *((_DWORD *)key + 4);
  *(_DWORD *)(ctx + 164) = *((_DWORD *)key + 5);
  *(_DWORD *)(ctx + 168) = *((_DWORD *)key + 6);
  *(_DWORD *)(ctx + 172) = *((_DWORD *)key + 7);
  *(_DWORD *)(ctx + 180) = *(_DWORD *)nonce;
  *(_DWORD *)(ctx + 184) = *((_DWORD *)nonce + 1);
  *(_DWORD *)(ctx + 188) = *((_DWORD *)nonce + 2);
  *(_QWORD *)(ctx + 104) = *(_QWORD *)nonce;
  v9 = *((_DWORD *)nonce + 2);
  result = (unsigned int)(*(_DWORD *)(ctx + 104) + HIDWORD(counter));// counter高位和nonce叠加了
  *(_DWORD *)(ctx + 176) = counter;             // counter
  *(_DWORD *)(ctx + 112) = v9;
  *(_DWORD *)(ctx + 180) = result;              // 修改叠加后的nonce部分
  *(_QWORD *)(ctx + 120) = counter;             // 保存计数器
  *(_QWORD *)(ctx + 64) = 64LL;                 // 密钥流位置,设置为64,让第一次调用xor的时候立即生成第一个密钥流块
  return result;
}

看到大量的变量名+偏移的赋值,说明此处可能是个结构体

然后可以看到,这里初始化了结构体内存之后,将一个16字节的固定数据保存到了ctx+128开始的地方

然后是将key赋值在ctx+144开始的地方

然后紧接着是counter和nonce,以及counter和nonce重叠部分相加运算

设置一个值为64,就是之前分析的position

逆向看这个有点不好看,但是重命名变量之后,看到这个排列方式(16字节魔数,32字节密钥,4字节计数器,12字节nonce),就是chacha20的初始化特征

然后看下一个函数,太长了分段看:

void __fastcall chacha20_xor(__int64 ctx, _BYTE *buf, __int64 length)
{

...

  if ( length )
  {
    ctx_1 = ctx;
    position = *(_QWORD *)(ctx + 64);           // counter
    buf_ptr_end = &buf[length];
    buf_ptr = buf;
    ctx_4 = (const __m128i *)(ctx + 64);        // counter+nonce的16字节
    do
    {
      if ( position <= 63 )                     // 循环里有个64的判断,每64次计数一循环
      {
        ctx_5 = ctx_1 + position;
      }
      else                                      // 否则就开始生成密钥流
      {                                         // 载入64字节初始状态,矩阵每个值取出来用于接下来运算
        // 太长了,暂时略,后面单独介绍
      }                                         // 异或加密,计数器++
      *buf_ptr++ ^= *(_BYTE *)ctx_5;
      position = *(_QWORD *)(ctx_1 + 64) + 1LL;
      *(_QWORD *)(ctx_1 + 64) = position;
    }
    while ( buf_ptr != buf_ptr_end );
  }
}

整体的框架如上,首先是一个判断63,小于等于63,就异或一个值(buf_ptr)和明文(ctx_5),然后position++

else的部分太长了,继续分段看:

      else                                      // 否则就开始生成密钥流
      {                                         // 载入64字节初始状态,矩阵每个值取出来用于接下来运算
        v1 = _mm_loadu_si128((const __m128i *)(ctx_1 + 128));
        n10 = 10;                               // 循环计次
        ctx_2 = ctx_1;
        *(__m128i *)ctx_1 = v1;
        a0 = _mm_cvtsi128_si32(v1);             // a0
        a1 = *(_DWORD *)(ctx_1 + 4);            // a1
        a2 = *(_DWORD *)(ctx_1 + 8);            // a2
        v10 = _mm_loadu_si128((const __m128i *)(ctx_1 + 144));
        v11 = a0;
        a3 = *(_DWORD *)(ctx_1 + 12);           // a3
        *(__m128i *)(ctx_1 + 16) = v10;
        a4 = _mm_cvtsi128_si32(v10);            // a4
        a5 = *(_DWORD *)(ctx_1 + 20);           // a5
        a7_1 = *(_DWORD *)(ctx_1 + 28);         // a7
        v16 = _mm_loadu_si128((const __m128i *)(ctx_1 + 160));
        *(__m128i *)(ctx_1 + 32) = v16;
        a8_1 = _mm_cvtsi128_si32(v16);          // a8
        a9_1 = *(_DWORD *)(ctx_1 + 36);         // a9
        v19 = _mm_loadu_si128((const __m128i *)(ctx_1 + 176));
        a10_1 = *(_DWORD *)(ctx_1 + 40);        // a10
        a11_1 = *(_DWORD *)(ctx_1 + 44);        // a11
        *(__m128i *)(ctx_1 + 48) = v19;
        a15 = *(_DWORD *)(ctx_1 + 60);          // a15
        a12_1 = _mm_cvtsi128_si32(v19);         // a12
        a13_1 = *(_DWORD *)(ctx_1 + 52);        // a13
        a14 = *(_DWORD *)(ctx_1 + 56);          // a14
        a6 = *(_DWORD *)(ctx_1 + 24);           // a6

首先是一系列赋值,分析可以看到,是从ctx_1赋值16个4字节给16个变量

这里的ctx_1的值来自v1,v1来自ctx+128,也就是之前初始化阶段分析得到的state的偏移

接下来是一个10次的循环:

        do
        {                                       // 大量的加法和循环右移组合,四分之一轮运算,总共双轮10次
                                                // 会出现32次右移
                                                // 最后计算的结果是生成新的4*4矩阵值
          v2 = a4 + v11;
          v27 = a5 + a1;
          v28 = a6 + a2;
          v29 = __ROL4__(v2 ^ a12_1, 16);
          v30 = __ROL4__(v27 ^ a13_1, 16);
          v31 = v29 + a8_1;
          v32 = v30 + a9_1;
          v33 = __ROL4__(v28 ^ a14, 16);
          v34 = v33 + a10_1;
          v35 = __ROL4__(v31 ^ a4, 12);
          v36 = __ROL4__(v32 ^ a5, 12);
          v37 = v35 + v2;
          v38 = v36 + v27;
          v39 = __ROL4__(v34 ^ a6, 12);
          v40 = v39 + v28;
          v41 = __ROL4__(v37 ^ v29, 8);
          v42 = __ROL4__(v38 ^ v30, 8);
          v43 = v42 + v32;
          v79 = v41 + v31;
          v44 = __ROL4__(v43 ^ v36, 7);
          v45 = __ROL4__(v40 ^ v33, 8);
          v46 = a7_1 + a3;
          v82 = __ROL4__((v41 + v31) ^ v35, 7);
          v47 = v44 + v37;
          v48 = v45 + v34;
          v49 = __ROL4__(v46 ^ a15, 16);
          v50 = __ROL4__(v48 ^ v39, 7);
          v51 = v50 + v38;
          v52 = v49 + a11_1;
          v53 = __ROL4__((v49 + a11_1) ^ a7_1, 12);
          v54 = __ROL4__(v51 ^ v41, 16);
          v55 = v53 + v46;
          v56 = __ROL4__(v55 ^ v49, 8);
          v57 = v56 + v52;
          v58 = __ROL4__(v47 ^ v56, 16);
          v59 = v57 ^ v53;
          v60 = v54 + v57;
          v61 = v58 + v48;
          v62 = __ROL4__(v59, 7);
          v63 = __ROL4__(v61 ^ v44, 12);
          v11 = v63 + v47;
          a15 = __ROL4__(v11 ^ v58, 8);
          a10_1 = a15 + v61;
          a5 = __ROL4__(a10_1 ^ v63, 7);
          v64 = v62 + v40;
          v65 = __ROL4__(v60 ^ v50, 12);
          a1 = v65 + v51;
          v66 = __ROL4__(v64 ^ v42, 16);
          a12_1 = __ROL4__(a1 ^ v54, 8);
          v67 = (v60 + a12_1) ^ v65;
          a11_1 = v60 + a12_1;
          v68 = v66 + v79;
          a6 = __ROL4__(v67, 7);
          v69 = v82 + v55;
          v70 = __ROL4__((v66 + v79) ^ v62, 12);
          v71 = __ROL4__(v69 ^ v45, 16);
          a2 = v70 + v64;
          v72 = v71 + v43;
          a13_1 = __ROL4__(a2 ^ v66, 8);
          v73 = __ROL4__(v72 ^ v82, 12);
          a8_1 = a13_1 + v68;
          a3 = v73 + v69;
          a7_1 = __ROL4__(a8_1 ^ v70, 7);
          a14 = __ROL4__(a3 ^ v71, 8);
          a9_1 = a14 + v72;
          a4 = __ROL4__(a9_1 ^ v73, 7);
          --n10;
        }                                       // 循环运算完成之后
                                                // 将最终结果赋值到矩阵里
        while ( n10 );

这里就是编译器优化的结果了,将每一次四分之一轮运算都内联到一起了,明显的特征,32个加-异或-右移位的组合,进行10轮循环

接着看:

        a13 = a13_1;
        v74 = a6;
        ctx_1 = ctx_2;
        *(_DWORD *)(ctx_2 + 48) = a12_1;
        *(_DWORD *)ctx_2 = v11;
        *(_DWORD *)(ctx_2 + 44) = a11_1;
        *(_DWORD *)(ctx_2 + 60) = a15;
        *(_DWORD *)(ctx_2 + 40) = a10_1;
        *(_DWORD *)(ctx_2 + 20) = a5;
        *(_DWORD *)(ctx_2 + 4) = a1;
        *(_DWORD *)(ctx_2 + 24) = v74;
        *(_DWORD *)(ctx_2 + 8) = a2;
        *(_DWORD *)(ctx_2 + 32) = a8_1;
        *(_DWORD *)(ctx_2 + 28) = a7_1;
        *(_DWORD *)(ctx_2 + 12) = a3;
        *(_DWORD *)(ctx_2 + 36) = a9_1;
        *(_DWORD *)(ctx_2 + 16) = a4;
        *(_DWORD *)(ctx_2 + 56) = a14;
        *(_DWORD *)(ctx_2 + 52) = a13;
        ctx_3 = (const __m128i *)ctx_2;
        do
        {                                       // 最后进行一个对矩阵的值的加法
          v76 = _mm_loadu_si128(ctx_3);
          v77 = _mm_loadu_si128(ctx_3 + 8);
          ++ctx_3;
          ctx_3[-1] = _mm_add_epi32(v77, v76);
        }
        while ( ctx_4 != ctx_3 );
        v78 = (*(_DWORD *)(ctx_2 + 176))++ == -1;
        if ( v78 )                              // 如果计数器过大,造成整数溢出,异常退出
        {
          v78 = (*(_DWORD *)(ctx_2 + 180))++ == -1;
          if ( v78 )
            __assert_fail("0 != counter[1]", "chacha.c", 0x50u, "chacha20_block_next");
        }
        *(_QWORD *)(ctx_2 + 64) = 0LL;
        ctx_5 = ctx_2;
      }                                         // 异或加密,计数器++

然后就是将10次2轮计算的结果保存到ctx_2,这里的异常处理是防止counter溢出

然后和原本的ctx_3进行加法,设置position=0

和之前正向代码的特征匹配,逆向看着就是更复杂了

示例题目分析&求解

题目是一个elf文件加密器和一个加密的文件,让我们求得原文

main函数:

int __fastcall main(int argc, const char **argv, const char **envp)
{
  const char *filename; // rbp
  size_t size; // r12
  char *pfilename; // rbx
  size_t n_1; // r13
  void *buf_1; // r12
  int n2; // ebp
  int fd; // eax
  int fd_1; // ebx
  const char *crypt; // rax
  void *buf; // [rsp+8h] [rbp-40h] BYREF
  size_t n[7]; // [rsp+10h] [rbp-38h] BYREF

  n[1] = __readfsqword(0x28u);
  if ( argc <= 1 )
  {
    crypt = "crypt";                            // 命令行参数需要是crypt
    if ( argc == 1 )
      crypt = *argv;
    n2 = 1;
    printf("Usage: %s file_to_encryptn", crypt);
    return n2;
  }
  filename = argv[1];
  if ( (unsigned int)ends_with(filename, ".undead", envp) )// 如果是.undead结尾的文件,则退出
  {
    n2 = 2;
    puts("error: that which is undead may not be encrypted");
    return n2;
  }
  size = strlen(filename) + 9;
  pfilename = (char *)malloc(size);
  strncpy(pfilename, filename, size);
  *(_QWORD *)&pfilename[strlen(pfilename)] = 'daednu.';// 拼接新的文件名
  if ( !(unsigned int)read_file(filename, n, &buf) )// 读取文件到buf
  {
    n_1 = n[0];                                 // 长度
    buf_1 = buf;
    encrypt_buf(buf, n[0], "BRAAAAAAAAAAAAAAAAAAAAAAAAAINS!!");// 加密
    n2 = rename(filename, pfilename);
    if ( n2 )
    {
      n2 = 4;
      perror("error renaming file");
    }
    else
    {
      fd = open(pfilename, 513);
      fd_1 = fd;
      if ( fd < 0 )
      {
        n2 = 5;
        perror("error opening new file");
      }
      else
      {
        write(fd, buf_1, n_1);
        close(fd_1);
        puts("successfully zombified your file!");
      }
    }
    return n2;
  }
  return main_cold();
}

不难猜出,flag是被加密了的,需要进行解密,这里加密使用了chacha20算法

那么根据异或可逆,chacha20相同key生成相同的keystream

那么只需要对加密的文件再次加密,就可以得到明文:

rev_crypt_of_the_undead ➤ cp flag.txt.undead 1.txt

rev_crypt_of_the_undead ➤ ./crypt 1.txt
successfully zombified your file!

rev_crypt_of_the_undead ➤ cat 1.txt.undead
HTB{und01ng_th3_curs3_0f_und34th}

总结:chacha20 逆向题目解法

和RC4一样,都是通过key生成一个东西(rc4是s-box,chacha20是keystream),然后基于这个东西对明文进行异或

对于程序使用相同的key,那么对密文加密就可以得到明文

参考资料

  • [0] chacha20加密算法笔记 - 知乎
  • [1] Ginurx/chacha20-c: ChaCha20 stream cipher implemented in C

原文始发于微信公众号(神农Sec):ChaCha20 加密算法的实现与逆向分析

 

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年6月30日00:24:59
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   ChaCha20 加密算法的实现与逆向分析http://cn-sec.com/archives/4209343.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息