macOS์—์„œ ํ”„๋กœ์„ธ์Šค ๋ฐ ์ปค๋„ ํ™•์žฅ์„ ๋ณดํ˜ธํ•˜๋Š” ๋ฐฉ๋ฒ•

์•ˆ๋…•, ํ•˜๋ธŒ๋ฅด! ์˜ค๋Š˜์€ macOS์—์„œ ๊ณต๊ฒฉ์ž์˜ ๊ณต๊ฒฉ์œผ๋กœ๋ถ€ํ„ฐ ํ”„๋กœ์„ธ์Šค๋ฅผ ๋ณดํ˜ธํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์ด๋Š” ๋ฐ”์ด๋Ÿฌ์Šค ๋ฐฑ์‹ ์ด๋‚˜ ๋ฐฑ์—… ์‹œ์Šคํ…œ์— ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค. ํŠนํžˆ macOS์—๋Š” ํ”„๋กœ์„ธ์Šค๋ฅผ "์ข…๋ฃŒ"ํ•˜๋Š” ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์ด์— ๋Œ€ํ•œ ๋‚ด์šฉ๊ณผ ์•„๋ž˜์˜ ๋ณดํ˜ธ ๋ฐฉ๋ฒ•์„ ์ฝ์–ด๋ณด์„ธ์š”.

macOS์—์„œ ํ”„๋กœ์„ธ์Šค ๋ฐ ์ปค๋„ ํ™•์žฅ์„ ๋ณดํ˜ธํ•˜๋Š” ๋ฐฉ๋ฒ•

ํ”„๋กœ์„ธ์Šค๋ฅผ "์ข…๋ฃŒ"ํ•˜๋Š” ๊ณ ์ „์ ์ธ ๋ฐฉ๋ฒ•

ํ”„๋กœ์„ธ์Šค๋ฅผ "์ข…๋ฃŒ"ํ•˜๋Š” ์ž˜ ์•Œ๋ ค์ง„ ๋ฐฉ๋ฒ•์€ SIGKILL ์‹ ํ˜ธ๋ฅผ ํ”„๋กœ์„ธ์Šค์— ๋ณด๋‚ด๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. bash๋ฅผ ํ†ตํ•ด ํ‘œ์ค€ "kill -SIGKILL PID" ๋˜๋Š” "pkill -9 NAME"์„ ํ˜ธ์ถœํ•˜์—ฌ ์ข…๋ฃŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. "kill" ๋ช…๋ น์€ UNIX ์‹œ๋Œ€๋ถ€ํ„ฐ ์•Œ๋ ค์ ธ ์™”์œผ๋ฉฐ macOS๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๋‹ค๋ฅธ UNIX ์œ ์‚ฌ ์‹œ์Šคํ…œ์—์„œ๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

UNIX ๊ณ„์—ด ์‹œ์Šคํ…œ๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ macOS์—์„œ๋Š” SIGKILL ๋ฐ SIGSTOP ๋‘ ๊ฐ€์ง€๋ฅผ ์ œ์™ธํ•œ ํ”„๋กœ์„ธ์Šค์— ๋Œ€ํ•œ ๋ชจ๋“  ์‹ ํ˜ธ๋ฅผ ๊ฐ€๋กœ์ฑŒ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ธฐ์‚ฌ์—์„œ๋Š” ์ฃผ๋กœ ํ”„๋กœ์„ธ์Šค๋ฅผ ์ข…๋ฃŒ์‹œํ‚ค๋Š” ์‹ ํ˜ธ์ธ SIGKILL ์‹ ํ˜ธ์— ์ค‘์ ์„ ๋‘˜ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

macOS ์‚ฌ์–‘

macOS์—์„œ๋Š” XNU ์ปค๋„์˜ kill ์‹œ์Šคํ…œ ํ˜ธ์ถœ์ด psignal(SIGKILL,...) ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž ๊ณต๊ฐ„์—์„œ psignal ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋Š” ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž ์ž‘์—…์ด ๋ฌด์—‡์ธ์ง€ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ์ปค๋„์˜ ๋‚ด๋ถ€ ๋ฉ”์ปค๋‹ˆ์ฆ˜์—์„œ psignal ํ•จ์ˆ˜์— ๋Œ€ํ•œ ํ˜ธ์ถœ์„ ์ œ๊ฑฐํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค(๋น„๋ก ์‚ฌ์†Œํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์ง€๋งŒ ๋‹ค๋ฅธ ๊ธฐ์‚ฌ์—์„œ ๋‹ค๋ฃจ๊ฒ ์Šต๋‹ˆ๋‹ค ๐Ÿ™‚ - ์„œ๋ช… ํ™•์ธ, ๋ฉ”๋ชจ๋ฆฌ ์˜ค๋ฅ˜, ์ข…๋ฃŒ/์ข…๋ฃŒ ์ฒ˜๋ฆฌ, ํŒŒ์ผ ๋ณดํ˜ธ ์œ„๋ฐ˜ ๋“ฑ) .

ํ•จ์ˆ˜์™€ ํ•ด๋‹น ์‹œ์Šคํ…œ ํ˜ธ์ถœ๋กœ ๊ฒ€ํ† ๋ฅผ ์‹œ์ž‘ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. terminate_with_payload. ์ „ํ†ต์ ์ธ ์ข…๋ฃŒ ํ˜ธ์ถœ ์™ธ์—๋„ macOS ์šด์˜ ์ฒด์ œ์—๋งŒ ์ ์šฉ๋˜๊ณ  BSD์—๋Š” ์—†๋Š” ๋Œ€์ฒด ์ ‘๊ทผ ๋ฐฉ์‹์ด ์žˆ์Œ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‘ ์‹œ์Šคํ…œ ํ˜ธ์ถœ์˜ ์ž‘๋™ ์›๋ฆฌ๋„ ์œ ์‚ฌํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ์ปค๋„ ํ•จ์ˆ˜ psignal์— ๋Œ€ํ•œ ์ง์ ‘ ํ˜ธ์ถœ์ž…๋‹ˆ๋‹ค. ๋˜ํ•œ ํ”„๋กœ์„ธ์Šค๋ฅผ ์ข…๋ฃŒํ•˜๊ธฐ ์ „์— "cansignal" ๊ฒ€์‚ฌ(ํ”„๋กœ์„ธ์Šค๊ฐ€ ๋‹ค๋ฅธ ํ”„๋กœ์„ธ์Šค์— ์‹ ํ˜ธ๋ฅผ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋Š”์ง€ ์—ฌ๋ถ€)๊ฐ€ ์ˆ˜ํ–‰๋ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ์‹œ์Šคํ…œ์€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์‹œ์Šคํ…œ ํ”„๋กœ์„ธ์Šค๋ฅผ ์ข…๋ฃŒํ•˜๋Š” ๊ฒƒ์„ ํ—ˆ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

static int
terminate_with_payload_internal(struct proc *cur_proc, int target_pid, uint32_t reason_namespace,
				uint64_t reason_code, user_addr_t payload, uint32_t payload_size,
				user_addr_t reason_string, uint64_t reason_flags)
{
...
	target_proc = proc_find(target_pid);
...
	if (!cansignal(cur_proc, cur_cred, target_proc, SIGKILL)) {
		proc_rele(target_proc);
		return EPERM;
	}
...
	if (target_pid == cur_proc->p_pid) {
		/*
		 * psignal_thread_with_reason() will pend a SIGKILL on the specified thread or
		 * return if the thread and/or task are already terminating. Either way, the
		 * current thread won't return to userspace.
		 */
		psignal_thread_with_reason(target_proc, current_thread(), SIGKILL, signal_reason);
	} else {
		psignal_with_reason(target_proc, SIGKILL, signal_reason);
	}
...
}

๋Ÿฐ์นญ

์‹œ์Šคํ…œ ์‹œ์ž‘ ์‹œ ๋ฐ๋ชฌ์„ ์ƒ์„ฑํ•˜๊ณ  ์ˆ˜๋ช…์„ ์ œ์–ดํ•˜๋Š” โ€‹โ€‹ํ‘œ์ค€ ๋ฐฉ๋ฒ•์€ launchd์ž…๋‹ˆ๋‹ค. ์†Œ์Šค๋Š” macOS 10.10๊นŒ์ง€์˜ ์ด์ „ ๋ฒ„์ „์˜ launchctl์šฉ์ด๋ฉฐ ์ฝ”๋“œ ์˜ˆ์ œ๋Š” ์„ค๋ช… ๋ชฉ์ ์œผ๋กœ ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค. ์ตœ์‹  launchctl์€ XPC๋ฅผ ํ†ตํ•ด launchd ์‹ ํ˜ธ๋ฅผ ๋ณด๋‚ด๊ณ , launchctl ๋กœ์ง์ด XPC๋กœ ์ด๋™๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์ •ํ™•ํžˆ ์–ด๋–ป๊ฒŒ ์ค‘์ง€๋˜๋Š”์ง€ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. SIGTERM ์‹ ํ˜ธ๋ฅผ ๋ณด๋‚ด๊ธฐ ์ „์— "proc_terminate" ์‹œ์Šคํ…œ ํ˜ธ์ถœ์„ ์‚ฌ์šฉํ•˜์—ฌ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ค‘์ง€ํ•˜๋ ค๊ณ  ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค.

<launchctl src/core.c>
...
	error = proc_terminate(j->p, &sig);
	if (error) {
		job_log(j, LOG_ERR | LOG_CONSOLE, "Could not terminate job: %d: %s", error, strerror(error));
		job_log(j, LOG_NOTICE | LOG_CONSOLE, "Using fallback option to terminate job...");
		error = kill2(j->p, SIGTERM);
		if (error) {
			job_log(j, LOG_ERR, "Could not signal job: %d: %s", error, strerror(error));
		} 
...
<>

๋‚ด๋ถ€์ ์œผ๋กœ proc_terminate๋Š” ์ด๋ฆ„์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  SIGTERM์„ ์‚ฌ์šฉํ•˜์—ฌ psignal๋ฟ๋งŒ ์•„๋‹ˆ๋ผ SIGKILL๋„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฐ„์ ‘ ์‚ดํ•ด - ์ž์› ์ œํ•œ

๋” ํฅ๋ฏธ๋กœ์šด ์‚ฌ๋ก€๋Š” ๋‹ค๋ฅธ ์‹œ์Šคํ…œ ํ˜ธ์ถœ์—์„œ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ”„๋กœ์„ธ์Šค_์ •์ฑ…. ์ด ์‹œ์Šคํ…œ ํ˜ธ์ถœ์˜ ์ผ๋ฐ˜์ ์ธ ์šฉ๋„๋Š” ํŒŒ์ผ ์บ์‹ฑ ํ™œ๋™์œผ๋กœ ์ธํ•ด ์‹œ์Šคํ…œ ์†๋„๊ฐ€ ํฌ๊ฒŒ ๋Š๋ ค์ง€์ง€ ์•Š๋„๋ก ์ธ๋ฑ์„œ๊ฐ€ CPU ์‹œ๊ฐ„ ๋ฐ ๋ฉ”๋ชจ๋ฆฌ ํ• ๋‹น๋Ÿ‰์„ ์ œํ•œํ•˜๋Š” ๋“ฑ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ฆฌ์†Œ์Šค๋ฅผ ์ œํ•œํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. proc_apply_resource_actions ํ•จ์ˆ˜์—์„œ ๋ณผ ์ˆ˜ ์žˆ๋“ฏ์ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ๋ฆฌ์†Œ์Šค ์ œํ•œ์— ๋„๋‹ฌํ•˜๋ฉด SIGKILL ์‹ ํ˜ธ๊ฐ€ ํ”„๋กœ์„ธ์Šค๋กœ ์ „์†ก๋ฉ๋‹ˆ๋‹ค.

์ด ์‹œ์Šคํ…œ ํ˜ธ์ถœ์€ ์ž ์žฌ์ ์œผ๋กœ ํ”„๋กœ์„ธ์Šค๋ฅผ ์ข…๋ฃŒํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ์‹œ์Šคํ…œ์€ ์‹œ์Šคํ…œ ํ˜ธ์ถœ์„ ํ˜ธ์ถœํ•˜๋Š” ํ”„๋กœ์„ธ์Šค์˜ ๊ถŒํ•œ์„ ์ ์ ˆํ•˜๊ฒŒ ํ™•์ธํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ ํ™•์ธ ์ค‘ ์กด์žฌํ–ˆ๋‹ค๊ทธ๋Ÿฌ๋‚˜ ์ด ์กฐ๊ฑด์„ ์šฐํšŒํ•˜๋ ค๋ฉด ๋Œ€์ฒด ํ”Œ๋ž˜๊ทธ PROC_POLICY_ACTION_SET์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์œผ๋กœ ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ CPU ์‚ฌ์šฉ ํ• ๋‹น๋Ÿ‰์„ "์ œํ•œ"(์˜ˆ: 1ns๋งŒ ์‹คํ–‰ํ•˜๋„๋ก ํ—ˆ์šฉ)ํ•˜๋ฉด ์‹œ์Šคํ…œ์˜ ๋ชจ๋“  ํ”„๋กœ์„ธ์Šค๋ฅผ ์ข…๋ฃŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๋งฌ์›จ์–ด๋Š” ๋ฐ”์ด๋Ÿฌ์Šค ๋ฐฑ์‹  ํ”„๋กœ์„ธ์Šค๋ฅผ ํฌํ•จํ•˜์—ฌ ์‹œ์Šคํ…œ์˜ ๋ชจ๋“  ํ”„๋กœ์„ธ์Šค๋ฅผ ์ข…๋ฃŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ ํฅ๋ฏธ๋กœ์šด ์ ์€ pid 1(launchctl)์„ ์‚ฌ์šฉํ•˜์—ฌ ํ”„๋กœ์„ธ์Šค๋ฅผ ์ข…๋ฃŒํ•  ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ํšจ๊ณผ์ž…๋‹ˆ๋‹ค. SIGKILL ์‹ ํ˜ธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋ ค๊ณ  ํ•  ๋•Œ ์ปค๋„ ํŒจ๋‹‰์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

macOS์—์„œ ํ”„๋กœ์„ธ์Šค ๋ฐ ์ปค๋„ ํ™•์žฅ์„ ๋ณดํ˜ธํ•˜๋Š” ๋ฐฉ๋ฒ•

๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•?

ํ”„๋กœ์„ธ์Šค๊ฐ€ ์ข…๋ฃŒ๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•˜๋Š” ๊ฐ€์žฅ ๊ฐ„๋‹จํ•œ ๋ฐฉ๋ฒ•์€ ์‹œ์Šคํ…œ ํ˜ธ์ถœ ํ…Œ์ด๋ธ”์˜ ํ•จ์ˆ˜ ํฌ์ธํ„ฐ๋ฅผ ๋ฐ”๊พธ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋ถˆํ–‰ํ•˜๊ฒŒ๋„ ์ด ๋ฐฉ๋ฒ•์€ ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ์ด์œ ๋กœ ์‚ฌ์†Œํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ฒซ์งธ, sysent์˜ ๋ฉ”๋ชจ๋ฆฌ ์œ„์น˜๋ฅผ ์ œ์–ดํ•˜๋Š” โ€‹โ€‹๊ธฐํ˜ธ๋Š” XNU ์ปค๋„ ๊ธฐํ˜ธ์—๋งŒ ํ•ด๋‹น๋˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ์ปค๋„ ๊ธฐํ˜ธ์—์„œ๋„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ํ•จ์ˆ˜๋ฅผ ๋™์ ์œผ๋กœ ๋ถ„ํ•ดํ•˜๊ณ  ๊ทธ ์•ˆ์˜ ํฌ์ธํ„ฐ๋ฅผ ๊ฒ€์ƒ‰ํ•˜๋Š” ๋“ฑ ๊ฒฝํ—˜์  ๊ฒ€์ƒ‰ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋‘˜์งธ, ํ…Œ์ด๋ธ”์˜ ํ•ญ๋ชฉ ๊ตฌ์กฐ๋Š” ์ปค๋„์ด ์ปดํŒŒ์ผ๋œ ํ”Œ๋ž˜๊ทธ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง‘๋‹ˆ๋‹ค. CONFIG_REQUIRES_U32_MUNGING ํ”Œ๋ž˜๊ทธ๊ฐ€ ์„ ์–ธ๋˜๋ฉด ๊ตฌ์กฐ์˜ ํฌ๊ธฐ๊ฐ€ ๋ณ€๊ฒฝ๋˜๊ณ  ์ถ”๊ฐ€ ํ•„๋“œ๊ฐ€ ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค. sy_arg_munge32. ์ปค๋„์ด ์–ด๋–ค ํ”Œ๋ž˜๊ทธ๋กœ ์ปดํŒŒ์ผ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ์ถ”๊ฐ€ ๊ฒ€์‚ฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ฑฐ๋‚˜ ์•Œ๋ ค์ง„ ํฌ์ธํ„ฐ์— ๋Œ€ํ•ด ํ•จ์ˆ˜ ํฌ์ธํ„ฐ๋ฅผ ํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

struct sysent {         /* system call table */
        sy_call_t       *sy_call;       /* implementing function */
#if CONFIG_REQUIRES_U32_MUNGING || (__arm__ && (__BIGGEST_ALIGNMENT__ > 4))
        sy_munge_t      *sy_arg_munge32; /* system call arguments munger for 32-bit process */
#endif
        int32_t         sy_return_type; /* system call return types */
        int16_t         sy_narg;        /* number of args */
        uint16_t        sy_arg_bytes;   /* Total size of arguments in bytes for
                                         * 32-bit system calls
                                         */
};

๋‹คํ–‰์Šค๋Ÿฝ๊ฒŒ๋„ ์ตœ์‹  ๋ฒ„์ „์˜ macOS์—์„œ Apple์€ ํ”„๋กœ์„ธ์Šค ์ž‘์—…์„ ์œ„ํ•œ ์ƒˆ๋กœ์šด API๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. Endpoint Security API๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋‹ค๋ฅธ ํ”„๋กœ์„ธ์Šค์— ๋Œ€ํ•œ ๋งŽ์€ ์š”์ฒญ์„ ์Šน์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์œ„์—์„œ ์–ธ๊ธ‰ํ•œ API๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด SIGKILL ์‹ ํ˜ธ๋ฅผ ํฌํ•จํ•˜์—ฌ ํ”„๋กœ์„ธ์Šค์— ๋Œ€ํ•œ ๋ชจ๋“  ์‹ ํ˜ธ๋ฅผ ์ฐจ๋‹จํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

#include <bsm/libbsm.h>
#include <EndpointSecurity/EndpointSecurity.h>
#include <unistd.h>

int main(int argc, const char * argv[]) {
    es_client_t* cli = nullptr;
    {
        auto res = es_new_client(&cli, ^(es_client_t * client, const es_message_t * message) {
            switch (message->event_type) {
                case ES_EVENT_TYPE_AUTH_SIGNAL:
                {
                    auto& msg = message->event.signal;
                    auto target = msg.target;
                    auto& token = target->audit_token;
                    auto pid = audit_token_to_pid(token);
                    printf("signal '%d' sent to pid '%d'n", msg.sig, pid);
                    es_respond_auth_result(client, message, pid == getpid() ? ES_AUTH_RESULT_DENY : ES_AUTH_RESULT_ALLOW, false);
                }
                    break;
                default:
                    break;
            }
        });
    }

    {
        es_event_type_t evs[] = { ES_EVENT_TYPE_AUTH_SIGNAL };
        es_subscribe(cli, evs, sizeof(evs) / sizeof(*evs));
    }

    printf("%dn", getpid());
    sleep(60); // could be replaced with other waiting primitive

    es_unsubscribe_all(cli);
    es_delete_client(cli);

    return 0;
}

๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์‹ ํ˜ธ ๋ณดํ˜ธ ๋ฐฉ๋ฒ•(์ •์ฑ… proc_check_signal)์„ ์ œ๊ณตํ•˜๋Š” MAC ์ •์ฑ…์„ ์ปค๋„์— ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ์ง€๋งŒ API๋Š” ๊ณต์‹์ ์œผ๋กœ ์ง€์›๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ปค๋„ ํ™•์žฅ ๋ณดํ˜ธ

์‹œ์Šคํ…œ์˜ ํ”„๋กœ์„ธ์Šค๋ฅผ ๋ณดํ˜ธํ•˜๋Š” ๊ฒƒ ์™ธ์—๋„ ์ปค๋„ ํ™•์žฅ ์ž์ฒด(kext)๋ฅผ ๋ณดํ˜ธํ•˜๋Š” ๊ฒƒ๋„ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. macOS๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ IOKit ์žฅ์น˜ ๋“œ๋ผ์ด๋ฒ„๋ฅผ ์‰ฝ๊ฒŒ ๊ฐœ๋ฐœํ•  ์ˆ˜ ์žˆ๋Š” ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. IOKit์€ ์žฅ์น˜ ์ž‘์—…์„ ์œ„ํ•œ ๋„๊ตฌ๋ฅผ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ ์™ธ์—๋„ C++ ํด๋ž˜์Šค ์ธ์Šคํ„ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋“œ๋ผ์ด๋ฒ„๋ฅผ ์Šคํƒํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž ๊ณต๊ฐ„์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ํด๋ž˜์Šค์˜ ๋“ฑ๋ก๋œ ์ธ์Šคํ„ด์Šค๋ฅผ "์ฐพ์•„" ์ปค๋„-์‚ฌ์šฉ์ž ๊ณต๊ฐ„ ๊ด€๊ณ„๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์‹œ์Šคํ…œ์˜ ํด๋ž˜์Šค ์ธ์Šคํ„ด์Šค ์ˆ˜๋ฅผ ๊ฐ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ioclasscount ์œ ํ‹ธ๋ฆฌํ‹ฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

my_kext_ioservice = 1
my_kext_iouserclient = 1

๋“œ๋ผ์ด๋ฒ„ ์Šคํƒ์— ๋“ฑ๋กํ•˜๋ ค๋Š” ๋ชจ๋“  ์ปค๋„ ํ™•์žฅ์€ IOService์—์„œ ์ƒ์†๋˜๋Š” ํด๋ž˜์Šค(์˜ˆ: my_kext_ioservice)๋ฅผ ์„ ์–ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์—ฐ๊ฒฐํ•˜๋ฉด IOUserClient์—์„œ ์ƒ์†๋˜๋Š” ํด๋ž˜์Šค์˜ ์ƒˆ ์ธ์Šคํ„ด์Šค๊ฐ€ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค(์˜ˆ: my_kext_iouserclient).

์‹œ์Šคํ…œ์—์„œ ๋“œ๋ผ์ด๋ฒ„๋ฅผ ์–ธ๋กœ๋“œํ•˜๋ ค๊ณ  ํ•  ๋•Œ(kextunload ๋ช…๋ น) ๊ฐ€์ƒ ํ•จ์ˆ˜ "boolitter(IOOptionBits ์˜ต์…˜)"์ด ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. kextunload๋ฅผ ๋น„ํ™œ์„ฑํ™”ํ•˜๊ธฐ ์œ„ํ•ด ์–ธ๋กœ๋“œ๋ฅผ ์‹œ๋„ํ•  ๋•Œ ์ข…๋ฃŒ ํ˜ธ์ถœ์—์„œ false๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒƒ์œผ๋กœ ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค.

bool Kext::terminate(IOOptionBits options)
{

  if (!IsUnloadAllowed)
  {
    // Unload is not allowed, returning false
    return false;
  }

  return super::terminate(options);
}

IsUnloadAllowed ํ”Œ๋ž˜๊ทธ๋Š” ๋กœ๋“œ ์‹œ IOUserClient์— ์˜ํ•ด ์„ค์ •๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์šด๋กœ๋“œ ์ œํ•œ์ด ์žˆ๋Š” ๊ฒฝ์šฐ kextunload ๋ช…๋ น์€ ๋‹ค์Œ ์ถœ๋ ฅ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

admin@admins-Mac drivermanager % sudo kextunload ./test.kext
Password:
(kernel) Can't remove kext my.kext.test; services failed to terminate - 0xe00002c7.
Failed to unload my.kext.test - (iokit/common) unsupported function.

IOUserClient์— ๋Œ€ํ•ด์„œ๋„ ์œ ์‚ฌํ•œ ๋ณดํ˜ธ๋ฅผ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํด๋ž˜์Šค ์ธ์Šคํ„ด์Šค๋Š” IOKitLib ์‚ฌ์šฉ์ž ๊ณต๊ฐ„ ํ•จ์ˆ˜ "IOCatalogueTerminate(mach_port_t, uint32_t flag, io_name_t ์„ค๋ช…);"์„ ์‚ฌ์šฉํ•˜์—ฌ ์–ธ๋กœ๋“œ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž ๊ณต๊ฐ„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด "์ฃฝ์„" ๋•Œ๊นŒ์ง€, ์ฆ‰ "clientDied" ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋˜์ง€ ์•Š์„ ๋•Œ๊นŒ์ง€ "terminate" ๋ช…๋ น์„ ํ˜ธ์ถœํ•˜๋ฉด false๋ฅผ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํŒŒ์ผ ๋ณดํ˜ธ

ํŒŒ์ผ์„ ๋ณดํ˜ธํ•˜๋ ค๋ฉด ํŒŒ์ผ์— ๋Œ€ํ•œ ์•ก์„ธ์Šค๋ฅผ ์ œํ•œํ•  ์ˆ˜ ์žˆ๋Š” Kauth API๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์œผ๋กœ ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค. Apple์€ ๋ฒ”์œ„ ๋‚ด ๋‹ค์–‘ํ•œ โ€‹โ€‹์ด๋ฒคํŠธ์— ๋Œ€ํ•œ ์•Œ๋ฆผ์„ ๊ฐœ๋ฐœ์ž์—๊ฒŒ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์šฐ๋ฆฌ์—๊ฒŒ๋Š” KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA ๋ฐ KAUTH_VNODE_DELETE_CHILD ์ž‘์—…์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ํŒŒ์ผ์— ๋Œ€ํ•œ ์•ก์„ธ์Šค๋ฅผ ์ œํ•œํ•˜๋Š” ๊ฐ€์žฅ ์‰ฌ์šด ๋ฐฉ๋ฒ•์€ ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. "vn_getpath" API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํŒŒ์ผ ๊ฒฝ๋กœ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ๊ฒฝ๋กœ ์ ‘๋‘์–ด๋ฅผ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค. ํŒŒ์ผ ํด๋” ๊ฒฝ๋กœ ์ด๋ฆ„ ๋ณ€๊ฒฝ์„ ์ตœ์ ํ™”ํ•˜๊ธฐ ์œ„ํ•ด ์‹œ์Šคํ…œ์€ ๊ฐ ํŒŒ์ผ์— ๋Œ€ํ•œ ์•ก์„ธ์Šค๋ฅผ ์Šน์ธํ•˜์ง€ ์•Š๊ณ  ์ด๋ฆ„์ด ๋ณ€๊ฒฝ๋œ ํด๋” ์ž์ฒด์—๋งŒ ์•ก์„ธ์Šค๋ฅผ ์Šน์ธํ•ฉ๋‹ˆ๋‹ค. ์ƒ์œ„ ๊ฒฝ๋กœ๋ฅผ ๋น„๊ตํ•˜๊ณ  ์ด์— ๋Œ€ํ•ด KAUTH_VNODE_DELETE๋ฅผ ์ œํ•œํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

macOS์—์„œ ํ”„๋กœ์„ธ์Šค ๋ฐ ์ปค๋„ ํ™•์žฅ์„ ๋ณดํ˜ธํ•˜๋Š” ๋ฐฉ๋ฒ•

์ด ์ ‘๊ทผ ๋ฐฉ์‹์˜ ๋‹จ์ ์€ ์ ‘๋‘์‚ฌ ์ˆ˜๊ฐ€ ์ฆ๊ฐ€ํ•จ์— ๋”ฐ๋ผ ์„ฑ๋Šฅ์ด ์ €ํ•˜๋  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋น„๊ต๊ฐ€ O(prefix*length)์™€ ๋™์ผํ•˜์ง€ ์•Š์€์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด(์—ฌ๊ธฐ์„œ prefix๋Š” ์ ‘๋‘์‚ฌ์˜ ์ˆ˜, length๋Š” ๋ฌธ์ž์—ด์˜ ๊ธธ์ด) ์ ‘๋‘์‚ฌ๋กœ ๊ตฌ์ถ•๋œ ๊ฒฐ์ •๋ก ์  ์œ ํ•œ ์ž๋™ ์žฅ์น˜(DFA)๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฃผ์–ด์ง„ ์ ‘๋‘์‚ฌ ์„ธํŠธ์— ๋Œ€ํ•ด DFA๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๊ณ ๋ คํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ๊ฐ ์ ‘๋‘์‚ฌ์˜ ์‹œ์ž‘ ๋ถ€๋ถ„์—์„œ ์ปค์„œ๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ์ปค์„œ๊ฐ€ ๋™์ผํ•œ ๋ฌธ์ž๋ฅผ ๊ฐ€๋ฆฌํ‚ค๋Š” ๊ฒฝ์šฐ ๊ฐ ์ปค์„œ๋ฅผ ํ•œ ๋ฌธ์ž์”ฉ ๋Š˜๋ฆฌ๊ณ  ๋™์ผํ•œ ์ค„์˜ ๊ธธ์ด๊ฐ€ XNUMX๋งŒํผ ๊ธธ์–ด์ง„๋‹ค๋Š” ์ ์„ ๊ธฐ์–ตํ•˜์„ธ์š”. ์„œ๋กœ ๋‹ค๋ฅธ ๊ธฐํ˜ธ๋ฅผ ๊ฐ€์ง„ ๋‘ ๊ฐœ์˜ ์ปค์„œ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ ์ปค์„œ๊ฐ€ ๊ฐ€๋ฆฌํ‚ค๋Š” ๊ธฐํ˜ธ์— ๋”ฐ๋ผ ์ปค์„œ๋ฅผ ๊ทธ๋ฃน์œผ๋กœ ๋‚˜๋ˆ„๊ณ  ๊ฐ ๊ทธ๋ฃน์— ๋Œ€ํ•ด ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ๋ฐ˜๋ณตํ•ฉ๋‹ˆ๋‹ค.

์ฒซ ๋ฒˆ์งธ ๊ฒฝ์šฐ(์ปค์„œ ์•„๋ž˜์˜ ๋ชจ๋“  ๋ฌธ์ž๊ฐ€ ๋™์ผํ•จ) ๋™์ผํ•œ ํ–‰์„ ๋”ฐ๋ผ ์ „ํ™˜์ด ํ•˜๋‚˜๋งŒ ์žˆ๋Š” DFA ์ƒํƒœ๋ฅผ ์–ป์Šต๋‹ˆ๋‹ค. ๋‘ ๋ฒˆ์งธ ๊ฒฝ์šฐ์—๋Š” ํ•จ์ˆ˜๋ฅผ ์žฌ๊ท€์ ์œผ๋กœ ํ˜ธ์ถœํ•˜์—ฌ ์–ป์€ ํ›„์† ์ƒํƒœ๋กœ์˜ ํฌ๊ธฐ 256(๋ฌธ์ž ์ˆ˜ ๋ฐ ์ตœ๋Œ€ ๊ทธ๋ฃน ์ˆ˜)์˜ ์ „ํ™˜ ํ…Œ์ด๋ธ”์„ ์–ป์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ์ ‘๋‘์‚ฌ ์„ธํŠธ("/foo/bar/tmp/", "/var/db/foo/", "/foo/bar/aba/", "foo/bar/aac/")์— ๋Œ€ํ•ด ๋‹ค์Œ์„ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. DFA. ๊ทธ๋ฆผ์—๋Š” ๋‹ค๋ฅธ ์ƒํƒœ๋กœ ์ด์–ด์ง€๋Š” ์ „ํ™˜๋งŒ ํ‘œ์‹œ๋˜๋ฉฐ ๋‹ค๋ฅธ ์ „ํ™˜์€ ์ตœ์ข… ์ƒํƒœ๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค.

macOS์—์„œ ํ”„๋กœ์„ธ์Šค ๋ฐ ์ปค๋„ ํ™•์žฅ์„ ๋ณดํ˜ธํ•˜๋Š” ๋ฐฉ๋ฒ•

DKA ์ฃผ๋ฅผ ํ†ต๊ณผํ•  ๋•Œ 3๊ฐ€์ง€ ๊ฒฝ์šฐ๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  1. ์ตœ์ข… ์ƒํƒœ์— ๋„๋‹ฌํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ฒฝ๋กœ๊ฐ€ ๋ณดํ˜ธ๋˜๊ณ  KAUTH_VNODE_DELETE, KAUTH_VNODE_WRITE_DATA ๋ฐ KAUTH_VNODE_DELETE_CHILD ์ž‘์—…์ด ์ œํ•œ๋ฉ๋‹ˆ๋‹ค.
  2. ์ตœ์ข… ์ƒํƒœ์— ๋„๋‹ฌํ•˜์ง€ ์•Š์•˜์ง€๋งŒ ๊ฒฝ๋กœ๊ฐ€ "์ข…๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค"(๋„ ์ข…๊ฒฐ์ž์— ๋„๋‹ฌํ•จ). ๊ฒฝ๋กœ๋Š” ์ƒ์œ„ ๊ฒฝ๋กœ์ด๋ฏ€๋กœ KAUTH_VNODE_DELETE๋ฅผ ์ œํ•œํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. vnode๊ฐ€ ํด๋”์ธ ๊ฒฝ์šฐ ๋์— '/'๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด "/foor/bar/t" ํŒŒ์ผ๋กœ ์ œํ•œ๋  ์ˆ˜ ์žˆ๋Š”๋ฐ ์ด๋Š” ์ž˜๋ชป๋œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.
  3. ์ตœ์ข… ์ƒํƒœ์— ๋„๋‹ฌํ•˜์ง€ ์•Š์•˜์œผ๋ฉฐ ๊ฒฝ๋กœ๊ฐ€ ๋๋‚˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ด ์ ‘๋‘์‚ฌ์™€ ์ผ์น˜ํ•˜๋Š” ์ ‘๋‘์‚ฌ๊ฐ€ ์—†์œผ๋ฏ€๋กœ ์ œํ•œ ์‚ฌํ•ญ์ด ์ ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๊ฒฐ๋ก 

๊ฐœ๋ฐœ๋˜๋Š” ๋ณด์•ˆ ์†”๋ฃจ์…˜์˜ ๋ชฉํ‘œ๋Š” ์‚ฌ์šฉ์ž์™€ ๋ฐ์ดํ„ฐ์˜ ๋ณด์•ˆ ์ˆ˜์ค€์„ ๋†’์ด๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ํ•œํŽธ์œผ๋กœ ์ด ๋ชฉํ‘œ๋Š” ์šด์˜ ์ฒด์ œ ์ž์ฒด๊ฐ€ "์•ฝํ•œ" ์ทจ์•ฝ์ ์„ ํ•ด๊ฒฐํ•˜๋Š” Acronis ์†Œํ”„ํŠธ์›จ์–ด ์ œํ’ˆ์˜ ๊ฐœ๋ฐœ์„ ํ†ตํ•ด ๋‹ฌ์„ฑ๋ฉ๋‹ˆ๋‹ค. ๋ฐ˜๋ฉด, OS ์ธก๋ฉด์—์„œ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ๋Š” ๋ณด์•ˆ ์ธก๋ฉด์˜ ๊ฐ•ํ™”๋ฅผ ์†Œํ™€ํžˆ ํ•ด์„œ๋Š” ์•ˆ ๋ฉ๋‹ˆ๋‹ค. ํŠนํžˆ ์ด๋Ÿฌํ•œ ์ทจ์•ฝ์ ์„ ํ•ด๊ฒฐํ•˜๋ฉด ์ œํ’ˆ์œผ๋กœ์„œ์˜ ์•ˆ์ •์„ฑ์ด ๋†’์•„์ง€๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์ด ์ทจ์•ฝ์ ์€ Apple ์ œํ’ˆ ๋ณด์•ˆ ํŒ€์— ๋ณด๊ณ ๋˜์—ˆ์œผ๋ฉฐ macOS 10.14.5(https://support.apple.com/en-gb/HT210119)์—์„œ ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

macOS์—์„œ ํ”„๋กœ์„ธ์Šค ๋ฐ ์ปค๋„ ํ™•์žฅ์„ ๋ณดํ˜ธํ•˜๋Š” ๋ฐฉ๋ฒ•

์ด ๋ชจ๋“  ์ž‘์—…์€ ์œ ํ‹ธ๋ฆฌํ‹ฐ๊ฐ€ ์ปค๋„์— ๊ณต์‹์ ์œผ๋กœ ์„ค์น˜๋œ ๊ฒฝ์šฐ์—๋งŒ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฆ‰, ์™ธ๋ถ€ ๋ฐ ์›์น˜ ์•Š๋Š” ์†Œํ”„ํŠธ์›จ์–ด์— ๋Œ€ํ•œ ํ—ˆ์ ์ด ์—†์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๋ณด์‹œ๋‹ค์‹œํ”ผ ๋ฐ”์ด๋Ÿฌ์Šค ๋ฐฑ์‹  ๋ฐ ๋ฐฑ์—… ์‹œ์Šคํ…œ๊ณผ ๊ฐ™์€ ํ•ฉ๋ฒ•์ ์ธ ํ”„๋กœ๊ทธ๋žจ์„ ๋ณดํ˜ธํ•˜๋Š” ๊ฒƒ์—๋„ ์ž‘์—…์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ด์ œ ์ƒˆ๋กœ์šด macOS์šฉ Acronis ์ œํ’ˆ์—๋Š” ์‹œ์Šคํ…œ ์–ธ๋กœ๋“œ์— ๋Œ€ํ•œ ์ถ”๊ฐ€ ๋ณดํ˜ธ ๊ธฐ๋Šฅ์ด ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค.

์ถœ์ฒ˜ : habr.com

์ฝ”๋ฉ˜ํŠธ๋ฅผ ์ถ”๊ฐ€