eSIM_BLEED
lpac + OpenEUICC에서 93개의 취약점을 발견했다. CVSS 9.8 킬 체인으로 네트워크 공격자가 SIM 인증 정보를 탈취할 수 있다. 모든 오픈소스 eSIM 도구가 TLS 검증을 비활성화한 채 배포되고 있다.
SOURCE_BLEED_TEAM
eSIM Security Research
// TL;DR
lpac은 가장 널리 사용되는 오픈소스 eSIM LPA 클라이언트다. OpenEUICC는 lpac을 JNI로 호출하는 대표적인 Android eSIM 관리 앱이다. 이 두 프로젝트가 오픈소스 eSIM 생태계의 근간을 이루고 있다. 감사 결과, 두 프로젝트 모두 TLS 인증서 검증이 완전히 비활성화된 상태로 배포되고 있음을 확인했다. 이는 모든 SM-DP+ 연결이 단순한 중간자 공격에 노출됨을 의미한다. 여기에 DER 파서의 정수 오버플로우와 검증 없는 JNI 포인터 캐스팅이 결합되면, 네트워크 공격자가 가입자 인증 정보(Ki, OPc, IMSI)를 탈취하거나 원격 코드 실행을 달성할 수 있다.
01 // DAMAGE_REPORT
| DOMAIN | CRIT | HIGH | MED | LOW | TOTAL |
|---|---|---|---|---|---|
| MEMORY / PARSER | 5 | 10 | 11 | 8 | 34 |
| NETWORK / TLS | 3 | 5 | 2 | 2 | 12 |
| DRIVER / OS | 2 | 5 | 7 | 5 | 19 |
| JNI BRIDGE | 2 | 3 | 5 | - | 10 |
| ANDROID | - | 3 | 8 | 7 | 18 |
02 // THE_TRIPLE_THREAT
세 가지 종류의 취약점이 결합되어 가장 치명적인 공격 표면을 형성한다. 각각 단독으로도 위험하지만, 셋이 합쳐지면 CVSS 9.8 킬 체인이 완성된다.
TLS VALIDATION: OFF
lpac의 두 HTTP 백엔드 — curl과 WinHTTP — 모두 인증서 검증이 완전히 비활성화된 상태로 배포된다. 설정 오류가 아니다. 하드코딩이다.
libcurl._curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
libcurl._curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);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 프로파일 다운로드를 가로챌 수 있다.
GSMA SGP.22 RSP 보안 모델이 완전히 무력화된다. 단독 발견만으로도 CVE 부여 대상이다.
DER PARSER: CHAINABLE OVERFLOW
euicc/derutil.c 의 자체 구현 DER/ASN.1 파서는 모든 eSIM 프로토콜 처리의 핵심이다. 이 파서에 체이닝 가능한 세 가지 정수 오버플로우가 존재하며, 전체 파스 트리를 오염시킨다.
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--;
}
}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 corruptedcptr = 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체인: 조작된 length 필드 → self.length 오염 → unpack_next가 임의 메모리를 읽는다. 모든 ES10a/b/c 및 ES8+ 파서 경로가 영향을 받는다. SM-DP+ 응답을 통해 네트워크에서 도달 가능하다.
JNI BRIDGE: ARBITRARY MEMORY READ
OpenEUICC는 JNI를 통해 lpac을 호출한다. lpac-jni.c 의 브릿지 코드는 Java jlong 값을 — 어떤 검증도 없이 — C 포인터로 직접 캐스팅한다. 이러한 호출 지점이 15곳에 존재한다.
// 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의 포인터 연산이 시작되는 경계 — 그런데 이 경계에 어떤 안전장치도 없다.
Java 측에서 임의 메모리 읽기가 가능하다. 민감 데이터 제로화가 전혀 이루어지지 않으므로, 이전 세션의 ICCID/Ki/IMEI를 힙에서 복구할 수 있다.
03 // KILL_CHAIN: REMOTE PROFILE HIJACKING
공격자가 DNS를 오염시킨다: SM-DP+ 도메인이 공격자 IP로 해석됨
lpac이 자체 서명 인증서로 연결 — VERIFYPEER=0이 모든 인증서를 수락
공격자가 악의적 DER이 base64 인코딩된 조작된 ES9+ JSON을 응답
base64_decode_len() 정수 오버플로우 → 작은 버퍼 할당, 큰 쓰기
Length 필드 조작 → self.length 오염 → unpack_next OOB R/W
힙 메타데이터 덮어쓰기 → 코드 실행
가입자 인증 정보 추출: 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에서 정수 언더플로우를 유발한다:
// 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-overflowREPRODUCTION STATUS
외부 연구자(Jhury Kevin Lastre)에 의해 ASAN 크래시가 확인되었다. 트리거 조건은 SM-DP+ 응답 내 단일 비정상 TLV뿐이다. TLS 검증이 비활성화된 상태에서는 공격자 측 인증이 필요 없다.
05 // MORE_KILL_CHAINS
MALICIOUS_eUICC → RCE
악성 SIM 카드 → DER length=0xFFFFFFFE → self.length 오버플로우 → ICCID malloc(1) → 힙 오버플로우 → stack protector 없음 → RCE
MALICIOUS_APP → PROFILE_THEFT
악성 앱 → lpa: URI 인텐트(exported, 권한 가드 없음) → 공격자 SMDP 주입 → TLS 우회 → 프로파일 탈취
WINHTTP_STACK_OVERFLOW → RCE
악성 QR 코드 → 256자 초과 hostname → sizeof/wchar 불일치 → 스택 버퍼 오버플로우 → ASLR/canary 없음 → 직접 RCE
LOCAL_SUPPLY_CHAIN
환경변수 인젝션(APDU_UQMI_PROGRAM) + SO 하이재킹(dlopen 검증 없음) + Magisk TMPDIR 레이스 → root 포함 임의 실행
JNI_HANDLE → MEMORY_READ
jlong 핸들 조작 → 이중 포인터 역참조 → 임의 메모리 읽기 → 힙에서 ICCID/Ki 복구(제로화 없음)
06 // ROOT_CAUSE_ANALYSIS
93개의 발견 사항은 5가지 체계적 패턴으로 수렴한다. 개별 증상이 아닌, 패턴을 고쳐야 한다.
ZERO-LENGTH DEFENSE ABSENT
bin2hex, bin2gsmbcd, base64_decode — 어느 것도 len=0을 처리하지 않는다. TLV length=0은 유효한 BER/DER이다.
INTEGER OVERFLOW GUARDS MISSING
2*bin_len, length*2+1, (len+2)/3*4+1 — 모든 크기 계산에 가드가 없다. uint32_t 오버플로우 → 작은 malloc → 대량 쓰기.
RETURN VALUES IGNORED
bin2hex/bin2gsmbcd는 오류 시 -1을 반환한다. 모든 ES8/ES9/ES10 파서가 이를 무시하고 오염된 데이터로 진행한다.
JNI RETURNS UNCHECKED
GetStringUTFChars, FindClass, calloc — 30곳 이상의 호출 지점에서 NULL 검사가 없다. Java-C 경계에 안전망이 전혀 없다.
BUILD HARDENING ABSENT
FORTIFY_SOURCE 없음, stack-protector 없음, PIE/RELRO 없음. 익스플로잇 개발 난이도가 '어려움'에서 '사소함'으로 떨어진다.
07 // WHAT_IS_AT_STAKE
eSIM 프로파일에는 다음이 포함되어 있다:
가입자 인증 키. 이것만 있으면 공격자가 SIM을 복제하여 모든 통화와 SMS를 수신할 수 있다.
통신사 변형 알고리즘 설정값. Ki와 결합하면 완전한 Milenage 인증 우회가 가능하다.
국제 모바일 가입자 식별번호. 추적, 표적 감청, 신원 도용이 가능해진다.
이것들은 모바일 보안의 핵심 자산이다. 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 프로비저닝 트래픽이 네트워크 인접 공격자에게 가로챌 수 있었다고 가정해야 한다.