welcome

First, we can take a look at framework/src/main.rs. Our framework-solve/solve.py actually interacts with this program.

You can see that the program first declares two accounts: challenger and solver, then deploys two programs: challenge and solve. The challenge program is the Aptos Move program located under framework/challenge, while solve is the program we upload, located under framework-solve/solve.

Next, the program, acting as the challenger, calls challenge::welcome::initialize and then waits to allow us to call any_address::module::function, but the caller is always solver. The loop exits after executing exploit::solve.

Finally, if challenge::welcome::is_solve executes normally, the flag is sent.

/* welcome/framework/src/main.rs */
022 | 
023 | async fn handle_client(mut stream: TcpStream) -> Result<(), Box<dyn Error>> {
024 |     let modules = vec!["welcome"];
025 | 
026 |     // Initialize Named Addresses
027 |     let named_addresses = vec![
028 |         (
029 |             "challenger",
030 |             "0xf75daa73fc071f93593335eb9033da804777eb94491650dd3f095ce6f778acb6",
031 |         ),
032 |         (
033 |             "solver",
034 |             "0x9c3b634ac05d0af393e0f93b9b19b61e7cac1c519f566276aa0c6fd15dac12aa",
035 |         ),
036 |         ("challenge", "0x1337"),
037 |         ("solution", "0x1338"),
038 |     ]
039 |     .into_iter()
040 |     .map(|(name, addr)| (name.to_string(), NumericalAddress::parse_str(addr).unwrap()))
041 |     .collect::<Vec<_>>();
062 |     // Publish Challenge Module
063 | 
064 |     let chall_addr = publish_modules(&mut adapter, modules, "challenger", "challenge")?;
065 |     // Read Solution Module
066 |     let solution_data = read_solution_module(&mut stream)?;
067 | 
068 |     // Send Challenge Address
069 |     send_message(
070 |         &mut stream,
071 |         format!("[SERVER] Challenge modules published at: {}", chall_addr),
072 |     )?;
073 | 
074 |     // Publish Solution Module
075 |     let sol_addr = publish_solution_module(&mut adapter, solution_data, "solver", "solve")?;
083 |     // Call initialize Function
084 |     let ret_val = call_function(
085 |         &mut adapter,
086 |         chall_addr,
087 |         "welcome",
088 |         "initialize",
089 |         "challenger",
090 |     )?;
091 | 
092 |     // Call solve Function
093 |     loop {
094 |         send_message(
095 |             &mut stream,
096 |             "[SERVER] function to invoke: ".to_owned(),
097 |         )?;
098 |         stream.flush();
099 |         let function_to_invoke = read_function_to_invoke(&mut stream)?;
100 |         if let Some((address, module, function)) = parse_function_to_invoke(&function_to_invoke) {
101 |             let ret_val = call_function(&mut adapter, address, module, function, "solver")?;
102 | 
103 |             if module == "exploit" && function == "solve" {
104 |                 break;
105 |             }
106 |         }
107 |     }
108 | 
109 |     // Check Solution
110 |     let sol_ret = call_function(&mut adapter, chall_addr, "welcome", "is_solved", "challenger");
111 |     validate_solution(sol_ret, &mut stream)?;
112 | 
113 |     Ok(())
114 | }

So our target is to make challenge::welcome::is_solve to return normally.

initialize creates a ChallengeStatus object and stores it under the account name (which is effectively the caller: challenger).

solve and is_solve always use the object stored in @challenger , no matter who calls it, so we just need to call solve to set challenge_status.is_solved to true and pass the is_solved check.

super_mario_16 / super_mario_32

The goal of the challenge is for mario.hp > bowser.hp, with bowser.hp starting at 254.

The start_game function allows us to obtain a Mario object with an initial hp of 0. We can increase hp by 2 each time we call train_mario. However, since hp is a u8, it can only increase up to 254. Adding 2 will cause an overflow and reset hp to 0.

We can notice that the set_hp function allows us to pass an Object<Bowser> and modify its hp to any value, with no permission controls in place. We can use this function to set bowser.hp to 0. After calling train_mario once, we can then use the battle function with Mario to win.

super_mario_64

Compared to super_mario_32, set_hp now has authentication. It requires that the signer account must be the owner of the bowser_obj. Since bowser_obj's owner is an object created during initialize, which belongs to the challenger who called initialize, we are unable to call set_hp.

However, in the battle function, if Mario and Bowser have the same hp (which we can achieve by using start_game and calling train_mario 127 times), the Mario struct associated with config.wrapper will be transferred to us. Since this object actually contains the Peach, Mario, and Bowser structs, we become the owner of the Object<Bowser>. At this point, we can call set_hp to set Bowser's hp to 0, and then use a Mario with hp set to 2 to call battle and win.

simple_swap