某程 encode 算法分析
本帖最后由 Lychow 于 2025-2-11 09:52 编辑 样本与之前的魔改MD5是同一个so,就换了个方法,直接IDA梭哈
第一个传参是明文,第二个传参是请求体的长度
代码逻辑很清晰了,将明文,明文长度和输出都传进了 ctrip_enc
直接进入 ctrip_enc
### 填充模式
这里可以看到他对明文长度进行了计算,将受影响的算法提取出来
```
if ( (input_len & 0xF) != 0 )
input_len_10 = (input_len + 16) & 0xFFFFFFF0;
else
input_len_10 = input_len + 16;
// ============================== //
if ( (input_len & 0xF) != 0 )
v8 = 16 - (input_len & 0xF);
else
v8 = 16;
//===============================//
input_ = (char *)malloc(input_len_10);
memcpy(input_, input, (unsigned int)input_len);
memset(&input_, v8, (unsigned int)(v8 - 1) + 1LL);
```
如果明文长度是 16的倍数,就+16,如果不是16的倍数就+16再向下对齐到16的倍数
然后如果明文长度是16的倍数,v8赋值为16,如果不是16的倍数就v8赋值为 16 - (input_len & 0xF);
再申请长度对齐后的空间,将明文 和 v8 都 赋值过去
以上,其实他实际上就是实现标准的 AES 的 pkcs7 填充模式,没有进行魔改
而后将 填充后的明文和长度,进入方法 ctrip_enc_internal
```
v10 = ctrip_enc_internal((__int64)input_, input_len_10, &v12, 16, output);
```
v12的值上面可以看到赋值是, v12 = xmmword_112D4; 直接点过去
看着啥也不像,长度正好为 16,暂且将这个当做一个 IV 吧
### encrypt_one 生成真实key
进入方法 ctrip_enc_internal
一步步来,首先random_key,就是通过时间生成随机数,得到一个随机key
然后将随机key和上面的iv,v20 传入 encrypt_one
通过下面v20的方法调用发现 `aes_setkey_enc(CTX, v20, 0x80u);`
那么大胆猜测 v20 就是真实key,随机数+iv 通过 encrypt_one方法计算得出真实的aes key
看一下关键的 encrypt_one 方法
```cpp
void __fastcall encrypt_one(_OWORD *randomkey, _OWORD *iv, unsigned __int8 **a3)
{
//
aes_gen_tables();
v6 = (unsigned __int8 *)malloc(0x10uLL);
*a3 = v6;
v7 = v6;
*(_OWORD *)v6 = *randomkey;
v8 = (int8x8_t *)malloc(0x10uLL);
*(_OWORD *)v8->n64_u64 = *iv;
sbox = get_sbox();
v10 = v7;
v11 = v7;
v12 = v7;
v13 = *(_BYTE *)(sbox + *v7);
v14 = v7;
v15 = v7;
v16 = v7;
*v7 = v13;
LOBYTE(v10) = *(_BYTE *)(sbox + v10);
v17 = v7;
v18 = v7;
v19 = v7;
v7 = v10;
LOBYTE(v11) = *(_BYTE *)(sbox + v11);
v20 = v7;
v21 = v7;
v22 = v7;
v7 = v11;
LOBYTE(v12) = *(_BYTE *)(sbox + v12);
v27.n64_u8 = v13;
v23 = v7;
v27.n64_u8 = v10;
v7 = v12;
LOBYTE(v14) = *(_BYTE *)(sbox + v14);
v24 = v7;
v27.n64_u8 = v11;
v25 = v7;
v7 = v14;
LOBYTE(v15) = *(_BYTE *)(sbox + v15);
v27.n64_u8 = v12;
v27.n64_u8 = v14;
v26 = 2;
v7 = v15;
LOBYTE(v16) = *(_BYTE *)(sbox + v16);
v27.n64_u8 = v15;
v7 = v16;
LOBYTE(v17) = *(_BYTE *)(sbox + v17);
v27.n64_u8 = v16;
v7 = v17;
LOBYTE(v18) = *(_BYTE *)(sbox + v18);
v27.n64_u8 = v17;
v7 = v18;
LOBYTE(v19) = *(_BYTE *)(sbox + v19);
v29.n64_u8 = v18;
v7 = v19;
LOBYTE(v20) = *(_BYTE *)(sbox + v20);
v29.n64_u8 = v19;
v7 = v20;
LOBYTE(v21) = *(_BYTE *)(sbox + v21);
v29.n64_u8 = v20;
v7 = v21;
v28 = *(_BYTE *)(sbox + v22);
v29.n64_u8 = v21;
v7 = v28;
LOBYTE(v10) = *(_BYTE *)(sbox + v23);
v29.n64_u8 = v28;
v7 = v10;
LOBYTE(v11) = *(_BYTE *)(sbox + v24);
v29.n64_u8 = v10;
v7 = v11;
v29.n64_u8 = v11;
v29.n64_u8 = *(_BYTE *)(sbox + v25);
v7 = v29.n64_u8;
while ( 1 )
{
v30 = veor_s8(v29, v8).n64_u64;
*(int8x8_t *)v7 = veor_s8(v27, (int8x8_t)v8->n64_u64);
*((_QWORD *)v7 + 1) = v30;
row_rotation(v7, 4LL, 1LL);
if ( !v26 )
break;
column_rotation(v8, 4LL, 1LL);
v31 = get_sbox();
v32 = v8->n64_u8;
v27.n64_u64 = *(unsigned __int64 *)v7;
v29.n64_u64 = *(_QWORD *)(v7 + 8);
--v26;
v8->n64_u8 = *(_BYTE *)(v31 + v8->n64_u8);
v33 = *(_BYTE *)(v31 + v32);
v34 = v8->n64_u8;
v8->n64_u8 = v33;
v35 = *(_BYTE *)(v31 + v34);
v36 = v8->n64_u8;
v8->n64_u8 = v35;
v37 = *(_BYTE *)(v31 + v36);
v38 = v8->n64_u8;
v8->n64_u8 = v37;
v39 = *(_BYTE *)(v31 + v38);
v40 = v8->n64_u8;
v8->n64_u8 = v39;
v41 = *(_BYTE *)(v31 + v40);
v42 = v8->n64_u8;
v8->n64_u8 = v41;
v43 = *(_BYTE *)(v31 + v42);
v44 = v8->n64_u8;
v8->n64_u8 = v43;
v45 = *(_BYTE *)(v31 + v44);
v46 = v8.n64_u8;
v8->n64_u8 = v45;
v47 = *(_BYTE *)(v31 + v46);
v48 = v8.n64_u8;
v8.n64_u8 = v47;
v49 = *(_BYTE *)(v31 + v48);
v50 = v8.n64_u8;
v8.n64_u8 = v49;
v51 = *(_BYTE *)(v31 + v50);
v52 = v8.n64_u8;
v8.n64_u8 = v51;
v53 = *(_BYTE *)(v31 + v52);
v54 = v8.n64_u8;
v8.n64_u8 = v53;
v55 = *(_BYTE *)(v31 + v54);
v56 = v8.n64_u8;
v8.n64_u8 = v55;
v57 = *(_BYTE *)(v31 + v56);
v58 = v8.n64_u8;
v8.n64_u8 = v57;
v59 = *(_BYTE *)(v31 + v58);
v60 = v8.n64_u8;
v8.n64_u8 = v59;
v8.n64_u8 = *(_BYTE *)(v31 + v60);
}
free(v8);
}
```
代码量还是比较小的,而且没有加混淆,感觉好处理,那就一步步看
#### s盒替换
首先调用 aes_gen_tables();,但是这个方法没有入参也没有返回,就当是初始化方法,暂时先不管,遇到了再回来看
```
v7= v6 = *randomkey;
v8->n64_u64 = *iv;
sbox = get_sbox();
```
这里然后将randomkey赋值给了v7,iv赋值给了v8, 以及获取了sbox
```
char *get_fbox()
{
aes_gen_tables();
return &byte_1D090;
}
```
点过去 byte_1D090 发现没有值,那么aes_gen_tables 实际上就是为了给这个sbox赋值
unidbg 看下返回结果
对比标准的sbox
```
Sbox = (
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
。。。。。。
)
```
从头到尾,一个个对比过了,没毛病,一个字没动
后面的代码非常长,而且有点不太好看逻辑
但是如果我们细心观察,我们就可以发现,他的从v7中取值和赋值是可以 一 一对应上的
将代码整理一下:
```
v10 = v7;
LOBYTE(v10) = *(_BYTE *)(sbox + v10);
v11 = v7;
LOBYTE(v11) = *(_BYTE *)(sbox + v11);
```
不就是取值然后把取的值当成索引嘛,直接使用 java 进行还原
```
private static void take_sbox(byte[] data) {
for (int i = 0; i < data.length; i++) {
int byte_data = data & 0xFF;
data = SBOX;
}
}
```
所以,这个方法前面的那么大一串,实际上6行就能解决,实际上就是做了s盒替换
#### **行移位**+列混淆
然后就进入了一个while循环
里面其实就调用了两个方法
```
row_rotation(v7, 4LL, 1LL);
column_rotation(v8, 4LL, 1LL);
```
至于后面的一大串代码就很熟悉了,get_sbox(),v33 = *(_BYTE *)(sbox_1 + v32);
就是上面刚说过的s盒替换,不过是这里换成了 iv 去替换
现在的问题就是这个 row_rotation 和 column_rotation 到底做了啥
unidbg在 row_rotation 执行前和执行后分别打印入参的值
可以发现值的变换情况
```
执行前:
D4 3F 8B 24 1A 5E 7E 9D 24 14 7E C6 49 7F DB 9E
执行后:
3F 8B 24 D4 7E 9D 1A 5E C6 24 14 7E 49 7F DB 9E
```
注意观察不难发现,他是四个字节分组,然后第一组循环左移一位,第二组循环左移两位,第三组循环左移三位,第四组循环左移四位(等于没动)
就是标准AES的 行移位
```
D4 3F 8B 24 ---> 3F 8B 24 D4 //左移一位
1A 5E 7E 9D ---> 7E 9D 1A 5E //左移两位
24 14 7E C6 ---> C6 24 14 7E //左移三位
49 7F DB 9E ---> 49 7F DB 9E //左移四位(等于没动)
```
java 还原,懒得写逻辑,直接强行换位
ok,开始看那个列混淆
同样unidbg打断点,查看执行前后的值
```
执行前:
C0 B4 07 51 A4 A2 62 B3 30 7E 3C 81 46 C5 F2 75
执行后:
A4 7E F2 51 30 C5 07 B3 46 B4 62 81 C0 A2 3C 75
```
再次尝试找到规律
```
C0 B4 07 51 A4 7E F2 51
A4 A2 62 B3 30 C5 07 B3
30 7E 3C 81 46 B4 62 81
46 C5 F2 75 C0 A2 3C 75
```
同样,直接 java写替换代码就行,不复杂
然后执行了 --v26;,但是v26在前文中固定赋值 v26 = 2;
所以会执行两次行 行移位+类混淆, sbox替换 IV
encrypt_one 这就结束了,最后的结果就是真实的aes key
### AES 校验
前面说了,aes_setkey_enc(CTX, v20, 0x80u); 大胆猜测 v20是真实key
既然加密方法是aes_crypt_cbc,那肯定还有IV
而方法传入都很明了,只有一个v22未知,那就v22基本就是IV了
IDA点过去查看v22的值
````
xmmword_112C4 DCB 0x69, 0xD2, 0x55, 0xB8, 0x32, 0x9E, 0xAC, 0xD4, 0xC, 0x2A, 0x9C, 0x8B, 0x68, 0x75, 0x87, 5
````
通过验证,是标准AES没有魔改,Key和IV也是对的
### encrypt_two 随机key插入密文
```
encrypt_two(input, input_len_10, (__int64)&randomkey, iv_len, output);
```
input 经过前面的AES加密后,已经变成了密文,所以这里传入的实际上是密文 + 密文长度 + 随机key(非真实key)+iv_len+输出结果
encrypt_two 方法就没有啥技巧了,也没有标准方法调用,里面就纯计算
所以,老老实实还原咯
众所周知 input_len 最少都是16位的,所以直接刨去 `if ( (int)input_len < 1 )`和 `if ( input_len == 1 )`判断的部分
代码段开始的部分应该在这里
```
v10 = input_len & 0xFFFFFFFE;
v12 = 0;
v13 = 0;
v14 = input + 1;
v15 = v10;
do
{
v16 = *(v14 - 1);
v17 = *v14;
v14 += 2;
v15 -= 2LL;
v12 += v16;
v13 += v17;
}
while ( v15 );
```
v10 是 输入长度对齐到偶数长度,然后 v15=v10,并且每次循环-2直到等于0
每次循环都依次往后取两位值,分别累加在v12和v13上
v10和v15 已经减到0,所以以下的判断全部跳过
```
if ( v10 != input_len )
goto LABEL_9;
LABEL_11:
if ( ivlen < 1 )
goto LABEL_19;
LABEL_12:
if ( ivlen == 1 )
{
v21 = 0LL;
}
else
```
然后开始执行对 randomKey 的操作
```
v21 = ivlen & 0xFFFFFFFE;
v22 = 0;
v23 = (unsigned __int8 *)(randomkey + 1);
v24 = v21;
do
{
v25 = *(v23 - 1);
v26 = *v23;
v23 += 2;
v24 -= 2LL;
v11 += v25;
v22 += v26;
}
while ( v24 );
```
同理,和input的处理是一样的,每次循环都依次往后取两位值,分别累加在v11和v22上
```
*output = malloc((int)(ivlen + input_len));
result = memcpy(output, input, (int)input_len);
if ( ivlen >= 1 )
{
v32 = 0LL;
LODWORD(i) = 0;
do
{
v34 = v32 + (int)input_len;
v35 = *output;
v36 = *(_BYTE *)(randomkey + v32);
for ( i = ((int)i + v11) % (int)(v32 + input_len) + 1LL; v34 > i; *v37 = v38 )
{
v37 = (_BYTE *)(v35 + v34);
v38 = *(_BYTE *)(v35 + v34-- - 1);
}
++v32;
*(_BYTE *)(v35 + i) = v36;
}
while ( v32 != ivlen );
}
```
首先为 output 申请 ivlen + input_len 个长度的空间(这里应该是keylen)
然后将 input 赋值给 output
然后循环key_len次,遍历randomKey 赋值给 v36
```
for ( i = ((int)i + v11) % (int)(v32 + input_len) + 1LL; v34 > i; *v37 = v38 )
{
v37 = (_BYTE *)(v35 + v34);
v38 = *(_BYTE *)(v35 + v34-- - 1);
}
```
这里v37 = v38,所以逻辑应该是移位,给后面的 `*(_BYTE *)(v35 + i) = v36;`腾出位置
然后不断计算i值,将密钥插入进密文中,得到最后结果
### 总结
1. 随机数生成随机key;
2. 随机key加上iv通过运算得到真实aes_key;
3. 真实aes_key+固定IV 进行AES CBC加密生成密文;
4. 最后将随机key通过运算插入到密文中。
解密的话也很简单,从密文中抽取随机key出来,然后通过encryptOne与IV计算得到真实AES_KEY,再加上另一个固定IV通过 AES CBC解密得到明文。 感谢分享 这么流弊 感谢分享 感谢分享,新年大货 算法总是有些搞不懂,这篇文章让我学习到了很多 感谢分享{:1_893:} 对称加密把秘钥放进去吗?上不去下不来 BrutusScipio 发表于 2025-2-12 23:58
对称加密把秘钥放进去吗?上不去下不来
啥上不去下不来,放进去的不是真实参与加密的密钥,是一个随机值,把这个随机值再和IV进行运算才是真实的key
页:
[1]