RC4

精力一直放在Web和一些奇奇怪怪的东西上(比如 rust web应用()),突然发现好久没做逆向题了,知识点都忘记了很多,甚至IDA都不会用了(其实本来就不怎么会用())

所以重新学一下逆向的一些知识,顺便进行一些梳理和整合。

RC4算法

RC4加密算法是一种对称加密算法。

什么是对称加密算法?

对称加密(也叫私钥加密)指加密和解密使用相同密钥的加密算法。有时又叫传统密码算法,就是加密密钥能够从解密密钥中推算出来,同时解密密钥也可以从加密密钥中推算出来。而在大多数的对称算法中,加密密钥和解密密钥是相同的,所以也称这种加密算法为秘密密钥算法或单密钥算法。它要求发送方和接收方在安全通信之前,商定一个密钥。对称算法的安全性依赖于密钥,泄漏密钥就意味着任何人都可以对他们发送或接收的消息解密,所以密钥的保密性对通信的安全性至关重要。

RC4算法实现过程

RC4算法包括初始化算法(KSA)和随即子密码生成算法(PRGA)两部分。

KSA

我们先来看看算法中的初始化部分

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
c
/*初始化函数*/
void rc4_init(unsigned char*s,unsigned char*key, unsigned long Len)
{
    int i=0,j=0;
    char k[256]={0};
    unsigned char tmp=0;
    for(i=0;i<256;i++) {
        s[i]=i;
        k[i]=key[i%Len];
    }
    for(i=0;i<256;i++) {
        j=(j+s[i]+k[i])%256;
        tmp=s[i];
        s[i]=s[j];//交换s[i]和s[j]
        s[j]=tmp;
    }
}

参数1是一个256长度的char型数组,定义为: unsigned char sBox[256];

参数2是密钥,其内容可以随便定义:char key[256];

参数3是密钥的长度,Len = strlen(key);

初始化的过程是先有一个sbox(s盒),s盒的长度是256,然后有一个随意长度的密钥,一个中间的临时变量(tmp),在初始化的过程中,先定义一个 i ,将 i 从0开始到255以此遍历填充进s盒中;然后定义一个 j ,使用j=(j+s[i]+k[i])%256,得到 j 是几;然后 i 和 j 作为s盒的索引,进行随机化处理。因此,

1
2
3
4
j=(j+s[i]+k[i])%256;
        tmp=s[i];
        s[i]=s[j];//交换s[i]和s[j]
        s[j]=tmp;

这一部分是根据密钥来打乱s盒,i 确保S-box的每个元素都得到处理,j 保证S-box的搅乱是随机的。

PRGA

接下来我们来看看随即子密码生成算法,即加解密的过程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
c
/*加解密*/
void rc4_crypt(unsigned char*s,unsigned char*Data,unsigned long Len)
{
    int i=0,j=0,t=0;
    unsigned long k=0;
    unsigned char tmp;
    for(k=0;k<Len;k++)
    {
        i=(i+1)%256;
        j=(j+s[i])%256;
        tmp=s[i];
        s[i]=s[j];//交换s[x]和s[y]
        s[j]=tmp;
        t=(s[i]+s[j])%256;
        Data[k]^=s[t];
    }
}

参数1是上边rc4_init函数中,被搅乱的S-box;

参数2是需要加密的数据data;

参数3是data的长度.

我们看到我们现在填入了要加密的内容,来分析一下加密的过程。

首先是在KSA中被打乱的s盒,这时候我们就不再需要密钥了,我们继续对s盒中的元素进行处理。

通过对s盒中的元素的索引 i 和 j 进行处理:

1
2
3
4
5
        i=(i+1)%256;
        j=(j+s[i])%256;
        tmp=s[i];
        s[i]=s[j];//交换s[x]和s[y]
        s[j]=tmp;

这一部分,我们使 i 加一,得到新的 i 然后 j 等于 j 加上新的 i 索引的数字,然后 i 和 j 定位了新的位置,然后通过t=(s[i]+s[j])%256;,来得到密钥流,最后使用密钥流和要加密的数据进行异或得到密文。

即每收到一个字节,就进行while循环。通过一定的算法定位S盒中的一个元素,并与输入字节异或,得到k。循环中还改变了S盒。如果输入的是明文,输出的就是密文;如果输入的是密文,输出的就是明文。

我们来看看完整的程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
c
//程序开始
#include<stdio.h>
#include<string.h>
typedef unsigned longULONG;

/*初始化函数*/
void rc4_init(unsigned char*s, unsigned char*key, unsigned long Len)
{
    int i = 0, j = 0;
    char k[256] = { 0 };
    unsigned char tmp = 0;
    for (i = 0; i<256; i++)
    {
        s[i] = i;
        k[i] = key[i%Len];
    }
    for (i = 0; i<256; i++)
    {
        j = (j + s[i] + k[i]) % 256;
        tmp = s[i];
        s[i] = s[j];//交换s[i]和s[j]
        s[j] = tmp;
    }
}

/*加解密*/
void rc4_crypt(unsigned char*s, unsigned char*Data, unsigned long Len)
{
    int i = 0, j = 0, t = 0;
    unsigned long k = 0;
    unsigned char tmp;
    for (k = 0; k<Len; k++)
    {
        i = (i + 1) % 256;
        j = (j + s[i]) % 256;
        tmp = s[i];
        s[i] = s[j];//交换s[x]和s[y]
        s[j] = tmp;
        t = (s[i] + s[j]) % 256;
        Data[k] ^= s[t];
    }
}

int main()
{
    unsigned char s[256] = { 0 }, s2[256] = { 0 };//S-box
    char key[256] = { "justfortest" };
    char pData[512] = "这是一个用来加密的数据Data";
    unsigned long len = strlen(pData);
    int i;

    printf("pData=%s\n", pData);
    printf("key=%s,length=%d\n\n", key, strlen(key));
    rc4_init(s, (unsigned char*)key, strlen(key));//已经完成了初始化
    printf("完成对S[i]的初始化,如下:\n\n");
    for (i = 0; i<256; i++)
    {
        printf("%02X", s[i]);
        if (i && (i + 1) % 16 == 0)putchar('\n');
    }
    printf("\n\n");
    for (i = 0; i<256; i++)//用s2[i]暂时保留经过初始化的s[i],很重要的!!!
    {
        s2[i] = s[i];
    }
    printf("已经初始化,现在加密:\n\n");
    rc4_crypt(s, (unsigned char*)pData, len);//加密
    printf("pData=%s\n\n", pData);
    printf("已经加密,现在解密:\n\n");
    //rc4_init(s,(unsignedchar*)key,strlen(key));//初始化密钥
    rc4_crypt(s2, (unsigned char*)pData, len);//解密
    printf("pData=%s\n\n", pData);
    return 0;
}

//程序完

这时候有人就要问了,你写这么绕,这么模糊谁能看得懂啊?

所以我们来举几个例子帮助理解:

加密流程详解

一、数学实现

  1. 初始化S盒(Key Scheduling Algorithm, KSA)
  • 初始状态:S盒是一个长度为256的数组(S[0], S[1], …, S[255]),初始时按顺序填充0到255:

    1
    2
    
    plaintext
    S[0] = 0, S[1] = 1, ..., S[255] = 255
    
  • 密钥预处理:如果密钥长度不足256字节,循环重复密钥直到填满256字节。例如密钥是KEY(3字节),则扩展为K E Y K E Y K E Y ...

  • 洗牌算法

    1
    2
    3
    4
    5
    
    python
    j = 0
    for i in 0到255:
        j = (j + S[i] + 密钥[i % 密钥长度]) % 256  # 用密钥决定交换位置
        交换 S[i] 和 S[j]
    

    关键点:密钥的每个字节都会影响洗牌过程,但前几个字节的影响可能被后续步骤覆盖。

  1. 生成密钥流(Pseudo-Random Generation Algorithm, PRGA)
  • 初始化指针:i = 0, j = 0

  • 循环生成每个密钥字节

    1
    2
    3
    4
    5
    6
    
    python
    i = (i + 1) % 256          # i每次向前移动1步
    j = (j + S[i]) % 256       # j根据S[i]的值跳跃
    交换 S[i] 和 S[j]          # 动态打乱S盒
    t = (S[i] + S[j]) % 256    # 计算索引t
    密钥字节 = S[t]            # 输出密钥流的一个字节
    

    关键点:每生成一个密钥字节,S盒的状态都会改变,密钥流依赖于动态变化的S盒。

二、举个具体例子

示例1:初始化S盒

假设密钥是[0x01, 0x02, 0x03](3字节):

  1. 初始化S盒

    1
    2
    
    plaintext
    S = [0,1,2,3, ..., 255]
    
  2. 洗牌过程

    (前3步演示):

    • i=0

      • j = (0 + S[0] + Key[0]) % 256 = (0 + 0 + 1) % 256 = 1
      • 交换 S[0] 和 S[1] → S变为 [1,0,2,3, ..., 255]
    • i=1

      • j = (1 + S[1] + Key[1]) % 256 = (1 + 0 + 2) % 256 = 3
      • 交换 S[1] 和 S[3] → S变为 [1,3,2,0, ..., 255]
    • i=2

      • j = (3 + S[2] + Key[2]) % 256 = (3 + 2 + 3) % 256 = 8
      • 交换 S[2] 和 S[8] → S[8]的值被换到位置2。
    • …(持续到i=255)

示例2:生成密钥流

假设初始化后的S盒为 [3, 1, 4, 0, 2, 5, ...](简化举例):

  • 第1个密钥字节

    • i = (0 + 1) = 1
    • j = (0 + S[1]) = 0 + 1 = 1
    • 交换 S[1] 和 S[1](无变化)
    • t = (S[1] + S[1]) = 1 + 1 = 2
    • 密钥字节 = S[2] = 4 → 密钥流第一个字节是4
  • 第2个密钥字节

    • i = 2
    • j = (1 + S[2]) = 1 + 4 = 5
    • 交换 S[2] 和 S[5] → S[2]=5, S[5]=4
    • t = (5 + 4) = 9 → 密钥字节 = S[9]

例题

接下来我们看看RC4算法在CTF中的具体实现

[BabyAlgorithm](https://buuoj.cn/challenges#[第五章 CTF之RE章]BabyAlgorithm)

导入IDA,查看main函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
c
__int64 __fastcall main(int a1, char **a2, char **a3)
{
__int64 result; // rax
int i; // [rsp+Ch] [rbp-E4h]
char v5[16]; // [rsp+10h] [rbp-E0h] BYREF
char s[64]; // [rsp+20h] [rbp-D0h] BYREF
char v7[64]; // [rsp+60h] [rbp-90h] BYREF
char v8[72]; // [rsp+A0h] [rbp-50h] BYREF
unsigned __int64 v9; // [rsp+E8h] [rbp-8h]

v9 = __readfsqword(0x28u);
memset(v8, 0, 0x40uLL);
v8[0] = 0xC6;
v8[1] = 0x21;
v8[2] = 0xCA;
v8[3] = 0xBF;
v8[4] = 0x51;
v8[5] = 0x43;
v8[6] = 0x37;
v8[7] = 0x31;
v8[8] = 0x75;
v8[9] = 0xE4;
v8[0xA] = 0x8E;
v8[0xB] = 0xC0;
v8[0xC] = 0x54;
v8[0xD] = 0x6F;
v8[0xE] = 0x8F;
v8[0xF] = 0xEE;
v8[0x10] = 0xF8;
v8[0x11] = 0x5A;
v8[0x12] = 0xA2;
v8[0x13] = 0xC1;
v8[0x14] = 0xEB;
v8[0x15] = 0xA5;
v8[0x16] = 0x34;
v8[0x17] = 0x6D;
v8[0x18] = 0x71;
v8[0x19] = 0x55;
v8[0x1A] = 8;
v8[0x1B] = 7;
v8[0x1C] = 0xB2;
v8[0x1D] = 0xA8;
v8[0x1E] = 0x2F;
v8[0x1F] = 0xF4;
v8[0x20] = 0x51;
v8[0x21] = 0x8E;
v8[0x22] = 0xC;
v8[0x23] = 0xCC;
qmemcpy(&v8[0x24], "3S1", 3);
v8[0x28] = 0x40;
v8[0x29] = 0xD6;
v8[0x2A] = 0xCA;
v8[0x2B] = 0xEC;
v8[0x2C] = 0xD4;
puts("Input flag: ");
__isoc99_scanf("%63s", s);
if ( strlen(s) == 0x2D )
{
    strcpy(v5, "Nu1Lctf233");
    sub_400874(v5, s, v7);
    for ( i = 0; i <= 0x2C; ++i )
    {
    if ( v7[i] != v8[i] )
    {
        puts("GG!");
        return 0LL;
    }
    }
    puts("Congratulations!");
    result = 0LL;
}
else
{
    puts("GG!");
    result = 0LL;
}
return result;
}

注意下面两个函数都有一个共同的特征%256,这是RC4算法的典型特征

sub_400874 中,sub_40067A()和sub_400753()两个函数都是加密算法,

sub_40067A()是流密钥的生成,

sub_400753()是加密。

sub_400874:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
__int64 __fastcall sub_400874(__int64 a1, __int64 a2, __int64 a3)
{
char v5[264]; // [rsp+20h] [rbp-110h] BYREF
unsigned __int64 v6; // [rsp+128h] [rbp-8h]

v6 = __readfsqword(0x28u);
sub_40067A(a1, v5);
sub_400753(v5, a2, a3);
return 0LL;
}

sub_40067A(流密钥生成):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
__int64 __fastcall sub_40067A(const char *a1, __int64 a2)
{
int v3; // [rsp+10h] [rbp-10h]
int i; // [rsp+14h] [rbp-Ch]
int j; // [rsp+18h] [rbp-8h]
int v6; // [rsp+1Ch] [rbp-4h]

v6 = strlen(a1);
v3 = 0;
for ( i = 0; i <= 255; ++i )
    *(_BYTE *)(i + a2) = i;
for ( j = 0; j <= 255; ++j )
{
    v3 = (*(unsigned __int8 *)(j + a2) + v3 + a1[j % v6]) % 256;
    sub_400646(j + a2, a2 + v3);
}
return 0LL;
}

sub_400753(加密):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
__int64 __fastcall sub_400753(__int64 a1, const char *a2, __int64 a3)
{
int v5; // [rsp+24h] [rbp-1Ch]
int v6; // [rsp+28h] [rbp-18h]
size_t v7; // [rsp+30h] [rbp-10h]
size_t v8; // [rsp+38h] [rbp-8h]

v5 = 0;
v6 = 0;
v7 = 0LL;
v8 = strlen(a2);
while ( v7 < v8 )
{
    v5 = (v5 + 1) % 256;
    v6 = (v6 + *(unsigned __int8 *)(v5 + a1)) % 256;
    sub_400646(v5 + a1, a1 + v6);
    *(_BYTE *)(a3 + v7) = a2[v7] ^ *(_BYTE *)((unsigned __int8)(*(_BYTE *)(v5 + a1) + *(_BYTE *)(v6 + a1)) + a1);
    ++v7;
}
return 0LL;
}

本题的密钥是 Nu1Lctf233

加密后的密文是 v8,即

1
[0xc6,0x21,0xca,0xbf,0x51,0x43,0x37,0x31,0x75,0xe4,0x8e,0xc0,0x54,0x6f,0x8f,0xee,0xf8,0x5a,0xa2,0xc1,0xeb,0xa5,0x34,0x6d,0x71,0x55,0x8,0x7,0xb2,0xa8,0x2f,0xf4,0x51,0x8e,0xc,0xcc,0x33,0x53,0x31,0x0,0x40,0xd6,0xca,0xec,0xd4 ]

这个数组的解出来是乱码,这里base64加utf-8编码就可以得到密文

1
2
3
4
5
6
7
8
python
import base64
a=[0xc6,0x21,0xca,0xbf,0x51,0x43,0x37,0x31,0x75,0xe4,0x8e,0xc0,0x54,0x6f,0x8f,0xee,0xf8,0x5a,0xa2,0xc1,0xeb,0xa5,0x34,0x6d,0x71,0x55,0x8,0x7,0xb2,0xa8,0x2f,0xf4,0x51,0x8e,0xc,0xcc,0x33,0x53,0x31,0x0,0x40,0xd6,0xca,0xec,0xd4]
s=""
for i in a:
    s+=chr(i)
print(s)
print(str(base64.b64encode(s.encode('utf-8')), 'utf-8'))

密文:w4Yhw4rCv1FDNzF1w6TCjsOAVG/Cj8Ouw7hawqLDgcOrwqU0bXFVCAfCssKoL8O0UcKODMOMM1MxAEDDlsOKw6zDlA==

贴上搜到的脚本,拿到 flag

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
python
import base64
def rc4_main(key = "init_key", message = "init_message"):
    print("RC4解密主函数调用成功")
    print('\n')
    s_box = rc4_init_sbox(key)
    crypt = rc4_excrypt(message, s_box)
    return crypt
    
def rc4_init_sbox(key):
    s_box = list(range(256)) 
    print("原来的 s 盒:%s" % s_box)
    print('\n')
    j = 0
    for i in range(256):
        j = (j + s_box[i] + ord(key[i % len(key)])) % 256
        s_box[i], s_box[j] = s_box[j], s_box[i]
    print("混乱后的 s 盒:%s"% s_box)
    print('\n')
    return s_box
    
def rc4_excrypt(plain, box):
    print("调用解密程序成功。")
    print('\n')
    plain = base64.b64decode(plain.encode('utf-8'))
    plain = bytes.decode(plain)
    res = []
    i = j = 0
    for s in plain:
        i = (i + 1) % 256
        j = (j + box[i]) % 256
        box[i], box[j] = box[j], box[i]
        t = (box[i] + box[j]) % 256
        k = box[t]
        res.append(chr(ord(s) ^ k))
    print("res用于解密字符串,解密后是:%res" %res)
    print('\n')
    cipher = "".join(res)
    print("解密后的字符串是:%s" %cipher)
    print('\n')
    print("解密后的输出(没经过任何编码):")
    print('\n')
    return  cipher

a=[0xc6,0x21,0xca,0xbf,0x51,0x43,0x37,0x31,0x75,0xe4,0x8e,0xc0,0x54,0x6f,0x8f,0xee,0xf8,0x5a,0xa2,0xc1,0xeb,0xa5,0x34,0x6d,0x71,0x55,0x8,0x7,0xb2,0xa8,0x2f,0xf4,0x51,0x8e,0xc,0xcc,0x33,0x53,0x31,0x0,0x40,0xd6,0xca,0xec,0xd4]
s=""
for i in a:
    s+=chr(i)

s=str(base64.b64encode(s.encode('utf-8')), 'utf-8')

rc4_main("Nu1Lctf233", s)

参考:

RC4加密算法与逆向方法简析

RC4加密算法

RC4加密算法及逆向方法初探

[BUUCTF_N1book_RE_第五章 CTF之RE章]BabyAlgorithm

最后,家人们,补药用rust写脚本😭😭😭 🤡🤡🤡

Licensed under CC BY-NC-SA 4.0
Build by Oight
使用 Hugo 构建
主题 StackJimmy 设计