[고독한 늑대들] Windows만 하던 놈이 macOS 맨땅에 헤딩하기 Part 0 (KR)

image

서론

안녕하세요! 오랜만에 인사드리는 gongjae 입니다 👋

[도닦기] 시리즈가 9단계로 무사히 막을 내렸습니다. 여러가지로 저에게 도움이 많이 됐던 시리즈이고, 막상 끝나고 나니 이제부턴 뭘 해야 할지 고민이 많아지더라구요 ㅎㅎ.. 아무래도 저희 팀 내부에서도 이렇게 팀 프로젝트로 연구글 쓰는 것도 괜찮다고 생각하여 각자 원하는 주제를 가지고 함께 공부하며 연구글을 쓰려고 하고 있습니다. 이번에 제가 속한 팀명은 [고독한 늑대들] 입니다~! 🐺 왜 고독한 늑대들이냐구요? 바쁘고 할게 많고 주제가 다른 사람들만 모여서..ㅋㅋ

아무렴 어떻습니까! 어쨌든 이번 시리즈에서의 저희 포지션은 바로 macOS 입니다! 갑자기 웬 macOS냐구요? 그야 멋. 있. 으. 니. 까.

image

지금까지 제가 쓴 글은 전부 Windows였습니다. ALPC, Named Pipe, Chrome 샌드박스… 물론 Windows가 멋이 없다는게 아닙니다. 가장 큰 이유는 이런식으로 Windows에 대해 공부하며 풀 체인 공부도 해보면서 자연스럽게 다른 OS에서는 어떤식으로 취약점이 발생하고, 어떻게 체이닝이 가능한가?에 대해 관심이 생겨서 이렇게 주제를 가져와봤습니다~!

문제는 제가 살면서 맥북을 한 번도 안 써본 촌놈이라는 겁니다. 컴퓨터 하면 당연히 Windows인 줄 알고 살았거든요. 그래서 며칠 만에 깨달았습니다. Windows랑 너무 다르다는 것을…. 😇

image

그래서 이번 Part 0 에서는 버그를 찾기 전에, 먼저 이 OS에서 어디를 봐야 하는지 부터 정리해보려고 합니다. 저는 먼저 이렇게 구조를 그리고 시작하는게 좋더라구요.🤭

참고로 블로그에는 이미 clalxk 님의 훌륭한 macOS 시리즈가 있습니다.

https://hackyboiz.github.io/2025/10/23/clalxk/imageIO_ko/

https://hackyboiz.github.io/2025/08/07/clalxk/MacOS_Sandbox_Escape_ko/

https://hackyboiz.github.io/2025/05/11/clalxk/MacOS_SIP-Bypass_ko/

https://hackyboiz.github.io/2025/01/19/clalxk/MacOS_TCC-Bypass_ko/

위 글들이 “이 버그가 어떻게 터졌나” 를 다룬다면, 이 글은 그보다 한참 앞 단계인 “저 버그 어디서 찾는데?” 를 다룹니다. 이 글 먼저 보시고 저 clalxk님의 글을 보시면 더 macOS에 대해 이해하기 쉬울 거라고 생각합니다! 제가 그랬기 때문

Part 0을 시작으로 macOS 취약점의 발톱 때라도 따라잡기 위한 여정을 시작해볼까요~? 🐺

1. 취약점을 찾기 전에 해야 할 초벌?구이

TCC를 우회할 건지, 샌드박스를 탈출할 건지, 커널을 볼 건지, 이런 걸 생각하기 이전에 저희는 먼저 이 OS에서 타겟을 어떻게 찾는지부터 확인해야 합니다. 그리고 여기서 나오는 결과물이 곧 “어디를 팔지” 목록이 됩니다.

1) 파일이 디스크에 없다고?

Windows면 C:\Windows\System32\kernel32.dll 집어서 IDA에 던지면 끝이었는데… macOS도 그럴 줄 알았다면 큰 오산!

image

/bin/ls 가 방금 없다고 나온 그 파일을 쓰겠다고 적어놓고 있고, ls 는 아주 잘 돌아갑니다. 😇

위 현상이 발생하는 이유는 dyld shared cache 때문입니다. macOS는 시스템 라이브러리를 전부 하나로 말아서 링크까지 미리 끝내놓고, 그 다음 원본 파일을 디스크에서 치워버립니다. 즉 otool -L 의 경로는 파일이 있는 위치가 아니라 캐시 안에서 찾을 이름표라고 할 수 있죠.

그럼 그 캐시 본체는 어디 있느냐, 여기 있습니다.

$ /bin/ls /System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/ | wc -l
      82
$ du -sh /System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/
2.4G

$ DSC=/System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_arm64e
$ ipsw dyld info $DSC
...
Num Images     = 4088
Num SubCaches  = 79
Shared Region:  6GB, address: 0x180000000 -> 0x339B10000

파일 82개에 2.4GB네요. 참고로 ipsw dyld info 에 파일명만 주면 현재 디렉터리에서 찾기 때문에 does not exist 가 뜰 수 있습니다. 경로를 통째로 주거나 위처럼 변수에 넣어두는 게 편해요. (Windows Powershell로 다져진 변수 넣기!)

라이브러리 4088개가 6GB 주소공간에 통째로 들어 있습니다. Windows로 치면 System32 폴더 전체가 파일 하나인 셈이죠. 꺼내는 건 ipsw 명령어를 사용하면 됩니다.

$ ipsw dyld extract $DSC \
    /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation \
    --output ./out
   • Created out/Foundation

$ /bin/ls -lh out/
-rwxr-xr-x  31M  Foundation

0.6초 만에 31MB짜리 Foundation 이 튀어나옵니다. 이제 이걸 IDA에 던지면 되겠죠? 물론 캐시에 들어간 건 공유 라이브러리뿐입니다. 데몬이나 실행파일은 /usr/libexec/trustd 처럼 디스크에 그대로 있어요. 이 글에서 앞으로 분석하는 건 대부분 그쪽입니다. 이럴거면 왜 알려줌?

2) 함수 이름도 없다고?

파일은 꺼냈으니 이제 열어서 분석하면 되나! 아니요.. 여기서 또 한 번 막힙니다.

image

$ ipsw macho info --arch arm64e --starts /usr/libexec/trustd | /usr/bin/grep -c '^0x'
1270
$ nm -arch arm64e /usr/libexec/trustd | /usr/bin/grep -cE '^[0-9a-f]+ [tT] '
1

함수는 1,270개인데 이름이 붙어 있는 건 1개입니다. Windows에서 PDB 없는 바이너리 열었을 때 sub_1800???? 만 잔뜩 나오던 그 기분 그대로네요.. 그런데 여기서 macOS가 저같은 사람들 생각해서 하나 정보를 줍니다. Objective-C 메서드 이름은 스트립을 해도 안 지워진다는 사실을 말이죠.

$ ipsw macho info --arch arm64e --objc -V /usr/libexec/trustd
// 0x100027108
- (_Bool)fetchNext:(id)next context:(id)context;
// 0x1000271bc
- (void)recordSSRFShadowBuckets:(unsigned int)buckets forContext:(id)context;

같은 바이너리에서 메서드 236개가 이름 그대로, 주소까지 붙어서 나옵니다. 이유는 간단한데, ObjC는 함수를 주소로 부르는 게 아니라 fetchNext:context: 같은 문자열 이름으로 런타임에 찾아서 부르기 때문입니다. 이름을 지우면 프로그램 자체가 안 돌아가요. 그래서 nm 이 보는 심볼 테이블이 아니라 __objc_* 라는 별도 영역에 따로 남아 있습니다.

저희 입장에서 이게 왜 좋냐면, 어떤 데몬이 밖으로 무슨 기능을 열어놨는지를 리버싱하기 전에 목록으로 먼저 볼 수 있다는 뜻이기 때문입니다. 시스템 데몬 581개 중 340개(59%)가 ObjC라 웬만하면 통하고요.

3) 디버거도 안 붙는다고?

엥 걍 동적으로 확인하면 되는거 아님?? - 네 안됩니다.

image

image

/bin/ls 조차 디버거 밑에서 안 돕니다. 범인은 SIP(System Integrity Protection) 인데, Apple이 서명한 플랫폼 바이너리는 디버깅을 못 하게 막아놨거든요. root여도 마찬가지입니다. 물론 SIP를 끄면 붙습니다. 근데 그러면 제가 보고 있는 시스템이 실제 사용자 시스템이랑 달라져버리죠. 기껏 버그 찾아놓고 “그거 SIP 끈 상태에서만 되는 거 아니냐” 소리 들으면 좀 그렇잖아요. 괜히 서럽네..

그래서 macOS 버그헌팅은 정적 분석이 기본기가 됩니다. 디스크에 있는 것만으로 가설을 다 세워놓고, 확인은 제가 직접 만든 프로세스로 하는 순서가 돼요. 다행히 정적으로 알아낼 수 있는 게 생각보다 훨씬 많습니다. 다음 두 개가 그 얘기입니다.

4) 모든 것은 파일에 적혀 있다!

Windows에서 “이 프로세스가 뭘 할 수 있나”를 보려면 토큰 까고, 권한 목록 뽑고, Integrity Level 확인하고… 어쨌든 돌고 있는 프로세스를 붙잡아야 했습니다. macOS는 그럴 필요가 없습니다. 그냥 파일만 열면 된다는 점!

$ codesign -d --entitlements - --xml /usr/libexec/lsd | plutil -p -
{
  "com.apple.private.amfi.can-check-trust-cache" => true
  "com.apple.private.coreservices.canmaplsdatabase" => true
  ...
}

이걸 entitlement(엔타이틀먼트) 라고 부릅니다. 코드 서명 안에 박혀 있는 권한 선언이라 실행할 필요도, 붙을 필요도 없고 파일만 있으면 읽힙니다. lsd 하나만 해도 com.apple.private.* 가 18개나 들어 있네요. 이름만 봐도 대충 뜻이 보이는 것도 재밌는 부분입니다. com.apple.private.* 로 시작하면 Apple 내부용이라 서드파티는 못 받는 권한이고, kTCCService* 가 붙어 있으면 동의창 없이 카메라·마이크·디스크에 접근할 수 있다는 뜻입니다. com.apple.rootless.* 는 아까 그 SIP 보호 영역을 건드릴 수 있다는 얘기고요.

codesign 만 돌리면 되니까 아예 한 대를 통째로 세봤습니다. root도 실행도 필요 없습니다.

# /usr/libexec + /usr/sbin + /usr/bin 실행파일에 codesign 을 전부 돌린 결과
실행파일                          : 1550
엔타이틀먼트를 하나라도 가진 것    :  523   (34%)
 ├ com.apple.private.*            :  465
 ├ TCC 계열                        :  140
 └ rootless(SIP) 계열              :   93

TCC 권한을 들고 있는 바이너리가 140개, SIP 쪽을 건드릴 수 있는 게 93개. 이게 저희가 찾던 타겟 벡터입니다. 하지만 타겟 벡터를 찾았다고 끝이 아니겠죠 당연히? 당연하게도 TCC나 SIP쪽을 건드리는 친구들이라면 제가 접근을 못 할 가능성이 높을테니 말이죠..

Windows의 ALPC나 named pipe 자리에, macOS에는 Mach 포트와 그 위에 얹힌 XPC 가 있습니다. 서비스마다 com.apple.xxxd 같은 이름이 있고 그 이름으로 찾아서 붙는 구조입니다. 그러니까 “내가 저 서비스에 붙을 수 있나”가 공격면의 나머지 절반이 됩니다. 하지만 놀랍게도 이것도 파일 내에 존재합니다!

image

.sb 파일 552개가 그냥 텍스트로 디스크에 놓여 있습니다. (deny default) 로 전부 막아놓고 (allow ...) 로 필요한 것만 여는 구조인데, 저희가 볼 건 mach-lookup 하나입니다. “이 이름의 서비스를 찾아서 연결해도 된다”는 뜻이거든요. 누가 누구한테 말을 걸 수 있는지가 글자로 적혀 있는 셈입니다. App Store 앱이 쓰는 application.sb 에서 세봅시다.

image

샌드박스에 갇힌 앱이 연결해도 되는 서비스가 176개. 그럼 그 176개 뒤에 뭐가 있느냐, 그것도 파일 하나에 다 있습니다. /System/Library/xpc/launchd.plist 를 까보면 LaunchDaemon 917개(그중 830개가 root)가 mach 서비스 2,028개를 등록해놓고 있습니다. 리눅스 명령어 딸~깍으로 이 많은 정보를 알 수 있다니, 이것 참 간편하군요! ㅎㅎ

5) 이 모든 걸 곱하면? → 타겟 목록이 나온다!

이제 4)에서 뽑은 것들을 곱해봅시다. 176개를 launchd 등록 정보에 걸어서 실물 바이너리를 찾고, 그 바이너리의 엔타이틀먼트까지 읽어본 결과입니다.

샌드박스 앱이 닿을 수 있는 서비스              : 176
 └ launchd 에 실물 데몬으로 등록된 것           : 140
    └ 그중 root 로 도는 것                      : 132
       └ 뒤에 있는 서로 다른 root 바이너리       :  99
          ├ TCC 계열 엔타이틀먼트 보유           :  56
          └ rootless(SIP) 계열 보유              :  23

176에서 140으로 줄어든 건 나머지가 LaunchAgent나 프레임워크 안에 들어 있는 서비스라 이번 계산에선 뺐기 때문입니다. 그러니까 저건 오히려 적게 센 숫자입니다.

  • 샌드박스에 갇힌 앱 하나가 root로 도는 바이너리 99개한테 말을 걸 수 있고, 그중 56개는 동의창을 건너뛸 수 있는 TCC 권한을, 23개는 SIP 영역을 건드릴 수 있는 권한을 들고 있다!
  • 그리고 여기까지 오는 데 쓴 건 codesign 과 grep, 파이썬 몇 줄이 전부라는 것. 디버거도, root도, 실행도 없다는 점!

여기까지가 Part 0에서 하고 싶었던 얘기의 전부입니다. 원래 시작이 반이라고들 하니까요! ㅎㅎ 그리고 이 99개를 손에 들고 나면 길이 두 갈래로 갈립니다. 바로 로직 버그와 메모리 커럽션입니다.

2. Logical Bug in macOS

macOS에서의 로직 버그는 전형적인 형태가 confused deputy입니다. 권한 없는 제가, 권한 있는 놈을 시켜서 제가 못 하는 일을 대신 하게 만드는 거죠. 도닦기에서 샌드박스 탈출할 때 썼던 그 구조 그대로입니다. 위의 99개가 전부 후보라고 할 수 있겠죠?

Apple에서 주로 로직 버그로 판단되는 취약점은 이렇습니다.

경계 넘으면 뭐가 되나 타겟 벡터중 어느 것인지?
TCC 동의창 없이 화면 녹화·마이크·~/Library/Messages TCC 계열 140개
App Sandbox 감옥 밖에서 내 코드가 돈다. 보통 체인의 두 번째 칸 mach-lookup 176개
root 시스템 전체 쓰기, 데몬 교체, 지속성 root 바이너리 99개
SIP 플랫폼 파일 수정, kext 로드, 남의 앱 데이터 rootless 계열 93개
Gatekeeper 서명 안 된 코드가 경고 없이 실행됨 파일을 만들며 quarantine 을 안 붙이는 놈

SIP가 따로 한 줄인 게 중요합니다. root를 먹어도 끝이 아닙니다. 이런 점도 Windows와는 다르죠? 아까 저를 막았던 그 SIP가 root조차 막고 있으니까요. macOS에서는 root 와 SIP 우회가 별개의 칸입니다.

그럼 실제로는 어디서 터지나. 제가 읽은 공개 사례들은 대부분 이 다섯 안에 들어갔습니다.

패턴 무슨 일이 일어나나
① 클라이언트 검증 누락 XPC 연결 수락 핸들러가 무조건 YES ⇒ 누구나 그 인터페이스를 부름
② PID 로 상대를 확인 PID가 재사용. 요청을 보낸 뒤 내 프로세스를 허용된 바이너리로 갈아치우면 서버는 바뀐 쪽을 검사함 (audit token 을 써야 하는 이유)
③ 경로를 신뢰 내가 준 경로를 그대로 씀 ⇒ 경로 탈출, 링크로 검사 시점과 사용 시점을 벌리는 TOCTOU
④ quarantine 을 안 붙임 특권 서비스가 내 파일을 풀거나 만들며 격리 속성을 빼먹음 ⇒ Gatekeeper 우회
⑤ 프로세스 주입 DYLD_INSERT_LIBRARIES, dylib 하이재킹, get-task-allow ⇒ 남의 권한으로 내 코드 실행

전부 메모리를 건든다기 보단 정상 코드를 권한 검증 누락으로 인해 다른 행위를 할 수 있게끔 만드는 공통점이 있다고 볼 수 있겠죠?

3. Memory Corruption in macOS

Windows에서 Heap overflow나 OOBW 하나 찾으면 그 다음 생각은 정해져 있었죠. “오 이거 익스 되겠는데?” 근데 Apple Silicon에서는 그 직감이 통하질 않습니다… 하드웨어가 두 겹으로 막고 있거든요.

첫 번째가 PAC(Pointer Authentication) 입니다. 포인터의 안 쓰는 상위 비트에 암호학적 서명을 박아두고, 쓰기 전에 검증하는 기능이에요. C++ 가상 함수 호출을 까보면 이렇게 생겼습니다.

000000018079a528	ldr	x16, [x0]                   ; 객체에서 vtable 포인터를 읽고
000000018079a52c	mov	x17, x0                     ; 모디파이어 = 그 포인터가 놓인 주소
000000018079a530	movk	x17, #0x634f, lsl #48       ;   + 클래스별 상수를 섞어서
000000018079a534	autda	x16, x17                    ; vtable 포인터를 인증한다   <- 1차 관문
000000018079a538	ldr	x8, [x16, #0x10]!           ; 슬롯에서 함수 포인터를 꺼낸다
                                                    ;   (! = pre-index, x16 도 슬롯 주소로 갱신)
000000018079a53c	movk	x16, #0x4444, lsl #48       ; 모디파이어 = 슬롯 주소 + 함수별 상수
000000018079a540	blraa	x8, x16                     ; 함수 포인터를 인증하며 호출   <- 2차 관문

#0x634f 가 클래스마다 다른 상수(discriminator)입니다. 그래서 heap overflow 로 옆 객체의 vtable 포인터를 덮어봐야, 제가 쓴 값에는 서명이 안 붙어 있으니 autda 에서 바로 죽습니다. Windows에서 vtable 덮고 신나게 태우던 그 감각이 여기선 안 통해요. (서명을 위조할 수 있으면 그건 그거대로 대형 취약점!)

두 번째는 MTE(Memory Tagging Extension) 입니다. 할당마다 태그를 붙여놓고 접근할 때 하드웨어가 태그를 대조하는데, OOB 로 옆 할당을 건드리면 태그가 안 맞아서 그 자리에서 터집니다. 다만 태그가 붙는 건 작은 할당까지고, 그보다 큰 할당엔 태그가 아예 없습니다. 애플이 MIE(Memory Integrity Enforcement) 라고 부르는 게 이거고요. 실제로 확인해보면 전부 활성화 되어 있는 걸 확인할 수 있습니다.

image

밑에 같이 뽑아본 부팅 순서도 재밌습니다. SPTM → SK → TXM → XNU, 우리가 아는 그 커널이 네 번째로 뜹니다. SPTM이 페이지 테이블 변경을 독점하고 TXM이 코드 서명 판정을 들고 있어서, 커널에서 코드 실행을 얻어도 위에 세 겹이 더 남아 있다는 뜻이죠. Windows에서 커널이 사실상 끝이었던 거랑은 많이 다르지 않나요? 우하하..

그래서 macOS에서는 버그를 찾은 다음이 아니라 찾기 전에 이 질문을 먼저 던지게 됩니다. “내가 덮은 이 값이 결국 뭘로 쓰이는가?”

그 값이 결국… 판정
함수 포인터로 불린다 (vtable 등) ❌ PAC 에서 죽음. 시간 쓰지 말 것
데이터로만 읽힌다 (정수·길이·인덱스) ✅ 살아있음
버퍼 포인터·쓰기·liveness 로 귀결된다 ✅ 살아있음

실제로 2026년 5월 MIE 걸린 M5 맥에서 나온 첫 공개 커널 익스플로잇도 코드 포인터를 안 건드리는 data-only 방식이었습니다. 그러니까 여기선 heap overflow 를 찾았다고 끝이 아니라, 그게 뭘 덮는지까지 봐야 값어치가 정해지는 셈이죠. 그럼 어디서 찾느냐. 1장에서 뽑은 목록에서 바이트를 파싱하는 놈을 고르면 됩니다. 이미지·폰트·비디오 파서, 그리고 항상 소켓을 열고 도는 데몬들이 그 자리예요.

4. Apple Security Bounty — ASB

지금까지 나온 경계들에 우선순위를 매기는 가장 확실한 방법이 있습니다. 애플이 직접 값을 매겨서 공개해놨거든요!! 물론 그림의 떡이지만.. 이런 카테고리 표를 보는 것만으로도 Apple이 원하는 취약점이 어떤 것인지를 짐작할 수 있겠죠?

https://security.apple.com/bounty/categories/

5. 마치며

Windows에서 사용했던 여러 로직들인 — IPC 연결을 보는 것, 권한 경계를 보는 것, 파서를 보는 것 — 사실 macOS에서도 어느정도 결이 비슷한게 많습니다. 다만 방법론이 조금 다를 뿐이죠.

앞으로의 계획은 로직 버그부터 시작해 메모리 커럽션, 마지막엔 커널까지도 공부하며 macOS의 발톱 때를 벗겨 보겠습니다!! 음하하~

긴 글 읽어주셔서 감사하고 다음 편에서 뵙겠습니다! 🐺