PARADIGM CTF 2022 Question Analysis 3 – Lockbox2 Analysis

Firstly, analyze the Setup contract, as shown below:

Code Snippet 1

From above figure we can see, it imports Lockbox2.sol contract at the beginning, leaving it alone for now. Go to next, it defines Setup contract which initializes a new lockbox2 contract. After that, it is a isSolved function. This function isview modified and not on the chain. It returns the non-value of the return value of the lockbox2 contract of the lockedfunction which is a bool type. So it seems that the premise of capturing the flag is to make the locked function of lockbox2 return false.

Next, Lockbox2 contract:

Code Snippet 2

Let’s analyze the code line by line. At first, A global variable-locked is defined and initialized as true. Second, it’s solved function without parameters. For this function, It first declares a bool type array successes with a length of 5. The subscripts from 0 to 4 correspond to the five return values. Continue to look at each line, which is to call the stage1–5 functions of the current contract, and the calldata data starts from the 4th bit of

Starting from the fourth digit, that is, not counting the function signature. Then assign the success of each call as a bool value to the successes array. Then there is a loop through the array of bool type, if each is true, continue to run, set locked to false. Only the solve entry can change the locked variable. It seems that the key problem is the 5 calls of the solve function.

Continue to analyze the following 5 calls.

Stage 1: it requires the length of less than 500.

Stage 2: The parameter passed in is a uint256 array with length 4. In the first 4 lines of incoming, a line of 32 bytes, each line represents a number. And then all 4 numbers must be satisfied the conditions that it cannot divide any number except 1 and itself. This is easy to be constructed.

Stage 3: The incoming parameters are 3 uint256 type data. First, mstore is called, and b is stored in the position of a in the stack, b represents the data, and a represents the position in the stack. Then a program call, where the sum of a, b represents an address, and then the staticcall function statically calls this address. We cannot construct such an address, so the return must be empty.

Next line, it requires the length of the return value needs to be equal to the incoming parameter c. According to stage 2, it is known that c must not be able to divide any number except 1 and itself, and the length of the return value here must be 0, which seems impossible. Because I noticed that there is an mstore at the beginning, which is stored anywhere in memory, this mstore must be meaningful. First of all, through the data, we learned about the characteristics of solidity. Since the return value of the call is empty, it will be stored in a special location 0x60 ( Next, we debug to verify.

Code Snippet 3

It can be seen that after the static call, mload reads the value at the 0x60 position in the stack, so we only need to store data in the specified position in mstore, and then let stage3 pass. We can pass in 0x61, 0x0101, 0x1.

First save 0101 to 0x61 through mstore. Because mstore is used, 32 bytes of data are stored, 0x0101 high-order bits are filled with 0, and 32 bytes are filled. Because the 60 position has a total of 32 bytes, but mstore starts from the 61 position, and the entire 60 can not be stored, so 80 positions will be occupied, shown as below figure.

Code Snippet 4

Now that the 60 position is successfully controlled, mstore(a,b) will fetch the number at 0x60 due to it is before the declaration of bytes memory data and returned data is empty. So mstore is used to control the input parameters to pass stage3.

Stage 4: stage 4 passes in two parameters, both of which are bytes type. Firstly, a contract is created with parameter and saved on the chain. It will return an address. Then perform a static call with parameter b. the following code is a require judgment which requires the code of the newly generated contract must equal to the public key of tx.origin. This is also easier to construct.

Code Snippet 5

Next we print tx.origin,and found there are two more 0s in front of tx.origin. That means when constructing the public key, the first two digits are required to be ‘00’.

Code Snippet 6
Code Snippet 7

We first use the above script to run a public key and a private key, and output a public key whose first two digits are 0.

Code Snippet 8

Then we need to construct an opcode to keep the public key content on the chain.

PUSH1 0x40
PUSH1 0x06
PUSH1 0x0

Concatenate the above opcode with the constructed public key. First explain the meaning of opcode, push 40 into the stack, copy it, and then push both 06 and 0 into the stack. At this point, the contents of the stack are 0, 0b, 40, and 40 from the top of the stack down. codecopy receives 3 parameters, 0, 0b, 40 from the top of the stack which represent the target position, the current position and the code length respectively.

The code of public key is 64 bytes, so the length is 0x40. Because the content of the public key is output of the opcode, so the current position of the public key is the sum of the length of the opcode bytecode, that is 11=0x0b. Afterwards, we need to copy the public key (code) from 0b to 0. Put 0 on the stack, and now there are 0, 40 in the stack. Then execute return command, and return the content from position 0 to 40. So far, the opcode deploys the content of the public key to the chain. Full version likes below:

Code Snippet 6

That means, a should be passed in the bytes array, b shall be passed in 0x0.

Stage 5: the last one. It calls back the solve function of the current contract. But if it fails to return, the first pass succeeds and the second pass fails. We control the gas when we consider the call. This is done later when debugging, which is easier to handle. First, construct the data. First look at the function signature of solve.

Code Snippet 7

Then it is to construct the data. First, you need to get 4 uint256 type data, which satisfies >=1, and can only be divided by 1 and itself. The above has been constructed.

Code Snippet 8

This structure can meet stage 2, stage 3, the reasons have been explained above. Next, stage 4. Because there are 3 integers only, it is missing one, and stage 2 is not satisfied, so it has to find another number.

First of all, let’s find out how the bytes string is stored in memory. Although stage 4 lets you pass 2 strings, but only a is useful. For string value, it stores the length first, and then stores the content. The in the first line represents the offset, so our next construction goes to line 4, which just conflicts with the fourth parameter of stage 2. Because it starts from the 61st position, the 4th line is definitely not full, we can add 0 to the following data. So we construct data with the length of 0100, and let its performance in the 4th line be 01, which satisfies stage 2.

Code Snippet 9

We only need to construct as above to satisfy stage 1, 2, and 3. The data required by Stage 4, it can be done by abi encoding. Then fill it with 0 until the length up to 0100 bytes. The full data is shown below:

Code Snippet 10
Code Snippet 11

Controlled by contract call, and passing in our Now we only need to find the gas cost of stage 5. The figure is shown below:

Code Snippet 12

We print the result of gas here directly, the result is 409548.

Code Snippet 13

Since it is a bit troublesome to find, it’s better to engage in directly, like below:

Code Snippet 14
Code Snippet 15

Then we see that the result of locked is false after executing solve. Capturing the flag.

Code Snippet 16

Numen Cyber Labs is committed to facilitating the safe development of Web 3.0. We are dedicated to the security of the blockchain ecosystem, as well as operating systems & browser/mobile security. We regularly disseminate analyses on topics such as these, please stay tuned or visit our blog here for more!

This blog was originally published on our Medium Account.


More Posts