제 미디움에서 가져온 글이라 반말체인점 양해 부탁드립니다ㅠㅠ
얼마 전, 아는 사람이 지갑이 해킹된 것 같다고 호소해왔다.
“지갑에 BNB를 넣으니까 자꾸 바로 다른 지갑으로 자동이체된 것처럼 빠져나가고 있다.”
이건 Wallet Sweeping이라는 해킹의 종류인데, 인터넷에서 누군가 관심사가 비슷하다며 자신의 작품이라는 파일을 보내왔고, 이걸 열어본 지인의 컴퓨터에 일종의 원격 악성코드(?)가 심어지게 된 것이 아닌가 강력하게 의심되었다.
빠져나간 BNB야 어쩔 수 없지만, 문제는 지갑 안에 들어있던 NFT였다. 아무래도 자동화된 스크립트가 지갑에 들어있는 BNB를 지속적으로 빼오도록 트랜잭션을 생성했지만, NFT를 빼오지는 안았던 모양이다.
하지만 NFT를 빼오려고 해도, 지갑에 수수료인 BNB를 넣고나면 곧바로 출금이 되어버리니…그리고 BSC 체인의 NFT는 딱히 오픈씨처럼 GUI로 된 전송방식이 없다는 듯 싶었다. 나도 BSC는 이번 기회에 처음 다루게 되었다.
아무튼 나에게 NFT를 구조(?)해달라는 요청이 있어 조금 더 조사해보니, 꽤나 악질적이고 여러 곳에서 피해사례들이 올라와있는 것을 확인할 수 있었다.
혹시 관련 사례와 해결법이 있는지 검색해보니, 다행히도 무려 화이트햇 그룹이 있어 NFT를 구조해주고 있었다. 아래에 링크를 남긴다.
(물론 나는 화이트햇의 도움을 받진 않았다.)
위 디스코드에 접속해 whitehat 채널에 가서 제공하는 폼을 작성하고 넘기면 시작된다.
다만, NFT를 구출해오는데 최소 가격이 1천불부터 시작되니까, 내 NFT가 그보다 저렴하다면 굳이 구해올 필요는 없겠지만…
문제는 이 NFT의 가격이 구입가 기준 10,000불이 넘어간다는 거다.
일단 화이트햇은 최후의 보류로 두고, 나는 두 가지 방법을 고려했다.
1. 수수료 대납(Fee delegation) 컨트랙트를 만들어 트랜잭션을 대신 생성해 보낸다.
직접 web3.js나 기타 web3 라이브러리를 사용해보면 이해가 가겠지만, 보통 트랜잭션을 보내기 위해서는 대략적으로 다음과 같은 방식을 거친다.
- web3의 월렛 인스턴스를 만들어 내 지갑을 추가한다.
- 트랜잭션의 형태(보내는사람, 보낼사람, 데이터 등등)을 만들어 서명한다(sign).
- 트랜잭션을 보낸다(Send).
이 3가지의 과정을 똑같이 수행하는데, 일단 2번까지만 수행한 다음, 이 트랜잭션을 덩어리로 묶어서 Proxy Contract로 보내면, 해당 컨트랙트가 이 트랜잭션을 대신 쏘게 되고, 수수료도 컨트랙트나 컨트랙트의 owner가 지불하게 되는 것이다.
이 방식을 사용하면 NFT를 해킹당한 지갑 입장에서는 수수료를 내지 않고 NFT를 옮길 수 있으니, 아래에서 소개할 두번째 방법보다는 훨씬 좋아보였다.
하지만 이 방식은 애초에 불가능하다.
간단히 말하자면, approve를 Proxy Contract가 받아낼 수 없기 때문이다. approve 자체도 수수료가 들어가거든…
하…오픈씨에 들어가서 내 NFT를 다른 사람에게 보내거나, 판매 리스트에 올린다고 생각해보자.
오픈씨가 보유한 컨트랙트는 이용자의 지갑주소에서 “우리가 너 대신에 NFT를 전송할 권한을 줘”라며 approve를 요청한다.
그러니 아무리 Transaction을 묶어서 Proxy Contract로 보내더라도, 승인 권한을 받지 못한 Proxy Contract는 이 NFT 전송 트랜잭션을 보내도 거절당하게 된다.
또한, 승인권한을 Proxy Contract로 주기 위한 SetApprovalForAll 메소드를 실행시키기 위해서도 가스비가 발생하기에, 결국 이 방법은 사용되지 못했다.
다만!
이 방법은 Wallet Sweeping을 예방할 수 있는 좋은 방법이다. 만약 내가 아주 많은 NFT를 보유한 지갑이 있다면, 미리 Proxy Contract를 만들고 승인을 해두면, 이후에 Sweeping을 당하더라도 Proxy Contract를 이용해 NFT를 빼내올 수 있으니 말이다. 그래서 일단 Proxy Contract를 공개해둔다.
(다만, 이 컨트랙트는 단순 샘플이니 보안을 신경쓴다면 조치를 더 취해야 할 것이다.)
pragma solidity ^0.8.0;
import “@openzeppelin/contracts/utils/cryptography/ECDSA.sol”;
contract Proxy {
using ECDSA for bytes32;
// verify the data and execute the data at the target address
function forward(address _to, bytes calldata _data, bytes memory _signature) external returns (bytes memory _result) {
bool success;
verifySignature(_to, _data, _signature);
(success, _result) = _to.call(_data);
if (!success) {
// solhint-disable-next-line no-inline-assembly
assembly {
returndatacopy(0, 0, returndatasize())
revert(0, returndatasize())
}
}
}
// Recover signer public key and verify that it’s a whitelisted signer.
function verifySignature(address _to, bytes calldata _data, bytes memory signature) private view {
require(_to != address(0), “invalid target address”);
bytes memory payload = abi.encode(_to, _data);
address signerAddress = keccak256(payload).toEthSignedMessageHash().recover(signature);
}
}
그래서 어쨋든 두번째 방법으로 넘어갔다.
2. 가스비를 속사포처럼 쏘면서 NFT 트랜스퍼 트랜잭션도 계속 보낸다.
굉장히 훌륭하지만, 무식한;;; 방법이다.
이 방법을 거의 처음 생각해내고 실행해낸 사람의 글이 있어서 소개한다.
https://medium.com/mycrypto/operation-cryptokitty-rescue-93fd8e00e4f8
무려 크립토키티;;;를 빼내온 사람의 얘기다.
방법은 단순하다.
- 해킹당한 지갑에 지속적으로 BNB를 보낸다.
- BNB를 계속 보내는 동안, NFT의 전송 트랜잭션도 계속 보낸다.
- 지갑에 BNB가 들어오고, 나가기 전에 NFT 전송 트랜잭션이 들어가면 NFT가 무사히 구조된다.
절대! 이 방법을 다 손으로 할 생각은 절대 금물이다!
위는 실제로 이 해킹사건의 피해지갑의 트랜잭션을 조회한 것이다.
왼쪽에서 볼 수 있듯이, 지갑에 일단 BNB가 전송되면, 단 두 블록만에 BNB가 전부 빠져나간다.
2 블록이면 대략 6초이지만, 실제로는 지갑에 들어온 BNB를 캐치하고 곧바로 빼내오는 트랜잭션을 전송했을 것으로 생각되니 틈이 나는 시간은 3초 이내라는 거다. 이걸 손으로 일단 BNB 지갑에서 전송하고, 해킹당한 지갑으로 가서 NFT를 다시 전송하고…를 할 수는 없다.
나는 web3.js 라이브러리로 다음과 같은 두 코드를 돌렸다.
let web3;
var contract;
let nftAddress;
let nftABI;
if(options.network == “testnet”){
web3 = new Web3(‘https://data-seed-prebsc-1-s1.binance.org:8545/');
nftAddress = contractData.BNBProxyAddress;
nftABI = contractData.BNBProxyABI;
contract = new web3.eth.Contract(nftABI,nftAddress) ;
}else if(options.network == “mainnet”){
web3 = new Web3(‘https://bsc-dataseed.binance.org/');
nftAddress = contractData.BNBProxyAddress;
nftABI = contractData.BNBProxyABI;
contract = new web3.eth.Contract(nftABI,nftAddress) ;
}
const Address = configData.BnbTreasuryAccount;
const PrivateKey = configData.BnbPrivateKey;
const Address2 = configData.BnbTreasuryAccount2;
ret = await web3.eth.accounts.wallet.add(PrivateKey);
for(let i = 0 ; i< 10;i++){
ret = await web3.eth.sendTransaction({
from: Address,
to: Address2,
value: web3.utils.toWei(“0.006”,”ether”),
gas: ‘100000’
}).then((res) => {console.log(res);})
}
이게 1번 코드이고, 총 10번동안 0.006BNB씩 해킹당한 지갑주소에 전송한다.
그리고 저 코드를 돌리는 동안 다음 코드를 동시에 여러 터미널에서 돌린다.
let ret;
let web3;
var contract;
let nftAddress;
let nftABI;
if(options.network == “testnet”){
web3 = new Web3(‘https://data-seed-prebsc-1-s1.binance.org:8545/');
nftAddress = contractData.BNBNFTContractTestnet;
nftABI = contractData.BNBNFTABI;
contract = new web3.eth.Contract(nftABI,nftAddress) ;
}else if(options.network == “mainnet”){
web3 = new Web3(‘https://bsc-dataseed.binance.org/');
nftAddress = contractData.BNBNFTContractTestnet;
nftABI = contractData.BNBNFTABI;
contract = new web3.eth.Contract(nftABI,nftAddress) ;
}
const Address = configData.BnbTreasuryAccount;
const PrivateKey = configData.BnbPrivateKey;
const Address2 = configData.BnbTreasuryAccount2;
const PrivateKey2 = configData.BnbPrivateKey2;
ret = await web3.eth.accounts.wallet.add(PrivateKey);
ret = await web3.eth.sendTransaction({
from: Address,
to: nftAddress,
data: contract.methods.transferFrom(minterAddress,minterAddress2, tokenID).encodeABI(),
gas: ‘300000’
}).then((res) => {console.log(res);})
코드 2번은 해킹당한 지갑에서 다른 지갑으로 NFT를 전송하는 코드이다. 이 코드는 10번의 BNB 전송이 돌아가는 사이에 최대한 동시에 많이 들어가야 하기에, 일단 전송 코드를 돌리고 그 사이에 대략 5개의 터미널에서 계속해서 실행시켰다.
BAAM!!
결국 NFT는 구조되었다.
지속적으로 Transaction은 insufficient fund로 뜨다가, 결국 0.3BNB정도를 소모한 끝에 구출해낼 수 있었다.
화이트햇에게 요청하는 것에 비하면 아주 저렴하게 구조했다는게 뿌듯하긴 했지만, 뭐…해킹범에게 정의구현을 한 건 아니니깐…
어쨋든 검색해보니 국외뿐만 아니라 국내에도 이런 피해를 입고, 구출해내지 못한 NFT가 있는 사람도 꽤 있는 듯 하니, 이번 글이 도움이 됐으면 좋겠다.
NFT를 일일이 찾아서 빼내는게 스마트컨트랙트 호출을 해야하다보니 ETH나 BNB 전송보다 복잡하기도 하고, 거래소로 들어가면 오프체인으로 세탁이 가능한 ETH나 BNB와 달리 NFT는 오프체인 거래소가 잘 없지 않나요?(이건 제가 잘 몰라서..) 이런 요건들이 아마 ROI가 안 나온다고 생각한게 아닐까.. 개인적인 추측입니다 ㅎ
글로 올라오는건 처음보는군요 ㅎㅎ
좋은글이네요