일렉트럼 송금 사고: 수수료를 올리던 이용자가 다른 거래에 서명했다
비트코인을 보냈는데 지갑에 계속 ‘미확인’이라고 표시된다. 받는 사람은 입금을 기다리고, 보내는 사람은 수수료를 너무 적게 냈는지 생각하게 된다. 이때 수수료를 올리는 기능은 이미 접수한 송금에 급행 요금을 보태는 것처럼 보일 수 있다. 그런데 수수료를 더 냈을 뿐인데 돈이 처음 지정한 상대가 아닌 공격자에게 갔다면, 어디서 일이 어긋난 것일까.
핵심은 수수료를 올리는 과정에도 거래에 대한 서명이 들어간다는 점이다. 지갑은 단순히 처리 속도를 바꾸라는 신호를 보내는 것이 아니라, 새 수수료를 반영한 거래를 만들어 승인하고 전파한다. 따라서 수수료 증액의 출발점이 된 거래 내용이 잘못됐다면, 이용자가 의도한 일과 지갑이 실제로 승인한 일이 달라질 수 있다. 일렉트럼의 보안 권고문에 기록된 송금 사고가 바로 이 틈을 보여 준다.
수수료를 올릴 때도 돈의 행선지에 서명한다
비트코인 거래에는 어떤 자금을 써서 어느 주소로 얼마를 보낼지가 담긴다. 이때 쓸 자금의 출처를 ‘입력’이라고 부른다. 과거에 받은 자금을 이번 송금에 사용하는 것이다. 거래를 보내려면 그 자금을 사용할 권한이 있다는 전자서명이 필요하다. 지갑은 이용자의 비밀키로 이 서명을 만들며, 서명은 거래 내용과 연결된다.
여기서 서명은 단순한 본인 확인과 다르다. ‘이 지갑의 주인이 맞다’고 밝히는 데서 끝나는 것이 아니라, 특정한 거래 내용에 자금 사용 권한을 부여한다. 따라서 정상적인 비밀키로 만든 서명이라도 그 대상이 이용자가 원하지 않은 거래라면 문제가 생긴다. 비밀키가 안전하게 보관됐는지와 원하는 거래에 서명했는지는 서로 관련되지만 같은 질문은 아니다.
송금이 미확인 상태라는 것은 아직 채굴자가 그 거래를 블록에 넣지 않았다는 뜻이다. 채굴자는 일반적으로 높은 수수료를 내는 거래를 우선하므로, 수수료를 올리면 처리 유인을 높일 수 있다. 다만 일렉트럼 문서가 설명하는 수수료 증액은 모든 거래에 적용하는 별도 요금 납부가 아니다. 대체 가능한 미확인 거래를 더 높은 수수료가 붙은 새 거래로 바꾸는 방식이다.
이용자가 수수료를 정하면 지갑은 그에 맞는 대체 거래를 준비한다. 그리고 이용자는 그 거래에 서명하고 네트워크로 전파한다. 흔히 RBF라고 부르는 방식이다. 정상적인 경우에는 원래 의도한 송금을 유지하면서 수수료를 높이는 데 쓰인다. 그러나 서명하는 대상은 ‘수수료 차액’만이 아니다. 받는 주소와 금액 등 자금 이동 내용이 담긴 거래다.
미확인 거래의 처리를 유도하는 다른 방법도 있다. ‘자식이 부모의 비용을 낸다’는 뜻의 CPFP는 앞선 거래를 대체하지 않고, 그 거래에서 받은 자금을 사용하는 새 거래를 만든다. 새 거래에 높은 수수료를 붙여 앞선 거래의 낮은 수수료를 보완하는 것이다. 자식 거래를 처리하려면 그 자금의 출처인 부모 거래도 처리해야 하므로 이런 유인이 생긴다.
이 방식은 받는 사람이 쓰거나, 보내는 사람에게 돌아오는 거스름돈이 있을 때 보내는 사람이 쓸 수도 있다. 중요한 차이는 부모 거래의 수수료를 바꿔 다시 서명하는 것과, 부모에게 의존하는 별도 거래를 만드는 것이 다른 동작이라는 점이다. 이번 사고에서 악용된 것은 조작된 거래를 대상으로 한 RBF 수수료 증액이었다. 대기 중인 거래를 돕는 모든 방법을 같은 행동으로 묶으면 사고가 난 지점을 놓치게 된다.
지갑의 ‘미확인’ 내역은 어디서 왔을까
그렇다면 지갑은 수수료를 올릴 대상이 어떤 거래인지 어디서 알아낼까. 일렉트럼은 이용자의 기기에서 비트코인의 모든 거래를 직접 처리하는 대신 서버의 도움을 받는다. 서버가 주소와 관련된 거래 내역을 알려 주고, 지갑이 만든 거래를 네트워크로 전파하는 역할도 맡는다. 이런 분담 덕분에 지갑은 적은 자원으로 빠르게 작동할 수 있다.
그렇다고 서버의 말을 모두 그대로 믿는 것은 아니다. 일렉트럼은 블록에 포함된 거래에 대해서는 그 거래가 실제로 블록에 들어갔다는 증거를 확인한다. 이를 간편 결제 검증이라고 한다. 반면 아직 블록에 들어가지 않은 미확인 거래에는 같은 포함 증거가 없다. 일렉트럼 문서는 이 단계의 거래 정보는 주 서버가 알려 주는 내용에 의존한다고 설명한다.
따라서 지갑 화면에 거래가 나타나는 것과, 그 거래가 블록에 기록돼 확인된 것은 구분해야 한다. 특히 미확인 내역은 서버가 지갑에 전하는 정보이기도 하다. 서버가 어떤 거래를 누락하거나 잘못된 내용을 전달하면, 화면은 이용자의 송금이 어디까지 진행됐는지 정확하게 보여 주지 못할 수 있다. ‘미확인’이라는 표시는 그 자체로 거래의 유효성이나 전파 사실을 모두 증명하지 않는다.
일렉트럼이 여러 서버와 연결된다는 사실도 이 차이를 없애지는 않는다. 문서에 따르면 여러 서버에서 블록 관련 정보와 수수료 추정치를 얻지만, 이용자 주소의 거래 내역을 받아 오는 역할은 선택된 주 서버가 맡는다. 여러 연결이 있다는 사실만으로 화면의 미확인 거래 하나하나를 여러 서버가 대조한다고 볼 수는 없는 구조다.
일렉트럼 보안 권고문이 지적한 공격은 이 정보 경로를 이용했다. 대상은 4.8.3 미만의 취약한 버전이며, 이용자가 연결한 주 서버가 악성 서버인 상황이었다. 공격자는 비트코인 블록에 기록된 송금을 고친 것이 아니라, 지갑이 받아들이는 미확인 거래 내역을 바꿨다.
가짜 거래에 유효한 서명을 붙인 순간
권고문에 따르면 공격은 이용자가 새 거래를 전파하려는 순간부터 시작할 수 있었다. 악성 서버는 전달받은 원래 거래를 네트워크로 보내지 않고 붙잡아 뒀다. 그런 다음 같은 자금을 쓰지만 받는 주소를 공격자의 주소로 바꾼 다른 거래를 지갑의 내역에 넣었다. 지갑 화면에는 원래 거래 대신 이 바뀐 거래가 미확인 상태로 나타날 수 있었다.
두 거래는 같은 자금을 서로 다른 곳에 보내려 한다는 점에서 충돌한다. 그러나 공격자가 끼워 넣은 거래는 처음부터 네트워크가 받아들일 수 있는 유효한 거래가 아니었다. 서명이 유효하지 않았기 때문이다. 공격자는 이용자의 자금을 쓸 수 있는 정상적인 서명을 갖고 있지 않았다. 그 상태로는 공격자 주소를 적어 넣는 것만으로 돈을 가져갈 수 없었다.
취약한 지갑은 이 서명 문제를 확인하지 않은 채 해당 거래를 미확인 내역에 올릴 수 있었다. 이용자가 그 내역을 자신이 방금 보낸 송금이라고 여기고 수수료를 올리면, 상황이 달라졌다. 지갑이 공격자 주소가 담긴 거래에 더 높은 수수료를 반영하고, 이용자의 비밀키로 새로 서명해 전파했기 때문이다. 원래는 유효하지 않았던 거래가 실제 자금 사용 권한을 얻는 순간이었다.
이때 공격자에게 간 돈은 추가로 낸 수수료가 잘못 배달된 것이 아니다. 수수료는 채굴자가 거래를 처리하도록 하는 보상이고, 공격자 주소로 지급되는 금액은 거래에 적힌 별도의 자금 이동이다. 이용자는 처리 비용을 조정한다고 생각했지만, 실제로는 행선지가 바뀐 거래를 승인한 셈이다. 수수료 인상은 공격자가 돈을 받는 이유라기보다, 필요한 서명을 얻어 내는 계기가 됐다.
전자서명은 거래를 승인할 권한이 있는지를 증명하지만, 이용자가 왜 승인했는지까지 판단하지는 않는다. ‘원래 송금이 늦어서 서둘러 처리하려 했다’는 사람의 의도와 ‘이 주소로 지급하는 거래에 서명했다’는 기계의 처리는 다를 수 있다. 이번 공격은 그 차이를 이용했다. 이 경로에서는 비밀키를 서버로 넘기거나 공격자가 비밀키를 훔칠 필요가 없었다.
권고문에는 속임수를 더 그럴듯하게 만든 정황도 담겨 있다. 두 이용자 보고 모두 공격자 주소의 마지막 네 글자가 원래 받는 주소의 마지막 네 글자와 같도록 만들어졌다고 한다. 주소 전체는 달라도 끝부분만 보면 같은 주소처럼 보일 여지가 있었던 것이다. 권고문은 두 번째 이용자의 보고를 근거로 조작된 거래의 수수료가 0이었다고 기록했다. 일렉트럼 측은 이용자가 수수료를 올리도록 유도하려는 의도였을 것으로 추정했다.
이 정황은 공격자가 단지 잘못된 숫자나 주소를 표시하는 데서 멈추지 않았음을 보여 준다. 원래 거래와 닮은 주소는 다른 거래라는 사실을 알아채기 어렵게 하고, 수수료가 없는 거래는 처리되지 않는 이유를 수수료에서 찾게 할 수 있다. 다만 실제 이용자가 화면의 어느 부분을 보고 어떤 생각으로 승인했는지까지 권고문이 입증하는 것은 아니다. 확인되는 것은 이런 형태의 거래가 제시됐고, 그 거래의 수수료를 올린 뒤 손실을 봤다는 보고다.
일렉트럼 측이 기록한 이용자 보고는 두 건이다. 첫 이용자는 송금 후 지갑 내역에 다른 거래가 나타난 것을 발견했고, 원래 거래가 비트코인 네트워크에 전파되지 않았다고 보고했다. 두 번째 이용자는 바뀐 거래의 수수료를 올린 뒤 돈을 잃었다고 알렸다. 첫 보고는 거래 표시와 전파가 어긋난 정황을, 두 번째 보고는 수수료 증액을 거쳐 실제 손실로 이어진 경로를 드러낸다.
이 두 건으로 전체 피해자 수나 손실액, 공격의 발생 빈도를 계산할 수는 없다. 또한 여기서 확인된 경로는 취약한 일렉트럼과 악성 서버의 조합에 관한 것이다. 다른 지갑에도 같은 문제가 있다거나, 비트코인에서 수수료를 올리는 행위 자체가 공격자에게 돈을 보내는 기능이라는 결론은 자료의 범위를 벗어난다.
수정한 곳은 수수료가 아니라 거래를 받아들이는 단계다
취약한 버전의 지갑은 서명이 유효하지 않은 거래도 먼저 내역에 올리고, 나중에 이용자가 수수료를 조정할 때 유효한 서명을 붙일 수 있었다. 4.8.3에서는 지갑이 소유한 자금을 쓰는 거래라면 그 입력의 서명이 유효한지부터 확인한 뒤 미확인 내역에 넣는다.
여기서 확인하는 질문은 ‘이 돈을 쓰는 데 필요한 서명이 유효한가’다. ‘이용자가 지금 이 수신자에게 돈을 보내고 싶어 하는가’라는 의도를 알아내는 검사는 아니다.
검증 대상도 구분돼 있다. 권고문은 수정 뒤에도 지갑이 소유한 입력만 확인하며, 다른 사람이 자기 자금을 써서 지갑으로 보내는 입금 거래는 같은 서명 검증 대상이 아니라고 밝힌다. 내가 내 자금을 쓰는 거래와 다른 사람이 자기 자금으로 내게 보내는 거래는 출처가 다르다. 이번에 추가한 검사는 전자에 해당한다.
외부에서 오는 미확인 거래까지 온전히 검증하려면 그 거래의 서명만 보는 것으로 끝나지 않는다. 사용한 자금의 출처인 앞선 미확인 거래도 확인하고, 이미 블록에 들어간 거래에 닿을 때까지 거슬러 올라가야 한다. 권고문이 설명하듯 이는 더 넓은 거래 검증이 필요한 작업이다. 따라서 4.8.3을 모든 미확인 거래를 완전히 검증하는 지갑으로 바꾼 수정이라고 설명해서는 안 된다. 이번 공격을 막은 지점과 지갑이 전체 네트워크 정보를 확인하는 범위는 구별된다.
처음의 질문으로 돌아가면 답은 분명하다. 수수료를 올려서 돈의 행선지가 저절로 바뀐 것이 아니다. 악성 서버가 다른 행선지의 거래를 원래 송금처럼 보이게 했고, 수수료를 올리는 과정에서 그 거래에 유효한 서명이 붙었다. 이 사고가 드러낸 것은 속도를 높이는 버튼처럼 보이는 행동도 결국 특정한 자금 이동을 승인하는 일이라는 사실이다.