배경
CertiK의 이전 기사, "체인으로 연결된 초보자 게임, 대규모 러그풀 기동 공개"에서 다음을 노리는 초보자 봇이 밝혀졌습니다. 약 두 달 만에 200건 이상의 대규모 출구 사기(이하 러그풀로 통칭)를 완료한 대규모 출구 사기 자동 수확기 주소 0xdf1a에 대해 설명한 바 있지만, 이 그룹은 한 가지 러그풀 기술만 가지고 있는 것이 아닙니다.
이 주소 배후 그룹의 러그풀 수법은 이전 게시물에서 MUMI 토큰을 예로 들어 설명했습니다: 토큰의 총 공급량을 수정하거나 전송 이벤트를 보내지 않고 코드 백도어를 통해 직접 세금 주소의 토큰 잔액을 수정.
오늘의 포스팅에서는 복잡한 세금 함수 로직을 사용하여 RugPull에 사용할 수 있는 전송 함수를 가리는 그룹의 또 다른 RugPull 전술의 예로 토큰 "ZhongHua"를 살펴보겠습니다. 다음으로, 주소 0xdf1a의 또 다른 러그풀 기법의 세부 사항을 "ZhongHua" 토큰의 사례를 통해 분석해 보겠습니다.
사기 자세히 살펴보기
이 경우 프로젝트는 총 999억 중화를 약 5,884 WETH로 교환하여 유동성 풀을 고갈시켰습니다. 풀의 유동성을 끌어올릴 수 있습니다. 전체 러그풀 사기에 대해 더 깊이 이해하기 위해 사건의 시작부터 일련의 과정을 살펴보겠습니다.
토큰 배포
1월 18일 오전 1시 40분(UTC), 공격자 주소(? 0x74fc)는 중화(?0x71d7)라는 이름의 ERC20 토큰을 배포하고 10억 개의 토큰을 사전 채굴하여 공격자 주소(?0x74fcfc)로 보냈습니다.
사전 채굴된 토큰의 수는 컨트랙트 소스 코드에 정의된 수와 동일합니다. 컨트랙트의 소스 코드에 정의된 토큰 수와 일치합니다.
유동성 추가.
오후 1시 50분(토큰 생성 후 10분)에 공격자 주소(?0x74fc)가 유동성 추가를 준비하기 위해 Uniswap V2 라우터에 중화 토큰에 대한 승인 권한을 부여합니다.
1분 후, 공격자 주소(? 0x74fc)는 라우터에서 addLiquidityETH 함수를 호출하여 유동성을 추가하여 중화-WETH 유동성 풀(?0x5c8b)을 만들고, 미리 채굴한 모든 토큰과 1.5 ETH를 유동성 풀에 추가하여 약 1.225 LP 토큰으로 끝납니다.
위 토큰 전송 로그에서 이를 확인할 수 있습니다. 공격자(?0x74fc)가 중화 토큰 컨트랙트 자체에 0개의 토큰을 전송한 전송이 한 번 있습니다.
이 이체는 유동성을 추가하는 일반적인 이체는 아니지만, 토큰 컨트랙트의 소스 코드를 살펴보면 이체 주소에서 돈을 공제하고 부과할 수수료를 계산한 다음 토큰 주소로 수수료를 전송하는 _getAmount 함수를 구현하여 토큰 주소가 수수료를 받았음을 나타내는 트리거를 발동하는 것을 확인할 수 있습니다. 그런 다음 토큰 주소로 수수료를 전송하고 토큰 주소가 수수료를 받았음을 나타내는 전송 이벤트를 트리거합니다.
_getAmount는 송금 발신자가 _owner인지 여부를 결정합니다. getAmount 함수는 전송 발신자가 _owner인지 여부를 확인하고, _owner인 경우 수수료가 0으로 설정됩니다. 소유 컨트랙트가 배포될 때 생성자의 입력 파라미터로 _owner가 주어집니다.
중화 토큰 컨트랙트는 소유자 컨트랙트를 상속받습니다. Ownable 컨트랙트를 상속하고 배포 시점에 배포자 msg.sender를 Ownable 생성자의 입력 파라미터로 받습니다.
따라서 공격자 주소(?0x74fc)는 토큰 컨트랙트의 _owner이며, 유동성을 추가하는 0 토큰 전송은 _getAmount 함수를 통해 전송되는데, _getAmount는 전송 및 transferFrom 함수 내에서 호출되기 때문입니다. 함수 내에서 호출되기 때문입니다.
유동성을 영구적으로 잠그기
1:51에 (유동성 풀이 생성되고 1분 이내) 공격자 주소(?0x74fc )는 유동성을 추가하여 획득한 모든 1.225 LP 토큰을 0xdead 주소로 직접 전송하여 LP 토큰에 대한 영구 잠금을 완료합니다.
MUMI 토큰의 경우와 마찬가지로 LP가 잠기면 잠기면 공격자의 주소(?0x74fc)는 이론적으로 유동성을 제거하여 더 이상 러그풀을 할 수 없습니다. 그리고 0xdf1a 주소가 초보자 봇을 대상으로 한 러그풀 사기의 경우, 이 단계는 주로 초보자 봇의 사기 방지 스크립트를 속이는 데 사용됩니다.
이 시점까지 사용자는 미리 채굴된 토큰을 모두 유동성 풀에 추가하는 데 사용한 것으로 보였으며 이상 징후는 발생하지 않았습니다.
RugPull
2시 10분(중화 토큰이 생성된 지 약 30분 후), 공격자 주소 2(?0x5100)가 러그풀 전용 공격 컨트랙트(?0xc403)를 배포했습니다.
MUMI 토큰의 경우와 동일합니다. MUMI 토큰의 경우와 마찬가지로 이 프로젝트는 중화 토큰 컨트랙트를 배포하는 데 사용된 것과 동일한 공격 주소를 사용하지 않았고, 러그풀에 사용된 공격 컨트랙트는 오픈 소스가 아니었으며, 이는 기술자가 소스를 추적하는 것을 더 어렵게 만들기 위해 대부분의 러그풀 사기에서 공통적으로 나타나는 특징입니다.
오전 7시 46분(토큰 컨트랙트가 생성된 지 약 6시간 후), 공격자 주소 2(?0x5100)가 RugPull을 수행했습니다.
그는 공격 컨트랙트(? 0xc403)의 "swapExactETHForTokens" 메서드를 사용하여 공격 컨트랙트에서 999억 개의 중화 토큰을 약 5.884 ETH로 전환하고 풀의 유동성 대부분을 고갈시켰습니다.
공격 컨트랙트(?0xc403)는 오픈소스가 아니기 때문에 오픈 소스가 아니기 때문에 바이트코드를 디컴파일한 결과는 다음과 같습니다:
https://app.dedaub.com/ethereum/address/ 0xc40343c5d0e9744a7dfd8eb7cd311e9cec49bd2e/decompiled
"의 컨트랙트(?0xc403)를 공격합니다. swapExactETHForTokens" 함수의 주요 기능은 UniswapV2 라우터를 승인하여 최대 중화 토큰 전송 권한을 부여한 다음 라우터를 통해 호출자가 "xt"의 수를 지정하는 것입니다. 그런 다음 라우터는 호출자가 "xt"(공격 컨트랙트(?0xc403)가 소유한)로 지정한 중화 토큰 수를 ETH로 변환하여 공격 컨트랙트(?0xc403)에 선언된 "_rescue" 주소로 보냅니다.
주소 "_rescue " 주소는 공격 컨트랙트의 정확한 배포자 주소(?0xc403)인 공격자 주소 2(?0x5100)에 해당합니다.
결국, 프로젝트 측은 999억 달러를 사용했습니다. 중화는 WETH의 유동성 풀을 소진하여 러그풀을 완료했습니다.
이전 글의 MUMI 사례와 마찬가지로 공격 컨트랙트(?0xc403)에서 중화 토큰의 출처를 먼저 확인해야 합니다. 이전 글에서 중화 토큰의 총 공급량이 10억 개라는 것을 알 수 있었고, 러그풀 종료 후 블록 익스플로러에서 조회한 중화 토큰의 총 공급량은 여전히 10억이지만 공격 컨트랙트(?0xc403)에서 판매한 토큰의 수는 999억으로, 9,990억인 컨트랙트(?0xc403)의 토큰 수입니다. /strong>이며, 이는 계약에 의해 기록된 총 공급량의 999배에 해당하는 것으로, 이 훨씬 더 많은 토큰은 어디에서 나온 것일까요?
컨트랙트의 ERC20 토큰 전송 이벤트 내역을 살펴본 결과, MUMI 토큰의 러그풀 사례와 마찬가지로 중화 토큰 사례의 공격 컨트랙트(?0xc403)에도 ERC20 토큰 전송 이벤트가 없는 것을 확인했습니다.
MUMI 사례에서 세금 계약의 토큰은 토큰 컨트랙트의 잔액을 직접 수정하여 세금 계약이 총 공급량보다 훨씬 많은 토큰을 직접 소유할 수 있도록 합니다. MUMI 토큰 컨트랙트는 잔액을 수정할 때 토큰의 총 공급량을 수정하지 않고 전송 이벤트를 트리거하지 않기 때문에 MUMI 사례에서 세금 컨트랙트로부터 토큰이 전송되는 것을 볼 수 없으며, 세금 컨트랙트가 러그풀에 사용한 토큰이 갑자기 나타난 것과 같습니다.
중화 사례로 돌아가서 공격 컨트랙트(?0xc403)의 중화 토큰도 갑자기 나타나기 때문에 중화 토큰 컨트랙트에서 "balance" 키워드로도 검색해봅니다. 중화 토큰 컨트랙트에서 "balance"를 검색합니다.
결과에 따르면 전체 토큰 컨트랙트에서 잔액 변수에 대한 변경은 "_getAmount", "_transferFrom" 및 "_transferBasic" 함수.
여기서 "_getAmount"는 송금 수수료 부과 로직을 처리하는 데 사용되며, "_transferFrom" 및 "_transferFrom"과 "_transferBasic"은 이체 로직을 다루고 있으며, 아래와 같이 MUMI 토큰에서는 잔액을 직접 수정하는 문이 없습니다.
더 중요한 것은 MUMI 토큰 컨트랙트가 직접적으로 세금 계약의 잔액을 수정할 때 전송 이벤트가 트리거되지 않기 때문에 블록 브라우저에서 세금 계약의 토큰 전송 이벤트를 확인할 수 없지만 세금 계약은 많은 수의 토큰을 가질 수 있습니다.
그러나 중화 토큰 컨트랙트에서는 "_getAmount", "_transferFrom", 또는 "_transferBasic" 함수 모두 잔액을 변경 한 후 전송 이벤트를 올바르게 트리거했으며, 이는 공격 계약과 관련된 전송 이벤트에 대한 이전 쿼리 (?0xc403)와 충돌하여 토큰이 전송되는 것을 찾을 수 없습니다. 전송 이벤트가 충돌합니다.
이번 공격 컨트랙트(?0xc403)의 토큰은 MUMI의 경우와 달리 정말 갑자기 나타났을 가능성이 있나요?
공개된 수법
공격 컨트랙트의 토큰은 어디에서 나오는가
공격 계약의 토큰은 어디에서 나오는가? strong>
사례를 분석하는 과정에서 중화 컨트랙트의 잔액이 변경될 때마다 전송 이벤트가 올바르게 트리거되었지만, 공격 컨트랙트와 관련된 토큰 전송 기록(?0xc403)을 찾을 수 없거나 전송 이벤트와 관련된 토큰 전송 기록을 찾을 수 없어 분석을 위한 새로운 아이디어를 찾아야 합니다.
많은 전송 기록을 찾아보던 중, 토큰 컨트랙트에서 토큰을 판매하는 역할을 하는 컨트랙트의 "performZhongSwap" 기능을 돌파구로 삼았고, 분석했던 다른 러그풀 이벤트에서도 "RugPull" 이벤트가 많다는 것을 알게 되었습니다. 저희가 분석한 다른 러그풀 이벤트에서는 이러한 기능이 러그풀의 백도어로 사용된 경우가 많았습니다.
다른 함수를 확인했지만 아무것도 찾지 못했습니다. 그래서 "전송" 함수 자체를 살펴보기 시작했습니다. 공격자가 어떤 방식으로 RugPull을 수행하든 "전송" 함수의 구현 로직에는 가장 중요한 정보가 포함되어 있어야 합니다.
치명적인 전송 함수는 최초의 함수입니다. 치명적 전송
토큰 컨트랙트의 "전송" 함수는 "_transferFrom " 함수를 직접 호출합니다.
이것은 " 전송" 함수는 토큰 전송을 수행하고 전송이 완료되면 전송 이벤트를 트리거합니다.
그러나 토큰 전송을 수행하기 전에 "transfer" 함수는 "_isNotTax" 함수를 사용하여 전송이 전에 해당하는지 여부를 확인합니다. "transfer" 함수는 "_isNotTax" 함수를 사용하여 송금인이 면세 주소인지 확인하고, 그렇지 않은 경우 "_getAmount" 함수를 사용하여 세금을 징수하고, 그렇지 않은 경우 세금을 징수하지 않고 토큰을 수신자에게 직접 전송하는데, 여기서 문제가 발생합니다.
앞에서 언급했듯이 "_getAmount" 구현에서 토큰 컨트랙트는 발신자의 잔액을 확인하고 발신자에게서 돈을 공제하여 토큰 컨트랙트에 수수료를 토큰 컨트랙트에 보냅니다.
그리고 문제는 발신자가 면세 주소가 아닌 경우에만 "_getAmount"가 호출된다는 것입니다. 송금인이 면세 주소인 경우, 해당 금액은 수취인의 잔액에 더해집니다.
문제는 명확해집니다: 면세 주소를 송금인으로 사용하여 송금할 때 토큰 컨트랙트는 송금인의 잔액이 충분한지 여부를 확인하지 않고 송금인의 현상금에서 잔액을 가져가지도 않으며, 송금인의 현상금에서 해당 금액을 빼지도 않습니다. 심지어 발신자의 잔액에서 금액을 빼지도 않습니다. 즉, 토큰 컨트랙트에 정의된 면세 주소라면 어떤 주소로든 토큰을 얼마든지 보낼 수 있습니다. 이것이 공격 컨트랙트(?0xc403)가 총 토큰 공급량의 999배를 직접 전송할 수 있는 이유입니다.
검사 결과, 토큰 컨트랙트는 생성자에서 _taxReceipt만 면세 주소로 설정했고, _taxReceipt에 해당하는 주소가 공격 컨트랙트(?0xc403)인 것으로 확인되었습니다.
중화 토큰의 러그풀이 확인되었습니다: 공격자는 특정 로직을 사용하여 권한 있는 주소의 잔액 확인을 우회하여 권한 있는 주소가 토큰을 허공에서 전송할 수 있도록 하여 러그풀을 완료했습니다.
수익 창출 방법
위에 설명된 취약점을 악용하여 공격자 주소 2(?0x5100)는 권한이 있는 공격 컨트랙트(?0xc403)를 " swapExactETHForTokens"의 권한 있는 공격 컨트랙트(?0xc403)를 직접 호출하여 러그풀을 완료합니다. "swapExactETHForTokens" 함수에서 공격 컨트랙트(?0xc403)는 Uniswap V2 라우터 토큰을 부여합니다. 공격 컨트랙트(?0xc403)는 유니스왑 V2 라우터에 토큰 전송 권한을 부여한 다음 라우터의 토큰 교환 기능을 직접 호출하여 999억 개의 중화 토큰을 풀의 5.88 ETH와 교환했습니다.
사실, 프로젝트 소유자는 위에서 설명한 러그풀 거래 외에도 러그풀 거래 도중 풀의 5.88 ETH를 전송하는데 공격 컨트랙트(?0xc403)도 사용했습니다. 0xc403)은 중간에 11 토큰을 매도하여 누적 9.64 ETH를 얻었으며, 마지막 RugPull 트랜잭션으로 총 15.52 ETH를 얻었습니다.
이 프로젝트는 토큰 판매 도중에 다른 EOA 주소를 사용하여 공격 컨트랙트(?0xc403)를 호출하기도 했습니다. 토큰 세일을 계속 현금화하려는 진짜 의도를 위장하기 위해 토큰을 판매하는 다른 발신자로 위장했습니다.
요약
이제 중화 토큰 러그풀 사건 전체를 되돌아보며 생각해 보니, 방법 자체는 매우 간단하지만 권한이 있는 주소의 토큰 잔액 확인을 취소하는 것뿐입니다. 하지만 이 사례를 분석할 때 왜 그렇게 잘 진행되지 않았을까요? 주된 이유는 두 가지로 요약할 수 있습니다.
1. 보안 보호와 공격의 지평이 다릅니다. 보안 실무자에게 코드 잔액 검사는 가장 기본적인 보안이 완료되어야 하므로 대부분의 보안 실무자는 무의식적으로 사용자의 잔액 검사에서 "전송" 기능을 완료해야 이러한 취약점의 경계를 완화할 수 있다고 생각할 것입니다(또는 이러한 유형의 취약점은 너무 기본적이므로 공격자는 사용하지 않을 것입니다.) 사용하지 않을 것입니다).
그러나 공격자 입장에서는 가장 효과적인 공격이 가장 초보적인 공격인 경우가 많으므로 밸런스를 확인하지 않는 것을 효과적이면서도 쉽게 간과되는 러그풀 기법으로 사용하지 않을 이유가 없습니다. 적어도 케이스 특성화 측면에서 볼 때, 중화 토큰 케이스의 러그풀 기법은 흔적이 가장 적게 남았고, 다른 유형의 러그풀보다 추적하기가 훨씬 어려웠으며, 결국 코드 백도어를 찾기 위해 수동 코드 감사가 필요했던 것도 사실입니다.
2. 프로젝트 측에서 잔액 확인이 필요 없는 권한 있는 주소의 백도어 코드를 의도적으로 은폐하고 있습니다. 프로젝트 측은 심지어 권한이 없는 주소의 경우 토큰 주소 인출 및 재투자 로직인 세금 이체 계산 로직의 완전한 세트를 달성하기 위해 별도로 복잡한 이체 로직을 달성하기 위해 토큰이 합리적으로 보이도록 합니다. 다른 주소에서 돈을 이체하는 것은 일반적인 행동과 다르지 않으며, 코드를 자세히 살펴보지 않으면 아무것도 감지할 수 없습니다.
MUMI 토큰에 대한 러그풀 팀의 전략과 중화 토큰에 대한 전략을 대조해보면, 둘 다 특권을 가진 주소가 많은 수의 토큰을 제어할 수 있도록 비교적 은밀한 방식으로 이루어졌습니다.
뮤미 토큰 러그풀의 경우 프로젝트 소유자가 총공급량을 수정하거나 전송 이벤트를 트리거하지 않고 직접 잔액을 수정하여 사용자가 특권 주소가 이미 대량의 토큰을 소유하고 있다는 사실을 인지하지 못하도록 했습니다.
중화 토큰의 경우는 더욱 철저하여 특권 주소의 잔액을 확인하지 않기 때문에 소스코드를 보는 것 외에는 특권 주소에 토큰이 무제한으로 있다는 사실을 알 수 없습니다(balanceOf로 쿼리하면 특권 주소의 잔액은 0으로 표시됨). 주소의 잔액은 0으로 표시되지만 무제한 토큰을 전송할 수 있습니다).
중화 토큰의 러그풀 사례는 보안 측면에서 악당으로부터 보호하는 것이 아니라 신사를 제한하는 데만 사용할 수 있는 ERC20 토큰 표준의 잠재적인 보안 문제를 반영합니다. 공격자는 표준을 준수하는 비즈니스 로직을 구현하면서 탐지하기 어려운 백도어를 숨기는 경우가 많습니다. 토큰 동작을 표준화하면 백도어를 숨길 가능성을 피할 수 있고 기능의 유연성은 떨어지지만 보안이 강화됩니다.