CRITICALAPR_02_202625_MIN_READ

eSIM_BLEED

lpac + OpenEUICC에서 93개의 취약점을 발견했다. CVSS 9.8 킬 체인으로 네트워크 공격자가 SIM 인증 정보를 탈취할 수 있다. 모든 오픈소스 eSIM 도구가 TLS 검증을 비활성화한 채 배포되고 있다.

sim_card

SOURCE_BLEED_TEAM

eSIM Security Research

93VULNS
9.8MAX_CVSS
6KILL_CHAINS

// TL;DR

lpac은 가장 널리 사용되는 오픈소스 eSIM LPA 클라이언트다. OpenEUICC는 lpac을 JNI로 호출하는 대표적인 Android eSIM 관리 앱이다. 이 두 프로젝트가 오픈소스 eSIM 생태계의 근간을 이루고 있다. 감사 결과, 두 프로젝트 모두 TLS 인증서 검증이 완전히 비활성화된 상태로 배포되고 있음을 확인했다. 이는 모든 SM-DP+ 연결이 단순한 중간자 공격에 노출됨을 의미한다. 여기에 DER 파서의 정수 오버플로우와 검증 없는 JNI 포인터 캐스팅이 결합되면, 네트워크 공격자가 가입자 인증 정보(Ki, OPc, IMSI)를 탈취하거나 원격 코드 실행을 달성할 수 있다.

01 // DAMAGE_REPORT

12CRITICAL
26HIGH
33MEDIUM
22LOW
DOMAINCRITHIGHMEDLOWTOTAL
MEMORY / PARSER51011834
NETWORK / TLS352212
DRIVER / OS257519
JNI BRIDGE235-10
ANDROID-38718

02 // THE_TRIPLE_THREAT

세 가지 종류의 취약점이 결합되어 가장 치명적인 공격 표면을 형성한다. 각각 단독으로도 위험하지만, 셋이 합쳐지면 CVSS 9.8 킬 체인이 완성된다.

lock_open
THREAT_01

TLS VALIDATION: OFF

CRITICAL8.1

lpac의 두 HTTP 백엔드 — curl과 WinHTTP — 모두 인증서 검증이 완전히 비활성화된 상태로 배포된다. 설정 오류가 아니다. 하드코딩이다.

driver/http/curl.c:91-92C
libcurl._curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
libcurl._curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
driver/http/winhttp.c:71C
WinHttpSetOption(hRequest, WINHTTP_OPTION_SECURITY_FLAGS,
    &(DWORD){SECURITY_FLAG_IGNORE_UNKNOWN_CA}, sizeof(DWORD));

OpenEUICC는 여기서 한 술 더 뜬다. AllowAllTrustManager checkServerTrusted() 가 아무 동작 없이 리턴하는 TrustManager를 사용한다. Lint 경고는 @SuppressLint로 억제되어 있다.

결과적으로 어떤 인증서든 수락된다. 자체 서명, 만료, 도메인 불일치, 폐기 — 전부 통과한다. 카페 Wi-Fi에서 DNS 스푸핑만 가능한 공격자도 SM-DP+ 서버를 사칭하여 가입자 마스터 키가 포함된 eSIM 프로파일 다운로드를 가로챌 수 있다.

IMPACT

GSMA SGP.22 RSP 보안 모델이 완전히 무력화된다. 단독 발견만으로도 CVE 부여 대상이다.

data_object
THREAT_02

DER PARSER: CHAINABLE OVERFLOW

CRITICAL8.1

euicc/derutil.c 의 자체 구현 DER/ASN.1 파서는 모든 eSIM 프로토콜 처리의 핵심이다. 이 파서에 체이닝 가능한 세 가지 정수 오버플로우가 존재하며, 전체 파스 트리를 오염시킨다.

derutil.c:40-52 — Length field overflowC
if (result->length & 0x80) {
    uint8_t lengthlen = result->length & 0x7F;  // max 127, no cap
    result->length = 0;
    for (int i = 0; i < lengthlen; i++) {
        result->length = (result->length << 8) | *cptr;
        // lengthlen >= 5 → uint32_t wraps around
        cptr++; rlen--;
    }
}
derutil.c:61 — self.length poisoningC
result->self.length = result->value - result->self.ptr + result->length;
// length=0xFFFFFFFE + 3-byte header → self.length=1 (overflow)
// Every subsequent unpack_next() offset is now corrupted
derutil.c:66-75 — Underflow to ~4GB readC
cptr = prev->self.ptr + prev->self.length;
rlen = buffer_len - (cptr - buffer);
// When self.length is corrupted: cptr > buffer + buffer_len
// rlen wraps to ~4,294,967,295 → reads far beyond buffer
IMPACT

체인: 조작된 length 필드 → self.length 오염 → unpack_next가 임의 메모리를 읽는다. 모든 ES10a/b/c 및 ES8+ 파서 경로가 영향을 받는다. SM-DP+ 응답을 통해 네트워크에서 도달 가능하다.

memory
THREAT_03

JNI BRIDGE: ARBITRARY MEMORY READ

CRITICAL7.8

OpenEUICC는 JNI를 통해 lpac을 호출한다. lpac-jni.c 의 브릿지 코드는 Java jlong 값을 — 어떤 검증도 없이 — C 포인터로 직접 캐스팅한다. 이러한 호출 지점이 15곳에 존재한다.

lpac-jni.c:273 — stringDeref: double dereferenceC
// Java passes any 64-bit value as jlong "handle"
// C side blindly casts and double-dereferences:
return toJString(env, *((char **) curr));
// curr = attacker-controlled address → arbitrary memory read primitive

추가로, 약 30개의 JNI 반환값(GetStringUTFChars, FindClass, calloc)에 대한 NULL 검사가 전혀 없다. JNI 브릿지는 Java의 타입 안전성이 끝나고 C의 포인터 연산이 시작되는 경계 — 그런데 이 경계에 어떤 안전장치도 없다.

IMPACT

Java 측에서 임의 메모리 읽기가 가능하다. 민감 데이터 제로화가 전혀 이루어지지 않으므로, 이전 세션의 ICCID/Ki/IMEI를 힙에서 복구할 수 있다.

03 // KILL_CHAIN: REMOTE PROFILE HIJACKING

CVSS 9.8REMOTE / NO_AUTH / NO_INTERACTION
01
DNS_SPOOF

공격자가 DNS를 오염시킨다: SM-DP+ 도메인이 공격자 IP로 해석됨

02
TLS_BYPASS

lpac이 자체 서명 인증서로 연결 — VERIFYPEER=0이 모든 인증서를 수락

03
PAYLOAD_INJECT

공격자가 악의적 DER이 base64 인코딩된 조작된 ES9+ JSON을 응답

04
BASE64_OVERFLOW

base64_decode_len() 정수 오버플로우 → 작은 버퍼 할당, 큰 쓰기

05
DER_CHAIN

Length 필드 조작 → self.length 오염 → unpack_next OOB R/W

06
HEAP_CORRUPT

힙 메타데이터 덮어쓰기 → 코드 실행

07
EXFILTRATE

가입자 인증 정보 추출: Ki, OPc, IMSI — SIM 복제 달성

빌드 보안 강화 플래그의 부재(FORTIFY_SOURCE없음, stack-protector없음, PIE/RELRO 없음)로 인해 힙 손상에서 코드 실행까지 어떤 제동장치도 없다. 네트워크 포지션에서 완전한 가입자 침해까지 이어지는 교과서적 체인이다.

04 // ASAN_CONFIRMED: THE ZERO-LENGTH ICCID

발견 사항 중 하나가 AddressSanitizer 크래시 재현을 통해 독립적으로 확인되었다. SM-DP+ 응답 내 길이 0인 ICCID TLV(5A 00)가 hexutil.c에서 정수 언더플로우를 유발한다:

hexutil.c:107 — bin2gsmbcdC
// bin_len == 0 (from zero-length ICCID TLV)
int n = euicc_hexutil_bin2hex(output, output_len, bin, bin_len);
// n = 0
n -= 1;
// n = -1, but cast to uint32_t = 0xFFFFFFFF
gsmbcd_swap_chars(output, n);
// Attempts to swap 4,294,967,295 bytes → ASAN: heap-buffer-overflow

REPRODUCTION STATUS

외부 연구자(Jhury Kevin Lastre)에 의해 ASAN 크래시가 확인되었다. 트리거 조건은 SM-DP+ 응답 내 단일 비정상 TLV뿐이다. TLS 검증이 비활성화된 상태에서는 공격자 측 인증이 필요 없다.

05 // MORE_KILL_CHAINS

KC-2

MALICIOUS_eUICC → RCE

PHYSICAL7.5

악성 SIM 카드 → DER length=0xFFFFFFFE → self.length 오버플로우 → ICCID malloc(1) → 힙 오버플로우 → stack protector 없음 → RCE

KC-3

MALICIOUS_APP → PROFILE_THEFT

ANDROID7.5

악성 앱 → lpa: URI 인텐트(exported, 권한 가드 없음) → 공격자 SMDP 주입 → TLS 우회 → 프로파일 탈취

KC-4

WINHTTP_STACK_OVERFLOW → RCE

WINDOWS8.1

악성 QR 코드 → 256자 초과 hostname → sizeof/wchar 불일치 → 스택 버퍼 오버플로우 → ASLR/canary 없음 → 직접 RCE

KC-5

LOCAL_SUPPLY_CHAIN

LOCAL7.8

환경변수 인젝션(APDU_UQMI_PROGRAM) + SO 하이재킹(dlopen 검증 없음) + Magisk TMPDIR 레이스 → root 포함 임의 실행

KC-6

JNI_HANDLE → MEMORY_READ

LOCAL7.0

jlong 핸들 조작 → 이중 포인터 역참조 → 임의 메모리 읽기 → 힙에서 ICCID/Ki 복구(제로화 없음)

06 // ROOT_CAUSE_ANALYSIS

93개의 발견 사항은 5가지 체계적 패턴으로 수렴한다. 개별 증상이 아닌, 패턴을 고쳐야 한다.

01

ZERO-LENGTH DEFENSE ABSENT

bin2hex, bin2gsmbcd, base64_decode — 어느 것도 len=0을 처리하지 않는다. TLV length=0은 유효한 BER/DER이다.

02

INTEGER OVERFLOW GUARDS MISSING

2*bin_len, length*2+1, (len+2)/3*4+1 — 모든 크기 계산에 가드가 없다. uint32_t 오버플로우 → 작은 malloc → 대량 쓰기.

03

RETURN VALUES IGNORED

bin2hex/bin2gsmbcd는 오류 시 -1을 반환한다. 모든 ES8/ES9/ES10 파서가 이를 무시하고 오염된 데이터로 진행한다.

04

JNI RETURNS UNCHECKED

GetStringUTFChars, FindClass, calloc — 30곳 이상의 호출 지점에서 NULL 검사가 없다. Java-C 경계에 안전망이 전혀 없다.

05

BUILD HARDENING ABSENT

FORTIFY_SOURCE 없음, stack-protector 없음, PIE/RELRO 없음. 익스플로잇 개발 난이도가 '어려움'에서 '사소함'으로 떨어진다.

07 // WHAT_IS_AT_STAKE

eSIM 프로파일에는 다음이 포함되어 있다:

Ki

가입자 인증 키. 이것만 있으면 공격자가 SIM을 복제하여 모든 통화와 SMS를 수신할 수 있다.

OPc

통신사 변형 알고리즘 설정값. Ki와 결합하면 완전한 Milenage 인증 우회가 가능하다.

IMSI

국제 모바일 가입자 식별번호. 추적, 표적 감청, 신원 도용이 가능해진다.

이것들은 모바일 보안의 핵심 자산이다. SM-DP+ 프로파일 다운로드는 이 인증 정보가 네트워크를 통과하는 유일한 순간이다. GSMA는 바로 이 순간을 보호하기 위해 SGP.22에 TLS 상호 인증을 설계했다. lpac은 그 보호를 완전히 제거하고 있다.

08 // NOT_ALL_BAD

공정하게 평가하자. 두 프로젝트 모두 실질적인 보안 강점이 있다:

  • check_circleGSMA CI 루트 인증서 피닝— TLS 검증이 활성화된 경우, OpenEUICC는 시스템 CA 저장소가 아닌 GSMA CI 루트에 핀닝한다
  • check_circleHTTPS 강제— HttpInterfaceImpl이 https:// 이외의 URL을 거부한다
  • check_circleposix_spawnp over system()— uqmi가 명시적 argv를 사용하여 쉘 인젝션을 방지한다
  • check_circle권한 분리— OpenEUICC가 컴파일 타임에 권한/비권한 빌드를 분리한다
  • check_circleEuiccService 권한 가드— 시스템 서비스에 BIND_EUICC_SERVICE 서명 보호가 적용되어 있다

09 // FINAL_ASSESSMENT

오픈소스 eSIM 스택에는 개별 버그를 넘어서는 근본적인 보안 문제가 있다. 기본값으로 비활성화된 TLS 검증, 정수 오버플로우 가드 없이 작성된 자체 ASN.1 파서, 타입 안전성이 없는 JNI 브릿지 — 이것들은 아키텍처적 문제이며, 아키텍처적 해결이 필요하다.

가장 시급한 수정은 놀라울 정도로 간단하다: CURLOPT_SSL_VERIFYPEER를 1로 설정하면 된다. 한 줄의 코드 변경으로 가장 위험한 공격 벡터가 차단된다. DER 파서와 JNI 브릿지는 더 깊은 작업이 필요하지만, TLS 수정은 몇 년 전에 이루어졌어야 할 한 줄짜리 변경이다.

lpac이나 OpenEUICC — 또는 이들 기반의 어떤 도구든 — 사용하고 있다면, eSIM 프로비저닝 트래픽이 네트워크 인접 공격자에게 가로챌 수 있었다고 가정해야 한다.

eSIMGSMASGP.22lpacOpenEUICCTLSDERJNISAST

RESPONSIBLE DISCLOSURE IN PROGRESS. FULL TECHNICAL DETAILS WILL BE RELEASED AFTER VENDOR PATCHES ARE AVAILABLE.