macOS IPC - Inter Process Communication
포트를 통한 Mach 메시징
기본 정보
Mach는 작업을 리소스 공유의 가장 작은 단위로 사용하며, 각 작업에는 여러 스레드가 포함될 수 있습니다. 이러한 작업과 스레드는 1:1로 POSIX 프로세스와 스레드에 매핑됩니다.
작업 간 통신은 Mach Inter-Process Communication (IPC)을 통해 이루어지며, 커널이 관리하는 메시지 큐처럼 작동하는 포트 간에 메시지가 전송됩니다.
포트는 Mach IPC의 기본 요소입니다. 메시지를 보내고 받는 데 사용할 수 있습니다.
각 프로세스에는 IPC 테이블이 있어 프로세스의 mach 포트를 찾을 수 있습니다. Mach 포트의 이름은 실제로 숫자(커널 객체에 대한 포인터)입니다.
프로세스는 또한 다른 작업에게 일부 권한을 가진 포트 이름을 보낼 수 있으며, 커널은 이를 다른 작업의 IPC 테이블에 등록합니다.
포트 권한
작업이 수행할 수 있는 작업을 정의하는 포트 권한은 이 통신에 중요합니다. 가능한 포트 권한은 (여기에서 정의된 내용):
수신 권한은 포트로 전송된 메시지를 수신할 수 있게 합니다. Mach 포트는 MPSC (다중 생산자, 단일 소비자) 큐이므로 전체 시스템에서 각 포트에 대해 하나의 수신 권한만 있을 수 있습니다 (여러 프로세스가 하나의 파이프의 읽기 끝에 대한 파일 디스크립터를 모두 보유할 수 있는 파이프와는 달리).
수신 권한을 가진 작업은 메시지를 수신하고 송신 권한을 생성할 수 있어 메시지를 보낼 수 있습니다. 처음에는 자체 작업만 수신 권한을 가집니다.
수신 권한의 소유자가 죽거나 종료하면 송신 권한이 무용지물이 됩니다 (죽은 이름).
송신 권한은 포트로 메시지를 보낼 수 있게 합니다.
송신 권한은 복제될 수 있어 송신 권한을 가진 작업이 권한을 복제하고 제3의 작업에게 부여할 수 있습니다.
포트 권한은 Mac 메시지를 통해 전달될 수도 있습니다.
한 번 송신 권한은 포트로 한 번의 메시지를 보낼 수 있고 그 후 사라집니다.
이 권한은 복제될 수 없지만 이동될 수 있습니다.
포트 집합 권한은 단일 포트가 아닌 _포트 세트_를 나타냅니다. 포트 세트에서 메시지를 디큐하는 것은 그 포트가 포함하는 포트 중 하나에서 메시지를 디큐합니다. 포트 세트는 Unix의
select
/poll
/epoll
/kqueue
와 매우 유사하게 여러 포트에서 동시에 수신할 수 있습니다.죽은 이름은 실제 포트 권한이 아니라 단순히 자리 표시자입니다. 포트가 파괴되면 포트에 대한 모든 기존 포트 권한이 죽은 이름으로 변합니다.
작업은 다른 작업에게 송신 권한을 전달하여 메시지를 다시 보낼 수 있습니다. 송신 권한은 복제될 수 있어 작업이 권한을 복제하고 제3의 작업에게 권한을 부여할 수 있습니다. 이는 부트스트랩 서버라는 중간 프로세스와 결합되어 작업 간 효과적인 통신을 가능하게 합니다.
파일 포트
파일 포트는 Mac 포트(맥 포트 권한을 사용)에 파일 디스크립터를 캡슐화할 수 있습니다. 주어진 FD에서 fileport_makeport
를 사용하여 fileport
를 만들고 fileport_makefd
를 사용하여 fileport에서 FD를 만들 수 있습니다.
통신 설정
이전에 언급했듯이 Mach 메시지를 사용하여 권한을 보낼 수 있지만, 이미 Mach 메시지를 보낼 권한이 없는 경우 권한을 보내려면 이미 권한이 있어야 합니다. 그렇다면 첫 번째 통신은 어떻게 설정됩니까?
이를 위해 부트스트랩 서버(mac의 launchd)가 관여됩니다. 누구나 부트스트랩 서버에 SEND 권한을 얻을 수 있으므로, 다른 프로세스에 메시지를 보낼 권한을 요청할 수 있습니다:
작업 A는 새 포트를 생성하여 그것에 대한 수신 권한을 얻습니다.
수신 권한을 보유한 작업 A는 포트에 대한 송신 권한을 생성합니다.
작업 A는 부트스트랩 서버와 연결을 설정하고, 처음에 생성한 포트에 대한 송신 권한을 부트스트랩 서버에 보냅니다.
누구나 부트스트랩 서버에 SEND 권한을 얻을 수 있습니다.
작업 A는 부트스트랩 서버에
bootstrap_register
메시지를 보내 **com.apple.taska
**와 같은 이름으로 지정된 포트를 연결합니다.작업 B는 서비스 이름에 대한 부트스트랩 룩업을 실행하기 위해 부트스트랩 서버와 상호 작용합니다 (
bootstrap_lookup
). 따라서 부트스트랩 서버가 응답하려면 작업 B는 룩업 메시지 내에서 이전에 생성한 포트에 대한 SEND 권한을 부트스트랩 서버에 보냅니다. 룩업이 성공하면 서버는 Task A로부터 받은 SEND 권한을 복제하고 Task B에게 전송합니다.
누구나 부트스트랩 서버에 SEND 권한을 얻을 수 있습니다.
이 SEND 권한으로 작업 B는 작업 A에게 메시지를 보낼 수 있습니다.
양방향 통신을 위해 일반적으로 작업 B는 수신 권한과 송신 권한이 있는 새 포트를 생성하고 송신 권한을 작업 A에게 제공하여 작업 B에게 메시지를 보낼 수 있게 합니다 (양방향 통신).
부트스트랩 서버는 작업이 주장하는 서비스 이름을 인증할 수 없습니다. 이는 작업이 잠재적으로 시스템 작업을 가장할 수 있음을 의미합니다. 예를 들어 권한 서비스 이름을 가장하여 모든 요청을 승인할 수 있습니다.
그런 다음 Apple은 시스템 제공 서비스의 이름을 안전한 구성 파일에 저장합니다. 이 파일은 SIP로 보호된 디렉토리인 /System/Library/LaunchDaemons
및 /System/Library/LaunchAgents
에 있습니다. 각 서비스 이름 옆에는 관련된 이진 파일도 저장됩니다. 부트스트랩 서버는 이러한 서비스 이름마다 수신 권한을 생성하고 보유합니다.
이러한 사전 정의된 서비스에 대해서는 룩업 프로세스가 약간 다릅니다. 서비스 이름이 조회될 때, launchd는 서비스를 동적으로 시작합니다. 새로운 워크플로우는 다음과 같습니다:
작업 B는 서비스 이름에 대한 부트스트랩 룩업을 시작합니다.
launchd는 작업이 실행 중인지 확인하고 실행 중이 아니면 시작합니다.
작업 A (서비스)는 부트스트랩 체크인(
bootstrap_check_in()
)을 수행합니다. 여기서 부트스트랩 서버는 SEND 권한을 생성하고 보유하며 수신 권한을 작업 A에게 전달합니다.launchd는 SEND 권한을 복제하고 작업 B에게 전송합니다.
작업 B는 수신 권한과 송신 권한이 있는 새 포트를 생성하고 송신 권한을 작업 A에게 제공하여 작업 B에게 메시지를 보낼 수 있게 합니다 (양방향 통신).
그러나 이 프로세스는 사전 정의된 시스템 작업에만 적용됩니다. 비시스템 작업은 여전히 처음에 설명한 대로 작동하며, 이는 가장할 수 있는 가능성을 열어둘 수 있습니다.
따라서 launchd가 절대로 충돌해서는 안 되며, 그렇게 되면 전체 시스템이 충돌합니다.
Mach 메시지
mach_msg
함수는 본질적으로 시스템 호출로, Mach 메시지를 보내고 받기 위해 사용됩니다. 이 함수는 보내야 하는 메시지를 초기 인자로 필요로 합니다. 이 메시지는 mach_msg_header_t
구조체로 시작해야 하며 실제 메시지 내용이 뒤따라야 합니다. 이 구조체는 다음과 같이 정의됩니다:
프로세스가 _수신 권한을 보유하면 Mach 포트에서 메시지를 수신할 수 있습니다. 반대로 보내는 쪽(sender)은 _송신 권한 또는 _일회용 송신 권한_을 부여받습니다. 일회용 송신 권한은 한 번의 메시지를 보낸 후에 무효화됩니다.
초기 필드 **msgh_bits
**는 비트맵입니다:
첫 번째 비트(가장 중요함)는 메시지가 복잡함을 나타내는 데 사용됩니다(자세한 내용은 아래 참조)
3번째와 4번째 비트는 커널에 의해 사용됩니다
두 번째 바이트의 가장 낮은 5개 비트는 **바우처(voucher)**에 사용할 수 있습니다: 키/값 조합을 보내는 또 다른 유형의 포트입니다.
세 번째 바이트의 가장 낮은 5개 비트는 로컬 포트에 사용할 수 있습니다
네 번째 바이트의 가장 낮은 5개 비트는 원격 포트에 사용할 수 있습니다
바우처, 로컬 및 원격 포트에 지정할 수 있는 유형은 다음과 같습니다(mach/message.h 참조):
예를 들어, MACH_MSG_TYPE_MAKE_SEND_ONCE
는 이 포트를 위해 파생 및 전송되어야 하는 한 번만 보내기 권한을 나타내는 데 사용될 수 있습니다. 또한 수신자가 응답을 보낼 수 없도록 하려면 MACH_PORT_NULL
을 지정할 수 있습니다.
쉬운 양방향 통신을 위해 프로세스는 메시지 헤더의 응답 포트(msgh_local_port
)라고 불리는 mach 포트를 지정할 수 있으며, 메시지의 수신자는 이 메시지에 대한 응답을 보낼 수 있습니다.
이러한 종류의 양방향 통신은 응답을 기대하는 XPC 메시지에서 사용되며 (xpc_connection_send_message_with_reply
및 xpc_connection_send_message_with_reply_sync
), 일반적으로 서로 다른 포트가 생성되어 양방향 통신을 생성하는 방법에 대해 이전에 설명한 대로 사용됩니다.
메시지 헤더의 다른 필드는 다음과 같습니다:
msgh_size
: 전체 패킷의 크기.msgh_remote_port
: 이 메시지가 전송된 포트.msgh_voucher_port
: mach 바우처.msgh_id
: 수신자가 해석하는 이 메시지의 ID.
mach 메시지는 mach 포트
를 통해 전송되며, 이는 mach 커널에 내장된 단일 수신자, 다중 송신자 통신 채널입니다. 여러 프로세스가 mach 포트로 메시지를 보낼 수 있지만, 언제든지 단일 프로세스만 읽을 수 있습니다.
그런 다음 메시지는 mach_msg_header_t
헤더, 바디 및 트레일러(있는 경우)로 형성되며, 응답 권한을 부여할 수 있습니다. 이러한 경우에는 커널이 메시지를 한 작업에서 다른 작업으로 전달하기만 하면 됩니다.
트레일러는 커널에 의해 메시지에 추가된 정보로(사용자가 설정할 수 없음) 메시지 수신 시 MACH_RCV_TRAILER_<trailer_opt>
플래그로 요청할 수 있습니다(요청할 수 있는 다양한 정보가 있음).
복잡한 메시지
그러나 커널이 수신자에게 이러한 객체를 전송해야 하는 추가 포트 권한이나 메모리 공유와 같은 더 복잡한 메시지도 있습니다. 이러한 경우에는 헤더 msgh_bits
의 가장 상위 비트가 설정됩니다.
전달할 수 있는 가능한 디스크립터는 mach/message.h
에서 정의됩니다.
Mac Ports APIs
포트는 작업 네임스페이스에 연결되어 있으므로 포트를 생성하거나 검색하려면 작업 네임스페이스도 쿼리됩니다 (mach/mach_port.h
에서 자세히 설명):
mach_port_allocate
|mach_port_construct
: 포트를 생성합니다.mach_port_allocate
는 포트 세트를 생성할 수도 있습니다: 포트 그룹에 대한 수신 권한. 메시지를 수신할 때 어디서 메시지가 왔는지 표시됩니다.mach_port_allocate_name
: 포트의 이름을 변경합니다 (기본적으로 32비트 정수).mach_port_names
: 대상에서 포트 이름 가져오기mach_port_type
: 이름에 대한 작업의 권한 가져오기mach_port_rename
: 포트의 이름 바꾸기 (FD의 dup2와 유사)mach_port_allocate
: 새로운 RECEIVE, PORT_SET 또는 DEAD_NAME 할당mach_port_insert_right
: 수신할 수 있는 포트에 새로운 권한 생성mach_port_...
mach_msg
|mach_msg_overwrite
: mach 메시지를 보내고 받는 데 사용되는 함수입니다. 덮어쓰기 버전은 메시지 수신을 위한 다른 버퍼를 지정할 수 있습니다 (다른 버전은 그냥 재사용합니다).
Debug mach_msg
함수 **mach_msg
**와 **mach_msg_overwrite
**는 메시지를 보내고 받는 데 사용되는 함수이므로 이러한 함수에 중단점을 설정하면 보낸 메시지와 받은 메시지를 검사할 수 있습니다.
예를 들어 디버깅할 수 있는 모든 응용 프로그램을 시작하면 이 함수를 사용할 것이므로 libSystem.B
를 로드할 것입니다.
**mach_msg
**의 인수를 얻으려면 레지스터를 확인하십시오. 이것이 인수들입니다 (mach/message.h 참조):
레지스트리에서 값을 가져옵니다:
메시지 헤더를 검사하여 첫 번째 인수를 확인하십시오:
그 유형의 mach_msg_bits_t
는 응답을 허용하는 데 매우 일반적입니다.
포트 나열
이름은 포트에 기본적으로 지정된 이름입니다 (첫 3바이트에서 증가하는 방법을 확인하십시오). **ipc-object
**는 포트의 가려진 고유 식별자입니다.
또한 send
권한만 있는 포트는 해당 소유자를 식별하는 데 사용됨을 주목하십시오 (포트 이름 + pid).
또한 **+
**를 사용하여 동일한 포트에 연결된 다른 작업을 나타내는 방법에 주목하십시오.
또한 procesxp를 사용하여 등록된 서비스 이름도 볼 수 있습니다 (SIP가 비활성화되어 있어 com.apple.system-task-port
가 필요한 경우):
이 도구를 iOS에 설치하려면 http://newosxbook.com/tools/binpack64-256.tar.gz에서 다운로드할 수 있습니다.
코드 예시
sender가 포트를 할당하고 org.darlinghq.example
이름에 대한 send right를 생성하여 이를 부트스트랩 서버에 보낸 것을 주목하십시오. 수신자는 해당 이름에 대한 send right를 요청하고 이를 사용하여 메시지를 보냈습니다.
sender.c 파일
특권 포트
일부 특별한 포트는 작업이 해당 포트에 대한 SEND 권한을 갖는 경우 특정 민감한 작업을 수행하거나 특정 민감한 데이터에 액세스할 수 있도록 합니다. 이는 이러한 포트가 공격자의 관점에서 매우 흥미로울 뿐만 아니라 작업 간에 SEND 권한을 공유할 수 있기 때문입니다.
호스트 특수 포트
이러한 포트는 숫자로 표시됩니다.
SEND 권한은 **host_get_special_port
**를 호출하여 얻을 수 있으며 RECEIVE 권한은 **host_set_special_port
**를 호출하여 얻을 수 있습니다. 그러나 두 호출 모두 루트만 액세스할 수 있는 host_priv
포트가 필요합니다. 또한 과거에 루트가 **host_set_special_port
**를 호출하고 임의로 탈취할 수 있었으며, 예를 들어 HOST_KEXTD_PORT
를 탈취하여 코드 서명을 우회할 수 있었습니다 (SIP가 이를 방 지하도록 변경되었습니다).
이러한 포트는 2개의 그룹으로 나뉩니다: 첫 7개 포트는 커널이 소유하며 1은 HOST_PORT
, 2는 HOST_PRIV_PORT
, 3은 HOST_IO_MASTER_PORT
이고 7은 HOST_MAX_SPECIAL_KERNEL_PORT
입니다.
8부터 시작하는 포트는 시스템 데몬이 소유하며 host_special_ports.h
에서 선언된 포트를 찾을 수 있습니다.
호스트 포트: 프로세스가 이 포트에 대한 SEND 권한을 갖는 경우 시스템에 대한 정보를 얻을 수 있으며 다음과 같은 루틴을 호출할 수 있습니다:
host_processor_info
: 프로세서 정보 가져오기host_info
: 호스트 정보 가져오기host_virtual_physical_table_info
: 가상/물리 페이지 테이 블 (MACH_VMDEBUG 필요)host_statistics
: 호스트 통계 가져오기mach_memory_info
: 커널 메모리 레이아웃 가져오기호스트 Priv 포트: 이 포트에 대한 SEND 권한을 갖는 프로, 세스는 부팅 데이터를 표시하거나 커널 확장을 로드하려는 특권 작업을 수행할 수 있습니다. 이 권한을 얻으려면 프로세스가 루트여야합니다.
또한
kext_request
API를 호출하려면 Apple 이진 파일에만 제공되는com.apple.private.kext*
기타 엔티틀먼트가 필요합니다.호출할 수 있는 다른 루틴은 다음과 같습니다:
host_get_boot_info
:machine_boot_info()
가져오기host_priv_statistics
: 특권 통계 가져오기vm_allocate_cpm
: 연속 물리 메모리 할당host_processors
: 호스트 프로세서에 대한 SEND 권한mach_vm_wire
: 메모리 상주화루트가 이 권한에 액세스할 수 있으므로
host_set_[special/exception]_port[s]
를 호출하여 호스트 특수 또는 예외 포트를 탈취할 수 있습니다.
모든 호스트 특수 포트를 볼 수 있습니다.
특별 포트 작업
이 포트들은 잘 알려진 서비스를 위해 예약된 포트들입니다. task_[get/set]_special_port
를 호출하여 이러한 포트들을 가져오거나 설정할 수 있습니다. 이러한 포트들은 task_special_ports.h
에서 찾을 수 있습니다:
여기에서:
TASK_KERNEL_PORT[task-self send right]: 이 작업을 제어하는 데 사용되는 포트. 작업에 영향을 주는 메시지를 보내기 위해 사용됩니다. 이는 **mach_task_self (아래의 작업 포트 참조)**에 의해 반환된 포트입니다.
TASK_BOOTSTRAP_PORT[bootstrap send right]: 작업의 부트스트랩 포트. 다른 시스템 서비스 포트의 반환을 요청하는 메시지를 보내기 위해 사용됩니다.
TASK_HOST_NAME_PORT[host-self send right]: 포함된 호스트의 정보를 요청하는 데 사용되는 포트. 이는 mach_host_self에 의해 반환된 포트입니다.
TASK_WIRED_LEDGER_PORT[ledger send right]: 이 작업이 유선 커널 메모리를 가져오는 소스를 명명하는 포트.
TASK_PAGED_LEDGER_PORT[ledger send right]: 이 작업이 기본 메모리 관리 메모리를 가져오는 소스를 명명하는 포트.
작업 포트
원래 Mach에는 "프로세스"가 아닌 "작업"이 있었으며 이는 스레드의 컨테이너처럼 고려되었습니다. Mach가 BSD와 병합될 때 각 작업은 BSD 프로세스와 관련되었습니다. 따라서 모든 BSD 프로세스에는 프로세스로서 필요한 세부 정보가 있고 모든 Mach 작업에도 내부 작업이 있습니다 (커널 작업인 존재하지 않는 pid 0인 경우를 제외).
이와 관련된 두 가지 매우 흥미로운 함수가 있습니다:
task_for_pid(target_task_port, pid, &task_port_of_pid)
:pid
로 지정된 작업과 관련된 작업 포트에 대한 SEND 권한을 얻고 일반적으로mach_task_self()
를 사용한 호출자 작업인target_task_port
에 제공합니다. (다른 작업에 대한 SEND 포트일 수 있음.)pid_for_task(task, &pid)
: 작업에 대한 SEND 권한이 있는 경우 해당 작업이 관련된 PID를 찾습니다.
작업 내에서 작업을 수행하려면 mach_task_self()
를 호출하여 자체에 대한 SEND
권한이 필요했습니다 (task_self_trap
(28)을 사용함). 이 권한을 통해 작업은 다음과 같은 여러 작업을 수행할 수 있습니다:
task_threads
: 작업의 스레드의 모든 작업 포트에 대한 SEND 권한 가져오기task_info
: 작업에 대한 정보 가져오기task_suspend/resume
: 작업 일시 중지 또는 재개task_[get/set]_special_port
thread_create
: 스레드 생성task_[get/set]_state
: 작업 상태 제어그리고 더 많은 것은 mach/task.h에서 찾을 수 있습니다.
다른 작업의 작업 포트에 대한 SEND 권한이 있는 경우 다른 작업에서 이러한 작업을 수행할 수 있습니다.
또한, 작업 포트는 vm_map
포트이기도 하며 vm_read()
및 vm_write()
와 같은 함수를 사용하여 작업 내부의 메모리를 읽고 조작할 수 있습니다. 이는 기본적으로 다른 작업의 작업 포트에 대한 SEND 권한이 있는 작업이 해당 작업에 코드를 삽입할 수 있음을 의미합니다.
커널도 작업이기 때문에 누군가가 kernel_task
에 대한 SEND 권한을 획들하면 커널이 아무 것이나 실행할 수 있게 됩니다 (탈옥).
**
mach_task_self()
를 호출하여 호출자 작업에 대한 이 포트의 **이름을 가져옵니다. 이 포트는exec()
를 통해만 상속됩니다.fork()
로 생성된 새 작업은 새 작업 포트를 받습니다 (exec()
이후 suid 이진 파일에서도 새 작업 포트를 받습니다). 작업을 생성하고 해당 포트를 가져오는 유일한 방법은fork()
를 수행하는 동안 "port swap dance"를 수행하는 것입니다.이 포트에 액세스하는 제한 사항은 이진 파일
AppleMobileFileIntegrity
의macos_task_policy
에서 확인할 수 있습니다:앱이
com.apple.security.get-task-allow
엔터티먼트를 가지고 있으면 동일한 사용자의 프로세스가 작업 포트에 액세스할 수 있습니다 (일반적으로 디버깅을 위해 Xcode에서 추가됨). 인증 프로세스는 프로덕션 릴리스에서 허용하지 않습니다.com.apple.system-task-ports
엔터티먼트를 가진 앱은 커널을 제외한 모든 프로세스의 작업 포트를 가져올 수 있습니다. 이전 버전에서는 **task_for_pid-allow
**로 불렸습니다. 이 권한은 Apple 애플리케이션에만 부여됩니다.루트는 하드닝된 런타임으로 컴파일되지 않은 애플리케이션의 작업 포트에 액세스할 수 있습니다 (Apple에서 제공되지 않음).
작업 이름 포트: _작업 포트_의 권한이 없는 버전입니다. 작업을 참조하지만 제어할 수는 없습니다. 이를 통해 사용 가능한 유일한 것은 task_info()
인 것 같습니다.
스레드 포트
스레드에도 연결된 포트가 있으며 이는 **task_threads
**를 호출하는 작업 및 processor_set_threads
에서 볼 수 있습니다. 스레드 포트에 대한 SEND 권한을 얻으면 thread_act
하위 시스템의 함수를 사용할 수 있습니다:
thread_terminate
thread_[get/set]_state
act_[get/set]_state
thread_[suspend/resume]
thread_info
...
모든 스레드는 **mach_thread_sef
**를 호출하여 이 포트를 얻을 수 있습니다.
작업 포트를 통한 스레드 내 셸코드 삽입
셸코드를 다음에서 가져올 수 있습니다:
Introduction to ARM64v8entitlements.plist
Description
The entitlements.plist
file contains a list of entitlements that are granted to a process. These entitlements define the capabilities and resources that the process is allowed to access on macOS. By modifying the entitlements of a process, an attacker can potentially escalate privileges or abuse inter-process communication (IPC) mechanisms.
Impact
Manipulating the entitlements of a process can lead to privilege escalation, allowing an attacker to perform unauthorized actions or access restricted resources. This can be used as part of an attack chain to gain further access to the system or to bypass security controls.
Detection
Monitoring changes to the entitlements.plist
file and auditing the entitlements granted to processes can help detect unauthorized modifications. Tools like log
and audit
can be used to track changes and identify suspicious activity related to entitlement manipulation.
Mitigation
To mitigate the risk of entitlement abuse, it is important to regularly review and update the entitlements granted to processes. Additionally, restricting the use of sensitive entitlements and implementing least privilege principles can help reduce the impact of potential abuse.
이전 프로그램을 컴파일하고 동일한 사용자로 코드를 주입할 수 있도록 엔터티먼트를 추가하십시오 (그렇지 않으면 sudo를 사용해야 합니다).
Last updated