이더리움과 Eclipse attack

p2p 네트워크는 많은 취약점을 가지고 있는데 대표적인 것이 Eclipse attack이다. Eclipse attack은 네트워크 전체를 공격하는 것이 아니라 목표로 하는 노드의 Routing table을 공격하여 목표로 하는 노드와 전체 네트워크 사이에 악의적인 노드를 집어넣는 공격이다. Routing table을 공격하는 방법이기 때문에 routing-table poisoning이라고도 불린다.

이더리움도 p2p 네트워크를 사용하여 메시지를 주고받기 때문에 eclipse attack이 가능하리라 생각은 했는데 지난 3월 1일 발표 된 페이퍼에 따르면 단 2개의 노드만으로 하나의 노드를 완전히 고립시키는 것이 가능하다고 한다. 이 페이퍼는 올해 1월 진행됐던 바운티 프로그램에서 나온 문제점들을 정리한 페이퍼로 크게 세 가지 공격방법으로 나눌 수 있다.

우선 첫 번째 문제는 이해하기 위해 이더리움이 p2p 네트워크를 어떻게 관리하는지 이해해야 한다. 네트워크 그래프 구성에 가장 중요한 것은 다른 노드를 어떻게 찾을 것인가 하는 것이다. 이를 흔히 node discovery라고 하는데 이더리움은 node discovery를 위해 DHT(Distributed Hash Table) 프로토콜 중 하나인 Kademlia의 일부를 수정해서 사용한다.

Kademlia가 다른 DHT와 다른 가장 큰 특징은 노드 간의 거리를 XOR distance로 측정한다는 것이다. XOR distance의 거리는 symmetric 하므로 노드 아이디만 알고 있으면, 노드 A가 생각하는 노드 B까지의 거리나, 노드 B가 생각하는 노드 A까지의 거리나, 노드 C가 생각하는 노드 A와 노드 B 사이의 거리가 같다. 따라서 각 노드는 자신이 알고 있는 노드 중에서 자신과 가까운 노드들과만 통신하면 적은 연결 수로도 큰 네트워크를 구성할 수 있다는 장점이 있다. Kademlia 페이퍼에는 대략 노드의 개수를 N이라고 할 때 각 노드는 O(log(N))개의 연결만 유지하면 된다고 말한다.

이더리움이 Eclipse attack에 취약했던 근본적인 원인이 여기에 있다. Kademlia 프로토콜에서는 거리를 노드 아이디를 기반으로 구하는데, 어떤 노드의 아이디가 무엇인지 네트워크 안에 있는 모든 노드가 같은 값으로 알고 있어야 한다. 그래야 Kademlia가 주장하는 효율성을 가져올 수 있다. 문제는 분산 환경에서 각자의 아이디를 동의하는 방법이 없기 때문에 연결을 요청하는 노드가 자신의 public key를 알려주면 그 public key를 노드 아이디를 Keccak hashing 한 값을 노드 아이디로 준다. 즉, 각 노드가 자신의 아이디를 정하여 알려주는 것이다. 따라서 하나의 머신에서 여러 개의 노드 아이디를 생성해낼 수 있고, 악의적인 사용자가 하나의 머신을 여러 개의 노드인 것처럼 꾸미는 Sybil attack이 가능해진다.

이를 막기 위해 IP 주소와 노드 아이디를 기억하여 하나의 주소에서 여러 개의 키를 사용 못 하게 하는 것이 필요하지만, 아직 구현된 솔루션은 존재하지 않는다.

두 번째 공격 방법은 서버의 시간을 조정하는 것이다. 이더리움은 replay-attack을 막기 위해서 메시지에 보낸 시간을 적는다. 그리고 받은 메시지의 시간이 현재 시각과 20초 이상 차이가 나면 replay-attack으로 받은 메시지로 가정하고 무시한다. 문제는 replay-attack인지 판단하기 위해 메시지에 적힌 시간을 사용할 뿐, 메시지를 보낸 실제 시간은 알 수 없다는 것이다. 예를 들어 두 노드 사이에 시간이 20초 이상 난다면, replay-attack이 아닌 정상적인 메시지도 절대 처리되지 않는다.

현대 서버 환경에서 이럴 일은 거의 없다. 보통 서버 시간을 수동으로 설정하지 않고 NTP(Network Time Protocol)를 이용해 주기적으로 동기화하기 때문이다. 따라서 모든 서버의 시간은 길어봐야 몇 초 이내의 오차를 가질 것이라고 가정해도 된다.

하지만 이는 정상적인 상황에서만 가능한 가정이다. 만약 희생자가 될 노드가 사용할 NTP 서버를 해킹하거나 NTP 메시지를 가로채 동기화하지 못 하는 상황을 만들면 어떻게 될까. 아니면 조금 더 적극적으로 NTP 메시지를 가로채 실제 시간과 20초 이상 앞으로 당기면 어떻게 될까.

이 경우 희생자 노드는 네트워크상의 모든 노드와 시간 차이가 나게 될 테니, 다른 노드들은 모두 이 노드에서 보내는 메시지를 무시할 것이다. 그렇게 되면 희생자 노드는 네트워크에서 잊히게 되고 결국 희생자와 통신하는 것은 NTP 서버를 해킹했던 공격자 노드밖에 남지 않게 된다.

이는 메시지에 시간을 적는 방식으로 replay-attack을 막으려고 했던 이더리움 프로토콜의 문제다. 이런 공격을 막기 위해서는 세션별 혹은 메시지 별로 nonce를 사용하여 sign 하는 방법을 이용해야 한다. 하지만 이에 관한 솔루션도 아직 이더리움에는 구현된 것은 없는 것으로 보인다. 따라서 이더리움 노드를 돌리고 있는 서버는 시간이 잘 동기화되고 있는지 주기적으로 확인하는 방법밖에 없다.

마지막 공격 방법은 일종의 타이밍 어택이다. 이더리움은 네트워크에 너무 많은 리소스를 사용하지 않기 위해 통신할 최대 연결 수를 정해놓고 있다. 현재 최대 연결 수는 25개로 25개 이상의 연결이 맺어지면 더는 새 연결을 요청하거나 받아주지 않는다. 반대로 말해서 어떤 노드의 연결을 25개 점유할 수 있으면 이 노드는 더 이상 새 연결을 요청하지도 받아주지도 않을 것이다. 그렇다면 이것은 어떻게 가능할까?

간단하다. 연결이 0개인 순간을 노리면 된다. 어떤 서버라도 서버가 처음 시작했을 때는 연결이 0개인 상태가 된다. 이 타이밍을 노려서 25개의 연결을 먼저 맺으면 된다. 그렇다면 서버가 재시작되는 순간은 어떻게 찾을까?

하드웨어 문제든 OS 업데이트든 이더리움 클라이언트를 업데이트하든 서버가 재시작될 이유는 많이 있다. 하지만 조금 더 공격적으로 DoS 공격을 하여 이더리움 클라이언트를 죽이는 방법도 있다. 보통 이더리움 마이닝 노드는 서버가 종료되면 바로 다시 시작하게 돼 있기 때문에 DoS 공격으로 서버를 죽이고 나면 얼마 지나지 않아 서버가 재시작할 것이고 이때 25개 이상의 연결을 빠르게 요청하면 된다.

이를 막는 방법은 앞에서 설명한 두 방법보다 쉽다. 앞의 두 방법은 프로토콜 자체를 바꿔야 하지만 이 공격은 구현체를 수정하는 것만으로 쉽게 막을 수 있다. 전체 연결 개수 제한과 별개로 외부에서 들어오는 연결의 개수를 제한하는 것이다. 즉, 최소 몇 개의 연결은 내가 요청하는 노드와 연결이 되도록 하면 누군가 다른 사람에 의해 내 연결이 독점 당할 위험은 사라진다. 이 솔루션은 2월에 릴리즈 된 go-ethereum 1.8 버전부터 적용된 것으로 보인다.

댓글

  1. kademlia에 대해 알아보던 중에 방문하였습니다. 좋은 자료를 공유해주셔서 감사합니다. 자주 참고하로 들리겠습니다 :)

    답글삭제

댓글 쓰기

이 블로그의 인기 게시물

[C++] enum class - 안전하고 쓰기 쉬운 enum

RAII는 무엇인가

Log Aggregator 비교 - Scribe, Flume, Fluentd, logstash

[Python] cache 데코레이터로 최적화하기

[Web] SpeechSynthesis - TTS API