보안의 제1원칙: 왜 장부를 먼저 정리하고 돈을 내줘야 하는가 morgan021 2025. 11. 8.
> _
디지털 아키텍처는 종종 현실 세계의 은유로 설명된다. 은행 금고, 우편 배달부, 신원 인증. 하지만 이 모든 은유가 무너지는 지점이 있다. 바로 코드의 실행 순서가 물리 법칙을 무시하고, 시간과 상태를 뒤엉키게 만들 때다. 코드의 세계에서 '나중에'라는 약속은 '지금'이라는 현실을 배신할 수 있다.
우리가 구축한 이 거대한 디지털 금융 시스템, 스마트 컨트랙트라는 이름의 이 자동화된 계약들은 한 가지 치명적인 가정을 기반으로 세워졌다. 그것은 바로 코드가 위에서 아래로, 순차적으로, 그리고 방해받지 않고 실행될 것이라는 순진한 믿음이다. 이 믿음이 어떻게 수십억 원의 손실로 이어졌는지, 그리고 왜 코드의 순서를 바꾸는 것만이 유일한 방어선인지 이해하는 것은 이 세계의 제1원칙을 배우는 것과 같다.
보안의 세계는 복잡한 암호학이나 난해한 알고리즘으로 이루어진 것처럼 보이지만, 그 본질은 지독히도 단순한 논리에 있다. 돈을 보내기 전에 장부에서 돈을 먼저 빼는 것. 이 간단한 순서를 지키지 않았을 때, 모든 것이 무너진다.
재진입 공격이란 무엇인가?
모든 디지털 보안 교과서의 첫 페이지를 장식하는 사건이 있다. 2016년의 'The DAO' 해킹. 이것은 단순한 사고가 아니라, 이더리움이라는 생태계의 철학을 송두리째 바꾼 원죄와도 같다. The DAO는 탈중앙화된 자율 투자 펀드로, 수천 명의 투자자로부터 막대한 자금을 모았다. 문제는 이 펀드에서 자금을 인출하는 withdrawBalance라는 코드 조각에 있었다.
이 함수의 로직은 지극히 상식적이었다.
- 당신이 인출할 자격이 있는지 확인한다(Check).
- 당신의 주소로 요청한 만큼의 이더(ETH)를 전송한다(Interact).
- 당신의 계좌 잔액을 0으로 업데이트한다(Effect).
완벽해 보이지 않는가? 하지만 공격자는 이 순서의 치명적인 허점을 발견했다. 공격자는 일반적인 지갑이 아닌, '악성 스마트 컨트랙트'를 이용해 이 withdrawBalance 함수를 호출했다. 이 악성 컨트랙트는 2번 단계에서 이더를 전송받는 순간, 아직 3번 단계(잔액 0으로 업데이트)가 실행되기 전에, 다시 1번 단계의 withdrawBalance 함수를 또 호출(재진입)하도록 프로그래밍되어 있었다.
EVM(이더리움 가상 머신)은 이 재귀적인 호출을 충실히 수행했다.
- [호출 1] DAO 컨트랙트가 공격자의 잔액(예: 100 ETH)을 확인한다(Check).
- [호출 1] DAO가 공격자의 악성 컨트랙트에 100 ETH를 전송한다(Interact).
- [재진입 -> 호출 2] 악성 컨트랙트가 100 ETH를 받자마자, 즉시
withdrawBalance를 다시 호출한다. - [호출 2] DAO 컨트랙트가 공격자의 잔액을 확인한다. 1번 호출의 3번 단계(잔액 0)가 아직 실행되지 않았으므로, 잔액은 여전히 100 ETH로 조회된다(Check).
- [호출 2] DAO가 공격자의 악성 컨트랙트에 100 ETH를 또 전송한다(Interact).
- 이 과정이 가스가 소진될 때까지 반복되었다.
결과는 참혹했다. 공격자는 자신의 잔액을 단 한 번도 소진하지 않은 채, DAO의 금고가 바닥날 때까지 자금을 반복적으로 인출했다. 이것이 재진입(Re-Entrancy) 공격이다. 이 공격은 시스템의 암호를 푼 것이 아니라, 오직 시스템이 정의한 '논리적 순서'의 허점만을 이용했다. 이 사건으로 인해 이더리움 네트워크는 하드포크를 감행해야 했고, 이더리움과 이더리움 클래식이라는 두 개의 블록체인으로 갈라서는 상처를 입었다.
위험한 순서: Check-Interact-Effect
The DAO의 비극은 이 위험한 패턴을 전 세계에 각인시켰다. 바로 'Check-Interact-Effect'다.
- Check (확인): 사용자가 돈을 인출할 자격이 있는가? (잔액 확인)
- Interact (상호작용): 사용자에게 돈을 보낸다. (외부 주소, 즉 '신뢰할 수 없는' 코드를 호출)
- Effect (효과): 사용자의 잔액을 차감한다. (내부 상태 변경)
문제는 항상 'Interact' 단계에 있다. 외부 주소로 돈을 보낸다는 것은, EVM의 제어권을 잠시 외부의 '신뢰할 수 없는' 컨트랙트에게 넘겨준다는 의미다. 이 외부 컨트랙트가 무슨 짓을 할지 우리는 알 수 없다. The DAO의 경우처럼, 그 컨트랙트가 우리에게 제어권을 돌려주기 전에 우리를 다시 호출할 수도 있다.
이때 'Effect'가 'Interact'보다 뒤에 있다면, 시스템은 치명적인 '중간 상태'에 놓이게 된다. 즉, 돈은 보냈지만 장부에는 아직 기록되지 않은 상태. 공격자는 바로 이 시간적, 논리적 틈새를 파고들어, 장부가 업데이트되기 전에 계속해서 'Check' 단계를 통과하며 돈을 인출한다. 이것은 은행원이 고객에게 현금을 먼저 내어준 뒤, 돌아서서 장부를 정리하려는 것과 같다. 고객이 장부를 정리하기 전에 다시 줄을 서서 현금을 받아 간다면, 은행원은 그것을 막을 방법이 없다.
'Interact'는 시스템의 신뢰 경계를 벗어나는 모든 행위를 의미한다. 다른 컨트랙트를 호출하는 것, 프리컴파일을 호출하는 것, 심지어 네이티브 모듈이 VM 내부를 호출하는 것까지 포함한다. 신뢰할 수 없는 대상과의 상호작용은 언제나 모든 내부 상태 변경이 완료된 후에, 가장 마지막에 수행되어야 한다.
어떤 토큰은 이 위험한 순서를 따르는 이유?
이 8년 전의 고전적인 실수가 현대의 복잡한 인터체인 프로젝트에서 그대로 재현되는 것을 보는 것은 놀라운 일이다. 최근에 내가 분석했던 한 코드는 네이티브 프레임워크(NF, 네이티브 언어)와 가상 머신(VM)이라는 두 개의 다른 세계를 연결하며 토큰을 옮기는 기능이 있었다.
해당 모듈의 핵심 기능인 '변환 함수'는 사용자의 VM 토큰(ERC-20)을 받아, 그것을 소각하고, 그 대가로 네이티브 코인을 발행(mint)해준다. 해당 함수의 코드는 The DAO의 비극을 정확하게 따르고 있었다.
- Check (확인): 사용자의 ERC-20 잔액이 충분한지 확인한다.
- Interact (상호작용): 네이티브 코드가 'VM 호출기'를 통해, 사용자가 등록한 (악의적일 수 있는) '신뢰할 수 없는' ERC-20 컨트랙트에게 해당 잔액을 전송하도록 지시한다.
- Effect (효과): 만약 2번의 VM 호출이 오류 없이 성공적으로 반환되면, 네이티브 코드는 이것이 '성공'이라고 맹목적으로 신뢰하고, 사용자에게 네이티브 코인을 발행해준다.
이것은 The DAO의 실수보다 한 차원 더 복잡하고 위험하다. 솔리디티 대 솔리디티의 재진입이 아니라, 네이티브 언어가 VM을 호출했다가, 그 VM이 다시 네이티브 언어를 재진입하는 '교차 언어 재진입'이기 때문이다.
만약 공격자가 해당 코드를 지나칠 때 The DAO 사건을 기억하고 있다면 어떻게 될까. 우리가 나눈 대화처럼, 공격자는 2번 'Interact' 단계에서 해당 코인을 발행하는 함수를 재호출할 수 있다. 3번 'Effect'(네이티브 코인 발행)가 아직 실행되지 않았기에, 1번 'Check'(잔액 확인)는 계속 통과될 것이고, 공격자는 자신이 가진 ERC-20 토큰은 전혀 소각하지 않은 채(혹은 단 한 번만 소각하고) 네이티브 코인을 이중, 삼중으로 발행받을 수 있다.
안전한 순서: Checks-Effects-Interactions
The DAO의 교훈은 명확한 해결책을 제시했다. 순서를 바꾸는 것. 이 패턴이 바로 'Checks-Effects-Interactions' (CEI)다.
- Checks (확인): 사용자가 인출할 자격이 있는가? (잔액 100 확인)
- Effects (효과): 즉시 사용자의 잔액을 0으로 업데이트한다. (내부 상태 변경을 먼저 수행)
- Interactions (상호작용): 사용자에게 100을 전송한다. (외부 호출을 가장 마지막에 수행)
이 안전한 순서에서는, 3번 'Interact' 단계에서 공격자가 재진입을 시도하여 1번 'Check' 단계로 돌아가더라도, 2번 'Effect' 단계가 이미 실행되어 잔액이 0으로 업데이트되었기 때문에, 1번 'Check'는 즉시 실패한다. 공격자는 단 한 번의 인출만 성공할 수 있을 뿐, 반복적인 탈취는 불가능하다.
이것은 은행원이 고객에게 현금을 내주기 전에, 먼저 컴퓨터에 '출금 완료'를 입력하고 저장 버튼을 누르는 것과 같다. 고객이 다시 줄을 서도, 전산상 잔액은 이미 0이다. 이 간단한 순서의 변경이 수십억 달러의 자산을 지켜낸다. 모든 스마트 컨트랙트 개발자, 나아가 네이티브 모듈과 VM을 연동하는 모든 시스템 설계자는 이 원칙을 코드의 첫 줄처럼 새겨야 한다.
'변환 함수' 수정안: 사용자의 잔액을 먼저 차감하는 방법
다시 돌아와서, 최근에 분석했던 '변환 함수'도 이 CEI 패턴을 따라야만 한다. 하지만 네이티브 언어가 VM의 잔액(ERC-20)을 직접 수정(Effect)할 수는 없다. 이 경우 'Effect'는 어떻게 적용해야 할까?
이 문제를 해결하는 방법은 두 가지다. 첫째는 '재진입 방지 락'을 네이티브 코드 수준에서 구현하는 것이다. 즉, 함수가 시작될 때 isConverting[userAddress] = true라는 플래그를 상태에 저장하고, 함수가 끝나면 false로 되돌린다. 만약 함수가 재진입되면, 이 플래그를 확인하고 즉시 실행을 중단시킨다.
둘째는 '사후 검증(Post-Interaction Check)'을 추가하는 것이다. 이 패턴은 CEI의 정신을 따르면서도, 신뢰할 수 없는 외부 호출을 다루는 데 더 적합하다.
- 수정안 (사후 검증):
- Check (사전 확인): 잔액이 100임을 확인한다.
- Interact (외부 호출): 신뢰할 수 없는 ERC-20에게 토큰을 전송하라고 요청한다.
- Post-Check (사후 확인): "잔액이 이제 0이 되었는가?"를 다시 확인한다. 만약 재진입 공격으로 잔액이 여전히 100이거나, 혹은 악성 코드가 100이 아닌 50만 전송했다면, 이 검증은 실패한다.
- Effect (상태 변경): 오직 3번의 사후 검증이 성공했을 때만 네이티브 코인 100개를 발행한다.
이 '사후 검증' 패턴은 외부 호출의 반환값을 맹목적으로 신뢰하는 대신, 외부 호출이 실제로 일으킨 '상태 변화'(잔액 감소)를 직접 검증함으로써 재진입 공격을 원천적으로 차단한다.
이 패턴이 모든 것을 해결하는가? (CEI 패턴의 한계와 다른 방어의 필요성)
CEI 패턴(혹은 '사후 검증'을 포함한 변형)은 재진입 공격을 방어하는 데 매우 강력하다. 하지만 이것이 모든 문제를 해결하는 만병통치약은 아니다.
첫째, CEI 패턴은 더 복잡한 로직에서는 적용하기 까다롭다. 때로는 외부 호출의 다양한 결과값에 따라 내부 상태를 다르게 변경해야 하는 경우도 있다.
둘째, 이 패턴은 '재진입 방지 락(Mutex Lock)'만큼 강력하지 않다. 락은 isConverting = true라는 플래그를 세워, 함수 자체가 두 번 실행되는 것을 원천적으로 차단한다. CEI 패턴이 순서의 문제라면, 락은 동시성의 문제다. 민감한 코드에는 이 두 가지 방어를 모두 적용하는 것이 가장 안전하다.
결국 시스템 보안이란 하나의 완벽한 패턴이 아니라, 각기 다른 위협(논리적, 경제적, 동시성)을 막아내는 방어막들의 겹겹이 쌓인 레이어다. CEI는 그중 가장 중요하고 기본적인 첫 번째 방어막일 뿐이다.
3줄 요약
- 재진입 공격은 "Check(확인) -> Interact(외부 호출) -> Effect(상태 변경)"라는 위험한 순서를 악용하며, The DAO 해킹이 이 패턴으로 발생했다.
- 네이티브 코드가 솔리디티와 통신하는 경우, 어떤 코드에서는 신뢰할 수 없는 VM을 먼저 호출(Interact)한 뒤, 성공하면 네이티브 코인을 발행(Effect)하는 동일한 위험 패턴을 가지고 있다.
- 이에 대한 방어는 "Check -> Effect -> Interact" 순서로 변경하거나, 최소한 외부 호출 후에 잔액을 '사후 검증'하는 것이다.
'MACHINE: EXPLOIT' 카테고리의 다른 글
| 레고인가, 조각상인가? 블록체인을 짓는 두 가지 철학 (0) | 2025.11.12 |
|---|---|
| 블록체인의 인터넷은 누가 움직이는가 (0) | 2025.11.12 |
| 이름은 운명이다? 메타마스크는 왜 스냅을 선택했나 (0) | 2025.11.10 |
| 지분 증명 시대의 보이지 않는 권력자들, 검증인 (0) | 2025.11.10 |
| 실행자의 유령. 코드상의 나는 진짜 나인가? (0) | 2025.11.08 |
| 블록체인은 어떻게 현실을 움직이는가? 오라클의 양방향 혁명 (0) | 2025.11.07 |
| 블록체인의 심장, EVM은 왜 외부 세계와 단절되어야 하는가 (0) | 2025.11.07 |
| AI '기억 예산'의 숨겨진 규칙. "네"라고 대답했지만, 아무것도 기억하지 않는다 (0) | 2025.11.06 |
| 인터넷이 정보를 해방시켰다면, IBC는 자산을 해방시킨다 (0) | 2025.11.05 |
| EVM은 왜 미래를 예측할 수 없는가 (0) | 2025.11.04 |