文章来源:https://xz.aliyun.com/news/18319
简介
ChaCha20是一种流密码算法,用于加密和解密数据流。它由Daniel J. Bernstein于2008年设计,是Salsa20的改进版,被广泛应用于网络通信、加密文件和存储介质等领域。
ChaCha20算法使用256位的密钥(32字节) 和一个12字节的随机数(称为nonce) 作为输入,生成一个可变长度的伪随机比特流,然后与明文进行异或运算得到密文。ChaCha20算法的特点是快速、安全、易于实现和内存友好,适用于高速网络通信和移动设备。
ChaCha20 的解密过程与加密完全相同,因为异或操作是可逆的,类似的算法还有RC4
与RC4的区别
Chacha20和RC4都是流密码算法,但它们的加密原理有以下几个区别:
- 密钥长度:Chacha20的密钥长度为256位(32字节) ,而RC4的密钥长度为1到256字节不等。因此,Chacha20可以提供更强的密钥安全性,也更适合用于现代加密应用。
- 加密过程:在Chacha20中,通过对输入的明文数据块进行置换和加密操作,生成密文输出。每次加密需要根据密钥和计数器生成一个新的置换和密钥流。而RC4是在密钥生成完毕后,直接用密钥流对输入的明文进行异或操作得到密文输出。
- 安全性:由于RC4存在一些安全性问题,它已经不被推荐使用。而Chacha20已经被广泛应用于TLS、SSH、IPsec等加密通信协议中,并且其安全性得到了广泛认可。
chacha20 算法流程介绍
- 初始化:
-
- 构建一个固定的 4x4 矩阵(64字节)。
-
- 这个矩阵由四部分填充:固定的128位魔数(16字节) 、256位密钥(32字节) 、96位Nonce(12字节) 和一个 32位计数器(4字节) 。
- 密钥流生成:
-
- 将初始状态矩阵复制一份。
-
- 对副本进行20轮高度混淆的轮函数操作(核心是Quarter Round,即ARX)。
-
- 将混淆后的矩阵与原始矩阵相加,得到一个64字节的密钥流块。
-
- 将计数器加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 加密算法的实现与逆向分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论