За­чем в крип­тоал­горит­мах нуж­ны «магичес­кие» кон­стан­ты и что будет, если их поменять? В этой статье мы пре­пари­руем прог­рам­мную защиту, исполь­зующую кас­томный поточ­ный шифр. С помощью x32dbg и IDA Pro мы вос­ста­новим алго­ритм, най­дем его кор­ни в извес­тном GSM-шиф­ре и раз­берем, как ошиб­ки в так­тирова­нии регис­тров при­водят к катас­тро­фичес­ким кол­лизи­ям. Ты уви­дишь, почему попыт­ки «улуч­шить» клас­сичес­кие крип­тоал­горит­мы прев­раща­ют защиту в тык­ву и как подоб­ные баги выяв­ляют­ся в ходе ревер­са.

Ес­ли ты решил стать хакером, то на сво­ем неп­ростом тер­нистом пути ты навер­няка разоб­рал не один стан­дар­тный крип­тоал­горитм. Да что там говорить, мы и вмес­те изу­чали алго­рит­мы Rabbit и EdDSA. Думаю, про­дира­ясь сквозь суровый матан (точ­нее, сквозь дис­крет­ную матема­тику) и теорию этих алго­рит­мов, ты задавал себе воп­рос: а зачем вооб­ще такие слож­ности? К чему эти уче­ные‑вер­ченые так намуд­рили, зачем в каж­дом алго­рит­ме имен­но такое количес­тво битов, так­тов, вол­шебных кон­стант, которые усложня­ют и замед­ляют выпол­нение кода, а глав­ное, поз­воля­ют его безоши­боч­но детек­тировать, к при­меру с помощью Krypto Analyzer? Почему нель­зя замутить свой собс­твен­ный уни­каль­ный крип­тоал­горитм име­ни себя, ска­жем поменяв зна­чения кон­стант, упростив для ско­рос­ти мик­широва­ние регис­тров или умень­шив либо уве­личив их бит­ность? Ведь на выходе крип­тора все рав­но будет высоко­энтро­пий­ная каша, и пря­мой брут­форс зай­мет срок величи­ной с десяток воз­растов Все­лен­ной. В этой статье мы на кон­крет­ном при­мере раз­берем, чем чре­вата подоб­ная самоде­ятель­ность в сос­тавле­нии крип­тоал­горит­ма.

Итак, задача: у нас име­ется некий софт от очень извес­тной телеком­муника­цион­ной фир­мы, наз­вание которой я не буду упо­минать. Софт офор­млен в виде локаль­ного веб‑при­ложе­ния с интерфей­сом в бра­узе­ре (мы раз­бирали подоб­ное в статье «В обход стра­жи. Отла­жива­ем код на PHP, упа­кован­ный SourceGuardian»). Лицен­зирова­ние прог­раммы про­исхо­дит тут же, в бра­узе­ре, путем заг­рузки лицен­зион­ного фай­ла. Он при­вязан к кон­крет­ному компь­юте­ру поль­зовате­ля (точ­нее, к серий­ному номеру прог­раммы, который отсы­лает­ся вен­дору для получе­ния лицен­зион­ного фай­ла). Файл набит высоко­энтро­пий­ными бинар­ными дан­ными, что натал­кива­ет на мысль: он зак­рипто­ван, при­чем ключ рас­шифров­ки как‑то свя­зан с серий­ником. Поэто­му наша задача — ревер­сировать и, по воз­можнос­ти, обра­тить алго­ритм шиф­рования (исклю­читель­но в иссле­дова­тель­ских целях).

Не буду углублять­ся в осо­бен­ности реали­зации бра­узер­ного веб‑при­ложе­ния: мы с такими уже стал­кивались, да и вооб­ще статья не об этом. В двух сло­вах отме­чу, что для работы мы исполь­зуем отладчик x32dbg, которым атта­чим­ся к при­ложе­нию, реали­зующе­му обра­бот­ку зап­росов к localhost, затем прос­тым поис­ком в памяти стро­ки «Cannot open license file» лег­ко выходим на код откры­тия лицен­зион­ного фай­ла и чте­ния из него зашиф­рован­ных дан­ных.

Даль­ше дело тех­ники: ста­вим точ­ку оста­нова на этот код и смот­рим, в какой момент высоко­энтро­пий­ная каша в буфере чте­ния из фай­ла прев­ратит­ся во что‑либо более осмыслен­ное. Дол­го ждать не при­ходит­ся: бук­валь­но нес­коль­кими стро­ками ниже про­исхо­дит рас­шифров­ка.

Как видишь, пос­ле двух пос­ледова­тель­ных вызовов sub_10002450(v287, v11) и sub_10002770(v287, (char *)lpBuffer, cchWideChar) буфер lpBuffer содер­жит тек­сто­вые дан­ные лицен­зии, которые уже сле­дующей стро­кой начина­ют ана­лизи­ровать­ся. Пос­мотрим, что внут­ри этих фун­кций. Нач­нем с sub_10002450. Ее IDA’шный псев­докод выг­лядит при­мер­но вот так:

void __thiscall sub_10002450(OLECHAR ***this, OLECHAR *psz) // this родительский объект криптора, pcz текстовая строка, ключ расшифровки
{
v2 = psz;
lpWideCharStr = psz;
if ( psz && wcslen(psz) )
{
sub_10001B10(this + 1, psz);
memset(this + 2, 0, 0x100u);
this[66] = 0;
this[67] = (OLECHAR **)324508639;
this[68] = (OLECHAR **)610839776;
this[69] = (OLECHAR **)-38177487;
this[70] = (OLECHAR **)-2147483550;
this[71] = (OLECHAR **)1073741856;
this[72] = (OLECHAR **)268435458;
this[73] = (OLECHAR **)0x7FFFFFFF;
this[74] = (OLECHAR **)0x3FFFFFFF;
this[75] = (OLECHAR **)0xFFFFFFF;
this[76] = (OLECHAR **)0x80000000;
this[77] = (OLECHAR **)-1073741824;
this[78] = (OLECHAR **)-268435456;
... // копирование первых 12 байт ключа pcz в массив v24 с нулевым байтом-заполнителем
v18 = v24[3] | ((v24[2] | ((v24[1] | (v24[0] << 8)) << 8)) << 8);
v19 = v24[7] | ((v24[6] | ((v24[5] | (v24[4] << 8)) << 8)) << 8);
v20 = v24[9] | (v24[8] << 8);
this[68] = (OLECHAR **)v19;
v21 = v24[11] | ((v24[10] | (v20 << 8)) << 8);
this[69] = (OLECHAR **)v21;
if ( !v18 )
v18 = 324508639;
this[67] = (OLECHAR **)v18;
if ( !v19 )
this[68] = (OLECHAR **)610839776;
if ( !v21 )
this[69] = (OLECHAR **)-38177487;
}
}

По­хоже на клас­сичес­кую ини­циали­зацию объ­екта крип­тора, с которой мы не раз стал­кивались при ана­лизе дру­гих крип­тоал­горит­мов. Стро­ки this[66] — this[78] — это явно сло­во сос­тояния алго­рит­ма, сос­тоящее из 13 32-бит­ных перемен­ных, ини­циали­зиру­ющих­ся шес­тнад­цатерич­ными кон­стан­тами 0, 13579BDF, 2468ACE0, FDB97531, 80000062, 40000020, 10000002, 7FFFFFFF, 3FFFFFFF, FFFFFFF, 80000000, C0000000 и F0000000. Затем вто­рая, третья и чет­вертая перемен­ные сос­тояния пере­ини­циали­зиру­ются 12 пер­выми бай­тами клю­ча.

Как выг­лядит сам ключ, мы уже успе­ли под­смот­реть в отладчи­ке. Это слеп­ленные вмес­те стро­ки: три десятич­ные циф­ры вер­сии про­дук­та, три десятич­ные циф­ры под­версии и его серий­ный номер. Нап­ример, для име­ющей­ся у нас вер­сии 5.15 ключ будет 0050150900-0, а соот­ветс­тву­ющие ему зна­чения перемен­ных — 30303530, 31353039 и 30302d30.

Не буду заг­ромож­дать повес­тво­вание излишни­ми под­робнос­тями перево­да псев­докода фун­кции sub_10002770 на C# с сок­ращени­ем, ска­жу толь­ко, что при этом выяс­няет­ся любопыт­ный факт: реаль­ное сло­во сос­тояния крип­тора сок­раща­ется до этих самых ини­циали­зиру­емых бай­тами клю­ча трех перемен­ных (назовем их условно state67, state68 и state69), осталь­ные ини­циали­зиро­ван­ные перемен­ные — прос­то кон­стан­ты, не меня­ющиеся во вре­мя шагов алго­рит­ма:

for (int i = 0; i < data.Length; ++i)
{
UInt32 v4 = 8;
UInt32 v5 = state67;
byte v13 = (byte)(state68 & 1);
UInt32 v15 = data[i];
UInt32 v16 = 0;
byte v14 = (byte)(state69 & 1);
UInt32 v6;
UInt32 v7;
byte v8;
UInt32 v9;
UInt32 v10;
UInt32 v11;
do
{
if ((v5 & 1) != 0)
{
v6 = state68;
v5 = 0x80000000 | 0x40000031 ^ v5;
if ((v6 & 1) != 0)
{
v13 = 1;
v7 = 0xc0000000 | v6 ^ 0x20000010;
v8 = v14;
}
else
{
v13 = 0;
v8 = v14;
v7 = 0x3fffffff & (state68 >> 1);
}
state68 = v7;
}
else
{
v9 = state69;
v5 = 0x7fffffff & (v5 >> 1);
if ((v9 & 1) != 0)
{
v10 = v9 ^ 0x8000001;
v8 = 1;
state69 = 0xf0000000 | v10;
}
else
{
state69 = 0x0fffffff & (v9 >> 1);
v8 = 0;
}
v14 = v8;
}
v11 = (UInt32 )(v8 ^ v13 | ( v16<<1));
v16 = v11;
--v4;
}
while (v4!=0);
state67 = v5;
data[i] = (byte)(v15 ^ v11);
}

Ал­горитм с виду дос­таточ­но прос­той, но Krypto Analyzer его не детек­тиру­ет, и исполь­зуемые им кон­стан­ты ниг­де не гуг­лятся. По мод­ным ныне веяниям, нат­равив на код искусс­твен­ный интеллект, получа­ем малов­разуми­тель­ные ссыл­ки на оте­чес­твен­ный ГОСТ Р (или даже «Куз­нечик») и на пер­воначаль­ную вер­сию поточ­ного алго­рит­ма шиф­рования евро­пей­ско­го мобиль­ного GSM-тра­фика под наз­вани­ем A5/1. Пос­ледний вари­ант выг­лядит доволь­но убе­дитель­но для соф­та от ком­пании, занима­ющей­ся мобиль­ной связью. Окей, при­мем на веру.

Продолжение доступно только участникам

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии