指定CRC32反构数据

原文章地址:https://blog.csdn.net/sugar13/article/details/51029312

指定CRC反构数据

【摘要】 
针对CRC32算法,给定希望产生的CRC32校验和,通过修改给定文件中连续4个字节,将CRC32改变成希望产生的值。

1、 题目

  给出一组具体的题目,可以便于对问题的分析与解答,并用来验证算法的正确。 
  已知如下数据: 
00 01 02 03 04 05 06 07 08 09 ?? ?? ?? ?? 0A 0B 0C 0D 0E 0F 
  向问号处填入4个字节的数字,使数据的CRC32校验和为DEADBEEF

2、 CRC32算法

  这里的CRC32校验和,以主流文件校验工具提供的CRC32为准,其模型为: 
Bits=32,(校验和的位数) 
TruncPoly=0x104C11DB7,(多项式系数序列) 
InitRem=0xFFFFFFFF,(余数的初值) 
FinalXor=0xFFFFFFFF,(最终结果需要异或的值) 
ReflectIn=true,(数据输入时高低颠倒) 
ReflectRem=true。(余数输出之前先高低颠倒) 
  其中,TruncPoly最高位的1通常省略不写,也就是0x04C11DB7。 
  下面给出一个典型的计算函数。函数中,CRC32算法的核心部分,在于前半部分的移位,按情况与多项式的异或。至于后边将余数的高低位进行的颠倒,以及最终异或的常量,只是收尾。 
  计算函数如下:

#include <assert.h>
#include <stdint.h>

uint32_t crc32_checksum( const uint8_t *buf, unsigned len )
{
    assert (buf != 0);

    // initial remainder
    uint32_t rem = 0xFFFFFFFF;
    for (unsigned i = 0; i < len; ++i)
    {
        // reflect input
        for (unsigned j = 31; j >= 24; --j)
        {
            if (((buf[i] << j) ^ rem) & 0x80000000)
            {
                // truncated polynominal
                rem = (rem << 1) ^ 0x04C11DB7;
            }
            else
            {
                rem = rem << 1;
            }
        }
    }

    // reflect remainder
    uint32_t ref = 0;
    for (unsigned i = 0; i < 32; ++i)
    {
        ref |= ((rem >> i) & 1) << (31 - i);
    }

    // final xor value
    return ref ^ 0xFFFFFFFF;
}

3、 定义运算符

  定义需要的运算符,可以便于书写、推导计算方法。 
  仿照CRC32算法核心部分,定义二进制序列的“冗余”运算符:“\”,二进制序列X对多项式P(保留最高位的1,共计33位)的冗余:X\P,其定义为: 
  直到X的高于32的位全部为0为止,找到X的不为0的最高位的位置n,将P左移(32-n)位得到Q,通过把X^Q赋值给X,使X的不为0的最高位成为0,不断重复该过程;最后保留X的最低32位,就是冗余的结果。上述操作只是为了得到运算结果,而不是修改X的值。 
  定义了冗余运算符,就可以写出循环冗余的核心部分的递推公式: 

Rn+1=((Rn<<1)^(In<<32))∖P,n=0,1,2,3,...Rn+1=((Rn<<1)^(In<<32))∖P,n=0,1,2,3,...


  设R′n=Rn^(In<<31)Rn′=Rn^(In<<31),R′nRn′的最高位即第31位记做r′31r31′,也就是: 

Rn+1={Rn<<1,(Rn<<1)^P,r′31=0r′31=1Rn+1={Rn<<1,r31′=0(Rn<<1)^P,r31′=1


  其中,<<<<是左移操作的运算符,^^是异或操作的运算符,RnRn是记录余数的寄存器(共计32位),R0R0是寄存器的初值,InIn是输入数据的第nn 个位,PP是除数多项式(共计33位)。 
  由于我们习惯按照字节进行处理,所以再列出针对字节流In+7In+6⋯InIn+7In+6⋯In 的递推公式: 

Rn+8=((Rn<<8)^(InIn+1⋯In+7<<32))∖P,n=0,1,2,3,⋯Rn+8=((Rn<<8)^(InIn+1⋯In+7<<32))∖P,n=0,1,2,3,⋯


  其中,输入数据的字节顺序进行了高低颠倒,原因在于参数ReflectIn为真。注意,参数ReflectIn的含义为,字节内的位是否颠倒输入。为假表示不需要颠倒,按照从高位到低位的顺序依次输入;为真表示需要颠倒,按照从低位到高位的顺序依次输入。另外,nn的取值不仅仅是0、8、16、24……当nn为1、2、3、……时,上述递推公式仍然是成立的,所以nn的有效范围仍然写作0、1、2、3……

4、 逆运算和反运算

  循环冗余的递推公式是可逆的。在循环冗余递推公式中,PP是固定的常量,已知RnRn和InIn,可以求得Rn+1Rn+1。逆运算便是,已知Rn+1Rn+1和InIn,求得RnRn。与逆运算相对,反运算便是,已知Rn+1Rn+1和RnRn,求得InIn。 
  我们分析一下逆运算。对Rn+1Rn+1的值有贡献的,包括(Rn<<1)(Rn<<1)的值、(In<<32)(In<<32)的值,以及可能出现进行异或操作的PP。考虑到(Rn<<1)(Rn<<1)、(In<<32)(In<<32)的最低位一定是0,所以Rn+1Rn+1的最低位只能来自于PP。要让逆运算存在,其充分必要条件是:PP的最低位是1。我们选取的多项式满足这个条件,实际上这本来就是理所当然的条件。假如某种CRC标准的除数多项式,其最低位是0,那么算出的余数一定是偶数,余数的最低位就失去了意义。 
  我们可以得到循环冗余逆运算的递推公式如下: 

Rn={(Rn+1>>1)^(In<<31),(Rn+1>>1)^(In<<31)^(P>>1),(Rn+1&1)=0(Rn+1&1)=1Rn={(Rn+1>>1)^(In<<31),(Rn+1&1)=0(Rn+1>>1)^(In<<31)^(P>>1),(Rn+1&1)=1


  其中,&&运算符是“按位与”运算,(Rn+1&1)(Rn+1&1)的含义就是取Rn+1Rn+1的最低位,递推公式是依据这个最低位有不同公式的公式。 
  根据上面的分析,已知Rn+1Rn+1和InIn,是可以求得RnRn的。那么,已知Rn+1Rn+1和RnRn,是否可以求得InIn呢?答案是不一定。InIn的取值只有0和1,RnRn和InIn配合,得到的值可能与Rn+1Rn+1并不相等,因为Rn+1Rn+1一共有232232种取值,而当RnRn一定时,InIn一共有2种取值,得到的结果也只有2种取值,不一定恰好落到Rn+1Rn+1上。 
  那么,增加InIn的位数,借此增加InIn取值的可能性,是不是就能够求得InIn呢?实际上,由于PP是33位的,所以令InIn增加到32位,便可让InIn有232232种取值,用穷举就可以得到正确的值。但穷举法很耗时,而且时间复杂度是指数级别的。在这里我们尝试用数学方法计算。32位的递推公式如下: 

Rn+32=((Rn<<32)^(InIn+1⋯In+31<<32))∖P,n=0,1,2,3,⋯Rn+32=((Rn<<32)^(InIn+1⋯In+31<<32))∖P,n=0,1,2,3,⋯


  等式右边,RnRn左移的位数,由最开始的左移1位,以及按照字节处理的左移8位,现在变成左移32位,这已经从量变提升为质变了。由于异或操作具有交换律,而等式右边的两个数都是左移32位,因此交换其顺序,就得到了如下的式子: 

Rn+32=((InIn+1⋯In+31<<32)^(Rn<<32))∖P,n=0,1,2,3,⋯Rn+32=((InIn+1⋯In+31<<32)^(Rn<<32))∖P,n=0,1,2,3,⋯


  我们把Rn+32Rn+32对32位的RnRn进行循环冗余逆运算,得到的结果就是InIn+1⋯In+31InIn+1⋯In+31的值。由于我们习惯按照字节进行处理,所以将这个32位的值拆成4个字节,注意参数ReflectIn为真,因此4个字节的各个位的顺序是: 
In+7In+6⋯In,In+15In+14⋯In+8,In+23In+22⋯In+16,In+31In+30⋯In+24In+7In+6⋯In,In+15In+14⋯In+8,In+23In+22⋯In+16,In+31In+30⋯In+24 
  我们把RnRn也按照字节拆开。首先把RnRn用32个位表示: 

r0,r1,r2,……,r31r0,r1,r2,……,r31


  由于ReflectIn为真,因此RnRn的值作为输入数据,需要由最低位开始输入: 

Rn=r0r1r2⋯r31Rn=r0r1r2⋯r31


  按照字节拆开之后,4个字节的各个位的顺序是: 

r7r6⋯r0,r15r14⋯r8,r23r22⋯r16,r31r30⋯r24r7r6⋯r0,r15r14⋯r8,r23r22⋯r16,r31r30⋯r24


  结论就是,Rn+32Rn+32对RnRn的反运算,等效于Rn+32Rn+32对上面4个字节的rr的逆运算。

5、 题目分解

  我们把题目中,进行CRC运算的各个步骤列成表格: 

余数赋初值处理开始的已知数处理中间的未知数处理结束的已知数余数各位高低颠倒最终结果处理数组数据00 01 02 03 04 05 06 07 08 09I7⋯0I15⋯8I23⋯16I31⋯240A 0B 0C 0D 0E 0F−−rem=0xFFFFFFFFrem=U31U30⋯U0rem=V31V30⋯V0rem=W31W30⋯W0rem=W0W1⋯W31W0W1⋯W31^0xFFFFFFFF余数赋初值处理数组数据rem=0xFFFFFFFF处理开始的已知数00 01 02 03 04 05 06 07 08 09rem=U31U30⋯U0处理中间的未知数I7⋯0I15⋯8I23⋯16I31⋯24rem=V31V30⋯V0处理结束的已知数0A 0B 0C 0D 0E 0Frem=W31W30⋯W0余数各位高低颠倒−rem=W0W1⋯W31最终结果−W0W1⋯W31^0xFFFFFFFF

  我们可以用开始部分的已知数求出U31U30⋯U0U31U30⋯U0的值,把最终结果变换成W31W30⋯W0W31W30⋯W0的值,用 W31W30⋯W0W31W30⋯W0对结束部分的已知数求逆运算得到V31V30⋯V0V31V30⋯V0的值,最后用V31V30⋯V0V31V30⋯V0对U31U30⋯U0U31U30⋯U0求逆运算,就可以得到I7⋯0I15⋯8I23⋯16I31⋯24I7⋯0I15⋯8I23⋯16I31⋯24的值了。由于进行了两次逆运算,而第二次逆运算正好有4个字节,这4个字节恰好可以填在未知数的4个字节的位置,因此可以将U31U30⋯U0U31U30⋯U0暂时填到未知数的部分,将两次逆运算合并成一次逆运算。 
  由于参数ReflectIn为真,导致运算过程中,所有的运算数据都是高低颠倒的,所以,为了便于处理,这里将余数本身进行高低颠倒,数据输入的操作改为添至余数的低位,移位操作也改为右移,逆运算中的移位操作则改为左移,多项式也进行颠倒。 

6、 处理数组

void crc32_gen_array( uint32_t crc, int pos, uint8_t *buf, int len )
{
    assert (pos >= 0);
    assert (buf != 0);
    assert (pos + 4 <= len);

    uint32_t rem = 0xFFFFFFFF;
    for (int i = 0; i < pos; ++i)
    {
        rem ^= buf[i];

        for (int j = 0; j < 8; ++j)
        {
            rem = (rem >> 1) ^ (rem & 0x00000001 ? 0xEDB88320 : 0);
        }
    }

    for (int i = 0; i < 4; ++i)
    {
        buf[pos + i] = (rem >> (8 * i)) & 0xFF;
    }

    rem = ~crc;
    for (int i = len - 1; i >= pos; --i)
    {
        for (int j = 0; j < 8; ++j)
        {
            rem = (rem << 1) ^ (rem & 0x80000000 ? 0xDB710641 : 0);
        }

        rem ^= buf[i];
    }

    for (int i = 0; i < 4; ++i)
    {
        buf[pos + i] = (rem >> (8 * i)) & 0xFF;
    }

    return;
}

各个参数的含义为: 
crc:指定要构造的校验和; 
pos:指定数据在反构数组中的位置; 
buf:指定等待反构的数组; 
len:等待反构数组的长度。 
用这个方法解答文章开头的题目,并验证结果。

#include <stdio.h>

void test1()
{
    uint8_t buf[20] =
    {
        0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
        0, 0, 0, 0,
        0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
    };

    crc32_gen_array (0xDEADBEEF, 10, buf, 20);

    for (int i = 10; i < 14; i++)
    {
        printf ("%02X ", buf[i]);
    }

    printf ("%08X \n", crc32_checksum (buf, 20));

    return;
}

  在主函数中调用测试函数,运行结果如下: 
76 EF 99 DE DEADBEEF 
  成功算出来了填入的数字。

7、 驱动表法

  通常,CRC32的计算都是以字节为最小单位,而算法中,在循环内部存在着分支判断语句,这样的语句会严重影响运算效率,所以出现了将一个字节的8位进行整体处理的方法。该方法事先将一个字节的256种可能全部列举出来,然后用查表法对号入座,因此被称为“驱动表法”。 
  算法中的冗余运算及其逆运算,都可以用驱动表法进行改造,这样,处理大量数据的时候可以显著提高效率。 
驱动表法能够成立,在于异或操作的交换律。将8次移位、异或的组合操作,拆成8次移位、8次异或,结果是不变的。 
  我们把上文中的计算CRC32的函数,以及反构数组的函数,全部用驱动表法进行改造。注意,计算CRC32的函数,在改造之前,首先用逆序进行了一次改造。改造后的函数如下:

static uint32_t s_gen_table[0x100] = { 0 };
static uint32_t s_inv_table[0x100] = { 0 };

void init_table()
{
    for (int i = 0; i < 0x100; ++i)
    {
        uint32_t gen = i;
        uint32_t inv = i << 24;

        for (int j = 0; j < 8; ++j)
        {
            gen = (gen >> 1) ^ (gen & 0x00000001 ? 0xEDB88320 : 0);
            inv = (inv << 1) ^ (inv & 0x80000000 ? 0xDB710641 : 0);
        }

        s_gen_table[i] = gen;
        s_inv_table[i] = inv;
    }

    return;
}

uint32_t crc32_by_table( const uint8_t *buf, unsigned len )
{
    assert (buf != 0);

    uint32_t rem = 0xFFFFFFFF;
    for (unsigned i = 0; i < len; ++i)
    {
        rem = (rem >> 8) ^ s_gen_table[(rem ^ buf[i]) & 0xFF];
    }

    return ~rem;
}
 
void crc32_gen_by_table( uint32_t crc, int pos, uint8_t *buf, int len )
{
    assert (pos >= 0);
    assert (buf != 0);
    assert (pos + 4 <= len);

    uint32_t rem = 0xFFFFFFFF;
    for (int i = 0; i < pos; ++i)
    {
        rem = (rem >> 8) ^ s_gen_table[(rem ^ buf[i]) & 0xFF];
    }

    for (int i = 0; i < 4; ++i)
    {
        buf[pos + i] = (rem >> (8 * i)) & 0xFF;
    }

    rem = ~crc;
    for (int i = len - 1; i >= pos; --i)
    {
        rem = (rem << 8) ^ s_inv_table[rem >> 24] ^ buf[i];
    }

    for (int i = 0; i < 4; ++i)
    {
        buf[pos + i] = (rem >> (8 * i)) & 0xFF;
    }

    return;
}

  其中,s_gen_table和s_inv_table是驱动表,调用init_table函数来初始化驱动表。我们可以在初始化之后,把驱动表打印出来,然后以常量静态数组的方式定义驱动表,省去初始化驱动表的函数。

8、 处理文件

  我们的最终目的,是反构文件,改造文件成我们需要的校验和。注意,文件的尺寸可能超过程序可以申请的最大内存的大小,所以要一部分一部分的读取文件。

void crc32_gen_file( uint32_t crc, int64_t pos, const char *filename )
{
    assert (pos >= 0);
    assert (filename != 0);
    assert (filename[0] != 0);

    FILE *stream = fopen (filename, "rb+");
    if (stream == 0)
    {
        return;
    }

    enum { BUF_SIZE = 0x40000 };
    uint8_t buf[BUF_SIZE] = { 0 };
    _fseeki64 (stream, pos, SEEK_SET);
    fwrite (buf, 1, 4, stream);

    uint32_t rem = 0xFFFFFFFF;
    for (int64_t i = 0; i < pos; ++i)
    {
        if (i % BUF_SIZE == 0)
        {
            _fseeki64 (stream, i, SEEK_SET);
            fread (buf, 1, BUF_SIZE, stream);
        }
        rem = (rem >> 8) ^ s_gen_table[(rem ^ buf[i % BUF_SIZE]) & 0xFF];
    }

    for (int i = 0; i < 4; ++i)
    {
        buf[i] = (rem >> (8 * i)) & 0xFF;
    }
    _fseeki64 (stream, pos, SEEK_SET);
    fwrite (buf, 1, 4, stream);
 
    rem = ~crc;
    _fseeki64 (stream, 0, SEEK_END);
    int64_t len = _ftelli64 (stream);
    for (int64_t i = len - 1; i >= pos; --i)
    {
        if (i == len - 1 || i % BUF_SIZE == BUF_SIZE - 1)
        {
            _fseeki64 (stream, i / BUF_SIZE * BUF_SIZE, SEEK_SET);
            fread (buf, 1, BUF_SIZE, stream);
        }

        rem = (rem << 8) ^ s_inv_table[rem >> 24] ^ buf[i % BUF_SIZE];
    }

    for (int i = 0; i < 4; ++i)
    {
        buf[i] = (rem >> (8 * i)) & 0xFF;
    }
    _fseeki64 (stream, pos, SEEK_SET);
    fwrite (buf, 1, 4, stream);
    fclose (stream);

    return;
}

  自己创建一个文件,然后用这个函数反构文件,然后用文件检验程序计算其CRC32,发现能够成功。


附注:头一次用markdown编辑公式,实在是好麻烦……

猜你喜欢

转载自blog.csdn.net/fz835304205/article/details/82594276