[번역] What is a “good” memory corruption vulnerability? (Project Zero)

Part 1 of 4. Written by Chris Evans from Project Zero, Google. (Original Post)

Translated by Brian Pak (Cai)


“좋은” 메모리 코럽션 취약점은 무엇인가?

소프트웨어에는 많은 메모리 코럽션 (변조) 취약점들이 존재하지만, 모두 다 같은 영향력을 가지진 않는다. 주어진 메모리 코럽션 버그의 “유용함”은 어떻게 보면 이 버그를 얼마나 안정적(reliable)으로 익스플로잇 할 수 있느냐로 결정할 수 있는데, 운이 좋은 케이스에서는 버그를 100% 안정적으로 익스플로잇 할 수 있다.

이번 블로그 시리즈에서는, 우리 버그 트랙커에 공개된 최근 버그들을 예제로 활용하여 어떤 종류의 메모리 코럽션 취약점들이 100% 안정적으로 익스플로잇 가능한지에 대해서 알아볼 것이다. 이러한 연구와 자료들을 보안 커뮤니티에 제공함으로써, 관련 공격들에 대한 방어를 좀 더 수월하게 하려한다. 안정적인 익스플로잇을 위해 사용된 툴들과 기술들을 공부 및 연구함으로 새로운 방어기법 뿐만 아니라 현존하는 기법들을 더 보완하는데에도 유용하게 사용될 수 있다. 예를 들어, 아래에서는 타입 컨퓨젼 (type confusion) 취약점들이 왜 엄청 위험할 수 있는지에 대해서 설명하고, 이를 막기 위한 컴파일러단의 보호 기법을 고안해본다.

 

“안정적”이다라는게 무슨 뜻이지?
처음에는 좀 바보같은 질문으로 들릴지 모르겠지만, 사실 “안정적”이다라는데는 여러가지 의미가 있을 수 있다. 여기서 지금 ‘이게 올바른 정의이다’라는 것을 말하고자 하는건 아니고, “안정성”에 대해서 이야기할 때 고려해봐야하는 질문들을 몇 가지 나열해본다:

  • 익스플로잇이 항상 크래쉬를 유발하는가? – 이게 아마 공격자 입장에서 봤을때는 최악의 결과일거다: 크래쉬는 발견되기 쉽기 때문이다.
  • 익스플로잇이 실패시 깔끔하게 처리되는가? – 우린 이걸 “aborting” 한다고 부르는데, 익스플로잇은 실패했지만 크래쉬나 발견될만한 흔적을 남기지 않고 깨끗하게 종료되는것을 뜻한다.
  • 익스플로잇이 각기 다른 패치 상태에서도 잘 작동하는가?
  • 익스플로잇이 EMET, Grsecurity, 또는 안티바이러스 제품들과 같은 추가적인 보안/보호 소프트웨어의 존재 아래서도 작동하는가?
  • 익스플로잇이 특이한 환경을 맞닥뜨렸을때 어떻게 작동하는가? 성공할건가, 크래쉬를 낼건가, 아니면 abort할 것인가?
  • 익스플로잇이 크로스 플랫폼, 그리고 크로스 버전인가?
  • 익스플로잇이 꽤나 안정적인 “실행의 연속 (continuation of execution)” 속성을 가지고 있는가? 예를 들어, post-exploitation을 진행하는데 지장이 없고 다른 눈에 띄는 현상들은 없는가?

이런 다양하고 복잡한 면들을 봤을때, 이 글에서 “100% 안정적이다”라고 하는게 무슨 뜻인지 정의를 하는게 좋겠다. 여기서 “100% 안정적인” 익스플로잇 이라는것은:

  1. 결정적(deterministic)이고 완전히 설명가능 한 스텝들로, 어떤 특정 버전 및 환경에 대해서는 익스플로잇의 성공이 보장되고;
  2. 성공하지 못할 경우, 최소한 위에서 언급된 안정성을 위협하는 요소들을 발견하고 크래쉬가 아닌 abort 상태로 만들 수 있는 컨트롤을 허락하는 익스플로잇을 말한다.

 

버그 종류: 스택 코럽션
스택 쿠키나 스택 변수 순서 재배치와 같은 최신 컴파일러 보안 기법들의 존재에도 불구하고, 익스플로잇 관점에서 봤을때 흥미로운 스택 코럽션 버그들을 구경하곤 한다. 예를 들어, bug 291에선 오픈소스 JPEG XR 이미지 디코더에 존재했던 꽤나 재밌는 스택 코럽션에 대해서 설명한다. 여기서는 스택 배열의 인덱싱 오류가 스택쿠키를 “건너뛰어” 데이터를 쓸 수 있게 해주는 덕분에 스택 보호 기법들을 우회할 수 있었다. 공개된 PoC는 안정적으로, 심지어 결정적으로(deterministically), ebp의 값을 0xffffffef로 변조 한 뒤 크래쉬를 낸다. 이러한 점은 이 버그를 어느정도 안정적으로 익스플로잇 할 수 있다는걸 뜻한다.

스택 코럽션 버그들로는 대게 100% 안정적인 익스플로잇으로 만들 수 있는데, 이는 취약점을 트리거하는 시점에 스택 메모리가 보통 일관적이기 때문이다. 아래 샘플 코드는 스택 코럽션을 통해서 간단히 계산기를 띄우는 것을 보여준다. 만약 첫번째에 제대로 작동했다면, 또 다시 시도해도 작동할 것이다!

 

버그 종류: 힙 블럭간 오버플로우 또는 코럽션
아마 우리가 가장 많이 보는 취약점 부류일거다. 힙에 할당된 메모리 영역을 넘어서 데이터를 쓰게 되는 경우에는 대부분 익스플로잇 가능하다. 하지만, 그러한 취약점이 100% 안정적인 익스플로잇이 되는 경우는 드물다. 예전에 한번 가능한 적이 있긴 했는데, 바로 off-by-one heap overflow in glibc 이 녀석이다. 사실 이건 극히 드문 케이스인데, 왜냐하면 공격 대상이 익스플로잇 시점에 힙 메모리 영역 상태가 항상 같았던 커맨드라인 바이너리였기 때문이다. 훨씬 더 흔한 경우는 공격자가 전혀 그 상태를 모르는 힙을 공격하는 시나리오다 — 원격 서비스라던지, 웹 브라우져의 렌더러 혹은 커널 같은 맥락에서 말이다.

전혀 알지 못하는 힙 상태에서, 익스플로잇 과정에 도움이 되도록 힙 조각들을 (예측 가능하도록) 라인업 시키는게 목적인 “heap grooming” 이라는 기법이 있긴하다. Project Zero 블로그를 뒤져보면 Safari 익스플로잇이라던가, Flash regex 익스플로잇, 혹은 Flash 4GB-out-of-bounds 익스플로잇과 같은 훌륭한 예제들을 찾을 수 있다.  Heap grooming을 통해서 꽤 안정적인 익스플로잇을 만들어낼 수는 있지만, 이 기술 자체에 보통 확률적인 요소가 여전히 존재하기 때문에 100% 안정적인 익스플로잇이라고 하기엔 부족한 감이 있다.

아래 코드 샘플이 결정성 (determinism)과 비결정성 (non-determinism)을 보여주는 예제다:

주어진 머신위에서 특정한 시드 (seed)값을 던져주면 힙은 보통 같은 상태(state)로 정렬되기 마련인데 이건 커맨드라인 바이너리들은 매번 새로운 힙으로 시작하기 때문이다. (사실 영향을 주는 모든 가능한 변수들을 고려한것은 아니기 때문에, 이러한 상태를 결정적이다고 말하는건 그만하도록 하겠다. 그리고 같은 리눅스 운영체제를 설치한다고 해도 설치된 라이브러리에 따라서 동적링커의 malloc 패턴이 달라지거나 할 수 있다는 점도 염두해두자.) 어쨋든, 위의 코드를 돌리면 어떤 힙 레이아웃에서는 계산기를 띄울 것이고, 어떤 힙 레이아웃에서는 아무일도 일어나지 않을것이다.

 

버그 종류: use-after-free
Use-after-free 취약점은 아주 안정적인 익스플로잇으로 연결될 수 있다. 특히, “free”와 “use”가 붙어있거나 공격자가 스크립팅 언어를 통해서 free를 트리거할 수 있을 경우엔 더욱 말이다. 익스플로잇의 안정성은 대부분 요즘의 힙 할당자(heap allocator)들이 구현되어있는 방식 때문이다: 만약 사이즈 X의 오브젝트가 free되었다면, 보통 이 영역의 메모리가 다음에 사이즈 X만큼 할당할 때 사용된다. 이러한 방식은 “cache hot”한 메모리 영역 사용을 극대화 해준다. 하지만, 엄청 결정적이게 만들기도 하는 요소이기도 하다. 좋은 Use-after-free 익스플로잇 공부거리를 찾는다면 2013년에 공개된 이 Pinkie Pie 익스플로잇을 보면 될듯 싶다. 해당 익스플로잇에 엄청 비결정적인 부분들이 많긴 하지만, step 2를 보면 free된 오브젝트가 어떻게 유용하게, 그리고 deterministic하게 사용되었는지 보여준다.

하지만, use-after-free 버그들이 100% 안정성을 가진 익스플로잇을 위한 완벽한 기반은 아니다. 일반적인 문제점들을 짚어보면:

  • Threading (쓰레딩). 힙의 구현체에 따라서, 때론 두 번째 쓰레드가 익스플로잇이 진행중인 쓰레드에서 사용되야할 free된 메모리를 먼저 집어갈 수도 있다.
  • Heap corner-cases (힙 예외들). 힙의 구현체에 따라서, 때론 free가 실행되면서 다른 중요 내부 구조 등이 변경/업데이트 될 수 있다.  흔치 않은 케이스지만, 익스플로잇 시점에서 힙의 상태를 정확히 알 수 없기 때문에 장담하기 힘들다.
  • 오브젝트 사이즈 변동의 영향. 사실 이건 특정 소프트웨어 버젼에 대해서는 익스플로잇의 안정성에 영향을 주지 않지만, 패치가 나와서 중요한 오브젝트의 크기를 크거나 작게 만들어 버림으로써 익스플로잇이 더이상 작동하지 않을 수도 있다.

아래의 간단한 코드 샘플은 use-after-free 버그들이 얼마나 영향력이 있을 수 있는지 보여준다:

 

버그 종류: 힙 블럭 내 오버플로우 및 상대적 overwrite
힙 블럭내 오버플로우나 상대적인 오버라잇은 매우 강력한 익스플로잇 기반을 제공한다. 이번에는 샘플 코드부터 보자:

이 같은 버그는 무지 강력한데, 왜냐하면 메모리 코럽션이 힙 블럭 경계를 넘지 않기 때문이다. 그러므로, 알 수 없는 힙 상태 때문에 생겼던 불확실성이나 비결정성과 같은 문제가 사라져버린다. 힙의 상태가 어떻든지 상관없이 같은 데이터를 주면 항상 같은 코럽션이 일어난다. 이런 종류의 버그들은 100% 안정성의 익스플로잇으로 진전될 가능성이 매우 크다. Bug 251은 힙 블럭 안에서 일어난 버퍼 오버플로우의 리얼월드 예제이다. Bug 265는 흔치 않은 케이스긴 하지만 인덱싱 에러를 통해 out-of-bounds write이 일어나지만 하나의 힙 블럭안에서만 제한되는 흥미로운 예제라고 할 수 있다. 트리거하는 방식 또한 재밌다: 가상 펜으로 가상 화면에 글자들을 쓰는 프로토콜이었는데, 프로토콜 메세지를 통해 가상 펜의 위치를 화면 밖의 위치로 설정했다! 제공된 PoC로 매번 같은 메모리 주소 (0x2000000000)를 free하면서 크래쉬를 낼 수 있었다.

 

버그 종류: 타입 컨퓨젼 (type confusion)
타입 컨퓨젼 버그 역시 100% 안정성을 가진 익스플로잇을 만들 수 있는 기반을 제공할 수 있는 아주 강력한 녀석이다. 타입 컨퓨젼 버그를 트리거되는 상황을 보면, 코드가  A 타입(API 타입)이라고 믿고 있는 어떤 오브젝트에 대한 레퍼런스를 가지고 있지만, 코드가 헷갈린거고 사실은 해당 오브젝트는 B 타입(in-memory 타입) 인것이다. A 타입과 B 타입의 메모리에서의 구조가 어떠냐에 따라서 매우 이상하지만 대게 완전히 결정적인 사이드 이펙트가 일어날 수 있다. 코드 샘플을 봐 보자:

코드에서 보다시피, 전반적으로 계산을 하지 말도록 시도하고 있지만, 타입 컨퓨젼 때문에 CalculatorDecider::UWannaRun() 함수가 실제로는 (null이 아닌) 포인터가 있는 메모리 값에 대해서 boolean 체크를 하게 된다. 그래서 결국엔 항상 계산을 하게 된다. (라고 말했지만 정말일까? 필자의 머신에서는 안정적으로 항상 계산기를 띄웠지만, 여기엔 비결정성의 요소가 숨어있다. 이게 무엇인지는 생각하는건 독자들의 몫으로 남기도록 하겠다.)

실제 프로그램에서 발생한 타입 컨퓨젼 버그와 익스플로잇의 좋은 예제/설명은 2013년 Pwn2Own에 구글 크롬을 상대로 참가했던 MWR Infosecurity의 블로그 포스트에서 볼 수 있다. 흥미롭게도, 버그가 쉽게 100% 안정적인 익스플로잇으로 연결되지 않은 케이스다. 이 경우에는 훨씬 큰 API 타입에 비해서 작은 in-memory 타입의 컨퓨젼이 있었다. API 타입에 있는 필드들에 대한 오프셋이 in-memory 타입의 사이즈보다 큰 바람에,  필드들을 참조할 때 거의 모두 힙 경계를 넘어서 액세스하게 된 것이다. 위에서 본 것처럼, 힙 경계를 넘는건 안정성을 위해서는 “ㄴㄴ”다. 다음 그림이 타입 컨퓨젼에서의 멤버 사용의 큰 두가지 가능성을 보여준다. 왼쪽에 있는 오브젝트는 컴파일러가 (잘못된 캐스팅 때문에) 생성한 코드에서 쓰이는 오브젝트를 나타낸다. 하지만 런타임에서는 가르켜진 오브젝트 메모리가 더 작게되는데 메모리에서의 타입이 다르기 때문이다. 런타임 오브젝트의 바운드 안에 있는 참조들은 예측한대로 작동할 것이다 — ASLR을 무력화하는데 쓰일 수 있는 GetSize() 메소드를 통한 infoleak 처럼 말이다. 그에 비해, 힙 경계를 넘어서 참조하는 경우는 out-of-bounds 액세스이고, 그렇기 때문에 결정적으로 작동하지 않을 확률이 크다 — SetFlags() 메소드를 통한 메모리 변조 처럼 말이다.

 

케이스 스터디: ShaderParameter 힙 코럽션, 올드스쿨 방법으로.
이번 포스팅은 버그의 안정성 (reliability)에 대해서 설명했는데, 이제와서 불안정한(unreliable) 익스플로잇을 소개하려고 한다는게 이상해 보일 수 있다. 하지만 이건 간단한데서 부터 시작해서 똑같은 버그들을 가지고 더욱 더 안정적인 것을 만들어내는 과정을 위해서이다. 일단 불안정한 익스플로잇으로 시작해서 훌륭한 안정성을 가질때까지 보완해보도록 하겠다.

그래서 이번 포스팅은 최근에 패치된 Adobe Flash를 공격하는걸로 끝을 내볼까 한다. Bug 324인데, ShaderParameter ActionScript 클래스에 관련해서 out-of-bounds write이 가능한 녀석이다. 공격자는 shader 프로그램에서 상대적인 비정상적인 인덱스에 원하는 32-bit 값을 쓸 수 있고, 큰 인덱스를 사용하게 되면 힙 블럭의 끝 부분에 out-of-bounds write을 할 수 있게 된다.

이러한 out-of-bounds write 도구가 있으면, 요즘엔 Adobe Flash를 공격하는 꽤나 전형적인 방식이 있다: heap grooming을 이용해서 Vector.<uint> 버퍼 오브젝트를 덮어쓰는 오브젝트의 뒤에 오도록 유도하는것이다. 덮어쓰면서 Vector의 길이 (length) 필드를 수정하게 되고, 결과적으로 Vector 오브젝트 이후의 프로세스 메모리를 맘대로 읽거나 쓸 수 있게 된다.

어느정도 주석처리 된 Linux x64 익스플로잇이 여기에 첨부되어있다. 딱히 안정성을 높이기 위한 노력은 안했다; 아마 훨씬 더 안정적으로 만들 수 있기는 할테지만 말이다. 힙 블럭 경계를 넘어서 메모리 변조를 시키기 때문에 이 익스플로잇을 가지고는 위에 언급한대로 100% 안정성을 얻기는 어렵다.

Adobe가 이 버그를 고치기 위한 패치를 릴리즈한 이후에 (하지만 우리가 이 버그에 대한 자세한 정보는 내놓기 훨씬 이전에), 제로데이는 아니지만 exploit pack들에서 이미 해당 버그의 익스플로잇들이 보이기 시작했다. 뭐 공격자들이 바이너리 비교 (diffing)에 있어서 빠르거나, MAPP 정보통이 있거나, 이미 해당 취약점을 타겟팅 공격에 쓰고 있었을 수도 있다. 하지만 이 ‘사건’이 일어난 덕분에 같은 버그에 대한 다른 익스플로잇을 분석할 핑계를 줬는데 — @HaifeiLi에 의하면 여기서 사용된 것들도 Vector 공격 기법을 사용했다더라.

일단 이번 포스팅은 해당 버그에 대한 얘기가 아직 끝나지 않았다는 약속과 함께 여기서 마치는걸로 하겠다. 이번 시리즈의 다음 포스팅에서는 “이 버그를 좀 더 안정적으로 쓸 수 있을까?”라는 질문을 던질 것이다. 그리고, 예상할 수 있겠지만, 대답은 yes가 될 것이다.

 

3 Responses

  1. ROCKSTAR says:

    간단하네요.

  2. dokydoky says:

    좋은 번역 감사합니다.

  3. x90c says:

    타입 분류 취약점에 대한 개념이 잘 나와 있네요. 실제 코드 분석 부분은 조금 어려운거 같은데… 타입 A와 타입 B가 혼동해서 발생한다는 상위 개념은 이해가 되는거 같네요. 좀 자세히 살펴봐야 할 것 같습니다.

Leave a Reply

Your email address will not be published. Required fields are marked *