LINE Corporation이 2023년 10월 1일부로 LY Corporation이 되었습니다. LY Corporation의 새로운 기술 블로그를 소개합니다. LY Corporation Tech Blog

Blog


Another one bites the apple!

안녕하세요. LINE에서 보안 업무를 담당하고 있는 장준호입니다. 저는 LINE에서 제공하는 서비스들을 해킹하고, 어떻게 더 안전하게 만들 수 있을 지 연구합니다. 또한 취미로 다른 회사의 제품에서 보안 취약점을 찾고 리포팅하여 더 안전한 세상을 만드는 데에 기여하고 있습니다. 해커들 사이에서는 이렇게 보안 취약점을 찾는 행위를 버그 헌팅(bug hunting)이라고 표현합니다. 해커들은 버그 바운티를 통해 상금을 획득하거나 해커로서 명성을 얻기 위해, 혹은 순수하게 재밌어서 해킹을 하곤 합니다. 이번 포스팅에서는 많은 해커들이 버그 헌팅 대상으로 삼는 Apple 제품에서 버그 헌팅을 수행한 과정에 대해 공유하고자 합니다. 

버그 헌팅 대상 선정

Apple에서는 MacBook, iMac, Mac Pro, iPhone, iPad, Apple Watch 등 다양한 제품을 판매하고 있는데요. 그 제품들을 크게 데스크톱과 모바일 장치로 나눌 수 있습니다. 데스크탑에서는 OS(Operating System)로 macOS를 사용하고 모바일 장치에서는 iOS를 사용합니다.

macOS 대 iOS 

macOS와 iOS는 구조적으로 매우 유사합니다. 같은 종류의 데몬(daemon)이 다수 실행되고 있고 사용자 프로세스 간 통신하는 프로토콜도 비슷합니다. 그리고 OS 커널(kernel)의 구조도 흡사합니다. 다만 데스크탑과 모바일 장치는 내부 구성에 차이가 있고 그에 따라 장치 드라이버와 모바일 장치의 효율화를 위한 캐시(cache) 시스템에도 차이가 있습니다. 그럼 해커의 시각에서는 둘 중 어떤 OS가 버그 헌팅하기 쉬울까요? iOS에서는 사용자 레벨 디버깅뿐만 아니라 커널 레벨 디버깅 또한 매우 어렵습니다. 최신 iOS에서 커널 디버깅을 하는 것은 취약점 없이는 불가능하기 때문에 마치 Black-box testing을 하는 것과 같습니다. 반면, macOS에서는 VM(Virtual Machine)을 통하면 커널 디버깅이 용이하고, fuzzer(자동 취약점 탐지 도구)를 개발하고 실행하는 것도 용이합니다. 그래서 일반적으로 macOS에서 버그 헌팅해서 취약점을 찾고, 이를 iOS에 적용하는 경우가 많습니다(물론 iOS에서만 발생할 수 있는 취약점이 있기 때문에 모바일 장치의 특성을 이용한 부분들만 노리는 해커들도 있습니다). 이런 특성을 고려하여 이번 연구에서는 macOS를 대상으로 버그 헌팅을 진행하였습니다.

해킹 목표 설정

macOS 내에도 다양한 해킹 대상이 있습니다. 해커들이 주로 선택하는 대상은 웹 브라우저인 Safari나 데몬, 커널 등입니다. Safari 대상으로는 원격 코드 실행(Remote Code Execution)을 목표로 해킹을 시도하고, 데몬이나 커널을 대상으로는 로컬 권한 상승(Local Privilege Escalation)을 목표로 해킹을 시도합니다. Safari를 통해 로컬 권한 상승까지 해킹에 성공하면 해당 공격 코드를 500,000 달러(USD) 이상에 판매할 수 있을 정도로 가치가 높습니다. 그래서 많은 해커들이 호시탐탐 취약점을 찾으려고 기회를 노리고 있습니다(참고). 이번 연구에서는 로컬 권한 상승이 가능한 커널 취약점을 찾는 것을 목표로 진행했습니다.

macOS 커널 살펴보기

macOS와 iOS 커널은 XNU(X is Not Unix)라는 이름의 커널을 사용하고 있습니다. XNU 커널은 크게 두 가지 오픈소스 OS를 수정하여 만들었습니다. 우선 BSD(Berkeley Software Distribution) 시스템을 이용해 system call과 파일 시스템을 구성하였고, 두 번째로 Mach(카네기 멜런 대학교 Mach 커널) 시스템을 통해 사용자 간 통신(IPC: Inter-Process Communication)을 구현하였습니다. 다른 Unix OS와의 차이점은 Mach 시스템을 사용한 부분입니다.

XNU 커널

Mach 커널이란?

Mach 커널에서는 task, thread, port와 같이 시스템을 이루는 기본적인(primitive) 기능을 정의하고 제공합니다. 이번 글에서는 커널 버그 헌팅을 하기 위해 사용자 레벨 프로세스가 어떻게 커널과 통신하는가에 집중해보겠습니다. 사용자 공간에서 실행되는 프로세스들은 'Mach messages'라는 프로토콜을 이용해 커널과 통신합니다. 이 Mach messages는 RPC(Remote Procedure Call)의 일종으로 아래서 설명드릴 MiG라는 프로그램으로 생성합니다.

사용자 공간과 커널 공간 간 통신

MiG(Mach Interface Generator)란?

MiG는 RPC 코드를 생성하는 도구입니다. MiG를 통해 클라이언트와 서버 코드를 생성할 수 있습니다. 커널과 통신할 때 일반적으로 사용자가 클라이언트 역할을 하고 커널이 서버 역할을 합니다. MiG는 macOS에서 기본으로 제공하는 프로그램이라서 터미널 환경에서 'man mig'를 실행하면 매뉴얼을 살펴볼 수 있습니다.

MiG 프로그램 매뉴얼

MiG 프로그램에 definition 파일을 입력하면 클라이언트와 서버 코드를 파일로 출력합니다. 예를 들어 host_kernel_version이라는 함수를 정의한 파일을 프로그램에 입력하면, 아래 그림처럼 mach_hostUser.c에서 확인할 수 있는 클라이언트 RPC 코드와 mach_hostServer.c에서 확인할 수 있는 서버 RPC 코드를 생성합니다.

MiG가 생성한 host_kernel_version 함수

커널 버전 출력 프로그램 만들기

다음으로 커널과의 통신을 이해하기 위해 현재 커널 시스템의 버전을 출력하는 프로그램을 만들어 보겠습니다. 이는 host_kernel_version 함수를 이용하여 쉽게 만들 수 있습니다. 아래와 같이 코딩하고 컴파일한 후 실행하면 macOS의 커널 버전을 알 수 있습니다.

커널 버전 출력 프로그램

host_kernel_version 함수의 동작을 더 깊숙이 들여다보면, 커널과 Mach 메시지를 주고받기 위해 클라이언트, 서버 코드가 실행되고 있습니다. host_kernel_version 함수를 호출하면 사용자 라이브러리에서 Mach 메시지를 프로토콜에 맞게 구성하여 커널 핸들러에게 메시지를 보냅니다. 커널 핸들러는 사용자 라이브러리로부터 받은 Mach 메시지를 해석하고 커널 버전을 가져오는 함수를 실행합니다. 결론적으로 사용자 프로세스에서 사용하는 다양한 함수들은 이러한 방식으로 Mach 메시지를 이용해 커널과 통신하고 있습니다.

Mach 메시지를 통한 host_kernel_version 함수 실행 과정 예시

Fuzzing, 무작위 데이터를 입력하여 취약점을 탐지하는 기술

Fuzzing은 자동으로 취약점을 탐지하는 기술 중 하나입니다. 대상 프로그램에 무작위 데이터를 입력하여 비정상적인 행동을 하게 하거나, crash가 발생해서 비정상적으로 종료되도록 만들어 취약점을 탐지합니다. Fuzzer는 fuzzing을 수행하는 도구입니다. 이번 연구에서는 커널을 대상으로, 특히 커널의 Mach 통신을 대상으로 fuzzer를 설계하였습니다. 

MiG Fuzzer 설계

사용자 프로세스에서 커널을 호출할 수 있는 함수들을 모아 그중 일부를 무작위로 선택해서 실행합니다. 앞서 설명한 Mach 메시지를 생성하는 클라이언트 코드를 MiG를 통해 구현하여 커널에 Mach 메시지를 전달하도록 구성하였습니다. 아래 그림에서 fuzzing 대상은 빨간색으로 표시한 커널 코드입니다. 커널에 보낼 Mach 메시지를 생성하는 과정에서 인자 값의 유효성을 확인하는 부분을 모두 제거하여 무작위값을 제한 없이 커널에 전달할 수 있도록 만들었습니다.

MiG Fuzzer

예를 들어 MiG Fuzzer에서 무작위로 선택된 함수가 host_info 함수라고 가정하겠습니다. 아래 그림과 같이 함수 테이블인 mig_table 변수에서 _call_host_info 함수를 선택하여 호출합니다.

MiG Fuzzer 함수 테이블에서 함수 하나를 선택

_call_host_info 함수에서는 fuzzing을 수행할 임의의 인자들을 설정하여 host_info 함수를 호출합니다.

MiG Fuzzer의 _call_host_info 함수

host_info 함수에서는 입력받은 인자 값으로 Mach 메시지를 구성하여 커널 핸들러로 전송합니다.

MiG Fuzzer 라이브러리의 host_info 함수

이렇게 임의의 함수를 선택한 뒤 해당 함수의 인자를 임의의 값으로 설정하여 커널 핸들러에 보내는 행위를 지속적으로 수행합니다. 그 후 macOS 커널이 비정상적인 행동을 보이는지 혹은 비정상적으로 종료되는지 탐지하여 취약점을 찾아냅니다.

Fuzzing 수행

다음으로는 fuzzing을 수행하는 환경을 어떻게 구성하는지와 취약점을 어떻게 판별하는지 알아보겠습니다. Fuzzing을 수행하기 위해선 fuzzer 제작뿐 아니라, fuzzing을 수행할 환경까지 구성해야 합니다. 또한 취약점이 잘 유발되었는지 판별하고 crash가 발생했을 때 발생한 원인을 자동으로 분류하는 등의 기능을 하는 다른 프로그램들도 필요합니다. Fuzzing이 자동으로 원활하게 진행될 수 있도록 이러한 프로그램들을 묶어서 'fuzzing 프레임워크'를 구성합니다.

Fuzzing 수행 환경 구성

Fuzzing은 무작위로 데이터를 입력하기 때문에 최대한 많은 대상을 실행할 수 있도록 수행 환경을 구성하면 좋습니다. 저는 MacBook 4대에 macOS VM을 각 2대씩 설치하여 총 8개의 fuzzing 환경을 구성하였습니다.

8 VMs on 4 MacBooks

Fuzzing 프레임워크 구성

각 VM에서 MiG Fuzzer를 실행하고 crash가 발생하면 crash 리포트를 아래 그림처럼 Crash Collector 서버에 보내도록 구성했습니다. Crash Collector 서버에서는 자동으로 crash를 분류하여 추후 crash를 확인할 때 분석이 더 필요한 취약점인지 아닌지 판별하는 것을 도와줍니다.

Fuzzing 프레임워크

Fuzzing 수행 결과

약 2주간 fuzzing 수행 후 아래와 같은 결과를 얻었습니다. 각 VM에서 받은 crash 리포트 내용에서 중요 내용을 추출하여 고정 길이의 MD5 hash를 생성합니다. 이 hash 값을 이용해 중복되는 내용을 제거하여 한눈에 취약성을 파악할 수 있도록 구성했습니다.

Fuzzing 결과

Crash 분석

Fuzzing 결과를 분석하고 취약 가능성이 높은 crash들을 분류하고 나면, 해당 crash를 다시 발생시킬 수 있는지 판단합니다. 어떤 코드가 대상 커널에 crash를 발생시켰는지 확인하기 위해 crash를 재현하여 분석합니다. Fuzzing을 수행할 때 어떤 코드를 실행했는지 기록해 두면, 추후에 crash를 재현하는 게 쉽습니다. 이어서 crash 리포트 분석부터 crash 재현, 그리고 crash 발생 원인 분석 내용을 공유하겠습니다.

Crash 리포트 분석

Crash 리포트를 분석하면 어떤 함수에서 crash가 발생했는지 알 수 있습니다. 아래 그림은 흥미로웠던 crash 리포트 내용 중 일부를 발췌한 것입니다. 커널의 mach_vm_page_range_query 함수에서 vm_map_page_range_info_internal 함수를 호출하고 그 내부에서 bzero 함수를 수행하다가 crash가 발생했다는 걸 알 수 있습니다.

Crash 리포트

Crash 재현

Fuzzing을 수행하면서 기록한 내용을 토대로 crash를 발생시키는 코드를 작성하였습니다. 이렇게 crash 또는 취약점을 발현시키는 가장 간단한 코드를 PoC(Proof of Concept)라고 합니다.

간단한 PoC 코드

이제 이 코드를 실행하기만 하면, 커널에서 crash가 발생하면서 재부팅될 것입니다(테스트해보실 분은 VM에서 실행하는 걸 추천드립니다 :)). 이 코드는 macOS 10.14.4, iOS 12.2 대상으로 crash가 발생합니다.

Crash 발생 원인 분석

PoC에서 입력한 인자 값을 따라가면서 crash가 발생한 원인을 분석하겠습니다. XNU 커널 소스 코드는 일부가 공개되어 있습니다. 해당 코드를 바탕으로 crash가 발생하는 함수 내 코드를 따라가겠습니다.

PoC 코드에서 mach_vm_page_range_query 함수를 호출하였는데, 2번째 인자는 address, 3번째 인자는 size 변수 값입니다. 이 값은 각각 0x10과 0xffffffffffffffff(16개의 f)입니다. 여기서 size 값을 64비트(8바이트) integer로 표현하면 -1 값과 동일합니다. 해당 값이 그대로 커널 내 mach_vm_page_range_query 함수의 인자로 들어가고, 함수 내의 로컬 변수인 start 값은 0end 값은 0x1000으로 계산됩니다. end 변수의 값을 구할 때 address와 size를 더한 값을 mach_vm_round_page 함수에 인자로 넣어 호출하는데요. 이때 integer overflow가 발생합니다(0x10 값과 0xffffffffffffffff 값을 더하면 0x10000000000000009 값이 나오는데, 이 값은 64비트 interger로 표현하는 범위에서 맨 앞의 1 값이 포함되지 않기 때문에, 0x9 값이 됩니다). 이 부분을 체크하지 않은 게 crash가 발생한 원인이라고 볼 수 있습니다.

mach_vm_page_range_query 함수 내부 분석 1

Crash 가치 평가

해당 crash가 취약점으로서 가치가 있는지 판단하기 위해 crash가 발생한 부분까지 커널 소스 코드를 따라가도록 하겠습니다.

커널 소스 코드 추적

앞서 구한 start와 end 변수 값을 이용해서 num_pages 값을 구합니다. 그리고 num_pages 값을 이용해 kalloc 함수를 호출하여 커널 힙(heap) 영역 메모리를 할당합니다. info 변수는 32바이트, local_disp 변수는 4바이트 크기의 메모리가 할당된 포인터 값을 가지고 있습니다.

mach_vm_page_range_query 함수 내부 분석 2

그리고 while 반복문이 실행되면서 vm_map_page_range_info_internal 함수를 호출합니다.

mach_vm_page_range_query 함수 내부 분석 3

그러면 두 번째 while 반복문이 실행되어 vm_map_page_range_info_internal 함수를 호출합니다. 이때 해당 함수 내부를 살펴보겠습니다. 초기 입력한 size 값이 매우 크기 때문에 curr_e_offset 변수 값이 1GB(gigabyte)의 큰 값으로 설정되었습니다. 결과적으로 bzero 함수에서 앞서 32바이트로 할당한 info 변수가 가리키는 힙 메모리를 시작으로 약 8MB 정도의 크기를 0으로 초기화합니다. 

vm_map_page_range_info_internal 함수 내부 분석

평가

로컬 권한 상승을 목표로 해킹할 땐 보통 'Fuzzing → crash 다수 발견 → crash 분류 → 유용해 보이는 crash 분석 → 익스플로잇(exploit) 공격 수행(crash를 일으키지 않으면서 원하는 코드를 실행하도록 작업)'과 같은 순서로 진행하는데요. 공격이 성공하기 전에 커널 crash가 발생하면 재부팅되어 버리기 때문에 커널 crash를 발생시키지 않으면서 공격에 성공해야 합니다. 위 취약점에선 32바이트로 할당한 커널 힙 메모리에 8MB 크기를 덮어쓰기(overwrite)합니다. 커널 힙 메모리에는 다양한 객체의 값과 page를 구성하는 메타 데이터 값들이 존재하는데요. 이런 값들이 변조되면 커널 crash를 유발하지 않고 공격을 이어나가는 게 어렵습니다. 따라서 커널 crash를 발생시키지 않으면서 공격할 수 있는 새로운 기술을 연구하거나 다른 취약점을 찾아야 합니다.

커널 소스 코드 분석 중 발견된 또 다른 취약점 유발 포인트

앞서 설명한 mach_vm_page_range_query 함수와 vm_map_page_range_info_internal 함수 내부를 분석하던 중 원인은 같으나 다른 취약점을 유발하는 포인트를 찾았습니다. address 변수 값을 0xfffffffffffee010처럼 큰 값으로 설정할 경우, bzero 함수를 실행하지 않고 local_disp 변수가 가리키는 커널 힙 메모리에 값을 덮어쓰기합니다. 

mach_vm_page_range_query 함수 내부 분석 4

하지만 이 취약점 또한 4바이트 크기로 할당한 힙 메모리에 1MB 크기의 값으로 덮어쓰기를 수행하기 때문에 앞서 살펴본 취약점과 같은 문제가 있습니다.

힙 버퍼 overflow

취약점 활용 방법 연구

현재 틈틈이 시간을 내어 이 취약점을 활용할 수 있는 방안을 연구하고 있습니다. 한 가지 떠오른 아이디어는 아래 설명드릴 '커널 힙 feng-shui' 기술을 활용하는 것입니다.

커널 힙 feng-shui란?

'힙 feng-shui'는 풍수(風水)라는 말에서 유래된 용어이며, 힙 메모리를 해커가 원하는 모양으로 만들어 공격하는 방법 중 하나입니다. 만약 이 기술을 활용하여 local_disp 변수가 가리키는 힙 메모리 주소부터 1MB 정도를 불필요한 값들로 할당하여 커널 crash가 유발되지 않도록 만든다면 취약점을 충분히 활용할 수 있을 것입니다.

커널 힙 feng-shui

위와 같이 커널 힙 메모리를 구성하고 1MB 이후 위치에 참조 개수(reference count)를 두는 특정 객체를 할당할 수 있다면, 1MB를 덮어쓰기한 후 참조 개수값을 0으로 만들어 'Use After Free' 취약점으로 변형할 수 있습니다. Use after free 취약점으로 변형하면 많은 해커들이 해당 취약점을 활용한 방법을 이용해 커널 권한을 획득하여 로컬 권한을 상승시킬 수 있습니다.

커널 힙 feng-shui를 이용해 Use After Free 취약점으로 변형

마치며

해당 취약점은 2019년 5월 14일 공개된 macOS 14.4.5, iOS 12.3 업데이트에서 패치되었습니다(취약점 번호: CVE-2019-8576). 발견한 취약점을 패치하기 위해 Apple에서 제시하는 방법을 참고하여 취약점에 대한 내용을 최대한 상세하게 적어 전용 메일 주소(product-security@apple.com)로 제출했습니다. 메일로 리포팅하자 먼저 Apple에서 자동으로 번호를 부여한 답장이 왔고, 조금 후에 해당 취약점 담당자가 연락해 왔습니다. 상세하진 않았지만 패치 일정을 알려주었고 보안 업데이트 노트에 남길 이름을 물어봤습니다. Apple에서는 여러 취약점을 리포팅 받아 주기적으로 보안 업데이트를 수행합니다. 이번에는 취약점 리포팅부터 패치까지 약 3개월이 걸렸는데요. 보안 업데이트 주기에 따라 리포팅 후 패치까지 보통 1개월에서 3개월 정도 걸리는 것 같습니다.

지금까지 커널 버그 헌팅 방법과 발견한 취약점을 분석한 내용을 공유했습니다. 아직은 활용성이 낮아 보이지만, 커널 힙 메모리를 잘 다룰 수 있는 방법을 더 연구하면 충분히 활용성을 높일 수 있을 것으로 보입니다. 관심 있으신 분들과 같이 서로 아이디어를 공유하면서 연구를 진행하면 좋을 것 같습니다. 긴 글 읽어주셔서 감사합니다.