Cryptohack. Решение Flipping Cookie, Lazy CBC

Приветствую, Хабр! В новой статье я продолжаю рассказывать о слабых местах режима шифрования CBC и разбираю ещё парочку задач на CBC с Cryptohack. Конкретно сегодня поговорим о том почему использование ключа в качестве инициализирующего вектора может быть плохой идеей и ещё раз посмотрим на трюк из предыдущей статьи, где мы манипулировали шифротекстом чтобы изменить расшифрованный текст. Дабы сэкономить время, в данной статье я не буду возвращаться к описанию работы режима CBC и заново объяснять то, что, как я считаю, я достаточно подробно разобрал в предыдущей статье. Если в какой-то момент чтения вы обнаружите, что не понимаете о чём идёт речь, я советую обратиться к моим более старым публикациям. Если и после прочтения предыдущих публикаций ничего не понятно — пишите в комментарии, будем разбираться :)

Bit Flipping Attack

Итак, сначала мы вернёмся к теме предыдущей статьи. Я напомню, что там мы атаковали систему используя тот факт, что заменяя байты в зашифрованном тексте в блоке N — 1 мы можем предсказуемо повлиять на расшифровку блока N. Я это изображал на вот такой схеме:

f3d9fc0fdfefd9ae1aa255697a743c7b.png

Там я показывал как можно «угадывать» байты открытого текста, если CBC работает в связке c PKCS7. Но фундаментально уязвимость в том, что манипулируя зашифрованными данными мы можем предсказуемо управлять байтами открытого текста. Атаки, которые используют эту уязвимость часто обобщённо называют Bit (или Byte) Flipping Attack. Именно её мы по сути и проводили.

Сейчас рассмотрим немного другую ситуацию — теперь мы знаем конкретные значения расшифрованного текста (или по крайней мере его части) и хотим его изменить. Принципы от этого не меняются. Пусть у нас есть блок шифротекста C, который расшифровывается в текст P. Но мы хотим получить значение P'. Ну тогда давайте посчитаем значение \Delta

\Delta = P \oplus P'

Тогда чтобы C расшифровывался в P' достаточно применить к предыдущему блоку такую же разность. Для простоты записи пусть C — единственный блок текста, перед ним есть только IV. Значит нужно заменить IV на IV'

IV' = IV \oplus \Delta = IV \oplus P \oplus P'

Собственно по теории на этом всё, давайте к практике.

Задача

Разберём задачу с Cryptohack под названием Flipping Cookie (название как бы намекает). Условие следующее: мы можем взаимодействовать с сервером, у которого вот такой исходный код:

from Crypto.Cipher import AES
import os
from Crypto.Util.Padding import pad, unpad
from datetime import datetime, timedelta


KEY = ?
FLAG = ?


@chal.route('/flipping_cookie/check_admin///')
def check_admin(cookie, iv):
    cookie = bytes.fromhex(cookie)
    iv = bytes.fromhex(iv)

    try:
        cipher = AES.new(KEY, AES.MODE_CBC, iv)
        decrypted = cipher.decrypt(cookie)
        unpadded = unpad(decrypted, 16)
    except ValueError as e:
        return {"error": str(e)}

    if b"admin=True" in unpadded.split(b";"):
        return {"flag": FLAG}
    else:
        return {"error": "Only admin can read the flag"}


@chal.route('/flipping_cookie/get_cookie/')
def get_cookie():
    expires_at = (datetime.today() + timedelta(days=1)).strftime("%s")
    cookie = f"admin=False;expiry={expires_at}".encode()

    iv = os.urandom(16)
    padded = pad(cookie, 16)
    cipher = AES.new(KEY, AES.MODE_CBC, iv)
    encrypted = cipher.encrypt(padded)
    ciphertext = iv.hex() + encrypted.hex()

    return {"cookie": ciphertext}

Из кода видим, что сервер умеет выполнять два действия:

1) get_cookie() — эта функция зашифровывает сообщение cookie и возвращает нам.

2) check_admin(cookie, iv) — расшифровывает переданное сообщение cookie и проверяет есть ли в расшифрованном сообщение значение admin=True и в таком случае возвращает нам флаг, а в противном случае отдаёт ошибку.

Ну то есть очевидно, что в зашифрованном сообщении у нас записано False, а мы хотим чтобы было True. Заметьте, что в слове False больше букв, чем в True, поэтому мы будем заменять False на True;. Тогда начало расшифрованного текста изменится вот так:

admin=False;expiry=admin=True;;expiry=

Лишняя точка с запятой не будет помехой, т.к. после расшифровке делается сплит по ней и изменится только количество элементов в массиве, то есть unpadded.split(b";") отдаст не ["admin=False", "expiry="], а ["admin=True", "", "expiry="].

А чтобы провести замену следуем вышеприведенным формулам.

  1. Переводим admin=False и admin=True; в хекс (для этого на странице с заданием есть инструмент). Получаем 61646d696e3d46616c7365 и 61646d696e3d547275653b соответственно.

  2. Дальше ксорим их между собой (для этого тоже есть инструмент). Получаем значение 000000000000121319165e, и добавляем в конец ещё 5 нулевых байт, чтобы длина была равна 16, это наша \Delta

  3. Используем get_cookie() для получения зашифрованного текста.

  4. Отрезаем первые 16 байт полученного значения, это IV. Ксорим IV и \Delta, это IV'.

  5. В check_admin() передаём зашифрованный текст (то, что осталось когда отрезали IV) и IV'

  6. Флаг получен!

Lazy CBC

А сейчас мы посмотрим что будет если полениться и использовать вместо инициализирующего вектора ключ шифрования. Ну действительно, удобно же — не передавать IV каждый раз, а просто использовать ключ. Кажется, что ничего плохого произойти не может. Ключ всё ещё никто не знает, а мы сэкономили себе 16 байт трафика и усилия по генерации вектора каждый раз.

Однако, как минимум, инициализирующий вектор нужно менять при каждой новой сессии, т.к. их повторение — это нехорошо. Но сегодня я на этом останавливаться не буду. Гораздо большая проблема возникает, когда в системе есть возможность шифровать и расшифровывать сообщения. Да, задача, которую я сейчас разберу будет выглядеть как сферический конь в вакууме (таких систем якобы не существует). Но я напоминаю, что задача — это простая модель. В сложных системах бывают чудеса и не такого уровня.

Задача

Как обычно, есть код сервера:

from Crypto.Cipher import AES


KEY = ?
FLAG = ?


@chal.route('/lazy_cbc/encrypt//')
def encrypt(plaintext):
    plaintext = bytes.fromhex(plaintext)
    if len(plaintext) % 16 != 0:
        return {"error": "Data length must be multiple of 16"}

    cipher = AES.new(KEY, AES.MODE_CBC, KEY)
    encrypted = cipher.encrypt(plaintext)

    return {"ciphertext": encrypted.hex()}


@chal.route('/lazy_cbc/get_flag/<key>/')
def get_flag(key):
    key = bytes.fromhex(key)

    if key == KEY:
        return {"plaintext": FLAG.encode().hex()}
    else:
        return {"error": "invalid key"}


@chal.route('/lazy_cbc/receive/<ciphertext>/')
def receive(ciphertext):
    ciphertext = bytes.fromhex(ciphertext)
    if len(ciphertext) % 16 != 0:
        return {"error": "Data length must be multiple of 16"}

    cipher = AES.new(KEY, AES.MODE_CBC, KEY)
    decrypted = cipher.decrypt(ciphertext)

    try:
        decrypted.decode() # ensure plaintext is valid ascii
    except UnicodeDecodeError:
        return {"error": "Invalid plaintext: " + decrypted.hex()}

    return {"success": "Your message has been received"}</code></pre>

<p>Функции у&nbsp;него такие: </p>

<ol><li><p><code>encrypt()</code>&nbsp;— зашифровывает произвольный текст с&nbsp;помощью AES в&nbsp;режиме CBC</p></li><li><p><code>receive()</code>&nbsp;— принимает зашифрованный текст, расшифровывает его и&nbsp;проверяет что&nbsp;его можно декодировать в&nbsp;юникод. Если нельзя, то&nbsp;возвращает ошибку, которая включает расшифрованный текст. А&nbsp;если декодирование проходит успешно, то&nbsp;возвращает сообщение об&nbsp;успехе.</p></li><li><p><code>get_flag()</code>&nbsp;— принимает ключ, и&nbsp;если этот ключ равен ключу шифрования сервера, то&nbsp;возвращает флаг.</p></li></ol>

<p>В&nbsp;общем, чтобы получить флаг надо&nbsp;найти ключ шифрования. А&nbsp;как&nbsp;получить ключ шифрования сейчас посмотрим что&nbsp;можно сделать. Самая важная деталь в&nbsp;задаче&nbsp;— при&nbsp;шифровании ключ используется в&nbsp;качестве инициализирующего вектора и&nbsp;этим будем пользоваться. Алгоритм атаки такой: </p>

<ol><li><p>зашифровываем 3&nbsp;блока текста такого, который нельзя декодировать в&nbsp;юникод. Это нужно, чтобы функция <code>receive()</code> точно вернула нам расшифрованный текст.</p></li><li><p>второй блок зашифровнного текста заменяем на&nbsp;нули, а&nbsp;третий блок меняем на&nbsp;первый. Вместо <img alt="C_1,C_2,C_3" src="https://habrastorage.org/getpro/habr/upload_files/556/fdf/fea/556fdffeacbbff3609af736044ab1429.svg" /> получаем <img alt="C_1,0,C_1" src="https://habrastorage.org/getpro/habr/upload_files/897/dc6/3d7/897dc63d7159d983d71aadabd11ef9b3.svg" /></p></li><li><p>отдаём это в&nbsp;<code>receive()</code> и&nbsp;получаем расшифрованный текст.</p></li><li><p>ксорим первый и&nbsp;третий блоки расшифрованного текста, чтобы получить ключ</p></li></ol>

<p>Чтобы понять как&nbsp;это работает посмотрите на&nbsp;схему. Первый блок после расшифровки AES суммируется с&nbsp;ключом, то&nbsp;есть <img alt="P_1 = D(C_1) \oplus K" src="https://habrastorage.org/getpro/habr/upload_files/3cc/a7e/5a4/3cca7e5a428b5f1d1c001747d130eae5.svg" />, на&nbsp;схеме <img alt="D(C_1)" src="https://habrastorage.org/getpro/habr/upload_files/801/8a9/01d/8018a901d30240f0f0d9a2a9de3f8787.svg" /> я&nbsp;обозначаю как&nbsp;T. Так как&nbsp;третий блок мы заменили на&nbsp;первый, он тоже после расшифровки AES будет равен T. Однако ксорится он будет уже не&nbsp;с&nbsp;ключом, а&nbsp;со&nbsp;вторым блоком. Но&nbsp;второй блок мы заменили на&nbsp;нули, значит при&nbsp;расшифровке третьего блока мы получим <img alt="P_1' = D(C_1) \oplus 0 = D(C_1)" src="https://habrastorage.org/getpro/habr/upload_files/ad5/aa6/345/ad5aa6345f7fcf7bcd87276e6bba4f7b.svg" />. Ну и&nbsp;дальше вычисление ключа должно быть очевидным <img alt="P_1 \oplus P_1' = D(C_1) \oplus K \oplus D(C_1) = K" src="https://habrastorage.org/getpro/habr/upload_files/148/fb6/f22/148fb6f22b88e555261c9f57d030c184.svg" /></p>

<p><img src="https://habrastorage.org/r/w1560/getpro/habr/upload_files/7fc/6b3/0b1/7fc6b30b1c2b470596f09ff578762d7a.png" alt="7fc6b30b1c2b470596f09ff578762d7a.png" /></p>

<h2>Заключение</h2>

<p>Вот и&nbsp;всё, что&nbsp;я&nbsp;хотел рассказать сегодня. Задачи оказались настолько просты, что&nbsp;нам не&nbsp;пришлось даже писать код для&nbsp;решения. Оставляйте свои комментарии, вопросы, если что-то&nbsp;осталось непонятным, и&nbsp;stay tuned for more:)</p>
    
            <p class="copyrights"><span class="source">&copy;&nbsp;<a target="_blank" rel="nofollow" href="https://habr.com/ru/articles/868736/?utm_source=habrahabr&amp;amp;utm_medium=rss&amp;amp;utm_campaign=868736">Habrahabr.ru</a></span></p>
                    </div>
                                                    
            <br>
            <!--<div align="left">
                <script type="text/topadvert">
                load_event: page_load
                feed_id: 12105
                pattern_id: 8187
                tech_model:
                </script><script type="text/javascript" charset="utf-8" defer="defer" async="async" src="//loader.topadvert.ru/load.js"></script>
            </div>
            <br>-->

            <div style="padding-left: 20px;">
                <script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-2514821055276660"
                        crossorigin="anonymous"></script>
                <!-- PCNews 336x280 -->
                <ins class="adsbygoogle"
                     style="display:block"
                     data-ad-client="ca-pub-2514821055276660"
                     data-ad-slot="1200562049"
                     data-ad-format="auto"></ins>
                <script>
                    (adsbygoogle = window.adsbygoogle || []).push({});
                </script>
            </div>
            <!-- comments -->
                            <noindex>
                    <div style="margin: 25px;" id="disqus_thread"></div>
                    <script type="text/javascript">
                        var disqus_shortname = 'pcnewsru';
                        var disqus_identifier = '1579768';
                        var disqus_title = 'Cryptohack. Решение Flipping Cookie, Lazy CBC';
                        var disqus_url = 'http://pcnews.ru/blogs/cryptohack_resenie_flipping_cookie_lazy_cbc-1579768.html';

                        (function() {
                            var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
                            dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
                            (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
                        })();
                    </script>
                    <!--<noscript>Please enable JavaScript to view the <a rel="nofollow" href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>-->
                    <!--<a href="http://disqus.com" rel="nofollow" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>-->
                </noindex>
            
        </div>

        <br class="clearer"/>
    </div>
    <br class="clearer"/>

    

        <div id="footer-2nd"></div>

        <div id="footer">
            <br/><br/>
            <ul class="horz-menu">
                <li class="about"><a href="/info/about.html" title="О проекте">О
                        проекте</a></li>
                <li class="additional-menu"><a href="/archive.html" title="Архив материалов">Архив</a>
                </li>
                <li class="additional-menu"><a href="/info/reklama.html"
                                               title="Реклама" class="menu-item"><strong>Реклама</strong></a>
                    <a href="/info/partners.html" title="Партнёры"
                       class="menu-item">Партнёры</a>
                    <a href="/info/legal.html" title="Правовая информация"
                       class="menu-item">Правовая информация</a>
                    <a href="/info/contacts.html" title="Контакты"
                       class="menu-item">Контакты</a>
                    <a href="/feedback.html" title="Обратная связь" class="menu-item">Обратная
                        связь</a></li>
                <li class="email"><a href="mailto:pcnews@pcnews.ru" title="Пишите нам на pcnews@pcnews.ru"><img
                                src="/media/i/email.gif" alt="e-mail"/></a></li>
                <li style="visibility: hidden">
                    <noindex>
                        <!-- Rating@Mail.ru counter -->
                        <script type="text/javascript">
                            var _tmr = window._tmr || (window._tmr = []);
                            _tmr.push({id: "93125", type: "pageView", start: (new Date()).getTime()});
                            (function (d, w, id) {
                                if (d.getElementById(id)) return;
                                var ts = d.createElement("script");
                                ts.type = "text/javascript";
                                ts.async = true;
                                ts.id = id;
                                ts.src = (d.location.protocol == "https:" ? "https:" : "http:") + "//top-fwz1.mail.ru/js/code.js";
                                var f = function () {
                                    var s = d.getElementsByTagName("script")[0];
                                    s.parentNode.insertBefore(ts, s);
                                };
                                if (w.opera == "[object Opera]") {
                                    d.addEventListener("DOMContentLoaded", f, false);
                                } else {
                                    f();
                                }
                            })(document, window, "topmailru-code");
                        </script>
                        <noscript>
                            <div style="position:absolute;left:-10000px;">
                                <img src="//top-fwz1.mail.ru/counter?id=93125;js=na" style="border:0;" height="1"
                                     width="1" alt="Рейтинг@Mail.ru"/>
                            </div>
                        </noscript>
                        <!-- //Rating@Mail.ru counter -->

                    </noindex>
                </li>
            </ul>
        </div>

        <!--[if lte IE 7]>
        <iframe id="popup-iframe" frameborder="0" scrolling="no"></iframe>
        <![endif]-->
        <!--<div id="robot-image"><img class="rbimg" src="i/robot-img.png" alt="" width="182" height="305" /></div>-->
        <!--[if IE 6]>
        <script>DD_belatedPNG.fix('#robot-image, .rbimg');</script><![endif]-->

    </div>

<!--[if lte IE 7]>
<iframe id="ie-popup-iframe" frameborder="0" scrolling="no"></iframe>
<![endif]-->


    <div id="footer-adlinks"></div>

    
    
    
        <noindex>


            <!--LiveInternet counter--><script type="text/javascript">
                document.write("<a rel='nofollow' href='//www.liveinternet.ru/click' "+
                    "target=_blank><img src='//counter.yadro.ru/hit?t45.6;r"+
                    escape(document.referrer)+((typeof(screen)=="undefined")?"":
                        ";s"+screen.width+"*"+screen.height+"*"+(screen.colorDepth?
                            screen.colorDepth:screen.pixelDepth))+";u"+escape(document.URL)+
                    ";"+Math.random()+
                    "' alt='' title='LiveInternet' "+
                    "border='0' width='1' height='1'><\/a>")
            </script><!--/LiveInternet-->

            <!-- Rating@Mail.ru counter -->
            <script type="text/javascript">
                var _tmr = window._tmr || (window._tmr = []);
                _tmr.push({id: "93125", type: "pageView", start: (new Date()).getTime()});
                (function (d, w, id) {
                    if (d.getElementById(id)) return;
                    var ts = d.createElement("script"); ts.type = "text/javascript"; ts.async = true; ts.id = id;
                    ts.src = "https://top-fwz1.mail.ru/js/code.js";
                    var f = function () {var s = d.getElementsByTagName("script")[0]; s.parentNode.insertBefore(ts, s);};
                    if (w.opera == "[object Opera]") { d.addEventListener("DOMContentLoaded", f, false); } else { f(); }
                })(document, window, "topmailru-code");
            </script><noscript><div>
                    <img src="https://top-fwz1.mail.ru/counter?id=93125;js=na" style="border:0;position:absolute;left:-9999px;" alt="Top.Mail.Ru" />
                </div></noscript>
            <!-- //Rating@Mail.ru counter -->



            <!-- Yandex.Metrika counter -->
            <script type="text/javascript">
                (function (d, w, c) {
                    (w[c] = w[c] || []).push(function () {
                        try {
                            w.yaCounter23235610 = new Ya.Metrika({
                                id: 23235610,
                                clickmap: true,
                                trackLinks: true,
                                accurateTrackBounce: true,
                                webvisor: true,
                                trackHash: true
                            });
                        } catch (e) {
                        }
                    });

                    var n = d.getElementsByTagName("script")[0],
                        s = d.createElement("script"),
                        f = function () {
                            n.parentNode.insertBefore(s, n);
                        };
                    s.type = "text/javascript";
                    s.async = true;
                    s.src = "https://mc.yandex.ru/metrika/watch.js";

                    if (w.opera == "[object Opera]") {
                        d.addEventListener("DOMContentLoaded", f, false);
                    } else {
                        f();
                    }
                })(document, window, "yandex_metrika_callbacks");
            </script>
            <noscript>
                <div><img src="https://mc.yandex.ru/watch/23235610" style="position:absolute; left:-9999px;" alt=""/>
                </div>
            </noscript>
            <!-- /Yandex.Metrika counter -->

            <!-- Default Statcounter code for PCNews.ru http://pcnews.ru-->
            <script type="text/javascript">
                var sc_project=9446204;
                var sc_invisible=1;
                var sc_security="14d6509a";
            </script>
            <script type="text/javascript"
                    src="https://www.statcounter.com/counter/counter.js"
                    async></script>
            <!-- End of Statcounter Code -->

            <script>
                (function (i, s, o, g, r, a, m) {
                    i['GoogleAnalyticsObject'] = r;
                    i[r] = i[r] || function () {
                            (i[r].q = i[r].q || []).push(arguments)
                        }, i[r].l = 1 * new Date();
                    a = s.createElement(o),
                        m = s.getElementsByTagName(o)[0];
                    a.async = 1;
                    a.src = g;
                    m.parentNode.insertBefore(a, m)
                })(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');

                ga('create', 'UA-46280051-1', 'pcnews.ru');
                ga('send', 'pageview');

            </script>

            <script async="async" src="/assets/uptolike.js?pid=49295"></script>

        </noindex>
    



<!--<div id="AdwolfBanner40x200_842695" ></div>-->
<!--AdWolf Asynchronous Code Start -->

<script type="text/javascript" src="https://pcnews.ru/js/blockAdblock.js"></script>

<script type="text/javascript" src="/assets/jquery.min.js"></script>
<script type="text/javascript" src="/assets/a70a9c7f/jquery/jquery.json.js"></script>
<script type="text/javascript" src="/assets/a70a9c7f/jquery/jquery.form.js"></script>
<script type="text/javascript" src="/assets/a70a9c7f/jquery/jquery.easing.1.2.js"></script>
<script type="text/javascript" src="/assets/a70a9c7f/jquery/effects.core.js"></script>
<script type="text/javascript" src="/assets/a70a9c7f/js/browser-sniff.js"></script>
<script type="text/javascript" src="/assets/a70a9c7f/js/scripts.js"></script>
<script type="text/javascript" src="/assets/a70a9c7f/js/pcnews-utils.js"></script>
<script type="text/javascript" src="/assets/a70a9c7f/js/pcnews-auth.js"></script>
<script type="text/javascript" src="/assets/a70a9c7f/js/pcnews-fiximg.js"></script>
<script type="text/javascript" src="/assets/a70a9c7f/js/pcnews-infobox.js"></script>
</body>
</html>