구독해서 새 게시물에 대한 알림을 받으세요.

리눅스 방화벽을 남용하기: Spectrum 을 만들 수 있었던 ​해킹​

2018-04-12

4분 읽기
이 게시물은 English, Français, Deutsch, 日本語, Español简体中文로도 이용할 수 있습니다.

13334109713_0b32435032_z

얼마전 우리는 Spectrum을 발표하였습니다: 어떤 TCP 기반의 프로토콜이라도 DDoS 방어, 로드밸런싱 그리고 컨텐츠 가속을 할 수 있는 새로운 Cloudflare의 기능입니다.

CC BY-SA 2.0 image by Staffan Vilcans

Spectrum을 만들기 시작하고 얼마 되지 않아서 중요한 기술적 난관에 부딛히게 되었습니다: Spectrum은 1부터 65535 사이의 어떤 유효한 TCP 포트라도 접속을 허용해야 합니다. 우리의 리눅스 엣지 서버에서는 "임의의 포트 번호에 인바운드 연결을 허용"은 불가능합니다. 이것은 리눅스만의 제한은 아닙니다: 이것은 대부분 운영 체제의 네트워크 어플리케이션의 기반인 BSD 소켓 API의 특성입니다. 내부적으로 Spectrum을 완성하기 위해서 풀어야 하는 서로 겹치는 문제가 둘 있었습니다:

  • 1에서 65535 사이의 모든 포트 번호에 TCP 연결을 어떻게 받아들일 것인가

  • 매우 많은 수의 IP 주소로 오는 연결을 받아들이도록 단일 리눅스 서버를 어떻게 설정할 것인가 (우리는 애니캐스트 대역에 수많은 IP주소를 갖고 있습니다)

서버에 수백만의 IP를 할당

Cloudflare의 엣지 서버는 거의 동일한 구성을 갖고 있습니다. 초창기에는 루프백 네트워크 인터페이스에 특정한 /32 (그리고 /128) IP 주소를 할당하였습니다[1]. 이것은 수십개의 IP주소만 갖고 있었을 때에는 잘 동작 하였지만 더 성장함에 따라 확대 적용하는 것에는 실패하였습니다.

그때 "AnyIP" 트릭이 등장하였습니다. AnyIP는 단일 주소가 아니라 전체 IP 프리픽스 (서브넷)을 루프백 인터페이스에 할당하도록 해 줍니다. 사실 AnyIP를 많이 사용하고 있습니다: 여러분 컴퓨터에는 루브백 인터페이스에 127.0.0.0/8 이 할당되어 있습니다. 컴퓨터의 관점에서 본다면 127.0.0.1 에서 127.255.255.254 사이의 모든 주소가 로컬 머신에 할당된 것입니다.

ip route add local 192.0.2.0/24 dev lo

이 트릭은 127.0.0.1/8 대역 이외에도 적용 가능합니다. 192.0.2.0/24 전체를 로컬에 할당한 것처럼 보이게 하려면 다음을 실행하세요:

nc -l 192.0.2.1 8080

다음으로 이 IP 주소 중 하나의 포트 8080에 바인딩하는 것도 문제 없습니다:

ip route add local 2001:db8::/64 dev lo

IPv6 을 그렇게 동작하게 하는것은 조금 더 어렵습니다:

불행히도 v4 예제처럼 v6 IP주소를 그렇게 할당할 수는 없습니다. 이걸 하기 위해서는 추가적인 권한이 필요한 IP_FREEBIND 소켓 옵션을 사용해야 합니다. 완벽히 하자면 net.ipv6.ip_nonlocal_bind sysctl 이 있습니다만 수정하기 않기를 권장합니다.

$ ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536
    inet 1.1.1.0/24 scope global lo
       valid_lft forever preferred_lft forever
    inet 104.16.0.0/16 scope global lo
       valid_lft forever preferred_lft forever
...

이 AnyIP 트릭은 각 서버에 로컬 인터페이스로 할당된 수백만의 IP 주소를 가능하게 합니다:

모든 포트에 바인딩

두번째로 큰 문제는 임의의 포트 번호에 TCP 소켓을 여는 기능입니다. 리눅스와 BSD 소켓 API를 지원하는 시스템에서는 일반적으로 하나의 bind시스템 콜로 특정 TCP 포트 번호에만 바인딩이 가능 합니다. 한번의 명령으로 여러 포트에 바인드하는 것은 가능하지 않습니다.

단순히 생각하면 가능한 65535 포트 각각에 대해 bind를 65535 번하는 것입니다. 물론 이것도 생각해 볼 수 있습니다만 끔찍한 결과를 초래할 수 있습니다:

/* Yes, really, this is all you need. */
#define INET_LHTABLE_SIZE       32

내부적으로 리눅스 커널은 리스닝 소켓을 포트 번호로 인덱싱된 해시 테이블 LHTABLE 에 저장하고 32 버킷을 사용 합니다.

6.5만개의 포트를 열게 된다면 이 테이블에서 찾는 것은 매우 느려집니다: 각각의 해시 테이블 버킷이 2천개의 아이템을 포함할 수 있기 때문입니다.

이 문제를 해결하는 또 다른 방법은 iptable 의 풍부한 NAT 기능을 사용하는 것입니다. 들어오는 패킷의 수신 주소를 특정 주소/포트로 바꾸어 쓰고 거기에서 어플리케이션이 바인드하고 있는 것입니다.

이 방법을 해 보지는 않았습니다만 그 이유는 iptables의 conntrack모듈이 필요하기 때문입니다. 예전에 우리는 성능 문제가 되는 경우를 찾아 내었고 conntrack은 우리가 접하게 되는 큰 DDoS 공격을 처리할 수 없습니다.

추가적으로 NAT방식으로는 수신자 IP 주소 정보를 잃어버릴 수 있습니다. 이 문제를 보완하기 위해서 SO_ORIGINAL_DST라는 잘 알려지지 않은 소켓 옵션이 있습니다만 코드가 그렇게 좋아 보이지는 않습니다.

다행히 6.5만개 포트에 모두 바인딩하거나 conntrack을 사용하지 않아도 문제를 해결할 방법이 있습니다.

구조용 방화벽

더 자세히 들어가기 전에 운영체제에서 네트워크 패킷의 일반적인 흐름에 대해 다시 알아봅시다.

공통적으로 수신 패킷 경로에는 두가지의 나뉘어진 계층이 있습니다:

  • IP 방화벽

  • 네트워크 스택

이것들은 개념적으로 별개입니다. IP 방화벽은 일반적으로 상태 없는 소프트웨어입니다(conntrack과 IP 조각 재조립은 일단 별개로 합시다). 방화벽은 IP 패킷을 분석해서 ACCEPT할지 DROP할지를 결정합니다. 참고로 이 계층은 _어플리케이션_이나 _소켓_이 아니라 패킷 과 _포트 번호_에 대한 것입니다.

그리고 네트워크 스택이 있습니다. 이 괴물은 많은 상태를 관리합니다. 주된 작업은 IP패킷을 수신하여 _소켓_으로 보내는 것이며 이후 사용자 공간의 _어플리케이션_에 의해 처리됩니다. 네트워크 스택은 사용자 공간에서 공유되는 추상화 계층을 관리합니다. TCP 흐름을 재조립하고, 라우팅을 처리하며, 어떤 IP가 로컬인지를 판별합니다.

upload-1

마법의 먼지

Source: still from YouTube

TPROXY
This target is only valid in the mangle table, in the 
PREROUTING chain and user-defined chains which are only 
called from this chain.  It redirects the packet to a local 
socket without changing the packet header in any way. It can
also change the mark value which can then be used in 
advanced routing rules. 

그러다 TPROXY iptable 모듈을 만나게 되었습니다. 공식 문서는 지나치기 쉽습니다:

추가적인 문서는 커널에서 찾을 수 있습니다.

더 생각해 볼 수도록 더 궁금하게 되었습니다.

그래서... 결국 TPROXY가 하는 일은 무엇일까요?

마술 트릭을 밝히자

case NFT_LOOKUP_LISTENER:
  sk = inet_lookup_listener(net, &tcp_hashinfo, skb,
				    ip_hdrlen(skb) +
				      __tcp_hdrlen(tcph),
				    saddr, sport,
				    daddr, dport,
				    in->ifindex, 0);

TPROXY 코드는 놀랍게도 간단합니다:

이 내용을 다시 이야기 하면: 방화벽의 일부인 iptables 모듈에서 inet_lookup_listener를 호출합니다. 이 함수는 src/dst/port/IP 의 4개 튜플을 인수로 받으며 그 연결을 성립시킬 수 있는 리스닝 소켓을 리턴합니다. 이것은 네트워크 스택 소켓 처리의 핵심 기능입니다.

다시 말합니다: 방화벽 코드가 소켓 처리 함수를 부릅니다.

skb->sk = sk;

이후 TPROXY 는 실제로 소켓 할당을 합니다:

이 행에서 struct sock 소켓을 수신 패킷에 할당하고 처리를 완료 합니다.

3649474619_3b800400e9_z-1

모자에서 토끼를 끌어내자

CC BY-SA 2.0 image by Angela Boothroyd

# Set 192.0.2.0/24 to be routed locally with AnyIP.
# Make it explicit that the source IP used for this network
# when connecting locally should be in 127.0.0.0/8 range.
# This is needed since otherwise the TPROXY rule would match
# both forward and backward traffic. We want it to catch 
# forward traffic only.
sudo ip route add local 192.0.2.0/24 dev lo src 127.0.0.1

# Set the magical TPROXY routing
sudo iptables -t mangle -I PREROUTING \
        -d 192.0.2.0/24 -p tcp \
        -j TPROXY --on-port=1234 --on-ip=127.0.0.1

TPROXY를 이용하면 모든 포트에 바인딩하는 트릭을 매우 쉽게 할 수 있습니다. 다음과 같이 설정합니다:

IP_TRANSPARENT (since Linux 2.6.24)
Setting this boolean option enables transparent proxying on
this socket.  This socket option allows the calling applica‐
tion to bind to a nonlocal IP address and operate both as a
client and a server with the foreign address as the local
end‐point.  NOTE: this requires that routing be set up in
a way that packets going to the foreign address are routed 
through the TProxy box (i.e., the system hosting the 
application that employs the IP_TRANSPARENT socket option).
Enabling this socket option requires superuser privileges
(the CAP_NET_ADMIN capability).

TProxy redirection with the iptables TPROXY target also
requires that this option be set on the redirected socket.

이 설정에 추가하여 TCP 서버를 SO_TRANSPARENT 소켓 옵션과 같이 시작해야 합니다. 아래 예제가 동작하려면 tcp://127.0.0.1:1234 에서 리스닝할 필요가 있습니다. SO_TRANSPARENT 매뉴얼 페이지에는 다음과 같이 나와 있습니다:

import socket

IP_TRANSPARENT = 19

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.setsockopt(socket.IPPROTO_IP, IP_TRANSPARENT, 1)

s.bind(('127.0.0.1', 1234))
s.listen(32)
print("[+] Bound to tcp://127.0.0.1:1234")
while True:
    c, (r_ip, r_port) = s.accept()
    l_ip, l_port = c.getsockname()
    print("[ ] Connection from tcp://%s:%d to tcp://%s:%d" % (r_ip, r_port, l_ip, l_port))
    c.send(b"hello world\n")
    c.close()

여기 간단한 파이썬 서버가 있습니다:

$ nc -v 192.0.2.1 9999
Connection to 192.0.2.1 9999 port [tcp/*] succeeded!
hello world

서버를 실행 후에 임의의 IP주소로 연결할 수 있습니다:

$ sudo python3 transparent2.py
[+] Bound to tcp://127.0.0.1:1234
[ ] Connection from tcp://127.0.0.1:60036 to tcp://192.0.2.1:9999

더 중요한 건 아무도 해당 IP와 포트에 리스닝하고 있지 않아도 이 연결의 수신 주소가 192.0.2.1 포트 9999라고 서버가 보고하고 있다는 것입니다.

짠! 이것이 conntrack을 사용하지 않고 리눅스의 임의의 포트에 바인딩하는 방법입니다.

이게 끝!

이 글에서 우리는 원래 투명 프록시을 돕기 위해 만들어졌던 잘 알려져 있지 않은 iptables 모듈을 사용하는 방법에 대해서 알아 보았습니다. 이것의 도움을 받아서 표준 BSD 소켓 API로는 불가능하다고 생각했던 것을을 할 수 있었고 별도의 커널 패치를 만들지 않아도 되었습니다.

TPROXY 모듈은 리눅스 방화벽이 일반적으로 네트워크 스택에서 이루어지는 일을 수행한다는 점에서 매우 특이합니다. 공식 문서는 다소 부족해서 많은 리눅스 사용자들이 이 모듈의 진정한 힘을 이해하고 있다고 보기는 어렵습니다.

TPROXY는 우리의 Spectrum 제품이 수정 없는 커널에서 잘 동작하도록 하고 있습니다. 이건 iptables와 네트워크 스택을 이해하도록 노력하는 것이 얼마나 중요한지 보여주는 또 다른 예이기도 합니다!


저수준 소켓 작업이 재미있어 보이나요? 런던, 오스틴, 샌프란시스코 및 폴란드 바르샤바의 세계 최고 수준의 팀에 합류 하세요.


  1. 적절한 rp_filter와 BGP 설정에 추가로 IP 주소를 루프백 인터페이스에 할당하여 우리의 엣지 서버에서 임의의 IP대역을 처리하도록 합니다. ↩︎

This is a Korean translation of a existing post by Marek Majkowski, translated by Junho Choi.

Cloudflare에서는 전체 기업 네트워크를 보호하고, 고객이 인터넷 규모의 애플리케이션을 효과적으로 구축하도록 지원하며, 웹 사이트와 인터넷 애플리케이션을 가속화하고, DDoS 공격을 막으며, 해커를 막고, Zero Trust로 향하는 고객의 여정을 지원합니다.

어떤 장치로든 1.1.1.1에 방문해 인터넷을 더 빠르고 안전하게 만들어 주는 Cloudflare의 무료 앱을 사용해 보세요.

더 나은 인터넷을 만들기 위한 Cloudflare의 사명을 자세히 알아보려면 여기에서 시작하세요. 새로운 커리어 경로를 찾고 있다면 채용 공고를 확인해 보세요.
제품 뉴스SpectrumDDoS보안Linux속도 및 신뢰성

X에서 팔로우하기

Marek Majkowski|@majek04
Cloudflare|@cloudflare

관련 게시물

2024년 11월 20일 오후 10:00

Bigger and badder: how DDoS attack sizes have evolved over the last decade

If we plot the metrics associated with large DDoS attacks observed in the last 10 years, does it show a straight, steady increase in an exponential curve that keeps becoming steeper, or is it closer to a linear growth? Our analysis found the growth is not linear but rather is exponential, with the slope varying depending on the metric (rps, pps or bps). ...