В этой статье я попытаюсь объяснить вам как происходит реверс инжиниринг смарт-контрактов используя Ethereum Virtual Machine и плагин Trail of Bits’ Ethersplay для Binary Ninja
По-реверсили потихонечку.
Виртуальная машина Ethereum (EVM) основана на stack-based и является quasi-Turing complete, это означает:
1) stack-based -- Вместо того чтобы полагаться на регистры, любая операция будет полностью содержаться в стеке. Операнды, операторы и вызовы функций помещаются в стек, и EVM понимает, как действовать с этими данными и заставить смарт-контракт исполняться. Ethereum использует постфиксную нотацию для реализации на основе стека. В очень упрощенном виде это означает, что последний оператор, помещенный в стек, будет действовать с данными, помещенными в стек до него. Пример: мы запрограммированы на понимание формата 2 + 2. В нашей голове мы знаем, что оператор (+) в середине означает, что мы хотим применить сложение 2 и 2. Помещение + между двумя операндами - это лишь один из способов представления того, что мы хотим, чтобы произошло: мы могли бы с таким же успехом представить это как 2 2 +, что является постфиксной записью.
2) quasi-Turing complete: Язык или механизм выполнения кода считается "полным по Тьюрингу", если он может решить любую поставленную вами задачу. Неважно, сколько времени это займет, лишь бы он мог теоретически решить ее. Язык сценариев Биткойна не является полным по Тьюрингу, потому что с его помощью можно решить очень мало задач. В EVM вы можете решить всё, но мы говорим "квази-Тьюринг-полный", потому что вы ограничены стоимостью. Поскольку каждый, кто выпускает транзакцию, должен заплатить цену за газ (или цену за вычислительную единицу этой транзакции), сложные проблемы становятся чрезвычайно дорогостоящими. Таким образом, хотя EVM может решить любую задачу, которую вы ему поставите, необходимость платить за Gas делает очень сложные задачи экономически невыполнимыми.
Байткод контракта - это байткод того, что в конечном итоге будет находиться на блокчейне, плюс байткод, необходимый для транзакции размещения этого байткода на блокчейне и инициализации смарт-контракта (запуск конструктора).
Runtime байткод - это только тот байткод, который в итоге оказывается на блокчейне. Сюда не входит байткод, необходимый для инициализации контракта и размещения его на блокчейне.
Давайте возьмем смарт-контракт Greeter.sol и рассмотрим разницу.
При компиляции solc --bin Greeter.sol чтобы получить байт-код контракта, мы получаем:
При компиляции с solc --bin-runtime Greeter.sol чтобы получить только runtime байт-код, мы получаем:
Как видите, runtime байт-код является подмножеством полного байт-кода контракта:
В этом руководстве мы будем использовать плагин Trail of Bits Ethersplay для Binary Ninja для дизассемблирования байт-кода.
Мы будем использовать Greeter.sol . Инструкция по добавлению плагина Ethersplay в Binary Ninja здесь Напоминаем, что мы будем реверсировать только runtime байт-код , так как только так мы на самом деле поймем что на самом деле делает контракт.
Плагин Ethersplay идентифицирует все функции в байт-коде среды выполнения и логически выделяет их для вас. В нашем контракте Ethersplay обнаружил две функции: kill() и greet(). Вскоре мы узнаем, как они были извлечены.
Когда вы совершаете транзакцию со смарт-контрактом, первая часть кода, с которой будет взаимодействовать ваша транзакция, — это диспетчер этого контракта. Диспетчер берет данные о вашей транзакции и определяет функцию, с которой вы пытаетесь взаимодействовать.
Первые инструкции, которые мы видим в диспетчере:
Существует 16 различных версий PUSH инструкций ( PUSH1… PUSH16). Число сообщает EVM, сколько байт мы помещаем в стек.
Первые две инструкции, PUSH1 0x60 а также PUSH1 0x40, помещают 0x60 и 0x40 в стек соответственно. После выполнения этих инструкций runtime-стек будет выглядеть следующим образом:
MSTORE определяется в документации Solidity следующим образом:
Аргументы функции считываются с вершины стека вниз, то есть мы получаем mstore (0x40, 0x60). Это эквивалентно mem[0x40...0x40+32] := 0x60.
mstore выталкивает два элемента из стека, так что стек теперь пуст! Наша следующая инструкция:
После PUSH10x4, в стеке только один элемент.
0: 0x4
Функция CALLDATASIZE подталкивает размер calldata (эквивалент msg.data) в стек. Вы можете отправлять любые данные на любой смарт-контракт. CALLDATASIZE просто проверит, какой длинны эти данные.
После вызова CALLDATASIZE, стек выглядит так:
1: (however long the msg.data or calldata is)
0: 0x4
Это следующая инструкция LT, сокращение от «меньше чем», и работает так:
lt помещает в стек 1, если первый аргумент меньше второго аргумента, и 0, если это не так. В нашем коде мы получаем
Почему EVM проверяет, чтобы наши calldata были как минимум 4 байта? Из-за того, как работают идентификаторы функций .
Каждая функция идентифицируется первыми четырьмя байтами ее keccak256 хэш. То есть вы помещаете прототип функции (имя функции и аргументы, которые она принимает) в keccak256 хэш-функцию. В нашем контракте есть:
Таким образом, функции идентифицируют cfae3217 для greet(), а также 41c0e1b5 для kill(). Диспетчер проверяет наличие calldata (или данные сообщения), которые вы отправили в контракт, имеют ли длину не менее 4 байтов, чтобы убедиться, что вы действительно пытаетесь связаться с функцией!
Идентификатор функции всегда имеют длину 4 байта, поэтому, если все сообщения, которые вы отправляете смарт-контракту, меньше 4 байтов, то они не дойдут до функции. Если calldatasizе меньше 4 байт, байт-код немедленно отправляет вас к блоку кода в конце, заканчивая выполнение контракта.
Рассмотрим как это происходит.
Если lt((however long the msg.data or calldata is), 0x4) оценивает 1 ( правда, или другими словами, calldata меньше 4 байт), то после извлечения двух верхних значений из стека, lt помещает 1 в стек.
Далее у нас есть PUSH 0x4c а потом JUMPI. После PUSH 0x4c, наш стек выглядит так:
JUMPI это условный переход и переходит к определенной метке/местоположению, если выполняется условие.
В нашем случае label смещено 0x4c в коде, и cond равен 1, поэтому он оценивается как истинный. Это означает, что программа переходит к смещению 0x4c.
Вот команды во втором блоке:
PUSH1 0x0 помещает 0 в конец стека.
CALLDATALOAD принимает в качестве аргумента индекс в данных вызова, отправленных в смарт-контракт, и считывает 32 байта из этого индекса, например:
CALLDATALOAD помещает прочитанные 32 байта на вершину стека. С 0 индекс передан из PUSH1 0x0 командой, CALLDATALOAD считывает 32 байта данных вызова, начиная с байта 0, а затем помещает их на вершину стека (после извлечения исходного 0x0). Новый стек:
Следующая инструкция PUSH29 0x100000000....
SWAPi меняет местами верхний элемент в стеке с ith в пункт после него. В этом случае инструкция SWAP1 меняет местами верхний элемент в стеке с первым, следующим за ним.
Следующая инструкция DIV, которая становится div(x, y) с этими, x/y. В этом случае х = 32 bytes of calldata starting at byte 0 и у = 0x100000000....
0x100000000.... имеет длину 29 байт, состоящую из 1 в начале, за которой следуют все 0. Ранее мы прочитали 32 байта из calldata. Разделив наши 32 байта calldata на 10000... Что оставит нам только самые верхние 4 байта нашей calldataload, начиная с индекса 0. Эти четыре байта — первые четыре байта в calldataload, начиная с индекса 0 — являются идентификатором функции! Если эта часть вам непонятна, подумайте об этом так: в base10, 123456000 / 100 = 123456. В base16 ничем не отличается. При делении 32-байтового значения (по основанию 16) на 29-байтовое значение (16 ^ 29) останутся только верхние 4 байта.
Результат DIV помещается в стек.
Далее мы видим PUSH4 0xffffffff потом AND, что в нашем случае будет AND в 0xffffffff с идентификатором функции, отправленным из calldata. Это делается просто для того, чтобы обнулить оставшиеся 28 байтов после идентификатора функции в том же элементе стека. А DUP1 следует инструкция, которая дублирует первый элемент в стеке (в данном случае идентификатор функции) и помещает его на вершину стека:
Наконец, мы видим PUSH4 0x41c0e1b5. Это идентификатор функции для kill(). Мы помещаем его в стек, потому что хотим сравнить его с идентификатором функции calldata.
Следующая инструкция EQ или eq(x, y), которая извлекает x и y из стека, помещает в стек 1, если они равны, или 0 в противном случае. Проверка на равенство между идентификатором функции calldata и всеми идентификаторами функций в смарт-контракте.
После PUSH2 0x51, у нас есть:
Мы нажимаем 0x51 потому что именно здесь мы будем прыгать в программе, если JUMPI условие выполнено. Другими словами, мы переходим к смещению 0x51 в коде, если идентификатор функции, отправленный из calldata, совпал с kill(), так как 0x51 это адрес где kill() живет!
После JUMPI, мы либо пошли на 0x51, или продолжаем выполнения программы.
Наш стек содержит только:
Вы заметите, что если мы не делаем прыжок к kill() функции, диспетчер реализует ту же логику для сравнения идентификатора функции calldata с greet() идентификатор функции. Диспетчер проверит каждую функцию в смарт-контракте, и если не найдет ту, которая соответствует отправленному вами идентификатору, то направит вас к выходному блоку.
Перевод вот ЭТОЙ статьи.
По-реверсили потихонечку.
Ethereum Virtual Machine
Виртуальная машина Ethereum (EVM) основана на stack-based и является quasi-Turing complete, это означает:
1) stack-based -- Вместо того чтобы полагаться на регистры, любая операция будет полностью содержаться в стеке. Операнды, операторы и вызовы функций помещаются в стек, и EVM понимает, как действовать с этими данными и заставить смарт-контракт исполняться. Ethereum использует постфиксную нотацию для реализации на основе стека. В очень упрощенном виде это означает, что последний оператор, помещенный в стек, будет действовать с данными, помещенными в стек до него. Пример: мы запрограммированы на понимание формата 2 + 2. В нашей голове мы знаем, что оператор (+) в середине означает, что мы хотим применить сложение 2 и 2. Помещение + между двумя операндами - это лишь один из способов представления того, что мы хотим, чтобы произошло: мы могли бы с таким же успехом представить это как 2 2 +, что является постфиксной записью.
2) quasi-Turing complete: Язык или механизм выполнения кода считается "полным по Тьюрингу", если он может решить любую поставленную вами задачу. Неважно, сколько времени это займет, лишь бы он мог теоретически решить ее. Язык сценариев Биткойна не является полным по Тьюрингу, потому что с его помощью можно решить очень мало задач. В EVM вы можете решить всё, но мы говорим "квази-Тьюринг-полный", потому что вы ограничены стоимостью. Поскольку каждый, кто выпускает транзакцию, должен заплатить цену за газ (или цену за вычислительную единицу этой транзакции), сложные проблемы становятся чрезвычайно дорогостоящими. Таким образом, хотя EVM может решить любую задачу, которую вы ему поставите, необходимость платить за Gas делает очень сложные задачи экономически невыполнимыми.
Bytecode vs. Runtime Bytecode
При компиляции контракта вы можете получить либо байткод контракта, либо runtime байткод .Байткод контракта - это байткод того, что в конечном итоге будет находиться на блокчейне, плюс байткод, необходимый для транзакции размещения этого байткода на блокчейне и инициализации смарт-контракта (запуск конструктора).
Runtime байткод - это только тот байткод, который в итоге оказывается на блокчейне. Сюда не входит байткод, необходимый для инициализации контракта и размещения его на блокчейне.
Давайте возьмем смарт-контракт Greeter.sol и рассмотрим разницу.
Код:
contract mortal {
/* Define variable owner of the type address */
address owner;
/* This function is executed at initialization and sets the owner of the contract */
function mortal() { owner = msg.sender; }
/* Function to recover the funds on the contract */
function kill() { if (msg.sender == owner) selfdestruct(owner); }
}
contract greeter is mortal {
/* Define variable greeting of the type string */
string greeting;
/* This runs when the contract is executed */
function greeter(string _greeting) public {
greeting = _greeting;
}
/* Main function */
function greet() constant returns (string) {
return greeting;
}
}
При компиляции solc --bin Greeter.sol чтобы получить байт-код контракта, мы получаем:
6060604052341561000f57600080fd5b6040516103a93803806103a983398101604052808051820191905050336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055508060019080519060200190610081929190610088565b505061012d565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f106100c957805160ff19168380011785556100f7565b828001600101855582156100f7579182015b828111156100f65782518255916020019190600101906100db565b5b5090506101049190610108565b5090565b61012a91905b8082111561012657600081600090555060010161010e565b5090565b90565b61026d8061013c6000396000f30060606040526004361061004c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806341c0e1b514610051578063cfae321714610066575b600080fd5b341561005c57600080fd5b6100646100f4565b005b341561007157600080fd5b610079610185565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156100b957808201518184015260208101905061009e565b50505050905090810190601f1680156100e65780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610183576000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16ff5b565b61018d61022d565b60018054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156102235780601f106101f857610100808354040283529160200191610223565b820191906000526020600020905b81548152906001019060200180831161020657829003601f168201915b5050505050905090565b6020604051908101604052806000815250905600a165627a7a723058204138c228602c9c0426658c0d46685e1d9c157ff1f92cb6e28acb9124230493210029
При компиляции с solc --bin-runtime Greeter.sol чтобы получить только runtime байт-код, мы получаем:
60606040526004361061004c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806341c0e1b514610051578063cfae321714610066575b600080fd5b341561005c57600080fd5b6100646100f4565b005b341561007157600080fd5b610079610185565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156100b957808201518184015260208101905061009e565b50505050905090810190601f1680156100e65780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610183576000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16ff5b565b61018d61022d565b60018054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156102235780601f106101f857610100808354040283529160200191610223565b820191906000526020600020905b81548152906001019060200180831161020657829003601f168201915b5050505050905090565b6020604051908101604052806000815250905600a165627a7a723058204138c228602c9c0426658c0d46685e1d9c157ff1f92cb6e28acb9124230493210029
Как видите, runtime байт-код является подмножеством полного байт-кода контракта:
Реверс
В этом руководстве мы будем использовать плагин Trail of Bits Ethersplay для Binary Ninja для дизассемблирования байт-кода.
Мы будем использовать Greeter.sol . Инструкция по добавлению плагина Ethersplay в Binary Ninja здесь Напоминаем, что мы будем реверсировать только runtime байт-код , так как только так мы на самом деле поймем что на самом деле делает контракт.
Плагин Ethersplay идентифицирует все функции в байт-коде среды выполнения и логически выделяет их для вас. В нашем контракте Ethersplay обнаружил две функции: kill() и greet(). Вскоре мы узнаем, как они были извлечены.
Когда вы совершаете транзакцию со смарт-контрактом, первая часть кода, с которой будет взаимодействовать ваша транзакция, — это диспетчер этого контракта. Диспетчер берет данные о вашей транзакции и определяет функцию, с которой вы пытаетесь взаимодействовать.
Первые инструкции, которые мы видим в диспетчере:
Код:
PUSH1 0x60 // argument 2 of mstore: the value to store in memory
PUSH1 0x40 // argument 1 of mstore: where to store that value in memory
MSTORE // mstore(0x40, 0x60)
PUSH1 0x4
CALLDATASIZE
LT
PUSH2 0x4c
JUMPI
Существует 16 различных версий PUSH инструкций ( PUSH1… PUSH16). Число сообщает EVM, сколько байт мы помещаем в стек.
Первые две инструкции, PUSH1 0x60 а также PUSH1 0x40, помещают 0x60 и 0x40 в стек соответственно. После выполнения этих инструкций runtime-стек будет выглядеть следующим образом:
Код:
1: 0x40
0: 0x60
MSTORE определяется в документации Solidity следующим образом:
| Instruction | Result |
|---|---|
| mstore(p, v) | mem[p..(p+32)) := v |
Аргументы функции считываются с вершины стека вниз, то есть мы получаем mstore (0x40, 0x60). Это эквивалентно mem[0x40...0x40+32] := 0x60.
mstore выталкивает два элемента из стека, так что стек теперь пуст! Наша следующая инструкция:
Код:
PUSH1 0x4
CALLDATASIZE
LT
PUSH 0x4c
JUMPI
После PUSH10x4, в стеке только один элемент.
0: 0x4
Функция CALLDATASIZE подталкивает размер calldata (эквивалент msg.data) в стек. Вы можете отправлять любые данные на любой смарт-контракт. CALLDATASIZE просто проверит, какой длинны эти данные.
После вызова CALLDATASIZE, стек выглядит так:
1: (however long the msg.data or calldata is)
0: 0x4
Это следующая инструкция LT, сокращение от «меньше чем», и работает так:
| Instruction | Result |
|---|---|
| mstore(p, v) | mem[p..(p+32)) := v |
| lt(x, y) | 1 if x < y, 0 otherwise |
lt помещает в стек 1, если первый аргумент меньше второго аргумента, и 0, если это не так. В нашем коде мы получаем
Код:
lt((however long the msg.data or calldata is), 0x4).
Почему EVM проверяет, чтобы наши calldata были как минимум 4 байта? Из-за того, как работают идентификаторы функций .
Каждая функция идентифицируется первыми четырьмя байтами ее keccak256 хэш. То есть вы помещаете прототип функции (имя функции и аргументы, которые она принимает) в keccak256 хэш-функцию. В нашем контракте есть:
Код:
keccak256("greet()") = cfae3217...
keccak256("kill()") = 41c0e1b5...
Таким образом, функции идентифицируют cfae3217 для greet(), а также 41c0e1b5 для kill(). Диспетчер проверяет наличие calldata (или данные сообщения), которые вы отправили в контракт, имеют ли длину не менее 4 байтов, чтобы убедиться, что вы действительно пытаетесь связаться с функцией!
Идентификатор функции всегда имеют длину 4 байта, поэтому, если все сообщения, которые вы отправляете смарт-контракту, меньше 4 байтов, то они не дойдут до функции. Если calldatasizе меньше 4 байт, байт-код немедленно отправляет вас к блоку кода в конце, заканчивая выполнение контракта.
Рассмотрим как это происходит.
Если lt((however long the msg.data or calldata is), 0x4) оценивает 1 ( правда, или другими словами, calldata меньше 4 байт), то после извлечения двух верхних значений из стека, lt помещает 1 в стек.
Код:
0: 1
Далее у нас есть PUSH 0x4c а потом JUMPI. После PUSH 0x4c, наш стек выглядит так:
Код:
1: 0x4c
0: 1
JUMPI это условный переход и переходит к определенной метке/местоположению, если выполняется условие.
| Instruction | Result |
|---|---|
| mstore(p, v) | mem[p..(p+32)) := v |
| lt(x, y) | 1 if x < y, 0 otherwise |
| jumpi(label, cond) | jump to label if cond is nonzero |
В нашем случае label смещено 0x4c в коде, и cond равен 1, поэтому он оценивается как истинный. Это означает, что программа переходит к смещению 0x4c.
Отправка
Давайте посмотрим, как нужная функция извлекается из calldata. Стек пуст после последнего JUMPI инструкции.
Вот команды во втором блоке:
Код:
PUSH1 0x0
CALLDATALOAD
PUSH29 0x100000000....
SWAP1
DIV
PUSH4 0xffffffff
AND
DUP1
PUSH4 0x41c0e1b5
EQ
PUSH2 0x51
JUMPI
PUSH1 0x0 помещает 0 в конец стека.
Код:
0: 0
CALLDATALOAD принимает в качестве аргумента индекс в данных вызова, отправленных в смарт-контракт, и считывает 32 байта из этого индекса, например:
| Instruction | Result |
|---|---|
| mstore(p, v) | mem[p..(p+32)) := v |
| lt(x, y) | 1 if x < y, 0 otherwise |
| jumpi(label, cond) | jump to label if cond is nonzero |
| calldataload(p) | call data starting from position p (32 bytes) |
CALLDATALOAD помещает прочитанные 32 байта на вершину стека. С 0 индекс передан из PUSH1 0x0 командой, CALLDATALOAD считывает 32 байта данных вызова, начиная с байта 0, а затем помещает их на вершину стека (после извлечения исходного 0x0). Новый стек:
Код:
0: 32 bytes of calldata starting at byte 0
Следующая инструкция PUSH29 0x100000000....
Код:
1: 0x100000000....
0: 32 bytes of calldata starting at byte 0
SWAPi меняет местами верхний элемент в стеке с ith в пункт после него. В этом случае инструкция SWAP1 меняет местами верхний элемент в стеке с первым, следующим за ним.
| Instruction | Result |
|---|---|
| mstore(p, v) | mem[p..(p+32)) := v |
| lt(x, y) | 1 if x < y, 0 otherwise |
| jumpi(label, cond) | jump to label if cond is nonzero |
| calldataload(p) | call data starting from position p (32 bytes) |
| swap1 … swap16 | swap topmost and ith stack slot below it |
Код:
1: 32 bytes of calldata starting at byte 0
0: PUSH29 0x100000000....
Следующая инструкция DIV, которая становится div(x, y) с этими, x/y. В этом случае х = 32 bytes of calldata starting at byte 0 и у = 0x100000000....
0x100000000.... имеет длину 29 байт, состоящую из 1 в начале, за которой следуют все 0. Ранее мы прочитали 32 байта из calldata. Разделив наши 32 байта calldata на 10000... Что оставит нам только самые верхние 4 байта нашей calldataload, начиная с индекса 0. Эти четыре байта — первые четыре байта в calldataload, начиная с индекса 0 — являются идентификатором функции! Если эта часть вам непонятна, подумайте об этом так: в base10, 123456000 / 100 = 123456. В base16 ничем не отличается. При делении 32-байтового значения (по основанию 16) на 29-байтовое значение (16 ^ 29) останутся только верхние 4 байта.
Результат DIV помещается в стек.
Код:
0: function identifier from calldata
Далее мы видим PUSH4 0xffffffff потом AND, что в нашем случае будет AND в 0xffffffff с идентификатором функции, отправленным из calldata. Это делается просто для того, чтобы обнулить оставшиеся 28 байтов после идентификатора функции в том же элементе стека. А DUP1 следует инструкция, которая дублирует первый элемент в стеке (в данном случае идентификатор функции) и помещает его на вершину стека:
Код:
1: function identifier from calldata
0: function identifier from calldata
Наконец, мы видим PUSH4 0x41c0e1b5. Это идентификатор функции для kill(). Мы помещаем его в стек, потому что хотим сравнить его с идентификатором функции calldata.
Код:
2: 0x41c0e1b5
1: function identifier from calldata
0: function identifier from calldata
Следующая инструкция EQ или eq(x, y), которая извлекает x и y из стека, помещает в стек 1, если они равны, или 0 в противном случае. Проверка на равенство между идентификатором функции calldata и всеми идентификаторами функций в смарт-контракте.
Код:
1: (1 if calldata functio identifier matched kill() function identifier, 0 otherwise)
0: function identifier from calldata
После PUSH2 0x51, у нас есть:
Код:
2: 0x51
1: (1 if calldata functio identifier matched kill() function identifier, 0 otherwise)
0: function identifier from calldata
Мы нажимаем 0x51 потому что именно здесь мы будем прыгать в программе, если JUMPI условие выполнено. Другими словами, мы переходим к смещению 0x51 в коде, если идентификатор функции, отправленный из calldata, совпал с kill(), так как 0x51 это адрес где kill() живет!
После JUMPI, мы либо пошли на 0x51, или продолжаем выполнения программы.
Наш стек содержит только:
Код:
0: function identifier from calldata
Вы заметите, что если мы не делаем прыжок к kill() функции, диспетчер реализует ту же логику для сравнения идентификатора функции calldata с greet() идентификатор функции. Диспетчер проверит каждую функцию в смарт-контракте, и если не найдет ту, которая соответствует отправленному вами идентификатору, то направит вас к выходному блоку.
Перевод вот ЭТОЙ статьи.
Последнее редактирование: