There exists a wrong implementation in the precompile 0x9. When calculate the output involved with non-zero aligned inputs, the output and the gas result is totally wrong. This would lead to the wrong gas usage and further wrong result with the correct EVM-consensus implemented validator, resulting the network partition.
Vulnerability Details
In the precompile function of [1], it decodes the rounds and the following parameters hRaw,mRaw,tRaw using the beginning buffer of the data. However, the data buffer could have offset and when the offset is non-zero, the decoded parameter is wrong. The official definition of the Blake precompile could be found in [2].
export function precompile09(opts: PrecompileInput): ExecResult {
...
const rounds = new DataView(data.subarray(0, 4).buffer).getUint32(0) // [1] wrong align for the
const hRaw = new DataView(data.buffer, 4, 64)
const mRaw = new DataView(data.buffer, 68, 128)
const tRaw = new DataView(data.buffer, 196, 16)
Take [1] for example, it decode the first 4 bytes of the data array buffer. However, the byteOffset of the data could be non-zero. This result in the wrong memory access (we read the wrong & stale memory data) when calculate the parameter. This further leads to the wrong output result and the gas result.
Hence we need to take byteOffset into consideration. Following parameter calculation is an suggested fix:
const rounds = new DataView(data.buffer, data.byteOffset).getUint32(0)
const hRaw = new DataView(data.buffer, data.byteOffset + 4, 64)
const mRaw = new DataView(data.buffer, data.byteOffset + 68, 128)
const tRaw = new DataView(data.buffer, data.byteOffset + 196, 16)
Impact Details
By loading the malicious PoC tx we provided, the contract call result is totally wrong. What's more, the gas calculation is wrong as well. This could lead to the consensus issue within the execution layer. Furthermore, if any dApp using this precompile under this wrong implementation, it could get the wrong & unexpected result, leading to the DoS or fund lock/stolen issue in that case.
We observe that the return value is 0x08c9bcf367e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d1487c967f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b. However, the correct return value should be 0x772acbd3f30b0c3f5f53e8b836ab406f7d8d46fd4b27e2ce2ecd67dbf18c958741e2c49d1f1b1a463907a484f970c057dab9684062b82fda69e8a0057e14766f. (We will later explain how we get the correct value/ ground truth.)
By applying the suggested fix in the bug description, we get the correct execution result.
Ground truth
We use a foundry test to get the correct result since it uses ethereum mainnet as the execution backend. Following is the test file:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
contract BlakePoCTest is Test {
function setUp() public {}
function testBlakeResult() public {
address target = address(0x0);
bytes memory code = hex"366000602037600080366020600060095AF1593D6000593E3D90F3";
vm.etch(
target , code
);
(bool success, bytes memory result) = target.call(hex"0000000c28c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b3dd8338ed89de6791854126751ac933302810c04147014e9eb472e4dbc09d3c96abb531c9ae39c9e6c454cb83913d688795e237837d30258d11ea7c75201003000454cb83913d688795e237837d30258d11ea7c752011af5b8015c64d39ab44c60ead8317f9f5a9b6c4c01000000000100ca9a3b000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000");
console.log("success:", success);
console.logBytes(result);
}
}