Прочитав новость о том, что Центризбирком РФ выложил результаты выборов на своем сайте в обфусцированном виде, многие начали публиковать в комментариях свои варианты деобфускаторов, как с использованием OCR, так и без него. Но я подумал, что есть более первостепенная задача — а именно выгрузка и сохранение данных с сайта ЦИК, так как они могут в любой момент измениться, и никто этого не заметит.

Кому интересны только сырые обфусцированные данные, архив с ними можно скачать здесь (внимание: в распакованном виде файлы занимают 11 ГБ). А кому интересно как я их получил, и какие методы обфускации в них применяются — добро пожаловать под кат.

Я нашел crawler на основе Selenium на GitHub, но так как мой основной язык — Swift, то я сделал решение на нём.

В первую очередь необходимо получить список УИК. Это легко сделать, посмотрев, какой запрос отправляет браузер при раскрытии дерева на сайте. После этого, посмотрев id корневого узла (параметр tvd) в URL страницы http://www.izbirkom.ru/region/izbirkom?action=show&root=0&tvd=100100225883177&vrn=100100225883172&prver=0&pronetvd=null&region=0&sub_region=0&type=242&report_mode=null, мы можем рекурсивно получить всех его потомков.

В результате у нас получается список из примерно ста тысяч URL, которые нам нужно скачать. Здесь возникают две проблемы. Первая — сервер очень медленно отдает страницы, и вторая — CAPTCHA. Опытным путем было установлено, что без проблем можно скачивать в 10 потоков с одного IP-адреса, при этом на получение всех данных ушло примерно часов 12. А для решения CAPTCHA в macOS есть готовое решение под названием Vision.framework. Всего в несколько строк кода мы можем с высокой точностью распознать символы на изображении. Дополнительно нам облегчает задачу тот факт, что на изображении всегда 5 символов и используются только цифры.

CAPTCHA с сайта ЦИК РФ
CAPTCHA с сайта ЦИК РФ
import Vision

extension Data {
    func solveCaptcha(handler: @escaping (String?) -> Void) {
        let requestHandler = VNImageRequestHandler(data: self, options: [:])
        let request = VNRecognizeTextRequest { (request, error) in
            let candidates = (request.results as? [VNRecognizedTextObservation])?
                .first?
                .topCandidates(10)
                .map {
                    String(
                        $0.string
                            .lowercased()
                            .replacingOccurrences(of: "o", with: "0")
                            .unicodeScalars
                            .filter { CharacterSet.decimalDigits.contains($0) }
                    )
                }
            handler(candidates?.first(where: { $0.count == 5 }))
        }
        try! requestHandler.perform([request])
    }
}

Итак, когда у нас скачаны все данные, можно спокойно проводить их анализ для дальнейшей деобфускации. Бегло просмотрев содержание HTML файлов, я заметил 3 метода, которые там применяются:

  • Подмена таблицы cmap в шрифтах. Данная таблица отвечает за соответствие глифов символам Unicode. Всего на сервере имеется ровно 100 заранее сгенерированных модификаций шрифта PT Sans с рандомизированной таблицей. При загрузке любой страницы сервер выбирает случайный шрифт, присваивает его некоторым элементам на странице и производит замену символов, чтобы глифы соответствовали отображаемому контенту. При этом индексы глифов в модифицированных шрифтах соответствуют оригинальному, что позволяет элементарно восстановить реальный текст.

  • Лишние элементы в DOM со случайным содержимым, скрытые при помощи CSS.

.jcie_gwkc .oqr_lda::after {
	content: '6';
	display: inline-block;
	overflow: hidden;
	height: 0px;
	width: 0px;
	opacity: 0;
}

.jcie_gwkc .dha_pso {
	position: absolute;
	top: -99999px;
	left: -999999px;
}
  • Отложенная модификация DOM при помощи Javascript. В дополнении к предыдущему методу, при загрузке страницы выполняется скрипт, который через 700 миллисекунд меняет содержимое и стили некоторых элементов, заменяя некоторые названия партий и отображая часть элементов, которые изначально были скрыты.

var njh_bqp = function(hv_hl, am_dm, pr_yw) {
    var hzq_hyc = pr_yw.getElementsByClassName(hv_hl);
    for (var i = 0; i < hzq_hyc.length; i++) {
        var v = hzq_hyc[i].innerHTML.split('');
        v.splice(am_dm, 1);
        hzq_hyc[i].innerHTML = v.join('');
    };
};
var awi_mbt = function(mz_cg, bn_wh, dr_hy) {
    var dfi_vmn = dr_hy.getElementsByTagName('td');
    var nx_rx = lec(dfi_vmn[mz_cg]);
    var xv_qx = lec(dfi_vmn[bn_wh]);
    var fn_gy = nx_rx.innerHTML;
    var gq_va = xv_qx.innerHTML;
    nx_rx.innerHTML = gq_va;
    xv_qx.innerHTML = fn_gy;
};
if (!lec) {
    var lec = function(a) {
        var b = a.lastElementChild;
        if (!b) return a;
        if (b.lastElementChild) return lec(b);
        return b;
    };
};;
var wfr_zfa = function(ak_mj, wp_jg, ym_sj) {
    var ske_fin = ym_sj.getElementsByClassName(ak_mj);
    for (var i = 0; i < ske_fin.length; i++) {
        ske_fin[i].innerHTML = wp_jg;
    };
};
var a = function() {
    var bfnv_bmdk = document.getElementsByClassName('bfnv_bmdk')[0];
    bfnv_bmdk.style.position = 'relative';
    setTimeout(function() {
        bfnv_bmdk.style.removeProperty('opacity');
        bfnv_bmdk.style.removeProperty('visibility');
    }, 700);
    var cqer_smez = document.getElementsByClassName('cqer_smez')[0];
    cqer_smez.style.position = 'relative';
    setTimeout(function() {
        cqer_smez.style.removeProperty('opacity');
        cqer_smez.style.removeProperty('visibility');
    }, 700);
    var vbwt_brne = document.getElementsByClassName('vbwt_brne')[0];
    vbwt_brne.style.position = 'relative';
    setTimeout(function() {
        vbwt_brne.style.removeProperty('opacity');
        vbwt_brne.style.removeProperty('visibility');
    }, 700);
    var lxdj_vpoq = document.getElementsByClassName('lxdj_vpoq')[0];
    lxdj_vpoq.style.position = 'relative';
    setTimeout(function() {
        lxdj_vpoq.style.removeProperty('opacity');
        lxdj_vpoq.style.removeProperty('visibility');
    }, 700);
    njh_bqp('kjz_etk', -1, lxdj_vpoq);
    njh_bqp('kjz_etk', 0, lxdj_vpoq);
    njh_bqp('mqi_vob', -1, lxdj_vpoq);
    njh_bqp('omx_tpy', -1, lxdj_vpoq);
    njh_bqp('omx_tpy', 0, lxdj_vpoq);
    njh_bqp('wjd_qyl', -1, lxdj_vpoq);
    njh_bqp('tzw_yva', -1, lxdj_vpoq);
    awi_mbt('6', '66', lxdj_vpoq);
    njh_bqp('ane_fsn', -1, lxdj_vpoq);
    njh_bqp('dpg_pkj', -1, lxdj_vpoq);
    njh_bqp('kbo_shr', -1, lxdj_vpoq);
    njh_bqp('vhq_iml', -1, lxdj_vpoq);
    njh_bqp('mgx_sza', -1, lxdj_vpoq);
    njh_bqp('rzb_vsq', -1, lxdj_vpoq);
    njh_bqp('phm_lgy', -1, lxdj_vpoq);
    njh_bqp('iyd_flo', -1, lxdj_vpoq);
    njh_bqp('qfj_tbx', -1, lxdj_vpoq);
    njh_bqp('fno_szj', -1, lxdj_vpoq);
    njh_bqp('dcv_rjz', -1, lxdj_vpoq);
    njh_bqp('dcv_rjz', 0, lxdj_vpoq);
    njh_bqp('bzp_raf', -1, lxdj_vpoq);
    njh_bqp('zpr_efy', -1, lxdj_vpoq);
    njh_bqp('jro_yht', -1, lxdj_vpoq);
    njh_bqp('lde_fby', -1, lxdj_vpoq);
    njh_bqp('lde_fby', 22, lxdj_vpoq);
    njh_bqp('jec_nmx', -1, lxdj_vpoq);
    njh_bqp('pni_raq', -1, lxdj_vpoq);
    njh_bqp('qfm_kai', -1, lxdj_vpoq);
    wfr_zfa('szh_wzj', '4. Политическая партия "НОВЫЕ ЛЮДИ"', lxdj_vpoq);
    njh_bqp('jpk_ywt', -1, lxdj_vpoq);
    njh_bqp('jpk_ywt', 0, lxdj_vpoq);
    njh_bqp('mws_mda', -1, lxdj_vpoq);
    njh_bqp('msz_rfh', -1, lxdj_vpoq);
    awi_mbt('49', '76', lxdj_vpoq);
    njh_bqp('opc_enx', -1, lxdj_vpoq);
    njh_bqp('opd_bel', -1, lxdj_vpoq);
    njh_bqp('opd_bel', 19, lxdj_vpoq);
    njh_bqp('bko_rsh', -1, lxdj_vpoq);
    njh_bqp('mbv_jos', -1, lxdj_vpoq);
    njh_bqp('jrq_ebc', -1, lxdj_vpoq);
    njh_bqp('jvw_sxq', -1, lxdj_vpoq);
    njh_bqp('awh_jbv', -1, lxdj_vpoq);
    njh_bqp('wia_vqw', -1, lxdj_vpoq);
    njh_bqp('xzs_wfz', -1, lxdj_vpoq);
    njh_bqp('gbc_eon', -1, lxdj_vpoq);
    njh_bqp('dmh_min', -1, lxdj_vpoq);
    wfr_zfa('mzf_xhk', '12. Политическая партия ЗЕЛЕНАЯ АЛЬТЕРНАТИВА', lxdj_vpoq);
    njh_bqp('knq_dpz', -1, lxdj_vpoq);
    njh_bqp('knq_dpz', 0, lxdj_vpoq);
    wfr_zfa('cxo_jct', '13. ВСЕРОССИЙСКАЯ ПОЛИТИЧЕСКАЯ ПАРТИЯ "РОДИНА"', lxdj_vpoq);
    njh_bqp('hkz_lnj', -1, lxdj_vpoq);
    njh_bqp('izn_cxg', -1, lxdj_vpoq);
    njh_bqp('izn_cxg', 0, lxdj_vpoq);
    var evxg_vkoj = document.getElementsByClassName('evxg_vkoj')[0];
    evxg_vkoj.style.position = 'relative';
    setTimeout(function() {
        evxg_vkoj.style.removeProperty('opacity');
        evxg_vkoj.style.removeProperty('visibility');
    }, 700);
};
document.addEventListener('DOMContentLoaded', a);

Исходный код для загрузки данных можно найти на GitHub. При наличии свободного времени, и если никто до этого не опубликует очищенные данные, напишу свой вариант деобфускатора и выложу все данные, сконвертированные во что-то удобное, к примеру CSV.

UPD1: @aulitin прогнал файлы через свой деобфускатор и выложил результат здесь

UPD2: @lifeair опубликовал код для конвертации из HTML в JSON

UPD3: нашел канал в телеграм, где выложены некоторые данные в xlsx из другого источника, в том числе по одномандатным округам. Также группа в Facebook c анализом результатов, вот пара интересных ссылок оттуда:

UPD4: @AlexShpilkin опубликовал описание по-английски и параллельный код деобфускатора на Питоне

UPD5: на сайте Новой Газеты появилось открытое письмо к Памфиловой на тему обфускации результатов выборов

Комментарии (33)


  1. romankonstant
    22.09.2021 19:19
    +27

    Возможно проблемы прозрачности выборов стоит решать как-то иначе :))) А статья прекрасная


    1. Drunik
      23.09.2021 15:42
      +3

      пока я не смогу проверить как учёлся именно мой голос - все эти блокчейн-бигдата чистая профанация. О какой приватности голосования идёт речь когда оно провдится мной в личном кабинете госуслуг где всё обо мне известно? Мой бюллетень с моими отметками нужно присылать мне на почту, и выдавать уникальный код по которому я всегда могу проверить правильность учёта своего голоса. Это как минимум - останется вопрос с недопущением электронного вброса левых бюллетеней неголосовавших людей. Тогда это будет хоть на что-то похоже.


      1. A_J
        23.09.2021 17:08
        +1

        Возможность проверки результата - палка о двух концах. Если можете проверить вы, то сможет и кто-то другой. В идеале - человек, которого погнали голосовать должен иметь возможность:
        1. Проверить как учтён его голос на самом деле
        2. Показать "проверяющему" любой желаемый результат


        1. Gengenid
          23.09.2021 17:20
          +2

          2 пункт лишний. Если у человека есть "проверяющий" - это уже никакая не демократия. Смысла нет в выборах вообще.


      1. Vinchi
        24.09.2021 12:06

        Ровно тоже самое слышал от многих


    1. selivanov_pavel
      24.09.2021 07:43

      Товарищ майор вас за экстремизм посадит за такие комментарии.


  1. kryvichh
    22.09.2021 19:53
    +1

    Криптовыборы?


    1. rashid-m
      25.09.2021 00:03

      у вас буква "т" лишняя


  1. Gengenid
    22.09.2021 20:55
    +14

    Вот как так можно: чисто первичные данные по УИКам занимают дай бог 20 МБ, к примеру, в csv. Но чтобы их получить, нужно сгенерировать и передать 11 ГБ. И это они называют защитой от атак.


    1. Mozhaiskiy
      22.09.2021 22:22
      +25

      Называть они это могут как угодно, задача же в другом: максимально затруднить проверку и контроль результатов, как минимум, в первые часы и дни, когда внезапные изменения могут иметь общественный резонанс.


      1. jorikdima
        22.09.2021 22:44
        +12

        О каком резонансе речь? Все все знают. Кто чё не видел?


        1. Buzzzzer
          23.09.2021 08:26
          +14

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


          1. max_mustermann
            23.09.2021 09:40
            -1

            Ну-ну, развейте вашу мысль. А теперь эти люди прочтут статью на хабре, скачают с Гитхаба скрипт, распарсят обфусцированные данные и... и пелена спадёт с глаз и они воскликнут "о боже, нас обманывают, данные подделаны" или что?


            1. Anonerror
              23.09.2021 10:58
              +1

              Далеко не все из "этих людей" вообще знают, что такое гитхаб) они больше телевизор смотрят


              1. purgenetik
                29.09.2021 17:32

                Там была табличка "Сарказм"


        1. Kanut
          23.09.2021 11:58
          +1

          А разве не так что официально оспаривать результаты выборов можно только в течении ограниченного интервала времени?


  1. aulitin
    22.09.2021 22:43
    +11

    Как я уже писал на github, в архиве не хватает результатов по одномандатным округам. Деобфускатор можно использовать мой https://github.com/ulex/izbirkom21, в нем есть какие-то известные проблемы?


  1. aulitin
    22.09.2021 22:58
    +5

    Деобфусцировал и выложил ваши данные
    https://github.com/ulex/izbirkom21/releases/tag/V1.00


    Но если бы я занимался анализом результатов, то я бы их не использовал. Решение с отрытым кодом можно проверить и убедиться, что все в порядке, а проверить Ваши данные нельзя.


    1. apple01
      23.09.2021 03:18

      Я пытался скачать out.zip, но при деархивации выходит ошибка «unexpected end of archive».


      1. aulitin
        23.09.2021 03:25
        +1

        скачал с github, хэш сходится, 7-zip test проходит без ошибок
        SHA256: 96F624DD90E343DEA088E5647EC462818A4D9F1501F15EF78D5C3AE96A541AC7
        на github есть вариант в 7z архиве, который сжался в 10 раз компактнее и уже распаршенный sqlite


    1. illusionofchaos Автор
      23.09.2021 14:55
      +2

      Так я же опубликовал на GitHub исходный код, который я использовал для выкачивания, нужен только мак и около 12 часов, чтобы все выгрузить еще раз. Если будете запускать, советую пользоваться мобильным интернетом или мобильным прокси, чтобы снизить вероятность бана по IP.

      Код писал на скорую руку, просто чтобы один раз запустить, но перед загрузкой на GitHub разделил его на несколько executable по шагам:

      1. Step1DownloadUikTree - получает дерево всех страниц и сохраняет в root.json

      2. Step2ConvertUikTreeToUrlList - конвертирует root.json в urls.csv со строками в формате id;url

      3. Step3DownloadHtml - считывает все URL из urls.csv, для каждого проверяет наличие файла с таким именем в папке html, при отсутствии скачивает, используя 10 потоков и распознавая капчу при необходимости

      4. Step4DownloadFonts - проходит по содержимому всех файлов из папки html, при помощи поиска по регулярному выражению fonts1/[^\.]+\.otf находит URL шрифтов, далее сохраняет каждый шрифт в папку html/fonts1, предварительно проверяя отсутствие файла с таким именем


  1. meet2code
    23.09.2021 00:09
    +16

    Как мне радостно видеть такие статьи на хабре. И аналитикам полезно, и просто по-человечески приятно, что не дают легко изворачиваться хитрецам, загоняя их всё дальше и дальше в угол.


    1. Goupil
      23.09.2021 08:58
      +10

      Я тоже это поддерживаю, но потом из России хабр придется читать через тор.


      1. simpleadmin
        23.09.2021 09:25
        +2

        Читать Хабр через tor или просто европейские прокси уже почти нереально. Постоянные "Connection refused" - вероятно защита от DDoS совсем испортилась.

        Пока спасают RU-прокси, но их "осталось на 3 дня" ...


    1. Kuzyok777
      23.09.2021 09:07
      +2

      В угол? Интересно, как вы себе представляете момент, когда зогоните в угол? :)


  1. avshukan
    23.09.2021 09:56
    -1

    Плюс в карму!


  1. alvaz
    23.09.2021 10:37
    +3

    Ну если честно, по человечески завидую людям которые готовы тратить свое время и ресурсы на эту работу. Только за сегодняшнее утро пришло две ссылки на подобные материалы на Хабре. Впрочем чего это я, тоже ведь трачу свое время на чтение и коментарий. Пойду лучше делом займусь.


  1. x2v0
    23.09.2021 13:14

    Это просто праздник какой-то ... За одни сутки столько всего узнал об искусстве парсинга.

    Как спарсить любой сайт?


  1. kirilldemidov
    23.09.2021 15:42

    Было бы очень здорово получить очищенные данные!


  1. AlexShpilkin
    23.09.2021 15:42
    +1

    Одна поправка — CSS generated content может быть не только невидимый, но и видимый тоже.

    Надеялся, что эта история ещё немножко поживёт вне открытых источников и вся эта гонка вооружений ещё немножко постоит на месте, но не судьба, похоже. Впрочем, вся эта обфускация безусловно совершенный nerd-sniping.

    Коли такое дело, вот ещё тогда моё описание по-английски и параллельный код деобфускатора на Питоне: https://purl.org/cikrf/un/unfuck.py.html (ссылки на только скрипт и историю изменений внутри).


    1. aulitin
      23.09.2021 23:47
      +1

      Очень странная надежда. Если под гонкой вооружений Вы имеете в виду технические меры, то это заведомо проигрышная позиция для ЦИК-а: любую обфускацию можно обойти. Это уже проходили на примере телеграма. И чем сложнее, тем больше нердов будет этим заниматься. Запишите мой контакт — я готов безвозмездно помогать, если потребуется.


      Очень жаль, что Вы не выложили Ваш деобфускатор сразу. Это единственное production-ready решение, которое я видел. Если бы Вы выложили его сразу, то я бы даже не стал тратить на альтернативное решение своё время. Я его начал делать, когда Вы уже закончили.


  1. CorvenDalas
    23.09.2021 15:42

    Статья отличная, дело важное, думаю, ее удалят после прочтения (не)товарищем майором. Смысла, правда, не так уж много, ведь в основном подтасовка происходила за тех, кто не приходил, или происходили вбросы. Так после деобфускации можно будет говорить только о мертвых избирателях. А вот если выложить как-то форму подтверэдения своего голоса-это будет полное отсутствие тайны голосования, хотя это дело сулит неприятностями только автору статьи


  1. ilyabo
    29.09.2021 17:32
    +1

    здесь еще одна интерактивная карта:
    https://studio.unfolded.ai/public/b7833d0f-43e8-4e2f-8190-fd1414f62c0c/embed