// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import"forge-std/Test.sol";
/*
Lottery game: anyone can call pickWinner to get prize if you are lucky.
Refers to JST contract backdoor. many rugged style's contract has similar pattern.
Looks like theres is no setwinner function in contract, how admin can rug?
*/
contract ContractTest is Test {
LotteryGameLotteryGameContract;
functiontestUnsafeCall() public {
address alice = vm.addr(1);
address bob = vm.addr(2);
LotteryGameContract = newLotteryGame();
console.log("Alice perform pickWinner, of couse she will not be a winner");
vm.prank(alice);
LotteryGameContract.pickWinner(address(alice));
console.log("Prize: ",LotteryGameContract.prize());
console.log("Now, admin set the winner to drain out the prize.");
LotteryGameContract.pickWinner(address(bob));
console.log("Admin manipulated winner: ",LotteryGameContract.winner());
console.log("Exploit completed");
}
receive() payable external{}
}
contract LotteryGame {
uint256 public prize = 1000;
address public winner;
address public admin = msg.sender;
modifier safeCheck() {
if (msg.sender == referee()) {
_;
} else {
getkWinner();
}
}
functionreferee() internal view returns (address user) {
assembly {
// load admin value at slot 2 of storage
user := sload(2)
}
}
functionpickWinner(address random) safeCheck public {
assembly {
// admin backddoor which can set winner addresssstore(1, random)
}
}
functiongetkWinner() public view returns(address) {
console.log("Current winner: ",winner);
return winner;
}
}
코드 test결과이다.
Alice가 우승자를 뽑는 데 그녀는 우승자가 아니라고 말한다.
현재 우승자는 없지만 관리자가 우승자를 조작했다고 나와있다.
Trace를 분석해 보자
alice address : 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf
bob address: 0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF
assembly{
mul(1, add(2, 3)) -> push1 3 push1 2 add push1 1 mul
어셈블리 로컬 변수: let x := add(2, 3), let y := mload(0x40)
}
외부 변수 액세스
functionf(uint x) public {
assembly{
x := sub(x, 1)
}
}
라벨 let x := 10repeat: x := sub(x,1) jumpi(repeat, eq(x,0))
for { let i := 0 } lt (i, x) { i:= add(i, 1) } { y := mul(2, y) }
ifslt(x, 0) { x:= sub(0, x) }
switch x case0 { y := mul(x, 2) } default { y:= 0 }
functionf(x) -> y { switch x case0 { y := 1 } default { y := mul(x, f(sub(x, 1))) } }
대표적으로 이렇게 있는 데 opcode 앞에 s가 있으면 Storage와 관련이 있고 m이 있으면 memory와 관련이 있다.
문제에서 sload(p)는 storage[p]를 뜻하고 sstore(p, v)는 storage[p] := v를 뜻한다. 다음과 같이 작동을 한다.
functionreferee() internal view returns (address user) {
assembly {
// load admin value at slot 2 of storage
user := sload(2) // user = storage[2]
}
}
functionpickWinner(address random) safeCheck public {
assembly {
// admin backddoor which can set winner addresssstore(1, random) // storage[1] = random
}
}
처음 LotteryGameContract가 생성된 Storage를 확인해 보면 다음과 같다.