Skip to content
KitploitKITPLOIT
도구익스플로잇블로그
Log in
제출
도구익스플로잇블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

피드문의개인정보© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
TheLastBundleMismatch — CVE-2023-45777에 대한 분석 및 익스플로잇, Android 13의 AccountManagerService 내 Intent 검증 우회 — "Lazy Bundle" 완화 조치에도 불구하고 | Kitploit
도구/GitHubGitHub/michalbednarski/thelastbundlemismatch
Android SecurityVulnerability AnalysisExploitationBinary AnalysisPapers & Research
GitHubmichalbednarski/thelastbundlemismatch

TheLastBundleMismatch

CVE-2023-45777에 대한 분석 및 익스플로잇, Android 13의 AccountManagerService 내 Intent 검증 우회 — "Lazy Bundle" 완화 조치에도 불구하고

저장소 보기

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유
10114672년 전Kitploit 검토 완료

Mysterious patch

이번에는 Android Security Bulletin에서 CVE-2023-45777에 대한 수정 사항으로 등장한 패치부터 시작해 보겠습니다.```diff diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java index 7a19d034c2c8..5238595fe2a2 100644 --- a/services/core/java/com/android/server/accounts/AccountManagerService.java +++ b/services/core/java/com/android/server/accounts/AccountManagerService.java @@ -4923,7 +4923,7 @@ public class AccountManagerService p.setDataPosition(0); Bundle simulateBundle = p.readBundle(); p.recycle();

  •        Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT);
    
  •        Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT, Intent.class);
           if (intent != null && intent.getClass() != Intent.class) {
               return false;
           }
    
몇몇 사람들은 이 문제에 대해 충분히 궁금해하여 저에게 물어보기도 했습니다. 이전에는 힌트만 알려드렸는데, 이제 이 이슈에 대한 전체 분석 글을 공개합니다.

하지만 먼저 이 패치에서 무슨 일이 일어나고 있는지에 대한 맥락을 살펴보겠습니다.

이것은 [`checkKeyIntent()` 메서드](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/accounts/AccountManagerService.java;l=4938-4954;drc=47de64a38aa1799cb41f41b2ea0c539ee61de64d)의 변경 사항입니다. 이 메서드는 애플리케이션이 제공한 `Intent`가 시스템이 (시스템 권한을 사용하여) 실행하기에 안전한지 확인하기 위해 여러 검사를 수행합니다.

먼저, 이 메서드는 `checkKeyIntentParceledCorrectly()`를 사용하여 검사 중인 `Bundle`을 직렬화한 다음 다시 역직렬화하고, 그 전에 `Bundle`에서 가져온 `Intent`가 이러한 과정을 거친 후의 `Bundle`의 `Intent`와 일치하는지 확인합니다. `Intent`의 실행은 검증을 수행하는 프로세스가 아닌 다른 시스템 앱 프로세스에서 발생하므로, 이전에는 [`AccountManagerService` 내부의 검증 중에는 안전해 보이지만 다음 프로세스로 전송된 후에는 다른 `Intent`를 포함하는 `Bundle`을 구성하는 것이 가능했습니다](https://github.com/michalbednarski/IntentsLab/issues/2#issuecomment-344365482). 이는 `Bundle`을 다음 프로세스로 보내는 것을 시뮬레이션하여 이러한 상황을 감지합니다.

`checkKeyIntentParceledCorrectly()` 다음에는 `bundle.getParcelable()` 호출이 있는데, 이 패치는 이 호출을 임의의 객체를 생성할 수 있는 더 이상 사용되지 않는 버전에서 역직렬화하려는 객체가 두 번째 매개변수에 지정된 유형인지 검증하는 버전으로 전환합니다.

유형 매개변수가 있는 이 버전은 더 큰 `Parcel`/`Bundle` 강화의 일부로 Android 13에서 도입되었습니다. 특히, Android 13 이전에는 `Bundle`이 프로세스 간에 전송될 때 항목에 접근할 때까지 직렬화된 데이터 전체의 원본 복사본을 유지했으며, 이때 모든 값이 역직렬화되었습니다. 이제 `Bundle`을 수신한 후 어떤 값에 처음 접근하면 `String` 키와 기본 유형의 값만 역직렬화되고, 기본 유형이 아닌 값은 `LazyValue`로 남게 됩니다. `LazyValue`는 직렬화된 데이터의 일부로 길이를 저장하므로 직렬화/역직렬화 로직이 일치하지 않더라도 이러한 불일치가 다른 항목에 영향을 미치지 않도록 보장합니다.

자세히 살펴보기 전에 `LazyValue`를 살펴보겠습니다. 해당 소스 코드에는 [데이터 구조를 설명하는 훌륭한 주석이 있습니다](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4392-4399;drc=03c34f57c05feecfb090de3917787f049cb5f804).```
                     |   4B   |   4B   |
mSource = Parcel{... |  type  | length | object | ...}
                     a        b        c        d
length = d - c
mPosition = a
mLength = d - a

mPosition 및 mLength는 원본 Parcel에서 전체 LazyValue 데이터의 위치를 설명하며, type과 length를 포함합니다. "length"(앞에 "m"이 없는 것)는 Parcel에 기록된 길이 값을 의미하며 헤더(type 및 length)는 제외합니다.

LazyValue를 포함하는 Bundle이 다른 프로세스로 전달되는 경우, type 및 length 필드를 포함한 전체 LazyValue가 Bundle.mParcelledData에서 대상 Parcel로 그대로 복사됩니다

LazyValue로 표현되는 Bundle 항목에 접근하면, Parcel이 mPosition으로 되감기고 readValue()가 호출됩니다. bundle.getParcelable()에 타입 인수가 전달되면 이는 readValue()로 전파되어, 언파셀링될 타입이 예상된 타입인지 확인할 뿐만 아니라 언파셀링 후 언파셀링된 값의 타입이 예상된 타입인지 검증합니다. 언파셀링 후 LazyValue는 교체되므로, 다음에 Bundle이 Parcel에 기록될 때 값은 writeValue()를 통해 다시 직렬화됩니다.

타입이 지정된 Bundle.get*()/Parcel.read*() 매개변수의 사용은 주로 Parcel.readParcelableList()와 같은 메서드와 관련이 있습니다. 이 메서드는 ArrayList를 반환하며, Java 타입 소거(Type Erasure)로 인해 List<SomeParcelableType> field = parcel.readParcelableList();와 같은 코드를 작성하더라도 <SomeParcelableType> 부분은 런타임에 강제되지 않으므로, 이러한 List에는 시스템에서 사용 가능한 모든 Parcelable 클래스가 포함될 수 있으며, 따라서 시스템에서 사용 가능한 모든 createFromParcel/writeToParcel이 해당 List를 포함하는 타입의 직렬화/역직렬화의 일부로 사용될 수 있습니다.

또한 Android 보안 및 개인정보 보호 팀의 이러한 메커니즘 도입에 관한 프레젠테이션(슬라이드, 비디오)을 확인해 보시기 바랍니다.

그러나 여기서는 반환된 객체의 타입을 명시적으로 확인하기도 하므로 타입이 지정된 버전의 사용이 중복되는 것으로 보입니다. 그렇다면 여기서 무슨 일이 일어나고 있으며 어떤 취약점이 수정되고 있는 것일까요?

부작용

처음부터 패치를 다시 살펴보겠습니다.

  • "intent" 키 아래에서 역직렬화된 값이 Intent인 경우
    • 시스템이 실행하기에 안전한 구성 요소를 가리키는지 검증됩니다.
    • Intent 객체에서 불일치가 발생할 수 있다면 훨씬 더 큰 문제가 있을 것입니다
  • 역직렬화되는 값이 Intent가 아닌 경우
    • 문제를 일으키려면 다른 프로세스로 전송된 후 Bundle 내부에 Intent가 있어야 하지만, Parcelable의 타입은 가능한 불일치보다 더 이른 오프셋에 저장되며 LazyValue의 길이 접두사(length-prefixing)로 인해 writeToParcel/createFromParcel 불일치 시 다음 키-값 쌍을 수정할 수 없습니다.

그렇다면 타입 인수 없이 bundle.getParcelable(AccountManager.KEY_INTENT)를 호출하면 여기서 어떤 위험한 일이 발생할 수 있을까요?

[다음 문단에 답이 있습니다. 읽기 전에 추측해 보세요. 제가 퍼소나(fursona)를 가지고 있다면 여기에 예술 작품이 들어갈 자리일 것입니다]

답은 다른 키 아래에 저장되고 다음 프로세스에 그대로 전달될 LazyValue의 원시 데이터를 실제로 수정하는 관련 없는 createFromParcel()을 호출하는 것입니다.

제공된 Parcel에서 실제로 writeInt()를 호출할 수 있는 createFromParcel() 구현이 있습니다.

하지만 이는 writeInt가 실수로 배치되었기 때문이 아니라 제한 없는 리플렉션(reflection) 때문입니다. 특히 PackageParser 내부에는 다음과 같은 코드가 있습니다:```java final Class cls = (Class) Class.forName(componentName); final Constructor cons = cls.getConstructor(Parcel.class);

intentsList = new ArrayList<>(N); for (int i = 0; i < N; ++i) { intentsList.add(cons.newInstance(in)); }

We can have `Parcel` object which was passed to `createFromParcel` passed to any available in system `public` constructor that accepts single `Parcel` argument

And then [we have following code](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/PooledStringWriter.java;l=51-56;drc=782d49826862cbdc9d020fc9d85f8a6f64675dcb):```java
public PooledStringWriter(Parcel out) {
    mOut = out;
    mPool = new HashMap<>();
    mStart = out.dataPosition();
    out.writeInt(0); // reserve space for final pool size.
}

We've got constructor that calls writeInt(0) on provided Parcel, however there are few things that complicate exploitation

First of all, while it isn't directly visible in source code, immediately after newInstance() is called, a cast is performed and ClassCastException is thrown

Swallow the Exception

I needed something that would during createFromParcel call createFromParcel of another class under try block and then fail to propagate caught Exception

This is part where exploit doesn't actually work on pure AOSP, I've used Samsung specific class

도구 다운로드