2026년 2월 23일 월요일

(번역기) 보안 사고 업데이트: ioTube 브리지 취약점 및 복구 로드맵

 https://x.com/iotex_io/status/2025824807120412842


요약 보고서

2026년 2월 21일, IoTeX 팀은 IoTeX 멀티체인 브리지인 ioTube에서 발생한 보안 침해 사고를 인지하고 비상 대응 체제로 전환했습니다. 이번 공격은 이더리움 체인 내에서만 진행되었습니다. 본 보고서는 발생한 사건의 전말, 자금 현황, 그리고 향후 복구 및 보상 계획을 자세히 설명하기 위해 작성되었습니다. 포렌식 분석 결과, 이번 공격은 정교하고 전문적인 공격으로, 과거 발생했던 주요 DeFi 공격 사건들과 연관이 있을 가능성이 있는 것으로 나타났습니다. 공격 주체에 대한 자세한 정보는 사후 분석 보고서에 포함될 예정입니다.


영향

* IoTeX L1 체인은 안전합니다: IoTeX 레이어 1 체인, 합의 메커니즘 및 모든 네이티브 스마트 계약은 손상되지 않았습니다.

* IoTeX 및 거래소의 사용자 자산은 안전합니다: IoTeX 체인과 중앙 집중식 거래소에 있는 IOTX 토큰은 영향을 받지 않습니다.

* 이번 침해는 ioTube의 이더리움 측 브리지 계약에만 영향이 있었고, BSC 및 Base와 같은 다른 체인의 브리지 계약은 영향을 받지 않았습니다.


사건 개요: 근본 원인 및 기술적 분석

이번 공격은 정교한 4단계 공격 과정을 통해 ioTube 브리지의 이더리움 사이드를 표적으로 삼았습니다.

* 검증자 키 탈취: 이더리움 상의 검증자 계약 소유자 계정이 탈취되어 공격자가 관리자 권한을 획득했습니다.

* 악성 업그레이드: 공격자는 이 접근 권한을 이용하여 검증자 계약을 모든 서명 및 유효성 검사를 우회하는 악성 버전으로 업그레이드했습니다.

* 계약 탈취: 검증자 계층을 장악한 공격자는 MintPool(토큰 발행)과 TokenSafe(예비 자산)를 제어하게 되었습니다.

* 자산 유출: 공격자는 4억 1천만 개의 CIOTX 토큰을 발행하고 브리지 예비 자산에서 약 440만 달러 상당의 다양한 토큰을 유출했습니다.


현재 자산 현황 및 복구 노력

사고 발생 후 몇 시간 만에 이를 감지하고 피해를 최소화하기 위한 즉각적인 조치를 취했습니다. 온체인 추적 결과, 도난당한 자산의 대부분은 이미 안전하게 보호되고 있습니다.

파트 A: 4억 1천만 CIOTX (MintPool)

공격자는 4억 1천만 CIOTX를 발행했습니다. 당사의 신속 대응 프로토콜을 통해 발행된 토큰의 86% 이상이 이미 잠금 또는 동결되었으며, 이는 당사의 체인 수준 제어를 통해 직접 보호할 수 있는 자산입니다. 추가로 12.8%(5,240만 IOTX)는 바이낸스에서 추적되었으며, 바이낸스 및 거래 파트너와 협력하여 동결 조치를 진행하고 있습니다. 0.4%(170만)는 탈중앙화 거래소(DEX)에서 스왑되었으며, 현재 위험에 노출되어 있습니다.

* 이더리움 및 베이스(3억 1,500만 CIOTX): 유동성이나 브릿지 경로 없이 완전히 잠겨 있습니다.

* IoTeX 체인(9,500만 IOTX): 적극적으로 복구 중입니다. 온체인 추적이 완료되었습니다. 9,500만 CIOTX 중:

- 4,050만 CIOTX는 공격자 지갑에 남아 있습니다. IoTeX 체인에서 공격자가 제어하는 ​​주소 29개를 확인했으며, 체인 수준 패치를 통해 모두 블랙리스트에 추가했습니다. 현재 체인 위임자들에게 패치를 배포하고 있습니다.

- 5,240만 CIOTX가 바이낸스에 입금되었습니다. 이 중 4,160만 CIOTX는 거래 파트너(이지비트, 체인지나우 등)를 통해 이동되었습니다. 바이낸스와 해당 거래 파트너들과 협력하여 입금된 모든 자금을 동결하고 있습니다.

- 170만 CIOTX가 탈중앙화 거래소(DEX)에서 다른 토큰으로 교환되었습니다. 이는 전체 발행 CIOTX의 0.4%에 해당하는, 위험에 처한 유일한 부분입니다.


파트 B: 브리지 준비금(TokenSafe)

공격자는 탈취한 준비금 토큰(USDC, USDT, WBTC, WETH 및 기타 자산 포함)을 약 2,183 ETH로 전환했습니다. 이 중 약 1,572 ETH는 THORChain을 통해 비트코인으로 브리지되었으며, 나머지는 현재 모니터링 중인 중간 이더리움 주소에 분산되어 있습니다.

현황: 총 66.78 BTC를 보유하고 있는 4개의 비트코인 ​​주소를 확인했습니다.

12V7jhcPnqnGbRFMasSW2CZVBd8qpvUgAK

16xusPKLMyqK68SkhfXDtic6AJPDi51tqh

1PN2BoHU4buDQWcrNHk9T9NBA2qX8oyYEc

135oSa2fobTxtHtm5dwTREDyRY2o1DG1Aw

모니터링: 이 보고서 작성 시점 기준으로 모든 BTC는 미사용 상태입니다. 해당 주소들은 플래그가 지정되어 있으며, 저희 팀과 분석 파트너가 24시간 내내 지속적으로 모니터링하고 있습니다.


즉각적인 조치 및 서비스 재개 일정

저희 팀은 모든 서비스를 완전히 복구하기 위해 24시간 내내 노력하고 있습니다.

* IoTeX L1 체인: 현재 체인 위임자들에게 패치를 배포하기 시작했습니다. 충분한 수의 위임자들이 패치를 완료하면 합의 및 정상 운영이 자동으로 재개됩니다.

* 거래소 활동: 출금은 24~48시간 이내에 재개될 예정이며, 입금 기능은 그 직후에 재개될 것입니다.

* 커뮤니티 AMA: 24~48시간 이내에 자세한 내용을 안내하는 커뮤니티 AMA를 진행할 예정입니다.

* 보상 계획: 영향을 받은 브리지 사용자들을 위한 자세한 보상 계획은 48시간 이내에 발표될 예정입니다.

* ioTube 브리지: 독립적인 보안 감사가 완료될 때까지 모든 체인에서 브리지 운영이 중단됩니다. 또한 IIP-55 구현 및 배포를 신속하게 진행할 것입니다.

* 법률 및 포렌식: 사법 당국 및 최고 수준의 온체인 분석 회사와 협력하고 있습니다. 공격자의 주소는 Etherscan에서 0x6487B5006904f3Db3C4a3654409AE92b87eD442f(Etherscan에서는 Fake_Phishing2054654로 표시됨)로 플래그가 지정되었습니다.

* 화이트햇 현상금: 공격자에게 자금을 자발적으로 반환하는 경우 화이트햇 현상금을 제공하는 온체인 메시지가 전송될 예정입니다.


커뮤니티에 대한 우리의 약속

커뮤니티 자산의 보안은 우리의 최우선 과제입니다. 우리는 피해를 입은 사용자에게 완전한 보상을 제공하기 위해 최선을 다할 것입니다.

* 보상: 피해를 입은 브리지 사용자를 위한 자세한 보상 계획은 48시간 이내에 발표될 예정입니다.

* 임시 지원: 이 기간 동안 브리지 복구 요청은 개별적으로 수동으로 처리됩니다.

* 투명성: 창립팀과 함께하는 커뮤니티 AMA(Ask Me Anything)를 24~48시간 이내에 개최할 예정입니다.


앞으로 나아갈 길: 보안 강화

이번 사건은 IoTeX 생태계를 위한 가장 강력하고 다층적인 보안 프레임워크를 구현하게 될 터닝 포인트가 될 것입니다.

* 철저한 재감사: ioTube는 독립적인 보안 감사가 완료되고 모든 취약점이 해결될 때까지 일시 중단됩니다.

* 분산형 거버넌스: 단일 장애 지점을 제거하기 위해 다중 서명 및 24시간 타임락 제어 기능을 도입합니다. 또한, 다자간 검증자 세트를 통해 브리지 검증을 분산화하는 거버넌스 제안인 IIP-55의 구현 및 배포를 가속화할 것입니다.

* 지능형 보호 장치: 위험을 사전에 차단하기 위해 새로운 거래당 및 일일 거래량 제한을 통합하고 있습니다.

* 보안 버그 바운티: 글로벌 화이트햇 커뮤니티와 협력하여 생태계를 보호하기 위해 확장된 버그 바운티 프로그램을 시작합니다.


이번 사태를 함께 극복해 나가는 동안 IoTeX 커뮤니티 여러분의 인내와 지속적인 지원에 감사드립니다.

IoTeX 팀

2024년 10월 2일 수요일

Java 8 Spring Boot 세팅

오랜만에 확인해보니 Spring Initializer(https://start.spring.io/)에서 Java 8 지원이 끊겼다. 17부터만 제공하여 17 프로젝트로부터 Java 8로 변경하는 내용을 간단히 정리.


1. spring-boot-starter-parent의 버전을 2.6.2로 변경한다.

<parent>

  • <groupId>org.springframework.boot</groupId>
  • <artifactId>spring-boot-starter-parent</artifactId>
  • <version>2.6.2</version>


2. properties 태그에서 java.version을 1.8로 변경해주고, maven-jar-plugin.version도 3.1.1로 낮춰준다.

<properties>

  • <java.version>1.8</java.version>
  • <maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>

React Query

React Query에 대해 간단히 정리.

React Query는 서버와의 통신 작업을 진행할 때 반복적으로 발생하는 번거로운 작업을 안정적으로 대신해주는 역할을 한다고 이해하고 있다. 데이터의 상태와 생명 주기를 관리해주는 도구로 볼 수 있고, 사용했을 때 코드가 간결해지고 유지 보수가 쉬워지는 장점이 보인다.

React Query는 고맙게도 데이터를 가져온 뒤 그 결과를 상태로 관리해준다. useState와 useEffect를 이용해 직접 데이터를 다루지 않아도 되고, 로딩 중이나 에러가 발생하는 상황의 처리도 더 간결한 방식으로 사용할 수 있다. 

const { data, isLoading, isError } = useQuery(['api이름'], 초기로드함수);

- data: 데이터를 성공적으로 가져온 경우 여기에 데이터가 담긴다.

- isLoading: 데이터가 로딩 중일 때

- isError: 데이터 로드 실패 시


React Query는 데이터 요청을 캐싱한다. 무슨 얘기냐면 데이터 변경 없이 컴포넌트가 다시 렌더링 되어야 하는 상황에서 동일한 데이터에 대한 불필요한 추가 요청을 막아준다. 이전 데이터를 캐시에서 가져오는데, 사용자가 원하면 invalidateQueries를 통해 캐시를 무효화하고 최신 데이터를 가져오도록 할 수도 있다.


구현 패턴은 비슷해지는데

1. useQuery로 데이터를 가져오고

2. useMutation을 사용해 데이터를 수정한다. 그리고 queryClient.invalidateQueries로 데이터를 갱신한다.


// api 호출부.
import axios from 'axios';

export const fetchItems = async () => {
  const response = await axios.get('/items');
  return response.data;
};

export const updateItem = async ({ id, field1, field2 }) => {
  const response = await axios.put(`/items/${id}`, { field1, field2, });
  return response.data;
};


// 컴포넌트에서 사용
import React from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { fetchItems, updateItem } from './api';

const 컴포넌트 = () => {
  const queryClient = useQueryClient();
  const { data: items, isLoading, isError, refetch } = useQuery(['items'], fetchItems);

  // 업데이트 시 mutation.mutate() 식으로 사용.
  const mutation = useMutation(updateItem, {
    onSuccess: () => {
      queryClient.invalidateQueries(['items']); // 서버 데이터와 다시 동기화
    },
  });

  if (isLoading) return <div>Loading...</div>;
  if (isError) return <div>Error loading data</div>;

  // 렌더링 코드 반환 구문
};

2024년 2월 2일 금요일

REACT - JSX

회사에서 리액트 스터디를 하게되서..

책을 한권 구매해서 순번을 정해 한 챕터씩 맡아 내용을 정리하기로 했다.

난 챕터 3의 JSX를 정리하게 되서 짧막하게 정리해본다.


배경은?

최근의 웹은 더욱 상호 작용적으로 변하면서 점점 JavaScript 코드가 컨텐츠를 결정하게 되었다.

React가 렌더링 로직과 마크업을 같은 장소, 즉 컴포넌트에 위치시키게 된 것은 흐름을 반영한 자연스러운 결과다. 로직과 마크업을 합치기로 했다면, 이왕에 HTML 구문 그대로를 JavaScript 파일 안에 넣을 수 있으면 더 직관적일 것이다.


JSX : Javascript Syntax eXtension

한줄 정리 : JSX는 JavaScript 파일 안에 HTML과 유사한 마크업을 작성할 수 있게 해주는 JavaScript 확장 문법이다. (HTML과 살짝 다르다.)


그럼 왜 사용하는가?

JSX를 사용하면 코드가 간결해져 보기에 편하다. createElement 함수를 이용해 엘리먼트를 만드는 방법과 비교하면 차이가 극명한데, JSX 방식을 사용하면 JSX 내용을 보고 결과물을 예상하기에도 좋다.


JSX는 HTML보다 조금 더 엄격하게 동작하는데 아래의 3가지 룰이 추가된다.

1. 단일 루트 요소를 반환해야 한다.

 : 여러 개의 요소를 반환하고 싶은 경우 <div> 태그와 같은 것으로 감싸줄 필요가 있다. HTML 트리에 흔적을 남기고 싶지 않은 경우라면 <>와 </>의 빈 태그를 활용해도 된다. (이런 태그를 Fragment라 부른다.) 


2. 태그는 반드시 받아야 한다.

 : <img>, <li>와 같은 태그도 예외가 없다. 반드시 <img />, <li> ~~~ </li>와 같이 닫아주어야 한다.


3. 낙타 표기법으로 작성한다.

 : JSX도 결국 JavaScript 코드로 변환하는 과정을 거치게 된다. JSX로 작성된 속성은 JavaScript 객체의 키가 되는데 JavaScript에는 변수 이름에 제한이 있다. 예를 들면 대시를 포함하거나 class와 같은 예약어를 사용할 수 없는데, 이에 대한 방안으로 React에서는 낙타 표기법으로 속성을 표기하기로 하였다.

stroke-width는 strokeWidth, class는 className 식으로 작성해야 한다.


다행스러운 것은 HTML을 JSX로 변환해주는 온라인 컨버터가 존재한다는 것인데 주소는 다음과 같다.

https://transform.tools/html-to-jsx


누군가는 마크업에 간단한 로직을 포함하거나 동적으로 변하는 프로퍼티를 넣고 싶을 수 있다. JSX에선 중괄호를 이용해 이러한 작업을 할 수 있게 지원한다. (값의 대입 뿐만 아니라 함수의 호출도 지원한다.)

+ CSS나 오브젝트를 전달하고 싶은 경우 '{{ ... }}'의 이중 중괄호로 표현해야 동작한다.



2023년 3월 11일 토요일

Protobuf-net 서버-클라이언트 데이터 송수신 예제

성능 신경쓰지 않고 단순히 데이터 주고 받는데 집중하면 동기 방식의 TCP/IP에 protobuf-net 조합이 가장 간편한 것 같다. 예제는 ChatGPT에서 찾은 것으로 단순하게 메시지 하나를 전달한다. 이 정도 예제만 바로 찾을 수 있어도 접근하기가 얼마나 쉬워지는지. 아쉬운 부분은 회사에서는 ChatGPT가 막혀있다.


아래는 서버 코드. 클라이언트 네트워크 스트림을 대상으로 Serialize를 호출하면 데이터가 클라이언트 쪽으로 전송된다. Protobuf-net을 사용하는 경우 *.proto 파일을 별도로 작성하고, 관리하지 않아도 되기 때문에 사용하기 간편하다.


using System;
using System.Net;
using System.Net.Sockets;
using ProtoBuf;

public class Program
{
    private const int Port = 12345;
    private const int BufferSize = 1024;

    [ProtoContract]
    public class Message
    {
        [ProtoMember(1)]
        public int Id { get; set; }
        [ProtoMember(2)]
        public string Text { get; set; }
    }

    static void Main(string[] args)
    {
        var listener = new TcpListener(IPAddress.Any, Port);
        listener.Start();
        Console.WriteLine($"Listening on port {Port}");

        while (true)
        {
            using (var client = listener.AcceptTcpClient())
            {
                var stream = client.GetStream();
                var buffer = new byte[BufferSize];
                var bytesRead = stream.Read(buffer, 0, BufferSize);
                var message = Serializer.Deserialize<Message>(new ArraySegment<byte>(buffer, 0, bytesRead));

                Console.WriteLine($"Received message from client: {message.Text}");

                var response = new Message { Id = 1, Text = "Hello from server!" };
                Serializer.Serialize(stream, response);
                Console.WriteLine($"Sent response to client: {response.Text}");
            }
        }
    }
}

다음은 클라이언트. ProtoContract 클래스는 서로 일치해야 한다.

using System;
using System.Net.Sockets;
using ProtoBuf;

public class Program
{
    private const string Host = "localhost";
    private const int Port = 12345;
    private const int BufferSize = 1024;

    [ProtoContract]
    public class Message
    {
        [ProtoMember(1)]
        public int Id { get; set; }
        [ProtoMember(2)]
        public string Text { get; set; }
    }

    static void Main(string[] args)
    {
        using (var client = new TcpClient(Host, Port))
        {
            var stream = client.GetStream();

            var message = new Message { Id = 1, Text = "Hello from client!" };
            Serializer.Serialize(stream, message);
            Console.WriteLine($"Sent message to server: {message.Text}");

            var buffer = new byte[BufferSize];
            var bytesRead = stream.Read(buffer, 0, BufferSize);
            var response = Serializer.Deserialize<Message>(new ArraySegment<byte>(buffer, 0, bytesRead));

            Console.WriteLine($"Received response from server: {response.Text}");
        }
    }
}


2021년 9월 26일 일요일

Visual Studio 2019+ D(서브) 드라이브 설치

Visual Studio 2019 버전을 메인 드라이브인 C 드라이브가 아닌 다른 드라이브에 설치하는 방법에 대해 정리해 본다.


SSD 128 GB 정도의 작은 저장 장치를 메인 드라이브로 사용하는 경우 용량 부족으로 Visual Studio 2019 설치가 어려운 경우가 있는데 윈도우 시스템의 Junction(리눅스 시스템의 Symbolic Link) 기능을 이용해 일부 설치 용량을 서브 드라이브에 할당시킬 수 있다.


어느 정도의 공간 절약이 되냐면 Professional 버전 기준 .NET 데스크탑 개발 환경과 C++ 데스크탑 개발 환경을 설치할 때 12GB 가량 공간이 필요한데 Junction을 이용하면 4GB 정도 공간 절약이 가능하다.


원문 링크 : https://eventhorizon.tistory.com/110


방법>

1. 관리자 권한으로 cmd.exe를 실행한다.

2. 실제 설치가 이루어질 디렉토리를 D 드라이브에 생성한다.

3. 링크를 배치할 디렉토리를 C 드라이브에 생성한다.

4. mklink 명령어를 이용해 링크를 생성한다.

5. Visual Studio 설치를 진행한다.


ex) Professional 기본 설치 경로를 이용하는 경우 예시. 2단계 메인 드라이브에 디렉터리를 생성할 때 'Professional' 디렉토리는 제외해서 생성해야 한다.

1. mkdir -p "D:\Program Files (x86)\Microsoft Visual Studio\2019\Professional"

2. mkdir -p "C:\Program Files (x86)\Microsoft Visual Studio\2019"

3. mklink /j Professional "D:\Program Files (x86)\Microsoft Visual Studio\2019\Professional"




2021년 1월 26일 화요일

[Protocol Buffers] optimize_for 옵션

프로토콜 버퍼 컴파일러(protoc) 옵션으로 사용 목적에 따라 SPEED, CODE_SIZE, 또는 LITE_RUNTIME 값을 지정할 수 있다.


기본 값은 SPEED이다.


• SPEED (기본) : 기본 값인 만큼 프로토콜 버퍼의 모든 기능(Descriptor, Reflection)이 포함되며 최적화된 코드를 생성해 준다.


• CODE_SIZE : SPEED 옵션 대비 작은 코드 사이즈를 갖는 결과 파일을 만들어 주지만 SPEED 보다 동작 속도가 느려진다. 매우 많은 수의 .proto 파일을 포함하고(메시지 개수에 코드 사이즈가 비례하므로) 있거나 속도가 중요하지 않은 앱에서 유용한 옵션이다.


• LITE_RUNTIME : 'lite' 런타임 라이브러리에만 의존하는 클래스를 생성한다. (libprotobuf 대신 libprotobuf-lite) 전체 라이브러리보다 10배 작은 라이브러리로 동작이 가능하기 때문에 휴대폰과 같이 제한된 플랫폼에서 실행되는 경우 유용하다. 생성된 클래스는 Message 대신 MessageLite 인터페이스를 구현하며 이 옵션으로 컴파일 하는 경우 Descriptor, Reflection 등의 기능은 제외된다.


* 동일한 proto 파일을 여러 번 로드할 때 Descriptor로 부터 에러가 발생하는 경우가 있는데 이러한 경우엔 Descriptor 기능이 제외된 LITE_RUNTIME 옵션을 사용하는 것을 고려해볼 수 있다.

https://github.com/protocolbuffers/protobuf/issues/4126


일반적인 타겟 환경을 가진 프로그램이라면 기본 값을 그대로 쓰면 될 것 같고, 바이너리 사이즈가 중요한 프로그램이라면 CODE_SIZE, LITE_RUNTIME 등의 옵션을 고려해 볼만 한 것 같다. 사실 protoc에 의해 생성된 클래스를 그대로 사용하는 것이 일반적이라 Descriptor 같은 기능은 고급 사용자가 아니면 잘 쓰이지 않는다.