Learning với First Flight #1: PasswordStore

Category
First Flight
Status
Finished
Publishing/Release Date
Jun 30, 2024
notion image
 
Để 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ạn
if (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.
notion image
Và sau đó mình dùng địa chỉ 0xAb8…35cb2 để getPassword
notion image
notion image
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 getPassword
function 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ông
Cuố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
notion image
đã 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
notion image

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.