To solve this challenge, we must find a set of valid parameters for the heist
function, ensuring that these parameters pass three prime number checks in the contract, while also satisfying .
Let's break down each check:
Wilson's theorem is used for the first primality check:
This part of the challenge uses Wilson's theorem to check for primality by verifying if , indicating that is a prime number.
It first requires us to return the factorials of all numbers in the range , and these should be divided into three groups: , , and . The factorial of each number in these groups should be stored in 8, 12, and 16 bytes, respectively. Then, by checking keccak256(abi.encodePacked(precomputedFactorials[0], precomputedFactorials[1], precomputedFactorials[2]))
, it ensures that the returned content is correct.
It is worth noting that when encoding, the challenge uses abi.encodePacked()
, which should be handled with care for dynamic length data. This is because the nature of this encoding method can lead to situations such as: abi.encodePacked("123", "4") == abi.encodePacked("1", "234")
.
Therefore, in this case, we can adjust the lengths of these three groups of bytes without altering the relationship between each byte, leading the contract to read incorrect values. Through various permutations and combinations, it is found that many composite numbers can pass this check, and we skip the specific choice of for now.
The second prime check involves a method using cyclotomic polynomials:
According to the comment, the property used here is that such that is prime if and only if . A quick online search didn't yield information on this method for checking primality. However, it's notable that the cyclotomics used in these functions are pre-initialized in memory and placed at the 0x760
position:
Assuming the implementation of this check is correct, the only approach would be to somehow modify this segment of memory. The method I discovered involves the use of (bool success, bytes memory data) = msg.sender.call("");
within the Wilson's theorem primality check. We can append additional bytes to the originally returned bytes[3]
, or directly return bytes[4]
instead. I opted for bytes[4]
, setting the length of the last bytes array to 0 initially. Utilizing forge debug, it was found that the fourth bytes in array would start writing to memory from the 0x340 (0xc0 + 0x280)
position.
To overwrite the original data at position 0x760
, it's only necessary to ensure that the length of the fourth bytes exceeds 0x400 (0x360 + 0x400 = 0x760)
. As for what data to overwrite with, by observing the formula, it can be discovered that if all the cycloByte
are zeros, then regardless of the parameters, result
will always equal 0. Consequently, result % int256(n) == 0
, allowing us to directly overwrite the entire table with zeros.
The last prime check utilizes an extension of Wilson's theorem, which is as follows:
This concept isn't hard to grasp. Assuming is composite, then:
, where
For that are not square numbers, it's evident that . However, for square numbers , we encounter the case where , and the aforementioned condition may not necessarily hold. Yet, it can be observed that:
Therefore, this condition also holds true for all square numbers , and a simple verification reveals that 4 does not satisfy this condition. Through this discovery, we know that 4 is the only composite number that can pass this check. The next step is to ensure that the case of passes the first check based on Wilson's theorem.
When , the contract reads the value of a factorial using 8 bytes. Thus, when , it reads from the 57th to 64th bytes starting from f0
. My approach was to make f0
an empty bytes, so the read position coincides exactly with the length of f1
. I set the length of f1
to 199 bytes, and placed the rest in f2
, enabling to pass the primality check.
Taking all of the above into account, we can now pass this challenge.