[Paper Review] Exploiting Android’s Hardened Memory Allocator

저자Philipp Mao et al.
연도2024
게재처USENIX WOOT
유형attack
URLhttps://www.usenix.org/conference/woot24/presentation/mao

Introduction

최근 많은 메모리 오염 취약점은 heap과 관련되어있고, android 역시 heap 기반의 취약점으로 arbitrary code excution이 가능함. 구글에서 이를 해결하기 위해 Scudo라는 allocator를 개발하여 android 11부터 사용하기 시작함.

ptmalloc, jemalloc과 같이 Scudo 이전의 allocator 들에 대한 연구는 이미 많이 진행되었지만, Scudo에 대한 연구는 거의 진행되지 않았음.
→ 사실 잘 찾아보면 블로그 몇개 존재함..

따라서 이 연구에서는 먼저 Scudo에 대한 깊이 있는 연구를 제시함. 또한, Scudo의 보안 한계점을 찾고, 이를 이용하여 두가지의 익스플로잇 전략을 제시함. 첫번째 공격 방법의 경우 android 14에서 패치가 되었지만, 두번째 전략은 아직 패치되지 않음.

 

이 논문이 기여한 점은 다음과 같음.

  • Scudo의 보안 특징을 분석하고 체계화
  • Scudo를 타켓으로 한 두 가지 익스플로잇 전략 제안
  • 공격 전략을 사용한 실제 case study 진행
  • 가능한 mitigation 제시
  • Scudo를 분석, 익스플로잇하기 위한 gdb plugin, python library 개발

 


Background

이 논문의 배경지식으로는 Scudo의 보안 특성에 대해 다룸.

Scudo의 보안 특성은 크게 4가지 종류로 나눌 수 있다.

  1. Isolation : chunk는 크기별로 전용 구역에 나뉘어져 할당됨
  2. Randomization : 할당되는 chunk의 순서가 랜덤으로 정해짐
  3. Protection : chunk header의 checksum을 계속 체크(overflow, DF 방지)
  4. Separation : 포인터는 분리되어 매핑되고, guard page에 의해 보호됨

 

각각을 조금 더 자세히 정리해보자.

먼저, 다음과 같은 취약한 코드가 있다고 상정해보자.

1 int main(){ 
2 char tmp[0x100]; 
3 void∗ class_0_secondary_chunk = malloc(0x20000); 
4 void∗ class_1_chunk = malloc(0x8); 
5 void∗ class_2_chunk = malloc(0x18); 
6 printf("victim:%pn", class_2_chunk); 
7 while(1){ 
8     read(0, tmp, 0x100); 
9     int status = ∗(int∗)tmp; 
10    int size = ∗(int∗)(tmp+sizeof(int)); 
11    char∗ chunk = (char∗)malloc(0x18); 
12    printf("address:%pn", chunk); 
13    memcpy(chunk, tmp+sizeof(int)∗2, size); 
14    if(status & 0x2){free(chunk);} 
15    if(status & 0x4){free(chunk);break;} 
16    if(status & 0x8){free(class_2_chunk);}  
17    }
18 }

 

13번째 줄에서는 heap overflow, 14-15번째 줄에서는 Double free를 발생시킬 수 있다.

Isolation

할당하려는 크기에 맞는 classId를 정해주고, 정해진 class에 따라 해당 구역에 할당된다.

각 구역의 앞 뒤로 guard page가 삽입되어 접근 시에 segmentation fault를 발생시킨다. heap overflow를 막을 수 있는 것이다.

위와 같이 할당되는 것을 확인할 수 있다. class마다 0x40000 크기의 구역을 할당받게 되고, 앞뒤로 guard page가 있는 것을 확인할 수 있다.

또한, primary 할당자에 의해 할당될 수 없는 크기의 경우는 secondary 할당자에 의해 할당된다.

 

Randomization

각 class 마다 할당 대기 chunk를 미리 가지게 된다. 이 chunk들은 TransferBatch를 통해서 받게 되는데, TransferBatch에서 free list를 구성할 당시에 chunk의 순서가 shuffle되고, 이를 그대로 class에게 전달해주게 된다. 따라서 연속된 할당이더라도 region 내에서의 위치는 랜덤하게 적용된다.

위의 사진은 Randomization을 보여주는 예시이다. 위의 코드를 5번 반복하도록, 2번 각각 실행한 결과이다. chunk가 할당되는 주소가 계속 바뀌고, 2번의 실행에서도 서로 다른 것을 확인할 수 있다.

 

Protection

chunk를 할당하면, 받은 pointer의 앞 0x10만큼의 부분에 chunk header가 삽입된다.

chunk header는 위의 값들을 가진다. 여기서 눈여겨 볼 부분은 State와 Checksum 부분이다.

먼저 State 부분은 해당 chunk가 할당된, 해제된, 격리된(해제되었지만 재사용 x) 상태임을 나타낸다. 이를 통해 Double free를 방지할 수 있다.

Checksum 부분은 cookie 값, chunk의 주소값, 헤더 필드의 값들을 통해서 CRC32 checksum으로 채워진다. 해당 값을 확인함으로써 heap chunk overwrite를 감지할 수 있다. chucksum은 Scudo가 chunk에 접근할 때마다 재계산하고 비교하기 때문에 공격을 빠르게 탐지, 방어할 수 있다.

 

Separation

Secondary allocator에 의해 할당된 Secondary chunk는 기존 chunk header에다가 추가로 확장된 header를 가진다. 이 확장된 header는 pointer의 0x40 앞에 존재한다.

Prev, Next 필드에는 이전, 다음 Secondary chunk를 가리키는 포인터, MapBase와 MapSize는 guard page를 포함한 매핑의 base 주소와 크기 (CommitBase, CommitSize는 guard page 포함 x)를 저장한다.

이 확장된 헤더는 따로 checksum을 가지지는 않는다. 하지만 매핑에 하나의 청크만 저장되어 있고, 헤더가 저장된 구역이 guard page에 의해 보호되기 때문에 안전하다고 본다.

 


Threat Model

  • 공격자는 악의적인 app, 타켓은 다른 취약한 앱, 혹은 시스템 서비스
  • 공격자는 타겟의 exposed functionality를 이용하여 타겟의 메모리를 조작
  • 타겟에는 메모리 취약점이 존재

 


Before Exploit

공격을 수행하기 위해서, 먼저 scudo의 RandomizeProtect를 무력화시킨다.

모든 안드로이드 앱(시스템 서비스 포함)은 Zygote 프로세스로부터 fork된다. 이는 처리 속도, 메모리 소비 등의 성능 향상을 위한 메커니즘이다.

https://developer.android.com/topic/performance/memory-overview?hl=ko#SharingRAM

그러나 하나의 프로세스로부터 fork되기 때문에 모든 유저 프로세스는 동일한 ASLR 레이아웃을 공유하고, Scudo의 region 마찬가지이다. Zygote 프로세스는 Scudo를 초기화하면서 쿠키도 설정하고, Randomization seed도 설정하기 때문에, 악의적인 앱은 Zygote로부터 fork된 다른 프로세스의 chunk가 어디에 할당될지 알아낼 수 있다.

 

이렇게 할당된 주소를 알아내고, 쿠키값까지 알아내면(직접 leak or 브루트포싱) 올바른 checksum을 구성할 수 있으므로 Protect를 무력화할 수 있다.

 


Exploit Techniques

실제 공격 전략은 2가지를 제시한다. 두 공격 모두 inline pointer를 조작하는 방법을 사용한다.

Background의 Separation에서 제시한 secondary chunk의 extended header를 다시 살펴보면, 이전, 이후 secondary chunk를 가리키는 prev, next 값과 해당 chunk의 시작 위치를 나타내는 CommitBase 값이 존재하는 것을 확인할 수 있다.

CommitBase 값은 secondary chunk의 시작 주소를 가지고 있으며, 만약 secondary chunk가 해제되면, free list에는 이 CommitBase 값이 저장된다. 이후에 다시 secondary chunk의 할당 요청이 들어오면 free list에 저장된 값의 주소에 chunk를 새롭게 할당해주는 것이다.

따라서 위의 그림과 같이 overflow를 통해 secondary chunk header를 다른 값으로 채워넣으면, CommitBase에 적힌 주소에 접근하게 되어 Segmentation fault가 발생하게 된다.

다르게 말하면, 내가 원하는 주소를 CommitBase에 적을 수 있다면, 해당 주소에 chunk를 할당할 수 있다는 것이다.

이 논문에서 제시한 2가지의 공격 전략은 모두 위의 기술을 사용한다.

 

Forged CommitBase

논문에서 제시한 첫번째 공격 전략이다. 위에서 설명한 대로, CommitBase에 원하는 주소를 입력하고 해당 위치에 chunk를 할당하는 그대로의 방법이다.

공격 순서는 다음과 같다.

  1. 먼저 정상적인 Primary Chunk가 할당되어 있다.
  2. overflow 등의 메모리 오염 공격을 통해 ClassId를 0으로 바꿔주고(secondary chunk로 취급) CommitBase에 chunk를 할당하고 싶은 주소를 적어준다.
  3. 해당 chunk를 해제하면 CommitBase에 적힌 주소가 Secondary free list에 저장된다.
  4. 다시 할당 요청을 하면 해당 주소에 chunk가 할당된다. (이후 ROP 등의 공격 가능)

 

 

Safe Unlink

논문에서 제시한 두번째 공격 전략이다. 이 전략은 조금 더 복잡하다.

 

 

 


Case Study

CVE-2015-1528을 그대로 사용하여 공격을 구성하였다. 해당 CVE는 heap overflow, underflow가 발생하는 취약점이다. 이 취약점은 jemalloc을 대상으로 했지만, scudo에도 그대로 적용 가능하다.

 https://nvd.nist.gov/vuln/detail/CVE-2015-1528

System Server의 service 중 하나인 SensorService를 대상으로 공격을 진행할 것이다. 시스템 서버에서 작동 중인 서비스는 다른 앱에 Binder IPC로 노출되어 있다. Binder는 안드로이드의 IPC 메커니즘으로, 안드로이드 앱과 서비스 사이의 통신을 가능하게 한다.

공격을 진행하기 위해서 악의적인 앱에서 두번의 Binder request를 SensorService에 보내게 된다. 첫번째 요청을 통해 secondary chunk free list에 공격하고자 하는 stack 주소를 작성하고, 두번째 요청을 통해 해당 stack 주소에 ROP chain을 작성한다.

status_t BnSensorServer::onTransact(uint32_t code, const Parcel& data, Parcel∗ reply, uint32_t flags) {  
	switch(code) { 
		case CREATE_SENSOR_DIRECT_CONNECTION: { 
		CHECK_INTERFACE(ISensorServer, data, reply); 
		String16& opPackageName = data.readString16(); 
		const int deviceId = data.readInt32(); 
		uint32_t size = data.readUint32(); 
		int32_t type = data.readInt32(); 
		int32_t format = data.readInt32(); 
		native_handle_t *resource = data.readNativeHandle(); 
		if (resource == nullptr) { 
		return BAD_VALUE; 
		}  
		native_handle_set_fdsan_tag(resource); 
		sp<ISensorEventConnection> ch = createSensorDirectConnection(...);
		native_handle_close_with_tag(resource); 
		native_handle_delete(resource); reply−>writeStrongBinder(IInterface::asBinder(ch)); 
		return NO_ERROR; 
	} 
	... 
}

 

위 코드는 SensorService에서 memory corruption이 발생하는 코드 일부이다. 인자의 code와 data는 공격자가 원하는 값을 줄 수 있으므로, CREATE_SENSOR_DIRECT_CONNECTION case를 발생시키고, 취약한 함수인 readNativeHandle()를 호출하게 만들어서 chunk header를 조작할 수 있다.

First Binder Request

 

공격자가 첫번째로 전달해주는 값은 위와 같다. 회색 표시된 부분은 공격가 크게 상관없는 부분이다.

초록색으로 표시된 nrFds에 -23을 줌으로써 readNativeHandle() 함수의 read에서 underflow가 발생하게 만든다. 즉, fake secondary header를 실제 chunk 바로 이전 부분에 새롭게 만드는 꼴이 되는 것이다.

그리고 그 fake secondary header의 값을 구성하는 것이 노란색으로 표시한 부분들이다. CommitBase에 stack 주소를 넣어서 해당 위치에 chunk가 위치될 수 있도록 header 값을 구성하였다.

native_handle_delete() 함수에서 할당된 chunk가 해제되면서 secondary chunk free list에 stack 주소가 들어가게 된다.

 

Second Binder Request

두번째로 전달해주는 값이다. 마찬가지로 회색 부분은 공격과 상관 없는 부분이다.

ROPChain의 크기 만큼 numInts를 넣어주고, 마지막에 ROPChain을 줌으로써 stack에 ROPChain이 적히게 된다. 이후에 main thread와 SensorService 간에 race condition이 발생하게 되는데, 만약 SensorService가 선점할 시 프로세스가 종료되고, main thread가 선점할 시 ROP chain이 잘 작동되어 원하는 공격이 수행된다.

 


Discussion

Mitigations

Safe Unlink 기법의 경우 Android 14 업데이트에서 막혔다. PerClass의 free list가 실제 pointer 대신 primary chunk heap region의 상대적인 offset을 저장하도록 바뀌었다. 그로 인해 fake linked list를 만드는 게 불가능해졌다.

 

Forged CommitBase 기법의 경우, 논문 저자가 mitigation을 제시했다. secondary chunk가 해제될 때마다, CommitBase에 저장된 값을 참고하여 실제로 그곳에 할당이 됐었는지 확인하는 것이다. 실제 LLVM repository에 pull request를 날렸지만, 성능 오버헤드로 인해 merge되지는 못했다. (즉, 아직 해당 공격은 가능하다는 것..!)

 

ARM MTE

arm의 MTE(Memory Tagging Extension)은 포인터의 top bits에 태깅을 통해서 overflow, UAF를 방지하는 기법이다. 할당된 주소와 이를 가리키는 포인터의 태그가 일치하지 않다면, 이를 memory corruption으로 간주하고 segfault를 일으키는 것이다.

MTE를 적용시키면 이 논문의 공격 선제 조건에 해당하는 memory corruption 자체가 힘들어지기 때문에 이 또한 위의 공격을 막아낼 수 있는 방법 중 하나가 된다.

 https://source.android.com/docs/security/test/memory-safety/arm-mte?hl=ko

https://www.usenix.org/publications/login/summer2019/serebryany

Quarantine

Scudo는 quarantine 기법을 사용할 수 있다. 이는 해제된 chunk를 바로 재사용하지 않고, 따로 모아놨다가 일정한 기준이 넘으면 다시 재사용할 수 있는 상태로 바꾸는 것이다.

quarantine 기법을 사용하면 UAF를 방지할 수 있다. 또한, 이 논문에서 제시한 공격 역시 해제된 chunk를 바로 재사용할 수 없기에 더 힘들어지게 된다.

하지만 그만큼 memory overhead가 높아져서 quarantine을 사용하지 않는 것이 기본값으로 설정되어 있다.

 


Conclusion

이 논문에서는 안드로이드의 Scudo를 설명하고, 이를 공격하는 전략을 제시하고 있다. 먼저 Scudo의 4가지 보안 특성을 나열하였다. 그 다음 메모리 오염 취약점을 바탕으로 한 2가지 공격 시나리오를 보여주었다. 실제 CVE를 활용하여 공격 시나리오를 적용하여 보여주었다. 마지막으로 공격을 방지하기 위한 mitigation까지 제시하고, 공격에 사용된 code와 방어 기법을 github에 공개하였다.

https://github.com/HexHive/scudo-exploitation

 

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤