Translation or Review

[Stefan Esser] Tales from iOS 6 Exploitation and iOS 7 Security Changes (번역)

comalmot_ 2022. 4. 7. 15:03
반응형

동기

Google Project Zero 블로그에서 이 글을 보다가 Finding an entry point for kernel code execution 대목에서 언급이 되어 리뷰하게 되었습니다.

개요

이 발표에서는 posix_spawn() 취약점에 대해서 이야기합니다. 또 이것을 어떻게 information leak 이상의 취약점인지 알아낸 과정도 소개합니다. 또 iOS 7의 보안에서 어떤 다양한 변화가 있었는지도 설명합니다.

Part 1 : posix_spawn() - The info leak that was more..


SyScan 싱가포르 2013에서 다수의 취약점이

posix_spawn() 취약점도 그 중 하나였는데요, posix_spawn()은 프로세스를 Spawn/Execute 하는데 더할 나위없이 강력한 수단이었습니다. 이 취약점은 당시 Kernel Heap Infomation leak 으로 분류되었습니다.

posix_spawn() File Actions

File Action은 Parent가 Child의 File Descriptor를 열고/닫고/복제하는 것을 허용합니다.
이러한 동작들은 크기가 약 1040 byte인 구조체에서 정의가 되며, 작은 Header가 앞에 붙게 됩니다.

Posix_spawn()에서 사용되는 구조체 정보 (출처 : Tales from iOS 6 Exploitation and iOS 7 Security 슬라이드 )

구조체를 살펴보면, 열거형 psfa\_t는 각 File Action을 나타내고 있으며, 이것이 구조체 \_psfa\_action의 최상단에 위치하고 있습니다.

사용자 제공 크기(구조체의 크기가 변동이 가능하므로)가 큰지, 작은지를 확인하고 난 뒤, Action을 설명하는 Data는 Kernel에 복사됩니다.

if (px_args.file_actions_size != 0) {
     /* Limit file_actions to allowed number of open files */
    int maxfa = (p->p_limit ? p->p_rlimit[RLIMIT_NOFILE].rlim_cur : NOFILE); // File Action Limit 확인
     if (px_args.file_actions_size < PSF_ACTIONS_SIZE(1) || 
     px_args.file_actions_size > PSF_ACTIONS_SIZE(maxfa)) { // File Action Size 재확인
         error = EINVAL;
         goto bad;
     }
     MALLOC(px_sfap, _posix_spawn_file_actions_t, px_args.file_actions_size, M_TEMP, M_WAITOK) 

    if (px_sfap == NULL) {
     error = ENOMEM;
     goto bad;
     }

 imgp->ip_px_sfa = px_sfap;
 if ((error = copyin(px_args.file_actions, px_sfap, px_args.file_actions_size)) != 0) // 커널에 복사
 goto bad;
}

posix_spawn() File Actions Incomplete Verification

이 발표에서는 아래 이유를 들어 Incomplete Verification 이라고 설명하고 있습니다.

  • 신뢰할 수 있는 데이터 내부의 File Action 수 때문에 취약하다. 제공된 데이터가 카운트에 충분한지 확인되지 않았으며 이를 통해 루프를 돌고 있는 데이터는 버퍼 외부에서 데이터를 읽을 수 있고 Crash를 낼 수 있다.
static int exec_handle_file_actions(struct image_params *imgp, short psa_flags) 
{
 int error = 0;
 int action;
 proc_t p = vfs_context_proc(imgp->ip_vfs_context);
 _posix_spawn_file_actions_t px_sfap = imgp->ip_px_sfa;
 int ival[2]; /* dummy retval for system calls) */

 for (action = 0; action < px_sfap->psfa_act_count; action++) { // 바로 이 psfa_act_count가 문제가 됩니다. 
     _psfa_action_t *psfa = &px_sfap->psfa_act_acts[action];
     switch(psfa->psfaa_type) {
         case PSFA_OPEN: {
            ...

posix_spawn() File Actions Information Leak

데이터와, 데이터의 크기를 잘 작성하게 되면, PSFA_OPEN 파일 액션을 사용하여 Kernel Heap에서 바이트들을 Leak할 수 있습니다.

아래와 같이, 파일명의 첫 번째 요소가 버퍼 내에서 시작되도록 사이즈를 선택하고, 파일 이름의 끝은 Kernel Heap에서 가져옵니다.

fnctl(F_GETPATH) 를 사용하면, Leak된 바이트를 얻을 수 있습니다.

fnctl(F_GETPATH) 의 결과 (출처 : https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/fcntl.2.html)

스택 오버플로우에서는 아래와 같이 파일 경로를 가져올 수 있다고 말하고 있습니다. 여기서는 fnctl(psfa->psfaa_filedes, F_GETPATH, psfa->psfao_path) 가 될 것 같네요.

if (fcntl(fd, F_GETPATH, pathbuf) >= 0) {
    // pathbuf now contains *a* path to the open file descriptor
}

Only an Information Leak?

이 발표에서는, 만약 posix_spawn() 이 information leak보다 더 많은 걸 할 수 있다면..? 이라는 질문을 트위터에서 받고 Information Leak만이 아니라 더 뭔가를 할 수 있을지 고민했다고 말씀하셨습니다.

information leak보다 더 많은 걸 할 수 있으려면, Buffer의 외부에 뭔가를 쓰거나/읽을 수 있어야 합니다.

이 과정에서 exec_handle_file_actions() 함수가 어디든지 Write가 가능한지 확인하게 되었으며, 이를 악용할 수 있다면..? 이라는 생각을 하셨다고 합니다.

Structure of exec_handle_file_actions

이 함수는 두 가지의 Loop로 구성되어 있으며, 이 두 루프 사이에 Error로 인한 종료 구문을 두고 있습니다.

각 Loop 모두 아래 케이스에 대한 Switch 구문을 구현하고 있습니다.

  • PSFA_OPEN
  • PSFA_DUP2
  • PSFA_CLOSE
  • PSFA_INHERIT

이제 위 네 가지 케이스를 모두 살펴볼 것 입니다.

PSFA_OPEN

첫 번째 루프에서 PSFA_OPEN의 1, 2번째 파트에서는 Write가 없었습니다.

PSFA_DUP2

마찬가지로 첫 번째 루프에서 PSFA_DUP2의 1번째 파트는 Write가 없었습니다.

PSFA_CLOSE

PSFA_CLOSE도 위와 같습니다.

PSFA_INHERIT

그런데 PSFA_INHERIT에서는 Write를 발견할 수 있었는데요, 아래는 코드 조각입니다.

case PSFA_INHERIT: {
	struct fileproc *fp;
	int fd = psfa->psfaa_filedes;
	/*
	* Check to see if the descriptor exists, and
	* ensure it's -not- marked as close-on-exec.
	* [Less code than the equivalent F_GETFD/F_SETFD.]
	*/
 	proc_fdlock(p);
 	if ((error = fp_lookup(p, fd, &fp, 1)) == 0) {
 		*fdflags(p, fd) &= ~UF_EXCLOSE; // This is a write in form of a binary AND
 		(void) fp_drop(p, fd, fp, 1);
 	}
 	proc_fdunlock(p);
 }
 break;

fdflags 매크로를 사용해서, 이진 AND 연산을 통해 Write를 수행하고 있습니다.

 

The filedesc struct

fd_ofileflags는 사실 Byte 배열입니다. 이제 우리는 그것이 어떻게 할당되는지를 확인합니다.

struct filedesc {
	struct fileproc **fd_ofiles; /* file structures for open files */
	char *fd_ofileflags; /* per-process open file flags */
	struct vnode *fd_cdir; /* current directory */
	struct vnode *fd_rdir; /* root directory */
	int fd_nfiles; /* number of open files allocated */
	int fd_lastfile; /* high-water mark of fd_ofiles */
	int fd_freefile; /* approx. next free file */
	u_short fd_cmask; /* mask for file creation */
	uint32_t fd_refcnt; /* reference count */
	int fd_knlistsize; /* size of knlist */
	struct klist *fd_knlist; /* list of attached knotes */
	u_long fd_knhashmask; /* size of knhash */
 	struct klist *fd_knhash; /* hash table for attached knotes */
 	int fd_flags;
};

filedesc 구조체입니다.

 

Where does fd_ofileflags come from?

fd_ofile flags는 실제로 할당된 메모리 블록의 시작이 아닙니다.

fd_ofiles를 5 * 현재 최대 파일 디스크립터로 처음 할당한 다음 fd_ofileflag를 마지막 "현재 최대 파일 디스크립터"를 가리키도록 설정합니다.

MALLOC_ZONE(newofiles, struct fileproc **,
numfiles * OFILESIZE, M_OFILETABL, M_WAITOK);
proc_fdlock(p);
if (newofiles == NULL) {
	return (ENOMEM);
}
if (fdp->fd_nfiles >= numfiles) {
	FREE_ZONE(newofiles, numfiles * OFILESIZE, M_OFILETABL);
	continue;
}
newofileflags = (char *) &newofiles[numfiles];

...

ofiles = fdp->fd_ofiles;
fdp->fd_ofiles = newofiles;
fdp->fd_ofileflags = newofileflags;
fdp->fd_nfiles = numfiles;
FREE_ZONE(ofiles, oldnfiles * OFILESIZE, M_OFILETABL);

 

What do we know so far?

fd_ofileflags는 버퍼의 시작이 아니지만 MALLOC_ZONE()으로 할당된 버퍼 중 하나를 가리킵니다.
동적 버퍼 MALLOC_ZONE()이 kalloc()와 동일하고 마지막으로 fd_ofileflag 길이가 "현재 최대 파일 디스크립터"인 경우 버퍼 외부에 쓰려면 fdflags에 잘못된 파일 설명자를 전달해야 합니다.

 

PSFA_INHERIT and illegal file descriptors?

PSFA_INHERIT에서 통과된(passed) 파일 디스크립터는 fp_lookup()에 의해서 확인되기 때문에, 여기서 조작된 파일 디스크립터나 fdflags를 전달할 수 없습니다.

 

Is there a write in the second loop?

두 번째 루프에는 fdflag write를 포함하고 있습니다(binary OR을 통한 write). 그리고 파일 디스크립터는 psfaa_filedes 또는 psfaa_openargs.psfao_oflag로 채워집니다.

이 두 변수 모두 첫 번째 루프에서 "유효한" 파일 디스크립터만 포함하도록 선택됩니다. 

proc_fdlock(p);
for (action = 0; action < px_sfap->psfa_act_count; action++) {
	 _psfa_action_t *psfa = &px_sfap->psfa_act_acts[action];
	 int fd = psfa->psfaa_filedes;       // 첫 번째 루프와 같은 부분1
	 switch (psfa->psfaa_type) {
 		case PSFA_DUP2:
 			fd = psfa->psfaa_openargs.psfao_oflag; // 첫 번째 루프와 같은 부분2
 		/*FALLTHROUGH*/
 		case PSFA_OPEN:
 		case PSFA_INHERIT:
 			*fdflags(p, fd) |= UF_INHERIT; // 또다른 Write!!
 			break;
 		case PSFA_CLOSE:
 			break;
	 }
 }
 proc_fdunlock(p);

 

Vulnerable or Not?

첫 번째 루프와 두 번째 루프 모두 fdflags에 전달된 파일 디스크립터가 검증되나, 두 검증 과정에서 중요한 차이를 확인할 수 있습니다.

 

Write One

첫 번째 Write를 하기 위해서, 파일 디스크립터는 메모리에서 읽히며, 검증되고, Write를 위해 사용됩니다.

 

case PSFA_INHERIT: {
	struct fileproc *fp;
	int fd = psfa->psfaa_filedes; // psfaa_filedes를 메모리에서 읽음.
	 /*
	 * Check to see if the descriptor exists, and
	 * ensure it's -not- marked as close-on-exec.
	 * [Less code than the equivalent F_GETFD/F_SETFD.]
	 */
 	proc_fdlock(p);
 	if ((error = fp_lookup(p, fd, &fp, 1)) == 0) { // 검증!
 		*fdflags(p, fd) &= ~UF_EXCLOSE; // Write!!
 		(void) fp_drop(p, fd, fp, 1);
	}
 	proc_fdunlock(p);
 	}
 break;

 

Write Two

두 번째 루프에서 사용된 파일 디스크립터는 메모리에서 읽혀지고 (psfa->psfaa_filedes) 그 다음에 사용됩니다.(used)

 

첫 번째 루프 체크에 의존을 하기 때문에, 두 번째 루프에는 체크 요소가 존재하지 않습니다.

 

proc_fdlock(p);
for (action = 0; action < px_sfap->psfa_act_count; action++) {
	_psfa_action_t *psfa = &px_sfap->psfa_act_acts[action];
	int fd = psfa->psfaa_filedes; // 메모리에서 데이터 읽어오기1
	switch (psfa->psfaa_type) {
		case PSFA_DUP2:
			fd = psfa->psfaa_openargs.psfao_oflag; // 메모리에서 데이터 읽어오기2
 		/*FALLTHROUGH*/
 		case PSFA_OPEN:
 		case PSFA_INHERIT:
 			*fdflags(p, fd) |= UF_INHERIT; // Write!!
 		break;
 		case PSFA_CLOSE:
 		break;
 	}
 }
 proc_fdunlock(p);

 

Difference in Writes: TOCTOU

이 두 Write들 (Write One, Write Two)의 명백한 차이는 바로 TOCTOU 입니다.

Write One은, 검증 전에 read가 수행됩니다.

Write Two는,  검증 후 최종 re-read가 수행됩니다. (역 : PSFA_DUP2에 있는 두 번째 read from memory를 말하는 것 같습니다.)

 

 

최종적으로, 위와 같이 정리할 수 있습니다.

 

Is difference in TOCTOU a vulnerability here?

- Re-phrasing

TOCTOU 중에 파일 디스크립터가 포함된 메모리가 변경이 가능할까요?

 

- Under normal circumstances

파일 디스크립터는 이 커널 스레드만 액세스할 수 있는 메모리에서 읽힙니다.

중간 값(파일 디스크립터)은 변경되지 않으므로 TOCTOU 문제는 없습니다.

 

하지만! 우리는 정상적인 상황에 넣어있지 않으며, 버퍼 외부에서 File Action을 읽을 수 있는 취약점(Write 설명에서 보였던 Read)이 있으며, 버퍼 외부의 모든 것들을 다른 커널 스레드를 활용해 수정할 수 있습니다(아까 언급한 Write).

 

그리고 이것은 TOCTOU 또는 Race condition 취약점이 됩니다.

 

 

Winning the Race?

만약 우리가 검증과 re-read 사이에서 메모리를 바꿀 수 있다면 Race Condition 공격을 할 수 있습니다.

그렇기 때문에, 우리는 적당한 때에 메모리 수정을 하기 위한 두 번째 스레드가 필요합니다.

우리는 매끄러운 동기화를 필요로 하고, 루프 1과 루프 2에서의 체크인 루프 간에 변경하기에 충분한 빠르기를 가져야 하겠습니다.

우리는 최대한 익스플로잇 가능성을 높이기 위해 취약한 커널 스레드의 속도를 늦추려고 노력할 것 입니다.

 

Slowing down exec_handle_file_actions()

루프의 속도를 늦추는 방법에는 무엇이 있을까요?

 

첫 번째로는 루프가 더 많이 돌아가게끔 하는 것 입니다. 

-> 이것은 파일 액션의 수를 늘리면 되겠습니다.

 

두 번째로는 루프 내부의 작업 속도를 저하시키는 것입니다.

-> 이것은 open() / dup2() / close() 와 같은 함수를 사용하면 되겠습니다.

 

Increasing number of file actions?

파일 액션의 수를 늘리면 뭔가 해낼 수 있을까요?

 

각 파일 동작은 1040 바이트입니다.

이 동작들은 kalloc()을 사용하여 할당됩니다. 그렇기 때문에 4kb 또는 12kb 메모리를 가지게 됩니다. 즉 3에서 11개의 파일 액션을 위한 유일한 공간이기 때문에 현저한 속도 저하에 충분하지 않게 됩니다.

 

그런데 아까 후자(루프 내부의 작업 속도 저하)로 다시 돌아와봅시다.

우리는 dup2()도 없고, close()도 없어 이를 활용하여 작업 속도를 저하시키기는 어렵습니다.

 

하지만, open()은 어떨까요?

 

Manpage of open()

open()의 Manpage를 봅시다...

open 함수에서 눈여겨 보아야할 부분이 하나 있는데, 바로 oflag라는 파라미터입니다.

이 oflag라는 파라미터에 들어가는 옵션들이 굉장히 많은데, 옵션들 중에 O_SHLOCK과 O_EXLOCK을 눈여겨 보아야 합니다.

 

open() 이라는 함수는 File에 Lock을 거는 것(비밀번호로 잠그는 것이 아님)을 지원합니다. 이 기능을 어떻게 활용하냐면...

만약 우리가 이미 lock된 파일을 연다고 생각해보면, posix_spawn은 lock이 풀릴 때까지 sleep을 하게 됩니다.

 

Winning the Race !!

Race condition 조건을 쉽게 맞출 수 있다는 것을 확인하였으며, 이제 우리는 파일 lock을 통해서 우리의 두 번째 스레드와 동기화만 해주면 됩니다.

 

File Locking Sync

 

At this point we have the following

위 공격 과정을 살펴보면, 3개의 파일 액션, 2개의 파일 Lock, 2개의 스레드로 쉽게 Race Condition을 할 수 있습니다.

우리는 posix_spawn으로 할당된 kalloc 공간 (1536 이상) 을 처리해야하며, 파일 액션 2의 일부와 파일 액션 3는 버퍼 외부에 있습니다.

 

이러한 작업을 수행하려면, Heap-Feng-Shui가 필요하다고 보면 되겠습니다.

 

How to control the write?

*fdflags(p, fd) |= UF_INHERIT;

이 Write는 UF_INHERIT(0x20) 에 이진 OR 연산을 수행합니다.

우리는 메모리 내 어디든지  바이트의 특정 bit 5만 설정할 수 있습니다.

 

또 Write는 fd_ofileflags에 대해 상대적입니다.

 

그러면 문제가 있습니다. fd_ofileflags는 어디에 있는걸까요?

 

Where is fd_ofileflags?

fd_ofileflags는 프로세스가 시작된 후에 할당되지만, 어디에 있는지 알 수 없습니다(주소의 개념).

 

fd_ofileflags의 주소를 알아내기 위해 우리는 약간의 information leak을 필요로 하지만, 현재로서는 우리가 fd_ofileflags의 주소를 leak 할 수는 없습니다.

 

따라서, man-made information leak을 위해 relative write를 활용해야 합니다.

 

Force fd_ofileflags relocation

fd_ofileflags는 알수 없는 위치에 할당되었습니다. 그 이유는 relative write를 악용하기 위해서는 적어도 우리가 그것을 재배치할 수 있어야 하기 때문입니다. 

기본적으로 허용되는 파일 디스크립터는 256개로 시작하며, 모든 파일 디스크립터가 소진될 때 fdalloc()에서 재할당이 발생합니다.

int fdalloc(proc_t p, int want, int *result)
{
 	...
 	lim = min((int)p->p_rlimit[RLIMIT_NOFILE].rlim_cur, maxfiles);
 	for (;;) {
 	...
 	/*
 	* No space in current array. Expand?
 	*/
 	if (fdp->fd_nfiles >= lim)
 		return (EMFILE);
 	if (fdp->fd_nfiles < NDEXTENT)
 		numfiles = NDEXTENT;
 	else
 		numfiles = 2 * fdp->fd_nfiles;
 	/* Enforce lim */
 	if (numfiles > lim)
 		numfiles = lim;
 	proc_fdunlock(p);
 	MALLOC_ZONE(newofiles, struct fileproc **,
 	numfiles * OFILESIZE, M_OFILETABL, M_WAITOK);
 	proc_fdlock(p);
 	if (newofiles == NULL) {
 		return (ENOMEM);
 	}
 ...
 newofileflags = (char *) &newofiles[numfiles];

fd_ofileflags 재할당을 강제하는 것은 다음과 같습니다.

 

1. setrlimit(RLIMIT_NOFILE)을 사용하여, 열 수 있는 파일 제한을 257로 증가시키는 방법

2. dup2()를 사용하여, 허용된 가장 높은 파일 디스크립터를 강제로 사용하는 방법

 

메모리 할당은 5 * 257, 즉 1285로 이루어질 것입니다.

이렇게 재할당된 fd_ofileflag는 kalloc.1536 영역으로 끝나게 됩니다.

 

 

Relocated... What now?

재할당을 통해서, fd_ofileflag를 다른 블록과 상대적인 위치에 배치할 수 있습니다.

그러면, 아래 세 가지 요소를 고민해볼 수 있습니다.

- kalloc.1536 영역의 Heap-Feng-Shui가 필요합니다.

- 또 relative 바이너리 기반 OR을 0x20과 했을때 이것을 가지고 무엇을 할 수 있을까에 대해서 고민해보아야 합니다.

- 또 Azimuth의 vm_map_copy_t self locating technique도 생각해볼 수 있습니다.

 

Self-Locating with vm_map_copy_t

1.두 개의 vm_map_copy_t 구조체 뒤에 fd_ofileflag를 재배치해야 합니다. 

2. relative write를 사용하여 첫 번째 vm_map_copy_t 의 2번째 바이트를 늘립니다.

3. information leak에 대한 첫 번째 메시지를 받을 수 있습니다.

4. 두 번째 vm_map_copy_t의 주소를 포함해서 표시할 수 있습니다.

5. fd_ofileflags 구조체의 내용도 마찬가지로요.

왼쪽부터 1, 2, 3, 4, 5 순서

 

What we have so far ...

• vm_map_copy_t(OOL mach_msg)를 통해 kalloc.1536 영역을 채웁니다.
• 구멍을 엿보고 fd_ofileflag 위치를 그 안으로 트리거합니다(setrlimit + dup2)
두 개의 구멍(H1 다음에 H2)을 더 찌르고 H2를 초기 파일 작업 2+3(A 닫기+B 열기)으로 다시 채웁니다(OOL machsg).
• posix_spawn을 합니다
• 파일 A를 해제하고 파일 B를 기다릴 때 다른 스레드가 메모리를 수정하도록 합니다.

 

• 두 번째 스레드는 H2에 구멍을 내고 새 파일 액션으로 다시 채웁니다.
• 파일 작업 2가 PSFA_CLOSE에서 PSFA_DUP2로 변경되었습니다.
• 파일 액션 2의 파일 디스크립터가 첫 번째 vm_map_copy_t 구조의 크기 필드의 relative 위치로 설정됩니다.
• 두 번째 스레드는 posix_spawn을 wake-up하기 위해 파일 B를 닫습니다.
• posix_spawn이 오류와 함께 반환된 후 첫 번째 메시지(info leak에 대한)를 수신합니다.
=> 유출된 데이터에서 이제 fd_ofileflags의 주소를 알 수 있습니다.

 

Now write where?

이제 우리는 fd_ofileflags의 주소를 알 수 있으며, 메모리의 어느 곳이던지 Write가 가능합니다.

이제 우리가 원하는 코드를 실행하기 위해 덮어쓸 항목을 선택해야하는데요, 여러가지 가능성이 있지만 Buffer Overflow를 만들기 위해서 데이터 객체의 크기 필드를 찾습니다.

 

From Data Objects to Overflows....

이제 우리는 아래 세 가지의 문제들을 해결해야 합니다.

1. 덮어쓸 데이터 객체를 생성하는 방법

2. 그 객체의 주소를 알아내서 Write

3. 마지막으로 kfree를 비정상적인 영역으로 트리거하기 위해서 데이터 객체를 파괴해야 합니다.

 

Creating Data and Leaking its Address

Data 객체를 만드는 것은 OSUnserializeXML()을 활용하면 쉽게 가능합니다.

 

이것은 io_service_open_extended()와 그 함수의 파라미터 속성들을 통해서 가능하며 현재 상황에서 leak은 굉장히 쉽습니다. (앞에서 언급했던 방법을 사용하면)

우리는 데이터 객체와 그에 대한 256 개의 참조를 배열에 넣었으며, Array bucket이 kalloc.1536 영역에 할당됩니다.

vm_map_copy_t 의 self-locating과 동시에, Array bucket의 콘텐츠를 leak 할 수 있습니다.

-> 그리고 이것이 데이터 객체의 주소를 알아내는 결과를 가져옵니다.

 

Overwriting and Destorying the Data Object

이제 데이터 객체의 Capacity 필드를 타겟으로 posix_spawn() 공격을 다시 해야 합니다.

그런 다음 드라이버 연결을 끊어서 데이터 객체를 free할 수 있습니다.

-> 이렇게 하면 데이터 버퍼가 잘못된 영역으로 이동합니다.

-> 그 다음, 해당 영역에서 하는 할당은 짧은 버퍼를 반환하며, OOL mach_msg를 전송하여 Overflow를 트리거할 수 있게 됩니다.

 

What to overflow into...

이제 우리는 posix_spawn()의 외부에서 Heap-based Overflow를 트리거할 수 있습니다.

Overflow 트리거가 가능하기 때문에, 이제 그 대상을 찾아야 합니다.

몇 가지 예를 들면, 드라이버 연결에 의해 발생하는 IOUserClient 오버플로(임의 코드 실행 취약점), vm_map_copy_t 에서 Heap-Feng-Shui를 통해 발생하는 임의의 info leak 취약점이 있습니다.

Overflowing into vm_map_copy_t

이제 vm_map_copy_t 구조체를 Overflow하게 되면, 커널의 모든 위치에서 User-space로 어떤 양이던, 바이트를 읽을 수 있습니다. 이 동작은 가짜 vm_map_copy_t header를 설정하기만 하면 됩니다. 그리고 나서 메시지(객체 주소 leak)를 받습니다.

 

Overflowing into a driver connection

IOUserClient 객체 인스턴스로 오버플로우 한 뒤 vtable을 우리가 원하는 method들로 교체하고(vtable Overwrite), retainCount를 높은 값으로 설정하여 큰 문제가 발생하지 않도록 합니다.

-> 그런데 왜 vtable을 Overwrite할까요?

 

Vtable where are thou?

우리의 fake vtable은 그저 메모리에 넣기만 하면 되는 포인터들의 목록입니다.

 

1. 우리는 mach_msg를 통해 커널 메모리에 vtable을 넣을 수 있습니다.

이것을 위해서 kalloc.1536 영역을 대상으로 사용합니다. 왜냐하면, 충분한 공간이면 더 긴 vtable을 넣을 수 있기 때문입니다. 또 우리는 이미 상대적 위치에 있는 block의 주소를 알고 있습니다. 

 

From vtable to Pwnage

이 시점에서 우리는 vtable이 가리켜야할 주소를 선택해야 합니다.

그런데 이를 위해서는 커널의 현재 주소, 그리고 안에 있는 정보들을 알아야합니다.

이것을 수행하려면 KASLR info leak이 필요합니다. 커널의 베이스 주소를 leak한다던지, vm_map_copy_t 기술을 사용해서 객체의 vtable을 leak을 하기 위해서 말이죠.

또 두 번째로는 유저 클라이언트 객체를 대신하여 vm_map_copy_t를 overflo 시켜서  leak을 할 수도 있습니다.

 

kern_return_t iokit_user_client_trap(struct iokit_user_client_trap_args *args)
{
 	kern_return_t result = kIOReturnBadArgument;
	IOUserClient *userClient;
 	if ((userClient = OSDynamicCast(IOUserClient,
 		iokit_lookup_connect_ref_current_task((OSObject *)(args->userClientRef))))) {
 		IOExternalTrap *trap;
 		IOService *target = NULL;
 		trap = userClient->getTargetAndTrapForIndex(&target, args->index); // fake vtable needs to implement this
 		if (trap && target) {
 			IOTrap func;
 			func = trap->func;
 			if (func) {
 				result = (target->*func)(args->p1, args->p2, args->p3, args->p4, args->p5, args->p6);
 			}
 		}
 	userClient->release();
 	}
 return result;
}

이것은 iokit_user_client_trap 함수의 코드입니다. 여기서부터 IOUserClient 외부 트랩을 찾는 것이 가장 쉽습니다. mach_trap 100, iokit_user_clinet_trap에서 호출할 수 있으며, 커널의 임의 매개 변수를 상ㅇ해서 임의 함수를 호출할 수 있게 됩니다.

 

 

IOExternalTrap * IOUserClient::
getExternalTrapForIndex(UInt32 index)
{
 return NULL;
}
IOExternalTrap * IOUserClient::
getTargetAndTrapForIndex(IOService ** targetP, UInt32 index)
{
 IOExternalTrap *trap = getExternalTrapForIndex(index);
 if (trap) {
 *targetP = trap->object;
 }
 return trap;
}

IOUserClient의 기본 구현은 getExternalTrapForIndex()를 호출합니다. 이 함수는 기본적으로 NULL을 반환하는 함수입니다. 그리고 우리는 getExternalTrapForIndex()만 덮어써야 합니다.

 

IOExternalTrap * IOUserClient::
OUR_FAKE_getExternalTrapForIndex(void *index)
{
 return index;
}
IOExternalTrap * IOUserClient::
getTargetAndTrapForIndex(IOService ** targetP, UInt32 index)
{
 IOExternalTrap *trap = getExternalTrapForIndex(index);
 if (trap) {
 *targetP = trap->object;
 }
 return trap;
}

vtable에서 getTargetAndTrapForIndex() 함수를 기본 형태로 설정을 한 뒤에, getExternalTrapForIndex()를 MOV R0, R1; BX LR 같은 명령을 수행하는 가젯으로 설정합니다.

 

다시 iokit_user_client_trap() 함수로 돌아와봅시다.

kern_return_t iokit_user_client_trap(struct iokit_user_client_trap_args *args)
{
 	kern_return_t result = kIOReturnBadArgument;
 	IOUserClient *userClient;
	if ((userClient = OSDynamicCast(IOUserClient,
 	iokit_lookup_connect_ref_current_task((OSObject *)(args->userClientRef))))) {
 		IOExternalTrap *trap;
 		IOService *target = NULL;
 		trap = userClient->getTargetAndTrapForIndex(&target, args->index); // index from user space will be used as kernel pointer to IOExternalTrap
 		if (trap && target) {
 			IOTrap func;
 			func = trap->func;
 			if (func) {
 				result = (target->*func)(args->p1, args->p2, args->p3, args->p4, args->p5, args->p6);
 			}
 		}
 	userClient->release();
 	}
 	return result;
}

이제 우리는 iokit_user_client_trap의 index 인수를 버퍼에 설정해서, 최대 7의 매개변수를 가진 커널의 모든 함수를 호출할 수 있게 됩니다.

반응형