Để bắt đầu cho chuỗi học hành audit smart contract thì mình bắt đầu với First Flight 1 chương trình learning của Code Hawks. First Flight sẽ đi từ các mức độ dễ đến khó, từ sLOC nhỏ đến sLOC to, khá phù hợp cho mình học tập.
Ở trong First Flight 1 này thì sẽ là PasswordStore, nghe là đủ hiểu sử dụng blockchain để thực hiện lưu password rồi. Giờ thì mình cùng xem qua full code nhé.
// SPDX-License-Identifier: MIT pragma solidity 0.8.18; /* * @author not-so-secure-dev * @title PasswordStore * @notice This contract allows you to store a private password that others won't be able to see. * You can update your password at any time. */ contract PasswordStore { error PasswordStore__NotOwner(); address private s_owner; string private s_password; event SetNetPassword(); constructor() { s_owner = msg.sender; } /* * @notice This function allows only the owner to set a new password. * @param newPassword The new password to set. */ function setPassword(string memory newPassword) external { s_password = newPassword; emit SetNetPassword(); } /* * @notice This allows only the owner to retrieve the password. * @param newPassword The new password to set. */ function getPassword() external view returns (string memory) { if (msg.sender != s_owner) { revert PasswordStore__NotOwner(); } return s_password; } }
1, Phân tích code
Code khá ngắn nên khá dễ để hiểu cách hoạt động của code này.
Đầu tiên là hàm khai báo contract name
contract PasswordStore cái này thì đơn giản không có gì.Tiếp đến
error PasswordStore__NotOwner(); là định nghĩa 1 lỗi tùy chỉnh, ví dụ như là đoạnif (msg.sender != s_owner) { revert PasswordStore__NotOwner(); }
nếu như người gọi lệnh không phải là người tạo ra contract này thì sẽ báo lỗi
PasswordStore__NotOwner();Ví dụ mình triển khai code trên trong https://remix.ethereum.org/ Mình sẽ sử dụng địa chỉ 0x5B3…eddC4 để deploy contract.
Và sau đó mình dùng địa chỉ 0xAb8…35cb2 để getPassword
Trên console sẽ
revert báo lỗi PasswordStore__NotOwner Tiếp đến
address private s_owner; khai báo 1 biến s_owner thuộc kiểu address. sử dụng để lưu trữ address dạng 0x..... Biến này dc khai báo private nên chỉ được gọi ở trong hợp đồng.Tiếp đến là
string private s_password; khai báo biến s_password là dạng string để lưu trữ mật khẩu dạng clear text và cũng được đặt private.event SetNetPassword(); Khai báo 1 event là SetNetPassword khi mà hành động set password thành công.Với event thì phải đi kèm với
emit , event là khai báo còn emit sẽ phát ra sự kiện đó ra bên ngoài contract. Từ đó các ứng dụng bên ngoài khi lắng nghe các sự kiện của hợp đồng có thể nhận được thông báo.constructor() { s_owner = msg.sender; }
Khai báo hàm constructor. Trong solidity hàm constructor sẽ chỉ được khai báo 1 lần và được sử dụng để:
- Constructor được sử dụng để thiết lập các giá trị ban đầu cho các biến trạng thái ví dụ:
constructor(string memory _name, uint256 _price) { name = _name; price = _price; owner = msg.sender; // Địa chỉ người triển khai hợp đồng trở thành chủ sở hữu }
- Bất kỳ logic nào cần được thực hiện chỉ một lần khi hợp đồng được tạo ra có thể được đặt trong constructor
- Constructor có thể nhận các tham số đầu vào từ người triển khai hợp đồng
constructor(uint256 _goal, uint256 _durationInDays) { owner = payable(msg.sender); // Người triển khai trở thành chủ sở hữu và có thể nhận tiền goal = _goal; // Mục tiêu gây quỹ (tính bằng wei) deadline = block.timestamp + (_durationInDays * 1 days); // Thời hạn gây quỹ }
Tiếp đến là hàm
setPassword function setPassword(string memory newPassword) external { s_password = newPassword; emit SetNetPassword(); }
Hàm này sẽ nhận 1 dữ liệu với biến newPassword dạng string memory. Memory trong solidity ám chỉ việc sẽ lưu biến đó trong bộ nhớ, chứ không phải lưu trên blockchain.
Tiếp đến hàm được đặt
external nghĩa là hàm này có thể được gọi từ bên ngoài. Sau đó sẽ set
s_password = newPassword; là đưa dữ liệu vào biến s_password . Sau khi thực hiện xong sẽ phát ra 1 sự kiện SetNetPassword. Tiếp đến hàm
getPasswordfunction getPassword() external view returns (string memory) { if (msg.sender != s_owner) { revert PasswordStore__NotOwner(); } return s_password; }
Trong đoạn
function getPassword() external view returns (string memory) sẽ khai báo là hàm này external view có thể được gọi từ bên ngoài và chỉ có thể view không thể sửa đổi trạng thái được. Sau đó sẽ return ra 1 string đã được lưu tạm thời trong bộ nhớ.Kiểm tra điều kiện
if (msg.sender != s_owner) { xem người gọi hàm có phải là người tao ra hợp đồng như ở trên đã giải thích hay khôngCuối cùng là trả ra password được lưu trong bộ nhớ
return s_password;2, Tìm lỗ hổng
Để tìm lỗ hổng thì mình thực hiện theo top 10 của immunefi như sau:
Vulnerabilities | PasswordStore |
None | |
None | |
None | |
2 | |
None | |
None | |
None | |
None | |
None | |
None |
Mình sẽ áp dụng 10 lỗi trên để review cho code PasswordStore này.
2.1. Improper Input Validation
Dựa vào code trên giờ mình sẽ đi tìm các input đầu vào:
Có mỗi hàm
setPassword là có input string là password thì có thể là bất kỳ ký tự nào, nên không cần phải validate.2.2. Incorrect Calculation
Code trên không thực hiện tính toán gì.
2.3. Oracle/Price Manipulation
Code trên cũng không có lấy giá từ oracle hay thực hiện tính toán giá.
2.4. Weak Access Control
Trong đoạn code trên mặc dù có khai báo constructor
s_owner = msg.sender; nhưng trong hàm setPassword lại không thực hiện check chỉ s_owner mới được set mật khẩu. ⇒ Code trên có 1 lỗi về acces control: Bất kỳ ai cũng có thể set được mật khẩu
Một logic đặc biệt nữa là password khi lưu trên contract thì bất kỳ ai cũng có thể thấy được.
⇒ Mật khẩu dễ dàng bị lộ khi lưu trên hợp đồng
2.5. Replay Attacks/Signature Malleability
Code trên không có thực hiện các lệnh call hay transfer
2.6. Rounding Error
Code trên không có thực hiện tính toán và làm tròn số.
2.7. Reentrancy
Code trên không có thực hiện các lệnh call hay transfer
2.8. Frontrunning
Code trên không có thực hiện các lệnh call hay transfer
2.9. Uninitialized Proxy
Code trên không có proxy
2.10. Governance Attacks
Code trên không thực hiện governance
3, Viết testcase
Vậy chúng ta có 2 lỗi và sẽ thực hiện viết testcase cho từng lỗi như sau
3.1. Bất kỳ ai cũng có thể set được mật khẩu
Dựa vào test case set mật khẩu trước đó:
function test_owner_can_set_password() public { vm.startPrank(owner); string memory expectedPassword = "myNewPassword"; passwordStore.setPassword(expectedPassword); string memory actualPassword = passwordStore.getPassword(); assertEq(actualPassword, expectedPassword); }
Mình sẽ sửa lại khi khởi tạo sẽ thay bằng 1 địa chỉ khác như sau
function test_not_owner_can_set_password() public { vm.startPrank(address(1)); string memory expectedPassword = "myNewPassword"; passwordStore.setPassword(expectedPassword); }
Và sau đó mình tiến hành chạy thử bằng foundry
đã pass test case test_not_owner_can_set_password.
3.2. Mật khẩu dễ dàng bị lộ khi lưu trên hợp đồng
Mình sẽ thực hiện test case như sau:
- Đầu tiên là set mật khẩu
- Tiếp đến sẽ thực hiện load biếnpasswordStore trong bộ nhớ
- Convert thành string
- Và in ra console
function test_not_owner_can_get_password() public { vm.startPrank(owner); string memory expectedPassword = "myNewPassword"; passwordStore.setPassword(expectedPassword); uint256 S_PASSWORD_STORAGE_SLOT_VALUE = 1; bytes32 slotData = vm.load( address(passwordStore), bytes32(S_PASSWORD_STORAGE_SLOT_VALUE) ); string memory anyoneCanReadPassword = string( abi.encodePacked(slotData) ); console.log(anyoneCanReadPassword); }
Sau đó thực hiện chạy test case với foundry
4, Fix lỗi
Với lỗi đầu tiên nên thêm đoạn check
if (msg.sender != s_owner) { revert PasswordStore__NotOwner(); }
trước khi thực hiện set mật khẩu.
Còn lỗi thứ 2 thì khá khó, mục đích của blockchain không phải để lưu trữ mật khẩu nên việc lưu trữ mật khẩu để giữ bí mật không phải là cách tốt.