揭秘LedgerCTF的AES白盒挑战:逆向工程与密码学分析
引言
大约一个月前,我的同事b0n0n正在研究ledgerctf谜题,并挑战我查看ctf2二进制文件。我最终接受了挑战,本文将讨论该保护方案及其破解方法。在深入之前,先提供一些背景信息。
ledger是一家成立于2014年的法国安全公司,专注于密码学、加密货币和硬件。他们最近上线了三个不同的谜题,以庆祝其漏洞赏金计划的正式启动。第二个挑战称为ctf2,正是我们今天要讨论的内容。ctf2是一个ELF64二进制文件,可从此处下载。该二进制文件大约11MB,用C++编写,甚至包含符号信息,这非常棒。
整体概览
初步侦察
首先,你肯定已经注意到二进制文件中包含大量数据,如下图所示。这意味着要么二进制文件被加壳且IDA难以识别代码片段,要么这些确实是真实数据。
由于我们已经知道二进制文件未被剥离,第一种假设很可能不成立。通过浏览反汇编器中的代码,没有发现任何异常;一切看起来都很健康。没有混淆、代码加密或任何类型的加壳迹象。此时,我们确信这是一个纯粹的反向工程挑战,一帆风顺!
扩散
二进制文件期望一个序列号作为输入,该序列号是由32个十六进制字符组成的字符串,例如:00112233445566778899AABBCCDDEEFF。然后,有一个包含16轮的循环,逐字符遍历序列号,并构建15个 blob,每个16字节长;我称它们为i0、i1、...、i14(这非常直观)。此循环的每一轮初始化每个i的一个字节(因此有16轮)。当前的输入序列号字节通过一个巨大的替换盒(我称为sbx,长度为11534336字节)发送。这基本上将输入序列号扩散到这些blob中。如果上述解释不够清晰,以下是美化后的C代码:
c
while(Idx < 16) {
sbx++;
char CurrentByteString[3] = {
Serial[Idx],
Serial[Idx + 1],
0
};
Idx += 2LL;
uint8_t CurrentByte = strtol(CurrentByteString, 0LL, 16);
i0[sbx[-1]] = CurrentByte;
i1[sbx[15]] = CurrentByte;
i2[sbx[31]] = CurrentByte;
i3[sbx[47]] = CurrentByte;
i4[sbx[63]] = CurrentByte;
i5[sbx[79]] = CurrentByte;
i6[sbx[95]] = CurrentByte;
i7[sbx[111]] = CurrentByte;
i8[sbx[127]] = CurrentByte;
i9[sbx[143]] = CurrentByte;
i10[sbx[159]] = CurrentByte;
i11[sbx[175]] = CurrentByte;
i12[sbx[191]] = CurrentByte;
i13[sbx[207]] = CurrentByte;
i14[sbx[223]] = CurrentByte;
}
混淆
在上述操作之后,发生了一系列不一定立即有意义的操作。就我而言,这尚未引起我的关注,因为我尚未看到与输入序列号字节或i的清晰关系。由于这两者是唯一源自用户输入的数据,目前我只关心这些。
接下来,我们遇到以下代码:
c
do
{
v16 = v15 + 4;
do
{
rd = rand();
v18 = (unsigned __int8)(((unsigned __int64)rd >> 56) + rd) - ((unsigned int)(rd >> 31) >> 24);
mask[v15] = v18;
mask3[v15] = v18;
shiftedmask[v15++] = v18;
}
while ( v15 != v16 );
}
while ( v15 != 16 );
我从这部分了解到有新玩家加入。基本上,三个16字节的blob,分别称为mask、mask3和shiftedmask,使用从rand()派生的值进行初始化。起初,看到伪随机值参与其中确实有点令人困惑,但我们可以假设这些操作稍后会被其他操作取消。让类似加密的算法产生非确定性结果是没有意义的。PRNG使用time(NULL)进行播种。
在此之后,还有一系列我们不在乎的其他操作。您可以将这些视为黑盒,生成确定性输出。这意味着我们能够在需要时方便地转储生成的值。值得一提的是,它基本上在mask3内部混合了一堆值。
c
shiftrows((unsigned __int8 (*)[4])shiftedmask);
shiftrows((unsigned __int8 (*)[4])mask3);
v19 = mul3[(unsigned __int8)byte_D03774] ^ mul2[mask3[0]] ^ byte_D03778 ^ byte_D0377C;
// ... 更多类似操作
mul3和mul2基本上是数组,构造为在GF(2**8)内mul2[idx] = idx * 2和mul3[idx] = idx * 3。
一个可能有趣的点是,其中有一个小的反调试。文件被打开并使用std::vector的一个构造函数读取,该构造函数将std::ifstreambuf_iterator作为输入。生成某种校验和,并将在稍后的schedule例程中使用。这意味着如果您要修补二进制文件,算法最终将生成错误的值。同样,这几乎不是一个不便之处,因为我们可以将其转储并继续我们的生活。
生成
此时,上述15个i用于初始化我称为s0、s1、...、s14的变量。同样,这是15个blob,每个16字节。它们被传递给schedule函数,该函数将在s数组上执行大量算术操作。同样,尚不需要理解schedule;就我们而言,它是一个黑盒,接受s作为输入并返回不同的s作为输出,仅此而已。
这些16字节(方便的是,XMM寄存器是16字节长,允许编译器优化操作这些blob的代码)(s0、...、s14)被异或在一起,如果结果的xmmword遵守一系列约束,那么您将获得成功消息。
这些约束看起来像这样:
c
h1 = mxor.m128i_u8[0] | ((mxor.m128i_u8[4] | ((mxor.m128i_u8[8] | ((mxor.m128i_u8[12] | ((mxor.m128i_u8[1] | ((mxor.m128i_u8[5] | ((mxor.m128i_u8[9] | ((unsigned __int64)mxor.m128i_u8[13] << 8)) << 8)) << 8)) << 8)) << 8)) << 8)) << 8);
h2 = mxor.m128i_u8[2] | ((mxor.m128i_u8[6] | ((mxor.m128i_u8[10] | ((mxor.m128i_u8[14] | ((mxor.m128i_u8[3] | ((mxor.m128i_u8[7] | ((mxor.m128i_u8[11] | ((unsigned __int64)mxor.m128i_u8[15] << 8)) << 8)) << 8)) << 8)) << 8)) << 8)) << 8);
if ( BYTE6(h2) == 'i'
&& BYTE5(h2) == '7'
&& BYTE4(h2) == '\x13'
&& (mxor.m128i_u8[2] | ((mxor.m128i_u8[6] | ((mxor.m128i_u8[10] | ((mxor.m128i_u8[14] | ((mxor.m128i_u8[3] | ((mxor.m128i_u8[7] | ((mxor.m128i_u8[11] | ((unsigned int)mxor.m128i_u8[15] << 8)) << 8)) << 8)) << 8)) << 8)) << 8)) << 8)) >> 24 == 66
&& (unsigned __int8)((mxor.m128i_u8[2] | ((mxor.m128i_u8[6] | ((mxor.m128i_u8[10] | ((mxor.m128i_u8[14] | ((mxor.m128i_u8[3] | ((mxor.m128i_u8[7] | ((mxor.m128i_u8[11] | ((unsigned int)mxor.m128i_u8[15] << 8)) << 8)) << 8)) << 8)) << 8)) << 8)) << 8)) >> 16) == 105
&& BYTE1(h2) == 55
&& mxor.m128i_i8[2] == 19
&& HIBYTE(h1) == 66
&& BYTE6(h1) == 105
&& BYTE5(h1) == 55
&& BYTE4(h1) == 19
&& (mxor.m128i_u8[0] | ((mxor.m128i_u8[4] | ((mxor.m128i_u8[8] | ((mxor.m128i_u8[12] | ((mxor.m128i_u8[1] | ((mxor.m128i_u8[5] | ((mxor.m128i_u8[9] | ((unsigned int)mxor.m128i_u8[13] << 8)) << 8)) << 8)) << 8)) << 8)) << 8)) << 8)) >> 24 == 66
&& (unsigned __int8)((mxor.m128i_u8[0] | ((mxor.m128i_u8[4] | ((mxor.m128i_u8[8] | ((mxor.m128i_u8[12] | ((mxor.m128i_u8[1] | ((mxor.m128i_u8[5] | ((mxor.m128i_u8[9] | ((unsigned int)mxor.m128i_u8[13] << 8)) << 8)) << 8)) << 8)) << 8)) << 8)) << 8)) >> 16) == 105
&& BYTE1(h1) == 55
&& mxor.m128i_i8[0] == 19
&& h2 >> 56 == 66 )
{
puts("**** Login Successful ****");
v42 = 0;
}
else
{
puts("**** Login Failed ****");
v42 = 1;
}
这堆垃圾简单地转化为win = (mxor == 0x42424242696969693737373713131313ULL) :)。
深入分析
现在是时候深入并亲自动手了。我们大致知道需要实现什么,但不确定如何实现。我们知道需要进行一些转储:mask、mask3、shiftedmask、crc、sbx、mul2和mul3。简单,机械。
最重要的未知部分是更多理解schedule。您可以将其视为挑战的核心。所以让我们开始吧。
schedule
乍一看,该函数看起来并不太糟糕,这总是好的。函数的第一部分是随机选择s变量之一(变量i用于索引存储所有s的状态数组)。
c
for(i = rand() % 15; scheduling[i] == 40; i = rand() % 15);
nround = scheduling[i];
随后的switch case对选定的s变量应用一种类型的转换(算术转换)。为了跟踪已应用于每个s变量的轮数,使用了一个称为scheduling的数组。当四十轮已应用于每个s时,算法停止。还值得指出的是,这里有一个小的反调试;一轮开始时启动计时器(t1),结束时停止(t2)。如果发现t1和t2之间有任何异常延迟,后续计算将产生错误结果。
我们可以在switch case中观察到6种不同类型的操作。其中一些看起来非常容易反转,而其他一些则需要更多工作。但此时,这让我想起了我在2013年分析过的这个AES白盒。这个没有混淆,这使得处理起来容易得多。我当时做的是非常简单:分而治之。我将每一轮分为四个部分。每个四分之一轮作为一个黑盒函数,接受4字节输入并生成4字节输出(因此每轮生成16字节/128位)。我需要找到4字节输入,给我想要的4字节输出。解决这些四分之一可以同时进行,并且从所需的输出开始,您可以从第N轮走回第N-1轮。这基本上是我对ctf2的计划。
此时,我已经将schedule函数提取到我自己的程序中。我清理了代码,并确保它产生与程序本身相同的结果(总是有趣的调试)。换句话说,我准备好继续分析所有算术轮。
case 0: 编码
这种情况非常简单,如下所示:
c
case 0:
s0[i] = _mm_xor_si128(_mm_load_si128(&s0[i]), *(__m128i *)mask);
break;
因此,反转它是一个简单的XOR操作:
c
void reverse_0(Slot_t &Output, Slot_t &Input) {
Input = _mm_xor_si128(_mm_load_si128(&Output), mask);
}
case 1, 5, 9, 13, 17, 21, 25, 29, 33, 37: SubBytes
这种情况与之前相比可能看起来更令人生畏(笑)。以下是我清理和美化后的样子:
c
case 1:
case 5:
case 9:
case 13:
case 17:
case 21:
case 25:
case 29:
case 33:
case 37: {
v54 = nround >> 2;
v55 = Slot->m128i_u8[0];
v77.m128i_u64[0] = mask.m128i_u8[0];
v56 = v54;
v54 <<= 20;
v79 = mask.m128i_u8[1];
v81 = mask.m128i_u8[2];
v57 = &sboxes[256 * (v55 + (v56 << 12))];
v58 = Slot->m128i_u8[1];
v80 = &sboxes[256 * v58 + v54];
v60 = Slot->m128i_u8[2];
v61 = &sboxes[256 * v60 + v54];
v62 = Slot->m128i_u8[3];
v83 = &sboxes[256 * v62 + v54];
v64 = Slot->m128i_u8[4];
v84 = &sboxes[256 * v64 + v54];
v65 = Slot->m128i_u8[6];
v85 = &sboxes[256 * uint64_t(Slot->m128i_u8[5]) + v54];
v66 = &sboxes[256 * v65 + v54];
v67 = Slot->m128i_u8[7];
v68 = &sboxes[256 * v67 + v54];
v69 = Slot->m128i_u8[8];
v88 = mask.m128i_u8[8];
v89 = &sboxes[256 * v69 + v54];
v90 = mask.m128i_u8[9];
v70 = v54 + (uint64_t(Slot->m128i_u8[9]) << 8);
v92 = mask.m128i_u8[10];
v91 = &sboxes[v70];
v71 = Slot->m128i_u8[10];
v94 = mask.m128i_u8[11];
v96 = mask.m128i_u8[12];
v93 = &sboxes[256 * v71 + v54];
v72 = Slot->m128i_u8[11];
v98 = mask.m128i_u8[13];
v95 = &sboxes[256 * v72 + v54];
v73 = Slot->m128i_u8[12];
v100 = mask.m128i_u8[14];
v97 = &sboxes[256 * v73 + v54];
v99 = &sboxes[256 * uint64_t(Slot->m128i_u8[13]) + v54];
v101 = &sboxes[256 * uint64_t(Slot->m128i_u8[14]) + v54];
Slot->m128i_u8[0] = v57[mask.m128i_u8[0]];
Slot->m128i_u8[1] = v80[mask.m128i_u8[1] + 0x10000];
Slot->m128i_u8[2] = v61[mask.m128i_u8[2] + 0x20000];
Slot->m128i_u8[3] = v83[mask.m128i_u8[3] + 196608];
Slot->m128i_u8[4] = v84[mask.m128i_u8[4] + 0x40000];
Slot->m128i_u8[5] = v85[mask.m128i_u8[5] + 327680];
Slot->m128i_u8[6] = v66[mask.m128i_u8[6] + 393216];
Slot->m128i_u8[7] = v68[mask.m128i_u8[7] + 458752];
Slot->m128i_u8[8] = v89[mask.m128i_u8[8] + 0x80000];
Slot->m128i_u8[9] = v91[mask.m128i_u8[9] + 589824];
Slot->m128i_u8[10] = v93[mask.m128i_u8[10] + 655360];
Slot->m128i_u8[11] = v95[mask.m128i_u8[11] + 720896];
Slot->m128i_u8[12] = v97[mask.m128i_u8[12] + 786432];
Slot->m128i_u8[13] = v99[mask.m128i_u8[13] + 851968];
Slot->m128i_u8[14] = v101[mask.m128i_u8[14] + 917504];
Slot->m128i_u8[15] = sboxes[256 * uint64_t(Slot->m128i_u8[15]) + 983040 + v54 + mask.m128i_u8[15]];
*Slot = _mm_xor_si128(*Slot, crc);
break;
}
我总是关注的是:输入和输出字节之间的关系。请记住,每一轮作为一个函数,接受16字节blob作为输入(我的代码中的Slot_t)并返回另一个16字节blob作为输出。由于我们感兴趣的是编写一个可以找到生成特定输出的输入的函数,因此识别输出如何构建以及使用哪些输入字节来构建它非常重要。
让我们仔细看看输出的第一个字节是如何生成的。我们从函数的末尾开始,并追溯引用,直到遇到输入状态的字节。在这种情况下,我们追溯v57的来源,然后是v55和v56。v55是输入状态的第一个字节,很好。v56是一个编码轮数的数字。我们现在不一定关心它,但很好意识到轮数是此函数的一个参数;而不仅仅是输入字节。好的,所以我们知道输出的第一个字节是通过输入的第一个字节构建的,简单。比我在查看Hex-Rays输出时最初预期的要简单。但我接受简单:)。
如果您对每个字节重复上述步骤,您基本上会意识到输出的每个字节都依赖于一个输入字节。它们都彼此独立,这甚至更好。这意味着我们可以非常容易地暴力破解输入值以生成特定的输出值。这很棒,因为它...计算成本非常低;如此便宜,我们甚至不费心,我们继续下一个案例。
理论上,我们甚至可以并行化以下内容,但可能不值得这样做,因为已经很快。
c
void reverse_37(const uint32_t nround, Slot_t &Output, Slot_t &Input) {
uint8_t is[16];
for (uint32_t i = 0; i < 16; ++i) {
for (uint32_t c = 0; c < 0x100; ++c) {
Input.m128i_u8[i] = c;
round(nround, &Input);
if (Input.m128i_u8[i] == Output.m128i_u8[i]) {
is[i] = c;
break;
}
}
}
memcpy(Input.m128i_u8, is, 16);
}
有趣的是,如果您修补了挑战二进制文件,这是另一个事情会出错的地方。crc值在函数末尾用于XOR输出状态,并会污染您的结果,狡猾:)。
case 2, 6, 10, 14, 18, 22, 26, 30, 34, 38: ShiftRows
不错,我们已经解决了六种情况中的两种。这种情况看起来也不太好,它非常短,编写反转看起来足够容易:
c
case 2:
case 6:
case 10:
case 14:
case 18:
case 22:
case 26:
case 30:
case 34:
case 38: {
v42 = Slot->m128i_u8[6];
v43 = Slot->m128i_u8[4];
v44 = Slot->m128i_u8[5];
Slot->m128i_u8[6] = Slot->m128i_u8[7];
Slot->m128i_u8[5] = v42;
v45 = Slot->m128i_u8[8];
v46 = Slot->m128i_u8[11];
Slot->m128i_u8[4] = v44;
Slot->m128i_u8[7] = v43;
v47 = Slot->m128i_u8[10];
v48 = Slot->m128i_u8[9];
Slot->m128i_u8[10] = v45;
Slot->m128i_u8[9] = v46;
v49 = Slot->m128i_u8[13];
v50 = Slot->m128i_u8[12];
Slot->m128i_u8[8] = v47;
Slot->m128i_u8[11] = v48;
v51 = Slot->m128i_u8[15];
v52 = Slot->m128i_u8[14];
Slot->m128i_u8[13] = v50;
Slot->m128i_u8[14] = v49;
Slot->m128i_u8[12] = v51;
Slot->m128i_u8[15] = v52;
break;
}
通过快速查看此函数,您清楚地了解到它是某种洗牌操作。无论出于何种原因,这不是我擅长的脑力体操。我通常使用的技巧是给它一个如下所示的输入:\x00\x01\x02\x03...并观察结果。
c
void test_reverse38() {
const uint8_t Input[16] {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f
};
Slot_t InputSlot;
memcpy(&InputSlot.m128i_u8, Input, 16);
round(38, &InputSlot);
hexdump(stdout, &InputSlot.m128i_u8, 16);
}
如果我们应用上述技巧,我们得到以下内容:
makefile
0000: 00 01 02 03 05 06 07 04 0A 0B 08 09 0F 0C 0D 0E ................
从这里开始,(至少对我来说)弄清楚洗牌的效果要容易得多。例如,我们已经知道前四个字节无事可做,因为它们没有被洗牌。我们知道我们需要取Output[7]并将其放入Input[4],Output[4]放入Input[5],依此类推。经过一些脑力体操后,我最终得到了这个例程:
c
void reverse_38(Slot_t &Output, Slot_t &Input) {
uint8_t s4 = Output.m128i_u8[4];
Output.m128i_u8[4] = Output.m128i_u8[7];
uint8_t s5 = Output.m128i_u8[5];
Output.m128i_u8[5] = s4;
uint8_t s6 = Output.m128i_u8[6];
Output.m128i_u8[6] = s5;
uint8_t s7 = Output.m128i_u8[7];
Output.m128i_u8[7] = s6;
uint8_t s8 = Output.m128i_u8[8];
Output.m128i_u8[8] = Output.m128i_u8[10];
uint8_t s9 = Output.m128i_u8[9];
Output.m128i_u8[9] = Output.m128i_u8[11];
Output.m128i_u8[10] = s8;
Output.m128i_u8[11] = s9;
uint8_t s12 = Output.m128i_u8[12];
Output.m128i_u8[12] = Output.m128i_u8[13];
uint8_t s13 = Output.m128i_u8[13];
Output.m128i_u8[13] = Output.m128i_u8[14];
Output.m128i_u8[14] = Output.m128i_u8[15];
Output.m128i_u8[15] = s12;
memcpy(Input.m128i_u8, Output.m128i_u8, 16);
}
下一个!
case 3, 7, 11, 15, 19, 23, 27, 31, 35: MixColumns
这种情况基本上是最烦人的。乍一看,它看起来与我们之前分析的案例1非常相似,但...不完全一样。
c
case 3:
case 7:
case 11:
case 15:
case 19:
case 23:
case 27:
case 31:
case 35: {
v7 = Slot->m128i_u8[0];
v8 = Slot->m128i_u8[4];
v9 = Slot->m128i_u8[1];
v10 = Slot->m128i_u8[5];
v11 = Slot->m128i_u8[14] ^ Slot->m128i_u8[10];
v12 = mul3[v8] ^ mul2[v7] ^ Slot->m128i_u8[12] ^ Slot->m128i_u8[8];
v81 = Slot->m128i_u8[3];
uint8_t v78x = v12;
uint8_t v79x = mul3[v10] ^ mul2[v9] ^ Slot->m128i_u8[13] ^ Slot->m128i_u8[9];
v77.m128i_u64[0] = Slot->m128i_u8[2];
v13 = mul2[v77.m128i_u64[0]] ^ v11;
v14 = Slot->m128i_u8[6];
uint8_t v80x = mul3[v14] ^ v13;
v15 = Slot->m128i_u8[7];
uint8_t v82x = mul3[v15] ^ mul2[v81] ^ Slot->m128i_u8[15] ^ Slot->m128i_u8[11];
v16 = mul2[v8] ^ Slot->m128i_u8[12] ^ Slot->m128i_u8[0];
v17 = Slot->m128i_u8[8];
uint8_t v83x = mul3[v17] ^ v16;
v18 = mul2[v10] ^ Slot->m128i_u8[13] ^ Slot->m128i_u8[1];
v19 = Slot->m128i_u8[9];
v20 = Slot->m128i_u8[14] ^ Slot->m128i_u8[2];
uint8_t v84x = mul3[v19] ^ v18;
v21 = mul2[v14] ^ v20;
v22 = Slot->m128i_u8[10];
v23 = Slot->m128i_u8[15] ^ Slot->m128i_u8[3];
uint8_t v85x = mul3[v22] ^ v21;
v24 = mul2[v15] ^ v23;
v25 = Slot->m128i_u8[11];
v26 = Slot->m128i_u8[4] ^ Slot->m128i_u8[0];
uint8_t v86x = mul3[v25] ^ v24;
v27 = mul2[v17] ^ v26;
v28 = Slot->m128i_u8[12];
v29 = Slot->m128i_u8[5] ^ Slot->m128i_u8[1];
uint8_t v87x = mul3[v28] ^ v27;
v30 = mul2[v19] ^ v29;
v31 = Slot->m128i_u8[13];
v32 = Slot->m128i_u8[6] ^ Slot->m128i_u8[2];
uint8_t v88x = mul3[v31] ^ v30;
v33 = mul2[v22] ^ v32;
v34 = Slot->m128i_u8[14];
v35 = Slot->m128i_u8[7] ^ Slot->m128i_u8[3];
uint8_t v89x = mul3[v34] ^ v33;
v36 = mul2[v25] ^ v35;
v37 = Slot->m128i_u8[15];
v38 = Slot->m128i_u8[8] ^ Slot->m128i_u8[4];
uint8_t v90x = mul3[v37] ^ v36;
uint8_t v7x = mul2[v28] ^ v38 ^ mul3[v7];
v9 = mul2[v31] ^ Slot->m128i_u8[9] ^ Slot->m128i_u8[5] ^ mul3[v9];
v39 = mul3[v77.m128i_u64[0]] ^ mul2[v34] ^ Slot->m128i_u8[10] ^ Slot->m128i_u8[6];
v40 = mul3[v81] ^ Slot->m128i_u8[11] ^ Slot->m128i_u8[7] ^ mul2[v37];
Slot->m128i_u8[0] = v78x;
Slot->m128i_u8[1] = v79x;
Slot->m128i_u8[2] = v80x;
Slot->m128i_u8[3] = v82x;
Slot->m128i_u8[4] = v83x;
Slot->m128i_u8[5] = v84x;
Slot->m128i_u8[6] = v85x;
Slot->m128i_u8[7] = v86x;
Slot->m128i_u8[8] = v87x;
Slot->m128i_u8[9] = v88x;
Slot->m128i_u8[10] = v89x;
Slot->m128i_u8[11] = v90x;
Slot->m128i_u8[12] = v7x;
Slot->m128i_u8[13] = uint8_t(v9);
Slot->m128i_u8[14] = v39;
Slot->m128i_u8[15] = v40;
break;
}
这次如果我们仔细看,我们注意到每四个字节的输出依赖于四个字节的输入。并且每四个输出字节的每个字节都依赖于那四个输入字节。
这意味着您不能像之前那样逐字节暴力破解。您必须暴力破解四个字节...这比我们上面看到的要昂贵得多。我们唯一的好处是我们可以并行暴力破解它们,因为它们彼此独立。每个一个线程应该可以完成工作。
在这个阶段,我已经在各种错误或愚蠢的事情上浪费了一堆时间;所以我决定编写这个非常简单的朴素暴力函数(它既不漂亮也不快...但此时我已经与它和平相处):
c
void reverse_35(Slot_t &Output, Slot_t &Input) {
uint8_t final_result[16];
std::thread t0([Input, Output, &final_result]() mutable {
for (uint64_t a = 0; a < 0x100; ++a) {
for (uint64_t b = 0; b < 0x100; ++b) {
for (uint64_t c = 0; c < 0x100; ++c) {
for (uint64_t d = 0; d < 0x100; ++d) {
Input.m128i_u8[0] = uint8_t(a);
Input.m128i_u8[4] = uint8_t(b);
Input.m128i_u8[8] = uint8_t(c);
Input.m128i_u8[12] = uint8_t(d);
round(35, &Input);
if (Input.m128i_u8[0] == Output.m128i_u8[0] && Input.m128i_u8[4] == Output.m128i_u8[4] &&
Input.m128i_u8[8] == Output.m128i_u8[8] && Input.m128i_u8[12] == Output.m128i_u8[12]) {
final_result[0] = uint8_t(a);
final_result[4] = uint8_t(b);
final_result[8] = uint8_t(c);
final_result[12] = uint8_t(d);
return;
}
}
}
}
}
});
// ... 类似地为其他三个线程 t1, t2, t3
t0.join();
t1.join();
t2.join();
t3.join();
memcpy(Input.m128i_u8, final_result, 16);
return;
}
每个线程恢复四个字节,结果聚合在final_result中,简单。
case 4, 8, 12, 16, 20, 24, 28, 32, 36: AddRoundKey
这种情况是另一种简单的情况,简单的XOR操作即可反转操作:
c
case 4:
case 8:
case 12:
case 16:
case 20:
case 24:
case 28:
case 32:
case 36: {
*Slot = _mm_xor_si128(_mm_load_si128(Slot), mask3);
break;
}
请注意,mask3是当您在一轮中引入异常延迟时被修改的数组之一;例如,如果您正在调试。这是另一个可能产生错误结果的地方:)。
c
void reverse_36(Slot_t &Output, Slot_t &Input) {
Input = _mm_xor_si128(_mm_load_si128(&Output), mask3);
}
case 39: 解码
最后,我们的最后一种情况是另一种非常简单的情况:
c
case 39: {
*Slot = _mm_xor_si128(_mm_load_si128(Slot), shiftedmask);
break;
}
用以下内容反转:
c
void reverse_39(Slot_t &Output, Slot_t &Input) {
Input = _mm_xor_si128(_mm_load_si128(&Output), shiftedmask);
}
反转
在这个阶段,我们拥有所有需要的小块来找到生成特定输出状态的输入状态。我们简单地将所有编写的reverse_例程组合成一个函数,该函数基本上是schedule的逆函数。我们还创建了一个实用函数,对状态应用四十次unround以完全反转它:从下到上。
c
void recover_state(Slot_t &Output, Slot_t &Input) {
for (int32_t i = 39; i > -1