I solved all the questions in the competition and won second place!
https://x.com/Aptos/status/1836706894087700800
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.
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.
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.