ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 아이폰 메일 취약점 분석: iOS Zero Click Mail
    Analysis 2020. 4. 26. 18:16

    애플 ios의 기본 탑재 앱 중 하나인 mail 앱에서 보안 취약점이 발견되었다. 이 취약점은 ZecOps에서 발표하였는데, 자세한 정보는 여기에서 찾을 수 있다. 다른 취약점들보다 여파가 커질 수 있는 문제이기에 떠들썩하고, 적반하장이긴 하지만 안드로이드 진영에서는 이 때다 싶어서 공격을 퍼붓기도 한다. 어떤 os이거나 취약점이 발견되고 수정되는 것은 일상적인 일이지만 개인적으로도 이 취약점의 경우 조금 심각해 보이기는 한다.


    높은 점유율

    가장 먼저 주목할 점은 애플의 mail 앱의 점유율이 높다는 점이다. (참조) 조사마다 차이는 있겠지만 구글의 gmail에 필적하는 30%에 육박하는 점유율을 가지고 있다. 실상 아이폰 사용자의 대다수가 사용하고 있다는 것이다. 아직 밝혀지지는 않았지만 이 취약점을 사용한 악성 코드가 있었다면 사실상 대부분의 아이폰 사용자를 공격 대상으로 삼을 수 있었다는 점에서 무서운 일이 아닐 수 없다.

     

    Zero-Click Attack

    다음으로 무서운 점은 이번 취약점은 제로 클릭으로 작동할 수 있다는 점이다. 이메일과 관련된 대부분의 취약점은 사용자가 이메일을 여는 행동을 기점으로 트리거가 된다. 첨부파일에 악성코드가 있어서 첨부파일을 열면 동작이 시작된다던지, 메일을 여는 순간 메일 내용에 숨겨진 코드가 실행되던지 하는 식이다.

     

    하지만 이번 취약점은 사용자가 인지하지 못한 상태에서 백그라운드에서 동작할 수 있다. 백그라운드 대몬 (ios13) 또는 메일 클라이언트 (ios12)가 메일 서버로부터 메일을 내려받아 메모리에 올리는 순간 악성 코드가 작동된다. ios 구조상 확률은 희박하지만 만약 실행된 악성코드가 메일함의 메일을 지워버린다면 사용자는 전혀 눈치채지 못할 것이다.

     

    오랜 기간의 방치

    이 취약점은 ios (2012년 9월)부터 존재했다고 한다. 아이폰 6가 출시된 이래 쭉 존재해 왔던 취약점이라는 것이다. 제로데이가 아니라 9년차에 접어든 것이다.

     

    9년의 세월 동안 이 취약점을 누군가 알고 있었고 이를 활용한 악성 코드를 가지고 있었을 확률이 얼마나 클까. 아마 모르는 사이에 여러 곳에서 사용되고 있지 않았을까 하는 의심을 감출 수 없다.

     

     

    감염 증상

    연구자들에 따르면 이번 취약점을 이용한 POC가 구동될 때 메일 클라이언트가 잠깐 느려지거나 앱이 크래시 되는 정도의 영향만을 준다고 한다. 사용자가 공격을 받더라도 알아채기 어려울 수밖에 없다.

    이 취약점을 통한 공격이 수행되다가 실패할 경우 ios 종류에 따라 다음 두 가지 반응이 나타난다.

     

    ios12 메일 앱이 잠시 크래쉬됨

     

    ios13 “This message has no content.”이라는 메시지가 남는다.

     

     

    취약점 원리

    이 취약점이 일어나는 라이브러리는 다음과 같다.

    /System/Library/PrivateFrameworks/MIME.framework/MIME

     

    그리고 이 취약점이 존재하는 함수는 다음과 같다.

    [MFMutableData appendBytes:length:]

     

    MFMutableData의 appedBytes 함수의 구현 안에 취약점이 나타난다. 여기에서 발견된 취약점은 두 가지인데, 하나는 OOB(Out of Bound) Write이고 다른 하나는 Heap Overflow이다. 둘 모두 아주 전통적인 형태로 결국 문제의 원인은 리턴값 체크를 안해서이다.

     

    둘 다 문제지만, 그 중에서 위에 설명한대로 메일앱에 영향을 줄 수 있는 것은 remote에서 건드릴 수 있는 Heap Overflow 취약점이다.

     

     

    취약점1: OOB Write

    먼저 OOB(Out of Bound) Write가 일어나는 경우에 대해 설명하면, 내부적으로 데이터 크기가 변경될 경우 메모리 맵드 파일의 크기를 조정하고 조정된 영역에 memmove()로 데이터를 옮기고 있다. 그런데, 메모리 맵드 파일의 크기를 조정하는 용도로 ftruncate()를 호출한 이후 이 함수의 리턴값을 체크하지 않는다. 애플 문서에 보면 ftruncate()는 다음과 같다.

    ftruncate() and truncate() cause the file named by path, or referenced by fildes, to be truncated 
     (or extended) to length bytes in size. If the file size exceeds length, any extra data is discarded.  
    If the file size is smaller than length, the file is extended and filled with zeros to the indicated  
    length.  The ftruncate() form requires the file to be open for writing. 
    RETURN VALUES 
        A value of 0 is returned if the call succeeds.  If the call fails a -1 is returned, and the global  
        variable errno specifies the error.

     

    ftrunctae()가 실패할 경우 -1이 리턴되고 errno가 셋팅된다. 즉 이 함수가 실패할 수 있다는 말인데, MFMutableData의 appedBytes 함수는 이를 무시하고 진행된다. 그런 경우 다음 단계의 memmove()가 작동하고 OOB가 발생한다.


    물론 이 상황을 발생시키기 위해서는 다른 취약점을 이용해 appendBytes가 호출하는 setLegth함수의 flush 플래그를 변경해 주어야 한다. 확률이 적은 일이라 생각할 수도 있겠지만 요즘 공격 코드들의 복잡도를 보면 마냥 안심할 부분은 아니라고 생각된다.

     

     

    취약점2: Heap Overflow

    Heap Overflow도 OOB Write와 같은 위치에서 발생하는데, 문제가 되는 것은 이 취약점이 remote에서 사용이 가능하다는 점이다. 메일앱이 메일을 다운로드하는 과정에서 트리거되기 때문이다.

     

    이 취약점의 콜스택은 다음과 같다. 개인적으로 Objective C스타일이 별로 익숙하지 않아 밑에서부터는 그냥 C++ 형태로 표현하겠다. 사실 대부분의 코드가 바이너리 리버싱을 통해 나온 수도 코드이므로 원본 코드가 Objective C인지 C++인지는 큰 의미가 없다.

     

    MFDAMessageContentConsumer::consumeData() 
    -- MFMutableData::appendData() 
       -- MFMutableData::appendBytes() 
          -- MFMutableData::_mapMutableData()

     

    consumeData함수는 이메일을 raw MIME 포맷으로 내려받을 때 불려진다. IMAP의 경우는 MFConnection::readLineIntoData()로 불려지기는 하지만 이후에는 같은 로직으로 취약하다.

    consumeData함수는 MFMutableData를 내부적으로 사용하고 threshold를 0x200000 바이트로 설정하는데, 이 크기가 넘어갈 경우 MFMutableData::_mapMutableData()가 호출된다.

     

    _mapMutableData함수는 mmap을 시도하는데 이 시도가 실패할 경우 MFMutableData__mapMutableData___block_invoke()를 호출한다. 취약점은 여기에 있다.

    다음은 연구자들이 리버싱으로 찾은 것으로 보이는 _MapMutableData의 수도 코드이다.

    MFMutableData::_mapMutableData() 
    { 
      //... 
      result = mmap(0LL, v8, v9, 2, v6, 0LL);  // MMAP을 시도한다. 
      if (result == -1){ 
          //... 
          // MMAP이 실패할 경우 호출된다. 
          result = (void *)MFMutableData__mapMutableData___block_invoke(&v21);
      } 
      //... 
    } 

    상기에 설명한 것처럼 MMAP이 실패하면 result를 MFMutableData__mapMutableData___block_invoke()의 결과로 대체하고 있다.

    다음은 연구자들이 리버싱으로 찾은걸로 보이는 MFMutableData__mapMutableData___block_invoke()의 수도 코드이다.

    void MFMutableData__mapMutableData___block_invoke(__int64 data) 
    { 
      __int64 result; // x0 
      
      data->vm = 0;     
      data->length = 0; 
      data->capacity = 8; // capacity를 8로 세팅한다. 
      result = calloc(data->capacity, 1); // 8 크기의 새로운 메모리를 할당한다. 
      data->bytes = result; // MMAP 실패로 -1로 되어있을 result를 8 크기의 새 메모리로 변경한다. 
      return result; 
    } 

    MFMutableData__mapMutableData___block_invoke()의 결과는 8 크기의 새로운 메모리이고 이것이 result로 사용된다.

    다음은 연구자들이 리버싱으로 찾은 것 처럼 보이는 appendBytes()의 수도 코드이다.

    MFMutableData::appendBytes()  
    { 
      int length = [self length]; 
      //... 
      bytes = self->bytes; 
      if(!bytes){ 
         // 여기에서 _MapMutableData가 불려지고 result가 bytes에 들어간다.
         bytes = [self _mapMutableData]; 
      } 
      copy_dst = bytes + length;  // bytes + length가 copy_dst에 들어간다. 
      //... 
      // copy_dst에 append_length 길이만큼 append_bytes를 복사한다.
      platform_memmove(copy_dst, append_bytes, append_length); 
    } 


    이제 한 눈에 보이는 것처럼 mmap이 실패할 경우, copy_dst는 length가 0일 경우에도 8바이트밖에 되지 않는 공간이다. 그것도 calloc()으로 할당한 힙공간이다. 여기에 8바이트 이상 되는 데이터를 쓰는 순간 OOB가 발생한다.

    그럼 mmap을 실패시키려면 어떻게 해야할까. 가장 무식한 방법은 큰 사이즈의 이메일을 보내는 것이다. 실제 연구자들이 제공한 POC도 큰 사이즈의 메일을 보내는 방법으로 취약점을 구동시키는 형태이다. 큰 사이즈가 유일한 방법이라면 이는 실제로 일어나기는 힘든 상황이긴 하다.


    그러나 연구자들은 multi-part, RTF를 활용해서 일반적인 사이즈의 이메일로도 취약점 구동이 가능하다고 반복해서 강조한다. 아무래도 악용을 우려해 이 부분을 자세히 설명하지는 않은 것 같지만, 실제로 일어날 가능성이 충분하다고 판단하는 것으로 보인다.

     

     

    PATCH

    애플은 위 두가지 취약점에 대한 패치를 iOS 13.4.5 Beta에 포함시켰다. 결국 리턴값 체크가 안된 것이 원인이기에, 패치의 내용도 리턴값 체크에 지나지 않는다.

     

     

    Learning

    애플은 트리거링이 어렵기 때문에 해당 취약점들로 인한 피해는 없다고 말하고 있다. 하지만 긴 시간 동안 뚫려있었던 구멍이어서 과연 그렇게 순진하게 믿을 수 있는지는 잘 모르겠다. 개인적으로 OOB Write는 몰라도 Heap Overflow는 구동시킬 방법이 참 많아 보인다.

    한 가지 생각해 볼 점은 패치 내용이 말해주듯이 이 취약점들은 애초에 아주 쉽게 막을 수 있는 취약점들이었다는 것이다. 개인적으로 아무리 시스템콜이라고 해도 리턴값을 체크하지 않는 것은 프로페셔널로서 기본이 안된 것이라는게 상식이던 시절부터 개발을 시작했다. 그래서 이런 코드를 만드는 후배들을 보면 내심 답답한 경우가 많다. 물론 최근에는 스크립트 랭귀지가 대세이고 익셉션들은 내가 처리할 일이 아닌 것처럼 느껴지는 시대이지만, 네이티브 프로그래밍에서는 절대로 조심해야 할 부분이다.

    천하의 애플도 이런 문제적인 코드를 9년 가까이 방치할 만큼 코드 리뷰를 할 여력이 없다는게 웃프다. 아마 늘어나는 단말 종류와 단축되는 일정 때문이리라 이해를 해보려고 하지만 씁쓸하다.

    나는 아직 작업할 때 습관적으로 new가 실패할 경우의 코드를 치고 있다. 아마 손에서 일을 놓는 그 날까지 그럴 것 같다.


    Fin.

     

    반응형

    댓글

Calvin's Memo