Получение данных из турнирных систем олимпиад по информатике

Попробуем получить какие-либо данные с сервера, на котором исполняются для проверки посланные нами решения. На первый взгляд нам это даст немногое, однако всмотримся получше в то, что сможем получить:
  1. Наборы тестов (входных данных) — мы в любом случае получаем самый первый тест, а если у нас есть хотя бы какое-нибудь мало-мальски рабочее (проходящее несколько тестов) решение, то получаем и следующие — это даёт занятные результаты:
    • Можно увидеть сами входные данные — это часто позволяет исправить ошибки в решении (к примеру, если мы и не предполагали подобных случаев);
    • Главное — можно, загрузив входные данные, подсовывать их программе с решением «в лоб» (например, полный перебор), которая не проходит в системе по причинам превышения допустимого времени выполнения или размера памяти (а ведь сейчас таких задач очень много — даже начинающий программист практически у всех задач видит элементарное решение, потому авторы заставляют придумывать новые, хитрые и быстрые алгоритмы), а затем отправлять результаты в программе «из if-ов» (то есть для каждого из предлагаемых тестов условными конструкциями выбирать уже готовый ответ);
    • Можно использовать не плохое решение, а решение, которое работает на неподдерживаемых сервером языке программирования или платформе;
    • Если задача заключается в написании программы, повторяющей естественные действия человека, такие, как распознавание текста или подсчёт объектов, можно передавать полученные тесты для решения автору;
  2. Техническая информация о сервере, компиляторах или даже о таких серьёзных вещах, как локальная сеть.

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

Прежде чем мы перейдём к делу, хочу предупредить — пожалуйста, не применяйте этот метод на практике. Подробнее об этом Вы можете прочитать в конце статьи.

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

Попробуем передать данные в вердикте. Пусть «решение» передаёт нам данные по частям, например, беря очередной бит и вызывая вердикт в зависимости от его значения. Так, мы можем вызвать превышение допустимого времени выполнения (Time Limit exceed — TL) бесконечным циклом, памяти (Memory Limit exceed — ML) созданием огромного массива, неправильный ответ (Wrong Answer — WA) или ошибку типа вывода (Presentation Error — PE) выводом совершенно не относящейся к теме информации, ошибку выполнения (Runtime Error — Crash) делением на ноль, и так далее.

Каждому из вариантов вердикта мы сопоставим несколько бит, а один из вердиктов отведём под EOF (сигнал конца файла). Далее просто создадим скрипт, который будет формировать и отправлять «решения» на сервер, а затем собирать их результаты. То есть, отправляемое решение может быть таким:
#include <stdio.h>

const int position = $position$;

void call_wa_pe() {
    printf("vJLnKLnjkD\n");
}

void call_tl() {
    while (1) {}
}

void call_crashdvz() {
    int a = 0;
    printf("%d", 5/a);
}

void return_data(int data) {
    if (data == 0)
        call_crashdvz();
    else if (data == 1)
        call_wa_pe();
}

int main() {
    int ch, i = 0, j, tmp;
    while ((ch = getchar()) != EOF) {
        tmp = 1;
        for (j = 0; j < 8; j++) {
            if (i == position) {
                return_data(ch/tmp%2);
                return 0;
            }
            i++;
            tmp *= 2;
        }
    }
    call_tl();
    return 0;
}


Здесь при формировании решений мы просто будем увеличивать на один некую переменную, а затем подставлять в коде её вместо $position$, потом получать результат по битам, пока не получим EOF.

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

Кулхацкерам на заметку
  • Часто системы представляют нам целый зоопарк возможных вердиктов. Например, кроме классических TL, VL, WA и PE иногда различаются Security Violation (SV — при обращении к запрещённым ресурсам) и разновидности Crash — stack overflow, access denied, math error и многие другие. Просто взгляните на монитор системы и в документацию.
  • Приведённые в условии задачи примеры входных и выходных данных сразу же сообщите скрипту, чтобы не узнавать уже известное.
  • Ограничьте количество вариантов — ведь вовсе не все возможные байты могут содержаться во входных данных, так, почти всегда можно отбросить старший бит — вряд ли в тесте будут русские буквы или различные специальные знаки. Главное — если в задаче на ввод подаются числа (так бывает в более чем половине задач), вариантов байта может быть только 11 — 12, то реально передавать даже по байту за раз.
  • Можно передавать за один запрос информацию разной длины. Например, если вам не хватило вариантов, чтобы одним вердиктом всегда отправлять три бита (вариантов меньше восьми), зато избыток вариантов для двух бит (вариантов больше четырёх), сделайте варианты, которые отправляют часто используемые сочетания типа трёх бит 000 (остальные пусть так и будут отправлять по два бита).
  • Пока не достигнут конец байта (восемь бит), можно (ведь в файле всегда целое число байт) сделать EOF посередине байта сигналом того, что оставшиеся биты — нули/единицы. Ещё лучше в это время добавлять вместо EOF ещё один вариант.
  • Очевидно, если длина входных данных всегда одна и та же, либо она дана или её можно как-либо вычислить по остальным данным, EOF не нужен совсем. И наоборот — так, в задачах часто даётся количество строк, затем перечисляются строки. Тогда нужно передавать только строки, завершая их EOF, ведь количество даётся лишь для удобства. То естьпередаются или данные о длине, или EOF.
  • Задействуйте простой алгоритм архивации при передаче входных данных в несколько десятков символов.
  • Используйте обфускацию, а всё псевдо-решение сделайте под стиль кода новичка.
  • Для WA стоит генерировать различные случайные строки (это туда, где в примере кода строка «vJLnKLnjkD»). Лучше, чтобы они были не зашиты в коде решения константой, а создавались из каких-либо составляющих.
  • Чтобы добавить ещё один вариант вердикта, стоит отделить WA от PE (PE обычно возникает, если формат вывода совершенно отличен от нужного, но для этого нужно анализировать условие задачи).
  • Можно передавать после EOF ещё и контрольную сумму, которая поможет вам проверить, передались ли данные правильно.
  • Каждый раз меняйте «легенду» вердиктов — один раз TL означает 10, другой — вовсе EOF.
  • Лучше сделать разные варианты псевдо-решения, да ещё и на нескольких языках программирования.
  • Задействуйте несколько аккаунтов (естественно, заходя с разных IP и маскируясь под разные браузеры).
  • Сделайте задержки между отправками большими, насколько это возможно.

А чтобы всё выглядело ещё более естественно, в последних четырёх пунктах следует применить генератор случайных чисел.

Все эти советы помогают скрыть факт проведения атаки и заметно сократить её время.


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

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

По понятным причинам я не выкладываю здесь никаких скриптов ни под какие системы, иначе вскоре количество решённых задач у отдельных личностей резко повысилось бы. Тем более, скрипт можно написать так, что даже для продуктивной атаки ему будет достаточно передать лишь ссылку на страницу с задачей.

Подведём итоги. У нас есть способ за несколько десятков запросов получить «Зачтено» из большинства плохих решений, на любой системе. Что это даёт? Ничего — ведь подобные системы делаются только для саморазвития. Так, в этой статье мы просто рассмотрели один из методов обхода защиты, что полезно, например, для создателей такого программного обеспечения. Я никому не советую применять такие методы на практике — это очень не понравится организаторам и принесёт вам много проблем. Лучше сядьте и решите несколько задач. Решите честно. Станьте умнее, не переходите на тёмную сторону.
+39
1 июля 2011, 12:25
19
hx0 30,0

комментарии (42)

+2
kekekeks #
Насколько я помню, во время, когда участники решают задачи, полный набор тестов не используется вообще, решения тестируются лишь на соответствие тем входным данным, что даны участникам. И уже когда этап закончен идёт полная проверка. Так было на всеросе несколько лет назад, по крайней мере.
+3
hx0 #
Да, я знаю о такой практике, но сейчас это встречается редко, а на онлайновых сборниках задач этого вовсе нет.
0
brainerazer #
Украина, республиканский этап олимпиады, всё так.
0
hx0 #
А я вот сколько участвовал (Уральские олимпиады, Всероссийская олимпиада), только один раз такое видел.
0
yeputons #
Должно быть, вы участвовали в командных олимпиадах.
0
hx0 #
В том числе и в командных. Как бы то ни было, это не отменяет метода в статье для хоть каких-нибудь олимпиад.
+3
mariofag #
В Латвии на всех нац. олимпиадах именно так, а вот на BOI (Baltic Olympiad in Informatics) и IOI (International) решение тестируется на всех тестах, однако то, будет ли результат тестирования доступен участникам, или нет, зависит от разных условий (на BOI это определяется типом задачи, на IOI полный результат можно посмотреть, потратив Release Token).
+1
hardex #
Очень часто сразу используется полный набор, чтобы установить, кто первый правильно сдал. Но при этом обычно выдают только номер первого непрошедшего теста.
0
BullDER #
В этом году ввели новую систему на Всероссийской олимпиаде… Можно получить до 10-15 фидбэков во время тура… Фидбэки даются по наборам тестов, тоесть либо в группе тестов даются все результаты, либо по всей группе тестов… Вроде правильно написал…
+1
mariofag #
Как вы это вовремя запостили: как раз через пару дней после открытия официального сервера для практики IOI и за три неделе до, непосредственно, самого IOI %)
–2
ertaquo #
Задания по программированию на большинстве этих олимпиад решаются довольно просто. Проверка производится методом «черного ящика» — программе скармливают входные данные в определенном формате (который описывается в задании) и проверяют выходные данные. Входных данных обычно два-три варианта, один из которых указан в задании. Вообще, проще эти задания решить, чем пытаться что-то ломать. БОльшую сложность представляют теоретические задания, которые выполняются или на бумажке, или на какой-то дельфевой программе (которая то ли клиент-серверная, то ли просто сохраняет решение в файл, который потом проверяется отдельной программой). В Москве, может быть, и по-другому происходит это, но в Тюмени на областных и в прошлом году в Смоленске оно примерно так и было.
+3
hx0 #
Подобные системы проверки в реальном времени — это из серьёзных олимпиад. Мне кажется, Вы что-то упустили.
+1
ertaquo #
Возможно. Те, на которых я был (в школе и ССУЗе), серьезными назвать нельзя.
+2
Punk_UnDeaD #
Всегда надо прогонять минимум 10 тестов, причём 3-5 должны быть нагрузочными.
Задачи, где ответ да/нет, можно давать только сериями, в смысле ответ должен быть «да да да да нет нет да нет нет да», чтобы половину не отхапал рандом.

Теоретические задания на бумажке чреваты тем, что организаторы могут представить себе в голове неверный алгоритм.
0
agul #
На олимпиадах высокого уровня обычно бывает не менее 40 тестов.
+1
mariofag #
> а затем отправлять результаты в программе «из if-ов»
Когда-то давно, на национальной олимпиаде Латвии по информатике, один человек именно так и сделал.
Его программа на паскале состояла из 6к+ строчек и компилилась около получаса. Вы представляете, что с ним сделали организаторы, да? %)
0
facepalmLite #
В моей недолгой практике была подобная история. В задаче набор входных данных содержал всего 10000 вариантов. Ну, я и еще 6 человек написали простую генерацию «в лоб» всех ответов с оформлением этого в большую ifовину. Организаторы решения засчитали и признали свой просчет.
0
Punk_UnDeaD #
Когда ответов мало, а время вычисления гигантское — этот вариант единственно правильный.
В самом деле не важно, откуда вы берёте синусы определённой точности, из таблицы Брадиса или вычисляете разложением в ряд.
Но огромное значение имеет, как быстро вы сможете дать ответ на задачу по расстановке 12 ферзей.
0
facepalmLite #
Автор задачи надеялся на динамическое решение. Так что вариант точно не был единственным.
0
hx0 #
Просчёт?!? Сейчас огромное количество задач так решается. И на разборах так и говорят: «Необходимо было просчитать ответы локально...»
0
facepalmLite #
Не знаю… Мне такое попалось лишь один раз и в местечковой (хоть и «международной») олимпиаде. Да и я как-то в стороне от основного движения (географически удален).
0
facepalmLite #
А о «просчёте» в ответе на комментарий выше написал.
+2
Tobanab #
«Вы взломали наши сервера? Значит вы выиграли олимпиаду...»
0
hx0 #
Это было бы неплохо. Хотя бы давали бы отдельный приз. Так нет же, дисквалифицируют и винят потом.
+5
Nickname #
И правильно делают.
0
hx0 #
Согласен, иначе бы сейчас это были олимпиады по ИБ, а не по спортивному программированию.
+2
Tobanab #
Можно проводить олимпиады 2 в 1
–1
danfe #
Сперва прочитал как «дисквалифицируют и винтят потом.» o_O
+4
vanfukov #
Вступление к статье не помешало бы.
+9
nalp #
а позвольте узнать, вы сами пользуетесь тем, что написали?
с высоты 5-ти летнего опыта в олимпиадном программировании мне кажется, что это все полная ерунда
+3
nalp #
дело в том, что в некоторых соревнованиях (Codeforces, TopCoder) полный набор тестов открывается, когда уже нет способов повлиять на результат

в АСМ-контестах это занятие глупое, компьютерное время очень ограничено и ценно, чтобы заниматься такой ерундой

а offline-системы ломать вообще бездарное занятие, потому что никто не смотрит на результаты их — читеров всегда было много; поэтому гораздо важнее как вы выступаете на online-соревнованиях, это показатель.

К тому же эти все соревнования нужны просто для тренировки и саморазвития, и вы можете сколько угодно хвастаться тем, что что-то сумели взломать, но мозгов больше от этого не станет. Лучше решайте задачи честно
0
hx0 #
Да, это я и пытался донести в двух последних абзацах.
0
hx0 #
Очевидно, нет. Это просто возможность. Я согласен и несколько раз упомянул то, что занятие это глупое.
+1
naryl #
> аунтефицируясь

Попробуйте более простое “заходя”. :)
+1
hx0 #
Давайте! А то действительно слишком много длинных слов. :-)
–1
snizovtsev #
После прочтения статьи хочется сказать: спасибо, капитан.

Да, теоретическая возможность вытащить тесты есть. При условии, что у нас не ограничено количество посылок и время. На практике же либо есть ограничение на скорость посылок (1 в минуту), либо вас быстро дисквалифицируют, увидя аномально высокую активность.

Так что максимум что можно сделать — понять примерно в каком месте программы что-то не так (расставляя assert-ы, бесконечные while). Я сам такое практиковал. Но это не читерство, а способ отладки в ущерб штрафному времени.
+4
MikeMirzayanov #
Есть такая «классическая» (да, именно в кавычках) задача. Напишите программу, которая по заданному тексту на литературном английском языке выводит четность года (0 или 1), когда был написан этот текст. В самом деле, в условиях типичного ACM-ICPC соревнования она может быть решена :) В статье автор изложил идею ее решения.
0
agul #
На пробном туре Всесибирской олимпиады была подобная задача (дается номер теста, надо вывести 1 или 0, причем ответы — абсолютно рандомные) — она решалась именно таким методом (:
0
hx0 #
Хорошо, не зря топик написал оказывается!)
0
agul #
Но подобные методы на олимпиадах занимают слишком много времени — если задача не подобна той, которую я описал, то быстрее ее правильно решить, чем тесты вытаскивать.
0
0leGG #
а на основном — то же самое, но не рандом, а двоичное разложение Пи =)
–1
stingray #
Видел я одного такого хакера на контесте. Результат он получил вполне ожидаемый.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.