Skip to content
KitploitKITPLOIT
도구블로그
Log in
제출
도구블로그
제출

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

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

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2022-20474 — Android CVE-2022-20474에 대한 상세 기술 분석 및 개념 증명(PoC)으로, 음수 길이의 LazyValue를 악용하여 스스로 변경되는 Bundle 동작을 유발하는 Bundle 불일치 취약점입니다. | Kitploit
도구/GitHubGitHub/cxxsheng/cve-2022-20474
Android SecurityVulnerability AnalysisExploitationPapers & ResearchLearning & EducationBinary Exploitation
GitHubcxxsheng/cve-2022-20474

CVE-2022-20474

Android CVE-2022-20474에 대한 상세 기술 분석 및 개념 증명(PoC)으로, 음수 길이의 LazyValue를 악용하여 스스로 변경되는 Bundle 동작을 유발하는 Bundle 불일치 취약점입니다.

저장소 보기
20151년 전Kitploit 검토 완료

인기

모두 보기 →

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

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

CVE-2022-20474 분석 – LazyValue 환경에서의 Self-changed Bundle

서문

참고: 이 글을 읽기 전에 Bundle Mismatch 관련 취약점에 대한 기본적인 이해가 있어야 합니다. 다음 참고 자료를 아직 읽지 않으셨다면 먼저 읽어보시길 권장합니다:

  1. Bundle 풍수 – Android 직렬화와 역직렬화 불일치 취약점 상세: 고전적인 입문 수준 튜토리얼.
  2. Android 역직렬화 취약점 공격과 방어의 역사: 훌륭한 요약 글.
  3. TheLastBundleMismatch: 최초의 LazyValue 패턴 Bundle Mismatch 글.

배경

최근 michalbednarski의 LeakValue 글을 열심히 공부하고 있었습니다. Canyie와 논의하던 중, 이 글에서 LazyValue 시나리오에서 Self-changing Bundle이 발생하는 경우도 언급되었다고 해서 원문을 다시 찾아보니 실제로 그런 내용이 있었는데, Michal의 글을 읽을 때 바로 놓쳤습니다. 원문에서는 다음과 같이 말합니다:

(Also LazyValue with negative length specified can be used (without using other bugs described in this writeup) to create self-changing Bundle, the thing LazyValue was created to eliminate. But that is another story (and separately reported to Google), in this exploit I'm aiming for more)

Michal이 가리키는 것은 CVE-2022-20474일 것입니다. (bulletin, patch). 패치 링크의 함수가 완전하지 않아 직접 보완하여 자세히 살펴보았습니다:

@@ -4388,6 +4388,9 @@
    public Object readLazyValue(@Nullable ClassLoader loader) {
         int start = dataPosition();
         int type = readInt();
         if (isLengthPrefixed(type)) {
             int objectLength = readInt();
+            if (objectLength < 0) {
+                return null;
+            }
             int end = MathUtils.addOrThrow(dataPosition(), objectLength);
             int valueLength = end - start;
             setDataPosition(end);
             return new LazyValue(this, start, valueLength, type, loader);
         } else {
            return readValue(type, loader, /* clazz */ null);
         }
    }             

코드에서 objectLength는 LazyValue의 length입니다. 실제로 이것은 LazyValue가 포함하는 가변 객체의 길이에 불과하며, 전체 LazyValue의 길이는 mLength 필드, 즉 코드에서 valueLength로 제어됩니다. 이 값은 LazyValue의 생성자에서 mLength로 전달됩니다.

이제 LazyValue의 레이아웃 형식을 비교해 보겠습니다:

       /**
         *                      |   4B   |   4B   |
         * mSource = Parcel{... |  type  | length | object | ...}
         *                      a        b        c        d
         * length = d - c
         * mPosition = a
         * mLength = d - a
         */

위의 내용을 바탕으로 다음과 같은 사실을 얻을 수 있습니다:

  1. mLength는 전체 LazyValue의 길이를 나타내며, mLength = objectLength + 8바이트입니다.
  2. objectLength는 0 이상이어야 합니다.
  3. LazyValue 객체에는 mLength만 저장되고 objectLength는 저장되지 않습니다. LazyValue가 메모리 복사를 할 때 전체 객체를 기준으로 복사하기 때문입니다.
  4. 읽기 이후 포인터는 앞으로 이동하며, LazyValue를 다시 읽을 수도 있습니다.

그런 다음 심사숙고 끝에 이러한 사실은 별로 쓸모가 없다는 데 동의했습니다. 위의 사실에 기반하면 읽기 시에만 한 번 수정할 수 있기 때문입니다. Self-changed Bundle의 핵심 아이디어는 읽기가 완료된 후에 수정되어야 보안 검사를 우회할 수 있다는 것입니다.

포기하려는 순간, 패치 설명에 몇 가지 세부 사항이 있음을 발견했습니다:

Addresses a security vulnerability where a (-8) length object would cause dataPosition to be reset back to the statt of the value, and be re-read again.

비정상적인 objectLength

objectLength가 -8일 때 몇 가지 문제가 발생한다고 언급되어 있습니다. 이것은 우리에게 추가적인 힌트를 주었습니다. 이 경우 LazyValue가 정상적으로 적용(apply)될 수 있을까요?

        @Override
        public Object apply(@Nullable Class<?> clazz, @Nullable Class<?>[] itemTypes) {
            Parcel source = mSource;
            if (source != null) {
                synchronized (source) {
                    // Check mSource != null guarantees callers won't ever see different objects.
                    if (mSource != null) {
                        int restore = source.dataPosition();
                        try {
                            source.setDataPosition(mPosition);
                            mObject = source.readValue(mLoader, clazz, itemTypes);
                        } finally {
                            source.setDataPosition(restore);
                        }
                        mSource = null;
                    }
                }
            }
            return mObject;
        }

  	/**
     * @see #readValue(int, ClassLoader, Class, Class[])
     */
    @Nullable
    private <T> T readValue(@Nullable ClassLoader loader, @Nullable Class<T> clazz,
            @Nullable Class<?>... itemTypes) {
        int type = readInt();
        final T object;
        if (isLengthPrefixed(type)) {
            int length = readInt();
            int start = dataPosition();
            object = readValue(type, loader, clazz, itemTypes);
            int actual = dataPosition() - start;
            if (actual != length) {
                Slog.wtfStack(TAG,
                        "Unparcelling of " + object + " of type " + Parcel.valueTypeToString(type)
                                + "  consumed " + actual + " bytes, but " + length + " expected.");
            }
        } else {
            object = readValue(type, loader, clazz, itemTypes);
        }
        return object;
    }

실제 readValue는 mPosition부터 읽기 시작하여 LazyType과 objectLength를 차례로 읽은 후 정상적인 Value 읽기 흐름으로 들어갑니다. 예를 들어 Parcelable은 ClassName을 읽은 후 createFromParcel을 실행합니다. 읽기가 완료되면 일반적인 Key-Value와 전혀 차이가 없으며 후속 직렬화에도 영향을 미치지 않습니다. 다시 생각해보면, Self-changed Bundle의 핵심은 읽기 완료 후 수정하는 것입니다. 여기서는 단순한 범위 초과 읽기에 불과하므로 이 방향은 통하지 않을 것 같습니다.

그렇다면 LazyValue가 이 과정에서 apply되지 않는 경우, 즉 LazyValue 상태로 IPC에 계속 참여하는 경우에는 writeToParcel 함수가 호출됩니다:

     public void writeToParcel(Parcel out) {
            Parcel source = mSource;
            if (source != null) {
                synchronized (source) {
                    if (mSource != null) {
                        out.appendFrom(source, mPosition, mLength);
                        return;
                    }
                }
            }
            out.writeValue(mObject);
        }

전체 LazyValue가 직접 복사됩니다. 단, mLength = 0인 경우는 예외입니다. 잠깐만요! 위에서 mLength = objectLength + 8바이트라고 했는데, 패치 정보에 따르면 취약점을 트리거하려면 objectLength가 -8이어야 하므로 mLength = 0이 성립합니다. 즉, 이 시나리오에서 전체 LazyValue가 사라져서 String Key만 복사됩니다. 따라서 누락된 쓰기가 발생하여 Self-changed Bundle의 조건이 바로 충족됩니다.

원인을 알았으니 재현을 시작할 수 있습니다. 하지만 그 전에 몇 가지 세부 사항이 더 필요합니다.

세부 사항 1: 두 가지 유형의 Bundle

 static final int BUNDLE_MAGIC = 0x4C444E42; // 'B' 'N' 'D' 'L'
 private static final int BUNDLE_MAGIC_NATIVE = 0x4C444E44; // 'B' 'N' 'D' 'N'

알다시피 Bundle의 메모리 레이아웃은 대략 다음과 같습니다:

       /**
         *        |   4B   |   4B   |   4B   |
         * Bundle{| length |  MAGIC |  size  | Key  | Value | Key  | Value | ...}
         *
         */

MAGIC은 Bundle의 메모리 레이아웃에서 매직 넘버로, BUNDLE_MAGIC 또는 BUNDLE_MAGIC_NATIVE가 될 수 있습니다. 가장 중요한 차이점은 BUNDLE_MAGIC을 사용하면 Key-Value가 역직렬화 완료 후 재정렬된다는 점입니다. 다음 코드와 같습니다:

도구 다운로드