Unlocking the Lockbox2 | ParadigmCTF’22
Published: 2022-08-24 05:50:00

This weekend was fun, thanks to [Paradigm](https://twitter.com/paradigm) for organising a great [CTF](https://twitter.com/paradigm_ctf) again, that has very original and really tough challenges. Got to learn a lot while attempting/solving some of them. This CTF sometimes reminds me of IITJEE exam (a not-for-noobs high school/10+2 level exam in India).

[Lockbox (the first)](https://github.com/paradigmxyz/paradigm-ctf-2021/tree/master/lockbox/public/contracts) was very interesting challenge. I attempted it last year during the CTF, found it very tough. This year’s challenge isn’t any less. From seeming like you can do it, then making you feel like it’s literally impossible, Lockbox kept the reputation with it's 2nd iteration.

The Lockbox2 challenge is available [here](https://github.com/zemse/lockbox2-rocks/tree/main/contracts). In this post, we'll understand the challenge and solve it (based on how I approached it during the CTF). I've posted links wherever necessary. As a heads up, this is a quite long post, it's intended for beginners/intermediates to be able to follow. For experts, feel free to skim through the post or directly skip to the final solution [here](https://github.com/zemse/lockbox2-rocks/blob/main/test/Lockbox2.ts).

I'd like to thank [@rileyholterhus](https://twitter.com/rileyholterhus), the Lockbox2 puzzle creator, to do a quick review of this post and provide valuable suggestions 🙏.

The Setup contract deploys a Lockbox2 contract, that contains a variable called `locked`, which is instantiated as `true`. The challenge requires this locked to be set as `false`. We can see that if get the `solve()` method to execute successfully, it’d set locked variable as `false`. But there are some preceding conditions which depend on input, which if not met, the `solve()` tx would fail. The challenge is all about crafting an input that passes the combination of conditions, similar to cracking [combination locks](https://en.wikipedia.org/wiki/Rotary_combination_lock)!

![A scene from Bollywood movie Happy New Year, Boman Irani attempting to crack a rotary combination lock of the vault](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eqbfiattq8ukedtujw9h.png)

Crafting inputs and improving it sounds fun. However, interestingly, the solidity `solve()` method does not take any input! 

```solidity
function solve() external {
```

If the solve method doesn't accept any input, how are we supposed to insert the crafted input?!

![Meme expressing this is just the first of all problems we will face](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r5jist9i81gl23f29osn.png)

The thing here is, you can basically pass more calldata than needed, solidity does not really care about it (ik this is pretty obvious now, if I’d show this to myself of two years ago I'd have no clue). 

So if we use the usual libs that rely on ABI, the generated calldata will be just the 4 bytes selector and we won’t be able to able to have them encode it for us. So we have to get our hands dirty and do it ourselves. Check out the Contract ABI Spec in the [Solidity docs](https://docs.soliditylang.org/en/latest/abi-spec.html).

First, we will go through the challenge just to get it’s overview and understand it. Then we can get started into crafting the calldata all together.

## Overview of solve method

The solve method makes 5 delegate calls to the self contract, to invoke the logic for those 5 stages.

It’s easily visible that calldata beyond the 4 bytes `msg.data[4:]` is passed to the particular stage method.

Here, delegatecall preserves the `msg.sender`. The intention could be to just add some friction for those who have not played enough with delegatecall. Or if not, we'll get to know shortly ;)

### Stage 1

Quite straight forward, the calldata must be under 500 bytes.

### Stage 2

Looks at first 4 words and expects them to either be a prime number or simply 1. Each of those 4 numbers should small enough (like up to 1000), so that the for-loop can iterate within tx gas limit.

### Stage 3

Looks at first 3 words as `a`, `b` and `c`. Makes a staticcall to `address(a + b)`. Note that from previous stage, the words are quite small numbers (~1000) and hence their sum would also be small. This means we can’t target this staticcall to a contract that we could practically deploy. This has to either be a precompile address or empty account. 

If it is a precompile, then we have just a handful of [choices](https://github.com/ethereumjs/ethereumjs-monorepo/blob/master/packages/evm/src/precompiles/index.ts).

A staticcall to empty account would be successful and return empty data (you can verify this by doing a simple eth_call to random address, it won’t give json rpc error response).

Btw, this one has an interesting `mstore(a, b)` over there. This logic basically stores value `b` at memory location `a`. By initial thought, memory location cannot be very huge and maybe it just want's to restrict `a` to be sufficiently small, but isn't that implied from stage2? Can't make sense of it so far, let's move on for now.

### Stage 4

Things get nasty here since now it involves dynamic types. It is important to understand how solidity handles dynamic types, specifically bytes in calldata for this case. Check out dynamic types section in the Contract ABI spec [here](https://docs.soliditylang.org/en/latest/abi-spec.html#use-of-dynamic-types).

For simplicity, let’s ignore previous stages, just look at this in order to understand what this stage needs in order to pass through.

The gist of the conditions:
1. It takes two byte strings, `a` and `b`.
2. Deploys a contract using `a` as the creation code. The bytecode should be a ECDSA public key (length 64) of the `tx.origin` (the EOA wallet that we will use to interact). Note that ECDSA of ecdsa public key gives ethereum address (after chopping of the upper 12 bytes).
3. Makes a staticcall to the contract just deployed, using `b` as the calldata, this call should be successful.

We can easily deploy a contract containing any code we want. Here, we want to have the `tx.origin`’s public key as the bytecode. So one thing is we should fix a EOA wallet. And then the challenge is getting the static call to succeed. Easiest approach I could think of is having first byte as zero (STOP opcode), so that it halts execution and staticcall passes.

If you notice, this does not involve `b` at all. So basically `b` taking any value can work, which is one less headache.

Making this stage pass individually is straight forward. It will be challenging to make combination of all stages pass.

### Stage 5

It internally makes a msg call to solve function which should fail somehow. Though there’s a difference of `msg.sender` in both `solve()` invocation, `msg.sender` is really not consumed anywhere to make a difference. At first it might seem strange, how it is possible for one thing to work first and then not work in the same env conditions? 

The key to this stage, is the fact that in a CALL only 63/64 of the available gas is passed (source [evm.codes](https://www.evm.codes)). If we pass just enough gas limit, slightly less gas would be passed such that internal message call fails due to out of gas, passing the check in stage 5. Also it is important to note that 1/64 gas should be enough to perform later operations, else it will not work. It also makes sense why there's `locked` variable is set to `false`, instead of having a `solved` variable which is set to `true` like in other challenges, since it'd cost less gas.

## Solving the challenge

### Step 1 - Solve Stage 4

I started from this stage because it has the most complex calldata, and getting other stage passed could look like minor tweaking in the calldata (this is what I expected, I believe it was a right decision).

In stage4, we need to write a bytecode that returns public key which has first byte as zero.

Okay, but for that we need a public key, and for that we need a wallet/private key in order to be the tx.origin.

I’ll just use ethers.js in my terminal (using [ethers-repl](https://github.com/zemse/ethers-repl)) to get a wallet real quick. You can use anything which gets the job done.

```ts
sohamzemse@MacBook-Pro % ethers
ethers-repl> while((w = ethers.Wallet.createRandom()).publicKey.slice(4,6) !== '00') {}
undefined
ethers-repl> w.privateKey
'0x9b067b56552d3369e7762f0f92051db20a2db034e8e9fc803a71e64ae5b163b4'
ethers-repl> w.publicKey
'0x0400a86410b6215c11e36c6a60d02277415f69393b692b6799805ee75969df78d25398733fc3e0438bbcbb9e37fa1ff5da79660324a68452a4311cc7238d7431fa'
```

Note that the public key here has a `0x04` prefix byte, which is to signify that this is an uncompressed ECDSA public key. The actual public key follows after the prefix byte.

Now we need a code that creates contract with the public key as the bytecode. It's pretty opinionated, you can use solidity/yul/vyper/huff/etk/whatever. I happen to be an evm nerd and I was in a hurry, so couldn't stop myself from writing down the following raw evm code.

```assembly
$ vim lockbox_stage4.evm
=========lockbox_stage4.evm========================
push1 0x40 // 64 bytes
dup1
push1 0xb // offset in this code
push1 0 // offset in memory
codecopy
push1 0
return
// public key
00a86410b6215c11e36c6a60d02277415f69393b692b6799805ee75969df78d25398733fc3e0438bbcbb9e37fa1ff5da79660324a68452a4311cc7238d7431fa
===================================================

$ evm-run lockbox_stage4.evm
code 604080600B6000396000f300A86410B6215C11E36C6A60D02277415F69393B692B6799805EE75969DF78D25398733FC3E0438BBCBB9E37FA1FF5DA79660324A68452A4311CC7238D7431FA
Returned: 00a86410b6215c11e36c6a60d02277415f69393b692b6799805ee75969df78d25398733fc3e0438bbcbb9e37fa1ff5da79660324a68452a4311cc7238d7431fa
gasUsed: 30
``` 

I'm using [evm-run](https://github.com/zemse/evm-run) for assembling + running evm code. (Sidenote: you can checkout [ETK](https://github.com/quilt/etk) which helps with labels as well). 

We can see that the bytecode on execution spits out the public key in the returndata (so that it becomes the contract bytecode). Okay, so we got the code that should be passed as to the first input `a`. And in case of `b` we can pass anything, we can just use empty bytes for now i.e. `0x`.

Now it's time to encode the calldata that should work with stage4. Notice that ethers.js strictly requires `"0x"` prefix, so we have to add that at the start of the bytecode.

```ts
ethers-repl> encoded = defaultAbiCoder.encode(['bytes', 'bytes'], ['0x604080600B6000396000f300A86410B6215C11E36C6A60D02277415F69393B692B6799805EE75969DF78D25398733FC3E0438BBCBB9E37FA1FF5DA79660324A68452A4311CC7238D7431FA', '0x'])
'0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000004b604080600b6000396000f300a86410b6215c11e36c6a60d02277415f69393b692b6799805ee75969df78d25398733fc3e0438bbcbb9e37fa1ff5da79660324a68452a4311cc7238d7431fa0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
ethers-repl> 
ethers-repl> wordify(encoded)
[
  '0000000000000000000000000000000000000000000000000000000000000040',
  '00000000000000000000000000000000000000000000000000000000000000c0',
  '000000000000000000000000000000000000000000000000000000000000004b',
  '604080600b6000396000f300a86410b6215c11e36c6a60d02277415f69393b69',
  '2b6799805ee75969df78d25398733fc3e0438bbcbb9e37fa1ff5da79660324a6',
  '8452a4311cc7238d7431fa000000000000000000000000000000000000000000',
  '0000000000000000000000000000000000000000000000000000000000000000'
]
ethers-repl> 
ethers-repl> selector('solve()')
'0x890d6908'
```

Putting all of this together, I'm locally using hardhat:

```ts
// use the private key we generated earlier (for public key to have first byte zero)
const wallet = new Wallet('0x9b067b56552d3369e7762f0f92051db20a2db034e8e9fc803a71e64ae5b163b4', hre.ethers.provider);
// fund this wallet with some eth to pay for tx fees
const hardhatAccount0 = (await hre.ethers.getSigners())[0];
await hardhatAccount0.sendTransaction({
  to: wallet.address,
  value: parseEther("1"),
});

// we are not using the usual abi encoding tools, because this challenge involves manipulating the calldata to make the all stages passed
await wallet.sendTransaction({
  to: contract.address,
  gasLimit: 29_000_000, // pass some huge gas limit, bcz estimate has some issues
  data: '0x890d6908' + // 4 byte selector for solve() method
  [
    '0000000000000000000000000000000000000000000000000000000000000040',
    '00000000000000000000000000000000000000000000000000000000000000c0',
    '000000000000000000000000000000000000000000000000000000000000004b',
    '604080600b6000396000f300a86410b6215c11e36c6a60d02277415f69393b69',
    '2b6799805ee75969df78d25398733fc3e0438bbcbb9e37fa1ff5da79660324a6',
    '8452a4311cc7238d7431fa000000000000000000000000000000000000000000',
    '0000000000000000000000000000000000000000000000000000000000000000'
  ].join('')
});
```

Now execute the tx, and you should see stage4 pass. I’m using Hardhat framework to view the execution trace, using the [hardhat-tracer](https://github.com/zemse/hardhat-tracer) plugin. (Sidenote: A great alternative is using [Foundry](https://github.com/foundry-rs/foundry) and it also has tracing built-in).

![Screenshot of execution trace generated using hardhat-tracer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mqy97yq9n2cxk6fob32p.png)

The stage4 is highlighted in the screenshot above, you can see that rest of the stages fail with empty revert data as `UnknownError`, while there is no error in case of stage4.

Stage4 done, now lets get others!

## Step 2 - Solve Stage 2

The actual condition in the code first checks if the value is greater than or equal to one and then goes into a for-loop that requires `word % j != 0` for all `2 <= j < word`. This step basically translates to the requirement of the first 4 words to be a prime number or 1 and the number cannot be huge, otherwise the for loop will go out of gas.

Okay, lets look at our calldata:

```ts
"0x890d6908" + // 4 byte selector for solve() method
[
  "0000000000000000000000000000000000000000000000000000000000000040", // arr[0]
  "00000000000000000000000000000000000000000000000000000000000000c0", // arr[1]
  "000000000000000000000000000000000000000000000000000000000000004b", // arr[2]
  "604080600b6000396000f300a86410b6215c11e36c6a60d02277415f69393b69", // arr[3]
  "2b6799805ee75969df78d25398733fc3e0438bbcbb9e37fa1ff5da79660324a6",
  "8452a4311cc7238d7431fa000000000000000000000000000000000000000000",
  "0000000000000000000000000000000000000000000000000000000000000000",
].join(""),
```

We can clearly see that arr[3] is huge. Also note that the first three words are not prime numbers either. We need to fix this. 

Let’s see how dynamic types are encoded in the case of `(bytes a, bytes b)`. The first word contains the location where content of `a` starts. And second word contains the location where content of `b` starts. This means we can move the content of `a` anywhere we want.

One easy idea is to simply move the data bit forward, by two words, in order to have arr[3] to take a value that’s within iterative limits as well as making arr[2] and arr[3] independent of the byte strings. So basically we can put any kind of stuff we want over there, the contract be won’t bothered about it at all.

```ts
"0x890d6908" + // 4 byte selector for solve() method
[
  "0000000000000000000000000000000000000000000000000000000000000080", // arr[0], changed from 0x40 to 0x80 (+0x40)
  "0000000000000000000000000000000000000000000000000000000000000100", // arr[1], changed from 0xc0 to 0x100 (+0x40)
  "0000000000000000000000000000000000000000000000000000000000000001", // arr[2] (just inserted)
  "0000000000000000000000000000000000000000000000000000000000000001", // arr[3] (just inserted)
  "000000000000000000000000000000000000000000000000000000000000004b",
  "604080600b6000396000f300a86410b6215c11e36c6a60d02277415f69393b69",
  "2b6799805ee75969df78d25398733fc3e0438bbcbb9e37fa1ff5da79660324a6",
  "8452a4311cc7238d7431fa000000000000000000000000000000000000000000",
  "0000000000000000000000000000000000000000000000000000000000000000",
].join(""),
```

We have inserted two words just after the first two words. We can put anything there, but having `1` in there, simply works for now.

Also we need the first two words to be a prime number. So the first two words are `0x80` (128) and `0x100` (256). Let's see some prime numbers just after them (so we can push them bit further). We know 131 and 257 are primes.

```ts
"0x890d6908" + // 4 byte selector for solve() method
[
  "0000000000000000000000000000000000000000000000000000000000000083", // arr[0], changed from 0x80 to 0x83 (+0x03)
  "0000000000000000000000000000000000000000000000000000000000000101", // arr[1], changed from 0x100 to 0x101 (+0x01)
  "0000000000000000000000000000000000000000000000000000000000000001", // arr[2]
  "0000000000000000000000000000000000000000000000000000000000000001", // arr[3]
  "000000", // a is moved further by 3 bytes
  "000000000000000000000000000000000000000000000000000000000000004b",
  "604080600b6000396000f300a86410b6215c11e36c6a60d02277415f69393b69",
  "2b6799805ee75969df78d25398733fc3e0438bbcbb9e37fa1ff5da79660324a6",
  "8452a4311cc7238d7431fa000000000000000000000000000000000000000000",
  "00", // b is moved further by 1 byte
  "0000000000000000000000000000000000000000000000000000000000000000",
].join(""),
```

Now with this, we can see Stage2 passes! Following is a screenshot of trace generated on hardhat using hardhat-tracer, and the delegate call to stage2 is highlighted for visibility, we can see it does not revert.

![Terminal screenshot of stage2 passing](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bmb7dkzgbxxt5hvhan4q.png)

Ah, finally stage2 passed! Actually, we've done a lot so far. Pads on the back!

## Step 3 - Solve Stage3

Here, a static call is made to `a + b`, which in our case currently it's `0x83 + 0x101 = 0x184`. That's definitely not a precompile address atm, can be checked by making an eth_call.

If it's not a precompile, then the static call would still success, however result would be empty data. And this stage requires that the third word should equal to the length of the return data. 

The third word can be anything only that stage2 restricts the third word to a non-zero value (1 or prime). It means there should be some return data. But how? We definitely cannot deploy a contract at an address with so many zeros! So it means the `a + b` needs to be a precompile for this to work.

I couldn't find an up-to-date list of precompiles, had to look into client implementations. Here is the list in [EthereumJS/EVM](https://github.com/ethereumjs/ethereumjs-monorepo/blob/ffe3402a69e04f48654f13127becf86a8a46485a/packages/evm/src/precompiles/index.ts). We can clearly see precompiles are only upto 0x12. Also the return data length should be a prime number, when called with empty data. We can just try this using `eth_call`. For that, I'm quickly jumping into ethers-repl on my terminal.

```ts
$ ethers
ethers-repl> await mainnet.call({to: '0x0000000000000000000000000000000000000012', data: '0x'})
// {"jsonrpc":"2.0","id":44,"error":{"code":-32602,"message":"invalid 1st argument: transaction 'data': value was too short"}}
```

Couldn't really find any precompile that works with empty calldata and gives a return data with prime length. It means we cannot use a precompile. After playing around a bit, it was fairly convincing that forget prime length return data, there is no way this static call could return a non-zero length return data.

But wait, didn't we just eliminated all the possibilities? Is this challenge even solvable? 

_\*Looks at the scoreboard, single integer number of people have solved it\*_ 

Okay, means definitely there's some advanced stuff going on here and something is missing in my observations.

Oh! could it be related to that weird `mstore(a, b)` which I believed it was there for no reason. Hmm, after a careful observation, I could see that we do not need to have the static call return a data with prime length, the check is `data.length == c`, where `data.length` is taken from memory. So we have to use the `mstore(a, b)` to write at a location and manipulate the value of `data.length`. To know where in the memory `data.length` is, I'm just using hardhat-tracer to print MLOAD operations.

```sh
$ npx hardhat test --trace --opcodes MLOAD
```

![Terminal screenshot of display of MLOAD opcodes during the execution](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/12h46gbvl0hb77t60orj.png)

Here, we can see after the static call to the `0x00..184` address, there is an MLOAD at location 0x60 for reading `data.length`. Memory location 0x60 is a special one because it always points to zero, it's used as the initial value for dynamic memory arrays when they are empty ([source](https://docs.soliditylang.org/en/latest/internals/layout_in_memory.html#layout-in-memory)).

Note that `mstore(a, b)` is done before the declaration of `bytes memory data`. And since the return data is empty, solidity uses 0x60 as the location for the `data` variable. Solidity docs mention that this memory slot should not be written to, since it'd cause empty arrays to assume non-zero length. This helps us!

Now comes another tricky part, we have to figure out how to rearrange our calldata. I'm reverting our calldata changes back to step3 initial, and then moving it further by one word (instead of two words as we previously did). We have to do this because the `mstore(a, b)` writes at memory slot `a`, and we want it to be `0x60`.

```ts
"0x890d6908" + // 4 byte selector for solve() method
[
  "0000000000000000000000000000000000000000000000000000000000000060", // 0x40 to 0x60
  "00000000000000000000000000000000000000000000000000000000000000e0", // 0xc0 to 0xe0
  "0000000000000000000000000000000000000000000000000000000000000000", // inserted here
  "000000000000000000000000000000000000000000000000000000000000004b",
  "604080600b6000396000f300a86410b6215c11e36c6a60d02277415f69393b69",
  "2b6799805ee75969df78d25398733fc3e0438bbcbb9e37fa1ff5da79660324a6",
  "8452a4311cc7238d7431fa000000000000000000000000000000000000000000",
  "0000000000000000000000000000000000000000000000000000000000000000",
].join(""),
```

Okay, but what about previous stage where these first 4 words need to be primes? We might slightly change them. We know that 0x61 and 0xe3 is a prime. So let's change the first two words to ensure they are prime. That will require shifting the content of towards right by one byte.

```ts
"0x890d6908" + // 4 byte selector for solve() method
[
  "0000000000000000000000000000000000000000000000000000000000000061", // 0x60 to 0x61
  "00000000000000000000000000000000000000000000000000000000000000e3", // 0xe0 to 0xe3
  "0000000000000000000000000000000000000000000000000000000000000001", // keeping this 1
  "00000000000000000000000000000000000000000000000000000000000000004b", // one byte extra here
  "604080600b6000396000f300a86410b6215c11e36c6a60d02277415f69393b69",
  "2b6799805ee75969df78d25398733fc3e0438bbcbb9e37fa1ff5da79660324a6",
  "8452a4311cc7238d7431fa000000000000000000000000000000000000000000",
  "0000000000000000000000000000000000000000000000000000000000000000000000", // three bytes extra here
].join(""),
```

However, you can see that 4th word is zero. Previously it was length of the byte string `a` in stage4. Since the length was under 1 byte, the 4th word became zero. Just to recall, byte string `a` is the creation code of a contract that gets deployed in stage4. Is there a way we can increase the length, by adding some redundant code without affecting behaviour?

Turns out that the answer is yes, we can! This is the evm code we previously used. The current code length is 0x4b, and we want it to exceed 0x100, so that the "1" gets in the 4th word.

```ts
ethers-repl> wordify('00'.repeat(0x100 - 0x4b))
[
  '0000000000000000000000000000000000000000000000000000000000000000',
  '0000000000000000000000000000000000000000000000000000000000000000',
  '0000000000000000000000000000000000000000000000000000000000000000',
  '0000000000000000000000000000000000000000000000000000000000000000',
  '0000000000000000000000000000000000000000000000000000000000000000',
  '000000000000000000000000000000000000000000'
]
```

We can just put the above zeros after our bytecode

```
push1 0x40 // 64 bytes
dup1
push1 0xb // offset in this code
push1 0 // offset in memory
codecopy
push1 0
return
// public key
00a86410b6215c11e36c6a60d02277415f69393b692b6799805ee75969df78d25398733fc3e0438bbcbb9e37fa1ff5da79660324a68452a4311cc7238d7431fa
// add any code after this, it is not used
// redundant code which makes bytecode length 0x100
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000

===================================================

$ evm-run lockbox_stage4.evm
code 604080600B6000396000f300A86410B6215C11E36C6A60D02277415F69393B692B6799805EE75969DF78D25398733FC3E0438BBCBB9E37FA1FF5DA79660324A68452A4311CC7238D7431FA00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
Returned: 00a86410b6215c11e36c6a60d02277415f69393b692b6799805ee75969df78d25398733fc3e0438bbcbb9e37fa1ff5da79660324a68452a4311cc7238d7431fa
```



Now we have to repeat the steps.

```ts
ethers-repl> bytecode = '604080600B......00000000' // paste here
ethers-repl> hexDataLength(bytecode)
256 // this is 0x100
ethers-repl> encoded = defaultAbiCoder.encode(['bytes', 'bytes'], [bytecode, '0x'])
ethers-repl> wordify(encoded)
[
  '0000000000000000000000000000000000000000000000000000000000000040',
  '0000000000000000000000000000000000000000000000000000000000000160',
  '0000000000000000000000000000000000000000000000000000000000000100',
  '604080600b6000396000f300a86410b6215c11e36c6a60d02277415f69393b69',
  '2b6799805ee75969df78d25398733fc3e0438bbcbb9e37fa1ff5da79660324a6',
  '8452a4311cc7238d7431fa000000000000000000000000000000000000000000',
  '0000000000000000000000000000000000000000000000000000000000000000',
  '0000000000000000000000000000000000000000000000000000000000000000',
  '0000000000000000000000000000000000000000000000000000000000000000',
  '0000000000000000000000000000000000000000000000000000000000000000',
  '0000000000000000000000000000000000000000000000000000000000000000',
  '0000000000000000000000000000000000000000000000000000000000000000'
]
```

We have to now repeat the steps similar to previous. I'll just paste the updated calldata.

```ts
"0x890d6908" + // 4 byte selector for solve() method
[
  "0000000000000000000000000000000000000000000000000000000000000061", // 0x60 to 0x61
  "0000000000000000000000000000000000000000000000000000000000000161", // 0x160 to 0x161
  "0000000000000000000000000000000000000000000000000000000000000001", // keeping this 1
  "000000000000000000000000000000000000000000000000000000000000000100", // one byte extra here
  "604080600b6000396000f300a86410b6215c11e36c6a60d02277415f69393b69",
  "2b6799805ee75969df78d25398733fc3e0438bbcbb9e37fa1ff5da79660324a6",
  "8452a4311cc7238d7431fa000000000000000000000000000000000000000000",
  "0000000000000000000000000000000000000000000000000000000000000000",
  "0000000000000000000000000000000000000000000000000000000000000000",
  "0000000000000000000000000000000000000000000000000000000000000000",
  "0000000000000000000000000000000000000000000000000000000000000000",
  "0000000000000000000000000000000000000000000000000000000000000000",
  "0000000000000000000000000000000000000000000000000000000000000000",
].join(""),
```

Note that here, 0x161 is written to memory location 0x61, which basically writes 1 to the 0x60 word (data.length memory slot), the rest 61 is written to next slot and it does not matter.

Now we've got stage1, stage2, stage3, stage4 passing, only stage5 is failing.

![Terminal screenshot of first 4 stages passing, while stage 5 failing](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5tmxljmg8p2pw2ly97fe.png)

## Step 5 - Solve Stage 5

This stage makes a call to the solve() method again, and in the trace above, we can see everything passes but only the stage5 check, which wants the solve() invocation from stage5 to fail.

It's based on the fact that when an execution context makes a CALL, it can at max give 63/64 of the gas available. We have to carefully pass a value of `gasLimit` such that when 63/64 of the gas available is passed to the solve() internal message call, it's not enough for it and it goes out of gas. That only fails the internal msg call, and 1/64 gas is available for the stage5's execution context to proceed. After trying for some values, I found `560_000` to be just enough to work.

![Terminal screenshot of all stages of challenge solved](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/slq23oxyd6vsjruxorso.png)

Yay! The challenge is solved! 

Se* is cool but have you ever tried catching a flag :P

![The feeling when you catch the flag](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gfpjqgalusrkwx078iw1.jpg)

Stay tuned to [@paradigm_ctf](https://twitter.com/paradigm_ctf) for the future editions. Hopefully, we’ll see Lockbox3!

Also, if you made this far, thanks! Btw I’m Soham, and I mostly do open-source contributions to some ethereum dev tools. As a disclosure: hardhat-tracer, evm-run, ethers-repl mentioned above are some of my github projects. Oh yeah, also I'm attending my first Devcon this year as a Devcon Scholar, would love to meet if you are coming too! Looking forward to buidl more stuff. My social links: [github](https://github.com/zemse), [twitter](https://twitter.com/0xZemse).