Прочитав новость о том, что Центризбирком РФ выложил результаты выборов на своем сайте в обфусцированном виде, многие начали публиковать в комментариях свои варианты деобфускаторов, как с использованием 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®ion=0&sub_region=0&type=242&report_mode=null, мы можем рекурсивно получить всех его потомков.
В результате у нас получается список из примерно ста тысяч URL, которые нам нужно скачать. Здесь возникают две проблемы. Первая — сервер очень медленно отдает страницы, и вторая — CAPTCHA. Опытным путем было установлено, что без проблем можно скачивать в 10 потоков с одного IP-адреса, при этом на получение всех данных ушло примерно часов 12. А для решения CAPTCHA в macOS есть готовое решение под названием Vision.framework. Всего в несколько строк кода мы можем с высокой точностью распознать символы на изображении. Дополнительно нам облегчает задачу тот факт, что на изображении всегда 5 символов и используются только цифры.
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)
Gengenid
22.09.2021 20:55+14Вот как так можно: чисто первичные данные по УИКам занимают дай бог 20 МБ, к примеру, в csv. Но чтобы их получить, нужно сгенерировать и передать 11 ГБ. И это они называют защитой от атак.
Mozhaiskiy
22.09.2021 22:22+25Называть они это могут как угодно, задача же в другом: максимально затруднить проверку и контроль результатов, как минимум, в первые часы и дни, когда внезапные изменения могут иметь общественный резонанс.
jorikdima
22.09.2021 22:44+12О каком резонансе речь? Все все знают. Кто чё не видел?
Buzzzzer
23.09.2021 08:26+14Много людей, которые не видят.
Как ни удивительно, но есть люди, которые верят, что всё честно и так надо, а "вот это вот всё" и вообще вся оппозиция — это вбросы, проплаченные зарубежными врагами и "навальнистами". Никаких аргументов слышать они не желают.
Моментально записывают тебя в "навальнисты", на которых у них уже настроен внутренний фаервол.
Поэтому пропаганда очень-очень хорошо работает. Обфускация, затягивание и усложнение парсинга результатов — это тоже методы пропаганды.max_mustermann
23.09.2021 09:40-1Ну-ну, развейте вашу мысль. А теперь эти люди прочтут статью на хабре, скачают с Гитхаба скрипт, распарсят обфусцированные данные и... и пелена спадёт с глаз и они воскликнут "о боже, нас обманывают, данные подделаны" или что?
Anonerror
23.09.2021 10:58+1Далеко не все из "этих людей" вообще знают, что такое гитхаб) они больше телевизор смотрят
Kanut
23.09.2021 11:58+1А разве не так что официально оспаривать результаты выборов можно только в течении ограниченного интервала времени?
aulitin
22.09.2021 22:43+11Как я уже писал на github, в архиве не хватает результатов по одномандатным округам. Деобфускатор можно использовать мой https://github.com/ulex/izbirkom21, в нем есть какие-то известные проблемы?
aulitin
22.09.2021 22:58+5Деобфусцировал и выложил ваши данные
https://github.com/ulex/izbirkom21/releases/tag/V1.00Но если бы я занимался анализом результатов, то я бы их не использовал. Решение с отрытым кодом можно проверить и убедиться, что все в порядке, а проверить Ваши данные нельзя.
apple01
23.09.2021 03:18Я пытался скачать out.zip, но при деархивации выходит ошибка «unexpected end of archive».
aulitin
23.09.2021 03:25+1скачал с github, хэш сходится, 7-zip test проходит без ошибок
SHA256:96F624DD90E343DEA088E5647EC462818A4D9F1501F15EF78D5C3AE96A541AC7
на github есть вариант в 7z архиве, который сжался в 10 раз компактнее и уже распаршенный sqlite
illusionofchaos Автор
23.09.2021 14:55+2Так я же опубликовал на GitHub исходный код, который я использовал для выкачивания, нужен только мак и около 12 часов, чтобы все выгрузить еще раз. Если будете запускать, советую пользоваться мобильным интернетом или мобильным прокси, чтобы снизить вероятность бана по IP.
Код писал на скорую руку, просто чтобы один раз запустить, но перед загрузкой на GitHub разделил его на несколько executable по шагам:
Step1DownloadUikTree - получает дерево всех страниц и сохраняет в
root.json
Step2ConvertUikTreeToUrlList - конвертирует
root.json
вurls.csv
со строками в форматеid;url
Step3DownloadHtml - считывает все URL из
urls.csv
, для каждого проверяет наличие файла с таким именем в папкеhtml
, при отсутствии скачивает, используя 10 потоков и распознавая капчу при необходимостиStep4DownloadFonts - проходит по содержимому всех файлов из папки
html
, при помощи поиска по регулярному выражениюfonts1/[^\.]+\.otf
находит URL шрифтов, далее сохраняет каждый шрифт в папкуhtml/fonts1
, предварительно проверяя отсутствие файла с таким именем
meet2code
23.09.2021 00:09+16Как мне радостно видеть такие статьи на хабре. И аналитикам полезно, и просто по-человечески приятно, что не дают легко изворачиваться хитрецам, загоняя их всё дальше и дальше в угол.
Goupil
23.09.2021 08:58+10Я тоже это поддерживаю, но потом из России хабр придется читать через тор.
simpleadmin
23.09.2021 09:25+2Читать Хабр через tor или просто европейские прокси уже почти нереально. Постоянные "Connection refused" - вероятно защита от DDoS совсем испортилась.
Пока спасают RU-прокси, но их "осталось на 3 дня" ...
Kuzyok777
23.09.2021 09:07+2В угол? Интересно, как вы себе представляете момент, когда зогоните в угол? :)
alvaz
23.09.2021 10:37+3Ну если честно, по человечески завидую людям которые готовы тратить свое время и ресурсы на эту работу. Только за сегодняшнее утро пришло две ссылки на подобные материалы на Хабре. Впрочем чего это я, тоже ведь трачу свое время на чтение и коментарий. Пойду лучше делом займусь.
x2v0
23.09.2021 13:14Это просто праздник какой-то ... За одни сутки столько всего узнал об искусстве парсинга.
AlexShpilkin
23.09.2021 15:42+1Одна поправка — CSS generated content может быть не только невидимый, но и видимый тоже.
Надеялся, что эта история ещё немножко поживёт вне открытых источников и вся эта гонка вооружений ещё немножко постоит на месте, но не судьба, похоже. Впрочем, вся эта обфускация безусловно совершенный nerd-sniping.
Коли такое дело, вот ещё тогда моё описание по-английски и параллельный код деобфускатора на Питоне: https://purl.org/cikrf/un/unfuck.py.html (ссылки на только скрипт и историю изменений внутри).
aulitin
23.09.2021 23:47+1Очень странная надежда. Если под гонкой вооружений Вы имеете в виду технические меры, то это заведомо проигрышная позиция для ЦИК-а: любую обфускацию можно обойти. Это уже проходили на примере телеграма. И чем сложнее, тем больше нердов будет этим заниматься. Запишите мой контакт — я готов безвозмездно помогать, если потребуется.
Очень жаль, что Вы не выложили Ваш деобфускатор сразу. Это единственное production-ready решение, которое я видел. Если бы Вы выложили его сразу, то я бы даже не стал тратить на альтернативное решение своё время. Я его начал делать, когда Вы уже закончили.
CorvenDalas
23.09.2021 15:42Статья отличная, дело важное, думаю, ее удалят после прочтения (не)товарищем майором. Смысла, правда, не так уж много, ведь в основном подтасовка происходила за тех, кто не приходил, или происходили вбросы. Так после деобфускации можно будет говорить только о мертвых избирателях. А вот если выложить как-то форму подтверэдения своего голоса-это будет полное отсутствие тайны голосования, хотя это дело сулит неприятностями только автору статьи
ilyabo
29.09.2021 17:32+1здесь еще одна интерактивная карта:
https://studio.unfolded.ai/public/b7833d0f-43e8-4e2f-8190-fd1414f62c0c/embed
romankonstant
Возможно проблемы прозрачности выборов стоит решать как-то иначе :))) А статья прекрасная
Drunik
пока я не смогу проверить как учёлся именно мой голос - все эти блокчейн-бигдата чистая профанация. О какой приватности голосования идёт речь когда оно провдится мной в личном кабинете госуслуг где всё обо мне известно? Мой бюллетень с моими отметками нужно присылать мне на почту, и выдавать уникальный код по которому я всегда могу проверить правильность учёта своего голоса. Это как минимум - останется вопрос с недопущением электронного вброса левых бюллетеней неголосовавших людей. Тогда это будет хоть на что-то похоже.
A_J
Возможность проверки результата - палка о двух концах. Если можете проверить вы, то сможет и кто-то другой. В идеале - человек, которого погнали голосовать должен иметь возможность:
1. Проверить как учтён его голос на самом деле
2. Показать "проверяющему" любой желаемый результат
Gengenid
2 пункт лишний. Если у человека есть "проверяющий" - это уже никакая не демократия. Смысла нет в выборах вообще.
Vinchi
Ровно тоже самое слышал от многих
selivanov_pavel
Товарищ майор вас за экстремизм посадит за такие комментарии.