Strange_rc4

第一节:题目

flag是加密之后的一串31字节的原始二进制密文,c1是它的加密程序,用法是 C1.exe <输入文件> <输出文件>。
也就是说这道题要我们求 C1 的逆。标题叫 Strange_rc4,那肯定不是原来的rc4的流程。

第二节:过程

我们要求C1的逆,先把C1放进IDA里看看怎么个事,可以看到比较乱一时半会看不到main,我们shift+F12,Usage: cmain.exe input output

之后x进入交叉引用,只有一处,之后F5构建伪代码,然后rename那些看不懂的部分,就可以得到

int __fastcall main(int argc, char **argv)
{
    FILE *fin;
    FILE *fout;
    int size;
    void *buf;
    uint8_t state[296];

    if (argc != 3) {
        printf("Usage: cmain.exe <input> <output>\n");
        return 1;
    }

    fin = NULL;
    if (fopen_s(&fin, argv[1], "rb") != 0) {          // argv+8 = argv[1]
        printf("[!] cannot open input\n");
        return 1;
    }

    fseek(fin, 0, SEEK_END);
    size = ftell(fin);                                 // 先量长度
    fseek(fin, 0, SEEK_SET);
    buf = malloc(size);
    if (buf == NULL)
        return 1;

    fread(buf, 1, size, fin);
    fclose(fin);

    memset(state, 0, 296);
    rc4_init(state, rc4_key, 16, buf, size);           // 密钥 16 字节
    rc4_ksa(state);
    rc4_prga(state);                                   // 原地加密 buf

    fout = NULL;
    if (fopen_s(&fout, argv[2], "wb") != 0) {          // argv+16 = argv[2]
        printf("[!] cannot open output\n");
        return 1;
    }
    fwrite(buf, 1, size, fout);
    fclose(fout);
    free(buf);
    printf("[+] encrypted %ld bytes -> %s\n", size, argv[2]);
    return 0;
}


发现了吗?size不变说明了明文密文长度相同,rc4_prga(state)直接fwrite(buf,...),没有把结果返回buf,就是原地加密了

不过事实上这道题不用这么复杂,都说了是Strange_rc4了,那我们造几个输入喂给C1,看看它到底怎么变的
先造两个只差一个字节的输入,各跑一次,看看他们的区别
testA:31个0x00

a078244ef1bb5512ae5987d88ee781c453f63cf4ed89f091ec4a345f1f1d46

testB:31个0x00,只把第11个字节(偏移 10)从00改成01
a078244ef1bb5512ae5988d88ee781c453f63cf4ed89f091ec4a345f1f1d46

发现了吗?只有第11个字节不一样,说明输出第i个字节只由输入第i个字节决定,31个位置互不干扰

既然互不干扰,那就盯住一个位置,看看它到底做了个什么变化。我们造8 个1字节的文件,内容分别是0到7,经过C1之后的变化:

0x00 -> 0xa0    0x04 -> 0x9c
0x01 -> 0x9f    0x05 -> 0x9b
0x02 -> 0x9e    0x06 -> 0x9a
0x03 -> 0x9d    0x07 -> 0x99

发现了加密的规律,是每个输出挨着减1——低位被翻转了,这说明是异或;整体又比输入高了0x9f。写出来就是

c = (p ^ k) + 1

k是那个位置的密钥流字节,这里k = 0x9f。
如果我们令p=0,那么c=k+1。也就是说,把文件全填0跑一次C1,输出减1就是整条密钥流
那解出flag就轻而易举了

key: 9f77234df0ba5411ad5886d78de680c352f53bf3ec88ef90eb49335e1e1c45

反过来解就一行:p=(c-1)^k
脚本:

import os, subprocess, tempfile

C1 = r"C1.exe"
enc = open("flag.enc", "rb").read()

tmp = tempfile.mkdtemp()
f_in = os.path.join(tmp, "zero.bin")
f_out = os.path.join(tmp, "zero.enc")
open(f_in, "wb").write(bytes(len(enc)))                 # 全零,长度和密文一致
subprocess.run([C1, f_in, f_out], check=True)

k = bytes(x - 1 for x in open(f_out, "rb").read())      # 全零输入 → 减 1 就是密钥流
print(bytes(((c - 1) & 0xFF) ^ k[i] for i, c in enumerate(enc)).decode())

最终flag

flag{Y0u_cr4ck_7h3_57r4ng3_RC4}

所以免去了很多不必要的读代码的时间,拿到这种题目还是先拿一些测试数据看看有没有规律比较好