3월 10, 2023

클레이튼 DApp에 자금 세탁기가? - Stake.ly Mixer 의혹에 관한 기술적 분석

Donkey 때도 그렇고, 실타래 때도 그렇고, 국내 DApp에서 이슈가 생길 때마다 블로그에 글을 쓰는 것이 좋게 느껴지지는 않아서, 다음부터는 이러한 "크립토 렉카" 같은 글은 지양해야겠다고 생각했습니다만, Klaytn의 사내 벤처 회사가 만든 DApp에 자금 세탁 기능이 포함되어 있다고 해서 너무 궁금해진 나머지, 코드를 분석해 정말로 자금 세탁 기능이 있는 것인지 확인해 보았습니다.

클레이튼 DApp에 자금 세탁기가? - Stake.ly Mixer 의혹에 관한 기술적 분석

대체 무슨 일이 일어나고 있는거야?

크립토체크라고 하는 텔레그램 채널에서 "매달 50만 클레이 입금하는 지갑" 이라는 게시글이 올라옵니다. 2월 22일부터 매일 50만 클레이씩 653만 클레이를 바이낸스로 송금하고 있는 지갑이었습니다.

이 지갑을 변창호 코인사관학교 라고 하는 텔레그램 채널에서 분석을 하기 시작합니다. 분석을 해본 결과, 해당 자금의 출처는 클레이튼의 사내 창업 벤처인 크래커랩스에서 만든 DApp인 Stake.ly와 연관된 Contract임을 알아내게 됩니다.

Stake.ly는 클레이튼의 Liquid Staking 서비스로, 사용자들에게 Klay를 받아서, Klay Staking이 가능한 지정된 General Council 노드들이 대신 Staking을 해주고, 그 보상을 사용자들에게 분배해주는 서비스입니다.

Stake.ly 작동 구조 (출처)

위의 구조도와 같이, Klay를 예치하면, 예치의 증표로 같은 양만큼의 stKlay 토큰을 지급하기 때문에, Klay가 Protocol 바깥으로 나오기 위해서는 비슷한 양의 stKlay가 소각되어야 만 합니다. (예치 증서 없이 은행 자금이 새어 나와서는 안되니까요.)

그런데, 의심 계좌들은 딱히 stake를 하지도, stKlay를 소각하지 않고도, Klay를 잔뜩 받아갑니다. 여기서 이 분석글은 Stake.ly 서비스에 숨겨진 Mixer 기능이 있다고 주장을 합니다.

Mixer란?

Mixer라고 하는 것은 가상자산 세탁 기법의 일종입니다. 해킹된 자금과 같이 불법적인 가상 자산을 막는 방법은 1) 불법 행위에 연루된 지갑을 차단하거나, 2) 불법 자산을 추적하는 것입니다. 그런데 여러 입금자의 자금을 한데 넣고 서로 다른 주소로 보내게 되면, 어느 전송이 불법 자산의 전송인지 추적하기 어렵게 됩니다. 이런 원리로, 여러 사람의 여러 자금을 한데 모아서 숨겨진 장부를 가지고 누가 누구에게 전송 하였는지 알 수 없게 만들어주는 소프트웨어를 믹서라고 합니다. 대표적으로는 최근에 미 재무부에 의해서 제재를 받은 토네이도 캐시가 있습니다.

분석 글은 0x9f25a3bd 함수가 믹싱 행위를 한다고 보았습니다. allocate와 같이 이름이 등록 되지도 않았고, 주로 자금을 받아낸 지갑들이 0x9f25a3bd 함수에 의해 자금을 받았기 때문닙니다.

Kracker Labs의 해명

Kracker Labs는 3월 9일 해명글  을 게시하였습니다. 자금난으로 인해서 Klay를 팔았고, 클레이튼 운영사인 크러스트와는 독립된 기관이니 내부자 거래는 없었다는 내용이 핵심입니다. 문제가 된 컨트랙트와 0x9f25a3bd 함수에 대한 이야기는 전혀 없습니다.

해치랩스 임종규 이사 ”클레이튼 온보딩 프로젝트 보안감사는 했다” ...크래커랩스의 ‘리퀴드 스테이킹’ 오딧팅 논란 | 블록미디어
클레이튼을 운영하는 크러스트의 사내 벤처(CIC) 회사가 대량의 클레이 코인 매물을 내놨다는 주장과 관련, 클레이튼 블록체인 보안 감사가 허술하게 진행된 것이 아니냐는 지적이 나온다. 문제의 사내 벤처인 크래커랩스는 스테이크닷리(stake.ly)라는 리퀴드 스테이킹

Stake.ly에 코드 감사를 담당한 해치랩스에서도 해당 컨트랙트에 대한 이야기는 크게 없는 것으로 보입니다.

현 시점에서 크래커랩스가 자금 확보를 위해서 Klay를 처분한 사실은 자명합니다. 내부자 거래 였는지 여부에 대해서는 사실 대부분의 자금 출처가 크러스트와 그 연관사 자금에 연관되어 있기 때문에 의심스럽기는 하나 기술적으로는 알 길이 없습니다.

그렇다면 여기서 분석글이 제시한 대로, 0x9f25a3bd 함수는 자금 세탁을 위해서 작성된, 믹싱 함수 인 것일까요? 만약 그렇다면, 이는 자금 세탁의 결정적인 증거가 될 것입니다.

지루한 기술적 이야기 - 코드를 분석해보자

문제가 되었던 0x857a054ed25820e707b051066073ac45896a7240 컨트랙트의 바이트 코드를 가져와서, (바이트 코드는 기계가 읽을 수 있는 형태의 숫자 코드로, 블록체인 상에는 모든 DApp의 바이트 코드가 공개되어 있다.) 디컴파일(바이트코드로부터 사람이 읽을 수 있는 코드를 유추하는 기술)을 했습니다.

function allocate(address varg0, address varg1, uint256 varg2) public nonPayable { 
    require(msg.data.length - 4 >= 96);
    require(varg0 == varg0);
    require(varg1 == varg1);
    require(_deposit != 2, Error('ReentrancyGuard: reentrant call'));
    _deposit = 2;
    0x3815(msg.sender);
    ...
    v2 = stor_4d_0_19.allocate(varg0, varg1, varg2).gas(msg.gas);
    ...
    _deposit = 1;
}

allocate 의 코드의 경우 위와 같습니다. 유추해 낸 것이므로, 완벽하지는 않습니다. 윗부분에 ReentrancyGuard는 재진입 공격을 막기 위한 코드이고, 0x3815는 해당 컨트랙트의 관리자인지 확인하는 함수입니다. 그냥 보기에 크게 하는 것 없이 stor_4d_0_19allocate를 호출해 주는 것으로 보입니다.

function 0x9f25a3bd(uint256 varg0) public nonPayable { 
    ...
    0x3890(varg0);
    require(stor_4d_0_19.code.size);
    v0 = stor_4d_0_19.call(0x13a3395200000000000000000000000000000000000000000000000000000000, address(varg0)).gas(msg.gas);
    require(v0); // checks call status, propagates error data on error
    v1 = _SafeSub(this.balance, this.balance);
    v2 = _SafeAdd(_allocate, v1);
    _allocate = v2;
    _deposit = 1;
}

그 다음에는 문제의 0x9f25a3bd 함수를 보겠습니다. 윗 부분은 거의 비슷하고, 0x3890 함수를 호출한 다음, stor_4d_0_19에  0x13a339520 함수를 호출해 줍니다. 하지만 allocate, 0x13a33 함수 중 어느 것도 자금을 직접적으로 보내지는 않으므로, 0x3890 함수에 무언가가 있을 것으로 보입니다.

function 0x3890(uint256 varg0) private { 
    v0 = address(varg0);
    require(stor_4d_0_19.code.size);
    v1, v2, v3 = stor_4d_0_19.harvest(v0, _feeRate).gas(msg.gas);
    ...
        v10 = _feeCollector.call().value(v2).gas(2300 * !v2);
        ...
            v11, v12 = _deleteAdmin.getUserDeposit(stor_54_0_19).gas(msg.gas);
            ...
            v13 = 0x3d49(this, stor_4a_0_19, stor_54_0_19); // ClaimReward
                v14 = _SafeMul(v3, stor_52); // feeRate로 추정
                require(100, Panic(18));
                v12 = v14 / 100;
            ...
            v13 = 0x3d49(this, stor_4a_0_19, stor_54_0_19); // ClaimReward
            ...
			v17 = _deleteAdmin.call(0x3a2bf94e00000000000000000000000000000000000000000000000000000000, stor_54_0_19, v12, _rewardRate).gas(msg.gas);
            ...
            v19 = stor_53_0_19.allocate(stor_4a_0_19, v18).value(v18).gas(msg.gas);
                   ...
}

0x3890 함수는 굉장히 긴데, 중요한 함수 호출 부분만 보면 다음과 같습니다. stor_4d_0_19를 통해 harvest 함수를 호출하는데, 이는 각 스테이킹 노드를 돌면서, Klay를 회수하는 함수로 보입니다. 회수된 이익금 중 일부를 _feeCollector에게 보내고, 0x3d49 함수를 이용해서 스테이킹 보상을 수령한 다음, Admin이 Protocol Fee 만큼의 출금요청을 하게 합니다. (0x3a2bf942requestWithdraw로 추정) 그리고 나서 가장 중요하게 stor_53_0_19에게 allocate를 호출하는데, 이때, 회수한 Klay와 스테이킹 보상을 모두 모아 stor_53_0_19에게 보냅니다.

stor_53_0_19는 온체인 상에서 0xcacaab1e74b97ee7b189e2292f647d1fa95d5c29로 추정되는데, 이 컨트랙트의 allocate 코드를 분석해 보면, 합이 10000인 4개의 변수 만큼 각 자금을 분배한다는 점, 함수 명이 allocate인 점을 미루어 보아, 자금을 일정 비율로 지정된 주소로 분배해주는 함수로 보입니다. (다만 Stake.ly 문서의 리워드 구성 에 따르면, 비율 항목은 3개인데 왜 4개 항목으로 구성했는지 알기 어렵습니다.)

재미있는 사실은 stor_53_0_19allocate 함수와 stor_4d_0_19allocate 함수는 구현 모양새가 다른데, stor_4d의 경우에는 Vault 주소만 호출할 수 있으며, 지정된 분배 전략 컨트랙트가 자금을 분배하는 반면에, stor_53의 경우에는 Owner가 임의로 설정한 주소들로 임의로 분배되도록 설계되어 있습니다.

function allocate(address varg0, address varg1, uint256 varg2) public payable { 
    ...
    0x2569(varg0); // getStrategy
    require(varg2 > 0, Error('not a positive number'));
    require(varg0.code.size);
    v0 = varg0.allocate(varg1, varg2).value(msg.value).gas(msg.gas);
    require(v0); // checks call status, propagates error data on error
    emit Allocated(varg0, varg1, varg2);
    stor_65 = 1;
}

결론

공개된 바이트코드를 기반으로 Stake.ly 구현체를 분석해 본 바, 의심되는 0xf925a3bd 함수가 자금 세탁을 위해 설계된 함수로 보기는 어렵습니다. 많은 클레이튼 기반 DApp 들이 기능 오작동이나 업데이트 시 계산값 오류 등을 정정하기 위한 관리자 권한으로 자금 조정 가능을 가지고 있기 때문입니다.

하지만, 관리자의 권한으로 임의로 사용자 자금을 빼 낼 수 있도록 구현한 것은 사실입니다. 보안 감사에서 이러한 사실이 별도로 보고되지 않은 것은 아마도, 관리자는 신뢰받는 대상이므로, 악용하지 않을 것이라는 확신이 있었거나, 감사 당시에 해당 함수가 존재하지 않았을 가능성이 있습니다.

어느 쪽이든, Kracker Labs는 코드 원본을 공개하고, 해당 함수의 구현 목적이 무엇이며, 투자자 기망의 의도가 없었음을 명확히 해야 할 것 입니다.

그리고 마지막으로, 많은 클레이튼 기반 서비스들은 아직도 코드를 공개하지 않고 있습니다. 코드가 공개되어 있었다면, 사실 이러한 관리자에 의한 임의의 자금 이동은 존재할 수 없었을 것입니다. 이런 사건 사고들을 거울 삼아 클레이튼 체인에도 코드를 모두 공개하는 문화가 자리 잡았으면 하는 바램입니다.