UAF in Chromium RenderFrameHost class and iframe

reference

RenderFrameHost

Chrome Browsersite isolation 기능을 사용해서 한 탭마다 하나의 Process가 맵핑되게 한다. 하나의 Chrome ProcessBrowser ProcessRenderer Process로 나눌 수 있다. Renderer Process에는 여러 프레임들이 로딩될 수 있다. Chrome ProcessHTML, CSS, JavaScript등의 구문을 분석하여 Main Frame에 표시한다. 이 RendererMain FrameBrowser Process가 추적할 수 있게 도와주는 것이 바로 RenderFrameHost 객체이다. 하나의 Frame만 로딩된다면 덜 복잡하겠지만, iframe과 같이 하나의 Renderer Process 밑에 여러 하위 Frame들이 로딩될 수 있다. 만약 iframe으로 불러온 하위 Frame이 Main Frame에서 불러온 것이라면 단순히 Frame 객체를 생성하지만, 만약 다른 곳에서 불러온 것이라면 site isolation에 의해 새로운 Renderer Process가 생성된다.

Mojo

Chromium에는 두 가지 IPC가 존재한다. 레거시 IPCMojo IPC가 있는데 요즘은 대부분 Mojo IPC를 이용해 Renderer ProcessBrowser Process가 통신을 수행한다. Mojo Interface는 Frame별로 바인딩된다. 즉, iframe이 생성되면 Renderer Process단에서 새로운 Mojo 바인딩를 요청하고 Browser Process에서 새로운 Mojo Interface 객체를 할당해준다.

Mojo를 이용한다면 웹 페이지에 직접적인 접근 없이 샌드박스에서 허용하지 않은 작업에 대해서 쉽게 접근할 수 있다. 그러나 Mojo Interface 객체RenderFrameHost의 WebContentsImpl 객체나 RenderFrameProcess 객채에 직접 접근해야 할 때가 있다. 이는 Mojo Interface 생성자에서 Interface를 인스턴스화한 RenderFrameHost에 대한 raw pointer를 제공하는 것이다. 이렇게 하면 SensorProviderProxyImpl 생성자에서 멤버 변수로 들고 갈 수 있다. 해당 동작은 SensorProviderProxyImpl에서 확인할 수 있다.

1
2
3
4
5
6
7
8
SensorProviderProxyImpl::SensorProviderProxyImpl(
    PermissionControllerImpl* permission_controller,
    RenderFrameHost* render_frame_host)
    : permission_controller_(permission_controller),
      render_frame_host_(render_frame_host) {
  DCHECK(permission_controller);
  DCHECK(render_frame_host);
}

여기서 한 가지 의문을 가져야 한다. SensorProviderProxyImpl 객체에서 RenderFrameHost에 대한 raw pointer를 사용하고 있는데, 과연 Mojo InterfaceRenderFrameHost 객체보다 수명이 더 길지 않다는 것을 보증할 수 있을까? 이 질문에 대한 답은 아래 코드를 통해 확인할 수 있다.

1
2
3
4
5
6
7
8
9
10
void RenderFrameHostImpl::GetSensorProvider(
    mojo::PendingReceiver<device::mojom::SensorProvider> receiver) {
  if (!sensor_provider_proxy_) {
    sensor_provider_proxy_ = std::make_unique<SensorProviderProxyImpl>(
        PermissionControllerImpl::FromBrowserContext(
            GetProcess()->GetBrowserContext()),
        this);
  }
  sensor_provider_proxy_->Bind(std::move(receiver));
}

만약, sensor_provider_proxy pointer가 초기화되지 않았다면, make_unique를 통해 초기화되므로 unique_ptr의 특성을 따라 SensorProviderProxyImpl 객체는 RenderFrameHost 객체와 lifetime이 서로 동일하기 때문에 같이 소멸되는 것을 보장받는다.

이러한 방법 말고도 Mojo를 이용해서 인스턴스화 하는 방법도 존재한다. Mojo::MakeSelfOwnedReceiver에 따르면 receiver는 stand-alone 객체로 존재하며 이 객체는 impl을 가지고 있으며 바인드된 interface endpoint가 오류를 검출하면 자동으로 정리된다고 한다.

1
2
3
4
5
6
7
SelfOwnedReceiverRef<Interface> MakeSelfOwnedReceiver(
    std::unique_ptr<Impl> impl,
    PendingReceiver<Interface> receiver,
    scoped_refptr<base::SequencedTaskRunner> task_runner = nullptr) {
  return StrongBinding<Interface>::Create(std::move(impl), std::move(receiver),
                                          std::move(task_runner));
}

즉, Mojo Interface객체는 Mojo Connectionlifetime이 동일하다. 이는, UI Thread가 RenderFrameHost 객체를 파괴하고 Mojo Connection(self-owned)이 살아있다면, endpoint에서 오류가 검출되기 전까지 Mojo Message를 처리하는 상황이 올 수도 있다. 한마디로 Mojo InterfaceFree된 RenderFrameHost에 관한 Message를 처리하게 되면 UaF가 일어나는 것이다.

Chromium에는 이러한 상황을 해결하기 위한 몇 가지 방법이 존재한다.

  1. WebContentsObserver: 선언한 Mojo InterfaceWebContentsObserver에서 상속되었을 경우, 특정 callback events들이 제공되는데, 해당 callback 중에는 RenderFrameHost 객체가 삭제될 때마다 트리거되는 RenderFrameDeleted가 있다. 해당 코드에서는 render_frame_hostnullptr로 초기화한다. InstalledAppProviderImpl에서 확인할 수 있다.
1
2
3
4
5
6
void InstalledAppProviderImpl::RenderFrameDeleted(
    RenderFrameHost* render_frame_host) {
  if (render_frame_host_ == render_frame_host) {
    render_frame_host_ = nullptr;
  }
}
  1. FrameServicebase: WebContentsObserver와 비슷하며 RenderFrameHost가 삭제되는 즉시, 메모리가 해제되는 것을 보장한다.
1
2
3
4
5
6
7
8
9
10
void RenderFrameDeleted(RenderFrameHost* render_frame_host) final {
    DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

    if (render_frame_host == render_frame_host_) {
      DVLOG(1) << __func__ << ": RenderFrame destroyed.";
      Close();
    }
  }
...
RenderFrameHost* const render_frame_host_ = nullptr;

Issue 1068395

Link
다음 crbug issue를 분석하여 Chromium에서 일어나는 UaF 버그에 대해 더 자세히 알아보도록 하겠다. (참고로 밑에 소스코드들은 stable version의 Chromium과 다르다.)

1
2
3
4
5
6
7
8
9
10
11
12
void RenderFrameHostImpl::BindSmsReceiverReceiver(
    mojo::PendingReceiver<blink::mojom::SmsReceiver> receiver) {

  if (GetParent() && !GetMainFrame()->GetLastCommittedOrigin().IsSameOriginWith(
                         GetLastCommittedOrigin())) {
    mojo::ReportBadMessage("Must have the same origin as the top-level frame.");
    return;
  }

  auto* fetcher = SmsFetcher::Get(GetProcess()->GetBrowserContext(), this);
  SmsService::Create(fetcher, this, std::move(receiver));
}

먼저 GetBrowserContext()this(RenderFrameHost object)를 인자로 갖고 SmsFetcher::Get 함수를 호출하고 SmsFetcher 객체를 가진 pointer를 반환한다. 그리고 해당 pointerRenderFrameHost(이하 RFH)를 인자로 갖는 SmsService::Create 함수를 호출한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SmsService::SmsService(
    SmsFetcher* fetcher,
    const url::Origin& origin,
    RenderFrameHost* host,
    mojo::PendingReceiver<blink::mojom::SmsReceiver> receiver)
    : FrameServiceBase(host, std::move(receiver)),
      fetcher_(fetcher),
      origin_(origin) {}
      
void SmsService::Create(
    SmsFetcher* fetcher,
    RenderFrameHost* host,
    mojo::PendingReceiver<blink::mojom::SmsReceiver> receiver) {
  DCHECK(host);

  // SmsService owns itself. It will self-destruct when a mojo interface
  // error occurs, the render frame host is deleted, or the render frame host
  // navigates to a new document.
  new SmsService(fetcher, host, std::move(receiver));
}

코드 주석에 나와있듯이 SmsService는 self-owned이다. 만약 Mojo Interface에서 에러가 검출되거나 RFH가 삭제되면 RFH는 새로운 문서로 이동한다. 위에서 설명한 Mojo::MakeSelfOwnedReceiver을 사용하진 않았지만, SmsService는 FrameServiceBase를 상속하므로 비슷한 효과를 낼 수 있다. 우선 이 코드에는 UaF 버그가 존재하지 않는다. 이유는 FrameServiceBase의 특성을 잘 생각해보면 알 수 있다. 다시 처음으로 돌아가서 SmsService::Get 함수와 SmsService::Create 함수를 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SmsFetcher* SmsFetcher::Get(BrowserContext* context, RenderFrameHost* rfh) {

  auto* stored_fetcher = static_cast<SmsFetcherImpl*>(
      context->GetUserData(kSmsFetcherImplKeyName));

  if (!stored_fetcher || !stored_fetcher->CanReceiveSms()) { 
    auto fetcher =
        std::make_unique<SmsFetcherImpl>(context, SmsProvider::Create(rfh));
    context->SetUserData(kSmsFetcherImplKeyName, std::move(fetcher));
  }

  return static_cast<SmsFetcherImpl*>(
      context->GetUserData(kSmsFetcherImplKeyName)); 
}

먼저 BrowserContextSmsFetcher 객체가 저장되어 있는지 확인한다. SmsFetcherBrowserContext가 둘 다 살아있고 SMS message를 수신할 수 있는 경우 해당 유저 데이터를 반환한다. 그러나 해당 조건문을 통과하지 못할 경우, 새로운 SmsFetcherImpl 객체를 생성한다. SmsProvider::Create 함수도 같이 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
std::unique_ptr<SmsProvider> SmsProvider::Create(RenderFrameHost* rfh) {
#if defined(OS_ANDROID)
  if (base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
          switches::kWebOtpBackend) ==
      switches::kWebOtpBackendSmsVerification) {
    return std::make_unique<SmsProviderGmsVerification>();
  }
  return std::make_unique<SmsProviderGmsUserConsent>(rfh); 
#else
  return nullptr;
#endif
}

여기에는 두 가지 SmsProvider type이 존재한다.

  1. SmsProviderGmsVerification: 상관 X
  2. SmsProviderGmsUserConsent: RFH raw pointer를 생성자의 인자로 사용한다. 우리가 주목해야 할 type이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
SmsProviderGmsUserConsent::SmsProviderGmsUserConsent(RenderFrameHost* rfh)
    : SmsProvider(), render_frame_host_(rfh) { 
  // This class is constructed a single time whenever the
  // first web page uses the SMS Retriever API to wait for
  // SMSes.
  JNIEnv* env = AttachCurrentThread();
  j_sms_receiver_.Reset(Java_SmsUserConsentReceiver_create(
      env, reinterpret_cast<intptr_t>(this)));
}


void SmsProviderGmsUserConsent::Retrieve() {
  JNIEnv* env = AttachCurrentThread();

  WebContents* web_contents =
      WebContents::FromRenderFrameHost(render_frame_host_); 
  if (!web_contents || !web_contents->GetTopLevelNativeWindow())
    return;

  Java_SmsUserConsentReceiver_listen(
      env, j_sms_receiver_,
      web_contents->GetTopLevelNativeWindow()->GetJavaObject());
}

SmsProviderGmsUserConsent 클래스 내부에 RFH raw pointer가 저장되어있다. 또한 SmsProviderGmsUserConsent::Retrieve 함수를 호출할 때마다 RFH에 변수에 접근하게 된다. 그리고 제일 중요한 것은 WebContents::FromRenderFrameHost 함수에 접근할 때 RFH가 alive 한지 freed된 상태인지 검증하는 코드가 존재하지 않아 UaF로 이어질 수 있다.

여기서 재밌는 점은 새로운 SmsService가 계속 만들어지면 SmsFetcherImpl는 계속해서 생성되지 않지만, RFH 객체에 대한 참조를 제공한다. 즉, 새로운 SmsProvider Mojo Interface에 대해 동일한 RFH raw pointer가 재사용되는 것이기 때문에 UaF에 매우 취약하다. 따라서 SmsService Interface에 바인드하는 새로운 RFH 객체가 존재할 경우, dangling RFH pointerSmsService객채에 저장된다. 시나리오는 다음과 같다.

  1. iframe(A)을 만들고 SmsReceiver에 바인드한다. 바인드가 성공적으로 되면 SmsFetecherImpl객체와 iframe(A)에 대한 RFH raw pointer를 갖고 있는 SmsProviderGmsUserConsent 객체가 생성된다.
  2. iframe(B)를 만들고 SmsReceiver에 바인드한다. 이때 SmsFetcherImpl객채에 대한 참조가 생성된다.
  3. iframe(A)를 삭제한다. iframe(A)RFH 객체가 삭제되지만 SmsProviderGmsUserConsent에는 아직 RFH raw pointer에 대한 참조가 존재한다.
  4. iframe(B)에 대한 SmsReceiver를 호출한다. 여기서 SmsproviderGmsUserConsent::Retrieve 함수 내부에 WebContents* web_contents = WebContents::FromRenderFrameHost(render_frame_host_); 가 트리거 되면서 UaF가 발생하게 된다.

해커 입장에는 다행스럽게도 raw pointer를 가진 객체 덕분에 FrameServiceBase를 아무 쓸모없게 만들어버렸다.

A more detailed analysis

우리는 이 UaF를 쓸모있게(?) 트리거하기 위해 WebContents::FromRenderFrameHost(render_frame_host)의 동작을 자세하게 분석해 볼 필요가 있다. 먼저 SmsProviderGmsUserConsent::Retrieve 함수를 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
void SmsProviderGmsUserConsent::Retrieve() {
  JNIEnv* env = AttachCurrentThread();

  WebContents* web_contents =
      WebContents::FromRenderFrameHost(render_frame_host_);

  if (!web_contents || !web_contents->GetTopLevelNativeWindow())
    return;

  Java_SmsUserConsentReceiver_listen(
      env, j_sms_receiver_,
      web_contents->GetTopLevelNativeWindow()->GetJavaObject());
}

WebContents::FromRenderFrameHost 함수 인자에 RFH raw pointer을 넣어 호출하고, null pointer인지 검사한 후, 특정 Java code를 수행한다. 다음은 WebContents::FromRenderFrameHost 함수의 동작 과정이다.

1
2
3
4
5
6
7
8
9
10
11
WebContents* WebContents::FromRenderFrameHost(RenderFrameHost* rfh) {
  if (!rfh)
    return nullptr;

  if (!rfh->IsCurrent() && base::FeatureList::IsEnabled(
			       kCheckWebContentsAccessFromNonCurrentFrame)) {
    // TODO(crbug.com/1059903): return nullptr here eventually.
    base::debug::DumpWithoutCrashing();
  }

  return static_cast<RenderFrameHostImpl*>(rfh)->delegate()->GetAsWebContents(); 

총 2곳에서 RFH raw pointer을 이용한 function call을 수행하는데 해당 함수들의 정의는 다음과 같다.

1
2
3
virtual bool IsCurrent() = 0;

virtual WebContents* GetAsWebContents();

두 함수는 모두 virtual로 선언되어 있다. 즉, 컴파일러가 가상 함수들을 처리하기 위해 virtual table을 생성하는데, 우리가 Freed RFH raw pointer악의적인 virtual table를 가리키게 할 수 있다면 임의의 함수를 호출할 수 있게 된다.