Команда Bushwhackers
Скромные парни из МГУ не очень афишируют свои личности, но мы, как самый передовой рупор хакерского искусства на территории ex-USSR :), считаем, что страна должна знать своих героев. Вот самая публичная часть группы — авторы этой статьи:
Максим Мальков, Эмиль Лернер, Анатолий Иванов, Вадим Шейдаев, Роман Лозко
iCTF — классический attack-defenсe CTF, правила его следующие: каждой команде дается доступ на типовую машину (вулнбокс) с уязвимыми сервисами, работоспособность которых участники должны поддерживать на протяжении всей игры. Все вулнбоксы команд-участниц объединены в сеть. Обнаружив уязвимость в одном из предложенных сервисов, необходимо написать эксплоит, который крадет флаги (секретную информацию) у других команд, ну и запатчить найденную уязвимость на своей машине. Уязвимости зачастую представляют собой типовые баги из реальной жизни, например SQL injection, Remote Code Execution, Buffer Overflow, но чаще — что-то хитроумное логическое, что нельзя просто так взять и запатчить (или даже обнаружить).
На attack-defenсe сервисы командам обычно раздаются в виде образа виртуальной машины, который каждая команда разворачивает самостоятельно. Для доступа к игровой сети традиционно используется VPN. Однако в этот раз организаторы решили пойти «по дороге с облаками» и разместить все вулнбоксы самостоятельно, в AWS cloud — команды-участницы получали лишь SSH-доступ к этим машинам. VPN-доступа не было, и вулнбокс остался единственным способом попасть в игровую сеть. Также организаторы предоставили специальный Python-модуль, реализующий API, через который происходило все взаимодействие с ними — даже отправка сообщений о проблемах (хотя все всё равно использовали IRC) и получение SSH-ключей для доступа к вулнбоксам. О последнем подробнее поговорим позднее :).
Все игровое время — а в этот раз игра продолжалась аж 24 часа без перерыва! — разбивается на раунды по несколько минут. Каждый раунд бот организаторов проверяет доступность сервисов и добавляет свежие флаги, которые действительны и приносят очки лишь в течение нескольких последующих раундов. Захваченные флаги сдают через API скоринговой системы организаторов. Если сдашь успешно — команде начисляют флаги за атаку.
Одна из особенностей игры состоит в том, что для получения очков необязательно находить уязвимости самостоятельно. Можно подождать, пока другая команда решит сервис и проэксплуатирует уязвимость на твоей машине. Тогда во входящем сетевом трафике можно будет найти чужой эксплоит, разобраться, как он работает, и использовать его против других команд. Это очень важная особенность любого attack-defenсe CTF. В каких-то случаях эксплоит тривиально повторяется. Иногда он помогает натолкнуть на мысль, где находится уязвимость, а иногда эксплоит сложно отличить от бота организаторов. Бывает, что мониторинг трафика помогает улучшить собственный эксплоит. Даже если мы уже «решили» сервис и стали эксплуатировать там какую-то уязвимость, часто оказывалось, что эта уязвимость была не единственной, и мы находили эксплоиты других уязвимостей в трафике. Не говоря уж о том, что мониторинг трафика просто необходим для защиты (а не только повторения чужих эксплоитов).
Вулнбокс устроен таким образом, что каждый сервис запущен от своего пользователя. Больше сервисы никак не изолированы друг от друга (хотя на это стоит обращать внимание).
Как мы сказали, на этом iCTF вулнбокс был единственным способом доступа в игровую сеть. Это привело к тому, что многие команды запускали свои эксплоиты для простоты прямо на вулнбоксе, по соседству с уязвимыми сервисами. Некоторые команды не озаботились изоляцией эксплоита от уязвимых программ, чем мы смогли с успехом воспользоваться.
Уязвимость в сервисе, дающая RCE, позволяет выполнять вообще любые команды на машине с правами пользователя сервиса. В некоторых случаях получается через уязвимость в одном сервисе вытаскивать флаги из других сервисов, чем мы тоже, конечно, воспользовались. Хотя при подготовке игрового образа организаторы стараются, чтобы это было невозможно, поскольку это не очень честно. Но это далеко не самое интересное.
Из-за упомянутой особенности организации игры через RCE в сервисе иногда получалось вытащить исходные тексты эксплоитов, которые команды запускали на машине. Сами эксплоиты, к сожалению, нам мало чем помогли, потому что к тому моменту мы их уже тоже написали.
Кстати, в коде эксплоитов большинство команд использовали API организаторов для отправки флагов и получения за них очков, поэтому многие найденные с помощью RCE эксплоиты содержали логин и пароль к этому API в открытом виде. Как мы уже говорили, этот API использовался и для получения SSH-доступа на вулнбокс. Таким образом, мы получили root-доступ к некоторым машинам других команд, откуда можно было собирать флаги со всех сервисов сразу.
Изначально мы воспользовались советом организаторов и тоже запускали наши эксплоиты прямо на вулнбоксе. Мы заранее предусмотрели, чтобы эксплоиты лежали в директориях, недоступных для чтения сервисам. Да и они все равно не содержали пароль от API, поскольку мы использовали свою фирменную систему для учета флагов и отправки их организаторам для подсчета.
Однако вскоре стало понятно, что одна не очень мощная виртуалка не может потянуть сразу и запуск эксплоитов против нескольких сотен команд, и работу уязвимых сервисов. Поэтому мы подняли мощный сервер в том же регионе AWS, пробросили SSH-туннель до вулнбокса и запускали эксплоиты там. Это позволило нам использовать эксплоиты против сотни команд без ощутимой сетевой задержки.
В конце игры было полное безумие. За семь часов до окончания мы решили последний сервис, и тогда противостояние между нами и нашим ближайшим соперником вышло на новый уровень. Разница в абсолютном счете была сравнима с количеством очков, зарабатываемых командами за раунд! Стоило нашим эксплоитам перестать работать хотя бы на один раунд, и мы бы сразу же значительно отстали... а слабых звеньев было много: сам вулнбокс, наш сервак на Amazon, VPN между ними, нестабильно работающий API организаторов для сдачи флагов...
Логично, что мы сильно перепугались, когда у нас вдруг упал и перезапустился вулнбокс. Это ведь могло быть что угодно. Кто-то смог проэксплуатировать машину и получить рут? Или он просто ребутнулся под нагрузкой? В суматохе мы потеряли один раунд очков, не сумев сориентироваться и восстановить VPN за несколько минут. К последовавшему за ним еще одному ребуту мы были готовы лучше и смогли восстановить связь в течение одного раунда, сохранив поток очков. По косвенным признакам ребуты были вызваны неудачными попытками соперников проэксплуатировать известные баги в ядре для повышения привилегий до рута.
Ну и в самом-самом конце игры, на десерт, мы получили очень мощную сетевую DoS-атаку. Как выяснилось впоследствии, атака была направлена конкретно на нас, производилась с вулнбоксов большого числа команд-участников, предположительно взломанных кем-то, и привела к тому, что легла вообще вся игровая сеть, а не только мы. На тот момент у нас было незначительное преимущество в очках.
Организаторы временно прервали игру, чтобы разобраться, в чем причина сетевых проблем. После часа разбирательств с сетью игру попробовали продолжить, но сеть так и не смогла нормально подняться.
<zanardi> Everything is thoroughly fucked [sic]
(из IRC, Giovanni «zanardi» Vigna — профессор UCSB, организатор iCTF)
Организаторы решили на этом закончить игру, однако объявление победителя отложили на неделю: между первыми двумя командами в топе был очень маленький разрыв.
После перепроверки логов и дампов сетевого трафика они пришли к выводу, что таблица очков на момент остановки игры соответствует действительности, и объявили нас, команду Bushwhackers, победителями, что, помимо прочего, дает попадание в финал DEFCON CTF в Лас-Вегасе.
Бонус: разбор сервиса
Дан исполняемый бинарь, который представляет собой эмулятор процессора, придуманного организаторами. Рядом валяется файл world.rom
, содержащий программу, исполняемую на этом процессоре.
Как оказалось впоследствии, процессор не новый. Первый раз он был использован на финале CSAW 2016. Однако мы решили не идти легким путем и сами разреверсили процессор и написали дизассемблер.
World.rom
— это игра, где нужно бродить по банку, заводить себе карточку, покупать карточки у кардера, писать на провода и умирать (да, там была предусмотрена такая концовка!). Флаги хранились в системе в виде имени держателя карты. Проверочная система загружала флаги в систему, регистрируя дебетовые карты.
В задании нужно было заметить, что у сарая нет одной стены: приватный ключ генерировался каждый раз один и тот же. То есть у каждого клиента один и тот же «секретный» ключ. Так как сервис предоставляет возможность выдать флаг тому, у кого есть валидная подпись запроса на флаг, то дальнейшая эксплуатация не вызывает никаких трудностей:
- Крадем дебетовую карту у незнакомца-кардера, стоящего в стороне.
- Идем к кассиру в банк.
- Просим у него выдать флаг по подписи (можно взять любую валидную, в том числе свою).
- Получаем флаг!
- ??????????
- PROFIT.
Почему приватный ключ может генерироваться один и тот же? Нужно посмотреть, как происходит генерация ключа.
В ходе реверсинга процессора встречается обработка специальных ячеек памяти, при обращении к которым в ct64::operator[]
происходят некоторые специальные действия, будем называть их портами. При обращении к порту 202h выставляется флаг CPU_context->pending_create_out
. Где он еще используется? В данном случае с поиском cross-references может быть беда, можно декомпильнуть всю программу в файл (Generate C file) и дальше поискать по тексту.
Главная процедура эмуляции процессора выглядит так:
void __cdecl ct64::run(ct64 *const CPU_context)
{
while ( ct64::tick(CPU_context) )
ct64::flush_changes(CPU_context);
}
В ct64::tick
происходит эмуляция инструкций процессора. Посмотрим ct64::flush_changes
. Там действительно обнаруживается данный код:
if ( CPU_context->pending_create_out )
{
std::vector<unsigned short,std::allocator<unsigned short>>::push_back(
&CPU_context->create_out_buffer,
&CPU_context->main_memory[514]);
if ( std::vector<unsigned short,std::allocator<unsigned short>>::size(&CPU_context->create_out_buffer) > 0x7F )
{
v1 = std::vector<unsigned short,std::allocator<unsigned short>>::operator[](&CPU_context->create_out_buffer, 0LL);
memcpy(&account, v1, 0x100uLL);
std::vector<unsigned short,std::allocator<unsigned short>>::clear(&CPU_context->create_out_buffer);
goodrand(account.acct, 8uLL);
gen_rsa(account.pubkey, account.privkey);
snprintf(
acctstr,
0x40uLL,
"%s/%04x%04x%04x%04x",
CPU_context->acct_path,
account.acct[3],
account.acct[2],
account.acct[1],
account.acct[0]);
fp = fopen(acctstr, "r");
if ( fp )
exit(1);
fp = fopen(acctstr, "w");
if ( !fp )
exit(1);
fwrite(&account, 0x208uLL, 1uLL, fp);
fclose(fp);
std::vector<unsigned short,std::allocator<unsigned short>>::resize(&CPU_context->create_in_buffer, 0x104uLL);
v2 = std::vector<unsigned short,std::allocator<unsigned short>>::operator[](&CPU_context->create_in_buffer, 0LL);
memcpy(v2, &account, 0x208uLL);
v3 = std::vector<unsigned short,std::allocator<unsigned short>>::end(&CPU_context->create_in_buffer)._M_current;
v4._M_current = std::vector<unsigned short,std::allocator<unsigned short>>::begin(&CPU_context->create_in_buffer)._M_current;
std::reverse<<span class=nobr>gnu_cxx::</span>normal_iterator<unsigned short *,std::vector<unsigned short,std::allocator<unsigned short>>>>(
v4,
(<span class=nobr>gnu_cxx::</span>normal_iterator<short unsigned int*,std::vector<short unsigned int,std::allocator<short unsigned int> > >)v3);
}
CPU_context->pending_create_out = 0;
}
Вот и нужная функция gen_rsa
. Посмотрим, что внутри:
void __cdecl gen_rsa(mem_t *pub_out, mem_t *priv_out)
{
__int64 v2; // rax@1
uint64_t rseed; // [sp+18h] [bp-B8h]@1
mpz_t intern; // [sp+20h] [bp-B0h]@1
mpz_t intern2; // [sp+30h] [bp-A0h]@1
mpz_t p; // [sp+40h] [bp-90h]@1
mpz_t q; // [sp+50h] [bp-80h]@1
mpz_t n; // [sp+60h] [bp-70h]@1
mpz_t e; // [sp+70h] [bp-60h]@1
mpz_t d; // [sp+80h] [bp-50h]@1
mpz_t toitent; // [sp+90h] [bp-40h]@1
gmp_randstate_t state; // [sp+A0h] [bp-30h]@1
__int64 v13; // [sp+C8h] [bp-8h]@1
v13 = *MK_FP(<span class=nobr>FS</span>, 40LL);
goodrand(rseed, 8uLL);
__gmp_randinit_mt(state, 8LL);
__gmp_randseed_ui(state, rseed);
__gmpz_inits(intern, intern2, p, q, n);
__gmpz_urandomb(intern, state, 510LL);
__gmpz_nextprime(p, intern);
__gmpz_urandomb(intern, state, 514LL);
__gmpz_nextprime(q, intern);
__gmpz_mul(n, p, q);
__gmpz_sub_ui(intern, p, 1LL);
__gmpz_sub_ui(intern2, q, 1LL);
__gmpz_lcm(toitent, intern, intern2);
__gmpz_set_ui(e, 65537LL);
__gmpz_invert(d, e, toitent);
deconvert(pub_out, n);
deconvert(priv_out, d);
__gmpz_clears(intern, intern2, p, q, n);
v2 = *MK_FP(<span class=nobr>FS</span>, 40LL) ^ v13;
}
Видишь ли ты то, что вижу я? Функция goodrand
генерирует рандом:
void __cdecl goodrand(void *buf, size_t count)
{
if ( randfd == -1 )
randfd = open("/dev/urandom", 0, 0LL);
read(randfd, buf, count);
}
Посмотрим еще раз на вызов этой функции из gen_rsa
: goodrand(rseed, 8uLL);
. Мы передаем rseed по значению, а не по ссылке. Это значение не инициализировано, кроме того, оно воспринимается как указатель. При этом дальше мы считаем, что в rseed записаны случайные байты. Соответственно, вся суть проблемы в том, что программист забыл взять указатель на rseed. Почему программа от этого не падает? Оставим это в качестве упражнения читателю.
В ассемблерном коде это выглядит так: mov rax, [rbp+rseed], в hex 48 8B 85 48 FF FF FF
. Надо заменить mov
на lea
. Опкод mov
в данном случае — это 8B
, опкод lea
— 8D
. После замены этого байта (получаем 48 8D 85 48 FF FF FF
) проверяем результат:
lea rax, [rbp+rseed]
goodrand(&rseed, 8uLL);
Запуск игры показывает, что теперь каждый раз генерируются разные пары ключей, при этом логика работы не нарушена.